Skip to content

fix(init): a re-run init keeps what update would keep, and re-checks the config under its lock - #216

Merged
PrzemekGalarowicz merged 2 commits into
mainfrom
claude/optimistic-heisenberg-8rw8ke
Sep 24, 2026
Merged

PrzemekGalarowicz merged 2 commits into
mainfrom
claude/optimistic-heisenberg-8rw8ke

Conversation

@PrzemekGalarowicz

Copy link
Copy Markdown
Contributor

What this changes

A re-run pharn init carries over hand-added capabilities (PHARN-11, #205), but it used a narrower rule than pharn update's merge table (lib/merge-capabilities.ts). It also broke PHARN-03's lock contract (#197). The review of the last 18 commits reproduced all of these:

  • Manual became auto. A source: "manual" entry that the archetypes also selected was re-recorded as auto. A later archetype change then let update report "REMOVED — no longer selected" and drop a capability the user asked for by name. update keeps it manual (merge row 3).
  • Unreadable capabilities were dropped. An entry that upstream still ships, but this CLI can't parse, was removed from the config and its files were orphaned. update keeps it (merge row 0, kept-frozen).
  • Silent drops. A manual entry that upstream no longer ships was dropped without saying so. update names it (dropped-gone).
  • Lost concurrent writes. init reads the config before its prompts and takes the lock only after them, but never re-checked the config. A concurrent pharn add during the confirm prompt was overwritten.

Now:

  • src/commands/init.ts:
    • One tolerant read of the previous config. The fingerprint is taken first (absent / unreadable:<code> / sha256 of the bytes), then the config is parsed.
    • A pure carryOver sorts previous entries by update's rules:
      • manual: stays manual, sticky;
      • kept: can't be parsed upstream, any source, written back verbatim;
      • extra: manual but not selected by the archetypes, installed again;
      • gone: no longer shipped upstream, dropped and named.
    • Inside withProjectLock, assertConfigFingerprintUnchanged runs before any write (including the backup). If the config changed, init refuses with exit 1.
  • src/lib/pharn-config.ts: adds configFingerprint and assertConfigFingerprintUnchanged, reusing the existing ProjectChangedError message.
  • src/steps/install-archetype.ts: an InstallCarry argument replaces the bare manualKeys. Kept entries are written verbatim and listed in frozenCapabilities. Their records are carried over from a stamp-valid store; an absent or stale store is never minted from and never trusted.
  • src/steps/archetype-summary.ts + src/types.ts: carried entries show as "added by hand". They no longer also appear under SKIPPED.
  • Docs: docs/commands/init.md and docs/reference/pharn-config.md no longer say a re-run init resets every entry to auto. CHANGELOG [Unreleased] → Fixed.

Decisions you made during this run:

  • At GATE 1: a hand-added capability that upstream can't be parsed is left alone.
  • After the grill: the same applies to automatic ones ("Yes, leave them alone too").

Second of the four fixes from the 18-commit review. #213 was the first.

Type of change

  • feat — new stack option, wizard step, or command capability
  • fix — bug fix
  • docs — docs-only change
  • chore / refactor — tooling or internal restructure, no behavior change

Area(s) touched

commands/init | steps/install-archetype, steps/archetype-summary | lib/pharn-config | types | docs

Checklist

  • Read the existing file(s) before editing; followed the ESM .js-extension import convention.
  • Updated the matching tests/*.test.ts. 8 cases in tests/init.test.ts fail against the base src/ (checked by stashing it).
  • Updated the relevant docs/ pages.
  • Security invariants preserved. Kept entries come only from the local, ingest-validated config. Upstream names in index.unknown are used only as set keys.

Quality gates

  • npm run check passes locally (format:check + lint + typecheck + test): 1486 tests.
  • npm run build succeeds.
  • npm run test:coverage passes (coverage thresholds met).

Floor workflow tests: 754/754 passed. validate.mjs: GREEN. Pharn-dev verdicts: regress no-regressions, verify PASS, review GREEN (3 minor advisory findings). I ran all gates with the CI-equivalent setup described in #213 (root with the permission-override capabilities dropped, proxy variables unset).

Notes for the reviewer

  • carryOver (src/commands/init.ts) is where init's rules match lib/merge-capabilities.ts rows 0, 3, 6 and 7. An entry with no source is deliberately not inferred here, because the merge is the only place allowed to resolve a missing source.
  • Kept records are selected at the clone's layout, the same as update. A re-init that also moves the install flat → pharn/ carries none; this is noted in keptRecords.
  • Follow-up (outside this plan's files): CLAUDE.md still describes init's carry as manual-only via manualKeys.

🤖 Generated with Claude Code

https://claude.ai/code/session_0199owRmYfskqYQVQrVP679o


Generated by Claude Code

…the config under its lock

A re-run `pharn init` carried over hand-added capabilities, but by a narrower
rule than `pharn update`'s merge table:
- a `manual` entry the archetypes ALSO selected was re-recorded `auto`, so a
  later archetype change let `update` drop a capability the user asked for
  by name (merge row 3 keeps it sticky);
- an entry upstream still ships but this CLI cannot parse was dropped from
  the config and its files orphaned, where `update` keeps it (row 0);
- a manual entry upstream no longer ships was dropped silently.

init now applies the same rules. Manual entries stay manual. Unparseable
entries of any source are written back verbatim, listed in
`frozenCapabilities`, and keep their records from a stamp-valid store.
Dropped manual entries are named. Carried entries show as "added by hand"
in the summary instead of also appearing under SKIPPED.

init also reads the config before its prompts and takes the lock after
them, but never re-checked it (PHARN-03's contract for add/update/remove),
so a concurrent `pharn add` during the confirm was lost. It now takes a
fingerprint of pharn.config.json (absent / unreadable / sha256 of the
bytes) BEFORE the tolerant parse, and refuses under the lock, writing
nothing, if it changed.

Docs: docs/reference/pharn-config.md and the init.md config table no
longer say a re-run init resets every entry to `auto`.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0199owRmYfskqYQVQrVP679o
@coderabbitai

coderabbitai Bot commented Sep 24, 2026 •

Copy link
Copy Markdown

Important

  • 🔍 Trigger review

This repository does not receive automatic reviews because it has fewer than 10 stars.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Advanced

Run ID: dbd047fb-de71-4d4e-bc67-e2aa13a56e8e


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Resolve the overlap with #215 (init keeps the config keys pharn does not
own): both helpers are kept, `keptRecords` and `readCarriedEntries`. The
three doc passages and the CHANGELOG now describe both carry-overs.

Two of #215's comments are updated. One cited the removed
carriedManualCapabilities. The other said a key edited while init's prompt
is open is carried; init now refuses first when the config changed, so the
key is still not lost. Adds one test: kept entries and user-owned keys
survive the same re-run. Regress and verify re-run on the merged tree.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0199owRmYfskqYQVQrVP679o
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants