@andyqiu/codeforge 0.6.4 → 0.6.6

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
@@ -28,6 +28,7 @@ __export(exports_worktree_ops, {
28
28
  listWorktrees: () => listWorktrees,
29
29
  getMergeConflicts: () => getMergeConflicts,
30
30
  ensureWorktree: () => ensureWorktree,
31
+ deleteBranchIfExists: () => deleteBranchIfExists,
31
32
  commitWorktreeIfDirty: () => commitWorktreeIfDirty
32
33
  });
33
34
  import { execFile as execFile2 } from "node:child_process";
@@ -60,7 +61,35 @@ async function removeWorktree(opts) {
60
61
  const args = ["worktree", "remove", opts.worktree_path];
61
62
  if (opts.force)
62
63
  args.push("--force");
63
- await runGit(opts.root, args, opts.git_timeout_ms ?? 5000);
64
+ try {
65
+ await runGit(opts.root, args, opts.git_timeout_ms ?? 5000);
66
+ } catch (err) {
67
+ if (isWorktreeAbsentError(err))
68
+ return;
69
+ throw err;
70
+ }
71
+ }
72
+ function isWorktreeAbsentError(err) {
73
+ const msg = err instanceof Error ? err.message : String(err);
74
+ return /is not a working tree/i.test(msg) || /No such file or directory/i.test(msg) || /not a valid path/i.test(msg);
75
+ }
76
+ async function deleteBranchIfExists(opts) {
77
+ const root = path11.resolve(opts.root);
78
+ const timeout = opts.git_timeout_ms ?? 5000;
79
+ try {
80
+ await runGit(root, ["rev-parse", "--verify", `refs/heads/${opts.branch}`], 3000);
81
+ } catch {
82
+ return { deleted: false };
83
+ }
84
+ try {
85
+ await runGit(root, ["branch", "-D", opts.branch], timeout);
86
+ return { deleted: true };
87
+ } catch (err) {
88
+ const msg = err instanceof Error ? err.message : String(err);
89
+ if (/not found/i.test(msg))
90
+ return { deleted: false };
91
+ throw err;
92
+ }
64
93
  }
