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
33 changes: 33 additions & 0 deletions .dev/features/add-after-withheld-update/GRILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# GRILL — add-after-withheld-update

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

## Findings

```yaml
- type: FINDING
rule_id: 'P0'
severity: important
file: '.dev/features/add-after-withheld-update/PLAN.md:12'
problem: "`add` also merges records stamped with the config pair and refreshes `commit`; at a pending version the clone's commit differs from the recorded one. The plan must confirm that `add` writing the clone's commit does not invalidate the records stamp the withheld update left (stamp = config's old skillsVersion/commit)."
evidence: "`add`'s version gate accepts a clone at `skillsVersion` OR `pendingSkillsVersion`"
- type: FINDING
rule_id: 'P5'
severity: minor
file: '.dev/features/add-after-withheld-update/PLAN.md:33'
problem: "The `unreadable` label is not in FORCEABLE_SKIPS but is a skip label; the set test must use exactly {modified, unrecorded}, not 'forceable'."
evidence: 'whose skip labels are all in `{modified, unrecorded}`'
- type: FINDING
rule_id: 'P7'
severity: minor
file: '.dev/features/add-after-withheld-update/PLAN.md:31'
problem: "A new config field must round-trip through every writer that spreads `...config` (add/remove) and be dropped by init's fresh config; confirm no writer re-serializes a stale pending value after a complete update."
evidence: '`PharnConfig.pendingSkillsVersion?: string` (additive, P7)'
```

## Summary

The key risk is the `commit`/records-stamp interaction in `add` at a pending version — verify in build.

ADVISORY VERDICT: 3 concerns raised (0 blocking-severity, 3 advisory) — for the human to weigh before /pharn-dev-build.
75 changes: 75 additions & 0 deletions .dev/features/add-after-withheld-update/PLAN.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
# PLAN — add-after-withheld-update (PHARN-05: one kept local edit must not dead-end `pharn add`)

