@lmzhen/dsh-evolution-merge 0.32.6 → 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,6 +3700,12 @@ 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 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
3708
+ };
3703
3709
  return {
3704
3710
  wireVersion: 1,
3705
3711
  readAt,
@@ -3707,10 +3713,7 @@ function runView(run, readAt, includeResultRef = false) {
3707
3713
  state,
3708
3714
  command: record.command ?? "scan",
3709
3715
  ...record.stage === void 0 ? {} : { stage: record.stage },
3710
- progress: record.progress === void 0 ? null : {
3711
- done: record.progress.done,
3712
- total: record.progress.total
3713
- },
3716
+ progress,
3714
3717
  startedAt: record.startedAt,
3715
3718
  ...record.endedAt === void 0 ? {} : { endedAt: record.endedAt },
3716
3719
  ...stopReason === void 0 ? {} : { stopReason },
@@ -5056,7 +5059,8 @@ const MERGE_ROUTES = {
5056
5059
  view: MERGE_PATHS.view,
5057
5060
  rows: MERGE_PATHS.rows,
5058
5061
  body: MERGE_PATHS.body ?? MERGE_BASE + "body",
5059
- history: MERGE_PATHS.history ?? MERGE_BASE + "history"
5062
+ history: MERGE_PATHS.history ?? MERGE_BASE + "history",
5063
+ decision: MERGE_BASE + "decision"
5060
5064
  };
5061
5065
  function makeMergeRoutes(deps, _audit) {
5062
5066
  const absent = () => json(404, {
@@ -5171,7 +5175,6 @@ function makeMergeRoutes(deps, _audit) {
5171
5175
  const id = query.get("id");
5172
5176
  const name = query.get("name");
5173
5177
  const side = query.get("side");
5174
- if (id !== null && name !== null) return bad("id and name are mutually exclusive");
5175
5178
  if ((id === null || id === "") && (name === null || name === "")) return json(404, {
5176
5179
  wireVersion: 1,
5177
5180
  code: "merge-body-not-found",
@@ -5184,21 +5187,21 @@ function makeMergeRoutes(deps, _audit) {
5184
5187
  code: "merge-state-blocked",
5185
5188
  readiness: snapshot.blocked.readiness
5186
5189
  });
5187
- 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));
5188
5192
  if (refs.length === 0) return json(404, {
5189
5193
  wireVersion: 1,
5190
5194
  code: "merge-body-not-found",
5191
5195
  message: "merge body not found"
5192
5196
  });
5193
- 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");
5194
5198
  const row = snapshot.rows.find((item) => item.row.id === refs[0].rowId);
5195
5199
  if (row?.stage?.state === "lost" || row?.stage?.state === "expired") return json(404, {
5196
5200
  wireVersion: 1,
5197
5201
  code: "merge-body-not-found",
5198
5202
  message: "merge stage is lost"
5199
5203
  });
5200
- const sideFilter = side === "a" ? "live" : side === "b" ? "archive" : side === "live" || side === "archive" ? side : void 0;
5201
- const ref = (sideFilter === void 0 ? refs[0] : refs.find((item) => item.side === sideFilter)) ?? refs[0];
5204
+ const ref = refs[0];
5202
5205
  return json(200, buildBodyView(snapshot, ref, (source.bodyRef === void 0 ? null : await source.bodyRef(ref)) ?? ""));
5203
5206
  }
5204
5207
  const readiness = await notReady(source);
@@ -5234,6 +5237,35 @@ function makeMergeRoutes(deps, _audit) {
5234
5237
  if (readiness !== null) return readiness;
5235
5238
  return json(200, toHistoryView(await source.history?.() ?? [], Date.now()));
5236
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
+ }
5237
5269
  }
5238
5270
  ];
5239
5271
  }
@@ -9261,16 +9293,22 @@ function unitFor(deps) {
9261
9293
  function createMergeRuntime(deps) {
9262
9294
  const readOnly = createReadOnlyMergeService(deps.io, deps.home);
9263
9295
  const timeout = deps.timeoutMinutes > 0 ? deps.timeoutMinutes : void 0;
9264
- 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 = () => ({
9265
9303
  io: deps.io,
9266
9304
  home: deps.home,
9267
9305
  registry: deps.registry,
9268
- platformWrite: deps.platformWrite,
9306
+ platformWrite: platformWriteOf(),
9269
9307
  now: deps.now,
9270
9308
  ...deps.model === void 0 ? {} : { model: deps.model },
9271
9309
  ...deps.isOwnerActive === void 0 ? {} : { isOwnerActive: deps.isOwnerActive },
9272
9310
  ...timeout === void 0 ? {} : { timeoutMinutes: timeout }
9273
- };
9311
+ });
9274
9312
  const stage = deps.stage;
9275
9313
  const stageHooks = stage === void 0 ? {} : {
9276
9314
  onChild: (context) => {
@@ -9284,6 +9322,7 @@ function createMergeRuntime(deps) {
9284
9322
  const parent = context?.parent ?? deps.parent;
9285
9323
  const agentless = context?.agentless === true;
9286
9324
  const detached = deps.detached !== false;
9325
+ const params = paramsOf();
9287
9326
  if (command.verb === "pair") {
9288
9327
  const signal = new AbortController().signal;
9289
9328
  if (command.a === command.b) return fail("pair", "pair-same-source", "merge pair 需要两个不同技能源:" + command.a, "重新指定两个不同的 family-managed 技能");
@@ -9323,9 +9362,9 @@ function createMergeRuntime(deps) {
9323
9362
  if (existing?.state === "pending") return fail("pair", "pair-pending", "该 pair 已在待批准队列:" + existing.id, "先运行 /evolution merge approve/reject/discard " + existing.id, existing.runId);
9324
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);
9325
9364
  const forced = command.force === true ? forcedDecisionFor(command.a, command.b) : void 0;
9326
- const approval = deps.params.mergeApproval;
9365
+ const approval = params.mergeApproval;
9327
9366
  const result = await runScanCommand({
9328
- ...common,
9367
+ ...common(),
9329
9368
  approval,
9330
9369
  cards,
9331
9370
  ...detached ? { detached: true } : {},
@@ -9340,12 +9379,12 @@ function createMergeRuntime(deps) {
9340
9379
  parent,
9341
9380
  allow: deps.allow,
9342
9381
  ...deps.cwd === void 0 ? {} : { cwd: deps.cwd },
9343
- timeoutMs: deps.params.mergePairTimeoutMinutes * 6e4,
9382
+ timeoutMs: params.mergePairTimeoutMinutes * 6e4,
9344
9383
  approval,
9345
9384
  commitPort: skillLibraryPort(deps.skills),
9346
9385
  now: deps.now,
9347
9386
  ...unitFor(deps) === void 0 ? {} : { unit: unitFor(deps) },
9348
- ...deps.params.limitsSnapshot === void 0 ? {} : { limits: deps.params.limitsSnapshot.skillLimits },
9387
+ ...params.limitsSnapshot === void 0 ? {} : { limits: params.limitsSnapshot.skillLimits },
9349
9388
  ...deps.isBlocked === void 0 ? {} : { isBlocked: deps.isBlocked },
9350
9389
  ...stageHooks
9351
9390
  });
@@ -9379,12 +9418,12 @@ function createMergeRuntime(deps) {
9379
9418
  for (const card of cards) if (await deps.isBlocked?.(card.name) === true) blocked.add(card.name);
9380
9419
  const managed = new Set(cards.filter((card) => card.managed === true && card.protectedBy == null && !blocked.has(card.name)).map((card) => card.name));
9381
9420
  const loadedState = await loadMergeState(deps.io, deps.home);
9382
- const skipped = skippedPairKeys(loadedState.state.rows, deps.params.mergeSkippedTtlDays, deps.now());
9421
+ const skipped = skippedPairKeys(loadedState.state.rows, params.mergeSkippedTtlDays, deps.now());
9383
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));
9384
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);
9385
9424
  const result = await runScanCommand({
9386
- ...common,
9387
- approval: deps.params.mergeApproval,
9425
+ ...common(),
9426
+ approval: params.mergeApproval,
9388
9427
  notes,
9389
9428
  cards,
9390
9429
  ...detached ? { detached: true } : {},
@@ -9416,12 +9455,12 @@ function createMergeRuntime(deps) {
9416
9455
  parent,
9417
9456
  allow: deps.allow,
9418
9457
  ...deps.cwd === void 0 ? {} : { cwd: deps.cwd },
9419
- timeoutMs: deps.params.mergePairTimeoutMinutes * 6e4,
9420
- approval: deps.params.mergeApproval,
9458
+ timeoutMs: params.mergePairTimeoutMinutes * 6e4,
9459
+ approval: params.mergeApproval,
9421
9460
  commitPort: skillLibraryPort(deps.skills),
9422
9461
  now: deps.now,
9423
9462
  ...unitFor(deps) === void 0 ? {} : { unit: unitFor(deps) },
9424
- ...deps.params.limitsSnapshot === void 0 ? {} : { limits: deps.params.limitsSnapshot.skillLimits },
9463
+ ...params.limitsSnapshot === void 0 ? {} : { limits: params.limitsSnapshot.skillLimits },
9425
9464
  ...deps.isBlocked === void 0 ? {} : { isBlocked: deps.isBlocked },
9426
9465
  ...stageHooks
9427
9466
  })
@@ -9449,8 +9488,8 @@ function createMergeRuntime(deps) {
9449
9488
  b: row.b
9450
9489
  };
9451
9490
  const result = await runScanCommand({
9452
- ...common,
9453
- approval: deps.params.mergeApproval,
9491
+ ...common(),
9492
+ approval: params.mergeApproval,
9454
9493
  notes: ["candidates --retry " + row.id],
9455
9494
  cards,
9456
9495
  makePorts: (runId) => createChannelScanPorts({
@@ -9467,12 +9506,12 @@ function createMergeRuntime(deps) {
9467
9506
  parent,
9468
9507
  allow: deps.allow,
9469
9508
  ...deps.cwd === void 0 ? {} : { cwd: deps.cwd },
9470
- timeoutMs: deps.params.mergePairTimeoutMinutes * 6e4,
9471
- approval: deps.params.mergeApproval,
9509
+ timeoutMs: params.mergePairTimeoutMinutes * 6e4,
9510
+ approval: params.mergeApproval,
9472
9511
  commitPort: skillLibraryPort(deps.skills),
9473
9512
  now: deps.now,
9474
9513
  ...unitFor(deps) === void 0 ? {} : { unit: unitFor(deps) },
9475
- ...deps.params.limitsSnapshot === void 0 ? {} : { limits: deps.params.limitsSnapshot.skillLimits },
9514
+ ...params.limitsSnapshot === void 0 ? {} : { limits: params.limitsSnapshot.skillLimits },
9476
9515
  ...deps.isBlocked === void 0 ? {} : { isBlocked: deps.isBlocked },
9477
9516
  ...stageHooks
9478
9517
  })
@@ -9514,7 +9553,7 @@ function createMergeRuntime(deps) {
9514
9553
  io: deps.io,
9515
9554
  home: deps.home,
9516
9555
  now: deps.now,
9517
- ttlDays: deps.params.mergeSkippedTtlDays,
9556
+ ttlDays: params.mergeSkippedTtlDays,
9518
9557
  ...managed === void 0 ? {} : { managed },
9519
9558
  ...live === void 0 ? {} : { live }
9520
9559
  });
@@ -9529,7 +9568,7 @@ function createMergeRuntime(deps) {
9529
9568
  }
9530
9569
  if (command.verb === "doctor") return {
9531
9570
  ok: true,
9532
- 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")
9533
9572
  };
9534
9573
  if (command.verb === "report" || command.verb === "runs" || command.verb === "cancel") {
9535
9574
  const registry = deps.registry;
@@ -9541,7 +9580,7 @@ function createMergeRuntime(deps) {
9541
9580
  io: deps.io,
9542
9581
  home: deps.home,
9543
9582
  runs: mergeRuns,
9544
- approval: deps.params.mergeApproval,
9583
+ approval: params.mergeApproval,
9545
9584
  now: deps.now
9546
9585
  })))
9547
9586
  };
@@ -9657,13 +9696,13 @@ function createMergeRuntime(deps) {
9657
9696
  round2: () => failed("author", "resume-needs-child", "fuse resume needs a new child")
9658
9697
  };
9659
9698
  const result = await runResumeCommand({
9660
- ...common,
9699
+ ...common(),
9661
9700
  rowId: row.id,
9662
9701
  ports: basePorts,
9663
- approval: deps.params.mergeApproval,
9702
+ approval: params.mergeApproval,
9664
9703
  ...agentless ? { agentless: true } : {},
9665
9704
  ...detached ? { detached: true } : {},
9666
- pairTimeoutMinutes: deps.params.mergePairTimeoutMinutes,
9705
+ pairTimeoutMinutes: params.mergePairTimeoutMinutes,
9667
9706
  ...deps.isOwnerActive === void 0 ? {} : { isOwnerActive: deps.isOwnerActive },
9668
9707
  makePorts: (runId) => createChannelScanPorts({
9669
9708
  io: deps.io,
@@ -9675,12 +9714,12 @@ function createMergeRuntime(deps) {
9675
9714
  parent,
9676
9715
  allow: deps.allow,
9677
9716
  ...deps.cwd === void 0 ? {} : { cwd: deps.cwd },
9678
- timeoutMs: deps.params.mergePairTimeoutMinutes * 6e4,
9679
- approval: deps.params.mergeApproval,
9717
+ timeoutMs: params.mergePairTimeoutMinutes * 6e4,
9718
+ approval: params.mergeApproval,
9680
9719
  commitPort: skillLibraryPort(deps.skills),
9681
9720
  now: deps.now,
9682
9721
  ...unitFor(deps) === void 0 ? {} : { unit: unitFor(deps) },
9683
- ...deps.params.limitsSnapshot === void 0 ? {} : { limits: deps.params.limitsSnapshot.skillLimits },
9722
+ ...params.limitsSnapshot === void 0 ? {} : { limits: params.limitsSnapshot.skillLimits },
9684
9723
  ...deps.isBlocked === void 0 ? {} : { isBlocked: deps.isBlocked }
9685
9724
  }),
9686
9725
  round2WithRun: createNewChildRound2({
@@ -9692,11 +9731,11 @@ function createMergeRuntime(deps) {
9692
9731
  parentRunId: (runId) => runId,
9693
9732
  allow: deps.allow,
9694
9733
  ...deps.cwd === void 0 ? {} : { cwd: deps.cwd },
9695
- timeoutMs: deps.params.mergePairTimeoutMinutes * 6e4,
9734
+ timeoutMs: params.mergePairTimeoutMinutes * 6e4,
9696
9735
  commitPort: skillLibraryPort(deps.skills),
9697
9736
  now: deps.now,
9698
9737
  ...unitFor(deps) === void 0 ? {} : { unit: unitFor(deps) },
9699
- ...deps.params.limitsSnapshot === void 0 ? {} : { limits: deps.params.limitsSnapshot.skillLimits },
9738
+ ...params.limitsSnapshot === void 0 ? {} : { limits: params.limitsSnapshot.skillLimits },
9700
9739
  ...deps.isBlocked === void 0 ? {} : { isBlocked: deps.isBlocked },
9701
9740
  ...stageHooks
9702
9741
  })
@@ -9811,11 +9850,11 @@ function createMergeRuntime(deps) {
9811
9850
  }
9812
9851
  }
9813
9852
  const result = await runApproveCommand({
9814
- ...common,
9853
+ ...common(),
9815
9854
  rowId: command.id,
9816
9855
  ...agentless ? { agentless: true } : {},
9817
9856
  ...detached ? { detached: true } : {},
9818
- pairTimeoutMinutes: deps.params.mergePairTimeoutMinutes,
9857
+ pairTimeoutMinutes: params.mergePairTimeoutMinutes,
9819
9858
  ports: {
9820
9859
  stage: createStageForCards({
9821
9860
  io: deps.io,
@@ -9833,11 +9872,11 @@ function createMergeRuntime(deps) {
9833
9872
  parentRunId: (runId) => runId,
9834
9873
  allow: deps.allow,
9835
9874
  ...deps.cwd === void 0 ? {} : { cwd: deps.cwd },
9836
- timeoutMs: deps.params.mergePairTimeoutMinutes * 6e4,
9875
+ timeoutMs: params.mergePairTimeoutMinutes * 6e4,
9837
9876
  commitPort: skillLibraryPort(deps.skills),
9838
9877
  now: deps.now,
9839
9878
  ...unitFor(deps) === void 0 ? {} : { unit: unitFor(deps) },
9840
- ...deps.params.limitsSnapshot === void 0 ? {} : { limits: deps.params.limitsSnapshot.skillLimits },
9879
+ ...params.limitsSnapshot === void 0 ? {} : { limits: params.limitsSnapshot.skillLimits },
9841
9880
  ...deps.isBlocked === void 0 ? {} : { isBlocked: deps.isBlocked },
9842
9881
  ...stageHooks
9843
9882
  })
@@ -9864,7 +9903,7 @@ function createMergeRuntime(deps) {
9864
9903
  io: deps.io,
9865
9904
  home: deps.home,
9866
9905
  runs: runs !== void 0 && runs.some((item) => item.id === found.id) ? runs : [...runs ?? [], found],
9867
- approval: deps.params.mergeApproval,
9906
+ approval: params.mergeApproval,
9868
9907
  now: deps.now
9869
9908
  });
9870
9909
  if (snapshot.blocked !== void 0) return fail("status", snapshot.blocked.code, snapshot.blocked.message, "先修复合并状态(/evolution merge doctor),再查看运行");
@@ -9881,7 +9920,7 @@ function createMergeRuntime(deps) {
9881
9920
  const list = cards ?? await deps.cards(new AbortController().signal).catch(() => []);
9882
9921
  return list.length === 0 ? void 0 : liveRowFactsOf(list);
9883
9922
  };
9884
- 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);
9885
9924
  if (text !== null) return {
9886
9925
  ok: true,
9887
9926
  text
@@ -10545,8 +10584,9 @@ const inject = [];
10545
10584
  function apply(ctx, rawConfig = {}) {
10546
10585
  registerOwnerSettings(ctx, "evolution-merge", Config);
10547
10586
  configureMergePresentation(ctx);
10548
- let params = resolveMergeParams(rawConfig);
10587
+ const startupParams = resolveMergeParams(rawConfig);
10549
10588
  let mergeRun;
10589
+ let decide;
10550
10590
  let source;
10551
10591
  let doctorFact;
10552
10592
  const mergeService = {
@@ -10568,7 +10608,17 @@ function apply(ctx, rawConfig = {}) {
10568
10608
  };
10569
10609
  const mergeContext = ctx;
10570
10610
  if (typeof mergeContext.provide === "function") mergeContext.provide("evolutionMerge", mergeService);
10571
- 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));
10572
10622
  ctx.inject(["commands", "evolutionIo"], (scope) => {
10573
10623
  const registry = scope.get("evolutionIo");
10574
10624
  if (registry === void 0) return;
@@ -10619,13 +10669,15 @@ function apply(ctx, rawConfig = {}) {
10619
10669
  });
10620
10670
  }, "evolution-merge: restore supersede");
10621
10671
  const limitsSnapshot = resolveLimitsSnapshot(ctx.get("evolutionPolicy")?.get?.());
10622
- const hostApproval = ctx.get("evolutionApproval");
10623
- const platformWrite = typeof hostApproval?.platformWrite === "boolean" ? hostApproval.platformWrite : params.platformWrite ?? false;
10624
- params = {
10625
- ...params,
10626
- limitsSnapshot,
10627
- platformWrite
10672
+ const getPlatformWrite = () => {
10673
+ const hostApproval = ctx.get("evolutionApproval");
10674
+ return typeof hostApproval?.platformWrite === "boolean" ? hostApproval.platformWrite : resolveMergeParams(rawConfig).platformWrite ?? false;
10628
10675
  };
10676
+ const getParams = () => ({
10677
+ ...resolveMergeParams(rawConfig),
10678
+ limitsSnapshot,
10679
+ platformWrite: getPlatformWrite()
10680
+ });
10629
10681
  const skills = newSkillLibrary({
10630
10682
  io,
10631
10683
  ctx,
@@ -10668,7 +10720,7 @@ function apply(ctx, rawConfig = {}) {
10668
10720
  io,
10669
10721
  home,
10670
10722
  now: () => Date.now(),
10671
- ttlDays: params.mergeSkippedTtlDays,
10723
+ ttlDays: getParams().mergeSkippedTtlDays,
10672
10724
  ...live === void 0 ? {} : { live }
10673
10725
  });
10674
10726
  if (reconciled.actions.length > 0) ctx.logger?.info?.("merge reconcile: " + String(reconciled.actions.length) + " action(s)");
@@ -10705,7 +10757,7 @@ function apply(ctx, rawConfig = {}) {
10705
10757
  } : void 0;
10706
10758
  });
10707
10759
  const stage = new StageToolRegistry();
10708
- const engineInput = resolveEngineInput({ modelsRoot: params.mergeModelsRoot });
10760
+ const engineInput = resolveEngineInput({ modelsRoot: startupParams.mergeModelsRoot });
10709
10761
  const engineConfig = engineConfigFromRoot(engineInput.modelsRoot);
10710
10762
  const llm = ctx.get("llm");
10711
10763
  const defaultModel = ctx.get("agentDefaultModel");
@@ -10761,12 +10813,13 @@ function apply(ctx, rawConfig = {}) {
10761
10813
  });
10762
10814
  source = {
10763
10815
  async snapshot() {
10816
+ const current = getParams();
10764
10817
  return await readMergeSnapshot({
10765
10818
  io,
10766
10819
  home,
10767
10820
  runs: runs.runs("merge"),
10768
- approval: params.mergeApproval,
10769
- platformWrite,
10821
+ approval: current.mergeApproval,
10822
+ platformWrite: getPlatformWrite(),
10770
10823
  hostOnly: hostCapabilities.hostOnly,
10771
10824
  live: async () => liveRowFactsOf(await readSkillCards(skills)),
10772
10825
  history: async () => {
@@ -10818,7 +10871,7 @@ function apply(ctx, rawConfig = {}) {
10818
10871
  return await resolveBody(io, home, ref, async () => ref.side === "archive" ? await skills.readArchived(ref.name) : await skills.read(ref.name));
10819
10872
  },
10820
10873
  approval() {
10821
- return params.mergeApproval;
10874
+ return getParams().mergeApproval;
10822
10875
  },
10823
10876
  readiness() {
10824
10877
  return ready;
@@ -10854,13 +10907,13 @@ function apply(ctx, rawConfig = {}) {
10854
10907
  registry: runs,
10855
10908
  skills,
10856
10909
  now: () => Date.now(),
10857
- params,
10858
- platformWrite,
10910
+ getParams,
10911
+ getPlatformWrite,
10859
10912
  ...defaultModel === void 0 ? {} : { model: defaultModel.currentSelection().model },
10860
10913
  channel,
10861
10914
  parent: void 0,
10862
10915
  allow: ["merge_stage"],
10863
- timeoutMinutes: params.mergeRunTimeoutMinutes,
10916
+ timeoutMinutes: startupParams.mergeRunTimeoutMinutes,
10864
10917
  hostCapabilities,
10865
10918
  isBlocked: async (name) => {
10866
10919
  return await ctx.get("evolutionCurator")?.isBlocked?.(name) === true;
@@ -10892,6 +10945,20 @@ function apply(ctx, rawConfig = {}) {
10892
10945
  };
10893
10946
  return runMergeCommand(runtime, rawInput, invocation);
10894
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
+ };
10895
10962
  const command = createMergeCommand(runtime);
10896
10963
  if (command !== null) scope.effect(() => commands.register(command), "evolution-merge: command");
10897
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);
@@ -86,7 +86,15 @@ function runView(run, readAt, includeResultRef = false) {
86
86
  : run.meta?.approval === 'pending' ? 'awaiting-approval'
87
87
  : record.state;
88
88
  const stopReason = run.meta?.stopReason ?? (record.state === 'cancelled' ? 'cancelled' : record.state === 'failed' ? 'failed' : record.state === 'succeeded' ? 'completed' : undefined);
89
- return { wireVersion: 1, readAt, id: record.id, state, command: record.command ?? 'scan', ...(record.stage === undefined ? {} : { stage: record.stage }), progress: record.progress === undefined ? null : { done: record.progress.done, total: record.progress.total }, 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' }] };
89
+ // The maker records progress while running; a terminal run that actually produced pairs
90
+ // must not keep the last in-flight 0/N. The UI shows what happened, not the last tick.
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,
96
+ };
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' }] };
90
98
  }
91
99
  export function buildFacts(snapshot) {
92
100
  const ordered = [...snapshot.runs].sort((a, b) => b.record.startedAt - a.record.startedAt);
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.32.6",
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.32.6",
36
- "@lmzhen/dsh-evolution-commands": "^0.32.6",
37
- "@lmzhen/dsh-evolution-merge-engine": "0.32.6"
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"