@lmzhen/dsh-evolution-merge 0.33.0 → 0.34.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/index.js CHANGED
@@ -3700,9 +3700,11 @@ function runView(run, readAt, includeResultRef = false) {
3700
3700
  const record = run.record;
3701
3701
  const state = run.meta?.phase === "undone" ? "undone" : run.meta?.stopReason === "interrupted" ? "interrupted" : run.meta?.approval === "pending" ? "awaiting-approval" : record.state;
3702
3702
  const stopReason = run.meta?.stopReason ?? (record.state === "cancelled" ? "cancelled" : record.state === "failed" ? "failed" : record.state === "succeeded" ? "completed" : void 0);
3703
- const progress = record.progress === void 0 ? null : {
3704
- done: state === "succeeded" || state === "undone" ? Math.max(record.progress.done, run.pairs.length) : record.progress.done,
3705
- total: record.progress.total
3703
+ const reportPairs = run.report?.processedPairs?.length ?? run.report?.pairs.length;
3704
+ const terminal = state === "succeeded" || state === "undone";
3705
+ const progress = record.progress === void 0 && reportPairs === void 0 ? null : {
3706
+ done: terminal ? Math.max(record.progress?.done ?? 0, reportPairs ?? 0, run.pairs.length) : record.progress?.done ?? reportPairs ?? 0,
3707
+ total: record.progress?.total ?? reportPairs ?? 0
3706
3708
  };
3707
3709
  return {
3708
3710
  wireVersion: 1,
@@ -5057,7 +5059,8 @@ const MERGE_ROUTES = {
5057
5059
  view: MERGE_PATHS.view,
5058
5060
  rows: MERGE_PATHS.rows,
5059
5061
  body: MERGE_PATHS.body ?? MERGE_BASE + "body",
5060
- history: MERGE_PATHS.history ?? MERGE_BASE + "history"
5062
+ history: MERGE_PATHS.history ?? MERGE_BASE + "history",
5063
+ decision: MERGE_BASE + "decision"
5061
5064
  };
5062
5065
  function makeMergeRoutes(deps, _audit) {
5063
5066
  const absent = () => json(404, {
@@ -5172,7 +5175,6 @@ function makeMergeRoutes(deps, _audit) {
5172
5175
  const id = query.get("id");
5173
5176
  const name = query.get("name");
5174
5177
  const side = query.get("side");
5175
- if (id !== null && name !== null) return bad("id and name are mutually exclusive");
5176
5178
  if ((id === null || id === "") && (name === null || name === "")) return json(404, {
5177
5179
  wireVersion: 1,
5178
5180
  code: "merge-body-not-found",
@@ -5185,21 +5187,21 @@ function makeMergeRoutes(deps, _audit) {
5185
5187
  code: "merge-state-blocked",
5186
5188
  readiness: snapshot.blocked.readiness
5187
5189
  });
5188
- const refs = id !== null && id !== "" ? snapshot.bodyRefs.filter((ref) => ref.rowId === id) : snapshot.bodyRefs.filter((ref) => ref.name === name);
5190
+ const sideFilter = side === "a" ? "live" : side === "b" ? "archive" : side === "live" || side === "archive" ? side : void 0;
5191
+ const refs = snapshot.bodyRefs.filter((ref) => (id === null || id === "" || ref.rowId === id) && (name === null || name === "" || ref.name === name) && (sideFilter === void 0 || ref.side === sideFilter));
5189
5192
  if (refs.length === 0) return json(404, {
5190
5193
  wireVersion: 1,
5191
5194
  code: "merge-body-not-found",
5192
5195
  message: "merge body not found"
5193
5196
  });
5194
- if (name !== null && refs.length > 1) return bad("name matches multiple merge rows");
5197
+ if (refs.length > 1) return bad("merge body is ambiguous; pass id, name and side");
5195
5198
  const row = snapshot.rows.find((item) => item.row.id === refs[0].rowId);
5196
5199
  if (row?.stage?.state === "lost" || row?.stage?.state === "expired") return json(404, {
5197
5200
  wireVersion: 1,
5198
5201
  code: "merge-body-not-found",
5199
5202
  message: "merge stage is lost"
5200
5203
  });
5201
- const sideFilter = side === "a" ? "live" : side === "b" ? "archive" : side === "live" || side === "archive" ? side : void 0;
5202
- const ref = (sideFilter === void 0 ? refs[0] : refs.find((item) => item.side === sideFilter)) ?? refs[0];
5204
+ const ref = refs[0];
5203
5205
  return json(200, buildBodyView(snapshot, ref, (source.bodyRef === void 0 ? null : await source.bodyRef(ref)) ?? ""));
5204
5206
  }
5205
5207
  const readiness = await notReady(source);
@@ -5235,6 +5237,35 @@ function makeMergeRoutes(deps, _audit) {
5235
5237
  if (readiness !== null) return readiness;
5236
5238
  return json(200, toHistoryView(await source.history?.() ?? [], Date.now()));
5237
5239
  }
5240
+ },
5241
+ {
5242
+ path: MERGE_ROUTES.decision,
5243
+ methods: ["POST"],
5244
+ requestBody: "buffered",
5245
+ fetch: async (request) => {
5246
+ if (request.method !== "POST") return methodNotAllowed(["POST"]);
5247
+ if (deps.decide === void 0) return absent();
5248
+ let body;
5249
+ try {
5250
+ body = await request.json();
5251
+ } catch {
5252
+ return bad("decision body must be JSON");
5253
+ }
5254
+ if (typeof body !== "object" || body === null) return bad("decision body must be an object");
5255
+ const input = body;
5256
+ if (typeof input.rowId !== "string" || input.rowId === "") return bad("rowId is required");
5257
+ if (input.verb !== "approve" && input.verb !== "reject") return bad("verb must be approve or reject");
5258
+ if (typeof input.sessionId !== "string" || input.sessionId === "") return bad("sessionId is required");
5259
+ const result = await deps.decide({
5260
+ rowId: input.rowId,
5261
+ verb: input.verb,
5262
+ sessionId: input.sessionId
5263
+ }, _audit);
5264
+ return json(result.ok ? 200 : result.code === "E-305" ? 409 : 400, {
5265
+ wireVersion: 1,
5266
+ ...result
5267
+ });
5268
+ }
5238
5269
  }
5239
5270
  ];
5240
5271
  }
@@ -9262,16 +9293,22 @@ function unitFor(deps) {
9262
9293
  function createMergeRuntime(deps) {
9263
9294
  const readOnly = createReadOnlyMergeService(deps.io, deps.home);
9264
9295
  const timeout = deps.timeoutMinutes > 0 ? deps.timeoutMinutes : void 0;
9265
- const common = {
9296
+ const paramsOf = () => {
9297
+ if (deps.getParams !== void 0) return deps.getParams();
9298
+ if (deps.params !== void 0) return deps.params;
9299
+ throw new Error("merge runtime requires getParams or params");
9300
+ };
9301
+ const platformWriteOf = () => deps.getPlatformWrite?.() ?? deps.platformWrite ?? false;
9302
+ const common = () => ({
9266
9303
  io: deps.io,
9267
9304
  home: deps.home,
9268
9305
  registry: deps.registry,
9269
- platformWrite: deps.platformWrite,
9306
+ platformWrite: platformWriteOf(),
9270
9307
  now: deps.now,
9271
9308
  ...deps.model === void 0 ? {} : { model: deps.model },
9272
9309
  ...deps.isOwnerActive === void 0 ? {} : { isOwnerActive: deps.isOwnerActive },
9273
9310
  ...timeout === void 0 ? {} : { timeoutMinutes: timeout }
9274
- };
9311
+ });
9275
9312
  const stage = deps.stage;
9276
9313
  const stageHooks = stage === void 0 ? {} : {
9277
9314
  onChild: (context) => {
@@ -9285,6 +9322,7 @@ function createMergeRuntime(deps) {
9285
9322
  const parent = context?.parent ?? deps.parent;
9286
9323
  const agentless = context?.agentless === true;
9287
9324
  const detached = deps.detached !== false;
9325
+ const params = paramsOf();
9288
9326
  if (command.verb === "pair") {
9289
9327
  const signal = new AbortController().signal;
9290
9328
  if (command.a === command.b) return fail("pair", "pair-same-source", "merge pair 需要两个不同技能源:" + command.a, "重新指定两个不同的 family-managed 技能");
@@ -9324,9 +9362,9 @@ function createMergeRuntime(deps) {
9324
9362
  if (existing?.state === "pending") return fail("pair", "pair-pending", "该 pair 已在待批准队列:" + existing.id, "先运行 /evolution merge approve/reject/discard " + existing.id, existing.runId);
9325
9363
  if (!cleared && (existing?.state === "approved" || existing?.state === "fuse")) return fail("pair", "pair-fuse", "该 pair 已在待合并队列,可复用决策重跑", "运行 /evolution merge resume --retry " + (existing.runId ?? existing.id), existing.runId);
9326
9364
  const forced = command.force === true ? forcedDecisionFor(command.a, command.b) : void 0;
9327
- const approval = deps.params.mergeApproval;
9365
+ const approval = params.mergeApproval;
9328
9366
  const result = await runScanCommand({
9329
- ...common,
9367
+ ...common(),
9330
9368
  approval,
9331
9369
  cards,
9332
9370
  ...detached ? { detached: true } : {},
@@ -9341,12 +9379,12 @@ function createMergeRuntime(deps) {
9341
9379
  parent,
9342
9380
  allow: deps.allow,
9343
9381
  ...deps.cwd === void 0 ? {} : { cwd: deps.cwd },
9344
- timeoutMs: deps.params.mergePairTimeoutMinutes * 6e4,
9382
+ timeoutMs: params.mergePairTimeoutMinutes * 6e4,
9345
9383
  approval,
9346
9384
  commitPort: skillLibraryPort(deps.skills),
9347
9385
  now: deps.now,
9348
9386
  ...unitFor(deps) === void 0 ? {} : { unit: unitFor(deps) },
9349
- ...deps.params.limitsSnapshot === void 0 ? {} : { limits: deps.params.limitsSnapshot.skillLimits },
9387
+ ...params.limitsSnapshot === void 0 ? {} : { limits: params.limitsSnapshot.skillLimits },
9350
9388
  ...deps.isBlocked === void 0 ? {} : { isBlocked: deps.isBlocked },
9351
9389
  ...stageHooks
9352
9390
  });
@@ -9380,12 +9418,12 @@ function createMergeRuntime(deps) {
9380
9418
  for (const card of cards) if (await deps.isBlocked?.(card.name) === true) blocked.add(card.name);
9381
9419
  const managed = new Set(cards.filter((card) => card.managed === true && card.protectedBy == null && !blocked.has(card.name)).map((card) => card.name));
9382
9420
  const loadedState = await loadMergeState(deps.io, deps.home);
9383
- const skipped = skippedPairKeys(loadedState.state.rows, deps.params.mergeSkippedTtlDays, deps.now());
9421
+ const skipped = skippedPairKeys(loadedState.state.rows, params.mergeSkippedTtlDays, deps.now());
9384
9422
  const held = new Set(loadedState.state.rows.filter((row) => row.queue === "pending" || row.queue === "fuse" || row.queue === "verify" && (row.runId !== void 0 && deps.now() - Date.parse(row.updatedAt) < 10 * 6e4 || !canRetry(row))).map((row) => row.pairKey));
9385
9423
  const notes = loadedState.state.rows.filter((row) => row.state === "pending" || row.state === "approved" || row.state === "fuse" || row.state === "skipped").map((row) => "conflict:" + row.state + ":" + row.pairKey + ":" + row.id);
9386
9424
  const result = await runScanCommand({
9387
- ...common,
9388
- approval: deps.params.mergeApproval,
9425
+ ...common(),
9426
+ approval: params.mergeApproval,
9389
9427
  notes,
9390
9428
  cards,
9391
9429
  ...detached ? { detached: true } : {},
@@ -9417,12 +9455,12 @@ function createMergeRuntime(deps) {
9417
9455
  parent,
9418
9456
  allow: deps.allow,
9419
9457
  ...deps.cwd === void 0 ? {} : { cwd: deps.cwd },
9420
- timeoutMs: deps.params.mergePairTimeoutMinutes * 6e4,
9421
- approval: deps.params.mergeApproval,
9458
+ timeoutMs: params.mergePairTimeoutMinutes * 6e4,
9459
+ approval: params.mergeApproval,
9422
9460
  commitPort: skillLibraryPort(deps.skills),
9423
9461
  now: deps.now,
9424
9462
  ...unitFor(deps) === void 0 ? {} : { unit: unitFor(deps) },
9425
- ...deps.params.limitsSnapshot === void 0 ? {} : { limits: deps.params.limitsSnapshot.skillLimits },
9463
+ ...params.limitsSnapshot === void 0 ? {} : { limits: params.limitsSnapshot.skillLimits },
9426
9464
  ...deps.isBlocked === void 0 ? {} : { isBlocked: deps.isBlocked },
9427
9465
  ...stageHooks
9428
9466
  })
@@ -9450,8 +9488,8 @@ function createMergeRuntime(deps) {
9450
9488
  b: row.b
9451
9489
  };
9452
9490
  const result = await runScanCommand({
9453
- ...common,
9454
- approval: deps.params.mergeApproval,
9491
+ ...common(),
9492
+ approval: params.mergeApproval,
9455
9493
  notes: ["candidates --retry " + row.id],
9456
9494
  cards,
9457
9495
  makePorts: (runId) => createChannelScanPorts({
@@ -9468,12 +9506,12 @@ function createMergeRuntime(deps) {
9468
9506
  parent,
9469
9507
  allow: deps.allow,
9470
9508
  ...deps.cwd === void 0 ? {} : { cwd: deps.cwd },
9471
- timeoutMs: deps.params.mergePairTimeoutMinutes * 6e4,
9472
- approval: deps.params.mergeApproval,
9509
+ timeoutMs: params.mergePairTimeoutMinutes * 6e4,
9510
+ approval: params.mergeApproval,
9473
9511
  commitPort: skillLibraryPort(deps.skills),
9474
9512
  now: deps.now,
9475
9513
  ...unitFor(deps) === void 0 ? {} : { unit: unitFor(deps) },
9476
- ...deps.params.limitsSnapshot === void 0 ? {} : { limits: deps.params.limitsSnapshot.skillLimits },
9514
+ ...params.limitsSnapshot === void 0 ? {} : { limits: params.limitsSnapshot.skillLimits },
9477
9515
  ...deps.isBlocked === void 0 ? {} : { isBlocked: deps.isBlocked },
9478
9516
  ...stageHooks
9479
9517
  })
@@ -9515,7 +9553,7 @@ function createMergeRuntime(deps) {
9515
9553
  io: deps.io,
9516
9554
  home: deps.home,
9517
9555
  now: deps.now,
9518
- ttlDays: deps.params.mergeSkippedTtlDays,
9556
+ ttlDays: params.mergeSkippedTtlDays,
9519
9557
  ...managed === void 0 ? {} : { managed },
9520
9558
  ...live === void 0 ? {} : { live }
9521
9559
  });
@@ -9530,7 +9568,7 @@ function createMergeRuntime(deps) {
9530
9568
  }
9531
9569
  if (command.verb === "doctor") return {
9532
9570
  ok: true,
9533
- text: (await mergeDoctor(deps.io, deps.home, deps.params.mergeModelsRoot)).map((row) => row.key + " [" + row.status + "] " + row.label + ":" + row.detail).join("\n")
9571
+ text: (await mergeDoctor(deps.io, deps.home, params.mergeModelsRoot)).map((row) => row.key + " [" + row.status + "] " + row.label + ":" + row.detail).join("\n")
9534
9572
  };
9535
9573
  if (command.verb === "report" || command.verb === "runs" || command.verb === "cancel") {
9536
9574
  const registry = deps.registry;
@@ -9542,7 +9580,7 @@ function createMergeRuntime(deps) {
9542
9580
  io: deps.io,
9543
9581
  home: deps.home,
9544
9582
  runs: mergeRuns,
9545
- approval: deps.params.mergeApproval,
9583
+ approval: params.mergeApproval,
9546
9584
  now: deps.now
9547
9585
  })))
9548
9586
  };
@@ -9658,13 +9696,13 @@ function createMergeRuntime(deps) {
9658
9696
  round2: () => failed("author", "resume-needs-child", "fuse resume needs a new child")
9659
9697
  };
9660
9698
  const result = await runResumeCommand({
9661
- ...common,
9699
+ ...common(),
9662
9700
  rowId: row.id,
9663
9701
  ports: basePorts,
9664
- approval: deps.params.mergeApproval,
9702
+ approval: params.mergeApproval,
9665
9703
  ...agentless ? { agentless: true } : {},
9666
9704
  ...detached ? { detached: true } : {},
9667
- pairTimeoutMinutes: deps.params.mergePairTimeoutMinutes,
9705
+ pairTimeoutMinutes: params.mergePairTimeoutMinutes,
9668
9706
  ...deps.isOwnerActive === void 0 ? {} : { isOwnerActive: deps.isOwnerActive },
9669
9707
  makePorts: (runId) => createChannelScanPorts({
9670
9708
  io: deps.io,
@@ -9676,12 +9714,12 @@ function createMergeRuntime(deps) {
9676
9714
  parent,
9677
9715
  allow: deps.allow,
9678
9716
  ...deps.cwd === void 0 ? {} : { cwd: deps.cwd },
9679
- timeoutMs: deps.params.mergePairTimeoutMinutes * 6e4,
9680
- approval: deps.params.mergeApproval,
9717
+ timeoutMs: params.mergePairTimeoutMinutes * 6e4,
9718
+ approval: params.mergeApproval,
9681
9719
  commitPort: skillLibraryPort(deps.skills),
9682
9720
  now: deps.now,
9683
9721
  ...unitFor(deps) === void 0 ? {} : { unit: unitFor(deps) },
9684
- ...deps.params.limitsSnapshot === void 0 ? {} : { limits: deps.params.limitsSnapshot.skillLimits },
9722
+ ...params.limitsSnapshot === void 0 ? {} : { limits: params.limitsSnapshot.skillLimits },
9685
9723
  ...deps.isBlocked === void 0 ? {} : { isBlocked: deps.isBlocked }
9686
9724
  }),
9687
9725
  round2WithRun: createNewChildRound2({
@@ -9693,11 +9731,11 @@ function createMergeRuntime(deps) {
9693
9731
  parentRunId: (runId) => runId,
9694
9732
  allow: deps.allow,
9695
9733
  ...deps.cwd === void 0 ? {} : { cwd: deps.cwd },
9696
- timeoutMs: deps.params.mergePairTimeoutMinutes * 6e4,
9734
+ timeoutMs: params.mergePairTimeoutMinutes * 6e4,
9697
9735
  commitPort: skillLibraryPort(deps.skills),
9698
9736
  now: deps.now,
9699
9737
  ...unitFor(deps) === void 0 ? {} : { unit: unitFor(deps) },
9700
- ...deps.params.limitsSnapshot === void 0 ? {} : { limits: deps.params.limitsSnapshot.skillLimits },
9738
+ ...params.limitsSnapshot === void 0 ? {} : { limits: params.limitsSnapshot.skillLimits },
9701
9739
  ...deps.isBlocked === void 0 ? {} : { isBlocked: deps.isBlocked },
9702
9740
  ...stageHooks
9703
9741
  })
@@ -9812,11 +9850,11 @@ function createMergeRuntime(deps) {
9812
9850
  }
9813
9851
  }
9814
9852
  const result = await runApproveCommand({
9815
- ...common,
9853
+ ...common(),
9816
9854
  rowId: command.id,
9817
9855
  ...agentless ? { agentless: true } : {},
9818
9856
  ...detached ? { detached: true } : {},
9819
- pairTimeoutMinutes: deps.params.mergePairTimeoutMinutes,
9857
+ pairTimeoutMinutes: params.mergePairTimeoutMinutes,
9820
9858
  ports: {
9821
9859
  stage: createStageForCards({
9822
9860
  io: deps.io,
@@ -9834,11 +9872,11 @@ function createMergeRuntime(deps) {
9834
9872
  parentRunId: (runId) => runId,
9835
9873
  allow: deps.allow,
9836
9874
  ...deps.cwd === void 0 ? {} : { cwd: deps.cwd },
9837
- timeoutMs: deps.params.mergePairTimeoutMinutes * 6e4,
9875
+ timeoutMs: params.mergePairTimeoutMinutes * 6e4,
9838
9876
  commitPort: skillLibraryPort(deps.skills),
9839
9877
  now: deps.now,
9840
9878
  ...unitFor(deps) === void 0 ? {} : { unit: unitFor(deps) },
9841
- ...deps.params.limitsSnapshot === void 0 ? {} : { limits: deps.params.limitsSnapshot.skillLimits },
9879
+ ...params.limitsSnapshot === void 0 ? {} : { limits: params.limitsSnapshot.skillLimits },
9842
9880
  ...deps.isBlocked === void 0 ? {} : { isBlocked: deps.isBlocked },
9843
9881
  ...stageHooks
9844
9882
  })
@@ -9865,7 +9903,7 @@ function createMergeRuntime(deps) {
9865
9903
  io: deps.io,
9866
9904
  home: deps.home,
9867
9905
  runs: runs !== void 0 && runs.some((item) => item.id === found.id) ? runs : [...runs ?? [], found],
9868
- approval: deps.params.mergeApproval,
9906
+ approval: params.mergeApproval,
9869
9907
  now: deps.now
9870
9908
  });
9871
9909
  if (snapshot.blocked !== void 0) return fail("status", snapshot.blocked.code, snapshot.blocked.message, "先修复合并状态(/evolution merge doctor),再查看运行");
@@ -9882,7 +9920,7 @@ function createMergeRuntime(deps) {
9882
9920
  const list = cards ?? await deps.cards(new AbortController().signal).catch(() => []);
9883
9921
  return list.length === 0 ? void 0 : liveRowFactsOf(list);
9884
9922
  };
9885
- const text = await handleReadOnlyMergeCommand(command, deps.io, deps.home, cards, runs, deps.params.mergeApproval, liveForRead);
9923
+ const text = await handleReadOnlyMergeCommand(command, deps.io, deps.home, cards, runs, params.mergeApproval, liveForRead);
9886
9924
  if (text !== null) return {
9887
9925
  ok: true,
9888
9926
  text
@@ -10546,8 +10584,9 @@ const inject = [];
10546
10584
  function apply(ctx, rawConfig = {}) {
10547
10585
  registerOwnerSettings(ctx, "evolution-merge", Config);
10548
10586
  configureMergePresentation(ctx);
10549
- let params = resolveMergeParams(rawConfig);
10587
+ const startupParams = resolveMergeParams(rawConfig);
10550
10588
  let mergeRun;
10589
+ let decide;
10551
10590
  let source;
10552
10591
  let doctorFact;
10553
10592
  const mergeService = {
@@ -10569,7 +10608,17 @@ function apply(ctx, rawConfig = {}) {
10569
10608
  };
10570
10609
  const mergeContext = ctx;
10571
10610
  if (typeof mergeContext.provide === "function") mergeContext.provide("evolutionMerge", mergeService);
10572
- registerHostRoutes(ctx, (audit) => makeMergeRoutes({ getSource: () => source }, audit));
10611
+ registerHostRoutes(ctx, (audit) => makeMergeRoutes({
10612
+ getSource: () => source,
10613
+ decide: async (request, _audit) => {
10614
+ if (decide === void 0) return {
10615
+ ok: false,
10616
+ code: "E-305",
10617
+ message: "合并服务尚未完成挂载"
10618
+ };
10619
+ return await decide(request);
10620
+ }
10621
+ }, audit));
10573
10622
  ctx.inject(["commands", "evolutionIo"], (scope) => {
10574
10623
  const registry = scope.get("evolutionIo");
10575
10624
  if (registry === void 0) return;
@@ -10620,13 +10669,15 @@ function apply(ctx, rawConfig = {}) {
10620
10669
  });
10621
10670
  }, "evolution-merge: restore supersede");
10622
10671
  const limitsSnapshot = resolveLimitsSnapshot(ctx.get("evolutionPolicy")?.get?.());
10623
- const hostApproval = ctx.get("evolutionApproval");
10624
- const platformWrite = typeof hostApproval?.platformWrite === "boolean" ? hostApproval.platformWrite : params.platformWrite ?? false;
10625
- params = {
10626
- ...params,
10627
- limitsSnapshot,
10628
- platformWrite
10672
+ const getPlatformWrite = () => {
10673
+ const hostApproval = ctx.get("evolutionApproval");
10674
+ return typeof hostApproval?.platformWrite === "boolean" ? hostApproval.platformWrite : resolveMergeParams(rawConfig).platformWrite ?? false;
10629
10675
  };
10676
+ const getParams = () => ({
10677
+ ...resolveMergeParams(rawConfig),
10678
+ limitsSnapshot,
10679
+ platformWrite: getPlatformWrite()
10680
+ });
10630
10681
  const skills = newSkillLibrary({
10631
10682
  io,
10632
10683
  ctx,
@@ -10669,7 +10720,7 @@ function apply(ctx, rawConfig = {}) {
10669
10720
  io,
10670
10721
  home,
10671
10722
  now: () => Date.now(),
10672
- ttlDays: params.mergeSkippedTtlDays,
10723
+ ttlDays: getParams().mergeSkippedTtlDays,
10673
10724
  ...live === void 0 ? {} : { live }
10674
10725
  });
10675
10726
  if (reconciled.actions.length > 0) ctx.logger?.info?.("merge reconcile: " + String(reconciled.actions.length) + " action(s)");
@@ -10706,7 +10757,7 @@ function apply(ctx, rawConfig = {}) {
10706
10757
  } : void 0;
10707
10758
  });
10708
10759
  const stage = new StageToolRegistry();
10709
- const engineInput = resolveEngineInput({ modelsRoot: params.mergeModelsRoot });
10760
+ const engineInput = resolveEngineInput({ modelsRoot: startupParams.mergeModelsRoot });
10710
10761
  const engineConfig = engineConfigFromRoot(engineInput.modelsRoot);
10711
10762
  const llm = ctx.get("llm");
10712
10763
  const defaultModel = ctx.get("agentDefaultModel");
@@ -10762,12 +10813,13 @@ function apply(ctx, rawConfig = {}) {
10762
10813
  });
10763
10814
  source = {
10764
10815
  async snapshot() {
10816
+ const current = getParams();
10765
10817
  return await readMergeSnapshot({
10766
10818
  io,
10767
10819
  home,
10768
10820
  runs: runs.runs("merge"),
10769
- approval: params.mergeApproval,
10770
- platformWrite,
10821
+ approval: current.mergeApproval,
10822
+ platformWrite: getPlatformWrite(),
10771
10823
  hostOnly: hostCapabilities.hostOnly,
10772
10824
  live: async () => liveRowFactsOf(await readSkillCards(skills)),
10773
10825
  history: async () => {
@@ -10819,7 +10871,7 @@ function apply(ctx, rawConfig = {}) {
10819
10871
  return await resolveBody(io, home, ref, async () => ref.side === "archive" ? await skills.readArchived(ref.name) : await skills.read(ref.name));
10820
10872
  },
10821
10873
  approval() {
10822
- return params.mergeApproval;
10874
+ return getParams().mergeApproval;
10823
10875
  },
10824
10876
  readiness() {
10825
10877
  return ready;
@@ -10855,13 +10907,13 @@ function apply(ctx, rawConfig = {}) {
10855
10907
  registry: runs,
10856
10908
  skills,
10857
10909
  now: () => Date.now(),
10858
- params,
10859
- platformWrite,
10910
+ getParams,
10911
+ getPlatformWrite,
10860
10912
  ...defaultModel === void 0 ? {} : { model: defaultModel.currentSelection().model },
10861
10913
  channel,
10862
10914
  parent: void 0,
10863
10915
  allow: ["merge_stage"],
10864
- timeoutMinutes: params.mergeRunTimeoutMinutes,
10916
+ timeoutMinutes: startupParams.mergeRunTimeoutMinutes,
10865
10917
  hostCapabilities,
10866
10918
  isBlocked: async (name) => {
10867
10919
  return await ctx.get("evolutionCurator")?.isBlocked?.(name) === true;
@@ -10893,6 +10945,20 @@ function apply(ctx, rawConfig = {}) {
10893
10945
  };
10894
10946
  return runMergeCommand(runtime, rawInput, invocation);
10895
10947
  };
10948
+ const agents = ctx.get("agents");
10949
+ decide = async ({ rowId, verb, sessionId }) => {
10950
+ const agent = agents?.get(sessionId);
10951
+ if (agent === void 0 || agent === null) return {
10952
+ ok: false,
10953
+ code: "E-305",
10954
+ message: "当前会话没有可用的 Agent;请在会话打开时操作,或复制命令到 CLI 执行"
10955
+ };
10956
+ const result = await mergeService.run("/evolution merge " + verb + " " + rowId, { agent });
10957
+ return {
10958
+ ok: result.ok,
10959
+ message: result.text
10960
+ };
10961
+ };
10896
10962
  const command = createMergeCommand(runtime);
10897
10963
  if (command !== null) scope.effect(() => commands.register(command), "evolution-merge: command");
10898
10964
  ctx.inject(["tools"], (toolScope) => {
@@ -137,8 +137,9 @@ export const inject = [];
137
137
  export function apply(ctx, rawConfig = {}) {
138
138
  registerOwnerSettings(ctx, 'evolution-merge', Config);
139
139
  configureMergePresentation(ctx);
140
- let params = resolveMergeParams(rawConfig);
140
+ const startupParams = resolveMergeParams(rawConfig);
141
141
  let mergeRun;
142
+ let decide;
142
143
  let source;
143
144
  let doctorFact;
144
145
  const mergeService = {
@@ -157,7 +158,14 @@ export function apply(ctx, rawConfig = {}) {
157
158
  ;
158
159
  mergeContext.provide('evolutionMerge', mergeService);
159
160
  }
160
- registerHostRoutes(ctx, audit => makeMergeRoutes({ getSource: () => source }, audit));
161
+ registerHostRoutes(ctx, audit => makeMergeRoutes({
162
+ getSource: () => source,
163
+ decide: async (request, _audit) => {
164
+ if (decide === undefined)
165
+ return { ok: false, code: 'E-305', message: '合并服务尚未完成挂载' };
166
+ return await decide(request);
167
+ },
168
+ }, audit));
161
169
  ctx.inject(['commands', 'evolutionIo'], (scope) => {
162
170
  const registry = scope.get('evolutionIo');
163
171
  if (registry === undefined)
@@ -188,9 +196,11 @@ export function apply(ctx, rawConfig = {}) {
188
196
  }, 'evolution-merge: restore supersede');
189
197
  const policySnapshot = ctx.get('evolutionPolicy');
190
198
  const limitsSnapshot = resolveLimitsSnapshot(policySnapshot?.get?.());
191
- const hostApproval = ctx.get('evolutionApproval');
192
- const platformWrite = typeof hostApproval?.platformWrite === 'boolean' ? hostApproval.platformWrite : (params.platformWrite ?? false);
193
- params = { ...params, limitsSnapshot, platformWrite };
199
+ const getPlatformWrite = () => {
200
+ const hostApproval = ctx.get('evolutionApproval');
201
+ return typeof hostApproval?.platformWrite === 'boolean' ? hostApproval.platformWrite : (resolveMergeParams(rawConfig).platformWrite ?? false);
202
+ };
203
+ const getParams = () => ({ ...resolveMergeParams(rawConfig), limitsSnapshot, platformWrite: getPlatformWrite() });
194
204
  const skills = newSkillLibrary({
195
205
  io, ctx, authority: authorityFrom({ origin: 'subagent' }),
196
206
  limits: limitsSnapshot.skillLimits,
@@ -225,7 +235,7 @@ export function apply(ctx, rawConfig = {}) {
225
235
  const pending = v4.state.rows.some(row => row.state === 'pending');
226
236
  const library = pending ? await cards().catch(() => []) : [];
227
237
  const live = library.length === 0 ? undefined : liveRowFactsOf(library);
228
- const reconciled = await reconcileMergeState({ io, home, now: () => Date.now(), ttlDays: params.mergeSkippedTtlDays, ...(live === undefined ? {} : { live }) });
238
+ const reconciled = await reconcileMergeState({ io, home, now: () => Date.now(), ttlDays: getParams().mergeSkippedTtlDays, ...(live === undefined ? {} : { live }) });
229
239
  if (reconciled.actions.length > 0)
230
240
  ctx.logger?.info?.('merge reconcile: ' + String(reconciled.actions.length) + ' action(s)');
231
241
  await transactIo(io, reportsSweepLockTarget(), async () => {
@@ -242,7 +252,7 @@ export function apply(ctx, rawConfig = {}) {
242
252
  return state.kind === 'unavailable' ? { code: 'E-323', detail: state.reason } : undefined;
243
253
  });
244
254
  const stage = new StageToolRegistry();
245
- const engineInput = resolveEngineInput({ modelsRoot: params.mergeModelsRoot });
255
+ const engineInput = resolveEngineInput({ modelsRoot: startupParams.mergeModelsRoot });
246
256
  const engineConfig = engineConfigFromRoot(engineInput.modelsRoot);
247
257
  const llm = ctx.get('llm');
248
258
  const defaultModel = ctx.get('agentDefaultModel');
@@ -283,7 +293,8 @@ export function apply(ctx, rawConfig = {}) {
283
293
  });
284
294
  source = {
285
295
  async snapshot() {
286
- return await readMergeSnapshot({ io, home, runs: runs.runs('merge'), approval: params.mergeApproval, platformWrite, hostOnly: hostCapabilities.hostOnly, live: async () => liveRowFactsOf(await readSkillCards(skills)), history: async () => {
296
+ const current = getParams();
297
+ return await readMergeSnapshot({ io, home, runs: runs.runs('merge'), approval: current.mergeApproval, platformWrite: getPlatformWrite(), hostOnly: hostCapabilities.hostOnly, live: async () => liveRowFactsOf(await readSkillCards(skills)), history: async () => {
287
298
  const items = new Map();
288
299
  const receipts = [...await readCommitReceipts(io, home)].sort((a, b) => a.at - b.at);
289
300
  const generationOf = new Map(receipts.map((receipt, index) => [receipt.runId, index + 1]));
@@ -310,7 +321,7 @@ export function apply(ctx, rawConfig = {}) {
310
321
  } });
311
322
  },
312
323
  async bodyRef(ref) { return await resolveBody(io, home, ref, async () => ref.side === 'archive' ? await skills.readArchived(ref.name) : await skills.read(ref.name)); },
313
- approval() { return params.mergeApproval; },
324
+ approval() { return getParams().mergeApproval; },
314
325
  readiness() { return ready; },
315
326
  commands() {
316
327
  return ['merge scan', 'merge doctor', 'merge status', 'merge pending', 'merge skipped', 'merge report', 'merge runs', 'merge cancel', 'merge reconcile'];
@@ -327,9 +338,9 @@ export function apply(ctx, rawConfig = {}) {
327
338
  if (commands === undefined)
328
339
  return;
329
340
  const runtime = createMergeRuntime({
330
- io, home, registry: runs, skills, now: () => Date.now(), params, platformWrite,
341
+ io, home, registry: runs, skills, now: () => Date.now(), getParams, getPlatformWrite,
331
342
  ...(defaultModel === undefined ? {} : { model: defaultModel.currentSelection().model }),
332
- channel, parent: undefined, allow: ['merge_stage'], timeoutMinutes: params.mergeRunTimeoutMinutes,
343
+ channel, parent: undefined, allow: ['merge_stage'], timeoutMinutes: startupParams.mergeRunTimeoutMinutes,
333
344
  hostCapabilities,
334
345
  isBlocked: async (name) => {
335
346
  const curator = ctx.get('evolutionCurator');
@@ -358,6 +369,15 @@ export function apply(ctx, rawConfig = {}) {
358
369
  return { ok: false, text: formatMergeError(commandError('merge', state.code, state.message, '先修复合并状态(/evolution merge doctor),再重试')) };
359
370
  return runMergeCommand(runtime, rawInput, invocation);
360
371
  };
372
+ const agents = ctx.get('agents');
373
+ decide = async ({ rowId, verb, sessionId }) => {
374
+ const agent = agents?.get(sessionId);
375
+ if (agent === undefined || agent === null) {
376
+ return { ok: false, code: 'E-305', message: '当前会话没有可用的 Agent;请在会话打开时操作,或复制命令到 CLI 执行' };
377
+ }
378
+ const result = await mergeService.run('/evolution merge ' + verb + ' ' + rowId, { agent });
379
+ return { ok: result.ok, message: result.text };
380
+ };
361
381
  const command = createMergeCommand(runtime);
362
382
  if (command !== null)
363
383
  scope.effect(() => commands.register(command), 'evolution-merge: command');
@@ -12,7 +12,19 @@ export declare const MERGE_ROUTES: {
12
12
  rows: `/api/dsh-evolution/${string}`;
13
13
  body: `/api/dsh-evolution/${string}`;
14
14
  history: `/api/dsh-evolution/${string}`;
15
+ decision: `/api/dsh-evolution/${string}`;
15
16
  };
17
+ export interface MergeDecisionRequest {
18
+ readonly rowId: string;
19
+ readonly verb: 'approve' | 'reject';
20
+ readonly sessionId: string;
21
+ }
22
+ export interface MergeDecisionResult {
23
+ readonly ok: boolean;
24
+ readonly message: string;
25
+ readonly code?: string;
26
+ readonly runId?: string;
27
+ }
16
28
  export interface MergeHistoryItem {
17
29
  readonly name: string;
18
30
  readonly kind: 'merged' | 'deleted' | 'split' | 'other';
@@ -35,5 +47,6 @@ export interface MergeRouteSource {
35
47
  }
36
48
  export declare function makeMergeRoutes(deps: {
37
49
  getSource: () => MergeRouteSource | undefined;
50
+ decide?: (request: MergeDecisionRequest, audit: DecisionAudit) => Promise<MergeDecisionResult>;
38
51
  }, _audit: DecisionAudit): ConnectionFetchRouteLike[];
39
52
  //# sourceMappingURL=merge-routes.d.ts.map
@@ -9,6 +9,7 @@ export const MERGE_ROUTES = {
9
9
  rows: MERGE_PATHS.rows,
10
10
  body: (MERGE_PATHS.body ?? MERGE_BASE + 'body'),
11
11
  history: (MERGE_PATHS.history ?? MERGE_BASE + 'history'),
12
+ decision: (MERGE_BASE + 'decision'),
12
13
  };
13
14
  export function makeMergeRoutes(deps, _audit) {
14
15
  const absent = () => json(404, { wireVersion: 1, code: 'E-325', message: 'merge service is not mounted' });
@@ -110,8 +111,6 @@ export function makeMergeRoutes(deps, _audit) {
110
111
  const id = query.get('id');
111
112
  const name = query.get('name');
112
113
  const side = query.get('side');
113
- if (id !== null && name !== null)
114
- return bad('id and name are mutually exclusive');
115
114
  if ((id === null || id === '') && (name === null || name === ''))
116
115
  return json(404, { wireVersion: 1, code: 'merge-body-not-found', message: 'merge body requires id or name' });
117
116
  if (source.snapshot !== undefined) {
@@ -120,16 +119,18 @@ export function makeMergeRoutes(deps, _audit) {
120
119
  return json(503, { wireVersion: 1, code: 'merge-state-blocked', readiness: snapshot.blocked.readiness });
121
120
  // Body/history are read-only projections of already-written files; a host without
122
121
  // the live agent data plane can still read past reports and archived bodies.
123
- const refs = id !== null && id !== '' ? snapshot.bodyRefs.filter(ref => ref.rowId === id) : snapshot.bodyRefs.filter(ref => ref.name === name);
122
+ const sideFilter = side === 'a' ? 'live' : side === 'b' ? 'archive' : side === 'live' || side === 'archive' ? side : undefined;
123
+ const refs = snapshot.bodyRefs.filter(ref => (id === null || id === '' || ref.rowId === id)
124
+ && (name === null || name === '' || ref.name === name)
125
+ && (sideFilter === undefined || ref.side === sideFilter));
124
126
  if (refs.length === 0)
125
127
  return json(404, { wireVersion: 1, code: 'merge-body-not-found', message: 'merge body not found' });
126
- if (name !== null && refs.length > 1)
127
- return bad('name matches multiple merge rows');
128
+ if (refs.length > 1)
129
+ return bad('merge body is ambiguous; pass id, name and side');
128
130
  const row = snapshot.rows.find(item => item.row.id === refs[0].rowId);
129
131
  if (row?.stage?.state === 'lost' || row?.stage?.state === 'expired')
130
132
  return json(404, { wireVersion: 1, code: 'merge-body-not-found', message: 'merge stage is lost' });
131
- const sideFilter = side === 'a' ? 'live' : side === 'b' ? 'archive' : side === 'live' || side === 'archive' ? side : undefined;
132
- const ref = (sideFilter === undefined ? refs[0] : refs.find(item => item.side === sideFilter)) ?? refs[0];
133
+ const ref = refs[0];
133
134
  const content = source.bodyRef === undefined ? null : await source.bodyRef(ref);
134
135
  return json(200, buildBodyView(snapshot, ref, content ?? ''));
135
136
  }
@@ -167,6 +168,35 @@ export function makeMergeRoutes(deps, _audit) {
167
168
  return json(200, toHistoryView(await source.history?.() ?? [], Date.now()));
168
169
  },
169
170
  },
171
+ {
172
+ path: MERGE_ROUTES.decision,
173
+ methods: ['POST'],
174
+ requestBody: 'buffered',
175
+ fetch: async (request) => {
176
+ if (request.method !== 'POST')
177
+ return methodNotAllowed(['POST']);
178
+ if (deps.decide === undefined)
179
+ return absent();
180
+ let body;
181
+ try {
182
+ body = await request.json();
183
+ }
184
+ catch {
185
+ return bad('decision body must be JSON');
186
+ }
187
+ if (typeof body !== 'object' || body === null)
188
+ return bad('decision body must be an object');
189
+ const input = body;
190
+ if (typeof input.rowId !== 'string' || input.rowId === '')
191
+ return bad('rowId is required');
192
+ if (input.verb !== 'approve' && input.verb !== 'reject')
193
+ return bad('verb must be approve or reject');
194
+ if (typeof input.sessionId !== 'string' || input.sessionId === '')
195
+ return bad('sessionId is required');
196
+ const result = await deps.decide({ rowId: input.rowId, verb: input.verb, sessionId: input.sessionId }, _audit);
197
+ return json(result.ok ? 200 : result.code === 'E-305' ? 409 : 400, { wireVersion: 1, ...result });
198
+ },
199
+ },
170
200
  ];
171
201
  }
172
202
  //# sourceMappingURL=merge-routes.js.map
@@ -18,8 +18,13 @@ export interface MergeRuntimeDeps {
18
18
  readonly registry: MergeRegistryPort;
19
19
  readonly skills: SkillLibrary;
20
20
  readonly now: () => number;
21
- readonly params: MergeParams;
22
- readonly platformWrite: boolean;
21
+ /** Live params: re-resolve at each operation, never cache an apply-time value. */
22
+ readonly getParams?: () => MergeParams;
23
+ /** Legacy fixture shape; used only when getParams is absent. */
24
+ readonly params?: MergeParams;
25
+ readonly getPlatformWrite?: () => boolean;
26
+ /** Legacy fixture shape; used only when getPlatformWrite is absent. */
27
+ readonly platformWrite?: boolean;
23
28
  readonly channel: ConversationChannelLike;
24
29
  readonly hostCapabilities?: HostCapabilities;
25
30
  readonly parent: unknown;
@@ -78,13 +78,21 @@ export function unitFor(deps) {
78
78
  export function createMergeRuntime(deps) {
79
79
  const readOnly = createReadOnlyMergeService(deps.io, deps.home);
80
80
  const timeout = deps.timeoutMinutes > 0 ? deps.timeoutMinutes : undefined;
81
- const common = {
81
+ const paramsOf = () => {
82
+ if (deps.getParams !== undefined)
83
+ return deps.getParams();
84
+ if (deps.params !== undefined)
85
+ return deps.params;
86
+ throw new Error('merge runtime requires getParams or params');
87
+ };
88
+ const platformWriteOf = () => deps.getPlatformWrite?.() ?? deps.platformWrite ?? false;
89
+ const common = () => ({
82
90
  io: deps.io, home: deps.home, registry: deps.registry,
83
- platformWrite: deps.platformWrite, now: deps.now,
91
+ platformWrite: platformWriteOf(), now: deps.now,
84
92
  ...(deps.model === undefined ? {} : { model: deps.model }),
85
93
  ...(deps.isOwnerActive === undefined ? {} : { isOwnerActive: deps.isOwnerActive }),
86
94
  ...(timeout === undefined ? {} : { timeoutMinutes: timeout }),
87
- };
95
+ });
88
96
  const stage = deps.stage;
89
97
  const stageHooks = stage === undefined ? {} : {
90
98
  onChild: (context) => { stage.bind(context); },
@@ -95,6 +103,7 @@ export function createMergeRuntime(deps) {
95
103
  const parent = context?.parent ?? deps.parent;
96
104
  const agentless = context?.agentless === true;
97
105
  const detached = deps.detached !== false;
106
+ const params = paramsOf();
98
107
  if (command.verb === 'pair') {
99
108
  const signal = new AbortController().signal;
100
109
  if (command.a === command.b)
@@ -155,9 +164,9 @@ export function createMergeRuntime(deps) {
155
164
  return fail('pair', 'pair-fuse', '该 pair 已在待合并队列,可复用决策重跑', '运行 /evolution merge resume --retry ' + (existing.runId ?? existing.id), existing.runId);
156
165
  }
157
166
  const forced = command.force === true ? forcedDecisionFor(command.a, command.b) : undefined;
158
- const approval = deps.params.mergeApproval;
167
+ const approval = params.mergeApproval;
159
168
  const result = await runScanCommand({
160
- ...common,
169
+ ...common(),
161
170
  approval,
162
171
  cards,
163
172
  ...(detached ? { detached: true } : {}),
@@ -167,10 +176,10 @@ export function createMergeRuntime(deps) {
167
176
  screening: () => Promise.resolve(pair),
168
177
  channel: deps.channel, parent, allow: deps.allow,
169
178
  ...(deps.cwd === undefined ? {} : { cwd: deps.cwd }),
170
- timeoutMs: deps.params.mergePairTimeoutMinutes * 60_000,
179
+ timeoutMs: params.mergePairTimeoutMinutes * 60_000,
171
180
  approval,
172
181
  commitPort: skillLibraryPort(deps.skills), now: deps.now, ...(unitFor(deps) === undefined ? {} : { unit: unitFor(deps) }),
173
- ...(deps.params.limitsSnapshot === undefined ? {} : { limits: deps.params.limitsSnapshot.skillLimits }),
182
+ ...(params.limitsSnapshot === undefined ? {} : { limits: params.limitsSnapshot.skillLimits }),
174
183
  ...(deps.isBlocked === undefined ? {} : { isBlocked: deps.isBlocked }),
175
184
  ...stageHooks,
176
185
  });
@@ -201,7 +210,7 @@ export function createMergeRuntime(deps) {
201
210
  .filter(card => card.managed === true && card.protectedBy == null && !blocked.has(card.name))
202
211
  .map(card => card.name));
203
212
  const loadedState = await loadMergeState(deps.io, deps.home);
204
- const skipped = skippedPairKeys(loadedState.state.rows, deps.params.mergeSkippedTtlDays, deps.now());
213
+ const skipped = skippedPairKeys(loadedState.state.rows, params.mergeSkippedTtlDays, deps.now());
205
214
  const held = new Set(loadedState.state.rows.filter(row => row.queue === 'pending' || row.queue === 'fuse'
206
215
  || (row.queue === 'verify' && (row.runId !== undefined && deps.now() - Date.parse(row.updatedAt) < 10 * 60_000 || !canRetry(row))))
207
216
  .map(row => row.pairKey));
@@ -209,8 +218,8 @@ export function createMergeRuntime(deps) {
209
218
  .filter(row => row.state === 'pending' || row.state === 'approved' || row.state === 'fuse' || row.state === 'skipped')
210
219
  .map(row => 'conflict:' + row.state + ':' + row.pairKey + ':' + row.id);
211
220
  const result = await runScanCommand({
212
- ...common,
213
- approval: deps.params.mergeApproval,
221
+ ...common(),
222
+ approval: params.mergeApproval,
214
223
  notes,
215
224
  cards,
216
225
  ...(detached ? { detached: true } : {}),
@@ -238,10 +247,10 @@ export function createMergeRuntime(deps) {
238
247
  },
239
248
  channel: deps.channel, parent, allow: deps.allow,
240
249
  ...(deps.cwd === undefined ? {} : { cwd: deps.cwd }),
241
- timeoutMs: deps.params.mergePairTimeoutMinutes * 60_000,
242
- approval: deps.params.mergeApproval,
250
+ timeoutMs: params.mergePairTimeoutMinutes * 60_000,
251
+ approval: params.mergeApproval,
243
252
  commitPort: skillLibraryPort(deps.skills), now: deps.now, ...(unitFor(deps) === undefined ? {} : { unit: unitFor(deps) }),
244
- ...(deps.params.limitsSnapshot === undefined ? {} : { limits: deps.params.limitsSnapshot.skillLimits }),
253
+ ...(params.limitsSnapshot === undefined ? {} : { limits: params.limitsSnapshot.skillLimits }),
245
254
  ...(deps.isBlocked === undefined ? {} : { isBlocked: deps.isBlocked }),
246
255
  ...stageHooks,
247
256
  }),
@@ -267,8 +276,8 @@ export function createMergeRuntime(deps) {
267
276
  const cards = await deps.cards(signal);
268
277
  const pairCandidate = { a: row.a, b: row.b };
269
278
  const result = await runScanCommand({
270
- ...common,
271
- approval: deps.params.mergeApproval,
279
+ ...common(),
280
+ approval: params.mergeApproval,
272
281
  notes: ['candidates --retry ' + row.id],
273
282
  cards,
274
283
  makePorts: runId => createChannelScanPorts({
@@ -280,10 +289,10 @@ export function createMergeRuntime(deps) {
280
289
  },
281
290
  channel: deps.channel, parent, allow: deps.allow,
282
291
  ...(deps.cwd === undefined ? {} : { cwd: deps.cwd }),
283
- timeoutMs: deps.params.mergePairTimeoutMinutes * 60_000,
284
- approval: deps.params.mergeApproval,
292
+ timeoutMs: params.mergePairTimeoutMinutes * 60_000,
293
+ approval: params.mergeApproval,
285
294
  commitPort: skillLibraryPort(deps.skills), now: deps.now, ...(unitFor(deps) === undefined ? {} : { unit: unitFor(deps) }),
286
- ...(deps.params.limitsSnapshot === undefined ? {} : { limits: deps.params.limitsSnapshot.skillLimits }),
295
+ ...(params.limitsSnapshot === undefined ? {} : { limits: params.limitsSnapshot.skillLimits }),
287
296
  ...(deps.isBlocked === undefined ? {} : { isBlocked: deps.isBlocked }),
288
297
  ...stageHooks,
289
298
  }),
@@ -318,7 +327,7 @@ export function createMergeRuntime(deps) {
318
327
  const managed = cards === undefined || cards.length === 0 ? undefined : new Set(cards.map(card => card.name));
319
328
  const live = cards === undefined || cards.length === 0 ? undefined : liveRowFactsOf(cards);
320
329
  try {
321
- const report = await reconcileMergeState({ io: deps.io, home: deps.home, now: deps.now, ttlDays: deps.params.mergeSkippedTtlDays, ...(managed === undefined ? {} : { managed }), ...(live === undefined ? {} : { live }) });
330
+ const report = await reconcileMergeState({ io: deps.io, home: deps.home, now: deps.now, ttlDays: params.mergeSkippedTtlDays, ...(managed === undefined ? {} : { managed }), ...(live === undefined ? {} : { live }) });
322
331
  if (report.state === 'blocked')
323
332
  return fail('reconcile', report.code ?? 'merge-state-blocked', report.message ?? '合并状态不可读', '先修复合并状态,再运行 /evolution merge doctor');
324
333
  return { ok: true, text: formatReconcileReport(report) };
@@ -328,14 +337,14 @@ export function createMergeRuntime(deps) {
328
337
  }
329
338
  }
330
339
  if (command.verb === 'doctor') {
331
- const rows = await mergeDoctor(deps.io, deps.home, deps.params.mergeModelsRoot);
340
+ const rows = await mergeDoctor(deps.io, deps.home, params.mergeModelsRoot);
332
341
  return { ok: true, text: rows.map(row => row.key + ' [' + row.status + '] ' + row.label + ':' + row.detail).join('\n') };
333
342
  }
334
343
  if (command.verb === 'report' || command.verb === 'runs' || command.verb === 'cancel') {
335
344
  const registry = deps.registry;
336
345
  if (command.verb === 'runs') {
337
346
  const mergeRuns = registry.runs('merge');
338
- const snapshot = await readMergeSnapshot({ io: deps.io, home: deps.home, runs: mergeRuns, approval: deps.params.mergeApproval, now: deps.now });
347
+ const snapshot = await readMergeSnapshot({ io: deps.io, home: deps.home, runs: mergeRuns, approval: params.mergeApproval, now: deps.now });
339
348
  return { ok: true, text: formatRunsView(buildRunsView(snapshot)) };
340
349
  }
341
350
  if (command.verb === 'cancel') {
@@ -443,27 +452,27 @@ export function createMergeRuntime(deps) {
443
452
  round2: () => failed('author', 'resume-needs-child', 'fuse resume needs a new child'),
444
453
  };
445
454
  const result = await runResumeCommand({
446
- ...common, rowId: row.id, ports: basePorts, approval: deps.params.mergeApproval,
455
+ ...common(), rowId: row.id, ports: basePorts, approval: params.mergeApproval,
447
456
  ...(agentless ? { agentless: true } : {}),
448
457
  ...(detached ? { detached: true } : {}),
449
- pairTimeoutMinutes: deps.params.mergePairTimeoutMinutes,
458
+ pairTimeoutMinutes: params.mergePairTimeoutMinutes,
450
459
  ...(deps.isOwnerActive === undefined ? {} : { isOwnerActive: deps.isOwnerActive }),
451
460
  makePorts: runId => createChannelScanPorts({
452
461
  io: deps.io, home: deps.home, runId, cards, screening: pair,
453
462
  channel: deps.channel, parent, allow: deps.allow,
454
463
  ...(deps.cwd === undefined ? {} : { cwd: deps.cwd }),
455
- timeoutMs: deps.params.mergePairTimeoutMinutes * 60_000,
456
- approval: deps.params.mergeApproval, commitPort: skillLibraryPort(deps.skills), now: deps.now, ...(unitFor(deps) === undefined ? {} : { unit: unitFor(deps) }),
457
- ...(deps.params.limitsSnapshot === undefined ? {} : { limits: deps.params.limitsSnapshot.skillLimits }),
464
+ timeoutMs: params.mergePairTimeoutMinutes * 60_000,
465
+ approval: params.mergeApproval, commitPort: skillLibraryPort(deps.skills), now: deps.now, ...(unitFor(deps) === undefined ? {} : { unit: unitFor(deps) }),
466
+ ...(params.limitsSnapshot === undefined ? {} : { limits: params.limitsSnapshot.skillLimits }),
458
467
  ...(deps.isBlocked === undefined ? {} : { isBlocked: deps.isBlocked }),
459
468
  }),
460
469
  round2WithRun: createNewChildRound2({
461
470
  io: deps.io, home: deps.home, cards, channel: deps.channel, parent,
462
471
  parentRunId: (runId) => runId,
463
472
  allow: deps.allow, ...(deps.cwd === undefined ? {} : { cwd: deps.cwd }),
464
- timeoutMs: deps.params.mergePairTimeoutMinutes * 60_000,
473
+ timeoutMs: params.mergePairTimeoutMinutes * 60_000,
465
474
  commitPort: skillLibraryPort(deps.skills), now: deps.now, ...(unitFor(deps) === undefined ? {} : { unit: unitFor(deps) }),
466
- ...(deps.params.limitsSnapshot === undefined ? {} : { limits: deps.params.limitsSnapshot.skillLimits }),
475
+ ...(params.limitsSnapshot === undefined ? {} : { limits: params.limitsSnapshot.skillLimits }),
467
476
  ...(deps.isBlocked === undefined ? {} : { isBlocked: deps.isBlocked }),
468
477
  ...stageHooks,
469
478
  }),
@@ -563,20 +572,20 @@ export function createMergeRuntime(deps) {
563
572
  }
564
573
  }
565
574
  const result = await runApproveCommand({
566
- ...common,
575
+ ...common(),
567
576
  rowId: command.id,
568
577
  ...(agentless ? { agentless: true } : {}),
569
578
  ...(detached ? { detached: true } : {}),
570
- pairTimeoutMinutes: deps.params.mergePairTimeoutMinutes,
579
+ pairTimeoutMinutes: params.mergePairTimeoutMinutes,
571
580
  ports: {
572
581
  stage: createStageForCards({ io: deps.io, home: deps.home, runId: command.id, cards, now: deps.now }),
573
582
  round2: createNewChildRound2({
574
583
  io: deps.io, home: deps.home, cards, channel: deps.channel, parent,
575
584
  parentRunId: (runId) => runId,
576
585
  allow: deps.allow, ...(deps.cwd === undefined ? {} : { cwd: deps.cwd }),
577
- timeoutMs: deps.params.mergePairTimeoutMinutes * 60_000,
586
+ timeoutMs: params.mergePairTimeoutMinutes * 60_000,
578
587
  commitPort: skillLibraryPort(deps.skills), now: deps.now, ...(unitFor(deps) === undefined ? {} : { unit: unitFor(deps) }),
579
- ...(deps.params.limitsSnapshot === undefined ? {} : { limits: deps.params.limitsSnapshot.skillLimits }),
588
+ ...(params.limitsSnapshot === undefined ? {} : { limits: params.limitsSnapshot.skillLimits }),
580
589
  ...(deps.isBlocked === undefined ? {} : { isBlocked: deps.isBlocked }),
581
590
  ...stageHooks,
582
591
  }),
@@ -597,7 +606,7 @@ export function createMergeRuntime(deps) {
597
606
  const found = runs?.find(item => item.id === command.id) ?? registry.find(command.id);
598
607
  if (found !== undefined && found.kind === 'merge') {
599
608
  // CLI consumer of the RunDetail envelope: current pair, per-pair outcome, stop reason.
600
- 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 });
609
+ const snapshot = await readMergeSnapshot({ io: deps.io, home: deps.home, runs: runs !== undefined && runs.some(item => item.id === found.id) ? runs : [...(runs ?? []), found], approval: params.mergeApproval, now: deps.now });
601
610
  if (snapshot.blocked !== undefined)
602
611
  return fail('status', snapshot.blocked.code, snapshot.blocked.message, '先修复合并状态(/evolution merge doctor),再查看运行');
603
612
  const detail = buildRunDetail(snapshot, found.id);
@@ -615,7 +624,7 @@ export function createMergeRuntime(deps) {
615
624
  const list = cards ?? await deps.cards(new AbortController().signal).catch(() => []);
616
625
  return list.length === 0 ? undefined : liveRowFactsOf(list);
617
626
  };
618
- const text = await handleReadOnlyMergeCommand(command, deps.io, deps.home, cards, runs, deps.params.mergeApproval, liveForRead);
627
+ const text = await handleReadOnlyMergeCommand(command, deps.io, deps.home, cards, runs, params.mergeApproval, liveForRead);
619
628
  if (text !== null)
620
629
  return { ok: true, text };
621
630
  return readOnly.execute(command);
@@ -88,9 +88,11 @@ function runView(run, readAt, includeResultRef = false) {
88
88
  const stopReason = run.meta?.stopReason ?? (record.state === 'cancelled' ? 'cancelled' : record.state === 'failed' ? 'failed' : record.state === 'succeeded' ? 'completed' : undefined);
89
89
  // The maker records progress while running; a terminal run that actually produced pairs
90
90
  // must not keep the last in-flight 0/N. The UI shows what happened, not the last tick.
91
- const progress = record.progress === undefined ? null : {
92
- done: state === 'succeeded' || state === 'undone' ? Math.max(record.progress.done, run.pairs.length) : record.progress.done,
93
- total: record.progress.total,
91
+ const reportPairs = run.report?.processedPairs?.length ?? run.report?.pairs.length;
92
+ const terminal = state === 'succeeded' || state === 'undone';
93
+ const progress = record.progress === undefined && reportPairs === undefined ? null : {
94
+ done: terminal ? Math.max(record.progress?.done ?? 0, reportPairs ?? 0, run.pairs.length) : record.progress?.done ?? reportPairs ?? 0,
95
+ total: record.progress?.total ?? reportPairs ?? 0,
94
96
  };
95
97
  return { wireVersion: 1, readAt, id: record.id, state, command: record.command ?? 'scan', ...(record.stage === undefined ? {} : { stage: record.stage }), progress, startedAt: record.startedAt, ...(record.endedAt === undefined ? {} : { endedAt: record.endedAt }), ...(stopReason === undefined ? {} : { stopReason }), ...(record.failure === undefined ? {} : { failure: { stage: 'settle', code: 'run-failed', retryable: false, retryFrom: 'none', detail: record.failure } }), ...(record.parentRunId === undefined ? {} : { parentRunId: record.parentRunId }), ...(run.pairs[0]?.pairKey === undefined ? {} : { currentPairKey: run.pairs[0].pairKey }), ...(record.resultRef === undefined ? {} : { resultRef: record.resultRef }), actions: [{ id: 'status', command: base + 'status ' + record.id, confirm: false, enabled: true }, { id: 'report', command: base + 'report ' + record.id, confirm: false, enabled: record.resultRef !== undefined }, { id: 'cancel', command: base + 'cancel ' + record.id, confirm: false, enabled: record.state === 'running' }] };
96
98
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@lmzhen/dsh-evolution-merge",
3
3
  "description": "Skill auto-merge domain: screening, two-round authoring, queues and commit orchestration (community build)",
4
- "version": "0.33.0",
4
+ "version": "0.34.0",
5
5
  "publishConfig": {
6
6
  "access": "public"
7
7
  },
@@ -32,9 +32,9 @@
32
32
  },
33
33
  "dependencies": {
34
34
  "@deepseek-ai/schemastery": "^3.18.5",
35
- "@lmzhen/dsh-evolution-core": "^0.33.0",
36
- "@lmzhen/dsh-evolution-commands": "^0.33.0",
37
- "@lmzhen/dsh-evolution-merge-engine": "0.33.0"
35
+ "@lmzhen/dsh-evolution-core": "^0.34.0",
36
+ "@lmzhen/dsh-evolution-commands": "^0.34.0",
37
+ "@lmzhen/dsh-evolution-merge-engine": "0.34.0"
38
38
  },
39
39
  "peerDependencies": {
40
40
  "@deepseek-ai/cordis": "^4.0.5"