@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.
Files changed (187) hide show
  1. package/README.md +25 -7
  2. package/data/minhash_params.json +1063 -0
  3. package/lib/index.js +7308 -1800
  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/library.js +4 -1
  9. package/lib/types/adapters/model-runner.d.ts +0 -5
  10. package/lib/types/adapters/model-runner.js +8 -2
  11. package/lib/types/adapters/model.d.ts +31 -0
  12. package/lib/types/adapters/model.js +47 -0
  13. package/lib/types/adapters/select.js +12 -8
  14. package/lib/types/approve-command.d.ts +14 -1
  15. package/lib/types/approve-command.js +72 -44
  16. package/lib/types/authoring-flow.d.ts +19 -1
  17. package/lib/types/authoring-flow.js +49 -15
  18. package/lib/types/channel-port.d.ts +3 -1
  19. package/lib/types/channel-port.js +8 -3
  20. package/lib/types/command-defs.d.ts +5 -0
  21. package/lib/types/command-defs.js +33 -0
  22. package/lib/types/command-surface.d.ts +5 -0
  23. package/lib/types/command-surface.js +11 -0
  24. package/lib/types/commands.d.ts +3 -4
  25. package/lib/types/commands.js +33 -72
  26. package/lib/types/commit-assembly.d.ts +16 -1
  27. package/lib/types/commit-assembly.js +72 -11
  28. package/lib/types/commit-execute.d.ts +14 -2
  29. package/lib/types/commit-execute.js +76 -14
  30. package/lib/types/commit-plan.d.ts +7 -1
  31. package/lib/types/commit-plan.js +4 -0
  32. package/lib/types/commit-receipt.d.ts +3 -0
  33. package/lib/types/commit-recovery.d.ts +7 -2
  34. package/lib/types/commit-recovery.js +73 -96
  35. package/lib/types/contracts.d.ts +44 -1
  36. package/lib/types/contracts.js +1 -1
  37. package/lib/types/coordinator.d.ts +3 -2
  38. package/lib/types/coordinator.js +4 -3
  39. package/lib/types/decision.js +1 -1
  40. package/lib/types/doctor.d.ts +18 -1
  41. package/lib/types/doctor.js +164 -10
  42. package/lib/types/domain/authoring-manifest.d.ts +26 -0
  43. package/lib/types/domain/authoring-manifest.js +43 -0
  44. package/lib/types/domain/claim.d.ts +4 -1
  45. package/lib/types/domain/claim.js +2 -2
  46. package/lib/types/domain/commit-journal-store.js +5 -1
  47. package/lib/types/domain/commit-journal.js +1 -1
  48. package/lib/types/domain/consistency.d.ts +22 -0
  49. package/lib/types/domain/consistency.js +50 -0
  50. package/lib/types/domain/engine-criteria.d.ts +10 -0
  51. package/lib/types/domain/engine-criteria.js +11 -0
  52. package/lib/types/domain/engine-input.d.ts +10 -0
  53. package/lib/types/domain/engine-input.js +26 -0
  54. package/lib/types/domain/errors.d.ts +15 -1
  55. package/lib/types/domain/errors.js +23 -4
  56. package/lib/types/domain/evidence-hash.d.ts +3 -0
  57. package/lib/types/domain/evidence-hash.js +14 -0
  58. package/lib/types/domain/failure-route.d.ts +14 -0
  59. package/lib/types/domain/failure-route.js +12 -0
  60. package/lib/types/domain/ledger.js +10 -1
  61. package/lib/types/domain/live-facts.d.ts +31 -0
  62. package/lib/types/domain/live-facts.js +54 -0
  63. package/lib/types/domain/merge-limits.d.ts +18 -0
  64. package/lib/types/domain/merge-limits.js +22 -0
  65. package/lib/types/domain/merge-report.d.ts +41 -0
  66. package/lib/types/domain/merge-report.js +2 -0
  67. package/lib/types/domain/merge-snapshot.d.ts +99 -0
  68. package/lib/types/domain/merge-snapshot.js +82 -0
  69. package/lib/types/domain/migrate-v3.d.ts +20 -0
  70. package/lib/types/domain/migrate-v3.js +68 -0
  71. package/lib/types/domain/pair-ledger.d.ts +34 -0
  72. package/lib/types/domain/pair-ledger.js +96 -0
  73. package/lib/types/domain/pre-1b-backup.d.ts +43 -0
  74. package/lib/types/domain/pre-1b-backup.js +140 -0
  75. package/lib/types/domain/provider-key.d.ts +2 -0
  76. package/lib/types/domain/provider-key.js +11 -0
  77. package/lib/types/domain/read-probe.d.ts +5 -0
  78. package/lib/types/domain/read-probe.js +11 -0
  79. package/lib/types/domain/reconcile.d.ts +47 -0
  80. package/lib/types/domain/reconcile.js +178 -0
  81. package/lib/types/domain/resolve-pair.d.ts +2 -9
  82. package/lib/types/domain/resolve-pair.js +2 -15
  83. package/lib/types/domain/row-tx.d.ts +27 -0
  84. package/lib/types/domain/row-tx.js +31 -0
  85. package/lib/types/domain/row-v3-bridge.d.ts +6 -0
  86. package/lib/types/domain/row-v3-bridge.js +47 -0
  87. package/lib/types/domain/row-v3.d.ts +113 -0
  88. package/lib/types/domain/row-v3.js +107 -0
  89. package/lib/types/domain/row-v4.d.ts +148 -0
  90. package/lib/types/domain/row-v4.js +159 -0
  91. package/lib/types/domain/row.d.ts +1 -0
  92. package/lib/types/domain/run-coordinator.d.ts +51 -0
  93. package/lib/types/domain/run-coordinator.js +45 -0
  94. package/lib/types/domain/schema4.d.ts +53 -0
  95. package/lib/types/domain/schema4.js +129 -0
  96. package/lib/types/domain/selection-policy.d.ts +14 -0
  97. package/lib/types/domain/selection-policy.js +6 -0
  98. package/lib/types/domain/skipped-expiry.d.ts +5 -0
  99. package/lib/types/domain/skipped-expiry.js +16 -0
  100. package/lib/types/domain/stage-aggregate.d.ts +96 -0
  101. package/lib/types/domain/stage-aggregate.js +293 -0
  102. package/lib/types/domain/staging-index.d.ts +15 -0
  103. package/lib/types/domain/staging-index.js +41 -0
  104. package/lib/types/domain/state-bootstrap.d.ts +43 -0
  105. package/lib/types/domain/state-bootstrap.js +267 -0
  106. package/lib/types/domain/state-v3.d.ts +4 -0
  107. package/lib/types/domain/state-v3.js +4 -0
  108. package/lib/types/domain/undo-journal.d.ts +19 -0
  109. package/lib/types/domain/undo-journal.js +20 -0
  110. package/lib/types/domain/undo-receipt.d.ts +18 -0
  111. package/lib/types/domain/undo-receipt.js +22 -0
  112. package/lib/types/domain/validation-plan.d.ts +65 -0
  113. package/lib/types/domain/validation-plan.js +49 -0
  114. package/lib/types/facts.d.ts +26 -0
  115. package/lib/types/facts.js +65 -0
  116. package/lib/types/gate-filter.d.ts +31 -0
  117. package/lib/types/gate-filter.js +64 -0
  118. package/lib/types/index.d.ts +7 -2
  119. package/lib/types/index.js +185 -82
  120. package/lib/types/ladder.js +1 -1
  121. package/lib/types/merge-command.d.ts +3 -1
  122. package/lib/types/merge-command.js +10 -6
  123. package/lib/types/merge-routes.d.ts +12 -7
  124. package/lib/types/merge-routes.js +87 -23
  125. package/lib/types/merge-tool.d.ts +1 -1
  126. package/lib/types/merge-tool.js +2 -2
  127. package/lib/types/metrics.d.ts +16 -0
  128. package/lib/types/metrics.js +27 -0
  129. package/lib/types/new-child-authoring.d.ts +5 -1
  130. package/lib/types/new-child-authoring.js +71 -13
  131. package/lib/types/params.d.ts +2 -4
  132. package/lib/types/params.js +13 -0
  133. package/lib/types/pending-actions.d.ts +2 -2
  134. package/lib/types/pending-actions.js +17 -17
  135. package/lib/types/prompts.d.ts +2 -4
  136. package/lib/types/prompts.js +21 -8
  137. package/lib/types/propose.d.ts +26 -0
  138. package/lib/types/propose.js +69 -0
  139. package/lib/types/read-commands.d.ts +4 -4
  140. package/lib/types/read-commands.js +19 -26
  141. package/lib/types/recovery-coordinator.d.ts +34 -0
  142. package/lib/types/recovery-coordinator.js +84 -0
  143. package/lib/types/recovery-projection.d.ts +31 -0
  144. package/lib/types/recovery-projection.js +96 -0
  145. package/lib/types/resume-command.d.ts +7 -1
  146. package/lib/types/resume-command.js +50 -14
  147. package/lib/types/revision.d.ts +17 -0
  148. package/lib/types/revision.js +39 -3
  149. package/lib/types/round2-outcome.d.ts +2 -1
  150. package/lib/types/round2-outcome.js +68 -12
  151. package/lib/types/run-shell.d.ts +16 -2
  152. package/lib/types/run-shell.js +49 -5
  153. package/lib/types/runtime.d.ts +4 -6
  154. package/lib/types/runtime.js +341 -86
  155. package/lib/types/scan-command.d.ts +1 -0
  156. package/lib/types/scan-command.js +2 -0
  157. package/lib/types/scan-ports.d.ts +21 -1
  158. package/lib/types/scan-ports.js +51 -23
  159. package/lib/types/scan-run.d.ts +17 -0
  160. package/lib/types/scan-run.js +221 -63
  161. package/lib/types/screening-port.d.ts +47 -0
  162. package/lib/types/screening-port.js +93 -0
  163. package/lib/types/screening.d.ts +30 -0
  164. package/lib/types/screening.js +75 -0
  165. package/lib/types/stage-tool-registry.d.ts +7 -0
  166. package/lib/types/stage-tool-registry.js +12 -1
  167. package/lib/types/state.d.ts +15 -1
  168. package/lib/types/state.js +39 -17
  169. package/lib/types/status.d.ts +8 -0
  170. package/lib/types/status.js +64 -0
  171. package/lib/types/surface/body.d.ts +4 -0
  172. package/lib/types/surface/body.js +16 -0
  173. package/lib/types/surface/merge-view.d.ts +14 -0
  174. package/lib/types/surface/merge-view.js +126 -0
  175. package/lib/types/target-claim.d.ts +1 -0
  176. package/lib/types/target-claim.js +63 -18
  177. package/lib/types/undo-operation.d.ts +24 -0
  178. package/lib/types/undo-operation.js +135 -0
  179. package/lib/types/unit-claim.d.ts +5 -0
  180. package/lib/types/unit-claim.js +15 -0
  181. package/lib/types/wire-projection.d.ts +36 -0
  182. package/lib/types/wire-projection.js +69 -0
  183. package/lib/types/wire.d.ts +273 -0
  184. package/lib/types/wire.js +3 -0
  185. package/package.json +10 -4
  186. package/lib/types/routes.d.ts +0 -70
  187. package/lib/types/routes.js +0 -78
