Skip to content

fix(mt#4986): Stop overwriting a project's own CLAUDE.md, and say which file was left alone - #3643

Merged
edobry merged 3 commits into
mainfrom
task/mt-4986
Sep 4, 2026
Merged

edobry merged 3 commits into
mainfrom
task/mt-4986

Conversation

@minsky-ai

@minsky-ai minsky-ai Bot commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

Summary

minsky init in 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-written
140-byte CLAUDE.md — house conventions, a "tabs not spaces" rule, a "never touch legacy/
without asking Dana" rule — ran minsky init and got back 15,085 bytes, grep -c for the three
original rules returning 0, and no warning. In the same run its own
.claude/rules/acme-house-style.md survived 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.md genuinely 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 agree
exactly — a disagreement between any two is either a destroyed user file or a Minsky repo that can
no longer refresh its own CLAUDE.md:

  1. The two writers refuse the write (targets/claude-md.ts, targets/agents-md.ts). This is the
    floor rather than target selection alone, because runMinskyCompile returns early on an explicit
    --target claude.md and never reaches the probe — a selection-only guard would leave the one
    invocation an operator reaches for by name still destroying the file.
  2. listOutputFiles reports no outputs for a foreign file, which is what stops --check calling
    it stale. checkStaleness drives its whole comparison off that list, so answering there covers
    every caller at once.
  3. Target selection drops the target, with a kind: "foreign" gate entry distinct from the
    existing "harness" one — the two have opposite remedies, and offering --target as the fix
    for 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.md that is genuinely ours,
which is an inert pipeline with no error anywhere.

SC3 needed a gate ADDED, 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 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 --check would keep reporting a foreign CLAUDE.md stale.
AGENTS.md's already-exists escape is narrowed the same way and applied unconditionally:
existingOutputs.agentsMd is only ever consulted under claude-code, so narrowing that field alone
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 — the same trap mt#4866 recorded on its own PR. Without the mirror, a project
with its own CLAUDE.md is told at every commit that it is stale, and the invocation that would
refresh 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. init warns before its
unreachability warning — with CLAUDE.md left alone every base rule is also unreachable, and that
message on its own sends the operator hunting for a frontmatter problem that does not exist.
runMinskyCompile seeds the report from the gate, because a gated-out target never runs and so its
own refusal never fires.

The banner literal was duplicated in both writers; it now lives in banner-constants.ts beside the
detection 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 AND alwaysApply: false
(isEligibleForClaudeRules), and every base rule is alwaysApply: true. So a project whose
CLAUDE.md Minsky does not own now receives zero always-apply rules. Verified in the post-fix
run: .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.ts

 381 pass
 0 fail
 952 expect() calls
Ran 381 tests across 16 files. [2.05s]

Broader 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:

 1155 pass
 0 fail
 4534 expect() calls
Ran 1155 tests across 49 files. [4.20s]

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) and AGENTS.md (56 B):

$ bun run src/cli.ts init --repo <scratch> --backend minsky --rule-format minsky --mcp false
minsky init: wrote 4 base rule(s) to .minsky/rules.
minsky init: <scratch>/CLAUDE.md was left untouched — it does not carry Minsky's
  generated-file banner, so it is treated as yours and is never overwritten. ...
minsky init: 4 scaffolded rule(s) are not reachable by Claude Code — ...

$ md5 -q CLAUDE.md AGENTS.md          # unchanged from before the run
2449d2706880e62d6376cb5f1ee94342
4df191c745ef025e3b7a9c4dc40e5464

The floor, via an explicit --target (the path that bypasses the probe):

$ bun run src/cli.ts compile --target agents.md
[compile] <scratch>/AGENTS.md was left untouched — ...
$ md5 -q AGENTS.md
4df191c745ef025e3b7a9c4dc40e5464      # byte-identical

Unit: does not write a foreign CLAUDE.md, and leaves it byte-identical /
... a foreign AGENTS.md ... — both read the file back rather than trusting filesWritten.

