Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions .dev/features/reinit-preserve-edits/GRILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# GRILL — reinit-preserve-edits

Plan: `.dev/features/reinit-preserve-edits/PLAN.md` · spec-hash `bca940a5…d729d3c4e` matches live
`ARCHITECTURE.md`. Registered grillers: `{"registered":0,"grillers":[]}` → inline axes only.

## Findings

```yaml
- type: FINDING
rule_id: 'P3'
severity: minor
file: '.dev/features/reinit-preserve-edits/PLAN.md:32'
problem: 'Reading the previous config and deciding manual carry-over is a new concern for init.ts; it should stay a small, named helper so the command keeps one axis (orchestration).'
evidence: 'reads the existing config tolerantly'
- type: FINDING
rule_id: 'P5'
severity: important
file: '.dev/features/reinit-preserve-edits/PLAN.md:28'
problem: "The scan in the prompt and the scan before the copy run at different times; the backup must use its OWN scan (the one immediately before the write), not the prompt's, or an edit made while the prompt was open is lost."
evidence: 'before `installCapabilities`, `scanDest` over the expected install paths'
- type: FINDING
rule_id: 'P1'
severity: minor
file: '.dev/features/reinit-preserve-edits/PLAN.md:41'
problem: 'pharn.config.json itself is overwritten by re-init; it is not in the install manifest, so it is not backed up. Say so (the manual entries are carried over instead).'
evidence: 'copies those files to `.pharn-backup/<ts>/`'
```

ADVISORY VERDICT: 3 concerns raised (0 blocking-severity, 3 advisory) — for the human to weigh before /pharn-dev-build.
73 changes: 73 additions & 0 deletions .dev/features/reinit-preserve-edits/PLAN.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
# PLAN — reinit-preserve-edits (PHARN-11: re-running `init` must back up edits and keep manual adds)

- spec_content_hash: bca940a5ad247c120e6d8a3acba119d0d8df51dca275964d0e54c48d729d3c4e # fix #4
- increment: on a re-install over an existing project, `init` (1) lists the existing files that DIFFER
from upstream first in its overwrite prompt, saying they will be backed up; (2) copies those files to
`.pharn-backup/<ts>/` (the same `scanDest` + `createBackup` `add` uses) before the first write, naming
the directory at creation; and (3) keeps every `source: 'manual'` capability of a readable existing
archetype config that is still in the fetched index — installing it and recording it as `manual` —
instead of rewriting the config from archetype resolution alone.
- layer(s): the CLI itself (`src/steps/overwrite-check.ts`, `src/steps/install-archetype.ts`, `src/commands/init.ts`)
- constitution_refs: [P0, P1, P3, P4, P5, P7]

## Discovery — verified this run (P6)

