great-cto 3.72.0 → 3.74.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 (90) hide show
  1. package/board/.claude-plugin/plugin.json +1 -1
  2. package/board/agents/accounting-reviewer.md +2 -2
  3. package/board/agents/adtech-privacy-reviewer.md +3 -3
  4. package/board/agents/ai-eval-engineer.md +3 -3
  5. package/board/agents/ai-prompt-architect.md +3 -3
  6. package/board/agents/ai-security-reviewer.md +2 -2
  7. package/board/agents/api-platform-reviewer.md +2 -2
  8. package/board/agents/app-scaffolder.md +3 -3
  9. package/board/agents/architect.md +8 -8
  10. package/board/agents/auth-engineer.md +4 -4
  11. package/board/agents/ci-resolver.md +5 -5
  12. package/board/agents/cli-reviewer.md +2 -2
  13. package/board/agents/cmmc-reviewer.md +2 -2
  14. package/board/agents/cms-reviewer.md +2 -2
  15. package/board/agents/code-reviewer.md +4 -4
  16. package/board/agents/connector-builder.md +5 -5
  17. package/board/agents/continuous-learner.md +3 -3
  18. package/board/agents/coordinator.md +4 -4
  19. package/board/agents/data-platform-reviewer.md +2 -2
  20. package/board/agents/db-migration-reviewer.md +2 -2
  21. package/board/agents/decision-scorer.md +3 -3
  22. package/board/agents/design-advisor.md +3 -3
  23. package/board/agents/devops.md +12 -12
  24. package/board/agents/devtools-reviewer.md +2 -2
  25. package/board/agents/dpdpa-reviewer.md +2 -2
  26. package/board/agents/e2e-test-engineer.md +1 -1
  27. package/board/agents/edtech-reviewer.md +3 -3
  28. package/board/agents/enterprise-saas-reviewer.md +2 -2
  29. package/board/agents/firmware-reviewer.md +2 -2
  30. package/board/agents/game-reviewer.md +2 -2
  31. package/board/agents/gdpr-reviewer.md +3 -3
  32. package/board/agents/geo-routing-engineer.md +5 -5
  33. package/board/agents/gov-reviewer.md +2 -2
  34. package/board/agents/growth-engineer.md +3 -3
  35. package/board/agents/healthcare-reviewer.md +3 -3
  36. package/board/agents/hr-ai-reviewer.md +3 -3
  37. package/board/agents/infra-provisioner.md +3 -3
  38. package/board/agents/infra-reviewer.md +2 -2
  39. package/board/agents/insurance-reviewer.md +2 -2
  40. package/board/agents/integrations-engineer.md +5 -5
  41. package/board/agents/knowledge-extractor.md +2 -2
  42. package/board/agents/l3-support.md +8 -8
  43. package/board/agents/legal-reviewer.md +3 -3
  44. package/board/agents/library-reviewer.md +2 -2
  45. package/board/agents/marketplace-reviewer.md +2 -2
  46. package/board/agents/mcp-server-reviewer.md +2 -2
  47. package/board/agents/media-pipeline-engineer.md +5 -5
  48. package/board/agents/migration-import-engineer.md +5 -5
  49. package/board/agents/mlops-reviewer.md +2 -2
  50. package/board/agents/mobile-app-builder.md +6 -6
  51. package/board/agents/mobile-store-reviewer.md +2 -2
  52. package/board/agents/msp-reviewer.md +2 -2
  53. package/board/agents/oracle-reviewer.md +2 -2
  54. package/board/agents/pci-reviewer.md +2 -2
  55. package/board/agents/performance-engineer.md +3 -3
  56. package/board/agents/pm.md +6 -6
  57. package/board/agents/procurement-reviewer.md +2 -2
  58. package/board/agents/product-owner.md +5 -5
  59. package/board/agents/project-auditor.md +6 -6
  60. package/board/agents/qa-engineer.md +11 -11
  61. package/board/agents/quant-researcher.md +2 -2
  62. package/board/agents/rcm-reviewer.md +3 -3
  63. package/board/agents/regulated-reviewer.md +2 -2
  64. package/board/agents/security-officer.md +8 -8
  65. package/board/agents/senior-dev.md +7 -7
  66. package/board/agents/smart-contract-auditor.md +4 -4
  67. package/board/agents/streaming-reviewer.md +2 -2
  68. package/board/agents/subscription-billing-engineer.md +5 -5
  69. package/board/agents/tax-reviewer.md +3 -3
  70. package/board/agents/us-ai-reviewer.md +2 -2
  71. package/board/agents/us-privacy-reviewer.md +3 -3
  72. package/board/agents/voice-ai-reviewer.md +3 -3
  73. package/board/agents/web-store-reviewer.md +2 -2
  74. package/board/scripts/codex-pipeline.mjs +42 -7
  75. package/board/scripts/lib/agent-events.mjs +5 -1
  76. package/board/scripts/lib/cost-meter.mjs +34 -2
  77. package/board/scripts/lib/cross-model-review.mjs +107 -9
  78. package/board/scripts/lib/gate-tier.mjs +1 -1
  79. package/board/scripts/lib/host-graph-bindings.mjs +15 -3
  80. package/board/scripts/lib/host-graph-events.mjs +23 -4
  81. package/board/scripts/lib/host-graph-runtime.mjs +10 -3
  82. package/board/scripts/lib/kimi-review-profile.mjs +551 -0
  83. package/board/scripts/lib/kimi-review-store.mjs +469 -0
  84. package/board/scripts/lib/system-map.mjs +4 -2
  85. package/board/scripts/lib/work-publication.mjs +10 -9
  86. package/board/scripts/skill-lint.mjs +3 -3
  87. package/dist/main.js +82 -16
  88. package/dist/release-activation.mjs +3 -1
  89. package/dist/telemetry.js +2 -2
  90. package/package.json +1 -1
