create-agent-rig 0.9.1 → 0.10.1

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 (235) hide show
  1. package/CHANGELOG.md +462 -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 +508 -101
  11. package/packages/cli/dist/index.js +658 -47
  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 +45 -12
  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/gate-stop-dod.mjs +4 -2
  44. package/templates/agent-os/universal/.claude/hooks/inject-rules.mjs +83 -4
  45. package/templates/agent-os/universal/.claude/rules/autonomy.md +55 -27
  46. package/templates/agent-os/universal/.claude/rules/invariants.md +9 -8
  47. package/templates/agent-os/universal/.claude/rules/workflow.md +59 -22
  48. package/templates/agent-os/universal/.claude/scripts/decision-router.mjs +1 -1
  49. package/templates/agent-os/universal/.claude/scripts/detect-missed-gate.mjs +12 -9
  50. package/templates/agent-os/universal/.claude/scripts/doctor.mjs +100 -2
  51. package/templates/agent-os/universal/.claude/scripts/lib/claim-records.mjs +236 -6
  52. package/templates/agent-os/universal/.claude/scripts/queue/state.mjs +2 -2
  53. package/templates/agent-os/universal/.claude/scripts/reconcile-external-prs.mjs +4 -3
  54. package/templates/agent-os/universal/.claude/scripts/revalidate.mjs +16 -0
  55. package/templates/agent-os/universal/.claude/settings.json +0 -8
  56. package/templates/agent-os/universal/.claude/skills/check-premises/SKILL.md +45 -12
  57. package/templates/agent-os/universal/.claude/skills/loop/SKILL.md +31 -6
  58. package/templates/agent-os/universal/.claude/skills/new-invariant/SKILL.md +5 -5
  59. package/templates/agent-os/universal/.claude/skills/pr-ship/SKILL.md +1 -1
  60. package/templates/agent-os/universal/.claude/skills/worktree-task/SKILL.md +3 -3
  61. package/templates/agent-os/universal/.codex/agents/code-reviewer.toml +2 -2
  62. package/templates/agent-os/universal/.codex/agents/implementation-agent.toml +6 -0
  63. package/templates/agent-os/universal/.codex/agents/prose-reviewer.toml +2 -2
  64. package/templates/agent-os/universal/.codex/agents/security-scanner.toml +1 -1
  65. package/templates/agent-os/universal/.codex/agents/test-writer.toml +1 -1
  66. package/templates/agent-os/universal/.codex/hooks.json +0 -10
  67. package/templates/agent-os/universal/AGENTS.md +208 -107
  68. package/templates/agent-os/universal/CLAUDE.md +15 -162
  69. package/templates/agent-os/universal/PLAN.md +17 -10
  70. package/templates/agent-os/universal/docs/decisions/agents-md-canonical.md +313 -0
  71. package/templates/agent-os/universal/docs/decisions/codex-adapter.md +15 -10
  72. package/templates/agent-os/universal/docs/decisions/review-lanes.md +12 -8
  73. package/templates/agent-os/universal/docs/decisions/session-start-wire-format.md +206 -0
  74. package/templates/agent-os/universal/docs/decisions/subagent-routing.md +6 -3
  75. package/templates/agent-os/universal/docs/decisions/workflow-layer-split.md +235 -0
  76. package/templates/agent-os/universal/layers.json +25 -30
  77. package/templates/hash-history.json +112 -40
  78. package/templates/release-ledger.json +3 -1
  79. package/packages/cli/dist/lib/composition.js +0 -20
  80. package/packages/cli/dist/lib/targets.js +0 -28
  81. package/packages/cli/dist/policy/benchmark/corpus.js +0 -165
  82. package/packages/cli/dist/policy/core/adapter.js +0 -18
  83. package/packages/cli/dist/policy/core/coverage.js +0 -253
  84. package/packages/cli/dist/policy/core/decision-record.js +0 -287
  85. package/packages/cli/dist/policy/core/declaration.js +0 -127
  86. package/packages/cli/dist/policy/core/evidence-matrix.js +0 -94
  87. package/packages/cli/dist/policy/core/probe.js +0 -442
  88. package/packages/cli/dist/policy/core/registry.js +0 -115
  89. package/packages/cli/dist/policy/core/validation.js +0 -275
  90. package/packages/cli/dist/policy/core/vocabulary.js +0 -123
  91. package/packages/cli/dist/policy/harness/claude.js +0 -47
  92. package/packages/cli/dist/policy/harness/codex.js +0 -87
  93. package/packages/cli/dist/policy/harness/index.js +0 -15
  94. package/packages/cli/dist/policy/harness/shared-hooks.js +0 -28
  95. package/packages/cli/dist/policy/index.js +0 -17
  96. package/templates/agent-os/init/AGENTS.md +0 -201
  97. package/templates/agent-os/init/CLAUDE.md +0 -201
  98. package/templates/agent-os/stack/aws-cdk/.agents/skills/post-deploy-verify/SKILL.md +0 -105
  99. package/templates/agent-os/stack/aws-cdk/.agents/skills/ro-debug/SKILL.md +0 -117
  100. package/templates/agent-os/stack/aws-cdk/.claude/agents/cdk-diff-reviewer.md +0 -89
  101. package/templates/agent-os/stack/aws-cdk/.claude/rules/aws-cdk.md +0 -105
  102. package/templates/agent-os/stack/aws-cdk/.claude/skills/post-deploy-verify/SKILL.md +0 -105
  103. package/templates/agent-os/stack/aws-cdk/.claude/skills/ro-debug/SKILL.md +0 -117
  104. package/templates/agent-os/stack/aws-cdk/.codex/agents/cdk-diff-reviewer.toml +0 -6
  105. package/templates/agent-os/stack/node-ts/.claude/hooks/dod-checks.json +0 -1
  106. package/templates/agent-os/stack/node-ts/.claude/rules/node-ts.md +0 -85
  107. package/templates/agent-os/universal/.claude/hooks/guard-core-purity.mjs +0 -89
  108. package/templates/agent-os/universal/.claude/hooks/guard-web-boundary.mjs +0 -68
  109. package/templates/agent-os/universal/.claude/rules/architecture.md +0 -81
  110. package/templates/skeleton/aws-serverless/.github/workflows/ci.yml +0 -28
  111. package/templates/skeleton/aws-serverless/.github/workflows/deploy.yml +0 -90
  112. package/templates/skeleton/aws-serverless/README.md +0 -181
  113. package/templates/skeleton/aws-serverless/apps/web/next.config.mjs +0 -17
  114. package/templates/skeleton/aws-serverless/apps/web/package.json +0 -19
  115. package/templates/skeleton/aws-serverless/apps/web/src/app/layout.tsx +0 -17
  116. package/templates/skeleton/aws-serverless/apps/web/src/app/page.tsx +0 -96
  117. package/templates/skeleton/aws-serverless/apps/web/src/lib/api.ts +0 -36
  118. package/templates/skeleton/aws-serverless/apps/web/src/lib/validate.ts +0 -23
  119. package/templates/skeleton/aws-serverless/apps/web/test/shared-validation.test.ts +0 -38
  120. package/templates/skeleton/aws-serverless/apps/web/tsconfig.json +0 -14
  121. package/templates/skeleton/aws-serverless/eslint.config.mjs +0 -20
  122. package/templates/skeleton/aws-serverless/gitignore +0 -56
  123. package/templates/skeleton/aws-serverless/infra/bin/app.ts +0 -79
  124. package/templates/skeleton/aws-serverless/infra/cdk.json +0 -3
  125. package/templates/skeleton/aws-serverless/infra/lib/app-stack.ts +0 -248
  126. package/templates/skeleton/aws-serverless/infra/lib/web-stack.ts +0 -45
  127. package/templates/skeleton/aws-serverless/infra/package.json +0 -18
  128. package/templates/skeleton/aws-serverless/infra/test/allowed-origins.test.ts +0 -301
  129. package/templates/skeleton/aws-serverless/infra/test/app-composition.test.ts +0 -137
  130. package/templates/skeleton/aws-serverless/infra/test/app-stack.test.ts +0 -120
  131. package/templates/skeleton/aws-serverless/infra/test/web-stack.test.ts +0 -44
  132. package/templates/skeleton/aws-serverless/package.json +0 -30
  133. package/templates/skeleton/aws-serverless/packages/core/package.json +0 -11
  134. package/templates/skeleton/aws-serverless/packages/core/src/events.ts +0 -14
  135. package/templates/skeleton/aws-serverless/packages/core/src/index.ts +0 -15
  136. package/templates/skeleton/aws-serverless/packages/core/src/note.ts +0 -69
  137. package/templates/skeleton/aws-serverless/packages/core/test/events.test.ts +0 -23
  138. package/templates/skeleton/aws-serverless/packages/core/test/note.test.ts +0 -101
  139. package/templates/skeleton/aws-serverless/packages/db/package.json +0 -14
  140. package/templates/skeleton/aws-serverless/packages/db/src/client.ts +0 -17
  141. package/templates/skeleton/aws-serverless/packages/db/src/index.ts +0 -2
  142. package/templates/skeleton/aws-serverless/packages/db/src/note-model.ts +0 -52
  143. package/templates/skeleton/aws-serverless/packages/db/test/note-model.test.ts +0 -91
  144. package/templates/skeleton/aws-serverless/packages/shared/package.json +0 -11
  145. package/templates/skeleton/aws-serverless/packages/shared/src/env.ts +0 -17
  146. package/templates/skeleton/aws-serverless/packages/shared/src/errors.ts +0 -33
  147. package/templates/skeleton/aws-serverless/packages/shared/src/index.ts +0 -3
  148. package/templates/skeleton/aws-serverless/packages/shared/src/logger.ts +0 -20
  149. package/templates/skeleton/aws-serverless/packages/shared/test/env.test.ts +0 -26
  150. package/templates/skeleton/aws-serverless/packages/shared/test/errors.test.ts +0 -28
  151. package/templates/skeleton/aws-serverless/packages/shared/test/logger.test.ts +0 -19
  152. package/templates/skeleton/aws-serverless/pnpm-lock.yaml +0 -2855
  153. package/templates/skeleton/aws-serverless/pnpm-workspace.yaml +0 -14
  154. package/templates/skeleton/aws-serverless/services/api/package.json +0 -15
  155. package/templates/skeleton/aws-serverless/services/api/src/adapters/sqs-publisher.ts +0 -26
  156. package/templates/skeleton/aws-serverless/services/api/src/handlers/create-note.ts +0 -69
  157. package/templates/skeleton/aws-serverless/services/api/src/handlers/list-notes.ts +0 -37
  158. package/templates/skeleton/aws-serverless/services/api/src/list-main.ts +0 -12
  159. package/templates/skeleton/aws-serverless/services/api/src/main.ts +0 -21
  160. package/templates/skeleton/aws-serverless/services/api/src/usecases/create-note.ts +0 -41
  161. package/templates/skeleton/aws-serverless/services/api/src/usecases/list-notes.ts +0 -14
  162. package/templates/skeleton/aws-serverless/services/api/test/create-note.handler.test.ts +0 -211
  163. package/templates/skeleton/aws-serverless/services/api/test/create-note.usecase.test.ts +0 -45
  164. package/templates/skeleton/aws-serverless/services/api/test/list-notes.test.ts +0 -122
  165. package/templates/skeleton/aws-serverless/services/api/test/sqs-publisher.test.ts +0 -22
  166. package/templates/skeleton/aws-serverless/services/worker/package.json +0 -12
  167. package/templates/skeleton/aws-serverless/services/worker/src/handlers/note-created.ts +0 -15
  168. package/templates/skeleton/aws-serverless/services/worker/src/main.ts +0 -7
  169. package/templates/skeleton/aws-serverless/services/worker/src/usecases/process-note-created.ts +0 -37
  170. package/templates/skeleton/aws-serverless/services/worker/test/note-created.test.ts +0 -61
  171. package/templates/skeleton/aws-serverless/tsconfig.base.json +0 -15
  172. package/templates/skeleton/aws-serverless/tsconfig.json +0 -16
  173. package/templates/skeleton/aws-serverless/vitest.config.ts +0 -14
  174. package/templates/skeleton/node-service/.github/workflows/ci.yml +0 -27
  175. package/templates/skeleton/node-service/.github/workflows/deploy.yml +0 -29
  176. package/templates/skeleton/node-service/README.md +0 -104
  177. package/templates/skeleton/node-service/apps/web/next.config.mjs +0 -17
  178. package/templates/skeleton/node-service/apps/web/package.json +0 -19
  179. package/templates/skeleton/node-service/apps/web/src/app/layout.tsx +0 -17
  180. package/templates/skeleton/node-service/apps/web/src/app/page.tsx +0 -96
  181. package/templates/skeleton/node-service/apps/web/src/lib/api.ts +0 -29
  182. package/templates/skeleton/node-service/apps/web/src/lib/validate.ts +0 -23
  183. package/templates/skeleton/node-service/apps/web/test/shared-validation.test.ts +0 -38
  184. package/templates/skeleton/node-service/apps/web/tsconfig.json +0 -14
  185. package/templates/skeleton/node-service/eslint.config.mjs +0 -20
  186. package/templates/skeleton/node-service/gitignore +0 -54
  187. package/templates/skeleton/node-service/package.json +0 -30
  188. package/templates/skeleton/node-service/packages/core/package.json +0 -11
  189. package/templates/skeleton/node-service/packages/core/src/events.ts +0 -14
  190. package/templates/skeleton/node-service/packages/core/src/index.ts +0 -15
  191. package/templates/skeleton/node-service/packages/core/src/note.ts +0 -69
  192. package/templates/skeleton/node-service/packages/core/test/events.test.ts +0 -23
  193. package/templates/skeleton/node-service/packages/core/test/note.test.ts +0 -101
  194. package/templates/skeleton/node-service/packages/db/package.json +0 -12
  195. package/templates/skeleton/node-service/packages/db/src/index.ts +0 -1
  196. package/templates/skeleton/node-service/packages/db/src/note-store.ts +0 -100
  197. package/templates/skeleton/node-service/packages/db/test/note-store.test.ts +0 -100
  198. package/templates/skeleton/node-service/packages/shared/package.json +0 -11
  199. package/templates/skeleton/node-service/packages/shared/src/env.ts +0 -17
  200. package/templates/skeleton/node-service/packages/shared/src/errors.ts +0 -33
  201. package/templates/skeleton/node-service/packages/shared/src/index.ts +0 -3
  202. package/templates/skeleton/node-service/packages/shared/src/logger.ts +0 -20
  203. package/templates/skeleton/node-service/packages/shared/test/env.test.ts +0 -26
  204. package/templates/skeleton/node-service/packages/shared/test/errors.test.ts +0 -28
  205. package/templates/skeleton/node-service/packages/shared/test/logger.test.ts +0 -19
  206. package/templates/skeleton/node-service/pnpm-lock.yaml +0 -2402
  207. package/templates/skeleton/node-service/pnpm-workspace.yaml +0 -13
  208. package/templates/skeleton/node-service/scripts/build-artifact.mjs +0 -34
  209. package/templates/skeleton/node-service/services/api/package.json +0 -17
  210. package/templates/skeleton/node-service/services/api/src/adapters/spool-publisher.ts +0 -23
  211. package/templates/skeleton/node-service/services/api/src/handlers/create-note.ts +0 -40
  212. package/templates/skeleton/node-service/services/api/src/handlers/list-notes.ts +0 -23
  213. package/templates/skeleton/node-service/services/api/src/main.ts +0 -40
  214. package/templates/skeleton/node-service/services/api/src/server.ts +0 -172
  215. package/templates/skeleton/node-service/services/api/src/static-dir.ts +0 -20
  216. package/templates/skeleton/node-service/services/api/src/usecases/create-note.ts +0 -30
  217. package/templates/skeleton/node-service/services/api/src/usecases/list-notes.ts +0 -14
  218. package/templates/skeleton/node-service/services/api/test/artifact.test.ts +0 -92
  219. package/templates/skeleton/node-service/services/api/test/create-note.handler.test.ts +0 -64
  220. package/templates/skeleton/node-service/services/api/test/create-note.usecase.test.ts +0 -43
  221. package/templates/skeleton/node-service/services/api/test/list-notes.test.ts +0 -48
  222. package/templates/skeleton/node-service/services/api/test/package-manager.test.ts +0 -40
  223. package/templates/skeleton/node-service/services/api/test/package-manager.ts +0 -51
  224. package/templates/skeleton/node-service/services/api/test/server.test.ts +0 -208
  225. package/templates/skeleton/node-service/services/api/test/spool-publisher.test.ts +0 -32
  226. package/templates/skeleton/node-service/services/api/test/static-dir.test.ts +0 -29
  227. package/templates/skeleton/node-service/services/worker/package.json +0 -16
  228. package/templates/skeleton/node-service/services/worker/src/main.ts +0 -28
  229. package/templates/skeleton/node-service/services/worker/src/spool.ts +0 -60
  230. package/templates/skeleton/node-service/services/worker/src/usecases/process-note-created.ts +0 -38
  231. package/templates/skeleton/node-service/services/worker/test/process-note-created.test.ts +0 -34
  232. package/templates/skeleton/node-service/services/worker/test/spool.test.ts +0 -76
  233. package/templates/skeleton/node-service/tsconfig.base.json +0 -15
  234. package/templates/skeleton/node-service/tsconfig.json +0 -13
  235. 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,22 @@ 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');
