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.
- package/CHANGELOG.md +466 -3
- package/README.md +267 -299
- package/package.json +8 -14
- package/packages/cli/dist/commands/create.js +56 -88
- package/packages/cli/dist/commands/doctor.js +213 -0
- package/packages/cli/dist/commands/init.js +151 -54
- package/packages/cli/dist/commands/integrations.js +468 -0
- package/packages/cli/dist/commands/memory.js +63 -4
- package/packages/cli/dist/commands/setup-wizard.js +61 -0
- package/packages/cli/dist/commands/uninstall.js +1223 -0
- package/packages/cli/dist/commands/upgrade.js +489 -89
- package/packages/cli/dist/index.js +657 -49
- package/packages/cli/dist/integrations/declaration.js +158 -0
- package/packages/cli/dist/integrations/doctor-guards.js +126 -0
- package/packages/cli/dist/integrations/doctor-workflow.js +25 -0
- package/packages/cli/dist/integrations/mcp-json.js +107 -0
- package/packages/cli/dist/integrations/memory-doctor.js +124 -0
- package/packages/cli/dist/integrations/registry.js +22 -0
- package/packages/cli/dist/integrations/spawn.js +228 -0
- package/packages/cli/dist/integrations/spec-kit.js +280 -0
- package/packages/cli/dist/integrations/verify.js +193 -0
- package/packages/cli/dist/integrations/windows-job.js +275 -0
- package/packages/cli/dist/lib/elevated-paths.js +74 -0
- package/packages/cli/dist/lib/install-set.js +1 -44
- package/packages/cli/dist/lib/manifest.js +86 -9
- package/packages/cli/dist/lib/prompts.js +0 -23
- package/packages/cli/dist/lib/safe-path.js +193 -2
- package/packages/cli/dist/lib/safe-text.js +78 -0
- package/packages/cli/dist/lib/substitute.js +1 -52
- package/packages/cli/dist/lib/summary.js +5 -5
- package/packages/cli/dist/templates.js +4 -14
- package/scripts/prepare.mjs +28 -12
- package/templates/agent-os/subagent-routing.json +4 -4
- package/templates/agent-os/universal/.agents/skills/check-premises/SKILL.md +13 -9
- package/templates/agent-os/universal/.agents/skills/loop/SKILL.md +31 -6
- package/templates/agent-os/universal/.agents/skills/new-invariant/SKILL.md +5 -5
- package/templates/agent-os/universal/.agents/skills/pr-ship/SKILL.md +1 -1
- package/templates/agent-os/universal/.agents/skills/worktree-task/SKILL.md +3 -3
- package/templates/agent-os/universal/.claude/agents/code-reviewer.md +8 -6
- package/templates/agent-os/universal/.claude/agents/implementation-agent.md +41 -0
- package/templates/agent-os/universal/.claude/agents/prose-reviewer.md +8 -4
- package/templates/agent-os/universal/.claude/agents/security-scanner.md +4 -2
- package/templates/agent-os/universal/.claude/agents/test-writer.md +2 -2
- package/templates/agent-os/universal/.claude/hooks/inject-rules.mjs +83 -4
- package/templates/agent-os/universal/.claude/rules/autonomy.md +55 -27
- package/templates/agent-os/universal/.claude/rules/invariants.md +9 -8
- package/templates/agent-os/universal/.claude/rules/workflow.md +47 -22
- package/templates/agent-os/universal/.claude/scripts/decision-router.mjs +1 -1
- package/templates/agent-os/universal/.claude/scripts/detect-missed-gate.mjs +12 -9
- package/templates/agent-os/universal/.claude/scripts/doctor.mjs +100 -2
- package/templates/agent-os/universal/.claude/scripts/lib/claim-records.mjs +236 -6
- package/templates/agent-os/universal/.claude/scripts/queue/core.mjs +43 -0
- package/templates/agent-os/universal/.claude/scripts/queue/jira.mjs +18 -1
- package/templates/agent-os/universal/.claude/scripts/queue/state.mjs +2 -2
- package/templates/agent-os/universal/.claude/scripts/reconcile-external-prs.mjs +273 -35
- package/templates/agent-os/universal/.claude/scripts/revalidate.mjs +16 -0
- package/templates/agent-os/universal/.claude/settings.json +0 -8
- package/templates/agent-os/universal/.claude/skills/check-premises/SKILL.md +13 -9
- package/templates/agent-os/universal/.claude/skills/loop/SKILL.md +31 -6
- package/templates/agent-os/universal/.claude/skills/new-invariant/SKILL.md +5 -5
- package/templates/agent-os/universal/.claude/skills/pr-ship/SKILL.md +1 -1
- package/templates/agent-os/universal/.claude/skills/worktree-task/SKILL.md +3 -3
- package/templates/agent-os/universal/.codex/agents/code-reviewer.toml +2 -2
- package/templates/agent-os/universal/.codex/agents/implementation-agent.toml +6 -0
- package/templates/agent-os/universal/.codex/agents/prose-reviewer.toml +2 -2
- package/templates/agent-os/universal/.codex/agents/security-scanner.toml +1 -1
- package/templates/agent-os/universal/.codex/agents/test-writer.toml +1 -1
- package/templates/agent-os/universal/.codex/hooks.json +0 -10
- package/templates/agent-os/universal/AGENTS.md +208 -107
- package/templates/agent-os/universal/CLAUDE.md +15 -162
- package/templates/agent-os/universal/PLAN.md +17 -10
- package/templates/agent-os/universal/docs/decisions/agents-md-canonical.md +313 -0
- package/templates/agent-os/universal/docs/decisions/codex-adapter.md +15 -10
- package/templates/agent-os/universal/docs/decisions/review-lanes.md +12 -8
- package/templates/agent-os/universal/docs/decisions/session-start-wire-format.md +206 -0
- package/templates/agent-os/universal/docs/decisions/subagent-routing.md +6 -3
- package/templates/agent-os/universal/docs/decisions/workflow-layer-split.md +235 -0
- package/templates/agent-os/universal/layers.json +25 -30
- package/templates/hash-history.json +73 -24
- package/templates/release-ledger.json +3 -1
- package/packages/cli/dist/lib/composition.js +0 -20
- package/packages/cli/dist/lib/targets.js +0 -28
- package/packages/cli/dist/policy/benchmark/corpus.js +0 -165
- package/packages/cli/dist/policy/core/adapter.js +0 -18
- package/packages/cli/dist/policy/core/coverage.js +0 -253
- package/packages/cli/dist/policy/core/decision-record.js +0 -287
- package/packages/cli/dist/policy/core/declaration.js +0 -127
- package/packages/cli/dist/policy/core/evidence-matrix.js +0 -94
- package/packages/cli/dist/policy/core/probe.js +0 -442
- package/packages/cli/dist/policy/core/registry.js +0 -115
- package/packages/cli/dist/policy/core/validation.js +0 -275
- package/packages/cli/dist/policy/core/vocabulary.js +0 -123
- package/packages/cli/dist/policy/harness/claude.js +0 -47
- package/packages/cli/dist/policy/harness/codex.js +0 -87
- package/packages/cli/dist/policy/harness/index.js +0 -15
- package/packages/cli/dist/policy/harness/shared-hooks.js +0 -28
- package/packages/cli/dist/policy/index.js +0 -17
- package/templates/agent-os/init/AGENTS.md +0 -201
- package/templates/agent-os/init/CLAUDE.md +0 -201
- package/templates/agent-os/stack/aws-cdk/.agents/skills/post-deploy-verify/SKILL.md +0 -105
- package/templates/agent-os/stack/aws-cdk/.agents/skills/ro-debug/SKILL.md +0 -117
- package/templates/agent-os/stack/aws-cdk/.claude/agents/cdk-diff-reviewer.md +0 -89
- package/templates/agent-os/stack/aws-cdk/.claude/rules/aws-cdk.md +0 -105
- package/templates/agent-os/stack/aws-cdk/.claude/skills/post-deploy-verify/SKILL.md +0 -105
- package/templates/agent-os/stack/aws-cdk/.claude/skills/ro-debug/SKILL.md +0 -117
- package/templates/agent-os/stack/aws-cdk/.codex/agents/cdk-diff-reviewer.toml +0 -6
- package/templates/agent-os/stack/node-ts/.claude/hooks/dod-checks.json +0 -1
- package/templates/agent-os/stack/node-ts/.claude/rules/node-ts.md +0 -85
- package/templates/agent-os/universal/.claude/hooks/guard-core-purity.mjs +0 -89
- package/templates/agent-os/universal/.claude/hooks/guard-web-boundary.mjs +0 -68
- package/templates/agent-os/universal/.claude/rules/architecture.md +0 -81
- package/templates/skeleton/aws-serverless/.github/workflows/ci.yml +0 -28
- package/templates/skeleton/aws-serverless/.github/workflows/deploy.yml +0 -90
- package/templates/skeleton/aws-serverless/README.md +0 -181
- package/templates/skeleton/aws-serverless/apps/web/next.config.mjs +0 -17
- package/templates/skeleton/aws-serverless/apps/web/package.json +0 -19
- package/templates/skeleton/aws-serverless/apps/web/src/app/layout.tsx +0 -17
- package/templates/skeleton/aws-serverless/apps/web/src/app/page.tsx +0 -96
- package/templates/skeleton/aws-serverless/apps/web/src/lib/api.ts +0 -36
- package/templates/skeleton/aws-serverless/apps/web/src/lib/validate.ts +0 -23
- package/templates/skeleton/aws-serverless/apps/web/test/shared-validation.test.ts +0 -38
- package/templates/skeleton/aws-serverless/apps/web/tsconfig.json +0 -14
- package/templates/skeleton/aws-serverless/eslint.config.mjs +0 -20
- package/templates/skeleton/aws-serverless/gitignore +0 -56
- package/templates/skeleton/aws-serverless/infra/bin/app.ts +0 -79
- package/templates/skeleton/aws-serverless/infra/cdk.json +0 -3
- package/templates/skeleton/aws-serverless/infra/lib/app-stack.ts +0 -248
- package/templates/skeleton/aws-serverless/infra/lib/web-stack.ts +0 -45
- package/templates/skeleton/aws-serverless/infra/package.json +0 -18
- package/templates/skeleton/aws-serverless/infra/test/allowed-origins.test.ts +0 -301
- package/templates/skeleton/aws-serverless/infra/test/app-composition.test.ts +0 -137
- package/templates/skeleton/aws-serverless/infra/test/app-stack.test.ts +0 -120
- package/templates/skeleton/aws-serverless/infra/test/web-stack.test.ts +0 -44
- package/templates/skeleton/aws-serverless/package.json +0 -30
- package/templates/skeleton/aws-serverless/packages/core/package.json +0 -11
- package/templates/skeleton/aws-serverless/packages/core/src/events.ts +0 -14
- package/templates/skeleton/aws-serverless/packages/core/src/index.ts +0 -15
- package/templates/skeleton/aws-serverless/packages/core/src/note.ts +0 -69
- package/templates/skeleton/aws-serverless/packages/core/test/events.test.ts +0 -23
- package/templates/skeleton/aws-serverless/packages/core/test/note.test.ts +0 -101
- package/templates/skeleton/aws-serverless/packages/db/package.json +0 -14
- package/templates/skeleton/aws-serverless/packages/db/src/client.ts +0 -17
- package/templates/skeleton/aws-serverless/packages/db/src/index.ts +0 -2
- package/templates/skeleton/aws-serverless/packages/db/src/note-model.ts +0 -52
- package/templates/skeleton/aws-serverless/packages/db/test/note-model.test.ts +0 -91
- package/templates/skeleton/aws-serverless/packages/shared/package.json +0 -11
- package/templates/skeleton/aws-serverless/packages/shared/src/env.ts +0 -17
- package/templates/skeleton/aws-serverless/packages/shared/src/errors.ts +0 -33
- package/templates/skeleton/aws-serverless/packages/shared/src/index.ts +0 -3
- package/templates/skeleton/aws-serverless/packages/shared/src/logger.ts +0 -20
- package/templates/skeleton/aws-serverless/packages/shared/test/env.test.ts +0 -26
- package/templates/skeleton/aws-serverless/packages/shared/test/errors.test.ts +0 -28
- package/templates/skeleton/aws-serverless/packages/shared/test/logger.test.ts +0 -19
- package/templates/skeleton/aws-serverless/pnpm-lock.yaml +0 -2855
- package/templates/skeleton/aws-serverless/pnpm-workspace.yaml +0 -14
- package/templates/skeleton/aws-serverless/services/api/package.json +0 -15
- package/templates/skeleton/aws-serverless/services/api/src/adapters/sqs-publisher.ts +0 -26
- package/templates/skeleton/aws-serverless/services/api/src/handlers/create-note.ts +0 -69
- package/templates/skeleton/aws-serverless/services/api/src/handlers/list-notes.ts +0 -37
- package/templates/skeleton/aws-serverless/services/api/src/list-main.ts +0 -12
- package/templates/skeleton/aws-serverless/services/api/src/main.ts +0 -21
- package/templates/skeleton/aws-serverless/services/api/src/usecases/create-note.ts +0 -41
- package/templates/skeleton/aws-serverless/services/api/src/usecases/list-notes.ts +0 -14
- package/templates/skeleton/aws-serverless/services/api/test/create-note.handler.test.ts +0 -211
- package/templates/skeleton/aws-serverless/services/api/test/create-note.usecase.test.ts +0 -45
- package/templates/skeleton/aws-serverless/services/api/test/list-notes.test.ts +0 -122
- package/templates/skeleton/aws-serverless/services/api/test/sqs-publisher.test.ts +0 -22
- package/templates/skeleton/aws-serverless/services/worker/package.json +0 -12
- package/templates/skeleton/aws-serverless/services/worker/src/handlers/note-created.ts +0 -15
- package/templates/skeleton/aws-serverless/services/worker/src/main.ts +0 -7
- package/templates/skeleton/aws-serverless/services/worker/src/usecases/process-note-created.ts +0 -37
- package/templates/skeleton/aws-serverless/services/worker/test/note-created.test.ts +0 -61
- package/templates/skeleton/aws-serverless/tsconfig.base.json +0 -15
- package/templates/skeleton/aws-serverless/tsconfig.json +0 -16
- package/templates/skeleton/aws-serverless/vitest.config.ts +0 -14
- package/templates/skeleton/node-service/.github/workflows/ci.yml +0 -27
- package/templates/skeleton/node-service/.github/workflows/deploy.yml +0 -29
- package/templates/skeleton/node-service/README.md +0 -104
- package/templates/skeleton/node-service/apps/web/next.config.mjs +0 -17
- package/templates/skeleton/node-service/apps/web/package.json +0 -19
- package/templates/skeleton/node-service/apps/web/src/app/layout.tsx +0 -17
- package/templates/skeleton/node-service/apps/web/src/app/page.tsx +0 -96
- package/templates/skeleton/node-service/apps/web/src/lib/api.ts +0 -29
- package/templates/skeleton/node-service/apps/web/src/lib/validate.ts +0 -23
- package/templates/skeleton/node-service/apps/web/test/shared-validation.test.ts +0 -38
- package/templates/skeleton/node-service/apps/web/tsconfig.json +0 -14
- package/templates/skeleton/node-service/eslint.config.mjs +0 -20
- package/templates/skeleton/node-service/gitignore +0 -54
- package/templates/skeleton/node-service/package.json +0 -30
- package/templates/skeleton/node-service/packages/core/package.json +0 -11
- package/templates/skeleton/node-service/packages/core/src/events.ts +0 -14
- package/templates/skeleton/node-service/packages/core/src/index.ts +0 -15
- package/templates/skeleton/node-service/packages/core/src/note.ts +0 -69
- package/templates/skeleton/node-service/packages/core/test/events.test.ts +0 -23
- package/templates/skeleton/node-service/packages/core/test/note.test.ts +0 -101
- package/templates/skeleton/node-service/packages/db/package.json +0 -12
- package/templates/skeleton/node-service/packages/db/src/index.ts +0 -1
- package/templates/skeleton/node-service/packages/db/src/note-store.ts +0 -100
- package/templates/skeleton/node-service/packages/db/test/note-store.test.ts +0 -100
- package/templates/skeleton/node-service/packages/shared/package.json +0 -11
- package/templates/skeleton/node-service/packages/shared/src/env.ts +0 -17
- package/templates/skeleton/node-service/packages/shared/src/errors.ts +0 -33
- package/templates/skeleton/node-service/packages/shared/src/index.ts +0 -3
- package/templates/skeleton/node-service/packages/shared/src/logger.ts +0 -20
- package/templates/skeleton/node-service/packages/shared/test/env.test.ts +0 -26
- package/templates/skeleton/node-service/packages/shared/test/errors.test.ts +0 -28
- package/templates/skeleton/node-service/packages/shared/test/logger.test.ts +0 -19
- package/templates/skeleton/node-service/pnpm-lock.yaml +0 -2402
- package/templates/skeleton/node-service/pnpm-workspace.yaml +0 -13
- package/templates/skeleton/node-service/scripts/build-artifact.mjs +0 -34
- package/templates/skeleton/node-service/services/api/package.json +0 -17
- package/templates/skeleton/node-service/services/api/src/adapters/spool-publisher.ts +0 -23
- package/templates/skeleton/node-service/services/api/src/handlers/create-note.ts +0 -40
- package/templates/skeleton/node-service/services/api/src/handlers/list-notes.ts +0 -23
- package/templates/skeleton/node-service/services/api/src/main.ts +0 -40
- package/templates/skeleton/node-service/services/api/src/server.ts +0 -172
- package/templates/skeleton/node-service/services/api/src/static-dir.ts +0 -20
- package/templates/skeleton/node-service/services/api/src/usecases/create-note.ts +0 -30
- package/templates/skeleton/node-service/services/api/src/usecases/list-notes.ts +0 -14
- package/templates/skeleton/node-service/services/api/test/artifact.test.ts +0 -92
- package/templates/skeleton/node-service/services/api/test/create-note.handler.test.ts +0 -64
- package/templates/skeleton/node-service/services/api/test/create-note.usecase.test.ts +0 -43
- package/templates/skeleton/node-service/services/api/test/list-notes.test.ts +0 -48
- package/templates/skeleton/node-service/services/api/test/package-manager.test.ts +0 -40
- package/templates/skeleton/node-service/services/api/test/package-manager.ts +0 -51
- package/templates/skeleton/node-service/services/api/test/server.test.ts +0 -208
- package/templates/skeleton/node-service/services/api/test/spool-publisher.test.ts +0 -32
- package/templates/skeleton/node-service/services/api/test/static-dir.test.ts +0 -29
- package/templates/skeleton/node-service/services/worker/package.json +0 -16
- package/templates/skeleton/node-service/services/worker/src/main.ts +0 -28
- package/templates/skeleton/node-service/services/worker/src/spool.ts +0 -60
- package/templates/skeleton/node-service/services/worker/src/usecases/process-note-created.ts +0 -38
- package/templates/skeleton/node-service/services/worker/test/process-note-created.test.ts +0 -34
- package/templates/skeleton/node-service/services/worker/test/spool.test.ts +0 -76
- package/templates/skeleton/node-service/tsconfig.base.json +0 -15
- package/templates/skeleton/node-service/tsconfig.json +0 -13
- 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
|
|
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
|
-
/**
|
|
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
|
|
110
|
-
|
|
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
|
+
}
|