create-agent-rig 0.9.0 → 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 (237) hide show
  1. package/CHANGELOG.md +466 -3
  2. package/README.md +267 -299
  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 +151 -54
  7. package/packages/cli/dist/commands/integrations.js +468 -0
  8. package/packages/cli/dist/commands/memory.js +63 -4
  9. package/packages/cli/dist/commands/setup-wizard.js +61 -0
  10. package/packages/cli/dist/commands/uninstall.js +1223 -0
  11. package/packages/cli/dist/commands/upgrade.js +489 -89
  12. package/packages/cli/dist/index.js +657 -49
  13. package/packages/cli/dist/integrations/declaration.js +158 -0
  14. package/packages/cli/dist/integrations/doctor-guards.js +126 -0
  15. package/packages/cli/dist/integrations/doctor-workflow.js +25 -0
  16. package/packages/cli/dist/integrations/mcp-json.js +107 -0
  17. package/packages/cli/dist/integrations/memory-doctor.js +124 -0
  18. package/packages/cli/dist/integrations/registry.js +22 -0
  19. package/packages/cli/dist/integrations/spawn.js +228 -0
  20. package/packages/cli/dist/integrations/spec-kit.js +280 -0
  21. package/packages/cli/dist/integrations/verify.js +193 -0
  22. package/packages/cli/dist/integrations/windows-job.js +275 -0
  23. package/packages/cli/dist/lib/elevated-paths.js +74 -0
  24. package/packages/cli/dist/lib/install-set.js +1 -44
  25. package/packages/cli/dist/lib/manifest.js +86 -9
  26. package/packages/cli/dist/lib/prompts.js +0 -23
  27. package/packages/cli/dist/lib/safe-path.js +193 -2
  28. package/packages/cli/dist/lib/safe-text.js +78 -0
  29. package/packages/cli/dist/lib/substitute.js +1 -52
  30. package/packages/cli/dist/lib/summary.js +5 -5
  31. package/packages/cli/dist/templates.js +4 -14
  32. package/scripts/prepare.mjs +28 -12
  33. package/templates/agent-os/subagent-routing.json +4 -4
  34. package/templates/agent-os/universal/.agents/skills/check-premises/SKILL.md +13 -9
  35. package/templates/agent-os/universal/.agents/skills/loop/SKILL.md +31 -6
  36. package/templates/agent-os/universal/.agents/skills/new-invariant/SKILL.md +5 -5
  37. package/templates/agent-os/universal/.agents/skills/pr-ship/SKILL.md +1 -1
  38. package/templates/agent-os/universal/.agents/skills/worktree-task/SKILL.md +3 -3
  39. package/templates/agent-os/universal/.claude/agents/code-reviewer.md +8 -6
  40. package/templates/agent-os/universal/.claude/agents/implementation-agent.md +41 -0
  41. package/templates/agent-os/universal/.claude/agents/prose-reviewer.md +8 -4
  42. package/templates/agent-os/universal/.claude/agents/security-scanner.md +4 -2
  43. package/templates/agent-os/universal/.claude/agents/test-writer.md +2 -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 +47 -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/core.mjs +43 -0
  53. package/templates/agent-os/universal/.claude/scripts/queue/jira.mjs +18 -1
  54. package/templates/agent-os/universal/.claude/scripts/queue/state.mjs +2 -2
  55. package/templates/agent-os/universal/.claude/scripts/reconcile-external-prs.mjs +273 -35
  56. package/templates/agent-os/universal/.claude/scripts/revalidate.mjs +16 -0
  57. package/templates/agent-os/universal/.claude/settings.json +0 -8
  58. package/templates/agent-os/universal/.claude/skills/check-premises/SKILL.md +13 -9
  59. package/templates/agent-os/universal/.claude/skills/loop/SKILL.md +31 -6
  60. package/templates/agent-os/universal/.claude/skills/new-invariant/SKILL.md +5 -5
  61. package/templates/agent-os/universal/.claude/skills/pr-ship/SKILL.md +1 -1
  62. package/templates/agent-os/universal/.claude/skills/worktree-task/SKILL.md +3 -3
  63. package/templates/agent-os/universal/.codex/agents/code-reviewer.toml +2 -2
  64. package/templates/agent-os/universal/.codex/agents/implementation-agent.toml +6 -0
  65. package/templates/agent-os/universal/.codex/agents/prose-reviewer.toml +2 -2
  66. package/templates/agent-os/universal/.codex/agents/security-scanner.toml +1 -1
  67. package/templates/agent-os/universal/.codex/agents/test-writer.toml +1 -1
  68. package/templates/agent-os/universal/.codex/hooks.json +0 -10
  69. package/templates/agent-os/universal/AGENTS.md +208 -107
  70. package/templates/agent-os/universal/CLAUDE.md +15 -162
  71. package/templates/agent-os/universal/PLAN.md +17 -10
  72. package/templates/agent-os/universal/docs/decisions/agents-md-canonical.md +313 -0
  73. package/templates/agent-os/universal/docs/decisions/codex-adapter.md +15 -10
  74. package/templates/agent-os/universal/docs/decisions/review-lanes.md +12 -8
  75. package/templates/agent-os/universal/docs/decisions/session-start-wire-format.md +206 -0
  76. package/templates/agent-os/universal/docs/decisions/subagent-routing.md +6 -3
  77. package/templates/agent-os/universal/docs/decisions/workflow-layer-split.md +235 -0
  78. package/templates/agent-os/universal/layers.json +25 -30
  79. package/templates/hash-history.json +73 -24
  80. package/templates/release-ledger.json +3 -1
  81. package/packages/cli/dist/lib/composition.js +0 -20
  82. package/packages/cli/dist/lib/targets.js +0 -28
  83. package/packages/cli/dist/policy/benchmark/corpus.js +0 -165
  84. package/packages/cli/dist/policy/core/adapter.js +0 -18
  85. package/packages/cli/dist/policy/core/coverage.js +0 -253
  86. package/packages/cli/dist/policy/core/decision-record.js +0 -287
  87. package/packages/cli/dist/policy/core/declaration.js +0 -127
  88. package/packages/cli/dist/policy/core/evidence-matrix.js +0 -94
  89. package/packages/cli/dist/policy/core/probe.js +0 -442
  90. package/packages/cli/dist/policy/core/registry.js +0 -115
  91. package/packages/cli/dist/policy/core/validation.js +0 -275
  92. package/packages/cli/dist/policy/core/vocabulary.js +0 -123
  93. package/packages/cli/dist/policy/harness/claude.js +0 -47
  94. package/packages/cli/dist/policy/harness/codex.js +0 -87
  95. package/packages/cli/dist/policy/harness/index.js +0 -15
  96. package/packages/cli/dist/policy/harness/shared-hooks.js +0 -28
  97. package/packages/cli/dist/policy/index.js +0 -17
  98. package/templates/agent-os/init/AGENTS.md +0 -201
  99. package/templates/agent-os/init/CLAUDE.md +0 -201
  100. package/templates/agent-os/stack/aws-cdk/.agents/skills/post-deploy-verify/SKILL.md +0 -105
  101. package/templates/agent-os/stack/aws-cdk/.agents/skills/ro-debug/SKILL.md +0 -117
  102. package/templates/agent-os/stack/aws-cdk/.claude/agents/cdk-diff-reviewer.md +0 -89
  103. package/templates/agent-os/stack/aws-cdk/.claude/rules/aws-cdk.md +0 -105
  104. package/templates/agent-os/stack/aws-cdk/.claude/skills/post-deploy-verify/SKILL.md +0 -105
  105. package/templates/agent-os/stack/aws-cdk/.claude/skills/ro-debug/SKILL.md +0 -117
  106. package/templates/agent-os/stack/aws-cdk/.codex/agents/cdk-diff-reviewer.toml +0 -6
  107. package/templates/agent-os/stack/node-ts/.claude/hooks/dod-checks.json +0 -1
  108. package/templates/agent-os/stack/node-ts/.claude/rules/node-ts.md +0 -85
  109. package/templates/agent-os/universal/.claude/hooks/guard-core-purity.mjs +0 -89
  110. package/templates/agent-os/universal/.claude/hooks/guard-web-boundary.mjs +0 -68
  111. package/templates/agent-os/universal/.claude/rules/architecture.md +0 -81
  112. package/templates/skeleton/aws-serverless/.github/workflows/ci.yml +0 -28
  113. package/templates/skeleton/aws-serverless/.github/workflows/deploy.yml +0 -90
  114. package/templates/skeleton/aws-serverless/README.md +0 -181
  115. package/templates/skeleton/aws-serverless/apps/web/next.config.mjs +0 -17
  116. package/templates/skeleton/aws-serverless/apps/web/package.json +0 -19
  117. package/templates/skeleton/aws-serverless/apps/web/src/app/layout.tsx +0 -17
  118. package/templates/skeleton/aws-serverless/apps/web/src/app/page.tsx +0 -96
  119. package/templates/skeleton/aws-serverless/apps/web/src/lib/api.ts +0 -36
  120. package/templates/skeleton/aws-serverless/apps/web/src/lib/validate.ts +0 -23
  121. package/templates/skeleton/aws-serverless/apps/web/test/shared-validation.test.ts +0 -38
  122. package/templates/skeleton/aws-serverless/apps/web/tsconfig.json +0 -14
  123. package/templates/skeleton/aws-serverless/eslint.config.mjs +0 -20
  124. package/templates/skeleton/aws-serverless/gitignore +0 -56
  125. package/templates/skeleton/aws-serverless/infra/bin/app.ts +0 -79
  126. package/templates/skeleton/aws-serverless/infra/cdk.json +0 -3
  127. package/templates/skeleton/aws-serverless/infra/lib/app-stack.ts +0 -248
  128. package/templates/skeleton/aws-serverless/infra/lib/web-stack.ts +0 -45
  129. package/templates/skeleton/aws-serverless/infra/package.json +0 -18
  130. package/templates/skeleton/aws-serverless/infra/test/allowed-origins.test.ts +0 -301
  131. package/templates/skeleton/aws-serverless/infra/test/app-composition.test.ts +0 -137
  132. package/templates/skeleton/aws-serverless/infra/test/app-stack.test.ts +0 -120
  133. package/templates/skeleton/aws-serverless/infra/test/web-stack.test.ts +0 -44
  134. package/templates/skeleton/aws-serverless/package.json +0 -30
  135. package/templates/skeleton/aws-serverless/packages/core/package.json +0 -11
  136. package/templates/skeleton/aws-serverless/packages/core/src/events.ts +0 -14
  137. package/templates/skeleton/aws-serverless/packages/core/src/index.ts +0 -15
  138. package/templates/skeleton/aws-serverless/packages/core/src/note.ts +0 -69
  139. package/templates/skeleton/aws-serverless/packages/core/test/events.test.ts +0 -23
  140. package/templates/skeleton/aws-serverless/packages/core/test/note.test.ts +0 -101
  141. package/templates/skeleton/aws-serverless/packages/db/package.json +0 -14
  142. package/templates/skeleton/aws-serverless/packages/db/src/client.ts +0 -17
  143. package/templates/skeleton/aws-serverless/packages/db/src/index.ts +0 -2
  144. package/templates/skeleton/aws-serverless/packages/db/src/note-model.ts +0 -52
  145. package/templates/skeleton/aws-serverless/packages/db/test/note-model.test.ts +0 -91
  146. package/templates/skeleton/aws-serverless/packages/shared/package.json +0 -11
  147. package/templates/skeleton/aws-serverless/packages/shared/src/env.ts +0 -17
  148. package/templates/skeleton/aws-serverless/packages/shared/src/errors.ts +0 -33
  149. package/templates/skeleton/aws-serverless/packages/shared/src/index.ts +0 -3
  150. package/templates/skeleton/aws-serverless/packages/shared/src/logger.ts +0 -20
  151. package/templates/skeleton/aws-serverless/packages/shared/test/env.test.ts +0 -26
  152. package/templates/skeleton/aws-serverless/packages/shared/test/errors.test.ts +0 -28
  153. package/templates/skeleton/aws-serverless/packages/shared/test/logger.test.ts +0 -19
  154. package/templates/skeleton/aws-serverless/pnpm-lock.yaml +0 -2855
  155. package/templates/skeleton/aws-serverless/pnpm-workspace.yaml +0 -14
  156. package/templates/skeleton/aws-serverless/services/api/package.json +0 -15
  157. package/templates/skeleton/aws-serverless/services/api/src/adapters/sqs-publisher.ts +0 -26
  158. package/templates/skeleton/aws-serverless/services/api/src/handlers/create-note.ts +0 -69
  159. package/templates/skeleton/aws-serverless/services/api/src/handlers/list-notes.ts +0 -37
  160. package/templates/skeleton/aws-serverless/services/api/src/list-main.ts +0 -12
  161. package/templates/skeleton/aws-serverless/services/api/src/main.ts +0 -21
  162. package/templates/skeleton/aws-serverless/services/api/src/usecases/create-note.ts +0 -41
  163. package/templates/skeleton/aws-serverless/services/api/src/usecases/list-notes.ts +0 -14
  164. package/templates/skeleton/aws-serverless/services/api/test/create-note.handler.test.ts +0 -211
  165. package/templates/skeleton/aws-serverless/services/api/test/create-note.usecase.test.ts +0 -45
  166. package/templates/skeleton/aws-serverless/services/api/test/list-notes.test.ts +0 -122
  167. package/templates/skeleton/aws-serverless/services/api/test/sqs-publisher.test.ts +0 -22
  168. package/templates/skeleton/aws-serverless/services/worker/package.json +0 -12
  169. package/templates/skeleton/aws-serverless/services/worker/src/handlers/note-created.ts +0 -15
  170. package/templates/skeleton/aws-serverless/services/worker/src/main.ts +0 -7
  171. package/templates/skeleton/aws-serverless/services/worker/src/usecases/process-note-created.ts +0 -37
  172. package/templates/skeleton/aws-serverless/services/worker/test/note-created.test.ts +0 -61
  173. package/templates/skeleton/aws-serverless/tsconfig.base.json +0 -15
  174. package/templates/skeleton/aws-serverless/tsconfig.json +0 -16
  175. package/templates/skeleton/aws-serverless/vitest.config.ts +0 -14
  176. package/templates/skeleton/node-service/.github/workflows/ci.yml +0 -27
  177. package/templates/skeleton/node-service/.github/workflows/deploy.yml +0 -29
  178. package/templates/skeleton/node-service/README.md +0 -104
  179. package/templates/skeleton/node-service/apps/web/next.config.mjs +0 -17
  180. package/templates/skeleton/node-service/apps/web/package.json +0 -19
  181. package/templates/skeleton/node-service/apps/web/src/app/layout.tsx +0 -17
  182. package/templates/skeleton/node-service/apps/web/src/app/page.tsx +0 -96
  183. package/templates/skeleton/node-service/apps/web/src/lib/api.ts +0 -29
  184. package/templates/skeleton/node-service/apps/web/src/lib/validate.ts +0 -23
  185. package/templates/skeleton/node-service/apps/web/test/shared-validation.test.ts +0 -38
  186. package/templates/skeleton/node-service/apps/web/tsconfig.json +0 -14
  187. package/templates/skeleton/node-service/eslint.config.mjs +0 -20
  188. package/templates/skeleton/node-service/gitignore +0 -54
  189. package/templates/skeleton/node-service/package.json +0 -30
  190. package/templates/skeleton/node-service/packages/core/package.json +0 -11
  191. package/templates/skeleton/node-service/packages/core/src/events.ts +0 -14
  192. package/templates/skeleton/node-service/packages/core/src/index.ts +0 -15
  193. package/templates/skeleton/node-service/packages/core/src/note.ts +0 -69
  194. package/templates/skeleton/node-service/packages/core/test/events.test.ts +0 -23
  195. package/templates/skeleton/node-service/packages/core/test/note.test.ts +0 -101
  196. package/templates/skeleton/node-service/packages/db/package.json +0 -12
  197. package/templates/skeleton/node-service/packages/db/src/index.ts +0 -1
  198. package/templates/skeleton/node-service/packages/db/src/note-store.ts +0 -100
  199. package/templates/skeleton/node-service/packages/db/test/note-store.test.ts +0 -100
  200. package/templates/skeleton/node-service/packages/shared/package.json +0 -11
  201. package/templates/skeleton/node-service/packages/shared/src/env.ts +0 -17
  202. package/templates/skeleton/node-service/packages/shared/src/errors.ts +0 -33
  203. package/templates/skeleton/node-service/packages/shared/src/index.ts +0 -3
  204. package/templates/skeleton/node-service/packages/shared/src/logger.ts +0 -20
  205. package/templates/skeleton/node-service/packages/shared/test/env.test.ts +0 -26
  206. package/templates/skeleton/node-service/packages/shared/test/errors.test.ts +0 -28
  207. package/templates/skeleton/node-service/packages/shared/test/logger.test.ts +0 -19
  208. package/templates/skeleton/node-service/pnpm-lock.yaml +0 -2402
  209. package/templates/skeleton/node-service/pnpm-workspace.yaml +0 -13
  210. package/templates/skeleton/node-service/scripts/build-artifact.mjs +0 -34
  211. package/templates/skeleton/node-service/services/api/package.json +0 -17
  212. package/templates/skeleton/node-service/services/api/src/adapters/spool-publisher.ts +0 -23
  213. package/templates/skeleton/node-service/services/api/src/handlers/create-note.ts +0 -40
  214. package/templates/skeleton/node-service/services/api/src/handlers/list-notes.ts +0 -23
  215. package/templates/skeleton/node-service/services/api/src/main.ts +0 -40
  216. package/templates/skeleton/node-service/services/api/src/server.ts +0 -172
  217. package/templates/skeleton/node-service/services/api/src/static-dir.ts +0 -20
  218. package/templates/skeleton/node-service/services/api/src/usecases/create-note.ts +0 -30
  219. package/templates/skeleton/node-service/services/api/src/usecases/list-notes.ts +0 -14
  220. package/templates/skeleton/node-service/services/api/test/artifact.test.ts +0 -92
  221. package/templates/skeleton/node-service/services/api/test/create-note.handler.test.ts +0 -64
  222. package/templates/skeleton/node-service/services/api/test/create-note.usecase.test.ts +0 -43
  223. package/templates/skeleton/node-service/services/api/test/list-notes.test.ts +0 -48
  224. package/templates/skeleton/node-service/services/api/test/package-manager.test.ts +0 -40
  225. package/templates/skeleton/node-service/services/api/test/package-manager.ts +0 -51
  226. package/templates/skeleton/node-service/services/api/test/server.test.ts +0 -208
  227. package/templates/skeleton/node-service/services/api/test/spool-publisher.test.ts +0 -32
  228. package/templates/skeleton/node-service/services/api/test/static-dir.test.ts +0 -29
  229. package/templates/skeleton/node-service/services/worker/package.json +0 -16
  230. package/templates/skeleton/node-service/services/worker/src/main.ts +0 -28
  231. package/templates/skeleton/node-service/services/worker/src/spool.ts +0 -60
  232. package/templates/skeleton/node-service/services/worker/src/usecases/process-note-created.ts +0 -38
  233. package/templates/skeleton/node-service/services/worker/test/process-note-created.test.ts +0 -34
  234. package/templates/skeleton/node-service/services/worker/test/spool.test.ts +0 -76
  235. package/templates/skeleton/node-service/tsconfig.base.json +0 -15
  236. package/templates/skeleton/node-service/tsconfig.json +0 -13
  237. package/templates/skeleton/node-service/vitest.config.ts +0 -12
