@lmzhen/dsh-evolution-merge 0.29.0 → 0.30.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/index.js +1125 -568
- package/lib/types/adapters/channel.js +29 -0
- package/lib/types/adapters/engine-screen.d.ts +1 -1
- package/lib/types/adapters/engine-screen.js +45 -0
- package/lib/types/adapters/library.js +14 -0
- package/lib/types/adapters/model-runner.js +34 -0
- package/lib/types/adapters/select.d.ts +3 -0
- package/lib/types/adapters/select.js +35 -0
- package/lib/types/adapters/undo.js +35 -0
- package/lib/types/approve-command.js +42 -0
- package/lib/types/authoring-flow.js +48 -0
- package/lib/types/channel-port.js +74 -0
- package/lib/types/commands.d.ts +9 -0
- package/lib/types/commands.js +79 -0
- package/lib/types/commit-assembly.js +48 -0
- package/lib/types/commit-execute.js +52 -0
- package/lib/types/commit-plan.js +56 -0
- package/lib/types/commit-receipt.d.ts +2 -0
- package/lib/types/commit-receipt.js +85 -0
- package/lib/types/contracts.d.ts +14 -5
- package/lib/types/contracts.js +57 -0
- package/lib/types/coordinator.d.ts +3 -0
- package/lib/types/coordinator.js +131 -0
- package/lib/types/decision.js +61 -0
- package/lib/types/doctor.js +50 -0
- package/lib/types/domain/claim.d.ts +10 -0
- package/lib/types/domain/claim.js +12 -0
- package/lib/types/domain/commit-journal-store.d.ts +6 -0
- package/lib/types/domain/commit-journal-store.js +22 -0
- package/lib/types/domain/commit-journal.d.ts +16 -0
- package/lib/types/domain/commit-journal.js +25 -0
- package/lib/types/domain/errors.d.ts +10 -0
- package/lib/types/domain/errors.js +7 -0
- package/lib/types/domain/failure-route.d.ts +25 -0
- package/lib/types/domain/failure-route.js +22 -0
- package/lib/types/domain/indexes.d.ts +21 -0
- package/lib/types/domain/indexes.js +15 -0
- package/lib/types/domain/ledger.d.ts +17 -0
- package/lib/types/domain/ledger.js +65 -0
- package/lib/types/domain/markdown.d.ts +2 -0
- package/lib/types/domain/markdown.js +57 -0
- package/lib/types/domain/migrate-v1.d.ts +41 -0
- package/lib/types/domain/migrate-v1.js +23 -0
- package/lib/types/domain/pair-index-store.d.ts +6 -0
- package/lib/types/domain/pair-index-store.js +25 -0
- package/lib/types/domain/pair-index.d.ts +9 -0
- package/lib/types/domain/pair-index.js +28 -0
- package/lib/types/domain/resolve-pair.d.ts +11 -0
- package/lib/types/domain/resolve-pair.js +17 -0
- package/lib/types/domain/row.d.ts +113 -0
- package/lib/types/domain/row.js +111 -0
- package/lib/types/domain/run-record.d.ts +45 -0
- package/lib/types/domain/run-record.js +24 -0
- package/lib/types/domain/state.d.ts +57 -0
- package/lib/types/domain/state.js +65 -0
- package/lib/types/gate-cache.js +36 -0
- package/lib/types/index.js +241 -0
- package/lib/types/ladder.js +162 -0
- package/lib/types/merge-command.js +43 -0
- package/lib/types/merge-routes.d.ts +14 -4
- package/lib/types/merge-routes.js +99 -0
- package/lib/types/merge-tool.js +69 -0
- package/lib/types/new-child-authoring.js +83 -0
- package/lib/types/pair-command.js +18 -0
- package/lib/types/params.js +49 -0
- package/lib/types/pending-actions.js +28 -0
- package/lib/types/prompts.js +60 -0
- package/lib/types/read-commands.js +32 -0
- package/lib/types/report.js +70 -0
- package/lib/types/resume-command.js +57 -0
- package/lib/types/round2-outcome.js +50 -0
- package/lib/types/routes.d.ts +5 -2
- package/lib/types/routes.js +75 -0
- package/lib/types/rules.js +55 -0
- package/lib/types/run-shell.d.ts +7 -0
- package/lib/types/run-shell.js +69 -0
- package/lib/types/runtime.d.ts +2 -0
- package/lib/types/runtime.js +269 -0
- package/lib/types/scan-command.d.ts +1 -0
- package/lib/types/scan-command.js +30 -0
- package/lib/types/scan-ports.js +93 -0
- package/lib/types/scan-run.d.ts +2 -0
- package/lib/types/scan-run.js +186 -0
- package/lib/types/skill-card.js +2 -0
- package/lib/types/skipped.js +24 -0
- package/lib/types/stage-tool-registry.js +32 -0
- package/lib/types/stage-tool.js +31 -0
- package/lib/types/staged-tree.js +75 -0
- package/lib/types/staging-files.js +86 -0
- package/lib/types/staging.d.ts +7 -0
- package/lib/types/staging.js +149 -0
- package/lib/types/state.js +103 -0
- package/lib/types/status.d.ts +2 -1
- package/lib/types/status.js +28 -0
- package/lib/types/supersede.js +14 -0
- package/lib/types/undo-merge.js +23 -0
- package/package.json +4 -4
package/lib/index.js
CHANGED
|
@@ -1,9 +1,44 @@
|
|
|
1
1
|
import { join } from "node:path";
|
|
2
2
|
import z from "@deepseek-ai/schemastery";
|
|
3
|
-
import { screen, selectCandidates } from "@lmzhen/dsh-evolution-merge-engine";
|
|
3
|
+
import { pairKeyOf, screen, selectCandidates, toEngineCandidate } from "@lmzhen/dsh-evolution-merge-engine";
|
|
4
4
|
import { DEFAULT_SKILL_LIMITS, authorityFrom, contentHash, createSubagentChannel, evolutionHome, evolutionIoAdapter, executeUnitUndo, isPresent, newRunRegistry, newSkillLibrary, parseFrontmatter, planUnitUndoAcross, policyStageLimits, probeAbsent, probeList, probePresent, probeText, probeUnknown, scanContentThreats, supportRefs, transactIo, validateFrontmatter, validateSupportPath } from "@lmzhen/dsh-evolution-core";
|
|
5
5
|
import { HOST_ROUTES, json, methodNotAllowed, registerHostRoutes } from "@lmzhen/dsh-evolution-core/http-routes";
|
|
6
6
|
import { createHash } from "node:crypto";
|
|
7
|
+
//#region lib/types/adapters/select.js
|
|
8
|
+
/** Host selector correction: engine selector does not route judge errors to candidates. */
|
|
9
|
+
function selectWithErrors(report) {
|
|
10
|
+
const base = selectCandidates(report);
|
|
11
|
+
const doc = report.reportDoc ?? report;
|
|
12
|
+
const rows = Array.isArray(doc.pairs_final) ? doc.pairs_final : [];
|
|
13
|
+
const errors = /* @__PURE__ */ new Set();
|
|
14
|
+
for (const row of rows) {
|
|
15
|
+
if (row.error !== true) continue;
|
|
16
|
+
const a = typeof row.a === "string" ? row.a : "";
|
|
17
|
+
const b = typeof row.b === "string" ? row.b : "";
|
|
18
|
+
if (a !== "" && b !== "") errors.add(pairKeyOf(a, b));
|
|
19
|
+
}
|
|
20
|
+
const candidates = base.candidates.filter((candidate) => !errors.has(pairKeyOf(candidate.a, candidate.b)));
|
|
21
|
+
const skipped = base.skipped.filter((item) => !errors.has(item.pairKey));
|
|
22
|
+
const reportOnly = base.reportOnly.filter((row) => {
|
|
23
|
+
const value = row;
|
|
24
|
+
if (typeof value.a !== "string" || typeof value.b !== "string") return true;
|
|
25
|
+
return !errors.has(pairKeyOf(value.a, value.b));
|
|
26
|
+
});
|
|
27
|
+
for (const row of rows) {
|
|
28
|
+
if (row.error !== true) continue;
|
|
29
|
+
const a = typeof row.a === "string" ? row.a : "";
|
|
30
|
+
const b = typeof row.b === "string" ? row.b : "";
|
|
31
|
+
if (a === "" || b === "") continue;
|
|
32
|
+
candidates.push(toEngineCandidate(row, typeof row.reason === "string" ? row.reason : "judge-error"));
|
|
33
|
+
}
|
|
34
|
+
return {
|
|
35
|
+
...base,
|
|
36
|
+
candidates,
|
|
37
|
+
skipped,
|
|
38
|
+
reportOnly
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
//#endregion
|
|
7
42
|
//#region lib/types/adapters/engine-screen.js
|
|
8
43
|
/**
|
|
9
44
|
* G3 screen adapter: cards -> corpus snapshot -> EnginePort.screen.
|
|
@@ -97,24 +132,27 @@ function createEngineRunner(llm, route) {
|
|
|
97
132
|
* The data layer keeps four states; the user face collapses verify/fuse into one retry
|
|
98
133
|
* queue and keeps the stage as a chip. The API queue must never expose the raw state.
|
|
99
134
|
*/
|
|
100
|
-
const MERGE_STATE_VERSION =
|
|
135
|
+
const MERGE_STATE_VERSION = 2;
|
|
101
136
|
const MERGE_PAIR_STATES = [
|
|
102
137
|
"verify",
|
|
103
138
|
"pending",
|
|
139
|
+
"approved",
|
|
104
140
|
"fuse",
|
|
105
141
|
"skipped"
|
|
106
142
|
];
|
|
107
143
|
const MERGE_QUEUES = [
|
|
144
|
+
"verify",
|
|
108
145
|
"pending",
|
|
109
|
-
"
|
|
146
|
+
"fuse",
|
|
110
147
|
"skipped"
|
|
111
148
|
];
|
|
112
149
|
/** User-facing queue of one data state. verify/fuse share retry, not two tabs. */
|
|
113
150
|
function queueOf(state) {
|
|
114
151
|
switch (state) {
|
|
152
|
+
case "verify": return "verify";
|
|
115
153
|
case "pending": return "pending";
|
|
116
|
-
case "
|
|
117
|
-
case "fuse": return "
|
|
154
|
+
case "approved":
|
|
155
|
+
case "fuse": return "fuse";
|
|
118
156
|
case "skipped": return "skipped";
|
|
119
157
|
}
|
|
120
158
|
}
|
|
@@ -123,6 +161,7 @@ function stageOf(state) {
|
|
|
123
161
|
switch (state) {
|
|
124
162
|
case "verify": return "verify";
|
|
125
163
|
case "pending": return "decide";
|
|
164
|
+
case "approved":
|
|
126
165
|
case "fuse": return "author";
|
|
127
166
|
case "skipped": return null;
|
|
128
167
|
}
|
|
@@ -142,7 +181,7 @@ function rowIdOf(key) {
|
|
|
142
181
|
}
|
|
143
182
|
function emptyMergeState() {
|
|
144
183
|
return {
|
|
145
|
-
schemaVersion:
|
|
184
|
+
schemaVersion: 2,
|
|
146
185
|
rows: []
|
|
147
186
|
};
|
|
148
187
|
}
|
|
@@ -155,25 +194,43 @@ function canRetry(record) {
|
|
|
155
194
|
return record.failure.retryable && record.failure.retryFrom !== "none";
|
|
156
195
|
}
|
|
157
196
|
//#endregion
|
|
158
|
-
//#region lib/types/
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
197
|
+
//#region lib/types/domain/migrate-v1.js
|
|
198
|
+
function migrateV1Row(row) {
|
|
199
|
+
const queue = row.state === "pending" ? "pending" : row.state === "skipped" ? "skipped" : row.state === "fuse" ? "fuse" : "verify";
|
|
200
|
+
return {
|
|
201
|
+
schemaVersion: 2,
|
|
202
|
+
id: row.id,
|
|
203
|
+
pairKey: row.pairKey,
|
|
204
|
+
a: row.a,
|
|
205
|
+
b: row.b,
|
|
206
|
+
state: row.state,
|
|
207
|
+
queue,
|
|
208
|
+
...row.failure === void 0 ? {} : { failure: row.failure },
|
|
209
|
+
...row.decision === void 0 ? {} : { decision: row.decision },
|
|
210
|
+
...row.runId === void 0 ? {} : { runId: row.runId },
|
|
211
|
+
...row.unitId === void 0 ? {} : { unitId: row.unitId },
|
|
212
|
+
...row.origin === void 0 ? {} : { origin: row.origin },
|
|
213
|
+
...row.attempts === void 0 ? {} : { attempts: row.attempts },
|
|
214
|
+
...row.stagingRef === void 0 ? {} : { stagingRef: row.stagingRef },
|
|
215
|
+
...row.stagingExpiresAt === void 0 ? {} : { stagingExpiresAt: row.stagingExpiresAt },
|
|
216
|
+
createdAt: row.createdAt,
|
|
217
|
+
updatedAt: row.updatedAt
|
|
218
|
+
};
|
|
169
219
|
}
|
|
170
|
-
|
|
171
|
-
|
|
220
|
+
//#endregion
|
|
221
|
+
//#region lib/types/domain/ledger.js
|
|
222
|
+
/** Pure v2 ledger normalization/projection. No IO. */
|
|
223
|
+
function isRow(value) {
|
|
224
|
+
const row = value;
|
|
225
|
+
return typeof row.id === "string" && typeof row.pairKey === "string" && typeof row.a === "string" && typeof row.b === "string" && typeof row.state === "string" && typeof row.createdAt === "string" && typeof row.updatedAt === "string";
|
|
172
226
|
}
|
|
173
|
-
function
|
|
227
|
+
function parseLedger(raw) {
|
|
174
228
|
if (raw === null) return {
|
|
175
229
|
ok: true,
|
|
176
|
-
|
|
230
|
+
ledger: {
|
|
231
|
+
schemaVersion: 2,
|
|
232
|
+
rows: []
|
|
233
|
+
}
|
|
177
234
|
};
|
|
178
235
|
let parsed;
|
|
179
236
|
try {
|
|
@@ -181,52 +238,76 @@ function parseState(raw) {
|
|
|
181
238
|
} catch {
|
|
182
239
|
return {
|
|
183
240
|
ok: false,
|
|
184
|
-
|
|
241
|
+
ledger: {
|
|
242
|
+
schemaVersion: 2,
|
|
243
|
+
rows: []
|
|
244
|
+
},
|
|
245
|
+
note: "invalid json"
|
|
185
246
|
};
|
|
186
247
|
}
|
|
187
248
|
const body = parsed;
|
|
188
249
|
if (!Array.isArray(body.rows)) return {
|
|
189
250
|
ok: false,
|
|
190
|
-
|
|
251
|
+
ledger: {
|
|
252
|
+
schemaVersion: 2,
|
|
253
|
+
rows: []
|
|
254
|
+
},
|
|
255
|
+
note: "rows missing"
|
|
191
256
|
};
|
|
192
257
|
const rows = [];
|
|
193
258
|
let skipped = 0;
|
|
194
259
|
for (const value of body.rows) {
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
260
|
+
if (body.schemaVersion === 1) {
|
|
261
|
+
const legacy = value;
|
|
262
|
+
if (typeof legacy.id !== "string" || typeof legacy.pairKey !== "string" || typeof legacy.a !== "string" || typeof legacy.b !== "string" || typeof legacy.state !== "string" || typeof legacy.createdAt !== "string" || typeof legacy.updatedAt !== "string") {
|
|
263
|
+
skipped += 1;
|
|
264
|
+
continue;
|
|
265
|
+
}
|
|
266
|
+
const row = migrateV1Row(legacy);
|
|
267
|
+
rows.push(row);
|
|
198
268
|
continue;
|
|
199
269
|
}
|
|
200
|
-
if (!
|
|
270
|
+
if (!isRow(value)) {
|
|
201
271
|
skipped += 1;
|
|
202
272
|
continue;
|
|
203
273
|
}
|
|
204
|
-
|
|
205
|
-
id: row.id,
|
|
206
|
-
pairKey: row.pairKey,
|
|
207
|
-
a: row.a,
|
|
208
|
-
b: row.b,
|
|
209
|
-
state: row.state,
|
|
210
|
-
queue: queueOf(row.state),
|
|
211
|
-
createdAt: row.createdAt,
|
|
212
|
-
updatedAt: row.updatedAt,
|
|
213
|
-
...row.failure === void 0 ? {} : { failure: row.failure },
|
|
214
|
-
...row.decision === void 0 ? {} : { decision: row.decision },
|
|
215
|
-
...typeof row.runId === "string" ? { runId: row.runId } : {},
|
|
216
|
-
...typeof row.unitId === "string" ? { unitId: row.unitId } : {},
|
|
217
|
-
...row.origin === "agent" || row.origin === "human" ? { origin: row.origin } : {},
|
|
218
|
-
...typeof row.attempts === "number" ? { attempts: row.attempts } : {}
|
|
219
|
-
};
|
|
220
|
-
rows.push(normalized);
|
|
274
|
+
rows.push(value);
|
|
221
275
|
}
|
|
222
|
-
const
|
|
223
|
-
const note = versionNote === void 0 ? skipped === 0 ? void 0 : String(skipped) + " invalid row(s) skipped" : skipped === 0 ? versionNote : versionNote + "; " + String(skipped) + " invalid row(s) skipped";
|
|
276
|
+
const note = skipped === 0 ? void 0 : String(skipped) + " invalid row(s) skipped";
|
|
224
277
|
return {
|
|
225
278
|
ok: true,
|
|
226
|
-
|
|
279
|
+
ledger: {
|
|
280
|
+
schemaVersion: 2,
|
|
281
|
+
rows
|
|
282
|
+
},
|
|
227
283
|
...note === void 0 ? {} : { note }
|
|
228
284
|
};
|
|
229
285
|
}
|
|
286
|
+
//#endregion
|
|
287
|
+
//#region lib/types/state.js
|
|
288
|
+
/**
|
|
289
|
+
* The one merge-state ledger: <home>/merge/merge-state.json.
|
|
290
|
+
*
|
|
291
|
+
* Reads are pure projections (no write, no reconciliation side effect). Every write goes
|
|
292
|
+
* through mutateMergeState and a single transactIo, so there is no second file and no
|
|
293
|
+
* incremental writer. A row's queue is always derived from its state on read; a hand-edited
|
|
294
|
+
* mismatch is repaired by the next write, never by a GET.
|
|
295
|
+
*/
|
|
296
|
+
function mergeStatePath(home) {
|
|
297
|
+
return join(home, "merge", "merge-state.json");
|
|
298
|
+
}
|
|
299
|
+
function parseState(raw) {
|
|
300
|
+
const parsed = parseLedger(raw);
|
|
301
|
+
if (!parsed.ok) return {
|
|
302
|
+
ok: false,
|
|
303
|
+
rows: []
|
|
304
|
+
};
|
|
305
|
+
return {
|
|
306
|
+
ok: true,
|
|
307
|
+
rows: parsed.ledger.rows,
|
|
308
|
+
...parsed.note === void 0 ? {} : { note: parsed.note }
|
|
309
|
+
};
|
|
310
|
+
}
|
|
230
311
|
/** Pure read: never writes, never deletes a row. */
|
|
231
312
|
async function loadMergeState(io, home) {
|
|
232
313
|
let raw = null;
|
|
@@ -236,7 +317,7 @@ async function loadMergeState(io, home) {
|
|
|
236
317
|
return {
|
|
237
318
|
ok: false,
|
|
238
319
|
state: {
|
|
239
|
-
schemaVersion:
|
|
320
|
+
schemaVersion: 2,
|
|
240
321
|
rows: []
|
|
241
322
|
}
|
|
242
323
|
};
|
|
@@ -245,7 +326,7 @@ async function loadMergeState(io, home) {
|
|
|
245
326
|
return {
|
|
246
327
|
ok: parsed.ok,
|
|
247
328
|
state: {
|
|
248
|
-
schemaVersion:
|
|
329
|
+
schemaVersion: 2,
|
|
249
330
|
rows: parsed.rows
|
|
250
331
|
},
|
|
251
332
|
...parsed.note === void 0 ? {} : { note: parsed.note }
|
|
@@ -258,7 +339,7 @@ async function mutateMergeState(io, home, now, fn) {
|
|
|
258
339
|
const out = fn(parseState(current).rows);
|
|
259
340
|
result = out.result;
|
|
260
341
|
return JSON.stringify({
|
|
261
|
-
schemaVersion:
|
|
342
|
+
schemaVersion: 2,
|
|
262
343
|
rows: out.rows
|
|
263
344
|
}, null, 2);
|
|
264
345
|
});
|
|
@@ -325,135 +406,374 @@ async function rowsOf(io, home, queue) {
|
|
|
325
406
|
return queue === void 0 ? loaded.state.rows : loaded.state.rows.filter((row) => row.queue === queue);
|
|
326
407
|
}
|
|
327
408
|
//#endregion
|
|
328
|
-
//#region lib/types/
|
|
409
|
+
//#region lib/types/staging.js
|
|
329
410
|
/**
|
|
330
|
-
*
|
|
411
|
+
* Staging is a run ARTIFACT, not a queue: <home>/merge/runs/<runId>/.
|
|
331
412
|
*
|
|
332
|
-
*
|
|
333
|
-
*
|
|
334
|
-
*
|
|
413
|
+
* createStaging writes a sibling .partial tree first and renames it into place; the .stage
|
|
414
|
+
* marker is the last write inside the partial. A directory without that marker is a
|
|
415
|
+
* half-created artifact and reads as unknown, never as "empty".
|
|
335
416
|
*/
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
code: "invalid-transition",
|
|
340
|
-
message: "cannot apply " + event.type + " to " + (state ?? "no row")
|
|
341
|
-
};
|
|
417
|
+
const STAGING_SCHEMA_VERSION = 1;
|
|
418
|
+
function stageKeyOf(runId, pairKey) {
|
|
419
|
+
return runId + "/pairs/" + createHash("sha256").update(pairKey).digest("hex").slice(0, 16);
|
|
342
420
|
}
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
return {
|
|
346
|
-
id: row.id,
|
|
347
|
-
pairKey: row.pairKey,
|
|
348
|
-
a: row.a,
|
|
349
|
-
b: row.b,
|
|
350
|
-
state: row.state,
|
|
351
|
-
queue: row.queue,
|
|
352
|
-
createdAt: row.createdAt,
|
|
353
|
-
updatedAt: at,
|
|
354
|
-
...row.runId === void 0 ? {} : { runId: row.runId },
|
|
355
|
-
...row.unitId === void 0 ? {} : { unitId: row.unitId },
|
|
356
|
-
...row.origin === void 0 ? {} : { origin: row.origin },
|
|
357
|
-
...row.decision === void 0 ? {} : { decision: row.decision },
|
|
358
|
-
...row.attempts === void 0 ? {} : { attempts: row.attempts },
|
|
359
|
-
...row.stagingRef === void 0 ? {} : { stagingRef: row.stagingRef },
|
|
360
|
-
...row.stagingExpiresAt === void 0 ? {} : { stagingExpiresAt: row.stagingExpiresAt }
|
|
361
|
-
};
|
|
421
|
+
function stagingRoot(home, runId) {
|
|
422
|
+
return join(home, "merge", "runs", runId);
|
|
362
423
|
}
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
...base(row, at),
|
|
368
|
-
state,
|
|
369
|
-
queue: queueOf(state),
|
|
370
|
-
...patch
|
|
371
|
-
}
|
|
372
|
-
};
|
|
424
|
+
/** Relative support paths only; the staging tree must never escape its run directory. */
|
|
425
|
+
function safeRelative$2(path) {
|
|
426
|
+
if (path === "" || path.startsWith("/") || path.startsWith("\\") || path.includes("\0")) return false;
|
|
427
|
+
return path.replace(/\\/g, "/").split("/").every((part) => part !== "" && part !== "." && part !== "..");
|
|
373
428
|
}
|
|
374
|
-
function
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
429
|
+
function manifestText(manifest) {
|
|
430
|
+
return JSON.stringify(manifest, null, 2) + "\n";
|
|
431
|
+
}
|
|
432
|
+
async function createStaging(io, home, input) {
|
|
433
|
+
const final = stagingRoot(home, input.runId);
|
|
434
|
+
if (await io.exists(final)) return {
|
|
435
|
+
ok: false,
|
|
436
|
+
message: "staging already exists for run " + input.runId
|
|
437
|
+
};
|
|
438
|
+
const names = /* @__PURE__ */ new Set();
|
|
439
|
+
for (const source of input.sources) {
|
|
440
|
+
if (names.has(source.name)) return {
|
|
441
|
+
ok: false,
|
|
442
|
+
message: "duplicate source " + source.name
|
|
379
443
|
};
|
|
380
|
-
|
|
381
|
-
return {
|
|
382
|
-
ok:
|
|
383
|
-
|
|
384
|
-
id: rowIdOf(key),
|
|
385
|
-
pairKey: key,
|
|
386
|
-
a: event.a,
|
|
387
|
-
b: event.b,
|
|
388
|
-
state: "verify",
|
|
389
|
-
queue: "retry",
|
|
390
|
-
createdAt: event.at,
|
|
391
|
-
updatedAt: event.at
|
|
392
|
-
}
|
|
444
|
+
names.add(source.name);
|
|
445
|
+
for (const file of source.files) if (!safeRelative$2(file.path)) return {
|
|
446
|
+
ok: false,
|
|
447
|
+
message: "unsafe support path: " + file.path
|
|
393
448
|
};
|
|
394
449
|
}
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
450
|
+
const partial = final + ".partial";
|
|
451
|
+
try {
|
|
452
|
+
await io.remove(partial);
|
|
453
|
+
for (const source of input.sources) {
|
|
454
|
+
const dir = join(partial, "sources", source.name);
|
|
455
|
+
await io.writeText(join(dir, "SKILL.md"), source.body);
|
|
456
|
+
for (const file of source.files) await io.writeText(join(dir, ...file.path.split("/")), file.content);
|
|
457
|
+
}
|
|
458
|
+
const at = input.now();
|
|
459
|
+
const manifest = {
|
|
460
|
+
schemaVersion: 1,
|
|
461
|
+
runId: input.runId,
|
|
462
|
+
target: input.target,
|
|
463
|
+
...input.mode === void 0 ? {} : { mode: input.mode },
|
|
464
|
+
sources: input.sources.map((source) => source.name),
|
|
465
|
+
promptVersion: input.promptVersion,
|
|
466
|
+
createdAt: at,
|
|
467
|
+
expiresAt: at + Math.max(0, input.ttlDays) * 864e5
|
|
403
468
|
};
|
|
404
|
-
|
|
469
|
+
await io.writeText(join(partial, "manifest.json"), manifestText(manifest));
|
|
470
|
+
await io.writeText(join(partial, ".stage"), "");
|
|
471
|
+
await io.rename(partial, final);
|
|
405
472
|
return {
|
|
406
473
|
ok: true,
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
}
|
|
474
|
+
path: final,
|
|
475
|
+
manifest
|
|
476
|
+
};
|
|
477
|
+
} catch (error) {
|
|
478
|
+
try {
|
|
479
|
+
await io.remove(partial);
|
|
480
|
+
} catch {}
|
|
481
|
+
return {
|
|
482
|
+
ok: false,
|
|
483
|
+
message: "staging create failed: " + (error instanceof Error ? error.message : String(error))
|
|
418
484
|
};
|
|
419
|
-
}
|
|
420
|
-
if (current === void 0) return invalid(event, void 0);
|
|
421
|
-
switch (event.type) {
|
|
422
|
-
case "round1-keep": return current.state === "verify" ? next(current, event.at, "skipped", { origin: "agent" }) : invalid(event, current.state);
|
|
423
|
-
case "round1-merge":
|
|
424
|
-
if (current.state !== "verify") return invalid(event, current.state);
|
|
425
|
-
return event.approval === "on" ? next(current, event.at, "pending", {
|
|
426
|
-
...event.decision === void 0 ? {} : { decision: event.decision },
|
|
427
|
-
...event.stagingRef === void 0 ? {} : { stagingRef: event.stagingRef },
|
|
428
|
-
...event.stagingExpiresAt === void 0 ? {} : { stagingExpiresAt: event.stagingExpiresAt }
|
|
429
|
-
}) : next(current, event.at, "fuse", {
|
|
430
|
-
...event.decision === void 0 ? {} : { decision: event.decision },
|
|
431
|
-
...event.stagingRef === void 0 ? {} : { stagingRef: event.stagingRef },
|
|
432
|
-
...event.stagingExpiresAt === void 0 ? {} : { stagingExpiresAt: event.stagingExpiresAt }
|
|
433
|
-
});
|
|
434
|
-
case "round1-fail": return current.state === "verify" ? next(current, event.at, "verify", {
|
|
435
|
-
failure: event.failure,
|
|
436
|
-
attempts: (current.attempts ?? 0) + 1
|
|
437
|
-
}) : invalid(event, current.state);
|
|
438
|
-
case "approve": return current.state === "pending" ? next(current, event.at, "fuse", { ...event.runId === void 0 ? {} : { runId: event.runId } }) : invalid(event, current.state);
|
|
439
|
-
case "reject": return current.state === "pending" ? next(current, event.at, "skipped", { origin: "human" }) : invalid(event, current.state);
|
|
440
|
-
case "round2-fail": return current.state === "fuse" ? next(current, event.at, "fuse", {
|
|
441
|
-
failure: event.failure,
|
|
442
|
-
attempts: (current.attempts ?? 0) + 1
|
|
443
|
-
}) : invalid(event, current.state);
|
|
444
|
-
case "round2-success": return current.state === "fuse" ? {
|
|
445
|
-
ok: true,
|
|
446
|
-
row: null
|
|
447
|
-
} : invalid(event, current.state);
|
|
448
|
-
case "restore": return current.state === "skipped" ? next(current, event.at, "verify") : invalid(event, current.state);
|
|
449
485
|
}
|
|
450
486
|
}
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
487
|
+
async function readStaging(io, home, runId) {
|
|
488
|
+
const path = stagingRoot(home, runId);
|
|
489
|
+
if (!await io.exists(path)) return probeAbsent();
|
|
490
|
+
if (!await io.exists(join(path, ".stage"))) return probeUnknown("staging " + runId + " has no .stage marker (half-created)");
|
|
491
|
+
const probe = await probeText(io, join(path, "manifest.json"));
|
|
492
|
+
if (!isPresent(probe)) return probe;
|
|
493
|
+
try {
|
|
494
|
+
const parsed = JSON.parse(probe.value);
|
|
495
|
+
if (parsed.schemaVersion !== 1 || parsed.runId !== runId || typeof parsed.target !== "string" || !Array.isArray(parsed.sources)) return probeUnknown("staging manifest has an unexpected shape");
|
|
496
|
+
return probePresent({
|
|
497
|
+
path,
|
|
498
|
+
manifest: parsed
|
|
499
|
+
});
|
|
500
|
+
} catch (error) {
|
|
501
|
+
return probeUnknown("staging manifest is not JSON: " + (error instanceof Error ? error.message : String(error)));
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
async function ensureStaging(io, home, input) {
|
|
505
|
+
const existing = await readStaging(io, home, input.runId);
|
|
506
|
+
if (existing.kind === "present") return {
|
|
507
|
+
ok: true,
|
|
508
|
+
path: stagingRoot(home, input.runId),
|
|
509
|
+
manifest: existing.value.manifest
|
|
510
|
+
};
|
|
511
|
+
return createStaging(io, home, input);
|
|
512
|
+
}
|
|
513
|
+
async function setStagingTarget(io, home, stageKey, target, mode) {
|
|
514
|
+
const view = await readStaging(io, home, stageKey);
|
|
515
|
+
if (view.kind !== "present") return false;
|
|
516
|
+
const manifest = Object.assign({}, view.value.manifest, { target }, mode === void 0 ? {} : { mode });
|
|
517
|
+
await io.writeText(join(stagingRoot(home, stageKey), "manifest.json"), JSON.stringify(manifest, null, 2) + "\n");
|
|
518
|
+
return true;
|
|
519
|
+
}
|
|
520
|
+
async function dropStaging(io, home, stageKey) {
|
|
521
|
+
await io.remove(stagingRoot(home, stageKey));
|
|
522
|
+
}
|
|
523
|
+
/** Remove expired run-level staging trees; `keep` names runs a live row still points at. */
|
|
524
|
+
async function cleanupExpiredStaging(io, home, now, keep = /* @__PURE__ */ new Set()) {
|
|
525
|
+
let runs;
|
|
526
|
+
try {
|
|
527
|
+
runs = await io.list(join(home, "merge", "runs"));
|
|
528
|
+
} catch {
|
|
529
|
+
return {
|
|
530
|
+
removed: [],
|
|
531
|
+
kept: [],
|
|
532
|
+
unknown: []
|
|
533
|
+
};
|
|
534
|
+
}
|
|
535
|
+
const removed = [];
|
|
536
|
+
const kept = [];
|
|
537
|
+
const unknown = [];
|
|
538
|
+
for (const runId of runs.sort()) {
|
|
539
|
+
if (keep.has(runId)) {
|
|
540
|
+
kept.push(runId);
|
|
541
|
+
continue;
|
|
542
|
+
}
|
|
543
|
+
const view = await readStaging(io, home, runId);
|
|
544
|
+
if (view.kind === "unknown") {
|
|
545
|
+
unknown.push(runId);
|
|
546
|
+
continue;
|
|
547
|
+
}
|
|
548
|
+
if (view.kind !== "present" || view.value.manifest.expiresAt > now) {
|
|
549
|
+
kept.push(runId);
|
|
550
|
+
continue;
|
|
551
|
+
}
|
|
552
|
+
try {
|
|
553
|
+
await io.remove(stagingRoot(home, runId));
|
|
554
|
+
removed.push(runId);
|
|
555
|
+
} catch {
|
|
556
|
+
unknown.push(runId);
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
return {
|
|
560
|
+
removed,
|
|
561
|
+
kept,
|
|
562
|
+
unknown
|
|
563
|
+
};
|
|
564
|
+
}
|
|
565
|
+
//#endregion
|
|
566
|
+
//#region lib/types/commit-receipt.js
|
|
567
|
+
/**
|
|
568
|
+
* Commit receipt: <run>/commit.json, written immediately after a successful commit.
|
|
569
|
+
* It is the idempotency/recovery marker — merge resume and run start read it before
|
|
570
|
+
* replaying a commit. Receipt write failure must not turn a landed commit into a failure;
|
|
571
|
+
* the caller reports the warning instead.
|
|
572
|
+
*/
|
|
573
|
+
function commitReceiptPath(home, runId) {
|
|
574
|
+
return join(stagingRoot(home, runId), "commit.json");
|
|
575
|
+
}
|
|
576
|
+
function durableReceiptDir(home, parentRunId) {
|
|
577
|
+
return join(home, "merge", "receipts", parentRunId);
|
|
578
|
+
}
|
|
579
|
+
function durableReceiptPath(home, parentRunId, stageKey) {
|
|
580
|
+
const leaf = stageKey.split("/").pop() ?? stageKey;
|
|
581
|
+
return join(durableReceiptDir(home, parentRunId), leaf + ".json");
|
|
582
|
+
}
|
|
583
|
+
function parseReceipt(raw) {
|
|
584
|
+
if (raw === null) return null;
|
|
585
|
+
try {
|
|
586
|
+
const parsed = JSON.parse(raw);
|
|
587
|
+
if (typeof parsed.runId !== "string" || typeof parsed.unitId !== "string" || typeof parsed.target !== "string" || typeof parsed.create !== "boolean" || !Array.isArray(parsed.sources) || !Array.isArray(parsed.archived) || typeof parsed.at !== "number") return null;
|
|
588
|
+
return parsed;
|
|
589
|
+
} catch {
|
|
590
|
+
return null;
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
async function writeCommitReceipt(io, home, receipt) {
|
|
594
|
+
const path = commitReceiptPath(home, receipt.runId);
|
|
595
|
+
await io.writeText(path, JSON.stringify(receipt, null, 2) + "\n");
|
|
596
|
+
const parent = receipt.parentRunId ?? receipt.runId.split("/pairs/")[0] ?? receipt.runId;
|
|
597
|
+
await io.writeText(durableReceiptPath(home, parent, receipt.runId), JSON.stringify({
|
|
598
|
+
...receipt,
|
|
599
|
+
parentRunId: parent
|
|
600
|
+
}, null, 2) + "\n");
|
|
601
|
+
return path;
|
|
602
|
+
}
|
|
603
|
+
async function readCommitReceipts(io, home) {
|
|
604
|
+
const root = join(home, "merge", "receipts");
|
|
605
|
+
const out = [];
|
|
606
|
+
let runs;
|
|
607
|
+
try {
|
|
608
|
+
runs = await io.list(root);
|
|
609
|
+
} catch {
|
|
610
|
+
return out;
|
|
611
|
+
}
|
|
612
|
+
for (const run of runs.sort()) {
|
|
613
|
+
const dir = join(root, run);
|
|
614
|
+
let files;
|
|
615
|
+
try {
|
|
616
|
+
files = await io.list(dir);
|
|
617
|
+
} catch {
|
|
618
|
+
continue;
|
|
619
|
+
}
|
|
620
|
+
for (const file of files.sort()) {
|
|
621
|
+
if (!file.endsWith(".json")) continue;
|
|
622
|
+
const receipt = parseReceipt(await io.readText(join(dir, file)));
|
|
623
|
+
if (receipt !== null) out.push(receipt);
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
return out;
|
|
627
|
+
}
|
|
628
|
+
async function readCommitReceipt(io, home, runId, target) {
|
|
629
|
+
const direct = parseReceipt(await io.readText(commitReceiptPath(home, runId)));
|
|
630
|
+
if (direct !== null && (target === void 0 || direct.target === target)) return direct;
|
|
631
|
+
try {
|
|
632
|
+
const names = await io.list(durableReceiptDir(home, runId));
|
|
633
|
+
for (const name of names.sort()) {
|
|
634
|
+
const receipt = parseReceipt(await io.readText(join(durableReceiptDir(home, runId), name)));
|
|
635
|
+
if (receipt !== null && (target === void 0 || receipt.target === target)) return receipt;
|
|
636
|
+
}
|
|
637
|
+
} catch {}
|
|
638
|
+
return null;
|
|
639
|
+
}
|
|
640
|
+
//#endregion
|
|
641
|
+
//#region lib/types/coordinator.js
|
|
642
|
+
/**
|
|
643
|
+
* Pure merge transition table. No io, no run shell, no subagents.
|
|
644
|
+
*
|
|
645
|
+
* Every transition returns the whole next row (or a drop), so the caller can persist it
|
|
646
|
+
* through mutateMergeState in one transaction. Invalid transitions are refused by name;
|
|
647
|
+
* a caller never "repairs" a row by guessing which state the operator meant.
|
|
648
|
+
*/
|
|
649
|
+
function invalid(event, state) {
|
|
650
|
+
return {
|
|
651
|
+
ok: false,
|
|
652
|
+
code: "invalid-transition",
|
|
653
|
+
message: "cannot apply " + event.type + " to " + (state ?? "no row")
|
|
654
|
+
};
|
|
655
|
+
}
|
|
656
|
+
/** Drop fields that must not survive a transition; optional fields stay absent. */
|
|
657
|
+
function base(row, at) {
|
|
658
|
+
return {
|
|
659
|
+
id: row.id,
|
|
660
|
+
pairKey: row.pairKey,
|
|
661
|
+
a: row.a,
|
|
662
|
+
b: row.b,
|
|
663
|
+
state: row.state,
|
|
664
|
+
queue: row.queue,
|
|
665
|
+
createdAt: row.createdAt,
|
|
666
|
+
updatedAt: at,
|
|
667
|
+
...row.runId === void 0 ? {} : { runId: row.runId },
|
|
668
|
+
...row.unitId === void 0 ? {} : { unitId: row.unitId },
|
|
669
|
+
...row.origin === void 0 ? {} : { origin: row.origin },
|
|
670
|
+
...row.decision === void 0 ? {} : { decision: row.decision },
|
|
671
|
+
...row.sourceRevision === void 0 ? {} : { sourceRevision: row.sourceRevision },
|
|
672
|
+
...row.targetRevision === void 0 ? {} : { targetRevision: row.targetRevision },
|
|
673
|
+
...row.attempts === void 0 ? {} : { attempts: row.attempts },
|
|
674
|
+
...row.stagingRef === void 0 ? {} : { stagingRef: row.stagingRef },
|
|
675
|
+
...row.stagingExpiresAt === void 0 ? {} : { stagingExpiresAt: row.stagingExpiresAt },
|
|
676
|
+
...row.expired === void 0 ? {} : { expired: row.expired },
|
|
677
|
+
...row.reason === void 0 ? {} : { reason: row.reason },
|
|
678
|
+
...row.target === void 0 ? {} : { target: row.target }
|
|
679
|
+
};
|
|
680
|
+
}
|
|
681
|
+
function next(row, at, state, patch = {}) {
|
|
682
|
+
return {
|
|
683
|
+
ok: true,
|
|
684
|
+
row: {
|
|
685
|
+
...base(row, at),
|
|
686
|
+
state,
|
|
687
|
+
queue: queueOf(state),
|
|
688
|
+
...patch
|
|
689
|
+
}
|
|
690
|
+
};
|
|
691
|
+
}
|
|
692
|
+
function transition(current, event) {
|
|
693
|
+
if (event.type === "candidate") {
|
|
694
|
+
if (current !== void 0) return {
|
|
695
|
+
ok: true,
|
|
696
|
+
row: current
|
|
697
|
+
};
|
|
698
|
+
const key = pairKey(event.a, event.b);
|
|
699
|
+
return {
|
|
700
|
+
ok: true,
|
|
701
|
+
row: {
|
|
702
|
+
id: rowIdOf(key),
|
|
703
|
+
pairKey: key,
|
|
704
|
+
a: event.a,
|
|
705
|
+
b: event.b,
|
|
706
|
+
state: "verify",
|
|
707
|
+
queue: "verify",
|
|
708
|
+
createdAt: event.at,
|
|
709
|
+
updatedAt: event.at
|
|
710
|
+
}
|
|
711
|
+
};
|
|
712
|
+
}
|
|
713
|
+
if (event.type === "source-gone") return {
|
|
714
|
+
ok: true,
|
|
715
|
+
row: null
|
|
716
|
+
};
|
|
717
|
+
if (event.type === "gate-keep") {
|
|
718
|
+
if (current !== void 0) return {
|
|
719
|
+
ok: true,
|
|
720
|
+
row: current
|
|
721
|
+
};
|
|
722
|
+
const key = pairKey(event.a, event.b);
|
|
723
|
+
return {
|
|
724
|
+
ok: true,
|
|
725
|
+
row: {
|
|
726
|
+
id: rowIdOf(key),
|
|
727
|
+
pairKey: key,
|
|
728
|
+
a: event.a,
|
|
729
|
+
b: event.b,
|
|
730
|
+
state: "skipped",
|
|
731
|
+
queue: "skipped",
|
|
732
|
+
origin: "agent",
|
|
733
|
+
createdAt: event.at,
|
|
734
|
+
updatedAt: event.at
|
|
735
|
+
}
|
|
736
|
+
};
|
|
737
|
+
}
|
|
738
|
+
if (current === void 0) return invalid(event, void 0);
|
|
739
|
+
switch (event.type) {
|
|
740
|
+
case "round1-keep": return current.state === "verify" ? next(current, event.at, "skipped", { origin: "agent" }) : invalid(event, current.state);
|
|
741
|
+
case "round1-merge":
|
|
742
|
+
if (current.state !== "verify") return invalid(event, current.state);
|
|
743
|
+
return event.approval === "on" ? next(current, event.at, "pending", {
|
|
744
|
+
...event.decision === void 0 ? {} : { decision: event.decision },
|
|
745
|
+
...event.stagingRef === void 0 ? {} : { stagingRef: event.stagingRef },
|
|
746
|
+
...event.stagingExpiresAt === void 0 ? {} : { stagingExpiresAt: event.stagingExpiresAt }
|
|
747
|
+
}) : next(current, event.at, "approved", {
|
|
748
|
+
...event.decision === void 0 ? {} : { decision: event.decision },
|
|
749
|
+
...event.stagingRef === void 0 ? {} : { stagingRef: event.stagingRef },
|
|
750
|
+
...event.stagingExpiresAt === void 0 ? {} : { stagingExpiresAt: event.stagingExpiresAt }
|
|
751
|
+
});
|
|
752
|
+
case "round1-fail": return current.state === "verify" ? next(current, event.at, "verify", {
|
|
753
|
+
failure: event.failure,
|
|
754
|
+
attempts: (current.attempts ?? 0) + 1
|
|
755
|
+
}) : invalid(event, current.state);
|
|
756
|
+
case "approve": return current.state === "pending" ? next(current, event.at, "approved", { ...event.runId === void 0 ? {} : { runId: event.runId } }) : invalid(event, current.state);
|
|
757
|
+
case "reject": return current.state === "pending" ? next(current, event.at, "skipped", { origin: "human" }) : invalid(event, current.state);
|
|
758
|
+
case "round2-cancel": return current.state === "fuse" || current.state === "approved" ? next(current, event.at, "approved") : invalid(event, current.state);
|
|
759
|
+
case "round2-fail": return current.state === "fuse" || current.state === "approved" ? next(current, event.at, "approved", {
|
|
760
|
+
failure: event.failure,
|
|
761
|
+
attempts: (current.attempts ?? 0) + 1
|
|
762
|
+
}) : invalid(event, current.state);
|
|
763
|
+
case "round2-success": return current.state === "fuse" || current.state === "approved" ? {
|
|
764
|
+
ok: true,
|
|
765
|
+
row: null
|
|
766
|
+
} : invalid(event, current.state);
|
|
767
|
+
case "restore": return current.state === "skipped" ? next(current, event.at, "verify") : invalid(event, current.state);
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
//#endregion
|
|
771
|
+
//#region lib/types/status.js
|
|
772
|
+
function countRows(rows) {
|
|
773
|
+
const counts = {
|
|
774
|
+
verify: 0,
|
|
775
|
+
pending: 0,
|
|
776
|
+
fuse: 0,
|
|
457
777
|
skipped: 0
|
|
458
778
|
};
|
|
459
779
|
for (const row of rows) counts[queueOf(row.state)] += 1;
|
|
@@ -466,7 +786,7 @@ function describe(row) {
|
|
|
466
786
|
}
|
|
467
787
|
function formatStatus(rows) {
|
|
468
788
|
const counts = countRows(rows);
|
|
469
|
-
const lines = ["merge: pending " + String(counts.pending) + ",
|
|
789
|
+
const lines = ["merge: verify " + String(counts.verify) + ", pending " + String(counts.pending) + ", fuse " + String(counts.fuse) + ", skipped " + String(counts.skipped)];
|
|
470
790
|
for (const row of rows) lines.push("- " + describe(row));
|
|
471
791
|
return lines.join("\n");
|
|
472
792
|
}
|
|
@@ -500,22 +820,33 @@ function toRowView(row) {
|
|
|
500
820
|
...row.attempts === void 0 ? {} : { attempts: row.attempts },
|
|
501
821
|
...row.origin === void 0 ? {} : { origin: row.origin },
|
|
502
822
|
...row.stagingExpiresAt === void 0 ? {} : { stagingExpiresAt: row.stagingExpiresAt },
|
|
823
|
+
...row.expired === void 0 ? {} : { expired: row.expired },
|
|
824
|
+
...row.reason === void 0 ? {} : { reason: row.reason },
|
|
825
|
+
summary: [
|
|
826
|
+
row.a + " + " + row.b,
|
|
827
|
+
row.decision?.rationale ?? row.reason ?? "",
|
|
828
|
+
row.failure?.message ?? ""
|
|
829
|
+
],
|
|
503
830
|
actions: row.state === "pending" ? ["/evolution merge approve " + row.id, "/evolution merge reject " + row.id] : row.state === "skipped" ? ["/evolution merge skipped --restore " + row.id] : ["/evolution merge resume " + row.id]
|
|
504
831
|
};
|
|
505
832
|
}
|
|
506
833
|
function buildMergeView(rows, commands, latestRun, approval) {
|
|
507
834
|
const counts = countRows(rows);
|
|
508
835
|
return {
|
|
509
|
-
schemaVersion:
|
|
836
|
+
schemaVersion: 2,
|
|
510
837
|
counts,
|
|
511
838
|
tabs: [
|
|
839
|
+
{
|
|
840
|
+
queue: "verify",
|
|
841
|
+
count: counts.verify
|
|
842
|
+
},
|
|
512
843
|
{
|
|
513
844
|
queue: "pending",
|
|
514
845
|
count: counts.pending
|
|
515
846
|
},
|
|
516
847
|
{
|
|
517
|
-
queue: "
|
|
518
|
-
count: counts.
|
|
848
|
+
queue: "fuse",
|
|
849
|
+
count: counts.fuse
|
|
519
850
|
},
|
|
520
851
|
{
|
|
521
852
|
queue: "skipped",
|
|
@@ -554,8 +885,70 @@ function toRunFact(run) {
|
|
|
554
885
|
};
|
|
555
886
|
}
|
|
556
887
|
//#endregion
|
|
888
|
+
//#region lib/types/domain/markdown.js
|
|
889
|
+
/** Minimal safe Markdown renderer for the read-only body route. */
|
|
890
|
+
function escapeHtml(text) {
|
|
891
|
+
return text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
892
|
+
}
|
|
893
|
+
function renderMarkdown(source) {
|
|
894
|
+
const fence = String.fromCharCode(96, 96, 96);
|
|
895
|
+
const lines = source.split(/\r?\n/);
|
|
896
|
+
const out = [];
|
|
897
|
+
let inCode = false;
|
|
898
|
+
let inList = false;
|
|
899
|
+
for (const line of lines) {
|
|
900
|
+
if (line.startsWith(fence)) {
|
|
901
|
+
if (inCode) {
|
|
902
|
+
out.push("</code></pre>");
|
|
903
|
+
inCode = false;
|
|
904
|
+
} else {
|
|
905
|
+
out.push("<pre><code>");
|
|
906
|
+
inCode = true;
|
|
907
|
+
}
|
|
908
|
+
continue;
|
|
909
|
+
}
|
|
910
|
+
if (inCode) {
|
|
911
|
+
out.push(escapeHtml(line));
|
|
912
|
+
continue;
|
|
913
|
+
}
|
|
914
|
+
const heading = /^(#{1,6})\s+(.*)$/.exec(line);
|
|
915
|
+
if (heading !== null) {
|
|
916
|
+
if (inList) {
|
|
917
|
+
out.push("</ul>");
|
|
918
|
+
inList = false;
|
|
919
|
+
}
|
|
920
|
+
const level = heading[1]?.length ?? 1;
|
|
921
|
+
out.push("<h" + String(level) + ">" + escapeHtml(heading[2] ?? "") + "</h" + String(level) + ">");
|
|
922
|
+
continue;
|
|
923
|
+
}
|
|
924
|
+
if (/^[-*]\s+/.test(line)) {
|
|
925
|
+
if (!inList) {
|
|
926
|
+
out.push("<ul>");
|
|
927
|
+
inList = true;
|
|
928
|
+
}
|
|
929
|
+
out.push("<li>" + escapeHtml(line.replace(/^[-*]\s+/, "")) + "</li>");
|
|
930
|
+
continue;
|
|
931
|
+
}
|
|
932
|
+
if (inList) {
|
|
933
|
+
out.push("</ul>");
|
|
934
|
+
inList = false;
|
|
935
|
+
}
|
|
936
|
+
out.push(line.trim() === "" ? "" : "<p>" + escapeHtml(line) + "</p>");
|
|
937
|
+
}
|
|
938
|
+
if (inList) out.push("</ul>");
|
|
939
|
+
if (inCode) out.push("</code></pre>");
|
|
940
|
+
return out.join("\n");
|
|
941
|
+
}
|
|
942
|
+
//#endregion
|
|
557
943
|
//#region lib/types/merge-routes.js
|
|
558
|
-
const
|
|
944
|
+
const MERGE_PATHS = HOST_ROUTES.merge;
|
|
945
|
+
const MERGE_BASE = MERGE_PATHS.view.slice(0, -4);
|
|
946
|
+
const MERGE_ROUTES = {
|
|
947
|
+
view: MERGE_PATHS.view,
|
|
948
|
+
rows: MERGE_PATHS.rows,
|
|
949
|
+
body: MERGE_PATHS.body ?? MERGE_BASE + "body",
|
|
950
|
+
history: MERGE_PATHS.history ?? MERGE_BASE + "history"
|
|
951
|
+
};
|
|
559
952
|
function makeMergeRoutes(deps, _audit) {
|
|
560
953
|
const absent = () => json(503, {
|
|
561
954
|
ok: false,
|
|
@@ -593,12 +986,12 @@ function makeMergeRoutes(deps, _audit) {
|
|
|
593
986
|
const source = deps.getSource();
|
|
594
987
|
if (source === void 0) return absent();
|
|
595
988
|
const query = url(request).searchParams;
|
|
596
|
-
const queue = query.get("queue") ?? "
|
|
597
|
-
if (queue !== "pending" && queue !== "
|
|
989
|
+
const queue = query.get("queue") ?? "verify";
|
|
990
|
+
if (queue !== "verify" && queue !== "pending" && queue !== "fuse" && queue !== "skipped") return json(400, {
|
|
598
991
|
ok: false,
|
|
599
992
|
error: {
|
|
600
993
|
code: "bad-queue",
|
|
601
|
-
message: "queue must be pending/
|
|
994
|
+
message: "queue must be verify/pending/fuse/skipped"
|
|
602
995
|
}
|
|
603
996
|
});
|
|
604
997
|
const cursor = Number.parseInt(query.get("cursor") ?? "0", 10);
|
|
@@ -610,7 +1003,7 @@ function makeMergeRoutes(deps, _audit) {
|
|
|
610
1003
|
}
|
|
611
1004
|
},
|
|
612
1005
|
{
|
|
613
|
-
path: MERGE_ROUTES.
|
|
1006
|
+
path: MERGE_ROUTES.body,
|
|
614
1007
|
methods: ["GET"],
|
|
615
1008
|
requestBody: "buffered",
|
|
616
1009
|
fetch: async (request) => {
|
|
@@ -618,29 +1011,35 @@ function makeMergeRoutes(deps, _audit) {
|
|
|
618
1011
|
if (denied !== null) return denied;
|
|
619
1012
|
const source = deps.getSource();
|
|
620
1013
|
if (source === void 0) return absent();
|
|
621
|
-
const
|
|
622
|
-
|
|
1014
|
+
const query = url(request).searchParams;
|
|
1015
|
+
const id = query.get("id");
|
|
1016
|
+
const side = query.get("side");
|
|
1017
|
+
if (id === null || id === "" || side !== "a" && side !== "b") return json(400, {
|
|
623
1018
|
ok: false,
|
|
624
1019
|
error: {
|
|
625
|
-
code: "
|
|
626
|
-
message: "id
|
|
1020
|
+
code: "bad-body-query",
|
|
1021
|
+
message: "id and side=a|b are required"
|
|
627
1022
|
}
|
|
628
1023
|
});
|
|
629
|
-
const
|
|
630
|
-
return
|
|
1024
|
+
const content = await source.body(id, side);
|
|
1025
|
+
return content === null ? json(404, {
|
|
631
1026
|
ok: false,
|
|
632
1027
|
error: {
|
|
633
|
-
code: "
|
|
634
|
-
message: "merge
|
|
1028
|
+
code: "body-not-found",
|
|
1029
|
+
message: "merge body not found: " + id
|
|
635
1030
|
}
|
|
636
1031
|
}) : json(200, {
|
|
637
1032
|
ok: true,
|
|
638
|
-
data:
|
|
1033
|
+
data: {
|
|
1034
|
+
content,
|
|
1035
|
+
markdown: true,
|
|
1036
|
+
html: renderMarkdown(content)
|
|
1037
|
+
}
|
|
639
1038
|
});
|
|
640
1039
|
}
|
|
641
1040
|
},
|
|
642
1041
|
{
|
|
643
|
-
path: MERGE_ROUTES.
|
|
1042
|
+
path: MERGE_ROUTES.history,
|
|
644
1043
|
methods: ["GET"],
|
|
645
1044
|
requestBody: "buffered",
|
|
646
1045
|
fetch: async (request) => {
|
|
@@ -648,11 +1047,9 @@ function makeMergeRoutes(deps, _audit) {
|
|
|
648
1047
|
if (denied !== null) return denied;
|
|
649
1048
|
const source = deps.getSource();
|
|
650
1049
|
if (source === void 0) return absent();
|
|
651
|
-
const query = url(request).searchParams;
|
|
652
|
-
const limit = Number.parseInt(query.get("limit") ?? "20", 10);
|
|
653
1050
|
return json(200, {
|
|
654
1051
|
ok: true,
|
|
655
|
-
data: { items:
|
|
1052
|
+
data: { items: await source.history() }
|
|
656
1053
|
});
|
|
657
1054
|
}
|
|
658
1055
|
}
|
|
@@ -660,12 +1057,12 @@ function makeMergeRoutes(deps, _audit) {
|
|
|
660
1057
|
}
|
|
661
1058
|
//#endregion
|
|
662
1059
|
//#region lib/types/commands.js
|
|
663
|
-
const USAGE = "usage: /evolution merge scan | pair <a> <b> | status [id] | report [id] | cancel [id] | runs | doctor | pending | approve <id> | reject <id> | resume [id] [--clear <id>] | skipped [--restore <id>] | undo <target> --run <id>";
|
|
1060
|
+
const USAGE = "usage: /evolution merge scan | pair [--force] <a> <b> | status [id] | report [id] | cancel [id] | runs | doctor | pending [--expired] | candidates [--retry] | approve <id> | reject <id> | resume [id] [--retry] [--clear <id>] | skipped [--restore <id>] | discard <id> | undo <target> --run <id>";
|
|
664
1061
|
function flags(args, allowed) {
|
|
665
1062
|
const values = /* @__PURE__ */ new Map();
|
|
666
1063
|
for (let index = 0; index < args.length; index += 1) {
|
|
667
1064
|
const token = args[index];
|
|
668
|
-
if (!token.startsWith("--")) continue;
|
|
1065
|
+
if (!token.startsWith("--") || token === "--force" || token === "--expired" || token === "--retry") continue;
|
|
669
1066
|
const name = token.slice(2);
|
|
670
1067
|
if (!allowed.includes(name)) return {
|
|
671
1068
|
values,
|
|
@@ -681,8 +1078,12 @@ function flags(args, allowed) {
|
|
|
681
1078
|
}
|
|
682
1079
|
return { values };
|
|
683
1080
|
}
|
|
1081
|
+
function switches(args) {
|
|
1082
|
+
return new Set(args.filter((token) => token === "--force" || token === "--expired" || token === "--retry").map((token) => token.slice(2)));
|
|
1083
|
+
}
|
|
684
1084
|
function positional(args) {
|
|
685
|
-
|
|
1085
|
+
const filtered = args.filter((token) => token !== "--force" && token !== "--expired" && token !== "--retry");
|
|
1086
|
+
return filtered.filter((token, index) => !token.startsWith("--") && !(index > 0 && filtered[index - 1].startsWith("--")));
|
|
686
1087
|
}
|
|
687
1088
|
function parseMergeCommand(argv) {
|
|
688
1089
|
const [verb, ...rest] = argv;
|
|
@@ -692,6 +1093,7 @@ function parseMergeCommand(argv) {
|
|
|
692
1093
|
help: true
|
|
693
1094
|
};
|
|
694
1095
|
const pos = positional(rest);
|
|
1096
|
+
const marked = switches(rest);
|
|
695
1097
|
switch (verb) {
|
|
696
1098
|
case "scan": return pos.length === 0 ? {
|
|
697
1099
|
ok: true,
|
|
@@ -706,7 +1108,8 @@ function parseMergeCommand(argv) {
|
|
|
706
1108
|
command: {
|
|
707
1109
|
verb: "pair",
|
|
708
1110
|
a: pos[0],
|
|
709
|
-
b: pos[1]
|
|
1111
|
+
b: pos[1],
|
|
1112
|
+
...marked.has("force") ? { force: true } : {}
|
|
710
1113
|
}
|
|
711
1114
|
} : {
|
|
712
1115
|
ok: false,
|
|
@@ -764,7 +1167,21 @@ function parseMergeCommand(argv) {
|
|
|
764
1167
|
};
|
|
765
1168
|
case "pending": return pos.length === 0 ? {
|
|
766
1169
|
ok: true,
|
|
767
|
-
command: {
|
|
1170
|
+
command: {
|
|
1171
|
+
verb: "pending",
|
|
1172
|
+
...marked.has("expired") ? { expired: true } : {}
|
|
1173
|
+
}
|
|
1174
|
+
} : {
|
|
1175
|
+
ok: false,
|
|
1176
|
+
code: "usage",
|
|
1177
|
+
message: USAGE
|
|
1178
|
+
};
|
|
1179
|
+
case "candidates": return pos.length === 0 ? {
|
|
1180
|
+
ok: true,
|
|
1181
|
+
command: {
|
|
1182
|
+
verb: "candidates",
|
|
1183
|
+
...marked.has("retry") ? { retry: true } : {}
|
|
1184
|
+
}
|
|
768
1185
|
} : {
|
|
769
1186
|
ok: false,
|
|
770
1187
|
code: "usage",
|
|
@@ -810,7 +1227,8 @@ function parseMergeCommand(argv) {
|
|
|
810
1227
|
command: {
|
|
811
1228
|
verb: "resume",
|
|
812
1229
|
...pos[0] === void 0 ? {} : { id: pos[0] },
|
|
813
|
-
...clear === void 0 ? {} : { clear }
|
|
1230
|
+
...clear === void 0 ? {} : { clear },
|
|
1231
|
+
...marked.has("retry") ? { retry: true } : {}
|
|
814
1232
|
}
|
|
815
1233
|
};
|
|
816
1234
|
}
|
|
@@ -835,6 +1253,17 @@ function parseMergeCommand(argv) {
|
|
|
835
1253
|
}
|
|
836
1254
|
};
|
|
837
1255
|
}
|
|
1256
|
+
case "discard": return pos.length === 1 ? {
|
|
1257
|
+
ok: true,
|
|
1258
|
+
command: {
|
|
1259
|
+
verb: "discard",
|
|
1260
|
+
id: pos[0]
|
|
1261
|
+
}
|
|
1262
|
+
} : {
|
|
1263
|
+
ok: false,
|
|
1264
|
+
code: "usage",
|
|
1265
|
+
message: USAGE
|
|
1266
|
+
};
|
|
838
1267
|
case "undo": {
|
|
839
1268
|
const parsed = flags(rest, ["run"]);
|
|
840
1269
|
if (parsed.error !== void 0) return {
|
|
@@ -861,7 +1290,7 @@ function parseMergeCommand(argv) {
|
|
|
861
1290
|
default: return {
|
|
862
1291
|
ok: false,
|
|
863
1292
|
code: "usage",
|
|
864
|
-
message: "unknown merge command: " + verb + "\nusage: /evolution merge scan | pair <a> <b> | status [id] | report [id] | cancel [id] | runs | doctor | pending | approve <id> | reject <id> | resume [id] [--clear <id>] | skipped [--restore <id>] | undo <target> --run <id>"
|
|
1293
|
+
message: "unknown merge command: " + verb + "\nusage: /evolution merge scan | pair [--force] <a> <b> | status [id] | report [id] | cancel [id] | runs | doctor | pending [--expired] | candidates [--retry] | approve <id> | reject <id> | resume [id] [--retry] [--clear <id>] | skipped [--restore <id>] | discard <id> | undo <target> --run <id>"
|
|
865
1294
|
};
|
|
866
1295
|
}
|
|
867
1296
|
}
|
|
@@ -879,7 +1308,15 @@ async function handleReadOnlyMergeCommand(command, io, home) {
|
|
|
879
1308
|
}
|
|
880
1309
|
return formatStatus(loaded.state.rows);
|
|
881
1310
|
}
|
|
882
|
-
if (command.verb === "pending")
|
|
1311
|
+
if (command.verb === "pending") {
|
|
1312
|
+
const loaded = await loadMergeState(io, home);
|
|
1313
|
+
const rows = command.expired === true ? loaded.state.rows.filter((row) => row.expired !== void 0) : loaded.state.rows;
|
|
1314
|
+
return command.expired === true ? formatStatus(rows) : formatQueue(rows, "pending");
|
|
1315
|
+
}
|
|
1316
|
+
if (command.verb === "candidates") {
|
|
1317
|
+
const loaded = await loadMergeState(io, home);
|
|
1318
|
+
return formatQueue(command.retry === true ? loaded.state.rows.filter((row) => row.state === "approved") : loaded.state.rows, command.retry === true ? "fuse" : "verify");
|
|
1319
|
+
}
|
|
883
1320
|
if (command.verb === "skipped") return formatQueue((await loadMergeState(io, home)).state.rows, "skipped");
|
|
884
1321
|
return null;
|
|
885
1322
|
}
|
|
@@ -887,6 +1324,13 @@ function mergeCommandHelp() {
|
|
|
887
1324
|
return mergeUsage();
|
|
888
1325
|
}
|
|
889
1326
|
//#endregion
|
|
1327
|
+
//#region lib/types/domain/errors.js
|
|
1328
|
+
function formatMergeError(error) {
|
|
1329
|
+
if (error.kind === "parameter") return "merge: " + error.message + " (" + error.code + ")";
|
|
1330
|
+
const detail = "\n" + (error.detail ?? "no additional detail");
|
|
1331
|
+
return "merge failed: " + error.message + " (" + error.code + ")" + detail + "\nnext: fix the target or rerun after the cause is gone";
|
|
1332
|
+
}
|
|
1333
|
+
//#endregion
|
|
890
1334
|
//#region lib/types/merge-command.js
|
|
891
1335
|
function createReadOnlyMergeService(io, home) {
|
|
892
1336
|
return { async execute(command) {
|
|
@@ -918,7 +1362,11 @@ async function runMergeCommand(service, rawInput, invocation) {
|
|
|
918
1362
|
} catch (error) {
|
|
919
1363
|
return {
|
|
920
1364
|
ok: false,
|
|
921
|
-
text:
|
|
1365
|
+
text: formatMergeError({
|
|
1366
|
+
kind: "runtime",
|
|
1367
|
+
code: "merge-command-failed",
|
|
1368
|
+
message: error instanceof Error ? error.message : String(error)
|
|
1369
|
+
})
|
|
922
1370
|
};
|
|
923
1371
|
}
|
|
924
1372
|
}
|
|
@@ -959,11 +1407,58 @@ function isSkippedPair(keys, a, b) {
|
|
|
959
1407
|
return keys.has(pairKey(a, b));
|
|
960
1408
|
}
|
|
961
1409
|
//#endregion
|
|
1410
|
+
//#region lib/types/domain/pair-index.js
|
|
1411
|
+
function parsePairIndex(raw) {
|
|
1412
|
+
const index = /* @__PURE__ */ new Map();
|
|
1413
|
+
if (raw === null) return index;
|
|
1414
|
+
let parsed;
|
|
1415
|
+
try {
|
|
1416
|
+
parsed = JSON.parse(raw);
|
|
1417
|
+
} catch {
|
|
1418
|
+
return index;
|
|
1419
|
+
}
|
|
1420
|
+
const body = parsed;
|
|
1421
|
+
if (!Array.isArray(body.entries)) return index;
|
|
1422
|
+
for (const value of body.entries) {
|
|
1423
|
+
const entry = value;
|
|
1424
|
+
if (typeof entry.pairKey !== "string" || typeof entry.runId !== "string" || typeof entry.result !== "string") continue;
|
|
1425
|
+
if (![
|
|
1426
|
+
"merged",
|
|
1427
|
+
"kept",
|
|
1428
|
+
"failed",
|
|
1429
|
+
"cancelled",
|
|
1430
|
+
"undone"
|
|
1431
|
+
].includes(entry.result)) continue;
|
|
1432
|
+
index.set(entry.pairKey, entry);
|
|
1433
|
+
}
|
|
1434
|
+
return index;
|
|
1435
|
+
}
|
|
1436
|
+
function serializePairIndex(index) {
|
|
1437
|
+
return JSON.stringify({
|
|
1438
|
+
schemaVersion: 1,
|
|
1439
|
+
entries: [...index.values()]
|
|
1440
|
+
}, null, 2);
|
|
1441
|
+
}
|
|
1442
|
+
//#endregion
|
|
1443
|
+
//#region lib/types/domain/pair-index-store.js
|
|
1444
|
+
/** Durable pairKey index at <home>/merge/pair-index.json. */
|
|
1445
|
+
function pairIndexPath(home) {
|
|
1446
|
+
return join(home, "merge", "pair-index.json");
|
|
1447
|
+
}
|
|
1448
|
+
async function recordPair(io, home, entry) {
|
|
1449
|
+
await transactIo(io, pairIndexPath(home), (current) => {
|
|
1450
|
+
const index = parsePairIndex(current);
|
|
1451
|
+
index.set(entry.pairKey, entry);
|
|
1452
|
+
return serializePairIndex(index);
|
|
1453
|
+
});
|
|
1454
|
+
}
|
|
1455
|
+
//#endregion
|
|
962
1456
|
//#region lib/types/scan-run.js
|
|
963
1457
|
/**
|
|
964
1458
|
* The scan pair loop. Registry/report lifecycle stays in withMergeRun; this module owns
|
|
965
1459
|
* candidate → round1 → pending/fuse → commit transitions and returns the report draft.
|
|
966
1460
|
*/
|
|
1461
|
+
const isAborted = (signal) => signal.aborted;
|
|
967
1462
|
function at(now) {
|
|
968
1463
|
return new Date(now()).toISOString();
|
|
969
1464
|
}
|
|
@@ -995,8 +1490,9 @@ async function runScan(options) {
|
|
|
995
1490
|
const candidates = await options.ports.screening(options.signal);
|
|
996
1491
|
const before = await loadMergeState(options.io, options.home);
|
|
997
1492
|
const queueBefore = {
|
|
1493
|
+
verify: 0,
|
|
998
1494
|
pending: 0,
|
|
999
|
-
|
|
1495
|
+
fuse: 0,
|
|
1000
1496
|
skipped: 0
|
|
1001
1497
|
};
|
|
1002
1498
|
for (const row of before.state.rows) queueBefore[row.queue] += 1;
|
|
@@ -1012,11 +1508,11 @@ async function runScan(options) {
|
|
|
1012
1508
|
const remaining = [];
|
|
1013
1509
|
let stopReason = "completed";
|
|
1014
1510
|
for (let index = 0; index < candidates.length; index += 1) {
|
|
1015
|
-
if (options.signal
|
|
1511
|
+
if (isAborted(options.signal)) {
|
|
1016
1512
|
stopReason = "cancelled";
|
|
1017
1513
|
remaining.push(...candidates.slice(index).map((candidate) => ({
|
|
1018
1514
|
pairKey: pairKey(candidate.a, candidate.b),
|
|
1019
|
-
queue: "
|
|
1515
|
+
queue: "verify"
|
|
1020
1516
|
})));
|
|
1021
1517
|
break;
|
|
1022
1518
|
}
|
|
@@ -1024,7 +1520,7 @@ async function runScan(options) {
|
|
|
1024
1520
|
stopReason = "timeout";
|
|
1025
1521
|
remaining.push(...candidates.slice(index).map((candidate) => ({
|
|
1026
1522
|
pairKey: pairKey(candidate.a, candidate.b),
|
|
1027
|
-
queue: "
|
|
1523
|
+
queue: "verify"
|
|
1028
1524
|
})));
|
|
1029
1525
|
break;
|
|
1030
1526
|
}
|
|
@@ -1051,7 +1547,7 @@ async function runScan(options) {
|
|
|
1051
1547
|
pairKey: pairKey(candidate.a, candidate.b),
|
|
1052
1548
|
a: candidate.a,
|
|
1053
1549
|
b: candidate.b,
|
|
1054
|
-
queue: "
|
|
1550
|
+
queue: "verify",
|
|
1055
1551
|
candidate,
|
|
1056
1552
|
outcome: "failed",
|
|
1057
1553
|
archived: [],
|
|
@@ -1068,6 +1564,14 @@ async function runScan(options) {
|
|
|
1068
1564
|
failure: failureOf("decide", error)
|
|
1069
1565
|
};
|
|
1070
1566
|
}
|
|
1567
|
+
if (isAborted(options.signal)) {
|
|
1568
|
+
stopReason = "cancelled";
|
|
1569
|
+
remaining.push(...candidates.slice(index).map((candidate) => ({
|
|
1570
|
+
pairKey: pairKey(candidate.a, candidate.b),
|
|
1571
|
+
queue: "verify"
|
|
1572
|
+
})));
|
|
1573
|
+
break;
|
|
1574
|
+
}
|
|
1071
1575
|
if (round1.kind === "failed") {
|
|
1072
1576
|
const next = transition(row, {
|
|
1073
1577
|
type: "round1-fail",
|
|
@@ -1090,6 +1594,11 @@ async function runScan(options) {
|
|
|
1090
1594
|
if (next.ok && next.row !== null) {
|
|
1091
1595
|
row = next.row;
|
|
1092
1596
|
await upsertRow(options.io, options.home, options.now, row);
|
|
1597
|
+
await recordPair(options.io, options.home, {
|
|
1598
|
+
pairKey: row.pairKey,
|
|
1599
|
+
runId: row.runId ?? "merge",
|
|
1600
|
+
result: "kept"
|
|
1601
|
+
});
|
|
1093
1602
|
}
|
|
1094
1603
|
pairs.push(pairOf(row, candidate, "kept", round1.decision === void 0 ? {} : { decision: round1.decision }));
|
|
1095
1604
|
totals.kept += 1;
|
|
@@ -1105,8 +1614,8 @@ async function runScan(options) {
|
|
|
1105
1614
|
type: "round1-merge",
|
|
1106
1615
|
approval: "on",
|
|
1107
1616
|
decision: round1.decision,
|
|
1108
|
-
...round1.
|
|
1109
|
-
...round1.
|
|
1617
|
+
...round1.sourceRevision === void 0 ? {} : { sourceRevision: round1.sourceRevision },
|
|
1618
|
+
...round1.targetRevision === void 0 ? {} : { targetRevision: round1.targetRevision },
|
|
1110
1619
|
at: at(options.now)
|
|
1111
1620
|
});
|
|
1112
1621
|
if (next.ok && next.row !== null) {
|
|
@@ -1135,6 +1644,22 @@ async function runScan(options) {
|
|
|
1135
1644
|
}
|
|
1136
1645
|
row = fuse.row;
|
|
1137
1646
|
await upsertRow(options.io, options.home, options.now, row);
|
|
1647
|
+
if (isAborted(options.signal)) {
|
|
1648
|
+
const cancelled = transition(row, {
|
|
1649
|
+
type: "round2-cancel",
|
|
1650
|
+
at: at(options.now)
|
|
1651
|
+
});
|
|
1652
|
+
if (cancelled.ok && cancelled.row !== null) {
|
|
1653
|
+
row = cancelled.row;
|
|
1654
|
+
await upsertRow(options.io, options.home, options.now, row);
|
|
1655
|
+
}
|
|
1656
|
+
stopReason = "cancelled";
|
|
1657
|
+
remaining.push(...candidates.slice(index).map((candidate) => ({
|
|
1658
|
+
pairKey: pairKey(candidate.a, candidate.b),
|
|
1659
|
+
queue: "fuse"
|
|
1660
|
+
})));
|
|
1661
|
+
break;
|
|
1662
|
+
}
|
|
1138
1663
|
await stage("author", {
|
|
1139
1664
|
done: index,
|
|
1140
1665
|
total: candidates.length
|
|
@@ -1207,7 +1732,7 @@ function mergeReportDigestPath(home, runId) {
|
|
|
1207
1732
|
function isReport(value) {
|
|
1208
1733
|
if (typeof value !== "object" || value === null) return false;
|
|
1209
1734
|
const report = value;
|
|
1210
|
-
return report.schemaVersion ===
|
|
1735
|
+
return report.schemaVersion === 2 && report.kind === "merge" && typeof report.runId === "string" && typeof report.command === "string" && typeof report.startedAt === "number" && typeof report.endedAt === "number" && Array.isArray(report.pairs) && typeof report.totals === "object" && Array.isArray(report.notes);
|
|
1211
1736
|
}
|
|
1212
1737
|
function digest(report) {
|
|
1213
1738
|
const lines = [];
|
|
@@ -1254,8 +1779,9 @@ function emptyDraft(reason) {
|
|
|
1254
1779
|
scope: {
|
|
1255
1780
|
candidatePairs: 0,
|
|
1256
1781
|
queueBefore: {
|
|
1782
|
+
verify: 0,
|
|
1257
1783
|
pending: 0,
|
|
1258
|
-
|
|
1784
|
+
fuse: 0,
|
|
1259
1785
|
skipped: 0
|
|
1260
1786
|
}
|
|
1261
1787
|
},
|
|
@@ -1272,6 +1798,7 @@ function emptyDraft(reason) {
|
|
|
1272
1798
|
async function withMergeRun(options) {
|
|
1273
1799
|
const handle = await options.registry.begin("merge", {
|
|
1274
1800
|
command: options.command,
|
|
1801
|
+
mergeStatus: "running",
|
|
1275
1802
|
...options.total === void 0 ? {} : { total: options.total }
|
|
1276
1803
|
});
|
|
1277
1804
|
const startedAt = handle.startedAt;
|
|
@@ -1280,64 +1807,77 @@ async function withMergeRun(options) {
|
|
|
1280
1807
|
done: 0,
|
|
1281
1808
|
total: options.total
|
|
1282
1809
|
});
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1810
|
+
const finish = async () => {
|
|
1811
|
+
let draft;
|
|
1812
|
+
try {
|
|
1813
|
+
draft = await options.body({
|
|
1814
|
+
runId: handle.id,
|
|
1815
|
+
startedAt,
|
|
1816
|
+
signal: handle.signal,
|
|
1817
|
+
stage
|
|
1818
|
+
});
|
|
1819
|
+
} catch (error) {
|
|
1820
|
+
draft = emptyDraft("run body failed: " + (error instanceof Error ? error.message : String(error)));
|
|
1821
|
+
}
|
|
1822
|
+
const report = {
|
|
1823
|
+
schemaVersion: 2,
|
|
1286
1824
|
runId: handle.id,
|
|
1825
|
+
kind: "merge",
|
|
1826
|
+
command: options.command,
|
|
1287
1827
|
startedAt,
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1828
|
+
endedAt: options.now(),
|
|
1829
|
+
stopReason: draft.stopReason,
|
|
1830
|
+
approval: options.approval,
|
|
1831
|
+
scope: draft.scope,
|
|
1832
|
+
pairs: draft.pairs,
|
|
1833
|
+
totals: draft.totals,
|
|
1834
|
+
notes: draft.notes,
|
|
1835
|
+
...draft.remaining === void 0 ? {} : { remaining: draft.remaining }
|
|
1836
|
+
};
|
|
1837
|
+
let reportPath;
|
|
1838
|
+
try {
|
|
1839
|
+
reportPath = await writeMergeReport(options.io, options.home, report);
|
|
1840
|
+
} catch (error) {
|
|
1841
|
+
await options.registry.settle(handle.id, {
|
|
1842
|
+
state: "failed",
|
|
1843
|
+
failure: "report write failed: " + (error instanceof Error ? error.message : String(error))
|
|
1844
|
+
});
|
|
1845
|
+
return {
|
|
1846
|
+
ok: false,
|
|
1847
|
+
runId: handle.id,
|
|
1848
|
+
message: "report write failed"
|
|
1849
|
+
};
|
|
1850
|
+
}
|
|
1851
|
+
const runState = report.stopReason === "completed" ? "succeeded" : report.stopReason === "cancelled" ? "cancelled" : "failed";
|
|
1852
|
+
const mergeStatus = report.pairs.some((pair) => pair.outcome === "pending") ? "awaiting-approval" : runState;
|
|
1313
1853
|
await options.registry.settle(handle.id, {
|
|
1314
|
-
state:
|
|
1315
|
-
|
|
1854
|
+
state: runState,
|
|
1855
|
+
resultRef: reportPath,
|
|
1856
|
+
mergeStatus,
|
|
1857
|
+
...runState === "failed" ? { failure: "merge " + report.stopReason } : {}
|
|
1316
1858
|
});
|
|
1859
|
+
if (report.stopReason === "completed") return {
|
|
1860
|
+
ok: true,
|
|
1861
|
+
runId: handle.id,
|
|
1862
|
+
reportPath,
|
|
1863
|
+
report
|
|
1864
|
+
};
|
|
1317
1865
|
return {
|
|
1318
1866
|
ok: false,
|
|
1319
1867
|
runId: handle.id,
|
|
1320
|
-
message: "
|
|
1868
|
+
message: "merge " + report.stopReason,
|
|
1869
|
+
reportPath
|
|
1321
1870
|
};
|
|
1322
|
-
}
|
|
1323
|
-
const runState = report.stopReason === "completed" ? "succeeded" : report.stopReason === "cancelled" ? "cancelled" : "failed";
|
|
1324
|
-
await options.registry.settle(handle.id, {
|
|
1325
|
-
state: runState,
|
|
1326
|
-
resultRef: reportPath,
|
|
1327
|
-
...runState === "failed" ? { failure: "merge " + report.stopReason } : {}
|
|
1328
|
-
});
|
|
1329
|
-
if (report.stopReason === "completed") return {
|
|
1330
|
-
ok: true,
|
|
1331
|
-
runId: handle.id,
|
|
1332
|
-
reportPath,
|
|
1333
|
-
report
|
|
1334
|
-
};
|
|
1335
|
-
return {
|
|
1336
|
-
ok: false,
|
|
1337
|
-
runId: handle.id,
|
|
1338
|
-
message: "merge " + report.stopReason,
|
|
1339
|
-
reportPath
|
|
1340
1871
|
};
|
|
1872
|
+
if (options.detached === true) {
|
|
1873
|
+
finish().catch(() => {});
|
|
1874
|
+
return {
|
|
1875
|
+
ok: true,
|
|
1876
|
+
accepted: true,
|
|
1877
|
+
runId: handle.id
|
|
1878
|
+
};
|
|
1879
|
+
}
|
|
1880
|
+
return await finish();
|
|
1341
1881
|
}
|
|
1342
1882
|
//#endregion
|
|
1343
1883
|
//#region lib/types/scan-command.js
|
|
@@ -1352,6 +1892,7 @@ async function runScanCommand(options) {
|
|
|
1352
1892
|
mergeApproval: options.approval
|
|
1353
1893
|
},
|
|
1354
1894
|
now: options.now,
|
|
1895
|
+
...options.detached === void 0 ? {} : { detached: options.detached },
|
|
1355
1896
|
async body(ctx) {
|
|
1356
1897
|
const ports = options.makePorts === void 0 ? options.ports : options.makePorts(ctx.runId);
|
|
1357
1898
|
if (ports === void 0) throw new Error("scan requires ports or makePorts");
|
|
@@ -1431,7 +1972,17 @@ async function recordRound2Outcome(io, home, now, row, outcome) {
|
|
|
1431
1972
|
if (transition(row, {
|
|
1432
1973
|
type: "round2-success",
|
|
1433
1974
|
at
|
|
1434
|
-
}).ok)
|
|
1975
|
+
}).ok) {
|
|
1976
|
+
await dropRow(io, home, now, row.id);
|
|
1977
|
+
const target = outcome.target?.actual ?? outcome.target?.requested ?? row.decision?.target;
|
|
1978
|
+
await recordPair(io, home, {
|
|
1979
|
+
pairKey: row.pairKey,
|
|
1980
|
+
runId: row.runId ?? "merge",
|
|
1981
|
+
result: "merged",
|
|
1982
|
+
...target === void 0 ? {} : { target },
|
|
1983
|
+
...outcome.unitId === void 0 ? {} : { reportRef: outcome.unitId }
|
|
1984
|
+
});
|
|
1985
|
+
}
|
|
1435
1986
|
return {
|
|
1436
1987
|
merged: true,
|
|
1437
1988
|
pair: {
|
|
@@ -1460,7 +2011,15 @@ async function recordRound2Outcome(io, home, now, row, outcome) {
|
|
|
1460
2011
|
failure,
|
|
1461
2012
|
at
|
|
1462
2013
|
});
|
|
1463
|
-
if (next.ok && next.row !== null)
|
|
2014
|
+
if (next.ok && next.row !== null) {
|
|
2015
|
+
await upsertRow(io, home, now, next.row);
|
|
2016
|
+
await recordPair(io, home, {
|
|
2017
|
+
pairKey: row.pairKey,
|
|
2018
|
+
runId: row.runId ?? "merge",
|
|
2019
|
+
result: "failed",
|
|
2020
|
+
...row.decision?.target === void 0 ? {} : { target: row.decision.target }
|
|
2021
|
+
});
|
|
2022
|
+
}
|
|
1464
2023
|
return {
|
|
1465
2024
|
merged: false,
|
|
1466
2025
|
pair: {
|
|
@@ -1525,8 +2084,9 @@ async function runApproveCommand(options) {
|
|
|
1525
2084
|
scope: {
|
|
1526
2085
|
candidatePairs: 1,
|
|
1527
2086
|
queueBefore: {
|
|
2087
|
+
verify: 0,
|
|
1528
2088
|
pending: 0,
|
|
1529
|
-
|
|
2089
|
+
fuse: 1,
|
|
1530
2090
|
skipped: 0
|
|
1531
2091
|
}
|
|
1532
2092
|
},
|
|
@@ -1547,276 +2107,112 @@ async function runApproveCommand(options) {
|
|
|
1547
2107
|
kept: 0,
|
|
1548
2108
|
pending: 0,
|
|
1549
2109
|
failed: 1
|
|
1550
|
-
}
|
|
1551
|
-
};
|
|
1552
|
-
}
|
|
1553
|
-
});
|
|
1554
|
-
}
|
|
1555
|
-
//#endregion
|
|
1556
|
-
//#region lib/types/resume-command.js
|
|
1557
|
-
async function runResumeCommand(options) {
|
|
1558
|
-
const row = (await loadMergeState(options.io, options.home)).state.rows.find((item) => item.id === options.rowId);
|
|
1559
|
-
if (row === void 0) return {
|
|
1560
|
-
ok: false,
|
|
1561
|
-
message: "merge row not found: " + options.rowId
|
|
1562
|
-
};
|
|
1563
|
-
if (row.state === "verify") return await runScanCommand({
|
|
1564
|
-
io: options.io,
|
|
1565
|
-
home: options.home,
|
|
1566
|
-
registry: options.registry,
|
|
1567
|
-
approval: true,
|
|
1568
|
-
platformWrite: options.platformWrite,
|
|
1569
|
-
now: options.now,
|
|
1570
|
-
...options.makePorts === void 0 ? { ports: {
|
|
1571
|
-
...options.ports,
|
|
1572
|
-
screening: () => Promise.resolve([{
|
|
1573
|
-
a: row.a,
|
|
1574
|
-
b: row.b
|
|
1575
|
-
}])
|
|
1576
|
-
} } : { makePorts: (runId) => {
|
|
1577
|
-
const makePorts = options.makePorts;
|
|
1578
|
-
if (makePorts === void 0) throw new Error("resume makePorts disappeared");
|
|
1579
|
-
return {
|
|
1580
|
-
...makePorts(runId),
|
|
1581
|
-
screening: () => Promise.resolve([{
|
|
1582
|
-
a: row.a,
|
|
1583
|
-
b: row.b
|
|
1584
|
-
}])
|
|
1585
|
-
};
|
|
1586
|
-
} },
|
|
1587
|
-
...options.timeoutMinutes === void 0 ? {} : { timeoutMinutes: options.timeoutMinutes }
|
|
1588
|
-
});
|
|
1589
|
-
if (row.state !== "fuse") return {
|
|
1590
|
-
ok: false,
|
|
1591
|
-
message: "row is not resumable from state " + row.state
|
|
1592
|
-
};
|
|
1593
|
-
const decision = row.decision;
|
|
1594
|
-
if (decision === void 0) return {
|
|
1595
|
-
ok: false,
|
|
1596
|
-
message: "fuse row has no decision"
|
|
1597
|
-
};
|
|
1598
|
-
return await withMergeRun({
|
|
1599
|
-
io: options.io,
|
|
1600
|
-
home: options.home,
|
|
1601
|
-
registry: options.registry,
|
|
1602
|
-
command: "resume",
|
|
1603
|
-
approval: {
|
|
1604
|
-
platformWrite: options.platformWrite,
|
|
1605
|
-
mergeApproval: true
|
|
1606
|
-
},
|
|
1607
|
-
now: options.now,
|
|
1608
|
-
async body(ctx) {
|
|
1609
|
-
await ctx.stage("author", {
|
|
1610
|
-
done: 0,
|
|
1611
|
-
total: 1
|
|
1612
|
-
});
|
|
1613
|
-
let outcome;
|
|
1614
|
-
try {
|
|
1615
|
-
outcome = options.round2WithRun === void 0 ? await options.ports.round2(row, decision, ctx.signal) : await options.round2WithRun(row, decision, ctx.signal, ctx.runId);
|
|
1616
|
-
} catch (error) {
|
|
1617
|
-
outcome = {
|
|
1618
|
-
kind: "failed",
|
|
1619
|
-
failure: {
|
|
1620
|
-
stage: "author",
|
|
1621
|
-
code: "merge-failed",
|
|
1622
|
-
message: error instanceof Error ? error.message : String(error),
|
|
1623
|
-
retryable: true,
|
|
1624
|
-
retryFrom: "agent2"
|
|
1625
|
-
}
|
|
1626
|
-
};
|
|
1627
|
-
}
|
|
1628
|
-
const record = await recordRound2Outcome(options.io, options.home, options.now, row, outcome);
|
|
1629
|
-
return {
|
|
1630
|
-
stopReason: record.merged ? "completed" : "failed",
|
|
1631
|
-
scope: {
|
|
1632
|
-
candidatePairs: 1,
|
|
1633
|
-
queueBefore: {
|
|
1634
|
-
pending: 0,
|
|
1635
|
-
retry: 1,
|
|
1636
|
-
skipped: 0
|
|
1637
|
-
}
|
|
1638
|
-
},
|
|
1639
|
-
pairs: [record.pair],
|
|
1640
|
-
totals: record.merged ? {
|
|
1641
|
-
merged: 1,
|
|
1642
|
-
kept: 0,
|
|
1643
|
-
pending: 0,
|
|
1644
|
-
failed: 0
|
|
1645
|
-
} : {
|
|
1646
|
-
merged: 0,
|
|
1647
|
-
kept: 0,
|
|
1648
|
-
pending: 0,
|
|
1649
|
-
failed: 1
|
|
1650
|
-
},
|
|
1651
|
-
notes: []
|
|
1652
|
-
};
|
|
1653
|
-
}
|
|
1654
|
-
});
|
|
1655
|
-
}
|
|
1656
|
-
//#endregion
|
|
1657
|
-
//#region lib/types/staging.js
|
|
1658
|
-
/**
|
|
1659
|
-
* Staging is a run ARTIFACT, not a queue: <home>/merge/runs/<runId>/.
|
|
1660
|
-
*
|
|
1661
|
-
* createStaging writes a sibling .partial tree first and renames it into place; the .stage
|
|
1662
|
-
* marker is the last write inside the partial. A directory without that marker is a
|
|
1663
|
-
* half-created artifact and reads as unknown, never as "empty".
|
|
1664
|
-
*/
|
|
1665
|
-
const STAGING_SCHEMA_VERSION = 1;
|
|
1666
|
-
function stageKeyOf(runId, pairKey) {
|
|
1667
|
-
return runId + "/pairs/" + createHash("sha256").update(pairKey).digest("hex").slice(0, 16);
|
|
1668
|
-
}
|
|
1669
|
-
function stagingRoot(home, runId) {
|
|
1670
|
-
return join(home, "merge", "runs", runId);
|
|
1671
|
-
}
|
|
1672
|
-
/** Relative support paths only; the staging tree must never escape its run directory. */
|
|
1673
|
-
function safeRelative$2(path) {
|
|
1674
|
-
if (path === "" || path.startsWith("/") || path.startsWith("\\") || path.includes("\0")) return false;
|
|
1675
|
-
return path.replace(/\\/g, "/").split("/").every((part) => part !== "" && part !== "." && part !== "..");
|
|
1676
|
-
}
|
|
1677
|
-
function manifestText(manifest) {
|
|
1678
|
-
return JSON.stringify(manifest, null, 2) + "\n";
|
|
1679
|
-
}
|
|
1680
|
-
async function createStaging(io, home, input) {
|
|
1681
|
-
const final = stagingRoot(home, input.runId);
|
|
1682
|
-
if (await io.exists(final)) return {
|
|
1683
|
-
ok: false,
|
|
1684
|
-
message: "staging already exists for run " + input.runId
|
|
1685
|
-
};
|
|
1686
|
-
const names = /* @__PURE__ */ new Set();
|
|
1687
|
-
for (const source of input.sources) {
|
|
1688
|
-
if (names.has(source.name)) return {
|
|
1689
|
-
ok: false,
|
|
1690
|
-
message: "duplicate source " + source.name
|
|
1691
|
-
};
|
|
1692
|
-
names.add(source.name);
|
|
1693
|
-
for (const file of source.files) if (!safeRelative$2(file.path)) return {
|
|
1694
|
-
ok: false,
|
|
1695
|
-
message: "unsafe support path: " + file.path
|
|
1696
|
-
};
|
|
1697
|
-
}
|
|
1698
|
-
const partial = final + ".partial";
|
|
1699
|
-
try {
|
|
1700
|
-
await io.remove(partial);
|
|
1701
|
-
for (const source of input.sources) {
|
|
1702
|
-
const dir = join(partial, "sources", source.name);
|
|
1703
|
-
await io.writeText(join(dir, "SKILL.md"), source.body);
|
|
1704
|
-
for (const file of source.files) await io.writeText(join(dir, ...file.path.split("/")), file.content);
|
|
1705
|
-
}
|
|
1706
|
-
const at = input.now();
|
|
1707
|
-
const manifest = {
|
|
1708
|
-
schemaVersion: 1,
|
|
1709
|
-
runId: input.runId,
|
|
1710
|
-
target: input.target,
|
|
1711
|
-
...input.mode === void 0 ? {} : { mode: input.mode },
|
|
1712
|
-
sources: input.sources.map((source) => source.name),
|
|
1713
|
-
promptVersion: input.promptVersion,
|
|
1714
|
-
createdAt: at,
|
|
1715
|
-
expiresAt: at + Math.max(0, input.ttlDays) * 864e5
|
|
1716
|
-
};
|
|
1717
|
-
await io.writeText(join(partial, "manifest.json"), manifestText(manifest));
|
|
1718
|
-
await io.writeText(join(partial, ".stage"), "");
|
|
1719
|
-
await io.rename(partial, final);
|
|
1720
|
-
return {
|
|
1721
|
-
ok: true,
|
|
1722
|
-
path: final,
|
|
1723
|
-
manifest
|
|
1724
|
-
};
|
|
1725
|
-
} catch (error) {
|
|
1726
|
-
try {
|
|
1727
|
-
await io.remove(partial);
|
|
1728
|
-
} catch {}
|
|
1729
|
-
return {
|
|
1730
|
-
ok: false,
|
|
1731
|
-
message: "staging create failed: " + (error instanceof Error ? error.message : String(error))
|
|
1732
|
-
};
|
|
1733
|
-
}
|
|
1734
|
-
}
|
|
1735
|
-
async function readStaging(io, home, runId) {
|
|
1736
|
-
const path = stagingRoot(home, runId);
|
|
1737
|
-
if (!await io.exists(path)) return probeAbsent();
|
|
1738
|
-
if (!await io.exists(join(path, ".stage"))) return probeUnknown("staging " + runId + " has no .stage marker (half-created)");
|
|
1739
|
-
const probe = await probeText(io, join(path, "manifest.json"));
|
|
1740
|
-
if (!isPresent(probe)) return probe;
|
|
1741
|
-
try {
|
|
1742
|
-
const parsed = JSON.parse(probe.value);
|
|
1743
|
-
if (parsed.schemaVersion !== 1 || parsed.runId !== runId || typeof parsed.target !== "string" || !Array.isArray(parsed.sources)) return probeUnknown("staging manifest has an unexpected shape");
|
|
1744
|
-
return probePresent({
|
|
1745
|
-
path,
|
|
1746
|
-
manifest: parsed
|
|
1747
|
-
});
|
|
1748
|
-
} catch (error) {
|
|
1749
|
-
return probeUnknown("staging manifest is not JSON: " + (error instanceof Error ? error.message : String(error)));
|
|
1750
|
-
}
|
|
1751
|
-
}
|
|
1752
|
-
async function ensureStaging(io, home, input) {
|
|
1753
|
-
const existing = await readStaging(io, home, input.runId);
|
|
1754
|
-
if (existing.kind === "present") return {
|
|
1755
|
-
ok: true,
|
|
1756
|
-
path: stagingRoot(home, input.runId),
|
|
1757
|
-
manifest: existing.value.manifest
|
|
1758
|
-
};
|
|
1759
|
-
return createStaging(io, home, input);
|
|
1760
|
-
}
|
|
1761
|
-
async function setStagingTarget(io, home, stageKey, target, mode) {
|
|
1762
|
-
const view = await readStaging(io, home, stageKey);
|
|
1763
|
-
if (view.kind !== "present") return false;
|
|
1764
|
-
const manifest = Object.assign({}, view.value.manifest, { target }, mode === void 0 ? {} : { mode });
|
|
1765
|
-
await io.writeText(join(stagingRoot(home, stageKey), "manifest.json"), JSON.stringify(manifest, null, 2) + "\n");
|
|
1766
|
-
return true;
|
|
1767
|
-
}
|
|
1768
|
-
async function dropStaging(io, home, stageKey) {
|
|
1769
|
-
await io.remove(stagingRoot(home, stageKey));
|
|
2110
|
+
}
|
|
2111
|
+
};
|
|
2112
|
+
}
|
|
2113
|
+
});
|
|
1770
2114
|
}
|
|
1771
2115
|
//#endregion
|
|
1772
|
-
//#region lib/types/
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
}
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
2116
|
+
//#region lib/types/resume-command.js
|
|
2117
|
+
async function runResumeCommand(options) {
|
|
2118
|
+
const row = (await loadMergeState(options.io, options.home)).state.rows.find((item) => item.id === options.rowId);
|
|
2119
|
+
if (row === void 0) return {
|
|
2120
|
+
ok: false,
|
|
2121
|
+
message: "merge row not found: " + options.rowId
|
|
2122
|
+
};
|
|
2123
|
+
if (row.state === "verify") return await runScanCommand({
|
|
2124
|
+
io: options.io,
|
|
2125
|
+
home: options.home,
|
|
2126
|
+
registry: options.registry,
|
|
2127
|
+
approval: true,
|
|
2128
|
+
platformWrite: options.platformWrite,
|
|
2129
|
+
now: options.now,
|
|
2130
|
+
...options.makePorts === void 0 ? { ports: {
|
|
2131
|
+
...options.ports,
|
|
2132
|
+
screening: () => Promise.resolve([{
|
|
2133
|
+
a: row.a,
|
|
2134
|
+
b: row.b
|
|
2135
|
+
}])
|
|
2136
|
+
} } : { makePorts: (runId) => {
|
|
2137
|
+
const makePorts = options.makePorts;
|
|
2138
|
+
if (makePorts === void 0) throw new Error("resume makePorts disappeared");
|
|
2139
|
+
return {
|
|
2140
|
+
...makePorts(runId),
|
|
2141
|
+
screening: () => Promise.resolve([{
|
|
2142
|
+
a: row.a,
|
|
2143
|
+
b: row.b
|
|
2144
|
+
}])
|
|
2145
|
+
};
|
|
2146
|
+
} },
|
|
2147
|
+
...options.timeoutMinutes === void 0 ? {} : { timeoutMinutes: options.timeoutMinutes }
|
|
2148
|
+
});
|
|
2149
|
+
if (row.state !== "fuse" && row.state !== "approved") return {
|
|
2150
|
+
ok: false,
|
|
2151
|
+
message: "row is not resumable from state " + row.state
|
|
2152
|
+
};
|
|
2153
|
+
const decision = row.decision;
|
|
2154
|
+
if (decision === void 0) return {
|
|
2155
|
+
ok: false,
|
|
2156
|
+
message: "fuse row has no decision"
|
|
2157
|
+
};
|
|
2158
|
+
return await withMergeRun({
|
|
2159
|
+
io: options.io,
|
|
2160
|
+
home: options.home,
|
|
2161
|
+
registry: options.registry,
|
|
2162
|
+
command: "resume",
|
|
2163
|
+
approval: {
|
|
2164
|
+
platformWrite: options.platformWrite,
|
|
2165
|
+
mergeApproval: true
|
|
2166
|
+
},
|
|
2167
|
+
now: options.now,
|
|
2168
|
+
async body(ctx) {
|
|
2169
|
+
await ctx.stage("author", {
|
|
2170
|
+
done: 0,
|
|
2171
|
+
total: 1
|
|
2172
|
+
});
|
|
2173
|
+
let outcome;
|
|
2174
|
+
try {
|
|
2175
|
+
outcome = options.round2WithRun === void 0 ? await options.ports.round2(row, decision, ctx.signal) : await options.round2WithRun(row, decision, ctx.signal, ctx.runId);
|
|
2176
|
+
} catch (error) {
|
|
2177
|
+
outcome = {
|
|
2178
|
+
kind: "failed",
|
|
2179
|
+
failure: {
|
|
2180
|
+
stage: "author",
|
|
2181
|
+
code: "merge-failed",
|
|
2182
|
+
message: error instanceof Error ? error.message : String(error),
|
|
2183
|
+
retryable: true,
|
|
2184
|
+
retryFrom: "agent2"
|
|
2185
|
+
}
|
|
2186
|
+
};
|
|
2187
|
+
}
|
|
2188
|
+
const record = await recordRound2Outcome(options.io, options.home, options.now, row, outcome);
|
|
2189
|
+
return {
|
|
2190
|
+
stopReason: record.merged ? "completed" : "failed",
|
|
2191
|
+
scope: {
|
|
2192
|
+
candidatePairs: 1,
|
|
2193
|
+
queueBefore: {
|
|
2194
|
+
verify: 0,
|
|
2195
|
+
pending: 0,
|
|
2196
|
+
fuse: 1,
|
|
2197
|
+
skipped: 0
|
|
2198
|
+
}
|
|
2199
|
+
},
|
|
2200
|
+
pairs: [record.pair],
|
|
2201
|
+
totals: record.merged ? {
|
|
2202
|
+
merged: 1,
|
|
2203
|
+
kept: 0,
|
|
2204
|
+
pending: 0,
|
|
2205
|
+
failed: 0
|
|
2206
|
+
} : {
|
|
2207
|
+
merged: 0,
|
|
2208
|
+
kept: 0,
|
|
2209
|
+
pending: 0,
|
|
2210
|
+
failed: 1
|
|
2211
|
+
},
|
|
2212
|
+
notes: []
|
|
2213
|
+
};
|
|
1817
2214
|
}
|
|
1818
|
-
}
|
|
1819
|
-
return null;
|
|
2215
|
+
});
|
|
1820
2216
|
}
|
|
1821
2217
|
//#endregion
|
|
1822
2218
|
//#region lib/types/undo-merge.js
|
|
@@ -1900,7 +2296,7 @@ async function mergeDoctor(io, home, configuredRoot) {
|
|
|
1900
2296
|
key: "state",
|
|
1901
2297
|
label: "合并状态",
|
|
1902
2298
|
status: loaded.note === void 0 ? "ok" : "warn",
|
|
1903
|
-
detail: loaded.note ?? "pairs " + String(loaded.state.rows.length) + ";pending " + String(counts.pending) + " /
|
|
2299
|
+
detail: loaded.note ?? "pairs " + String(loaded.state.rows.length) + ";verify " + String(counts.verify) + " / pending " + String(counts.pending) + " / fuse " + String(counts.fuse) + " / skipped " + String(counts.skipped)
|
|
1904
2300
|
});
|
|
1905
2301
|
const modelRoot = configuredRoot !== void 0 && configuredRoot !== "" ? configuredRoot : process.env.SKILL_MERGE_MODELS_ROOT;
|
|
1906
2302
|
if (modelRoot === void 0 || modelRoot === "") rows.push({
|
|
@@ -2494,6 +2890,17 @@ async function assembleStagedCommit(io, home, runId, port, options) {
|
|
|
2494
2890
|
});
|
|
2495
2891
|
}
|
|
2496
2892
|
//#endregion
|
|
2893
|
+
//#region lib/types/domain/commit-journal-store.js
|
|
2894
|
+
/** Durable commit journal: <home>/merge/journals/<run>.json. */
|
|
2895
|
+
function journalPath(home, runId) {
|
|
2896
|
+
return join(home, "merge", "journals", runId.replace(/[^A-Za-z0-9_-]+/g, "_") + ".json");
|
|
2897
|
+
}
|
|
2898
|
+
async function writeJournal(io, home, journal) {
|
|
2899
|
+
const path = journalPath(home, journal.runId);
|
|
2900
|
+
await io.writeText(path, JSON.stringify(journal, null, 2));
|
|
2901
|
+
return path;
|
|
2902
|
+
}
|
|
2903
|
+
//#endregion
|
|
2497
2904
|
//#region lib/types/commit-execute.js
|
|
2498
2905
|
async function commitStagedRun(io, home, runId, port, options) {
|
|
2499
2906
|
const assembled = await assembleStagedCommit(io, home, runId, port, {
|
|
@@ -2504,12 +2911,32 @@ async function commitStagedRun(io, home, runId, port, options) {
|
|
|
2504
2911
|
ok: false,
|
|
2505
2912
|
message: assembled.message
|
|
2506
2913
|
};
|
|
2507
|
-
const
|
|
2508
|
-
|
|
2509
|
-
|
|
2510
|
-
|
|
2511
|
-
|
|
2914
|
+
const journal = {
|
|
2915
|
+
runId,
|
|
2916
|
+
target: assembled.plan.target,
|
|
2917
|
+
create: !assembled.plan.targetExists,
|
|
2918
|
+
sources: assembled.plan.sources,
|
|
2919
|
+
phase: "prepared",
|
|
2920
|
+
updatedAt: new Date(options.now()).toISOString()
|
|
2512
2921
|
};
|
|
2922
|
+
try {
|
|
2923
|
+
await writeJournal(io, home, journal);
|
|
2924
|
+
} catch {}
|
|
2925
|
+
const result = await port.commitMerge(assembled.plan);
|
|
2926
|
+
if (!result.ok) {
|
|
2927
|
+
try {
|
|
2928
|
+
await writeJournal(io, home, {
|
|
2929
|
+
...journal,
|
|
2930
|
+
phase: "failed",
|
|
2931
|
+
updatedAt: new Date(options.now()).toISOString()
|
|
2932
|
+
});
|
|
2933
|
+
} catch {}
|
|
2934
|
+
return {
|
|
2935
|
+
ok: false,
|
|
2936
|
+
message: result.message,
|
|
2937
|
+
plan: assembled.plan
|
|
2938
|
+
};
|
|
2939
|
+
}
|
|
2513
2940
|
if (result.unitId === void 0) return {
|
|
2514
2941
|
ok: true,
|
|
2515
2942
|
plan: assembled.plan,
|
|
@@ -2523,11 +2950,20 @@ async function commitStagedRun(io, home, runId, port, options) {
|
|
|
2523
2950
|
target: assembled.plan.target,
|
|
2524
2951
|
create: !assembled.plan.targetExists,
|
|
2525
2952
|
sources: assembled.plan.sources,
|
|
2953
|
+
...assembled.plan.sources[0] === void 0 ? {} : { pairKey: pairKey(assembled.plan.target, assembled.plan.sources[0]) },
|
|
2526
2954
|
archived: result.archived ?? [],
|
|
2527
2955
|
at: options.now()
|
|
2528
2956
|
};
|
|
2529
2957
|
try {
|
|
2530
2958
|
await writeCommitReceipt(io, home, receipt);
|
|
2959
|
+
try {
|
|
2960
|
+
await writeJournal(io, home, {
|
|
2961
|
+
...journal,
|
|
2962
|
+
unitId: result.unitId,
|
|
2963
|
+
phase: "committed",
|
|
2964
|
+
updatedAt: new Date(options.now()).toISOString()
|
|
2965
|
+
});
|
|
2966
|
+
} catch {}
|
|
2531
2967
|
return {
|
|
2532
2968
|
ok: true,
|
|
2533
2969
|
plan: assembled.plan,
|
|
@@ -2902,11 +3338,12 @@ function createNewChildRound2(options) {
|
|
|
2902
3338
|
//#endregion
|
|
2903
3339
|
//#region lib/types/adapters/library.js
|
|
2904
3340
|
function skillLibraryPort(skills) {
|
|
3341
|
+
const archivedNames = skills.archivedNames;
|
|
2905
3342
|
return {
|
|
2906
3343
|
readSkill: (name) => skills.read(name),
|
|
2907
3344
|
isArchived: async (name) => {
|
|
2908
|
-
if (typeof
|
|
2909
|
-
const names = await
|
|
3345
|
+
if (typeof archivedNames !== "function") return false;
|
|
3346
|
+
const names = await archivedNames.call(skills);
|
|
2910
3347
|
return names === null ? true : names.includes(name);
|
|
2911
3348
|
},
|
|
2912
3349
|
commitMerge: (plan) => skills.commitMerge(plan)
|
|
@@ -2941,6 +3378,7 @@ function createMergeRuntime(deps) {
|
|
|
2941
3378
|
};
|
|
2942
3379
|
return { async execute(command, context) {
|
|
2943
3380
|
const parent = context?.parent ?? deps.parent;
|
|
3381
|
+
const detached = deps.detached !== false;
|
|
2944
3382
|
if (command.verb === "pair") {
|
|
2945
3383
|
const signal = new AbortController().signal;
|
|
2946
3384
|
const cards = await deps.cards(signal);
|
|
@@ -2948,33 +3386,62 @@ function createMergeRuntime(deps) {
|
|
|
2948
3386
|
a: command.a,
|
|
2949
3387
|
b: command.b
|
|
2950
3388
|
}];
|
|
3389
|
+
let forced;
|
|
3390
|
+
if (command.force === true) {
|
|
3391
|
+
const key = pairKey(command.a, command.b);
|
|
3392
|
+
forced = (await loadMergeState(deps.io, deps.home)).state.rows.find((row) => row.pairKey === key);
|
|
3393
|
+
if (forced === void 0 || forced.decision === void 0) return {
|
|
3394
|
+
ok: false,
|
|
3395
|
+
text: "pair --force 需要已有决策,找不到该 pair 的 decision: " + key
|
|
3396
|
+
};
|
|
3397
|
+
}
|
|
3398
|
+
const approval = command.force === true ? false : deps.params.mergeApproval;
|
|
2951
3399
|
const result = await runScanCommand({
|
|
2952
3400
|
...common,
|
|
2953
|
-
approval
|
|
2954
|
-
|
|
2955
|
-
|
|
2956
|
-
|
|
2957
|
-
|
|
2958
|
-
|
|
2959
|
-
|
|
2960
|
-
|
|
2961
|
-
|
|
2962
|
-
|
|
2963
|
-
|
|
2964
|
-
|
|
2965
|
-
|
|
2966
|
-
|
|
2967
|
-
|
|
2968
|
-
|
|
2969
|
-
|
|
3401
|
+
approval,
|
|
3402
|
+
...detached ? { detached: true } : {},
|
|
3403
|
+
makePorts: (runId) => {
|
|
3404
|
+
const ports = createChannelScanPorts({
|
|
3405
|
+
io: deps.io,
|
|
3406
|
+
home: deps.home,
|
|
3407
|
+
runId,
|
|
3408
|
+
cards,
|
|
3409
|
+
screening: () => Promise.resolve(pair),
|
|
3410
|
+
channel: deps.channel,
|
|
3411
|
+
parent,
|
|
3412
|
+
allow: deps.allow,
|
|
3413
|
+
...deps.cwd === void 0 ? {} : { cwd: deps.cwd },
|
|
3414
|
+
timeoutMs: deps.params.mergePairTimeoutMinutes * 6e4,
|
|
3415
|
+
approval,
|
|
3416
|
+
commitPort: skillLibraryPort(deps.skills),
|
|
3417
|
+
now: deps.now,
|
|
3418
|
+
...stageHooks
|
|
3419
|
+
});
|
|
3420
|
+
return forced === void 0 ? ports : {
|
|
3421
|
+
...ports,
|
|
3422
|
+
round1: () => Promise.resolve({
|
|
3423
|
+
kind: "merge",
|
|
3424
|
+
decision: forced.decision
|
|
3425
|
+
})
|
|
3426
|
+
};
|
|
3427
|
+
}
|
|
2970
3428
|
});
|
|
2971
|
-
|
|
2972
|
-
ok: true,
|
|
2973
|
-
text: "merge pair 完成:merged " + String(result.report.totals.merged) + " / pending " + String(result.report.totals.pending) + " / failed " + String(result.report.totals.failed)
|
|
2974
|
-
} : {
|
|
3429
|
+
if (!result.ok) return {
|
|
2975
3430
|
ok: false,
|
|
2976
3431
|
text: result.message
|
|
2977
3432
|
};
|
|
3433
|
+
if ("accepted" in result) return {
|
|
3434
|
+
ok: true,
|
|
3435
|
+
text: "merge pair 已启动:" + result.runId
|
|
3436
|
+
};
|
|
3437
|
+
if (!("report" in result)) return {
|
|
3438
|
+
ok: false,
|
|
3439
|
+
text: "merge pair started without a report"
|
|
3440
|
+
};
|
|
3441
|
+
return {
|
|
3442
|
+
ok: true,
|
|
3443
|
+
text: "merge pair 完成:merged " + String(result.report.totals.merged) + " / pending " + String(result.report.totals.pending) + " / failed " + String(result.report.totals.failed)
|
|
3444
|
+
};
|
|
2978
3445
|
}
|
|
2979
3446
|
if (command.verb === "scan") {
|
|
2980
3447
|
const signal = new AbortController().signal;
|
|
@@ -2985,6 +3452,7 @@ function createMergeRuntime(deps) {
|
|
|
2985
3452
|
const result = await runScanCommand({
|
|
2986
3453
|
...common,
|
|
2987
3454
|
approval: deps.params.mergeApproval,
|
|
3455
|
+
...detached ? { detached: true } : {},
|
|
2988
3456
|
makePorts: (runId) => createChannelScanPorts({
|
|
2989
3457
|
io: deps.io,
|
|
2990
3458
|
home: deps.home,
|
|
@@ -3010,13 +3478,22 @@ function createMergeRuntime(deps) {
|
|
|
3010
3478
|
...stageHooks
|
|
3011
3479
|
})
|
|
3012
3480
|
});
|
|
3013
|
-
|
|
3014
|
-
ok: true,
|
|
3015
|
-
text: "merge scan 完成:merged " + String(result.report.totals.merged) + " / pending " + String(result.report.totals.pending) + " / failed " + String(result.report.totals.failed)
|
|
3016
|
-
} : {
|
|
3481
|
+
if (!result.ok) return {
|
|
3017
3482
|
ok: false,
|
|
3018
3483
|
text: result.message
|
|
3019
3484
|
};
|
|
3485
|
+
if ("accepted" in result) return {
|
|
3486
|
+
ok: true,
|
|
3487
|
+
text: "merge scan 已启动:" + result.runId
|
|
3488
|
+
};
|
|
3489
|
+
if (!("report" in result)) return {
|
|
3490
|
+
ok: false,
|
|
3491
|
+
text: "merge scan started without a report"
|
|
3492
|
+
};
|
|
3493
|
+
return {
|
|
3494
|
+
ok: true,
|
|
3495
|
+
text: "merge scan 完成:merged " + String(result.report.totals.merged) + " / pending " + String(result.report.totals.pending) + " / failed " + String(result.report.totals.failed)
|
|
3496
|
+
};
|
|
3020
3497
|
}
|
|
3021
3498
|
if (command.verb === "reject") {
|
|
3022
3499
|
const row = (await loadMergeState(deps.io, deps.home)).state.rows.find((item) => item.id === command.id);
|
|
@@ -3087,6 +3564,22 @@ function createMergeRuntime(deps) {
|
|
|
3087
3564
|
text: report.value.slice(0, 12e3)
|
|
3088
3565
|
};
|
|
3089
3566
|
}
|
|
3567
|
+
if (command.verb === "discard") {
|
|
3568
|
+
const row = (await loadMergeState(deps.io, deps.home)).state.rows.find((item) => item.id === command.id);
|
|
3569
|
+
if (row === void 0) return {
|
|
3570
|
+
ok: false,
|
|
3571
|
+
text: "merge row not found: " + command.id
|
|
3572
|
+
};
|
|
3573
|
+
if (row.state !== "pending" && row.state !== "approved" && row.state !== "fuse") return {
|
|
3574
|
+
ok: false,
|
|
3575
|
+
text: "merge row is not discardable: " + command.id
|
|
3576
|
+
};
|
|
3577
|
+
await dropRow(deps.io, deps.home, deps.now, row.id);
|
|
3578
|
+
return {
|
|
3579
|
+
ok: true,
|
|
3580
|
+
text: "已丢弃合并行:" + command.id
|
|
3581
|
+
};
|
|
3582
|
+
}
|
|
3090
3583
|
if (command.verb === "undo") {
|
|
3091
3584
|
const receipt = await readCommitReceipt(deps.io, deps.home, command.run, command.target);
|
|
3092
3585
|
if (receipt === null) return {
|
|
@@ -3098,6 +3591,12 @@ function createMergeRuntime(deps) {
|
|
|
3098
3591
|
text: "receipt target is " + receipt.target + ", not " + command.target
|
|
3099
3592
|
};
|
|
3100
3593
|
const result = await undoMergeReceipt(receipt, skillLibraryUndoPort(deps.skills));
|
|
3594
|
+
if (result.ok && receipt.pairKey !== void 0) await recordPair(deps.io, deps.home, {
|
|
3595
|
+
pairKey: receipt.pairKey,
|
|
3596
|
+
runId: receipt.parentRunId ?? receipt.runId,
|
|
3597
|
+
result: "undone",
|
|
3598
|
+
target: receipt.target
|
|
3599
|
+
});
|
|
3101
3600
|
return {
|
|
3102
3601
|
ok: result.ok,
|
|
3103
3602
|
text: result.message
|
|
@@ -3129,7 +3628,7 @@ function createMergeRuntime(deps) {
|
|
|
3129
3628
|
};
|
|
3130
3629
|
}
|
|
3131
3630
|
const loaded = await loadMergeState(deps.io, deps.home);
|
|
3132
|
-
const row = command.id === void 0 ? loaded.state.rows.find((item) => item.state === "fuse" || item.state === "verify") : loaded.state.rows.find((item) => item.id === command.id);
|
|
3631
|
+
const row = command.id === void 0 ? loaded.state.rows.find((item) => item.state === "fuse" || item.state === "verify" || command.retry === true && item.state === "approved") : loaded.state.rows.find((item) => item.id === command.id);
|
|
3133
3632
|
if (row === void 0) return {
|
|
3134
3633
|
ok: false,
|
|
3135
3634
|
text: "merge row not found: " + (command.id ?? "(auto)")
|
|
@@ -3188,13 +3687,22 @@ function createMergeRuntime(deps) {
|
|
|
3188
3687
|
...stageHooks
|
|
3189
3688
|
})
|
|
3190
3689
|
});
|
|
3191
|
-
|
|
3192
|
-
ok: true,
|
|
3193
|
-
text: "merge resume 完成:merged " + String(result.report.totals.merged)
|
|
3194
|
-
} : {
|
|
3690
|
+
if (!result.ok) return {
|
|
3195
3691
|
ok: false,
|
|
3196
3692
|
text: result.message
|
|
3197
3693
|
};
|
|
3694
|
+
if ("accepted" in result) return {
|
|
3695
|
+
ok: true,
|
|
3696
|
+
text: "merge resume 已启动:" + result.runId
|
|
3697
|
+
};
|
|
3698
|
+
if (!("report" in result)) return {
|
|
3699
|
+
ok: false,
|
|
3700
|
+
text: "merge resume started without a report"
|
|
3701
|
+
};
|
|
3702
|
+
return {
|
|
3703
|
+
ok: true,
|
|
3704
|
+
text: "merge resume 完成:merged " + String(result.report.totals.merged)
|
|
3705
|
+
};
|
|
3198
3706
|
}
|
|
3199
3707
|
if (command.verb === "approve") {
|
|
3200
3708
|
const signal = new AbortController().signal;
|
|
@@ -3215,13 +3723,22 @@ function createMergeRuntime(deps) {
|
|
|
3215
3723
|
now: deps.now
|
|
3216
3724
|
}) }
|
|
3217
3725
|
});
|
|
3218
|
-
|
|
3219
|
-
ok: true,
|
|
3220
|
-
text: "merge approve 完成:merged " + String(result.report.totals.merged)
|
|
3221
|
-
} : {
|
|
3726
|
+
if (!result.ok) return {
|
|
3222
3727
|
ok: false,
|
|
3223
3728
|
text: result.message
|
|
3224
3729
|
};
|
|
3730
|
+
if ("accepted" in result) return {
|
|
3731
|
+
ok: true,
|
|
3732
|
+
text: "merge approve 已启动:" + result.runId
|
|
3733
|
+
};
|
|
3734
|
+
if (!("report" in result)) return {
|
|
3735
|
+
ok: false,
|
|
3736
|
+
text: "merge approve started without a report"
|
|
3737
|
+
};
|
|
3738
|
+
return {
|
|
3739
|
+
ok: true,
|
|
3740
|
+
text: "merge approve 完成:merged " + String(result.report.totals.merged)
|
|
3741
|
+
};
|
|
3225
3742
|
}
|
|
3226
3743
|
const text = await handleReadOnlyMergeCommand(command, deps.io, deps.home);
|
|
3227
3744
|
if (text !== null) return {
|
|
@@ -3672,6 +4189,9 @@ function apply(ctx, rawConfig = {}) {
|
|
|
3672
4189
|
if (registry === void 0) return;
|
|
3673
4190
|
const io = evolutionIoAdapter(() => registry.provider());
|
|
3674
4191
|
const home = evolutionHome();
|
|
4192
|
+
loadMergeState(io, home).then((loaded) => cleanupExpiredStaging(io, home, Date.now(), new Set(loaded.state.rows.map((row) => row.runId).filter((id) => id !== void 0)))).catch((error) => {
|
|
4193
|
+
ctx.logger.warn("merge staging cleanup failed: " + (error instanceof Error ? error.message : String(error)));
|
|
4194
|
+
});
|
|
3675
4195
|
const runs = newRunRegistry({
|
|
3676
4196
|
io,
|
|
3677
4197
|
home,
|
|
@@ -3707,8 +4227,11 @@ function apply(ctx, rawConfig = {}) {
|
|
|
3707
4227
|
io,
|
|
3708
4228
|
home,
|
|
3709
4229
|
engine: { screen },
|
|
3710
|
-
select:
|
|
3711
|
-
|
|
4230
|
+
select: selectWithErrors,
|
|
4231
|
+
cfg: {
|
|
4232
|
+
...engineConfig ?? {},
|
|
4233
|
+
judge: "deepseek"
|
|
4234
|
+
},
|
|
3712
4235
|
...runner === void 0 ? {} : { runner },
|
|
3713
4236
|
async onSelection(selection) {
|
|
3714
4237
|
const loaded = await loadMergeState(io, home);
|
|
@@ -3736,6 +4259,40 @@ function apply(ctx, rawConfig = {}) {
|
|
|
3736
4259
|
runs() {
|
|
3737
4260
|
return Promise.resolve(runs.runs());
|
|
3738
4261
|
},
|
|
4262
|
+
async body(id, side) {
|
|
4263
|
+
const row = (await loadMergeState(io, home)).state.rows.find((item) => item.id === id);
|
|
4264
|
+
if (row === void 0) return null;
|
|
4265
|
+
return await skills.read(side === "a" ? row.a : row.b);
|
|
4266
|
+
},
|
|
4267
|
+
async history() {
|
|
4268
|
+
const receipts = await readCommitReceipts(io, home);
|
|
4269
|
+
const items = /* @__PURE__ */ new Map();
|
|
4270
|
+
for (const receipt of receipts) for (const source of receipt.sources) {
|
|
4271
|
+
const key = source + ":" + receipt.runId;
|
|
4272
|
+
if (items.has(key)) continue;
|
|
4273
|
+
items.set(key, {
|
|
4274
|
+
name: source,
|
|
4275
|
+
kind: "merged",
|
|
4276
|
+
reason: "Merged into " + receipt.target,
|
|
4277
|
+
lastVersion: 1,
|
|
4278
|
+
archivedAt: new Date(receipt.at).toISOString(),
|
|
4279
|
+
sourceRunId: receipt.runId
|
|
4280
|
+
});
|
|
4281
|
+
}
|
|
4282
|
+
const archived = await skills.archivedNames?.() ?? [];
|
|
4283
|
+
const seen = new Set([...items.values()].map((item) => item.name));
|
|
4284
|
+
for (const name of archived) {
|
|
4285
|
+
if (seen.has(name)) continue;
|
|
4286
|
+
items.set("archived:" + name, {
|
|
4287
|
+
name,
|
|
4288
|
+
kind: "other",
|
|
4289
|
+
reason: "archived",
|
|
4290
|
+
lastVersion: 1,
|
|
4291
|
+
archivedAt: new Date(Date.now()).toISOString()
|
|
4292
|
+
});
|
|
4293
|
+
}
|
|
4294
|
+
return [...items.values()];
|
|
4295
|
+
},
|
|
3739
4296
|
approval() {
|
|
3740
4297
|
return params.mergeApproval;
|
|
3741
4298
|
},
|
|
@@ -3811,4 +4368,4 @@ function apply(ctx, rawConfig = {}) {
|
|
|
3811
4368
|
});
|
|
3812
4369
|
}
|
|
3813
4370
|
//#endregion
|
|
3814
|
-
export { Config, MERGE_DOCTOR_SCHEMA_VERSION, MERGE_MAX_ATTEMPTS, MERGE_PAIR_STATES, MERGE_PARAM_DEFAULTS, MERGE_PROMPT_VERSION, MERGE_QUEUES, MERGE_ROUTES, MERGE_STAGE_TOOL_NAME, MERGE_STATE_VERSION, STAGING_SCHEMA_VERSION, STANDBY_PROMPT, StageToolRegistry, apply, applyStageTool, approvePending, assembleStagedCommit, buildCommitPlan, buildMergeView, buildRound1Prompt, buildRound2Prompt, buildStandbyPrompt, canRetry, commitReceiptPath, commitStagedRun, committedPairs, configureMergePresentation, copyStagedFile, countRows, createChannelScanPorts, createEngineRunner, createEngineScreening, createMergeCommand, createMergeRuntime, createMergeStageTool, createNewChildRound2, createReadOnlyMergeService, createStaging, dropRow, dropStaging, durableReceiptDir, durableReceiptPath, emptyMergeState, engineConfigFromEnv, ensureStaging, formatQueue, formatStatus, handleReadOnlyMergeCommand, hasBlockingFinding, inject, isSkippedPair, listStagedFiles, loadMergeState, makeMergeRoutes, mergeCommandHelp, mergeDoctor, mergeReportDigestPath, mergeReportPath, mergeRunIdsToSupersede, mergeStatePath, mergeUsage, moveRow, mutateMergeState, pageQueue, pairKey, parseDecision, parseMergeCommand, queueOf, readCommitReceipt, readMergeReport, readStagedFile, readStagedTree, readStaging, recordRound2Outcome, rejectPending, resolveMergeParams, resolveTargetName, rewriteFrontmatterName, rowIdOf, rowsOf, runApproveCommand, runAuthoring, runMergeCommand, runNewChildAuthoring, runPairCommand, runResumeCommand, runRound1OverChannel, runScan, runScanCommand, setStagingTarget, skillLibraryPort, skillLibraryUndoPort, skippedPairKeys, snapshotCards, stageKeyOf, stageOf, stagingRoot, subagentChannelAdapter, supersedeMergeRuns, toRowView, toRunFact, transition, undoMergeReceipt, upsertRow, validateStagedTree, withMergeRun, writeCommitReceipt, writeMergeReport, writeStagedFile };
|
|
4371
|
+
export { Config, MERGE_DOCTOR_SCHEMA_VERSION, MERGE_MAX_ATTEMPTS, MERGE_PAIR_STATES, MERGE_PARAM_DEFAULTS, MERGE_PROMPT_VERSION, MERGE_QUEUES, MERGE_ROUTES, MERGE_STAGE_TOOL_NAME, MERGE_STATE_VERSION, STAGING_SCHEMA_VERSION, STANDBY_PROMPT, StageToolRegistry, apply, applyStageTool, approvePending, assembleStagedCommit, buildCommitPlan, buildMergeView, buildRound1Prompt, buildRound2Prompt, buildStandbyPrompt, canRetry, cleanupExpiredStaging, commitReceiptPath, commitStagedRun, committedPairs, configureMergePresentation, copyStagedFile, countRows, createChannelScanPorts, createEngineRunner, createEngineScreening, createMergeCommand, createMergeRuntime, createMergeStageTool, createNewChildRound2, createReadOnlyMergeService, createStaging, dropRow, dropStaging, durableReceiptDir, durableReceiptPath, emptyMergeState, engineConfigFromEnv, ensureStaging, formatQueue, formatStatus, handleReadOnlyMergeCommand, hasBlockingFinding, inject, isSkippedPair, listStagedFiles, loadMergeState, makeMergeRoutes, mergeCommandHelp, mergeDoctor, mergeReportDigestPath, mergeReportPath, mergeRunIdsToSupersede, mergeStatePath, mergeUsage, moveRow, mutateMergeState, pageQueue, pairKey, parseDecision, parseMergeCommand, queueOf, readCommitReceipt, readCommitReceipts, readMergeReport, readStagedFile, readStagedTree, readStaging, recordRound2Outcome, rejectPending, resolveMergeParams, resolveTargetName, rewriteFrontmatterName, rowIdOf, rowsOf, runApproveCommand, runAuthoring, runMergeCommand, runNewChildAuthoring, runPairCommand, runResumeCommand, runRound1OverChannel, runScan, runScanCommand, setStagingTarget, skillLibraryPort, skillLibraryUndoPort, skippedPairKeys, snapshotCards, stageKeyOf, stageOf, stagingRoot, subagentChannelAdapter, supersedeMergeRuns, toRowView, toRunFact, transition, undoMergeReceipt, upsertRow, validateStagedTree, withMergeRun, writeCommitReceipt, writeMergeReport, writeStagedFile };
|