Skip to content

feat(sync,init): own only a marked block in AGENTS.md; init emits only the tools a repo uses - #160

Merged
CodeWithJuber merged 5 commits into
masterfrom
fix/agents-md-managed-block
Sep 24, 2026
Merged

CodeWithJuber merged 5 commits into
masterfrom
fix/agents-md-managed-block

Conversation

@CodeWithJuber

Copy link
Copy Markdown
Owner

What & why

An evaluation of the HostLelo site found a high-severity problem: "forge init/sync replace a hand-written AGENTS.md; Stop-hook autosync silently reverts human edits". What it measured on a scratch copy of HostLelo:

  • forge init --no-settings rewrote AGENTS.md (its md5 changed) into the generic # AGENTS.md — engineering rules.
  • The repo's own Cursor Cloud, lint, e2e and WHMCS instructions were moved to AGENTS.md.forge-bak, which no agent reads.
  • init also created .aider.conf.yml, .codex/, .continue/, .cursor/, .gemini/, .openclaw/, .roo/, .vscode/, .zed/, .mcp.json and .gitattributes.
  • Once Forge managed the file, the Stop-hook autoSyncIfDrifted compared the whole file and rewrote any human edit, with no backup.
  • forge doctor told users "stale or hand-edited — run forge sync", which is the destructive path.

What this PR changes:

AGENTS.md is a managed block (61aa2db, 2aae444)

  • Forge now owns only the text between <!-- forge:begin --> and <!-- forge:end -->. Sync appends the block to a hand-written file, then compares and rewrites only the block.
  • Auto-sync does the same. It never adds a block to a file that has none, and it no longer runs the full per-tool sync.
  • One classifier, agentsMdStatus, is shared by sync, auto-sync and doctor.
  • A file an older Forge generated whole converts without losing anything: the hash in its header identifies Forge's text. If the generated text itself was edited, only an explicit forge sync converts it, after writing a timestamped AGENTS.md.forge-bak-<time> backup.
  • Damaged markers are left alone, with a warning.
  • Notes added under a generated CLAUDE.md header survive later syncs.

init emits only the tools a repo uses (61aa2db, 399573f)

  • The default is Claude plus the tools the repo already shows. --tools <list|all> overrides it.
  • The choice is recorded in .forge/forge.config.json. sync, doctor --fix and integrations use the same set.
  • A repo that never ran init still gets every tool.

Fixes from the independent review of 61aa2db (I reproduced each one first):

  • Major: MCP ownership. integrations add recorded ownership for MCP targets it never wrote. After a default init plus add context7, mcp.adopted listed seven configs that did not exist. A person's own context7 entry in .cursor/mcp.json was then overwritten by forge init --tools claude,cursor and deleted by integrations remove. Now ownership is recorded only for the targets in the recorded set. When a tool joins the set later, claimEmittedIntegrations takes ownership of the copies sync wrote there (entries that were absent or identical to Forge's spec). A person's divergent entry stays theirs, and Forge's own copies still get spec updates and are removed by remove.
  • forge tools <name>. It synced with the recorded set only, so after a default init forge tools cursor wrote no Cursor config. The tool is now added to the recorded set in the same config write.
  • Marker lines in the body. A rule, fact or lesson with a line that exactly matched <!-- forge:end --> ended the block early. AGENTS.md grew by 25 B on every sync and every auto-sync and never stabilised. Such lines are now indented one space.
  • Size checks. The Codex and Windsurf checks measured only Forge's own text. A 37,992 B file was reported as native (6476/32768 B), and Codex cuts off the end of the file, which is where the block now sits. The checks and a new sync warning now measure the file on disk.
  • Stranded AGENTS.md.forge-bak. It was reported only when an explicit sync converted the file. When the Stop hook converted it, which usually happens first, nothing mentioned it again. forge sync and forge doctor now warn for as long as the backup holds text that AGENTS.md does not.
  • Docs that were no longer true. Quickstart, integrations and tools docs, the examples README, the landing copy and the onboarding diagrams no longer say init or add write every tool. config.mdx now says auto-sync also refreshes Continue's rules copy. The integrations dry run lists the actual files.
  • Commit type. 61aa2db added a feature under fix(sync). I did not split it, because that would rewrite this branch's history. Instead 399573f is typed feat(init) and this PR title is feat, so the release is a minor bump whether the PR is squash-merged or merge-committed. I did not add !/BREAKING CHANGE: repos that already ran init keep emitting every tool, and only a new forge init emits fewer files (--tools all restores the old behaviour). Whether that change to init's default needs a major bump is the maintainer's call.

