@lmzhen/dsh-evolution-merge 0.0.0 → 0.29.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. package/README.md +27 -1
  2. package/lib/index.js +3814 -0
  3. package/lib/types/adapters/channel.d.ts +42 -0
  4. package/lib/types/adapters/engine-screen.d.ts +55 -0
  5. package/lib/types/adapters/library.d.ts +8 -0
  6. package/lib/types/adapters/model-runner.d.ts +28 -0
  7. package/lib/types/adapters/undo.d.ts +9 -0
  8. package/lib/types/approve-command.d.ts +22 -0
  9. package/lib/types/authoring-flow.d.ts +40 -0
  10. package/lib/types/channel-port.d.ts +61 -0
  11. package/lib/types/commands.d.ts +55 -0
  12. package/lib/types/commit-assembly.d.ts +27 -0
  13. package/lib/types/commit-execute.d.ts +29 -0
  14. package/lib/types/commit-plan.d.ts +45 -0
  15. package/lib/types/commit-receipt.d.ts +17 -0
  16. package/lib/types/contracts.d.ts +141 -0
  17. package/lib/types/coordinator.d.ts +65 -0
  18. package/lib/types/decision.d.ts +18 -0
  19. package/lib/types/doctor.d.ts +10 -0
  20. package/lib/types/gate-cache.d.ts +12 -0
  21. package/lib/types/index.d.ts +81 -0
  22. package/lib/types/ladder.d.ts +65 -0
  23. package/lib/types/merge-command.d.ts +39 -0
  24. package/lib/types/merge-routes.d.ts +24 -0
  25. package/lib/types/merge-tool.d.ts +22 -0
  26. package/lib/types/new-child-authoring.d.ts +29 -0
  27. package/lib/types/pair-command.d.ts +20 -0
  28. package/lib/types/params.d.ts +14 -0
  29. package/lib/types/pending-actions.d.ts +17 -0
  30. package/lib/types/prompts.d.ts +15 -0
  31. package/lib/types/read-commands.d.ts +9 -0
  32. package/lib/types/report.d.ts +10 -0
  33. package/lib/types/resume-command.d.ts +24 -0
  34. package/lib/types/round2-outcome.d.ts +13 -0
  35. package/lib/types/routes.d.ts +64 -0
  36. package/lib/types/rules.d.ts +24 -0
  37. package/lib/types/run-shell.d.ts +65 -0
  38. package/lib/types/runtime.d.ts +37 -0
  39. package/lib/types/scan-command.d.ts +21 -0
  40. package/lib/types/scan-ports.d.ts +32 -0
  41. package/lib/types/scan-run.d.ts +46 -0
  42. package/lib/types/skill-card.d.ts +12 -0
  43. package/lib/types/skipped.d.ts +8 -0
  44. package/lib/types/stage-tool-registry.d.ts +14 -0
  45. package/lib/types/stage-tool.d.ts +45 -0
  46. package/lib/types/staged-tree.d.ts +4 -0
  47. package/lib/types/staging-files.d.ts +15 -0
  48. package/lib/types/staging.d.ts +49 -0
  49. package/lib/types/state.d.ts +29 -0
  50. package/lib/types/status.d.ts +14 -0
  51. package/lib/types/supersede.d.ts +8 -0
  52. package/lib/types/undo-merge.d.ts +13 -0
  53. package/package.json +31 -3
