@lmzhen/dsh-evolution-merge 0.30.9 → 0.30.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/index.js +157 -74
- package/lib/types/approve-command.js +49 -36
- package/lib/types/commit-execute.js +5 -1
- package/lib/types/commit-recovery.js +8 -1
- package/lib/types/index.js +4 -0
- package/lib/types/read-commands.d.ts +3 -1
- package/lib/types/read-commands.js +3 -2
- package/lib/types/runtime.d.ts +2 -0
- package/lib/types/runtime.js +8 -2
- package/lib/types/target-claim.d.ts +7 -0
- package/lib/types/target-claim.js +46 -0
- package/package.json +3 -3
package/lib/index.js
CHANGED
|
@@ -3,7 +3,7 @@ import z from "@deepseek-ai/schemastery";
|
|
|
3
3
|
import { pairKeyOf, screen, selectCandidates, toEngineCandidate } from "@lmzhen/dsh-evolution-merge-engine";
|
|
4
4
|
import { DEFAULT_SKILL_LIMITS, authorityFrom, contentHash, createSubagentChannel, evolutionHome, evolutionIoAdapter, executeUnitUndo, isPresent, newRunRegistry, newSkillLibrary, parseFrontmatter, planUnitUndoAcross, policyStageLimits, probeAbsent, probeList, probePresent, probeText, probeUnknown, scanContentThreats, skillsRoot, supportRefs, transactIo, validateFrontmatter, validateSupportPath } from "@lmzhen/dsh-evolution-core";
|
|
5
5
|
import { HOST_ROUTES, json, methodNotAllowed, registerHostRoutes } from "@lmzhen/dsh-evolution-core/http-routes";
|
|
6
|
-
import { createHash } from "node:crypto";
|
|
6
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
7
7
|
//#region lib/types/adapters/select.js
|
|
8
8
|
/** Host selector correction: engine selector does not route judge errors to candidates. */
|
|
9
9
|
function selectWithErrors(report) {
|
|
@@ -799,7 +799,18 @@ async function recoverCommitJournals(io, home, now, skillRoot, restoreSource, is
|
|
|
799
799
|
});
|
|
800
800
|
continue;
|
|
801
801
|
}
|
|
802
|
-
|
|
802
|
+
let snapshot;
|
|
803
|
+
try {
|
|
804
|
+
snapshot = await readCommitSnapshot(io, home, journal.runId);
|
|
805
|
+
} catch (error) {
|
|
806
|
+
reports.push({
|
|
807
|
+
runId: journal.runId,
|
|
808
|
+
phase: journal.phase,
|
|
809
|
+
action: "snapshot-read-failed: " + (error instanceof Error ? error.message : String(error)),
|
|
810
|
+
ok: false
|
|
811
|
+
});
|
|
812
|
+
continue;
|
|
813
|
+
}
|
|
803
814
|
let action = "report-only";
|
|
804
815
|
let ok = true;
|
|
805
816
|
if (recovery === "retry-commit") action = "no-host-write";
|
|
@@ -1531,7 +1542,7 @@ function mergeUsage() {
|
|
|
1531
1542
|
}
|
|
1532
1543
|
//#endregion
|
|
1533
1544
|
//#region lib/types/read-commands.js
|
|
1534
|
-
async function handleReadOnlyMergeCommand(command, io, home) {
|
|
1545
|
+
async function handleReadOnlyMergeCommand(command, io, home, cards) {
|
|
1535
1546
|
const withNote = (text, note) => note === void 0 ? text : "note: " + note + "\n" + text;
|
|
1536
1547
|
if (command.verb === "status") {
|
|
1537
1548
|
const loaded = await loadMergeState(io, home);
|
|
@@ -1544,7 +1555,8 @@ async function handleReadOnlyMergeCommand(command, io, home) {
|
|
|
1544
1555
|
if (command.verb === "pending") {
|
|
1545
1556
|
const loaded = await loadMergeState(io, home);
|
|
1546
1557
|
const rows = command.expired === true ? loaded.state.rows.filter((row) => row.expired !== void 0) : loaded.state.rows;
|
|
1547
|
-
|
|
1558
|
+
const live = cards === void 0 ? rows : rows.filter((row) => cards.some((card) => card.name === row.a) && cards.some((card) => card.name === row.b));
|
|
1559
|
+
return withNote(command.expired === true ? formatStatus(live) : formatQueue(live, "pending"), loaded.note);
|
|
1548
1560
|
}
|
|
1549
1561
|
if (command.verb === "candidates") {
|
|
1550
1562
|
const loaded = await loadMergeState(io, home);
|
|
@@ -2215,6 +2227,56 @@ async function rejectPending(io, home, now, id) {
|
|
|
2215
2227
|
return await act(io, home, now, id, "reject");
|
|
2216
2228
|
}
|
|
2217
2229
|
//#endregion
|
|
2230
|
+
//#region lib/types/domain/claim.js
|
|
2231
|
+
function acquireClaim(current, now, leaseMs, token) {
|
|
2232
|
+
if (current !== void 0 && current.expiresAt > now) return void 0;
|
|
2233
|
+
return {
|
|
2234
|
+
id: "claim",
|
|
2235
|
+
token,
|
|
2236
|
+
expiresAt: now + Math.max(1, leaseMs)
|
|
2237
|
+
};
|
|
2238
|
+
}
|
|
2239
|
+
function releaseClaim(current, token) {
|
|
2240
|
+
return current?.token === token ? void 0 : current;
|
|
2241
|
+
}
|
|
2242
|
+
//#endregion
|
|
2243
|
+
//#region lib/types/target-claim.js
|
|
2244
|
+
/** Durable target claim: one approve writer per merge target at a time. */
|
|
2245
|
+
const LEASE_MS = 5 * 6e4;
|
|
2246
|
+
function claimPath(home, target) {
|
|
2247
|
+
return join(home, "merge", "claims", contentHash(target) + ".json");
|
|
2248
|
+
}
|
|
2249
|
+
async function readClaim(io, home, target) {
|
|
2250
|
+
const path = claimPath(home, target);
|
|
2251
|
+
if (!await io.exists(path)) return void 0;
|
|
2252
|
+
const raw = await io.readText(path);
|
|
2253
|
+
if (raw === null) return void 0;
|
|
2254
|
+
const parsed = JSON.parse(raw);
|
|
2255
|
+
if (typeof parsed.token !== "string" || typeof parsed.expiresAt !== "number") throw new Error("corrupt target claim: " + target);
|
|
2256
|
+
return {
|
|
2257
|
+
id: "claim",
|
|
2258
|
+
token: parsed.token,
|
|
2259
|
+
expiresAt: parsed.expiresAt
|
|
2260
|
+
};
|
|
2261
|
+
}
|
|
2262
|
+
async function acquireTargetClaim(io, home, target, now) {
|
|
2263
|
+
const current = await readClaim(io, home, target);
|
|
2264
|
+
const token = randomUUID();
|
|
2265
|
+
const claim = acquireClaim(current, now, LEASE_MS, token);
|
|
2266
|
+
if (claim === void 0) return null;
|
|
2267
|
+
await io.writeText(claimPath(home, target), JSON.stringify(claim) + "\n");
|
|
2268
|
+
return {
|
|
2269
|
+
token,
|
|
2270
|
+
async release() {
|
|
2271
|
+
const next = releaseClaim(await readClaim(io, home, target), token);
|
|
2272
|
+
if (next === void 0) try {
|
|
2273
|
+
await io.remove(claimPath(home, target));
|
|
2274
|
+
} catch {}
|
|
2275
|
+
else await io.writeText(claimPath(home, target), JSON.stringify(next) + "\n");
|
|
2276
|
+
}
|
|
2277
|
+
};
|
|
2278
|
+
}
|
|
2279
|
+
//#endregion
|
|
2218
2280
|
//#region lib/types/round2-outcome.js
|
|
2219
2281
|
async function recordRound2Outcome(io, home, now, row, outcome) {
|
|
2220
2282
|
const at = new Date(now()).toISOString();
|
|
@@ -2288,79 +2350,89 @@ async function recordRound2Outcome(io, home, now, row, outcome) {
|
|
|
2288
2350
|
//#endregion
|
|
2289
2351
|
//#region lib/types/approve-command.js
|
|
2290
2352
|
async function runApproveCommand(options) {
|
|
2291
|
-
const
|
|
2292
|
-
|
|
2293
|
-
|
|
2294
|
-
message: taken.message
|
|
2295
|
-
};
|
|
2296
|
-
const row = taken.row;
|
|
2297
|
-
const decision = row.decision;
|
|
2298
|
-
if (decision === void 0) return {
|
|
2353
|
+
const target = (await loadMergeState(options.io, options.home)).state.rows.find((item) => item.id === options.rowId)?.decision?.target;
|
|
2354
|
+
const claim = target === void 0 ? null : await acquireTargetClaim(options.io, options.home, target, options.now());
|
|
2355
|
+
if (target !== void 0 && claim === null) return {
|
|
2299
2356
|
ok: false,
|
|
2300
|
-
message: "
|
|
2357
|
+
message: "target already claimed by another approve: " + target
|
|
2301
2358
|
};
|
|
2302
|
-
|
|
2303
|
-
|
|
2304
|
-
|
|
2305
|
-
|
|
2306
|
-
|
|
2307
|
-
|
|
2308
|
-
|
|
2309
|
-
|
|
2310
|
-
|
|
2311
|
-
|
|
2312
|
-
|
|
2313
|
-
|
|
2314
|
-
|
|
2315
|
-
|
|
2316
|
-
|
|
2317
|
-
|
|
2318
|
-
|
|
2319
|
-
|
|
2320
|
-
|
|
2321
|
-
|
|
2322
|
-
|
|
2323
|
-
|
|
2324
|
-
|
|
2325
|
-
|
|
2326
|
-
|
|
2327
|
-
|
|
2328
|
-
|
|
2329
|
-
|
|
2330
|
-
|
|
2359
|
+
try {
|
|
2360
|
+
const taken = await approvePending(options.io, options.home, options.now, options.rowId);
|
|
2361
|
+
if (!taken.ok) return {
|
|
2362
|
+
ok: false,
|
|
2363
|
+
message: taken.message
|
|
2364
|
+
};
|
|
2365
|
+
const row = taken.row;
|
|
2366
|
+
const decision = row.decision;
|
|
2367
|
+
if (decision === void 0) return {
|
|
2368
|
+
ok: false,
|
|
2369
|
+
message: "pending row has no decision"
|
|
2370
|
+
};
|
|
2371
|
+
return await withMergeRun({
|
|
2372
|
+
io: options.io,
|
|
2373
|
+
home: options.home,
|
|
2374
|
+
registry: options.registry,
|
|
2375
|
+
command: "approve",
|
|
2376
|
+
approval: {
|
|
2377
|
+
platformWrite: options.platformWrite,
|
|
2378
|
+
mergeApproval: true
|
|
2379
|
+
},
|
|
2380
|
+
now: options.now,
|
|
2381
|
+
async body(ctx) {
|
|
2382
|
+
const stage = (name) => ctx.stage(name, {
|
|
2383
|
+
done: 0,
|
|
2384
|
+
total: 1
|
|
2385
|
+
});
|
|
2386
|
+
await stage("author");
|
|
2387
|
+
let outcome;
|
|
2388
|
+
try {
|
|
2389
|
+
outcome = await options.ports.round2(row, decision, ctx.signal, ctx.runId);
|
|
2390
|
+
} catch (error) {
|
|
2391
|
+
outcome = {
|
|
2392
|
+
kind: "failed",
|
|
2393
|
+
failure: {
|
|
2394
|
+
stage: "author",
|
|
2395
|
+
code: "merge-failed",
|
|
2396
|
+
message: error instanceof Error ? error.message : String(error),
|
|
2397
|
+
retryable: true,
|
|
2398
|
+
retryFrom: "agent2"
|
|
2399
|
+
}
|
|
2400
|
+
};
|
|
2401
|
+
}
|
|
2402
|
+
const base = {
|
|
2403
|
+
scope: {
|
|
2404
|
+
candidatePairs: 1,
|
|
2405
|
+
queueBefore: {
|
|
2406
|
+
verify: 0,
|
|
2407
|
+
pending: 0,
|
|
2408
|
+
fuse: 1,
|
|
2409
|
+
skipped: 0
|
|
2410
|
+
}
|
|
2411
|
+
},
|
|
2412
|
+
notes: []
|
|
2331
2413
|
};
|
|
2332
|
-
|
|
2333
|
-
|
|
2334
|
-
|
|
2335
|
-
|
|
2336
|
-
|
|
2337
|
-
|
|
2414
|
+
const record = await recordRound2Outcome(options.io, options.home, options.now, row, outcome);
|
|
2415
|
+
return {
|
|
2416
|
+
...base,
|
|
2417
|
+
stopReason: record.merged ? "completed" : "failed",
|
|
2418
|
+
pairs: [record.pair],
|
|
2419
|
+
totals: record.merged ? {
|
|
2420
|
+
merged: 1,
|
|
2421
|
+
kept: 0,
|
|
2338
2422
|
pending: 0,
|
|
2339
|
-
|
|
2340
|
-
|
|
2423
|
+
failed: 0
|
|
2424
|
+
} : {
|
|
2425
|
+
merged: 0,
|
|
2426
|
+
kept: 0,
|
|
2427
|
+
pending: 0,
|
|
2428
|
+
failed: 1
|
|
2341
2429
|
}
|
|
2342
|
-
}
|
|
2343
|
-
|
|
2344
|
-
|
|
2345
|
-
|
|
2346
|
-
|
|
2347
|
-
|
|
2348
|
-
stopReason: record.merged ? "completed" : "failed",
|
|
2349
|
-
pairs: [record.pair],
|
|
2350
|
-
totals: record.merged ? {
|
|
2351
|
-
merged: 1,
|
|
2352
|
-
kept: 0,
|
|
2353
|
-
pending: 0,
|
|
2354
|
-
failed: 0
|
|
2355
|
-
} : {
|
|
2356
|
-
merged: 0,
|
|
2357
|
-
kept: 0,
|
|
2358
|
-
pending: 0,
|
|
2359
|
-
failed: 1
|
|
2360
|
-
}
|
|
2361
|
-
};
|
|
2362
|
-
}
|
|
2363
|
-
});
|
|
2430
|
+
};
|
|
2431
|
+
}
|
|
2432
|
+
});
|
|
2433
|
+
} finally {
|
|
2434
|
+
await claim?.release();
|
|
2435
|
+
}
|
|
2364
2436
|
}
|
|
2365
2437
|
//#endregion
|
|
2366
2438
|
//#region lib/types/resume-command.js
|
|
@@ -3224,6 +3296,9 @@ async function commitStagedRun(io, home, runId, port, options) {
|
|
|
3224
3296
|
};
|
|
3225
3297
|
try {
|
|
3226
3298
|
await writeCommitReceipt(io, home, receipt);
|
|
3299
|
+
try {
|
|
3300
|
+
await io.remove(snapshotPath(home, runId));
|
|
3301
|
+
} catch {}
|
|
3227
3302
|
return {
|
|
3228
3303
|
ok: true,
|
|
3229
3304
|
plan: assembled.plan,
|
|
@@ -3705,6 +3780,10 @@ function createMergeRuntime(deps) {
|
|
|
3705
3780
|
ok: false,
|
|
3706
3781
|
text: "merge pair 不接受受保护/保护状态未知技能: " + command.a + " / " + command.b
|
|
3707
3782
|
};
|
|
3783
|
+
if (deps.isBlocked?.(command.a) === true || deps.isBlocked?.(command.b) === true) return {
|
|
3784
|
+
ok: false,
|
|
3785
|
+
text: "merge pair 不接受被范围门拦下的技能: " + command.a + " / " + command.b
|
|
3786
|
+
};
|
|
3708
3787
|
const pair = [{
|
|
3709
3788
|
a: command.a,
|
|
3710
3789
|
b: command.b
|
|
@@ -3771,7 +3850,7 @@ function createMergeRuntime(deps) {
|
|
|
3771
3850
|
if (command.verb === "scan") {
|
|
3772
3851
|
const signal = new AbortController().signal;
|
|
3773
3852
|
const cards = await deps.cards(signal);
|
|
3774
|
-
const managed = new Set(cards.filter((card) => card.managed === true && card.protectedBy == null).map((card) => card.name));
|
|
3853
|
+
const managed = new Set(cards.filter((card) => card.managed === true && card.protectedBy == null && deps.isBlocked?.(card.name) !== true).map((card) => card.name));
|
|
3775
3854
|
const loadedState = await loadMergeState(deps.io, deps.home);
|
|
3776
3855
|
const skipped = skippedPairKeys(loadedState.state.rows, deps.params.mergeSkippedTtlDays, deps.now());
|
|
3777
3856
|
const held = new Set(loadedState.state.rows.filter((row) => row.queue === "pending" || row.queue === "fuse" || row.queue === "verify" && !canRetry(row)).map((row) => row.pairKey));
|
|
@@ -4114,7 +4193,8 @@ function createMergeRuntime(deps) {
|
|
|
4114
4193
|
text: "merge approve 完成:merged " + String(result.report.totals.merged)
|
|
4115
4194
|
};
|
|
4116
4195
|
}
|
|
4117
|
-
const
|
|
4196
|
+
const cards = command.verb === "pending" ? await deps.cards(new AbortController().signal) : void 0;
|
|
4197
|
+
const text = await handleReadOnlyMergeCommand(command, deps.io, deps.home, cards);
|
|
4118
4198
|
if (text !== null) return {
|
|
4119
4199
|
ok: true,
|
|
4120
4200
|
text
|
|
@@ -4708,6 +4788,9 @@ function apply(ctx, rawConfig = {}) {
|
|
|
4708
4788
|
parent: void 0,
|
|
4709
4789
|
allow: ["merge_stage"],
|
|
4710
4790
|
timeoutMinutes: params.mergeRunTimeoutMinutes,
|
|
4791
|
+
isBlocked: (name) => {
|
|
4792
|
+
return ctx.get("evolutionCurator")?.gates?.isBlocked?.(name) === true;
|
|
4793
|
+
},
|
|
4711
4794
|
async cards(signal) {
|
|
4712
4795
|
const listed = await skills.list({ withContent: true });
|
|
4713
4796
|
if (signal.aborted) return [];
|
|
@@ -1,42 +1,55 @@
|
|
|
1
1
|
import { approvePending } from "./pending-actions.js";
|
|
2
|
+
import { loadMergeState } from "./state.js";
|
|
3
|
+
import { acquireTargetClaim } from "./target-claim.js";
|
|
2
4
|
import { withMergeRun } from "./run-shell.js";
|
|
3
5
|
import { recordRound2Outcome } from "./round2-outcome.js";
|
|
4
6
|
export async function runApproveCommand(options) {
|
|
5
|
-
const
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
const
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
7
|
+
const loaded = await loadMergeState(options.io, options.home);
|
|
8
|
+
const before = loaded.state.rows.find(item => item.id === options.rowId);
|
|
9
|
+
const target = before?.decision?.target;
|
|
10
|
+
const claim = target === undefined ? null : await acquireTargetClaim(options.io, options.home, target, options.now());
|
|
11
|
+
if (target !== undefined && claim === null)
|
|
12
|
+
return { ok: false, message: 'target already claimed by another approve: ' + target };
|
|
13
|
+
try {
|
|
14
|
+
const taken = await approvePending(options.io, options.home, options.now, options.rowId);
|
|
15
|
+
if (!taken.ok)
|
|
16
|
+
return { ok: false, message: taken.message };
|
|
17
|
+
const row = taken.row;
|
|
18
|
+
const decision = row.decision;
|
|
19
|
+
if (decision === undefined)
|
|
20
|
+
return { ok: false, message: 'pending row has no decision' };
|
|
21
|
+
return await withMergeRun({
|
|
22
|
+
io: options.io,
|
|
23
|
+
home: options.home,
|
|
24
|
+
registry: options.registry,
|
|
25
|
+
command: 'approve',
|
|
26
|
+
approval: { platformWrite: options.platformWrite, mergeApproval: true },
|
|
27
|
+
now: options.now,
|
|
28
|
+
async body(ctx) {
|
|
29
|
+
const stage = (name) => ctx.stage(name, { done: 0, total: 1 });
|
|
30
|
+
await stage('author');
|
|
31
|
+
let outcome;
|
|
32
|
+
try {
|
|
33
|
+
outcome = await options.ports.round2(row, decision, ctx.signal, ctx.runId);
|
|
34
|
+
}
|
|
35
|
+
catch (error) {
|
|
36
|
+
outcome = { kind: 'failed', failure: { stage: 'author', code: 'merge-failed', message: error instanceof Error ? error.message : String(error), retryable: true, retryFrom: 'agent2' } };
|
|
37
|
+
}
|
|
38
|
+
const base = { scope: { candidatePairs: 1, queueBefore: { verify: 0, pending: 0, fuse: 1, skipped: 0 } }, notes: [] };
|
|
39
|
+
const record = await recordRound2Outcome(options.io, options.home, options.now, row, outcome);
|
|
40
|
+
return {
|
|
41
|
+
...base,
|
|
42
|
+
stopReason: record.merged ? 'completed' : 'failed',
|
|
43
|
+
pairs: [record.pair],
|
|
44
|
+
totals: record.merged
|
|
45
|
+
? { merged: 1, kept: 0, pending: 0, failed: 0 }
|
|
46
|
+
: { merged: 0, kept: 0, pending: 0, failed: 1 },
|
|
47
|
+
};
|
|
48
|
+
},
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
finally {
|
|
52
|
+
await claim?.release();
|
|
53
|
+
}
|
|
41
54
|
}
|
|
42
55
|
//# sourceMappingURL=approve-command.js.map
|
|
@@ -2,7 +2,7 @@ import { assembleStagedCommit } from "./commit-assembly.js";
|
|
|
2
2
|
import { writeCommitReceipt } from "./commit-receipt.js";
|
|
3
3
|
import { pairKey } from "./contracts.js";
|
|
4
4
|
import { writeJournal } from "./domain/commit-journal-store.js";
|
|
5
|
-
import { writeCommitSnapshot } from "./commit-snapshot.js";
|
|
5
|
+
import { snapshotPath, writeCommitSnapshot } from "./commit-snapshot.js";
|
|
6
6
|
export async function commitStagedRun(io, home, runId, port, options) {
|
|
7
7
|
const assembled = await assembleStagedCommit(io, home, runId, port, { origin: options.origin, summary: options.summary });
|
|
8
8
|
if (!assembled.ok)
|
|
@@ -46,6 +46,10 @@ export async function commitStagedRun(io, home, runId, port, options) {
|
|
|
46
46
|
};
|
|
47
47
|
try {
|
|
48
48
|
await writeCommitReceipt(io, home, receipt);
|
|
49
|
+
try {
|
|
50
|
+
await io.remove(snapshotPath(home, runId));
|
|
51
|
+
}
|
|
52
|
+
catch { /* committed journal is the record */ }
|
|
49
53
|
return { ok: true, plan: assembled.plan, result, receipt };
|
|
50
54
|
}
|
|
51
55
|
catch (error) {
|
|
@@ -42,7 +42,14 @@ export async function recoverCommitJournals(io, home, now, skillRoot, restoreSou
|
|
|
42
42
|
reports.push({ runId: journal.runId, phase: journal.phase, action: 'receipt-committed', ok: true });
|
|
43
43
|
continue;
|
|
44
44
|
}
|
|
45
|
-
|
|
45
|
+
let snapshot;
|
|
46
|
+
try {
|
|
47
|
+
snapshot = await readCommitSnapshot(io, home, journal.runId);
|
|
48
|
+
}
|
|
49
|
+
catch (error) {
|
|
50
|
+
reports.push({ runId: journal.runId, phase: journal.phase, action: 'snapshot-read-failed: ' + (error instanceof Error ? error.message : String(error)), ok: false });
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
46
53
|
let action = 'report-only';
|
|
47
54
|
let ok = true;
|
|
48
55
|
if (recovery === 'retry-commit') {
|
package/lib/types/index.js
CHANGED
|
@@ -218,6 +218,10 @@ export function apply(ctx, rawConfig = {}) {
|
|
|
218
218
|
const runtime = createMergeRuntime({
|
|
219
219
|
io, home, registry: runs, skills, now: () => Date.now(), params, platformWrite: true,
|
|
220
220
|
channel, parent: undefined, allow: ['merge_stage'], timeoutMinutes: params.mergeRunTimeoutMinutes,
|
|
221
|
+
isBlocked: (name) => {
|
|
222
|
+
const curator = ctx.get('evolutionCurator');
|
|
223
|
+
return curator?.gates?.isBlocked?.(name) === true;
|
|
224
|
+
},
|
|
221
225
|
async cards(signal) {
|
|
222
226
|
const listed = await skills.list({ withContent: true });
|
|
223
227
|
if (signal.aborted)
|
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import type { EvolutionIoLike } from '@lmzhen/dsh-evolution-core';
|
|
6
6
|
import type { MergeCliCommand } from './commands.ts';
|
|
7
|
-
export declare function handleReadOnlyMergeCommand(command: MergeCliCommand, io: EvolutionIoLike, home: string
|
|
7
|
+
export declare function handleReadOnlyMergeCommand(command: MergeCliCommand, io: EvolutionIoLike, home: string, cards?: readonly {
|
|
8
|
+
readonly name: string;
|
|
9
|
+
}[]): Promise<string | null>;
|
|
8
10
|
export declare function mergeCommandHelp(): string;
|
|
9
11
|
//# sourceMappingURL=read-commands.d.ts.map
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { loadMergeState } from "./state.js";
|
|
2
2
|
import { formatQueue, formatStatus } from "./status.js";
|
|
3
3
|
import { mergeUsage } from "./commands.js";
|
|
4
|
-
export async function handleReadOnlyMergeCommand(command, io, home) {
|
|
4
|
+
export async function handleReadOnlyMergeCommand(command, io, home, cards) {
|
|
5
5
|
const withNote = (text, note) => note === undefined ? text : 'note: ' + note + '\n' + text;
|
|
6
6
|
if (command.verb === 'status') {
|
|
7
7
|
const loaded = await loadMergeState(io, home);
|
|
@@ -14,7 +14,8 @@ export async function handleReadOnlyMergeCommand(command, io, home) {
|
|
|
14
14
|
if (command.verb === 'pending') {
|
|
15
15
|
const loaded = await loadMergeState(io, home);
|
|
16
16
|
const rows = command.expired === true ? loaded.state.rows.filter(row => row.expired !== undefined) : loaded.state.rows;
|
|
17
|
-
|
|
17
|
+
const live = cards === undefined ? rows : rows.filter(row => cards.some(card => card.name === row.a) && cards.some(card => card.name === row.b));
|
|
18
|
+
return withNote(command.expired === true ? formatStatus(live) : formatQueue(live, 'pending'), loaded.note);
|
|
18
19
|
}
|
|
19
20
|
if (command.verb === 'candidates') {
|
|
20
21
|
const loaded = await loadMergeState(io, home);
|
package/lib/types/runtime.d.ts
CHANGED
|
@@ -31,6 +31,8 @@ export interface MergeRuntimeDeps {
|
|
|
31
31
|
/** Start scan/pair in the background and return the run id immediately. */
|
|
32
32
|
readonly detached?: boolean;
|
|
33
33
|
readonly cards: (signal: AbortSignal) => Promise<readonly SkillCard[]>;
|
|
34
|
+
/** Curator gate set: excluded/referenced/suppressed/protected builtins. */
|
|
35
|
+
readonly isBlocked?: (name: string) => boolean;
|
|
34
36
|
/** Run-scoped engine screening. */
|
|
35
37
|
readonly screeningForRun: (cards: readonly SkillCard[], runId: string, signal: AbortSignal) => Promise<readonly MergeCandidate[]>;
|
|
36
38
|
readonly stage?: MergeStageBindingPort;
|
package/lib/types/runtime.js
CHANGED
|
@@ -59,6 +59,9 @@ export function createMergeRuntime(deps) {
|
|
|
59
59
|
|| cardA.protectionUnknown === true || cardB.protectionUnknown === true) {
|
|
60
60
|
return { ok: false, text: 'merge pair 不接受受保护/保护状态未知技能: ' + command.a + ' / ' + command.b };
|
|
61
61
|
}
|
|
62
|
+
if (deps.isBlocked?.(command.a) === true || deps.isBlocked?.(command.b) === true) {
|
|
63
|
+
return { ok: false, text: 'merge pair 不接受被范围门拦下的技能: ' + command.a + ' / ' + command.b };
|
|
64
|
+
}
|
|
62
65
|
const pair = [{ a: command.a, b: command.b }];
|
|
63
66
|
const existing = (await loadMergeState(deps.io, deps.home)).state.rows.find(row => row.pairKey === pairKey(command.a, command.b));
|
|
64
67
|
if (existing?.state === 'skipped') {
|
|
@@ -101,7 +104,9 @@ export function createMergeRuntime(deps) {
|
|
|
101
104
|
if (command.verb === 'scan') {
|
|
102
105
|
const signal = new AbortController().signal;
|
|
103
106
|
const cards = await deps.cards(signal);
|
|
104
|
-
const managed = new Set(cards
|
|
107
|
+
const managed = new Set(cards
|
|
108
|
+
.filter(card => card.managed === true && card.protectedBy == null && deps.isBlocked?.(card.name) !== true)
|
|
109
|
+
.map(card => card.name));
|
|
105
110
|
const loadedState = await loadMergeState(deps.io, deps.home);
|
|
106
111
|
const skipped = skippedPairKeys(loadedState.state.rows, deps.params.mergeSkippedTtlDays, deps.now());
|
|
107
112
|
const held = new Set(loadedState.state.rows.filter(row => row.queue === 'pending' || row.queue === 'fuse' || (row.queue === 'verify' && !canRetry(row))).map(row => row.pairKey));
|
|
@@ -314,7 +319,8 @@ export function createMergeRuntime(deps) {
|
|
|
314
319
|
return { ok: false, text: 'merge approve started without a report' };
|
|
315
320
|
return { ok: true, text: 'merge approve 完成:merged ' + String(result.report.totals.merged) };
|
|
316
321
|
}
|
|
317
|
-
const
|
|
322
|
+
const cards = command.verb === 'pending' ? await deps.cards(new AbortController().signal) : undefined;
|
|
323
|
+
const text = await handleReadOnlyMergeCommand(command, deps.io, deps.home, cards);
|
|
318
324
|
if (text !== null)
|
|
319
325
|
return { ok: true, text };
|
|
320
326
|
return readOnly.execute(command);
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { type EvolutionIoLike } from '@lmzhen/dsh-evolution-core';
|
|
2
|
+
export interface TargetClaim {
|
|
3
|
+
readonly token: string;
|
|
4
|
+
release(): Promise<void>;
|
|
5
|
+
}
|
|
6
|
+
export declare function acquireTargetClaim(io: EvolutionIoLike, home: string, target: string, now: number): Promise<TargetClaim | null>;
|
|
7
|
+
//# sourceMappingURL=target-claim.d.ts.map
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/** Durable target claim: one approve writer per merge target at a time. */
|
|
2
|
+
import { randomUUID } from 'node:crypto';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { contentHash } from '@lmzhen/dsh-evolution-core';
|
|
5
|
+
import { acquireClaim, releaseClaim } from "./domain/claim.js";
|
|
6
|
+
const LEASE_MS = 5 * 60_000;
|
|
7
|
+
function claimPath(home, target) {
|
|
8
|
+
return join(home, 'merge', 'claims', contentHash(target) + '.json');
|
|
9
|
+
}
|
|
10
|
+
async function readClaim(io, home, target) {
|
|
11
|
+
const path = claimPath(home, target);
|
|
12
|
+
if (!(await io.exists(path)))
|
|
13
|
+
return undefined;
|
|
14
|
+
const raw = await io.readText(path);
|
|
15
|
+
if (raw === null)
|
|
16
|
+
return undefined;
|
|
17
|
+
const parsed = JSON.parse(raw);
|
|
18
|
+
if (typeof parsed.token !== 'string' || typeof parsed.expiresAt !== 'number')
|
|
19
|
+
throw new Error('corrupt target claim: ' + target);
|
|
20
|
+
return { id: 'claim', token: parsed.token, expiresAt: parsed.expiresAt };
|
|
21
|
+
}
|
|
22
|
+
export async function acquireTargetClaim(io, home, target, now) {
|
|
23
|
+
const current = await readClaim(io, home, target);
|
|
24
|
+
const token = randomUUID();
|
|
25
|
+
const claim = acquireClaim(current, now, LEASE_MS, token);
|
|
26
|
+
if (claim === undefined)
|
|
27
|
+
return null;
|
|
28
|
+
await io.writeText(claimPath(home, target), JSON.stringify(claim) + '\n');
|
|
29
|
+
return {
|
|
30
|
+
token,
|
|
31
|
+
async release() {
|
|
32
|
+
const latest = await readClaim(io, home, target);
|
|
33
|
+
const next = releaseClaim(latest, token);
|
|
34
|
+
if (next === undefined) {
|
|
35
|
+
try {
|
|
36
|
+
await io.remove(claimPath(home, target));
|
|
37
|
+
}
|
|
38
|
+
catch { /* already gone */ }
|
|
39
|
+
}
|
|
40
|
+
else {
|
|
41
|
+
await io.writeText(claimPath(home, target), JSON.stringify(next) + '\n');
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
//# sourceMappingURL=target-claim.js.map
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lmzhen/dsh-evolution-merge",
|
|
3
3
|
"description": "Skill auto-merge domain: screening, two-round authoring, queues and commit orchestration (community build)",
|
|
4
|
-
"version": "0.30.
|
|
4
|
+
"version": "0.30.11",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
7
7
|
},
|
|
@@ -27,8 +27,8 @@
|
|
|
27
27
|
"license": "MIT",
|
|
28
28
|
"dependencies": {
|
|
29
29
|
"@deepseek-ai/schemastery": "^3.18.5",
|
|
30
|
-
"@lmzhen/dsh-evolution-core": "^0.30.
|
|
31
|
-
"@lmzhen/dsh-evolution-merge-engine": "^0.30.
|
|
30
|
+
"@lmzhen/dsh-evolution-core": "^0.30.11",
|
|
31
|
+
"@lmzhen/dsh-evolution-merge-engine": "^0.30.11"
|
|
32
32
|
},
|
|
33
33
|
"peerDependencies": {
|
|
34
34
|
"@deepseek-ai/cordis": "^4.0.5"
|