@@ -0,0 +1,64 @@
1
+ import { pairKey } from "./contracts.js";
2
+ import { runLadder, selectedForAudit } from "./ladder.js";
3
+ function withGate(candidate, verdicts, auditOverride) {
4
+ const last = verdicts[verdicts.length - 1];
5
+ if (last === undefined)
6
+ return candidate;
7
+ return {
8
+ ...candidate,
9
+ gate: {
10
+ level: last.level,
11
+ verdict: last.verdict,
12
+ reason: last.reason,
13
+ ...(last.cached ? { cached: true } : {}),
14
+ ...(auditOverride ? { auditOverride: true } : {}),
15
+ },
16
+ };
17
+ }
18
+ export async function filterCandidates(candidates, options) {
19
+ const kept = [];
20
+ const dropped = [];
21
+ const errors = [];
22
+ let calls = 0;
23
+ for (const candidate of candidates) {
24
+ const first = await runLadder({
25
+ level: 'L0', candidate, facts: options.facts, model: options.model,
26
+ modelName: options.modelName, signal: options.signal,
27
+ ...(options.cache === undefined ? {} : { cache: options.cache }),
28
+ });
29
+ calls += first.calls;
30
+ if (first.route === 'error') {
31
+ errors.push({ candidate, message: first.error ?? 'gate error' });
32
+ continue;
33
+ }
34
+ if (first.route !== 'keep' || first.droppedAt !== 'L0') {
35
+ if (first.route === 'merge' || first.route === 'undecided')
36
+ kept.push({ candidate: withGate(candidate, first.verdicts, false), verdicts: first.verdicts, auditOverride: false });
37
+ else
38
+ dropped.push({ candidate, verdicts: first.verdicts, auditOverride: false });
39
+ continue;
40
+ }
41
+ if (!selectedForAudit(pairKey(candidate.a, candidate.b), options.runSeed, options.auditRate ?? 0.1)) {
42
+ dropped.push({ candidate, verdicts: first.verdicts, auditOverride: false });
43
+ continue;
44
+ }
45
+ const audit = await runLadder({
46
+ level: 'L0', startLevel: 'L2', candidate, facts: options.facts, model: options.model,
47
+ modelName: options.modelName, signal: options.signal,
48
+ ...(options.cache === undefined ? {} : { cache: options.cache }),
49
+ });
50
+ calls += audit.calls;
51
+ if (audit.route === 'merge' || audit.route === 'undecided') {
52
+ const all = [...first.verdicts, ...audit.verdicts];
53
+ kept.push({ candidate: withGate(candidate, all, true), verdicts: all, auditOverride: true });
54
+ }
55
+ else if (audit.route === 'error') {
56
+ errors.push({ candidate, message: audit.error ?? 'audit gate error' });
57
+ }
58
+ else {
59
+ dropped.push({ candidate, verdicts: [...first.verdicts, ...audit.verdicts], auditOverride: true });
60
+ }
61
+ }
62
+ return { kept, dropped, errors, calls };
63
+ }
64
+ //# sourceMappingURL=gate-filter.js.map
@@ -22,7 +22,11 @@ export interface MergePresentationScope {
22
22
  }, owner?: unknown) => unknown;
