@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.
- package/README.md +24 -1
- package/lib/index.js +3997 -0
- package/lib/types/adapters/channel.d.ts +42 -0
- package/lib/types/adapters/library.d.ts +8 -0
- package/lib/types/adapters/model.d.ts +31 -0
- package/lib/types/adapters/undo.d.ts +9 -0
- package/lib/types/approve-command.d.ts +22 -0
- package/lib/types/authoring-flow.d.ts +35 -0
- package/lib/types/channel-port.d.ts +60 -0
- package/lib/types/commands.d.ts +55 -0
- package/lib/types/commit-assembly.d.ts +25 -0
- package/lib/types/commit-execute.d.ts +29 -0
- package/lib/types/commit-plan.d.ts +45 -0
- package/lib/types/commit-receipt.d.ts +14 -0
- package/lib/types/contracts.d.ts +136 -0
- package/lib/types/coordinator.d.ts +57 -0
- package/lib/types/decision.d.ts +18 -0
- package/lib/types/doctor.d.ts +10 -0
- package/lib/types/gate-cache.d.ts +12 -0
- package/lib/types/gate-filter.d.ts +31 -0
- package/lib/types/index.d.ts +82 -0
- package/lib/types/ladder.d.ts +65 -0
- package/lib/types/merge-command.d.ts +36 -0
- package/lib/types/merge-routes.d.ts +23 -0
- package/lib/types/merge-tool.d.ts +22 -0
- package/lib/types/new-child-authoring.d.ts +29 -0
- package/lib/types/pair-command.d.ts +20 -0
- package/lib/types/params.d.ts +13 -0
- package/lib/types/pending-actions.d.ts +17 -0
- package/lib/types/prompts.d.ts +13 -0
- package/lib/types/propose.d.ts +26 -0
- package/lib/types/read-commands.d.ts +9 -0
- package/lib/types/report.d.ts +10 -0
- package/lib/types/resume-command.d.ts +24 -0
- package/lib/types/round2-outcome.d.ts +13 -0
- package/lib/types/routes.d.ts +49 -0
- package/lib/types/rules.d.ts +24 -0
- package/lib/types/run-shell.d.ts +65 -0
- package/lib/types/runtime.d.ts +38 -0
- package/lib/types/scan-command.d.ts +21 -0
- package/lib/types/scan-ports.d.ts +32 -0
- package/lib/types/scan-run.d.ts +44 -0
- package/lib/types/screening-port.d.ts +47 -0
- package/lib/types/screening.d.ts +30 -0
- package/lib/types/skipped.d.ts +8 -0
- package/lib/types/stage-tool-registry.d.ts +14 -0
- package/lib/types/stage-tool.d.ts +45 -0
- package/lib/types/staged-tree.d.ts +4 -0
- package/lib/types/staging-files.d.ts +15 -0
- package/lib/types/staging.d.ts +43 -0
- package/lib/types/state.d.ts +29 -0
- package/lib/types/status.d.ts +14 -0
- package/lib/types/supersede.d.ts +8 -0
- package/lib/types/undo-merge.d.ts +13 -0
- 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 };
|