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.
Files changed (234) hide show
  1. package/CHANGELOG.md +419 -3
  2. package/README.md +267 -305
  3. package/package.json +8 -14
  4. package/packages/cli/dist/commands/create.js +56 -88
  5. package/packages/cli/dist/commands/doctor.js +213 -0
  6. package/packages/cli/dist/commands/init.js +111 -50
  7. package/packages/cli/dist/commands/integrations.js +468 -0
  8. package/packages/cli/dist/commands/setup-wizard.js +61 -0
  9. package/packages/cli/dist/commands/uninstall.js +1223 -0
  10. package/packages/cli/dist/commands/upgrade.js +472 -90
  11. package/packages/cli/dist/index.js +651 -46
  12. package/packages/cli/dist/integrations/declaration.js +158 -0
  13. package/packages/cli/dist/integrations/doctor-guards.js +126 -0
  14. package/packages/cli/dist/integrations/doctor-workflow.js +25 -0
  15. package/packages/cli/dist/integrations/mcp-json.js +107 -0
  16. package/packages/cli/dist/integrations/memory-doctor.js +124 -0
  17. package/packages/cli/dist/integrations/registry.js +22 -0
  18. package/packages/cli/dist/integrations/spawn.js +228 -0
  19. package/packages/cli/dist/integrations/spec-kit.js +280 -0
  20. package/packages/cli/dist/integrations/verify.js +193 -0
  21. package/packages/cli/dist/integrations/windows-job.js +275 -0
  22. package/packages/cli/dist/lib/elevated-paths.js +74 -0
  23. package/packages/cli/dist/lib/install-set.js +1 -44
  24. package/packages/cli/dist/lib/manifest.js +64 -5
  25. package/packages/cli/dist/lib/prompts.js +0 -23
  26. package/packages/cli/dist/lib/safe-path.js +193 -2
  27. package/packages/cli/dist/lib/safe-text.js +78 -0
  28. package/packages/cli/dist/lib/substitute.js +1 -52
  29. package/packages/cli/dist/lib/summary.js +5 -5
  30. package/packages/cli/dist/templates.js +4 -14
  31. package/scripts/prepare.mjs +28 -12
  32. package/templates/agent-os/subagent-routing.json +4 -4
  33. package/templates/agent-os/universal/.agents/skills/check-premises/SKILL.md +13 -9
  34. package/templates/agent-os/universal/.agents/skills/loop/SKILL.md +31 -6
  35. package/templates/agent-os/universal/.agents/skills/new-invariant/SKILL.md +5 -5
  36. package/templates/agent-os/universal/.agents/skills/pr-ship/SKILL.md +1 -1
  37. package/templates/agent-os/universal/.agents/skills/worktree-task/SKILL.md +3 -3
  38. package/templates/agent-os/universal/.claude/agents/code-reviewer.md +8 -6
  39. package/templates/agent-os/universal/.claude/agents/implementation-agent.md +41 -0
  40. package/templates/agent-os/universal/.claude/agents/prose-reviewer.md +8 -4
  41. package/templates/agent-os/universal/.claude/agents/security-scanner.md +4 -2
  42. package/templates/agent-os/universal/.claude/agents/test-writer.md +2 -2
  43. package/templates/agent-os/universal/.claude/hooks/inject-rules.mjs +83 -4
  44. package/templates/agent-os/universal/.claude/rules/autonomy.md +55 -27
  45. package/templates/agent-os/universal/.claude/rules/invariants.md +9 -8
  46. package/templates/agent-os/universal/.claude/rules/workflow.md +47 -22
  47. package/templates/agent-os/universal/.claude/scripts/decision-router.mjs +1 -1
  48. package/templates/agent-os/universal/.claude/scripts/detect-missed-gate.mjs +12 -9
  49. package/templates/agent-os/universal/.claude/scripts/doctor.mjs +100 -2
  50. package/templates/agent-os/universal/.claude/scripts/lib/claim-records.mjs +236 -6
  51. package/templates/agent-os/universal/.claude/scripts/queue/state.mjs +2 -2
  52. package/templates/agent-os/universal/.claude/scripts/reconcile-external-prs.mjs +4 -3
  53. package/templates/agent-os/universal/.claude/scripts/revalidate.mjs +16 -0
  54. package/templates/agent-os/universal/.claude/settings.json +0 -8
  55. package/templates/agent-os/universal/.claude/skills/check-premises/SKILL.md +13 -9
  56. package/templates/agent-os/universal/.claude/skills/loop/SKILL.md +31 -6
  57. package/templates/agent-os/universal/.claude/skills/new-invariant/SKILL.md +5 -5
  58. package/templates/agent-os/universal/.claude/skills/pr-ship/SKILL.md +1 -1
  59. package/templates/agent-os/universal/.claude/skills/worktree-task/SKILL.md +3 -3
  60. package/templates/agent-os/universal/.codex/agents/code-reviewer.toml +2 -2
  61. package/templates/agent-os/universal/.codex/agents/implementation-agent.toml +6 -0
  62. package/templates/agent-os/universal/.codex/agents/prose-reviewer.toml +2 -2
  63. package/templates/agent-os/universal/.codex/agents/security-scanner.toml +1 -1
  64. package/templates/agent-os/universal/.codex/agents/test-writer.toml +1 -1
  65. package/templates/agent-os/universal/.codex/hooks.json +0 -10
  66. package/templates/agent-os/universal/AGENTS.md +208 -107
  67. package/templates/agent-os/universal/CLAUDE.md +15 -162
  68. package/templates/agent-os/universal/PLAN.md +17 -10
  69. package/templates/agent-os/universal/docs/decisions/agents-md-canonical.md +313 -0
  70. package/templates/agent-os/universal/docs/decisions/codex-adapter.md +15 -10
  71. package/templates/agent-os/universal/docs/decisions/review-lanes.md +12 -8
  72. package/templates/agent-os/universal/docs/decisions/session-start-wire-format.md +206 -0
  73. package/templates/agent-os/universal/docs/decisions/subagent-routing.md +6 -3
  74. package/templates/agent-os/universal/docs/decisions/workflow-layer-split.md +235 -0
  75. package/templates/agent-os/universal/layers.json +25 -30
  76. package/templates/hash-history.json +8 -4
  77. package/templates/release-ledger.json +2 -1
  78. package/packages/cli/dist/lib/composition.js +0 -20
  79. package/packages/cli/dist/lib/targets.js +0 -28
  80. package/packages/cli/dist/policy/benchmark/corpus.js +0 -165
  81. package/packages/cli/dist/policy/core/adapter.js +0 -18
  82. package/packages/cli/dist/policy/core/coverage.js +0 -253
  83. package/packages/cli/dist/policy/core/decision-record.js +0 -287
  84. package/packages/cli/dist/policy/core/declaration.js +0 -127
  85. package/packages/cli/dist/policy/core/evidence-matrix.js +0 -94
  86. package/packages/cli/dist/policy/core/probe.js +0 -442
  87. package/packages/cli/dist/policy/core/registry.js +0 -115
  88. package/packages/cli/dist/policy/core/validation.js +0 -275
  89. package/packages/cli/dist/policy/core/vocabulary.js +0 -123
  90. package/packages/cli/dist/policy/harness/claude.js +0 -47
  91. package/packages/cli/dist/policy/harness/codex.js +0 -87
  92. package/packages/cli/dist/policy/harness/index.js +0 -15
  93. package/packages/cli/dist/policy/harness/shared-hooks.js +0 -28
  94. package/packages/cli/dist/policy/index.js +0 -17
  95. package/templates/agent-os/init/AGENTS.md +0 -201
  96. package/templates/agent-os/init/CLAUDE.md +0 -201
  97. package/templates/agent-os/stack/aws-cdk/.agents/skills/post-deploy-verify/SKILL.md +0 -105
  98. package/templates/agent-os/stack/aws-cdk/.agents/skills/ro-debug/SKILL.md +0 -117
  99. package/templates/agent-os/stack/aws-cdk/.claude/agents/cdk-diff-reviewer.md +0 -89
  100. package/templates/agent-os/stack/aws-cdk/.claude/rules/aws-cdk.md +0 -105
  101. package/templates/agent-os/stack/aws-cdk/.claude/skills/post-deploy-verify/SKILL.md +0 -105
  102. package/templates/agent-os/stack/aws-cdk/.claude/skills/ro-debug/SKILL.md +0 -117
  103. package/templates/agent-os/stack/aws-cdk/.codex/agents/cdk-diff-reviewer.toml +0 -6
  104. package/templates/agent-os/stack/node-ts/.claude/hooks/dod-checks.json +0 -1
  105. package/templates/agent-os/stack/node-ts/.claude/rules/node-ts.md +0 -85
  106. package/templates/agent-os/universal/.claude/hooks/guard-core-purity.mjs +0 -89
  107. package/templates/agent-os/universal/.claude/hooks/guard-web-boundary.mjs +0 -68
  108. package/templates/agent-os/universal/.claude/rules/architecture.md +0 -81
  109. package/templates/skeleton/aws-serverless/.github/workflows/ci.yml +0 -28
  110. package/templates/skeleton/aws-serverless/.github/workflows/deploy.yml +0 -90
  111. package/templates/skeleton/aws-serverless/README.md +0 -181
  112. package/templates/skeleton/aws-serverless/apps/web/next.config.mjs +0 -17
  113. package/templates/skeleton/aws-serverless/apps/web/package.json +0 -19
  114. package/templates/skeleton/aws-serverless/apps/web/src/app/layout.tsx +0 -17
  115. package/templates/skeleton/aws-serverless/apps/web/src/app/page.tsx +0 -96
  116. package/templates/skeleton/aws-serverless/apps/web/src/lib/api.ts +0 -36
  117. package/templates/skeleton/aws-serverless/apps/web/src/lib/validate.ts +0 -23
  118. package/templates/skeleton/aws-serverless/apps/web/test/shared-validation.test.ts +0 -38
  119. package/templates/skeleton/aws-serverless/apps/web/tsconfig.json +0 -14
  120. package/templates/skeleton/aws-serverless/eslint.config.mjs +0 -20
  121. package/templates/skeleton/aws-serverless/gitignore +0 -56
  122. package/templates/skeleton/aws-serverless/infra/bin/app.ts +0 -79
  123. package/templates/skeleton/aws-serverless/infra/cdk.json +0 -3
  124. package/templates/skeleton/aws-serverless/infra/lib/app-stack.ts +0 -248
  125. package/templates/skeleton/aws-serverless/infra/lib/web-stack.ts +0 -45
  126. package/templates/skeleton/aws-serverless/infra/package.json +0 -18
  127. package/templates/skeleton/aws-serverless/infra/test/allowed-origins.test.ts +0 -301
  128. package/templates/skeleton/aws-serverless/infra/test/app-composition.test.ts +0 -137
  129. package/templates/skeleton/aws-serverless/infra/test/app-stack.test.ts +0 -120
  130. package/templates/skeleton/aws-serverless/infra/test/web-stack.test.ts +0 -44
  131. package/templates/skeleton/aws-serverless/package.json +0 -30
  132. package/templates/skeleton/aws-serverless/packages/core/package.json +0 -11
  133. package/templates/skeleton/aws-serverless/packages/core/src/events.ts +0 -14
  134. package/templates/skeleton/aws-serverless/packages/core/src/index.ts +0 -15
  135. package/templates/skeleton/aws-serverless/packages/core/src/note.ts +0 -69
  136. package/templates/skeleton/aws-serverless/packages/core/test/events.test.ts +0 -23
  137. package/templates/skeleton/aws-serverless/packages/core/test/note.test.ts +0 -101
  138. package/templates/skeleton/aws-serverless/packages/db/package.json +0 -14
  139. package/templates/skeleton/aws-serverless/packages/db/src/client.ts +0 -17
  140. package/templates/skeleton/aws-serverless/packages/db/src/index.ts +0 -2
  141. package/templates/skeleton/aws-serverless/packages/db/src/note-model.ts +0 -52
  142. package/templates/skeleton/aws-serverless/packages/db/test/note-model.test.ts +0 -91
  143. package/templates/skeleton/aws-serverless/packages/shared/package.json +0 -11
  144. package/templates/skeleton/aws-serverless/packages/shared/src/env.ts +0 -17
  145. package/templates/skeleton/aws-serverless/packages/shared/src/errors.ts +0 -33
  146. package/templates/skeleton/aws-serverless/packages/shared/src/index.ts +0 -3
  147. package/templates/skeleton/aws-serverless/packages/shared/src/logger.ts +0 -20
  148. package/templates/skeleton/aws-serverless/packages/shared/test/env.test.ts +0 -26
  149. package/templates/skeleton/aws-serverless/packages/shared/test/errors.test.ts +0 -28
  150. package/templates/skeleton/aws-serverless/packages/shared/test/logger.test.ts +0 -19
  151. package/templates/skeleton/aws-serverless/pnpm-lock.yaml +0 -2855
  152. package/templates/skeleton/aws-serverless/pnpm-workspace.yaml +0 -14
  153. package/templates/skeleton/aws-serverless/services/api/package.json +0 -15
  154. package/templates/skeleton/aws-serverless/services/api/src/adapters/sqs-publisher.ts +0 -26
  155. package/templates/skeleton/aws-serverless/services/api/src/handlers/create-note.ts +0 -69
  156. package/templates/skeleton/aws-serverless/services/api/src/handlers/list-notes.ts +0 -37
  157. package/templates/skeleton/aws-serverless/services/api/src/list-main.ts +0 -12
  158. package/templates/skeleton/aws-serverless/services/api/src/main.ts +0 -21
  159. package/templates/skeleton/aws-serverless/services/api/src/usecases/create-note.ts +0 -41
  160. package/templates/skeleton/aws-serverless/services/api/src/usecases/list-notes.ts +0 -14
  161. package/templates/skeleton/aws-serverless/services/api/test/create-note.handler.test.ts +0 -211
  162. package/templates/skeleton/aws-serverless/services/api/test/create-note.usecase.test.ts +0 -45
  163. package/templates/skeleton/aws-serverless/services/api/test/list-notes.test.ts +0 -122
  164. package/templates/skeleton/aws-serverless/services/api/test/sqs-publisher.test.ts +0 -22
  165. package/templates/skeleton/aws-serverless/services/worker/package.json +0 -12
  166. package/templates/skeleton/aws-serverless/services/worker/src/handlers/note-created.ts +0 -15
  167. package/templates/skeleton/aws-serverless/services/worker/src/main.ts +0 -7
  168. package/templates/skeleton/aws-serverless/services/worker/src/usecases/process-note-created.ts +0 -37
  169. package/templates/skeleton/aws-serverless/services/worker/test/note-created.test.ts +0 -61
  170. package/templates/skeleton/aws-serverless/tsconfig.base.json +0 -15
  171. package/templates/skeleton/aws-serverless/tsconfig.json +0 -16
  172. package/templates/skeleton/aws-serverless/vitest.config.ts +0 -14
  173. package/templates/skeleton/node-service/.github/workflows/ci.yml +0 -27
  174. package/templates/skeleton/node-service/.github/workflows/deploy.yml +0 -29
  175. package/templates/skeleton/node-service/README.md +0 -104
  176. package/templates/skeleton/node-service/apps/web/next.config.mjs +0 -17
  177. package/templates/skeleton/node-service/apps/web/package.json +0 -19
  178. package/templates/skeleton/node-service/apps/web/src/app/layout.tsx +0 -17
  179. package/templates/skeleton/node-service/apps/web/src/app/page.tsx +0 -96
  180. package/templates/skeleton/node-service/apps/web/src/lib/api.ts +0 -29
  181. package/templates/skeleton/node-service/apps/web/src/lib/validate.ts +0 -23
  182. package/templates/skeleton/node-service/apps/web/test/shared-validation.test.ts +0 -38
  183. package/templates/skeleton/node-service/apps/web/tsconfig.json +0 -14
  184. package/templates/skeleton/node-service/eslint.config.mjs +0 -20
  185. package/templates/skeleton/node-service/gitignore +0 -54
  186. package/templates/skeleton/node-service/package.json +0 -30
  187. package/templates/skeleton/node-service/packages/core/package.json +0 -11
  188. package/templates/skeleton/node-service/packages/core/src/events.ts +0 -14
  189. package/templates/skeleton/node-service/packages/core/src/index.ts +0 -15
  190. package/templates/skeleton/node-service/packages/core/src/note.ts +0 -69
  191. package/templates/skeleton/node-service/packages/core/test/events.test.ts +0 -23
  192. package/templates/skeleton/node-service/packages/core/test/note.test.ts +0 -101
  193. package/templates/skeleton/node-service/packages/db/package.json +0 -12
  194. package/templates/skeleton/node-service/packages/db/src/index.ts +0 -1
  195. package/templates/skeleton/node-service/packages/db/src/note-store.ts +0 -100
  196. package/templates/skeleton/node-service/packages/db/test/note-store.test.ts +0 -100
  197. package/templates/skeleton/node-service/packages/shared/package.json +0 -11
  198. package/templates/skeleton/node-service/packages/shared/src/env.ts +0 -17
  199. package/templates/skeleton/node-service/packages/shared/src/errors.ts +0 -33
  200. package/templates/skeleton/node-service/packages/shared/src/index.ts +0 -3
  201. package/templates/skeleton/node-service/packages/shared/src/logger.ts +0 -20
  202. package/templates/skeleton/node-service/packages/shared/test/env.test.ts +0 -26
  203. package/templates/skeleton/node-service/packages/shared/test/errors.test.ts +0 -28
  204. package/templates/skeleton/node-service/packages/shared/test/logger.test.ts +0 -19
  205. package/templates/skeleton/node-service/pnpm-lock.yaml +0 -2402
  206. package/templates/skeleton/node-service/pnpm-workspace.yaml +0 -13
  207. package/templates/skeleton/node-service/scripts/build-artifact.mjs +0 -34
  208. package/templates/skeleton/node-service/services/api/package.json +0 -17
  209. package/templates/skeleton/node-service/services/api/src/adapters/spool-publisher.ts +0 -23
  210. package/templates/skeleton/node-service/services/api/src/handlers/create-note.ts +0 -40
  211. package/templates/skeleton/node-service/services/api/src/handlers/list-notes.ts +0 -23
  212. package/templates/skeleton/node-service/services/api/src/main.ts +0 -40
  213. package/templates/skeleton/node-service/services/api/src/server.ts +0 -172
  214. package/templates/skeleton/node-service/services/api/src/static-dir.ts +0 -20
  215. package/templates/skeleton/node-service/services/api/src/usecases/create-note.ts +0 -30
  216. package/templates/skeleton/node-service/services/api/src/usecases/list-notes.ts +0 -14
  217. package/templates/skeleton/node-service/services/api/test/artifact.test.ts +0 -92
  218. package/templates/skeleton/node-service/services/api/test/create-note.handler.test.ts +0 -64
  219. package/templates/skeleton/node-service/services/api/test/create-note.usecase.test.ts +0 -43
  220. package/templates/skeleton/node-service/services/api/test/list-notes.test.ts +0 -48
  221. package/templates/skeleton/node-service/services/api/test/package-manager.test.ts +0 -40
  222. package/templates/skeleton/node-service/services/api/test/package-manager.ts +0 -51
  223. package/templates/skeleton/node-service/services/api/test/server.test.ts +0 -208
  224. package/templates/skeleton/node-service/services/api/test/spool-publisher.test.ts +0 -32
  225. package/templates/skeleton/node-service/services/api/test/static-dir.test.ts +0 -29
  226. package/templates/skeleton/node-service/services/worker/package.json +0 -16
  227. package/templates/skeleton/node-service/services/worker/src/main.ts +0 -28
  228. package/templates/skeleton/node-service/services/worker/src/spool.ts +0 -60
  229. package/templates/skeleton/node-service/services/worker/src/usecases/process-note-created.ts +0 -38
  230. package/templates/skeleton/node-service/services/worker/test/process-note-created.test.ts +0 -34
  231. package/templates/skeleton/node-service/services/worker/test/spool.test.ts +0 -76
  232. package/templates/skeleton/node-service/tsconfig.base.json +0 -15
  233. package/templates/skeleton/node-service/tsconfig.json +0 -13
  234. package/templates/skeleton/node-service/vitest.config.ts +0 -12
