@lmzhen/dsh-evolution-merge 0.30.18 → 0.30.20
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 -4
- package/data/minhash_params.json +1063 -0
- package/lib/index.js +7313 -3272
- 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/model-runner.d.ts +0 -5
- package/lib/types/adapters/model-runner.js +8 -2
- package/lib/types/adapters/model.js +2 -1
- package/lib/types/adapters/select.js +12 -8
- package/lib/types/approve-command.d.ts +12 -1
- package/lib/types/approve-command.js +39 -6
- package/lib/types/authoring-flow.d.ts +16 -0
- package/lib/types/authoring-flow.js +37 -13
- 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 +1 -1
- package/lib/types/command-surface.js +10 -43
- package/lib/types/commands.d.ts +3 -4
- package/lib/types/commands.js +33 -72
- package/lib/types/commit-assembly.d.ts +14 -1
- package/lib/types/commit-assembly.js +65 -9
- package/lib/types/commit-execute.d.ts +7 -1
- package/lib/types/commit-execute.js +54 -22
- 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/contracts.d.ts +43 -0
- package/lib/types/doctor.d.ts +11 -1
- package/lib/types/doctor.js +81 -27
- package/lib/types/domain/authoring-manifest.d.ts +26 -0
- package/lib/types/domain/authoring-manifest.js +43 -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/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/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-v4.d.ts +148 -0
- package/lib/types/domain/row-v4.js +159 -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 +1 -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 +3 -14
- package/lib/types/domain/state-v3.js +3 -79
- 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 +3 -1
- package/lib/types/facts.js +13 -16
- package/lib/types/index.d.ts +2 -0
- package/lib/types/index.js +181 -68
- package/lib/types/merge-command.d.ts +3 -1
- package/lib/types/merge-command.js +10 -6
- package/lib/types/merge-routes.d.ts +10 -5
- package/lib/types/merge-routes.js +72 -7
- package/lib/types/merge-tool.d.ts +1 -1
- package/lib/types/merge-tool.js +2 -2
- package/lib/types/metrics.js +1 -1
- package/lib/types/new-child-authoring.d.ts +2 -0
- package/lib/types/new-child-authoring.js +57 -10
- 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 +4 -4
- package/lib/types/prompts.d.ts +2 -4
- package/lib/types/prompts.js +21 -8
- package/lib/types/read-commands.d.ts +3 -3
- package/lib/types/read-commands.js +19 -28
- 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 +5 -1
- package/lib/types/resume-command.js +46 -13
- 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 +59 -11
- package/lib/types/run-shell.d.ts +16 -2
- package/lib/types/run-shell.js +49 -5
- package/lib/types/runtime.d.ts +0 -5
- package/lib/types/runtime.js +274 -89
- package/lib/types/scan-command.d.ts +1 -0
- package/lib/types/scan-command.js +2 -0
- package/lib/types/scan-ports.d.ts +18 -0
- package/lib/types/scan-ports.js +49 -23
- package/lib/types/scan-run.d.ts +17 -0
- package/lib/types/scan-run.js +143 -48
- 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 +5 -5
- package/lib/types/state.js +22 -69
- package/lib/types/status.d.ts +6 -1
- package/lib/types/status.js +47 -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/undo-operation.d.ts +24 -0
- package/lib/types/undo-operation.js +135 -0
- package/lib/types/wire.d.ts +22 -3
- package/package.json +6 -4
package/lib/types/runtime.js
CHANGED
|
@@ -3,33 +3,64 @@
|
|
|
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';
|
|
7
8
|
import { targetClaimUnitPort } from "./unit-claim.js";
|
|
8
9
|
import { canRetry, pairKey } from "./contracts.js";
|
|
9
10
|
import { skippedPairKeys } from "./skipped.js";
|
|
10
11
|
import { createReadOnlyMergeService } from "./merge-command.js";
|
|
11
|
-
import { formatRunsView } from "./status.js";
|
|
12
|
-
import {
|
|
12
|
+
import { formatRunDetail, formatRunsView } from "./status.js";
|
|
13
|
+
import { readMergeSnapshot } from "./domain/merge-snapshot.js";
|
|
14
|
+
import { buildRunDetail, buildRunsView } from "./surface/merge-view.js";
|
|
13
15
|
import { handleReadOnlyMergeCommand } from "./read-commands.js";
|
|
14
16
|
import { runScanCommand } from "./scan-command.js";
|
|
15
17
|
import { runApproveCommand } from "./approve-command.js";
|
|
16
18
|
import { runResumeCommand } from "./resume-command.js";
|
|
17
|
-
import {
|
|
19
|
+
import { loadMergeState } from "./state.js";
|
|
20
|
+
import { pairLedger } from "./domain/pair-ledger.js";
|
|
18
21
|
import { readCommitReceipt } from "./commit-receipt.js";
|
|
19
|
-
import {
|
|
20
|
-
import {
|
|
21
|
-
import { readStagingIndex } from "./domain/staging-index.js";
|
|
22
|
+
import { runUndoOperation } from "./undo-operation.js";
|
|
23
|
+
import { readStageIndex } from "./domain/stage-aggregate.js";
|
|
22
24
|
import { skillLibraryUndoPort } from "./adapters/undo.js";
|
|
23
25
|
import { mergeDoctor } from "./doctor.js";
|
|
24
|
-
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";
|
|
25
29
|
import { createNewChildRound2 } from "./new-child-authoring.js";
|
|
26
30
|
import { skillLibraryPort } from "./adapters/library.js";
|
|
27
31
|
import { fitToolText } from "./presentation-budget.js";
|
|
28
|
-
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
|
+
}
|
|
29
50
|
function forcedDecisionFor(a, b) {
|
|
30
51
|
const sorted = [a, b].sort();
|
|
31
|
-
const
|
|
32
|
-
|
|
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
|
+
});
|
|
33
64
|
}
|
|
34
65
|
export function unitFor(deps) {
|
|
35
66
|
if (deps.unit !== undefined)
|
|
@@ -55,42 +86,59 @@ export function createMergeRuntime(deps) {
|
|
|
55
86
|
return {
|
|
56
87
|
async execute(command, context) {
|
|
57
88
|
const parent = context?.parent ?? deps.parent;
|
|
89
|
+
const agentless = context?.agentless === true;
|
|
58
90
|
const detached = deps.detached !== false;
|
|
59
91
|
if (command.verb === 'pair') {
|
|
60
92
|
const signal = new AbortController().signal;
|
|
61
93
|
if (command.a === command.b)
|
|
62
|
-
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
|
+
}
|
|
63
107
|
const cards = await deps.cards(signal);
|
|
64
108
|
const cardA = cards.find(card => card.name === command.a);
|
|
65
109
|
const cardB = cards.find(card => card.name === command.b);
|
|
66
110
|
if (cardA === undefined || cardB === undefined || cardA.managed !== true || cardB.managed !== true) {
|
|
67
|
-
return
|
|
111
|
+
return fail('pair', 'pair-not-managed', 'merge pair 仅支持 family-managed 技能:' + command.a + ' / ' + command.b, '运行 /evolution doctor 查看 family-managed 技能范围');
|
|
68
112
|
}
|
|
69
113
|
if (cardA.protectedBy != null || cardB.protectedBy != null
|
|
70
114
|
|| cardA.protectionUnknown === true || cardB.protectionUnknown === true) {
|
|
71
|
-
return
|
|
115
|
+
return fail('pair', 'pair-protected', 'merge pair 不接受受保护/保护状态未知技能:' + command.a + ' / ' + command.b, '解除保护或换一对技能后重试');
|
|
72
116
|
}
|
|
73
117
|
if ((await deps.isBlocked?.(command.a)) === true || (await deps.isBlocked?.(command.b)) === true) {
|
|
74
|
-
return
|
|
118
|
+
return fail('pair', 'pair-blocked', 'merge pair 不接受被范围门拦下的技能:' + command.a + ' / ' + command.b, '运行 /evolution scope 查看范围门,或换一对技能');
|
|
75
119
|
}
|
|
76
120
|
const pair = [{ a: command.a, b: command.b }];
|
|
77
121
|
const existing = (await loadMergeState(deps.io, deps.home)).state.rows.find(row => row.pairKey === pairKey(command.a, command.b));
|
|
78
122
|
if (existing?.state === 'skipped') {
|
|
79
|
-
const dropped = await
|
|
123
|
+
const dropped = await removeRuntimeRow(deps.io, deps.home, existing.id, 'superseded');
|
|
80
124
|
if (!dropped)
|
|
81
|
-
return
|
|
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);
|
|
82
129
|
}
|
|
83
130
|
if (existing?.state === 'pending') {
|
|
84
|
-
return
|
|
131
|
+
return fail('pair', 'pair-pending', '该 pair 已在待批准队列:' + existing.id, '先运行 /evolution merge approve/reject/discard ' + existing.id, existing.runId);
|
|
85
132
|
}
|
|
86
133
|
if (existing?.state === 'approved' || existing?.state === 'fuse') {
|
|
87
|
-
return
|
|
134
|
+
return fail('pair', 'pair-fuse', '该 pair 已在待合并队列,可复用决策重跑', '运行 /evolution merge resume --retry ' + (existing.runId ?? existing.id), existing.runId);
|
|
88
135
|
}
|
|
89
136
|
const forced = command.force === true ? forcedDecisionFor(command.a, command.b) : undefined;
|
|
90
137
|
const approval = deps.params.mergeApproval;
|
|
91
138
|
const result = await runScanCommand({
|
|
92
139
|
...common,
|
|
93
140
|
approval,
|
|
141
|
+
cards,
|
|
94
142
|
...(detached ? { detached: true } : {}),
|
|
95
143
|
makePorts: (runId) => {
|
|
96
144
|
const ports = createChannelScanPorts({
|
|
@@ -101,6 +149,7 @@ export function createMergeRuntime(deps) {
|
|
|
101
149
|
timeoutMs: deps.params.mergePairTimeoutMinutes * 60_000,
|
|
102
150
|
approval,
|
|
103
151
|
commitPort: skillLibraryPort(deps.skills), now: deps.now, ...(unitFor(deps) === undefined ? {} : { unit: unitFor(deps) }),
|
|
152
|
+
...(deps.params.limitsSnapshot === undefined ? {} : { limits: deps.params.limitsSnapshot.skillLimits }),
|
|
104
153
|
...(deps.isBlocked === undefined ? {} : { isBlocked: deps.isBlocked }),
|
|
105
154
|
...stageHooks,
|
|
106
155
|
});
|
|
@@ -108,14 +157,16 @@ export function createMergeRuntime(deps) {
|
|
|
108
157
|
},
|
|
109
158
|
});
|
|
110
159
|
if (!result.ok)
|
|
111
|
-
return
|
|
160
|
+
return failRun('pair', result.message, '修复后重新发起 pair;run 报告见 /evolution merge report', result);
|
|
112
161
|
if ('accepted' in result)
|
|
113
162
|
return { ok: true, text: 'merge pair 已启动:' + result.runId };
|
|
114
163
|
if (!('report' in result))
|
|
115
|
-
return
|
|
164
|
+
return fail('pair', 'report-missing', 'merge pair 已结束但没有写报告', '重新运行 pair,或运行 /evolution merge runs 查看运行');
|
|
116
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) };
|
|
117
166
|
}
|
|
118
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');
|
|
119
170
|
const signal = new AbortController().signal;
|
|
120
171
|
const cards = await deps.cards(signal);
|
|
121
172
|
const blocked = new Set();
|
|
@@ -130,57 +181,128 @@ export function createMergeRuntime(deps) {
|
|
|
130
181
|
const held = new Set(loadedState.state.rows.filter(row => row.queue === 'pending' || row.queue === 'fuse'
|
|
131
182
|
|| (row.queue === 'verify' && (row.runId !== undefined && deps.now() - Date.parse(row.updatedAt) < 10 * 60_000 || !canRetry(row))))
|
|
132
183
|
.map(row => row.pairKey));
|
|
133
|
-
const
|
|
184
|
+
const notes = loadedState.state.rows
|
|
134
185
|
.filter(row => row.state === 'pending' || row.state === 'approved' || row.state === 'fuse' || row.state === 'skipped')
|
|
135
186
|
.map(row => 'conflict:' + row.state + ':' + row.pairKey + ':' + row.id);
|
|
136
187
|
const result = await runScanCommand({
|
|
137
188
|
...common,
|
|
138
189
|
approval: deps.params.mergeApproval,
|
|
139
|
-
notes
|
|
190
|
+
notes,
|
|
191
|
+
cards,
|
|
140
192
|
...(detached ? { detached: true } : {}),
|
|
141
193
|
makePorts: runId => createChannelScanPorts({
|
|
142
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
|
+
},
|
|
143
199
|
screening: async (turnSignal) => {
|
|
144
200
|
const raw = await deps.screeningForRun(cards, runId, turnSignal);
|
|
201
|
+
const degraded = raw.degraded;
|
|
202
|
+
if (degraded !== undefined)
|
|
203
|
+
notes.push(...degraded);
|
|
145
204
|
const filtered = raw.filter((candidate) => {
|
|
146
205
|
const key = pairKey(candidate.a, candidate.b);
|
|
147
206
|
return managed.has(candidate.a) && managed.has(candidate.b) && !skipped.has(key) && !held.has(key);
|
|
148
207
|
});
|
|
149
208
|
const candidatePairs = raw.candidatePairs;
|
|
150
|
-
|
|
209
|
+
const reportOnly = raw.reportOnly;
|
|
210
|
+
return Object.assign(filtered, {
|
|
211
|
+
...(candidatePairs === undefined ? {} : { candidatePairs }),
|
|
212
|
+
...(reportOnly === undefined ? {} : { reportOnly }),
|
|
213
|
+
});
|
|
151
214
|
},
|
|
152
215
|
channel: deps.channel, parent, allow: deps.allow,
|
|
153
216
|
...(deps.cwd === undefined ? {} : { cwd: deps.cwd }),
|
|
154
217
|
timeoutMs: deps.params.mergePairTimeoutMinutes * 60_000,
|
|
155
218
|
approval: deps.params.mergeApproval,
|
|
156
219
|
commitPort: skillLibraryPort(deps.skills), now: deps.now, ...(unitFor(deps) === undefined ? {} : { unit: unitFor(deps) }),
|
|
220
|
+
...(deps.params.limitsSnapshot === undefined ? {} : { limits: deps.params.limitsSnapshot.skillLimits }),
|
|
157
221
|
...(deps.isBlocked === undefined ? {} : { isBlocked: deps.isBlocked }),
|
|
158
222
|
...stageHooks,
|
|
159
223
|
}),
|
|
160
224
|
});
|
|
161
225
|
if (!result.ok)
|
|
162
|
-
return
|
|
226
|
+
return failRun('scan', result.message, '修复后重新 scan;run 报告见 /evolution merge report', result);
|
|
163
227
|
if ('accepted' in result)
|
|
164
228
|
return { ok: true, text: 'merge scan 已启动:' + result.runId };
|
|
165
229
|
if (!('report' in result))
|
|
166
|
-
return
|
|
230
|
+
return fail('scan', 'report-missing', 'merge scan 已结束但没有写报告', '重新运行 scan,或运行 /evolution merge runs 查看运行');
|
|
167
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) };
|
|
168
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
|
+
}
|
|
169
275
|
if (command.verb === 'reject') {
|
|
170
276
|
const loaded = await loadMergeState(deps.io, deps.home);
|
|
171
277
|
const row = loaded.state.rows.find(item => item.id === command.id);
|
|
172
278
|
if (row === undefined)
|
|
173
|
-
return
|
|
174
|
-
const changed = await
|
|
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 => {
|
|
175
281
|
const current = tx.rows().find(item => item.id === row.id);
|
|
176
282
|
if (current === undefined || current.state !== 'pending')
|
|
177
283
|
return false;
|
|
178
284
|
return tx.transition(row.id, { type: 'skipped', reason: 'rejected', origin: 'human' }, current.version).ok;
|
|
179
285
|
});
|
|
180
286
|
if (!changed)
|
|
181
|
-
return
|
|
287
|
+
return fail('reject', 'row-not-pending', '该行不是 pending,或已被并发修改:' + command.id, '运行 /evolution merge status ' + command.id + ' 后重试', row.runId);
|
|
182
288
|
return { ok: true, text: '已拒绝合并:' + command.id };
|
|
183
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
|
+
}
|
|
184
306
|
if (command.verb === 'doctor') {
|
|
185
307
|
const rows = await mergeDoctor(deps.io, deps.home, deps.params.mergeModelsRoot);
|
|
186
308
|
return { ok: true, text: rows.map(row => row.key + ' [' + row.status + '] ' + row.label + ':' + row.detail).join('\n') };
|
|
@@ -189,96 +311,104 @@ export function createMergeRuntime(deps) {
|
|
|
189
311
|
const registry = deps.registry;
|
|
190
312
|
if (command.verb === 'runs') {
|
|
191
313
|
const mergeRuns = registry.runs('merge');
|
|
192
|
-
|
|
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)) };
|
|
193
316
|
}
|
|
194
317
|
if (command.verb === 'cancel') {
|
|
195
318
|
if (command.id !== undefined) {
|
|
196
319
|
const cancelled = registry.cancel(command.id);
|
|
197
|
-
return { ok:
|
|
320
|
+
return cancelled ? { ok: true, text: '已请求取消 ' + command.id } : fail('cancel', 'run-not-found', '没有可取消的 run:' + command.id, '运行 /evolution merge runs 查看可取消的运行');
|
|
198
321
|
}
|
|
199
|
-
const
|
|
200
|
-
|
|
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 运行' };
|
|
201
328
|
}
|
|
202
329
|
const run = command.id === undefined ? registry.runs('merge')[0] : registry.find(command.id);
|
|
203
330
|
if (run === undefined)
|
|
204
|
-
return
|
|
331
|
+
return fail('report', 'run-not-found', '找不到 merge run:' + (command.id ?? '(latest)'), '运行 /evolution merge runs 查看运行列表');
|
|
205
332
|
if (run.resultRef === undefined)
|
|
206
|
-
return
|
|
333
|
+
return fail('report', 'report-missing', '该 run 还没有报告:' + run.id, '等待 run 结束,或运行 /evolution merge status ' + run.id, run.id);
|
|
207
334
|
const report = await probeText(deps.io, run.resultRef);
|
|
208
335
|
if (report.kind === 'unknown')
|
|
209
|
-
return
|
|
336
|
+
return fail('report', 'report-unreadable', '报告不可读:' + run.resultRef + '(' + report.reason + ')', '检查结果文件权限,或重新运行该命令', run.id);
|
|
210
337
|
return report.kind === 'absent'
|
|
211
|
-
?
|
|
338
|
+
? fail('report', 'report-missing', '报告不存在:' + run.resultRef, '重新运行该命令生成报告', run.id)
|
|
212
339
|
: { ok: true, text: 'report: ' + run.resultRef + '\n' + fitToolText(report.value, 7600) };
|
|
213
340
|
}
|
|
214
341
|
if (command.verb === 'discard') {
|
|
215
342
|
const loaded = await loadMergeState(deps.io, deps.home);
|
|
216
343
|
const row = loaded.state.rows.find(item => item.id === command.id);
|
|
217
344
|
if (row === undefined)
|
|
218
|
-
return
|
|
345
|
+
return fail('discard', 'row-not-found', '找不到合并行 ' + command.id, '运行 /evolution merge status 查看当前队列', command.id);
|
|
219
346
|
if (row.state !== 'pending' && row.state !== 'approved' && row.state !== 'fuse')
|
|
220
|
-
return
|
|
221
|
-
const dropped = 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');
|
|
222
349
|
if (!dropped)
|
|
223
|
-
return
|
|
350
|
+
return fail('discard', 'row-changed', '丢弃时发现并发修改:' + command.id, '运行 /evolution merge status ' + command.id + ' 后重试');
|
|
224
351
|
return { ok: true, text: '已丢弃合并行:' + command.id };
|
|
225
352
|
}
|
|
226
353
|
if (command.verb === 'undo') {
|
|
227
354
|
const receipt = await readCommitReceipt(deps.io, deps.home, command.run, command.target);
|
|
228
355
|
if (receipt === null)
|
|
229
|
-
return
|
|
356
|
+
return fail('undo', 'receipt-not-found', '该 run 没有提交回执:' + command.run, '只有已提交的 run 可 undo;运行 /evolution merge runs 查看');
|
|
230
357
|
if (receipt.target !== command.target)
|
|
231
|
-
return
|
|
232
|
-
const
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
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, '检查回执与暂存后重试');
|
|
237
366
|
}
|
|
238
367
|
if (command.verb === 'skipped' && command.restore !== undefined) {
|
|
239
368
|
const loaded = await loadMergeState(deps.io, deps.home);
|
|
240
369
|
const row = loaded.state.rows.find(item => item.id === command.restore);
|
|
241
370
|
if (row === undefined || row.state !== 'skipped')
|
|
242
|
-
return
|
|
243
|
-
const dropped = 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');
|
|
244
373
|
if (!dropped)
|
|
245
|
-
return
|
|
374
|
+
return fail('skipped', 'row-changed', '恢复时发现并发修改:' + row.id, '运行 /evolution merge status 后重试');
|
|
246
375
|
return { ok: true, text: '已恢复参与筛选:' + row.id };
|
|
247
376
|
}
|
|
248
377
|
if (command.verb === 'resume') {
|
|
249
378
|
if (command.clear !== undefined) {
|
|
250
379
|
const cleared = (await loadMergeState(deps.io, deps.home)).state.rows.find(item => item.id === command.clear);
|
|
251
380
|
if (cleared === undefined)
|
|
252
|
-
return
|
|
253
|
-
const dropped = 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');
|
|
254
383
|
if (!dropped)
|
|
255
|
-
return
|
|
384
|
+
return fail('resume', 'row-changed', '清除时发现并发修改:' + cleared.id, '运行 /evolution merge status 后重试');
|
|
256
385
|
return { ok: true, text: '已清除合并行:' + cleared.id };
|
|
257
386
|
}
|
|
258
387
|
const loaded = await loadMergeState(deps.io, deps.home);
|
|
259
|
-
let row = command.id === undefined ? undefined : loaded.state.rows.find(item => item.id === command.id);
|
|
260
388
|
if (command.id === undefined) {
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
continue;
|
|
264
|
-
if (item.runId !== undefined && (await deps.isOwnerActive?.(item.runId)) === true)
|
|
265
|
-
continue;
|
|
266
|
-
row = item;
|
|
267
|
-
break;
|
|
268
|
-
}
|
|
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> 复用某一行的决策');
|
|
269
391
|
}
|
|
392
|
+
const row = loaded.state.rows.find(item => item.id === command.id);
|
|
270
393
|
if (row === undefined)
|
|
271
|
-
return
|
|
394
|
+
return fail('resume', 'row-not-found', '找不到可恢复行 ' + command.id, '运行 /evolution merge candidates 查看可恢复行');
|
|
272
395
|
const signal = new AbortController().signal;
|
|
273
396
|
const cards = await deps.cards(signal);
|
|
274
397
|
if ((row.state === 'fuse' || row.state === 'approved') && row.decision !== undefined && row.decision.mode !== 'create-umbrella') {
|
|
275
398
|
const targetCard = cards.find(card => card.name === row.decision?.target);
|
|
276
399
|
if (targetCard !== undefined && targetCard.managed !== true)
|
|
277
|
-
return
|
|
400
|
+
return fail('resume', 'target-not-managed', 'target 不是 family-managed:' + row.decision.target, '重新 pair 并选一个 managed 目标', row.runId);
|
|
278
401
|
if (targetCard !== undefined && (targetCard.protectionUnknown === true || targetCard.protectedBy != null))
|
|
279
|
-
return
|
|
402
|
+
return fail('resume', 'target-protected', 'target 受保护:' + row.decision.target, '解除保护或改选其它目标', row.runId);
|
|
280
403
|
if (await deps.isBlocked?.(row.decision.target) === true)
|
|
281
|
-
return
|
|
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);
|
|
282
412
|
}
|
|
283
413
|
const pair = () => Promise.resolve([{ a: row.a, b: row.b }]);
|
|
284
414
|
const failed = (stage, code, message) => Promise.resolve({
|
|
@@ -291,7 +421,8 @@ export function createMergeRuntime(deps) {
|
|
|
291
421
|
round2: () => failed('author', 'resume-needs-child', 'fuse resume needs a new child'),
|
|
292
422
|
};
|
|
293
423
|
const result = await runResumeCommand({
|
|
294
|
-
...common, rowId: row.id, ports: basePorts,
|
|
424
|
+
...common, rowId: row.id, ports: basePorts, approval: deps.params.mergeApproval,
|
|
425
|
+
...(agentless ? { agentless: true } : {}),
|
|
295
426
|
...(detached ? { detached: true } : {}),
|
|
296
427
|
pairTimeoutMinutes: deps.params.mergePairTimeoutMinutes,
|
|
297
428
|
...(deps.isOwnerActive === undefined ? {} : { isOwnerActive: deps.isOwnerActive }),
|
|
@@ -301,6 +432,7 @@ export function createMergeRuntime(deps) {
|
|
|
301
432
|
...(deps.cwd === undefined ? {} : { cwd: deps.cwd }),
|
|
302
433
|
timeoutMs: deps.params.mergePairTimeoutMinutes * 60_000,
|
|
303
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 }),
|
|
304
436
|
...(deps.isBlocked === undefined ? {} : { isBlocked: deps.isBlocked }),
|
|
305
437
|
}),
|
|
306
438
|
round2WithRun: createNewChildRound2({
|
|
@@ -308,16 +440,17 @@ export function createMergeRuntime(deps) {
|
|
|
308
440
|
allow: deps.allow, ...(deps.cwd === undefined ? {} : { cwd: deps.cwd }),
|
|
309
441
|
timeoutMs: deps.params.mergePairTimeoutMinutes * 60_000,
|
|
310
442
|
commitPort: skillLibraryPort(deps.skills), now: deps.now, ...(unitFor(deps) === undefined ? {} : { unit: unitFor(deps) }),
|
|
443
|
+
...(deps.params.limitsSnapshot === undefined ? {} : { limits: deps.params.limitsSnapshot.skillLimits }),
|
|
311
444
|
...(deps.isBlocked === undefined ? {} : { isBlocked: deps.isBlocked }),
|
|
312
445
|
...stageHooks,
|
|
313
446
|
}),
|
|
314
447
|
});
|
|
315
448
|
if (!result.ok)
|
|
316
|
-
return
|
|
449
|
+
return failRun('resume', result.message, '修复后重新 resume;run 报告见 /evolution merge report', result);
|
|
317
450
|
if ('accepted' in result)
|
|
318
451
|
return { ok: true, text: 'merge resume 已启动:' + result.runId };
|
|
319
452
|
if (!('report' in result))
|
|
320
|
-
return
|
|
453
|
+
return fail('resume', 'report-missing', 'merge resume 已结束但没有写报告', '重新运行 resume,或运行 /evolution merge runs 查看运行');
|
|
321
454
|
return { ok: true, text: 'merge resume 完成:merged ' + String(result.report.totals.merged) };
|
|
322
455
|
}
|
|
323
456
|
if (command.verb === 'approve') {
|
|
@@ -325,88 +458,140 @@ export function createMergeRuntime(deps) {
|
|
|
325
458
|
const cards = await deps.cards(signal);
|
|
326
459
|
let candidate = (await loadMergeState(deps.io, deps.home)).state.rows.find(row => row.id === command.id);
|
|
327
460
|
if (candidate !== undefined && candidate.stagingRef === undefined) {
|
|
328
|
-
const entry = await
|
|
461
|
+
const entry = await readStageIndex(deps.io, deps.home, candidate.id, deps.now());
|
|
329
462
|
if (entry !== null)
|
|
330
463
|
candidate = { ...candidate, stagingRef: entry.ref, stagingExpiresAt: entry.expiresAt };
|
|
331
464
|
}
|
|
332
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
|
+
}
|
|
333
477
|
const missingSource = [candidate.a, candidate.b].find(name => !cards.some(card => card.name === name && card.managed === true));
|
|
334
478
|
if (missingSource !== undefined) {
|
|
335
|
-
const dropped = await
|
|
479
|
+
const dropped = await removeRuntimeRow(deps.io, deps.home, candidate.id, 'superseded');
|
|
336
480
|
if (!dropped)
|
|
337
|
-
return
|
|
338
|
-
return
|
|
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);
|
|
339
498
|
}
|
|
340
499
|
const currentRevision = sourceRevisionOf(cards, candidate.a, candidate.b);
|
|
341
500
|
if (candidate.sourceRevision !== undefined && currentRevision !== undefined && currentRevision !== candidate.sourceRevision) {
|
|
342
|
-
const recorded = await
|
|
501
|
+
const recorded = await pairLedger(deps.io, deps.home).transact(tx => {
|
|
343
502
|
const current = tx.rows().find(item => item.id === candidate.id);
|
|
344
503
|
return current === undefined ? false : tx.transition(candidate.id, { type: 'expired', kind: 'source-changed' }, current.version).ok;
|
|
345
504
|
});
|
|
346
505
|
if (!recorded)
|
|
347
|
-
return
|
|
348
|
-
return
|
|
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);
|
|
349
508
|
}
|
|
350
509
|
const currentTargetRevision = targetRevisionOf(cards, candidate.decision.target);
|
|
351
510
|
if (candidate.targetRevision !== undefined && currentTargetRevision !== undefined
|
|
352
511
|
&& currentTargetRevision !== candidate.targetRevision) {
|
|
353
|
-
const recorded = await
|
|
512
|
+
const recorded = await pairLedger(deps.io, deps.home).transact(tx => {
|
|
354
513
|
const current = tx.rows().find(item => item.id === candidate.id);
|
|
355
514
|
return current === undefined ? false : tx.transition(candidate.id, { type: 'expired', kind: 'target-taken' }, current.version).ok;
|
|
356
515
|
});
|
|
357
516
|
if (!recorded)
|
|
358
|
-
return
|
|
359
|
-
return
|
|
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);
|
|
360
519
|
}
|
|
361
520
|
if (candidate.decision.mode !== 'create-umbrella') {
|
|
362
521
|
const targetCard = cards.find(card => card.name === candidate.decision?.target);
|
|
363
522
|
if (targetCard !== undefined && targetCard.managed !== true) {
|
|
364
|
-
return
|
|
523
|
+
return fail('approve', 'target-not-managed', 'target 不是 family-managed:' + candidate.decision.target, '重新 pair 并选一个 managed 目标', candidate.runId);
|
|
365
524
|
}
|
|
366
525
|
if (targetCard !== undefined && (targetCard.protectionUnknown === true || targetCard.protectedBy != null)) {
|
|
367
|
-
return
|
|
526
|
+
return fail('approve', 'target-protected', 'target 受保护:' + candidate.decision.target, '解除保护或改选其它目标', candidate.runId);
|
|
368
527
|
}
|
|
369
528
|
if (await deps.isBlocked?.(candidate.decision.target) === true) {
|
|
370
|
-
return
|
|
529
|
+
return fail('approve', 'target-blocked', 'target 被范围门拦下:' + candidate.decision.target, '运行 /evolution scope 查看范围门', candidate.runId);
|
|
371
530
|
}
|
|
372
531
|
}
|
|
373
532
|
if (candidate.decision.mode === 'merge-into-existing' && !cards.some(card => card.name === candidate.decision?.target)) {
|
|
374
|
-
const recorded = await
|
|
533
|
+
const recorded = await pairLedger(deps.io, deps.home).transact(tx => {
|
|
375
534
|
const current = tx.rows().find(item => item.id === candidate.id);
|
|
376
535
|
return current === undefined ? false : tx.transition(candidate.id, { type: 'expired', kind: 'target-taken' }, current.version).ok;
|
|
377
536
|
});
|
|
378
537
|
if (!recorded)
|
|
379
|
-
return
|
|
380
|
-
return
|
|
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);
|
|
381
540
|
}
|
|
382
541
|
}
|
|
383
542
|
const result = await runApproveCommand({
|
|
384
543
|
...common,
|
|
385
544
|
rowId: command.id,
|
|
545
|
+
...(agentless ? { agentless: true } : {}),
|
|
386
546
|
...(detached ? { detached: true } : {}),
|
|
387
547
|
pairTimeoutMinutes: deps.params.mergePairTimeoutMinutes,
|
|
388
548
|
ports: {
|
|
549
|
+
stage: createStageForCards({ io: deps.io, home: deps.home, runId: command.id, cards, now: deps.now }),
|
|
389
550
|
round2: createNewChildRound2({
|
|
390
551
|
io: deps.io, home: deps.home, cards, channel: deps.channel, parent,
|
|
391
552
|
allow: deps.allow, ...(deps.cwd === undefined ? {} : { cwd: deps.cwd }),
|
|
392
553
|
timeoutMs: deps.params.mergePairTimeoutMinutes * 60_000,
|
|
393
554
|
commitPort: skillLibraryPort(deps.skills), now: deps.now, ...(unitFor(deps) === undefined ? {} : { unit: unitFor(deps) }),
|
|
555
|
+
...(deps.params.limitsSnapshot === undefined ? {} : { limits: deps.params.limitsSnapshot.skillLimits }),
|
|
394
556
|
...(deps.isBlocked === undefined ? {} : { isBlocked: deps.isBlocked }),
|
|
557
|
+
...stageHooks,
|
|
395
558
|
}),
|
|
396
559
|
},
|
|
397
560
|
});
|
|
398
561
|
if (!result.ok)
|
|
399
|
-
return
|
|
562
|
+
return failRun('approve', result.message, '修复后重新 approve;run 报告见 /evolution merge report', result);
|
|
400
563
|
if ('accepted' in result)
|
|
401
564
|
return { ok: true, text: 'merge approve 已启动:' + result.runId };
|
|
402
565
|
if (!('report' in result))
|
|
403
|
-
return
|
|
566
|
+
return fail('approve', 'report-missing', 'merge approve 已结束但没有写报告', '重新运行 approve,或运行 /evolution merge runs 查看运行');
|
|
404
567
|
return { ok: true, text: 'merge approve 完成:merged ' + String(result.report.totals.merged) };
|
|
405
568
|
}
|
|
406
569
|
const cards = command.verb === 'pending' ? await deps.cards(new AbortController().signal) : undefined;
|
|
407
570
|
const registry = deps.registry;
|
|
408
571
|
const runs = command.verb === 'status' ? registry.runs('merge') : undefined;
|
|
409
|
-
|
|
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);
|
|
410
595
|
if (text !== null)
|
|
411
596
|
return { ok: true, text };
|
|
412
597
|
return readOnly.execute(command);
|