cli-aimlock 7.0.18 → 7.0.25

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.
@@ -0,0 +1,1322 @@
1
+ // Aimlock router: classify demand and order only server-resolved skill matches.
2
+
3
+ const CHAIN_KINDS = Object.freeze([
4
+ "code-risky", "calculator", "page-new", "merge", "probe-only", "chat",
5
+ ]);
6
+
7
+ const CLASSIFY_RULES = [
8
+ { priority: 60, pattern: /计算器|公式|测算|对账|指标固化|在线测算|计算工具/i, product: "calculator", risk: "low", chain: "calculator" },
9
+ { priority: 50, pattern: /分支合并|合并代码|merge|冲突解决/i, product: "merge", risk: "medium", chain: "merge" },
10
+ { priority: 40, pattern: /修改|更新|重构|修复|bug|调整|优化|改造|新增/i, product: "code", risk: "low", chain: "code-risky" },
11
+ { priority: 30, pattern: /新建页面|新建工具|创建页面|创建工具/i, product: "page", risk: "low", chain: "page-new" },
12
+ { priority: 20, pattern: /只读分析|分析代码|代码审查|review/i, product: "analysis", risk: "low", chain: "probe-only" },
13
+ { priority: 10, pattern: /纯查询|查看|读取|了解|咨询|闲聊/i, product: "none", risk: "low", chain: "chat" },
14
+ ];
15
+ const HIGH_RISK_PATTERNS = [/生产数据|支付|用户隐私|财务|密码|密钥|线上环境|production/i];
16
+ const RISK_LEVEL = Object.freeze({ low: 0, medium: 1, high: 2 });
17
+
18
+ function strongestRisk(...values) {
19
+ return values.filter((value) => value in RISK_LEVEL)
20
+ .sort((left, right) => RISK_LEVEL[right] - RISK_LEVEL[left])[0] ?? "low";
21
+ }
22
+
23
+ function classifyDemand(input) {
24
+ const goal = String(input?.goal ?? "").trim();
25
+ if (!goal) return { findings: [{ severity: "P0", ruleId: "CLASSIFY_GOAL", entityRef: "input.goal", message: "goal is required for classification", evidence: { example: "修改税率常量" } }] };
26
+ const declaredRisk = String(input?.risk ?? "").trim();
27
+ if (declaredRisk && !(declaredRisk in RISK_LEVEL)) {
28
+ return { findings: [{ severity: "P0", ruleId: "CLASSIFY_RISK", entityRef: "input.risk", message: "risk must be low, medium, or high", evidence: { example: "high" } }] };
29
+ }
30
+ const rule = CLASSIFY_RULES.filter((candidate) => candidate.pattern.test(goal))
31
+ .sort((left, right) => right.priority - left.priority)[0];
32
+ if (rule) {
33
+ const detectedRisk = HIGH_RISK_PATTERNS.some((pattern) => pattern.test(goal)) ? "high" : rule.risk;
34
+ const risk = strongestRisk(detectedRisk, declaredRisk, input?.modelClassification?.risk);
35
+ return { product: rule.product, risk, chain: rule.chain, confidence: 1, source: "rule" };
36
+ }
37
+ const assisted = input?.modelClassification;
38
+ const products = new Set(["calculator", "merge", "none", "analysis", "page", "code"]);
39
+ const risks = new Set(["low", "medium", "high"]);
40
+ const chains = new Set(CHAIN_KINDS);
41
+ if (assisted && typeof assisted === "object" && Number(assisted.confidence) >= 0.75
42
+ && products.has(assisted.product) && risks.has(assisted.risk) && chains.has(assisted.chain)) {
43
+ return { product: assisted.product, risk: strongestRisk(declaredRisk, assisted.risk), chain: assisted.chain,
44
+ confidence: Number(assisted.confidence), source: "model-assisted" };
45
+ }
46
+ return { product: null, risk: "unknown", chain: null, confidence: 0, source: "needs-clarification",
47
+ question: "请描述您想实现的产物类型?", options: ["计算工具", "代码修改", "新建页面", "分支合并", "只读分析"] };
48
+ }
49
+
50
+ function resolvedStepIds(input) {
51
+ if (!Array.isArray(input?.steps) || input.steps.some((step) => typeof step !== "string" || !step.trim())) {
52
+ return { findings: [{ severity: "P0", ruleId: "CHAIN_STEPS", entityRef: "input.steps", message: "steps must be the server-resolved skill ids", evidence: { example: ["blueprint", "validator"] } }] };
53
+ }
54
+ const steps = [...new Set(input.steps.map((step) => step.trim()))];
55
+ if (steps.length !== input.steps.length) {
56
+ return { findings: [{ severity: "P0", ruleId: "CHAIN_STEPS_DUPLICATE", entityRef: "input.steps", message: "steps must be unique", evidence: { example: steps } }] };
57
+ }
58
+ return { steps };
59
+ }
60
+
61
+ function buildChainPlan(input) {
62
+ const chain = String(input?.chain ?? "").trim();
63
+ if (!CHAIN_KINDS.includes(chain)) {
64
+ return { findings: [{ severity: "P0", ruleId: "CHAIN_KIND", entityRef: "input.chain", message: `unknown chain: ${chain}`, evidence: { example: "code-risky" } }] };
65
+ }
66
+ const resolved = resolvedStepIds(input);
67
+ if (resolved.findings) return resolved;
68
+ const steps = resolved.steps.flatMap((step) => step === "swarm"
69
+ ? ["coordinator.conflict-scan", "swarm"] : [step]);
70
+ const risk = String(input?.risk ?? "").trim();
71
+ if (!["low", "medium", "high"].includes(risk)) {
72
+ return { findings: [{ severity: "P0", ruleId: "CHAIN_RISK", entityRef: "input.risk", message: "risk must be low, medium, or high", evidence: { example: "medium" } }] };
73
+ }
74
+ if (risk === "high" && !steps.includes("validator")) {
75
+ return { findings: [{ severity: "P0", ruleId: "HIGH_RISK_VALIDATOR", entityRef: "input.steps", message: "high-risk work requires a server-resolved validator step", evidence: { example: ["validator"] } }] };
76
+ }
77
+ const completed = Array.isArray(input?.completed) ? input.completed : [];
78
+ const expected = steps.slice(0, completed.length);
79
+ if (new Set(completed).size !== completed.length
80
+ || completed.some((step, index) => step !== expected[index])) {
81
+ return { findings: [{ severity: "P0", ruleId: "CHAIN_ORDER", entityRef: "input.completed", message: "completed must be an exact chain prefix", evidence: { example: expected } }] };
82
+ }
83
+ const current = steps[completed.length] ?? null;
84
+ return { chain: steps, chainKind: chain, ready: current ? [current] : [], current,
85
+ blocked: current ? steps.slice(completed.length + 1).map((step) => ({ step, waitingFor: [current] })) : [],
86
+ totalSteps: steps.length, completedSteps: completed.length, risk };
87
+ }
88
+
89
+ function submitFeedback(input) {
90
+ const feedbackId = String(input?.feedbackId ?? "").trim();
91
+ const fromSkill = String(input?.fromSkill ?? "").trim();
92
+ const toSkill = String(input?.toSkill ?? "").trim();
93
+ const reason = String(input?.reason ?? "").trim();
94
+ const findings = [];
95
+ if (!feedbackId) findings.push({ severity: "P0", ruleId: "FEEDBACK_ID", entityRef: "input.feedbackId", message: "feedbackId is required", evidence: { example: "route-feedback-1" } });
96
+ if (!fromSkill) findings.push({ severity: "P0", ruleId: "FEEDBACK_FROM", entityRef: "input.fromSkill", message: "fromSkill is required", evidence: { example: "aimlock" } });
97
+ if (!toSkill) findings.push({ severity: "P0", ruleId: "FEEDBACK_TO", entityRef: "input.toSkill", message: "toSkill is required", evidence: { example: "validator" } });
98
+ if (!reason) findings.push({ severity: "P0", ruleId: "FEEDBACK_REASON", entityRef: "input.reason", message: "reason is required", evidence: { example: "routing repair" } });
99
+ if (findings.length) return { findings };
100
+ return { recorded: true, applied: false, persistenceRequired: true, feedbackId,
101
+ record: { feedbackId, fromSkill, toSkill, reason, demand: String(input?.demand ?? "").trim() } };
102
+ }
103
+
104
+ function chainStatus(input) {
105
+ const chainId = String(input?.chainId ?? "").trim();
106
+ const steps = Array.isArray(input?.steps) ? input.steps : [];
107
+ const completed = Array.isArray(input?.completed) ? input.completed : [];
108
+ if (!chainId || steps.length === 0 || new Set(steps).size !== steps.length
109
+ || completed.length > steps.length || completed.some((step, index) => step !== steps[index])) {
110
+ return { findings: [{ severity: "P0", ruleId: "CHAIN_STATUS_INVALID", entityRef: "input", message: "chainId is required and completed must be an exact prefix of unique steps", evidence: { example: { chainId: "chain-1", steps: ["validator"], completed: [] } } }] };
111
+ }
112
+ const current = steps[completed.length] ?? null;
113
+ return { chainId, steps, completed, current, totalSteps: steps.length,
114
+ completedSteps: completed.length, isComplete: completed.length === steps.length };
115
+ }
116
+
117
+ export { CHAIN_KINDS, CLASSIFY_RULES, HIGH_RISK_PATTERNS, classifyDemand, buildChainPlan, submitFeedback, chainStatus };
118
+ import { createHash } from "node:crypto";
119
+
120
+ // aimlock runtime v7.0.0 — 统一路由入口 + 变更门禁。Self-contained after build concat.
121
+ const LEGACY_REQUEST_SCHEMA = "aimlock.skill.request/1.0";
122
+ const LEGACY_RESPONSE_SCHEMA = "aimlock.skill.response/1.0";
123
+ const REQUEST_SCHEMA = "aimlock.skill.request/1.1";
124
+ const RESPONSE_SCHEMA = "aimlock.skill.response/1.1";
125
+ const ERROR_SCHEMA = "aimlock.skill.error/1.0";
126
+ const CONTRACT_SCHEMA = "aimlock.scope-contract/1.0";
127
+ const COMPILER_NAME = "aimlock";
128
+ const COMPILER_VERSION = "v7.0.25";
129
+ const KEEP_ALIVE_SECONDS = 90;
130
+ const KEEP_ALIVE_MESSAGE = "智能目标持续执行中,请勿关闭!";
131
+ const BYPASS_LINE_BUDGET = 500;
132
+ const LOCK_LINE_BUDGET = 500;
133
+ const PROBE_LINE_BUDGET = 500;
134
+ const PROBE_FILE_BUDGET = 3;
135
+ const TEST_EVIDENCE_SCHEMA = "cli.tax.test-evidence/1.0";
136
+ const SNAPSHOT_RECEIPT_SCHEMA = "aimlock.snapshot-receipt/1.0";
137
+ const SHA256_PATTERN = /^[0-9a-f]{64}$/;
138
+ const SNAPSHOT_IDENTIFIER_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
139
+
140
+ const OPERATIONS = [
141
+ "capabilities", "help", "intake", "classify", "scope-contract", "skill-route",
142
+ "propose-nodes", "accept-nodes", "snapshot-plan", "mutate-gate",
143
+ "continuity-check", "interrupt", "keep-alive", "delivery-doc", "validate-json",
144
+ "snapshot-verify", "run-status",
145
+ "chain-plan", "chain-status", "feedback",
146
+ ];
147
+ const PURE_OPERATIONS = new Set(OPERATIONS);
148
+ const OPERATION_CATALOG = Object.freeze(OPERATIONS.map((operation) => ({ operation, summary: operation })));
149
+
150
+ const OPERATION_SCHEMAS = Object.freeze({
151
+ "classify": {
152
+ input: {
153
+ type: "object",
154
+ required: ["goal", "targetFiles", "estimatedChangedLines", "difficulty", "risk",
155
+ "crossModule", "needParallel", "explicitAimlockRequested"],
156
+ properties: {
157
+ goal: { type: "string", minLength: 1 },
158
+ targetFiles: { type: "array", minItems: 1, items: { type: "string" } },
159
+ estimatedChangedLines: { type: "number", minimum: 0 },
160
+ difficulty: { type: "string", enum: ["low", "medium", "high"] },
161
+ risk: { type: "string", enum: ["low", "medium", "high"] },
162
+ crossModule: { type: "boolean" },
163
+ needParallel: { type: "boolean" },
164
+ explicitAimlockRequested: { type: "boolean" },
165
+ modelClassification: {
166
+ type: "object", optional: true,
167
+ required: ["product", "risk", "chain", "confidence"],
168
+ properties: {
169
+ product: { type: "string", enum: ["calculator", "merge", "none", "analysis", "page", "code"] },
170
+ risk: { type: "string", enum: ["low", "medium", "high"] },
171
+ chain: { type: "string", enum: CHAIN_KINDS },
172
+ confidence: { type: "number", minimum: 0, maximum: 1 },
173
+ },
174
+ },
175
+ },
176
+ },
177
+ output: {
178
+ type: "object",
179
+ properties: {
180
+ mode: { type: "string", enum: ["bypass", "lock", "probe", "swarm"] },
181
+ useAimlock: { type: "boolean" },
182
+ explicitAimlockRequested: { type: "boolean" },
183
+ friendlyNotice: { type: "object", optional: true },
184
+ product: { type: ["string", "null"] },
185
+ risk: { type: "string" },
186
+ chain: { type: ["string", "null"] },
187
+ confidence: { type: "number" },
188
+ classificationSource: { type: "string" },
189
+ question: { type: "string", optional: true },
190
+ options: { type: "array", items: { type: "string" }, optional: true },
191
+ },
192
+ },
193
+ },
194
+ "scope-contract": {
195
+ input: {
196
+ type: "object",
197
+ required: ["contract"],
198
+ properties: {
199
+ contract: {
200
+ type: "object",
201
+ required: ["allowedPaths", "maxChangedLines", "allowNewFiles", "allowDeleteFiles"],
202
+ properties: {
203
+ allowedPaths: { type: "array", items: { type: "string" }, description: "non-empty array of path prefixes in scope" },
204
+ forbiddenPaths: { type: "array", items: { type: "string" }, optional: true, description: "path prefixes forbidden by contract" },
205
+ maxChangedLines: { type: "number", description: "budget ceiling, must be > 0" },
206
+ allowNewFiles: { type: "boolean" },
207
+ allowDeleteFiles: { type: "boolean" },
208
+ },
209
+ },
210
+ },
211
+ },
212
+ output: {
213
+ type: "object",
214
+ properties: {
215
+ contract: { type: "object", description: "validated scope-contract object with schemaVersion" },
216
+ },
217
+ },
218
+ },
219
+ "skill-route": {
220
+ input: {
221
+ type: "object",
222
+ required: ["mode", "goalKind", "risk", "hasBlueprint", "contractUnclear", "hasArchitectureContract",
223
+ "newProject", "requiresConfirmation", "requiresCalculator", "requiresMerge",
224
+ "requiresValidation", "serverResolvedSkills"],
225
+ properties: {
226
+ mode: { type: "string", enum: ["bypass", "lock", "probe", "swarm"] },
227
+ goalKind: { type: "string", enum: ["code", "calculator", "mixed", "docs"] },
228
+ risk: { type: "string", enum: ["low", "medium", "high"] },
229
+ hasBlueprint: { type: "boolean" },
230
+ contractUnclear: { type: "boolean" },
231
+ hasArchitectureContract: { type: "boolean" },
232
+ newProject: { type: "boolean" },
233
+ requiresConfirmation: { type: "boolean" },
234
+ requiresCalculator: { type: "boolean" },
235
+ requiresMerge: { type: "boolean" },
236
+ requiresValidation: { type: "boolean" },
237
+ serverResolvedSkills: { type: "array", description: "server-authoritative matched skills only" },
238
+ userSpecifiedSkills: { type: "array", items: { type: "string" }, optional: true },
239
+ },
240
+ },
241
+ output: {
242
+ type: "object",
243
+ properties: {
244
+ hops: { type: "array", description: "routing decisions per skill" },
245
+ },
246
+ },
247
+ },
248
+ "mutate-gate": {
249
+ input: {
250
+ type: "object",
251
+ required: ["accepted", "receipt", "contract", "nodes"],
252
+ properties: {
253
+ accepted: { type: "boolean", description: "must be true to proceed" },
254
+ receipt: { type: "object", description: "binding receipt returned by snapshot-verify" },
255
+ nodes: { type: "array", minItems: 1, description: "non-empty array of Node objects" },
256
+ contract: { type: "object", description: "scope-contract object" },
257
+ },
258
+ },
259
+ output: {
260
+ type: "object",
261
+ properties: {
262
+ allowed: { type: "boolean" },
263
+ snapshotId: { type: "string" },
264
+ },
265
+ },
266
+ },
267
+ "continuity-check": {
268
+ input: {
269
+ type: "object",
270
+ required: ["changedLines", "maxChangedLines", "testsPassed", "omissionScanDone"],
271
+ properties: {
272
+ changedLines: { type: "number", description: "actual changed lines count" },
273
+ maxChangedLines: { type: "number", description: "contract budget ceiling" },
274
+ testsPassed: { type: "boolean" },
275
+ omissionScanDone: { type: "boolean" },
276
+ testEvidence: { type: "array", optional: true, description: "array of TestEvidence objects" },
277
+ changedNodes: {
278
+ type: "array",
279
+ optional: true,
280
+ description: "per-node changed lines for expansion check",
281
+ items: {
282
+ type: "object",
283
+ required: ["path", "diffType", "changedLines"],
284
+ properties: {
285
+ path: { type: "string" },
286
+ diffType: { type: "string", enum: ["added", "modified", "deleted"] },
287
+ changedLines: { type: "number" },
288
+ },
289
+ },
290
+ },
291
+ },
292
+ },
293
+ output: {
294
+ type: "object",
295
+ properties: {
296
+ trafficLight: { type: "string", enum: ["green", "yellow", "red"] },
297
+ reason: { type: "string" },
298
+ },
299
+ },
300
+ },
301
+ "propose-nodes": {
302
+ input: {
303
+ type: "object",
304
+ required: ["contract", "nodes"],
305
+ properties: {
306
+ contract: { type: "object", description: "scope-contract object" },
307
+ nodes: { type: "array", description: "array of Node objects to propose" },
308
+ },
309
+ },
310
+ output: {
311
+ type: "object",
312
+ properties: {
313
+ nodes: { type: "array" },
314
+ estimatedSum: { type: "number" },
315
+ contract: { type: "object" },
316
+ },
317
+ },
318
+ },
319
+ "accept-nodes": {
320
+ input: {
321
+ type: "object",
322
+ required: ["contract", "nodes"],
323
+ properties: {
324
+ contract: { type: "object", description: "scope-contract object" },
325
+ nodes: { type: "array", description: "array of Node objects to accept" },
326
+ conflicts: { type: "array", optional: true, description: "conflict objects to resolve" },
327
+ },
328
+ },
329
+ output: {
330
+ type: "object",
331
+ properties: {
332
+ autoAccept: { type: "boolean" },
333
+ escalate: { type: "boolean" },
334
+ nodes: { type: "array" },
335
+ },
336
+ },
337
+ },
338
+ "snapshot-plan": {
339
+ input: {
340
+ type: "object",
341
+ required: ["runId", "contract", "nodes"],
342
+ properties: {
343
+ runId: { type: "string" },
344
+ contract: { type: "object", description: "accepted scope-contract object" },
345
+ nodes: { type: "array", minItems: 1, description: "accepted modification nodes" },
346
+ },
347
+ },
348
+ output: {
349
+ type: "object",
350
+ properties: {
351
+ snapshot: { type: "object", description: "snapshot metadata including snapshotId and paths" },
352
+ },
353
+ },
354
+ },
355
+ "intake": {
356
+ input: {
357
+ type: "object",
358
+ properties: {
359
+ answers: {
360
+ type: "array",
361
+ optional: true,
362
+ description: "batch answers: array of {id, answer} or {question, answer}",
363
+ items: {
364
+ type: "object",
365
+ oneOf: [
366
+ { properties: { id: { type: "string" }, answer: { type: "string" } }, required: ["id", "answer"] },
367
+ { properties: { question: { type: "string" }, answer: { type: "string" } }, required: ["question", "answer"] },
368
+ ],
369
+ },
370
+ },
371
+ },
372
+ },
373
+ output: {
374
+ type: "object",
375
+ properties: {
376
+ answers: { type: "object", description: "map of question id → answer value" },
377
+ questions: { type: "array", description: "list of intake questions (when no answers provided)" },
378
+ },
379
+ },
380
+ },
381
+ "snapshot-verify": {
382
+ input: {
383
+ type: "object",
384
+ required: ["snapshotId", "contract", "nodes", "files"],
385
+ properties: {
386
+ snapshotId: { type: "string" },
387
+ contract: { type: "object", description: "accepted scope-contract object" },
388
+ nodes: { type: "array", minItems: 1, description: "accepted modification nodes" },
389
+ files: {
390
+ type: "array", minItems: 1,
391
+ items: {
392
+ type: "object", required: ["path", "sourceHash", "snapshotHash"],
393
+ properties: {
394
+ path: { type: "string" },
395
+ sourceHash: { type: "string", pattern: "^[0-9a-f]{64}$" },
396
+ snapshotHash: { type: "string", pattern: "^[0-9a-f]{64}$" },
397
+ },
398
+ },
399
+ },
400
+ },
401
+ },
402
+ output: {
403
+ type: "object",
404
+ properties: {
405
+ verified: { type: "boolean" },
406
+ snapshotId: { type: "string" },
407
+ fileCount: { type: "number" },
408
+ receipt: { type: "object", description: "contract/node/path bound snapshot receipt" },
409
+ },
410
+ },
411
+ },
412
+ "run-status": {
413
+ input: {
414
+ type: "object",
415
+ required: ["runId"],
416
+ properties: {
417
+ runId: { type: "string" },
418
+ state: {
419
+ type: "object", optional: true,
420
+ required: ["status", "currentPhase", "history"],
421
+ properties: {
422
+ status: { type: "string", enum: ["running", "blocked", "complete"] },
423
+ currentPhase: { type: "string" },
424
+ history: { type: "array" },
425
+ },
426
+ },
427
+ },
428
+ },
429
+ output: {
430
+ type: "object",
431
+ properties: {
432
+ runId: { type: "string" },
433
+ known: { type: "boolean" },
434
+ run: { type: "object" },
435
+ boundary: { type: "string" },
436
+ },
437
+ },
438
+ },
439
+ "chain-plan": {
440
+ input: {
441
+ type: "object",
442
+ required: ["chain", "risk", "steps"],
443
+ properties: {
444
+ chain: { type: "string", enum: ["code-risky", "calculator", "page-new", "merge", "probe-only", "chat"] },
445
+ risk: { type: "string", enum: ["low", "medium", "high"] },
446
+ steps: { type: "array", items: { type: "string" } },
447
+ completed: { type: "array", items: { type: "string" }, optional: true },
448
+ },
449
+ },
450
+ output: {
451
+ type: "object",
452
+ properties: {
453
+ chain: { type: "array", items: { type: "string" } },
454
+ ready: { type: "array" },
455
+ blocked: { type: "array" },
456
+ current: { type: ["string", "null"] },
457
+ totalSteps: { type: "number" },
458
+ completedSteps: { type: "number" },
459
+ },
460
+ },
461
+ },
462
+ "chain-status": {
463
+ input: {
464
+ type: "object",
465
+ required: ["chainId", "steps", "completed"],
466
+ properties: {
467
+ chainId: { type: "string" },
468
+ steps: { type: "array", items: { type: "string" } },
469
+ completed: { type: "array", items: { type: "string" } },
470
+ },
471
+ },
472
+ output: {
473
+ type: "object",
474
+ properties: {
475
+ chainId: { type: "string" },
476
+ current: { type: "string" },
477
+ isComplete: { type: "boolean" },
478
+ },
479
+ },
480
+ },
481
+ "feedback": {
482
+ input: {
483
+ type: "object",
484
+ required: ["feedbackId", "fromSkill", "toSkill", "reason"],
485
+ properties: {
486
+ feedbackId: { type: "string" },
487
+ fromSkill: { type: "string" },
488
+ toSkill: { type: "string" },
489
+ reason: { type: "string" },
490
+ demand: { type: "string", optional: true },
491
+ },
492
+ },
493
+ output: {
494
+ type: "object",
495
+ properties: {
496
+ recorded: { type: "boolean" },
497
+ applied: { type: "boolean" },
498
+ persistenceRequired: { type: "boolean" },
499
+ feedbackId: { type: "string" },
500
+ record: { type: "object" },
501
+ },
502
+ },
503
+ },
504
+ "capabilities": {
505
+ input: { type: "object", properties: {} },
506
+ output: {
507
+ type: "object",
508
+ properties: {
509
+ capabilities: { type: "object", description: "full capabilities object including operationSchemas" },
510
+ skill: { type: "object" },
511
+ firstUseNotice: { type: "object" },
512
+ },
513
+ },
514
+ },
515
+ "help": {
516
+ input: { type: "object", properties: {} },
517
+ output: {
518
+ type: "object",
519
+ properties: {
520
+ help: { type: "object", description: "help text and operation catalog with operationSchemas" },
521
+ },
522
+ },
523
+ },
524
+ "validate-json": {
525
+ input: {
526
+ type: "object",
527
+ required: ["project"],
528
+ properties: {
529
+ project: {
530
+ type: "object",
531
+ required: ["goal", "mode", "tasks"],
532
+ properties: {
533
+ goal: { type: "string" },
534
+ mode: { type: "string", enum: ["bypass", "lock", "probe", "swarm"] },
535
+ contract: { type: "object", optional: true },
536
+ tasks: { type: "array" },
537
+ },
538
+ },
539
+ },
540
+ },
541
+ output: {
542
+ type: "object",
543
+ properties: {
544
+ valid: { type: "boolean" },
545
+ },
546
+ },
547
+ },
548
+ "interrupt": {
549
+ input: {
550
+ type: "object",
551
+ required: ["forceStop", "isStatusQuery", "related"],
552
+ properties: {
553
+ forceStop: { type: "boolean" },
554
+ isStatusQuery: { type: "boolean" },
555
+ related: { type: "boolean" },
556
+ },
557
+ },
558
+ output: {
559
+ type: "object",
560
+ properties: {
561
+ kind: { type: "string", enum: ["stop", "status", "fuse", "spawn"] },
562
+ action: { type: "string" },
563
+ },
564
+ },
565
+ },
566
+ "keep-alive": {
567
+ input: {
568
+ type: "object",
569
+ required: ["goalComplete"],
570
+ properties: {
571
+ goalComplete: { type: "boolean" },
572
+ },
573
+ },
574
+ output: {
575
+ type: "object",
576
+ properties: {
577
+ armed: { type: "boolean" },
578
+ intervalSeconds: { type: "number" },
579
+ mode: { type: "string", enum: ["protocol"] },
580
+ callerTimerRequired: { type: "boolean" },
581
+ message: { type: "string", optional: true },
582
+ note: { type: "string" },
583
+ },
584
+ },
585
+ },
586
+ "delivery-doc": {
587
+ input: {
588
+ type: "object",
589
+ required: ["userConfirmed"],
590
+ properties: {
591
+ userConfirmed: { type: "boolean" },
592
+ },
593
+ },
594
+ output: {
595
+ type: "object",
596
+ properties: {
597
+ required: { type: "boolean" },
598
+ skip: { type: "boolean" },
599
+ },
600
+ },
601
+ },
602
+ });
603
+
604
+ const GOAL_KINDS = new Set(["code", "calculator", "mixed", "docs"]);
605
+ const MODES = new Set(["bypass", "lock", "probe", "swarm"]);
606
+ const DIFFICULTIES = new Set(["low", "medium", "high"]);
607
+ const RISKS = new Set(["low", "medium", "high"]);
608
+
609
+ function text(value) { return String(value ?? ""); }
610
+ function isObject(value) {
611
+ return value !== null && typeof value === "object" && !Array.isArray(value);
612
+ }
613
+ function finding(severity, ruleId, entityRef, message, evidence = {}) {
614
+ return { severity, ruleId, entityRef, message, evidence: { example: evidence.example ?? evidence } };
615
+ }
616
+ function okResponse(requestId, payload) {
617
+ return { schemaVersion: RESPONSE_SCHEMA, requestId, status: "succeeded", ...payload };
618
+ }
619
+ function blockedResponse(requestId, findings) {
620
+ return {
621
+ schemaVersion: RESPONSE_SCHEMA, requestId, status: "blocked",
622
+ validation: { valid: false, guarantee: "blocked", findings },
623
+ };
624
+ }
625
+ function failed(requestId, code, message) {
626
+ return {
627
+ schemaVersion: RESPONSE_SCHEMA, requestId, status: "failed", errorSchema: ERROR_SCHEMA,
628
+ error: { code, message },
629
+ };
630
+ }
631
+
632
+ function requireText(input, key) {
633
+ const value = text(input?.[key]).trim();
634
+ if (!value) return { error: finding("P0", "REQUIRED_FIELD", `input.${key}`, `${key} is required`, { example: `"string value"` }) };
635
+ return { value };
636
+ }
637
+ function requireBoolean(input, key) {
638
+ if (typeof input?.[key] !== "boolean") {
639
+ return { error: finding("P0", "REQUIRED_FIELD", `input.${key}`, `${key} must be boolean`, { example: true }) };
640
+ }
641
+ return { value: input[key] };
642
+ }
643
+ function requireNumber(input, key) {
644
+ if (typeof input?.[key] !== "number" || !Number.isFinite(input[key]) || input[key] < 0) {
645
+ return { error: finding("P0", "REQUIRED_FIELD", `input.${key}`, `${key} must be a finite number >= 0`, { example: 50 }) };
646
+ }
647
+ return { value: input[key] };
648
+ }
649
+ function requireStringArray(input, key) {
650
+ if (!Array.isArray(input?.[key]) || input[key].some((item) => typeof item !== "string")) {
651
+ return { error: finding("P0", "REQUIRED_FIELD", `input.${key}`, `${key} must be a string array`, { example: ["src/"] }) };
652
+ }
653
+ return { value: input[key] };
654
+ }
655
+ function requireChoice(input, key, choices) {
656
+ const value = text(input?.[key]).trim();
657
+ if (!choices.has(value)) {
658
+ return { error: finding("P0", "REQUIRED_FIELD", `input.${key}`, `${key} has an invalid value`, { example: [...choices][0] }) };
659
+ }
660
+ return { value };
661
+ }
662
+ function collect(parts) { return parts.filter((p) => p.error).map((p) => p.error); }
663
+
664
+ function canonicalJson(value) {
665
+ if (value === null || typeof value === "string" || typeof value === "boolean") return JSON.stringify(value);
666
+ if (typeof value === "number") {
667
+ if (!Number.isFinite(value)) throw new Error("snapshot binding contains a non-finite number");
668
+ return JSON.stringify(value);
669
+ }
670
+ if (Array.isArray(value)) return `[${value.map(canonicalJson).join(",")}]`;
671
+ if (!isObject(value)) throw new Error("snapshot binding must be JSON serializable");
672
+ return `{${Object.keys(value).sort().map((key) => `${JSON.stringify(key)}:${canonicalJson(value[key])}`).join(",")}}`;
673
+ }
674
+ function digest(value) { return createHash("sha256").update(canonicalJson(value)).digest("hex"); }
675
+ function requireSnapshotIdentifier(input, key) {
676
+ const candidate = requireText(input, key);
677
+ if (candidate.error) return candidate;
678
+ if (!SNAPSHOT_IDENTIFIER_PATTERN.test(candidate.value) || candidate.value === "." || candidate.value === "..") {
679
+ return { error: finding("P0", "SNAPSHOT_IDENTIFIER", `input.${key}`, `${key} must be a safe identifier without path separators`, { example: "run-1" }) };
680
+ }
681
+ return candidate;
682
+ }
683
+
684
+ const FIRST_USE_NOTICE = Object.freeze({
685
+ zh: "Aimlock 仅用于大型、深度、跨模块、高风险或并行修改,以及用户明确要求 Aimlock 的修改。未明确要求 Aimlock 的小型低难度需求应直接处理,或只调用一个匹配的专项技能。",
686
+ en: "Use Aimlock only for large, deep, cross-module, high-risk, parallel, or explicitly requested changes. Handle small low-difficulty work directly or use one matched specialist unless Aimlock was explicitly requested.",
687
+ ru: "Aimlock применяется только для крупных, сложных, межмодульных, рискованных, параллельных или явно назначенных изменений. Небольшую простую задачу выполняйте напрямую либо одним профильным навыком, если Aimlock не потребован явно.",
688
+ });
689
+ const BYPASS_NOTICE = Object.freeze({
690
+ zh: "需求较小且低风险,不建议使用 Aimlock;请直接处理,或只调用一个匹配的专项技能。",
691
+ en: "This request is small and low risk; Aimlock is not recommended. Handle it directly or use one matched specialist skill.",
692
+ ru: "Запрос небольшой и низкорисковый; Aimlock не рекомендуется. Выполните его напрямую либо используйте один профильный навык.",
693
+ });
694
+ const INTAKE_QUESTIONS = Object.freeze([
695
+ { id: "goal", required: true, prompt: "What must be true when this finishes, and what must never change?", example: "只改税率常量一行,不改其它计税逻辑" },
696
+ { id: "targetFiles", required: true, prompt: "Which file paths are in scope? Use unknown if not located yet.", example: "apps/web/src/tax.ts" },
697
+ { id: "estimatedChangedLines", required: true, prompt: "How many lines should change?", example: "1" },
698
+ { id: "difficulty", required: true, prompt: "Difficulty: low, medium, or high.", example: "low" },
699
+ { id: "risk", required: true, prompt: "Risk: low, medium, or high.", example: "low" },
700
+ { id: "crossModule", required: true, prompt: "Does this cross modules? yes or no.", example: "no" },
701
+ { id: "needParallel", required: true, prompt: "Must independent modules run in parallel? yes or no.", example: "no" },
702
+ { id: "explicitAimlockRequested", required: true, prompt: "Did the user explicitly require Aimlock for this change? yes or no.", example: "no" },
703
+ { id: "goalKind", required: true, prompt: "Goal kind: code, calculator, mixed, or docs.", example: "code" },
704
+ { id: "deliveryDoc", required: true, prompt: "After success, summarize a local delivery document? yes or no.", example: "no" },
705
+ ]);
706
+
707
+ function classifyMode(input) {
708
+ const goal = requireText(input, "goal");
709
+ const targetFiles = requireStringArray(input, "targetFiles");
710
+ const lines = requireNumber(input, "estimatedChangedLines");
711
+ const difficulty = requireChoice(input, "difficulty", DIFFICULTIES);
712
+ const risk = requireChoice(input, "risk", RISKS);
713
+ const crossModule = requireBoolean(input, "crossModule");
714
+ const needParallel = requireBoolean(input, "needParallel");
715
+ const explicitAimlockRequested = requireBoolean(input, "explicitAimlockRequested");
716
+ const findings = collect([goal, targetFiles, lines, difficulty, risk, crossModule,
717
+ needParallel, explicitAimlockRequested]);
718
+ if (targetFiles.value && targetFiles.value.length === 0) {
719
+ findings.push(finding("P0", "TARGET_FILES_EMPTY", "input.targetFiles", "targetFiles must not be empty", { example: ["src/file.ts"] }));
720
+ }
721
+ if (findings.length) return { findings };
722
+ const hasUnknownTarget = targetFiles.value.some((value) => value.trim().toLowerCase() === "unknown");
723
+ const bypass = lines.value <= BYPASS_LINE_BUDGET && difficulty.value === "low"
724
+ && crossModule.value === false && risk.value !== "high" && needParallel.value === false
725
+ && explicitAimlockRequested.value === false;
726
+ if (bypass) {
727
+ return { mode: "bypass", useAimlock: false, reason: "small low-difficulty demand",
728
+ friendlyNotice: BYPASS_NOTICE, goal: goal.value, targetFiles: targetFiles.value,
729
+ estimatedChangedLines: lines.value, difficulty: difficulty.value,
730
+ explicitAimlockRequested: explicitAimlockRequested.value };
731
+ }
732
+ const fileCount = targetFiles.value.length;
733
+ const lockable = !hasUnknownTarget && fileCount === 1 && lines.value <= LOCK_LINE_BUDGET
734
+ && crossModule.value === false && needParallel.value === false;
735
+ const probeable = fileCount <= PROBE_FILE_BUDGET && lines.value <= PROBE_LINE_BUDGET && crossModule.value === false && needParallel.value === false;
736
+ const mode = lockable ? "lock" : probeable ? "probe" : "swarm";
737
+ return { mode, useAimlock: true,
738
+ reason: lockable ? "single-file guarded change" : probeable ? "bounded guarded change" : "deep, over-three-file, cross-module, parallel, high-risk, or over-budget work",
739
+ goal: goal.value, targetFiles: targetFiles.value, estimatedChangedLines: lines.value,
740
+ difficulty: difficulty.value, explicitAimlockRequested: explicitAimlockRequested.value };
741
+ }
742
+
743
+ function safeRelativePath(value) {
744
+ const source = text(value).trim().replace(/\\/g, "/");
745
+ if (!source || source.startsWith("/") || source.startsWith("~") || source.includes(":")
746
+ || /[\u0000-\u001f\u007f]/.test(source)) return null;
747
+ const parts = source.split("/").filter(Boolean);
748
+ const windowsDevice = /^(?:CON|PRN|AUX|NUL|COM[1-9]|LPT[1-9])(?:\..*)?$/i;
749
+ if (parts.length === 0 || parts.some((part) => part === "." || part === ".."
750
+ || /[. ]$/.test(part) || windowsDevice.test(part))) return null;
751
+ return parts.join("/");
752
+ }
753
+ function matchesPrefix(filePath, prefixes) {
754
+ const normalizedPath = safeRelativePath(filePath);
755
+ return normalizedPath !== null && prefixes.some((prefix) => (
756
+ normalizedPath === prefix || normalizedPath.startsWith(`${prefix}/`)
757
+ ));
758
+ }
759
+ function pathAllowed(filePath, contract) {
760
+ return !matchesPrefix(filePath, contract.forbiddenPaths) && matchesPrefix(filePath, contract.allowedPaths);
761
+ }
762
+
763
+ function readContract(input) {
764
+ const contract = input?.contract;
765
+ if (!isObject(contract)) {
766
+ return { findings: [finding("P0", "CONTRACT_OBJECT", "input.contract", "contract must be an object", { example: { allowedPaths: ["src/"], maxChangedLines: 50, allowNewFiles: false, allowDeleteFiles: false } })] };
767
+ }
768
+ const allowedPaths = requireStringArray(contract, "allowedPaths");
769
+ const forbiddenPaths = contract.forbiddenPaths === undefined
770
+ ? { value: [] } : requireStringArray(contract, "forbiddenPaths");
771
+ const maxChangedLines = requireNumber(contract, "maxChangedLines");
772
+ const allowNewFiles = requireBoolean(contract, "allowNewFiles");
773
+ const allowDeleteFiles = requireBoolean(contract, "allowDeleteFiles");
774
+ const findings = collect([allowedPaths, forbiddenPaths, maxChangedLines, allowNewFiles, allowDeleteFiles]);
775
+ if (allowedPaths.value && allowedPaths.value.length === 0) findings.push(finding("P0", "CONTRACT_ALLOWED", "input.contract.allowedPaths", "allowedPaths must not be empty", { example: ["src/"] }));
776
+ if (maxChangedLines.value === 0) findings.push(finding("P0", "CONTRACT_BUDGET", "input.contract.maxChangedLines", "maxChangedLines must be > 0", { example: 50 }));
777
+ const normalizedAllowed = allowedPaths.value?.map(safeRelativePath) ?? [];
778
+ const normalizedForbidden = forbiddenPaths.value?.map(safeRelativePath) ?? [];
779
+ if (normalizedAllowed.some((value) => value === null)) findings.push(finding("P0", "CONTRACT_ALLOWED_PATH", "input.contract.allowedPaths", "allowedPaths must contain safe relative paths without dot segments", { example: ["src/"] }));
780
+ if (normalizedForbidden.some((value) => value === null)) findings.push(finding("P0", "CONTRACT_FORBIDDEN_PATH", "input.contract.forbiddenPaths", "forbiddenPaths must contain safe relative paths without dot segments", { example: ["secrets/"] }));
781
+ if (findings.length) return { findings };
782
+ return { contract: { schemaVersion: CONTRACT_SCHEMA, allowedPaths: normalizedAllowed, forbiddenPaths: normalizedForbidden, maxChangedLines: maxChangedLines.value, allowNewFiles: allowNewFiles.value, allowDeleteFiles: allowDeleteFiles.value } };
783
+ }
784
+
785
+ function validateNodes(input) {
786
+ const parsed = readContract(input);
787
+ if (parsed.findings) return parsed;
788
+ if (!Array.isArray(input.nodes) || input.nodes.length === 0) {
789
+ return { findings: [finding("P0", "NODES_REQUIRED", "input.nodes", "nodes must be a non-empty array", { example: [{ path: "src/tax.ts", reason: "update rate", estimatedLines: 1 }] })] };
790
+ }
791
+ const findings = [];
792
+ let estimatedSum = 0;
793
+ const normalizedNodes = [];
794
+ const seenPaths = new Set();
795
+ for (const [index, node] of input.nodes.entries()) {
796
+ const ref = `input.nodes[${index}]`;
797
+ if (!isObject(node)) {
798
+ findings.push(finding("P0", "NODE_OBJECT", ref, "node must be an object", { example: { path: "src/tax.ts", reason: "update rate", estimatedLines: 1 } }));
799
+ continue;
800
+ }
801
+ const filePath = text(node.path).trim();
802
+ const normalizedFilePath = safeRelativePath(filePath);
803
+ const reason = text(node.reason).trim();
804
+ const estimatedLines = node.estimatedLines;
805
+ if (!filePath) findings.push(finding("P0", "NODE_PATH", `${ref}.path`, "path is required", { example: "src/tax.ts" }));
806
+ else if (normalizedFilePath === null) findings.push(finding("P0", "NODE_PATH_UNSAFE", `${ref}.path`, "path must be a safe relative path without dot segments", { example: "src/tax.ts" }));
807
+ if (!reason) findings.push(finding("P0", "NODE_REASON", `${ref}.reason`, "reason is required", { example: "update tax rate" }));
808
+ if (typeof estimatedLines !== "number" || !Number.isFinite(estimatedLines) || estimatedLines < 0) {
809
+ findings.push(finding("P0", "NODE_LINES", `${ref}.estimatedLines`, "estimatedLines must be a finite number >= 0", { example: 1 }));
810
+ } else {
811
+ estimatedSum += estimatedLines;
812
+ }
813
+ if (normalizedFilePath !== null && !pathAllowed(normalizedFilePath, parsed.contract)) {
814
+ findings.push(finding("P0", "NODE_OUT_OF_SCOPE", `${ref}.path`, `${filePath} is outside the scope contract`, { example: "allowedPaths: [\"src/\"]" }));
815
+ }
816
+ if (normalizedFilePath !== null && seenPaths.has(normalizedFilePath)) {
817
+ findings.push(finding("P0", "NODE_PATH_DUPLICATE", `${ref}.path`, "node paths must be unique", { example: normalizedFilePath }));
818
+ } else if (normalizedFilePath !== null) {
819
+ seenPaths.add(normalizedFilePath);
820
+ }
821
+ if (node.isNewFile === true && parsed.contract.allowNewFiles === false) {
822
+ findings.push(finding("P0", "NODE_NEW_FILE", `${ref}.isNewFile`, "new files are forbidden by the contract", { example: false }));
823
+ }
824
+ if (node.isDelete === true && parsed.contract.allowDeleteFiles === false) {
825
+ findings.push(finding("P0", "NODE_DELETE", `${ref}.isDelete`, "deletes are forbidden by the contract", { example: false }));
826
+ }
827
+ if (node.diffType !== undefined && !["add", "modify", "delete"].includes(node.diffType)) {
828
+ findings.push(finding("P0", "NODE_DIFF_TYPE", `${ref}.diffType`, "diffType must be add, modify, or delete", { example: "modify" }));
829
+ }
830
+ if (normalizedFilePath !== null && reason
831
+ && typeof estimatedLines === "number" && Number.isFinite(estimatedLines) && estimatedLines >= 0) {
832
+ normalizedNodes.push({
833
+ path: normalizedFilePath, reason, estimatedLines,
834
+ diffType: node.diffType ?? (node.isDelete === true ? "delete" : node.isNewFile === true ? "add" : "modify"),
835
+ isNewFile: node.isNewFile === true, isDelete: node.isDelete === true,
836
+ });
837
+ }
838
+ }
839
+ if (estimatedSum > parsed.contract.maxChangedLines) {
840
+ findings.push(finding("P0", "NODE_BUDGET", "input.nodes", `estimated ${estimatedSum} lines exceed maxChangedLines ${parsed.contract.maxChangedLines}`, { example: "maxChangedLines: 50" }));
841
+ }
842
+ if (findings.length) return { findings };
843
+ return { contract: parsed.contract, nodes: normalizedNodes, estimatedSum };
844
+ }
845
+
846
+ function officialMatchAllowed(slug, demand) {
847
+ const active = demand.mode !== "bypass";
848
+ const codeGoal = demand.goalKind === "code" || demand.goalKind === "mixed";
849
+ if (slug === "calctool") return demand.goalKind === "calculator" || demand.requiresCalculator;
850
+ if (slug === "confirm-protocol") return demand.requiresConfirmation;
851
+ if (slug === "archguard") return codeGoal && (demand.hasArchitectureContract || demand.newProject);
852
+ if (slug === "blueprint") return active && ["probe", "swarm"].includes(demand.mode)
853
+ && demand.contractUnclear && !demand.hasBlueprint;
854
+ if (slug === "swarm") return active && demand.mode === "swarm";
855
+ if (slug === "validator") return active && (demand.risk === "high" || demand.requiresValidation);
856
+ if (slug === "mergeguard") return active && demand.requiresMerge;
857
+ return slug !== "aimlock";
858
+ }
859
+
860
+ function validateResolvedSkills(input, demand) {
861
+ if (!Array.isArray(input?.serverResolvedSkills)) {
862
+ return { findings: [finding("P0", "ROUTE_RESOLUTION_REQUIRED", "input.serverResolvedSkills", "server-resolved skill matches are required", { example: [] })] };
863
+ }
864
+ if (demand.mode === "bypass" && input.serverResolvedSkills.length > 1) {
865
+ return { findings: [finding("P0", "BYPASS_SINGLE_SKILL", "input.serverResolvedSkills", "bypass may recommend at most one specialist skill", { example: [] })] };
866
+ }
867
+ const specified = new Set(Array.isArray(input.userSpecifiedSkills) ? input.userSpecifiedSkills : []);
868
+ const findings = [];
869
+ for (const [index, skill] of input.serverResolvedSkills.entries()) {
870
+ const ref = `input.serverResolvedSkills[${index}]`;
871
+ if (!isObject(skill) || !/^[A-Za-z0-9]{10}$/.test(text(skill.runtimeCode))
872
+ || !text(skill.slug).trim() || typeof skill.call !== "boolean" || typeof skill.analyze !== "boolean") {
873
+ findings.push(finding("P0", "ROUTE_MATCH_SHAPE", ref, "resolved skill shape is invalid", { example: { runtimeCode: "AbCdEfGh12", slug: "skill", call: true, analyze: false } }));
874
+ continue;
875
+ }
876
+ if (!skill.call && !(skill.analyze && specified.has(skill.runtimeCode))) {
877
+ findings.push(finding("P0", "ROUTE_MATCH_AUTHORITY", ref, "resolved skill must be callable or an explicitly named analysis candidate", { example: true }));
878
+ }
879
+ if (skill.official !== false && !officialMatchAllowed(skill.slug, demand)) {
880
+ findings.push(finding("P0", "ROUTE_CAPABILITY_MISMATCH", ref, `${skill.slug} does not match this demand`, { example: demand.goalKind }));
881
+ }
882
+ }
883
+ return findings.length ? { findings } : { skills: input.serverResolvedSkills };
884
+ }
885
+
886
+ function routeSkills(input) {
887
+ const mode = text(input?.mode).trim();
888
+ const goalKind = text(input?.goalKind).trim();
889
+ const risk = requireChoice(input, "risk", RISKS);
890
+ const requiredBooleans = ["hasBlueprint", "contractUnclear", "hasArchitectureContract", "newProject",
891
+ "requiresConfirmation", "requiresCalculator", "requiresMerge", "requiresValidation"]
892
+ .map((key) => requireBoolean(input, key));
893
+ const findings = collect([risk, ...requiredBooleans]);
894
+ if (!MODES.has(mode)) findings.push(finding("P0", "MODE_REQUIRED", "input.mode", "mode must be bypass, lock, probe, or swarm", { example: "bypass" }));
895
+ if (!GOAL_KINDS.has(goalKind)) findings.push(finding("P0", "GOAL_KIND", "input.goalKind", "goalKind must be code, calculator, mixed, or docs", { example: "code" }));
896
+ if (input?.officialCatalog !== undefined || input?.useRegistry !== undefined) {
897
+ findings.push(finding("P0", "FULL_CATALOG_FORBIDDEN", "input", "full catalogs and local registries are forbidden; the server injects only matched skills", { example: false }));
898
+ }
899
+ if (findings.length) return { findings };
900
+ const demand = { mode, goalKind, risk: risk.value, hasBlueprint: input.hasBlueprint,
901
+ contractUnclear: input.contractUnclear,
902
+ hasArchitectureContract: input.hasArchitectureContract, newProject: input.newProject,
903
+ requiresConfirmation: input.requiresConfirmation, requiresCalculator: input.requiresCalculator,
904
+ requiresMerge: input.requiresMerge, requiresValidation: input.requiresValidation };
905
+ const resolved = validateResolvedSkills(input, demand);
906
+ if (resolved.findings) return resolved;
907
+ return { mode, hops: resolved.skills };
908
+ }
909
+
910
+ function snapshotBinding(input) {
911
+ const validated = validateNodes(input);
912
+ if (validated.findings) return validated;
913
+ const paths = validated.nodes.map((node) => node.path).sort();
914
+ return {
915
+ contract: validated.contract, nodes: validated.nodes, paths,
916
+ contractDigest: digest(validated.contract), nodeDigest: digest(validated.nodes),
917
+ pathDigest: digest(paths),
918
+ };
919
+ }
920
+
921
+ function snapshotPlan(input) {
922
+ const runId = requireSnapshotIdentifier(input, "runId");
923
+ const findings = collect([runId]);
924
+ if (input?.createBranch === true || input?.gitBranch === true || input?.worktree === true) findings.push(finding("P0", "BRANCH_FORBIDDEN", "input", "Aimlock forbids git branches and worktrees; snapshot with file copies", { example: false }));
925
+ const binding = snapshotBinding(input);
926
+ if (binding.findings) findings.push(...binding.findings);
927
+ if (findings.length) return { findings };
928
+ const snapshotId = `snap-${runId.value}`;
929
+ return { snapshot: {
930
+ snapshotId, method: "file-copy", snapshotRoot: `.aimlock/snapshots/${runId.value}`,
931
+ paths: binding.paths, contractDigest: binding.contractDigest,
932
+ nodeDigest: binding.nodeDigest, pathDigest: binding.pathDigest,
933
+ forbidGitBranch: true, forbidWorktree: true,
934
+ } };
935
+ }
936
+
937
+ function mutateGate(input) {
938
+ const accepted = requireBoolean(input, "accepted");
939
+ const findings = collect([accepted]);
940
+ if (input?.createBranch === true || input?.gitBranch === true) findings.push(finding("P0", "BRANCH_FORBIDDEN", "input", "mutate must not create a git branch", { example: false }));
941
+ if (accepted.value === false) findings.push(finding("P0", "MUTATE_NOT_ACCEPTED", "input.accepted", "mutate is forbidden until nodes are accepted", { example: true }));
942
+ const binding = snapshotBinding(input);
943
+ if (binding.findings) findings.push(...binding.findings);
944
+ const receipt = input?.receipt;
945
+ if (!isObject(receipt)) {
946
+ findings.push(finding("P0", "SNAPSHOT_RECEIPT", "input.receipt", "a snapshot-verify receipt is required", { example: { schemaVersion: SNAPSHOT_RECEIPT_SCHEMA } }));
947
+ } else if (!binding.findings) {
948
+ const expectedKeys = ["contractDigest", "fileManifestDigest", "nodeDigest", "pathDigest", "paths", "receiptDigest", "schemaVersion", "snapshotId"];
949
+ const core = {
950
+ schemaVersion: receipt.schemaVersion, snapshotId: receipt.snapshotId,
951
+ contractDigest: receipt.contractDigest, nodeDigest: receipt.nodeDigest,
952
+ pathDigest: receipt.pathDigest, fileManifestDigest: receipt.fileManifestDigest,
953
+ };
954
+ const validId = requireSnapshotIdentifier({ snapshotId: receipt.snapshotId }, "snapshotId");
955
+ const validReceipt = Object.keys(receipt).sort().join("\n") === expectedKeys.join("\n")
956
+ && !validId.error && receipt.schemaVersion === SNAPSHOT_RECEIPT_SCHEMA
957
+ && receipt.contractDigest === binding.contractDigest
958
+ && receipt.nodeDigest === binding.nodeDigest && receipt.pathDigest === binding.pathDigest
959
+ && Array.isArray(receipt.paths) && digest(receipt.paths) === binding.pathDigest
960
+ && SHA256_PATTERN.test(receipt.fileManifestDigest)
961
+ && receipt.receiptDigest === digest(core);
962
+ if (!validReceipt) findings.push(finding("P0", "SNAPSHOT_RECEIPT_BINDING", "input.receipt", "snapshot receipt does not match the accepted contract, nodes, and paths", { example: binding.pathDigest }));
963
+ }
964
+ if (findings.length) return { findings };
965
+ return { allowed: true, snapshotId: receipt.snapshotId, receiptDigest: receipt.receiptDigest };
966
+ }
967
+
968
+ function validateTestEvidence(value, ref) {
969
+ const findings = [];
970
+ if (!isObject(value)) return [finding("P0", "TEST_EVIDENCE_OBJECT", ref, "TestEvidence must be an object", { example: { schemaVersion: TEST_EVIDENCE_SCHEMA } })];
971
+ if (value.schemaVersion !== TEST_EVIDENCE_SCHEMA) findings.push(finding("P0", "TEST_EVIDENCE_SCHEMA", `${ref}.schemaVersion`, `Expected ${TEST_EVIDENCE_SCHEMA}`, { example: TEST_EVIDENCE_SCHEMA }));
972
+ if (!text(value.evidenceId).trim()) findings.push(finding("P0", "TEST_EVIDENCE_ID", `${ref}.evidenceId`, "evidenceId is required", { example: "test-1" }));
973
+ if (!["test", "build", "lint", "security", "benchmark"].includes(value.kind)) findings.push(finding("P0", "TEST_EVIDENCE_KIND", `${ref}.kind`, "kind is not supported", { example: "test" }));
974
+ if (!["local", "trusted-runner"].includes(value.runner)) findings.push(finding("P0", "TEST_EVIDENCE_RUNNER", `${ref}.runner`, "runner must be local or trusted-runner", { example: "local" }));
975
+ if (!text(value.command).trim()) findings.push(finding("P0", "TEST_EVIDENCE_COMMAND", `${ref}.command`, "command is required", { example: "pnpm test" }));
976
+ if (!Number.isInteger(value.exitCode)) findings.push(finding("P0", "TEST_EVIDENCE_EXIT", `${ref}.exitCode`, "exitCode must be an integer", { example: 0 }));
977
+ if (typeof value.durationMs !== "number" || !Number.isFinite(value.durationMs) || value.durationMs < 0) findings.push(finding("P0", "TEST_EVIDENCE_DURATION", `${ref}.durationMs`, "durationMs must be a finite number >= 0", { example: 100 }));
978
+ if (!text(value.summary).trim()) findings.push(finding("P0", "TEST_EVIDENCE_SUMMARY", `${ref}.summary`, "summary is required", { example: "all tests passed" }));
979
+ if (value.artifactSha256 !== undefined && !SHA256_PATTERN.test(value.artifactSha256)) findings.push(finding("P0", "TEST_EVIDENCE_ARTIFACT", `${ref}.artifactSha256`, "artifactSha256 must be a lowercase SHA-256 digest", { example: "a".repeat(64) }));
980
+ return findings;
981
+ }
982
+
983
+ function continuity(input) {
984
+ const changedLines = requireNumber(input, "changedLines");
985
+ const maxChangedLines = requireNumber(input, "maxChangedLines");
986
+ const testsPassed = requireBoolean(input, "testsPassed");
987
+ const omissionScanDone = requireBoolean(input, "omissionScanDone");
988
+ const findings = collect([changedLines, maxChangedLines, testsPassed, omissionScanDone]);
989
+ if (findings.length) return { findings };
990
+ // Per-node expansion check (document 3.4): warn when a single node exceeds 50% of maxChangedLines
991
+ const changedNodes = Array.isArray(input.changedNodes) ? input.changedNodes : [];
992
+ const expansionWarnings = [];
993
+ for (const [idx, node] of changedNodes.entries()) {
994
+ if (!isObject(node)) continue;
995
+ const nodeLines = typeof node.changedLines === "number" ? node.changedLines : 0;
996
+ const threshold = Math.floor(maxChangedLines.value * 0.5);
997
+ if (nodeLines > threshold && maxChangedLines.value > 0) {
998
+ expansionWarnings.push(finding("P1", "NODE_EXPANSION", `input.changedNodes[${idx}]`, `node '${node.path}' changedLines ${nodeLines} exceeds 50% of maxChangedLines (${maxChangedLines.value})`, { example: { path: node.path, diffType: node.diffType ?? "modified", changedLines: threshold } }));
999
+ }
1000
+ }
1001
+ if (changedLines.value > maxChangedLines.value) return { trafficLight: "red", reason: "changed lines exceed the contract budget", ...(expansionWarnings.length ? { expansionWarnings } : {}) };
1002
+ if (testsPassed.value === false) return { trafficLight: "red", reason: "continuity tests failed", ...(expansionWarnings.length ? { expansionWarnings } : {}) };
1003
+ const testEvidence = Array.isArray(input.testEvidence) ? input.testEvidence : [];
1004
+ if (testsPassed.value === true && testEvidence.length === 0) return { trafficLight: "yellow", reason: "tests passed but no evidence provided", ...(expansionWarnings.length ? { expansionWarnings } : {}) };
1005
+ const evidenceFindings = testEvidence.flatMap((evidence, index) => validateTestEvidence(evidence, `input.testEvidence[${index}]`));
1006
+ if (evidenceFindings.length) return { trafficLight: "red", reason: "test evidence is invalid", evidenceFindings, ...(expansionWarnings.length ? { expansionWarnings } : {}) };
1007
+ if (testEvidence.some((evidence) => evidence.exitCode !== 0)) return { trafficLight: "red", reason: "test evidence contains a failed command", ...(expansionWarnings.length ? { expansionWarnings } : {}) };
1008
+ if (omissionScanDone.value === false) return { trafficLight: "yellow", reason: "omission scan not done", ...(expansionWarnings.length ? { expansionWarnings } : {}) };
1009
+ return { trafficLight: "green", reason: "within budget, tests passed, omission scan done", ...(expansionWarnings.length ? { expansionWarnings } : {}) };
1010
+ }
1011
+
1012
+ function interrupt(input) {
1013
+ const forceStop = requireBoolean(input, "forceStop");
1014
+ const isStatusQuery = requireBoolean(input, "isStatusQuery");
1015
+ const related = requireBoolean(input, "related");
1016
+ const findings = collect([forceStop, isStatusQuery, related]);
1017
+ if (findings.length) return { findings };
1018
+ if (forceStop.value) return { kind: "stop", action: "stop the running aim immediately" };
1019
+ if (isStatusQuery.value) return { kind: "status", action: "report progress only; do not start new work" };
1020
+ if (related.value) return { kind: "fuse", action: "signal the running agent, re-scope, update the JSON tasks, continue" };
1021
+ return { kind: "spawn", action: "unrelated: dispatch a new temporary agent; do not hijack the current aim" };
1022
+ }
1023
+
1024
+ function keepAlive(input) {
1025
+ const goalComplete = requireBoolean(input, "goalComplete");
1026
+ if (goalComplete.error) return { findings: [goalComplete.error] };
1027
+ if (goalComplete.value) return { armed: false, intervalSeconds: 0, mode: "protocol", callerTimerRequired: false, note: "goal is complete; no caller timer is required" };
1028
+ return {
1029
+ armed: false,
1030
+ intervalSeconds: KEEP_ALIVE_SECONDS,
1031
+ mode: "protocol",
1032
+ callerTimerRequired: true,
1033
+ message: KEEP_ALIVE_MESSAGE,
1034
+ note: "Aimlock is stateless and does not arm a timer; the caller must schedule and send this message every 90 seconds",
1035
+ };
1036
+ }
1037
+
1038
+ function deliveryDoc(input) {
1039
+ const userConfirmed = requireBoolean(input, "userConfirmed");
1040
+ if (userConfirmed.error) return { findings: [userConfirmed.error] };
1041
+ if (userConfirmed.value === false) return { required: false, skip: true };
1042
+ return { required: true, skip: false, instruction: "workers write local notes; main agent merges one document" };
1043
+ }
1044
+
1045
+ function validateRunJson(project) {
1046
+ const f = [];
1047
+ if (!isObject(project)) return [finding("P0", "RUN_OBJECT", "input.project", "project must be an object", { example: { goal: "update tax", mode: "lock", tasks: [] } })];
1048
+ if (!text(project.goal).trim()) f.push(finding("P0", "RUN_GOAL", "input.project.goal", "goal is required", { example: "update tax rate" }));
1049
+ const mode = text(project.mode);
1050
+ if (!MODES.has(mode)) f.push(finding("P0", "RUN_MODE", "input.project.mode", "mode must be bypass, lock, probe, or swarm", { example: "lock" }));
1051
+ if (!Array.isArray(project.tasks)) f.push(finding("P0", "RUN_TASKS", "input.project.tasks", "tasks must be an array", { example: [{ path: "src/tax.ts" }] }));
1052
+ if (mode === "bypass") {
1053
+ const activeFields = Object.keys(project).filter((key) => !["goal", "mode", "tasks"].includes(key));
1054
+ if (activeFields.length || (Array.isArray(project.tasks) && project.tasks.length > 0)) {
1055
+ f.push(finding("P0", "BYPASS_ACTIVE_ARTIFACT", "input.project", "bypass must not contain a contract, tasks, snapshots, or active-chain artifacts", { example: { goal: "small edit", mode: "bypass", tasks: [] } }));
1056
+ }
1057
+ } else if (MODES.has(mode)) {
1058
+ const parsed = readContract({ contract: project.contract });
1059
+ if (parsed.findings) f.push(...parsed.findings);
1060
+ }
1061
+ return f;
1062
+ }
1063
+
1064
+ function validateRequest(request) {
1065
+ const f = [];
1066
+ if (!isObject(request)) return [finding("P0", "REQUEST_OBJECT", "request", "request must be an object", { example: { schemaVersion: REQUEST_SCHEMA, requestId: "req-1", operation: "capabilities" } })];
1067
+ if (![LEGACY_REQUEST_SCHEMA, REQUEST_SCHEMA].includes(request.schemaVersion)) f.push(finding("P0", "REQUEST_SCHEMA", "request.schemaVersion", `Expected ${LEGACY_REQUEST_SCHEMA} or ${REQUEST_SCHEMA}`, { example: REQUEST_SCHEMA }));
1068
+ if (!text(request.requestId)) f.push(finding("P0", "REQUEST_REQUIRED_FIELD", "request.requestId", "requestId is required", { example: "req-1" }));
1069
+ if (!text(request.operation)) f.push(finding("P0", "REQUEST_REQUIRED_FIELD", "request.operation", "operation is required", { example: "capabilities" }));
1070
+ return f;
1071
+ }
1072
+
1073
+ function legacyCompatibleInput(operation, input) {
1074
+ if (operation === "classify") {
1075
+ return { ...input, risk: input.risk ?? "medium", explicitAimlockRequested: input.explicitAimlockRequested ?? false };
1076
+ }
1077
+ if (operation === "skill-route") {
1078
+ return {
1079
+ ...input,
1080
+ contractUnclear: input.contractUnclear ?? input.hasBlueprint === false,
1081
+ hasArchitectureContract: input.hasArchitectureContract ?? false,
1082
+ newProject: input.newProject ?? false,
1083
+ requiresConfirmation: input.requiresConfirmation ?? false,
1084
+ };
1085
+ }
1086
+ return input;
1087
+ }
1088
+
1089
+ function handleClassify(requestId, input) {
1090
+ const declaredRisk = requireChoice(input, "risk", RISKS);
1091
+ if (declaredRisk.error) return blockedResponse(requestId, [declaredRisk.error]);
1092
+ // 第一层:确定性规则分类(产物类型/风险/链路)
1093
+ const demand = classifyDemand({ ...input, risk: declaredRisk.value });
1094
+ if (demand.findings) return blockedResponse(requestId, demand.findings);
1095
+ // 如果规则未命中,返回追问
1096
+ if (demand.source === "needs-clarification") {
1097
+ return okResponse(requestId, { ...demand, nextStep: { operation: "classify", instruction: "Answer the clarification question, then re-classify." } });
1098
+ }
1099
+ const result = classifyMode({ ...input, risk: demand.risk });
1100
+ if (result.findings) return blockedResponse(requestId, result.findings);
1101
+ return okResponse(requestId, {
1102
+ ...result,
1103
+ product: demand.product, risk: demand.risk, chain: demand.chain, confidence: demand.confidence, classificationSource: demand.source,
1104
+ nextStep: result.mode === "bypass"
1105
+ ? { operation: "skill-route", instruction: "Resolve at most one specialist skill; do not start the Aimlock chain." }
1106
+ : demand.chain === "chat" ? { operation: null, instruction: "Pure chat; no chain needed." }
1107
+ : { operation: "scope-contract", instruction: "Lock the active Aimlock scope before routing." },
1108
+ });
1109
+ }
1110
+
1111
+ function handleContract(requestId, input) {
1112
+ const parsed = readContract(input);
1113
+ if (parsed.findings) return blockedResponse(requestId, parsed.findings);
1114
+ return okResponse(requestId, { contract: parsed.contract, nextStep: { operation: "skill-route", instruction: "Match hops, then probe or mutate inside the contract." } });
1115
+ }
1116
+ function handleRoute(requestId, input) {
1117
+ const routed = routeSkills(input);
1118
+ if (routed.findings) return blockedResponse(requestId, routed.findings);
1119
+ if (routed.mode === "bypass") {
1120
+ return okResponse(requestId, { useAimlock: false, recommendedSkills: routed.hops,
1121
+ nextStep: { operation: null, instruction: "Handle directly or invoke only the returned specialist." } });
1122
+ }
1123
+ if (routed.hops.length === 0) {
1124
+ return okResponse(requestId, { hops: [],
1125
+ nextStep: { operation: "propose-nodes", instruction: "No specialist is needed; return modification nodes read-only." } });
1126
+ }
1127
+ return okResponse(requestId, { hops: routed.hops,
1128
+ nextStep: { operation: "chain-plan", instruction: "Plan only the returned server-resolved skill ids." } });
1129
+ }
1130
+ function handlePropose(requestId, input) {
1131
+ const result = validateNodes(input);
1132
+ if (result.findings) return blockedResponse(requestId, result.findings);
1133
+ return okResponse(requestId, { nodes: result.nodes, estimatedSum: result.estimatedSum, contract: result.contract, nextStep: { operation: "accept-nodes", instruction: "Accept in-scope nodes, or escalate conflicts." } });
1134
+ }
1135
+ function handleAccept(requestId, input) {
1136
+ const result = validateNodes(input);
1137
+ if (result.findings) return blockedResponse(requestId, result.findings);
1138
+ const conflicts = Array.isArray(input.conflicts) ? input.conflicts : null;
1139
+ if (input.conflicts !== undefined && !Array.isArray(input.conflicts))
1140
+ return blockedResponse(requestId, [finding("P0", "CONFLICTS_ARRAY", "input.conflicts", "conflicts must be an array when present", { example: [{ path: "src/tax.ts", reason: "conflict" }] })]);
1141
+ const autoAccept = !conflicts || conflicts.length === 0;
1142
+ return okResponse(requestId, { autoAccept, escalate: !autoAccept, nodes: result.nodes,
1143
+ nextStep: autoAccept ? { operation: "snapshot-plan", instruction: "Snapshot the target files, then mutate-gate." }
1144
+ : { operation: "accept-nodes", instruction: "Resolve conflicts with the main agent before mutate." } });
1145
+ }
1146
+ function handleSnapshot(requestId, input) {
1147
+ const result = snapshotPlan(input);
1148
+ if (result.findings) return blockedResponse(requestId, result.findings);
1149
+ return okResponse(requestId, { snapshot: result.snapshot, nextStep: { operation: "snapshot-verify", instruction: "Copy files into snapshotRoot, hash every source and snapshot copy, then verify equality." } });
1150
+ }
1151
+ function handleMutate(requestId, input) {
1152
+ const result = mutateGate(input);
1153
+ if (result.findings) return blockedResponse(requestId, result.findings);
1154
+ return okResponse(requestId, { allowed: true, snapshotId: result.snapshotId, nextStep: { operation: "continuity-check", instruction: "After mutate, traffic-light budget, tests, and omission scan." } });
1155
+ }
1156
+ function handleContinuity(requestId, input) {
1157
+ const result = continuity(input);
1158
+ if (result.findings) return blockedResponse(requestId, result.findings);
1159
+ const done = result.trafficLight === "green";
1160
+ const payload = { trafficLight: result.trafficLight, reason: result.reason };
1161
+ if (result.expansionWarnings && result.expansionWarnings.length) payload.expansionWarnings = result.expansionWarnings;
1162
+ if (result.evidenceFindings && result.evidenceFindings.length) payload.evidenceFindings = result.evidenceFindings;
1163
+ return okResponse(requestId, { ...payload,
1164
+ nextStep: done ? { operation: "keep-alive", instruction: "If the whole aim is complete, set goalComplete true." }
1165
+ : { operation: "mutate-gate", instruction: "Red or yellow: roll back from the snapshot and fix inside the contract." } });
1166
+ }
1167
+
1168
+ function snapshotVerify(input) {
1169
+ const sid = requireSnapshotIdentifier(input, "snapshotId");
1170
+ if (sid.error) return { findings: [sid.error] };
1171
+ const binding = snapshotBinding(input);
1172
+ if (binding.findings) return binding;
1173
+ const files = input?.files;
1174
+ if (!Array.isArray(files) || files.length === 0) return { findings: [finding("P0", "VERIFY_FILES", "input.files", "files must be a non-empty array", { example: [{ path: "src/tax.ts", sourceHash: "a".repeat(64), snapshotHash: "a".repeat(64) }] })] };
1175
+ const findings = [];
1176
+ const normalizedFiles = [];
1177
+ for (const [i, f] of files.entries()) {
1178
+ const ref = `input.files[${i}]`;
1179
+ const normalizedPath = isObject(f) ? safeRelativePath(f.path) : null;
1180
+ if (!isObject(f) || normalizedPath === null) {
1181
+ findings.push(finding("P0", "VERIFY_FILE_OBJECT", ref, "each file must have a non-empty path", { example: { path: "src/tax.ts", sourceHash: "a".repeat(64), snapshotHash: "a".repeat(64) } }));
1182
+ continue;
1183
+ }
1184
+ if (!SHA256_PATTERN.test(f.sourceHash)) findings.push(finding("P0", "VERIFY_SOURCE_HASH", `${ref}.sourceHash`, "sourceHash must be a lowercase SHA-256 digest", { example: "a".repeat(64) }));
1185
+ if (!SHA256_PATTERN.test(f.snapshotHash)) findings.push(finding("P0", "VERIFY_SNAPSHOT_HASH", `${ref}.snapshotHash`, "snapshotHash must be a lowercase SHA-256 digest", { example: "a".repeat(64) }));
1186
+ if (SHA256_PATTERN.test(f.sourceHash) && SHA256_PATTERN.test(f.snapshotHash) && f.sourceHash !== f.snapshotHash) findings.push(finding("P0", "SNAPSHOT_HASH_MISMATCH", ref, `${f.path} differs from its snapshot copy`, { example: { sourceHash: f.sourceHash, snapshotHash: f.sourceHash } }));
1187
+ if (SHA256_PATTERN.test(f.sourceHash) && SHA256_PATTERN.test(f.snapshotHash)) {
1188
+ normalizedFiles.push({ path: normalizedPath, sourceHash: f.sourceHash, snapshotHash: f.snapshotHash });
1189
+ }
1190
+ }
1191
+ normalizedFiles.sort((left, right) => left.path < right.path ? -1 : left.path > right.path ? 1 : 0);
1192
+ if (normalizedFiles.map((file) => file.path).join("\n") !== binding.paths.join("\n")) {
1193
+ findings.push(finding("P0", "SNAPSHOT_PATH_SET", "input.files", "snapshot files must exactly match the accepted node paths", { example: binding.paths }));
1194
+ }
1195
+ if (findings.length) return { findings };
1196
+ const core = {
1197
+ schemaVersion: SNAPSHOT_RECEIPT_SCHEMA, snapshotId: sid.value,
1198
+ contractDigest: binding.contractDigest, nodeDigest: binding.nodeDigest,
1199
+ pathDigest: binding.pathDigest, fileManifestDigest: digest(normalizedFiles),
1200
+ };
1201
+ return { verified: true, snapshotId: sid.value, fileCount: files.length,
1202
+ receipt: { ...core, paths: binding.paths, receiptDigest: digest(core) } };
1203
+ }
1204
+ function runStatus(input) {
1205
+ const rid = requireText(input, "runId");
1206
+ if (rid.error) return { findings: [rid.error] };
1207
+ const boundary = "Aimlock is stateless; run state must be supplied by the caller and is never persisted by this operation.";
1208
+ if (input.state === undefined) return { runId: rid.value, known: false, run: { status: "unknown", currentPhase: null, history: [] }, boundary };
1209
+ if (!isObject(input.state)) return { findings: [finding("P0", "RUN_STATE_OBJECT", "input.state", "state must be an object when supplied", { example: { status: "running", currentPhase: "validate", history: [] } })] };
1210
+ const findings = [];
1211
+ if (!["running", "blocked", "complete"].includes(input.state.status)) findings.push(finding("P0", "RUN_STATE_STATUS", "input.state.status", "status must be running, blocked, or complete", { example: "running" }));
1212
+ if (!text(input.state.currentPhase).trim()) findings.push(finding("P0", "RUN_STATE_PHASE", "input.state.currentPhase", "currentPhase is required", { example: "validate" }));
1213
+ if (!Array.isArray(input.state.history)) findings.push(finding("P0", "RUN_STATE_HISTORY", "input.state.history", "history must be an array", { example: [] }));
1214
+ if (findings.length) return { findings };
1215
+ return { runId: rid.value, known: true, run: { status: input.state.status, currentPhase: input.state.currentPhase, history: input.state.history }, boundary };
1216
+ }
1217
+
1218
+ function finish(requestId, result, payload) {
1219
+ if (result.findings) return blockedResponse(requestId, result.findings);
1220
+ return okResponse(requestId, payload ?? result);
1221
+ }
1222
+
1223
+ async function executeRun(request) {
1224
+ const requestFindings = validateRequest(request);
1225
+ if (requestFindings.length) {
1226
+ return { ...blockedResponse(request?.requestId ?? "unknown", requestFindings), errorSchema: ERROR_SCHEMA };
1227
+ }
1228
+ const { requestId, operation } = request;
1229
+ const rawInput = isObject(request.input) ? request.input : {};
1230
+ const input = request.schemaVersion === LEGACY_REQUEST_SCHEMA
1231
+ ? legacyCompatibleInput(operation, rawInput) : rawInput;
1232
+ if (operation === "capabilities") {
1233
+ return okResponse(requestId, {
1234
+ capabilities: {
1235
+ pure: true, stateless: true, networkRequired: false, filesystemRequired: false,
1236
+ operations: [...PURE_OPERATIONS], operationSchemas: OPERATION_SCHEMAS,
1237
+ modes: ["bypass", "lock", "probe", "swarm"], forbidGitBranch: true,
1238
+ keepAliveSeconds: KEEP_ALIVE_SECONDS, keepAliveMessage: KEEP_ALIVE_MESSAGE,
1239
+ routing: "server-resolved-on-demand", userSpecifiedField: "userSpecifiedSkills",
1240
+ localTrustedExecution: {
1241
+ requiredFor: ["filesystem-probe", "read-budget", "mutate-pass", "guarded-write", "autocoord-lease"],
1242
+ operations: ["capabilities", "probe", "reassess", "budget-init", "budget-read", "budget-status",
1243
+ "budget-extend", "gate-issue", "gate-verify", "guarded-write"],
1244
+ command: "cli-aimlock local <operation> <repositoryRoot>",
1245
+ schemaDiscovery: "cli-aimlock local capabilities <repositoryRoot>",
1246
+ boundary: "Only host writes routed through guarded-write are physically intercepted.",
1247
+ },
1248
+ },
1249
+ skill: { name: COMPILER_NAME, version: COMPILER_VERSION },
1250
+ firstUseNotice: FIRST_USE_NOTICE,
1251
+ nextStep: { operation: "intake", instruction: "Ask the intake questions one at a time. Do not mutate files yet." },
1252
+ });
1253
+ }
1254
+ if (operation === "help") {
1255
+ return okResponse(requestId, {
1256
+ help: { name: COMPILER_NAME, version: COMPILER_VERSION, operations: OPERATION_CATALOG, operationSchemas: OPERATION_SCHEMAS },
1257
+ nextStep: { operation: "intake", instruction: "Ask the intake questions one at a time." },
1258
+ });
1259
+ }
1260
+ if (operation === "intake") {
1261
+ const answers = input?.answers;
1262
+ if (Array.isArray(answers) && answers.length > 0) {
1263
+ // Normalize {question, answer} pairs into {id, answer} by matching prompt text
1264
+ const normalized = answers.map((a) => {
1265
+ if (a.id) return { id: a.id, answer: a.answer };
1266
+ if (a.question) {
1267
+ const match = INTAKE_QUESTIONS.find((q) => q.prompt === a.question || q.id === a.question);
1268
+ if (match) return { id: match.id, answer: a.answer };
1269
+ }
1270
+ return { id: a.id ?? null, answer: a.answer };
1271
+ });
1272
+ const requiredIds = new Set(INTAKE_QUESTIONS.filter((q) => q.required).map((q) => q.id));
1273
+ const providedIds = new Set(normalized.filter((a) => a.id).map((a) => a.id));
1274
+ const missing = [...requiredIds].filter((id) => !providedIds.has(id));
1275
+ if (missing.length) return blockedResponse(requestId, [finding("P0", "INTAKE_MISSING", "input.answers", `missing required answers: ${missing.join(", ")}`, { example: INTAKE_QUESTIONS.map((q) => ({ id: q.id, answer: q.example })) })]);
1276
+ const answerMap = Object.fromEntries(normalized.filter((a) => a.id).map((a) => [a.id, a.answer]));
1277
+ return okResponse(requestId, { answers: answerMap, nextStep: { operation: "classify", instruction: "Classify bypass/lock/probe/swarm from the answers. Do not mutate yet." } });
1278
+ }
1279
+ return okResponse(requestId, { questions: INTAKE_QUESTIONS, nextStep: { operation: "classify", instruction: "Classify bypass/lock/probe/swarm from the answers. Do not mutate yet." } });
1280
+ }
1281
+ if (operation === "classify") return handleClassify(requestId, input);
1282
+ if (operation === "scope-contract") return handleContract(requestId, input);
1283
+ if (operation === "skill-route") return handleRoute(requestId, input);
1284
+ if (operation === "propose-nodes") return handlePropose(requestId, input);
1285
+ if (operation === "accept-nodes") return handleAccept(requestId, input);
1286
+ if (operation === "snapshot-plan") return handleSnapshot(requestId, input);
1287
+ if (operation === "mutate-gate") return handleMutate(requestId, input);
1288
+ if (operation === "continuity-check") return handleContinuity(requestId, input);
1289
+ if (operation === "interrupt") return finish(requestId, interrupt(input));
1290
+ if (operation === "keep-alive") return finish(requestId, keepAlive(input));
1291
+ if (operation === "delivery-doc") return finish(requestId, deliveryDoc(input));
1292
+ if (operation === "snapshot-verify") return finish(requestId, snapshotVerify(input));
1293
+ if (operation === "run-status") return finish(requestId, runStatus(input));
1294
+ if (operation === "chain-plan") {
1295
+ const result = buildChainPlan(input);
1296
+ if (result.findings) return blockedResponse(requestId, result.findings);
1297
+ return okResponse(requestId, { ...result, nextStep: result.ready.length ? { operation: result.ready[0], instruction: `Execute first ready step: ${result.ready[0]}` } : { operation: "chain-status", instruction: "All steps blocked or complete." } });
1298
+ }
1299
+ if (operation === "chain-status") return finish(requestId, chainStatus(input));
1300
+ if (operation === "feedback") return finish(requestId, submitFeedback(input));
1301
+ if (operation === "validate-json") {
1302
+ const findings = validateRunJson(input.project);
1303
+ if (findings.length) return blockedResponse(requestId, findings);
1304
+ return okResponse(requestId, { valid: true, nextStep: { operation: "classify", instruction: "Run JSON is valid." } });
1305
+ }
1306
+ return failed(requestId, "UNSUPPORTED_OPERATION", `Unsupported operation: ${operation}`);
1307
+ }
1308
+
1309
+ export async function run(request) {
1310
+ const result = await executeRun(request);
1311
+ if (isObject(request) && request.schemaVersion === LEGACY_REQUEST_SCHEMA && isObject(result)) {
1312
+ return { ...result, schemaVersion: LEGACY_RESPONSE_SCHEMA };
1313
+ }
1314
+ return result;
1315
+ }
1316
+
1317
+ export {
1318
+ COMPILER_VERSION, CONTRACT_SCHEMA, PURE_OPERATIONS, OPERATION_CATALOG, INTAKE_QUESTIONS, OPERATION_SCHEMAS,
1319
+ KEEP_ALIVE_SECONDS, KEEP_ALIVE_MESSAGE, FIRST_USE_NOTICE, BYPASS_NOTICE,
1320
+ classifyMode, readContract, validateNodes, routeSkills, snapshotPlan, mutateGate,
1321
+ continuity, interrupt, keepAlive, deliveryDoc, validateRunJson, snapshotVerify, runStatus, okResponse, blockedResponse, finding,
1322
+ };