SC2 — the operator is told which file, why, and where the rules went

Both surfaces, live. init above; bare compile:

$ bun run src/cli.ts compile
[compile] <scratch>/CLAUDE.md was left untouched — it does not carry Minsky's generated-file
  banner, so it is treated as yours and is never overwritten. Minsky's rule sources are in
  .minsky/rules/; nothing loads them into your agent automatically while this file is yours,
  so an agent has to ask for one by name with `rules_get <name>`. To hand the file over to
  Minsky instead, move it aside and re-run.
[compile] <scratch>/AGENTS.md was left untouched — ...
[compile] Target "cursor-rules-ts": 4 file(s) written
[compile] Target "claude-rules": 0 file(s) written

Unit: reports the skip with a reason naming the file (both targets); foreignOutputSkipReason > names the file, the reason, and where the rules actually are; and
reports the skip BEFORE the unreachability warning it explains — the ordering is load-bearing, not
cosmetic.

SC3 — selection keys on the banner

Unit, in compile.test.ts foreign-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 in
src/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-code
used a bare "# Agents\n" fixture. Under this change that is a hand-written file, so asserting it
keeps the target would pin the exact behaviour mt#4986 removes. The fixture now carries the banner —
which is what an AGENTS.md that escape was written for actually looks like — and the hand-written
case is pinned separately as its own test.

SC4 — a foreign file is not stale, and is still hand-editable

$ bun run src/cli.ts compile --check --target claude.md
[compile] <scratch>/CLAUDE.md was left untouched — ...
  "filesWritten": [],
  "definitionsIncluded": [],
  "stale": false
EXIT=0

Edit guard, exercised live against the real hook rather than read:

$ echo '{"tool_name":"Edit","tool_input":{"file_path":"<scratch>/CLAUDE.md",...}}' \
    | bun .minsky/hooks/check-generated-file-edit.ts
EXIT=0                                   # foreign file — permitted

$ echo '{"tool_name":"Edit","tool_input":{"file_path":"<scratch>/AGENTS.md",...}}' \
    | bun .minsky/hooks/check-generated-file-edit.ts
{"permissionDecision":"deny", ... "Marker: [HTML comment: Generated by]"}

The pair is the point: the guard can fail, and it discriminates correctly.

SC5 — regression coverage for both targets, at init and at bare compile

New: monolithic-ownership.test.ts (12 cases). Extended: claude-md.test.ts and agents-md.test.ts
(5 each, target level), compile.test.ts (selection + probe), init-backend-selection.test.ts
(3, the init surface), compile-check-targets.test.ts (3, the pre-commit mirror).

AT3 — a banner-carrying file is still regenerated, and this repo is unaffected

$ printf '<!-- Generated by minsky rules compile. Do not edit directly. -->\n\n# stale\n' > AGENTS.md
before: 83 bytes
$ bun run src/cli.ts compile --target agents.md
[compile] Target "agents.md" output size: 14968 chars
after:  15112 bytes                       # regenerated, no "left untouched" message

Minsky's own repo, bun run src/cli.ts compile --check in the session: all 8 targets selected
(claude.md and agents.md among them), no staleness, exit 0. Both gates are inert here because
both files carry the banner.

AT5 — the per-file channels are unchanged

In the post-fix scratch run, .claude/rules/acme-house-style.md is byte-identical (md5
50820f15b26c7eebaa643e56c6113efd) and cursor-rules-ts wrote its 4 files normally. Pinned by
leaves the per-file targets alone.

Negative control:

The fix routes every path through one predicate, so forcing isForeignMonolith to return false
restores the complete pre-fix behaviour — writers write, selection never gates, listOutputFiles
returns 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:

 78 pass
 11 fail
Ran 89 tests across 4 files.

