@andyqiu/codeforge 0.6.8 → 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
  });
@@ -12373,7 +12379,13 @@ async function pruneOrphanWorktrees(mainRoot, opts = {}) {
12373
12379
  const r = await pruneDiscardedRegistryEntries(resolved);
12374
12380
  discardedPruned = r.pruned;
12375
12381
  } catch {}
12376
- 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 };
12377
12389
  }
12378
12390
 
12379
12391
  // lib/merge-gate.ts
@@ -13740,6 +13752,159 @@ async function execute15(input) {
13740
13752
  };
13741
13753
  }
13742
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
+ }
13743
13908
  // lib/opencode-runner.ts
13744
13909
  function makeOpencodeRunner(opts) {
13745
13910
  const log4 = opts.log ?? (() => {});
@@ -14001,18 +14166,18 @@ init_decision_parser();
14001
14166
 
14002
14167
  // lib/parent-map-store.ts
14003
14168
  import { promises as fs14 } from "node:fs";
14004
- import * as path18 from "node:path";
14169
+ import * as path19 from "node:path";
14005
14170
  var PARENT_MAP_VERSION = 1;
14006
14171
  var PARENT_MAP_LOCK_TIMEOUT_MS = 2000;
14007
14172
  var PARENT_MAP_MAX_ENTRIES = 256;
14008
14173
  function parentMapDir(mainRoot) {
14009
- return path18.join(runtimeDir(path18.resolve(mainRoot), { ensure: false }), "session-worktrees");
14174
+ return path19.join(runtimeDir(path19.resolve(mainRoot), { ensure: false }), "session-worktrees");
14010
14175
  }
14011
14176
  function parentMapPath(mainRoot) {
14012
- return path18.join(parentMapDir(mainRoot), "parent-map.json");
14177
+ return path19.join(parentMapDir(mainRoot), "parent-map.json");
14013
14178
  }
14014
14179
  function parentMapLockPath(mainRoot) {
14015
- return path18.join(parentMapDir(mainRoot), "parent-map.lock");
14180
+ return path19.join(parentMapDir(mainRoot), "parent-map.lock");
14016
14181
  }
14017
14182
  async function readParentMapFile(mainRoot) {
14018
14183
  const file = parentMapPath(mainRoot);
@@ -14032,7 +14197,7 @@ async function readParentMapFile(mainRoot) {
14032
14197
  }
14033
14198
  async function writeParentMapFile(mainRoot, payload) {
14034
14199
  const file = parentMapPath(mainRoot);
14035
- await fs14.mkdir(path18.dirname(file), { recursive: true });
14200
+ await fs14.mkdir(path19.dirname(file), { recursive: true });
14036
14201
  const tmp = `${file}.tmp-${process.pid}-${Date.now()}`;
14037
14202
  await fs14.writeFile(tmp, JSON.stringify(payload, null, 2), "utf8");
14038
14203
  await fs14.rename(tmp, file);
@@ -14067,7 +14232,7 @@ async function loadParentMap(mainRoot) {
14067
14232
  }
14068
14233
  async function mutateParentMap(mainRoot, mutator, opts = {}) {
14069
14234
  const lockPath = parentMapLockPath(mainRoot);
14070
- await fs14.mkdir(path18.dirname(lockPath), { recursive: true });
14235
+ await fs14.mkdir(path19.dirname(lockPath), { recursive: true });
14071
14236
  const lockOpts = {
14072
14237
  timeoutMs: opts.timeoutMs ?? PARENT_MAP_LOCK_TIMEOUT_MS,
14073
14238
  ...opts
@@ -14086,7 +14251,7 @@ async function appendParentEntry(mainRoot, childID, parentID, ts, opts = {}) {
14086
14251
  if (!childID || !parentID)
14087
14252
  return;
14088
14253
  const lockPath = parentMapLockPath(mainRoot);
14089
- await fs14.mkdir(path18.dirname(lockPath), { recursive: true });
14254
+ await fs14.mkdir(path19.dirname(lockPath), { recursive: true });
14090
14255
  const lockOpts = {
14091
14256
  timeoutMs: opts.timeoutMs ?? PARENT_MAP_LOCK_TIMEOUT_MS,
14092
14257
  ...opts
@@ -14420,7 +14585,7 @@ function clip3(s, max) {
14420
14585
 
14421
14586
  // lib/codeforge-runtime.ts
14422
14587
  import { promises as fs15 } from "node:fs";
14423
- import * as path19 from "node:path";
14588
+ import * as path20 from "node:path";
14424
14589
  var DEFAULT_RUNTIME = {
14425
14590
  autonomy: {
14426
14591
  downgrade_on_risky: true
@@ -14463,7 +14628,7 @@ function loadRuntimeSync(opts = {}) {
14463
14628
  }
14464
14629
  async function loadRuntime(opts = {}) {
14465
14630
  const root = opts.root ?? process.cwd();
14466
- const abs = path19.resolve(root, opts.file ?? CONFIG_FILE);
14631
+ const abs = path20.resolve(root, opts.file ?? CONFIG_FILE);
14467
14632
  let raw;
14468
14633
  try {
14469
14634
  raw = await fs15.readFile(abs, "utf8");
@@ -14747,7 +14912,7 @@ var toolHeartbeatServer = async (ctx) => {
14747
14912
  var handler6 = toolHeartbeatServer;
14748
14913
 
14749
14914
  // plugins/codeforge-tools.ts
14750
- var z16 = tool.schema;
14915
+ var z17 = tool.schema;
14751
14916
  var PLUGIN_NAME7 = "codeforge-tools";
14752
14917
  logLifecycle(PLUGIN_NAME7, "import");
14753
14918
  function wrap(output, metadata) {
@@ -14794,7 +14959,7 @@ function buildBrowserTools() {
14794
14959
  browser_navigate: tool({
14795
14960
  description: description5,
14796
14961
  args: {
14797
- url: z16.string().min(1).describe("要打开的 URL;必须是 http(s)")
14962
+ url: z17.string().min(1).describe("要打开的 URL;必须是 http(s)")
14798
14963
  },
14799
14964
  async execute(args) {
14800
14965
  return await runSafe("browser_navigate", async () => {
@@ -14809,7 +14974,7 @@ function buildBrowserTools() {
14809
14974
  browser_click: tool({
14810
14975
  description: description6,
14811
14976
  args: {
14812
- selector: z16.string().min(1).describe("CSS / Playwright locator,必须能唯一定位")
14977
+ selector: z17.string().min(1).describe("CSS / Playwright locator,必须能唯一定位")
14813
14978
  },
14814
14979
  async execute(args) {
14815
14980
  return await runSafe("browser_click", async () => {
@@ -14824,8 +14989,8 @@ function buildBrowserTools() {
14824
14989
  browser_fill: tool({
14825
14990
  description: description7,
14826
14991
  args: {
14827
- selector: z16.string().min(1).describe("CSS / Playwright locator"),
14828
- value: z16.string().describe("要填入的文本;原样写入不转义")
14992
+ selector: z17.string().min(1).describe("CSS / Playwright locator"),
14993
+ value: z17.string().describe("要填入的文本;原样写入不转义")
14829
14994
  },
14830
14995
  async execute(args) {
14831
14996
  return await runSafe("browser_fill", async () => {
@@ -14840,8 +15005,8 @@ function buildBrowserTools() {
14840
15005
  browser_screenshot: tool({
14841
15006
  description: description8,
14842
15007
  args: {
14843
- fullPage: z16.boolean().optional().describe("是否截全长页面(默认仅可视区)"),
14844
- selector: z16.string().optional().describe("CSS 选择器;指定时只截该元素")
15008
+ fullPage: z17.boolean().optional().describe("是否截全长页面(默认仅可视区)"),
15009
+ selector: z17.string().optional().describe("CSS 选择器;指定时只截该元素")
14845
15010
  },
14846
15011
  async execute(args) {
14847
15012
  return await runSafe("browser_screenshot", async () => {
@@ -14856,8 +15021,8 @@ function buildBrowserTools() {
14856
15021
  browser_console: tool({
14857
15022
  description: description9,
14858
15023
  args: {
14859
- level: z16.enum(["log", "info", "warn", "error", "debug"]).optional().describe("过滤级别"),
14860
- 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 的条目")
14861
15026
  },
14862
15027
  async execute(args) {
14863
15028
  return await runSafe("browser_console", async () => {
@@ -14872,8 +15037,8 @@ function buildBrowserTools() {
14872
15037
  browser_network: tool({
14873
15038
  description: description10,
14874
15039
  args: {
14875
- method: z16.string().optional().describe("HTTP 方法过滤(GET/POST/...)"),
14876
- 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 的请求")
14877
15042
  },
14878
15043
  async execute(args) {
14879
15044
  return await runSafe("browser_network", async () => {
@@ -14896,7 +15061,8 @@ var CORE_TOOL_NAMES = [
14896
15061
  "session_merge",
14897
15062
  "plan_write",
14898
15063
  "plan_read",
14899
- "adr_init"
15064
+ "adr_init",
15065
+ "worktrees_gc"
14900
15066
  ];
14901
15067
  var BROWSER_TOOL_NAMES = [
14902
15068
  "browser_navigate",
@@ -14933,31 +15099,32 @@ var codeforgeToolsServer = async (ctx) => {
14933
15099
  spawner,
14934
15100
  resolveCurrentSessionId: () => process.env["CODEFORGE_SESSION_ID"] ?? ""
14935
15101
  });
15102
+ __setContext3({ mainRoot: ctx.directory ?? process.cwd() });
14936
15103
  const browserTools = browserEnabled ? buildBrowserTools() : {};
14937
15104
  return {
14938
15105
  tool: {
14939
15106
  ast_edit: tool({
14940
15107
  description,
14941
15108
  args: {
14942
- action: z16.enum([
15109
+ action: z17.enum([
14943
15110
  "replace_anchor",
14944
15111
  "insert_after_anchor",
14945
15112
  "insert_before_anchor",
14946
15113
  "delete_range",
14947
15114
  "rename_symbol"
14948
15115
  ]).describe("编辑类型;不同 action 需要的字段不同"),
14949
- target: z16.string().min(1).describe("目标文件路径(相对 cwd 或绝对)"),
14950
- before_hash: z16.string().optional().describe("操作前的 sha256 hex(强烈建议传,新文件传 null/省略)"),
14951
- auto_stage: z16.boolean().optional().describe("已废弃(保留兼容字段):Phase 5 后 ast_edit 直接写入 session worktree,无需独立 stage"),
14952
- description: z16.string().optional().describe("可选变更说明(写入 audit log)"),
14953
- anchor: z16.string().optional().describe("anchor 类用:anchor 文本或 regex 源"),
14954
- regex: z16.boolean().optional().describe("anchor 是否按 RegExp 解释,默认 false"),
14955
- occurrence: z16.number().int().min(1).optional().describe("第几次匹配(1-based),默认 1"),
14956
- payload: z16.string().optional().describe("anchor 类用:要写入的内容"),
14957
- start: z16.number().int().min(1).optional().describe("delete_range 用:起始行(1-based)"),
14958
- end: z16.number().int().min(1).optional().describe("delete_range 用:结束行(1-based)"),
14959
- old_name: z16.string().optional().describe("rename_symbol 用:旧标识符"),
14960
- 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 用:新标识符")
14961
15128
  },
14962
15129
  async execute(args) {
14963
15130
  return await runSafeTracked("ast_edit", async () => {
@@ -14980,10 +15147,10 @@ var codeforgeToolsServer = async (ctx) => {
14980
15147
  repo_map: tool({
14981
15148
  description: description2,
14982
15149
  args: {
14983
- root: z16.string().optional().describe("扫描根目录,默认 cwd"),
14984
- top: z16.number().int().min(1).max(100).optional().describe("展示 top N 文件,默认 20"),
14985
- focus: z16.string().optional().describe("聚焦文件(POSIX 相对路径)"),
14986
- 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")
14987
15154
  },
14988
15155
  async execute(args) {
14989
15156
  return await runSafeTracked("repo_map", async () => {
@@ -15004,14 +15171,14 @@ var codeforgeToolsServer = async (ctx) => {
15004
15171
  rules_debug: tool({
15005
15172
  description: description3,
15006
15173
  args: {
15007
- current_agent: z16.string().optional().describe("当前 agent 名"),
15008
- root: z16.string().optional().describe("项目根目录"),
15009
- home_dir: z16.string().optional().describe("覆盖个人规则目录"),
15010
- project_dir: z16.string().optional().describe("覆盖项目规则目录"),
15011
- skip_personal: z16.boolean().optional(),
15012
- skip_project: z16.boolean().optional(),
15013
- skip_agent: z16.boolean().optional(),
15014
- 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 摘要")
15015
15182
  },
15016
15183
  async execute(args) {
15017
15184
  return await runSafe("rules_debug", async () => {
@@ -15026,14 +15193,14 @@ var codeforgeToolsServer = async (ctx) => {
15026
15193
  review_approval: tool({
15027
15194
  description: description4,
15028
15195
  args: {
15029
- verdict: z16.enum(["APPROVE", "APPROVE_WITH_NOTES"]).describe("审批层裁决(审计字段,与协议层独立);REQUEST_CHANGES / BLOCK 不应调本工具。⚠️ 不要把本字段值复制到 reviewer 输出的 `## Decision` 节首行 — 那是协议层 3 档。"),
15030
- pendingIds: z16.array(z16.string().min(1)).min(1).describe("本次 APPROVE 覆盖的 pending change id 列表"),
15031
- notes: z16.string().min(1).max(2000).describe("审阅意见摘要(建议 ≤ 500 字)"),
15032
- decisionLine: z16.string().optional().describe("`## Decision` 节首行原文(默认 verdict 字面量,机审证据)"),
15033
- source: z16.enum(["reviewer", "codeforge-fallback"]).optional().describe("写入来源;默认 'reviewer',codeforge 补写时传 'codeforge-fallback'"),
15034
- reviewerAgent: z16.string().optional().describe("写入 agent name(默认 'reviewer';fallback 时为 'codeforge')"),
15035
- sessionId: z16.string().optional().describe("reviewer 子 session id(boomerang 溯源用,可选)"),
15036
- 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(审计用,可选)")
15037
15204
  },
15038
15205
  async execute(args) {
15039
15206
  return await runSafe("review_approval", async () => {
@@ -15054,10 +15221,10 @@ var codeforgeToolsServer = async (ctx) => {
15054
15221
  model_chain: tool({
15055
15222
  description: description11,
15056
15223
  args: {
15057
- agent: z16.string().optional().describe("查指定 agent;不传 → 列出全部"),
15058
- current: z16.string().optional().describe("当前已用过的模型(<provider>/<id>),用于算下一档"),
15059
- root: z16.string().optional().describe("项目根目录,默认 cwd"),
15060
- 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")
15061
15228
  },
15062
15229
  async execute(args) {
15063
15230
  return await runSafe("model_chain", async () => {
@@ -15078,11 +15245,11 @@ var codeforgeToolsServer = async (ctx) => {
15078
15245
  session_merge: tool({
15079
15246
  description: description12,
15080
15247
  args: {
15081
- action: z16.enum(["merge", "status", "discard", "diff"]).describe("操作类型:merge=合并到主仓(orchestrator 专用)/ status=查询状态 / discard=放弃 / diff=查看 worktree 改动"),
15082
- session_id: z16.string().optional().describe("目标 session id;不传则用当前 session"),
15083
- plan_id: z16.string().optional().describe("关联的 plan_id(reviewer 校验时用),格式 plan-YYYYMMDD-HHmmss-NNN"),
15084
- force: z16.boolean().optional().describe("action=merge 时跳过 review 直接 squash merge(写审计)"),
15085
- 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)")
15086
15253
  },
15087
15254
  async execute(args, input) {
15088
15255
  return await runSafe("session_merge", async () => {
@@ -15118,9 +15285,9 @@ var codeforgeToolsServer = async (ctx) => {
15118
15285
  plan_write: tool({
15119
15286
  description: description13,
15120
15287
  args: {
15121
- title: z16.string().min(2).max(80).describe('方案简短标题(2-80 字),如 "实现 worktree session 隔离 Phase 1"'),
15122
- content: z16.string().min(10).describe("方案 markdown 全文,建议 ≥ 50 行"),
15123
- 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"]')
15124
15291
  },
15125
15292
  async execute(args) {
15126
15293
  return await runSafeTracked("plan_write", async () => {
@@ -15139,8 +15306,8 @@ var codeforgeToolsServer = async (ctx) => {
15139
15306
  plan_read: tool({
15140
15307
  description: description14,
15141
15308
  args: {
15142
- plan_id: z16.string().optional().describe("方案 ID(推荐),格式 plan-YYYYMMDD-HHmmss-NNN"),
15143
- 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 元数据时退而求其次)")
15144
15311
  },
15145
15312
  async execute(args) {
15146
15313
  return await runSafe("plan_read", async () => {
@@ -15163,11 +15330,11 @@ var codeforgeToolsServer = async (ctx) => {
15163
15330
  adr_init: tool({
15164
15331
  description: description15,
15165
15332
  args: {
15166
- cwd: z16.string().optional().describe("目标项目根目录,默认 process.cwd();通常无需传"),
15167
- force: z16.boolean().optional().describe("已存在文件覆盖;覆盖前自动 .bak.<ts> 备份"),
15168
- dryRun: z16.boolean().optional().describe("只输出将要执行的写入计划,不实际写盘"),
15169
- writePrepare: z16.boolean().optional().describe("(npm 项目)自动合并 git config core.hooksPath 到 package.json scripts.prepare;写前自动 backup"),
15170
- 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")
15171
15338
  },
15172
15339
  async execute(args) {
15173
15340
  return await runSafe("adr_init", async () => {
@@ -15179,6 +15346,27 @@ var codeforgeToolsServer = async (ctx) => {
15179
15346
  return wrap(result, { title: "adr_init" });
15180
15347
  });
15181
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
+ }
15182
15370
  })
15183
15371
  }
15184
15372
  };
@@ -15187,10 +15375,10 @@ var handler7 = codeforgeToolsServer;
15187
15375
 
15188
15376
  // plugins/discover-spec-suggest.ts
15189
15377
  import { readFileSync as readFileSync3, readdirSync, statSync as statSync2 } from "node:fs";
15190
- import { join as join16 } from "node:path";
15378
+ import { join as join17 } from "node:path";
15191
15379
 
15192
15380
  // lib/handoff-schema.ts
15193
- import { z as z17 } from "zod";
15381
+ import { z as z18 } from "zod";
15194
15382
 
15195
15383
  // node_modules/yaml/dist/index.js
15196
15384
  var composer = require_composer();
@@ -15243,92 +15431,92 @@ var MAX_HANDOFF_SIZE = 100 * 1024;
15243
15431
  var SLUG_REGEX = /^[a-z0-9][a-z0-9-]{0,49}$/;
15244
15432
  var SCORE_DIMENSIONS = ["functional", "ux", "technical", "constraints", "edge_cases"];
15245
15433
  var COMBO_VALUES = ["A", "B", "C", "D"];
15246
- var NeedSchema = z17.object({
15247
- id: z17.string().min(1),
15248
- type: z17.enum(["must", "should", "nice-to-have"]),
15249
- statement: z17.string().min(1),
15250
- rationale: z17.string().optional(),
15251
- 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()
15252
15440
  });
15253
- var BoundarySchema = z17.object({
15254
- excluded: z17.string().min(1),
15255
- reason: z17.string().min(1)
15441
+ var BoundarySchema = z18.object({
15442
+ excluded: z18.string().min(1),
15443
+ reason: z18.string().min(1)
15256
15444
  });
15257
- var AssumptionSchema = z17.object({
15258
- statement: z17.string().min(1),
15259
- confidence: z17.enum(["verified", "speculation", "high-risk-unknown"]),
15260
- 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()
15261
15449
  });
15262
- var OpenIssueSchema = z17.union([
15263
- z17.string().min(1),
15264
- z17.object({
15265
- id: z17.string().optional(),
15266
- question: z17.string().min(1),
15267
- 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()
15268
15456
  })
15269
15457
  ]);
15270
- var RedFlagsSchema = z17.object({
15271
- raised: z17.boolean(),
15272
- combos: z17.array(z17.enum(COMBO_VALUES)).default([]),
15273
- reasons: z17.array(z17.string()).default([]),
15274
- user_persisted_rounds: z17.number().int().nonnegative().default(0),
15275
- 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()
15276
15464
  }).superRefine((rf, ctx) => {
15277
15465
  if (rf.raised) {
15278
15466
  if (rf.combos.length === 0) {
15279
15467
  ctx.addIssue({
15280
- code: z17.ZodIssueCode.custom,
15468
+ code: z18.ZodIssueCode.custom,
15281
15469
  message: "red_flags.raised=true 时 combos 必须 >=1",
15282
15470
  path: ["combos"]
15283
15471
  });
15284
15472
  }
15285
15473
  if (rf.reasons.length === 0) {
15286
15474
  ctx.addIssue({
15287
- code: z17.ZodIssueCode.custom,
15475
+ code: z18.ZodIssueCode.custom,
15288
15476
  message: "red_flags.raised=true 时 reasons 必须 >=1",
15289
15477
  path: ["reasons"]
15290
15478
  });
15291
15479
  }
15292
15480
  }
15293
15481
  });
15294
- var ScoresSchema = z17.object(Object.fromEntries(SCORE_DIMENSIONS.map((d) => [d, z17.number().min(0).max(1)])));
15295
- var PreCodingBlockerSchema = z17.object({
15296
- id: z17.string().regex(/^PRE-\d+$/, "id 必须形如 PRE-1 / PRE-2"),
15297
- blocker: z17.string().min(1),
15298
- source: z17.enum(["assumption", "red_flag", "open_issue"]),
15299
- 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"])
15300
15488
  });
15301
- var KhRefSchema = z17.object({
15302
- kh_id: z17.string(),
15303
- title: z17.string(),
15304
- 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()
15305
15493
  });
15306
- var HandoffSchema = z17.object({
15307
- schema_version: z17.string().regex(/^\d+\.\d+\.\d+$/, "schema_version 必须形如 1.1.0 / 1.2.0"),
15308
- slug: z17.string().regex(SLUG_REGEX, "slug 仅允许 [a-z0-9-],长度 1-50,首字符为字母数字"),
15309
- title: z17.string().min(1).max(200),
15310
- created_at: z17.string().optional(),
15311
- discover_session_id: z17.string().optional(),
15312
- 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),
15313
15501
  scores: ScoresSchema,
15314
- needs: z17.array(NeedSchema).min(1, "needs 必须 >=1"),
15315
- boundaries: z17.array(BoundarySchema).min(1, "boundaries 必须 >=1"),
15316
- assumptions: z17.array(AssumptionSchema),
15317
- open_issues: z17.array(OpenIssueSchema).default([]),
15318
- rejected_alternatives: z17.array(z17.object({ option: z17.string(), reason: z17.string() })).default([]),
15319
- acceptance_criteria: z17.array(z17.object({
15320
- id: z17.string().regex(/^AC-/, "AC id 必须以 AC- 开头"),
15321
- description: z17.string().min(1),
15322
- measurable: z17.boolean().optional(),
15323
- 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()
15324
15512
  })).default([]),
15325
15513
  red_flags: RedFlagsSchema,
15326
- pre_coding_blockers: z17.array(PreCodingBlockerSchema).default([]),
15327
- kh_references: z17.array(KhRefSchema).default([]),
15328
- adr_refs: z17.array(z17.string()).default([]),
15329
- related_artifacts: z17.object({
15330
- prd: z17.string().optional(),
15331
- 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()
15332
15520
  }).optional()
15333
15521
  });
15334
15522
  function validateHandoff(rawYaml, fileSize) {
@@ -15354,9 +15542,9 @@ function validateHandoff(rawYaml, fileSize) {
15354
15542
  const result = HandoffSchema.safeParse(parsed);
15355
15543
  if (!result.success) {
15356
15544
  const first = result.error.issues[0];
15357
- const path20 = first?.path?.join(".") ?? "(root)";
15545
+ const path21 = first?.path?.join(".") ?? "(root)";
15358
15546
  const msg = first?.message ?? "unknown";
15359
- return { ok: false, reason: `schema 校验失败:${path20}: ${msg}` };
15547
+ return { ok: false, reason: `schema 校验失败:${path21}: ${msg}` };
15360
15548
  }
15361
15549
  return { ok: true, data: result.data, schemaVersion: result.data.schema_version };
15362
15550
  }
@@ -15373,7 +15561,7 @@ var SESSION_TTL_MS2 = 24 * 60 * 60 * 1000;
15373
15561
  var MATCH_THRESHOLD = 0.15;
15374
15562
  var MAX_CANDIDATES = 3;
15375
15563
  var NUDGE_MAX_LEN = 1500;
15376
- var SPECS_REL_DIR = join16("docs", "specs");
15564
+ var SPECS_REL_DIR = join17("docs", "specs");
15377
15565
  var sessionMap = new Map;
15378
15566
  function pruneIfOversize2() {
15379
15567
  while (sessionMap.size > SESSION_CAP2) {
@@ -15480,7 +15668,7 @@ function loadSpecs(rootDir, opts = {}) {
15480
15668
  const dirExists = opts.dirExists ?? defaultDirExists;
15481
15669
  const statReader = opts.statReader ?? defaultStatReader;
15482
15670
  const log6 = makePluginLogger(PLUGIN_NAME8);
15483
- const specsRoot = join16(rootDir, SPECS_REL_DIR);
15671
+ const specsRoot = join17(rootDir, SPECS_REL_DIR);
15484
15672
  const records = [];
15485
15673
  if (!dirExists(specsRoot)) {
15486
15674
  log6.info(`specs 目录不存在,plugin 将 no-op`, { specsRoot });
@@ -15501,7 +15689,7 @@ function loadSpecs(rootDir, opts = {}) {
15501
15689
  log6.info(`跳过非合法 slug 命名的条目`, { entry });
15502
15690
  continue;
15503
15691
  }
15504
- const specDir = join16(specsRoot, entry);
15692
+ const specDir = join17(specsRoot, entry);
15505
15693
  let dirStat;
15506
15694
  try {
15507
15695
  dirStat = statReader(specDir);
@@ -15514,7 +15702,7 @@ function loadSpecs(rootDir, opts = {}) {
15514
15702
  }
15515
15703
  if (!dirStat.isDirectory)
15516
15704
  continue;
15517
- const handoffPath = join16(specDir, "handoff.yaml");
15705
+ const handoffPath = join17(specDir, "handoff.yaml");
15518
15706
  let fileStat;
15519
15707
  try {
15520
15708
  fileStat = statReader(handoffPath);
@@ -15687,13 +15875,13 @@ var handler8 = discoverSpecSuggestServer;
15687
15875
 
15688
15876
  // lib/memories.ts
15689
15877
  import { promises as fs16 } from "node:fs";
15690
- import * as path20 from "node:path";
15691
- import * as os5 from "node:os";
15878
+ import * as path21 from "node:path";
15879
+ import * as os6 from "node:os";
15692
15880
  function resolveConfig(c) {
15693
15881
  return {
15694
15882
  projectRoot: c.projectRoot,
15695
- homeDir: c.homeDir ?? os5.homedir(),
15696
- projectName: c.projectName ?? path20.basename(c.projectRoot),
15883
+ homeDir: c.homeDir ?? os6.homedir(),
15884
+ projectName: c.projectName ?? path21.basename(c.projectRoot),
15697
15885
  now: c.now ?? Date.now,
15698
15886
  log: c.log ?? (() => {}),
15699
15887
  maxPerScope: c.maxPerScope ?? 1000
@@ -15701,9 +15889,9 @@ function resolveConfig(c) {
15701
15889
  }
15702
15890
  function fileFor(scope, cfg) {
15703
15891
  if (scope === "project") {
15704
- return path20.join(cfg.projectRoot, ".codeforge", "memories.json");
15892
+ return path21.join(cfg.projectRoot, ".codeforge", "memories.json");
15705
15893
  }
15706
- return path20.join(cfg.homeDir, ".codeforge", "memories.json");
15894
+ return path21.join(cfg.homeDir, ".codeforge", "memories.json");
15707
15895
  }
15708
15896
  async function readBank(p) {
15709
15897
  try {
@@ -15717,7 +15905,7 @@ async function readBank(p) {
15717
15905
  }
15718
15906
  }
15719
15907
  async function writeBank(p, items) {
15720
- await fs16.mkdir(path20.dirname(p), { recursive: true });
15908
+ await fs16.mkdir(path21.dirname(p), { recursive: true });
15721
15909
  const tmp = `${p}.tmp`;
15722
15910
  await fs16.writeFile(tmp, JSON.stringify(items, null, 2), "utf8");
15723
15911
  await fs16.rename(tmp, p);
@@ -16238,7 +16426,7 @@ var handler10 = modelFallbackServer;
16238
16426
 
16239
16427
  // plugins/subtask-heartbeat.ts
16240
16428
  import { promises as fsPromises } from "node:fs";
16241
- import * as path21 from "node:path";
16429
+ import * as path22 from "node:path";
16242
16430
  var recordSessionParent2 = recordSessionParent;
16243
16431
  var lookupParentSessionId2 = lookupParentSessionId;
16244
16432
  var deleteSessionParent2 = deleteSessionParent;
@@ -16362,11 +16550,11 @@ function extractTaskArgs(args) {
16362
16550
  const a = args;
16363
16551
  const rawDesc = typeof a["description"] === "string" ? a["description"] : null;
16364
16552
  const rawPrompt = typeof a["prompt"] === "string" ? a["prompt"] : null;
16365
- const description16 = rawDesc ?? (rawPrompt ? rawPrompt.slice(0, 60) : null);
16553
+ const description17 = rawDesc ?? (rawPrompt ? rawPrompt.slice(0, 60) : null);
16366
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;
16367
- if (!description16 && !subagentType)
16555
+ if (!description17 && !subagentType)
16368
16556
  return null;
16369
- return { description: description16, subagentType };
16557
+ return { description: description17, subagentType };
16370
16558
  }
16371
16559
  function enqueuePendingTask(parentID, entry, now = Date.now()) {
16372
16560
  const ts = entry.ts ?? now;
@@ -16585,7 +16773,7 @@ function buildErrorLogLine(errorReason, elapsedMs, now = Date.now()) {
16585
16773
  }
16586
16774
  async function appendSubagentLog(filePath, line, log7) {
16587
16775
  try {
16588
- await fsPromises.mkdir(path21.dirname(filePath), { recursive: true });
16776
+ await fsPromises.mkdir(path22.dirname(filePath), { recursive: true });
16589
16777
  await fsPromises.appendFile(filePath, line + `
16590
16778
  `, "utf8");
16591
16779
  } catch (err) {
@@ -16953,8 +17141,8 @@ var handler12 = parallelStatusServer;
16953
17141
 
16954
17142
  // plugins/parallel-tool-nudge.ts
16955
17143
  import { readFileSync as readFileSync4, readdirSync as readdirSync2, statSync as statSync3 } from "node:fs";
16956
- import { join as join18 } from "node:path";
16957
- import { homedir as homedir6 } from "node:os";
17144
+ import { join as join19 } from "node:path";
17145
+ import { homedir as homedir7 } from "node:os";
16958
17146
  var PLUGIN_NAME13 = "parallel-tool-nudge";
16959
17147
  logLifecycle(PLUGIN_NAME13, "import", {});
16960
17148
  var PARALLEL_SAFE_TOOLS = [
@@ -17006,10 +17194,10 @@ function loadAgentToolsMap(rootDir, opts = {}) {
17006
17194
  const reader = opts.reader ?? defaultReader2;
17007
17195
  const dirReader = opts.dirReader ?? defaultDirReader2;
17008
17196
  const dirExists = opts.dirExists ?? defaultDirExists2;
17009
- const homeAgentsDir = opts.homeAgentsDir ?? join18(homedir6(), ".config", "opencode", "agents");
17197
+ const homeAgentsDir = opts.homeAgentsDir ?? join19(homedir7(), ".config", "opencode", "agents");
17010
17198
  const candidateDirs = [
17011
- join18(rootDir, ".codeforge", "agents"),
17012
- join18(rootDir, "agents"),
17199
+ join19(rootDir, ".codeforge", "agents"),
17200
+ join19(rootDir, "agents"),
17013
17201
  homeAgentsDir
17014
17202
  ];
17015
17203
  const result = new Map;
@@ -17032,20 +17220,20 @@ function loadAgentToolsMap(rootDir, opts = {}) {
17032
17220
  for (const entry of entries) {
17033
17221
  if (!entry.endsWith(".md"))
17034
17222
  continue;
17035
- const path22 = join18(dir, entry);
17223
+ const path23 = join19(dir, entry);
17036
17224
  let content;
17037
17225
  try {
17038
- content = reader(path22);
17226
+ content = reader(path23);
17039
17227
  } catch (err) {
17040
17228
  log8.warn(`agent.md 读取失败(已跳过)`, {
17041
- path: path22,
17229
+ path: path23,
17042
17230
  error: err instanceof Error ? err.message : String(err)
17043
17231
  });
17044
17232
  continue;
17045
17233
  }
17046
17234
  const parsed = parseAgentFrontmatter(content);
17047
17235
  if (!parsed) {
17048
- log8.warn(`agent frontmatter 解析失败(已跳过)`, { path: path22 });
17236
+ log8.warn(`agent frontmatter 解析失败(已跳过)`, { path: path23 });
17049
17237
  continue;
17050
17238
  }
17051
17239
  if (result.has(parsed.name))
@@ -17205,7 +17393,7 @@ function prependUtf8Prelude(command) {
17205
17393
  return command;
17206
17394
  return PRELUDE + command;
17207
17395
  }
17208
- var handler14 = async (_ctx3) => {
17396
+ var handler14 = async (_ctx4) => {
17209
17397
  const enabled = process.platform === "win32" && process.env.CODEFORGE_DISABLE_PWSH_UTF8 !== "1";
17210
17398
  const reason = enabled ? "win32" : process.platform !== "win32" ? "non-win32" : "disabled-by-env";
17211
17399
  logLifecycle(PLUGIN_NAME14, "activate", { enabled, platform: process.platform, reason });
@@ -17237,7 +17425,7 @@ var handler14 = async (_ctx3) => {
17237
17425
 
17238
17426
  // lib/event-stream.ts
17239
17427
  import { promises as fs17 } from "node:fs";
17240
- import * as path22 from "node:path";
17428
+ import * as path23 from "node:path";
17241
17429
  async function loadSession(id, opts = {}) {
17242
17430
  const file = resolveSessionFile(id, opts);
17243
17431
  const raw = await fs17.readFile(file, "utf8");
@@ -17257,7 +17445,7 @@ async function listSessions(opts = {}) {
17257
17445
  for (const e of entries) {
17258
17446
  if (!e.isFile() || !e.name.endsWith(".jsonl"))
17259
17447
  continue;
17260
- const file = path22.join(dir, e.name);
17448
+ const file = path23.join(dir, e.name);
17261
17449
  const id = e.name.replace(/\.jsonl$/, "");
17262
17450
  try {
17263
17451
  const stat = await fs17.stat(file);
@@ -17284,11 +17472,11 @@ async function listSessions(opts = {}) {
17284
17472
  return out;
17285
17473
  }
17286
17474
  function resolveDir(opts = {}) {
17287
- const root = path22.resolve(opts.root ?? process.cwd());
17288
- 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");
17289
17477
  }
17290
17478
  function resolveSessionFile(id, opts = {}) {
17291
- return path22.join(resolveDir(opts), `${id}.jsonl`);
17479
+ return path23.join(resolveDir(opts), `${id}.jsonl`);
17292
17480
  }
17293
17481
  function parseJsonl(id, raw) {
17294
17482
  const events = [];
@@ -17552,10 +17740,10 @@ function isRecoveryWorthShowing(plan) {
17552
17740
 
17553
17741
  // lib/block-pending.ts
17554
17742
  import { promises as fs18 } from "node:fs";
17555
- import * as path23 from "node:path";
17743
+ import * as path24 from "node:path";
17556
17744
  function blockPendingFilePath(absRoot) {
17557
17745
  const rd = runtimeDir(absRoot, { ensure: false });
17558
- return path23.join(rd, "sessions", "autonomous-blocks.ndjson");
17746
+ return path24.join(rd, "sessions", "autonomous-blocks.ndjson");
17559
17747
  }
17560
17748
  function consumeLockPath(absRoot) {
17561
17749
  return blockPendingFilePath(absRoot) + ".consume.lock";
@@ -17620,7 +17808,7 @@ async function markBlocksConsumed(absRoot, entries) {
17620
17808
  if (entries.length === 0)
17621
17809
  return;
17622
17810
  const file = blockPendingFilePath(absRoot);
17623
- await fs18.mkdir(path23.dirname(file), { recursive: true });
17811
+ await fs18.mkdir(path24.dirname(file), { recursive: true });
17624
17812
  const now = new Date().toISOString();
17625
17813
  const lines = entries.map((e) => ({
17626
17814
  type: "consume",
@@ -17770,7 +17958,7 @@ var handler15 = sessionRecoveryServer;
17770
17958
 
17771
17959
  // plugins/subtasks.ts
17772
17960
  import { promises as fs19 } from "node:fs";
17773
- import * as path24 from "node:path";
17961
+ import * as path25 from "node:path";
17774
17962
 
17775
17963
  // lib/parallel-merge.ts
17776
17964
  init_worktree_ops();
@@ -18385,7 +18573,7 @@ function buildSystemPrompt(maxSubtasks) {
18385
18573
  ].join(`
18386
18574
  `);
18387
18575
  }
18388
- async function decomposeTask(description16, opts) {
18576
+ async function decomposeTask(description17, opts) {
18389
18577
  const log10 = opts.log ?? (() => {});
18390
18578
  if (opts.mockResponse) {
18391
18579
  return validateAndFinalize(opts.mockResponse, undefined, log10, opts.maxSubtasks ?? DEFAULT_MAX_SUBTASKS);
@@ -18395,7 +18583,7 @@ async function decomposeTask(description16, opts) {
18395
18583
  let childSessionId;
18396
18584
  try {
18397
18585
  const created = await opts.client.session.create({
18398
- body: { title: `decompose:${clip5(description16, 60)}` },
18586
+ body: { title: `decompose:${clip5(description17, 60)}` },
18399
18587
  query: opts.directory ? { directory: opts.directory } : undefined
18400
18588
  });
18401
18589
  if (created.error || !created.data?.id) {
@@ -18412,7 +18600,7 @@ async function decomposeTask(description16, opts) {
18412
18600
  path: { id: childSessionId },
18413
18601
  body: {
18414
18602
  system: systemText,
18415
- parts: [{ type: "text", text: description16 }]
18603
+ parts: [{ type: "text", text: description17 }]
18416
18604
  },
18417
18605
  query: opts.directory ? { directory: opts.directory } : undefined
18418
18606
  }));
@@ -18594,7 +18782,7 @@ function sleep2(ms) {
18594
18782
  // plugins/subtasks.ts
18595
18783
  var PLUGIN_NAME16 = "subtasks";
18596
18784
  function getLogFile(root = process.cwd()) {
18597
- return path24.join(runtimeDir(root), "logs", "subtasks.log");
18785
+ return path25.join(runtimeDir(root), "logs", "subtasks.log");
18598
18786
  }
18599
18787
  var VERB_RE = /^([a-zA-Z]{3,12})/;
18600
18788
  var CN_VERBS = [
@@ -18899,7 +19087,7 @@ async function writeLog(level, msg, data) {
18899
19087
  `;
18900
19088
  try {
18901
19089
  const logFile = getLogFile();
18902
- await fs19.mkdir(path24.dirname(logFile), { recursive: true });
19090
+ await fs19.mkdir(path25.dirname(logFile), { recursive: true });
18903
19091
  await fs19.appendFile(logFile, line, "utf8");
18904
19092
  } catch {}
18905
19093
  }
@@ -18916,8 +19104,8 @@ var subtasksServer = async (ctx) => {
18916
19104
  try {
18917
19105
  if (input?.command !== "parallel")
18918
19106
  return;
18919
- const description16 = (input.arguments ?? "").trim();
18920
- if (!description16) {
19107
+ const description17 = (input.arguments ?? "").trim();
19108
+ if (!description17) {
18921
19109
  return;
18922
19110
  }
18923
19111
  let autoMerge = false;
@@ -18956,7 +19144,7 @@ var subtasksServer = async (ctx) => {
18956
19144
  } : undefined;
18957
19145
  const replyLines = [];
18958
19146
  const messageCtx = {
18959
- content: `/parallel ${description16}`,
19147
+ content: `/parallel ${description17}`,
18960
19148
  reply: (s) => {
18961
19149
  replyLines.push(s);
18962
19150
  return Promise.resolve();
@@ -19532,7 +19720,7 @@ var handler18 = tokenManagerServer;
19532
19720
 
19533
19721
  // plugins/tool-policy.ts
19534
19722
  import { promises as fs20 } from "node:fs";
19535
- import * as path26 from "node:path";
19723
+ import * as path27 from "node:path";
19536
19724
 
19537
19725
  // lib/tool-risk.ts
19538
19726
  var RISK_PATTERNS = [
@@ -19686,7 +19874,7 @@ function buildHaystackFor(args, matchOn) {
19686
19874
  }
19687
19875
 
19688
19876
  // lib/file-regex-acl.ts
19689
- import * as path25 from "node:path";
19877
+ import * as path26 from "node:path";
19690
19878
  function compileRule(r) {
19691
19879
  if (r instanceof RegExp)
19692
19880
  return r;
@@ -19752,7 +19940,7 @@ function normalizePath2(p) {
19752
19940
  let s = p.replace(/\\/g, "/");
19753
19941
  if (s.startsWith("./"))
19754
19942
  s = s.slice(2);
19755
- s = path25.posix.normalize(s);
19943
+ s = path26.posix.normalize(s);
19756
19944
  return s;
19757
19945
  }
19758
19946
  function checkFileAccess(acl, file, op) {
@@ -19855,9 +20043,9 @@ function decideToolCall(ctx, cfg = {}, currentAgent) {
19855
20043
  const action = risks.length > 0 || worstAcl === "deny" ? "deny" : "allow";
19856
20044
  return { action, reasons, risks, acl: aclResults };
19857
20045
  }
19858
- var POLICY_PATH = path26.join(".codeforge", "policy.json");
20046
+ var POLICY_PATH = path27.join(".codeforge", "policy.json");
19859
20047
  async function loadPolicy(root = process.cwd()) {
19860
- const file = path26.join(root, POLICY_PATH);
20048
+ const file = path27.join(root, POLICY_PATH);
19861
20049
  try {
19862
20050
  const raw = await fs20.readFile(file, "utf8");
19863
20051
  const data = JSON.parse(raw);
@@ -19956,8 +20144,8 @@ var handler19 = toolPolicyServer;
19956
20144
 
19957
20145
  // plugins/update-checker.ts
19958
20146
  import { existsSync as existsSync6, rmSync } from "node:fs";
19959
- import { homedir as homedir8 } from "node:os";
19960
- import { join as join24 } from "node:path";
20147
+ import { homedir as homedir9 } from "node:os";
20148
+ import { join as join25 } from "node:path";
19961
20149
  import { spawnSync as spawnSync2 } from "node:child_process";
19962
20150
 
19963
20151
  // lib/update-checker-impl.ts
@@ -19974,8 +20162,8 @@ import {
19974
20162
  unlinkSync,
19975
20163
  writeFileSync as writeFileSync2
19976
20164
  } from "node:fs";
19977
- import { homedir as homedir7, tmpdir } from "node:os";
19978
- 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";
19979
20167
  import { fileURLToPath as fileURLToPath2 } from "node:url";
19980
20168
  import * as https from "node:https";
19981
20169
  import * as zlib from "node:zlib";
@@ -19983,7 +20171,7 @@ import * as zlib from "node:zlib";
19983
20171
  // lib/version-injected.ts
19984
20172
  function getInjectedVersion() {
19985
20173
  try {
19986
- const v = "0.6.8";
20174
+ const v = "0.6.12";
19987
20175
  if (typeof v === "string" && /^\d+\.\d+\.\d+/.test(v)) {
19988
20176
  return v;
19989
20177
  }
@@ -20073,17 +20261,17 @@ function readLocalVersion() {
20073
20261
  try {
20074
20262
  const here = fileURLToPath2(import.meta.url);
20075
20263
  const root = dirname14(dirname14(here));
20076
- const pkg = JSON.parse(readFileSync5(join23(root, "package.json"), "utf8"));
20264
+ const pkg = JSON.parse(readFileSync5(join24(root, "package.json"), "utf8"));
20077
20265
  return typeof pkg.version === "string" ? pkg.version : "0.0.0";
20078
20266
  } catch {
20079
20267
  return "0.0.0";
20080
20268
  }
20081
20269
  }
20082
20270
  function defaultCacheDir() {
20083
- return process.env["CODEFORGE_CACHE_DIR"] ?? join23(homedir7(), ".cache", "codeforge");
20271
+ return process.env["CODEFORGE_CACHE_DIR"] ?? join24(homedir8(), ".cache", "codeforge");
20084
20272
  }
20085
20273
  function defaultCacheFile() {
20086
- return join23(defaultCacheDir(), "update-check.json");
20274
+ return join24(defaultCacheDir(), "update-check.json");
20087
20275
  }
20088
20276
  function readCache(file) {
20089
20277
  try {
@@ -20239,14 +20427,14 @@ function defaultHttpFetcher(url2, timeoutMs) {
20239
20427
  });
20240
20428
  }
20241
20429
  async function downloadAndExtractBundle(opts) {
20242
- const tmpRoot = opts.tmpDir ?? mkdtempSync(join23(tmpdir(), "codeforge-update-"));
20430
+ const tmpRoot = opts.tmpDir ?? mkdtempSync(join24(tmpdir(), "codeforge-update-"));
20243
20431
  mkdirSync3(tmpRoot, { recursive: true });
20244
20432
  const fetcher = opts.tarballFetcher ?? defaultBinaryFetcher;
20245
20433
  const tarballBuf = await fetcher(opts.tarballUrl);
20246
20434
  verifyIntegrity(tarballBuf, opts.expectedIntegrity);
20247
20435
  const tarBuf = zlib.gunzipSync(tarballBuf);
20248
20436
  extractTarToDir(tarBuf, tmpRoot);
20249
- const bundlePath = join23(tmpRoot, "package", "dist", "index.js");
20437
+ const bundlePath = join24(tmpRoot, "package", "dist", "index.js");
20250
20438
  if (!existsSync5(bundlePath)) {
20251
20439
  throw new Error(`bundle_not_found: ${bundlePath}`);
20252
20440
  }
@@ -20286,11 +20474,11 @@ function extractTarToDir(tarBuf, destRoot) {
20286
20474
  offset += 512;
20287
20475
  if (typeFlag === "0" || typeFlag === "" || typeFlag === "\x00") {
20288
20476
  const fileBuf = tarBuf.subarray(offset, offset + size);
20289
- const dest = join23(destRoot, fullName);
20477
+ const dest = join24(destRoot, fullName);
20290
20478
  mkdirSync3(dirname14(dest), { recursive: true });
20291
20479
  writeFileSync2(dest, fileBuf);
20292
20480
  } else if (typeFlag === "5") {
20293
- mkdirSync3(join23(destRoot, fullName), { recursive: true });
20481
+ mkdirSync3(join24(destRoot, fullName), { recursive: true });
20294
20482
  }
20295
20483
  offset += Math.ceil(size / 512) * 512;
20296
20484
  }
@@ -20399,7 +20587,7 @@ function cleanupOldBackups(target, keep) {
20399
20587
  const base = target.substring(dir.length + 1);
20400
20588
  const prefix = `${base}.bak.`;
20401
20589
  const all = readdirSync3(dir).filter((f) => f.startsWith(prefix)).map((f) => {
20402
- const full = join23(dir, f);
20590
+ const full = join24(dir, f);
20403
20591
  let mtimeMs = 0;
20404
20592
  try {
20405
20593
  mtimeMs = statSync4(full).mtimeMs;
@@ -20421,7 +20609,7 @@ function loadCompatibility(opts) {
20421
20609
  const root = opts?.cwd ?? inferPluginRoot();
20422
20610
  if (!root)
20423
20611
  return null;
20424
- file = join23(root, "compatibility.json");
20612
+ file = join24(root, "compatibility.json");
20425
20613
  }
20426
20614
  if (!existsSync5(file))
20427
20615
  return null;
@@ -20612,11 +20800,11 @@ var updateCheckerServer = async (ctx) => {
20612
20800
  expectedIntegrity: npmResult.integrity
20613
20801
  });
20614
20802
  try {
20615
- const installMjs = join24(extractDir, "package", "install.mjs");
20803
+ const installMjs = join25(extractDir, "package", "install.mjs");
20616
20804
  if (existsSync6(installMjs)) {
20617
20805
  const nodeBin = resolveNodeBin();
20618
20806
  const r = spawnSync2(nodeBin, [installMjs, "--global", "--skip-build"], {
20619
- cwd: join24(extractDir, "package"),
20807
+ cwd: join25(extractDir, "package"),
20620
20808
  stdio: "pipe",
20621
20809
  encoding: "utf8"
20622
20810
  });
@@ -20717,14 +20905,14 @@ function detectOpencodeVersion() {
20717
20905
  }
20718
20906
  function getOpencodeBundlePath() {
20719
20907
  const candidates = [];
20720
- candidates.push(join24(homedir8(), ".config", "opencode", "codeforge", "index.js"));
20908
+ candidates.push(join25(homedir9(), ".config", "opencode", "codeforge", "index.js"));
20721
20909
  if (process.platform === "win32") {
20722
20910
  const appData = process.env["APPDATA"];
20723
20911
  if (appData)
20724
- candidates.push(join24(appData, "opencode", "codeforge", "index.js"));
20912
+ candidates.push(join25(appData, "opencode", "codeforge", "index.js"));
20725
20913
  const localAppData = process.env["LOCALAPPDATA"];
20726
20914
  if (localAppData)
20727
- candidates.push(join24(localAppData, "opencode", "codeforge", "index.js"));
20915
+ candidates.push(join25(localAppData, "opencode", "codeforge", "index.js"));
20728
20916
  }
20729
20917
  for (const c of candidates) {
20730
20918
  if (existsSync6(c))
@@ -20785,60 +20973,60 @@ async function postToast(ctx, message) {
20785
20973
  var handler20 = updateCheckerServer;
20786
20974
 
20787
20975
  // plugins/workflow-engine.ts
20788
- import * as path28 from "node:path";
20976
+ import * as path29 from "node:path";
20789
20977
 
20790
20978
  // lib/workflow-loader.ts
20791
20979
  import { promises as fs21 } from "node:fs";
20792
- import * as path27 from "node:path";
20793
- import { z as z18 } from "zod";
20794
- var ActionSchema = z18.object({
20795
- tool: z18.string().min(1, "action.tool 不能为空"),
20796
- args: z18.record(z18.string(), z18.unknown()).optional().default({}),
20797
- 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()
20798
20986
  });
20799
- var StepSchema = z18.object({
20800
- name: z18.string().min(1, "step.name 不能为空"),
20801
- agent: z18.string().min(1, "step.agent 不能为空").describe("agent 名(与 agents/<name>.md 对应)"),
20802
- description: z18.string().optional(),
20803
- inject_context: z18.record(z18.string(), z18.unknown()).optional(),
20804
- requires_human_approval: z18.boolean().optional().default(false),
20805
- actions: z18.array(ActionSchema).optional().default([]),
20806
- on_error: z18.enum(["retry", "skip", "abort"]).optional().default("abort"),
20807
- max_retries: z18.number().int().min(0).max(10).optional().default(2),
20808
- timeout: z18.string().regex(/^\d+(?:ms|s|m|h)$/, "timeout 必须是 数字+单位(ms/s/m/h),如 5m").optional(),
20809
- auto_feedback: z18.object({
20810
- test_cmd: z18.string().optional().describe("测试命令,如 npm test"),
20811
- lint_cmd: z18.string().optional().describe("lint 命令,如 npm run lint"),
20812
- max_retries: z18.number().int().min(1).max(10).optional().default(3),
20813
- error_excerpt_lines: z18.number().int().min(1).max(50).optional().default(5),
20814
- 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")
20815
21003
  }).refine((d) => Boolean(d.test_cmd || d.lint_cmd), { message: "auto_feedback 必须至少配置 test_cmd 或 lint_cmd 之一" }).optional(),
20816
- on_decision: z18.object({
20817
- APPROVE: z18.union([
20818
- z18.literal("continue"),
20819
- z18.literal("abort"),
20820
- 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) })
20821
21009
  ]).optional(),
20822
- REQUEST_CHANGES: z18.union([
20823
- z18.literal("continue"),
20824
- z18.literal("abort"),
20825
- 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) })
20826
21014
  ]).optional(),
20827
- BLOCK: z18.union([
20828
- z18.literal("continue"),
20829
- z18.literal("abort"),
20830
- 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) })
20831
21019
  ]).optional()
20832
21020
  }).refine((d) => Boolean(d.APPROVE || d.REQUEST_CHANGES || d.BLOCK), { message: "on_decision 必须至少配置 APPROVE / REQUEST_CHANGES / BLOCK 之一" }).optional()
20833
21021
  }).strict();
20834
- var WorkflowSchema = z18.object({
20835
- name: z18.string().min(1, "workflow.name 不能为空"),
20836
- description: z18.string().optional().default(""),
20837
- version: z18.string().optional().default("1.0.0"),
20838
- trigger: z18.string().min(1).refine((v) => /^\/[a-z][\w-]*$/.test(v) || /^event:[a-z][\w.-]+$/.test(v), "trigger 必须是 /command-name 或 event:xxx 形式"),
20839
- context_template: z18.string().optional(),
20840
- max_loops: z18.number().int().min(1).max(10).optional().default(3),
20841
- 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 不能为空")
20842
21030
  }).strict();
20843
21031
  function parseWorkflowYaml(yaml, sourcePath = "<inline>") {
20844
21032
  let raw;
@@ -20902,7 +21090,7 @@ async function loadWorkflowsFromDir(dir) {
20902
21090
  continue;
20903
21091
  if (!/\.ya?ml$/i.test(name))
20904
21092
  continue;
20905
- const full = path27.join(dir, name);
21093
+ const full = path28.join(dir, name);
20906
21094
  const r = await loadWorkflowFromFile(full);
20907
21095
  if (r.ok)
20908
21096
  loaded.push(r);
@@ -21292,7 +21480,7 @@ async function handleCommandInvoked(raw, workflowsDir = "workflows") {
21292
21480
  }
21293
21481
  var workflowEngineServer = async (ctx) => {
21294
21482
  const directory = ctx.directory ?? process.cwd();
21295
- const workflowsDir = path28.join(directory, "workflows");
21483
+ const workflowsDir = path29.join(directory, "workflows");
21296
21484
  ensureRegistry(workflowsDir).catch((err) => fallbackLog2.warn(`[${PLUGIN_NAME21}] preload workflows failed`, {
21297
21485
  error: err instanceof Error ? err.message : String(err)
21298
21486
  }));
@@ -21336,7 +21524,7 @@ var workflowEngineServer = async (ctx) => {
21336
21524
  var handler21 = workflowEngineServer;
21337
21525
 
21338
21526
  // plugins/session-worktree-guard.ts
21339
- import path29 from "node:path";
21527
+ import path30 from "node:path";
21340
21528
  import { stat } from "node:fs/promises";
21341
21529
  var PLUGIN_NAME22 = "session-worktree-guard";
21342
21530
  logLifecycle(PLUGIN_NAME22, "import", {});
@@ -21372,6 +21560,31 @@ function buildGitVcsWriteRegex(mainRoot) {
21372
21560
  var WRITE_TOOLS = new Set(["write", "edit", "ast_edit"]);
21373
21561
  var TOUCH_THROTTLE_MS = 5 * 60000;
21374
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
+ }
21375
21588
  var _sessionIdMissingWarned = false;
21376
21589
  var _bindFailNotified = new Set;
21377
21590
  function formatLazyBindDenyReason(input) {
@@ -21448,23 +21661,23 @@ var MERGE_CALLER_WHITELIST = new Set([
21448
21661
  var FORCE_MERGE_CALLER_WHITELIST = new Set([
21449
21662
  "codeforge"
21450
21663
  ]);
21451
- var CODEFORGE_WORKTREE_DIR_NAME = path29.join(".git", "codeforge-worktrees");
21664
+ var CODEFORGE_WORKTREE_DIR_NAME = path30.join(".git", "codeforge-worktrees");
21452
21665
  function worktreesRoot(mainRoot) {
21453
- return path29.join(mainRoot, CODEFORGE_WORKTREE_DIR_NAME);
21666
+ return path30.join(mainRoot, CODEFORGE_WORKTREE_DIR_NAME);
21454
21667
  }
21455
21668
  function isInsideAnyWorktreeDir(absPath, mainRoot) {
21456
- if (!path29.isAbsolute(absPath))
21669
+ if (!path30.isAbsolute(absPath))
21457
21670
  return false;
21458
21671
  const root = worktreesRoot(mainRoot);
21459
21672
  if (absPath === root)
21460
21673
  return false;
21461
- const prefix = root.endsWith(path29.sep) ? root : root + path29.sep;
21674
+ const prefix = root.endsWith(path30.sep) ? root : root + path30.sep;
21462
21675
  return absPath.startsWith(prefix);
21463
21676
  }
21464
21677
  function rewritePath(value, mainRoot, worktreeRoot) {
21465
21678
  if (!value)
21466
21679
  return null;
21467
- const resolved = path29.isAbsolute(value) ? value : path29.resolve(mainRoot, value);
21680
+ const resolved = path30.isAbsolute(value) ? value : path30.resolve(mainRoot, value);
21468
21681
  const wtPrefix2 = worktreeRoot.endsWith("/") ? worktreeRoot : worktreeRoot + "/";
21469
21682
  if (resolved === worktreeRoot || resolved.startsWith(wtPrefix2)) {
21470
21683
  return null;
@@ -21502,7 +21715,7 @@ function commandContainsMainRootExcludingWorktree(command, mainRoot, worktreePat
21502
21715
  }
21503
21716
  }
21504
21717
  const wtRoot = worktreesRoot(mainRoot);
21505
- const wtRootPrefix = wtRoot + path29.sep;
21718
+ const wtRootPrefix = wtRoot + path30.sep;
21506
21719
  const escapedWtRootPrefix = escapeRegex(wtRootPrefix);
21507
21720
  const wtPathPattern = escapedWtRootPrefix + `[^\\s'"\\x60)]*`;
21508
21721
  const allWorktreePathsReForEscape = new RegExp(wtPathPattern, "g");
@@ -21557,8 +21770,8 @@ function collectWritePaths(toolName, argsObj, worktreeRoot) {
21557
21770
  const candidate = toolName === "write" || toolName === "edit" ? argsObj["filePath"] : toolName === "ast_edit" ? argsObj["target"] : undefined;
21558
21771
  if (typeof candidate !== "string" || candidate.length === 0)
21559
21772
  return out;
21560
- const abs = path29.isAbsolute(candidate) ? candidate : path29.resolve(worktreeRoot, candidate);
21561
- 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("/");
21562
21775
  out.push(rel);
21563
21776
  return out;
21564
21777
  }
@@ -21569,7 +21782,7 @@ async function isCodeforgeManagedProject(mainRoot, opts = {}) {
21569
21782
  if (force === "1" || force === "true" || force === "yes")
21570
21783
  return true;
21571
21784
  try {
21572
- const st = await stat(path29.join(mainRoot, ".codeforge"));
21785
+ const st = await stat(path30.join(mainRoot, ".codeforge"));
21573
21786
  return st.isDirectory();
21574
21787
  } catch {
21575
21788
  return false;
@@ -21583,6 +21796,37 @@ function resolveMainRoot2(rawDir) {
21583
21796
  }
21584
21797
  return rawDir;
21585
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
+ }
21586
21830
  var sessionWorktreeGuardPlugin = async (ctx) => {
21587
21831
  const disableEnv = process.env["CODEFORGE_DISABLE_WORKTREE_GUARD"];
21588
21832
  if (disableEnv === "1" || disableEnv === "true" || disableEnv === "yes") {
@@ -21647,6 +21891,29 @@ var sessionWorktreeGuardPlugin = async (ctx) => {
21647
21891
  await safeAsync(PLUGIN_NAME22, "tool.execute.before", async () => {
21648
21892
  const toolName = input.tool;
21649
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
+ };
21650
21917
  let entry = null;
21651
21918
  try {
21652
21919
  entry = await getSessionWorktree(sessionId, mainRoot);
@@ -21690,6 +21957,12 @@ var sessionWorktreeGuardPlugin = async (ctx) => {
21690
21957
  if (!isWriteOperation(toolName, argsObj, mainRoot)) {
21691
21958
  return;
21692
21959
  }
21960
+ if (midMerge) {
21961
+ emitMergeBypassNotice("lazy-bind", {
21962
+ reason: "mid-merge: skip lazy-bind, passthrough to main repo"
21963
+ });
21964
+ return;
21965
+ }
21693
21966
  try {
21694
21967
  const parentId = lookupParentSessionId2(sessionId);
21695
21968
  if (parentId) {
@@ -21706,6 +21979,18 @@ var sessionWorktreeGuardPlugin = async (ctx) => {
21706
21979
  parent_branch: parentEntry.branch,
21707
21980
  parent_worktree: parentEntry.worktreePath
21708
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
+ });
21709
21994
  }
21710
21995
  }
21711
21996
  } catch (lookupErr) {
@@ -21935,6 +22220,11 @@ var sessionWorktreeGuardPlugin = async (ctx) => {
21935
22220
  if (toolName === "bash") {
21936
22221
  const command = argsObj["command"];
21937
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
+ }
21938
22228
  const caller = await resolveAgentForGuard({ sessionID: input.sessionID, agent: input.agent }, ctx.client, log13);
21939
22229
  if (caller !== null && CLASS_B_CALLER_WHITELIST.has(caller)) {
21940
22230
  log13.debug?.(`[class-b-whitelist] allow caller=${caller}`, { sessionId, tool: toolName, command: command.slice(0, 200) });
@@ -21966,182 +22256,85 @@ var sessionWorktreeGuardPlugin = async (ctx) => {
21966
22256
  }
21967
22257
  }
21968
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
+ };
21969
22280
  if (toolName === "write" || toolName === "edit") {
21970
22281
  const filePath = argsObj["filePath"];
21971
- if (typeof filePath === "string") {
21972
- const newPath = rewritePath(filePath, mainRoot, worktreePath);
21973
- if (newPath !== null) {
21974
- log13.info(`rewrote ${toolName}.filePath: ${filePath} → ${newPath}`);
21975
- safeWriteLog(PLUGIN_NAME22, {
21976
- hook: "tool.execute.before",
21977
- tool: toolName,
21978
- field: "filePath",
21979
- before: filePath,
21980
- after: newPath,
21981
- action: "rewrite"
21982
- });
21983
- output.args["filePath"] = newPath;
21984
- }
21985
- }
22282
+ if (typeof filePath === "string")
22283
+ applyRewrite("filePath", filePath);
21986
22284
  }
21987
22285
  if (toolName === "ast_edit") {
21988
22286
  const target = argsObj["target"];
21989
- if (typeof target === "string") {
21990
- const newTarget = rewritePath(target, mainRoot, worktreePath);
21991
- if (newTarget !== null) {
21992
- log13.info(`rewrote ast_edit.target: ${target} → ${newTarget}`);
21993
- safeWriteLog(PLUGIN_NAME22, {
21994
- hook: "tool.execute.before",
21995
- tool: toolName,
21996
- field: "target",
21997
- before: target,
21998
- after: newTarget,
21999
- action: "rewrite"
22000
- });
22001
- output.args["target"] = newTarget;
22002
- }
22003
- }
22287
+ if (typeof target === "string")
22288
+ applyRewrite("target", target);
22004
22289
  const root = argsObj["root"];
22005
- if (typeof root === "string") {
22006
- const newRoot = rewritePath(root, mainRoot, worktreePath);
22007
- if (newRoot !== null) {
22008
- log13.info(`rewrote ast_edit.root: ${root} → ${newRoot}`);
22009
- safeWriteLog(PLUGIN_NAME22, {
22010
- hook: "tool.execute.before",
22011
- tool: toolName,
22012
- field: "root",
22013
- before: root,
22014
- after: newRoot,
22015
- action: "rewrite"
22016
- });
22017
- output.args["root"] = newRoot;
22018
- }
22019
- }
22290
+ if (typeof root === "string")
22291
+ applyRewrite("root", root);
22020
22292
  }
22021
22293
  if (toolName === "bash") {
22022
22294
  const workdir = argsObj["workdir"];
22023
- if (typeof workdir === "string") {
22024
- const newWorkdir = rewritePath(workdir, mainRoot, worktreePath);
22025
- if (newWorkdir !== null) {
22026
- log13.info(`rewrote bash.workdir: ${workdir} → ${newWorkdir}`);
22027
- safeWriteLog(PLUGIN_NAME22, {
22028
- hook: "tool.execute.before",
22029
- tool: toolName,
22030
- field: "workdir",
22031
- before: workdir,
22032
- after: newWorkdir,
22033
- action: "rewrite"
22034
- });
22035
- output.args["workdir"] = newWorkdir;
22036
- }
22037
- }
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
+ });
22038
22305
  }
22039
22306
  });
22040
22307
  if (denied)
22041
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
+ });
22042
22331
  }
22043
22332
  };
22044
22333
  };
22045
22334
  var handler22 = sessionWorktreeGuardPlugin;
22046
22335
 
22047
- // lib/opencode-session-probe.ts
22048
- import * as path30 from "node:path";
22049
- import * as os6 from "node:os";
22050
- import { createRequire as createRequire2 } from "node:module";
22051
- var requireFromHere = createRequire2(import.meta.url);
22052
- var DEFAULT_LIVENESS_MS = 6 * 60 * 60000;
22053
- var DEFAULT_DB_PATH = path30.join(os6.homedir(), ".local/share/opencode/opencode.db");
22054
- function createSessionProbe(opts = {}) {
22055
- const dbPath = opts.dbPath ?? DEFAULT_DB_PATH;
22056
- const httpBaseUrl = opts.httpBaseUrl ?? process.env["OPENCODE_SERVER_URL"];
22057
- const livenessWindowMs = opts.livenessWindowMs ?? DEFAULT_LIVENESS_MS;
22058
- const timeoutMs = opts.timeoutMs ?? 1500;
22059
- let db = null;
22060
- let dbInitTried = false;
22061
- let dbStmt = null;
22062
- async function tryOpenDb() {
22063
- if (dbInitTried)
22064
- return db != null;
22065
- dbInitTried = true;
22066
- try {
22067
- const mod = requireFromHere("node:sqlite");
22068
- try {
22069
- db = new mod.DatabaseSync(dbPath, { readOnly: true });
22070
- } catch {
22071
- db = null;
22072
- dbStmt = null;
22073
- return false;
22074
- }
22075
- dbStmt = db.prepare("SELECT time_updated, time_archived FROM session WHERE id = ? LIMIT 1");
22076
- return true;
22077
- } catch {
22078
- db = null;
22079
- dbStmt = null;
22080
- return false;
22081
- }
22082
- }
22083
- async function probeHttp(sessionId) {
22084
- if (!httpBaseUrl)
22085
- return null;
22086
- try {
22087
- const ctrl = new AbortController;
22088
- const t = setTimeout(() => ctrl.abort(), timeoutMs);
22089
- const res = await fetch(`${httpBaseUrl.replace(/\/$/, "")}/session`, {
22090
- signal: ctrl.signal
22091
- }).finally(() => clearTimeout(t));
22092
- if (!res.ok)
22093
- return null;
22094
- const list = await res.json();
22095
- const hit = Array.isArray(list) && list.some((s) => s.id === sessionId);
22096
- return { alive: hit, source: "http" };
22097
- } catch {
22098
- return null;
22099
- }
22100
- }
22101
- async function probeSqlite(sessionId) {
22102
- if (!await tryOpenDb() || !dbStmt)
22103
- return null;
22104
- try {
22105
- const row = dbStmt.get(sessionId);
22106
- if (!row) {
22107
- return { alive: false, source: "sqlite" };
22108
- }
22109
- if (row.time_archived != null) {
22110
- return {
22111
- alive: false,
22112
- source: "sqlite",
22113
- time_archived: row.time_archived
22114
- };
22115
- }
22116
- const now = Date.now();
22117
- const tu = Number(row.time_updated) || 0;
22118
- const alive = now - tu < livenessWindowMs;
22119
- return { alive, source: "sqlite", time_updated: tu, time_archived: null };
22120
- } catch {
22121
- return null;
22122
- }
22123
- }
22124
- return {
22125
- async isSessionAlive(sessionId) {
22126
- const http = await probeHttp(sessionId);
22127
- if (http)
22128
- return http;
22129
- const sql = await probeSqlite(sessionId);
22130
- if (sql)
22131
- return sql;
22132
- return { alive: true, source: "unknown" };
22133
- },
22134
- close() {
22135
- try {
22136
- db?.close?.();
22137
- } catch {}
22138
- db = null;
22139
- dbStmt = null;
22140
- }
22141
- };
22142
- }
22143
-
22144
22336
  // plugins/worktree-lifecycle.ts
22337
+ init_worktree_ops();
22145
22338
  var PLUGIN_NAME23 = "worktree-lifecycle";
22146
22339
  logLifecycle(PLUGIN_NAME23, "import", {});
22147
22340
  var IDLE_TOAST_THROTTLE_MS = 60 * 60000;
@@ -22298,10 +22491,17 @@ var worktreeLifecyclePlugin = async (ctx) => {
22298
22491
  worktreePath: entry.worktreePath
22299
22492
  });
22300
22493
  } catch (err) {
22301
- log14.warn(`[lifecycle] discardSession failed`, {
22302
- sessionId: ended.sessionID,
22303
- error: err instanceof Error ? err.message : String(err)
22304
- });
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
+ }
22305
22505
  }
22306
22506
  lastIdleToastAt.delete(ended.sessionID);
22307
22507
  return;