23
23
  };
24
24
  }
25
- export declare function engineConfigFromEnv(configuredRoot?: string): Record<string, unknown> | undefined;
25
+ export interface EngineModelConfig {
26
+ readonly embedder_model: string;
27
+ readonly embedder_model_last: string;
28
+ }
29
+ export declare function engineConfigFromRoot(configuredRoot?: string): EngineModelConfig | undefined;
26
30
  export interface MergePresentationContext {
27
31
  inject(names: string[], run: (scope: MergePresentationScope) => void): void;
28
32
  readonly fiber?: unknown;
@@ -66,10 +70,11 @@ export * from './merge-tool.ts';
66
70
  export * from './commands.ts';
67
71
  export * from './scan-ports.ts';
68
72
  export * from './new-child-authoring.ts';
73
+ export * from './domain/authoring-manifest.ts';
74
+ export * from './domain/live-facts.ts';
69
75
  export * from './status.ts';
70
76
  export * from './skipped.ts';
71
77
  export * from './read-commands.ts';
72
- export * from './routes.ts';
73
78
  export * from './merge-routes.ts';
74
79
  export * from './merge-command.ts';
75
80
  export * from './params.ts';
@@ -4,13 +4,30 @@ 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, skillsRoot } 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, upsertRow } 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";
13
- import { transition } from "./coordinator.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";
30
+ import { pairKey, rowIdOf } from "./contracts.js";
14
31
  import { makeMergeRoutes } from "./merge-routes.js";
