@andyqiu/codeforge 0.5.2 → 0.5.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/codeforge.mjs +2 -1
- package/commands/autonomy.md +62 -0
- package/commands/pause.md +18 -0
- package/dist/index.js +384 -35
- package/package.json +1 -1
package/bin/codeforge.mjs
CHANGED
|
@@ -316,7 +316,8 @@ function cmdUpgrade(args) {
|
|
|
316
316
|
log(`步骤 2/2:codeforge install --global`)
|
|
317
317
|
|
|
318
318
|
if (!dryRun) {
|
|
319
|
-
const
|
|
319
|
+
const skipBuildFlag = process.platform === "win32" ? "-SkipBuild" : "--skip-build"
|
|
320
|
+
const code = installOpencode({ scope: "global", dryRun: false, extraArgs: [skipBuildFlag] })
|
|
320
321
|
if (code !== 0) {
|
|
321
322
|
err(`install --global 失败 (exit=${code})`)
|
|
322
323
|
return 1
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: autonomy
|
|
3
|
+
description: 查看或切换当前 session 的自主度档位(step/semi/full/auto)
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
查看或切换当前 session 的自主执行模式(autonomy)。
|
|
7
|
+
|
|
8
|
+
## 用法
|
|
9
|
+
|
|
10
|
+
- `/autonomy` — 显示当前档位和 budget 状态
|
|
11
|
+
- `/autonomy step` — 每个工具调用都要确认
|
|
12
|
+
- `/autonomy semi` — 危险工具确认,安全工具自动(默认)
|
|
13
|
+
- `/autonomy full` — 全自动工具执行(reviewer APPROVE 后仍需 `/merge` 手动触发)
|
|
14
|
+
- `/autonomy auto` — 全自动 + APPROVE 自动 merge(**最小干预模式**)
|
|
15
|
+
|
|
16
|
+
## ⚠️ auto 模式须知
|
|
17
|
+
|
|
18
|
+
切到 `auto` 等同 plandex `auto` 档位 —— **最小干预**而非零干预:
|
|
19
|
+
|
|
20
|
+
- reviewer **APPROVE** 后自动合并,无需手动 `/merge`
|
|
21
|
+
- **双重验证**:必须 reviewer 摘要 Decision=APPROVE **且** `approval-store` 存在对应 APPROVE 记录,缺一不可
|
|
22
|
+
- 任一失败 → fail-safe(不 merge,emit `autonomous.decision.parse_error` event + toast)
|
|
23
|
+
- **REQUEST_CHANGES** 自动循环(≤ 3 次),仍由 codeforge agent 派 coder 修
|
|
24
|
+
- **BLOCK** 通过 channels 通知(需配置 `.codeforge/channels.json`)
|
|
25
|
+
- channels 全失败时 fallback 写 `<runtimeDir>/sessions/autonomous-blocks.ndjson`
|
|
26
|
+
- **仅根 session 生效**(子 session 一律跳过,防 orchestrator 嵌套)
|
|
27
|
+
|
|
28
|
+
### 默认预算上限(任一耗尽自动降回 semi)
|
|
29
|
+
|
|
30
|
+
| 维度 | 默认值 |
|
|
31
|
+
|---|---|
|
|
32
|
+
| 总 tokens | 50,000 |
|
|
33
|
+
| 总 USD | $3 |
|
|
34
|
+
| Wall clock | 30 分钟 |
|
|
35
|
+
| auto merge 次数 | 5 |
|
|
36
|
+
| REQUEST_CHANGES 循环 | 3 |
|
|
37
|
+
|
|
38
|
+
### 逃生口
|
|
39
|
+
|
|
40
|
+
- `/pause` — 立刻退回 semi(不打断在途 merge)
|
|
41
|
+
- `/discard-session` — 不可逆,硬丢 worktree + branch
|
|
42
|
+
- budget 耗尽 — 自动降回 semi
|
|
43
|
+
- BLOCK — 强制通知 + 降档
|
|
44
|
+
|
|
45
|
+
## 行为对比
|
|
46
|
+
|
|
47
|
+
| 场景 | step | semi | full | auto |
|
|
48
|
+
|---|---|---|---|---|
|
|
49
|
+
| bash / edit 执行 | confirm | confirm | auto | auto |
|
|
50
|
+
| read / search 执行 | confirm | auto | auto | auto |
|
|
51
|
+
| reviewer APPROVE 后 | 手动 `/merge` | 手动 `/merge` | 手动 `/merge` | **自动 merge** |
|
|
52
|
+
| reviewer BLOCK 后 | 用户处理 | 用户处理 | 用户处理 | **channels 通知 + 降档** |
|
|
53
|
+
|
|
54
|
+
## 配置存储
|
|
55
|
+
|
|
56
|
+
`<runtimeDir>/autonomy/<sessionId>.json`(withFileLock 跨进程并发安全)
|
|
57
|
+
|
|
58
|
+
## 关联 ADR
|
|
59
|
+
|
|
60
|
+
- [autonomous-mode](../docs/adr/autonomous-mode.md) — 主 ADR(设计、双重验证、五维预算)
|
|
61
|
+
- [plandex-three-autonomy-modes](../docs/adr/plandex-three-autonomy-modes.md) — 三档基线
|
|
62
|
+
- [worktree-session-isolation](../docs/adr/worktree-session-isolation.md) — merge 闭环
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: pause
|
|
3
|
+
description: 立刻暂停当前 session 的 auto 模式,降回 semi
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
暂停当前 session 的自主执行模式(auto),降回 semi 模式。
|
|
7
|
+
|
|
8
|
+
**行为说明**:
|
|
9
|
+
|
|
10
|
+
- 如果按下时**正在进行自动 merge**,当次 merge 会**继续跑完**(`session_merge` 是原子事务:worktree commit + squash + commit + cleanup 一气呵成,无法中段抢断)
|
|
11
|
+
- `paused` 标志设置后,driver 会在**下一次** `task.completed` / `session.idle` 事件时生效
|
|
12
|
+
- 如需**立刻放弃**所有改动,请使用 `/discard-session`(不可逆,硬丢 worktree)
|
|
13
|
+
|
|
14
|
+
**恢复 auto 模式**:使用 `/autonomy auto`(会自动清除 paused 标志,允许 driver 重新激活)
|
|
15
|
+
|
|
16
|
+
**配置存储**:`<runtimeDir>/autonomy/<sessionId>.json`,跨进程并发安全(withFileLock)
|
|
17
|
+
|
|
18
|
+
**关联 ADR**:[autonomous-mode](../docs/adr/autonomous-mode.md)
|
package/dist/index.js
CHANGED
|
@@ -76,6 +76,9 @@ function planFilePath(absRoot, title) {
|
|
|
76
76
|
const slug = titleToSlug(title);
|
|
77
77
|
return normalize2(path2.join(dir, `${ts}-${slug}.md`));
|
|
78
78
|
}
|
|
79
|
+
function subagentLogPath(absRoot, parentID, childID) {
|
|
80
|
+
return normalize2(path2.join(runtimeDir(absRoot, { ensure: false }), "subagents", parentID, `${childID}.log`));
|
|
81
|
+
}
|
|
79
82
|
function normalize2(p) {
|
|
80
83
|
return path2.normalize(path2.resolve(p));
|
|
81
84
|
}
|
|
@@ -168,6 +171,20 @@ function globalConfigDir(env = process.env) {
|
|
|
168
171
|
function projectConfigDir(root = process.cwd()) {
|
|
169
172
|
return path3.join(root, ".codeforge");
|
|
170
173
|
}
|
|
174
|
+
function getCodeforgeConfig(opts = {}) {
|
|
175
|
+
const root = opts.root ?? process.cwd();
|
|
176
|
+
const env = opts.env ?? process.env;
|
|
177
|
+
const cacheKey = `codeforge:${root}`;
|
|
178
|
+
const cached = cacheGet(cacheKey);
|
|
179
|
+
if (cached !== undefined)
|
|
180
|
+
return cached;
|
|
181
|
+
const builtin = {};
|
|
182
|
+
const globalCfg = readJsonObject(path3.join(globalConfigDir(env), "codeforge.json"));
|
|
183
|
+
const projectCfg = readJsonObject(path3.join(projectConfigDir(root), "codeforge.json"));
|
|
184
|
+
const merged = { ...builtin, ...globalCfg, ...projectCfg };
|
|
185
|
+
cacheSet(cacheKey, merged);
|
|
186
|
+
return merged;
|
|
187
|
+
}
|
|
171
188
|
function getKhConfig(opts = {}) {
|
|
172
189
|
const root = opts.root ?? process.cwd();
|
|
173
190
|
const env = opts.env ?? process.env;
|
|
@@ -1021,6 +1038,14 @@ var init_autonomy = __esm(() => {
|
|
|
1021
1038
|
read: "auto",
|
|
1022
1039
|
search: "auto",
|
|
1023
1040
|
other: "auto"
|
|
1041
|
+
},
|
|
1042
|
+
auto: {
|
|
1043
|
+
bash: "auto",
|
|
1044
|
+
edit: "auto",
|
|
1045
|
+
webfetch: "auto",
|
|
1046
|
+
read: "auto",
|
|
1047
|
+
search: "auto",
|
|
1048
|
+
other: "auto"
|
|
1024
1049
|
}
|
|
1025
1050
|
};
|
|
1026
1051
|
RISK_PATTERNS = [
|
|
@@ -13361,6 +13386,20 @@ async function markInterruptedDirty(opts) {
|
|
|
13361
13386
|
}
|
|
13362
13387
|
});
|
|
13363
13388
|
}
|
|
13389
|
+
async function getCurrentWorktreeHead(worktreePath) {
|
|
13390
|
+
try {
|
|
13391
|
+
return (await runGit2(path13.resolve(worktreePath), ["rev-parse", "HEAD"])).trim();
|
|
13392
|
+
} catch {
|
|
13393
|
+
return "";
|
|
13394
|
+
}
|
|
13395
|
+
}
|
|
13396
|
+
async function getCurrentMainHead(mainRoot) {
|
|
13397
|
+
try {
|
|
13398
|
+
return (await runGit2(path13.resolve(mainRoot), ["rev-parse", "HEAD"])).trim();
|
|
13399
|
+
} catch {
|
|
13400
|
+
return "";
|
|
13401
|
+
}
|
|
13402
|
+
}
|
|
13364
13403
|
async function isWorktreeDirty(worktreePath) {
|
|
13365
13404
|
try {
|
|
13366
13405
|
const out = (await runGit2(worktreePath, ["status", "--porcelain"])).trim();
|
|
@@ -17797,6 +17836,10 @@ var modelFallbackServer = async (ctx) => {
|
|
|
17797
17836
|
var handler12 = modelFallbackServer;
|
|
17798
17837
|
|
|
17799
17838
|
// plugins/subtask-heartbeat.ts
|
|
17839
|
+
import { promises as fsPromises } from "node:fs";
|
|
17840
|
+
import * as path18 from "node:path";
|
|
17841
|
+
init_runtime_paths();
|
|
17842
|
+
init_global_config();
|
|
17800
17843
|
var PLUGIN_NAME13 = "subtask-heartbeat";
|
|
17801
17844
|
logLifecycle(PLUGIN_NAME13, "import", {});
|
|
17802
17845
|
var HEARTBEAT_INTERVAL_MS2 = 30000;
|
|
@@ -17807,11 +17850,75 @@ var PENDING_TASK_TTL_MS = 60000;
|
|
|
17807
17850
|
var PENDING_TASK_MAX_PARENTS = 64;
|
|
17808
17851
|
var PENDING_TASK_MAX_PER_PARENT = 16;
|
|
17809
17852
|
var DESCRIPTION_MAX_LEN = 60;
|
|
17853
|
+
var SESSION_PARENT_MAP_TTL_MS = 30 * 60000;
|
|
17854
|
+
var SESSION_PARENT_MAP_MAX_SIZE = 256;
|
|
17855
|
+
var PARENT_PARSE_FAIL_MAX_LOG = 10;
|
|
17810
17856
|
var inflight3 = new Map;
|
|
17811
17857
|
var pendingTask = new Map;
|
|
17858
|
+
var sessionParentMap = new Map;
|
|
17859
|
+
var _parentParseFailLogged = 0;
|
|
17812
17860
|
function _snapshotInflight() {
|
|
17813
17861
|
return [...inflight3.values()].map((r) => ({ ...r }));
|
|
17814
17862
|
}
|
|
17863
|
+
function recordSessionParent(childID, parentID, now = Date.now()) {
|
|
17864
|
+
if (!childID || !parentID)
|
|
17865
|
+
return;
|
|
17866
|
+
if (sessionParentMap.has(childID)) {
|
|
17867
|
+
sessionParentMap.set(childID, { parentID, ts: now });
|
|
17868
|
+
return;
|
|
17869
|
+
}
|
|
17870
|
+
if (sessionParentMap.size >= SESSION_PARENT_MAP_MAX_SIZE) {
|
|
17871
|
+
let oldestKey = null;
|
|
17872
|
+
let oldestTs = Number.POSITIVE_INFINITY;
|
|
17873
|
+
for (const [k, v] of sessionParentMap.entries()) {
|
|
17874
|
+
if (v.ts < oldestTs) {
|
|
17875
|
+
oldestTs = v.ts;
|
|
17876
|
+
oldestKey = k;
|
|
17877
|
+
}
|
|
17878
|
+
}
|
|
17879
|
+
if (oldestKey !== null)
|
|
17880
|
+
sessionParentMap.delete(oldestKey);
|
|
17881
|
+
}
|
|
17882
|
+
sessionParentMap.set(childID, { parentID, ts: now });
|
|
17883
|
+
}
|
|
17884
|
+
function lookupParentSessionId(childID, now = Date.now()) {
|
|
17885
|
+
const entry = sessionParentMap.get(childID);
|
|
17886
|
+
if (!entry)
|
|
17887
|
+
return;
|
|
17888
|
+
if (now - entry.ts > SESSION_PARENT_MAP_TTL_MS) {
|
|
17889
|
+
sessionParentMap.delete(childID);
|
|
17890
|
+
return;
|
|
17891
|
+
}
|
|
17892
|
+
return entry.parentID;
|
|
17893
|
+
}
|
|
17894
|
+
function deleteSessionParent(childID) {
|
|
17895
|
+
sessionParentMap.delete(childID);
|
|
17896
|
+
}
|
|
17897
|
+
function sweepExpiredSessionParents(now = Date.now()) {
|
|
17898
|
+
let removed = 0;
|
|
17899
|
+
for (const [k, v] of [...sessionParentMap.entries()]) {
|
|
17900
|
+
if (now - v.ts > SESSION_PARENT_MAP_TTL_MS) {
|
|
17901
|
+
sessionParentMap.delete(k);
|
|
17902
|
+
removed++;
|
|
17903
|
+
}
|
|
17904
|
+
}
|
|
17905
|
+
return removed;
|
|
17906
|
+
}
|
|
17907
|
+
function detectUnparsedParentID(event) {
|
|
17908
|
+
if (!event || typeof event !== "object")
|
|
17909
|
+
return false;
|
|
17910
|
+
const e = event;
|
|
17911
|
+
if (e.type !== "session.created")
|
|
17912
|
+
return false;
|
|
17913
|
+
if (extractCreatedChild(event))
|
|
17914
|
+
return false;
|
|
17915
|
+
try {
|
|
17916
|
+
const json = JSON.stringify(event);
|
|
17917
|
+
return /\bparent[_-]?[iI][dD]\b/.test(json);
|
|
17918
|
+
} catch {
|
|
17919
|
+
return false;
|
|
17920
|
+
}
|
|
17921
|
+
}
|
|
17815
17922
|
function getInflightSnapshot() {
|
|
17816
17923
|
return _snapshotInflight();
|
|
17817
17924
|
}
|
|
@@ -17928,7 +18035,8 @@ function registerInflight(payload, now = Date.now()) {
|
|
|
17928
18035
|
description: payload.description ?? null,
|
|
17929
18036
|
startedAt: now,
|
|
17930
18037
|
lastBeatAt: now,
|
|
17931
|
-
lastTool: null
|
|
18038
|
+
lastTool: null,
|
|
18039
|
+
pendingCalls: new Map
|
|
17932
18040
|
};
|
|
17933
18041
|
inflight3.set(payload.childID, r);
|
|
17934
18042
|
return r;
|
|
@@ -18027,6 +18135,94 @@ function buildEndToast(r, type, now = Date.now()) {
|
|
|
18027
18135
|
variant
|
|
18028
18136
|
};
|
|
18029
18137
|
}
|
|
18138
|
+
function sanitizeInputSummary(toolName, args) {
|
|
18139
|
+
if (!args || typeof args !== "object")
|
|
18140
|
+
return `${toolName} (no args)`;
|
|
18141
|
+
const a = args;
|
|
18142
|
+
const PATH_KEYS = [
|
|
18143
|
+
"path",
|
|
18144
|
+
"file",
|
|
18145
|
+
"filePath",
|
|
18146
|
+
"file_path",
|
|
18147
|
+
"filepath",
|
|
18148
|
+
"target"
|
|
18149
|
+
];
|
|
18150
|
+
for (const k of PATH_KEYS) {
|
|
18151
|
+
const v = a[k];
|
|
18152
|
+
if (typeof v === "string" && v.length > 0 && v.length <= 200) {
|
|
18153
|
+
return `${toolName} ${v}`;
|
|
18154
|
+
}
|
|
18155
|
+
}
|
|
18156
|
+
const cmd = a["command"];
|
|
18157
|
+
if (typeof cmd === "string" && cmd.length > 0) {
|
|
18158
|
+
const head = cmd.replace(/\s+/g, " ").trim().slice(0, 60);
|
|
18159
|
+
return `${toolName} $ ${head}`;
|
|
18160
|
+
}
|
|
18161
|
+
return `${toolName} (no path)`;
|
|
18162
|
+
}
|
|
18163
|
+
function buildBeforeLogLine(toolName, args, now = Date.now()) {
|
|
18164
|
+
return JSON.stringify({
|
|
18165
|
+
ts: Math.floor(now / 1000),
|
|
18166
|
+
tool: toolName,
|
|
18167
|
+
phase: "before",
|
|
18168
|
+
input_summary: sanitizeInputSummary(toolName, args)
|
|
18169
|
+
});
|
|
18170
|
+
}
|
|
18171
|
+
function buildAfterLogLine(toolName, ok, durationMs, now = Date.now()) {
|
|
18172
|
+
return JSON.stringify({
|
|
18173
|
+
ts: Math.floor(now / 1000),
|
|
18174
|
+
tool: toolName,
|
|
18175
|
+
phase: "after",
|
|
18176
|
+
ok,
|
|
18177
|
+
duration_ms: durationMs
|
|
18178
|
+
});
|
|
18179
|
+
}
|
|
18180
|
+
function buildSuccessNotice(r, logPath, now = Date.now()) {
|
|
18181
|
+
const agent = r.agent ? titleCase(r.agent) : "subagent";
|
|
18182
|
+
const elapsed = fmtElapsed(now - r.startedAt);
|
|
18183
|
+
return [`✅ ${agent} 完成(${elapsed})`, `\uD83D\uDCC4 完整日志: ${logPath}`].join(`
|
|
18184
|
+
`);
|
|
18185
|
+
}
|
|
18186
|
+
function buildFailureNotice(r, endedType, logPath, worktreePath, now = Date.now()) {
|
|
18187
|
+
const agent = r.agent ? titleCase(r.agent) : "subagent";
|
|
18188
|
+
const elapsed = fmtElapsed(now - r.startedAt);
|
|
18189
|
+
const verb = endedType === "session.deleted" ? "被取消" : "失败";
|
|
18190
|
+
const lines = [
|
|
18191
|
+
`❌ ${agent} ${verb}(${endedType}, ${elapsed})`,
|
|
18192
|
+
`\uD83D\uDCC4 完整日志: ${logPath}`
|
|
18193
|
+
];
|
|
18194
|
+
if (worktreePath)
|
|
18195
|
+
lines.push(`\uD83D\uDD0D worktree 保留: ${worktreePath}`);
|
|
18196
|
+
lines.push(`\uD83D\uDCA1 排查: cat ${logPath} | tail -50`);
|
|
18197
|
+
return lines.join(`
|
|
18198
|
+
`);
|
|
18199
|
+
}
|
|
18200
|
+
async function appendSubagentLog(filePath, line, log7) {
|
|
18201
|
+
try {
|
|
18202
|
+
await fsPromises.mkdir(path18.dirname(filePath), { recursive: true });
|
|
18203
|
+
await fsPromises.appendFile(filePath, line + `
|
|
18204
|
+
`, "utf8");
|
|
18205
|
+
} catch (err) {
|
|
18206
|
+
log7?.debug?.("appendSubagentLog 失败(已隔离)", {
|
|
18207
|
+
error: err instanceof Error ? err.message : String(err),
|
|
18208
|
+
file: filePath
|
|
18209
|
+
});
|
|
18210
|
+
}
|
|
18211
|
+
}
|
|
18212
|
+
function isLogPersistenceEnabled(cwd) {
|
|
18213
|
+
try {
|
|
18214
|
+
const cfg = getCodeforgeConfig({ root: cwd });
|
|
18215
|
+
const runtime = cfg.runtime;
|
|
18216
|
+
if (runtime && typeof runtime === "object") {
|
|
18217
|
+
const v = runtime.subagent_log_persistence;
|
|
18218
|
+
if (v === false)
|
|
18219
|
+
return false;
|
|
18220
|
+
}
|
|
18221
|
+
return true;
|
|
18222
|
+
} catch {
|
|
18223
|
+
return true;
|
|
18224
|
+
}
|
|
18225
|
+
}
|
|
18030
18226
|
function normalizeVariant2(raw) {
|
|
18031
18227
|
if (raw === "info" || raw === "warning")
|
|
18032
18228
|
return "default";
|
|
@@ -18061,12 +18257,17 @@ var subtaskHeartbeatServer = async (ctx) => {
|
|
|
18061
18257
|
intervalMs: HEARTBEAT_INTERVAL_MS2
|
|
18062
18258
|
});
|
|
18063
18259
|
const client = ctx.client;
|
|
18260
|
+
const cwd = ctx.directory;
|
|
18064
18261
|
const interval = setInterval(() => {
|
|
18065
18262
|
safeAsync(PLUGIN_NAME13, "interval", async () => {
|
|
18066
18263
|
const swept = sweepExpiredPendingTasks();
|
|
18067
18264
|
if (swept > 0) {
|
|
18068
18265
|
safeWriteLog(PLUGIN_NAME13, { hook: "interval", pending_task_swept: swept });
|
|
18069
18266
|
}
|
|
18267
|
+
const sweptParents = sweepExpiredSessionParents();
|
|
18268
|
+
if (sweptParents > 0) {
|
|
18269
|
+
safeWriteLog(PLUGIN_NAME13, { hook: "interval", session_parent_swept: sweptParents });
|
|
18270
|
+
}
|
|
18070
18271
|
const beats = pickHeartbeats();
|
|
18071
18272
|
if (beats.length === 0)
|
|
18072
18273
|
return;
|
|
@@ -18091,8 +18292,29 @@ var subtaskHeartbeatServer = async (ctx) => {
|
|
|
18091
18292
|
return {
|
|
18092
18293
|
event: async ({ event }) => {
|
|
18093
18294
|
await safeAsync(PLUGIN_NAME13, "event", async () => {
|
|
18295
|
+
try {
|
|
18296
|
+
if (detectUnparsedParentID(event) && _parentParseFailLogged < PARENT_PARSE_FAIL_MAX_LOG) {
|
|
18297
|
+
_parentParseFailLogged++;
|
|
18298
|
+
log7.warn("session.created 含 parentID 关键字但 extractCreatedChild 解析失败,可能 SDK schema 变更", {
|
|
18299
|
+
sample_count: _parentParseFailLogged,
|
|
18300
|
+
hint: "如频繁出现请检查 plugins/subtask-heartbeat.ts::extractCreatedChild 是否适配新 SDK"
|
|
18301
|
+
});
|
|
18302
|
+
safeWriteLog(PLUGIN_NAME13, {
|
|
18303
|
+
hook: "event",
|
|
18304
|
+
type: "session.created.unparsed-parent-id",
|
|
18305
|
+
sample_count: _parentParseFailLogged
|
|
18306
|
+
});
|
|
18307
|
+
}
|
|
18308
|
+
} catch {}
|
|
18094
18309
|
const created = extractCreatedChild(event);
|
|
18095
18310
|
if (created) {
|
|
18311
|
+
try {
|
|
18312
|
+
recordSessionParent(created.childID, created.parentID);
|
|
18313
|
+
} catch (err) {
|
|
18314
|
+
log7.warn("recordSessionParent 抛错(已隔离)", {
|
|
18315
|
+
error: err instanceof Error ? err.message : String(err)
|
|
18316
|
+
});
|
|
18317
|
+
}
|
|
18096
18318
|
const pending = dequeuePendingTask(created.parentID);
|
|
18097
18319
|
const record = registerInflight({
|
|
18098
18320
|
childID: created.childID,
|
|
@@ -18122,6 +18344,11 @@ var subtaskHeartbeatServer = async (ctx) => {
|
|
|
18122
18344
|
}
|
|
18123
18345
|
const ended = extractEndedSessionID(event);
|
|
18124
18346
|
if (ended) {
|
|
18347
|
+
if (ended.type === "session.deleted") {
|
|
18348
|
+
try {
|
|
18349
|
+
deleteSessionParent(ended.sessionID);
|
|
18350
|
+
} catch {}
|
|
18351
|
+
}
|
|
18125
18352
|
const r = clearInflight2(ended.sessionID);
|
|
18126
18353
|
if (r) {
|
|
18127
18354
|
const t = buildEndToast(r, ended.type);
|
|
@@ -18134,6 +18361,32 @@ var subtaskHeartbeatServer = async (ctx) => {
|
|
|
18134
18361
|
toast_sent: sent,
|
|
18135
18362
|
end_toast_message: t.message
|
|
18136
18363
|
});
|
|
18364
|
+
const logPath = subagentLogPath(cwd, r.parentID, r.childID);
|
|
18365
|
+
const isSuccess = ended.type === "session.idle";
|
|
18366
|
+
let text;
|
|
18367
|
+
if (isSuccess) {
|
|
18368
|
+
text = buildSuccessNotice(r, logPath);
|
|
18369
|
+
} else {
|
|
18370
|
+
let worktreePath = null;
|
|
18371
|
+
try {
|
|
18372
|
+
const entry = await getSessionWorktree(r.childID, cwd);
|
|
18373
|
+
worktreePath = entry?.worktreePath ?? null;
|
|
18374
|
+
} catch {}
|
|
18375
|
+
text = buildFailureNotice(r, ended.type, logPath, worktreePath);
|
|
18376
|
+
}
|
|
18377
|
+
const ocClient = ctx.client;
|
|
18378
|
+
const noticeSent = await sendParentNotice(ocClient, r.parentID, text, {
|
|
18379
|
+
directory: cwd,
|
|
18380
|
+
log: (lvl, msg, data) => log7[lvl === "info" ? "info" : "warn"](msg, data)
|
|
18381
|
+
});
|
|
18382
|
+
safeWriteLog(PLUGIN_NAME13, {
|
|
18383
|
+
hook: "event",
|
|
18384
|
+
type: `${ended.type}.notice`,
|
|
18385
|
+
child: r.childID,
|
|
18386
|
+
parent: r.parentID,
|
|
18387
|
+
notice_sent: noticeSent,
|
|
18388
|
+
success: isSuccess
|
|
18389
|
+
});
|
|
18137
18390
|
}
|
|
18138
18391
|
}
|
|
18139
18392
|
});
|
|
@@ -18167,8 +18420,44 @@ var subtaskHeartbeatServer = async (ctx) => {
|
|
|
18167
18420
|
});
|
|
18168
18421
|
}
|
|
18169
18422
|
}
|
|
18170
|
-
|
|
18423
|
+
const rec = inflight3.get(input.sessionID);
|
|
18424
|
+
if (rec) {
|
|
18171
18425
|
recordToolBeat(input.sessionID, input.tool);
|
|
18426
|
+
if (!rec.pendingCalls)
|
|
18427
|
+
rec.pendingCalls = new Map;
|
|
18428
|
+
if (typeof input.callID === "string") {
|
|
18429
|
+
rec.pendingCalls.set(input.callID, Date.now());
|
|
18430
|
+
}
|
|
18431
|
+
if (isLogPersistenceEnabled(cwd)) {
|
|
18432
|
+
const args = output?.args ?? null;
|
|
18433
|
+
const line = buildBeforeLogLine(input.tool, args);
|
|
18434
|
+
const file = subagentLogPath(cwd, rec.parentID, rec.childID);
|
|
18435
|
+
appendSubagentLog(file, line, log7);
|
|
18436
|
+
}
|
|
18437
|
+
}
|
|
18438
|
+
});
|
|
18439
|
+
},
|
|
18440
|
+
"tool.execute.after": async (input, _output) => {
|
|
18441
|
+
if (inflight3.size === 0)
|
|
18442
|
+
return;
|
|
18443
|
+
await safeAsync(PLUGIN_NAME13, "tool.execute.after", async () => {
|
|
18444
|
+
if (!input || typeof input.sessionID !== "string" || typeof input.tool !== "string")
|
|
18445
|
+
return;
|
|
18446
|
+
const rec = inflight3.get(input.sessionID);
|
|
18447
|
+
if (!rec)
|
|
18448
|
+
return;
|
|
18449
|
+
let durationMs = 0;
|
|
18450
|
+
if (rec.pendingCalls && typeof input.callID === "string") {
|
|
18451
|
+
const startedAt = rec.pendingCalls.get(input.callID);
|
|
18452
|
+
if (typeof startedAt === "number") {
|
|
18453
|
+
durationMs = Date.now() - startedAt;
|
|
18454
|
+
rec.pendingCalls.delete(input.callID);
|
|
18455
|
+
}
|
|
18456
|
+
}
|
|
18457
|
+
if (isLogPersistenceEnabled(cwd)) {
|
|
18458
|
+
const line = buildAfterLogLine(input.tool, true, durationMs);
|
|
18459
|
+
const file = subagentLogPath(cwd, rec.parentID, rec.childID);
|
|
18460
|
+
appendSubagentLog(file, line, log7);
|
|
18172
18461
|
}
|
|
18173
18462
|
});
|
|
18174
18463
|
}
|
|
@@ -18325,20 +18614,20 @@ function loadAgentToolsMap(rootDir, opts = {}) {
|
|
|
18325
18614
|
for (const entry of entries) {
|
|
18326
18615
|
if (!entry.endsWith(".md"))
|
|
18327
18616
|
continue;
|
|
18328
|
-
const
|
|
18617
|
+
const path19 = join15(dir, entry);
|
|
18329
18618
|
let content;
|
|
18330
18619
|
try {
|
|
18331
|
-
content = reader(
|
|
18620
|
+
content = reader(path19);
|
|
18332
18621
|
} catch (err) {
|
|
18333
18622
|
log8.warn(`agent.md 读取失败(已跳过)`, {
|
|
18334
|
-
path:
|
|
18623
|
+
path: path19,
|
|
18335
18624
|
error: err instanceof Error ? err.message : String(err)
|
|
18336
18625
|
});
|
|
18337
18626
|
continue;
|
|
18338
18627
|
}
|
|
18339
18628
|
const parsed = parseAgentFrontmatter(content);
|
|
18340
18629
|
if (!parsed) {
|
|
18341
|
-
log8.warn(`agent frontmatter 解析失败(已跳过)`, { path:
|
|
18630
|
+
log8.warn(`agent frontmatter 解析失败(已跳过)`, { path: path19 });
|
|
18342
18631
|
continue;
|
|
18343
18632
|
}
|
|
18344
18633
|
if (result.has(parsed.name))
|
|
@@ -18531,7 +18820,7 @@ var handler16 = async (_ctx3) => {
|
|
|
18531
18820
|
// lib/event-stream.ts
|
|
18532
18821
|
import { promises as fs15 } from "node:fs";
|
|
18533
18822
|
init_runtime_paths();
|
|
18534
|
-
import * as
|
|
18823
|
+
import * as path19 from "node:path";
|
|
18535
18824
|
async function loadSession(id, opts = {}) {
|
|
18536
18825
|
const file = resolveSessionFile(id, opts);
|
|
18537
18826
|
const raw = await fs15.readFile(file, "utf8");
|
|
@@ -18551,7 +18840,7 @@ async function listSessions(opts = {}) {
|
|
|
18551
18840
|
for (const e of entries) {
|
|
18552
18841
|
if (!e.isFile() || !e.name.endsWith(".jsonl"))
|
|
18553
18842
|
continue;
|
|
18554
|
-
const file =
|
|
18843
|
+
const file = path19.join(dir, e.name);
|
|
18555
18844
|
const id = e.name.replace(/\.jsonl$/, "");
|
|
18556
18845
|
try {
|
|
18557
18846
|
const stat = await fs15.stat(file);
|
|
@@ -18578,11 +18867,11 @@ async function listSessions(opts = {}) {
|
|
|
18578
18867
|
return out;
|
|
18579
18868
|
}
|
|
18580
18869
|
function resolveDir(opts = {}) {
|
|
18581
|
-
const root =
|
|
18582
|
-
return opts.sessions_dir ?
|
|
18870
|
+
const root = path19.resolve(opts.root ?? process.cwd());
|
|
18871
|
+
return opts.sessions_dir ? path19.resolve(root, opts.sessions_dir) : path19.join(runtimeDir(root), "sessions");
|
|
18583
18872
|
}
|
|
18584
18873
|
function resolveSessionFile(id, opts = {}) {
|
|
18585
|
-
return
|
|
18874
|
+
return path19.join(resolveDir(opts), `${id}.jsonl`);
|
|
18586
18875
|
}
|
|
18587
18876
|
function parseJsonl(id, raw) {
|
|
18588
18877
|
const events = [];
|
|
@@ -18939,7 +19228,7 @@ var handler17 = sessionRecoveryServer;
|
|
|
18939
19228
|
|
|
18940
19229
|
// plugins/subtasks.ts
|
|
18941
19230
|
import { promises as fs16 } from "node:fs";
|
|
18942
|
-
import * as
|
|
19231
|
+
import * as path20 from "node:path";
|
|
18943
19232
|
|
|
18944
19233
|
// lib/parallel-merge.ts
|
|
18945
19234
|
init_autonomy();
|
|
@@ -19764,7 +20053,7 @@ function sleep2(ms) {
|
|
|
19764
20053
|
init_runtime_paths();
|
|
19765
20054
|
var PLUGIN_NAME18 = "subtasks";
|
|
19766
20055
|
function getLogFile(root = process.cwd()) {
|
|
19767
|
-
return
|
|
20056
|
+
return path20.join(runtimeDir(root), "logs", "subtasks.log");
|
|
19768
20057
|
}
|
|
19769
20058
|
var VERB_RE = /^([a-zA-Z]{3,12})/;
|
|
19770
20059
|
var CN_VERBS = [
|
|
@@ -20069,7 +20358,7 @@ async function writeLog(level, msg, data) {
|
|
|
20069
20358
|
`;
|
|
20070
20359
|
try {
|
|
20071
20360
|
const logFile = getLogFile();
|
|
20072
|
-
await fs16.mkdir(
|
|
20361
|
+
await fs16.mkdir(path20.dirname(logFile), { recursive: true });
|
|
20073
20362
|
await fs16.appendFile(logFile, line, "utf8");
|
|
20074
20363
|
} catch {}
|
|
20075
20364
|
}
|
|
@@ -20587,11 +20876,11 @@ var handler20 = tokenManagerServer;
|
|
|
20587
20876
|
|
|
20588
20877
|
// plugins/tool-policy.ts
|
|
20589
20878
|
import { promises as fs17 } from "node:fs";
|
|
20590
|
-
import * as
|
|
20879
|
+
import * as path22 from "node:path";
|
|
20591
20880
|
init_autonomy();
|
|
20592
20881
|
|
|
20593
20882
|
// lib/file-regex-acl.ts
|
|
20594
|
-
import * as
|
|
20883
|
+
import * as path21 from "node:path";
|
|
20595
20884
|
function compileRule(r) {
|
|
20596
20885
|
if (r instanceof RegExp)
|
|
20597
20886
|
return r;
|
|
@@ -20657,7 +20946,7 @@ function normalizePath2(p) {
|
|
|
20657
20946
|
let s = p.replace(/\\/g, "/");
|
|
20658
20947
|
if (s.startsWith("./"))
|
|
20659
20948
|
s = s.slice(2);
|
|
20660
|
-
s =
|
|
20949
|
+
s = path21.posix.normalize(s);
|
|
20661
20950
|
return s;
|
|
20662
20951
|
}
|
|
20663
20952
|
function checkFileAccess(acl, file, op) {
|
|
@@ -20767,9 +21056,9 @@ function decideToolCall(ctx, cfg = {}, currentAgent) {
|
|
|
20767
21056
|
action = "deny";
|
|
20768
21057
|
return { action, reasons, autonomy: a, acl: aclResults };
|
|
20769
21058
|
}
|
|
20770
|
-
var POLICY_PATH =
|
|
21059
|
+
var POLICY_PATH = path22.join(".codeforge", "policy.json");
|
|
20771
21060
|
async function loadPolicy(root = process.cwd()) {
|
|
20772
|
-
const file =
|
|
21061
|
+
const file = path22.join(root, POLICY_PATH);
|
|
20773
21062
|
try {
|
|
20774
21063
|
const raw = await fs17.readFile(file, "utf8");
|
|
20775
21064
|
const data = JSON.parse(raw);
|
|
@@ -20898,7 +21187,7 @@ import {
|
|
|
20898
21187
|
writeFileSync as writeFileSync2
|
|
20899
21188
|
} from "node:fs";
|
|
20900
21189
|
import { homedir as homedir7, tmpdir } from "node:os";
|
|
20901
|
-
import { dirname as
|
|
21190
|
+
import { dirname as dirname11, join as join19 } from "node:path";
|
|
20902
21191
|
import { fileURLToPath } from "node:url";
|
|
20903
21192
|
import * as https from "node:https";
|
|
20904
21193
|
import * as zlib from "node:zlib";
|
|
@@ -20906,7 +21195,7 @@ import * as zlib from "node:zlib";
|
|
|
20906
21195
|
// lib/version-injected.ts
|
|
20907
21196
|
function getInjectedVersion() {
|
|
20908
21197
|
try {
|
|
20909
|
-
const v = "0.5.
|
|
21198
|
+
const v = "0.5.4";
|
|
20910
21199
|
if (typeof v === "string" && /^\d+\.\d+\.\d+/.test(v)) {
|
|
20911
21200
|
return v;
|
|
20912
21201
|
}
|
|
@@ -20995,7 +21284,7 @@ function readLocalVersion() {
|
|
|
20995
21284
|
return injected;
|
|
20996
21285
|
try {
|
|
20997
21286
|
const here = fileURLToPath(import.meta.url);
|
|
20998
|
-
const root =
|
|
21287
|
+
const root = dirname11(dirname11(here));
|
|
20999
21288
|
const pkg = JSON.parse(readFileSync5(join19(root, "package.json"), "utf8"));
|
|
21000
21289
|
return typeof pkg.version === "string" ? pkg.version : "0.0.0";
|
|
21001
21290
|
} catch {
|
|
@@ -21024,7 +21313,7 @@ function readCache(file) {
|
|
|
21024
21313
|
}
|
|
21025
21314
|
function writeCache(file, entry) {
|
|
21026
21315
|
try {
|
|
21027
|
-
mkdirSync3(
|
|
21316
|
+
mkdirSync3(dirname11(file), { recursive: true });
|
|
21028
21317
|
writeFileSync2(file, JSON.stringify(entry, null, 2), "utf8");
|
|
21029
21318
|
} catch {}
|
|
21030
21319
|
}
|
|
@@ -21210,7 +21499,7 @@ function extractTarToDir(tarBuf, destRoot) {
|
|
|
21210
21499
|
if (typeFlag === "0" || typeFlag === "" || typeFlag === "\x00") {
|
|
21211
21500
|
const fileBuf = tarBuf.subarray(offset, offset + size);
|
|
21212
21501
|
const dest = join19(destRoot, fullName);
|
|
21213
|
-
mkdirSync3(
|
|
21502
|
+
mkdirSync3(dirname11(dest), { recursive: true });
|
|
21214
21503
|
writeFileSync2(dest, fileBuf);
|
|
21215
21504
|
} else if (typeFlag === "5") {
|
|
21216
21505
|
mkdirSync3(join19(destRoot, fullName), { recursive: true });
|
|
@@ -21266,7 +21555,7 @@ function atomicReplaceBundle(opts) {
|
|
|
21266
21555
|
if (!existsSync4(source)) {
|
|
21267
21556
|
throw new Error(`atomic_source_missing: ${source}`);
|
|
21268
21557
|
}
|
|
21269
|
-
mkdirSync3(
|
|
21558
|
+
mkdirSync3(dirname11(target), { recursive: true });
|
|
21270
21559
|
const newPath = `${target}.new`;
|
|
21271
21560
|
const backupPath = `${target}.bak.${oldVersion}`;
|
|
21272
21561
|
let strategy = "rename";
|
|
@@ -21318,7 +21607,7 @@ function cleanupOldBackups(target, keep) {
|
|
|
21318
21607
|
if (keep <= 0)
|
|
21319
21608
|
return;
|
|
21320
21609
|
try {
|
|
21321
|
-
const dir =
|
|
21610
|
+
const dir = dirname11(target);
|
|
21322
21611
|
const base = target.substring(dir.length + 1);
|
|
21323
21612
|
const prefix = `${base}.bak.`;
|
|
21324
21613
|
const all = readdirSync3(dir).filter((f) => f.startsWith(prefix)).map((f) => {
|
|
@@ -21369,7 +21658,7 @@ function loadCompatibility(opts) {
|
|
|
21369
21658
|
function inferPluginRoot() {
|
|
21370
21659
|
try {
|
|
21371
21660
|
const here = fileURLToPath(import.meta.url);
|
|
21372
|
-
return
|
|
21661
|
+
return dirname11(dirname11(here));
|
|
21373
21662
|
} catch {
|
|
21374
21663
|
return null;
|
|
21375
21664
|
}
|
|
@@ -21632,11 +21921,11 @@ async function postToast(ctx, message) {
|
|
|
21632
21921
|
var handler22 = updateCheckerServer;
|
|
21633
21922
|
|
|
21634
21923
|
// plugins/workflow-engine.ts
|
|
21635
|
-
import * as
|
|
21924
|
+
import * as path24 from "node:path";
|
|
21636
21925
|
|
|
21637
21926
|
// lib/workflow-loader.ts
|
|
21638
21927
|
import { promises as fs18 } from "node:fs";
|
|
21639
|
-
import * as
|
|
21928
|
+
import * as path23 from "node:path";
|
|
21640
21929
|
import { z as z32 } from "zod";
|
|
21641
21930
|
var ActionSchema = z32.object({
|
|
21642
21931
|
tool: z32.string().min(1, "action.tool 不能为空"),
|
|
@@ -21749,7 +22038,7 @@ async function loadWorkflowsFromDir(dir) {
|
|
|
21749
22038
|
continue;
|
|
21750
22039
|
if (!/\.ya?ml$/i.test(name))
|
|
21751
22040
|
continue;
|
|
21752
|
-
const full =
|
|
22041
|
+
const full = path23.join(dir, name);
|
|
21753
22042
|
const r = await loadWorkflowFromFile(full);
|
|
21754
22043
|
if (r.ok)
|
|
21755
22044
|
loaded.push(r);
|
|
@@ -22139,7 +22428,7 @@ async function handleCommandInvoked(raw, workflowsDir = "workflows") {
|
|
|
22139
22428
|
}
|
|
22140
22429
|
var workflowEngineServer = async (ctx) => {
|
|
22141
22430
|
const directory = ctx.directory ?? process.cwd();
|
|
22142
|
-
const workflowsDir =
|
|
22431
|
+
const workflowsDir = path24.join(directory, "workflows");
|
|
22143
22432
|
ensureRegistry(workflowsDir).catch((err) => fallbackLog2.warn(`[${PLUGIN_NAME23}] preload workflows failed`, {
|
|
22144
22433
|
error: err instanceof Error ? err.message : String(err)
|
|
22145
22434
|
}));
|
|
@@ -22183,7 +22472,7 @@ var workflowEngineServer = async (ctx) => {
|
|
|
22183
22472
|
var handler23 = workflowEngineServer;
|
|
22184
22473
|
|
|
22185
22474
|
// plugins/session-worktree-guard.ts
|
|
22186
|
-
import
|
|
22475
|
+
import path25 from "node:path";
|
|
22187
22476
|
var PLUGIN_NAME24 = "session-worktree-guard";
|
|
22188
22477
|
logLifecycle(PLUGIN_NAME24, "import", {});
|
|
22189
22478
|
var WRITE_INTENT_RE = />(?!=)|\btee\b|\brm\b|\bmv\b|\bcp\b|\bmkdir\b|\btouch\b|\bchmod\b|\bchown\b|\bln\b/;
|
|
@@ -22212,7 +22501,7 @@ var WRITE_TOOLS = new Set(["write", "edit", "ast_edit"]);
|
|
|
22212
22501
|
function rewritePath(value, mainRoot, worktreeRoot) {
|
|
22213
22502
|
if (!value)
|
|
22214
22503
|
return null;
|
|
22215
|
-
const resolved =
|
|
22504
|
+
const resolved = path25.isAbsolute(value) ? value : path25.resolve(mainRoot, value);
|
|
22216
22505
|
if (resolved === mainRoot)
|
|
22217
22506
|
return worktreeRoot;
|
|
22218
22507
|
const prefix = mainRoot.endsWith("/") ? mainRoot : mainRoot + "/";
|
|
@@ -22295,6 +22584,29 @@ var sessionWorktreeGuardPlugin = async (ctx) => {
|
|
|
22295
22584
|
if (!isWriteOperation(toolName, argsObj, mainRoot)) {
|
|
22296
22585
|
return;
|
|
22297
22586
|
}
|
|
22587
|
+
try {
|
|
22588
|
+
const parentId = lookupParentSessionId(sessionId);
|
|
22589
|
+
if (parentId) {
|
|
22590
|
+
const parentEntry = await getSessionWorktree(parentId, mainRoot);
|
|
22591
|
+
if (parentEntry && parentEntry.status === "active") {
|
|
22592
|
+
entry = parentEntry;
|
|
22593
|
+
log13.debug?.(`[child-inherit] session ${sessionId} 继承父 ${parentId} 的 worktree`, { parentSessionId: parentId, worktreePath: parentEntry.worktreePath });
|
|
22594
|
+
safeWriteLog(PLUGIN_NAME24, {
|
|
22595
|
+
hook: "tool.execute.before",
|
|
22596
|
+
tool: toolName,
|
|
22597
|
+
sessionID: input.sessionID,
|
|
22598
|
+
action: "child-inherit",
|
|
22599
|
+
parent_session_id: parentId,
|
|
22600
|
+
parent_branch: parentEntry.branch,
|
|
22601
|
+
parent_worktree: parentEntry.worktreePath
|
|
22602
|
+
});
|
|
22603
|
+
}
|
|
22604
|
+
}
|
|
22605
|
+
} catch (lookupErr) {
|
|
22606
|
+
log13.debug?.("[child-inherit] lookupParentSessionId 抛错(已隔离,退回 lazy-bind)", { error: lookupErr instanceof Error ? lookupErr.message : String(lookupErr) });
|
|
22607
|
+
}
|
|
22608
|
+
}
|
|
22609
|
+
if (!entry) {
|
|
22298
22610
|
try {
|
|
22299
22611
|
entry = await bindSessionWorktree({ sessionId, mainRoot });
|
|
22300
22612
|
log13.info(`[lazy-bind] auto-created worktree for session ${sessionId}`, { branch: entry.branch, path: entry.worktreePath });
|
|
@@ -22337,8 +22649,16 @@ var sessionWorktreeGuardPlugin = async (ctx) => {
|
|
|
22337
22649
|
}
|
|
22338
22650
|
}
|
|
22339
22651
|
if (isWriteOp) {
|
|
22340
|
-
const
|
|
22341
|
-
|
|
22652
|
+
const inherited = entry.sessionId !== sessionId;
|
|
22653
|
+
const reasonBase = `[session-worktree-guard] DENIED: 当前 session 要求先调用 plan_read(plan_id="${entry.requiredPlanId}") 再执行写操作`;
|
|
22654
|
+
const reason = inherited ? `${reasonBase}
|
|
22655
|
+
[gate-deny] child session=${sessionId} 继承父 entry 但父 session planReadOk=false,父 session=${entry.sessionId} 需先调 plan_read(plan_id="${entry.requiredPlanId}")` : reasonBase;
|
|
22656
|
+
log13.warn(reason, {
|
|
22657
|
+
tool: toolName,
|
|
22658
|
+
sessionId,
|
|
22659
|
+
requiredPlanId: entry.requiredPlanId,
|
|
22660
|
+
inheritedFromParent: inherited ? entry.sessionId : undefined
|
|
22661
|
+
});
|
|
22342
22662
|
safeWriteLog(PLUGIN_NAME24, {
|
|
22343
22663
|
hook: "tool.execute.before",
|
|
22344
22664
|
tool: toolName,
|
|
@@ -22497,6 +22817,31 @@ var worktreeLifecyclePlugin = async (ctx) => {
|
|
|
22497
22817
|
});
|
|
22498
22818
|
return;
|
|
22499
22819
|
}
|
|
22820
|
+
try {
|
|
22821
|
+
const mainHead = await getCurrentMainHead(mainRoot);
|
|
22822
|
+
const worktreeHead = mainHead ? await getCurrentWorktreeHead(entry.worktreePath) : "";
|
|
22823
|
+
if (worktreeHead && worktreeHead === mainHead) {
|
|
22824
|
+
const fastDirty = await isWorktreeDirty(entry.worktreePath);
|
|
22825
|
+
if (!fastDirty) {
|
|
22826
|
+
await discardSession({ sessionId: ended.sessionID, mainRoot });
|
|
22827
|
+
safeWriteLog(PLUGIN_NAME25, {
|
|
22828
|
+
hook: "event",
|
|
22829
|
+
type: ended.type,
|
|
22830
|
+
sessionID: ended.sessionID,
|
|
22831
|
+
action: "discard-empty-fast-path",
|
|
22832
|
+
branch: entry.branch,
|
|
22833
|
+
worktreePath: entry.worktreePath
|
|
22834
|
+
});
|
|
22835
|
+
lastIdleToastAt.delete(ended.sessionID);
|
|
22836
|
+
return;
|
|
22837
|
+
}
|
|
22838
|
+
}
|
|
22839
|
+
} catch (err) {
|
|
22840
|
+
log14.warn(`[lifecycle] empty-worktree fast-path 检测失败 (回退到常规路径)`, {
|
|
22841
|
+
sessionId: ended.sessionID,
|
|
22842
|
+
error: err instanceof Error ? err.message : String(err)
|
|
22843
|
+
});
|
|
22844
|
+
}
|
|
22500
22845
|
let hadDirty = false;
|
|
22501
22846
|
try {
|
|
22502
22847
|
hadDirty = await isWorktreeDirty(entry.worktreePath);
|
|
@@ -22646,6 +22991,7 @@ function createCodeforgeServer(opts) {
|
|
|
22646
22991
|
const commandExecuteBeforeBucket = [];
|
|
22647
22992
|
const chatParamsBucket = [];
|
|
22648
22993
|
const toolExecuteBeforeBucket = [];
|
|
22994
|
+
const toolExecuteAfterBucket = [];
|
|
22649
22995
|
const chatMessagesTransformBucket = [];
|
|
22650
22996
|
const chatSystemTransformBucket = [];
|
|
22651
22997
|
const eventBucket = [];
|
|
@@ -22659,6 +23005,8 @@ function createCodeforgeServer(opts) {
|
|
|
22659
23005
|
chatParamsBucket.push(h["chat.params"]);
|
|
22660
23006
|
if (h["tool.execute.before"])
|
|
22661
23007
|
toolExecuteBeforeBucket.push(h["tool.execute.before"]);
|
|
23008
|
+
if (h["tool.execute.after"])
|
|
23009
|
+
toolExecuteAfterBucket.push(h["tool.execute.after"]);
|
|
22662
23010
|
if (h["experimental.chat.messages.transform"]) {
|
|
22663
23011
|
chatMessagesTransformBucket.push(h["experimental.chat.messages.transform"]);
|
|
22664
23012
|
}
|
|
@@ -22675,6 +23023,7 @@ function createCodeforgeServer(opts) {
|
|
|
22675
23023
|
"command.execute.before": makeSerialHook("command.execute.before", commandExecuteBeforeBucket),
|
|
22676
23024
|
"chat.params": makeSerialHook("chat.params", chatParamsBucket),
|
|
22677
23025
|
"tool.execute.before": makeSerialHook("tool.execute.before", toolExecuteBeforeBucket),
|
|
23026
|
+
"tool.execute.after": makeSerialHook("tool.execute.after", toolExecuteAfterBucket),
|
|
22678
23027
|
"experimental.chat.messages.transform": makeSerialHook("experimental.chat.messages.transform", chatMessagesTransformBucket),
|
|
22679
23028
|
"experimental.chat.system.transform": makeSerialHook("experimental.chat.system.transform", chatSystemTransformBucket),
|
|
22680
23029
|
event: async (envelope) => {
|