@lmzhen/dsh-evolution-merge 0.28.0 → 0.30.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (115) hide show
  1. package/README.md +21 -18
  2. package/lib/index.js +1583 -1209
  3. package/lib/types/adapters/channel.js +29 -0
  4. package/lib/types/adapters/engine-screen.d.ts +55 -0
  5. package/lib/types/adapters/engine-screen.js +45 -0
  6. package/lib/types/adapters/library.js +14 -0
  7. package/lib/types/adapters/model-runner.d.ts +28 -0
  8. package/lib/types/adapters/model-runner.js +34 -0
  9. package/lib/types/adapters/select.d.ts +3 -0
  10. package/lib/types/adapters/select.js +35 -0
  11. package/lib/types/adapters/undo.js +35 -0
  12. package/lib/types/approve-command.js +42 -0
  13. package/lib/types/authoring-flow.d.ts +7 -2
  14. package/lib/types/authoring-flow.js +48 -0
  15. package/lib/types/channel-port.d.ts +2 -1
  16. package/lib/types/channel-port.js +74 -0
  17. package/lib/types/commands.d.ts +9 -0
  18. package/lib/types/commands.js +79 -0
  19. package/lib/types/commit-assembly.d.ts +2 -0
  20. package/lib/types/commit-assembly.js +48 -0
  21. package/lib/types/commit-execute.js +52 -0
  22. package/lib/types/commit-plan.js +56 -0
  23. package/lib/types/commit-receipt.d.ts +6 -1
  24. package/lib/types/commit-receipt.js +85 -0
  25. package/lib/types/contracts.d.ts +21 -7
  26. package/lib/types/contracts.js +57 -0
  27. package/lib/types/coordinator.d.ts +11 -0
  28. package/lib/types/coordinator.js +131 -0
  29. package/lib/types/decision.js +61 -0
  30. package/lib/types/doctor.d.ts +1 -1
  31. package/lib/types/doctor.js +50 -0
  32. package/lib/types/domain/claim.d.ts +10 -0
  33. package/lib/types/domain/claim.js +12 -0
  34. package/lib/types/domain/commit-journal-store.d.ts +6 -0
  35. package/lib/types/domain/commit-journal-store.js +22 -0
  36. package/lib/types/domain/commit-journal.d.ts +16 -0
  37. package/lib/types/domain/commit-journal.js +25 -0
  38. package/lib/types/domain/errors.d.ts +10 -0
  39. package/lib/types/domain/errors.js +7 -0
  40. package/lib/types/domain/failure-route.d.ts +25 -0
  41. package/lib/types/domain/failure-route.js +22 -0
  42. package/lib/types/domain/indexes.d.ts +21 -0
  43. package/lib/types/domain/indexes.js +15 -0
  44. package/lib/types/domain/ledger.d.ts +17 -0
  45. package/lib/types/domain/ledger.js +65 -0
  46. package/lib/types/domain/markdown.d.ts +2 -0
  47. package/lib/types/domain/markdown.js +57 -0
  48. package/lib/types/domain/migrate-v1.d.ts +41 -0
  49. package/lib/types/domain/migrate-v1.js +23 -0
  50. package/lib/types/domain/pair-index-store.d.ts +6 -0
  51. package/lib/types/domain/pair-index-store.js +25 -0
  52. package/lib/types/domain/pair-index.d.ts +9 -0
  53. package/lib/types/domain/pair-index.js +28 -0
  54. package/lib/types/domain/resolve-pair.d.ts +11 -0
  55. package/lib/types/domain/resolve-pair.js +17 -0
  56. package/lib/types/domain/row.d.ts +113 -0
  57. package/lib/types/domain/row.js +111 -0
  58. package/lib/types/domain/run-record.d.ts +45 -0
  59. package/lib/types/domain/run-record.js +24 -0
  60. package/lib/types/domain/state.d.ts +57 -0
  61. package/lib/types/domain/state.js +65 -0
  62. package/lib/types/gate-cache.js +36 -0
  63. package/lib/types/index.d.ts +6 -7
  64. package/lib/types/index.js +241 -0
  65. package/lib/types/ladder.js +162 -0
  66. package/lib/types/merge-command.d.ts +3 -0
  67. package/lib/types/merge-command.js +43 -0
  68. package/lib/types/merge-routes.d.ts +15 -4
  69. package/lib/types/merge-routes.js +99 -0
  70. package/lib/types/merge-tool.js +69 -0
  71. package/lib/types/new-child-authoring.d.ts +1 -1
  72. package/lib/types/new-child-authoring.js +83 -0
  73. package/lib/types/pair-command.js +18 -0
  74. package/lib/types/params.d.ts +1 -0
  75. package/lib/types/params.js +49 -0
  76. package/lib/types/pending-actions.js +28 -0
  77. package/lib/types/prompts.d.ts +7 -5
  78. package/lib/types/prompts.js +60 -0
  79. package/lib/types/read-commands.js +32 -0
  80. package/lib/types/report.js +70 -0
  81. package/lib/types/resume-command.js +57 -0
  82. package/lib/types/round2-outcome.js +50 -0
  83. package/lib/types/routes.d.ts +20 -2
  84. package/lib/types/routes.js +75 -0
  85. package/lib/types/rules.js +55 -0
  86. package/lib/types/run-shell.d.ts +7 -0
  87. package/lib/types/run-shell.js +69 -0
  88. package/lib/types/runtime.d.ts +5 -4
  89. package/lib/types/runtime.js +269 -0
  90. package/lib/types/scan-command.d.ts +1 -0
  91. package/lib/types/scan-command.js +30 -0
  92. package/lib/types/scan-ports.d.ts +1 -1
  93. package/lib/types/scan-ports.js +93 -0
  94. package/lib/types/scan-run.d.ts +4 -0
  95. package/lib/types/scan-run.js +186 -0
  96. package/lib/types/skill-card.d.ts +12 -0
  97. package/lib/types/skill-card.js +2 -0
  98. package/lib/types/skipped.js +24 -0
  99. package/lib/types/stage-tool-registry.js +32 -0
  100. package/lib/types/stage-tool.js +31 -0
  101. package/lib/types/staged-tree.js +75 -0
  102. package/lib/types/staging-files.js +86 -0
  103. package/lib/types/staging.d.ts +13 -0
  104. package/lib/types/staging.js +149 -0
  105. package/lib/types/state.js +103 -0
  106. package/lib/types/status.d.ts +2 -1
  107. package/lib/types/status.js +28 -0
  108. package/lib/types/supersede.js +14 -0
  109. package/lib/types/undo-merge.js +23 -0
  110. package/package.json +4 -7
  111. package/lib/types/adapters/model.d.ts +0 -31
  112. package/lib/types/gate-filter.d.ts +0 -31
  113. package/lib/types/propose.d.ts +0 -26
  114. package/lib/types/screening-port.d.ts +0 -47
  115. package/lib/types/screening.d.ts +0 -30
package/lib/index.js CHANGED
@@ -1,8 +1,130 @@
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 { pairKeyOf, screen, selectCandidates, toEngineCandidate } 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/select.js
8
+ /** Host selector correction: engine selector does not route judge errors to candidates. */
9
+ function selectWithErrors(report) {
10
+ const base = selectCandidates(report);
11
+ const doc = report.reportDoc ?? report;
12
+ const rows = Array.isArray(doc.pairs_final) ? doc.pairs_final : [];
13
+ const errors = /* @__PURE__ */ new Set();
14
+ for (const row of rows) {
15
+ if (row.error !== true) continue;
16
+ const a = typeof row.a === "string" ? row.a : "";
17
+ const b = typeof row.b === "string" ? row.b : "";
18
+ if (a !== "" && b !== "") errors.add(pairKeyOf(a, b));
19
+ }
20
+ const candidates = base.candidates.filter((candidate) => !errors.has(pairKeyOf(candidate.a, candidate.b)));
21
+ const skipped = base.skipped.filter((item) => !errors.has(item.pairKey));
22
+ const reportOnly = base.reportOnly.filter((row) => {
23
+ const value = row;
24
+ if (typeof value.a !== "string" || typeof value.b !== "string") return true;
25
+ return !errors.has(pairKeyOf(value.a, value.b));
26
+ });
27
+ for (const row of rows) {
28
+ if (row.error !== true) continue;
29
+ const a = typeof row.a === "string" ? row.a : "";
30
+ const b = typeof row.b === "string" ? row.b : "";
31
+ if (a === "" || b === "") continue;
32
+ candidates.push(toEngineCandidate(row, typeof row.reason === "string" ? row.reason : "judge-error"));
33
+ }
34
+ return {
35
+ ...base,
36
+ candidates,
37
+ skipped,
38
+ reportOnly
39
+ };
40
+ }
41
+ //#endregion
42
+ //#region lib/types/adapters/engine-screen.js
43
+ /**
44
+ * G3 screen adapter: cards -> corpus snapshot -> EnginePort.screen.
45
+ *
46
+ * The vendor engine reads a corpus from disk; host cards are content. This module
47
+ * writes only SKILL.md support-free snapshots under the run directory. It does not
48
+ * decide candidates: the Q02 selector is injected so engine/report-map stays the
49
+ * single source of that policy.
50
+ */
51
+ function safeName(name, index) {
52
+ const cleaned = name.replace(/[^A-Za-z0-9._-]+/g, "-").replace(/^-+|-+$/g, "");
53
+ return (cleaned === "" ? "skill-" + String(index) : cleaned) + "__" + String(index);
54
+ }
55
+ /** Write the exact card bodies as a support-free corpus snapshot and return its root. */
56
+ async function snapshotCards(io, home, runId, cards) {
57
+ const root = join(home, "merge", "runs", runId, "corpus");
58
+ for (let index = 0; index < cards.length; index += 1) {
59
+ const card = cards[index];
60
+ if (card === void 0) continue;
61
+ await io.writeText(join(root, safeName(card.name, index), "SKILL.md"), card.body);
62
+ }
63
+ return root;
64
+ }
65
+ function createEngineScreening(deps) {
66
+ return async (cards, runId, signal) => {
67
+ const corpusDir = await snapshotCards(deps.io, deps.home, runId, cards);
68
+ const outDir = join(deps.home, "merge", "runs", runId, "engine");
69
+ const result = await deps.engine.screen({
70
+ corpusDir,
71
+ outDir,
72
+ ...deps.cfg === void 0 ? {} : { cfg: deps.cfg },
73
+ ...deps.runner === void 0 ? {} : { runner: deps.runner },
74
+ signal
75
+ });
76
+ 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));
77
+ const selection = deps.select(result.report);
78
+ if (deps.onSelection !== void 0) await deps.onSelection(selection, runId);
79
+ return Object.assign(selection.candidates.slice(), selection.candidatePairs === void 0 ? {} : { candidatePairs: selection.candidatePairs });
80
+ };
81
+ }
82
+ //#endregion
83
+ //#region lib/types/adapters/model-runner.js
84
+ function append(chunk, text) {
85
+ if (chunk.type === "text-delta" || chunk.type === "text") return text + (chunk.text ?? "");
86
+ if (chunk.type === "finish" && chunk.reason?.kind === "error") throw new Error("model call failed: " + (chunk.reason.failure?.message ?? "unknown"));
87
+ return text;
88
+ }
89
+ function createEngineRunner(llm, route) {
90
+ return async (prompt) => {
91
+ const selection = route();
92
+ let text = "";
93
+ if (llm.stream !== void 0) {
94
+ for await (const chunk of llm.stream({
95
+ provider: selection.provider,
96
+ model: selection.model,
97
+ messages: [{
98
+ role: "user",
99
+ content: [{
100
+ type: "text",
101
+ text: prompt
102
+ }]
103
+ }],
104
+ temperature: 0
105
+ })) text = append(chunk, text);
106
+ return text;
107
+ }
108
+ if (llm.prepareCall === void 0) throw new Error("llm service has neither stream nor prepareCall");
109
+ const call = await llm.prepareCall({
110
+ provider: selection.provider,
111
+ model: selection.model,
112
+ temperature: 0
113
+ });
114
+ for await (const chunk of call.stream({
115
+ ...call.config,
116
+ messages: [{
117
+ role: "user",
118
+ content: [{
119
+ type: "text",
120
+ text: prompt
121
+ }]
122
+ }]
123
+ })) text = append(chunk, text);
124
+ return text;
125
+ };
126
+ }
127
+ //#endregion
6
128
  //#region lib/types/contracts.js
7
129
  /**
8
130
  * Frozen merge-domain vocabulary. No io, no host, no imports from the plugin shell.
@@ -10,24 +132,27 @@ import { createHash } from "node:crypto";
10
132
  * The data layer keeps four states; the user face collapses verify/fuse into one retry
11
133
  * queue and keeps the stage as a chip. The API queue must never expose the raw state.
12
134
  */
13
- const MERGE_STATE_VERSION = 1;
135
+ const MERGE_STATE_VERSION = 2;
14
136
  const MERGE_PAIR_STATES = [
15
137
  "verify",
16
138
  "pending",
139
+ "approved",
17
140
  "fuse",
18
141
  "skipped"
19
142
  ];
20
143
  const MERGE_QUEUES = [
144
+ "verify",
21
145
  "pending",
22
- "retry",
146
+ "fuse",
23
147
  "skipped"
24
148
  ];
25
149
  /** User-facing queue of one data state. verify/fuse share retry, not two tabs. */
26
150
  function queueOf(state) {
27
151
  switch (state) {
152
+ case "verify": return "verify";
28
153
  case "pending": return "pending";
29
- case "verify":
30
- case "fuse": return "retry";
154
+ case "approved":
155
+ case "fuse": return "fuse";
31
156
  case "skipped": return "skipped";
32
157
  }
33
158
  }