package/lib/index.js ADDED
@@ -0,0 +1,3814 @@
1
+ import { join } from "node:path";
2
+ import z from "@deepseek-ai/schemastery";
3
+ import { screen, selectCandidates } from "@lmzhen/dsh-evolution-merge-engine";
4
+ import { DEFAULT_SKILL_LIMITS, authorityFrom, contentHash, createSubagentChannel, evolutionHome, evolutionIoAdapter, executeUnitUndo, isPresent, newRunRegistry, newSkillLibrary, parseFrontmatter, planUnitUndoAcross, policyStageLimits, probeAbsent, probeList, probePresent, probeText, probeUnknown, scanContentThreats, supportRefs, transactIo, validateFrontmatter, validateSupportPath } from "@lmzhen/dsh-evolution-core";
5
+ import { HOST_ROUTES, json, methodNotAllowed, registerHostRoutes } from "@lmzhen/dsh-evolution-core/http-routes";
6
+ import { createHash } from "node:crypto";
7
+ //#region lib/types/adapters/engine-screen.js
8
+ /**
9
+ * G3 screen adapter: cards -> corpus snapshot -> EnginePort.screen.
10
+ *
11
+ * The vendor engine reads a corpus from disk; host cards are content. This module
12
+ * writes only SKILL.md support-free snapshots under the run directory. It does not
13
+ * decide candidates: the Q02 selector is injected so engine/report-map stays the
14
+ * single source of that policy.
15
+ */
16
+ function safeName(name, index) {
17
+ const cleaned = name.replace(/[^A-Za-z0-9._-]+/g, "-").replace(/^-+|-+$/g, "");
18
+ return (cleaned === "" ? "skill-" + String(index) : cleaned) + "__" + String(index);
19
+ }
20
+ /** Write the exact card bodies as a support-free corpus snapshot and return its root. */
21
+ async function snapshotCards(io, home, runId, cards) {
22
+ const root = join(home, "merge", "runs", runId, "corpus");
23
+ for (let index = 0; index < cards.length; index += 1) {
24
+ const card = cards[index];
25
+ if (card === void 0) continue;
26
+ await io.writeText(join(root, safeName(card.name, index), "SKILL.md"), card.body);
27
+ }
28
+ return root;
29
+ }
30
+ function createEngineScreening(deps) {
31
+ return async (cards, runId, signal) => {
32
+ const corpusDir = await snapshotCards(deps.io, deps.home, runId, cards);
33
+ const outDir = join(deps.home, "merge", "runs", runId, "engine");
34
+ const result = await deps.engine.screen({
35
+ corpusDir,
36
+ outDir,
37
+ ...deps.cfg === void 0 ? {} : { cfg: deps.cfg },
38
+ ...deps.runner === void 0 ? {} : { runner: deps.runner },
39
+ signal
40
+ });
41
+ if (!result.ok) throw new Error("engine screen failed: " + result.error.code + (result.error.stage === void 0 ? "" : " @" + result.error.stage) + (result.error.detail === void 0 ? "" : ": " + result.error.detail));
42
+ const selection = deps.select(result.report);
43
+ if (deps.onSelection !== void 0) await deps.onSelection(selection, runId);
44
+ return Object.assign(selection.candidates.slice(), selection.candidatePairs === void 0 ? {} : { candidatePairs: selection.candidatePairs });
45
+ };
46
+ }
47
+ //#endregion
48
+ //#region lib/types/adapters/model-runner.js
49
+ function append(chunk, text) {
50
+ if (chunk.type === "text-delta" || chunk.type === "text") return text + (chunk.text ?? "");
51
+ if (chunk.type === "finish" && chunk.reason?.kind === "error") throw new Error("model call failed: " + (chunk.reason.failure?.message ?? "unknown"));
52
+ return text;
53
+ }
54
+ function createEngineRunner(llm, route) {
55
+ return async (prompt) => {
56
+ const selection = route();
57
+ let text = "";
58
+ if (llm.stream !== void 0) {
59
+ for await (const chunk of llm.stream({
60
+ provider: selection.provider,
61
+ model: selection.model,
62
+ messages: [{
63
+ role: "user",
64
+ content: [{
65
+ type: "text",
66
+ text: prompt
67
+ }]
68
+ }],
69
+ temperature: 0
70
+ })) text = append(chunk, text);
71
+ return text;
72
+ }
73
+ if (llm.prepareCall === void 0) throw new Error("llm service has neither stream nor prepareCall");
74
+ const call = await llm.prepareCall({
75
+ provider: selection.provider,
76
+ model: selection.model,
77
+ temperature: 0
78
+ });
79
+ for await (const chunk of call.stream({
80
+ ...call.config,
81
+ messages: [{
82
+ role: "user",
83
+ content: [{
84
+ type: "text",
85
+ text: prompt
86
+ }]
87
+ }]
88
+ })) text = append(chunk, text);
89
+ return text;
90
+ };
91
+ }
92
+ //#endregion
93
+ //#region lib/types/contracts.js
94
+ /**
95
+ * Frozen merge-domain vocabulary. No io, no host, no imports from the plugin shell.
96
+ *
97
+ * The data layer keeps four states; the user face collapses verify/fuse into one retry
98
+ * queue and keeps the stage as a chip. The API queue must never expose the raw state.
99
+ */
100
+ const MERGE_STATE_VERSION = 1;
101
+ const MERGE_PAIR_STATES = [
102
+ "verify",
103
+ "pending",
104
+ "fuse",
105
+ "skipped"
106
+ ];
107
+ const MERGE_QUEUES = [
108
+ "pending",
109
+ "retry",
110
+ "skipped"
111
+ ];
112
+ /** User-facing queue of one data state. verify/fuse share retry, not two tabs. */
113
+ function queueOf(state) {
114
+ switch (state) {
115
+ case "pending": return "pending";
116
+ case "verify":
117
+ case "fuse": return "retry";
118
+ case "skipped": return "skipped";
119
+ }
120
+ }
121
+ /** Coarse stage chip; skipped has no active stage. */
122
+ function stageOf(state) {
123
+ switch (state) {
124
+ case "verify": return "verify";
125
+ case "pending": return "decide";
126
+ case "fuse": return "author";
127
+ case "skipped": return null;
128
+ }
129
+ }
130
+ /** Stable pair identity; NUL cannot appear in a skill name. */
131
+ function pairKey(a, b) {
132
+ return a < b ? a + "\0" + b : b + "\0" + a;
133
+ }
134
+ /** Deterministic, URL-safe id derived from the pair key (FNV-1a 32). */
135
+ function rowIdOf(key) {
136
+ let hash = 2166136261;
137
+ for (let index = 0; index < key.length; index += 1) {
138
+ hash ^= key.charCodeAt(index);
139
+ hash = Math.imul(hash, 16777619) >>> 0;
140
+ }
141
+ return "mr_" + hash.toString(36).padStart(7, "0");
142
+ }
143
+ function emptyMergeState() {
144
+ return {
145
+ schemaVersion: 1,
146
+ rows: []
147
+ };
148
+ }
149
+ /** One record can be retried only when the failure says so and names a stage. */
150
+ const MERGE_MAX_ATTEMPTS = 3;
151
+ function canRetry(record) {
152
+ if (record.state === "skipped") return false;
153
+ if ((record.attempts ?? 0) >= 3) return false;
154
+ if (record.failure === void 0) return record.state !== "pending";
155
+ return record.failure.retryable && record.failure.retryFrom !== "none";
156
+ }
157
+ //#endregion
158
+ //#region lib/types/state.js
159
+ /**
160
+ * The one merge-state ledger: <home>/merge/merge-state.json.
161
+ *
162
+ * Reads are pure projections (no write, no reconciliation side effect). Every write goes
163
+ * through mutateMergeState and a single transactIo, so there is no second file and no
164
+ * incremental writer. A row's queue is always derived from its state on read; a hand-edited
165
+ * mismatch is repaired by the next write, never by a GET.
166
+ */
167
+ function mergeStatePath(home) {
168
+ return join(home, "merge", "merge-state.json");
169
+ }
170
+ function isState(value) {
171
+ return typeof value === "string" && MERGE_PAIR_STATES.includes(value);
172
+ }
173
+ function parseState(raw) {
174
+ if (raw === null) return {
175
+ ok: true,
176
+ rows: []
177
+ };
178
+ let parsed;
179
+ try {
180
+ parsed = JSON.parse(raw);
181
+ } catch {
182
+ return {
183
+ ok: false,
184
+ rows: []
185
+ };
186
+ }
187
+ const body = parsed;
188
+ if (!Array.isArray(body.rows)) return {
189
+ ok: false,
190
+ rows: []
191
+ };
192
+ const rows = [];
193
+ let skipped = 0;
194
+ for (const value of body.rows) {
195
+ const row = value;
196
+ if (typeof row.id !== "string" || typeof row.pairKey !== "string" || typeof row.a !== "string" || typeof row.b !== "string") {
197
+ skipped += 1;
198
+ continue;
199
+ }
200
+ if (!isState(row.state) || typeof row.createdAt !== "string" || typeof row.updatedAt !== "string") {
201
+ skipped += 1;
202
+ continue;
203
+ }
204
+ const normalized = {
205
+ id: row.id,
206
+ pairKey: row.pairKey,
207
+ a: row.a,
208
+ b: row.b,
209
+ state: row.state,
210
+ queue: queueOf(row.state),
211
+ createdAt: row.createdAt,
212
+ updatedAt: row.updatedAt,
213
+ ...row.failure === void 0 ? {} : { failure: row.failure },
214
+ ...row.decision === void 0 ? {} : { decision: row.decision },
215
+ ...typeof row.runId === "string" ? { runId: row.runId } : {},
216
+ ...typeof row.unitId === "string" ? { unitId: row.unitId } : {},
217
+ ...row.origin === "agent" || row.origin === "human" ? { origin: row.origin } : {},
218
+ ...typeof row.attempts === "number" ? { attempts: row.attempts } : {}
219
+ };
220
+ rows.push(normalized);
221
+ }
222
+ const versionNote = typeof body.schemaVersion === "number" && body.schemaVersion !== 1 ? "the index declares schemaVersion " + String(body.schemaVersion) + " while this build writes " + String(1) : void 0;
223
+ 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";
224
+ return {
225
+ ok: true,
226
+ rows,
227
+ ...note === void 0 ? {} : { note }
228
+ };
229
+ }
230
+ /** Pure read: never writes, never deletes a row. */
231
+ async function loadMergeState(io, home) {
232
+ let raw = null;
233
+ try {
234
+ raw = await io.readText(mergeStatePath(home));
235
+ } catch {
236
+ return {
237
+ ok: false,
238
+ state: {
239
+ schemaVersion: 1,
240
+ rows: []
241
+ }
242
+ };
243
+ }
244
+ const parsed = parseState(raw);
245
+ return {
246
+ ok: parsed.ok,
247
+ state: {
248
+ schemaVersion: 1,
249
+ rows: parsed.rows
250
+ },
251
+ ...parsed.note === void 0 ? {} : { note: parsed.note }
252
+ };
253
+ }
254
+ /** The ONE write point. The callback returns the whole row set, not a delta. */
255
+ async function mutateMergeState(io, home, now, fn) {
256
+ let result;
257
+ await transactIo(io, mergeStatePath(home), (current) => {
258
+ const out = fn(parseState(current).rows);
259
+ result = out.result;
260
+ return JSON.stringify({
261
+ schemaVersion: 1,
262
+ rows: out.rows
263
+ }, null, 2);
264
+ });
265
+ return result;
266
+ }
267
+ async function upsertRow(io, home, now, row) {
268
+ await mutateMergeState(io, home, now, (rows) => {
269
+ const index = rows.findIndex((item) => item.pairKey === row.pairKey);
270
+ const normalized = {
271
+ ...row,
272
+ queue: queueOf(row.state)
273
+ };
274
+ const next = [...rows];
275
+ if (index < 0) next.push(normalized);
276
+ else next[index] = {
277
+ ...normalized,
278
+ createdAt: rows[index]?.createdAt ?? normalized.createdAt
279
+ };
280
+ return {
281
+ rows: next,
282
+ result: void 0
283
+ };
284
+ });
285
+ }
286
+ async function moveRow(io, home, now, id, patch) {
287
+ return await mutateMergeState(io, home, now, (rows) => {
288
+ const index = rows.findIndex((item) => item.id === id);
289
+ if (index < 0) return {
290
+ rows,
291
+ result: void 0
292
+ };
293
+ const current = rows[index];
294
+ const state = patch.state ?? current.state;
295
+ const next = {
296
+ ...current,
297
+ state,
298
+ queue: queueOf(state),
299
+ updatedAt: new Date(now()).toISOString(),
300
+ ...patch.failure === void 0 ? {} : { failure: patch.failure },
301
+ ...patch.runId === void 0 ? {} : { runId: patch.runId },
302
+ ...patch.unitId === void 0 ? {} : { unitId: patch.unitId },
303
+ ...patch.origin === void 0 ? {} : { origin: patch.origin },
304
+ ...patch.attempts === void 0 ? {} : { attempts: patch.attempts }
305
+ };
306
+ const copy = [...rows];
307
+ copy[index] = next;
308
+ return {
309
+ rows: copy,
310
+ result: next
311
+ };
312
+ });
313
+ }
314
+ async function dropRow(io, home, now, id) {
315
+ return await mutateMergeState(io, home, now, (rows) => {
316
+ const next = rows.filter((item) => item.id !== id);
317
+ return {
318
+ rows: next,
319
+ result: next.length !== rows.length
320
+ };
321
+ });
322
+ }
323
+ async function rowsOf(io, home, queue) {
324
+ const loaded = await loadMergeState(io, home);
325
+ return queue === void 0 ? loaded.state.rows : loaded.state.rows.filter((row) => row.queue === queue);
326
+ }
327
+ //#endregion
328
+ //#region lib/types/coordinator.js
329
+ /**
330
+ * Pure merge transition table. No io, no run shell, no subagents.
331
+ *
332
+ * Every transition returns the whole next row (or a drop), so the caller can persist it
333
+ * through mutateMergeState in one transaction. Invalid transitions are refused by name;
334
+ * a caller never "repairs" a row by guessing which state the operator meant.
335
+ */
336
+ function invalid(event, state) {
337
+ return {
338
+ ok: false,
339
+ code: "invalid-transition",
340
+ message: "cannot apply " + event.type + " to " + (state ?? "no row")
341
+ };
342
+ }
343
+ /** Drop fields that must not survive a transition; optional fields stay absent. */
344
+ function base(row, at) {
345
+ return {
346
+ id: row.id,
347
+ pairKey: row.pairKey,
348
+ a: row.a,
349
+ b: row.b,
350
+ state: row.state,
351
+ queue: row.queue,
352
+ createdAt: row.createdAt,
353
+ updatedAt: at,
354
+ ...row.runId === void 0 ? {} : { runId: row.runId },
355
+ ...row.unitId === void 0 ? {} : { unitId: row.unitId },
356
+ ...row.origin === void 0 ? {} : { origin: row.origin },
357
+ ...row.decision === void 0 ? {} : { decision: row.decision },
358
+ ...row.attempts === void 0 ? {} : { attempts: row.attempts },
359
+ ...row.stagingRef === void 0 ? {} : { stagingRef: row.stagingRef },
360
+ ...row.stagingExpiresAt === void 0 ? {} : { stagingExpiresAt: row.stagingExpiresAt }
361
+ };
362
+ }
363
+ function next(row, at, state, patch = {}) {
364
+ return {
365
+ ok: true,
366
+ row: {
367
+ ...base(row, at),
368
+ state,
369
+ queue: queueOf(state),
370
+ ...patch
371
+ }
372
+ };
373
+ }
374
+ function transition(current, event) {
375
+ if (event.type === "candidate") {
376
+ if (current !== void 0) return {
377
+ ok: true,
378
+ row: current
379
+ };
380
+ const key = pairKey(event.a, event.b);
381
+ return {
382
+ ok: true,
383
+ row: {
384
+ id: rowIdOf(key),
385
+ pairKey: key,
386
+ a: event.a,
387
+ b: event.b,
388
+ state: "verify",
389
+ queue: "retry",
390
+ createdAt: event.at,
391
+ updatedAt: event.at
392
+ }
393
+ };
394
+ }
395
+ if (event.type === "source-gone") return {
396
+ ok: true,
397
+ row: null
398
+ };
399
+ if (event.type === "gate-keep") {
400
+ if (current !== void 0) return {
401
+ ok: true,
402
+ row: current
403
+ };
404
+ const key = pairKey(event.a, event.b);
405
+ return {
406
+ ok: true,
407
+ row: {
408
+ id: rowIdOf(key),
409
+ pairKey: key,
410
+ a: event.a,
411
+ b: event.b,
412
+ state: "skipped",
413
+ queue: "skipped",
414
+ origin: "agent",
415
+ createdAt: event.at,
416
+ updatedAt: event.at
417
+ }
418
+ };
419
+ }
420
+ if (current === void 0) return invalid(event, void 0);
421
+ switch (event.type) {
422
+ case "round1-keep": return current.state === "verify" ? next(current, event.at, "skipped", { origin: "agent" }) : invalid(event, current.state);
423
+ case "round1-merge":
424
+ if (current.state !== "verify") return invalid(event, current.state);
425
+ return event.approval === "on" ? next(current, event.at, "pending", {
426
+ ...event.decision === void 0 ? {} : { decision: event.decision },
427
+ ...event.stagingRef === void 0 ? {} : { stagingRef: event.stagingRef },
428
+ ...event.stagingExpiresAt === void 0 ? {} : { stagingExpiresAt: event.stagingExpiresAt }
429
+ }) : next(current, event.at, "fuse", {
430
+ ...event.decision === void 0 ? {} : { decision: event.decision },
431
+ ...event.stagingRef === void 0 ? {} : { stagingRef: event.stagingRef },
432
+ ...event.stagingExpiresAt === void 0 ? {} : { stagingExpiresAt: event.stagingExpiresAt }
433
+ });
434
+ case "round1-fail": return current.state === "verify" ? next(current, event.at, "verify", {
435
+ failure: event.failure,
436
+ attempts: (current.attempts ?? 0) + 1
437
+ }) : invalid(event, current.state);
438
+ case "approve": return current.state === "pending" ? next(current, event.at, "fuse", { ...event.runId === void 0 ? {} : { runId: event.runId } }) : invalid(event, current.state);
439
+ case "reject": return current.state === "pending" ? next(current, event.at, "skipped", { origin: "human" }) : invalid(event, current.state);
440
+ case "round2-fail": return current.state === "fuse" ? next(current, event.at, "fuse", {
441
+ failure: event.failure,
442
+ attempts: (current.attempts ?? 0) + 1
443
+ }) : invalid(event, current.state);
444
+ case "round2-success": return current.state === "fuse" ? {
445
+ ok: true,
446
+ row: null
447
+ } : invalid(event, current.state);
448
+ case "restore": return current.state === "skipped" ? next(current, event.at, "verify") : invalid(event, current.state);
449
+ }
450
+ }
451
+ //#endregion
452
+ //#region lib/types/status.js
453
+ function countRows(rows) {
454
+ const counts = {
455
+ pending: 0,
456
+ retry: 0,
457
+ skipped: 0
458
+ };
459
+ for (const row of rows) counts[queueOf(row.state)] += 1;
460
+ return counts;
461
+ }
462
+ function describe(row) {
463
+ const stage = stageOf(row.state);
464
+ const target = row.decision?.shouldMerge === true ? " → " + row.decision.target : "";
465
+ return row.a + " + " + row.b + " [" + row.state + (stage === null ? "" : "/" + stage) + "]" + target + (row.failure === void 0 ? "" : " — " + row.failure.message);
466
+ }
467
+ function formatStatus(rows) {
468
+ const counts = countRows(rows);
469
+ const lines = ["merge: pending " + String(counts.pending) + ", retry " + String(counts.retry) + ", skipped " + String(counts.skipped)];
470
+ for (const row of rows) lines.push("- " + describe(row));
471
+ return lines.join("\n");
472
+ }
473
+ function formatQueue(rows, queue) {
474
+ const selected = rows.filter((row) => queueOf(row.state) === queue);
475
+ if (selected.length === 0) return "merge " + queue + ": empty";
476
+ return ["merge " + queue + ": " + String(selected.length)].concat(selected.map((row) => "- " + describe(row))).join("\n");
477
+ }
478
+ //#endregion
479
+ //#region lib/types/routes.js
480
+ function toRowView(row) {
481
+ return {
482
+ id: row.id,
483
+ pairKey: row.pairKey,
484
+ a: row.a,
485
+ b: row.b,
486
+ state: row.state,
487
+ queue: queueOf(row.state),
488
+ stage: stageOf(row.state),
489
+ label: row.a + " + " + row.b,
490
+ updatedAt: row.updatedAt,
491
+ createdAt: row.createdAt,
492
+ ...row.decision?.target === void 0 ? {} : { target: row.decision.target },
493
+ ...row.decision?.confidence === void 0 ? {} : { confidence: row.decision.confidence },
494
+ ...row.failure === void 0 ? {} : { failure: {
495
+ stage: row.failure.stage,
496
+ code: row.failure.code,
497
+ message: row.failure.message,
498
+ retryable: row.failure.retryable
499
+ } },
500
+ ...row.attempts === void 0 ? {} : { attempts: row.attempts },
501
+ ...row.origin === void 0 ? {} : { origin: row.origin },
502
+ ...row.stagingExpiresAt === void 0 ? {} : { stagingExpiresAt: row.stagingExpiresAt },
503
+ actions: row.state === "pending" ? ["/evolution merge approve " + row.id, "/evolution merge reject " + row.id] : row.state === "skipped" ? ["/evolution merge skipped --restore " + row.id] : ["/evolution merge resume " + row.id]
504
+ };
505
+ }
506
+ function buildMergeView(rows, commands, latestRun, approval) {
507
+ const counts = countRows(rows);
508
+ return {
509
+ schemaVersion: 1,
510
+ counts,
511
+ tabs: [
512
+ {
513
+ queue: "pending",
514
+ count: counts.pending
515
+ },
516
+ {
517
+ queue: "retry",
518
+ count: counts.retry
519
+ },
520
+ {
521
+ queue: "skipped",
522
+ count: counts.skipped
523
+ }
524
+ ],
525
+ commands,
526
+ ...latestRun === void 0 ? {} : { latestRun },
527
+ ...approval === void 0 ? {} : { approval }
528
+ };
529
+ }
530
+ function pageQueue(rows, queue, cursor = 0, limit = 50) {
531
+ const filtered = rows.filter((row) => queueOf(row.state) === queue);
532
+ const size = Math.max(1, Math.min(200, limit));
533
+ const start = Math.max(0, cursor);
534
+ const items = filtered.slice(start, start + size).map(toRowView);
535
+ const next = start + size < filtered.length ? String(start + size) : void 0;
536
+ return {
537
+ queue,
538
+ items,
539
+ ...next === void 0 ? {} : { nextCursor: next }
540
+ };
541
+ }
542
+ function toRunFact(run) {
543
+ return {
544
+ id: run.id,
545
+ state: run.state,
546
+ ...run.command === void 0 ? {} : { command: run.command },
547
+ ...run.stage === void 0 ? {} : { stage: run.stage },
548
+ ...run.progress === void 0 ? {} : { progress: run.progress },
549
+ startedAt: run.startedAt,
550
+ ...run.endedAt === void 0 ? {} : { endedAt: run.endedAt },
551
+ ...run.resultRef === void 0 ? {} : { resultRef: run.resultRef },
552
+ ...run.failure === void 0 ? {} : { failure: run.failure },
553
+ ...run.supersededAt === void 0 ? {} : { supersededAt: run.supersededAt }
554
+ };
555
+ }
556
+ //#endregion
557
+ //#region lib/types/merge-routes.js
558
+ const MERGE_ROUTES = HOST_ROUTES.merge;
559
+ function makeMergeRoutes(deps, _audit) {
560
+ const absent = () => json(503, {
561
+ ok: false,
562
+ error: {
563
+ code: "merge-service-absent",
564
+ message: "merge service is not mounted"
565
+ }
566
+ });
567
+ const get = (request) => request.method === "GET" ? null : methodNotAllowed(["GET"]);
568
+ const url = (request) => new URL(request.url, "http://local");
569
+ return [
570
+ {
571
+ path: MERGE_ROUTES.view,
572
+ methods: ["GET"],
573
+ requestBody: "buffered",
574
+ fetch: async (request) => {
575
+ const denied = get(request);
576
+ if (denied !== null) return denied;
577
+ const source = deps.getSource();
578
+ if (source === void 0) return absent();
579
+ const latestMerge = (await source.runs(void 0, 20)).find((run) => run.kind === "merge");
580
+ return json(200, {
581
+ ok: true,
582
+ data: buildMergeView(await source.rows(), source.commands(), latestMerge === void 0 ? void 0 : toRunFact(latestMerge), source.approval?.())
583
+ });
584
+ }
585
+ },
586
+ {
587
+ path: MERGE_ROUTES.rows,
588
+ methods: ["GET"],
589
+ requestBody: "buffered",
590
+ fetch: async (request) => {
591
+ const denied = get(request);
592
+ if (denied !== null) return denied;
593
+ const source = deps.getSource();
594
+ if (source === void 0) return absent();
595
+ const query = url(request).searchParams;
596
+ const queue = query.get("queue") ?? "retry";
597
+ if (queue !== "pending" && queue !== "retry" && queue !== "skipped") return json(400, {
598
+ ok: false,
599
+ error: {
600
+ code: "bad-queue",
601
+ message: "queue must be pending/retry/skipped"
602
+ }
603
+ });
604
+ const cursor = Number.parseInt(query.get("cursor") ?? "0", 10);
605
+ const limit = Number.parseInt(query.get("limit") ?? "50", 10);
606
+ return json(200, {
607
+ ok: true,
608
+ data: pageQueue(await source.rows(), queue, Number.isFinite(cursor) ? cursor : 0, Number.isFinite(limit) ? limit : 50)
609
+ });
610
+ }
611
+ },
612
+ {
613
+ path: MERGE_ROUTES.run,
614
+ methods: ["GET"],
615
+ requestBody: "buffered",
616
+ fetch: async (request) => {
617
+ const denied = get(request);
618
+ if (denied !== null) return denied;
619
+ const source = deps.getSource();
620
+ if (source === void 0) return absent();
621
+ const id = url(request).searchParams.get("id");
622
+ if (id === null || id === "") return json(400, {
623
+ ok: false,
624
+ error: {
625
+ code: "missing-id",
626
+ message: "id is required"
627
+ }
628
+ });
629
+ const run = await source.run(id);
630
+ return run === void 0 ? json(404, {
631
+ ok: false,
632
+ error: {
633
+ code: "run-not-found",
634
+ message: "merge run not found: " + id
635
+ }
636
+ }) : json(200, {
637
+ ok: true,
638
+ data: toRunFact(run)
639
+ });
640
+ }
641
+ },
642
+ {
643
+ path: MERGE_ROUTES.runs,
644
+ methods: ["GET"],
645
+ requestBody: "buffered",
646
+ fetch: async (request) => {
647
+ const denied = get(request);
648
+ if (denied !== null) return denied;
649
+ const source = deps.getSource();
650
+ if (source === void 0) return absent();
651
+ const query = url(request).searchParams;
652
+ const limit = Number.parseInt(query.get("limit") ?? "20", 10);
653
+ return json(200, {
654
+ ok: true,
655
+ data: { items: (await source.runs(query.get("cursor") ?? void 0, Number.isFinite(limit) ? limit : 20)).map(toRunFact) }
656
+ });
657
+ }
658
+ }
659
+ ];
660
+ }
661
+ //#endregion
662
+ //#region lib/types/commands.js
663
+ const USAGE = "usage: /evolution merge scan | pair <a> <b> | status [id] | report [id] | cancel [id] | runs | doctor | pending | approve <id> | reject <id> | resume [id] [--clear <id>] | skipped [--restore <id>] | undo <target> --run <id>";
664
+ function flags(args, allowed) {
665
+ const values = /* @__PURE__ */ new Map();
666
+ for (let index = 0; index < args.length; index += 1) {
667
+ const token = args[index];
668
+ if (!token.startsWith("--")) continue;
669
+ const name = token.slice(2);
670
+ if (!allowed.includes(name)) return {
671
+ values,
672
+ error: "unknown flag: --" + name
673
+ };
674
+ const value = args[index + 1];
675
+ if (value === void 0 || value.startsWith("--")) return {
676
+ values,
677
+ error: "missing value for --" + name
678
+ };
679
+ values.set(name, value);
680
+ index += 1;
681
+ }
682
+ return { values };
683
+ }
684
+ function positional(args) {
685
+ return args.filter((token, index) => !token.startsWith("--") && !(index > 0 && args[index - 1].startsWith("--")));
686
+ }
687
+ function parseMergeCommand(argv) {
688
+ const [verb, ...rest] = argv;
689
+ if (verb === void 0 || verb === "help" || verb === "--help" || verb === "-h") return {
690
+ ok: true,
691
+ command: { verb: "scan" },
692
+ help: true
693
+ };
694
+ const pos = positional(rest);
695
+ switch (verb) {
696
+ case "scan": return pos.length === 0 ? {
697
+ ok: true,
698
+ command: { verb: "scan" }
699
+ } : {
700
+ ok: false,
701
+ code: "usage",
702
+ message: USAGE
703
+ };
704
+ case "pair": return pos.length === 2 ? {
705
+ ok: true,
706
+ command: {
707
+ verb: "pair",
708
+ a: pos[0],
709
+ b: pos[1]
710
+ }
711
+ } : {
712
+ ok: false,
713
+ code: "usage",
714
+ message: USAGE
715
+ };
716
+ case "status": return pos.length <= 1 ? {
717
+ ok: true,
718
+ command: {
719
+ verb: "status",
720
+ ...pos[0] === void 0 ? {} : { id: pos[0] }
721
+ }
722
+ } : {
723
+ ok: false,
724
+ code: "usage",
725
+ message: USAGE
726
+ };
727
+ case "report": return pos.length <= 1 ? {
728
+ ok: true,
729
+ command: {
730
+ verb: "report",
731
+ ...pos[0] === void 0 ? {} : { id: pos[0] }
732
+ }
733
+ } : {
734
+ ok: false,
735
+ code: "usage",
736
+ message: USAGE
737
+ };
738
+ case "cancel": return pos.length <= 1 ? {
739
+ ok: true,
740
+ command: {
741
+ verb: "cancel",
742
+ ...pos[0] === void 0 ? {} : { id: pos[0] }
743
+ }
744
+ } : {
745
+ ok: false,
746
+ code: "usage",
747
+ message: USAGE
748
+ };
749
+ case "runs": return pos.length === 0 ? {
750
+ ok: true,
751
+ command: { verb: "runs" }
752
+ } : {
753
+ ok: false,
754
+ code: "usage",
755
+ message: USAGE
756
+ };
757
+ case "doctor": return pos.length === 0 ? {
758
+ ok: true,
759
+ command: { verb: "doctor" }
760
+ } : {
761
+ ok: false,
762
+ code: "usage",
763
+ message: USAGE
764
+ };
765
+ case "pending": return pos.length === 0 ? {
766
+ ok: true,
767
+ command: { verb: "pending" }
768
+ } : {
769
+ ok: false,
770
+ code: "usage",
771
+ message: USAGE
772
+ };
773
+ case "approve": return pos.length === 1 ? {
774
+ ok: true,
775
+ command: {
776
+ verb: "approve",
777
+ id: pos[0]
778
+ }
779
+ } : {
780
+ ok: false,
781
+ code: "usage",
782
+ message: USAGE
783
+ };
784
+ case "reject": return pos.length === 1 ? {
785
+ ok: true,
786
+ command: {
787
+ verb: "reject",
788
+ id: pos[0]
789
+ }
790
+ } : {
791
+ ok: false,
792
+ code: "usage",
793
+ message: USAGE
794
+ };
795
+ case "resume": {
796
+ const parsed = flags(rest, ["clear"]);
797
+ if (parsed.error !== void 0) return {
798
+ ok: false,
799
+ code: "usage",
800
+ message: parsed.error
801
+ };
802
+ const clear = parsed.values.get("clear");
803
+ if (pos.length > 1) return {
804
+ ok: false,
805
+ code: "usage",
806
+ message: USAGE
807
+ };
808
+ return {
809
+ ok: true,
810
+ command: {
811
+ verb: "resume",
812
+ ...pos[0] === void 0 ? {} : { id: pos[0] },
813
+ ...clear === void 0 ? {} : { clear }
814
+ }
815
+ };
816
+ }
817
+ case "skipped": {
818
+ const parsed = flags(rest, ["restore"]);
819
+ if (parsed.error !== void 0) return {
820
+ ok: false,
821
+ code: "usage",
822
+ message: parsed.error
823
+ };
824
+ if (pos.length > 0) return {
825
+ ok: false,
826
+ code: "usage",
827
+ message: USAGE
828
+ };
829
+ const restore = parsed.values.get("restore");
830
+ return {
831
+ ok: true,
832
+ command: {
833
+ verb: "skipped",
834
+ ...restore === void 0 ? {} : { restore }
835
+ }
836
+ };
837
+ }
838
+ case "undo": {
839
+ const parsed = flags(rest, ["run"]);
840
+ if (parsed.error !== void 0) return {
841
+ ok: false,
842
+ code: "usage",
843
+ message: parsed.error
844
+ };
845
+ const target = pos[0];
846
+ const run = parsed.values.get("run");
847
+ if (target === void 0 || run === void 0 || pos.length !== 1) return {
848
+ ok: false,
849
+ code: "usage",
850
+ message: USAGE
851
+ };
852
+ return {
853
+ ok: true,
854
+ command: {
855
+ verb: "undo",
856
+ target,
857
+ run
858
+ }
859
+ };
860
+ }
861
+ default: return {
862
+ ok: false,
863
+ code: "usage",
864
+ message: "unknown merge command: " + verb + "\nusage: /evolution merge scan | pair <a> <b> | status [id] | report [id] | cancel [id] | runs | doctor | pending | approve <id> | reject <id> | resume [id] [--clear <id>] | skipped [--restore <id>] | undo <target> --run <id>"
865
+ };
866
+ }
867
+ }
868
+ function mergeUsage() {
869
+ return USAGE;
870
+ }
871
+ //#endregion
872
+ //#region lib/types/read-commands.js
873
+ async function handleReadOnlyMergeCommand(command, io, home) {
874
+ if (command.verb === "status") {
875
+ const loaded = await loadMergeState(io, home);
876
+ if (command.id !== void 0) {
877
+ const row = loaded.state.rows.find((item) => item.id === command.id);
878
+ return row === void 0 ? "merge row not found: " + command.id : formatStatus([row]);
879
+ }
880
+ return formatStatus(loaded.state.rows);
881
+ }
882
+ if (command.verb === "pending") return formatQueue((await loadMergeState(io, home)).state.rows, "pending");
883
+ if (command.verb === "skipped") return formatQueue((await loadMergeState(io, home)).state.rows, "skipped");
884
+ return null;
885
+ }
886
+ function mergeCommandHelp() {
887
+ return mergeUsage();
888
+ }
889
+ //#endregion
890
+ //#region lib/types/merge-command.js
891
+ function createReadOnlyMergeService(io, home) {
892
+ return { async execute(command) {
893
+ const text = await handleReadOnlyMergeCommand(command, io, home);
894
+ return text === null ? {
895
+ ok: false,
896
+ text: "merge " + command.verb + " 需要运行引擎(当前只挂载了只读命令)"
897
+ } : {
898
+ ok: true,
899
+ text
900
+ };
901
+ } };
902
+ }
903
+ async function runMergeCommand(service, rawInput, invocation) {
904
+ const tokens = rawInput.trim().split(/\s+/).filter(Boolean);
905
+ if (tokens[0] === "merge" || tokens[0] === "/merge" || tokens[0] === "evolution" || tokens[0] === "/evolution") tokens.shift();
906
+ if (tokens[0] === "merge") tokens.shift();
907
+ const parsed = parseMergeCommand(tokens);
908
+ if (!parsed.ok) return {
909
+ ok: false,
910
+ text: parsed.message
911
+ };
912
+ if (parsed.help === true) return {
913
+ ok: true,
914
+ text: mergeUsage()
915
+ };
916
+ try {
917
+ return await service.execute(parsed.command, invocation?.agent === void 0 ? {} : { parent: invocation.agent });
918
+ } catch (error) {
919
+ return {
920
+ ok: false,
921
+ text: "merge 命令失败:" + (error instanceof Error ? error.message : String(error)) + "\n运行 /evolution merge status 查看状态。"
922
+ };
923
+ }
924
+ }
925
+ function createMergeCommand(service) {
926
+ return {
927
+ name: "merge",
928
+ description: "技能自动合并:扫描、待批准、重跑、报告与 undo(兼容别名;请用 /evolution merge)",
929
+ input: { hint: mergeUsage() },
930
+ async handler(invocation) {
931
+ const result = await runMergeCommand(service, invocation.rawInput, invocation);
932
+ return {
933
+ kind: result.ok ? "success" : "error",
934
+ text: result.text
935
+ };
936
+ }
937
+ };
938
+ }
939
+ //#endregion
940
+ //#region lib/types/skipped.js
941
+ /**
942
+ * Skipped filtering: a pair the operator marked as not-worth-merging stays out of automatic
943
+ * screening until mergeSkippedTtlDays expires. Explicit /merge pair ignores this set.
944
+ */
945
+ function skippedPairKeys(rows, ttlDays, now) {
946
+ const keys = /* @__PURE__ */ new Set();
947
+ for (const row of rows) {
948
+ if (row.state !== "skipped") continue;
949
+ if (ttlDays <= 0) {
950
+ keys.add(row.pairKey);
951
+ continue;
952
+ }
953
+ const createdAt = Date.parse(row.createdAt);
954
+ if (!Number.isFinite(createdAt) || now - createdAt < ttlDays * 864e5) keys.add(row.pairKey);
955
+ }
956
+ return keys;
957
+ }
958
+ function isSkippedPair(keys, a, b) {
959
+ return keys.has(pairKey(a, b));
960
+ }
961
+ //#endregion
962
+ //#region lib/types/scan-run.js
963
+ /**
964
+ * The scan pair loop. Registry/report lifecycle stays in withMergeRun; this module owns
965
+ * candidate → round1 → pending/fuse → commit transitions and returns the report draft.
966
+ */
967
+ function at(now) {
968
+ return new Date(now()).toISOString();
969
+ }
970
+ function failureOf(stage, error) {
971
+ return {
972
+ stage,
973
+ code: "merge-failed",
974
+ message: error instanceof Error ? error.message : String(error),
975
+ retryable: true,
976
+ retryFrom: stage === "decide" ? "agent1" : "agent2"
977
+ };
978
+ }
979
+ function pairOf(row, candidate, outcome, extra = {}) {
980
+ return {
981
+ id: row.id,
982
+ pairKey: row.pairKey,
983
+ a: row.a,
984
+ b: row.b,
985
+ queue: row.queue,
986
+ candidate,
987
+ ...candidate.gate === void 0 ? {} : { gate: candidate.gate },
988
+ outcome,
989
+ archived: [],
990
+ ...extra
991
+ };
992
+ }
993
+ async function runScan(options) {
994
+ const stage = options.stage ?? (async () => {});
995
+ const candidates = await options.ports.screening(options.signal);
996
+ const before = await loadMergeState(options.io, options.home);
997
+ const queueBefore = {
998
+ pending: 0,
999
+ retry: 0,
1000
+ skipped: 0
1001
+ };
1002
+ for (const row of before.state.rows) queueBefore[row.queue] += 1;
1003
+ const startedAt = options.startedAt ?? options.now();
1004
+ const timeoutMs = (options.timeoutMinutes ?? 0) > 0 ? options.timeoutMinutes * 6e4 : 0;
1005
+ const pairs = [];
1006
+ const totals = {
1007
+ merged: 0,
1008
+ kept: 0,
1009
+ pending: 0,
1010
+ failed: 0
1011
+ };
1012
+ const remaining = [];
1013
+ let stopReason = "completed";
1014
+ for (let index = 0; index < candidates.length; index += 1) {
1015
+ if (options.signal.aborted) {
1016
+ stopReason = "cancelled";
1017
+ remaining.push(...candidates.slice(index).map((candidate) => ({
1018
+ pairKey: pairKey(candidate.a, candidate.b),
1019
+ queue: "retry"
1020
+ })));
1021
+ break;
1022
+ }
1023
+ if (timeoutMs > 0 && options.now() - startedAt >= timeoutMs) {
1024
+ stopReason = "timeout";
1025
+ remaining.push(...candidates.slice(index).map((candidate) => ({
1026
+ pairKey: pairKey(candidate.a, candidate.b),
1027
+ queue: "retry"
1028
+ })));
1029
+ break;
1030
+ }
1031
+ const candidate = candidates[index];
1032
+ await stage("decide", {
1033
+ done: index,
1034
+ total: candidates.length
1035
+ });
1036
+ let row;
1037
+ try {
1038
+ const created = transition(void 0, {
1039
+ type: "candidate",
1040
+ a: candidate.a,
1041
+ b: candidate.b,
1042
+ at: at(options.now)
1043
+ });
1044
+ if (!created.ok || created.row === null) throw new Error(created.ok ? "candidate row missing" : created.message);
1045
+ row = created.row;
1046
+ await upsertRow(options.io, options.home, options.now, row);
1047
+ } catch (error) {
1048
+ totals.failed += 1;
1049
+ pairs.push({
1050
+ id: "",
1051
+ pairKey: pairKey(candidate.a, candidate.b),
1052
+ a: candidate.a,
1053
+ b: candidate.b,
1054
+ queue: "retry",
1055
+ candidate,
1056
+ outcome: "failed",
1057
+ archived: [],
1058
+ failure: failureOf("screening", error)
1059
+ });
1060
+ continue;
1061
+ }
1062
+ let round1;
1063
+ try {
1064
+ round1 = await options.ports.round1(candidate, options.signal);
1065
+ } catch (error) {
1066
+ round1 = {
1067
+ kind: "failed",
1068
+ failure: failureOf("decide", error)
1069
+ };
1070
+ }
1071
+ if (round1.kind === "failed") {
1072
+ const next = transition(row, {
1073
+ type: "round1-fail",
1074
+ failure: round1.failure ?? failureOf("decide", "round1 failed"),
1075
+ at: at(options.now)
1076
+ });
1077
+ if (next.ok && next.row !== null) {
1078
+ row = next.row;
1079
+ await upsertRow(options.io, options.home, options.now, row);
1080
+ }
1081
+ pairs.push(pairOf(row, candidate, "failed", { failure: round1.failure ?? failureOf("decide", "round1 failed") }));
1082
+ totals.failed += 1;
1083
+ continue;
1084
+ }
1085
+ if (round1.kind === "keep") {
1086
+ const next = transition(row, {
1087
+ type: "round1-keep",
1088
+ at: at(options.now)
1089
+ });
1090
+ if (next.ok && next.row !== null) {
1091
+ row = next.row;
1092
+ await upsertRow(options.io, options.home, options.now, row);
1093
+ }
1094
+ pairs.push(pairOf(row, candidate, "kept", round1.decision === void 0 ? {} : { decision: round1.decision }));
1095
+ totals.kept += 1;
1096
+ continue;
1097
+ }
1098
+ if (round1.decision === void 0) {
1099
+ pairs.push(pairOf(row, candidate, "failed", { failure: failureOf("decide", "merge decision missing") }));
1100
+ totals.failed += 1;
1101
+ continue;
1102
+ }
1103
+ if (options.approval) {
1104
+ const next = transition(row, {
1105
+ type: "round1-merge",
1106
+ approval: "on",
1107
+ decision: round1.decision,
1108
+ ...round1.stagingRef === void 0 ? {} : { stagingRef: round1.stagingRef },
1109
+ ...round1.stagingExpiresAt === void 0 ? {} : { stagingExpiresAt: round1.stagingExpiresAt },
1110
+ at: at(options.now)
1111
+ });
1112
+ if (next.ok && next.row !== null) {
1113
+ row = next.row;
1114
+ await upsertRow(options.io, options.home, options.now, row);
1115
+ }
1116
+ pairs.push(pairOf(row, candidate, "pending", { decision: round1.decision }));
1117
+ totals.pending += 1;
1118
+ continue;
1119
+ }
1120
+ const fuse = transition(row, {
1121
+ type: "round1-merge",
1122
+ approval: "off",
1123
+ decision: round1.decision,
1124
+ ...round1.stagingRef === void 0 ? {} : { stagingRef: round1.stagingRef },
1125
+ ...round1.stagingExpiresAt === void 0 ? {} : { stagingExpiresAt: round1.stagingExpiresAt },
1126
+ at: at(options.now)
1127
+ });
1128
+ if (!fuse.ok || fuse.row === null) {
1129
+ pairs.push(pairOf(row, candidate, "failed", {
1130
+ decision: round1.decision,
1131
+ failure: failureOf("decide", "cannot enter fuse")
1132
+ }));
1133
+ totals.failed += 1;
1134
+ continue;
1135
+ }
1136
+ row = fuse.row;
1137
+ await upsertRow(options.io, options.home, options.now, row);
1138
+ await stage("author", {
1139
+ done: index,
1140
+ total: candidates.length
1141
+ });
1142
+ let round2;
1143
+ try {
1144
+ round2 = await options.ports.round2(row, round1.decision, options.signal);
1145
+ } catch (error) {
1146
+ round2 = {
1147
+ kind: "failed",
1148
+ failure: failureOf("author", error)
1149
+ };
1150
+ }
1151
+ if (round2.kind === "merged") {
1152
+ if (transition(row, {
1153
+ type: "round2-success",
1154
+ at: at(options.now)
1155
+ }).ok) await dropRow(options.io, options.home, options.now, row.id);
1156
+ pairs.push(pairOf(row, candidate, "merged", {
1157
+ decision: round1.decision,
1158
+ ...round2.unitId === void 0 ? {} : { unitId: round2.unitId },
1159
+ ...round2.target === void 0 ? {} : { target: round2.target },
1160
+ archived: round2.archived ?? []
1161
+ }));
1162
+ totals.merged += 1;
1163
+ continue;
1164
+ }
1165
+ const failed = round2.failure ?? failureOf("author", "round2 failed");
1166
+ const next = transition(row, {
1167
+ type: "round2-fail",
1168
+ failure: failed,
1169
+ at: at(options.now)
1170
+ });
1171
+ if (next.ok && next.row !== null) {
1172
+ row = next.row;
1173
+ await upsertRow(options.io, options.home, options.now, row);
1174
+ }
1175
+ pairs.push(pairOf(row, candidate, "failed", {
1176
+ decision: round1.decision,
1177
+ failure: failed
1178
+ }));
1179
+ totals.failed += 1;
1180
+ }
1181
+ return {
1182
+ stopReason,
1183
+ scope: {
1184
+ candidatePairs: candidates.candidatePairs ?? candidates.length,
1185
+ queueBefore
1186
+ },
1187
+ pairs,
1188
+ totals,
1189
+ notes: [],
1190
+ ...remaining.length === 0 ? {} : { remaining }
1191
+ };
1192
+ }
1193
+ //#endregion
1194
+ //#region lib/types/report.js
1195
+ /**
1196
+ * Merge run report: JSON is the machine source, Markdown is the human digest.
1197
+ *
1198
+ * Reports are written before registry.settle, so a settled run's resultRef is readable.
1199
+ * readMergeReport uses the three-state probe: absent is not "no pairs", unknown is not empty.
1200
+ */
1201
+ function mergeReportPath(home, runId) {
1202
+ return join(home, "reports", "merge-" + runId + ".json");
1203
+ }
1204
+ function mergeReportDigestPath(home, runId) {
1205
+ return join(home, "reports", "merge-" + runId + ".md");
1206
+ }
1207
+ function isReport(value) {
1208
+ if (typeof value !== "object" || value === null) return false;
1209
+ const report = value;
1210
+ 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);
1211
+ }
1212
+ function digest(report) {
1213
+ const lines = [];
1214
+ lines.push("# merge " + report.runId);
1215
+ lines.push("");
1216
+ lines.push("- command: " + report.command);
1217
+ lines.push("- stop: " + report.stopReason);
1218
+ lines.push("- pairs: " + String(report.pairs.length));
1219
+ lines.push("- totals: merged " + String(report.totals.merged) + ", kept " + String(report.totals.kept) + ", pending " + String(report.totals.pending) + ", failed " + String(report.totals.failed));
1220
+ for (const pair of report.pairs) lines.push("- " + pair.a + " + " + pair.b + " -> " + pair.outcome + (pair.target === void 0 ? "" : " (" + pair.target.actual + ")"));
1221
+ if (report.remaining !== void 0 && report.remaining.length > 0) {
1222
+ lines.push("");
1223
+ lines.push("- remaining: " + report.remaining.map((item) => item.pairKey).join(", "));
1224
+ }
1225
+ return lines.join("\n") + "\n";
1226
+ }
1227
+ /** Write JSON first, then the digest; returns the JSON path (the registry resultRef). */
1228
+ async function writeMergeReport(io, home, report) {
1229
+ const path = mergeReportPath(home, report.runId);
1230
+ await io.writeText(path, JSON.stringify(report, null, 2) + "\n");
1231
+ await io.writeText(mergeReportDigestPath(home, report.runId), digest(report));
1232
+ return path;
1233
+ }
1234
+ async function readMergeReport(io, home, runId) {
1235
+ const probe = await probeText(io, mergeReportPath(home, runId));
1236
+ if (!isPresent(probe)) return probe;
1237
+ try {
1238
+ const parsed = JSON.parse(probe.value);
1239
+ return isReport(parsed) ? probePresent(parsed) : probeUnknown("merge report has an unexpected shape");
1240
+ } catch (error) {
1241
+ return probeUnknown("merge report is not JSON: " + (error instanceof Error ? error.message : String(error)));
1242
+ }
1243
+ }
1244
+ /** Pairs this run actually merged (for merge undo / reports). */
1245
+ async function committedPairs(io, home, runId) {
1246
+ const probe = await readMergeReport(io, home, runId);
1247
+ return isPresent(probe) ? probe.value.pairs.filter((pair) => pair.outcome === "merged") : [];
1248
+ }
1249
+ //#endregion
1250
+ //#region lib/types/run-shell.js
1251
+ function emptyDraft(reason) {
1252
+ return {
1253
+ stopReason: "failed",
1254
+ scope: {
1255
+ candidatePairs: 0,
1256
+ queueBefore: {
1257
+ pending: 0,
1258
+ retry: 0,
1259
+ skipped: 0
1260
+ }
1261
+ },
1262
+ pairs: [],
1263
+ totals: {
1264
+ merged: 0,
1265
+ kept: 0,
1266
+ pending: 0,
1267
+ failed: 0
1268
+ },
1269
+ notes: [reason]
1270
+ };
1271
+ }
1272
+ async function withMergeRun(options) {
1273
+ const handle = await options.registry.begin("merge", {
1274
+ command: options.command,
1275
+ ...options.total === void 0 ? {} : { total: options.total }
1276
+ });
1277
+ const startedAt = handle.startedAt;
1278
+ const stage = (name, progress) => options.registry.stage(handle.id, name, progress);
1279
+ await stage("screening", options.total === void 0 ? void 0 : {
1280
+ done: 0,
1281
+ total: options.total
1282
+ });
1283
+ let draft;
1284
+ try {
1285
+ draft = await options.body({
1286
+ runId: handle.id,
1287
+ startedAt,
1288
+ signal: handle.signal,
1289
+ stage
1290
+ });
1291
+ } catch (error) {
1292
+ draft = emptyDraft("run body failed: " + (error instanceof Error ? error.message : String(error)));
1293
+ }
1294
+ const report = {
1295
+ schemaVersion: 1,
1296
+ runId: handle.id,
1297
+ kind: "merge",
1298
+ command: options.command,
1299
+ startedAt,
1300
+ endedAt: options.now(),
1301
+ stopReason: draft.stopReason,
1302
+ approval: options.approval,
1303
+ scope: draft.scope,
1304
+ pairs: draft.pairs,
1305
+ totals: draft.totals,
1306
+ notes: draft.notes,
1307
+ ...draft.remaining === void 0 ? {} : { remaining: draft.remaining }
1308
+ };
1309
+ let reportPath;
1310
+ try {
1311
+ reportPath = await writeMergeReport(options.io, options.home, report);
1312
+ } catch (error) {
1313
+ await options.registry.settle(handle.id, {
1314
+ state: "failed",
1315
+ failure: "report write failed: " + (error instanceof Error ? error.message : String(error))
1316
+ });
1317
+ return {
1318
+ ok: false,
1319
+ runId: handle.id,
1320
+ message: "report write failed"
1321
+ };
1322
+ }
1323
+ const runState = report.stopReason === "completed" ? "succeeded" : report.stopReason === "cancelled" ? "cancelled" : "failed";
1324
+ await options.registry.settle(handle.id, {
1325
+ state: runState,
1326
+ resultRef: reportPath,
1327
+ ...runState === "failed" ? { failure: "merge " + report.stopReason } : {}
1328
+ });
1329
+ if (report.stopReason === "completed") return {
1330
+ ok: true,
1331
+ runId: handle.id,
1332
+ reportPath,
1333
+ report
1334
+ };
1335
+ return {
1336
+ ok: false,
1337
+ runId: handle.id,
1338
+ message: "merge " + report.stopReason,
1339
+ reportPath
1340
+ };
1341
+ }
1342
+ //#endregion
1343
+ //#region lib/types/scan-command.js
1344
+ async function runScanCommand(options) {
1345
+ return await withMergeRun({
1346
+ io: options.io,
1347
+ home: options.home,
1348
+ registry: options.registry,
1349
+ command: "scan",
1350
+ approval: {
1351
+ platformWrite: options.platformWrite,
1352
+ mergeApproval: options.approval
1353
+ },
1354
+ now: options.now,
1355
+ async body(ctx) {
1356
+ const ports = options.makePorts === void 0 ? options.ports : options.makePorts(ctx.runId);
1357
+ if (ports === void 0) throw new Error("scan requires ports or makePorts");
1358
+ return await runScan({
1359
+ io: options.io,
1360
+ home: options.home,
1361
+ now: options.now,
1362
+ signal: ctx.signal,
1363
+ approval: options.approval,
1364
+ ports,
1365
+ stage: ctx.stage,
1366
+ startedAt: ctx.startedAt,
1367
+ ...options.timeoutMinutes === void 0 ? {} : { timeoutMinutes: options.timeoutMinutes }
1368
+ });
1369
+ }
1370
+ });
1371
+ }
1372
+ //#endregion
1373
+ //#region lib/types/pending-actions.js
1374
+ async function act(io, home, now, id, type) {
1375
+ return await mutateMergeState(io, home, now, (rows) => {
1376
+ const index = rows.findIndex((row) => row.id === id);
1377
+ if (index < 0) return {
1378
+ rows,
1379
+ result: {
1380
+ ok: false,
1381
+ reason: "not-found",
1382
+ message: "pending row not found: " + id
1383
+ }
1384
+ };
1385
+ const current = rows[index];
1386
+ if (current.state !== "pending") return {
1387
+ rows,
1388
+ result: {
1389
+ ok: false,
1390
+ reason: "not-pending",
1391
+ message: "row is not pending: " + current.state
1392
+ }
1393
+ };
1394
+ const next = transition(current, type === "approve" ? {
1395
+ type: "approve",
1396
+ at: new Date(now()).toISOString()
1397
+ } : {
1398
+ type: "reject",
1399
+ at: new Date(now()).toISOString()
1400
+ });
1401
+ if (!next.ok || next.row === null) return {
1402
+ rows,
1403
+ result: {
1404
+ ok: false,
1405
+ reason: "invalid",
1406
+ message: next.ok ? "transition produced no row" : next.message
1407
+ }
1408
+ };
1409
+ const copy = [...rows];
1410
+ copy[index] = next.row;
1411
+ return {
1412
+ rows: copy,
1413
+ result: {
1414
+ ok: true,
1415
+ row: next.row
1416
+ }
1417
+ };
1418
+ });
1419
+ }
1420
+ async function approvePending(io, home, now, id) {
1421
+ return await act(io, home, now, id, "approve");
1422
+ }
1423
+ async function rejectPending(io, home, now, id) {
1424
+ return await act(io, home, now, id, "reject");
1425
+ }
1426
+ //#endregion
1427
+ //#region lib/types/round2-outcome.js
1428
+ async function recordRound2Outcome(io, home, now, row, outcome) {
1429
+ const at = new Date(now()).toISOString();
1430
+ if (outcome.kind === "merged") {
1431
+ if (transition(row, {
1432
+ type: "round2-success",
1433
+ at
1434
+ }).ok) await dropRow(io, home, now, row.id);
1435
+ return {
1436
+ merged: true,
1437
+ pair: {
1438
+ id: row.id,
1439
+ pairKey: row.pairKey,
1440
+ a: row.a,
1441
+ b: row.b,
1442
+ queue: row.queue,
1443
+ ...row.decision === void 0 ? {} : { decision: row.decision },
1444
+ outcome: "merged",
1445
+ archived: outcome.archived ?? [],
1446
+ ...outcome.unitId === void 0 ? {} : { unitId: outcome.unitId },
1447
+ ...outcome.target === void 0 ? {} : { target: outcome.target }
1448
+ }
1449
+ };
1450
+ }
1451
+ const failure = outcome.failure ?? {
1452
+ stage: "author",
1453
+ code: "merge-failed",
1454
+ message: "round2 failed",
1455
+ retryable: true,
1456
+ retryFrom: "agent2"
1457
+ };
1458
+ const next = transition(row, {
1459
+ type: "round2-fail",
1460
+ failure,
1461
+ at
1462
+ });
1463
+ if (next.ok && next.row !== null) await upsertRow(io, home, now, next.row);
1464
+ return {
1465
+ merged: false,
1466
+ pair: {
1467
+ id: row.id,
1468
+ pairKey: row.pairKey,
1469
+ a: row.a,
1470
+ b: row.b,
1471
+ queue: row.queue,
1472
+ ...row.decision === void 0 ? {} : { decision: row.decision },
1473
+ outcome: "failed",
1474
+ archived: [],
1475
+ failure
1476
+ }
1477
+ };
1478
+ }
1479
+ //#endregion
1480
+ //#region lib/types/approve-command.js
1481
+ async function runApproveCommand(options) {
1482
+ const taken = await approvePending(options.io, options.home, options.now, options.rowId);
1483
+ if (!taken.ok) return {
1484
+ ok: false,
1485
+ message: taken.message
1486
+ };
1487
+ const row = taken.row;
1488
+ const decision = row.decision;
1489
+ if (decision === void 0) return {
1490
+ ok: false,
1491
+ message: "pending row has no decision"
1492
+ };
1493
+ return await withMergeRun({
1494
+ io: options.io,
1495
+ home: options.home,
1496
+ registry: options.registry,
1497
+ command: "approve",
1498
+ approval: {
1499
+ platformWrite: options.platformWrite,
1500
+ mergeApproval: true
1501
+ },
1502
+ now: options.now,
1503
+ async body(ctx) {
1504
+ const stage = (name) => ctx.stage(name, {
1505
+ done: 0,
1506
+ total: 1
1507
+ });
1508
+ await stage("author");
1509
+ let outcome;
1510
+ try {
1511
+ outcome = await options.ports.round2(row, decision, ctx.signal, ctx.runId);
1512
+ } catch (error) {
1513
+ outcome = {
1514
+ kind: "failed",
1515
+ failure: {
1516
+ stage: "author",
1517
+ code: "merge-failed",
1518
+ message: error instanceof Error ? error.message : String(error),
1519
+ retryable: true,
1520
+ retryFrom: "agent2"
1521
+ }
1522
+ };
1523
+ }
1524
+ const base = {
1525
+ scope: {
1526
+ candidatePairs: 1,
1527
+ queueBefore: {
1528
+ pending: 0,
1529
+ retry: 1,
1530
+ skipped: 0
1531
+ }
1532
+ },
1533
+ notes: []
1534
+ };
1535
+ const record = await recordRound2Outcome(options.io, options.home, options.now, row, outcome);
1536
+ return {
1537
+ ...base,
1538
+ stopReason: record.merged ? "completed" : "failed",
1539
+ pairs: [record.pair],
1540
+ totals: record.merged ? {
1541
+ merged: 1,
1542
+ kept: 0,
1543
+ pending: 0,
1544
+ failed: 0
1545
+ } : {
1546
+ merged: 0,
1547
+ kept: 0,
1548
+ pending: 0,
1549
+ failed: 1
1550
+ }
1551
+ };
1552
+ }
1553
+ });
1554
+ }
1555
+ //#endregion
1556
+ //#region lib/types/resume-command.js
1557
+ async function runResumeCommand(options) {
1558
+ const row = (await loadMergeState(options.io, options.home)).state.rows.find((item) => item.id === options.rowId);
1559
+ if (row === void 0) return {
1560
+ ok: false,
1561
+ message: "merge row not found: " + options.rowId
1562
+ };
1563
+ if (row.state === "verify") return await runScanCommand({
1564
+ io: options.io,
1565
+ home: options.home,
1566
+ registry: options.registry,
1567
+ approval: true,
1568
+ platformWrite: options.platformWrite,
1569
+ now: options.now,
1570
+ ...options.makePorts === void 0 ? { ports: {
1571
+ ...options.ports,
1572
+ screening: () => Promise.resolve([{
1573
+ a: row.a,
1574
+ b: row.b
1575
+ }])
1576
+ } } : { makePorts: (runId) => {
1577
+ const makePorts = options.makePorts;
1578
+ if (makePorts === void 0) throw new Error("resume makePorts disappeared");
1579
+ return {
1580
+ ...makePorts(runId),
1581
+ screening: () => Promise.resolve([{
1582
+ a: row.a,
1583
+ b: row.b
1584
+ }])
1585
+ };
1586
+ } },
1587
+ ...options.timeoutMinutes === void 0 ? {} : { timeoutMinutes: options.timeoutMinutes }
1588
+ });
1589
+ if (row.state !== "fuse") return {
1590
+ ok: false,
1591
+ message: "row is not resumable from state " + row.state
1592
+ };
1593
+ const decision = row.decision;
1594
+ if (decision === void 0) return {
1595
+ ok: false,
1596
+ message: "fuse row has no decision"
1597
+ };
1598
+ return await withMergeRun({
1599
+ io: options.io,
1600
+ home: options.home,
1601
+ registry: options.registry,
1602
+ command: "resume",
1603
+ approval: {
1604
+ platformWrite: options.platformWrite,
1605
+ mergeApproval: true
1606
+ },
1607
+ now: options.now,
1608
+ async body(ctx) {
1609
+ await ctx.stage("author", {
1610
+ done: 0,
1611
+ total: 1
1612
+ });
1613
+ let outcome;
1614
+ try {
1615
+ outcome = options.round2WithRun === void 0 ? await options.ports.round2(row, decision, ctx.signal) : await options.round2WithRun(row, decision, ctx.signal, ctx.runId);
1616
+ } catch (error) {
1617
+ outcome = {
1618
+ kind: "failed",
1619
+ failure: {
1620
+ stage: "author",
1621
+ code: "merge-failed",
1622
+ message: error instanceof Error ? error.message : String(error),
1623
+ retryable: true,
1624
+ retryFrom: "agent2"
1625
+ }
1626
+ };
1627
+ }
1628
+ const record = await recordRound2Outcome(options.io, options.home, options.now, row, outcome);
1629
+ return {
1630
+ stopReason: record.merged ? "completed" : "failed",
1631
+ scope: {
1632
+ candidatePairs: 1,
1633
+ queueBefore: {
1634
+ pending: 0,
1635
+ retry: 1,
1636
+ skipped: 0
1637
+ }
1638
+ },
1639
+ pairs: [record.pair],
1640
+ totals: record.merged ? {
1641
+ merged: 1,
1642
+ kept: 0,
1643
+ pending: 0,
1644
+ failed: 0
1645
+ } : {
1646
+ merged: 0,
1647
+ kept: 0,
1648
+ pending: 0,
1649
+ failed: 1
1650
+ },
1651
+ notes: []
1652
+ };
1653
+ }
1654
+ });
1655
+ }
1656
+ //#endregion
1657
+ //#region lib/types/staging.js
1658
+ /**
1659
+ * Staging is a run ARTIFACT, not a queue: <home>/merge/runs/<runId>/.
1660
+ *
1661
+ * createStaging writes a sibling .partial tree first and renames it into place; the .stage
1662
+ * marker is the last write inside the partial. A directory without that marker is a
1663
+ * half-created artifact and reads as unknown, never as "empty".
1664
+ */
1665
+ const STAGING_SCHEMA_VERSION = 1;
1666
+ function stageKeyOf(runId, pairKey) {
1667
+ return runId + "/pairs/" + createHash("sha256").update(pairKey).digest("hex").slice(0, 16);
1668
+ }
1669
+ function stagingRoot(home, runId) {
1670
+ return join(home, "merge", "runs", runId);
1671
+ }
1672
+ /** Relative support paths only; the staging tree must never escape its run directory. */
1673
+ function safeRelative$2(path) {
1674
+ if (path === "" || path.startsWith("/") || path.startsWith("\\") || path.includes("\0")) return false;
1675
+ return path.replace(/\\/g, "/").split("/").every((part) => part !== "" && part !== "." && part !== "..");
1676
+ }
1677
+ function manifestText(manifest) {
1678
+ return JSON.stringify(manifest, null, 2) + "\n";
1679
+ }
1680
+ async function createStaging(io, home, input) {
1681
+ const final = stagingRoot(home, input.runId);
1682
+ if (await io.exists(final)) return {
1683
+ ok: false,
1684
+ message: "staging already exists for run " + input.runId
1685
+ };
1686
+ const names = /* @__PURE__ */ new Set();
1687
+ for (const source of input.sources) {
1688
+ if (names.has(source.name)) return {
1689
+ ok: false,
1690
+ message: "duplicate source " + source.name
1691
+ };
1692
+ names.add(source.name);
1693
+ for (const file of source.files) if (!safeRelative$2(file.path)) return {
1694
+ ok: false,
1695
+ message: "unsafe support path: " + file.path
1696
+ };
1697
+ }
1698
+ const partial = final + ".partial";
1699
+ try {
1700
+ await io.remove(partial);
1701
+ for (const source of input.sources) {
1702
+ const dir = join(partial, "sources", source.name);
1703
+ await io.writeText(join(dir, "SKILL.md"), source.body);
1704
+ for (const file of source.files) await io.writeText(join(dir, ...file.path.split("/")), file.content);
1705
+ }
1706
+ const at = input.now();
1707
+ const manifest = {
1708
+ schemaVersion: 1,
1709
+ runId: input.runId,
1710
+ target: input.target,
1711
+ ...input.mode === void 0 ? {} : { mode: input.mode },
1712
+ sources: input.sources.map((source) => source.name),
1713
+ promptVersion: input.promptVersion,
1714
+ createdAt: at,
1715
+ expiresAt: at + Math.max(0, input.ttlDays) * 864e5
1716
+ };
1717
+ await io.writeText(join(partial, "manifest.json"), manifestText(manifest));
1718
+ await io.writeText(join(partial, ".stage"), "");
1719
+ await io.rename(partial, final);
1720
+ return {
1721
+ ok: true,
1722
+ path: final,
1723
+ manifest
1724
+ };
1725
+ } catch (error) {
1726
+ try {
1727
+ await io.remove(partial);
1728
+ } catch {}
1729
+ return {
1730
+ ok: false,
1731
+ message: "staging create failed: " + (error instanceof Error ? error.message : String(error))
1732
+ };
1733
+ }
1734
+ }
1735
+ async function readStaging(io, home, runId) {
1736
+ const path = stagingRoot(home, runId);
1737
+ if (!await io.exists(path)) return probeAbsent();
1738
+ if (!await io.exists(join(path, ".stage"))) return probeUnknown("staging " + runId + " has no .stage marker (half-created)");
1739
+ const probe = await probeText(io, join(path, "manifest.json"));
1740
+ if (!isPresent(probe)) return probe;
1741
+ try {
1742
+ const parsed = JSON.parse(probe.value);
1743
+ if (parsed.schemaVersion !== 1 || parsed.runId !== runId || typeof parsed.target !== "string" || !Array.isArray(parsed.sources)) return probeUnknown("staging manifest has an unexpected shape");
1744
+ return probePresent({
1745
+ path,
1746
+ manifest: parsed
1747
+ });
1748
+ } catch (error) {
1749
+ return probeUnknown("staging manifest is not JSON: " + (error instanceof Error ? error.message : String(error)));
1750
+ }
1751
+ }
1752
+ async function ensureStaging(io, home, input) {
1753
+ const existing = await readStaging(io, home, input.runId);
1754
+ if (existing.kind === "present") return {
1755
+ ok: true,
1756
+ path: stagingRoot(home, input.runId),
1757
+ manifest: existing.value.manifest
1758
+ };
1759
+ return createStaging(io, home, input);
1760
+ }
1761
+ async function setStagingTarget(io, home, stageKey, target, mode) {
1762
+ const view = await readStaging(io, home, stageKey);
1763
+ if (view.kind !== "present") return false;
1764
+ const manifest = Object.assign({}, view.value.manifest, { target }, mode === void 0 ? {} : { mode });
1765
+ await io.writeText(join(stagingRoot(home, stageKey), "manifest.json"), JSON.stringify(manifest, null, 2) + "\n");
1766
+ return true;
1767
+ }
1768
+ async function dropStaging(io, home, stageKey) {
1769
+ await io.remove(stagingRoot(home, stageKey));
1770
+ }
1771
+ //#endregion
1772
+ //#region lib/types/commit-receipt.js
1773
+ /**
1774
+ * Commit receipt: <run>/commit.json, written immediately after a successful commit.
1775
+ * It is the idempotency/recovery marker — merge resume and run start read it before
1776
+ * replaying a commit. Receipt write failure must not turn a landed commit into a failure;
1777
+ * the caller reports the warning instead.
1778
+ */
1779
+ function commitReceiptPath(home, runId) {
1780
+ return join(stagingRoot(home, runId), "commit.json");
1781
+ }
1782
+ function durableReceiptDir(home, parentRunId) {
1783
+ return join(home, "merge", "receipts", parentRunId);
1784
+ }
1785
+ function durableReceiptPath(home, parentRunId, stageKey) {
1786
+ const leaf = stageKey.split("/").pop() ?? stageKey;
1787
+ return join(durableReceiptDir(home, parentRunId), leaf + ".json");
1788
+ }
1789
+ function parseReceipt(raw) {
1790
+ if (raw === null) return null;
1791
+ try {
1792
+ const parsed = JSON.parse(raw);
1793
+ 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;
1794
+ return parsed;
1795
+ } catch {
1796
+ return null;
1797
+ }
1798
+ }
1799
+ async function writeCommitReceipt(io, home, receipt) {
1800
+ const path = commitReceiptPath(home, receipt.runId);
1801
+ await io.writeText(path, JSON.stringify(receipt, null, 2) + "\n");
1802
+ const parent = receipt.parentRunId ?? receipt.runId.split("/pairs/")[0] ?? receipt.runId;
1803
+ await io.writeText(durableReceiptPath(home, parent, receipt.runId), JSON.stringify({
1804
+ ...receipt,
1805
+ parentRunId: parent
1806
+ }, null, 2) + "\n");
1807
+ return path;
1808
+ }
1809
+ async function readCommitReceipt(io, home, runId, target) {
1810
+ const direct = parseReceipt(await io.readText(commitReceiptPath(home, runId)));
1811
+ if (direct !== null && (target === void 0 || direct.target === target)) return direct;
1812
+ try {
1813
+ const names = await io.list(durableReceiptDir(home, runId));
1814
+ for (const name of names.sort()) {
1815
+ const receipt = parseReceipt(await io.readText(join(durableReceiptDir(home, runId), name)));
1816
+ if (receipt !== null && (target === void 0 || receipt.target === target)) return receipt;
1817
+ }
1818
+ } catch {}
1819
+ return null;
1820
+ }
1821
+ //#endregion
1822
+ //#region lib/types/undo-merge.js
1823
+ async function undoMergeReceipt(receipt, port) {
1824
+ const targetVersions = (await port.listVersions(receipt.target)).filter((entry) => entry.unitId === receipt.unitId);
1825
+ if (targetVersions.length === 0) return {
1826
+ ok: false,
1827
+ message: "unit " + receipt.unitId + " is not in the target history; refusing to guess",
1828
+ reverted: []
1829
+ };
1830
+ const members = [{
1831
+ skill: receipt.target,
1832
+ entries: targetVersions,
1833
+ status: "live",
1834
+ ...receipt.create ? { createdInUnit: true } : {}
1835
+ }];
1836
+ for (const source of receipt.sources) {
1837
+ const live = await port.readSkill(source);
1838
+ members.push({
1839
+ skill: source,
1840
+ entries: (await port.listVersions(source)).filter((entry) => entry.unitId === receipt.unitId),
1841
+ status: live === null ? "archived" : "live",
1842
+ ...live === null ? { archivedPath: "archive" } : {}
1843
+ });
1844
+ }
1845
+ const plan = planUnitUndoAcross(members);
1846
+ if (plan.refusals.length > 0) return {
1847
+ ok: false,
1848
+ message: "merge undo preflight refused: " + plan.refusals.join("; "),
1849
+ reverted: []
1850
+ };
1851
+ return await executeUnitUndo(plan, port);
1852
+ }
1853
+ //#endregion
1854
+ //#region lib/types/adapters/undo.js
1855
+ /**
1856
+ * SkillLibrary-backed undo sink. Blobs are content-addressed but the library exposes them by
1857
+ * version, so the first readBlob call indexes all version bodies once; an undo is rare enough
1858
+ * that this is preferable to adding a second hash index.
1859
+ */
1860
+ function skillLibraryUndoPort(skills) {
1861
+ let blobs;
1862
+ const readBlob = async (hash) => {
1863
+ if (blobs === void 0) {
1864
+ blobs = /* @__PURE__ */ new Map();
1865
+ for (const summary of await skills.list()) for (const version of await skills.listVersions(summary.name)) {
1866
+ const body = await skills.readVersion(summary.name, version.v);
1867
+ if (body !== null) blobs.set(contentHash(body), body);
1868
+ }
1869
+ }
1870
+ return blobs.get(hash) ?? null;
1871
+ };
1872
+ return {
1873
+ readSkill: (name) => skills.read(name),
1874
+ readSupport: (name, path) => skills.readSupportFile(name, path),
1875
+ readBlob,
1876
+ async hasArchived(name) {
1877
+ return await skills.read(name) === null;
1878
+ },
1879
+ restoreFromArchive: (name) => skills.restoreFromArchive(name),
1880
+ archive: (name, reason) => skills.archive(name, { reason }),
1881
+ updateSkill: (name, body, anchor) => skills.update(name, body, void 0, anchor),
1882
+ writeSupportFile: (name, path, content, anchor) => skills.writeSupportFile(name, path, content, void 0, anchor),
1883
+ removeSupportFile: (name, path, anchor) => skills.removeSupportFile(name, path, void 0, anchor),
1884
+ listVersions: (name) => skills.listVersions(name)
1885
+ };
1886
+ }
1887
+ //#endregion
1888
+ //#region lib/types/doctor.js
1889
+ /**
1890
+ * Merge doctor rows: state, pending, gate cache and staging are each probed read-only.
1891
+ * A missing file is an empty feature, not a failure; an unreadable file is a warning the
1892
+ * operator can act on. This module never repairs or writes.
1893
+ */
1894
+ const MERGE_DOCTOR_SCHEMA_VERSION = 3;
1895
+ async function mergeDoctor(io, home, configuredRoot) {
1896
+ const rows = [];
1897
+ const loaded = await loadMergeState(io, home);
1898
+ const counts = countRows(loaded.state.rows);
1899
+ rows.push({
1900
+ key: "state",
1901
+ label: "合并状态",
1902
+ status: loaded.note === void 0 ? "ok" : "warn",
1903
+ detail: loaded.note ?? "pairs " + String(loaded.state.rows.length) + ";pending " + String(counts.pending) + " / retry " + String(counts.retry) + " / skipped " + String(counts.skipped)
1904
+ });
1905
+ const modelRoot = configuredRoot !== void 0 && configuredRoot !== "" ? configuredRoot : process.env.SKILL_MERGE_MODELS_ROOT;
1906
+ if (modelRoot === void 0 || modelRoot === "") rows.push({
1907
+ key: "models",
1908
+ label: "合并模型",
1909
+ status: "warn",
1910
+ detail: "未设置 SKILL_MERGE_MODELS_ROOT;合并扫描会 fail loud"
1911
+ });
1912
+ else {
1913
+ const int8 = join(modelRoot, "potion-multilingual-128M-int8");
1914
+ const pca = join(modelRoot, "potion-multilingual-128M-pca64-int8");
1915
+ const ok = await io.exists(int8) && await io.exists(pca);
1916
+ rows.push({
1917
+ key: "models",
1918
+ label: "合并模型",
1919
+ status: ok ? "ok" : "warn",
1920
+ detail: ok ? modelRoot : "缺少 int8/pca64:" + modelRoot
1921
+ });
1922
+ }
1923
+ const runsDir = join(home, "merge", "runs");
1924
+ const listed = await probeList(io, runsDir);
1925
+ rows.push({
1926
+ key: "staging",
1927
+ label: "暂存树",
1928
+ status: listed.kind === "unknown" ? "unknown" : "ok",
1929
+ detail: listed.kind === "unknown" ? "无法列出 " + runsDir + ":" + listed.reason : listed.kind === "absent" ? "0 个 run 目录" : String(listed.value.length) + " 个 run 目录"
1930
+ });
1931
+ rows.push({
1932
+ key: "staging-path",
1933
+ label: "暂存路径",
1934
+ status: "ok",
1935
+ detail: stagingRoot(home, "<runId>")
1936
+ });
1937
+ return rows;
1938
+ }
1939
+ //#endregion
1940
+ //#region lib/types/decision.js
1941
+ function extractJson(text) {
1942
+ const body = /\`\`\`(?:json)?\s*([\s\S]*?)\`\`\`/i.exec(text)?.[1] ?? text;
1943
+ const start = body.indexOf("{");
1944
+ if (start < 0) return void 0;
1945
+ let depth = 0;
1946
+ for (let index = start; index < body.length; index += 1) if (body[index] === "{") depth += 1;
1947
+ else if (body[index] === "}") {
1948
+ depth -= 1;
1949
+ if (depth === 0) try {
1950
+ return JSON.parse(body.slice(start, index + 1));
1951
+ } catch {
1952
+ return;
1953
+ }
1954
+ }
1955
+ }
1956
+ function parseDecision(text, pair) {
1957
+ if (text === void 0 || text.trim() === "") return {
1958
+ ok: false,
1959
+ code: "decision-invalid",
1960
+ message: "empty decision text"
1961
+ };
1962
+ const value = extractJson(text);
1963
+ if (typeof value !== "object" || value === null) return {
1964
+ ok: false,
1965
+ code: "decision-invalid",
1966
+ message: "decision is not a JSON object"
1967
+ };
1968
+ const record = value;
1969
+ const shouldMerge = record.shouldMerge === true || record.should_merge === true;
1970
+ const mode = record.mode;
1971
+ if (mode !== "merge-into-existing" && mode !== "create-umbrella" && mode !== "keep") return {
1972
+ ok: false,
1973
+ code: "decision-invalid",
1974
+ message: "mode must be merge-into-existing, create-umbrella or keep"
1975
+ };
1976
+ const target = typeof record.target === "string" ? record.target.trim() : "";
1977
+ const sources = Array.isArray(record.sources) ? record.sources.filter((item) => typeof item === "string").map((item) => item.trim()).filter(Boolean) : [];
1978
+ const rationale = typeof record.rationale === "string" ? record.rationale.trim() : "";
1979
+ if (!shouldMerge || mode === "keep") return {
1980
+ ok: true,
1981
+ decision: {
1982
+ shouldMerge: false,
1983
+ mode: "keep",
1984
+ target,
1985
+ sources,
1986
+ rationale
1987
+ }
1988
+ };
1989
+ if (target === "") return {
1990
+ ok: false,
1991
+ code: "decision-invalid",
1992
+ message: "merge decision needs target"
1993
+ };
1994
+ if (sources.length === 0) return {
1995
+ ok: false,
1996
+ code: "decision-invalid",
1997
+ message: "merge decision needs sources"
1998
+ };
1999
+ if (!sources.includes(pair.a) || !sources.includes(pair.b)) return {
2000
+ ok: false,
2001
+ code: "decision-invalid",
2002
+ message: "sources must include both pair skills"
2003
+ };
2004
+ return {
2005
+ ok: true,
2006
+ decision: {
2007
+ shouldMerge: true,
2008
+ mode,
2009
+ target,
2010
+ sources,
2011
+ rationale,
2012
+ ...typeof record.confidence === "number" ? { confidence: record.confidence } : {}
2013
+ }
2014
+ };
2015
+ }
2016
+ //#endregion
2017
+ //#region lib/types/prompts.js
2018
+ const MERGE_PROMPT_VERSION = "merge-b/v1";
2019
+ const STANDBY_PROMPT = "You are a merge worker. Wait for instructions.";
2020
+ function sourcePath(name) {
2021
+ return "sources/" + name;
2022
+ }
2023
+ function mergedPath(target) {
2024
+ return "merged/" + target;
2025
+ }
2026
+ function wireDecision(decision) {
2027
+ return {
2028
+ shouldMerge: decision.shouldMerge,
2029
+ mode: decision.mode,
2030
+ target: decision.target,
2031
+ sources: decision.sources,
2032
+ rationale: decision.rationale,
2033
+ confidence: decision.confidence,
2034
+ a_summary: decision.aSummary,
2035
+ b_summary: decision.bSummary
2036
+ };
2037
+ }
2038
+ function buildRound1Prompt(candidate, cards) {
2039
+ return [
2040
+ "你是技能合并决策 agent。第 1 轮只做判断,不要写文件、不要修改任何源目录。",
2041
+ "",
2042
+ "在当前工作目录下用 merge_stage 的 list/read 完整阅读两个源技能:",
2043
+ "- A: " + sourcePath(candidate.a) + "(name=" + candidate.a + ")",
2044
+ "- B: " + sourcePath(candidate.b) + "(name=" + candidate.b + ")",
2045
+ "包含 SKILL.md 与 references/templates/scripts 等支撑文件。",
2046
+ "",
2047
+ "判断规则:",
2048
+ "1) 两者是否语义等价/互补到值得合并,而不是词面相似;",
2049
+ "2) 合并后是否更容易发现和使用,而不是变成更大的巨技能;",
2050
+ "3) 若合并会破坏触发面/生命周期/可发现性,应输出 keep。",
2051
+ "",
2052
+ "只输出一个 JSON(不要 markdown 围栏):",
2053
+ "{\"should_merge\":true|false,\"mode\":\"merge-into-existing|create-umbrella|keep\",\"target\":\"kebab-case\",\"sources\":[\"源技能名\"],\"rationale\":\"<=150字\",\"confidence\":0.0,\"a_summary\":\"A 的一句话摘要\",\"b_summary\":\"B 的一句话摘要\"}"
2054
+ ].join("\n");
2055
+ }
2056
+ function buildRound2Prompt(row, decision, mode = "continue") {
2057
+ const decisionJson = JSON.stringify(wireDecision(decision));
2058
+ const head = [
2059
+ "现在执行第 2 轮:把两个技能融合成一个完整、可用的技能,直接在当前工作目录的 merged/ 下写文件。",
2060
+ "不要修改 " + sourcePath(row.a) + " 或 " + sourcePath(row.b) + "(只读源副本);不要访问工作目录以外的路径。",
2061
+ ""
2062
+ ];
2063
+ const body = [
2064
+ "决策结果:" + decisionJson,
2065
+ "目标技能名:" + decision.target,
2066
+ "",
2067
+ "工作方式:",
2068
+ "- 用 merge_stage 的 list/read/write/copy 创建 " + mergedPath(decision.target) + "/SKILL.md 与需要的 references/templates/scripts;",
2069
+ "- 可以复制源文件作为草稿基础,然后逐段修改;可以多轮读写;",
2070
+ "- 保留两源独特内容,不编造事实,不留下 TODO/占位符;",
2071
+ "- SKILL.md 以合法 YAML frontmatter 开头,name 等于目标名,description 不超过 60 字符;",
2072
+ "- 正文与支撑文件的引用必须成对,路径只允许 references/templates/scripts/assets 下的相对路径。",
2073
+ "",
2074
+ "完成后只输出一个 JSON(不要 markdown 围栏):",
2075
+ "{\"target\":\"" + decision.target + "\",\"files\":[\"SKILL.md\",\"references/...\"],\"changelog\":[\"一条变更\"],\"notes\":\"可选的一句话\"}"
2076
+ ];
2077
+ return (mode === "fresh" ? [
2078
+ "基于已持久化的 decision。",
2079
+ ...head,
2080
+ ...body
2081
+ ] : [...head, ...body]).join("\n");
2082
+ }
2083
+ function buildStandbyPrompt() {
2084
+ return STANDBY_PROMPT;
2085
+ }
2086
+ //#endregion
2087
+ //#region lib/types/channel-port.js
2088
+ async function runRound1OverChannel(options, candidate, signal) {
2089
+ let childId;
2090
+ const dispose = async () => {
2091
+ if (childId !== void 0) {
2092
+ const id = childId;
2093
+ childId = void 0;
2094
+ try {
2095
+ await options.channel.dispose(id);
2096
+ } catch {}
2097
+ }
2098
+ };
2099
+ const opened = await options.channel.open({
2100
+ parent: options.parent,
2101
+ label: "merge-round1",
2102
+ promptText: STANDBY_PROMPT,
2103
+ kind: "reviewer",
2104
+ allow: options.allow,
2105
+ ...options.cwd === void 0 ? {} : { cwd: options.cwd },
2106
+ timeoutMs: options.timeoutMs,
2107
+ signal
2108
+ });
2109
+ if (opened.kind === "unavailable") return {
2110
+ outcome: {
2111
+ kind: "failed",
2112
+ failure: {
2113
+ stage: "decide",
2114
+ code: opened.code,
2115
+ message: opened.reason,
2116
+ retryable: true,
2117
+ retryFrom: "agent1"
2118
+ }
2119
+ },
2120
+ dispose
2121
+ };
2122
+ if (opened.kind === "aborted") return {
2123
+ outcome: {
2124
+ kind: "failed",
2125
+ failure: {
2126
+ stage: "decide",
2127
+ code: "aborted",
2128
+ message: "round1 aborted",
2129
+ retryable: false,
2130
+ retryFrom: "none"
2131
+ }
2132
+ },
2133
+ dispose
2134
+ };
2135
+ childId = opened.childId;
2136
+ if (opened.turn.stopReason !== "completed") {
2137
+ await dispose();
2138
+ return {
2139
+ outcome: {
2140
+ kind: "failed",
2141
+ failure: {
2142
+ stage: "decide",
2143
+ code: opened.turn.stopReason,
2144
+ message: "standby did not complete",
2145
+ retryable: true,
2146
+ retryFrom: "agent1"
2147
+ }
2148
+ },
2149
+ dispose
2150
+ };
2151
+ }
2152
+ try {
2153
+ options.onChildOpened?.(opened.childId);
2154
+ } catch (error) {
2155
+ await dispose();
2156
+ return {
2157
+ outcome: {
2158
+ kind: "failed",
2159
+ failure: {
2160
+ stage: "decide",
2161
+ code: "child-bind",
2162
+ message: String(error),
2163
+ retryable: true,
2164
+ retryFrom: "agent1"
2165
+ }
2166
+ },
2167
+ dispose
2168
+ };
2169
+ }
2170
+ const round1 = await options.channel.continueTurn(opened.childId, buildRound1Prompt(candidate, options.cards), {
2171
+ timeoutMs: options.timeoutMs,
2172
+ signal
2173
+ });
2174
+ if (round1.kind !== "ok") {
2175
+ await dispose();
2176
+ return {
2177
+ outcome: {
2178
+ kind: "failed",
2179
+ failure: {
2180
+ stage: "decide",
2181
+ code: round1.kind,
2182
+ message: round1.kind === "unavailable" ? round1.reason : "round1 aborted",
2183
+ retryable: round1.kind !== "aborted",
2184
+ retryFrom: round1.kind === "aborted" ? "none" : "agent1"
2185
+ }
2186
+ },
2187
+ dispose
2188
+ };
2189
+ }
2190
+ if (round1.turn.stopReason !== "completed") {
2191
+ await dispose();
2192
+ return {
2193
+ outcome: {
2194
+ kind: "failed",
2195
+ failure: {
2196
+ stage: "decide",
2197
+ code: round1.turn.stopReason,
2198
+ message: "round1 did not complete",
2199
+ retryable: true,
2200
+ retryFrom: "agent1"
2201
+ }
2202
+ },
2203
+ dispose
2204
+ };
2205
+ }
2206
+ const parsed = parseDecision(round1.turn.text, candidate);
2207
+ if (!parsed.ok) {
2208
+ await dispose();
2209
+ return {
2210
+ outcome: {
2211
+ kind: "failed",
2212
+ failure: {
2213
+ stage: "decide",
2214
+ code: parsed.code,
2215
+ message: parsed.message,
2216
+ retryable: true,
2217
+ retryFrom: "agent1"
2218
+ }
2219
+ },
2220
+ dispose
2221
+ };
2222
+ }
2223
+ if (!parsed.decision.shouldMerge || parsed.decision.mode === "keep") {
2224
+ await dispose();
2225
+ return {
2226
+ outcome: {
2227
+ kind: "keep",
2228
+ decision: parsed.decision
2229
+ },
2230
+ dispose
2231
+ };
2232
+ }
2233
+ const child = opened.childId;
2234
+ return {
2235
+ outcome: {
2236
+ kind: "merge",
2237
+ decision: parsed.decision
2238
+ },
2239
+ childId: child,
2240
+ dispose,
2241
+ async author(row, decision, turnSignal) {
2242
+ const turn = await options.channel.continueTurn(child, buildRound2Prompt(row, decision), {
2243
+ timeoutMs: options.timeoutMs,
2244
+ signal: turnSignal
2245
+ });
2246
+ if (turn.kind !== "ok") throw new Error(turn.kind === "unavailable" ? turn.reason : "round2 aborted");
2247
+ if (turn.turn.stopReason !== "completed") throw new Error("round2 did not complete: " + turn.turn.stopReason);
2248
+ return turn.turn.text;
2249
+ }
2250
+ };
2251
+ }
2252
+ //#endregion
2253
+ //#region lib/types/staged-tree.js
2254
+ /**
2255
+ * Read the merged tree a child wrote into a staging run.
2256
+ *
2257
+ * The tree is the commit source: SKILL.md is the body, every other file is a support file.
2258
+ * A missing SKILL.md is unknown, never an empty merge; the caller then refuses commit.
2259
+ */
2260
+ function safeRelative$1(path) {
2261
+ if (path === "" || path.startsWith("/") || path.startsWith("\\") || path.includes("\0")) return false;
2262
+ return path.replace(/\\/g, "/").split("/").every((part) => part !== "" && part !== "." && part !== "..");
2263
+ }
2264
+ async function walkFiles(io, root, rel = "") {
2265
+ const abs = rel === "" ? root : join(root, ...rel.split("/"));
2266
+ try {
2267
+ if (await io.readText(abs) !== null) return [rel];
2268
+ } catch {}
2269
+ const listed = await probeList(io, abs);
2270
+ if (listed.kind === "unknown") throw new Error("cannot list " + abs + ": " + listed.reason);
2271
+ if (listed.kind === "absent") return [];
2272
+ const out = [];
2273
+ for (const entry of listed.value) {
2274
+ if (entry === "" || entry === "." || entry === "..") continue;
2275
+ const child = rel === "" ? entry : rel + "/" + entry;
2276
+ if (!safeRelative$1(child)) continue;
2277
+ out.push(...await walkFiles(io, root, child));
2278
+ }
2279
+ return out;
2280
+ }
2281
+ async function readStagedTree(io, home, runId) {
2282
+ const staging = await readStaging(io, home, runId);
2283
+ if (staging.kind === "absent") return { kind: "absent" };
2284
+ if (staging.kind === "unknown") return probeUnknown(staging.reason);
2285
+ const target = staging.value.manifest.target;
2286
+ const root = join(stagingRoot(home, runId), "merged", target);
2287
+ let paths;
2288
+ try {
2289
+ paths = await walkFiles(io, root);
2290
+ } catch (error) {
2291
+ return probeUnknown("cannot list merged tree: " + (error instanceof Error ? error.message : String(error)));
2292
+ }
2293
+ if (!paths.includes("SKILL.md")) return probeUnknown("merged tree has no SKILL.md");
2294
+ const files = [];
2295
+ for (const path of paths.sort()) {
2296
+ if (path === "SKILL.md") continue;
2297
+ try {
2298
+ const content = await io.readText(join(root, ...path.split("/")));
2299
+ if (content === null) return probeUnknown("merged file disappeared: " + path);
2300
+ files.push({
2301
+ path,
2302
+ content
2303
+ });
2304
+ } catch (error) {
2305
+ return probeUnknown("cannot read merged file " + path + ": " + (error instanceof Error ? error.message : String(error)));
2306
+ }
2307
+ }
2308
+ const body = await io.readText(join(root, "SKILL.md"));
2309
+ if (body === null) return probeUnknown("merged SKILL.md disappeared");
2310
+ return {
2311
+ kind: "present",
2312
+ value: {
2313
+ target,
2314
+ body,
2315
+ files
2316
+ }
2317
+ };
2318
+ }
2319
+ //#endregion
2320
+ //#region lib/types/rules.js
2321
+ /**
2322
+ * Deterministic gate for a staged merge tree. No io, no model.
2323
+ *
2324
+ * block = commit must not proceed; warn = report only. The path and reference checks reuse
2325
+ * the skill-store's own `validateSupportPath` / `supportRefs`, so there is one whitelist.
2326
+ */
2327
+ const PLACEHOLDER = /\bTODO\b|\bxxx\b|\{\{[^}]*\}\}/i;
2328
+ const DESC_WARN_LENGTH = 60;
2329
+ function validateStagedTree(tree, limits = DEFAULT_SKILL_LIMITS) {
2330
+ const findings = [];
2331
+ const block = (rule, path, message) => {
2332
+ findings.push({
2333
+ rule,
2334
+ path,
2335
+ severity: "block",
2336
+ message
2337
+ });
2338
+ };
2339
+ const warn = (rule, path, message) => {
2340
+ findings.push({
2341
+ rule,
2342
+ path,
2343
+ severity: "warn",
2344
+ message
2345
+ });
2346
+ };
2347
+ const parsed = parseFrontmatter(tree.body);
2348
+ if (parsed === null) block("frontmatter", "SKILL.md", "SKILL.md has no valid frontmatter");
2349
+ else {
2350
+ const invalid = validateFrontmatter(tree.body, tree.target, limits);
2351
+ if (invalid !== null) block("frontmatter", "SKILL.md", invalid);
2352
+ if ((parsed.frontmatter.description ?? "").length > DESC_WARN_LENGTH) warn("desc-length", "SKILL.md", "description is longer than " + String(DESC_WARN_LENGTH) + " characters");
2353
+ }
2354
+ if (Buffer.byteLength(tree.body, "utf8") > limits.maxSkillFileBytes) block("bytes", "SKILL.md", "SKILL.md exceeds maxSkillFileBytes");
2355
+ if (scanContentThreats(tree.body) !== null) block("threat", "SKILL.md", "SKILL.md tripped the content threat scanner");
2356
+ if (PLACEHOLDER.test(tree.body)) warn("placeholder", "SKILL.md", "SKILL.md contains a TODO/xxx/{{...}} placeholder");
2357
+ const paths = /* @__PURE__ */ new Set();
2358
+ for (const file of tree.files) {
2359
+ const badPath = validateSupportPath(file.path);
2360
+ if (badPath !== null) {
2361
+ block("path", file.path, badPath);
2362
+ continue;
2363
+ }
2364
+ paths.add(file.path);
2365
+ if (Buffer.byteLength(file.content, "utf8") > limits.maxSkillFileBytes) block("bytes", file.path, "file exceeds maxSkillFileBytes");
2366
+ if (scanContentThreats(file.content) !== null) block("threat", file.path, "file tripped the content threat scanner");
2367
+ if (PLACEHOLDER.test(file.content)) warn("placeholder", file.path, "file contains a TODO/xxx/{{...}} placeholder");
2368
+ }
2369
+ for (const ref of supportRefs(tree.body)) if (!paths.has(ref)) block("refs-pair", ref, "body references a support file that is not staged");
2370
+ return findings;
2371
+ }
2372
+ function hasBlockingFinding(findings) {
2373
+ return findings.some((finding) => finding.severity === "block");
2374
+ }
2375
+ //#endregion
2376
+ //#region lib/types/commit-plan.js
2377
+ const MAX_SKILL_NAME_LENGTH = 64;
2378
+ /** Pick the first free target: requested, then -v2, -v3, ... (never truncate a name). */
2379
+ function resolveTargetName(requested, exists) {
2380
+ if (requested.length === 0 || requested.length > MAX_SKILL_NAME_LENGTH) return {
2381
+ ok: false,
2382
+ message: "target name must be 1.." + String(MAX_SKILL_NAME_LENGTH) + " characters"
2383
+ };
2384
+ if (!exists(requested)) return {
2385
+ ok: true,
2386
+ name: requested,
2387
+ suffix: 1
2388
+ };
2389
+ for (let suffix = 2; suffix <= 99; suffix += 1) {
2390
+ const candidate = requested + "-v" + String(suffix);
2391
+ if (candidate.length > MAX_SKILL_NAME_LENGTH) break;
2392
+ if (!exists(candidate)) return {
2393
+ ok: true,
2394
+ name: candidate,
2395
+ suffix
2396
+ };
2397
+ }
2398
+ return {
2399
+ ok: false,
2400
+ message: "no free target name for " + requested + " (tried up to -v99)"
2401
+ };
2402
+ }
2403
+ /** Rewrite only the frontmatter name; body bytes stay otherwise intact. */
2404
+ function rewriteFrontmatterName(body, name) {
2405
+ const lines = body.split("\n");
2406
+ if (lines[0]?.trim() !== "---") return body;
2407
+ const end = lines.findIndex((line, index) => index > 0 && line.trim() === "---");
2408
+ if (end < 0) return body;
2409
+ for (let index = 1; index < end; index += 1) if (/^name\s*:/.test(lines[index] ?? "")) {
2410
+ lines[index] = "name: " + name;
2411
+ return lines.join("\n");
2412
+ }
2413
+ return body;
2414
+ }
2415
+ function buildCommitPlan(input) {
2416
+ if (input.create !== (input.targetCurrent === null)) return {
2417
+ ok: false,
2418
+ message: "create flag disagrees with the live target"
2419
+ };
2420
+ const sources = input.sourceBodies.filter((source) => source.name !== input.target);
2421
+ if (sources.length === 0) return {
2422
+ ok: false,
2423
+ message: "a merge needs at least one source different from the target"
2424
+ };
2425
+ const body = rewriteFrontmatterName(input.mergedBody, input.target);
2426
+ return {
2427
+ ok: true,
2428
+ plan: {
2429
+ target: input.target,
2430
+ targetExists: !input.create,
2431
+ sources: sources.map((source) => source.name),
2432
+ body,
2433
+ supportFiles: input.mergedFiles,
2434
+ expectedTargetBody: input.targetCurrent,
2435
+ expectedSourceBodies: sources,
2436
+ origin: input.origin,
2437
+ summary: input.summary
2438
+ }
2439
+ };
2440
+ }
2441
+ //#endregion
2442
+ //#region lib/types/commit-assembly.js
2443
+ async function assembleStagedCommit(io, home, runId, port, options) {
2444
+ const staging = await readStaging(io, home, runId);
2445
+ if (!isPresent(staging)) return {
2446
+ ok: false,
2447
+ message: "staging is not present for run " + runId
2448
+ };
2449
+ const tree = await readStagedTree(io, home, runId);
2450
+ if (!isPresent(tree)) return {
2451
+ ok: false,
2452
+ message: "merged tree is not readable for run " + runId
2453
+ };
2454
+ const requested = staging.value.manifest.target;
2455
+ let target = requested;
2456
+ let targetCurrent = await port.readSkill(requested);
2457
+ const requestedArchived = targetCurrent === null && await port.isArchived?.(requested) === true;
2458
+ if (staging.value.manifest.mode === "create-umbrella" && (targetCurrent !== null || requestedArchived)) {
2459
+ target = requested;
2460
+ for (let suffix = 2; suffix <= 99; suffix += 1) {
2461
+ const candidate = requested + "-v" + String(suffix);
2462
+ if (await port.readSkill(candidate) === null && await port.isArchived?.(candidate) !== true) {
2463
+ target = candidate;
2464
+ targetCurrent = null;
2465
+ break;
2466
+ }
2467
+ }
2468
+ if (target === requested) return {
2469
+ ok: false,
2470
+ message: "no free umbrella target for " + requested
2471
+ };
2472
+ }
2473
+ const sources = [];
2474
+ for (const name of staging.value.manifest.sources) {
2475
+ const body = await port.readSkill(name);
2476
+ if (body === null) return {
2477
+ ok: false,
2478
+ message: "source skill is not live: " + name
2479
+ };
2480
+ sources.push({
2481
+ name,
2482
+ body
2483
+ });
2484
+ }
2485
+ return buildCommitPlan({
2486
+ target,
2487
+ create: targetCurrent === null,
2488
+ mergedBody: tree.value.body,
2489
+ mergedFiles: tree.value.files,
2490
+ targetCurrent,
2491
+ sourceBodies: sources,
2492
+ origin: options.origin,
2493
+ summary: options.summary
2494
+ });
2495
+ }
2496
+ //#endregion
2497
+ //#region lib/types/commit-execute.js
2498
+ async function commitStagedRun(io, home, runId, port, options) {
2499
+ const assembled = await assembleStagedCommit(io, home, runId, port, {
2500
+ origin: options.origin,
2501
+ summary: options.summary
2502
+ });
2503
+ if (!assembled.ok) return {
2504
+ ok: false,
2505
+ message: assembled.message
2506
+ };
2507
+ const result = await port.commitMerge(assembled.plan);
2508
+ if (!result.ok) return {
2509
+ ok: false,
2510
+ message: result.message,
2511
+ plan: assembled.plan
2512
+ };
2513
+ if (result.unitId === void 0) return {
2514
+ ok: true,
2515
+ plan: assembled.plan,
2516
+ result,
2517
+ warning: "commit landed without a unitId; receipt not written"
2518
+ };
2519
+ const receipt = {
2520
+ runId,
2521
+ parentRunId: runId.split("/pairs/")[0] ?? runId,
2522
+ unitId: result.unitId,
2523
+ target: assembled.plan.target,
2524
+ create: !assembled.plan.targetExists,
2525
+ sources: assembled.plan.sources,
2526
+ archived: result.archived ?? [],
2527
+ at: options.now()
2528
+ };
2529
+ try {
2530
+ await writeCommitReceipt(io, home, receipt);
2531
+ return {
2532
+ ok: true,
2533
+ plan: assembled.plan,
2534
+ result,
2535
+ receipt
2536
+ };
2537
+ } catch (error) {
2538
+ return {
2539
+ ok: true,
2540
+ plan: assembled.plan,
2541
+ result,
2542
+ warning: "commit landed but receipt write failed: " + (error instanceof Error ? error.message : String(error))
2543
+ };
2544
+ }
2545
+ }
2546
+ //#endregion
2547
+ //#region lib/types/authoring-flow.js
2548
+ async function runAuthoring(options) {
2549
+ const targetCard = options.cards.find((card) => card.name === options.target);
2550
+ if (targetCard?.protectionUnknown === true || targetCard?.protectedBy === "pinned") return {
2551
+ ok: false,
2552
+ message: "target is pinned: " + options.target
2553
+ };
2554
+ const stageKey = options.stageKey ?? options.runId;
2555
+ if (options.reuseStaging === true) {
2556
+ if (!await setStagingTarget(options.io, options.home, stageKey, options.target, options.mode)) return {
2557
+ ok: false,
2558
+ message: "staging missing or unreadable: " + stageKey
2559
+ };
2560
+ } else {
2561
+ const created = await createStaging(options.io, options.home, {
2562
+ runId: stageKey,
2563
+ target: options.target,
2564
+ ...options.mode === void 0 ? {} : { mode: options.mode },
2565
+ sources: options.cards.map((card) => ({
2566
+ name: card.name,
2567
+ body: card.body,
2568
+ files: card.files ?? []
2569
+ })),
2570
+ promptVersion: "merge-b/v1",
2571
+ ttlDays: 7,
2572
+ now: options.now
2573
+ });
2574
+ if (!created.ok) return {
2575
+ ok: false,
2576
+ message: created.message
2577
+ };
2578
+ }
2579
+ try {
2580
+ await options.author({
2581
+ home: options.home,
2582
+ runId: stageKey
2583
+ });
2584
+ } catch (error) {
2585
+ return {
2586
+ ok: false,
2587
+ message: "author failed: " + (error instanceof Error ? error.message : String(error))
2588
+ };
2589
+ }
2590
+ const tree = await readStagedTree(options.io, options.home, stageKey);
2591
+ if (tree.kind !== "present") return {
2592
+ ok: false,
2593
+ message: "staged tree is not readable: " + (tree.kind === "unknown" ? tree.reason : "absent")
2594
+ };
2595
+ const findings = validateStagedTree(tree.value);
2596
+ if (hasBlockingFinding(findings)) return {
2597
+ ok: false,
2598
+ message: "staged tree failed the rule gate",
2599
+ findings
2600
+ };
2601
+ const commit = await commitStagedRun(options.io, options.home, stageKey, options.commitPort, {
2602
+ origin: "subagent",
2603
+ summary: options.summary,
2604
+ now: options.now
2605
+ });
2606
+ if (!commit.ok) return {
2607
+ ok: false,
2608
+ message: commit.message
2609
+ };
2610
+ return {
2611
+ ok: true,
2612
+ tree: tree.value,
2613
+ commit
2614
+ };
2615
+ }
2616
+ //#endregion
2617
+ //#region lib/types/scan-ports.js
2618
+ function createChannelScanPorts(options) {
2619
+ const sessions = /* @__PURE__ */ new Map();
2620
+ return {
2621
+ screening: options.screening,
2622
+ async round1(candidate, signal) {
2623
+ const stageKey = stageKeyOf(options.runId, pairKey(candidate.a, candidate.b));
2624
+ const staged = await ensureStaging(options.io, options.home, {
2625
+ runId: stageKey,
2626
+ target: candidate.a + "-" + candidate.b,
2627
+ sources: options.cards.filter((card) => card.name === candidate.a || card.name === candidate.b).map((card) => ({
2628
+ name: card.name,
2629
+ body: card.body,
2630
+ files: card.files ?? []
2631
+ })),
2632
+ promptVersion: "merge-b/v1",
2633
+ ttlDays: 7,
2634
+ now: options.now
2635
+ });
2636
+ if (!staged.ok) return {
2637
+ kind: "failed",
2638
+ failure: {
2639
+ stage: "decide",
2640
+ code: "staging-create",
2641
+ message: staged.message,
2642
+ retryable: true,
2643
+ retryFrom: "agent1"
2644
+ }
2645
+ };
2646
+ const result = await runRound1OverChannel({
2647
+ channel: options.channel,
2648
+ parent: options.parent,
2649
+ cards: options.cards,
2650
+ allow: options.allow,
2651
+ cwd: stagingRoot(options.home, stageKey),
2652
+ timeoutMs: options.timeoutMs,
2653
+ onChildOpened: (childId) => options.onChild?.({
2654
+ runId: stageKey,
2655
+ childId,
2656
+ expectedChildId: childId,
2657
+ phase: "decide"
2658
+ })
2659
+ }, candidate, signal);
2660
+ if (result.outcome.kind !== "merge") await dropStaging(options.io, options.home, stageKey);
2661
+ else if (options.approval) await result.dispose();
2662
+ else {
2663
+ sessions.set(pairKey(candidate.a, candidate.b), {
2664
+ result,
2665
+ stageKey
2666
+ });
2667
+ if (result.childId !== void 0) options.onChild?.({
2668
+ runId: stageKey,
2669
+ childId: result.childId,
2670
+ expectedChildId: result.childId,
2671
+ phase: "author"
2672
+ });
2673
+ }
2674
+ return result.outcome.kind === "merge" ? {
2675
+ ...result.outcome,
2676
+ stagingRef: stageKey,
2677
+ stagingExpiresAt: options.now() + 7 * 864e5
2678
+ } : result.outcome;
2679
+ },
2680
+ async round2(row, decision, signal) {
2681
+ const key = pairKey(row.a, row.b);
2682
+ const entry = sessions.get(key);
2683
+ if (entry === void 0) return {
2684
+ kind: "failed",
2685
+ failure: {
2686
+ stage: "author",
2687
+ code: "no-session",
2688
+ message: "no same-child round2 session; approve must open a new child",
2689
+ retryable: false,
2690
+ retryFrom: "none"
2691
+ }
2692
+ };
2693
+ sessions.delete(key);
2694
+ try {
2695
+ const authored = await runAuthoring({
2696
+ io: options.io,
2697
+ home: options.home,
2698
+ runId: options.runId,
2699
+ stageKey: entry.stageKey,
2700
+ reuseStaging: true,
2701
+ target: decision.target,
2702
+ mode: decision.mode,
2703
+ cards: options.cards,
2704
+ commitPort: options.commitPort,
2705
+ now: options.now,
2706
+ summary: "merge " + row.a + " + " + row.b,
2707
+ author: async () => {
2708
+ await entry.result.author?.(row, decision, signal);
2709
+ return "authored";
2710
+ }
2711
+ });
2712
+ if (!authored.ok) return {
2713
+ kind: "failed",
2714
+ failure: {
2715
+ stage: "author",
2716
+ code: "authoring-failed",
2717
+ message: authored.message,
2718
+ retryable: true,
2719
+ retryFrom: "agent2"
2720
+ }
2721
+ };
2722
+ const receipt = authored.commit.receipt;
2723
+ return {
2724
+ kind: "merged",
2725
+ ...receipt?.unitId === void 0 ? {} : { unitId: receipt.unitId },
2726
+ archived: receipt?.archived ?? [],
2727
+ target: {
2728
+ requested: decision.target,
2729
+ actual: authored.commit.plan.target,
2730
+ created: receipt?.create ?? false
2731
+ }
2732
+ };
2733
+ } finally {
2734
+ await entry.result.dispose();
2735
+ if (entry.result.childId !== void 0) options.onChildClosed?.(entry.result.childId);
2736
+ }
2737
+ }
2738
+ };
2739
+ }
2740
+ //#endregion
2741
+ //#region lib/types/new-child-authoring.js
2742
+ async function runNewChildAuthoring(options, row, decision, signal) {
2743
+ const targetCard = options.cards.find((card) => card.name === decision.target);
2744
+ if (targetCard?.protectionUnknown === true || targetCard?.protectedBy === "pinned") return {
2745
+ kind: "failed",
2746
+ failure: {
2747
+ stage: "author",
2748
+ code: "target-pinned",
2749
+ message: "target is pinned: " + decision.target,
2750
+ retryable: false,
2751
+ retryFrom: "none"
2752
+ }
2753
+ };
2754
+ const stageKey = row.stagingRef ?? stageKeyOf(options.runId, pairKey(row.a, row.b));
2755
+ if ((await readStaging(options.io, options.home, stageKey)).kind === "present") await setStagingTarget(options.io, options.home, stageKey, decision.target, decision.mode);
2756
+ else {
2757
+ const staging = await createStaging(options.io, options.home, {
2758
+ runId: stageKey,
2759
+ target: decision.target,
2760
+ mode: decision.mode,
2761
+ sources: options.cards.filter((card) => card.name === row.a || card.name === row.b).map((card) => ({
2762
+ name: card.name,
2763
+ body: card.body,
2764
+ files: card.files ?? []
2765
+ })),
2766
+ promptVersion: "merge-b/v1",
2767
+ ttlDays: 7,
2768
+ now: options.now
2769
+ });
2770
+ if (!staging.ok) return {
2771
+ kind: "failed",
2772
+ failure: {
2773
+ stage: "author",
2774
+ code: "staging-create",
2775
+ message: staging.message,
2776
+ retryable: true,
2777
+ retryFrom: "agent2"
2778
+ }
2779
+ };
2780
+ }
2781
+ const opened = await options.channel.open({
2782
+ parent: options.parent,
2783
+ label: "merge-round2",
2784
+ promptText: STANDBY_PROMPT,
2785
+ kind: "maintainer",
2786
+ allow: options.allow,
2787
+ cwd: stagingRoot(options.home, stageKey),
2788
+ timeoutMs: options.timeoutMs,
2789
+ signal
2790
+ });
2791
+ if (opened.kind !== "ok") return {
2792
+ kind: "failed",
2793
+ failure: {
2794
+ stage: "author",
2795
+ code: opened.kind,
2796
+ message: opened.kind === "unavailable" ? opened.reason : "round2 aborted",
2797
+ retryable: opened.kind !== "aborted",
2798
+ retryFrom: opened.kind === "aborted" ? "none" : "agent2"
2799
+ }
2800
+ };
2801
+ options.onChild?.({
2802
+ runId: stageKey,
2803
+ childId: opened.childId,
2804
+ expectedChildId: opened.childId,
2805
+ phase: "author"
2806
+ });
2807
+ try {
2808
+ if (opened.turn.stopReason !== "completed") return {
2809
+ kind: "failed",
2810
+ failure: {
2811
+ stage: "author",
2812
+ code: opened.turn.stopReason,
2813
+ message: "standby did not complete",
2814
+ retryable: true,
2815
+ retryFrom: "agent2"
2816
+ }
2817
+ };
2818
+ const turn = await options.channel.continueTurn(opened.childId, buildRound2Prompt(row, decision, "fresh"), {
2819
+ timeoutMs: options.timeoutMs,
2820
+ signal
2821
+ });
2822
+ if (turn.kind !== "ok") return {
2823
+ kind: "failed",
2824
+ failure: {
2825
+ stage: "author",
2826
+ code: turn.kind,
2827
+ message: turn.kind === "unavailable" ? turn.reason : "round2 aborted",
2828
+ retryable: turn.kind !== "aborted",
2829
+ retryFrom: turn.kind === "aborted" ? "none" : "agent2"
2830
+ }
2831
+ };
2832
+ if (turn.turn.stopReason !== "completed") return {
2833
+ kind: "failed",
2834
+ failure: {
2835
+ stage: "author",
2836
+ code: turn.turn.stopReason,
2837
+ message: "round2 did not complete",
2838
+ retryable: true,
2839
+ retryFrom: "agent2"
2840
+ }
2841
+ };
2842
+ const tree = await readStagedTree(options.io, options.home, stageKey);
2843
+ if (tree.kind !== "present") return {
2844
+ kind: "failed",
2845
+ failure: {
2846
+ stage: "author",
2847
+ code: "staging-missing",
2848
+ message: tree.kind === "unknown" ? tree.reason : "staged tree absent",
2849
+ retryable: true,
2850
+ retryFrom: "agent2"
2851
+ }
2852
+ };
2853
+ if (hasBlockingFinding(validateStagedTree(tree.value))) return {
2854
+ kind: "failed",
2855
+ failure: {
2856
+ stage: "validate",
2857
+ code: "rule-gate",
2858
+ message: "staged tree failed the rule gate",
2859
+ retryable: true,
2860
+ retryFrom: "agent2"
2861
+ }
2862
+ };
2863
+ const commit = await commitStagedRun(options.io, options.home, stageKey, options.commitPort, {
2864
+ origin: "subagent",
2865
+ summary: "merge " + row.a + " + " + row.b,
2866
+ now: options.now
2867
+ });
2868
+ if (!commit.ok) return {
2869
+ kind: "failed",
2870
+ failure: {
2871
+ stage: "commit",
2872
+ code: "commit-failed",
2873
+ message: commit.message,
2874
+ retryable: true,
2875
+ retryFrom: "agent2"
2876
+ }
2877
+ };
2878
+ const receipt = commit.receipt;
2879
+ return {
2880
+ kind: "merged",
2881
+ ...receipt?.unitId === void 0 ? {} : { unitId: receipt.unitId },
2882
+ archived: receipt?.archived ?? [],
2883
+ target: {
2884
+ requested: decision.target,
2885
+ actual: commit.plan.target,
2886
+ created: receipt?.create ?? false
2887
+ }
2888
+ };
2889
+ } finally {
2890
+ try {
2891
+ await options.channel.dispose(opened.childId);
2892
+ } catch {}
2893
+ options.onChildClosed?.(opened.childId);
2894
+ }
2895
+ }
2896
+ function createNewChildRound2(options) {
2897
+ return (row, decision, signal, runId) => runNewChildAuthoring({
2898
+ ...options,
2899
+ runId
2900
+ }, row, decision, signal);
2901
+ }
2902
+ //#endregion
2903
+ //#region lib/types/adapters/library.js
2904
+ function skillLibraryPort(skills) {
2905
+ return {
2906
+ readSkill: (name) => skills.read(name),
2907
+ isArchived: async (name) => {
2908
+ if (typeof skills.archivedNames !== "function") return false;
2909
+ const names = await skills.archivedNames();
2910
+ return names === null ? true : names.includes(name);
2911
+ },
2912
+ commitMerge: (plan) => skills.commitMerge(plan)
2913
+ };
2914
+ }
2915
+ //#endregion
2916
+ //#region lib/types/runtime.js
2917
+ /**
2918
+ * The merge command runtime: cards -> screening -> channel ports -> scan/approve.
2919
+ * Plugin wiring supplies the channel, model and library; this module keeps the orchestration
2920
+ * out of apply() and makes the mutating verbs testable with fakes.
2921
+ */
2922
+ function createMergeRuntime(deps) {
2923
+ const readOnly = createReadOnlyMergeService(deps.io, deps.home);
2924
+ const timeout = deps.timeoutMinutes > 0 ? deps.timeoutMinutes : void 0;
2925
+ const common = {
2926
+ io: deps.io,
2927
+ home: deps.home,
2928
+ registry: deps.registry,
2929
+ platformWrite: deps.platformWrite,
2930
+ now: deps.now,
2931
+ ...timeout === void 0 ? {} : { timeoutMinutes: timeout }
2932
+ };
2933
+ const stage = deps.stage;
2934
+ const stageHooks = stage === void 0 ? {} : {
2935
+ onChild: (context) => {
2936
+ stage.bind(context);
2937
+ },
2938
+ onChildClosed: (childId) => {
2939
+ stage.unbind(childId);
2940
+ }
2941
+ };
2942
+ return { async execute(command, context) {
2943
+ const parent = context?.parent ?? deps.parent;
2944
+ if (command.verb === "pair") {
2945
+ const signal = new AbortController().signal;
2946
+ const cards = await deps.cards(signal);
2947
+ const pair = [{
2948
+ a: command.a,
2949
+ b: command.b
2950
+ }];
2951
+ const result = await runScanCommand({
2952
+ ...common,
2953
+ approval: deps.params.mergeApproval,
2954
+ makePorts: (runId) => createChannelScanPorts({
2955
+ io: deps.io,
2956
+ home: deps.home,
2957
+ runId,
2958
+ cards,
2959
+ screening: () => Promise.resolve(pair),
2960
+ channel: deps.channel,
2961
+ parent,
2962
+ allow: deps.allow,
2963
+ ...deps.cwd === void 0 ? {} : { cwd: deps.cwd },
2964
+ timeoutMs: deps.params.mergePairTimeoutMinutes * 6e4,
2965
+ approval: deps.params.mergeApproval,
2966
+ commitPort: skillLibraryPort(deps.skills),
2967
+ now: deps.now,
2968
+ ...stageHooks
2969
+ })
2970
+ });
2971
+ return result.ok ? {
2972
+ ok: true,
2973
+ text: "merge pair 完成:merged " + String(result.report.totals.merged) + " / pending " + String(result.report.totals.pending) + " / failed " + String(result.report.totals.failed)
2974
+ } : {
2975
+ ok: false,
2976
+ text: result.message
2977
+ };
2978
+ }
2979
+ if (command.verb === "scan") {
2980
+ const signal = new AbortController().signal;
2981
+ const cards = await deps.cards(signal);
2982
+ const loadedState = await loadMergeState(deps.io, deps.home);
2983
+ const skipped = skippedPairKeys(loadedState.state.rows, deps.params.mergeSkippedTtlDays, deps.now());
2984
+ const held = new Set(loadedState.state.rows.filter((row) => row.queue === "pending" || !canRetry(row)).map((row) => row.pairKey));
2985
+ const result = await runScanCommand({
2986
+ ...common,
2987
+ approval: deps.params.mergeApproval,
2988
+ makePorts: (runId) => createChannelScanPorts({
2989
+ io: deps.io,
2990
+ home: deps.home,
2991
+ runId,
2992
+ cards,
2993
+ screening: async (turnSignal) => {
2994
+ const raw = await deps.screeningForRun(cards, runId, turnSignal);
2995
+ const filtered = raw.filter((candidate) => {
2996
+ const key = pairKey(candidate.a, candidate.b);
2997
+ return !skipped.has(key) && !held.has(key);
2998
+ });
2999
+ const candidatePairs = raw.candidatePairs;
3000
+ return candidatePairs === void 0 ? filtered : Object.assign(filtered, { candidatePairs });
3001
+ },
3002
+ channel: deps.channel,
3003
+ parent,
3004
+ allow: deps.allow,
3005
+ ...deps.cwd === void 0 ? {} : { cwd: deps.cwd },
3006
+ timeoutMs: deps.params.mergePairTimeoutMinutes * 6e4,
3007
+ approval: deps.params.mergeApproval,
3008
+ commitPort: skillLibraryPort(deps.skills),
3009
+ now: deps.now,
3010
+ ...stageHooks
3011
+ })
3012
+ });
3013
+ return result.ok ? {
3014
+ ok: true,
3015
+ text: "merge scan 完成:merged " + String(result.report.totals.merged) + " / pending " + String(result.report.totals.pending) + " / failed " + String(result.report.totals.failed)
3016
+ } : {
3017
+ ok: false,
3018
+ text: result.message
3019
+ };
3020
+ }
3021
+ if (command.verb === "reject") {
3022
+ const row = (await loadMergeState(deps.io, deps.home)).state.rows.find((item) => item.id === command.id);
3023
+ if (row === void 0) return {
3024
+ ok: false,
3025
+ text: "merge row not found: " + command.id
3026
+ };
3027
+ const next = transition(row, {
3028
+ type: "reject",
3029
+ at: new Date(deps.now()).toISOString()
3030
+ });
3031
+ if (!next.ok || next.row === null) return {
3032
+ ok: false,
3033
+ text: "merge row is not pending: " + command.id
3034
+ };
3035
+ await upsertRow(deps.io, deps.home, deps.now, next.row);
3036
+ return {
3037
+ ok: true,
3038
+ text: "已拒绝合并:" + command.id
3039
+ };
3040
+ }
3041
+ if (command.verb === "doctor") return {
3042
+ ok: true,
3043
+ text: (await mergeDoctor(deps.io, deps.home, deps.params.mergeModelsRoot)).map((row) => row.key + " [" + row.status + "] " + row.label + ":" + row.detail).join("\n")
3044
+ };
3045
+ if (command.verb === "report" || command.verb === "runs" || command.verb === "cancel") {
3046
+ const registry = deps.registry;
3047
+ if (command.verb === "runs") {
3048
+ const mergeRuns = registry.runs("merge");
3049
+ return {
3050
+ ok: true,
3051
+ text: mergeRuns.length === 0 ? "merge runs: empty" : ["merge runs: " + String(mergeRuns.length)].concat(mergeRuns.map((run) => "- " + run.id + " [" + run.state + "]" + (run.resultRef === void 0 ? "" : " " + run.resultRef))).join("\n")
3052
+ };
3053
+ }
3054
+ if (command.verb === "cancel") {
3055
+ if (command.id !== void 0) {
3056
+ const cancelled = registry.cancel(command.id);
3057
+ return {
3058
+ ok: cancelled,
3059
+ text: cancelled ? "已请求取消 " + command.id : "没有可取消的 run: " + command.id
3060
+ };
3061
+ }
3062
+ const count = registry.cancelAll();
3063
+ return {
3064
+ ok: true,
3065
+ text: "已请求取消 " + String(count) + " 个运行"
3066
+ };
3067
+ }
3068
+ const run = command.id === void 0 ? registry.runs("merge")[0] : registry.find(command.id);
3069
+ if (run === void 0) return {
3070
+ ok: false,
3071
+ text: "merge run not found: " + (command.id ?? "(latest)")
3072
+ };
3073
+ if (run.resultRef === void 0) return {
3074
+ ok: false,
3075
+ text: "merge run has no report yet: " + run.id
3076
+ };
3077
+ const report = await probeText(deps.io, run.resultRef);
3078
+ if (report.kind === "unknown") return {
3079
+ ok: false,
3080
+ text: "report is unreadable: " + run.resultRef + ":" + report.reason
3081
+ };
3082
+ return report.kind === "absent" ? {
3083
+ ok: false,
3084
+ text: "report is missing: " + run.resultRef
3085
+ } : {
3086
+ ok: true,
3087
+ text: report.value.slice(0, 12e3)
3088
+ };
3089
+ }
3090
+ if (command.verb === "undo") {
3091
+ const receipt = await readCommitReceipt(deps.io, deps.home, command.run, command.target);
3092
+ if (receipt === null) return {
3093
+ ok: false,
3094
+ text: "merge run has no commit receipt: " + command.run
3095
+ };
3096
+ if (receipt.target !== command.target) return {
3097
+ ok: false,
3098
+ text: "receipt target is " + receipt.target + ", not " + command.target
3099
+ };
3100
+ const result = await undoMergeReceipt(receipt, skillLibraryUndoPort(deps.skills));
3101
+ return {
3102
+ ok: result.ok,
3103
+ text: result.message
3104
+ };
3105
+ }
3106
+ if (command.verb === "skipped" && command.restore !== void 0) {
3107
+ const row = (await loadMergeState(deps.io, deps.home)).state.rows.find((item) => item.id === command.restore);
3108
+ if (row === void 0 || row.state !== "skipped") return {
3109
+ ok: false,
3110
+ text: "skipped row not found: " + command.restore
3111
+ };
3112
+ await dropRow(deps.io, deps.home, deps.now, row.id);
3113
+ return {
3114
+ ok: true,
3115
+ text: "已恢复参与筛选:" + row.id
3116
+ };
3117
+ }
3118
+ if (command.verb === "resume") {
3119
+ if (command.clear !== void 0) {
3120
+ const cleared = (await loadMergeState(deps.io, deps.home)).state.rows.find((item) => item.id === command.clear);
3121
+ if (cleared === void 0) return {
3122
+ ok: false,
3123
+ text: "merge row not found: " + command.clear
3124
+ };
3125
+ await dropRow(deps.io, deps.home, deps.now, cleared.id);
3126
+ return {
3127
+ ok: true,
3128
+ text: "已清除合并行:" + cleared.id
3129
+ };
3130
+ }
3131
+ const loaded = await loadMergeState(deps.io, deps.home);
3132
+ 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);
3133
+ if (row === void 0) return {
3134
+ ok: false,
3135
+ text: "merge row not found: " + (command.id ?? "(auto)")
3136
+ };
3137
+ const signal = new AbortController().signal;
3138
+ const cards = await deps.cards(signal);
3139
+ const pair = () => Promise.resolve([{
3140
+ a: row.a,
3141
+ b: row.b
3142
+ }]);
3143
+ const failed = (stage, code, message) => Promise.resolve({
3144
+ kind: "failed",
3145
+ failure: {
3146
+ stage,
3147
+ code,
3148
+ message,
3149
+ retryable: false,
3150
+ retryFrom: "none"
3151
+ }
3152
+ });
3153
+ const basePorts = {
3154
+ screening: pair,
3155
+ round1: () => failed("decide", "resume-needs-ports", "verify resume needs makePorts"),
3156
+ round2: () => failed("author", "resume-needs-child", "fuse resume needs a new child")
3157
+ };
3158
+ const result = await runResumeCommand({
3159
+ ...common,
3160
+ rowId: row.id,
3161
+ ports: basePorts,
3162
+ makePorts: (runId) => createChannelScanPorts({
3163
+ io: deps.io,
3164
+ home: deps.home,
3165
+ runId,
3166
+ cards,
3167
+ screening: pair,
3168
+ channel: deps.channel,
3169
+ parent,
3170
+ allow: deps.allow,
3171
+ ...deps.cwd === void 0 ? {} : { cwd: deps.cwd },
3172
+ timeoutMs: deps.params.mergePairTimeoutMinutes * 6e4,
3173
+ approval: deps.params.mergeApproval,
3174
+ commitPort: skillLibraryPort(deps.skills),
3175
+ now: deps.now
3176
+ }),
3177
+ round2WithRun: createNewChildRound2({
3178
+ io: deps.io,
3179
+ home: deps.home,
3180
+ cards,
3181
+ channel: deps.channel,
3182
+ parent,
3183
+ allow: deps.allow,
3184
+ ...deps.cwd === void 0 ? {} : { cwd: deps.cwd },
3185
+ timeoutMs: deps.params.mergePairTimeoutMinutes * 6e4,
3186
+ commitPort: skillLibraryPort(deps.skills),
3187
+ now: deps.now,
3188
+ ...stageHooks
3189
+ })
3190
+ });
3191
+ return result.ok ? {
3192
+ ok: true,
3193
+ text: "merge resume 完成:merged " + String(result.report.totals.merged)
3194
+ } : {
3195
+ ok: false,
3196
+ text: result.message
3197
+ };
3198
+ }
3199
+ if (command.verb === "approve") {
3200
+ const signal = new AbortController().signal;
3201
+ const cards = await deps.cards(signal);
3202
+ const result = await runApproveCommand({
3203
+ ...common,
3204
+ rowId: command.id,
3205
+ ports: { round2: createNewChildRound2({
3206
+ io: deps.io,
3207
+ home: deps.home,
3208
+ cards,
3209
+ channel: deps.channel,
3210
+ parent,
3211
+ allow: deps.allow,
3212
+ ...deps.cwd === void 0 ? {} : { cwd: deps.cwd },
3213
+ timeoutMs: deps.params.mergePairTimeoutMinutes * 6e4,
3214
+ commitPort: skillLibraryPort(deps.skills),
3215
+ now: deps.now
3216
+ }) }
3217
+ });
3218
+ return result.ok ? {
3219
+ ok: true,
3220
+ text: "merge approve 完成:merged " + String(result.report.totals.merged)
3221
+ } : {
3222
+ ok: false,
3223
+ text: result.message
3224
+ };
3225
+ }
3226
+ const text = await handleReadOnlyMergeCommand(command, deps.io, deps.home);
3227
+ if (text !== null) return {
3228
+ ok: true,
3229
+ text
3230
+ };
3231
+ return readOnly.execute(command);
3232
+ } };
3233
+ }
3234
+ //#endregion
3235
+ //#region lib/types/params.js
3236
+ const MERGE_PARAM_DEFAULTS = {
3237
+ mergeApproval: false,
3238
+ mergeSkippedTtlDays: 0,
3239
+ mergeRunTimeoutMinutes: 0,
3240
+ mergePairTimeoutMinutes: 15,
3241
+ mergeModelsRoot: ""
3242
+ };
3243
+ const KEYS = new Set(Object.keys(MERGE_PARAM_DEFAULTS));
3244
+ function materialize(value) {
3245
+ if (typeof value === "object" && value !== null && typeof value.get === "function") return value.get();
3246
+ return value;
3247
+ }
3248
+ function nonNegative(name, value) {
3249
+ const resolved = materialize(value);
3250
+ if (typeof resolved !== "number" || !Number.isInteger(resolved) || resolved < 0) throw new Error(name + " must be a non-negative integer");
3251
+ return resolved;
3252
+ }
3253
+ function resolveMergeParams(input = {}) {
3254
+ if (typeof input !== "object" || input === null || Array.isArray(input)) throw new Error("merge params must be an object");
3255
+ for (const key of Object.keys(input)) if (!KEYS.has(key)) throw new Error("unknown merge parameter: " + key);
3256
+ const record = input;
3257
+ const approval = record.mergeApproval === void 0 ? MERGE_PARAM_DEFAULTS.mergeApproval : materialize(record.mergeApproval);
3258
+ if (typeof approval !== "boolean") throw new Error("mergeApproval must be a boolean");
3259
+ return {
3260
+ mergeApproval: approval,
3261
+ mergeSkippedTtlDays: record.mergeSkippedTtlDays === void 0 ? MERGE_PARAM_DEFAULTS.mergeSkippedTtlDays : nonNegative("mergeSkippedTtlDays", record.mergeSkippedTtlDays),
3262
+ mergeRunTimeoutMinutes: record.mergeRunTimeoutMinutes === void 0 ? MERGE_PARAM_DEFAULTS.mergeRunTimeoutMinutes : nonNegative("mergeRunTimeoutMinutes", record.mergeRunTimeoutMinutes),
3263
+ mergeModelsRoot: record.mergeModelsRoot === void 0 ? MERGE_PARAM_DEFAULTS.mergeModelsRoot : (() => {
3264
+ const value = materialize(record.mergeModelsRoot);
3265
+ if (typeof value !== "string") throw new Error("mergeModelsRoot must be a string");
3266
+ return value;
3267
+ })(),
3268
+ mergePairTimeoutMinutes: record.mergePairTimeoutMinutes === void 0 ? MERGE_PARAM_DEFAULTS.mergePairTimeoutMinutes : (() => {
3269
+ const value = nonNegative("mergePairTimeoutMinutes", record.mergePairTimeoutMinutes);
3270
+ if (value === 0) throw new Error("mergePairTimeoutMinutes must be > 0");
3271
+ return value;
3272
+ })()
3273
+ };
3274
+ }
3275
+ //#endregion
3276
+ //#region lib/types/adapters/channel.js
3277
+ function convert(outcome) {
3278
+ if (outcome.kind !== "ok") return outcome;
3279
+ return {
3280
+ kind: "ok",
3281
+ childId: String(outcome.childId),
3282
+ turn: {
3283
+ text: outcome.turn.text,
3284
+ stopReason: outcome.turn.stopReason
3285
+ }
3286
+ };
3287
+ }
3288
+ function subagentChannelAdapter(channel) {
3289
+ return {
3290
+ async open(spec) {
3291
+ if (channel.open === void 0) return {
3292
+ kind: "unavailable",
3293
+ code: "E-323",
3294
+ reason: "subagent channel has no open()"
3295
+ };
3296
+ return convert(await channel.open(spec));
3297
+ },
3298
+ async continueTurn(childId, promptText, options) {
3299
+ if (channel.continueTurn === void 0) return {
3300
+ kind: "unavailable",
3301
+ code: "E-323",
3302
+ reason: "subagent channel has no continueTurn()"
3303
+ };
3304
+ return convert(await channel.continueTurn(childId, promptText, options));
3305
+ },
3306
+ async dispose(childId) {
3307
+ if (channel.dispose === void 0) return;
3308
+ await channel.dispose(childId);
3309
+ }
3310
+ };
3311
+ }
3312
+ //#endregion
3313
+ //#region lib/types/stage-tool-registry.js
3314
+ var StageToolRegistry = class {
3315
+ bindings = /* @__PURE__ */ new Map();
3316
+ bind(childId, context) {
3317
+ this.bindings.set(childId, context);
3318
+ }
3319
+ unbind(childId) {
3320
+ this.bindings.delete(childId);
3321
+ }
3322
+ clear() {
3323
+ this.bindings.clear();
3324
+ }
3325
+ resolve(exec) {
3326
+ const childId = childIdOf(exec);
3327
+ return childId === void 0 ? void 0 : this.bindings.get(childId);
3328
+ }
3329
+ };
3330
+ function childIdOf(exec) {
3331
+ if (typeof exec !== "object" || exec === null) return void 0;
3332
+ const agent = exec.agent;
3333
+ if (typeof agent !== "object" || agent === null) return void 0;
3334
+ const session = agent.session;
3335
+ if (typeof session === "object" && session !== null) {
3336
+ const id = session.id;
3337
+ if (typeof id === "string") return id;
3338
+ }
3339
+ const sessionId = agent.sessionId;
3340
+ return typeof sessionId === "string" ? sessionId : void 0;
3341
+ }
3342
+ //#endregion
3343
+ //#region lib/types/staging-files.js
3344
+ /**
3345
+ * Staged file operations used by the authoring tool.
3346
+ *
3347
+ * The child never receives a general filesystem write. Every path is resolved under
3348
+ * <run>/sources/<source>/ or <run>/merged/<target>/ and must stay there; the staging
3349
+ * manifest supplies the target name. Reads are three-state, writes are atomic at the
3350
+ * file level, and copy only goes sources -> merged.
3351
+ */
3352
+ function safeRelative(path) {
3353
+ if (path === "" || path.startsWith("/") || path.startsWith("\\") || path.includes("\0")) return false;
3354
+ return path.replace(/\\/g, "/").split("/").every((part) => part !== "" && part !== "." && part !== "..");
3355
+ }
3356
+ function areaRoot(home, runId, target, area, source) {
3357
+ const root = stagingRoot(home, runId);
3358
+ if (area === "merged") return join(root, "merged", target);
3359
+ if (source === void 0 || source === "") return null;
3360
+ return join(root, "sources", source);
3361
+ }
3362
+ async function writeStagedFile(io, home, runId, path, content, source) {
3363
+ if (!safeRelative(path)) return {
3364
+ ok: false,
3365
+ message: "unsafe staged path: " + path
3366
+ };
3367
+ const staging = await readStaging(io, home, runId);
3368
+ if (!isPresent(staging)) return {
3369
+ ok: false,
3370
+ message: "staging is not present for run " + runId
3371
+ };
3372
+ const root = areaRoot(home, runId, staging.value.manifest.target, "merged", source);
3373
+ if (root === null) return {
3374
+ ok: false,
3375
+ message: "staged write requires merged area"
3376
+ };
3377
+ const target = join(root, ...path.split("/"));
3378
+ try {
3379
+ await io.writeText(target, content);
3380
+ return {
3381
+ ok: true,
3382
+ path
3383
+ };
3384
+ } catch (error) {
3385
+ return {
3386
+ ok: false,
3387
+ message: "staged write failed: " + (error instanceof Error ? error.message : String(error))
3388
+ };
3389
+ }
3390
+ }
3391
+ async function readStagedFile(io, home, runId, path, area, source) {
3392
+ if (!safeRelative(path)) return probeUnknown("unsafe staged path: " + path);
3393
+ const staging = await readStaging(io, home, runId);
3394
+ if (!isPresent(staging)) return staging.kind === "absent" ? { kind: "absent" } : probeUnknown(staging.reason);
3395
+ const root = areaRoot(home, runId, staging.value.manifest.target, area, source);
3396
+ if (root === null) return probeUnknown("missing source name for sources area");
3397
+ try {
3398
+ const value = await io.readText(join(root, ...path.split("/")));
3399
+ return value === null ? { kind: "absent" } : {
3400
+ kind: "present",
3401
+ value
3402
+ };
3403
+ } catch (error) {
3404
+ return probeUnknown("staged read failed: " + (error instanceof Error ? error.message : String(error)));
3405
+ }
3406
+ }
3407
+ async function listStagedFiles(io, home, runId, area, path = "", source) {
3408
+ if (path !== "" && !safeRelative(path)) return probeUnknown("unsafe staged path: " + path);
3409
+ const staging = await readStaging(io, home, runId);
3410
+ if (!isPresent(staging)) return staging.kind === "absent" ? { kind: "absent" } : probeUnknown(staging.reason);
3411
+ const root = areaRoot(home, runId, staging.value.manifest.target, area, source);
3412
+ if (root === null) return probeUnknown("missing source name for sources area");
3413
+ try {
3414
+ return {
3415
+ kind: "present",
3416
+ value: [...await io.list(join(root, ...path.split("/").filter(Boolean)))].sort()
3417
+ };
3418
+ } catch (error) {
3419
+ return probeUnknown("staged list failed: " + (error instanceof Error ? error.message : String(error)));
3420
+ }
3421
+ }
3422
+ /** Copy one source file into the merged target; the only cross-area operation allowed. */
3423
+ async function copyStagedFile(io, home, runId, source, from, to) {
3424
+ if (!safeRelative(from) || !safeRelative(to)) return {
3425
+ ok: false,
3426
+ message: "unsafe staged copy path"
3427
+ };
3428
+ const sourceFile = await readStagedFile(io, home, runId, from, "sources", source);
3429
+ if (!isPresent(sourceFile)) return {
3430
+ ok: false,
3431
+ message: "source file is not readable: " + from
3432
+ };
3433
+ return await writeStagedFile(io, home, runId, to, sourceFile.value);
3434
+ }
3435
+ //#endregion
3436
+ //#region lib/types/stage-tool.js
3437
+ async function applyStageTool(io, home, ctx, op) {
3438
+ if (ctx.childId !== ctx.expectedChildId) return {
3439
+ ok: false,
3440
+ code: "not-merge-child",
3441
+ message: "merge_stage is only available to the bound merge child"
3442
+ };
3443
+ if (op.op === "write" || op.op === "copy") {
3444
+ if (ctx.phase !== "author") return {
3445
+ ok: false,
3446
+ code: "phase",
3447
+ message: "merge_stage write/copy is only allowed in the author phase"
3448
+ };
3449
+ }
3450
+ if (op.op === "list") return probeResult(await listStagedFiles(io, home, ctx.runId, op.area, op.path ?? "", op.source), "list");
3451
+ if (op.op === "read") return probeResult(await readStagedFile(io, home, ctx.runId, op.path, op.area, op.source), "read");
3452
+ if (op.op === "write") {
3453
+ const result = await writeStagedFile(io, home, ctx.runId, op.path, op.content);
3454
+ return result.ok ? {
3455
+ ok: true,
3456
+ value: { path: result.path }
3457
+ } : {
3458
+ ok: false,
3459
+ code: "io",
3460
+ message: result.message
3461
+ };
3462
+ }
3463
+ const result = await copyStagedFile(io, home, ctx.runId, op.source, op.from, op.to);
3464
+ return result.ok ? {
3465
+ ok: true,
3466
+ value: { path: result.path }
3467
+ } : {
3468
+ ok: false,
3469
+ code: "io",
3470
+ message: result.message
3471
+ };
3472
+ }
3473
+ function probeResult(probe, what) {
3474
+ if (probe.kind === "present") return {
3475
+ ok: true,
3476
+ value: probe.value
3477
+ };
3478
+ if (probe.kind === "absent") return {
3479
+ ok: false,
3480
+ code: "not-found",
3481
+ message: what + ": not found"
3482
+ };
3483
+ return {
3484
+ ok: false,
3485
+ code: "io",
3486
+ message: what + ": " + probe.reason
3487
+ };
3488
+ }
3489
+ //#endregion
3490
+ //#region lib/types/merge-tool.js
3491
+ const MERGE_STAGE_TOOL_NAME = "merge_stage";
3492
+ function shapeError(message) {
3493
+ return {
3494
+ ok: false,
3495
+ code: "bad-path",
3496
+ message
3497
+ };
3498
+ }
3499
+ function operation(args) {
3500
+ const op = args.op;
3501
+ if (op === "list") {
3502
+ const area = args.area;
3503
+ if (area !== "sources" && area !== "merged") return shapeError("list.area must be sources or merged");
3504
+ return {
3505
+ op,
3506
+ area,
3507
+ ...typeof args.path === "string" ? { path: args.path } : {},
3508
+ ...typeof args.source === "string" ? { source: args.source } : {}
3509
+ };
3510
+ }
3511
+ if (op === "read") {
3512
+ const area = args.area;
3513
+ if (area !== "sources" && area !== "merged") return shapeError("read.area must be sources or merged");
3514
+ if (typeof args.path !== "string" || args.path === "") return shapeError("read.path is required");
3515
+ return {
3516
+ op,
3517
+ area,
3518
+ path: args.path,
3519
+ ...typeof args.source === "string" ? { source: args.source } : {}
3520
+ };
3521
+ }
3522
+ if (op === "write") {
3523
+ if (typeof args.path !== "string" || args.path === "") return shapeError("write.path is required");
3524
+ if (typeof args.content !== "string") return shapeError("write.content must be a string");
3525
+ return {
3526
+ op,
3527
+ path: args.path,
3528
+ content: args.content
3529
+ };
3530
+ }
3531
+ if (op === "copy") {
3532
+ if (typeof args.source !== "string" || typeof args.from !== "string" || typeof args.to !== "string") return shapeError("copy requires source/from/to");
3533
+ return {
3534
+ op,
3535
+ source: args.source,
3536
+ from: args.from,
3537
+ to: args.to
3538
+ };
3539
+ }
3540
+ return shapeError("op must be list/read/write/copy");
3541
+ }
3542
+ function createMergeStageTool(io, home, context) {
3543
+ return {
3544
+ name: MERGE_STAGE_TOOL_NAME,
3545
+ description: "Read source copies and write the merged skill into the staging tree. list/read use area=sources|merged plus source (for sources); write/copy paths are relative to merged/<target> (for example path=SKILL.md, from=SKILL.md, to=references/x.md). Write/copy are only allowed in the author phase.",
3546
+ parameters: {
3547
+ type: "object",
3548
+ properties: {
3549
+ op: {
3550
+ type: "string",
3551
+ enum: [
3552
+ "list",
3553
+ "read",
3554
+ "write",
3555
+ "copy"
3556
+ ]
3557
+ },
3558
+ area: {
3559
+ type: "string",
3560
+ enum: ["sources", "merged"]
3561
+ },
3562
+ path: { type: "string" },
3563
+ source: { type: "string" },
3564
+ content: { type: "string" },
3565
+ from: { type: "string" },
3566
+ to: { type: "string" }
3567
+ },
3568
+ required: ["op"],
3569
+ additionalProperties: false
3570
+ },
3571
+ output: {
3572
+ schema: { type: "string" },
3573
+ render: (_args, value) => [{
3574
+ type: "text",
3575
+ text: value
3576
+ }]
3577
+ },
3578
+ async execute(args, exec) {
3579
+ const op = operation(args);
3580
+ if ("ok" in op) return JSON.stringify(op);
3581
+ const ctx = context(exec);
3582
+ if (ctx === void 0) return JSON.stringify({
3583
+ ok: false,
3584
+ code: "not-merge-child",
3585
+ message: "merge_stage is only available to an active merge child"
3586
+ });
3587
+ return JSON.stringify(await applyStageTool(io, home, ctx, op));
3588
+ }
3589
+ };
3590
+ }
3591
+ //#endregion
3592
+ //#region lib/types/supersede.js
3593
+ function mergeRunIdsToSupersede(runs) {
3594
+ return runs.filter((run) => run.kind === "merge" && run.supersededAt === void 0).map((run) => run.id);
3595
+ }
3596
+ async function supersedeMergeRuns(registry) {
3597
+ const shared = registry.supersedeKind;
3598
+ if (typeof shared === "function") return await shared.call(registry, "merge");
3599
+ const ids = mergeRunIdsToSupersede(registry.runs());
3600
+ if (ids.length === 0) return 0;
3601
+ await registry.supersede(ids);
3602
+ return ids.length;
3603
+ }
3604
+ //#endregion
3605
+ //#region lib/types/pair-command.js
3606
+ async function runPairCommand(options) {
3607
+ return await runScanCommand({
3608
+ io: options.io,
3609
+ home: options.home,
3610
+ registry: options.registry,
3611
+ approval: options.approval,
3612
+ platformWrite: options.platformWrite,
3613
+ now: options.now,
3614
+ ports: {
3615
+ ...options.ports,
3616
+ screening: () => Promise.resolve([{
3617
+ a: options.a,
3618
+ b: options.b
3619
+ }])
3620
+ },
3621
+ ...options.timeoutMinutes === void 0 ? {} : { timeoutMinutes: options.timeoutMinutes }
3622
+ });
3623
+ }
3624
+ //#endregion
3625
+ //#region lib/types/index.js
3626
+ /** E3 knobs are volatile (user layer); the E2 pair timeout is deployment-only. */
3627
+ const Config = z.object({
3628
+ mergeApproval: z.boolean().default(false).volatile(),
3629
+ mergeSkippedTtlDays: z.number().min(0).default(0).volatile(),
3630
+ mergeRunTimeoutMinutes: z.number().min(0).default(0).volatile(),
3631
+ mergePairTimeoutMinutes: z.number().min(1).default(15),
3632
+ mergeModelsRoot: z.string().default("")
3633
+ });
3634
+ function engineConfigFromEnv(configuredRoot) {
3635
+ const root = configuredRoot !== void 0 && configuredRoot !== "" ? configuredRoot : process.env.SKILL_MERGE_MODELS_ROOT;
3636
+ if (root === void 0 || root === "") return void 0;
3637
+ return {
3638
+ models_dir: "",
3639
+ offline: true,
3640
+ auto_provision: false,
3641
+ embed_backend: "local",
3642
+ embedder_model: join(root, "potion-multilingual-128M-int8"),
3643
+ embedder_model_last: join(root, "potion-multilingual-128M-pca64-int8")
3644
+ };
3645
+ }
3646
+ /** Suppress the platform auto form; the registry renders the merge card instead. */
3647
+ function configureMergePresentation(ctx) {
3648
+ ctx.inject(["settings"], (scope) => {
3649
+ const settings = scope.settings;
3650
+ if (typeof settings?.configure !== "function") return;
3651
+ const disposer = settings.configure({ auto: false }, ctx.fiber);
3652
+ if (typeof disposer === "function") scope.effect(() => disposer, "evolution-merge: settings presentation");
3653
+ });
3654
+ }
3655
+ const inject = [];
3656
+ function apply(ctx, rawConfig = {}) {
3657
+ configureMergePresentation(ctx);
3658
+ const params = resolveMergeParams(rawConfig);
3659
+ let mergeRun;
3660
+ const mergeService = { async run(rawInput, invocation) {
3661
+ return mergeRun === void 0 ? {
3662
+ ok: false,
3663
+ text: "merge 未就绪:evolutionIo 尚未挂载"
3664
+ } : mergeRun(rawInput, invocation);
3665
+ } };
3666
+ const mergeContext = ctx;
3667
+ if (typeof mergeContext.provide === "function") mergeContext.provide("evolutionMerge", mergeService);
3668
+ let source;
3669
+ registerHostRoutes(ctx, (audit) => makeMergeRoutes({ getSource: () => source }, audit));
3670
+ ctx.inject(["commands", "evolutionIo"], (scope) => {
3671
+ const registry = scope.get("evolutionIo");
3672
+ if (registry === void 0) return;
3673
+ const io = evolutionIoAdapter(() => registry.provider());
3674
+ const home = evolutionHome();
3675
+ const runs = newRunRegistry({
3676
+ io,
3677
+ home,
3678
+ warn: (message) => {
3679
+ ctx.logger.warn(message);
3680
+ }
3681
+ });
3682
+ runs.load();
3683
+ ctx.effect(() => {
3684
+ const host = ctx;
3685
+ if (typeof host.on !== "function") return () => {};
3686
+ return host.on("evolution/skill-mutated", (event) => {
3687
+ if (event.action === "restore") supersedeMergeRuns(runs);
3688
+ });
3689
+ }, "evolution-merge: restore supersede");
3690
+ const policySnapshot = ctx.get("evolutionPolicy");
3691
+ const skills = newSkillLibrary({
3692
+ io,
3693
+ ctx,
3694
+ authority: authorityFrom({ origin: "subagent" }),
3695
+ limits: {
3696
+ ...DEFAULT_SKILL_LIMITS,
3697
+ ...policyStageLimits(policySnapshot?.get?.())
3698
+ }
3699
+ });
3700
+ const channel = subagentChannelAdapter(createSubagentChannel(ctx));
3701
+ const stage = new StageToolRegistry();
3702
+ const engineConfig = engineConfigFromEnv(params.mergeModelsRoot);
3703
+ const llm = ctx.get("llm");
3704
+ const defaultModel = ctx.get("agentDefaultModel");
3705
+ const runner = llm === void 0 || defaultModel === void 0 ? void 0 : createEngineRunner(llm, () => defaultModel.currentSelection());
3706
+ const engineScreening = createEngineScreening({
3707
+ io,
3708
+ home,
3709
+ engine: { screen },
3710
+ select: selectCandidates,
3711
+ ...engineConfig === void 0 ? {} : { cfg: engineConfig },
3712
+ ...runner === void 0 ? {} : { runner },
3713
+ async onSelection(selection) {
3714
+ const loaded = await loadMergeState(io, home);
3715
+ const at = (/* @__PURE__ */ new Date()).toISOString();
3716
+ for (const skipped of selection.skipped) {
3717
+ if (loaded.state.rows.some((row) => row.pairKey === skipped.pairKey)) continue;
3718
+ const next = transition(void 0, {
3719
+ type: "gate-keep",
3720
+ a: skipped.a,
3721
+ b: skipped.b,
3722
+ ...skipped.reason === void 0 ? {} : { reason: skipped.reason },
3723
+ at
3724
+ });
3725
+ if (next.ok && next.row !== null) await upsertRow(io, home, () => Date.now(), next.row);
3726
+ }
3727
+ }
3728
+ });
3729
+ source = {
3730
+ rows() {
3731
+ return loadMergeState(io, home).then((loaded) => loaded.state.rows);
3732
+ },
3733
+ run(id) {
3734
+ return Promise.resolve(runs.runs().find((record) => record.id === id));
3735
+ },
3736
+ runs() {
3737
+ return Promise.resolve(runs.runs());
3738
+ },
3739
+ approval() {
3740
+ return params.mergeApproval;
3741
+ },
3742
+ commands() {
3743
+ return [
3744
+ "merge scan",
3745
+ "merge doctor",
3746
+ "merge status",
3747
+ "merge pending",
3748
+ "merge skipped",
3749
+ "merge report",
3750
+ "merge runs",
3751
+ "merge cancel"
3752
+ ];
3753
+ }
3754
+ };
3755
+ const commands = scope.get("commands");
3756
+ if (commands === void 0) return;
3757
+ const runtime = createMergeRuntime({
3758
+ io,
3759
+ home,
3760
+ registry: runs,
3761
+ skills,
3762
+ now: () => Date.now(),
3763
+ params,
3764
+ platformWrite: true,
3765
+ channel,
3766
+ parent: void 0,
3767
+ allow: ["merge_stage"],
3768
+ timeoutMinutes: params.mergeRunTimeoutMinutes,
3769
+ async cards(signal) {
3770
+ const listed = await skills.list({ withContent: true });
3771
+ if (signal.aborted) return [];
3772
+ return Promise.all(listed.map(async (item) => {
3773
+ const probe = await skills.listSupportFiles(item.name);
3774
+ const paths = probe.kind === "present" ? probe.value : [];
3775
+ const files = await Promise.all(paths.map(async (path) => ({
3776
+ path,
3777
+ content: await skills.readSupportFile(item.name, path) ?? ""
3778
+ })));
3779
+ return {
3780
+ name: item.name,
3781
+ description: item.description,
3782
+ body: item.content ?? "",
3783
+ files,
3784
+ protectedBy: item.protectedBy,
3785
+ protectionUnknown: item.protectionUnknown
3786
+ };
3787
+ }));
3788
+ },
3789
+ screeningForRun: engineScreening,
3790
+ stage: {
3791
+ bind: (context) => {
3792
+ stage.bind(context.childId, context);
3793
+ },
3794
+ unbind: (childId) => {
3795
+ stage.unbind(childId);
3796
+ }
3797
+ }
3798
+ });
3799
+ mergeRun = (rawInput, invocation) => runMergeCommand(runtime, rawInput, invocation);
3800
+ const command = createMergeCommand(runtime);
3801
+ scope.effect(() => commands.register(command), "evolution-merge: command");
3802
+ ctx.inject(["tools"], (toolScope) => {
3803
+ const tools = toolScope.get("tools");
3804
+ if (tools === void 0) return;
3805
+ const tool = createMergeStageTool(io, home, (exec) => stage.resolve(exec));
3806
+ toolScope.effect(() => tools.register(tool), "evolution-merge: merge_stage");
3807
+ });
3808
+ scope.effect(() => () => {
3809
+ runs.cancelAll();
3810
+ }, "evolution-merge: runs");
3811
+ });
3812
+ }
3813
+ //#endregion
3814
+ export { Config, MERGE_DOCTOR_SCHEMA_VERSION, MERGE_MAX_ATTEMPTS, MERGE_PAIR_STATES, MERGE_PARAM_DEFAULTS, MERGE_PROMPT_VERSION, MERGE_QUEUES, MERGE_ROUTES, MERGE_STAGE_TOOL_NAME, MERGE_STATE_VERSION, STAGING_SCHEMA_VERSION, STANDBY_PROMPT, StageToolRegistry, apply, applyStageTool, approvePending, assembleStagedCommit, buildCommitPlan, buildMergeView, buildRound1Prompt, buildRound2Prompt, buildStandbyPrompt, canRetry, commitReceiptPath, commitStagedRun, committedPairs, configureMergePresentation, copyStagedFile, countRows, createChannelScanPorts, createEngineRunner, createEngineScreening, createMergeCommand, createMergeRuntime, createMergeStageTool, createNewChildRound2, createReadOnlyMergeService, createStaging, dropRow, dropStaging, durableReceiptDir, durableReceiptPath, emptyMergeState, engineConfigFromEnv, ensureStaging, formatQueue, formatStatus, handleReadOnlyMergeCommand, hasBlockingFinding, inject, isSkippedPair, listStagedFiles, loadMergeState, makeMergeRoutes, mergeCommandHelp, mergeDoctor, mergeReportDigestPath, mergeReportPath, mergeRunIdsToSupersede, mergeStatePath, mergeUsage, moveRow, mutateMergeState, pageQueue, pairKey, parseDecision, parseMergeCommand, queueOf, readCommitReceipt, readMergeReport, readStagedFile, readStagedTree, readStaging, recordRound2Outcome, rejectPending, resolveMergeParams, resolveTargetName, rewriteFrontmatterName, rowIdOf, rowsOf, runApproveCommand, runAuthoring, runMergeCommand, runNewChildAuthoring, runPairCommand, runResumeCommand, runRound1OverChannel, runScan, runScanCommand, setStagingTarget, skillLibraryPort, skillLibraryUndoPort, skippedPairKeys, snapshotCards, stageKeyOf, stageOf, stagingRoot, subagentChannelAdapter, supersedeMergeRuns, toRowView, toRunFact, transition, undoMergeReceipt, upsertRow, validateStagedTree, withMergeRun, writeCommitReceipt, writeMergeReport, writeStagedFile };