@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/agents/codeforge.md +6 -7
- package/agents/coder-deep.md +3 -4
- package/agents/coder-quick.md +1 -1
- package/agents/coder.md +2 -3
- package/agents/discover-challenger.md +4 -5
- package/agents/discover.md +3 -4
- package/agents/planner.md +2 -2
- package/agents/reviewer-lite.md +9 -3
- package/agents/reviewer.md +6 -4
- package/bin/codeforge.mjs +4 -3
- package/codeforge.json +37 -31
- package/dist/index.js +751 -364
- package/package.json +1 -1
- package/schemas/codeforge.schema.json +11 -1
- package/scripts/sync-agent-models.mjs +6 -0
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
|
|
10668
|
-
import * as
|
|
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 =
|
|
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
|
|
11012
|
+
return path12.join(runtimeDir(path12.resolve(mainRoot), { ensure: false }), "session-worktrees");
|
|
10794
11013
|
}
|
|
10795
11014
|
function registryPath(mainRoot) {
|
|
10796
|
-
return
|
|
11015
|
+
return path12.join(registryDir(mainRoot), "registry.json");
|
|
10797
11016
|
}
|
|
10798
11017
|
function registryLockPath(mainRoot) {
|
|
10799
|
-
return
|
|
11018
|
+
return path12.join(registryDir(mainRoot), "registry.lock");
|
|
10800
11019
|
}
|
|
10801
|
-
|
|
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
|
-
|
|
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 {
|
|
10814
|
-
return {
|
|
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
|
|
11097
|
+
await fs10.mkdir(path12.dirname(file), { recursive: true });
|
|
10820
11098
|
const tmp = `${file}.tmp-${process.pid}-${Date.now()}`;
|
|
10821
|
-
await
|
|
10822
|
-
await
|
|
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
|
|
11134
|
+
await fs10.mkdir(path12.dirname(lockPath), { recursive: true });
|
|
10827
11135
|
return await withFileLock(lockPath, async () => {
|
|
10828
|
-
const reg = await
|
|
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 =
|
|
11146
|
+
const mainRoot = path12.resolve(opts.mainRoot);
|
|
10839
11147
|
const branch = opts.branchName ?? `codeforge/session-${opts.sessionId}`;
|
|
10840
|
-
const worktreesDir = opts.worktrees_dir ??
|
|
11148
|
+
const worktreesDir = opts.worktrees_dir ?? path12.join(mainRoot, DEFAULT_WORKTREE_SUBDIR);
|
|
10841
11149
|
const lockPath = registryLockPath(mainRoot);
|
|
10842
|
-
await
|
|
11150
|
+
await fs10.mkdir(path12.dirname(lockPath), { recursive: true });
|
|
10843
11151
|
return await withFileLock(lockPath, async () => {
|
|
10844
|
-
const reg = await
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
11413
|
+
const resolved = path12.resolve(mainRoot);
|
|
11064
11414
|
const result = {
|
|
11065
11415
|
cleanedCreating: [],
|
|
11066
11416
|
finishedRemoving: [],
|
|
11067
11417
|
keptConservative: []
|
|
11068
11418
|
};
|
|
11069
|
-
const
|
|
11070
|
-
|
|
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
|
|
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(
|
|
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 =
|
|
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((
|
|
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
|
-
|
|
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((
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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((
|
|
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
|
-
|
|
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(
|
|
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 =
|
|
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) =>
|
|
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
|
|
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 =
|
|
11816
|
+
const codeforgeWorktreeRoot = path12.resolve(path12.join(resolved, DEFAULT_WORKTREE_SUBDIR));
|
|
11404
11817
|
const fsWorktreePaths = [];
|
|
11405
11818
|
try {
|
|
11406
|
-
const names = await
|
|
11819
|
+
const names = await fs10.readdir(codeforgeWorktreeRoot);
|
|
11407
11820
|
for (const name of names) {
|
|
11408
|
-
fsWorktreePaths.push(
|
|
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) =>
|
|
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 +
|
|
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
|
|
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
|
|
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
|
|
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
|
|
11536
|
-
|
|
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
|
|
11539
|
-
|
|
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 (
|
|
11547
|
-
|
|
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
|
|
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
|
|
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
|
|
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:
|
|
12183
|
+
await el.screenshot({ path: path14 });
|
|
11716
12184
|
} else {
|
|
11717
|
-
await page.screenshot({ path:
|
|
12185
|
+
await page.screenshot({ path: path14, fullPage: opts?.fullPage });
|
|
11718
12186
|
}
|
|
11719
|
-
return { ok: true, path:
|
|
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
|
|
12037
|
-
import * as
|
|
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 =
|
|
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 =
|
|
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 =
|
|
12542
|
+
const abs = path14.resolve(root, opts.file ?? CONFIG_FILE);
|
|
12075
12543
|
let raw;
|
|
12076
12544
|
try {
|
|
12077
|
-
raw = await
|
|
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
|
|
12540
|
-
import * as
|
|
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 =
|
|
13028
|
+
const file = path15.join(mainRoot, CONFIG_REL);
|
|
12549
13029
|
let raw;
|
|
12550
13030
|
try {
|
|
12551
|
-
raw = await
|
|
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((
|
|
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
|
-
|
|
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
|
|
13226
|
-
import * as
|
|
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 =
|
|
13236
|
-
this.base = opts.base ?
|
|
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
|
|
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 =
|
|
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 =
|
|
13823
|
+
const abs = path16.join(this.base, entry.path);
|
|
13278
13824
|
try {
|
|
13279
|
-
const content = await
|
|
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
|
|
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 =
|
|
13906
|
+
const abs = path16.join(this.base, e.path);
|
|
13361
13907
|
try {
|
|
13362
|
-
await
|
|
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
|
|
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
|
|
13925
|
+
return path16.join(this.base, "index.json");
|
|
13380
13926
|
}
|
|
13381
13927
|
lockPath() {
|
|
13382
|
-
return
|
|
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
|
|
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
|
|
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 =
|
|
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
|
|
14006
|
+
await fs13.mkdir(path16.dirname(file), { recursive: true });
|
|
13461
14007
|
const tmp = `${file}.tmp-${process.pid}-${Date.now()}`;
|
|
13462
|
-
await
|
|
13463
|
-
await
|
|
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
|
|
13527
|
-
import * as
|
|
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 =
|
|
14179
|
+
const abs = path17.resolve(args.path);
|
|
13634
14180
|
try {
|
|
13635
|
-
const content = await
|
|
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
|
|
14206
|
+
import * as path18 from "node:path";
|
|
13661
14207
|
import * as url from "node:url";
|
|
13662
14208
|
function resolveAssetsRoot() {
|
|
13663
|
-
const here =
|
|
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(
|
|
13667
|
-
return
|
|
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 =
|
|
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 ?
|
|
13678
|
-
|
|
13679
|
-
process.env["APPDATA"] ?
|
|
13680
|
-
process.env["LOCALAPPDATA"] ?
|
|
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 =
|
|
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 =
|
|
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 =
|
|
13770
|
-
const dstAbs =
|
|
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,
|
|
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(
|
|
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 =
|
|
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,
|
|
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
|
|
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 =
|
|
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
|
|
14669
|
+
const log5 = opts.log ?? (() => {});
|
|
14124
14670
|
if (!client?.session) {
|
|
14125
|
-
|
|
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
|
-
|
|
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
|
-
|
|
14697
|
+
log5("warn", "[sendParentNotice] promptAsync 返回 error", { error: res.error });
|
|
14152
14698
|
return false;
|
|
14153
14699
|
}
|
|
14154
14700
|
return true;
|
|
14155
14701
|
} catch (err) {
|
|
14156
|
-
|
|
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
|
-
|
|
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: ${
|
|
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.
|
|
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, {
|