15
32
  import { createMergeCommand, runMergeCommand } from "./merge-command.js";
16
33
  import { createMergeRuntime } from "./runtime.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(),
@@ -27,15 +59,11 @@ export const Config = z.object({
27
59
  mergePairTimeoutMinutes: z.number().min(1).default(15),
28
60
  mergeModelsRoot: z.string().default(''),
29
61
  });
30
- export function engineConfigFromEnv(configuredRoot) {
31
- const root = configuredRoot !== undefined && configuredRoot !== '' ? configuredRoot : process.env.SKILL_MERGE_MODELS_ROOT;
32
- if (root === undefined || root === '')
62
+ export function engineConfigFromRoot(configuredRoot) {
63
+ const root = configuredRoot?.trim() ?? '';
64
+ if (root === '')
33
65
  return undefined;
34
66
  return {
35
- models_dir: '',
36
- offline: true,
37
- auto_provision: false,
38
- embed_backend: 'local',
39
67
  embedder_model: join(root, 'potion-multilingual-128M-int8'),
40
68
  embedder_model_last: join(root, 'potion-multilingual-128M-pca64-int8'),
41
69
  };
@@ -88,24 +116,37 @@ export * from "./merge-tool.js";
88
116
  export * from "./commands.js";
89
117
  export * from "./scan-ports.js";
90
118
  export * from "./new-child-authoring.js";
119
+ export * from "./domain/authoring-manifest.js";
120
+ export * from "./domain/live-facts.js";
91
121
  export * from "./status.js";
92
122
  export * from "./skipped.js";
93
123
  export * from "./read-commands.js";
94
- export * from "./routes.js";
95
124
  export * from "./merge-routes.js";
96
125
  export * from "./merge-command.js";
97
126
  export * from "./params.js";
98
127
  export * from "./doctor.js";
99
128
  export * from "./supersede.js";
100
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
+ }
101
134
  export const inject = [];
102
135
  export function apply(ctx, rawConfig = {}) {
103
136
  configureMergePresentation(ctx);
104
- const params = resolveMergeParams(rawConfig);
137
+ let params = resolveMergeParams(rawConfig);
105
138
  let mergeRun;
139
+ let source;
140
+ let doctorFact;
106
141
  const mergeService = {
107
142
  async run(rawInput, invocation) {
108
- 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();
109
150
  },
110
151
  };
111
152
  const mergeContext = ctx;
@@ -113,7 +154,6 @@ export function apply(ctx, rawConfig = {}) {
113
154
  ;
114
155
  mergeContext.provide('evolutionMerge', mergeService);
115
156
  }
116
- let source;
117
157
  registerHostRoutes(ctx, audit => makeMergeRoutes({ getSource: () => source }, audit));
118
158
  ctx.inject(['commands', 'evolutionIo'], (scope) => {
119
159
  const registry = scope.get('evolutionIo');
@@ -121,9 +161,19 @@ export function apply(ctx, rawConfig = {}) {
121
161
  return;
122
162
  const io = evolutionIoAdapter(() => registry.provider());
123
163
  const home = evolutionHome();
124
- 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) }));
125
176
  const runs = newRunRegistry({ io, home, warn: (message) => { ctx.logger.warn(message); } });
126
- void runs.load();
127
177
  ctx.effect(() => {
128
178
  const host = ctx;
129
179
  if (typeof host.on !== 'function')
@@ -134,22 +184,56 @@ export function apply(ctx, rawConfig = {}) {
134
184
  });
135
185
  }, 'evolution-merge: restore supersede');
