@lmzhen/dsh-evolution-merge 0.30.17 → 0.30.19
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 +25 -7
- package/data/minhash_params.json +1063 -0
- package/lib/index.js +7308 -1800
- package/lib/types/adapters/channel.d.ts +1 -0
- package/lib/types/adapters/channel.js +15 -1
- package/lib/types/adapters/engine-screen.d.ts +1 -1
- package/lib/types/adapters/engine-screen.js +5 -2
- package/lib/types/adapters/library.js +4 -1
- package/lib/types/adapters/model-runner.d.ts +0 -5
- package/lib/types/adapters/model-runner.js +8 -2
- package/lib/types/adapters/model.d.ts +31 -0
- package/lib/types/adapters/model.js +47 -0
- package/lib/types/adapters/select.js +12 -8
- package/lib/types/approve-command.d.ts +14 -1
- package/lib/types/approve-command.js +72 -44
- package/lib/types/authoring-flow.d.ts +19 -1
- package/lib/types/authoring-flow.js +49 -15
- package/lib/types/channel-port.d.ts +3 -1
- package/lib/types/channel-port.js +8 -3
- package/lib/types/command-defs.d.ts +5 -0
- package/lib/types/command-defs.js +33 -0
- package/lib/types/command-surface.d.ts +5 -0
- package/lib/types/command-surface.js +11 -0
- package/lib/types/commands.d.ts +3 -4
- package/lib/types/commands.js +33 -72
- package/lib/types/commit-assembly.d.ts +16 -1
- package/lib/types/commit-assembly.js +72 -11
- package/lib/types/commit-execute.d.ts +14 -2
- package/lib/types/commit-execute.js +76 -14
- package/lib/types/commit-plan.d.ts +7 -1
- package/lib/types/commit-plan.js +4 -0
- package/lib/types/commit-receipt.d.ts +3 -0
- package/lib/types/commit-recovery.d.ts +7 -2
- package/lib/types/commit-recovery.js +73 -96
- package/lib/types/contracts.d.ts +44 -1
- package/lib/types/contracts.js +1 -1
- package/lib/types/coordinator.d.ts +3 -2
- package/lib/types/coordinator.js +4 -3
- package/lib/types/decision.js +1 -1
- package/lib/types/doctor.d.ts +18 -1
- package/lib/types/doctor.js +164 -10
- package/lib/types/domain/authoring-manifest.d.ts +26 -0
- package/lib/types/domain/authoring-manifest.js +43 -0
- package/lib/types/domain/claim.d.ts +4 -1
- package/lib/types/domain/claim.js +2 -2
- package/lib/types/domain/commit-journal-store.js +5 -1
- package/lib/types/domain/commit-journal.js +1 -1
- package/lib/types/domain/consistency.d.ts +22 -0
- package/lib/types/domain/consistency.js +50 -0
- package/lib/types/domain/engine-criteria.d.ts +10 -0
- package/lib/types/domain/engine-criteria.js +11 -0
- package/lib/types/domain/engine-input.d.ts +10 -0
- package/lib/types/domain/engine-input.js +26 -0
- package/lib/types/domain/errors.d.ts +15 -1
- package/lib/types/domain/errors.js +23 -4
- package/lib/types/domain/evidence-hash.d.ts +3 -0
- package/lib/types/domain/evidence-hash.js +14 -0
- package/lib/types/domain/failure-route.d.ts +14 -0
- package/lib/types/domain/failure-route.js +12 -0
- package/lib/types/domain/ledger.js +10 -1
- package/lib/types/domain/live-facts.d.ts +31 -0
- package/lib/types/domain/live-facts.js +54 -0
- package/lib/types/domain/merge-limits.d.ts +18 -0
- package/lib/types/domain/merge-limits.js +22 -0
- package/lib/types/domain/merge-report.d.ts +41 -0
- package/lib/types/domain/merge-report.js +2 -0
- package/lib/types/domain/merge-snapshot.d.ts +99 -0
- package/lib/types/domain/merge-snapshot.js +82 -0
- package/lib/types/domain/migrate-v3.d.ts +20 -0
- package/lib/types/domain/migrate-v3.js +68 -0
- package/lib/types/domain/pair-ledger.d.ts +34 -0
- package/lib/types/domain/pair-ledger.js +96 -0
- package/lib/types/domain/pre-1b-backup.d.ts +43 -0
- package/lib/types/domain/pre-1b-backup.js +140 -0
- package/lib/types/domain/provider-key.d.ts +2 -0
- package/lib/types/domain/provider-key.js +11 -0
- package/lib/types/domain/read-probe.d.ts +5 -0
- package/lib/types/domain/read-probe.js +11 -0
- package/lib/types/domain/reconcile.d.ts +47 -0
- package/lib/types/domain/reconcile.js +178 -0
- package/lib/types/domain/resolve-pair.d.ts +2 -9
- package/lib/types/domain/resolve-pair.js +2 -15
- package/lib/types/domain/row-tx.d.ts +27 -0
- package/lib/types/domain/row-tx.js +31 -0
- package/lib/types/domain/row-v3-bridge.d.ts +6 -0
- package/lib/types/domain/row-v3-bridge.js +47 -0
- package/lib/types/domain/row-v3.d.ts +113 -0
- package/lib/types/domain/row-v3.js +107 -0
- package/lib/types/domain/row-v4.d.ts +148 -0
- package/lib/types/domain/row-v4.js +159 -0
- package/lib/types/domain/row.d.ts +1 -0
- package/lib/types/domain/run-coordinator.d.ts +51 -0
- package/lib/types/domain/run-coordinator.js +45 -0
- package/lib/types/domain/schema4.d.ts +53 -0
- package/lib/types/domain/schema4.js +129 -0
- package/lib/types/domain/selection-policy.d.ts +14 -0
- package/lib/types/domain/selection-policy.js +6 -0
- package/lib/types/domain/skipped-expiry.d.ts +5 -0
- package/lib/types/domain/skipped-expiry.js +16 -0
- package/lib/types/domain/stage-aggregate.d.ts +96 -0
- package/lib/types/domain/stage-aggregate.js +293 -0
- package/lib/types/domain/staging-index.d.ts +15 -0
- package/lib/types/domain/staging-index.js +41 -0
- package/lib/types/domain/state-bootstrap.d.ts +43 -0
- package/lib/types/domain/state-bootstrap.js +267 -0
- package/lib/types/domain/state-v3.d.ts +4 -0
- package/lib/types/domain/state-v3.js +4 -0
- package/lib/types/domain/undo-journal.d.ts +19 -0
- package/lib/types/domain/undo-journal.js +20 -0
- package/lib/types/domain/undo-receipt.d.ts +18 -0
- package/lib/types/domain/undo-receipt.js +22 -0
- package/lib/types/domain/validation-plan.d.ts +65 -0
- package/lib/types/domain/validation-plan.js +49 -0
- package/lib/types/facts.d.ts +26 -0
- package/lib/types/facts.js +65 -0
- package/lib/types/gate-filter.d.ts +31 -0
- package/lib/types/gate-filter.js +64 -0
- package/lib/types/index.d.ts +7 -2
- package/lib/types/index.js +185 -82
- package/lib/types/ladder.js +1 -1
- package/lib/types/merge-command.d.ts +3 -1
- package/lib/types/merge-command.js +10 -6
- package/lib/types/merge-routes.d.ts +12 -7
- package/lib/types/merge-routes.js +87 -23
- package/lib/types/merge-tool.d.ts +1 -1
- package/lib/types/merge-tool.js +2 -2
- package/lib/types/metrics.d.ts +16 -0
- package/lib/types/metrics.js +27 -0
- package/lib/types/new-child-authoring.d.ts +5 -1
- package/lib/types/new-child-authoring.js +71 -13
- package/lib/types/params.d.ts +2 -4
- package/lib/types/params.js +13 -0
- package/lib/types/pending-actions.d.ts +2 -2
- package/lib/types/pending-actions.js +17 -17
- package/lib/types/prompts.d.ts +2 -4
- package/lib/types/prompts.js +21 -8
- package/lib/types/propose.d.ts +26 -0
- package/lib/types/propose.js +69 -0
- package/lib/types/read-commands.d.ts +4 -4
- package/lib/types/read-commands.js +19 -26
- package/lib/types/recovery-coordinator.d.ts +34 -0
- package/lib/types/recovery-coordinator.js +84 -0
- package/lib/types/recovery-projection.d.ts +31 -0
- package/lib/types/recovery-projection.js +96 -0
- package/lib/types/resume-command.d.ts +7 -1
- package/lib/types/resume-command.js +50 -14
- package/lib/types/revision.d.ts +17 -0
- package/lib/types/revision.js +39 -3
- package/lib/types/round2-outcome.d.ts +2 -1
- package/lib/types/round2-outcome.js +68 -12
- package/lib/types/run-shell.d.ts +16 -2
- package/lib/types/run-shell.js +49 -5
- package/lib/types/runtime.d.ts +4 -6
- package/lib/types/runtime.js +341 -86
- package/lib/types/scan-command.d.ts +1 -0
- package/lib/types/scan-command.js +2 -0
- package/lib/types/scan-ports.d.ts +21 -1
- package/lib/types/scan-ports.js +51 -23
- package/lib/types/scan-run.d.ts +17 -0
- package/lib/types/scan-run.js +221 -63
- package/lib/types/screening-port.d.ts +47 -0
- package/lib/types/screening-port.js +93 -0
- package/lib/types/screening.d.ts +30 -0
- package/lib/types/screening.js +75 -0
- package/lib/types/stage-tool-registry.d.ts +7 -0
- package/lib/types/stage-tool-registry.js +12 -1
- package/lib/types/state.d.ts +15 -1
- package/lib/types/state.js +39 -17
- package/lib/types/status.d.ts +8 -0
- package/lib/types/status.js +64 -0
- package/lib/types/surface/body.d.ts +4 -0
- package/lib/types/surface/body.js +16 -0
- package/lib/types/surface/merge-view.d.ts +14 -0
- package/lib/types/surface/merge-view.js +126 -0
- package/lib/types/target-claim.d.ts +1 -0
- package/lib/types/target-claim.js +63 -18
- package/lib/types/undo-operation.d.ts +24 -0
- package/lib/types/undo-operation.js +135 -0
- package/lib/types/unit-claim.d.ts +5 -0
- package/lib/types/unit-claim.js +15 -0
- package/lib/types/wire-projection.d.ts +36 -0
- package/lib/types/wire-projection.js +69 -0
- package/lib/types/wire.d.ts +273 -0
- package/lib/types/wire.js +3 -0
- package/package.json +10 -4
- package/lib/types/routes.d.ts +0 -70
- package/lib/types/routes.js +0 -78
package/lib/types/runtime.js
CHANGED
|
@@ -3,30 +3,71 @@
|
|
|
3
3
|
* Plugin wiring supplies the channel, model and library; this module keeps the orchestration
|
|
4
4
|
* out of apply() and makes the mutating verbs testable with fakes.
|
|
5
5
|
*/
|
|
6
|
-
import {
|
|
6
|
+
import { createHash } from 'node:crypto';
|
|
7
|
+
import { errorText, probeText, SkillLibrary, UnitTransactions, skillLibraryUnitBackend } from '@lmzhen/dsh-evolution-core';
|
|
8
|
+
import { targetClaimUnitPort } from "./unit-claim.js";
|
|
7
9
|
import { canRetry, pairKey } from "./contracts.js";
|
|
8
10
|
import { skippedPairKeys } from "./skipped.js";
|
|
9
11
|
import { createReadOnlyMergeService } from "./merge-command.js";
|
|
12
|
+
import { formatRunDetail, formatRunsView } from "./status.js";
|
|
13
|
+
import { readMergeSnapshot } from "./domain/merge-snapshot.js";
|
|
14
|
+
import { buildRunDetail, buildRunsView } from "./surface/merge-view.js";
|
|
10
15
|
import { handleReadOnlyMergeCommand } from "./read-commands.js";
|
|
11
16
|
import { runScanCommand } from "./scan-command.js";
|
|
12
17
|
import { runApproveCommand } from "./approve-command.js";
|
|
13
18
|
import { runResumeCommand } from "./resume-command.js";
|
|
14
|
-
import {
|
|
15
|
-
import {
|
|
19
|
+
import { loadMergeState } from "./state.js";
|
|
20
|
+
import { pairLedger } from "./domain/pair-ledger.js";
|
|
16
21
|
import { readCommitReceipt } from "./commit-receipt.js";
|
|
17
|
-
import {
|
|
18
|
-
import {
|
|
22
|
+
import { runUndoOperation } from "./undo-operation.js";
|
|
23
|
+
import { readStageIndex } from "./domain/stage-aggregate.js";
|
|
19
24
|
import { skillLibraryUndoPort } from "./adapters/undo.js";
|
|
20
25
|
import { mergeDoctor } from "./doctor.js";
|
|
21
|
-
import {
|
|
26
|
+
import { formatReconcileReport, reconcileMergeState } from "./domain/reconcile.js";
|
|
27
|
+
import { commandError, formatMergeError } from "./domain/errors.js";
|
|
28
|
+
import { createChannelScanPorts, createStageForCards } from "./scan-ports.js";
|
|
22
29
|
import { createNewChildRound2 } from "./new-child-authoring.js";
|
|
23
30
|
import { skillLibraryPort } from "./adapters/library.js";
|
|
24
31
|
import { fitToolText } from "./presentation-budget.js";
|
|
25
|
-
import { sourceRevisionOf, targetRevisionOf } from "./revision.js";
|
|
32
|
+
import { sourceRevisionOf, targetRevisionOf, sourceRevisionRefOf, targetRevisionRefOf, supportDigestOf, revisionMismatch } from "./revision.js";
|
|
33
|
+
import { inFlightPairRun, readRunMetas } from "./domain/run-coordinator.js";
|
|
34
|
+
import { liveRowFactsOf } from "./domain/live-facts.js";
|
|
35
|
+
function fail(stage, code, hint, next, runId) {
|
|
36
|
+
return { ok: false, text: formatMergeError(commandError(stage, code, hint, next, runId)) };
|
|
37
|
+
}
|
|
38
|
+
function failRun(stage, hint, next, result) {
|
|
39
|
+
return result.error === undefined ? fail(stage, 'run-failed', hint, next, result.runId) : { ok: false, text: formatMergeError(result.error) };
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* The approved/fuse row that already holds a target. Pair/approve move a row to
|
|
43
|
+
* approved/fuse BEFORE their run starts, so this row IS the in-flight claim a second
|
|
44
|
+
* same-target run would collide with (the pair itself is guarded by run-meta).
|
|
45
|
+
*/
|
|
46
|
+
function busyTarget(rows, target, excludeRowId) {
|
|
47
|
+
const row = rows.find(item => item.id !== excludeRowId && (item.state === 'approved' || item.state === 'fuse') && item.decision?.target === target);
|
|
48
|
+
return row === undefined ? undefined : { id: row.id, ...(row.runId === undefined ? {} : { runId: row.runId }) };
|
|
49
|
+
}
|
|
26
50
|
function forcedDecisionFor(a, b) {
|
|
27
51
|
const sorted = [a, b].sort();
|
|
28
|
-
const
|
|
29
|
-
|
|
52
|
+
const raw = ('merge-' + sorted.join('-')).replace(/[^a-zA-Z0-9._-]+/g, '-').replace(/^-+|-+$/g, '');
|
|
53
|
+
const target = raw.length <= 64 ? raw : raw.slice(0, 55) + '-' + createHash('sha256').update(raw).digest('hex').slice(0, 8);
|
|
54
|
+
return { shouldMerge: true, mode: 'create-umbrella', target, sources: [a, b], rationale: 'forced pair merge of ' + sorted.join(' and '), confidence: 0.5, aSummary: 'A: ' + sorted[0] + ' 的现有技能面。', bSummary: 'B: ' + sorted[1] + ' 的现有技能面。' };
|
|
55
|
+
}
|
|
56
|
+
async function removeRuntimeRow(io, home, id, event, unitId) {
|
|
57
|
+
return await pairLedger(io, home).transact(tx => {
|
|
58
|
+
const current = tx.rows().find(item => item.id === id);
|
|
59
|
+
if (current === undefined)
|
|
60
|
+
return false;
|
|
61
|
+
const unit = event === 'committed' ? { unitId: unitId ?? 'unknown', evidenceSha256: 'legacy' } : undefined;
|
|
62
|
+
return tx.remove(id, event, unit, current.version).ok;
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
export function unitFor(deps) {
|
|
66
|
+
if (deps.unit !== undefined)
|
|
67
|
+
return deps.unit;
|
|
68
|
+
if (!(deps.skills instanceof SkillLibrary))
|
|
69
|
+
return undefined;
|
|
70
|
+
return new UnitTransactions({ io: deps.io, home: deps.home, backend: skillLibraryUnitBackend(deps.skills), claim: targetClaimUnitPort(deps.io, deps.home, deps.now), now: deps.now });
|
|
30
71
|
}
|
|
31
72
|
export function createMergeRuntime(deps) {
|
|
32
73
|
const readOnly = createReadOnlyMergeService(deps.io, deps.home);
|
|
@@ -45,40 +86,59 @@ export function createMergeRuntime(deps) {
|
|
|
45
86
|
return {
|
|
46
87
|
async execute(command, context) {
|
|
47
88
|
const parent = context?.parent ?? deps.parent;
|
|
89
|
+
const agentless = context?.agentless === true;
|
|
48
90
|
const detached = deps.detached !== false;
|
|
49
91
|
if (command.verb === 'pair') {
|
|
50
92
|
const signal = new AbortController().signal;
|
|
51
93
|
if (command.a === command.b)
|
|
52
|
-
return
|
|
94
|
+
return fail('pair', 'pair-same-source', 'merge pair 需要两个不同技能源:' + command.a, '重新指定两个不同的 family-managed 技能');
|
|
95
|
+
if ([command.a, command.b].some(name => name.includes('/') || name.includes(':')))
|
|
96
|
+
return fail('pair', 'target-invalid', '技能名不能包含 provider/name 或 provider:name 语法:' + command.a + ' / ' + command.b, '使用裸技能名重新 pair');
|
|
97
|
+
// FR-RUN-03: a session-less invocation cannot host a merge child. Answer E-305 in
|
|
98
|
+
// the three-line shape instead of opening a run that is guaranteed to fail.
|
|
99
|
+
if (agentless)
|
|
100
|
+
return fail('pair', 'e-305', errorText('e-305-this-invocation-carries-no', { a1: 'pair' }), '在 GUI 或 CLI 的会话里运行 /evolution merge pair ' + command.a + ' ' + command.b);
|
|
101
|
+
// FR-EXEC-17/NFR-04: two pair runs for one pair must not race for the same row.
|
|
102
|
+
const pairKeyValue = pairKey(command.a, command.b);
|
|
103
|
+
const inFlight = inFlightPairRun(await readRunMetas(deps.io, deps.home), pairKeyValue);
|
|
104
|
+
if (inFlight !== undefined && (deps.isOwnerActive === undefined || (await deps.isOwnerActive(inFlight.runId)) === true)) {
|
|
105
|
+
return fail('pair', 'merge-pair-in-flight', '同一 pair 已在运行 ' + inFlight.runId, '等待该 run 结束,或运行 /evolution merge status ' + inFlight.runId, inFlight.runId);
|
|
106
|
+
}
|
|
53
107
|
const cards = await deps.cards(signal);
|
|
54
108
|
const cardA = cards.find(card => card.name === command.a);
|
|
55
109
|
const cardB = cards.find(card => card.name === command.b);
|
|
56
110
|
if (cardA === undefined || cardB === undefined || cardA.managed !== true || cardB.managed !== true) {
|
|
57
|
-
return
|
|
111
|
+
return fail('pair', 'pair-not-managed', 'merge pair 仅支持 family-managed 技能:' + command.a + ' / ' + command.b, '运行 /evolution doctor 查看 family-managed 技能范围');
|
|
58
112
|
}
|
|
59
113
|
if (cardA.protectedBy != null || cardB.protectedBy != null
|
|
60
114
|
|| cardA.protectionUnknown === true || cardB.protectionUnknown === true) {
|
|
61
|
-
return
|
|
115
|
+
return fail('pair', 'pair-protected', 'merge pair 不接受受保护/保护状态未知技能:' + command.a + ' / ' + command.b, '解除保护或换一对技能后重试');
|
|
62
116
|
}
|
|
63
117
|
if ((await deps.isBlocked?.(command.a)) === true || (await deps.isBlocked?.(command.b)) === true) {
|
|
64
|
-
return
|
|
118
|
+
return fail('pair', 'pair-blocked', 'merge pair 不接受被范围门拦下的技能:' + command.a + ' / ' + command.b, '运行 /evolution scope 查看范围门,或换一对技能');
|
|
65
119
|
}
|
|
66
120
|
const pair = [{ a: command.a, b: command.b }];
|
|
67
121
|
const existing = (await loadMergeState(deps.io, deps.home)).state.rows.find(row => row.pairKey === pairKey(command.a, command.b));
|
|
68
122
|
if (existing?.state === 'skipped') {
|
|
69
|
-
await
|
|
123
|
+
const dropped = await removeRuntimeRow(deps.io, deps.home, existing.id, 'superseded');
|
|
124
|
+
if (!dropped)
|
|
125
|
+
return fail('pair', 'row-changed', '清理 skipped 行时发现并发修改:' + existing.id, '运行 /evolution merge status 后重试');
|
|
126
|
+
}
|
|
127
|
+
if (existing?.state === 'verify' && existing.runId !== undefined && (await deps.isOwnerActive?.(existing.runId)) === true) {
|
|
128
|
+
return fail('pair', 'merge-pair-in-flight', '同一 pair 已在运行 ' + existing.runId, '等待该 run 结束,或运行 /evolution merge status ' + existing.runId, existing.runId);
|
|
70
129
|
}
|
|
71
130
|
if (existing?.state === 'pending') {
|
|
72
|
-
return
|
|
131
|
+
return fail('pair', 'pair-pending', '该 pair 已在待批准队列:' + existing.id, '先运行 /evolution merge approve/reject/discard ' + existing.id, existing.runId);
|
|
73
132
|
}
|
|
74
133
|
if (existing?.state === 'approved' || existing?.state === 'fuse') {
|
|
75
|
-
return
|
|
134
|
+
return fail('pair', 'pair-fuse', '该 pair 已在待合并队列,可复用决策重跑', '运行 /evolution merge resume --retry ' + (existing.runId ?? existing.id), existing.runId);
|
|
76
135
|
}
|
|
77
136
|
const forced = command.force === true ? forcedDecisionFor(command.a, command.b) : undefined;
|
|
78
137
|
const approval = deps.params.mergeApproval;
|
|
79
138
|
const result = await runScanCommand({
|
|
80
139
|
...common,
|
|
81
140
|
approval,
|
|
141
|
+
cards,
|
|
82
142
|
...(detached ? { detached: true } : {}),
|
|
83
143
|
makePorts: (runId) => {
|
|
84
144
|
const ports = createChannelScanPorts({
|
|
@@ -88,21 +148,25 @@ export function createMergeRuntime(deps) {
|
|
|
88
148
|
...(deps.cwd === undefined ? {} : { cwd: deps.cwd }),
|
|
89
149
|
timeoutMs: deps.params.mergePairTimeoutMinutes * 60_000,
|
|
90
150
|
approval,
|
|
91
|
-
commitPort: skillLibraryPort(deps.skills), now: deps.now,
|
|
151
|
+
commitPort: skillLibraryPort(deps.skills), now: deps.now, ...(unitFor(deps) === undefined ? {} : { unit: unitFor(deps) }),
|
|
152
|
+
...(deps.params.limitsSnapshot === undefined ? {} : { limits: deps.params.limitsSnapshot.skillLimits }),
|
|
153
|
+
...(deps.isBlocked === undefined ? {} : { isBlocked: deps.isBlocked }),
|
|
92
154
|
...stageHooks,
|
|
93
155
|
});
|
|
94
156
|
return forced === undefined ? ports : { ...ports, round1: () => Promise.resolve({ kind: 'merge', decision: forced }) };
|
|
95
157
|
},
|
|
96
158
|
});
|
|
97
159
|
if (!result.ok)
|
|
98
|
-
return
|
|
160
|
+
return failRun('pair', result.message, '修复后重新发起 pair;run 报告见 /evolution merge report', result);
|
|
99
161
|
if ('accepted' in result)
|
|
100
162
|
return { ok: true, text: 'merge pair 已启动:' + result.runId };
|
|
101
163
|
if (!('report' in result))
|
|
102
|
-
return
|
|
164
|
+
return fail('pair', 'report-missing', 'merge pair 已结束但没有写报告', '重新运行 pair,或运行 /evolution merge runs 查看运行');
|
|
103
165
|
return { ok: true, text: 'merge pair 完成:merged ' + String(result.report.totals.merged) + ' / pending ' + String(result.report.totals.pending) + ' / failed ' + String(result.report.totals.failed) };
|
|
104
166
|
}
|
|
105
167
|
if (command.verb === 'scan') {
|
|
168
|
+
if (agentless)
|
|
169
|
+
return fail('scan', 'e-305', errorText('e-305-this-invocation-carries-no', { a1: 'scan' }), '在 GUI 或 CLI 的会话里运行 /evolution merge scan');
|
|
106
170
|
const signal = new AbortController().signal;
|
|
107
171
|
const cards = await deps.cards(signal);
|
|
108
172
|
const blocked = new Set();
|
|
@@ -117,52 +181,128 @@ export function createMergeRuntime(deps) {
|
|
|
117
181
|
const held = new Set(loadedState.state.rows.filter(row => row.queue === 'pending' || row.queue === 'fuse'
|
|
118
182
|
|| (row.queue === 'verify' && (row.runId !== undefined && deps.now() - Date.parse(row.updatedAt) < 10 * 60_000 || !canRetry(row))))
|
|
119
183
|
.map(row => row.pairKey));
|
|
120
|
-
const
|
|
184
|
+
const notes = loadedState.state.rows
|
|
121
185
|
.filter(row => row.state === 'pending' || row.state === 'approved' || row.state === 'fuse' || row.state === 'skipped')
|
|
122
186
|
.map(row => 'conflict:' + row.state + ':' + row.pairKey + ':' + row.id);
|
|
123
187
|
const result = await runScanCommand({
|
|
124
188
|
...common,
|
|
125
189
|
approval: deps.params.mergeApproval,
|
|
126
|
-
notes
|
|
190
|
+
notes,
|
|
191
|
+
cards,
|
|
127
192
|
...(detached ? { detached: true } : {}),
|
|
128
193
|
makePorts: runId => createChannelScanPorts({
|
|
129
194
|
io: deps.io, home: deps.home, runId, cards,
|
|
195
|
+
resolve: async (candidate) => {
|
|
196
|
+
const live = await deps.cards(signal);
|
|
197
|
+
return live.some(card => card.name === candidate.a) && live.some(card => card.name === candidate.b);
|
|
198
|
+
},
|
|
130
199
|
screening: async (turnSignal) => {
|
|
131
200
|
const raw = await deps.screeningForRun(cards, runId, turnSignal);
|
|
201
|
+
const degraded = raw.degraded;
|
|
202
|
+
if (degraded !== undefined)
|
|
203
|
+
notes.push(...degraded);
|
|
132
204
|
const filtered = raw.filter((candidate) => {
|
|
133
205
|
const key = pairKey(candidate.a, candidate.b);
|
|
134
206
|
return managed.has(candidate.a) && managed.has(candidate.b) && !skipped.has(key) && !held.has(key);
|
|
135
207
|
});
|
|
136
208
|
const candidatePairs = raw.candidatePairs;
|
|
137
|
-
|
|
209
|
+
const reportOnly = raw.reportOnly;
|
|
210
|
+
return Object.assign(filtered, {
|
|
211
|
+
...(candidatePairs === undefined ? {} : { candidatePairs }),
|
|
212
|
+
...(reportOnly === undefined ? {} : { reportOnly }),
|
|
213
|
+
});
|
|
138
214
|
},
|
|
139
215
|
channel: deps.channel, parent, allow: deps.allow,
|
|
140
216
|
...(deps.cwd === undefined ? {} : { cwd: deps.cwd }),
|
|
141
217
|
timeoutMs: deps.params.mergePairTimeoutMinutes * 60_000,
|
|
142
218
|
approval: deps.params.mergeApproval,
|
|
143
|
-
commitPort: skillLibraryPort(deps.skills), now: deps.now,
|
|
219
|
+
commitPort: skillLibraryPort(deps.skills), now: deps.now, ...(unitFor(deps) === undefined ? {} : { unit: unitFor(deps) }),
|
|
220
|
+
...(deps.params.limitsSnapshot === undefined ? {} : { limits: deps.params.limitsSnapshot.skillLimits }),
|
|
221
|
+
...(deps.isBlocked === undefined ? {} : { isBlocked: deps.isBlocked }),
|
|
144
222
|
...stageHooks,
|
|
145
223
|
}),
|
|
146
224
|
});
|
|
147
225
|
if (!result.ok)
|
|
148
|
-
return
|
|
226
|
+
return failRun('scan', result.message, '修复后重新 scan;run 报告见 /evolution merge report', result);
|
|
149
227
|
if ('accepted' in result)
|
|
150
228
|
return { ok: true, text: 'merge scan 已启动:' + result.runId };
|
|
151
229
|
if (!('report' in result))
|
|
152
|
-
return
|
|
230
|
+
return fail('scan', 'report-missing', 'merge scan 已结束但没有写报告', '重新运行 scan,或运行 /evolution merge runs 查看运行');
|
|
153
231
|
return { ok: true, text: 'merge scan 完成:merged ' + String(result.report.totals.merged) + ' / pending ' + String(result.report.totals.pending) + ' / failed ' + String(result.report.totals.failed) };
|
|
154
232
|
}
|
|
233
|
+
if (command.verb === 'candidates' && command.retry === true) {
|
|
234
|
+
if (command.id === undefined || command.id === '')
|
|
235
|
+
return fail('candidates', 'candidates-id-required', 'candidates --retry 需要行 id', '运行 /evolution merge candidates 查看可重试行');
|
|
236
|
+
const loaded = await loadMergeState(deps.io, deps.home);
|
|
237
|
+
const row = loaded.state.rows.find(item => item.id === command.id);
|
|
238
|
+
if (row === undefined)
|
|
239
|
+
return fail('candidates', 'row-not-found', '找不到候选行:' + command.id, '运行 /evolution merge candidates 查看候选');
|
|
240
|
+
if (row.state !== 'verify')
|
|
241
|
+
return fail('candidates', 'candidates-retry-unavailable', '只有待判断行可重跑第 1 轮:' + row.state, '改用 resume --retry 或重新 pair');
|
|
242
|
+
const signal = new AbortController().signal;
|
|
243
|
+
const cards = await deps.cards(signal);
|
|
244
|
+
const pairCandidate = { a: row.a, b: row.b };
|
|
245
|
+
const result = await runScanCommand({
|
|
246
|
+
...common,
|
|
247
|
+
approval: deps.params.mergeApproval,
|
|
248
|
+
notes: ['candidates --retry ' + row.id],
|
|
249
|
+
cards,
|
|
250
|
+
makePorts: runId => createChannelScanPorts({
|
|
251
|
+
io: deps.io, home: deps.home, runId, cards,
|
|
252
|
+
screening: async () => [pairCandidate],
|
|
253
|
+
resolve: async (candidate) => {
|
|
254
|
+
const live = await deps.cards(signal);
|
|
255
|
+
return live.some(card => card.name === candidate.a) && live.some(card => card.name === candidate.b);
|
|
256
|
+
},
|
|
257
|
+
channel: deps.channel, parent, allow: deps.allow,
|
|
258
|
+
...(deps.cwd === undefined ? {} : { cwd: deps.cwd }),
|
|
259
|
+
timeoutMs: deps.params.mergePairTimeoutMinutes * 60_000,
|
|
260
|
+
approval: deps.params.mergeApproval,
|
|
261
|
+
commitPort: skillLibraryPort(deps.skills), now: deps.now, ...(unitFor(deps) === undefined ? {} : { unit: unitFor(deps) }),
|
|
262
|
+
...(deps.params.limitsSnapshot === undefined ? {} : { limits: deps.params.limitsSnapshot.skillLimits }),
|
|
263
|
+
...(deps.isBlocked === undefined ? {} : { isBlocked: deps.isBlocked }),
|
|
264
|
+
...stageHooks,
|
|
265
|
+
}),
|
|
266
|
+
});
|
|
267
|
+
if (!result.ok)
|
|
268
|
+
return failRun('candidates', result.message, '修复后重试 candidates --retry', result);
|
|
269
|
+
if ('accepted' in result)
|
|
270
|
+
return { ok: true, text: 'candidates --retry 已启动:' + result.runId };
|
|
271
|
+
if (!('report' in result))
|
|
272
|
+
return fail('candidates', 'report-missing', 'candidates --retry 已结束但没有写报告', '运行 /evolution merge runs 查看运行');
|
|
273
|
+
return { ok: true, text: 'candidates --retry 完成:merged ' + String(result.report.totals.merged) + ' / pending ' + String(result.report.totals.pending) + ' / failed ' + String(result.report.totals.failed) };
|
|
274
|
+
}
|
|
155
275
|
if (command.verb === 'reject') {
|
|
156
276
|
const loaded = await loadMergeState(deps.io, deps.home);
|
|
157
277
|
const row = loaded.state.rows.find(item => item.id === command.id);
|
|
158
278
|
if (row === undefined)
|
|
159
|
-
return
|
|
160
|
-
const
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
279
|
+
return fail('reject', 'row-not-found', '找不到合并行 ' + command.id, '运行 /evolution merge pending 查看待处理行', command.id);
|
|
280
|
+
const changed = await pairLedger(deps.io, deps.home).transact(tx => {
|
|
281
|
+
const current = tx.rows().find(item => item.id === row.id);
|
|
282
|
+
if (current === undefined || current.state !== 'pending')
|
|
283
|
+
return false;
|
|
284
|
+
return tx.transition(row.id, { type: 'skipped', reason: 'rejected', origin: 'human' }, current.version).ok;
|
|
285
|
+
});
|
|
286
|
+
if (!changed)
|
|
287
|
+
return fail('reject', 'row-not-pending', '该行不是 pending,或已被并发修改:' + command.id, '运行 /evolution merge status ' + command.id + ' 后重试', row.runId);
|
|
164
288
|
return { ok: true, text: '已拒绝合并:' + command.id };
|
|
165
289
|
}
|
|
290
|
+
if (command.verb === 'reconcile') {
|
|
291
|
+
const cards = await deps.cards(new AbortController().signal).catch(() => undefined);
|
|
292
|
+
// An empty/unreadable library is NOT "every source is gone": only a non-empty live
|
|
293
|
+
// list may authorize source-gone convergence.
|
|
294
|
+
const managed = cards === undefined || cards.length === 0 ? undefined : new Set(cards.map(card => card.name));
|
|
295
|
+
const live = cards === undefined || cards.length === 0 ? undefined : liveRowFactsOf(cards);
|
|
296
|
+
try {
|
|
297
|
+
const report = await reconcileMergeState({ io: deps.io, home: deps.home, now: deps.now, ttlDays: deps.params.mergeSkippedTtlDays, ...(managed === undefined ? {} : { managed }), ...(live === undefined ? {} : { live }) });
|
|
298
|
+
if (report.state === 'blocked')
|
|
299
|
+
return fail('reconcile', report.code ?? 'merge-state-blocked', report.message ?? '合并状态不可读', '先修复合并状态,再运行 /evolution merge doctor');
|
|
300
|
+
return { ok: true, text: formatReconcileReport(report) };
|
|
301
|
+
}
|
|
302
|
+
catch (error) {
|
|
303
|
+
return fail('reconcile', 'reconcile-failed', error instanceof Error ? error.message : String(error), '运行 /evolution merge doctor 查看状态后重试');
|
|
304
|
+
}
|
|
305
|
+
}
|
|
166
306
|
if (command.verb === 'doctor') {
|
|
167
307
|
const rows = await mergeDoctor(deps.io, deps.home, deps.params.mergeModelsRoot);
|
|
168
308
|
return { ok: true, text: rows.map(row => row.key + ' [' + row.status + '] ' + row.label + ':' + row.detail).join('\n') };
|
|
@@ -171,82 +311,105 @@ export function createMergeRuntime(deps) {
|
|
|
171
311
|
const registry = deps.registry;
|
|
172
312
|
if (command.verb === 'runs') {
|
|
173
313
|
const mergeRuns = registry.runs('merge');
|
|
174
|
-
|
|
314
|
+
const snapshot = await readMergeSnapshot({ io: deps.io, home: deps.home, runs: mergeRuns, approval: deps.params.mergeApproval, now: deps.now });
|
|
315
|
+
return { ok: true, text: formatRunsView(buildRunsView(snapshot)) };
|
|
175
316
|
}
|
|
176
317
|
if (command.verb === 'cancel') {
|
|
177
318
|
if (command.id !== undefined) {
|
|
178
319
|
const cancelled = registry.cancel(command.id);
|
|
179
|
-
return { ok:
|
|
320
|
+
return cancelled ? { ok: true, text: '已请求取消 ' + command.id } : fail('cancel', 'run-not-found', '没有可取消的 run:' + command.id, '运行 /evolution merge runs 查看可取消的运行');
|
|
180
321
|
}
|
|
181
|
-
const
|
|
182
|
-
|
|
322
|
+
const runs = registry.runs('merge');
|
|
323
|
+
let count = 0;
|
|
324
|
+
for (const run of runs)
|
|
325
|
+
if (registry.cancel(run.id))
|
|
326
|
+
count += 1;
|
|
327
|
+
return { ok: true, text: '已请求取消 ' + String(count) + ' 个 merge 运行' };
|
|
183
328
|
}
|
|
184
329
|
const run = command.id === undefined ? registry.runs('merge')[0] : registry.find(command.id);
|
|
185
330
|
if (run === undefined)
|
|
186
|
-
return
|
|
331
|
+
return fail('report', 'run-not-found', '找不到 merge run:' + (command.id ?? '(latest)'), '运行 /evolution merge runs 查看运行列表');
|
|
187
332
|
if (run.resultRef === undefined)
|
|
188
|
-
return
|
|
333
|
+
return fail('report', 'report-missing', '该 run 还没有报告:' + run.id, '等待 run 结束,或运行 /evolution merge status ' + run.id, run.id);
|
|
189
334
|
const report = await probeText(deps.io, run.resultRef);
|
|
190
335
|
if (report.kind === 'unknown')
|
|
191
|
-
return
|
|
336
|
+
return fail('report', 'report-unreadable', '报告不可读:' + run.resultRef + '(' + report.reason + ')', '检查结果文件权限,或重新运行该命令', run.id);
|
|
192
337
|
return report.kind === 'absent'
|
|
193
|
-
?
|
|
338
|
+
? fail('report', 'report-missing', '报告不存在:' + run.resultRef, '重新运行该命令生成报告', run.id)
|
|
194
339
|
: { ok: true, text: 'report: ' + run.resultRef + '\n' + fitToolText(report.value, 7600) };
|
|
195
340
|
}
|
|
196
341
|
if (command.verb === 'discard') {
|
|
197
342
|
const loaded = await loadMergeState(deps.io, deps.home);
|
|
198
343
|
const row = loaded.state.rows.find(item => item.id === command.id);
|
|
199
344
|
if (row === undefined)
|
|
200
|
-
return
|
|
345
|
+
return fail('discard', 'row-not-found', '找不到合并行 ' + command.id, '运行 /evolution merge status 查看当前队列', command.id);
|
|
201
346
|
if (row.state !== 'pending' && row.state !== 'approved' && row.state !== 'fuse')
|
|
202
|
-
return
|
|
203
|
-
await
|
|
347
|
+
return fail('discard', 'row-not-discardable', '该行状态是 ' + row.state + ',不可丢弃:' + command.id, '只有 pending/approved/fuse 行可丢弃', row.runId);
|
|
348
|
+
const dropped = await removeRuntimeRow(deps.io, deps.home, row.id, 'superseded');
|
|
349
|
+
if (!dropped)
|
|
350
|
+
return fail('discard', 'row-changed', '丢弃时发现并发修改:' + command.id, '运行 /evolution merge status ' + command.id + ' 后重试');
|
|
204
351
|
return { ok: true, text: '已丢弃合并行:' + command.id };
|
|
205
352
|
}
|
|
206
353
|
if (command.verb === 'undo') {
|
|
207
354
|
const receipt = await readCommitReceipt(deps.io, deps.home, command.run, command.target);
|
|
208
355
|
if (receipt === null)
|
|
209
|
-
return
|
|
356
|
+
return fail('undo', 'receipt-not-found', '该 run 没有提交回执:' + command.run, '只有已提交的 run 可 undo;运行 /evolution merge runs 查看');
|
|
210
357
|
if (receipt.target !== command.target)
|
|
211
|
-
return
|
|
212
|
-
const
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
358
|
+
return fail('undo', 'receipt-target-mismatch', '回执目标是 ' + receipt.target + ',不是 ' + command.target, '按回执目标重新运行 undo');
|
|
359
|
+
const undoRunId = receipt.parentRunId ?? receipt.runId;
|
|
360
|
+
const undoRegistry = deps.registry;
|
|
361
|
+
if (undoRegistry.runs('merge').some(run => run.id === undoRunId && run.supersededAt !== undefined))
|
|
362
|
+
return fail('undo', 'run-superseded', '该 run 已因技能树 restore 失效,不能再 undo:' + undoRunId, '运行 /evolution merge runs 查看失效标记');
|
|
363
|
+
const unit = unitFor(deps);
|
|
364
|
+
const result = await runUndoOperation({ io: deps.io, home: deps.home, runId: undoRunId, port: skillLibraryUndoPort(deps.skills), now: deps.now, ...(receipt.pairKey === undefined ? {} : { pairKey: receipt.pairKey }), ...(unit === undefined ? {} : { finalizeUnit: id => unit.finalize.call(unit, id) }) });
|
|
365
|
+
return result.ok ? { ok: true, text: result.message } : fail('undo', 'undo-failed', result.message, '检查回执与暂存后重试');
|
|
217
366
|
}
|
|
218
367
|
if (command.verb === 'skipped' && command.restore !== undefined) {
|
|
219
368
|
const loaded = await loadMergeState(deps.io, deps.home);
|
|
220
369
|
const row = loaded.state.rows.find(item => item.id === command.restore);
|
|
221
370
|
if (row === undefined || row.state !== 'skipped')
|
|
222
|
-
return
|
|
223
|
-
await
|
|
371
|
+
return fail('skipped', 'row-not-found', '找不到可恢复的 skipped 行:' + command.restore, '运行 /evolution merge skipped 查看跳过队列', command.restore);
|
|
372
|
+
const dropped = await removeRuntimeRow(deps.io, deps.home, row.id, 'restored');
|
|
373
|
+
if (!dropped)
|
|
374
|
+
return fail('skipped', 'row-changed', '恢复时发现并发修改:' + row.id, '运行 /evolution merge status 后重试');
|
|
224
375
|
return { ok: true, text: '已恢复参与筛选:' + row.id };
|
|
225
376
|
}
|
|
226
377
|
if (command.verb === 'resume') {
|
|
227
378
|
if (command.clear !== undefined) {
|
|
228
379
|
const cleared = (await loadMergeState(deps.io, deps.home)).state.rows.find(item => item.id === command.clear);
|
|
229
380
|
if (cleared === undefined)
|
|
230
|
-
return
|
|
231
|
-
await
|
|
381
|
+
return fail('resume', 'row-not-found', '找不到要清除的行:' + command.clear, '运行 /evolution merge status 查看当前队列');
|
|
382
|
+
const dropped = await removeRuntimeRow(deps.io, deps.home, cleared.id, 'superseded');
|
|
383
|
+
if (!dropped)
|
|
384
|
+
return fail('resume', 'row-changed', '清除时发现并发修改:' + cleared.id, '运行 /evolution merge status 后重试');
|
|
232
385
|
return { ok: true, text: '已清除合并行:' + cleared.id };
|
|
233
386
|
}
|
|
234
387
|
const loaded = await loadMergeState(deps.io, deps.home);
|
|
235
|
-
let row = command.id === undefined ? undefined : loaded.state.rows.find(item => item.id === command.id);
|
|
236
388
|
if (command.id === undefined) {
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
continue;
|
|
240
|
-
if (item.runId !== undefined && (await deps.isOwnerActive?.(item.runId)) === true)
|
|
241
|
-
continue;
|
|
242
|
-
row = item;
|
|
243
|
-
break;
|
|
244
|
-
}
|
|
389
|
+
const resumable = loaded.state.rows.filter(item => item.state === 'fuse' || item.state === 'verify' || (command.retry === true && item.state === 'approved'));
|
|
390
|
+
return fail('resume', 'resume-id-required', 'merge resume 需要 <id>;可恢复行:' + (resumable.length === 0 ? '(none)' : resumable.map(item => item.id).join(', ')), '运行 /evolution merge resume --retry <id> 复用某一行的决策');
|
|
245
391
|
}
|
|
392
|
+
const row = loaded.state.rows.find(item => item.id === command.id);
|
|
246
393
|
if (row === undefined)
|
|
247
|
-
return
|
|
394
|
+
return fail('resume', 'row-not-found', '找不到可恢复行 ' + command.id, '运行 /evolution merge candidates 查看可恢复行');
|
|
248
395
|
const signal = new AbortController().signal;
|
|
249
396
|
const cards = await deps.cards(signal);
|
|
397
|
+
if ((row.state === 'fuse' || row.state === 'approved') && row.decision !== undefined && row.decision.mode !== 'create-umbrella') {
|
|
398
|
+
const targetCard = cards.find(card => card.name === row.decision?.target);
|
|
399
|
+
if (targetCard !== undefined && targetCard.managed !== true)
|
|
400
|
+
return fail('resume', 'target-not-managed', 'target 不是 family-managed:' + row.decision.target, '重新 pair 并选一个 managed 目标', row.runId);
|
|
401
|
+
if (targetCard !== undefined && (targetCard.protectionUnknown === true || targetCard.protectedBy != null))
|
|
402
|
+
return fail('resume', 'target-protected', 'target 受保护:' + row.decision.target, '解除保护或改选其它目标', row.runId);
|
|
403
|
+
if (await deps.isBlocked?.(row.decision.target) === true)
|
|
404
|
+
return fail('resume', 'target-blocked', 'target 被范围门拦下:' + row.decision.target, '运行 /evolution scope 查看范围门', row.runId);
|
|
405
|
+
}
|
|
406
|
+
// NFR-04: a resume commits into the same target a running approve/pair may hold;
|
|
407
|
+
// the target is serialised here too, not only at the commit claim.
|
|
408
|
+
if ((row.state === 'fuse' || row.state === 'approved') && row.decision !== undefined) {
|
|
409
|
+
const busy = busyTarget(loaded.state.rows, row.decision.target, row.id);
|
|
410
|
+
if (busy !== undefined)
|
|
411
|
+
return fail('resume', 'merge-target-busy', '目标 ' + row.decision.target + ' 正被 ' + busy.id + ' 占用,串行等待', '等待占用行结算,或 /evolution merge discard ' + busy.id, busy.runId);
|
|
412
|
+
}
|
|
250
413
|
const pair = () => Promise.resolve([{ a: row.a, b: row.b }]);
|
|
251
414
|
const failed = (stage, code, message) => Promise.resolve({
|
|
252
415
|
kind: 'failed',
|
|
@@ -258,85 +421,177 @@ export function createMergeRuntime(deps) {
|
|
|
258
421
|
round2: () => failed('author', 'resume-needs-child', 'fuse resume needs a new child'),
|
|
259
422
|
};
|
|
260
423
|
const result = await runResumeCommand({
|
|
261
|
-
...common, rowId: row.id, ports: basePorts,
|
|
424
|
+
...common, rowId: row.id, ports: basePorts, approval: deps.params.mergeApproval,
|
|
425
|
+
...(agentless ? { agentless: true } : {}),
|
|
426
|
+
...(detached ? { detached: true } : {}),
|
|
427
|
+
pairTimeoutMinutes: deps.params.mergePairTimeoutMinutes,
|
|
262
428
|
...(deps.isOwnerActive === undefined ? {} : { isOwnerActive: deps.isOwnerActive }),
|
|
263
429
|
makePorts: runId => createChannelScanPorts({
|
|
264
430
|
io: deps.io, home: deps.home, runId, cards, screening: pair,
|
|
265
431
|
channel: deps.channel, parent, allow: deps.allow,
|
|
266
432
|
...(deps.cwd === undefined ? {} : { cwd: deps.cwd }),
|
|
267
433
|
timeoutMs: deps.params.mergePairTimeoutMinutes * 60_000,
|
|
268
|
-
approval: deps.params.mergeApproval, commitPort: skillLibraryPort(deps.skills), now: deps.now,
|
|
434
|
+
approval: deps.params.mergeApproval, commitPort: skillLibraryPort(deps.skills), now: deps.now, ...(unitFor(deps) === undefined ? {} : { unit: unitFor(deps) }),
|
|
435
|
+
...(deps.params.limitsSnapshot === undefined ? {} : { limits: deps.params.limitsSnapshot.skillLimits }),
|
|
436
|
+
...(deps.isBlocked === undefined ? {} : { isBlocked: deps.isBlocked }),
|
|
269
437
|
}),
|
|
270
438
|
round2WithRun: createNewChildRound2({
|
|
271
439
|
io: deps.io, home: deps.home, cards, channel: deps.channel, parent,
|
|
272
440
|
allow: deps.allow, ...(deps.cwd === undefined ? {} : { cwd: deps.cwd }),
|
|
273
441
|
timeoutMs: deps.params.mergePairTimeoutMinutes * 60_000,
|
|
274
|
-
commitPort: skillLibraryPort(deps.skills), now: deps.now,
|
|
442
|
+
commitPort: skillLibraryPort(deps.skills), now: deps.now, ...(unitFor(deps) === undefined ? {} : { unit: unitFor(deps) }),
|
|
443
|
+
...(deps.params.limitsSnapshot === undefined ? {} : { limits: deps.params.limitsSnapshot.skillLimits }),
|
|
444
|
+
...(deps.isBlocked === undefined ? {} : { isBlocked: deps.isBlocked }),
|
|
275
445
|
...stageHooks,
|
|
276
446
|
}),
|
|
277
447
|
});
|
|
278
448
|
if (!result.ok)
|
|
279
|
-
return
|
|
449
|
+
return failRun('resume', result.message, '修复后重新 resume;run 报告见 /evolution merge report', result);
|
|
280
450
|
if ('accepted' in result)
|
|
281
451
|
return { ok: true, text: 'merge resume 已启动:' + result.runId };
|
|
282
452
|
if (!('report' in result))
|
|
283
|
-
return
|
|
453
|
+
return fail('resume', 'report-missing', 'merge resume 已结束但没有写报告', '重新运行 resume,或运行 /evolution merge runs 查看运行');
|
|
284
454
|
return { ok: true, text: 'merge resume 完成:merged ' + String(result.report.totals.merged) };
|
|
285
455
|
}
|
|
286
456
|
if (command.verb === 'approve') {
|
|
287
457
|
const signal = new AbortController().signal;
|
|
288
458
|
const cards = await deps.cards(signal);
|
|
289
|
-
|
|
459
|
+
let candidate = (await loadMergeState(deps.io, deps.home)).state.rows.find(row => row.id === command.id);
|
|
460
|
+
if (candidate !== undefined && candidate.stagingRef === undefined) {
|
|
461
|
+
const entry = await readStageIndex(deps.io, deps.home, candidate.id, deps.now());
|
|
462
|
+
if (entry !== null)
|
|
463
|
+
candidate = { ...candidate, stagingRef: entry.ref, stagingExpiresAt: entry.expiresAt };
|
|
464
|
+
}
|
|
290
465
|
if (candidate !== undefined && candidate.state === 'pending' && candidate.decision !== undefined) {
|
|
466
|
+
const target = candidate.decision.target;
|
|
467
|
+
const rowsNow = (await loadMergeState(deps.io, deps.home)).state.rows;
|
|
468
|
+
// NFR-04: a second approve for the same target waits for the holder.
|
|
469
|
+
const busy = busyTarget(rowsNow, target, candidate.id);
|
|
470
|
+
if (busy !== undefined) {
|
|
471
|
+
return fail('approve', 'merge-target-busy', '目标 ' + target + ' 正被 ' + busy.id + ' 占用,串行等待', '等待占用行结算,或 /evolution merge discard ' + busy.id, busy.runId);
|
|
472
|
+
}
|
|
473
|
+
const targetCardNow = cards.find(card => card.name === target);
|
|
474
|
+
if (targetCardNow !== undefined && (targetCardNow.protectionUnknown === true || targetCardNow.protectedBy != null)) {
|
|
475
|
+
return fail('approve', 'target-protected', '目标技能「' + target + '」在决策后变为受保护,拒绝提交', '解除保护或改选其它目标', candidate.runId);
|
|
476
|
+
}
|
|
291
477
|
const missingSource = [candidate.a, candidate.b].find(name => !cards.some(card => card.name === name && card.managed === true));
|
|
292
478
|
if (missingSource !== undefined) {
|
|
293
|
-
await
|
|
294
|
-
|
|
479
|
+
const dropped = await removeRuntimeRow(deps.io, deps.home, candidate.id, 'superseded');
|
|
480
|
+
if (!dropped)
|
|
481
|
+
return fail('approve', 'row-changed', 'source-gone,但行已被并发修改,未清理:' + candidate.id, '运行 /evolution merge status ' + candidate.id + ' 后重试');
|
|
482
|
+
return fail('approve', 'source-gone', missingSource + ' 已不在 family-managed 技能库,已从 active 队列清除', '重新 pair 该技能对;剩余行用 /evolution merge discard ' + candidate.id, candidate.runId);
|
|
483
|
+
}
|
|
484
|
+
const sourceExpected = candidate.decision?.sourceRevisionRef ?? candidate.sourceRevisionRef;
|
|
485
|
+
const targetExpected = candidate.decision?.targetRevisionRef ?? candidate.targetRevisionRef;
|
|
486
|
+
const targetMismatch = revisionMismatch(targetExpected, targetRevisionRefOf(cards, candidate.decision.target));
|
|
487
|
+
if (targetMismatch !== null) {
|
|
488
|
+
await pairLedger(deps.io, deps.home).transact(tx => { const current = tx.rows().find(item => item.id === candidate.id); return current === undefined ? false : tx.transition(candidate.id, { type: 'expired', kind: 'target-taken' }, current.version).ok; });
|
|
489
|
+
return fail('approve', 'target-taken', '目标 ' + candidate.decision.target + ' 在决策后已被占用', '重新 pair,或 /evolution merge discard ' + candidate.id, candidate.runId);
|
|
490
|
+
}
|
|
491
|
+
if (candidate.decision.supportDigest !== undefined && candidate.decision.supportDigest !== supportDigestOf(cards, [candidate.decision.target, candidate.a, candidate.b])) {
|
|
492
|
+
await pairLedger(deps.io, deps.home).transact(tx => { const current = tx.rows().find(item => item.id === candidate.id); return current === undefined ? false : tx.transition(candidate.id, { type: 'expired', kind: 'source-changed' }, current.version).ok; });
|
|
493
|
+
return fail('approve', 'source-changed', '源技能内容在决策后变化,拒绝照跑', '重新 pair,或 /evolution merge discard ' + candidate.id, candidate.runId);
|
|
494
|
+
}
|
|
495
|
+
if (revisionMismatch(sourceExpected, sourceRevisionRefOf(cards, candidate.a, candidate.b)) !== null) {
|
|
496
|
+
await pairLedger(deps.io, deps.home).transact(tx => { const current = tx.rows().find(item => item.id === candidate.id); return current === undefined ? false : tx.transition(candidate.id, { type: 'expired', kind: 'source-changed' }, current.version).ok; });
|
|
497
|
+
return fail('approve', 'source-changed', '源技能参考在决策后变化,拒绝照跑', '重新 pair,或 /evolution merge discard ' + candidate.id, candidate.runId);
|
|
295
498
|
}
|
|
296
499
|
const currentRevision = sourceRevisionOf(cards, candidate.a, candidate.b);
|
|
297
500
|
if (candidate.sourceRevision !== undefined && currentRevision !== undefined && currentRevision !== candidate.sourceRevision) {
|
|
298
|
-
const
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
501
|
+
const recorded = await pairLedger(deps.io, deps.home).transact(tx => {
|
|
502
|
+
const current = tx.rows().find(item => item.id === candidate.id);
|
|
503
|
+
return current === undefined ? false : tx.transition(candidate.id, { type: 'expired', kind: 'source-changed' }, current.version).ok;
|
|
504
|
+
});
|
|
505
|
+
if (!recorded)
|
|
506
|
+
return fail('approve', 'row-changed', '记录 source-changed 时发现并发修改:' + candidate.id, '运行 /evolution merge status ' + candidate.id + ' 后重试');
|
|
507
|
+
return fail('approve', 'source-changed', '源技能内容在决策后变化,拒绝照跑', '重新 pair,或 /evolution merge discard ' + candidate.id, candidate.runId);
|
|
302
508
|
}
|
|
303
509
|
const currentTargetRevision = targetRevisionOf(cards, candidate.decision.target);
|
|
304
510
|
if (candidate.targetRevision !== undefined && currentTargetRevision !== undefined
|
|
305
511
|
&& currentTargetRevision !== candidate.targetRevision) {
|
|
306
|
-
const
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
512
|
+
const recorded = await pairLedger(deps.io, deps.home).transact(tx => {
|
|
513
|
+
const current = tx.rows().find(item => item.id === candidate.id);
|
|
514
|
+
return current === undefined ? false : tx.transition(candidate.id, { type: 'expired', kind: 'target-taken' }, current.version).ok;
|
|
515
|
+
});
|
|
516
|
+
if (!recorded)
|
|
517
|
+
return fail('approve', 'row-changed', '记录 target-taken 时发现并发修改:' + candidate.id, '运行 /evolution merge status ' + candidate.id + ' 后重试');
|
|
518
|
+
return fail('approve', 'target-taken', '目标 ' + candidate.decision.target + ' 在决策后变化,拒绝照跑', '重新 pair,或 /evolution merge discard ' + candidate.id, candidate.runId);
|
|
519
|
+
}
|
|
520
|
+
if (candidate.decision.mode !== 'create-umbrella') {
|
|
521
|
+
const targetCard = cards.find(card => card.name === candidate.decision?.target);
|
|
522
|
+
if (targetCard !== undefined && targetCard.managed !== true) {
|
|
523
|
+
return fail('approve', 'target-not-managed', 'target 不是 family-managed:' + candidate.decision.target, '重新 pair 并选一个 managed 目标', candidate.runId);
|
|
524
|
+
}
|
|
525
|
+
if (targetCard !== undefined && (targetCard.protectionUnknown === true || targetCard.protectedBy != null)) {
|
|
526
|
+
return fail('approve', 'target-protected', 'target 受保护:' + candidate.decision.target, '解除保护或改选其它目标', candidate.runId);
|
|
527
|
+
}
|
|
528
|
+
if (await deps.isBlocked?.(candidate.decision.target) === true) {
|
|
529
|
+
return fail('approve', 'target-blocked', 'target 被范围门拦下:' + candidate.decision.target, '运行 /evolution scope 查看范围门', candidate.runId);
|
|
530
|
+
}
|
|
310
531
|
}
|
|
311
532
|
if (candidate.decision.mode === 'merge-into-existing' && !cards.some(card => card.name === candidate.decision?.target)) {
|
|
312
|
-
const
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
533
|
+
const recorded = await pairLedger(deps.io, deps.home).transact(tx => {
|
|
534
|
+
const current = tx.rows().find(item => item.id === candidate.id);
|
|
535
|
+
return current === undefined ? false : tx.transition(candidate.id, { type: 'expired', kind: 'target-taken' }, current.version).ok;
|
|
536
|
+
});
|
|
537
|
+
if (!recorded)
|
|
538
|
+
return fail('approve', 'row-changed', '记录 target-taken 时发现并发修改:' + candidate.id, '运行 /evolution merge status ' + candidate.id + ' 后重试');
|
|
539
|
+
return fail('approve', 'target-taken', '目标 ' + candidate.decision.target + ' 已不存在,行保留 pending --expired', '重新 pair,或 /evolution merge discard ' + candidate.id, candidate.runId);
|
|
316
540
|
}
|
|
317
541
|
}
|
|
318
542
|
const result = await runApproveCommand({
|
|
319
543
|
...common,
|
|
320
544
|
rowId: command.id,
|
|
545
|
+
...(agentless ? { agentless: true } : {}),
|
|
546
|
+
...(detached ? { detached: true } : {}),
|
|
547
|
+
pairTimeoutMinutes: deps.params.mergePairTimeoutMinutes,
|
|
321
548
|
ports: {
|
|
549
|
+
stage: createStageForCards({ io: deps.io, home: deps.home, runId: command.id, cards, now: deps.now }),
|
|
322
550
|
round2: createNewChildRound2({
|
|
323
551
|
io: deps.io, home: deps.home, cards, channel: deps.channel, parent,
|
|
324
552
|
allow: deps.allow, ...(deps.cwd === undefined ? {} : { cwd: deps.cwd }),
|
|
325
553
|
timeoutMs: deps.params.mergePairTimeoutMinutes * 60_000,
|
|
326
|
-
commitPort: skillLibraryPort(deps.skills), now: deps.now,
|
|
554
|
+
commitPort: skillLibraryPort(deps.skills), now: deps.now, ...(unitFor(deps) === undefined ? {} : { unit: unitFor(deps) }),
|
|
555
|
+
...(deps.params.limitsSnapshot === undefined ? {} : { limits: deps.params.limitsSnapshot.skillLimits }),
|
|
556
|
+
...(deps.isBlocked === undefined ? {} : { isBlocked: deps.isBlocked }),
|
|
557
|
+
...stageHooks,
|
|
327
558
|
}),
|
|
328
559
|
},
|
|
329
560
|
});
|
|
330
561
|
if (!result.ok)
|
|
331
|
-
return
|
|
562
|
+
return failRun('approve', result.message, '修复后重新 approve;run 报告见 /evolution merge report', result);
|
|
332
563
|
if ('accepted' in result)
|
|
333
564
|
return { ok: true, text: 'merge approve 已启动:' + result.runId };
|
|
334
565
|
if (!('report' in result))
|
|
335
|
-
return
|
|
566
|
+
return fail('approve', 'report-missing', 'merge approve 已结束但没有写报告', '重新运行 approve,或运行 /evolution merge runs 查看运行');
|
|
336
567
|
return { ok: true, text: 'merge approve 完成:merged ' + String(result.report.totals.merged) };
|
|
337
568
|
}
|
|
338
569
|
const cards = command.verb === 'pending' ? await deps.cards(new AbortController().signal) : undefined;
|
|
339
|
-
const
|
|
570
|
+
const registry = deps.registry;
|
|
571
|
+
const runs = command.verb === 'status' ? registry.runs('merge') : undefined;
|
|
572
|
+
if (command.verb === 'status' && command.id !== undefined) {
|
|
573
|
+
const found = runs?.find(item => item.id === command.id) ?? registry.find(command.id);
|
|
574
|
+
if (found !== undefined && found.kind === 'merge') {
|
|
575
|
+
// CLI consumer of the RunDetail envelope: current pair, per-pair outcome, stop reason.
|
|
576
|
+
const snapshot = await readMergeSnapshot({ io: deps.io, home: deps.home, runs: runs !== undefined && runs.some(item => item.id === found.id) ? runs : [...(runs ?? []), found], approval: deps.params.mergeApproval, now: deps.now });
|
|
577
|
+
if (snapshot.blocked !== undefined)
|
|
578
|
+
return fail('status', snapshot.blocked.code, snapshot.blocked.message, '先修复合并状态(/evolution merge doctor),再查看运行');
|
|
579
|
+
const detail = buildRunDetail(snapshot, found.id);
|
|
580
|
+
if (detail === null)
|
|
581
|
+
return fail('status', 'run-not-found', '找不到 merge run ' + found.id, '运行 /evolution merge runs 查看运行列表');
|
|
582
|
+
return { ok: true, text: formatRunDetail(detail) };
|
|
583
|
+
}
|
|
584
|
+
const row = (await loadMergeState(deps.io, deps.home)).state.rows.find(item => item.id === command.id);
|
|
585
|
+
if (row === undefined)
|
|
586
|
+
return fail('status', 'row-not-found', '找不到合并行或 run:' + command.id, '运行 /evolution merge status 查看队列,或 /evolution merge runs 查看运行');
|
|
587
|
+
}
|
|
588
|
+
// FR-LIFE-14: CLI reads derive the same pending expiry the snapshot route does;
|
|
589
|
+
// the library read stays lazy (only invoked when a pending row needs derivation).
|
|
590
|
+
const liveForRead = async () => {
|
|
591
|
+
const list = cards ?? await deps.cards(new AbortController().signal).catch(() => []);
|
|
592
|
+
return list.length === 0 ? undefined : liveRowFactsOf(list);
|
|
593
|
+
};
|
|
594
|
+
const text = await handleReadOnlyMergeCommand(command, deps.io, deps.home, cards, runs, deps.params.mergeApproval, liveForRead);
|
|
340
595
|
if (text !== null)
|
|
341
596
|
return { ok: true, text };
|
|
342
597
|
return readOnly.execute(command);
|