create-agent-rig 0.9.1 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (234) hide show
  1. package/CHANGELOG.md +419 -3
  2. package/README.md +267 -305
  3. package/package.json +8 -14
  4. package/packages/cli/dist/commands/create.js +56 -88
  5. package/packages/cli/dist/commands/doctor.js +213 -0
  6. package/packages/cli/dist/commands/init.js +111 -50
  7. package/packages/cli/dist/commands/integrations.js +468 -0
  8. package/packages/cli/dist/commands/setup-wizard.js +61 -0
  9. package/packages/cli/dist/commands/uninstall.js +1223 -0
  10. package/packages/cli/dist/commands/upgrade.js +472 -90
  11. package/packages/cli/dist/index.js +651 -46
  12. package/packages/cli/dist/integrations/declaration.js +158 -0
  13. package/packages/cli/dist/integrations/doctor-guards.js +126 -0
  14. package/packages/cli/dist/integrations/doctor-workflow.js +25 -0
  15. package/packages/cli/dist/integrations/mcp-json.js +107 -0
  16. package/packages/cli/dist/integrations/memory-doctor.js +124 -0
  17. package/packages/cli/dist/integrations/registry.js +22 -0
  18. package/packages/cli/dist/integrations/spawn.js +228 -0
  19. package/packages/cli/dist/integrations/spec-kit.js +280 -0
  20. package/packages/cli/dist/integrations/verify.js +193 -0
  21. package/packages/cli/dist/integrations/windows-job.js +275 -0
  22. package/packages/cli/dist/lib/elevated-paths.js +74 -0
  23. package/packages/cli/dist/lib/install-set.js +1 -44
  24. package/packages/cli/dist/lib/manifest.js +64 -5
  25. package/packages/cli/dist/lib/prompts.js +0 -23
  26. package/packages/cli/dist/lib/safe-path.js +193 -2
  27. package/packages/cli/dist/lib/safe-text.js +78 -0
  28. package/packages/cli/dist/lib/substitute.js +1 -52
  29. package/packages/cli/dist/lib/summary.js +5 -5
  30. package/packages/cli/dist/templates.js +4 -14
  31. package/scripts/prepare.mjs +28 -12
  32. package/templates/agent-os/subagent-routing.json +4 -4
  33. package/templates/agent-os/universal/.agents/skills/check-premises/SKILL.md +13 -9
  34. package/templates/agent-os/universal/.agents/skills/loop/SKILL.md +31 -6
  35. package/templates/agent-os/universal/.agents/skills/new-invariant/SKILL.md +5 -5
  36. package/templates/agent-os/universal/.agents/skills/pr-ship/SKILL.md +1 -1
  37. package/templates/agent-os/universal/.agents/skills/worktree-task/SKILL.md +3 -3
  38. package/templates/agent-os/universal/.claude/agents/code-reviewer.md +8 -6
  39. package/templates/agent-os/universal/.claude/agents/implementation-agent.md +41 -0
  40. package/templates/agent-os/universal/.claude/agents/prose-reviewer.md +8 -4
  41. package/templates/agent-os/universal/.claude/agents/security-scanner.md +4 -2
  42. package/templates/agent-os/universal/.claude/agents/test-writer.md +2 -2
  43. package/templates/agent-os/universal/.claude/hooks/inject-rules.mjs +83 -4
  44. package/templates/agent-os/universal/.claude/rules/autonomy.md +55 -27
  45. package/templates/agent-os/universal/.claude/rules/invariants.md +9 -8
  46. package/templates/agent-os/universal/.claude/rules/workflow.md +47 -22
  47. package/templates/agent-os/universal/.claude/scripts/decision-router.mjs +1 -1
  48. package/templates/agent-os/universal/.claude/scripts/detect-missed-gate.mjs +12 -9
  49. package/templates/agent-os/universal/.claude/scripts/doctor.mjs +100 -2
  50. package/templates/agent-os/universal/.claude/scripts/lib/claim-records.mjs +236 -6
  51. package/templates/agent-os/universal/.claude/scripts/queue/state.mjs +2 -2
  52. package/templates/agent-os/universal/.claude/scripts/reconcile-external-prs.mjs +4 -3
  53. package/templates/agent-os/universal/.claude/scripts/revalidate.mjs +16 -0
  54. package/templates/agent-os/universal/.claude/settings.json +0 -8
  55. package/templates/agent-os/universal/.claude/skills/check-premises/SKILL.md +13 -9
  56. package/templates/agent-os/universal/.claude/skills/loop/SKILL.md +31 -6
  57. package/templates/agent-os/universal/.claude/skills/new-invariant/SKILL.md +5 -5
  58. package/templates/agent-os/universal/.claude/skills/pr-ship/SKILL.md +1 -1
  59. package/templates/agent-os/universal/.claude/skills/worktree-task/SKILL.md +3 -3
  60. package/templates/agent-os/universal/.codex/agents/code-reviewer.toml +2 -2
  61. package/templates/agent-os/universal/.codex/agents/implementation-agent.toml +6 -0
  62. package/templates/agent-os/universal/.codex/agents/prose-reviewer.toml +2 -2
  63. package/templates/agent-os/universal/.codex/agents/security-scanner.toml +1 -1
  64. package/templates/agent-os/universal/.codex/agents/test-writer.toml +1 -1
  65. package/templates/agent-os/universal/.codex/hooks.json +0 -10
  66. package/templates/agent-os/universal/AGENTS.md +208 -107
  67. package/templates/agent-os/universal/CLAUDE.md +15 -162
  68. package/templates/agent-os/universal/PLAN.md +17 -10
  69. package/templates/agent-os/universal/docs/decisions/agents-md-canonical.md +313 -0
  70. package/templates/agent-os/universal/docs/decisions/codex-adapter.md +15 -10
  71. package/templates/agent-os/universal/docs/decisions/review-lanes.md +12 -8
  72. package/templates/agent-os/universal/docs/decisions/session-start-wire-format.md +206 -0
  73. package/templates/agent-os/universal/docs/decisions/subagent-routing.md +6 -3
  74. package/templates/agent-os/universal/docs/decisions/workflow-layer-split.md +235 -0
  75. package/templates/agent-os/universal/layers.json +25 -30
  76. package/templates/hash-history.json +8 -4
  77. package/templates/release-ledger.json +2 -1
  78. package/packages/cli/dist/lib/composition.js +0 -20
  79. package/packages/cli/dist/lib/targets.js +0 -28
  80. package/packages/cli/dist/policy/benchmark/corpus.js +0 -165
  81. package/packages/cli/dist/policy/core/adapter.js +0 -18
  82. package/packages/cli/dist/policy/core/coverage.js +0 -253
  83. package/packages/cli/dist/policy/core/decision-record.js +0 -287
  84. package/packages/cli/dist/policy/core/declaration.js +0 -127
  85. package/packages/cli/dist/policy/core/evidence-matrix.js +0 -94
  86. package/packages/cli/dist/policy/core/probe.js +0 -442
  87. package/packages/cli/dist/policy/core/registry.js +0 -115
  88. package/packages/cli/dist/policy/core/validation.js +0 -275
  89. package/packages/cli/dist/policy/core/vocabulary.js +0 -123
  90. package/packages/cli/dist/policy/harness/claude.js +0 -47
  91. package/packages/cli/dist/policy/harness/codex.js +0 -87
  92. package/packages/cli/dist/policy/harness/index.js +0 -15
  93. package/packages/cli/dist/policy/harness/shared-hooks.js +0 -28
  94. package/packages/cli/dist/policy/index.js +0 -17
  95. package/templates/agent-os/init/AGENTS.md +0 -201
  96. package/templates/agent-os/init/CLAUDE.md +0 -201
  97. package/templates/agent-os/stack/aws-cdk/.agents/skills/post-deploy-verify/SKILL.md +0 -105
  98. package/templates/agent-os/stack/aws-cdk/.agents/skills/ro-debug/SKILL.md +0 -117
  99. package/templates/agent-os/stack/aws-cdk/.claude/agents/cdk-diff-reviewer.md +0 -89
  100. package/templates/agent-os/stack/aws-cdk/.claude/rules/aws-cdk.md +0 -105
  101. package/templates/agent-os/stack/aws-cdk/.claude/skills/post-deploy-verify/SKILL.md +0 -105
  102. package/templates/agent-os/stack/aws-cdk/.claude/skills/ro-debug/SKILL.md +0 -117
  103. package/templates/agent-os/stack/aws-cdk/.codex/agents/cdk-diff-reviewer.toml +0 -6
  104. package/templates/agent-os/stack/node-ts/.claude/hooks/dod-checks.json +0 -1
  105. package/templates/agent-os/stack/node-ts/.claude/rules/node-ts.md +0 -85
  106. package/templates/agent-os/universal/.claude/hooks/guard-core-purity.mjs +0 -89
  107. package/templates/agent-os/universal/.claude/hooks/guard-web-boundary.mjs +0 -68
  108. package/templates/agent-os/universal/.claude/rules/architecture.md +0 -81
  109. package/templates/skeleton/aws-serverless/.github/workflows/ci.yml +0 -28
  110. package/templates/skeleton/aws-serverless/.github/workflows/deploy.yml +0 -90
  111. package/templates/skeleton/aws-serverless/README.md +0 -181
  112. package/templates/skeleton/aws-serverless/apps/web/next.config.mjs +0 -17
  113. package/templates/skeleton/aws-serverless/apps/web/package.json +0 -19
  114. package/templates/skeleton/aws-serverless/apps/web/src/app/layout.tsx +0 -17
  115. package/templates/skeleton/aws-serverless/apps/web/src/app/page.tsx +0 -96
  116. package/templates/skeleton/aws-serverless/apps/web/src/lib/api.ts +0 -36
  117. package/templates/skeleton/aws-serverless/apps/web/src/lib/validate.ts +0 -23
  118. package/templates/skeleton/aws-serverless/apps/web/test/shared-validation.test.ts +0 -38
  119. package/templates/skeleton/aws-serverless/apps/web/tsconfig.json +0 -14
  120. package/templates/skeleton/aws-serverless/eslint.config.mjs +0 -20
  121. package/templates/skeleton/aws-serverless/gitignore +0 -56
  122. package/templates/skeleton/aws-serverless/infra/bin/app.ts +0 -79
  123. package/templates/skeleton/aws-serverless/infra/cdk.json +0 -3
  124. package/templates/skeleton/aws-serverless/infra/lib/app-stack.ts +0 -248
  125. package/templates/skeleton/aws-serverless/infra/lib/web-stack.ts +0 -45
  126. package/templates/skeleton/aws-serverless/infra/package.json +0 -18
  127. package/templates/skeleton/aws-serverless/infra/test/allowed-origins.test.ts +0 -301
  128. package/templates/skeleton/aws-serverless/infra/test/app-composition.test.ts +0 -137
  129. package/templates/skeleton/aws-serverless/infra/test/app-stack.test.ts +0 -120
  130. package/templates/skeleton/aws-serverless/infra/test/web-stack.test.ts +0 -44
  131. package/templates/skeleton/aws-serverless/package.json +0 -30
  132. package/templates/skeleton/aws-serverless/packages/core/package.json +0 -11
  133. package/templates/skeleton/aws-serverless/packages/core/src/events.ts +0 -14
  134. package/templates/skeleton/aws-serverless/packages/core/src/index.ts +0 -15
  135. package/templates/skeleton/aws-serverless/packages/core/src/note.ts +0 -69
  136. package/templates/skeleton/aws-serverless/packages/core/test/events.test.ts +0 -23
  137. package/templates/skeleton/aws-serverless/packages/core/test/note.test.ts +0 -101
  138. package/templates/skeleton/aws-serverless/packages/db/package.json +0 -14
  139. package/templates/skeleton/aws-serverless/packages/db/src/client.ts +0 -17
  140. package/templates/skeleton/aws-serverless/packages/db/src/index.ts +0 -2
  141. package/templates/skeleton/aws-serverless/packages/db/src/note-model.ts +0 -52
  142. package/templates/skeleton/aws-serverless/packages/db/test/note-model.test.ts +0 -91
  143. package/templates/skeleton/aws-serverless/packages/shared/package.json +0 -11
  144. package/templates/skeleton/aws-serverless/packages/shared/src/env.ts +0 -17
  145. package/templates/skeleton/aws-serverless/packages/shared/src/errors.ts +0 -33
  146. package/templates/skeleton/aws-serverless/packages/shared/src/index.ts +0 -3
  147. package/templates/skeleton/aws-serverless/packages/shared/src/logger.ts +0 -20
  148. package/templates/skeleton/aws-serverless/packages/shared/test/env.test.ts +0 -26
  149. package/templates/skeleton/aws-serverless/packages/shared/test/errors.test.ts +0 -28
  150. package/templates/skeleton/aws-serverless/packages/shared/test/logger.test.ts +0 -19
  151. package/templates/skeleton/aws-serverless/pnpm-lock.yaml +0 -2855
  152. package/templates/skeleton/aws-serverless/pnpm-workspace.yaml +0 -14
  153. package/templates/skeleton/aws-serverless/services/api/package.json +0 -15
  154. package/templates/skeleton/aws-serverless/services/api/src/adapters/sqs-publisher.ts +0 -26
  155. package/templates/skeleton/aws-serverless/services/api/src/handlers/create-note.ts +0 -69
  156. package/templates/skeleton/aws-serverless/services/api/src/handlers/list-notes.ts +0 -37
  157. package/templates/skeleton/aws-serverless/services/api/src/list-main.ts +0 -12
  158. package/templates/skeleton/aws-serverless/services/api/src/main.ts +0 -21
  159. package/templates/skeleton/aws-serverless/services/api/src/usecases/create-note.ts +0 -41
  160. package/templates/skeleton/aws-serverless/services/api/src/usecases/list-notes.ts +0 -14
  161. package/templates/skeleton/aws-serverless/services/api/test/create-note.handler.test.ts +0 -211
  162. package/templates/skeleton/aws-serverless/services/api/test/create-note.usecase.test.ts +0 -45
  163. package/templates/skeleton/aws-serverless/services/api/test/list-notes.test.ts +0 -122
  164. package/templates/skeleton/aws-serverless/services/api/test/sqs-publisher.test.ts +0 -22
  165. package/templates/skeleton/aws-serverless/services/worker/package.json +0 -12
  166. package/templates/skeleton/aws-serverless/services/worker/src/handlers/note-created.ts +0 -15
  167. package/templates/skeleton/aws-serverless/services/worker/src/main.ts +0 -7
  168. package/templates/skeleton/aws-serverless/services/worker/src/usecases/process-note-created.ts +0 -37
  169. package/templates/skeleton/aws-serverless/services/worker/test/note-created.test.ts +0 -61
  170. package/templates/skeleton/aws-serverless/tsconfig.base.json +0 -15
  171. package/templates/skeleton/aws-serverless/tsconfig.json +0 -16
  172. package/templates/skeleton/aws-serverless/vitest.config.ts +0 -14
  173. package/templates/skeleton/node-service/.github/workflows/ci.yml +0 -27
  174. package/templates/skeleton/node-service/.github/workflows/deploy.yml +0 -29
  175. package/templates/skeleton/node-service/README.md +0 -104
  176. package/templates/skeleton/node-service/apps/web/next.config.mjs +0 -17
  177. package/templates/skeleton/node-service/apps/web/package.json +0 -19
  178. package/templates/skeleton/node-service/apps/web/src/app/layout.tsx +0 -17
  179. package/templates/skeleton/node-service/apps/web/src/app/page.tsx +0 -96
  180. package/templates/skeleton/node-service/apps/web/src/lib/api.ts +0 -29
  181. package/templates/skeleton/node-service/apps/web/src/lib/validate.ts +0 -23
  182. package/templates/skeleton/node-service/apps/web/test/shared-validation.test.ts +0 -38
  183. package/templates/skeleton/node-service/apps/web/tsconfig.json +0 -14
  184. package/templates/skeleton/node-service/eslint.config.mjs +0 -20
  185. package/templates/skeleton/node-service/gitignore +0 -54
  186. package/templates/skeleton/node-service/package.json +0 -30
  187. package/templates/skeleton/node-service/packages/core/package.json +0 -11
  188. package/templates/skeleton/node-service/packages/core/src/events.ts +0 -14
  189. package/templates/skeleton/node-service/packages/core/src/index.ts +0 -15
  190. package/templates/skeleton/node-service/packages/core/src/note.ts +0 -69
  191. package/templates/skeleton/node-service/packages/core/test/events.test.ts +0 -23
  192. package/templates/skeleton/node-service/packages/core/test/note.test.ts +0 -101
  193. package/templates/skeleton/node-service/packages/db/package.json +0 -12
  194. package/templates/skeleton/node-service/packages/db/src/index.ts +0 -1
  195. package/templates/skeleton/node-service/packages/db/src/note-store.ts +0 -100
  196. package/templates/skeleton/node-service/packages/db/test/note-store.test.ts +0 -100
  197. package/templates/skeleton/node-service/packages/shared/package.json +0 -11
  198. package/templates/skeleton/node-service/packages/shared/src/env.ts +0 -17
  199. package/templates/skeleton/node-service/packages/shared/src/errors.ts +0 -33
  200. package/templates/skeleton/node-service/packages/shared/src/index.ts +0 -3
  201. package/templates/skeleton/node-service/packages/shared/src/logger.ts +0 -20
  202. package/templates/skeleton/node-service/packages/shared/test/env.test.ts +0 -26
  203. package/templates/skeleton/node-service/packages/shared/test/errors.test.ts +0 -28
  204. package/templates/skeleton/node-service/packages/shared/test/logger.test.ts +0 -19
  205. package/templates/skeleton/node-service/pnpm-lock.yaml +0 -2402
  206. package/templates/skeleton/node-service/pnpm-workspace.yaml +0 -13
  207. package/templates/skeleton/node-service/scripts/build-artifact.mjs +0 -34
  208. package/templates/skeleton/node-service/services/api/package.json +0 -17
  209. package/templates/skeleton/node-service/services/api/src/adapters/spool-publisher.ts +0 -23
  210. package/templates/skeleton/node-service/services/api/src/handlers/create-note.ts +0 -40
  211. package/templates/skeleton/node-service/services/api/src/handlers/list-notes.ts +0 -23
  212. package/templates/skeleton/node-service/services/api/src/main.ts +0 -40
  213. package/templates/skeleton/node-service/services/api/src/server.ts +0 -172
  214. package/templates/skeleton/node-service/services/api/src/static-dir.ts +0 -20
  215. package/templates/skeleton/node-service/services/api/src/usecases/create-note.ts +0 -30
  216. package/templates/skeleton/node-service/services/api/src/usecases/list-notes.ts +0 -14
  217. package/templates/skeleton/node-service/services/api/test/artifact.test.ts +0 -92
  218. package/templates/skeleton/node-service/services/api/test/create-note.handler.test.ts +0 -64
  219. package/templates/skeleton/node-service/services/api/test/create-note.usecase.test.ts +0 -43
  220. package/templates/skeleton/node-service/services/api/test/list-notes.test.ts +0 -48
  221. package/templates/skeleton/node-service/services/api/test/package-manager.test.ts +0 -40
  222. package/templates/skeleton/node-service/services/api/test/package-manager.ts +0 -51
  223. package/templates/skeleton/node-service/services/api/test/server.test.ts +0 -208
  224. package/templates/skeleton/node-service/services/api/test/spool-publisher.test.ts +0 -32
  225. package/templates/skeleton/node-service/services/api/test/static-dir.test.ts +0 -29
  226. package/templates/skeleton/node-service/services/worker/package.json +0 -16
  227. package/templates/skeleton/node-service/services/worker/src/main.ts +0 -28
  228. package/templates/skeleton/node-service/services/worker/src/spool.ts +0 -60
  229. package/templates/skeleton/node-service/services/worker/src/usecases/process-note-created.ts +0 -38
  230. package/templates/skeleton/node-service/services/worker/test/process-note-created.test.ts +0 -34
  231. package/templates/skeleton/node-service/services/worker/test/spool.test.ts +0 -76
  232. package/templates/skeleton/node-service/tsconfig.base.json +0 -15
  233. package/templates/skeleton/node-service/tsconfig.json +0 -13
  234. package/templates/skeleton/node-service/vitest.config.ts +0 -12
@@ -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
+ }