65
94
  async function listWorktrees(opts) {
66
95
  const out = await runGit(opts.root, ["worktree", "list", "--porcelain"], opts.git_timeout_ms ?? 3000);
@@ -248,9 +277,10 @@ function parseDecision(markdown) {
248
277
  return { token: null, reason: "Decision section is empty" };
249
278
  }
250
279
  const cleaned = firstLine.replace(/`/g, "").trim().toUpperCase();
251
- if (VALID_TOKENS.has(cleaned)) {
280
+ const normalized = cleaned === "APPROVE_WITH_NOTES" ? "APPROVE" : cleaned;
281
+ if (VALID_TOKENS.has(normalized)) {
252
282
  return {
253
- token: cleaned,
283
+ token: normalized,
254
284
  reason: firstLine.length > 200 ? firstLine.slice(0, 200) + "…" : firstLine
255
285
  };
256
286
  }
@@ -336,17 +366,17 @@ var require_visit = __commonJS((exports) => {
336
366
  visit.BREAK = BREAK;
337
367
  visit.SKIP = SKIP;
338
368
  visit.REMOVE = REMOVE;
339
- function visit_(key, node, visitor, path19) {
340
- const ctrl = callVisitor(key, node, visitor, path19);
369
+ function visit_(key, node, visitor, path20) {
370
+ const ctrl = callVisitor(key, node, visitor, path20);
341
371
  if (identity.isNode(ctrl) || identity.isPair(ctrl)) {
342
- replaceNode(key, path19, ctrl);
343
- return visit_(key, ctrl, visitor, path19);
372
+ replaceNode(key, path20, ctrl);
373
+ return visit_(key, ctrl, visitor, path20);
344
374
  }
345
375
  if (typeof ctrl !== "symbol") {
346
376
  if (identity.isCollection(node)) {
347
- path19 = Object.freeze(path19.concat(node));
377
+ path20 = Object.freeze(path20.concat(node));
348
378
  for (let i = 0;i < node.items.length; ++i) {
349
- const ci = visit_(i, node.items[i], visitor, path19);
379
+ const ci = visit_(i, node.items[i], visitor, path20);
350
380
  if (typeof ci === "number")
351
381
  i = ci - 1;
352
382
  else if (ci === BREAK)
@@ -357,13 +387,13 @@ var require_visit = __commonJS((exports) => {
357
387
  }
358
388
  }
359
389
  } else if (identity.isPair(node)) {
360
- path19 = Object.freeze(path19.concat(node));
361
- const ck = visit_("key", node.key, visitor, path19);
390
+ path20 = Object.freeze(path20.concat(node));
391
+ const ck = visit_("key", node.key, visitor, path20);
362
392
  if (ck === BREAK)
363
393
  return BREAK;
364
394
  else if (ck === REMOVE)
365
395
  node.key = null;
366
- const cv = visit_("value", node.value, visitor, path19);
396
+ const cv = visit_("value", node.value, visitor, path20);
367
397
  if (cv === BREAK)
368
398
  return BREAK;
369
399
  else if (cv === REMOVE)
@@ -384,17 +414,17 @@ var require_visit = __commonJS((exports) => {
384
414
  visitAsync.BREAK = BREAK;
385
415
  visitAsync.SKIP = SKIP;
386
416
  visitAsync.REMOVE = REMOVE;
387
- async function visitAsync_(key, node, visitor, path19) {
388
- const ctrl = await callVisitor(key, node, visitor, path19);
417
+ async function visitAsync_(key, node, visitor, path20) {
418
+ const ctrl = await callVisitor(key, node, visitor, path20);
389
419
  if (identity.isNode(ctrl) || identity.isPair(ctrl)) {
390
- replaceNode(key, path19, ctrl);
391
- return visitAsync_(key, ctrl, visitor, path19);
420
+ replaceNode(key, path20, ctrl);
421
+ return visitAsync_(key, ctrl, visitor, path20);
392
422
  }
393
423
  if (typeof ctrl !== "symbol") {
394
424
  if (identity.isCollection(node)) {
395
- path19 = Object.freeze(path19.concat(node));
425
+ path20 = Object.freeze(path20.concat(node));
396
426
  for (let i = 0;i < node.items.length; ++i) {
397
- const ci = await visitAsync_(i, node.items[i], visitor, path19);
427
+ const ci = await visitAsync_(i, node.items[i], visitor, path20);
398
428
  if (typeof ci === "number")
399
429
  i = ci - 1;
400
430
  else if (ci === BREAK)
@@ -405,13 +435,13 @@ var require_visit = __commonJS((exports) => {
405
435
  }
406
436
  }
407
437
  } else if (identity.isPair(node)) {
408
- path19 = Object.freeze(path19.concat(node));
409
- const ck = await visitAsync_("key", node.key, visitor, path19);
438
+ path20 = Object.freeze(path20.concat(node));
439
+ const ck = await visitAsync_("key", node.key, visitor, path20);
410
440
  if (ck === BREAK)
411
441
  return BREAK;
412
442
  else if (ck === REMOVE)
413
443
  node.key = null;
414
- const cv = await visitAsync_("value", node.value, visitor, path19);
444
+ const cv = await visitAsync_("value", node.value, visitor, path20);
415
445
  if (cv === BREAK)
416
446
  return BREAK;
417
447
  else if (cv === REMOVE)
@@ -438,23 +468,23 @@ var require_visit = __commonJS((exports) => {
438
468
  }
439
469
  return visitor;
440
470
  }
441
- function callVisitor(key, node, visitor, path19) {
471
+ function callVisitor(key, node, visitor, path20) {
442
472
  if (typeof visitor === "function")
443
- return visitor(key, node, path19);
473
+ return visitor(key, node, path20);
444
474
  if (identity.isMap(node))
445
- return visitor.Map?.(key, node, path19);
475
+ return visitor.Map?.(key, node, path20);
446
476
  if (identity.isSeq(node))
447
- return visitor.Seq?.(key, node, path19);
477
+ return visitor.Seq?.(key, node, path20);
448
478
  if (identity.isPair(node))
449
- return visitor.Pair?.(key, node, path19);
479
+ return visitor.Pair?.(key, node, path20);
450
480
  if (identity.isScalar(node))
451
- return visitor.Scalar?.(key, node, path19);
481
+ return visitor.Scalar?.(key, node, path20);
452
482
  if (identity.isAlias(node))
453
- return visitor.Alias?.(key, node, path19);
483
+ return visitor.Alias?.(key, node, path20);
454
484
  return;
455
485
  }
456
- function replaceNode(key, path19, node) {
457
- const parent = path19[path19.length - 1];
486
+ function replaceNode(key, path20, node) {
487
+ const parent = path20[path20.length - 1];
458
488
  if (identity.isCollection(parent)) {
459
489
  parent.items[key] = node;
460
490
  } else if (identity.isPair(parent)) {
@@ -1013,10 +1043,10 @@ var require_Collection = __commonJS((exports) => {
1013
1043
  var createNode = require_createNode();
1014
1044
  var identity = require_identity();
1015
1045
  var Node = require_Node();
1016
- function collectionFromPath(schema, path19, value) {
1046
+ function collectionFromPath(schema, path20, value) {
1017
1047
  let v = value;
1018
- for (let i = path19.length - 1;i >= 0; --i) {
1019
- const k = path19[i];
1048
+ for (let i = path20.length - 1;i >= 0; --i) {
1049
+ const k = path20[i];
1020
1050
  if (typeof k === "number" && Number.isInteger(k) && k >= 0) {
1021
1051
  const a = [];
1022
1052
  a[k] = v;
@@ -1035,7 +1065,7 @@ var require_Collection = __commonJS((exports) => {
1035
1065
  sourceObjects: new Map
1036
1066
  });
1037
1067
  }
1038
- var isEmptyPath = (path19) => path19 == null || typeof path19 === "object" && !!path19[Symbol.iterator]().next().done;
1068
+ var isEmptyPath = (path20) => path20 == null || typeof path20 === "object" && !!path20[Symbol.iterator]().next().done;
1039
1069
 
1040
1070
  class Collection extends Node.NodeBase {
1041
1071
  constructor(type, schema) {
@@ -1056,11 +1086,11 @@ var require_Collection = __commonJS((exports) => {
1056
1086
  copy.range = this.range.slice();
1057
1087
  return copy;
1058
1088
  }
1059
- addIn(path19, value) {
1060
- if (isEmptyPath(path19))
1089
+ addIn(path20, value) {
1090
+ if (isEmptyPath(path20))
1061
1091
  this.add(value);
1062
1092
  else {
1063
- const [key, ...rest] = path19;
1093
+ const [key, ...rest] = path20;
1064
1094
  const node = this.get(key, true);
1065
1095
  if (identity.isCollection(node))
1066
1096
  node.addIn(rest, value);
@@ -1070,8 +1100,8 @@ var require_Collection = __commonJS((exports) => {
1070
1100
  throw new Error(`Expected YAML collection at ${key}. Remaining path: ${rest}`);
1071
1101
  }
1072
1102
  }
1073
- deleteIn(path19) {
1074
- const [key, ...rest] = path19;
1103
+ deleteIn(path20) {
1104
+ const [key, ...rest] = path20;
1075
1105
  if (rest.length === 0)
1076
1106
  return this.delete(key);
1077
1107
  const node = this.get(key, true);
@@ -1080,8 +1110,8 @@ var require_Collection = __commonJS((exports) => {
1080
1110
  else
1081
1111
  throw new Error(`Expected YAML collection at ${key}. Remaining path: ${rest}`);
1082
1112
  }
1083
- getIn(path19, keepScalar) {
1084
- const [key, ...rest] = path19;
1113
+ getIn(path20, keepScalar) {
1114
+ const [key, ...rest] = path20;
1085
1115
  const node = this.get(key, true);
1086
1116
  if (rest.length === 0)
1087
1117
  return !keepScalar && identity.isScalar(node) ? node.value : node;
@@ -1096,15 +1126,15 @@ var require_Collection = __commonJS((exports) => {
1096
1126
  return n == null || allowScalar && identity.isScalar(n) && n.value == null && !n.commentBefore && !n.comment && !n.tag;
1097
1127
  });
1098
1128
  }
1099
- hasIn(path19) {
1100
- const [key, ...rest] = path19;
1129
+ hasIn(path20) {
1130
+ const [key, ...rest] = path20;
1101
1131
  if (rest.length === 0)
1102
1132
  return this.has(key);
1103
1133
  const node = this.get(key, true);
1104
1134
  return identity.isCollection(node) ? node.hasIn(rest) : false;
1105
1135
  }
1106
- setIn(path19, value) {
1107
- const [key, ...rest] = path19;
1136
+ setIn(path20, value) {
1137
+ const [key, ...rest] = path20;
1108
1138
  if (rest.length === 0) {
1109
1139
  this.set(key, value);
1110
1140
  } else {
@@ -3497,9 +3527,9 @@ var require_Document = __commonJS((exports) => {
3497
3527
  if (assertCollection(this.contents))
3498
3528
  this.contents.add(value);
3499
3529
  }
3500
- addIn(path19, value) {
3530
+ addIn(path20, value) {
3501
3531
  if (assertCollection(this.contents))
3502
- this.contents.addIn(path19, value);
3532
+ this.contents.addIn(path20, value);
3503
3533
  }
3504
3534
  createAlias(node, name) {
3505
3535
  if (!node.anchor) {
@@ -3548,30 +3578,30 @@ var require_Document = __commonJS((exports) => {
3548
3578
  delete(key) {
3549
3579
  return assertCollection(this.contents) ? this.contents.delete(key) : false;
3550
3580
  }
3551
- deleteIn(path19) {
3552
- if (Collection.isEmptyPath(path19)) {
3581
+ deleteIn(path20) {
3582
+ if (Collection.isEmptyPath(path20)) {
3553
3583
  if (this.contents == null)
3554
3584
  return false;
3555
3585
  this.contents = null;
3556
3586
  return true;
3557
3587
  }
3558
- return assertCollection(this.contents) ? this.contents.deleteIn(path19) : false;
3588
+ return assertCollection(this.contents) ? this.contents.deleteIn(path20) : false;
3559
3589
  }
3560
3590
  get(key, keepScalar) {
3561
3591
  return identity.isCollection(this.contents) ? this.contents.get(key, keepScalar) : undefined;
3562
3592
  }
3563
- getIn(path19, keepScalar) {
3564
- if (Collection.isEmptyPath(path19))
3593
+ getIn(path20, keepScalar) {
3594
+ if (Collection.isEmptyPath(path20))
3565
3595
  return !keepScalar && identity.isScalar(this.contents) ? this.contents.value : this.contents;
3566
- return identity.isCollection(this.contents) ? this.contents.getIn(path19, keepScalar) : undefined;
3596
+ return identity.isCollection(this.contents) ? this.contents.getIn(path20, keepScalar) : undefined;
3567
3597
  }
3568
3598
  has(key) {
3569
3599
  return identity.isCollection(this.contents) ? this.contents.has(key) : false;
3570
3600
  }
3571
- hasIn(path19) {
3572
- if (Collection.isEmptyPath(path19))
3601
+ hasIn(path20) {
3602
+ if (Collection.isEmptyPath(path20))
3573
3603
  return this.contents !== undefined;
3574
- return identity.isCollection(this.contents) ? this.contents.hasIn(path19) : false;
3604
+ return identity.isCollection(this.contents) ? this.contents.hasIn(path20) : false;
3575
3605
  }
3576
3606
  set(key, value) {
3577
3607
  if (this.contents == null) {
@@ -3580,13 +3610,13 @@ var require_Document = __commonJS((exports) => {
3580
3610
  this.contents.set(key, value);
3581
3611
  }
3582
3612
  }
3583
- setIn(path19, value) {
3584
- if (Collection.isEmptyPath(path19)) {
3613
+ setIn(path20, value) {
3614
+ if (Collection.isEmptyPath(path20)) {
3585
3615
  this.contents = value;
3586
3616
  } else if (this.contents == null) {
3587
- this.contents = Collection.collectionFromPath(this.schema, Array.from(path19), value);
3617
+ this.contents = Collection.collectionFromPath(this.schema, Array.from(path20), value);
3588
3618
  } else if (assertCollection(this.contents)) {
3589
- this.contents.setIn(path19, value);
3619
+ this.contents.setIn(path20, value);
3590
3620
  }
3591
3621
  }
3592
3622
  setSchema(version, options = {}) {
@@ -5481,9 +5511,9 @@ var require_cst_visit = __commonJS((exports) => {
5481
5511
  visit.BREAK = BREAK;
5482
5512
  visit.SKIP = SKIP;
5483
5513
  visit.REMOVE = REMOVE;
5484
- visit.itemAtPath = (cst, path19) => {
5514
+ visit.itemAtPath = (cst, path20) => {
5485
5515
  let item = cst;
5486
- for (const [field, index] of path19) {
5516
+ for (const [field, index] of path20) {
5487
5517
  const tok = item?.[field];
5488
5518
  if (tok && "items" in tok) {
5489
5519
  item = tok.items[index];
@@ -5492,23 +5522,23 @@ var require_cst_visit = __commonJS((exports) => {
5492
5522
  }
5493
5523
  return item;
5494
5524
  };
5495
- visit.parentCollection = (cst, path19) => {
5496
- const parent = visit.itemAtPath(cst, path19.slice(0, -1));
5497
- const field = path19[path19.length - 1][0];
5525
+ visit.parentCollection = (cst, path20) => {
5526
+ const parent = visit.itemAtPath(cst, path20.slice(0, -1));
5527
+ const field = path20[path20.length - 1][0];
5498
5528
  const coll = parent?.[field];
5499
5529
  if (coll && "items" in coll)
5500
5530
  return coll;
5501
5531
  throw new Error("Parent collection not found");
5502
5532
  };
5503
- function _visit(path19, item, visitor) {
5504
- let ctrl = visitor(item, path19);
5533
+ function _visit(path20, item, visitor) {
5534
+ let ctrl = visitor(item, path20);
5505
5535
  if (typeof ctrl === "symbol")
5506
5536
  return ctrl;
5507
5537
  for (const field of ["key", "value"]) {
5508
5538
  const token = item[field];
5509
5539
  if (token && "items" in token) {
5510
5540
  for (let i = 0;i < token.items.length; ++i) {
5511
- const ci = _visit(Object.freeze(path19.concat([[field, i]])), token.items[i], visitor);
5541
+ const ci = _visit(Object.freeze(path20.concat([[field, i]])), token.items[i], visitor);
5512
5542
  if (typeof ci === "number")
5513
5543
  i = ci - 1;
5514
5544
  else if (ci === BREAK)
@@ -5519,10 +5549,10 @@ var require_cst_visit = __commonJS((exports) => {
5519
5549
  }
5520
5550
  }
5521
5551
  if (typeof ctrl === "function" && field === "key")
5522
- ctrl = ctrl(item, path19);
5552
+ ctrl = ctrl(item, path20);
5523
5553
  }
5524
5554
  }
5525
- return typeof ctrl === "function" ? ctrl(item, path19) : ctrl;
5555
+ return typeof ctrl === "function" ? ctrl(item, path20) : ctrl;
5526
5556
  }
5527
5557
  exports.visit = visit;
5528
5558
  });
@@ -6791,14 +6821,14 @@ var require_parser = __commonJS((exports) => {
6791
6821
  case "scalar":
6792
6822
  case "single-quoted-scalar":
6793
6823
  case "double-quoted-scalar": {
6794
- const fs15 = this.flowScalar(this.type);
6824
+ const fs16 = this.flowScalar(this.type);
6795
6825
  if (atNextItem || it.value) {
6796
- map.items.push({ start, key: fs15, sep: [] });
6826
+ map.items.push({ start, key: fs16, sep: [] });
6797
6827
  this.onKeyLine = true;
6798
6828
  } else if (it.sep) {
6799
- this.stack.push(fs15);
6829
+ this.stack.push(fs16);
6800
6830
  } else {
6801
- Object.assign(it, { key: fs15, sep: [] });
6831
+ Object.assign(it, { key: fs16, sep: [] });
6802
6832
  this.onKeyLine = true;
6803
6833
  }
6804
6834
  return;
@@ -6926,13 +6956,13 @@ var require_parser = __commonJS((exports) => {
6926
6956
  case "scalar":
6927
6957
  case "single-quoted-scalar":
6928
6958
  case "double-quoted-scalar": {
6929
- const fs15 = this.flowScalar(this.type);
6959
+ const fs16 = this.flowScalar(this.type);
6930
6960
  if (!it || it.value)
6931
- fc.items.push({ start: [], key: fs15, sep: [] });
6961
+ fc.items.push({ start: [], key: fs16, sep: [] });
6932
6962
  else if (it.sep)
6933
- this.stack.push(fs15);
6963
+ this.stack.push(fs16);
6934
6964
  else
6935
- Object.assign(it, { key: fs15, sep: [] });
6965
+ Object.assign(it, { key: fs16, sep: [] });
6936
6966
  return;
6937
6967
  }
6938
6968
  case "flow-map-end":
@@ -9298,7 +9328,7 @@ async function resolveAgentForGuard(input, client, log4, opts = {}) {
9298
9328
  if (!opts.skipCache && chatAgentCacheReader) {
9299
9329
  try {
9300
9330
  const cached = chatAgentCacheReader(input.sessionID);
9301
- if (typeof cached === "string" && cached.length > 0) {
9331
+ if (typeof cached === "string") {
9302
9332
  return cached;
9303
9333
  }
9304
9334
  } catch (err) {
@@ -9310,7 +9340,7 @@ async function resolveAgentForGuard(input, client, log4, opts = {}) {
9310
9340
  }
9311
9341
  if (!opts.skipIpcLookup) {
9312
9342
  const viaSession = await resolveCurrentAgent(client, input.sessionID, log4);
9313
- if (typeof viaSession === "string" && viaSession.length > 0) {
9343
+ if (typeof viaSession === "string") {
9314
9344
  return viaSession;
9315
9345
  }
9316
9346
  }
@@ -10628,15 +10658,19 @@ class ApprovalStore {
10628
10658
  // tools/review-approval.ts
10629
10659
  var description4 = [
10630
10660
  "reviewer 专用:写入 APPROVE 审批记录。",
10631
- "**何时调用**:reviewer 给出 `## Decision\\nAPPROVE` 之前必须调本工具,",
10632
- "供 `/merge` 闭环或 codeforge orchestrator 后续放行依据。",
10661
+ "**何时调用**:reviewer 给出 `## Decision\\nAPPROVE` 之前必须调本工具。",
10662
+ "**两层语义独立**(ADR:decision-token-vs-approval-verdict-layering):",
10663
+ " - 本工具 `verdict` 字段属审批层,合法值:APPROVE / APPROVE_WITH_NOTES",
10664
+ " - reviewer 输出的 `## Decision` 节首行属协议层,合法值:APPROVE / REQUEST_CHANGES / BLOCK",
10665
+ " - ⚠️ 严禁把 `APPROVE_WITH_NOTES` 字面量写进 `## Decision` 节首行(容错层会归一,但其他变体如 APPROVE_MINOR 会失败 → merge-loop 误判死循环)",
10666
+ ' - ✅ 正确:verdict="APPROVE_WITH_NOTES" + `## Decision\\nAPPROVE`(首行写 APPROVE,详情在审批 notes)',
10633
10667
  "**pendingIds 格式**:推荐 `session:<sid>` / `plan:<plan_id>` / `decision:<hash>`;旧 `pc-<ts>-NNN` 仍兼容。",
10634
10668
  "**何时不调**:REQUEST_CHANGES / BLOCK 不调(无 APPROVE = 无审批记录)。",
10635
10669
  "**fallback**:codeforge 解析 reviewer boomerang 见 APPROVE 但无记录 → 自动以 source='codeforge-fallback' 补写。"
10636
10670
  ].join(`
10637
10671
  `);
10638
10672
  var ArgsSchema4 = z4.object({
10639
- verdict: z4.enum(["APPROVE", "APPROVE_WITH_NOTES"]).describe("审批裁决;REQUEST_CHANGES / BLOCK 不应调本工具"),
10673
+ verdict: z4.enum(["APPROVE", "APPROVE_WITH_NOTES"]).describe("审批层裁决(审计字段,与协议层独立);REQUEST_CHANGES / BLOCK 不应调本工具。⚠️ 不要把本字段值复制到 reviewer 输出的 `## Decision` 节首行 — 那是协议层 3 档。"),
10640
10674
  pendingIds: z4.array(z4.string().min(1)).min(1, "pendingIds 至少 1 条").describe("本次 APPROVE 覆盖的 id 列表。推荐 session:<sid> / plan:<plan_id> / decision:<hash>;旧 pc-xxx 兼容"),
10641
10675
  notes: z4.string().min(1, "notes 不能为空").max(2000, "notes 过长(> 2000 字),建议拆条").describe("审阅意见摘要(建议 ≤ 500 字)"),
10642
10676
  decisionLine: z4.string().optional().describe("`## Decision` 节首行原文(默认 verdict 字面量,机审证据)"),
@@ -11774,6 +11808,11 @@ function sleep(ms) {
11774
11808
  // lib/session-worktree.ts
11775
11809
  var REGISTRY_VERSION = 1;
11776
11810
  var DEFAULT_WORKTREE_SUBDIR = path13.join(".git", "codeforge-worktrees");
11811
+ function debugLog(msg) {
11812
+ if (process.env["CODEFORGE_DEBUG"]) {
11813
+ console.debug(`[session-worktree] ${msg}`);
11814
+ }
11815
+ }
11777
11816
  function registryDir(mainRoot) {
11778
11817
  return path13.join(runtimeDir(path13.resolve(mainRoot), { ensure: false }), "session-worktrees");
11779
11818
  }
@@ -11941,13 +11980,12 @@ async function mergeSessionBack(opts) {
11941
11980
  try {
11942
11981
  await removeWorktree({ root: mainRoot, worktree_path: wt, force: true });
11943
11982
  } catch (err) {
11944
- console.warn(`[session-worktree] removeWorktree 失败 (session=${opts.sessionId}): ${err.message}`);
11945
- }
11946
- try {
11947
- await runGit2(mainRoot, ["branch", "-D", branch]);
11948
- } catch (err) {
11949
- console.warn(`[session-worktree] branch -D ${branch} 失败 (可能已被 worktree remove 一并删): ${err.message}`);
11983
+ debugLog(`removeWorktree (merge) 非预期失败 (session=${opts.sessionId}): ${err.message}`);
11950
11984
  }
11985
+ await deleteBranchIfExists({ root: mainRoot, branch }).catch((err) => {
11986
+ debugLog(`deleteBranchIfExists (merge) 非预期失败: ${err.message}`);
11987
+ return { deleted: false };
11988
+ });
11951
11989
  await mutateRegistry(mainRoot, (reg) => {
11952
11990
  const e = reg.entries.find((x) => x.sessionId === opts.sessionId);
11953
11991
  if (e) {
@@ -11978,13 +12016,12 @@ async function discardSession(opts) {
11978
12016
  force: true
11979
12017
  });
11980
12018
  } catch (err) {
11981
- console.warn(`[session-worktree] removeWorktree (discard) 失败: ${err.message}`);
11982
- }
11983
- try {
11984
- await runGit2(mainRoot, ["branch", "-D", entry.branch]);
11985
- } catch (err) {
11986
- console.warn(`[session-worktree] branch -D ${entry.branch} (discard) 失败: ${err.message}`);
12019
+ debugLog(`removeWorktree (discard) 非预期失败: ${err.message}`);
11987
12020
  }
12021
+ await deleteBranchIfExists({ root: mainRoot, branch: entry.branch }).catch((err) => {
12022
+ debugLog(`deleteBranchIfExists (discard) 非预期失败: ${err.message}`);
12023
+ return { deleted: false };
12024
+ });
11988
12025
  await mutateRegistry(mainRoot, (reg) => {
11989
12026
  const e = reg.entries.find((x) => x.sessionId === opts.sessionId);
11990
12027
  if (e) {
@@ -12125,7 +12162,8 @@ Codeforge-Base: ${baseSha.slice(0, 12)}`;
12125
12162
  return subject + body + footer;
12126
12163
  }
12127
12164
  var ORPHAN_GRACE_MS = 60000;
12128
- var SEMANTIC_ORPHAN_MIN_AGE_MS = 24 * 60 * 60000;
12165
+ var SEMANTIC_ORPHAN_MIN_AGE_MS = 6 * 60 * 60000;
12166
+ var SEMANTIC_ORPHAN_UNKNOWN_TIMEOUT_MS = 72 * 60 * 60000;
12129
12167
  async function pruneDiscardedRegistryEntries(mainRoot, opts = {}) {
12130
12168
  const keepRecent = opts.keepRecent ?? 50;
12131
12169
  if (keepRecent < 0) {
@@ -12209,6 +12247,7 @@ async function pruneOrphanWorktrees(mainRoot, opts = {}) {
12209
12247
  });
12210
12248
  if (opts.isSessionAlive) {
12211
12249
  const minAge = opts.semanticOrphanMinAgeMs ?? SEMANTIC_ORPHAN_MIN_AGE_MS;
12250
+ const unknownTimeout = opts.semanticOrphanUnknownTimeoutMs ?? SEMANTIC_ORPHAN_UNKNOWN_TIMEOUT_MS;
12212
12251
  const probe = opts.isSessionAlive;
12213
12252
  await mutateRegistry(resolved, async (reg2) => {
12214
12253
  const now = Date.now();
@@ -12228,10 +12267,11 @@ async function pruneOrphanWorktrees(mainRoot, opts = {}) {
12228
12267
  continue;
12229
12268
  }
12230
12269
  if (aliveResult.source === "unknown") {
12231
- skipped++;
12232
- continue;
12233
- }
12234
- if (aliveResult.alive) {
12270
+ if (now - updatedMs < unknownTimeout) {
12271
+ skipped++;
12272
+ continue;
12273
+ }
12274
+ } else if (aliveResult.alive) {
12235
12275
  skipped++;
12236
12276
  continue;
12237
12277
  }
@@ -12248,15 +12288,14 @@ async function pruneOrphanWorktrees(mainRoot, opts = {}) {
12248
12288
  });
12249
12289
  continue;
12250
12290
  }
12251
- try {
12252
- await runGit2(resolved, ["branch", "-D", entry.branch]);
12253
- } catch {}
12291
+ await deleteBranchIfExists({ root: resolved, branch: entry.branch }).catch(() => {});
12254
12292
  entry.status = "discarded";
12255
12293
  entry.updatedAt = new Date().toISOString();
12294
+ const reasonSource = aliveResult.source === "unknown" ? `unknown-timeout (registry.updatedAt 已老于 ${unknownTimeout / 3600000}h)` : `opencode session ${aliveResult.source}: dead`;
12256
12295
  cleaned.push({
12257
12296
  sessionId: entry.sessionId,
12258
12297
  worktreePath: entry.worktreePath,
12259
- reason: `D 类语义孤儿 (opencode session ${aliveResult.source}: dead)`
12298
+ reason: `D 类语义孤儿 (${reasonSource})`
12260
12299
  });
12261
12300
  }
12262
12301
  });
@@ -12303,6 +12342,13 @@ async function pruneOrphanWorktrees(mainRoot, opts = {}) {
12303
12342
  } catch (err) {
12304
12343
  lastError = err instanceof Error ? err.message : String(err);
12305
12344
  }
12345
+ if (removed) {
12346
+ try {
12347
+ await fs10.stat(candidate);
12348
+ removed = false;
12349
+ lastError = lastError ?? "git worktree remove 返回成功但目录仍存在(C 类 fs-only orphan)";
12350
+ } catch {}
12351
+ }
12306
12352
  if (!removed && dirExists) {
12307
12353
  try {
12308
12354
  await fs10.rm(candidate, { recursive: true, force: true });
@@ -12326,6 +12372,39 @@ async function pruneOrphanWorktrees(mainRoot, opts = {}) {
12326
12372
  return { cleaned, failed, skipped, discardedPruned };
12327
12373
  }
12328
12374
 
12375
+ // lib/merge-gate.ts
12376
+ import { promises as fs11 } from "node:fs";
12377
+ import * as path14 from "node:path";
12378
+ var DEFAULT_MERGE_GATE_CONFIG = { enabled: true };
12379
+ var CONFIG_REL = ".codeforge/merge-gate.json";
12380
+ async function loadMergeGate(mainRoot) {
12381
+ const file = path14.join(mainRoot, CONFIG_REL);
12382
+ let raw;
12383
+ try {
12384
+ raw = await fs11.readFile(file, "utf8");
12385
+ } catch (err) {
12386
+ const e = err;
12387
+ if (e.code === "ENOENT")
12388
+ return { ...DEFAULT_MERGE_GATE_CONFIG };
12389
+ console.warn(`[merge-gate] 读取 ${CONFIG_REL} 失败,fail-safe 退化为 enabled=false: ${e.message}`);
12390
+ return { enabled: false };
12391
+ }
12392
+ let parsed;
12393
+ try {
12394
+ parsed = JSON.parse(raw);
12395
+ } catch (err) {
12396
+ console.warn(`[merge-gate] ${CONFIG_REL} JSON 解析失败,fail-safe 退化为 enabled=false: ${err instanceof Error ? err.message : String(err)}`);
12397
+ return { enabled: false };
12398
+ }
12399
+ if (!parsed || typeof parsed !== "object") {
12400
+ console.warn(`[merge-gate] ${CONFIG_REL} 顶层非 object,fail-safe 退化为 enabled=false`);
12401
+ return { enabled: false };
12402
+ }
12403
+ const obj = parsed;
12404
+ const enabled = typeof obj["enabled"] === "boolean" ? obj["enabled"] : DEFAULT_MERGE_GATE_CONFIG.enabled;
12405
+ return { enabled };
12406
+ }
12407
+
12329
12408
  // lib/merge-loop.ts
12330
12409
  var DEFAULT_MERGE_LOOP_CONFIG = {
12331
12410
  maxReviewLoops: 3,
@@ -12337,6 +12416,8 @@ var DEFAULT_MERGE_LOOP_CONFIG = {
12337
12416
  async function runMergeLoop(opts) {
12338
12417
  const config = { ...DEFAULT_MERGE_LOOP_CONFIG, ...opts.config ?? {} };
12339
12418
  const progress = opts.onProgress ?? (() => {});
12419
+ const mergeGateLoader = opts.__testHooks?.loadMergeGate ?? loadMergeGate;
12420
+ const mergeGate = config.mergeGate ?? await mergeGateLoader(opts.mainRoot);
12340
12421
  let loops = 0;
12341
12422
  let lastReviewSummary;
12342
12423
  progress("pre_check", `准备 merge session ${opts.sessionId}`);
@@ -12405,6 +12486,38 @@ async function runMergeLoop(opts) {
12405
12486
  if (reviewResult.decision === "APPROVE") {
12406
12487
  progress("do_merge", "APPROVE,执行 squash merge");
12407
12488
  await maybeAbort(opts, config, entry);
12489
+ if (mergeGate.enabled) {
12490
+ const store = opts.__testHooks?.approvalStore ?? ApprovalStore.forProject(opts.mainRoot);
12491
+ let approval;
12492
+ let approvalKey = `session:${opts.sessionId}`;
12493
+ try {
12494
+ approval = await store.getLatest(approvalKey);
12495
+ if (!approval && opts.planId) {
12496
+ approvalKey = `plan:${opts.planId}`;
12497
+ approval = await store.getLatest(approvalKey);
12498
+ }
12499
+ } catch (err) {
12500
+ console.warn(`[merge-loop] approval-store 查询失败 (session=${opts.sessionId}): ${err instanceof Error ? err.message : String(err)}`);
12501
+ approval = undefined;
12502
+ }
12503
+ const valid = !!approval && (approval.verdict === "APPROVE" || approval.verdict === "APPROVE_WITH_NOTES");
12504
+ if (!valid) {
12505
+ const blockReason = buildApprovalGateBlockReason({
12506
+ sessionId: opts.sessionId,
12507
+ ...opts.planId ? { planId: opts.planId } : {},
12508
+ approvalKey,
12509
+ storeVerdict: approval?.verdict ?? "NONE"
12510
+ });
12511
+ progress("block_pause", "approval-store 校验未通过");
12512
+ return {
12513
+ status: "blocked",
12514
+ loops,
12515
+ finalDecision: "APPROVE",
12516
+ blockReason,
12517
+ ...lastReviewSummary ? { lastReviewSummary } : {}
12518
+ };
12519
+ }
12520
+ }
12408
12521
  const { sha } = await mergeSessionBack({
12409
12522
  sessionId: opts.sessionId,
12410
12523
  mainRoot: opts.mainRoot
@@ -12494,6 +12607,22 @@ function buildForceMergeMessage(sessionId, entry) {
12494
12607
  ` + `Codeforge-Base: ${entry.baseSha.slice(0, 12)}
12495
12608
  ` + `Codeforge-Force-Merge: true`;
12496
12609
  }
12610
+ function buildApprovalGateBlockReason(args) {
12611
+ return `reviewer 摘要 APPROVE 但 approval-store 无对应记录
12612
+ ` + `(已查 session:${args.sessionId} 与 plan:${args.planId ?? "<无>"} 均无有效 APPROVE;` + `store verdict=${args.storeVerdict})
12613
+
12614
+ ` + `可能原因:
12615
+ ` + ` - reviewer agent 输出 APPROVE 但漏调 review_approval 工具写记录
12616
+ ` + ` - codeforge-fallback 也未补写
12617
+ ` + ` - 解析失真:摘要含 APPROVE 字面但实际未通过
12618
+
12619
+ ` + `解除路径三选一:
12620
+ ` + ` 1. 让 reviewer 重新跑一遍(重派 reviewer,确认调用 review_approval)
12621
+ ` + ` 2. codeforge 手动补写:调 review_approval(verdict=APPROVE, pendingIds=["${args.approvalKey}"], source="codeforge-fallback")
12622
+ ` + ` 3. 用户拍板:/merge --force 跳过 review(写 escape 审计)
12623
+
12624
+ ` + `ADR: session-merge-approval-hard-gate`;
12625
+ }
12497
12626
  async function maybeAbort(opts, config, entry) {
12498
12627
  if (!opts.signal?.aborted)
12499
12628
  return;
@@ -12783,8 +12912,8 @@ async function execute12(input) {
12783
12912
  import { z as z13 } from "zod";
12784
12913
 
12785
12914
  // lib/plan-store.ts
12786
- import { promises as fs11 } from "node:fs";
12787
- import * as path14 from "node:path";
12915
+ import { promises as fs12 } from "node:fs";
12916
+ import * as path15 from "node:path";
12788
12917
  var INDEX_VERSION = 1;
12789
12918
 
12790
12919
  class PlanStore {
@@ -12793,8 +12922,8 @@ class PlanStore {
12793
12922
  now;
12794
12923
  secondCounters = new Map;
12795
12924
  constructor(opts = {}) {
12796
- this.root = path14.resolve(opts.root ?? process.cwd());
12797
- this.base = opts.base ? path14.resolve(opts.base) : plansDir(this.root);
12925
+ this.root = path15.resolve(opts.root ?? process.cwd());
12926
+ this.base = opts.base ? path15.resolve(opts.base) : plansDir(this.root);
12798
12927
  this.now = opts.now ?? (() => new Date);
12799
12928
  }
12800
12929
  async write(input) {
@@ -12804,14 +12933,14 @@ class PlanStore {
12804
12933
  if (typeof input.content !== "string" || input.content.length === 0) {
12805
12934
  throw new Error("PlanStore.write: content 不能为空");
12806
12935
  }
12807
- await fs11.mkdir(this.base, { recursive: true });
12936
+ await fs12.mkdir(this.base, { recursive: true });
12808
12937
  const lockPath = this.lockPath();
12809
12938
  return await withFileLock(lockPath, async () => {
12810
12939
  const index = await this.readIndexLocked();
12811
12940
  const now = this.now();
12812
12941
  const planId = this.allocPlanId(now, index);
12813
12942
  const filename = this.composeFilename(planId, input.title);
12814
- const absFile = path14.join(this.base, filename);
12943
+ const absFile = path15.join(this.base, filename);
12815
12944
  await this.atomicWriteFile(absFile, input.content);
12816
12945
  const entry = {
12817
12946
  plan_id: planId,
@@ -12835,9 +12964,9 @@ class PlanStore {
12835
12964
  const entry = index.entries.find((e) => e.plan_id === planId);
12836
12965
  if (!entry)
12837
12966
  return null;
12838
- const abs = path14.join(this.base, entry.path);
12967
+ const abs = path15.join(this.base, entry.path);
12839
12968
  try {
12840
- const content = await fs11.readFile(abs, "utf8");
12969
+ const content = await fs12.readFile(abs, "utf8");
12841
12970
  return { entry, content };
12842
12971
  } catch (err) {
12843
12972
  const e = err;
@@ -12896,7 +13025,7 @@ class PlanStore {
12896
13025
  else if (e.status === "orphan")
12897
13026
  shouldDelete = true;
12898
13027
  if (shouldDelete) {
12899
- await fs11.rm(path14.join(this.base, e.path), { force: true }).catch(() => {});
13028
+ await fs12.rm(path15.join(this.base, e.path), { force: true }).catch(() => {});
12900
13029
  removed++;
12901
13030
  } else {
12902
13031
  keep.push(e);
@@ -12918,9 +13047,9 @@ class PlanStore {
12918
13047
  knownPaths.add(e.path);
12919
13048
  if (e.status !== "active")
12920
13049
  continue;
12921
- const abs = path14.join(this.base, e.path);
13050
+ const abs = path15.join(this.base, e.path);
12922
13051
  try {
12923
- await fs11.stat(abs);
13052
+ await fs12.stat(abs);
12924
13053
  } catch {
12925
13054
  e.status = "orphan";
12926
13055
  markedOrphan++;
@@ -12930,23 +13059,23 @@ class PlanStore {
12930
13059
  await this.writeIndexLocked(index);
12931
13060
  let unindexedFiles = [];
12932
13061
  try {
12933
- const all = await fs11.readdir(this.base);
13062
+ const all = await fs12.readdir(this.base);
12934
13063
  unindexedFiles = all.filter((f) => f.endsWith(".md")).filter((f) => !knownPaths.has(f));
12935
13064
  } catch {}
12936
13065
  return { markedOrphan, unindexedFiles };
12937
13066
  });
12938
13067
  }
12939
13068
  indexPath() {
12940
- return path14.join(this.base, "index.json");
13069
+ return path15.join(this.base, "index.json");
12941
13070
  }
12942
13071
  lockPath() {
12943
- return path14.join(this.base, "index.lock");
13072
+ return path15.join(this.base, "index.lock");
12944
13073
  }
12945
13074
  async readIndexLocked() {
12946
13075
  const file = this.indexPath();
12947
13076
  let raw;
12948
13077
  try {
12949
- raw = await fs11.readFile(file, "utf8");
13078
+ raw = await fs12.readFile(file, "utf8");
12950
13079
  } catch (err) {
12951
13080
  const e = err;
12952
13081
  if (e.code === "ENOENT")
@@ -12968,7 +13097,7 @@ class PlanStore {
12968
13097
  async archiveCorruptIndex(file) {
12969
13098
  const ts = this.now().toISOString().replace(/[-:.TZ]/g, "").slice(0, 14);
12970
13099
  const dst = `${file}.corrupt-${ts}`;
12971
- await fs11.rename(file, dst).catch(() => {});
13100
+ await fs12.rename(file, dst).catch(() => {});
12972
13101
  }
12973
13102
  async readIndex() {
12974
13103
  return this.readIndexLocked();
@@ -13013,15 +13142,15 @@ class PlanStore {
13013
13142
  const tsPart = m ? `${m[1]}-${m[2]}` : planId;
13014
13143
  const nnn = m ? m[3] : "000";
13015
13144
  const sample = planFilePath(this.root, title);
13016
- const base = path14.basename(sample, ".md");
13145
+ const base = path15.basename(sample, ".md");
13017
13146
  const slug = base.replace(/^\d{8}-\d{6}-?/, "") || "untitled";
13018
13147
  return `${tsPart}-${nnn}-${slug}.md`;
13019
13148
  }
13020
13149
  async atomicWriteFile(file, data) {
13021
- await fs11.mkdir(path14.dirname(file), { recursive: true });
13150
+ await fs12.mkdir(path15.dirname(file), { recursive: true });
13022
13151
  const tmp = `${file}.tmp-${process.pid}-${Date.now()}`;
13023
- await fs11.writeFile(tmp, data, "utf8");
13024
- await fs11.rename(tmp, file);
13152
+ await fs12.writeFile(tmp, data, "utf8");
13153
+ await fs12.rename(tmp, file);
13025
13154
  }
13026
13155
  }
13027
13156
  function formatTimestamp(d) {
@@ -13084,8 +13213,8 @@ async function execute13(input) {
13084
13213
  }
13085
13214
  }
13086
13215
  // tools/plan-read.ts
13087
- import { promises as fs12 } from "node:fs";
13088
- import * as path15 from "node:path";
13216
+ import { promises as fs13 } from "node:fs";
13217
+ import * as path16 from "node:path";
13089
13218
  import { z as z14 } from "zod";
13090
13219
  var description14 = [
13091
13220
  "读取方案文档内容,支持按 plan_id 或绝对路径查询。",
@@ -13191,9 +13320,9 @@ async function execute14(input) {
13191
13320
  };
13192
13321
  }
13193
13322
  }
13194
- const abs = path15.resolve(args.path);
13323
+ const abs = path16.resolve(args.path);
13195
13324
  try {
13196
- const content = await fs12.readFile(abs, "utf8");
13325
+ const content = await fs13.readFile(abs, "utf8");
13197
13326
  return {
13198
13327
  ok: true,
13199
13328
  content,
@@ -13218,16 +13347,16 @@ import { z as z15 } from "zod";
13218
13347
  // lib/adr-init.ts
13219
13348
  import { spawnSync } from "node:child_process";
13220
13349
  import { existsSync as existsSync4, promises as fsp } from "node:fs";
13221
- import * as path16 from "node:path";
13350
+ import * as path17 from "node:path";
13222
13351
  import * as url from "node:url";
13223
13352
  function resolveAssetsRoot() {
13224
- const here = path16.dirname(url.fileURLToPath(import.meta.url));
13353
+ const here = path17.dirname(url.fileURLToPath(import.meta.url));
13225
13354
  let dir = here;
13226
13355
  for (let i = 0;i < 6; i++) {
13227
- if (existsSync4(path16.join(dir, "package.json")) && existsSync4(path16.join(dir, "assets", "adr-init"))) {
13228
- return path16.join(dir, "assets", "adr-init");
13356
+ if (existsSync4(path17.join(dir, "package.json")) && existsSync4(path17.join(dir, "assets", "adr-init"))) {
13357
+ return path17.join(dir, "assets", "adr-init");
13229
13358
  }
13230
- const parent = path16.dirname(dir);
13359
+ const parent = path17.dirname(dir);
13231
13360
  if (parent === dir)
13232
13361
  break;
13233
13362
  dir = parent;
@@ -13235,13 +13364,13 @@ function resolveAssetsRoot() {
13235
13364
  const xdgConfig = process.env["XDG_CONFIG_HOME"];
13236
13365
  const homeDir = process.env["HOME"] ?? process.env["USERPROFILE"] ?? "";
13237
13366
  const fallbackRoots = [
13238
- xdgConfig ? path16.join(xdgConfig, "opencode") : null,
13239
- path16.join(homeDir, ".config", "opencode"),
13240
- process.env["APPDATA"] ? path16.join(process.env["APPDATA"], "opencode") : null,
13241
- process.env["LOCALAPPDATA"] ? path16.join(process.env["LOCALAPPDATA"], "opencode") : null
13367
+ xdgConfig ? path17.join(xdgConfig, "opencode") : null,
13368
+ path17.join(homeDir, ".config", "opencode"),
13369
+ process.env["APPDATA"] ? path17.join(process.env["APPDATA"], "opencode") : null,
13370
+ process.env["LOCALAPPDATA"] ? path17.join(process.env["LOCALAPPDATA"], "opencode") : null
13242
13371
  ].filter(Boolean);
13243
13372
  for (const root of fallbackRoots) {
13244
- const candidate = path16.join(root, "assets", "adr-init");
13373
+ const candidate = path17.join(root, "assets", "adr-init");
13245
13374
  if (existsSync4(candidate)) {
13246
13375
  return candidate;
13247
13376
  }
@@ -13278,7 +13407,7 @@ function runGitConfigHooksPath(cwd) {
13278
13407
  }
13279
13408
  }
13280
13409
  async function runAdrInit(opts = {}) {
13281
- const cwd = path16.resolve(opts.cwd ?? process.cwd());
13410
+ const cwd = path17.resolve(opts.cwd ?? process.cwd());
13282
13411
  const force = !!opts.force;
13283
13412
  const dryRun = !!opts.dryRun;
13284
13413
  const writePrepare = !!opts.writePrepare;
@@ -13327,8 +13456,8 @@ async function runAdrInit(opts = {}) {
13327
13456
  });
13328
13457
  }
13329
13458
  for (const item of plan) {
13330
- const srcAbs = path16.join(assetsRoot, item.src);
13331
- const dstAbs = path16.join(cwd, item.dst);
13459
+ const srcAbs = path17.join(assetsRoot, item.src);
13460
+ const dstAbs = path17.join(cwd, item.dst);
13332
13461
  if (!existsSync4(srcAbs)) {
13333
13462
  result.warnings.push(`资产缺失:${item.src}(跳过 ${item.dst})`);
13334
13463
  continue;
@@ -13341,7 +13470,7 @@ async function runAdrInit(opts = {}) {
13341
13470
  const bakRel = `${item.dst}.bak.${ts}`;
13342
13471
  if (!dryRun) {
13343
13472
  try {
13344
- await fsp.copyFile(dstAbs, path16.join(cwd, bakRel));
13473
+ await fsp.copyFile(dstAbs, path17.join(cwd, bakRel));
13345
13474
  } catch (e) {
13346
13475
  result.ok = false;
13347
13476
  result.reason = "io_error";
@@ -13353,7 +13482,7 @@ async function runAdrInit(opts = {}) {
13353
13482
  }
13354
13483
  if (!dryRun) {
13355
13484
  try {
13356
- await fsp.mkdir(path16.dirname(dstAbs), { recursive: true });
13485
+ await fsp.mkdir(path17.dirname(dstAbs), { recursive: true });
13357
13486
  await fsp.copyFile(srcAbs, dstAbs);
13358
13487
  if (item.chmod !== undefined) {
13359
13488
  try {
@@ -13383,7 +13512,7 @@ async function runAdrInit(opts = {}) {
13383
13512
  } else {
13384
13513
  result.suggestions.push("[dry-run] 将运行:git config core.hooksPath .githooks");
13385
13514
  }
13386
- const pkgPath = path16.join(cwd, "package.json");
13515
+ const pkgPath = path17.join(cwd, "package.json");
13387
13516
  const isNpm = existsSync4(pkgPath);
13388
13517
  if (isNpm) {
13389
13518
  if (writePrepare) {
@@ -13398,7 +13527,7 @@ async function runAdrInit(opts = {}) {
13398
13527
  const bakRel = `package.json.bak.${ts}`;
13399
13528
  if (!dryRun) {
13400
13529
  try {
13401
- await fsp.copyFile(pkgPath, path16.join(cwd, bakRel));
13530
+ await fsp.copyFile(pkgPath, path17.join(cwd, bakRel));
13402
13531
  } catch (e) {
13403
13532
  result.warnings.push(`备份 package.json 失败:${e instanceof Error ? e.message : String(e)}`);
13404
13533
  }
@@ -13777,24 +13906,24 @@ async function sendParentNotice(client, sessionID, text, opts = {}) {
13777
13906
  init_decision_parser();
13778
13907
 
13779
13908
  // lib/parent-map-store.ts
13780
- import { promises as fs13 } from "node:fs";
13781
- import * as path17 from "node:path";
13909
+ import { promises as fs14 } from "node:fs";
13910
+ import * as path18 from "node:path";
13782
13911
  var PARENT_MAP_VERSION = 1;
13783
13912
  var PARENT_MAP_LOCK_TIMEOUT_MS = 2000;
13784
13913
  var PARENT_MAP_MAX_ENTRIES = 256;
13785
13914
  function parentMapDir(mainRoot) {
13786
- return path17.join(runtimeDir(path17.resolve(mainRoot), { ensure: false }), "session-worktrees");
13915
+ return path18.join(runtimeDir(path18.resolve(mainRoot), { ensure: false }), "session-worktrees");
13787
13916
  }
13788
13917
  function parentMapPath(mainRoot) {
13789
- return path17.join(parentMapDir(mainRoot), "parent-map.json");
13918
+ return path18.join(parentMapDir(mainRoot), "parent-map.json");
13790
13919
  }
13791
13920
  function parentMapLockPath(mainRoot) {
13792
- return path17.join(parentMapDir(mainRoot), "parent-map.lock");
13921
+ return path18.join(parentMapDir(mainRoot), "parent-map.lock");
13793
13922
  }
13794
13923
  async function readParentMapFile(mainRoot) {
13795
13924
  const file = parentMapPath(mainRoot);
13796
13925
  try {
13797
- const raw = await fs13.readFile(file, "utf8");
13926
+ const raw = await fs14.readFile(file, "utf8");
13798
13927
  const parsed = JSON.parse(raw);
13799
13928
  if (parsed.version !== PARENT_MAP_VERSION || !Array.isArray(parsed.entries)) {
13800
13929
  return { version: PARENT_MAP_VERSION, entries: [] };
@@ -13809,10 +13938,10 @@ async function readParentMapFile(mainRoot) {
13809
13938
  }
13810
13939
  async function writeParentMapFile(mainRoot, payload) {
13811
13940
  const file = parentMapPath(mainRoot);
13812
- await fs13.mkdir(path17.dirname(file), { recursive: true });
13941
+ await fs14.mkdir(path18.dirname(file), { recursive: true });
13813
13942
  const tmp = `${file}.tmp-${process.pid}-${Date.now()}`;
13814
- await fs13.writeFile(tmp, JSON.stringify(payload, null, 2), "utf8");
13815
- await fs13.rename(tmp, file);
13943
+ await fs14.writeFile(tmp, JSON.stringify(payload, null, 2), "utf8");
13944
+ await fs14.rename(tmp, file);
13816
13945
  }
13817
13946
  function capEntriesByTsDesc(entries) {
13818
13947
  if (entries.length <= PARENT_MAP_MAX_ENTRIES)
@@ -13844,7 +13973,7 @@ async function loadParentMap(mainRoot) {
13844
13973
  }
13845
13974
  async function mutateParentMap(mainRoot, mutator, opts = {}) {
13846
13975
  const lockPath = parentMapLockPath(mainRoot);
13847
- await fs13.mkdir(path17.dirname(lockPath), { recursive: true });
13976
+ await fs14.mkdir(path18.dirname(lockPath), { recursive: true });
13848
13977
  const lockOpts = {
13849
13978
  timeoutMs: opts.timeoutMs ?? PARENT_MAP_LOCK_TIMEOUT_MS,
13850
13979
  ...opts
@@ -13863,7 +13992,7 @@ async function appendParentEntry(mainRoot, childID, parentID, ts, opts = {}) {
13863
13992
  if (!childID || !parentID)
13864
13993
  return;
13865
13994
  const lockPath = parentMapLockPath(mainRoot);
13866
- await fs13.mkdir(path17.dirname(lockPath), { recursive: true });
13995
+ await fs14.mkdir(path18.dirname(lockPath), { recursive: true });
13867
13996
  const lockOpts = {
13868
13997
  timeoutMs: opts.timeoutMs ?? PARENT_MAP_LOCK_TIMEOUT_MS,
13869
13998
  ...opts
@@ -14196,8 +14325,8 @@ function clip3(s, max) {
14196
14325
  }
14197
14326
 
14198
14327
  // lib/codeforge-runtime.ts
14199
- import { promises as fs14 } from "node:fs";
14200
- import * as path18 from "node:path";
14328
+ import { promises as fs15 } from "node:fs";
14329
+ import * as path19 from "node:path";
14201
14330
  var DEFAULT_RUNTIME = {
14202
14331
  autonomy: {
14203
14332
  downgrade_on_risky: true
@@ -14240,10 +14369,10 @@ function loadRuntimeSync(opts = {}) {
14240
14369
  }
14241
14370
  async function loadRuntime(opts = {}) {
14242
14371
  const root = opts.root ?? process.cwd();
14243
- const abs = path18.resolve(root, opts.file ?? CONFIG_FILE);
14372
+ const abs = path19.resolve(root, opts.file ?? CONFIG_FILE);
14244
14373
  let raw;
14245
14374
  try {
14246
- raw = await fs14.readFile(abs, "utf8");
14375
+ raw = await fs15.readFile(abs, "utf8");
14247
14376
  } catch (e) {
14248
14377
  const code = e.code;
14249
14378
  if (code === "ENOENT") {
@@ -14803,7 +14932,7 @@ var codeforgeToolsServer = async (ctx) => {
14803
14932
  review_approval: tool({
14804
14933
  description: description4,
14805
14934
  args: {
14806
- verdict: z16.enum(["APPROVE", "APPROVE_WITH_NOTES"]).describe("审批裁决;REQUEST_CHANGES / BLOCK 不应调本工具"),
14935
+ verdict: z16.enum(["APPROVE", "APPROVE_WITH_NOTES"]).describe("审批层裁决(审计字段,与协议层独立);REQUEST_CHANGES / BLOCK 不应调本工具。⚠️ 不要把本字段值复制到 reviewer 输出的 `## Decision` 节首行 — 那是协议层 3 档。"),
14807
14936
  pendingIds: z16.array(z16.string().min(1)).min(1).describe("本次 APPROVE 覆盖的 pending change id 列表"),
14808
14937
  notes: z16.string().min(1).max(2000).describe("审阅意见摘要(建议 ≤ 500 字)"),
14809
14938
  decisionLine: z16.string().optional().describe("`## Decision` 节首行原文(默认 verdict 字面量,机审证据)"),
@@ -14964,7 +15093,7 @@ var handler7 = codeforgeToolsServer;
14964
15093
 
14965
15094
  // plugins/discover-spec-suggest.ts
14966
15095
  import { readFileSync as readFileSync3, readdirSync, statSync as statSync2 } from "node:fs";
14967
- import { join as join15 } from "node:path";
15096
+ import { join as join16 } from "node:path";
14968
15097
 
14969
15098
  // lib/handoff-schema.ts
14970
15099
  import { z as z17 } from "zod";
@@ -15131,9 +15260,9 @@ function validateHandoff(rawYaml, fileSize) {
15131
15260
  const result = HandoffSchema.safeParse(parsed);
15132
15261
  if (!result.success) {
15133
15262
  const first = result.error.issues[0];
15134
- const path19 = first?.path?.join(".") ?? "(root)";
15263
+ const path20 = first?.path?.join(".") ?? "(root)";
15135
15264
  const msg = first?.message ?? "unknown";
15136
- return { ok: false, reason: `schema 校验失败:${path19}: ${msg}` };
15265
+ return { ok: false, reason: `schema 校验失败:${path20}: ${msg}` };
15137
15266
  }
15138
15267
  return { ok: true, data: result.data, schemaVersion: result.data.schema_version };
15139
15268
  }
@@ -15150,7 +15279,7 @@ var SESSION_TTL_MS2 = 24 * 60 * 60 * 1000;
15150
15279
  var MATCH_THRESHOLD = 0.15;
15151
15280
  var MAX_CANDIDATES = 3;
15152
15281
  var NUDGE_MAX_LEN = 1500;
15153
- var SPECS_REL_DIR = join15("docs", "specs");
15282
+ var SPECS_REL_DIR = join16("docs", "specs");
15154
15283
  var sessionMap = new Map;
15155
15284
  function pruneIfOversize2() {
15156
15285
  while (sessionMap.size > SESSION_CAP2) {
@@ -15257,7 +15386,7 @@ function loadSpecs(rootDir, opts = {}) {
15257
15386
  const dirExists = opts.dirExists ?? defaultDirExists;
15258
15387
  const statReader = opts.statReader ?? defaultStatReader;
15259
15388
  const log6 = makePluginLogger(PLUGIN_NAME8);
15260
- const specsRoot = join15(rootDir, SPECS_REL_DIR);
15389
+ const specsRoot = join16(rootDir, SPECS_REL_DIR);
15261
15390
  const records = [];
15262
15391
  if (!dirExists(specsRoot)) {
15263
15392
  log6.info(`specs 目录不存在,plugin 将 no-op`, { specsRoot });
@@ -15278,7 +15407,7 @@ function loadSpecs(rootDir, opts = {}) {
15278
15407
  log6.info(`跳过非合法 slug 命名的条目`, { entry });
15279
15408
  continue;
15280
15409
  }
15281
- const specDir = join15(specsRoot, entry);
15410
+ const specDir = join16(specsRoot, entry);
15282
15411
  let dirStat;
15283
15412
  try {
15284
15413
  dirStat = statReader(specDir);
@@ -15291,7 +15420,7 @@ function loadSpecs(rootDir, opts = {}) {
15291
15420
  }
15292
15421
  if (!dirStat.isDirectory)
15293
15422
  continue;
15294
- const handoffPath = join15(specDir, "handoff.yaml");
15423
+ const handoffPath = join16(specDir, "handoff.yaml");
15295
15424
  let fileStat;
15296
15425
  try {
15297
15426
  fileStat = statReader(handoffPath);
@@ -15463,14 +15592,14 @@ var discoverSpecSuggestServer = async (ctx) => {
15463
15592
  var handler8 = discoverSpecSuggestServer;
15464
15593
 
15465
15594
  // lib/memories.ts
15466
- import { promises as fs15 } from "node:fs";
15467
- import * as path19 from "node:path";
15595
+ import { promises as fs16 } from "node:fs";
15596
+ import * as path20 from "node:path";
15468
15597
  import * as os5 from "node:os";
15469
15598
  function resolveConfig(c) {
15470
15599
  return {
15471
15600
  projectRoot: c.projectRoot,
15472
15601
  homeDir: c.homeDir ?? os5.homedir(),
15473
- projectName: c.projectName ?? path19.basename(c.projectRoot),
15602
+ projectName: c.projectName ?? path20.basename(c.projectRoot),
15474
15603
  now: c.now ?? Date.now,
15475
15604
  log: c.log ?? (() => {}),
15476
15605
  maxPerScope: c.maxPerScope ?? 1000
@@ -15478,13 +15607,13 @@ function resolveConfig(c) {
15478
15607
  }
15479
15608
  function fileFor(scope, cfg) {
15480
15609
  if (scope === "project") {
15481
- return path19.join(cfg.projectRoot, ".codeforge", "memories.json");
15610
+ return path20.join(cfg.projectRoot, ".codeforge", "memories.json");
15482
15611
  }
15483
- return path19.join(cfg.homeDir, ".codeforge", "memories.json");
15612
+ return path20.join(cfg.homeDir, ".codeforge", "memories.json");
15484
15613
  }
15485
15614
  async function readBank(p) {
15486
15615
  try {
15487
- const raw = await fs15.readFile(p, "utf8");
15616
+ const raw = await fs16.readFile(p, "utf8");
15488
15617
  const arr = JSON.parse(raw);
15489
15618
  if (!Array.isArray(arr))
15490
15619
  return [];
@@ -15494,10 +15623,10 @@ async function readBank(p) {
15494
15623
  }
15495
15624
  }
15496
15625
  async function writeBank(p, items) {
15497
- await fs15.mkdir(path19.dirname(p), { recursive: true });
15626
+ await fs16.mkdir(path20.dirname(p), { recursive: true });
15498
15627
  const tmp = `${p}.tmp`;
15499
- await fs15.writeFile(tmp, JSON.stringify(items, null, 2), "utf8");
15500
- await fs15.rename(tmp, p);
15628
+ await fs16.writeFile(tmp, JSON.stringify(items, null, 2), "utf8");
15629
+ await fs16.rename(tmp, p);
15501
15630
  }
15502
15631
  function isMemory(x) {
15503
15632
  if (!x || typeof x !== "object")
@@ -16015,7 +16144,7 @@ var handler10 = modelFallbackServer;
16015
16144
 
16016
16145
  // plugins/subtask-heartbeat.ts
16017
16146
  import { promises as fsPromises } from "node:fs";
16018
- import * as path20 from "node:path";
16147
+ import * as path21 from "node:path";
16019
16148
  var recordSessionParent2 = recordSessionParent;
16020
16149
  var lookupParentSessionId2 = lookupParentSessionId;
16021
16150
  var deleteSessionParent2 = deleteSessionParent;
@@ -16362,7 +16491,7 @@ function buildErrorLogLine(errorReason, elapsedMs, now = Date.now()) {
16362
16491
  }
16363
16492
  async function appendSubagentLog(filePath, line, log7) {
16364
16493
  try {
16365
- await fsPromises.mkdir(path20.dirname(filePath), { recursive: true });
16494
+ await fsPromises.mkdir(path21.dirname(filePath), { recursive: true });
16366
16495
  await fsPromises.appendFile(filePath, line + `
16367
16496
  `, "utf8");
16368
16497
  } catch (err) {
@@ -16730,7 +16859,7 @@ var handler12 = parallelStatusServer;
16730
16859
 
16731
16860
  // plugins/parallel-tool-nudge.ts
16732
16861
  import { readFileSync as readFileSync4, readdirSync as readdirSync2, statSync as statSync3 } from "node:fs";
16733
- import { join as join17 } from "node:path";
16862
+ import { join as join18 } from "node:path";
16734
16863
  import { homedir as homedir6 } from "node:os";
16735
16864
  var PLUGIN_NAME13 = "parallel-tool-nudge";
16736
16865
  logLifecycle(PLUGIN_NAME13, "import", {});
@@ -16783,10 +16912,10 @@ function loadAgentToolsMap(rootDir, opts = {}) {
16783
16912
  const reader = opts.reader ?? defaultReader2;
16784
16913
  const dirReader = opts.dirReader ?? defaultDirReader2;
16785
16914
  const dirExists = opts.dirExists ?? defaultDirExists2;
16786
- const homeAgentsDir = opts.homeAgentsDir ?? join17(homedir6(), ".config", "opencode", "agents");
16915
+ const homeAgentsDir = opts.homeAgentsDir ?? join18(homedir6(), ".config", "opencode", "agents");
16787
16916
  const candidateDirs = [
16788
- join17(rootDir, ".codeforge", "agents"),
16789
- join17(rootDir, "agents"),
16917
+ join18(rootDir, ".codeforge", "agents"),
16918
+ join18(rootDir, "agents"),
16790
16919
  homeAgentsDir
16791
16920
  ];
16792
16921
  const result = new Map;
@@ -16809,20 +16938,20 @@ function loadAgentToolsMap(rootDir, opts = {}) {
16809
16938
  for (const entry of entries) {
16810
16939
  if (!entry.endsWith(".md"))
16811
16940
  continue;
16812
- const path21 = join17(dir, entry);
16941
+ const path22 = join18(dir, entry);
16813
16942
  let content;
16814
16943
  try {
16815
- content = reader(path21);
16944
+ content = reader(path22);
16816
16945
  } catch (err) {
16817
16946
  log8.warn(`agent.md 读取失败(已跳过)`, {
16818
- path: path21,
16947
+ path: path22,
16819
16948
  error: err instanceof Error ? err.message : String(err)
16820
16949
  });
16821
16950
  continue;
16822
16951
  }
16823
16952
  const parsed = parseAgentFrontmatter(content);
16824
16953
  if (!parsed) {
16825
- log8.warn(`agent frontmatter 解析失败(已跳过)`, { path: path21 });
16954
+ log8.warn(`agent frontmatter 解析失败(已跳过)`, { path: path22 });
16826
16955
  continue;
16827
16956
  }
16828
16957
  if (result.has(parsed.name))
@@ -17013,18 +17142,18 @@ var handler14 = async (_ctx3) => {
17013
17142
  };
17014
17143
 
17015
17144
  // lib/event-stream.ts
17016
- import { promises as fs16 } from "node:fs";
17017
- import * as path21 from "node:path";
17145
+ import { promises as fs17 } from "node:fs";
17146
+ import * as path22 from "node:path";
17018
17147
  async function loadSession(id, opts = {}) {
17019
17148
  const file = resolveSessionFile(id, opts);
17020
- const raw = await fs16.readFile(file, "utf8");
17149
+ const raw = await fs17.readFile(file, "utf8");
17021
17150
  return parseJsonl(id, raw);
17022
17151
  }
17023
17152
  async function listSessions(opts = {}) {
17024
17153
  const dir = resolveDir(opts);
17025
17154
  let entries;
17026
17155
  try {
17027
- entries = await fs16.readdir(dir, { withFileTypes: true });
17156
+ entries = await fs17.readdir(dir, { withFileTypes: true });
17028
17157
  } catch (err) {
17029
17158
  if (err.code === "ENOENT")
17030
17159
  return [];
@@ -17034,10 +17163,10 @@ async function listSessions(opts = {}) {
17034
17163
  for (const e of entries) {
17035
17164
  if (!e.isFile() || !e.name.endsWith(".jsonl"))
17036
17165
  continue;
17037
- const file = path21.join(dir, e.name);
17166
+ const file = path22.join(dir, e.name);
17038
17167
  const id = e.name.replace(/\.jsonl$/, "");
17039
17168
  try {
17040
- const stat = await fs16.stat(file);
17169
+ const stat = await fs17.stat(file);
17041
17170
  const headerLine = await readFirstLine(file);
17042
17171
  let started_at = stat.birthtimeMs;
17043
17172
  if (headerLine) {
@@ -17061,11 +17190,11 @@ async function listSessions(opts = {}) {
17061
17190
  return out;
17062
17191
  }
17063
17192
  function resolveDir(opts = {}) {
17064
- const root = path21.resolve(opts.root ?? process.cwd());
17065
- return opts.sessions_dir ? path21.resolve(root, opts.sessions_dir) : path21.join(runtimeDir(root), "sessions");
17193
+ const root = path22.resolve(opts.root ?? process.cwd());
17194
+ return opts.sessions_dir ? path22.resolve(root, opts.sessions_dir) : path22.join(runtimeDir(root), "sessions");
17066
17195
  }
17067
17196
  function resolveSessionFile(id, opts = {}) {
17068
- return path21.join(resolveDir(opts), `${id}.jsonl`);
17197
+ return path22.join(resolveDir(opts), `${id}.jsonl`);
17069
17198
  }
17070
17199
  function parseJsonl(id, raw) {
17071
17200
  const events = [];
@@ -17100,7 +17229,7 @@ function isEvent(obj) {
17100
17229
  }
17101
17230
  async function readFirstLine(file) {
17102
17231
  const buf = Buffer.alloc(4096);
17103
- const fh = await fs16.open(file, "r");
17232
+ const fh = await fs17.open(file, "r");
17104
17233
  try {
17105
17234
  const { bytesRead } = await fh.read(buf, 0, buf.length, 0);
17106
17235
  const s = buf.subarray(0, bytesRead).toString("utf8");
@@ -17328,11 +17457,11 @@ function isRecoveryWorthShowing(plan) {
17328
17457
  }
17329
17458
 
17330
17459
  // lib/block-pending.ts
17331
- import { promises as fs17 } from "node:fs";
17332
- import * as path22 from "node:path";
17460
+ import { promises as fs18 } from "node:fs";
17461
+ import * as path23 from "node:path";
17333
17462
  function blockPendingFilePath(absRoot) {
17334
17463
  const rd = runtimeDir(absRoot, { ensure: false });
17335
- return path22.join(rd, "sessions", "autonomous-blocks.ndjson");
17464
+ return path23.join(rd, "sessions", "autonomous-blocks.ndjson");
17336
17465
  }
17337
17466
  function consumeLockPath(absRoot) {
17338
17467
  return blockPendingFilePath(absRoot) + ".consume.lock";
@@ -17341,7 +17470,7 @@ async function scanBlockPending(absRoot, filterSessionId) {
17341
17470
  const file = blockPendingFilePath(absRoot);
17342
17471
  let raw;
17343
17472
  try {
17344
- raw = await fs17.readFile(file, "utf8");
17473
+ raw = await fs18.readFile(file, "utf8");
17345
17474
  } catch {
17346
17475
  return [];
17347
17476
  }
@@ -17397,7 +17526,7 @@ async function markBlocksConsumed(absRoot, entries) {
17397
17526
  if (entries.length === 0)
17398
17527
  return;
17399
17528
  const file = blockPendingFilePath(absRoot);
17400
- await fs17.mkdir(path22.dirname(file), { recursive: true });
17529
+ await fs18.mkdir(path23.dirname(file), { recursive: true });
17401
17530
  const now = new Date().toISOString();
17402
17531
  const lines = entries.map((e) => ({
17403
17532
  type: "consume",
@@ -17408,7 +17537,7 @@ async function markBlocksConsumed(absRoot, entries) {
17408
17537
  `) + `
17409
17538
  `;
17410
17539
  await withFileLock(consumeLockPath(absRoot), async () => {
17411
- await fs17.appendFile(file, lines, "utf8");
17540
+ await fs18.appendFile(file, lines, "utf8");
17412
17541
  });
17413
17542
  }
17414
17543
 
@@ -17546,8 +17675,8 @@ var sessionRecoveryServer = async (ctx) => {
17546
17675
  var handler15 = sessionRecoveryServer;
17547
17676
 
17548
17677
  // plugins/subtasks.ts
17549
- import { promises as fs18 } from "node:fs";
17550
- import * as path23 from "node:path";
17678
+ import { promises as fs19 } from "node:fs";
17679
+ import * as path24 from "node:path";
17551
17680
 
17552
17681
  // lib/parallel-merge.ts
17553
17682
  init_worktree_ops();
@@ -18371,7 +18500,7 @@ function sleep2(ms) {
18371
18500
  // plugins/subtasks.ts
18372
18501
  var PLUGIN_NAME16 = "subtasks";
18373
18502
  function getLogFile(root = process.cwd()) {
18374
- return path23.join(runtimeDir(root), "logs", "subtasks.log");
18503
+ return path24.join(runtimeDir(root), "logs", "subtasks.log");
18375
18504
  }
18376
18505
  var VERB_RE = /^([a-zA-Z]{3,12})/;
18377
18506
  var CN_VERBS = [
@@ -18676,8 +18805,8 @@ async function writeLog(level, msg, data) {
18676
18805
  `;
18677
18806
  try {
18678
18807
  const logFile = getLogFile();
18679
- await fs18.mkdir(path23.dirname(logFile), { recursive: true });
18680
- await fs18.appendFile(logFile, line, "utf8");
18808
+ await fs19.mkdir(path24.dirname(logFile), { recursive: true });
18809
+ await fs19.appendFile(logFile, line, "utf8");
18681
18810
  } catch {}
18682
18811
  }
18683
18812
  logLifecycle(PLUGIN_NAME16, "import");
@@ -19308,8 +19437,8 @@ var tokenManagerServer = async (ctx) => {
19308
19437
  var handler18 = tokenManagerServer;
19309
19438
 
19310
19439
  // plugins/tool-policy.ts
19311
- import { promises as fs19 } from "node:fs";
19312
- import * as path25 from "node:path";
19440
+ import { promises as fs20 } from "node:fs";
19441
+ import * as path26 from "node:path";
19313
19442
 
19314
19443
  // lib/tool-risk.ts
19315
19444
  var RISK_PATTERNS = [
@@ -19463,7 +19592,7 @@ function buildHaystackFor(args, matchOn) {
19463
19592
  }
19464
19593
 
19465
19594
  // lib/file-regex-acl.ts
19466
- import * as path24 from "node:path";
19595
+ import * as path25 from "node:path";
19467
19596
  function compileRule(r) {
19468
19597
  if (r instanceof RegExp)
19469
19598
  return r;
@@ -19529,7 +19658,7 @@ function normalizePath2(p) {
19529
19658
  let s = p.replace(/\\/g, "/");
19530
19659
  if (s.startsWith("./"))
19531
19660
  s = s.slice(2);
19532
- s = path24.posix.normalize(s);
19661
+ s = path25.posix.normalize(s);
19533
19662
  return s;
19534
19663
  }
19535
19664
  function checkFileAccess(acl, file, op) {
@@ -19632,11 +19761,11 @@ function decideToolCall(ctx, cfg = {}, currentAgent) {
19632
19761
  const action = risks.length > 0 || worstAcl === "deny" ? "deny" : "allow";
19633
19762
  return { action, reasons, risks, acl: aclResults };
19634
19763
  }
19635
- var POLICY_PATH = path25.join(".codeforge", "policy.json");
19764
+ var POLICY_PATH = path26.join(".codeforge", "policy.json");
19636
19765
  async function loadPolicy(root = process.cwd()) {
19637
- const file = path25.join(root, POLICY_PATH);
19766
+ const file = path26.join(root, POLICY_PATH);
19638
19767
  try {
19639
- const raw = await fs19.readFile(file, "utf8");
19768
+ const raw = await fs20.readFile(file, "utf8");
19640
19769
  const data = JSON.parse(raw);
19641
19770
  return data;
19642
19771
  } catch {
@@ -19734,7 +19863,7 @@ var handler19 = toolPolicyServer;
19734
19863
  // plugins/update-checker.ts
19735
19864
  import { existsSync as existsSync6, rmSync } from "node:fs";
19736
19865
  import { homedir as homedir8 } from "node:os";
19737
- import { join as join23 } from "node:path";
19866
+ import { join as join24 } from "node:path";
19738
19867
  import { spawnSync as spawnSync2 } from "node:child_process";
19739
19868
 
19740
19869
  // lib/update-checker-impl.ts
@@ -19752,7 +19881,7 @@ import {
19752
19881
  writeFileSync as writeFileSync2
19753
19882
  } from "node:fs";
19754
19883
  import { homedir as homedir7, tmpdir } from "node:os";
19755
- import { dirname as dirname14, join as join22 } from "node:path";
19884
+ import { dirname as dirname14, join as join23 } from "node:path";
19756
19885
  import { fileURLToPath as fileURLToPath2 } from "node:url";
19757
19886
  import * as https from "node:https";
19758
19887
  import * as zlib from "node:zlib";
@@ -19760,7 +19889,7 @@ import * as zlib from "node:zlib";
19760
19889
  // lib/version-injected.ts
19761
19890
  function getInjectedVersion() {
19762
19891
  try {
19763
- const v = "0.6.4";
19892
+ const v = "0.6.6";
19764
19893
  if (typeof v === "string" && /^\d+\.\d+\.\d+/.test(v)) {
19765
19894
  return v;
19766
19895
  }
@@ -19850,17 +19979,17 @@ function readLocalVersion() {
19850
19979
  try {
19851
19980
  const here = fileURLToPath2(import.meta.url);
19852
19981
  const root = dirname14(dirname14(here));
19853
- const pkg = JSON.parse(readFileSync5(join22(root, "package.json"), "utf8"));
19982
+ const pkg = JSON.parse(readFileSync5(join23(root, "package.json"), "utf8"));
19854
19983
  return typeof pkg.version === "string" ? pkg.version : "0.0.0";
19855
19984
  } catch {
19856
19985
  return "0.0.0";
19857
19986
  }
19858
19987
  }
19859
19988
  function defaultCacheDir() {
19860
- return process.env["CODEFORGE_CACHE_DIR"] ?? join22(homedir7(), ".cache", "codeforge");
19989
+ return process.env["CODEFORGE_CACHE_DIR"] ?? join23(homedir7(), ".cache", "codeforge");
19861
19990
  }
19862
19991
  function defaultCacheFile() {
19863
- return join22(defaultCacheDir(), "update-check.json");
19992
+ return join23(defaultCacheDir(), "update-check.json");
19864
19993
  }
19865
19994
  function readCache(file) {
19866
19995
  try {
@@ -20016,14 +20145,14 @@ function defaultHttpFetcher(url2, timeoutMs) {
20016
20145
  });
20017
20146
  }
20018
20147
  async function downloadAndExtractBundle(opts) {
20019
- const tmpRoot = opts.tmpDir ?? mkdtempSync(join22(tmpdir(), "codeforge-update-"));
20148
+ const tmpRoot = opts.tmpDir ?? mkdtempSync(join23(tmpdir(), "codeforge-update-"));
20020
20149
  mkdirSync3(tmpRoot, { recursive: true });
20021
20150
  const fetcher = opts.tarballFetcher ?? defaultBinaryFetcher;
20022
20151
  const tarballBuf = await fetcher(opts.tarballUrl);
20023
20152
  verifyIntegrity(tarballBuf, opts.expectedIntegrity);
20024
20153
  const tarBuf = zlib.gunzipSync(tarballBuf);
20025
20154
  extractTarToDir(tarBuf, tmpRoot);
20026
- const bundlePath = join22(tmpRoot, "package", "dist", "index.js");
20155
+ const bundlePath = join23(tmpRoot, "package", "dist", "index.js");
20027
20156
  if (!existsSync5(bundlePath)) {
20028
20157
  throw new Error(`bundle_not_found: ${bundlePath}`);
20029
20158
  }
@@ -20063,11 +20192,11 @@ function extractTarToDir(tarBuf, destRoot) {
20063
20192
  offset += 512;
20064
20193
  if (typeFlag === "0" || typeFlag === "" || typeFlag === "\x00") {
20065
20194
  const fileBuf = tarBuf.subarray(offset, offset + size);
20066
- const dest = join22(destRoot, fullName);
20195
+ const dest = join23(destRoot, fullName);
20067
20196
  mkdirSync3(dirname14(dest), { recursive: true });
20068
20197
  writeFileSync2(dest, fileBuf);
20069
20198
  } else if (typeFlag === "5") {
20070
- mkdirSync3(join22(destRoot, fullName), { recursive: true });
20199
+ mkdirSync3(join23(destRoot, fullName), { recursive: true });
20071
20200
  }
20072
20201
  offset += Math.ceil(size / 512) * 512;
20073
20202
  }
@@ -20176,7 +20305,7 @@ function cleanupOldBackups(target, keep) {
20176
20305
  const base = target.substring(dir.length + 1);
20177
20306
  const prefix = `${base}.bak.`;
20178
20307
  const all = readdirSync3(dir).filter((f) => f.startsWith(prefix)).map((f) => {
20179
- const full = join22(dir, f);
20308
+ const full = join23(dir, f);
20180
20309
  let mtimeMs = 0;
20181
20310
  try {
20182
20311
  mtimeMs = statSync4(full).mtimeMs;
@@ -20198,7 +20327,7 @@ function loadCompatibility(opts) {
20198
20327
  const root = opts?.cwd ?? inferPluginRoot();
20199
20328
  if (!root)
20200
20329
  return null;
20201
- file = join22(root, "compatibility.json");
20330
+ file = join23(root, "compatibility.json");
20202
20331
  }
20203
20332
  if (!existsSync5(file))
20204
20333
  return null;
@@ -20389,10 +20518,11 @@ var updateCheckerServer = async (ctx) => {
20389
20518
  expectedIntegrity: npmResult.integrity
20390
20519
  });
20391
20520
  try {
20392
- const installMjs = join23(extractDir, "package", "install.mjs");
20521
+ const installMjs = join24(extractDir, "package", "install.mjs");
20393
20522
  if (existsSync6(installMjs)) {
20394
- const r = spawnSync2(process.execPath, [installMjs, "--global", "--skip-build"], {
20395
- cwd: join23(extractDir, "package"),
20523
+ const nodeBin = resolveNodeBin();
20524
+ const r = spawnSync2(nodeBin, [installMjs, "--global", "--skip-build"], {
20525
+ cwd: join24(extractDir, "package"),
20396
20526
  stdio: "pipe",
20397
20527
  encoding: "utf8"
20398
20528
  });
@@ -20411,6 +20541,7 @@ var updateCheckerServer = async (ctx) => {
20411
20541
  safeWriteLog(PLUGIN_NAME20, {
20412
20542
  level: "warn",
20413
20543
  msg: "install_mjs_failed",
20544
+ nodeBin,
20414
20545
  status: r.status,
20415
20546
  stderr: r.stderr?.slice(0, 500),
20416
20547
  stdout: r.stdout?.slice(0, 500)
@@ -20449,6 +20580,41 @@ var updateCheckerServer = async (ctx) => {
20449
20580
  });
20450
20581
  return {};
20451
20582
  };
20583
+ function resolveNodeBin() {
20584
+ const IS_WIN = process.platform === "win32";
20585
+ try {
20586
+ const r = spawnSync2(IS_WIN ? "where" : "which", ["node"], {
20587
+ encoding: "utf8",
20588
+ stdio: "pipe"
20589
+ });
20590
+ if (r.status === 0 && r.stdout.trim()) {
20591
+ const first = r.stdout.trim().split(/\r?\n/)[0].trim();
20592
+ if (first)
20593
+ return first;
20594
+ }
20595
+ } catch {}
20596
+ const candidates = IS_WIN ? [
20597
+ "C:\\Program Files\\nodejs\\node.exe",
20598
+ "C:\\Program Files (x86)\\nodejs\\node.exe"
20599
+ ] : [
20600
+ "/usr/local/bin/node",
20601
+ "/usr/bin/node",
20602
+ "/opt/homebrew/bin/node",
20603
+ "/opt/homebrew/opt/node/bin/node"
20604
+ ];
20605
+ for (const c of candidates) {
20606
+ try {
20607
+ const t = spawnSync2(c, ["--version"], {
20608
+ encoding: "utf8",
20609
+ stdio: "pipe",
20610
+ timeout: 2000
20611
+ });
20612
+ if (t.status === 0)
20613
+ return c;
20614
+ } catch {}
20615
+ }
20616
+ return process.execPath;
20617
+ }
20452
20618
  function detectOpencodeVersion() {
20453
20619
  const env = process.env["OPENCODE_VERSION"];
20454
20620
  if (env && env.trim().length > 0)
@@ -20457,14 +20623,14 @@ function detectOpencodeVersion() {
20457
20623
  }
20458
20624
  function getOpencodeBundlePath() {
20459
20625
  const candidates = [];
20460
- candidates.push(join23(homedir8(), ".config", "opencode", "codeforge", "index.js"));
20626
+ candidates.push(join24(homedir8(), ".config", "opencode", "codeforge", "index.js"));
20461
20627
  if (process.platform === "win32") {
20462
20628
  const appData = process.env["APPDATA"];
20463
20629
  if (appData)
20464
- candidates.push(join23(appData, "opencode", "codeforge", "index.js"));
20630
+ candidates.push(join24(appData, "opencode", "codeforge", "index.js"));
20465
20631
  const localAppData = process.env["LOCALAPPDATA"];
20466
20632
  if (localAppData)
20467
- candidates.push(join23(localAppData, "opencode", "codeforge", "index.js"));
20633
+ candidates.push(join24(localAppData, "opencode", "codeforge", "index.js"));
20468
20634
  }
20469
20635
  for (const c of candidates) {
20470
20636
  if (existsSync6(c))
@@ -20525,11 +20691,11 @@ async function postToast(ctx, message) {
20525
20691
  var handler20 = updateCheckerServer;
20526
20692
 
20527
20693
  // plugins/workflow-engine.ts
20528
- import * as path27 from "node:path";
20694
+ import * as path28 from "node:path";
20529
20695
 
20530
20696
  // lib/workflow-loader.ts
20531
- import { promises as fs20 } from "node:fs";
20532
- import * as path26 from "node:path";
20697
+ import { promises as fs21 } from "node:fs";
20698
+ import * as path27 from "node:path";
20533
20699
  import { z as z18 } from "zod";
20534
20700
  var ActionSchema = z18.object({
20535
20701
  tool: z18.string().min(1, "action.tool 不能为空"),
@@ -20615,7 +20781,7 @@ function parseWorkflowYaml(yaml, sourcePath = "<inline>") {
20615
20781
  async function loadWorkflowFromFile(filePath) {
20616
20782
  let txt;
20617
20783
  try {
20618
- txt = await fs20.readFile(filePath, "utf8");
20784
+ txt = await fs21.readFile(filePath, "utf8");
20619
20785
  } catch (err) {
20620
20786
  return {
20621
20787
  ok: false,
@@ -20630,7 +20796,7 @@ async function loadWorkflowsFromDir(dir) {
20630
20796
  const failed = [];
20631
20797
  let entries;
20632
20798
  try {
20633
- entries = await fs20.readdir(dir);
20799
+ entries = await fs21.readdir(dir);
20634
20800
  } catch (err) {
20635
20801
  const e = err;
20636
20802
  if (e.code === "ENOENT")
@@ -20642,7 +20808,7 @@ async function loadWorkflowsFromDir(dir) {
20642
20808
  continue;
20643
20809
  if (!/\.ya?ml$/i.test(name))
20644
20810
  continue;
20645
- const full = path26.join(dir, name);
20811
+ const full = path27.join(dir, name);
20646
20812
  const r = await loadWorkflowFromFile(full);
20647
20813
  if (r.ok)
20648
20814
  loaded.push(r);
@@ -21032,7 +21198,7 @@ async function handleCommandInvoked(raw, workflowsDir = "workflows") {
21032
21198
  }
21033
21199
  var workflowEngineServer = async (ctx) => {
21034
21200
  const directory = ctx.directory ?? process.cwd();
21035
- const workflowsDir = path27.join(directory, "workflows");
21201
+ const workflowsDir = path28.join(directory, "workflows");
21036
21202
  ensureRegistry(workflowsDir).catch((err) => fallbackLog2.warn(`[${PLUGIN_NAME21}] preload workflows failed`, {
21037
21203
  error: err instanceof Error ? err.message : String(err)
21038
21204
  }));
@@ -21076,7 +21242,8 @@ var workflowEngineServer = async (ctx) => {
21076
21242
  var handler21 = workflowEngineServer;
21077
21243
 
21078
21244
  // plugins/session-worktree-guard.ts
21079
- import path28 from "node:path";
21245
+ import path29 from "node:path";
21246
+ import { stat } from "node:fs/promises";
21080
21247
  var PLUGIN_NAME22 = "session-worktree-guard";
21081
21248
  logLifecycle(PLUGIN_NAME22, "import", {});
21082
21249
  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/;
@@ -21188,23 +21355,23 @@ var MERGE_CALLER_WHITELIST = new Set([
21188
21355
  var FORCE_MERGE_CALLER_WHITELIST = new Set([
21189
21356
  "codeforge"
21190
21357
  ]);
21191
- var CODEFORGE_WORKTREE_DIR_NAME = path28.join(".git", "codeforge-worktrees");
21358
+ var CODEFORGE_WORKTREE_DIR_NAME = path29.join(".git", "codeforge-worktrees");
21192
21359
  function worktreesRoot(mainRoot) {
21193
- return path28.join(mainRoot, CODEFORGE_WORKTREE_DIR_NAME);
21360
+ return path29.join(mainRoot, CODEFORGE_WORKTREE_DIR_NAME);
21194
21361
  }
21195
21362
  function isInsideAnyWorktreeDir(absPath, mainRoot) {
21196
- if (!path28.isAbsolute(absPath))
21363
+ if (!path29.isAbsolute(absPath))
21197
21364
  return false;
21198
21365
  const root = worktreesRoot(mainRoot);
21199
21366
  if (absPath === root)
21200
21367
  return false;
21201
- const prefix = root.endsWith(path28.sep) ? root : root + path28.sep;
21368
+ const prefix = root.endsWith(path29.sep) ? root : root + path29.sep;
21202
21369
  return absPath.startsWith(prefix);
21203
21370
  }
21204
21371
  function rewritePath(value, mainRoot, worktreeRoot) {
21205
21372
  if (!value)
21206
21373
  return null;
21207
- const resolved = path28.isAbsolute(value) ? value : path28.resolve(mainRoot, value);
21374
+ const resolved = path29.isAbsolute(value) ? value : path29.resolve(mainRoot, value);
21208
21375
  const wtPrefix2 = worktreeRoot.endsWith("/") ? worktreeRoot : worktreeRoot + "/";
21209
21376
  if (resolved === worktreeRoot || resolved.startsWith(wtPrefix2)) {
21210
21377
  return null;
@@ -21242,7 +21409,7 @@ function commandContainsMainRootExcludingWorktree(command, mainRoot, worktreePat
21242
21409
  }
21243
21410
  }
21244
21411
  const wtRoot = worktreesRoot(mainRoot);
21245
- const wtRootPrefix = wtRoot + path28.sep;
21412
+ const wtRootPrefix = wtRoot + path29.sep;
21246
21413
  const escapedWtRootPrefix = escapeRegex(wtRootPrefix);
21247
21414
  const wtPathPattern = escapedWtRootPrefix + `[^\\s'"\\x60)]*`;
21248
21415
  const allWorktreePathsReForEscape = new RegExp(wtPathPattern, "g");
@@ -21297,12 +21464,24 @@ function collectWritePaths(toolName, argsObj, worktreeRoot) {
21297
21464
  const candidate = toolName === "write" || toolName === "edit" ? argsObj["filePath"] : toolName === "ast_edit" ? argsObj["target"] : undefined;
21298
21465
  if (typeof candidate !== "string" || candidate.length === 0)
21299
21466
  return out;
21300
- const abs = path28.isAbsolute(candidate) ? candidate : path28.resolve(worktreeRoot, candidate);
21301
- const rel = path28.relative(worktreeRoot, abs).split(path28.sep).join("/");
21467
+ const abs = path29.isAbsolute(candidate) ? candidate : path29.resolve(worktreeRoot, candidate);
21468
+ const rel = path29.relative(worktreeRoot, abs).split(path29.sep).join("/");
21302
21469
  out.push(rel);
21303
21470
  return out;
21304
21471
  }
21305
21472
  var log13 = makePluginLogger(PLUGIN_NAME22);
21473
+ async function isCodeforgeManagedProject(mainRoot, opts = {}) {
21474
+ const env = opts.env ?? process.env;
21475
+ const force = env["CODEFORGE_FORCE_WORKTREE_GUARD"];
21476
+ if (force === "1" || force === "true" || force === "yes")
21477
+ return true;
21478
+ try {
21479
+ const st = await stat(path29.join(mainRoot, ".codeforge"));
21480
+ return st.isDirectory();
21481
+ } catch {
21482
+ return false;
21483
+ }
21484
+ }
21306
21485
  function resolveMainRoot2(rawDir) {
21307
21486
  const worktreeMarker = "/.git/codeforge-worktrees/";
21308
21487
  const idx = rawDir.indexOf(worktreeMarker);
@@ -21325,6 +21504,17 @@ var sessionWorktreeGuardPlugin = async (ctx) => {
21325
21504
  return {};
21326
21505
  }
21327
21506
  const mainRoot = resolveMainRoot2(ctx.directory ?? process.cwd());
21507
+ if (!await isCodeforgeManagedProject(mainRoot)) {
21508
+ log13.info("[guard] 当前项目无 .codeforge/ 目录,session-worktree-guard 不启用;" + "如需在此项目启用 worktree 隔离:mkdir .codeforge", { mainRoot });
21509
+ safeWriteLog(PLUGIN_NAME22, {
21510
+ hook: "activate",
21511
+ action: "skip",
21512
+ source: "non-codeforge-project",
21513
+ mainRoot
21514
+ });
21515
+ logLifecycle(PLUGIN_NAME22, "activate", { skipped: "non-codeforge-project", mainRoot });
21516
+ return {};
21517
+ }
21328
21518
  let policyCfg = {};
21329
21519
  try {
21330
21520
  policyCfg = await loadPolicy(mainRoot);
@@ -21463,8 +21653,31 @@ var sessionWorktreeGuardPlugin = async (ctx) => {
21463
21653
  if (toolName === "session_merge") {
21464
21654
  const action = argsObj["action"];
21465
21655
  if (action === "merge") {
21466
- const caller = input.agent;
21467
- if (caller !== undefined && !MERGE_CALLER_WHITELIST.has(caller)) {
21656
+ const caller = await resolveAgentForGuard({ sessionID: input.sessionID, agent: input.agent }, ctx.client, log13);
21657
+ const wantsForce = argsObj["force"] === true;
21658
+ if (caller === null && wantsForce) {
21659
+ const reason = `[session-worktree-guard] DENIED: session_merge action=merge force=true 且 agent 解析失败 (null);` + `force 旁路无 approval backstop,fail-closed 拒绝 (ADR:subagent-force-merge-hard-deny)`;
21660
+ log13.warn(reason, {
21661
+ sessionId,
21662
+ tool: toolName,
21663
+ action,
21664
+ caller: null,
21665
+ force: true
21666
+ });
21667
+ safeWriteLog(PLUGIN_NAME22, {
21668
+ hook: "tool.execute.before",
21669
+ tool: toolName,
21670
+ sessionID: input.sessionID,
21671
+ action: "deny",
21672
+ source: "merge-caller-null-force-fail-closed",
21673
+ caller: null,
21674
+ merge_action: "merge",
21675
+ force: true
21676
+ });
21677
+ denied = new DeniedError(reason);
21678
+ return;
21679
+ }
21680
+ if (caller !== null && !MERGE_CALLER_WHITELIST.has(caller)) {
21468
21681
  const reason = `[session-worktree-guard] DENIED: session_merge action=merge 仅 codeforge orchestrator / discover / 用户可调;当前 caller=${caller}`;
21469
21682
  log13.warn(reason, {
21470
21683
  sessionId,
@@ -21484,8 +21697,7 @@ var sessionWorktreeGuardPlugin = async (ctx) => {
21484
21697
  denied = new DeniedError(reason);
21485
21698
  return;
21486
21699
  }
21487
- const wantsForce = argsObj["force"] === true;
21488
- if (wantsForce && caller !== undefined && !FORCE_MERGE_CALLER_WHITELIST.has(caller)) {
21700
+ if (wantsForce && caller !== null && !FORCE_MERGE_CALLER_WHITELIST.has(caller)) {
21489
21701
  const reason = `[session-worktree-guard] DENIED: caller=${caller} 禁止 force=true;` + `跳过 review 闭环是破坏性操作,仅用户经 codeforge orchestrator 显式 /merge --force 才允许 ` + `(ADR:discover-self-merge-permission)`;
21490
21702
  log13.warn(reason, {
21491
21703
  sessionId,
@@ -21712,12 +21924,12 @@ var sessionWorktreeGuardPlugin = async (ctx) => {
21712
21924
  var handler22 = sessionWorktreeGuardPlugin;
21713
21925
 
21714
21926
  // lib/opencode-session-probe.ts
21715
- import * as path29 from "node:path";
21927
+ import * as path30 from "node:path";
21716
21928
  import * as os6 from "node:os";
21717
21929
  import { createRequire as createRequire2 } from "node:module";
21718
21930
  var requireFromHere = createRequire2(import.meta.url);
21719
21931
  var DEFAULT_LIVENESS_MS = 6 * 60 * 60000;
21720
- var DEFAULT_DB_PATH = path29.join(os6.homedir(), ".local/share/opencode/opencode.db");
21932
+ var DEFAULT_DB_PATH = path30.join(os6.homedir(), ".local/share/opencode/opencode.db");
21721
21933
  function createSessionProbe(opts = {}) {
21722
21934
  const dbPath = opts.dbPath ?? DEFAULT_DB_PATH;
21723
21935
  const httpBaseUrl = opts.httpBaseUrl ?? process.env["OPENCODE_SERVER_URL"];
@@ -21821,7 +22033,8 @@ var _pruneTimer;
21821
22033
  var _probe = null;
21822
22034
  var log14 = makePluginLogger(PLUGIN_NAME23);
21823
22035
  var worktreeLifecyclePlugin = async (ctx) => {
21824
- const mainRoot = ctx.directory;
22036
+ const rawDir = ctx.directory ?? process.cwd();
22037
+ const mainRoot = resolveMainRoot2(rawDir);
21825
22038
  logLifecycle(PLUGIN_NAME23, "activate", {
21826
22039
  mainRoot: mainRoot ?? "(not set)",
21827
22040
  idle_threshold_ms: IDLE_TOAST_THROTTLE_MS
@@ -21829,6 +22042,17 @@ var worktreeLifecyclePlugin = async (ctx) => {
21829
22042
  if (!mainRoot) {
21830
22043
  return {};
21831
22044
  }
22045
+ if (!await isCodeforgeManagedProject(mainRoot)) {
22046
+ log14.info("[lifecycle] 当前项目无 .codeforge/ 目录,worktree-lifecycle 不启用", { mainRoot });
22047
+ safeWriteLog(PLUGIN_NAME23, {
22048
+ hook: "activate",
22049
+ action: "skip",
22050
+ source: "non-codeforge-project",
22051
+ mainRoot
22052
+ });
22053
+ logLifecycle(PLUGIN_NAME23, "activate", { skipped: "non-codeforge-project", mainRoot });
22054
+ return {};
22055
+ }
21832
22056
  const client = ctx.client;
21833
22057
  if (_probe) {
21834
22058
  try {