@@ -13,14 +13,14 @@
13
13
  // node scripts/lib/cross-model-review.mjs --diff /tmp/d.diff --spec docs/architecture/ARCH-x.md
14
14
  // GREAT_CTO_CROSS_REVIEW_MODEL=google/gemini-2.5-pro node ... --diff -
15
15
 
16
- import { readFileSync } from 'node:fs';
16
+ import { readFileSync, lstatSync, openSync, closeSync, fstatSync, readSync, constants } from 'node:fs';
17
17
  import { fileURLToPath } from 'node:url';
18
18
  import { execFileSync } from 'node:child_process';
19
19
  import { costForUsage, round4, resolvePrice } from './cost-meter.mjs';
20
20
  import { resolveSecondOpinion, codexReview } from './second-opinion.mjs';
21
21
  import { principalError } from './provider-exhaustion.mjs';
22
22
  import { existsSync, appendFileSync, mkdirSync } from 'node:fs';
23
- import { join } from 'node:path';
23
+ import { join, resolve, parse, sep } from 'node:path';
24
24
  import { reviewInputBinding, parseLegacyReview } from './review-contract.mjs';
25
25
 
26
26
  /**
@@ -34,9 +34,14 @@ import { reviewInputBinding, parseLegacyReview } from './review-contract.mjs';
34
34
  export const EXIT = Object.freeze({ PASS: 0, BLOCK: 1, USAGE: 2, SKIPPED: 3 });
35
35
 
36
36
  /**
37
- * Which provider reviews, decided from three sources in a fixed order:
38
- * an explicit `--provider`, then the project's `capabilities: second_opinion`,
39
- * then — for compatibility with every script that set it — the OpenRouter env.
37
+ * Which provider reviews, decided from two sources in a fixed order:
38
+ * an explicit `--provider`, then the project's `capabilities: second_opinion`.
39
+ *
40
+ * An exported OPENROUTER_API_KEY used to be a third source: with nothing
41
+ * declared, the key chose OpenRouter and the diff left the machine without
42
+ * anyone deciding it should (CSO SEC-2, 27.10). The key is a credential, not a
43
+ * decision — undeclared stays undeclared, and the skip line names the
44
+ * declaration that would turn the review on.
40
45
  *
41
46
  * Returns the resolver's four states plus `source`, so the log line can say
42
47
  * why this provider and not another. Pure: `codex` and `env` are injected.
@@ -48,9 +53,9 @@ export function decideProvider({ argv = [], projectMd = '', codex = null, env =
48
53
  return { ...r, source: '--provider' };
49
54
  }
50
55
  const fromProject = resolveSecondOpinion({ projectMd, codex, env });
51
- if (fromProject.state !== 'undeclared') return { ...fromProject, source: 'PROJECT.md' };
52
- if (env.OPENROUTER_API_KEY) {
53
- return { state: 'declared', provider: 'openrouter', why: '', source: 'OPENROUTER_API_KEY (second_opinion undeclared)' };
56
+ if (fromProject.state === 'undeclared' && env.OPENROUTER_API_KEY) {
57
+ return { ...fromProject, source: 'PROJECT.md',
58
+ why: `${fromProject.why}; OPENROUTER_API_KEY is set but does not choose the provider — declare \`second_opinion: openrouter\` or pass --provider openrouter` };
54
59
  }
55
60
  return { ...fromProject, source: 'PROJECT.md' };
56
61
  }
@@ -128,6 +133,93 @@ export async function callOpenRouter({ apiKey, model, system, user, maxTokens =
128
133
 
129
134
  function readArg(argv, name) { const i = argv.indexOf(name); return i > -1 ? argv[i + 1] : null; }
130
135
 
136
+ /** Read explicitly selected input without following symlinks or special files. */
137
+ function readProfileFile(file, root, cap) {
138
+ if (typeof file !== 'string' || !file || file === '-' || Buffer.byteLength(file) > 4096
139
+ || file.split(/[\\/]/).includes('..') || /[\x00-\x1f\x7f]/.test(file)) throw new Error('invalid-input-file');
140
+ const target = resolve(root, file), parsed = parse(target);
141
+ const parts = target.slice(parsed.root.length).split(sep);
142
+ let current = parsed.root;
143
+ for (let i = 0; i < parts.length; i++) {
144
+ if (parts[i] === '.git' || /^\.env(?:\.|$)/i.test(parts[i])) throw new Error('invalid-input-file');
145
+ current = join(current, parts[i]);
146
+ const st = lstatSync(current);
147
+ if (st.isSymbolicLink() || (i < parts.length - 1 ? !st.isDirectory() : !st.isFile())) throw new Error('invalid-input-file');
148
+ }
149
+ const fd = openSync(target, constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK);
150
+ try {
151
+ const st = fstatSync(fd);
152
+ if (!st.isFile() || st.size > cap) throw new Error('invalid-input-file');
153
+ const bytes = Buffer.alloc(cap + 1);
154
+ let size = 0, n;
155
+ while (size <= cap && (n = readSync(fd, bytes, size, cap + 1 - size, null)) > 0) size += n;
156
+ if (size > cap) throw new Error('invalid-input-file');
157
+ return new TextDecoder('utf-8', { fatal: true }).decode(bytes.subarray(0, size));
158
+ } finally { closeSync(fd); }
159
+ }
160
+
161
+ /** Check explicit runtime roots before any credential getter is evaluated. */
162
+ export function validateKimiRuntimePaths(root, operatorRoot) {
163
+ for (const [directory, allowMissing] of [[root, false], [operatorRoot, true]]) {
164
+ if (typeof directory !== 'string' || !directory || Buffer.byteLength(directory) > 4096
165
+ || /[\x00-\x1f\x7f]/.test(directory)) throw new Error('invalid-runtime-path');
166
+ const target = resolve(directory), parsed = parse(target);
167
+ let current = parsed.root, missing = false;
168
+ for (const part of target.slice(parsed.root.length).split(sep).filter(Boolean)) {
169
+ if (Buffer.byteLength(part) > 255 || part === '.git' || /^\.env(?:\.|$)/i.test(part)) throw new Error('invalid-runtime-path');
170
+ current = join(current, part);
171
+ if (missing) continue;
172
+ try {
173
+ const st = lstatSync(current);
174
+ if (st.isSymbolicLink() || !st.isDirectory()) throw new Error('invalid-runtime-path');
175
+ } catch (error) {
176
+ if (allowMissing && error.code === 'ENOENT') missing = true;
177
+ else throw error;
178
+ }
179
+ }
180
+ }
181
+ }
182
+
183
+ /** Explicit extra opinion. Runtime capabilities are resolved only after admission. */
184
+ export async function runProfileReview(options = {}) {
185
+ const argv = options.argv ?? [];
186
+ if (!argv.includes('--profile')) return null;
187
+ const denied = () => ({ exitCode: EXIT.USAGE, summary: { status: 'usage-error', verdict: null,
188
+ advisory: true, authority: 'NONE', releaseEligibility: 'NOT_CHECKED', reasonCode: 'invalid-profile-invocation' } });
189
+ const allowed = new Set(['--profile', '--envelope', '--task', '--context-class', '--reason', '--root', '--operator-root', '--diff', '--spec']);
190
+ const flags = new Map();
191
+ for (let i = 0; i < argv.length; i += 2) {
192
+ const flag = argv[i], value = argv[i + 1];
193
+ if (!allowed.has(flag) || flags.has(flag) || typeof value !== 'string' || !value || value.startsWith('--')) return denied();
194
+ flags.set(flag, value);
195
+ }
196
+ if (flags.get('--profile') !== 'kimi-readonly-v1'
197
+ || ['--envelope', '--task', '--context-class', '--reason', '--operator-root'].some(k => !flags.has(k))) return denied();
198
+ let root, operatorRoot, envelope, profileApi;
199
+ try {
200
+ root = resolve(flags.get('--root') || process.cwd());
201
+ operatorRoot = resolve(root, flags.get('--operator-root'));
202
+ validateKimiRuntimePaths(root, operatorRoot);
203
+ envelope = JSON.parse(readProfileFile(flags.get('--envelope'), root, 131072));
204
+ if (!envelope || envelope.mode !== 'diff-review' || envelope.taskId !== flags.get('--task')
205
+ || envelope.contextClass !== flags.get('--context-class') || envelope.reason !== flags.get('--reason')) return denied();
206
+ for (const [flag, field, cap] of [['--diff', 'diff', 96000], ['--spec', 'spec', 16000]]) {
207
+ if (flags.has(flag) && readProfileFile(flags.get(flag), root, cap) !== envelope[field]) return denied();
208
+ }
209
+ profileApi = await import('./kimi-review-profile.mjs');
210
+ profileApi.buildKimiInputBinding(envelope);
211
+ } catch { return denied(); }
212
+ const result = await profileApi.runKimiReview({ profile: flags.get('--profile'), envelope, root,
213
+ operatorRoot,
214
+ get apiKey() { return options.apiKey ?? (options.env ?? process.env).OPENROUTER_API_KEY; },
215
+ fetchImpl: options.fetchImpl, now: options.now, uuid: options.uuid, signal: options.signal, timeoutMs: options.timeoutMs });
216
+ const complete = result.status === 'complete' && ['PASS', 'BLOCK'].includes(result.verdict);
217
+ return { exitCode: complete ? result.verdict === 'BLOCK' ? EXIT.BLOCK : EXIT.PASS : EXIT.SKIPPED,
218
+ summary: { status: result.status, reasonCode: result.reasonCode, verdict: complete ? result.verdict : null,
219
+ advisory: true, authority: 'NONE', releaseEligibility: 'NOT_CHECKED', bindingDigest: result.bindingDigest,
220
+ findings: result.result?.findings?.length ?? null, costState: result.costState, costUsd: result.costUsd } };
221
+ }
222
+
131
223
  /**
132
224
  * The tree's identity at review time — git HEAD and whether it was dirty.
133
225
  * CLI-only: the pure `reviewLogLine` above never shells out; per this file's
@@ -147,6 +239,11 @@ function gitIdentity(cwd) {
147
239
  }
148
240
 
149
241
  async function main(argv) {
242
+ if (argv.includes('--profile')) {
243
+ const r = await runProfileReview({ argv, fetchImpl: fetch });
244
+ console.log(JSON.stringify(r.summary));
245
+ process.exit(r.exitCode);
246
+ }
150
247
  const diffPath = readArg(argv, '--diff');
151
248
  if (!diffPath) { console.error('Usage: cross-model-review.mjs --diff <file|-> [--spec <file>] [--model <slug>] [--provider codex|openrouter]'); process.exit(EXIT.USAGE); }
152
249
 
@@ -216,7 +313,8 @@ async function main(argv) {
216
313
  // was present and costForUsage prices an unknown model at nothing. A reviewer
217
314
  // that reads as free is the defect this repository has removed twice already.
218
315
  const priced = resolvePrice(res.model).price != null;
219
- const cost = res.usage && priced ? round4(costForUsage({ model: res.model, usage: res.usage })) : null;
316
+ const calculated = res.usage && priced ? costForUsage({ model: res.model, usage: res.usage }) : null;
317
+ const cost = calculated === null ? null : round4(calculated);
220
318
 
221
319
  for (const f of findings) console.log(` ${f.severity} ${f.file}:${f.line} — ${f.issue}`);
222
320
  if (verdict == null) {
@@ -18,7 +18,7 @@
18
18
  // Thirty evals clear the interval. Only some of those may drop a gate, and the
19
19
  // three cuts are mechanical rather than judgement:
20
20
  //
21
- // - `sharedExpanded` non-empty: the harness inlined `_shared` contracts into
21
+ // - `sharedExpanded` non-empty: the harness inlined shared contracts into
22
22
  // the actor's prompt, so the agent was handed something it may never fetch
23
23
  // on a live run. On 2026-08-07 architect did not fetch the one whose command
24
24
  // sat verbatim in its own file. An eval that inlines the handoff measures an
@@ -104,12 +104,24 @@ export function nativeGraphObservations(root, { store = nativeGraphStore(), jour
104
104
  return rows;
105
105
  }
106
106
 
107
- /** Bounded page of exactly one explicitly enrolled session. */
107
+ /**
108
+ * Bounded page of exactly one session of this project and host.
109
+ *
110
+ * An enrolled session is a live read (`enrolled: true, retained: false`). After unbind or
111
+ * disable the enrollment is gone but the private journal stays (`retainedHistory`), and it
112
+ * remains readable as `enrolled: false, retained: true`. That read is derived from the
113
+ * exact project, host and session, creates no enrollment and cannot resume recording; a
114
+ * session with neither an enrollment nor a journal is refused as before.
115
+ */
108
116
  export function readNativeGraphPage({ root, host, sessionId, cursor = null, limit = 200 }, { store = nativeGraphStore(), journalStore } = {}) {
109
117
  if (!hostSet.has(host) || !identity(sessionId)) throw Error('invalid native graph binding');
110
118
  const b = load(root, host, sessionId, store);
111
- if (!b) throw Error('native session is not enrolled in this project');
112
- return projectHostObservations(b, { store: journalStore, cursor, limit });
119
+ if (b) return { ...projectHostObservations(b, { store: journalStore, cursor, limit }), enrolled: true, retained: false };
120
+ // Same journal identity that bindNativeGraph gives this session: enrollment adds no secret.
121
+ const retained = { ...graphBinding({ root: realpathSync(root), runId: sessionId, sessionId }), host };
122
+ const graph = projectHostObservations(retained, { store: journalStore, cursor, limit });
123
+ if (graph.health === 'absent') throw Error('native session is not enrolled in this project');
124
+ return { ...graph, enrolled: false, retained: true };
113
125
  }