264
- return 0;
499
+ const notice = renderAgentsRescueNotice(plan.agentsRescue, true);
500
+ if (notice !== null)
501
+ process.stdout.write(notice);
502
+ // The same exit the real run gives: `applyUpgrade` refuses this status.
503
+ return plan.agentsRescue.holdBack && plan.agentsRescue.status === 'unsafe' ? 1 : 0;
265
504
  }
266
505
  // 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.
506
+ // anything is written. On a terminal that is a question; off one it is a
507
+ // refusal — never guess for a run that cannot be asked, least of all when
508
+ // the answer rewrites its repository.
270
509
  const isInteractive = Boolean(process.stdin.isTTY && process.stderr.isTTY);
271
510
  if (values.yes !== true) {
272
511
  if (!isInteractive) {
@@ -284,8 +523,18 @@ async function runUpgrade(rawArgs) {
284
523
  return 0;
285
524
  }
286
525
  }
526
+ // Decided from the PLAN, before applying it: whether this run is the one
527
+ // that actually adopts the shim (round 5 advisory — a positive completion
528
+ // line, not only ever bad news at the end of a run). `applyUpgrade` writes
529
+ // every `update`/`new` action unconditionally, so a `claudeAction` verdict
530
+ // of `update` here means CLAUDE.md really will be (or already was) written
531
+ // as the shim by the time this function returns.
532
+ const adoptsShimThisRun = claudeAction?.verdict === 'update';
287
533
  const result = await applyUpgrade(cwd, plan);
288
534
  process.stdout.write(`\nWrote ${result.written.length} files.\n`);
535
+ if (adoptsShimThisRun) {
536
+ process.stdout.write('CLAUDE.md now imports AGENTS.md.\n');
537
+ }
289
538
  // The subsystem manifest is machine-scoped and written by `setup`; an
290
539
  // upgrade re-runs the same derivation so `installedVersion` follows the
291
540
  // executable the root now holds. It is never part of the rig manifest or of
@@ -306,9 +555,362 @@ async function runUpgrade(rawArgs) {
306
555
  throw error;
307
556
  process.stdout.write(`Subsystem manifest: not refreshed — ${error.message} (${error.code})\n`);
308
557
  }
558
+ // Printed LAST, deliberately — round 4, blocker 1's "the run must not end
559
+ // on a bare 'Wrote N files.' that reads as success while CLAUDE.md is held
560
+ // back" — so this is what a reader scrolling to the bottom of the run
561
+ // actually sees, whether the news is "not finished, here is exactly what
562
+ // to run next" or a small, ordinary cleanup note. `null` (round 5,
563
+ // blockers 1/2) means genuinely nothing to add: an ordinary, readable,
564
+ // customised AGENTS.md conflict, or unrelated clutter at the rescue path,
565
+ // both print nothing here at all.
566
+ // A `cleanup` the apply step did not carry out (the leftover changed or went
567
+ // away after the plan) reports nothing rather than a removal.
568
+ const rescue = plan.agentsRescue.status === 'cleanup' && !result.removedRescue
569
+ ? { holdBack: false, status: 'none' }
570
+ : plan.agentsRescue;
571
+ const notice = renderAgentsRescueNotice(rescue, false);
572
+ if (notice !== null)
573
+ process.stdout.write(notice);
574
+ return 0;
575
+ }
576
+ const UNINSTALL_MARK = {
577
+ remove: '-',
578
+ absent: '·',
579
+ preserved: '!',
580
+ };
581
+ /**
582
+ * `removed` is always what actually happened — `applyUninstall`'s own result
583
+ * — never a re-derivation of the plan: empty on a dry run, a consent refusal,
584
+ * or a plan that itself failed, and on a partial failure the SUBSET that
585
+ * finished, not every `remove`-verdict path the plan named. `planned` is the
586
+ * plan's own answer regardless of outcome, so a caller can tell "what would
587
+ * this have done" from "what did it do" even when they differ.
588
+ *
589
+ * `changedSincePlanning` and `protectedHooksAtApply` paths are both folded
590
+ * into `preserved` — the first with reason
591
+ * {@link CHANGED_SINCE_PLANNING_REASON}, the second worded by
592
+ * {@link protectedFileReason} — the SAME function `planUninstall` itself
593
+ * calls to word a hook it protects at PLAN time, so a reader cannot tell
594
+ * which pass discovered the protection from the wording alone, and picking
595
+ * the right one of the four underlying strings never happens twice: a
596
+ * directly-named hook's wording depends on `entry.wiringKind` (a `kept`
597
+ * wiring file's hook was never "preserved as edited", and reusing that
598
+ * wording told two contradictory stories about the same file), a
599
+ * transitively-imported one on `entry.importedBy`, and one protected only by
600
+ * the conservative superset sweep — never traced at all — on
601
+ * `entry.unverifiedBecause`, which names the file whose own unreadability
602
+ * triggered the sweep rather than claiming a connection this command never
603
+ * confirmed. Neither `changedSincePlanning` nor
604
+ * `protectedHooksAtApply` is in `plan.actions` (both were `remove` at plan
605
+ * time and only discovered otherwise at apply time), but both are exactly as
606
+ * un-removed as any other preserved path, and a caller reading `preserved`
607
+ * for "what did this run leave behind" must see them there too, not in a
608
+ * third and fourth, easy-to-miss list.
609
+ *
610
+ * `outcome` is passed straight through from `applyUninstall`'s own result:
611
+ * present on every completed, non-dry-run call, absent on `--dry-run` and on
612
+ * a hard failure alike — this function never invents or infers it.
613
+ */
614
+ function uninstallPayload(dryRun, actions, removed, applied) {
615
+ const of = (verdict) => actions.filter((a) => a.verdict === verdict).map((a) => a.rel);
616
+ const payload = {
617
+ schemaVersion: 1,
618
+ command: 'uninstall',
619
+ dryRun,
620
+ planned: of('remove'),
621
+ removed: [...removed],
622
+ absent: of('absent'),
623
+ preserved: [
624
+ ...actions
625
+ .filter((a) => a.verdict === 'preserved')
626
+ .map((a) => ({ path: a.rel, reason: a.reason ?? '' })),
627
+ ...(applied?.changedSincePlanning ?? []).map((path) => ({
628
+ path,
629
+ reason: CHANGED_SINCE_PLANNING_REASON,
630
+ })),
631
+ ...(applied?.protectedHooksAtApply ?? []).map(({ rel, wiringRel, wiringKind, importedBy, unverifiedBecause }) => ({
632
+ path: rel,
633
+ reason: protectedFileReason(wiringRel, wiringKind, importedBy, unverifiedBecause),
634
+ })),
635
+ ],
636
+ notes: actions.filter((a) => a.note !== undefined).map((a) => ({ path: a.rel, note: a.note })),
637
+ manifestRemoved: applied?.manifestRemoved ?? false,
638
+ };
639
+ if (applied?.completed !== undefined)
640
+ payload.completed = applied.completed;
641
+ if (applied?.remaining !== undefined)
642
+ payload.remaining = applied.remaining;
643
+ if (applied?.error !== undefined)
644
+ payload.error = applied.error;
645
+ if (applied?.outcome !== undefined)
646
+ payload.outcome = applied.outcome;
647
+ return payload;
648
+ }
649
+ function renderUninstallPlan(repoDir, plan) {
650
+ const of = (verdict) => plan.actions.filter((a) => a.verdict === verdict);
651
+ const lines = [`agent-rig uninstall — ${repoDir}`, ''];
652
+ for (const verdict of ['remove', 'preserved', 'absent']) {
653
+ for (const action of of(verdict)) {
654
+ // `reason` (only ever on `preserved`) and `note` (round 4, blocker 2 —
655
+ // only ever on `remove`, for the CLAUDE.md/AGENTS.md pair disclosure)
656
+ // never both apply to the same action, so one disclosure slot renders
657
+ // either.
658
+ const disclosure = action.reason ?? action.note;
659
+ lines.push(` ${UNINSTALL_MARK[verdict]} ${action.rel}` + (disclosure ? ` — ${disclosure}` : ''));
660
+ }
661
+ }
662
+ lines.push('', ` ${of('remove').length} to remove, ${of('preserved').length} preserved, ` +
663
+ `${of('absent').length} already gone`);
664
+ return `${lines.join('\n')}\n`;
665
+ }
666
+ async function runUninstall(rawArgs) {
667
+ let positionals;
668
+ let values;
669
+ try {
670
+ ({ positionals, values } = parseArgs({
671
+ args: rawArgs,
672
+ options: {
673
+ 'dry-run': { type: 'boolean' },
674
+ json: { type: 'boolean' },
675
+ yes: { type: 'boolean' },
676
+ detach: { type: 'boolean' },
677
+ // `--no-color` for the same reason it is accepted on `init` and
678
+ // `upgrade`: USAGE offers it without scoping it to one command. It
679
+ // has no observable effect here specifically — uninstall's report
680
+ // never uses the colour palette in the first place — accepted only
681
+ // so the flag never produces an "unknown option" error a reader of
682
+ // USAGE would not expect.
683
+ 'no-color': { type: 'boolean' },
684
+ },
685
+ allowPositionals: true,
686
+ }));
687
+ }
688
+ catch (error) {
689
+ process.stderr.write(`${error.message}\n\n${USAGE}\n`);
690
+ return 1;
691
+ }
692
+ if (positionals.length > 1) {
693
+ process.stderr.write(`${USAGE}\n`);
694
+ return 1;
695
+ }
696
+ const repoDir = path.resolve(process.cwd(), positionals[0] ?? '.');
697
+ const dryRun = values['dry-run'] === true;
698
+ const json = values.json === true;
699
+ const yes = values.yes === true;
700
+ const detach = values.detach === true;
701
+ let plan;
702
+ try {
703
+ plan = await planUninstall(repoDir);
704
+ }
705
+ catch (error) {
706
+ // `UninstallError` is a message this command composed on purpose — the
707
+ // usual case. Anything else (EACCES, ENOTDIR, a permission the caller did
708
+ // not expect) is unplanned, but `--json` promises one JSON object and
709
+ // nothing else on stdout regardless of which kind it is: a stack trace on
710
+ // stderr with no payload at all breaks that promise for a caller who only
711
+ // ever reads stdout. Off `--json`, the trace is still the right
712
+ // diagnostic, so it is rethrown to `main()`'s own handler unchanged.
713
+ const message = error instanceof Error ? error.message : String(error);
714
+ if (json) {
715
+ process.stdout.write(`${JSON.stringify(uninstallPayload(dryRun, [], [], { manifestRemoved: false, error: message }))}\n`);
716
+ return 1;
717
+ }
718
+ if (error instanceof UninstallError) {
719
+ process.stderr.write(`${message}\n`);
720
+ return 1;
721
+ }
722
+ throw error;
723
+ }
724
+ if (plan.noManifest) {
725
+ if (json) {
726
+ // `outcome` names an END STATE a real run reached; `--dry-run` never
727
+ // reaches one, even here — "nothing installed" is an end state only
728
+ // once a real (non-dry) run has acted, or declined to act, on it.
729
+ process.stdout.write(`${JSON.stringify(uninstallPayload(dryRun, [], [], dryRun
730
+ ? { manifestRemoved: false }
731
+ : { manifestRemoved: false, outcome: 'uninstalled' }))}\n`);
732
+ }
733
+ else {
734
+ process.stdout.write(`No rig manifest found in ${repoDir} — nothing to uninstall.\n`);
735
+ }
736
+ return 0;
737
+ }
738
+ // The plan is the review step, so it is shown before anything is decided —
739
+ // in both output modes, and before the consent question below, not after.
740
+ if (!json)
741
+ process.stdout.write(renderUninstallPlan(repoDir, plan));
742
+ if (dryRun) {
743
+ if (json) {
744
+ process.stdout.write(`${JSON.stringify(uninstallPayload(true, plan.actions, []))}\n`);
745
+ }
746
+ else {
747
+ process.stdout.write('\nDry run — nothing removed.\n');
748
+ }
749
+ return 0;
750
+ }
751
+ // Consent, never guessed, least of all when the answer deletes files — the
752
+ // same shape `upgrade` asks before it writes: `--yes` up front, a prompt on
753
+ // a terminal, and an outright refusal off one. `--json` stays
754
+ // non-interactive on principle, the same reason `--version --json` and
755
+ // every other JSON payload here never prompts: it is read by a script, and
756
+ // a script blocking on a TTY question is a hang, not a safeguard. So
757
+ // without `--yes` it gets the same refusal a non-interactive run gets,
758
+ // reported in its own shape instead of a stderr sentence.
759
+ const isInteractive = Boolean(process.stdin.isTTY && process.stderr.isTTY);
760
+ if (!yes) {
761
+ if (json || !isInteractive) {
762
+ const message = 'Refusing to remove files without --yes in a non-interactive run.';
763
+ if (json) {
764
+ process.stdout.write(`${JSON.stringify(uninstallPayload(false, plan.actions, [], { manifestRemoved: false, error: message }))}\n`);
765
+ }
766
+ else {
767
+ process.stderr.write(`${message} Re-run with --yes once the plan above is what you want ` +
768
+ '(or --dry-run to keep looking).\n');
769
+ }
770
+ return 1;
771
+ }
772
+ const confirmed = await promptConfirm('\nRemove these files?', {
773
+ input: process.stdin,
774
+ output: process.stderr,
775
+ isInteractive,
776
+ });
777
+ if (!confirmed) {
778
+ process.stdout.write('Nothing removed.\n');
779
+ return 0;
780
+ }
781
+ }
782
+ let result;
783
+ try {
784
+ result = await applyUninstall(repoDir, plan, { detach });
785
+ }
786
+ catch (error) {
787
+ // Mirrors the `planUninstall` try/catch above, for the same reason: the
788
+ // apply-time hook-protection re-check added in the same change as this
789
+ // comment reads the filesystem again (`regularFileStatus`, `readFile`)
790
+ // OUTSIDE of `applyUninstall`'s own per-file try/catch, so an EACCES or
791
+ // ENOTDIR surfacing from THAT read must not escape as a bare stack trace
792
+ // with no JSON on stdout — `--json` promises exactly one object there
793
+ // regardless of which kind of failure this is.
794
+ const message = error instanceof Error ? error.message : String(error);
795
+ if (json) {
796
+ // `removed: []` here is not a guess: EVERY call inside `applyUninstall`
797
+ // that can throw uncaught (`rigOwnedPaths`, the apply-time
798
+ // `protectedHooksFor` pass) runs strictly before its own removal loop
799
+ // starts — that loop wraps every per-file removal in its OWN
800
+ // try/catch and always RETURNS a result (with the real `removed` so
801
+ // far) rather than throwing. So an exception reaching this `catch` can
802
+ // only mean nothing was removed yet. This is a structural property of
803
+ // `applyUninstall`'s own control flow (see the comment immediately
804
+ // above its removal loop), not backed by a test of the hypothetical
805
+ // case, which does not exist today — if a future edit adds a
806
+ // throwing call INSIDE or AFTER that loop, this array would start
807
+ // lying, silently, exactly here.
808
+ process.stdout.write(`${JSON.stringify(uninstallPayload(false, plan.actions, [], { manifestRemoved: false, error: message }))}\n`);
809
+ return 1;
810
+ }
811
+ if (error instanceof UninstallError) {
812
+ process.stderr.write(`${message}\n`);
813
+ return 1;
814
+ }
815
+ throw error;
816
+ }
817
+ if (result.error !== undefined) {
818
+ if (json) {
819
+ process.stdout.write(`${JSON.stringify(uninstallPayload(false, plan.actions, result.removed, {
820
+ manifestRemoved: false,
821
+ completed: result.completed,
822
+ remaining: result.remaining,
823
+ error: result.error,
824
+ changedSincePlanning: result.changedSincePlanning,
825
+ protectedHooksAtApply: result.protectedHooksAtApply,
826
+ }))}\n`);
827
+ }
828
+ else {
829
+ process.stderr.write(`\nStopped after a failure: ${result.error}\n` +
830
+ ` completed: ${(result.completed ?? []).join(', ') || '(none)'}\n` +
831
+ ` remaining: ${(result.remaining ?? []).join(', ') || '(none)'}\n` +
832
+ `The manifest was kept — re-run to continue.\n`);
833
+ }
834
+ return 1;
835
+ }
836
+ if (json) {
837
+ process.stdout.write(`${JSON.stringify(uninstallPayload(false, plan.actions, result.removed, {
838
+ manifestRemoved: result.manifestRemoved,
839
+ changedSincePlanning: result.changedSincePlanning,
840
+ protectedHooksAtApply: result.protectedHooksAtApply,
841
+ outcome: result.outcome,
842
+ }))}\n`);
843
+ return 0;
844
+ }
845
+ // The three outcomes `--json` names structurally are said in prose here
846
+ // too, not only encoded in a field: `preserved` below is the IDENTICAL
847
+ // list `uninstallPayload` already built for `--json` above (not a second,
848
+ // separately-maintained computation of the same thing) — the plan's own
849
+ // `preserved` verdicts, any path caught changed only at apply time, and
850
+ // any hook a wiring file's OWN apply-time edit or symlink just protected,
851
+ // each with the reason that pass recorded. All three are equally "left
852
+ // behind", and a report naming only one kind — or naming a count instead
853
+ // of the paths themselves — would read as if the others never happened, or
854
+ // leave an operator with nothing to grep for once the count passes a
855
+ // handful.
856
+ const { preserved } = uninstallPayload(false, plan.actions, result.removed, {
857
+ manifestRemoved: result.manifestRemoved,
858
+ changedSincePlanning: result.changedSincePlanning,
859
+ protectedHooksAtApply: result.protectedHooksAtApply,
860
+ outcome: result.outcome,
861
+ });
862
+ const preservedList = () => preserved.map(({ path, reason }) => ` ! ${path}${reason ? ` — ${reason}` : ''}`).join('\n');
863
+ // A roll-up, on top of the per-line reasons, distinguishing what the
864
+ // command actually traced from what it kept only as a precaution — at
865
+ // the scale a symlinked, single-seeded hook dependency can now produce
866
+ // (dozens of paths swept in by caution alone), one line naming the split
867
+ // does more for an operator than reading every reason individually
868
+ // (UX-lens review, RP-181, carried since cycle 5 as the roll-up advisory).
869
+ const unverifiedCount = preserved.filter((p) => isUnverifiedReason(p.reason)).length;
870
+ const rollup = unverifiedCount > 0
871
+ ? ` (${preserved.length - unverifiedCount} genuinely referenced or imported; ` +
872
+ `${unverifiedCount} kept only as a precaution — something needed to verify them ` +
873
+ `could not be read)\n`
874
+ : '';
875
+ if (result.outcome === 'detached') {
876
+ process.stdout.write(`\nDetached: removed ${result.removed.length} files and the manifest.\n` +
877
+ (preserved.length > 0
878
+ ? `${preserved.length} file(s) left behind — they are yours now, uninstall no longer owns them:\n` +
879
+ rollup +
880
+ `${preservedList()}\n`
881
+ : ''));
882
+ }
883
+ else if (result.manifestRemoved) {
884
+ process.stdout.write(`\nRemoved ${result.removed.length} files and the manifest.\n`);
885
+ }
886
+ else {
887
+ // Every removal that was planned succeeded, but something else was
888
+ // preserved (in the plan, or discovered changed at apply time) — the rig
889
+ // still owns bytes it did not remove, so the manifest naming them was
890
+ // kept on purpose, not left behind by a failure. Named, not only
891
+ // counted: a run with the now-larger preserved count a transitive-import
892
+ // walk can produce still needs to be actionable from this one line of
893
+ // output, without re-running `--json` just to learn what survived.
894
+ process.stdout.write(`\nRemoved ${result.removed.length} files. ${preserved.length} preserved — the manifest ` +
895
+ `was kept: the rig is still installed.\n${rollup}${preservedList()}\n`);
896
+ }
897
+ // A removal is a working-tree change, not a commit — uninstall never
898
+ // touches git history itself (docs/command-contract.md, "## uninstall
899
+ // (RP-181)"), so nothing here is recorded until a run stages and commits
900
+ // it. Said only when something was actually deleted; a preserved-only or
901
+ // no-op run leaves nothing to stage.
902
+ if (result.removed.length > 0) {
903
+ process.stdout.write('Run `git add -A` and commit to record the removal.\n');
904
+ }
309
905
  return 0;
310
906
  }
