@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.
- package/CHANGELOG.md +12 -0
- package/LICENSE +21 -0
- package/README.en.md +107 -0
- package/README.md +103 -0
- package/bin/agentkit.mjs +4 -0
- package/bin/cli.mjs +273 -0
- package/core/atomic-fs.mjs +23 -0
- package/core/cli-help.mjs +54 -0
- package/core/content-digest.mjs +66 -0
- package/core/digest.mjs +67 -0
- package/core/json-schema-lite.mjs +60 -0
- package/core/legacy-entry.mjs +37 -0
- package/core/reflection.mjs +142 -0
- package/core/runtime-bundle.mjs +101 -0
- package/docs/loop/embedded-review-adapter.md +41 -0
- package/docs/loop/loop-state-machine.md +43 -0
- package/docs/loop/recovery-and-fuses.md +34 -0
- package/docs/orchestrate/dispatch-contract.md +92 -0
- package/docs/orchestrate/failure-routing-and-recovery.md +46 -0
- package/docs/orchestrate/host-capability-cache.md +170 -0
- package/docs/orchestrate/isolation-fallback.md +18 -0
- package/docs/orchestrate/model-routing-config.md +186 -0
- package/docs/orchestrate/orchestration-runtime.md +261 -0
- package/docs/orchestrate/review-budget.md +90 -0
- package/docs/orchestrate/task-playbooks.md +85 -0
- package/docs/orchestrate/user-facing-reporting.md +14 -0
- package/docs/verify/evidence-schema.md +167 -0
- package/docs/verify/input-preparation.md +44 -0
- package/docs/verify/verification-protocol.md +76 -0
- package/docs/worktree/batch-integration.md +176 -0
- package/docs/worktree/delivery-identity.md +41 -0
- package/docs/worktree/profile.md +107 -0
- package/docs/worktree/reclaim-and-watch.md +96 -0
- package/docs/worktree/review-lifecycle.md +92 -0
- package/docs/worktree/spawn-and-stack.md +74 -0
- package/domains/loop/loop-runtime.mjs +1056 -0
- package/domains/orchestrate/contract-tool.mjs +169 -0
- package/domains/orchestrate/host_capability_cache.mjs +437 -0
- package/domains/orchestrate/orchestration-ledger.mjs +332 -0
- package/domains/orchestrate/orchestration-metadata.mjs +4 -0
- package/domains/orchestrate/orchestration-reflection.mjs +119 -0
- package/domains/orchestrate/resolve_model_policy.mjs +311 -0
- package/domains/orchestrate/review-budget.mjs +162 -0
- package/domains/orchestrate/worker-capability-preflight.mjs +227 -0
- package/domains/verify/verification-runtime.mjs +1638 -0
- package/domains/worktree/worktree-archive.mjs +135 -0
- package/domains/worktree/worktree-artifact.mjs +123 -0
- package/domains/worktree/worktree-batch-integrate.mjs +713 -0
- package/domains/worktree/worktree-batch-plan.mjs +198 -0
- package/domains/worktree/worktree-batch-result.mjs +241 -0
- package/domains/worktree/worktree-core.mjs +908 -0
- package/domains/worktree/worktree-doctor.mjs +493 -0
- package/domains/worktree/worktree-history.mjs +377 -0
- package/domains/worktree/worktree-learning.mjs +110 -0
- package/domains/worktree/worktree-lifecycle.mjs +786 -0
- package/domains/worktree/worktree-merge-preview.mjs +409 -0
- package/domains/worktree/worktree-mgr.mjs +261 -0
- package/domains/worktree/worktree-process.mjs +55 -0
- package/domains/worktree/worktree-profile.mjs +800 -0
- package/domains/worktree/worktree-provider-gitlab.mjs +59 -0
- package/domains/worktree/worktree-reclaim.mjs +683 -0
- package/domains/worktree/worktree-review-refresh.mjs +574 -0
- package/domains/worktree/worktree-review-watch.mjs +661 -0
- package/domains/worktree/worktree-scan.mjs +510 -0
- package/domains/worktree/worktree-trace-test-worker.mjs +23 -0
- package/domains/worktree/worktree-trace.mjs +478 -0
- package/manage-worktrees/SKILL.md +87 -0
- package/manage-worktrees/agents/openai.yaml +4 -0
- package/manage-worktrees/scripts/worktree-mgr.mjs +10 -0
- package/manage-worktrees/scripts/worktree-scan.mjs +10 -0
- package/orchestrate-subagents/SKILL.md +173 -0
- package/orchestrate-subagents/agents/openai.yaml +4 -0
- package/orchestrate-subagents/scripts/contract-tool.mjs +10 -0
- package/orchestrate-subagents/scripts/host_capability_cache.mjs +10 -0
- package/orchestrate-subagents/scripts/orchestration-ledger.mjs +10 -0
- package/orchestrate-subagents/scripts/orchestration-reflection.mjs +10 -0
- package/orchestrate-subagents/scripts/resolve_model_policy.mjs +10 -0
- package/orchestrate-subagents/scripts/review-budget.mjs +10 -0
- package/orchestrate-subagents/scripts/worker-capability-preflight.mjs +10 -0
- package/package.json +48 -0
- package/run-agent-verify-loop/SKILL.md +127 -0
- package/run-agent-verify-loop/agents/openai.yaml +4 -0
- package/run-agent-verify-loop/scripts/loop-runtime.mjs +10 -0
- package/schemas/artifact-ref-v1.schema.json +23 -0
- package/schemas/batch-result-v1.schema.json +138 -0
- package/schemas/controller-recheck-record-v1.schema.json +22 -0
- package/schemas/convergence-report-v1.schema.json +9 -0
- package/schemas/effective-worker-capability-v1.schema.json +36 -0
- package/schemas/embedded-verification-record-v1.schema.json +32 -0
- package/schemas/evidence-package-v1.schema.json +41 -0
- package/schemas/improvement-proposal-v1.schema.json +18 -0
- package/schemas/loop-state-v1.schema.json +34 -0
- package/schemas/model-policy-resolution-v1.schema.json +41 -0
- package/schemas/orchestration-ledger-v1.schema.json +110 -0
- package/schemas/reflection-record-v1.schema.json +24 -0
- package/schemas/review-result-v1.schema.json +37 -0
- package/schemas/task-contract-v1.schema.json +83 -0
- package/schemas/verification-profile-v1.schema.json +60 -0
- package/schemas/worker-capability-requirements-v1.schema.json +21 -0
- package/schemas/worktree-binding-v1.schema.json +14 -0
- package/shell-manifest.json +79 -0
- package/verify-agent-output/SKILL.md +119 -0
- package/verify-agent-output/agents/openai.yaml +4 -0
- package/verify-agent-output/scripts/verification-runtime.mjs +10 -0
|
@@ -0,0 +1,1056 @@
|
|
|
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
|
+
appendFileSync,
|
|
8
|
+
closeSync,
|
|
9
|
+
existsSync,
|
|
10
|
+
fsyncSync,
|
|
11
|
+
linkSync,
|
|
12
|
+
lstatSync,
|
|
13
|
+
mkdirSync,
|
|
14
|
+
openSync,
|
|
15
|
+
readFileSync,
|
|
16
|
+
readdirSync,
|
|
17
|
+
realpathSync,
|
|
18
|
+
renameSync,
|
|
19
|
+
statSync,
|
|
20
|
+
unlinkSync,
|
|
21
|
+
writeFileSync,
|
|
22
|
+
} from 'node:fs';
|
|
23
|
+
import { tmpdir } from 'node:os';
|
|
24
|
+
import { dirname, isAbsolute, join, relative, resolve, sep } from 'node:path';
|
|
25
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
26
|
+
import { createReflectionKit } from '../../core/reflection.mjs';
|
|
27
|
+
import { validateJsonSchema } from '../../core/json-schema-lite.mjs';
|
|
28
|
+
import { atomicWriteJson, atomicWriteText, writeNewJson } from '../../core/atomic-fs.mjs';
|
|
29
|
+
import { createDigestKit } from '../../core/digest.mjs';
|
|
30
|
+
import { distributionDigest, skillDistributionRoots } from '../../core/content-digest.mjs';
|
|
31
|
+
|
|
32
|
+
export const RUNTIME_VERSION = '1.0.0';
|
|
33
|
+
const SKILL_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..', '..', 'run-agent-verify-loop');
|
|
34
|
+
// 摘要覆盖 Skill 目录 + 共享 core + canonical schemas:执行真正依赖的全部分发内容。
|
|
35
|
+
// PACKAGE_ROOT 是模块常量,传入自定义 root 只替换 Skill 目录那一段,便于测试摘要与安装路径无关。
|
|
36
|
+
const PACKAGE_ROOT = resolve(SKILL_ROOT, '..');
|
|
37
|
+
const DOMAIN_ROOT = dirname(fileURLToPath(import.meta.url));
|
|
38
|
+
export function skillContentDigest(root = SKILL_ROOT) {
|
|
39
|
+
return distributionDigest(skillDistributionRoots({ packageRoot: PACKAGE_ROOT, skillRoot: root, domainRoot: DOMAIN_ROOT, docsRoot: join(PACKAGE_ROOT, 'docs', 'loop') }));
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const schema = (name) => parseJsonStrict(readFileSync(join(SKILL_ROOT, '..', 'schemas', name), 'utf8'));
|
|
43
|
+
const DIGEST_PATTERN = /^sha256:[0-9a-f]{64}$/;
|
|
44
|
+
const FINDING_CLASSES = new Set(['functional', 'scope', 'verification_definition', 'safety']);
|
|
45
|
+
const PROVIDERS = new Set(['verify-agent-output', 'embedded']);
|
|
46
|
+
|
|
47
|
+
export class LoopValidationError extends Error {}
|
|
48
|
+
|
|
49
|
+
// strict=true 保留本 Skill 既有的代理对与非有限 number 校验;错误类仍是本地的 LoopValidationError。
|
|
50
|
+
const digestKit = createDigestKit({ ValidationError: LoopValidationError, strict: true });
|
|
51
|
+
// 严格度保持本 Skill 现状(宽松版),不随 core 合并而收紧。
|
|
52
|
+
const { buildProposal, buildReflection, readAndValidateReflection, verifyEvidenceRefs } = createReflectionKit({ strict: false });
|
|
53
|
+
export const { canonicalJson, envelopeDigest } = digestKit;
|
|
54
|
+
const { sha256, assertValidUnicode: validUnicode } = digestKit;
|
|
55
|
+
export class LoopOperationalAbort extends Error {
|
|
56
|
+
constructor(code, message) {
|
|
57
|
+
super(message);
|
|
58
|
+
this.code = code;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
class StrictParser {
|
|
63
|
+
constructor(text) { this.text = text; this.index = 0; }
|
|
64
|
+
space() { while (/\s/u.test(this.text[this.index] ?? '')) this.index += 1; }
|
|
65
|
+
parse() {
|
|
66
|
+
const value = this.value();
|
|
67
|
+
this.space();
|
|
68
|
+
if (this.index !== this.text.length) throw new LoopValidationError(`JSON 尾部非法,位置 ${this.index}`);
|
|
69
|
+
return value;
|
|
70
|
+
}
|
|
71
|
+
value() {
|
|
72
|
+
this.space();
|
|
73
|
+
const char = this.text[this.index];
|
|
74
|
+
if (char === '{') return this.object();
|
|
75
|
+
if (char === '[') return this.array();
|
|
76
|
+
if (char === '"') return this.string();
|
|
77
|
+
if (char === '-' || /[0-9]/u.test(char ?? '')) return this.number();
|
|
78
|
+
for (const [token, value] of [['true', true], ['false', false], ['null', null]]) {
|
|
79
|
+
if (this.text.startsWith(token, this.index)) { this.index += token.length; return value; }
|
|
80
|
+
}
|
|
81
|
+
throw new LoopValidationError(`JSON 值非法,位置 ${this.index}`);
|
|
82
|
+
}
|
|
83
|
+
object() {
|
|
84
|
+
this.index += 1;
|
|
85
|
+
this.space();
|
|
86
|
+
const value = Object.create(null);
|
|
87
|
+
const keys = new Set();
|
|
88
|
+
if (this.text[this.index] === '}') { this.index += 1; return value; }
|
|
89
|
+
while (true) {
|
|
90
|
+
this.space();
|
|
91
|
+
if (this.text[this.index] !== '"') throw new LoopValidationError(`JSON key 非字符串,位置 ${this.index}`);
|
|
92
|
+
const key = this.string();
|
|
93
|
+
if (keys.has(key)) throw new LoopValidationError(`JSON object 存在重复 key: ${key}`);
|
|
94
|
+
keys.add(key);
|
|
95
|
+
this.space();
|
|
96
|
+
if (this.text[this.index] !== ':') throw new LoopValidationError(`JSON object 缺少冒号,位置 ${this.index}`);
|
|
97
|
+
this.index += 1;
|
|
98
|
+
value[key] = this.value();
|
|
99
|
+
this.space();
|
|
100
|
+
if (this.text[this.index] === '}') { this.index += 1; return value; }
|
|
101
|
+
if (this.text[this.index] !== ',') throw new LoopValidationError(`JSON object 缺少逗号,位置 ${this.index}`);
|
|
102
|
+
this.index += 1;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
array() {
|
|
106
|
+
this.index += 1;
|
|
107
|
+
this.space();
|
|
108
|
+
const value = [];
|
|
109
|
+
if (this.text[this.index] === ']') { this.index += 1; return value; }
|
|
110
|
+
while (true) {
|
|
111
|
+
value.push(this.value());
|
|
112
|
+
this.space();
|
|
113
|
+
if (this.text[this.index] === ']') { this.index += 1; return value; }
|
|
114
|
+
if (this.text[this.index] !== ',') throw new LoopValidationError(`JSON array 缺少逗号,位置 ${this.index}`);
|
|
115
|
+
this.index += 1;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
string() {
|
|
119
|
+
const start = this.index;
|
|
120
|
+
this.index += 1;
|
|
121
|
+
let escaped = false;
|
|
122
|
+
while (this.index < this.text.length) {
|
|
123
|
+
const char = this.text[this.index];
|
|
124
|
+
if (!escaped && char === '"') {
|
|
125
|
+
this.index += 1;
|
|
126
|
+
const value = JSON.parse(this.text.slice(start, this.index));
|
|
127
|
+
validUnicode(value);
|
|
128
|
+
return value;
|
|
129
|
+
}
|
|
130
|
+
if (!escaped && char === '\\') escaped = true;
|
|
131
|
+
else escaped = false;
|
|
132
|
+
this.index += 1;
|
|
133
|
+
}
|
|
134
|
+
throw new LoopValidationError(`JSON string 未闭合,位置 ${start}`);
|
|
135
|
+
}
|
|
136
|
+
number() {
|
|
137
|
+
const match = this.text.slice(this.index).match(/^-?(?:0|[1-9]\d*)(?:\.\d+)?(?:[eE][+-]?\d+)?/u);
|
|
138
|
+
if (!match) throw new LoopValidationError(`JSON number 非法,位置 ${this.index}`);
|
|
139
|
+
this.index += match[0].length;
|
|
140
|
+
const value = Number(match[0]);
|
|
141
|
+
if (!Number.isFinite(value)) throw new LoopValidationError('JSON number 必须有限');
|
|
142
|
+
return value;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export function parseJsonStrict(text) { return new StrictParser(text).parse(); }
|
|
147
|
+
|
|
148
|
+
function readJson(path) { return parseJsonStrict(readFileSync(path, 'utf8')); }
|
|
149
|
+
function processIsAlive(pid) {
|
|
150
|
+
if (!Number.isInteger(pid) || pid <= 0) return false;
|
|
151
|
+
try { process.kill(pid, 0); return true; }
|
|
152
|
+
catch (error) { return Boolean(error && typeof error === 'object' && error.code === 'EPERM'); }
|
|
153
|
+
}
|
|
154
|
+
function recoverOrphanReclaim(path) {
|
|
155
|
+
const reclaimPath = `${path}.reclaim`;
|
|
156
|
+
if (!existsSync(reclaimPath)) return;
|
|
157
|
+
let owner;
|
|
158
|
+
try { owner = parseJsonStrict(readFileSync(reclaimPath, 'utf8')); } catch { throw new LoopValidationError('state-root lock reclaim 内容损坏;拒绝自动接管'); }
|
|
159
|
+
if (!Number.isInteger(Number(owner.pid)) || Number(owner.pid) <= 0 || typeof owner.token !== 'string' || !owner.token) throw new LoopValidationError('state-root lock reclaim owner 无效;拒绝自动接管');
|
|
160
|
+
if (processIsAlive(Number(owner.pid))) throw new LoopValidationError('state-root lock 正在执行 stale recovery');
|
|
161
|
+
let latest;
|
|
162
|
+
try { latest = parseJsonStrict(readFileSync(reclaimPath, 'utf8')); } catch (error) { if (error && typeof error === 'object' && error.code === 'ENOENT') return; throw new LoopValidationError('state-root lock reclaim 内容损坏;拒绝自动接管'); }
|
|
163
|
+
if (Number(latest.pid) !== Number(owner.pid) || latest.token !== owner.token) return;
|
|
164
|
+
try { unlinkSync(reclaimPath); } catch (error) { if (!error || typeof error !== 'object' || error.code !== 'ENOENT') throw error; }
|
|
165
|
+
}
|
|
166
|
+
export function acquireLock(path) {
|
|
167
|
+
for (let attempt = 0; attempt < 4; attempt += 1) {
|
|
168
|
+
recoverOrphanReclaim(path);
|
|
169
|
+
const owner = { pid: process.pid, token: randomUUID(), acquired_at: new Date().toISOString() };
|
|
170
|
+
const candidate = `${path}.${owner.pid}.${owner.token}.candidate`;
|
|
171
|
+
const fd = openSync(candidate, 'wx', 0o600);
|
|
172
|
+
try { writeFileSync(fd, `${JSON.stringify(owner)}\n`); fsyncSync(fd); } finally { closeSync(fd); }
|
|
173
|
+
try {
|
|
174
|
+
if (existsSync(`${path}.reclaim`)) throw new LoopValidationError('state-root lock 正在执行 stale recovery');
|
|
175
|
+
linkSync(candidate, path); unlinkSync(candidate); return owner;
|
|
176
|
+
}
|
|
177
|
+
catch (error) {
|
|
178
|
+
try { unlinkSync(candidate); } catch {}
|
|
179
|
+
if (error instanceof LoopValidationError) throw error;
|
|
180
|
+
if (!error || typeof error !== 'object' || error.code !== 'EEXIST') throw error;
|
|
181
|
+
let current;
|
|
182
|
+
try { current = parseJsonStrict(readFileSync(path, 'utf8')); } catch { throw new LoopValidationError('state-root lock 内容损坏;拒绝自动接管'); }
|
|
183
|
+
if (!Number.isInteger(Number(current.pid)) || Number(current.pid) <= 0) throw new LoopValidationError('state-root lock owner 无效;拒绝自动接管');
|
|
184
|
+
if (processIsAlive(Number(current.pid))) throw new LoopValidationError(`state-root lock 正被 PID ${current.pid} 持有`);
|
|
185
|
+
const reclaimPath = `${path}.reclaim`;
|
|
186
|
+
const reclaimOwner = { pid: process.pid, token: randomUUID(), acquired_at: new Date().toISOString() };
|
|
187
|
+
const reclaimCandidate = `${reclaimPath}.${reclaimOwner.pid}.${reclaimOwner.token}.candidate`;
|
|
188
|
+
const reclaimFd = openSync(reclaimCandidate, 'wx', 0o600);
|
|
189
|
+
try { writeFileSync(reclaimFd, `${JSON.stringify(reclaimOwner)}\n`); fsyncSync(reclaimFd); } finally { closeSync(reclaimFd); }
|
|
190
|
+
try { linkSync(reclaimCandidate, reclaimPath); } catch (reclaimError) { if (!reclaimError || typeof reclaimError !== 'object' || reclaimError.code !== 'EEXIST') throw reclaimError; throw new LoopValidationError('state-root lock 正在执行 stale recovery'); }
|
|
191
|
+
finally { try { unlinkSync(reclaimCandidate); } catch {} }
|
|
192
|
+
try {
|
|
193
|
+
let latest;
|
|
194
|
+
try { latest = parseJsonStrict(readFileSync(path, 'utf8')); } catch (latestError) { if (latestError && typeof latestError === 'object' && latestError.code === 'ENOENT') continue; throw new LoopValidationError('state-root lock 内容损坏;拒绝自动接管'); }
|
|
195
|
+
if (Number(latest.pid) !== Number(current.pid) || latest.token !== current.token) continue;
|
|
196
|
+
if (processIsAlive(Number(latest.pid))) throw new LoopValidationError(`state-root lock 正被 PID ${latest.pid} 持有`);
|
|
197
|
+
unlinkSync(path);
|
|
198
|
+
} finally { releaseLock(reclaimPath, reclaimOwner); }
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
throw new LoopValidationError('无法获取 state-root lock');
|
|
202
|
+
}
|
|
203
|
+
export function releaseLock(path, owner) {
|
|
204
|
+
let current;
|
|
205
|
+
try { current = parseJsonStrict(readFileSync(path, 'utf8')); } catch (error) { if (error && typeof error === 'object' && error.code === 'ENOENT') return false; throw new LoopValidationError('state-root lock 在持有期间损坏;拒绝删除未知 owner 的 lock'); }
|
|
206
|
+
if (current.pid !== owner.pid || current.token !== owner.token) return false;
|
|
207
|
+
unlinkSync(path);
|
|
208
|
+
return true;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function git(args, cwd, encoding = 'utf8') {
|
|
212
|
+
return execFileSync('git', args, { cwd, encoding, stdio: ['ignore', 'pipe', 'pipe'], maxBuffer: 64 * 1024 * 1024 });
|
|
213
|
+
}
|
|
214
|
+
function gitStatus(args, cwd) { return spawnSync('git', args, { cwd, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] }); }
|
|
215
|
+
|
|
216
|
+
function validateArtifact(artifact) {
|
|
217
|
+
if (artifact?.schema_version !== 1) throw new LoopValidationError('Artifact schema_version 必须为 1');
|
|
218
|
+
for (const field of ['provider', 'repository_id', 'object_format', 'base_sha', 'artifact_sha']) if (!artifact[field]) throw new LoopValidationError(`Artifact 缺少 ${field}`);
|
|
219
|
+
if (!['manage-worktrees', 'caller-supplied'].includes(artifact.provider)) throw new LoopValidationError('Artifact provider 非法');
|
|
220
|
+
const size = artifact.object_format === 'sha1' ? 40 : artifact.object_format === 'sha256' ? 64 : 0;
|
|
221
|
+
if (!size || !new RegExp(`^[0-9a-f]{${size}}$`, 'u').test(artifact.base_sha) || !new RegExp(`^[0-9a-f]{${size}}$`, 'u').test(artifact.artifact_sha)) throw new LoopValidationError('Artifact object format 或 SHA 非法');
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function validateProfilePath(path) {
|
|
225
|
+
if (!path || isAbsolute(path) || path.includes('\\') || path.includes('..') || path.includes(':(') || /[*?[\]]/u.test(path)) throw new LoopValidationError(`非法 verifier path: ${path}`);
|
|
226
|
+
const core = path.endsWith('/') ? path.slice(0, -1) : path;
|
|
227
|
+
if (!core || core.split('/').some((part) => !part || part === '.' || part === '..')) throw new LoopValidationError(`非法 verifier path: ${path}`);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function profilePathMatches(path, rule) { return rule.endsWith('/') ? path.startsWith(rule) : path === rule; }
|
|
231
|
+
|
|
232
|
+
function verifyProtectedPaths(artifact, profile, workdir) {
|
|
233
|
+
const protectedPaths = profile.protected_verifier_paths;
|
|
234
|
+
const allowed = profile.allowed_validation_changes;
|
|
235
|
+
const bytes = git(['diff', '--name-status', '-z', '--no-renames', artifact.base_sha, artifact.artifact_sha], workdir, 'buffer');
|
|
236
|
+
const tokens = bytes.toString('utf8').split('\0').filter(Boolean);
|
|
237
|
+
const changed = [];
|
|
238
|
+
for (let index = 0; index < tokens.length; index += 2) if (tokens[index + 1]) changed.push(tokens[index + 1]);
|
|
239
|
+
const violations = changed.filter((path) => protectedPaths.some((rule) => profilePathMatches(path, rule)) && !allowed.some((rule) => profilePathMatches(path, rule)));
|
|
240
|
+
if (violations.length > 0) throw new LoopOperationalAbort('protected_path_violation', `未授权修改 verifier path: ${violations.join(', ')}`);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function verifyGitArtifact(artifact, workdir, frozenIdentity = null) {
|
|
244
|
+
validateArtifact(artifact);
|
|
245
|
+
const root = realpathSync(String(git(['rev-parse', '--show-toplevel'], workdir)).trim());
|
|
246
|
+
if (root !== realpathSync(workdir)) throw new LoopOperationalAbort('stale_precondition', 'workdir 必须是 worktree 根目录');
|
|
247
|
+
const objectFormat = String(git(['rev-parse', '--show-object-format'], root)).trim();
|
|
248
|
+
if (objectFormat !== artifact.object_format) throw new LoopOperationalAbort('stale_precondition', 'object format 已变化');
|
|
249
|
+
for (const field of ['base_sha', 'artifact_sha']) if (String(git(['cat-file', '-t', artifact[field]], root)).trim() !== 'commit') throw new LoopValidationError(`${field} 不是 commit`);
|
|
250
|
+
if (gitStatus(['merge-base', '--is-ancestor', artifact.base_sha, artifact.artifact_sha], root).status !== 0) throw new LoopValidationError('base_sha 不是 artifact ancestor');
|
|
251
|
+
if (String(git(['rev-parse', 'HEAD'], root)).trim() !== artifact.artifact_sha) throw new LoopOperationalAbort('stale_precondition', 'HEAD 偏离 Artifact');
|
|
252
|
+
if (git(['status', '--porcelain=v1', '-z', '--untracked-files=all'], root, 'buffer').length > 0) throw new LoopOperationalAbort('stale_precondition', 'embedded workdir 不干净');
|
|
253
|
+
const roots = String(git(['rev-list', '--max-parents=0', artifact.artifact_sha], root)).trim().split('\n').filter(Boolean).sort();
|
|
254
|
+
const identity = `git:${objectFormat}:${sha256(Buffer.from(canonicalJson(roots), 'utf8'))}`;
|
|
255
|
+
if (frozenIdentity && frozenIdentity !== identity) throw new LoopOperationalAbort('stale_precondition', 'repository identity 已变化');
|
|
256
|
+
return identity;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function validateContract(contract) {
|
|
260
|
+
if (contract?.schema_version !== 1 || !contract.contract_id || !contract.objective) throw new LoopValidationError('Task Contract schema/id/objective 无效');
|
|
261
|
+
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)) throw new LoopValidationError('Task Contract scope 无效');
|
|
262
|
+
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) || contract.permissions.mode === 'read_only' && contract.permissions.writable_paths.length) throw new LoopValidationError('Task Contract permissions 无效');
|
|
263
|
+
if (!contract.environment || typeof contract.environment.repository !== 'string' || !contract.environment.repository || !['shared_tree', 'worktree', 'caller_supplied'].includes(contract.environment.isolation)) throw new LoopValidationError('Task Contract environment 无效');
|
|
264
|
+
if (!Array.isArray(contract.stop_conditions) || !contract.extensions || typeof contract.extensions !== 'object' || Array.isArray(contract.extensions)) throw new LoopValidationError('Task Contract stop_conditions/extensions 无效');
|
|
265
|
+
if (!Array.isArray(contract.acceptance) || contract.acceptance.length === 0) throw new LoopValidationError('acceptance 不能为空');
|
|
266
|
+
const ids = new Set();
|
|
267
|
+
for (const item of contract.acceptance) {
|
|
268
|
+
if (!item?.contract_item_id || !item?.requirement || ids.has(item.contract_item_id)) throw new LoopValidationError('acceptance ID 缺失或重复');
|
|
269
|
+
ids.add(item.contract_item_id);
|
|
270
|
+
}
|
|
271
|
+
if (!Array.isArray(contract.skill_set)) throw new LoopValidationError('skill_set 必须是数组');
|
|
272
|
+
if (!DIGEST_PATTERN.test(contract.contract_digest) || envelopeDigest(contract, 'contract_digest') !== contract.contract_digest) throw new LoopValidationError('Task Contract digest 无效');
|
|
273
|
+
return ids;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
function validateProfile(profile, ids) {
|
|
277
|
+
try { validateJsonSchema(profile, schema('verification-profile-v1.schema.json'), 'Verification Profile'); } catch (error) { throw new LoopValidationError(error instanceof Error ? error.message : String(error)); }
|
|
278
|
+
if (profile?.schema_version !== 1 || !profile.profile_id || !Array.isArray(profile.l0_checks) || profile.l0_checks.length === 0 || !Array.isArray(profile.l1_review) || profile.l1_review.length === 0) throw new LoopValidationError('Verification Profile 无效');
|
|
279
|
+
const checks = new Set();
|
|
280
|
+
for (const check of profile.l0_checks) {
|
|
281
|
+
if (!check?.check_id || checks.has(check.check_id) || !Array.isArray(check.argv) || check.argv.length === 0 || check.argv.some((item) => typeof item !== 'string')) throw new LoopValidationError('L0 check 无效或重复');
|
|
282
|
+
checks.add(check.check_id);
|
|
283
|
+
if (!['smoke', 'final', 'both'].includes(check.stage) || !Number.isInteger(check.timeout_ms) || check.timeout_ms <= 0 || !Array.isArray(check.expected_exit_codes) || check.expected_exit_codes.length === 0 || check.expected_exit_codes.some((code) => !Number.isInteger(code))) throw new LoopValidationError(`${check.check_id} 配置无效`);
|
|
284
|
+
if (!check.cwd_rel || isAbsolute(check.cwd_rel) || check.cwd_rel.includes('\\') || check.cwd_rel.split('/').some((part) => part === '..')) throw new LoopValidationError(`${check.check_id} cwd_rel 非法`);
|
|
285
|
+
}
|
|
286
|
+
if (!profile.l0_checks.some((check) => ['smoke', 'both'].includes(check.stage)) || !profile.l0_checks.some((check) => ['final', 'both'].includes(check.stage))) throw new LoopValidationError('Verification Profile 必须覆盖 smoke 与 final L0');
|
|
287
|
+
for (const review of profile.l1_review) if (!ids.has(review?.contract_item_id)) throw new LoopValidationError(`L1 引用未知 acceptance: ${review?.contract_item_id}`);
|
|
288
|
+
if (!Array.isArray(profile.protected_verifier_paths) || !Array.isArray(profile.allowed_validation_changes)) throw new LoopValidationError('Profile path 字段必须是数组');
|
|
289
|
+
for (const path of [...profile.protected_verifier_paths, ...profile.allowed_validation_changes]) validateProfilePath(path);
|
|
290
|
+
if (!profile.runtime || !Array.isArray(profile.runtime.env_allowlist) || profile.runtime.env_allowlist.some((name) => typeof name !== 'string' || !name) || typeof profile.runtime.executable_paths !== 'object' || !Number.isInteger(profile.runtime.max_log_bytes) || profile.runtime.max_log_bytes <= 0) throw new LoopValidationError('Profile runtime 无效');
|
|
291
|
+
if (!['disabled', 'isolated', 'trusted_identity'].includes(profile.runtime.cache_policy) || !['denied', 'contract_authorized'].includes(profile.runtime.network_policy)) throw new LoopValidationError('Profile cache/network policy 无效');
|
|
292
|
+
if (!['none', 'release', 'destructive', 'external_side_effect'].includes(profile.human_gate)) throw new LoopValidationError('human_gate 非法');
|
|
293
|
+
for (const check of profile.l0_checks) {
|
|
294
|
+
const executable = profile.runtime.executable_paths[check.argv[0]];
|
|
295
|
+
if (!executable || !isAbsolute(executable) || !statSync(executable).isFile()) throw new LoopValidationError(`未冻结 executable: ${check.argv[0]}`);
|
|
296
|
+
}
|
|
297
|
+
if (!DIGEST_PATTERN.test(profile.verification_profile_digest) || envelopeDigest(profile, 'verification_profile_digest') !== profile.verification_profile_digest) throw new LoopValidationError('Profile digest 无效');
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
function validateSkillBinding(contract, contentDigest) {
|
|
301
|
+
const entry = contract.skill_set.find((item) => item?.name === 'run-agent-verify-loop');
|
|
302
|
+
if (!entry || entry.content_digest !== contentDigest) throw new LoopValidationError('Task Contract 未绑定当前 run-agent-verify-loop 内容摘要');
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
function validateReview(review, snapshot, ids) {
|
|
306
|
+
try { validateJsonSchema(review, schema('review-result-v1.schema.json'), 'Review Result'); } catch (error) { throw new LoopValidationError(error instanceof Error ? error.message : String(error)); }
|
|
307
|
+
if (review?.schema_version !== 1 || !review.review_result_id || !['fail', 'no_defect_found', 'undecidable'].includes(review.verdict)) throw new LoopValidationError('Review Result schema/id/verdict 无效');
|
|
308
|
+
if (review.contract_digest !== snapshot.contract_digest || review.verification_profile_digest !== snapshot.verification_profile_digest || canonicalJson(review.artifact_ref) !== canonicalJson(snapshot.current_iteration.artifact_ref)) throw new LoopValidationError('Review Result binding 不匹配');
|
|
309
|
+
if (review.challenge_nonce !== snapshot.current_iteration.review_challenge_nonce) throw new LoopValidationError('Review Result challenge nonce 不匹配或已重放');
|
|
310
|
+
if (!Array.isArray(review.findings) || !Array.isArray(review.forensics)) throw new LoopValidationError('Review Result findings/forensics 无效');
|
|
311
|
+
for (const finding of review.findings) {
|
|
312
|
+
if (!ids.has(finding?.contract_item_id) || !FINDING_CLASSES.has(finding?.class)) throw new LoopValidationError('finding ID/class 无效');
|
|
313
|
+
for (const field of ['evidence', 'expected', 'actual']) if (typeof finding[field] !== 'string' || !finding[field].trim()) throw new LoopValidationError(`finding ${field} 必须是非空字符串`);
|
|
314
|
+
}
|
|
315
|
+
if (review.forensics.some((item) => typeof item !== 'string' || !item.trim())) throw new LoopValidationError('forensics 必须是非空字符串数组');
|
|
316
|
+
if (review.verdict === 'fail' && review.findings.length === 0) throw new LoopValidationError('fail 必须有 finding');
|
|
317
|
+
if (review.verdict === 'no_defect_found' && (review.findings.length > 0 || review.forensics.length === 0)) throw new LoopValidationError('no_defect_found 必须无 finding 且有 forensics');
|
|
318
|
+
if (review.verdict === 'undecidable' && review.findings.length === 0) throw new LoopValidationError('undecidable 必须说明证据缺口');
|
|
319
|
+
if (!DIGEST_PATTERN.test(review.review_result_digest) || envelopeDigest(review, 'review_result_digest') !== review.review_result_digest) throw new LoopValidationError('Review Result digest 无效');
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
function validateEvidence(evidence, snapshot) {
|
|
323
|
+
if (evidence?.schema_version !== 1 || evidence.protocol_version !== 1 || evidence.provenance?.provider !== 'verify-agent-output') throw new LoopValidationError('Evidence schema/protocol/provider 无效');
|
|
324
|
+
if (typeof evidence.runtime_version !== 'string' || !/^\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?$/u.test(evidence.runtime_version)) throw new LoopValidationError('Evidence runtime_version 非法');
|
|
325
|
+
if (!['pass', 'fail', 'undecidable', 'blocked_safety'].includes(evidence.terminal_outcome) || evidence.completion_scope !== 'verification_only') throw new LoopValidationError('Evidence outcome/scope 无效');
|
|
326
|
+
if (evidence.contract_digest !== snapshot.contract_digest || evidence.verification_profile_digest !== snapshot.verification_profile_digest || canonicalJson(evidence.artifact_ref) !== canonicalJson(snapshot.current_iteration.artifact_ref)) throw new LoopValidationError('Evidence binding 不匹配');
|
|
327
|
+
if (evidence.run_id !== snapshot.current_iteration.verification_run_id) throw new LoopValidationError('Evidence run_id 与 iteration 不一致');
|
|
328
|
+
if (evidence.terminal_outcome === 'fail') {
|
|
329
|
+
const checks = [...(evidence.stages?.smoke_l0?.checks ?? []), ...(evidence.stages?.final_l0?.checks ?? [])];
|
|
330
|
+
const findings = evidence.stages?.l1_review?.findings ?? [];
|
|
331
|
+
if (!checks.some((item) => item?.passed === false) && findings.length === 0) throw new LoopValidationError('fail Evidence 缺少稳定失败依据');
|
|
332
|
+
}
|
|
333
|
+
if (!DIGEST_PATTERN.test(evidence.evidence_digest) || envelopeDigest(evidence, 'evidence_digest') !== evidence.evidence_digest) throw new LoopValidationError('Evidence digest 无效');
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
function sanitize(text, environment, maxBytes) {
|
|
337
|
+
let output = text;
|
|
338
|
+
for (const [name, value] of Object.entries(environment)) if (value && /(token|secret|password|credential|api[_-]?key)/iu.test(name)) output = output.split(value).join('[REDACTED]');
|
|
339
|
+
output = output.replace(/\bBearer\s+[A-Za-z0-9._~+/=-]+/giu, 'Bearer [REDACTED]').replace(/\b(?:ghp|glpat|sk)-[A-Za-z0-9_-]{12,}\b/gu, '[REDACTED]');
|
|
340
|
+
const bytes = Buffer.from(output, 'utf8');
|
|
341
|
+
return bytes.length <= maxBytes ? output : `${bytes.subarray(0, maxBytes).toString('utf8')}\n[TRUNCATED]\n`;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
function persistLog(loopDir, text) {
|
|
345
|
+
const digest = sha256(Buffer.from(text, 'utf8'));
|
|
346
|
+
const directory = join(loopDir, 'logs');
|
|
347
|
+
mkdirSync(directory, { recursive: true, mode: 0o700 });
|
|
348
|
+
const name = `${digest.slice(7)}.log`;
|
|
349
|
+
if (!existsSync(join(directory, name))) writeFileSync(join(directory, name), text, { flag: 'wx', mode: 0o600 });
|
|
350
|
+
return { log_digest: digest, log_ref: `logs/${name}` };
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
function canonicalFuturePath(path) {
|
|
354
|
+
let cursor = resolve(path);
|
|
355
|
+
const suffix = [];
|
|
356
|
+
while (!existsSync(cursor)) {
|
|
357
|
+
const parent = dirname(cursor);
|
|
358
|
+
if (parent === cursor) break;
|
|
359
|
+
suffix.unshift(cursor.slice(parent.length + 1));
|
|
360
|
+
cursor = parent;
|
|
361
|
+
}
|
|
362
|
+
return resolve(realpathSync(cursor), ...suffix);
|
|
363
|
+
}
|
|
364
|
+
function pathInside(candidate, parent) {
|
|
365
|
+
const rel = relative(realpathSync(parent), canonicalFuturePath(candidate));
|
|
366
|
+
return rel === '' || (!rel.startsWith(`..${sep}`) && rel !== '..' && !isAbsolute(rel));
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
function fileIdentity(path) {
|
|
370
|
+
const absolute = realpathSync(path);
|
|
371
|
+
const stat = statSync(absolute);
|
|
372
|
+
return { path: absolute, size: stat.size, mode: stat.mode, digest: sha256(readFileSync(absolute)) };
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
function freezeArgvFiles(profile, workdir) {
|
|
376
|
+
if (!workdir) return {};
|
|
377
|
+
const frozen = {};
|
|
378
|
+
for (const check of profile.l0_checks) {
|
|
379
|
+
const cwd = resolve(workdir, check.cwd_rel);
|
|
380
|
+
for (let index = 1; index < check.argv.length; index += 1) {
|
|
381
|
+
const argument = check.argv[index];
|
|
382
|
+
if (argument.startsWith('-')) continue;
|
|
383
|
+
const candidate = isAbsolute(argument) ? argument : resolve(cwd, argument);
|
|
384
|
+
if (!existsSync(candidate) || !statSync(candidate).isFile()) continue;
|
|
385
|
+
const absolute = realpathSync(candidate);
|
|
386
|
+
frozen[`${check.check_id}:${index}`] = { argument_path: resolve(candidate), ...fileIdentity(absolute) };
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
return frozen;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
function executeEmbeddedChecks(snapshot, loopDir) {
|
|
393
|
+
if (!snapshot.workdir) throw new LoopValidationError('embedded provider 必须在 init 提供 --workdir');
|
|
394
|
+
verifyGitArtifact(snapshot.current_iteration.artifact_ref, snapshot.workdir, snapshot.runtime_repository_identity);
|
|
395
|
+
const profile = readJson(join(loopDir, 'profile.json'));
|
|
396
|
+
const environment = {};
|
|
397
|
+
for (const name of profile.runtime.env_allowlist) if (process.env[name] !== undefined) environment[name] = String(process.env[name]);
|
|
398
|
+
const results = [];
|
|
399
|
+
for (const check of profile.l0_checks) {
|
|
400
|
+
verifyGitArtifact(snapshot.current_iteration.artifact_ref, snapshot.workdir, snapshot.runtime_repository_identity);
|
|
401
|
+
const cwd = resolve(snapshot.workdir, check.cwd_rel);
|
|
402
|
+
if (!pathInside(cwd, snapshot.workdir)) throw new LoopValidationError(`${check.check_id} cwd_rel 越界`);
|
|
403
|
+
const executable = profile.runtime.executable_paths[check.argv[0]];
|
|
404
|
+
let live;
|
|
405
|
+
try { const absolute = realpathSync(executable); const stat = statSync(absolute); live = { path: absolute, size: stat.size, mode: stat.mode, digest: sha256(readFileSync(absolute)) }; }
|
|
406
|
+
catch (error) { throw new LoopOperationalAbort('check_runtime_failure', `${check.check_id} executable 无法执行: ${error instanceof Error ? error.message : String(error)}`); }
|
|
407
|
+
if (!snapshot.executable_identities?.[check.argv[0]] || canonicalJson(live) !== canonicalJson(snapshot.executable_identities[check.argv[0]])) throw new LoopOperationalAbort('check_runtime_failure', `冻结 executable 已变化: ${check.argv[0]}`);
|
|
408
|
+
for (const [key, identity] of Object.entries(snapshot.argv_file_identities ?? {})) {
|
|
409
|
+
const [checkId, rawIndex] = key.split(':');
|
|
410
|
+
if (checkId !== check.check_id) continue;
|
|
411
|
+
const argument = check.argv[Number(rawIndex)];
|
|
412
|
+
const candidate = isAbsolute(argument) ? argument : resolve(cwd, argument);
|
|
413
|
+
let argumentLive;
|
|
414
|
+
try { argumentLive = { argument_path: resolve(candidate), ...fileIdentity(candidate) }; }
|
|
415
|
+
catch (error) { throw new LoopOperationalAbort('check_runtime_failure', `冻结 argv 文件不可用: ${key}: ${error instanceof Error ? error.message : String(error)}`); }
|
|
416
|
+
if (canonicalJson(argumentLive) !== canonicalJson(identity)) throw new LoopOperationalAbort('check_runtime_failure', `冻结 argv 文件已变化: ${key}`);
|
|
417
|
+
}
|
|
418
|
+
const result = spawnSync(executable, check.argv.slice(1), { cwd, env: environment, encoding: 'utf8', timeout: check.timeout_ms, maxBuffer: Math.max(profile.runtime.max_log_bytes * 4, 1024 * 1024), shell: false, stdio: ['ignore', 'pipe', 'pipe'] });
|
|
419
|
+
const timedOut = result.error?.code === 'ETIMEDOUT';
|
|
420
|
+
const exitCode = Number.isInteger(result.status) ? result.status : null;
|
|
421
|
+
const log = sanitize([result.stdout, result.stderr, result.error?.message].filter(Boolean).join('\n'), environment, profile.runtime.max_log_bytes);
|
|
422
|
+
const persisted = persistLog(loopDir, log);
|
|
423
|
+
if (result.error && !timedOut) throw new LoopOperationalAbort('check_runtime_failure', `${check.check_id} 无法执行,诊断 ${persisted.log_ref}: ${result.error.message}`);
|
|
424
|
+
if (exitCode === null && !timedOut) throw new LoopOperationalAbort('check_runtime_failure', `${check.check_id} 未产生退出码,诊断 ${persisted.log_ref}`);
|
|
425
|
+
results.push({ check_id: check.check_id, exit_code: exitCode, expected_exit_codes: check.expected_exit_codes, timed_out: timedOut, passed: !timedOut && check.expected_exit_codes.includes(exitCode), ...persisted });
|
|
426
|
+
verifyGitArtifact(snapshot.current_iteration.artifact_ref, snapshot.workdir, snapshot.runtime_repository_identity);
|
|
427
|
+
}
|
|
428
|
+
return { checks: results, passed: results.every((item) => item.passed) };
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
function readJournal(loopDir) {
|
|
432
|
+
const path = join(loopDir, 'events.ndjson');
|
|
433
|
+
if (!existsSync(path)) throw new LoopValidationError('loop 缺少 event journal');
|
|
434
|
+
const text = readFileSync(path, 'utf8');
|
|
435
|
+
const lastNewline = text.lastIndexOf('\n');
|
|
436
|
+
const complete = lastNewline < 0 ? '' : text.slice(0, lastNewline + 1);
|
|
437
|
+
const trailing = lastNewline === text.length - 1 ? '' : text.slice(lastNewline + 1);
|
|
438
|
+
const events = complete.split('\n').filter(Boolean).map((line) => parseJsonStrict(line));
|
|
439
|
+
let previous = null;
|
|
440
|
+
for (let index = 0; index < events.length; index += 1) {
|
|
441
|
+
const event = events[index];
|
|
442
|
+
if (event.revision !== index || event.previous_event_digest !== previous || !DIGEST_PATTERN.test(event.event_digest ?? '') || envelopeDigest(event, 'event_digest') !== event.event_digest) throw new LoopValidationError(`event journal 链在 revision ${index} 无效`);
|
|
443
|
+
previous = event.event_digest;
|
|
444
|
+
}
|
|
445
|
+
return { events, complete, trailing };
|
|
446
|
+
}
|
|
447
|
+
function loadLoop(loopDir) {
|
|
448
|
+
const journal = readJournal(loopDir);
|
|
449
|
+
const { events } = journal;
|
|
450
|
+
if (events.length === 0) throw new LoopValidationError('loop journal 为空');
|
|
451
|
+
const latest = events.at(-1).snapshot;
|
|
452
|
+
const path = join(loopDir, 'snapshot.json');
|
|
453
|
+
if (!existsSync(path)) return { snapshot: latest, needsRepair: true, events, journal };
|
|
454
|
+
const file = readJson(path);
|
|
455
|
+
const snapshotDrift = file.revision !== latest.revision || canonicalJson(file) !== canonicalJson(latest);
|
|
456
|
+
const needsRepair = snapshotDrift || Boolean(journal.trailing);
|
|
457
|
+
return { snapshot: snapshotDrift ? latest : file, needsRepair, events, journal };
|
|
458
|
+
}
|
|
459
|
+
function ensureConvergenceReport(loopDir, snapshot) {
|
|
460
|
+
if (!['completed', 'stopped'].includes(snapshot.state) || snapshot.convergence_report_ref) return snapshot;
|
|
461
|
+
const contract = readJson(join(loopDir, 'contract.json'));
|
|
462
|
+
const report = {
|
|
463
|
+
schema_version: 1,
|
|
464
|
+
report_id: randomUUID(),
|
|
465
|
+
loop_id: snapshot.loop_id,
|
|
466
|
+
contract_digest: snapshot.contract_digest,
|
|
467
|
+
skill_set: contract.skill_set,
|
|
468
|
+
outcome: snapshot.state,
|
|
469
|
+
iterations: snapshot.iterations.length,
|
|
470
|
+
failure_signatures: [...new Set(snapshot.iterations.map((item) => item.failure_signature).filter(Boolean))],
|
|
471
|
+
strategy_changes: snapshot.iterations.filter((item) => item.next_action).map((item) => ({ iteration: item.index, action: item.next_action })),
|
|
472
|
+
verification_aborts: snapshot.current_iteration?.verification_abort ? [snapshot.current_iteration.verification_abort] : snapshot.terminal?.kind === 'verification_abort' ? [snapshot.terminal] : [],
|
|
473
|
+
verification_gaps: snapshot.iterations.filter((item) => item.outcome === 'undecidable').map((item) => ({ iteration: item.index, outcome: item.outcome })),
|
|
474
|
+
reflection_refs: snapshot.reflection_refs ?? [],
|
|
475
|
+
improvement_proposal_refs: snapshot.improvement_proposal_refs ?? [],
|
|
476
|
+
generated_at: new Date().toISOString(),
|
|
477
|
+
};
|
|
478
|
+
report.report_digest = envelopeDigest(report, 'report_digest');
|
|
479
|
+
const ref = `convergence-report-${report.report_id}.json`;
|
|
480
|
+
if (existsSync(join(loopDir, ref))) {
|
|
481
|
+
const existing = readJson(join(loopDir, ref));
|
|
482
|
+
if (envelopeDigest(existing, 'report_digest') !== existing.report_digest || existing.loop_id !== snapshot.loop_id) throw new LoopValidationError('已有 Convergence Report 无效');
|
|
483
|
+
return { ...snapshot, convergence_report_ref: { ref, report_id: existing.report_id, report_digest: existing.report_digest } };
|
|
484
|
+
}
|
|
485
|
+
writeNewJson(join(loopDir, ref), report);
|
|
486
|
+
return { ...snapshot, convergence_report_ref: { ref, report_id: report.report_id, report_digest: report.report_digest } };
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
function persistTransition(loopDir, snapshot, kind) {
|
|
490
|
+
const terminalized = ensureConvergenceReport(loopDir, snapshot);
|
|
491
|
+
const next = { ...terminalized, revision: terminalized.revision + 1, updated_at: new Date().toISOString() };
|
|
492
|
+
const previous = readJournal(loopDir).events.at(-1)?.event_digest ?? null;
|
|
493
|
+
const event = { schema_version: 1, revision: next.revision, kind, recorded_at: next.updated_at, previous_event_digest: previous, snapshot: next };
|
|
494
|
+
event.event_digest = envelopeDigest(event, 'event_digest');
|
|
495
|
+
const fd = openSync(join(loopDir, 'events.ndjson'), 'a', 0o600);
|
|
496
|
+
try { appendFileSync(fd, `${canonicalJson(event)}\n`, 'utf8'); fsyncSync(fd); } finally { closeSync(fd); }
|
|
497
|
+
atomicWriteJson(join(loopDir, 'snapshot.json'), next);
|
|
498
|
+
return next;
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
function withRootLock(stateRoot, callback) {
|
|
502
|
+
mkdirSync(stateRoot, { recursive: true, mode: 0o700 });
|
|
503
|
+
const path = join(stateRoot, '.loop-runtime.lock');
|
|
504
|
+
const owner = acquireLock(path);
|
|
505
|
+
try { return callback(); } finally { releaseLock(path, owner); }
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
function stateRootForLoop(loopDir) {
|
|
509
|
+
const stateRoot = realpathSync(resolve(loopDir, '..', '..'));
|
|
510
|
+
const identityPath = join(stateRoot, '.state-root-identity.json');
|
|
511
|
+
if (!existsSync(identityPath)) throw new LoopValidationError('state root identity 缺失,疑似复制或越权移动');
|
|
512
|
+
const identity = readJson(identityPath);
|
|
513
|
+
if (identity.schema_version !== 1 || identity.canonical_path !== stateRoot || !DIGEST_PATTERN.test(identity.identity_digest ?? '') || envelopeDigest(identity, 'identity_digest') !== identity.identity_digest) throw new LoopValidationError('state root identity 与当前位置不匹配,拒绝复制态重放');
|
|
514
|
+
return stateRoot;
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
function initializeStateRootIdentity(stateRoot) {
|
|
518
|
+
const canonical = realpathSync(stateRoot);
|
|
519
|
+
const path = join(canonical, '.state-root-identity.json');
|
|
520
|
+
if (existsSync(path)) {
|
|
521
|
+
const identity = readJson(path);
|
|
522
|
+
if (identity.canonical_path !== canonical || envelopeDigest(identity, 'identity_digest') !== identity.identity_digest) throw new LoopValidationError('state root identity 无效');
|
|
523
|
+
return;
|
|
524
|
+
}
|
|
525
|
+
const identity = { schema_version: 1, state_root_id: randomUUID(), canonical_path: canonical };
|
|
526
|
+
identity.identity_digest = envelopeDigest(identity, 'identity_digest');
|
|
527
|
+
writeNewJson(path, identity);
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
function adoptStateRoot(options) {
|
|
531
|
+
const stateRoot = realpathSync(required(options, 'state-root'));
|
|
532
|
+
const loopsDir = join(stateRoot, 'loops');
|
|
533
|
+
if (!existsSync(loopsDir) || !lstatSync(loopsDir).isDirectory()) throw new LoopValidationError('adopt-root 只接受已有 loops/ 的 state root');
|
|
534
|
+
return withRootLock(stateRoot, () => {
|
|
535
|
+
const loopNames = readdirSync(loopsDir).sort();
|
|
536
|
+
if (loopNames.length === 0) throw new LoopValidationError('adopt-root 不用于初始化空 state root;请使用 init');
|
|
537
|
+
for (const name of loopNames) {
|
|
538
|
+
const loopDir = join(loopsDir, name);
|
|
539
|
+
if (!lstatSync(loopDir).isDirectory() || lstatSync(loopDir).isSymbolicLink()) throw new LoopValidationError(`非法 Loop 目录: ${name}`);
|
|
540
|
+
const loaded = loadLoop(loopDir);
|
|
541
|
+
const contract = readJson(join(loopDir, 'contract.json'));
|
|
542
|
+
const profile = readJson(join(loopDir, 'profile.json'));
|
|
543
|
+
const ids = validateContract(contract);
|
|
544
|
+
validateProfile(profile, ids);
|
|
545
|
+
if (loaded.snapshot.loop_id !== name || loaded.snapshot.contract_digest !== contract.contract_digest || loaded.snapshot.verification_profile_digest !== profile.verification_profile_digest) throw new LoopValidationError(`Loop ${name} 的冻结 envelope 与 journal 不一致`);
|
|
546
|
+
const repository = contract.environment?.repository;
|
|
547
|
+
if (typeof repository === 'string' && repository !== 'none' && existsSync(repository) && pathInside(stateRoot, realpathSync(repository))) throw new LoopValidationError(`state root 位于 Loop ${name} 的业务仓库内,拒绝接管`);
|
|
548
|
+
}
|
|
549
|
+
const identityPath = join(stateRoot, '.state-root-identity.json');
|
|
550
|
+
let previous = null;
|
|
551
|
+
if (existsSync(identityPath)) {
|
|
552
|
+
try {
|
|
553
|
+
const value = readJson(identityPath);
|
|
554
|
+
previous = { state_root_id: value.state_root_id ?? null, canonical_path: value.canonical_path ?? null, identity_digest: value.identity_digest ?? null };
|
|
555
|
+
} catch { previous = { state_root_id: null, canonical_path: null, identity_digest: null }; }
|
|
556
|
+
}
|
|
557
|
+
const identity = { schema_version: 1, state_root_id: randomUUID(), canonical_path: stateRoot, adopted_at: new Date().toISOString(), adopted_from: previous };
|
|
558
|
+
identity.identity_digest = envelopeDigest(identity, 'identity_digest');
|
|
559
|
+
atomicWriteJson(identityPath, identity);
|
|
560
|
+
return { adopted: true, state_root: stateRoot, state_root_id: identity.state_root_id, loops: loopNames.length, previous_identity: previous };
|
|
561
|
+
});
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
function mutateLoop(loopDir, expectedRevision, callback) {
|
|
565
|
+
const stateRoot = stateRootForLoop(loopDir);
|
|
566
|
+
return withRootLock(stateRoot, () => {
|
|
567
|
+
const loaded = loadLoop(loopDir);
|
|
568
|
+
let snapshot = loaded.snapshot;
|
|
569
|
+
if (loaded.journal.trailing) atomicWriteText(join(loopDir, 'events.ndjson'), loaded.journal.complete);
|
|
570
|
+
if (loaded.needsRepair) atomicWriteJson(join(loopDir, 'snapshot.json'), snapshot);
|
|
571
|
+
if (expectedRevision !== null && snapshot.revision !== expectedRevision) throw new LoopValidationError(`revision 冲突:expected ${expectedRevision}, actual ${snapshot.revision}`);
|
|
572
|
+
if (snapshot.skill_provenance.content_digest !== skillContentDigest()) {
|
|
573
|
+
if (!['completed', 'stopped'].includes(snapshot.state)) {
|
|
574
|
+
snapshot = persistTransition(loopDir, { ...snapshot, state: 'stopped', terminal: { kind: 'operational_abort', code: 'skill_drift', diagnostics: 'Skill 内容摘要发生变化' } }, 'skill_drift');
|
|
575
|
+
throw new LoopOperationalAbort('skill_drift', `Loop 已停止 at revision ${snapshot.revision}`);
|
|
576
|
+
}
|
|
577
|
+
throw new LoopOperationalAbort('skill_drift', '终态 Loop 的冻结 Skill 已变化,不能恢复或改写');
|
|
578
|
+
}
|
|
579
|
+
try {
|
|
580
|
+
const result = callback(snapshot, stateRoot);
|
|
581
|
+
return persistTransition(loopDir, result.snapshot, result.kind);
|
|
582
|
+
} catch (error) {
|
|
583
|
+
if (error instanceof LoopOperationalAbort && !['completed', 'stopped'].includes(snapshot.state)) persistTransition(loopDir, { ...snapshot, state: 'stopped', terminal: { kind: 'operational_abort', code: error.code, diagnostics: error.message } }, 'operational_abort');
|
|
584
|
+
throw error;
|
|
585
|
+
}
|
|
586
|
+
});
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
function consumedRunIds(stateRoot) {
|
|
590
|
+
const loopsDir = join(stateRoot, 'loops');
|
|
591
|
+
const ids = new Set();
|
|
592
|
+
if (!existsSync(loopsDir)) return ids;
|
|
593
|
+
for (const name of readdirSync(loopsDir)) {
|
|
594
|
+
const loopDir = join(loopsDir, name);
|
|
595
|
+
if (!existsSync(join(loopDir, 'events.ndjson'))) continue;
|
|
596
|
+
for (const event of readJournal(loopDir).events) {
|
|
597
|
+
for (const id of event.snapshot?.consumed_verification_run_ids ?? []) ids.add(id);
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
return ids;
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
export function failureSignature(l0Result, reviewResult) {
|
|
604
|
+
const keys = [];
|
|
605
|
+
for (const check of l0Result?.checks ?? []) if (!check.passed) keys.push({ kind: 'l0', check_id: check.check_id });
|
|
606
|
+
for (const finding of reviewResult?.findings ?? []) keys.push({ kind: 'l1', contract_item_id: finding.contract_item_id, class: finding.class });
|
|
607
|
+
const unique = [...new Map(keys.map((item) => [canonicalJson(item), item])).values()].sort((left, right) => {
|
|
608
|
+
const a = canonicalJson(left);
|
|
609
|
+
const b = canonicalJson(right);
|
|
610
|
+
return a < b ? -1 : a > b ? 1 : 0;
|
|
611
|
+
});
|
|
612
|
+
return unique.length === 0 ? null : sha256(Buffer.from(canonicalJson(unique), 'utf8'));
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
function evidenceFailureInputs(evidence) {
|
|
616
|
+
const smoke = evidence.stages?.smoke_l0?.checks ?? [];
|
|
617
|
+
const final = evidence.stages?.final_l0?.checks ?? [];
|
|
618
|
+
const review = evidence.stages?.l1_review?.findings ? evidence.stages.l1_review : null;
|
|
619
|
+
return { l0: { checks: [...smoke, ...final] }, review };
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
function applyOutcome(snapshot, outcome, source) {
|
|
623
|
+
const index = snapshot.current_iteration.index;
|
|
624
|
+
let signature = null;
|
|
625
|
+
if (outcome === 'fail') {
|
|
626
|
+
const inputs = source.kind === 'evidence' ? evidenceFailureInputs(source.value) : { l0: source.value.l0_result, review: source.value.l1_result };
|
|
627
|
+
signature = failureSignature(inputs.l0, inputs.review);
|
|
628
|
+
}
|
|
629
|
+
const iteration = {
|
|
630
|
+
index,
|
|
631
|
+
artifact_ref: snapshot.current_iteration.artifact_ref,
|
|
632
|
+
verification_run_id: snapshot.current_iteration.verification_run_id ?? null,
|
|
633
|
+
outcome,
|
|
634
|
+
failure_signature: signature,
|
|
635
|
+
evidence_digest: source.kind === 'evidence' ? source.value.evidence_digest : null,
|
|
636
|
+
embedded_record_digest: source.kind === 'embedded' ? source.value.record_digest : null,
|
|
637
|
+
completed_at: new Date().toISOString(),
|
|
638
|
+
};
|
|
639
|
+
const iterations = [...snapshot.iterations, iteration];
|
|
640
|
+
if (outcome === 'pass') {
|
|
641
|
+
if (snapshot.human_gate.required) return { ...snapshot, iterations, current_iteration: null, state: 'waiting_human', human_gate: { ...snapshot.human_gate, status: 'pending' }, terminal: null };
|
|
642
|
+
return { ...snapshot, iterations, current_iteration: null, state: 'completed', terminal: { kind: 'completed', outcome: 'pass' } };
|
|
643
|
+
}
|
|
644
|
+
if (outcome === 'blocked_safety' || outcome === 'undecidable') return { ...snapshot, iterations, current_iteration: null, state: 'stopped', terminal: { kind: outcome, outcome } };
|
|
645
|
+
const consecutive = signature ? [...iterations].reverse().findIndex((item) => item.failure_signature !== signature) : 0;
|
|
646
|
+
const count = consecutive === -1 ? iterations.length : consecutive;
|
|
647
|
+
if (index >= snapshot.limits.max_iterations) return { ...snapshot, iterations, current_iteration: null, state: 'stopped', terminal: { kind: 'max_iterations', failure_signature: signature } };
|
|
648
|
+
if (signature && count >= snapshot.limits.consecutive_identical_signature) return { ...snapshot, iterations, current_iteration: null, state: 'stopped', terminal: { kind: 'identical_failure_fuse', failure_signature: signature, consecutive: count } };
|
|
649
|
+
if (snapshot.policy.on_failure === 'stop') return { ...snapshot, iterations, current_iteration: null, state: 'stopped', terminal: { kind: 'policy_stop', failure_signature: signature } };
|
|
650
|
+
return { ...snapshot, iterations, current_iteration: null, state: 'active', terminal: null, next_action: snapshot.policy.on_failure };
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
const CLI_SPEC = {
|
|
654
|
+
capabilities: { values: [], flags: ['json'] },
|
|
655
|
+
'adopt-root': { values: ['state-root'], flags: [] },
|
|
656
|
+
init: { values: ['contract', 'profile', 'provider', 'state-root', 'loop-id', 'workdir', 'max-iterations', 'consecutive-identical-signature', 'on-failure', 'goal-ref'], flags: ['allow-repository-state', 'network-isolated'] },
|
|
657
|
+
'record-artifact': { values: ['loop', 'artifact', 'verification-run-id', 'expected-revision'], flags: [] },
|
|
658
|
+
'run-embedded-l0': { values: ['loop', 'expected-revision'], flags: [] },
|
|
659
|
+
'record-embedded-review': { values: ['loop', 'review', 'assurance', 'reviewer-run-id', 'expected-revision'], flags: [] },
|
|
660
|
+
'record-evidence': { values: ['loop', 'evidence', 'expected-revision'], flags: [] },
|
|
661
|
+
'record-verification-abort': { values: ['loop', 'run-id', 'code', 'diagnostics-ref', 'expected-revision'], flags: [] },
|
|
662
|
+
next: { values: ['loop', 'expected-revision'], flags: [] }, resume: { values: ['loop', 'verification-run-id', 'expected-revision'], flags: [] },
|
|
663
|
+
stop: { values: ['loop', 'reason', 'expected-revision'], flags: [] }, 'human-gate': { values: ['loop', 'decision', 'expected-revision'], flags: [] },
|
|
664
|
+
'record-reflection': { values: ['loop', 'input', 'expected-revision'], flags: [] }, 'convergence-report': { values: ['loop'], flags: [] },
|
|
665
|
+
'propose-improvement': { values: ['loop', 'reflection', 'input', 'expected-revision'], flags: [] },
|
|
666
|
+
status: { values: ['loop'], flags: [] }, inspect: { values: ['loop'], flags: [] }, validate: { values: ['loop'], flags: [] }, doctor: { values: ['loop'], flags: [] },
|
|
667
|
+
};
|
|
668
|
+
|
|
669
|
+
function parseCli(argv) {
|
|
670
|
+
const command = argv[0] ?? '';
|
|
671
|
+
const spec = CLI_SPEC[command];
|
|
672
|
+
if (!spec) throw new LoopValidationError('命令必须是 capabilities/adopt-root/init/record-artifact/run-embedded-l0/record-embedded-review/record-evidence/record-verification-abort/next/resume/stop/human-gate/record-reflection/convergence-report/propose-improvement/status/inspect/validate/doctor');
|
|
673
|
+
const options = {};
|
|
674
|
+
const flags = new Set();
|
|
675
|
+
for (let index = 1; index < argv.length; index += 1) {
|
|
676
|
+
const token = argv[index];
|
|
677
|
+
if (!token.startsWith('--')) throw new LoopValidationError(`未知位置参数: ${token}`);
|
|
678
|
+
const name = token.slice(2);
|
|
679
|
+
if (spec.flags.includes(name)) {
|
|
680
|
+
if (argv[index + 1] !== undefined && !argv[index + 1].startsWith('--')) throw new LoopValidationError(`--${name} 不接受值`);
|
|
681
|
+
flags.add(name);
|
|
682
|
+
} else if (spec.values.includes(name)) {
|
|
683
|
+
if (argv[index + 1] === undefined || argv[index + 1].startsWith('--')) throw new LoopValidationError(`--${name} 缺少值`);
|
|
684
|
+
options[name] = argv[index + 1]; index += 1;
|
|
685
|
+
} else throw new LoopValidationError(`未知选项: --${name}`);
|
|
686
|
+
}
|
|
687
|
+
return { command, options, flags };
|
|
688
|
+
}
|
|
689
|
+
function required(options, name) { if (!options[name]) throw new LoopValidationError(`缺少 --${name}`); return options[name]; }
|
|
690
|
+
function expectedRevision(options) {
|
|
691
|
+
if (options['expected-revision'] === undefined) return null;
|
|
692
|
+
const value = Number(options['expected-revision']);
|
|
693
|
+
if (!Number.isInteger(value) || value < 0) throw new LoopValidationError('--expected-revision 必须是非负整数');
|
|
694
|
+
return value;
|
|
695
|
+
}
|
|
696
|
+
function positiveInteger(options, name, fallback) {
|
|
697
|
+
const value = options[name] === undefined ? fallback : Number(options[name]);
|
|
698
|
+
if (!Number.isInteger(value) || value <= 0) throw new LoopValidationError(`--${name} 必须是正整数`);
|
|
699
|
+
return value;
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
function capabilities() {
|
|
703
|
+
return {
|
|
704
|
+
skill: 'run-agent-verify-loop',
|
|
705
|
+
runtime_version: RUNTIME_VERSION,
|
|
706
|
+
providers: ['verify-agent-output', 'embedded'],
|
|
707
|
+
contracts: { task_contract: [1], verification_profile: [1], artifact_ref: [1], review_result: [1], evidence_package: [1], embedded_verification_record: [1], loop_state: [1], reflection_record: [1], convergence_report: [1], improvement_proposal: [1] },
|
|
708
|
+
features: ['journal-recovery', 'revision-lock', 'failure-fuse', 'state-root-unmodified-copy-detection', 'state-root-explicit-adoption', 'skill-drift', 'human-gate', 'terminal-convergence-report', 'evidence-bound-reflection', 'proposed-only-improvement'],
|
|
709
|
+
content_digest: skillContentDigest(),
|
|
710
|
+
};
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
function initialize(options, flags) {
|
|
714
|
+
const contract = readJson(required(options, 'contract'));
|
|
715
|
+
const profile = readJson(required(options, 'profile'));
|
|
716
|
+
const ids = validateContract(contract);
|
|
717
|
+
validateProfile(profile, ids);
|
|
718
|
+
const contentDigest = skillContentDigest();
|
|
719
|
+
validateSkillBinding(contract, contentDigest);
|
|
720
|
+
const provider = required(options, 'provider');
|
|
721
|
+
if (!PROVIDERS.has(provider)) throw new LoopValidationError('provider 必须是 verify-agent-output 或 embedded');
|
|
722
|
+
const stateRoot = resolve(options['state-root'] ?? join(tmpdir(), 'run-agent-verify-loop-state'));
|
|
723
|
+
const loopId = options['loop-id'] ?? randomUUID();
|
|
724
|
+
if (!/^[A-Za-z0-9._-]+$/u.test(loopId)) throw new LoopValidationError('loop-id 非法');
|
|
725
|
+
const workdir = options.workdir ? realpathSync(options.workdir) : null;
|
|
726
|
+
if (provider === 'embedded' && !workdir) throw new LoopValidationError('embedded provider 必须提供 --workdir');
|
|
727
|
+
const contractRepository = typeof contract.environment?.repository === 'string' && contract.environment.repository !== 'none' && existsSync(contract.environment.repository)
|
|
728
|
+
? realpathSync(contract.environment.repository)
|
|
729
|
+
: null;
|
|
730
|
+
if ((workdir && pathInside(stateRoot, workdir) || contractRepository && pathInside(stateRoot, contractRepository)) && !flags.has('allow-repository-state')) throw new LoopValidationError('state root 位于仓库内;需显式授权');
|
|
731
|
+
if (provider === 'embedded' && profile.runtime.network_policy === 'denied' && !flags.has('network-isolated')) throw new LoopValidationError('embedded network_policy=denied 时必须提供 --network-isolated assurance');
|
|
732
|
+
const maxIterations = positiveInteger(options, 'max-iterations', 3);
|
|
733
|
+
const fuse = positiveInteger(options, 'consecutive-identical-signature', 2);
|
|
734
|
+
const onFailure = options['on-failure'] ?? 'retry';
|
|
735
|
+
if (!['retry', 'rollback', 'change_strategy', 'stop'].includes(onFailure)) throw new LoopValidationError('on-failure 非法');
|
|
736
|
+
return withRootLock(stateRoot, () => {
|
|
737
|
+
initializeStateRootIdentity(stateRoot);
|
|
738
|
+
mkdirSync(join(stateRoot, 'loops'), { recursive: true, mode: 0o700 });
|
|
739
|
+
const loopDir = join(stateRoot, 'loops', loopId);
|
|
740
|
+
mkdirSync(loopDir, { recursive: false, mode: 0o700 });
|
|
741
|
+
const now = new Date().toISOString();
|
|
742
|
+
const snapshot = {
|
|
743
|
+
schema_version: 1,
|
|
744
|
+
runtime_version: RUNTIME_VERSION,
|
|
745
|
+
loop_id: loopId,
|
|
746
|
+
goal_ref: options['goal-ref'] ?? null,
|
|
747
|
+
revision: 0,
|
|
748
|
+
state: 'active',
|
|
749
|
+
contract_digest: contract.contract_digest,
|
|
750
|
+
verification_profile_digest: profile.verification_profile_digest,
|
|
751
|
+
provider,
|
|
752
|
+
limits: { max_iterations: maxIterations, consecutive_identical_signature: fuse },
|
|
753
|
+
policy: { on_failure: onFailure, escalation: [] },
|
|
754
|
+
workdir,
|
|
755
|
+
network_isolation_assurance: flags.has('network-isolated') ? 'host_reported' : 'not_required',
|
|
756
|
+
runtime_repository_identity: null,
|
|
757
|
+
executable_identities: Object.fromEntries(Object.entries(profile.runtime.executable_paths).map(([name, path]) => [name, fileIdentity(path)])),
|
|
758
|
+
argv_file_identities: freezeArgvFiles(profile, workdir),
|
|
759
|
+
current_iteration: null,
|
|
760
|
+
iterations: [],
|
|
761
|
+
consumed_verification_run_ids: [],
|
|
762
|
+
human_gate: { required: profile.human_gate !== 'none', status: profile.human_gate === 'none' ? 'not_required' : 'pending' },
|
|
763
|
+
skill_provenance: { name: 'run-agent-verify-loop', version: RUNTIME_VERSION, content_digest: contentDigest },
|
|
764
|
+
reflection_refs: [],
|
|
765
|
+
improvement_proposal_refs: [],
|
|
766
|
+
convergence_report_ref: null,
|
|
767
|
+
terminal: null,
|
|
768
|
+
created_at: now,
|
|
769
|
+
updated_at: now,
|
|
770
|
+
};
|
|
771
|
+
writeNewJson(join(loopDir, 'contract.json'), contract);
|
|
772
|
+
writeNewJson(join(loopDir, 'profile.json'), profile);
|
|
773
|
+
const initialEvent = { schema_version: 1, revision: 0, kind: 'initialized', recorded_at: now, previous_event_digest: null, snapshot };
|
|
774
|
+
initialEvent.event_digest = envelopeDigest(initialEvent, 'event_digest');
|
|
775
|
+
writeFileSync(join(loopDir, 'events.ndjson'), `${canonicalJson(initialEvent)}\n`, { flag: 'wx', mode: 0o600 });
|
|
776
|
+
atomicWriteJson(join(loopDir, 'snapshot.json'), snapshot);
|
|
777
|
+
return { loop_id: loopId, loop_dir: loopDir, revision: 0, state: 'active', provider };
|
|
778
|
+
});
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
function recordReflection(options) {
|
|
782
|
+
const loopDir = realpathSync(required(options, 'loop'));
|
|
783
|
+
const input = readJson(required(options, 'input'));
|
|
784
|
+
return mutateLoop(loopDir, expectedRevision(options), (snapshot) => {
|
|
785
|
+
const record = buildReflection({ input, runDir: loopDir, scope: { contract_digest: snapshot.contract_digest, loop_id: snapshot.loop_id, iteration: snapshot.current_iteration?.index ?? snapshot.iterations.at(-1)?.index ?? null }, skill: snapshot.skill_provenance, parseJsonStrict, canonicalJson, envelopeDigest });
|
|
786
|
+
mkdirSync(join(loopDir, 'reflections'), { recursive: true, mode: 0o700 });
|
|
787
|
+
const ref = `reflections/${record.reflection_id}.json`;
|
|
788
|
+
writeNewJson(join(loopDir, ref), record);
|
|
789
|
+
return { snapshot: { ...snapshot, reflection_refs: [...snapshot.reflection_refs, { reflection_id: record.reflection_id, reflection_digest: record.reflection_digest, ref }] }, kind: 'reflection_recorded' };
|
|
790
|
+
});
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
function convergenceReport(options) {
|
|
794
|
+
const loopDir = realpathSync(required(options, 'loop'));
|
|
795
|
+
stateRootForLoop(loopDir);
|
|
796
|
+
const snapshot = loadLoop(loopDir).snapshot;
|
|
797
|
+
if (!['completed', 'stopped'].includes(snapshot.state) || !snapshot.convergence_report_ref) throw new LoopValidationError('只有 completed/stopped Loop 才有 Convergence Report');
|
|
798
|
+
const report = readJson(join(loopDir, snapshot.convergence_report_ref.ref));
|
|
799
|
+
if (envelopeDigest(report, 'report_digest') !== report.report_digest || report.report_digest !== snapshot.convergence_report_ref.report_digest) throw new LoopValidationError('Convergence Report digest 无效');
|
|
800
|
+
return report;
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
function proposeImprovement(options) {
|
|
804
|
+
const loopDir = realpathSync(required(options, 'loop'));
|
|
805
|
+
const input = readJson(required(options, 'input'));
|
|
806
|
+
const reflectionPath = resolve(loopDir, required(options, 'reflection'));
|
|
807
|
+
if (!pathInside(reflectionPath, loopDir)) throw new LoopValidationError('Reflection 路径越出 loop 目录');
|
|
808
|
+
return mutateLoop(loopDir, expectedRevision(options), (snapshot) => {
|
|
809
|
+
const reflection = readAndValidateReflection(reflectionPath, 'run-agent-verify-loop', parseJsonStrict, envelopeDigest);
|
|
810
|
+
if (!snapshot.reflection_refs.some((item) => item.reflection_digest === reflection.reflection_digest)) throw new LoopValidationError('Reflection 未登记到当前 Loop');
|
|
811
|
+
const proposal = buildProposal({ input, reflections: [reflection], skill: snapshot.skill_provenance, envelopeDigest });
|
|
812
|
+
mkdirSync(join(loopDir, 'proposals'), { recursive: true, mode: 0o700 });
|
|
813
|
+
const ref = `proposals/${proposal.proposal_id}.json`;
|
|
814
|
+
writeNewJson(join(loopDir, ref), proposal);
|
|
815
|
+
return { snapshot: { ...snapshot, improvement_proposal_refs: [...snapshot.improvement_proposal_refs, { proposal_id: proposal.proposal_id, proposal_digest: proposal.proposal_digest, ref }] }, kind: 'improvement_proposed' };
|
|
816
|
+
});
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
function recordArtifact(options) {
|
|
820
|
+
const loopDir = realpathSync(required(options, 'loop'));
|
|
821
|
+
const artifact = readJson(required(options, 'artifact'));
|
|
822
|
+
validateArtifact(artifact);
|
|
823
|
+
return mutateLoop(loopDir, expectedRevision(options), (snapshot) => {
|
|
824
|
+
if (snapshot.state !== 'active' || snapshot.current_iteration) throw new LoopValidationError('当前 Loop 不能登记新 Artifact');
|
|
825
|
+
const index = snapshot.iterations.length + 1;
|
|
826
|
+
if (index > snapshot.limits.max_iterations) throw new LoopValidationError('已达到 max_iterations');
|
|
827
|
+
const verificationRunId = snapshot.provider === 'verify-agent-output' ? required(options, 'verification-run-id') : null;
|
|
828
|
+
let identity = snapshot.runtime_repository_identity;
|
|
829
|
+
if (snapshot.workdir) {
|
|
830
|
+
identity = verifyGitArtifact(artifact, snapshot.workdir, identity);
|
|
831
|
+
verifyProtectedPaths(artifact, readJson(join(loopDir, 'profile.json')), snapshot.workdir);
|
|
832
|
+
}
|
|
833
|
+
return { snapshot: { ...snapshot, runtime_repository_identity: identity, current_iteration: { index, artifact_ref: artifact, verification_run_id: verificationRunId, review_challenge_nonce: randomUUID(), embedded_l0: null, verification_abort: null }, next_action: 'verify' }, kind: 'artifact_recorded' };
|
|
834
|
+
});
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
function runEmbeddedL0(options) {
|
|
838
|
+
const loopDir = realpathSync(required(options, 'loop'));
|
|
839
|
+
return mutateLoop(loopDir, expectedRevision(options), (snapshot) => {
|
|
840
|
+
if (snapshot.provider !== 'embedded' || snapshot.state !== 'active' || !snapshot.current_iteration || snapshot.current_iteration.embedded_l0) throw new LoopValidationError('当前状态不能运行 embedded L0');
|
|
841
|
+
const result = executeEmbeddedChecks(snapshot, loopDir);
|
|
842
|
+
return { snapshot: { ...snapshot, current_iteration: { ...snapshot.current_iteration, embedded_l0: result } }, kind: 'embedded_l0_recorded' };
|
|
843
|
+
});
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
function recordEmbeddedReview(options) {
|
|
847
|
+
const loopDir = realpathSync(required(options, 'loop'));
|
|
848
|
+
const review = readJson(required(options, 'review'));
|
|
849
|
+
const assurance = required(options, 'assurance');
|
|
850
|
+
const reviewerRunId = required(options, 'reviewer-run-id');
|
|
851
|
+
if (!['host_reported', 'user_relayed'].includes(assurance)) throw new LoopValidationError('assurance 非法');
|
|
852
|
+
return mutateLoop(loopDir, expectedRevision(options), (snapshot) => {
|
|
853
|
+
if (snapshot.provider !== 'embedded' || snapshot.state !== 'active' || !snapshot.current_iteration?.embedded_l0) throw new LoopValidationError('当前状态不能登记 embedded review');
|
|
854
|
+
if (reviewerRunId === snapshot.loop_id || reviewerRunId === snapshot.current_iteration.verification_run_id) throw new LoopValidationError('reviewer-run-id 必须与 controller/verification run 隔离');
|
|
855
|
+
const contract = readJson(join(loopDir, 'contract.json'));
|
|
856
|
+
const ids = validateContract(contract);
|
|
857
|
+
validateReview(review, snapshot, ids);
|
|
858
|
+
const safety = review.findings.some((finding) => finding.class === 'safety');
|
|
859
|
+
const outcome = safety ? 'blocked_safety' : review.verdict === 'undecidable' ? 'undecidable' : !snapshot.current_iteration.embedded_l0.passed || review.verdict === 'fail' ? 'fail' : 'pass';
|
|
860
|
+
const record = {
|
|
861
|
+
schema_version: 1,
|
|
862
|
+
record_type: 'embedded_verification_record',
|
|
863
|
+
record_id: randomUUID(),
|
|
864
|
+
loop_id: snapshot.loop_id,
|
|
865
|
+
iteration: snapshot.current_iteration.index,
|
|
866
|
+
contract_digest: snapshot.contract_digest,
|
|
867
|
+
verification_profile_digest: snapshot.verification_profile_digest,
|
|
868
|
+
artifact_ref: snapshot.current_iteration.artifact_ref,
|
|
869
|
+
l0_result: snapshot.current_iteration.embedded_l0,
|
|
870
|
+
l1_result: review,
|
|
871
|
+
independent_context: { assurance, reviewer_run_id: reviewerRunId },
|
|
872
|
+
outcome,
|
|
873
|
+
};
|
|
874
|
+
record.record_digest = envelopeDigest(record, 'record_digest');
|
|
875
|
+
const path = join(loopDir, `embedded-${snapshot.current_iteration.index}.json`);
|
|
876
|
+
let persisted = record;
|
|
877
|
+
if (existsSync(path)) {
|
|
878
|
+
persisted = readJson(path);
|
|
879
|
+
if (envelopeDigest(persisted, 'record_digest') !== persisted.record_digest
|
|
880
|
+
|| persisted.loop_id !== snapshot.loop_id
|
|
881
|
+
|| persisted.iteration !== snapshot.current_iteration.index
|
|
882
|
+
|| canonicalJson(persisted.artifact_ref) !== canonicalJson(snapshot.current_iteration.artifact_ref)
|
|
883
|
+
|| canonicalJson(persisted.l0_result) !== canonicalJson(snapshot.current_iteration.embedded_l0)
|
|
884
|
+
|| canonicalJson(persisted.l1_result) !== canonicalJson(review)) {
|
|
885
|
+
throw new LoopValidationError('已存在的 orphan embedded record 与当前 transition 不兼容');
|
|
886
|
+
}
|
|
887
|
+
} else writeNewJson(path, record);
|
|
888
|
+
return { snapshot: applyOutcome(snapshot, persisted.outcome, { kind: 'embedded', value: persisted }), kind: `embedded_${persisted.outcome}` };
|
|
889
|
+
});
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
function recordEvidence(options) {
|
|
893
|
+
const loopDir = realpathSync(required(options, 'loop'));
|
|
894
|
+
const evidence = readJson(required(options, 'evidence'));
|
|
895
|
+
return mutateLoop(loopDir, expectedRevision(options), (snapshot, stateRoot) => {
|
|
896
|
+
if (snapshot.provider !== 'verify-agent-output' || snapshot.state !== 'active' || !snapshot.current_iteration) throw new LoopValidationError('当前状态不能消费 Evidence');
|
|
897
|
+
validateEvidence(evidence, snapshot);
|
|
898
|
+
if (consumedRunIds(stateRoot).has(evidence.run_id)) throw new LoopValidationError(`Evidence run_id 已被消费: ${evidence.run_id}`);
|
|
899
|
+
const path = join(loopDir, `evidence-${snapshot.current_iteration.index}.json`);
|
|
900
|
+
if (existsSync(path)) {
|
|
901
|
+
if (canonicalJson(readJson(path)) !== canonicalJson(evidence)) throw new LoopValidationError('iteration Evidence 文件已存在且内容不同');
|
|
902
|
+
} else writeNewJson(path, evidence);
|
|
903
|
+
const next = applyOutcome({ ...snapshot, consumed_verification_run_ids: [...snapshot.consumed_verification_run_ids, evidence.run_id] }, evidence.terminal_outcome, { kind: 'evidence', value: evidence });
|
|
904
|
+
return { snapshot: next, kind: `evidence_${evidence.terminal_outcome}` };
|
|
905
|
+
});
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
function recordVerificationAbort(options) {
|
|
909
|
+
const loopDir = realpathSync(required(options, 'loop'));
|
|
910
|
+
const runId = required(options, 'run-id');
|
|
911
|
+
const code = required(options, 'code');
|
|
912
|
+
const diagnosticsRef = required(options, 'diagnostics-ref');
|
|
913
|
+
return mutateLoop(loopDir, expectedRevision(options), (snapshot) => {
|
|
914
|
+
if (snapshot.provider !== 'verify-agent-output' || snapshot.state !== 'active' || snapshot.current_iteration?.verification_run_id !== runId) throw new LoopValidationError('verification abort 与当前 iteration 不匹配');
|
|
915
|
+
return { snapshot: { ...snapshot, state: 'stopped', current_iteration: { ...snapshot.current_iteration, verification_abort: { run_id: runId, code, diagnostics_ref: diagnosticsRef } }, terminal: { kind: 'verification_abort', run_id: runId, code, diagnostics_ref: diagnosticsRef } }, kind: 'verification_abort' };
|
|
916
|
+
});
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
function nextIteration(options) {
|
|
920
|
+
const loopDir = realpathSync(required(options, 'loop'));
|
|
921
|
+
return mutateLoop(loopDir, expectedRevision(options), (snapshot) => {
|
|
922
|
+
if (snapshot.state !== 'active' || snapshot.current_iteration || snapshot.iterations.at(-1)?.outcome !== 'fail') throw new LoopValidationError('当前状态不需要 next');
|
|
923
|
+
return { snapshot: { ...snapshot, next_action: 'record_artifact' }, kind: 'next_iteration_ready' };
|
|
924
|
+
});
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
function resume(options) {
|
|
928
|
+
const loopDir = realpathSync(required(options, 'loop'));
|
|
929
|
+
return mutateLoop(loopDir, expectedRevision(options), (snapshot) => {
|
|
930
|
+
if (snapshot.state !== 'stopped' || snapshot.terminal?.kind !== 'verification_abort' || !snapshot.current_iteration) throw new LoopValidationError('只允许恢复 verification_abort');
|
|
931
|
+
const runId = required(options, 'verification-run-id');
|
|
932
|
+
if (runId === snapshot.current_iteration.verification_run_id) throw new LoopValidationError('resume 必须绑定新的 verification run ID');
|
|
933
|
+
return { snapshot: { ...snapshot, state: 'active', terminal: null, convergence_report_ref: null, current_iteration: { ...snapshot.current_iteration, verification_run_id: runId, verification_abort: null }, next_action: 'verify' }, kind: 'verification_resumed' };
|
|
934
|
+
});
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
function stop(options) {
|
|
938
|
+
const loopDir = realpathSync(required(options, 'loop'));
|
|
939
|
+
return mutateLoop(loopDir, expectedRevision(options), (snapshot) => {
|
|
940
|
+
if (!['active', 'waiting_human'].includes(snapshot.state)) throw new LoopValidationError(`不能停止状态 ${snapshot.state}`);
|
|
941
|
+
return { snapshot: { ...snapshot, state: 'stopped', terminal: { kind: 'controller_stop', reason: options.reason ?? 'explicit stop' } }, kind: 'stopped' };
|
|
942
|
+
});
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
function humanGate(options) {
|
|
946
|
+
const loopDir = realpathSync(required(options, 'loop'));
|
|
947
|
+
const decision = required(options, 'decision');
|
|
948
|
+
if (!['approved', 'rejected'].includes(decision)) throw new LoopValidationError('decision 必须 approved/rejected');
|
|
949
|
+
return mutateLoop(loopDir, expectedRevision(options), (snapshot) => {
|
|
950
|
+
if (snapshot.state !== 'waiting_human' || !snapshot.human_gate.required) throw new LoopValidationError('当前没有 pending H gate');
|
|
951
|
+
if (decision === 'approved') return { snapshot: { ...snapshot, state: 'completed', human_gate: { ...snapshot.human_gate, status: 'approved' }, terminal: { kind: 'completed', outcome: 'pass' } }, kind: 'human_gate_approved' };
|
|
952
|
+
return { snapshot: { ...snapshot, state: 'stopped', human_gate: { ...snapshot.human_gate, status: 'rejected' }, terminal: { kind: 'human_gate_rejected' } }, kind: 'human_gate_rejected' };
|
|
953
|
+
});
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
function status(options) {
|
|
957
|
+
const loopDir = realpathSync(required(options, 'loop'));
|
|
958
|
+
stateRootForLoop(loopDir);
|
|
959
|
+
const loaded = loadLoop(loopDir);
|
|
960
|
+
return { ...loaded.snapshot, recovery_needed: loaded.needsRepair };
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
function validateLoop(options) {
|
|
964
|
+
const loopDir = realpathSync(required(options, 'loop'));
|
|
965
|
+
stateRootForLoop(loopDir);
|
|
966
|
+
const loaded = loadLoop(loopDir);
|
|
967
|
+
const snapshot = loaded.snapshot;
|
|
968
|
+
const contract = readJson(join(loopDir, 'contract.json'));
|
|
969
|
+
const profile = readJson(join(loopDir, 'profile.json'));
|
|
970
|
+
const ids = validateContract(contract);
|
|
971
|
+
validateProfile(profile, ids);
|
|
972
|
+
if (contract.contract_digest !== snapshot.contract_digest || profile.verification_profile_digest !== snapshot.verification_profile_digest) throw new LoopValidationError('冻结 envelope 与 snapshot 不一致');
|
|
973
|
+
for (const iteration of snapshot.iterations) {
|
|
974
|
+
const path = iteration.evidence_digest ? join(loopDir, `evidence-${iteration.index}.json`) : join(loopDir, `embedded-${iteration.index}.json`);
|
|
975
|
+
const value = readJson(path);
|
|
976
|
+
const field = iteration.evidence_digest ? 'evidence_digest' : 'record_digest';
|
|
977
|
+
if (envelopeDigest(value, field) !== value[field] || value[field] !== (iteration.evidence_digest ?? iteration.embedded_record_digest)) throw new LoopValidationError(`iteration ${iteration.index} 摘要无效`);
|
|
978
|
+
if (!iteration.evidence_digest) {
|
|
979
|
+
for (const check of value.l0_result?.checks ?? []) {
|
|
980
|
+
const logPath = join(loopDir, check.log_ref);
|
|
981
|
+
if (!existsSync(logPath) || sha256(readFileSync(logPath)) !== check.log_digest) throw new LoopValidationError(`iteration ${iteration.index} 日志摘要无效: ${check.check_id}`);
|
|
982
|
+
}
|
|
983
|
+
}
|
|
984
|
+
}
|
|
985
|
+
for (const ref of snapshot.reflection_refs ?? []) {
|
|
986
|
+
const value = readAndValidateReflection(join(loopDir, ref.ref), 'run-agent-verify-loop', parseJsonStrict, envelopeDigest);
|
|
987
|
+
if (value.reflection_digest !== ref.reflection_digest) throw new LoopValidationError(`Reflection digest 无效: ${ref.ref}`);
|
|
988
|
+
verifyEvidenceRefs(loopDir, value.evidence_refs, parseJsonStrict, canonicalJson);
|
|
989
|
+
}
|
|
990
|
+
for (const ref of snapshot.improvement_proposal_refs ?? []) {
|
|
991
|
+
const value = readJson(join(loopDir, ref.ref));
|
|
992
|
+
if (value.lifecycle !== 'proposed' || value.target_skill?.name !== 'run-agent-verify-loop' || envelopeDigest(value, 'proposal_digest') !== value.proposal_digest || value.proposal_digest !== ref.proposal_digest) throw new LoopValidationError(`Proposal 无效: ${ref.ref}`);
|
|
993
|
+
}
|
|
994
|
+
if (snapshot.convergence_report_ref) convergenceReport({ loop: loopDir });
|
|
995
|
+
return { valid: true, loop_id: snapshot.loop_id, revision: snapshot.revision, state: snapshot.state, iterations: snapshot.iterations.length, recovery_needed: loaded.needsRepair };
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
function doctor(options) {
|
|
999
|
+
const loopDir = realpathSync(required(options, 'loop'));
|
|
1000
|
+
const loaded = loadLoop(loopDir);
|
|
1001
|
+
const stateRoot = realpathSync(resolve(loopDir, '..', '..'));
|
|
1002
|
+
let identityError = null;
|
|
1003
|
+
try { stateRootForLoop(loopDir); } catch (error) { identityError = error instanceof Error ? error.message : String(error); }
|
|
1004
|
+
const currentDigest = skillContentDigest();
|
|
1005
|
+
const drift = currentDigest !== loaded.snapshot.skill_provenance.content_digest;
|
|
1006
|
+
const lockPresent = existsSync(join(stateRoot, '.loop-runtime.lock'));
|
|
1007
|
+
const findings = identityError ? ['state_root_identity_invalid'] : [];
|
|
1008
|
+
return { healthy: !loaded.needsRepair && !lockPresent && !drift && !identityError, loop_id: loaded.snapshot.loop_id, revision: loaded.snapshot.revision, snapshot_matches_journal: !loaded.needsRepair, lock_present: lockPresent, skill_drift: drift, frozen_content_digest: loaded.snapshot.skill_provenance.content_digest, current_content_digest: currentDigest, state_root_identity_valid: !identityError, findings, diagnostics: identityError, recovery_command: identityError ? `adopt-root --state-root ${stateRoot}` : null };
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
export function main(argv = process.argv.slice(2)) {
|
|
1012
|
+
const { command, options, flags } = parseCli(argv);
|
|
1013
|
+
switch (command) {
|
|
1014
|
+
case 'capabilities': return capabilities();
|
|
1015
|
+
case 'adopt-root': return adoptStateRoot(options);
|
|
1016
|
+
case 'init': return initialize(options, flags);
|
|
1017
|
+
case 'record-artifact': return recordArtifact(options);
|
|
1018
|
+
case 'run-embedded-l0': return runEmbeddedL0(options);
|
|
1019
|
+
case 'record-embedded-review': return recordEmbeddedReview(options);
|
|
1020
|
+
case 'record-evidence': return recordEvidence(options);
|
|
1021
|
+
case 'record-verification-abort': return recordVerificationAbort(options);
|
|
1022
|
+
case 'next': return nextIteration(options);
|
|
1023
|
+
case 'resume': return resume(options);
|
|
1024
|
+
case 'stop': return stop(options);
|
|
1025
|
+
case 'human-gate': return humanGate(options);
|
|
1026
|
+
case 'record-reflection': return recordReflection(options);
|
|
1027
|
+
case 'convergence-report': return convergenceReport(options);
|
|
1028
|
+
case 'propose-improvement': return proposeImprovement(options);
|
|
1029
|
+
case 'status': return status(options);
|
|
1030
|
+
case 'inspect': return status(options);
|
|
1031
|
+
case 'validate': return validateLoop(options);
|
|
1032
|
+
case 'doctor': return doctor(options);
|
|
1033
|
+
default: throw new LoopValidationError('命令必须是 capabilities/adopt-root/init/record-artifact/run-embedded-l0/record-embedded-review/record-evidence/record-verification-abort/next/resume/stop/human-gate/record-reflection/convergence-report/propose-improvement/status/inspect/validate/doctor');
|
|
1034
|
+
}
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
export function isCliEntry(metaUrl, argv1) {
|
|
1038
|
+
if (!argv1) return false;
|
|
1039
|
+
try { return realpathSync(fileURLToPath(metaUrl)) === realpathSync(argv1); }
|
|
1040
|
+
catch { return pathToFileURL(resolve(argv1)).href === metaUrl; }
|
|
1041
|
+
}
|
|
1042
|
+
|
|
1043
|
+
export function runCli(argv = process.argv.slice(2)) {
|
|
1044
|
+
try {
|
|
1045
|
+
process.stdout.write(`${JSON.stringify(main(argv), null, 2)}\n`);
|
|
1046
|
+
return 0;
|
|
1047
|
+
}
|
|
1048
|
+
catch (error) {
|
|
1049
|
+
const code = error instanceof LoopOperationalAbort ? error.code : error instanceof LoopValidationError ? 'invalid_input' : 'runtime_error';
|
|
1050
|
+
process.stderr.write(`${JSON.stringify({ error: code, message: error instanceof Error ? error.message : String(error) })}\n`);
|
|
1051
|
+
return error instanceof LoopValidationError ? 2 : 3;
|
|
1052
|
+
}
|
|
1053
|
+
}
|
|
1054
|
+
|
|
1055
|
+
|
|
1056
|
+
if (isCliEntry(import.meta.url, process.argv[1])) process.exitCode = runCli();
|