@@ -0,0 +1,468 @@
1
+ import { createHash, randomBytes } from 'node:crypto';
2
+ import { constants } from 'node:fs';
3
+ import { lstat, mkdir, open, rename, unlink } from 'node:fs/promises';
4
+ import path from 'node:path';
5
+ import { parseArgs } from 'node:util';
6
+ import { DECLARATION_REL, MAX_DECLARATION_BYTES, parseDeclaration, serializeDeclaration, } from '../integrations/declaration.js';
7
+ import { REGISTRY } from '../integrations/registry.js';
8
+ import { resolveReadableInside, resolveWritableInside } from '../lib/safe-path.js';
9
+ import { hasControlCharacter } from '../lib/safe-text.js';
10
+ import { editMcpServers, readMcpConfig } from '../integrations/mcp-json.js';
11
+ import { initFileContents } from './init.js';
12
+ import { MANIFEST_REL, parseManifest, sha256 } from '../lib/manifest.js';
13
+ import { subsystemsManifestPath } from '../lib/subsystems.js';
14
+ import { runSpecKitLifecycle, SPEC_KIT_VERSION, } from '../integrations/spec-kit.js';
15
+ const MCP = '.mcp.json';
16
+ const CODEX_CONFIG = '.codex/config.toml';
17
+ const MAX_MCP_BYTES = 64 * 1024;
18
+ export const INTEGRATIONS_VERBS = ['list', 'add', 'apply', 'remove'];
19
+ class Refusal extends Error {
20
+ }
21
+ const hash = (value) => createHash('sha256').update(JSON.stringify(value)).digest('hex');
22
+ function serverFor(id) {
23
+ if (id === 'figma-mcp')
24
+ return { name: 'figma', server: { type: 'http', url: 'https://mcp.figma.com/mcp' } };
25
+ if (id === 'atlassian-mcp')
26
+ return { name: 'atlassian', server: { type: 'http', url: 'https://mcp.atlassian.com/v2/mcp' } };
27
+ if (id === 'basic-memory')
28
+ return { name: 'basic-memory', server: { command: 'uvx', args: ['basic-memory', 'mcp'] } };
29
+ throw new Refusal('not-in-matrix');
30
+ }
31
+ function codexSection(id) {
32
+ const { name, server } = serverFor(id);
33
+ if ('type' in server && server.type === 'http')
34
+ return `[mcp_servers.${name}]\nurl = ${JSON.stringify(server.url)}\n`;
35
+ if ('command' in server && Array.isArray(server.args))
36
+ return `[mcp_servers.${name}]\ncommand = ${JSON.stringify(server.command)}\nargs = [${server.args.map((arg) => JSON.stringify(arg)).join(', ')}]\n`;
37
+ throw new Refusal('codex-provider-not-renderable');
38
+ }
39
+ async function hasMachineMemoryManifest(env) {
40
+ let file;
41
+ try {
42
+ file = subsystemsManifestPath(env, process.platform);
43
+ }
44
+ catch {
45
+ return false;
46
+ }
47
+ let info;
48
+ try {
49
+ info = await lstat(file);
50
+ if (!info.isFile() || info.isSymbolicLink() || info.size > MAX_MCP_BYTES)
51
+ return false;
52
+ }
53
+ catch {
54
+ return false;
55
+ }
56
+ const flags = constants.O_RDONLY |
57
+ (process.platform === 'win32' ? 0 : constants.O_NOFOLLOW | constants.O_NONBLOCK);
58
+ let handle;
59
+ try {
60
+ handle = await open(file, flags);
61
+ }
62
+ catch {
63
+ return false;
64
+ }
65
+ try {
66
+ const current = await handle.stat();
67
+ if (!current.isFile() || current.size > MAX_MCP_BYTES)
68
+ return false;
69
+ const bytes = Buffer.alloc(MAX_MCP_BYTES + 1);
70
+ let offset = 0;
71
+ while (offset < bytes.length) {
72
+ const { bytesRead } = await handle.read(bytes, offset, bytes.length - offset, null);
73
+ if (bytesRead === 0)
74
+ break;
75
+ offset += bytesRead;
76
+ }
77
+ if (offset > MAX_MCP_BYTES)
78
+ return false;
79
+ const text = new TextDecoder('utf-8', { fatal: true, ignoreBOM: true }).decode(bytes.subarray(0, offset));
80
+ const parsed = JSON.parse(text);
81
+ return (typeof parsed === 'object' &&
82
+ parsed !== null &&
83
+ typeof parsed.entries?.memory === 'object' &&
84
+ parsed.entries?.memory !== null);
85
+ }
86
+ catch {
87
+ return false;
88
+ }
89
+ finally {
90
+ await handle.close().catch(() => undefined);
91
+ }
92
+ }
93
+ function renderCodexConfig(base, entries) {
94
+ const sections = entries
95
+ .filter((entry) => entry.id !== 'spec-kit' && entry.harnesses?.includes('codex'))
96
+ .map((entry) => codexSection(entry.id))
97
+ .sort((a, b) => a.localeCompare(b));
98
+ return Buffer.from(`${base.replace(/\n*$/, '\n')}${sections.length ? `\n${sections.join('\n')}` : ''}`);
99
+ }
100
+ async function snapshot(root, rel) {
101
+ const resolved = await resolveReadableInside(root, rel, 'file');
102
+ if (resolved.status === 'absent')
103
+ return { rel, bytes: null };
104
+ if (resolved.status !== 'ok')
105
+ throw new Refusal('preimage-unreadable');
106
+ const max = rel === DECLARATION_REL ? MAX_DECLARATION_BYTES : MAX_MCP_BYTES;
107
+ const flags = constants.O_RDONLY |
108
+ (process.platform === 'win32' ? 0 : constants.O_NOFOLLOW | constants.O_NONBLOCK);
109
+ const handle = await open(resolved.path, flags);
110
+ try {
111
+ const info = await handle.stat();
112
+ if (!info.isFile() || info.size > max)
113
+ throw new Refusal('preimage-unreadable-or-too-large');
114
+ const buffer = Buffer.alloc(max + 1);
115
+ let offset = 0;
116
+ while (offset < buffer.length) {
117
+ const { bytesRead } = await handle.read(buffer, offset, buffer.length - offset, null);
118
+ if (bytesRead === 0)
119
+ break;
120
+ offset += bytesRead;
121
+ }
122
+ if (offset > max)
123
+ throw new Refusal('preimage-too-large');
124
+ return { rel, bytes: buffer.subarray(0, offset) };
125
+ }
126
+ finally {
127
+ await handle.close();
128
+ }
129
+ }
130
+ function decode(bytes) {
131
+ try {
132
+ return new TextDecoder('utf-8', { fatal: true, ignoreBOM: true }).decode(bytes);
133
+ }
134
+ catch {
135
+ throw new Refusal('preimage-invalid-utf8');
136
+ }
137
+ }
138
+ function readConfig(file) {
139
+ if (file.bytes === null)
140
+ return { mcpServers: {} };
141
+ try {
142
+ return readMcpConfig(decode(file.bytes));
143
+ }
144
+ catch {
145
+ throw new Refusal('mcp-config-invalid-or-ambiguous');
146
+ }
147
+ }
148
+ const equalBytes = (a, b) => a === null ? b === null : b !== null && a.equals(b);
149
+ async function checkSnapshots(root, files) {
150
+ for (const file of files) {
151
+ const current = await snapshot(root, file.rel);
152
+ if (!equalBytes(current.bytes, file.bytes))
153
+ throw new Refusal('changed-since-plan');
154
+ }
155
+ }
156
+ async function atomicWrite(root, rel, bytes) {
157
+ let dest = await resolveWritableInside(root, rel);
158
+ if (dest === null)
159
+ throw new Refusal('write-path-unsafe');
160
+ await mkdir(path.dirname(dest), { recursive: true });
161
+ dest = await resolveWritableInside(root, rel);
162
+ if (dest === null)
163
+ throw new Refusal('write-path-unsafe');
164
+ const temporary = path.join(path.dirname(dest), `.${path.basename(dest)}.${randomBytes(12).toString('hex')}.tmp`);
165
+ let created = false;
166
+ try {
167
+ const handle = await open(temporary, 'wx', 0o600);
168
+ created = true;
169
+ try {
170
+ await handle.writeFile(bytes);
171
+ }
172
+ finally {
173
+ await handle.close();
174
+ }
175
+ if ((await resolveWritableInside(root, rel)) !== dest)
176
+ throw new Refusal('write-path-changed');
177
+ await rename(temporary, dest);
178
+ created = false;
179
+ }
180
+ finally {
181
+ if (created)
182
+ await unlink(temporary).catch(() => undefined);
183
+ }
184
+ }
185
+ async function applyEdits(root, preimages, edits) {
186
+ await checkSnapshots(root, preimages);
187
+ for (const edit of edits) {
188
+ if ((await resolveWritableInside(root, edit.rel)) === null)
189
+ throw new Refusal('write-path-unsafe');
190
+ }
191
+ const written = [];
192
+ try {
193
+ for (const edit of edits) {
194
+ await checkSnapshots(root, [{ rel: edit.rel, bytes: edit.bytes }]);
195
+ await atomicWrite(root, edit.rel, edit.next);
196
+ written.push(edit);
197
+ }
198
+ }
199
+ catch (error) {
200
+ let restored = true;
201
+ for (const edit of written.reverse()) {
202
+ try {
203
+ await checkSnapshots(root, [{ rel: edit.rel, bytes: edit.next }]);
204
+ if (edit.bytes !== null)
205
+ await atomicWrite(root, edit.rel, edit.bytes);
206
+ else {
207
+ const dest = await resolveWritableInside(root, edit.rel);
208
+ if (dest === null)
209
+ throw new Refusal('rollback-path-unsafe');
210
+ await unlink(dest);
211
+ }
212
+ }
213
+ catch {
214
+ restored = false;
215
+ }
216
+ }
217
+ if (!restored)
218
+ throw new Refusal('write-failed-rollback-incomplete');
219
+ throw error;
220
+ }
221
+ }
222
+ export async function runIntegrationsCommand(options) {
223
+ const json = options.args.includes('--json');
224
+ const verb = INTEGRATIONS_VERBS.find((value) => value === options.verb);
225
+ const respond = (payload, prose, exitCode = 0) => ({
226
+ exitCode,
227
+ stdout: json
228
+ ? `${JSON.stringify({ schemaVersion: 1, command: 'setup', verb: verb ?? 'unknown', ...payload })}\n`
229
+ : exitCode === 0
230
+ ? `${prose}\n`
231
+ : '',
232
+ stderr: !json && exitCode !== 0 ? `${prose}\n` : '',
233
+ });
234
+ let upstreamApplied = false;
235
+ let observed;
236
+ try {
237
+ if (verb === undefined)
238
+ throw new Refusal(`unknown-verb; known: ${INTEGRATIONS_VERBS.join(', ')}`);
239
+ if (options.args.some((arg) => hasControlCharacter(arg)))
240
+ throw new Refusal('malformed-argument-control-character');
241
+ const { values, positionals } = parseArgs({
242
+ args: options.args,
243
+ allowPositionals: true,
244
+ options: {
245
+ json: { type: 'boolean' },
246
+ ...(verb === 'list'
247
+ ? {}
248
+ : {
249
+ yes: { type: 'boolean' },
250
+ 'dry-run': { type: 'boolean' },
251
+ ...(verb === 'add'
252
+ ? {
253
+ harness: { type: 'string', multiple: true },
254
+ required: { type: 'boolean' },
255
+ version: { type: 'string' },
256
+ adopt: { type: 'boolean' },
257
+ }
258
+ : {}),
259
+ }),
260
+ },
261
+ });
262
+ const registry = options.registry ?? REGISTRY;
263
+ if (verb === 'list') {
264
+ if (positionals.length)
265
+ throw new Refusal('list-takes-no-provider');
266
+ return respond({ integrations: registry }, registry.map((entry) => `${entry.id}: ${entry.displayName}`).join('\n'));
267
+ }
268
+ if (positionals.length > 1 || (verb !== 'apply' && positionals.length !== 1))
269
+ throw new Refusal(verb === 'apply' ? 'at-most-one-id-required' : 'exactly-one-id-required');
270
+ const id = positionals[0];
271
+ if (id !== undefined && !registry.some((entry) => entry.id === id))
272
+ throw new Refusal('not-in-matrix');
273
+ if (values.adopt && id !== 'spec-kit')
274
+ throw new Refusal('adopt-only-for-spec-kit');
275
+ const [declarationFile, mcpFile, codexFile, manifestFile] = await Promise.all([
276
+ snapshot(options.cwd, DECLARATION_REL),
277
+ snapshot(options.cwd, MCP),
278
+ snapshot(options.cwd, CODEX_CONFIG),
279
+ snapshot(options.cwd, MANIFEST_REL),
280
+ ]);
281
+ const parsed = declarationFile.bytes === null
282
+ ? { status: 'ok', entries: [], rejected: [] }
283
+ : parseDeclaration(decode(declarationFile.bytes), registry);
284
+ if (parsed.status !== 'ok' || parsed.rejected.length)
285
+ throw new Refusal('declaration-invalid-or-rejected');
286
+ let entries = structuredClone(parsed.entries);
287
+ let targets = structuredClone(parsed.targets);
288
+ if (verb === 'add') {
289
+ const previous = entries.find((entry) => entry.id === id);
290
+ const requested = values.harness;
291
+ if (requested !== undefined &&
292
+ (!Array.isArray(requested) ||
293
+ !requested.every((harness) => typeof harness === 'string') ||
294
+ new Set(requested).size !== requested.length))
295
+ throw new Refusal('malformed-provider-selection');
296
+ const harnesses = [
297
+ ...new Set([
298
+ ...(previous?.harnesses ?? []),
299
+ ...(requested ?? previous?.harnesses ?? ['claude-code']),
300
+ ]),
301
+ ];
302
+ const candidate = {
303
+ ...previous,
304
+ id: id,
305
+ selected: true,
306
+ harnesses: harnesses,
307
+ ...(values.required !== undefined ? { required: values.required } : {}),
308
+ ...(values.version !== undefined ? { version: values.version } : {}),
309
+ ...(id === 'spec-kit' ? { version: values.version ?? SPEC_KIT_VERSION } : {}),
310
+ };
311
+ const checked = parseDeclaration(JSON.stringify({ schemaVersion: 1, integrations: [candidate] }), registry);
312
+ if (checked.status !== 'ok' || checked.rejected.length || checked.entries.length !== 1)
313
+ throw new Refusal('malformed-provider-selection');
314
+ entries = [...entries.filter((entry) => entry.id !== id), checked.entries[0]];
315
+ }
316
+ const selected = entries.filter((entry) => id === undefined || entry.id === id);
317
+ if (id !== undefined && !selected.length)
318
+ throw new Refusal('integration-not-declared');
319
+ const specKit = selected.find((entry) => entry.id === 'spec-kit');
320
+ let upstreamOptions;
321
+ let upstreamPlan = [];
322
+ if (specKit) {
323
+ if (specKit.version !== SPEC_KIT_VERSION || specKit.targets !== undefined)
324
+ throw new Refusal('spec-kit-pinned-version-and-upstream-ownership-required');
325
+ upstreamOptions = {
326
+ repoDir: options.cwd,
327
+ operation: verb,
328
+ harnesses: specKit.harnesses ?? [],
329
+ managed: parsed.entries.some((entry) => entry.id === 'spec-kit'),
330
+ adopt: values.adopt === true,
331
+ consent: false,
332
+ };
333
+ const planned = await runSpecKitLifecycle(upstreamOptions);
334
+ if (planned.reason !== 'consent-required')
335
+ throw new Refusal(planned.reason ?? 'upstream-plan-refused');
336
+ upstreamPlan = planned.plan;
337
+ if (verb === 'remove')
338
+ entries = entries.filter((entry) => entry.id !== 'spec-kit');
339
+ }
340
+ const selectedMcp = selected.filter((entry) => entry.id !== 'spec-kit');
341
+ const needsClaude = selectedMcp.some((entry) => entry.harnesses?.includes('claude-code'));
342
+ const servers = needsClaude ? { ...readConfig(mcpFile).mcpServers } : {};
343
+ const mcpChanges = new Map();
344
+ for (const entry of selectedMcp) {
345
+ if (entry.harnesses === undefined || entry.harnesses.length === 0)
346
+ throw new Refusal('harness-unsupported-or-pending');
347
+ if (!entry.harnesses.includes('claude-code')) {
348
+ if (verb === 'remove')
349
+ entries = entries.filter((candidate) => candidate.id !== entry.id);
350
+ continue;
351
+ }
352
+ const { name, server } = serverFor(entry.id);
353
+ const exists = Object.hasOwn(servers, name);
354
+ const ownership = entry.targets?.['claude-code']?.entryHash;
355
+ if (exists &&
356
+ (ownership === undefined || hash(servers[name]) !== ownership || ownership !== hash(server)))
357
+ throw new Refusal('foreign-or-modified-mcp-entry');
358
+ if (verb === 'remove') {
359
+ if (!exists || ownership === undefined)
360
+ throw new Refusal('owned-mcp-entry-absent');
361
+ delete servers[name];
362
+ entries = entries.filter((candidate) => candidate.id !== entry.id);
363
+ mcpChanges.set(name, undefined);
364
+ }
365
+ else {
366
+ if (!exists) {
367
+ servers[name] = server;
368
+ mcpChanges.set(name, server);
369
+ }
370
+ entry.targets = { ...entry.targets, 'claude-code': { entryHash: hash(server) } };
371
+ }
372
+ }
373
+ const codexSelected = selectedMcp.some((entry) => entry.harnesses?.includes('codex'));
374
+ let nextCodex = codexFile.bytes;
375
+ if (codexSelected) {
376
+ const fragment = selectedMcp
377
+ .filter((entry) => entry.harnesses?.includes('codex'))
378
+ .map((entry) => codexSection(entry.id))
379
+ .join('\n');
380
+ const manifest = manifestFile.bytes === null ? null : parseManifest(decode(manifestFile.bytes));
381
+ if (manifest === null)
382
+ throw new Refusal('release-manifest-unreadable');
383
+ const base = (await initFileContents(options.cwd, manifest.project, manifest.layers)).get(CODEX_CONFIG);
384
+ if (base === undefined)
385
+ throw new Refusal('release-codex-baseline-unavailable');
386
+ if (codexFile.bytes === null) {
387
+ if (verb !== 'apply' || targets?.codex === undefined)
388
+ throw new Refusal('codex-config-absent-explicit-apply-required');
389
+ }
390
+ else if (targets?.codex !== undefined) {
391
+ if (sha256(codexFile.bytes) !== targets.codex.fileHash)
392
+ throw new Refusal(`codex-config-conflict; managed provider fragment:\n${fragment}`);
393
+ }
394
+ else if (manifest.files[CODEX_CONFIG] !== sha256(codexFile.bytes)) {
395
+ throw new Refusal(`codex-config-not-release-baseline; managed provider fragment:\n${fragment}`);
396
+ }
397
+ nextCodex = renderCodexConfig(base, entries);
398
+ targets = { ...(targets ?? {}), codex: { fileHash: sha256(nextCodex) } };
399
+ }
400
+ const nextDeclaration = Buffer.from(serializeDeclaration(entries, targets));
401
+ if (nextDeclaration.length > MAX_DECLARATION_BYTES)
402
+ throw new Refusal('declaration-too-large');
403
+ const nextMcp = mcpChanges.size > 0
404
+ ? Buffer.from(editMcpServers(mcpFile.bytes === null ? '{\n "mcpServers": {}\n}\n' : decode(mcpFile.bytes), mcpChanges))
405
+ : mcpFile.bytes;
406
+ if (nextMcp !== null && nextMcp.length > MAX_MCP_BYTES)
407
+ throw new Refusal('mcp-config-too-large');
408
+ const edits = [];
409
+ if (!equalBytes(mcpFile.bytes, nextMcp) && nextMcp !== null)
410
+ edits.push({ ...mcpFile, next: nextMcp });
411
+ if (!equalBytes(codexFile.bytes, nextCodex) && nextCodex !== null)
412
+ edits.push({ ...codexFile, next: nextCodex });
413
+ if (!equalBytes(declarationFile.bytes, nextDeclaration))
414
+ edits.push({ ...declarationFile, next: nextDeclaration });
415
+ const basicMemory = selected.some((entry) => entry.id === 'basic-memory');
416
+ const coexistence = basicMemory && (await hasMachineMemoryManifest(options.env ?? process.env))
417
+ ? ' existing Memory subsystem detected; Basic Memory can coexist, but this setup does not connect them.'
418
+ : '';
419
+ const basicBoundary = basicMemory
420
+ ? ' Basic Memory is a wiring-only preview: it configures local, per-machine storage only; does not automatically access Memory; does not synchronize across machines; and uvx is a launcher, not a verified runtime.'
421
+ : '';
422
+ const plan = `${verb}: ${selected.map((entry) => entry.id).join(', ') || 'no integrations'}; write ${edits.map((edit) => edit.rel).join(', ') || 'nothing'}. MCP wiring does not verify authorization, connectivity or trust.${basicBoundary}${coexistence}${upstreamPlan.length ? '\n' + upstreamPlan.join('\n') : ''}`;
423
+ if (values['dry-run'])
424
+ return respond({ outcome: 'planned', dryRun: true, changed: false, plan }, plan);
425
+ if (!values.yes &&
426
+ (json ||
427
+ options.isTTY === false ||
428
+ options.confirm === undefined ||
429
+ !(await options.confirm(plan))))
430
+ throw new Refusal('yes-required-for-json-or-noninteractive');
431
+ const preimages = [declarationFile, mcpFile, codexFile, manifestFile];
432
+ await checkSnapshots(options.cwd, preimages);
433
+ if (upstreamOptions) {
434
+ const rechecked = await runSpecKitLifecycle(upstreamOptions);
435
+ if (rechecked.reason !== 'consent-required' ||
436
+ JSON.stringify(rechecked.plan) !== JSON.stringify(upstreamPlan))
437
+ throw new Refusal('changed-since-plan');
438
+ const result = await (options.runSpecKit ?? runSpecKitLifecycle)({
439
+ ...upstreamOptions,
440
+ consent: true,
441
+ });
442
+ observed = result.observed;
443
+ if (!result.ok)
444
+ throw new Refusal(result.reason ?? 'upstream-incomplete-use-adopt-and-status');
445
+ upstreamApplied = true;
446
+ }
447
+ // An upstream lifecycle may rewrite files this operation neither writes nor
448
+ // reads: Spec Kit regenerates .codex/config.toml, as CRLF on Windows. The
449
+ // Codex config and release manifest are inputs only when a Codex MCP entry
450
+ // is rendered, so only then must they still match the plan.
451
+ await applyEdits(options.cwd, upstreamApplied && !codexSelected ? [declarationFile, mcpFile] : preimages, edits);
452
+ return respond({
453
+ outcome: verb === 'remove' ? 'removed' : 'written',
454
+ changed: edits.length !== 0,
455
+ ...(id === undefined ? {} : { id }),
456
+ integrations: entries,
457
+ ...(observed === undefined ? {} : { observed }),
458
+ }, plan);
459
+ }
460
+ catch (error) {
461
+ const failure = error instanceof Refusal ? error.message : 'invalid-arguments-or-unreadable-state';
462
+ const reason = upstreamApplied
463
+ ? `upstream-success-intent-not-recorded; use adopt/status; ${failure}`
464
+ : failure;
465
+ const diagnosis = observed === undefined ? '' : `\nUpstream status: ${JSON.stringify(observed)}`;
466
+ return respond({ outcome: 'refused', reason, ...(observed === undefined ? {} : { observed }) }, `setup: ${reason}${diagnosis}`, 1);
467
+ }
468
+ }
@@ -3,7 +3,11 @@
3
3
  // resolves the executable from the machine subsystem manifest `setup` wrote