311
907
  async function main() {
908
+ if (process.argv[2] === 'doctor') {
909
+ const result = await runDoctor({ cwd: process.cwd(), args: process.argv.slice(3) });
910
+ process.stdout.write(result.stdout);
911
+ process.stderr.write(result.stderr);
912
+ return result.exitCode;
913
+ }
312
914
  if (process.argv[2] === 'init') {
313
915
  return runInit(process.argv.slice(3));
314
916
  }
@@ -318,6 +920,9 @@ async function main() {
318
920
  if (process.argv[2] === 'upgrade') {
319
921
  return runUpgrade(process.argv.slice(3));
320
922
  }
923
+ if (process.argv[2] === 'uninstall') {
924
+ return runUninstall(process.argv.slice(3));
925
+ }
321
926
  if (process.argv[2] === 'memory') {
322
927
  // The consumer path of the RP-19 handshake: manifest → `--version --json`
323
928
  // → exit 4 on a foreign major → doctor/load passed through (`load` gains a
@@ -335,13 +940,13 @@ async function main() {
335
940
  args: process.argv.slice(2),
336
941
  options: {
337
942
  help: { type: 'boolean', short: 'h' },
338
- target: { type: 'string' },
339
943
  version: { type: 'boolean' },
340
944
  // `--json` is read on `--version` alone: the handshake object of
341
945
  // docs/command-contract.md, one JSON line and nothing else on stdout.
342
946
  json: { type: 'boolean' },
343
947
  'no-git': { type: 'boolean' },
344
948
  'no-color': { type: 'boolean' },
949
+ layer: { type: 'string', multiple: true },
345
950
  },
346
951
  allowPositionals: true,
347
952
  }));
@@ -367,32 +972,37 @@ async function main() {
367
972
  process.stderr.write(`${USAGE}\n`);
368
973
  return 1;
369
974
  }
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
- });
975
+ const layerResult = resolveLayerFlag(values.layer);
976
+ if ('error' in layerResult) {
977
+ process.stderr.write(`${layerResult.error}\n\n${USAGE}\n`);
978
+ return 1;
385
979
  }