Result on a copy of HostLelo's AGENTS.md/CLAUDE.md: forge init --no-settings keeps the original 3579 B byte for byte (checked with cmp) and appends the block after them. It creates only .mcp.json, .forge/ and .gitattributes, leaves CLAUDE.md identical, and prints no warnings.

Tests added:

  • test/agents_block.test.js (new, 17 tests): block helpers, preservation by init/sync/auto-sync, legacy conversion and backup, damaged markers, doctor states, marker lines in the body staying stable, whole-file size checks, and warnings about a stranded backup.
  • test/init_tools.test.js (new, 21 tests): tool detection and parsing, init's default and --tools, backward compatibility, recordedTools/mcpTargetFilesFor/planIntegration, per-target MCP ownership across enabling a tool and remove, ownership when a tool joins the set later, forge tools <name> adding the tool, and CLI paths.
  • Updated test/init.test.js, test/regressions.test.js and test/sync.test.js, which asserted the old replace-and-back-up and emit-every-tool behaviour.
  • Negative controls: I reverted each review fix locally and confirmed the new tests fail against 61aa2db's code.

Checks run on HEAD 399573f (Node v22.22.2):

  • npm test: exit 0. 1478 tests: 1475 pass, 0 fail, 3 skipped (the baseline at 93f37a8 was 1437 passing, 3 skipped).
  • npm run check: exit 0. 14 warnings and 2 infos, the same counts as master.
  • npm run typecheck: exit 0.
  • node src/cli.js docs check: exit 0. Its only advisory, "ARCHITECTURE.md repo-map out of date", is also on master.

Known limits:

  • There is no forge sync --tools flag. Change the set with forge init --tools.
  • An old-format AGENTS.md with CRLF line endings fails the hash check, so it is treated as edited: an explicit sync converts it with a backup.
  • The appended block keeps its own # AGENTS.md heading.
  • The Stop hook no longer refreshes MCP configs or the CLAUDE.md header line.

Checklist

  • npm test passes (Node 18/20/22) — passes on Node v22.22.2 only; I did not run 18 or 20 locally (CI runs 20/22)
  • npm run check passes (Biome lint + format)
  • New public functions have a test
  • Conventional commit message (feat:/fix:/docs: …)
  • CHANGELOG.md updated under ## [Unreleased]
  • No new runtime dependency (dev deps ok)
  • Substrate/docs updated if this changes forge substrate, forge impact, router/gate, or MCP substrate tools — n/a: none of these change. The sync/init/doctor/integrations/tools docs are updated.

Risk & rollback

  • Risk level: medium. It changes what every sync and Stop hook writes to AGENTS.md, and what forge init emits by default. Mitigations: conversion is lossless, or backed up when the generated text was edited; damaged markers are never written; repos that already ran init keep emitting every tool; the behaviour is covered by the tests above and by the end-to-end runs.
  • Rollback plan: revert the PR (or git revert 399573f 2aae444 61aa2db). Caveat: the old code treats any AGENTS.md containing forge:sync: as fully generated, and the block's header contains it. After a rollback, the next sync or Stop hook would rewrite the whole file and drop the text around the block, with no backup. Before downgrading, copy AGENTS.md aside or set FORGE_AUTOSYNC=0, and restore the hand-written part afterwards. .forge/forge.config.json tools is ignored by the old code, which emits every tool again.

Extra checks (tick if applicable)

  • npm run typecheck passes
  • Input validated at boundaries; errors handled (no swallowing) — an unknown --tools name aborts before any write; a corrupt config is refused, never overwritten; damaged markers are reported, not guessed; new warnings are surfaced by sync, doctor and the CLI
  • Authorization/ownership checked (if it touches access) — n/a: no access control changes. File ownership (the AGENTS.md block, per-target MCP adoptions) is covered by tests.
  • Logs contain no secrets/PII — the new messages contain only file names, tool names and byte counts
  • If AI-assisted: I understand it, verified the package APIs, and it has tests — AI-assisted (Claude). It uses only Node built-ins and has tests; the human author still needs to confirm they understand the change.

Found by an evaluation run against the HostLelo site (CodeWithJuber/my-next-app).

🤖 Generated with Claude Code

https://claude.ai/code/session_01UUhB8JaPayd43w37dxiXrW


Generated by Claude Code

… tools