136
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 };
137
191
  const skills = newSkillLibrary({
138
192
  io, ctx, authority: authorityFrom({ origin: 'subagent' }),
139
- limits: { ...DEFAULT_SKILL_LIMITS, ...policyStageLimits(policySnapshot?.get?.()) },
193
+ limits: limitsSnapshot.skillLimits,
140
194
  });
141
- const mergeSkillsRoot = skills.root ?? skillsRoot();
142
- void recoverCommitJournals(io, home, Date.now(), mergeSkillsRoot, async (name) => {
143
- const result = await skills.restoreFromArchive(name);
144
- return result.ok;
145
- }, (runId) => {
146
- const run = runs.find(runId);
147
- return run !== undefined && run.state === 'running';
148
- }).then((reports) => { if (reports.length > 0)
149
- 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))); });
150
233
  const channel = subagentChannelAdapter(createSubagentChannel(ctx));
151
234
  const stage = new StageToolRegistry();
152
- const engineConfig = engineConfigFromEnv(params.mergeModelsRoot);
235
+ const engineInput = resolveEngineInput({ modelsRoot: params.mergeModelsRoot });
236
+ const engineConfig = engineConfigFromRoot(engineInput.modelsRoot);
153
237
  const llm = ctx.get('llm');
154
238
  const defaultModel = ctx.get('agentDefaultModel');
