@andyqiu/codeforge 0.8.11 → 0.8.12

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/dist/index.js CHANGED
@@ -372,17 +372,17 @@ var require_visit = __commonJS((exports) => {
372
372
  visit.BREAK = BREAK;
373
373
  visit.SKIP = SKIP;
374
374
  visit.REMOVE = REMOVE;
375
- function visit_(key, node, visitor, path21) {
376
- const ctrl = callVisitor(key, node, visitor, path21);
375
+ function visit_(key, node, visitor, path22) {
376
+ const ctrl = callVisitor(key, node, visitor, path22);
377
377
  if (identity.isNode(ctrl) || identity.isPair(ctrl)) {
378
- replaceNode(key, path21, ctrl);
379
- return visit_(key, ctrl, visitor, path21);
378
+ replaceNode(key, path22, ctrl);
379
+ return visit_(key, ctrl, visitor, path22);
380
380
  }
381
381
  if (typeof ctrl !== "symbol") {
382
382
  if (identity.isCollection(node)) {
383
- path21 = Object.freeze(path21.concat(node));
383
+ path22 = Object.freeze(path22.concat(node));
384
384
  for (let i = 0;i < node.items.length; ++i) {
385
- const ci = visit_(i, node.items[i], visitor, path21);
385
+ const ci = visit_(i, node.items[i], visitor, path22);
386
386
  if (typeof ci === "number")
387
387
  i = ci - 1;
388
388
  else if (ci === BREAK)
@@ -393,13 +393,13 @@ var require_visit = __commonJS((exports) => {
393
393
  }
394
394
  }
395
395
  } else if (identity.isPair(node)) {
396
- path21 = Object.freeze(path21.concat(node));
397
- const ck = visit_("key", node.key, visitor, path21);
396
+ path22 = Object.freeze(path22.concat(node));
397
+ const ck = visit_("key", node.key, visitor, path22);
398
398
  if (ck === BREAK)
399
399
  return BREAK;
400
400
  else if (ck === REMOVE)
401
401
  node.key = null;
402
- const cv = visit_("value", node.value, visitor, path21);
402
+ const cv = visit_("value", node.value, visitor, path22);
403
403
  if (cv === BREAK)
404
404
  return BREAK;
405
405
  else if (cv === REMOVE)
@@ -420,17 +420,17 @@ var require_visit = __commonJS((exports) => {
420
420
  visitAsync.BREAK = BREAK;
421
421
  visitAsync.SKIP = SKIP;
422
422
  visitAsync.REMOVE = REMOVE;
423
- async function visitAsync_(key, node, visitor, path21) {
424
- const ctrl = await callVisitor(key, node, visitor, path21);
423
+ async function visitAsync_(key, node, visitor, path22) {
424
+ const ctrl = await callVisitor(key, node, visitor, path22);
425
425
  if (identity.isNode(ctrl) || identity.isPair(ctrl)) {
426
- replaceNode(key, path21, ctrl);
427
- return visitAsync_(key, ctrl, visitor, path21);
426
+ replaceNode(key, path22, ctrl);
427
+ return visitAsync_(key, ctrl, visitor, path22);
428
428
  }
429
429
  if (typeof ctrl !== "symbol") {
430
430
  if (identity.isCollection(node)) {
431
- path21 = Object.freeze(path21.concat(node));
431
+ path22 = Object.freeze(path22.concat(node));
432
432
  for (let i = 0;i < node.items.length; ++i) {
433
- const ci = await visitAsync_(i, node.items[i], visitor, path21);
433
+ const ci = await visitAsync_(i, node.items[i], visitor, path22);
434
434
  if (typeof ci === "number")
435
435
  i = ci - 1;
436
436
  else if (ci === BREAK)
@@ -441,13 +441,13 @@ var require_visit = __commonJS((exports) => {
441
441
  }
442
442
  }
443
443
  } else if (identity.isPair(node)) {
444
- path21 = Object.freeze(path21.concat(node));
445
- const ck = await visitAsync_("key", node.key, visitor, path21);
444
+ path22 = Object.freeze(path22.concat(node));
445
+ const ck = await visitAsync_("key", node.key, visitor, path22);
446
446
  if (ck === BREAK)
447
447
  return BREAK;
448
448
  else if (ck === REMOVE)
449
449
  node.key = null;
450
- const cv = await visitAsync_("value", node.value, visitor, path21);
450
+ const cv = await visitAsync_("value", node.value, visitor, path22);
451
451
  if (cv === BREAK)
452
452
  return BREAK;
453
453
  else if (cv === REMOVE)
@@ -474,23 +474,23 @@ var require_visit = __commonJS((exports) => {
474
474
  }
475
475
  return visitor;
476
476
  }
477
- function callVisitor(key, node, visitor, path21) {
477
+ function callVisitor(key, node, visitor, path22) {
478
478
  if (typeof visitor === "function")
479
- return visitor(key, node, path21);
479
+ return visitor(key, node, path22);
480
480
  if (identity.isMap(node))
481
- return visitor.Map?.(key, node, path21);
481
+ return visitor.Map?.(key, node, path22);
482
482
  if (identity.isSeq(node))
483
- return visitor.Seq?.(key, node, path21);
483
+ return visitor.Seq?.(key, node, path22);
484
484
  if (identity.isPair(node))
485
- return visitor.Pair?.(key, node, path21);
485
+ return visitor.Pair?.(key, node, path22);
486
486
  if (identity.isScalar(node))
487
- return visitor.Scalar?.(key, node, path21);
487
+ return visitor.Scalar?.(key, node, path22);
488
488
  if (identity.isAlias(node))
489
- return visitor.Alias?.(key, node, path21);
489
+ return visitor.Alias?.(key, node, path22);
490
490
  return;
491
491
  }
492
- function replaceNode(key, path21, node) {
493
- const parent = path21[path21.length - 1];
492
+ function replaceNode(key, path22, node) {
493
+ const parent = path22[path22.length - 1];
494
494
  if (identity.isCollection(parent)) {
495
495
  parent.items[key] = node;
496
496
  } else if (identity.isPair(parent)) {
@@ -1049,10 +1049,10 @@ var require_Collection = __commonJS((exports) => {
1049
1049
  var createNode = require_createNode();
1050
1050
  var identity = require_identity();
1051
1051
  var Node = require_Node();
1052
- function collectionFromPath(schema, path21, value) {
1052
+ function collectionFromPath(schema, path22, value) {
1053
1053
  let v = value;
1054
- for (let i = path21.length - 1;i >= 0; --i) {
1055
- const k = path21[i];
1054
+ for (let i = path22.length - 1;i >= 0; --i) {
1055
+ const k = path22[i];
1056
1056
  if (typeof k === "number" && Number.isInteger(k) && k >= 0) {
1057
1057
  const a = [];
1058
1058
  a[k] = v;
@@ -1071,7 +1071,7 @@ var require_Collection = __commonJS((exports) => {
1071
1071
  sourceObjects: new Map
1072
1072
  });
1073
1073
  }
1074
- var isEmptyPath = (path21) => path21 == null || typeof path21 === "object" && !!path21[Symbol.iterator]().next().done;
1074
+ var isEmptyPath = (path22) => path22 == null || typeof path22 === "object" && !!path22[Symbol.iterator]().next().done;
1075
1075
 
1076
1076
  class Collection extends Node.NodeBase {
1077
1077
  constructor(type, schema) {
@@ -1092,11 +1092,11 @@ var require_Collection = __commonJS((exports) => {
1092
1092
  copy.range = this.range.slice();
1093
1093
  return copy;
1094
1094
  }
1095
- addIn(path21, value) {
1096
- if (isEmptyPath(path21))
1095
+ addIn(path22, value) {
1096
+ if (isEmptyPath(path22))
1097
1097
  this.add(value);
1098
1098
  else {
1099
- const [key, ...rest] = path21;
1099
+ const [key, ...rest] = path22;
1100
1100
  const node = this.get(key, true);
1101
1101
  if (identity.isCollection(node))
1102
1102
  node.addIn(rest, value);
@@ -1106,8 +1106,8 @@ var require_Collection = __commonJS((exports) => {
1106
1106
  throw new Error(`Expected YAML collection at ${key}. Remaining path: ${rest}`);
1107
1107
  }
1108
1108
  }
1109
- deleteIn(path21) {
1110
- const [key, ...rest] = path21;
1109
+ deleteIn(path22) {
1110
+ const [key, ...rest] = path22;
1111
1111
  if (rest.length === 0)
1112
1112
  return this.delete(key);
1113
1113
  const node = this.get(key, true);
@@ -1116,8 +1116,8 @@ var require_Collection = __commonJS((exports) => {
1116
1116
  else
1117
1117
  throw new Error(`Expected YAML collection at ${key}. Remaining path: ${rest}`);
1118
1118
  }
1119
- getIn(path21, keepScalar) {
1120
- const [key, ...rest] = path21;
1119
+ getIn(path22, keepScalar) {
1120
+ const [key, ...rest] = path22;
1121
1121
  const node = this.get(key, true);
1122
1122
  if (rest.length === 0)
1123
1123
  return !keepScalar && identity.isScalar(node) ? node.value : node;
@@ -1132,15 +1132,15 @@ var require_Collection = __commonJS((exports) => {
1132
1132
  return n == null || allowScalar && identity.isScalar(n) && n.value == null && !n.commentBefore && !n.comment && !n.tag;
1133
1133
  });
1134
1134
  }
1135
- hasIn(path21) {
1136
- const [key, ...rest] = path21;
1135
+ hasIn(path22) {
1136
+ const [key, ...rest] = path22;
1137
1137
  if (rest.length === 0)
1138
1138
  return this.has(key);
1139
1139
  const node = this.get(key, true);
1140
1140
  return identity.isCollection(node) ? node.hasIn(rest) : false;
1141
1141
  }
1142
- setIn(path21, value) {
1143
- const [key, ...rest] = path21;
1142
+ setIn(path22, value) {
1143
+ const [key, ...rest] = path22;
1144
1144
  if (rest.length === 0) {
1145
1145
  this.set(key, value);
1146
1146
  } else {
@@ -3533,9 +3533,9 @@ var require_Document = __commonJS((exports) => {
3533
3533
  if (assertCollection(this.contents))
3534
3534
  this.contents.add(value);
3535
3535
  }
3536
- addIn(path21, value) {
3536
+ addIn(path22, value) {
3537
3537
  if (assertCollection(this.contents))
3538
- this.contents.addIn(path21, value);
3538
+ this.contents.addIn(path22, value);
3539
3539
  }
3540
3540
  createAlias(node, name) {
3541
3541
  if (!node.anchor) {
@@ -3584,30 +3584,30 @@ var require_Document = __commonJS((exports) => {
3584
3584
  delete(key) {
3585
3585
  return assertCollection(this.contents) ? this.contents.delete(key) : false;
3586
3586
  }
3587
- deleteIn(path21) {
3588
- if (Collection.isEmptyPath(path21)) {
3587
+ deleteIn(path22) {
3588
+ if (Collection.isEmptyPath(path22)) {
3589
3589
  if (this.contents == null)
3590
3590
  return false;
3591
3591
  this.contents = null;
3592
3592
  return true;
3593
3593
  }
3594
- return assertCollection(this.contents) ? this.contents.deleteIn(path21) : false;
3594
+ return assertCollection(this.contents) ? this.contents.deleteIn(path22) : false;
3595
3595
  }
3596
3596
  get(key, keepScalar) {
3597
3597
  return identity.isCollection(this.contents) ? this.contents.get(key, keepScalar) : undefined;
3598
3598
  }
3599
- getIn(path21, keepScalar) {
3600
- if (Collection.isEmptyPath(path21))
3599
+ getIn(path22, keepScalar) {
3600
+ if (Collection.isEmptyPath(path22))
3601
3601
  return !keepScalar && identity.isScalar(this.contents) ? this.contents.value : this.contents;
3602
- return identity.isCollection(this.contents) ? this.contents.getIn(path21, keepScalar) : undefined;
3602
+ return identity.isCollection(this.contents) ? this.contents.getIn(path22, keepScalar) : undefined;
3603
3603
  }
3604
3604
  has(key) {
3605
3605
  return identity.isCollection(this.contents) ? this.contents.has(key) : false;
3606
3606
  }
3607
- hasIn(path21) {
3608
- if (Collection.isEmptyPath(path21))
3607
+ hasIn(path22) {
3608
+ if (Collection.isEmptyPath(path22))
3609
3609
  return this.contents !== undefined;
3610
- return identity.isCollection(this.contents) ? this.contents.hasIn(path21) : false;
3610
+ return identity.isCollection(this.contents) ? this.contents.hasIn(path22) : false;
3611
3611
  }
3612
3612
  set(key, value) {
3613
3613
  if (this.contents == null) {
@@ -3616,13 +3616,13 @@ var require_Document = __commonJS((exports) => {
3616
3616
  this.contents.set(key, value);
3617
3617
  }
3618
3618
  }
3619
- setIn(path21, value) {
3620
- if (Collection.isEmptyPath(path21)) {
3619
+ setIn(path22, value) {
3620
+ if (Collection.isEmptyPath(path22)) {
3621
3621
  this.contents = value;
3622
3622
  } else if (this.contents == null) {
3623
- this.contents = Collection.collectionFromPath(this.schema, Array.from(path21), value);
3623
+ this.contents = Collection.collectionFromPath(this.schema, Array.from(path22), value);
3624
3624
  } else if (assertCollection(this.contents)) {
3625
- this.contents.setIn(path21, value);
3625
+ this.contents.setIn(path22, value);
3626
3626
  }
3627
3627
  }
3628
3628
  setSchema(version2, options = {}) {
@@ -5517,9 +5517,9 @@ var require_cst_visit = __commonJS((exports) => {
5517
5517
  visit.BREAK = BREAK;
5518
5518
  visit.SKIP = SKIP;
5519
5519
  visit.REMOVE = REMOVE;
5520
- visit.itemAtPath = (cst, path21) => {
5520
+ visit.itemAtPath = (cst, path22) => {
5521
5521
  let item = cst;
5522
- for (const [field, index] of path21) {
5522
+ for (const [field, index] of path22) {
5523
5523
  const tok = item?.[field];
5524
5524
  if (tok && "items" in tok) {
5525
5525
  item = tok.items[index];
@@ -5528,23 +5528,23 @@ var require_cst_visit = __commonJS((exports) => {
5528
5528
  }
5529
5529
  return item;
5530
5530
  };
5531
- visit.parentCollection = (cst, path21) => {
5532
- const parent = visit.itemAtPath(cst, path21.slice(0, -1));
5533
- const field = path21[path21.length - 1][0];
5531
+ visit.parentCollection = (cst, path22) => {
5532
+ const parent = visit.itemAtPath(cst, path22.slice(0, -1));
5533
+ const field = path22[path22.length - 1][0];
5534
5534
  const coll = parent?.[field];
5535
5535
  if (coll && "items" in coll)
5536
5536
  return coll;
5537
5537
  throw new Error("Parent collection not found");
5538
5538
  };
5539
- function _visit(path21, item, visitor) {
5540
- let ctrl = visitor(item, path21);
5539
+ function _visit(path22, item, visitor) {
5540
+ let ctrl = visitor(item, path22);
5541
5541
  if (typeof ctrl === "symbol")
5542
5542
  return ctrl;
5543
5543
  for (const field of ["key", "value"]) {
5544
5544
  const token = item[field];
5545
5545
  if (token && "items" in token) {
5546
5546
  for (let i = 0;i < token.items.length; ++i) {
5547
- const ci = _visit(Object.freeze(path21.concat([[field, i]])), token.items[i], visitor);
5547
+ const ci = _visit(Object.freeze(path22.concat([[field, i]])), token.items[i], visitor);
5548
5548
  if (typeof ci === "number")
5549
5549
  i = ci - 1;
5550
5550
  else if (ci === BREAK)
@@ -5555,10 +5555,10 @@ var require_cst_visit = __commonJS((exports) => {
5555
5555
  }
5556
5556
  }
5557
5557
  if (typeof ctrl === "function" && field === "key")
5558
- ctrl = ctrl(item, path21);
5558
+ ctrl = ctrl(item, path22);
5559
5559
  }
5560
5560
  }
5561
- return typeof ctrl === "function" ? ctrl(item, path21) : ctrl;
5561
+ return typeof ctrl === "function" ? ctrl(item, path22) : ctrl;
5562
5562
  }
5563
5563
  exports.visit = visit;
5564
5564
  });
@@ -6827,14 +6827,14 @@ var require_parser = __commonJS((exports) => {
6827
6827
  case "scalar":
6828
6828
  case "single-quoted-scalar":
6829
6829
  case "double-quoted-scalar": {
6830
- const fs16 = this.flowScalar(this.type);
6830
+ const fs17 = this.flowScalar(this.type);
6831
6831
  if (atNextItem || it.value) {
6832
- map2.items.push({ start, key: fs16, sep: [] });
6832
+ map2.items.push({ start, key: fs17, sep: [] });
6833
6833
  this.onKeyLine = true;
6834
6834
  } else if (it.sep) {
6835
- this.stack.push(fs16);
6835
+ this.stack.push(fs17);
6836
6836
  } else {
6837
- Object.assign(it, { key: fs16, sep: [] });
6837
+ Object.assign(it, { key: fs17, sep: [] });
6838
6838
  this.onKeyLine = true;
6839
6839
  }
6840
6840
  return;
@@ -6962,13 +6962,13 @@ var require_parser = __commonJS((exports) => {
6962
6962
  case "scalar":
6963
6963
  case "single-quoted-scalar":
6964
6964
  case "double-quoted-scalar": {
6965
- const fs16 = this.flowScalar(this.type);
6965
+ const fs17 = this.flowScalar(this.type);
6966
6966
  if (!it || it.value)
6967
- fc.items.push({ start: [], key: fs16, sep: [] });
6967
+ fc.items.push({ start: [], key: fs17, sep: [] });
6968
6968
  else if (it.sep)
6969
- this.stack.push(fs16);
6969
+ this.stack.push(fs17);
6970
6970
  else
6971
- Object.assign(it, { key: fs16, sep: [] });
6971
+ Object.assign(it, { key: fs17, sep: [] });
6972
6972
  return;
6973
6973
  }
6974
6974
  case "flow-map-end":
@@ -24224,6 +24224,9 @@ async function reconcileOneStaleTransitional(mainRoot, snap, result) {
24224
24224
  } catch (err) {
24225
24225
  debugLog(`stale salvage 后清理 worktree 失败 (${snap.sessionId}): ${err.message}`);
24226
24226
  }
24227
+ await deleteBranchIfExists({ root: mainRoot, branch: snap.branch }).catch((err) => {
24228
+ debugLog(`stale salvage 后删分支失败 (${snap.sessionId}): ${err.message}`);
24229
+ });
24227
24230
  result.staleSalvaged.push(snap.sessionId);
24228
24231
  }
24229
24232
  function summarizeReconcileDigest(result, prune, maxList = 3) {
@@ -24819,7 +24822,11 @@ var description4 = [
24819
24822
  " reviewTarget 缺失时默认 'code'([Session Merge Review] 流按合同即 code review)。",
24820
24823
  " 显式传入的值优先,不覆盖。plan:/decision:/pc- id 不补全。",
24821
24824
  "**何时不调**:REQUEST_CHANGES / BLOCK 不调(无 APPROVE = 无审批记录)。",
24822
- "**fallback**:codeforge 解析 reviewer boomerang 见 APPROVE 但无记录 → 自动以 source='codeforge-fallback' 补写。"
24825
+ "**fallback**:codeforge 解析 reviewer boomerang 见 APPROVE 但无记录 → 自动以 source='codeforge-fallback' 补写。",
24826
+ "**⚠️ 承诺语义(辅助提示,非安全保证)**(ADR:reviewer-approval-decision-consistency):",
24827
+ " 调此工具 = 你已决定 APPROVE/APPROVE_WITH_NOTES,**不可**再输出 REQUEST_CHANGES/BLOCK。",
24828
+ " 工具层无法强制校验(不能作为关键控制点);语义防线由 reviewer.md 文案约束承担(Phase 1),",
24829
+ " Phase 2 将在 review decision collection boundary 层自动作废同轮 stale approval。"
24823
24830
  ].join(`
24824
24831
  `);
24825
24832
  var ArgsSchema4 = exports_external.object({
@@ -25916,50 +25923,218 @@ function toEntry(r) {
25916
25923
  }
25917
25924
  // lib/merge-gate.ts
25918
25925
  import { promises as fs13 } from "node:fs";
25926
+ import * as os5 from "node:os";
25919
25927
  import * as path16 from "node:path";
25920
25928
  var DEFAULT_MERGE_GATE_CONFIG = {
25921
25929
  enabled: true,
25922
25930
  approvalPreCheck: true,
25923
- preCheckTtlSeconds: 3600
25931
+ preCheckTtlSeconds: 3600,
25932
+ requireUserConfirm: true
25924
25933
  };
25925
- var CONFIG_REL = ".codeforge/merge-gate.json";
25926
- async function loadMergeGate(mainRoot) {
25927
- const file2 = path16.join(mainRoot, CONFIG_REL);
25934
+ var PROJECT_CONFIG_REL = ".codeforge/merge-gate.json";
25935
+ var GLOBAL_CONFIG_PATH = path16.join(os5.homedir(), ".config", "codeforge", "merge-gate.json");
25936
+ async function readOptionalJson(filePath, label) {
25928
25937
  let raw;
25929
25938
  try {
25930
- raw = await fs13.readFile(file2, "utf8");
25939
+ raw = await fs13.readFile(filePath, "utf8");
25931
25940
  } catch (err) {
25932
25941
  const e = err;
25933
25942
  if (e.code === "ENOENT")
25934
- return { ...DEFAULT_MERGE_GATE_CONFIG };
25935
- console.warn(`[merge-gate] 读取 ${CONFIG_REL} 失败,fail-safe 退化为 enabled=false: ${e.message}`);
25936
- return { enabled: false, approvalPreCheck: false };
25943
+ return null;
25944
+ console.warn(`[merge-gate] 读取 ${label} 失败,跳过该层配置: ${e.message}`);
25945
+ return null;
25937
25946
  }
25938
25947
  let parsed;
25939
25948
  try {
25940
25949
  parsed = JSON.parse(raw);
25941
25950
  } catch (err) {
25942
- console.warn(`[merge-gate] ${CONFIG_REL} JSON 解析失败,fail-safe 退化为 enabled=false: ${err instanceof Error ? err.message : String(err)}`);
25943
- return { enabled: false, approvalPreCheck: false };
25951
+ console.warn(`[merge-gate] ${label} JSON 解析失败,跳过该层配置: ${err instanceof Error ? err.message : String(err)}`);
25952
+ return null;
25944
25953
  }
25945
- if (!parsed || typeof parsed !== "object") {
25946
- console.warn(`[merge-gate] ${CONFIG_REL} 顶层非 object,fail-safe 退化为 enabled=false`);
25947
- return { enabled: false, approvalPreCheck: false };
25954
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
25955
+ console.warn(`[merge-gate] ${label} 顶层非 object,跳过该层配置`);
25956
+ return null;
25948
25957
  }
25949
- const obj = parsed;
25950
- const enabled = typeof obj["enabled"] === "boolean" ? obj["enabled"] : DEFAULT_MERGE_GATE_CONFIG.enabled;
25951
- const approvalPreCheck = typeof obj["approvalPreCheck"] === "boolean" ? obj["approvalPreCheck"] : DEFAULT_MERGE_GATE_CONFIG.approvalPreCheck ?? false;
25952
- let preCheckTtlSeconds = DEFAULT_MERGE_GATE_CONFIG.preCheckTtlSeconds ?? 3600;
25953
- const rawTtl = obj["preCheckTtlSeconds"];
25954
- if (typeof rawTtl === "number" && rawTtl > 0) {
25955
- if (rawTtl > 86400) {
25956
- console.warn(`[merge-gate] preCheckTtlSeconds=${rawTtl} 超过 24h 上界,截断为 86400`);
25957
- preCheckTtlSeconds = 86400;
25958
- } else {
25959
- preCheckTtlSeconds = rawTtl;
25958
+ return parsed;
25959
+ }
25960
+ async function loadMergeGateWithPaths(mainRoot, globalConfigPath = GLOBAL_CONFIG_PATH) {
25961
+ const projectConfigPath = path16.join(mainRoot, PROJECT_CONFIG_REL);
25962
+ const [globalObj, projectObj] = await Promise.all([
25963
+ readOptionalJson(globalConfigPath, "~/.config/codeforge/merge-gate.json"),
25964
+ readOptionalJson(projectConfigPath, PROJECT_CONFIG_REL)
25965
+ ]);
25966
+ const enabled = (() => {
25967
+ if (projectObj && typeof projectObj["enabled"] === "boolean")
25968
+ return projectObj["enabled"];
25969
+ if (globalObj && typeof globalObj["enabled"] === "boolean")
25970
+ return globalObj["enabled"];
25971
+ return DEFAULT_MERGE_GATE_CONFIG.enabled;
25972
+ })();
25973
+ const approvalPreCheck = (() => {
25974
+ if (projectObj && typeof projectObj["approvalPreCheck"] === "boolean")
25975
+ return projectObj["approvalPreCheck"];
25976
+ if (globalObj && typeof globalObj["approvalPreCheck"] === "boolean")
25977
+ return globalObj["approvalPreCheck"];
25978
+ return DEFAULT_MERGE_GATE_CONFIG.approvalPreCheck ?? true;
25979
+ })();
25980
+ const preCheckTtlSeconds = (() => {
25981
+ const rawTtl = projectObj?.["preCheckTtlSeconds"] ?? globalObj?.["preCheckTtlSeconds"];
25982
+ if (typeof rawTtl === "number" && rawTtl > 0) {
25983
+ if (rawTtl > 86400) {
25984
+ console.warn(`[merge-gate] preCheckTtlSeconds=${rawTtl} 超过 24h 上界,截断为 86400`);
25985
+ return 86400;
25986
+ }
25987
+ return rawTtl;
25988
+ }
25989
+ return DEFAULT_MERGE_GATE_CONFIG.preCheckTtlSeconds ?? 3600;
25990
+ })();
25991
+ const requireUserConfirm = (() => {
25992
+ if (projectObj && projectObj["requireUserConfirm"] === false) {
25993
+ console.warn("[merge-gate] 项目级配置不允许设 requireUserConfirm: false(安全锁定)。" + "若需关闭门 B,请在全局配置 ~/.config/codeforge/merge-gate.json 中设置。" + "项目级 false 被静默忽略,门 B 保持开启。");
25994
+ }
25995
+ if (globalObj?.["requireUserConfirm"] === false)
25996
+ return false;
25997
+ return DEFAULT_MERGE_GATE_CONFIG.requireUserConfirm ?? true;
25998
+ })();
25999
+ return { enabled, approvalPreCheck, preCheckTtlSeconds, requireUserConfirm };
26000
+ }
26001
+ async function loadMergeGate(mainRoot) {
26002
+ return loadMergeGateWithPaths(mainRoot, GLOBAL_CONFIG_PATH);
26003
+ }
26004
+
26005
+ // lib/confirm-store.ts
26006
+ import { promises as fs14 } from "node:fs";
26007
+ import * as path17 from "node:path";
26008
+ var CONFIRM_TTL_MS = 30 * 60 * 1000;
26009
+ function confirmRoot(mainRoot) {
26010
+ return path17.join(runtimeDir(mainRoot, { ensure: false }), "confirm");
26011
+ }
26012
+ function pendingFile(mainRoot, sid) {
26013
+ return path17.join(confirmRoot(mainRoot), "pending", `${sid}.json`);
26014
+ }
26015
+ function consumingFile(mainRoot, sid) {
26016
+ return path17.join(confirmRoot(mainRoot), "consuming", `${sid}.json`);
26017
+ }
26018
+ function consumedFile(mainRoot, sid) {
26019
+ return path17.join(confirmRoot(mainRoot), "consumed", `${sid}.json`);
26020
+ }
26021
+ async function ensureConfirmDirs(mainRoot) {
26022
+ const root = confirmRoot(mainRoot);
26023
+ await Promise.all([
26024
+ fs14.mkdir(path17.join(root, "pending"), { recursive: true }),
26025
+ fs14.mkdir(path17.join(root, "consuming"), { recursive: true }),
26026
+ fs14.mkdir(path17.join(root, "consumed"), { recursive: true })
26027
+ ]);
26028
+ }
26029
+ async function readJsonSafe(file2) {
26030
+ let raw;
26031
+ try {
26032
+ raw = await fs14.readFile(file2, "utf8");
26033
+ } catch (e) {
26034
+ if (e.code === "ENOENT")
26035
+ return null;
26036
+ return null;
26037
+ }
26038
+ try {
26039
+ const parsed = JSON.parse(raw);
26040
+ if (typeof parsed.sessionId !== "string" || typeof parsed.worktreeHeadSha !== "string" || typeof parsed.approvalSha !== "string" || typeof parsed.ts !== "number" || typeof parsed.text !== "string" || parsed.status !== "PENDING" && parsed.status !== "CONSUMING" && parsed.status !== "CONSUMED") {
26041
+ return null;
25960
26042
  }
26043
+ return parsed;
26044
+ } catch {
26045
+ return null;
26046
+ }
26047
+ }
26048
+ async function writeJsonAtomic(file2, rec) {
26049
+ const tmp = file2 + ".tmp." + Date.now();
26050
+ try {
26051
+ await fs14.writeFile(tmp, JSON.stringify(rec, null, 2), "utf8");
26052
+ await fs14.rename(tmp, file2);
26053
+ } catch (e) {
26054
+ await fs14.unlink(tmp).catch(() => {});
26055
+ throw e;
26056
+ }
26057
+ }
26058
+ async function writeConfirmRecord(mainRoot, rec) {
26059
+ await ensureConfirmDirs(mainRoot);
26060
+ const full = { ...rec, status: "PENDING" };
26061
+ await writeJsonAtomic(pendingFile(mainRoot, rec.sessionId), full);
26062
+ }
26063
+ async function readPendingConfirm(mainRoot, sid) {
26064
+ return readJsonSafe(pendingFile(mainRoot, sid));
26065
+ }
26066
+ function isHeadDrifted(rec, curHead) {
26067
+ if (!curHead)
26068
+ return true;
26069
+ return rec.worktreeHeadSha !== curHead;
26070
+ }
26071
+ function isConfirmValid(rec, curHead, approval, now = Date.now()) {
26072
+ if (!rec)
26073
+ return false;
26074
+ if (rec.status !== "PENDING")
26075
+ return false;
26076
+ if (isHeadDrifted(rec, curHead))
26077
+ return false;
26078
+ if (!approval)
26079
+ return false;
26080
+ if (approval.verdict !== "APPROVE" && approval.verdict !== "APPROVE_WITH_NOTES")
26081
+ return false;
26082
+ if (!approval.coveredSha)
26083
+ return false;
26084
+ if (rec.approvalSha !== approval.coveredSha)
26085
+ return false;
26086
+ if (now - rec.ts > CONFIRM_TTL_MS)
26087
+ return false;
26088
+ return true;
26089
+ }
26090
+ async function claimConfirmIfValid(mainRoot, sid, currentHeadSha, approval, now = Date.now()) {
26091
+ const rec = await readPendingConfirm(mainRoot, sid);
26092
+ if (!rec)
26093
+ return { ok: false, reason: "no_pending_record" };
26094
+ if (!isConfirmValid(rec, currentHeadSha, approval, now)) {
26095
+ return { ok: false, reason: "invalid_or_drifted" };
26096
+ }
26097
+ await ensureConfirmDirs(mainRoot);
26098
+ try {
26099
+ await fs14.rename(pendingFile(mainRoot, sid), consumingFile(mainRoot, sid));
26100
+ } catch (e) {
26101
+ const err = e;
26102
+ if (err.code === "ENOENT") {
26103
+ return { ok: false, reason: "already_claimed" };
26104
+ }
26105
+ throw e;
26106
+ }
26107
+ const claimed = { ...rec, status: "CONSUMING" };
26108
+ await writeJsonAtomic(consumingFile(mainRoot, sid), claimed);
26109
+ return { ok: true, rec: claimed };
26110
+ }
26111
+ async function finalizeConfirmConsumed(mainRoot, sid) {
26112
+ try {
26113
+ await ensureConfirmDirs(mainRoot);
26114
+ const rec = await readJsonSafe(consumingFile(mainRoot, sid));
26115
+ const consumed = {
26116
+ ...rec ?? { sessionId: sid, worktreeHeadSha: "", approvalSha: "", ts: Date.now(), text: "" },
26117
+ status: "CONSUMED"
26118
+ };
26119
+ await writeJsonAtomic(consumedFile(mainRoot, sid), consumed);
26120
+ await fs14.unlink(consumingFile(mainRoot, sid)).catch(() => {});
26121
+ } catch (err) {
26122
+ console.warn(`[confirm-store] finalizeConfirmConsumed 失败(sid=${sid}),已合入,不阻断:${err instanceof Error ? err.message : String(err)}`);
26123
+ }
26124
+ }
26125
+ async function failConfirmConsumed(mainRoot, sid) {
26126
+ try {
26127
+ await ensureConfirmDirs(mainRoot);
26128
+ const rec = await readJsonSafe(consumingFile(mainRoot, sid));
26129
+ const consumed = {
26130
+ ...rec ?? { sessionId: sid, worktreeHeadSha: "", approvalSha: "", ts: Date.now(), text: "" },
26131
+ status: "CONSUMED"
26132
+ };
26133
+ await writeJsonAtomic(consumedFile(mainRoot, sid), consumed);
26134
+ await fs14.unlink(consumingFile(mainRoot, sid)).catch(() => {});
26135
+ } catch (err) {
26136
+ console.warn(`[confirm-store] failConfirmConsumed 失败(sid=${sid}),fail-closed 已记录:${err instanceof Error ? err.message : String(err)}`);
25961
26137
  }
25962
- return { enabled, approvalPreCheck, preCheckTtlSeconds };
25963
26138
  }
25964
26139
 
25965
26140
  // lib/merge-loop.ts
@@ -26000,7 +26175,8 @@ async function runMergeLoop(opts) {
26000
26175
  const { sha } = await mergeSessionBack({
26001
26176
  sessionId: opts.sessionId,
26002
26177
  mainRoot: opts.mainRoot,
26003
- commitMessage: message
26178
+ commitMessage: message,
26179
+ ...opts.planStore ? { planStore: opts.planStore } : {}
26004
26180
  });
26005
26181
  return { status: "force_merged", commitSha: sha, loops: 0 };
26006
26182
  }
@@ -26027,14 +26203,50 @@ async function runMergeLoop(opts) {
26027
26203
  });
26028
26204
  }
26029
26205
  progress("approval_pre_check", `skip_review | reviewTarget=${hit.reviewTarget} | coveredSha=${hit.coveredSha.slice(0, 12)} | ttlOk`);
26030
- const { sha } = await mergeSessionBack({
26031
- sessionId: opts.sessionId,
26032
- mainRoot: opts.mainRoot,
26033
- ...opts.summary ? { summary: opts.summary } : {}
26034
- });
26206
+ {
26207
+ const preCheckApproval = (() => {
26208
+ return {
26209
+ verdict: "APPROVE",
26210
+ coveredSha: hit.coveredSha,
26211
+ pendingId: `session:${opts.sessionId}`,
26212
+ createdAt: new Date().toISOString()
26213
+ };
26214
+ })();
26215
+ const gate = await claimConfirmGate({
26216
+ mainRoot: opts.mainRoot,
26217
+ sessionId: opts.sessionId,
26218
+ worktreePath: entry.worktreePath,
26219
+ approval: preCheckApproval,
26220
+ mergeGate,
26221
+ getCurrentWorktreeHeadFn: opts.__testHooks?.getCurrentWorktreeHead
26222
+ });
26223
+ if (!gate.ok) {
26224
+ progress("block_pause", "门 B 对话确认未通过(pre-check 路径)");
26225
+ return {
26226
+ status: "blocked",
26227
+ loops: 0,
26228
+ finalDecision: "APPROVE",
26229
+ blockReason: gate.blockReason,
26230
+ lastReviewSummary: `approval-pre-check 命中但门 B 阻断:${gate.blockReason}`
26231
+ };
26232
+ }
26233
+ }
26234
+ let preCheckSha;
26235
+ try {
26236
+ ({ sha: preCheckSha } = await mergeSessionBack({
26237
+ sessionId: opts.sessionId,
26238
+ mainRoot: opts.mainRoot,
26239
+ ...opts.summary ? { summary: opts.summary } : {},
26240
+ ...opts.planStore ? { planStore: opts.planStore } : {}
26241
+ }));
26242
+ } catch (mergeErr) {
26243
+ await failConfirmConsumed(opts.mainRoot, opts.sessionId);
26244
+ throw mergeErr;
26245
+ }
26246
+ await finalizeConfirmConsumed(opts.mainRoot, opts.sessionId);
26035
26247
  return {
26036
26248
  status: "skipped_by_approval",
26037
- commitSha: sha,
26249
+ commitSha: preCheckSha,
26038
26250
  loops: 0,
26039
26251
  finalDecision: "APPROVE",
26040
26252
  lastReviewSummary: `approval-pre-check 命中:coveredSha=${hit.coveredSha}, reviewTarget=${hit.reviewTarget}`
@@ -26119,15 +26331,43 @@ async function runMergeLoop(opts) {
26119
26331
  ...lastReviewSummary ? { lastReviewSummary } : {}
26120
26332
  };
26121
26333
  }
26334
+ {
26335
+ const gate = await claimConfirmGate({
26336
+ mainRoot: opts.mainRoot,
26337
+ sessionId: opts.sessionId,
26338
+ worktreePath: entry.worktreePath,
26339
+ approval: approval ?? undefined,
26340
+ mergeGate,
26341
+ getCurrentWorktreeHeadFn: opts.__testHooks?.getCurrentWorktreeHead
26342
+ });
26343
+ if (!gate.ok) {
26344
+ progress("block_pause", "门 B 对话确认未通过(S6 路径)");
26345
+ return {
26346
+ status: "blocked",
26347
+ loops,
26348
+ finalDecision: "APPROVE",
26349
+ blockReason: gate.blockReason,
26350
+ ...lastReviewSummary ? { lastReviewSummary } : {}
26351
+ };
26352
+ }
26353
+ }
26122
26354
  }
26123
- const { sha } = await mergeSessionBack({
26124
- sessionId: opts.sessionId,
26125
- mainRoot: opts.mainRoot,
26126
- ...opts.summary ? { summary: opts.summary } : {}
26127
- });
26355
+ let s6Sha;
26356
+ try {
26357
+ ({ sha: s6Sha } = await mergeSessionBack({
26358
+ sessionId: opts.sessionId,
26359
+ mainRoot: opts.mainRoot,
26360
+ ...opts.summary ? { summary: opts.summary } : {},
26361
+ ...opts.planStore ? { planStore: opts.planStore } : {}
26362
+ }));
26363
+ } catch (mergeErr) {
26364
+ await failConfirmConsumed(opts.mainRoot, opts.sessionId);
26365
+ throw mergeErr;
26366
+ }
26367
+ await finalizeConfirmConsumed(opts.mainRoot, opts.sessionId);
26128
26368
  return {
26129
26369
  status: "merged",
26130
- commitSha: sha,
26370
+ commitSha: s6Sha,
26131
26371
  loops,
26132
26372
  finalDecision: "APPROVE",
26133
26373
  lastReviewSummary
@@ -26203,6 +26443,26 @@ async function runMergeLoop(opts) {
26203
26443
  }
26204
26444
  }
26205
26445
  }
26446
+ async function claimConfirmGate(args) {
26447
+ const { mainRoot, sessionId, worktreePath, approval, mergeGate } = args;
26448
+ if (!mergeGate.requireUserConfirm) {
26449
+ return { ok: true, skip: true };
26450
+ }
26451
+ const headOf = args.getCurrentWorktreeHeadFn ?? getCurrentWorktreeHead;
26452
+ const curHead = await headOf(worktreePath).catch(() => "");
26453
+ const approvalArg = approval ? { verdict: approval.verdict, coveredSha: approval.coveredSha ?? "" } : null;
26454
+ const claim = await claimConfirmIfValid(mainRoot, sessionId, curHead, approvalArg).catch((e) => ({
26455
+ ok: false,
26456
+ reason: `io_error:${e instanceof Error ? e.message : String(e)}`
26457
+ }));
26458
+ if (!claim.ok) {
26459
+ return {
26460
+ ok: false,
26461
+ blockReason: `对话确认门(门 B)未通过(${claim.reason}):请重新说「确认合入」。`
26462
+ };
26463
+ }
26464
+ return { ok: true, claimed: true };
26465
+ }
26206
26466
  function buildForceMergeMessage(sessionId, entry) {
26207
26467
  return `session(${sessionId}): merge ${entry.branch} [force-merge: skipped review]
26208
26468
 
@@ -26658,12 +26918,14 @@ async function execute12(input) {
26658
26918
  ...mergeArgs.plan_id ? { planId: mergeArgs.plan_id } : {},
26659
26919
  ...mergeArgs.force ? { force: true } : {},
26660
26920
  ...mergeArgs.summary ? { summary: mergeArgs.summary } : {},
26921
+ ..._ctx.planStore ? { planStore: _ctx.planStore } : {},
26661
26922
  spawner: _ctx.spawner,
26662
26923
  ...sendProgress ? {
26663
26924
  onProgress: (state, detail) => {
26664
26925
  Promise.resolve(sendProgress(state, detail)).catch(() => {});
26665
26926
  }
26666
- } : {}
26927
+ } : {},
26928
+ ..._ctx.__testHooks ? { __testHooks: _ctx.__testHooks } : {}
26667
26929
  });
26668
26930
  return { ok: true, action: "merge", data: result };
26669
26931
  } catch (err) {
@@ -26675,8 +26937,8 @@ async function execute12(input) {
26675
26937
  }
26676
26938
  }
26677
26939
  // lib/plan-store.ts
26678
- import { promises as fs14 } from "node:fs";
26679
- import * as path17 from "node:path";
26940
+ import { promises as fs15 } from "node:fs";
26941
+ import * as path18 from "node:path";
26680
26942
  var INDEX_VERSION = 1;
26681
26943
 
26682
26944
  class PlanStore {
@@ -26685,8 +26947,8 @@ class PlanStore {
26685
26947
  now;
26686
26948
  secondCounters = new Map;
26687
26949
  constructor(opts = {}) {
26688
- this.root = path17.resolve(opts.root ?? process.cwd());
26689
- this.base = opts.base ? path17.resolve(opts.base) : plansDir(this.root);
26950
+ this.root = path18.resolve(opts.root ?? process.cwd());
26951
+ this.base = opts.base ? path18.resolve(opts.base) : plansDir(this.root);
26690
26952
  this.now = opts.now ?? (() => new Date);
26691
26953
  }
26692
26954
  async write(input) {
@@ -26696,14 +26958,14 @@ class PlanStore {
26696
26958
  if (typeof input.content !== "string" || input.content.length === 0) {
26697
26959
  throw new Error("PlanStore.write: content 不能为空");
26698
26960
  }
26699
- await fs14.mkdir(this.base, { recursive: true });
26961
+ await fs15.mkdir(this.base, { recursive: true });
26700
26962
  const lockPath = this.lockPath();
26701
26963
  return await withFileLock(lockPath, async () => {
26702
26964
  const index = await this.readIndexLocked();
26703
26965
  const now = this.now();
26704
26966
  const planId = this.allocPlanId(now, index);
26705
26967
  const filename = this.composeFilename(planId, input.title);
26706
- const absFile = path17.join(this.base, filename);
26968
+ const absFile = path18.join(this.base, filename);
26707
26969
  await this.atomicWriteFile(absFile, input.content);
26708
26970
  const entry = {
26709
26971
  plan_id: planId,
@@ -26727,9 +26989,9 @@ class PlanStore {
26727
26989
  const entry = index.entries.find((e) => e.plan_id === planId);
26728
26990
  if (!entry)
26729
26991
  return null;
26730
- const abs = path17.join(this.base, entry.path);
26992
+ const abs = path18.join(this.base, entry.path);
26731
26993
  try {
26732
- const content = await fs14.readFile(abs, "utf8");
26994
+ const content = await fs15.readFile(abs, "utf8");
26733
26995
  return { entry, content };
26734
26996
  } catch (err) {
26735
26997
  const e = err;
@@ -26788,7 +27050,7 @@ class PlanStore {
26788
27050
  else if (e.status === "orphan")
26789
27051
  shouldDelete = true;
26790
27052
  if (shouldDelete) {
26791
- await fs14.rm(path17.join(this.base, e.path), { force: true }).catch(() => {});
27053
+ await fs15.rm(path18.join(this.base, e.path), { force: true }).catch(() => {});
26792
27054
  removed++;
26793
27055
  } else {
26794
27056
  keep.push(e);
@@ -26810,9 +27072,9 @@ class PlanStore {
26810
27072
  knownPaths.add(e.path);
26811
27073
  if (e.status !== "active")
26812
27074
  continue;
26813
- const abs = path17.join(this.base, e.path);
27075
+ const abs = path18.join(this.base, e.path);
26814
27076
  try {
26815
- await fs14.stat(abs);
27077
+ await fs15.stat(abs);
26816
27078
  } catch {
26817
27079
  e.status = "orphan";
26818
27080
  markedOrphan++;
@@ -26822,23 +27084,23 @@ class PlanStore {
26822
27084
  await this.writeIndexLocked(index);
26823
27085
  let unindexedFiles = [];
26824
27086
  try {
26825
- const all = await fs14.readdir(this.base);
27087
+ const all = await fs15.readdir(this.base);
26826
27088
  unindexedFiles = all.filter((f) => f.endsWith(".md")).filter((f) => !knownPaths.has(f));
26827
27089
  } catch {}
26828
27090
  return { markedOrphan, unindexedFiles };
26829
27091
  });
26830
27092
  }
26831
27093
  indexPath() {
26832
- return path17.join(this.base, "index.json");
27094
+ return path18.join(this.base, "index.json");
26833
27095
  }
26834
27096
  lockPath() {
26835
- return path17.join(this.base, "index.lock");
27097
+ return path18.join(this.base, "index.lock");
26836
27098
  }
26837
27099
  async readIndexLocked() {
26838
27100
  const file2 = this.indexPath();
26839
27101
  let raw;
26840
27102
  try {
26841
- raw = await fs14.readFile(file2, "utf8");
27103
+ raw = await fs15.readFile(file2, "utf8");
26842
27104
  } catch (err) {
26843
27105
  const e = err;
26844
27106
  if (e.code === "ENOENT")
@@ -26860,7 +27122,7 @@ class PlanStore {
26860
27122
  async archiveCorruptIndex(file2) {
26861
27123
  const ts = this.now().toISOString().replace(/[-:.TZ]/g, "").slice(0, 14);
26862
27124
  const dst = `${file2}.corrupt-${ts}`;
26863
- await fs14.rename(file2, dst).catch(() => {});
27125
+ await fs15.rename(file2, dst).catch(() => {});
26864
27126
  }
26865
27127
  async readIndex() {
26866
27128
  return this.readIndexLocked();
@@ -26905,15 +27167,15 @@ class PlanStore {
26905
27167
  const tsPart = m ? `${m[1]}-${m[2]}` : planId;
26906
27168
  const nnn = m ? m[3] : "000";
26907
27169
  const sample = planFilePath(this.root, title);
26908
- const base = path17.basename(sample, ".md");
27170
+ const base = path18.basename(sample, ".md");
26909
27171
  const slug = base.replace(/^\d{8}-\d{6}-?/, "") || "untitled";
26910
27172
  return `${tsPart}-${nnn}-${slug}.md`;
26911
27173
  }
26912
27174
  async atomicWriteFile(file2, data) {
26913
- await fs14.mkdir(path17.dirname(file2), { recursive: true });
27175
+ await fs15.mkdir(path18.dirname(file2), { recursive: true });
26914
27176
  const tmp = `${file2}.tmp-${process.pid}-${Date.now()}`;
26915
- await fs14.writeFile(tmp, data, "utf8");
26916
- await fs14.rename(tmp, file2);
27177
+ await fs15.writeFile(tmp, data, "utf8");
27178
+ await fs15.rename(tmp, file2);
26917
27179
  }
26918
27180
  }
26919
27181
  function formatTimestamp(d) {
@@ -26976,8 +27238,8 @@ async function execute13(input) {
26976
27238
  }
26977
27239
  }
26978
27240
  // tools/plan-read.ts
26979
- import { promises as fs15 } from "node:fs";
26980
- import * as path18 from "node:path";
27241
+ import { promises as fs16 } from "node:fs";
27242
+ import * as path19 from "node:path";
26981
27243
  var description14 = [
26982
27244
  "读取方案文档内容,支持按 plan_id 或绝对路径查询。",
26983
27245
  "**何时调用**:",
@@ -27082,9 +27344,9 @@ async function execute14(input) {
27082
27344
  };
27083
27345
  }
27084
27346
  }
27085
- const abs = path18.resolve(args.path);
27347
+ const abs = path19.resolve(args.path);
27086
27348
  try {
27087
- const content = await fs15.readFile(abs, "utf8");
27349
+ const content = await fs16.readFile(abs, "utf8");
27088
27350
  return {
27089
27351
  ok: true,
27090
27352
  content,
@@ -27106,16 +27368,16 @@ async function execute14(input) {
27106
27368
  // lib/adr-init.ts
27107
27369
  import { spawnSync } from "node:child_process";
27108
27370
  import { existsSync as existsSync4, promises as fsp } from "node:fs";
27109
- import * as path19 from "node:path";
27371
+ import * as path20 from "node:path";
27110
27372
  import * as url2 from "node:url";
27111
27373
  function resolveAssetsRoot() {
27112
- const here = path19.dirname(url2.fileURLToPath(import.meta.url));
27374
+ const here = path20.dirname(url2.fileURLToPath(import.meta.url));
27113
27375
  let dir = here;
27114
27376
  for (let i = 0;i < 6; i++) {
27115
- if (existsSync4(path19.join(dir, "package.json")) && existsSync4(path19.join(dir, "assets", "adr-init"))) {
27116
- return path19.join(dir, "assets", "adr-init");
27377
+ if (existsSync4(path20.join(dir, "package.json")) && existsSync4(path20.join(dir, "assets", "adr-init"))) {
27378
+ return path20.join(dir, "assets", "adr-init");
27117
27379
  }
27118
- const parent = path19.dirname(dir);
27380
+ const parent = path20.dirname(dir);
27119
27381
  if (parent === dir)
27120
27382
  break;
27121
27383
  dir = parent;
@@ -27123,13 +27385,13 @@ function resolveAssetsRoot() {
27123
27385
  const xdgConfig = process.env["XDG_CONFIG_HOME"];
27124
27386
  const homeDir = process.env["HOME"] ?? process.env["USERPROFILE"] ?? "";
27125
27387
  const fallbackRoots = [
27126
- xdgConfig ? path19.join(xdgConfig, "opencode") : null,
27127
- path19.join(homeDir, ".config", "opencode"),
27128
- process.env["APPDATA"] ? path19.join(process.env["APPDATA"], "opencode") : null,
27129
- process.env["LOCALAPPDATA"] ? path19.join(process.env["LOCALAPPDATA"], "opencode") : null
27388
+ xdgConfig ? path20.join(xdgConfig, "opencode") : null,
27389
+ path20.join(homeDir, ".config", "opencode"),
27390
+ process.env["APPDATA"] ? path20.join(process.env["APPDATA"], "opencode") : null,
27391
+ process.env["LOCALAPPDATA"] ? path20.join(process.env["LOCALAPPDATA"], "opencode") : null
27130
27392
  ].filter(Boolean);
27131
27393
  for (const root of fallbackRoots) {
27132
- const candidate = path19.join(root, "assets", "adr-init");
27394
+ const candidate = path20.join(root, "assets", "adr-init");
27133
27395
  if (existsSync4(candidate)) {
27134
27396
  return candidate;
27135
27397
  }
@@ -27166,7 +27428,7 @@ function runGitConfigHooksPath(cwd) {
27166
27428
  }
27167
27429
  }
27168
27430
  async function runAdrInit(opts = {}) {
27169
- const cwd = path19.resolve(opts.cwd ?? process.cwd());
27431
+ const cwd = path20.resolve(opts.cwd ?? process.cwd());
27170
27432
  const force = !!opts.force;
27171
27433
  const dryRun = !!opts.dryRun;
27172
27434
  const writePrepare = !!opts.writePrepare;
@@ -27215,8 +27477,8 @@ async function runAdrInit(opts = {}) {
27215
27477
  });
27216
27478
  }
27217
27479
  for (const item of plan) {
27218
- const srcAbs = path19.join(assetsRoot, item.src);
27219
- const dstAbs = path19.join(cwd, item.dst);
27480
+ const srcAbs = path20.join(assetsRoot, item.src);
27481
+ const dstAbs = path20.join(cwd, item.dst);
27220
27482
  if (!existsSync4(srcAbs)) {
27221
27483
  result.warnings.push(`资产缺失:${item.src}(跳过 ${item.dst})`);
27222
27484
  continue;
@@ -27229,7 +27491,7 @@ async function runAdrInit(opts = {}) {
27229
27491
  const bakRel = `${item.dst}.bak.${ts}`;
27230
27492
  if (!dryRun) {
27231
27493
  try {
27232
- await fsp.copyFile(dstAbs, path19.join(cwd, bakRel));
27494
+ await fsp.copyFile(dstAbs, path20.join(cwd, bakRel));
27233
27495
  } catch (e) {
27234
27496
  result.ok = false;
27235
27497
  result.reason = "io_error";
@@ -27241,7 +27503,7 @@ async function runAdrInit(opts = {}) {
27241
27503
  }
27242
27504
  if (!dryRun) {
27243
27505
  try {
27244
- await fsp.mkdir(path19.dirname(dstAbs), { recursive: true });
27506
+ await fsp.mkdir(path20.dirname(dstAbs), { recursive: true });
27245
27507
  await fsp.copyFile(srcAbs, dstAbs);
27246
27508
  if (item.chmod !== undefined) {
27247
27509
  try {
@@ -27271,7 +27533,7 @@ async function runAdrInit(opts = {}) {
27271
27533
  } else {
27272
27534
  result.suggestions.push("[dry-run] 将运行:git config core.hooksPath .githooks");
27273
27535
  }
27274
- const pkgPath = path19.join(cwd, "package.json");
27536
+ const pkgPath = path20.join(cwd, "package.json");
27275
27537
  const isNpm = existsSync4(pkgPath);
27276
27538
  if (isNpm) {
27277
27539
  if (writePrepare) {
@@ -27286,7 +27548,7 @@ async function runAdrInit(opts = {}) {
27286
27548
  const bakRel = `package.json.bak.${ts}`;
27287
27549
  if (!dryRun) {
27288
27550
  try {
27289
- await fsp.copyFile(pkgPath, path19.join(cwd, bakRel));
27551
+ await fsp.copyFile(pkgPath, path20.join(cwd, bakRel));
27290
27552
  } catch (e) {
27291
27553
  result.warnings.push(`备份 package.json 失败:${e instanceof Error ? e.message : String(e)}`);
27292
27554
  }
@@ -27406,12 +27668,12 @@ async function execute15(input) {
27406
27668
  }
27407
27669
  }
27408
27670
  // lib/opencode-session-probe.ts
27409
- import * as path20 from "node:path";
27410
- import * as os5 from "node:os";
27671
+ import * as path21 from "node:path";
27672
+ import * as os6 from "node:os";
27411
27673
  import { createRequire as createRequire2 } from "node:module";
27412
27674
  var requireFromHere = createRequire2(import.meta.url);
27413
27675
  var DEFAULT_LIVENESS_MS = 6 * 60 * 60000;
27414
- var DEFAULT_DB_PATH = path20.join(os5.homedir(), ".local/share/opencode/opencode.db");
27676
+ var DEFAULT_DB_PATH = path21.join(os6.homedir(), ".local/share/opencode/opencode.db");
27415
27677
  function createSessionProbe(opts = {}) {
27416
27678
  const dbPath = opts.dbPath ?? DEFAULT_DB_PATH;
27417
27679
  const httpBaseUrl = opts.httpBaseUrl ?? process.env["OPENCODE_SERVER_URL"];
@@ -27566,6 +27828,241 @@ async function execute16(input) {
27566
27828
  probe.close();
27567
27829
  }
27568
27830
  }
27831
+ // lib/llm-retry.ts
27832
+ var DEFAULT_RETRY_CONFIG = {
27833
+ maxRetries: 2,
27834
+ baseDelayMs: 1000,
27835
+ maxDelayMs: 8000,
27836
+ jitterRatio: 0.25,
27837
+ minAttemptWindowMs: 30000
27838
+ };
27839
+ function classifyError(err) {
27840
+ const e = err;
27841
+ if (e instanceof Error && e.name === "AbortError" || e && e.name === "AbortError" || e && e.name === "MessageAbortedError") {
27842
+ return "fatal";
27843
+ }
27844
+ if (e && e.__cfTimeout === true) {
27845
+ return "fatal";
27846
+ }
27847
+ const errName = e ? e.name : undefined;
27848
+ if (errName === "MessageOutputLengthError")
27849
+ return "fatal";
27850
+ const msg = getMsg(err);
27851
+ if (/finish[=\s]+length/i.test(msg))
27852
+ return "fatal";
27853
+ if (errName === "APIError") {
27854
+ const data = e.data ?? {};
27855
+ const isRetryable = data.isRetryable;
27856
+ const statusCode = data.statusCode;
27857
+ if (typeof isRetryable === "boolean") {
27858
+ return isRetryable ? "retryable" : "fatal";
27859
+ }
27860
+ if (typeof statusCode === "number") {
27861
+ if (statusCode === 429 || statusCode === 529 || statusCode >= 500 && statusCode < 600) {
27862
+ return "retryable";
27863
+ }
27864
+ if (statusCode >= 400 && statusCode < 500)
27865
+ return "fatal";
27866
+ }
27867
+ return "retryable";
27868
+ }
27869
+ const NETWORK_KEYWORDS = [
27870
+ "http2",
27871
+ "client connection lost",
27872
+ "econnreset",
27873
+ "econnrefused",
27874
+ "etimedout",
27875
+ "epipe",
27876
+ "socket hang up",
27877
+ "fetch failed",
27878
+ "network"
27879
+ ];
27880
+ const msgLower = msg.toLowerCase();
27881
+ if (NETWORK_KEYWORDS.some((kw) => msgLower.includes(kw))) {
27882
+ return "retryable";
27883
+ }
27884
+ if (/\b(5\d{2}|529|429)\b/.test(msg))
27885
+ return "retryable";
27886
+ if (errName === "ProviderAuthError" || errName === "ContextOverflowError")
27887
+ return "fatal";
27888
+ if (/\b4[0-9]{2}\b/.test(msg) && !/\b429\b/.test(msg))
27889
+ return "fatal";
27890
+ return "fatal";
27891
+ }
27892
+ function extractRetryAfterMs(err) {
27893
+ const headers = getResponseHeaders(err) ?? getResponseHeaders(err?.cause);
27894
+ if (!headers)
27895
+ return;
27896
+ const headerValue = findHeader(headers, "retry-after");
27897
+ if (!headerValue)
27898
+ return;
27899
+ return parseRetryAfterHeader(headerValue);
27900
+ }
27901
+ function getResponseHeaders(src) {
27902
+ if (!src || typeof src !== "object")
27903
+ return;
27904
+ const e = src;
27905
+ const data = e["data"];
27906
+ if (data && typeof data === "object") {
27907
+ const rh = data["responseHeaders"];
27908
+ if (rh && typeof rh === "object" && !Array.isArray(rh)) {
27909
+ return rh;
27910
+ }
27911
+ }
27912
+ return;
27913
+ }
27914
+ function findHeader(headers, name) {
27915
+ const lower = name.toLowerCase();
27916
+ for (const [k, v] of Object.entries(headers)) {
27917
+ if (k.toLowerCase() === lower)
27918
+ return v;
27919
+ }
27920
+ return;
27921
+ }
27922
+ function parseRetryAfterHeader(value) {
27923
+ const trimmed = value.trim();
27924
+ const seconds = parseFloat(trimmed);
27925
+ if (!isNaN(seconds) && seconds >= 0) {
27926
+ return Math.ceil(seconds * 1000);
27927
+ }
27928
+ const d = new Date(trimmed);
27929
+ if (!isNaN(d.getTime())) {
27930
+ const ms = d.getTime() - Date.now();
27931
+ return ms > 0 ? ms : 0;
27932
+ }
27933
+ return;
27934
+ }
27935
+ async function withLlmRetry(fn, opts = {}) {
27936
+ const maxRetries = opts.maxRetries ?? DEFAULT_RETRY_CONFIG.maxRetries;
27937
+ const baseDelayMs = opts.baseDelayMs ?? DEFAULT_RETRY_CONFIG.baseDelayMs;
27938
+ const maxDelayMs = opts.maxDelayMs ?? DEFAULT_RETRY_CONFIG.maxDelayMs;
27939
+ const jitterRatio = opts.jitterRatio ?? DEFAULT_RETRY_CONFIG.jitterRatio;
27940
+ const minAttemptWindowMs = opts.minAttemptWindowMs ?? DEFAULT_RETRY_CONFIG.minAttemptWindowMs;
27941
+ const maxTotalRetryMs = opts.maxTotalRetryMs;
27942
+ const maxAttempts = maxRetries + 1;
27943
+ const startedAt = Date.now();
27944
+ let lastError;
27945
+ let attempt = 0;
27946
+ while (attempt < maxAttempts) {
27947
+ if (attempt > 0 && opts.signal?.aborted) {
27948
+ const e = new Error("withLlmRetry: aborted by signal");
27949
+ e.name = "AbortError";
27950
+ throw e;
27951
+ }
27952
+ if (attempt > 0 && maxTotalRetryMs != null) {
27953
+ const elapsed = Date.now() - startedAt;
27954
+ const remaining = maxTotalRetryMs - elapsed;
27955
+ const backoff = calcBackoff(attempt, baseDelayMs, maxDelayMs, jitterRatio);
27956
+ const gate = Math.max(backoff, minAttemptWindowMs);
27957
+ if (remaining < gate) {
27958
+ opts.log?.("warn", `[llm-retry] budget gate: remaining=${remaining}ms < gate=${gate}ms, stopping after attempt ${attempt}`);
27959
+ throw lastError ?? new Error("withLlmRetry: budget exhausted");
27960
+ }
27961
+ }
27962
+ attempt++;
27963
+ try {
27964
+ const result = await fn();
27965
+ if (opts.isRetryableResult) {
27966
+ const failErr = opts.isRetryableResult(result);
27967
+ if (failErr) {
27968
+ const cls = classifyError(failErr);
27969
+ if (cls === "retryable" && attempt < maxAttempts) {
27970
+ lastError = failErr;
27971
+ const delayMs = await handleRetryDelay(failErr, attempt, maxAttempts, baseDelayMs, maxDelayMs, jitterRatio, opts);
27972
+ if (delayMs < 0)
27973
+ throw lastError;
27974
+ continue;
27975
+ }
27976
+ throw failErr;
27977
+ }
27978
+ }
27979
+ return result;
27980
+ } catch (err) {
27981
+ if (err.name === "AbortError")
27982
+ throw err;
27983
+ const cls = classifyError(err);
27984
+ if (cls === "fatal" || attempt >= maxAttempts) {
27985
+ throw err;
27986
+ }
27987
+ lastError = err;
27988
+ const delayMs = await handleRetryDelay(err, attempt, maxAttempts, baseDelayMs, maxDelayMs, jitterRatio, opts);
27989
+ if (delayMs < 0) {
27990
+ const e = new Error("withLlmRetry: aborted during retry sleep");
27991
+ e.name = "AbortError";
27992
+ throw e;
27993
+ }
27994
+ }
27995
+ }
27996
+ throw lastError ?? new Error("withLlmRetry: unexpected loop exit");
27997
+ }
27998
+ async function handleRetryDelay(err, attempt, maxAttempts, baseDelayMs, maxDelayMs, jitterRatio, opts) {
27999
+ const getRetryAfterMs = opts.getRetryAfterMs ?? extractRetryAfterMs;
28000
+ const backoff = calcBackoff(attempt, baseDelayMs, maxDelayMs, jitterRatio);
28001
+ const retryAfterMs = getRetryAfterMs(err);
28002
+ let delayMs;
28003
+ if (retryAfterMs != null) {
28004
+ delayMs = retryAfterMs;
28005
+ } else {
28006
+ if (isLikelyRateLimitError(err)) {
28007
+ opts.log?.("warn", `[llm-retry] 429/rate-limit 但拿不到 Retry-After, 使用默认 backoff=${backoff}ms`, {
28008
+ err: getMsg(err)
28009
+ });
28010
+ }
28011
+ delayMs = backoff;
28012
+ }
28013
+ opts.onRetry?.(attempt, maxAttempts, err, delayMs);
28014
+ opts.log?.("warn", `[llm-retry] attempt ${attempt}/${maxAttempts} failed: ${getMsg(err)}, retry in ${delayMs}ms`, { err: getMsg(err), delayMs });
28015
+ return sleep2(delayMs, opts.signal);
28016
+ }
28017
+ function calcBackoff(attempt, baseMs, maxMs, jitterRatio) {
28018
+ const expo = Math.min(baseMs * Math.pow(2, attempt - 1), maxMs);
28019
+ const jitter = expo * jitterRatio * (Math.random() * 2 - 1);
28020
+ return Math.max(0, Math.round(expo + jitter));
28021
+ }
28022
+ function sleep2(ms, signal) {
28023
+ if (signal?.aborted)
28024
+ return Promise.resolve(-1);
28025
+ if (ms <= 0)
28026
+ return Promise.resolve(0);
28027
+ return new Promise((resolve16) => {
28028
+ let settled = false;
28029
+ const timer = setTimeout(() => {
28030
+ if (settled)
28031
+ return;
28032
+ settled = true;
28033
+ signal?.removeEventListener("abort", onAbort);
28034
+ resolve16(ms);
28035
+ }, ms);
28036
+ const onAbort = () => {
28037
+ if (settled)
28038
+ return;
28039
+ settled = true;
28040
+ clearTimeout(timer);
28041
+ resolve16(-1);
28042
+ };
28043
+ signal?.addEventListener("abort", onAbort, { once: true });
28044
+ });
28045
+ }
28046
+ function isLikelyRateLimitError(err) {
28047
+ if (!err)
28048
+ return false;
28049
+ const msg = getMsg(err).toLowerCase();
28050
+ return msg.includes("429") || msg.includes("rate limit") || msg.includes("too many requests");
28051
+ }
28052
+ function getMsg(err) {
28053
+ if (!err)
28054
+ return "";
28055
+ if (err instanceof Error)
28056
+ return err.message;
28057
+ if (typeof err === "string")
28058
+ return err;
28059
+ try {
28060
+ return JSON.stringify(err);
28061
+ } catch {
28062
+ return String(err);
28063
+ }
28064
+ }
28065
+
27569
28066
  // lib/opencode-runner.ts
27570
28067
  function pickLastText(parts) {
27571
28068
  for (let i = parts.length - 1;i >= 0; i--) {
@@ -27699,66 +28196,85 @@ ${r.text.slice(0, 800)}`
27699
28196
  return { ok: true, summary: r.text || "(coder 无文本输出)" };
27700
28197
  }
27701
28198
  async runSubagent(opts, parentSessionIdOverride) {
27702
- let childId;
27703
- try {
27704
- const created = await this.opts.client.session.create({
27705
- body: { title: clip2(opts.title, 80) },
27706
- query: this.opts.directory ? { directory: this.opts.directory } : undefined
27707
- });
27708
- if (created.error || !created.data?.id) {
27709
- throw new Error(`session.create 失败: ${describe4(created.error) || "no id"}`);
28199
+ const retryCfg = this.opts.retry;
28200
+ const retryOpts = {
28201
+ maxRetries: retryCfg?.maxRetries ?? DEFAULT_RETRY_CONFIG.maxRetries,
28202
+ baseDelayMs: retryCfg?.baseDelayMs ?? DEFAULT_RETRY_CONFIG.baseDelayMs,
28203
+ maxDelayMs: retryCfg?.maxDelayMs ?? DEFAULT_RETRY_CONFIG.maxDelayMs,
28204
+ jitterRatio: retryCfg?.jitterRatio ?? DEFAULT_RETRY_CONFIG.jitterRatio,
28205
+ minAttemptWindowMs: retryCfg?.minAttemptWindowMs ?? DEFAULT_RETRY_CONFIG.minAttemptWindowMs,
28206
+ maxTotalRetryMs: retryCfg?.maxTotalRetryMs ?? opts.timeoutMs,
28207
+ signal: opts.signal,
28208
+ log: this.opts.log,
28209
+ isRetryableResult: (result) => {
28210
+ if (result.llmError == null)
28211
+ return;
28212
+ const cls = classifyError(result.llmError);
28213
+ return cls === "retryable" ? new Error(describe4(result.llmError)) : undefined;
27710
28214
  }
27711
- childId = created.data.id;
27712
- const parentSessionId = parentSessionIdOverride ?? this.opts.parentSessionId ?? "";
27713
- if (parentSessionId) {
27714
- try {
27715
- recordSessionParent(childId, parentSessionId);
27716
- } catch (err) {
27717
- this.opts.log?.("warn", `[spawner] recordSessionParent 失败 child=${childId}`, {
27718
- err: describe4(err),
27719
- parentSessionId
27720
- });
28215
+ };
28216
+ return withLlmRetry(async () => {
28217
+ let childId;
28218
+ try {
28219
+ const created = await this.opts.client.session.create({
28220
+ body: { title: clip2(opts.title, 80) },
28221
+ query: this.opts.directory ? { directory: this.opts.directory } : undefined
28222
+ });
28223
+ if (created.error || !created.data?.id) {
28224
+ throw new Error(`session.create 失败: ${describe4(created.error) || "no id"}`);
27721
28225
  }
27722
- }
27723
- const promptPromise = Promise.resolve(this.opts.client.session.prompt({
27724
- path: { id: childId },
27725
- body: {
27726
- agent: opts.agentName,
27727
- parts: [{ type: "text", text: opts.prompt }]
27728
- },
27729
- query: this.opts.directory ? { directory: this.opts.directory } : undefined
27730
- }));
27731
- const res = await raceAbortTimeout(promptPromise, opts.signal, opts.timeoutMs, opts.title);
27732
- if (res.error || !res.data) {
27733
- throw new Error(`session.prompt 失败: ${describe4(res.error) || "no data"}`);
27734
- }
27735
- const text = pickLastText(res.data.parts ?? []);
27736
- const finishReason = (res.data.info?.finish ?? "").toLowerCase();
27737
- const llmError = res.data.info?.error;
27738
- return llmError !== undefined ? { text, finishReason, llmError } : { text, finishReason };
27739
- } finally {
27740
- if (childId) {
27741
- try {
27742
- await this.opts.client.session.delete({
27743
- path: { id: childId },
27744
- query: this.opts.directory ? { directory: this.opts.directory } : undefined
27745
- });
27746
- } catch (err) {
27747
- this.opts.log?.("warn", `[spawner] session.delete 失败 ${childId}`, {
27748
- err: describe4(err)
27749
- });
28226
+ childId = created.data.id;
28227
+ const parentSessionId = parentSessionIdOverride ?? this.opts.parentSessionId ?? "";
28228
+ if (parentSessionId) {
28229
+ try {
28230
+ recordSessionParent(childId, parentSessionId);
28231
+ } catch (err) {
28232
+ this.opts.log?.("warn", `[spawner] recordSessionParent 失败 child=${childId}`, {
28233
+ err: describe4(err),
28234
+ parentSessionId
28235
+ });
28236
+ }
27750
28237
  }
27751
- const mainRoot = this.opts.mainRoot ?? this.opts.directory;
27752
- if (mainRoot) {
27753
- const cid = childId;
27754
- discardSession({ sessionId: cid, mainRoot }).catch((err) => {
27755
- this.opts.log?.("warn", `[spawner] auto-discard 失败 ${cid}`, {
28238
+ const promptPromise = Promise.resolve(this.opts.client.session.prompt({
28239
+ path: { id: childId },
28240
+ body: {
28241
+ agent: opts.agentName,
28242
+ parts: [{ type: "text", text: opts.prompt }]
28243
+ },
28244
+ query: this.opts.directory ? { directory: this.opts.directory } : undefined
28245
+ }));
28246
+ const res = await raceAbortTimeout(promptPromise, opts.signal, opts.timeoutMs, opts.title);
28247
+ if (res.error || !res.data) {
28248
+ throw new Error(`session.prompt 失败: ${describe4(res.error) || "no data"}`);
28249
+ }
28250
+ const text = pickLastText(res.data.parts ?? []);
28251
+ const finishReason = (res.data.info?.finish ?? "").toLowerCase();
28252
+ const llmError = res.data.info?.error;
28253
+ return llmError !== undefined ? { text, finishReason, llmError } : { text, finishReason };
28254
+ } finally {
28255
+ if (childId) {
28256
+ try {
28257
+ await this.opts.client.session.delete({
28258
+ path: { id: childId },
28259
+ query: this.opts.directory ? { directory: this.opts.directory } : undefined
28260
+ });
28261
+ } catch (err) {
28262
+ this.opts.log?.("warn", `[spawner] session.delete 失败 ${childId}`, {
27756
28263
  err: describe4(err)
27757
28264
  });
27758
- });
28265
+ }
28266
+ const mainRoot = this.opts.mainRoot ?? this.opts.directory;
28267
+ if (mainRoot) {
28268
+ const cid = childId;
28269
+ discardSession({ sessionId: cid, mainRoot }).catch((err) => {
28270
+ this.opts.log?.("warn", `[spawner] auto-discard 失败 ${cid}`, {
28271
+ err: describe4(err)
28272
+ });
28273
+ });
28274
+ }
27759
28275
  }
27760
28276
  }
27761
- }
28277
+ }, retryOpts);
27762
28278
  }
27763
28279
  }
27764
28280
  async function raceAbortTimeout(p, signal, timeoutMs, label) {
@@ -27774,7 +28290,9 @@ async function raceAbortTimeout(p, signal, timeoutMs, label) {
27774
28290
  return;
27775
28291
  settled = true;
27776
28292
  signal?.removeEventListener("abort", onAbort);
27777
- reject(new Error(`${label} 超时 (${timeoutMs}ms)`));
28293
+ const timeoutErr = new Error(`${label} 超时 (${timeoutMs}ms)`);
28294
+ timeoutErr["__cfTimeout"] = true;
28295
+ reject(timeoutErr);
27778
28296
  }, timeoutMs);
27779
28297
  const onAbort = () => {
27780
28298
  if (settled)
@@ -28335,10 +28853,11 @@ var codeforgeToolsServer = async (ctx) => {
28335
28853
  browser_enabled: browserEnabled,
28336
28854
  config_source: rt.ok ? "codeforge.json" : "built-in"
28337
28855
  });
28856
+ const planStore = new PlanStore({ root: ctx.directory ?? process.cwd() });
28338
28857
  __setContext2({
28339
28858
  resolveCurrentSessionId: () => process.env["CODEFORGE_SESSION_ID"],
28340
28859
  resolveMainRoot: () => ctx.directory ?? process.cwd(),
28341
- store: new PlanStore({ root: ctx.directory ?? process.cwd() })
28860
+ store: planStore
28342
28861
  });
28343
28862
  const spawner = new ProductionSpawner({
28344
28863
  client: ctx.client,
@@ -28349,7 +28868,8 @@ var codeforgeToolsServer = async (ctx) => {
28349
28868
  __setContext({
28350
28869
  mainRoot: ctx.directory ?? process.cwd(),
28351
28870
  spawner,
28352
- resolveCurrentSessionId: () => process.env["CODEFORGE_SESSION_ID"] ?? ""
28871
+ resolveCurrentSessionId: () => process.env["CODEFORGE_SESSION_ID"] ?? "",
28872
+ planStore
28353
28873
  });
28354
28874
  __setContext3({ mainRoot: ctx.directory ?? process.cwd() });
28355
28875
  const browserTools = browserEnabled ? buildBrowserTools() : {};
@@ -28627,7 +29147,7 @@ var handler7 = codeforgeToolsServer;
28627
29147
 
28628
29148
  // plugins/discover-spec-suggest.ts
28629
29149
  import { readFileSync as readFileSync3, readdirSync, statSync as statSync2 } from "node:fs";
28630
- import { join as join18 } from "node:path";
29150
+ import { join as join19 } from "node:path";
28631
29151
 
28632
29152
  // node_modules/yaml/dist/index.js
28633
29153
  var composer = require_composer();
@@ -28791,9 +29311,9 @@ function validateHandoff(rawYaml, fileSize) {
28791
29311
  const result = HandoffSchema.safeParse(parsed);
28792
29312
  if (!result.success) {
28793
29313
  const first = result.error.issues[0];
28794
- const path21 = first?.path?.join(".") ?? "(root)";
29314
+ const path22 = first?.path?.join(".") ?? "(root)";
28795
29315
  const msg = first?.message ?? "unknown";
28796
- return { ok: false, reason: `schema 校验失败:${path21}: ${msg}` };
29316
+ return { ok: false, reason: `schema 校验失败:${path22}: ${msg}` };
28797
29317
  }
28798
29318
  return { ok: true, data: result.data, schemaVersion: result.data.schema_version };
28799
29319
  }
@@ -28810,7 +29330,7 @@ var SESSION_TTL_MS2 = 24 * 60 * 60 * 1000;
28810
29330
  var MATCH_THRESHOLD = 0.15;
28811
29331
  var MAX_CANDIDATES = 3;
28812
29332
  var NUDGE_MAX_LEN = 1500;
28813
- var SPECS_REL_DIR = join18("docs", "specs");
29333
+ var SPECS_REL_DIR = join19("docs", "specs");
28814
29334
  var sessionMap = new Map;
28815
29335
  function pruneIfOversize2() {
28816
29336
  while (sessionMap.size > SESSION_CAP2) {
@@ -28917,7 +29437,7 @@ function loadSpecs(rootDir, opts = {}) {
28917
29437
  const dirExists = opts.dirExists ?? defaultDirExists;
28918
29438
  const statReader = opts.statReader ?? defaultStatReader;
28919
29439
  const log6 = makePluginLogger(PLUGIN_NAME8);
28920
- const specsRoot = join18(rootDir, SPECS_REL_DIR);
29440
+ const specsRoot = join19(rootDir, SPECS_REL_DIR);
28921
29441
  const records = [];
28922
29442
  if (!dirExists(specsRoot)) {
28923
29443
  log6.info(`specs 目录不存在,plugin 将 no-op`, { specsRoot });
@@ -28938,7 +29458,7 @@ function loadSpecs(rootDir, opts = {}) {
28938
29458
  log6.info(`跳过非合法 slug 命名的条目`, { entry });
28939
29459
  continue;
28940
29460
  }
28941
- const specDir = join18(specsRoot, entry);
29461
+ const specDir = join19(specsRoot, entry);
28942
29462
  let dirStat;
28943
29463
  try {
28944
29464
  dirStat = statReader(specDir);
@@ -28951,7 +29471,7 @@ function loadSpecs(rootDir, opts = {}) {
28951
29471
  }
28952
29472
  if (!dirStat.isDirectory)
28953
29473
  continue;
28954
- const handoffPath = join18(specDir, "handoff.yaml");
29474
+ const handoffPath = join19(specDir, "handoff.yaml");
28955
29475
  let fileStat;
28956
29476
  try {
28957
29477
  fileStat = statReader(handoffPath);
@@ -29123,14 +29643,14 @@ var discoverSpecSuggestServer = async (ctx) => {
29123
29643
  var handler8 = discoverSpecSuggestServer;
29124
29644
 
29125
29645
  // lib/memories.ts
29126
- import { promises as fs16 } from "node:fs";
29127
- import * as path21 from "node:path";
29128
- import * as os6 from "node:os";
29646
+ import { promises as fs17 } from "node:fs";
29647
+ import * as path22 from "node:path";
29648
+ import * as os7 from "node:os";
29129
29649
  function resolveConfig(c) {
29130
29650
  return {
29131
29651
  projectRoot: c.projectRoot,
29132
- homeDir: c.homeDir ?? os6.homedir(),
29133
- projectName: c.projectName ?? path21.basename(c.projectRoot),
29652
+ homeDir: c.homeDir ?? os7.homedir(),
29653
+ projectName: c.projectName ?? path22.basename(c.projectRoot),
29134
29654
  now: c.now ?? Date.now,
29135
29655
  log: c.log ?? (() => {}),
29136
29656
  maxPerScope: c.maxPerScope ?? 1000
@@ -29138,13 +29658,13 @@ function resolveConfig(c) {
29138
29658
  }
29139
29659
  function fileFor(scope, cfg) {
29140
29660
  if (scope === "project") {
29141
- return path21.join(cfg.projectRoot, ".codeforge", "memories.json");
29661
+ return path22.join(cfg.projectRoot, ".codeforge", "memories.json");
29142
29662
  }
29143
- return path21.join(cfg.homeDir, ".codeforge", "memories.json");
29663
+ return path22.join(cfg.homeDir, ".codeforge", "memories.json");
29144
29664
  }
29145
29665
  async function readBank(p) {
29146
29666
  try {
29147
- const raw = await fs16.readFile(p, "utf8");
29667
+ const raw = await fs17.readFile(p, "utf8");
29148
29668
  const arr = JSON.parse(raw);
29149
29669
  if (!Array.isArray(arr))
29150
29670
  return [];
@@ -29154,10 +29674,10 @@ async function readBank(p) {
29154
29674
  }
29155
29675
  }
29156
29676
  async function writeBank(p, items) {
29157
- await fs16.mkdir(path21.dirname(p), { recursive: true });
29677
+ await fs17.mkdir(path22.dirname(p), { recursive: true });
29158
29678
  const tmp = `${p}.tmp`;
29159
- await fs16.writeFile(tmp, JSON.stringify(items, null, 2), "utf8");
29160
- await fs16.rename(tmp, p);
29679
+ await fs17.writeFile(tmp, JSON.stringify(items, null, 2), "utf8");
29680
+ await fs17.rename(tmp, p);
29161
29681
  }
29162
29682
  function isMemory(x) {
29163
29683
  if (!x || typeof x !== "object")
@@ -29675,8 +30195,8 @@ var handler10 = modelFallbackServer;
29675
30195
 
29676
30196
  // plugins/parallel-tool-nudge.ts
29677
30197
  import { readFileSync as readFileSync4, readdirSync as readdirSync2, statSync as statSync3 } from "node:fs";
29678
- import { join as join20 } from "node:path";
29679
- import { homedir as homedir7 } from "node:os";
30198
+ import { join as join21 } from "node:path";
30199
+ import { homedir as homedir8 } from "node:os";
29680
30200
  var PLUGIN_NAME11 = "parallel-tool-nudge";
29681
30201
  logLifecycle(PLUGIN_NAME11, "import", {});
29682
30202
  var PARALLEL_SAFE_TOOLS = [
@@ -29728,10 +30248,10 @@ function loadAgentToolsMap(rootDir, opts = {}) {
29728
30248
  const reader = opts.reader ?? defaultReader2;
29729
30249
  const dirReader = opts.dirReader ?? defaultDirReader2;
29730
30250
  const dirExists = opts.dirExists ?? defaultDirExists2;
29731
- const homeAgentsDir = opts.homeAgentsDir ?? join20(homedir7(), ".config", "opencode", "agents");
30251
+ const homeAgentsDir = opts.homeAgentsDir ?? join21(homedir8(), ".config", "opencode", "agents");
29732
30252
  const candidateDirs = [
29733
- join20(rootDir, ".codeforge", "agents"),
29734
- join20(rootDir, "agents"),
30253
+ join21(rootDir, ".codeforge", "agents"),
30254
+ join21(rootDir, "agents"),
29735
30255
  homeAgentsDir
29736
30256
  ];
29737
30257
  const result = new Map;
@@ -29754,20 +30274,20 @@ function loadAgentToolsMap(rootDir, opts = {}) {
29754
30274
  for (const entry of entries) {
29755
30275
  if (!entry.endsWith(".md"))
29756
30276
  continue;
29757
- const path22 = join20(dir, entry);
30277
+ const path23 = join21(dir, entry);
29758
30278
  let content;
29759
30279
  try {
29760
- content = reader(path22);
30280
+ content = reader(path23);
29761
30281
  } catch (err) {
29762
30282
  log7.warn(`agent.md 读取失败(已跳过)`, {
29763
- path: path22,
30283
+ path: path23,
29764
30284
  error: err instanceof Error ? err.message : String(err)
29765
30285
  });
29766
30286
  continue;
29767
30287
  }
29768
30288
  const parsed = parseAgentFrontmatter(content);
29769
30289
  if (!parsed) {
29770
- log7.warn(`agent frontmatter 解析失败(已跳过)`, { path: path22 });
30290
+ log7.warn(`agent frontmatter 解析失败(已跳过)`, { path: path23 });
29771
30291
  continue;
29772
30292
  }
29773
30293
  if (result.has(parsed.name))
@@ -29958,18 +30478,18 @@ var handler12 = async (_ctx4) => {
29958
30478
  };
29959
30479
 
29960
30480
  // lib/event-stream.ts
29961
- import { promises as fs17 } from "node:fs";
29962
- import * as path22 from "node:path";
30481
+ import { promises as fs18 } from "node:fs";
30482
+ import * as path23 from "node:path";
29963
30483
  async function loadSession(id, opts = {}) {
29964
30484
  const file2 = resolveSessionFile(id, opts);
29965
- const raw = await fs17.readFile(file2, "utf8");
30485
+ const raw = await fs18.readFile(file2, "utf8");
29966
30486
  return parseJsonl(id, raw);
29967
30487
  }
29968
30488
  async function listSessions(opts = {}) {
29969
30489
  const dir = resolveDir(opts);
29970
30490
  let entries;
29971
30491
  try {
29972
- entries = await fs17.readdir(dir, { withFileTypes: true });
30492
+ entries = await fs18.readdir(dir, { withFileTypes: true });
29973
30493
  } catch (err) {
29974
30494
  if (err.code === "ENOENT")
29975
30495
  return [];
@@ -29979,10 +30499,10 @@ async function listSessions(opts = {}) {
29979
30499
  for (const e of entries) {
29980
30500
  if (!e.isFile() || !e.name.endsWith(".jsonl"))
29981
30501
  continue;
29982
- const file2 = path22.join(dir, e.name);
30502
+ const file2 = path23.join(dir, e.name);
29983
30503
  const id = e.name.replace(/\.jsonl$/, "");
29984
30504
  try {
29985
- const stat = await fs17.stat(file2);
30505
+ const stat = await fs18.stat(file2);
29986
30506
  const headerLine = await readFirstLine(file2);
29987
30507
  let started_at = stat.birthtimeMs;
29988
30508
  if (headerLine) {
@@ -30006,11 +30526,11 @@ async function listSessions(opts = {}) {
30006
30526
  return out;
30007
30527
  }
30008
30528
  function resolveDir(opts = {}) {
30009
- const root = path22.resolve(opts.root ?? process.cwd());
30010
- return opts.sessions_dir ? path22.resolve(root, opts.sessions_dir) : path22.join(runtimeDir(root), "sessions");
30529
+ const root = path23.resolve(opts.root ?? process.cwd());
30530
+ return opts.sessions_dir ? path23.resolve(root, opts.sessions_dir) : path23.join(runtimeDir(root), "sessions");
30011
30531
  }
30012
30532
  function resolveSessionFile(id, opts = {}) {
30013
- return path22.join(resolveDir(opts), `${id}.jsonl`);
30533
+ return path23.join(resolveDir(opts), `${id}.jsonl`);
30014
30534
  }
30015
30535
  function parseJsonl(id, raw) {
30016
30536
  const events = [];
@@ -30045,7 +30565,7 @@ function isEvent(obj) {
30045
30565
  }
30046
30566
  async function readFirstLine(file2) {
30047
30567
  const buf = Buffer.alloc(4096);
30048
- const fh = await fs17.open(file2, "r");
30568
+ const fh = await fs18.open(file2, "r");
30049
30569
  try {
30050
30570
  const { bytesRead } = await fh.read(buf, 0, buf.length, 0);
30051
30571
  const s = buf.subarray(0, bytesRead).toString("utf8");
@@ -30273,11 +30793,11 @@ function isRecoveryWorthShowing(plan) {
30273
30793
  }
30274
30794
 
30275
30795
  // lib/block-pending.ts
30276
- import { promises as fs18 } from "node:fs";
30277
- import * as path23 from "node:path";
30796
+ import { promises as fs19 } from "node:fs";
30797
+ import * as path24 from "node:path";
30278
30798
  function blockPendingFilePath(absRoot) {
30279
30799
  const rd = runtimeDir(absRoot, { ensure: false });
30280
- return path23.join(rd, "sessions", "autonomous-blocks.ndjson");
30800
+ return path24.join(rd, "sessions", "autonomous-blocks.ndjson");
30281
30801
  }
30282
30802
  function consumeLockPath(absRoot) {
30283
30803
  return blockPendingFilePath(absRoot) + ".consume.lock";
@@ -30286,7 +30806,7 @@ async function scanBlockPending(absRoot, filterSessionId) {
30286
30806
  const file2 = blockPendingFilePath(absRoot);
30287
30807
  let raw;
30288
30808
  try {
30289
- raw = await fs18.readFile(file2, "utf8");
30809
+ raw = await fs19.readFile(file2, "utf8");
30290
30810
  } catch {
30291
30811
  return [];
30292
30812
  }
@@ -30342,7 +30862,7 @@ async function markBlocksConsumed(absRoot, entries) {
30342
30862
  if (entries.length === 0)
30343
30863
  return;
30344
30864
  const file2 = blockPendingFilePath(absRoot);
30345
- await fs18.mkdir(path23.dirname(file2), { recursive: true });
30865
+ await fs19.mkdir(path24.dirname(file2), { recursive: true });
30346
30866
  const now = new Date().toISOString();
30347
30867
  const lines = entries.map((e) => ({
30348
30868
  type: "consume",
@@ -30353,7 +30873,7 @@ async function markBlocksConsumed(absRoot, entries) {
30353
30873
  `) + `
30354
30874
  `;
30355
30875
  await withFileLock(consumeLockPath(absRoot), async () => {
30356
- await fs18.appendFile(file2, lines, "utf8");
30876
+ await fs19.appendFile(file2, lines, "utf8");
30357
30877
  });
30358
30878
  }
30359
30879
 
@@ -30492,7 +31012,7 @@ var handler13 = sessionRecoveryServer;
30492
31012
 
30493
31013
  // plugins/subtask-heartbeat.ts
30494
31014
  import { promises as fsPromises } from "node:fs";
30495
- import * as path24 from "node:path";
31015
+ import * as path25 from "node:path";
30496
31016
  var recordSessionParent2 = recordSessionParent;
30497
31017
  var lookupParentSessionId2 = lookupParentSessionId;
30498
31018
  var deleteSessionParent2 = deleteSessionParent;
@@ -30833,7 +31353,7 @@ function buildErrorLogLine(errorReason, elapsedMs, now = Date.now()) {
30833
31353
  }
30834
31354
  async function appendSubagentLog(filePath, line, log9) {
30835
31355
  try {
30836
- await fsPromises.mkdir(path24.dirname(filePath), { recursive: true });
31356
+ await fsPromises.mkdir(path25.dirname(filePath), { recursive: true });
30837
31357
  await fsPromises.appendFile(filePath, line + `
30838
31358
  `, "utf8");
30839
31359
  } catch (err) {
@@ -31135,8 +31655,8 @@ var subtaskHeartbeatServer = async (ctx) => {
31135
31655
  var handler14 = subtaskHeartbeatServer;
31136
31656
 
31137
31657
  // plugins/subtasks.ts
31138
- import { promises as fs19 } from "node:fs";
31139
- import * as path25 from "node:path";
31658
+ import { promises as fs20 } from "node:fs";
31659
+ import * as path26 from "node:path";
31140
31660
 
31141
31661
  // lib/parallel-merge.ts
31142
31662
  init_worktree_ops();
@@ -31144,7 +31664,7 @@ init_worktree_ops();
31144
31664
  // plugins/subtasks.ts
31145
31665
  var PLUGIN_NAME15 = "subtasks";
31146
31666
  function getLogFile(root = process.cwd()) {
31147
- return path25.join(runtimeDir(root), "logs", "subtasks.log");
31667
+ return path26.join(runtimeDir(root), "logs", "subtasks.log");
31148
31668
  }
31149
31669
  async function writeLog(level, msg, data) {
31150
31670
  const line = JSON.stringify({
@@ -31157,8 +31677,8 @@ async function writeLog(level, msg, data) {
31157
31677
  `;
31158
31678
  try {
31159
31679
  const logFile = getLogFile();
31160
- await fs19.mkdir(path25.dirname(logFile), { recursive: true });
31161
- await fs19.appendFile(logFile, line, "utf8");
31680
+ await fs20.mkdir(path26.dirname(logFile), { recursive: true });
31681
+ await fs20.appendFile(logFile, line, "utf8");
31162
31682
  } catch {}
31163
31683
  }
31164
31684
  logLifecycle(PLUGIN_NAME15, "import");
@@ -31732,8 +32252,8 @@ var tokenManagerServer = async (ctx) => {
31732
32252
  var handler17 = tokenManagerServer;
31733
32253
 
31734
32254
  // plugins/tool-policy.ts
31735
- import { promises as fs20 } from "node:fs";
31736
- import * as path27 from "node:path";
32255
+ import { promises as fs21 } from "node:fs";
32256
+ import * as path28 from "node:path";
31737
32257
 
31738
32258
  // lib/tool-risk.ts
31739
32259
  var RISK_PATTERNS = [
@@ -31887,7 +32407,7 @@ function buildHaystackFor(args, matchOn) {
31887
32407
  }
31888
32408
 
31889
32409
  // lib/file-regex-acl.ts
31890
- import * as path26 from "node:path";
32410
+ import * as path27 from "node:path";
31891
32411
  function compileRule(r) {
31892
32412
  if (r instanceof RegExp)
31893
32413
  return r;
@@ -31953,7 +32473,7 @@ function normalizePath(p) {
31953
32473
  let s = p.replace(/\\/g, "/");
31954
32474
  if (s.startsWith("./"))
31955
32475
  s = s.slice(2);
31956
- s = path26.posix.normalize(s);
32476
+ s = path27.posix.normalize(s);
31957
32477
  return s;
31958
32478
  }
31959
32479
  function checkFileAccess(acl, file2, op) {
@@ -32056,11 +32576,11 @@ function decideToolCall(ctx, cfg = {}, currentAgent) {
32056
32576
  const action = risks.length > 0 || worstAcl === "deny" ? "deny" : "allow";
32057
32577
  return { action, reasons, risks, acl: aclResults };
32058
32578
  }
32059
- var POLICY_PATH = path27.join(".codeforge", "policy.json");
32579
+ var POLICY_PATH = path28.join(".codeforge", "policy.json");
32060
32580
  async function loadPolicy(root = process.cwd()) {
32061
- const file2 = path27.join(root, POLICY_PATH);
32581
+ const file2 = path28.join(root, POLICY_PATH);
32062
32582
  try {
32063
- const raw = await fs20.readFile(file2, "utf8");
32583
+ const raw = await fs21.readFile(file2, "utf8");
32064
32584
  const data = JSON.parse(raw);
32065
32585
  return data;
32066
32586
  } catch {
@@ -32157,20 +32677,20 @@ var handler18 = toolPolicyServer;
32157
32677
 
32158
32678
  // plugins/update-checker.ts
32159
32679
  import { closeSync, existsSync as existsSync5, mkdirSync as mkdirSync3, openSync, readFileSync as readFileSync6, rmSync, statSync as statSync4, writeFileSync as writeFileSync2, writeSync } from "node:fs";
32160
- import { homedir as homedir8 } from "node:os";
32161
- import { dirname as dirname16, join as join26 } from "node:path";
32680
+ import { homedir as homedir9 } from "node:os";
32681
+ import { dirname as dirname16, join as join27 } from "node:path";
32162
32682
  import { spawn } from "node:child_process";
32163
32683
 
32164
32684
  // lib/update-checker-impl.ts
32165
32685
  import { readFileSync as readFileSync5 } from "node:fs";
32166
- import { dirname as dirname15, join as join25 } from "node:path";
32686
+ import { dirname as dirname15, join as join26 } from "node:path";
32167
32687
  import { fileURLToPath as fileURLToPath2 } from "node:url";
32168
32688
  import * as https from "node:https";
32169
32689
 
32170
32690
  // lib/version-injected.ts
32171
32691
  function getInjectedVersion() {
32172
32692
  try {
32173
- const v = "0.8.11";
32693
+ const v = "0.8.12";
32174
32694
  if (typeof v === "string" && /^\d+\.\d+\.\d+/.test(v)) {
32175
32695
  return v;
32176
32696
  }
@@ -32218,7 +32738,7 @@ function readLocalVersion() {
32218
32738
  try {
32219
32739
  const here = fileURLToPath2(import.meta.url);
32220
32740
  const root = dirname15(dirname15(here));
32221
- const pkg = JSON.parse(readFileSync5(join25(root, "package.json"), "utf8"));
32741
+ const pkg = JSON.parse(readFileSync5(join26(root, "package.json"), "utf8"));
32222
32742
  return typeof pkg.version === "string" ? pkg.version : "0.0.0";
32223
32743
  } catch {
32224
32744
  return "0.0.0";
@@ -32298,7 +32818,7 @@ var PLUGIN_NAME19 = "update-checker";
32298
32818
  var PLUGIN_VERSION = "3.0.0";
32299
32819
  var _updateCheckStarted = false;
32300
32820
  function getCacheFile() {
32301
- return join26(process.env["CODEFORGE_CACHE_DIR"] ?? join26(homedir8(), ".cache", "codeforge"), "update-check.json");
32821
+ return join27(process.env["CODEFORGE_CACHE_DIR"] ?? join27(homedir9(), ".cache", "codeforge"), "update-check.json");
32302
32822
  }
32303
32823
  function readLastInstalledVersion() {
32304
32824
  try {
@@ -32319,7 +32839,7 @@ function writeLastInstalledVersion(v) {
32319
32839
  } catch {}
32320
32840
  }
32321
32841
  function getLockFile() {
32322
- return join26(process.env["CODEFORGE_CACHE_DIR"] ?? join26(homedir8(), ".cache", "codeforge"), "install.lock");
32842
+ return join27(process.env["CODEFORGE_CACHE_DIR"] ?? join27(homedir9(), ".cache", "codeforge"), "install.lock");
32323
32843
  }
32324
32844
  function tryAcquireInstallLock() {
32325
32845
  try {
@@ -32512,7 +33032,7 @@ var updateCheckerServer = async (ctx) => {
32512
33032
  try {
32513
33033
  const npmRoot2 = await getNpmGlobalRoot(npmBin);
32514
33034
  if (npmRoot2) {
32515
- const oldPkgDir = join26(npmRoot2, ...u.package.split("/"));
33035
+ const oldPkgDir = join27(npmRoot2, ...u.package.split("/"));
32516
33036
  if (existsSync5(oldPkgDir)) {
32517
33037
  rmSync(oldPkgDir, { recursive: true, force: true });
32518
33038
  safeWriteLog(PLUGIN_NAME19, { level: "info", msg: "npm_install_old_dir_removed", path: oldPkgDir });
@@ -32530,8 +33050,8 @@ var updateCheckerServer = async (ctx) => {
32530
33050
  }
32531
33051
  safeWriteLog(PLUGIN_NAME19, { level: "info", msg: "npm_install_success", remote });
32532
33052
  try {
32533
- const cacheRoot = process.env["XDG_CACHE_HOME"] ?? join26(homedir8(), ".cache");
32534
- const opencodeCache = join26(cacheRoot, "opencode", "packages", "@andyqiu", "codeforge@latest");
33053
+ const cacheRoot = process.env["XDG_CACHE_HOME"] ?? join27(homedir9(), ".cache");
33054
+ const opencodeCache = join27(cacheRoot, "opencode", "packages", "@andyqiu", "codeforge@latest");
32535
33055
  if (existsSync5(opencodeCache)) {
32536
33056
  rmSync(opencodeCache, { recursive: true, force: true });
32537
33057
  safeWriteLog(PLUGIN_NAME19, { level: "info", msg: "opencode_plugin_cache_cleared", path: opencodeCache });
@@ -32540,8 +33060,8 @@ var updateCheckerServer = async (ctx) => {
32540
33060
  safeWriteLog(PLUGIN_NAME19, { level: "warn", msg: "opencode_plugin_cache_clear_failed", error: e.message });
32541
33061
  }
32542
33062
  const npmRoot = await getNpmGlobalRoot(npmBin);
32543
- const pkgRoot = npmRoot ? join26(npmRoot, "@andyqiu", "codeforge") : null;
32544
- const installMjs = pkgRoot ? join26(pkgRoot, "install.mjs") : null;
33063
+ const pkgRoot = npmRoot ? join27(npmRoot, "@andyqiu", "codeforge") : null;
33064
+ const installMjs = pkgRoot ? join27(pkgRoot, "install.mjs") : null;
32545
33065
  if (!installMjs || !existsSync5(installMjs)) {
32546
33066
  safeWriteLog(PLUGIN_NAME19, { level: "warn", msg: "install_mjs_not_found", path: installMjs ?? "null" });
32547
33067
  await postToast(ctx, `[codeforge] ⚠ npm 包已升级 ${local} → ${remote},但资产部署未完成。下次启动将重试,或手动运行:codeforge upgrade`);
@@ -32580,12 +33100,195 @@ async function postToast(ctx, message) {
32580
33100
  }
32581
33101
  var handler19 = updateCheckerServer;
32582
33102
 
32583
- // plugins/workflow-engine.ts
33103
+ // plugins/user-merge-confirm.ts
33104
+ import { execFile as execFile4 } from "node:child_process";
33105
+ import { promisify as promisify2 } from "node:util";
33106
+
33107
+ // lib/merge-confirm-semantics.ts
33108
+ import { promises as fs22 } from "node:fs";
32584
33109
  import * as path29 from "node:path";
33110
+ var RULES_REL_PATH = "workflows/_merge-confirm-semantics.yaml";
33111
+ async function loadMergeConfirmSemantics(repoRoot) {
33112
+ const file2 = path29.join(repoRoot, RULES_REL_PATH);
33113
+ const txt = await fs22.readFile(file2, "utf8");
33114
+ return $parse(txt);
33115
+ }
33116
+ function includesAny(haystack, needles) {
33117
+ const lower = haystack.toLowerCase();
33118
+ return needles.some((n) => lower.includes(n.toLowerCase()));
33119
+ }
33120
+ function stripAffirmatives(text, affirmatives) {
33121
+ let lower = text.toLowerCase();
33122
+ let matched = false;
33123
+ const sorted = [...affirmatives].sort((a, b) => b.length - a.length);
33124
+ for (const phrase of sorted) {
33125
+ const p = phrase.toLowerCase();
33126
+ if (lower.includes(p)) {
33127
+ matched = true;
33128
+ lower = lower.split(p).join(" ");
33129
+ }
33130
+ }
33131
+ return { matched, remainder: lower };
33132
+ }
33133
+ function classifyMergeConfirm(text, rules) {
33134
+ if (!text || !text.trim())
33135
+ return "NOT_CONFIRMED";
33136
+ const { matched, remainder } = stripAffirmatives(text, rules.affirmative);
33137
+ if (!matched)
33138
+ return "NOT_CONFIRMED";
33139
+ if (includesAny(remainder, rules.ambiguous_or_negative))
33140
+ return "NOT_CONFIRMED";
33141
+ if (rules.confirm_plus_new_request_is_not_confirmed && includesAny(remainder, rules.new_request_signals)) {
33142
+ return "NOT_CONFIRMED";
33143
+ }
33144
+ return "CONFIRMED";
33145
+ }
33146
+
33147
+ // plugins/user-merge-confirm.ts
33148
+ var PLUGIN_NAME20 = "user-merge-confirm";
33149
+ var execFileAsync = promisify2(execFile4);
33150
+ logLifecycle(PLUGIN_NAME20, "import", {});
33151
+ async function getWorktreeHead(worktreePath) {
33152
+ if (!worktreePath)
33153
+ return "";
33154
+ try {
33155
+ const { stdout } = await execFileAsync("git", ["rev-parse", "HEAD"], {
33156
+ cwd: worktreePath,
33157
+ timeout: 5000
33158
+ });
33159
+ return stdout.trim();
33160
+ } catch {
33161
+ return "";
33162
+ }
33163
+ }
33164
+ var userMergeConfirmPlugin = async (ctx) => {
33165
+ const mainRoot = ctx.directory;
33166
+ logLifecycle(PLUGIN_NAME20, "activate", { directory: mainRoot });
33167
+ return {
33168
+ "chat.message": async (input, _output) => {
33169
+ await safeAsync(PLUGIN_NAME20, "chat.message", async () => {
33170
+ if (input?.role !== "user")
33171
+ return;
33172
+ const sid = input?.sessionID;
33173
+ if (typeof sid !== "string" || !sid)
33174
+ return;
33175
+ const owner = await resolveWorktreeOwner({
33176
+ sessionId: sid,
33177
+ mainRoot
33178
+ }).catch(() => null);
33179
+ if (!owner || !owner.ok) {
33180
+ safeWriteLog(PLUGIN_NAME20, {
33181
+ hook: "chat.message",
33182
+ sid,
33183
+ action: "skip_registry_unreadable",
33184
+ via: owner?.via
33185
+ });
33186
+ return;
33187
+ }
33188
+ const targetRootSessionId = owner.ownerSessionId;
33189
+ if (sid !== targetRootSessionId) {
33190
+ safeWriteLog(PLUGIN_NAME20, {
33191
+ hook: "chat.message",
33192
+ sid,
33193
+ action: "skip_child_session",
33194
+ ownerSessionId: targetRootSessionId
33195
+ });
33196
+ return;
33197
+ }
33198
+ const text = extractUserText(input);
33199
+ if (!text?.trim())
33200
+ return;
33201
+ const entry = owner.entry;
33202
+ if (!entry || entry.status !== "active") {
33203
+ return;
33204
+ }
33205
+ if (entry.kind === "parallel-lane" || entry.kind === "merge-fix")
33206
+ return;
33207
+ let approval;
33208
+ try {
33209
+ const store = ApprovalStore.forProject(mainRoot);
33210
+ approval = await store.getLatest(`session:${targetRootSessionId}`);
33211
+ } catch {
33212
+ return;
33213
+ }
33214
+ if (!approval)
33215
+ return;
33216
+ if (approval.verdict !== "APPROVE" && approval.verdict !== "APPROVE_WITH_NOTES")
33217
+ return;
33218
+ if (!approval.coveredSha) {
33219
+ safeWriteLog(PLUGIN_NAME20, {
33220
+ hook: "chat.message",
33221
+ sid: targetRootSessionId,
33222
+ action: "no_covered_sha_early_exit"
33223
+ });
33224
+ return;
33225
+ }
33226
+ let mergeConfirmRules;
33227
+ try {
33228
+ const semantics = await loadMergeConfirmSemantics(mainRoot);
33229
+ mergeConfirmRules = semantics.merge_confirm;
33230
+ } catch {
33231
+ safeWriteLog(PLUGIN_NAME20, {
33232
+ hook: "chat.message",
33233
+ sid: targetRootSessionId,
33234
+ action: "skip_rules_load_failed"
33235
+ });
33236
+ return;
33237
+ }
33238
+ const verdict = classifyMergeConfirm(text, mergeConfirmRules);
33239
+ if (verdict !== "CONFIRMED") {
33240
+ safeWriteLog(PLUGIN_NAME20, {
33241
+ hook: "chat.message",
33242
+ sid: targetRootSessionId,
33243
+ action: "not_confirmed",
33244
+ text_preview: text.slice(0, 50)
33245
+ });
33246
+ return;
33247
+ }
33248
+ const currentHead = await getWorktreeHead(entry.worktreePath);
33249
+ if (!currentHead) {
33250
+ safeWriteLog(PLUGIN_NAME20, {
33251
+ hook: "chat.message",
33252
+ sid: targetRootSessionId,
33253
+ action: "skip_head_unavailable",
33254
+ worktreePath: entry.worktreePath
33255
+ });
33256
+ return;
33257
+ }
33258
+ await writeConfirmRecord(mainRoot, {
33259
+ sessionId: targetRootSessionId,
33260
+ worktreeHeadSha: currentHead,
33261
+ approvalSha: approval.coveredSha,
33262
+ ts: Date.now(),
33263
+ text,
33264
+ status: "PENDING"
33265
+ });
33266
+ safeWriteLog(PLUGIN_NAME20, {
33267
+ hook: "chat.message",
33268
+ sid: targetRootSessionId,
33269
+ action: "confirm_recorded",
33270
+ head: currentHead.slice(0, 8),
33271
+ approvalSha: approval.coveredSha.slice(0, 8)
33272
+ });
33273
+ safeWriteLog(PLUGIN_NAME20, {
33274
+ hook: "chat.message",
33275
+ sid: targetRootSessionId,
33276
+ action: "anchor_notice",
33277
+ anchor: `[CODEFORGE_CONFIRM_PENDING sid=${targetRootSessionId}]`
33278
+ });
33279
+ console.log(`[CODEFORGE_CONFIRM_PENDING sid=${targetRootSessionId}] ` + `已记录合入确认,codeforge 将执行合入。`);
33280
+ });
33281
+ }
33282
+ };
33283
+ };
33284
+ var handler20 = userMergeConfirmPlugin;
33285
+
33286
+ // plugins/workflow-engine.ts
33287
+ import * as path31 from "node:path";
32585
33288
 
32586
33289
  // lib/workflow-loader.ts
32587
- import { promises as fs21 } from "node:fs";
32588
- import * as path28 from "node:path";
33290
+ import { promises as fs23 } from "node:fs";
33291
+ import * as path30 from "node:path";
32589
33292
  var ActionSchema = exports_external.object({
32590
33293
  tool: exports_external.string().min(1, "action.tool 不能为空"),
32591
33294
  args: exports_external.record(exports_external.string(), exports_external.unknown()).optional().default({}),
@@ -32670,7 +33373,7 @@ function parseWorkflowYaml(yaml, sourcePath = "<inline>") {
32670
33373
  async function loadWorkflowFromFile(filePath) {
32671
33374
  let txt;
32672
33375
  try {
32673
- txt = await fs21.readFile(filePath, "utf8");
33376
+ txt = await fs23.readFile(filePath, "utf8");
32674
33377
  } catch (err) {
32675
33378
  return {
32676
33379
  ok: false,
@@ -32685,7 +33388,7 @@ async function loadWorkflowsFromDir(dir) {
32685
33388
  const failed = [];
32686
33389
  let entries;
32687
33390
  try {
32688
- entries = await fs21.readdir(dir);
33391
+ entries = await fs23.readdir(dir);
32689
33392
  } catch (err) {
32690
33393
  const e = err;
32691
33394
  if (e.code === "ENOENT")
@@ -32697,7 +33400,7 @@ async function loadWorkflowsFromDir(dir) {
32697
33400
  continue;
32698
33401
  if (!/\.ya?ml$/i.test(name))
32699
33402
  continue;
32700
- const full = path28.join(dir, name);
33403
+ const full = path30.join(dir, name);
32701
33404
  const r = await loadWorkflowFromFile(full);
32702
33405
  if (r.ok)
32703
33406
  loaded.push(r);
@@ -33032,9 +33735,9 @@ async function runStepAutoFeedback(step, adapter) {
33032
33735
  }
33033
33736
 
33034
33737
  // plugins/workflow-engine.ts
33035
- var PLUGIN_NAME20 = "workflow-engine";
33036
- logLifecycle(PLUGIN_NAME20, "import", {});
33037
- var fallbackLog2 = makePluginLogger(PLUGIN_NAME20);
33738
+ var PLUGIN_NAME21 = "workflow-engine";
33739
+ logLifecycle(PLUGIN_NAME21, "import", {});
33740
+ var fallbackLog2 = makePluginLogger(PLUGIN_NAME21);
33038
33741
  var _registry = null;
33039
33742
  async function loadRegistry(workflowsDir) {
33040
33743
  const { loaded, failed } = await loadWorkflowsFromDir(workflowsDir);
@@ -33054,32 +33757,32 @@ async function handleCommandInvoked(raw, workflowsDir = "workflows") {
33054
33757
  const log13 = ctx.log ?? fallbackLog2;
33055
33758
  const command = typeof ctx.command === "string" ? ctx.command : null;
33056
33759
  if (!command) {
33057
- log13.warn(`[${PLUGIN_NAME20}] command.invoked 缺 command 字段`, ctx);
33760
+ log13.warn(`[${PLUGIN_NAME21}] command.invoked 缺 command 字段`, ctx);
33058
33761
  return null;
33059
33762
  }
33060
33763
  const reg = await ensureRegistry(workflowsDir);
33061
33764
  if (reg.errors.length) {
33062
- log13.warn(`[${PLUGIN_NAME20}] 有 ${reg.errors.length} 个 workflow 加载失败`, reg.errors);
33765
+ log13.warn(`[${PLUGIN_NAME21}] 有 ${reg.errors.length} 个 workflow 加载失败`, reg.errors);
33063
33766
  }
33064
33767
  const wf = reg.workflows.find((w) => matchesTrigger(w, command));
33065
33768
  if (!wf) {
33066
- log13.info(`[${PLUGIN_NAME20}] no workflow matches "${command}"`);
33769
+ log13.info(`[${PLUGIN_NAME21}] no workflow matches "${command}"`);
33067
33770
  return null;
33068
33771
  }
33069
- log13.info(`[${PLUGIN_NAME20}] dispatch "${command}" → workflow "${wf.name}"`);
33772
+ log13.info(`[${PLUGIN_NAME21}] dispatch "${command}" → workflow "${wf.name}"`);
33070
33773
  try {
33071
33774
  const result = await run(wf, {
33072
33775
  mode: ctx.adapter ? "real" : "dry_run",
33073
33776
  autonomy: ctx.autonomy ?? "semi",
33074
33777
  adapter: ctx.adapter
33075
33778
  });
33076
- log13.info(`[${PLUGIN_NAME20}] workflow "${wf.name}" 完成 (${result.plan.mode})`, {
33779
+ log13.info(`[${PLUGIN_NAME21}] workflow "${wf.name}" 完成 (${result.plan.mode})`, {
33077
33780
  steps: result.plan.steps.length,
33078
33781
  results: result.results.length
33079
33782
  });
33080
33783
  return result;
33081
33784
  } catch (err) {
33082
- log13.error(`[${PLUGIN_NAME20}] workflow "${wf.name}" 执行失败`, {
33785
+ log13.error(`[${PLUGIN_NAME21}] workflow "${wf.name}" 执行失败`, {
33083
33786
  error: err instanceof Error ? err.message : String(err)
33084
33787
  });
33085
33788
  throw err;
@@ -33087,16 +33790,16 @@ async function handleCommandInvoked(raw, workflowsDir = "workflows") {
33087
33790
  }
33088
33791
  var workflowEngineServer = async (ctx) => {
33089
33792
  const directory = ctx.directory ?? process.cwd();
33090
- const workflowsDir = path29.join(directory, "workflows");
33091
- ensureRegistry(workflowsDir).catch((err) => fallbackLog2.warn(`[${PLUGIN_NAME20}] preload workflows failed`, {
33793
+ const workflowsDir = path31.join(directory, "workflows");
33794
+ ensureRegistry(workflowsDir).catch((err) => fallbackLog2.warn(`[${PLUGIN_NAME21}] preload workflows failed`, {
33092
33795
  error: err instanceof Error ? err.message : String(err)
33093
33796
  }));
33094
- logLifecycle(PLUGIN_NAME20, "activate", { directory, workflowsDir });
33797
+ logLifecycle(PLUGIN_NAME21, "activate", { directory, workflowsDir });
33095
33798
  return {
33096
33799
  "command.execute.before": async (input, output) => {
33097
- await safeAsync(PLUGIN_NAME20, "command.execute.before", async () => {
33800
+ await safeAsync(PLUGIN_NAME21, "command.execute.before", async () => {
33098
33801
  const cmd = input.command.startsWith("/") ? input.command : `/${input.command}`;
33099
- safeWriteLog(PLUGIN_NAME20, {
33802
+ safeWriteLog(PLUGIN_NAME21, {
33100
33803
  hook: "command.execute.before",
33101
33804
  command: cmd,
33102
33805
  sessionID: input.sessionID
@@ -33111,14 +33814,14 @@ var workflowEngineServer = async (ctx) => {
33111
33814
  });
33112
33815
  },
33113
33816
  "chat.message": async (input, output) => {
33114
- await safeAsync(PLUGIN_NAME20, "chat.message", async () => {
33817
+ await safeAsync(PLUGIN_NAME21, "chat.message", async () => {
33115
33818
  const text = extractUserText(output).trim();
33116
33819
  if (!text.startsWith("/"))
33117
33820
  return;
33118
33821
  const cmd = text.split(/\s+/)[0];
33119
33822
  if (!cmd)
33120
33823
  return;
33121
- safeWriteLog(PLUGIN_NAME20, {
33824
+ safeWriteLog(PLUGIN_NAME21, {
33122
33825
  hook: "chat.message",
33123
33826
  command: cmd,
33124
33827
  sessionID: input.sessionID
@@ -33128,13 +33831,13 @@ var workflowEngineServer = async (ctx) => {
33128
33831
  }
33129
33832
  };
33130
33833
  };
33131
- var handler20 = workflowEngineServer;
33834
+ var handler21 = workflowEngineServer;
33132
33835
 
33133
33836
  // plugins/session-worktree-guard.ts
33134
- import path30 from "node:path";
33837
+ import path32 from "node:path";
33135
33838
  import { stat } from "node:fs/promises";
33136
- var PLUGIN_NAME21 = "session-worktree-guard";
33137
- logLifecycle(PLUGIN_NAME21, "import", {});
33839
+ var PLUGIN_NAME22 = "session-worktree-guard";
33840
+ logLifecycle(PLUGIN_NAME22, "import", {});
33138
33841
  var WRITE_INTENT_RE = />(?![=&])(?!\s*\/dev\/(?:null|stdout|stderr|fd\/\d+)\b)|\btee\b|\brm\b|\bmv\b|\bcp\b|\bmkdir\b|\btouch\b|\bchmod\b|\bchown\b|\bln\b/;
33139
33842
  var READ_ONLY_COMMANDS = /^\s*(?:ls|cat|head|tail|grep|rg|find|fd|wc|stat|file|which|whereis|echo|pwd|cd|pushd|popd|env|printenv|type|less|more|sort|uniq|awk|tr|cut|jq|date|whoami|id|uname|node|npx|tsc|diff|python3?|git(?:\s+-C\s+\S+)?\s+(?:log|show|diff|status|branch|tag|remote|config\s+--get|rev-parse|rev-list|ls-files|ls-tree|cat-file|describe|reflog|blame|shortlog|name-rev|symbolic-ref|merge-base|worktree\s+list|stash\s+list|stash\s+show))\b/;
33140
33843
  var SIDE_EFFECT_TOKEN_RE = />(?![=&])(?!\s*\/dev\/(?:null|stdout|stderr|fd\/\d+)\b)|\|\s*tee\b|\btee\b/;
@@ -33269,28 +33972,28 @@ var MERGE_CALLER_WHITELIST = new Set([
33269
33972
  var FORCE_MERGE_CALLER_WHITELIST = new Set([
33270
33973
  "codeforge"
33271
33974
  ]);
33272
- var CODEFORGE_WORKTREE_DIR_NAME = path30.join(".git", "codeforge-worktrees");
33975
+ var CODEFORGE_WORKTREE_DIR_NAME = path32.join(".git", "codeforge-worktrees");
33273
33976
  function worktreesRoot(mainRoot) {
33274
- return path30.join(mainRoot, CODEFORGE_WORKTREE_DIR_NAME);
33977
+ return path32.join(mainRoot, CODEFORGE_WORKTREE_DIR_NAME);
33275
33978
  }
33276
33979
  function isInsideAnyWorktreeDir(absPath, mainRoot) {
33277
- if (!path30.isAbsolute(absPath))
33980
+ if (!path32.isAbsolute(absPath))
33278
33981
  return false;
33279
33982
  const root = worktreesRoot(mainRoot);
33280
33983
  if (absPath === root)
33281
33984
  return false;
33282
- const prefix = root.endsWith(path30.sep) ? root : root + path30.sep;
33985
+ const prefix = root.endsWith(path32.sep) ? root : root + path32.sep;
33283
33986
  return absPath.startsWith(prefix);
33284
33987
  }
33285
33988
  function worktreeRootOf(absPath, mainRoot) {
33286
33989
  const root = worktreesRoot(mainRoot);
33287
- const prefix = root.endsWith(path30.sep) ? root : root + path30.sep;
33990
+ const prefix = root.endsWith(path32.sep) ? root : root + path32.sep;
33288
33991
  if (!absPath.startsWith(prefix))
33289
33992
  return null;
33290
- const seg = absPath.slice(prefix.length).split(path30.sep)[0];
33993
+ const seg = absPath.slice(prefix.length).split(path32.sep)[0];
33291
33994
  if (!seg || seg === "..")
33292
33995
  return null;
33293
- return path30.join(root, seg);
33996
+ return path32.join(root, seg);
33294
33997
  }
33295
33998
  function stripPairedQuotes(raw) {
33296
33999
  if (raw.length >= 2) {
@@ -33321,7 +34024,7 @@ function resolveExplicitWorktreeTarget(argsObj, mainRoot) {
33321
34024
  for (const cand of candidates) {
33322
34025
  if (!cand)
33323
34026
  continue;
33324
- const abs = path30.resolve(mainRoot, cand);
34027
+ const abs = path32.resolve(mainRoot, cand);
33325
34028
  if (isInsideAnyWorktreeDir(abs, mainRoot)) {
33326
34029
  const wtRoot = worktreeRootOf(abs, mainRoot);
33327
34030
  if (wtRoot)
@@ -33334,14 +34037,14 @@ function resolveExplicitWorktreeTarget(argsObj, mainRoot) {
33334
34037
  }
33335
34038
  }
33336
34039
  function stripSharedGitDirRef(command, mainRoot) {
33337
- const escGitDir = escapeRegex2(path30.join(mainRoot, ".git"));
34040
+ const escGitDir = escapeRegex2(path32.join(mainRoot, ".git"));
33338
34041
  const re = new RegExp(`--git-dir(?:=|\\s+)['"]?${escGitDir}(?:/[^\\s'"\\x60)]*)?['"]?`, "g");
33339
34042
  return command.replace(re, " ");
33340
34043
  }
33341
34044
  function rewritePath(value, mainRoot, worktreeRoot) {
33342
34045
  if (!value)
33343
34046
  return null;
33344
- const resolved = path30.isAbsolute(value) ? value : path30.resolve(mainRoot, value);
34047
+ const resolved = path32.isAbsolute(value) ? value : path32.resolve(mainRoot, value);
33345
34048
  const wtPrefix2 = worktreeRoot.endsWith("/") ? worktreeRoot : worktreeRoot + "/";
33346
34049
  if (resolved === worktreeRoot || resolved.startsWith(wtPrefix2)) {
33347
34050
  return null;
@@ -33379,7 +34082,7 @@ function commandContainsMainRootExcludingWorktree(command, mainRoot, worktreePat
33379
34082
  }
33380
34083
  }
33381
34084
  const wtRoot = worktreesRoot(mainRoot);
33382
- const wtRootPrefix = wtRoot + path30.sep;
34085
+ const wtRootPrefix = wtRoot + path32.sep;
33383
34086
  const escapedWtRootPrefix = escapeRegex2(wtRootPrefix);
33384
34087
  const wtPathPattern = escapedWtRootPrefix + `[^\\s'"\\x60)]*`;
33385
34088
  const allWorktreePathsReForEscape = new RegExp(wtPathPattern, "g");
@@ -33434,19 +34137,19 @@ function collectWritePaths(toolName, argsObj, worktreeRoot) {
33434
34137
  const candidate = toolName === "write" || toolName === "edit" ? argsObj["filePath"] : toolName === "ast_edit" ? argsObj["target"] : undefined;
33435
34138
  if (typeof candidate !== "string" || candidate.length === 0)
33436
34139
  return out;
33437
- const abs = path30.isAbsolute(candidate) ? candidate : path30.resolve(worktreeRoot, candidate);
33438
- const rel = path30.relative(worktreeRoot, abs).split(path30.sep).join("/");
34140
+ const abs = path32.isAbsolute(candidate) ? candidate : path32.resolve(worktreeRoot, candidate);
34141
+ const rel = path32.relative(worktreeRoot, abs).split(path32.sep).join("/");
33439
34142
  out.push(rel);
33440
34143
  return out;
33441
34144
  }
33442
- var log13 = makePluginLogger(PLUGIN_NAME21);
34145
+ var log13 = makePluginLogger(PLUGIN_NAME22);
33443
34146
  async function isCodeforgeManagedProject(mainRoot, opts = {}) {
33444
34147
  const env = opts.env ?? process.env;
33445
34148
  const force = env["CODEFORGE_FORCE_WORKTREE_GUARD"];
33446
34149
  if (force === "1" || force === "true" || force === "yes")
33447
34150
  return true;
33448
34151
  try {
33449
- const st = await stat(path30.join(mainRoot, ".codeforge"));
34152
+ const st = await stat(path32.join(mainRoot, ".codeforge"));
33450
34153
  return st.isDirectory();
33451
34154
  } catch {
33452
34155
  return false;
@@ -33476,11 +34179,11 @@ var STALE_MERGE_HEAD_MS = 24 * 60 * 60000;
33476
34179
  var _staleMergeHeadWarned = false;
33477
34180
  var _mergeBypassToastShown = false;
33478
34181
  async function isMainRepoMidMerge(mainRoot, opts = {}) {
33479
- const gitDir = path30.join(mainRoot, ".git");
34182
+ const gitDir = path32.join(mainRoot, ".git");
33480
34183
  const markers = [
33481
- path30.join(gitDir, "MERGE_HEAD"),
33482
- path30.join(gitDir, "rebase-merge"),
33483
- path30.join(gitDir, "CHERRY_PICK_HEAD")
34184
+ path32.join(gitDir, "MERGE_HEAD"),
34185
+ path32.join(gitDir, "rebase-merge"),
34186
+ path32.join(gitDir, "CHERRY_PICK_HEAD")
33484
34187
  ];
33485
34188
  let freshest = -1;
33486
34189
  for (const m of markers) {
@@ -33507,25 +34210,25 @@ var sessionWorktreeGuardPlugin = async (ctx) => {
33507
34210
  const disableEnv = process.env["CODEFORGE_DISABLE_WORKTREE_GUARD"];
33508
34211
  if (disableEnv === "1" || disableEnv === "true" || disableEnv === "yes") {
33509
34212
  log13.warn("[guard] CODEFORGE_DISABLE_WORKTREE_GUARD 已启用,session-worktree-guard 全部 hook 跳过;" + "本次 opencode 会话所有写操作将直接落到主工作区(失去隔离保护)", { env: disableEnv });
33510
- safeWriteLog(PLUGIN_NAME21, {
34213
+ safeWriteLog(PLUGIN_NAME22, {
33511
34214
  hook: "activate",
33512
34215
  action: "skip",
33513
34216
  source: "disable-env",
33514
34217
  env_value: disableEnv
33515
34218
  });
33516
- logLifecycle(PLUGIN_NAME21, "activate", { disabled_by_env: true });
34219
+ logLifecycle(PLUGIN_NAME22, "activate", { disabled_by_env: true });
33517
34220
  return {};
33518
34221
  }
33519
34222
  const mainRoot = resolveMainRoot2(ctx.directory ?? process.cwd());
33520
34223
  if (!await isCodeforgeManagedProject(mainRoot)) {
33521
34224
  log13.info("[guard] 当前项目无 .codeforge/ 目录,session-worktree-guard 不启用;" + "如需在此项目启用 worktree 隔离:mkdir .codeforge", { mainRoot });
33522
- safeWriteLog(PLUGIN_NAME21, {
34225
+ safeWriteLog(PLUGIN_NAME22, {
33523
34226
  hook: "activate",
33524
34227
  action: "skip",
33525
34228
  source: "non-codeforge-project",
33526
34229
  mainRoot
33527
34230
  });
33528
- logLifecycle(PLUGIN_NAME21, "activate", { skipped: "non-codeforge-project", mainRoot });
34231
+ logLifecycle(PLUGIN_NAME22, "activate", { skipped: "non-codeforge-project", mainRoot });
33529
34232
  return {};
33530
34233
  }
33531
34234
  let policyCfg = {};
@@ -33538,7 +34241,7 @@ var sessionWorktreeGuardPlugin = async (ctx) => {
33538
34241
  });
33539
34242
  }
33540
34243
  const perAgentEnabled = !!policyCfg.per_agent && Object.keys(policyCfg.per_agent).length > 0;
33541
- logLifecycle(PLUGIN_NAME21, "activate", {
34244
+ logLifecycle(PLUGIN_NAME22, "activate", {
33542
34245
  mainRoot,
33543
34246
  CODEFORGE_SESSION_ID: process.env["CODEFORGE_SESSION_ID"] ?? "(not set)"
33544
34247
  });
@@ -33554,7 +34257,7 @@ var sessionWorktreeGuardPlugin = async (ctx) => {
33554
34257
  opencode_version_hint: "需 opencode >= 0.x 才会在 tool.execute.before 注入 input.sessionID"
33555
34258
  });
33556
34259
  }
33557
- safeWriteLog(PLUGIN_NAME21, {
34260
+ safeWriteLog(PLUGIN_NAME22, {
33558
34261
  hook: "tool.execute.before",
33559
34262
  tool: input.tool,
33560
34263
  action: "skip",
@@ -33564,12 +34267,12 @@ var sessionWorktreeGuardPlugin = async (ctx) => {
33564
34267
  return;
33565
34268
  }
33566
34269
  let denied;
33567
- await safeAsync(PLUGIN_NAME21, "tool.execute.before", async () => {
34270
+ await safeAsync(PLUGIN_NAME22, "tool.execute.before", async () => {
33568
34271
  const toolName = input.tool;
33569
34272
  const argsObj = output.args ?? {};
33570
34273
  const midMerge = isWriteOperation(toolName, argsObj, mainRoot) || toolName === "bash" ? await isMainRepoMidMerge(mainRoot, { log: log13 }) : false;
33571
34274
  const emitMergeBypassNotice = (subClass, detail) => {
33572
- safeWriteLog(PLUGIN_NAME21, {
34275
+ safeWriteLog(PLUGIN_NAME22, {
33573
34276
  hook: "tool.execute.before",
33574
34277
  tool: toolName,
33575
34278
  sessionID: input.sessionID,
@@ -33602,7 +34305,7 @@ var sessionWorktreeGuardPlugin = async (ctx) => {
33602
34305
  tool: toolName,
33603
34306
  error: errMsg
33604
34307
  });
33605
- safeWriteLog(PLUGIN_NAME21, {
34308
+ safeWriteLog(PLUGIN_NAME22, {
33606
34309
  hook: "tool.execute.before",
33607
34310
  tool: toolName,
33608
34311
  sessionID: input.sessionID,
@@ -33619,7 +34322,7 @@ var sessionWorktreeGuardPlugin = async (ctx) => {
33619
34322
  tool: toolName,
33620
34323
  error: errMsg
33621
34324
  });
33622
- safeWriteLog(PLUGIN_NAME21, {
34325
+ safeWriteLog(PLUGIN_NAME22, {
33623
34326
  hook: "tool.execute.before",
33624
34327
  tool: toolName,
33625
34328
  sessionID: input.sessionID,
@@ -33646,7 +34349,7 @@ var sessionWorktreeGuardPlugin = async (ctx) => {
33646
34349
  if (parentEntry && parentEntry.status === "active") {
33647
34350
  entry = parentEntry;
33648
34351
  log13.debug?.(`[child-inherit] session ${sessionId} 继承父 ${parentId} 的 worktree`, { parentSessionId: parentId, worktreePath: parentEntry.worktreePath });
33649
- safeWriteLog(PLUGIN_NAME21, {
34352
+ safeWriteLog(PLUGIN_NAME22, {
33650
34353
  hook: "tool.execute.before",
33651
34354
  tool: toolName,
33652
34355
  sessionID: input.sessionID,
@@ -33658,7 +34361,7 @@ var sessionWorktreeGuardPlugin = async (ctx) => {
33658
34361
  } else if (!parentEntry) {
33659
34362
  entry = await bindSessionWorktree({ sessionId: parentId, mainRoot });
33660
34363
  log13.info(`[child-inherit-bind-root] session ${sessionId} 触发父 ${parentId} 的 worktree lazy-bind`, { parentSessionId: parentId, branch: entry.branch, worktreePath: entry.worktreePath });
33661
- safeWriteLog(PLUGIN_NAME21, {
34364
+ safeWriteLog(PLUGIN_NAME22, {
33662
34365
  hook: "tool.execute.before",
33663
34366
  tool: toolName,
33664
34367
  sessionID: input.sessionID,
@@ -33677,7 +34380,7 @@ var sessionWorktreeGuardPlugin = async (ctx) => {
33677
34380
  try {
33678
34381
  entry = await bindSessionWorktree({ sessionId, mainRoot });
33679
34382
  log13.info(`[lazy-bind] auto-created worktree for session ${sessionId}`, { branch: entry.branch, path: entry.worktreePath });
33680
- safeWriteLog(PLUGIN_NAME21, {
34383
+ safeWriteLog(PLUGIN_NAME22, {
33681
34384
  hook: "tool.execute.before",
33682
34385
  tool: toolName,
33683
34386
  sessionID: input.sessionID,
@@ -33702,7 +34405,7 @@ var sessionWorktreeGuardPlugin = async (ctx) => {
33702
34405
  error: errMsg,
33703
34406
  throttled: alreadyNotified
33704
34407
  });
33705
- safeWriteLog(PLUGIN_NAME21, {
34408
+ safeWriteLog(PLUGIN_NAME22, {
33706
34409
  hook: "tool.execute.before",
33707
34410
  tool: toolName,
33708
34411
  sessionID: input.sessionID,
@@ -33746,7 +34449,7 @@ var sessionWorktreeGuardPlugin = async (ctx) => {
33746
34449
  caller: null,
33747
34450
  force: true
33748
34451
  });
33749
- safeWriteLog(PLUGIN_NAME21, {
34452
+ safeWriteLog(PLUGIN_NAME22, {
33750
34453
  hook: "tool.execute.before",
33751
34454
  tool: toolName,
33752
34455
  sessionID: input.sessionID,
@@ -33767,7 +34470,7 @@ var sessionWorktreeGuardPlugin = async (ctx) => {
33767
34470
  action,
33768
34471
  caller
33769
34472
  });
33770
- safeWriteLog(PLUGIN_NAME21, {
34473
+ safeWriteLog(PLUGIN_NAME22, {
33771
34474
  hook: "tool.execute.before",
33772
34475
  tool: toolName,
33773
34476
  sessionID: input.sessionID,
@@ -33788,7 +34491,7 @@ var sessionWorktreeGuardPlugin = async (ctx) => {
33788
34491
  caller,
33789
34492
  force: true
33790
34493
  });
33791
- safeWriteLog(PLUGIN_NAME21, {
34494
+ safeWriteLog(PLUGIN_NAME22, {
33792
34495
  hook: "tool.execute.before",
33793
34496
  tool: toolName,
33794
34497
  sessionID: input.sessionID,
@@ -33812,7 +34515,7 @@ var sessionWorktreeGuardPlugin = async (ctx) => {
33812
34515
  tool: toolName,
33813
34516
  source: "per-agent-acl-fail-closed"
33814
34517
  });
33815
- safeWriteLog(PLUGIN_NAME21, {
34518
+ safeWriteLog(PLUGIN_NAME22, {
33816
34519
  hook: "tool.execute.before",
33817
34520
  tool: toolName,
33818
34521
  sessionID: input.sessionID,
@@ -33837,7 +34540,7 @@ var sessionWorktreeGuardPlugin = async (ctx) => {
33837
34540
  path: relPath,
33838
34541
  acl_decision: dec
33839
34542
  });
33840
- safeWriteLog(PLUGIN_NAME21, {
34543
+ safeWriteLog(PLUGIN_NAME22, {
33841
34544
  hook: "tool.execute.before",
33842
34545
  tool: toolName,
33843
34546
  sessionID: input.sessionID,
@@ -33881,7 +34584,7 @@ var sessionWorktreeGuardPlugin = async (ctx) => {
33881
34584
  requiredPlanId: entry.requiredPlanId,
33882
34585
  inheritedFromParent: inherited ? entry.sessionId : undefined
33883
34586
  });
33884
- safeWriteLog(PLUGIN_NAME21, {
34587
+ safeWriteLog(PLUGIN_NAME22, {
33885
34588
  hook: "tool.execute.before",
33886
34589
  tool: toolName,
33887
34590
  sessionID: input.sessionID,
@@ -33903,7 +34606,7 @@ var sessionWorktreeGuardPlugin = async (ctx) => {
33903
34606
  if (!stillHit) {
33904
34607
  hitMainRoot = false;
33905
34608
  if (detectBashWriteIntent(command, mainRoot)) {
33906
- safeWriteLog(PLUGIN_NAME21, {
34609
+ safeWriteLog(PLUGIN_NAME22, {
33907
34610
  hook: "tool.execute.before",
33908
34611
  tool: toolName,
33909
34612
  sessionID: input.sessionID,
@@ -33924,7 +34627,7 @@ var sessionWorktreeGuardPlugin = async (ctx) => {
33924
34627
  const caller = await resolveAgentForGuard({ sessionID: input.sessionID, agent: input.agent }, ctx.client, log13);
33925
34628
  if (caller !== null && CLASS_B_CALLER_WHITELIST.has(caller)) {
33926
34629
  log13.debug?.(`[class-b-whitelist] allow caller=${caller}`, { sessionId, tool: toolName, command: command.slice(0, 200) });
33927
- safeWriteLog(PLUGIN_NAME21, {
34630
+ safeWriteLog(PLUGIN_NAME22, {
33928
34631
  hook: "tool.execute.before",
33929
34632
  tool: toolName,
33930
34633
  sessionID: input.sessionID,
@@ -33938,7 +34641,7 @@ var sessionWorktreeGuardPlugin = async (ctx) => {
33938
34641
  const snippet = command.length > 60 ? command.slice(0, 60) + "…" : command;
33939
34642
  const reason = `[session-worktree-guard] DENIED: bash.command 含主仓绝对路径写操作 (${snippet}) [caller=${callerTag}],请在当前 session worktree (${worktreePath}) 内操作`;
33940
34643
  log13.warn(reason, { sessionId, caller: callerTag, command: command.slice(0, 200) });
33941
- safeWriteLog(PLUGIN_NAME21, {
34644
+ safeWriteLog(PLUGIN_NAME22, {
33942
34645
  hook: "tool.execute.before",
33943
34646
  tool: toolName,
33944
34647
  sessionID: input.sessionID,
@@ -33962,7 +34665,7 @@ var sessionWorktreeGuardPlugin = async (ctx) => {
33962
34665
  return;
33963
34666
  }
33964
34667
  log13.info(`rewrote ${toolName}.${field}: ${value} → ${newPath}`);
33965
- safeWriteLog(PLUGIN_NAME21, {
34668
+ safeWriteLog(PLUGIN_NAME22, {
33966
34669
  hook: "tool.execute.before",
33967
34670
  tool: toolName,
33968
34671
  field,
@@ -33993,7 +34696,7 @@ var sessionWorktreeGuardPlugin = async (ctx) => {
33993
34696
  } else if (!midMerge && entry.status === "active") {
33994
34697
  output.args["workdir"] = worktreePath;
33995
34698
  log13.info(`injected default bash.workdir → ${worktreePath}`);
33996
- safeWriteLog(PLUGIN_NAME21, {
34699
+ safeWriteLog(PLUGIN_NAME22, {
33997
34700
  hook: "tool.execute.before",
33998
34701
  tool: toolName,
33999
34702
  field: "workdir",
@@ -34009,7 +34712,7 @@ var sessionWorktreeGuardPlugin = async (ctx) => {
34009
34712
  const newReadPath = rewritePath(filePath, mainRoot, worktreePath);
34010
34713
  if (newReadPath !== null) {
34011
34714
  output.args["filePath"] = newReadPath;
34012
- safeWriteLog(PLUGIN_NAME21, {
34715
+ safeWriteLog(PLUGIN_NAME22, {
34013
34716
  hook: "tool.execute.before",
34014
34717
  tool: toolName,
34015
34718
  field: "filePath",
@@ -34033,7 +34736,7 @@ var sessionWorktreeGuardPlugin = async (ctx) => {
34033
34736
  throw denied;
34034
34737
  },
34035
34738
  "tool.execute.after": async (input, output) => {
34036
- await safeAsync(PLUGIN_NAME21, "tool.execute.after", async () => {
34739
+ await safeAsync(PLUGIN_NAME22, "tool.execute.after", async () => {
34037
34740
  const callID = input.callID;
34038
34741
  if (!callID)
34039
34742
  return;
@@ -34045,7 +34748,7 @@ var sessionWorktreeGuardPlugin = async (ctx) => {
34045
34748
  output.output = warning + `
34046
34749
 
34047
34750
  ` + prev;
34048
- safeWriteLog(PLUGIN_NAME21, {
34751
+ safeWriteLog(PLUGIN_NAME22, {
34049
34752
  hook: "tool.execute.after",
34050
34753
  tool: notice.tool,
34051
34754
  sessionID: input.sessionID,
@@ -34056,12 +34759,12 @@ var sessionWorktreeGuardPlugin = async (ctx) => {
34056
34759
  }
34057
34760
  };
34058
34761
  };
34059
- var handler21 = sessionWorktreeGuardPlugin;
34762
+ var handler22 = sessionWorktreeGuardPlugin;
34060
34763
 
34061
34764
  // plugins/worktree-lifecycle.ts
34062
34765
  init_worktree_ops();
34063
- var PLUGIN_NAME22 = "worktree-lifecycle";
34064
- logLifecycle(PLUGIN_NAME22, "import", {});
34766
+ var PLUGIN_NAME23 = "worktree-lifecycle";
34767
+ logLifecycle(PLUGIN_NAME23, "import", {});
34065
34768
  var IDLE_TOAST_THROTTLE_MS = 60 * 60000;
34066
34769
  var IDLE_TOAST_DURATION_MS = 8000;
34067
34770
  var IDLE_TOAST_REMINDER_INTERVAL_MS = 30 * 60000;
@@ -34078,7 +34781,7 @@ async function maybeNotifyRegistryUnreadable(client, rec, prune) {
34078
34781
  _registryUnreadableNotified = false;
34079
34782
  return;
34080
34783
  }
34081
- safeWriteLog(PLUGIN_NAME22, {
34784
+ safeWriteLog(PLUGIN_NAME23, {
34082
34785
  hook: "registry-unreadable",
34083
34786
  reason: unreadable.reason,
34084
34787
  detail: unreadable.detail,
@@ -34097,11 +34800,11 @@ async function maybeNotifyRegistryUnreadable(client, rec, prune) {
34097
34800
  return;
34098
34801
  await showToast2(client, { message: msg, variant: "warning", duration: 1e4, title: "CodeForge" }, log14).catch(() => {});
34099
34802
  }
34100
- var log14 = makePluginLogger(PLUGIN_NAME22);
34803
+ var log14 = makePluginLogger(PLUGIN_NAME23);
34101
34804
  var worktreeLifecyclePlugin = async (ctx) => {
34102
34805
  const rawDir = ctx.directory ?? process.cwd();
34103
34806
  const mainRoot = resolveMainRoot2(rawDir);
34104
- logLifecycle(PLUGIN_NAME22, "activate", {
34807
+ logLifecycle(PLUGIN_NAME23, "activate", {
34105
34808
  mainRoot: mainRoot ?? "(not set)",
34106
34809
  idle_threshold_ms: IDLE_TOAST_THROTTLE_MS
34107
34810
  });
@@ -34115,13 +34818,13 @@ var worktreeLifecyclePlugin = async (ctx) => {
34115
34818
  }
34116
34819
  if (!await shouldRunWorktreeGc(mainRoot)) {
34117
34820
  log14.info("[lifecycle] 当前项目无 .codeforge/ 目录且 registry 无条目,worktree-lifecycle 不启用", { mainRoot });
34118
- safeWriteLog(PLUGIN_NAME22, {
34821
+ safeWriteLog(PLUGIN_NAME23, {
34119
34822
  hook: "activate",
34120
34823
  action: "skip",
34121
34824
  source: "non-codeforge-project",
34122
34825
  mainRoot
34123
34826
  });
34124
- logLifecycle(PLUGIN_NAME22, "activate", { skipped: "non-codeforge-project", mainRoot });
34827
+ logLifecycle(PLUGIN_NAME23, "activate", { skipped: "non-codeforge-project", mainRoot });
34125
34828
  return {};
34126
34829
  }
34127
34830
  if (myGeneration !== _activateGeneration)
@@ -34134,14 +34837,14 @@ var worktreeLifecyclePlugin = async (ctx) => {
34134
34837
  }
34135
34838
  _probe = createSessionProbe();
34136
34839
  setImmediate(() => {
34137
- safeAsync(PLUGIN_NAME22, "activate.pruneOrphan", async () => {
34840
+ safeAsync(PLUGIN_NAME23, "activate.pruneOrphan", async () => {
34138
34841
  const rec = await reconcileTransitionalEntries(mainRoot);
34139
34842
  if (rec.cleanedCreating.length > 0 || rec.finishedRemoving.length > 0 || rec.keptConservative.length > 0) {
34140
- safeWriteLog(PLUGIN_NAME22, { hook: "activate.reconcile", ...rec });
34843
+ safeWriteLog(PLUGIN_NAME23, { hook: "activate.reconcile", ...rec });
34141
34844
  }
34142
34845
  const merc = await reconcileInterruptedMerges(mainRoot);
34143
34846
  if (merc.recoveredMerged.length > 0 || merc.rolledBack.length > 0 || merc.conflicts.length > 0 || merc.registryUnreadable) {
34144
- safeWriteLog(PLUGIN_NAME22, { hook: "activate.reconcileMerge", ...merc });
34847
+ safeWriteLog(PLUGIN_NAME23, { hook: "activate.reconcileMerge", ...merc });
34145
34848
  }
34146
34849
  const result = await pruneOrphanWorktrees(mainRoot, {
34147
34850
  isSessionAlive: _probe.isSessionAlive
@@ -34149,7 +34852,7 @@ var worktreeLifecyclePlugin = async (ctx) => {
34149
34852
  await maybeNotifyRegistryUnreadable(client, rec, result);
34150
34853
  if (result.cleaned.length > 0 || result.failed.length > 0) {
34151
34854
  log14.info(`[pruneOrphan] cleaned=${result.cleaned.length} failed=${result.failed.length} skipped=${result.skipped}`);
34152
- safeWriteLog(PLUGIN_NAME22, {
34855
+ safeWriteLog(PLUGIN_NAME23, {
34153
34856
  hook: "activate.pruneOrphan",
34154
34857
  cleaned: result.cleaned,
34155
34858
  failed: result.failed,
@@ -34160,7 +34863,7 @@ var worktreeLifecyclePlugin = async (ctx) => {
34160
34863
  });
34161
34864
  _pruneTimer = setInterval(() => {
34162
34865
  if (pruneRunning) {
34163
- safeWriteLog(PLUGIN_NAME22, {
34866
+ safeWriteLog(PLUGIN_NAME23, {
34164
34867
  hook: "interval.pruneOrphan",
34165
34868
  action: "skip",
34166
34869
  reason: "previous prune still running"
@@ -34168,15 +34871,15 @@ var worktreeLifecyclePlugin = async (ctx) => {
34168
34871
  return;
34169
34872
  }
34170
34873
  pruneRunning = true;
34171
- safeAsync(PLUGIN_NAME22, "interval.pruneOrphan", async () => {
34874
+ safeAsync(PLUGIN_NAME23, "interval.pruneOrphan", async () => {
34172
34875
  try {
34173
34876
  const rec = await reconcileTransitionalEntries(mainRoot);
34174
34877
  if (rec.cleanedCreating.length > 0 || rec.finishedRemoving.length > 0 || rec.keptConservative.length > 0) {
34175
- safeWriteLog(PLUGIN_NAME22, { hook: "interval.reconcile", ...rec });
34878
+ safeWriteLog(PLUGIN_NAME23, { hook: "interval.reconcile", ...rec });
34176
34879
  }
34177
34880
  const merc = await reconcileInterruptedMerges(mainRoot);
34178
34881
  if (merc.recoveredMerged.length > 0 || merc.rolledBack.length > 0 || merc.conflicts.length > 0 || merc.registryUnreadable) {
34179
- safeWriteLog(PLUGIN_NAME22, { hook: "interval.reconcileMerge", ...merc });
34882
+ safeWriteLog(PLUGIN_NAME23, { hook: "interval.reconcileMerge", ...merc });
34180
34883
  }
34181
34884
  const result = await pruneOrphanWorktrees(mainRoot, {
34182
34885
  isSessionAlive: _probe.isSessionAlive
@@ -34184,7 +34887,7 @@ var worktreeLifecyclePlugin = async (ctx) => {
34184
34887
  await maybeNotifyRegistryUnreadable(client, rec, result);
34185
34888
  if (result.cleaned.length > 0 || result.failed.length > 0) {
34186
34889
  log14.info(`[pruneOrphan interval] cleaned=${result.cleaned.length} failed=${result.failed.length} skipped=${result.skipped}`);
34187
- safeWriteLog(PLUGIN_NAME22, {
34890
+ safeWriteLog(PLUGIN_NAME23, {
34188
34891
  hook: "interval.pruneOrphan",
34189
34892
  cleaned: result.cleaned,
34190
34893
  failed: result.failed,
@@ -34199,14 +34902,14 @@ var worktreeLifecyclePlugin = async (ctx) => {
34199
34902
  _pruneTimer.unref();
34200
34903
  return {
34201
34904
  event: async ({ event }) => {
34202
- await safeAsync(PLUGIN_NAME22, "event", async () => {
34905
+ await safeAsync(PLUGIN_NAME23, "event", async () => {
34203
34906
  const ended = extractEndedSessionID(event);
34204
34907
  if (!ended)
34205
34908
  return;
34206
34909
  if (ended.type === "session.deleted") {
34207
34910
  const entry = await getSessionWorktree(ended.sessionID, mainRoot);
34208
34911
  if (!entry || entry.status !== "active") {
34209
- safeWriteLog(PLUGIN_NAME22, {
34912
+ safeWriteLog(PLUGIN_NAME23, {
34210
34913
  hook: "event",
34211
34914
  type: ended.type,
34212
34915
  sessionID: ended.sessionID,
@@ -34221,7 +34924,7 @@ var worktreeLifecyclePlugin = async (ctx) => {
34221
34924
  const fastDirty = await isWorktreeDirty(entry.worktreePath);
34222
34925
  if (!fastDirty) {
34223
34926
  await discardSession({ sessionId: ended.sessionID, mainRoot });
34224
- safeWriteLog(PLUGIN_NAME22, {
34927
+ safeWriteLog(PLUGIN_NAME23, {
34225
34928
  hook: "event",
34226
34929
  type: ended.type,
34227
34930
  sessionID: ended.sessionID,
@@ -34256,7 +34959,7 @@ var worktreeLifecyclePlugin = async (ctx) => {
34256
34959
  }
34257
34960
  try {
34258
34961
  await discardSession({ sessionId: ended.sessionID, mainRoot });
34259
- safeWriteLog(PLUGIN_NAME22, {
34962
+ safeWriteLog(PLUGIN_NAME23, {
34260
34963
  hook: "event",
34261
34964
  type: ended.type,
34262
34965
  sessionID: ended.sessionID,
@@ -34297,7 +35000,7 @@ var worktreeLifecyclePlugin = async (ctx) => {
34297
35000
  const idleMin = Math.round(idleMs / 60000);
34298
35001
  const msg = `\uD83D\uDCA4 Session ${ended.sessionID.slice(0, 8)} worktree 已空闲 ${idleMin}min,` + `用 /merge 收尾或 /discard-session 放弃`;
34299
35002
  const sent = await showToast2(client, { message: msg, variant: "default", duration: IDLE_TOAST_DURATION_MS, title: "CodeForge" }, log14);
34300
- safeWriteLog(PLUGIN_NAME22, {
35003
+ safeWriteLog(PLUGIN_NAME23, {
34301
35004
  hook: "event",
34302
35005
  type: ended.type,
34303
35006
  sessionID: ended.sessionID,
@@ -34310,7 +35013,7 @@ var worktreeLifecyclePlugin = async (ctx) => {
34310
35013
  }
34311
35014
  };
34312
35015
  };
34313
- var handler22 = worktreeLifecyclePlugin;
35016
+ var handler23 = worktreeLifecyclePlugin;
34314
35017
 
34315
35018
  // src/index.ts
34316
35019
  var PLUGIN_ID = "codeforge";
@@ -34336,9 +35039,10 @@ var HANDLERS = [
34336
35039
  { name: "tool-heartbeat", init: handler6 },
34337
35040
  { name: "tool-policy", init: handler18 },
34338
35041
  { name: "update-checker", init: handler19 },
34339
- { name: "workflow-engine", init: handler20 },
34340
- { name: "session-worktree-guard", init: handler21 },
34341
- { name: "worktree-lifecycle", init: handler22 }
35042
+ { name: "user-merge-confirm", init: handler20 },
35043
+ { name: "workflow-engine", init: handler21 },
35044
+ { name: "session-worktree-guard", init: handler22 },
35045
+ { name: "worktree-lifecycle", init: handler23 }
34342
35046
  ];
34343
35047
  function makeSerialHook(hookName, fns) {
34344
35048
  return async (input, output) => {