fix(mt#4986): Stop overwriting a project's own CLAUDE.md, and say which file was left alone - #3643
Conversation
…ich file was left alone `minsky init` in a project that already had agent instructions destroyed them. Measured at `1c7a6366c`: a scratch repo with a hand-written 140-byte CLAUDE.md got back 15,085 bytes, zero original lines surviving, no warning — while its own `.claude/rules/acme-house-style.md` was left byte-identical. That asymmetry is the whole finding: Minsky plays nice in every per-file channel and claimed sole ownership of the two monolithic files by inheritance from the plant, where CLAUDE.md genuinely IS wholly generated. Ownership now keys on the generation banner, in one shared predicate (`compile/monolithic-ownership.ts`) with three consumers that must agree: the two writers refuse the write, `listOutputFiles` reports no outputs so `--check` cannot call a user's file stale, and target selection drops the target. Absent is not foreign — a fresh project still gets the full output — and an unreadable file fails OPEN, because the other direction silently stops maintaining a CLAUDE.md that is ours. SC3 needed an added gate, not the narrowing the spec described: `claude.md` was pushed unconditionally (`compile.ts:143`) and had no presence rule to narrow, so a literal reading would have missed the file the task is named for. `AGENTS.md`'s existing already-exists escape is narrowed the same way, and unconditionally — `existingOutputs.agentsMd` is only consulted under claude-code, which would have left a Cursor project's hand-written AGENTS.md unprotected. `src/hooks/pre-commit.ts`'s `compileCheckTargets` is a parallel implementation of the same mapping and is gated in lockstep, or a project with its own CLAUDE.md is told at every commit that it is stale with no invocation able to refresh it. Silence would reproduce the defect quietly, so both surfaces report: `init` warns before its unreachability warning (with CLAUDE.md left alone every base rule is also unreachable, and that message alone sends the operator hunting for a frontmatter problem that does not exist), and `runMinskyCompile` seeds the report from the gate — a gated-out target never runs, so its own refusal never fires. Known interim cost, recorded rather than papered over: `.claude/rules/` only accepts rules with globs AND `alwaysApply: false`, so a project whose CLAUDE.md we do not own now receives no always-apply rules at all. That is strictly better than destroying the file and it is not silent. ask#11711 owns where they go instead. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012aTeW8XXjVuEF5GykykSwq
Minsky Reviewer StatusVerdict: APPROVED — no blocking findings Commands
|
There was a problem hiding this comment.
Independent adversarial review (Chinese-wall)
Reviewer: minsky-reviewer[bot] via openai:gpt-5
Tier: 2
Solid, well-tested change that centralizes monolithic ownership and propagates gates/refusals through compile, selection, CLI/init reporting, staleness, and pre-commit. Tests cover the new behavior thoroughly, and the banner constant is de-duplicated. I found one blocking omission and a few non-blocking robustness nits: (1) BLOCKING — docs were not updated for the new user-visible behavior (foreign-file protection, banner ownership, how to hand over ownership, impact on --check/pre-commit). (2) NON-BLOCKING — pre-commit re-implements the banner-scan logic locally and can drift from the domain predicate; consider sharing the scan window and helper. (3) NON-BLOCKING — fail-open classification returns "absent" on any read error; consider distinguishing ENOENT vs. EACCES/other. (4) NON-BLOCKING — brittle target→path ternary when seeding foreign skip reasons; prefer an explicit mapping. (5) NON-BLOCKING — verify all consumers of the new skippedForeignOutputs field. Address the docs gap and consider the small nits; with that, this looks ready to merge.
Findings
- [BLOCKING] packages/domain/src/compile/compile.ts:15 — Documentation not updated for user-visible behavior change (foreign-file protection and new messages)
This PR changes user-facing behavior forminsky init/compile: it now refuses to overwrite foreignCLAUDE.md/AGENTS.md, adjusts target selection, and emits new operator-facing messages (e.g.,foreignOutputSkipReason). No docs were updated in this PR to describe the new protection semantics, how to hand ownership to Minsky, or why--checkstops flagging staleness in these cases. Per the Documentation Impact policy, this is a blocking omission. Please add or amend the relevant docs (e.g., compile target docs, onboarding/init guide, rules/template system guide) to describe: - How ownership is determined (banner),
- What happens on foreign files (never overwritten),
- How to proceed if users want Minsky to take over (move aside/re-run), and
- The effect on
compile --checkand pre-commit checks. - [NON-BLOCKING] src/hooks/pre-commit.ts:2657 — Foreign-ownership check logic is duplicated locally and can drift from the domain predicate
isForeignMonolithForCheck(around src/hooks/pre-commit.ts:2657-2690) re-implements the banner-scan logic and hardcodes a 5-line window, importing onlyGENERATION_BANNER_PATTERNS. The canonical predicate and window live inpackages/domain/src/compile/monolithic-ownership.ts(BANNER_SCAN_LINES = 5,hasGenerationBanner). If either the scan window or predicate changes there, this hook will diverge. Suggest factoring a lightweight, import-safe helper (e.g., exporthasGenerationBannerandBANNER_SCAN_LINES) or at minimum sharing the scan-window constant to avoid silent mismatch (Pattern-sweep: same duplication appears in the probe vs. pre-commit mirror by design; ensure the scan parameters stay single-sourced). - [NON-BLOCKING] packages/domain/src/compile/monolithic-ownership.ts:58 — Fail-open classification treats any read error as 'absent' — risk of misclassifying a foreign file
readMonolithicOwnership(packages/domain/src/compile/monolithic-ownership.ts:58-80) returns "absent" on any read error, not just ENOENT. While the comment explains the tradeoff, a permissions error (EACCES) or transient read failure could incorrectly mark a foreign file as writable. Consider distinguishing ENOENT vs. other errors, emitting a designed observable (e.g., a structured reason or warning) for non-ENOENT failures, or failing closed on EACCES specifically to avoid accidental overwrite attempts. - [NON-BLOCKING] packages/domain/src/compile/compile.ts:318 — Target-to-outputPath mapping is baked into a ternary and not future-proof
When seedinggateSkippedForeign, the code mapsentry.targetto an output path viaentry.target === "claude.md" ? "CLAUDE.md" : "AGENTS.md"(packages/domain/src/compile/compile.ts:318-325). This will silently mis-map if additional monolithic targets are ever added. Prefer an explicit mapping (switch/record keyed by known targets) with anassertNever/default case to force deliberate updates on new targets. - [NON-BLOCKING] packages/domain/src/compile/types.ts:60 — MinskyCompileResult shape changed — verify all renderers/consumers handle
skippedForeignOutputs
packages/domain/src/compile/types.ts:60-78addsskippedForeignOutputstoMinskyCompileResult. The CLI renderers were updated insrc/adapters/shared/commands/compile/compile-commands.tsandsrc/commands/compile/index.ts, andinit.tsthreads it into warnings. Please double-check any other consumers (e.g., API surfaces, JSON outputs, tests relying on exact shape) to avoid unrendered messages or schema mismatches. If the result is serialized externally anywhere, document the new field.
Documentation impact
- blocking-needs-update — The PR changes user-visible behavior: refusing to overwrite foreign
CLAUDE.md/AGENTS.md, new selection gates, and new operator-facing messages surfaced byinit/compile. I did not find updates underdocs/describing the banner-based ownership, the refusal semantics, how to hand over ownership, or why--check/pre-commit stops flagging staleness when the file is foreign. This invalidates existing mental models for init/compile. Please add docs in the compile/init guidance (e.g., docs/cli or rules/template-system-guide.md) detailing the new behavior and remedies.
Affected: docs/rules/template-system-guide.md
…sed on a file we cannot read Five findings, all real, all addressed. BLOCKING — docs. `docs/rules/template-system-guide.md` (which mt#4974 rewrote as the shipped-corpus guide, so it is the right home) gains a section covering how ownership is decided, what happens to a foreign file, what the operator is told, how to hand a file over to Minsky, why there is deliberately no force flag, and the effect on `compile --check` and pre-commit. It also states the interim cost plainly rather than leaving the reader to discover it: while your CLAUDE.md is your own, the base rules reach your agent through no automatic channel. NON-BLOCKING, and the most consequential of the four: `readMonolithicOwnership` folded EVERY read error into "absent", which I had justified on the grounds that the following write would fail loudly. That does not hold for the case that matters — a file can be unreadable and still writable (mode 0200), so a user's file would have been silently destroyed by the very module written to prevent it. Only ENOENT is now "absent"; anything else is "unreadable" and is never written. Verified live: a mode-200 foreign CLAUDE.md survives byte-identical where the previous revision would have replaced it. That change made the skip message assert something the run had not checked — "it does not carry the banner", for a file nobody could read. The reason is now state-aware, and `monolithicSkipIfNotOurs` is the single place pairing the read with the wording it licenses, so a caller cannot report a cause the read did not establish. NON-BLOCKING — the pre-commit copy of the banner scan is deleted and delegates to the domain predicate. My stated rationale for duplicating it was wrong on the facts: it cited the import-graph argument that justifies duplicating `readRecordedHarnessForCheck`, but that one lives in `compile/compile.ts` which pulls the whole compile service, whereas `monolithic-ownership.ts` imports `fs/promises`, a type-only module, and constants this file already loads. NON-BLOCKING — the target-to-filename ternary is an explicit `MONOLITHIC_TARGET_OUTPUTS` record; an unknown target now yields no entry rather than a message naming the wrong file. NON-BLOCKING — consumer sweep for `skippedForeignOutputs`: the two CLI renderers and `init.ts` were already updated, the MCP tool result returns the whole object so the optional field passes through additively, and the legacy `rules compile` path (`rules/operations/crud-operations.ts`) uses a different result type entirely. No consumer left unhandled. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012aTeW8XXjVuEF5GykykSwq
There was a problem hiding this comment.
Independent adversarial review (Chinese-wall)
Reviewer: minsky-reviewer[bot] via openai:gpt-5
Tier: 2
Re-verification complete. The prior BLOCKING concern was addressed: ownership is centralized in a shared predicate, both monolithic writers refuse to overwrite foreign files, target selection now gates on banner-based ownership (including for CLAUDE.md, which previously lacked a gate), and --check no longer reports user-owned files as stale. The pre-commit mirror was updated to reflect the same gating and provides clear operator messaging for both harness and foreign-file cases. Documentation was updated to describe the behavior and its interim limitations. I found no new critical defects introduced by the fix. Non-blocking notes acknowledge the improved surfacing mechanics. Overall, this lands the SC1–SC5 criteria coherently and is safe to merge.
Findings
- [NON-BLOCKING] src/hooks/pre-commit.ts:2411 — Gate message now covers both harness and foreign-file cases
The revised informational log for gated-out targets (lines 2411-2422) now distinguishes between the harness gate and the foreign-file ownership gate, with opposite remedies. This addresses ambiguity noted in prior review. No action needed; recording as verified improvement. - [NON-BLOCKING] packages/domain/src/compile/compile.ts:315 — Added probe variant returns gate report for surfaced skips
probeMinskyCompileTargetsWithGateReportsurfaces both target selections and gated-out entries so callers (includingrunMinskyCompile) can report foreign-file skips even when the writer never runs. This resolves the visibility gap. No change requested. - [NON-BLOCKING] packages/domain/src/compile/targets/claude-md.ts:130 — Staleness exclusion implemented via listOutputFiles for foreign files
listOutputFilesnow returns an empty list when the on-disk CLAUDE.md is foreign, ensuring--checkdoes not falsely report it as stale. Mirrored inagents-md.ts. Behavior matches SC4. No change requested.
Spec verification
| Criterion | Status | Evidence |
|---|---|---|
| SC1 — A CLAUDE.md or AGENTS.md that does NOT carry the generated-file banner is treated as user-authored and is never written by init or by compile. | Met | packages/domain/src/compile/targets/claude-md.ts:181-214 — early guard calls monolithicSkipIfNotOurs(...) and returns without writing when foreign; packages/domain/src/compile/targets/agents-md.ts:194-222 mirrors the same refusal. The predicate that treats unreadable/missing-banner files as foreign lives in packages/domain/src/compile/monolithic-ownership.ts:118-147, 156-186. |
| SC2 — When a target is skipped for that reason, the operator is told which file was left alone, why, and where Minsky's rules went instead. | Met | packages/domain/src/compile/monolithic-ownership.ts:161-186 — foreignOutputSkipReason(...) composes a message naming the file, the banner-based cause (or unreadable), and explicitly states .minsky/rules/ and rules_get <name>; packages/domain/src/compile/compile.ts:329-352 — runMinskyCompile seeds skippedForeignOutputs from the gate so the message survives even when the writer never runs; src/hooks/pre-commit.ts:2411-2422, 2674-2711 — pre-commit mirrors the gate and logs an explanatory message for gated-out targets including the foreign-file case. |
| SC3 — Presence-based target selection is narrowed to key on the banner rather than mere presence; CLAUDE.md gains a banner-keyed exclusion; behavior for banner-carrying files unchanged. | Met | packages/domain/src/compile/compile.ts:120-172, 292-312 — minskyCompileTargetsWithGateReport accepts foreignOutputs and drops claude.md / agents.md when their on-disk files are foreign; packages/domain/src/compile/compile.ts:316-325 — probeMinskyCompileTargetsWithGateReport reads isForeignMonolith for CLAUDE.md/AGENTS.md; src/hooks/pre-commit.ts:2411-2422, 2680-2711 — pre-commit’s compileCheckTargetsWithGateReport mirrors the same foreignOutputs gating. |
| SC4 — Staleness check and generated-file-edit guard behave correctly for owned and foreign cases: a foreign file is not reported stale and is not blocked from hand-editing. | Met | packages/domain/src/compile/targets/claude-md.ts:130-141 — listOutputFiles returns [] when foreign (via isForeignMonolith), preventing --check from calling a user file stale; packages/domain/src/compile/targets/agents-md.ts:154-165 mirrors the same; banner detection keys on shared patterns in packages/domain/src/rules/compile/banner-constants.ts:49-81 — the edit guard uses the same patterns (unchanged here) and the new ownership predicate scans the first 5 lines (packages/domain/src/compile/monolithic-ownership.ts:70-78), matching the guard’s window. |
| SC5 — Regression tests cover the foreign-file case for both monolithic targets, at init and at bare compile. | Met | packages/domain/src/compile/monolithic-ownership.test.ts — new suite verifies banner detection, ownership classification including unreadable cases, mapping to output names, skip-reason messaging, and monolithicSkipIfNotOurs behavior. Additional tests cited in the PR cover target selection and hook mirrors (not shown in this diff), and existing claude/agents target tests were updated accordingly per PR description. |
Documentation impact
- updated-in-pr — This PR adds a new section to docs/rules/template-system-guide.md describing monolithic file ownership, skip messaging, effects on compile --check, and user remedies (diff hunk added lines 84-148). The doc now matches the implemented behavior; no further doc updates required.
Affected: docs/rules/template-system-guide.md
There was a problem hiding this comment.
Independent adversarial review (Chinese-wall)
Reviewer: minsky-reviewer[bot] via openai:gpt-5
Tier: 2
Re-verification complete. The prior BLOCKING concern is addressed: ownership is centralized in a shared predicate, both monolithic writers refuse to overwrite foreign files, target selection gates on banner-based ownership (including for CLAUDE.md, which previously lacked a gate), and compile --check/pre-commit no longer flag user-owned files as stale. Operator-facing messaging surfaces which file was left alone and why, and the CLI/init print it in the right places. Tests comprehensively cover the new behavior, and documentation was updated accordingly. I found no new critical defects introduced by this fix. Approved to merge.
Spec verification
| Criterion | Status | Evidence |
|---|---|---|
| SC1 — A CLAUDE.md or AGENTS.md that does NOT carry the generated-file banner is treated as user-authored and is never written by init or by compile. | Met | packages/domain/src/compile/targets/claude-md.ts:165-207 — monolithicSkipIfNotOurs(...) guard returns early with filesWritten: [] and definitionsIncluded: [] when foreign; packages/domain/src/compile/targets/agents-md.ts:185-214 mirrors the same refusal. The ownership predicate and helpers live in packages/domain/src/compile/monolithic-ownership.ts:119-187 (isForeignMonolith, monolithicSkipIfNotOurs). |
| SC2 — When a target is skipped for that reason, the operator is TOLD: which file was left alone, why, and where Minsky's rules went instead. | Met | packages/domain/src/compile/monolithic-ownership.ts:149-187 — foreignOutputSkipReason(...) composes a message naming the file, the banner-based or unreadable cause, and points to .minsky/rules/ with rules_get <name>; packages/domain/src/compile/compile.ts:288-325, 361-392 — probeMinskyCompileTargetsWithGateReport and runMinskyCompile seed skippedForeignOutputs so the message surfaces even when the writer never runs; packages/domain/src/init.ts:357-368 — initializeProject prints these reasons before reachability warnings; src/adapters/shared/commands/compile/compile-commands.ts:104-117 and src/commands/compile/index.ts:96-104 — CLI surfaces skippedForeignOutputs at top level. |
| SC3 — Presence-based target selection is narrowed to key on the BANNER rather than mere presence; CLAUDE.md gains a banner-keyed exclusion; behavior for banner-carrying files unchanged. | Met | packages/domain/src/compile/compile.ts:99-172 — minskyCompileTargetsWithGateReport accepts foreignOutputs and drops claude.md/agents.md when their on-disk files are foreign; packages/domain/src/compile/compile.ts:266-312 — probeMinskyCompileTargetsWithGateReport reads isForeignMonolith for CLAUDE.md/AGENTS.md; tests in packages/domain/src/compile/compile.test.ts:265-339 assert dropping claude.md and agents.md appropriately and keeping per-file targets; src/hooks/pre-commit.ts:2674-2738 mirrors the same gating in compileCheckTargetsWithGateReport for pre-commit. |
| SC4 — Staleness check and generated-file-edit guard behave correctly for owned and foreign cases: a foreign file must not be reported stale, and must not be blocked from hand-editing. | Met | packages/domain/src/compile/targets/claude-md.ts:139-147 and packages/domain/src/compile/targets/agents-md.ts:154-165 — listOutputFiles returns [] when isForeignMonolith is true, preventing --check from marking a user file stale; banner scan window aligned via packages/domain/src/compile/monolithic-ownership.ts:49-60 (BANNER_SCAN_LINES = 5) and packages/domain/src/rules/compile/banner-constants.ts:49-81 patterns — same window as the generated-file edit guard uses; tests in packages/domain/src/compile/targets/*-md.test.ts assert no outputs for foreign files. |
| SC5 — A regression test covers the foreign-file case for BOTH monolithic targets, at init and at bare compile. | Met | packages/domain/src/compile/monolithic-ownership.test.ts — comprehensive unit tests for banner detection, ownership classification, skip reasons, mapping; packages/domain/src/compile/targets/claude-md.test.ts and targets/agents-md.test.ts — end-to-end foreign-file refusal and messaging; packages/domain/src/compile/compile.test.ts — selection/probe behavior for foreign CLAUDE.md/AGENTS.md; packages/domain/src/init-backend-selection.test.ts: additions at end verify init emits the foreign-file skip reason and ordering before reachability; src/hooks/compile-check-targets.test.ts adds the pre-commit mirror tests. |
Adoption sweep
| Symbol | Kind | Consumers found | Classification | Notes |
|---|---|---|---|---|
| packages/domain/src/compile/probeMinskyCompileTargetsWithGateReport | function | packages/domain/src/compile/compile.ts:374 — used to seed gate-skipped foreign outputs in runMinskyCompile, packages/domain/src/compile/compile.ts:266 — primary export; local module consumer | Adopted | New helper exported alongside existing probe to surface gate-skipped targets; consumed internally by runMinskyCompile. |
Documentation impact
- updated-in-pr — This PR adds a new section to docs/rules/template-system-guide.md explaining monolithic file ownership, skip messaging, effects on compile --check and pre-commit, and user remedies. The prose matches the implemented behavior (foreign-file refusal, banner-based ownership, and surfaced reasons).
Affected: docs/rules/template-system-guide.md
Summary
minsky initin a project that already has agent instructions silently destroyed them.Measured pre-fix at
1c7a6366c(the control for AT1): a scratch repo carrying a hand-written140-byte
CLAUDE.md— house conventions, a "tabs not spaces" rule, a "never touchlegacy/without asking Dana" rule — ran
minsky initand got back 15,085 bytes,grep -cfor the threeoriginal rules returning 0, and no warning. In the same run its own
.claude/rules/acme-house-style.mdsurvived byte-identical.That asymmetry is the finding. Minsky plays nice with foreign content in every per-file channel it
writes and claimed sole ownership only of the two monolithic markdown files — by inheritance
rather than by choice, because in the Minsky repository
CLAUDE.mdgenuinely IS wholly generated,so the plant's posture shipped into the product without the user-owned case ever being in frame.
Raised by the principal mid-review of PR #3629: "are we assuming sole ownership or will we play
nice?"
What changed
Ownership keys on the generation banner, in one shared predicate
(
packages/domain/src/compile/monolithic-ownership.ts) with three consumers that have to agreeexactly — a disagreement between any two is either a destroyed user file or a Minsky repo that can
no longer refresh its own
CLAUDE.md:targets/claude-md.ts,targets/agents-md.ts). This is thefloor rather than target selection alone, because
runMinskyCompilereturns early on an explicit--target claude.mdand never reaches the probe — a selection-only guard would leave the oneinvocation an operator reaches for by name still destroying the file.
listOutputFilesreports no outputs for a foreign file, which is what stops--checkcallingit stale.
checkStalenessdrives its whole comparison off that list, so answering there coversevery caller at once.
kind: "foreign"gate entry distinct from theexisting
"harness"one — the two have opposite remedies, and offering--targetas the fixfor a foreign skip would overwrite the file the gate is protecting.
Absent is not foreign (a fresh project still gets the full output), and an unreadable file fails
open — the other direction would silently stop maintaining a
CLAUDE.mdthat is genuinely ours,which is an inert pipeline with no error anywhere.
SC3 needed a gate ADDED, not the narrowing the spec described.
claude.mdwas pushedunconditionally (
compile.ts:143) and had no presence rule to narrow, so a literal reading of"narrow the presence-based rule" would not have reached the file this task is named for — and without
it SC4 cannot pass, because
compile --checkwould keep reporting a foreignCLAUDE.mdstale.AGENTS.md's already-exists escape is narrowed the same way and applied unconditionally:existingOutputs.agentsMdis only ever consulted under claude-code, so narrowing that field alonewould have left a Cursor project's hand-written
AGENTS.mdunprotected.src/hooks/pre-commit.ts'scompileCheckTargetsis a parallel implementation of the same mapping andis gated in lockstep — the same trap mt#4866 recorded on its own PR. Without the mirror, a project
with its own
CLAUDE.mdis told at every commit that it is stale, and the invocation that wouldrefresh it is refused by the writer, so the operator has no way out at all.
Silence would reproduce the defect quietly, so both surfaces report.
initwarns before itsunreachability warning — with
CLAUDE.mdleft alone every base rule is also unreachable, and thatmessage on its own sends the operator hunting for a frontmatter problem that does not exist.
runMinskyCompileseeds the report from the gate, because a gated-out target never runs and so itsown refusal never fires.
The banner literal was duplicated in both writers; it now lives in
banner-constants.tsbeside thedetection patterns, which is the module that exists (mt#1798) to keep emission and detection from
drifting. Adding a third copy for the ownership predicate is exactly what it was built to prevent.
Known interim cost — recorded, not papered over
.claude/rules/only accepts rules that declare globs ANDalwaysApply: false(
isEligibleForClaudeRules), and every base rule isalwaysApply: true. So a project whoseCLAUDE.mdMinsky does not own now receives zero always-apply rules. Verified in the post-fixrun:
.claude/rules/got only the user's own file.That is strictly better than destroying their file, and it is not silent — it is what SC2's message
says, in as many words. Where the rules should go instead is ask#11711 (open, principal-owned),
which mt#4986's spec scopes out of this change and which should be answered before mt#573 is planned.
Execution evidence:
bun test --preload ./tests/setup.ts --timeout=15000 packages/domain/src/compile/ packages/domain/src/init-backend-selection.test.ts src/hooks/compile-check-targets.test.tsBroader sweep —
bun scripts/run-related-tests.ts packages/domain/src/compile/compile.ts packages/domain/src/compile/monolithic-ownership.ts packages/domain/src/init.ts src/hooks/pre-commit.ts packages/domain/src/rules/compile/banner-constants.ts:Typecheck: 0 errors across 8 projects (
infra/skipped, deps not installed locally; CI covers it).Lint: 0 errors, 0 warnings across 4,383 files.
SC1 — a non-banner-carrying CLAUDE.md/AGENTS.md is never written
Live, post-fix, in a scratch repo with a foreign
CLAUDE.md(140 B) andAGENTS.md(56 B):The floor, via an explicit
--target(the path that bypasses the probe):Unit:
does not write a foreign CLAUDE.md, and leaves it byte-identical/... a foreign AGENTS.md ...— both read the file back rather than trustingfilesWritten.SC2 — the operator is told which file, why, and where the rules went
Both surfaces, live.
initabove; barecompile:Unit:
reports the skip with a reason naming the file(both targets);foreignOutputSkipReason > names the file, the reason, and where the rules actually are; andreports the skip BEFORE the unreachability warning it explains— the ordering is load-bearing, notcosmetic.
SC3 — selection keys on the banner
Unit, in
compile.test.tsforeign-ownership gate (mt#4986 SC3):drops claude.md when CLAUDE.md is the user's;drops agents.md when AGENTS.md is the user's, on ANY harness;leaves the per-file targets alone;changes nothing when both monolithic outputs are ours; plus the two gate-kind tests.End-to-end through the probe:
a hand-written AGENTS.md does NOT keep agents.md,a hand-written CLAUDE.md drops claude.md, which had no gate at all before. Mirrored insrc/hooks/compile-check-targets.test.ts.Two existing tests were updated rather than left passing, and this is a deliberate behavior
change, not a fixture tidy-up:
mt#4866: an existing AGENTS.md keeps agents.md under claude-codeused a bare
"# Agents\n"fixture. Under this change that is a hand-written file, so asserting itkeeps the target would pin the exact behaviour mt#4986 removes. The fixture now carries the banner —
which is what an
AGENTS.mdthat escape was written for actually looks like — and the hand-writtencase is pinned separately as its own test.
SC4 — a foreign file is not stale, and is still hand-editable
Edit guard, exercised live against the real hook rather than read:
The pair is the point: the guard can fail, and it discriminates correctly.
SC5 — regression coverage for both targets, at
initand at barecompileNew:
monolithic-ownership.test.ts(12 cases). Extended:claude-md.test.tsandagents-md.test.ts(5 each, target level),
compile.test.ts(selection + probe),init-backend-selection.test.ts(3, the
initsurface),compile-check-targets.test.ts(3, the pre-commit mirror).AT3 — a banner-carrying file is still regenerated, and this repo is unaffected
Minsky's own repo,
bun run src/cli.ts compile --checkin the session: all 8 targets selected(
claude.mdandagents.mdamong them), no staleness, exit 0. Both gates are inert here becauseboth files carry the banner.
AT5 — the per-file channels are unchanged
In the post-fix scratch run,
.claude/rules/acme-house-style.mdis byte-identical (md550820f15b26c7eebaa643e56c6113efd) andcursor-rules-tswrote its 4 files normally. Pinned byleaves the per-file targets alone.Negative control:
The fix routes every path through one predicate, so forcing
isForeignMonolithto returnfalserestores the complete pre-fix behaviour — writers write, selection never gates,
listOutputFilesreturns the path — rather than reverting one line and leaving a state that is neither pre- nor
post-fix (mt#4512). Observed with that revert in place:
Reverted before commit; the suite is green above.
What this control does NOT cover, stated rather than implied: the
init-reporting tests and thepre-commit mirror tests feed
skippedForeignOutputs/foreignOutputsin directly, so they do notroute through the predicate and stayed green under the revert. They test the reporting and mapping
layers, which did not exist pre-fix at all — there is no earlier state in which they could have run.
Live verification
Everything under Execution evidence above was run live against a sandboxed scratch repo (empty
HOME/XDG_*,CLAUDECODE=1) using the session's source viabun run src/cli.ts— not the bundle,which
scripts/cli-entry.tscan serve stale.Deploy verification:
All ten changed source files return
truefromisDeploySurfaceFile, so no[no-deploy-impact]claim is made and §10 applies: after merge,
deployment_wait-for-latestforminsky-mcpandreviewerwithnotBefore= the merge timestamp andexpectCommitSha= the merge SHA, healthidentity asserted from the response body, and build identity resolved by correlating the deploy
workflow runs to the merge SHA if it comes back
indeterminate(both are image-source services, soit will). No new external-system integration, so no live-integration exercise is owed.
Judgment calls
two functions in
compile.ts. Its branch predates mt#4866 entirely — 230 lines against main's 325,with no harness gate at all — so it must rebase over a rewrite of those functions whether or not
this lands. Waiting was the worse option in the other direction: feat(mt#3854): Make .codex a compile output so the harness config stops fossilizing #3253 is blocked on its own rebase,
so "wait for it to merge" is an open-ended hold on a defect that destroys user files today. The four
files carrying most of this change (
claude-md.ts,agents-md.ts,init.ts,staleness.ts) areclear of it.
definitionsIncludedis empty on the refusal path, not the rules that would have been emitted.Reporting them as included would tell
init's reachability accounting (mt#4770) that four baserules reached the agent when they reached nothing — the same false-completion shape as listing an
unwritten file in
filesWritten.cursorRulesis deliberately not narrowed..cursor/rules/is a per-file channel that alreadycoexists correctly with hand-authored files; keying its selection on a banner would be a regression.
Task: mt#4986 · Planning audit and gate walk on the task record.