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