@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.
Files changed (143) hide show
  1. package/README.md +21 -4
  2. package/data/minhash_params.json +1063 -0
  3. package/lib/index.js +7313 -3272
  4. package/lib/types/adapters/channel.d.ts +1 -0
  5. package/lib/types/adapters/channel.js +15 -1
  6. package/lib/types/adapters/engine-screen.d.ts +1 -1
  7. package/lib/types/adapters/engine-screen.js +5 -2
  8. package/lib/types/adapters/model-runner.d.ts +0 -5
  9. package/lib/types/adapters/model-runner.js +8 -2
  10. package/lib/types/adapters/model.js +2 -1
  11. package/lib/types/adapters/select.js +12 -8
  12. package/lib/types/approve-command.d.ts +12 -1
  13. package/lib/types/approve-command.js +39 -6
  14. package/lib/types/authoring-flow.d.ts +16 -0
  15. package/lib/types/authoring-flow.js +37 -13
  16. package/lib/types/channel-port.d.ts +3 -1
  17. package/lib/types/channel-port.js +8 -3
  18. package/lib/types/command-defs.d.ts +5 -0
  19. package/lib/types/command-defs.js +33 -0
  20. package/lib/types/command-surface.d.ts +1 -1
  21. package/lib/types/command-surface.js +10 -43
  22. package/lib/types/commands.d.ts +3 -4
  23. package/lib/types/commands.js +33 -72
  24. package/lib/types/commit-assembly.d.ts +14 -1
  25. package/lib/types/commit-assembly.js +65 -9
  26. package/lib/types/commit-execute.d.ts +7 -1
  27. package/lib/types/commit-execute.js +54 -22
  28. package/lib/types/commit-plan.d.ts +7 -1
  29. package/lib/types/commit-plan.js +4 -0
  30. package/lib/types/commit-receipt.d.ts +3 -0
  31. package/lib/types/contracts.d.ts +43 -0
  32. package/lib/types/doctor.d.ts +11 -1
  33. package/lib/types/doctor.js +81 -27
  34. package/lib/types/domain/authoring-manifest.d.ts +26 -0
  35. package/lib/types/domain/authoring-manifest.js +43 -0
  36. package/lib/types/domain/engine-criteria.d.ts +10 -0
  37. package/lib/types/domain/engine-criteria.js +11 -0
  38. package/lib/types/domain/engine-input.d.ts +10 -0
  39. package/lib/types/domain/engine-input.js +26 -0
  40. package/lib/types/domain/errors.d.ts +15 -1
  41. package/lib/types/domain/errors.js +23 -4
  42. package/lib/types/domain/evidence-hash.d.ts +3 -0
  43. package/lib/types/domain/evidence-hash.js +14 -0
  44. package/lib/types/domain/failure-route.d.ts +14 -0
  45. package/lib/types/domain/failure-route.js +12 -0
  46. package/lib/types/domain/live-facts.d.ts +31 -0
  47. package/lib/types/domain/live-facts.js +54 -0
  48. package/lib/types/domain/merge-limits.d.ts +18 -0
  49. package/lib/types/domain/merge-limits.js +22 -0
  50. package/lib/types/domain/merge-report.d.ts +41 -0
  51. package/lib/types/domain/merge-report.js +2 -0
  52. package/lib/types/domain/merge-snapshot.d.ts +99 -0
  53. package/lib/types/domain/merge-snapshot.js +82 -0
  54. package/lib/types/domain/pair-ledger.d.ts +34 -0
  55. package/lib/types/domain/pair-ledger.js +96 -0
  56. package/lib/types/domain/pre-1b-backup.d.ts +43 -0
  57. package/lib/types/domain/pre-1b-backup.js +140 -0
  58. package/lib/types/domain/provider-key.d.ts +2 -0
  59. package/lib/types/domain/provider-key.js +11 -0
  60. package/lib/types/domain/read-probe.d.ts +5 -0
  61. package/lib/types/domain/read-probe.js +11 -0
  62. package/lib/types/domain/reconcile.d.ts +47 -0
  63. package/lib/types/domain/reconcile.js +178 -0
  64. package/lib/types/domain/resolve-pair.d.ts +2 -9
  65. package/lib/types/domain/resolve-pair.js +2 -15
  66. package/lib/types/domain/row-v4.d.ts +148 -0
  67. package/lib/types/domain/row-v4.js +159 -0
  68. package/lib/types/domain/run-coordinator.d.ts +51 -0
  69. package/lib/types/domain/run-coordinator.js +45 -0
  70. package/lib/types/domain/schema4.d.ts +53 -0
  71. package/lib/types/domain/schema4.js +129 -0
  72. package/lib/types/domain/selection-policy.d.ts +14 -0
  73. package/lib/types/domain/selection-policy.js +6 -0
  74. package/lib/types/domain/skipped-expiry.d.ts +5 -0
  75. package/lib/types/domain/skipped-expiry.js +16 -0
  76. package/lib/types/domain/stage-aggregate.d.ts +96 -0
  77. package/lib/types/domain/stage-aggregate.js +293 -0
  78. package/lib/types/domain/staging-index.d.ts +1 -0
  79. package/lib/types/domain/state-bootstrap.d.ts +43 -0
  80. package/lib/types/domain/state-bootstrap.js +267 -0
  81. package/lib/types/domain/state-v3.d.ts +3 -14
  82. package/lib/types/domain/state-v3.js +3 -79
  83. package/lib/types/domain/undo-journal.d.ts +19 -0
  84. package/lib/types/domain/undo-journal.js +20 -0
  85. package/lib/types/domain/undo-receipt.d.ts +18 -0
  86. package/lib/types/domain/undo-receipt.js +22 -0
  87. package/lib/types/domain/validation-plan.d.ts +65 -0
  88. package/lib/types/domain/validation-plan.js +49 -0
  89. package/lib/types/facts.d.ts +3 -1
  90. package/lib/types/facts.js +13 -16
  91. package/lib/types/index.d.ts +2 -0
  92. package/lib/types/index.js +181 -68
  93. package/lib/types/merge-command.d.ts +3 -1
  94. package/lib/types/merge-command.js +10 -6
  95. package/lib/types/merge-routes.d.ts +10 -5
  96. package/lib/types/merge-routes.js +72 -7
  97. package/lib/types/merge-tool.d.ts +1 -1
  98. package/lib/types/merge-tool.js +2 -2
  99. package/lib/types/metrics.js +1 -1
  100. package/lib/types/new-child-authoring.d.ts +2 -0
  101. package/lib/types/new-child-authoring.js +57 -10
  102. package/lib/types/params.d.ts +2 -4
  103. package/lib/types/params.js +13 -0
  104. package/lib/types/pending-actions.d.ts +2 -2
  105. package/lib/types/pending-actions.js +4 -4
  106. package/lib/types/prompts.d.ts +2 -4
  107. package/lib/types/prompts.js +21 -8
  108. package/lib/types/read-commands.d.ts +3 -3
  109. package/lib/types/read-commands.js +19 -28
  110. package/lib/types/recovery-coordinator.d.ts +34 -0
  111. package/lib/types/recovery-coordinator.js +84 -0
  112. package/lib/types/recovery-projection.d.ts +31 -0
  113. package/lib/types/recovery-projection.js +96 -0
  114. package/lib/types/resume-command.d.ts +5 -1
  115. package/lib/types/resume-command.js +46 -13
  116. package/lib/types/revision.d.ts +17 -0
  117. package/lib/types/revision.js +39 -3
  118. package/lib/types/round2-outcome.d.ts +2 -1
  119. package/lib/types/round2-outcome.js +59 -11
  120. package/lib/types/run-shell.d.ts +16 -2
  121. package/lib/types/run-shell.js +49 -5
  122. package/lib/types/runtime.d.ts +0 -5
  123. package/lib/types/runtime.js +274 -89
  124. package/lib/types/scan-command.d.ts +1 -0
  125. package/lib/types/scan-command.js +2 -0
  126. package/lib/types/scan-ports.d.ts +18 -0
  127. package/lib/types/scan-ports.js +49 -23
  128. package/lib/types/scan-run.d.ts +17 -0
  129. package/lib/types/scan-run.js +143 -48
  130. package/lib/types/stage-tool-registry.d.ts +7 -0
  131. package/lib/types/stage-tool-registry.js +12 -1
  132. package/lib/types/state.d.ts +5 -5
  133. package/lib/types/state.js +22 -69
  134. package/lib/types/status.d.ts +6 -1
  135. package/lib/types/status.js +47 -0
  136. package/lib/types/surface/body.d.ts +4 -0
  137. package/lib/types/surface/body.js +16 -0
  138. package/lib/types/surface/merge-view.d.ts +14 -0
  139. package/lib/types/surface/merge-view.js +126 -0
  140. package/lib/types/undo-operation.d.ts +24 -0
  141. package/lib/types/undo-operation.js +135 -0
  142. package/lib/types/wire.d.ts +22 -3
  143. package/package.json +6 -4