155
239
  const runner = llm === undefined || defaultModel === undefined
@@ -160,63 +244,80 @@ export function apply(ctx, rawConfig = {}) {
160
244
  home,
161
245
  engine: { screen: engineScreen },
162
246
  select: selectWithErrors,
163
- cfg: { ...(engineConfig ?? {}), judge: 'deepseek' },
247
+ cfg: { ...engineConfig, judge: runner === undefined ? 'stub' : 'deepseek', ...(engineInput.maxTokens === 0 ? {} : { maxTokens: engineInput.maxTokens }), ...(engineInput.timeoutMs === 0 ? {} : { timeoutMs: engineInput.timeoutMs }) },
164
248
  ...(runner === undefined ? {} : { runner }),
165
- async onSelection(selection) {
166
- const loaded = await loadMergeState(io, home);
167
- const at = new Date().toISOString();
168
- for (const skipped of selection.skipped) {
169
- if (loaded.state.rows.some(row => row.pairKey === skipped.pairKey))
170
- continue;
171
- const next = transition(undefined, {
172
- type: 'gate-keep', a: skipped.a, b: skipped.b,
173
- ...(skipped.reason === undefined ? {} : { reason: skipped.reason }), at,
174
- });
175
- if (next.ok && next.row !== null)
176
- await upsertRow(io, home, () => Date.now(), next.row);
177
- }
178
- },
179
- });
180
- source = {
181
- rows() { return loadMergeState(io, home).then(loaded => loaded.state.rows); },
182
- run(id) { return Promise.resolve(runs.runs().find(record => record.id === id)); },
183
- runs() { return Promise.resolve(runs.runs()); },
184
- async body(id, side) {
185
- const row = (await loadMergeState(io, home)).state.rows.find(item => item.id === id);
186
- if (row === undefined)
187
- return null;
188
- return await skills.read(side === 'a' ? row.a : row.b);
189
- },
190
- async history() {
191
- const receipts = await readCommitReceipts(io, home);
192
- const items = new Map();
193
- for (const receipt of receipts) {
194
- for (const source of receipt.sources) {
195
- const key = source + ':' + receipt.runId;
196
- 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))
197
257
  continue;
198
- 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);
199
264
  }
200
265
  }
201
- const archived = await skills.archivedNames?.() ?? [];
202
- const seen = new Set([...items.values()].map(item => item.name));
203
- for (const name of archived) {
204
- if (seen.has(name))
205
- continue;
206
- 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)));
207
271
  }