386
980
  const { projectDir, projectName } = await createProject(dirArg, {
387
981
  cwd: process.cwd(),
388
- target,
389
982
  git: values['no-git'] !== true,
983
+ withWorkflow: layerResult.withWorkflow,
390
984
  });
391
985
  const palette = makePalette(Boolean(process.stdout.isTTY) && !process.env.NO_COLOR && values['no-color'] !== true);
392
986
  const summary = await collectGovernance(projectDir);
393
- process.stdout.write('\n' + renderSummary(projectName, target, dirArg, summary, palette));
987
+ process.stdout.write('\n' + renderSummary(projectName, dirArg, summary, palette));
394
988
  return 0;
395
989
  }
990
+ /**
991
+ * A reader closing stdout/stderr must not produce an EPIPE stack trace or
992
+ * replace the command's exit code. Other stream errors set failure only when
993
+ * no failing verdict has already been assigned. Built-CLI pipe regressions
994
+ * live in packages/cli/test/integrations-cli.test.ts.
995
+ */
996
+ function quietlyExitOnEpipe(stream) {
997
+ stream.on('error', (error) => {
998
+ if (error.code === 'EPIPE')
999
+ return;
1000
+ if (process.exitCode === undefined || process.exitCode === 0)
1001
+ process.exitCode = 1;
1002
+ });
1003
+ }
1004
+ quietlyExitOnEpipe(process.stdout);
1005
+ quietlyExitOnEpipe(process.stderr);
396
1006
  main()
397
1007
  .then((code) => {
398
1008
  process.exitCode = code;
@@ -400,7 +1010,8 @@ main()
400
1010
  .catch((error) => {
401
1011
  if (error instanceof CreateError ||
402
1012
  error instanceof InitError ||
403
- error instanceof UpgradeError) {
1013
+ error instanceof UpgradeError ||
1014
+ error instanceof UninstallError) {
404
1015
  process.stderr.write(`${error.message}\n`);
405
1016
  }
406
1017
  else {