@@ -1,5 +1,6 @@
1
- import { loadMergeState } from "./state.js";
2
1
  import { MERGE_COMMAND_SURFACE } from "./command-surface.js";
2
+ import { readMergeSnapshot } from "./domain/merge-snapshot.js";
3
+ import { buildFacts as buildSnapshotFacts } from "./surface/merge-view.js";
3
4
  const queueOf = (row) => row.state === 'approved' ? 'fuse' : row.queue;
4
5
  const isExpired = (row) => row.expired !== undefined;
5
6
  export function countsOf(rows) {
@@ -14,8 +15,9 @@ export function countsOf(rows) {
14
15
  }
15
16
  return { active, expired };
16
17
  }
17
- export function toRunView(record, readAt) {
18
- const state = (record.mergeStatus ?? record.state);
18
+ export function toRunView(record, readAt, meta = null) {
19
+ const state = record.state;
20
+ void meta;
19
21
  return {
20
22
  wireVersion: 1,
21
23
  readAt,
@@ -28,7 +30,6 @@ export function toRunView(record, readAt) {
28
30
  ...(record.endedAt === undefined ? {} : { endedAt: record.endedAt }),
29
31
  ...(record.failure === undefined ? {} : { failure: { stage: 'settle', code: 'run-failed', retryable: false, retryFrom: 'none', detail: record.failure } }),
30
32
  ...(record.parentRunId === undefined ? {} : { parentRunId: record.parentRunId }),
31
- ...(record.pairKey === undefined ? {} : { currentPairKey: record.pairKey }),
32
33
  actions: [
33
34
  { id: 'status', command: '/evolution merge status ' + record.id, confirm: false, enabled: true },
34
35
  { id: 'report', command: '/evolution merge report ' + record.id, confirm: false, enabled: record.resultRef !== undefined },
@@ -38,27 +39,23 @@ export function toRunView(record, readAt) {
38
39
  }
39
40
  /** GET projection: reads state + runs; never writes. */
40
41
  export async function readFacts(options) {
41
- const loaded = await loadMergeState(options.io, options.home);
42
- return buildFacts({
43
- rows: loaded.state.rows,
44
- runs: options.runs,
45
- approval: options.approval,
46
- readAt: options.readAt,
47
- notes: loaded.ok ? [] : [loaded.note ?? 'state-unreadable'],
48
- });
42
+ const snapshot = await readMergeSnapshot({ io: options.io, home: options.home, runs: options.runs, approval: options.approval, now: () => options.readAt });
43
+ if (snapshot.blocked !== undefined)
44
+ throw new Error(snapshot.blocked.code + ': ' + snapshot.blocked.message);
45
+ return buildSnapshotFacts(snapshot);
49
46
  }
50
47
  export function buildFacts(input) {
51
48
  const ordered = [...input.runs].sort((a, b) => b.startedAt - a.startedAt);
52
- const active = ordered.find(run => (run.mergeStatus ?? run.state) === 'running');
53
- const last = ordered.find(run => (run.mergeStatus ?? run.state) !== 'running');
49
+ const active = ordered.find(run => run.state === 'running');
50
+ const last = ordered.find(run => run.state !== 'running');
54
51
  const notes = input.notes ?? [];
55
52
  return {
56
53
  wireVersion: 1,
57
54
  readAt: input.readAt,
58
55
  approval: { mergeApproval: input.approval },
59
56
  counts: countsOf(input.rows),
60
- active: active === undefined ? null : toRunView(active, input.readAt),
61
- last: last === undefined ? null : toRunView(last, input.readAt),
57
+ active: active === undefined ? null : toRunView(active, input.readAt, input.runMeta?.get(active.id) ?? null),
58
+ last: last === undefined ? null : toRunView(last, input.readAt, input.runMeta?.get(last.id) ?? null),
62
59
  startCommands: { scan: '/evolution merge scan', pair: '/evolution merge pair <a> <b>' },
63
60
  fallback: MERGE_COMMAND_SURFACE.fallback,
64
61
  notes,
@@ -70,6 +70,8 @@ export * from './merge-tool.ts';
70
70
  export * from './commands.ts';
71
71
  export * from './scan-ports.ts';
72
72
  export * from './new-child-authoring.ts';
73
+ export * from './domain/authoring-manifest.ts';
74
+ export * from './domain/live-facts.ts';
73
75
  export * from './status.ts';
74
76
  export * from './skipped.ts';
75
77
  export * from './read-commands.ts';
@@ -4,12 +4,29 @@ import { screen as engineScreen } from '@lmzhen/dsh-evolution-merge-engine';
4
4
  import { selectWithErrors } from "./adapters/select.js";
5
5
  import { createEngineScreening } from "./adapters/engine-screen.js";
6
6
  import { createEngineRunner } from "./adapters/model-runner.js";
7
- import { authorityFrom, createSubagentChannel, DEFAULT_SKILL_LIMITS, evolutionHome, evolutionIoAdapter, newRunRegistry, newSkillLibrary, policyStageLimits } from '@lmzhen/dsh-evolution-core';
7
+ import { authorityFrom, createSubagentChannel, evolutionHome, evolutionIoAdapter, newRunRegistry, newSkillLibrary, reportsSweepLockTarget, sweepReports, transactIo } from '@lmzhen/dsh-evolution-core';
8
8
  import { registerHostRoutes } from '@lmzhen/dsh-evolution-core/http-routes';
9
- import { loadMergeState, transactRows } from "./state.js";
10
- import { cleanupExpiredStaging } from "./staging.js";
11
- import { recoverCommitJournals } from "./commit-recovery.js";
9
+ import { loadMergeState } from "./state.js";
10
+ import { pairLedger, normalizePairLedger } from "./domain/pair-ledger.js";
11
+ import { legacyToV3 } from "./domain/row-v3-bridge.js";
12
+ import { readV4Readiness } from "./domain/schema4.js";
13
+ import { readMergeSnapshot } from "./domain/merge-snapshot.js";
12
14
  import { readCommitReceipts } from "./commit-receipt.js";
15
+ import { UnitTransactions, skillLibraryUnitBackend } from '@lmzhen/dsh-evolution-core';
16
+ import { targetClaimUnitPort } from "./unit-claim.js";
17
+ import { resolveLimitsSnapshot } from "./domain/merge-limits.js";
18
+ import { resolveEngineInput } from "./domain/engine-input.js";
19
+ import { readReconcileReport, reconcileMergeState } from "./domain/reconcile.js";
20
+ import { readRunMeta, writeRunMeta } from "./domain/run-coordinator.js";
21
+ import { liveRowFactsOf } from "./domain/live-facts.js";
22
+ import { filterSelection } from "./domain/selection-policy.js";
23
+ import { cleanupStage } from "./domain/stage-aggregate.js";
24
+ import { buildDoctorView } from "./surface/merge-view.js";
25
+ import { mergeDoctorFact } from "./doctor.js";
26
+ import { commandError, formatMergeError } from "./domain/errors.js";
27
+ import { resolveBody } from "./surface/body.js";
28
+ import { ensureMergeStateReady } from "./domain/state-bootstrap.js";
29
+ import { recoverCommitUnits } from "./recovery-coordinator.js";
13
30
  import { pairKey, rowIdOf } from "./contracts.js";
14
31
  import { makeMergeRoutes } from "./merge-routes.js";
15
32
  import { createMergeCommand, runMergeCommand } from "./merge-command.js";
@@ -19,6 +36,21 @@ import { subagentChannelAdapter } from "./adapters/channel.js";
19
36
  import { StageToolRegistry } from "./stage-tool-registry.js";
20
37
  import { createMergeStageTool } from "./merge-tool.js";
21
38
  import { supersedeMergeRuns } from "./supersede.js";
39
+ /**
40
+ * One library read used by both the runtime card port and the read-time convergence
41
+ * facts. Read-only: the snapshot/doctor consumers must never write.
42
+ * @param skills - the mounted skill store.
43
+ * @returns every live skill as a merge card (body + support files).
44
+ */
45
+ async function readSkillCards(skills) {
46
+ const listed = await skills.list({ withContent: true });
47
+ return await Promise.all(listed.map(async (item) => {
48
+ const probe = await skills.listSupportFiles(item.name);
49
+ const paths = probe.kind === 'present' ? probe.value : [];
50
+ const files = await Promise.all(paths.map(async (path) => ({ path, content: await skills.readSupportFile(item.name, path) ?? '' })));
51
+ return { name: item.name, description: item.description, body: item.content ?? '', files, protectedBy: item.protectedBy, protectionUnknown: item.protectionUnknown, managed: item.managed };
52
+ }));
53
+ }
22
54
  /** E3 knobs are volatile (user layer); the E2 pair timeout is deployment-only. */
23
55
  export const Config = z.object({
24
56
  mergeApproval: z.boolean().default(false).volatile(),
@@ -84,6 +116,8 @@ export * from "./merge-tool.js";
84
116
  export * from "./commands.js";
85
117
  export * from "./scan-ports.js";
86
118
  export * from "./new-child-authoring.js";
119
+ export * from "./domain/authoring-manifest.js";
120
+ export * from "./domain/live-facts.js";
87
121
  export * from "./status.js";
88
122
  export * from "./skipped.js";
89
123
  export * from "./read-commands.js";
@@ -93,14 +127,26 @@ export * from "./params.js";
93
127
  export * from "./doctor.js";
94
128
  export * from "./supersede.js";
95
129
  export * from "./runtime.js";
130
+ /** Not-yet-mounted command face: three lines so a script can read code/hint/next. */
131
+ function formatUnreadyError() {
132
+ return formatMergeError(commandError('merge', 'merge-not-ready', 'evolutionIo 尚未挂载', '等待插件加载完成后重试;运行 /evolution doctor 查看挂载状态'));
133
+ }
96
134
  export const inject = [];
97
135
  export function apply(ctx, rawConfig = {}) {
98
136
  configureMergePresentation(ctx);
99
- const params = resolveMergeParams(rawConfig);
137
+ let params = resolveMergeParams(rawConfig);
100
138
  let mergeRun;
139
+ let source;
140
+ let doctorFact;
101
141
  const mergeService = {
102
142
  async run(rawInput, invocation) {
103
- return mergeRun === undefined ? { ok: false, text: 'merge 未就绪:evolutionIo 尚未挂载' } : mergeRun(rawInput, invocation);
143
+ return mergeRun === undefined ? { ok: false, text: formatUnreadyError() } : mergeRun(rawInput, invocation);
144
+ },
145
+ /** Global-doctor consumer: merges buildDoctorView over the shared snapshot with the reconcile report. */
146
+ async doctorFact() {
147
+ if (doctorFact === undefined)
148
+ return { status: 'unknown', detail: '合并服务尚未完成挂载', remedy: '稍后重新运行 /evolution doctor' };
149
+ return await doctorFact();
104
150
  },
105
151
  };
106
152
  const mergeContext = ctx;
@@ -108,7 +154,6 @@ export function apply(ctx, rawConfig = {}) {
108
154
  ;
109
155
  mergeContext.provide('evolutionMerge', mergeService);
110
156
  }
111
- let source;
112
157
  registerHostRoutes(ctx, audit => makeMergeRoutes({ getSource: () => source }, audit));
113
158
  ctx.inject(['commands', 'evolutionIo'], (scope) => {
114
159
  const registry = scope.get('evolutionIo');
@@ -116,7 +161,18 @@ export function apply(ctx, rawConfig = {}) {
116
161
  return;
117
162
  const io = evolutionIoAdapter(() => registry.provider());
118
163
  const home = evolutionHome();
119
- void loadMergeState(io, home).then(loaded => cleanupExpiredStaging(io, home, Date.now(), new Set(loaded.state.rows.map(row => row.runId).filter((id) => id !== undefined)))).catch((error) => { ctx.logger.warn('merge staging cleanup failed: ' + (error instanceof Error ? error.message : String(error))); });
164
+ const ready = (async () => {
165
+ const v4 = await readV4Readiness(io, home);
166
+ if (v4.ok) {
167
+ const loaded = await loadMergeState(io, home);
168
+ return { ok: true, schema: 3, migrated: false, state: { schemaVersion: 3, rows: loaded.state.rows.map(legacyToV3).filter((row) => row !== null) }, quarantine: [], report: null };
169
+ }
170
+ const state = await ensureMergeStateReady(io, home);
171
+ if (!state.ok)
172
+ return v4.code === 'merge-state-forward-only' || v4.code === 'merge-state-needs-repair' ? { ok: false, code: v4.code, message: v4.message } : state;
173
+ const normalized = await normalizePairLedger(io, home);
174
+ return normalized.ok ? state : { ok: false, code: 'merge-state-needs-repair', message: normalized.message ?? 'v4 normalize failed' };
175
+ })().catch(error => ({ ok: false, code: 'merge-state-migration-blocked', message: error instanceof Error ? error.message : String(error) }));
120
176
  const runs = newRunRegistry({ io, home, warn: (message) => { ctx.logger.warn(message); } });
121
177
  ctx.effect(() => {
122
178
  const host = ctx;
@@ -128,18 +184,56 @@ export function apply(ctx, rawConfig = {}) {
128
184
  });
129
185
  }, 'evolution-merge: restore supersede');
130
186
  const policySnapshot = ctx.get('evolutionPolicy');
187
+ const limitsSnapshot = resolveLimitsSnapshot(policySnapshot?.get?.());
188
+ const hostApproval = ctx.get('evolutionApproval');
189
+ const platformWrite = typeof hostApproval?.platformWrite === 'boolean' ? hostApproval.platformWrite : (params.platformWrite ?? false);
190
+ params = { ...params, limitsSnapshot, platformWrite };
131
191
  const skills = newSkillLibrary({
132
192
  io, ctx, authority: authorityFrom({ origin: 'subagent' }),
133
- limits: { ...DEFAULT_SKILL_LIMITS, ...policyStageLimits(policySnapshot?.get?.()) },
193
+ limits: limitsSnapshot.skillLimits,
134
194
  });
135
- void runs.load().then(() => recoverCommitJournals(io, home, Date.now(), (runId) => {
136
- const run = runs.find(runId);
137
- return run !== undefined && run.state === 'running';
138
- })).then((reports) => { if (reports.length > 0)
139
- ctx.logger.info('merge commit recovery: ' + reports.map(report => report.runId + '=' + report.action).join(', ')); }).catch((error) => { ctx.logger.warn('merge commit recovery failed: ' + (error instanceof Error ? error.message : String(error))); });
195
+ void ready.then(value => {
196
+ if (!value.ok) {
197
+ ctx.logger.warn('merge state is not ready: ' + value.code + ': ' + value.message);
198
+ return;
199
+ }
200
+ const unit = new UnitTransactions({ io, home, backend: skillLibraryUnitBackend(skills), claim: targetClaimUnitPort(io, home, () => Date.now()), now: () => Date.now() });
201
+ const cards = async () => await readSkillCards(skills);
202
+ void (async () => {
203
+ // NFR-05: load the shared run index BEFORE recovery. Recovery settles through the
204
+ // registry's controlled orphan upgrade (settleRecovered), which is the only way a
205
+ // committed unit can turn an orphan-stamped run back into succeeded.
206
+ const loaded = await runs.load();
207
+ if (!loaded.ok)
208
+ ctx.logger.warn('merge run index: ' + (loaded.note ?? 'unreadable'));
209
+ const reports = await recoverCommitUnits({ io, home, unit, now: () => Date.now(), isActive: () => false, runs });
210
+ if (reports.length > 0)
211
+ ctx.logger.info('merge unit recovery: ' + reports.map(report => report.runId + '/' + report.unitId + '=' + report.action).join(', '));
212
+ const v4 = await readV4Readiness(io, home);
213
+ if (v4.ok) {
214
+ const cleanup = await cleanupStage(io, home, Date.now(), new Map(v4.state.rows.map(row => [row.id, { state: row.state }])));
215
+ if (cleanup.expired.length > 0 || cleanup.removed.length > 0)
216
+ ctx.logger.info('merge stage cleanup: ' + String(cleanup.expired.length) + ' expired, ' + String(cleanup.removed.length) + ' removed, ' + String(cleanup.orphans.length) + ' orphan(s)');
217
+ // Reconcile is the one row-state convergence writer; startup simply invokes it.
218
+ // A live library is only supplied when it is non-empty (an empty/unreadable list
219
+ // must never authorize source-gone removal).
220
+ const pending = v4.state.rows.some(row => row.state === 'pending');
221
+ const library = pending ? await cards().catch(() => []) : [];
222
+ const live = library.length === 0 ? undefined : liveRowFactsOf(library);
223
+ const reconciled = await reconcileMergeState({ io, home, now: () => Date.now(), ttlDays: params.mergeSkippedTtlDays, ...(live === undefined ? {} : { live }) });
224
+ if (reconciled.actions.length > 0)
225
+ ctx.logger?.info?.('merge reconcile: ' + String(reconciled.actions.length) + ' action(s)');
226
+ await transactIo(io, reportsSweepLockTarget(), async () => {
227
+ await sweepReports({ io, dir: join(home, 'reports'), buckets: [{ prefix: 'merge-', keep: 20 }], owner: 'evolution-merge', warn: message => ctx.logger.warn(message) });
228
+ return null;
229
+ });
230
+ }
231
+ })().catch((error) => { ctx.logger.warn('merge recovery failed: ' + (error instanceof Error ? error.message : String(error))); });
232
+ }).catch((error) => { ctx.logger.warn('merge state readiness failed: ' + (error instanceof Error ? error.message : String(error))); });
140
233
  const channel = subagentChannelAdapter(createSubagentChannel(ctx));
141
234
  const stage = new StageToolRegistry();
142
- const engineConfig = engineConfigFromRoot(params.mergeModelsRoot);
235
+ const engineInput = resolveEngineInput({ modelsRoot: params.mergeModelsRoot });
236
+ const engineConfig = engineConfigFromRoot(engineInput.modelsRoot);
143
237
  const llm = ctx.get('llm');
144
238
  const defaultModel = ctx.get('agentDefaultModel');
145
239
  const runner = llm === undefined || defaultModel === undefined
@@ -150,63 +244,80 @@ export function apply(ctx, rawConfig = {}) {
150
244
  home,
151
245
  engine: { screen: engineScreen },
152
246
  select: selectWithErrors,
153
- cfg: { ...engineConfig, judge: 'deepseek' },
247
+ cfg: { ...engineConfig, judge: runner === undefined ? 'stub' : 'deepseek', ...(engineInput.maxTokens === 0 ? {} : { maxTokens: engineInput.maxTokens }), ...(engineInput.timeoutMs === 0 ? {} : { timeoutMs: engineInput.timeoutMs }) },
154
248
  ...(runner === undefined ? {} : { runner }),
155
- async onSelection(selection) {
156
- const loaded = await loadMergeState(io, home);
157
- const at = new Date().toISOString();
158
- for (const skipped of selection.skipped) {
159
- if (loaded.state.rows.some(row => row.pairKey === skipped.pairKey))
160
- continue;
161
- const key = pairKey(skipped.a, skipped.b);
162
- await transactRows(io, home, tx => tx.createSkipped({
163
- id: rowIdOf(key), pairKey: key, a: skipped.a, b: skipped.b, version: 0,
164
- createdAt: at, updatedAt: at, origin: 'agent',
165
- state: 'skipped', reason: skipped.reason ?? 'gate-keep', attempts: 0,
166
- }).ok);
167
- }
168
- },
169
- });
170
- source = {
171
- rows() { return loadMergeState(io, home).then(loaded => loaded.state.rows); },
172
- run(id) { return Promise.resolve(runs.runs().find(record => record.id === id)); },
173
- runs() { return Promise.resolve(runs.runs()); },
174
- async body(id, side) {
175
- const row = (await loadMergeState(io, home)).state.rows.find(item => item.id === id);
176
- if (row === undefined)
177
- return null;
178
- return await skills.read(side === 'a' ? row.a : row.b);
179
- },
180
- async history() {
181
- const receipts = await readCommitReceipts(io, home);
182
- const items = new Map();
183
- for (const receipt of receipts) {
184
- for (const source of receipt.sources) {
185
- const key = source + ':' + receipt.runId;
186
- if (items.has(key))
249
+ async onSelection(selection, _runId, cards) {
250
+ try {
251
+ const byName = new Map(cards.map(card => [card.name, card]));
252
+ const filtered = filterSelection(selection, name => byName.get(name)?.managed === true, name => { const card = byName.get(name); return card?.protectionUnknown === true || card?.protectedBy != null; });
253
+ const loaded = await loadMergeState(io, home);
254
+ const at = new Date().toISOString();
255
+ for (const skipped of filtered.skipped) {
256
+ if (loaded.state.rows.some(row => row.pairKey === skipped.pairKey))
187
257
  continue;
188
- items.set(key, { name: source, kind: 'merged', reason: 'Merged into ' + receipt.target, lastVersion: 1, archivedAt: new Date(receipt.at).toISOString(), sourceRunId: receipt.runId });
258
+ const key = pairKey(skipped.a, skipped.b);
259
+ await pairLedger(io, home).transact(tx => tx.createSkipped({
260
+ id: rowIdOf(key), pairKey: key, a: skipped.a, b: skipped.b, version: 0,
261
+ createdAt: at, updatedAt: at, origin: 'agent',
262
+ state: 'skipped', reason: skipped.reason ?? 'gate-keep', attempts: 0,
263
+ }).ok);
189
264
  }
190
265
  }
191
- const archived = await skills.archivedNames?.() ?? [];
192
- const seen = new Set([...items.values()].map(item => item.name));
193
- for (const name of archived) {
194
- if (seen.has(name))
195
- continue;
196
- items.set('archived:' + name, { name, kind: 'other', reason: 'archived', lastVersion: 1, archivedAt: new Date(Date.now()).toISOString() });
266
+ catch (error) {
267
+ const meta = await readRunMeta(io, home, _runId);
268
+ if (meta !== null)
269
+ await writeRunMeta(io, home, { ...meta, degraded: true, updatedAt: new Date().toISOString() });
270
+ ctx.logger.warn('merge selection reportOnly write failed (degraded): ' + (error instanceof Error ? error.message : String(error)));
197
271
  }
198
- return [...items.values()];
199
272
  },
273
+ });
274
+ source = {
275
+ async snapshot() {
276
+ return await readMergeSnapshot({ io, home, runs: runs.runs('merge'), approval: params.mergeApproval, live: async () => liveRowFactsOf(await readSkillCards(skills)), history: async () => {
277
+ const items = new Map();
278
+ const receipts = [...await readCommitReceipts(io, home)].sort((a, b) => a.at - b.at);
279
+ const generationOf = new Map(receipts.map((receipt, index) => [receipt.runId, index + 1]));
280
+ for (const receipt of receipts)
281
+ for (const name of receipt.sources)
282
+ items.set(name, { name, kind: 'merged', reason: '已合并进「' + receipt.target + '」,最后一版保留可查。', reasonCode: 'merged', lastVersion: 1, archivedAt: receipt.at, sourceRunId: receipt.runId, runId: receipt.runId, absorbedInto: receipt.target, generation: generationOf.get(receipt.runId) ?? 1 });
283
+ const archived = await skills.archivedNames?.() ?? [];
284
+ const reasonOf = skills.readArchiveReason;
285
+ for (const name of archived) {
286
+ if (items.has(name))
287
+ continue;
288
+ const rawReason = reasonOf === undefined ? null : await reasonOf.call(skills, name).catch(() => null);
289
+ const lower = (rawReason ?? '').toLowerCase();
290
+ const kind = lower.includes('split') ? 'split' : lower.includes('delet') || lower.includes('remove') ? 'deleted' : 'other';
291
+ items.set(name, { name, kind, reason: rawReason ?? '技能已归档,最后一版保留可查。', reasonCode: kind === 'other' ? 'archived' : kind, lastVersion: 1, archivedAt: Date.now() });
292
+ }
293
+ const out = [];
294
+ for (const item of items.values()) {
295
+ const versions = await skills.listVersions(item.name).catch(() => []);
296
+ const lastVersion = versions.at(-1)?.v ?? item.lastVersion;
297
+ out.push({ ...item, lastVersion, generation: item.generation ?? lastVersion });
298
+ }
299
+ return out;
300
+ } });
301
+ },
302
+ async bodyRef(ref) { return await resolveBody(io, home, ref, async () => ref.side === 'archive' ? await skills.readArchived(ref.name) : await skills.read(ref.name)); },
200
303
  approval() { return params.mergeApproval; },
304
+ readiness() { return ready; },
201
305
  commands() {
202
- return ['merge scan', 'merge doctor', 'merge status', 'merge pending', 'merge skipped', 'merge report', 'merge runs', 'merge cancel'];
306
+ return ['merge scan', 'merge doctor', 'merge status', 'merge pending', 'merge skipped', 'merge report', 'merge runs', 'merge cancel', 'merge reconcile'];
203
307
  },
204
308
  };
309
+ doctorFact = async () => {
310
+ const current = source;
311
+ if (current?.snapshot === undefined)
312
+ return { status: 'unknown', detail: '合并快照来源尚未挂载', remedy: '稍后重新运行 /evolution doctor' };
313
+ const view = buildDoctorView(await current.snapshot());
314
+ return mergeDoctorFact(view, await readReconcileReport(io, home));
315
+ };
205
316
  const commands = scope.get('commands');
206
317
  if (commands === undefined)
207
318
  return;
208
319
  const runtime = createMergeRuntime({
209
- io, home, registry: runs, skills, now: () => Date.now(), params, platformWrite: true,
320
+ io, home, registry: runs, skills, now: () => Date.now(), params, platformWrite,
210
321
  channel, parent: undefined, allow: ['merge_stage'], timeoutMinutes: params.mergeRunTimeoutMinutes,
211
322
  isBlocked: async (name) => {
212
323
  const curator = ctx.get('evolutionCurator');
@@ -217,15 +328,9 @@ export function apply(ctx, rawConfig = {}) {
217
328
  return run !== undefined && run.state === 'running';
218
329
  },
219
330
  async cards(signal) {
220
- const listed = await skills.list({ withContent: true });
221
331
  if (signal.aborted)
222
332
  return [];
223
- return Promise.all(listed.map(async (item) => {
224
- const probe = await skills.listSupportFiles(item.name);
225
- const paths = probe.kind === 'present' ? probe.value : [];
226
- const files = await Promise.all(paths.map(async (path) => ({ path, content: await skills.readSupportFile(item.name, path) ?? '' })));
227
- return { name: item.name, description: item.description, body: item.content ?? '', files, protectedBy: item.protectedBy, protectionUnknown: item.protectionUnknown, managed: item.managed };
228
- }));
333
+ return await readSkillCards(skills);
229
334
  },
230
335
  screeningForRun: engineScreening,
231
336
  stage: {
@@ -233,14 +338,22 @@ export function apply(ctx, rawConfig = {}) {
233
338
  unbind: (childId) => { stage.unbind(childId); },
234
339
  },
235
340
  });
236
- mergeRun = (rawInput, invocation) => runMergeCommand(runtime, rawInput, invocation);
341
+ mergeRun = async (rawInput, invocation) => {
342
+ const state = await ready;
343
+ // These verbs answer usefully on a blocked state; reconcile reports the block without writing.
344
+ const allowedWhenBlocked = /(?:^|\s)(doctor|status|pending|skipped|report|runs|reconcile)(?:\s|$)/.test(rawInput);
345
+ if (!state.ok && !allowedWhenBlocked)
346
+ return { ok: false, text: formatMergeError(commandError('merge', state.code, state.message, '先修复合并状态(/evolution merge doctor),再重试')) };
347
+ return runMergeCommand(runtime, rawInput, invocation);
348
+ };
237
349
  const command = createMergeCommand(runtime);
238
- scope.effect(() => commands.register(command), 'evolution-merge: command');
350
+ if (command !== null)
351
+ scope.effect(() => commands.register(command), 'evolution-merge: command');
239
352
  ctx.inject(['tools'], (toolScope) => {
240
353
  const tools = toolScope.get('tools');
241
354
  if (tools === undefined)
242
355
  return;
243
- const tool = createMergeStageTool(io, home, exec => stage.resolve(exec));
356
+ const tool = createMergeStageTool(io, home, exec => stage.resolve(exec), exec => stage.snapshot(exec));
244
357
  toolScope.effect(() => tools.register(tool), 'evolution-merge: merge_stage');
245
358
  });
246
359
  scope.effect(() => () => { runs.cancelAll(); }, 'evolution-merge: runs');
@@ -11,6 +11,8 @@ export interface MergeCommandResult {
11
11
  export interface MergeCommandContext {
12
12
  /** The invoking agent, when a command (not a route) initiated the run. */
13
13
  readonly parent?: unknown;
14
+ /** True when the invocation carried no agent at all (the E-305 shape). */
15
+ readonly agentless?: boolean;
14
16
  }
15
17
  export interface MergeCommandService {
16
18
  execute(command: MergeCliCommand, context?: MergeCommandContext): Promise<MergeCommandResult>;
@@ -35,5 +37,5 @@ export declare function createReadOnlyMergeService(io: EvolutionIoLike, home: st
35
37
  export declare function runMergeCommand(service: MergeCommandService, rawInput: string, invocation?: {
36
38
  readonly agent?: unknown;
37
39
  }): Promise<MergeCommandResult>;
38
- export declare function createMergeCommand(service: MergeCommandService): MergeCommandDefinitionLike;
40
+ export declare function createMergeCommand(service: MergeCommandService, version?: string): MergeCommandDefinitionLike | null;
39
41
  //# sourceMappingURL=merge-command.d.ts.map
@@ -1,12 +1,14 @@
1
1
  import { parseMergeCommand, mergeUsage } from "./commands.js";
2
+ import { aliasRegistration } from '@lmzhen/dsh-evolution-commands';
3
+ import { MERGE_ALIAS_DEF, MERGE_PACKAGE_VERSION } from "./command-defs.js";
2
4
  import { handleReadOnlyMergeCommand } from "./read-commands.js";
3
- import { formatMergeError } from "./domain/errors.js";
5
+ import { commandError, formatMergeError } from "./domain/errors.js";
4
6
  export function createReadOnlyMergeService(io, home) {
5
7
  return {
6
8
  async execute(command) {
7
9
  const text = await handleReadOnlyMergeCommand(command, io, home);
8
10
  return text === null
9
- ? { ok: false, text: 'merge ' + command.verb + ' 需要运行引擎(当前只挂载了只读命令)' }
11
+ ? { ok: false, text: formatMergeError(commandError(command.verb, 'merge-engine-required', '该命令需要合并运行时(当前只挂载了只读命令)', '安装 evolution-merge 完整行后重试;只读队列见 /evolution merge status')) }
10
12
  : { ok: true, text };
11
13
  },
12
14
  };
@@ -23,16 +25,18 @@ export async function runMergeCommand(service, rawInput, invocation) {
23
25
  if (parsed.help === true)
24
26
  return { ok: true, text: mergeUsage() };
25
27
  try {
26
- return await service.execute(parsed.command, invocation?.agent === undefined ? {} : { parent: invocation.agent });
28
+ return await service.execute(parsed.command, invocation?.agent === undefined ? { agentless: true } : { parent: invocation.agent });
27
29
  }
28
30
  catch (error) {
29
- return { ok: false, text: formatMergeError({ kind: 'runtime', code: 'merge-command-failed', message: error instanceof Error ? error.message : String(error) }) };
31
+ return { ok: false, text: formatMergeError(commandError('command', 'merge-command-failed', error instanceof Error ? error.message : String(error), '重试;若持续失败,运行 /evolution merge doctor 查看状态')) };
30
32
  }
31
33
  }
32
- export function createMergeCommand(service) {
34
+ export function createMergeCommand(service, version = MERGE_PACKAGE_VERSION) {
35
+ if (aliasRegistration(version, MERGE_ALIAS_DEF) === 'removed')
36
+ return null;
33
37
  return {
34
38
  name: 'merge',
35
- description: '技能自动合并:扫描、待批准、重跑、报告与 undo(兼容别名;请用 /evolution merge)',
39
+ description: (aliasRegistration(version, MERGE_ALIAS_DEF) === 'deprecated' ? '已弃用,请用 /evolution merge。' : '') + '技能自动合并:扫描、待批准、重跑、报告与 undo(兼容别名;请用 /evolution merge)',
36
40
  input: { hint: mergeUsage() },
37
41
  async handler(invocation) {
38
42
  const result = await runMergeCommand(service, invocation.rawInput, invocation);
@@ -5,6 +5,8 @@
5
5
  import type { RunRecord } from '@lmzhen/dsh-evolution-core';
6
6
  import { type ConnectionFetchRouteLike, type DecisionAudit } from '@lmzhen/dsh-evolution-core/http-routes';
7
7
  import type { MergeRow } from './contracts.ts';
8
+ import type { StateReadiness } from './domain/state-bootstrap.ts';
9
+ import type { MergeSnapshot, BodyRef } from './domain/merge-snapshot.ts';
8
10
  export declare const MERGE_ROUTES: {
9
11
  view: `/api/dsh-evolution/${string}`;
10
12
  rows: `/api/dsh-evolution/${string}`;
@@ -20,13 +22,16 @@ export interface MergeHistoryItem {
20
22
  readonly sourceRunId?: string;
21
23
  }
22
24
  export interface MergeRouteSource {
23
- rows(): Promise<readonly MergeRow[]>;
24
- run(id: string): Promise<RunRecord | undefined>;
25
- runs(cursor?: string, limit?: number): Promise<readonly RunRecord[]>;
26
- body(id: string, side: 'a' | 'b'): Promise<string | null>;
27
- history(): Promise<readonly MergeHistoryItem[]>;
25
+ snapshot?(): Promise<MergeSnapshot>;
26
+ bodyRef?(ref: BodyRef): Promise<string | null>;
27
+ rows?(): Promise<readonly MergeRow[]>;
28
+ run?(id: string): Promise<RunRecord | undefined>;
29
+ runs?(cursor?: string, limit?: number): Promise<readonly RunRecord[]>;
30
+ body?(id: string, side: 'a' | 'b'): Promise<string | null>;
31
+ history?(): Promise<readonly MergeHistoryItem[]>;
28
32
  commands(): readonly string[];
29
33
  approval?(): boolean;
34
+ readiness?(): Promise<StateReadiness>;
30
35
  }
31
36
  export declare function makeMergeRoutes(deps: {
32
37
  getSource: () => MergeRouteSource | undefined;