@@ -1,36 +1,53 @@
1
1
  #!/usr/bin/env node
2
+ import { readFile } from 'node:fs/promises';
3
+ import path from 'node:path';
2
4
  import { parseArgs } from 'node:util';
3
5
  import { CreateError, createProject } from './commands/create.js';
4
6
  import { InitError, initFileContents, initProject, planInit } from './commands/init.js';
7
+ import { INTEGRATIONS_VERBS, runIntegrationsCommand } from './commands/integrations.js';
8
+ import { runSetupWizard } from './commands/setup-wizard.js';
5
9
  import { execFileRunner, setupSubsystems } from './commands/setup.js';
6
- import { UpgradeError, applyUpgrade, planUpgrade } from './commands/upgrade.js';
10
+ import { AGENTS_MD_RESCUE, UpgradeError, applyUpgrade, planUpgrade } from './commands/upgrade.js';
11
+ import { CHANGED_SINCE_PLANNING_REASON, UninstallError, applyUninstall, isUnverifiedReason, planUninstall, protectedFileReason, } from './commands/uninstall.js';
7
12
  import { makePalette } from './lib/colors.js';
8
- import { readManifest } from './lib/manifest.js';
13
+ import { readManifest, sha256 } from './lib/manifest.js';
9
14
  import { SubsystemsError, refreshSubsystems, subsystemsManifestPath } from './lib/subsystems.js';
