@andyqiu/codeforge 0.8.2 → 0.8.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -372,17 +372,17 @@ var require_visit = __commonJS((exports) => {
372
372
  visit.BREAK = BREAK;
373
373
  visit.SKIP = SKIP;
374
374
  visit.REMOVE = REMOVE;
375
- function visit_(key, node, visitor, path20) {
376
- const ctrl = callVisitor(key, node, visitor, path20);
375
+ function visit_(key, node, visitor, path21) {
376
+ const ctrl = callVisitor(key, node, visitor, path21);
377
377
  if (identity.isNode(ctrl) || identity.isPair(ctrl)) {
378
- replaceNode(key, path20, ctrl);
379
- return visit_(key, ctrl, visitor, path20);
378
+ replaceNode(key, path21, ctrl);
379
+ return visit_(key, ctrl, visitor, path21);
380
380
  }
381
381
  if (typeof ctrl !== "symbol") {
382
382
  if (identity.isCollection(node)) {
383
- path20 = Object.freeze(path20.concat(node));
383
+ path21 = Object.freeze(path21.concat(node));
384
384
  for (let i = 0;i < node.items.length; ++i) {
385
- const ci = visit_(i, node.items[i], visitor, path20);
385
+ const ci = visit_(i, node.items[i], visitor, path21);
386
386
  if (typeof ci === "number")
387
387
  i = ci - 1;
388
388
  else if (ci === BREAK)
@@ -393,13 +393,13 @@ var require_visit = __commonJS((exports) => {
393
393
  }
394
394
  }
395
395
  } else if (identity.isPair(node)) {
396
- path20 = Object.freeze(path20.concat(node));
397
- const ck = visit_("key", node.key, visitor, path20);
396
+ path21 = Object.freeze(path21.concat(node));
397
+ const ck = visit_("key", node.key, visitor, path21);
398
398
  if (ck === BREAK)
399
399
  return BREAK;
400
400
  else if (ck === REMOVE)
401
401
  node.key = null;
402
- const cv = visit_("value", node.value, visitor, path20);
402
+ const cv = visit_("value", node.value, visitor, path21);
403
403
  if (cv === BREAK)
404
404
  return BREAK;
405
405
  else if (cv === REMOVE)
@@ -420,17 +420,17 @@ var require_visit = __commonJS((exports) => {
420
420
  visitAsync.BREAK = BREAK;
421
421
  visitAsync.SKIP = SKIP;
422
422
  visitAsync.REMOVE = REMOVE;
423
- async function visitAsync_(key, node, visitor, path20) {
424
- 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);
425
425
  if (identity.isNode(ctrl) || identity.isPair(ctrl)) {
426
- replaceNode(key, path20, ctrl);
427
- return visitAsync_(key, ctrl, visitor, path20);
426
+ replaceNode(key, path21, ctrl);
427
+ return visitAsync_(key, ctrl, visitor, path21);
428
428
  }
429
429
  if (typeof ctrl !== "symbol") {
430
430
  if (identity.isCollection(node)) {
431
- path20 = Object.freeze(path20.concat(node));
431
+ path21 = Object.freeze(path21.concat(node));
432
432
  for (let i = 0;i < node.items.length; ++i) {
433
- const ci = await visitAsync_(i, node.items[i], visitor, path20);
433
+ const ci = await visitAsync_(i, node.items[i], visitor, path21);
434
434
  if (typeof ci === "number")
435
435
  i = ci - 1;
436
436
  else if (ci === BREAK)
@@ -441,13 +441,13 @@ var require_visit = __commonJS((exports) => {
441
441
  }
442
442
  }
443
443
  } else if (identity.isPair(node)) {
444
- path20 = Object.freeze(path20.concat(node));
445
- 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);
446
446
  if (ck === BREAK)
447
447
  return BREAK;
448
448
  else if (ck === REMOVE)
449
449
  node.key = null;
450
- const cv = await visitAsync_("value", node.value, visitor, path20);
450
+ const cv = await visitAsync_("value", node.value, visitor, path21);
451
451
  if (cv === BREAK)
452
452
  return BREAK;
453
453
  else if (cv === REMOVE)
@@ -474,23 +474,23 @@ var require_visit = __commonJS((exports) => {
474
474
  }
475
475
  return visitor;
476
476
  }
477
- function callVisitor(key, node, visitor, path20) {
477
+ function callVisitor(key, node, visitor, path21) {
478
478
  if (typeof visitor === "function")
479
- return visitor(key, node, path20);
479
+ return visitor(key, node, path21);
480
480
  if (identity.isMap(node))
481
- return visitor.Map?.(key, node, path20);
481
+ return visitor.Map?.(key, node, path21);
482
482
  if (identity.isSeq(node))
483
- return visitor.Seq?.(key, node, path20);
483
+ return visitor.Seq?.(key, node, path21);
484
484
  if (identity.isPair(node))
485
- return visitor.Pair?.(key, node, path20);
485
+ return visitor.Pair?.(key, node, path21);
486
486
  if (identity.isScalar(node))
487
- return visitor.Scalar?.(key, node, path20);
487
+ return visitor.Scalar?.(key, node, path21);
488
488
  if (identity.isAlias(node))
489
- return visitor.Alias?.(key, node, path20);
489
+ return visitor.Alias?.(key, node, path21);
490
490
  return;
491
491
  }
492
- function replaceNode(key, path20, node) {
493
- const parent = path20[path20.length - 1];
492
+ function replaceNode(key, path21, node) {
493
+ const parent = path21[path21.length - 1];
494
494
  if (identity.isCollection(parent)) {
495
495
  parent.items[key] = node;
496
496
  } else if (identity.isPair(parent)) {
@@ -1049,10 +1049,10 @@ var require_Collection = __commonJS((exports) => {
1049
1049
  var createNode = require_createNode();
1050
1050
  var identity = require_identity();
1051
1051
  var Node = require_Node();
1052
- function collectionFromPath(schema, path20, value) {
1052
+ function collectionFromPath(schema, path21, value) {
1053
1053
  let v = value;
1054
- for (let i = path20.length - 1;i >= 0; --i) {
1055
- const k = path20[i];
1054
+ for (let i = path21.length - 1;i >= 0; --i) {
1055
+ const k = path21[i];
1056
1056
  if (typeof k === "number" && Number.isInteger(k) && k >= 0) {
1057
1057
  const a = [];
1058
1058
  a[k] = v;
@@ -1071,7 +1071,7 @@ var require_Collection = __commonJS((exports) => {
1071
1071
  sourceObjects: new Map
1072
1072
  });
1073
1073
  }
1074
- var isEmptyPath = (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;
1075
1075
 
1076
1076
  class Collection extends Node.NodeBase {
1077
1077
  constructor(type, schema) {
@@ -1092,11 +1092,11 @@ var require_Collection = __commonJS((exports) => {
1092
1092
  copy.range = this.range.slice();
1093
1093
  return copy;
1094
1094
  }
1095
- addIn(path20, value) {
1096
- if (isEmptyPath(path20))
1095
+ addIn(path21, value) {
1096
+ if (isEmptyPath(path21))
1097
1097
  this.add(value);
1098
1098
  else {
1099
- const [key, ...rest] = path20;
1099
+ const [key, ...rest] = path21;
1100
1100
  const node = this.get(key, true);
1101
1101
  if (identity.isCollection(node))
1102
1102
  node.addIn(rest, value);
@@ -1106,8 +1106,8 @@ var require_Collection = __commonJS((exports) => {
1106
1106
  throw new Error(`Expected YAML collection at ${key}. Remaining path: ${rest}`);
1107
1107
  }
1108
1108
  }
1109
- deleteIn(path20) {
1110
- const [key, ...rest] = path20;
1109
+ deleteIn(path21) {
1110
+ const [key, ...rest] = path21;
1111
1111
  if (rest.length === 0)
1112
1112
  return this.delete(key);
1113
1113
  const node = this.get(key, true);
@@ -1116,8 +1116,8 @@ var require_Collection = __commonJS((exports) => {
1116
1116
  else
1117
1117
  throw new Error(`Expected YAML collection at ${key}. Remaining path: ${rest}`);
1118
1118
  }
1119
- getIn(path20, keepScalar) {
1120
- const [key, ...rest] = path20;
1119
+ getIn(path21, keepScalar) {
1120
+ const [key, ...rest] = path21;
1121
1121
  const node = this.get(key, true);
1122
1122
  if (rest.length === 0)
1123
1123
  return !keepScalar && identity.isScalar(node) ? node.value : node;
@@ -1132,15 +1132,15 @@ var require_Collection = __commonJS((exports) => {
1132
1132
  return n == null || allowScalar && identity.isScalar(n) && n.value == null && !n.commentBefore && !n.comment && !n.tag;
1133
1133
  });
1134
1134
  }
1135
- hasIn(path20) {
1136
- const [key, ...rest] = path20;
1135
+ hasIn(path21) {
1136
+ const [key, ...rest] = path21;
1137
1137
  if (rest.length === 0)
1138
1138
  return this.has(key);
1139
1139
  const node = this.get(key, true);
1140
1140
  return identity.isCollection(node) ? node.hasIn(rest) : false;
1141
1141
  }
1142
- setIn(path20, value) {
1143
- const [key, ...rest] = path20;
1142
+ setIn(path21, value) {
1143
+ const [key, ...rest] = path21;
1144
1144
  if (rest.length === 0) {
1145
1145
  this.set(key, value);
1146
1146
  } else {
@@ -3533,9 +3533,9 @@ var require_Document = __commonJS((exports) => {
3533
3533
  if (assertCollection(this.contents))
3534
3534
  this.contents.add(value);
3535
3535
  }
3536
- addIn(path20, value) {
3536
+ addIn(path21, value) {
3537
3537
  if (assertCollection(this.contents))
3538
- this.contents.addIn(path20, value);
3538
+ this.contents.addIn(path21, value);
3539
3539
  }
3540
3540
  createAlias(node, name) {
3541
3541
  if (!node.anchor) {
@@ -3584,30 +3584,30 @@ var require_Document = __commonJS((exports) => {
3584
3584
  delete(key) {
3585
3585
  return assertCollection(this.contents) ? this.contents.delete(key) : false;
3586
3586
  }
3587
- deleteIn(path20) {
3588
- if (Collection.isEmptyPath(path20)) {
3587
+ deleteIn(path21) {
3588
+ if (Collection.isEmptyPath(path21)) {
3589
3589
  if (this.contents == null)
3590
3590
  return false;
3591
3591
  this.contents = null;
3592
3592
  return true;
3593
3593
  }
3594
- return assertCollection(this.contents) ? this.contents.deleteIn(path20) : false;
3594
+ return assertCollection(this.contents) ? this.contents.deleteIn(path21) : false;
3595
3595
  }
3596
3596
  get(key, keepScalar) {
3597
3597
  return identity.isCollection(this.contents) ? this.contents.get(key, keepScalar) : undefined;
3598
3598
  }
3599
- getIn(path20, keepScalar) {
3600
- if (Collection.isEmptyPath(path20))
3599
+ getIn(path21, keepScalar) {
3600
+ if (Collection.isEmptyPath(path21))
3601
3601
  return !keepScalar && identity.isScalar(this.contents) ? this.contents.value : this.contents;
3602
- return identity.isCollection(this.contents) ? this.contents.getIn(path20, keepScalar) : undefined;
3602
+ return identity.isCollection(this.contents) ? this.contents.getIn(path21, keepScalar) : undefined;
3603
3603
  }
3604
3604
  has(key) {
3605
3605
  return identity.isCollection(this.contents) ? this.contents.has(key) : false;
3606
3606
  }
3607
- hasIn(path20) {
3608
- if (Collection.isEmptyPath(path20))
3607
+ hasIn(path21) {
3608
+ if (Collection.isEmptyPath(path21))
3609
3609
  return this.contents !== undefined;
3610
- return identity.isCollection(this.contents) ? this.contents.hasIn(path20) : false;
3610
+ return identity.isCollection(this.contents) ? this.contents.hasIn(path21) : false;
3611
3611
  }
3612
3612
  set(key, value) {
3613
3613
  if (this.contents == null) {
@@ -3616,13 +3616,13 @@ var require_Document = __commonJS((exports) => {
3616
3616
  this.contents.set(key, value);
3617
3617
  }
3618
3618
  }
3619
- setIn(path20, value) {
3620
- if (Collection.isEmptyPath(path20)) {
3619
+ setIn(path21, value) {
3620
+ if (Collection.isEmptyPath(path21)) {
3621
3621
  this.contents = value;
3622
3622
  } else if (this.contents == null) {
3623
- this.contents = Collection.collectionFromPath(this.schema, Array.from(path20), value);
3623
+ this.contents = Collection.collectionFromPath(this.schema, Array.from(path21), value);
3624
3624
  } else if (assertCollection(this.contents)) {
3625
- this.contents.setIn(path20, value);
3625
+ this.contents.setIn(path21, value);
3626
3626
  }
3627
3627
  }
3628
3628
  setSchema(version, options = {}) {
@@ -5517,9 +5517,9 @@ var require_cst_visit = __commonJS((exports) => {
5517
5517
  visit.BREAK = BREAK;
5518
5518
  visit.SKIP = SKIP;
5519
5519
  visit.REMOVE = REMOVE;
5520
- visit.itemAtPath = (cst, path20) => {
5520
+ visit.itemAtPath = (cst, path21) => {
5521
5521
  let item = cst;
5522
- for (const [field, index] of path20) {
5522
+ for (const [field, index] of path21) {
5523
5523
  const tok = item?.[field];
5524
5524
  if (tok && "items" in tok) {
5525
5525
  item = tok.items[index];
@@ -5528,23 +5528,23 @@ var require_cst_visit = __commonJS((exports) => {
5528
5528
  }
5529
5529
  return item;
5530
5530
  };
5531
- visit.parentCollection = (cst, path20) => {
5532
- const parent = visit.itemAtPath(cst, path20.slice(0, -1));
5533
- 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];
5534
5534
  const coll = parent?.[field];
5535
5535
  if (coll && "items" in coll)
5536
5536
  return coll;
5537
5537
  throw new Error("Parent collection not found");
5538
5538
  };
5539
- function _visit(path20, item, visitor) {
5540
- let ctrl = visitor(item, path20);
5539
+ function _visit(path21, item, visitor) {
5540
+ let ctrl = visitor(item, path21);
5541
5541
  if (typeof ctrl === "symbol")
5542
5542
  return ctrl;
5543
5543
  for (const field of ["key", "value"]) {
5544
5544
  const token = item[field];
5545
5545
  if (token && "items" in token) {
5546
5546
  for (let i = 0;i < token.items.length; ++i) {
5547
- const ci = _visit(Object.freeze(path20.concat([[field, i]])), token.items[i], visitor);
5547
+ const ci = _visit(Object.freeze(path21.concat([[field, i]])), token.items[i], visitor);
5548
5548
  if (typeof ci === "number")
5549
5549
  i = ci - 1;
5550
5550
  else if (ci === BREAK)
@@ -5555,10 +5555,10 @@ var require_cst_visit = __commonJS((exports) => {
5555
5555
  }
5556
5556
  }
5557
5557
  if (typeof ctrl === "function" && field === "key")
5558
- ctrl = ctrl(item, path20);
5558
+ ctrl = ctrl(item, path21);
5559
5559
  }
5560
5560
  }
5561
- return typeof ctrl === "function" ? ctrl(item, path20) : ctrl;
5561
+ return typeof ctrl === "function" ? ctrl(item, path21) : ctrl;
5562
5562
  }
5563
5563
  exports.visit = visit;
5564
5564
  });
@@ -6827,14 +6827,14 @@ var require_parser = __commonJS((exports) => {
6827
6827
  case "scalar":
6828
6828
  case "single-quoted-scalar":
6829
6829
  case "double-quoted-scalar": {
6830
- const fs15 = this.flowScalar(this.type);
6830
+ const fs16 = this.flowScalar(this.type);
6831
6831
  if (atNextItem || it.value) {
6832
- map.items.push({ start, key: fs15, sep: [] });
6832
+ map.items.push({ start, key: fs16, sep: [] });
6833
6833
  this.onKeyLine = true;
6834
6834
  } else if (it.sep) {
6835
- this.stack.push(fs15);
6835
+ this.stack.push(fs16);
6836
6836
  } else {
6837
- Object.assign(it, { key: fs15, sep: [] });
6837
+ Object.assign(it, { key: fs16, sep: [] });
6838
6838
  this.onKeyLine = true;
6839
6839
  }
6840
6840
  return;
@@ -6962,13 +6962,13 @@ var require_parser = __commonJS((exports) => {
6962
6962
  case "scalar":
6963
6963
  case "single-quoted-scalar":
6964
6964
  case "double-quoted-scalar": {
6965
- const fs15 = this.flowScalar(this.type);
6965
+ const fs16 = this.flowScalar(this.type);
6966
6966
  if (!it || it.value)
6967
- fc.items.push({ start: [], key: fs15, sep: [] });
6967
+ fc.items.push({ start: [], key: fs16, sep: [] });
6968
6968
  else if (it.sep)
6969
- this.stack.push(fs15);
6969
+ this.stack.push(fs16);
6970
6970
  else
6971
- Object.assign(it, { key: fs15, sep: [] });
6971
+ Object.assign(it, { key: fs16, sep: [] });
6972
6972
  return;
6973
6973
  }
6974
6974
  case "flow-map-end":
@@ -7314,11 +7314,11 @@ function shouldStopByStuck(history, cfg) {
7314
7314
  async function withTimeout3(p, timeoutMs) {
7315
7315
  if (timeoutMs <= 0)
7316
7316
  return await p;
7317
- return await new Promise((resolve16, reject) => {
7317
+ return await new Promise((resolve17, reject) => {
7318
7318
  const timer = setTimeout(() => reject(new Error(`timeout after ${timeoutMs}ms`)), timeoutMs);
7319
7319
  Promise.resolve(p).then((v) => {
7320
7320
  clearTimeout(timer);
7321
- resolve16(v);
7321
+ resolve17(v);
7322
7322
  }, (err) => {
7323
7323
  clearTimeout(timer);
7324
7324
  reject(err);
@@ -10679,8 +10679,8 @@ class ApprovalStore {
10679
10679
  // lib/session-worktree.ts
10680
10680
  init_worktree_ops();
10681
10681
  import { execFile as execFile3 } from "node:child_process";
10682
- import { promises as fs10 } from "node:fs";
10683
- import * as path12 from "node:path";
10682
+ import { promises as fs11 } from "node:fs";
10683
+ import * as path13 from "node:path";
10684
10684
 
10685
10685
  // lib/file-lock.ts
10686
10686
  import { promises as fs8 } from "node:fs";
@@ -10796,25 +10796,120 @@ function sleep(ms) {
10796
10796
  return new Promise((resolve9) => setTimeout(resolve9, ms));
10797
10797
  }
10798
10798
 
10799
- // lib/parent-map-store.ts
10799
+ // lib/merge-journal.ts
10800
10800
  import { promises as fs9 } from "node:fs";
10801
10801
  import * as path11 from "node:path";
10802
+ var MERGE_JOURNAL_VERSION = 1;
10803
+ var MERGE_PHASE_RANK = {
10804
+ started: 1,
10805
+ committed: 2,
10806
+ cleaned: 3,
10807
+ done: 4,
10808
+ aborted: 0
10809
+ };
10810
+ function isCommittedOrLater(phase) {
10811
+ return MERGE_PHASE_RANK[phase] >= MERGE_PHASE_RANK.committed;
10812
+ }
10813
+ function journalDir(mainRoot) {
10814
+ return path11.join(runtimeDir(path11.resolve(mainRoot), { ensure: false }), "session-worktrees");
10815
+ }
10816
+ function mergeJournalPath(mainRoot) {
10817
+ return path11.join(journalDir(mainRoot), "merge-journal.json");
10818
+ }
10819
+ function mergeJournalLockPath(mainRoot) {
10820
+ return path11.join(journalDir(mainRoot), "merge-journal.lock");
10821
+ }
10822
+ async function readJournalFile(mainRoot) {
10823
+ const file = mergeJournalPath(mainRoot);
10824
+ try {
10825
+ const raw = await fs9.readFile(file, "utf8");
10826
+ const parsed = JSON.parse(raw);
10827
+ if (parsed.version !== MERGE_JOURNAL_VERSION || !Array.isArray(parsed.entries)) {
10828
+ return { version: MERGE_JOURNAL_VERSION, entries: [] };
10829
+ }
10830
+ return parsed;
10831
+ } catch (err) {
10832
+ const e = err;
10833
+ if (e.code === "ENOENT")
10834
+ return { version: MERGE_JOURNAL_VERSION, entries: [] };
10835
+ return { version: MERGE_JOURNAL_VERSION, entries: [] };
10836
+ }
10837
+ }
10838
+ async function writeJournalFile(mainRoot, payload) {
10839
+ const file = mergeJournalPath(mainRoot);
10840
+ await fs9.mkdir(path11.dirname(file), { recursive: true });
10841
+ const tmp = `${file}.tmp-${process.pid}-${Date.now()}`;
10842
+ await fs9.writeFile(tmp, JSON.stringify(payload, null, 2), "utf8");
10843
+ await fs9.rename(tmp, file);
10844
+ }
10845
+ async function listMergeJournalEntries(mainRoot) {
10846
+ const f = await readJournalFile(mainRoot);
10847
+ return f.entries;
10848
+ }
10849
+ async function getMergeJournalEntry(mainRoot, sessionId) {
10850
+ const f = await readJournalFile(mainRoot);
10851
+ return f.entries.find((e) => e.sessionId === sessionId) ?? null;
10852
+ }
10853
+ async function recordMergeJournalPhase(mainRoot, input) {
10854
+ const lockPath = mergeJournalLockPath(mainRoot);
10855
+ await fs9.mkdir(path11.dirname(lockPath), { recursive: true });
10856
+ await withFileLock(lockPath, async () => {
10857
+ const f = await readJournalFile(mainRoot);
10858
+ const now = new Date().toISOString();
10859
+ const idx = f.entries.findIndex((e) => e.sessionId === input.sessionId);
10860
+ if (idx < 0) {
10861
+ const entry = {
10862
+ sessionId: input.sessionId,
10863
+ approvalSha: input.approvalSha,
10864
+ phase: input.phase,
10865
+ updatedAt: now,
10866
+ ...input.mergedCommitSha ? { mergedCommitSha: input.mergedCommitSha } : {}
10867
+ };
10868
+ f.entries.push(entry);
10869
+ } else {
10870
+ const prev = f.entries[idx];
10871
+ if (prev.approvalSha !== input.approvalSha) {
10872
+ f.entries[idx] = {
10873
+ sessionId: input.sessionId,
10874
+ approvalSha: input.approvalSha,
10875
+ phase: input.phase,
10876
+ updatedAt: now,
10877
+ ...input.mergedCommitSha ? { mergedCommitSha: input.mergedCommitSha } : {}
10878
+ };
10879
+ } else {
10880
+ const mergedCommitSha = input.mergedCommitSha ?? prev.mergedCommitSha;
10881
+ f.entries[idx] = {
10882
+ sessionId: input.sessionId,
10883
+ approvalSha: input.approvalSha,
10884
+ phase: input.phase,
10885
+ updatedAt: now,
10886
+ ...mergedCommitSha ? { mergedCommitSha } : {}
10887
+ };
10888
+ }
10889
+ }
10890
+ await writeJournalFile(mainRoot, f);
10891
+ });
10892
+ }
10893
+
10894
+ // lib/parent-map-store.ts
10895
+ import { promises as fs10 } from "node:fs";
10896
+ import * as path12 from "node:path";
10802
10897
  var PARENT_MAP_VERSION = 1;
10803
10898
  var PARENT_MAP_LOCK_TIMEOUT_MS = 2000;
10804
10899
  var PARENT_MAP_MAX_ENTRIES = 256;
10805
10900
  function parentMapDir(mainRoot) {
10806
- return path11.join(runtimeDir(path11.resolve(mainRoot), { ensure: false }), "session-worktrees");
10901
+ return path12.join(runtimeDir(path12.resolve(mainRoot), { ensure: false }), "session-worktrees");
10807
10902
  }
10808
10903
  function parentMapPath(mainRoot) {
10809
- return path11.join(parentMapDir(mainRoot), "parent-map.json");
10904
+ return path12.join(parentMapDir(mainRoot), "parent-map.json");
10810
10905
  }
10811
10906
  function parentMapLockPath(mainRoot) {
10812
- return path11.join(parentMapDir(mainRoot), "parent-map.lock");
10907
+ return path12.join(parentMapDir(mainRoot), "parent-map.lock");
10813
10908
  }
10814
10909
  async function readParentMapFile(mainRoot) {
10815
10910
  const file = parentMapPath(mainRoot);
10816
10911
  try {
10817
- const raw = await fs9.readFile(file, "utf8");
10912
+ const raw = await fs10.readFile(file, "utf8");
10818
10913
  const parsed = JSON.parse(raw);
10819
10914
  if (parsed.version !== PARENT_MAP_VERSION || !Array.isArray(parsed.entries)) {
10820
10915
  return { version: PARENT_MAP_VERSION, entries: [] };
@@ -10829,10 +10924,10 @@ async function readParentMapFile(mainRoot) {
10829
10924
  }
10830
10925
  async function writeParentMapFile(mainRoot, payload) {
10831
10926
  const file = parentMapPath(mainRoot);
10832
- await fs9.mkdir(path11.dirname(file), { recursive: true });
10927
+ await fs10.mkdir(path12.dirname(file), { recursive: true });
10833
10928
  const tmp = `${file}.tmp-${process.pid}-${Date.now()}`;
10834
- await fs9.writeFile(tmp, JSON.stringify(payload, null, 2), "utf8");
10835
- await fs9.rename(tmp, file);
10929
+ await fs10.writeFile(tmp, JSON.stringify(payload, null, 2), "utf8");
10930
+ await fs10.rename(tmp, file);
10836
10931
  }
10837
10932
  function capEntriesByTsDesc(entries) {
10838
10933
  if (entries.length <= PARENT_MAP_MAX_ENTRIES)
@@ -10864,7 +10959,7 @@ async function loadParentMap(mainRoot) {
10864
10959
  }
10865
10960
  async function mutateParentMap(mainRoot, mutator, opts = {}) {
10866
10961
  const lockPath = parentMapLockPath(mainRoot);
10867
- await fs9.mkdir(path11.dirname(lockPath), { recursive: true });
10962
+ await fs10.mkdir(path12.dirname(lockPath), { recursive: true });
10868
10963
  const lockOpts = {
10869
10964
  timeoutMs: opts.timeoutMs ?? PARENT_MAP_LOCK_TIMEOUT_MS,
10870
10965
  ...opts
@@ -10883,7 +10978,7 @@ async function appendParentEntry(mainRoot, childID, parentID, ts, opts = {}) {
10883
10978
  if (!childID || !parentID)
10884
10979
  return;
10885
10980
  const lockPath = parentMapLockPath(mainRoot);
10886
- await fs9.mkdir(path11.dirname(lockPath), { recursive: true });
10981
+ await fs10.mkdir(path12.dirname(lockPath), { recursive: true });
10887
10982
  const lockOpts = {
10888
10983
  timeoutMs: opts.timeoutMs ?? PARENT_MAP_LOCK_TIMEOUT_MS,
10889
10984
  ...opts
@@ -11002,20 +11097,20 @@ function sweepExpiredSessionParents(now = Date.now()) {
11002
11097
 
11003
11098
  // lib/session-worktree.ts
11004
11099
  var REGISTRY_VERSION = 1;
11005
- var DEFAULT_WORKTREE_SUBDIR = path12.join(".git", "codeforge-worktrees");
11100
+ var DEFAULT_WORKTREE_SUBDIR = path13.join(".git", "codeforge-worktrees");
11006
11101
  function debugLog(msg) {
11007
11102
  if (process.env["CODEFORGE_DEBUG"]) {
11008
11103
  console.debug(`[session-worktree] ${msg}`);
11009
11104
  }
11010
11105
  }
11011
11106
  function registryDir(mainRoot) {
11012
- return path12.join(runtimeDir(path12.resolve(mainRoot), { ensure: false }), "session-worktrees");
11107
+ return path13.join(runtimeDir(path13.resolve(mainRoot), { ensure: false }), "session-worktrees");
11013
11108
  }
11014
11109
  function registryPath(mainRoot) {
11015
- return path12.join(registryDir(mainRoot), "registry.json");
11110
+ return path13.join(registryDir(mainRoot), "registry.json");
11016
11111
  }
11017
11112
  function registryLockPath(mainRoot) {
11018
- return path12.join(registryDir(mainRoot), "registry.lock");
11113
+ return path13.join(registryDir(mainRoot), "registry.lock");
11019
11114
  }
11020
11115
 
11021
11116
  class RegistryCorruptError extends Error {
@@ -11046,7 +11141,7 @@ async function readRegistryResult(mainRoot) {
11046
11141
  const file = registryPath(mainRoot);
11047
11142
  let raw;
11048
11143
  try {
11049
- raw = await fs10.readFile(file, "utf8");
11144
+ raw = await fs11.readFile(file, "utf8");
11050
11145
  } catch (err) {
11051
11146
  const e = err;
11052
11147
  if (e.code === "ENOENT")
@@ -11094,24 +11189,24 @@ async function readRegistry(mainRoot) {
11094
11189
  }
11095
11190
  async function writeRegistry(mainRoot, reg) {
11096
11191
  const file = registryPath(mainRoot);
11097
- await fs10.mkdir(path12.dirname(file), { recursive: true });
11192
+ await fs11.mkdir(path13.dirname(file), { recursive: true });
11098
11193
  const tmp = `${file}.tmp-${process.pid}-${Date.now()}`;
11099
- await fs10.writeFile(tmp, JSON.stringify(reg, null, 2), "utf8");
11100
- await fs10.rename(tmp, file);
11194
+ await fs11.writeFile(tmp, JSON.stringify(reg, null, 2), "utf8");
11195
+ await fs11.rename(tmp, file);
11101
11196
  }
11102
11197
  async function backupCorruptRegistry(mainRoot) {
11103
11198
  const file = registryPath(mainRoot);
11104
- const dir = path12.dirname(file);
11105
- const base = path12.basename(file);
11199
+ const dir = path13.dirname(file);
11200
+ const base = path13.basename(file);
11106
11201
  try {
11107
- const names = await fs10.readdir(dir);
11202
+ const names = await fs11.readdir(dir);
11108
11203
  const existing = names.find((n) => n.startsWith(`${base}.corrupt.`));
11109
11204
  if (existing)
11110
- return path12.join(dir, existing);
11205
+ return path13.join(dir, existing);
11111
11206
  } catch {}
11112
11207
  const backup = `${file}.corrupt.${Date.now()}`;
11113
11208
  try {
11114
- await fs10.copyFile(file, backup);
11209
+ await fs11.copyFile(file, backup);
11115
11210
  return backup;
11116
11211
  } catch {
11117
11212
  return;
@@ -11131,7 +11226,7 @@ async function loadRegistryForMutation(mainRoot) {
11131
11226
  }
11132
11227
  async function mutateRegistry(mainRoot, fn) {
11133
11228
  const lockPath = registryLockPath(mainRoot);
11134
- await fs10.mkdir(path12.dirname(lockPath), { recursive: true });
11229
+ await fs11.mkdir(path13.dirname(lockPath), { recursive: true });
11135
11230
  return await withFileLock(lockPath, async () => {
11136
11231
  const reg = await loadRegistryForMutation(mainRoot);
11137
11232
  const result = await fn(reg);
@@ -11143,11 +11238,11 @@ async function bindSessionWorktree(opts) {
11143
11238
  if (!opts.sessionId || opts.sessionId.trim() === "") {
11144
11239
  throw new Error("bindSessionWorktree: sessionId 不能为空");
11145
11240
  }
11146
- const mainRoot = path12.resolve(opts.mainRoot);
11241
+ const mainRoot = path13.resolve(opts.mainRoot);
11147
11242
  const branch = opts.branchName ?? `codeforge/session-${opts.sessionId}`;
11148
- const worktreesDir = opts.worktrees_dir ?? path12.join(mainRoot, DEFAULT_WORKTREE_SUBDIR);
11243
+ const worktreesDir = opts.worktrees_dir ?? path13.join(mainRoot, DEFAULT_WORKTREE_SUBDIR);
11149
11244
  const lockPath = registryLockPath(mainRoot);
11150
- await fs10.mkdir(path12.dirname(lockPath), { recursive: true });
11245
+ await fs11.mkdir(path13.dirname(lockPath), { recursive: true });
11151
11246
  return await withFileLock(lockPath, async () => {
11152
11247
  const reg = await loadRegistryForMutation(mainRoot);
11153
11248
  const existing = reg.entries.find((e) => e.sessionId === opts.sessionId);
@@ -11198,7 +11293,7 @@ async function registryHasEntries(mainRoot) {
11198
11293
  var OWNER_RESOLVE_MAX_DEPTH = 16;
11199
11294
  async function resolveWorktreeOwner(opts) {
11200
11295
  const sid = opts.sessionId;
11201
- const mainRoot = path12.resolve(opts.mainRoot);
11296
+ const mainRoot = path13.resolve(opts.mainRoot);
11202
11297
  const snap = await readRegistryResult(mainRoot);
11203
11298
  if (snap.kind === "corrupt" || snap.kind === "version_too_new") {
11204
11299
  return {
@@ -11212,8 +11307,8 @@ async function resolveWorktreeOwner(opts) {
11212
11307
  const entries = snap.kind === "ok" ? snap.registry.entries : [];
11213
11308
  const activeById = (id) => entries.find((e) => e.sessionId === id && e.status === "active") ?? null;
11214
11309
  if (opts.worktreePath) {
11215
- const target = path12.resolve(opts.worktreePath);
11216
- const byPath = entries.find((e) => e.status === "active" && path12.resolve(e.worktreePath) === target);
11310
+ const target = path13.resolve(opts.worktreePath);
11311
+ const byPath = entries.find((e) => e.status === "active" && path13.resolve(e.worktreePath) === target);
11217
11312
  if (byPath) {
11218
11313
  return { ok: true, ownerSessionId: byPath.sessionId, entry: byPath, via: "worktree-path" };
11219
11314
  }
@@ -11257,7 +11352,7 @@ async function touchEntryUpdatedAt(opts) {
11257
11352
  });
11258
11353
  }
11259
11354
  async function mergeSessionBack(opts) {
11260
- const mainRoot = path12.resolve(opts.mainRoot);
11355
+ const mainRoot = path13.resolve(opts.mainRoot);
11261
11356
  const entry = await getSessionWorktree(opts.sessionId, mainRoot);
11262
11357
  if (!entry) {
11263
11358
  throw new Error(`mergeSessionBack: session ${opts.sessionId} 没有绑定 worktree`);
@@ -11271,6 +11366,38 @@ async function mergeSessionBack(opts) {
11271
11366
  const wt = entry.worktreePath;
11272
11367
  const branch = entry.branch;
11273
11368
  const baseSha = entry.baseSha;
11369
+ const approvalSha = (await runGit2(wt, ["rev-parse", "HEAD"])).trim();
11370
+ {
11371
+ const existing = await getMergeJournalEntry(mainRoot, opts.sessionId).catch(() => null);
11372
+ let recoveredSha;
11373
+ if (existing && existing.approvalSha === approvalSha && isCommittedOrLater(existing.phase) && existing.mergedCommitSha) {
11374
+ recoveredSha = existing.mergedCommitSha;
11375
+ } else {
11376
+ const footerSha = await findMergeCommitBySession(mainRoot, opts.sessionId);
11377
+ if (footerSha)
11378
+ recoveredSha = footerSha;
11379
+ }
11380
+ if (recoveredSha) {
11381
+ await finalizeMergeIdempotent({
11382
+ mainRoot,
11383
+ sessionId: opts.sessionId,
11384
+ worktreePath: wt,
11385
+ branch,
11386
+ approvalSha,
11387
+ mergedCommitSha: recoveredSha,
11388
+ ...opts.planStore ? { planStore: opts.planStore } : {},
11389
+ ...entry.requiredPlanId ? { requiredPlanId: entry.requiredPlanId } : {}
11390
+ });
11391
+ return { sha: recoveredSha, squashedCommits: [] };
11392
+ }
11393
+ }
11394
+ await recordMergeJournalPhase(mainRoot, {
11395
+ sessionId: opts.sessionId,
11396
+ approvalSha,
11397
+ phase: "started"
11398
+ }).catch((err) => {
11399
+ debugLog(`recordMergeJournalPhase(started) 失败 (${opts.sessionId}): ${err.message}`);
11400
+ });
11274
11401
  const wtStatus = (await runGit2(wt, ["status", "--porcelain"])).trim();
11275
11402
  if (wtStatus.length > 0) {
11276
11403
  await runGit2(wt, ["add", "-A"]);
@@ -11310,11 +11437,27 @@ async function mergeSessionBack(opts) {
11310
11437
  } else {}
11311
11438
  const squashedRaw = await runGit2(wt, ["log", "--format=%s", `${baseSha}..HEAD`]);
11312
11439
  const squashedCommits = squashedRaw.split(/\r?\n/).map((s) => s.trim()).filter(Boolean);
11313
- const message = opts.commitMessage ?? buildMergeMessage(opts.sessionId, branch, baseSha, squashedCommits);
11440
+ const message = opts.commitMessage ?? await buildMergeMessage({
11441
+ sessionId: opts.sessionId,
11442
+ branch,
11443
+ baseSha,
11444
+ squashed: squashedCommits,
11445
+ ...opts.planStore ? { planStore: opts.planStore } : {},
11446
+ ...entry.requiredPlanId ? { requiredPlanId: entry.requiredPlanId } : {},
11447
+ ...opts.summary ? { summary: opts.summary } : {}
11448
+ });
11314
11449
  await runGitWithEnv(mainRoot, ["commit", "-m", message], {
11315
11450
  SKIP_DEV_SYNC_CHECK: "1"
11316
11451
  });
11317
11452
  const newSha = (await runGit2(mainRoot, ["rev-parse", "HEAD"])).trim();
11453
+ await recordMergeJournalPhase(mainRoot, {
11454
+ sessionId: opts.sessionId,
11455
+ approvalSha,
11456
+ phase: "committed",
11457
+ mergedCommitSha: newSha
11458
+ }).catch((err) => {
11459
+ debugLog(`recordMergeJournalPhase(committed) 失败 (${opts.sessionId}): ${err.message}`);
11460
+ });
11318
11461
  try {
11319
11462
  await removeWorktree({ root: mainRoot, worktree_path: wt, force: true });
11320
11463
  } catch (err) {
@@ -11324,6 +11467,14 @@ async function mergeSessionBack(opts) {
11324
11467
  debugLog(`deleteBranchIfExists (merge) 非预期失败: ${err.message}`);
11325
11468
  return { deleted: false };
11326
11469
  });
11470
+ await recordMergeJournalPhase(mainRoot, {
11471
+ sessionId: opts.sessionId,
11472
+ approvalSha,
11473
+ phase: "cleaned",
11474
+ mergedCommitSha: newSha
11475
+ }).catch((err) => {
11476
+ debugLog(`recordMergeJournalPhase(cleaned) 失败 (${opts.sessionId}): ${err.message}`);
11477
+ });
11327
11478
  await mutateRegistry(mainRoot, (reg) => {
11328
11479
  const e = reg.entries.find((x) => x.sessionId === opts.sessionId);
11329
11480
  if (e) {
@@ -11331,6 +11482,14 @@ async function mergeSessionBack(opts) {
11331
11482
  e.updatedAt = new Date().toISOString();
11332
11483
  }
11333
11484
  });
11485
+ await recordMergeJournalPhase(mainRoot, {
11486
+ sessionId: opts.sessionId,
11487
+ approvalSha,
11488
+ phase: "done",
11489
+ mergedCommitSha: newSha
11490
+ }).catch((err) => {
11491
+ debugLog(`recordMergeJournalPhase(done) 失败 (${opts.sessionId}): ${err.message}`);
11492
+ });
11334
11493
  if (opts.planStore && entry.requiredPlanId) {
11335
11494
  await opts.planStore.markMerged(entry.requiredPlanId, opts.sessionId).catch((err) => {
11336
11495
  console.warn(`[session-worktree] planStore.markMerged(${entry.requiredPlanId}) 失败: ${err instanceof Error ? err.message : String(err)}`);
@@ -11338,8 +11497,162 @@ async function mergeSessionBack(opts) {
11338
11497
  }
11339
11498
  return { sha: newSha, squashedCommits };
11340
11499
  }
11500
+ async function findMergeCommitBySession(mainRoot, sessionId) {
11501
+ try {
11502
+ const out = await runGit2(path13.resolve(mainRoot), [
11503
+ "log",
11504
+ "-F",
11505
+ `--grep=Codeforge-Session: ${sessionId}`,
11506
+ "--format=%H",
11507
+ "-n",
11508
+ "1"
11509
+ ]);
11510
+ return out.trim().split(/\r?\n/)[0]?.trim() ?? "";
11511
+ } catch {
11512
+ return "";
11513
+ }
11514
+ }
11515
+ async function finalizeMergeIdempotent(opts) {
11516
+ const { mainRoot, sessionId, worktreePath, branch } = opts;
11517
+ try {
11518
+ await removeWorktree({ root: mainRoot, worktree_path: worktreePath, force: true });
11519
+ } catch (err) {
11520
+ debugLog(`finalizeMergeIdempotent removeWorktree 非预期失败 (${sessionId}): ${err.message}`);
11521
+ }
11522
+ await deleteBranchIfExists({ root: mainRoot, branch }).catch(() => ({ deleted: false }));
11523
+ await recordMergeJournalPhase(mainRoot, {
11524
+ sessionId,
11525
+ approvalSha: opts.approvalSha,
11526
+ phase: "cleaned",
11527
+ mergedCommitSha: opts.mergedCommitSha
11528
+ }).catch(() => {});
11529
+ await mutateRegistry(mainRoot, (reg) => {
11530
+ const e = reg.entries.find((x) => x.sessionId === sessionId);
11531
+ if (e && e.status !== "merged") {
11532
+ e.status = "merged";
11533
+ e.updatedAt = new Date().toISOString();
11534
+ }
11535
+ });
11536
+ await recordMergeJournalPhase(mainRoot, {
11537
+ sessionId,
11538
+ approvalSha: opts.approvalSha,
11539
+ phase: "done",
11540
+ mergedCommitSha: opts.mergedCommitSha
11541
+ }).catch(() => {});
11542
+ if (opts.planStore && opts.requiredPlanId) {
11543
+ await opts.planStore.markMerged(opts.requiredPlanId, sessionId).catch((err) => {
11544
+ console.warn(`[session-worktree] finalizeMergeIdempotent planStore.markMerged(${opts.requiredPlanId}) 失败: ${err instanceof Error ? err.message : String(err)}`);
11545
+ });
11546
+ }
11547
+ }
11548
+ async function reconcileInterruptedMerges(mainRoot, opts = {}) {
11549
+ const resolved = path13.resolve(mainRoot);
11550
+ const result = {
11551
+ recoveredMerged: [],
11552
+ rolledBack: [],
11553
+ conflicts: []
11554
+ };
11555
+ const snap = await readRegistryResult(resolved);
11556
+ if (snap.kind === "corrupt") {
11557
+ result.registryUnreadable = { reason: "corrupt", detail: snap.reason, path: snap.path };
11558
+ return result;
11559
+ }
11560
+ if (snap.kind === "version_too_new") {
11561
+ result.registryUnreadable = {
11562
+ reason: "version_too_new",
11563
+ detail: `v${snap.foundVersion} > 支持 v${snap.supportedVersion}`,
11564
+ path: snap.path
11565
+ };
11566
+ return result;
11567
+ }
11568
+ const entries = snap.kind === "ok" ? snap.registry.entries : [];
11569
+ const journal = await listMergeJournalEntries(resolved).catch(() => []);
11570
+ if (journal.length === 0)
11571
+ return result;
11572
+ const hasStaged = opts.hasStagedOverride ?? (async (root) => {
11573
+ try {
11574
+ await runGit2(root, ["diff", "--cached", "--quiet"]);
11575
+ return false;
11576
+ } catch {
11577
+ return true;
11578
+ }
11579
+ });
11580
+ for (const j of journal) {
11581
+ if (j.phase === "done" || j.phase === "aborted")
11582
+ continue;
11583
+ const regEntry = entries.find((e) => e.sessionId === j.sessionId);
11584
+ const footerSha = await findMergeCommitBySession(resolved, j.sessionId);
11585
+ const footerHit = footerSha.length > 0;
11586
+ const journalCommitted = isCommittedOrLater(j.phase);
11587
+ const conflictA = journalCommitted && !footerHit;
11588
+ const conflictB = !journalCommitted && footerHit;
11589
+ const conflictC = journalCommitted && footerHit && !!j.mergedCommitSha && j.mergedCommitSha !== footerSha;
11590
+ if (conflictA || conflictB || conflictC) {
11591
+ result.conflicts.push({
11592
+ sessionId: j.sessionId,
11593
+ state: "D",
11594
+ action: conflictA ? "journal=committed 但主仓无 merge commit(footer 未命中)→ 保守不动,需人工核查" : conflictB ? "journal=started 但主仓已有 merge commit(footer 命中)→ 保守不动,需人工核查" : "journal.mergedCommitSha 与主仓 footer sha 不一致 → 保守不动,需人工核查"
11595
+ });
11596
+ continue;
11597
+ }
11598
+ if (journalCommitted && footerHit) {
11599
+ if (regEntry && regEntry.status === "merged") {
11600
+ await recordMergeJournalPhase(resolved, {
11601
+ sessionId: j.sessionId,
11602
+ approvalSha: j.approvalSha,
11603
+ phase: "done",
11604
+ mergedCommitSha: footerSha
11605
+ }).catch(() => {});
11606
+ continue;
11607
+ }
11608
+ let worktreeExists = false;
11609
+ if (regEntry?.worktreePath) {
11610
+ try {
11611
+ const st = await fs11.stat(regEntry.worktreePath);
11612
+ worktreeExists = st.isDirectory();
11613
+ } catch {
11614
+ worktreeExists = false;
11615
+ }
11616
+ }
11617
+ await finalizeMergeIdempotent({
11618
+ mainRoot: resolved,
11619
+ sessionId: j.sessionId,
11620
+ worktreePath: regEntry?.worktreePath ?? "",
11621
+ branch: regEntry?.branch ?? `codeforge/session-${j.sessionId}`,
11622
+ approvalSha: j.approvalSha,
11623
+ mergedCommitSha: footerSha
11624
+ });
11625
+ result.recoveredMerged.push({
11626
+ sessionId: j.sessionId,
11627
+ state: worktreeExists ? "A" : "B",
11628
+ action: worktreeExists ? "主仓已有 merge commit,registry 未标 merged → 补标 merged + 清 worktree(绝不重 merge)" : "worktree 已删但 registry 未标 merged + journal committed → 仅补标 merged(绝不重 merge)"
11629
+ });
11630
+ continue;
11631
+ }
11632
+ if (!journalCommitted && !footerHit) {
11633
+ const staged = await hasStaged(resolved);
11634
+ if (staged) {
11635
+ await runGit2(resolved, ["reset", "--hard", "HEAD"]).catch((err) => {
11636
+ debugLog(`reconcileInterruptedMerges 态 C reset 失败 (${j.sessionId}): ${err.message}`);
11637
+ });
11638
+ await recordMergeJournalPhase(resolved, {
11639
+ sessionId: j.sessionId,
11640
+ approvalSha: j.approvalSha,
11641
+ phase: "aborted"
11642
+ }).catch(() => {});
11643
+ result.rolledBack.push({
11644
+ sessionId: j.sessionId,
11645
+ state: "C",
11646
+ action: "崩在 squash commit 之前,主仓有 staged → git reset --hard HEAD 还原(改动仍在 worktree 可重跑)"
11647
+ });
11648
+ }
11649
+ continue;
11650
+ }
11651
+ }
11652
+ return result;
11653
+ }
11341
11654
  async function discardSession(opts) {
11342
- const mainRoot = path12.resolve(opts.mainRoot);
11655
+ const mainRoot = path13.resolve(opts.mainRoot);
11343
11656
  const entry = await getSessionWorktree(opts.sessionId, mainRoot);
11344
11657
  if (!entry) {
11345
11658
  return;
@@ -11390,8 +11703,34 @@ async function markInterruptedDirty(opts) {
11390
11703
  });
11391
11704
  }
11392
11705
  var SALVAGE_BRANCH_PREFIX = "codeforge/salvage/";
11706
+ var TRANSITIONAL_STALE_MS = 72 * 60 * 60000;
11707
+ async function markAbandoned(opts) {
11708
+ return await mutateRegistry(opts.mainRoot, (reg) => {
11709
+ const e = reg.entries.find((x) => x.sessionId === opts.sessionId);
11710
+ if (!e)
11711
+ return false;
11712
+ if (e.status === "merged" || e.status === "discarded" || e.status === "abandoned") {
11713
+ return false;
11714
+ }
11715
+ e.status = "abandoned";
11716
+ e.updatedAt = new Date().toISOString();
11717
+ return true;
11718
+ });
11719
+ }
11720
+ async function createSalvageBranch(opts) {
11721
+ const mainRoot = path13.resolve(opts.mainRoot);
11722
+ const ts = new Date().toISOString().replace(/[-:.TZ]/g, "").slice(0, 14);
11723
+ const branch = `${SALVAGE_BRANCH_PREFIX}${opts.sessionId}-${ts}`;
11724
+ try {
11725
+ await runGit2(mainRoot, ["branch", branch, opts.fromRef]);
11726
+ return branch;
11727
+ } catch (err) {
11728
+ debugLog(`createSalvageBranch 失败 (session=${opts.sessionId}): ${err.message}`);
11729
+ return null;
11730
+ }
11731
+ }
11393
11732
  async function listSalvageBranches(mainRoot) {
11394
- const resolved = path12.resolve(mainRoot);
11733
+ const resolved = path13.resolve(mainRoot);
11395
11734
  try {
11396
11735
  const out = await runGit2(resolved, [
11397
11736
  "for-each-ref",
@@ -11409,12 +11748,17 @@ async function listSalvageBranches(mainRoot) {
11409
11748
  return [];
11410
11749
  }
11411
11750
  }
11412
- async function reconcileTransitionalEntries(mainRoot) {
11413
- const resolved = path12.resolve(mainRoot);
11751
+ async function reconcileTransitionalEntries(mainRoot, opts = {}) {
11752
+ const resolved = path13.resolve(mainRoot);
11753
+ const staleMs = opts.transitionalStaleMs ?? TRANSITIONAL_STALE_MS;
11754
+ const now = opts.now ?? Date.now();
11414
11755
  const result = {
11415
11756
  cleanedCreating: [],
11416
11757
  finishedRemoving: [],
11417
- keptConservative: []
11758
+ keptConservative: [],
11759
+ staleSalvaged: [],
11760
+ staleCleaned: [],
11761
+ staleKept: []
11418
11762
  };
11419
11763
  const snap = await readRegistryResult(resolved);
11420
11764
  if (snap.kind === "corrupt") {
@@ -11433,17 +11777,40 @@ async function reconcileTransitionalEntries(mainRoot) {
11433
11777
  const hasTransitional = entries.some((e) => e.status === "creating" || e.status === "removing");
11434
11778
  if (!hasTransitional)
11435
11779
  return result;
11780
+ const staleSnapshots = [];
11436
11781
  await mutateRegistry(resolved, async (reg) => {
11437
11782
  for (const entry of reg.entries) {
11438
11783
  if (entry.status !== "creating" && entry.status !== "removing")
11439
11784
  continue;
11440
11785
  let dirExists = true;
11441
11786
  try {
11442
- const st = await fs10.stat(entry.worktreePath);
11787
+ const st = await fs11.stat(entry.worktreePath);
11443
11788
  dirExists = st.isDirectory();
11444
11789
  } catch {
11445
11790
  dirExists = false;
11446
11791
  }
11792
+ const parsedAt = Date.parse(entry.updatedAt);
11793
+ const isStale = Number.isFinite(parsedAt) && now - parsedAt > staleMs;
11794
+ if (isStale && dirExists) {
11795
+ staleSnapshots.push({
11796
+ sessionId: entry.sessionId,
11797
+ branch: entry.branch,
11798
+ worktreePath: entry.worktreePath,
11799
+ baseSha: entry.baseSha,
11800
+ status: entry.status,
11801
+ dirExists
11802
+ });
11803
+ continue;
11804
+ }
11805
+ if (isStale && !dirExists && entry.status === "removing") {
11806
+ await deleteBranchIfExists({ root: resolved, branch: entry.branch }).catch(() => ({
11807
+ deleted: false
11808
+ }));
11809
+ entry.status = "abandoned";
11810
+ entry.updatedAt = new Date().toISOString();
11811
+ result.staleCleaned.push(entry.sessionId);
11812
+ continue;
11813
+ }
11447
11814
  if (entry.status === "creating") {
11448
11815
  if (!dirExists) {
11449
11816
  entry.status = "discarded";
@@ -11482,15 +11849,88 @@ async function reconcileTransitionalEntries(mainRoot) {
11482
11849
  }
11483
11850
  }
11484
11851
  });
11852
+ for (const snap2 of staleSnapshots) {
11853
+ await reconcileOneStaleTransitional(resolved, snap2, result);
11854
+ }
11485
11855
  return result;
11486
11856
  }
11857
+ var STALE_SALVAGE_CHECKPOINT_MSG = "[codeforge-checkpoint] stale-transitional-salvage";
11858
+ async function reconcileOneStaleTransitional(mainRoot, snap, result) {
11859
+ let hasChanges;
11860
+ if (!snap.baseSha) {
11861
+ hasChanges = true;
11862
+ } else {
11863
+ const dirty = await isWorktreeDirty(snap.worktreePath);
11864
+ if (dirty) {
11865
+ hasChanges = true;
11866
+ } else {
11867
+ const head = await getCurrentWorktreeHead(snap.worktreePath);
11868
+ hasChanges = head.length === 0 || head !== snap.baseSha;
11869
+ }
11870
+ }
11871
+ if (!hasChanges) {
11872
+ await markAbandoned({ sessionId: snap.sessionId, mainRoot });
11873
+ try {
11874
+ await removeWorktree({ root: mainRoot, worktree_path: snap.worktreePath, force: true });
11875
+ } catch (err) {
11876
+ debugLog(`stale 清理 worktree 失败 (${snap.sessionId}): ${err.message}`);
11877
+ }
11878
+ await deleteBranchIfExists({ root: mainRoot, branch: snap.branch }).catch(() => ({
11879
+ deleted: false
11880
+ }));
11881
+ result.staleCleaned.push(snap.sessionId);
11882
+ return;
11883
+ }
11884
+ let salvageOk = true;
11885
+ try {
11886
+ if (await isWorktreeDirty(snap.worktreePath)) {
11887
+ await checkpointCommit({
11888
+ worktreePath: snap.worktreePath,
11889
+ message: STALE_SALVAGE_CHECKPOINT_MSG
11890
+ });
11891
+ }
11892
+ } catch (err) {
11893
+ debugLog(`stale checkpoint commit 失败 (${snap.sessionId}): ${err.message}`);
11894
+ salvageOk = false;
11895
+ }
11896
+ let salvageBranch = null;
11897
+ if (salvageOk) {
11898
+ const head = await getCurrentWorktreeHead(snap.worktreePath);
11899
+ if (head.length === 0) {
11900
+ salvageOk = false;
11901
+ } else {
11902
+ salvageBranch = await createSalvageBranch({
11903
+ mainRoot,
11904
+ sessionId: snap.sessionId,
11905
+ fromRef: head
11906
+ });
11907
+ if (!salvageBranch)
11908
+ salvageOk = false;
11909
+ }
11910
+ }
11911
+ if (!salvageOk) {
11912
+ debugLog(`stale salvage 失败,保留 worktree 待重试 (${snap.sessionId})`);
11913
+ result.staleKept.push(snap.sessionId);
11914
+ return;
11915
+ }
11916
+ await markAbandoned({ sessionId: snap.sessionId, mainRoot });
11917
+ try {
11918
+ await removeWorktree({ root: mainRoot, worktree_path: snap.worktreePath, force: true });
11919
+ } catch (err) {
11920
+ debugLog(`stale salvage 后清理 worktree 失败 (${snap.sessionId}): ${err.message}`);
11921
+ }
11922
+ result.staleSalvaged.push(snap.sessionId);
11923
+ }
11487
11924
  function summarizeReconcileDigest(result, prune, maxList = 3) {
11488
11925
  const unreadable = result.registryUnreadable ?? prune?.registryUnreadable;
11489
11926
  if (unreadable) {
11490
11927
  const hint = unreadable.reason === "version_too_new" ? "版本过高,请升级 CodeForge 后再操作" : "文件损坏,已备份留痕,请检查后删除让系统重建";
11491
11928
  return `[codeforge] ⚠️ session 追踪文件不可读(${unreadable.reason}:${unreadable.detail}),本轮已跳过 worktree 清理(fail-closed,未删除任何 worktree)。${hint}。`;
11492
11929
  }
11493
- const totalReconcile = result.cleanedCreating.length + result.finishedRemoving.length + result.keptConservative.length;
11930
+ const staleSalvaged = result.staleSalvaged?.length ?? 0;
11931
+ const staleCleaned = result.staleCleaned?.length ?? 0;
11932
+ const staleKept = result.staleKept?.length ?? 0;
11933
+ const totalReconcile = result.cleanedCreating.length + result.finishedRemoving.length + result.keptConservative.length + staleSalvaged + staleCleaned + staleKept;
11494
11934
  const cleanedPrune = prune?.cleaned.length ?? 0;
11495
11935
  const failedPrune = prune?.failed.length ?? 0;
11496
11936
  if (totalReconcile === 0 && cleanedPrune === 0 && failedPrune === 0)
@@ -11505,6 +11945,12 @@ function summarizeReconcileDigest(result, prune, maxList = 3) {
11505
11945
  if (result.keptConservative.length > 0) {
11506
11946
  parts.push(`保守保留 ${result.keptConservative.length}`);
11507
11947
  }
11948
+ if (staleSalvaged > 0)
11949
+ parts.push(`超期留痕 ${staleSalvaged}`);
11950
+ if (staleCleaned > 0)
11951
+ parts.push(`超期清理 ${staleCleaned}`);
11952
+ if (staleKept > 0)
11953
+ parts.push(`超期保留 ${staleKept}`);
11508
11954
  if (cleanedPrune > 0)
11509
11955
  parts.push(`清理僵尸 worktree ${cleanedPrune}`);
11510
11956
  if (failedPrune > 0)
@@ -11512,6 +11958,8 @@ function summarizeReconcileDigest(result, prune, maxList = 3) {
11512
11958
  const sample = [
11513
11959
  ...result.cleanedCreating,
11514
11960
  ...result.finishedRemoving,
11961
+ ...result.staleSalvaged ?? [],
11962
+ ...result.staleCleaned ?? [],
11515
11963
  ...prune?.cleaned ?? []
11516
11964
  ];
11517
11965
  const uniq = [...new Set(sample)].slice(0, maxList);
@@ -11519,9 +11967,127 @@ function summarizeReconcileDigest(result, prune, maxList = 3) {
11519
11967
  const sampleStr = uniq.length > 0 ? `(${uniq.join(", ")}${more})` : "";
11520
11968
  return `[codeforge] worktree 收敛:${parts.join(",")}${sampleStr}`;
11521
11969
  }
11970
+ function buildHealthDigest(input) {
11971
+ const signals = [];
11972
+ if (input.registry.kind === "corrupt") {
11973
+ signals.push({
11974
+ kind: "registry_corrupt",
11975
+ count: 1,
11976
+ nextStep: "session 追踪文件损坏,已备份留痕,请检查后删除让系统重建(这不代表 worktree 丢了)。"
11977
+ });
11978
+ } else if (input.registry.kind === "version_too_new") {
11979
+ signals.push({
11980
+ kind: "registry_version_too_new",
11981
+ count: 1,
11982
+ nextStep: "session 追踪文件版本过高,请运行 `bash install.sh --global` 升级 CodeForge 后再操作。"
11983
+ });
11984
+ }
11985
+ const entries = input.registry.kind === "ok" ? input.registry.registry.entries : [];
11986
+ const now = Date.now();
11987
+ const staleCount = entries.filter((e) => {
11988
+ if (e.status !== "creating" && e.status !== "removing")
11989
+ return false;
11990
+ const ts = Date.parse(e.updatedAt);
11991
+ return Number.isFinite(ts) && now - ts > TRANSITIONAL_STALE_MS;
11992
+ }).length;
11993
+ if (staleCount > 0) {
11994
+ signals.push({
11995
+ kind: "transitional_stale",
11996
+ count: staleCount,
11997
+ nextStep: `${staleCount} 个 worktree 超期未收尾,重启 opencode 触发自动收尾,或用 /discard-session 放弃。`
11998
+ });
11999
+ }
12000
+ const salvageCount = input.salvageBranches.length;
12001
+ if (salvageCount > 0) {
12002
+ signals.push({
12003
+ kind: "abandoned_salvage",
12004
+ count: salvageCount,
12005
+ nextStep: `${salvageCount} 个放弃的改动已留痕,可用 \`git cherry-pick <ref>\` 找回,或忽略让系统稍后清理。`
12006
+ });
12007
+ }
12008
+ if (input.pendingReviewCount > 0) {
12009
+ signals.push({
12010
+ kind: "pending_review",
12011
+ count: input.pendingReviewCount,
12012
+ nextStep: `${input.pendingReviewCount} 个 session 待审,先 /review 通过后再 /merge 合入。`
12013
+ });
12014
+ }
12015
+ const laneConflictCount = input.laneConflictCount ?? 0;
12016
+ if (laneConflictCount > 0) {
12017
+ signals.push({
12018
+ kind: "lane_conflict",
12019
+ count: laneConflictCount,
12020
+ nextStep: `${laneConflictCount} 路并行改动有冲突需人工解,用 session_merge action=status 查看详情。`
12021
+ });
12022
+ }
12023
+ const awaitingCount = input.journal.filter((j) => isCommittedOrLater(j.phase) && j.phase !== "done").length;
12024
+ if (awaitingCount > 0) {
12025
+ signals.push({
12026
+ kind: "awaiting_confirm",
12027
+ count: awaitingCount,
12028
+ nextStep: `${awaitingCount} 个合并已提交但未收尾,触发 GC 或重启 opencode 后会自动补完,无需手动干预。`
12029
+ });
12030
+ }
12031
+ const ownerAliasCount = input.ownerAliasCount ?? 0;
12032
+ if (ownerAliasCount > 0) {
12033
+ signals.push({
12034
+ kind: "owner_alias",
12035
+ count: ownerAliasCount,
12036
+ nextStep: `${ownerAliasCount} 个子 session 审批已归一到父 worktree,无需操作(信息性)。`,
12037
+ informational: true
12038
+ });
12039
+ }
12040
+ const rank = (s) => {
12041
+ if (s.kind === "registry_corrupt" || s.kind === "registry_version_too_new")
12042
+ return 0;
12043
+ if (s.informational)
12044
+ return 2;
12045
+ return 1;
12046
+ };
12047
+ signals.sort((a, b) => rank(a) - rank(b));
12048
+ const actionable = signals.filter((s) => !s.informational);
12049
+ const hasIssues = actionable.length > 0;
12050
+ let summary = "";
12051
+ if (hasIssues) {
12052
+ const head = actionable[0];
12053
+ const rest = actionable.length - 1;
12054
+ const restStr = rest > 0 ? ` 另有 ${rest} 类待处理(详见 session_merge status / codeforge doctor --runtime)。` : "";
12055
+ summary = `[codeforge] worktree 健康:${head.nextStep}${restStr}`;
12056
+ }
12057
+ return { hasIssues, signals, summary };
12058
+ }
12059
+ async function collectHealthDigest(mainRoot) {
12060
+ const resolved = path13.resolve(mainRoot);
12061
+ const [registry, journal, salvageBranches] = await Promise.all([
12062
+ readRegistryResult(resolved).catch(() => ({ kind: "missing" })),
12063
+ listMergeJournalEntries(resolved).catch(() => []),
12064
+ listSalvageBranches(resolved).catch(() => [])
12065
+ ]);
12066
+ const entries = registry.kind === "ok" ? registry.registry.entries : [];
12067
+ const activeEntries = entries.filter((e) => e.status === "active");
12068
+ let pendingReviewCount = 0;
12069
+ if (activeEntries.length > 0) {
12070
+ const store = ApprovalStore.forProject(resolved);
12071
+ const checks = await Promise.all(activeEntries.map(async (e) => {
12072
+ try {
12073
+ const latest = await store.getLatest(`session:${e.sessionId}`);
12074
+ return latest ? 0 : 1;
12075
+ } catch {
12076
+ return 0;
12077
+ }
12078
+ }));
12079
+ pendingReviewCount = checks.reduce((a, b) => a + b, 0);
12080
+ }
12081
+ return buildHealthDigest({
12082
+ registry,
12083
+ journal,
12084
+ salvageBranches,
12085
+ pendingReviewCount
12086
+ });
12087
+ }
11522
12088
  async function getCurrentWorktreeHead(worktreePath) {
11523
12089
  try {
11524
- return (await runGit2(path12.resolve(worktreePath), ["rev-parse", "HEAD"])).trim();
12090
+ return (await runGit2(path13.resolve(worktreePath), ["rev-parse", "HEAD"])).trim();
11525
12091
  } catch {
11526
12092
  return "";
11527
12093
  }
@@ -11540,7 +12106,7 @@ async function checkpointCommit(opts) {
11540
12106
  }
11541
12107
  var CHECKPOINT_MESSAGE_PREFIX = "[codeforge-checkpoint] pre-lane-dispatch";
11542
12108
  async function checkpointSessionWorktree(opts) {
11543
- const mainRoot = path12.resolve(opts.mainRoot);
12109
+ const mainRoot = path13.resolve(opts.mainRoot);
11544
12110
  await loadRegistryForMutation(mainRoot);
11545
12111
  const entry = await getSessionWorktree(opts.sessionId, mainRoot);
11546
12112
  if (!entry) {
@@ -11563,19 +12129,19 @@ async function checkpointSessionWorktree(opts) {
11563
12129
  return { sessionId: opts.sessionId, worktreePath: wt, committed: true, head };
11564
12130
  }
11565
12131
  function runGit2(cwd, args, timeoutMs = 1e4) {
11566
- return new Promise((resolve11, reject) => {
12132
+ return new Promise((resolve12, reject) => {
11567
12133
  execFile3("git", args, { cwd, timeout: timeoutMs, windowsHide: true, encoding: "utf8" }, (err, stdout, stderr) => {
11568
12134
  if (err) {
11569
12135
  reject(new Error(`git ${args.join(" ")} (cwd=${cwd}) 失败: ${stderr?.trim() || err.message}`));
11570
12136
  return;
11571
12137
  }
11572
- resolve11(stdout);
12138
+ resolve12(stdout);
11573
12139
  });
11574
12140
  });
11575
12141
  }
11576
12142
  function runGitWithEnv(cwd, args, envOverrides, timeoutMs = 1e4) {
11577
12143
  const inheritedEnv = process["env"];
11578
- return new Promise((resolve11, reject) => {
12144
+ return new Promise((resolve12, reject) => {
11579
12145
  execFile3("git", args, {
11580
12146
  cwd,
11581
12147
  timeout: timeoutMs,
@@ -11587,7 +12153,7 @@ function runGitWithEnv(cwd, args, envOverrides, timeoutMs = 1e4) {
11587
12153
  reject(new Error(`git ${args.join(" ")} (cwd=${cwd}) 失败: ${stderr?.trim() || err.message}`));
11588
12154
  return;
11589
12155
  }
11590
- resolve11(stdout);
12156
+ resolve12(stdout);
11591
12157
  });
11592
12158
  });
11593
12159
  }
@@ -11605,7 +12171,7 @@ async function getBuildScript(mainRoot) {
11605
12171
  async function shouldSkipDevOnce(mainRoot, stagedPaths, worktreePath) {
11606
12172
  let distMtimeSec;
11607
12173
  try {
11608
- const st = await fs10.stat(path12.join(mainRoot, "dist/index.js"));
12174
+ const st = await fs11.stat(path13.join(mainRoot, "dist/index.js"));
11609
12175
  distMtimeSec = Math.floor(st.mtimeMs / 1000);
11610
12176
  } catch {
11611
12177
  return false;
@@ -11625,39 +12191,81 @@ async function shouldSkipDevOnce(mainRoot, stagedPaths, worktreePath) {
11625
12191
  async function statSourceMtime(rel, mainRoot, worktreePath) {
11626
12192
  if (worktreePath) {
11627
12193
  try {
11628
- const st = await fs10.stat(path12.join(worktreePath, rel));
12194
+ const st = await fs11.stat(path13.join(worktreePath, rel));
11629
12195
  return Math.floor(st.mtimeMs / 1000);
11630
12196
  } catch {}
11631
12197
  }
11632
12198
  try {
11633
- const st = await fs10.stat(path12.join(mainRoot, rel));
12199
+ const st = await fs11.stat(path13.join(mainRoot, rel));
11634
12200
  return Math.floor(st.mtimeMs / 1000);
11635
12201
  } catch {
11636
12202
  return null;
11637
12203
  }
11638
12204
  }
11639
12205
  function runCmd(cmd, args, cwd, timeoutMs = 300000) {
11640
- return new Promise((resolve11, reject) => {
12206
+ return new Promise((resolve12, reject) => {
11641
12207
  execFile3(cmd, args, { cwd, timeout: timeoutMs, windowsHide: true, encoding: "utf8" }, (err, stdout, stderr) => {
11642
12208
  if (err) {
11643
12209
  reject(new Error(`${cmd} ${args.join(" ")} (cwd=${cwd}) 失败: ${stderr?.trim() || err.message}`));
11644
12210
  return;
11645
12211
  }
11646
- resolve11(stdout);
12212
+ resolve12(stdout);
11647
12213
  });
11648
12214
  });
11649
12215
  }
11650
- function buildMergeMessage(sessionId, branch, baseSha, squashed) {
11651
- const subject = `session(${sessionId}): merge ${branch}`;
11652
- const body = squashed.length > 0 ? `
12216
+ var MERGE_MESSAGE_NOISE_PREFIXES = [
12217
+ "[codeforge-checkpoint]",
12218
+ "[checkpoint]",
12219
+ "auto-commit before merge",
12220
+ "chore(release)",
12221
+ "merge-sync",
12222
+ "sync-to-main",
12223
+ "release sync"
12224
+ ];
12225
+ function filterSquashedCommits(commits, noisePrefixes = MERGE_MESSAGE_NOISE_PREFIXES) {
12226
+ const kept = [];
12227
+ let dropped = 0;
12228
+ for (const c of commits) {
12229
+ const isNoise = noisePrefixes.some((p) => c.includes(p));
12230
+ if (isNoise)
12231
+ dropped++;
12232
+ else
12233
+ kept.push(c);
12234
+ }
12235
+ return { kept, dropped };
12236
+ }
12237
+ async function buildMergeMessage(args) {
12238
+ const { sessionId, branch, baseSha, squashed } = args;
12239
+ const sid8 = sessionId.slice(0, 8);
12240
+ let subject;
12241
+ if (args.planStore && args.requiredPlanId) {
12242
+ try {
12243
+ const planRead = await args.planStore.read(args.requiredPlanId);
12244
+ const title = planRead?.entry.title?.trim();
12245
+ if (title)
12246
+ subject = `${title} (session ${sid8})`;
12247
+ } catch {}
12248
+ }
12249
+ if (!subject) {
12250
+ const summary = args.summary?.trim();
12251
+ if (summary)
12252
+ subject = `${summary} (session ${sid8})`;
12253
+ }
12254
+ if (!subject)
12255
+ subject = `session(${sessionId}): merge ${branch}`;
12256
+ const { kept, dropped } = filterSquashedCommits(squashed);
12257
+ const body = kept.length > 0 ? `
11653
12258
 
11654
12259
  Squashed commits:
11655
- ${squashed.map((s) => ` - ${s}`).join(`
12260
+ ${kept.map((s) => ` - ${s}`).join(`
11656
12261
  `)}` : "";
11657
- const footer = `
12262
+ let footer = `
11658
12263
 
11659
12264
  Codeforge-Session: ${sessionId}
11660
12265
  Codeforge-Base: ${baseSha.slice(0, 12)}`;
12266
+ if (dropped > 0)
12267
+ footer += `
12268
+ Codeforge-Squash-Dropped: ${dropped}`;
11661
12269
  return subject + body + footer;
11662
12270
  }
11663
12271
  var ORPHAN_GRACE_MS = 60000;
@@ -11668,7 +12276,7 @@ async function pruneDiscardedRegistryEntries(mainRoot, opts = {}) {
11668
12276
  if (keepRecent < 0) {
11669
12277
  throw new Error(`pruneDiscardedRegistryEntries: keepRecent 必须 ≥ 0,收到 ${keepRecent}`);
11670
12278
  }
11671
- return await mutateRegistry(path12.resolve(mainRoot), (reg) => {
12279
+ return await mutateRegistry(path13.resolve(mainRoot), (reg) => {
11672
12280
  const discarded = [];
11673
12281
  const others = [];
11674
12282
  for (const e of reg.entries) {
@@ -11685,7 +12293,7 @@ async function pruneDiscardedRegistryEntries(mainRoot, opts = {}) {
11685
12293
  });
11686
12294
  }
11687
12295
  async function pruneOrphanWorktrees(mainRoot, opts = {}) {
11688
- const resolved = path12.resolve(mainRoot);
12296
+ const resolved = path13.resolve(mainRoot);
11689
12297
  const cleaned = [];
11690
12298
  const failed = [];
11691
12299
  let skipped = 0;
@@ -11710,7 +12318,7 @@ async function pruneOrphanWorktrees(mainRoot, opts = {}) {
11710
12318
  } catch {
11711
12319
  gitWorktrees = [];
11712
12320
  }
11713
- const gitWorktreePaths = new Set(gitWorktrees.map((w) => path12.resolve(w.path)));
12321
+ const gitWorktreePaths = new Set(gitWorktrees.map((w) => path13.resolve(w.path)));
11714
12322
  await mutateRegistry(resolved, async (reg2) => {
11715
12323
  const now = Date.now();
11716
12324
  for (const entry of reg2.entries) {
@@ -11720,7 +12328,7 @@ async function pruneOrphanWorktrees(mainRoot, opts = {}) {
11720
12328
  let dirExists = true;
11721
12329
  let dirMtimeMs = 0;
11722
12330
  try {
11723
- const st = await fs10.stat(wt);
12331
+ const st = await fs11.stat(wt);
11724
12332
  dirExists = st.isDirectory();
11725
12333
  dirMtimeMs = st.mtimeMs;
11726
12334
  } catch {
@@ -11813,12 +12421,12 @@ async function pruneOrphanWorktrees(mainRoot, opts = {}) {
11813
12421
  }
11814
12422
  });
11815
12423
  }
11816
- const codeforgeWorktreeRoot = path12.resolve(path12.join(resolved, DEFAULT_WORKTREE_SUBDIR));
12424
+ const codeforgeWorktreeRoot = path13.resolve(path13.join(resolved, DEFAULT_WORKTREE_SUBDIR));
11817
12425
  const fsWorktreePaths = [];
11818
12426
  try {
11819
- const names = await fs10.readdir(codeforgeWorktreeRoot);
12427
+ const names = await fs11.readdir(codeforgeWorktreeRoot);
11820
12428
  for (const name of names) {
11821
- fsWorktreePaths.push(path12.resolve(path12.join(codeforgeWorktreeRoot, name)));
12429
+ fsWorktreePaths.push(path13.resolve(path13.join(codeforgeWorktreeRoot, name)));
11822
12430
  }
11823
12431
  } catch {}
11824
12432
  const candidatePaths = new Set([
@@ -11826,16 +12434,16 @@ async function pruneOrphanWorktrees(mainRoot, opts = {}) {
11826
12434
  ...fsWorktreePaths
11827
12435
  ]);
11828
12436
  const reg = await readRegistry(resolved);
11829
- const knownPaths = new Set(reg.entries.map((e) => path12.resolve(e.worktreePath)));
12437
+ const knownPaths = new Set(reg.entries.map((e) => path13.resolve(e.worktreePath)));
11830
12438
  for (const candidate of candidatePaths) {
11831
12439
  if (knownPaths.has(candidate))
11832
12440
  continue;
11833
- if (candidate !== codeforgeWorktreeRoot && !candidate.startsWith(codeforgeWorktreeRoot + path12.sep)) {
12441
+ if (candidate !== codeforgeWorktreeRoot && !candidate.startsWith(codeforgeWorktreeRoot + path13.sep)) {
11834
12442
  continue;
11835
12443
  }
11836
12444
  let dirExists = true;
11837
12445
  try {
11838
- const st = await fs10.stat(candidate);
12446
+ const st = await fs11.stat(candidate);
11839
12447
  if (Date.now() - st.mtimeMs < ORPHAN_GRACE_MS) {
11840
12448
  skipped++;
11841
12449
  continue;
@@ -11857,14 +12465,14 @@ async function pruneOrphanWorktrees(mainRoot, opts = {}) {
11857
12465
  }
11858
12466
  if (removed) {
11859
12467
  try {
11860
- await fs10.stat(candidate);
12468
+ await fs11.stat(candidate);
11861
12469
  removed = false;
11862
12470
  lastError = lastError ?? "git worktree remove 返回成功但目录仍存在(C 类 fs-only orphan)";
11863
12471
  } catch {}
11864
12472
  }
11865
12473
  if (!removed && dirExists) {
11866
12474
  try {
11867
- await fs10.rm(candidate, { recursive: true, force: true });
12475
+ await fs11.rm(candidate, { recursive: true, force: true });
11868
12476
  removed = true;
11869
12477
  } catch (err) {
11870
12478
  lastError = `git remove 失败: ${lastError}; fs.rm 也失败: ${err instanceof Error ? err.message : String(err)}`;
@@ -12040,7 +12648,7 @@ async function execute4(input) {
12040
12648
  import { z as z5 } from "zod";
12041
12649
 
12042
12650
  // lib/browser-control.ts
12043
- import * as path13 from "node:path";
12651
+ import * as path14 from "node:path";
12044
12652
  var DEFAULT_CONFIG2 = {
12045
12653
  enabled: false,
12046
12654
  headless: true,
@@ -12052,7 +12660,7 @@ var DEFAULT_CONFIG2 = {
12052
12660
  bufferLimit: 500
12053
12661
  };
12054
12662
  function defaultScreenshotDir(root = process.cwd()) {
12055
- return path13.join(runtimeDir(root), "browser", "screenshots");
12663
+ return path14.join(runtimeDir(root), "browser", "screenshots");
12056
12664
  }
12057
12665
  function checkUrl(url, cfg = DEFAULT_CONFIG2) {
12058
12666
  if (typeof url !== "string" || url.trim() === "") {
@@ -12177,14 +12785,14 @@ async function tryCreatePlaywrightController(cfg = DEFAULT_CONFIG2, resolver = d
12177
12785
  async screenshot(opts) {
12178
12786
  try {
12179
12787
  const dir = cfg.screenshotDir && cfg.screenshotDir.trim().length > 0 ? cfg.screenshotDir : defaultScreenshotDir();
12180
- const path14 = `${dir}/${Date.now()}.png`;
12788
+ const path15 = `${dir}/${Date.now()}.png`;
12181
12789
  if (opts?.selector) {
12182
12790
  const el = await page.locator(opts.selector).first();
12183
- await el.screenshot({ path: path14 });
12791
+ await el.screenshot({ path: path15 });
12184
12792
  } else {
12185
- await page.screenshot({ path: path14, fullPage: opts?.fullPage });
12793
+ await page.screenshot({ path: path15, fullPage: opts?.fullPage });
12186
12794
  }
12187
- return { ok: true, path: path14 };
12795
+ return { ok: true, path: path15 };
12188
12796
  } catch (err) {
12189
12797
  return { ok: false, error: describe3(err) };
12190
12798
  }
@@ -12501,8 +13109,8 @@ async function execute10(input) {
12501
13109
  import { z as z11 } from "zod";
12502
13110
 
12503
13111
  // lib/model-config.ts
12504
- import { promises as fs11 } from "node:fs";
12505
- import * as path14 from "node:path";
13112
+ import { promises as fs12 } from "node:fs";
13113
+ import * as path15 from "node:path";
12506
13114
 
12507
13115
  // lib/model-tier.ts
12508
13116
  var TIER_ORDER = ["quick", "balanced", "deep", "ultra"];
@@ -12519,12 +13127,12 @@ var PROVIDER_MODEL_RE = /^[a-z0-9-]+\/[a-zA-Z0-9._-]+$/;
12519
13127
  function findConfigFileSync(opts = {}) {
12520
13128
  const root = opts.root ?? process.cwd();
12521
13129
  const fsSync = __require("node:fs");
12522
- const abs = path14.resolve(root, opts.file ?? CONFIG_FILE);
13130
+ const abs = path15.resolve(root, opts.file ?? CONFIG_FILE);
12523
13131
  return fsSync.existsSync(abs) ? abs : null;
12524
13132
  }
12525
13133
  function loadModelConfigSync(opts = {}) {
12526
13134
  const root = opts.root ?? process.cwd();
12527
- const abs = path14.resolve(root, opts.file ?? CONFIG_FILE);
13135
+ const abs = path15.resolve(root, opts.file ?? CONFIG_FILE);
12528
13136
  const fsSync = __require("node:fs");
12529
13137
  if (!fsSync.existsSync(abs)) {
12530
13138
  return { ok: false, warnings: [], error: `config_not_found: ${abs}` };
@@ -12539,10 +13147,10 @@ function loadModelConfigSync(opts = {}) {
12539
13147
  }
12540
13148
  async function loadModelConfig(opts = {}) {
12541
13149
  const root = opts.root ?? process.cwd();
12542
- const abs = path14.resolve(root, opts.file ?? CONFIG_FILE);
13150
+ const abs = path15.resolve(root, opts.file ?? CONFIG_FILE);
12543
13151
  let raw;
12544
13152
  try {
12545
- raw = await fs11.readFile(abs, "utf8");
13153
+ raw = await fs12.readFile(abs, "utf8");
12546
13154
  } catch (e) {
12547
13155
  const code = e.code;
12548
13156
  if (code === "ENOENT") {
@@ -13016,8 +13624,8 @@ function toEntry(r) {
13016
13624
  import { z as z12 } from "zod";
13017
13625
 
13018
13626
  // lib/merge-gate.ts
13019
- import { promises as fs12 } from "node:fs";
13020
- import * as path15 from "node:path";
13627
+ import { promises as fs13 } from "node:fs";
13628
+ import * as path16 from "node:path";
13021
13629
  var DEFAULT_MERGE_GATE_CONFIG = {
13022
13630
  enabled: true,
13023
13631
  approvalPreCheck: true,
@@ -13025,10 +13633,10 @@ var DEFAULT_MERGE_GATE_CONFIG = {
13025
13633
  };
13026
13634
  var CONFIG_REL = ".codeforge/merge-gate.json";
13027
13635
  async function loadMergeGate(mainRoot) {
13028
- const file = path15.join(mainRoot, CONFIG_REL);
13636
+ const file = path16.join(mainRoot, CONFIG_REL);
13029
13637
  let raw;
13030
13638
  try {
13031
- raw = await fs12.readFile(file, "utf8");
13639
+ raw = await fs13.readFile(file, "utf8");
13032
13640
  } catch (err) {
13033
13641
  const e = err;
13034
13642
  if (e.code === "ENOENT")
@@ -13130,7 +13738,8 @@ async function runMergeLoop(opts) {
13130
13738
  progress("approval_pre_check", `skip_review | reviewTarget=${hit.reviewTarget} | coveredSha=${hit.coveredSha.slice(0, 12)} | ttlOk`);
13131
13739
  const { sha } = await mergeSessionBack({
13132
13740
  sessionId: opts.sessionId,
13133
- mainRoot: opts.mainRoot
13741
+ mainRoot: opts.mainRoot,
13742
+ ...opts.summary ? { summary: opts.summary } : {}
13134
13743
  });
13135
13744
  return {
13136
13745
  status: "skipped_by_approval",
@@ -13222,7 +13831,8 @@ async function runMergeLoop(opts) {
13222
13831
  }
13223
13832
  const { sha } = await mergeSessionBack({
13224
13833
  sessionId: opts.sessionId,
13225
- mainRoot: opts.mainRoot
13834
+ mainRoot: opts.mainRoot,
13835
+ ...opts.summary ? { summary: opts.summary } : {}
13226
13836
  });
13227
13837
  return {
13228
13838
  status: "merged",
@@ -13443,7 +14053,7 @@ function isAbortError(err) {
13443
14053
  return err instanceof Error && err.name === "AbortError";
13444
14054
  }
13445
14055
  function withTimeout2(p, ms, label, signal, hbOpts) {
13446
- return new Promise((resolve12, reject) => {
14056
+ return new Promise((resolve13, reject) => {
13447
14057
  const startedAt = Date.now();
13448
14058
  let hbTimer = null;
13449
14059
  let timer;
@@ -13483,7 +14093,7 @@ function withTimeout2(p, ms, label, signal, hbOpts) {
13483
14093
  }
13484
14094
  p.then((v) => {
13485
14095
  cleanup();
13486
- resolve12(v);
14096
+ resolve13(v);
13487
14097
  }, (e) => {
13488
14098
  cleanup();
13489
14099
  reject(e);
@@ -13512,7 +14122,8 @@ var ArgsSchema12 = z12.discriminatedUnion("action", [
13512
14122
  action: z12.literal("merge"),
13513
14123
  session_id: z12.string().optional().describe("默认当前 session"),
13514
14124
  plan_id: PlanIdSchema,
13515
- force: z12.boolean().optional().describe("跳过 review 直接 squash merge(写审计)")
14125
+ force: z12.boolean().optional().describe("跳过 review 直接 squash merge(写审计)"),
14126
+ summary: z12.string().optional().describe("merge commit subject 兜底摘要(plan title 缺失时用)")
13516
14127
  }),
13517
14128
  z12.object({
13518
14129
  action: z12.literal("status"),
@@ -13534,9 +14145,10 @@ var ArgsSchema12 = z12.discriminatedUnion("action", [
13534
14145
  })
13535
14146
  ]);
13536
14147
  async function buildStatusSummary(entry, mainRoot) {
13537
- const [salvageBranches, allEntries] = await Promise.all([
14148
+ const [salvageBranches, allEntries, health] = await Promise.all([
13538
14149
  listSalvageBranches(mainRoot).catch(() => []),
13539
- listEntries(mainRoot).catch(() => [])
14150
+ listEntries(mainRoot).catch(() => []),
14151
+ collectHealthDigest(mainRoot).catch(() => null)
13540
14152
  ]);
13541
14153
  const abandonedCount = allEntries.filter((e) => e.status === "abandoned").length;
13542
14154
  const pendingCount = allEntries.filter((e) => e.status === "interrupted_dirty" || e.status === "creating" || e.status === "removing").length;
@@ -13564,6 +14176,11 @@ async function buildStatusSummary(entry, mainRoot) {
13564
14176
  default:
13565
14177
  nextStep = "未知状态,保守不处理;如有疑问请人工检查 worktree。";
13566
14178
  }
14179
+ const healthDigest = health && health.hasIssues ? health : null;
14180
+ if (healthDigest && healthDigest.summary) {
14181
+ nextStep = `${nextStep}
14182
+ ${healthDigest.summary}`;
14183
+ }
13567
14184
  return {
13568
14185
  worktreePath: entry.worktreePath,
13569
14186
  status: entry.status,
@@ -13571,7 +14188,8 @@ async function buildStatusSummary(entry, mainRoot) {
13571
14188
  salvageBranches,
13572
14189
  abandonedCount,
13573
14190
  pendingCount,
13574
- nextStep
14191
+ nextStep,
14192
+ health: healthDigest
13575
14193
  };
13576
14194
  }
13577
14195
  var _ctx = {};
@@ -13748,6 +14366,7 @@ async function execute12(input) {
13748
14366
  mainRoot,
13749
14367
  ...mergeArgs.plan_id ? { planId: mergeArgs.plan_id } : {},
13750
14368
  ...mergeArgs.force ? { force: true } : {},
14369
+ ...mergeArgs.summary ? { summary: mergeArgs.summary } : {},
13751
14370
  spawner: _ctx.spawner,
13752
14371
  ...sendProgress ? {
13753
14372
  onProgress: (state, detail) => {
@@ -13768,8 +14387,8 @@ async function execute12(input) {
13768
14387
  import { z as z13 } from "zod";
13769
14388
 
13770
14389
  // lib/plan-store.ts
13771
- import { promises as fs13 } from "node:fs";
13772
- import * as path16 from "node:path";
14390
+ import { promises as fs14 } from "node:fs";
14391
+ import * as path17 from "node:path";
13773
14392
  var INDEX_VERSION = 1;
13774
14393
 
13775
14394
  class PlanStore {
@@ -13778,8 +14397,8 @@ class PlanStore {
13778
14397
  now;
13779
14398
  secondCounters = new Map;
13780
14399
  constructor(opts = {}) {
13781
- this.root = path16.resolve(opts.root ?? process.cwd());
13782
- this.base = opts.base ? path16.resolve(opts.base) : plansDir(this.root);
14400
+ this.root = path17.resolve(opts.root ?? process.cwd());
14401
+ this.base = opts.base ? path17.resolve(opts.base) : plansDir(this.root);
13783
14402
  this.now = opts.now ?? (() => new Date);
13784
14403
  }
13785
14404
  async write(input) {
@@ -13789,14 +14408,14 @@ class PlanStore {
13789
14408
  if (typeof input.content !== "string" || input.content.length === 0) {
13790
14409
  throw new Error("PlanStore.write: content 不能为空");
13791
14410
  }
13792
- await fs13.mkdir(this.base, { recursive: true });
14411
+ await fs14.mkdir(this.base, { recursive: true });
13793
14412
  const lockPath = this.lockPath();
13794
14413
  return await withFileLock(lockPath, async () => {
13795
14414
  const index = await this.readIndexLocked();
13796
14415
  const now = this.now();
13797
14416
  const planId = this.allocPlanId(now, index);
13798
14417
  const filename = this.composeFilename(planId, input.title);
13799
- const absFile = path16.join(this.base, filename);
14418
+ const absFile = path17.join(this.base, filename);
13800
14419
  await this.atomicWriteFile(absFile, input.content);
13801
14420
  const entry = {
13802
14421
  plan_id: planId,
@@ -13820,9 +14439,9 @@ class PlanStore {
13820
14439
  const entry = index.entries.find((e) => e.plan_id === planId);
13821
14440
  if (!entry)
13822
14441
  return null;
13823
- const abs = path16.join(this.base, entry.path);
14442
+ const abs = path17.join(this.base, entry.path);
13824
14443
  try {
13825
- const content = await fs13.readFile(abs, "utf8");
14444
+ const content = await fs14.readFile(abs, "utf8");
13826
14445
  return { entry, content };
13827
14446
  } catch (err) {
13828
14447
  const e = err;
@@ -13881,7 +14500,7 @@ class PlanStore {
13881
14500
  else if (e.status === "orphan")
13882
14501
  shouldDelete = true;
13883
14502
  if (shouldDelete) {
13884
- await fs13.rm(path16.join(this.base, e.path), { force: true }).catch(() => {});
14503
+ await fs14.rm(path17.join(this.base, e.path), { force: true }).catch(() => {});
13885
14504
  removed++;
13886
14505
  } else {
13887
14506
  keep.push(e);
@@ -13903,9 +14522,9 @@ class PlanStore {
13903
14522
  knownPaths.add(e.path);
13904
14523
  if (e.status !== "active")
13905
14524
  continue;
13906
- const abs = path16.join(this.base, e.path);
14525
+ const abs = path17.join(this.base, e.path);
13907
14526
  try {
13908
- await fs13.stat(abs);
14527
+ await fs14.stat(abs);
13909
14528
  } catch {
13910
14529
  e.status = "orphan";
13911
14530
  markedOrphan++;
@@ -13915,23 +14534,23 @@ class PlanStore {
13915
14534
  await this.writeIndexLocked(index);
13916
14535
  let unindexedFiles = [];
13917
14536
  try {
13918
- const all = await fs13.readdir(this.base);
14537
+ const all = await fs14.readdir(this.base);
13919
14538
  unindexedFiles = all.filter((f) => f.endsWith(".md")).filter((f) => !knownPaths.has(f));
13920
14539
  } catch {}
13921
14540
  return { markedOrphan, unindexedFiles };
13922
14541
  });
13923
14542
  }
13924
14543
  indexPath() {
13925
- return path16.join(this.base, "index.json");
14544
+ return path17.join(this.base, "index.json");
13926
14545
  }
13927
14546
  lockPath() {
13928
- return path16.join(this.base, "index.lock");
14547
+ return path17.join(this.base, "index.lock");
13929
14548
  }
13930
14549
  async readIndexLocked() {
13931
14550
  const file = this.indexPath();
13932
14551
  let raw;
13933
14552
  try {
13934
- raw = await fs13.readFile(file, "utf8");
14553
+ raw = await fs14.readFile(file, "utf8");
13935
14554
  } catch (err) {
13936
14555
  const e = err;
13937
14556
  if (e.code === "ENOENT")
@@ -13953,7 +14572,7 @@ class PlanStore {
13953
14572
  async archiveCorruptIndex(file) {
13954
14573
  const ts = this.now().toISOString().replace(/[-:.TZ]/g, "").slice(0, 14);
13955
14574
  const dst = `${file}.corrupt-${ts}`;
13956
- await fs13.rename(file, dst).catch(() => {});
14575
+ await fs14.rename(file, dst).catch(() => {});
13957
14576
  }
13958
14577
  async readIndex() {
13959
14578
  return this.readIndexLocked();
@@ -13998,15 +14617,15 @@ class PlanStore {
13998
14617
  const tsPart = m ? `${m[1]}-${m[2]}` : planId;
13999
14618
  const nnn = m ? m[3] : "000";
14000
14619
  const sample = planFilePath(this.root, title);
14001
- const base = path16.basename(sample, ".md");
14620
+ const base = path17.basename(sample, ".md");
14002
14621
  const slug = base.replace(/^\d{8}-\d{6}-?/, "") || "untitled";
14003
14622
  return `${tsPart}-${nnn}-${slug}.md`;
14004
14623
  }
14005
14624
  async atomicWriteFile(file, data) {
14006
- await fs13.mkdir(path16.dirname(file), { recursive: true });
14625
+ await fs14.mkdir(path17.dirname(file), { recursive: true });
14007
14626
  const tmp = `${file}.tmp-${process.pid}-${Date.now()}`;
14008
- await fs13.writeFile(tmp, data, "utf8");
14009
- await fs13.rename(tmp, file);
14627
+ await fs14.writeFile(tmp, data, "utf8");
14628
+ await fs14.rename(tmp, file);
14010
14629
  }
14011
14630
  }
14012
14631
  function formatTimestamp(d) {
@@ -14069,8 +14688,8 @@ async function execute13(input) {
14069
14688
  }
14070
14689
  }
14071
14690
  // tools/plan-read.ts
14072
- import { promises as fs14 } from "node:fs";
14073
- import * as path17 from "node:path";
14691
+ import { promises as fs15 } from "node:fs";
14692
+ import * as path18 from "node:path";
14074
14693
  import { z as z14 } from "zod";
14075
14694
  var description14 = [
14076
14695
  "读取方案文档内容,支持按 plan_id 或绝对路径查询。",
@@ -14176,9 +14795,9 @@ async function execute14(input) {
14176
14795
  };
14177
14796
  }
14178
14797
  }
14179
- const abs = path17.resolve(args.path);
14798
+ const abs = path18.resolve(args.path);
14180
14799
  try {
14181
- const content = await fs14.readFile(abs, "utf8");
14800
+ const content = await fs15.readFile(abs, "utf8");
14182
14801
  return {
14183
14802
  ok: true,
14184
14803
  content,
@@ -14203,16 +14822,16 @@ import { z as z15 } from "zod";
14203
14822
  // lib/adr-init.ts
14204
14823
  import { spawnSync } from "node:child_process";
14205
14824
  import { existsSync as existsSync4, promises as fsp } from "node:fs";
14206
- import * as path18 from "node:path";
14825
+ import * as path19 from "node:path";
14207
14826
  import * as url from "node:url";
14208
14827
  function resolveAssetsRoot() {
14209
- const here = path18.dirname(url.fileURLToPath(import.meta.url));
14828
+ const here = path19.dirname(url.fileURLToPath(import.meta.url));
14210
14829
  let dir = here;
14211
14830
  for (let i = 0;i < 6; i++) {
14212
- if (existsSync4(path18.join(dir, "package.json")) && existsSync4(path18.join(dir, "assets", "adr-init"))) {
14213
- return path18.join(dir, "assets", "adr-init");
14831
+ if (existsSync4(path19.join(dir, "package.json")) && existsSync4(path19.join(dir, "assets", "adr-init"))) {
14832
+ return path19.join(dir, "assets", "adr-init");
14214
14833
  }
14215
- const parent = path18.dirname(dir);
14834
+ const parent = path19.dirname(dir);
14216
14835
  if (parent === dir)
14217
14836
  break;
14218
14837
  dir = parent;
@@ -14220,13 +14839,13 @@ function resolveAssetsRoot() {
14220
14839
  const xdgConfig = process.env["XDG_CONFIG_HOME"];
14221
14840
  const homeDir = process.env["HOME"] ?? process.env["USERPROFILE"] ?? "";
14222
14841
  const fallbackRoots = [
14223
- xdgConfig ? path18.join(xdgConfig, "opencode") : null,
14224
- path18.join(homeDir, ".config", "opencode"),
14225
- process.env["APPDATA"] ? path18.join(process.env["APPDATA"], "opencode") : null,
14226
- process.env["LOCALAPPDATA"] ? path18.join(process.env["LOCALAPPDATA"], "opencode") : null
14842
+ xdgConfig ? path19.join(xdgConfig, "opencode") : null,
14843
+ path19.join(homeDir, ".config", "opencode"),
14844
+ process.env["APPDATA"] ? path19.join(process.env["APPDATA"], "opencode") : null,
14845
+ process.env["LOCALAPPDATA"] ? path19.join(process.env["LOCALAPPDATA"], "opencode") : null
14227
14846
  ].filter(Boolean);
14228
14847
  for (const root of fallbackRoots) {
14229
- const candidate = path18.join(root, "assets", "adr-init");
14848
+ const candidate = path19.join(root, "assets", "adr-init");
14230
14849
  if (existsSync4(candidate)) {
14231
14850
  return candidate;
14232
14851
  }
@@ -14263,7 +14882,7 @@ function runGitConfigHooksPath(cwd) {
14263
14882
  }
14264
14883
  }
14265
14884
  async function runAdrInit(opts = {}) {
14266
- const cwd = path18.resolve(opts.cwd ?? process.cwd());
14885
+ const cwd = path19.resolve(opts.cwd ?? process.cwd());
14267
14886
  const force = !!opts.force;
14268
14887
  const dryRun = !!opts.dryRun;
14269
14888
  const writePrepare = !!opts.writePrepare;
@@ -14312,8 +14931,8 @@ async function runAdrInit(opts = {}) {
14312
14931
  });
14313
14932
  }
14314
14933
  for (const item of plan) {
14315
- const srcAbs = path18.join(assetsRoot, item.src);
14316
- const dstAbs = path18.join(cwd, item.dst);
14934
+ const srcAbs = path19.join(assetsRoot, item.src);
14935
+ const dstAbs = path19.join(cwd, item.dst);
14317
14936
  if (!existsSync4(srcAbs)) {
14318
14937
  result.warnings.push(`资产缺失:${item.src}(跳过 ${item.dst})`);
14319
14938
  continue;
@@ -14326,7 +14945,7 @@ async function runAdrInit(opts = {}) {
14326
14945
  const bakRel = `${item.dst}.bak.${ts}`;
14327
14946
  if (!dryRun) {
14328
14947
  try {
14329
- await fsp.copyFile(dstAbs, path18.join(cwd, bakRel));
14948
+ await fsp.copyFile(dstAbs, path19.join(cwd, bakRel));
14330
14949
  } catch (e) {
14331
14950
  result.ok = false;
14332
14951
  result.reason = "io_error";
@@ -14338,7 +14957,7 @@ async function runAdrInit(opts = {}) {
14338
14957
  }
14339
14958
  if (!dryRun) {
14340
14959
  try {
14341
- await fsp.mkdir(path18.dirname(dstAbs), { recursive: true });
14960
+ await fsp.mkdir(path19.dirname(dstAbs), { recursive: true });
14342
14961
  await fsp.copyFile(srcAbs, dstAbs);
14343
14962
  if (item.chmod !== undefined) {
14344
14963
  try {
@@ -14368,7 +14987,7 @@ async function runAdrInit(opts = {}) {
14368
14987
  } else {
14369
14988
  result.suggestions.push("[dry-run] 将运行:git config core.hooksPath .githooks");
14370
14989
  }
14371
- const pkgPath = path18.join(cwd, "package.json");
14990
+ const pkgPath = path19.join(cwd, "package.json");
14372
14991
  const isNpm = existsSync4(pkgPath);
14373
14992
  if (isNpm) {
14374
14993
  if (writePrepare) {
@@ -14383,7 +15002,7 @@ async function runAdrInit(opts = {}) {
14383
15002
  const bakRel = `package.json.bak.${ts}`;
14384
15003
  if (!dryRun) {
14385
15004
  try {
14386
- await fsp.copyFile(pkgPath, path18.join(cwd, bakRel));
15005
+ await fsp.copyFile(pkgPath, path19.join(cwd, bakRel));
14387
15006
  } catch (e) {
14388
15007
  result.warnings.push(`备份 package.json 失败:${e instanceof Error ? e.message : String(e)}`);
14389
15008
  }
@@ -14506,12 +15125,12 @@ async function execute15(input) {
14506
15125
  import { z as z16 } from "zod";
14507
15126
 
14508
15127
  // lib/opencode-session-probe.ts
14509
- import * as path19 from "node:path";
15128
+ import * as path20 from "node:path";
14510
15129
  import * as os5 from "node:os";
14511
15130
  import { createRequire as createRequire2 } from "node:module";
14512
15131
  var requireFromHere = createRequire2(import.meta.url);
14513
15132
  var DEFAULT_LIVENESS_MS = 6 * 60 * 60000;
14514
- var DEFAULT_DB_PATH = path19.join(os5.homedir(), ".local/share/opencode/opencode.db");
15133
+ var DEFAULT_DB_PATH = path20.join(os5.homedir(), ".local/share/opencode/opencode.db");
14515
15134
  function createSessionProbe(opts = {}) {
14516
15135
  const dbPath = opts.dbPath ?? DEFAULT_DB_PATH;
14517
15136
  const httpBaseUrl = opts.httpBaseUrl ?? process.env["OPENCODE_SERVER_URL"];
@@ -14611,11 +15230,13 @@ var description16 = [
14611
15230
  "**模式**:",
14612
15231
  "- 默认(保守):沿用自动 GC 的时间护栏(6h/72h),只清确凿僵尸",
14613
15232
  "- force=true(高风险):时间护栏归零,连 probe 判 unknown 的也立即清,可能误删活跃 session 的 worktree",
15233
+ "- dryRun=true:不实际清理,先返回当前 worktree 健康摘要(让用户在执行前看到影响)",
14614
15234
  "**何时不需要**:自动 30min interval 正常跑时无需手动调。"
14615
15235
  ].join(`
14616
15236
  `);
14617
15237
  var ArgsSchema16 = z16.object({
14618
- force: z16.boolean().optional().describe("高风险:时间护栏归零,连 probe unknown 的 worktree 也立即清,可能误删活跃 session worktree。默认 false(保守)")
15238
+ force: z16.boolean().optional().describe("高风险:时间护栏归零,连 probe unknown 的 worktree 也立即清,可能误删活跃 session worktree。默认 false(保守)"),
15239
+ dryRun: z16.boolean().optional().describe("true=只预览不清理:返回当前 worktree 健康摘要(健康度信号 + 下一步提示),让用户执行 gc 前看到影响")
14619
15240
  });
14620
15241
  var _ctx3 = {};
14621
15242
  function __setContext3(ctx) {
@@ -14633,6 +15254,15 @@ async function execute16(input) {
14633
15254
  };
14634
15255
  }
14635
15256
  const force = parsed.data.force === true;
15257
+ const dryRun = parsed.data.dryRun === true;
15258
+ if (dryRun) {
15259
+ try {
15260
+ const health = await collectHealthDigest(getMainRoot2());
15261
+ return { ok: true, mode: "dry-run", health };
15262
+ } catch (err) {
15263
+ return { ok: false, error: err instanceof Error ? err.message : String(err) };
15264
+ }
15265
+ }
14636
15266
  const probe = createSessionProbe();
14637
15267
  try {
14638
15268
  const result = await pruneOrphanWorktrees(getMainRoot2(), {
@@ -14856,7 +15486,7 @@ async function raceAbortTimeout(p, signal, timeoutMs, label) {
14856
15486
  e.name = "AbortError";
14857
15487
  throw e;
14858
15488
  }
14859
- return await new Promise((resolve15, reject) => {
15489
+ return await new Promise((resolve16, reject) => {
14860
15490
  let settled = false;
14861
15491
  const timer = setTimeout(() => {
14862
15492
  if (settled)
@@ -14881,7 +15511,7 @@ async function raceAbortTimeout(p, signal, timeoutMs, label) {
14881
15511
  settled = true;
14882
15512
  clearTimeout(timer);
14883
15513
  signal?.removeEventListener("abort", onAbort);
14884
- resolve15(v);
15514
+ resolve16(v);
14885
15515
  }, (e) => {
14886
15516
  if (settled)
14887
15517
  return;
@@ -15716,7 +16346,7 @@ var handler7 = codeforgeToolsServer;
15716
16346
 
15717
16347
  // plugins/discover-spec-suggest.ts
15718
16348
  import { readFileSync as readFileSync3, readdirSync, statSync as statSync2 } from "node:fs";
15719
- import { join as join17 } from "node:path";
16349
+ import { join as join18 } from "node:path";
15720
16350
 
15721
16351
  // lib/handoff-schema.ts
15722
16352
  import { z as z18 } from "zod";
@@ -15883,9 +16513,9 @@ function validateHandoff(rawYaml, fileSize) {
15883
16513
  const result = HandoffSchema.safeParse(parsed);
15884
16514
  if (!result.success) {
15885
16515
  const first = result.error.issues[0];
15886
- const path20 = first?.path?.join(".") ?? "(root)";
16516
+ const path21 = first?.path?.join(".") ?? "(root)";
15887
16517
  const msg = first?.message ?? "unknown";
15888
- return { ok: false, reason: `schema 校验失败:${path20}: ${msg}` };
16518
+ return { ok: false, reason: `schema 校验失败:${path21}: ${msg}` };
15889
16519
  }
15890
16520
  return { ok: true, data: result.data, schemaVersion: result.data.schema_version };
15891
16521
  }
@@ -15902,7 +16532,7 @@ var SESSION_TTL_MS2 = 24 * 60 * 60 * 1000;
15902
16532
  var MATCH_THRESHOLD = 0.15;
15903
16533
  var MAX_CANDIDATES = 3;
15904
16534
  var NUDGE_MAX_LEN = 1500;
15905
- var SPECS_REL_DIR = join17("docs", "specs");
16535
+ var SPECS_REL_DIR = join18("docs", "specs");
15906
16536
  var sessionMap = new Map;
15907
16537
  function pruneIfOversize2() {
15908
16538
  while (sessionMap.size > SESSION_CAP2) {
@@ -16009,7 +16639,7 @@ function loadSpecs(rootDir, opts = {}) {
16009
16639
  const dirExists = opts.dirExists ?? defaultDirExists;
16010
16640
  const statReader = opts.statReader ?? defaultStatReader;
16011
16641
  const log6 = makePluginLogger(PLUGIN_NAME8);
16012
- const specsRoot = join17(rootDir, SPECS_REL_DIR);
16642
+ const specsRoot = join18(rootDir, SPECS_REL_DIR);
16013
16643
  const records = [];
16014
16644
  if (!dirExists(specsRoot)) {
16015
16645
  log6.info(`specs 目录不存在,plugin 将 no-op`, { specsRoot });
@@ -16030,7 +16660,7 @@ function loadSpecs(rootDir, opts = {}) {
16030
16660
  log6.info(`跳过非合法 slug 命名的条目`, { entry });
16031
16661
  continue;
16032
16662
  }
16033
- const specDir = join17(specsRoot, entry);
16663
+ const specDir = join18(specsRoot, entry);
16034
16664
  let dirStat;
16035
16665
  try {
16036
16666
  dirStat = statReader(specDir);
@@ -16043,7 +16673,7 @@ function loadSpecs(rootDir, opts = {}) {
16043
16673
  }
16044
16674
  if (!dirStat.isDirectory)
16045
16675
  continue;
16046
- const handoffPath = join17(specDir, "handoff.yaml");
16676
+ const handoffPath = join18(specDir, "handoff.yaml");
16047
16677
  let fileStat;
16048
16678
  try {
16049
16679
  fileStat = statReader(handoffPath);
@@ -16215,14 +16845,14 @@ var discoverSpecSuggestServer = async (ctx) => {
16215
16845
  var handler8 = discoverSpecSuggestServer;
16216
16846
 
16217
16847
  // lib/memories.ts
16218
- import { promises as fs15 } from "node:fs";
16219
- import * as path20 from "node:path";
16848
+ import { promises as fs16 } from "node:fs";
16849
+ import * as path21 from "node:path";
16220
16850
  import * as os6 from "node:os";
16221
16851
  function resolveConfig(c) {
16222
16852
  return {
16223
16853
  projectRoot: c.projectRoot,
16224
16854
  homeDir: c.homeDir ?? os6.homedir(),
16225
- projectName: c.projectName ?? path20.basename(c.projectRoot),
16855
+ projectName: c.projectName ?? path21.basename(c.projectRoot),
16226
16856
  now: c.now ?? Date.now,
16227
16857
  log: c.log ?? (() => {}),
16228
16858
  maxPerScope: c.maxPerScope ?? 1000
@@ -16230,13 +16860,13 @@ function resolveConfig(c) {
16230
16860
  }
16231
16861
  function fileFor(scope, cfg) {
16232
16862
  if (scope === "project") {
16233
- return path20.join(cfg.projectRoot, ".codeforge", "memories.json");
16863
+ return path21.join(cfg.projectRoot, ".codeforge", "memories.json");
16234
16864
  }
16235
- return path20.join(cfg.homeDir, ".codeforge", "memories.json");
16865
+ return path21.join(cfg.homeDir, ".codeforge", "memories.json");
16236
16866
  }
16237
16867
  async function readBank(p) {
16238
16868
  try {
16239
- const raw = await fs15.readFile(p, "utf8");
16869
+ const raw = await fs16.readFile(p, "utf8");
16240
16870
  const arr = JSON.parse(raw);
16241
16871
  if (!Array.isArray(arr))
16242
16872
  return [];
@@ -16246,10 +16876,10 @@ async function readBank(p) {
16246
16876
  }
16247
16877
  }
16248
16878
  async function writeBank(p, items) {
16249
- await fs15.mkdir(path20.dirname(p), { recursive: true });
16879
+ await fs16.mkdir(path21.dirname(p), { recursive: true });
16250
16880
  const tmp = `${p}.tmp`;
16251
- await fs15.writeFile(tmp, JSON.stringify(items, null, 2), "utf8");
16252
- await fs15.rename(tmp, p);
16881
+ await fs16.writeFile(tmp, JSON.stringify(items, null, 2), "utf8");
16882
+ await fs16.rename(tmp, p);
16253
16883
  }
16254
16884
  function isMemory(x) {
16255
16885
  if (!x || typeof x !== "object")
@@ -16767,7 +17397,7 @@ var handler10 = modelFallbackServer;
16767
17397
 
16768
17398
  // plugins/parallel-tool-nudge.ts
16769
17399
  import { readFileSync as readFileSync4, readdirSync as readdirSync2, statSync as statSync3 } from "node:fs";
16770
- import { join as join19 } from "node:path";
17400
+ import { join as join20 } from "node:path";
16771
17401
  import { homedir as homedir7 } from "node:os";
16772
17402
  var PLUGIN_NAME11 = "parallel-tool-nudge";
16773
17403
  logLifecycle(PLUGIN_NAME11, "import", {});
@@ -16820,10 +17450,10 @@ function loadAgentToolsMap(rootDir, opts = {}) {
16820
17450
  const reader = opts.reader ?? defaultReader2;
16821
17451
  const dirReader = opts.dirReader ?? defaultDirReader2;
16822
17452
  const dirExists = opts.dirExists ?? defaultDirExists2;
16823
- const homeAgentsDir = opts.homeAgentsDir ?? join19(homedir7(), ".config", "opencode", "agents");
17453
+ const homeAgentsDir = opts.homeAgentsDir ?? join20(homedir7(), ".config", "opencode", "agents");
16824
17454
  const candidateDirs = [
16825
- join19(rootDir, ".codeforge", "agents"),
16826
- join19(rootDir, "agents"),
17455
+ join20(rootDir, ".codeforge", "agents"),
17456
+ join20(rootDir, "agents"),
16827
17457
  homeAgentsDir
16828
17458
  ];
16829
17459
  const result = new Map;
@@ -16846,20 +17476,20 @@ function loadAgentToolsMap(rootDir, opts = {}) {
16846
17476
  for (const entry of entries) {
16847
17477
  if (!entry.endsWith(".md"))
16848
17478
  continue;
16849
- const path21 = join19(dir, entry);
17479
+ const path22 = join20(dir, entry);
16850
17480
  let content;
16851
17481
  try {
16852
- content = reader(path21);
17482
+ content = reader(path22);
16853
17483
  } catch (err) {
16854
17484
  log7.warn(`agent.md 读取失败(已跳过)`, {
16855
- path: path21,
17485
+ path: path22,
16856
17486
  error: err instanceof Error ? err.message : String(err)
16857
17487
  });
16858
17488
  continue;
16859
17489
  }
16860
17490
  const parsed = parseAgentFrontmatter(content);
16861
17491
  if (!parsed) {
16862
- log7.warn(`agent frontmatter 解析失败(已跳过)`, { path: path21 });
17492
+ log7.warn(`agent frontmatter 解析失败(已跳过)`, { path: path22 });
16863
17493
  continue;
16864
17494
  }
16865
17495
  if (result.has(parsed.name))
@@ -17050,18 +17680,18 @@ var handler12 = async (_ctx4) => {
17050
17680
  };
17051
17681
 
17052
17682
  // lib/event-stream.ts
17053
- import { promises as fs16 } from "node:fs";
17054
- import * as path21 from "node:path";
17683
+ import { promises as fs17 } from "node:fs";
17684
+ import * as path22 from "node:path";
17055
17685
  async function loadSession(id, opts = {}) {
17056
17686
  const file = resolveSessionFile(id, opts);
17057
- const raw = await fs16.readFile(file, "utf8");
17687
+ const raw = await fs17.readFile(file, "utf8");
17058
17688
  return parseJsonl(id, raw);
17059
17689
  }
17060
17690
  async function listSessions(opts = {}) {
17061
17691
  const dir = resolveDir(opts);
17062
17692
  let entries;
17063
17693
  try {
17064
- entries = await fs16.readdir(dir, { withFileTypes: true });
17694
+ entries = await fs17.readdir(dir, { withFileTypes: true });
17065
17695
  } catch (err) {
17066
17696
  if (err.code === "ENOENT")
17067
17697
  return [];
@@ -17071,10 +17701,10 @@ async function listSessions(opts = {}) {
17071
17701
  for (const e of entries) {
17072
17702
  if (!e.isFile() || !e.name.endsWith(".jsonl"))
17073
17703
  continue;
17074
- const file = path21.join(dir, e.name);
17704
+ const file = path22.join(dir, e.name);
17075
17705
  const id = e.name.replace(/\.jsonl$/, "");
17076
17706
  try {
17077
- const stat = await fs16.stat(file);
17707
+ const stat = await fs17.stat(file);
17078
17708
  const headerLine = await readFirstLine(file);
17079
17709
  let started_at = stat.birthtimeMs;
17080
17710
  if (headerLine) {
@@ -17098,11 +17728,11 @@ async function listSessions(opts = {}) {
17098
17728
  return out;
17099
17729
  }
17100
17730
  function resolveDir(opts = {}) {
17101
- const root = path21.resolve(opts.root ?? process.cwd());
17102
- return opts.sessions_dir ? path21.resolve(root, opts.sessions_dir) : path21.join(runtimeDir(root), "sessions");
17731
+ const root = path22.resolve(opts.root ?? process.cwd());
17732
+ return opts.sessions_dir ? path22.resolve(root, opts.sessions_dir) : path22.join(runtimeDir(root), "sessions");
17103
17733
  }
17104
17734
  function resolveSessionFile(id, opts = {}) {
17105
- return path21.join(resolveDir(opts), `${id}.jsonl`);
17735
+ return path22.join(resolveDir(opts), `${id}.jsonl`);
17106
17736
  }
17107
17737
  function parseJsonl(id, raw) {
17108
17738
  const events = [];
@@ -17137,7 +17767,7 @@ function isEvent(obj) {
17137
17767
  }
17138
17768
  async function readFirstLine(file) {
17139
17769
  const buf = Buffer.alloc(4096);
17140
- const fh = await fs16.open(file, "r");
17770
+ const fh = await fs17.open(file, "r");
17141
17771
  try {
17142
17772
  const { bytesRead } = await fh.read(buf, 0, buf.length, 0);
17143
17773
  const s = buf.subarray(0, bytesRead).toString("utf8");
@@ -17365,11 +17995,11 @@ function isRecoveryWorthShowing(plan) {
17365
17995
  }
17366
17996
 
17367
17997
  // lib/block-pending.ts
17368
- import { promises as fs17 } from "node:fs";
17369
- import * as path22 from "node:path";
17998
+ import { promises as fs18 } from "node:fs";
17999
+ import * as path23 from "node:path";
17370
18000
  function blockPendingFilePath(absRoot) {
17371
18001
  const rd = runtimeDir(absRoot, { ensure: false });
17372
- return path22.join(rd, "sessions", "autonomous-blocks.ndjson");
18002
+ return path23.join(rd, "sessions", "autonomous-blocks.ndjson");
17373
18003
  }
17374
18004
  function consumeLockPath(absRoot) {
17375
18005
  return blockPendingFilePath(absRoot) + ".consume.lock";
@@ -17378,7 +18008,7 @@ async function scanBlockPending(absRoot, filterSessionId) {
17378
18008
  const file = blockPendingFilePath(absRoot);
17379
18009
  let raw;
17380
18010
  try {
17381
- raw = await fs17.readFile(file, "utf8");
18011
+ raw = await fs18.readFile(file, "utf8");
17382
18012
  } catch {
17383
18013
  return [];
17384
18014
  }
@@ -17434,7 +18064,7 @@ async function markBlocksConsumed(absRoot, entries) {
17434
18064
  if (entries.length === 0)
17435
18065
  return;
17436
18066
  const file = blockPendingFilePath(absRoot);
17437
- await fs17.mkdir(path22.dirname(file), { recursive: true });
18067
+ await fs18.mkdir(path23.dirname(file), { recursive: true });
17438
18068
  const now = new Date().toISOString();
17439
18069
  const lines = entries.map((e) => ({
17440
18070
  type: "consume",
@@ -17445,7 +18075,7 @@ async function markBlocksConsumed(absRoot, entries) {
17445
18075
  `) + `
17446
18076
  `;
17447
18077
  await withFileLock(consumeLockPath(absRoot), async () => {
17448
- await fs17.appendFile(file, lines, "utf8");
18078
+ await fs18.appendFile(file, lines, "utf8");
17449
18079
  });
17450
18080
  }
17451
18081
 
@@ -17584,7 +18214,7 @@ var handler13 = sessionRecoveryServer;
17584
18214
 
17585
18215
  // plugins/subtask-heartbeat.ts
17586
18216
  import { promises as fsPromises } from "node:fs";
17587
- import * as path23 from "node:path";
18217
+ import * as path24 from "node:path";
17588
18218
  var recordSessionParent2 = recordSessionParent;
17589
18219
  var lookupParentSessionId2 = lookupParentSessionId;
17590
18220
  var deleteSessionParent2 = deleteSessionParent;
@@ -17925,7 +18555,7 @@ function buildErrorLogLine(errorReason, elapsedMs, now = Date.now()) {
17925
18555
  }
17926
18556
  async function appendSubagentLog(filePath, line, log9) {
17927
18557
  try {
17928
- await fsPromises.mkdir(path23.dirname(filePath), { recursive: true });
18558
+ await fsPromises.mkdir(path24.dirname(filePath), { recursive: true });
17929
18559
  await fsPromises.appendFile(filePath, line + `
17930
18560
  `, "utf8");
17931
18561
  } catch (err) {
@@ -18227,8 +18857,8 @@ var subtaskHeartbeatServer = async (ctx) => {
18227
18857
  var handler14 = subtaskHeartbeatServer;
18228
18858
 
18229
18859
  // plugins/subtasks.ts
18230
- import { promises as fs18 } from "node:fs";
18231
- import * as path24 from "node:path";
18860
+ import { promises as fs19 } from "node:fs";
18861
+ import * as path25 from "node:path";
18232
18862
 
18233
18863
  // lib/parallel-merge.ts
18234
18864
  init_worktree_ops();
@@ -18236,7 +18866,7 @@ init_worktree_ops();
18236
18866
  // plugins/subtasks.ts
18237
18867
  var PLUGIN_NAME15 = "subtasks";
18238
18868
  function getLogFile(root = process.cwd()) {
18239
- return path24.join(runtimeDir(root), "logs", "subtasks.log");
18869
+ return path25.join(runtimeDir(root), "logs", "subtasks.log");
18240
18870
  }
18241
18871
  async function writeLog(level, msg, data) {
18242
18872
  const line = JSON.stringify({
@@ -18249,8 +18879,8 @@ async function writeLog(level, msg, data) {
18249
18879
  `;
18250
18880
  try {
18251
18881
  const logFile = getLogFile();
18252
- await fs18.mkdir(path24.dirname(logFile), { recursive: true });
18253
- await fs18.appendFile(logFile, line, "utf8");
18882
+ await fs19.mkdir(path25.dirname(logFile), { recursive: true });
18883
+ await fs19.appendFile(logFile, line, "utf8");
18254
18884
  } catch {}
18255
18885
  }
18256
18886
  logLifecycle(PLUGIN_NAME15, "import");
@@ -18824,8 +19454,8 @@ var tokenManagerServer = async (ctx) => {
18824
19454
  var handler17 = tokenManagerServer;
18825
19455
 
18826
19456
  // plugins/tool-policy.ts
18827
- import { promises as fs19 } from "node:fs";
18828
- import * as path26 from "node:path";
19457
+ import { promises as fs20 } from "node:fs";
19458
+ import * as path27 from "node:path";
18829
19459
 
18830
19460
  // lib/tool-risk.ts
18831
19461
  var RISK_PATTERNS = [
@@ -18979,7 +19609,7 @@ function buildHaystackFor(args, matchOn) {
18979
19609
  }
18980
19610
 
18981
19611
  // lib/file-regex-acl.ts
18982
- import * as path25 from "node:path";
19612
+ import * as path26 from "node:path";
18983
19613
  function compileRule(r) {
18984
19614
  if (r instanceof RegExp)
18985
19615
  return r;
@@ -19045,7 +19675,7 @@ function normalizePath(p) {
19045
19675
  let s = p.replace(/\\/g, "/");
19046
19676
  if (s.startsWith("./"))
19047
19677
  s = s.slice(2);
19048
- s = path25.posix.normalize(s);
19678
+ s = path26.posix.normalize(s);
19049
19679
  return s;
19050
19680
  }
19051
19681
  function checkFileAccess(acl, file, op) {
@@ -19148,11 +19778,11 @@ function decideToolCall(ctx, cfg = {}, currentAgent) {
19148
19778
  const action = risks.length > 0 || worstAcl === "deny" ? "deny" : "allow";
19149
19779
  return { action, reasons, risks, acl: aclResults };
19150
19780
  }
19151
- var POLICY_PATH = path26.join(".codeforge", "policy.json");
19781
+ var POLICY_PATH = path27.join(".codeforge", "policy.json");
19152
19782
  async function loadPolicy(root = process.cwd()) {
19153
- const file = path26.join(root, POLICY_PATH);
19783
+ const file = path27.join(root, POLICY_PATH);
19154
19784
  try {
19155
- const raw = await fs19.readFile(file, "utf8");
19785
+ const raw = await fs20.readFile(file, "utf8");
19156
19786
  const data = JSON.parse(raw);
19157
19787
  return data;
19158
19788
  } catch {
@@ -19250,19 +19880,19 @@ var handler18 = toolPolicyServer;
19250
19880
  // plugins/update-checker.ts
19251
19881
  import { existsSync as existsSync5, mkdirSync as mkdirSync3, readFileSync as readFileSync6, rmSync, writeFileSync as writeFileSync2 } from "node:fs";
19252
19882
  import { homedir as homedir8 } from "node:os";
19253
- import { dirname as dirname15, join as join25 } from "node:path";
19883
+ import { dirname as dirname16, join as join26 } from "node:path";
19254
19884
  import { spawn } from "node:child_process";
19255
19885
 
19256
19886
  // lib/update-checker-impl.ts
19257
19887
  import { readFileSync as readFileSync5 } from "node:fs";
19258
- import { dirname as dirname14, join as join24 } from "node:path";
19888
+ import { dirname as dirname15, join as join25 } from "node:path";
19259
19889
  import { fileURLToPath as fileURLToPath2 } from "node:url";
19260
19890
  import * as https from "node:https";
19261
19891
 
19262
19892
  // lib/version-injected.ts
19263
19893
  function getInjectedVersion() {
19264
19894
  try {
19265
- const v = "0.8.2";
19895
+ const v = "0.8.4";
19266
19896
  if (typeof v === "string" && /^\d+\.\d+\.\d+/.test(v)) {
19267
19897
  return v;
19268
19898
  }
@@ -19309,8 +19939,8 @@ function readLocalVersion() {
19309
19939
  return injected;
19310
19940
  try {
19311
19941
  const here = fileURLToPath2(import.meta.url);
19312
- const root = dirname14(dirname14(here));
19313
- const pkg = JSON.parse(readFileSync5(join24(root, "package.json"), "utf8"));
19942
+ const root = dirname15(dirname15(here));
19943
+ const pkg = JSON.parse(readFileSync5(join25(root, "package.json"), "utf8"));
19314
19944
  return typeof pkg.version === "string" ? pkg.version : "0.0.0";
19315
19945
  } catch {
19316
19946
  return "0.0.0";
@@ -19346,7 +19976,7 @@ async function fetchLatestFromNpm(opts) {
19346
19976
  return { version, tarballUrl, integrity };
19347
19977
  }
19348
19978
  function defaultHttpFetcher(url2, timeoutMs) {
19349
- return new Promise((resolve16, reject) => {
19979
+ return new Promise((resolve17, reject) => {
19350
19980
  const u = new URL(url2);
19351
19981
  const headers = {
19352
19982
  "User-Agent": "codeforge-update-checker",
@@ -19363,7 +19993,7 @@ function defaultHttpFetcher(url2, timeoutMs) {
19363
19993
  const status = res.statusCode ?? 0;
19364
19994
  if (status === 404) {
19365
19995
  res.resume();
19366
- resolve16(null);
19996
+ resolve17(null);
19367
19997
  return;
19368
19998
  }
19369
19999
  if (status >= 400) {
@@ -19374,7 +20004,7 @@ function defaultHttpFetcher(url2, timeoutMs) {
19374
20004
  let body = "";
19375
20005
  res.setEncoding("utf8");
19376
20006
  res.on("data", (chunk) => body += chunk);
19377
- res.on("end", () => resolve16(body));
20007
+ res.on("end", () => resolve17(body));
19378
20008
  });
19379
20009
  req.on("timeout", () => {
19380
20010
  req.destroy();
@@ -19390,7 +20020,7 @@ var PLUGIN_NAME19 = "update-checker";
19390
20020
  var PLUGIN_VERSION = "3.0.0";
19391
20021
  var _updateCheckStarted = false;
19392
20022
  function getCacheFile() {
19393
- return join25(process.env["CODEFORGE_CACHE_DIR"] ?? join25(homedir8(), ".cache", "codeforge"), "update-check.json");
20023
+ return join26(process.env["CODEFORGE_CACHE_DIR"] ?? join26(homedir8(), ".cache", "codeforge"), "update-check.json");
19394
20024
  }
19395
20025
  function readLastInstalledVersion() {
19396
20026
  try {
@@ -19406,12 +20036,12 @@ function readLastInstalledVersion() {
19406
20036
  function writeLastInstalledVersion(v) {
19407
20037
  try {
19408
20038
  const f = getCacheFile();
19409
- mkdirSync3(dirname15(f), { recursive: true });
20039
+ mkdirSync3(dirname16(f), { recursive: true });
19410
20040
  writeFileSync2(f, JSON.stringify({ installedVersion: v }, null, 2), "utf8");
19411
20041
  } catch {}
19412
20042
  }
19413
20043
  function spawnAsync(cmd, args, opts = {}) {
19414
- return new Promise((resolve16) => {
20044
+ return new Promise((resolve17) => {
19415
20045
  const chunks = [];
19416
20046
  const errChunks = [];
19417
20047
  const proc = spawn(cmd, args, {
@@ -19425,13 +20055,13 @@ function spawnAsync(cmd, args, opts = {}) {
19425
20055
  if (opts.timeout) {
19426
20056
  timer = setTimeout(() => {
19427
20057
  proc.kill();
19428
- resolve16({ status: -1, stdout: "", stderr: "timeout" });
20058
+ resolve17({ status: -1, stdout: "", stderr: "timeout" });
19429
20059
  }, opts.timeout);
19430
20060
  }
19431
20061
  proc.on("close", (code) => {
19432
20062
  if (timer)
19433
20063
  clearTimeout(timer);
19434
- resolve16({
20064
+ resolve17({
19435
20065
  status: code ?? -1,
19436
20066
  stdout: Buffer.concat(chunks).toString("utf8"),
19437
20067
  stderr: Buffer.concat(errChunks).toString("utf8")
@@ -19440,7 +20070,7 @@ function spawnAsync(cmd, args, opts = {}) {
19440
20070
  proc.on("error", () => {
19441
20071
  if (timer)
19442
20072
  clearTimeout(timer);
19443
- resolve16({ status: -1, stdout: "", stderr: "spawn error" });
20073
+ resolve17({ status: -1, stdout: "", stderr: "spawn error" });
19444
20074
  });
19445
20075
  });
19446
20076
  }
@@ -19571,17 +20201,18 @@ var updateCheckerServer = async (ctx) => {
19571
20201
  }
19572
20202
  safeWriteLog(PLUGIN_NAME19, { level: "info", msg: "npm_install_success", remote });
19573
20203
  try {
19574
- const opencodeCache = join25(homedir8(), ".cache", "opencode", "packages", "@andyqiu", "codeforge@latest");
20204
+ const cacheRoot = process.env["XDG_CACHE_HOME"] ?? join26(homedir8(), ".cache");
20205
+ const opencodeCache = join26(cacheRoot, "opencode", "packages", "@andyqiu", "codeforge@latest");
19575
20206
  if (existsSync5(opencodeCache)) {
19576
20207
  rmSync(opencodeCache, { recursive: true, force: true });
19577
- safeWriteLog(PLUGIN_NAME19, { level: "info", msg: "opencode_cache_cleared", path: opencodeCache });
20208
+ safeWriteLog(PLUGIN_NAME19, { level: "info", msg: "opencode_plugin_cache_cleared", path: opencodeCache });
19578
20209
  }
19579
20210
  } catch (e) {
19580
- safeWriteLog(PLUGIN_NAME19, { level: "warn", msg: "opencode_cache_clear_failed", error: e.message });
20211
+ safeWriteLog(PLUGIN_NAME19, { level: "warn", msg: "opencode_plugin_cache_clear_failed", error: e.message });
19581
20212
  }
19582
20213
  const npmRoot = await getNpmGlobalRoot(npmBin);
19583
- const pkgRoot = npmRoot ? join25(npmRoot, "@andyqiu", "codeforge") : null;
19584
- const installMjs = pkgRoot ? join25(pkgRoot, "install.mjs") : null;
20214
+ const pkgRoot = npmRoot ? join26(npmRoot, "@andyqiu", "codeforge") : null;
20215
+ const installMjs = pkgRoot ? join26(pkgRoot, "install.mjs") : null;
19585
20216
  if (!installMjs || !existsSync5(installMjs)) {
19586
20217
  safeWriteLog(PLUGIN_NAME19, { level: "warn", msg: "install_mjs_not_found", path: installMjs ?? "null" });
19587
20218
  await postToast(ctx, `[codeforge] ⚠ npm 包已升级 ${local} → ${remote},但资产部署未完成。下次启动将重试,或手动运行:codeforge upgrade`);
@@ -19598,26 +20229,31 @@ var updateCheckerServer = async (ctx) => {
19598
20229
  }
19599
20230
  writeLastInstalledVersion(remote);
19600
20231
  safeWriteLog(PLUGIN_NAME19, { level: "info", msg: "auto_install_full_success", local, remote, nodeBin, npmBin });
19601
- await postToast(ctx, `[codeforge] 已升级 ${local} → ${remote}(重启 opencode 生效)
19602
- 回滚:npm install -g ${u.package}@${local}`);
20232
+ await postToast(ctx, `[Codeforge] 已升级 ${remote},重启 opencode 生效`);
19603
20233
  });
19604
20234
  });
19605
20235
  });
19606
20236
  return {};
19607
20237
  };
19608
20238
  async function postToast(ctx, message) {
19609
- await safeAsync(PLUGIN_NAME19, "client.app.log", async () => {
19610
- await ctx.client.app.log({ body: { service: PLUGIN_NAME19, level: "info", message } });
20239
+ await safeAsync(PLUGIN_NAME19, "tui.showToast", async () => {
20240
+ const tui = ctx.client.tui;
20241
+ if (!tui || typeof tui.showToast !== "function") {
20242
+ safeWriteLog(PLUGIN_NAME19, { level: "info", msg: "tui_show_toast_unavailable_fallback", message });
20243
+ return;
20244
+ }
20245
+ await tui.showToast({ body: { message, variant: "info", duration: 8000 } });
20246
+ safeWriteLog(PLUGIN_NAME19, { level: "info", msg: "tui_show_toast_ok" });
19611
20247
  });
19612
20248
  }
19613
20249
  var handler19 = updateCheckerServer;
19614
20250
 
19615
20251
  // plugins/workflow-engine.ts
19616
- import * as path28 from "node:path";
20252
+ import * as path29 from "node:path";
19617
20253
 
19618
20254
  // lib/workflow-loader.ts
19619
- import { promises as fs20 } from "node:fs";
19620
- import * as path27 from "node:path";
20255
+ import { promises as fs21 } from "node:fs";
20256
+ import * as path28 from "node:path";
19621
20257
  import { z as z19 } from "zod";
19622
20258
  var ActionSchema = z19.object({
19623
20259
  tool: z19.string().min(1, "action.tool 不能为空"),
@@ -19703,7 +20339,7 @@ function parseWorkflowYaml(yaml, sourcePath = "<inline>") {
19703
20339
  async function loadWorkflowFromFile(filePath) {
19704
20340
  let txt;
19705
20341
  try {
19706
- txt = await fs20.readFile(filePath, "utf8");
20342
+ txt = await fs21.readFile(filePath, "utf8");
19707
20343
  } catch (err) {
19708
20344
  return {
19709
20345
  ok: false,
@@ -19718,7 +20354,7 @@ async function loadWorkflowsFromDir(dir) {
19718
20354
  const failed = [];
19719
20355
  let entries;
19720
20356
  try {
19721
- entries = await fs20.readdir(dir);
20357
+ entries = await fs21.readdir(dir);
19722
20358
  } catch (err) {
19723
20359
  const e = err;
19724
20360
  if (e.code === "ENOENT")
@@ -19730,7 +20366,7 @@ async function loadWorkflowsFromDir(dir) {
19730
20366
  continue;
19731
20367
  if (!/\.ya?ml$/i.test(name))
19732
20368
  continue;
19733
- const full = path27.join(dir, name);
20369
+ const full = path28.join(dir, name);
19734
20370
  const r = await loadWorkflowFromFile(full);
19735
20371
  if (r.ok)
19736
20372
  loaded.push(r);
@@ -20120,7 +20756,7 @@ async function handleCommandInvoked(raw, workflowsDir = "workflows") {
20120
20756
  }
20121
20757
  var workflowEngineServer = async (ctx) => {
20122
20758
  const directory = ctx.directory ?? process.cwd();
20123
- const workflowsDir = path28.join(directory, "workflows");
20759
+ const workflowsDir = path29.join(directory, "workflows");
20124
20760
  ensureRegistry(workflowsDir).catch((err) => fallbackLog2.warn(`[${PLUGIN_NAME20}] preload workflows failed`, {
20125
20761
  error: err instanceof Error ? err.message : String(err)
20126
20762
  }));
@@ -20164,7 +20800,7 @@ var workflowEngineServer = async (ctx) => {
20164
20800
  var handler20 = workflowEngineServer;
20165
20801
 
20166
20802
  // plugins/session-worktree-guard.ts
20167
- import path29 from "node:path";
20803
+ import path30 from "node:path";
20168
20804
  import { stat } from "node:fs/promises";
20169
20805
  var PLUGIN_NAME21 = "session-worktree-guard";
20170
20806
  logLifecycle(PLUGIN_NAME21, "import", {});
@@ -20302,28 +20938,28 @@ var MERGE_CALLER_WHITELIST = new Set([
20302
20938
  var FORCE_MERGE_CALLER_WHITELIST = new Set([
20303
20939
  "codeforge"
20304
20940
  ]);
20305
- var CODEFORGE_WORKTREE_DIR_NAME = path29.join(".git", "codeforge-worktrees");
20941
+ var CODEFORGE_WORKTREE_DIR_NAME = path30.join(".git", "codeforge-worktrees");
20306
20942
  function worktreesRoot(mainRoot) {
20307
- return path29.join(mainRoot, CODEFORGE_WORKTREE_DIR_NAME);
20943
+ return path30.join(mainRoot, CODEFORGE_WORKTREE_DIR_NAME);
20308
20944
  }
20309
20945
  function isInsideAnyWorktreeDir(absPath, mainRoot) {
20310
- if (!path29.isAbsolute(absPath))
20946
+ if (!path30.isAbsolute(absPath))
20311
20947
  return false;
20312
20948
  const root = worktreesRoot(mainRoot);
20313
20949
  if (absPath === root)
20314
20950
  return false;
20315
- const prefix = root.endsWith(path29.sep) ? root : root + path29.sep;
20951
+ const prefix = root.endsWith(path30.sep) ? root : root + path30.sep;
20316
20952
  return absPath.startsWith(prefix);
20317
20953
  }
20318
20954
  function worktreeRootOf(absPath, mainRoot) {
20319
20955
  const root = worktreesRoot(mainRoot);
20320
- const prefix = root.endsWith(path29.sep) ? root : root + path29.sep;
20956
+ const prefix = root.endsWith(path30.sep) ? root : root + path30.sep;
20321
20957
  if (!absPath.startsWith(prefix))
20322
20958
  return null;
20323
- const seg = absPath.slice(prefix.length).split(path29.sep)[0];
20959
+ const seg = absPath.slice(prefix.length).split(path30.sep)[0];
20324
20960
  if (!seg || seg === "..")
20325
20961
  return null;
20326
- return path29.join(root, seg);
20962
+ return path30.join(root, seg);
20327
20963
  }
20328
20964
  function stripPairedQuotes(raw) {
20329
20965
  if (raw.length >= 2) {
@@ -20354,7 +20990,7 @@ function resolveExplicitWorktreeTarget(argsObj, mainRoot) {
20354
20990
  for (const cand of candidates) {
20355
20991
  if (!cand)
20356
20992
  continue;
20357
- const abs = path29.resolve(mainRoot, cand);
20993
+ const abs = path30.resolve(mainRoot, cand);
20358
20994
  if (isInsideAnyWorktreeDir(abs, mainRoot)) {
20359
20995
  const wtRoot = worktreeRootOf(abs, mainRoot);
20360
20996
  if (wtRoot)
@@ -20367,14 +21003,14 @@ function resolveExplicitWorktreeTarget(argsObj, mainRoot) {
20367
21003
  }
20368
21004
  }
20369
21005
  function stripSharedGitDirRef(command, mainRoot) {
20370
- const escGitDir = escapeRegex(path29.join(mainRoot, ".git"));
21006
+ const escGitDir = escapeRegex(path30.join(mainRoot, ".git"));
20371
21007
  const re = new RegExp(`--git-dir(?:=|\\s+)['"]?${escGitDir}(?:/[^\\s'"\\x60)]*)?['"]?`, "g");
20372
21008
  return command.replace(re, " ");
20373
21009
  }
20374
21010
  function rewritePath(value, mainRoot, worktreeRoot) {
20375
21011
  if (!value)
20376
21012
  return null;
20377
- const resolved = path29.isAbsolute(value) ? value : path29.resolve(mainRoot, value);
21013
+ const resolved = path30.isAbsolute(value) ? value : path30.resolve(mainRoot, value);
20378
21014
  const wtPrefix2 = worktreeRoot.endsWith("/") ? worktreeRoot : worktreeRoot + "/";
20379
21015
  if (resolved === worktreeRoot || resolved.startsWith(wtPrefix2)) {
20380
21016
  return null;
@@ -20412,7 +21048,7 @@ function commandContainsMainRootExcludingWorktree(command, mainRoot, worktreePat
20412
21048
  }
20413
21049
  }
20414
21050
  const wtRoot = worktreesRoot(mainRoot);
20415
- const wtRootPrefix = wtRoot + path29.sep;
21051
+ const wtRootPrefix = wtRoot + path30.sep;
20416
21052
  const escapedWtRootPrefix = escapeRegex(wtRootPrefix);
20417
21053
  const wtPathPattern = escapedWtRootPrefix + `[^\\s'"\\x60)]*`;
20418
21054
  const allWorktreePathsReForEscape = new RegExp(wtPathPattern, "g");
@@ -20467,8 +21103,8 @@ function collectWritePaths(toolName, argsObj, worktreeRoot) {
20467
21103
  const candidate = toolName === "write" || toolName === "edit" ? argsObj["filePath"] : toolName === "ast_edit" ? argsObj["target"] : undefined;
20468
21104
  if (typeof candidate !== "string" || candidate.length === 0)
20469
21105
  return out;
20470
- const abs = path29.isAbsolute(candidate) ? candidate : path29.resolve(worktreeRoot, candidate);
20471
- const rel = path29.relative(worktreeRoot, abs).split(path29.sep).join("/");
21106
+ const abs = path30.isAbsolute(candidate) ? candidate : path30.resolve(worktreeRoot, candidate);
21107
+ const rel = path30.relative(worktreeRoot, abs).split(path30.sep).join("/");
20472
21108
  out.push(rel);
20473
21109
  return out;
20474
21110
  }
@@ -20479,7 +21115,7 @@ async function isCodeforgeManagedProject(mainRoot, opts = {}) {
20479
21115
  if (force === "1" || force === "true" || force === "yes")
20480
21116
  return true;
20481
21117
  try {
20482
- const st = await stat(path29.join(mainRoot, ".codeforge"));
21118
+ const st = await stat(path30.join(mainRoot, ".codeforge"));
20483
21119
  return st.isDirectory();
20484
21120
  } catch {
20485
21121
  return false;
@@ -20509,11 +21145,11 @@ var STALE_MERGE_HEAD_MS = 24 * 60 * 60000;
20509
21145
  var _staleMergeHeadWarned = false;
20510
21146
  var _mergeBypassToastShown = false;
20511
21147
  async function isMainRepoMidMerge(mainRoot, opts = {}) {
20512
- const gitDir = path29.join(mainRoot, ".git");
21148
+ const gitDir = path30.join(mainRoot, ".git");
20513
21149
  const markers = [
20514
- path29.join(gitDir, "MERGE_HEAD"),
20515
- path29.join(gitDir, "rebase-merge"),
20516
- path29.join(gitDir, "CHERRY_PICK_HEAD")
21150
+ path30.join(gitDir, "MERGE_HEAD"),
21151
+ path30.join(gitDir, "rebase-merge"),
21152
+ path30.join(gitDir, "CHERRY_PICK_HEAD")
20517
21153
  ];
20518
21154
  let freshest = -1;
20519
21155
  for (const m of markers) {
@@ -21172,6 +21808,10 @@ var worktreeLifecyclePlugin = async (ctx) => {
21172
21808
  if (rec.cleanedCreating.length > 0 || rec.finishedRemoving.length > 0 || rec.keptConservative.length > 0) {
21173
21809
  safeWriteLog(PLUGIN_NAME22, { hook: "activate.reconcile", ...rec });
21174
21810
  }
21811
+ const merc = await reconcileInterruptedMerges(mainRoot);
21812
+ if (merc.recoveredMerged.length > 0 || merc.rolledBack.length > 0 || merc.conflicts.length > 0 || merc.registryUnreadable) {
21813
+ safeWriteLog(PLUGIN_NAME22, { hook: "activate.reconcileMerge", ...merc });
21814
+ }
21175
21815
  const result = await pruneOrphanWorktrees(mainRoot, {
21176
21816
  isSessionAlive: _probe.isSessionAlive
21177
21817
  });
@@ -21203,6 +21843,10 @@ var worktreeLifecyclePlugin = async (ctx) => {
21203
21843
  if (rec.cleanedCreating.length > 0 || rec.finishedRemoving.length > 0 || rec.keptConservative.length > 0) {
21204
21844
  safeWriteLog(PLUGIN_NAME22, { hook: "interval.reconcile", ...rec });
21205
21845
  }
21846
+ const merc = await reconcileInterruptedMerges(mainRoot);
21847
+ if (merc.recoveredMerged.length > 0 || merc.rolledBack.length > 0 || merc.conflicts.length > 0 || merc.registryUnreadable) {
21848
+ safeWriteLog(PLUGIN_NAME22, { hook: "interval.reconcileMerge", ...merc });
21849
+ }
21206
21850
  const result = await pruneOrphanWorktrees(mainRoot, {
21207
21851
  isSessionAlive: _probe.isSessionAlive
21208
21852
  });
@@ -21476,6 +22120,7 @@ var src_default = pluginModule;
21476
22120
  export {
21477
22121
  src_default as default,
21478
22122
  createCodeforgeServer,
22123
+ collectHealthDigest,
21479
22124
  codeforgeServer,
21480
22125
  codeforgeDevServer
21481
22126
  };