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
@@ -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
+ }
@@ -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
+ }