@@ -36,6 +161,7 @@ function stageOf(state) {
36
161
  switch (state) {
37
162
  case "verify": return "verify";
38
163
  case "pending": return "decide";
164
+ case "approved":
39
165
  case "fuse": return "author";
40
166
  case "skipped": return null;
41
167
  }
@@ -55,36 +181,56 @@ function rowIdOf(key) {
55
181
  }
56
182
  function emptyMergeState() {
57
183
  return {
58
- schemaVersion: 1,
184
+ schemaVersion: 2,
59
185
  rows: []
60
186
  };
61
187
  }
62
188
  /** One record can be retried only when the failure says so and names a stage. */
189
+ const MERGE_MAX_ATTEMPTS = 3;
63
190
  function canRetry(record) {
64
191
  if (record.state === "skipped") return false;
192
+ if ((record.attempts ?? 0) >= 3) return false;
65
193
  if (record.failure === void 0) return record.state !== "pending";
66
194
  return record.failure.retryable && record.failure.retryFrom !== "none";
67
195
  }
68
196
  //#endregion
69
- //#region lib/types/state.js
70
- /**
71
- * The one merge-state ledger: <home>/merge/merge-state.json.
72
- *
73
- * Reads are pure projections (no write, no reconciliation side effect). Every write goes
74
- * through mutateMergeState and a single transactIo, so there is no second file and no
75
- * incremental writer. A row's queue is always derived from its state on read; a hand-edited
76
- * mismatch is repaired by the next write, never by a GET.
77
- */
78
- function mergeStatePath(home) {
79
- return join(home, "merge", "merge-state.json");
197
+ //#region lib/types/domain/migrate-v1.js
198
+ function migrateV1Row(row) {
199
+ const queue = row.state === "pending" ? "pending" : row.state === "skipped" ? "skipped" : row.state === "fuse" ? "fuse" : "verify";
200
+ return {
201
+ schemaVersion: 2,
202
+ id: row.id,
203
+ pairKey: row.pairKey,
204
+ a: row.a,
205
+ b: row.b,
206
+ state: row.state,
207
+ queue,
208
+ ...row.failure === void 0 ? {} : { failure: row.failure },
209
+ ...row.decision === void 0 ? {} : { decision: row.decision },
210
+ ...row.runId === void 0 ? {} : { runId: row.runId },
211
+ ...row.unitId === void 0 ? {} : { unitId: row.unitId },
212
+ ...row.origin === void 0 ? {} : { origin: row.origin },
213
+ ...row.attempts === void 0 ? {} : { attempts: row.attempts },
214
+ ...row.stagingRef === void 0 ? {} : { stagingRef: row.stagingRef },
215
+ ...row.stagingExpiresAt === void 0 ? {} : { stagingExpiresAt: row.stagingExpiresAt },
216
+ createdAt: row.createdAt,
217
+ updatedAt: row.updatedAt
218
+ };
80
219
  }
81
- function isState(value) {
82
- return typeof value === "string" && MERGE_PAIR_STATES.includes(value);
220
+ //#endregion
221
+ //#region lib/types/domain/ledger.js
222
+ /** Pure v2 ledger normalization/projection. No IO. */
223
+ function isRow(value) {
224
+ const row = value;
225
+ return typeof row.id === "string" && typeof row.pairKey === "string" && typeof row.a === "string" && typeof row.b === "string" && typeof row.state === "string" && typeof row.createdAt === "string" && typeof row.updatedAt === "string";
83
226
  }
84
- function parseState(raw) {
227
+ function parseLedger(raw) {
85
228
  if (raw === null) return {
86
229
  ok: true,
87
- rows: []
230
+ ledger: {
231
+ schemaVersion: 2,
232
+ rows: []
233
+ }
88
234
  };
89
235
  let parsed;
90
236
  try {
@@ -92,52 +238,76 @@ function parseState(raw) {
92
238
  } catch {
93
239
  return {
94
240
  ok: false,
95
- rows: []
241
+ ledger: {
242
+ schemaVersion: 2,
243
+ rows: []
244
+ },
245
+ note: "invalid json"
96
246
  };
97
247
  }
98
248
  const body = parsed;
99
249
  if (!Array.isArray(body.rows)) return {
100
250
  ok: false,
101
- rows: []
251
+ ledger: {
252
+ schemaVersion: 2,
253
+ rows: []
254
+ },
255
+ note: "rows missing"
102
256
  };
103
257
  const rows = [];
104
258
  let skipped = 0;
105
259
  for (const value of body.rows) {
106
- const row = value;
107
- if (typeof row.id !== "string" || typeof row.pairKey !== "string" || typeof row.a !== "string" || typeof row.b !== "string") {
108
- skipped += 1;
260
+ if (body.schemaVersion === 1) {
261
+ const legacy = value;
262
+ if (typeof legacy.id !== "string" || typeof legacy.pairKey !== "string" || typeof legacy.a !== "string" || typeof legacy.b !== "string" || typeof legacy.state !== "string" || typeof legacy.createdAt !== "string" || typeof legacy.updatedAt !== "string") {
263
+ skipped += 1;
264
+ continue;
265
+ }
266
+ const row = migrateV1Row(legacy);
267
+ rows.push(row);
109
268
  continue;
110
269
  }
111
- if (!isState(row.state) || typeof row.createdAt !== "string" || typeof row.updatedAt !== "string") {
270
+ if (!isRow(value)) {
112
271
  skipped += 1;
113
272
  continue;
114
273
  }
115
- const normalized = {
116
- id: row.id,
117
- pairKey: row.pairKey,
118
- a: row.a,
119
- b: row.b,
120
- state: row.state,
121
- queue: queueOf(row.state),
122
- createdAt: row.createdAt,
123
- updatedAt: row.updatedAt,
124
- ...row.failure === void 0 ? {} : { failure: row.failure },
125
- ...row.decision === void 0 ? {} : { decision: row.decision },
126
- ...typeof row.runId === "string" ? { runId: row.runId } : {},
127
- ...typeof row.unitId === "string" ? { unitId: row.unitId } : {},
128
- ...row.origin === "agent" || row.origin === "human" ? { origin: row.origin } : {},
129
- ...typeof row.attempts === "number" ? { attempts: row.attempts } : {}
130
- };
131
- rows.push(normalized);
274
+ rows.push(value);
132
275
  }
133
- const versionNote = typeof body.schemaVersion === "number" && body.schemaVersion !== 1 ? "the index declares schemaVersion " + String(body.schemaVersion) + " while this build writes " + String(1) : void 0;
134
- const note = versionNote === void 0 ? skipped === 0 ? void 0 : String(skipped) + " invalid row(s) skipped" : skipped === 0 ? versionNote : versionNote + "; " + String(skipped) + " invalid row(s) skipped";
276
+ const note = skipped === 0 ? void 0 : String(skipped) + " invalid row(s) skipped";
135
277
  return {
136
278
  ok: true,
137
- rows,
279
+ ledger: {
280
+ schemaVersion: 2,
281
+ rows
282
+ },
138
283
  ...note === void 0 ? {} : { note }
139
284
  };
140
285
  }
286
+ //#endregion
287
+ //#region lib/types/state.js
288
+ /**
289
+ * The one merge-state ledger: <home>/merge/merge-state.json.
290
+ *
291
+ * Reads are pure projections (no write, no reconciliation side effect). Every write goes
292
+ * through mutateMergeState and a single transactIo, so there is no second file and no
293
+ * incremental writer. A row's queue is always derived from its state on read; a hand-edited
294
+ * mismatch is repaired by the next write, never by a GET.
295
+ */
296
+ function mergeStatePath(home) {
297
+ return join(home, "merge", "merge-state.json");
298
+ }
299
+ function parseState(raw) {
300
+ const parsed = parseLedger(raw);
301
+ if (!parsed.ok) return {
302
+ ok: false,
303
+ rows: []
304
+ };
305
+ return {
306
+ ok: true,
307
+ rows: parsed.ledger.rows,
308
+ ...parsed.note === void 0 ? {} : { note: parsed.note }
309
+ };
310
+ }
141
311
  /** Pure read: never writes, never deletes a row. */
142
312
  async function loadMergeState(io, home) {
143
313
  let raw = null;
@@ -147,7 +317,7 @@ async function loadMergeState(io, home) {
147
317
  return {
148
318
  ok: false,
149
319
  state: {
150
- schemaVersion: 1,
320
+ schemaVersion: 2,
151
321
  rows: []
152
322
  }
153
323
  };
@@ -156,7 +326,7 @@ async function loadMergeState(io, home) {
156
326
  return {
157
327
  ok: parsed.ok,
158
328
  state: {
159
- schemaVersion: 1,
329
+ schemaVersion: 2,
160
330
  rows: parsed.rows
161
331
  },
162
332
  ...parsed.note === void 0 ? {} : { note: parsed.note }
@@ -169,7 +339,7 @@ async function mutateMergeState(io, home, now, fn) {
169
339
  const out = fn(parseState(current).rows);
170
340
  result = out.result;
171
341
  return JSON.stringify({
172
- schemaVersion: 1,
342
+ schemaVersion: 2,
173
343
  rows: out.rows
174
344
  }, null, 2);
175
345
  });
@@ -236,11 +406,374 @@ async function rowsOf(io, home, queue) {
236
406
  return queue === void 0 ? loaded.state.rows : loaded.state.rows.filter((row) => row.queue === queue);
237
407
  }
238
408
  //#endregion
409
+ //#region lib/types/staging.js
410
+ /**
411
+ * Staging is a run ARTIFACT, not a queue: <home>/merge/runs/<runId>/.
412
+ *
413
+ * createStaging writes a sibling .partial tree first and renames it into place; the .stage
414
+ * marker is the last write inside the partial. A directory without that marker is a
415
+ * half-created artifact and reads as unknown, never as "empty".
416
+ */
417
+ const STAGING_SCHEMA_VERSION = 1;
418
+ function stageKeyOf(runId, pairKey) {
419
+ return runId + "/pairs/" + createHash("sha256").update(pairKey).digest("hex").slice(0, 16);
420
+ }
421
+ function stagingRoot(home, runId) {
422
+ return join(home, "merge", "runs", runId);
423
+ }
424
+ /** Relative support paths only; the staging tree must never escape its run directory. */
425
+ function safeRelative$2(path) {
426
+ if (path === "" || path.startsWith("/") || path.startsWith("\\") || path.includes("\0")) return false;
427
+ return path.replace(/\\/g, "/").split("/").every((part) => part !== "" && part !== "." && part !== "..");
428
+ }
429
+ function manifestText(manifest) {
430
+ return JSON.stringify(manifest, null, 2) + "\n";
431
+ }
432
+ async function createStaging(io, home, input) {
433
+ const final = stagingRoot(home, input.runId);
434
+ if (await io.exists(final)) return {
435
+ ok: false,
436
+ message: "staging already exists for run " + input.runId
437
+ };
438
+ const names = /* @__PURE__ */ new Set();
439
+ for (const source of input.sources) {
440
+ if (names.has(source.name)) return {
441
+ ok: false,
442
+ message: "duplicate source " + source.name
443
+ };
444
+ names.add(source.name);
445
+ for (const file of source.files) if (!safeRelative$2(file.path)) return {
446
+ ok: false,
447
+ message: "unsafe support path: " + file.path
448
+ };
449
+ }
450
+ const partial = final + ".partial";
451
+ try {
452
+ await io.remove(partial);
453
+ for (const source of input.sources) {
454
+ const dir = join(partial, "sources", source.name);
455
+ await io.writeText(join(dir, "SKILL.md"), source.body);
456
+ for (const file of source.files) await io.writeText(join(dir, ...file.path.split("/")), file.content);
457
+ }
458
+ const at = input.now();
459
+ const manifest = {
460
+ schemaVersion: 1,
461
+ runId: input.runId,
462
+ target: input.target,
463
+ ...input.mode === void 0 ? {} : { mode: input.mode },
464
+ sources: input.sources.map((source) => source.name),
465
+ promptVersion: input.promptVersion,
466
+ createdAt: at,
467
+ expiresAt: at + Math.max(0, input.ttlDays) * 864e5
468
+ };
469
+ await io.writeText(join(partial, "manifest.json"), manifestText(manifest));
470
+ await io.writeText(join(partial, ".stage"), "");
471
+ await io.rename(partial, final);
472
+ return {
473
+ ok: true,
474
+ path: final,
475
+ manifest
476
+ };
477
+ } catch (error) {
478
+ try {
479
+ await io.remove(partial);
480
+ } catch {}
481
+ return {
482
+ ok: false,
483
+ message: "staging create failed: " + (error instanceof Error ? error.message : String(error))
484
+ };
485
+ }
486
+ }
487
+ async function readStaging(io, home, runId) {
488
+ const path = stagingRoot(home, runId);
489
+ if (!await io.exists(path)) return probeAbsent();
490
+ if (!await io.exists(join(path, ".stage"))) return probeUnknown("staging " + runId + " has no .stage marker (half-created)");
491
+ const probe = await probeText(io, join(path, "manifest.json"));
492
+ if (!isPresent(probe)) return probe;
493
+ try {
494
+ const parsed = JSON.parse(probe.value);
495
+ if (parsed.schemaVersion !== 1 || parsed.runId !== runId || typeof parsed.target !== "string" || !Array.isArray(parsed.sources)) return probeUnknown("staging manifest has an unexpected shape");
496
+ return probePresent({
497
+ path,
498
+ manifest: parsed
499
+ });
500
+ } catch (error) {
501
+ return probeUnknown("staging manifest is not JSON: " + (error instanceof Error ? error.message : String(error)));
502
+ }
503
+ }
504
+ async function ensureStaging(io, home, input) {
505
+ const existing = await readStaging(io, home, input.runId);
506
+ if (existing.kind === "present") return {
507
+ ok: true,
508
+ path: stagingRoot(home, input.runId),
509
+ manifest: existing.value.manifest
510
+ };
511
+ return createStaging(io, home, input);
512
+ }
513
+ async function setStagingTarget(io, home, stageKey, target, mode) {
514
+ const view = await readStaging(io, home, stageKey);
515
+ if (view.kind !== "present") return false;
516
+ const manifest = Object.assign({}, view.value.manifest, { target }, mode === void 0 ? {} : { mode });
517
+ await io.writeText(join(stagingRoot(home, stageKey), "manifest.json"), JSON.stringify(manifest, null, 2) + "\n");
518
+ return true;
519
+ }
520
+ async function dropStaging(io, home, stageKey) {
521
+ await io.remove(stagingRoot(home, stageKey));
522
+ }
523
+ /** Remove expired run-level staging trees; `keep` names runs a live row still points at. */
524
+ async function cleanupExpiredStaging(io, home, now, keep = /* @__PURE__ */ new Set()) {
525
+ let runs;
526
+ try {
527
+ runs = await io.list(join(home, "merge", "runs"));
528
+ } catch {
529
+ return {
530
+ removed: [],
531
+ kept: [],
532
+ unknown: []
533
+ };
534
+ }
535
+ const removed = [];
536
+ const kept = [];
537
+ const unknown = [];
538
+ for (const runId of runs.sort()) {
539
+ if (keep.has(runId)) {
540
+ kept.push(runId);
541
+ continue;
542
+ }
543
+ const view = await readStaging(io, home, runId);
544
+ if (view.kind === "unknown") {
545
+ unknown.push(runId);
546
+ continue;
547
+ }
548
+ if (view.kind !== "present" || view.value.manifest.expiresAt > now) {
549
+ kept.push(runId);
550
+ continue;
551
+ }
552
+ try {
553
+ await io.remove(stagingRoot(home, runId));
554
+ removed.push(runId);
555
+ } catch {
556
+ unknown.push(runId);
557
+ }
558
+ }
559
+ return {
560
+ removed,
561
+ kept,
562
+ unknown
563
+ };
564
+ }
565
+ //#endregion
566
+ //#region lib/types/commit-receipt.js
567
+ /**
568
+ * Commit receipt: <run>/commit.json, written immediately after a successful commit.
569
+ * It is the idempotency/recovery marker — merge resume and run start read it before
570
+ * replaying a commit. Receipt write failure must not turn a landed commit into a failure;
571
+ * the caller reports the warning instead.
572
+ */
573
+ function commitReceiptPath(home, runId) {
574
+ return join(stagingRoot(home, runId), "commit.json");
575
+ }
576
+ function durableReceiptDir(home, parentRunId) {
577
+ return join(home, "merge", "receipts", parentRunId);
578
+ }
579
+ function durableReceiptPath(home, parentRunId, stageKey) {
580
+ const leaf = stageKey.split("/").pop() ?? stageKey;
581
+ return join(durableReceiptDir(home, parentRunId), leaf + ".json");
582
+ }
583
+ function parseReceipt(raw) {
584
+ if (raw === null) return null;
585
+ try {
586
+ const parsed = JSON.parse(raw);
587
+ if (typeof parsed.runId !== "string" || typeof parsed.unitId !== "string" || typeof parsed.target !== "string" || typeof parsed.create !== "boolean" || !Array.isArray(parsed.sources) || !Array.isArray(parsed.archived) || typeof parsed.at !== "number") return null;
588
+ return parsed;
589
+ } catch {
590
+ return null;
591
+ }
592
+ }
593
+ async function writeCommitReceipt(io, home, receipt) {
594
+ const path = commitReceiptPath(home, receipt.runId);
595
+ await io.writeText(path, JSON.stringify(receipt, null, 2) + "\n");
596
+ const parent = receipt.parentRunId ?? receipt.runId.split("/pairs/")[0] ?? receipt.runId;
597
+ await io.writeText(durableReceiptPath(home, parent, receipt.runId), JSON.stringify({
598
+ ...receipt,
599
+ parentRunId: parent
600
+ }, null, 2) + "\n");
601
+ return path;
602
+ }
603
+ async function readCommitReceipts(io, home) {
604
+ const root = join(home, "merge", "receipts");
605
+ const out = [];
606
+ let runs;
607
+ try {
608
+ runs = await io.list(root);
609
+ } catch {
610
+ return out;
611
+ }
612
+ for (const run of runs.sort()) {
613
+ const dir = join(root, run);
614
+ let files;
615
+ try {
616
+ files = await io.list(dir);
617
+ } catch {
618
+ continue;
619
+ }
620
+ for (const file of files.sort()) {
621
+ if (!file.endsWith(".json")) continue;
622
+ const receipt = parseReceipt(await io.readText(join(dir, file)));
623
+ if (receipt !== null) out.push(receipt);
624
+ }
625
+ }
626
+ return out;
627
+ }
628
+ async function readCommitReceipt(io, home, runId, target) {
629
+ const direct = parseReceipt(await io.readText(commitReceiptPath(home, runId)));
630
+ if (direct !== null && (target === void 0 || direct.target === target)) return direct;
631
+ try {
632
+ const names = await io.list(durableReceiptDir(home, runId));
633
+ for (const name of names.sort()) {
634
+ const receipt = parseReceipt(await io.readText(join(durableReceiptDir(home, runId), name)));
635
+ if (receipt !== null && (target === void 0 || receipt.target === target)) return receipt;
636
+ }
637
+ } catch {}
638
+ return null;
639
+ }
640
+ //#endregion
641
+ //#region lib/types/coordinator.js
642
+ /**
643
+ * Pure merge transition table. No io, no run shell, no subagents.
644
+ *
645
+ * Every transition returns the whole next row (or a drop), so the caller can persist it
646
+ * through mutateMergeState in one transaction. Invalid transitions are refused by name;
647
+ * a caller never "repairs" a row by guessing which state the operator meant.
648
+ */
649
+ function invalid(event, state) {
650
+ return {
651
+ ok: false,
652
+ code: "invalid-transition",
653
+ message: "cannot apply " + event.type + " to " + (state ?? "no row")
654
+ };
655
+ }
656
+ /** Drop fields that must not survive a transition; optional fields stay absent. */
657
+ function base(row, at) {
658
+ return {
659
+ id: row.id,
660
+ pairKey: row.pairKey,
661
+ a: row.a,
662
+ b: row.b,
663
+ state: row.state,
664
+ queue: row.queue,
665
+ createdAt: row.createdAt,
666
+ updatedAt: at,
667
+ ...row.runId === void 0 ? {} : { runId: row.runId },
668
+ ...row.unitId === void 0 ? {} : { unitId: row.unitId },
669
+ ...row.origin === void 0 ? {} : { origin: row.origin },
670
+ ...row.decision === void 0 ? {} : { decision: row.decision },
671
+ ...row.sourceRevision === void 0 ? {} : { sourceRevision: row.sourceRevision },
672
+ ...row.targetRevision === void 0 ? {} : { targetRevision: row.targetRevision },
673
+ ...row.attempts === void 0 ? {} : { attempts: row.attempts },
674
+ ...row.stagingRef === void 0 ? {} : { stagingRef: row.stagingRef },
675
+ ...row.stagingExpiresAt === void 0 ? {} : { stagingExpiresAt: row.stagingExpiresAt },
676
+ ...row.expired === void 0 ? {} : { expired: row.expired },
677
+ ...row.reason === void 0 ? {} : { reason: row.reason },
678
+ ...row.target === void 0 ? {} : { target: row.target }
679
+ };
680
+ }
681
+ function next(row, at, state, patch = {}) {
682
+ return {
683
+ ok: true,
684
+ row: {
685
+ ...base(row, at),
686
+ state,
687
+ queue: queueOf(state),
688
+ ...patch
689
+ }
690
+ };
691
+ }
692
+ function transition(current, event) {
693
+ if (event.type === "candidate") {
694
+ if (current !== void 0) return {
695
+ ok: true,
696
+ row: current
697
+ };
698
+ const key = pairKey(event.a, event.b);
699
+ return {
700
+ ok: true,
701
+ row: {
702
+ id: rowIdOf(key),
703
+ pairKey: key,
704
+ a: event.a,
705
+ b: event.b,
706
+ state: "verify",
707
+ queue: "verify",
708
+ createdAt: event.at,
709
+ updatedAt: event.at
710
+ }
711
+ };
712
+ }
713
+ if (event.type === "source-gone") return {
714
+ ok: true,
715
+ row: null
716
+ };
717
+ if (event.type === "gate-keep") {
718
+ if (current !== void 0) return {
719
+ ok: true,
720
+ row: current
721
+ };
722
+ const key = pairKey(event.a, event.b);
723
+ return {
724
+ ok: true,
725
+ row: {
726
+ id: rowIdOf(key),
727
+ pairKey: key,
728
+ a: event.a,
729
+ b: event.b,
730
+ state: "skipped",
731
+ queue: "skipped",
732
+ origin: "agent",
733
+ createdAt: event.at,
734
+ updatedAt: event.at
735
+ }
736
+ };
737
+ }
738
+ if (current === void 0) return invalid(event, void 0);
739
+ switch (event.type) {
740
+ case "round1-keep": return current.state === "verify" ? next(current, event.at, "skipped", { origin: "agent" }) : invalid(event, current.state);
741
+ case "round1-merge":
742
+ if (current.state !== "verify") return invalid(event, current.state);
743
+ return event.approval === "on" ? next(current, event.at, "pending", {
744
+ ...event.decision === void 0 ? {} : { decision: event.decision },
745
+ ...event.stagingRef === void 0 ? {} : { stagingRef: event.stagingRef },
746
+ ...event.stagingExpiresAt === void 0 ? {} : { stagingExpiresAt: event.stagingExpiresAt }
747
+ }) : next(current, event.at, "approved", {
748
+ ...event.decision === void 0 ? {} : { decision: event.decision },
749
+ ...event.stagingRef === void 0 ? {} : { stagingRef: event.stagingRef },
750
+ ...event.stagingExpiresAt === void 0 ? {} : { stagingExpiresAt: event.stagingExpiresAt }
751
+ });
752
+ case "round1-fail": return current.state === "verify" ? next(current, event.at, "verify", {
753
+ failure: event.failure,
754
+ attempts: (current.attempts ?? 0) + 1
755
+ }) : invalid(event, current.state);
756
+ case "approve": return current.state === "pending" ? next(current, event.at, "approved", { ...event.runId === void 0 ? {} : { runId: event.runId } }) : invalid(event, current.state);
757
+ case "reject": return current.state === "pending" ? next(current, event.at, "skipped", { origin: "human" }) : invalid(event, current.state);
758
+ case "round2-cancel": return current.state === "fuse" || current.state === "approved" ? next(current, event.at, "approved") : invalid(event, current.state);
759
+ case "round2-fail": return current.state === "fuse" || current.state === "approved" ? next(current, event.at, "approved", {
760
+ failure: event.failure,
761
+ attempts: (current.attempts ?? 0) + 1
762
+ }) : invalid(event, current.state);
763
+ case "round2-success": return current.state === "fuse" || current.state === "approved" ? {
764
+ ok: true,
765
+ row: null
766
+ } : invalid(event, current.state);
767
+ case "restore": return current.state === "skipped" ? next(current, event.at, "verify") : invalid(event, current.state);
768
+ }
769
+ }
770
+ //#endregion
239
771
  //#region lib/types/status.js
240
772
  function countRows(rows) {
241
773
  const counts = {
774
+ verify: 0,
242
775
  pending: 0,
243
- retry: 0,
776
+ fuse: 0,
244
777
  skipped: 0
245
778
  };
246
779
  for (const row of rows) counts[queueOf(row.state)] += 1;
@@ -253,7 +786,7 @@ function describe(row) {
253
786
  }
254
787
  function formatStatus(rows) {
255
788
  const counts = countRows(rows);
256
- const lines = ["merge: pending " + String(counts.pending) + ", retry " + String(counts.retry) + ", skipped " + String(counts.skipped)];
789
+ const lines = ["merge: verify " + String(counts.verify) + ", pending " + String(counts.pending) + ", fuse " + String(counts.fuse) + ", skipped " + String(counts.skipped)];
257
790
  for (const row of rows) lines.push("- " + describe(row));
258
791
  return lines.join("\n");
259
792
  }
@@ -274,29 +807,55 @@ function toRowView(row) {
274
807
  queue: queueOf(row.state),
275
808
  stage: stageOf(row.state),
276
809
  label: row.a + " + " + row.b,
277
- updatedAt: row.updatedAt
810
+ updatedAt: row.updatedAt,
811
+ createdAt: row.createdAt,
812
+ ...row.decision?.target === void 0 ? {} : { target: row.decision.target },
813
+ ...row.decision?.confidence === void 0 ? {} : { confidence: row.decision.confidence },
814
+ ...row.failure === void 0 ? {} : { failure: {
815
+ stage: row.failure.stage,
816
+ code: row.failure.code,
817
+ message: row.failure.message,
818
+ retryable: row.failure.retryable
819
+ } },
820
+ ...row.attempts === void 0 ? {} : { attempts: row.attempts },
821
+ ...row.origin === void 0 ? {} : { origin: row.origin },
822
+ ...row.stagingExpiresAt === void 0 ? {} : { stagingExpiresAt: row.stagingExpiresAt },
823
+ ...row.expired === void 0 ? {} : { expired: row.expired },
824
+ ...row.reason === void 0 ? {} : { reason: row.reason },
825
+ summary: [
826
+ row.a + " + " + row.b,
827
+ row.decision?.rationale ?? row.reason ?? "",
828
+ row.failure?.message ?? ""
829
+ ],
830
+ 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
831
  };
279
832
  }
280
- function buildMergeView(rows, commands) {
833
+ function buildMergeView(rows, commands, latestRun, approval) {
281
834
  const counts = countRows(rows);
282
835
  return {
283
- schemaVersion: 1,
836
+ schemaVersion: 2,
284
837
  counts,
285
838
  tabs: [
839
+ {
840
+ queue: "verify",
841
+ count: counts.verify
842
+ },
286
843
  {
287
844
  queue: "pending",
288
845
  count: counts.pending
289
846
  },
290
847
  {
291
- queue: "retry",
292
- count: counts.retry
848
+ queue: "fuse",
849
+ count: counts.fuse
293
850
  },
294
851
  {
295
852
  queue: "skipped",
296
853
  count: counts.skipped
297
854
  }
298
855
  ],
299
- commands
856
+ commands,
857
+ ...latestRun === void 0 ? {} : { latestRun },
858
+ ...approval === void 0 ? {} : { approval }
300
859
  };
301
860
  }
302
861
  function pageQueue(rows, queue, cursor = 0, limit = 50) {
@@ -326,8 +885,70 @@ function toRunFact(run) {
326
885
  };
327
886
  }
328
887
  //#endregion
888
+ //#region lib/types/domain/markdown.js
889
+ /** Minimal safe Markdown renderer for the read-only body route. */
890
+ function escapeHtml(text) {
891
+ return text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
892
+ }
893
+ function renderMarkdown(source) {
894
+ const fence = String.fromCharCode(96, 96, 96);
895
+ const lines = source.split(/\r?\n/);
896
+ const out = [];
897
+ let inCode = false;
898
+ let inList = false;
899
+ for (const line of lines) {
900
+ if (line.startsWith(fence)) {
901
+ if (inCode) {
902
+ out.push("</code></pre>");
903
+ inCode = false;
904
+ } else {
905
+ out.push("<pre><code>");
906
+ inCode = true;
907
+ }
908
+ continue;
909
+ }
910
+ if (inCode) {
911
+ out.push(escapeHtml(line));
912
+ continue;
913
+ }
914
+ const heading = /^(#{1,6})\s+(.*)$/.exec(line);
915
+ if (heading !== null) {
916
+ if (inList) {
917
+ out.push("</ul>");
918
+ inList = false;
919
+ }
920
+ const level = heading[1]?.length ?? 1;
921
+ out.push("<h" + String(level) + ">" + escapeHtml(heading[2] ?? "") + "</h" + String(level) + ">");
922
+ continue;
923
+ }
924
+ if (/^[-*]\s+/.test(line)) {
925
+ if (!inList) {
926
+ out.push("<ul>");
927
+ inList = true;
928
+ }
929
+ out.push("<li>" + escapeHtml(line.replace(/^[-*]\s+/, "")) + "</li>");
930
+ continue;
931
+ }
932
+ if (inList) {
933
+ out.push("</ul>");
934
+ inList = false;
935
+ }
936
+ out.push(line.trim() === "" ? "" : "<p>" + escapeHtml(line) + "</p>");
937
+ }
938
+ if (inList) out.push("</ul>");
939
+ if (inCode) out.push("</code></pre>");
940
+ return out.join("\n");
941
+ }
942
+ //#endregion
329
943
  //#region lib/types/merge-routes.js
330
- const MERGE_ROUTES = HOST_ROUTES.merge;
944
+ const MERGE_PATHS = HOST_ROUTES.merge;
945
+ const MERGE_BASE = MERGE_PATHS.view.slice(0, -4);
946
+ const MERGE_ROUTES = {
947
+ view: MERGE_PATHS.view,
948
+ rows: MERGE_PATHS.rows,
949
+ body: MERGE_PATHS.body ?? MERGE_BASE + "body",
950
+ history: MERGE_PATHS.history ?? MERGE_BASE + "history"
951
+ };
331
952
  function makeMergeRoutes(deps, _audit) {
332
953
  const absent = () => json(503, {
333
954
  ok: false,
@@ -348,9 +969,10 @@ function makeMergeRoutes(deps, _audit) {
348
969
  if (denied !== null) return denied;
349
970
  const source = deps.getSource();
350
971
  if (source === void 0) return absent();
972
+ const latestMerge = (await source.runs(void 0, 20)).find((run) => run.kind === "merge");
351
973
  return json(200, {
352
974
  ok: true,
353
- data: buildMergeView(await source.rows(), source.commands())
975
+ data: buildMergeView(await source.rows(), source.commands(), latestMerge === void 0 ? void 0 : toRunFact(latestMerge), source.approval?.())
354
976
  });
355
977
  }
356
978
  },
@@ -364,12 +986,12 @@ function makeMergeRoutes(deps, _audit) {
364
986
  const source = deps.getSource();
365
987
  if (source === void 0) return absent();
366
988
  const query = url(request).searchParams;
367
- const queue = query.get("queue") ?? "retry";
368
- if (queue !== "pending" && queue !== "retry" && queue !== "skipped") return json(400, {
989
+ const queue = query.get("queue") ?? "verify";
990
+ if (queue !== "verify" && queue !== "pending" && queue !== "fuse" && queue !== "skipped") return json(400, {
369
991
  ok: false,
370
992
  error: {
371
993
  code: "bad-queue",
372
- message: "queue must be pending/retry/skipped"
994
+ message: "queue must be verify/pending/fuse/skipped"
373
995
  }
374
996
  });
375
997
  const cursor = Number.parseInt(query.get("cursor") ?? "0", 10);
@@ -381,7 +1003,7 @@ function makeMergeRoutes(deps, _audit) {
381
1003
  }
382
1004
  },
383
1005
  {
384
- path: MERGE_ROUTES.run,
1006
+ path: MERGE_ROUTES.body,
385
1007
  methods: ["GET"],
386
1008
  requestBody: "buffered",
387
1009
  fetch: async (request) => {
@@ -389,29 +1011,35 @@ function makeMergeRoutes(deps, _audit) {
389
1011
  if (denied !== null) return denied;
390
1012
  const source = deps.getSource();
391
1013
  if (source === void 0) return absent();
392
- const id = url(request).searchParams.get("id");
393
- if (id === null || id === "") return json(400, {
1014
+ const query = url(request).searchParams;
1015
+ const id = query.get("id");
1016
+ const side = query.get("side");
1017
+ if (id === null || id === "" || side !== "a" && side !== "b") return json(400, {
394
1018
  ok: false,
395
1019
  error: {
396
- code: "missing-id",
397
- message: "id is required"
1020
+ code: "bad-body-query",
1021
+ message: "id and side=a|b are required"
398
1022
  }
399
1023
  });
400
- const run = await source.run(id);
401
- return run === void 0 ? json(404, {
1024
+ const content = await source.body(id, side);
1025
+ return content === null ? json(404, {
402
1026
  ok: false,
403
1027
  error: {
404
- code: "run-not-found",
405
- message: "merge run not found: " + id
1028
+ code: "body-not-found",
1029
+ message: "merge body not found: " + id
406
1030
  }
407
1031
  }) : json(200, {
408
1032
  ok: true,
409
- data: toRunFact(run)
1033
+ data: {
1034
+ content,
1035
+ markdown: true,
1036
+ html: renderMarkdown(content)
1037
+ }
410
1038
  });
411
1039
  }
412
1040
  },
413
1041
  {
414
- path: MERGE_ROUTES.runs,
1042
+ path: MERGE_ROUTES.history,
415
1043
  methods: ["GET"],
416
1044
  requestBody: "buffered",
417
1045
  fetch: async (request) => {
@@ -419,11 +1047,9 @@ function makeMergeRoutes(deps, _audit) {
419
1047
  if (denied !== null) return denied;
420
1048
  const source = deps.getSource();
421
1049
  if (source === void 0) return absent();
422
- const query = url(request).searchParams;
423
- const limit = Number.parseInt(query.get("limit") ?? "20", 10);
424
1050
  return json(200, {
425
1051
  ok: true,
426
- data: { items: (await source.runs(query.get("cursor") ?? void 0, Number.isFinite(limit) ? limit : 20)).map(toRunFact) }
1052
+ data: { items: await source.history() }
427
1053
  });
428
1054
  }
429
1055
  }
@@ -431,12 +1057,12 @@ function makeMergeRoutes(deps, _audit) {
431
1057
  }
432
1058
  //#endregion
433
1059
  //#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>";
1060
+ const USAGE = "usage: /evolution merge scan | pair [--force] <a> <b> | status [id] | report [id] | cancel [id] | runs | doctor | pending [--expired] | candidates [--retry] | approve <id> | reject <id> | resume [id] [--retry] [--clear <id>] | skipped [--restore <id>] | discard <id> | undo <target> --run <id>";
435
1061
  function flags(args, allowed) {
436
1062
  const values = /* @__PURE__ */ new Map();
437
1063
  for (let index = 0; index < args.length; index += 1) {
438
1064
  const token = args[index];
439
- if (!token.startsWith("--")) continue;
1065
+ if (!token.startsWith("--") || token === "--force" || token === "--expired" || token === "--retry") continue;
440
1066
  const name = token.slice(2);
441
1067
  if (!allowed.includes(name)) return {
442
1068
  values,
@@ -452,8 +1078,12 @@ function flags(args, allowed) {
452
1078
  }
453
1079
  return { values };
454
1080
  }
1081
+ function switches(args) {
1082
+ return new Set(args.filter((token) => token === "--force" || token === "--expired" || token === "--retry").map((token) => token.slice(2)));
1083
+ }
455
1084
  function positional(args) {
456
- return args.filter((token, index) => !token.startsWith("--") && !(index > 0 && args[index - 1].startsWith("--")));
1085
+ const filtered = args.filter((token) => token !== "--force" && token !== "--expired" && token !== "--retry");
1086
+ return filtered.filter((token, index) => !token.startsWith("--") && !(index > 0 && filtered[index - 1].startsWith("--")));
457
1087
  }
458
1088
  function parseMergeCommand(argv) {
459
1089
  const [verb, ...rest] = argv;
@@ -463,6 +1093,7 @@ function parseMergeCommand(argv) {
463
1093
  help: true
464
1094
  };
465
1095
  const pos = positional(rest);
1096
+ const marked = switches(rest);
466
1097
  switch (verb) {
467
1098
  case "scan": return pos.length === 0 ? {
468
1099
  ok: true,
@@ -477,7 +1108,8 @@ function parseMergeCommand(argv) {
477
1108
  command: {
478
1109
  verb: "pair",
479
1110
  a: pos[0],
480
- b: pos[1]
1111
+ b: pos[1],
1112
+ ...marked.has("force") ? { force: true } : {}
481
1113
  }
482
1114
  } : {
483
1115
  ok: false,
@@ -525,17 +1157,31 @@ function parseMergeCommand(argv) {
525
1157
  code: "usage",
526
1158
  message: USAGE
527
1159
  };
528
- case "doctor": return pos.length === 0 ? {
1160
+ case "doctor": return pos.length === 0 ? {
1161
+ ok: true,
1162
+ command: { verb: "doctor" }
1163
+ } : {
1164
+ ok: false,
1165
+ code: "usage",
1166
+ message: USAGE
1167
+ };
1168
+ case "pending": return pos.length === 0 ? {
529
1169
  ok: true,
530
- command: { verb: "doctor" }
1170
+ command: {
1171
+ verb: "pending",
1172
+ ...marked.has("expired") ? { expired: true } : {}
1173
+ }
531
1174
  } : {
532
1175
  ok: false,
533
1176
  code: "usage",
534
1177
  message: USAGE
535
1178
  };
536
- case "pending": return pos.length === 0 ? {
1179
+ case "candidates": return pos.length === 0 ? {
537
1180
  ok: true,
538
- command: { verb: "pending" }
1181
+ command: {
1182
+ verb: "candidates",
1183
+ ...marked.has("retry") ? { retry: true } : {}
1184
+ }
539
1185
  } : {
540
1186
  ok: false,
541
1187
  code: "usage",
@@ -581,7 +1227,8 @@ function parseMergeCommand(argv) {
581
1227
  command: {
582
1228
  verb: "resume",
583
1229
  ...pos[0] === void 0 ? {} : { id: pos[0] },
584
- ...clear === void 0 ? {} : { clear }
1230
+ ...clear === void 0 ? {} : { clear },
1231
+ ...marked.has("retry") ? { retry: true } : {}
585
1232
  }
586
1233
  };
587
1234
  }
@@ -606,6 +1253,17 @@ function parseMergeCommand(argv) {
606
1253
  }
607
1254
  };
608
1255
  }
1256
+ case "discard": return pos.length === 1 ? {
1257
+ ok: true,
1258
+ command: {
1259
+ verb: "discard",
1260
+ id: pos[0]
1261
+ }
1262
+ } : {
1263
+ ok: false,
1264
+ code: "usage",
1265
+ message: USAGE
1266
+ };
609
1267
  case "undo": {
610
1268
  const parsed = flags(rest, ["run"]);
611
1269
  if (parsed.error !== void 0) return {
@@ -632,7 +1290,7 @@ function parseMergeCommand(argv) {
632
1290
  default: return {
633
1291
  ok: false,
634
1292
  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>"
1293
+ message: "unknown merge command: " + verb + "\nusage: /evolution merge scan | pair [--force] <a> <b> | status [id] | report [id] | cancel [id] | runs | doctor | pending [--expired] | candidates [--retry] | approve <id> | reject <id> | resume [id] [--retry] [--clear <id>] | skipped [--restore <id>] | discard <id> | undo <target> --run <id>"
636
1294
  };
637
1295
  }
638
1296
  }
@@ -650,7 +1308,15 @@ async function handleReadOnlyMergeCommand(command, io, home) {
650
1308
  }
651
1309
  return formatStatus(loaded.state.rows);
652
1310
  }
653
- if (command.verb === "pending") return formatQueue((await loadMergeState(io, home)).state.rows, "pending");
1311
+ if (command.verb === "pending") {
1312
+ const loaded = await loadMergeState(io, home);
1313
+ const rows = command.expired === true ? loaded.state.rows.filter((row) => row.expired !== void 0) : loaded.state.rows;
1314
+ return command.expired === true ? formatStatus(rows) : formatQueue(rows, "pending");
1315
+ }
1316
+ if (command.verb === "candidates") {
1317
+ const loaded = await loadMergeState(io, home);
1318
+ return formatQueue(command.retry === true ? loaded.state.rows.filter((row) => row.state === "approved") : loaded.state.rows, command.retry === true ? "fuse" : "verify");
1319
+ }
654
1320
  if (command.verb === "skipped") return formatQueue((await loadMergeState(io, home)).state.rows, "skipped");
655
1321
  return null;
656
1322
  }
@@ -658,6 +1324,13 @@ function mergeCommandHelp() {
658
1324
  return mergeUsage();
659
1325
  }
660
1326
  //#endregion
1327
+ //#region lib/types/domain/errors.js
1328
+ function formatMergeError(error) {
1329
+ if (error.kind === "parameter") return "merge: " + error.message + " (" + error.code + ")";
1330
+ const detail = "\n" + (error.detail ?? "no additional detail");
1331
+ return "merge failed: " + error.message + " (" + error.code + ")" + detail + "\nnext: fix the target or rerun after the cause is gone";
1332
+ }
1333
+ //#endregion
661
1334
  //#region lib/types/merge-command.js
662
1335
  function createReadOnlyMergeService(io, home) {
663
1336
  return { async execute(command) {
@@ -671,35 +1344,43 @@ function createReadOnlyMergeService(io, home) {
671
1344
  };
672
1345
  } };
673
1346
  }
1347
+ async function runMergeCommand(service, rawInput, invocation) {
1348
+ const tokens = rawInput.trim().split(/\s+/).filter(Boolean);
1349
+ if (tokens[0] === "merge" || tokens[0] === "/merge" || tokens[0] === "evolution" || tokens[0] === "/evolution") tokens.shift();
1350
+ if (tokens[0] === "merge") tokens.shift();
1351
+ const parsed = parseMergeCommand(tokens);
1352
+ if (!parsed.ok) return {
1353
+ ok: false,
1354
+ text: parsed.message
1355
+ };
1356
+ if (parsed.help === true) return {
1357
+ ok: true,
1358
+ text: mergeUsage()
1359
+ };
1360
+ try {
1361
+ return await service.execute(parsed.command, invocation?.agent === void 0 ? {} : { parent: invocation.agent });
1362
+ } catch (error) {
1363
+ return {
1364
+ ok: false,
1365
+ text: formatMergeError({
1366
+ kind: "runtime",
1367
+ code: "merge-command-failed",
1368
+ message: error instanceof Error ? error.message : String(error)
1369
+ })
1370
+ };
1371
+ }
1372
+ }
674
1373
  function createMergeCommand(service) {
675
1374
  return {
676
1375
  name: "merge",
677
- description: "技能自动合并:扫描、待批准、重跑、报告与 undo",
1376
+ description: "技能自动合并:扫描、待批准、重跑、报告与 undo(兼容别名;请用 /evolution merge)",
678
1377
  input: { hint: mergeUsage() },
679
1378
  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()
1379
+ const result = await runMergeCommand(service, invocation.rawInput, invocation);
1380
+ return {
1381
+ kind: result.ok ? "success" : "error",
1382
+ text: result.text
690
1383
  };
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
1384
  }
704
1385
  };
705
1386
  }
@@ -726,97 +1407,50 @@ function isSkippedPair(keys, a, b) {
726
1407
  return keys.has(pairKey(a, b));
727
1408
  }
728
1409
  //#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
- };
1410
+ //#region lib/types/domain/pair-index.js
1411
+ function parsePairIndex(raw) {
1412
+ const index = /* @__PURE__ */ new Map();
1413
+ if (raw === null) return index;
1414
+ let parsed;
1415
+ try {
1416
+ parsed = JSON.parse(raw);
1417
+ } catch {
1418
+ return index;
793
1419
  }
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);
1420
+ const body = parsed;
1421
+ if (!Array.isArray(body.entries)) return index;
1422
+ for (const value of body.entries) {
1423
+ const entry = value;
1424
+ if (typeof entry.pairKey !== "string" || typeof entry.runId !== "string" || typeof entry.result !== "string") continue;
1425
+ if (![
1426
+ "merged",
1427
+ "kept",
1428
+ "failed",
1429
+ "cancelled",
1430
+ "undone"
1431
+ ].includes(entry.result)) continue;
1432
+ index.set(entry.pairKey, entry);
819
1433
  }
1434
+ return index;
1435
+ }
1436
+ function serializePairIndex(index) {
1437
+ return JSON.stringify({
1438
+ schemaVersion: 1,
1439
+ entries: [...index.values()]
1440
+ }, null, 2);
1441
+ }
1442
+ //#endregion
1443
+ //#region lib/types/domain/pair-index-store.js
1444
+ /** Durable pairKey index at <home>/merge/pair-index.json. */
1445
+ function pairIndexPath(home) {
1446
+ return join(home, "merge", "pair-index.json");
1447
+ }
1448
+ async function recordPair(io, home, entry) {
1449
+ await transactIo(io, pairIndexPath(home), (current) => {
1450
+ const index = parsePairIndex(current);
1451
+ index.set(entry.pairKey, entry);
1452
+ return serializePairIndex(index);
1453
+ });
820
1454
  }
821
1455
  //#endregion
822
1456
  //#region lib/types/scan-run.js
@@ -824,6 +1458,7 @@ function transition(current, event) {
824
1458
  * The scan pair loop. Registry/report lifecycle stays in withMergeRun; this module owns
825
1459
  * candidate → round1 → pending/fuse → commit transitions and returns the report draft.
826
1460
  */
1461
+ const isAborted = (signal) => signal.aborted;
827
1462
  function at(now) {
828
1463
  return new Date(now()).toISOString();
829
1464
  }
@@ -852,14 +1487,15 @@ function pairOf(row, candidate, outcome, extra = {}) {
852
1487
  }
853
1488
  async function runScan(options) {
854
1489
  const stage = options.stage ?? (async () => {});
1490
+ const candidates = await options.ports.screening(options.signal);
855
1491
  const before = await loadMergeState(options.io, options.home);
856
1492
  const queueBefore = {
1493
+ verify: 0,
857
1494
  pending: 0,
858
- retry: 0,
1495
+ fuse: 0,
859
1496
  skipped: 0
860
1497
  };
861
1498
  for (const row of before.state.rows) queueBefore[row.queue] += 1;
862
- const candidates = await options.ports.screening(options.signal);
863
1499
  const startedAt = options.startedAt ?? options.now();
864
1500
  const timeoutMs = (options.timeoutMinutes ?? 0) > 0 ? options.timeoutMinutes * 6e4 : 0;
865
1501
  const pairs = [];
@@ -872,11 +1508,11 @@ async function runScan(options) {
872
1508
  const remaining = [];
873
1509
  let stopReason = "completed";
874
1510
  for (let index = 0; index < candidates.length; index += 1) {
875
- if (options.signal.aborted) {
1511
+ if (isAborted(options.signal)) {
876
1512
  stopReason = "cancelled";
877
1513
  remaining.push(...candidates.slice(index).map((candidate) => ({
878
1514
  pairKey: pairKey(candidate.a, candidate.b),
879
- queue: "retry"
1515
+ queue: "verify"
880
1516
  })));
881
1517
  break;
882
1518
  }
@@ -884,7 +1520,7 @@ async function runScan(options) {
884
1520
  stopReason = "timeout";
885
1521
  remaining.push(...candidates.slice(index).map((candidate) => ({
886
1522
  pairKey: pairKey(candidate.a, candidate.b),
887
- queue: "retry"
1523
+ queue: "verify"
888
1524
  })));
889
1525
  break;
890
1526
  }
@@ -911,7 +1547,7 @@ async function runScan(options) {
911
1547
  pairKey: pairKey(candidate.a, candidate.b),
912
1548
  a: candidate.a,
913
1549
  b: candidate.b,
914
- queue: "retry",
1550
+ queue: "verify",
915
1551
  candidate,
916
1552
  outcome: "failed",
917
1553
  archived: [],
@@ -928,6 +1564,14 @@ async function runScan(options) {
928
1564
  failure: failureOf("decide", error)
929
1565
  };
930
1566
  }
1567
+ if (isAborted(options.signal)) {
1568
+ stopReason = "cancelled";
1569
+ remaining.push(...candidates.slice(index).map((candidate) => ({
1570
+ pairKey: pairKey(candidate.a, candidate.b),
1571
+ queue: "verify"
1572
+ })));
1573
+ break;
1574
+ }
931
1575
  if (round1.kind === "failed") {
932
1576
  const next = transition(row, {
933
1577
  type: "round1-fail",
@@ -950,6 +1594,11 @@ async function runScan(options) {
950
1594
  if (next.ok && next.row !== null) {
951
1595
  row = next.row;
952
1596
  await upsertRow(options.io, options.home, options.now, row);
1597
+ await recordPair(options.io, options.home, {
1598
+ pairKey: row.pairKey,
1599
+ runId: row.runId ?? "merge",
1600
+ result: "kept"
1601
+ });
953
1602
  }
954
1603
  pairs.push(pairOf(row, candidate, "kept", round1.decision === void 0 ? {} : { decision: round1.decision }));
955
1604
  totals.kept += 1;
@@ -965,6 +1614,8 @@ async function runScan(options) {
965
1614
  type: "round1-merge",
966
1615
  approval: "on",
967
1616
  decision: round1.decision,
1617
+ ...round1.sourceRevision === void 0 ? {} : { sourceRevision: round1.sourceRevision },
1618
+ ...round1.targetRevision === void 0 ? {} : { targetRevision: round1.targetRevision },
968
1619
  at: at(options.now)
969
1620
  });
970
1621
  if (next.ok && next.row !== null) {
@@ -979,6 +1630,8 @@ async function runScan(options) {
979
1630
  type: "round1-merge",
980
1631
  approval: "off",
981
1632
  decision: round1.decision,
1633
+ ...round1.stagingRef === void 0 ? {} : { stagingRef: round1.stagingRef },
1634
+ ...round1.stagingExpiresAt === void 0 ? {} : { stagingExpiresAt: round1.stagingExpiresAt },
982
1635
  at: at(options.now)
983
1636
  });
984
1637
  if (!fuse.ok || fuse.row === null) {
@@ -991,6 +1644,22 @@ async function runScan(options) {
991
1644
  }
992
1645
  row = fuse.row;
993
1646
  await upsertRow(options.io, options.home, options.now, row);
1647
+ if (isAborted(options.signal)) {
1648
+ const cancelled = transition(row, {
1649
+ type: "round2-cancel",
1650
+ at: at(options.now)
1651
+ });
1652
+ if (cancelled.ok && cancelled.row !== null) {
1653
+ row = cancelled.row;
1654
+ await upsertRow(options.io, options.home, options.now, row);
1655
+ }
1656
+ stopReason = "cancelled";
1657
+ remaining.push(...candidates.slice(index).map((candidate) => ({
1658
+ pairKey: pairKey(candidate.a, candidate.b),
1659
+ queue: "fuse"
1660
+ })));
1661
+ break;
1662
+ }
994
1663
  await stage("author", {
995
1664
  done: index,
996
1665
  total: candidates.length
@@ -1037,7 +1706,7 @@ async function runScan(options) {
1037
1706
  return {
1038
1707
  stopReason,
1039
1708
  scope: {
1040
- candidatePairs: candidates.length,
1709
+ candidatePairs: candidates.candidatePairs ?? candidates.length,
1041
1710
  queueBefore
1042
1711
  },
1043
1712
  pairs,
@@ -1063,7 +1732,7 @@ function mergeReportDigestPath(home, runId) {
1063
1732
  function isReport(value) {
1064
1733
  if (typeof value !== "object" || value === null) return false;
1065
1734
  const report = value;
1066
- return report.schemaVersion === 1 && report.kind === "merge" && typeof report.runId === "string" && typeof report.command === "string" && typeof report.startedAt === "number" && typeof report.endedAt === "number" && Array.isArray(report.pairs) && typeof report.totals === "object" && Array.isArray(report.notes);
1735
+ return report.schemaVersion === 2 && report.kind === "merge" && typeof report.runId === "string" && typeof report.command === "string" && typeof report.startedAt === "number" && typeof report.endedAt === "number" && Array.isArray(report.pairs) && typeof report.totals === "object" && Array.isArray(report.notes);
1067
1736
  }
1068
1737
  function digest(report) {
1069
1738
  const lines = [];
@@ -1110,8 +1779,9 @@ function emptyDraft(reason) {
1110
1779
  scope: {
1111
1780
  candidatePairs: 0,
1112
1781
  queueBefore: {
1782
+ verify: 0,
1113
1783
  pending: 0,
1114
- retry: 0,
1784
+ fuse: 0,
1115
1785
  skipped: 0
1116
1786
  }
1117
1787
  },
@@ -1128,6 +1798,7 @@ function emptyDraft(reason) {
1128
1798
  async function withMergeRun(options) {
1129
1799
  const handle = await options.registry.begin("merge", {
1130
1800
  command: options.command,
1801
+ mergeStatus: "running",
1131
1802
  ...options.total === void 0 ? {} : { total: options.total }
1132
1803
  });
1133
1804
  const startedAt = handle.startedAt;
@@ -1136,64 +1807,77 @@ async function withMergeRun(options) {
1136
1807
  done: 0,
1137
1808
  total: options.total
1138
1809
  });
1139
- let draft;
1140
- try {
1141
- draft = await options.body({
1810
+ const finish = async () => {
1811
+ let draft;
1812
+ try {
1813
+ draft = await options.body({
1814
+ runId: handle.id,
1815
+ startedAt,
1816
+ signal: handle.signal,
1817
+ stage
1818
+ });
1819
+ } catch (error) {
1820
+ draft = emptyDraft("run body failed: " + (error instanceof Error ? error.message : String(error)));
1821
+ }
1822
+ const report = {
1823
+ schemaVersion: 2,
1142
1824
  runId: handle.id,
1825
+ kind: "merge",
1826
+ command: options.command,
1143
1827
  startedAt,
1144
- signal: handle.signal,
1145
- stage
1146
- });
1147
- } catch (error) {
1148
- draft = emptyDraft("run body failed: " + (error instanceof Error ? error.message : String(error)));
1149
- }
1150
- const report = {
1151
- schemaVersion: 1,
1152
- runId: handle.id,
1153
- kind: "merge",
1154
- command: options.command,
1155
- startedAt,
1156
- endedAt: options.now(),
1157
- stopReason: draft.stopReason,
1158
- approval: options.approval,
1159
- scope: draft.scope,
1160
- pairs: draft.pairs,
1161
- totals: draft.totals,
1162
- notes: draft.notes,
1163
- ...draft.remaining === void 0 ? {} : { remaining: draft.remaining }
1164
- };
1165
- let reportPath;
1166
- try {
1167
- reportPath = await writeMergeReport(options.io, options.home, report);
1168
- } catch (error) {
1828
+ endedAt: options.now(),
1829
+ stopReason: draft.stopReason,
1830
+ approval: options.approval,
1831
+ scope: draft.scope,
1832
+ pairs: draft.pairs,
1833
+ totals: draft.totals,
1834
+ notes: draft.notes,
1835
+ ...draft.remaining === void 0 ? {} : { remaining: draft.remaining }
1836
+ };
1837
+ let reportPath;
1838
+ try {
1839
+ reportPath = await writeMergeReport(options.io, options.home, report);
1840
+ } catch (error) {
1841
+ await options.registry.settle(handle.id, {
1842
+ state: "failed",
1843
+ failure: "report write failed: " + (error instanceof Error ? error.message : String(error))
1844
+ });
1845
+ return {
1846
+ ok: false,
1847
+ runId: handle.id,
1848
+ message: "report write failed"
1849
+ };
1850
+ }
1851
+ const runState = report.stopReason === "completed" ? "succeeded" : report.stopReason === "cancelled" ? "cancelled" : "failed";
1852
+ const mergeStatus = report.pairs.some((pair) => pair.outcome === "pending") ? "awaiting-approval" : runState;
1169
1853
  await options.registry.settle(handle.id, {
1170
- state: "failed",
1171
- failure: "report write failed: " + (error instanceof Error ? error.message : String(error))
1854
+ state: runState,
1855
+ resultRef: reportPath,
1856
+ mergeStatus,
1857
+ ...runState === "failed" ? { failure: "merge " + report.stopReason } : {}
1172
1858
  });
1859
+ if (report.stopReason === "completed") return {
1860
+ ok: true,
1861
+ runId: handle.id,
1862
+ reportPath,
1863
+ report
1864
+ };
1173
1865
  return {
1174
1866
  ok: false,
1175
1867
  runId: handle.id,
1176
- message: "report write failed"
1868
+ message: "merge " + report.stopReason,
1869
+ reportPath
1177
1870
  };
1178
- }
1179
- const runState = report.stopReason === "completed" ? "succeeded" : report.stopReason === "cancelled" ? "cancelled" : "failed";
1180
- await options.registry.settle(handle.id, {
1181
- state: runState,
1182
- resultRef: reportPath,
1183
- ...runState === "failed" ? { failure: "merge " + report.stopReason } : {}
1184
- });
1185
- if (report.stopReason === "completed") return {
1186
- ok: true,
1187
- runId: handle.id,
1188
- reportPath,
1189
- report
1190
- };
1191
- return {
1192
- ok: false,
1193
- runId: handle.id,
1194
- message: "merge " + report.stopReason,
1195
- reportPath
1196
1871
  };
1872
+ if (options.detached === true) {
1873
+ finish().catch(() => {});
1874
+ return {
1875
+ ok: true,
1876
+ accepted: true,
1877
+ runId: handle.id
1878
+ };
1879
+ }
1880
+ return await finish();
1197
1881
  }
1198
1882
  //#endregion
1199
1883
  //#region lib/types/scan-command.js
@@ -1208,6 +1892,7 @@ async function runScanCommand(options) {
1208
1892
  mergeApproval: options.approval
1209
1893
  },
1210
1894
  now: options.now,
1895
+ ...options.detached === void 0 ? {} : { detached: options.detached },
1211
1896
  async body(ctx) {
1212
1897
  const ports = options.makePorts === void 0 ? options.ports : options.makePorts(ctx.runId);
1213
1898
  if (ports === void 0) throw new Error("scan requires ports or makePorts");
@@ -1287,7 +1972,17 @@ async function recordRound2Outcome(io, home, now, row, outcome) {
1287
1972
  if (transition(row, {
1288
1973
  type: "round2-success",
1289
1974
  at
1290
- }).ok) await dropRow(io, home, now, row.id);
1975
+ }).ok) {
1976
+ await dropRow(io, home, now, row.id);
1977
+ const target = outcome.target?.actual ?? outcome.target?.requested ?? row.decision?.target;
1978
+ await recordPair(io, home, {
1979
+ pairKey: row.pairKey,
1980
+ runId: row.runId ?? "merge",
1981
+ result: "merged",
1982
+ ...target === void 0 ? {} : { target },
1983
+ ...outcome.unitId === void 0 ? {} : { reportRef: outcome.unitId }
1984
+ });
1985
+ }
1291
1986
  return {
1292
1987
  merged: true,
1293
1988
  pair: {
@@ -1316,7 +2011,15 @@ async function recordRound2Outcome(io, home, now, row, outcome) {
1316
2011
  failure,
1317
2012
  at
1318
2013
  });
1319
- if (next.ok && next.row !== null) await upsertRow(io, home, now, next.row);
2014
+ if (next.ok && next.row !== null) {
2015
+ await upsertRow(io, home, now, next.row);
2016
+ await recordPair(io, home, {
2017
+ pairKey: row.pairKey,
2018
+ runId: row.runId ?? "merge",
2019
+ result: "failed",
2020
+ ...row.decision?.target === void 0 ? {} : { target: row.decision.target }
2021
+ });
2022
+ }
1320
2023
  return {
1321
2024
  merged: false,
1322
2025
  pair: {
@@ -1381,8 +2084,9 @@ async function runApproveCommand(options) {
1381
2084
  scope: {
1382
2085
  candidatePairs: 1,
1383
2086
  queueBefore: {
2087
+ verify: 0,
1384
2088
  pending: 0,
1385
- retry: 1,
2089
+ fuse: 1,
1386
2090
  skipped: 0
1387
2091
  }
1388
2092
  },
@@ -1442,7 +2146,7 @@ async function runResumeCommand(options) {
1442
2146
  } },
1443
2147
  ...options.timeoutMinutes === void 0 ? {} : { timeoutMinutes: options.timeoutMinutes }
1444
2148
  });
1445
- if (row.state !== "fuse") return {
2149
+ if (row.state !== "fuse" && row.state !== "approved") return {
1446
2150
  ok: false,
1447
2151
  message: "row is not resumable from state " + row.state
1448
2152
  };
@@ -1487,8 +2191,9 @@ async function runResumeCommand(options) {
1487
2191
  scope: {
1488
2192
  candidatePairs: 1,
1489
2193
  queueBefore: {
2194
+ verify: 0,
1490
2195
  pending: 0,
1491
- retry: 1,
2196
+ fuse: 1,
1492
2197
  skipped: 0
1493
2198
  }
1494
2199
  },
@@ -1497,136 +2202,17 @@ async function runResumeCommand(options) {
1497
2202
  merged: 1,
1498
2203
  kept: 0,
1499
2204
  pending: 0,
1500
- failed: 0
1501
- } : {
1502
- merged: 0,
1503
- kept: 0,
1504
- pending: 0,
1505
- failed: 1
1506
- },
1507
- notes: []
1508
- };
1509
- }
1510
- });
1511
- }
1512
- //#endregion
1513
- //#region lib/types/staging.js
1514
- /**
1515
- * Staging is a run ARTIFACT, not a queue: <home>/merge/runs/<runId>/.
1516
- *
1517
- * createStaging writes a sibling .partial tree first and renames it into place; the .stage
1518
- * marker is the last write inside the partial. A directory without that marker is a
1519
- * half-created artifact and reads as unknown, never as "empty".
1520
- */
1521
- const STAGING_SCHEMA_VERSION = 1;
1522
- function stagingRoot(home, runId) {
1523
- return join(home, "merge", "runs", runId);
1524
- }
1525
- /** Relative support paths only; the staging tree must never escape its run directory. */
1526
- function safeRelative$2(path) {
1527
- if (path === "" || path.startsWith("/") || path.startsWith("\\") || path.includes("\0")) return false;
1528
- return path.replace(/\\/g, "/").split("/").every((part) => part !== "" && part !== "." && part !== "..");
1529
- }
1530
- function manifestText(manifest) {
1531
- return JSON.stringify(manifest, null, 2) + "\n";
1532
- }
1533
- async function createStaging(io, home, input) {
1534
- const final = stagingRoot(home, input.runId);
1535
- if (await io.exists(final)) return {
1536
- ok: false,
1537
- message: "staging already exists for run " + input.runId
1538
- };
1539
- const names = /* @__PURE__ */ new Set();
1540
- for (const source of input.sources) {
1541
- if (names.has(source.name)) return {
1542
- ok: false,
1543
- message: "duplicate source " + source.name
1544
- };
1545
- names.add(source.name);
1546
- for (const file of source.files) if (!safeRelative$2(file.path)) return {
1547
- ok: false,
1548
- message: "unsafe support path: " + file.path
1549
- };
1550
- }
1551
- const partial = final + ".partial";
1552
- try {
1553
- await io.remove(partial);
1554
- for (const source of input.sources) {
1555
- const dir = join(partial, "sources", source.name);
1556
- await io.writeText(join(dir, "SKILL.md"), source.body);
1557
- for (const file of source.files) await io.writeText(join(dir, ...file.path.split("/")), file.content);
2205
+ failed: 0
2206
+ } : {
2207
+ merged: 0,
2208
+ kept: 0,
2209
+ pending: 0,
2210
+ failed: 1
2211
+ },
2212
+ notes: []
2213
+ };
1558
2214
  }
1559
- const at = input.now();
1560
- const manifest = {
1561
- schemaVersion: 1,
1562
- runId: input.runId,
1563
- target: input.target,
1564
- sources: input.sources.map((source) => source.name),
1565
- promptVersion: input.promptVersion,
1566
- createdAt: at,
1567
- expiresAt: at + Math.max(0, input.ttlDays) * 864e5
1568
- };
1569
- await io.writeText(join(partial, "manifest.json"), manifestText(manifest));
1570
- await io.writeText(join(partial, ".stage"), "");
1571
- await io.rename(partial, final);
1572
- return {
1573
- ok: true,
1574
- path: final,
1575
- manifest
1576
- };
1577
- } catch (error) {
1578
- try {
1579
- await io.remove(partial);
1580
- } catch {}
1581
- return {
1582
- ok: false,
1583
- message: "staging create failed: " + (error instanceof Error ? error.message : String(error))
1584
- };
1585
- }
1586
- }
1587
- async function readStaging(io, home, runId) {
1588
- const path = stagingRoot(home, runId);
1589
- if (!await io.exists(path)) return probeAbsent();
1590
- if (!await io.exists(join(path, ".stage"))) return probeUnknown("staging " + runId + " has no .stage marker (half-created)");
1591
- const probe = await probeText(io, join(path, "manifest.json"));
1592
- if (!isPresent(probe)) return probe;
1593
- try {
1594
- const parsed = JSON.parse(probe.value);
1595
- if (parsed.schemaVersion !== 1 || parsed.runId !== runId || typeof parsed.target !== "string" || !Array.isArray(parsed.sources)) return probeUnknown("staging manifest has an unexpected shape");
1596
- return probePresent({
1597
- path,
1598
- manifest: parsed
1599
- });
1600
- } catch (error) {
1601
- return probeUnknown("staging manifest is not JSON: " + (error instanceof Error ? error.message : String(error)));
1602
- }
1603
- }
1604
- //#endregion
1605
- //#region lib/types/commit-receipt.js
1606
- /**
1607
- * Commit receipt: <run>/commit.json, written immediately after a successful commit.
1608
- * It is the idempotency/recovery marker — merge resume and run start read it before
1609
- * replaying a commit. Receipt write failure must not turn a landed commit into a failure;
1610
- * the caller reports the warning instead.
1611
- */
1612
- function commitReceiptPath(home, runId) {
1613
- return join(stagingRoot(home, runId), "commit.json");
1614
- }
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;
1619
- }
1620
- async function readCommitReceipt(io, home, runId) {
1621
- const raw = await io.readText(commitReceiptPath(home, runId));
1622
- if (raw === null) return null;
1623
- try {
1624
- const parsed = JSON.parse(raw);
1625
- if (typeof parsed.runId !== "string" || typeof parsed.unitId !== "string" || typeof parsed.target !== "string" || typeof parsed.create !== "boolean" || !Array.isArray(parsed.sources) || !Array.isArray(parsed.archived) || typeof parsed.at !== "number") return null;
1626
- return parsed;
1627
- } catch {
1628
- return null;
1629
- }
2215
+ });
1630
2216
  }
1631
2217
  //#endregion
1632
2218
  //#region lib/types/undo-merge.js
@@ -1695,267 +2281,6 @@ function skillLibraryUndoPort(skills) {
1695
2281
  };
1696
2282
  }
1697
2283
  //#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
2284
  //#region lib/types/doctor.js
1960
2285
  /**
1961
2286
  * Merge doctor rows: state, pending, gate cache and staging are each probed read-only.
@@ -1963,7 +2288,7 @@ function hasGateCache(cache) {
1963
2288
  * operator can act on. This module never repairs or writes.
1964
2289
  */
1965
2290
  const MERGE_DOCTOR_SCHEMA_VERSION = 3;
1966
- async function mergeDoctor(io, home) {
2291
+ async function mergeDoctor(io, home, configuredRoot) {
1967
2292
  const rows = [];
1968
2293
  const loaded = await loadMergeState(io, home);
1969
2294
  const counts = countRows(loaded.state.rows);
@@ -1971,15 +2296,26 @@ async function mergeDoctor(io, home) {
1971
2296
  key: "state",
1972
2297
  label: "合并状态",
1973
2298
  status: loaded.note === void 0 ? "ok" : "warn",
1974
- detail: loaded.note ?? "pairs " + String(loaded.state.rows.length) + ";pending " + String(counts.pending) + " / retry " + String(counts.retry) + " / skipped " + String(counts.skipped)
2299
+ detail: loaded.note ?? "pairs " + String(loaded.state.rows.length) + ";verify " + String(counts.verify) + " / pending " + String(counts.pending) + " / fuse " + String(counts.fuse) + " / skipped " + String(counts.skipped)
1975
2300
  });
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) + " 条"
2301
+ const modelRoot = configuredRoot !== void 0 && configuredRoot !== "" ? configuredRoot : process.env.SKILL_MERGE_MODELS_ROOT;
2302
+ if (modelRoot === void 0 || modelRoot === "") rows.push({
2303
+ key: "models",
2304
+ label: "合并模型",
2305
+ status: "warn",
2306
+ detail: "未设置 SKILL_MERGE_MODELS_ROOT;合并扫描会 fail loud"
1982
2307
  });
2308
+ else {
2309
+ const int8 = join(modelRoot, "potion-multilingual-128M-int8");
2310
+ const pca = join(modelRoot, "potion-multilingual-128M-pca64-int8");
2311
+ const ok = await io.exists(int8) && await io.exists(pca);
2312
+ rows.push({
2313
+ key: "models",
2314
+ label: "合并模型",
2315
+ status: ok ? "ok" : "warn",
2316
+ detail: ok ? modelRoot : "缺少 int8/pca64:" + modelRoot
2317
+ });
2318
+ }
1983
2319
  const runsDir = join(home, "merge", "runs");
1984
2320
  const listed = await probeList(io, runsDir);
1985
2321
  rows.push({
@@ -2075,45 +2411,73 @@ function parseDecision(text, pair) {
2075
2411
  }
2076
2412
  //#endregion
2077
2413
  //#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.";
2414
+ const MERGE_PROMPT_VERSION = "merge-b/v1";
2415
+ const STANDBY_PROMPT = "You are a merge worker. Wait for instructions.";
2416
+ function sourcePath(name) {
2417
+ return "sources/" + name;
2418
+ }
2419
+ function mergedPath(target) {
2420
+ return "merged/" + target;
2421
+ }
2422
+ function wireDecision(decision) {
2423
+ return {
2424
+ shouldMerge: decision.shouldMerge,
2425
+ mode: decision.mode,
2426
+ target: decision.target,
2427
+ sources: decision.sources,
2428
+ rationale: decision.rationale,
2429
+ confidence: decision.confidence,
2430
+ a_summary: decision.aSummary,
2431
+ b_summary: decision.bSummary
2432
+ };
2433
+ }
2081
2434
  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
2435
  return [
2086
- ROUND1_SYSTEM,
2436
+ "你是技能合并决策 agent。第 1 轮只做判断,不要写文件、不要修改任何源目录。",
2087
2437
  "",
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\".",
2438
+ "在当前工作目录下用 merge_stage 的 list/read 完整阅读两个源技能:",
2439
+ "- A: " + sourcePath(candidate.a) + "(name=" + candidate.a + ")",
2440
+ "- B: " + sourcePath(candidate.b) + "(name=" + candidate.b + ")",
2441
+ "包含 SKILL.md 与 references/templates/scripts 等支撑文件。",
2090
2442
  "",
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
- })
2443
+ "判断规则:",
2444
+ "1) 两者是否语义等价/互补到值得合并,而不是词面相似;",
2445
+ "2) 合并后是否更容易发现和使用,而不是变成更大的巨技能;",
2446
+ "3) 若合并会破坏触发面/生命周期/可发现性,应输出 keep。",
2447
+ "",
2448
+ "只输出一个 JSON(不要 markdown 围栏):",
2449
+ "{\"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
2450
  ].join("\n");
2102
2451
  }
2103
- function buildRound2Prompt(row, decision) {
2104
- return [
2105
- ROUND2_SYSTEM,
2452
+ function buildRound2Prompt(row, decision, mode = "continue") {
2453
+ const decisionJson = JSON.stringify(wireDecision(decision));
2454
+ const head = [
2455
+ "现在执行第 2 轮:把两个技能融合成一个完整、可用的技能,直接在当前工作目录的 merged/ 下写文件。",
2456
+ "不要修改 " + sourcePath(row.a) + " 或 " + sourcePath(row.b) + "(只读源副本);不要访问工作目录以外的路径。",
2457
+ ""
2458
+ ];
2459
+ const body = [
2460
+ "决策结果:" + decisionJson,
2461
+ "目标技能名:" + decision.target,
2106
2462
  "",
2107
- "decision: " + JSON.stringify(decision),
2108
- "pair: " + JSON.stringify({
2109
- a: row.a,
2110
- b: row.b
2111
- }),
2112
- "target: " + JSON.stringify(decision.target),
2463
+ "工作方式:",
2464
+ "- 用 merge_stage 的 list/read/write/copy 创建 " + mergedPath(decision.target) + "/SKILL.md 与需要的 references/templates/scripts;",
2465
+ "- 可以复制源文件作为草稿基础,然后逐段修改;可以多轮读写;",
2466
+ "- 保留两源独特内容,不编造事实,不留下 TODO/占位符;",
2467
+ "- SKILL.md 以合法 YAML frontmatter 开头,name 等于目标名,description 不超过 60 字符;",
2468
+ "- 正文与支撑文件的引用必须成对,路径只允许 references/templates/scripts/assets 下的相对路径。",
2113
2469
  "",
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");
2470
+ "完成后只输出一个 JSON(不要 markdown 围栏):",
2471
+ "{\"target\":\"" + decision.target + "\",\"files\":[\"SKILL.md\",\"references/...\"],\"changelog\":[\"一条变更\"],\"notes\":\"可选的一句话\"}"
2472
+ ];
2473
+ return (mode === "fresh" ? [
2474
+ "基于已持久化的 decision。",
2475
+ ...head,
2476
+ ...body
2477
+ ] : [...head, ...body]).join("\n");
2478
+ }
2479
+ function buildStandbyPrompt() {
2480
+ return STANDBY_PROMPT;
2117
2481
  }
2118
2482
  //#endregion
2119
2483
  //#region lib/types/channel-port.js
@@ -2131,7 +2495,7 @@ async function runRound1OverChannel(options, candidate, signal) {
2131
2495
  const opened = await options.channel.open({
2132
2496
  parent: options.parent,
2133
2497
  label: "merge-round1",
2134
- promptText: buildRound1Prompt(candidate, options.cards),
2498
+ promptText: STANDBY_PROMPT,
2135
2499
  kind: "reviewer",
2136
2500
  allow: options.allow,
2137
2501
  ...options.cwd === void 0 ? {} : { cwd: options.cwd },
@@ -2173,6 +2537,60 @@ async function runRound1OverChannel(options, candidate, signal) {
2173
2537
  failure: {
2174
2538
  stage: "decide",
2175
2539
  code: opened.turn.stopReason,
2540
+ message: "standby did not complete",
2541
+ retryable: true,
2542
+ retryFrom: "agent1"
2543
+ }
2544
+ },
2545
+ dispose
2546
+ };
2547
+ }
2548
+ try {
2549
+ options.onChildOpened?.(opened.childId);
2550
+ } catch (error) {
2551
+ await dispose();
2552
+ return {
2553
+ outcome: {
2554
+ kind: "failed",
2555
+ failure: {
2556
+ stage: "decide",
2557
+ code: "child-bind",
2558
+ message: String(error),
2559
+ retryable: true,
2560
+ retryFrom: "agent1"
2561
+ }
2562
+ },
2563
+ dispose
2564
+ };
2565
+ }
2566
+ const round1 = await options.channel.continueTurn(opened.childId, buildRound1Prompt(candidate, options.cards), {
2567
+ timeoutMs: options.timeoutMs,
2568
+ signal
2569
+ });
2570
+ if (round1.kind !== "ok") {
2571
+ await dispose();
2572
+ return {
2573
+ outcome: {
2574
+ kind: "failed",
2575
+ failure: {
2576
+ stage: "decide",
2577
+ code: round1.kind,
2578
+ message: round1.kind === "unavailable" ? round1.reason : "round1 aborted",
2579
+ retryable: round1.kind !== "aborted",
2580
+ retryFrom: round1.kind === "aborted" ? "none" : "agent1"
2581
+ }
2582
+ },
2583
+ dispose
2584
+ };
2585
+ }
2586
+ if (round1.turn.stopReason !== "completed") {
2587
+ await dispose();
2588
+ return {
2589
+ outcome: {
2590
+ kind: "failed",
2591
+ failure: {
2592
+ stage: "decide",
2593
+ code: round1.turn.stopReason,
2176
2594
  message: "round1 did not complete",
2177
2595
  retryable: true,
2178
2596
  retryFrom: "agent1"
@@ -2181,7 +2599,7 @@ async function runRound1OverChannel(options, candidate, signal) {
2181
2599
  dispose
2182
2600
  };
2183
2601
  }
2184
- const parsed = parseDecision(opened.turn.text, candidate);
2602
+ const parsed = parseDecision(round1.turn.text, candidate);
2185
2603
  if (!parsed.ok) {
2186
2604
  await dispose();
2187
2605
  return {
@@ -2395,9 +2813,10 @@ function buildCommitPlan(input) {
2395
2813
  ok: false,
2396
2814
  message: "create flag disagrees with the live target"
2397
2815
  };
2398
- if (input.sourceBodies.length === 0) return {
2816
+ const sources = input.sourceBodies.filter((source) => source.name !== input.target);
2817
+ if (sources.length === 0) return {
2399
2818
  ok: false,
2400
- message: "a merge needs at least one source"
2819
+ message: "a merge needs at least one source different from the target"
2401
2820
  };
2402
2821
  const body = rewriteFrontmatterName(input.mergedBody, input.target);
2403
2822
  return {
@@ -2405,11 +2824,11 @@ function buildCommitPlan(input) {
2405
2824
  plan: {
2406
2825
  target: input.target,
2407
2826
  targetExists: !input.create,
2408
- sources: input.sourceBodies.map((source) => source.name),
2827
+ sources: sources.map((source) => source.name),
2409
2828
  body,
2410
2829
  supportFiles: input.mergedFiles,
2411
2830
  expectedTargetBody: input.targetCurrent,
2412
- expectedSourceBodies: input.sourceBodies,
2831
+ expectedSourceBodies: sources,
2413
2832
  origin: input.origin,
2414
2833
  summary: input.summary
2415
2834
  }
@@ -2428,7 +2847,25 @@ async function assembleStagedCommit(io, home, runId, port, options) {
2428
2847
  ok: false,
2429
2848
  message: "merged tree is not readable for run " + runId
2430
2849
  };
2431
- const targetCurrent = await port.readSkill(staging.value.manifest.target);
2850
+ const requested = staging.value.manifest.target;
2851
+ let target = requested;
2852
+ let targetCurrent = await port.readSkill(requested);
2853
+ const requestedArchived = targetCurrent === null && await port.isArchived?.(requested) === true;
2854
+ if (staging.value.manifest.mode === "create-umbrella" && (targetCurrent !== null || requestedArchived)) {
2855
+ target = requested;
2856
+ for (let suffix = 2; suffix <= 99; suffix += 1) {
2857
+ const candidate = requested + "-v" + String(suffix);
2858
+ if (await port.readSkill(candidate) === null && await port.isArchived?.(candidate) !== true) {
2859
+ target = candidate;
2860
+ targetCurrent = null;
2861
+ break;
2862
+ }
2863
+ }
2864
+ if (target === requested) return {
2865
+ ok: false,
2866
+ message: "no free umbrella target for " + requested
2867
+ };
2868
+ }
2432
2869
  const sources = [];
2433
2870
  for (const name of staging.value.manifest.sources) {
2434
2871
  const body = await port.readSkill(name);
@@ -2442,7 +2879,7 @@ async function assembleStagedCommit(io, home, runId, port, options) {
2442
2879
  });
2443
2880
  }
2444
2881
  return buildCommitPlan({
2445
- target: staging.value.manifest.target,
2882
+ target,
2446
2883
  create: targetCurrent === null,
2447
2884
  mergedBody: tree.value.body,
2448
2885
  mergedFiles: tree.value.files,
@@ -2453,6 +2890,17 @@ async function assembleStagedCommit(io, home, runId, port, options) {
2453
2890
  });
2454
2891
  }
2455
2892
  //#endregion
2893
+ //#region lib/types/domain/commit-journal-store.js
2894
+ /** Durable commit journal: <home>/merge/journals/<run>.json. */
2895
+ function journalPath(home, runId) {
2896
+ return join(home, "merge", "journals", runId.replace(/[^A-Za-z0-9_-]+/g, "_") + ".json");
2897
+ }
2898
+ async function writeJournal(io, home, journal) {
2899
+ const path = journalPath(home, journal.runId);
2900
+ await io.writeText(path, JSON.stringify(journal, null, 2));
2901
+ return path;
2902
+ }
2903
+ //#endregion
2456
2904
  //#region lib/types/commit-execute.js
2457
2905
  async function commitStagedRun(io, home, runId, port, options) {
2458
2906
  const assembled = await assembleStagedCommit(io, home, runId, port, {
@@ -2463,12 +2911,32 @@ async function commitStagedRun(io, home, runId, port, options) {
2463
2911
  ok: false,
2464
2912
  message: assembled.message
2465
2913
  };
2466
- const result = await port.commitMerge(assembled.plan);
2467
- if (!result.ok) return {
2468
- ok: false,
2469
- message: result.message,
2470
- plan: assembled.plan
2914
+ const journal = {
2915
+ runId,
2916
+ target: assembled.plan.target,
2917
+ create: !assembled.plan.targetExists,
2918
+ sources: assembled.plan.sources,
2919
+ phase: "prepared",
2920
+ updatedAt: new Date(options.now()).toISOString()
2471
2921
  };
2922
+ try {
2923
+ await writeJournal(io, home, journal);
2924
+ } catch {}
2925
+ const result = await port.commitMerge(assembled.plan);
2926
+ if (!result.ok) {
2927
+ try {
2928
+ await writeJournal(io, home, {
2929
+ ...journal,
2930
+ phase: "failed",
2931
+ updatedAt: new Date(options.now()).toISOString()
2932
+ });
2933
+ } catch {}
2934
+ return {
2935
+ ok: false,
2936
+ message: result.message,
2937
+ plan: assembled.plan
2938
+ };
2939
+ }
2472
2940
  if (result.unitId === void 0) return {
2473
2941
  ok: true,
2474
2942
  plan: assembled.plan,
@@ -2477,15 +2945,25 @@ async function commitStagedRun(io, home, runId, port, options) {
2477
2945
  };
2478
2946
  const receipt = {
2479
2947
  runId,
2948
+ parentRunId: runId.split("/pairs/")[0] ?? runId,
2480
2949
  unitId: result.unitId,
2481
2950
  target: assembled.plan.target,
2482
2951
  create: !assembled.plan.targetExists,
2483
2952
  sources: assembled.plan.sources,
2953
+ ...assembled.plan.sources[0] === void 0 ? {} : { pairKey: pairKey(assembled.plan.target, assembled.plan.sources[0]) },
2484
2954
  archived: result.archived ?? [],
2485
2955
  at: options.now()
2486
2956
  };
2487
2957
  try {
2488
2958
  await writeCommitReceipt(io, home, receipt);
2959
+ try {
2960
+ await writeJournal(io, home, {
2961
+ ...journal,
2962
+ unitId: result.unitId,
2963
+ phase: "committed",
2964
+ updatedAt: new Date(options.now()).toISOString()
2965
+ });
2966
+ } catch {}
2489
2967
  return {
2490
2968
  ok: true,
2491
2969
  plan: assembled.plan,
@@ -2504,26 +2982,40 @@ async function commitStagedRun(io, home, runId, port, options) {
2504
2982
  //#endregion
2505
2983
  //#region lib/types/authoring-flow.js
2506
2984
  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 {
2985
+ const targetCard = options.cards.find((card) => card.name === options.target);
2986
+ if (targetCard?.protectionUnknown === true || targetCard?.protectedBy === "pinned") return {
2520
2987
  ok: false,
2521
- message: created.message
2988
+ message: "target is pinned: " + options.target
2522
2989
  };
2990
+ const stageKey = options.stageKey ?? options.runId;
2991
+ if (options.reuseStaging === true) {
2992
+ if (!await setStagingTarget(options.io, options.home, stageKey, options.target, options.mode)) return {
2993
+ ok: false,
2994
+ message: "staging missing or unreadable: " + stageKey
2995
+ };
2996
+ } else {
2997
+ const created = await createStaging(options.io, options.home, {
2998
+ runId: stageKey,
2999
+ target: options.target,
3000
+ ...options.mode === void 0 ? {} : { mode: options.mode },
3001
+ sources: options.cards.map((card) => ({
3002
+ name: card.name,
3003
+ body: card.body,
3004
+ files: card.files ?? []
3005
+ })),
3006
+ promptVersion: "merge-b/v1",
3007
+ ttlDays: 7,
3008
+ now: options.now
3009
+ });
3010
+ if (!created.ok) return {
3011
+ ok: false,
3012
+ message: created.message
3013
+ };
3014
+ }
2523
3015
  try {
2524
3016
  await options.author({
2525
3017
  home: options.home,
2526
- runId: options.runId
3018
+ runId: stageKey
2527
3019
  });
2528
3020
  } catch (error) {
2529
3021
  return {
@@ -2531,7 +3023,7 @@ async function runAuthoring(options) {
2531
3023
  message: "author failed: " + (error instanceof Error ? error.message : String(error))
2532
3024
  };
2533
3025
  }
2534
- const tree = await readStagedTree(options.io, options.home, options.runId);
3026
+ const tree = await readStagedTree(options.io, options.home, stageKey);
2535
3027
  if (tree.kind !== "present") return {
2536
3028
  ok: false,
2537
3029
  message: "staged tree is not readable: " + (tree.kind === "unknown" ? tree.reason : "absent")
@@ -2542,7 +3034,7 @@ async function runAuthoring(options) {
2542
3034
  message: "staged tree failed the rule gate",
2543
3035
  findings
2544
3036
  };
2545
- const commit = await commitStagedRun(options.io, options.home, options.runId, options.commitPort, {
3037
+ const commit = await commitStagedRun(options.io, options.home, stageKey, options.commitPort, {
2546
3038
  origin: "subagent",
2547
3039
  summary: options.summary,
2548
3040
  now: options.now
@@ -2564,30 +3056,67 @@ function createChannelScanPorts(options) {
2564
3056
  return {
2565
3057
  screening: options.screening,
2566
3058
  async round1(candidate, signal) {
3059
+ const stageKey = stageKeyOf(options.runId, pairKey(candidate.a, candidate.b));
3060
+ const staged = await ensureStaging(options.io, options.home, {
3061
+ runId: stageKey,
3062
+ target: candidate.a + "-" + candidate.b,
3063
+ sources: options.cards.filter((card) => card.name === candidate.a || card.name === candidate.b).map((card) => ({
3064
+ name: card.name,
3065
+ body: card.body,
3066
+ files: card.files ?? []
3067
+ })),
3068
+ promptVersion: "merge-b/v1",
3069
+ ttlDays: 7,
3070
+ now: options.now
3071
+ });
3072
+ if (!staged.ok) return {
3073
+ kind: "failed",
3074
+ failure: {
3075
+ stage: "decide",
3076
+ code: "staging-create",
3077
+ message: staged.message,
3078
+ retryable: true,
3079
+ retryFrom: "agent1"
3080
+ }
3081
+ };
2567
3082
  const result = await runRound1OverChannel({
2568
3083
  channel: options.channel,
2569
3084
  parent: options.parent,
2570
3085
  cards: options.cards,
2571
3086
  allow: options.allow,
2572
- ...options.cwd === void 0 ? {} : { cwd: options.cwd },
2573
- timeoutMs: options.timeoutMs
3087
+ cwd: stagingRoot(options.home, stageKey),
3088
+ timeoutMs: options.timeoutMs,
3089
+ onChildOpened: (childId) => options.onChild?.({
3090
+ runId: stageKey,
3091
+ childId,
3092
+ expectedChildId: childId,
3093
+ phase: "decide"
3094
+ })
2574
3095
  }, candidate, signal);
2575
- if (result.outcome.kind === "merge") if (options.approval) await result.dispose();
3096
+ if (result.outcome.kind !== "merge") await dropStaging(options.io, options.home, stageKey);
3097
+ else if (options.approval) await result.dispose();
2576
3098
  else {
2577
- sessions.set(pairKey(candidate.a, candidate.b), result);
3099
+ sessions.set(pairKey(candidate.a, candidate.b), {
3100
+ result,
3101
+ stageKey
3102
+ });
2578
3103
  if (result.childId !== void 0) options.onChild?.({
2579
- runId: options.runId,
3104
+ runId: stageKey,
2580
3105
  childId: result.childId,
2581
3106
  expectedChildId: result.childId,
2582
3107
  phase: "author"
2583
3108
  });
2584
3109
  }
2585
- return result.outcome;
3110
+ return result.outcome.kind === "merge" ? {
3111
+ ...result.outcome,
3112
+ stagingRef: stageKey,
3113
+ stagingExpiresAt: options.now() + 7 * 864e5
3114
+ } : result.outcome;
2586
3115
  },
2587
3116
  async round2(row, decision, signal) {
2588
3117
  const key = pairKey(row.a, row.b);
2589
- const session = sessions.get(key);
2590
- if (session === void 0) return {
3118
+ const entry = sessions.get(key);
3119
+ if (entry === void 0) return {
2591
3120
  kind: "failed",
2592
3121
  failure: {
2593
3122
  stage: "author",
@@ -2603,13 +3132,16 @@ function createChannelScanPorts(options) {
2603
3132
  io: options.io,
2604
3133
  home: options.home,
2605
3134
  runId: options.runId,
3135
+ stageKey: entry.stageKey,
3136
+ reuseStaging: true,
2606
3137
  target: decision.target,
3138
+ mode: decision.mode,
2607
3139
  cards: options.cards,
2608
3140
  commitPort: options.commitPort,
2609
3141
  now: options.now,
2610
3142
  summary: "merge " + row.a + " + " + row.b,
2611
3143
  author: async () => {
2612
- await session.author?.(row, decision, signal);
3144
+ await entry.result.author?.(row, decision, signal);
2613
3145
  return "authored";
2614
3146
  }
2615
3147
  });
@@ -2623,20 +3155,20 @@ function createChannelScanPorts(options) {
2623
3155
  retryFrom: "agent2"
2624
3156
  }
2625
3157
  };
2626
- const receipt = authored.commit.ok ? authored.commit.receipt : void 0;
3158
+ const receipt = authored.commit.receipt;
2627
3159
  return {
2628
3160
  kind: "merged",
2629
3161
  ...receipt?.unitId === void 0 ? {} : { unitId: receipt.unitId },
2630
3162
  archived: receipt?.archived ?? [],
2631
3163
  target: {
2632
3164
  requested: decision.target,
2633
- actual: decision.target,
3165
+ actual: authored.commit.plan.target,
2634
3166
  created: receipt?.create ?? false
2635
3167
  }
2636
3168
  };
2637
3169
  } finally {
2638
- await session.dispose();
2639
- if (session.childId !== void 0) options.onChildClosed?.(session.childId);
3170
+ await entry.result.dispose();
3171
+ if (entry.result.childId !== void 0) options.onChildClosed?.(entry.result.childId);
2640
3172
  }
2641
3173
  }
2642
3174
  };
@@ -2644,35 +3176,51 @@ function createChannelScanPorts(options) {
2644
3176
  //#endregion
2645
3177
  //#region lib/types/new-child-authoring.js
2646
3178
  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 {
3179
+ const targetCard = options.cards.find((card) => card.name === decision.target);
3180
+ if (targetCard?.protectionUnknown === true || targetCard?.protectedBy === "pinned") return {
2660
3181
  kind: "failed",
2661
3182
  failure: {
2662
3183
  stage: "author",
2663
- code: "staging-create",
2664
- message: staging.message,
2665
- retryable: true,
2666
- retryFrom: "agent2"
3184
+ code: "target-pinned",
3185
+ message: "target is pinned: " + decision.target,
3186
+ retryable: false,
3187
+ retryFrom: "none"
2667
3188
  }
2668
3189
  };
3190
+ const stageKey = row.stagingRef ?? stageKeyOf(options.runId, pairKey(row.a, row.b));
3191
+ if ((await readStaging(options.io, options.home, stageKey)).kind === "present") await setStagingTarget(options.io, options.home, stageKey, decision.target, decision.mode);
3192
+ else {
3193
+ const staging = await createStaging(options.io, options.home, {
3194
+ runId: stageKey,
3195
+ target: decision.target,
3196
+ mode: decision.mode,
3197
+ sources: options.cards.filter((card) => card.name === row.a || card.name === row.b).map((card) => ({
3198
+ name: card.name,
3199
+ body: card.body,
3200
+ files: card.files ?? []
3201
+ })),
3202
+ promptVersion: "merge-b/v1",
3203
+ ttlDays: 7,
3204
+ now: options.now
3205
+ });
3206
+ if (!staging.ok) return {
3207
+ kind: "failed",
3208
+ failure: {
3209
+ stage: "author",
3210
+ code: "staging-create",
3211
+ message: staging.message,
3212
+ retryable: true,
3213
+ retryFrom: "agent2"
3214
+ }
3215
+ };
3216
+ }
2669
3217
  const opened = await options.channel.open({
2670
3218
  parent: options.parent,
2671
3219
  label: "merge-round2",
2672
- promptText: buildRound2Prompt(row, decision),
3220
+ promptText: STANDBY_PROMPT,
2673
3221
  kind: "maintainer",
2674
3222
  allow: options.allow,
2675
- ...options.cwd === void 0 ? {} : { cwd: options.cwd },
3223
+ cwd: stagingRoot(options.home, stageKey),
2676
3224
  timeoutMs: options.timeoutMs,
2677
3225
  signal
2678
3226
  });
@@ -2687,7 +3235,7 @@ async function runNewChildAuthoring(options, row, decision, signal) {
2687
3235
  }
2688
3236
  };
2689
3237
  options.onChild?.({
2690
- runId: options.runId,
3238
+ runId: stageKey,
2691
3239
  childId: opened.childId,
2692
3240
  expectedChildId: opened.childId,
2693
3241
  phase: "author"
@@ -2698,12 +3246,36 @@ async function runNewChildAuthoring(options, row, decision, signal) {
2698
3246
  failure: {
2699
3247
  stage: "author",
2700
3248
  code: opened.turn.stopReason,
3249
+ message: "standby did not complete",
3250
+ retryable: true,
3251
+ retryFrom: "agent2"
3252
+ }
3253
+ };
3254
+ const turn = await options.channel.continueTurn(opened.childId, buildRound2Prompt(row, decision, "fresh"), {
3255
+ timeoutMs: options.timeoutMs,
3256
+ signal
3257
+ });
3258
+ if (turn.kind !== "ok") return {
3259
+ kind: "failed",
3260
+ failure: {
3261
+ stage: "author",
3262
+ code: turn.kind,
3263
+ message: turn.kind === "unavailable" ? turn.reason : "round2 aborted",
3264
+ retryable: turn.kind !== "aborted",
3265
+ retryFrom: turn.kind === "aborted" ? "none" : "agent2"
3266
+ }
3267
+ };
3268
+ if (turn.turn.stopReason !== "completed") return {
3269
+ kind: "failed",
3270
+ failure: {
3271
+ stage: "author",
3272
+ code: turn.turn.stopReason,
2701
3273
  message: "round2 did not complete",
2702
3274
  retryable: true,
2703
3275
  retryFrom: "agent2"
2704
3276
  }
2705
3277
  };
2706
- const tree = await readStagedTree(options.io, options.home, options.runId);
3278
+ const tree = await readStagedTree(options.io, options.home, stageKey);
2707
3279
  if (tree.kind !== "present") return {
2708
3280
  kind: "failed",
2709
3281
  failure: {
@@ -2724,7 +3296,7 @@ async function runNewChildAuthoring(options, row, decision, signal) {
2724
3296
  retryFrom: "agent2"
2725
3297
  }
2726
3298
  };
2727
- const commit = await commitStagedRun(options.io, options.home, options.runId, options.commitPort, {
3299
+ const commit = await commitStagedRun(options.io, options.home, stageKey, options.commitPort, {
2728
3300
  origin: "subagent",
2729
3301
  summary: "merge " + row.a + " + " + row.b,
2730
3302
  now: options.now
@@ -2746,7 +3318,7 @@ async function runNewChildAuthoring(options, row, decision, signal) {
2746
3318
  archived: receipt?.archived ?? [],
2747
3319
  target: {
2748
3320
  requested: decision.target,
2749
- actual: decision.target,
3321
+ actual: commit.plan.target,
2750
3322
  created: receipt?.create ?? false
2751
3323
  }
2752
3324
  };
@@ -2766,8 +3338,14 @@ function createNewChildRound2(options) {
2766
3338
  //#endregion
2767
3339
  //#region lib/types/adapters/library.js
2768
3340
  function skillLibraryPort(skills) {
3341
+ const archivedNames = skills.archivedNames;
2769
3342
  return {
2770
3343
  readSkill: (name) => skills.read(name),
3344
+ isArchived: async (name) => {
3345
+ if (typeof archivedNames !== "function") return false;
3346
+ const names = await archivedNames.call(skills);
3347
+ return names === null ? true : names.includes(name);
3348
+ },
2771
3349
  commitMerge: (plan) => skills.commitMerge(plan)
2772
3350
  };
2773
3351
  }
@@ -2800,6 +3378,7 @@ function createMergeRuntime(deps) {
2800
3378
  };
2801
3379
  return { async execute(command, context) {
2802
3380
  const parent = context?.parent ?? deps.parent;
3381
+ const detached = deps.detached !== false;
2803
3382
  if (command.verb === "pair") {
2804
3383
  const signal = new AbortController().signal;
2805
3384
  const cards = await deps.cards(signal);
@@ -2807,48 +3386,86 @@ function createMergeRuntime(deps) {
2807
3386
  a: command.a,
2808
3387
  b: command.b
2809
3388
  }];
3389
+ let forced;
3390
+ if (command.force === true) {
3391
+ const key = pairKey(command.a, command.b);
3392
+ forced = (await loadMergeState(deps.io, deps.home)).state.rows.find((row) => row.pairKey === key);
3393
+ if (forced === void 0 || forced.decision === void 0) return {
3394
+ ok: false,
3395
+ text: "pair --force 需要已有决策,找不到该 pair 的 decision: " + key
3396
+ };
3397
+ }
3398
+ const approval = command.force === true ? false : deps.params.mergeApproval;
2810
3399
  const result = await runScanCommand({
2811
3400
  ...common,
2812
- approval: deps.params.mergeApproval,
2813
- makePorts: (runId) => createChannelScanPorts({
2814
- io: deps.io,
2815
- home: deps.home,
2816
- runId,
2817
- cards,
2818
- screening: () => Promise.resolve(pair),
2819
- channel: deps.channel,
2820
- parent,
2821
- allow: deps.allow,
2822
- ...deps.cwd === void 0 ? {} : { cwd: deps.cwd },
2823
- timeoutMs: deps.params.mergePairTimeoutMinutes * 6e4,
2824
- approval: deps.params.mergeApproval,
2825
- commitPort: skillLibraryPort(deps.skills),
2826
- now: deps.now,
2827
- ...stageHooks
2828
- })
3401
+ approval,
3402
+ ...detached ? { detached: true } : {},
3403
+ makePorts: (runId) => {
3404
+ const ports = createChannelScanPorts({
3405
+ io: deps.io,
3406
+ home: deps.home,
3407
+ runId,
3408
+ cards,
3409
+ screening: () => Promise.resolve(pair),
3410
+ channel: deps.channel,
3411
+ parent,
3412
+ allow: deps.allow,
3413
+ ...deps.cwd === void 0 ? {} : { cwd: deps.cwd },
3414
+ timeoutMs: deps.params.mergePairTimeoutMinutes * 6e4,
3415
+ approval,
3416
+ commitPort: skillLibraryPort(deps.skills),
3417
+ now: deps.now,
3418
+ ...stageHooks
3419
+ });
3420
+ return forced === void 0 ? ports : {
3421
+ ...ports,
3422
+ round1: () => Promise.resolve({
3423
+ kind: "merge",
3424
+ decision: forced.decision
3425
+ })
3426
+ };
3427
+ }
2829
3428
  });
2830
- return result.ok ? {
2831
- ok: true,
2832
- text: "merge pair 完成:merged " + String(result.report.totals.merged) + " / pending " + String(result.report.totals.pending) + " / failed " + String(result.report.totals.failed)
2833
- } : {
3429
+ if (!result.ok) return {
2834
3430
  ok: false,
2835
3431
  text: result.message
2836
3432
  };
3433
+ if ("accepted" in result) return {
3434
+ ok: true,
3435
+ text: "merge pair 已启动:" + result.runId
3436
+ };
3437
+ if (!("report" in result)) return {
3438
+ ok: false,
3439
+ text: "merge pair started without a report"
3440
+ };
3441
+ return {
3442
+ ok: true,
3443
+ text: "merge pair 完成:merged " + String(result.report.totals.merged) + " / pending " + String(result.report.totals.pending) + " / failed " + String(result.report.totals.failed)
3444
+ };
2837
3445
  }
2838
3446
  if (command.verb === "scan") {
2839
3447
  const signal = new AbortController().signal;
2840
3448
  const cards = await deps.cards(signal);
2841
- const skipped = skippedPairKeys((await loadMergeState(deps.io, deps.home)).state.rows, deps.params.mergeSkippedTtlDays, deps.now());
3449
+ const loadedState = await loadMergeState(deps.io, deps.home);
3450
+ const skipped = skippedPairKeys(loadedState.state.rows, deps.params.mergeSkippedTtlDays, deps.now());
3451
+ const held = new Set(loadedState.state.rows.filter((row) => row.queue === "pending" || !canRetry(row)).map((row) => row.pairKey));
2842
3452
  const result = await runScanCommand({
2843
3453
  ...common,
2844
3454
  approval: deps.params.mergeApproval,
3455
+ ...detached ? { detached: true } : {},
2845
3456
  makePorts: (runId) => createChannelScanPorts({
2846
3457
  io: deps.io,
2847
3458
  home: deps.home,
2848
3459
  runId,
2849
3460
  cards,
2850
3461
  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)));
3462
+ const raw = await deps.screeningForRun(cards, runId, turnSignal);
3463
+ const filtered = raw.filter((candidate) => {
3464
+ const key = pairKey(candidate.a, candidate.b);
3465
+ return !skipped.has(key) && !held.has(key);
3466
+ });
3467
+ const candidatePairs = raw.candidatePairs;
3468
+ return candidatePairs === void 0 ? filtered : Object.assign(filtered, { candidatePairs });
2852
3469
  },
2853
3470
  channel: deps.channel,
2854
3471
  parent,
@@ -2861,13 +3478,22 @@ function createMergeRuntime(deps) {
2861
3478
  ...stageHooks
2862
3479
  })
2863
3480
  });
2864
- return result.ok ? {
2865
- ok: true,
2866
- text: "merge scan 完成:merged " + String(result.report.totals.merged) + " / pending " + String(result.report.totals.pending) + " / failed " + String(result.report.totals.failed)
2867
- } : {
3481
+ if (!result.ok) return {
2868
3482
  ok: false,
2869
3483
  text: result.message
2870
3484
  };
3485
+ if ("accepted" in result) return {
3486
+ ok: true,
3487
+ text: "merge scan 已启动:" + result.runId
3488
+ };
3489
+ if (!("report" in result)) return {
3490
+ ok: false,
3491
+ text: "merge scan started without a report"
3492
+ };
3493
+ return {
3494
+ ok: true,
3495
+ text: "merge scan 完成:merged " + String(result.report.totals.merged) + " / pending " + String(result.report.totals.pending) + " / failed " + String(result.report.totals.failed)
3496
+ };
2871
3497
  }
2872
3498
  if (command.verb === "reject") {
2873
3499
  const row = (await loadMergeState(deps.io, deps.home)).state.rows.find((item) => item.id === command.id);
@@ -2891,7 +3517,7 @@ function createMergeRuntime(deps) {
2891
3517
  }
2892
3518
  if (command.verb === "doctor") return {
2893
3519
  ok: true,
2894
- text: (await mergeDoctor(deps.io, deps.home)).map((row) => row.key + " [" + row.status + "] " + row.label + ":" + row.detail).join("\n")
3520
+ text: (await mergeDoctor(deps.io, deps.home, deps.params.mergeModelsRoot)).map((row) => row.key + " [" + row.status + "] " + row.label + ":" + row.detail).join("\n")
2895
3521
  };
2896
3522
  if (command.verb === "report" || command.verb === "runs" || command.verb === "cancel") {
2897
3523
  const registry = deps.registry;
@@ -2938,8 +3564,24 @@ function createMergeRuntime(deps) {
2938
3564
  text: report.value.slice(0, 12e3)
2939
3565
  };
2940
3566
  }
3567
+ if (command.verb === "discard") {
3568
+ const row = (await loadMergeState(deps.io, deps.home)).state.rows.find((item) => item.id === command.id);
3569
+ if (row === void 0) return {
3570
+ ok: false,
3571
+ text: "merge row not found: " + command.id
3572
+ };
3573
+ if (row.state !== "pending" && row.state !== "approved" && row.state !== "fuse") return {
3574
+ ok: false,
3575
+ text: "merge row is not discardable: " + command.id
3576
+ };
3577
+ await dropRow(deps.io, deps.home, deps.now, row.id);
3578
+ return {
3579
+ ok: true,
3580
+ text: "已丢弃合并行:" + command.id
3581
+ };
3582
+ }
2941
3583
  if (command.verb === "undo") {
2942
- const receipt = await readCommitReceipt(deps.io, deps.home, command.run);
3584
+ const receipt = await readCommitReceipt(deps.io, deps.home, command.run, command.target);
2943
3585
  if (receipt === null) return {
2944
3586
  ok: false,
2945
3587
  text: "merge run has no commit receipt: " + command.run
@@ -2949,6 +3591,12 @@ function createMergeRuntime(deps) {
2949
3591
  text: "receipt target is " + receipt.target + ", not " + command.target
2950
3592
  };
2951
3593
  const result = await undoMergeReceipt(receipt, skillLibraryUndoPort(deps.skills));
3594
+ if (result.ok && receipt.pairKey !== void 0) await recordPair(deps.io, deps.home, {
3595
+ pairKey: receipt.pairKey,
3596
+ runId: receipt.parentRunId ?? receipt.runId,
3597
+ result: "undone",
3598
+ target: receipt.target
3599
+ });
2952
3600
  return {
2953
3601
  ok: result.ok,
2954
3602
  text: result.message
@@ -2980,7 +3628,7 @@ function createMergeRuntime(deps) {
2980
3628
  };
2981
3629
  }
2982
3630
  const loaded = await loadMergeState(deps.io, deps.home);
2983
- const row = command.id === void 0 ? loaded.state.rows.find((item) => item.state === "fuse" || item.state === "verify") : loaded.state.rows.find((item) => item.id === command.id);
3631
+ const row = command.id === void 0 ? loaded.state.rows.find((item) => item.state === "fuse" || item.state === "verify" || command.retry === true && item.state === "approved") : loaded.state.rows.find((item) => item.id === command.id);
2984
3632
  if (row === void 0) return {
2985
3633
  ok: false,
2986
3634
  text: "merge row not found: " + (command.id ?? "(auto)")
@@ -3039,13 +3687,22 @@ function createMergeRuntime(deps) {
3039
3687
  ...stageHooks
3040
3688
  })
3041
3689
  });
3042
- return result.ok ? {
3043
- ok: true,
3044
- text: "merge resume 完成:merged " + String(result.report.totals.merged)
3045
- } : {
3690
+ if (!result.ok) return {
3046
3691
  ok: false,
3047
3692
  text: result.message
3048
3693
  };
3694
+ if ("accepted" in result) return {
3695
+ ok: true,
3696
+ text: "merge resume 已启动:" + result.runId
3697
+ };
3698
+ if (!("report" in result)) return {
3699
+ ok: false,
3700
+ text: "merge resume started without a report"
3701
+ };
3702
+ return {
3703
+ ok: true,
3704
+ text: "merge resume 完成:merged " + String(result.report.totals.merged)
3705
+ };
3049
3706
  }
3050
3707
  if (command.verb === "approve") {
3051
3708
  const signal = new AbortController().signal;
@@ -3066,13 +3723,22 @@ function createMergeRuntime(deps) {
3066
3723
  now: deps.now
3067
3724
  }) }
3068
3725
  });
3069
- return result.ok ? {
3070
- ok: true,
3071
- text: "merge approve 完成:merged " + String(result.report.totals.merged)
3072
- } : {
3726
+ if (!result.ok) return {
3073
3727
  ok: false,
3074
3728
  text: result.message
3075
3729
  };
3730
+ if ("accepted" in result) return {
3731
+ ok: true,
3732
+ text: "merge approve 已启动:" + result.runId
3733
+ };
3734
+ if (!("report" in result)) return {
3735
+ ok: false,
3736
+ text: "merge approve started without a report"
3737
+ };
3738
+ return {
3739
+ ok: true,
3740
+ text: "merge approve 完成:merged " + String(result.report.totals.merged)
3741
+ };
3076
3742
  }
3077
3743
  const text = await handleReadOnlyMergeCommand(command, deps.io, deps.home);
3078
3744
  if (text !== null) return {
@@ -3088,7 +3754,8 @@ const MERGE_PARAM_DEFAULTS = {
3088
3754
  mergeApproval: false,
3089
3755
  mergeSkippedTtlDays: 0,
3090
3756
  mergeRunTimeoutMinutes: 0,
3091
- mergePairTimeoutMinutes: 15
3757
+ mergePairTimeoutMinutes: 15,
3758
+ mergeModelsRoot: ""
3092
3759
  };
3093
3760
  const KEYS = new Set(Object.keys(MERGE_PARAM_DEFAULTS));
3094
3761
  function materialize(value) {
@@ -3110,6 +3777,11 @@ function resolveMergeParams(input = {}) {
3110
3777
  mergeApproval: approval,
3111
3778
  mergeSkippedTtlDays: record.mergeSkippedTtlDays === void 0 ? MERGE_PARAM_DEFAULTS.mergeSkippedTtlDays : nonNegative("mergeSkippedTtlDays", record.mergeSkippedTtlDays),
3112
3779
  mergeRunTimeoutMinutes: record.mergeRunTimeoutMinutes === void 0 ? MERGE_PARAM_DEFAULTS.mergeRunTimeoutMinutes : nonNegative("mergeRunTimeoutMinutes", record.mergeRunTimeoutMinutes),
3780
+ mergeModelsRoot: record.mergeModelsRoot === void 0 ? MERGE_PARAM_DEFAULTS.mergeModelsRoot : (() => {
3781
+ const value = materialize(record.mergeModelsRoot);
3782
+ if (typeof value !== "string") throw new Error("mergeModelsRoot must be a string");
3783
+ return value;
3784
+ })(),
3113
3785
  mergePairTimeoutMinutes: record.mergePairTimeoutMinutes === void 0 ? MERGE_PARAM_DEFAULTS.mergePairTimeoutMinutes : (() => {
3114
3786
  const value = nonNegative("mergePairTimeoutMinutes", record.mergePairTimeoutMinutes);
3115
3787
  if (value === 0) throw new Error("mergePairTimeoutMinutes must be > 0");
@@ -3118,81 +3790,6 @@ function resolveMergeParams(input = {}) {
3118
3790
  };
3119
3791
  }
3120
3792
  //#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
3793
  //#region lib/types/adapters/channel.js
3197
3794
  function convert(outcome) {
3198
3795
  if (outcome.kind !== "ok") return outcome;
@@ -3230,313 +3827,6 @@ function subagentChannelAdapter(channel) {
3230
3827
  };
3231
3828
  }
3232
3829
  //#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
3830
  //#region lib/types/stage-tool-registry.js
3541
3831
  var StageToolRegistry = class {
3542
3832
  bindings = /* @__PURE__ */ new Map();
@@ -3769,7 +4059,7 @@ function operation(args) {
3769
4059
  function createMergeStageTool(io, home, context) {
3770
4060
  return {
3771
4061
  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.",
4062
+ 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
4063
  parameters: {
3774
4064
  type: "object",
3775
4065
  properties: {
@@ -3855,8 +4145,21 @@ const Config = z.object({
3855
4145
  mergeApproval: z.boolean().default(false).volatile(),
3856
4146
  mergeSkippedTtlDays: z.number().min(0).default(0).volatile(),
3857
4147
  mergeRunTimeoutMinutes: z.number().min(0).default(0).volatile(),
3858
- mergePairTimeoutMinutes: z.number().min(1).default(15)
4148
+ mergePairTimeoutMinutes: z.number().min(1).default(15),
4149
+ mergeModelsRoot: z.string().default("")
3859
4150
  });
4151
+ function engineConfigFromEnv(configuredRoot) {
4152
+ const root = configuredRoot !== void 0 && configuredRoot !== "" ? configuredRoot : process.env.SKILL_MERGE_MODELS_ROOT;
4153
+ if (root === void 0 || root === "") return void 0;
4154
+ return {
4155
+ models_dir: "",
4156
+ offline: true,
4157
+ auto_provision: false,
4158
+ embed_backend: "local",
4159
+ embedder_model: join(root, "potion-multilingual-128M-int8"),
4160
+ embedder_model_last: join(root, "potion-multilingual-128M-pca64-int8")
4161
+ };
4162
+ }
3860
4163
  /** Suppress the platform auto form; the registry renders the merge card instead. */
3861
4164
  function configureMergePresentation(ctx) {
3862
4165
  ctx.inject(["settings"], (scope) => {
@@ -3870,6 +4173,15 @@ const inject = [];
3870
4173
  function apply(ctx, rawConfig = {}) {
3871
4174
  configureMergePresentation(ctx);
3872
4175
  const params = resolveMergeParams(rawConfig);
4176
+ let mergeRun;
4177
+ const mergeService = { async run(rawInput, invocation) {
4178
+ return mergeRun === void 0 ? {
4179
+ ok: false,
4180
+ text: "merge 未就绪:evolutionIo 尚未挂载"
4181
+ } : mergeRun(rawInput, invocation);
4182
+ } };
4183
+ const mergeContext = ctx;
4184
+ if (typeof mergeContext.provide === "function") mergeContext.provide("evolutionMerge", mergeService);
3873
4185
  let source;
3874
4186
  registerHostRoutes(ctx, (audit) => makeMergeRoutes({ getSource: () => source }, audit));
3875
4187
  ctx.inject(["commands", "evolutionIo"], (scope) => {
@@ -3877,6 +4189,9 @@ function apply(ctx, rawConfig = {}) {
3877
4189
  if (registry === void 0) return;
3878
4190
  const io = evolutionIoAdapter(() => registry.provider());
3879
4191
  const home = evolutionHome();
4192
+ loadMergeState(io, home).then((loaded) => cleanupExpiredStaging(io, home, Date.now(), new Set(loaded.state.rows.map((row) => row.runId).filter((id) => id !== void 0)))).catch((error) => {
4193
+ ctx.logger.warn("merge staging cleanup failed: " + (error instanceof Error ? error.message : String(error)));
4194
+ });
3880
4195
  const runs = newRunRegistry({
3881
4196
  io,
3882
4197
  home,
@@ -3904,9 +4219,36 @@ function apply(ctx, rawConfig = {}) {
3904
4219
  });
3905
4220
  const channel = subagentChannelAdapter(createSubagentChannel(ctx));
3906
4221
  const stage = new StageToolRegistry();
4222
+ const engineConfig = engineConfigFromEnv(params.mergeModelsRoot);
3907
4223
  const llm = ctx.get("llm");
3908
4224
  const defaultModel = ctx.get("agentDefaultModel");
3909
- const modelPort = llm === void 0 || defaultModel === void 0 ? void 0 : createModelPort(llm, () => defaultModel.currentSelection());
4225
+ const runner = llm === void 0 || defaultModel === void 0 ? void 0 : createEngineRunner(llm, () => defaultModel.currentSelection());
4226
+ const engineScreening = createEngineScreening({
4227
+ io,
4228
+ home,
4229
+ engine: { screen },
4230
+ select: selectWithErrors,
4231
+ cfg: {
4232
+ ...engineConfig ?? {},
4233
+ judge: "deepseek"
4234
+ },
4235
+ ...runner === void 0 ? {} : { runner },
4236
+ async onSelection(selection) {
4237
+ const loaded = await loadMergeState(io, home);
4238
+ const at = (/* @__PURE__ */ new Date()).toISOString();
4239
+ for (const skipped of selection.skipped) {
4240
+ if (loaded.state.rows.some((row) => row.pairKey === skipped.pairKey)) continue;
4241
+ const next = transition(void 0, {
4242
+ type: "gate-keep",
4243
+ a: skipped.a,
4244
+ b: skipped.b,
4245
+ ...skipped.reason === void 0 ? {} : { reason: skipped.reason },
4246
+ at
4247
+ });
4248
+ if (next.ok && next.row !== null) await upsertRow(io, home, () => Date.now(), next.row);
4249
+ }
4250
+ }
4251
+ });
3910
4252
  source = {
3911
4253
  rows() {
3912
4254
  return loadMergeState(io, home).then((loaded) => loaded.state.rows);
@@ -3917,6 +4259,43 @@ function apply(ctx, rawConfig = {}) {
3917
4259
  runs() {
3918
4260
  return Promise.resolve(runs.runs());
3919
4261
  },
4262
+ async body(id, side) {
4263
+ const row = (await loadMergeState(io, home)).state.rows.find((item) => item.id === id);
4264
+ if (row === void 0) return null;
4265
+ return await skills.read(side === "a" ? row.a : row.b);
4266
+ },
4267
+ async history() {
4268
+ const receipts = await readCommitReceipts(io, home);
4269
+ const items = /* @__PURE__ */ new Map();
4270
+ for (const receipt of receipts) for (const source of receipt.sources) {
4271
+ const key = source + ":" + receipt.runId;
4272
+ if (items.has(key)) continue;
4273
+ items.set(key, {
4274
+ name: source,
4275
+ kind: "merged",
4276
+ reason: "Merged into " + receipt.target,
4277
+ lastVersion: 1,
4278
+ archivedAt: new Date(receipt.at).toISOString(),
4279
+ sourceRunId: receipt.runId
4280
+ });
4281
+ }
4282
+ const archived = await skills.archivedNames?.() ?? [];
4283
+ const seen = new Set([...items.values()].map((item) => item.name));
4284
+ for (const name of archived) {
4285
+ if (seen.has(name)) continue;
4286
+ items.set("archived:" + name, {
4287
+ name,
4288
+ kind: "other",
4289
+ reason: "archived",
4290
+ lastVersion: 1,
4291
+ archivedAt: new Date(Date.now()).toISOString()
4292
+ });
4293
+ }
4294
+ return [...items.values()];
4295
+ },
4296
+ approval() {
4297
+ return params.mergeApproval;
4298
+ },
3920
4299
  commands() {
3921
4300
  return [
3922
4301
  "merge scan",
@@ -3932,7 +4311,7 @@ function apply(ctx, rawConfig = {}) {
3932
4311
  };
3933
4312
  const commands = scope.get("commands");
3934
4313
  if (commands === void 0) return;
3935
- const command = createMergeCommand(createMergeRuntime({
4314
+ const runtime = createMergeRuntime({
3936
4315
  io,
3937
4316
  home,
3938
4317
  registry: runs,
@@ -3947,31 +4326,24 @@ function apply(ctx, rawConfig = {}) {
3947
4326
  async cards(signal) {
3948
4327
  const listed = await skills.list({ withContent: true });
3949
4328
  if (signal.aborted) return [];
3950
- return listed.map((item) => ({
3951
- name: item.name,
3952
- description: item.description,
3953
- body: item.content ?? ""
4329
+ return Promise.all(listed.map(async (item) => {
4330
+ const probe = await skills.listSupportFiles(item.name);
4331
+ const paths = probe.kind === "present" ? probe.value : [];
4332
+ const files = await Promise.all(paths.map(async (path) => ({
4333
+ path,
4334
+ content: await skills.readSupportFile(item.name, path) ?? ""
4335
+ })));
4336
+ return {
4337
+ name: item.name,
4338
+ description: item.description,
4339
+ body: item.content ?? "",
4340
+ files,
4341
+ protectedBy: item.protectedBy,
4342
+ protectionUnknown: item.protectionUnknown
4343
+ };
3954
4344
  }));
3955
4345
  },
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
- } },
4346
+ screeningForRun: engineScreening,
3975
4347
  stage: {
3976
4348
  bind: (context) => {
3977
4349
  stage.bind(context.childId, context);
@@ -3980,7 +4352,9 @@ function apply(ctx, rawConfig = {}) {
3980
4352
  stage.unbind(childId);
3981
4353
  }
3982
4354
  }
3983
- }));
4355
+ });
4356
+ mergeRun = (rawInput, invocation) => runMergeCommand(runtime, rawInput, invocation);
4357
+ const command = createMergeCommand(runtime);
3984
4358
  scope.effect(() => commands.register(command), "evolution-merge: command");
3985
4359
  ctx.inject(["tools"], (toolScope) => {
3986
4360
  const tools = toolScope.get("tools");
@@ -3994,4 +4368,4 @@ function apply(ctx, rawConfig = {}) {
3994
4368
  });
3995
4369
  }
3996
4370
  //#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 };
4371
+ 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, cleanupExpiredStaging, 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, readCommitReceipts, 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 };