@cr1992/agentkit 1.0.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 (104) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/LICENSE +21 -0
  3. package/README.en.md +107 -0
  4. package/README.md +103 -0
  5. package/bin/agentkit.mjs +4 -0
  6. package/bin/cli.mjs +273 -0
  7. package/core/atomic-fs.mjs +23 -0
  8. package/core/cli-help.mjs +54 -0
  9. package/core/content-digest.mjs +66 -0
  10. package/core/digest.mjs +67 -0
  11. package/core/json-schema-lite.mjs +60 -0
  12. package/core/legacy-entry.mjs +37 -0
  13. package/core/reflection.mjs +142 -0
  14. package/core/runtime-bundle.mjs +101 -0
  15. package/docs/loop/embedded-review-adapter.md +41 -0
  16. package/docs/loop/loop-state-machine.md +43 -0
  17. package/docs/loop/recovery-and-fuses.md +34 -0
  18. package/docs/orchestrate/dispatch-contract.md +92 -0
  19. package/docs/orchestrate/failure-routing-and-recovery.md +46 -0
  20. package/docs/orchestrate/host-capability-cache.md +170 -0
  21. package/docs/orchestrate/isolation-fallback.md +18 -0
  22. package/docs/orchestrate/model-routing-config.md +186 -0
  23. package/docs/orchestrate/orchestration-runtime.md +261 -0
  24. package/docs/orchestrate/review-budget.md +90 -0
  25. package/docs/orchestrate/task-playbooks.md +85 -0
  26. package/docs/orchestrate/user-facing-reporting.md +14 -0
  27. package/docs/verify/evidence-schema.md +167 -0
  28. package/docs/verify/input-preparation.md +44 -0
  29. package/docs/verify/verification-protocol.md +76 -0
  30. package/docs/worktree/batch-integration.md +176 -0
  31. package/docs/worktree/delivery-identity.md +41 -0
  32. package/docs/worktree/profile.md +107 -0
  33. package/docs/worktree/reclaim-and-watch.md +96 -0
  34. package/docs/worktree/review-lifecycle.md +92 -0
  35. package/docs/worktree/spawn-and-stack.md +74 -0
  36. package/domains/loop/loop-runtime.mjs +1056 -0
  37. package/domains/orchestrate/contract-tool.mjs +169 -0
  38. package/domains/orchestrate/host_capability_cache.mjs +437 -0
  39. package/domains/orchestrate/orchestration-ledger.mjs +332 -0
  40. package/domains/orchestrate/orchestration-metadata.mjs +4 -0
  41. package/domains/orchestrate/orchestration-reflection.mjs +119 -0
  42. package/domains/orchestrate/resolve_model_policy.mjs +311 -0
  43. package/domains/orchestrate/review-budget.mjs +162 -0
  44. package/domains/orchestrate/worker-capability-preflight.mjs +227 -0
  45. package/domains/verify/verification-runtime.mjs +1638 -0
  46. package/domains/worktree/worktree-archive.mjs +135 -0
  47. package/domains/worktree/worktree-artifact.mjs +123 -0
  48. package/domains/worktree/worktree-batch-integrate.mjs +713 -0
  49. package/domains/worktree/worktree-batch-plan.mjs +198 -0
  50. package/domains/worktree/worktree-batch-result.mjs +241 -0
  51. package/domains/worktree/worktree-core.mjs +908 -0
  52. package/domains/worktree/worktree-doctor.mjs +493 -0
  53. package/domains/worktree/worktree-history.mjs +377 -0
  54. package/domains/worktree/worktree-learning.mjs +110 -0
  55. package/domains/worktree/worktree-lifecycle.mjs +786 -0
  56. package/domains/worktree/worktree-merge-preview.mjs +409 -0
  57. package/domains/worktree/worktree-mgr.mjs +261 -0
  58. package/domains/worktree/worktree-process.mjs +55 -0
  59. package/domains/worktree/worktree-profile.mjs +800 -0
  60. package/domains/worktree/worktree-provider-gitlab.mjs +59 -0
  61. package/domains/worktree/worktree-reclaim.mjs +683 -0
  62. package/domains/worktree/worktree-review-refresh.mjs +574 -0
  63. package/domains/worktree/worktree-review-watch.mjs +661 -0
  64. package/domains/worktree/worktree-scan.mjs +510 -0
  65. package/domains/worktree/worktree-trace-test-worker.mjs +23 -0
  66. package/domains/worktree/worktree-trace.mjs +478 -0
  67. package/manage-worktrees/SKILL.md +87 -0
  68. package/manage-worktrees/agents/openai.yaml +4 -0
  69. package/manage-worktrees/scripts/worktree-mgr.mjs +10 -0
  70. package/manage-worktrees/scripts/worktree-scan.mjs +10 -0
  71. package/orchestrate-subagents/SKILL.md +173 -0
  72. package/orchestrate-subagents/agents/openai.yaml +4 -0
  73. package/orchestrate-subagents/scripts/contract-tool.mjs +10 -0
  74. package/orchestrate-subagents/scripts/host_capability_cache.mjs +10 -0
  75. package/orchestrate-subagents/scripts/orchestration-ledger.mjs +10 -0
  76. package/orchestrate-subagents/scripts/orchestration-reflection.mjs +10 -0
  77. package/orchestrate-subagents/scripts/resolve_model_policy.mjs +10 -0
  78. package/orchestrate-subagents/scripts/review-budget.mjs +10 -0
  79. package/orchestrate-subagents/scripts/worker-capability-preflight.mjs +10 -0
  80. package/package.json +48 -0
  81. package/run-agent-verify-loop/SKILL.md +127 -0
  82. package/run-agent-verify-loop/agents/openai.yaml +4 -0
  83. package/run-agent-verify-loop/scripts/loop-runtime.mjs +10 -0
  84. package/schemas/artifact-ref-v1.schema.json +23 -0
  85. package/schemas/batch-result-v1.schema.json +138 -0
  86. package/schemas/controller-recheck-record-v1.schema.json +22 -0
  87. package/schemas/convergence-report-v1.schema.json +9 -0
  88. package/schemas/effective-worker-capability-v1.schema.json +36 -0
  89. package/schemas/embedded-verification-record-v1.schema.json +32 -0
  90. package/schemas/evidence-package-v1.schema.json +41 -0
  91. package/schemas/improvement-proposal-v1.schema.json +18 -0
  92. package/schemas/loop-state-v1.schema.json +34 -0
  93. package/schemas/model-policy-resolution-v1.schema.json +41 -0
  94. package/schemas/orchestration-ledger-v1.schema.json +110 -0
  95. package/schemas/reflection-record-v1.schema.json +24 -0
  96. package/schemas/review-result-v1.schema.json +37 -0
  97. package/schemas/task-contract-v1.schema.json +83 -0
  98. package/schemas/verification-profile-v1.schema.json +60 -0
  99. package/schemas/worker-capability-requirements-v1.schema.json +21 -0
  100. package/schemas/worktree-binding-v1.schema.json +14 -0
  101. package/shell-manifest.json +79 -0
  102. package/verify-agent-output/SKILL.md +119 -0
  103. package/verify-agent-output/agents/openai.yaml +4 -0
  104. package/verify-agent-output/scripts/verification-runtime.mjs +10 -0
