create-agent-rig 0.9.1 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +419 -3
- package/README.md +267 -305
- package/package.json +8 -14
- package/packages/cli/dist/commands/create.js +56 -88
- package/packages/cli/dist/commands/doctor.js +213 -0
- package/packages/cli/dist/commands/init.js +111 -50
- package/packages/cli/dist/commands/integrations.js +468 -0
- package/packages/cli/dist/commands/setup-wizard.js +61 -0
- package/packages/cli/dist/commands/uninstall.js +1223 -0
- package/packages/cli/dist/commands/upgrade.js +472 -90
- package/packages/cli/dist/index.js +651 -46
- package/packages/cli/dist/integrations/declaration.js +158 -0
- package/packages/cli/dist/integrations/doctor-guards.js +126 -0
- package/packages/cli/dist/integrations/doctor-workflow.js +25 -0
- package/packages/cli/dist/integrations/mcp-json.js +107 -0
- package/packages/cli/dist/integrations/memory-doctor.js +124 -0
- package/packages/cli/dist/integrations/registry.js +22 -0
- package/packages/cli/dist/integrations/spawn.js +228 -0
- package/packages/cli/dist/integrations/spec-kit.js +280 -0
- package/packages/cli/dist/integrations/verify.js +193 -0
- package/packages/cli/dist/integrations/windows-job.js +275 -0
- package/packages/cli/dist/lib/elevated-paths.js +74 -0
- package/packages/cli/dist/lib/install-set.js +1 -44
- package/packages/cli/dist/lib/manifest.js +64 -5
- package/packages/cli/dist/lib/prompts.js +0 -23
- package/packages/cli/dist/lib/safe-path.js +193 -2
- package/packages/cli/dist/lib/safe-text.js +78 -0
- package/packages/cli/dist/lib/substitute.js +1 -52
- package/packages/cli/dist/lib/summary.js +5 -5
- package/packages/cli/dist/templates.js +4 -14
- package/scripts/prepare.mjs +28 -12
- package/templates/agent-os/subagent-routing.json +4 -4
- package/templates/agent-os/universal/.agents/skills/check-premises/SKILL.md +13 -9
- package/templates/agent-os/universal/.agents/skills/loop/SKILL.md +31 -6
- package/templates/agent-os/universal/.agents/skills/new-invariant/SKILL.md +5 -5
- package/templates/agent-os/universal/.agents/skills/pr-ship/SKILL.md +1 -1
- package/templates/agent-os/universal/.agents/skills/worktree-task/SKILL.md +3 -3
- package/templates/agent-os/universal/.claude/agents/code-reviewer.md +8 -6
- package/templates/agent-os/universal/.claude/agents/implementation-agent.md +41 -0
- package/templates/agent-os/universal/.claude/agents/prose-reviewer.md +8 -4
- package/templates/agent-os/universal/.claude/agents/security-scanner.md +4 -2
- package/templates/agent-os/universal/.claude/agents/test-writer.md +2 -2
- package/templates/agent-os/universal/.claude/hooks/inject-rules.mjs +83 -4
- package/templates/agent-os/universal/.claude/rules/autonomy.md +55 -27
- package/templates/agent-os/universal/.claude/rules/invariants.md +9 -8
- package/templates/agent-os/universal/.claude/rules/workflow.md +47 -22
- package/templates/agent-os/universal/.claude/scripts/decision-router.mjs +1 -1
- package/templates/agent-os/universal/.claude/scripts/detect-missed-gate.mjs +12 -9
- package/templates/agent-os/universal/.claude/scripts/doctor.mjs +100 -2
- package/templates/agent-os/universal/.claude/scripts/lib/claim-records.mjs +236 -6
- package/templates/agent-os/universal/.claude/scripts/queue/state.mjs +2 -2
- package/templates/agent-os/universal/.claude/scripts/reconcile-external-prs.mjs +4 -3
- package/templates/agent-os/universal/.claude/scripts/revalidate.mjs +16 -0
- package/templates/agent-os/universal/.claude/settings.json +0 -8
- package/templates/agent-os/universal/.claude/skills/check-premises/SKILL.md +13 -9
- package/templates/agent-os/universal/.claude/skills/loop/SKILL.md +31 -6
- package/templates/agent-os/universal/.claude/skills/new-invariant/SKILL.md +5 -5
- package/templates/agent-os/universal/.claude/skills/pr-ship/SKILL.md +1 -1
- package/templates/agent-os/universal/.claude/skills/worktree-task/SKILL.md +3 -3
- package/templates/agent-os/universal/.codex/agents/code-reviewer.toml +2 -2
- package/templates/agent-os/universal/.codex/agents/implementation-agent.toml +6 -0
- package/templates/agent-os/universal/.codex/agents/prose-reviewer.toml +2 -2
- package/templates/agent-os/universal/.codex/agents/security-scanner.toml +1 -1
- package/templates/agent-os/universal/.codex/agents/test-writer.toml +1 -1
- package/templates/agent-os/universal/.codex/hooks.json +0 -10
- package/templates/agent-os/universal/AGENTS.md +208 -107
- package/templates/agent-os/universal/CLAUDE.md +15 -162
- package/templates/agent-os/universal/PLAN.md +17 -10
- package/templates/agent-os/universal/docs/decisions/agents-md-canonical.md +313 -0
- package/templates/agent-os/universal/docs/decisions/codex-adapter.md +15 -10
- package/templates/agent-os/universal/docs/decisions/review-lanes.md +12 -8
- package/templates/agent-os/universal/docs/decisions/session-start-wire-format.md +206 -0
- package/templates/agent-os/universal/docs/decisions/subagent-routing.md +6 -3
- package/templates/agent-os/universal/docs/decisions/workflow-layer-split.md +235 -0
- package/templates/agent-os/universal/layers.json +25 -30
- package/templates/hash-history.json +8 -4
- package/templates/release-ledger.json +2 -1
- package/packages/cli/dist/lib/composition.js +0 -20
- package/packages/cli/dist/lib/targets.js +0 -28
- package/packages/cli/dist/policy/benchmark/corpus.js +0 -165
- package/packages/cli/dist/policy/core/adapter.js +0 -18
- package/packages/cli/dist/policy/core/coverage.js +0 -253
- package/packages/cli/dist/policy/core/decision-record.js +0 -287
- package/packages/cli/dist/policy/core/declaration.js +0 -127
- package/packages/cli/dist/policy/core/evidence-matrix.js +0 -94
- package/packages/cli/dist/policy/core/probe.js +0 -442
- package/packages/cli/dist/policy/core/registry.js +0 -115
- package/packages/cli/dist/policy/core/validation.js +0 -275
- package/packages/cli/dist/policy/core/vocabulary.js +0 -123
- package/packages/cli/dist/policy/harness/claude.js +0 -47
- package/packages/cli/dist/policy/harness/codex.js +0 -87
- package/packages/cli/dist/policy/harness/index.js +0 -15
- package/packages/cli/dist/policy/harness/shared-hooks.js +0 -28
- package/packages/cli/dist/policy/index.js +0 -17
- package/templates/agent-os/init/AGENTS.md +0 -201
- package/templates/agent-os/init/CLAUDE.md +0 -201
- package/templates/agent-os/stack/aws-cdk/.agents/skills/post-deploy-verify/SKILL.md +0 -105
- package/templates/agent-os/stack/aws-cdk/.agents/skills/ro-debug/SKILL.md +0 -117
- package/templates/agent-os/stack/aws-cdk/.claude/agents/cdk-diff-reviewer.md +0 -89
- package/templates/agent-os/stack/aws-cdk/.claude/rules/aws-cdk.md +0 -105
- package/templates/agent-os/stack/aws-cdk/.claude/skills/post-deploy-verify/SKILL.md +0 -105
- package/templates/agent-os/stack/aws-cdk/.claude/skills/ro-debug/SKILL.md +0 -117
- package/templates/agent-os/stack/aws-cdk/.codex/agents/cdk-diff-reviewer.toml +0 -6
- package/templates/agent-os/stack/node-ts/.claude/hooks/dod-checks.json +0 -1
- package/templates/agent-os/stack/node-ts/.claude/rules/node-ts.md +0 -85
- package/templates/agent-os/universal/.claude/hooks/guard-core-purity.mjs +0 -89
- package/templates/agent-os/universal/.claude/hooks/guard-web-boundary.mjs +0 -68
- package/templates/agent-os/universal/.claude/rules/architecture.md +0 -81
- package/templates/skeleton/aws-serverless/.github/workflows/ci.yml +0 -28
- package/templates/skeleton/aws-serverless/.github/workflows/deploy.yml +0 -90
- package/templates/skeleton/aws-serverless/README.md +0 -181
- package/templates/skeleton/aws-serverless/apps/web/next.config.mjs +0 -17
- package/templates/skeleton/aws-serverless/apps/web/package.json +0 -19
- package/templates/skeleton/aws-serverless/apps/web/src/app/layout.tsx +0 -17
- package/templates/skeleton/aws-serverless/apps/web/src/app/page.tsx +0 -96
- package/templates/skeleton/aws-serverless/apps/web/src/lib/api.ts +0 -36
- package/templates/skeleton/aws-serverless/apps/web/src/lib/validate.ts +0 -23
- package/templates/skeleton/aws-serverless/apps/web/test/shared-validation.test.ts +0 -38
- package/templates/skeleton/aws-serverless/apps/web/tsconfig.json +0 -14
- package/templates/skeleton/aws-serverless/eslint.config.mjs +0 -20
- package/templates/skeleton/aws-serverless/gitignore +0 -56
- package/templates/skeleton/aws-serverless/infra/bin/app.ts +0 -79
- package/templates/skeleton/aws-serverless/infra/cdk.json +0 -3
- package/templates/skeleton/aws-serverless/infra/lib/app-stack.ts +0 -248
- package/templates/skeleton/aws-serverless/infra/lib/web-stack.ts +0 -45
- package/templates/skeleton/aws-serverless/infra/package.json +0 -18
- package/templates/skeleton/aws-serverless/infra/test/allowed-origins.test.ts +0 -301
- package/templates/skeleton/aws-serverless/infra/test/app-composition.test.ts +0 -137
- package/templates/skeleton/aws-serverless/infra/test/app-stack.test.ts +0 -120
- package/templates/skeleton/aws-serverless/infra/test/web-stack.test.ts +0 -44
- package/templates/skeleton/aws-serverless/package.json +0 -30
- package/templates/skeleton/aws-serverless/packages/core/package.json +0 -11
- package/templates/skeleton/aws-serverless/packages/core/src/events.ts +0 -14
- package/templates/skeleton/aws-serverless/packages/core/src/index.ts +0 -15
- package/templates/skeleton/aws-serverless/packages/core/src/note.ts +0 -69
- package/templates/skeleton/aws-serverless/packages/core/test/events.test.ts +0 -23
- package/templates/skeleton/aws-serverless/packages/core/test/note.test.ts +0 -101
- package/templates/skeleton/aws-serverless/packages/db/package.json +0 -14
- package/templates/skeleton/aws-serverless/packages/db/src/client.ts +0 -17
- package/templates/skeleton/aws-serverless/packages/db/src/index.ts +0 -2
- package/templates/skeleton/aws-serverless/packages/db/src/note-model.ts +0 -52
- package/templates/skeleton/aws-serverless/packages/db/test/note-model.test.ts +0 -91
- package/templates/skeleton/aws-serverless/packages/shared/package.json +0 -11
- package/templates/skeleton/aws-serverless/packages/shared/src/env.ts +0 -17
- package/templates/skeleton/aws-serverless/packages/shared/src/errors.ts +0 -33
- package/templates/skeleton/aws-serverless/packages/shared/src/index.ts +0 -3
- package/templates/skeleton/aws-serverless/packages/shared/src/logger.ts +0 -20
- package/templates/skeleton/aws-serverless/packages/shared/test/env.test.ts +0 -26
- package/templates/skeleton/aws-serverless/packages/shared/test/errors.test.ts +0 -28
- package/templates/skeleton/aws-serverless/packages/shared/test/logger.test.ts +0 -19
- package/templates/skeleton/aws-serverless/pnpm-lock.yaml +0 -2855
- package/templates/skeleton/aws-serverless/pnpm-workspace.yaml +0 -14
- package/templates/skeleton/aws-serverless/services/api/package.json +0 -15
- package/templates/skeleton/aws-serverless/services/api/src/adapters/sqs-publisher.ts +0 -26
- package/templates/skeleton/aws-serverless/services/api/src/handlers/create-note.ts +0 -69
- package/templates/skeleton/aws-serverless/services/api/src/handlers/list-notes.ts +0 -37
- package/templates/skeleton/aws-serverless/services/api/src/list-main.ts +0 -12
- package/templates/skeleton/aws-serverless/services/api/src/main.ts +0 -21
- package/templates/skeleton/aws-serverless/services/api/src/usecases/create-note.ts +0 -41
- package/templates/skeleton/aws-serverless/services/api/src/usecases/list-notes.ts +0 -14
- package/templates/skeleton/aws-serverless/services/api/test/create-note.handler.test.ts +0 -211
- package/templates/skeleton/aws-serverless/services/api/test/create-note.usecase.test.ts +0 -45
- package/templates/skeleton/aws-serverless/services/api/test/list-notes.test.ts +0 -122
- package/templates/skeleton/aws-serverless/services/api/test/sqs-publisher.test.ts +0 -22
- package/templates/skeleton/aws-serverless/services/worker/package.json +0 -12
- package/templates/skeleton/aws-serverless/services/worker/src/handlers/note-created.ts +0 -15
- package/templates/skeleton/aws-serverless/services/worker/src/main.ts +0 -7
- package/templates/skeleton/aws-serverless/services/worker/src/usecases/process-note-created.ts +0 -37
- package/templates/skeleton/aws-serverless/services/worker/test/note-created.test.ts +0 -61
- package/templates/skeleton/aws-serverless/tsconfig.base.json +0 -15
- package/templates/skeleton/aws-serverless/tsconfig.json +0 -16
- package/templates/skeleton/aws-serverless/vitest.config.ts +0 -14
- package/templates/skeleton/node-service/.github/workflows/ci.yml +0 -27
- package/templates/skeleton/node-service/.github/workflows/deploy.yml +0 -29
- package/templates/skeleton/node-service/README.md +0 -104
- package/templates/skeleton/node-service/apps/web/next.config.mjs +0 -17
- package/templates/skeleton/node-service/apps/web/package.json +0 -19
- package/templates/skeleton/node-service/apps/web/src/app/layout.tsx +0 -17
- package/templates/skeleton/node-service/apps/web/src/app/page.tsx +0 -96
- package/templates/skeleton/node-service/apps/web/src/lib/api.ts +0 -29
- package/templates/skeleton/node-service/apps/web/src/lib/validate.ts +0 -23
- package/templates/skeleton/node-service/apps/web/test/shared-validation.test.ts +0 -38
- package/templates/skeleton/node-service/apps/web/tsconfig.json +0 -14
- package/templates/skeleton/node-service/eslint.config.mjs +0 -20
- package/templates/skeleton/node-service/gitignore +0 -54
- package/templates/skeleton/node-service/package.json +0 -30
- package/templates/skeleton/node-service/packages/core/package.json +0 -11
- package/templates/skeleton/node-service/packages/core/src/events.ts +0 -14
- package/templates/skeleton/node-service/packages/core/src/index.ts +0 -15
- package/templates/skeleton/node-service/packages/core/src/note.ts +0 -69
- package/templates/skeleton/node-service/packages/core/test/events.test.ts +0 -23
- package/templates/skeleton/node-service/packages/core/test/note.test.ts +0 -101
- package/templates/skeleton/node-service/packages/db/package.json +0 -12
- package/templates/skeleton/node-service/packages/db/src/index.ts +0 -1
- package/templates/skeleton/node-service/packages/db/src/note-store.ts +0 -100
- package/templates/skeleton/node-service/packages/db/test/note-store.test.ts +0 -100
- package/templates/skeleton/node-service/packages/shared/package.json +0 -11
- package/templates/skeleton/node-service/packages/shared/src/env.ts +0 -17
- package/templates/skeleton/node-service/packages/shared/src/errors.ts +0 -33
- package/templates/skeleton/node-service/packages/shared/src/index.ts +0 -3
- package/templates/skeleton/node-service/packages/shared/src/logger.ts +0 -20
- package/templates/skeleton/node-service/packages/shared/test/env.test.ts +0 -26
- package/templates/skeleton/node-service/packages/shared/test/errors.test.ts +0 -28
- package/templates/skeleton/node-service/packages/shared/test/logger.test.ts +0 -19
- package/templates/skeleton/node-service/pnpm-lock.yaml +0 -2402
- package/templates/skeleton/node-service/pnpm-workspace.yaml +0 -13
- package/templates/skeleton/node-service/scripts/build-artifact.mjs +0 -34
- package/templates/skeleton/node-service/services/api/package.json +0 -17
- package/templates/skeleton/node-service/services/api/src/adapters/spool-publisher.ts +0 -23
- package/templates/skeleton/node-service/services/api/src/handlers/create-note.ts +0 -40
- package/templates/skeleton/node-service/services/api/src/handlers/list-notes.ts +0 -23
- package/templates/skeleton/node-service/services/api/src/main.ts +0 -40
- package/templates/skeleton/node-service/services/api/src/server.ts +0 -172
- package/templates/skeleton/node-service/services/api/src/static-dir.ts +0 -20
- package/templates/skeleton/node-service/services/api/src/usecases/create-note.ts +0 -30
- package/templates/skeleton/node-service/services/api/src/usecases/list-notes.ts +0 -14
- package/templates/skeleton/node-service/services/api/test/artifact.test.ts +0 -92
- package/templates/skeleton/node-service/services/api/test/create-note.handler.test.ts +0 -64
- package/templates/skeleton/node-service/services/api/test/create-note.usecase.test.ts +0 -43
- package/templates/skeleton/node-service/services/api/test/list-notes.test.ts +0 -48
- package/templates/skeleton/node-service/services/api/test/package-manager.test.ts +0 -40
- package/templates/skeleton/node-service/services/api/test/package-manager.ts +0 -51
- package/templates/skeleton/node-service/services/api/test/server.test.ts +0 -208
- package/templates/skeleton/node-service/services/api/test/spool-publisher.test.ts +0 -32
- package/templates/skeleton/node-service/services/api/test/static-dir.test.ts +0 -29
- package/templates/skeleton/node-service/services/worker/package.json +0 -16
- package/templates/skeleton/node-service/services/worker/src/main.ts +0 -28
- package/templates/skeleton/node-service/services/worker/src/spool.ts +0 -60
- package/templates/skeleton/node-service/services/worker/src/usecases/process-note-created.ts +0 -38
- package/templates/skeleton/node-service/services/worker/test/process-note-created.test.ts +0 -34
- package/templates/skeleton/node-service/services/worker/test/spool.test.ts +0 -76
- package/templates/skeleton/node-service/tsconfig.base.json +0 -15
- package/templates/skeleton/node-service/tsconfig.json +0 -13
- package/templates/skeleton/node-service/vitest.config.ts +0 -12
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
# AGENTS.md is canonical; CLAUDE.md is a shim
|
|
2
|
+
|
|
3
|
+
The rule lives in `AGENTS.md`, under "One operating system, two harnesses".
|
|
4
|
+
This file explains the model behind it, what is and is not claimed about
|
|
5
|
+
Claude Code's own `AGENTS.md` support, and what an existing rig's upgrade does
|
|
6
|
+
with a `CLAUDE.md`/`AGENTS.md` pair it already has (RP-186).
|
|
7
|
+
|
|
8
|
+
## The model
|
|
9
|
+
|
|
10
|
+
Before this change, `templates/agent-os/universal/CLAUDE.md` was the authored
|
|
11
|
+
source and `AGENTS.md` was a byte-identical Codex-facing projection of it,
|
|
12
|
+
mechanically generated by `scripts/sync-codex-adapter.mjs`. That inverted the
|
|
13
|
+
provider-neutral framing the rest of this rulebook states everywhere else:
|
|
14
|
+
the rules are not Claude Code's, and authoring them under a Claude-Code-named
|
|
15
|
+
file made `AGENTS.md` — a name several tools other than Claude Code already
|
|
16
|
+
read — the copy.
|
|
17
|
+
|
|
18
|
+
`AGENTS.md` is now the canonical, provider-neutral source: the full rulebook
|
|
19
|
+
text lives there, once, and the generator authors it in that file directly.
|
|
20
|
+
`CLAUDE.md` is a short compatibility shim: a Claude Code `@AGENTS.md` import
|
|
21
|
+
(see "Claude Code's own import syntax" below) plus anything genuinely
|
|
22
|
+
specific to Claude Code — nothing that duplicates the rulebook. Neither file
|
|
23
|
+
is generated from the other any more; both are authored tree files under
|
|
24
|
+
`templates/agent-os/universal/`, and `scripts/sync-agent-os.mjs` composes each
|
|
25
|
+
one (elevated paths and this repository's addendum go into `AGENTS.md` only)
|
|
26
|
+
independently.
|
|
27
|
+
|
|
28
|
+
## What is claimed about Claude Code reading AGENTS.md — and what is not
|
|
29
|
+
|
|
30
|
+
**Verified against Anthropic's Claude Code documentation** (`docs.claude.com`
|
|
31
|
+
→ `code.claude.com/docs/en/memory`, "AGENTS.md" section, read 2026-09-20):
|
|
32
|
+
|
|
33
|
+
- Claude Code can read a repository's `AGENTS.md` directly, without a
|
|
34
|
+
`CLAUDE.md`, starting with Claude Code v2.1.277.
|
|
35
|
+
- That support is conditional, not universal. Paraphrased from the docs (this
|
|
36
|
+
rulebook names no cloud provider — `test/template/composition.test.ts`):
|
|
37
|
+
in some sessions, such as ones on certain third-party model-hosting
|
|
38
|
+
providers or with telemetry disabled, Claude Code cannot read `AGENTS.md`
|
|
39
|
+
at all — and separately, whenever a `CLAUDE.md`
|
|
40
|
+
(or a same-named file nested under a project's `.claude` directory, or
|
|
41
|
+
`CLAUDE.local.md`) exists anywhere in the working directory or above it,
|
|
42
|
+
Claude Code reads that file **instead of** `AGENTS.md` by default (the
|
|
43
|
+
`claude-md-or-agents-md` project-instructions setting, the default).
|
|
44
|
+
- The documented remedy for exactly this situation is the pattern this
|
|
45
|
+
project ships: *"When Claude isn't reading your AGENTS.md directly, you can
|
|
46
|
+
still keep it as the one file every tool shares by putting an `@AGENTS.md`
|
|
47
|
+
import in a CLAUDE.md next to it… Add any Claude-specific instructions
|
|
48
|
+
below the import, and Claude reads the imported file first, then the
|
|
49
|
+
rest"* — with the worked example:
|
|
50
|
+
|
|
51
|
+
```markdown
|
|
52
|
+
@AGENTS.md
|
|
53
|
+
|
|
54
|
+
## Claude Code
|
|
55
|
+
|
|
56
|
+
Use plan mode for changes under `src/billing/`.
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
This project's `CLAUDE.md` template follows that exact shape.
|
|
60
|
+
|
|
61
|
+
**What this project does NOT claim**, stated because the shim's own text and
|
|
62
|
+
this record both have to survive a reader who takes them literally:
|
|
63
|
+
|
|
64
|
+
- It does **not** claim Claude Code always reads `AGENTS.md` on its own. It
|
|
65
|
+
might, depending on version and configuration — the shim exists precisely
|
|
66
|
+
for the sessions and configurations where it does not, and costs nothing
|
|
67
|
+
when it does (the docs state the import is never read twice).
|
|
68
|
+
- It does **not** claim `AGENTS.md` appears in `/memory` or `/context` when
|
|
69
|
+
Claude reads it directly through the setting — the docs say it does not,
|
|
70
|
+
and point at the `AGENTS.md loaded` session-start line instead.
|
|
71
|
+
- The `@AGENTS.md` import syntax itself: relative and absolute paths are both
|
|
72
|
+
allowed, imports are expanded at launch, and recursive imports are capped
|
|
73
|
+
at four hops — none of which this project's shim relies on beyond the one,
|
|
74
|
+
first-line import.
|
|
75
|
+
|
|
76
|
+
## The migration (RP-186)
|
|
77
|
+
|
|
78
|
+
A rig installed before this change has a `CLAUDE.md`/`AGENTS.md` pair that is
|
|
79
|
+
byte-identical and recorded as such in its manifest (`.claude/.rig-manifest.json`
|
|
80
|
+
— see `packages/cli/src/lib/manifest.ts`). Both files are ordinary
|
|
81
|
+
manifest-tracked paths (`MAPS` in `packages/cli/src/commands/init.ts`), so
|
|
82
|
+
`upgrade`/`uninstall` need no special-cased migration logic for the
|
|
83
|
+
*ordinary* per-file cases — the same hash-based verdict machinery that
|
|
84
|
+
decides `update` / `conflict` / `deleted` for every other tracked file
|
|
85
|
+
governs both, independently. One coupling IS special-cased: CLAUDE.md's own
|
|
86
|
+
verdict is never allowed to become `update` — writing the `@AGENTS.md`
|
|
87
|
+
shim — while the on-disk AGENTS.md cannot actually SERVE as the rulebook.
|
|
88
|
+
|
|
89
|
+
**Round 5 design ruling (replacing round 4's verdict-only rule, gate cycle 4
|
|
90
|
+
blocker 1): the hold condition is CONTENT, not verdict.** CLAUDE.md is held
|
|
91
|
+
back only when AGENTS.md is absent (`deleted`), or present but not a
|
|
92
|
+
readable rulebook — no fenced ` ```elevated-paths ` block at all, or one
|
|
93
|
+
that parses to nothing once comments and blank lines are stripped (see
|
|
94
|
+
`packages/cli/src/lib/elevated-paths.ts`'s `isReadableRulebook`, mirroring
|
|
95
|
+
the gate sweep's own `parseElevatedPaths` in
|
|
96
|
+
`templates/agent-os/universal/.claude/scripts/detect-missed-gate.mjs` — a
|
|
97
|
+
script that ships only with the opt-in workflow layer, so this Core rule is
|
|
98
|
+
implemented independently of it rather than importing it — kept in step by a
|
|
99
|
+
correspondence test). An AGENTS.md the user CUSTOMISED but that
|
|
100
|
+
still carries a non-empty `elevated-paths` block lets the shim through — the
|
|
101
|
+
shim then imports the user's own rulebook, which is the point of it — and
|
|
102
|
+
AGENTS.md itself stays a perfectly ordinary, QUIET `conflict`: the same
|
|
103
|
+
`edited since it was installed` line every other kept file gets, no rescue
|
|
104
|
+
file, no special wording anywhere. This matters because a customised
|
|
105
|
+
AGENTS.md is not a broken rulebook — it is this project's own SHIPPED,
|
|
106
|
+
DESIGNED steady state: `AGENTS.md` itself tells every project to extend its
|
|
107
|
+
`elevated-paths` block for its own paths. Round 4's rule kept AGENTS.md's
|
|
108
|
+
own *verdict* (`conflict` or `deleted`) as the trigger, which made every
|
|
109
|
+
customisation indistinguishable from a genuinely broken rulebook — measured
|
|
110
|
+
directly against the real pre-RP-186 payload with one added path line: every
|
|
111
|
+
`upgrade` said "the migration is NOT finished", wrote a rescue file that
|
|
112
|
+
carried NONE of the user's own lines, and closed on `mv AGENTS.md.rig-new
|
|
113
|
+
AGENTS.md` — following it would have deleted user-authored rulebook content,
|
|
114
|
+
and merging into it and `mv`-ing looped forever.
|
|
115
|
+
|
|
116
|
+
When genuinely held back, `upgrade` keeps CLAUDE.md as its current bytes,
|
|
117
|
+
verdict `conflict`, with a reason naming AGENTS.md's own state and pointing
|
|
118
|
+
at the rescue-file section below for the exact remedy (round 5, blocker 2:
|
|
119
|
+
the reason text never hardcodes `mv` — the actual remedy depends on the
|
|
120
|
+
rescue file's own status, decided separately, and a fixed `mv` promise here
|
|
121
|
+
survived even the case where `mv` would be wrong).
|
|
122
|
+
|
|
123
|
+
**Held back re-vouches, and that has an `uninstall` consequence (PR #241
|
|
124
|
+
round 3, blocker 2; measured correct in round 4).** Holding CLAUDE.md back
|
|
125
|
+
does not merely skip writing it — it also records `sha256` of its CURRENT
|
|
126
|
+
bytes into the next manifest (`nextFiles['CLAUDE.md']`), the same way an
|
|
127
|
+
ordinary `update` would record the new bytes. This is deliberate (see "The
|
|
128
|
+
remedy" below: it is what lets a LATER `upgrade` resolve cleanly instead of
|
|
129
|
+
falling through to "not a version this rig ever released" forever) — but it
|
|
130
|
+
means `uninstall`, which reads `manifest.files` exactly like every other
|
|
131
|
+
rig-owned path, now sees a held-back CLAUDE.md as rig-owned and unedited
|
|
132
|
+
since, and **removes it**, precisely as it would any other untouched file.
|
|
133
|
+
It does not "leave it in place" the way a genuine `conflict` (bytes the
|
|
134
|
+
manifest never vouched for) does. Pinned by `packages/cli/test/upgrade.test.ts`'s
|
|
135
|
+
test "uninstall removes a held-back CLAUDE.md — the re-vouch makes it
|
|
136
|
+
rig-owned again" (absent in a generated rig — `packages/cli/test/` ships no
|
|
137
|
+
test into the payload, so a mention of it is not a citation this rulebook's
|
|
138
|
+
own checker, `evidence-pointers.test.ts`, examines: it reads `test/` and
|
|
139
|
+
`templates/agent-os/`, never `packages/`).
|
|
140
|
+
|
|
141
|
+
**Round 4, blocker 2 (CLI-UX): that removal is now disclosed, not silent.**
|
|
142
|
+
Removing CLAUDE.md while AGENTS.md is not a clean removal (preserved as
|
|
143
|
+
edited, or already gone) — or the other way around — is exactly the moment
|
|
144
|
+
the rulebook could end up with no readable copy left at all, and a bare
|
|
145
|
+
`- CLAUDE.md` line did not say so. `UninstallAction` now carries an optional
|
|
146
|
+
`note` (distinct from `reason`, which stays `preserved`-only) for this one
|
|
147
|
+
case, set at PLAN time so `--dry-run` and a real run print it identically,
|
|
148
|
+
and carried in `--json` as a `notes` array alongside `preserved`. Pinned by
|
|
149
|
+
`packages/cli/test/uninstall.test.ts`'s tests "discloses that removing
|
|
150
|
+
CLAUDE.md leaves AGENTS.md, preserved as edited, as the only rulebook copy",
|
|
151
|
+
"discloses the same thing in the opposite direction when AGENTS.md is the
|
|
152
|
+
one being removed", "discloses when the sibling is already gone (absent),
|
|
153
|
+
not only when it is preserved as edited", "adds no note when both are a
|
|
154
|
+
clean removal", and "the disclosure is identical on --dry-run and a real
|
|
155
|
+
run — set at plan time, never at apply time" (absent in a generated rig,
|
|
156
|
+
same reason as above).
|
|
157
|
+
|
|
158
|
+
| Case | Upgrade outcome | Uninstall outcome |
|
|
159
|
+
| --- | --- | --- |
|
|
160
|
+
| Both untouched since install | Both `update`: `CLAUDE.md` becomes the shim, `AGENTS.md` becomes canonical | Both removed (rig-owned, unedited); no `note` — a clean removal on both sides |
|
|
161
|
+
| `CLAUDE.md` edited by the user, AGENTS.md untouched | `conflict`: kept exactly as the user left it, never force-shimmed; if the edited content is not already the `@AGENTS.md` shim, the reason also says it shadows AGENTS.md (Claude Code reads CLAUDE.md instead, by default) and names the fix; AGENTS.md upgrades independently | `CLAUDE.md` left in place — its own `conflict` verdict is never recorded in `manifest.files` (`upgrade.ts`, the per-file loop's `conflict` branch: "deliberately NOT recorded in `files`"), so `planUninstall` gives it no action at all; it is simply never touched. AGENTS.md removed WITH a `note`, measured verbatim: `this is the rig's own AGENTS.md — removing it leaves CLAUDE.md, which exists and is yours (untracked by this rig), as the only rulebook copy` — the untracked-but-present sibling wording, pinned by `packages/cli/test/uninstall.test.ts`'s test "when CLAUDE.md is the untracked sibling after a real upgrade dropped its conflict verdict from the manifest, the AGENTS.md removal note says it EXISTS — never "absent" (mirror direction)" (absent in a generated rig, same reason as above) |
|
|
162
|
+
| `AGENTS.md` **customised WITH a readable `elevated-paths` block**, CLAUDE.md untouched (round 5's central case — the shipped rulebook's own designed steady state) | AGENTS.md `conflict`, perfectly QUIET — the ordinary `edited since it was installed` line, nothing else. CLAUDE.md is **NOT held back**: verdict `update`, the shim IS adopted, and it imports the user's own customised rulebook | CLAUDE.md removed WITH a `note` (never held back, so never re-vouched — same as any ordinary untouched shim), measured verbatim: `this is the rig's own CLAUDE.md — removing it leaves AGENTS.md, which exists and is yours (untracked by this rig), as the only rulebook copy` — pinned by `packages/cli/test/uninstall.test.ts`'s test "when the sibling is untracked by the manifest (present on disk, never recorded), says it EXISTS — never "absent"" (absent in a generated rig, same reason as above). AGENTS.md itself is left in place — an ordinary, untracked `conflict`, never an uninstall action at all |
|
|
163
|
+
| `AGENTS.md` edited WITHOUT a readable `elevated-paths` block (no block at all, or one that parses to nothing), CLAUDE.md untouched | AGENTS.md `conflict`: kept exactly as the user left it, never overwritten with the canonical text. CLAUDE.md is **held back**: verdict `conflict` (not `update`), kept as its current (old, still-readable) bytes, never written as the shim | AGENTS.md left in place — untracked `conflict`, no uninstall action. **CLAUDE.md is REMOVED** — the hold re-vouches it, so it reads as rig-owned and unedited — WITH a `note`, measured verbatim: `this is the rig's own CLAUDE.md — removing it leaves AGENTS.md, which exists and is yours (untracked by this rig), as the only rulebook copy` — pinned by `packages/cli/test/uninstall.test.ts`'s test "when the sibling is untracked by the manifest (present on disk, never recorded), says it EXISTS — never "absent"" (absent in a generated rig, same reason as above) |
|
|
164
|
+
| `AGENTS.md` deleted by the user, CLAUDE.md untouched | AGENTS.md `deleted`: stays deleted. CLAUDE.md is **held back** the same way, for the same reason (there is no rulebook file at all otherwise) | AGENTS.md stays absent — a genuinely absent path the manifest still records a hash for (`upgrade.ts`'s `deleted` branch keeps `nextFiles['AGENTS.md']` pointing at the recognised, pre-deletion hash, so it stays a manifest-tracked path rather than dropping out the way an ordinary `conflict` does); the `upgrade` REPORT — not the manifest — is what labels it `you removed (left removed)`, in its own summary line (`index.ts`). **CLAUDE.md is REMOVED**, for the same re-vouching reason as the row above — WITH a `note`, measured verbatim: `this is the rig's own CLAUDE.md — removing it leaves AGENTS.md, which is already gone, as the only rulebook copy` — the genuinely-absent sibling wording, pinned by `packages/cli/test/uninstall.test.ts`'s test "discloses when the sibling is already gone (absent), not only when it is preserved as edited" (absent in a generated rig, same reason as above) |
|
|
165
|
+
| `CLAUDE.md` deleted by the user | `deleted`: stays deleted, never restored as the new shim; unaffected by AGENTS.md's own state | already absent |
|
|
166
|
+
| `AGENTS.md` deleted by the user, CLAUDE.md deleted too | Both `deleted`: stays deleted on both sides — there is nothing left to hold back | already absent |
|
|
167
|
+
| The migration already finished (CLAUDE.md is already the shim), THEN AGENTS.md is deleted on a later run | AGENTS.md `deleted`: stays deleted. CLAUDE.md's own verdict is `unchanged` — the held-back coupling above only overrides a verdict that would otherwise become `update`, and an already-adopted shim's verdict never is, so it is left exactly as it is rather than resurrected, rewritten, or held back a second time | CLAUDE.md (the shim) removed like any other untouched file — WITH a `note`, measured verbatim: `this is the rig's own CLAUDE.md — removing it leaves AGENTS.md, which is already gone, as the only rulebook copy` — pinned by `packages/cli/test/uninstall.test.ts`'s test "discloses when the sibling is already gone (absent), not only when it is preserved as edited" (absent in a generated rig, same reason as above); AGENTS.md already absent. Upgrade side pinned by `packages/cli/test/upgrade.test.ts`'s test "AGENTS.md deleted after the migration already finished: the already-adopted shim is left exactly alone" (absent in a generated rig, same reason as above) |
|
|
168
|
+
| A repo that had its own `CLAUDE.md` (or `AGENTS.md`) before `init` | `init` **refuses outright** (`InitError`, non-zero exit) rather than installing over it or recording it as `kept` — `MAPS` files are the one install-time exception to the generic "kept" path every other pre-existing process file gets. Pinned by `packages/cli/test/init.test.ts`'s tests "refuses to clobber an existing CLAUDE.md unless forced" and "refuses to clobber an existing AGENTS.md" — named by full path deliberately: `test/e2e/init.test.ts` (a different suite) shares the bare basename `init.test.ts`, and either mention is absent in a generated rig for the same reason as above regardless | N/A — `init` never installed here, so there is nothing for `uninstall` to have owned |
|
|
169
|
+
|
|
170
|
+
The round-5 rule was re-derived and pinned as a 4×3 grid — AGENTS.md's axis
|
|
171
|
+
split into `pristine` / `edited-with-block` / `edited-without-block` /
|
|
172
|
+
`deleted`, crossed with CLAUDE.md's `pristine` / `edited` / `deleted` — each
|
|
173
|
+
cell's expected verdicts and hold-back flag hand-derived from the rule
|
|
174
|
+
itself, never copied from `upgrade.ts`'s own branching, by
|
|
175
|
+
`packages/cli/test/upgrade.test.ts`'s parametrised test "AGENTS.md %s ×
|
|
176
|
+
CLAUDE.md %s" and its own central case "AGENTS.md customised WITH a readable
|
|
177
|
+
elevated-paths block: the shim is adopted, not held back, and no rescue file
|
|
178
|
+
appears" (absent in a generated rig, same reason as above). Verified AT THE
|
|
179
|
+
CLI BOUNDARY against a customised, still-readable AGENTS.md by
|
|
180
|
+
`packages/cli/test/cli-report.test.ts`'s test "fresh init, then a path line
|
|
181
|
+
added to elevated-paths: upgrade --yes is silent about it, exit 0, AGENTS.md
|
|
182
|
+
unchanged" (same absence), and against the real pre-RP-186 payload by
|
|
183
|
+
`test/e2e/agents-md-migration.test.ts` (absent in a generated rig) › "a
|
|
184
|
+
legacy rig whose AGENTS.md was customised (still a readable rulebook) before
|
|
185
|
+
upgrading: the shim is adopted in one run, and a second run is a no-op".
|
|
186
|
+
|
|
187
|
+
Round 4, blocker 1's rescue file (below) is invisible to the table above — it
|
|
188
|
+
is never a manifest-tracked path — so `uninstall` treats it out of band: a
|
|
189
|
+
byte-identical copy of what THIS release would render right now is
|
|
190
|
+
`remove`d like any other untouched file; anything else is the user's,
|
|
191
|
+
`preserved`. Pinned by `packages/cli/test/uninstall.test.ts`'s tests "treats
|
|
192
|
+
a byte-identical rescue file as removable" and "leaves a differing rescue
|
|
193
|
+
file alone, reported as preserved — not this release's own bytes" (absent
|
|
194
|
+
in a generated rig, same reason as above).
|
|
195
|
+
|
|
196
|
+
## The remedy that actually works — measured, not assumed (PR #241 rounds 3–5)
|
|
197
|
+
|
|
198
|
+
AGENTS.md's own conflict names a fix, but three readings of it do not work,
|
|
199
|
+
and this project measured each rather than asserting any of them:
|
|
200
|
+
|
|
201
|
+
- **Copying the file the conflict's own `new version:` line points at.**
|
|
202
|
+
That path is the RAW template — still carrying the unsubstituted
|
|
203
|
+
per-project placeholder (`substituteContent` replaces it by exact match;
|
|
204
|
+
see `packages/cli/src/lib/substitute.ts` — spelled out here would itself
|
|
205
|
+
be substituted away by this repo's own dogfood sync, which is exactly why
|
|
206
|
+
it is not), because templates are substituted at install/upgrade time,
|
|
207
|
+
not at rest. Pasting it verbatim gives Claude Code the wrong project name.
|
|
208
|
+
(Round 5: the raw template DOES still carry a real, non-empty
|
|
209
|
+
`elevated-paths` block, so under the content-based hold rule it no longer
|
|
210
|
+
latches the hold the way it used to — but it is still the wrong remedy,
|
|
211
|
+
since it hands the project the wrong name throughout its own rulebook.)
|
|
212
|
+
- **Restoring "the exact bytes the rig installed".** Once AGENTS.md is
|
|
213
|
+
already a conflict, the manifest no longer records what those bytes were
|
|
214
|
+
(the generic rule: "a conflict is not recorded" — deliberately unchanged
|
|
215
|
+
by this ticket, see the top of this section) — there is nothing left on
|
|
216
|
+
disk or in the manifest to restore TO, and this record makes no claim
|
|
217
|
+
about whether any specific past release's bytes would be recognised
|
|
218
|
+
another way (that would need its own test, not an assertion here).
|
|
219
|
+
- **Round 3's own remedy: printing the rendered content to stdout for a
|
|
220
|
+
verbatim paste.** Gate cycle 3 measured this directly, with the built CLI,
|
|
221
|
+
and it does not work either: pasting the block verbatim, or de-indented,
|
|
222
|
+
left AGENTS.md at `conflict` and CLAUDE.md held back regardless — nothing
|
|
223
|
+
about a terminal's own line-wrapping and a human's own copy-paste survives
|
|
224
|
+
byte-for-byte reliably enough for `planUpgrade`'s exact-bytes comparison.
|
|
225
|
+
The dump was also ~270 lines sitting between the plan and the consent
|
|
226
|
+
prompt, scrolling the actionable lines off screen, and the run ended on
|
|
227
|
+
"Wrote N files." even while CLAUDE.md stayed held back — a bare success
|
|
228
|
+
line over an unfinished migration.
|
|
229
|
+
- **Round 4's own remedy: a rescue file keyed on AGENTS.md's verdict alone.**
|
|
230
|
+
Gate cycle 4 measured this against the real pre-RP-186 payload and found
|
|
231
|
+
it did the wrong thing for the project's own designed steady state (a
|
|
232
|
+
customised, still-readable AGENTS.md) — see "Round 5 design ruling" above
|
|
233
|
+
— and, separately, that its `differs` case still closed on `mv`: pasting a
|
|
234
|
+
hostile or merely stale pre-existing rescue file over AGENTS.md as if this
|
|
235
|
+
run had written or verified it.
|
|
236
|
+
|
|
237
|
+
**The fix that is always reachable, with no dependency on release history,
|
|
238
|
+
a terminal, or a human's paste at all — and that only ever appears in the
|
|
239
|
+
GENUINELY held-back state (round 5):** `create-agent-rig upgrade` WRITES the
|
|
240
|
+
already-rendered (substituted) content for AGENTS.md — the same bytes
|
|
241
|
+
`UpgradePlan.contents` carries — to a real sibling file, `AGENTS_MD_RESCUE`
|
|
242
|
+
(`AGENTS.md.rig-new`, `packages/cli/src/commands/upgrade.ts`), through the
|
|
243
|
+
same symlink-refusing write path (`resolveWritableInside`) every other file
|
|
244
|
+
in the plan goes through. It is never recorded in the manifest. The rescue
|
|
245
|
+
file's status is decided ENTIRELY AT PLAN TIME (round 5, blocker 3) —
|
|
246
|
+
`would-write` | `identical` | `differs` | `unsafe` — so a dry run and a real
|
|
247
|
+
run report the identical status, and `applyUpgrade` refuses `unsafe`
|
|
248
|
+
(a symlink, a directory, or similar sitting at the path) BEFORE touching
|
|
249
|
+
anything else at all, including the ordinary payload writes and the
|
|
250
|
+
manifest — closing gate cycle 4's own finding that a symlinked rescue file
|
|
251
|
+
used to leave other files rewritten and the manifest stale, and that a
|
|
252
|
+
directory there crashed with an uncaught `EISDIR`.
|
|
253
|
+
|
|
254
|
+
The closing instruction is printed as a short, delimited block, LAST, after
|
|
255
|
+
the plan, after any wiring hand-over, after "Wrote N files." — the previous
|
|
256
|
+
remedy's placement between the plan and the consent prompt is exactly why
|
|
257
|
+
cycle 3 found "Wrote N files." reading as success while the migration was
|
|
258
|
+
not finished. **`mv` is printed ONLY for `would-write` and `identical`** —
|
|
259
|
+
bytes this run either just wrote or independently verified byte-identical to
|
|
260
|
+
its own rendering. **For `differs`, the remedy is `rm` (or restoring
|
|
261
|
+
AGENTS.md some other way), never `mv`** (round 5, blocker 2: printing `mv`
|
|
262
|
+
there installs bytes this run never wrote or verified as the live rulebook
|
|
263
|
+
the moment the instruction is followed literally). A leftover, byte-identical
|
|
264
|
+
rescue file is cleaned up once AGENTS.md resolves and the rig is NOT held
|
|
265
|
+
back; unrelated clutter at the path (a stray directory or symlink) on an
|
|
266
|
+
otherwise healthy rig is never touched and never even mentioned (round 5,
|
|
267
|
+
blocker 3's own "must not affect upgrade at all").
|
|
268
|
+
|
|
269
|
+
Verified AT THE CLI BOUNDARY — the spawned, built binary, never
|
|
270
|
+
`plan.contents` as the oracle for "the bytes are right" (that circularity was
|
|
271
|
+
cycle 3's finding about round 3's own test) — by
|
|
272
|
+
`packages/cli/test/cli-report.test.ts`'s describe block "AGENTS.md.rig-new —
|
|
273
|
+
the CLI-boundary remedy for a GENUINELY held-back AGENTS.md", including "the
|
|
274
|
+
rescue file is byte-identical to an independently-rendered AGENTS.md for the
|
|
275
|
+
same project — not `plan.contents`" (the independent oracle is a SEPARATE
|
|
276
|
+
`initProject` call, forced to the same project name), "never overwrites a
|
|
277
|
+
pre-existing AGENTS.md.rig-new that differs from the rendered bytes, and
|
|
278
|
+
NEVER prints `mv` for it", "a symlinked AGENTS.md.rig-new in the held-back
|
|
279
|
+
state: exit 1, a clean message, zero files changed", and "a directory at
|
|
280
|
+
AGENTS.md.rig-new in the held-back state: exit 1, a clean message, no
|
|
281
|
+
EISDIR crash" (absent in a generated rig, same reason as above). The
|
|
282
|
+
plan-time state machine itself is pinned at the `planUpgrade`/`applyUpgrade`
|
|
283
|
+
level by `packages/cli/test/upgrade.test.ts`'s describe blocks "the
|
|
284
|
+
AGENTS.md.rig-new rescue file — GENUINELY held back only (round 5)" and
|
|
285
|
+
"AGENTS.md.rig-new clutter on a HEALTHY (never held back) rig must not
|
|
286
|
+
affect upgrade at all" (same absence). And the whole remedy is followed end
|
|
287
|
+
to end against the real pre-RP-186 payload — not a fixture — by
|
|
288
|
+
`test/e2e/agents-md-migration.test.ts` (absent in a generated rig) › "a
|
|
289
|
+
legacy rig whose AGENTS.md was edited before upgrading: CLAUDE.md is held
|
|
290
|
+
back, never shimmed over an unreadable rulebook — and the rescue file
|
|
291
|
+
resolves it end to end".
|
|
292
|
+
|
|
293
|
+
Also see the byte-identity tests this ticket replaced, listed in the PR
|
|
294
|
+
description as an old-test → new-test table.
|
|
295
|
+
|
|
296
|
+
## What did not change
|
|
297
|
+
|
|
298
|
+
- Provider-specific wiring stays provider-specific: `.claude/settings.json`,
|
|
299
|
+
`.claude/agents/`, `.codex/hooks.json`, `.codex/config.toml` are untouched
|
|
300
|
+
by this ticket and are never duplicated into either rulebook file.
|
|
301
|
+
- The `elevated-paths` block moved with the canonical text: it is declared in
|
|
302
|
+
`AGENTS.md` now, not `CLAUDE.md`. Every mechanical reader of that block —
|
|
303
|
+
`readDeclaredPaths`, and through it the PR-lane router and the queue's own
|
|
304
|
+
tier bookkeeping, all of them part of the opt-in workflow layer and named
|
|
305
|
+
by the core rulebook only to describe what that layer adds — reads
|
|
306
|
+
`AGENTS.md` first and `CLAUDE.md` second, so a not-yet-migrated rig (or a
|
|
307
|
+
test fixture that still writes the block into a `CLAUDE.md`) is not
|
|
308
|
+
silently un-declared.
|
|
309
|
+
- Zero new dependencies; the project-name token this generator substitutes
|
|
310
|
+
everywhere else is unaffected (it is not written literally here — this
|
|
311
|
+
file is itself substituted); no application skeleton, benchmark, agent-bus
|
|
312
|
+
or mandatory Memory
|
|
313
|
+
came back.
|
|
@@ -4,15 +4,20 @@ Status: accepted for AR-113; subagent routing extended for RP-166.
|
|
|
4
4
|
|
|
5
5
|
## Decision
|
|
6
6
|
|
|
7
|
-
Claude-shaped files remain the authoring surface
|
|
8
|
-
|
|
9
|
-
wiring (`.codex/`); the generated files are checked
|
|
10
|
-
repository and are materialised into a
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
7
|
+
Claude-shaped files remain the authoring surface for skills, custom agents and
|
|
8
|
+
hook wiring. The generator derives the Codex skills (`.agents/`), custom
|
|
9
|
+
agents and hook wiring (`.codex/`) from them; the generated files are checked
|
|
10
|
+
for drift in the generator repository and are materialised into a generated
|
|
11
|
+
project as its local, versioned operating system. The rulebook itself is the
|
|
12
|
+
one exception, since RP-186: `AGENTS.md` is the canonical, authored,
|
|
13
|
+
provider-neutral source — not derived from `CLAUDE.md` — and `CLAUDE.md` is a
|
|
14
|
+
short, separately authored compatibility shim (`@AGENTS.md` import plus
|
|
15
|
+
Claude-Code-specific notes; see `docs/decisions/agents-md-canonical.md`). A
|
|
16
|
+
generated project does not ship the generator's projector for the derived
|
|
17
|
+
files. Its Claude and Codex files are versioned snapshots: a downstream
|
|
18
|
+
project that deliberately changes one side must either make the equivalent
|
|
19
|
+
local change on the other side or take a newer generated release. There is no
|
|
20
|
+
downstream automatic drift check.
|
|
16
21
|
|
|
17
22
|
## Why
|
|
18
23
|
|
|
@@ -26,7 +31,7 @@ table, and the projector refuses a missing or orphaned agent profile. It also
|
|
|
26
31
|
refuses duplicate source-agent names across layers, since one profile name
|
|
27
32
|
cannot route two definitions.
|
|
28
33
|
|
|
29
|
-
Frequent bounded work (`test-writer`, `prose-reviewer`) uses `gpt-5.6-terra`;
|
|
34
|
+
Frequent bounded work (`test-writer`, `implementation-agent`, `prose-reviewer`) uses `gpt-5.6-terra`;
|
|
30
35
|
correctness, security, and infrastructure gates use `gpt-5.6-sol`. Every named
|
|
31
36
|
gate uses `high` reasoning effort. Unnamed subagents inherit repository defaults
|
|
32
37
|
of `gpt-5.6-terra` and `medium` from `.codex/config.toml`. `xhigh` is not a
|
|
@@ -4,10 +4,13 @@ The rule lives in `.claude/rules/workflow.md`, under "PR flow". This file
|
|
|
4
4
|
explains the one part of it that reads like an inconsistency and is not, so that
|
|
5
5
|
nobody "tidies" the two sets into agreement. It is not loaded into any session.
|
|
6
6
|
|
|
7
|
-
## The sets
|
|
7
|
+
## The sets (opt-in workflow layer)
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
|
|
9
|
+
All three mechanisms below — the gate sweep, the router, and the queue's
|
|
10
|
+
elevated ration — ship with the opt-in workflow layer (`init --layer
|
|
11
|
+
workflow`); a Core-only rig applies the same rulebook exemptions by hand,
|
|
12
|
+
since none of these scripts exist for it to run. Three different mechanisms
|
|
13
|
+
each carry their own notion of "this file is only words":
|
|
11
14
|
|
|
12
15
|
**The first two carve out the rulebook**, and only then differ; the third runs
|
|
13
16
|
*after* the sweep has already answered, so it sees only what survived:
|
|
@@ -29,7 +32,7 @@ test in the rig belongs there. The router's sets stay where they are, for the
|
|
|
29
32
|
reason this whole record exists.
|
|
30
33
|
|
|
31
34
|
**Neither row applies to a decision record, including this one.** Both
|
|
32
|
-
predicates recognise `CLAUDE.md` anywhere, everything under `.claude/`, and —
|
|
35
|
+
predicates recognise `CLAUDE.md` and `AGENTS.md` anywhere, everything under `.claude/`, and —
|
|
33
36
|
since these records were extracted — everything under `docs/decisions/`, through
|
|
34
37
|
the one shared `isDecisionRecord`. A change here escalates the sweep and takes
|
|
35
38
|
the `model` lane, exactly as the rule it explains would. The router's call is
|
|
@@ -58,10 +61,11 @@ is one of the risk flags — and its extension happens to be `.txt`.
|
|
|
58
61
|
|
|
59
62
|
Copying the sweep's `.mdx` into the router's prose set would put executable MDX
|
|
60
63
|
back on the prose lane. MDX carries components and imports; it is a program that
|
|
61
|
-
renders, not a document that is read. The ration
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
64
|
+
renders, not a document that is read. The ration (workflow layer, like the
|
|
65
|
+
mechanism it rations) reads it the same way and for the same reason
|
|
66
|
+
(`spacing-rations-mechanisms.md`, also workflow layer): calling it prose there
|
|
67
|
+
would clear the spacing hold on a file this rig treats as a program, on the
|
|
68
|
+
permissive side.
|
|
65
69
|
|
|
66
70
|
## The shape of the mistake this prevents
|
|
67
71
|
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
# SessionStart hook output: one JSON wire format, both harnesses
|
|
2
|
+
|
|
3
|
+
Status: accepted for RP-185.
|
|
4
|
+
|
|
5
|
+
## Decision
|
|
6
|
+
|
|
7
|
+
`inject-rules.mjs` (the SessionStart hook that re-injects the autonomy rules on
|
|
8
|
+
startup, resume and compaction) prints one JSON object to stdout, unconditionally,
|
|
9
|
+
for both Claude Code and Codex:
|
|
10
|
+
|
|
11
|
+
```json
|
|
12
|
+
{"hookSpecificOutput":{"hookEventName":"SessionStart","additionalContext":"…"}}
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
No trailing newline, no other top-level fields, no provider branching. The
|
|
16
|
+
`additionalContext` value carries exactly the text the hook used to write
|
|
17
|
+
directly to stdout (the `[agent-os] …` banner, the notice, the excerpted rules).
|
|
18
|
+
|
|
19
|
+
## Why
|
|
20
|
+
|
|
21
|
+
Measured, 2026-09-17, Codex 0.154.0 on Windows: with the previous plain-text
|
|
22
|
+
output — stdout beginning with the literal characters `[agent-os] Autonomy
|
|
23
|
+
rules refresh …` — Codex printed `Hook failed └ hook returned invalid session
|
|
24
|
+
start JSON output`, and the autonomy refresh never reached the session's
|
|
25
|
+
context. Claude Code was unaffected by that same plain-text form. A second
|
|
26
|
+
SessionStart hook in the same measured session, one that already emitted
|
|
27
|
+
`{"hookSpecificOutput":{"hookEventName":"SessionStart","additionalContext":…}}`,
|
|
28
|
+
did reach the session — which is what pointed at the wire format rather than at
|
|
29
|
+
"Codex rejects plain text" as the fix.
|
|
30
|
+
|
|
31
|
+
That framing matters because it is not what Codex's own documentation says.
|
|
32
|
+
[Codex's hooks reference](https://learn.chatgpt.com/docs/hooks) states plainly
|
|
33
|
+
that plain text on stdout is accepted for `session_start` and is added as
|
|
34
|
+
developer context — so "Codex does not support plain-text SessionStart output"
|
|
35
|
+
would be false, and this file does not claim it. What the page does not
|
|
36
|
+
document is *how* Codex decides a given stdout is JSON rather than plain text,
|
|
37
|
+
or what happens when that decision goes the wrong way. The measured symptom —
|
|
38
|
+
an output whose first character is `[` reported as *invalid* JSON, rather than
|
|
39
|
+
silently read as plain text — is consistent with a sniff that treats a leading
|
|
40
|
+
`[` (or `{`) as a signal to attempt a JSON parse, and reports a failure rather
|
|
41
|
+
than falling back when that parse does not succeed. That mechanism is inferred
|
|
42
|
+
from the symptom, not read off the page, and is stated here as an inference,
|
|
43
|
+
not a documented fact.
|
|
44
|
+
|
|
45
|
+
## Why the JSON envelope, not a reworded plain-text banner
|
|
46
|
+
|
|
47
|
+
The alternative fix — keep printing plain text, just not starting with `[` —
|
|
48
|
+
would have worked too, but it leaves the same undocumented sniffing behaviour
|
|
49
|
+
one character choice away from breaking again, for a reason nobody would think
|
|
50
|
+
to look for. The JSON form sidesteps the ambiguity entirely rather than
|
|
51
|
+
tip-toeing around it, and it turns out to need no provider branching, because
|
|
52
|
+
both harnesses already document the identical shape:
|
|
53
|
+
|
|
54
|
+
- **Codex** ([learn.chatgpt.com/docs/hooks](https://learn.chatgpt.com/docs/hooks)):
|
|
55
|
+
gives the worked example
|
|
56
|
+
`{"hookSpecificOutput":{"hookEventName":"SessionStart","additionalContext":"Load the workspace conventions before editing."}}`
|
|
57
|
+
as JSON output for a `session_start` hook.
|
|
58
|
+
- **Claude Code** ([code.claude.com/docs/en/hooks](https://code.claude.com/docs/en/hooks)):
|
|
59
|
+
documents the same nested shape for `SessionStart` — `hookSpecificOutput`
|
|
60
|
+
with `hookEventName` (must equal `"SessionStart"`) and `additionalContext` —
|
|
61
|
+
and states that JSON is detected when stdout starts with `{` and ends with
|
|
62
|
+
`}`; anything else is treated as plain text and added to context the same
|
|
63
|
+
way. `systemMessage` and `terminalSequence` are also accepted fields there,
|
|
64
|
+
unused here because nothing today needs them.
|
|
65
|
+
|
|
66
|
+
One shape, honoured by both harnesses' own documentation, is what
|
|
67
|
+
`invariants.md` ("One mechanism, one implementation") asks for whenever a
|
|
68
|
+
single source can serve two consumers — the alternative, a provider check that
|
|
69
|
+
picks plain text for one harness and JSON for the other, would be exactly the
|
|
70
|
+
kind of guessed, undocumented branching this repository's rules warn against
|
|
71
|
+
("never invent APIs or behavior").
|
|
72
|
+
|
|
73
|
+
## What this does not claim
|
|
74
|
+
|
|
75
|
+
- Not that Codex rejects plain text in general — its own docs say otherwise.
|
|
76
|
+
- Not a documented account of Codex's JSON-vs-plain-text sniffing — that
|
|
77
|
+
mechanism is not published; only the fix (always emit the one documented
|
|
78
|
+
JSON shape) is asserted, not the internal reason it was needed.
|
|
79
|
+
- Not that `source: "startup"`, `"resume"` and `"compact"` are guaranteed
|
|
80
|
+
identical by either harness's spec beyond what each page states; both are
|
|
81
|
+
silent on any per-source difference in output handling, and the hook applies
|
|
82
|
+
the same output uniformly across all three because nothing in either
|
|
83
|
+
contract says to do otherwise.
|
|
84
|
+
|
|
85
|
+
## The exit path: exitCode vs exit(), and what it trades
|
|
86
|
+
|
|
87
|
+
The envelope change alone re-armed the original defect at a different trigger.
|
|
88
|
+
`inject-rules.mjs` ended with `process.exit(main())`, and `process.exit()` tears
|
|
89
|
+
the process down without waiting for a queued `stdout.write()` to drain. Under
|
|
90
|
+
the old plain-text wire format a write a pipe's buffer could not hold in one
|
|
91
|
+
piece degraded to *partial rules text* — readable, if incomplete. Under the
|
|
92
|
+
JSON envelope the same truncation is *invalid JSON* — precisely the state
|
|
93
|
+
Codex was measured rejecting wholesale, just moved from "the output starts
|
|
94
|
+
with `[`" to "the output was cut off mid-object". Exit code 0 either way, so
|
|
95
|
+
nothing downstream reports it.
|
|
96
|
+
|
|
97
|
+
Reproduced independently by two review passes at HEAD before the fix: a
|
|
98
|
+
consumer that does not start reading until well after the child would have
|
|
99
|
+
exited loses everything past the pipe's buffer — at one measurement, a 74 KB
|
|
100
|
+
rules file delivered 0 bytes; at another, 65536 of 73893. Both parse as
|
|
101
|
+
`Unterminated string`.
|
|
102
|
+
|
|
103
|
+
The fix is `process.exitCode = main()` in place of `process.exit(main())`.
|
|
104
|
+
Every path through `main()` returns `0`, so the exit STATUS does not change.
|
|
105
|
+
What changes is whether the process terminates before the write finishes:
|
|
106
|
+
`exitCode` lets Node's event loop drain naturally, which is what lets a large
|
|
107
|
+
payload actually reach a reader. Verified against the same probe shape that
|
|
108
|
+
found the defect, at four payload sizes (7 083 B through 1 002 552 B) and
|
|
109
|
+
three consumer shapes (a non-draining reader, a slow reader at 4 KiB/50 ms,
|
|
110
|
+
and a plain file redirect): every case delivered the complete envelope and
|
|
111
|
+
parsed. Pinned in the generator's `hooks.test.ts` (absent in a generated rig)
|
|
112
|
+
› "delivers the whole envelope even when the reader does not drain until
|
|
113
|
+
process.exit(main()) would already have torn the process down", which goes
|
|
114
|
+
red (`Unterminated string`, at a byte count that is host-dependent — kernel
|
|
115
|
+
pipe buffer size and scheduler timing both vary) if the single line is
|
|
116
|
+
reverted. The pinned payload is sized for a deterministic kill rather than a
|
|
117
|
+
merely likely one: a smaller payload truncated on nearly every reversion run
|
|
118
|
+
on every host checked, but not every one, and a pin the defect can slip
|
|
119
|
+
through occasionally is a pin that will eventually be green on a real
|
|
120
|
+
revert. That pin's coverage is Linux-shaped: Node documents pipe writes as
|
|
121
|
+
synchronous on Windows and asynchronous on POSIX, so the same reversion is
|
|
122
|
+
expected to have little or nothing to catch on a Windows lane — the test's
|
|
123
|
+
own comment says so, so a future reader does not mistake a Linux-only kill
|
|
124
|
+
for cross-platform cover.
|
|
125
|
+
|
|
126
|
+
**What this trades away, stated plainly rather than left to be discovered:**
|
|
127
|
+
`process.exit()` also GUARANTEED teardown, and `exitCode` does not. A consumer
|
|
128
|
+
that never reads stdout at all no longer gets a fast, wrong exit 0 — it gets
|
|
129
|
+
a hook that stays alive indefinitely, waiting on the write. Measured: still
|
|
130
|
+
running 8 seconds in in one review's reproduction, at 74 KB and 1 MB payloads
|
|
131
|
+
with nobody draining; completing the instant a reader appeared. A probe built
|
|
132
|
+
to refuse reading until the child would already have exited measurably
|
|
133
|
+
DEADLOCKS this version, where `process.exit()` would have terminated
|
|
134
|
+
(truncated, but terminated). Nothing in this file bounds that wait — the
|
|
135
|
+
calling harness's own hook timeout does. Not reachable at the size this hook
|
|
136
|
+
ships today (a few KB, well under a second to write), but a real behaviour
|
|
137
|
+
change on a project whose `autonomy.md` grows large, or whose harness stops
|
|
138
|
+
reading a hook's stdout entirely. The trade is made on purpose: a loud hang
|
|
139
|
+
bounded by the harness's own timeout is preferred over a silent, truncated
|
|
140
|
+
"success" with no bound on how wrong it can be.
|
|
141
|
+
|
|
142
|
+
A second, smaller consequence of the same change: a reader that vanishes
|
|
143
|
+
MID-write (a closed pipe, a harness that kills this process before reading)
|
|
144
|
+
now surfaces as an unhandled `error` event on `process.stdout` — exit 1 with
|
|
145
|
+
a Node stack trace on stderr, where the old `process.exit()` path exited 0
|
|
146
|
+
silently in the same situation. The handler is narrow rather than blanket,
|
|
147
|
+
and the difference is measured, not theoretical: an earlier draft of this
|
|
148
|
+
fix silenced every stdout error unconditionally, and security review found
|
|
149
|
+
that with stdout redirected to `/dev/full` — a genuine write failure
|
|
150
|
+
(ENOSPC), with the reader still fully attached — that blanket form exited 0
|
|
151
|
+
with nothing delivered and no diagnostic, the exact silent-loss shape this
|
|
152
|
+
whole file exists to avoid, moved one write call over. The handler now
|
|
153
|
+
distinguishes the two: EPIPE (the reader is gone; there is nothing left to
|
|
154
|
+
report to) stays silent, and anything else is written to stderr and marks
|
|
155
|
+
the exit non-zero. Pinned in the generator's `hooks.test.ts` (absent in a
|
|
156
|
+
generated rig) › "silently exits 0 when the reader is gone before the write
|
|
157
|
+
starts (EPIPE)" and › "reports a genuine stdout write failure on stderr and
|
|
158
|
+
marks the exit non-zero, rather than looking like a healthy session".
|
|
159
|
+
|
|
160
|
+
**Left for a separate decision, not for this one:** seven sibling hooks in
|
|
161
|
+
this same directory still end in `process.exit(…)` with no wait for a
|
|
162
|
+
pending write — `block-no-verify.mjs`, `guard-rulebook.mjs`,
|
|
163
|
+
`guard-subagent-model.mjs`, `guard-bash.mjs`, `guard-secret-file.mjs`
|
|
164
|
+
(`process.exit(status)`), `gate-stop-dod.mjs` (`process.exit(code)`) and
|
|
165
|
+
`warn-subagent-routing.mjs` (the first four and the last end
|
|
166
|
+
`process.exit(main())`). Their payloads are short (a refusal message, not a
|
|
167
|
+
whole rules file), so the exposure is far smaller, but the reasoning above
|
|
168
|
+
now lives in one hook's comments only — `invariants.md`'s "one mechanism,
|
|
169
|
+
one implementation" would ask for the same pattern everywhere it applies.
|
|
170
|
+
This change deliberately does not touch the other seven: changing every
|
|
171
|
+
`process.exit()` call in the hooks directory in a PR whose stated purpose is
|
|
172
|
+
a SessionStart wire-format fix is exactly the scope creep `autonomy.md`'s
|
|
173
|
+
Tier-2 discipline exists to catch. Recorded here so the inconsistency is a
|
|
174
|
+
known, named backlog item rather than something the next reader has to
|
|
175
|
+
rediscover.
|
|
176
|
+
|
|
177
|
+
## Risk and rollback
|
|
178
|
+
|
|
179
|
+
Tier 2 (`templates/agent-os/universal/.claude/hooks/` is a declared elevated
|
|
180
|
+
path) for both decisions this record carries, each with its own risk and its
|
|
181
|
+
own rollback:
|
|
182
|
+
|
|
183
|
+
**The wire format.** The blast radius is narrow: this hook's own stdout
|
|
184
|
+
contract, read only by the two harnesses' SessionStart machinery. If either
|
|
185
|
+
harness's documented shape turns out to differ from what was fetched here, or
|
|
186
|
+
a future harness version stops accepting it, rollback is reverting
|
|
187
|
+
`inject-rules.mjs` to write plain text again — a one-line change, the same
|
|
188
|
+
one this decision replaces. The old plain-text form is pinned by a regression
|
|
189
|
+
test precisely so it is not reintroduced by accident while chasing an
|
|
190
|
+
unrelated fix: the generator's `hooks.test.ts` (absent in a generated rig) ›
|
|
191
|
+
"never regresses to the old bare [agent-os]-prefixed plain-text stdout".
|
|
192
|
+
|
|
193
|
+
**The exit path** ("The exit path: exitCode vs exit(), and what it trades",
|
|
194
|
+
above). The blast radius is this hook's shutdown behaviour, not its output
|
|
195
|
+
shape: a consumer that never drains stdout at all now holds this process
|
|
196
|
+
alive rather than letting it exit truncated, bounded only by the calling
|
|
197
|
+
harness's own hook timeout. If that trade turns out to be wrong — a harness
|
|
198
|
+
with no such timeout, or one where a hung hook process is worse than a
|
|
199
|
+
truncated one — rollback is reverting the single `process.exitCode = main()`
|
|
200
|
+
line to `process.exit(main())`, independently of the wire-format decision
|
|
201
|
+
above; the two lines do not depend on each other. That reintroduces the
|
|
202
|
+
flush defect this record measures, so a revert of this line alone should
|
|
203
|
+
also remove or explicitly override the test that pins it: the generator's
|
|
204
|
+
`hooks.test.ts` (absent in a generated rig) › "delivers the whole envelope
|
|
205
|
+
even when the reader does not drain until process.exit(main()) would already
|
|
206
|
+
have torn the process down".
|
|
@@ -11,7 +11,7 @@ Each named subagent this rig ships pins the model and the effort it reads with:
|
|
|
11
11
|
| Role | Claude Code | Codex |
|
|
12
12
|
| --- | --- | --- |
|
|
13
13
|
| `code-reviewer`, `security-scanner`, a stack's infrastructure reviewer | `claude-opus-5`, `high` | `gpt-5.6-sol`, `high` |
|
|
14
|
-
| `test-writer`, `prose-reviewer` | `claude-sonnet-5`, `high` | `gpt-5.6-terra`, `high` |
|
|
14
|
+
| `test-writer`, `implementation-agent`, `prose-reviewer` | `claude-sonnet-5`, `high` | `gpt-5.6-terra`, `high` |
|
|
15
15
|
| a subagent with no definition | `claude-sonnet-5`; effort follows the session | `gpt-5.6-terra`, `medium` |
|
|
16
16
|
|
|
17
17
|
In this project the pins are the files themselves: `model:` and `effort:` in
|
|
@@ -93,8 +93,11 @@ environment: `subagent-routing.test.ts` (absent in a generated rig) ›
|
|
|
93
93
|
In a generated project the definitions are the policy (next section), so an agent
|
|
94
94
|
a project adds with a pin is a role, and a role whose definition is changed to
|
|
95
95
|
`model: inherit` has stopped being one — by that project's reviewed decision.
|
|
96
|
-
- **The built-in agents get no definitions here.**
|
|
97
|
-
|
|
96
|
+
- **The built-in agents get no definitions here.** Ordinary implementation has its
|
|
97
|
+
own role, `implementation-agent`, on the same tier as `test-writer`: frequent,
|
|
98
|
+
bounded work, not the reviewer tier. A general-purpose subagent remains for
|
|
99
|
+
ad-hoc work no role covers, and follows the unnamed default like any subagent
|
|
100
|
+
without a definition.
|
|
98
101
|
- **The warning is a warning.** A session started with `CLAUDE_CODE_EFFORT_LEVEL` set
|
|
99
102
|
still runs every gate — at that level.
|
|
100
103
|
|