114
126
 
115
127
  /** Project opt-in before SessionStart permits observation from the real start event. */
@@ -1,5 +1,6 @@
1
1
  /** Host observations are metadata, never controller approval or execution authority. */
2
2
  import { createHash } from 'node:crypto';
3
+ import { readFileSync } from 'node:fs';
3
4
 
4
5
  const hash = value => createHash('sha256').update(JSON.stringify(value)).digest('hex');
5
6
  const safeId = value => typeof value === 'string' && /^[a-zA-Z0-9_.:-]{1,160}$/.test(value) ? value : null;
@@ -9,11 +10,29 @@ const hosts = new Set(['codex', 'claude-code']);
9
10
  const modes = new Set(['controlled', 'native']);
10
11
  const timestamp = value => typeof value === 'string' && value.length <= 40 && Number.isFinite(Date.parse(value)) ? new Date(value).toISOString() : null;
11
12
 
13
+ // Live acceptance, by exact host version (shared/host-graph-acceptance.json). Support is never
14
+ // inferred from semver syntax: only a version that passed a live run is `supported`, a dimension
15
+ // that was exercised and not delivered is `unsupported`, everything else stays `unknown`.
16
+ let acceptanceCache;
17
+ export function loadHostGraphAcceptance() {
18
+ if (acceptanceCache !== undefined) return acceptanceCache;
19
+ try {
20
+ const doc = JSON.parse(readFileSync(new URL('../../shared/host-graph-acceptance.json', import.meta.url), 'utf8'));
21
+ acceptanceCache = doc?.schema === 1 && Array.isArray(doc.accepted) ? doc.accepted : null;
22
+ } catch { acceptanceCache = null; } // unreadable: support stays unknown, and the audit says so
23
+ return acceptanceCache;
24
+ }
25
+ export function acceptedEntry({ host, mode, sourceVersion, acceptance = loadHostGraphAcceptance() } = {}) {
26
+ if (!Array.isArray(acceptance) || !version(sourceVersion)) return null;
27
+ return acceptance.find(e => e && e.host === host && e.mode === mode && e.version === sourceVersion) || null;
28
+ }
29
+
12
30
  /** Supported event families do not attest that a particular run was fully observed. */
