@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/lib/index.js CHANGED
@@ -1,8 +1,95 @@
1
+ import { join } from "node:path";
1
2
  import z from "@deepseek-ai/schemastery";
2
- import { DEFAULT_SKILL_LIMITS, affinity, authorityFrom, contentHash, createSubagentChannel, evolutionHome, evolutionIoAdapter, executeUnitUndo, isPresent, newRunRegistry, newSkillLibrary, parseFrontmatter, planUnitUndoAcross, policyStageLimits, probeAbsent, probeList, probePresent, probeText, probeUnknown, scanContentThreats, supportRefs, transactIo, validateFrontmatter, validateSupportPath, vocabulary } from "@lmzhen/dsh-evolution-core";
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 tokens = invocation.rawInput.trim().split(/\s+/).filter(Boolean);
681
- if (tokens[0] === "merge" || tokens[0] === "/merge") tokens.shift();
682
- const parsed = parseMergeCommand(tokens);
683
- if (!parsed.ok) return {
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
- async function writeCommitReceipt(io, home, receipt) {
1616
- const path = commitReceiptPath(home, receipt.runId);
1617
- await io.writeText(path, JSON.stringify(receipt, null, 2) + "\n");
1618
- return path;
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
- async function readCommitReceipt(io, home, runId) {
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 cache = await loadGateCache(io, home);
1977
- rows.push({
1978
- key: "gate-cache",
1979
- label: "闸门缓存",
1980
- status: cache.note === void 0 ? "ok" : "warn",
1981
- detail: cache.note ?? String(cache.cache.size) + " 条"
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-pair/v1";
2079
- const ROUND1_SYSTEM = "You decide whether two skill documents describe the same reusable capability. Shared vocabulary or a shared preamble is not duplication. Return only strict JSON.";
2080
- const ROUND2_SYSTEM = "You write the merged skill into the staging tree through the merge_stage tool. Never modify the live skill tree. Follow the supplied decision exactly.";
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
- ROUND1_SYSTEM,
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
- "Return JSON: {\"should_merge\":true|false,\"mode\":\"merge-into-existing\"|\"create-umbrella\"|\"keep\",\"target\":\"<name>\",\"sources\":[\"<name>\",\"<name>\"],\"rationale\":\"<=120 chars\",\"confidence\":0..1}",
2089
- "If not enough evidence, set should_merge=false and mode=\"keep\".",
2047
+ "判断规则:",
2048
+ "1) 两者是否语义等价/互补到值得合并,而不是词面相似;",
2049
+ "2) 合并后是否更容易发现和使用,而不是变成更大的巨技能;",
2050
+ "3) 若合并会破坏触发面/生命周期/可发现性,应输出 keep。",
2090
2051
  "",
2091
- "skill_a: " + JSON.stringify({
2092
- name: candidate.a,
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
- return [
2105
- ROUND2_SYSTEM,
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
- "decision: " + JSON.stringify(decision),
2108
- "pair: " + JSON.stringify({
2109
- a: row.a,
2110
- b: row.b
2111
- }),
2112
- "target: " + JSON.stringify(decision.target),
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
- "Use merge_stage: list/read the source copies, write merged/" + decision.target + "/SKILL.md and any support files, then copy required support files. Do not write outside merged/" + decision.target + "/.",
2115
- "When the staged target is complete, reply with one short sentence. Do not return a JSON plan."
2116
- ].join("\n");
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: buildRound1Prompt(candidate, options.cards),
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(opened.turn.text, candidate);
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
- if (input.sourceBodies.length === 0) return {
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: input.sourceBodies.map((source) => source.name),
2431
+ sources: sources.map((source) => source.name),
2409
2432
  body,
2410
2433
  supportFiles: input.mergedFiles,
2411
2434
  expectedTargetBody: input.targetCurrent,
2412
- expectedSourceBodies: input.sourceBodies,
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 targetCurrent = await port.readSkill(staging.value.manifest.target);
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: staging.value.manifest.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 created = await createStaging(options.io, options.home, {
2508
- runId: options.runId,
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: created.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: options.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, options.runId);
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, options.runId, options.commitPort, {
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
- ...options.cwd === void 0 ? {} : { cwd: options.cwd },
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 === "merge") if (options.approval) await result.dispose();
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), result);
2663
+ sessions.set(pairKey(candidate.a, candidate.b), {
2664
+ result,
2665
+ stageKey
2666
+ });
2578
2667
  if (result.childId !== void 0) options.onChild?.({
2579
- runId: options.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 session = sessions.get(key);
2590
- if (session === void 0) return {
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 session.author?.(row, decision, signal);
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.ok ? authored.commit.receipt : void 0;
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: decision.target,
2729
+ actual: authored.commit.plan.target,
2634
2730
  created: receipt?.create ?? false
2635
2731
  }
2636
2732
  };
2637
2733
  } finally {
2638
- await session.dispose();
2639
- if (session.childId !== void 0) options.onChildClosed?.(session.childId);
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 staging = await createStaging(options.io, options.home, {
2648
- runId: options.runId,
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: "staging-create",
2664
- message: staging.message,
2665
- retryable: true,
2666
- retryFrom: "agent2"
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: buildRound2Prompt(row, decision),
2784
+ promptText: STANDBY_PROMPT,
2673
2785
  kind: "maintainer",
2674
2786
  allow: options.allow,
2675
- ...options.cwd === void 0 ? {} : { cwd: options.cwd },
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: options.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, options.runId);
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, options.runId, options.commitPort, {
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: decision.target,
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 skipped = skippedPairKeys((await loadMergeState(deps.io, deps.home)).state.rows, deps.params.mergeSkippedTtlDays, deps.now());
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
- return (deps.screeningForRun === void 0 ? await deps.screening(cards, turnSignal) : await deps.screeningForRun(cards, runId, turnSignal)).filter((candidate) => !skipped.has(pairKey(candidate.a, candidate.b)));
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 modelPort = llm === void 0 || defaultModel === void 0 ? void 0 : createModelPort(llm, () => defaultModel.currentSelection());
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 command = createMergeCommand(createMergeRuntime({
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
- name: item.name,
3952
- description: item.description,
3953
- body: item.content ?? ""
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
- screening(cards) {
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, GATE_CACHE_VERSION, GateCache, MERGE_DOCTOR_SCHEMA_VERSION, MERGE_PAIR_STATES, MERGE_PARAM_DEFAULTS, MERGE_PROMPT_VERSION, MERGE_QUEUES, MERGE_ROUTES, MERGE_STAGE_TOOL_NAME, MERGE_STATE_VERSION, STAGING_SCHEMA_VERSION, StageToolRegistry, apply, applyStageTool, approvePending, assembleStagedCommit, buildCommitPlan, buildMergeView, buildRound1Prompt, buildRound2Prompt, buildScreeningCandidates, canRetry, commitReceiptPath, commitStagedRun, committedPairs, configureMergePresentation, copyStagedFile, countRows, createChannelScanPorts, createMergeCommand, createMergeRuntime, createMergeStageTool, createModelPort, createNewChildRound2, createReadOnlyMergeService, createScreeningForRun, createStaging, dropRow, emptyMergeState, filterCandidates, formatQueue, formatStatus, gateCacheKey, gateCachePath, handleReadOnlyMergeCommand, hasBlockingFinding, hasGateCache, inject, isSkippedPair, lexicalCandidates, listStagedFiles, loadGateCache, loadMergeState, makeMergeRoutes, mergeCommandHelp, mergeDoctor, mergeReportDigestPath, mergeReportPath, mergeRunIdsToSupersede, mergeStatePath, mergeUsage, moveRow, mutateMergeState, nameEntropy, pageQueue, pairKey, parseDecision, parseMergeCommand, parseVerdict, prefilter, proposePairs, queueOf, readCommitReceipt, readMergeReport, readStagedFile, readStagedTree, readStaging, recordRound2Outcome, rejectPending, resolveMergeParams, resolveTargetName, rewriteFrontmatterName, rowIdOf, rowsOf, runApproveCommand, runAuthoring, runLadder, runNewChildAuthoring, runPairCommand, runResumeCommand, runRound1OverChannel, runScan, runScanCommand, saveGateCache, selectedForAudit, skillLibraryPort, skillLibraryUndoPort, skippedPairKeys, stageOf, stagingRoot, subagentChannelAdapter, supersedeMergeRuns, toRowView, toRunFact, transition, undoMergeReceipt, upsertRow, validateStagedTree, withMergeRun, writeCommitReceipt, writeMergeReport, writeStagedFile };
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 };