- spec_content_hash: bca940a5ad247c120e6d8a3acba119d0d8df51dca275964d0e54c48d729d3c4e # fix #4
- increment: when `update` withholds the version bump ONLY because of `modified` / `unrecorded` skips
(the user's own kept edits), it records the version it DID apply as an additive
`pendingSkillsVersion` in `pharn.config.json` (cleared by the next complete run); `add`'s version
gate accepts a clone at `skillsVersion` OR `pendingSkillsVersion`. When the gate still refuses, the
message names the real ways out instead of looping on "run `pharn update`".
- layer(s): the CLI itself (`src/types.ts`, `src/lib/pharn-config.ts`, `src/commands/{update,add}.ts`)
- constitution_refs: [P0, P1, P4, P5, P7]

## Discovery — verified this run (P6)

Reproduced (`repro/add-deadlock`): `init` 6.11.1 → one local edit in `.claude/commands/pharn-plan.md` →
`update --yes` to 6.17.1 twice, each "skipped 1", config stays 6.11.1 → `pharn add a11y` exit 1 "Skills
version mismatch … run `pharn update` first". Loop: `update` can never finish while the edit is kept.
Code: `update.ts` `versionWithheld = plan.counts.skipped > 0` keeps `skillsVersion`/`commit`;
`add.ts` `versionGate` compares `readSkillsVersion(clone) !== config.skillsVersion`;
`docs/commands/add.md` "`pharn update` is the only resolution".

Why restricting to `modified`/`unrecorded`: those are files the user owns and chose to keep — everything
else was upgraded, so the tree IS at the new version apart from them. `unverifiable` (no usable
records baseline → every present differing file skipped) and `unreadable` can leave most of the tree at
the old version, so they do NOT set `pendingSkillsVersion` and the gate keeps refusing.

## Files

- `src/types.ts` — `PharnConfig.pendingSkillsVersion?: string` (additive, P7) — layer CLI
- `src/lib/pharn-config.ts` — ingest: a present `pendingSkillsVersion` failing `VERSION_RE` is dropped
(like a garbage `layout`), so a hand-edit can only fail closed — layer CLI/lib
- `src/commands/update.ts` — on a withheld run whose skip labels are all in `{modified, unrecorded}`,
write `pendingSkillsVersion: installedVersion`; on a complete run, delete the field; any other
withheld run leaves it absent — layer CLI/command
- `src/commands/add.ts` — `versionGate` passes when the clone's version equals `skillsVersion` or
`pendingSkillsVersion`; its refusal names `pharn update --force` (backs up edits) and reverting the
edits as the ways out when a pending version exists but upstream moved again — layer CLI/command
- `tests/update.test.ts` — withheld-by-`modified` → `pendingSkillsVersion` set; withheld-by-
`unverifiable` → not set; complete run → cleared
- `tests/add.test.ts` — clone at pending version → add proceeds and never touches `skillsVersion`;
clone at a third version → refused with the new message
- `tests/pharn-config.test.ts` — valid pending round-trips; garbage pending dropped; absent is legal
- `docs/commands/add.md` — "Version mismatch" section: the kept-edits case now works (P4)
- `docs/reference/pharn-config.md` — document `pendingSkillsVersion` (P4)
- `CLAUDE.md` — add/update paragraphs (P4)

## Contracts satisfied

- CLAUDE.md "`add` must never stamp a newer `skillsVersion` over unchanged old bytes" — preserved: `add`
still never writes `skillsVersion`, and `pendingSkillsVersion` exists only when every non-user file
was upgraded.

## Evals to write (P1)

- per test file above; the add-at-pending and update-sets-pending cases fail on the base source.

## Guarantee audit (P0)

- "`add` installs only at a version the project's non-user-owned files are at" → floor: exact version
string membership in `{skillsVersion, pendingSkillsVersion}`, where `pendingSkillsVersion` is written
only when every skip label ∈ `{modified, unrecorded}` (enum membership).
- A hand-edited `pendingSkillsVersion` is validated by `VERSION_RE`; a well-formed forged one could let
`add` install at that version — the same trust level as a hand-edited `skillsVersion` today (advisory,
named).

## Trust audit (P2)

- No new remote input; the new config field is local, hand-editable, regex-validated at ingest.

## Determinism audit (P5)

- Enum/string membership only; terminal = the existing named refusal.

## Open questions (HALT)

- none
30 changes: 30 additions & 0 deletions .dev/features/add-after-withheld-update/REGRESSION.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# REGRESSION — add-after-withheld-update

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

## Base and partition

- **base:** `a6c55bc7400903236b1f1ee36432a9d74b15e1d0` (`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/add.md`, `docs/reference/pharn-config.md`, `src/commands/add.ts`, `src/commands/update.ts`, `src/lib/pharn-config.ts`, `src/types.ts`, `tests/add.test.ts`, `tests/pharn-config.test.ts`, `tests/update.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.
46 changes: 46 additions & 0 deletions .dev/features/add-after-withheld-update/REVIEW.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
# REVIEW — add-after-withheld-update

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

## Floor-gate findings (blocking)

None.

- **L-floor (P0):** the gate passes on exact membership in `{skillsVersion, pendingSkillsVersion}`;
`pendingSkillsVersion` is written only when every skip label ∈ `{modified, unrecorded}` (enum set) and
is `VERSION_RE`-validated at ingest. Grill concern #1 was real and is fixed in build: at a pending
version `add` keeps the config's `(skillsVersion, commit)` pair (`addStamp`), so it never stamps a
version the kept edits did not receive and the records stamp stays consistent (tested).
- **L-eval (P1):** 8 new cases fail on the base source: 3 in `update` (set by user-edit skips, not set by
`unverifiable`, cleared by a complete run), 2 in `add` (proceeds at pending without advancing the pair;
third version refused with the new message), 3 ingest cases.
- **L-trust (P2):** no new remote input; the field is local and regex-validated.
- **L-axis (P3):** type in `types.ts`, ingest in `pharn-config.ts`, writer in `update.ts`, reader in
`add.ts` — each file keeps its own axis.

## Advisory findings

```yaml
- type: FINDING
rule_id: 'P4'
severity: minor
file: 'docs/reference/pharn-config.md:15'
problem: 'The field table at the top of the reference does not list `pendingSkillsVersion`; it is described in the prose note below it.'
evidence: "| `skillsVersion` | string | The repo's `SKILLS_VERSION` at the installed commit |"
- type: FINDING
rule_id: 'P5'
severity: minor
file: 'src/commands/status.ts'
problem: '`status` still reports the install as outdated (skillsVersion is honest) without mentioning that a pending version exists; a user may not realize `add` works now.'
evidence: 'printArchetypeVersion(config, latest)'
- 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, 3 advisory findings (minor). No lesson proposed for canon.
17 changes: 17 additions & 0 deletions .dev/features/add-after-withheld-update/SHIP.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# SHIP — add-after-withheld-update

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/add-after-withheld-update/VERIFY.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# VERIFY — add-after-withheld-update

## 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.
28 changes: 28 additions & 0 deletions .dev/features/add-after-withheld-update/regression-report.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
{
"base": "a6c55bc7400903236b1f1ee36432a9d74b15e1d0",
"inside": [
"CLAUDE.md",
"docs/commands/add.md",
"docs/reference/pharn-config.md",
"src/commands/add.ts",
"src/commands/update.ts",
"src/lib/pharn-config.ts",
"src/types.ts",
"tests/add.test.ts",
"tests/pharn-config.test.ts",
"tests/update.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/add-after-withheld-update/verify-report.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
"feature": "add-after-withheld-update",
"gates": {
"format:check": 0,
"lint": 0,
"lint:md": 0,
"test": 0,
"typecheck": 0,
"validate": 0
},
"verdict": "PASS",
"failing_gates": [],
"verifiers": {
"registered": 0,
"findings": []
}
}
4 changes: 2 additions & 2 deletions .pharn/writes-scope.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"scope": [
".dev/features/hook-wiring-drift/SHIP.md"
".dev/features/add-after-withheld-update/SHIP.md"
],
"set_by": ".claude/commands/pharn-dev-ship.md",
"set_at": "2026-09-24T08:17:20.630Z"
"set_at": "2026-09-24T08:22:49.900Z"
}
Loading
Loading