4
4
  // (RP-147), performs `--version --json` first, refuses a foreign contract
5
5
  // major with exit 4 before doctor/load ever run, and otherwise hands the verb
6
- // and its arguments to Memory verbatim and returns Memory's answer unchanged.
6
+ // and its arguments to Memory and returns Memory's answer unchanged. The one
7
+ // thing it adds to the arguments: a `load` that names no `--timeout-ms` gets
8
+ // Memory's default internal deadline appended (`deadlinesFor`, RP-183);
9
+ // everything the caller wrote passes through verbatim, and `doctor` is never
10
+ // touched.
7
11
  //
8
12
  // What it never does, by construction: import Memory code, read Memory's
9
13
  // storage tree, or reinterpret a doctor/load payload — the only Memory bytes
@@ -22,8 +26,62 @@
22
26
  import { execFileRunner } from './setup.js';
23
27
  import { MEMORY_CONTRACT_MAJOR, SubsystemsError, handshake, readSubsystemsManifest, subsystemsManifestPath, } from '../lib/subsystems.js';
24
28
  const MEMORY_VERBS = ['doctor', 'load'];
25
- /** doctor/load do real work; the caller-supplied `--timeout-ms` is Memory's own bound. */
29
+ /**
30
+ * The outer deadline: the rig kills the Memory child after this many ms. It is
31
+ * the consumer's bound, and it should not be the one that fires first — a kill
32
+ * leaves no JSON answer behind. So `load` always carries an INTERNAL deadline
33
+ * as well (RP-183): Memory's own `--timeout-ms`, which per the Memory owner
34
+ * (RP-183, Jira) it answers with a typed result instead of being killed.
35
+ * `doctor` accepts no such flag per the same source — its args are never
36
+ * touched. Pinned in packages/cli/test/memory.test.ts, "RP-183".
37
+ */
26
38
  const VERB_TIMEOUT_MS = 60_000;