13
- export function hostGraphCapabilities({ host, mode, sourceVersion } = {}) {
14
- const known = false; // Version compatibility is established by the collector's capability audit, never semver syntax alone.
15
- return { lifecycle: known ? 'supported' : 'unknown', tools: known ? 'supported' : 'unknown',
16
- parentChild: known ? 'supported' : 'unknown', verification: 'unsupported', gates: 'unsupported', artifacts: 'unsupported' };
31
+ export function hostGraphCapabilities({ host, mode, sourceVersion, acceptance } = {}) {
32
+ const entry = acceptedEntry({ host, mode, sourceVersion, ...(acceptance !== undefined ? { acceptance } : {}) });
33
+ const dim = key => entry?.dimensions?.[key] === true ? 'supported' : entry?.dimensions?.[key] === false ? 'unsupported' : 'unknown';
34
+ return { lifecycle: dim('lifecycle'), tools: dim('tools'), parentChild: dim('parentChild'),
35
+ verification: 'unsupported', gates: 'unsupported', artifacts: 'unsupported' };
17
36
  }
18
37
 
19
38
  function toolClass(name) {
@@ -1,5 +1,6 @@
1
1
  /** Provider-free CLI surface audit. Help text is not execution or hook-delivery proof. */
2
2
  import { spawnSync } from 'node:child_process';
3
+ import { loadHostGraphAcceptance } from './host-graph-events.mjs';
3
4
 
4
5
  const hosts = new Set(['codex', 'claude-code']);
5
6
  const commandOptions = Object.freeze({ encoding: 'utf8', timeout: 10000, maxBuffer: 1024 * 1024, windowsHide: true });
@@ -13,7 +14,7 @@ function command(run, bin, args) {
13
14
  catch { return { status: null, error: { code: 'AUDIT_COMMAND_FAILED' } }; }
14
15
  }
15
16
 
16
- export function auditHostGraphRuntime({ host, bin = host === 'claude-code' ? 'claude' : 'codex', run = spawnSync } = {}) {
17
+ export function auditHostGraphRuntime({ host, bin = host === 'claude-code' ? 'claude' : 'codex', run = spawnSync, acceptance = loadHostGraphAcceptance() } = {}) {
17
18
  if (!hosts.has(host)) throw Error('unsupported graph host');
18
19
  const result = { host, sourceVersion: null, state: 'unreadable', controlled: { streamEvents: null },
19
20
  native: { hookEvents: null }, liveAcceptance: 'not-checked' };
@@ -23,6 +24,12 @@ export function auditHostGraphRuntime({ host, bin = host === 'claude-code' ? 'cl
23
24
  return result;
24
25
  }
25
26
  result.sourceVersion = sourceVersion(version);
27
+ // Which modes of this exact version passed live acceptance; an unreadable record is not "none".
28
+ if (acceptance === null) result.liveAcceptance = 'unreadable';
29
+ else {
30
+ const modes = acceptance.filter(e => e?.host === host && e.version === result.sourceVersion).map(e => e.mode);
31
+ if (modes.length) result.liveAcceptance = { state: 'accepted', modes: [...new Set(modes)].sort() };
32
+ }
26
33
  const help = command(run, bin, host === 'codex' ? ['exec', '--help'] : ['--help']);
27
34
  if (help?.error || help?.status !== 0) return result;
28
35
  const text = String(help.stdout || '');
@@ -33,6 +40,6 @@ export function auditHostGraphRuntime({ host, bin = host === 'claude-code' ? 'cl
33
40
  return result;
34
41
  }
35
42
 
36
- export function auditHostGraphRuntimes({ run = spawnSync, binaries = {} } = {}) {
37
- return ['codex', 'claude-code'].map(host => auditHostGraphRuntime({ host, run, bin: binaries[host] || (host === 'codex' ? 'codex' : 'claude') }));
43
+ export function auditHostGraphRuntimes({ run = spawnSync, binaries = {}, acceptance = loadHostGraphAcceptance() } = {}) {
44
+ return ['codex', 'claude-code'].map(host => auditHostGraphRuntime({ host, run, acceptance, bin: binaries[host] || (host === 'codex' ? 'codex' : 'claude') }));
38
45
  }