@andyqiu/codeforge 0.7.10 → 0.8.0

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
@@ -10585,6 +10585,21 @@ class ApprovalStore {
10585
10585
  all.sort((a, b) => b.createdAt.localeCompare(a.createdAt));
10586
10586
  return all[0];
10587
10587
  }
10588
+ async findAliasApprovals(ownerSessionId) {
10589
+ if (!ownerSessionId)
10590
+ return [];
10591
+ const dirs = await this.safeReaddir(this.base);
10592
+ const out = [];
10593
+ for (const name of dirs) {
10594
+ if (!name.startsWith("session:"))
10595
+ continue;
10596
+ const latest = await this.getLatest(name);
10597
+ if (latest?.aliasOf === ownerSessionId)
10598
+ out.push(latest);
10599
+ }
10600
+ out.sort((a, b) => b.createdAt.localeCompare(a.createdAt));
10601
+ return out;
10602
+ }
10588
10603
  async recordEscape(meta) {
10589
10604
  const dir = path8.join(this.base, "escapes");
10590
10605
  await fs6.mkdir(dir, { recursive: true });
@@ -10664,8 +10679,8 @@ class ApprovalStore {
10664
10679
  // lib/session-worktree.ts
10665
10680
  init_worktree_ops();
10666
10681
  import { execFile as execFile3 } from "node:child_process";
10667
- import { promises as fs9 } from "node:fs";
10668
- import * as path11 from "node:path";
10682
+ import { promises as fs10 } from "node:fs";
10683
+ import * as path12 from "node:path";
10669
10684
 
10670
10685
  // lib/file-lock.ts
10671
10686
  import { promises as fs8 } from "node:fs";
@@ -10781,51 +10796,344 @@ function sleep(ms) {
10781
10796
  return new Promise((resolve9) => setTimeout(resolve9, ms));
10782
10797
  }
10783
10798
 
10799
+ // lib/parent-map-store.ts
10800
+ import { promises as fs9 } from "node:fs";
10801
+ import * as path11 from "node:path";
10802
+ var PARENT_MAP_VERSION = 1;
10803
+ var PARENT_MAP_LOCK_TIMEOUT_MS = 2000;
10804
+ var PARENT_MAP_MAX_ENTRIES = 256;
10805
+ function parentMapDir(mainRoot) {
10806
+ return path11.join(runtimeDir(path11.resolve(mainRoot), { ensure: false }), "session-worktrees");
10807
+ }
10808
+ function parentMapPath(mainRoot) {
10809
+ return path11.join(parentMapDir(mainRoot), "parent-map.json");
10810
+ }
10811
+ function parentMapLockPath(mainRoot) {
10812
+ return path11.join(parentMapDir(mainRoot), "parent-map.lock");
10813
+ }
10814
+ async function readParentMapFile(mainRoot) {
10815
+ const file = parentMapPath(mainRoot);
10816
+ try {
10817
+ const raw = await fs9.readFile(file, "utf8");
10818
+ const parsed = JSON.parse(raw);
10819
+ if (parsed.version !== PARENT_MAP_VERSION || !Array.isArray(parsed.entries)) {
10820
+ return { version: PARENT_MAP_VERSION, entries: [] };
10821
+ }
10822
+ return parsed;
10823
+ } catch (err) {
10824
+ const e = err;
10825
+ if (e.code === "ENOENT")
10826
+ return { version: PARENT_MAP_VERSION, entries: [] };
10827
+ return { version: PARENT_MAP_VERSION, entries: [] };
10828
+ }
10829
+ }
10830
+ async function writeParentMapFile(mainRoot, payload) {
10831
+ const file = parentMapPath(mainRoot);
10832
+ await fs9.mkdir(path11.dirname(file), { recursive: true });
10833
+ 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);
10836
+ }
10837
+ function capEntriesByTsDesc(entries) {
10838
+ if (entries.length <= PARENT_MAP_MAX_ENTRIES)
10839
+ return entries;
10840
+ return [...entries].sort((a, b) => b.ts - a.ts).slice(0, PARENT_MAP_MAX_ENTRIES);
10841
+ }
10842
+ async function loadParentMap(mainRoot) {
10843
+ const out = new Map;
10844
+ let file;
10845
+ try {
10846
+ file = await readParentMapFile(mainRoot);
10847
+ } catch {
10848
+ return out;
10849
+ }
10850
+ const valid = [];
10851
+ for (const e of file.entries) {
10852
+ if (!e || typeof e.childID !== "string" || typeof e.parentID !== "string")
10853
+ continue;
10854
+ if (!e.childID || !e.parentID)
10855
+ continue;
10856
+ const ts = typeof e.ts === "number" ? e.ts : Date.now();
10857
+ valid.push({ childID: e.childID, parentID: e.parentID, ts });
10858
+ }
10859
+ const capped = capEntriesByTsDesc(valid);
10860
+ for (const e of capped) {
10861
+ out.set(e.childID, { parentID: e.parentID, ts: e.ts });
10862
+ }
10863
+ return out;
10864
+ }
10865
+ async function mutateParentMap(mainRoot, mutator, opts = {}) {
10866
+ const lockPath = parentMapLockPath(mainRoot);
10867
+ await fs9.mkdir(path11.dirname(lockPath), { recursive: true });
10868
+ const lockOpts = {
10869
+ timeoutMs: opts.timeoutMs ?? PARENT_MAP_LOCK_TIMEOUT_MS,
10870
+ ...opts
10871
+ };
10872
+ await withFileLock(lockPath, async () => {
10873
+ const current = await readParentMapFile(mainRoot);
10874
+ const input = current.entries.slice();
10875
+ const next = await mutator(input);
10876
+ if (!Array.isArray(next))
10877
+ return;
10878
+ const capped = capEntriesByTsDesc(next);
10879
+ await writeParentMapFile(mainRoot, { version: PARENT_MAP_VERSION, entries: capped });
10880
+ }, lockOpts);
10881
+ }
10882
+ async function appendParentEntry(mainRoot, childID, parentID, ts, opts = {}) {
10883
+ if (!childID || !parentID)
10884
+ return;
10885
+ const lockPath = parentMapLockPath(mainRoot);
10886
+ await fs9.mkdir(path11.dirname(lockPath), { recursive: true });
10887
+ const lockOpts = {
10888
+ timeoutMs: opts.timeoutMs ?? PARENT_MAP_LOCK_TIMEOUT_MS,
10889
+ ...opts
10890
+ };
10891
+ await withFileLock(lockPath, async () => {
10892
+ const current = await readParentMapFile(mainRoot);
10893
+ const idx = current.entries.findIndex((e) => e.childID === childID);
10894
+ if (idx >= 0) {
10895
+ current.entries[idx] = { childID, parentID, ts };
10896
+ } else {
10897
+ current.entries.push({ childID, parentID, ts });
10898
+ }
10899
+ const capped = capEntriesByTsDesc(current.entries);
10900
+ await writeParentMapFile(mainRoot, { version: PARENT_MAP_VERSION, entries: capped });
10901
+ }, lockOpts);
10902
+ }
10903
+
10904
+ // lib/parent-map-runtime.ts
10905
+ var log4 = makePluginLogger("parent-map-runtime");
10906
+ var SESSION_PARENT_MAP_TTL_MS = 30 * 60000;
10907
+ var SESSION_PARENT_MAP_MAX_SIZE = PARENT_MAP_MAX_ENTRIES;
10908
+ var sessionParentMap = new Map;
10909
+ var _persistRoot = null;
10910
+ function _setPersistRootForTests(root) {
10911
+ _persistRoot = root;
10912
+ }
10913
+ function _bulkInjectSessionParentMap(entries) {
10914
+ for (const e of entries) {
10915
+ if (!e.childID || !e.parentID)
10916
+ continue;
10917
+ sessionParentMap.set(e.childID, { parentID: e.parentID, ts: e.ts });
10918
+ }
10919
+ }
10920
+ function _capSessionParentMap() {
10921
+ if (sessionParentMap.size <= SESSION_PARENT_MAP_MAX_SIZE)
10922
+ return 0;
10923
+ const sorted = [...sessionParentMap.entries()].sort((a, b) => b[1].ts - a[1].ts);
10924
+ const keep = new Set(sorted.slice(0, SESSION_PARENT_MAP_MAX_SIZE).map(([k]) => k));
10925
+ let removed = 0;
10926
+ for (const k of [...sessionParentMap.keys()]) {
10927
+ if (!keep.has(k)) {
10928
+ sessionParentMap.delete(k);
10929
+ removed++;
10930
+ }
10931
+ }
10932
+ return removed;
10933
+ }
10934
+ function recordSessionParent(childID, parentID, now = Date.now()) {
10935
+ if (!childID || !parentID)
10936
+ return;
10937
+ if (sessionParentMap.has(childID)) {
10938
+ sessionParentMap.set(childID, { parentID, ts: now });
10939
+ } else {
10940
+ if (sessionParentMap.size >= SESSION_PARENT_MAP_MAX_SIZE) {
10941
+ let oldestKey = null;
10942
+ let oldestTs = Number.POSITIVE_INFINITY;
10943
+ for (const [k, v] of sessionParentMap.entries()) {
10944
+ if (v.ts < oldestTs) {
10945
+ oldestTs = v.ts;
10946
+ oldestKey = k;
10947
+ }
10948
+ }
10949
+ if (oldestKey !== null)
10950
+ sessionParentMap.delete(oldestKey);
10951
+ }
10952
+ sessionParentMap.set(childID, { parentID, ts: now });
10953
+ }
10954
+ if (_persistRoot) {
10955
+ appendParentEntry(_persistRoot, childID, parentID, now).catch((err) => {
10956
+ log4.warn("appendParentEntry 失败(已隔离)", {
10957
+ error: err instanceof Error ? err.message : String(err),
10958
+ childID
10959
+ });
10960
+ });
10961
+ }
10962
+ }
10963
+ function lookupParentSessionId(childID, now = Date.now()) {
10964
+ const entry = sessionParentMap.get(childID);
10965
+ if (!entry)
10966
+ return;
10967
+ if (now - entry.ts > SESSION_PARENT_MAP_TTL_MS) {
10968
+ sessionParentMap.delete(childID);
10969
+ return;
10970
+ }
10971
+ return entry.parentID;
10972
+ }
10973
+ function deleteSessionParent(childID) {
10974
+ sessionParentMap.delete(childID);
10975
+ if (_persistRoot) {
10976
+ mutateParentMap(_persistRoot, (entries) => entries.filter((e) => e.childID !== childID)).catch((err) => {
10977
+ log4.warn("mutateParentMap (delete) 失败(已隔离)", {
10978
+ error: err instanceof Error ? err.message : String(err),
10979
+ childID
10980
+ });
10981
+ });
10982
+ }
10983
+ }
10984
+ function sweepExpiredSessionParents(now = Date.now()) {
10985
+ let removed = 0;
10986
+ for (const [k, v] of [...sessionParentMap.entries()]) {
10987
+ if (now - v.ts > SESSION_PARENT_MAP_TTL_MS) {
10988
+ sessionParentMap.delete(k);
10989
+ removed++;
10990
+ }
10991
+ }
10992
+ if (_persistRoot) {
10993
+ mutateParentMap(_persistRoot, (entries) => entries.filter((e) => now - e.ts <= SESSION_PARENT_MAP_TTL_MS)).catch((err) => {
10994
+ log4.warn("mutateParentMap (sweep) 失败(已隔离)", {
10995
+ error: err instanceof Error ? err.message : String(err),
10996
+ removed
10997
+ });
10998
+ });
10999
+ }
11000
+ return removed;
11001
+ }
11002
+
10784
11003
  // lib/session-worktree.ts
10785
11004
  var REGISTRY_VERSION = 1;
10786
- var DEFAULT_WORKTREE_SUBDIR = path11.join(".git", "codeforge-worktrees");
11005
+ var DEFAULT_WORKTREE_SUBDIR = path12.join(".git", "codeforge-worktrees");
10787
11006
  function debugLog(msg) {
10788
11007
  if (process.env["CODEFORGE_DEBUG"]) {
10789
11008
  console.debug(`[session-worktree] ${msg}`);
10790
11009
  }
10791
11010
  }
10792
11011
  function registryDir(mainRoot) {
10793
- return path11.join(runtimeDir(path11.resolve(mainRoot), { ensure: false }), "session-worktrees");
11012
+ return path12.join(runtimeDir(path12.resolve(mainRoot), { ensure: false }), "session-worktrees");
10794
11013
  }
10795
11014
  function registryPath(mainRoot) {
10796
- return path11.join(registryDir(mainRoot), "registry.json");
11015
+ return path12.join(registryDir(mainRoot), "registry.json");
10797
11016
  }
10798
11017
  function registryLockPath(mainRoot) {
10799
- return path11.join(registryDir(mainRoot), "registry.lock");
11018
+ return path12.join(registryDir(mainRoot), "registry.lock");
10800
11019
  }
10801
- async function readRegistry(mainRoot) {
11020
+
11021
+ class RegistryCorruptError extends Error {
11022
+ registryPath;
11023
+ backupPath;
11024
+ constructor(reason, registryPath2, backupPath) {
11025
+ super(`session 追踪文件损坏(${reason}):${registryPath2}。` + `为防止用空表覆盖丢失 worktree 追踪记录,已拒绝写入(fail-closed)。` + (backupPath ? `已备份损坏文件到 ${backupPath}。` : "") + `请检查/备份后删除该文件让系统重建空 registry,或恢复正确内容后重试。`);
11026
+ this.name = "RegistryCorruptError";
11027
+ this.registryPath = registryPath2;
11028
+ if (backupPath)
11029
+ this.backupPath = backupPath;
11030
+ }
11031
+ }
11032
+
11033
+ class RegistryVersionTooNewError extends Error {
11034
+ registryPath;
11035
+ foundVersion;
11036
+ supportedVersion;
11037
+ constructor(registryPath2, foundVersion, supportedVersion) {
11038
+ super(`session 追踪文件版本(v${foundVersion})高于当前 CodeForge 支持的版本(v${supportedVersion}):` + `${registryPath2}。已启用只读保护拒绝写入(fail-closed,绝不降级覆盖有效数据)。` + `请升级 CodeForge 到匹配版本后再操作。`);
11039
+ this.name = "RegistryVersionTooNewError";
11040
+ this.registryPath = registryPath2;
11041
+ this.foundVersion = foundVersion;
11042
+ this.supportedVersion = supportedVersion;
11043
+ }
11044
+ }
11045
+ async function readRegistryResult(mainRoot) {
10802
11046
  const file = registryPath(mainRoot);
11047
+ let raw;
10803
11048
  try {
10804
- const raw = await fs9.readFile(file, "utf8");
10805
- const parsed = JSON.parse(raw);
10806
- if (parsed.version !== REGISTRY_VERSION || !Array.isArray(parsed.entries)) {
10807
- return { version: REGISTRY_VERSION, entries: [] };
10808
- }
10809
- return parsed;
11049
+ raw = await fs10.readFile(file, "utf8");
10810
11050
  } catch (err) {
10811
11051
  const e = err;
10812
11052
  if (e.code === "ENOENT")
10813
- return { version: REGISTRY_VERSION, entries: [] };
10814
- return { version: REGISTRY_VERSION, entries: [] };
11053
+ return { kind: "missing" };
11054
+ return { kind: "corrupt", reason: `read_error: ${e.code ?? e.message}`, path: file };
11055
+ }
11056
+ let parsed;
11057
+ try {
11058
+ parsed = JSON.parse(raw);
11059
+ } catch (err) {
11060
+ return { kind: "corrupt", reason: `parse_error: ${err.message}`, path: file };
11061
+ }
11062
+ if (typeof parsed !== "object" || parsed === null) {
11063
+ return { kind: "corrupt", reason: "structure: 顶层非对象", path: file };
10815
11064
  }
11065
+ const obj = parsed;
11066
+ if (typeof obj.version !== "number" || !Number.isInteger(obj.version) || obj.version < 1) {
11067
+ return {
11068
+ kind: "corrupt",
11069
+ reason: `structure: version 非正整数 (${String(obj.version)})`,
11070
+ path: file
11071
+ };
11072
+ }
11073
+ if (obj.version > REGISTRY_VERSION) {
11074
+ return {
11075
+ kind: "version_too_new",
11076
+ foundVersion: obj.version,
11077
+ supportedVersion: REGISTRY_VERSION,
11078
+ path: file
11079
+ };
11080
+ }
11081
+ if (!Array.isArray(obj.entries)) {
11082
+ return { kind: "corrupt", reason: "structure: entries 非数组", path: file };
11083
+ }
11084
+ return {
11085
+ kind: "ok",
11086
+ registry: { version: REGISTRY_VERSION, entries: obj.entries }
11087
+ };
11088
+ }
11089
+ async function readRegistry(mainRoot) {
11090
+ const res = await readRegistryResult(mainRoot);
11091
+ if (res.kind === "ok")
11092
+ return res.registry;
11093
+ return { version: REGISTRY_VERSION, entries: [] };
10816
11094
  }
10817
11095
  async function writeRegistry(mainRoot, reg) {
10818
11096
  const file = registryPath(mainRoot);
10819
- await fs9.mkdir(path11.dirname(file), { recursive: true });
11097
+ await fs10.mkdir(path12.dirname(file), { recursive: true });
10820
11098
  const tmp = `${file}.tmp-${process.pid}-${Date.now()}`;
10821
- await fs9.writeFile(tmp, JSON.stringify(reg, null, 2), "utf8");
10822
- await fs9.rename(tmp, file);
11099
+ await fs10.writeFile(tmp, JSON.stringify(reg, null, 2), "utf8");
11100
+ await fs10.rename(tmp, file);
11101
+ }
11102
+ async function backupCorruptRegistry(mainRoot) {
11103
+ const file = registryPath(mainRoot);
11104
+ const dir = path12.dirname(file);
11105
+ const base = path12.basename(file);
11106
+ try {
11107
+ const names = await fs10.readdir(dir);
11108
+ const existing = names.find((n) => n.startsWith(`${base}.corrupt.`));
11109
+ if (existing)
11110
+ return path12.join(dir, existing);
11111
+ } catch {}
11112
+ const backup = `${file}.corrupt.${Date.now()}`;
11113
+ try {
11114
+ await fs10.copyFile(file, backup);
11115
+ return backup;
11116
+ } catch {
11117
+ return;
11118
+ }
11119
+ }
11120
+ async function loadRegistryForMutation(mainRoot) {
11121
+ const res = await readRegistryResult(mainRoot);
11122
+ if (res.kind === "ok")
11123
+ return res.registry;
11124
+ if (res.kind === "missing")
11125
+ return { version: REGISTRY_VERSION, entries: [] };
11126
+ if (res.kind === "version_too_new") {
11127
+ throw new RegistryVersionTooNewError(res.path, res.foundVersion, res.supportedVersion);
11128
+ }
11129
+ const backupPath = await backupCorruptRegistry(mainRoot);
11130
+ throw new RegistryCorruptError(res.reason, res.path, backupPath);
10823
11131
  }
10824
11132
  async function mutateRegistry(mainRoot, fn) {
10825
11133
  const lockPath = registryLockPath(mainRoot);
10826
- await fs9.mkdir(path11.dirname(lockPath), { recursive: true });
11134
+ await fs10.mkdir(path12.dirname(lockPath), { recursive: true });
10827
11135
  return await withFileLock(lockPath, async () => {
10828
- const reg = await readRegistry(mainRoot);
11136
+ const reg = await loadRegistryForMutation(mainRoot);
10829
11137
  const result = await fn(reg);
10830
11138
  await writeRegistry(mainRoot, reg);
10831
11139
  return result;
@@ -10835,13 +11143,13 @@ async function bindSessionWorktree(opts) {
10835
11143
  if (!opts.sessionId || opts.sessionId.trim() === "") {
10836
11144
  throw new Error("bindSessionWorktree: sessionId 不能为空");
10837
11145
  }
10838
- const mainRoot = path11.resolve(opts.mainRoot);
11146
+ const mainRoot = path12.resolve(opts.mainRoot);
10839
11147
  const branch = opts.branchName ?? `codeforge/session-${opts.sessionId}`;
10840
- const worktreesDir = opts.worktrees_dir ?? path11.join(mainRoot, DEFAULT_WORKTREE_SUBDIR);
11148
+ const worktreesDir = opts.worktrees_dir ?? path12.join(mainRoot, DEFAULT_WORKTREE_SUBDIR);
10841
11149
  const lockPath = registryLockPath(mainRoot);
10842
- await fs9.mkdir(path11.dirname(lockPath), { recursive: true });
11150
+ await fs10.mkdir(path12.dirname(lockPath), { recursive: true });
10843
11151
  return await withFileLock(lockPath, async () => {
10844
- const reg = await readRegistry(mainRoot);
11152
+ const reg = await loadRegistryForMutation(mainRoot);
10845
11153
  const existing = reg.entries.find((e) => e.sessionId === opts.sessionId);
10846
11154
  if (existing && existing.status === "active")
10847
11155
  return existing;
@@ -10887,6 +11195,48 @@ async function registryHasEntries(mainRoot) {
10887
11195
  return false;
10888
11196
  }
10889
11197
  }
11198
+ var OWNER_RESOLVE_MAX_DEPTH = 16;
11199
+ async function resolveWorktreeOwner(opts) {
11200
+ const sid = opts.sessionId;
11201
+ const mainRoot = path12.resolve(opts.mainRoot);
11202
+ const snap = await readRegistryResult(mainRoot);
11203
+ if (snap.kind === "corrupt" || snap.kind === "version_too_new") {
11204
+ return {
11205
+ ok: false,
11206
+ ownerSessionId: sid,
11207
+ entry: null,
11208
+ via: "registry-unreadable",
11209
+ reason: snap.kind === "corrupt" ? `registry corrupt: ${snap.reason}` : `registry version_too_new: v${snap.foundVersion} > 支持 v${snap.supportedVersion}`
11210
+ };
11211
+ }
11212
+ const entries = snap.kind === "ok" ? snap.registry.entries : [];
11213
+ const activeById = (id) => entries.find((e) => e.sessionId === id && e.status === "active") ?? null;
11214
+ if (opts.worktreePath) {
11215
+ const target = path12.resolve(opts.worktreePath);
11216
+ const byPath = entries.find((e) => e.status === "active" && path12.resolve(e.worktreePath) === target);
11217
+ if (byPath) {
11218
+ return { ok: true, ownerSessionId: byPath.sessionId, entry: byPath, via: "worktree-path" };
11219
+ }
11220
+ }
11221
+ const selfEntry = activeById(sid);
11222
+ if (selfEntry) {
11223
+ return { ok: true, ownerSessionId: sid, entry: selfEntry, via: "self" };
11224
+ }
11225
+ const visited = new Set([sid]);
11226
+ let cur = sid;
11227
+ for (let depth = 0;depth < OWNER_RESOLVE_MAX_DEPTH; depth++) {
11228
+ const parent = lookupParentSessionId(cur);
11229
+ if (!parent || visited.has(parent))
11230
+ break;
11231
+ visited.add(parent);
11232
+ const parentEntry = activeById(parent);
11233
+ if (parentEntry) {
11234
+ return { ok: true, ownerSessionId: parent, entry: parentEntry, via: "ancestor" };
11235
+ }
11236
+ cur = parent;
11237
+ }
11238
+ return { ok: true, ownerSessionId: sid, entry: null, via: "fallback-self" };
11239
+ }
10890
11240
  async function markPlanReadOk(opts) {
10891
11241
  return await mutateRegistry(opts.mainRoot, (reg) => {
10892
11242
  const entry = reg.entries.find((e) => e.sessionId === opts.sessionId);
@@ -10907,7 +11257,7 @@ async function touchEntryUpdatedAt(opts) {
10907
11257
  });
10908
11258
  }
10909
11259
  async function mergeSessionBack(opts) {
10910
- const mainRoot = path11.resolve(opts.mainRoot);
11260
+ const mainRoot = path12.resolve(opts.mainRoot);
10911
11261
  const entry = await getSessionWorktree(opts.sessionId, mainRoot);
10912
11262
  if (!entry) {
10913
11263
  throw new Error(`mergeSessionBack: session ${opts.sessionId} 没有绑定 worktree`);
@@ -10989,7 +11339,7 @@ async function mergeSessionBack(opts) {
10989
11339
  return { sha: newSha, squashedCommits };
10990
11340
  }
10991
11341
  async function discardSession(opts) {
10992
- const mainRoot = path11.resolve(opts.mainRoot);
11342
+ const mainRoot = path12.resolve(opts.mainRoot);
10993
11343
  const entry = await getSessionWorktree(opts.sessionId, mainRoot);
10994
11344
  if (!entry) {
10995
11345
  return;
@@ -11041,7 +11391,7 @@ async function markInterruptedDirty(opts) {
11041
11391
  }
11042
11392
  var SALVAGE_BRANCH_PREFIX = "codeforge/salvage/";
11043
11393
  async function listSalvageBranches(mainRoot) {
11044
- const resolved = path11.resolve(mainRoot);
11394
+ const resolved = path12.resolve(mainRoot);
11045
11395
  try {
11046
11396
  const out = await runGit2(resolved, [
11047
11397
  "for-each-ref",
@@ -11060,14 +11410,27 @@ async function listSalvageBranches(mainRoot) {
11060
11410
  }
11061
11411
  }
11062
11412
  async function reconcileTransitionalEntries(mainRoot) {
11063
- const resolved = path11.resolve(mainRoot);
11413
+ const resolved = path12.resolve(mainRoot);
11064
11414
  const result = {
11065
11415
  cleanedCreating: [],
11066
11416
  finishedRemoving: [],
11067
11417
  keptConservative: []
11068
11418
  };
11069
- const snapshot = await readRegistry(resolved);
11070
- const hasTransitional = snapshot.entries.some((e) => e.status === "creating" || e.status === "removing");
11419
+ const snap = await readRegistryResult(resolved);
11420
+ if (snap.kind === "corrupt") {
11421
+ result.registryUnreadable = { reason: "corrupt", detail: snap.reason, path: snap.path };
11422
+ return result;
11423
+ }
11424
+ if (snap.kind === "version_too_new") {
11425
+ result.registryUnreadable = {
11426
+ reason: "version_too_new",
11427
+ detail: `v${snap.foundVersion} > 支持 v${snap.supportedVersion}`,
11428
+ path: snap.path
11429
+ };
11430
+ return result;
11431
+ }
11432
+ const entries = snap.kind === "ok" ? snap.registry.entries : [];
11433
+ const hasTransitional = entries.some((e) => e.status === "creating" || e.status === "removing");
11071
11434
  if (!hasTransitional)
11072
11435
  return result;
11073
11436
  await mutateRegistry(resolved, async (reg) => {
@@ -11076,7 +11439,7 @@ async function reconcileTransitionalEntries(mainRoot) {
11076
11439
  continue;
11077
11440
  let dirExists = true;
11078
11441
  try {
11079
- const st = await fs9.stat(entry.worktreePath);
11442
+ const st = await fs10.stat(entry.worktreePath);
11080
11443
  dirExists = st.isDirectory();
11081
11444
  } catch {
11082
11445
  dirExists = false;
@@ -11121,9 +11484,44 @@ async function reconcileTransitionalEntries(mainRoot) {
11121
11484
  });
11122
11485
  return result;
11123
11486
  }
11487
+ function summarizeReconcileDigest(result, prune, maxList = 3) {
11488
+ const unreadable = result.registryUnreadable ?? prune?.registryUnreadable;
11489
+ if (unreadable) {
11490
+ const hint = unreadable.reason === "version_too_new" ? "版本过高,请升级 CodeForge 后再操作" : "文件损坏,已备份留痕,请检查后删除让系统重建";
11491
+ return `[codeforge] ⚠️ session 追踪文件不可读(${unreadable.reason}:${unreadable.detail}),本轮已跳过 worktree 清理(fail-closed,未删除任何 worktree)。${hint}。`;
11492
+ }
11493
+ const totalReconcile = result.cleanedCreating.length + result.finishedRemoving.length + result.keptConservative.length;
11494
+ const cleanedPrune = prune?.cleaned.length ?? 0;
11495
+ const failedPrune = prune?.failed.length ?? 0;
11496
+ if (totalReconcile === 0 && cleanedPrune === 0 && failedPrune === 0)
11497
+ return "";
11498
+ const parts = [];
11499
+ if (result.cleanedCreating.length > 0) {
11500
+ parts.push(`清理半成品 ${result.cleanedCreating.length}`);
11501
+ }
11502
+ if (result.finishedRemoving.length > 0) {
11503
+ parts.push(`收尾删除 ${result.finishedRemoving.length}`);
11504
+ }
11505
+ if (result.keptConservative.length > 0) {
11506
+ parts.push(`保守保留 ${result.keptConservative.length}`);
11507
+ }
11508
+ if (cleanedPrune > 0)
11509
+ parts.push(`清理僵尸 worktree ${cleanedPrune}`);
11510
+ if (failedPrune > 0)
11511
+ parts.push(`清理失败 ${failedPrune}`);
11512
+ const sample = [
11513
+ ...result.cleanedCreating,
11514
+ ...result.finishedRemoving,
11515
+ ...prune?.cleaned ?? []
11516
+ ];
11517
+ const uniq = [...new Set(sample)].slice(0, maxList);
11518
+ const more = sample.length > uniq.length ? ` 等` : "";
11519
+ const sampleStr = uniq.length > 0 ? `(${uniq.join(", ")}${more})` : "";
11520
+ return `[codeforge] worktree 收敛:${parts.join(",")}${sampleStr}`;
11521
+ }
11124
11522
  async function getCurrentWorktreeHead(worktreePath) {
11125
11523
  try {
11126
- return (await runGit2(path11.resolve(worktreePath), ["rev-parse", "HEAD"])).trim();
11524
+ return (await runGit2(path12.resolve(worktreePath), ["rev-parse", "HEAD"])).trim();
11127
11525
  } catch {
11128
11526
  return "";
11129
11527
  }
@@ -11142,7 +11540,8 @@ async function checkpointCommit(opts) {
11142
11540
  }
11143
11541
  var CHECKPOINT_MESSAGE_PREFIX = "[codeforge-checkpoint] pre-lane-dispatch";
11144
11542
  async function checkpointSessionWorktree(opts) {
11145
- const mainRoot = path11.resolve(opts.mainRoot);
11543
+ const mainRoot = path12.resolve(opts.mainRoot);
11544
+ await loadRegistryForMutation(mainRoot);
11146
11545
  const entry = await getSessionWorktree(opts.sessionId, mainRoot);
11147
11546
  if (!entry) {
11148
11547
  throw new Error(`checkpointSessionWorktree: session ${opts.sessionId} 没有绑定 worktree`);
@@ -11164,19 +11563,19 @@ async function checkpointSessionWorktree(opts) {
11164
11563
  return { sessionId: opts.sessionId, worktreePath: wt, committed: true, head };
11165
11564
  }
11166
11565
  function runGit2(cwd, args, timeoutMs = 1e4) {
11167
- return new Promise((resolve10, reject) => {
11566
+ return new Promise((resolve11, reject) => {
11168
11567
  execFile3("git", args, { cwd, timeout: timeoutMs, windowsHide: true, encoding: "utf8" }, (err, stdout, stderr) => {
11169
11568
  if (err) {
11170
11569
  reject(new Error(`git ${args.join(" ")} (cwd=${cwd}) 失败: ${stderr?.trim() || err.message}`));
11171
11570
  return;
11172
11571
  }
11173
- resolve10(stdout);
11572
+ resolve11(stdout);
11174
11573
  });
11175
11574
  });
11176
11575
  }
11177
11576
  function runGitWithEnv(cwd, args, envOverrides, timeoutMs = 1e4) {
11178
11577
  const inheritedEnv = process["env"];
11179
- return new Promise((resolve10, reject) => {
11578
+ return new Promise((resolve11, reject) => {
11180
11579
  execFile3("git", args, {
11181
11580
  cwd,
11182
11581
  timeout: timeoutMs,
@@ -11188,7 +11587,7 @@ function runGitWithEnv(cwd, args, envOverrides, timeoutMs = 1e4) {
11188
11587
  reject(new Error(`git ${args.join(" ")} (cwd=${cwd}) 失败: ${stderr?.trim() || err.message}`));
11189
11588
  return;
11190
11589
  }
11191
- resolve10(stdout);
11590
+ resolve11(stdout);
11192
11591
  });
11193
11592
  });
11194
11593
  }
@@ -11206,7 +11605,7 @@ async function getBuildScript(mainRoot) {
11206
11605
  async function shouldSkipDevOnce(mainRoot, stagedPaths, worktreePath) {
11207
11606
  let distMtimeSec;
11208
11607
  try {
11209
- const st = await fs9.stat(path11.join(mainRoot, "dist/index.js"));
11608
+ const st = await fs10.stat(path12.join(mainRoot, "dist/index.js"));
11210
11609
  distMtimeSec = Math.floor(st.mtimeMs / 1000);
11211
11610
  } catch {
11212
11611
  return false;
@@ -11226,25 +11625,25 @@ async function shouldSkipDevOnce(mainRoot, stagedPaths, worktreePath) {
11226
11625
  async function statSourceMtime(rel, mainRoot, worktreePath) {
11227
11626
  if (worktreePath) {
11228
11627
  try {
11229
- const st = await fs9.stat(path11.join(worktreePath, rel));
11628
+ const st = await fs10.stat(path12.join(worktreePath, rel));
11230
11629
  return Math.floor(st.mtimeMs / 1000);
11231
11630
  } catch {}
11232
11631
  }
11233
11632
  try {
11234
- const st = await fs9.stat(path11.join(mainRoot, rel));
11633
+ const st = await fs10.stat(path12.join(mainRoot, rel));
11235
11634
  return Math.floor(st.mtimeMs / 1000);
11236
11635
  } catch {
11237
11636
  return null;
11238
11637
  }
11239
11638
  }
11240
11639
  function runCmd(cmd, args, cwd, timeoutMs = 300000) {
11241
- return new Promise((resolve10, reject) => {
11640
+ return new Promise((resolve11, reject) => {
11242
11641
  execFile3(cmd, args, { cwd, timeout: timeoutMs, windowsHide: true, encoding: "utf8" }, (err, stdout, stderr) => {
11243
11642
  if (err) {
11244
11643
  reject(new Error(`${cmd} ${args.join(" ")} (cwd=${cwd}) 失败: ${stderr?.trim() || err.message}`));
11245
11644
  return;
11246
11645
  }
11247
- resolve10(stdout);
11646
+ resolve11(stdout);
11248
11647
  });
11249
11648
  });
11250
11649
  }
@@ -11269,7 +11668,7 @@ async function pruneDiscardedRegistryEntries(mainRoot, opts = {}) {
11269
11668
  if (keepRecent < 0) {
11270
11669
  throw new Error(`pruneDiscardedRegistryEntries: keepRecent 必须 ≥ 0,收到 ${keepRecent}`);
11271
11670
  }
11272
- return await mutateRegistry(path11.resolve(mainRoot), (reg) => {
11671
+ return await mutateRegistry(path12.resolve(mainRoot), (reg) => {
11273
11672
  const discarded = [];
11274
11673
  const others = [];
11275
11674
  for (const e of reg.entries) {
@@ -11286,10 +11685,24 @@ async function pruneDiscardedRegistryEntries(mainRoot, opts = {}) {
11286
11685
  });
11287
11686
  }
11288
11687
  async function pruneOrphanWorktrees(mainRoot, opts = {}) {
11289
- const resolved = path11.resolve(mainRoot);
11688
+ const resolved = path12.resolve(mainRoot);
11290
11689
  const cleaned = [];
11291
11690
  const failed = [];
11292
11691
  let skipped = 0;
11692
+ const health = await readRegistryResult(resolved);
11693
+ if (health.kind === "corrupt" || health.kind === "version_too_new") {
11694
+ debugLog(`pruneOrphanWorktrees 跳过:registry 不可读 (${health.kind}),绝不清理 worktree(fail-closed)`);
11695
+ return {
11696
+ cleaned,
11697
+ failed,
11698
+ skipped,
11699
+ registryUnreadable: {
11700
+ reason: health.kind,
11701
+ detail: health.kind === "corrupt" ? health.reason : `v${health.foundVersion} > 支持 v${health.supportedVersion}`,
11702
+ path: health.path
11703
+ }
11704
+ };
11705
+ }
11293
11706
  let gitWorktrees = [];
11294
11707
  try {
11295
11708
  const mod = await Promise.resolve().then(() => (init_worktree_ops(), exports_worktree_ops));
@@ -11297,7 +11710,7 @@ async function pruneOrphanWorktrees(mainRoot, opts = {}) {
11297
11710
  } catch {
11298
11711
  gitWorktrees = [];
11299
11712
  }
11300
- const gitWorktreePaths = new Set(gitWorktrees.map((w) => path11.resolve(w.path)));
11713
+ const gitWorktreePaths = new Set(gitWorktrees.map((w) => path12.resolve(w.path)));
11301
11714
  await mutateRegistry(resolved, async (reg2) => {
11302
11715
  const now = Date.now();
11303
11716
  for (const entry of reg2.entries) {
@@ -11307,7 +11720,7 @@ async function pruneOrphanWorktrees(mainRoot, opts = {}) {
11307
11720
  let dirExists = true;
11308
11721
  let dirMtimeMs = 0;
11309
11722
  try {
11310
- const st = await fs9.stat(wt);
11723
+ const st = await fs10.stat(wt);
11311
11724
  dirExists = st.isDirectory();
11312
11725
  dirMtimeMs = st.mtimeMs;
11313
11726
  } catch {
@@ -11400,12 +11813,12 @@ async function pruneOrphanWorktrees(mainRoot, opts = {}) {
11400
11813
  }
11401
11814
  });
11402
11815
  }
11403
- const codeforgeWorktreeRoot = path11.resolve(path11.join(resolved, DEFAULT_WORKTREE_SUBDIR));
11816
+ const codeforgeWorktreeRoot = path12.resolve(path12.join(resolved, DEFAULT_WORKTREE_SUBDIR));
11404
11817
  const fsWorktreePaths = [];
11405
11818
  try {
11406
- const names = await fs9.readdir(codeforgeWorktreeRoot);
11819
+ const names = await fs10.readdir(codeforgeWorktreeRoot);
11407
11820
  for (const name of names) {
11408
- fsWorktreePaths.push(path11.resolve(path11.join(codeforgeWorktreeRoot, name)));
11821
+ fsWorktreePaths.push(path12.resolve(path12.join(codeforgeWorktreeRoot, name)));
11409
11822
  }
11410
11823
  } catch {}
11411
11824
  const candidatePaths = new Set([
@@ -11413,16 +11826,16 @@ async function pruneOrphanWorktrees(mainRoot, opts = {}) {
11413
11826
  ...fsWorktreePaths
11414
11827
  ]);
11415
11828
  const reg = await readRegistry(resolved);
11416
- const knownPaths = new Set(reg.entries.map((e) => path11.resolve(e.worktreePath)));
11829
+ const knownPaths = new Set(reg.entries.map((e) => path12.resolve(e.worktreePath)));
11417
11830
  for (const candidate of candidatePaths) {
11418
11831
  if (knownPaths.has(candidate))
11419
11832
  continue;
11420
- if (candidate !== codeforgeWorktreeRoot && !candidate.startsWith(codeforgeWorktreeRoot + path11.sep)) {
11833
+ if (candidate !== codeforgeWorktreeRoot && !candidate.startsWith(codeforgeWorktreeRoot + path12.sep)) {
11421
11834
  continue;
11422
11835
  }
11423
11836
  let dirExists = true;
11424
11837
  try {
11425
- const st = await fs9.stat(candidate);
11838
+ const st = await fs10.stat(candidate);
11426
11839
  if (Date.now() - st.mtimeMs < ORPHAN_GRACE_MS) {
11427
11840
  skipped++;
11428
11841
  continue;
@@ -11444,14 +11857,14 @@ async function pruneOrphanWorktrees(mainRoot, opts = {}) {
11444
11857
  }
11445
11858
  if (removed) {
11446
11859
  try {
11447
- await fs9.stat(candidate);
11860
+ await fs10.stat(candidate);
11448
11861
  removed = false;
11449
11862
  lastError = lastError ?? "git worktree remove 返回成功但目录仍存在(C 类 fs-only orphan)";
11450
11863
  } catch {}
11451
11864
  }
11452
11865
  if (!removed && dirExists) {
11453
11866
  try {
11454
- await fs9.rm(candidate, { recursive: true, force: true });
11867
+ await fs10.rm(candidate, { recursive: true, force: true });
11455
11868
  removed = true;
11456
11869
  } catch (err) {
11457
11870
  lastError = `git remove 失败: ${lastError}; fs.rm 也失败: ${err instanceof Error ? err.message : String(err)}`;
@@ -11506,7 +11919,8 @@ var ArgsSchema4 = z4.object({
11506
11919
  sessionId: z4.string().optional().describe("reviewer 子 session id(boomerang 溯源用,可选)"),
11507
11920
  model: z4.string().optional().describe("审批模型 id(审计用,可选)"),
11508
11921
  coveredSha: z4.string().optional().describe("approval 写入时 worktree HEAD sha;对 session:<sid> id 工具会自动从 worktree HEAD 捕获(缺失时),显式传入值优先。ADR:review-approval-auto-covered-sha / ADR:merge-approval-pre-check"),
11509
- reviewTarget: z4.string().optional().describe("本次审阅的 review_target 值(reviewer.md 词表:code / code:typescript / code:python / code:csharp-lua-c / plan_only / adr / docs / decision_only)。对 session:<sid> id 缺失时默认 'code',显式传入值优先。pre-check 仅放行 startsWith('code') 的值。ADR:review-approval-auto-covered-sha / ADR:merge-approval-pre-check")
11922
+ reviewTarget: z4.string().optional().describe("本次审阅的 review_target 值(reviewer.md 词表:code / code:typescript / code:python / code:csharp-lua-c / plan_only / adr / docs / decision_only)。对 session:<sid> id 缺失时默认 'code',显式传入值优先。pre-check 仅放行 startsWith('code') 的值。ADR:review-approval-auto-covered-sha / ADR:merge-approval-pre-check"),
11923
+ worktreePath: z4.string().optional().describe("reviewer prompt 的 worktree_path 绝对路径;对 session:<sid> id 用于 owner 归一反查(优先于 parentMap 上溯,不依赖进程内映射)。ADR:worktree-approval-owner-reconciliation")
11510
11924
  });
11511
11925
  var _approvalStore = null;
11512
11926
  function getApprovalStore() {
@@ -11514,7 +11928,7 @@ function getApprovalStore() {
11514
11928
  _approvalStore = ApprovalStore.forProject(process.cwd());
11515
11929
  return _approvalStore;
11516
11930
  }
11517
- var _worktreeResolvers = { getSessionWorktree, getCurrentWorktreeHead };
11931
+ var _worktreeResolvers = { getSessionWorktree, getCurrentWorktreeHead, resolveWorktreeOwner };
11518
11932
  async function execute4(input) {
11519
11933
  const parsed = ArgsSchema4.safeParse(input);
11520
11934
  if (!parsed.success) {
@@ -11527,44 +11941,98 @@ async function execute4(input) {
11527
11941
  const approvals = getApprovalStore();
11528
11942
  const now = new Date().toISOString();
11529
11943
  const written = [];
11944
+ const buildMeta = (pendingId, extra) => ({
11945
+ pendingId,
11946
+ verdict: args.verdict,
11947
+ reviewer: {
11948
+ agent: args.reviewerAgent ?? (args.source === "codeforge-fallback" ? "codeforge" : "reviewer"),
11949
+ ...args.sessionId ? { sessionId: args.sessionId } : {},
11950
+ ...args.model ? { model: args.model } : {},
11951
+ source: args.source ?? "reviewer"
11952
+ },
11953
+ targets: [],
11954
+ decisionLine: args.decisionLine ?? args.verdict,
11955
+ notes: args.notes,
11956
+ createdAt: now,
11957
+ ...extra.coveredSha ? { coveredSha: extra.coveredSha } : {},
11958
+ ...extra.reviewTarget ? { reviewTarget: extra.reviewTarget } : {},
11959
+ ...extra.resolvedOwnerSessionId ? { resolvedOwnerSessionId: extra.resolvedOwnerSessionId } : {},
11960
+ ...extra.aliasOf ? { aliasOf: extra.aliasOf } : {},
11961
+ ...extra.ownerResolved !== undefined ? { ownerResolved: extra.ownerResolved } : {},
11962
+ escapeHatch: null
11963
+ });
11530
11964
  for (const id of args.pendingIds) {
11531
- let coveredSha = args.coveredSha;
11532
- let reviewTarget = args.reviewTarget;
11533
11965
  const sessionMatch = /^session:(.+)$/.exec(id);
11534
- if (sessionMatch) {
11535
- const sid = sessionMatch[1];
11536
- if (!coveredSha) {
11966
+ if (!sessionMatch) {
11967
+ const meta = buildMeta(id, {
11968
+ coveredSha: args.coveredSha,
11969
+ reviewTarget: args.reviewTarget
11970
+ });
11971
+ const file = await approvals.record(meta);
11972
+ written.push({ pendingId: id, file });
11973
+ continue;
11974
+ }
11975
+ const sid = sessionMatch[1];
11976
+ let owner;
11977
+ try {
11978
+ owner = await _worktreeResolvers.resolveWorktreeOwner({
11979
+ sessionId: sid,
11980
+ mainRoot: process.cwd(),
11981
+ ...args.worktreePath ? { worktreePath: args.worktreePath } : {}
11982
+ });
11983
+ } catch {
11984
+ owner = {
11985
+ ok: false,
11986
+ ownerSessionId: sid,
11987
+ entry: null,
11988
+ via: "registry-unreadable",
11989
+ reason: "resolveWorktreeOwner threw"
11990
+ };
11991
+ }
11992
+ const ownerResolved = owner.ok && owner.entry !== null;
11993
+ let coveredSha = args.coveredSha;
11994
+ const reviewTarget = args.reviewTarget ?? "code";
11995
+ if (!coveredSha) {
11996
+ let wtPath = owner.entry?.worktreePath;
11997
+ if (!wtPath) {
11537
11998
  try {
11538
- const entry = await _worktreeResolvers.getSessionWorktree(sid, process.cwd());
11539
- if (entry?.worktreePath) {
11540
- const head = await _worktreeResolvers.getCurrentWorktreeHead(entry.worktreePath);
11541
- if (head)
11542
- coveredSha = head;
11543
- }
11999
+ const fallbackEntry = await _worktreeResolvers.getSessionWorktree(sid, process.cwd());
12000
+ wtPath = fallbackEntry?.worktreePath;
11544
12001
  } catch {}
11545
12002
  }
11546
- if (!reviewTarget)
11547
- reviewTarget = "code";
12003
+ if (wtPath) {
12004
+ const head = await _worktreeResolvers.getCurrentWorktreeHead(wtPath);
12005
+ if (head)
12006
+ coveredSha = head;
12007
+ }
12008
+ }
12009
+ if (ownerResolved && owner.ownerSessionId !== sid) {
12010
+ const ownerKey = `session:${owner.ownerSessionId}`;
12011
+ const ownerMeta = buildMeta(ownerKey, {
12012
+ coveredSha,
12013
+ reviewTarget,
12014
+ resolvedOwnerSessionId: sid,
12015
+ ownerResolved: true
12016
+ });
12017
+ const ownerFile = await approvals.record(ownerMeta);
12018
+ written.push({ pendingId: ownerKey, file: ownerFile });
12019
+ const aliasMeta = buildMeta(id, {
12020
+ coveredSha,
12021
+ reviewTarget,
12022
+ aliasOf: owner.ownerSessionId,
12023
+ ownerResolved: true
12024
+ });
12025
+ const aliasFile = await approvals.record(aliasMeta);
12026
+ written.push({ pendingId: id, file: aliasFile });
12027
+ } else {
12028
+ const meta = buildMeta(id, {
12029
+ coveredSha,
12030
+ reviewTarget,
12031
+ ownerResolved
12032
+ });
12033
+ const file = await approvals.record(meta);
12034
+ written.push({ pendingId: id, file });
11548
12035
  }
11549
- const meta = {
11550
- pendingId: id,
11551
- verdict: args.verdict,
11552
- reviewer: {
11553
- agent: args.reviewerAgent ?? (args.source === "codeforge-fallback" ? "codeforge" : "reviewer"),
11554
- ...args.sessionId ? { sessionId: args.sessionId } : {},
11555
- ...args.model ? { model: args.model } : {},
11556
- source: args.source ?? "reviewer"
11557
- },
11558
- targets: [],
11559
- decisionLine: args.decisionLine ?? args.verdict,
11560
- notes: args.notes,
11561
- createdAt: now,
11562
- ...coveredSha ? { coveredSha } : {},
11563
- ...reviewTarget ? { reviewTarget } : {},
11564
- escapeHatch: null
11565
- };
11566
- const file = await approvals.record(meta);
11567
- written.push({ pendingId: id, file });
11568
12036
  }
11569
12037
  return { ok: true, written };
11570
12038
  }
@@ -11572,7 +12040,7 @@ async function execute4(input) {
11572
12040
  import { z as z5 } from "zod";
11573
12041
 
11574
12042
  // lib/browser-control.ts
11575
- import * as path12 from "node:path";
12043
+ import * as path13 from "node:path";
11576
12044
  var DEFAULT_CONFIG2 = {
11577
12045
  enabled: false,
11578
12046
  headless: true,
@@ -11584,7 +12052,7 @@ var DEFAULT_CONFIG2 = {
11584
12052
  bufferLimit: 500
11585
12053
  };
11586
12054
  function defaultScreenshotDir(root = process.cwd()) {
11587
- return path12.join(runtimeDir(root), "browser", "screenshots");
12055
+ return path13.join(runtimeDir(root), "browser", "screenshots");
11588
12056
  }
11589
12057
  function checkUrl(url, cfg = DEFAULT_CONFIG2) {
11590
12058
  if (typeof url !== "string" || url.trim() === "") {
@@ -11709,14 +12177,14 @@ async function tryCreatePlaywrightController(cfg = DEFAULT_CONFIG2, resolver = d
11709
12177
  async screenshot(opts) {
11710
12178
  try {
11711
12179
  const dir = cfg.screenshotDir && cfg.screenshotDir.trim().length > 0 ? cfg.screenshotDir : defaultScreenshotDir();
11712
- const path13 = `${dir}/${Date.now()}.png`;
12180
+ const path14 = `${dir}/${Date.now()}.png`;
11713
12181
  if (opts?.selector) {
11714
12182
  const el = await page.locator(opts.selector).first();
11715
- await el.screenshot({ path: path13 });
12183
+ await el.screenshot({ path: path14 });
11716
12184
  } else {
11717
- await page.screenshot({ path: path13, fullPage: opts?.fullPage });
12185
+ await page.screenshot({ path: path14, fullPage: opts?.fullPage });
11718
12186
  }
11719
- return { ok: true, path: path13 };
12187
+ return { ok: true, path: path14 };
11720
12188
  } catch (err) {
11721
12189
  return { ok: false, error: describe3(err) };
11722
12190
  }
@@ -12033,8 +12501,8 @@ async function execute10(input) {
12033
12501
  import { z as z11 } from "zod";
12034
12502
 
12035
12503
  // lib/model-config.ts
12036
- import { promises as fs10 } from "node:fs";
12037
- import * as path13 from "node:path";
12504
+ import { promises as fs11 } from "node:fs";
12505
+ import * as path14 from "node:path";
12038
12506
 
12039
12507
  // lib/model-tier.ts
12040
12508
  var TIER_ORDER = ["quick", "balanced", "deep", "ultra"];
@@ -12051,12 +12519,12 @@ var PROVIDER_MODEL_RE = /^[a-z0-9-]+\/[a-zA-Z0-9._-]+$/;
12051
12519
  function findConfigFileSync(opts = {}) {
12052
12520
  const root = opts.root ?? process.cwd();
12053
12521
  const fsSync = __require("node:fs");
12054
- const abs = path13.resolve(root, opts.file ?? CONFIG_FILE);
12522
+ const abs = path14.resolve(root, opts.file ?? CONFIG_FILE);
12055
12523
  return fsSync.existsSync(abs) ? abs : null;
12056
12524
  }
12057
12525
  function loadModelConfigSync(opts = {}) {
12058
12526
  const root = opts.root ?? process.cwd();
12059
- const abs = path13.resolve(root, opts.file ?? CONFIG_FILE);
12527
+ const abs = path14.resolve(root, opts.file ?? CONFIG_FILE);
12060
12528
  const fsSync = __require("node:fs");
12061
12529
  if (!fsSync.existsSync(abs)) {
12062
12530
  return { ok: false, warnings: [], error: `config_not_found: ${abs}` };
@@ -12071,10 +12539,10 @@ function loadModelConfigSync(opts = {}) {
12071
12539
  }
12072
12540
  async function loadModelConfig(opts = {}) {
12073
12541
  const root = opts.root ?? process.cwd();
12074
- const abs = path13.resolve(root, opts.file ?? CONFIG_FILE);
12542
+ const abs = path14.resolve(root, opts.file ?? CONFIG_FILE);
12075
12543
  let raw;
12076
12544
  try {
12077
- raw = await fs10.readFile(abs, "utf8");
12545
+ raw = await fs11.readFile(abs, "utf8");
12078
12546
  } catch (e) {
12079
12547
  const code = e.code;
12080
12548
  if (code === "ENOENT") {
@@ -12285,6 +12753,18 @@ function normalizeThinking(ctx, raw) {
12285
12753
  if (o.budget_tokens !== undefined && (typeof o.budget_tokens !== "number" || o.budget_tokens < 0)) {
12286
12754
  return { ok: false, error: `${ctx}.thinking.budget_tokens: must be non-negative number` };
12287
12755
  }
12756
+ const REASONING_EFFORT_VALUES = ["low", "medium", "high", "xhigh"];
12757
+ if (o.reasoning_effort !== undefined) {
12758
+ if (typeof o.reasoning_effort !== "string" || !REASONING_EFFORT_VALUES.includes(o.reasoning_effort)) {
12759
+ return { ok: false, error: `${ctx}.thinking.reasoning_effort: must be one of ${REASONING_EFFORT_VALUES.join("/")}` };
12760
+ }
12761
+ }
12762
+ const THINKING_LEVEL_VALUES = ["low", "medium", "high"];
12763
+ if (o.thinking_level !== undefined) {
12764
+ if (typeof o.thinking_level !== "string" || !THINKING_LEVEL_VALUES.includes(o.thinking_level)) {
12765
+ return { ok: false, error: `${ctx}.thinking.thinking_level: must be one of ${THINKING_LEVEL_VALUES.join("/")}` };
12766
+ }
12767
+ }
12288
12768
  return { ok: true, value: { ...o, type: o.type } };
12289
12769
  }
12290
12770
  function normalizeRuntime(raw) {
@@ -12536,8 +13016,8 @@ function toEntry(r) {
12536
13016
  import { z as z12 } from "zod";
12537
13017
 
12538
13018
  // lib/merge-gate.ts
12539
- import { promises as fs11 } from "node:fs";
12540
- import * as path14 from "node:path";
13019
+ import { promises as fs12 } from "node:fs";
13020
+ import * as path15 from "node:path";
12541
13021
  var DEFAULT_MERGE_GATE_CONFIG = {
12542
13022
  enabled: true,
12543
13023
  approvalPreCheck: true,
@@ -12545,10 +13025,10 @@ var DEFAULT_MERGE_GATE_CONFIG = {
12545
13025
  };
12546
13026
  var CONFIG_REL = ".codeforge/merge-gate.json";
12547
13027
  async function loadMergeGate(mainRoot) {
12548
- const file = path14.join(mainRoot, CONFIG_REL);
13028
+ const file = path15.join(mainRoot, CONFIG_REL);
12549
13029
  let raw;
12550
13030
  try {
12551
- raw = await fs11.readFile(file, "utf8");
13031
+ raw = await fs12.readFile(file, "utf8");
12552
13032
  } catch (err) {
12553
13033
  const e = err;
12554
13034
  if (e.code === "ENOENT")
@@ -12715,6 +13195,13 @@ async function runMergeLoop(opts) {
12715
13195
  console.warn(`[merge-loop] approval-store 查询失败 (session=${opts.sessionId}): ${err instanceof Error ? err.message : String(err)}`);
12716
13196
  approval = undefined;
12717
13197
  }
13198
+ if (!approval) {
13199
+ const aliasHit = await tryOwnerAliasBackstop({ opts, entry, store });
13200
+ if (aliasHit) {
13201
+ approval = aliasHit.approval;
13202
+ approvalKey = aliasHit.approvalKey;
13203
+ }
13204
+ }
12718
13205
  const valid = !!approval && (approval.verdict === "APPROVE" || approval.verdict === "APPROVE_WITH_NOTES");
12719
13206
  if (!valid) {
12720
13207
  const blockReason = buildApprovalGateBlockReason({
@@ -12901,6 +13388,11 @@ async function tryApprovalPreCheck(args) {
12901
13388
  console.warn(`[merge-loop] pre-check getLatest 失败 (session=${opts.sessionId}): ${err instanceof Error ? err.message : String(err)}`);
12902
13389
  return { ok: false, reason: "no-approval" };
12903
13390
  }
13391
+ if (!approval) {
13392
+ const aliasHit = await tryOwnerAliasBackstop({ opts, entry, store });
13393
+ if (aliasHit)
13394
+ approval = aliasHit.approval;
13395
+ }
12904
13396
  if (!approval)
12905
13397
  return { ok: false, reason: "no-approval" };
12906
13398
  if (approval.verdict !== "APPROVE" && approval.verdict !== "APPROVE_WITH_NOTES") {
@@ -12920,11 +13412,38 @@ async function tryApprovalPreCheck(args) {
12920
13412
  return { ok: false, reason: "ttl-expired" };
12921
13413
  return { ok: true, coveredSha: approval.coveredSha, reviewTarget: approval.reviewTarget };
12922
13414
  }
13415
+ async function tryOwnerAliasBackstop(args) {
13416
+ const { opts, entry, store } = args;
13417
+ const resolveOwner = opts.__testHooks?.resolveWorktreeOwner ?? resolveWorktreeOwner;
13418
+ const validVerdict = (a) => !!a && (a.verdict === "APPROVE" || a.verdict === "APPROVE_WITH_NOTES");
13419
+ try {
13420
+ const owner = await resolveOwner({
13421
+ sessionId: opts.sessionId,
13422
+ mainRoot: opts.mainRoot,
13423
+ worktreePath: entry.worktreePath
13424
+ });
13425
+ if (owner.ok && owner.ownerSessionId !== opts.sessionId) {
13426
+ const key = `session:${owner.ownerSessionId}`;
13427
+ const a = await store.getLatest(key);
13428
+ if (validVerdict(a))
13429
+ return { approval: a, approvalKey: key };
13430
+ }
13431
+ } catch {}
13432
+ if (typeof store.findAliasApprovals === "function") {
13433
+ try {
13434
+ const aliases = await store.findAliasApprovals(opts.sessionId);
13435
+ const hit = aliases.find(validVerdict);
13436
+ if (hit)
13437
+ return { approval: hit, approvalKey: hit.pendingId };
13438
+ } catch {}
13439
+ }
13440
+ return;
13441
+ }
12923
13442
  function isAbortError(err) {
12924
13443
  return err instanceof Error && err.name === "AbortError";
12925
13444
  }
12926
13445
  function withTimeout2(p, ms, label, signal, hbOpts) {
12927
- return new Promise((resolve11, reject) => {
13446
+ return new Promise((resolve12, reject) => {
12928
13447
  const startedAt = Date.now();
12929
13448
  let hbTimer = null;
12930
13449
  let timer;
@@ -12964,7 +13483,7 @@ function withTimeout2(p, ms, label, signal, hbOpts) {
12964
13483
  }
12965
13484
  p.then((v) => {
12966
13485
  cleanup();
12967
- resolve11(v);
13486
+ resolve12(v);
12968
13487
  }, (e) => {
12969
13488
  cleanup();
12970
13489
  reject(e);
@@ -13087,6 +13606,24 @@ async function isSessionIdValid(sid, mainRoot) {
13087
13606
  return false;
13088
13607
  }
13089
13608
  }
13609
+ async function checkRegistryHealth(mainRoot, action) {
13610
+ const health = await readRegistryResult(mainRoot);
13611
+ if (health.kind === "corrupt") {
13612
+ return {
13613
+ ok: false,
13614
+ action,
13615
+ error: `session 追踪文件损坏(${health.reason}),暂时无法读取 worktree 状态` + `——这**不代表**没有 worktree。请检查 ${health.path}` + `(如有 .corrupt.* 备份可对照),确认后删除损坏文件让系统重建,或恢复正确内容。`
13616
+ };
13617
+ }
13618
+ if (health.kind === "version_too_new") {
13619
+ return {
13620
+ ok: false,
13621
+ action,
13622
+ error: `session 追踪文件版本(v${health.foundVersion})高于当前 CodeForge 支持` + `(v${health.supportedVersion}),已启用只读保护。请升级 CodeForge 后再查看/操作` + `(原文件未被改动)。`
13623
+ };
13624
+ }
13625
+ return null;
13626
+ }
13090
13627
  async function execute12(input) {
13091
13628
  const parsed = ArgsSchema12.safeParse(input);
13092
13629
  if (!parsed.success) {
@@ -13102,6 +13639,9 @@ async function execute12(input) {
13102
13639
  const mainRoot = getMainRoot();
13103
13640
  const sessionId = await resolveSessionId(args.session_id);
13104
13641
  if (args.action === "status") {
13642
+ const bad = await checkRegistryHealth(mainRoot, "status");
13643
+ if (bad)
13644
+ return bad;
13105
13645
  const entry = await getSessionWorktree(sessionId, mainRoot);
13106
13646
  if (!entry) {
13107
13647
  return {
@@ -13114,6 +13654,9 @@ async function execute12(input) {
13114
13654
  return { ok: true, action: "status", data: { ...entry, summary } };
13115
13655
  }
13116
13656
  if (args.action === "discard") {
13657
+ const bad = await checkRegistryHealth(mainRoot, "discard");
13658
+ if (bad)
13659
+ return bad;
13117
13660
  await discardSession({ sessionId, mainRoot });
13118
13661
  return {
13119
13662
  ok: true,
@@ -13122,6 +13665,9 @@ async function execute12(input) {
13122
13665
  };
13123
13666
  }
13124
13667
  if (args.action === "diff") {
13668
+ const bad = await checkRegistryHealth(mainRoot, "diff");
13669
+ if (bad)
13670
+ return bad;
13125
13671
  const entry = await getSessionWorktree(sessionId, mainRoot);
13126
13672
  if (!entry) {
13127
13673
  return { ok: false, action: "diff", error: `session ${sessionId} 没有绑定 worktree` };
@@ -13222,8 +13768,8 @@ async function execute12(input) {
13222
13768
  import { z as z13 } from "zod";
13223
13769
 
13224
13770
  // lib/plan-store.ts
13225
- import { promises as fs12 } from "node:fs";
13226
- import * as path15 from "node:path";
13771
+ import { promises as fs13 } from "node:fs";
13772
+ import * as path16 from "node:path";
13227
13773
  var INDEX_VERSION = 1;
13228
13774
 
13229
13775
  class PlanStore {
@@ -13232,8 +13778,8 @@ class PlanStore {
13232
13778
  now;
13233
13779
  secondCounters = new Map;
13234
13780
  constructor(opts = {}) {
13235
- this.root = path15.resolve(opts.root ?? process.cwd());
13236
- this.base = opts.base ? path15.resolve(opts.base) : plansDir(this.root);
13781
+ this.root = path16.resolve(opts.root ?? process.cwd());
13782
+ this.base = opts.base ? path16.resolve(opts.base) : plansDir(this.root);
13237
13783
  this.now = opts.now ?? (() => new Date);
13238
13784
  }
13239
13785
  async write(input) {
@@ -13243,14 +13789,14 @@ class PlanStore {
13243
13789
  if (typeof input.content !== "string" || input.content.length === 0) {
13244
13790
  throw new Error("PlanStore.write: content 不能为空");
13245
13791
  }
13246
- await fs12.mkdir(this.base, { recursive: true });
13792
+ await fs13.mkdir(this.base, { recursive: true });
13247
13793
  const lockPath = this.lockPath();
13248
13794
  return await withFileLock(lockPath, async () => {
13249
13795
  const index = await this.readIndexLocked();
13250
13796
  const now = this.now();
13251
13797
  const planId = this.allocPlanId(now, index);
13252
13798
  const filename = this.composeFilename(planId, input.title);
13253
- const absFile = path15.join(this.base, filename);
13799
+ const absFile = path16.join(this.base, filename);
13254
13800
  await this.atomicWriteFile(absFile, input.content);
13255
13801
  const entry = {
13256
13802
  plan_id: planId,
@@ -13274,9 +13820,9 @@ class PlanStore {
13274
13820
  const entry = index.entries.find((e) => e.plan_id === planId);
13275
13821
  if (!entry)
13276
13822
  return null;
13277
- const abs = path15.join(this.base, entry.path);
13823
+ const abs = path16.join(this.base, entry.path);
13278
13824
  try {
13279
- const content = await fs12.readFile(abs, "utf8");
13825
+ const content = await fs13.readFile(abs, "utf8");
13280
13826
  return { entry, content };
13281
13827
  } catch (err) {
13282
13828
  const e = err;
@@ -13335,7 +13881,7 @@ class PlanStore {
13335
13881
  else if (e.status === "orphan")
13336
13882
  shouldDelete = true;
13337
13883
  if (shouldDelete) {
13338
- await fs12.rm(path15.join(this.base, e.path), { force: true }).catch(() => {});
13884
+ await fs13.rm(path16.join(this.base, e.path), { force: true }).catch(() => {});
13339
13885
  removed++;
13340
13886
  } else {
13341
13887
  keep.push(e);
@@ -13357,9 +13903,9 @@ class PlanStore {
13357
13903
  knownPaths.add(e.path);
13358
13904
  if (e.status !== "active")
13359
13905
  continue;
13360
- const abs = path15.join(this.base, e.path);
13906
+ const abs = path16.join(this.base, e.path);
13361
13907
  try {
13362
- await fs12.stat(abs);
13908
+ await fs13.stat(abs);
13363
13909
  } catch {
13364
13910
  e.status = "orphan";
13365
13911
  markedOrphan++;
@@ -13369,23 +13915,23 @@ class PlanStore {
13369
13915
  await this.writeIndexLocked(index);
13370
13916
  let unindexedFiles = [];
13371
13917
  try {
13372
- const all = await fs12.readdir(this.base);
13918
+ const all = await fs13.readdir(this.base);
13373
13919
  unindexedFiles = all.filter((f) => f.endsWith(".md")).filter((f) => !knownPaths.has(f));
13374
13920
  } catch {}
13375
13921
  return { markedOrphan, unindexedFiles };
13376
13922
  });
13377
13923
  }
13378
13924
  indexPath() {
13379
- return path15.join(this.base, "index.json");
13925
+ return path16.join(this.base, "index.json");
13380
13926
  }
13381
13927
  lockPath() {
13382
- return path15.join(this.base, "index.lock");
13928
+ return path16.join(this.base, "index.lock");
13383
13929
  }
13384
13930
  async readIndexLocked() {
13385
13931
  const file = this.indexPath();
13386
13932
  let raw;
13387
13933
  try {
13388
- raw = await fs12.readFile(file, "utf8");
13934
+ raw = await fs13.readFile(file, "utf8");
13389
13935
  } catch (err) {
13390
13936
  const e = err;
13391
13937
  if (e.code === "ENOENT")
@@ -13407,7 +13953,7 @@ class PlanStore {
13407
13953
  async archiveCorruptIndex(file) {
13408
13954
  const ts = this.now().toISOString().replace(/[-:.TZ]/g, "").slice(0, 14);
13409
13955
  const dst = `${file}.corrupt-${ts}`;
13410
- await fs12.rename(file, dst).catch(() => {});
13956
+ await fs13.rename(file, dst).catch(() => {});
13411
13957
  }
13412
13958
  async readIndex() {
13413
13959
  return this.readIndexLocked();
@@ -13452,15 +13998,15 @@ class PlanStore {
13452
13998
  const tsPart = m ? `${m[1]}-${m[2]}` : planId;
13453
13999
  const nnn = m ? m[3] : "000";
13454
14000
  const sample = planFilePath(this.root, title);
13455
- const base = path15.basename(sample, ".md");
14001
+ const base = path16.basename(sample, ".md");
13456
14002
  const slug = base.replace(/^\d{8}-\d{6}-?/, "") || "untitled";
13457
14003
  return `${tsPart}-${nnn}-${slug}.md`;
13458
14004
  }
13459
14005
  async atomicWriteFile(file, data) {
13460
- await fs12.mkdir(path15.dirname(file), { recursive: true });
14006
+ await fs13.mkdir(path16.dirname(file), { recursive: true });
13461
14007
  const tmp = `${file}.tmp-${process.pid}-${Date.now()}`;
13462
- await fs12.writeFile(tmp, data, "utf8");
13463
- await fs12.rename(tmp, file);
14008
+ await fs13.writeFile(tmp, data, "utf8");
14009
+ await fs13.rename(tmp, file);
13464
14010
  }
13465
14011
  }
13466
14012
  function formatTimestamp(d) {
@@ -13523,8 +14069,8 @@ async function execute13(input) {
13523
14069
  }
13524
14070
  }
13525
14071
  // tools/plan-read.ts
13526
- import { promises as fs13 } from "node:fs";
13527
- import * as path16 from "node:path";
14072
+ import { promises as fs14 } from "node:fs";
14073
+ import * as path17 from "node:path";
13528
14074
  import { z as z14 } from "zod";
13529
14075
  var description14 = [
13530
14076
  "读取方案文档内容,支持按 plan_id 或绝对路径查询。",
@@ -13630,9 +14176,9 @@ async function execute14(input) {
13630
14176
  };
13631
14177
  }
13632
14178
  }
13633
- const abs = path16.resolve(args.path);
14179
+ const abs = path17.resolve(args.path);
13634
14180
  try {
13635
- const content = await fs13.readFile(abs, "utf8");
14181
+ const content = await fs14.readFile(abs, "utf8");
13636
14182
  return {
13637
14183
  ok: true,
13638
14184
  content,
@@ -13657,16 +14203,16 @@ import { z as z15 } from "zod";
13657
14203
  // lib/adr-init.ts
13658
14204
  import { spawnSync } from "node:child_process";
13659
14205
  import { existsSync as existsSync4, promises as fsp } from "node:fs";
13660
- import * as path17 from "node:path";
14206
+ import * as path18 from "node:path";
13661
14207
  import * as url from "node:url";
13662
14208
  function resolveAssetsRoot() {
13663
- const here = path17.dirname(url.fileURLToPath(import.meta.url));
14209
+ const here = path18.dirname(url.fileURLToPath(import.meta.url));
13664
14210
  let dir = here;
13665
14211
  for (let i = 0;i < 6; i++) {
13666
- if (existsSync4(path17.join(dir, "package.json")) && existsSync4(path17.join(dir, "assets", "adr-init"))) {
13667
- return path17.join(dir, "assets", "adr-init");
14212
+ if (existsSync4(path18.join(dir, "package.json")) && existsSync4(path18.join(dir, "assets", "adr-init"))) {
14213
+ return path18.join(dir, "assets", "adr-init");
13668
14214
  }
13669
- const parent = path17.dirname(dir);
14215
+ const parent = path18.dirname(dir);
13670
14216
  if (parent === dir)
13671
14217
  break;
13672
14218
  dir = parent;
@@ -13674,13 +14220,13 @@ function resolveAssetsRoot() {
13674
14220
  const xdgConfig = process.env["XDG_CONFIG_HOME"];
13675
14221
  const homeDir = process.env["HOME"] ?? process.env["USERPROFILE"] ?? "";
13676
14222
  const fallbackRoots = [
13677
- xdgConfig ? path17.join(xdgConfig, "opencode") : null,
13678
- path17.join(homeDir, ".config", "opencode"),
13679
- process.env["APPDATA"] ? path17.join(process.env["APPDATA"], "opencode") : null,
13680
- process.env["LOCALAPPDATA"] ? path17.join(process.env["LOCALAPPDATA"], "opencode") : null
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
13681
14227
  ].filter(Boolean);
13682
14228
  for (const root of fallbackRoots) {
13683
- const candidate = path17.join(root, "assets", "adr-init");
14229
+ const candidate = path18.join(root, "assets", "adr-init");
13684
14230
  if (existsSync4(candidate)) {
13685
14231
  return candidate;
13686
14232
  }
@@ -13717,7 +14263,7 @@ function runGitConfigHooksPath(cwd) {
13717
14263
  }
13718
14264
  }
13719
14265
  async function runAdrInit(opts = {}) {
13720
- const cwd = path17.resolve(opts.cwd ?? process.cwd());
14266
+ const cwd = path18.resolve(opts.cwd ?? process.cwd());
13721
14267
  const force = !!opts.force;
13722
14268
  const dryRun = !!opts.dryRun;
13723
14269
  const writePrepare = !!opts.writePrepare;
@@ -13766,8 +14312,8 @@ async function runAdrInit(opts = {}) {
13766
14312
  });
13767
14313
  }
13768
14314
  for (const item of plan) {
13769
- const srcAbs = path17.join(assetsRoot, item.src);
13770
- const dstAbs = path17.join(cwd, item.dst);
14315
+ const srcAbs = path18.join(assetsRoot, item.src);
14316
+ const dstAbs = path18.join(cwd, item.dst);
13771
14317
  if (!existsSync4(srcAbs)) {
13772
14318
  result.warnings.push(`资产缺失:${item.src}(跳过 ${item.dst})`);
13773
14319
  continue;
@@ -13780,7 +14326,7 @@ async function runAdrInit(opts = {}) {
13780
14326
  const bakRel = `${item.dst}.bak.${ts}`;
13781
14327
  if (!dryRun) {
13782
14328
  try {
13783
- await fsp.copyFile(dstAbs, path17.join(cwd, bakRel));
14329
+ await fsp.copyFile(dstAbs, path18.join(cwd, bakRel));
13784
14330
  } catch (e) {
13785
14331
  result.ok = false;
13786
14332
  result.reason = "io_error";
@@ -13792,7 +14338,7 @@ async function runAdrInit(opts = {}) {
13792
14338
  }
13793
14339
  if (!dryRun) {
13794
14340
  try {
13795
- await fsp.mkdir(path17.dirname(dstAbs), { recursive: true });
14341
+ await fsp.mkdir(path18.dirname(dstAbs), { recursive: true });
13796
14342
  await fsp.copyFile(srcAbs, dstAbs);
13797
14343
  if (item.chmod !== undefined) {
13798
14344
  try {
@@ -13822,7 +14368,7 @@ async function runAdrInit(opts = {}) {
13822
14368
  } else {
13823
14369
  result.suggestions.push("[dry-run] 将运行:git config core.hooksPath .githooks");
13824
14370
  }
13825
- const pkgPath = path17.join(cwd, "package.json");
14371
+ const pkgPath = path18.join(cwd, "package.json");
13826
14372
  const isNpm = existsSync4(pkgPath);
13827
14373
  if (isNpm) {
13828
14374
  if (writePrepare) {
@@ -13837,7 +14383,7 @@ async function runAdrInit(opts = {}) {
13837
14383
  const bakRel = `package.json.bak.${ts}`;
13838
14384
  if (!dryRun) {
13839
14385
  try {
13840
- await fsp.copyFile(pkgPath, path17.join(cwd, bakRel));
14386
+ await fsp.copyFile(pkgPath, path18.join(cwd, bakRel));
13841
14387
  } catch (e) {
13842
14388
  result.warnings.push(`备份 package.json 失败:${e instanceof Error ? e.message : String(e)}`);
13843
14389
  }
@@ -13960,12 +14506,12 @@ async function execute15(input) {
13960
14506
  import { z as z16 } from "zod";
13961
14507
 
13962
14508
  // lib/opencode-session-probe.ts
13963
- import * as path18 from "node:path";
14509
+ import * as path19 from "node:path";
13964
14510
  import * as os5 from "node:os";
13965
14511
  import { createRequire as createRequire2 } from "node:module";
13966
14512
  var requireFromHere = createRequire2(import.meta.url);
13967
14513
  var DEFAULT_LIVENESS_MS = 6 * 60 * 60000;
13968
- var DEFAULT_DB_PATH = path18.join(os5.homedir(), ".local/share/opencode/opencode.db");
14514
+ var DEFAULT_DB_PATH = path19.join(os5.homedir(), ".local/share/opencode/opencode.db");
13969
14515
  function createSessionProbe(opts = {}) {
13970
14516
  const dbPath = opts.dbPath ?? DEFAULT_DB_PATH;
13971
14517
  const httpBaseUrl = opts.httpBaseUrl ?? process.env["OPENCODE_SERVER_URL"];
@@ -14120,14 +14666,14 @@ function pickLastText(parts) {
14120
14666
  return "";
14121
14667
  }
14122
14668
  async function sendParentNotice(client, sessionID, text, opts = {}) {
14123
- const log4 = opts.log ?? (() => {});
14669
+ const log5 = opts.log ?? (() => {});
14124
14670
  if (!client?.session) {
14125
- log4("warn", "[sendParentNotice] client.session 不可用,noop");
14671
+ log5("warn", "[sendParentNotice] client.session 不可用,noop");
14126
14672
  return false;
14127
14673
  }
14128
14674
  const sessionAny = client.session;
14129
14675
  if (typeof sessionAny.promptAsync !== "function") {
14130
- log4("warn", "[sendParentNotice] promptAsync 不可用(SDK 太老?),noop");
14676
+ log5("warn", "[sendParentNotice] promptAsync 不可用(SDK 太老?),noop");
14131
14677
  return false;
14132
14678
  }
14133
14679
  try {
@@ -14148,12 +14694,12 @@ async function sendParentNotice(client, sessionID, text, opts = {}) {
14148
14694
  }
14149
14695
  });
14150
14696
  if (res && typeof res === "object" && "error" in res && res.error) {
14151
- log4("warn", "[sendParentNotice] promptAsync 返回 error", { error: res.error });
14697
+ log5("warn", "[sendParentNotice] promptAsync 返回 error", { error: res.error });
14152
14698
  return false;
14153
14699
  }
14154
14700
  return true;
14155
14701
  } catch (err) {
14156
- log4("warn", "[sendParentNotice] 抛错(已隔离)", {
14702
+ log5("warn", "[sendParentNotice] 抛错(已隔离)", {
14157
14703
  error: err instanceof Error ? err.message : String(err)
14158
14704
  });
14159
14705
  return false;
@@ -14162,219 +14708,31 @@ async function sendParentNotice(client, sessionID, text, opts = {}) {
14162
14708
 
14163
14709
  // lib/spawner-production.ts
14164
14710
  init_decision_parser();
14165
-
14166
- // lib/parent-map-store.ts
14167
- import { promises as fs14 } from "node:fs";
14168
- import * as path19 from "node:path";
14169
- var PARENT_MAP_VERSION = 1;
14170
- var PARENT_MAP_LOCK_TIMEOUT_MS = 2000;
14171
- var PARENT_MAP_MAX_ENTRIES = 256;
14172
- function parentMapDir(mainRoot) {
14173
- return path19.join(runtimeDir(path19.resolve(mainRoot), { ensure: false }), "session-worktrees");
14174
- }
14175
- function parentMapPath(mainRoot) {
14176
- return path19.join(parentMapDir(mainRoot), "parent-map.json");
14177
- }
14178
- function parentMapLockPath(mainRoot) {
14179
- return path19.join(parentMapDir(mainRoot), "parent-map.lock");
14180
- }
14181
- async function readParentMapFile(mainRoot) {
14182
- const file = parentMapPath(mainRoot);
14183
- try {
14184
- const raw = await fs14.readFile(file, "utf8");
14185
- const parsed = JSON.parse(raw);
14186
- if (parsed.version !== PARENT_MAP_VERSION || !Array.isArray(parsed.entries)) {
14187
- return { version: PARENT_MAP_VERSION, entries: [] };
14188
- }
14189
- return parsed;
14190
- } catch (err) {
14191
- const e = err;
14192
- if (e.code === "ENOENT")
14193
- return { version: PARENT_MAP_VERSION, entries: [] };
14194
- return { version: PARENT_MAP_VERSION, entries: [] };
14195
- }
14196
- }
14197
- async function writeParentMapFile(mainRoot, payload) {
14198
- const file = parentMapPath(mainRoot);
14199
- await fs14.mkdir(path19.dirname(file), { recursive: true });
14200
- const tmp = `${file}.tmp-${process.pid}-${Date.now()}`;
14201
- await fs14.writeFile(tmp, JSON.stringify(payload, null, 2), "utf8");
14202
- await fs14.rename(tmp, file);
14203
- }
14204
- function capEntriesByTsDesc(entries) {
14205
- if (entries.length <= PARENT_MAP_MAX_ENTRIES)
14206
- return entries;
14207
- return [...entries].sort((a, b) => b.ts - a.ts).slice(0, PARENT_MAP_MAX_ENTRIES);
14208
- }
14209
- async function loadParentMap(mainRoot) {
14210
- const out = new Map;
14211
- let file;
14212
- try {
14213
- file = await readParentMapFile(mainRoot);
14214
- } catch {
14215
- return out;
14216
- }
14217
- const valid = [];
14218
- for (const e of file.entries) {
14219
- if (!e || typeof e.childID !== "string" || typeof e.parentID !== "string")
14220
- continue;
14221
- if (!e.childID || !e.parentID)
14222
- continue;
14223
- const ts = typeof e.ts === "number" ? e.ts : Date.now();
14224
- valid.push({ childID: e.childID, parentID: e.parentID, ts });
14225
- }
14226
- const capped = capEntriesByTsDesc(valid);
14227
- for (const e of capped) {
14228
- out.set(e.childID, { parentID: e.parentID, ts: e.ts });
14229
- }
14230
- return out;
14231
- }
14232
- async function mutateParentMap(mainRoot, mutator, opts = {}) {
14233
- const lockPath = parentMapLockPath(mainRoot);
14234
- await fs14.mkdir(path19.dirname(lockPath), { recursive: true });
14235
- const lockOpts = {
14236
- timeoutMs: opts.timeoutMs ?? PARENT_MAP_LOCK_TIMEOUT_MS,
14237
- ...opts
14238
- };
14239
- await withFileLock(lockPath, async () => {
14240
- const current = await readParentMapFile(mainRoot);
14241
- const input = current.entries.slice();
14242
- const next = await mutator(input);
14243
- if (!Array.isArray(next))
14244
- return;
14245
- const capped = capEntriesByTsDesc(next);
14246
- await writeParentMapFile(mainRoot, { version: PARENT_MAP_VERSION, entries: capped });
14247
- }, lockOpts);
14248
- }
14249
- async function appendParentEntry(mainRoot, childID, parentID, ts, opts = {}) {
14250
- if (!childID || !parentID)
14251
- return;
14252
- const lockPath = parentMapLockPath(mainRoot);
14253
- await fs14.mkdir(path19.dirname(lockPath), { recursive: true });
14254
- const lockOpts = {
14255
- timeoutMs: opts.timeoutMs ?? PARENT_MAP_LOCK_TIMEOUT_MS,
14256
- ...opts
14257
- };
14258
- await withFileLock(lockPath, async () => {
14259
- const current = await readParentMapFile(mainRoot);
14260
- const idx = current.entries.findIndex((e) => e.childID === childID);
14261
- if (idx >= 0) {
14262
- current.entries[idx] = { childID, parentID, ts };
14263
- } else {
14264
- current.entries.push({ childID, parentID, ts });
14265
- }
14266
- const capped = capEntriesByTsDesc(current.entries);
14267
- await writeParentMapFile(mainRoot, { version: PARENT_MAP_VERSION, entries: capped });
14268
- }, lockOpts);
14269
- }
14270
-
14271
- // lib/parent-map-runtime.ts
14272
- var log4 = makePluginLogger("parent-map-runtime");
14273
- var SESSION_PARENT_MAP_TTL_MS = 30 * 60000;
14274
- var SESSION_PARENT_MAP_MAX_SIZE = PARENT_MAP_MAX_ENTRIES;
14275
- var sessionParentMap = new Map;
14276
- var _persistRoot = null;
14277
- function _setPersistRootForTests(root) {
14278
- _persistRoot = root;
14279
- }
14280
- function _bulkInjectSessionParentMap(entries) {
14281
- for (const e of entries) {
14282
- if (!e.childID || !e.parentID)
14283
- continue;
14284
- sessionParentMap.set(e.childID, { parentID: e.parentID, ts: e.ts });
14285
- }
14286
- }
14287
- function _capSessionParentMap() {
14288
- if (sessionParentMap.size <= SESSION_PARENT_MAP_MAX_SIZE)
14289
- return 0;
14290
- const sorted = [...sessionParentMap.entries()].sort((a, b) => b[1].ts - a[1].ts);
14291
- const keep = new Set(sorted.slice(0, SESSION_PARENT_MAP_MAX_SIZE).map(([k]) => k));
14292
- let removed = 0;
14293
- for (const k of [...sessionParentMap.keys()]) {
14294
- if (!keep.has(k)) {
14295
- sessionParentMap.delete(k);
14296
- removed++;
14297
- }
14298
- }
14299
- return removed;
14300
- }
14301
- function recordSessionParent(childID, parentID, now = Date.now()) {
14302
- if (!childID || !parentID)
14303
- return;
14304
- if (sessionParentMap.has(childID)) {
14305
- sessionParentMap.set(childID, { parentID, ts: now });
14306
- } else {
14307
- if (sessionParentMap.size >= SESSION_PARENT_MAP_MAX_SIZE) {
14308
- let oldestKey = null;
14309
- let oldestTs = Number.POSITIVE_INFINITY;
14310
- for (const [k, v] of sessionParentMap.entries()) {
14311
- if (v.ts < oldestTs) {
14312
- oldestTs = v.ts;
14313
- oldestKey = k;
14314
- }
14315
- }
14316
- if (oldestKey !== null)
14317
- sessionParentMap.delete(oldestKey);
14318
- }
14319
- sessionParentMap.set(childID, { parentID, ts: now });
14320
- }
14321
- if (_persistRoot) {
14322
- appendParentEntry(_persistRoot, childID, parentID, now).catch((err) => {
14323
- log4.warn("appendParentEntry 失败(已隔离)", {
14324
- error: err instanceof Error ? err.message : String(err),
14325
- childID
14326
- });
14327
- });
14328
- }
14329
- }
14330
- function lookupParentSessionId(childID, now = Date.now()) {
14331
- const entry = sessionParentMap.get(childID);
14332
- if (!entry)
14333
- return;
14334
- if (now - entry.ts > SESSION_PARENT_MAP_TTL_MS) {
14335
- sessionParentMap.delete(childID);
14336
- return;
14337
- }
14338
- return entry.parentID;
14339
- }
14340
- function deleteSessionParent(childID) {
14341
- sessionParentMap.delete(childID);
14342
- if (_persistRoot) {
14343
- mutateParentMap(_persistRoot, (entries) => entries.filter((e) => e.childID !== childID)).catch((err) => {
14344
- log4.warn("mutateParentMap (delete) 失败(已隔离)", {
14345
- error: err instanceof Error ? err.message : String(err),
14346
- childID
14347
- });
14348
- });
14349
- }
14350
- }
14351
- function sweepExpiredSessionParents(now = Date.now()) {
14352
- let removed = 0;
14353
- for (const [k, v] of [...sessionParentMap.entries()]) {
14354
- if (now - v.ts > SESSION_PARENT_MAP_TTL_MS) {
14355
- sessionParentMap.delete(k);
14356
- removed++;
14357
- }
14358
- }
14359
- if (_persistRoot) {
14360
- mutateParentMap(_persistRoot, (entries) => entries.filter((e) => now - e.ts <= SESSION_PARENT_MAP_TTL_MS)).catch((err) => {
14361
- log4.warn("mutateParentMap (sweep) 失败(已隔离)", {
14362
- error: err instanceof Error ? err.message : String(err),
14363
- removed
14364
- });
14365
- });
14366
- }
14367
- return removed;
14368
- }
14369
-
14370
- // lib/spawner-production.ts
14371
14711
  class ProductionSpawner {
14372
14712
  opts;
14373
14713
  constructor(opts) {
14374
14714
  this.opts = opts;
14375
14715
  }
14376
14716
  async spawnReviewer(args) {
14377
- const prompt = buildReviewerPrompt(args);
14717
+ let ownerSessionId = args.sessionId;
14718
+ const ownerRoot = this.opts.mainRoot ?? this.opts.directory;
14719
+ if (ownerRoot) {
14720
+ try {
14721
+ const owner = await resolveWorktreeOwner({
14722
+ sessionId: args.sessionId,
14723
+ mainRoot: ownerRoot,
14724
+ ...args.worktreePath ? { worktreePath: args.worktreePath } : {}
14725
+ });
14726
+ if (owner.ok)
14727
+ ownerSessionId = owner.ownerSessionId;
14728
+ } catch (err) {
14729
+ this.opts.log?.("warn", `[spawner] resolveWorktreeOwner 失败,回退原 sessionId`, {
14730
+ err: describe4(err),
14731
+ sessionId: args.sessionId
14732
+ });
14733
+ }
14734
+ }
14735
+ const prompt = buildReviewerPrompt({ ...args, ownerSessionId });
14378
14736
  let r;
14379
14737
  try {
14380
14738
  r = await this.runSubagent({
@@ -14535,16 +14893,17 @@ async function raceAbortTimeout(p, signal, timeoutMs, label) {
14535
14893
  });
14536
14894
  }
14537
14895
  function buildReviewerPrompt(args) {
14896
+ const ownerSessionId = args.ownerSessionId ?? args.sessionId;
14538
14897
  const lines = [
14539
14898
  "[Session Merge Review]",
14540
14899
  "",
14541
- `session_id: ${args.sessionId}`,
14900
+ `session_id: ${ownerSessionId}`,
14542
14901
  `worktree_path: ${args.worktreePath}`,
14543
14902
  `base_sha: ${args.baseSha}`
14544
14903
  ];
14545
14904
  if (args.planId)
14546
14905
  lines.push(`plan_id: ${args.planId}`);
14547
- lines.push(`round: ${args.round}/${args.maxRounds}`, "", "请按 reviewer.md 「worktree-session 审阅」模式执行:", "1. 若有 plan_id → 先 plan_read(plan_id=...) 拿方案", "2. 跑 `git -C <worktree_path> diff <base_sha>..HEAD` 看改动", '3. APPROVE 前必须先调 review_approval(verdict=APPROVE, pendingIds=["session:<session_id>"], reviewTarget="code") 写审批', " (coveredSha 工具会自动从 worktree HEAD 捕获,无需手传)", "4. 输出 ## Decision 节,首行 APPROVE / REQUEST_CHANGES / BLOCK 之一");
14906
+ lines.push(`round: ${args.round}/${args.maxRounds}`, "", "请按 reviewer.md 「worktree-session 审阅」模式执行:", "1. 若有 plan_id → 先 plan_read(plan_id=...) 拿方案", "2. 跑 `git -C <worktree_path> diff <base_sha>..HEAD` 看改动", '3. APPROVE 前必须先调 review_approval(verdict=APPROVE, pendingIds=["session:<session_id>"], reviewTarget="code", worktreePath="<worktree_path>") 写审批', " (coveredSha 工具会自动从 worktree HEAD 捕获,无需手传)", "4. 输出 ## Decision 节,首行 APPROVE / REQUEST_CHANGES / BLOCK 之一");
14548
14907
  if (args.prevSummary) {
14549
14908
  lines.push("", "## 上一轮 reviewer 意见", "", args.prevSummary, "", "请确认 coder 是否已按意见修复");
14550
14909
  }
@@ -18903,7 +19262,7 @@ import * as https from "node:https";
18903
19262
  // lib/version-injected.ts
18904
19263
  function getInjectedVersion() {
18905
19264
  try {
18906
- const v = "0.7.10";
19265
+ const v = "0.8.0";
18907
19266
  if (typeof v === "string" && /^\d+\.\d+\.\d+/.test(v)) {
18908
19267
  return v;
18909
19268
  }
@@ -20745,6 +21104,32 @@ var pruneRunning = false;
20745
21104
  var _pruneTimer;
20746
21105
  var _activateGeneration = 0;
20747
21106
  var _probe = null;
21107
+ var _registryUnreadableNotified = false;
21108
+ async function maybeNotifyRegistryUnreadable(client, rec, prune) {
21109
+ const unreadable = rec.registryUnreadable ?? prune.registryUnreadable;
21110
+ if (!unreadable) {
21111
+ _registryUnreadableNotified = false;
21112
+ return;
21113
+ }
21114
+ safeWriteLog(PLUGIN_NAME22, {
21115
+ hook: "registry-unreadable",
21116
+ reason: unreadable.reason,
21117
+ detail: unreadable.detail,
21118
+ path: unreadable.path,
21119
+ action: "skip-cleanup-fail-closed"
21120
+ });
21121
+ if (_registryUnreadableNotified)
21122
+ return;
21123
+ _registryUnreadableNotified = true;
21124
+ const msg = summarizeReconcileDigest(rec, {
21125
+ cleaned: [],
21126
+ failed: [],
21127
+ ...prune.registryUnreadable ? { registryUnreadable: prune.registryUnreadable } : {}
21128
+ });
21129
+ if (!msg)
21130
+ return;
21131
+ await showToast2(client, { message: msg, variant: "warning", duration: 1e4, title: "CodeForge" }, log14).catch(() => {});
21132
+ }
20748
21133
  var log14 = makePluginLogger(PLUGIN_NAME22);
20749
21134
  var worktreeLifecyclePlugin = async (ctx) => {
20750
21135
  const rawDir = ctx.directory ?? process.cwd();
@@ -20790,6 +21175,7 @@ var worktreeLifecyclePlugin = async (ctx) => {
20790
21175
  const result = await pruneOrphanWorktrees(mainRoot, {
20791
21176
  isSessionAlive: _probe.isSessionAlive
20792
21177
  });
21178
+ await maybeNotifyRegistryUnreadable(client, rec, result);
20793
21179
  if (result.cleaned.length > 0 || result.failed.length > 0) {
20794
21180
  log14.info(`[pruneOrphan] cleaned=${result.cleaned.length} failed=${result.failed.length} skipped=${result.skipped}`);
20795
21181
  safeWriteLog(PLUGIN_NAME22, {
@@ -20820,6 +21206,7 @@ var worktreeLifecyclePlugin = async (ctx) => {
20820
21206
  const result = await pruneOrphanWorktrees(mainRoot, {
20821
21207
  isSessionAlive: _probe.isSessionAlive
20822
21208
  });
21209
+ await maybeNotifyRegistryUnreadable(client, rec, result);
20823
21210
  if (result.cleaned.length > 0 || result.failed.length > 0) {
20824
21211
  log14.info(`[pruneOrphan interval] cleaned=${result.cleaned.length} failed=${result.failed.length} skipped=${result.skipped}`);
20825
21212
  safeWriteLog(PLUGIN_NAME22, {