@andyqiu/codeforge 0.6.7 → 0.6.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
@@ -26,6 +26,8 @@ __export(exports_worktree_ops, {
26
26
  mergeCommit: () => mergeCommit,
27
27
  mergeAbort: () => mergeAbort,
28
28
  listWorktrees: () => listWorktrees,
29
+ isWorktreeAbsentError: () => isWorktreeAbsentError,
30
+ isBranchAbsentError: () => isBranchAbsentError,
29
31
  getMergeConflicts: () => getMergeConflicts,
30
32
  ensureWorktree: () => ensureWorktree,
31
33
  deleteBranchIfExists: () => deleteBranchIfExists,
@@ -91,6 +93,10 @@ async function deleteBranchIfExists(opts) {
91
93
  throw err;
92
94
  }
93
95
  }
96
+ function isBranchAbsentError(err) {
97
+ const msg = err instanceof Error ? err.message : String(err);
98
+ return /not found/i.test(msg);
99
+ }
94
100
  async function listWorktrees(opts) {
95
101
  const out = await runGit(opts.root, ["worktree", "list", "--porcelain"], opts.git_timeout_ms ?? 3000);
96
102
  return parseWorktreeList(out);
@@ -366,17 +372,17 @@ var require_visit = __commonJS((exports) => {
366
372
  visit.BREAK = BREAK;
367
373
  visit.SKIP = SKIP;
368
374
  visit.REMOVE = REMOVE;
369
- function visit_(key, node, visitor, path20) {
370
- const ctrl = callVisitor(key, node, visitor, path20);
375
+ function visit_(key, node, visitor, path21) {
376
+ const ctrl = callVisitor(key, node, visitor, path21);
371
377
  if (identity.isNode(ctrl) || identity.isPair(ctrl)) {
372
- replaceNode(key, path20, ctrl);
373
- return visit_(key, ctrl, visitor, path20);
378
+ replaceNode(key, path21, ctrl);
379
+ return visit_(key, ctrl, visitor, path21);
374
380
  }
375
381
  if (typeof ctrl !== "symbol") {
376
382
  if (identity.isCollection(node)) {
377
- path20 = Object.freeze(path20.concat(node));
383
+ path21 = Object.freeze(path21.concat(node));
378
384
  for (let i = 0;i < node.items.length; ++i) {
379
- const ci = visit_(i, node.items[i], visitor, path20);
385
+ const ci = visit_(i, node.items[i], visitor, path21);
380
386
  if (typeof ci === "number")
381
387
  i = ci - 1;
382
388
  else if (ci === BREAK)
@@ -387,13 +393,13 @@ var require_visit = __commonJS((exports) => {
387
393
  }
388
394
  }
389
395
  } else if (identity.isPair(node)) {
390
- path20 = Object.freeze(path20.concat(node));
391
- const ck = visit_("key", node.key, visitor, path20);
396
+ path21 = Object.freeze(path21.concat(node));
397
+ const ck = visit_("key", node.key, visitor, path21);
392
398
  if (ck === BREAK)
393
399
  return BREAK;
394
400
  else if (ck === REMOVE)
395
401
  node.key = null;
396
- const cv = visit_("value", node.value, visitor, path20);
402
+ const cv = visit_("value", node.value, visitor, path21);
397
403
  if (cv === BREAK)
398
404
  return BREAK;
399
405
  else if (cv === REMOVE)
@@ -414,17 +420,17 @@ var require_visit = __commonJS((exports) => {
414
420
  visitAsync.BREAK = BREAK;
415
421
  visitAsync.SKIP = SKIP;
416
422
  visitAsync.REMOVE = REMOVE;
417
- async function visitAsync_(key, node, visitor, path20) {
418
- const ctrl = await callVisitor(key, node, visitor, path20);
423
+ async function visitAsync_(key, node, visitor, path21) {
424
+ const ctrl = await callVisitor(key, node, visitor, path21);
419
425
  if (identity.isNode(ctrl) || identity.isPair(ctrl)) {
420
- replaceNode(key, path20, ctrl);
421
- return visitAsync_(key, ctrl, visitor, path20);
426
+ replaceNode(key, path21, ctrl);
427
+ return visitAsync_(key, ctrl, visitor, path21);
422
428
  }
423
429
  if (typeof ctrl !== "symbol") {
424
430
  if (identity.isCollection(node)) {
425
- path20 = Object.freeze(path20.concat(node));
431
+ path21 = Object.freeze(path21.concat(node));
426
432
  for (let i = 0;i < node.items.length; ++i) {
427
- const ci = await visitAsync_(i, node.items[i], visitor, path20);
433
+ const ci = await visitAsync_(i, node.items[i], visitor, path21);
428
434
  if (typeof ci === "number")
429
435
  i = ci - 1;
430
436
  else if (ci === BREAK)
@@ -435,13 +441,13 @@ var require_visit = __commonJS((exports) => {
435
441
  }
436
442
  }
437
443
  } else if (identity.isPair(node)) {
438
- path20 = Object.freeze(path20.concat(node));
439
- const ck = await visitAsync_("key", node.key, visitor, path20);
444
+ path21 = Object.freeze(path21.concat(node));
445
+ const ck = await visitAsync_("key", node.key, visitor, path21);
440
446
  if (ck === BREAK)
441
447
  return BREAK;
442
448
  else if (ck === REMOVE)
443
449
  node.key = null;
444
- const cv = await visitAsync_("value", node.value, visitor, path20);
450
+ const cv = await visitAsync_("value", node.value, visitor, path21);
445
451
  if (cv === BREAK)
446
452
  return BREAK;
447
453
  else if (cv === REMOVE)
@@ -468,23 +474,23 @@ var require_visit = __commonJS((exports) => {
468
474
  }
469
475
  return visitor;
470
476
  }
471
- function callVisitor(key, node, visitor, path20) {
477
+ function callVisitor(key, node, visitor, path21) {
472
478
  if (typeof visitor === "function")
473
- return visitor(key, node, path20);
479
+ return visitor(key, node, path21);
474
480
  if (identity.isMap(node))
475
- return visitor.Map?.(key, node, path20);
481
+ return visitor.Map?.(key, node, path21);
476
482
  if (identity.isSeq(node))
477
- return visitor.Seq?.(key, node, path20);
483
+ return visitor.Seq?.(key, node, path21);
478
484
  if (identity.isPair(node))
479
- return visitor.Pair?.(key, node, path20);
485
+ return visitor.Pair?.(key, node, path21);
480
486
  if (identity.isScalar(node))
481
- return visitor.Scalar?.(key, node, path20);
487
+ return visitor.Scalar?.(key, node, path21);
482
488
  if (identity.isAlias(node))
483
- return visitor.Alias?.(key, node, path20);
489
+ return visitor.Alias?.(key, node, path21);
484
490
  return;
485
491
  }
486
- function replaceNode(key, path20, node) {
487
- const parent = path20[path20.length - 1];
492
+ function replaceNode(key, path21, node) {
493
+ const parent = path21[path21.length - 1];
488
494
  if (identity.isCollection(parent)) {
489
495
  parent.items[key] = node;
490
496
  } else if (identity.isPair(parent)) {
@@ -1043,10 +1049,10 @@ var require_Collection = __commonJS((exports) => {
1043
1049
  var createNode = require_createNode();
1044
1050
  var identity = require_identity();
1045
1051
  var Node = require_Node();
1046
- function collectionFromPath(schema, path20, value) {
1052
+ function collectionFromPath(schema, path21, value) {
1047
1053
  let v = value;
1048
- for (let i = path20.length - 1;i >= 0; --i) {
1049
- const k = path20[i];
1054
+ for (let i = path21.length - 1;i >= 0; --i) {
1055
+ const k = path21[i];
1050
1056
  if (typeof k === "number" && Number.isInteger(k) && k >= 0) {
1051
1057
  const a = [];
1052
1058
  a[k] = v;
@@ -1065,7 +1071,7 @@ var require_Collection = __commonJS((exports) => {
1065
1071
  sourceObjects: new Map
1066
1072
  });
1067
1073
  }
1068
- var isEmptyPath = (path20) => path20 == null || typeof path20 === "object" && !!path20[Symbol.iterator]().next().done;
1074
+ var isEmptyPath = (path21) => path21 == null || typeof path21 === "object" && !!path21[Symbol.iterator]().next().done;
1069
1075
 
1070
1076
  class Collection extends Node.NodeBase {
1071
1077
  constructor(type, schema) {
@@ -1086,11 +1092,11 @@ var require_Collection = __commonJS((exports) => {
1086
1092
  copy.range = this.range.slice();
1087
1093
  return copy;
1088
1094
  }
1089
- addIn(path20, value) {
1090
- if (isEmptyPath(path20))
1095
+ addIn(path21, value) {
1096
+ if (isEmptyPath(path21))
1091
1097
  this.add(value);
1092
1098
  else {
1093
- const [key, ...rest] = path20;
1099
+ const [key, ...rest] = path21;
1094
1100
  const node = this.get(key, true);
1095
1101
  if (identity.isCollection(node))
1096
1102
  node.addIn(rest, value);
@@ -1100,8 +1106,8 @@ var require_Collection = __commonJS((exports) => {
1100
1106
  throw new Error(`Expected YAML collection at ${key}. Remaining path: ${rest}`);
1101
1107
  }
1102
1108
  }
1103
- deleteIn(path20) {
1104
- const [key, ...rest] = path20;
1109
+ deleteIn(path21) {
1110
+ const [key, ...rest] = path21;
1105
1111
  if (rest.length === 0)
1106
1112
  return this.delete(key);
1107
1113
  const node = this.get(key, true);
@@ -1110,8 +1116,8 @@ var require_Collection = __commonJS((exports) => {
1110
1116
  else
1111
1117
  throw new Error(`Expected YAML collection at ${key}. Remaining path: ${rest}`);
1112
1118
  }
1113
- getIn(path20, keepScalar) {
1114
- const [key, ...rest] = path20;
1119
+ getIn(path21, keepScalar) {
1120
+ const [key, ...rest] = path21;
1115
1121
  const node = this.get(key, true);
1116
1122
  if (rest.length === 0)
1117
1123
  return !keepScalar && identity.isScalar(node) ? node.value : node;
@@ -1126,15 +1132,15 @@ var require_Collection = __commonJS((exports) => {
1126
1132
  return n == null || allowScalar && identity.isScalar(n) && n.value == null && !n.commentBefore && !n.comment && !n.tag;
1127
1133
  });
1128
1134
  }
1129
- hasIn(path20) {
1130
- const [key, ...rest] = path20;
1135
+ hasIn(path21) {
1136
+ const [key, ...rest] = path21;
1131
1137
  if (rest.length === 0)
1132
1138
  return this.has(key);
1133
1139
  const node = this.get(key, true);
1134
1140
  return identity.isCollection(node) ? node.hasIn(rest) : false;
1135
1141
  }
1136
- setIn(path20, value) {
1137
- const [key, ...rest] = path20;
1142
+ setIn(path21, value) {
1143
+ const [key, ...rest] = path21;
1138
1144
  if (rest.length === 0) {
1139
1145
  this.set(key, value);
1140
1146
  } else {
@@ -3527,9 +3533,9 @@ var require_Document = __commonJS((exports) => {
3527
3533
  if (assertCollection(this.contents))
3528
3534
  this.contents.add(value);
3529
3535
  }
3530
- addIn(path20, value) {
3536
+ addIn(path21, value) {
3531
3537
  if (assertCollection(this.contents))
3532
- this.contents.addIn(path20, value);
3538
+ this.contents.addIn(path21, value);
3533
3539
  }
3534
3540
  createAlias(node, name) {
3535
3541
  if (!node.anchor) {
@@ -3578,30 +3584,30 @@ var require_Document = __commonJS((exports) => {
3578
3584
  delete(key) {
3579
3585
  return assertCollection(this.contents) ? this.contents.delete(key) : false;
3580
3586
  }
3581
- deleteIn(path20) {
3582
- if (Collection.isEmptyPath(path20)) {
3587
+ deleteIn(path21) {
3588
+ if (Collection.isEmptyPath(path21)) {
3583
3589
  if (this.contents == null)
3584
3590
  return false;
3585
3591
  this.contents = null;
3586
3592
  return true;
3587
3593
  }
3588
- return assertCollection(this.contents) ? this.contents.deleteIn(path20) : false;
3594
+ return assertCollection(this.contents) ? this.contents.deleteIn(path21) : false;
3589
3595
  }
3590
3596
  get(key, keepScalar) {
3591
3597
  return identity.isCollection(this.contents) ? this.contents.get(key, keepScalar) : undefined;
3592
3598
  }
3593
- getIn(path20, keepScalar) {
3594
- if (Collection.isEmptyPath(path20))
3599
+ getIn(path21, keepScalar) {
3600
+ if (Collection.isEmptyPath(path21))
3595
3601
  return !keepScalar && identity.isScalar(this.contents) ? this.contents.value : this.contents;
3596
- return identity.isCollection(this.contents) ? this.contents.getIn(path20, keepScalar) : undefined;
3602
+ return identity.isCollection(this.contents) ? this.contents.getIn(path21, keepScalar) : undefined;
3597
3603
  }
3598
3604
  has(key) {
3599
3605
  return identity.isCollection(this.contents) ? this.contents.has(key) : false;
3600
3606
  }
3601
- hasIn(path20) {
3602
- if (Collection.isEmptyPath(path20))
3607
+ hasIn(path21) {
3608
+ if (Collection.isEmptyPath(path21))
3603
3609
  return this.contents !== undefined;
3604
- return identity.isCollection(this.contents) ? this.contents.hasIn(path20) : false;
3610
+ return identity.isCollection(this.contents) ? this.contents.hasIn(path21) : false;
3605
3611
  }
3606
3612
  set(key, value) {
3607
3613
  if (this.contents == null) {
@@ -3610,13 +3616,13 @@ var require_Document = __commonJS((exports) => {
3610
3616
  this.contents.set(key, value);
3611
3617
  }
3612
3618
  }
3613
- setIn(path20, value) {
3614
- if (Collection.isEmptyPath(path20)) {
3619
+ setIn(path21, value) {
3620
+ if (Collection.isEmptyPath(path21)) {
3615
3621
  this.contents = value;
3616
3622
  } else if (this.contents == null) {
3617
- this.contents = Collection.collectionFromPath(this.schema, Array.from(path20), value);
3623
+ this.contents = Collection.collectionFromPath(this.schema, Array.from(path21), value);
3618
3624
  } else if (assertCollection(this.contents)) {
3619
- this.contents.setIn(path20, value);
3625
+ this.contents.setIn(path21, value);
3620
3626
  }
3621
3627
  }
3622
3628
  setSchema(version, options = {}) {
@@ -5511,9 +5517,9 @@ var require_cst_visit = __commonJS((exports) => {
5511
5517
  visit.BREAK = BREAK;
5512
5518
  visit.SKIP = SKIP;
5513
5519
  visit.REMOVE = REMOVE;
5514
- visit.itemAtPath = (cst, path20) => {
5520
+ visit.itemAtPath = (cst, path21) => {
5515
5521
  let item = cst;
5516
- for (const [field, index] of path20) {
5522
+ for (const [field, index] of path21) {
5517
5523
  const tok = item?.[field];
5518
5524
  if (tok && "items" in tok) {
5519
5525
  item = tok.items[index];
@@ -5522,23 +5528,23 @@ var require_cst_visit = __commonJS((exports) => {
5522
5528
  }
5523
5529
  return item;
5524
5530
  };
5525
- visit.parentCollection = (cst, path20) => {
5526
- const parent = visit.itemAtPath(cst, path20.slice(0, -1));
5527
- const field = path20[path20.length - 1][0];
5531
+ visit.parentCollection = (cst, path21) => {
5532
+ const parent = visit.itemAtPath(cst, path21.slice(0, -1));
5533
+ const field = path21[path21.length - 1][0];
5528
5534
  const coll = parent?.[field];
5529
5535
  if (coll && "items" in coll)
5530
5536
  return coll;
5531
5537
  throw new Error("Parent collection not found");
5532
5538
  };
5533
- function _visit(path20, item, visitor) {
5534
- let ctrl = visitor(item, path20);
5539
+ function _visit(path21, item, visitor) {
5540
+ let ctrl = visitor(item, path21);
5535
5541
  if (typeof ctrl === "symbol")
5536
5542
  return ctrl;
5537
5543
  for (const field of ["key", "value"]) {
5538
5544
  const token = item[field];
5539
5545
  if (token && "items" in token) {
5540
5546
  for (let i = 0;i < token.items.length; ++i) {
5541
- const ci = _visit(Object.freeze(path20.concat([[field, i]])), token.items[i], visitor);
5547
+ const ci = _visit(Object.freeze(path21.concat([[field, i]])), token.items[i], visitor);
5542
5548
  if (typeof ci === "number")
5543
5549
  i = ci - 1;
5544
5550
  else if (ci === BREAK)
@@ -5549,10 +5555,10 @@ var require_cst_visit = __commonJS((exports) => {
5549
5555
  }
5550
5556
  }
5551
5557
  if (typeof ctrl === "function" && field === "key")
5552
- ctrl = ctrl(item, path20);
5558
+ ctrl = ctrl(item, path21);
5553
5559
  }
5554
5560
  }
5555
- return typeof ctrl === "function" ? ctrl(item, path20) : ctrl;
5561
+ return typeof ctrl === "function" ? ctrl(item, path21) : ctrl;
5556
5562
  }
5557
5563
  exports.visit = visit;
5558
5564
  });
@@ -10677,7 +10683,9 @@ var ArgsSchema4 = z4.object({
10677
10683
  source: z4.enum(["reviewer", "codeforge-fallback"]).optional().describe("写入来源;默认 'reviewer',codeforge 补写时传 'codeforge-fallback'"),
10678
10684
  reviewerAgent: z4.string().optional().describe("写入 agent name(默认 'reviewer';fallback 时为 'codeforge')"),
10679
10685
  sessionId: z4.string().optional().describe("reviewer 子 session id(boomerang 溯源用,可选)"),
10680
- model: z4.string().optional().describe("审批模型 id(审计用,可选)")
10686
+ model: z4.string().optional().describe("审批模型 id(审计用,可选)"),
10687
+ coveredSha: z4.string().optional().describe("approval 写入时 worktree HEAD sha;reviewer 调此工具时传入(git -C <worktreePath> rev-parse HEAD)。pre-check 强绑定核心字段,缺失则 pre-check 不命中。ADR:merge-approval-pre-check"),
10688
+ reviewTarget: z4.string().optional().describe("本次审阅的 review_target 值(reviewer.md 词表:code / code:typescript / code:python / code:csharp-lua-c / plan_only / adr / docs / decision_only)。pre-check 仅放行 startsWith('code') 的值;缺失或其他值均不命中。ADR:merge-approval-pre-check")
10681
10689
  });
10682
10690
  var _approvalStore = null;
10683
10691
  function getApprovalStore() {
@@ -10711,6 +10719,8 @@ async function execute4(input) {
10711
10719
  decisionLine: args.decisionLine ?? args.verdict,
10712
10720
  notes: args.notes,
10713
10721
  createdAt: now,
10722
+ ...args.coveredSha ? { coveredSha: args.coveredSha } : {},
10723
+ ...args.reviewTarget ? { reviewTarget: args.reviewTarget } : {},
10714
10724
  escapeHatch: null
10715
10725
  };
10716
10726
  const file = await approvals.record(meta);
@@ -12369,13 +12379,23 @@ async function pruneOrphanWorktrees(mainRoot, opts = {}) {
12369
12379
  const r = await pruneDiscardedRegistryEntries(resolved);
12370
12380
  discardedPruned = r.pruned;
12371
12381
  } catch {}
12372
- return { cleaned, failed, skipped, discardedPruned };
12382
+ let gitAdminPruned = 0;
12383
+ try {
12384
+ const out = await runGit2(resolved, ["worktree", "prune", "--verbose"]);
12385
+ gitAdminPruned = out.split(`
12386
+ `).filter((l) => /\bRemoving\b/i.test(l)).length;
12387
+ } catch {}
12388
+ return { cleaned, failed, skipped, discardedPruned, gitAdminPruned };
12373
12389
  }
12374
12390
 
12375
12391
  // lib/merge-gate.ts
12376
12392
  import { promises as fs11 } from "node:fs";
12377
12393
  import * as path14 from "node:path";
12378
- var DEFAULT_MERGE_GATE_CONFIG = { enabled: true };
12394
+ var DEFAULT_MERGE_GATE_CONFIG = {
12395
+ enabled: true,
12396
+ approvalPreCheck: true,
12397
+ preCheckTtlSeconds: 3600
12398
+ };
12379
12399
  var CONFIG_REL = ".codeforge/merge-gate.json";
12380
12400
  async function loadMergeGate(mainRoot) {
12381
12401
  const file = path14.join(mainRoot, CONFIG_REL);
@@ -12387,29 +12407,40 @@ async function loadMergeGate(mainRoot) {
12387
12407
  if (e.code === "ENOENT")
12388
12408
  return { ...DEFAULT_MERGE_GATE_CONFIG };
12389
12409
  console.warn(`[merge-gate] 读取 ${CONFIG_REL} 失败,fail-safe 退化为 enabled=false: ${e.message}`);
12390
- return { enabled: false };
12410
+ return { enabled: false, approvalPreCheck: false };
12391
12411
  }
12392
12412
  let parsed;
12393
12413
  try {
12394
12414
  parsed = JSON.parse(raw);
12395
12415
  } catch (err) {
12396
12416
  console.warn(`[merge-gate] ${CONFIG_REL} JSON 解析失败,fail-safe 退化为 enabled=false: ${err instanceof Error ? err.message : String(err)}`);
12397
- return { enabled: false };
12417
+ return { enabled: false, approvalPreCheck: false };
12398
12418
  }
12399
12419
  if (!parsed || typeof parsed !== "object") {
12400
12420
  console.warn(`[merge-gate] ${CONFIG_REL} 顶层非 object,fail-safe 退化为 enabled=false`);
12401
- return { enabled: false };
12421
+ return { enabled: false, approvalPreCheck: false };
12402
12422
  }
12403
12423
  const obj = parsed;
12404
12424
  const enabled = typeof obj["enabled"] === "boolean" ? obj["enabled"] : DEFAULT_MERGE_GATE_CONFIG.enabled;
12405
- return { enabled };
12425
+ const approvalPreCheck = typeof obj["approvalPreCheck"] === "boolean" ? obj["approvalPreCheck"] : DEFAULT_MERGE_GATE_CONFIG.approvalPreCheck ?? false;
12426
+ let preCheckTtlSeconds = DEFAULT_MERGE_GATE_CONFIG.preCheckTtlSeconds ?? 3600;
12427
+ const rawTtl = obj["preCheckTtlSeconds"];
12428
+ if (typeof rawTtl === "number" && rawTtl > 0) {
12429
+ if (rawTtl > 86400) {
12430
+ console.warn(`[merge-gate] preCheckTtlSeconds=${rawTtl} 超过 24h 上界,截断为 86400`);
12431
+ preCheckTtlSeconds = 86400;
12432
+ } else {
12433
+ preCheckTtlSeconds = rawTtl;
12434
+ }
12435
+ }
12436
+ return { enabled, approvalPreCheck, preCheckTtlSeconds };
12406
12437
  }
12407
12438
 
12408
12439
  // lib/merge-loop.ts
12409
12440
  var DEFAULT_MERGE_LOOP_CONFIG = {
12410
12441
  maxReviewLoops: 3,
12411
12442
  autoCoder: true,
12412
- reviewTimeoutMs: 180000,
12443
+ reviewTimeoutMs: 360000,
12413
12444
  coderTimeoutMs: 600000,
12414
12445
  abortDirtyStrategy: "checkpoint"
12415
12446
  };
@@ -12447,6 +12478,43 @@ async function runMergeLoop(opts) {
12447
12478
  });
12448
12479
  return { status: "force_merged", commitSha: sha, loops: 0 };
12449
12480
  }
12481
+ if (mergeGate.enabled && mergeGate.approvalPreCheck) {
12482
+ progress("approval_pre_check", "检查既有 approval 是否可跳过 reviewer");
12483
+ const preStore = opts.__testHooks?.approvalStore ?? ApprovalStore.forProject(opts.mainRoot);
12484
+ const hit = await tryApprovalPreCheck({ opts, entry, mergeGate, store: preStore });
12485
+ if (hit.ok) {
12486
+ await maybeAbort(opts, config, entry);
12487
+ if (typeof preStore.recordEscape === "function") {
12488
+ await preStore.recordEscape({
12489
+ pendingId: `session:${opts.sessionId}`,
12490
+ timestamp: new Date().toISOString(),
12491
+ agent: "codeforge-pre-check",
12492
+ sessionId: opts.sessionId,
12493
+ reason: `approval-pre-check: skipped reviewer (coveredSha=${hit.coveredSha})`,
12494
+ pendingMeta: {
12495
+ target: "worktree",
12496
+ sourceHash: hit.coveredSha,
12497
+ newSize: 0
12498
+ }
12499
+ }).catch((err) => {
12500
+ console.warn(`[merge-loop] pre-check recordEscape 失败 (session=${opts.sessionId}): ${err instanceof Error ? err.message : String(err)}`);
12501
+ });
12502
+ }
12503
+ progress("approval_pre_check", `skip_review | reviewTarget=${hit.reviewTarget} | coveredSha=${hit.coveredSha.slice(0, 12)} | ttlOk`);
12504
+ const { sha } = await mergeSessionBack({
12505
+ sessionId: opts.sessionId,
12506
+ mainRoot: opts.mainRoot
12507
+ });
12508
+ return {
12509
+ status: "skipped_by_approval",
12510
+ commitSha: sha,
12511
+ loops: 0,
12512
+ finalDecision: "APPROVE",
12513
+ lastReviewSummary: `approval-pre-check 命中:coveredSha=${hit.coveredSha}, reviewTarget=${hit.reviewTarget}`
12514
+ };
12515
+ }
12516
+ progress("approval_pre_check", `未命中(${hit.reason}),走 review-loop`);
12517
+ }
12450
12518
  while (true) {
12451
12519
  await maybeAbort(opts, config, entry);
12452
12520
  loops += 1;
@@ -12667,6 +12735,44 @@ async function handleAbortDirty(opts, config, entry) {
12667
12735
  console.warn(`[merge-loop] abort-dirty 处理失败 (session=${opts.sessionId}): ${err instanceof Error ? err.message : String(err)}`);
12668
12736
  }
12669
12737
  }
12738
+ async function tryApprovalPreCheck(args) {
12739
+ const { opts, entry, mergeGate, store } = args;
12740
+ const isDirty = opts.__testHooks?.isWorktreeDirty ?? isWorktreeDirty;
12741
+ const headOf = opts.__testHooks?.getCurrentWorktreeHead ?? getCurrentWorktreeHead;
12742
+ let dirty;
12743
+ try {
12744
+ dirty = await isDirty(entry.worktreePath);
12745
+ } catch {
12746
+ return { ok: false, reason: "dirty-check-failed" };
12747
+ }
12748
+ if (dirty)
12749
+ return { ok: false, reason: "dirty" };
12750
+ let approval;
12751
+ try {
12752
+ approval = await store.getLatest(`session:${opts.sessionId}`);
12753
+ } catch (err) {
12754
+ console.warn(`[merge-loop] pre-check getLatest 失败 (session=${opts.sessionId}): ${err instanceof Error ? err.message : String(err)}`);
12755
+ return { ok: false, reason: "no-approval" };
12756
+ }
12757
+ if (!approval)
12758
+ return { ok: false, reason: "no-approval" };
12759
+ if (approval.verdict !== "APPROVE" && approval.verdict !== "APPROVE_WITH_NOTES") {
12760
+ return { ok: false, reason: "verdict" };
12761
+ }
12762
+ if (approval.reviewTarget?.startsWith("code") !== true) {
12763
+ return { ok: false, reason: "target" };
12764
+ }
12765
+ if (!approval.coveredSha)
12766
+ return { ok: false, reason: "no-covered-sha" };
12767
+ const head = await headOf(entry.worktreePath);
12768
+ if (approval.coveredSha !== head)
12769
+ return { ok: false, reason: "sha-mismatch" };
12770
+ const ttlSeconds = Math.min(mergeGate.preCheckTtlSeconds ?? 3600, 86400);
12771
+ const age = Date.now() - Date.parse(approval.createdAt);
12772
+ if (!(age <= ttlSeconds * 1000))
12773
+ return { ok: false, reason: "ttl-expired" };
12774
+ return { ok: true, coveredSha: approval.coveredSha, reviewTarget: approval.reviewTarget };
12775
+ }
12670
12776
  function isAbortError(err) {
12671
12777
  return err instanceof Error && err.name === "AbortError";
12672
12778
  }
@@ -13646,6 +13752,159 @@ async function execute15(input) {
13646
13752
  };
13647
13753
  }
13648
13754
  }
13755
+ // tools/worktrees-gc.ts
13756
+ import { z as z16 } from "zod";
13757
+
13758
+ // lib/opencode-session-probe.ts
13759
+ import * as path18 from "node:path";
13760
+ import * as os5 from "node:os";
13761
+ import { createRequire as createRequire2 } from "node:module";
13762
+ var requireFromHere = createRequire2(import.meta.url);
13763
+ var DEFAULT_LIVENESS_MS = 6 * 60 * 60000;
13764
+ var DEFAULT_DB_PATH = path18.join(os5.homedir(), ".local/share/opencode/opencode.db");
13765
+ function createSessionProbe(opts = {}) {
13766
+ const dbPath = opts.dbPath ?? DEFAULT_DB_PATH;
13767
+ const httpBaseUrl = opts.httpBaseUrl ?? process.env["OPENCODE_SERVER_URL"];
13768
+ const livenessWindowMs = opts.livenessWindowMs ?? DEFAULT_LIVENESS_MS;
13769
+ const timeoutMs = opts.timeoutMs ?? 1500;
13770
+ let db = null;
13771
+ let dbInitTried = false;
13772
+ let dbStmt = null;
13773
+ async function tryOpenDb() {
13774
+ if (dbInitTried)
13775
+ return db != null;
13776
+ dbInitTried = true;
13777
+ try {
13778
+ const mod = requireFromHere("node:sqlite");
13779
+ try {
13780
+ db = new mod.DatabaseSync(dbPath, { readOnly: true });
13781
+ } catch {
13782
+ db = null;
13783
+ dbStmt = null;
13784
+ return false;
13785
+ }
13786
+ dbStmt = db.prepare("SELECT time_updated, time_archived FROM session WHERE id = ? LIMIT 1");
13787
+ return true;
13788
+ } catch {
13789
+ db = null;
13790
+ dbStmt = null;
13791
+ return false;
13792
+ }
13793
+ }
13794
+ async function probeHttp(sessionId) {
13795
+ if (!httpBaseUrl)
13796
+ return null;
13797
+ try {
13798
+ const ctrl = new AbortController;
13799
+ const t = setTimeout(() => ctrl.abort(), timeoutMs);
13800
+ const res = await fetch(`${httpBaseUrl.replace(/\/$/, "")}/session`, {
13801
+ signal: ctrl.signal
13802
+ }).finally(() => clearTimeout(t));
13803
+ if (!res.ok)
13804
+ return null;
13805
+ const list = await res.json();
13806
+ const hit = Array.isArray(list) && list.some((s) => s.id === sessionId);
13807
+ return { alive: hit, source: "http" };
13808
+ } catch {
13809
+ return null;
13810
+ }
13811
+ }
13812
+ async function probeSqlite(sessionId) {
13813
+ if (!await tryOpenDb() || !dbStmt)
13814
+ return null;
13815
+ try {
13816
+ const row = dbStmt.get(sessionId);
13817
+ if (!row) {
13818
+ return { alive: false, source: "sqlite" };
13819
+ }
13820
+ if (row.time_archived != null) {
13821
+ return {
13822
+ alive: false,
13823
+ source: "sqlite",
13824
+ time_archived: row.time_archived
13825
+ };
13826
+ }
13827
+ const now = Date.now();
13828
+ const tu = Number(row.time_updated) || 0;
13829
+ const alive = now - tu < livenessWindowMs;
13830
+ return { alive, source: "sqlite", time_updated: tu, time_archived: null };
13831
+ } catch {
13832
+ return null;
13833
+ }
13834
+ }
13835
+ return {
13836
+ async isSessionAlive(sessionId) {
13837
+ const http = await probeHttp(sessionId);
13838
+ if (http)
13839
+ return http;
13840
+ const sql = await probeSqlite(sessionId);
13841
+ if (sql)
13842
+ return sql;
13843
+ return { alive: true, source: "unknown" };
13844
+ },
13845
+ close() {
13846
+ try {
13847
+ db?.close?.();
13848
+ } catch {}
13849
+ db = null;
13850
+ dbStmt = null;
13851
+ }
13852
+ };
13853
+ }
13854
+
13855
+ // tools/worktrees-gc.ts
13856
+ var description16 = [
13857
+ "手动触发 worktree 垃圾回收(清理僵尸 / 孤儿 worktree + dangling git 元数据)。",
13858
+ "**何时调用**:",
13859
+ "- 用户发现 worktree 堆积(git worktree list 一大堆 codeforge-session-*)",
13860
+ "- 自动 GC 疑似停跑(lifecycle 因项目无 .codeforge/ skip 过一段时间)",
13861
+ "**模式**:",
13862
+ "- 默认(保守):沿用自动 GC 的时间护栏(6h/72h),只清确凿僵尸",
13863
+ "- force=true(高风险):时间护栏归零,连 probe 判 unknown 的也立即清,可能误删活跃 session 的 worktree",
13864
+ "**何时不需要**:自动 30min interval 正常跑时无需手动调。"
13865
+ ].join(`
13866
+ `);
13867
+ var ArgsSchema16 = z16.object({
13868
+ force: z16.boolean().optional().describe("高风险:时间护栏归零,连 probe unknown 的 worktree 也立即清,可能误删活跃 session worktree。默认 false(保守)")
13869
+ });
13870
+ var _ctx3 = {};
13871
+ function __setContext3(ctx) {
13872
+ _ctx3 = ctx;
13873
+ }
13874
+ function getMainRoot2() {
13875
+ return _ctx3.mainRoot ?? process.cwd();
13876
+ }
13877
+ async function execute16(input) {
13878
+ const parsed = ArgsSchema16.safeParse(input);
13879
+ if (!parsed.success) {
13880
+ return {
13881
+ ok: false,
13882
+ error: parsed.error.issues.map((i) => `${i.path.join(".") || "(root)"}: ${i.message}`).join("; ")
13883
+ };
13884
+ }
13885
+ const force = parsed.data.force === true;
13886
+ const probe = createSessionProbe();
13887
+ try {
13888
+ const result = await pruneOrphanWorktrees(getMainRoot2(), {
13889
+ isSessionAlive: probe.isSessionAlive,
13890
+ ...force ? { semanticOrphanMinAgeMs: 0, semanticOrphanUnknownTimeoutMs: 0 } : {}
13891
+ });
13892
+ return {
13893
+ ok: true,
13894
+ mode: force ? "force" : "conservative",
13895
+ cleaned: result.cleaned,
13896
+ failed: result.failed,
13897
+ skipped: result.skipped,
13898
+ discardedPruned: result.discardedPruned ?? 0,
13899
+ gitAdminPruned: result.gitAdminPruned ?? 0,
13900
+ cleanedCount: result.cleaned.length
13901
+ };
13902
+ } catch (err) {
13903
+ return { ok: false, error: err instanceof Error ? err.message : String(err) };
13904
+ } finally {
13905
+ probe.close();
13906
+ }
13907
+ }
13649
13908
  // lib/opencode-runner.ts
13650
13909
  function makeOpencodeRunner(opts) {
13651
13910
  const log4 = opts.log ?? (() => {});
@@ -13907,18 +14166,18 @@ init_decision_parser();
13907
14166
 
13908
14167
  // lib/parent-map-store.ts
13909
14168
  import { promises as fs14 } from "node:fs";
13910
- import * as path18 from "node:path";
14169
+ import * as path19 from "node:path";
13911
14170
  var PARENT_MAP_VERSION = 1;
13912
14171
  var PARENT_MAP_LOCK_TIMEOUT_MS = 2000;
13913
14172
  var PARENT_MAP_MAX_ENTRIES = 256;
13914
14173
  function parentMapDir(mainRoot) {
13915
- return path18.join(runtimeDir(path18.resolve(mainRoot), { ensure: false }), "session-worktrees");
14174
+ return path19.join(runtimeDir(path19.resolve(mainRoot), { ensure: false }), "session-worktrees");
13916
14175
  }
13917
14176
  function parentMapPath(mainRoot) {
13918
- return path18.join(parentMapDir(mainRoot), "parent-map.json");
14177
+ return path19.join(parentMapDir(mainRoot), "parent-map.json");
13919
14178
  }
13920
14179
  function parentMapLockPath(mainRoot) {
13921
- return path18.join(parentMapDir(mainRoot), "parent-map.lock");
14180
+ return path19.join(parentMapDir(mainRoot), "parent-map.lock");
13922
14181
  }
13923
14182
  async function readParentMapFile(mainRoot) {
13924
14183
  const file = parentMapPath(mainRoot);
@@ -13938,7 +14197,7 @@ async function readParentMapFile(mainRoot) {
13938
14197
  }
13939
14198
  async function writeParentMapFile(mainRoot, payload) {
13940
14199
  const file = parentMapPath(mainRoot);
13941
- await fs14.mkdir(path18.dirname(file), { recursive: true });
14200
+ await fs14.mkdir(path19.dirname(file), { recursive: true });
13942
14201
  const tmp = `${file}.tmp-${process.pid}-${Date.now()}`;
13943
14202
  await fs14.writeFile(tmp, JSON.stringify(payload, null, 2), "utf8");
13944
14203
  await fs14.rename(tmp, file);
@@ -13973,7 +14232,7 @@ async function loadParentMap(mainRoot) {
13973
14232
  }
13974
14233
  async function mutateParentMap(mainRoot, mutator, opts = {}) {
13975
14234
  const lockPath = parentMapLockPath(mainRoot);
13976
- await fs14.mkdir(path18.dirname(lockPath), { recursive: true });
14235
+ await fs14.mkdir(path19.dirname(lockPath), { recursive: true });
13977
14236
  const lockOpts = {
13978
14237
  timeoutMs: opts.timeoutMs ?? PARENT_MAP_LOCK_TIMEOUT_MS,
13979
14238
  ...opts
@@ -13992,7 +14251,7 @@ async function appendParentEntry(mainRoot, childID, parentID, ts, opts = {}) {
13992
14251
  if (!childID || !parentID)
13993
14252
  return;
13994
14253
  const lockPath = parentMapLockPath(mainRoot);
13995
- await fs14.mkdir(path18.dirname(lockPath), { recursive: true });
14254
+ await fs14.mkdir(path19.dirname(lockPath), { recursive: true });
13996
14255
  const lockOpts = {
13997
14256
  timeoutMs: opts.timeoutMs ?? PARENT_MAP_LOCK_TIMEOUT_MS,
13998
14257
  ...opts
@@ -14124,7 +14383,7 @@ class ProductionSpawner {
14124
14383
  prompt,
14125
14384
  title: `[merge-review] sess=${args.sessionId.slice(0, 8)} r=${args.round}/${args.maxRounds}`,
14126
14385
  ...args.signal ? { signal: args.signal } : {},
14127
- timeoutMs: this.opts.reviewerTimeoutMs ?? 180000
14386
+ timeoutMs: this.opts.reviewerTimeoutMs ?? 360000
14128
14387
  }, args.sessionId);
14129
14388
  } catch (err) {
14130
14389
  throw err;
@@ -14326,7 +14585,7 @@ function clip3(s, max) {
14326
14585
 
14327
14586
  // lib/codeforge-runtime.ts
14328
14587
  import { promises as fs15 } from "node:fs";
14329
- import * as path19 from "node:path";
14588
+ import * as path20 from "node:path";
14330
14589
  var DEFAULT_RUNTIME = {
14331
14590
  autonomy: {
14332
14591
  downgrade_on_risky: true
@@ -14369,7 +14628,7 @@ function loadRuntimeSync(opts = {}) {
14369
14628
  }
14370
14629
  async function loadRuntime(opts = {}) {
14371
14630
  const root = opts.root ?? process.cwd();
14372
- const abs = path19.resolve(root, opts.file ?? CONFIG_FILE);
14631
+ const abs = path20.resolve(root, opts.file ?? CONFIG_FILE);
14373
14632
  let raw;
14374
14633
  try {
14375
14634
  raw = await fs15.readFile(abs, "utf8");
@@ -14653,7 +14912,7 @@ var toolHeartbeatServer = async (ctx) => {
14653
14912
  var handler6 = toolHeartbeatServer;
14654
14913
 
14655
14914
  // plugins/codeforge-tools.ts
14656
- var z16 = tool.schema;
14915
+ var z17 = tool.schema;
14657
14916
  var PLUGIN_NAME7 = "codeforge-tools";
14658
14917
  logLifecycle(PLUGIN_NAME7, "import");
14659
14918
  function wrap(output, metadata) {
@@ -14700,7 +14959,7 @@ function buildBrowserTools() {
14700
14959
  browser_navigate: tool({
14701
14960
  description: description5,
14702
14961
  args: {
14703
- url: z16.string().min(1).describe("要打开的 URL;必须是 http(s)")
14962
+ url: z17.string().min(1).describe("要打开的 URL;必须是 http(s)")
14704
14963
  },
14705
14964
  async execute(args) {
14706
14965
  return await runSafe("browser_navigate", async () => {
@@ -14715,7 +14974,7 @@ function buildBrowserTools() {
14715
14974
  browser_click: tool({
14716
14975
  description: description6,
14717
14976
  args: {
14718
- selector: z16.string().min(1).describe("CSS / Playwright locator,必须能唯一定位")
14977
+ selector: z17.string().min(1).describe("CSS / Playwright locator,必须能唯一定位")
14719
14978
  },
14720
14979
  async execute(args) {
14721
14980
  return await runSafe("browser_click", async () => {
@@ -14730,8 +14989,8 @@ function buildBrowserTools() {
14730
14989
  browser_fill: tool({
14731
14990
  description: description7,
14732
14991
  args: {
14733
- selector: z16.string().min(1).describe("CSS / Playwright locator"),
14734
- value: z16.string().describe("要填入的文本;原样写入不转义")
14992
+ selector: z17.string().min(1).describe("CSS / Playwright locator"),
14993
+ value: z17.string().describe("要填入的文本;原样写入不转义")
14735
14994
  },
14736
14995
  async execute(args) {
14737
14996
  return await runSafe("browser_fill", async () => {
@@ -14746,8 +15005,8 @@ function buildBrowserTools() {
14746
15005
  browser_screenshot: tool({
14747
15006
  description: description8,
14748
15007
  args: {
14749
- fullPage: z16.boolean().optional().describe("是否截全长页面(默认仅可视区)"),
14750
- selector: z16.string().optional().describe("CSS 选择器;指定时只截该元素")
15008
+ fullPage: z17.boolean().optional().describe("是否截全长页面(默认仅可视区)"),
15009
+ selector: z17.string().optional().describe("CSS 选择器;指定时只截该元素")
14751
15010
  },
14752
15011
  async execute(args) {
14753
15012
  return await runSafe("browser_screenshot", async () => {
@@ -14762,8 +15021,8 @@ function buildBrowserTools() {
14762
15021
  browser_console: tool({
14763
15022
  description: description9,
14764
15023
  args: {
14765
- level: z16.enum(["log", "info", "warn", "error", "debug"]).optional().describe("过滤级别"),
14766
- sinceTs: z16.number().optional().describe("timestamp >= sinceTs 的条目")
15024
+ level: z17.enum(["log", "info", "warn", "error", "debug"]).optional().describe("过滤级别"),
15025
+ sinceTs: z17.number().optional().describe("timestamp >= sinceTs 的条目")
14767
15026
  },
14768
15027
  async execute(args) {
14769
15028
  return await runSafe("browser_console", async () => {
@@ -14778,8 +15037,8 @@ function buildBrowserTools() {
14778
15037
  browser_network: tool({
14779
15038
  description: description10,
14780
15039
  args: {
14781
- method: z16.string().optional().describe("HTTP 方法过滤(GET/POST/...)"),
14782
- sinceTs: z16.number().optional().describe("start_ts >= sinceTs 的请求")
15040
+ method: z17.string().optional().describe("HTTP 方法过滤(GET/POST/...)"),
15041
+ sinceTs: z17.number().optional().describe("start_ts >= sinceTs 的请求")
14783
15042
  },
14784
15043
  async execute(args) {
14785
15044
  return await runSafe("browser_network", async () => {
@@ -14802,7 +15061,8 @@ var CORE_TOOL_NAMES = [
14802
15061
  "session_merge",
14803
15062
  "plan_write",
14804
15063
  "plan_read",
14805
- "adr_init"
15064
+ "adr_init",
15065
+ "worktrees_gc"
14806
15066
  ];
14807
15067
  var BROWSER_TOOL_NAMES = [
14808
15068
  "browser_navigate",
@@ -14839,31 +15099,32 @@ var codeforgeToolsServer = async (ctx) => {
14839
15099
  spawner,
14840
15100
  resolveCurrentSessionId: () => process.env["CODEFORGE_SESSION_ID"] ?? ""
14841
15101
  });
15102
+ __setContext3({ mainRoot: ctx.directory ?? process.cwd() });
14842
15103
  const browserTools = browserEnabled ? buildBrowserTools() : {};
14843
15104
  return {
14844
15105
  tool: {
14845
15106
  ast_edit: tool({
14846
15107
  description,
14847
15108
  args: {
14848
- action: z16.enum([
15109
+ action: z17.enum([
14849
15110
  "replace_anchor",
14850
15111
  "insert_after_anchor",
14851
15112
  "insert_before_anchor",
14852
15113
  "delete_range",
14853
15114
  "rename_symbol"
14854
15115
  ]).describe("编辑类型;不同 action 需要的字段不同"),
14855
- target: z16.string().min(1).describe("目标文件路径(相对 cwd 或绝对)"),
14856
- before_hash: z16.string().optional().describe("操作前的 sha256 hex(强烈建议传,新文件传 null/省略)"),
14857
- auto_stage: z16.boolean().optional().describe("已废弃(保留兼容字段):Phase 5 后 ast_edit 直接写入 session worktree,无需独立 stage"),
14858
- description: z16.string().optional().describe("可选变更说明(写入 audit log)"),
14859
- anchor: z16.string().optional().describe("anchor 类用:anchor 文本或 regex 源"),
14860
- regex: z16.boolean().optional().describe("anchor 是否按 RegExp 解释,默认 false"),
14861
- occurrence: z16.number().int().min(1).optional().describe("第几次匹配(1-based),默认 1"),
14862
- payload: z16.string().optional().describe("anchor 类用:要写入的内容"),
14863
- start: z16.number().int().min(1).optional().describe("delete_range 用:起始行(1-based)"),
14864
- end: z16.number().int().min(1).optional().describe("delete_range 用:结束行(1-based)"),
14865
- old_name: z16.string().optional().describe("rename_symbol 用:旧标识符"),
14866
- new_name: z16.string().optional().describe("rename_symbol 用:新标识符")
15116
+ target: z17.string().min(1).describe("目标文件路径(相对 cwd 或绝对)"),
15117
+ before_hash: z17.string().optional().describe("操作前的 sha256 hex(强烈建议传,新文件传 null/省略)"),
15118
+ auto_stage: z17.boolean().optional().describe("已废弃(保留兼容字段):Phase 5 后 ast_edit 直接写入 session worktree,无需独立 stage"),
15119
+ description: z17.string().optional().describe("可选变更说明(写入 audit log)"),
15120
+ anchor: z17.string().optional().describe("anchor 类用:anchor 文本或 regex 源"),
15121
+ regex: z17.boolean().optional().describe("anchor 是否按 RegExp 解释,默认 false"),
15122
+ occurrence: z17.number().int().min(1).optional().describe("第几次匹配(1-based),默认 1"),
15123
+ payload: z17.string().optional().describe("anchor 类用:要写入的内容"),
15124
+ start: z17.number().int().min(1).optional().describe("delete_range 用:起始行(1-based)"),
15125
+ end: z17.number().int().min(1).optional().describe("delete_range 用:结束行(1-based)"),
15126
+ old_name: z17.string().optional().describe("rename_symbol 用:旧标识符"),
15127
+ new_name: z17.string().optional().describe("rename_symbol 用:新标识符")
14867
15128
  },
14868
15129
  async execute(args) {
14869
15130
  return await runSafeTracked("ast_edit", async () => {
@@ -14886,10 +15147,10 @@ var codeforgeToolsServer = async (ctx) => {
14886
15147
  repo_map: tool({
14887
15148
  description: description2,
14888
15149
  args: {
14889
- root: z16.string().optional().describe("扫描根目录,默认 cwd"),
14890
- top: z16.number().int().min(1).max(100).optional().describe("展示 top N 文件,默认 20"),
14891
- focus: z16.string().optional().describe("聚焦文件(POSIX 相对路径)"),
14892
- max_files: z16.number().int().min(10).max(5000).optional().describe("扫描上限,默认 500")
15150
+ root: z17.string().optional().describe("扫描根目录,默认 cwd"),
15151
+ top: z17.number().int().min(1).max(100).optional().describe("展示 top N 文件,默认 20"),
15152
+ focus: z17.string().optional().describe("聚焦文件(POSIX 相对路径)"),
15153
+ max_files: z17.number().int().min(10).max(5000).optional().describe("扫描上限,默认 500")
14893
15154
  },
14894
15155
  async execute(args) {
14895
15156
  return await runSafeTracked("repo_map", async () => {
@@ -14910,14 +15171,14 @@ var codeforgeToolsServer = async (ctx) => {
14910
15171
  rules_debug: tool({
14911
15172
  description: description3,
14912
15173
  args: {
14913
- current_agent: z16.string().optional().describe("当前 agent 名"),
14914
- root: z16.string().optional().describe("项目根目录"),
14915
- home_dir: z16.string().optional().describe("覆盖个人规则目录"),
14916
- project_dir: z16.string().optional().describe("覆盖项目规则目录"),
14917
- skip_personal: z16.boolean().optional(),
14918
- skip_project: z16.boolean().optional(),
14919
- skip_agent: z16.boolean().optional(),
14920
- markdown: z16.boolean().optional().describe("默认 true:渲染 markdown 摘要")
15174
+ current_agent: z17.string().optional().describe("当前 agent 名"),
15175
+ root: z17.string().optional().describe("项目根目录"),
15176
+ home_dir: z17.string().optional().describe("覆盖个人规则目录"),
15177
+ project_dir: z17.string().optional().describe("覆盖项目规则目录"),
15178
+ skip_personal: z17.boolean().optional(),
15179
+ skip_project: z17.boolean().optional(),
15180
+ skip_agent: z17.boolean().optional(),
15181
+ markdown: z17.boolean().optional().describe("默认 true:渲染 markdown 摘要")
14921
15182
  },
14922
15183
  async execute(args) {
14923
15184
  return await runSafe("rules_debug", async () => {
@@ -14932,14 +15193,14 @@ var codeforgeToolsServer = async (ctx) => {
14932
15193
  review_approval: tool({
14933
15194
  description: description4,
14934
15195
  args: {
14935
- verdict: z16.enum(["APPROVE", "APPROVE_WITH_NOTES"]).describe("审批层裁决(审计字段,与协议层独立);REQUEST_CHANGES / BLOCK 不应调本工具。⚠️ 不要把本字段值复制到 reviewer 输出的 `## Decision` 节首行 — 那是协议层 3 档。"),
14936
- pendingIds: z16.array(z16.string().min(1)).min(1).describe("本次 APPROVE 覆盖的 pending change id 列表"),
14937
- notes: z16.string().min(1).max(2000).describe("审阅意见摘要(建议 ≤ 500 字)"),
14938
- decisionLine: z16.string().optional().describe("`## Decision` 节首行原文(默认 verdict 字面量,机审证据)"),
14939
- source: z16.enum(["reviewer", "codeforge-fallback"]).optional().describe("写入来源;默认 'reviewer',codeforge 补写时传 'codeforge-fallback'"),
14940
- reviewerAgent: z16.string().optional().describe("写入 agent name(默认 'reviewer';fallback 时为 'codeforge')"),
14941
- sessionId: z16.string().optional().describe("reviewer 子 session id(boomerang 溯源用,可选)"),
14942
- model: z16.string().optional().describe("审批模型 id(审计用,可选)")
15196
+ verdict: z17.enum(["APPROVE", "APPROVE_WITH_NOTES"]).describe("审批层裁决(审计字段,与协议层独立);REQUEST_CHANGES / BLOCK 不应调本工具。⚠️ 不要把本字段值复制到 reviewer 输出的 `## Decision` 节首行 — 那是协议层 3 档。"),
15197
+ pendingIds: z17.array(z17.string().min(1)).min(1).describe("本次 APPROVE 覆盖的 pending change id 列表"),
15198
+ notes: z17.string().min(1).max(2000).describe("审阅意见摘要(建议 ≤ 500 字)"),
15199
+ decisionLine: z17.string().optional().describe("`## Decision` 节首行原文(默认 verdict 字面量,机审证据)"),
15200
+ source: z17.enum(["reviewer", "codeforge-fallback"]).optional().describe("写入来源;默认 'reviewer',codeforge 补写时传 'codeforge-fallback'"),
15201
+ reviewerAgent: z17.string().optional().describe("写入 agent name(默认 'reviewer';fallback 时为 'codeforge')"),
15202
+ sessionId: z17.string().optional().describe("reviewer 子 session id(boomerang 溯源用,可选)"),
15203
+ model: z17.string().optional().describe("审批模型 id(审计用,可选)")
14943
15204
  },
14944
15205
  async execute(args) {
14945
15206
  return await runSafe("review_approval", async () => {
@@ -14960,10 +15221,10 @@ var codeforgeToolsServer = async (ctx) => {
14960
15221
  model_chain: tool({
14961
15222
  description: description11,
14962
15223
  args: {
14963
- agent: z16.string().optional().describe("查指定 agent;不传 → 列出全部"),
14964
- current: z16.string().optional().describe("当前已用过的模型(<provider>/<id>),用于算下一档"),
14965
- root: z16.string().optional().describe("项目根目录,默认 cwd"),
14966
- config_file: z16.string().optional().describe("配置文件名;默认 codeforge.json")
15224
+ agent: z17.string().optional().describe("查指定 agent;不传 → 列出全部"),
15225
+ current: z17.string().optional().describe("当前已用过的模型(<provider>/<id>),用于算下一档"),
15226
+ root: z17.string().optional().describe("项目根目录,默认 cwd"),
15227
+ config_file: z17.string().optional().describe("配置文件名;默认 codeforge.json")
14967
15228
  },
14968
15229
  async execute(args) {
14969
15230
  return await runSafe("model_chain", async () => {
@@ -14984,11 +15245,11 @@ var codeforgeToolsServer = async (ctx) => {
14984
15245
  session_merge: tool({
14985
15246
  description: description12,
14986
15247
  args: {
14987
- action: z16.enum(["merge", "status", "discard", "diff"]).describe("操作类型:merge=合并到主仓(orchestrator 专用)/ status=查询状态 / discard=放弃 / diff=查看 worktree 改动"),
14988
- session_id: z16.string().optional().describe("目标 session id;不传则用当前 session"),
14989
- plan_id: z16.string().optional().describe("关联的 plan_id(reviewer 校验时用),格式 plan-YYYYMMDD-HHmmss-NNN"),
14990
- force: z16.boolean().optional().describe("action=merge 时跳过 review 直接 squash merge(写审计)"),
14991
- stat: z16.boolean().optional().describe("action=diff 时:true=只显示文件列表+统计,false=完整 diff(默认 false)")
15248
+ action: z17.enum(["merge", "status", "discard", "diff"]).describe("操作类型:merge=合并到主仓(orchestrator 专用)/ status=查询状态 / discard=放弃 / diff=查看 worktree 改动"),
15249
+ session_id: z17.string().optional().describe("目标 session id;不传则用当前 session"),
15250
+ plan_id: z17.string().optional().describe("关联的 plan_id(reviewer 校验时用),格式 plan-YYYYMMDD-HHmmss-NNN"),
15251
+ force: z17.boolean().optional().describe("action=merge 时跳过 review 直接 squash merge(写审计)"),
15252
+ stat: z17.boolean().optional().describe("action=diff 时:true=只显示文件列表+统计,false=完整 diff(默认 false)")
14992
15253
  },
14993
15254
  async execute(args, input) {
14994
15255
  return await runSafe("session_merge", async () => {
@@ -15024,9 +15285,9 @@ var codeforgeToolsServer = async (ctx) => {
15024
15285
  plan_write: tool({
15025
15286
  description: description13,
15026
15287
  args: {
15027
- title: z16.string().min(2).max(80).describe('方案简短标题(2-80 字),如 "实现 worktree session 隔离 Phase 1"'),
15028
- content: z16.string().min(10).describe("方案 markdown 全文,建议 ≥ 50 行"),
15029
- tags: z16.array(z16.string().min(1)).optional().describe('标签(可选),如 ["phase:1", "arch:worktree"]')
15288
+ title: z17.string().min(2).max(80).describe('方案简短标题(2-80 字),如 "实现 worktree session 隔离 Phase 1"'),
15289
+ content: z17.string().min(10).describe("方案 markdown 全文,建议 ≥ 50 行"),
15290
+ tags: z17.array(z17.string().min(1)).optional().describe('标签(可选),如 ["phase:1", "arch:worktree"]')
15030
15291
  },
15031
15292
  async execute(args) {
15032
15293
  return await runSafeTracked("plan_write", async () => {
@@ -15045,8 +15306,8 @@ var codeforgeToolsServer = async (ctx) => {
15045
15306
  plan_read: tool({
15046
15307
  description: description14,
15047
15308
  args: {
15048
- plan_id: z16.string().optional().describe("方案 ID(推荐),格式 plan-YYYYMMDD-HHmmss-NNN"),
15049
- path: z16.string().optional().describe("方案绝对路径(兜底用,没有 plan_id 元数据时退而求其次)")
15309
+ plan_id: z17.string().optional().describe("方案 ID(推荐),格式 plan-YYYYMMDD-HHmmss-NNN"),
15310
+ path: z17.string().optional().describe("方案绝对路径(兜底用,没有 plan_id 元数据时退而求其次)")
15050
15311
  },
15051
15312
  async execute(args) {
15052
15313
  return await runSafe("plan_read", async () => {
@@ -15069,11 +15330,11 @@ var codeforgeToolsServer = async (ctx) => {
15069
15330
  adr_init: tool({
15070
15331
  description: description15,
15071
15332
  args: {
15072
- cwd: z16.string().optional().describe("目标项目根目录,默认 process.cwd();通常无需传"),
15073
- force: z16.boolean().optional().describe("已存在文件覆盖;覆盖前自动 .bak.<ts> 备份"),
15074
- dryRun: z16.boolean().optional().describe("只输出将要执行的写入计划,不实际写盘"),
15075
- writePrepare: z16.boolean().optional().describe("(npm 项目)自动合并 git config core.hooksPath 到 package.json scripts.prepare;写前自动 backup"),
15076
- installPrePush: z16.boolean().optional().describe("是否同时生成 .githooks/pre-push hook,默认 true")
15333
+ cwd: z17.string().optional().describe("目标项目根目录,默认 process.cwd();通常无需传"),
15334
+ force: z17.boolean().optional().describe("已存在文件覆盖;覆盖前自动 .bak.<ts> 备份"),
15335
+ dryRun: z17.boolean().optional().describe("只输出将要执行的写入计划,不实际写盘"),
15336
+ writePrepare: z17.boolean().optional().describe("(npm 项目)自动合并 git config core.hooksPath 到 package.json scripts.prepare;写前自动 backup"),
15337
+ installPrePush: z17.boolean().optional().describe("是否同时生成 .githooks/pre-push hook,默认 true")
15077
15338
  },
15078
15339
  async execute(args) {
15079
15340
  return await runSafe("adr_init", async () => {
@@ -15085,6 +15346,27 @@ var codeforgeToolsServer = async (ctx) => {
15085
15346
  return wrap(result, { title: "adr_init" });
15086
15347
  });
15087
15348
  }
15349
+ }),
15350
+ worktrees_gc: tool({
15351
+ description: description16,
15352
+ args: {
15353
+ force: z17.boolean().optional().describe("高风险:时间护栏归零,连 probe unknown 的 worktree 也立即清,可能误删活跃 session worktree。默认 false(保守)")
15354
+ },
15355
+ async execute(args) {
15356
+ return await runSafe("worktrees_gc", async () => {
15357
+ const v = projectValidate("worktrees_gc", ArgsSchema16, args);
15358
+ if (!v.ok)
15359
+ return wrap(JSON.parse(v.output));
15360
+ const result = await execute16(v.data);
15361
+ const meta = { title: "worktrees_gc" };
15362
+ const r = result;
15363
+ if (r.ok) {
15364
+ meta["cleaned"] = r.cleanedCount ?? 0;
15365
+ meta["gitAdminPruned"] = r.gitAdminPruned ?? 0;
15366
+ }
15367
+ return wrap(result, meta);
15368
+ });
15369
+ }
15088
15370
  })
15089
15371
  }
15090
15372
  };
@@ -15093,10 +15375,10 @@ var handler7 = codeforgeToolsServer;
15093
15375
 
15094
15376
  // plugins/discover-spec-suggest.ts
15095
15377
  import { readFileSync as readFileSync3, readdirSync, statSync as statSync2 } from "node:fs";
15096
- import { join as join16 } from "node:path";
15378
+ import { join as join17 } from "node:path";
15097
15379
 
15098
15380
  // lib/handoff-schema.ts
15099
- import { z as z17 } from "zod";
15381
+ import { z as z18 } from "zod";
15100
15382
 
15101
15383
  // node_modules/yaml/dist/index.js
15102
15384
  var composer = require_composer();
@@ -15149,92 +15431,92 @@ var MAX_HANDOFF_SIZE = 100 * 1024;
15149
15431
  var SLUG_REGEX = /^[a-z0-9][a-z0-9-]{0,49}$/;
15150
15432
  var SCORE_DIMENSIONS = ["functional", "ux", "technical", "constraints", "edge_cases"];
15151
15433
  var COMBO_VALUES = ["A", "B", "C", "D"];
15152
- var NeedSchema = z17.object({
15153
- id: z17.string().min(1),
15154
- type: z17.enum(["must", "should", "nice-to-have"]),
15155
- statement: z17.string().min(1),
15156
- rationale: z17.string().optional(),
15157
- acceptance: z17.array(z17.string()).optional()
15434
+ var NeedSchema = z18.object({
15435
+ id: z18.string().min(1),
15436
+ type: z18.enum(["must", "should", "nice-to-have"]),
15437
+ statement: z18.string().min(1),
15438
+ rationale: z18.string().optional(),
15439
+ acceptance: z18.array(z18.string()).optional()
15158
15440
  });
15159
- var BoundarySchema = z17.object({
15160
- excluded: z17.string().min(1),
15161
- reason: z17.string().min(1)
15441
+ var BoundarySchema = z18.object({
15442
+ excluded: z18.string().min(1),
15443
+ reason: z18.string().min(1)
15162
15444
  });
15163
- var AssumptionSchema = z17.object({
15164
- statement: z17.string().min(1),
15165
- confidence: z17.enum(["verified", "speculation", "high-risk-unknown"]),
15166
- needs_validation_by: z17.enum(["plan", "coder", "runtime"]).optional()
15445
+ var AssumptionSchema = z18.object({
15446
+ statement: z18.string().min(1),
15447
+ confidence: z18.enum(["verified", "speculation", "high-risk-unknown"]),
15448
+ needs_validation_by: z18.enum(["plan", "coder", "runtime"]).optional()
15167
15449
  });
15168
- var OpenIssueSchema = z17.union([
15169
- z17.string().min(1),
15170
- z17.object({
15171
- id: z17.string().optional(),
15172
- question: z17.string().min(1),
15173
- blocking: z17.boolean().optional()
15450
+ var OpenIssueSchema = z18.union([
15451
+ z18.string().min(1),
15452
+ z18.object({
15453
+ id: z18.string().optional(),
15454
+ question: z18.string().min(1),
15455
+ blocking: z18.boolean().optional()
15174
15456
  })
15175
15457
  ]);
15176
- var RedFlagsSchema = z17.object({
15177
- raised: z17.boolean(),
15178
- combos: z17.array(z17.enum(COMBO_VALUES)).default([]),
15179
- reasons: z17.array(z17.string()).default([]),
15180
- user_persisted_rounds: z17.number().int().nonnegative().default(0),
15181
- downstream_advisory: z17.string().nullable().optional()
15458
+ var RedFlagsSchema = z18.object({
15459
+ raised: z18.boolean(),
15460
+ combos: z18.array(z18.enum(COMBO_VALUES)).default([]),
15461
+ reasons: z18.array(z18.string()).default([]),
15462
+ user_persisted_rounds: z18.number().int().nonnegative().default(0),
15463
+ downstream_advisory: z18.string().nullable().optional()
15182
15464
  }).superRefine((rf, ctx) => {
15183
15465
  if (rf.raised) {
15184
15466
  if (rf.combos.length === 0) {
15185
15467
  ctx.addIssue({
15186
- code: z17.ZodIssueCode.custom,
15468
+ code: z18.ZodIssueCode.custom,
15187
15469
  message: "red_flags.raised=true 时 combos 必须 >=1",
15188
15470
  path: ["combos"]
15189
15471
  });
15190
15472
  }
15191
15473
  if (rf.reasons.length === 0) {
15192
15474
  ctx.addIssue({
15193
- code: z17.ZodIssueCode.custom,
15475
+ code: z18.ZodIssueCode.custom,
15194
15476
  message: "red_flags.raised=true 时 reasons 必须 >=1",
15195
15477
  path: ["reasons"]
15196
15478
  });
15197
15479
  }
15198
15480
  }
15199
15481
  });
15200
- var ScoresSchema = z17.object(Object.fromEntries(SCORE_DIMENSIONS.map((d) => [d, z17.number().min(0).max(1)])));
15201
- var PreCodingBlockerSchema = z17.object({
15202
- id: z17.string().regex(/^PRE-\d+$/, "id 必须形如 PRE-1 / PRE-2"),
15203
- blocker: z17.string().min(1),
15204
- source: z17.enum(["assumption", "red_flag", "open_issue"]),
15205
- must_resolve_by: z17.enum(["user", "codeforge"])
15482
+ var ScoresSchema = z18.object(Object.fromEntries(SCORE_DIMENSIONS.map((d) => [d, z18.number().min(0).max(1)])));
15483
+ var PreCodingBlockerSchema = z18.object({
15484
+ id: z18.string().regex(/^PRE-\d+$/, "id 必须形如 PRE-1 / PRE-2"),
15485
+ blocker: z18.string().min(1),
15486
+ source: z18.enum(["assumption", "red_flag", "open_issue"]),
15487
+ must_resolve_by: z18.enum(["user", "codeforge"])
15206
15488
  });
15207
- var KhRefSchema = z17.object({
15208
- kh_id: z17.string(),
15209
- title: z17.string(),
15210
- relevance: z17.enum(["positive", "negative", "neutral"]).optional()
15489
+ var KhRefSchema = z18.object({
15490
+ kh_id: z18.string(),
15491
+ title: z18.string(),
15492
+ relevance: z18.enum(["positive", "negative", "neutral"]).optional()
15211
15493
  });
15212
- var HandoffSchema = z17.object({
15213
- schema_version: z17.string().regex(/^\d+\.\d+\.\d+$/, "schema_version 必须形如 1.1.0 / 1.2.0"),
15214
- slug: z17.string().regex(SLUG_REGEX, "slug 仅允许 [a-z0-9-],长度 1-50,首字符为字母数字"),
15215
- title: z17.string().min(1).max(200),
15216
- created_at: z17.string().optional(),
15217
- discover_session_id: z17.string().optional(),
15218
- weighted_score: z17.number().min(0).max(1),
15494
+ var HandoffSchema = z18.object({
15495
+ schema_version: z18.string().regex(/^\d+\.\d+\.\d+$/, "schema_version 必须形如 1.1.0 / 1.2.0"),
15496
+ slug: z18.string().regex(SLUG_REGEX, "slug 仅允许 [a-z0-9-],长度 1-50,首字符为字母数字"),
15497
+ title: z18.string().min(1).max(200),
15498
+ created_at: z18.string().optional(),
15499
+ discover_session_id: z18.string().optional(),
15500
+ weighted_score: z18.number().min(0).max(1),
15219
15501
  scores: ScoresSchema,
15220
- needs: z17.array(NeedSchema).min(1, "needs 必须 >=1"),
15221
- boundaries: z17.array(BoundarySchema).min(1, "boundaries 必须 >=1"),
15222
- assumptions: z17.array(AssumptionSchema),
15223
- open_issues: z17.array(OpenIssueSchema).default([]),
15224
- rejected_alternatives: z17.array(z17.object({ option: z17.string(), reason: z17.string() })).default([]),
15225
- acceptance_criteria: z17.array(z17.object({
15226
- id: z17.string().regex(/^AC-/, "AC id 必须以 AC- 开头"),
15227
- description: z17.string().min(1),
15228
- measurable: z17.boolean().optional(),
15229
- metric: z17.string().optional()
15502
+ needs: z18.array(NeedSchema).min(1, "needs 必须 >=1"),
15503
+ boundaries: z18.array(BoundarySchema).min(1, "boundaries 必须 >=1"),
15504
+ assumptions: z18.array(AssumptionSchema),
15505
+ open_issues: z18.array(OpenIssueSchema).default([]),
15506
+ rejected_alternatives: z18.array(z18.object({ option: z18.string(), reason: z18.string() })).default([]),
15507
+ acceptance_criteria: z18.array(z18.object({
15508
+ id: z18.string().regex(/^AC-/, "AC id 必须以 AC- 开头"),
15509
+ description: z18.string().min(1),
15510
+ measurable: z18.boolean().optional(),
15511
+ metric: z18.string().optional()
15230
15512
  })).default([]),
15231
15513
  red_flags: RedFlagsSchema,
15232
- pre_coding_blockers: z17.array(PreCodingBlockerSchema).default([]),
15233
- kh_references: z17.array(KhRefSchema).default([]),
15234
- adr_refs: z17.array(z17.string()).default([]),
15235
- related_artifacts: z17.object({
15236
- prd: z17.string().optional(),
15237
- transcript: z17.string().optional()
15514
+ pre_coding_blockers: z18.array(PreCodingBlockerSchema).default([]),
15515
+ kh_references: z18.array(KhRefSchema).default([]),
15516
+ adr_refs: z18.array(z18.string()).default([]),
15517
+ related_artifacts: z18.object({
15518
+ prd: z18.string().optional(),
15519
+ transcript: z18.string().optional()
15238
15520
  }).optional()
15239
15521
  });
15240
15522
  function validateHandoff(rawYaml, fileSize) {
@@ -15260,9 +15542,9 @@ function validateHandoff(rawYaml, fileSize) {
15260
15542
  const result = HandoffSchema.safeParse(parsed);
15261
15543
  if (!result.success) {
15262
15544
  const first = result.error.issues[0];
15263
- const path20 = first?.path?.join(".") ?? "(root)";
15545
+ const path21 = first?.path?.join(".") ?? "(root)";
15264
15546
  const msg = first?.message ?? "unknown";
15265
- return { ok: false, reason: `schema 校验失败:${path20}: ${msg}` };
15547
+ return { ok: false, reason: `schema 校验失败:${path21}: ${msg}` };
15266
15548
  }
15267
15549
  return { ok: true, data: result.data, schemaVersion: result.data.schema_version };
15268
15550
  }
@@ -15279,7 +15561,7 @@ var SESSION_TTL_MS2 = 24 * 60 * 60 * 1000;
15279
15561
  var MATCH_THRESHOLD = 0.15;
15280
15562
  var MAX_CANDIDATES = 3;
15281
15563
  var NUDGE_MAX_LEN = 1500;
15282
- var SPECS_REL_DIR = join16("docs", "specs");
15564
+ var SPECS_REL_DIR = join17("docs", "specs");
15283
15565
  var sessionMap = new Map;
15284
15566
  function pruneIfOversize2() {
15285
15567
  while (sessionMap.size > SESSION_CAP2) {
@@ -15386,7 +15668,7 @@ function loadSpecs(rootDir, opts = {}) {
15386
15668
  const dirExists = opts.dirExists ?? defaultDirExists;
15387
15669
  const statReader = opts.statReader ?? defaultStatReader;
15388
15670
  const log6 = makePluginLogger(PLUGIN_NAME8);
15389
- const specsRoot = join16(rootDir, SPECS_REL_DIR);
15671
+ const specsRoot = join17(rootDir, SPECS_REL_DIR);
15390
15672
  const records = [];
15391
15673
  if (!dirExists(specsRoot)) {
15392
15674
  log6.info(`specs 目录不存在,plugin 将 no-op`, { specsRoot });
@@ -15407,7 +15689,7 @@ function loadSpecs(rootDir, opts = {}) {
15407
15689
  log6.info(`跳过非合法 slug 命名的条目`, { entry });
15408
15690
  continue;
15409
15691
  }
15410
- const specDir = join16(specsRoot, entry);
15692
+ const specDir = join17(specsRoot, entry);
15411
15693
  let dirStat;
15412
15694
  try {
15413
15695
  dirStat = statReader(specDir);
@@ -15420,7 +15702,7 @@ function loadSpecs(rootDir, opts = {}) {
15420
15702
  }
15421
15703
  if (!dirStat.isDirectory)
15422
15704
  continue;
15423
- const handoffPath = join16(specDir, "handoff.yaml");
15705
+ const handoffPath = join17(specDir, "handoff.yaml");
15424
15706
  let fileStat;
15425
15707
  try {
15426
15708
  fileStat = statReader(handoffPath);
@@ -15593,13 +15875,13 @@ var handler8 = discoverSpecSuggestServer;
15593
15875
 
15594
15876
  // lib/memories.ts
15595
15877
  import { promises as fs16 } from "node:fs";
15596
- import * as path20 from "node:path";
15597
- import * as os5 from "node:os";
15878
+ import * as path21 from "node:path";
15879
+ import * as os6 from "node:os";
15598
15880
  function resolveConfig(c) {
15599
15881
  return {
15600
15882
  projectRoot: c.projectRoot,
15601
- homeDir: c.homeDir ?? os5.homedir(),
15602
- projectName: c.projectName ?? path20.basename(c.projectRoot),
15883
+ homeDir: c.homeDir ?? os6.homedir(),
15884
+ projectName: c.projectName ?? path21.basename(c.projectRoot),
15603
15885
  now: c.now ?? Date.now,
15604
15886
  log: c.log ?? (() => {}),
15605
15887
  maxPerScope: c.maxPerScope ?? 1000
@@ -15607,9 +15889,9 @@ function resolveConfig(c) {
15607
15889
  }
15608
15890
  function fileFor(scope, cfg) {
15609
15891
  if (scope === "project") {
15610
- return path20.join(cfg.projectRoot, ".codeforge", "memories.json");
15892
+ return path21.join(cfg.projectRoot, ".codeforge", "memories.json");
15611
15893
  }
15612
- return path20.join(cfg.homeDir, ".codeforge", "memories.json");
15894
+ return path21.join(cfg.homeDir, ".codeforge", "memories.json");
15613
15895
  }
15614
15896
  async function readBank(p) {
15615
15897
  try {
@@ -15623,7 +15905,7 @@ async function readBank(p) {
15623
15905
  }
15624
15906
  }
15625
15907
  async function writeBank(p, items) {
15626
- await fs16.mkdir(path20.dirname(p), { recursive: true });
15908
+ await fs16.mkdir(path21.dirname(p), { recursive: true });
15627
15909
  const tmp = `${p}.tmp`;
15628
15910
  await fs16.writeFile(tmp, JSON.stringify(items, null, 2), "utf8");
15629
15911
  await fs16.rename(tmp, p);
@@ -16144,7 +16426,7 @@ var handler10 = modelFallbackServer;
16144
16426
 
16145
16427
  // plugins/subtask-heartbeat.ts
16146
16428
  import { promises as fsPromises } from "node:fs";
16147
- import * as path21 from "node:path";
16429
+ import * as path22 from "node:path";
16148
16430
  var recordSessionParent2 = recordSessionParent;
16149
16431
  var lookupParentSessionId2 = lookupParentSessionId;
16150
16432
  var deleteSessionParent2 = deleteSessionParent;
@@ -16268,11 +16550,11 @@ function extractTaskArgs(args) {
16268
16550
  const a = args;
16269
16551
  const rawDesc = typeof a["description"] === "string" ? a["description"] : null;
16270
16552
  const rawPrompt = typeof a["prompt"] === "string" ? a["prompt"] : null;
16271
- const description16 = rawDesc ?? (rawPrompt ? rawPrompt.slice(0, 60) : null);
16553
+ const description17 = rawDesc ?? (rawPrompt ? rawPrompt.slice(0, 60) : null);
16272
16554
  const subagentType = typeof a["subagent_type"] === "string" && a["subagent_type"] || typeof a["agent"] === "string" && a["agent"] || typeof a["agentType"] === "string" && a["agentType"] || typeof a["agent_type"] === "string" && a["agent_type"] || null;
16273
- if (!description16 && !subagentType)
16555
+ if (!description17 && !subagentType)
16274
16556
  return null;
16275
- return { description: description16, subagentType };
16557
+ return { description: description17, subagentType };
16276
16558
  }
16277
16559
  function enqueuePendingTask(parentID, entry, now = Date.now()) {
16278
16560
  const ts = entry.ts ?? now;
@@ -16491,7 +16773,7 @@ function buildErrorLogLine(errorReason, elapsedMs, now = Date.now()) {
16491
16773
  }
16492
16774
  async function appendSubagentLog(filePath, line, log7) {
16493
16775
  try {
16494
- await fsPromises.mkdir(path21.dirname(filePath), { recursive: true });
16776
+ await fsPromises.mkdir(path22.dirname(filePath), { recursive: true });
16495
16777
  await fsPromises.appendFile(filePath, line + `
16496
16778
  `, "utf8");
16497
16779
  } catch (err) {
@@ -16859,8 +17141,8 @@ var handler12 = parallelStatusServer;
16859
17141
 
16860
17142
  // plugins/parallel-tool-nudge.ts
16861
17143
  import { readFileSync as readFileSync4, readdirSync as readdirSync2, statSync as statSync3 } from "node:fs";
16862
- import { join as join18 } from "node:path";
16863
- import { homedir as homedir6 } from "node:os";
17144
+ import { join as join19 } from "node:path";
17145
+ import { homedir as homedir7 } from "node:os";
16864
17146
  var PLUGIN_NAME13 = "parallel-tool-nudge";
16865
17147
  logLifecycle(PLUGIN_NAME13, "import", {});
16866
17148
  var PARALLEL_SAFE_TOOLS = [
@@ -16912,10 +17194,10 @@ function loadAgentToolsMap(rootDir, opts = {}) {
16912
17194
  const reader = opts.reader ?? defaultReader2;
16913
17195
  const dirReader = opts.dirReader ?? defaultDirReader2;
16914
17196
  const dirExists = opts.dirExists ?? defaultDirExists2;
16915
- const homeAgentsDir = opts.homeAgentsDir ?? join18(homedir6(), ".config", "opencode", "agents");
17197
+ const homeAgentsDir = opts.homeAgentsDir ?? join19(homedir7(), ".config", "opencode", "agents");
16916
17198
  const candidateDirs = [
16917
- join18(rootDir, ".codeforge", "agents"),
16918
- join18(rootDir, "agents"),
17199
+ join19(rootDir, ".codeforge", "agents"),
17200
+ join19(rootDir, "agents"),
16919
17201
  homeAgentsDir
16920
17202
  ];
16921
17203
  const result = new Map;
@@ -16938,20 +17220,20 @@ function loadAgentToolsMap(rootDir, opts = {}) {
16938
17220
  for (const entry of entries) {
16939
17221
  if (!entry.endsWith(".md"))
16940
17222
  continue;
16941
- const path22 = join18(dir, entry);
17223
+ const path23 = join19(dir, entry);
16942
17224
  let content;
16943
17225
  try {
16944
- content = reader(path22);
17226
+ content = reader(path23);
16945
17227
  } catch (err) {
16946
17228
  log8.warn(`agent.md 读取失败(已跳过)`, {
16947
- path: path22,
17229
+ path: path23,
16948
17230
  error: err instanceof Error ? err.message : String(err)
16949
17231
  });
16950
17232
  continue;
16951
17233
  }
16952
17234
  const parsed = parseAgentFrontmatter(content);
16953
17235
  if (!parsed) {
16954
- log8.warn(`agent frontmatter 解析失败(已跳过)`, { path: path22 });
17236
+ log8.warn(`agent frontmatter 解析失败(已跳过)`, { path: path23 });
16955
17237
  continue;
16956
17238
  }
16957
17239
  if (result.has(parsed.name))
@@ -17111,7 +17393,7 @@ function prependUtf8Prelude(command) {
17111
17393
  return command;
17112
17394
  return PRELUDE + command;
17113
17395
  }
17114
- var handler14 = async (_ctx3) => {
17396
+ var handler14 = async (_ctx4) => {
17115
17397
  const enabled = process.platform === "win32" && process.env.CODEFORGE_DISABLE_PWSH_UTF8 !== "1";
17116
17398
  const reason = enabled ? "win32" : process.platform !== "win32" ? "non-win32" : "disabled-by-env";
17117
17399
  logLifecycle(PLUGIN_NAME14, "activate", { enabled, platform: process.platform, reason });
@@ -17143,7 +17425,7 @@ var handler14 = async (_ctx3) => {
17143
17425
 
17144
17426
  // lib/event-stream.ts
17145
17427
  import { promises as fs17 } from "node:fs";
17146
- import * as path22 from "node:path";
17428
+ import * as path23 from "node:path";
17147
17429
  async function loadSession(id, opts = {}) {
17148
17430
  const file = resolveSessionFile(id, opts);
17149
17431
  const raw = await fs17.readFile(file, "utf8");
@@ -17163,7 +17445,7 @@ async function listSessions(opts = {}) {
17163
17445
  for (const e of entries) {
17164
17446
  if (!e.isFile() || !e.name.endsWith(".jsonl"))
17165
17447
  continue;
17166
- const file = path22.join(dir, e.name);
17448
+ const file = path23.join(dir, e.name);
17167
17449
  const id = e.name.replace(/\.jsonl$/, "");
17168
17450
  try {
17169
17451
  const stat = await fs17.stat(file);
@@ -17190,11 +17472,11 @@ async function listSessions(opts = {}) {
17190
17472
  return out;
17191
17473
  }
17192
17474
  function resolveDir(opts = {}) {
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");
17475
+ const root = path23.resolve(opts.root ?? process.cwd());
17476
+ return opts.sessions_dir ? path23.resolve(root, opts.sessions_dir) : path23.join(runtimeDir(root), "sessions");
17195
17477
  }
17196
17478
  function resolveSessionFile(id, opts = {}) {
17197
- return path22.join(resolveDir(opts), `${id}.jsonl`);
17479
+ return path23.join(resolveDir(opts), `${id}.jsonl`);
17198
17480
  }
17199
17481
  function parseJsonl(id, raw) {
17200
17482
  const events = [];
@@ -17458,10 +17740,10 @@ function isRecoveryWorthShowing(plan) {
17458
17740
 
17459
17741
  // lib/block-pending.ts
17460
17742
  import { promises as fs18 } from "node:fs";
17461
- import * as path23 from "node:path";
17743
+ import * as path24 from "node:path";
17462
17744
  function blockPendingFilePath(absRoot) {
17463
17745
  const rd = runtimeDir(absRoot, { ensure: false });
17464
- return path23.join(rd, "sessions", "autonomous-blocks.ndjson");
17746
+ return path24.join(rd, "sessions", "autonomous-blocks.ndjson");
17465
17747
  }
17466
17748
  function consumeLockPath(absRoot) {
17467
17749
  return blockPendingFilePath(absRoot) + ".consume.lock";
@@ -17526,7 +17808,7 @@ async function markBlocksConsumed(absRoot, entries) {
17526
17808
  if (entries.length === 0)
17527
17809
  return;
17528
17810
  const file = blockPendingFilePath(absRoot);
17529
- await fs18.mkdir(path23.dirname(file), { recursive: true });
17811
+ await fs18.mkdir(path24.dirname(file), { recursive: true });
17530
17812
  const now = new Date().toISOString();
17531
17813
  const lines = entries.map((e) => ({
17532
17814
  type: "consume",
@@ -17676,7 +17958,7 @@ var handler15 = sessionRecoveryServer;
17676
17958
 
17677
17959
  // plugins/subtasks.ts
17678
17960
  import { promises as fs19 } from "node:fs";
17679
- import * as path24 from "node:path";
17961
+ import * as path25 from "node:path";
17680
17962
 
17681
17963
  // lib/parallel-merge.ts
17682
17964
  init_worktree_ops();
@@ -18291,7 +18573,7 @@ function buildSystemPrompt(maxSubtasks) {
18291
18573
  ].join(`
18292
18574
  `);
18293
18575
  }
18294
- async function decomposeTask(description16, opts) {
18576
+ async function decomposeTask(description17, opts) {
18295
18577
  const log10 = opts.log ?? (() => {});
18296
18578
  if (opts.mockResponse) {
18297
18579
  return validateAndFinalize(opts.mockResponse, undefined, log10, opts.maxSubtasks ?? DEFAULT_MAX_SUBTASKS);
@@ -18301,7 +18583,7 @@ async function decomposeTask(description16, opts) {
18301
18583
  let childSessionId;
18302
18584
  try {
18303
18585
  const created = await opts.client.session.create({
18304
- body: { title: `decompose:${clip5(description16, 60)}` },
18586
+ body: { title: `decompose:${clip5(description17, 60)}` },
18305
18587
  query: opts.directory ? { directory: opts.directory } : undefined
18306
18588
  });
18307
18589
  if (created.error || !created.data?.id) {
@@ -18318,7 +18600,7 @@ async function decomposeTask(description16, opts) {
18318
18600
  path: { id: childSessionId },
18319
18601
  body: {
18320
18602
  system: systemText,
18321
- parts: [{ type: "text", text: description16 }]
18603
+ parts: [{ type: "text", text: description17 }]
18322
18604
  },
18323
18605
  query: opts.directory ? { directory: opts.directory } : undefined
18324
18606
  }));
@@ -18500,7 +18782,7 @@ function sleep2(ms) {
18500
18782
  // plugins/subtasks.ts
18501
18783
  var PLUGIN_NAME16 = "subtasks";
18502
18784
  function getLogFile(root = process.cwd()) {
18503
- return path24.join(runtimeDir(root), "logs", "subtasks.log");
18785
+ return path25.join(runtimeDir(root), "logs", "subtasks.log");
18504
18786
  }
18505
18787
  var VERB_RE = /^([a-zA-Z]{3,12})/;
18506
18788
  var CN_VERBS = [
@@ -18805,7 +19087,7 @@ async function writeLog(level, msg, data) {
18805
19087
  `;
18806
19088
  try {
18807
19089
  const logFile = getLogFile();
18808
- await fs19.mkdir(path24.dirname(logFile), { recursive: true });
19090
+ await fs19.mkdir(path25.dirname(logFile), { recursive: true });
18809
19091
  await fs19.appendFile(logFile, line, "utf8");
18810
19092
  } catch {}
18811
19093
  }
@@ -18822,8 +19104,8 @@ var subtasksServer = async (ctx) => {
18822
19104
  try {
18823
19105
  if (input?.command !== "parallel")
18824
19106
  return;
18825
- const description16 = (input.arguments ?? "").trim();
18826
- if (!description16) {
19107
+ const description17 = (input.arguments ?? "").trim();
19108
+ if (!description17) {
18827
19109
  return;
18828
19110
  }
18829
19111
  let autoMerge = false;
@@ -18862,7 +19144,7 @@ var subtasksServer = async (ctx) => {
18862
19144
  } : undefined;
18863
19145
  const replyLines = [];
18864
19146
  const messageCtx = {
18865
- content: `/parallel ${description16}`,
19147
+ content: `/parallel ${description17}`,
18866
19148
  reply: (s) => {
18867
19149
  replyLines.push(s);
18868
19150
  return Promise.resolve();
@@ -19438,7 +19720,7 @@ var handler18 = tokenManagerServer;
19438
19720
 
19439
19721
  // plugins/tool-policy.ts
19440
19722
  import { promises as fs20 } from "node:fs";
19441
- import * as path26 from "node:path";
19723
+ import * as path27 from "node:path";
19442
19724
 
19443
19725
  // lib/tool-risk.ts
19444
19726
  var RISK_PATTERNS = [
@@ -19592,7 +19874,7 @@ function buildHaystackFor(args, matchOn) {
19592
19874
  }
19593
19875
 
19594
19876
  // lib/file-regex-acl.ts
19595
- import * as path25 from "node:path";
19877
+ import * as path26 from "node:path";
19596
19878
  function compileRule(r) {
19597
19879
  if (r instanceof RegExp)
19598
19880
  return r;
@@ -19658,7 +19940,7 @@ function normalizePath2(p) {
19658
19940
  let s = p.replace(/\\/g, "/");
19659
19941
  if (s.startsWith("./"))
19660
19942
  s = s.slice(2);
19661
- s = path25.posix.normalize(s);
19943
+ s = path26.posix.normalize(s);
19662
19944
  return s;
19663
19945
  }
19664
19946
  function checkFileAccess(acl, file, op) {
@@ -19761,9 +20043,9 @@ function decideToolCall(ctx, cfg = {}, currentAgent) {
19761
20043
  const action = risks.length > 0 || worstAcl === "deny" ? "deny" : "allow";
19762
20044
  return { action, reasons, risks, acl: aclResults };
19763
20045
  }
19764
- var POLICY_PATH = path26.join(".codeforge", "policy.json");
20046
+ var POLICY_PATH = path27.join(".codeforge", "policy.json");
19765
20047
  async function loadPolicy(root = process.cwd()) {
19766
- const file = path26.join(root, POLICY_PATH);
20048
+ const file = path27.join(root, POLICY_PATH);
19767
20049
  try {
19768
20050
  const raw = await fs20.readFile(file, "utf8");
19769
20051
  const data = JSON.parse(raw);
@@ -19862,8 +20144,8 @@ var handler19 = toolPolicyServer;
19862
20144
 
19863
20145
  // plugins/update-checker.ts
19864
20146
  import { existsSync as existsSync6, rmSync } from "node:fs";
19865
- import { homedir as homedir8 } from "node:os";
19866
- import { join as join24 } from "node:path";
20147
+ import { homedir as homedir9 } from "node:os";
20148
+ import { join as join25 } from "node:path";
19867
20149
  import { spawnSync as spawnSync2 } from "node:child_process";
19868
20150
 
19869
20151
  // lib/update-checker-impl.ts
@@ -19880,8 +20162,8 @@ import {
19880
20162
  unlinkSync,
19881
20163
  writeFileSync as writeFileSync2
19882
20164
  } from "node:fs";
19883
- import { homedir as homedir7, tmpdir } from "node:os";
19884
- import { dirname as dirname14, join as join23 } from "node:path";
20165
+ import { homedir as homedir8, tmpdir } from "node:os";
20166
+ import { dirname as dirname14, join as join24 } from "node:path";
19885
20167
  import { fileURLToPath as fileURLToPath2 } from "node:url";
19886
20168
  import * as https from "node:https";
19887
20169
  import * as zlib from "node:zlib";
@@ -19889,7 +20171,7 @@ import * as zlib from "node:zlib";
19889
20171
  // lib/version-injected.ts
19890
20172
  function getInjectedVersion() {
19891
20173
  try {
19892
- const v = "0.6.7";
20174
+ const v = "0.6.12";
19893
20175
  if (typeof v === "string" && /^\d+\.\d+\.\d+/.test(v)) {
19894
20176
  return v;
19895
20177
  }
@@ -19979,17 +20261,17 @@ function readLocalVersion() {
19979
20261
  try {
19980
20262
  const here = fileURLToPath2(import.meta.url);
19981
20263
  const root = dirname14(dirname14(here));
19982
- const pkg = JSON.parse(readFileSync5(join23(root, "package.json"), "utf8"));
20264
+ const pkg = JSON.parse(readFileSync5(join24(root, "package.json"), "utf8"));
19983
20265
  return typeof pkg.version === "string" ? pkg.version : "0.0.0";
19984
20266
  } catch {
19985
20267
  return "0.0.0";
19986
20268
  }
19987
20269
  }
19988
20270
  function defaultCacheDir() {
19989
- return process.env["CODEFORGE_CACHE_DIR"] ?? join23(homedir7(), ".cache", "codeforge");
20271
+ return process.env["CODEFORGE_CACHE_DIR"] ?? join24(homedir8(), ".cache", "codeforge");
19990
20272
  }
19991
20273
  function defaultCacheFile() {
19992
- return join23(defaultCacheDir(), "update-check.json");
20274
+ return join24(defaultCacheDir(), "update-check.json");
19993
20275
  }
19994
20276
  function readCache(file) {
19995
20277
  try {
@@ -20145,14 +20427,14 @@ function defaultHttpFetcher(url2, timeoutMs) {
20145
20427
  });
20146
20428
  }
20147
20429
  async function downloadAndExtractBundle(opts) {
20148
- const tmpRoot = opts.tmpDir ?? mkdtempSync(join23(tmpdir(), "codeforge-update-"));
20430
+ const tmpRoot = opts.tmpDir ?? mkdtempSync(join24(tmpdir(), "codeforge-update-"));
20149
20431
  mkdirSync3(tmpRoot, { recursive: true });
20150
20432
  const fetcher = opts.tarballFetcher ?? defaultBinaryFetcher;
20151
20433
  const tarballBuf = await fetcher(opts.tarballUrl);
20152
20434
  verifyIntegrity(tarballBuf, opts.expectedIntegrity);
20153
20435
  const tarBuf = zlib.gunzipSync(tarballBuf);
20154
20436
  extractTarToDir(tarBuf, tmpRoot);
20155
- const bundlePath = join23(tmpRoot, "package", "dist", "index.js");
20437
+ const bundlePath = join24(tmpRoot, "package", "dist", "index.js");
20156
20438
  if (!existsSync5(bundlePath)) {
20157
20439
  throw new Error(`bundle_not_found: ${bundlePath}`);
20158
20440
  }
@@ -20192,11 +20474,11 @@ function extractTarToDir(tarBuf, destRoot) {
20192
20474
  offset += 512;
20193
20475
  if (typeFlag === "0" || typeFlag === "" || typeFlag === "\x00") {
20194
20476
  const fileBuf = tarBuf.subarray(offset, offset + size);
20195
- const dest = join23(destRoot, fullName);
20477
+ const dest = join24(destRoot, fullName);
20196
20478
  mkdirSync3(dirname14(dest), { recursive: true });
20197
20479
  writeFileSync2(dest, fileBuf);
20198
20480
  } else if (typeFlag === "5") {
20199
- mkdirSync3(join23(destRoot, fullName), { recursive: true });
20481
+ mkdirSync3(join24(destRoot, fullName), { recursive: true });
20200
20482
  }
20201
20483
  offset += Math.ceil(size / 512) * 512;
20202
20484
  }
@@ -20305,7 +20587,7 @@ function cleanupOldBackups(target, keep) {
20305
20587
  const base = target.substring(dir.length + 1);
20306
20588
  const prefix = `${base}.bak.`;
20307
20589
  const all = readdirSync3(dir).filter((f) => f.startsWith(prefix)).map((f) => {
20308
- const full = join23(dir, f);
20590
+ const full = join24(dir, f);
20309
20591
  let mtimeMs = 0;
20310
20592
  try {
20311
20593
  mtimeMs = statSync4(full).mtimeMs;
@@ -20327,7 +20609,7 @@ function loadCompatibility(opts) {
20327
20609
  const root = opts?.cwd ?? inferPluginRoot();
20328
20610
  if (!root)
20329
20611
  return null;
20330
- file = join23(root, "compatibility.json");
20612
+ file = join24(root, "compatibility.json");
20331
20613
  }
20332
20614
  if (!existsSync5(file))
20333
20615
  return null;
@@ -20518,11 +20800,11 @@ var updateCheckerServer = async (ctx) => {
20518
20800
  expectedIntegrity: npmResult.integrity
20519
20801
  });
20520
20802
  try {
20521
- const installMjs = join24(extractDir, "package", "install.mjs");
20803
+ const installMjs = join25(extractDir, "package", "install.mjs");
20522
20804
  if (existsSync6(installMjs)) {
20523
20805
  const nodeBin = resolveNodeBin();
20524
20806
  const r = spawnSync2(nodeBin, [installMjs, "--global", "--skip-build"], {
20525
- cwd: join24(extractDir, "package"),
20807
+ cwd: join25(extractDir, "package"),
20526
20808
  stdio: "pipe",
20527
20809
  encoding: "utf8"
20528
20810
  });
@@ -20623,14 +20905,14 @@ function detectOpencodeVersion() {
20623
20905
  }
20624
20906
  function getOpencodeBundlePath() {
20625
20907
  const candidates = [];
20626
- candidates.push(join24(homedir8(), ".config", "opencode", "codeforge", "index.js"));
20908
+ candidates.push(join25(homedir9(), ".config", "opencode", "codeforge", "index.js"));
20627
20909
  if (process.platform === "win32") {
20628
20910
  const appData = process.env["APPDATA"];
20629
20911
  if (appData)
20630
- candidates.push(join24(appData, "opencode", "codeforge", "index.js"));
20912
+ candidates.push(join25(appData, "opencode", "codeforge", "index.js"));
20631
20913
  const localAppData = process.env["LOCALAPPDATA"];
20632
20914
  if (localAppData)
20633
- candidates.push(join24(localAppData, "opencode", "codeforge", "index.js"));
20915
+ candidates.push(join25(localAppData, "opencode", "codeforge", "index.js"));
20634
20916
  }
20635
20917
  for (const c of candidates) {
20636
20918
  if (existsSync6(c))
@@ -20691,60 +20973,60 @@ async function postToast(ctx, message) {
20691
20973
  var handler20 = updateCheckerServer;
20692
20974
 
20693
20975
  // plugins/workflow-engine.ts
20694
- import * as path28 from "node:path";
20976
+ import * as path29 from "node:path";
20695
20977
 
20696
20978
  // lib/workflow-loader.ts
20697
20979
  import { promises as fs21 } from "node:fs";
20698
- import * as path27 from "node:path";
20699
- import { z as z18 } from "zod";
20700
- var ActionSchema = z18.object({
20701
- tool: z18.string().min(1, "action.tool 不能为空"),
20702
- args: z18.record(z18.string(), z18.unknown()).optional().default({}),
20703
- on_error: z18.enum(["retry", "skip", "abort"]).optional()
20980
+ import * as path28 from "node:path";
20981
+ import { z as z19 } from "zod";
20982
+ var ActionSchema = z19.object({
20983
+ tool: z19.string().min(1, "action.tool 不能为空"),
20984
+ args: z19.record(z19.string(), z19.unknown()).optional().default({}),
20985
+ on_error: z19.enum(["retry", "skip", "abort"]).optional()
20704
20986
  });
20705
- var StepSchema = z18.object({
20706
- name: z18.string().min(1, "step.name 不能为空"),
20707
- agent: z18.string().min(1, "step.agent 不能为空").describe("agent 名(与 agents/<name>.md 对应)"),
20708
- description: z18.string().optional(),
20709
- inject_context: z18.record(z18.string(), z18.unknown()).optional(),
20710
- requires_human_approval: z18.boolean().optional().default(false),
20711
- actions: z18.array(ActionSchema).optional().default([]),
20712
- on_error: z18.enum(["retry", "skip", "abort"]).optional().default("abort"),
20713
- max_retries: z18.number().int().min(0).max(10).optional().default(2),
20714
- timeout: z18.string().regex(/^\d+(?:ms|s|m|h)$/, "timeout 必须是 数字+单位(ms/s/m/h),如 5m").optional(),
20715
- auto_feedback: z18.object({
20716
- test_cmd: z18.string().optional().describe("测试命令,如 npm test"),
20717
- lint_cmd: z18.string().optional().describe("lint 命令,如 npm run lint"),
20718
- max_retries: z18.number().int().min(1).max(10).optional().default(3),
20719
- error_excerpt_lines: z18.number().int().min(1).max(50).optional().default(5),
20720
- escalate_to: z18.string().optional().default("reviewer").describe("超上限后兜底 agent,默认 reviewer")
20987
+ var StepSchema = z19.object({
20988
+ name: z19.string().min(1, "step.name 不能为空"),
20989
+ agent: z19.string().min(1, "step.agent 不能为空").describe("agent 名(与 agents/<name>.md 对应)"),
20990
+ description: z19.string().optional(),
20991
+ inject_context: z19.record(z19.string(), z19.unknown()).optional(),
20992
+ requires_human_approval: z19.boolean().optional().default(false),
20993
+ actions: z19.array(ActionSchema).optional().default([]),
20994
+ on_error: z19.enum(["retry", "skip", "abort"]).optional().default("abort"),
20995
+ max_retries: z19.number().int().min(0).max(10).optional().default(2),
20996
+ timeout: z19.string().regex(/^\d+(?:ms|s|m|h)$/, "timeout 必须是 数字+单位(ms/s/m/h),如 5m").optional(),
20997
+ auto_feedback: z19.object({
20998
+ test_cmd: z19.string().optional().describe("测试命令,如 npm test"),
20999
+ lint_cmd: z19.string().optional().describe("lint 命令,如 npm run lint"),
21000
+ max_retries: z19.number().int().min(1).max(10).optional().default(3),
21001
+ error_excerpt_lines: z19.number().int().min(1).max(50).optional().default(5),
21002
+ escalate_to: z19.string().optional().default("reviewer").describe("超上限后兜底 agent,默认 reviewer")
20721
21003
  }).refine((d) => Boolean(d.test_cmd || d.lint_cmd), { message: "auto_feedback 必须至少配置 test_cmd 或 lint_cmd 之一" }).optional(),
20722
- on_decision: z18.object({
20723
- APPROVE: z18.union([
20724
- z18.literal("continue"),
20725
- z18.literal("abort"),
20726
- z18.object({ action: z18.literal("goto"), target: z18.string().min(1) })
21004
+ on_decision: z19.object({
21005
+ APPROVE: z19.union([
21006
+ z19.literal("continue"),
21007
+ z19.literal("abort"),
21008
+ z19.object({ action: z19.literal("goto"), target: z19.string().min(1) })
20727
21009
  ]).optional(),
20728
- REQUEST_CHANGES: z18.union([
20729
- z18.literal("continue"),
20730
- z18.literal("abort"),
20731
- z18.object({ action: z18.literal("goto"), target: z18.string().min(1) })
21010
+ REQUEST_CHANGES: z19.union([
21011
+ z19.literal("continue"),
21012
+ z19.literal("abort"),
21013
+ z19.object({ action: z19.literal("goto"), target: z19.string().min(1) })
20732
21014
  ]).optional(),
20733
- BLOCK: z18.union([
20734
- z18.literal("continue"),
20735
- z18.literal("abort"),
20736
- z18.object({ action: z18.literal("goto"), target: z18.string().min(1) })
21015
+ BLOCK: z19.union([
21016
+ z19.literal("continue"),
21017
+ z19.literal("abort"),
21018
+ z19.object({ action: z19.literal("goto"), target: z19.string().min(1) })
20737
21019
  ]).optional()
20738
21020
  }).refine((d) => Boolean(d.APPROVE || d.REQUEST_CHANGES || d.BLOCK), { message: "on_decision 必须至少配置 APPROVE / REQUEST_CHANGES / BLOCK 之一" }).optional()
20739
21021
  }).strict();
20740
- var WorkflowSchema = z18.object({
20741
- name: z18.string().min(1, "workflow.name 不能为空"),
20742
- description: z18.string().optional().default(""),
20743
- version: z18.string().optional().default("1.0.0"),
20744
- trigger: z18.string().min(1).refine((v) => /^\/[a-z][\w-]*$/.test(v) || /^event:[a-z][\w.-]+$/.test(v), "trigger 必须是 /command-name 或 event:xxx 形式"),
20745
- context_template: z18.string().optional(),
20746
- max_loops: z18.number().int().min(1).max(10).optional().default(3),
20747
- steps: z18.array(StepSchema).min(1, "workflow.steps 不能为空")
21022
+ var WorkflowSchema = z19.object({
21023
+ name: z19.string().min(1, "workflow.name 不能为空"),
21024
+ description: z19.string().optional().default(""),
21025
+ version: z19.string().optional().default("1.0.0"),
21026
+ trigger: z19.string().min(1).refine((v) => /^\/[a-z][\w-]*$/.test(v) || /^event:[a-z][\w.-]+$/.test(v), "trigger 必须是 /command-name 或 event:xxx 形式"),
21027
+ context_template: z19.string().optional(),
21028
+ max_loops: z19.number().int().min(1).max(10).optional().default(3),
21029
+ steps: z19.array(StepSchema).min(1, "workflow.steps 不能为空")
20748
21030
  }).strict();
20749
21031
  function parseWorkflowYaml(yaml, sourcePath = "<inline>") {
20750
21032
  let raw;
@@ -20808,7 +21090,7 @@ async function loadWorkflowsFromDir(dir) {
20808
21090
  continue;
20809
21091
  if (!/\.ya?ml$/i.test(name))
20810
21092
  continue;
20811
- const full = path27.join(dir, name);
21093
+ const full = path28.join(dir, name);
20812
21094
  const r = await loadWorkflowFromFile(full);
20813
21095
  if (r.ok)
20814
21096
  loaded.push(r);
@@ -21198,7 +21480,7 @@ async function handleCommandInvoked(raw, workflowsDir = "workflows") {
21198
21480
  }
21199
21481
  var workflowEngineServer = async (ctx) => {
21200
21482
  const directory = ctx.directory ?? process.cwd();
21201
- const workflowsDir = path28.join(directory, "workflows");
21483
+ const workflowsDir = path29.join(directory, "workflows");
21202
21484
  ensureRegistry(workflowsDir).catch((err) => fallbackLog2.warn(`[${PLUGIN_NAME21}] preload workflows failed`, {
21203
21485
  error: err instanceof Error ? err.message : String(err)
21204
21486
  }));
@@ -21242,7 +21524,7 @@ var workflowEngineServer = async (ctx) => {
21242
21524
  var handler21 = workflowEngineServer;
21243
21525
 
21244
21526
  // plugins/session-worktree-guard.ts
21245
- import path29 from "node:path";
21527
+ import path30 from "node:path";
21246
21528
  import { stat } from "node:fs/promises";
21247
21529
  var PLUGIN_NAME22 = "session-worktree-guard";
21248
21530
  logLifecycle(PLUGIN_NAME22, "import", {});
@@ -21278,6 +21560,31 @@ function buildGitVcsWriteRegex(mainRoot) {
21278
21560
  var WRITE_TOOLS = new Set(["write", "edit", "ast_edit"]);
21279
21561
  var TOUCH_THROTTLE_MS = 5 * 60000;
21280
21562
  var _touchCache = new Map;
21563
+ var REWRITE_NOTICE_CAP = 200;
21564
+ var _rewriteNotices = new Map;
21565
+ function recordRewriteNotice(callID, notice) {
21566
+ if (!_rewriteNotices.has(callID) && _rewriteNotices.size >= REWRITE_NOTICE_CAP) {
21567
+ const oldest = _rewriteNotices.keys().next().value;
21568
+ if (oldest !== undefined)
21569
+ _rewriteNotices.delete(oldest);
21570
+ }
21571
+ _rewriteNotices.set(callID, notice);
21572
+ }
21573
+ function consumeRewriteNotice(callID) {
21574
+ const notice = _rewriteNotices.get(callID);
21575
+ if (!notice)
21576
+ return;
21577
+ _rewriteNotices.delete(callID);
21578
+ return notice;
21579
+ }
21580
+ function formatRewriteWarning(notice) {
21581
+ const lines = notice.rewrites.map((r) => ` - ${r.field}: ${r.before} → ${r.after}`).join(`
21582
+ `);
21583
+ return `⚠️ [codeforge worktree 隔离] 本次 ${notice.tool} 的写入路径已被自动重定向到 session worktree,文件**不在主仓**:
21584
+ ` + `${lines}
21585
+ ` + `worktree=${notice.worktreePath}
21586
+ ` + `如需进主仓,请走 /merge(review-fix-review 闭环)。直接读主仓路径看不到本次改动。`;
21587
+ }
21281
21588
  var _sessionIdMissingWarned = false;
21282
21589
  var _bindFailNotified = new Set;
21283
21590
  function formatLazyBindDenyReason(input) {
@@ -21354,23 +21661,23 @@ var MERGE_CALLER_WHITELIST = new Set([
21354
21661
  var FORCE_MERGE_CALLER_WHITELIST = new Set([
21355
21662
  "codeforge"
21356
21663
  ]);
21357
- var CODEFORGE_WORKTREE_DIR_NAME = path29.join(".git", "codeforge-worktrees");
21664
+ var CODEFORGE_WORKTREE_DIR_NAME = path30.join(".git", "codeforge-worktrees");
21358
21665
  function worktreesRoot(mainRoot) {
21359
- return path29.join(mainRoot, CODEFORGE_WORKTREE_DIR_NAME);
21666
+ return path30.join(mainRoot, CODEFORGE_WORKTREE_DIR_NAME);
21360
21667
  }
21361
21668
  function isInsideAnyWorktreeDir(absPath, mainRoot) {
21362
- if (!path29.isAbsolute(absPath))
21669
+ if (!path30.isAbsolute(absPath))
21363
21670
  return false;
21364
21671
  const root = worktreesRoot(mainRoot);
21365
21672
  if (absPath === root)
21366
21673
  return false;
21367
- const prefix = root.endsWith(path29.sep) ? root : root + path29.sep;
21674
+ const prefix = root.endsWith(path30.sep) ? root : root + path30.sep;
21368
21675
  return absPath.startsWith(prefix);
21369
21676
  }
21370
21677
  function rewritePath(value, mainRoot, worktreeRoot) {
21371
21678
  if (!value)
21372
21679
  return null;
21373
- const resolved = path29.isAbsolute(value) ? value : path29.resolve(mainRoot, value);
21680
+ const resolved = path30.isAbsolute(value) ? value : path30.resolve(mainRoot, value);
21374
21681
  const wtPrefix2 = worktreeRoot.endsWith("/") ? worktreeRoot : worktreeRoot + "/";
21375
21682
  if (resolved === worktreeRoot || resolved.startsWith(wtPrefix2)) {
21376
21683
  return null;
@@ -21408,7 +21715,7 @@ function commandContainsMainRootExcludingWorktree(command, mainRoot, worktreePat
21408
21715
  }
21409
21716
  }
21410
21717
  const wtRoot = worktreesRoot(mainRoot);
21411
- const wtRootPrefix = wtRoot + path29.sep;
21718
+ const wtRootPrefix = wtRoot + path30.sep;
21412
21719
  const escapedWtRootPrefix = escapeRegex(wtRootPrefix);
21413
21720
  const wtPathPattern = escapedWtRootPrefix + `[^\\s'"\\x60)]*`;
21414
21721
  const allWorktreePathsReForEscape = new RegExp(wtPathPattern, "g");
@@ -21463,8 +21770,8 @@ function collectWritePaths(toolName, argsObj, worktreeRoot) {
21463
21770
  const candidate = toolName === "write" || toolName === "edit" ? argsObj["filePath"] : toolName === "ast_edit" ? argsObj["target"] : undefined;
21464
21771
  if (typeof candidate !== "string" || candidate.length === 0)
21465
21772
  return out;
21466
- const abs = path29.isAbsolute(candidate) ? candidate : path29.resolve(worktreeRoot, candidate);
21467
- const rel = path29.relative(worktreeRoot, abs).split(path29.sep).join("/");
21773
+ const abs = path30.isAbsolute(candidate) ? candidate : path30.resolve(worktreeRoot, candidate);
21774
+ const rel = path30.relative(worktreeRoot, abs).split(path30.sep).join("/");
21468
21775
  out.push(rel);
21469
21776
  return out;
21470
21777
  }
@@ -21475,7 +21782,7 @@ async function isCodeforgeManagedProject(mainRoot, opts = {}) {
21475
21782
  if (force === "1" || force === "true" || force === "yes")
21476
21783
  return true;
21477
21784
  try {
21478
- const st = await stat(path29.join(mainRoot, ".codeforge"));
21785
+ const st = await stat(path30.join(mainRoot, ".codeforge"));
21479
21786
  return st.isDirectory();
21480
21787
  } catch {
21481
21788
  return false;
@@ -21489,6 +21796,37 @@ function resolveMainRoot2(rawDir) {
21489
21796
  }
21490
21797
  return rawDir;
21491
21798
  }
21799
+ var STALE_MERGE_HEAD_MS = 24 * 60 * 60000;
21800
+ var _staleMergeHeadWarned = false;
21801
+ var _mergeBypassToastShown = false;
21802
+ async function isMainRepoMidMerge(mainRoot, opts = {}) {
21803
+ const gitDir = path30.join(mainRoot, ".git");
21804
+ const markers = [
21805
+ path30.join(gitDir, "MERGE_HEAD"),
21806
+ path30.join(gitDir, "rebase-merge"),
21807
+ path30.join(gitDir, "CHERRY_PICK_HEAD")
21808
+ ];
21809
+ let freshest = -1;
21810
+ for (const m of markers) {
21811
+ try {
21812
+ const st = await stat(m);
21813
+ if (st.mtimeMs > freshest)
21814
+ freshest = st.mtimeMs;
21815
+ } catch {}
21816
+ }
21817
+ if (freshest < 0)
21818
+ return false;
21819
+ const now = opts.now ?? Date.now();
21820
+ const staleMs = opts.staleMs ?? STALE_MERGE_HEAD_MS;
21821
+ if (now - freshest > staleMs) {
21822
+ if (!_staleMergeHeadWarned) {
21823
+ _staleMergeHeadWarned = true;
21824
+ opts.log?.warn(`[guard] 检测到陈旧 merge marker(mtime 超过 ${staleMs / 3600000}h),` + `不启用 merge 冲突 bypass;若确在解冲突请 git merge --continue/--abort 刷新状态`, { mainRoot, freshestMtimeMs: freshest });
21825
+ }
21826
+ return false;
21827
+ }
21828
+ return true;
21829
+ }
21492
21830
  var sessionWorktreeGuardPlugin = async (ctx) => {
21493
21831
  const disableEnv = process.env["CODEFORGE_DISABLE_WORKTREE_GUARD"];
21494
21832
  if (disableEnv === "1" || disableEnv === "true" || disableEnv === "yes") {
@@ -21553,6 +21891,29 @@ var sessionWorktreeGuardPlugin = async (ctx) => {
21553
21891
  await safeAsync(PLUGIN_NAME22, "tool.execute.before", async () => {
21554
21892
  const toolName = input.tool;
21555
21893
  const argsObj = output.args ?? {};
21894
+ const midMerge = isWriteOperation(toolName, argsObj, mainRoot) || toolName === "bash" ? await isMainRepoMidMerge(mainRoot, { log: log13 }) : false;
21895
+ const emitMergeBypassNotice = (subClass, detail) => {
21896
+ safeWriteLog(PLUGIN_NAME22, {
21897
+ hook: "tool.execute.before",
21898
+ tool: toolName,
21899
+ sessionID: input.sessionID,
21900
+ action: `merge-conflict-bypass-${subClass}`,
21901
+ ...detail
21902
+ });
21903
+ log13.warn(`[merge-conflict-bypass] ${subClass} 放行主仓直写`, {
21904
+ tool: toolName,
21905
+ ...detail
21906
+ });
21907
+ if (!_mergeBypassToastShown) {
21908
+ _mergeBypassToastShown = true;
21909
+ showToast2(ctx.client, {
21910
+ message: "⚠️ 主仓处于 merge 冲突态,session-worktree-guard 已临时放行主仓直写(解冲突期)",
21911
+ variant: "warning",
21912
+ duration: 8000,
21913
+ title: "CodeForge"
21914
+ }, log13).catch(() => {});
21915
+ }
21916
+ };
21556
21917
  let entry = null;
21557
21918
  try {
21558
21919
  entry = await getSessionWorktree(sessionId, mainRoot);
@@ -21596,6 +21957,12 @@ var sessionWorktreeGuardPlugin = async (ctx) => {
21596
21957
  if (!isWriteOperation(toolName, argsObj, mainRoot)) {
21597
21958
  return;
21598
21959
  }
21960
+ if (midMerge) {
21961
+ emitMergeBypassNotice("lazy-bind", {
21962
+ reason: "mid-merge: skip lazy-bind, passthrough to main repo"
21963
+ });
21964
+ return;
21965
+ }
21599
21966
  try {
21600
21967
  const parentId = lookupParentSessionId2(sessionId);
21601
21968
  if (parentId) {
@@ -21612,6 +21979,18 @@ var sessionWorktreeGuardPlugin = async (ctx) => {
21612
21979
  parent_branch: parentEntry.branch,
21613
21980
  parent_worktree: parentEntry.worktreePath
21614
21981
  });
21982
+ } else if (!parentEntry) {
21983
+ entry = await bindSessionWorktree({ sessionId: parentId, mainRoot });
21984
+ log13.info(`[child-inherit-bind-root] session ${sessionId} 触发父 ${parentId} 的 worktree lazy-bind`, { parentSessionId: parentId, branch: entry.branch, worktreePath: entry.worktreePath });
21985
+ safeWriteLog(PLUGIN_NAME22, {
21986
+ hook: "tool.execute.before",
21987
+ tool: toolName,
21988
+ sessionID: input.sessionID,
21989
+ action: "child-inherit-bind-root",
21990
+ parent_session_id: parentId,
21991
+ branch: entry.branch,
21992
+ worktreePath: entry.worktreePath
21993
+ });
21615
21994
  }
21616
21995
  }
21617
21996
  } catch (lookupErr) {
@@ -21841,6 +22220,11 @@ var sessionWorktreeGuardPlugin = async (ctx) => {
21841
22220
  if (toolName === "bash") {
21842
22221
  const command = argsObj["command"];
21843
22222
  if (typeof command === "string" && commandContainsMainRootExcludingWorktree(command, mainRoot, worktreePath) && detectBashWriteIntent(command, mainRoot)) {
22223
+ if (midMerge) {
22224
+ const snippet = command.length > 60 ? command.slice(0, 60) + "…" : command;
22225
+ emitMergeBypassNotice("classB", { command: snippet });
22226
+ return;
22227
+ }
21844
22228
  const caller = await resolveAgentForGuard({ sessionID: input.sessionID, agent: input.agent }, ctx.client, log13);
21845
22229
  if (caller !== null && CLASS_B_CALLER_WHITELIST.has(caller)) {
21846
22230
  log13.debug?.(`[class-b-whitelist] allow caller=${caller}`, { sessionId, tool: toolName, command: command.slice(0, 200) });
@@ -21872,182 +22256,85 @@ var sessionWorktreeGuardPlugin = async (ctx) => {
21872
22256
  }
21873
22257
  }
21874
22258
  }
22259
+ const rewritesThisCall = [];
22260
+ const applyRewrite = (field, value) => {
22261
+ const newPath = rewritePath(value, mainRoot, worktreePath);
22262
+ if (newPath === null)
22263
+ return;
22264
+ if (midMerge) {
22265
+ emitMergeBypassNotice("classA", { field, before: value, after: newPath });
22266
+ return;
22267
+ }
22268
+ log13.info(`rewrote ${toolName}.${field}: ${value} → ${newPath}`);
22269
+ safeWriteLog(PLUGIN_NAME22, {
22270
+ hook: "tool.execute.before",
22271
+ tool: toolName,
22272
+ field,
22273
+ before: value,
22274
+ after: newPath,
22275
+ action: "rewrite"
22276
+ });
22277
+ output.args[field] = newPath;
22278
+ rewritesThisCall.push({ field, before: value, after: newPath });
22279
+ };
21875
22280
  if (toolName === "write" || toolName === "edit") {
21876
22281
  const filePath = argsObj["filePath"];
21877
- if (typeof filePath === "string") {
21878
- const newPath = rewritePath(filePath, mainRoot, worktreePath);
21879
- if (newPath !== null) {
21880
- log13.info(`rewrote ${toolName}.filePath: ${filePath} → ${newPath}`);
21881
- safeWriteLog(PLUGIN_NAME22, {
21882
- hook: "tool.execute.before",
21883
- tool: toolName,
21884
- field: "filePath",
21885
- before: filePath,
21886
- after: newPath,
21887
- action: "rewrite"
21888
- });
21889
- output.args["filePath"] = newPath;
21890
- }
21891
- }
22282
+ if (typeof filePath === "string")
22283
+ applyRewrite("filePath", filePath);
21892
22284
  }
21893
22285
  if (toolName === "ast_edit") {
21894
22286
  const target = argsObj["target"];
21895
- if (typeof target === "string") {
21896
- const newTarget = rewritePath(target, mainRoot, worktreePath);
21897
- if (newTarget !== null) {
21898
- log13.info(`rewrote ast_edit.target: ${target} → ${newTarget}`);
21899
- safeWriteLog(PLUGIN_NAME22, {
21900
- hook: "tool.execute.before",
21901
- tool: toolName,
21902
- field: "target",
21903
- before: target,
21904
- after: newTarget,
21905
- action: "rewrite"
21906
- });
21907
- output.args["target"] = newTarget;
21908
- }
21909
- }
22287
+ if (typeof target === "string")
22288
+ applyRewrite("target", target);
21910
22289
  const root = argsObj["root"];
21911
- if (typeof root === "string") {
21912
- const newRoot = rewritePath(root, mainRoot, worktreePath);
21913
- if (newRoot !== null) {
21914
- log13.info(`rewrote ast_edit.root: ${root} → ${newRoot}`);
21915
- safeWriteLog(PLUGIN_NAME22, {
21916
- hook: "tool.execute.before",
21917
- tool: toolName,
21918
- field: "root",
21919
- before: root,
21920
- after: newRoot,
21921
- action: "rewrite"
21922
- });
21923
- output.args["root"] = newRoot;
21924
- }
21925
- }
22290
+ if (typeof root === "string")
22291
+ applyRewrite("root", root);
21926
22292
  }
21927
22293
  if (toolName === "bash") {
21928
22294
  const workdir = argsObj["workdir"];
21929
- if (typeof workdir === "string") {
21930
- const newWorkdir = rewritePath(workdir, mainRoot, worktreePath);
21931
- if (newWorkdir !== null) {
21932
- log13.info(`rewrote bash.workdir: ${workdir} → ${newWorkdir}`);
21933
- safeWriteLog(PLUGIN_NAME22, {
21934
- hook: "tool.execute.before",
21935
- tool: toolName,
21936
- field: "workdir",
21937
- before: workdir,
21938
- after: newWorkdir,
21939
- action: "rewrite"
21940
- });
21941
- output.args["workdir"] = newWorkdir;
21942
- }
21943
- }
22295
+ if (typeof workdir === "string")
22296
+ applyRewrite("workdir", workdir);
22297
+ }
22298
+ const _afterCallID = input.callID;
22299
+ if (_afterCallID && rewritesThisCall.length > 0) {
22300
+ recordRewriteNotice(_afterCallID, {
22301
+ tool: toolName,
22302
+ rewrites: rewritesThisCall,
22303
+ worktreePath
22304
+ });
21944
22305
  }
21945
22306
  });
21946
22307
  if (denied)
21947
22308
  throw denied;
22309
+ },
22310
+ "tool.execute.after": async (input, output) => {
22311
+ await safeAsync(PLUGIN_NAME22, "tool.execute.after", async () => {
22312
+ const callID = input.callID;
22313
+ if (!callID)
22314
+ return;
22315
+ const notice = consumeRewriteNotice(callID);
22316
+ if (!notice)
22317
+ return;
22318
+ const warning = formatRewriteWarning(notice);
22319
+ const prev = typeof output.output === "string" ? output.output : "";
22320
+ output.output = warning + `
22321
+
22322
+ ` + prev;
22323
+ safeWriteLog(PLUGIN_NAME22, {
22324
+ hook: "tool.execute.after",
22325
+ tool: notice.tool,
22326
+ sessionID: input.sessionID,
22327
+ action: "inject-rewrite-notice",
22328
+ rewrites: notice.rewrites.length
22329
+ });
22330
+ });
21948
22331
  }
21949
22332
  };
21950
22333
  };
21951
22334
  var handler22 = sessionWorktreeGuardPlugin;
21952
22335
 
21953
- // lib/opencode-session-probe.ts
21954
- import * as path30 from "node:path";
21955
- import * as os6 from "node:os";
21956
- import { createRequire as createRequire2 } from "node:module";
21957
- var requireFromHere = createRequire2(import.meta.url);
21958
- var DEFAULT_LIVENESS_MS = 6 * 60 * 60000;
21959
- var DEFAULT_DB_PATH = path30.join(os6.homedir(), ".local/share/opencode/opencode.db");
21960
- function createSessionProbe(opts = {}) {
21961
- const dbPath = opts.dbPath ?? DEFAULT_DB_PATH;
21962
- const httpBaseUrl = opts.httpBaseUrl ?? process.env["OPENCODE_SERVER_URL"];
21963
- const livenessWindowMs = opts.livenessWindowMs ?? DEFAULT_LIVENESS_MS;
21964
- const timeoutMs = opts.timeoutMs ?? 1500;
21965
- let db = null;
21966
- let dbInitTried = false;
21967
- let dbStmt = null;
21968
- async function tryOpenDb() {
21969
- if (dbInitTried)
21970
- return db != null;
21971
- dbInitTried = true;
21972
- try {
21973
- const mod = requireFromHere("node:sqlite");
21974
- try {
21975
- db = new mod.DatabaseSync(dbPath, { readOnly: true });
21976
- } catch {
21977
- db = null;
21978
- dbStmt = null;
21979
- return false;
21980
- }
21981
- dbStmt = db.prepare("SELECT time_updated, time_archived FROM session WHERE id = ? LIMIT 1");
21982
- return true;
21983
- } catch {
21984
- db = null;
21985
- dbStmt = null;
21986
- return false;
21987
- }
21988
- }
21989
- async function probeHttp(sessionId) {
21990
- if (!httpBaseUrl)
21991
- return null;
21992
- try {
21993
- const ctrl = new AbortController;
21994
- const t = setTimeout(() => ctrl.abort(), timeoutMs);
21995
- const res = await fetch(`${httpBaseUrl.replace(/\/$/, "")}/session`, {
21996
- signal: ctrl.signal
21997
- }).finally(() => clearTimeout(t));
21998
- if (!res.ok)
21999
- return null;
22000
- const list = await res.json();
22001
- const hit = Array.isArray(list) && list.some((s) => s.id === sessionId);
22002
- return { alive: hit, source: "http" };
22003
- } catch {
22004
- return null;
22005
- }
22006
- }
22007
- async function probeSqlite(sessionId) {
22008
- if (!await tryOpenDb() || !dbStmt)
22009
- return null;
22010
- try {
22011
- const row = dbStmt.get(sessionId);
22012
- if (!row) {
22013
- return { alive: false, source: "sqlite" };
22014
- }
22015
- if (row.time_archived != null) {
22016
- return {
22017
- alive: false,
22018
- source: "sqlite",
22019
- time_archived: row.time_archived
22020
- };
22021
- }
22022
- const now = Date.now();
22023
- const tu = Number(row.time_updated) || 0;
22024
- const alive = now - tu < livenessWindowMs;
22025
- return { alive, source: "sqlite", time_updated: tu, time_archived: null };
22026
- } catch {
22027
- return null;
22028
- }
22029
- }
22030
- return {
22031
- async isSessionAlive(sessionId) {
22032
- const http = await probeHttp(sessionId);
22033
- if (http)
22034
- return http;
22035
- const sql = await probeSqlite(sessionId);
22036
- if (sql)
22037
- return sql;
22038
- return { alive: true, source: "unknown" };
22039
- },
22040
- close() {
22041
- try {
22042
- db?.close?.();
22043
- } catch {}
22044
- db = null;
22045
- dbStmt = null;
22046
- }
22047
- };
22048
- }
22049
-
22050
22336
  // plugins/worktree-lifecycle.ts
22337
+ init_worktree_ops();
22051
22338
  var PLUGIN_NAME23 = "worktree-lifecycle";
22052
22339
  logLifecycle(PLUGIN_NAME23, "import", {});
22053
22340
  var IDLE_TOAST_THROTTLE_MS = 60 * 60000;
@@ -22204,10 +22491,17 @@ var worktreeLifecyclePlugin = async (ctx) => {
22204
22491
  worktreePath: entry.worktreePath
22205
22492
  });
22206
22493
  } catch (err) {
22207
- log14.warn(`[lifecycle] discardSession failed`, {
22208
- sessionId: ended.sessionID,
22209
- error: err instanceof Error ? err.message : String(err)
22210
- });
22494
+ const errMsg = err instanceof Error ? err.message : String(err);
22495
+ if (isWorktreeAbsentError(err) || isBranchAbsentError(err)) {
22496
+ if (process.env["CODEFORGE_DEBUG"]) {
22497
+ console.debug(`[worktree-lifecycle] discardSession absent-class silenced: ${errMsg}`);
22498
+ }
22499
+ } else {
22500
+ log14.warn(`[lifecycle] discardSession failed`, {
22501
+ sessionId: ended.sessionID,
22502
+ error: errMsg
22503
+ });
22504
+ }
22211
22505
  }
22212
22506
  lastIdleToastAt.delete(ended.sessionID);
22213
22507
  return;