forge init/sync replaced a hand-written AGENTS.md (the original went to
AGENTS.md.forge-bak, which no agent reads), and the Stop-hook auto-sync
then reverted any human edit to the now fully managed file.

Forge now owns only the text between <!-- forge:begin --> and
<!-- forge:end -->:
- sync appends the block to a hand-written file and afterwards compares
  and rewrites only the block (agentsMdStatus is the one classifier for
  sync, auto-sync and doctor; hasManagedBlock replaces isManaged here)
- auto-sync refreshes only the block, never adopts a file without one,
  and no longer runs the full per-tool emit
- a legacy fully generated AGENTS.md converts losslessly: the header hash
  identifies forge's bytes, so text added around them is kept; one edited
  inside the generated text is converted only by an explicit sync, after
  a timestamped AGENTS.md.forge-bak-<time> backup
- damaged markers are reported and left alone
- a generated CLAUDE.md keeps notes added under its header (marker line
  refreshed in place)
- doctor reports "hand-written AGENTS.md (no Forge block)" and its fix
  appends the block

forge init now emits config only for Claude plus the tools the repo
already shows (.cursor/, .codex/, .github/copilot-instructions.md, ...)
or an explicit --tools <list|all>, and records the set in
.forge/forge.config.json so sync, doctor --fix and integrations add emit
the same set. A repo with no recorded set still gets every tool.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UUhB8JaPayd43w37dxiXrW
Signed-off-by: Claude <noreply@anthropic.com>
…ile, report a stranded .forge-bak

Review follow-ups to the managed AGENTS.md block:

- A body line that reads exactly like `<!-- forge:end -->` (a multi-line
  rule, brain fact or lesson) ended the block early, so every sync and
  every Stop-hook auto-sync saw drift and left another stale tail behind;
  the file grew on each run. managedBlock now indents such lines one
  space (same Markdown rendering, no longer a whole-line marker).
- Codex's 32 KiB and Windsurf's ~12k-char checks measured only the
  canonical body. AGENTS.md is now the person's text plus the block, and
  those tools cut the end of the file, where the block sits. The rows and
  a new sync warning measure the file on disk (a 37,992 B file was
  reported as 6,476 B before).
- AGENTS.md.forge-bak (the hand-written file an older forge replaced) was
  mentioned only when an explicit sync converted a legacy file. The Stop
  hook usually converts first and its result is discarded, so the rules
  stayed invisible. strandedAgentsBackup() now drives a warning in both
  sync and doctor for as long as the backup holds text AGENTS.md lacks.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UUhB8JaPayd43w37dxiXrW
Signed-off-by: Claude <noreply@anthropic.com>
…ools`

`forge init --tools <list|all>` and the Claude-plus-detected default
arrived in 61aa2db under a fix type. This commit completes that feature
and carries its feat type, so the auto-release is a minor bump, not a
patch.

- integrations add recorded ownership (mcp.adopted) for every MCP
  target, including ones outside the recorded tool set that it never
  wrote. A same-name entry a person later wrote for such a tool was then
  overwritten by `forge init --tools ...` and deleted by `forge
  integrations remove` (breaks ME-08). Adoptions now cover only
  mcpTargetFilesFor(recorded set). When a tool joins the set later,
  claimEmittedIntegrations (run by init and `forge tools`) claims the
  copies sync wrote there by add's absent-or-identical rule, so spec
  updates and remove still reach forge's own copies and a divergent
  entry stays the person's.
- `forge tools <name>` synced with the recorded set only, so after a
  default init the chosen primary tool got no config at all.
  setPrimaryTool now adds the tool to a recorded set in the same config
  write; the CLI says so.
- The integrations dry run lists the files it would write instead of a
  fixed every-tool list.
- Docs: quickstart, integrations and tools docs, the examples README,
  the landing copy and the onboarding diagrams no longer say init or
  add write every tool.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UUhB8JaPayd43w37dxiXrW
Signed-off-by: Claude <noreply@anthropic.com>
@CodeWithJuber
CodeWithJuber marked this pull request as ready for review September 24, 2026 04:19
…ed-block

Signed-off-by: Claude <noreply@anthropic.com>

# Conflicts:
#	CHANGELOG.md
…ed-block

Signed-off-by: Claude <noreply@anthropic.com>

# Conflicts:
#	CHANGELOG.md
@CodeWithJuber
CodeWithJuber merged commit 750dfd6 into master Sep 24, 2026
12 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants