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