39
+ /** Memory's default internal deadline when the caller names none. */
40
+ const DEFAULT_MEMORY_TIMEOUT_MS = 45_000;
41
+ /**
42
+ * Headroom the outer deadline keeps above an explicit internal one. Sized per
43
+ * the Memory owner (RP-183, Jira): Memory's Windows cleanup may return up to
44
+ * 10 s after its own deadline fired; the rest is spawn overhead. The rig does
45
+ * not measure that lag itself.
46
+ */
47
+ const OUTER_MARGIN_MS = 15_000;
48
+ /**
49
+ * The largest delay `execFile`'s timer can hold: Node clamps a larger
50
+ * `setTimeout` to ONE millisecond, which would make the outer kill fire first
51
+ * — the inversion this whole helper exists to prevent. A caller who asks for
52
+ * more than ~24.8 days gets this ceiling as the outer deadline.
53
+ */
54
+ const MAX_TIMER_MS = 2_147_483_647;
55
+ const TIMEOUT_FLAG = '--timeout-ms';
56
+ /** Memory's own grammar for the value: a positive integer, milliseconds. */
57
+ const TIMEOUT_VALUE = /^[1-9][0-9]*$/;
58
+ /**
59
+ * The verb's argument list and outer deadline. `load` with no `--timeout-ms`
60
+ * gets the default appended; a caller-owned flag — any spelling — passes
61
+ * through verbatim, and only a well-formed `--timeout-ms <value>` pair can
62
+ * raise the outer deadline above it. Memory owns the refusal of a malformed
63
+ * value, so the rig neither corrects nor drops one.
64
+ */
65
+ const deadlinesFor = (verb, args) => {
66
+ if (verb !== 'load')
67
+ return { args: [...args], timeoutMs: VERB_TIMEOUT_MS };
68
+ const at = args.findIndex((arg) => arg === TIMEOUT_FLAG || arg.startsWith(`${TIMEOUT_FLAG}=`));
69
+ if (at === -1) {
70
+ return {
71
+ args: [...args, TIMEOUT_FLAG, String(DEFAULT_MEMORY_TIMEOUT_MS)],
72
+ timeoutMs: VERB_TIMEOUT_MS,
73
+ };
74
+ }
75
+ const value = args[at] === TIMEOUT_FLAG ? args[at + 1] : undefined;
76
+ const internal = value !== undefined && TIMEOUT_VALUE.test(value) ? Number(value) : null;
77
+ // A digit string too long for a safe integer — or for a double at all —
78
+ // still lands on the ceiling: `Number` yields a huge float or Infinity, and
79
+ // `Math.min` with the ceiling holds either.
80
+ const timeoutMs = internal !== null
81
+ ? Math.min(Math.max(VERB_TIMEOUT_MS, internal + OUTER_MARGIN_MS), MAX_TIMER_MS)
82
+ : VERB_TIMEOUT_MS;
83
+ return { args: [...args], timeoutMs };
84
+ };
27
85
  const jsonLine = (payload) => `${JSON.stringify(payload)}\n`;