208
- return [...items.values()];
209
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)); },
210
303
  approval() { return params.mergeApproval; },
304
+ readiness() { return ready; },
211
305
  commands() {
212
- 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'];
213
307
  },
214
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
+ };
215
316
  const commands = scope.get('commands');
216
317
  if (commands === undefined)
217
318
  return;
218
319
  const runtime = createMergeRuntime({
219
- io, home, registry: runs, skills, now: () => Date.now(), params, platformWrite: true,
320
+ io, home, registry: runs, skills, now: () => Date.now(), params, platformWrite,
220
321
  channel, parent: undefined, allow: ['merge_stage'], timeoutMinutes: params.mergeRunTimeoutMinutes,
221
322
  isBlocked: async (name) => {
222
323
  const curator = ctx.get('evolutionCurator');
@@ -227,15 +328,9 @@ export function apply(ctx, rawConfig = {}) {
227
328
  return run !== undefined && run.state === 'running';
228
329
  },
229
330
  async cards(signal) {
230
- const listed = await skills.list({ withContent: true });
231
331
  if (signal.aborted)
232
332
  return [];
233
- return Promise.all(listed.map(async (item) => {
234
- const probe = await skills.listSupportFiles(item.name);
235
- const paths = probe.kind === 'present' ? probe.value : [];
236
- const files = await Promise.all(paths.map(async (path) => ({ path, content: await skills.readSupportFile(item.name, path) ?? '' })));
237
- return { name: item.name, description: item.description, body: item.content ?? '', files, protectedBy: item.protectedBy, protectionUnknown: item.protectionUnknown, managed: item.managed };
238
- }));
333
+ return await readSkillCards(skills);
239
334
  },
240
335
  screeningForRun: engineScreening,
241
336
  stage: {
@@ -243,14 +338,22 @@ export function apply(ctx, rawConfig = {}) {
243
338
  unbind: (childId) => { stage.unbind(childId); },
244
339
  },
245
340
  });
246
- 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
+ };
247
349
  const command = createMergeCommand(runtime);
248
- scope.effect(() => commands.register(command), 'evolution-merge: command');
350
+ if (command !== null)
351
+ scope.effect(() => commands.register(command), 'evolution-merge: command');
249
352
  ctx.inject(['tools'], (toolScope) => {
250
353
  const tools = toolScope.get('tools');
251
354
  if (tools === undefined)
252
355
  return;
253
- const tool = createMergeStageTool(io, home, exec => stage.resolve(exec));
356
+ const tool = createMergeStageTool(io, home, exec => stage.resolve(exec), exec => stage.snapshot(exec));
254
357
  toolScope.effect(() => tools.register(tool), 'evolution-merge: merge_stage');
255
358
  });
256
359
  scope.effect(() => () => { runs.cancelAll(); }, 'evolution-merge: runs');
@@ -57,7 +57,7 @@ function payloadFor(level, candidate, facts) {
57
57
  }
58
58
  const SYSTEM = 'Judge whether two skill documents describe the same reusable capability. Shared vocabulary or a shared preamble is not duplication. Return only strict JSON: {"verdict":1|0|-1,"reason":"..."}';
59
59
  function extractJson(text) {
60
- const fenced = /\`\`\`(?:json)?\s*([\s\S]*?)\`\`\`/i.exec(text);
60
+ const fenced = /`\``(?:json)?\s*([\s\S]*?)`\``/i.exec(text);
61
61
  const body = fenced?.[1] ?? text;
62
62
  const start = body.indexOf('{');
63
63
  if (start < 0)
@@ -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);
@@ -1,10 +1,12 @@
1
1
  /**
2
- * Four read-only merge routes: view / rows / run / runs. The host loads rows and runs; these
3
- * handlers only project. A missing merge service answers 503 instead of pretending empty.
2
+ * Four read-only merge routes: view / rows / body / history.
3
+ * The host loads rows and runs; these handlers only project wire envelopes.
4
4
  */
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;