Reproduced (`repro/reinit`): `init`, `add a11y`, edit `pharn-spec.md`, `init` again, confirm → the
prompt shows 10 of 417 paths ("…and 407 more"), no `.pharn-backup/` is made, the edit is lost, and
`a11y` drops out of the config while its files stay. The CLI itself points users at `init` as the
repair (`LEGACY_CONFIG_MESSAGE`, `ConfigParseError`'s last resort). `steps/overwrite-check.ts` caps the
list at `MAX_LISTED = 10` with no edit classification; `steps/install-archetype.ts` builds the config from
scratch, every entry `source: 'auto'`. `lib/dest-drift.ts` `scanDest` + `lib/backup.ts` `createBackup` are
the existing primitives `add` already uses for exactly this.

## Files

- `src/steps/overwrite-check.ts` — `confirmWriteTargets` runs `scanDest` over the conflicting install
paths: the drifted ones are listed first under a heading saying they differ from upstream and will be
backed up to `.pharn-backup/` before being overwritten; the rest follow; the cap applies to the total — layer CLI/step
- `src/steps/install-archetype.ts` — before `installCapabilities`, `scanDest` over the expected install
paths; a non-empty `drifted` (and an empty `unsafe` — a symlinked destination is refused by the install
pre-flight anyway) goes to `createBackup`, logged at creation; the config marks the capabilities named in
a new `manualKeys` argument `source: 'manual'` — layer CLI/step
- `src/commands/init.ts` — reads the existing config tolerantly (absent / unreadable / invalid / legacy →
none; `init` is the recovery path and must never be blocked by the file it replaces); its
`source: 'manual'` entries still present in the fetched index and not already selected are added to the
install selection (named in one info line) and passed as `manualKeys` — layer CLI/command
- `tests/overwrite-check.test.ts` — drifted paths listed first with the backup note; identical files are
not called edits
- `tests/init-archetype.test.ts` — re-init backs up an edited file (bytes preserved under
`.pharn-backup/`, pointer printed) and keeps a manual capability (installed, `source: 'manual'`); a
corrupt existing config does not block re-init; a manual entry gone from the index is not resurrected
- `tests/init.test.ts` — the `runInstallArchetype` call gains its `manualKeys` argument
- `docs/commands/init.md` — re-install behavior (P4)
- `CLAUDE.md` — init step 5 (P4)

## Contracts satisfied

- The product's three edit protections (init's prompt, update's skips, `--force`'s backup) — init's is no
longer a bare "overwrite?" but names the edits and backs them up, as `add` does.
- `merge-capabilities.ts` "sticky manual" semantics — now honoured by `init` too.

## Evals to write (P1)

- listed above; backup, manual-keep and edit-first listing fail on the base source.

## Guarantee audit (P0)

- "an edited file overwritten by re-init is first copied to `.pharn-backup/`" → floor: sha256 inequality
(scanDest) + `createBackup` before `installCapabilities` (same primitives as `add`).
- "a manual capability survives re-init" → set membership over the existing config and the fetched index.
- Residual (named): `init` still overwrites after confirmation — the protection is the backup, not a skip.

## Trust audit (P2)

- The existing config is local, hand-editable input read through `readPharnConfig` (validated); failures
degrade to "no previous config", never to trusting unvalidated entries.

## Determinism audit (P5)

- Hash inequality, key membership; a failed read of the previous config → the documented fresh-install path.

## Open questions (HALT)

- none
30 changes: 30 additions & 0 deletions .dev/features/reinit-preserve-edits/REGRESSION.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# REGRESSION — reinit-preserve-edits

The verdict below is computed by `.dev/floor/check-regress.mjs`, not by this stage's judgment.

## Base and partition

- **base:** `770adad9a9213f43ced51c8e5b7ec3f22a2598e2` (`origin/main` at build time; the build is an uncommitted working tree on top of it).
- **inside** (each declared in `PLAN.md` `## Files`): `CLAUDE.md`, `docs/commands/init.md`, `src/commands/init.ts`, `src/steps/install-archetype.ts`, `src/steps/overwrite-check.ts`, `tests/init-archetype.test.ts`, `tests/init.test.ts`, `tests/overwrite-check.test.ts`.
- **scope partition:** `check-regress.mjs scope` exited **0**, `escaped: []`. `.pharn/` (hook scratch) and this
feature's own stage artifacts are not build output.
- **outside gates:** the stdlib `*.test.mjs` / `*.test.cjs` files + whole-repo `validate`; 0 committed eval pairs.
- **style-gate skip:** `inside` touches no shared style config, so `lint` / `format:check` / `lint:md` are absent from both maps.

## Per-gate exit codes

| gate | base | head | flipped? |
| ---------- | ---- | ---- | -------- |
| `tests` | 0 | 0 | no |
| `validate` | 0 | 0 | no |

- `regressions[]`: **empty**
- `pre_existing[]`: **empty**

## Verdict

**REGRESSIONS: none — no deterministically-detectable breakage outside the feature.**
(`regression-report.json` `.verdict` = `no-regressions`.)