28
86
  const isVerb = (verb) => MEMORY_VERBS.includes(verb);
29
87
  export async function runMemory(options) {
@@ -106,8 +164,9 @@ export async function runMemory(options) {
106
164
  stderr: `memory: the handshake did not answer as the manifest promised (${result.reason}); ${options.verb} was not run\n`,
107
165
  };
108
166
  const [command, script] = entry.invocation;
109
- const answer = await run(command, [script, options.verb, ...options.args], {
110
- timeoutMs: VERB_TIMEOUT_MS,
167
+ const deadlines = deadlinesFor(options.verb, options.args);
168
+ const answer = await run(command, [script, options.verb, ...deadlines.args], {
169
+ timeoutMs: deadlines.timeoutMs,
111
170
  });
112
171
  if (answer.spawnError)
113
172
  return {
@@ -0,0 +1,61 @@
1
+ import { createInterface } from 'node:readline/promises';
2
+ import { runIntegrationsCommand } from './integrations.js';
3
+ import { REGISTRY } from '../integrations/registry.js';
4
+ const PROVIDERS = REGISTRY.map((provider) => provider.id);
5
+ const HARNESSES = ['claude-code', 'codex', 'both'];
6
+ function refused(json, reason) {
7
+ return {
8
+ exitCode: 1,
9
+ stdout: json
10
+ ? `${JSON.stringify({ schemaVersion: 1, command: 'setup', outcome: 'refused', reason })}\n`
11
+ : '',
12
+ stderr: json ? '' : `setup: ${reason}\n`,
13
+ };
14
+ }
15
+ function selected(value, choices) {
16
+ if (value === undefined)
17
+ return undefined;
18
+ const index = Number(value);
19
+ if (Number.isInteger(index) && index >= 1 && index <= choices.length)
20
+ return choices[index - 1];
21
+ return choices.includes(value) ? value : undefined;
22
+ }
23
+ async function ask(step) {
24
+ const choices = step === 'provider' ? PROVIDERS : HARNESSES;
25
+ const label = step === 'provider' ? 'integration provider' : 'harness';
26
+ const display = choices.map((choice, index) => `${index + 1}) ${choice}`).join(', ');
27
+ const readline = createInterface({ input: process.stdin, output: process.stderr });
28
+ try {
29
+ const answer = await readline.question(`Select ${label} (${display}; Enter to cancel): `);
30
+ return answer.trim() === '' ? undefined : answer.trim();
31
+ }
32
+ finally {
33
+ readline.close();
34
+ }
35
+ }
36
+ /** Human-only selection surface; mutations remain in the deterministic command. */
37
+ export async function runSetupWizard(options) {
38
+ const json = options.args.includes('--json');
39
+ if (json)
40
+ return refused(true, 'setup-wizard-requires-an-interactive-terminal');
41
+ if (!options.isTTY)
42
+ return refused(false, 'setup-wizard-requires-an-interactive-terminal');
43
+ if (options.args.length !== 0)
44
+ return refused(false, 'setup-wizard-takes-no-options');
45
+ const prompt = options.promptChoice ?? ask;
46
+ const provider = selected(await prompt('provider'), PROVIDERS);
47
+ if (provider === undefined)
48
+ return { exitCode: 0, stdout: 'setup: cancelled\n', stderr: '' };
49
+ const harness = selected(await prompt('harness'), HARNESSES);
50
+ if (harness === undefined)
51
+ return { exitCode: 0, stdout: 'setup: cancelled\n', stderr: '' };
52
+ const selectedHarnesses = harness === 'both' ? ['claude-code', 'codex'] : [harness];
53
+ const args = [provider, ...selectedHarnesses.flatMap((item) => ['--harness', item])];
54
+ return (options.runIntegration ?? runIntegrationsCommand)({
55
+ verb: 'add',
56
+ args,
57
+ cwd: options.cwd,
58
+ isTTY: options.isTTY,
59
+ confirm: options.confirm,
60
+ });
61
+ }