10
- import { promptConfirm, promptTarget } from './lib/prompts.js';
15
+ import { promptConfirm } from './lib/prompts.js';
11
16
  import { collectGovernance, renderSummary } from './lib/summary.js';
12
- import { DEFAULT_TARGET, TARGET_NAMES } from './lib/targets.js';
13
17
  import { packageVersion, rigHandshake } from './lib/version.js';
14
18
  import { runMemory } from './commands/memory.js';
19
+ import { runDoctor } from './commands/doctor.js';
15
20
  const USAGE = `Usage: create-agent-rig <dir> [options]
16
21
 
17
- Scaffolds a new project into <dir>: a Claude Code + Codex agent operating system
18
- plus a runnable code skeleton. Refuses to write into a non-empty directory.
22
+ Scaffolds a new directory into <dir>: \`mkdir\` + \`git init\` + the same install
23
+ \`init\` runs into an existing repo — a Claude Code + Codex agent operating
24
+ system, no application scaffolding. Refuses to write into a non-empty
25
+ directory.
19
26
 
20
27
  Options
21
- --target <name> ${TARGET_NAMES.join(' | ')}
22
- (interactive selection when omitted on a terminal;
23
- required when not a terminal — default: ${DEFAULT_TARGET})
24
28
  --no-git skip git init + the pristine-template baseline commit
29
+ --layer workflow also install the experimental, opt-in workflow layer (the
30
+ queue adapter, the loop and pr-ship skills, run-state,
31
+ journal, revalidation, claim-records, and the PR-lifecycle
32
+ helpers) — see init below; default is the core layer only.
33
+ "workflow" is the only accepted name — process/Core
34
+ installs unconditionally and is never named. Repeatable;
35
+ repeating the same name is harmless.
25
36
  --no-color plain output (NO_COLOR is respected too)
26
37
  --version print the version (--version --json: the contract handshake,
27
38
  one JSON object with the name, version and contract version)
28
39
  -h, --help this text
29
40
 
30
- Also: create-agent-rig init [--dry-run]
41
+ Also: create-agent-rig init [--dry-run] [--layer workflow]
31
42
  Install the process layer (rules, gates, stop rules — no architecture
32
43
  assumptions) into the CURRENT existing repo. Refuses to clobber CLAUDE.md
33
44
  or AGENTS.md.
45
+ --layer workflow also installs the experimental workflow layer: an
46
+ autonomous, cooperative multi-session queue/loop/PR-lifecycle mechanism,
47
+ never required by Lean Core. Without it, only the core layer is installed.
48
+ A rig that already has the workflow layer keeps it on a plain re-run with
49
+ no flag — the flag only ever adds the layer, never drops one a previous
50
+ run already recorded.
34
51
  --force is deprecated: it refuses and points at upgrade, which refreshes a
35
52
  rig file by file. It is removed in 0.6.
36
53
 
@@ -45,6 +62,53 @@ Also: create-agent-rig setup --memory-root <checkout> [--memory-ref <sha>] [--dr
45
62
  one declared root. Performs the --version --json handshake first and refuses
46
63
  a foreign contract major with exit 4 before writing anything.
47
64
 
65
+ Also: create-agent-rig setup list [--json]
66
+ List the supported repository integrations.
67
+
68
+ Also: create-agent-rig doctor [--json]
69
+ Diagnose installed Rig files, guards, provider wiring and available runtimes.
70
+ Does not apply setup plans or observe connectivity, authorization or trust.
71
+
72
+ Also: create-agent-rig setup
73
+ Choose a provider and harness interactively, then approve its exact plan.
74
+
75
+ Also: create-agent-rig setup add <id> [--harness <name>] [--adopt] [--dry-run] [--yes] [--json]
76
+ Plan MCP wiring or the pinned official Spec Kit lifecycle. Record intent and
77
+ ownership in .rig/integrations.json. JSON never prompts; writes require --yes.
78
+ --adopt explicitly adopts an existing external Spec Kit installation.
79
+
80
+ Also: create-agent-rig setup apply [id] [--dry-run] [--yes] [--json]
81
+ Apply declared integration intent. Refuse if files changed after planning.
82
+ Claude project wiring does not prove authorization, connectivity or trust.
83
+
84
+ Also: create-agent-rig setup remove <id> [--dry-run] [--yes] [--json]
85
+ Remove proven Rig-owned wiring, or delegate Spec Kit removal to its official
86
+ CLI. Preserve foreign wiring and provider data; retire intent after success.
87
+
88
+ Also: create-agent-rig uninstall [dir] [--dry-run] [--yes] [--detach] [--json]
89
+ Remove what a rig installed from [dir] (default: the current directory) —
90
+ only files whose bytes on disk still match what the manifest recorded, are
91
+ still one of the exact paths this release installs, and still match right
92
+ up to the moment each one is removed; anything under .git is refused
93
+ outright, whatever hash a manifest pairs it with. Everything else (edited,
94
+ foreign, deleted already, kept by init, changed since the plan was shown, or
95
+ not a path this release owns) is left in place and reported. The manifest
96
+ is removed last, and only once every removal succeeded AND nothing was
97
+ preserved — a preserved path means the rig still owns bytes it did not
98
+ remove, so the evidence naming them stays; a failed run also keeps it, so a
99
+ re-run picks up where it stopped. --detach removes the manifest anyway,
100
+ after the same safe cleanup, leaving every preserved path for you and
101
+ printing the full handover list — it never forces away a conflicting or
102
+ modified file. --json's payload names which of three outcomes a run
103
+ reached: "uninstalled" (clean), "partial" (something kept, manifest stays),
104
+ "detached" (--detach: manifest gone, a handover list left behind). Prints
105
+ the plan, then asks before removing anything: --yes answers up front
106
+ (required off a terminal, and always required with --json, which never
107
+ prompts) — the same consent rule applies to --detach. --json prints one
108
+ JSON object and nothing else on stdout (see docs/command-contract.md);
109
+ without it, uninstall reports in prose like init and upgrade. Idempotent: a
110
+ repeat run finds no manifest and does nothing, exit 0.
111
+
48
112
  Also: create-agent-rig memory <doctor|load> [args…]
49
113
  Run a Memory verb through the registered executable: the --version --json
50
114
  handshake first (a foreign contract major exits 4 and the verb never runs),
@@ -54,6 +118,54 @@ Also: create-agent-rig memory <doctor|load> [args…]
54
118
  60 s bound). No manifest answers unsupported/absent (exit 0); an invalid
55
119
  invocation exits 2.`;