(fail) isForeignMonolith > is true only for the foreign case
(fail) probeMinskyCompileTargets > mt#4986: a hand-written AGENTS.md does NOT keep agents.md
(fail) probeMinskyCompileTargets > mt#4986: a hand-written CLAUDE.md drops claude.md, ...
(fail) claudeMdTarget > does not write a foreign CLAUDE.md, and leaves it byte-identical
(fail) claudeMdTarget > reports the skip with a reason naming the file
(fail) claudeMdTarget > reports NO definitions as included when the output was not written
(fail) claudeMdTarget > reports no output files for a foreign CLAUDE.md, so --check cannot call it stale
(fail) agentsMdTarget > does not write a foreign AGENTS.md, and leaves it byte-identical
(fail) agentsMdTarget > reports the skip with a reason naming the file
(fail) agentsMdTarget > reports NO definitions as included when the output was not written
(fail) agentsMdTarget > reports no output files for a foreign AGENTS.md, so --check cannot call it stale

Reverted before commit; the suite is green above.

What this control does NOT cover, stated rather than implied: the init-reporting tests and the
pre-commit mirror tests feed skippedForeignOutputs / foreignOutputs in directly, so they do not
route 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 via bun run src/cli.ts — not the bundle,
which scripts/cli-entry.ts can serve stale.

Deploy verification:

All ten changed source files return true from isDeploySurfaceFile, so no [no-deploy-impact]
claim is made
and §10 applies: after merge, deployment_wait-for-latest for minsky-mcp and
reviewer with notBefore = the merge timestamp and expectCommitSha = the merge SHA, health
identity 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, so
it will). No new external-system integration, so no live-integration exercise is owed.

Judgment calls

  • Proceeded despite a file-level collision with open PR feat(mt#3854): Make .codex a compile output so the harness config stops fossilizing #3253 (mt#3854), which modifies the same
    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) are
    clear of it.
  • definitionsIncluded is 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 base
    rules reached the agent when they reached nothing — the same false-completion shape as listing an
    unwritten file in filesWritten.
  • cursorRules is deliberately not narrowed. .cursor/rules/ is a per-file channel that already
    coexists 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.

edobry and others added 2 commits September 4, 2026 18:51
…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-ai minsky-ai Bot added the authorship/co-authored Co-authored by human and AI agent label Sep 4, 2026
@minsky-reviewer

minsky-reviewer Bot commented Sep 4, 2026 •

Copy link
Copy Markdown

Minsky Reviewer Status

Verdict: APPROVED — no blocking findings
Review: View review
Model: openai/gpt-5 | Tokens: 618K prompt, 7K completion | Duration: 108s
Mode: normal

Commands

  • /review — request a fresh review

@minsky-reviewer minsky-reviewer Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 for minsky init/compile: it now refuses to overwrite foreign CLAUDE.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 --check stops 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 --check and 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 only GENERATION_BANNER_PATTERNS. The canonical predicate and window live in packages/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., export hasGenerationBanner and BANNER_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 seeding gateSkippedForeign, the code maps entry.target to an output path via entry.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 an assertNever/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-78 adds skippedForeignOutputs to MinskyCompileResult. The CLI renderers were updated in src/adapters/shared/commands/compile/compile-commands.ts and src/commands/compile/index.ts, and init.ts threads 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 by init/compile. I did not find updates under docs/ 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

@minsky-reviewer minsky-reviewer Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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
    probeMinskyCompileTargetsWithGateReport surfaces both target selections and gated-out entries so callers (including runMinskyCompile) 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
    listOutputFiles now returns an empty list when the on-disk CLAUDE.md is foreign, ensuring --check does not falsely report it as stale. Mirrored in agents-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

@edobry
edobry merged commit be66b76 into main Sep 4, 2026
20 checks passed
@edobry
edobry deleted the task/mt-4986 branch September 4, 2026 23:19

@minsky-reviewer minsky-reviewer Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

authorship/co-authored Co-authored by human and AI agent

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant