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.
- package/board/.claude-plugin/plugin.json +1 -1
- package/board/agents/accounting-reviewer.md +2 -2
- package/board/agents/adtech-privacy-reviewer.md +3 -3
- package/board/agents/ai-eval-engineer.md +3 -3
- package/board/agents/ai-prompt-architect.md +3 -3
- package/board/agents/ai-security-reviewer.md +2 -2
- package/board/agents/api-platform-reviewer.md +2 -2
- package/board/agents/app-scaffolder.md +3 -3
- package/board/agents/architect.md +8 -8
- package/board/agents/auth-engineer.md +4 -4
- package/board/agents/ci-resolver.md +5 -5
- package/board/agents/cli-reviewer.md +2 -2
- package/board/agents/cmmc-reviewer.md +2 -2
- package/board/agents/cms-reviewer.md +2 -2
- package/board/agents/code-reviewer.md +4 -4
- package/board/agents/connector-builder.md +5 -5
- package/board/agents/continuous-learner.md +3 -3
- package/board/agents/coordinator.md +4 -4
- package/board/agents/data-platform-reviewer.md +2 -2
- package/board/agents/db-migration-reviewer.md +2 -2
- package/board/agents/decision-scorer.md +3 -3
- package/board/agents/design-advisor.md +3 -3
- package/board/agents/devops.md +12 -12
- package/board/agents/devtools-reviewer.md +2 -2
- package/board/agents/dpdpa-reviewer.md +2 -2
- package/board/agents/e2e-test-engineer.md +1 -1
- package/board/agents/edtech-reviewer.md +3 -3
- package/board/agents/enterprise-saas-reviewer.md +2 -2
- package/board/agents/firmware-reviewer.md +2 -2
- package/board/agents/game-reviewer.md +2 -2
- package/board/agents/gdpr-reviewer.md +3 -3
- package/board/agents/geo-routing-engineer.md +5 -5
- package/board/agents/gov-reviewer.md +2 -2
- package/board/agents/growth-engineer.md +3 -3
- package/board/agents/healthcare-reviewer.md +3 -3
- package/board/agents/hr-ai-reviewer.md +3 -3
- package/board/agents/infra-provisioner.md +3 -3
- package/board/agents/infra-reviewer.md +2 -2
- package/board/agents/insurance-reviewer.md +2 -2
- package/board/agents/integrations-engineer.md +5 -5
- package/board/agents/knowledge-extractor.md +2 -2
- package/board/agents/l3-support.md +8 -8
- package/board/agents/legal-reviewer.md +3 -3
- package/board/agents/library-reviewer.md +2 -2
- package/board/agents/marketplace-reviewer.md +2 -2
- package/board/agents/mcp-server-reviewer.md +2 -2
- package/board/agents/media-pipeline-engineer.md +5 -5
- package/board/agents/migration-import-engineer.md +5 -5
- package/board/agents/mlops-reviewer.md +2 -2
- package/board/agents/mobile-app-builder.md +6 -6
- package/board/agents/mobile-store-reviewer.md +2 -2
- package/board/agents/msp-reviewer.md +2 -2
- package/board/agents/oracle-reviewer.md +2 -2
- package/board/agents/pci-reviewer.md +2 -2
- package/board/agents/performance-engineer.md +3 -3
- package/board/agents/pm.md +6 -6
- package/board/agents/procurement-reviewer.md +2 -2
- package/board/agents/product-owner.md +5 -5
- package/board/agents/project-auditor.md +6 -6
- package/board/agents/qa-engineer.md +11 -11
- package/board/agents/quant-researcher.md +2 -2
- package/board/agents/rcm-reviewer.md +3 -3
- package/board/agents/regulated-reviewer.md +2 -2
- package/board/agents/security-officer.md +8 -8
- package/board/agents/senior-dev.md +7 -7
- package/board/agents/smart-contract-auditor.md +4 -4
- package/board/agents/streaming-reviewer.md +2 -2
- package/board/agents/subscription-billing-engineer.md +5 -5
- package/board/agents/tax-reviewer.md +3 -3
- package/board/agents/us-ai-reviewer.md +2 -2
- package/board/agents/us-privacy-reviewer.md +3 -3
- package/board/agents/voice-ai-reviewer.md +3 -3
- package/board/agents/web-store-reviewer.md +2 -2
- package/board/scripts/codex-pipeline.mjs +42 -7
- package/board/scripts/lib/agent-events.mjs +5 -1
- package/board/scripts/lib/cost-meter.mjs +34 -2
- package/board/scripts/lib/cross-model-review.mjs +107 -9
- package/board/scripts/lib/gate-tier.mjs +1 -1
- package/board/scripts/lib/host-graph-bindings.mjs +15 -3
- package/board/scripts/lib/host-graph-events.mjs +23 -4
- package/board/scripts/lib/host-graph-runtime.mjs +10 -3
- package/board/scripts/lib/kimi-review-profile.mjs +551 -0
- package/board/scripts/lib/kimi-review-store.mjs +469 -0
- package/board/scripts/lib/system-map.mjs +4 -2
- package/board/scripts/lib/work-publication.mjs +10 -9
- package/board/scripts/skill-lint.mjs +3 -3
- package/dist/main.js +82 -16
- package/dist/release-activation.mjs +3 -1
- package/dist/telemetry.js +2 -2
- 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
|
|
38
|
-
* an explicit `--provider`, then the project's `capabilities: second_opinion
|
|
39
|
-
*
|
|
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
|
|
52
|
-
|
|
53
|
-
|
|
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
|
|
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
|
|
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
|
-
/**
|
|
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 (
|
|
112
|
-
|
|
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
|
|
15
|
-
|
|
16
|
-
|
|
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
|
}
|