fix(bin): add CLAUDE.md symlink hygiene guard - #2401
Open
V4f1k wants to merge 13 commits into
Open
Conversation
Vedoucí hypotéza (no-mistakes přepisuje CLAUDE.md skrz symlink) byla vyvrácena: žádný commit z pipeline (`no-mistakes(document/review)`) nemění typ souboru, a aktuálně běžící worktree no-mistakes pro jmp-hub má symlink neporušený. Skutečná příčina: CLAUDE.md se v jmp-hubu stalo symlinkem na AGENTS.md až commitem 80f54e46 (PR kunchenguid#612, 5. 8. 2026 02:28, V4f1k). Každá větev fm/* založená před tímto commitem nese ve svojí historii starý CLAUDE.md jako obyčejný soubor - to není nic rozbité, jen ještě nedorovnané. Když se taková větev slučuje/rebasuje na základ, který už symlink má, Git na CLAUDE.md nahlásí konflikt "distinct types on each side" (přesně log z PR kunchenguid#621, merge 7166a1ec). Řešení konfliktu je ruční a chybové: v fm/don768-kanban-brana (86204b0f) byl soubor při řešení konfliktu úplně smazán a musel se druhý den ručně obnovit (5dd137c0). Ověřeno na všech 103 aktuálních fm/* větvích: každá s obyčejným souborem CLAUDE.md prokazatelně nemá 80f54e46 v historii; žádná větev odvozená od symlinku ho neztratila. Příčina je tedy běžné git chování při slučování zastaralých větví, ne chyba v našem bin/ kódu ani v no-mistakes - proto jen pojistka: - bin/fm-claude-symlink-check.sh: projekt-agnostická kontrola, tiše přeskočí repozitáře bez symlinkované CLAUDE.md politiky, jinak ověří CLAUDE.md ve worktree proti symlinku v základové větvi a při rozjetí vypíše přesný obnovovací příkaz. - bin/fm-brief.sh: každý ship brief (všechny tři delivery mody) teď pracovníka pošle kontrolu spustit těsně před `done`, tedy dřív než PR. - Testy: tests/fm-claude-symlink-check.test.sh (7 scénářů) a rozšíření tests/fm-brief.test.sh o ověření, že se sekce generuje ve všech třech modech. bin/fm-test-run.sh: zařazeno do rodiny pure-contract-unit.
…dation, and brief quoting
…red-target recovery
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Intent
Investigate why jmp-hub's CLAUDE.md -> AGENTS.md symlink (a captain-decided one-source-of-truth convention; the symlink direction must never be reversed or removed) keeps turning into a plain file on fm/* worker branches, causing repeated 'distinct types on each side' merge conflicts (documented in PR #621). The leading hypothesis to test first was that no-mistakes' prompt-injection defense neutralizes AGENTS.md/CLAUDE.md by writing through the symlink before running the gate agent, which would explain why every pipeline-processed branch is affected. That hypothesis had to be confirmed or refuted with hard evidence (commit SHAs, authorship, git log -p), not argued from plausibility. Investigation in a read-only clone of jmp-hub at projects/jmp-hub (never modified) found: CLAUDE.md became a symlink only in commit 80f54e46 (PR #612, 2026-08-05 02:28, author V4f1k). Checked all 103 current origin/fm/* branches: every branch whose CLAUDE.md is a plain file provably does NOT have 80f54e46 as an ancestor (it simply predates the symlink conversion and hasn't synced since - not corruption). Every branch descended from 80f54e46 still has the correct symlink. No no-mistakes-authored commit (grep for 'no-mistakes(document/review):' commits) ever changes CLAUDE.md's file mode, and the currently-running no-mistakes worktree for jmp-hub still has an intact symlink. This refutes the leading hypothesis with direct evidence. The actual mechanism: merging/rebasing a pre-conversion branch against a post-conversion base hits git's ordinary 'distinct types on each side' conflict on CLAUDE.md (exactly reproduced in PR #621's merge commit 7166a1ec, which resolved it correctly by keeping the symlink), and that manual conflict resolution is error-prone - fm/don768-kanban-brana's merge (86204b0f) dropped CLAUDE.md entirely instead of keeping the symlink, requiring a manual restore the next day (5dd137c0). This is not a bug in firstmate's own bin/ tooling (bin/fm-ensure-agents-md.sh was already confirmed symlink-aware and ruled out first) nor in no-mistakes; it is an operational hazard of syncing stale branches, so per the task's explicit instructions the deliverable is a safety net rather than a root-cause code fix: bin/fm-claude-symlink-check.sh is a new, project-agnostic, read-only guard script. It silently skips (exit 0) any repo whose resolved base branch does not manage CLAUDE.md as a symlink, so it is a no-op everywhere except a project that has actually adopted this convention. Where the base does manage it as a symlink, it verifies the worktree's CLAUDE.md still matches (present, is a symlink, correct target) and on failure prints the exact recovery commands (git checkout -- CLAUDE.md, or ln -sfn CLAUDE.md) and exits 1. It auto-detects the base branch (preferring the freshest locally-available remote-tracking ref, falling back to a local branch) or accepts an explicit ref for testing, mirroring the existing default_branch() pattern already duplicated in bin/fm-ff-lib.sh and bin/fm-fleet-sync.sh (deliberately not refactored into a shared lib - that consolidation is out of scope for this fix per explicit instructions to avoid repo-wide cleanup as a side effect). It is wired into bin/fm-brief.sh's shared 'Project memory' section so every ship-mode brief (no-mistakes, direct-PR, local-only) instructs the worker to run it right before reporting done - i.e. before a PR ever exists, not just left available. Test coverage: tests/fm-claude-symlink-check.test.sh (7 scenarios: matching symlink passes, regular-file demotion fails with both recovery commands present, missing file fails, wrong-target symlink fails, a repo with no symlink policy skips silently, a repo with no CLAUDE.md at all skips silently, and auto-detection of origin's default branch works) plus a new assertion in tests/fm-brief.test.sh confirming the hygiene-check section renders in all three delivery modes. bin/fm-test-run.sh gained one line classifying the new test into the existing pure-contract-unit family (same family as the closely related fm-ensure-agents-md.test.sh). Deliberately out of scope, per explicit instruction: do not touch jmp-hub at all (read-only investigation only, verified via git log/ls-tree/diff against the local read-only clone and its fetched remote branches, never committing there), do not un-break or rewrite history on any of the 34+ affected fm/* branches (most are dead; rewriting other workers' branches is not this task's job), do not reverse or remove the CLAUDE.md -> AGENTS.md symlink direction (captain's standing decision), and no repo-wide renaming/cleanup beyond this fix's own files. Full local verification already done directly (captain's standing instruction: the full bin/fm-test-run.sh suite is CI's job via --check-coverage in ci.yml, not duplicated locally) - bin/fm-lint.sh is clean on every touched script (fm-claude-symlink-check.sh, fm-brief.sh, fm-test-run.sh), the new and touched test files pass standalone and through the real bin/fm-test-run.sh harness, and the pure-contract-unit family run is green except two pre-existing, environment-dependent failures (missing tasks-axi and the @earendil-works/pi-coding-agent npm package in this session) confirmed unrelated to this diff by isolated reruns - neither failing test file references any file this change touches.
What Changed
bin/fm-claude-symlink-check.sh, a project-agnostic read-only guard that detects brokenCLAUDE.mdsymlinks and prints recovery commands.docs/scripts.md.Risk Assessment
✅ Low: Captain, the change is bounded and I found no material source-verifiable regressions or intent violations.
Testing
Fresh focused tests passed standalone and through the real harness. Manual CLI evidence confirms failure diagnostics, recovery guidance, committed-restore enforcement, and silent no-policy behavior; generated briefs contain the guard in all three delivery modes. No linters or full-suite tests were run, and the worktree is clean.
Evidence: CLI guard failure and recovery transcript
Evidence: Generated hygiene sections for all delivery modes
Evidence: Targeted harness transcript
Pipeline
Updates from git push no-mistakes
✅ **intent** - passed
✅ No issues found.
✅ **Rebase** - passed
✅ No issues found.
✅ **Review** - passed
✅ No issues found.
✅ **Test** - passed
✅ No issues found.
bash tests/fm-claude-symlink-check.test.shbash tests/fm-brief.test.shbin/fm-test-run.sh --json .../harness-targeted.json tests/fm-claude-symlink-check.test.sh tests/fm-brief.test.shManual Git fixture: broken worktree, uncommitted restore, committed recovery, and no-policy skipManual generation of no-mistakes, direct-PR, and local-only brief sectionsgit status --porcelain=v1 --untracked-files=all✅ **Document** - passed
✅ No issues found.
✅ **Lint** - passed
✅ No issues found.
✅ **Push** - passed
✅ No issues found.