@lmzhen/dsh-evolution-merge 0.28.0 → 0.29.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/README.md +21 -18
- package/lib/index.js +725 -908
- package/lib/types/adapters/engine-screen.d.ts +55 -0
- package/lib/types/adapters/model-runner.d.ts +28 -0
- package/lib/types/authoring-flow.d.ts +7 -2
- package/lib/types/channel-port.d.ts +2 -1
- package/lib/types/commit-assembly.d.ts +2 -0
- package/lib/types/commit-receipt.d.ts +4 -1
- package/lib/types/contracts.d.ts +7 -2
- package/lib/types/coordinator.d.ts +8 -0
- package/lib/types/doctor.d.ts +1 -1
- package/lib/types/index.d.ts +6 -7
- package/lib/types/merge-command.d.ts +3 -0
- package/lib/types/merge-routes.d.ts +1 -0
- package/lib/types/new-child-authoring.d.ts +1 -1
- package/lib/types/params.d.ts +1 -0
- package/lib/types/prompts.d.ts +7 -5
- package/lib/types/routes.d.ts +16 -1
- package/lib/types/runtime.d.ts +3 -4
- package/lib/types/scan-ports.d.ts +1 -1
- package/lib/types/scan-run.d.ts +2 -0
- package/lib/types/skill-card.d.ts +12 -0
- package/lib/types/staging.d.ts +6 -0
- package/package.json +3 -6
- package/lib/types/adapters/model.d.ts +0 -31
- package/lib/types/gate-filter.d.ts +0 -31
- package/lib/types/propose.d.ts +0 -26
- package/lib/types/screening-port.d.ts +0 -47
- package/lib/types/screening.d.ts +0 -30
package/lib/index.js
CHANGED
|
@@ -1,8 +1,95 @@
|
|
|
1
|
+
import { join } from "node:path";
|
|
1
2
|
import z from "@deepseek-ai/schemastery";
|
|
2
|
-
import {
|
|
3
|
+
import { screen, selectCandidates } from "@lmzhen/dsh-evolution-merge-engine";
|
|
4
|
+
import { DEFAULT_SKILL_LIMITS, authorityFrom, contentHash, createSubagentChannel, evolutionHome, evolutionIoAdapter, executeUnitUndo, isPresent, newRunRegistry, newSkillLibrary, parseFrontmatter, planUnitUndoAcross, policyStageLimits, probeAbsent, probeList, probePresent, probeText, probeUnknown, scanContentThreats, supportRefs, transactIo, validateFrontmatter, validateSupportPath } from "@lmzhen/dsh-evolution-core";
|
|
3
5
|
import { HOST_ROUTES, json, methodNotAllowed, registerHostRoutes } from "@lmzhen/dsh-evolution-core/http-routes";
|
|
4
|
-
import { join } from "node:path";
|
|
5
6
|
import { createHash } from "node:crypto";
|
|
7
|
+
//#region lib/types/adapters/engine-screen.js
|
|
8
|
+
/**
|
|
9
|
+
* G3 screen adapter: cards -> corpus snapshot -> EnginePort.screen.
|
|
10
|
+
*
|
|
11
|
+
* The vendor engine reads a corpus from disk; host cards are content. This module
|
|
12
|
+
* writes only SKILL.md support-free snapshots under the run directory. It does not
|
|
13
|
+
* decide candidates: the Q02 selector is injected so engine/report-map stays the
|
|
14
|
+
* single source of that policy.
|
|
15
|
+
*/
|
|
16
|
+
function safeName(name, index) {
|
|
17
|
+
const cleaned = name.replace(/[^A-Za-z0-9._-]+/g, "-").replace(/^-+|-+$/g, "");
|
|
18
|
+
return (cleaned === "" ? "skill-" + String(index) : cleaned) + "__" + String(index);
|
|
19
|
+
}
|
|
20
|
+
/** Write the exact card bodies as a support-free corpus snapshot and return its root. */
|
|
21
|
+
async function snapshotCards(io, home, runId, cards) {
|
|
22
|
+
const root = join(home, "merge", "runs", runId, "corpus");
|
|
23
|
+
for (let index = 0; index < cards.length; index += 1) {
|
|
24
|
+
const card = cards[index];
|
|
25
|
+
if (card === void 0) continue;
|
|
26
|
+
await io.writeText(join(root, safeName(card.name, index), "SKILL.md"), card.body);
|
|
27
|
+
}
|
|
28
|
+
return root;
|
|
29
|
+
}
|
|
30
|
+
function createEngineScreening(deps) {
|
|
31
|
+
return async (cards, runId, signal) => {
|
|
32
|
+
const corpusDir = await snapshotCards(deps.io, deps.home, runId, cards);
|
|
33
|
+
const outDir = join(deps.home, "merge", "runs", runId, "engine");
|
|
34
|
+
const result = await deps.engine.screen({
|
|
35
|
+
corpusDir,
|
|
36
|
+
outDir,
|
|
37
|
+
...deps.cfg === void 0 ? {} : { cfg: deps.cfg },
|
|
38
|
+
...deps.runner === void 0 ? {} : { runner: deps.runner },
|
|
39
|
+
signal
|
|
40
|
+
});
|
|
41
|
+
if (!result.ok) throw new Error("engine screen failed: " + result.error.code + (result.error.stage === void 0 ? "" : " @" + result.error.stage) + (result.error.detail === void 0 ? "" : ": " + result.error.detail));
|
|
42
|
+
const selection = deps.select(result.report);
|
|
43
|
+
if (deps.onSelection !== void 0) await deps.onSelection(selection, runId);
|
|
44
|
+
return Object.assign(selection.candidates.slice(), selection.candidatePairs === void 0 ? {} : { candidatePairs: selection.candidatePairs });
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
//#endregion
|
|
48
|
+
//#region lib/types/adapters/model-runner.js
|
|
49
|
+
function append(chunk, text) {
|
|
50
|
+
if (chunk.type === "text-delta" || chunk.type === "text") return text + (chunk.text ?? "");
|
|
51
|
+
if (chunk.type === "finish" && chunk.reason?.kind === "error") throw new Error("model call failed: " + (chunk.reason.failure?.message ?? "unknown"));
|
|
52
|
+
return text;
|
|
53
|
+
}
|
|
54
|
+
function createEngineRunner(llm, route) {
|
|
55
|
+
return async (prompt) => {
|
|
56
|
+
const selection = route();
|
|
57
|
+
let text = "";
|
|
58
|
+
if (llm.stream !== void 0) {
|
|
59
|
+
for await (const chunk of llm.stream({
|
|
60
|
+
provider: selection.provider,
|
|
61
|
+
model: selection.model,
|
|
62
|
+
messages: [{
|
|
63
|
+
role: "user",
|
|
64
|
+
content: [{
|
|
65
|
+
type: "text",
|
|
66
|
+
text: prompt
|
|
67
|
+
}]
|
|
68
|
+
}],
|
|
69
|
+
temperature: 0
|
|
70
|
+
})) text = append(chunk, text);
|
|
71
|
+
return text;
|
|
72
|
+
}
|
|
73
|
+
if (llm.prepareCall === void 0) throw new Error("llm service has neither stream nor prepareCall");
|
|
74
|
+
const call = await llm.prepareCall({
|
|
75
|
+
provider: selection.provider,
|
|
76
|
+
model: selection.model,
|
|
77
|
+
temperature: 0
|
|
78
|
+
});
|
|
79
|
+
for await (const chunk of call.stream({
|
|
80
|
+
...call.config,
|
|
81
|
+
messages: [{
|
|
82
|
+
role: "user",
|
|
83
|
+
content: [{
|
|
84
|
+
type: "text",
|
|
85
|
+
text: prompt
|
|
86
|
+
}]
|
|
87
|
+
}]
|
|
88
|
+
})) text = append(chunk, text);
|
|
89
|
+
return text;
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
//#endregion
|
|
6
93
|
//#region lib/types/contracts.js
|
|
7
94
|
/**
|
|
8
95
|
* Frozen merge-domain vocabulary. No io, no host, no imports from the plugin shell.
|
|
@@ -60,8 +147,10 @@ function emptyMergeState() {
|
|
|
60
147
|
};
|
|
61
148
|
}
|
|
62
149
|
/** One record can be retried only when the failure says so and names a stage. */
|
|
150
|
+
const MERGE_MAX_ATTEMPTS = 3;
|
|
63
151
|
function canRetry(record) {
|
|
64
152
|
if (record.state === "skipped") return false;
|
|
153
|
+
if ((record.attempts ?? 0) >= 3) return false;
|
|
65
154
|
if (record.failure === void 0) return record.state !== "pending";
|
|
66
155
|
return record.failure.retryable && record.failure.retryFrom !== "none";
|
|
67
156
|
}
|
|
@@ -236,6 +325,130 @@ async function rowsOf(io, home, queue) {
|
|
|
236
325
|
return queue === void 0 ? loaded.state.rows : loaded.state.rows.filter((row) => row.queue === queue);
|
|
237
326
|
}
|
|
238
327
|
//#endregion
|
|
328
|
+
//#region lib/types/coordinator.js
|
|
329
|
+
/**
|
|
330
|
+
* Pure merge transition table. No io, no run shell, no subagents.
|
|
331
|
+
*
|
|
332
|
+
* Every transition returns the whole next row (or a drop), so the caller can persist it
|
|
333
|
+
* through mutateMergeState in one transaction. Invalid transitions are refused by name;
|
|
334
|
+
* a caller never "repairs" a row by guessing which state the operator meant.
|
|
335
|
+
*/
|
|
336
|
+
function invalid(event, state) {
|
|
337
|
+
return {
|
|
338
|
+
ok: false,
|
|
339
|
+
code: "invalid-transition",
|
|
340
|
+
message: "cannot apply " + event.type + " to " + (state ?? "no row")
|
|
341
|
+
};
|
|
342
|
+
}
|
|
343
|
+
/** Drop fields that must not survive a transition; optional fields stay absent. */
|
|
344
|
+
function base(row, at) {
|
|
345
|
+
return {
|
|
346
|
+
id: row.id,
|
|
347
|
+
pairKey: row.pairKey,
|
|
348
|
+
a: row.a,
|
|
349
|
+
b: row.b,
|
|
350
|
+
state: row.state,
|
|
351
|
+
queue: row.queue,
|
|
352
|
+
createdAt: row.createdAt,
|
|
353
|
+
updatedAt: at,
|
|
354
|
+
...row.runId === void 0 ? {} : { runId: row.runId },
|
|
355
|
+
...row.unitId === void 0 ? {} : { unitId: row.unitId },
|
|
356
|
+
...row.origin === void 0 ? {} : { origin: row.origin },
|
|
357
|
+
...row.decision === void 0 ? {} : { decision: row.decision },
|
|
358
|
+
...row.attempts === void 0 ? {} : { attempts: row.attempts },
|
|
359
|
+
...row.stagingRef === void 0 ? {} : { stagingRef: row.stagingRef },
|
|
360
|
+
...row.stagingExpiresAt === void 0 ? {} : { stagingExpiresAt: row.stagingExpiresAt }
|
|
361
|
+
};
|
|
362
|
+
}
|
|
363
|
+
function next(row, at, state, patch = {}) {
|
|
364
|
+
return {
|
|
365
|
+
ok: true,
|
|
366
|
+
row: {
|
|
367
|
+
...base(row, at),
|
|
368
|
+
state,
|
|
369
|
+
queue: queueOf(state),
|
|
370
|
+
...patch
|
|
371
|
+
}
|
|
372
|
+
};
|
|
373
|
+
}
|
|
374
|
+
function transition(current, event) {
|
|
375
|
+
if (event.type === "candidate") {
|
|
376
|
+
if (current !== void 0) return {
|
|
377
|
+
ok: true,
|
|
378
|
+
row: current
|
|
379
|
+
};
|
|
380
|
+
const key = pairKey(event.a, event.b);
|
|
381
|
+
return {
|
|
382
|
+
ok: true,
|
|
383
|
+
row: {
|
|
384
|
+
id: rowIdOf(key),
|
|
385
|
+
pairKey: key,
|
|
386
|
+
a: event.a,
|
|
387
|
+
b: event.b,
|
|
388
|
+
state: "verify",
|
|
389
|
+
queue: "retry",
|
|
390
|
+
createdAt: event.at,
|
|
391
|
+
updatedAt: event.at
|
|
392
|
+
}
|
|
393
|
+
};
|
|
394
|
+
}
|
|
395
|
+
if (event.type === "source-gone") return {
|
|
396
|
+
ok: true,
|
|
397
|
+
row: null
|
|
398
|
+
};
|
|
399
|
+
if (event.type === "gate-keep") {
|
|
400
|
+
if (current !== void 0) return {
|
|
401
|
+
ok: true,
|
|
402
|
+
row: current
|
|
403
|
+
};
|
|
404
|
+
const key = pairKey(event.a, event.b);
|
|
405
|
+
return {
|
|
406
|
+
ok: true,
|
|
407
|
+
row: {
|
|
408
|
+
id: rowIdOf(key),
|
|
409
|
+
pairKey: key,
|
|
410
|
+
a: event.a,
|
|
411
|
+
b: event.b,
|
|
412
|
+
state: "skipped",
|
|
413
|
+
queue: "skipped",
|
|
414
|
+
origin: "agent",
|
|
415
|
+
createdAt: event.at,
|
|
416
|
+
updatedAt: event.at
|
|
417
|
+
}
|
|
418
|
+
};
|
|
419
|
+
}
|
|
420
|
+
if (current === void 0) return invalid(event, void 0);
|
|
421
|
+
switch (event.type) {
|
|
422
|
+
case "round1-keep": return current.state === "verify" ? next(current, event.at, "skipped", { origin: "agent" }) : invalid(event, current.state);
|
|
423
|
+
case "round1-merge":
|
|
424
|
+
if (current.state !== "verify") return invalid(event, current.state);
|
|
425
|
+
return event.approval === "on" ? next(current, event.at, "pending", {
|
|
426
|
+
...event.decision === void 0 ? {} : { decision: event.decision },
|
|
427
|
+
...event.stagingRef === void 0 ? {} : { stagingRef: event.stagingRef },
|
|
428
|
+
...event.stagingExpiresAt === void 0 ? {} : { stagingExpiresAt: event.stagingExpiresAt }
|
|
429
|
+
}) : next(current, event.at, "fuse", {
|
|
430
|
+
...event.decision === void 0 ? {} : { decision: event.decision },
|
|
431
|
+
...event.stagingRef === void 0 ? {} : { stagingRef: event.stagingRef },
|
|
432
|
+
...event.stagingExpiresAt === void 0 ? {} : { stagingExpiresAt: event.stagingExpiresAt }
|
|
433
|
+
});
|
|
434
|
+
case "round1-fail": return current.state === "verify" ? next(current, event.at, "verify", {
|
|
435
|
+
failure: event.failure,
|
|
436
|
+
attempts: (current.attempts ?? 0) + 1
|
|
437
|
+
}) : invalid(event, current.state);
|
|
438
|
+
case "approve": return current.state === "pending" ? next(current, event.at, "fuse", { ...event.runId === void 0 ? {} : { runId: event.runId } }) : invalid(event, current.state);
|
|
439
|
+
case "reject": return current.state === "pending" ? next(current, event.at, "skipped", { origin: "human" }) : invalid(event, current.state);
|
|
440
|
+
case "round2-fail": return current.state === "fuse" ? next(current, event.at, "fuse", {
|
|
441
|
+
failure: event.failure,
|
|
442
|
+
attempts: (current.attempts ?? 0) + 1
|
|
443
|
+
}) : invalid(event, current.state);
|
|
444
|
+
case "round2-success": return current.state === "fuse" ? {
|
|
445
|
+
ok: true,
|
|
446
|
+
row: null
|
|
447
|
+
} : invalid(event, current.state);
|
|
448
|
+
case "restore": return current.state === "skipped" ? next(current, event.at, "verify") : invalid(event, current.state);
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
//#endregion
|
|
239
452
|
//#region lib/types/status.js
|
|
240
453
|
function countRows(rows) {
|
|
241
454
|
const counts = {
|
|
@@ -274,10 +487,23 @@ function toRowView(row) {
|
|
|
274
487
|
queue: queueOf(row.state),
|
|
275
488
|
stage: stageOf(row.state),
|
|
276
489
|
label: row.a + " + " + row.b,
|
|
277
|
-
updatedAt: row.updatedAt
|
|
490
|
+
updatedAt: row.updatedAt,
|
|
491
|
+
createdAt: row.createdAt,
|
|
492
|
+
...row.decision?.target === void 0 ? {} : { target: row.decision.target },
|
|
493
|
+
...row.decision?.confidence === void 0 ? {} : { confidence: row.decision.confidence },
|
|
494
|
+
...row.failure === void 0 ? {} : { failure: {
|
|
495
|
+
stage: row.failure.stage,
|
|
496
|
+
code: row.failure.code,
|
|
497
|
+
message: row.failure.message,
|
|
498
|
+
retryable: row.failure.retryable
|
|
499
|
+
} },
|
|
500
|
+
...row.attempts === void 0 ? {} : { attempts: row.attempts },
|
|
501
|
+
...row.origin === void 0 ? {} : { origin: row.origin },
|
|
502
|
+
...row.stagingExpiresAt === void 0 ? {} : { stagingExpiresAt: row.stagingExpiresAt },
|
|
503
|
+
actions: row.state === "pending" ? ["/evolution merge approve " + row.id, "/evolution merge reject " + row.id] : row.state === "skipped" ? ["/evolution merge skipped --restore " + row.id] : ["/evolution merge resume " + row.id]
|
|
278
504
|
};
|
|
279
505
|
}
|
|
280
|
-
function buildMergeView(rows, commands) {
|
|
506
|
+
function buildMergeView(rows, commands, latestRun, approval) {
|
|
281
507
|
const counts = countRows(rows);
|
|
282
508
|
return {
|
|
283
509
|
schemaVersion: 1,
|
|
@@ -296,7 +522,9 @@ function buildMergeView(rows, commands) {
|
|
|
296
522
|
count: counts.skipped
|
|
297
523
|
}
|
|
298
524
|
],
|
|
299
|
-
commands
|
|
525
|
+
commands,
|
|
526
|
+
...latestRun === void 0 ? {} : { latestRun },
|
|
527
|
+
...approval === void 0 ? {} : { approval }
|
|
300
528
|
};
|
|
301
529
|
}
|
|
302
530
|
function pageQueue(rows, queue, cursor = 0, limit = 50) {
|
|
@@ -348,9 +576,10 @@ function makeMergeRoutes(deps, _audit) {
|
|
|
348
576
|
if (denied !== null) return denied;
|
|
349
577
|
const source = deps.getSource();
|
|
350
578
|
if (source === void 0) return absent();
|
|
579
|
+
const latestMerge = (await source.runs(void 0, 20)).find((run) => run.kind === "merge");
|
|
351
580
|
return json(200, {
|
|
352
581
|
ok: true,
|
|
353
|
-
data: buildMergeView(await source.rows(), source.commands())
|
|
582
|
+
data: buildMergeView(await source.rows(), source.commands(), latestMerge === void 0 ? void 0 : toRunFact(latestMerge), source.approval?.())
|
|
354
583
|
});
|
|
355
584
|
}
|
|
356
585
|
},
|
|
@@ -431,7 +660,7 @@ function makeMergeRoutes(deps, _audit) {
|
|
|
431
660
|
}
|
|
432
661
|
//#endregion
|
|
433
662
|
//#region lib/types/commands.js
|
|
434
|
-
const USAGE = "usage: merge scan | pair <a> <b> | status [id] | report [id] | cancel [id] | runs | doctor | pending | approve <id> | reject <id> | resume [id] [--clear <id>] | skipped [--restore <id>] | undo <target> --run <id>";
|
|
663
|
+
const USAGE = "usage: /evolution merge scan | pair <a> <b> | status [id] | report [id] | cancel [id] | runs | doctor | pending | approve <id> | reject <id> | resume [id] [--clear <id>] | skipped [--restore <id>] | undo <target> --run <id>";
|
|
435
664
|
function flags(args, allowed) {
|
|
436
665
|
const values = /* @__PURE__ */ new Map();
|
|
437
666
|
for (let index = 0; index < args.length; index += 1) {
|
|
@@ -632,7 +861,7 @@ function parseMergeCommand(argv) {
|
|
|
632
861
|
default: return {
|
|
633
862
|
ok: false,
|
|
634
863
|
code: "usage",
|
|
635
|
-
message: "unknown merge command: " + verb + "\nusage: merge scan | pair <a> <b> | status [id] | report [id] | cancel [id] | runs | doctor | pending | approve <id> | reject <id> | resume [id] [--clear <id>] | skipped [--restore <id>] | undo <target> --run <id>"
|
|
864
|
+
message: "unknown merge command: " + verb + "\nusage: /evolution merge scan | pair <a> <b> | status [id] | report [id] | cancel [id] | runs | doctor | pending | approve <id> | reject <id> | resume [id] [--clear <id>] | skipped [--restore <id>] | undo <target> --run <id>"
|
|
636
865
|
};
|
|
637
866
|
}
|
|
638
867
|
}
|
|
@@ -671,35 +900,39 @@ function createReadOnlyMergeService(io, home) {
|
|
|
671
900
|
};
|
|
672
901
|
} };
|
|
673
902
|
}
|
|
903
|
+
async function runMergeCommand(service, rawInput, invocation) {
|
|
904
|
+
const tokens = rawInput.trim().split(/\s+/).filter(Boolean);
|
|
905
|
+
if (tokens[0] === "merge" || tokens[0] === "/merge" || tokens[0] === "evolution" || tokens[0] === "/evolution") tokens.shift();
|
|
906
|
+
if (tokens[0] === "merge") tokens.shift();
|
|
907
|
+
const parsed = parseMergeCommand(tokens);
|
|
908
|
+
if (!parsed.ok) return {
|
|
909
|
+
ok: false,
|
|
910
|
+
text: parsed.message
|
|
911
|
+
};
|
|
912
|
+
if (parsed.help === true) return {
|
|
913
|
+
ok: true,
|
|
914
|
+
text: mergeUsage()
|
|
915
|
+
};
|
|
916
|
+
try {
|
|
917
|
+
return await service.execute(parsed.command, invocation?.agent === void 0 ? {} : { parent: invocation.agent });
|
|
918
|
+
} catch (error) {
|
|
919
|
+
return {
|
|
920
|
+
ok: false,
|
|
921
|
+
text: "merge 命令失败:" + (error instanceof Error ? error.message : String(error)) + "\n运行 /evolution merge status 查看状态。"
|
|
922
|
+
};
|
|
923
|
+
}
|
|
924
|
+
}
|
|
674
925
|
function createMergeCommand(service) {
|
|
675
926
|
return {
|
|
676
927
|
name: "merge",
|
|
677
|
-
description: "技能自动合并:扫描、待批准、重跑、报告与 undo",
|
|
928
|
+
description: "技能自动合并:扫描、待批准、重跑、报告与 undo(兼容别名;请用 /evolution merge)",
|
|
678
929
|
input: { hint: mergeUsage() },
|
|
679
930
|
async handler(invocation) {
|
|
680
|
-
const
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
kind: "error",
|
|
685
|
-
text: parsed.message
|
|
686
|
-
};
|
|
687
|
-
if (parsed.help === true) return {
|
|
688
|
-
kind: "success",
|
|
689
|
-
text: mergeUsage()
|
|
931
|
+
const result = await runMergeCommand(service, invocation.rawInput, invocation);
|
|
932
|
+
return {
|
|
933
|
+
kind: result.ok ? "success" : "error",
|
|
934
|
+
text: result.text
|
|
690
935
|
};
|
|
691
|
-
try {
|
|
692
|
-
const result = await service.execute(parsed.command, { ...invocation.agent === void 0 ? {} : { parent: invocation.agent } });
|
|
693
|
-
return {
|
|
694
|
-
kind: result.ok ? "success" : "error",
|
|
695
|
-
text: result.text
|
|
696
|
-
};
|
|
697
|
-
} catch (error) {
|
|
698
|
-
return {
|
|
699
|
-
kind: "error",
|
|
700
|
-
text: "merge 命令失败:" + (error instanceof Error ? error.message : String(error)) + "\n运行 /merge status 查看状态。"
|
|
701
|
-
};
|
|
702
|
-
}
|
|
703
936
|
}
|
|
704
937
|
};
|
|
705
938
|
}
|
|
@@ -726,99 +959,6 @@ function isSkippedPair(keys, a, b) {
|
|
|
726
959
|
return keys.has(pairKey(a, b));
|
|
727
960
|
}
|
|
728
961
|
//#endregion
|
|
729
|
-
//#region lib/types/coordinator.js
|
|
730
|
-
/**
|
|
731
|
-
* Pure merge transition table. No io, no run shell, no subagents.
|
|
732
|
-
*
|
|
733
|
-
* Every transition returns the whole next row (or a drop), so the caller can persist it
|
|
734
|
-
* through mutateMergeState in one transaction. Invalid transitions are refused by name;
|
|
735
|
-
* a caller never "repairs" a row by guessing which state the operator meant.
|
|
736
|
-
*/
|
|
737
|
-
function invalid(event, state) {
|
|
738
|
-
return {
|
|
739
|
-
ok: false,
|
|
740
|
-
code: "invalid-transition",
|
|
741
|
-
message: "cannot apply " + event.type + " to " + (state ?? "no row")
|
|
742
|
-
};
|
|
743
|
-
}
|
|
744
|
-
/** Drop fields that must not survive a transition; optional fields stay absent. */
|
|
745
|
-
function base(row, at) {
|
|
746
|
-
return {
|
|
747
|
-
id: row.id,
|
|
748
|
-
pairKey: row.pairKey,
|
|
749
|
-
a: row.a,
|
|
750
|
-
b: row.b,
|
|
751
|
-
state: row.state,
|
|
752
|
-
queue: row.queue,
|
|
753
|
-
createdAt: row.createdAt,
|
|
754
|
-
updatedAt: at,
|
|
755
|
-
...row.runId === void 0 ? {} : { runId: row.runId },
|
|
756
|
-
...row.unitId === void 0 ? {} : { unitId: row.unitId },
|
|
757
|
-
...row.origin === void 0 ? {} : { origin: row.origin },
|
|
758
|
-
...row.decision === void 0 ? {} : { decision: row.decision },
|
|
759
|
-
...row.attempts === void 0 ? {} : { attempts: row.attempts }
|
|
760
|
-
};
|
|
761
|
-
}
|
|
762
|
-
function next(row, at, state, patch = {}) {
|
|
763
|
-
return {
|
|
764
|
-
ok: true,
|
|
765
|
-
row: {
|
|
766
|
-
...base(row, at),
|
|
767
|
-
state,
|
|
768
|
-
queue: queueOf(state),
|
|
769
|
-
...patch
|
|
770
|
-
}
|
|
771
|
-
};
|
|
772
|
-
}
|
|
773
|
-
function transition(current, event) {
|
|
774
|
-
if (event.type === "candidate") {
|
|
775
|
-
if (current !== void 0) return {
|
|
776
|
-
ok: true,
|
|
777
|
-
row: current
|
|
778
|
-
};
|
|
779
|
-
const key = pairKey(event.a, event.b);
|
|
780
|
-
return {
|
|
781
|
-
ok: true,
|
|
782
|
-
row: {
|
|
783
|
-
id: rowIdOf(key),
|
|
784
|
-
pairKey: key,
|
|
785
|
-
a: event.a,
|
|
786
|
-
b: event.b,
|
|
787
|
-
state: "verify",
|
|
788
|
-
queue: "retry",
|
|
789
|
-
createdAt: event.at,
|
|
790
|
-
updatedAt: event.at
|
|
791
|
-
}
|
|
792
|
-
};
|
|
793
|
-
}
|
|
794
|
-
if (event.type === "source-gone") return {
|
|
795
|
-
ok: true,
|
|
796
|
-
row: null
|
|
797
|
-
};
|
|
798
|
-
if (current === void 0) return invalid(event, void 0);
|
|
799
|
-
switch (event.type) {
|
|
800
|
-
case "round1-keep": return current.state === "verify" ? next(current, event.at, "skipped", { origin: "agent" }) : invalid(event, current.state);
|
|
801
|
-
case "round1-merge":
|
|
802
|
-
if (current.state !== "verify") return invalid(event, current.state);
|
|
803
|
-
return event.approval === "on" ? next(current, event.at, "pending", event.decision === void 0 ? {} : { decision: event.decision }) : next(current, event.at, "fuse", event.decision === void 0 ? {} : { decision: event.decision });
|
|
804
|
-
case "round1-fail": return current.state === "verify" ? next(current, event.at, "verify", {
|
|
805
|
-
failure: event.failure,
|
|
806
|
-
attempts: (current.attempts ?? 0) + 1
|
|
807
|
-
}) : invalid(event, current.state);
|
|
808
|
-
case "approve": return current.state === "pending" ? next(current, event.at, "fuse", { ...event.runId === void 0 ? {} : { runId: event.runId } }) : invalid(event, current.state);
|
|
809
|
-
case "reject": return current.state === "pending" ? next(current, event.at, "skipped", { origin: "human" }) : invalid(event, current.state);
|
|
810
|
-
case "round2-fail": return current.state === "fuse" ? next(current, event.at, "fuse", {
|
|
811
|
-
failure: event.failure,
|
|
812
|
-
attempts: (current.attempts ?? 0) + 1
|
|
813
|
-
}) : invalid(event, current.state);
|
|
814
|
-
case "round2-success": return current.state === "fuse" ? {
|
|
815
|
-
ok: true,
|
|
816
|
-
row: null
|
|
817
|
-
} : invalid(event, current.state);
|
|
818
|
-
case "restore": return current.state === "skipped" ? next(current, event.at, "verify") : invalid(event, current.state);
|
|
819
|
-
}
|
|
820
|
-
}
|
|
821
|
-
//#endregion
|
|
822
962
|
//#region lib/types/scan-run.js
|
|
823
963
|
/**
|
|
824
964
|
* The scan pair loop. Registry/report lifecycle stays in withMergeRun; this module owns
|
|
@@ -852,6 +992,7 @@ function pairOf(row, candidate, outcome, extra = {}) {
|
|
|
852
992
|
}
|
|
853
993
|
async function runScan(options) {
|
|
854
994
|
const stage = options.stage ?? (async () => {});
|
|
995
|
+
const candidates = await options.ports.screening(options.signal);
|
|
855
996
|
const before = await loadMergeState(options.io, options.home);
|
|
856
997
|
const queueBefore = {
|
|
857
998
|
pending: 0,
|
|
@@ -859,7 +1000,6 @@ async function runScan(options) {
|
|
|
859
1000
|
skipped: 0
|
|
860
1001
|
};
|
|
861
1002
|
for (const row of before.state.rows) queueBefore[row.queue] += 1;
|
|
862
|
-
const candidates = await options.ports.screening(options.signal);
|
|
863
1003
|
const startedAt = options.startedAt ?? options.now();
|
|
864
1004
|
const timeoutMs = (options.timeoutMinutes ?? 0) > 0 ? options.timeoutMinutes * 6e4 : 0;
|
|
865
1005
|
const pairs = [];
|
|
@@ -965,6 +1105,8 @@ async function runScan(options) {
|
|
|
965
1105
|
type: "round1-merge",
|
|
966
1106
|
approval: "on",
|
|
967
1107
|
decision: round1.decision,
|
|
1108
|
+
...round1.stagingRef === void 0 ? {} : { stagingRef: round1.stagingRef },
|
|
1109
|
+
...round1.stagingExpiresAt === void 0 ? {} : { stagingExpiresAt: round1.stagingExpiresAt },
|
|
968
1110
|
at: at(options.now)
|
|
969
1111
|
});
|
|
970
1112
|
if (next.ok && next.row !== null) {
|
|
@@ -979,6 +1121,8 @@ async function runScan(options) {
|
|
|
979
1121
|
type: "round1-merge",
|
|
980
1122
|
approval: "off",
|
|
981
1123
|
decision: round1.decision,
|
|
1124
|
+
...round1.stagingRef === void 0 ? {} : { stagingRef: round1.stagingRef },
|
|
1125
|
+
...round1.stagingExpiresAt === void 0 ? {} : { stagingExpiresAt: round1.stagingExpiresAt },
|
|
982
1126
|
at: at(options.now)
|
|
983
1127
|
});
|
|
984
1128
|
if (!fuse.ok || fuse.row === null) {
|
|
@@ -1037,7 +1181,7 @@ async function runScan(options) {
|
|
|
1037
1181
|
return {
|
|
1038
1182
|
stopReason,
|
|
1039
1183
|
scope: {
|
|
1040
|
-
candidatePairs: candidates.length,
|
|
1184
|
+
candidatePairs: candidates.candidatePairs ?? candidates.length,
|
|
1041
1185
|
queueBefore
|
|
1042
1186
|
},
|
|
1043
1187
|
pairs,
|
|
@@ -1519,6 +1663,9 @@ async function runResumeCommand(options) {
|
|
|
1519
1663
|
* half-created artifact and reads as unknown, never as "empty".
|
|
1520
1664
|
*/
|
|
1521
1665
|
const STAGING_SCHEMA_VERSION = 1;
|
|
1666
|
+
function stageKeyOf(runId, pairKey) {
|
|
1667
|
+
return runId + "/pairs/" + createHash("sha256").update(pairKey).digest("hex").slice(0, 16);
|
|
1668
|
+
}
|
|
1522
1669
|
function stagingRoot(home, runId) {
|
|
1523
1670
|
return join(home, "merge", "runs", runId);
|
|
1524
1671
|
}
|
|
@@ -1561,6 +1708,7 @@ async function createStaging(io, home, input) {
|
|
|
1561
1708
|
schemaVersion: 1,
|
|
1562
1709
|
runId: input.runId,
|
|
1563
1710
|
target: input.target,
|
|
1711
|
+
...input.mode === void 0 ? {} : { mode: input.mode },
|
|
1564
1712
|
sources: input.sources.map((source) => source.name),
|
|
1565
1713
|
promptVersion: input.promptVersion,
|
|
1566
1714
|
createdAt: at,
|
|
@@ -1601,6 +1749,25 @@ async function readStaging(io, home, runId) {
|
|
|
1601
1749
|
return probeUnknown("staging manifest is not JSON: " + (error instanceof Error ? error.message : String(error)));
|
|
1602
1750
|
}
|
|
1603
1751
|
}
|
|
1752
|
+
async function ensureStaging(io, home, input) {
|
|
1753
|
+
const existing = await readStaging(io, home, input.runId);
|
|
1754
|
+
if (existing.kind === "present") return {
|
|
1755
|
+
ok: true,
|
|
1756
|
+
path: stagingRoot(home, input.runId),
|
|
1757
|
+
manifest: existing.value.manifest
|
|
1758
|
+
};
|
|
1759
|
+
return createStaging(io, home, input);
|
|
1760
|
+
}
|
|
1761
|
+
async function setStagingTarget(io, home, stageKey, target, mode) {
|
|
1762
|
+
const view = await readStaging(io, home, stageKey);
|
|
1763
|
+
if (view.kind !== "present") return false;
|
|
1764
|
+
const manifest = Object.assign({}, view.value.manifest, { target }, mode === void 0 ? {} : { mode });
|
|
1765
|
+
await io.writeText(join(stagingRoot(home, stageKey), "manifest.json"), JSON.stringify(manifest, null, 2) + "\n");
|
|
1766
|
+
return true;
|
|
1767
|
+
}
|
|
1768
|
+
async function dropStaging(io, home, stageKey) {
|
|
1769
|
+
await io.remove(stagingRoot(home, stageKey));
|
|
1770
|
+
}
|
|
1604
1771
|
//#endregion
|
|
1605
1772
|
//#region lib/types/commit-receipt.js
|
|
1606
1773
|
/**
|
|
@@ -1612,13 +1779,14 @@ async function readStaging(io, home, runId) {
|
|
|
1612
1779
|
function commitReceiptPath(home, runId) {
|
|
1613
1780
|
return join(stagingRoot(home, runId), "commit.json");
|
|
1614
1781
|
}
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1782
|
+
function durableReceiptDir(home, parentRunId) {
|
|
1783
|
+
return join(home, "merge", "receipts", parentRunId);
|
|
1784
|
+
}
|
|
1785
|
+
function durableReceiptPath(home, parentRunId, stageKey) {
|
|
1786
|
+
const leaf = stageKey.split("/").pop() ?? stageKey;
|
|
1787
|
+
return join(durableReceiptDir(home, parentRunId), leaf + ".json");
|
|
1619
1788
|
}
|
|
1620
|
-
|
|
1621
|
-
const raw = await io.readText(commitReceiptPath(home, runId));
|
|
1789
|
+
function parseReceipt(raw) {
|
|
1622
1790
|
if (raw === null) return null;
|
|
1623
1791
|
try {
|
|
1624
1792
|
const parsed = JSON.parse(raw);
|
|
@@ -1628,6 +1796,28 @@ async function readCommitReceipt(io, home, runId) {
|
|
|
1628
1796
|
return null;
|
|
1629
1797
|
}
|
|
1630
1798
|
}
|
|
1799
|
+
async function writeCommitReceipt(io, home, receipt) {
|
|
1800
|
+
const path = commitReceiptPath(home, receipt.runId);
|
|
1801
|
+
await io.writeText(path, JSON.stringify(receipt, null, 2) + "\n");
|
|
1802
|
+
const parent = receipt.parentRunId ?? receipt.runId.split("/pairs/")[0] ?? receipt.runId;
|
|
1803
|
+
await io.writeText(durableReceiptPath(home, parent, receipt.runId), JSON.stringify({
|
|
1804
|
+
...receipt,
|
|
1805
|
+
parentRunId: parent
|
|
1806
|
+
}, null, 2) + "\n");
|
|
1807
|
+
return path;
|
|
1808
|
+
}
|
|
1809
|
+
async function readCommitReceipt(io, home, runId, target) {
|
|
1810
|
+
const direct = parseReceipt(await io.readText(commitReceiptPath(home, runId)));
|
|
1811
|
+
if (direct !== null && (target === void 0 || direct.target === target)) return direct;
|
|
1812
|
+
try {
|
|
1813
|
+
const names = await io.list(durableReceiptDir(home, runId));
|
|
1814
|
+
for (const name of names.sort()) {
|
|
1815
|
+
const receipt = parseReceipt(await io.readText(join(durableReceiptDir(home, runId), name)));
|
|
1816
|
+
if (receipt !== null && (target === void 0 || receipt.target === target)) return receipt;
|
|
1817
|
+
}
|
|
1818
|
+
} catch {}
|
|
1819
|
+
return null;
|
|
1820
|
+
}
|
|
1631
1821
|
//#endregion
|
|
1632
1822
|
//#region lib/types/undo-merge.js
|
|
1633
1823
|
async function undoMergeReceipt(receipt, port) {
|
|
@@ -1695,267 +1885,6 @@ function skillLibraryUndoPort(skills) {
|
|
|
1695
1885
|
};
|
|
1696
1886
|
}
|
|
1697
1887
|
//#endregion
|
|
1698
|
-
//#region lib/types/ladder.js
|
|
1699
|
-
/**
|
|
1700
|
-
* L0–L2 candidate gate. This is a CANDIDATE classifier, not a content quality judge.
|
|
1701
|
-
* Routing follows the frozen table: L0 -1 stops; L0 1/0 → L1; L1 -1 stops; L1 0/1 → L2;
|
|
1702
|
-
* L2 1 → agent1; L2 0 → agent1 (undecided); L2 -1 → skipped.
|
|
1703
|
-
*/
|
|
1704
|
-
var GateCache = class GateCache {
|
|
1705
|
-
entries = /* @__PURE__ */ new Map();
|
|
1706
|
-
get(key) {
|
|
1707
|
-
return this.entries.get(key);
|
|
1708
|
-
}
|
|
1709
|
-
put(key, value) {
|
|
1710
|
-
this.entries.set(key, value);
|
|
1711
|
-
}
|
|
1712
|
-
get size() {
|
|
1713
|
-
return this.entries.size;
|
|
1714
|
-
}
|
|
1715
|
-
toObject() {
|
|
1716
|
-
return Object.fromEntries(this.entries);
|
|
1717
|
-
}
|
|
1718
|
-
static fromObject(value) {
|
|
1719
|
-
const cache = new GateCache();
|
|
1720
|
-
if (typeof value !== "object" || value === null) return cache;
|
|
1721
|
-
for (const [key, entry] of Object.entries(value)) {
|
|
1722
|
-
if (typeof entry !== "object" || entry === null) continue;
|
|
1723
|
-
const verdict = entry;
|
|
1724
|
-
if (verdict.verdict === 1 || verdict.verdict === 0 || verdict.verdict === -1) cache.put(key, {
|
|
1725
|
-
level: verdict.level === "L0" || verdict.level === "L1" || verdict.level === "L2" ? verdict.level : "L2",
|
|
1726
|
-
verdict: verdict.verdict,
|
|
1727
|
-
reason: typeof verdict.reason === "string" ? verdict.reason : "",
|
|
1728
|
-
evidence: Array.isArray(verdict.evidence) ? verdict.evidence : [],
|
|
1729
|
-
cached: true
|
|
1730
|
-
});
|
|
1731
|
-
}
|
|
1732
|
-
return cache;
|
|
1733
|
-
}
|
|
1734
|
-
};
|
|
1735
|
-
function gateCacheKey(level, payload, model) {
|
|
1736
|
-
return createHash("sha256").update(JSON.stringify({
|
|
1737
|
-
level,
|
|
1738
|
-
payload,
|
|
1739
|
-
model
|
|
1740
|
-
})).digest("hex");
|
|
1741
|
-
}
|
|
1742
|
-
function payloadFor(level, candidate, facts) {
|
|
1743
|
-
const a = facts.get(candidate.a);
|
|
1744
|
-
const b = facts.get(candidate.b);
|
|
1745
|
-
if (a === void 0 || b === void 0) throw new Error("candidate names are not in the fact set");
|
|
1746
|
-
const base = {
|
|
1747
|
-
level,
|
|
1748
|
-
skill_a: {
|
|
1749
|
-
name: a.name,
|
|
1750
|
-
description: a.description
|
|
1751
|
-
},
|
|
1752
|
-
skill_b: {
|
|
1753
|
-
name: b.name,
|
|
1754
|
-
description: b.description
|
|
1755
|
-
},
|
|
1756
|
-
scores: { jaccard_summary: candidate.score ?? null },
|
|
1757
|
-
schema: {
|
|
1758
|
-
verdict: "1 | 0 | -1",
|
|
1759
|
-
reason: "<=80 chars"
|
|
1760
|
-
}
|
|
1761
|
-
};
|
|
1762
|
-
if (level === "L0") return JSON.stringify(base);
|
|
1763
|
-
const withHead = {
|
|
1764
|
-
...base,
|
|
1765
|
-
skill_a: {
|
|
1766
|
-
...base.skill_a,
|
|
1767
|
-
head: a.body.slice(0, 2400)
|
|
1768
|
-
},
|
|
1769
|
-
skill_b: {
|
|
1770
|
-
...base.skill_b,
|
|
1771
|
-
head: b.body.slice(0, 2400)
|
|
1772
|
-
}
|
|
1773
|
-
};
|
|
1774
|
-
if (level === "L1") return JSON.stringify(withHead);
|
|
1775
|
-
return JSON.stringify({
|
|
1776
|
-
...withHead,
|
|
1777
|
-
skill_a: {
|
|
1778
|
-
...withHead.skill_a,
|
|
1779
|
-
body_excerpt: a.body.slice(0, 4e3)
|
|
1780
|
-
},
|
|
1781
|
-
skill_b: {
|
|
1782
|
-
...withHead.skill_b,
|
|
1783
|
-
body_excerpt: b.body.slice(0, 4e3)
|
|
1784
|
-
}
|
|
1785
|
-
});
|
|
1786
|
-
}
|
|
1787
|
-
const SYSTEM$1 = "Judge whether two skill documents describe the same reusable capability. Shared vocabulary or a shared preamble is not duplication. Return only strict JSON: {\"verdict\":1|0|-1,\"reason\":\"...\"}";
|
|
1788
|
-
function extractJson$1(text) {
|
|
1789
|
-
const body = /\`\`\`(?:json)?\s*([\s\S]*?)\`\`\`/i.exec(text)?.[1] ?? text;
|
|
1790
|
-
const start = body.indexOf("{");
|
|
1791
|
-
if (start < 0) return void 0;
|
|
1792
|
-
let depth = 0;
|
|
1793
|
-
for (let index = start; index < body.length; index += 1) if (body[index] === "{") depth += 1;
|
|
1794
|
-
else if (body[index] === "}") {
|
|
1795
|
-
depth -= 1;
|
|
1796
|
-
if (depth === 0) try {
|
|
1797
|
-
return JSON.parse(body.slice(start, index + 1));
|
|
1798
|
-
} catch {
|
|
1799
|
-
return;
|
|
1800
|
-
}
|
|
1801
|
-
}
|
|
1802
|
-
}
|
|
1803
|
-
function parseVerdict(level, raw, cached = false) {
|
|
1804
|
-
const value = typeof raw === "string" ? extractJson$1(raw) : raw;
|
|
1805
|
-
if (typeof value !== "object" || value === null) return void 0;
|
|
1806
|
-
const record = value;
|
|
1807
|
-
const verdict = record.verdict === 1 || record.verdict === "1" ? 1 : record.verdict === 0 || record.verdict === "0" ? 0 : record.verdict === -1 || record.verdict === "-1" ? -1 : void 0;
|
|
1808
|
-
if (verdict === void 0) return void 0;
|
|
1809
|
-
return {
|
|
1810
|
-
level,
|
|
1811
|
-
verdict,
|
|
1812
|
-
reason: typeof record.reason === "string" ? record.reason.slice(0, 80) : "",
|
|
1813
|
-
evidence: Array.isArray(record.evidence) ? record.evidence.filter((item) => typeof item === "object" && item !== null).map((item) => {
|
|
1814
|
-
const entry = item;
|
|
1815
|
-
return {
|
|
1816
|
-
path: typeof entry.path === "string" ? entry.path : "",
|
|
1817
|
-
line: typeof entry.line === "number" ? entry.line : -1,
|
|
1818
|
-
quote: typeof entry.quote === "string" ? entry.quote.slice(0, 120) : ""
|
|
1819
|
-
};
|
|
1820
|
-
}) : [],
|
|
1821
|
-
cached
|
|
1822
|
-
};
|
|
1823
|
-
}
|
|
1824
|
-
/** Deterministic 10% L0 audit selection from the run seed + pair key. */
|
|
1825
|
-
function selectedForAudit(pairKey, seed, rate = .1) {
|
|
1826
|
-
const digest = createHash("sha256").update(seed + "\0" + pairKey).digest();
|
|
1827
|
-
return digest[0] !== void 0 && digest[0] < Math.round(Math.max(0, Math.min(1, rate)) * 256);
|
|
1828
|
-
}
|
|
1829
|
-
async function runLadder(options) {
|
|
1830
|
-
const facts = new Map(options.facts.map((fact) => [fact.name, fact]));
|
|
1831
|
-
const verdicts = [];
|
|
1832
|
-
let calls = 0;
|
|
1833
|
-
let level = options.startLevel ?? "L0";
|
|
1834
|
-
for (;;) {
|
|
1835
|
-
let payload;
|
|
1836
|
-
try {
|
|
1837
|
-
payload = payloadFor(level, options.candidate, facts);
|
|
1838
|
-
} catch (error) {
|
|
1839
|
-
return {
|
|
1840
|
-
route: "error",
|
|
1841
|
-
verdicts,
|
|
1842
|
-
calls,
|
|
1843
|
-
error: error instanceof Error ? error.message : String(error)
|
|
1844
|
-
};
|
|
1845
|
-
}
|
|
1846
|
-
const key = gateCacheKey(level, payload, options.modelName);
|
|
1847
|
-
const cached = options.cache?.get(key);
|
|
1848
|
-
let verdict;
|
|
1849
|
-
if (cached !== void 0) verdict = {
|
|
1850
|
-
...cached,
|
|
1851
|
-
level,
|
|
1852
|
-
cached: true
|
|
1853
|
-
};
|
|
1854
|
-
else try {
|
|
1855
|
-
calls += 1;
|
|
1856
|
-
const raw = await options.model.callJson({
|
|
1857
|
-
system: SYSTEM$1,
|
|
1858
|
-
user: payload,
|
|
1859
|
-
maxTokens: 500,
|
|
1860
|
-
signal: options.signal
|
|
1861
|
-
});
|
|
1862
|
-
verdict = parseVerdict(level, raw);
|
|
1863
|
-
if (verdict !== void 0) options.cache?.put(key, verdict);
|
|
1864
|
-
} catch (error) {
|
|
1865
|
-
return {
|
|
1866
|
-
route: "error",
|
|
1867
|
-
verdicts,
|
|
1868
|
-
calls,
|
|
1869
|
-
error: error instanceof Error ? error.message : String(error)
|
|
1870
|
-
};
|
|
1871
|
-
}
|
|
1872
|
-
if (verdict === void 0) return {
|
|
1873
|
-
route: "error",
|
|
1874
|
-
verdicts,
|
|
1875
|
-
calls,
|
|
1876
|
-
error: "invalid gate JSON"
|
|
1877
|
-
};
|
|
1878
|
-
verdicts.push(verdict);
|
|
1879
|
-
if (level === "L0") {
|
|
1880
|
-
if (verdict.verdict === -1) return {
|
|
1881
|
-
route: "keep",
|
|
1882
|
-
verdicts,
|
|
1883
|
-
calls,
|
|
1884
|
-
droppedAt: "L0"
|
|
1885
|
-
};
|
|
1886
|
-
level = "L1";
|
|
1887
|
-
continue;
|
|
1888
|
-
}
|
|
1889
|
-
if (level === "L1") {
|
|
1890
|
-
if (verdict.verdict === -1) return {
|
|
1891
|
-
route: "keep",
|
|
1892
|
-
verdicts,
|
|
1893
|
-
calls,
|
|
1894
|
-
droppedAt: "L1"
|
|
1895
|
-
};
|
|
1896
|
-
level = "L2";
|
|
1897
|
-
continue;
|
|
1898
|
-
}
|
|
1899
|
-
if (verdict.verdict === 1) return {
|
|
1900
|
-
route: "merge",
|
|
1901
|
-
verdicts,
|
|
1902
|
-
calls
|
|
1903
|
-
};
|
|
1904
|
-
if (verdict.verdict === 0) return {
|
|
1905
|
-
route: "undecided",
|
|
1906
|
-
verdicts,
|
|
1907
|
-
calls
|
|
1908
|
-
};
|
|
1909
|
-
return {
|
|
1910
|
-
route: "keep",
|
|
1911
|
-
verdicts,
|
|
1912
|
-
calls,
|
|
1913
|
-
droppedAt: "L2"
|
|
1914
|
-
};
|
|
1915
|
-
}
|
|
1916
|
-
}
|
|
1917
|
-
//#endregion
|
|
1918
|
-
//#region lib/types/gate-cache.js
|
|
1919
|
-
/**
|
|
1920
|
-
* File-backed gate cache: <home>/merge/gate-cache.json.
|
|
1921
|
-
* Reads are three-state; a malformed file is reported, not overwritten. Writes are one
|
|
1922
|
-
* transact so two runs cannot interleave a partial cache.
|
|
1923
|
-
*/
|
|
1924
|
-
const GATE_CACHE_VERSION = 1;
|
|
1925
|
-
function gateCachePath(home) {
|
|
1926
|
-
return join(home, "merge", "gate-cache.json");
|
|
1927
|
-
}
|
|
1928
|
-
async function loadGateCache(io, home) {
|
|
1929
|
-
const probe = await probeText(io, gateCachePath(home));
|
|
1930
|
-
if (probe.kind === "absent") return { cache: new GateCache() };
|
|
1931
|
-
if (probe.kind === "unknown") return {
|
|
1932
|
-
cache: new GateCache(),
|
|
1933
|
-
note: probe.reason
|
|
1934
|
-
};
|
|
1935
|
-
try {
|
|
1936
|
-
const parsed = JSON.parse(probe.value);
|
|
1937
|
-
if (parsed.schemaVersion !== 1 || typeof parsed.entries !== "object" || parsed.entries === null) return {
|
|
1938
|
-
cache: new GateCache(),
|
|
1939
|
-
note: "gate cache has an unexpected shape"
|
|
1940
|
-
};
|
|
1941
|
-
return { cache: GateCache.fromObject(parsed.entries) };
|
|
1942
|
-
} catch (error) {
|
|
1943
|
-
return {
|
|
1944
|
-
cache: new GateCache(),
|
|
1945
|
-
note: "gate cache is not JSON: " + (error instanceof Error ? error.message : String(error))
|
|
1946
|
-
};
|
|
1947
|
-
}
|
|
1948
|
-
}
|
|
1949
|
-
async function saveGateCache(io, home, cache) {
|
|
1950
|
-
await transactIo(io, gateCachePath(home), () => JSON.stringify({
|
|
1951
|
-
schemaVersion: 1,
|
|
1952
|
-
entries: cache.toObject()
|
|
1953
|
-
}, null, 2) + "\n");
|
|
1954
|
-
}
|
|
1955
|
-
function hasGateCache(cache) {
|
|
1956
|
-
return cache.size > 0;
|
|
1957
|
-
}
|
|
1958
|
-
//#endregion
|
|
1959
1888
|
//#region lib/types/doctor.js
|
|
1960
1889
|
/**
|
|
1961
1890
|
* Merge doctor rows: state, pending, gate cache and staging are each probed read-only.
|
|
@@ -1963,7 +1892,7 @@ function hasGateCache(cache) {
|
|
|
1963
1892
|
* operator can act on. This module never repairs or writes.
|
|
1964
1893
|
*/
|
|
1965
1894
|
const MERGE_DOCTOR_SCHEMA_VERSION = 3;
|
|
1966
|
-
async function mergeDoctor(io, home) {
|
|
1895
|
+
async function mergeDoctor(io, home, configuredRoot) {
|
|
1967
1896
|
const rows = [];
|
|
1968
1897
|
const loaded = await loadMergeState(io, home);
|
|
1969
1898
|
const counts = countRows(loaded.state.rows);
|
|
@@ -1973,13 +1902,24 @@ async function mergeDoctor(io, home) {
|
|
|
1973
1902
|
status: loaded.note === void 0 ? "ok" : "warn",
|
|
1974
1903
|
detail: loaded.note ?? "pairs " + String(loaded.state.rows.length) + ";pending " + String(counts.pending) + " / retry " + String(counts.retry) + " / skipped " + String(counts.skipped)
|
|
1975
1904
|
});
|
|
1976
|
-
const
|
|
1977
|
-
rows.push({
|
|
1978
|
-
key: "
|
|
1979
|
-
label: "
|
|
1980
|
-
status:
|
|
1981
|
-
detail:
|
|
1905
|
+
const modelRoot = configuredRoot !== void 0 && configuredRoot !== "" ? configuredRoot : process.env.SKILL_MERGE_MODELS_ROOT;
|
|
1906
|
+
if (modelRoot === void 0 || modelRoot === "") rows.push({
|
|
1907
|
+
key: "models",
|
|
1908
|
+
label: "合并模型",
|
|
1909
|
+
status: "warn",
|
|
1910
|
+
detail: "未设置 SKILL_MERGE_MODELS_ROOT;合并扫描会 fail loud"
|
|
1982
1911
|
});
|
|
1912
|
+
else {
|
|
1913
|
+
const int8 = join(modelRoot, "potion-multilingual-128M-int8");
|
|
1914
|
+
const pca = join(modelRoot, "potion-multilingual-128M-pca64-int8");
|
|
1915
|
+
const ok = await io.exists(int8) && await io.exists(pca);
|
|
1916
|
+
rows.push({
|
|
1917
|
+
key: "models",
|
|
1918
|
+
label: "合并模型",
|
|
1919
|
+
status: ok ? "ok" : "warn",
|
|
1920
|
+
detail: ok ? modelRoot : "缺少 int8/pca64:" + modelRoot
|
|
1921
|
+
});
|
|
1922
|
+
}
|
|
1983
1923
|
const runsDir = join(home, "merge", "runs");
|
|
1984
1924
|
const listed = await probeList(io, runsDir);
|
|
1985
1925
|
rows.push({
|
|
@@ -2075,45 +2015,73 @@ function parseDecision(text, pair) {
|
|
|
2075
2015
|
}
|
|
2076
2016
|
//#endregion
|
|
2077
2017
|
//#region lib/types/prompts.js
|
|
2078
|
-
const MERGE_PROMPT_VERSION = "merge-
|
|
2079
|
-
const
|
|
2080
|
-
|
|
2018
|
+
const MERGE_PROMPT_VERSION = "merge-b/v1";
|
|
2019
|
+
const STANDBY_PROMPT = "You are a merge worker. Wait for instructions.";
|
|
2020
|
+
function sourcePath(name) {
|
|
2021
|
+
return "sources/" + name;
|
|
2022
|
+
}
|
|
2023
|
+
function mergedPath(target) {
|
|
2024
|
+
return "merged/" + target;
|
|
2025
|
+
}
|
|
2026
|
+
function wireDecision(decision) {
|
|
2027
|
+
return {
|
|
2028
|
+
shouldMerge: decision.shouldMerge,
|
|
2029
|
+
mode: decision.mode,
|
|
2030
|
+
target: decision.target,
|
|
2031
|
+
sources: decision.sources,
|
|
2032
|
+
rationale: decision.rationale,
|
|
2033
|
+
confidence: decision.confidence,
|
|
2034
|
+
a_summary: decision.aSummary,
|
|
2035
|
+
b_summary: decision.bSummary
|
|
2036
|
+
};
|
|
2037
|
+
}
|
|
2081
2038
|
function buildRound1Prompt(candidate, cards) {
|
|
2082
|
-
const byName = new Map(cards.map((card) => [card.name, card]));
|
|
2083
|
-
const a = byName.get(candidate.a);
|
|
2084
|
-
const b = byName.get(candidate.b);
|
|
2085
2039
|
return [
|
|
2086
|
-
|
|
2040
|
+
"你是技能合并决策 agent。第 1 轮只做判断,不要写文件、不要修改任何源目录。",
|
|
2041
|
+
"",
|
|
2042
|
+
"在当前工作目录下用 merge_stage 的 list/read 完整阅读两个源技能:",
|
|
2043
|
+
"- A: " + sourcePath(candidate.a) + "(name=" + candidate.a + ")",
|
|
2044
|
+
"- B: " + sourcePath(candidate.b) + "(name=" + candidate.b + ")",
|
|
2045
|
+
"包含 SKILL.md 与 references/templates/scripts 等支撑文件。",
|
|
2087
2046
|
"",
|
|
2088
|
-
"
|
|
2089
|
-
"
|
|
2047
|
+
"判断规则:",
|
|
2048
|
+
"1) 两者是否语义等价/互补到值得合并,而不是词面相似;",
|
|
2049
|
+
"2) 合并后是否更容易发现和使用,而不是变成更大的巨技能;",
|
|
2050
|
+
"3) 若合并会破坏触发面/生命周期/可发现性,应输出 keep。",
|
|
2090
2051
|
"",
|
|
2091
|
-
"
|
|
2092
|
-
|
|
2093
|
-
description: a?.description ?? "",
|
|
2094
|
-
body: (a?.body ?? "").slice(0, 6e3)
|
|
2095
|
-
}),
|
|
2096
|
-
"skill_b: " + JSON.stringify({
|
|
2097
|
-
name: candidate.b,
|
|
2098
|
-
description: b?.description ?? "",
|
|
2099
|
-
body: (b?.body ?? "").slice(0, 6e3)
|
|
2100
|
-
})
|
|
2052
|
+
"只输出一个 JSON(不要 markdown 围栏):",
|
|
2053
|
+
"{\"should_merge\":true|false,\"mode\":\"merge-into-existing|create-umbrella|keep\",\"target\":\"kebab-case\",\"sources\":[\"源技能名\"],\"rationale\":\"<=150字\",\"confidence\":0.0,\"a_summary\":\"A 的一句话摘要\",\"b_summary\":\"B 的一句话摘要\"}"
|
|
2101
2054
|
].join("\n");
|
|
2102
2055
|
}
|
|
2103
|
-
function buildRound2Prompt(row, decision) {
|
|
2104
|
-
|
|
2105
|
-
|
|
2056
|
+
function buildRound2Prompt(row, decision, mode = "continue") {
|
|
2057
|
+
const decisionJson = JSON.stringify(wireDecision(decision));
|
|
2058
|
+
const head = [
|
|
2059
|
+
"现在执行第 2 轮:把两个技能融合成一个完整、可用的技能,直接在当前工作目录的 merged/ 下写文件。",
|
|
2060
|
+
"不要修改 " + sourcePath(row.a) + " 或 " + sourcePath(row.b) + "(只读源副本);不要访问工作目录以外的路径。",
|
|
2061
|
+
""
|
|
2062
|
+
];
|
|
2063
|
+
const body = [
|
|
2064
|
+
"决策结果:" + decisionJson,
|
|
2065
|
+
"目标技能名:" + decision.target,
|
|
2106
2066
|
"",
|
|
2107
|
-
"
|
|
2108
|
-
"
|
|
2109
|
-
|
|
2110
|
-
|
|
2111
|
-
|
|
2112
|
-
"
|
|
2067
|
+
"工作方式:",
|
|
2068
|
+
"- 用 merge_stage 的 list/read/write/copy 创建 " + mergedPath(decision.target) + "/SKILL.md 与需要的 references/templates/scripts;",
|
|
2069
|
+
"- 可以复制源文件作为草稿基础,然后逐段修改;可以多轮读写;",
|
|
2070
|
+
"- 保留两源独特内容,不编造事实,不留下 TODO/占位符;",
|
|
2071
|
+
"- SKILL.md 以合法 YAML frontmatter 开头,name 等于目标名,description 不超过 60 字符;",
|
|
2072
|
+
"- 正文与支撑文件的引用必须成对,路径只允许 references/templates/scripts/assets 下的相对路径。",
|
|
2113
2073
|
"",
|
|
2114
|
-
"
|
|
2115
|
-
"
|
|
2116
|
-
]
|
|
2074
|
+
"完成后只输出一个 JSON(不要 markdown 围栏):",
|
|
2075
|
+
"{\"target\":\"" + decision.target + "\",\"files\":[\"SKILL.md\",\"references/...\"],\"changelog\":[\"一条变更\"],\"notes\":\"可选的一句话\"}"
|
|
2076
|
+
];
|
|
2077
|
+
return (mode === "fresh" ? [
|
|
2078
|
+
"基于已持久化的 decision。",
|
|
2079
|
+
...head,
|
|
2080
|
+
...body
|
|
2081
|
+
] : [...head, ...body]).join("\n");
|
|
2082
|
+
}
|
|
2083
|
+
function buildStandbyPrompt() {
|
|
2084
|
+
return STANDBY_PROMPT;
|
|
2117
2085
|
}
|
|
2118
2086
|
//#endregion
|
|
2119
2087
|
//#region lib/types/channel-port.js
|
|
@@ -2131,7 +2099,7 @@ async function runRound1OverChannel(options, candidate, signal) {
|
|
|
2131
2099
|
const opened = await options.channel.open({
|
|
2132
2100
|
parent: options.parent,
|
|
2133
2101
|
label: "merge-round1",
|
|
2134
|
-
promptText:
|
|
2102
|
+
promptText: STANDBY_PROMPT,
|
|
2135
2103
|
kind: "reviewer",
|
|
2136
2104
|
allow: options.allow,
|
|
2137
2105
|
...options.cwd === void 0 ? {} : { cwd: options.cwd },
|
|
@@ -2173,6 +2141,60 @@ async function runRound1OverChannel(options, candidate, signal) {
|
|
|
2173
2141
|
failure: {
|
|
2174
2142
|
stage: "decide",
|
|
2175
2143
|
code: opened.turn.stopReason,
|
|
2144
|
+
message: "standby did not complete",
|
|
2145
|
+
retryable: true,
|
|
2146
|
+
retryFrom: "agent1"
|
|
2147
|
+
}
|
|
2148
|
+
},
|
|
2149
|
+
dispose
|
|
2150
|
+
};
|
|
2151
|
+
}
|
|
2152
|
+
try {
|
|
2153
|
+
options.onChildOpened?.(opened.childId);
|
|
2154
|
+
} catch (error) {
|
|
2155
|
+
await dispose();
|
|
2156
|
+
return {
|
|
2157
|
+
outcome: {
|
|
2158
|
+
kind: "failed",
|
|
2159
|
+
failure: {
|
|
2160
|
+
stage: "decide",
|
|
2161
|
+
code: "child-bind",
|
|
2162
|
+
message: String(error),
|
|
2163
|
+
retryable: true,
|
|
2164
|
+
retryFrom: "agent1"
|
|
2165
|
+
}
|
|
2166
|
+
},
|
|
2167
|
+
dispose
|
|
2168
|
+
};
|
|
2169
|
+
}
|
|
2170
|
+
const round1 = await options.channel.continueTurn(opened.childId, buildRound1Prompt(candidate, options.cards), {
|
|
2171
|
+
timeoutMs: options.timeoutMs,
|
|
2172
|
+
signal
|
|
2173
|
+
});
|
|
2174
|
+
if (round1.kind !== "ok") {
|
|
2175
|
+
await dispose();
|
|
2176
|
+
return {
|
|
2177
|
+
outcome: {
|
|
2178
|
+
kind: "failed",
|
|
2179
|
+
failure: {
|
|
2180
|
+
stage: "decide",
|
|
2181
|
+
code: round1.kind,
|
|
2182
|
+
message: round1.kind === "unavailable" ? round1.reason : "round1 aborted",
|
|
2183
|
+
retryable: round1.kind !== "aborted",
|
|
2184
|
+
retryFrom: round1.kind === "aborted" ? "none" : "agent1"
|
|
2185
|
+
}
|
|
2186
|
+
},
|
|
2187
|
+
dispose
|
|
2188
|
+
};
|
|
2189
|
+
}
|
|
2190
|
+
if (round1.turn.stopReason !== "completed") {
|
|
2191
|
+
await dispose();
|
|
2192
|
+
return {
|
|
2193
|
+
outcome: {
|
|
2194
|
+
kind: "failed",
|
|
2195
|
+
failure: {
|
|
2196
|
+
stage: "decide",
|
|
2197
|
+
code: round1.turn.stopReason,
|
|
2176
2198
|
message: "round1 did not complete",
|
|
2177
2199
|
retryable: true,
|
|
2178
2200
|
retryFrom: "agent1"
|
|
@@ -2181,7 +2203,7 @@ async function runRound1OverChannel(options, candidate, signal) {
|
|
|
2181
2203
|
dispose
|
|
2182
2204
|
};
|
|
2183
2205
|
}
|
|
2184
|
-
const parsed = parseDecision(
|
|
2206
|
+
const parsed = parseDecision(round1.turn.text, candidate);
|
|
2185
2207
|
if (!parsed.ok) {
|
|
2186
2208
|
await dispose();
|
|
2187
2209
|
return {
|
|
@@ -2395,9 +2417,10 @@ function buildCommitPlan(input) {
|
|
|
2395
2417
|
ok: false,
|
|
2396
2418
|
message: "create flag disagrees with the live target"
|
|
2397
2419
|
};
|
|
2398
|
-
|
|
2420
|
+
const sources = input.sourceBodies.filter((source) => source.name !== input.target);
|
|
2421
|
+
if (sources.length === 0) return {
|
|
2399
2422
|
ok: false,
|
|
2400
|
-
message: "a merge needs at least one source"
|
|
2423
|
+
message: "a merge needs at least one source different from the target"
|
|
2401
2424
|
};
|
|
2402
2425
|
const body = rewriteFrontmatterName(input.mergedBody, input.target);
|
|
2403
2426
|
return {
|
|
@@ -2405,11 +2428,11 @@ function buildCommitPlan(input) {
|
|
|
2405
2428
|
plan: {
|
|
2406
2429
|
target: input.target,
|
|
2407
2430
|
targetExists: !input.create,
|
|
2408
|
-
sources:
|
|
2431
|
+
sources: sources.map((source) => source.name),
|
|
2409
2432
|
body,
|
|
2410
2433
|
supportFiles: input.mergedFiles,
|
|
2411
2434
|
expectedTargetBody: input.targetCurrent,
|
|
2412
|
-
expectedSourceBodies:
|
|
2435
|
+
expectedSourceBodies: sources,
|
|
2413
2436
|
origin: input.origin,
|
|
2414
2437
|
summary: input.summary
|
|
2415
2438
|
}
|
|
@@ -2428,7 +2451,25 @@ async function assembleStagedCommit(io, home, runId, port, options) {
|
|
|
2428
2451
|
ok: false,
|
|
2429
2452
|
message: "merged tree is not readable for run " + runId
|
|
2430
2453
|
};
|
|
2431
|
-
const
|
|
2454
|
+
const requested = staging.value.manifest.target;
|
|
2455
|
+
let target = requested;
|
|
2456
|
+
let targetCurrent = await port.readSkill(requested);
|
|
2457
|
+
const requestedArchived = targetCurrent === null && await port.isArchived?.(requested) === true;
|
|
2458
|
+
if (staging.value.manifest.mode === "create-umbrella" && (targetCurrent !== null || requestedArchived)) {
|
|
2459
|
+
target = requested;
|
|
2460
|
+
for (let suffix = 2; suffix <= 99; suffix += 1) {
|
|
2461
|
+
const candidate = requested + "-v" + String(suffix);
|
|
2462
|
+
if (await port.readSkill(candidate) === null && await port.isArchived?.(candidate) !== true) {
|
|
2463
|
+
target = candidate;
|
|
2464
|
+
targetCurrent = null;
|
|
2465
|
+
break;
|
|
2466
|
+
}
|
|
2467
|
+
}
|
|
2468
|
+
if (target === requested) return {
|
|
2469
|
+
ok: false,
|
|
2470
|
+
message: "no free umbrella target for " + requested
|
|
2471
|
+
};
|
|
2472
|
+
}
|
|
2432
2473
|
const sources = [];
|
|
2433
2474
|
for (const name of staging.value.manifest.sources) {
|
|
2434
2475
|
const body = await port.readSkill(name);
|
|
@@ -2442,7 +2483,7 @@ async function assembleStagedCommit(io, home, runId, port, options) {
|
|
|
2442
2483
|
});
|
|
2443
2484
|
}
|
|
2444
2485
|
return buildCommitPlan({
|
|
2445
|
-
target
|
|
2486
|
+
target,
|
|
2446
2487
|
create: targetCurrent === null,
|
|
2447
2488
|
mergedBody: tree.value.body,
|
|
2448
2489
|
mergedFiles: tree.value.files,
|
|
@@ -2477,6 +2518,7 @@ async function commitStagedRun(io, home, runId, port, options) {
|
|
|
2477
2518
|
};
|
|
2478
2519
|
const receipt = {
|
|
2479
2520
|
runId,
|
|
2521
|
+
parentRunId: runId.split("/pairs/")[0] ?? runId,
|
|
2480
2522
|
unitId: result.unitId,
|
|
2481
2523
|
target: assembled.plan.target,
|
|
2482
2524
|
create: !assembled.plan.targetExists,
|
|
@@ -2504,26 +2546,40 @@ async function commitStagedRun(io, home, runId, port, options) {
|
|
|
2504
2546
|
//#endregion
|
|
2505
2547
|
//#region lib/types/authoring-flow.js
|
|
2506
2548
|
async function runAuthoring(options) {
|
|
2507
|
-
const
|
|
2508
|
-
|
|
2509
|
-
target: options.target,
|
|
2510
|
-
sources: options.cards.map((card) => ({
|
|
2511
|
-
name: card.name,
|
|
2512
|
-
body: card.body,
|
|
2513
|
-
files: []
|
|
2514
|
-
})),
|
|
2515
|
-
promptVersion: "merge-pair/v1",
|
|
2516
|
-
ttlDays: 7,
|
|
2517
|
-
now: options.now
|
|
2518
|
-
});
|
|
2519
|
-
if (!created.ok) return {
|
|
2549
|
+
const targetCard = options.cards.find((card) => card.name === options.target);
|
|
2550
|
+
if (targetCard?.protectionUnknown === true || targetCard?.protectedBy === "pinned") return {
|
|
2520
2551
|
ok: false,
|
|
2521
|
-
message:
|
|
2552
|
+
message: "target is pinned: " + options.target
|
|
2522
2553
|
};
|
|
2554
|
+
const stageKey = options.stageKey ?? options.runId;
|
|
2555
|
+
if (options.reuseStaging === true) {
|
|
2556
|
+
if (!await setStagingTarget(options.io, options.home, stageKey, options.target, options.mode)) return {
|
|
2557
|
+
ok: false,
|
|
2558
|
+
message: "staging missing or unreadable: " + stageKey
|
|
2559
|
+
};
|
|
2560
|
+
} else {
|
|
2561
|
+
const created = await createStaging(options.io, options.home, {
|
|
2562
|
+
runId: stageKey,
|
|
2563
|
+
target: options.target,
|
|
2564
|
+
...options.mode === void 0 ? {} : { mode: options.mode },
|
|
2565
|
+
sources: options.cards.map((card) => ({
|
|
2566
|
+
name: card.name,
|
|
2567
|
+
body: card.body,
|
|
2568
|
+
files: card.files ?? []
|
|
2569
|
+
})),
|
|
2570
|
+
promptVersion: "merge-b/v1",
|
|
2571
|
+
ttlDays: 7,
|
|
2572
|
+
now: options.now
|
|
2573
|
+
});
|
|
2574
|
+
if (!created.ok) return {
|
|
2575
|
+
ok: false,
|
|
2576
|
+
message: created.message
|
|
2577
|
+
};
|
|
2578
|
+
}
|
|
2523
2579
|
try {
|
|
2524
2580
|
await options.author({
|
|
2525
2581
|
home: options.home,
|
|
2526
|
-
runId:
|
|
2582
|
+
runId: stageKey
|
|
2527
2583
|
});
|
|
2528
2584
|
} catch (error) {
|
|
2529
2585
|
return {
|
|
@@ -2531,7 +2587,7 @@ async function runAuthoring(options) {
|
|
|
2531
2587
|
message: "author failed: " + (error instanceof Error ? error.message : String(error))
|
|
2532
2588
|
};
|
|
2533
2589
|
}
|
|
2534
|
-
const tree = await readStagedTree(options.io, options.home,
|
|
2590
|
+
const tree = await readStagedTree(options.io, options.home, stageKey);
|
|
2535
2591
|
if (tree.kind !== "present") return {
|
|
2536
2592
|
ok: false,
|
|
2537
2593
|
message: "staged tree is not readable: " + (tree.kind === "unknown" ? tree.reason : "absent")
|
|
@@ -2542,7 +2598,7 @@ async function runAuthoring(options) {
|
|
|
2542
2598
|
message: "staged tree failed the rule gate",
|
|
2543
2599
|
findings
|
|
2544
2600
|
};
|
|
2545
|
-
const commit = await commitStagedRun(options.io, options.home,
|
|
2601
|
+
const commit = await commitStagedRun(options.io, options.home, stageKey, options.commitPort, {
|
|
2546
2602
|
origin: "subagent",
|
|
2547
2603
|
summary: options.summary,
|
|
2548
2604
|
now: options.now
|
|
@@ -2564,30 +2620,67 @@ function createChannelScanPorts(options) {
|
|
|
2564
2620
|
return {
|
|
2565
2621
|
screening: options.screening,
|
|
2566
2622
|
async round1(candidate, signal) {
|
|
2623
|
+
const stageKey = stageKeyOf(options.runId, pairKey(candidate.a, candidate.b));
|
|
2624
|
+
const staged = await ensureStaging(options.io, options.home, {
|
|
2625
|
+
runId: stageKey,
|
|
2626
|
+
target: candidate.a + "-" + candidate.b,
|
|
2627
|
+
sources: options.cards.filter((card) => card.name === candidate.a || card.name === candidate.b).map((card) => ({
|
|
2628
|
+
name: card.name,
|
|
2629
|
+
body: card.body,
|
|
2630
|
+
files: card.files ?? []
|
|
2631
|
+
})),
|
|
2632
|
+
promptVersion: "merge-b/v1",
|
|
2633
|
+
ttlDays: 7,
|
|
2634
|
+
now: options.now
|
|
2635
|
+
});
|
|
2636
|
+
if (!staged.ok) return {
|
|
2637
|
+
kind: "failed",
|
|
2638
|
+
failure: {
|
|
2639
|
+
stage: "decide",
|
|
2640
|
+
code: "staging-create",
|
|
2641
|
+
message: staged.message,
|
|
2642
|
+
retryable: true,
|
|
2643
|
+
retryFrom: "agent1"
|
|
2644
|
+
}
|
|
2645
|
+
};
|
|
2567
2646
|
const result = await runRound1OverChannel({
|
|
2568
2647
|
channel: options.channel,
|
|
2569
2648
|
parent: options.parent,
|
|
2570
2649
|
cards: options.cards,
|
|
2571
2650
|
allow: options.allow,
|
|
2572
|
-
|
|
2573
|
-
timeoutMs: options.timeoutMs
|
|
2651
|
+
cwd: stagingRoot(options.home, stageKey),
|
|
2652
|
+
timeoutMs: options.timeoutMs,
|
|
2653
|
+
onChildOpened: (childId) => options.onChild?.({
|
|
2654
|
+
runId: stageKey,
|
|
2655
|
+
childId,
|
|
2656
|
+
expectedChildId: childId,
|
|
2657
|
+
phase: "decide"
|
|
2658
|
+
})
|
|
2574
2659
|
}, candidate, signal);
|
|
2575
|
-
if (result.outcome.kind
|
|
2660
|
+
if (result.outcome.kind !== "merge") await dropStaging(options.io, options.home, stageKey);
|
|
2661
|
+
else if (options.approval) await result.dispose();
|
|
2576
2662
|
else {
|
|
2577
|
-
sessions.set(pairKey(candidate.a, candidate.b),
|
|
2663
|
+
sessions.set(pairKey(candidate.a, candidate.b), {
|
|
2664
|
+
result,
|
|
2665
|
+
stageKey
|
|
2666
|
+
});
|
|
2578
2667
|
if (result.childId !== void 0) options.onChild?.({
|
|
2579
|
-
runId:
|
|
2668
|
+
runId: stageKey,
|
|
2580
2669
|
childId: result.childId,
|
|
2581
2670
|
expectedChildId: result.childId,
|
|
2582
2671
|
phase: "author"
|
|
2583
2672
|
});
|
|
2584
2673
|
}
|
|
2585
|
-
return result.outcome
|
|
2674
|
+
return result.outcome.kind === "merge" ? {
|
|
2675
|
+
...result.outcome,
|
|
2676
|
+
stagingRef: stageKey,
|
|
2677
|
+
stagingExpiresAt: options.now() + 7 * 864e5
|
|
2678
|
+
} : result.outcome;
|
|
2586
2679
|
},
|
|
2587
2680
|
async round2(row, decision, signal) {
|
|
2588
2681
|
const key = pairKey(row.a, row.b);
|
|
2589
|
-
const
|
|
2590
|
-
if (
|
|
2682
|
+
const entry = sessions.get(key);
|
|
2683
|
+
if (entry === void 0) return {
|
|
2591
2684
|
kind: "failed",
|
|
2592
2685
|
failure: {
|
|
2593
2686
|
stage: "author",
|
|
@@ -2603,13 +2696,16 @@ function createChannelScanPorts(options) {
|
|
|
2603
2696
|
io: options.io,
|
|
2604
2697
|
home: options.home,
|
|
2605
2698
|
runId: options.runId,
|
|
2699
|
+
stageKey: entry.stageKey,
|
|
2700
|
+
reuseStaging: true,
|
|
2606
2701
|
target: decision.target,
|
|
2702
|
+
mode: decision.mode,
|
|
2607
2703
|
cards: options.cards,
|
|
2608
2704
|
commitPort: options.commitPort,
|
|
2609
2705
|
now: options.now,
|
|
2610
2706
|
summary: "merge " + row.a + " + " + row.b,
|
|
2611
2707
|
author: async () => {
|
|
2612
|
-
await
|
|
2708
|
+
await entry.result.author?.(row, decision, signal);
|
|
2613
2709
|
return "authored";
|
|
2614
2710
|
}
|
|
2615
2711
|
});
|
|
@@ -2623,20 +2719,20 @@ function createChannelScanPorts(options) {
|
|
|
2623
2719
|
retryFrom: "agent2"
|
|
2624
2720
|
}
|
|
2625
2721
|
};
|
|
2626
|
-
const receipt = authored.commit.
|
|
2722
|
+
const receipt = authored.commit.receipt;
|
|
2627
2723
|
return {
|
|
2628
2724
|
kind: "merged",
|
|
2629
2725
|
...receipt?.unitId === void 0 ? {} : { unitId: receipt.unitId },
|
|
2630
2726
|
archived: receipt?.archived ?? [],
|
|
2631
2727
|
target: {
|
|
2632
2728
|
requested: decision.target,
|
|
2633
|
-
actual:
|
|
2729
|
+
actual: authored.commit.plan.target,
|
|
2634
2730
|
created: receipt?.create ?? false
|
|
2635
2731
|
}
|
|
2636
2732
|
};
|
|
2637
2733
|
} finally {
|
|
2638
|
-
await
|
|
2639
|
-
if (
|
|
2734
|
+
await entry.result.dispose();
|
|
2735
|
+
if (entry.result.childId !== void 0) options.onChildClosed?.(entry.result.childId);
|
|
2640
2736
|
}
|
|
2641
2737
|
}
|
|
2642
2738
|
};
|
|
@@ -2644,35 +2740,51 @@ function createChannelScanPorts(options) {
|
|
|
2644
2740
|
//#endregion
|
|
2645
2741
|
//#region lib/types/new-child-authoring.js
|
|
2646
2742
|
async function runNewChildAuthoring(options, row, decision, signal) {
|
|
2647
|
-
const
|
|
2648
|
-
|
|
2649
|
-
target: decision.target,
|
|
2650
|
-
sources: options.cards.map((card) => ({
|
|
2651
|
-
name: card.name,
|
|
2652
|
-
body: card.body,
|
|
2653
|
-
files: []
|
|
2654
|
-
})),
|
|
2655
|
-
promptVersion: "merge-pair/v1",
|
|
2656
|
-
ttlDays: 7,
|
|
2657
|
-
now: options.now
|
|
2658
|
-
});
|
|
2659
|
-
if (!staging.ok) return {
|
|
2743
|
+
const targetCard = options.cards.find((card) => card.name === decision.target);
|
|
2744
|
+
if (targetCard?.protectionUnknown === true || targetCard?.protectedBy === "pinned") return {
|
|
2660
2745
|
kind: "failed",
|
|
2661
2746
|
failure: {
|
|
2662
2747
|
stage: "author",
|
|
2663
|
-
code: "
|
|
2664
|
-
message:
|
|
2665
|
-
retryable:
|
|
2666
|
-
retryFrom: "
|
|
2748
|
+
code: "target-pinned",
|
|
2749
|
+
message: "target is pinned: " + decision.target,
|
|
2750
|
+
retryable: false,
|
|
2751
|
+
retryFrom: "none"
|
|
2667
2752
|
}
|
|
2668
2753
|
};
|
|
2754
|
+
const stageKey = row.stagingRef ?? stageKeyOf(options.runId, pairKey(row.a, row.b));
|
|
2755
|
+
if ((await readStaging(options.io, options.home, stageKey)).kind === "present") await setStagingTarget(options.io, options.home, stageKey, decision.target, decision.mode);
|
|
2756
|
+
else {
|
|
2757
|
+
const staging = await createStaging(options.io, options.home, {
|
|
2758
|
+
runId: stageKey,
|
|
2759
|
+
target: decision.target,
|
|
2760
|
+
mode: decision.mode,
|
|
2761
|
+
sources: options.cards.filter((card) => card.name === row.a || card.name === row.b).map((card) => ({
|
|
2762
|
+
name: card.name,
|
|
2763
|
+
body: card.body,
|
|
2764
|
+
files: card.files ?? []
|
|
2765
|
+
})),
|
|
2766
|
+
promptVersion: "merge-b/v1",
|
|
2767
|
+
ttlDays: 7,
|
|
2768
|
+
now: options.now
|
|
2769
|
+
});
|
|
2770
|
+
if (!staging.ok) return {
|
|
2771
|
+
kind: "failed",
|
|
2772
|
+
failure: {
|
|
2773
|
+
stage: "author",
|
|
2774
|
+
code: "staging-create",
|
|
2775
|
+
message: staging.message,
|
|
2776
|
+
retryable: true,
|
|
2777
|
+
retryFrom: "agent2"
|
|
2778
|
+
}
|
|
2779
|
+
};
|
|
2780
|
+
}
|
|
2669
2781
|
const opened = await options.channel.open({
|
|
2670
2782
|
parent: options.parent,
|
|
2671
2783
|
label: "merge-round2",
|
|
2672
|
-
promptText:
|
|
2784
|
+
promptText: STANDBY_PROMPT,
|
|
2673
2785
|
kind: "maintainer",
|
|
2674
2786
|
allow: options.allow,
|
|
2675
|
-
|
|
2787
|
+
cwd: stagingRoot(options.home, stageKey),
|
|
2676
2788
|
timeoutMs: options.timeoutMs,
|
|
2677
2789
|
signal
|
|
2678
2790
|
});
|
|
@@ -2687,7 +2799,7 @@ async function runNewChildAuthoring(options, row, decision, signal) {
|
|
|
2687
2799
|
}
|
|
2688
2800
|
};
|
|
2689
2801
|
options.onChild?.({
|
|
2690
|
-
runId:
|
|
2802
|
+
runId: stageKey,
|
|
2691
2803
|
childId: opened.childId,
|
|
2692
2804
|
expectedChildId: opened.childId,
|
|
2693
2805
|
phase: "author"
|
|
@@ -2698,12 +2810,36 @@ async function runNewChildAuthoring(options, row, decision, signal) {
|
|
|
2698
2810
|
failure: {
|
|
2699
2811
|
stage: "author",
|
|
2700
2812
|
code: opened.turn.stopReason,
|
|
2813
|
+
message: "standby did not complete",
|
|
2814
|
+
retryable: true,
|
|
2815
|
+
retryFrom: "agent2"
|
|
2816
|
+
}
|
|
2817
|
+
};
|
|
2818
|
+
const turn = await options.channel.continueTurn(opened.childId, buildRound2Prompt(row, decision, "fresh"), {
|
|
2819
|
+
timeoutMs: options.timeoutMs,
|
|
2820
|
+
signal
|
|
2821
|
+
});
|
|
2822
|
+
if (turn.kind !== "ok") return {
|
|
2823
|
+
kind: "failed",
|
|
2824
|
+
failure: {
|
|
2825
|
+
stage: "author",
|
|
2826
|
+
code: turn.kind,
|
|
2827
|
+
message: turn.kind === "unavailable" ? turn.reason : "round2 aborted",
|
|
2828
|
+
retryable: turn.kind !== "aborted",
|
|
2829
|
+
retryFrom: turn.kind === "aborted" ? "none" : "agent2"
|
|
2830
|
+
}
|
|
2831
|
+
};
|
|
2832
|
+
if (turn.turn.stopReason !== "completed") return {
|
|
2833
|
+
kind: "failed",
|
|
2834
|
+
failure: {
|
|
2835
|
+
stage: "author",
|
|
2836
|
+
code: turn.turn.stopReason,
|
|
2701
2837
|
message: "round2 did not complete",
|
|
2702
2838
|
retryable: true,
|
|
2703
2839
|
retryFrom: "agent2"
|
|
2704
2840
|
}
|
|
2705
2841
|
};
|
|
2706
|
-
const tree = await readStagedTree(options.io, options.home,
|
|
2842
|
+
const tree = await readStagedTree(options.io, options.home, stageKey);
|
|
2707
2843
|
if (tree.kind !== "present") return {
|
|
2708
2844
|
kind: "failed",
|
|
2709
2845
|
failure: {
|
|
@@ -2724,7 +2860,7 @@ async function runNewChildAuthoring(options, row, decision, signal) {
|
|
|
2724
2860
|
retryFrom: "agent2"
|
|
2725
2861
|
}
|
|
2726
2862
|
};
|
|
2727
|
-
const commit = await commitStagedRun(options.io, options.home,
|
|
2863
|
+
const commit = await commitStagedRun(options.io, options.home, stageKey, options.commitPort, {
|
|
2728
2864
|
origin: "subagent",
|
|
2729
2865
|
summary: "merge " + row.a + " + " + row.b,
|
|
2730
2866
|
now: options.now
|
|
@@ -2746,7 +2882,7 @@ async function runNewChildAuthoring(options, row, decision, signal) {
|
|
|
2746
2882
|
archived: receipt?.archived ?? [],
|
|
2747
2883
|
target: {
|
|
2748
2884
|
requested: decision.target,
|
|
2749
|
-
actual:
|
|
2885
|
+
actual: commit.plan.target,
|
|
2750
2886
|
created: receipt?.create ?? false
|
|
2751
2887
|
}
|
|
2752
2888
|
};
|
|
@@ -2768,6 +2904,11 @@ function createNewChildRound2(options) {
|
|
|
2768
2904
|
function skillLibraryPort(skills) {
|
|
2769
2905
|
return {
|
|
2770
2906
|
readSkill: (name) => skills.read(name),
|
|
2907
|
+
isArchived: async (name) => {
|
|
2908
|
+
if (typeof skills.archivedNames !== "function") return false;
|
|
2909
|
+
const names = await skills.archivedNames();
|
|
2910
|
+
return names === null ? true : names.includes(name);
|
|
2911
|
+
},
|
|
2771
2912
|
commitMerge: (plan) => skills.commitMerge(plan)
|
|
2772
2913
|
};
|
|
2773
2914
|
}
|
|
@@ -2838,7 +2979,9 @@ function createMergeRuntime(deps) {
|
|
|
2838
2979
|
if (command.verb === "scan") {
|
|
2839
2980
|
const signal = new AbortController().signal;
|
|
2840
2981
|
const cards = await deps.cards(signal);
|
|
2841
|
-
const
|
|
2982
|
+
const loadedState = await loadMergeState(deps.io, deps.home);
|
|
2983
|
+
const skipped = skippedPairKeys(loadedState.state.rows, deps.params.mergeSkippedTtlDays, deps.now());
|
|
2984
|
+
const held = new Set(loadedState.state.rows.filter((row) => row.queue === "pending" || !canRetry(row)).map((row) => row.pairKey));
|
|
2842
2985
|
const result = await runScanCommand({
|
|
2843
2986
|
...common,
|
|
2844
2987
|
approval: deps.params.mergeApproval,
|
|
@@ -2848,7 +2991,13 @@ function createMergeRuntime(deps) {
|
|
|
2848
2991
|
runId,
|
|
2849
2992
|
cards,
|
|
2850
2993
|
screening: async (turnSignal) => {
|
|
2851
|
-
|
|
2994
|
+
const raw = await deps.screeningForRun(cards, runId, turnSignal);
|
|
2995
|
+
const filtered = raw.filter((candidate) => {
|
|
2996
|
+
const key = pairKey(candidate.a, candidate.b);
|
|
2997
|
+
return !skipped.has(key) && !held.has(key);
|
|
2998
|
+
});
|
|
2999
|
+
const candidatePairs = raw.candidatePairs;
|
|
3000
|
+
return candidatePairs === void 0 ? filtered : Object.assign(filtered, { candidatePairs });
|
|
2852
3001
|
},
|
|
2853
3002
|
channel: deps.channel,
|
|
2854
3003
|
parent,
|
|
@@ -2891,7 +3040,7 @@ function createMergeRuntime(deps) {
|
|
|
2891
3040
|
}
|
|
2892
3041
|
if (command.verb === "doctor") return {
|
|
2893
3042
|
ok: true,
|
|
2894
|
-
text: (await mergeDoctor(deps.io, deps.home)).map((row) => row.key + " [" + row.status + "] " + row.label + ":" + row.detail).join("\n")
|
|
3043
|
+
text: (await mergeDoctor(deps.io, deps.home, deps.params.mergeModelsRoot)).map((row) => row.key + " [" + row.status + "] " + row.label + ":" + row.detail).join("\n")
|
|
2895
3044
|
};
|
|
2896
3045
|
if (command.verb === "report" || command.verb === "runs" || command.verb === "cancel") {
|
|
2897
3046
|
const registry = deps.registry;
|
|
@@ -2939,7 +3088,7 @@ function createMergeRuntime(deps) {
|
|
|
2939
3088
|
};
|
|
2940
3089
|
}
|
|
2941
3090
|
if (command.verb === "undo") {
|
|
2942
|
-
const receipt = await readCommitReceipt(deps.io, deps.home, command.run);
|
|
3091
|
+
const receipt = await readCommitReceipt(deps.io, deps.home, command.run, command.target);
|
|
2943
3092
|
if (receipt === null) return {
|
|
2944
3093
|
ok: false,
|
|
2945
3094
|
text: "merge run has no commit receipt: " + command.run
|
|
@@ -3088,7 +3237,8 @@ const MERGE_PARAM_DEFAULTS = {
|
|
|
3088
3237
|
mergeApproval: false,
|
|
3089
3238
|
mergeSkippedTtlDays: 0,
|
|
3090
3239
|
mergeRunTimeoutMinutes: 0,
|
|
3091
|
-
mergePairTimeoutMinutes: 15
|
|
3240
|
+
mergePairTimeoutMinutes: 15,
|
|
3241
|
+
mergeModelsRoot: ""
|
|
3092
3242
|
};
|
|
3093
3243
|
const KEYS = new Set(Object.keys(MERGE_PARAM_DEFAULTS));
|
|
3094
3244
|
function materialize(value) {
|
|
@@ -3110,6 +3260,11 @@ function resolveMergeParams(input = {}) {
|
|
|
3110
3260
|
mergeApproval: approval,
|
|
3111
3261
|
mergeSkippedTtlDays: record.mergeSkippedTtlDays === void 0 ? MERGE_PARAM_DEFAULTS.mergeSkippedTtlDays : nonNegative("mergeSkippedTtlDays", record.mergeSkippedTtlDays),
|
|
3112
3262
|
mergeRunTimeoutMinutes: record.mergeRunTimeoutMinutes === void 0 ? MERGE_PARAM_DEFAULTS.mergeRunTimeoutMinutes : nonNegative("mergeRunTimeoutMinutes", record.mergeRunTimeoutMinutes),
|
|
3263
|
+
mergeModelsRoot: record.mergeModelsRoot === void 0 ? MERGE_PARAM_DEFAULTS.mergeModelsRoot : (() => {
|
|
3264
|
+
const value = materialize(record.mergeModelsRoot);
|
|
3265
|
+
if (typeof value !== "string") throw new Error("mergeModelsRoot must be a string");
|
|
3266
|
+
return value;
|
|
3267
|
+
})(),
|
|
3113
3268
|
mergePairTimeoutMinutes: record.mergePairTimeoutMinutes === void 0 ? MERGE_PARAM_DEFAULTS.mergePairTimeoutMinutes : (() => {
|
|
3114
3269
|
const value = nonNegative("mergePairTimeoutMinutes", record.mergePairTimeoutMinutes);
|
|
3115
3270
|
if (value === 0) throw new Error("mergePairTimeoutMinutes must be > 0");
|
|
@@ -3118,81 +3273,6 @@ function resolveMergeParams(input = {}) {
|
|
|
3118
3273
|
};
|
|
3119
3274
|
}
|
|
3120
3275
|
//#endregion
|
|
3121
|
-
//#region lib/types/screening.js
|
|
3122
|
-
/**
|
|
3123
|
-
* Deterministic candidate screening: name prefilter + lexical affinity.
|
|
3124
|
-
*
|
|
3125
|
-
* The semantic proposer (C3) is a separate port; this module is the zero-cost floor that
|
|
3126
|
-
* must work without a model. It reuses core `vocabulary`/`affinity` so drift/dedup and
|
|
3127
|
-
* merge screening share one tokenizer.
|
|
3128
|
-
*/
|
|
3129
|
-
/** Shannon entropy over characters; a cheap prefilter signal, not a name-quality judge. */
|
|
3130
|
-
function nameEntropy(name) {
|
|
3131
|
-
if (name === "") return 0;
|
|
3132
|
-
const counts = /* @__PURE__ */ new Map();
|
|
3133
|
-
for (const char of name) counts.set(char, (counts.get(char) ?? 0) + 1);
|
|
3134
|
-
let entropy = 0;
|
|
3135
|
-
for (const count of counts.values()) {
|
|
3136
|
-
const p = count / name.length;
|
|
3137
|
-
entropy -= p * Math.log2(p);
|
|
3138
|
-
}
|
|
3139
|
-
return entropy;
|
|
3140
|
-
}
|
|
3141
|
-
const MIN_NAME_ENTROPY = 1.5;
|
|
3142
|
-
const MIN_NAME_LENGTH = 6;
|
|
3143
|
-
function prefilter(cards) {
|
|
3144
|
-
return cards.filter((card) => {
|
|
3145
|
-
const name = card.name.trim();
|
|
3146
|
-
if (name.length < MIN_NAME_LENGTH) return false;
|
|
3147
|
-
if (nameEntropy(name) < MIN_NAME_ENTROPY) return false;
|
|
3148
|
-
return card.body.trim() !== "" || card.description.trim() !== "";
|
|
3149
|
-
});
|
|
3150
|
-
}
|
|
3151
|
-
function sortedPair(a, b) {
|
|
3152
|
-
return a < b ? [a, b] : [b, a];
|
|
3153
|
-
}
|
|
3154
|
-
function lexicalCandidates(cards, options = {}) {
|
|
3155
|
-
const bodyGate = options.bodyGate ?? .9;
|
|
3156
|
-
const summaryGate = options.summaryGate ?? .5;
|
|
3157
|
-
const minAffinity = options.minAffinity ?? .3;
|
|
3158
|
-
const eligible = prefilter(cards);
|
|
3159
|
-
const vectors = eligible.map((card) => ({
|
|
3160
|
-
card,
|
|
3161
|
-
summary: vocabulary(card.description),
|
|
3162
|
-
body: vocabulary(card.body)
|
|
3163
|
-
}));
|
|
3164
|
-
const candidates = [];
|
|
3165
|
-
for (let i = 0; i < vectors.length; i += 1) for (let j = i + 1; j < vectors.length; j += 1) {
|
|
3166
|
-
const left = vectors[i];
|
|
3167
|
-
const right = vectors[j];
|
|
3168
|
-
if (left === void 0 || right === void 0) continue;
|
|
3169
|
-
const summary = affinity(left.summary, right.summary);
|
|
3170
|
-
const body = affinity(left.body, right.body);
|
|
3171
|
-
const arms = [];
|
|
3172
|
-
if (body >= bodyGate) arms.push("body");
|
|
3173
|
-
if (summary >= summaryGate && body >= minAffinity) arms.push("summary");
|
|
3174
|
-
if (arms.length === 0) continue;
|
|
3175
|
-
const [a, b] = sortedPair(left.card.name, right.card.name);
|
|
3176
|
-
candidates.push({
|
|
3177
|
-
a,
|
|
3178
|
-
b,
|
|
3179
|
-
score: Math.max(body, summary),
|
|
3180
|
-
arms,
|
|
3181
|
-
source: "lexical"
|
|
3182
|
-
});
|
|
3183
|
-
}
|
|
3184
|
-
candidates.sort((left, right) => (right.score ?? 0) - (left.score ?? 0) || left.a.localeCompare(right.a) || left.b.localeCompare(right.b));
|
|
3185
|
-
const capped = options.maxCandidates === void 0 ? candidates : candidates.slice(0, options.maxCandidates);
|
|
3186
|
-
return {
|
|
3187
|
-
candidates: capped,
|
|
3188
|
-
stats: {
|
|
3189
|
-
skills: cards.length,
|
|
3190
|
-
filtered: cards.length - eligible.length,
|
|
3191
|
-
candidates: capped.length
|
|
3192
|
-
}
|
|
3193
|
-
};
|
|
3194
|
-
}
|
|
3195
|
-
//#endregion
|
|
3196
3276
|
//#region lib/types/adapters/channel.js
|
|
3197
3277
|
function convert(outcome) {
|
|
3198
3278
|
if (outcome.kind !== "ok") return outcome;
|
|
@@ -3230,313 +3310,6 @@ function subagentChannelAdapter(channel) {
|
|
|
3230
3310
|
};
|
|
3231
3311
|
}
|
|
3232
3312
|
//#endregion
|
|
3233
|
-
//#region lib/types/adapters/model.js
|
|
3234
|
-
function append(chunk, text) {
|
|
3235
|
-
if (chunk.type === "text-delta" || chunk.type === "text") return text + (chunk.text ?? "");
|
|
3236
|
-
if (chunk.type === "finish" && chunk.reason?.kind === "error") throw new Error("model call failed: " + (chunk.reason.failure?.message ?? "unknown"));
|
|
3237
|
-
return text;
|
|
3238
|
-
}
|
|
3239
|
-
function createModelPort(llm, route) {
|
|
3240
|
-
return { async callJson(request) {
|
|
3241
|
-
const selection = route();
|
|
3242
|
-
let text = "";
|
|
3243
|
-
if (llm.stream !== void 0) {
|
|
3244
|
-
for await (const chunk of llm.stream({
|
|
3245
|
-
provider: selection.provider,
|
|
3246
|
-
model: selection.model,
|
|
3247
|
-
system: request.system,
|
|
3248
|
-
messages: [{
|
|
3249
|
-
role: "user",
|
|
3250
|
-
content: [{
|
|
3251
|
-
type: "text",
|
|
3252
|
-
text: request.user
|
|
3253
|
-
}]
|
|
3254
|
-
}],
|
|
3255
|
-
maxTokens: request.maxTokens,
|
|
3256
|
-
temperature: 0,
|
|
3257
|
-
signal: request.signal
|
|
3258
|
-
})) text = append(chunk, text);
|
|
3259
|
-
return text;
|
|
3260
|
-
}
|
|
3261
|
-
if (llm.prepareCall === void 0) throw new Error("llm service has neither stream nor prepareCall");
|
|
3262
|
-
const call = await llm.prepareCall({
|
|
3263
|
-
provider: selection.provider,
|
|
3264
|
-
model: selection.model,
|
|
3265
|
-
maxTokens: request.maxTokens,
|
|
3266
|
-
temperature: 0
|
|
3267
|
-
}, request.signal);
|
|
3268
|
-
for await (const chunk of call.stream({
|
|
3269
|
-
...call.config,
|
|
3270
|
-
system: request.system,
|
|
3271
|
-
messages: [{
|
|
3272
|
-
role: "user",
|
|
3273
|
-
content: [{
|
|
3274
|
-
type: "text",
|
|
3275
|
-
text: request.user
|
|
3276
|
-
}]
|
|
3277
|
-
}],
|
|
3278
|
-
signal: request.signal
|
|
3279
|
-
})) text = append(chunk, text);
|
|
3280
|
-
return text;
|
|
3281
|
-
} };
|
|
3282
|
-
}
|
|
3283
|
-
//#endregion
|
|
3284
|
-
//#region lib/types/propose.js
|
|
3285
|
-
const SYSTEM = "You propose skill pairs that may be merged. Return only a JSON object: {\"pairs\":[{\"a\":\"skill-a\",\"b\":\"skill-b\",\"reason\":\"one sentence\"}]}. Use only supplied skill names. Do not decide whether a merge is good.";
|
|
3286
|
-
function parsePairs(raw, known, cap) {
|
|
3287
|
-
let value = raw;
|
|
3288
|
-
if (typeof raw === "string") try {
|
|
3289
|
-
value = JSON.parse(raw);
|
|
3290
|
-
} catch {
|
|
3291
|
-
return [];
|
|
3292
|
-
}
|
|
3293
|
-
if (typeof value !== "object" || value === null) return [];
|
|
3294
|
-
const pairs = value.pairs;
|
|
3295
|
-
if (!Array.isArray(pairs)) return [];
|
|
3296
|
-
const out = [];
|
|
3297
|
-
const seen = /* @__PURE__ */ new Set();
|
|
3298
|
-
for (const item of pairs) {
|
|
3299
|
-
if (out.length >= cap) break;
|
|
3300
|
-
const pair = item;
|
|
3301
|
-
if (typeof pair.a !== "string" || typeof pair.b !== "string") continue;
|
|
3302
|
-
const a = pair.a.trim();
|
|
3303
|
-
const b = pair.b.trim();
|
|
3304
|
-
if (a === b || !known.has(a) || !known.has(b)) continue;
|
|
3305
|
-
const [first, second] = a < b ? [a, b] : [b, a];
|
|
3306
|
-
const key = first + "\0" + second;
|
|
3307
|
-
if (seen.has(key)) continue;
|
|
3308
|
-
seen.add(key);
|
|
3309
|
-
out.push({
|
|
3310
|
-
a: first,
|
|
3311
|
-
b: second,
|
|
3312
|
-
source: "proposed",
|
|
3313
|
-
...typeof pair.reason === "string" && pair.reason.trim() !== "" ? { reason: pair.reason.trim() } : {}
|
|
3314
|
-
});
|
|
3315
|
-
}
|
|
3316
|
-
return out;
|
|
3317
|
-
}
|
|
3318
|
-
async function proposePairs(cards, port, options) {
|
|
3319
|
-
const shardSize = Math.max(2, options.shardSize ?? 60);
|
|
3320
|
-
const cap = Math.max(0, options.maxPairs ?? 200);
|
|
3321
|
-
const known = new Set(cards.map((card) => card.name));
|
|
3322
|
-
const candidates = [];
|
|
3323
|
-
const degraded = [];
|
|
3324
|
-
let shards = 0;
|
|
3325
|
-
for (let start = 0; start < cards.length && candidates.length < cap; start += shardSize) {
|
|
3326
|
-
const shard = cards.slice(start, start + shardSize);
|
|
3327
|
-
if (shard.length < 2) continue;
|
|
3328
|
-
shards += 1;
|
|
3329
|
-
try {
|
|
3330
|
-
const raw = await port.callJson({
|
|
3331
|
-
system: SYSTEM,
|
|
3332
|
-
user: JSON.stringify(shard.map((card) => ({
|
|
3333
|
-
name: card.name,
|
|
3334
|
-
description: card.description.slice(0, 400),
|
|
3335
|
-
body: card.body.slice(0, 600)
|
|
3336
|
-
}))),
|
|
3337
|
-
maxTokens: 1200,
|
|
3338
|
-
signal: options.signal
|
|
3339
|
-
});
|
|
3340
|
-
candidates.push(...parsePairs(raw, known, cap - candidates.length));
|
|
3341
|
-
} catch (error) {
|
|
3342
|
-
degraded.push("proposal shard failed: " + (error instanceof Error ? error.message : String(error)));
|
|
3343
|
-
}
|
|
3344
|
-
}
|
|
3345
|
-
return {
|
|
3346
|
-
candidates,
|
|
3347
|
-
shards,
|
|
3348
|
-
degraded
|
|
3349
|
-
};
|
|
3350
|
-
}
|
|
3351
|
-
//#endregion
|
|
3352
|
-
//#region lib/types/gate-filter.js
|
|
3353
|
-
function withGate(candidate, verdicts, auditOverride) {
|
|
3354
|
-
const last = verdicts[verdicts.length - 1];
|
|
3355
|
-
if (last === void 0) return candidate;
|
|
3356
|
-
return {
|
|
3357
|
-
...candidate,
|
|
3358
|
-
gate: {
|
|
3359
|
-
level: last.level,
|
|
3360
|
-
verdict: last.verdict,
|
|
3361
|
-
reason: last.reason,
|
|
3362
|
-
...last.cached ? { cached: true } : {},
|
|
3363
|
-
...auditOverride ? { auditOverride: true } : {}
|
|
3364
|
-
}
|
|
3365
|
-
};
|
|
3366
|
-
}
|
|
3367
|
-
async function filterCandidates(candidates, options) {
|
|
3368
|
-
const kept = [];
|
|
3369
|
-
const dropped = [];
|
|
3370
|
-
const errors = [];
|
|
3371
|
-
let calls = 0;
|
|
3372
|
-
for (const candidate of candidates) {
|
|
3373
|
-
const first = await runLadder({
|
|
3374
|
-
level: "L0",
|
|
3375
|
-
candidate,
|
|
3376
|
-
facts: options.facts,
|
|
3377
|
-
model: options.model,
|
|
3378
|
-
modelName: options.modelName,
|
|
3379
|
-
signal: options.signal,
|
|
3380
|
-
...options.cache === void 0 ? {} : { cache: options.cache }
|
|
3381
|
-
});
|
|
3382
|
-
calls += first.calls;
|
|
3383
|
-
if (first.route === "error") {
|
|
3384
|
-
errors.push({
|
|
3385
|
-
candidate,
|
|
3386
|
-
message: first.error ?? "gate error"
|
|
3387
|
-
});
|
|
3388
|
-
continue;
|
|
3389
|
-
}
|
|
3390
|
-
if (first.route !== "keep" || first.droppedAt !== "L0") {
|
|
3391
|
-
if (first.route === "merge" || first.route === "undecided") kept.push({
|
|
3392
|
-
candidate: withGate(candidate, first.verdicts, false),
|
|
3393
|
-
verdicts: first.verdicts,
|
|
3394
|
-
auditOverride: false
|
|
3395
|
-
});
|
|
3396
|
-
else dropped.push({
|
|
3397
|
-
candidate,
|
|
3398
|
-
verdicts: first.verdicts,
|
|
3399
|
-
auditOverride: false
|
|
3400
|
-
});
|
|
3401
|
-
continue;
|
|
3402
|
-
}
|
|
3403
|
-
if (!selectedForAudit(pairKey(candidate.a, candidate.b), options.runSeed, options.auditRate ?? .1)) {
|
|
3404
|
-
dropped.push({
|
|
3405
|
-
candidate,
|
|
3406
|
-
verdicts: first.verdicts,
|
|
3407
|
-
auditOverride: false
|
|
3408
|
-
});
|
|
3409
|
-
continue;
|
|
3410
|
-
}
|
|
3411
|
-
const audit = await runLadder({
|
|
3412
|
-
level: "L0",
|
|
3413
|
-
startLevel: "L2",
|
|
3414
|
-
candidate,
|
|
3415
|
-
facts: options.facts,
|
|
3416
|
-
model: options.model,
|
|
3417
|
-
modelName: options.modelName,
|
|
3418
|
-
signal: options.signal,
|
|
3419
|
-
...options.cache === void 0 ? {} : { cache: options.cache }
|
|
3420
|
-
});
|
|
3421
|
-
calls += audit.calls;
|
|
3422
|
-
if (audit.route === "merge" || audit.route === "undecided") {
|
|
3423
|
-
const all = [...first.verdicts, ...audit.verdicts];
|
|
3424
|
-
kept.push({
|
|
3425
|
-
candidate: withGate(candidate, all, true),
|
|
3426
|
-
verdicts: all,
|
|
3427
|
-
auditOverride: true
|
|
3428
|
-
});
|
|
3429
|
-
} else if (audit.route === "error") errors.push({
|
|
3430
|
-
candidate,
|
|
3431
|
-
message: audit.error ?? "audit gate error"
|
|
3432
|
-
});
|
|
3433
|
-
else dropped.push({
|
|
3434
|
-
candidate,
|
|
3435
|
-
verdicts: [...first.verdicts, ...audit.verdicts],
|
|
3436
|
-
auditOverride: true
|
|
3437
|
-
});
|
|
3438
|
-
}
|
|
3439
|
-
return {
|
|
3440
|
-
kept,
|
|
3441
|
-
dropped,
|
|
3442
|
-
errors,
|
|
3443
|
-
calls
|
|
3444
|
-
};
|
|
3445
|
-
}
|
|
3446
|
-
//#endregion
|
|
3447
|
-
//#region lib/types/screening-port.js
|
|
3448
|
-
/**
|
|
3449
|
-
* The real screening port: lexical floor + optional C3 proposal + optional L0–L2 gate.
|
|
3450
|
-
* Each stage is independently testable; this file only composes their frozen contracts.
|
|
3451
|
-
*/
|
|
3452
|
-
async function buildScreeningCandidates(options) {
|
|
3453
|
-
const lexical = lexicalCandidates(options.cards, options.lexical);
|
|
3454
|
-
const merged = /* @__PURE__ */ new Map();
|
|
3455
|
-
for (const candidate of lexical.candidates) merged.set(pairKey(candidate.a, candidate.b), candidate);
|
|
3456
|
-
const degraded = [];
|
|
3457
|
-
let proposed = 0;
|
|
3458
|
-
let shards = 0;
|
|
3459
|
-
if (options.propose !== void 0) {
|
|
3460
|
-
const proposal = await proposePairs(options.cards, options.propose.model, {
|
|
3461
|
-
signal: options.signal,
|
|
3462
|
-
...options.propose.shardSize === void 0 ? {} : { shardSize: options.propose.shardSize },
|
|
3463
|
-
...options.propose.maxPairs === void 0 ? {} : { maxPairs: options.propose.maxPairs }
|
|
3464
|
-
});
|
|
3465
|
-
proposed = proposal.candidates.length;
|
|
3466
|
-
shards = proposal.shards;
|
|
3467
|
-
degraded.push(...proposal.degraded);
|
|
3468
|
-
for (const candidate of proposal.candidates) {
|
|
3469
|
-
const key = pairKey(candidate.a, candidate.b);
|
|
3470
|
-
const existing = merged.get(key);
|
|
3471
|
-
if (existing === void 0) {
|
|
3472
|
-
merged.set(key, candidate);
|
|
3473
|
-
continue;
|
|
3474
|
-
}
|
|
3475
|
-
const arms = [...new Set([...existing.arms ?? [], ...candidate.arms ?? []])];
|
|
3476
|
-
merged.set(key, {
|
|
3477
|
-
...existing,
|
|
3478
|
-
...candidate.reason === void 0 || existing.reason !== void 0 ? {} : { reason: candidate.reason },
|
|
3479
|
-
...arms.length === 0 ? {} : { arms }
|
|
3480
|
-
});
|
|
3481
|
-
}
|
|
3482
|
-
}
|
|
3483
|
-
let candidates = [...merged.values()].sort((left, right) => (right.score ?? 0) - (left.score ?? 0) || left.a.localeCompare(right.a) || left.b.localeCompare(right.b));
|
|
3484
|
-
let kept = candidates.length;
|
|
3485
|
-
let dropped = 0;
|
|
3486
|
-
let gateCalls = 0;
|
|
3487
|
-
let gateErrors = 0;
|
|
3488
|
-
if (options.gate !== void 0) {
|
|
3489
|
-
const facts = options.cards.map((card) => ({
|
|
3490
|
-
name: card.name,
|
|
3491
|
-
description: card.description,
|
|
3492
|
-
body: card.body
|
|
3493
|
-
}));
|
|
3494
|
-
const filtered = await filterCandidates(candidates, {
|
|
3495
|
-
facts,
|
|
3496
|
-
model: options.gate.model,
|
|
3497
|
-
modelName: options.gate.modelName,
|
|
3498
|
-
signal: options.signal,
|
|
3499
|
-
runSeed: options.gate.runSeed,
|
|
3500
|
-
...options.gate.cache === void 0 ? {} : { cache: options.gate.cache },
|
|
3501
|
-
...options.gate.auditRate === void 0 ? {} : { auditRate: options.gate.auditRate }
|
|
3502
|
-
});
|
|
3503
|
-
candidates = filtered.kept.map((decision) => decision.candidate);
|
|
3504
|
-
kept = filtered.kept.length;
|
|
3505
|
-
dropped = filtered.dropped.length;
|
|
3506
|
-
gateCalls = filtered.calls;
|
|
3507
|
-
gateErrors = filtered.errors.length;
|
|
3508
|
-
for (const error of filtered.errors) degraded.push("gate error: " + error.candidate.a + " + " + error.candidate.b + ": " + error.message);
|
|
3509
|
-
}
|
|
3510
|
-
return {
|
|
3511
|
-
candidates,
|
|
3512
|
-
stats: {
|
|
3513
|
-
lexical: lexical.candidates.length,
|
|
3514
|
-
proposed,
|
|
3515
|
-
candidates: merged.size,
|
|
3516
|
-
kept,
|
|
3517
|
-
dropped,
|
|
3518
|
-
gateCalls,
|
|
3519
|
-
gateErrors,
|
|
3520
|
-
shards
|
|
3521
|
-
},
|
|
3522
|
-
degraded
|
|
3523
|
-
};
|
|
3524
|
-
}
|
|
3525
|
-
/** Build the run-scoped screening function; the run id seeds the 10% audit. */
|
|
3526
|
-
function createScreeningForRun(options) {
|
|
3527
|
-
const gate = options.gate === void 0 ? void 0 : {
|
|
3528
|
-
...options.gate,
|
|
3529
|
-
runSeed: options.runSeed
|
|
3530
|
-
};
|
|
3531
|
-
return async (signal) => (await buildScreeningCandidates({
|
|
3532
|
-
cards: options.cards,
|
|
3533
|
-
signal,
|
|
3534
|
-
...options.lexical === void 0 ? {} : { lexical: options.lexical },
|
|
3535
|
-
...options.propose === void 0 ? {} : { propose: options.propose },
|
|
3536
|
-
...gate === void 0 ? {} : { gate }
|
|
3537
|
-
})).candidates;
|
|
3538
|
-
}
|
|
3539
|
-
//#endregion
|
|
3540
3313
|
//#region lib/types/stage-tool-registry.js
|
|
3541
3314
|
var StageToolRegistry = class {
|
|
3542
3315
|
bindings = /* @__PURE__ */ new Map();
|
|
@@ -3769,7 +3542,7 @@ function operation(args) {
|
|
|
3769
3542
|
function createMergeStageTool(io, home, context) {
|
|
3770
3543
|
return {
|
|
3771
3544
|
name: MERGE_STAGE_TOOL_NAME,
|
|
3772
|
-
description: "Read source copies and write the merged skill into the staging tree. Write/copy are only allowed in the author phase.",
|
|
3545
|
+
description: "Read source copies and write the merged skill into the staging tree. list/read use area=sources|merged plus source (for sources); write/copy paths are relative to merged/<target> (for example path=SKILL.md, from=SKILL.md, to=references/x.md). Write/copy are only allowed in the author phase.",
|
|
3773
3546
|
parameters: {
|
|
3774
3547
|
type: "object",
|
|
3775
3548
|
properties: {
|
|
@@ -3855,8 +3628,21 @@ const Config = z.object({
|
|
|
3855
3628
|
mergeApproval: z.boolean().default(false).volatile(),
|
|
3856
3629
|
mergeSkippedTtlDays: z.number().min(0).default(0).volatile(),
|
|
3857
3630
|
mergeRunTimeoutMinutes: z.number().min(0).default(0).volatile(),
|
|
3858
|
-
mergePairTimeoutMinutes: z.number().min(1).default(15)
|
|
3631
|
+
mergePairTimeoutMinutes: z.number().min(1).default(15),
|
|
3632
|
+
mergeModelsRoot: z.string().default("")
|
|
3859
3633
|
});
|
|
3634
|
+
function engineConfigFromEnv(configuredRoot) {
|
|
3635
|
+
const root = configuredRoot !== void 0 && configuredRoot !== "" ? configuredRoot : process.env.SKILL_MERGE_MODELS_ROOT;
|
|
3636
|
+
if (root === void 0 || root === "") return void 0;
|
|
3637
|
+
return {
|
|
3638
|
+
models_dir: "",
|
|
3639
|
+
offline: true,
|
|
3640
|
+
auto_provision: false,
|
|
3641
|
+
embed_backend: "local",
|
|
3642
|
+
embedder_model: join(root, "potion-multilingual-128M-int8"),
|
|
3643
|
+
embedder_model_last: join(root, "potion-multilingual-128M-pca64-int8")
|
|
3644
|
+
};
|
|
3645
|
+
}
|
|
3860
3646
|
/** Suppress the platform auto form; the registry renders the merge card instead. */
|
|
3861
3647
|
function configureMergePresentation(ctx) {
|
|
3862
3648
|
ctx.inject(["settings"], (scope) => {
|
|
@@ -3870,6 +3656,15 @@ const inject = [];
|
|
|
3870
3656
|
function apply(ctx, rawConfig = {}) {
|
|
3871
3657
|
configureMergePresentation(ctx);
|
|
3872
3658
|
const params = resolveMergeParams(rawConfig);
|
|
3659
|
+
let mergeRun;
|
|
3660
|
+
const mergeService = { async run(rawInput, invocation) {
|
|
3661
|
+
return mergeRun === void 0 ? {
|
|
3662
|
+
ok: false,
|
|
3663
|
+
text: "merge 未就绪:evolutionIo 尚未挂载"
|
|
3664
|
+
} : mergeRun(rawInput, invocation);
|
|
3665
|
+
} };
|
|
3666
|
+
const mergeContext = ctx;
|
|
3667
|
+
if (typeof mergeContext.provide === "function") mergeContext.provide("evolutionMerge", mergeService);
|
|
3873
3668
|
let source;
|
|
3874
3669
|
registerHostRoutes(ctx, (audit) => makeMergeRoutes({ getSource: () => source }, audit));
|
|
3875
3670
|
ctx.inject(["commands", "evolutionIo"], (scope) => {
|
|
@@ -3904,9 +3699,33 @@ function apply(ctx, rawConfig = {}) {
|
|
|
3904
3699
|
});
|
|
3905
3700
|
const channel = subagentChannelAdapter(createSubagentChannel(ctx));
|
|
3906
3701
|
const stage = new StageToolRegistry();
|
|
3702
|
+
const engineConfig = engineConfigFromEnv(params.mergeModelsRoot);
|
|
3907
3703
|
const llm = ctx.get("llm");
|
|
3908
3704
|
const defaultModel = ctx.get("agentDefaultModel");
|
|
3909
|
-
const
|
|
3705
|
+
const runner = llm === void 0 || defaultModel === void 0 ? void 0 : createEngineRunner(llm, () => defaultModel.currentSelection());
|
|
3706
|
+
const engineScreening = createEngineScreening({
|
|
3707
|
+
io,
|
|
3708
|
+
home,
|
|
3709
|
+
engine: { screen },
|
|
3710
|
+
select: selectCandidates,
|
|
3711
|
+
...engineConfig === void 0 ? {} : { cfg: engineConfig },
|
|
3712
|
+
...runner === void 0 ? {} : { runner },
|
|
3713
|
+
async onSelection(selection) {
|
|
3714
|
+
const loaded = await loadMergeState(io, home);
|
|
3715
|
+
const at = (/* @__PURE__ */ new Date()).toISOString();
|
|
3716
|
+
for (const skipped of selection.skipped) {
|
|
3717
|
+
if (loaded.state.rows.some((row) => row.pairKey === skipped.pairKey)) continue;
|
|
3718
|
+
const next = transition(void 0, {
|
|
3719
|
+
type: "gate-keep",
|
|
3720
|
+
a: skipped.a,
|
|
3721
|
+
b: skipped.b,
|
|
3722
|
+
...skipped.reason === void 0 ? {} : { reason: skipped.reason },
|
|
3723
|
+
at
|
|
3724
|
+
});
|
|
3725
|
+
if (next.ok && next.row !== null) await upsertRow(io, home, () => Date.now(), next.row);
|
|
3726
|
+
}
|
|
3727
|
+
}
|
|
3728
|
+
});
|
|
3910
3729
|
source = {
|
|
3911
3730
|
rows() {
|
|
3912
3731
|
return loadMergeState(io, home).then((loaded) => loaded.state.rows);
|
|
@@ -3917,6 +3736,9 @@ function apply(ctx, rawConfig = {}) {
|
|
|
3917
3736
|
runs() {
|
|
3918
3737
|
return Promise.resolve(runs.runs());
|
|
3919
3738
|
},
|
|
3739
|
+
approval() {
|
|
3740
|
+
return params.mergeApproval;
|
|
3741
|
+
},
|
|
3920
3742
|
commands() {
|
|
3921
3743
|
return [
|
|
3922
3744
|
"merge scan",
|
|
@@ -3932,7 +3754,7 @@ function apply(ctx, rawConfig = {}) {
|
|
|
3932
3754
|
};
|
|
3933
3755
|
const commands = scope.get("commands");
|
|
3934
3756
|
if (commands === void 0) return;
|
|
3935
|
-
const
|
|
3757
|
+
const runtime = createMergeRuntime({
|
|
3936
3758
|
io,
|
|
3937
3759
|
home,
|
|
3938
3760
|
registry: runs,
|
|
@@ -3947,31 +3769,24 @@ function apply(ctx, rawConfig = {}) {
|
|
|
3947
3769
|
async cards(signal) {
|
|
3948
3770
|
const listed = await skills.list({ withContent: true });
|
|
3949
3771
|
if (signal.aborted) return [];
|
|
3950
|
-
return listed.map((item) =>
|
|
3951
|
-
|
|
3952
|
-
|
|
3953
|
-
|
|
3772
|
+
return Promise.all(listed.map(async (item) => {
|
|
3773
|
+
const probe = await skills.listSupportFiles(item.name);
|
|
3774
|
+
const paths = probe.kind === "present" ? probe.value : [];
|
|
3775
|
+
const files = await Promise.all(paths.map(async (path) => ({
|
|
3776
|
+
path,
|
|
3777
|
+
content: await skills.readSupportFile(item.name, path) ?? ""
|
|
3778
|
+
})));
|
|
3779
|
+
return {
|
|
3780
|
+
name: item.name,
|
|
3781
|
+
description: item.description,
|
|
3782
|
+
body: item.content ?? "",
|
|
3783
|
+
files,
|
|
3784
|
+
protectedBy: item.protectedBy,
|
|
3785
|
+
protectionUnknown: item.protectionUnknown
|
|
3786
|
+
};
|
|
3954
3787
|
}));
|
|
3955
3788
|
},
|
|
3956
|
-
|
|
3957
|
-
return Promise.resolve(lexicalCandidates(cards).candidates);
|
|
3958
|
-
},
|
|
3959
|
-
...modelPort === void 0 ? {} : { async screeningForRun(cards, runId, signal) {
|
|
3960
|
-
const cache = await loadGateCache(io, home);
|
|
3961
|
-
const candidates = await createScreeningForRun({
|
|
3962
|
-
cards,
|
|
3963
|
-
runSeed: runId,
|
|
3964
|
-
propose: { model: modelPort },
|
|
3965
|
-
gate: {
|
|
3966
|
-
model: modelPort,
|
|
3967
|
-
modelName: defaultModel?.currentSelection().model ?? "",
|
|
3968
|
-
runSeed: runId,
|
|
3969
|
-
cache: cache.cache
|
|
3970
|
-
}
|
|
3971
|
-
})(signal);
|
|
3972
|
-
await saveGateCache(io, home, cache.cache).catch(() => {});
|
|
3973
|
-
return candidates;
|
|
3974
|
-
} },
|
|
3789
|
+
screeningForRun: engineScreening,
|
|
3975
3790
|
stage: {
|
|
3976
3791
|
bind: (context) => {
|
|
3977
3792
|
stage.bind(context.childId, context);
|
|
@@ -3980,7 +3795,9 @@ function apply(ctx, rawConfig = {}) {
|
|
|
3980
3795
|
stage.unbind(childId);
|
|
3981
3796
|
}
|
|
3982
3797
|
}
|
|
3983
|
-
})
|
|
3798
|
+
});
|
|
3799
|
+
mergeRun = (rawInput, invocation) => runMergeCommand(runtime, rawInput, invocation);
|
|
3800
|
+
const command = createMergeCommand(runtime);
|
|
3984
3801
|
scope.effect(() => commands.register(command), "evolution-merge: command");
|
|
3985
3802
|
ctx.inject(["tools"], (toolScope) => {
|
|
3986
3803
|
const tools = toolScope.get("tools");
|
|
@@ -3994,4 +3811,4 @@ function apply(ctx, rawConfig = {}) {
|
|
|
3994
3811
|
});
|
|
3995
3812
|
}
|
|
3996
3813
|
//#endregion
|
|
3997
|
-
export { Config,
|
|
3814
|
+
export { Config, MERGE_DOCTOR_SCHEMA_VERSION, MERGE_MAX_ATTEMPTS, MERGE_PAIR_STATES, MERGE_PARAM_DEFAULTS, MERGE_PROMPT_VERSION, MERGE_QUEUES, MERGE_ROUTES, MERGE_STAGE_TOOL_NAME, MERGE_STATE_VERSION, STAGING_SCHEMA_VERSION, STANDBY_PROMPT, StageToolRegistry, apply, applyStageTool, approvePending, assembleStagedCommit, buildCommitPlan, buildMergeView, buildRound1Prompt, buildRound2Prompt, buildStandbyPrompt, canRetry, commitReceiptPath, commitStagedRun, committedPairs, configureMergePresentation, copyStagedFile, countRows, createChannelScanPorts, createEngineRunner, createEngineScreening, createMergeCommand, createMergeRuntime, createMergeStageTool, createNewChildRound2, createReadOnlyMergeService, createStaging, dropRow, dropStaging, durableReceiptDir, durableReceiptPath, emptyMergeState, engineConfigFromEnv, ensureStaging, formatQueue, formatStatus, handleReadOnlyMergeCommand, hasBlockingFinding, inject, isSkippedPair, listStagedFiles, loadMergeState, makeMergeRoutes, mergeCommandHelp, mergeDoctor, mergeReportDigestPath, mergeReportPath, mergeRunIdsToSupersede, mergeStatePath, mergeUsage, moveRow, mutateMergeState, pageQueue, pairKey, parseDecision, parseMergeCommand, queueOf, readCommitReceipt, readMergeReport, readStagedFile, readStagedTree, readStaging, recordRound2Outcome, rejectPending, resolveMergeParams, resolveTargetName, rewriteFrontmatterName, rowIdOf, rowsOf, runApproveCommand, runAuthoring, runMergeCommand, runNewChildAuthoring, runPairCommand, runResumeCommand, runRound1OverChannel, runScan, runScanCommand, setStagingTarget, skillLibraryPort, skillLibraryUndoPort, skippedPairKeys, snapshotCards, stageKeyOf, stageOf, stagingRoot, subagentChannelAdapter, supersedeMergeRuns, toRowView, toRunFact, transition, undoMergeReceipt, upsertRow, validateStagedTree, withMergeRun, writeCommitReceipt, writeMergeReport, writeStagedFile };
|