56
120
  async function runSetup(rawArgs) {
121
+ // Integration verbs and legacy machine-scoped Memory registration are
122
+ // separate dispatch paths.
123
+ const verb = rawArgs[0];
124
+ if (INTEGRATIONS_VERBS.includes(verb ?? '')) {
125
+ const isInteractive = Boolean(process.stdin.isTTY && process.stderr.isTTY);
126
+ const result = await runIntegrationsCommand({
127
+ verb: verb,
128
+ args: rawArgs.slice(1),
129
+ cwd: process.cwd(),
130
+ isTTY: isInteractive,
131
+ confirm: async (plan) => {
132
+ process.stderr.write(`${plan}\n`);
133
+ return promptConfirm('Apply this integration plan?', {
134
+ input: process.stdin,
135
+ output: process.stderr,
136
+ isInteractive,
137
+ });
138
+ },
139
+ });
140
+ process.stdout.write(result.stdout);
141
+ process.stderr.write(result.stderr);
142
+ return result.exitCode;
143
+ }
144
+ if (!rawArgs.some((arg) => arg === '--memory-root' || arg.startsWith('--memory-root=')) &&
145
+ (verb === undefined || verb.startsWith('-'))) {
146
+ const isInteractive = Boolean(process.stdin.isTTY && process.stderr.isTTY);
147
+ const result = await runSetupWizard({
148
+ cwd: process.cwd(),
149
+ isTTY: isInteractive,
150
+ args: rawArgs,
151
+ confirm: async (plan) => {
152
+ process.stderr.write(`${plan}\n`);
153
+ return promptConfirm('Apply this integration plan?', {
154
+ input: process.stdin,
155
+ output: process.stderr,
156
+ isInteractive,
157
+ });
158
+ },
159
+ });
160
+ process.stdout.write(result.stdout);
161
+ process.stderr.write(result.stderr);
162
+ return result.exitCode;
163
+ }
164
+ // Do not echo an untrusted positional value into terminal diagnostics.
165
+ if (verb !== undefined && !verb.startsWith('-')) {
166
+ process.stderr.write(`Unknown setup verb: expected one of ${INTEGRATIONS_VERBS.join(', ')} (or --memory-root for the legacy path).\n`);
167
+ return 1;
168
+ }
57
169
  let values;
58
170
  try {
59
171
  ({ values } = parseArgs({
@@ -100,6 +212,27 @@ async function runSetup(rawArgs) {
100
212
  throw error;
101
213
  }
102
214
  }
215
+ /**
216
+ * `--layer <name>`, repeatable (RP-180 round 2: the owner's spelling is
217
+ * `--layer workflow`, never `--with-workflow`). `workflow` is the only name a
218
+ * user may opt into today — `process`/Core installs unconditionally and is
219
+ * never something a user names. Repeating the same name is harmless; naming
220
+ * anything else is a usage error the caller reports the same way it reports
221
+ * any other bad flag (message to stderr + USAGE, exit 1) — neither `init` nor
222
+ * the top-level `create` accepts `--json` today, so there is no JSON error
223
+ * shape to match here; this follows the one shape those two commands already
224
+ * use.
225
+ */
226
+ function resolveLayerFlag(layer) {
227
+ const names = layer ?? [];
228
+ const unknown = names.find((name) => name !== 'workflow');
229
+ if (unknown !== undefined) {
230
+ return {
231
+ error: `Unknown --layer "${unknown}" — the only accepted layer name is "workflow" (process/Core installs unconditionally and is never named).`,
232
+ };
233
+ }
234
+ return { withWorkflow: names.length > 0 };
235
+ }
103
236
  async function runInit(rawArgs) {
104
237
  let values;
105
238
  try {
@@ -111,6 +244,7 @@ async function runInit(rawArgs) {
111
244
  'dry-run': { type: 'boolean' },
112
245
  force: { type: 'boolean' },
113
246
  'no-color': { type: 'boolean' },
247
+ layer: { type: 'string', multiple: true },
114
248
  },
115
249
  allowPositionals: false,
116
250
  }));
@@ -119,8 +253,14 @@ async function runInit(rawArgs) {
119
253
  process.stderr.write(`${error.message}\n\n${USAGE}\n`);
120
254
  return 1;
121
255
  }
256
+ const layerResult = resolveLayerFlag(values.layer);
257
+ if ('error' in layerResult) {
258
+ process.stderr.write(`${layerResult.error}\n\n${USAGE}\n`);
259
+ return 1;
260
+ }
122
261
  const cwd = process.cwd();
123
262
  const dryRun = values['dry-run'] === true;
263
+ const { withWorkflow } = layerResult;
124
264
  // `init` adopts a repo the rig knows nothing about. Run inside a rig `create`
125
265
  // generated — reachable when its CLAUDE.md was deleted — it is the wrong
126
266
  // command: it installs the process layer alone and never refreshes the stack
@@ -133,8 +273,8 @@ async function runInit(rawArgs) {
133
273
  // same limit `recordInstall` carries, stated in both places because either
134
274
  // one alone reads as wider.
135
275
  const existing = await readManifest(cwd);
136
- const plan = await planInit(cwd);
137
- process.stdout.write(`agent-rig init — process layer into ${cwd}\n\n` +
276
+ const plan = await planInit(cwd, { withWorkflow });
277
+ process.stdout.write(`agent-rig init — process layer${withWorkflow ? ' + the opt-in workflow layer' : ''} into ${cwd}\n\n` +
138
278
  plan.files.map((f) => ` + ${f.path}`).join('\n') +
139
279
  '\n');
140
280
  if (existing?.kind === 'create') {
@@ -145,7 +285,7 @@ async function runInit(rawArgs) {
145
285
  plan.conflicts.map((c) => ` · ${c}`).join('\n') +
146
286
  '\n');
147
287
  }
148
- const result = await initProject(cwd, { dryRun, force: values.force === true });
288
+ const result = await initProject(cwd, { dryRun, force: values.force === true, withWorkflow });
149
289
  if (dryRun) {
150
290
  process.stdout.write(`\nDry run — nothing written (${result.plannedCount} files planned).\n`);
151
291
  return 0;
@@ -160,6 +300,17 @@ async function runInit(rawArgs) {
160
300
  for (const wiringPath of ['.claude/settings.json', '.codex/hooks.json']) {
161
301
  if (!result.skipped.includes(wiringPath))
162
302
  continue;
303
+ const installedHash = existing?.files[wiringPath];
304
+ if (installedHash !== undefined) {
305
+ try {
306
+ if (sha256(await readFile(path.join(cwd, wiringPath))) === installedHash)
307
+ continue;
308
+ }
309
+ catch {
310
+ // The write preflight already classified this path. If it changes
311
+ // before reporting, fall through to the conservative warning.
312
+ }
313
+ }
163
314
  const wiring = generated.get(wiringPath) ?? '';
164
315
  process.stdout.write(`\n! ${wiringPath} already exists — it was kept, so the rig's hooks are NOT wired there.\n` +
165
316
  ` Until you merge these entries into it, nothing enforces the rules:\n\n` +
@@ -175,6 +326,7 @@ const MARK = {
175
326
  deleted: '-',
176
327
  wiring: '!',
177
328
  unchanged: '·',
329
+ retired: 'x',
178
330
  };
179
331
  function renderUpgradePlan(repoDir, plan) {
180
332
  const of = (verdict) => plan.actions.filter((a) => a.verdict === verdict);
@@ -184,9 +336,17 @@ function renderUpgradePlan(repoDir, plan) {
184
336
  ? ` no readable manifest here (deleted, never written, or unparseable) — matching files against released versions`
185
337
  : ` installed by ${plan.fromVersion}`,
186
338
  ` upgrading to ${plan.toVersion}`,
187
- '',
188
339
  ];
189
- for (const verdict of ['update', 'new', 'deleted', 'conflict', 'wiring']) {
340
+ // RP-180 round 4, blocker A(4): when a layer's membership was inferred
341
+ // from disk rather than read from a manifest, say so — and how much
342
+ // evidence it rested on — instead of silently deciding.
343
+ for (const note of plan.layerInference ?? []) {
344
+ lines.push(note.adopted
345
+ ? ` ${note.layer} layer inferred from ${note.present} of ${note.total} files on disk`
346
+ : ` ${note.present} of ${note.total} ${note.layer}-layer files found on disk — below quorum, left as your own (not adopted)`);
347
+ }
348
+ lines.push('');
349
+ for (const verdict of ['update', 'new', 'deleted', 'retired', 'conflict', 'wiring']) {
190
350
  for (const action of of(verdict)) {
191
351
  lines.push(` ${MARK[verdict]} ${action.rel}` + (action.reason ? ` — ${action.reason}` : ''));
192
352
  // A conflict is only useful if the new version can be diffed by hand.
@@ -195,13 +355,14 @@ function renderUpgradePlan(repoDir, plan) {
195
355
  }
196
356
  }
197
357
  }
198
- // Every one of `UpgradeVerdict`'s six members is accounted for here.
199
- // `wiring` and `deleted` each print their own line and were in none of the
200
- // buckets, so a reader counted lines and was told a smaller number.
201
- // (`unchanged` is counted and prints nothing — the sum is over actions, not
202
- // over printed lines.) The two appear only when they occurred, so a plan
203
- // without them renders exactly as it always has. Pinned by, in cli-report.test.ts,
204
- // "renders a plan with no wiring action exactly as it does today".
358
+ // Every one of `UpgradeVerdict`'s seven members is accounted for here.
359
+ // `wiring`, `deleted` and `retired` each print their own line and were in
360
+ // none of the buckets, so a reader counted lines and was told a smaller
361
+ // number. (`unchanged` is counted and prints nothing — the sum is over
362
+ // actions, not over printed lines.) The three appear only when they
363
+ // occurred, so a plan without them renders exactly as it always has. Pinned
364
+ // by, in cli-report.test.ts, "renders a plan with no wiring action exactly
365
+ // as it does today".
205
366
  // `deleted` before `wiring`, the relative order the plan prints them in.
206
367
  // ⚠ Only their order relative to EACH OTHER matches: the plan prints
207
368
  // `deleted` before `conflict` and the summary prints it after, so this is not
@@ -209,15 +370,85 @@ function renderUpgradePlan(repoDir, plan) {
209
370
  // "lists the two occasional buckets in the order the plan prints them".
210
371
  const occasional = [
211
372
  ['deleted', (n) => `${n} you removed (left removed)`],
373
+ ['retired', (n) => `${n} no longer shipped (now yours)`],
212
374
  ['wiring', (n) => `${n} wiring handed over`],
213
375
  ];
214
376
  const extra = occasional
215
377
  .map(([verdict, phrase]) => [of(verdict).length, phrase])
216
378
  .filter(([count]) => count > 0)
217
379
  .map(([count, phrase]) => phrase(count));
380
+ // RP-186 round 3 advisory: a held-back CLAUDE.md is `conflict` in name only
381
+ // — it is not the user's bytes kept aside, it is THIS release's own old
382
+ // content re-vouched pending a fix elsewhere (see the coupling in
383
+ // `upgrade.ts`), and counting it under "yours (kept)" tells the reader the
384
+ // opposite of what happened. Round 4 advisory: read the action's own
385
+ // `heldBack` field, set by `upgrade.ts` at the one place that decides it —
386
+ // sniffing `reason` for a fixed prefix broke the moment that wording
387
+ // changed, which is exactly what happened between round 2 and round 3.
388
+ const heldBack = of('conflict').filter((a) => a.heldBack === true);
389
+ const keptConflicts = of('conflict').length - heldBack.length;
390
+ const heldBackPhrase = heldBack.length > 0 ? [`${heldBack.length} held back (see reason above)`] : [];
218
391
  lines.push('', ` ${of('update').length} to replace, ${of('new').length} new, ` +
219
- `${of('conflict').length} yours (kept), ` +
220
- [...extra, `${of('unchanged').length} already current`].join(', '));
392
+ `${keptConflicts} yours (kept), ` +
393
+ [...extra, ...heldBackPhrase, `${of('unchanged').length} already current`].join(', '));
394
+ return `${lines.join('\n')}\n`;
395
+ }
396
+ /**
397
+ * The short, delimited remedy for a genuinely HELD-BACK AGENTS.md — round 5
398
+ * (replacing round 4's verdict-only rule, which fired this same notice for
399
+ * every edited AGENTS.md, including a customised-but-readable one: the
400
+ * shipped rulebook's own designed steady state). Printed as the LAST thing a
401
+ * run prints (after the plan, after any wiring hand-over, after "Wrote N
402
+ * files.") so it is what stays on screen. Never printed at all when
403
+ * `status.holdBack` is `false` and there is no rescue-file housekeeping to
404
+ * report (`status: 'none'`) — round 5, blockers 1 and 2: an ordinary,
405
+ * readable, customised AGENTS.md conflict earns no mention here whatsoever,
406
+ * and neither does unrelated clutter (a stray directory or symlink) sitting
407
+ * at the rescue path on an otherwise healthy rig.
408
+ *
409
+ * `mv` is printed ONLY for `would-write` and `identical` — bytes this run
410
+ * either just wrote or independently verified byte-identical to its own
411
+ * rendering. For `differs`, the file is NOT this run's — the remedy is `rm`
412
+ * (or restoring AGENTS.md some other way), never `mv` (round 5, blocker 2:
413
+ * printing `mv` there would install a hostile or merely stale file as the
414
+ * live rulebook the moment a user followed the instruction literally).
415
+ */
416
+ function renderAgentsRescueNotice(status, isDryRun) {
417
+ const lines = ['', `---- ${AGENTS_MD_RESCUE} ----`];
418
+ if (!status.holdBack) {
419
+ if (status.status === 'none')
420
+ return null;
421
+ // 'cleanup': a leftover regular file, byte-identical to this release's
422
+ // own rendering, with AGENTS.md itself already resolved — pure
423
+ // housekeeping, never "migration" wording.
424
+ lines.push(isDryRun
425
+ ? `A real run will remove a leftover ${AGENTS_MD_RESCUE} — it matches what would be installed.`
426
+ : `Removed a leftover ${AGENTS_MD_RESCUE} — it matched what this run just installed.`);
427
+ return `${lines.join('\n')}\n`;
428
+ }
429
+ lines.push('AGENTS.md needs your attention (see its line above) — the migration is NOT finished.', '', 'CLAUDE.md is held back — kept as its old content, not the new shim — until AGENTS.md', 'resolves: CLAUDE.md still imports AGENTS.md (`@AGENTS.md`), so shimming it now would', 'leave no rulebook loaded at all.');
430
+ switch (status.status) {
431
+ case 'would-write':
432
+ lines.push(isDryRun
433
+ ? `A real run (no --dry-run) will write this release's rendered AGENTS.md content to ${AGENTS_MD_RESCUE}.`
434
+ : `Wrote this release's rendered AGENTS.md content to ${AGENTS_MD_RESCUE}.`);
435
+ lines.push('', 'Review it, then:', ` mv ${AGENTS_MD_RESCUE} AGENTS.md`, ' create-agent-rig upgrade');
436
+ break;
437
+ case 'identical':
438
+ lines.push(`${AGENTS_MD_RESCUE} already holds this release's rendered content, unchanged.`);
439
+ lines.push('', 'Review it, then:', ` mv ${AGENTS_MD_RESCUE} AGENTS.md`, ' create-agent-rig upgrade');
440
+ break;
441
+ case 'differs':
442
+ lines.push(`${AGENTS_MD_RESCUE} already exists with content that is NOT this run's rendering —`, 'left untouched (it may be your own in-progress merge, or a leftover from another', 'release). Never `mv` this one over AGENTS.md — it is not bytes this run wrote or', 'verified.');
443
+ lines.push('', 'Resolve it, then re-run upgrade:', ` rm ${AGENTS_MD_RESCUE}`, ' # …or restore/repair AGENTS.md yourself, then:', ' create-agent-rig upgrade');
444
+ break;
445
+ case 'unsafe':
446
+ lines.push(`${AGENTS_MD_RESCUE} exists but is not a plain file (a symlink, a directory, or similar).`);
447
+ if (isDryRun)
448
+ lines.push('A real run refuses to touch it rather than write through it.');
449
+ lines.push('', 'Move or remove it by hand, then:', ' create-agent-rig upgrade');
450
+ break;
451
+ }
221
452
  return `${lines.join('\n')}\n`;
222
453
  }
223
454
  async function runUpgrade(rawArgs) {
@@ -259,14 +490,21 @@ async function runUpgrade(rawArgs) {
259
490
  '\n');
260
491
  }
261
492
  }
493
+ // Round 5: `plan.agentsRescue` is the one, plan-time-decided status this
494
+ // whole closing section reads — the same value a dry run and a real run
495
+ // both see, so they can never disagree about what would happen.
496
+ const claudeAction = plan.actions.find((a) => a.rel === 'CLAUDE.md');
262
497
  if (values['dry-run'] === true) {
263
498
  process.stdout.write('\nDry run — nothing written.\n');
499
+ const notice = renderAgentsRescueNotice(plan.agentsRescue, true);
500
+ if (notice !== null)
501
+ process.stdout.write(notice);
264
502
  return 0;
265
503
  }
266
504
  // The plan above is the review step, so it has to be answered before
267
- // anything is written. On a terminal that is a question; off one it is the
268
- // same refusal `create` makes without --target — never guess for a run that
269
- // cannot be asked, least of all when the answer rewrites its repository.
505
+ // anything is written. On a terminal that is a question; off one it is a
506
+ // refusal — never guess for a run that cannot be asked, least of all when
507
+ // the answer rewrites its repository.
270
508
  const isInteractive = Boolean(process.stdin.isTTY && process.stderr.isTTY);
271
509
  if (values.yes !== true) {
272
510
  if (!isInteractive) {
@@ -284,8 +522,18 @@ async function runUpgrade(rawArgs) {
284
522
  return 0;
285
523
  }
286
524
  }
525
+ // Decided from the PLAN, before applying it: whether this run is the one
526
+ // that actually adopts the shim (round 5 advisory — a positive completion
527
+ // line, not only ever bad news at the end of a run). `applyUpgrade` writes
528
+ // every `update`/`new` action unconditionally, so a `claudeAction` verdict
529
+ // of `update` here means CLAUDE.md really will be (or already was) written
530
+ // as the shim by the time this function returns.
531
+ const adoptsShimThisRun = claudeAction?.verdict === 'update';
287
532
  const result = await applyUpgrade(cwd, plan);
288
533
  process.stdout.write(`\nWrote ${result.written.length} files.\n`);
534
+ if (adoptsShimThisRun) {
535
+ process.stdout.write('CLAUDE.md now imports AGENTS.md.\n');
536
+ }
289
537
  // The subsystem manifest is machine-scoped and written by `setup`; an
290
538
  // upgrade re-runs the same derivation so `installedVersion` follows the
291
539
  // executable the root now holds. It is never part of the rig manifest or of
@@ -306,9 +554,357 @@ async function runUpgrade(rawArgs) {
306
554
  throw error;
307
555
  process.stdout.write(`Subsystem manifest: not refreshed — ${error.message} (${error.code})\n`);
308
556
  }
557
+ // Printed LAST, deliberately — round 4, blocker 1's "the run must not end
558
+ // on a bare 'Wrote N files.' that reads as success while CLAUDE.md is held
559
+ // back" — so this is what a reader scrolling to the bottom of the run
560
+ // actually sees, whether the news is "not finished, here is exactly what
561
+ // to run next" or a small, ordinary cleanup note. `null` (round 5,
562
+ // blockers 1/2) means genuinely nothing to add: an ordinary, readable,
563
+ // customised AGENTS.md conflict, or unrelated clutter at the rescue path,
564
+ // both print nothing here at all.
565
+ const notice = renderAgentsRescueNotice(plan.agentsRescue, false);
566
+ if (notice !== null)
567
+ process.stdout.write(notice);
568
+ return 0;
569
+ }
570
+ const UNINSTALL_MARK = {
571
+ remove: '-',
572
+ absent: '·',
573
+ preserved: '!',
574
+ };
575
+ /**
576
+ * `removed` is always what actually happened — `applyUninstall`'s own result
577
+ * — never a re-derivation of the plan: empty on a dry run, a consent refusal,
578
+ * or a plan that itself failed, and on a partial failure the SUBSET that
579
+ * finished, not every `remove`-verdict path the plan named. `planned` is the
580
+ * plan's own answer regardless of outcome, so a caller can tell "what would
581
+ * this have done" from "what did it do" even when they differ.
582
+ *
583
+ * `changedSincePlanning` and `protectedHooksAtApply` paths are both folded
584
+ * into `preserved` — the first with reason
585
+ * {@link CHANGED_SINCE_PLANNING_REASON}, the second worded by
586
+ * {@link protectedFileReason} — the SAME function `planUninstall` itself
587
+ * calls to word a hook it protects at PLAN time, so a reader cannot tell
588
+ * which pass discovered the protection from the wording alone, and picking
589
+ * the right one of the four underlying strings never happens twice: a
590
+ * directly-named hook's wording depends on `entry.wiringKind` (a `kept`
591
+ * wiring file's hook was never "preserved as edited", and reusing that
592
+ * wording told two contradictory stories about the same file), a
593
+ * transitively-imported one on `entry.importedBy`, and one protected only by
594
+ * the conservative superset sweep — never traced at all — on
595
+ * `entry.unverifiedBecause`, which names the file whose own unreadability
596
+ * triggered the sweep rather than claiming a connection this command never
597
+ * confirmed. Neither `changedSincePlanning` nor
598
+ * `protectedHooksAtApply` is in `plan.actions` (both were `remove` at plan
599
+ * time and only discovered otherwise at apply time), but both are exactly as
600
+ * un-removed as any other preserved path, and a caller reading `preserved`
601
+ * for "what did this run leave behind" must see them there too, not in a
602
+ * third and fourth, easy-to-miss list.
603
+ *
604
+ * `outcome` is passed straight through from `applyUninstall`'s own result:
605
+ * present on every completed, non-dry-run call, absent on `--dry-run` and on
606
+ * a hard failure alike — this function never invents or infers it.
607
+ */
608
+ function uninstallPayload(dryRun, actions, removed, applied) {
609
+ const of = (verdict) => actions.filter((a) => a.verdict === verdict).map((a) => a.rel);
610
+ const payload = {
611
+ schemaVersion: 1,
612
+ command: 'uninstall',
613
+ dryRun,
614
+ planned: of('remove'),
615
+ removed: [...removed],
616
+ absent: of('absent'),
617
+ preserved: [
618
+ ...actions
619
+ .filter((a) => a.verdict === 'preserved')
620
+ .map((a) => ({ path: a.rel, reason: a.reason ?? '' })),
621
+ ...(applied?.changedSincePlanning ?? []).map((path) => ({
622
+ path,
623
+ reason: CHANGED_SINCE_PLANNING_REASON,
624
+ })),
625
+ ...(applied?.protectedHooksAtApply ?? []).map(({ rel, wiringRel, wiringKind, importedBy, unverifiedBecause }) => ({
626
+ path: rel,
627
+ reason: protectedFileReason(wiringRel, wiringKind, importedBy, unverifiedBecause),
628
+ })),
629
+ ],
630
+ notes: actions.filter((a) => a.note !== undefined).map((a) => ({ path: a.rel, note: a.note })),
631
+ manifestRemoved: applied?.manifestRemoved ?? false,
632
+ };
633
+ if (applied?.completed !== undefined)
634
+ payload.completed = applied.completed;
635
+ if (applied?.remaining !== undefined)
636
+ payload.remaining = applied.remaining;
637
+ if (applied?.error !== undefined)
638
+ payload.error = applied.error;
639
+ if (applied?.outcome !== undefined)
640
+ payload.outcome = applied.outcome;
641
+ return payload;
642
+ }
643
+ function renderUninstallPlan(repoDir, plan) {
644
+ const of = (verdict) => plan.actions.filter((a) => a.verdict === verdict);
645
+ const lines = [`agent-rig uninstall — ${repoDir}`, ''];
646
+ for (const verdict of ['remove', 'preserved', 'absent']) {
647
+ for (const action of of(verdict)) {
648
+ // `reason` (only ever on `preserved`) and `note` (round 4, blocker 2 —
649
+ // only ever on `remove`, for the CLAUDE.md/AGENTS.md pair disclosure)
650
+ // never both apply to the same action, so one disclosure slot renders
651
+ // either.
652
+ const disclosure = action.reason ?? action.note;
653
+ lines.push(` ${UNINSTALL_MARK[verdict]} ${action.rel}` + (disclosure ? ` — ${disclosure}` : ''));
654
+ }
655
+ }
656
+ lines.push('', ` ${of('remove').length} to remove, ${of('preserved').length} preserved, ` +
657
+ `${of('absent').length} already gone`);
658
+ return `${lines.join('\n')}\n`;
659
+ }
660
+ async function runUninstall(rawArgs) {
661
+ let positionals;
662
+ let values;
663
+ try {
664
+ ({ positionals, values } = parseArgs({
665
+ args: rawArgs,
666
+ options: {
667
+ 'dry-run': { type: 'boolean' },
668
+ json: { type: 'boolean' },
669
+ yes: { type: 'boolean' },
670
+ detach: { type: 'boolean' },
671
+ // `--no-color` for the same reason it is accepted on `init` and
672
+ // `upgrade`: USAGE offers it without scoping it to one command. It
673
+ // has no observable effect here specifically — uninstall's report
674
+ // never uses the colour palette in the first place — accepted only
675
+ // so the flag never produces an "unknown option" error a reader of
676
+ // USAGE would not expect.
677
+ 'no-color': { type: 'boolean' },
678
+ },
679
+ allowPositionals: true,
680
+ }));
681
+ }
682
+ catch (error) {
683
+ process.stderr.write(`${error.message}\n\n${USAGE}\n`);
684
+ return 1;
685
+ }
686
+ if (positionals.length > 1) {
687
+ process.stderr.write(`${USAGE}\n`);
688
+ return 1;
689
+ }
690
+ const repoDir = path.resolve(process.cwd(), positionals[0] ?? '.');
691
+ const dryRun = values['dry-run'] === true;
692
+ const json = values.json === true;
693
+ const yes = values.yes === true;
694
+ const detach = values.detach === true;
695
+ let plan;
696
+ try {
697
+ plan = await planUninstall(repoDir);
698
+ }
699
+ catch (error) {
700
+ // `UninstallError` is a message this command composed on purpose — the
701
+ // usual case. Anything else (EACCES, ENOTDIR, a permission the caller did
702
+ // not expect) is unplanned, but `--json` promises one JSON object and
703
+ // nothing else on stdout regardless of which kind it is: a stack trace on
704
+ // stderr with no payload at all breaks that promise for a caller who only
705
+ // ever reads stdout. Off `--json`, the trace is still the right
706
+ // diagnostic, so it is rethrown to `main()`'s own handler unchanged.
707
+ const message = error instanceof Error ? error.message : String(error);
708
+ if (json) {
709
+ process.stdout.write(`${JSON.stringify(uninstallPayload(dryRun, [], [], { manifestRemoved: false, error: message }))}\n`);
710
+ return 1;
711
+ }
712
+ if (error instanceof UninstallError) {
713
+ process.stderr.write(`${message}\n`);
714
+ return 1;
715
+ }
716
+ throw error;
717
+ }
718
+ if (plan.noManifest) {
719
+ if (json) {
720
+ // `outcome` names an END STATE a real run reached; `--dry-run` never
721
+ // reaches one, even here — "nothing installed" is an end state only
722
+ // once a real (non-dry) run has acted, or declined to act, on it.
723
+ process.stdout.write(`${JSON.stringify(uninstallPayload(dryRun, [], [], dryRun
724
+ ? { manifestRemoved: false }
725
+ : { manifestRemoved: false, outcome: 'uninstalled' }))}\n`);
726
+ }
727
+ else {
728
+ process.stdout.write(`No rig manifest found in ${repoDir} — nothing to uninstall.\n`);
729
+ }
730
+ return 0;
731
+ }
732
+ // The plan is the review step, so it is shown before anything is decided —
733
+ // in both output modes, and before the consent question below, not after.
734
+ if (!json)
735
+ process.stdout.write(renderUninstallPlan(repoDir, plan));
736
+ if (dryRun) {
737
+ if (json) {
738
+ process.stdout.write(`${JSON.stringify(uninstallPayload(true, plan.actions, []))}\n`);
739
+ }
740
+ else {
741
+ process.stdout.write('\nDry run — nothing removed.\n');
742
+ }
743
+ return 0;
744
+ }
745
+ // Consent, never guessed, least of all when the answer deletes files — the
746
+ // same shape `upgrade` asks before it writes: `--yes` up front, a prompt on
747
+ // a terminal, and an outright refusal off one. `--json` stays
748
+ // non-interactive on principle, the same reason `--version --json` and
749
+ // every other JSON payload here never prompts: it is read by a script, and
750
+ // a script blocking on a TTY question is a hang, not a safeguard. So
751
+ // without `--yes` it gets the same refusal a non-interactive run gets,
752
+ // reported in its own shape instead of a stderr sentence.
753
+ const isInteractive = Boolean(process.stdin.isTTY && process.stderr.isTTY);
754
+ if (!yes) {
755
+ if (json || !isInteractive) {
756
+ const message = 'Refusing to remove files without --yes in a non-interactive run.';
757
+ if (json) {
758
+ process.stdout.write(`${JSON.stringify(uninstallPayload(false, plan.actions, [], { manifestRemoved: false, error: message }))}\n`);
759
+ }
760
+ else {
761
+ process.stderr.write(`${message} Re-run with --yes once the plan above is what you want ` +
762
+ '(or --dry-run to keep looking).\n');
763
+ }
764
+ return 1;
765
+ }
766
+ const confirmed = await promptConfirm('\nRemove these files?', {
767
+ input: process.stdin,
768
+ output: process.stderr,
769
+ isInteractive,
770
+ });
771
+ if (!confirmed) {
772
+ process.stdout.write('Nothing removed.\n');
773
+ return 0;
774
+ }
775
+ }
776
+ let result;
777
+ try {
778
+ result = await applyUninstall(repoDir, plan, { detach });
779
+ }
780
+ catch (error) {
781
+ // Mirrors the `planUninstall` try/catch above, for the same reason: the
782
+ // apply-time hook-protection re-check added in the same change as this
783
+ // comment reads the filesystem again (`regularFileStatus`, `readFile`)
784
+ // OUTSIDE of `applyUninstall`'s own per-file try/catch, so an EACCES or
785
+ // ENOTDIR surfacing from THAT read must not escape as a bare stack trace
786
+ // with no JSON on stdout — `--json` promises exactly one object there
787
+ // regardless of which kind of failure this is.
788
+ const message = error instanceof Error ? error.message : String(error);
789
+ if (json) {
790
+ // `removed: []` here is not a guess: EVERY call inside `applyUninstall`
791
+ // that can throw uncaught (`rigOwnedPaths`, the apply-time
792
+ // `protectedHooksFor` pass) runs strictly before its own removal loop
793
+ // starts — that loop wraps every per-file removal in its OWN
794
+ // try/catch and always RETURNS a result (with the real `removed` so
795
+ // far) rather than throwing. So an exception reaching this `catch` can
796
+ // only mean nothing was removed yet. This is a structural property of
797
+ // `applyUninstall`'s own control flow (see the comment immediately
798
+ // above its removal loop), not backed by a test of the hypothetical
799
+ // case, which does not exist today — if a future edit adds a
800
+ // throwing call INSIDE or AFTER that loop, this array would start
801
+ // lying, silently, exactly here.
802
+ process.stdout.write(`${JSON.stringify(uninstallPayload(false, plan.actions, [], { manifestRemoved: false, error: message }))}\n`);
803
+ return 1;
804
+ }
805
+ if (error instanceof UninstallError) {
806
+ process.stderr.write(`${message}\n`);
807
+ return 1;
808
+ }
809
+ throw error;
810
+ }
811
+ if (result.error !== undefined) {
812
+ if (json) {
813
+ process.stdout.write(`${JSON.stringify(uninstallPayload(false, plan.actions, result.removed, {
814
+ manifestRemoved: false,
815
+ completed: result.completed,
816
+ remaining: result.remaining,
817
+ error: result.error,
818
+ changedSincePlanning: result.changedSincePlanning,
819
+ protectedHooksAtApply: result.protectedHooksAtApply,
820
+ }))}\n`);
821
+ }
822
+ else {
823
+ process.stderr.write(`\nStopped after a failure: ${result.error}\n` +
824
+ ` completed: ${(result.completed ?? []).join(', ') || '(none)'}\n` +
825
+ ` remaining: ${(result.remaining ?? []).join(', ') || '(none)'}\n` +
826
+ `The manifest was kept — re-run to continue.\n`);
827
+ }
828
+ return 1;
829
+ }
830
+ if (json) {
831
+ process.stdout.write(`${JSON.stringify(uninstallPayload(false, plan.actions, result.removed, {
832
+ manifestRemoved: result.manifestRemoved,
833
+ changedSincePlanning: result.changedSincePlanning,
834
+ protectedHooksAtApply: result.protectedHooksAtApply,
835
+ outcome: result.outcome,
836
+ }))}\n`);
837
+ return 0;
838
+ }
839
+ // The three outcomes `--json` names structurally are said in prose here
840
+ // too, not only encoded in a field: `preserved` below is the IDENTICAL
841
+ // list `uninstallPayload` already built for `--json` above (not a second,
842
+ // separately-maintained computation of the same thing) — the plan's own
843
+ // `preserved` verdicts, any path caught changed only at apply time, and
844
+ // any hook a wiring file's OWN apply-time edit or symlink just protected,
845
+ // each with the reason that pass recorded. All three are equally "left
846
+ // behind", and a report naming only one kind — or naming a count instead
847
+ // of the paths themselves — would read as if the others never happened, or
848
+ // leave an operator with nothing to grep for once the count passes a
849
+ // handful.
850
+ const { preserved } = uninstallPayload(false, plan.actions, result.removed, {
851
+ manifestRemoved: result.manifestRemoved,
852
+ changedSincePlanning: result.changedSincePlanning,
853
+ protectedHooksAtApply: result.protectedHooksAtApply,
854
+ outcome: result.outcome,
855
+ });
856
+ const preservedList = () => preserved.map(({ path, reason }) => ` ! ${path}${reason ? ` — ${reason}` : ''}`).join('\n');
857
+ // A roll-up, on top of the per-line reasons, distinguishing what the
858
+ // command actually traced from what it kept only as a precaution — at
859
+ // the scale a symlinked, single-seeded hook dependency can now produce
860
+ // (dozens of paths swept in by caution alone), one line naming the split
861
+ // does more for an operator than reading every reason individually
862
+ // (UX-lens review, RP-181, carried since cycle 5 as the roll-up advisory).
863
+ const unverifiedCount = preserved.filter((p) => isUnverifiedReason(p.reason)).length;
864
+ const rollup = unverifiedCount > 0
865
+ ? ` (${preserved.length - unverifiedCount} genuinely referenced or imported; ` +
866
+ `${unverifiedCount} kept only as a precaution — something needed to verify them ` +
867
+ `could not be read)\n`
868
+ : '';
869
+ if (result.outcome === 'detached') {
870
+ process.stdout.write(`\nDetached: removed ${result.removed.length} files and the manifest.\n` +
871
+ (preserved.length > 0
872
+ ? `${preserved.length} file(s) left behind — they are yours now, uninstall no longer owns them:\n` +
873
+ rollup +
874
+ `${preservedList()}\n`
875
+ : ''));
876
+ }
877
+ else if (result.manifestRemoved) {
878
+ process.stdout.write(`\nRemoved ${result.removed.length} files and the manifest.\n`);
879
+ }
880
+ else {
881
+ // Every removal that was planned succeeded, but something else was
882
+ // preserved (in the plan, or discovered changed at apply time) — the rig
883
+ // still owns bytes it did not remove, so the manifest naming them was
884
+ // kept on purpose, not left behind by a failure. Named, not only
885
+ // counted: a run with the now-larger preserved count a transitive-import
886
+ // walk can produce still needs to be actionable from this one line of
887
+ // output, without re-running `--json` just to learn what survived.
888
+ process.stdout.write(`\nRemoved ${result.removed.length} files. ${preserved.length} preserved — the manifest ` +
889
+ `was kept: the rig is still installed.\n${rollup}${preservedList()}\n`);
890
+ }
891
+ // A removal is a working-tree change, not a commit — uninstall never
892
+ // touches git history itself (docs/command-contract.md, "## uninstall
893
+ // (RP-181)"), so nothing here is recorded until a run stages and commits
894
+ // it. Said only when something was actually deleted; a preserved-only or
895
+ // no-op run leaves nothing to stage.
896
+ if (result.removed.length > 0) {
897
+ process.stdout.write('Run `git add -A` and commit to record the removal.\n');
898
+ }
309
899
  return 0;
310
900
  }
311
901
  async function main() {
902
+ if (process.argv[2] === 'doctor') {
903
+ const result = await runDoctor({ cwd: process.cwd(), args: process.argv.slice(3) });
904
+ process.stdout.write(result.stdout);
905
+ process.stderr.write(result.stderr);
906
+ return result.exitCode;
907
+ }
312
908
  if (process.argv[2] === 'init') {
313
909
  return runInit(process.argv.slice(3));
314
910
  }
@@ -318,6 +914,9 @@ async function main() {
318
914
  if (process.argv[2] === 'upgrade') {
319
915
  return runUpgrade(process.argv.slice(3));
320
916
  }
917
+ if (process.argv[2] === 'uninstall') {
918
+ return runUninstall(process.argv.slice(3));
919
+ }
321
920
  if (process.argv[2] === 'memory') {
322
921
  // The consumer path of the RP-19 handshake: manifest → `--version --json`
323
922
  // → exit 4 on a foreign major → doctor/load passed through (`load` gains a
@@ -335,13 +934,13 @@ async function main() {
335
934
  args: process.argv.slice(2),
336
935
  options: {
337
936
  help: { type: 'boolean', short: 'h' },
338
- target: { type: 'string' },
339
937
  version: { type: 'boolean' },
340
938
  // `--json` is read on `--version` alone: the handshake object of
341
939
  // docs/command-contract.md, one JSON line and nothing else on stdout.
342
940
  json: { type: 'boolean' },
343
941
  'no-git': { type: 'boolean' },
344
942
  'no-color': { type: 'boolean' },
943
+ layer: { type: 'string', multiple: true },
345
944
  },
346
945
  allowPositionals: true,
347
946
  }));
@@ -367,32 +966,37 @@ async function main() {
367
966
  process.stderr.write(`${USAGE}\n`);
368
967
  return 1;
369
968
  }
370
- // Non-TTY correctness (polish brief §5): never prompt into a pipe — a
371
- // prompt would hang CI. Non-interactive runs must state the target.
372
- const isInteractive = Boolean(process.stdin.isTTY && process.stderr.isTTY);
373
- let target = values.target;
374
- if (!target) {
375
- if (!isInteractive) {
376
- process.stderr.write(`Missing --target in a non-interactive run. ` +
377
- `Pass --target <${TARGET_NAMES.join('|')}>.\n`);
378
- return 1;
379
- }
380
- target = await promptTarget(TARGET_NAMES, DEFAULT_TARGET, {
381
- input: process.stdin,
382
- output: process.stderr,
383
- isInteractive,
384
- });
969
+ const layerResult = resolveLayerFlag(values.layer);
970
+ if ('error' in layerResult) {
971
+ process.stderr.write(`${layerResult.error}\n\n${USAGE}\n`);
972
+ return 1;
385
973
  }
386
974
  const { projectDir, projectName } = await createProject(dirArg, {
387
975
  cwd: process.cwd(),
388
- target,
389
976
  git: values['no-git'] !== true,
977
+ withWorkflow: layerResult.withWorkflow,
390
978
  });
391
979
  const palette = makePalette(Boolean(process.stdout.isTTY) && !process.env.NO_COLOR && values['no-color'] !== true);
392
980
  const summary = await collectGovernance(projectDir);
393
- process.stdout.write('\n' + renderSummary(projectName, target, dirArg, summary, palette));
981
+ process.stdout.write('\n' + renderSummary(projectName, dirArg, summary, palette));
394
982
  return 0;
395
983
  }
984
+ /**
985
+ * A reader closing stdout/stderr must not produce an EPIPE stack trace or
986
+ * replace the command's exit code. Other stream errors set failure only when
987
+ * no failing verdict has already been assigned. Built-CLI pipe regressions
988
+ * live in packages/cli/test/integrations-cli.test.ts.
989
+ */
990
+ function quietlyExitOnEpipe(stream) {
991
+ stream.on('error', (error) => {
992
+ if (error.code === 'EPIPE')
993
+ return;
994
+ if (process.exitCode === undefined || process.exitCode === 0)
995
+ process.exitCode = 1;
996
+ });
997
+ }
998
+ quietlyExitOnEpipe(process.stdout);
999
+ quietlyExitOnEpipe(process.stderr);
396
1000
  main()
397
1001
  .then((code) => {
398
1002
  process.exitCode = code;
@@ -400,7 +1004,8 @@ main()
400
1004
  .catch((error) => {
401
1005
  if (error instanceof CreateError ||
402
1006
  error instanceof InitError ||
403
- error instanceof UpgradeError) {
1007
+ error instanceof UpgradeError ||
1008
+ error instanceof UninstallError) {
404
1009
  process.stderr.write(`${error.message}\n`);
405
1010
  }
406
1011
  else {