Repository navigation
feat(adf): doctor, score and bootstrap read the vendor mode record (#318 3/6) - #353
Conversation
…efs #318) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
stackbilt-admin
left a comment
There was a problem hiding this comment.
CHANGES_REQUESTED
Adversarial review of #353 (#318 PR 3/6) at bf890e6, against main at 9ce61b4. I built both in temp worktrees and ran E2E fixtures with the built CLI binaries (Windows, Git Bash).
The existing-user CI story holds, and so do the bootstrap data-loss fix, the score monotonicity and migrate CP-6. Three things need fixing first. Two are in adf vendor restore, which is supposed to refuse whenever content would be lost and can silently delete content instead. The third is a doctor remedy line that points at the wrong VENDORS entry.
Required changes
1. adf vendor restore silently deletes user content in a pointer, and exits 0. In packages/cli/src/commands/adf-vendor.ts:845-860 (restoreVendorFile), the only checks on an inferred or recorded pointer are countExtractable (bloat) and retained sections. Any line that tidy wouldn't extract and that doesn't sit under a retained heading gets dropped. This breaks INV-P3-9 and the CHANGELOG/cli-reference claim "refuses (exit 1, nothing written) whenever content would be lost".
Repro: start from a clean main-bootstrapped repo, make one edit to the hybrid CLAUDE.md, then run charter adf vendor restore CLAUDE.md. Every case below printed [ok] Restored CLAUDE.md as a thin pointer, exited 0, and lost the line:
- title changed to
# Acme Payments — agent notes→ reset to# CLAUDE.md - an extra blockquote line
> Escalate prod incidents to #oncall-payments.→ deleted - text inside
## Session Startreplaced withOur CI token lives in 1Password vault Eng-Prod.→ reverted to the template - a prose line after
<!-- charter:module-index:end -->→ deleted (see item 2)
Suggested fix: after building next, refuse when any non-blank line of current is missing from next. Skip lines that belong to a known Charter template: pointerTemplatesFor(file), the generic migrate pointer, and the module-index block between its markers. The contract enum is locked, so reuse hand-authored (remedy adf migrate --source F) or pointer-bloated. Add these four repros to the INV-P3-9 sweep. Today that sweep only covers fixtures that already refuse, and has no test that a successful restore keeps every user byte.
2. ## Module Index is reported as kept, then dropped (adf-vendor.ts:774 withoutModuleIndexSection, and :858 keptSections). When the user has content under ## Module Index, divergentRetainedHeadings lists that heading, so the output says Restored CLAUDE.md as a thin pointer (kept ## Module Index). But restoredPointer strips the whole Module Index section from the retained block, so the content goes. Either carry over the non-index lines of that section, or refuse under item 1. In both cases keptSections must only list what actually survives.
3. Doctor's duplicate-entry line names the ignored entry as the winner (packages/cli/src/commands/doctor.ts:153 entryDuplicate, filled at :276-277).
Repro: put - AGENTS.md: pointer and then - AGENTS.md: unmanaged under VENDORS. The resolved mode is unmanaged (last wins; the row shows recordedMode: 'unmanaged', and agent config says "1 file(s) recorded unmanaged"). Doctor still prints "the last entry ('AGENTS.md: pointer') wins. Delete the others." p.raw is the ignored earlier entry, as PR1's vendor status wording ("Ignoring VENDORS entry '{raw}' … The last entry for {file} wins") says. A user who follows the doctor line deletes the entry that actually wins, which flips the file's mode. Reword to something like "VENDORS lists {file} more than once in {manifestPath}; '{raw}' is ignored because a later entry wins. Delete that line." Then update the pin at packages/cli/src/__tests__/adf-vendor-doctor-score.test.ts:258, which currently asserts the wrong text.
Verified, no change needed
- Existing-user CI. 11 fixtures, each run on its own copy with both binaries: no VENDORS section (thin, hybrid, compiled, unmanaged), scaffold-only
.ai/,.ai/with just a manifest, broken.ai/(garbagecore.adfplus a missing DEFAULT_LOAD module), a monorepo withpackages/{a,b}/CLAUDE.md, no.ai/, the base repo, and a repo with a recorded VENDORS section.- Exit codes for
doctor --adf-only --ci,doctor --cianddoctor --format jsonare identical to main. - The overall JSON
statusis identical. - Every other check keeps its name, status and key set.
- The only differences are the one
adf vendor modeINFO row (it never affectshasWarn) and the agent-config/bloat PASS wording and counts. Main countedagents.mdandAGENTS.mdtwice on Windows (6 files, now 5). - This repo:
doctor --adf-only --ciexits 0 on both (PR adds the INFO line).scoreis 93/A on both.
- Exit codes for
- Score only rises. In 8 of the 11 fixtures it went from 66 to 69 (or 65 to 68 for compiled); the other 3 were unchanged. None decreased. The rise comes from
.cursorrulesbecoming substantive via ai-dir.- The broken
.ai/does not crash score (exit 0, 57 on both); the compile error is caught. - Score writes nothing.
git statusafter the run matches main; a stray.charter/appears only in repos without one, on both sides, so it is pre-existing. - Compile runs at most once per pointer file whose text is not substantive (≤3 per run), not once per module.
- The broken
- bootstrap
--force.- Data-loss fix confirmed. On main a hand-authored
CLAUDE.md(with no record) is overwritten in phase 3;HANDRULEis gone with no backup. - With the PR it is skipped with the warning, phase 4 migrates it,
HANDRULElands in.ai/core.adf,.pre-adf-migrate.bakexists, andCLAUDE.md: pointeris appended. - A
GEMINI.mdrecordedcompiledsurvives (on main it is turned into a pointer and its record is flipped). - The VENDORS carry-over keeps the original entry order and goes through
recordVendorModesadd-if-absent.
- Data-loss fix confirmed. On main a hand-authored
- restore, the parts that work. Clean pointers come back byte-equal (
unchanged, nothing written).## Environmentcontent is kept. Bloat refuses. The read-only-file revert puts the manifest back byte-for-byte (including removing the VENDORS section it had just created). Absent and unrecorded files recordpointerand restore with the right GEMINI template. The recorded-pointer-plus-compiled drift remedy round-trips to doctor PASS. - The revert logic itself is sound.
- The read/compare/write is not atomic, but the TOCTOU window is CLI-scale and acceptable.
- When the record add was
unchanged,afterstays null and no revert is attempted, which is correct because nothing changed. - A recorded pointer gets no record write.
- Doctor rows. All 9 kinds were exercised through the CLI with the expected statuses. Worst-row aggregation works (a duplicate alone exits 0). JSON
rowsappear on this check only, with exactly the schema keys.- Ruling on the
driftCompileddeviation: accepted. Putting the content-preservingvendor setfirst, naming what regenerating discards, and adding--forcefor an unmanaged file matches PR2'scompile --checkline. The literal template's remedy would not have worked for a hand-authored file. - Leaving the check out when there are no vendor files is also fine.
- Ruling on the
- migrate. Bare discovery skips files recorded unmanaged (text and JSON).
--source CLAUDE.mdmigrates, upsertspointer, and doctor afterwards shows "All 5 … match" with exit 0. - Repo invariants.
- Hooks:
git diff origin/main -- .githooks packages/cli/src/commands/hook.tsis empty. - LOC:
adf_commands_loc1027/1041 (14 lines of headroom),adf_migrate_loc780/792 (12 lines). PR4–PR6 need to budget for this. A PR6 rename that touches migrate has very little room. verify:adfandtypecheckpass.pnpm testgives 1228 passed and 5 failed; main gives 1179 passed with the same 5 Windows-only failures.
- Hooks:
- Superseded PR1 tests. They now pin the new behaviour rather than being loosened: GEMINI recorded compiled is skipped and absent,
--sourceon a recorded-unmanaged alias upserts toAGENTS.md: pointer, and thegit checkoutremedies becomevendor restore. The deleted "doctor/score unchanged" INV-1 tests are replaced by the new file.
Non-blocking notes
- Hand-edited compiled file. For a recorded pointer whose file is compiled output, restore discards any hand-edit in the compiled body. I tested
HAND EDIT … call Bob before deployingand it was lost with exit 0. The contract allows this (onlyretained-divergentrefuses), and the file says DO NOT EDIT, but a cheap guard would be to refuse when the body differs from a fresh in-memorycompileAdfof that target. - Write failures are reported as
record-write.adf-vendor.ts:903uses that refusal for a failed file write, so JSON consumers cannot tell "record skipped, nothing written" from "file write failed". The enum is locked, so at least make the text distinct (it already is) and document it.restoreNotReverted(:157) says to delete the line by hand, which is the exact inverse. Consider adding that for an absent file the leftoverpointerentry will show asrecorded-absentin doctor.
- CHANGELOG overclaims phase 4.
CHANGELOG.md:15says that with--yes"a hand-authored file is then migrated into.ai/by phase 4 as it would be without--force". That only holds without a VENDORS entry.- Repro: bootstrap on main (which records
CLAUDE.md: pointer), replaceCLAUDE.mdwith hand-written rules, then runbootstrap --force --yes. The file is preserved but not migrated (phase 4: "no migratable files"). - cli-reference states the rule correctly ("Phase 4 migrates only hand-authored files with no
VENDORSentry"). Align the CHANGELOG with it. - Under
--force --yeswith no record, the phase-3 warning also says "Run 'charter adf migrate --source CLAUDE.md' … first", even though phase 4 does exactly that a moment later.
- Repro: bootstrap on main (which records
- Score wording.
score.ts:1570: "inREADME.md, or your ADF files" has a stray comma when CLAUDE.md is dropped from the list.- A scaffold-only
.ai/(fresh bootstrap, no project content) already compiles to "substantive", soscorePopulateAiDirwill rarely fire. That is consistent with compile parity; just noting it.
- Help text.
adf.ts:977dropped "adf compile and adf tidy act on it" from the vendor help line. - Monorepo coverage. The deleted PR1 test "score is unchanged, including non-unique vendor basenames" (monorepo
packages/a/CLAUDE.md) has no replacement. Add a monorepo fixture to the INV-P3-7 sweep. In my E2E it goes 66 → 69 with no regression.
Follow-up issues to file (pre-existing on main; PR3 does not make them worse)
A. bootstrap --force replaces a hand-edited manifest.adf (and core.adf/state.adf) with the template. The old files are backed up to .ai/.backup/<name>.<ts>, but the live manifest loses its customisations. Repro on main and on the PR (identical except VENDORS):
- Bootstrap a repo.
- Add
.ai/payments.adfand register it with(Triggers on: stripe, payment). - Append
, sprint-customto planning's triggers. - Add a manifest RULE
Custom manifest rule XYZ. - Add a
📊 METRICS:section (custom_loc: 10 / 100 [lines]). - Run
charter bootstrap --force --yes --skip-install --skip-doctor.
Afterwards the planning trigger is back to the template, payments is re-registered by orphan detection as (Triggers on: payments) (custom triggers lost), and the custom RULE and the whole METRICS section are gone, along with any LOC ceilings a repo relies on. Any module registered by #348's migrate loses its custom triggers the same way. Custom content in core.adf/state.adf is also replaced (backed up). PR3 carries only VENDORS over. Suggested fix: under --force, merge the manifest (keep unknown sections, ON_DEMAND entries and METRICS) instead of replacing it, or skip manifest.adf unless an explicit --reset-manifest is given.
B. A circular import makes adf migrate write the generic pointer instead of the named template. adf-migrate.ts:102 builds POINTER_TEMPLATES when the module loads. When anything loads adf.js before adf-migrate.js, every POINTER_* is still undefined at that moment. The CLI's own order does this: index.js → doctor.js → adf.js → adf-migrate.js. migrateSource then falls back to the generic "This project uses [ADF]…" pointer.
I checked load orders on both main and the PR: loading ./adf, ./bootstrap, ./doctor, ./adf-tidy or ../index first gives GENERIC, and loading ./adf-migrate first gives the proper template. Results are identical on main.
User-visible repro: put hand-written rules in GEMINI.md (or AGENTS.md), run charter adf migrate --source GEMINI.md --yes, and the file starts with > This project uses [ADF]… instead of > **DO NOT add rules or context to this file.**. Doctor and tidy still treat it as a pointer, so there is no drift WARN; the effect is lost template wording.
Fix: resolve the template at call time (for example function pointerTemplateFor(name)). PR3's restoreTemplate already does this and so is unaffected.
…ording (refs #318) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Review round 1 addressed in 1a6d900:
Tests: 1237 passed, 5 failed. The 5 failures are the known Windows-only ones. |
Refs #318
PR 3 of 6.
doctor,scoreandbootstrapnow read theVENDORSmode record from PR1 instead of guessing from markers. This PR also addscharter adf vendor restore <file>(CP-5), andadf migratenow respects a recordedunmanaged. It builds on main @ 9ce61b4 (#350, #352).Before → after
recorded-drift, which fails--ciand the pre-commit hook. The remedy (adf vendor restore) clears itrecorded-absent, naming a recreate command for its mode.ai/ai-dir(in-memory compile), no recommendation--forceturned it into a pointervendor set+vendor restorecommands--force, hand-authored file--yes, phase 4 migrates itadf migrate, recorded unmanagedadf migrate --source F, F recorded unmanagedadf compile --checkpointer remediesgit checkout -- Fcharter adf vendor restore Fadf vendor restore FOperator sign-offs (Gate 2)
--ciand the pre-commit hook. This only affects repos with aVENDORSrecord. A repo without one gets a single INFO line and no new WARN. On this repo,doctor --adf-only --cistill exits 0 (main: PASS; this PR: PASS plus one INFO).bootstrap --forceno longer overwrites hand-authored or compiled vendor files. This changes what a documented flag does. It is listed under CHANGELOG Changed, and the docs/cli-referencebootstrap --forceline and the global--forcerow are updated.adf vendor restoreships in this PR.migrate --source Fupserts pointer.rowson theadf vendor modecheck only.Data-loss fix
On main,
bootstrap --forcewrote the template over every existing pointer-file name in phase 3, before phase 4 (migrate) could read it. Vendor files are not backed up (only.ai/*.adfgo to.ai/.backup/), so a hand-authoredCLAUDE.md's rules were gone and no.pre-adf-migrate.bakwas written. Reproduced end to end: on main the rule is not in.ai/and there is no backup. With this PR the rule lands in.ai/core.adf, the backup exists, andCLAUDE.md: pointeris recorded after the migration.A related fix found while testing:
--forcealso re-scaffoldsmanifest.adf, which dropped theVENDORSrecord before the pointer and migrate phases read it. Valid entries are now carried over to the new manifest through the shared writer (add-if-absent, re-parse guard).Invariants and where they are tested
All tests below are in
packages/cli/src/__tests__/adf-vendor-doctor-score.test.tsunless another file is named.--force) and fullbootstrap --force --yesruns.--sourcemigrates it and upserts. Covered by the bootstrap test, the bare-discovery test, the CP-6 test (doctor shows no drift afterwards), and the revert-on-failure test..ai/) are scored against the text-only rule main used, and the thin-pointer fixture gains exactly +8.CLAUDE.md. This also drops "orCLAUDE.md" from two generic recommendations whenCLAUDE.mdis a pointer or compiled.adf.tsstays at 1027/1041. The vendor help line was edited in place.adf-migrate.tsis 780/792.git diff origin/main -- .githooks packages/cli/src/commands/hook.tsis empty.Superseded PR1 tests that pinned behaviour "until PR3" were updated in
adf-vendor.test.ts, andadf-vendor-consumers.test.tsnow expects the restore remedies.Deviations from the addendum (deliberate)
doctorDriftCompiledlists the content-preserving fix (vendor set F <inferred>) first, then regenerating. When regenerating would lose something, it says so (discards N item(s)/discards its hand-authored content) and adds--forcefor an unmanaged file. This mirrors PR2'scompile --checkdrift line. The literal template put regenerating first and omitted--force, so for a hand-authored file its remedy would not have worked.adf vendor modeentry when there are no vendor files (manifest present, no files, no record), to avoid an "All 0 vendor file(s)…" line.recordVendorModeswith the re-parse guard.NOT in this PR
--sourceloop (PR4). The hooks are byte-identical to main.agents.mdrename and record-key move (PR6). This PR only reports them as the INFOlegacy-spellingrow.--fix.Verification
pnpm build,pnpm run typecheckandpnpm run verify:adfpass.pnpm test: 1228 passed, 5 failed. The failures are the 5 known Windows-only ones (bootstrap bootstrap: phases 6/7 collect warnings without printing them; absoluteAiDirArg misses Windows paths on POSIX #307 fixture, context-refresh "captures git signals", serve-context "returns existing snapshot", precommit-hook "auto-tidies bloated CLAUDE.md" and "re-stages modified .adf modules").Implemented via constellation-loop against a locked blueprint.
🤖 Generated with Claude Code