@@ -0,0 +1,1638 @@
1
+ #!/usr/bin/env node
2
+ // @ts-check
3
+
4
+ import { execFileSync, spawnSync } from 'node:child_process';
5
+ import { createHash, randomUUID } from 'node:crypto';
6
+ import {
7
+ accessSync,
8
+ appendFileSync,
9
+ closeSync,
10
+ constants as fsConstants,
11
+ existsSync,
12
+ fsyncSync,
13
+ linkSync,
14
+ lstatSync,
15
+ mkdtempSync,
16
+ mkdirSync,
17
+ openSync,
18
+ readFileSync,
19
+ readdirSync,
20
+ realpathSync,
21
+ renameSync,
22
+ rmSync,
23
+ statSync,
24
+ unlinkSync,
25
+ writeFileSync,
26
+ } from 'node:fs';
27
+ import { tmpdir } from 'node:os';
28
+ import { dirname, isAbsolute, join, relative, resolve, sep } from 'node:path';
29
+ import { fileURLToPath, pathToFileURL } from 'node:url';
30
+ import { createReflectionKit } from '../../core/reflection.mjs';
31
+ import { collectJsonSchemaErrors, validateJsonSchema } from '../../core/json-schema-lite.mjs';
32
+ import { atomicWriteJson, atomicWriteText, writeNewJson } from '../../core/atomic-fs.mjs';
33
+ import { createDigestKit } from '../../core/digest.mjs';
34
+ import { distributionDigest, skillDistributionRoots } from '../../core/content-digest.mjs';
35
+
36
+ export const RUNTIME_VERSION = '1.3.0';
37
+ export const PROTOCOL_VERSION = 1;
38
+ const DIGEST_PATTERN = /^sha256:[0-9a-f]{64}$/;
39
+ const FINDING_CLASSES = new Set(['functional', 'scope', 'verification_definition', 'safety']);
40
+ const REVIEW_FINDING_FIELDS = ['contract_item_id', 'class', 'evidence', 'expected', 'actual'];
41
+ const REVIEW_VERDICTS = new Set(['fail', 'no_defect_found', 'undecidable']);
42
+ const STAGES = new Set(['smoke', 'final', 'both']);
43
+ const TERMINAL_OUTCOMES = new Set(['pass', 'fail', 'undecidable', 'blocked_safety']);
44
+ const REVIEW_STDIN_MAX_BYTES = 1024 * 1024;
45
+ const SKILL_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..', '..', 'verify-agent-output');
46
+ // 摘要覆盖 Skill 目录 + 共享 core + canonical schemas:执行真正依赖的全部分发内容。
47
+ // PACKAGE_ROOT 是模块常量,传入自定义 root 只替换 Skill 目录那一段,便于测试摘要与安装路径无关。
48
+ const PACKAGE_ROOT = resolve(SKILL_ROOT, '..');
49
+ const DOMAIN_ROOT = dirname(fileURLToPath(import.meta.url));
50
+ export function skillContentDigest(root = SKILL_ROOT) {
51
+ return distributionDigest(skillDistributionRoots({ packageRoot: PACKAGE_ROOT, skillRoot: root, domainRoot: DOMAIN_ROOT, docsRoot: join(PACKAGE_ROOT, 'docs', 'verify') }));
52
+ }
53
+
54
+ const schema = (name) => parseJsonStrict(readFileSync(join(SKILL_ROOT, '..', 'schemas', name), 'utf8'));
55
+
56
+ export class ValidationError extends Error {}
57
+
58
+ // strict=true 保留本 Skill 既有的代理对与非有限 number 校验;错误类仍是本地的 ValidationError。
59
+ const digestKit = createDigestKit({ ValidationError, strict: true });
60
+ // 严格度保持本 Skill 现状(宽松版),不随 core 合并而收紧。
61
+ const { buildProposal, buildReflection, readAndValidateReflection, verifyEvidenceRefs } = createReflectionKit({ strict: false });
62
+ export const { canonicalJson, envelopeDigest } = digestKit;
63
+ const { sha256, assertValidUnicode } = digestKit;
64
+ export class OperationalAbort extends Error {
65
+ /** @param {string} code @param {string} message */
66
+ constructor(code, message) {
67
+ super(message);
68
+ this.code = code;
69
+ }
70
+ }
71
+
72
+ /** Strict JSON parser that rejects duplicate keys before materializing an object. */
73
+ class StrictJsonParser {
74
+ /** @param {string} text */
75
+ constructor(text) {
76
+ this.text = text;
77
+ this.index = 0;
78
+ }
79
+
80
+ parse() {
81
+ const value = this.value();
82
+ this.space();
83
+ if (this.index !== this.text.length) throw new ValidationError(`JSON 尾部存在非法内容,位置 ${this.index}`);
84
+ return value;
85
+ }
86
+
87
+ space() {
88
+ while (/\s/u.test(this.text[this.index] ?? '')) this.index += 1;
89
+ }
90
+
91
+ value() {
92
+ this.space();
93
+ const char = this.text[this.index];
94
+ if (char === '{') return this.object();
95
+ if (char === '[') return this.array();
96
+ if (char === '"') return this.string();
97
+ if (char === '-' || /[0-9]/u.test(char ?? '')) return this.number();
98
+ for (const [token, value] of [['true', true], ['false', false], ['null', null]]) {
99
+ if (this.text.startsWith(token, this.index)) {
100
+ this.index += token.length;
101
+ return value;
102
+ }
103
+ }
104
+ throw new ValidationError(`JSON 值非法,位置 ${this.index}`);
105
+ }
106
+
107
+ object() {
108
+ this.index += 1;
109
+ this.space();
110
+ const output = Object.create(null);
111
+ const keys = new Set();
112
+ if (this.text[this.index] === '}') {
113
+ this.index += 1;
114
+ return output;
115
+ }
116
+ while (true) {
117
+ this.space();
118
+ if (this.text[this.index] !== '"') throw new ValidationError(`JSON object key 非字符串,位置 ${this.index}`);
119
+ const key = this.string();
120
+ if (keys.has(key)) throw new ValidationError(`JSON object 存在重复 key: ${key}`);
121
+ keys.add(key);
122
+ this.space();
123
+ if (this.text[this.index] !== ':') throw new ValidationError(`JSON object 缺少冒号,位置 ${this.index}`);
124
+ this.index += 1;
125
+ output[key] = this.value();
126
+ this.space();
127
+ if (this.text[this.index] === '}') {
128
+ this.index += 1;
129
+ return output;
130
+ }
131
+ if (this.text[this.index] !== ',') throw new ValidationError(`JSON object 缺少逗号,位置 ${this.index}`);
132
+ this.index += 1;
133
+ }
134
+ }
135
+
136
+ array() {
137
+ this.index += 1;
138
+ this.space();
139
+ const output = [];
140
+ if (this.text[this.index] === ']') {
141
+ this.index += 1;
142
+ return output;
143
+ }
144
+ while (true) {
145
+ output.push(this.value());
146
+ this.space();
147
+ if (this.text[this.index] === ']') {
148
+ this.index += 1;
149
+ return output;
150
+ }
151
+ if (this.text[this.index] !== ',') throw new ValidationError(`JSON array 缺少逗号,位置 ${this.index}`);
152
+ this.index += 1;
153
+ }
154
+ }
155
+
156
+ string() {
157
+ const start = this.index;
158
+ this.index += 1;
159
+ let escaped = false;
160
+ while (this.index < this.text.length) {
161
+ const char = this.text[this.index];
162
+ if (!escaped && char === '"') {
163
+ this.index += 1;
164
+ const value = JSON.parse(this.text.slice(start, this.index));
165
+ assertValidUnicode(value);
166
+ return value;
167
+ }
168
+ if (!escaped && char === '\\') escaped = true;
169
+ else escaped = false;
170
+ this.index += 1;
171
+ }
172
+ throw new ValidationError(`JSON string 未闭合,位置 ${start}`);
173
+ }
174
+
175
+ number() {
176
+ const match = this.text.slice(this.index).match(/^-?(?:0|[1-9]\d*)(?:\.\d+)?(?:[eE][+-]?\d+)?/u);
177
+ if (!match) throw new ValidationError(`JSON number 非法,位置 ${this.index}`);
178
+ this.index += match[0].length;
179
+ const value = Number(match[0]);
180
+ if (!Number.isFinite(value)) throw new ValidationError('JSON number 必须是有限数值');
181
+ return value;
182
+ }
183
+ }
184
+
185
+ /** @param {string} value */
186
+ /** @param {string} text */
187
+ export function parseJsonStrict(text) {
188
+ return new StrictJsonParser(text).parse();
189
+ }
190
+
191
+ /** @param {unknown} value */
192
+ /** @param {string|Buffer} value */
193
+ /** @param {Record<string, unknown>} object @param {string} digestField */
194
+ /** @param {string} path */
195
+ function readJson(path) {
196
+ return /** @type {Record<string, any>} */ (parseJsonStrict(readFileSync(path, 'utf8')));
197
+ }
198
+
199
+ /** @param {string} path @param {unknown} value */
200
+ /** @param {string} path @param {string} value */
201
+ /** @param {string} path @param {unknown} value */
202
+ function processIsAlive(pid) {
203
+ if (!Number.isInteger(pid) || pid <= 0) return false;
204
+ try { process.kill(pid, 0); return true; }
205
+ catch (error) { return Boolean(error && typeof error === 'object' && error.code === 'EPERM'); }
206
+ }
207
+
208
+ function recoverOrphanReclaim(path) {
209
+ const reclaimPath = `${path}.reclaim`;
210
+ if (!existsSync(reclaimPath)) return;
211
+ let owner;
212
+ try { owner = parseJsonStrict(readFileSync(reclaimPath, 'utf8')); } catch { throw new ValidationError('run lock reclaim 内容损坏;拒绝自动接管'); }
213
+ if (!Number.isInteger(Number(owner.pid)) || Number(owner.pid) <= 0 || typeof owner.token !== 'string' || !owner.token) throw new ValidationError('run lock reclaim owner 无效;拒绝自动接管');
214
+ if (processIsAlive(Number(owner.pid))) throw new ValidationError('run lock 正在执行 stale recovery');
215
+ let latest;
216
+ try { latest = parseJsonStrict(readFileSync(reclaimPath, 'utf8')); } catch (error) { if (error && typeof error === 'object' && error.code === 'ENOENT') return; throw new ValidationError('run lock reclaim 内容损坏;拒绝自动接管'); }
217
+ if (Number(latest.pid) !== Number(owner.pid) || latest.token !== owner.token) return;
218
+ try { unlinkSync(reclaimPath); } catch (error) { if (!error || typeof error !== 'object' || error.code !== 'ENOENT') throw error; }
219
+ }
220
+
221
+ export function acquireLock(path) {
222
+ for (let attempt = 0; attempt < 4; attempt += 1) {
223
+ recoverOrphanReclaim(path);
224
+ const owner = { pid: process.pid, token: randomUUID(), acquired_at: new Date().toISOString() };
225
+ const candidate = `${path}.${owner.pid}.${owner.token}.candidate`;
226
+ const fd = openSync(candidate, 'wx', 0o600);
227
+ try { writeFileSync(fd, `${JSON.stringify(owner)}\n`); fsyncSync(fd); } finally { closeSync(fd); }
228
+ try {
229
+ if (existsSync(`${path}.reclaim`)) throw new ValidationError('run lock 正在执行 stale recovery');
230
+ linkSync(candidate, path); unlinkSync(candidate); return owner;
231
+ }
232
+ catch (error) {
233
+ try { unlinkSync(candidate); } catch {}
234
+ if (error instanceof ValidationError) throw error;
235
+ if (!error || typeof error !== 'object' || error.code !== 'EEXIST') throw error;
236
+ let current;
237
+ try { current = parseJsonStrict(readFileSync(path, 'utf8')); } catch { throw new ValidationError('run lock 内容损坏;拒绝自动接管'); }
238
+ if (!Number.isInteger(Number(current.pid)) || Number(current.pid) <= 0) throw new ValidationError('run lock owner 无效;拒绝自动接管');
239
+ if (processIsAlive(Number(current.pid))) throw new ValidationError(`run lock 正被 PID ${current.pid} 持有`);
240
+ const reclaimPath = `${path}.reclaim`;
241
+ const reclaimOwner = { pid: process.pid, token: randomUUID(), acquired_at: new Date().toISOString() };
242
+ const reclaimCandidate = `${reclaimPath}.${reclaimOwner.pid}.${reclaimOwner.token}.candidate`;
243
+ const reclaimFd = openSync(reclaimCandidate, 'wx', 0o600);
244
+ try { writeFileSync(reclaimFd, `${JSON.stringify(reclaimOwner)}\n`); fsyncSync(reclaimFd); } finally { closeSync(reclaimFd); }
245
+ try { linkSync(reclaimCandidate, reclaimPath); } catch (reclaimError) { if (!reclaimError || typeof reclaimError !== 'object' || reclaimError.code !== 'EEXIST') throw reclaimError; throw new ValidationError('run lock 正在执行 stale recovery'); }
246
+ finally { try { unlinkSync(reclaimCandidate); } catch {} }
247
+ try {
248
+ let latest;
249
+ try { latest = parseJsonStrict(readFileSync(path, 'utf8')); } catch (latestError) { if (latestError && typeof latestError === 'object' && latestError.code === 'ENOENT') continue; throw new ValidationError('run lock 内容损坏;拒绝自动接管'); }
250
+ if (Number(latest.pid) !== Number(current.pid) || latest.token !== current.token) continue;
251
+ if (processIsAlive(Number(latest.pid))) throw new ValidationError(`run lock 正被 PID ${latest.pid} 持有`);
252
+ unlinkSync(path);
253
+ } finally { releaseLock(reclaimPath, reclaimOwner); }
254
+ }
255
+ }
256
+ throw new ValidationError('无法获取 run lock');
257
+ }
258
+
259
+ export function releaseLock(path, owner) {
260
+ let current;
261
+ try { current = parseJsonStrict(readFileSync(path, 'utf8')); } catch (error) { if (error && typeof error === 'object' && error.code === 'ENOENT') return false; throw new ValidationError('run lock 在持有期间损坏;拒绝删除未知 owner 的 lock'); }
262
+ if (current.pid !== owner.pid || current.token !== owner.token) return false;
263
+ unlinkSync(path);
264
+ return true;
265
+ }
266
+
267
+ /** @param {string} root */
268
+ /** @param {string} [root] */
269
+ /** @param {string[]} args @param {string} cwd @param {BufferEncoding} [encoding] */
270
+ function git(args, cwd, encoding = 'utf8') {
271
+ return execFileSync('git', args, { cwd, encoding, stdio: ['ignore', 'pipe', 'pipe'], maxBuffer: 64 * 1024 * 1024 });
272
+ }
273
+
274
+ /** @param {string[]} args @param {string} cwd */
275
+ function gitStatus(args, cwd) {
276
+ return spawnSync('git', args, { cwd, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] });
277
+ }
278
+
279
+ /** @param {Record<string, any>} artifact @param {string} workdir @param {string|null} frozenIdentity */
280
+ export function verifyGitArtifact(artifact, workdir, frozenIdentity = null) {
281
+ const root = realpathSync(String(git(['rev-parse', '--show-toplevel'], workdir)).trim());
282
+ if (root !== realpathSync(workdir)) throw new OperationalAbort('stale_precondition', 'workdir 必须是 Git worktree 根目录');
283
+ const objectFormat = String(git(['rev-parse', '--show-object-format'], root)).trim();
284
+ if (artifact.object_format !== objectFormat) throw new OperationalAbort('stale_precondition', 'Artifact object_format 与仓库不一致');
285
+ const shaLength = objectFormat === 'sha256' ? 64 : objectFormat === 'sha1' ? 40 : 0;
286
+ if (!shaLength) throw new OperationalAbort('stale_precondition', `不支持 Git object format: ${objectFormat}`);
287
+ for (const field of ['base_sha', 'artifact_sha']) {
288
+ const value = String(artifact[field] ?? '');
289
+ if (!new RegExp(`^[0-9a-f]{${shaLength}}$`, 'u').test(value)) throw new ValidationError(`${field} 不是完整 ${objectFormat} object id`);
290
+ if (String(git(['cat-file', '-t', value], root)).trim() !== 'commit') throw new ValidationError(`${field} 不是 commit object`);
291
+ }
292
+ if (gitStatus(['merge-base', '--is-ancestor', artifact.base_sha, artifact.artifact_sha], root).status !== 0) {
293
+ throw new ValidationError('base_sha 不是 artifact_sha 的 ancestor');
294
+ }
295
+ const head = String(git(['rev-parse', 'HEAD'], root)).trim();
296
+ if (head !== artifact.artifact_sha) throw new OperationalAbort('stale_precondition', 'HEAD 已偏离冻结 artifact_sha');
297
+ const dirty = /** @type {Buffer} */ (git(['status', '--porcelain=v1', '-z', '--untracked-files=all'], root, 'buffer'));
298
+ if (dirty.length > 0) throw new OperationalAbort('stale_precondition', '验证 workdir 不是 clean 状态');
299
+ const roots = String(git(['rev-list', '--max-parents=0', artifact.artifact_sha], root)).trim().split('\n').filter(Boolean).sort();
300
+ const runtimeIdentity = `git:${objectFormat}:${sha256(Buffer.from(canonicalJson(roots), 'utf8'))}`;
301
+ if (frozenIdentity && runtimeIdentity !== frozenIdentity) throw new OperationalAbort('stale_precondition', 'repository identity 已变化');
302
+ return { workdir: root, runtime_repository_identity: runtimeIdentity };
303
+ }
304
+
305
+ /** @param {string} path */
306
+ function validateProfilePath(path) {
307
+ if (!path || isAbsolute(path) || path.includes('\\') || path.includes('..') || path.includes(':(') || /[*?[\]]/u.test(path)) {
308
+ throw new ValidationError(`非法 verifier path: ${path}`);
309
+ }
310
+ const core = path.endsWith('/') ? path.slice(0, -1) : path;
311
+ if (!core || core.split('/').some((part) => !part || part === '.' || part === '..')) throw new ValidationError(`非法 verifier path: ${path}`);
312
+ }
313
+
314
+ /** @param {string} path @param {string} rule */
315
+ function pathMatches(path, rule) {
316
+ return rule.endsWith('/') ? path.startsWith(rule) : path === rule;
317
+ }
318
+
319
+ /** @param {Record<string, any>} artifact @param {Record<string, any>} profile @param {string} workdir */
320
+ function verifyProtectedPaths(artifact, profile, workdir) {
321
+ const protectedPaths = profile.protected_verifier_paths;
322
+ const allowed = profile.allowed_validation_changes;
323
+ for (const path of [...protectedPaths, ...allowed]) validateProfilePath(path);
324
+ const bytes = /** @type {Buffer} */ (git(['diff', '--name-status', '-z', '--no-renames', artifact.base_sha, artifact.artifact_sha], workdir, 'buffer'));
325
+ const tokens = bytes.toString('utf8').split('\0').filter(Boolean);
326
+ const paths = [];
327
+ for (let index = 0; index < tokens.length; index += 2) {
328
+ if (tokens[index + 1]) paths.push(tokens[index + 1]);
329
+ }
330
+ const violations = paths.filter((path) => protectedPaths.some((rule) => pathMatches(path, rule)) && !allowed.some((rule) => pathMatches(path, rule)));
331
+ if (violations.length > 0) throw new OperationalAbort('protected_path_violation', `未授权修改 verifier path: ${violations.join(', ')}`);
332
+ return { changed_paths: paths, protected_path_violations: [] };
333
+ }
334
+
335
+ /** @param {string} label @param {string[]} issues */
336
+ function throwIssues(label, issues) {
337
+ if (issues.length > 0) throw new ValidationError(`${label} 校验失败(${issues.length} 项):\n- ${issues.join('\n- ')}`);
338
+ }
339
+
340
+ /** @param {Record<string, any>} object @param {string} field @param {string} label */
341
+ function digestIssues(object, field, label) {
342
+ if (!DIGEST_PATTERN.test(object?.[field] ?? '')) return [`${label}.${field} 格式不匹配`];
343
+ try { return envelopeDigest(object, field) === object[field] ? [] : [`${label}.${field} 与内容不匹配`]; }
344
+ catch (error) { return [`${label}.${field} 无法计算: ${error instanceof Error ? error.message : String(error)}`]; }
345
+ }
346
+
347
+ /** @param {Record<string, any>} contract */
348
+ function collectContractIssues(contract) {
349
+ const issues = collectJsonSchemaErrors(contract, schema('task-contract-v1.schema.json'), 'Task Contract');
350
+ if (!contract || typeof contract !== 'object' || Array.isArray(contract)) return issues;
351
+ if (!contract.scope || !Array.isArray(contract.scope.include) || !Array.isArray(contract.scope.exclude) || [...(contract.scope?.include ?? []), ...(contract.scope?.exclude ?? [])].some((item) => typeof item !== 'string' || !item)) issues.push('Task Contract.scope 必须包含非空字符串数组 include/exclude');
352
+ if (!contract.permissions || !['read_only', 'write'].includes(contract.permissions.mode) || !Array.isArray(contract.permissions.writable_paths) || contract.permissions.writable_paths.some((item) => typeof item !== 'string' || !item)) issues.push('Task Contract.permissions 无效');
353
+ if (contract.permissions?.mode === 'read_only' && contract.permissions.writable_paths?.length) issues.push('read_only 合同不能声明 writable_paths');
354
+ if (!contract.environment || typeof contract.environment.repository !== 'string' || !contract.environment.repository || !['shared_tree', 'worktree', 'caller_supplied'].includes(contract.environment.isolation)) issues.push('Task Contract.environment 无效');
355
+ if (!Array.isArray(contract.stop_conditions) || !contract.extensions || typeof contract.extensions !== 'object' || Array.isArray(contract.extensions)) issues.push('Task Contract.stop_conditions/extensions 无效');
356
+ const ids = new Set();
357
+ if (Array.isArray(contract.acceptance)) for (const item of contract.acceptance) {
358
+ if (item?.contract_item_id && ids.has(item.contract_item_id)) issues.push(`重复 contract_item_id: ${item.contract_item_id}`);
359
+ if (item?.contract_item_id) ids.add(item.contract_item_id);
360
+ }
361
+ issues.push(...digestIssues(contract, 'contract_digest', 'Task Contract'));
362
+ return [...new Set(issues)];
363
+ }
364
+
365
+ /** @param {Record<string, any>} contract */
366
+ function validateContract(contract) {
367
+ const issues = collectContractIssues(contract);
368
+ throwIssues('Task Contract', issues);
369
+ return new Set(contract.acceptance.map((item) => item.contract_item_id));
370
+ }
371
+
372
+ /** @param {Record<string, any>} profile @param {Set<string>} acceptanceIds */
373
+ function collectProfileIssues(profile, acceptanceIds) {
374
+ const issues = collectJsonSchemaErrors(profile, schema('verification-profile-v1.schema.json'), 'Verification Profile');
375
+ if (!profile || typeof profile !== 'object' || Array.isArray(profile)) return issues;
376
+ const checkIds = new Set();
377
+ if (Array.isArray(profile.l0_checks)) for (const check of profile.l0_checks) {
378
+ if (check?.check_id && checkIds.has(check.check_id)) issues.push(`重复 L0 check_id: ${check.check_id}`);
379
+ if (check?.check_id) checkIds.add(check.check_id);
380
+ if (check?.cwd_rel && (isAbsolute(check.cwd_rel) || check.cwd_rel.includes('\\') || check.cwd_rel.split('/').some((part) => part === '..'))) issues.push(`${check.check_id ?? 'L0 check'} cwd_rel 非法`);
381
+ }
382
+ if (Array.isArray(profile.l0_checks) && profile.l0_checks.length > 0 && (!profile.l0_checks.some((check) => ['smoke', 'both'].includes(check?.stage)) || !profile.l0_checks.some((check) => ['final', 'both'].includes(check?.stage)))) issues.push('Verification Profile 必须覆盖 smoke 与 final L0');
383
+ if (Array.isArray(profile.l1_review)) for (const review of profile.l1_review) {
384
+ if (review?.contract_item_id && !acceptanceIds.has(review.contract_item_id)) issues.push(`L1 引用未知 contract_item_id: ${review.contract_item_id}`);
385
+ }
386
+ for (const path of [...(Array.isArray(profile.protected_verifier_paths) ? profile.protected_verifier_paths : []), ...(Array.isArray(profile.allowed_validation_changes) ? profile.allowed_validation_changes : [])]) {
387
+ try { validateProfilePath(path); } catch (error) { issues.push(error instanceof Error ? error.message : String(error)); }
388
+ }
389
+ if (Array.isArray(profile.runtime?.env_allowlist) && profile.runtime.env_allowlist.some((name) => typeof name !== 'string' || !name)) issues.push('runtime.env_allowlist 只能包含非空变量名');
390
+ if (Array.isArray(profile.l0_checks) && profile.runtime?.executable_paths && typeof profile.runtime.executable_paths === 'object') for (const check of profile.l0_checks) {
391
+ const executable = profile.runtime.executable_paths[check?.argv?.[0]];
392
+ if (!executable || !isAbsolute(executable) || !existsSync(executable) || !statSync(executable).isFile()) issues.push(`未冻结绝对 executable: ${check?.argv?.[0] ?? '<missing>'}`);
393
+ }
394
+ issues.push(...digestIssues(profile, 'verification_profile_digest', 'Verification Profile'));
395
+ return [...new Set(issues)];
396
+ }
397
+
398
+ /** @param {Record<string, any>} profile @param {Set<string>} acceptanceIds */
399
+ function validateProfile(profile, acceptanceIds) {
400
+ throwIssues('Verification Profile', collectProfileIssues(profile, acceptanceIds));
401
+ }
402
+
403
+ /** @param {Record<string, any>} artifact */
404
+ function collectArtifactIssues(artifact) {
405
+ const issues = collectJsonSchemaErrors(artifact, schema('artifact-ref-v1.schema.json'), 'Artifact Ref');
406
+ if (artifact?.provider === 'manage-worktrees') {
407
+ if (!artifact.worktree_id) issues.push('manage-worktrees Artifact Ref 缺少 worktree_id');
408
+ if (!Number.isInteger(artifact.ownership_epoch) || artifact.ownership_epoch < 1) issues.push('manage-worktrees Artifact Ref ownership_epoch 无效');
409
+ }
410
+ return [...new Set(issues)];
411
+ }
412
+
413
+ /** @param {Record<string, any>} artifact */
414
+ function validateArtifact(artifact) {
415
+ throwIssues('Artifact Ref', collectArtifactIssues(artifact));
416
+ }
417
+
418
+ /** @param {Record<string, any>} contract @param {string} contentDigest */
419
+ function validateSkillBinding(contract, contentDigest) {
420
+ const entry = contract.skill_set.find((item) => item?.name === 'verify-agent-output');
421
+ if (!entry) throw new ValidationError('Task Contract skill_set 未冻结 verify-agent-output');
422
+ if (entry.content_digest !== contentDigest) throw new ValidationError('Task Contract 中 verify-agent-output content_digest 与当前安装不一致');
423
+ }
424
+
425
+ /** @param {string} candidate @param {string} parent */
426
+ function canonicalFuturePath(path) {
427
+ let cursor = resolve(path);
428
+ const suffix = [];
429
+ while (!existsSync(cursor)) {
430
+ const parent = dirname(cursor);
431
+ if (parent === cursor) break;
432
+ suffix.unshift(cursor.slice(parent.length + 1));
433
+ cursor = parent;
434
+ }
435
+ return resolve(realpathSync(cursor), ...suffix);
436
+ }
437
+
438
+ function pathInside(candidate, parent) {
439
+ const rel = relative(realpathSync(parent), canonicalFuturePath(candidate));
440
+ return rel === '' || (!rel.startsWith(`..${sep}`) && rel !== '..' && !isAbsolute(rel));
441
+ }
442
+
443
+ /** @param {string} text @param {Record<string,string>} environment @param {number} maxBytes */
444
+ function sanitizeLog(text, environment, maxBytes) {
445
+ let sanitized = text;
446
+ for (const [name, value] of Object.entries(environment)) {
447
+ if (value && /(token|secret|password|credential|api[_-]?key)/iu.test(name)) sanitized = sanitized.split(value).join('[REDACTED]');
448
+ }
449
+ sanitized = sanitized
450
+ .replace(/\bBearer\s+[A-Za-z0-9._~+/=-]+/giu, 'Bearer [REDACTED]')
451
+ .replace(/\b(?:ghp|glpat|sk)-[A-Za-z0-9_-]{12,}\b/gu, '[REDACTED]');
452
+ const bytes = Buffer.from(sanitized, 'utf8');
453
+ if (bytes.length <= maxBytes) return sanitized;
454
+ return `${bytes.subarray(0, maxBytes).toString('utf8')}\n[TRUNCATED]\n`;
455
+ }
456
+
457
+ /** @param {string} runDir @param {string} log */
458
+ function persistLog(runDir, log) {
459
+ const digest = sha256(Buffer.from(log, 'utf8'));
460
+ const name = `${digest.slice('sha256:'.length)}.log`;
461
+ const directory = join(runDir, 'logs');
462
+ mkdirSync(directory, { recursive: true, mode: 0o700 });
463
+ const path = join(directory, name);
464
+ if (!existsSync(path)) writeFileSync(path, log, { flag: 'wx', mode: 0o600 });
465
+ return { log_digest: digest, log_ref: `logs/${name}` };
466
+ }
467
+
468
+ function executableIdentity(path) {
469
+ const absolute = realpathSync(path);
470
+ const stat = statSync(absolute);
471
+ if (!stat.isFile()) throw new ValidationError(`executable 不是文件: ${path}`);
472
+ return { path: absolute, size: stat.size, mode: stat.mode, digest: sha256(readFileSync(absolute)) };
473
+ }
474
+
475
+ function verifyExecutable(snapshot, name, path) {
476
+ const frozen = snapshot.executable_identities?.[name];
477
+ let live;
478
+ try { live = executableIdentity(path); } catch (error) { throw new OperationalAbort('check_runtime_failure', `冻结 executable 不可用: ${name}: ${error instanceof Error ? error.message : String(error)}`); }
479
+ if (!frozen || canonicalJson(live) !== canonicalJson(frozen)) throw new OperationalAbort('check_runtime_failure', `冻结 executable 已变化: ${name}`);
480
+ }
481
+
482
+ function freezeArgvFiles(profile, workdir) {
483
+ const frozen = {};
484
+ for (const check of profile.l0_checks) {
485
+ const cwd = resolve(workdir, check.cwd_rel);
486
+ for (let index = 1; index < check.argv.length; index += 1) {
487
+ const argument = check.argv[index];
488
+ if (argument.startsWith('-')) continue;
489
+ const candidate = isAbsolute(argument) ? argument : resolve(cwd, argument);
490
+ if (!existsSync(candidate) || !statSync(candidate).isFile()) continue;
491
+ const absolute = realpathSync(candidate);
492
+ frozen[`${check.check_id}:${index}`] = { argument_path: resolve(candidate), ...executableIdentity(absolute) };
493
+ }
494
+ }
495
+ return frozen;
496
+ }
497
+
498
+ function verifyArgvFiles(snapshot, check) {
499
+ for (const [key, identity] of Object.entries(snapshot.argv_file_identities ?? {})) {
500
+ const [checkId, rawIndex] = key.split(':');
501
+ if (checkId !== check.check_id) continue;
502
+ const index = Number(rawIndex);
503
+ const argument = check.argv[index];
504
+ const cwd = resolve(snapshot.workdir, check.cwd_rel);
505
+ const candidate = isAbsolute(argument) ? argument : resolve(cwd, argument);
506
+ let live;
507
+ try { live = { argument_path: resolve(candidate), ...executableIdentity(candidate) }; } catch (error) { throw new OperationalAbort('check_runtime_failure', `冻结 argv 文件不可用: ${key}: ${error instanceof Error ? error.message : String(error)}`); }
508
+ if (canonicalJson(live) !== canonicalJson(identity)) throw new OperationalAbort('check_runtime_failure', `冻结 argv 文件已变化: ${key}`);
509
+ }
510
+ }
511
+
512
+ /** @param {Record<string, any>} snapshot @param {'smoke'|'final'} stage @param {string} runDir */
513
+ function executeChecks(snapshot, stage, runDir) {
514
+ verifyGitArtifact(snapshot.artifact_ref, snapshot.workdir, snapshot.runtime_repository_identity);
515
+ const profile = readJson(join(runDir, 'profile.json'));
516
+ const checks = profile.l0_checks.filter((check) => check.stage === stage || check.stage === 'both');
517
+ const environment = {};
518
+ for (const name of profile.runtime.env_allowlist) if (process.env[name] !== undefined) environment[name] = String(process.env[name]);
519
+ const results = [];
520
+ for (const check of checks) {
521
+ verifyGitArtifact(snapshot.artifact_ref, snapshot.workdir, snapshot.runtime_repository_identity);
522
+ const executable = profile.runtime.executable_paths[check.argv[0]];
523
+ verifyExecutable(snapshot, check.argv[0], executable);
524
+ verifyArgvFiles(snapshot, check);
525
+ const cwd = resolve(snapshot.workdir, check.cwd_rel);
526
+ if (!pathInside(cwd, snapshot.workdir)) throw new ValidationError(`${check.check_id} cwd_rel 越出 workdir`);
527
+ const result = spawnSync(executable, check.argv.slice(1), {
528
+ cwd,
529
+ env: environment,
530
+ encoding: 'utf8',
531
+ timeout: check.timeout_ms,
532
+ maxBuffer: Math.max(profile.runtime.max_log_bytes * 4, 1024 * 1024),
533
+ shell: false,
534
+ stdio: ['ignore', 'pipe', 'pipe'],
535
+ });
536
+ const timedOut = result.error?.code === 'ETIMEDOUT';
537
+ const exitCode = Number.isInteger(result.status) ? result.status : null;
538
+ const log = sanitizeLog([result.stdout, result.stderr, result.error?.message].filter(Boolean).join('\n'), environment, profile.runtime.max_log_bytes);
539
+ const persisted = persistLog(runDir, log);
540
+ if (result.error && !timedOut) throw new OperationalAbort('check_runtime_failure', `${check.check_id} 无法执行,诊断 ${persisted.log_ref}: ${result.error.message}`);
541
+ if (exitCode === null && !timedOut) throw new OperationalAbort('check_runtime_failure', `${check.check_id} 未产生退出码,诊断 ${persisted.log_ref}`);
542
+ results.push({
543
+ check_id: check.check_id,
544
+ argv: check.argv,
545
+ exit_code: exitCode,
546
+ expected_exit_codes: check.expected_exit_codes,
547
+ timed_out: timedOut,
548
+ passed: !timedOut && check.expected_exit_codes.includes(exitCode),
549
+ ...persisted,
550
+ });
551
+ verifyGitArtifact(snapshot.artifact_ref, snapshot.workdir, snapshot.runtime_repository_identity);
552
+ }
553
+ return { stage, checks: results, passed: results.every((item) => item.passed) };
554
+ }
555
+
556
+ /** @param {string} runDir */
557
+ function readJournal(runDir) {
558
+ const path = join(runDir, 'events.ndjson');
559
+ if (!existsSync(path)) throw new ValidationError('run 缺少 events.ndjson');
560
+ const text = readFileSync(path, 'utf8');
561
+ const lastNewline = text.lastIndexOf('\n');
562
+ const complete = lastNewline < 0 ? '' : text.slice(0, lastNewline + 1);
563
+ const trailing = lastNewline === text.length - 1 ? '' : text.slice(lastNewline + 1);
564
+ const events = complete.split('\n').filter(Boolean).map((line) => parseJsonStrict(line));
565
+ let previous = null;
566
+ for (let index = 0; index < events.length; index += 1) {
567
+ const event = events[index];
568
+ if (event.revision !== index || event.previous_event_digest !== previous || !DIGEST_PATTERN.test(event.event_digest ?? '') || envelopeDigest(event, 'event_digest') !== event.event_digest) throw new ValidationError(`event journal 链在 revision ${index} 无效`);
569
+ previous = event.event_digest;
570
+ }
571
+ return { events, complete, trailing };
572
+ }
573
+
574
+ /** @param {string} runDir */
575
+ function loadSnapshot(runDir) {
576
+ const journal = readJournal(runDir);
577
+ const { events } = journal;
578
+ if (events.length === 0) throw new ValidationError('event journal 为空');
579
+ const latest = events.at(-1).snapshot;
580
+ const snapshotPath = join(runDir, 'snapshot.json');
581
+ if (!existsSync(snapshotPath)) return { snapshot: latest, needsRepair: true, events, journal };
582
+ const snapshot = readJson(snapshotPath);
583
+ const snapshotDrift = snapshot.revision !== latest.revision || canonicalJson(snapshot) !== canonicalJson(latest);
584
+ const needsRepair = snapshotDrift || Boolean(journal.trailing);
585
+ return { snapshot: snapshotDrift ? latest : snapshot, needsRepair, events, journal };
586
+ }
587
+
588
+ /** @param {string} runDir @param {Record<string, any>} snapshot @param {string} kind */
589
+ function persistTransition(runDir, snapshot, kind) {
590
+ const next = { ...snapshot, revision: snapshot.revision + 1, updated_at: new Date().toISOString() };
591
+ const previous = readJournal(runDir).events.at(-1)?.event_digest ?? null;
592
+ const event = { schema_version: 1, revision: next.revision, kind, recorded_at: next.updated_at, previous_event_digest: previous, snapshot: next };
593
+ event.event_digest = envelopeDigest(event, 'event_digest');
594
+ const fd = openSync(join(runDir, 'events.ndjson'), 'a', 0o600);
595
+ try {
596
+ appendFileSync(fd, `${canonicalJson(event)}\n`, 'utf8');
597
+ fsyncSync(fd);
598
+ } finally {
599
+ closeSync(fd);
600
+ }
601
+ atomicWriteJson(join(runDir, 'snapshot.json'), next);
602
+ return next;
603
+ }
604
+
605
+ /** @param {string} runDir @param {() => any} callback */
606
+ function withLock(runDir, callback) {
607
+ const path = join(runDir, '.lock');
608
+ const owner = acquireLock(path);
609
+ try { return callback(); } finally { releaseLock(path, owner); }
610
+ }
611
+
612
+ /** @param {string} runDir @param {number|null} expectedRevision @param {(snapshot:Record<string,any>)=>{snapshot:Record<string,any>,kind:string}} callback */
613
+ function mutateRun(runDir, expectedRevision, callback) {
614
+ return withLock(runDir, () => {
615
+ const loaded = loadSnapshot(runDir);
616
+ let snapshot = loaded.snapshot;
617
+ if (loaded.journal.trailing) atomicWriteText(join(runDir, 'events.ndjson'), loaded.journal.complete);
618
+ if (loaded.needsRepair) atomicWriteJson(join(runDir, 'snapshot.json'), snapshot);
619
+ if (expectedRevision !== null && snapshot.revision !== expectedRevision) throw new ValidationError(`revision 冲突:expected ${expectedRevision}, actual ${snapshot.revision}`);
620
+ const currentDigest = skillContentDigest();
621
+ if (!snapshot.terminal && snapshot.skill_provenance.content_digest !== currentDigest) {
622
+ snapshot = persistTransition(runDir, {
623
+ ...snapshot,
624
+ status: 'aborted',
625
+ terminal: null,
626
+ operational_abort: { code: 'skill_drift', diagnostics: 'Skill 内容摘要与 init 时不一致' },
627
+ }, 'operational_abort');
628
+ throw new OperationalAbort('skill_drift', `run 已记录 abort at revision ${snapshot.revision}`);
629
+ }
630
+ try {
631
+ const result = callback(snapshot);
632
+ return persistTransition(runDir, result.snapshot, result.kind);
633
+ } catch (error) {
634
+ if (error instanceof OperationalAbort && !snapshot.terminal && snapshot.status !== 'aborted') {
635
+ persistTransition(runDir, {
636
+ ...snapshot,
637
+ status: 'aborted',
638
+ terminal: null,
639
+ operational_abort: { code: error.code, diagnostics: error.message },
640
+ }, 'operational_abort');
641
+ }
642
+ throw error;
643
+ }
644
+ });
645
+ }
646
+
647
+ /** @param {Record<string, any>} review @param {Record<string, any>} snapshot @param {Set<string>} acceptanceIds */
648
+ function validateReview(review, snapshot, acceptanceIds) {
649
+ try { validateJsonSchema(review, schema('review-result-v1.schema.json'), 'Review Result'); } catch (error) { throw new ValidationError(error instanceof Error ? error.message : String(error)); }
650
+ if (review.schema_version !== 1 || !review.review_result_id) throw new ValidationError('Review Result schema/id 无效');
651
+ if (review.contract_digest !== snapshot.contract_digest || review.verification_profile_digest !== snapshot.verification_profile_digest) throw new ValidationError('Review Result contract/profile binding 不匹配');
652
+ if (review.challenge_nonce !== snapshot.review_challenge_nonce) throw new ValidationError('Review Result challenge nonce 不匹配或已重放');
653
+ if (canonicalJson(review.artifact_ref) !== canonicalJson(snapshot.artifact_ref)) throw new ValidationError('Review Result Artifact binding 不匹配');
654
+ if (!REVIEW_VERDICTS.has(review.verdict) || !Array.isArray(review.findings) || !Array.isArray(review.forensics)) throw new ValidationError('Review Result verdict/findings/forensics 无效');
655
+ for (const finding of review.findings) {
656
+ if (!acceptanceIds.has(finding?.contract_item_id)) throw new ValidationError(`finding 引用未知 contract_item_id: ${finding?.contract_item_id}`);
657
+ if (!FINDING_CLASSES.has(finding?.class)) throw new ValidationError(`finding class 非法: ${finding?.class}`);
658
+ for (const field of ['evidence', 'expected', 'actual']) if (typeof finding[field] !== 'string' || !finding[field].trim()) throw new ValidationError(`finding ${field} 必须是非空字符串`);
659
+ }
660
+ if (review.forensics.some((item) => typeof item !== 'string' || !item.trim())) throw new ValidationError('forensics 必须是非空字符串数组');
661
+ if (review.verdict === 'fail' && review.findings.length === 0) throw new ValidationError('fail 必须包含 finding');
662
+ if (review.verdict === 'no_defect_found' && (review.findings.length > 0 || review.forensics.length === 0)) throw new ValidationError('no_defect_found 必须无 finding 且有 forensics');
663
+ if (review.verdict === 'undecidable' && review.findings.length === 0) throw new ValidationError('undecidable 必须说明缺失证据');
664
+ if (!DIGEST_PATTERN.test(review.review_result_digest) || envelopeDigest(review, 'review_result_digest') !== review.review_result_digest) throw new ValidationError('Review Result digest 无效');
665
+ }
666
+
667
+ /** @param {Record<string, any>} snapshot @param {string} runDir @param {string} outcome */
668
+ function terminalSnapshot(snapshot, runDir, outcome) {
669
+ if (!TERMINAL_OUTCOMES.has(outcome)) throw new ValidationError(`非法 terminal outcome: ${outcome}`);
670
+ const profile = readJson(join(runDir, 'profile.json'));
671
+ const limitations = [];
672
+ if (profile.runtime.network_policy === 'denied' && snapshot.network_isolation_assurance !== 'host_reported') limitations.push('network_policy_not_os_enforced');
673
+ if (profile.runtime.cache_policy === 'disabled') limitations.push('generic_runtime_cannot_prove_all_tool_caches_disabled');
674
+ if (!snapshot.review_result) limitations.push('l1_not_run');
675
+ const evidence = {
676
+ schema_version: 1,
677
+ run_id: snapshot.run_id,
678
+ protocol_version: PROTOCOL_VERSION,
679
+ runtime_version: RUNTIME_VERSION,
680
+ contract_digest: snapshot.contract_digest,
681
+ verification_profile_digest: snapshot.verification_profile_digest,
682
+ artifact_ref: snapshot.artifact_ref,
683
+ stages: {
684
+ smoke_l0: snapshot.stages.smoke_l0 ?? { status: 'not_run' },
685
+ l1_review: snapshot.review_result ?? { status: 'not_run' },
686
+ final_l0: snapshot.stages.final_l0 ?? { status: 'not_run' },
687
+ },
688
+ terminal_outcome: outcome,
689
+ completion_scope: 'verification_only',
690
+ human_gate_required: profile.human_gate !== 'none',
691
+ provenance: {
692
+ provider: 'verify-agent-output',
693
+ verified_at: snapshot.created_at,
694
+ verifier_run_id: snapshot.review_provenance?.verifier_run_id ?? 'not_run',
695
+ isolation_assurance: snapshot.review_provenance?.isolation_assurance ?? snapshot.planned_isolation_assurance,
696
+ skill: snapshot.skill_provenance,
697
+ limitations,
698
+ },
699
+ };
700
+ evidence.evidence_digest = envelopeDigest(evidence, 'evidence_digest');
701
+ const evidencePath = join(runDir, 'evidence.json');
702
+ let persisted = evidence;
703
+ if (existsSync(evidencePath)) {
704
+ persisted = readJson(evidencePath);
705
+ if (canonicalJson(persisted) !== canonicalJson(evidence)) {
706
+ throw new ValidationError('已存在的 orphan Evidence 与当前 terminal transition 不兼容');
707
+ }
708
+ } else writeNewJson(evidencePath, evidence);
709
+ return {
710
+ ...snapshot,
711
+ status: 'terminal',
712
+ stages: {
713
+ ...snapshot.stages,
714
+ smoke_l0: persisted.stages.smoke_l0?.status === 'not_run'
715
+ ? (snapshot.stages.smoke_l0 ?? { status: 'not_run' })
716
+ : persisted.stages.smoke_l0,
717
+ final_l0: persisted.stages.final_l0?.status === 'not_run'
718
+ ? (snapshot.stages.final_l0 ?? { status: 'not_run' })
719
+ : persisted.stages.final_l0,
720
+ },
721
+ terminal: { outcome, evidence_ref: 'evidence.json', evidence_digest: persisted.evidence_digest },
722
+ };
723
+ }
724
+
725
+ /** @param {string[]} argv */
726
+ const CLI_SPEC = {
727
+ capabilities: { values: [], flags: ['json'] },
728
+ scaffold: { values: ['kind', 'workdir', 'base-sha', 'artifact-sha', 'review-input'], flags: [] },
729
+ prepare: { values: ['workdir', 'out-dir'], flags: [] },
730
+ digest: { values: ['kind', 'input'], flags: [] },
731
+ readiness: { values: ['contract', 'profile', 'workdir', 'state-root'], flags: [] },
732
+ preflight: { values: ['contract', 'profile', 'artifact'], flags: ['network-isolated'] },
733
+ init: { values: ['contract', 'profile', 'artifact', 'workdir', 'state-root', 'isolation-assurance', 'run-id'], flags: ['network-isolated', 'allow-repository-state'] },
734
+ 'prepare-run': { values: ['contract', 'profile', 'artifact', 'workdir', 'state-root', 'isolation-assurance', 'run-id'], flags: ['network-isolated', 'allow-repository-state', 'verbose'] },
735
+ 'run-smoke': { values: ['run', 'expected-revision'], flags: ['verbose'] },
736
+ 'review-input': { values: ['run'], flags: [] },
737
+ 'review-bundle': { values: ['run', 'out'], flags: [] },
738
+ 'record-review': { values: ['run', 'review', 'verifier-run-id', 'isolation-assurance', 'expected-revision'], flags: ['stdin', 'verbose'] },
739
+ 'run-final': { values: ['run', 'expected-revision'], flags: ['verbose'] },
740
+ 'record-reflection': { values: ['run', 'input', 'expected-revision'], flags: [] },
741
+ 'propose-improvement': { values: ['run', 'reflection', 'input', 'expected-revision'], flags: [] },
742
+ status: { values: ['run'], flags: [] }, inspect: { values: ['run'], flags: [] }, validate: { values: ['run'], flags: [] }, doctor: { values: ['run'], flags: [] },
743
+ };
744
+ const CLI_USAGE = {
745
+ capabilities: '用法: capabilities [--json]',
746
+ scaffold: '用法: scaffold --kind contract|profile|artifact|review|bundle [--workdir <git-root> --base-sha <full-sha>] [--review-input <json>]',
747
+ prepare: '用法: prepare --workdir <git-root> [--out-dir <dir>]',
748
+ digest: '用法: digest --kind contract|profile|review --input <json>',
749
+ readiness: '用法: readiness --contract <json> --profile <json> --workdir <git-root> [--state-root <dir>]',
750
+ preflight: '用法: preflight --contract <json> --profile <json> --artifact <json> [--network-isolated]',
751
+ init: '用法: init --contract <json> --profile <json> --artifact <json> --workdir <git-root> --isolation-assurance host_reported|user_relayed [--state-root <dir>] [--run-id <id>]',
752
+ 'prepare-run': '用法: prepare-run --contract <json> --profile <json> --artifact <json> --workdir <git-root> --isolation-assurance host_reported|user_relayed [--state-root <dir>] [--run-id <id>] [--network-isolated] [--verbose]',
753
+ 'run-smoke': '用法: run-smoke --run <run-dir> [--expected-revision <n>] [--verbose]',
754
+ 'review-input': '用法: review-input --run <run-dir>',
755
+ 'review-bundle': '用法: review-bundle --run <run-dir> [--out <path>]',
756
+ 'record-review': '用法: record-review --run <run-dir> (--review <json>|--stdin) --verifier-run-id <id> --isolation-assurance host_reported|user_relayed [--expected-revision <n>] [--verbose]',
757
+ 'run-final': '用法: run-final --run <run-dir> [--expected-revision <n>] [--verbose]',
758
+ 'record-reflection': '用法: record-reflection --run <run-dir> --input <json> [--expected-revision <n>]',
759
+ 'propose-improvement': '用法: propose-improvement --run <run-dir> --reflection <ref> --input <json> [--expected-revision <n>]',
760
+ status: '用法: status --run <run-dir>',
761
+ inspect: '用法: inspect --run <run-dir>',
762
+ validate: '用法: validate --run <run-dir>',
763
+ doctor: '用法: doctor --run <run-dir>',
764
+ };
765
+ const CLI_COMMANDS = Object.keys(CLI_SPEC).join('/');
766
+
767
+ /** @param {string|null} command */
768
+ function helpText(command = null) {
769
+ if (command) {
770
+ if (!CLI_SPEC[command]) throw new ValidationError(`未知命令: ${command}`);
771
+ return `${CLI_USAGE[command] ?? `用法: ${command}`}\n`;
772
+ }
773
+ return [
774
+ 'verify-agent-output verification runtime',
775
+ '',
776
+ '命令:',
777
+ ...Object.keys(CLI_SPEC).map((name) => ` ${name.padEnd(20)} ${CLI_USAGE[name] ?? ''}`),
778
+ '',
779
+ '运行 `<command> --help` 查看子命令用法。',
780
+ ].join('\n') + '\n';
781
+ }
782
+
783
+ /** @param {string} command */
784
+ function usageHint(command) {
785
+ return CLI_USAGE[command] ? `;${CLI_USAGE[command]}` : '';
786
+ }
787
+
788
+ function parseCli(argv) {
789
+ const command = argv[0] ?? '';
790
+ const spec = CLI_SPEC[command];
791
+ if (!spec) throw new ValidationError(`命令必须是 ${CLI_COMMANDS}`);
792
+ const options = {};
793
+ const flags = new Set();
794
+ for (let index = 1; index < argv.length; index += 1) {
795
+ const token = argv[index];
796
+ if (!token.startsWith('--')) throw new ValidationError(`未知位置参数: ${token}${usageHint(command)}`);
797
+ const name = token.slice(2);
798
+ if (spec.flags.includes(name)) {
799
+ if (argv[index + 1] !== undefined && !argv[index + 1].startsWith('--')) throw new ValidationError(`--${name} 不接受值${usageHint(command)}`);
800
+ flags.add(name);
801
+ } else if (spec.values.includes(name)) {
802
+ if (argv[index + 1] === undefined || argv[index + 1].startsWith('--')) throw new ValidationError(`--${name} 缺少值${usageHint(command)}`);
803
+ options[name] = argv[index + 1];
804
+ index += 1;
805
+ } else throw new ValidationError(`未知选项: --${name}${usageHint(command)}`);
806
+ }
807
+ return { command, options, flags };
808
+ }
809
+
810
+ /** @param {Record<string,string>} options @param {string} name */
811
+ function required(options, name) {
812
+ if (!options[name]) throw new ValidationError(`缺少 --${name}`);
813
+ return options[name];
814
+ }
815
+
816
+ /** @param {Record<string,string>} options */
817
+ function expectedRevision(options) {
818
+ if (options['expected-revision'] === undefined) return null;
819
+ const value = Number(options['expected-revision']);
820
+ if (!Number.isInteger(value) || value < 0) throw new ValidationError('--expected-revision 必须是非负整数');
821
+ return value;
822
+ }
823
+
824
+ function capabilities() {
825
+ return {
826
+ skill: 'verify-agent-output',
827
+ runtime_version: RUNTIME_VERSION,
828
+ protocol_versions: [PROTOCOL_VERSION],
829
+ contracts: { task_contract: [1], verification_profile: [1], artifact_ref: [1], review_result: [1], evidence_package: [1], reflection_record: [1], improvement_proposal: [1] },
830
+ features: ['input-scaffold', 'digest-helper', 'aggregate-preflight', 'prepare-scaffold-chain', 'prepare-run', 'readiness-preconditions', 'review-bundle', 'review-stdin', 'compact-cli-output', 'command-help', 'git-artifact', 'strict-json', 'rfc8785-digest', 'argv-l0', 'immutable-evidence', 'journal-recovery', 'skill-drift', 'evidence-bound-reflection', 'proposed-only-improvement'],
831
+ content_digest: skillContentDigest(),
832
+ };
833
+ }
834
+
835
+ /** @param {Record<string, any>} value @param {string} field */
836
+ function addDigest(value, field) {
837
+ const output = { ...value };
838
+ delete output[field];
839
+ output[field] = envelopeDigest(output, field);
840
+ return output;
841
+ }
842
+
843
+ /** @param {string} workdir */
844
+ function scaffoldContract(workdir) {
845
+ return addDigest({
846
+ schema_version: 1,
847
+ contract_id: randomUUID(),
848
+ objective: 'TODO: describe the frozen artifact objective',
849
+ scope: { include: ['TODO'], exclude: [] },
850
+ acceptance: [{ contract_item_id: 'acceptance-1', requirement: 'TODO: replace with an observable requirement' }],
851
+ permissions: { mode: 'read_only', writable_paths: [] },
852
+ environment: { repository: resolve(workdir), isolation: 'caller_supplied' },
853
+ skill_set: [{ name: 'verify-agent-output', version: RUNTIME_VERSION, content_digest: skillContentDigest(), provider_mode: 'primary' }],
854
+ stop_conditions: [],
855
+ extensions: {},
856
+ }, 'contract_digest');
857
+ }
858
+
859
+ function scaffoldProfile() {
860
+ return addDigest({
861
+ schema_version: 1,
862
+ profile_id: randomUUID(),
863
+ l0_checks: [{ check_id: 'replace-with-real-check', argv: ['node', '--version'], cwd_rel: '.', stage: 'both', timeout_ms: 30_000, expected_exit_codes: [0] }],
864
+ l1_review: [{ contract_item_id: 'acceptance-1', lenses: ['functional', 'scope', 'verification_definition', 'safety'] }],
865
+ protected_verifier_paths: [],
866
+ allowed_validation_changes: [],
867
+ runtime: { env_allowlist: ['PATH'], executable_paths: { node: process.execPath }, cache_policy: 'trusted_identity', network_policy: 'contract_authorized', max_log_bytes: 1_048_576 },
868
+ human_gate: 'none',
869
+ }, 'verification_profile_digest');
870
+ }
871
+
872
+ /** @param {Record<string,string>} options */
873
+ function scaffoldArtifact(options) {
874
+ const workdir = realpathSync(required(options, 'workdir'));
875
+ const objectFormat = String(git(['rev-parse', '--show-object-format'], workdir)).trim();
876
+ const artifactSha = options['artifact-sha'] ?? String(git(['rev-parse', 'HEAD'], workdir)).trim();
877
+ const baseSha = required(options, 'base-sha');
878
+ const roots = String(git(['rev-list', '--max-parents=0', artifactSha], workdir)).trim().split('\n').filter(Boolean).sort();
879
+ const artifact = { schema_version: 1, provider: 'caller-supplied', repository_id: `git:${objectFormat}:${sha256(Buffer.from(canonicalJson(roots), 'utf8'))}`, object_format: objectFormat, base_sha: baseSha, artifact_sha: artifactSha };
880
+ validateArtifact(artifact);
881
+ verifyGitArtifact(artifact, workdir);
882
+ return artifact;
883
+ }
884
+
885
+ /** @param {Record<string,string>} options */
886
+ function scaffoldReview(options) {
887
+ const input = readJson(required(options, 'review-input'));
888
+ const contractDigest = input.contract_digest ?? input.contract?.contract_digest;
889
+ const profileDigest = input.verification_profile_digest;
890
+ const firstItem = input.reviewer_view?.[0];
891
+ if (!DIGEST_PATTERN.test(contractDigest ?? '') || !DIGEST_PATTERN.test(profileDigest ?? '') || !input.artifact_ref || !input.challenge_nonce || !firstItem?.contract_item_id) throw new ValidationError('review-input 缺少 Review Result 所需绑定字段');
892
+ return addDigest({
893
+ schema_version: 1,
894
+ review_result_id: randomUUID(),
895
+ contract_digest: contractDigest,
896
+ verification_profile_digest: profileDigest,
897
+ artifact_ref: input.artifact_ref,
898
+ challenge_nonce: input.challenge_nonce,
899
+ verdict: 'undecidable',
900
+ findings: [{ contract_item_id: firstItem.contract_item_id, class: 'verification_definition', evidence: 'TODO: identify the missing or conflicting evidence', expected: firstItem.requirement, actual: 'TODO: describe why the requirement cannot yet be decided' }],
901
+ forensics: ['TODO: replace with independent forensic actions before recording the review'],
902
+ }, 'review_result_digest');
903
+ }
904
+
905
+ /** @param {Record<string,string>} options */
906
+ function scaffold(options) {
907
+ if (!options.kind) throw new ValidationError(CLI_USAGE.scaffold);
908
+ const kind = options.kind;
909
+ if (['artifact', 'bundle'].includes(kind) && (!options.workdir || !options['base-sha'])) throw new ValidationError(`${CLI_USAGE.scaffold};artifact/bundle 必须提供 --workdir 与 --base-sha`);
910
+ if (kind === 'review' && !options['review-input']) throw new ValidationError(`${CLI_USAGE.scaffold};review 必须提供 --review-input`);
911
+ if (kind === 'contract') return scaffoldContract(options.workdir ?? process.cwd());
912
+ if (kind === 'profile') return scaffoldProfile();
913
+ if (kind === 'artifact') return scaffoldArtifact(options);
914
+ if (kind === 'review') return scaffoldReview(options);
915
+ if (kind === 'bundle') return { contract: scaffoldContract(required(options, 'workdir')), profile: scaffoldProfile(), artifact: scaffoldArtifact(options) };
916
+ throw new ValidationError('--kind 必须是 contract/profile/artifact/review/bundle');
917
+ }
918
+
919
+ const RUNTIME_SCRIPT_PATH = join(DOMAIN_ROOT, 'verification-runtime.mjs');
920
+ const PREPARE_NOTICE = 'Verification Profile 的 l0_checks 需 controller 按项目实际填写并确认;本命令不猜测任何测试命令,也不内置任何项目专属 preset。';
921
+
922
+ /** 串联 scaffold contract + scaffold profile,只产出骨架与 TODO 清单,不猜测测试命令。 @param {Record<string,string>} options */
923
+ function prepare(options) {
924
+ const workdir = realpathSync(required(options, 'workdir'));
925
+ const contract = scaffoldContract(workdir);
926
+ const profile = scaffoldProfile();
927
+ const result = {
928
+ schema_version: 1,
929
+ kind: 'prepare_scaffold_chain',
930
+ runtime_version: RUNTIME_VERSION,
931
+ workdir,
932
+ notice: PREPARE_NOTICE,
933
+ todo: [
934
+ 'contract.json: objective 替换 TODO,写清被验收产物的目标',
935
+ 'contract.json: scope.include / scope.exclude 按真实改动面填写',
936
+ 'contract.json: acceptance[] 每条给稳定唯一 contract_item_id 与可观察 requirement;acceptance-1 只是占位',
937
+ 'contract.json: permissions 与 environment.isolation 按实际授权和隔离方式填写',
938
+ 'profile.json: l0_checks 必须由 controller 按项目实际测试命令填写并确认;骨架里的 node --version 只是占位,不是推荐值',
939
+ 'profile.json: l0_checks 至少覆盖一条 smoke(stage=smoke|both)和一条 final(stage=final|both)',
940
+ 'profile.json: runtime.executable_paths 为每个 argv[0] 冻结绝对可执行文件路径',
941
+ 'profile.json: l1_review[].contract_item_id 必须引用 contract.json 里已存在的 acceptance ID',
942
+ 'profile.json: protected_verifier_paths / allowed_validation_changes 按验证定义面填写',
943
+ 'profile.json: human_gate、runtime.network_policy、runtime.cache_policy 按实际风险选择',
944
+ '两份文件改完后先用 digest 重算摘要,再跑 readiness 与 preflight;readiness 与 preflight 由 controller 自行执行,prepare 不代跑',
945
+ ],
946
+ next_steps: [
947
+ `node ${RUNTIME_SCRIPT_PATH} digest --kind contract --input <contract.json>`,
948
+ `node ${RUNTIME_SCRIPT_PATH} digest --kind profile --input <profile.json>`,
949
+ `node ${RUNTIME_SCRIPT_PATH} scaffold --kind artifact --workdir ${workdir} --base-sha <full-base-sha>`,
950
+ `node ${RUNTIME_SCRIPT_PATH} readiness --contract <contract.json> --profile <profile.json> --workdir ${workdir}`,
951
+ `node ${RUNTIME_SCRIPT_PATH} preflight --contract <contract.json> --profile <profile.json> --artifact <artifact.json>`,
952
+ ],
953
+ };
954
+ if (!options['out-dir']) return { ...result, contract, profile };
955
+ const outDir = resolve(options['out-dir']);
956
+ mkdirSync(outDir, { recursive: true, mode: 0o700 });
957
+ const contractPath = join(outDir, 'contract.json');
958
+ const profilePath = join(outDir, 'profile.json');
959
+ for (const path of [contractPath, profilePath]) if (existsSync(path)) throw new ValidationError(`prepare 拒绝覆盖已存在的文件: ${path}`);
960
+ writeNewJson(contractPath, contract);
961
+ writeNewJson(profilePath, profile);
962
+ return { ...result, out_dir: outDir, contract_path: contractPath, profile_path: profilePath };
963
+ }
964
+
965
+ /** @param {Record<string,string>} options */
966
+ function digestEnvelope(options) {
967
+ if (!options.kind || !options.input) throw new ValidationError(CLI_USAGE.digest);
968
+ const kind = options.kind;
969
+ const field = { contract: 'contract_digest', profile: 'verification_profile_digest', review: 'review_result_digest' }[kind];
970
+ if (!field) throw new ValidationError('--kind 必须是 contract/profile/review');
971
+ return addDigest(readJson(required(options, 'input')), field);
972
+ }
973
+
974
+ /** @param {Record<string,any>|null} contract @param {Record<string,any>|null} profile @param {Record<string,any>|null} artifact @param {Set<string>} flags */
975
+ function inspectValues(contract, profile, artifact, flags) {
976
+ const issues = [];
977
+ if (contract) issues.push(...collectContractIssues(contract));
978
+ const acceptanceIds = new Set(Array.isArray(contract?.acceptance) ? contract.acceptance.map((item) => item?.contract_item_id).filter(Boolean) : []);
979
+ if (profile) issues.push(...collectProfileIssues(profile, acceptanceIds));
980
+ if (artifact) issues.push(...collectArtifactIssues(artifact));
981
+ if (contract) {
982
+ try { validateSkillBinding(contract, skillContentDigest()); }
983
+ catch (error) { issues.push(error instanceof Error ? error.message : String(error)); }
984
+ }
985
+ if (profile?.runtime?.network_policy === 'denied' && !flags.has('network-isolated')) issues.push('network_policy=denied 时必须由宿主提供 --network-isolated assurance');
986
+ return {
987
+ valid: issues.length === 0,
988
+ errors: [...new Set(issues)],
989
+ content_digest: skillContentDigest(),
990
+ contract_digest: contract?.contract_digest ?? null,
991
+ verification_profile_digest: profile?.verification_profile_digest ?? null,
992
+ };
993
+ }
994
+
995
+ /** @param {Record<string,string>} options @param {Set<string>} flags */
996
+ function inspectInputs(options, flags) {
997
+ const issues = [];
998
+ const load = (name, label) => {
999
+ if (!options[name]) { issues.push(`${label} 缺少 --${name}`); return null; }
1000
+ try { return readJson(options[name]); }
1001
+ catch (error) { issues.push(`${label} 无法读取: ${error instanceof Error ? error.message : String(error)}`); return null; }
1002
+ };
1003
+ const contract = load('contract', 'Task Contract');
1004
+ const profile = load('profile', 'Verification Profile');
1005
+ const artifact = load('artifact', 'Artifact Ref');
1006
+ const report = inspectValues(contract, profile, artifact, flags);
1007
+ const errors = [...new Set([...issues, ...report.errors])];
1008
+ return {
1009
+ report: { ...report, valid: errors.length === 0, errors },
1010
+ contract,
1011
+ profile,
1012
+ artifact,
1013
+ };
1014
+ }
1015
+
1016
+ /** @param {Record<string,string>} options @param {Set<string>} flags */
1017
+ function preflight(options, flags) {
1018
+ return inspectInputs(options, flags).report;
1019
+ }
1020
+
1021
+ /** @param {string} path @param {number} mode */
1022
+ function accessible(path, mode) {
1023
+ try { accessSync(path, mode); return true; } catch { return false; }
1024
+ }
1025
+
1026
+ /** 找到 path 自身或最近一个已存在的祖先目录。 @param {string} path */
1027
+ function nearestExistingAncestor(path) {
1028
+ let cursor = resolve(path);
1029
+ while (!existsSync(cursor)) {
1030
+ const parent = dirname(cursor);
1031
+ if (parent === cursor) return cursor;
1032
+ cursor = parent;
1033
+ }
1034
+ return cursor;
1035
+ }
1036
+
1037
+ /**
1038
+ * 机械检查“环境前提”,不检查产物质量。任何未通过项都归类为 blocked/precondition,
1039
+ * 永远不是 Artifact fail。frozen executable / argv 文件的身份漂移由 run 内的冻结门禁
1040
+ * (check_runtime_failure)负责,因此可用 exclude 排除,避免两套语义互相顶掉。
1041
+ * @param {{contract?:Record<string,any>|null, profile?:Record<string,any>|null, workdir:string, stateRoot?:string|null, exclude?:RegExp|null}} input
1042
+ */
1043
+ function evaluateReadiness({ contract = null, profile = null, workdir, stateRoot = null, exclude = null }) {
1044
+ const checks = [];
1045
+ const notes = [];
1046
+ const record = (checkId, ok, detail) => {
1047
+ if (exclude && exclude.test(checkId)) return;
1048
+ checks.push({ check_id: checkId, ok, detail });
1049
+ };
1050
+
1051
+ let resolvedWorkdir = resolve(workdir);
1052
+ if (!existsSync(resolvedWorkdir)) record('workdir_git_root', false, `workdir 不存在: ${resolvedWorkdir}`);
1053
+ else if (!statSync(resolvedWorkdir).isDirectory()) record('workdir_git_root', false, `workdir 不是目录: ${resolvedWorkdir}`);
1054
+ else {
1055
+ resolvedWorkdir = realpathSync(resolvedWorkdir);
1056
+ const toplevel = gitStatus(['rev-parse', '--show-toplevel'], resolvedWorkdir);
1057
+ if (toplevel.status !== 0) record('workdir_git_root', false, `workdir 不是 Git 仓库: ${resolvedWorkdir}`);
1058
+ else if (realpathSync(String(toplevel.stdout).trim()) !== resolvedWorkdir) record('workdir_git_root', false, `workdir 不是 Git worktree 根目录,根目录是 ${String(toplevel.stdout).trim()}`);
1059
+ else record('workdir_git_root', true, `Git worktree 根目录: ${resolvedWorkdir}`);
1060
+ }
1061
+
1062
+ const stateRootPath = resolve(stateRoot ?? join(tmpdir(), 'verify-agent-output-state'));
1063
+ const stateAnchor = nearestExistingAncestor(stateRootPath);
1064
+ if (existsSync(stateRootPath) && !statSync(stateRootPath).isDirectory()) record('state_root_writable', false, `state root 不是目录: ${stateRootPath}`);
1065
+ else if (!accessible(stateAnchor, fsConstants.W_OK | fsConstants.X_OK)) record('state_root_writable', false, `state root 不可写: ${stateRootPath}(受阻于 ${stateAnchor})`);
1066
+ else record('state_root_writable', true, `state root 可写: ${stateRootPath}`);
1067
+
1068
+ if (contract && typeof contract.environment?.repository === 'string' && contract.environment.repository) {
1069
+ const declared = resolve(contract.environment.repository);
1070
+ if (existsSync(declared) && existsSync(resolvedWorkdir) && realpathSync(declared) !== resolvedWorkdir) {
1071
+ notes.push(`Task Contract environment.repository (${declared}) 与 workdir (${resolvedWorkdir}) 不是同一目录;readiness 不据此拦截,请自行确认是否有意为之。`);
1072
+ }
1073
+ }
1074
+
1075
+ const executablePaths = profile?.runtime?.executable_paths;
1076
+ if (executablePaths && typeof executablePaths === 'object') {
1077
+ for (const [name, path] of Object.entries(executablePaths)) {
1078
+ if (typeof path !== 'string' || !path) record(`executable:${name}`, false, `runtime.executable_paths.${name} 不是路径字符串`);
1079
+ else if (!isAbsolute(path)) record(`executable:${name}`, false, `runtime.executable_paths.${name} 必须是绝对路径: ${path}`);
1080
+ else if (!existsSync(path)) record(`executable:${name}`, false, `冻结 executable 不存在: ${name} -> ${path}`);
1081
+ else if (!statSync(path).isFile()) record(`executable:${name}`, false, `冻结 executable 不是文件: ${name} -> ${path}`);
1082
+ else if (!accessible(path, fsConstants.X_OK)) record(`executable:${name}`, false, `冻结 executable 不可执行: ${name} -> ${path}`);
1083
+ else record(`executable:${name}`, true, `${name} -> ${path}`);
1084
+ }
1085
+ }
1086
+
1087
+ if (Array.isArray(profile?.l0_checks)) {
1088
+ for (const check of profile.l0_checks) {
1089
+ const checkId = check?.check_id ?? '<missing>';
1090
+ if (typeof check?.cwd_rel !== 'string' || !Array.isArray(check?.argv)) continue;
1091
+ const cwd = resolve(resolvedWorkdir, check.cwd_rel);
1092
+ if (!existsSync(cwd)) record(`l0_cwd:${checkId}`, false, `L0 cwd 不存在: ${cwd}`);
1093
+ else if (!statSync(cwd).isDirectory()) record(`l0_cwd:${checkId}`, false, `L0 cwd 不是目录: ${cwd}`);
1094
+ else if (!pathInside(cwd, resolvedWorkdir)) record(`l0_cwd:${checkId}`, false, `L0 cwd 越出 workdir: ${cwd}`);
1095
+ else record(`l0_cwd:${checkId}`, true, cwd);
1096
+ // 只对已经存在的路径类参数做可读性检查,与 L0 冻结逻辑同口径:不猜测哪些参数是文件。
1097
+ for (let index = 1; index < check.argv.length; index += 1) {
1098
+ const argument = check.argv[index];
1099
+ if (typeof argument !== 'string' || argument.startsWith('-')) continue;
1100
+ const candidate = isAbsolute(argument) ? argument : resolve(cwd, argument);
1101
+ if (!existsSync(candidate) || !statSync(candidate).isFile()) continue;
1102
+ if (accessible(candidate, fsConstants.R_OK)) record(`argv_file:${checkId}:${index}`, true, candidate);
1103
+ else record(`argv_file:${checkId}:${index}`, false, `L0 argv 文件不可读: ${candidate}`);
1104
+ }
1105
+ }
1106
+ }
1107
+
1108
+ const allowlist = Array.isArray(profile?.runtime?.env_allowlist) ? profile.runtime.env_allowlist.filter((name) => typeof name === 'string' && name) : [];
1109
+ if (allowlist.length > 0) {
1110
+ const missing = allowlist.filter((name) => process.env[name] === undefined);
1111
+ notes.push(`env_allowlist 是否为 L0 必需无法机械判定,readiness 不猜测也不拦截;当前未设置的变量:${missing.length ? missing.join(', ') : '(无)'}。`);
1112
+ }
1113
+
1114
+ const blockers = checks.filter((item) => !item.ok).map((item) => ({ kind: 'precondition', check_id: item.check_id, detail: item.detail }));
1115
+ return {
1116
+ ready: blockers.length === 0,
1117
+ workdir: resolvedWorkdir,
1118
+ state_root: stateRootPath,
1119
+ blockers,
1120
+ checks,
1121
+ notes,
1122
+ blocker_semantics: 'blocked_precondition_not_artifact_defect',
1123
+ };
1124
+ }
1125
+
1126
+ /** @param {Record<string,string>} options */
1127
+ function readiness(options) {
1128
+ const workdir = required(options, 'workdir');
1129
+ const loaded = {};
1130
+ const readErrors = [];
1131
+ for (const [name, label] of [['contract', 'Task Contract'], ['profile', 'Verification Profile']]) {
1132
+ const path = required(options, name);
1133
+ try { loaded[name] = readJson(path); }
1134
+ catch (error) { readErrors.push({ kind: 'precondition', check_id: `${name}_readable`, detail: `${label} 无法读取或解析: ${error instanceof Error ? error.message : String(error)}` }); }
1135
+ }
1136
+ const report = evaluateReadiness({ contract: loaded.contract ?? null, profile: loaded.profile ?? null, workdir, stateRoot: options['state-root'] ?? null });
1137
+ if (readErrors.length === 0) return report;
1138
+ return { ...report, ready: false, blockers: [...readErrors, ...report.blockers] };
1139
+ }
1140
+
1141
+ /**
1142
+ * Happy path:只在 readiness 与 preflight 都通过后创建 run;输入文件只读,digest 在临时副本中规范化。
1143
+ * @param {Record<string,string>} options @param {Set<string>} flags
1144
+ */
1145
+ function prepareRun(options, flags) {
1146
+ const contractSource = readJson(required(options, 'contract'));
1147
+ const profileSource = readJson(required(options, 'profile'));
1148
+ const artifact = readJson(required(options, 'artifact'));
1149
+ const contract = addDigest(contractSource, 'contract_digest');
1150
+ const profile = addDigest(profileSource, 'verification_profile_digest');
1151
+ const workdir = required(options, 'workdir');
1152
+ const stateRoot = options['state-root'] ?? join(tmpdir(), 'verify-agent-output-state');
1153
+ const readinessReport = evaluateReadiness({ contract, profile, workdir, stateRoot });
1154
+ if (!readinessReport.ready) {
1155
+ return { prepared: false, status: 'blocked_precondition', readiness: readinessReport, preflight: null, run_id: null, revision: null, evidence_digest: null };
1156
+ }
1157
+ const preflightReport = inspectValues(contract, profile, artifact, flags);
1158
+ if (!preflightReport.valid) {
1159
+ return { prepared: false, status: 'invalid_input', readiness: readinessReport, preflight: preflightReport, run_id: null, revision: null, evidence_digest: null };
1160
+ }
1161
+
1162
+ const temporary = mkdtempSync(join(tmpdir(), 'verify-agent-output-prepare-'));
1163
+ try {
1164
+ const paths = {
1165
+ contract: join(temporary, 'contract.json'),
1166
+ profile: join(temporary, 'profile.json'),
1167
+ artifact: join(temporary, 'artifact.json'),
1168
+ };
1169
+ writeNewJson(paths.contract, contract);
1170
+ writeNewJson(paths.profile, profile);
1171
+ writeNewJson(paths.artifact, artifact);
1172
+ const initialized = initialize({
1173
+ ...options,
1174
+ contract: paths.contract,
1175
+ profile: paths.profile,
1176
+ artifact: paths.artifact,
1177
+ 'state-root': stateRoot,
1178
+ }, flags);
1179
+ return {
1180
+ prepared: true,
1181
+ ...initialized,
1182
+ readiness: readinessReport,
1183
+ preflight: preflightReport,
1184
+ normalized_digests: {
1185
+ contract_digest: contract.contract_digest,
1186
+ verification_profile_digest: profile.verification_profile_digest,
1187
+ },
1188
+ evidence_digest: null,
1189
+ };
1190
+ } finally {
1191
+ rmSync(temporary, { recursive: true, force: true });
1192
+ }
1193
+ }
1194
+
1195
+ /** @param {Record<string,string>} options @param {Set<string>} flags */
1196
+ function initialize(options, flags) {
1197
+ const checked = inspectInputs(options, flags);
1198
+ throwIssues('Verification input preflight', checked.report.errors);
1199
+ const { contract, profile, artifact } = checked;
1200
+ const workdir = realpathSync(required(options, 'workdir'));
1201
+ const plannedAssurance = required(options, 'isolation-assurance');
1202
+ if (!['host_reported', 'user_relayed'].includes(plannedAssurance)) throw new ValidationError('isolation-assurance 非法');
1203
+ const acceptanceIds = validateContract(contract);
1204
+ validateProfile(profile, acceptanceIds);
1205
+ validateArtifact(artifact);
1206
+ const contentDigest = skillContentDigest();
1207
+ validateSkillBinding(contract, contentDigest);
1208
+ const gitIdentity = verifyGitArtifact(artifact, workdir);
1209
+ verifyProtectedPaths(artifact, profile, workdir);
1210
+ const stateRoot = resolve(options['state-root'] ?? join(tmpdir(), 'verify-agent-output-state'));
1211
+ if (pathInside(stateRoot, workdir) && !flags.has('allow-repository-state')) throw new ValidationError('state root 位于仓库内;需显式 --allow-repository-state');
1212
+ mkdirSync(join(stateRoot, 'runs'), { recursive: true, mode: 0o700 });
1213
+ const runId = options['run-id'] ?? randomUUID();
1214
+ if (!/^[A-Za-z0-9._-]+$/u.test(runId)) throw new ValidationError('run-id 只允许字母、数字、点、下划线和连字符');
1215
+ const runDir = join(stateRoot, 'runs', runId);
1216
+ mkdirSync(runDir, { recursive: false, mode: 0o700 });
1217
+ const now = new Date().toISOString();
1218
+ const snapshot = {
1219
+ schema_version: 1,
1220
+ runtime_version: RUNTIME_VERSION,
1221
+ run_id: runId,
1222
+ revision: 0,
1223
+ status: 'initialized',
1224
+ contract_digest: contract.contract_digest,
1225
+ verification_profile_digest: profile.verification_profile_digest,
1226
+ artifact_ref: artifact,
1227
+ runtime_repository_identity: gitIdentity.runtime_repository_identity,
1228
+ executable_identities: Object.fromEntries(Object.entries(profile.runtime.executable_paths).map(([name, path]) => [name, executableIdentity(path)])),
1229
+ argv_file_identities: freezeArgvFiles(profile, workdir),
1230
+ workdir,
1231
+ network_isolation_assurance: flags.has('network-isolated') ? 'host_reported' : 'not_required',
1232
+ planned_isolation_assurance: plannedAssurance,
1233
+ review_challenge_nonce: randomUUID(),
1234
+ skill_provenance: { name: 'verify-agent-output', version: RUNTIME_VERSION, content_digest: contentDigest },
1235
+ stages: {},
1236
+ review_result: null,
1237
+ review_provenance: null,
1238
+ reflection_refs: [],
1239
+ improvement_proposal_refs: [],
1240
+ terminal: null,
1241
+ operational_abort: null,
1242
+ created_at: now,
1243
+ updated_at: now,
1244
+ };
1245
+ writeNewJson(join(runDir, 'contract.json'), contract);
1246
+ writeNewJson(join(runDir, 'profile.json'), profile);
1247
+ writeNewJson(join(runDir, 'artifact.json'), artifact);
1248
+ const initialEvent = { schema_version: 1, revision: 0, kind: 'initialized', recorded_at: now, previous_event_digest: null, snapshot };
1249
+ initialEvent.event_digest = envelopeDigest(initialEvent, 'event_digest');
1250
+ writeFileSync(join(runDir, 'events.ndjson'), `${canonicalJson(initialEvent)}\n`, { flag: 'wx', mode: 0o600 });
1251
+ atomicWriteJson(join(runDir, 'snapshot.json'), snapshot);
1252
+ return { run_id: runId, run_dir: runDir, revision: 0, status: snapshot.status, review_challenge_nonce: snapshot.review_challenge_nonce };
1253
+ }
1254
+
1255
+ /** @param {Record<string,string>} options */
1256
+ function recordReflection(options) {
1257
+ const runDir = realpathSync(required(options, 'run'));
1258
+ const input = readJson(required(options, 'input'));
1259
+ return mutateRun(runDir, expectedRevision(options), (snapshot) => {
1260
+ const record = buildReflection({
1261
+ input,
1262
+ runDir,
1263
+ scope: { contract_digest: snapshot.contract_digest, run_id: snapshot.run_id },
1264
+ skill: snapshot.skill_provenance,
1265
+ parseJsonStrict,
1266
+ canonicalJson,
1267
+ envelopeDigest,
1268
+ });
1269
+ mkdirSync(join(runDir, 'reflections'), { recursive: true, mode: 0o700 });
1270
+ const ref = `reflections/${record.reflection_id}.json`;
1271
+ writeNewJson(join(runDir, ref), record);
1272
+ return { snapshot: { ...snapshot, reflection_refs: [...(snapshot.reflection_refs ?? []), { reflection_id: record.reflection_id, reflection_digest: record.reflection_digest, ref }] }, kind: 'reflection_recorded' };
1273
+ });
1274
+ }
1275
+
1276
+ /** @param {Record<string,string>} options */
1277
+ function proposeImprovement(options) {
1278
+ const runDir = realpathSync(required(options, 'run'));
1279
+ const input = readJson(required(options, 'input'));
1280
+ const reflectionPath = resolve(runDir, required(options, 'reflection'));
1281
+ if (!pathInside(reflectionPath, runDir)) throw new ValidationError('Reflection 路径越出 run 目录');
1282
+ return mutateRun(runDir, expectedRevision(options), (snapshot) => {
1283
+ const reflection = readAndValidateReflection(reflectionPath, 'verify-agent-output', parseJsonStrict, envelopeDigest);
1284
+ if (!(snapshot.reflection_refs ?? []).some((item) => item.reflection_digest === reflection.reflection_digest)) throw new ValidationError('Reflection 未登记到当前 run');
1285
+ const proposal = buildProposal({ input, reflections: [reflection], skill: snapshot.skill_provenance, envelopeDigest });
1286
+ mkdirSync(join(runDir, 'proposals'), { recursive: true, mode: 0o700 });
1287
+ const ref = `proposals/${proposal.proposal_id}.json`;
1288
+ writeNewJson(join(runDir, ref), proposal);
1289
+ return { snapshot: { ...snapshot, improvement_proposal_refs: [...(snapshot.improvement_proposal_refs ?? []), { proposal_id: proposal.proposal_id, proposal_digest: proposal.proposal_digest, ref }] }, kind: 'improvement_proposed' };
1290
+ });
1291
+ }
1292
+
1293
+ /**
1294
+ * run 内的内联 readiness:只覆盖尚未被冻结身份门禁接管的环境前提。
1295
+ * executable / argv 文件的漂移仍由 verifyExecutable / verifyArgvFiles 判 check_runtime_failure,
1296
+ * 这里排除它们,避免同一现象出现两种 abort 语义。
1297
+ */
1298
+ const INLINE_READINESS_EXCLUDED = /^(?:executable|argv_file):/u;
1299
+
1300
+ /** @param {Record<string, any>} snapshot @param {string} runDir */
1301
+ function assertRunReadiness(snapshot, runDir) {
1302
+ const report = evaluateReadiness({
1303
+ contract: readJson(join(runDir, 'contract.json')),
1304
+ profile: readJson(join(runDir, 'profile.json')),
1305
+ workdir: snapshot.workdir,
1306
+ stateRoot: dirname(dirname(runDir)),
1307
+ exclude: INLINE_READINESS_EXCLUDED,
1308
+ });
1309
+ if (report.ready) return report;
1310
+ throw new OperationalAbort('stale_precondition', `readiness 前置检查未通过(环境问题,不是 Artifact 缺陷):${report.blockers.map((item) => `${item.check_id}: ${item.detail}`).join('; ')}`);
1311
+ }
1312
+
1313
+ /** @param {Record<string,string>} options */
1314
+ function runSmoke(options) {
1315
+ const runDir = realpathSync(required(options, 'run'));
1316
+ return mutateRun(runDir, expectedRevision(options), (snapshot) => {
1317
+ if (snapshot.status !== 'initialized') throw new ValidationError(`run-smoke 不接受状态 ${snapshot.status}`);
1318
+ assertRunReadiness(snapshot, runDir);
1319
+ const result = executeChecks(snapshot, 'smoke', runDir);
1320
+ let next = { ...snapshot, stages: { ...snapshot.stages, smoke_l0: result }, status: result.passed ? 'smoke_passed' : 'terminal' };
1321
+ if (!result.passed) next = terminalSnapshot(next, runDir, 'fail');
1322
+ return { snapshot: next, kind: result.passed ? 'smoke_passed' : 'smoke_failed' };
1323
+ });
1324
+ }
1325
+
1326
+ /** @param {Record<string,string>} options */
1327
+ function reviewInput(options) {
1328
+ const runDir = realpathSync(required(options, 'run'));
1329
+ const { snapshot } = loadSnapshot(runDir);
1330
+ if (snapshot.status !== 'smoke_passed') throw new ValidationError(`review-input 不接受状态 ${snapshot.status}`);
1331
+ verifyGitArtifact(snapshot.artifact_ref, snapshot.workdir, snapshot.runtime_repository_identity);
1332
+ const contract = readJson(join(runDir, 'contract.json'));
1333
+ const profile = readJson(join(runDir, 'profile.json'));
1334
+ const byId = new Map(contract.acceptance.map((item) => [item.contract_item_id, item]));
1335
+ return {
1336
+ schema_version: 1,
1337
+ run_id: snapshot.run_id,
1338
+ contract_digest: snapshot.contract_digest,
1339
+ verification_profile_digest: snapshot.verification_profile_digest,
1340
+ contract: { contract_id: contract.contract_id, objective: contract.objective, scope: contract.scope, acceptance: contract.acceptance, permissions: contract.permissions, contract_digest: contract.contract_digest },
1341
+ artifact_ref: snapshot.artifact_ref,
1342
+ verification_entry: {
1343
+ l0_checks: profile.l0_checks.map(({ check_id, argv, cwd_rel, stage, timeout_ms, expected_exit_codes }) => ({ check_id, argv, cwd_rel, stage, timeout_ms, expected_exit_codes })),
1344
+ protected_verifier_paths: profile.protected_verifier_paths,
1345
+ allowed_validation_changes: profile.allowed_validation_changes,
1346
+ },
1347
+ reviewer_view: profile.l1_review.map((item) => ({ ...item, requirement: byId.get(item.contract_item_id).requirement })),
1348
+ required_output: 'Review Result v1',
1349
+ challenge_nonce: snapshot.review_challenge_nonce,
1350
+ };
1351
+ }
1352
+
1353
+ /** 从协议真源取「证伪任务」原文,避免 bundle 里的提示词与 references 漂移。 */
1354
+ function falsificationTask() {
1355
+ const text = readFileSync(join(PACKAGE_ROOT, 'docs', 'verify', 'verification-protocol.md'), 'utf8');
1356
+ const section = text.split(/^## /mu).find((part) => part.startsWith('证伪任务'));
1357
+ if (!section) throw new ValidationError('verification-protocol.md 缺少「证伪任务」段');
1358
+ const fenced = section.match(/```text\n([\s\S]*?)\n```/u);
1359
+ if (!fenced) throw new ValidationError('「证伪任务」段缺少提示词代码块');
1360
+ return fenced[1];
1361
+ }
1362
+
1363
+ /** @param {{workdir:string, contractKind:string, digestCommand:string}} input */
1364
+ function reviewerPrompt({ workdir, contractKind, digestCommand }) {
1365
+ return [
1366
+ '你是独立验收者。本 bundle 自带全部验收输入;不要索取或采信实现者叙事、过程对话与“已经测试通过”一类自述。',
1367
+ '',
1368
+ '## 证伪任务',
1369
+ '',
1370
+ falsificationTask(),
1371
+ '',
1372
+ '## 输出契约(Review Result v1)',
1373
+ `- verdict 只能是三态之一:${[...REVIEW_VERDICTS].join(' | ')}。`,
1374
+ `- findings[] 每条必须包含五个非空字符串字段:${REVIEW_FINDING_FIELDS.join(' / ')};class 只能取 ${[...FINDING_CLASSES].join(' | ')}。`,
1375
+ '- forensics 必须是非空字符串数组,逐条记录你实际执行过的取证动作。',
1376
+ '- no_defect_found:findings 必须为空且 forensics 必须非空;它只表示在取证范围内未发现缺陷。',
1377
+ '- fail 与 undecidable:findings 至少一条,contract_item_id 只能引用 review_input.reviewer_view 中已冻结的 ID。',
1378
+ '- 任何 safety finding 都使最终 outcome 至少为 blocked_safety,不能被其他通过项抵消。',
1379
+ '- contract_digest、verification_profile_digest、artifact_ref、challenge_nonce 必须从 review_input 原样复制,不得改写或事后补写。',
1380
+ '- 完整结构以本 bundle 的 review_result_schema 为准;不要填写 L0 exit code,不生成 Evidence,不宣布任务完成。',
1381
+ '',
1382
+ '## 权限',
1383
+ `- 只读审查 ${workdir}:禁止 commit、checkout、切分支、reset、stash,禁止修改业务产物、Task Contract、Verification Profile 或任何验证定义。`,
1384
+ '',
1385
+ '## 停止条件',
1386
+ '- 产出一份三态 verdict 的 Review Result v1 后立即停止:不修复缺陷、不重跑 L0、不自行发起下一轮验收。',
1387
+ '',
1388
+ '## digest 回填',
1389
+ `- 填好除 review_result_digest 外的全部字段后运行:${digestCommand}`,
1390
+ '- 以该命令输出的新 JSON 作为最终 Review Result;不要手写 review_result_digest。',
1391
+ '',
1392
+ '## 合同种类',
1393
+ contractKind === 'projected'
1394
+ ? '- 本次是投影合同:只覆盖公共合同 acceptance 的一个子集。只对 review_input.contract.acceptance 列出的条目取证,不越界评判未投影条目。'
1395
+ : '- 本次是公共合同:review_input.contract.acceptance 即完整验收面。',
1396
+ ].join('\n');
1397
+ }
1398
+
1399
+ /** 打包一次可直接投递给任意 reviewer 的自包含验收输入。 @param {Record<string,string>} options */
1400
+ function reviewBundle(options) {
1401
+ const runDir = realpathSync(required(options, 'run'));
1402
+ const input = reviewInput({ run: runDir });
1403
+ const { snapshot } = loadSnapshot(runDir);
1404
+ const contract = readJson(join(runDir, 'contract.json'));
1405
+ const projection = contract.extensions && typeof contract.extensions === 'object' && !Array.isArray(contract.extensions) ? contract.extensions.projection : undefined;
1406
+ const contractKind = projection && typeof projection === 'object' ? 'projected' : 'public';
1407
+ const digestCommand = `node ${RUNTIME_SCRIPT_PATH} digest --kind review --input <review-result.json>`;
1408
+ const bundle = {
1409
+ schema_version: 1,
1410
+ bundle_kind: 'reviewer_dispatch',
1411
+ runtime_version: RUNTIME_VERSION,
1412
+ run_id: snapshot.run_id,
1413
+ contract_kind: contractKind,
1414
+ reviewer_prompt: reviewerPrompt({ workdir: snapshot.workdir, contractKind, digestCommand }),
1415
+ review_input: input,
1416
+ review_result_schema: schema('review-result-v1.schema.json'),
1417
+ artifact_ref: snapshot.artifact_ref,
1418
+ workdir: snapshot.workdir,
1419
+ permissions: {
1420
+ mode: 'read_only',
1421
+ writable_paths: [],
1422
+ forbidden_operations: ['git commit', 'git checkout', 'git switch', 'git reset', 'git stash', '修改业务产物', '修改 Task Contract / Verification Profile / 验证定义'],
1423
+ },
1424
+ stop_conditions: [
1425
+ '产出 fail / no_defect_found / undecidable 三态之一的 Review Result v1 后立即停止',
1426
+ '不修复缺陷、不重跑 L0、不自行发起下一轮验收',
1427
+ ],
1428
+ digest_backfill: {
1429
+ command: digestCommand,
1430
+ note: 'digest 输出的新 JSON 才是提交给 record-review 的最终 Review Result;不要手写 review_result_digest。',
1431
+ },
1432
+ controller_next_step: {
1433
+ note: '以下命令由 controller 执行,不属于 reviewer 权限。',
1434
+ command: `node ${RUNTIME_SCRIPT_PATH} record-review --run ${runDir} --review <review-result.json> --verifier-run-id <opaque-id> --isolation-assurance ${snapshot.planned_isolation_assurance}`,
1435
+ },
1436
+ };
1437
+ if (!options.out) return bundle;
1438
+ const outPath = resolve(options.out);
1439
+ const text = `${JSON.stringify(bundle, null, 2)}\n`;
1440
+ atomicWriteText(outPath, text);
1441
+ return { out: outPath, bytes: Buffer.byteLength(text, 'utf8'), run_id: bundle.run_id, contract_kind: contractKind };
1442
+ }
1443
+
1444
+ /** @param {Record<string,string>} options @param {Set<string>} flags */
1445
+ function reviewPayload(options, flags) {
1446
+ const fromStdin = flags.has('stdin');
1447
+ if (fromStdin && options.review) throw new ValidationError('record-review 的 --review 与 --stdin 互斥');
1448
+ if (!fromStdin) return readJson(required(options, 'review'));
1449
+ if (process.stdin.isTTY === true) throw new ValidationError('--stdin 拒绝从交互式 TTY 等待输入;请使用 pipe 或 --review <json>');
1450
+ const text = readFileSync(0, 'utf8');
1451
+ if (!text.trim()) throw new ValidationError('--stdin 未收到 Review Result JSON');
1452
+ if (Buffer.byteLength(text, 'utf8') > REVIEW_STDIN_MAX_BYTES) throw new ValidationError(`--stdin 超过 ${REVIEW_STDIN_MAX_BYTES} bytes 上限`);
1453
+ const parsed = parseJsonStrict(text);
1454
+ return parsed.review_result_digest ? parsed : addDigest(parsed, 'review_result_digest');
1455
+ }
1456
+
1457
+ /** @param {Record<string,string>} options @param {Set<string>} flags */
1458
+ function recordReview(options, flags) {
1459
+ const runDir = realpathSync(required(options, 'run'));
1460
+ const review = reviewPayload(options, flags);
1461
+ const verifierRunId = required(options, 'verifier-run-id');
1462
+ const assurance = required(options, 'isolation-assurance');
1463
+ if (!['host_reported', 'user_relayed'].includes(assurance)) throw new ValidationError('isolation-assurance 非法');
1464
+ return mutateRun(runDir, expectedRevision(options), (snapshot) => {
1465
+ if (snapshot.status !== 'smoke_passed') throw new ValidationError(`record-review 不接受状态 ${snapshot.status}`);
1466
+ if (verifierRunId === snapshot.run_id) throw new ValidationError('verifier-run-id 必须与被验收 run 隔离');
1467
+ if (assurance !== snapshot.planned_isolation_assurance) throw new ValidationError('isolation-assurance 与 init 冻结值不一致');
1468
+ verifyGitArtifact(snapshot.artifact_ref, snapshot.workdir, snapshot.runtime_repository_identity);
1469
+ const contract = readJson(join(runDir, 'contract.json'));
1470
+ const acceptanceIds = validateContract(contract);
1471
+ validateReview(review, snapshot, acceptanceIds);
1472
+ writeNewJson(join(runDir, 'review-result.json'), review);
1473
+ let next = { ...snapshot, status: 'review_recorded', review_result: review, review_provenance: { verifier_run_id: verifierRunId, isolation_assurance: assurance, challenge_nonce: snapshot.review_challenge_nonce } };
1474
+ const safety = review.findings.some((finding) => finding.class === 'safety');
1475
+ if (safety) next = terminalSnapshot(next, runDir, 'blocked_safety');
1476
+ else if (review.verdict === 'fail') next = terminalSnapshot(next, runDir, 'fail');
1477
+ else if (review.verdict === 'undecidable') next = terminalSnapshot(next, runDir, 'undecidable');
1478
+ return { snapshot: next, kind: next.status === 'terminal' ? `review_${next.terminal.outcome}` : 'review_recorded' };
1479
+ });
1480
+ }
1481
+
1482
+ /** @param {Record<string,string>} options */
1483
+ function runFinal(options) {
1484
+ const runDir = realpathSync(required(options, 'run'));
1485
+ return mutateRun(runDir, expectedRevision(options), (snapshot) => {
1486
+ if (snapshot.status !== 'review_recorded' || snapshot.review_result?.verdict !== 'no_defect_found') throw new ValidationError(`run-final 不接受状态 ${snapshot.status}`);
1487
+ const result = executeChecks(snapshot, 'final', runDir);
1488
+ let next = { ...snapshot, stages: { ...snapshot.stages, final_l0: result } };
1489
+ next = terminalSnapshot(next, runDir, result.passed ? 'pass' : 'fail');
1490
+ return { snapshot: next, kind: result.passed ? 'final_passed' : 'final_failed' };
1491
+ });
1492
+ }
1493
+
1494
+ /** @param {Record<string,string>} options */
1495
+ function status(options) {
1496
+ const runDir = realpathSync(required(options, 'run'));
1497
+ const loaded = loadSnapshot(runDir);
1498
+ return { ...loaded.snapshot, recovery_needed: loaded.needsRepair };
1499
+ }
1500
+
1501
+ /** @param {Record<string,string>} options */
1502
+ function validateRun(options) {
1503
+ const runDir = realpathSync(required(options, 'run'));
1504
+ const loaded = loadSnapshot(runDir);
1505
+ const snapshot = loaded.snapshot;
1506
+ const contract = readJson(join(runDir, 'contract.json'));
1507
+ const profile = readJson(join(runDir, 'profile.json'));
1508
+ const artifact = readJson(join(runDir, 'artifact.json'));
1509
+ const ids = validateContract(contract);
1510
+ validateProfile(profile, ids);
1511
+ validateArtifact(artifact);
1512
+ if (contract.contract_digest !== snapshot.contract_digest || profile.verification_profile_digest !== snapshot.verification_profile_digest || canonicalJson(artifact) !== canonicalJson(snapshot.artifact_ref)) throw new ValidationError('冻结 envelope 与 snapshot 不一致');
1513
+ const logProblems = [];
1514
+ for (const stage of Object.values(snapshot.stages)) {
1515
+ for (const check of stage?.checks ?? []) {
1516
+ const path = join(runDir, check.log_ref);
1517
+ if (!existsSync(path) || sha256(readFileSync(path)) !== check.log_digest) logProblems.push(check.check_id);
1518
+ }
1519
+ }
1520
+ let evidence = null;
1521
+ if (snapshot.terminal?.evidence_ref) {
1522
+ evidence = readJson(join(runDir, snapshot.terminal.evidence_ref));
1523
+ if (envelopeDigest(evidence, 'evidence_digest') !== evidence.evidence_digest || evidence.evidence_digest !== snapshot.terminal.evidence_digest) throw new ValidationError('Evidence digest 无效');
1524
+ if (evidence.run_id !== snapshot.run_id || evidence.terminal_outcome !== snapshot.terminal.outcome || evidence.contract_digest !== snapshot.contract_digest || evidence.verification_profile_digest !== snapshot.verification_profile_digest || canonicalJson(evidence.artifact_ref) !== canonicalJson(snapshot.artifact_ref) || canonicalJson(evidence.stages) !== canonicalJson({ smoke_l0: snapshot.stages.smoke_l0 ?? { status: 'not_run' }, l1_review: snapshot.review_result ?? { status: 'not_run' }, final_l0: snapshot.stages.final_l0 ?? { status: 'not_run' } })) throw new ValidationError('Evidence 与 terminal snapshot 不一致');
1525
+ }
1526
+ for (const ref of snapshot.reflection_refs ?? []) {
1527
+ const value = readAndValidateReflection(join(runDir, ref.ref), 'verify-agent-output', parseJsonStrict, envelopeDigest);
1528
+ if (value.reflection_digest !== ref.reflection_digest) throw new ValidationError(`Reflection digest 无效: ${ref.ref}`);
1529
+ verifyEvidenceRefs(runDir, value.evidence_refs, parseJsonStrict, canonicalJson);
1530
+ }
1531
+ for (const ref of snapshot.improvement_proposal_refs ?? []) {
1532
+ const value = readJson(join(runDir, ref.ref));
1533
+ if (value.lifecycle !== 'proposed' || value.target_skill?.name !== 'verify-agent-output' || envelopeDigest(value, 'proposal_digest') !== value.proposal_digest || value.proposal_digest !== ref.proposal_digest) throw new ValidationError(`Proposal digest/lifecycle 无效: ${ref.ref}`);
1534
+ }
1535
+ if (logProblems.length > 0) throw new ValidationError(`日志摘要无效: ${logProblems.join(', ')}`);
1536
+ return { valid: true, run_id: snapshot.run_id, revision: snapshot.revision, status: snapshot.status, evidence_digest: evidence?.evidence_digest ?? null, recovery_needed: loaded.needsRepair };
1537
+ }
1538
+
1539
+ /** @param {Record<string,string>} options */
1540
+ function doctor(options) {
1541
+ const runDir = realpathSync(required(options, 'run'));
1542
+ const loaded = loadSnapshot(runDir);
1543
+ const currentDigest = skillContentDigest();
1544
+ return {
1545
+ healthy: !loaded.needsRepair && !existsSync(join(runDir, '.lock')) && currentDigest === loaded.snapshot.skill_provenance.content_digest,
1546
+ run_id: loaded.snapshot.run_id,
1547
+ revision: loaded.snapshot.revision,
1548
+ snapshot_matches_journal: !loaded.needsRepair,
1549
+ lock_present: existsSync(join(runDir, '.lock')),
1550
+ skill_drift: currentDigest !== loaded.snapshot.skill_provenance.content_digest,
1551
+ // 旧 run 即使已漂移也要能只读检查:同时给出冻结时的摘要与当前摘要,便于判断漂移了什么。
1552
+ frozen_content_digest: loaded.snapshot.skill_provenance.content_digest,
1553
+ current_content_digest: currentDigest,
1554
+ };
1555
+ }
1556
+
1557
+ /** @param {string[]} argv */
1558
+ export function main(argv = process.argv.slice(2)) {
1559
+ if (['--help', '-h', 'help'].includes(argv[0] ?? '')) return { __help: helpText(argv[0] === 'help' ? argv[1] ?? null : null) };
1560
+ if (['--help', '-h'].includes(argv[1] ?? '')) return { __help: helpText(argv[0] ?? null) };
1561
+ const { command, options, flags } = parseCli(argv);
1562
+ switch (command) {
1563
+ case 'capabilities': return capabilities();
1564
+ case 'scaffold': return scaffold(options);
1565
+ case 'prepare': return prepare(options);
1566
+ case 'digest': return digestEnvelope(options);
1567
+ case 'readiness': return readiness(options);
1568
+ case 'preflight': return preflight(options, flags);
1569
+ case 'init': return initialize(options, flags);
1570
+ case 'prepare-run': return prepareRun(options, flags);
1571
+ case 'run-smoke': return runSmoke(options);
1572
+ case 'review-input': return reviewInput(options);
1573
+ case 'review-bundle': return reviewBundle(options);
1574
+ case 'record-review': return recordReview(options, flags);
1575
+ case 'run-final': return runFinal(options);
1576
+ case 'record-reflection': return recordReflection(options);
1577
+ case 'propose-improvement': return proposeImprovement(options);
1578
+ case 'status':
1579
+ case 'inspect': return status(options);
1580
+ case 'validate': return validateRun(options);
1581
+ case 'doctor': return doctor(options);
1582
+ default: throw new ValidationError(`命令必须是 ${CLI_COMMANDS}`);
1583
+ }
1584
+ }
1585
+
1586
+ /** @param {Record<string,any>} result */
1587
+ function compactRunResult(result) {
1588
+ const failedChecks = [];
1589
+ for (const stage of Object.values(result?.stages ?? {})) {
1590
+ for (const check of stage?.checks ?? []) if (check.passed === false) failedChecks.push(check.check_id);
1591
+ }
1592
+ for (const finding of result?.review_result?.findings ?? []) failedChecks.push(`l1:${finding.contract_item_id}`);
1593
+ const outcome = result?.terminal?.outcome ?? result?.status ?? (result?.prepared === false ? 'blocked' : null);
1594
+ return {
1595
+ run_id: result?.run_id ?? null,
1596
+ revision: result?.revision ?? null,
1597
+ status: outcome,
1598
+ failed_checks: [...new Set(failedChecks)],
1599
+ evidence_digest: result?.terminal?.evidence_digest ?? result?.evidence_digest ?? null,
1600
+ ...(result?.run_dir ? { run_dir: result.run_dir } : {}),
1601
+ ...(result?.prepared !== undefined ? { prepared: result.prepared } : {}),
1602
+ ...(result?.terminal && result.terminal.outcome !== 'pass' ? {
1603
+ next_mode_hint: '本次单 Artifact 验收已终止并保留 Evidence;若已授权修复且预期多轮,请用 run-agent-verify-loop 创建新 Artifact/run。',
1604
+ } : {}),
1605
+ };
1606
+ }
1607
+
1608
+ /** @param {string} metaUrl @param {string} argv1 */
1609
+ export function isCliEntry(metaUrl, argv1) {
1610
+ if (!argv1) return false;
1611
+ try {
1612
+ return realpathSync(fileURLToPath(metaUrl)) === realpathSync(argv1);
1613
+ } catch {
1614
+ return pathToFileURL(resolve(argv1)).href === metaUrl;
1615
+ }
1616
+ }
1617
+
1618
+ export function runCli(argv = process.argv.slice(2)) {
1619
+ try {
1620
+ const result = main(argv);
1621
+ const command = argv[0];
1622
+ if (result?.__help) process.stdout.write(result.__help);
1623
+ else {
1624
+ const compact = ['prepare-run', 'run-smoke', 'record-review', 'run-final'].includes(command) && !argv.includes('--verbose');
1625
+ process.stdout.write(`${JSON.stringify(compact ? compactRunResult(result) : result, null, 2)}\n`);
1626
+ }
1627
+ if (command === 'preflight' && result?.valid === false) return 2;
1628
+ if (command === 'readiness' && result?.ready === false) return 2;
1629
+ if (command === 'prepare-run' && result?.prepared === false) return 2;
1630
+ return 0;
1631
+ } catch (error) {
1632
+ const code = error instanceof OperationalAbort ? error.code : error instanceof ValidationError ? 'invalid_input' : 'runtime_error';
1633
+ process.stderr.write(`${JSON.stringify({ error: code, message: error instanceof Error ? error.message : String(error) })}\n`);
1634
+ return error instanceof ValidationError ? 2 : 3;
1635
+ }
1636
+ }
1637
+
1638
+ if (isCliEntry(import.meta.url, process.argv[1])) process.exitCode = runCli();