Residual (P0/P7): this catches exactly what its suite catches. The vitest suite exercising `src/**` is
owned by `/pharn-dev-build`'s floor and `/pharn-dev-verify`. This certifies the comparison, never the increment.
40 changes: 40 additions & 0 deletions .dev/features/reinit-preserve-edits/REVIEW.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# REVIEW — reinit-preserve-edits

Floor first: `node .dev/floor/validate.mjs .` → exit 0 (GREEN). Everything below is **advisory**.

## Floor-gate findings (blocking)

None.

- **L-floor (P0):** "an edited file is copied before re-init overwrites it" reduces to sha256 inequality
(`scanDest`) + `createBackup` before `installCapabilities` — the backup takes its OWN scan right before
the copy (grill #2), not the prompt's. "A manual capability survives re-init" reduces to key membership
over the previous config and the fetched index.
- **L-eval (P1):** 4 cases fail on the base source (edited-first prompt, backup + pointer, manual
recorded, manual carried by the command); controls: no backup when nothing was edited, no "(edited)"
when byte-identical, corrupt previous config does not block init. Coverage 97.14% (gate 97%).
- **L-trust (P2):** the previous config is read through `readPharnConfig` (validated); any failure
degrades to "nothing carried over".
- **L-axis (P3):** the carry-over is one named helper in `init.ts`; the backup lives in the install step
it protects; the prompt only reorders and annotates.

## Advisory findings

```yaml
- type: FINDING
rule_id: 'P7'
severity: minor
file: 'src/steps/install-archetype.ts'
problem: "pharn.config.json itself is not in the install manifest, so it is not backed up; its manual entries are carried over instead, but a hand-edited models/seam block is still reset to defaults by re-init (pre-existing, and named in ConfigParseError's message)."
evidence: 'models: DEFAULT_MODEL_ROUTING,'
- type: FINDING
rule_id: 'P4'
severity: minor
file: 'CHANGELOG.md:8'
problem: "No CHANGELOG `[Unreleased]` entry (not in the plan's `## Files`)."
evidence: '## [Unreleased]'
```

## Verdict

**GREEN** — 0 floor-gate findings, 2 advisory findings. No lesson proposed for canon.
17 changes: 17 additions & 0 deletions .dev/features/reinit-preserve-edits/SHIP.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# SHIP — reinit-preserve-edits

Stages run, in order: `/pharn-dev-plan` → GATE 1 (human: **Approve as written**) → `/pharn-dev-grill` →
`/pharn-dev-build` → `/pharn-dev-regress` → `/pharn-dev-verify` → `/pharn-dev-review` → GATE 2.

| stage | structural verdict (verbatim) |
| -------------------- | ------------------------------------------------------ |
| `/pharn-dev-build` | `node .dev/floor/validate.mjs .` exit `0` |
| `/pharn-dev-regress` | `regression-report.json` `.verdict` = `no-regressions` |
| `/pharn-dev-verify` | `verify-report.json` `.verdict` = `PASS` |

- Review: [`REVIEW.md`](REVIEW.md) · Grill (advisory): [`GRILL.md`](GRILL.md)
- Run ended at **GATE 2**. The human's standing instruction for this batch: open a PR and merge it only if
its CI checks are green.

chain ran; the named floor verdicts are as shown — this is NOT a judgment that the increment is good or
wise; that is the human's call at the post-review gate.
27 changes: 27 additions & 0 deletions .dev/features/reinit-preserve-edits/VERIFY.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# VERIFY — reinit-preserve-edits

## FLOOR layer (owns the verdict)

Gates run over the whole repo with the feature present, as a non-root user on node 22 with the session
proxy variables unset (as root with the proxy set, 5 pre-existing tests in `init.test.ts` /
`update.test.ts` fail for environmental reasons, identically at the baseline).

| gate | exit |
| -------------- | ---- |
| `format:check` | 0 |
| `lint` | 0 |
| `lint:md` | 0 |
| `test` | 0 |
| `typecheck` | 0 |
| `validate` | 0 |

No `structural:*` gate — the increment ships no eval-actual pair.

**VERDICT: PASS** (`.dev/floor/check-verify.mjs`, `failing_gates: []`).

## ADVISORY layer

`node .dev/floor/count-verifiers.mjs .` → `{"registered":0,"verifiers":[]}` — floor gates only.

Residual (P0/P7): "verified" means the named gates passed — not that the feature is correct in any sense
the suite does not encode.
26 changes: 26 additions & 0 deletions .dev/features/reinit-preserve-edits/regression-report.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
{
"base": "770adad9a9213f43ced51c8e5b7ec3f22a2598e2",
"inside": [
"CLAUDE.md",
"docs/commands/init.md",
"src/commands/init.ts",
"src/steps/install-archetype.ts",
"src/steps/overwrite-check.ts",
"tests/init-archetype.test.ts",
"tests/init.test.ts",
"tests/overwrite-check.test.ts"
],
"outside_gates": {
"tests": {
"base": 0,
"head": 0
},
"validate": {
"base": 0,
"head": 0
}
},
"regressions": [],
"pre_existing": [],
"verdict": "no-regressions"
}
17 changes: 17 additions & 0 deletions .dev/features/reinit-preserve-edits/verify-report.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
"feature": "reinit-preserve-edits",
"gates": {
"format:check": 0,
"lint": 0,
"lint:md": 0,
"test": 0,
"typecheck": 0,
"validate": 0
},
"verdict": "PASS",
"failing_gates": [],
"verifiers": {
"registered": 0,
"findings": []
}
}
6 changes: 3 additions & 3 deletions .pharn/writes-scope.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"scope": [
".dev/features/interrupt-exit-code/REVIEW.md"
".dev/features/reinit-preserve-edits/SHIP.md"
],
"set_by": ".claude/commands/pharn-dev-review.md",
"set_at": "2026-09-24T08:58:56.214Z"
"set_by": ".claude/commands/pharn-dev-ship.md",
"set_at": "2026-09-24T09:28:29.312Z"
}
2 changes: 1 addition & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ ESM-only (`"type": "module"`, NodeNext). **Relative imports must use `.js` exten
2. `detectArchetypesFromProject` (`lib/detect-archetype.ts`) — merges `package.json` dependency names + a bounded, symlink-safe file-tree walk into an `Archetype[]` (`ssr`/`backend`/`spa`/`lib`).
3. `fetchRepo` (`lib/repo.ts`) — ONE SHA resolve, then download pharn-oss's tarball from codeload and extract it with `lib/tar-extract.ts` into a temp dir (cleaned up in a `finally`; every `process.exit`/`cancelAndExit` happens AFTER it). No fetch dependency, no `git` binary, no cache.
4. `parseCapabilityIndex` (`lib/capability-index.ts`) + `resolveCapabilities` (`lib/resolve-capabilities.ts`) — select capabilities whose `applies` is `universal` or intersects the detected archetypes; skip the rest with a reason.
5. `runArchetypeSummary` (selected + skipped) → `install` / `cancel`; on `install`, `confirmWriteTargets` (`steps/overwrite-check.ts`) warns if any of the install's actual write targets already exist in the cwd — listing them (capped), default No, silent when none; it subsumes the old `confirmOverwriteIfExists` (its set includes `pharn.config.json`) and derives the target set from `lib/install-manifest.ts` (the shared install manifest, also used by `status`'s `diff.ts`). Then `runInstallArchetype` (`steps/install-archetype.ts` → `lib/install-capabilities.ts`) copies the capabilities + fixed product surfaces (including the layout-resolved product-loop boundary contract (`resolveFeaturesReadme`)) into the mirrored layout (flat OR `pharn/`) and writes the archetype `pharn.config.json` (`archetypes`, `capabilities`, `layout`, `skillsVersion` read from the fetched `SKILLS_VERSION` file via `readSkillsVersion`, `modules: []`, plus `models`/`seam` defaults; canonical `CONSTITUTION.md` copied verbatim). The `--archetype` CLI flag is a retained no-op alias for one release.
5. `runArchetypeSummary` (selected + skipped) → `install` / `cancel`; on `install`, `confirmWriteTargets` (`steps/overwrite-check.ts`) warns if any of the install's actual write targets already exist in the cwd — listing them (capped), default No, silent when none; it subsumes the old `confirmOverwriteIfExists` (its set includes `pharn.config.json`) and derives the target set from `lib/install-manifest.ts` (the shared install manifest, also used by `status`'s `diff.ts`). On a re-install it lists the files that differ from upstream (`scanDest`) first, marked `(edited)`; `runInstallArchetype` re-scans immediately before the copy and backs those up with `createBackup` (the same pair `add` uses), naming the directory at creation; and `init` reads the config it replaces TOLERANTLY (any failure → nothing carried over) to keep its `source: 'manual'` entries that the index still has — installed again and recorded `manual` via `manualKeys`. Then `runInstallArchetype` (`steps/install-archetype.ts` → `lib/install-capabilities.ts`) copies the capabilities + fixed product surfaces (including the layout-resolved product-loop boundary contract (`resolveFeaturesReadme`)) into the mirrored layout (flat OR `pharn/`) and writes the archetype `pharn.config.json` (`archetypes`, `capabilities`, `layout`, `skillsVersion` read from the fetched `SKILLS_VERSION` file via `readSkillsVersion`, `modules: []`, plus `models`/`seam` defaults; canonical `CONSTITUTION.md` copied verbatim). The `--archetype` CLI flag is a retained no-op alias for one release.

**`lib/install-capabilities.ts`** is the shared capability copy core — `installCapabilityDirs` (`add`) and `installCapabilities` (`init`); `update` applies the manifest per file instead. `installCapabilityDirs(repoDir, projectRoot, capabilities, paths?)` pre-flights **every** selected capability source (validated name via `CAPABILITY_NAME_RE` + `safeJoin` + existence + symlink rejection) before any write — no partial installs — then copies each griller/lens dir into the mirrored layout. `installCapabilities` additionally copies the fixed product surfaces: product `pharn-*` commands (excluding `pharn-dev-*`), `.cjs` hooks (excluding `*.test.cjs`), `settings.json` (**never** overwritten), the trusted docs, the layout-resolved product-loop boundary contract (`resolveFeaturesReadme`) (layout-invariant; guarded by `findSymlinkComponent`, not just the leaf `isSymlink`, since it is the one root-relative copy with an intermediate directory), `pharn-contracts/`, and `.dev/floor/` minus test files and its `test-fixtures/` subtree (a floor-RELATIVE segment match, single-sourced as `FLOOR_TEST_FIXTURES_DIR` and anchored at the floor root on both sides — an unanchored absolute-path match would prune the whole floor copy under an ancestor of that name). Copying from the untrusted clone is symlink-guarded (`isSymlink` reject / `noSymlinks` filter) and `safeJoin`-contained; file contents are copied verbatim, never executed. The DESTINATION is guarded too: before the first write, `installCapabilities` walks every path the install manifest (`collectExpectedInstallPaths`) says it writes, plus `.claude/settings.json`, with `findSymlinkComponent` and refuses the whole install naming each symlinked component (`cpSync` follows a symlinked `.claude/commands` or `pharn/` out of the project, and `safeJoin` is lexical). The install set is resolved by `lib/capability-index.ts` (`parseCapabilityIndex` — the untrusted-frontmatter → typed `CapabilityIndex` fetch boundary, reading only `name`/`role`/`applies` via a strict field reader) + `lib/resolve-capabilities.ts` (select where `applies` is `universal` or intersects the detected archetypes). The commit SHA is threaded from `fetchRepo` (`repo.sha`) — no separate GitHub fetch (closes the resolve/fetch TOCTOU).

Expand Down
Loading
Loading