@andyqiu/codeforge 0.5.3 → 0.5.5
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 +2 -1
- package/commands/autonomy.md +91 -0
- package/commands/pause.md +18 -0
- package/dist/index.js +582 -63
- package/package.json +1 -1
package/agents/codeforge.md
CHANGED
|
@@ -59,7 +59,7 @@ fallback_models:
|
|
|
59
59
|
- ❌ 不允许**为并行而并行**:改同文件 / 步骤间有真实依赖时必须串行
|
|
60
60
|
- ❌ 不允许在父对话直接吐长交付物内容
|
|
61
61
|
- ❌ 不允许自动派 coder 修 BLOCK;REQUEST_CHANGES 允许自动派 coder 修,**最多 3 次**
|
|
62
|
-
-
|
|
62
|
+
- ✅ codeforge 可在 review-fix-review APPROVE 后调 `session_merge action=merge`;其他 sub-agent 调此操作会被 guard 拦截(ADR:codeforge-merge-permission);force=true 必须先告知用户并解释跳过 review 的理由
|
|
63
63
|
|
|
64
64
|
## 能力边界(场景分派表)
|
|
65
65
|
|
|
@@ -78,6 +78,7 @@ fallback_models:
|
|
|
78
78
|
| **reviewer 报 REQUEST_CHANGES(代码 review,`code_review_loop_count` < 3)** | **自动派 coder 修**(带具体到行的 reviewer 意见 + 原 plan_id + sessionId),loop +1 | ❌ 同时派多个 coder;❌ 不带 reviewer 意见 |
|
|
79
79
|
| **reviewer 报 REQUEST_CHANGES(loop = 3)** | 转告用户「reviewer 3 次仍 REQUEST_CHANGES」,问「接受 `/merge` / 手动改 / `/discard-session`」三选一 | ❌ 继续派 coder |
|
|
80
80
|
| **reviewer 报 BLOCK** | 转告用户 + 建议派 planner 重设计(带原 plan_id + BLOCK 理由),等用户拍板 | ❌ 派 coder 强行绕过 BLOCK |
|
|
81
|
+
| **review-fix-review 全部通过(APPROVE)** | codeforge orchestrator 自动调 `session_merge action=merge` 完成合入(ADR:codeforge-merge-permission);用户也可通过 `/merge` 命令触发 | ❌ force=true 不告知用户;❌ 派其他 sub-agent 调 session_merge action=merge(会被 guard 拦截) |
|
|
81
82
|
| **coder 回报「PRE 阻断、拒绝启动」** | 转告用户阻断点 + 解除路径,等用户拍板,**不自动派下一棒** | ❌ 自动重派 coder 并强塞 `pre_ack=` |
|
|
82
83
|
| 用户中途插入新需求(原 task 未结束) | 询问用户「先取消 / 等当前完 / 并行」三选一 | ❌ 默默丢弃;❌ 同时派多个不告知 |
|
|
83
84
|
| **可并行任务** | 自动判断依赖,无强依赖时自动并行调度 | ❌ 串行派 N 个独立 task |
|
|
@@ -0,0 +1,91 @@
|
|
|
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
|
+
- `/autonomy status` — **Phase 2.2** 显示实时状态(档位 / paused / 五维 budget / 全局今日 USD / block-pending)
|
|
16
|
+
|
|
17
|
+
## ⚠️ auto 模式须知
|
|
18
|
+
|
|
19
|
+
切到 `auto` 等同 plandex `auto` 档位 —— **最小干预**而非零干预:
|
|
20
|
+
|
|
21
|
+
- reviewer **APPROVE** 后自动合并,无需手动 `/merge`
|
|
22
|
+
- **双重验证**:必须 reviewer 摘要 Decision=APPROVE **且** `approval-store` 存在对应 APPROVE 记录,缺一不可
|
|
23
|
+
- 任一失败 → fail-safe(不 merge,emit `autonomous.decision.parse_error` event + toast)
|
|
24
|
+
- **REQUEST_CHANGES** 自动循环(≤ 3 次),仍由 codeforge agent 派 coder 修
|
|
25
|
+
- **BLOCK** 通过 channels 通知(需配置 `.codeforge/channels.json`)
|
|
26
|
+
- channels 全失败时 fallback 写 `<runtimeDir>/sessions/autonomous-blocks.ndjson`
|
|
27
|
+
- **Phase 2.2**:下次 session.start 时 session-recovery plugin 自动扫描并提示用户处理
|
|
28
|
+
- **仅根 session 生效**(子 session 一律跳过,防 orchestrator 嵌套)
|
|
29
|
+
|
|
30
|
+
### 默认预算上限(任一耗尽自动降回 semi)
|
|
31
|
+
|
|
32
|
+
| 维度 | 默认值 |
|
|
33
|
+
|---|---|
|
|
34
|
+
| 总 tokens | 50,000 |
|
|
35
|
+
| 总 USD | $3 |
|
|
36
|
+
| Wall clock | 30 分钟 |
|
|
37
|
+
| auto merge 次数 | 5 |
|
|
38
|
+
| REQUEST_CHANGES 循环 | 3 |
|
|
39
|
+
| **全局每日 USD(Phase 2)** | **$10**(跨 session 累计;env `CODEFORGE_AUTONOMY_GLOBAL_MAX_USD` 或 `.codeforge/autonomy.json::global_max_usd_per_day` 覆盖) |
|
|
40
|
+
|
|
41
|
+
### Phase 2.2 新增:stuck-detector
|
|
42
|
+
|
|
43
|
+
driver 在每次 reviewer 完成时喂 progress 信号:
|
|
44
|
+
- APPROVE → progress +1(任务推进)
|
|
45
|
+
- REQUEST_CHANGES → progress 0(无推进)
|
|
46
|
+
- BLOCK → progress -1(明显退步)
|
|
47
|
+
|
|
48
|
+
连续 5 轮无进展 → 自动降回 semi + emit `autonomous.stuck` channel event。
|
|
49
|
+
**用户 `/autonomy auto` 显式切回时自动 reset stuck 窗口**(清 stale 信号)。
|
|
50
|
+
|
|
51
|
+
### 逃生口
|
|
52
|
+
|
|
53
|
+
- `/pause` — 立刻退回 semi(不打断在途 merge)
|
|
54
|
+
- `/discard-session` — 不可逆,硬丢 worktree + branch
|
|
55
|
+
- budget 耗尽 — 自动降回 semi
|
|
56
|
+
- BLOCK — 强制通知 + 降档
|
|
57
|
+
- stuck 检测 — 5 轮无进展自动降档
|
|
58
|
+
|
|
59
|
+
## 行为对比
|
|
60
|
+
|
|
61
|
+
| 场景 | step | semi | full | auto |
|
|
62
|
+
|---|---|---|---|---|
|
|
63
|
+
| bash / edit 执行 | confirm | confirm | auto | auto |
|
|
64
|
+
| read / search 执行 | confirm | auto | auto | auto |
|
|
65
|
+
| reviewer APPROVE 后 | 手动 `/merge` | 手动 `/merge` | 手动 `/merge` | **自动 merge** |
|
|
66
|
+
| reviewer BLOCK 后 | 用户处理 | 用户处理 | 用户处理 | **channels 通知 + 降档** |
|
|
67
|
+
|
|
68
|
+
## 配置存储
|
|
69
|
+
|
|
70
|
+
- per-session:`<runtimeDir>/autonomy/<sessionId>.json`(withFileLock 跨进程并发安全)
|
|
71
|
+
- 全局每日 USD:`<runtimeDir>/autonomy/global-budget-<YYYY-MM-DD>.json`(withFileLock)
|
|
72
|
+
|
|
73
|
+
## 关联 ADR
|
|
74
|
+
|
|
75
|
+
- [autonomous-mode](../docs/adr/autonomous-mode.md) — 主 ADR(设计、双重验证、五维预算)
|
|
76
|
+
- [autonomy-global-budget](../docs/adr/autonomy-global-budget.md) — Phase 2 全局 USD 预算 + stuck-detector + block-pending 消费
|
|
77
|
+
- [plandex-three-autonomy-modes](../docs/adr/plandex-three-autonomy-modes.md) — 三档基线
|
|
78
|
+
- [worktree-session-isolation](../docs/adr/worktree-session-isolation.md) — merge 闭环
|
|
79
|
+
|
|
80
|
+
---
|
|
81
|
+
|
|
82
|
+
## /autonomy status 处理流程(Phase 2.2)
|
|
83
|
+
|
|
84
|
+
当用户输入 `/autonomy status`:
|
|
85
|
+
|
|
86
|
+
1. 调 `tools/autonomy-status.ts::getAutonomyStatus(<absRoot>, <sessionId>)` 拿 `AutonomyStatusReport`
|
|
87
|
+
2. 调 `renderAutonomyStatus(report)` 转 Markdown 文本
|
|
88
|
+
3. 直接输出渲染结果到对话(无需 toast / channels)
|
|
89
|
+
|
|
90
|
+
> 实现位置:`tools/autonomy-status.ts`(与 `tools/autonomy-mode.ts` 同款 ts 工具)。
|
|
91
|
+
> agent 通过 read + 内联调用使用,无需 opencode tool 注册(零侵入约束)。
|
|
@@ -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,13 @@ 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
|
+
}
|
|
13364
13396
|
async function isWorktreeDirty(worktreePath) {
|
|
13365
13397
|
try {
|
|
13366
13398
|
const out = (await runGit2(worktreePath, ["status", "--porcelain"])).trim();
|
|
@@ -13398,6 +13430,27 @@ Codeforge-Base: ${baseSha.slice(0, 12)}`;
|
|
|
13398
13430
|
return subject + body + footer;
|
|
13399
13431
|
}
|
|
13400
13432
|
var ORPHAN_GRACE_MS = 60000;
|
|
13433
|
+
async function pruneDiscardedRegistryEntries(mainRoot, opts = {}) {
|
|
13434
|
+
const keepRecent = opts.keepRecent ?? 50;
|
|
13435
|
+
if (keepRecent < 0) {
|
|
13436
|
+
throw new Error(`pruneDiscardedRegistryEntries: keepRecent 必须 ≥ 0,收到 ${keepRecent}`);
|
|
13437
|
+
}
|
|
13438
|
+
return await mutateRegistry(path13.resolve(mainRoot), (reg) => {
|
|
13439
|
+
const discarded = [];
|
|
13440
|
+
const others = [];
|
|
13441
|
+
for (const e of reg.entries) {
|
|
13442
|
+
if (e.status === "discarded")
|
|
13443
|
+
discarded.push(e);
|
|
13444
|
+
else
|
|
13445
|
+
others.push(e);
|
|
13446
|
+
}
|
|
13447
|
+
discarded.sort((a, b) => a.updatedAt < b.updatedAt ? 1 : a.updatedAt > b.updatedAt ? -1 : 0);
|
|
13448
|
+
const kept = discarded.slice(0, keepRecent);
|
|
13449
|
+
const pruned = discarded.length - kept.length;
|
|
13450
|
+
reg.entries = [...others, ...kept];
|
|
13451
|
+
return { pruned, kept: kept.length };
|
|
13452
|
+
});
|
|
13453
|
+
}
|
|
13401
13454
|
async function pruneOrphanWorktrees(mainRoot) {
|
|
13402
13455
|
const resolved = path13.resolve(mainRoot);
|
|
13403
13456
|
const cleaned = [];
|
|
@@ -13515,7 +13568,12 @@ async function pruneOrphanWorktrees(mainRoot) {
|
|
|
13515
13568
|
failed.push({ worktreePath: candidate, error: lastError ?? "unknown" });
|
|
13516
13569
|
}
|
|
13517
13570
|
}
|
|
13518
|
-
|
|
13571
|
+
let discardedPruned = 0;
|
|
13572
|
+
try {
|
|
13573
|
+
const r = await pruneDiscardedRegistryEntries(resolved);
|
|
13574
|
+
discardedPruned = r.pruned;
|
|
13575
|
+
} catch {}
|
|
13576
|
+
return { cleaned, failed, skipped, discardedPruned };
|
|
13519
13577
|
}
|
|
13520
13578
|
|
|
13521
13579
|
// lib/merge-loop.ts
|
|
@@ -17797,6 +17855,10 @@ var modelFallbackServer = async (ctx) => {
|
|
|
17797
17855
|
var handler12 = modelFallbackServer;
|
|
17798
17856
|
|
|
17799
17857
|
// plugins/subtask-heartbeat.ts
|
|
17858
|
+
import { promises as fsPromises } from "node:fs";
|
|
17859
|
+
import * as path18 from "node:path";
|
|
17860
|
+
init_runtime_paths();
|
|
17861
|
+
init_global_config();
|
|
17800
17862
|
var PLUGIN_NAME13 = "subtask-heartbeat";
|
|
17801
17863
|
logLifecycle(PLUGIN_NAME13, "import", {});
|
|
17802
17864
|
var HEARTBEAT_INTERVAL_MS2 = 30000;
|
|
@@ -17807,11 +17869,75 @@ var PENDING_TASK_TTL_MS = 60000;
|
|
|
17807
17869
|
var PENDING_TASK_MAX_PARENTS = 64;
|
|
17808
17870
|
var PENDING_TASK_MAX_PER_PARENT = 16;
|
|
17809
17871
|
var DESCRIPTION_MAX_LEN = 60;
|
|
17872
|
+
var SESSION_PARENT_MAP_TTL_MS = 30 * 60000;
|
|
17873
|
+
var SESSION_PARENT_MAP_MAX_SIZE = 256;
|
|
17874
|
+
var PARENT_PARSE_FAIL_MAX_LOG = 10;
|
|
17810
17875
|
var inflight3 = new Map;
|
|
17811
17876
|
var pendingTask = new Map;
|
|
17877
|
+
var sessionParentMap = new Map;
|
|
17878
|
+
var _parentParseFailLogged = 0;
|
|
17812
17879
|
function _snapshotInflight() {
|
|
17813
17880
|
return [...inflight3.values()].map((r) => ({ ...r }));
|
|
17814
17881
|
}
|
|
17882
|
+
function recordSessionParent(childID, parentID, now = Date.now()) {
|
|
17883
|
+
if (!childID || !parentID)
|
|
17884
|
+
return;
|
|
17885
|
+
if (sessionParentMap.has(childID)) {
|
|
17886
|
+
sessionParentMap.set(childID, { parentID, ts: now });
|
|
17887
|
+
return;
|
|
17888
|
+
}
|
|
17889
|
+
if (sessionParentMap.size >= SESSION_PARENT_MAP_MAX_SIZE) {
|
|
17890
|
+
let oldestKey = null;
|
|
17891
|
+
let oldestTs = Number.POSITIVE_INFINITY;
|
|
17892
|
+
for (const [k, v] of sessionParentMap.entries()) {
|
|
17893
|
+
if (v.ts < oldestTs) {
|
|
17894
|
+
oldestTs = v.ts;
|
|
17895
|
+
oldestKey = k;
|
|
17896
|
+
}
|
|
17897
|
+
}
|
|
17898
|
+
if (oldestKey !== null)
|
|
17899
|
+
sessionParentMap.delete(oldestKey);
|
|
17900
|
+
}
|
|
17901
|
+
sessionParentMap.set(childID, { parentID, ts: now });
|
|
17902
|
+
}
|
|
17903
|
+
function lookupParentSessionId(childID, now = Date.now()) {
|
|
17904
|
+
const entry = sessionParentMap.get(childID);
|
|
17905
|
+
if (!entry)
|
|
17906
|
+
return;
|
|
17907
|
+
if (now - entry.ts > SESSION_PARENT_MAP_TTL_MS) {
|
|
17908
|
+
sessionParentMap.delete(childID);
|
|
17909
|
+
return;
|
|
17910
|
+
}
|
|
17911
|
+
return entry.parentID;
|
|
17912
|
+
}
|
|
17913
|
+
function deleteSessionParent(childID) {
|
|
17914
|
+
sessionParentMap.delete(childID);
|
|
17915
|
+
}
|
|
17916
|
+
function sweepExpiredSessionParents(now = Date.now()) {
|
|
17917
|
+
let removed = 0;
|
|
17918
|
+
for (const [k, v] of [...sessionParentMap.entries()]) {
|
|
17919
|
+
if (now - v.ts > SESSION_PARENT_MAP_TTL_MS) {
|
|
17920
|
+
sessionParentMap.delete(k);
|
|
17921
|
+
removed++;
|
|
17922
|
+
}
|
|
17923
|
+
}
|
|
17924
|
+
return removed;
|
|
17925
|
+
}
|
|
17926
|
+
function detectUnparsedParentID(event) {
|
|
17927
|
+
if (!event || typeof event !== "object")
|
|
17928
|
+
return false;
|
|
17929
|
+
const e = event;
|
|
17930
|
+
if (e.type !== "session.created")
|
|
17931
|
+
return false;
|
|
17932
|
+
if (extractCreatedChild(event))
|
|
17933
|
+
return false;
|
|
17934
|
+
try {
|
|
17935
|
+
const json = JSON.stringify(event);
|
|
17936
|
+
return /\bparent[_-]?[iI][dD]\b/.test(json);
|
|
17937
|
+
} catch {
|
|
17938
|
+
return false;
|
|
17939
|
+
}
|
|
17940
|
+
}
|
|
17815
17941
|
function getInflightSnapshot() {
|
|
17816
17942
|
return _snapshotInflight();
|
|
17817
17943
|
}
|
|
@@ -17928,7 +18054,8 @@ function registerInflight(payload, now = Date.now()) {
|
|
|
17928
18054
|
description: payload.description ?? null,
|
|
17929
18055
|
startedAt: now,
|
|
17930
18056
|
lastBeatAt: now,
|
|
17931
|
-
lastTool: null
|
|
18057
|
+
lastTool: null,
|
|
18058
|
+
pendingCalls: new Map
|
|
17932
18059
|
};
|
|
17933
18060
|
inflight3.set(payload.childID, r);
|
|
17934
18061
|
return r;
|
|
@@ -18027,6 +18154,94 @@ function buildEndToast(r, type, now = Date.now()) {
|
|
|
18027
18154
|
variant
|
|
18028
18155
|
};
|
|
18029
18156
|
}
|
|
18157
|
+
function sanitizeInputSummary(toolName, args) {
|
|
18158
|
+
if (!args || typeof args !== "object")
|
|
18159
|
+
return `${toolName} (no args)`;
|
|
18160
|
+
const a = args;
|
|
18161
|
+
const PATH_KEYS = [
|
|
18162
|
+
"path",
|
|
18163
|
+
"file",
|
|
18164
|
+
"filePath",
|
|
18165
|
+
"file_path",
|
|
18166
|
+
"filepath",
|
|
18167
|
+
"target"
|
|
18168
|
+
];
|
|
18169
|
+
for (const k of PATH_KEYS) {
|
|
18170
|
+
const v = a[k];
|
|
18171
|
+
if (typeof v === "string" && v.length > 0 && v.length <= 200) {
|
|
18172
|
+
return `${toolName} ${v}`;
|
|
18173
|
+
}
|
|
18174
|
+
}
|
|
18175
|
+
const cmd = a["command"];
|
|
18176
|
+
if (typeof cmd === "string" && cmd.length > 0) {
|
|
18177
|
+
const head = cmd.replace(/\s+/g, " ").trim().slice(0, 60);
|
|
18178
|
+
return `${toolName} $ ${head}`;
|
|
18179
|
+
}
|
|
18180
|
+
return `${toolName} (no path)`;
|
|
18181
|
+
}
|
|
18182
|
+
function buildBeforeLogLine(toolName, args, now = Date.now()) {
|
|
18183
|
+
return JSON.stringify({
|
|
18184
|
+
ts: Math.floor(now / 1000),
|
|
18185
|
+
tool: toolName,
|
|
18186
|
+
phase: "before",
|
|
18187
|
+
input_summary: sanitizeInputSummary(toolName, args)
|
|
18188
|
+
});
|
|
18189
|
+
}
|
|
18190
|
+
function buildAfterLogLine(toolName, ok, durationMs, now = Date.now()) {
|
|
18191
|
+
return JSON.stringify({
|
|
18192
|
+
ts: Math.floor(now / 1000),
|
|
18193
|
+
tool: toolName,
|
|
18194
|
+
phase: "after",
|
|
18195
|
+
ok,
|
|
18196
|
+
duration_ms: durationMs
|
|
18197
|
+
});
|
|
18198
|
+
}
|
|
18199
|
+
function buildSuccessNotice(r, logPath, now = Date.now()) {
|
|
18200
|
+
const agent = r.agent ? titleCase(r.agent) : "subagent";
|
|
18201
|
+
const elapsed = fmtElapsed(now - r.startedAt);
|
|
18202
|
+
return [`✅ ${agent} 完成(${elapsed})`, `\uD83D\uDCC4 完整日志: ${logPath}`].join(`
|
|
18203
|
+
`);
|
|
18204
|
+
}
|
|
18205
|
+
function buildFailureNotice(r, endedType, logPath, worktreePath, now = Date.now()) {
|
|
18206
|
+
const agent = r.agent ? titleCase(r.agent) : "subagent";
|
|
18207
|
+
const elapsed = fmtElapsed(now - r.startedAt);
|
|
18208
|
+
const verb = endedType === "session.deleted" ? "被取消" : "失败";
|
|
18209
|
+
const lines = [
|
|
18210
|
+
`❌ ${agent} ${verb}(${endedType}, ${elapsed})`,
|
|
18211
|
+
`\uD83D\uDCC4 完整日志: ${logPath}`
|
|
18212
|
+
];
|
|
18213
|
+
if (worktreePath)
|
|
18214
|
+
lines.push(`\uD83D\uDD0D worktree 保留: ${worktreePath}`);
|
|
18215
|
+
lines.push(`\uD83D\uDCA1 排查: cat ${logPath} | tail -50`);
|
|
18216
|
+
return lines.join(`
|
|
18217
|
+
`);
|
|
18218
|
+
}
|
|
18219
|
+
async function appendSubagentLog(filePath, line, log7) {
|
|
18220
|
+
try {
|
|
18221
|
+
await fsPromises.mkdir(path18.dirname(filePath), { recursive: true });
|
|
18222
|
+
await fsPromises.appendFile(filePath, line + `
|
|
18223
|
+
`, "utf8");
|
|
18224
|
+
} catch (err) {
|
|
18225
|
+
log7?.debug?.("appendSubagentLog 失败(已隔离)", {
|
|
18226
|
+
error: err instanceof Error ? err.message : String(err),
|
|
18227
|
+
file: filePath
|
|
18228
|
+
});
|
|
18229
|
+
}
|
|
18230
|
+
}
|
|
18231
|
+
function isLogPersistenceEnabled(cwd) {
|
|
18232
|
+
try {
|
|
18233
|
+
const cfg = getCodeforgeConfig({ root: cwd });
|
|
18234
|
+
const runtime = cfg.runtime;
|
|
18235
|
+
if (runtime && typeof runtime === "object") {
|
|
18236
|
+
const v = runtime.subagent_log_persistence;
|
|
18237
|
+
if (v === false)
|
|
18238
|
+
return false;
|
|
18239
|
+
}
|
|
18240
|
+
return true;
|
|
18241
|
+
} catch {
|
|
18242
|
+
return true;
|
|
18243
|
+
}
|
|
18244
|
+
}
|
|
18030
18245
|
function normalizeVariant2(raw) {
|
|
18031
18246
|
if (raw === "info" || raw === "warning")
|
|
18032
18247
|
return "default";
|
|
@@ -18061,12 +18276,17 @@ var subtaskHeartbeatServer = async (ctx) => {
|
|
|
18061
18276
|
intervalMs: HEARTBEAT_INTERVAL_MS2
|
|
18062
18277
|
});
|
|
18063
18278
|
const client = ctx.client;
|
|
18279
|
+
const cwd = ctx.directory;
|
|
18064
18280
|
const interval = setInterval(() => {
|
|
18065
18281
|
safeAsync(PLUGIN_NAME13, "interval", async () => {
|
|
18066
18282
|
const swept = sweepExpiredPendingTasks();
|
|
18067
18283
|
if (swept > 0) {
|
|
18068
18284
|
safeWriteLog(PLUGIN_NAME13, { hook: "interval", pending_task_swept: swept });
|
|
18069
18285
|
}
|
|
18286
|
+
const sweptParents = sweepExpiredSessionParents();
|
|
18287
|
+
if (sweptParents > 0) {
|
|
18288
|
+
safeWriteLog(PLUGIN_NAME13, { hook: "interval", session_parent_swept: sweptParents });
|
|
18289
|
+
}
|
|
18070
18290
|
const beats = pickHeartbeats();
|
|
18071
18291
|
if (beats.length === 0)
|
|
18072
18292
|
return;
|
|
@@ -18091,8 +18311,29 @@ var subtaskHeartbeatServer = async (ctx) => {
|
|
|
18091
18311
|
return {
|
|
18092
18312
|
event: async ({ event }) => {
|
|
18093
18313
|
await safeAsync(PLUGIN_NAME13, "event", async () => {
|
|
18314
|
+
try {
|
|
18315
|
+
if (detectUnparsedParentID(event) && _parentParseFailLogged < PARENT_PARSE_FAIL_MAX_LOG) {
|
|
18316
|
+
_parentParseFailLogged++;
|
|
18317
|
+
log7.warn("session.created 含 parentID 关键字但 extractCreatedChild 解析失败,可能 SDK schema 变更", {
|
|
18318
|
+
sample_count: _parentParseFailLogged,
|
|
18319
|
+
hint: "如频繁出现请检查 plugins/subtask-heartbeat.ts::extractCreatedChild 是否适配新 SDK"
|
|
18320
|
+
});
|
|
18321
|
+
safeWriteLog(PLUGIN_NAME13, {
|
|
18322
|
+
hook: "event",
|
|
18323
|
+
type: "session.created.unparsed-parent-id",
|
|
18324
|
+
sample_count: _parentParseFailLogged
|
|
18325
|
+
});
|
|
18326
|
+
}
|
|
18327
|
+
} catch {}
|
|
18094
18328
|
const created = extractCreatedChild(event);
|
|
18095
18329
|
if (created) {
|
|
18330
|
+
try {
|
|
18331
|
+
recordSessionParent(created.childID, created.parentID);
|
|
18332
|
+
} catch (err) {
|
|
18333
|
+
log7.warn("recordSessionParent 抛错(已隔离)", {
|
|
18334
|
+
error: err instanceof Error ? err.message : String(err)
|
|
18335
|
+
});
|
|
18336
|
+
}
|
|
18096
18337
|
const pending = dequeuePendingTask(created.parentID);
|
|
18097
18338
|
const record = registerInflight({
|
|
18098
18339
|
childID: created.childID,
|
|
@@ -18122,6 +18363,11 @@ var subtaskHeartbeatServer = async (ctx) => {
|
|
|
18122
18363
|
}
|
|
18123
18364
|
const ended = extractEndedSessionID(event);
|
|
18124
18365
|
if (ended) {
|
|
18366
|
+
if (ended.type === "session.deleted") {
|
|
18367
|
+
try {
|
|
18368
|
+
deleteSessionParent(ended.sessionID);
|
|
18369
|
+
} catch {}
|
|
18370
|
+
}
|
|
18125
18371
|
const r = clearInflight2(ended.sessionID);
|
|
18126
18372
|
if (r) {
|
|
18127
18373
|
const t = buildEndToast(r, ended.type);
|
|
@@ -18134,6 +18380,32 @@ var subtaskHeartbeatServer = async (ctx) => {
|
|
|
18134
18380
|
toast_sent: sent,
|
|
18135
18381
|
end_toast_message: t.message
|
|
18136
18382
|
});
|
|
18383
|
+
const logPath = subagentLogPath(cwd, r.parentID, r.childID);
|
|
18384
|
+
const isSuccess = ended.type === "session.idle";
|
|
18385
|
+
let text;
|
|
18386
|
+
if (isSuccess) {
|
|
18387
|
+
text = buildSuccessNotice(r, logPath);
|
|
18388
|
+
} else {
|
|
18389
|
+
let worktreePath = null;
|
|
18390
|
+
try {
|
|
18391
|
+
const entry = await getSessionWorktree(r.childID, cwd);
|
|
18392
|
+
worktreePath = entry?.worktreePath ?? null;
|
|
18393
|
+
} catch {}
|
|
18394
|
+
text = buildFailureNotice(r, ended.type, logPath, worktreePath);
|
|
18395
|
+
}
|
|
18396
|
+
const ocClient = ctx.client;
|
|
18397
|
+
const noticeSent = await sendParentNotice(ocClient, r.parentID, text, {
|
|
18398
|
+
directory: cwd,
|
|
18399
|
+
log: (lvl, msg, data) => log7[lvl === "info" ? "info" : "warn"](msg, data)
|
|
18400
|
+
});
|
|
18401
|
+
safeWriteLog(PLUGIN_NAME13, {
|
|
18402
|
+
hook: "event",
|
|
18403
|
+
type: `${ended.type}.notice`,
|
|
18404
|
+
child: r.childID,
|
|
18405
|
+
parent: r.parentID,
|
|
18406
|
+
notice_sent: noticeSent,
|
|
18407
|
+
success: isSuccess
|
|
18408
|
+
});
|
|
18137
18409
|
}
|
|
18138
18410
|
}
|
|
18139
18411
|
});
|
|
@@ -18167,8 +18439,44 @@ var subtaskHeartbeatServer = async (ctx) => {
|
|
|
18167
18439
|
});
|
|
18168
18440
|
}
|
|
18169
18441
|
}
|
|
18170
|
-
|
|
18442
|
+
const rec = inflight3.get(input.sessionID);
|
|
18443
|
+
if (rec) {
|
|
18171
18444
|
recordToolBeat(input.sessionID, input.tool);
|
|
18445
|
+
if (!rec.pendingCalls)
|
|
18446
|
+
rec.pendingCalls = new Map;
|
|
18447
|
+
if (typeof input.callID === "string") {
|
|
18448
|
+
rec.pendingCalls.set(input.callID, Date.now());
|
|
18449
|
+
}
|
|
18450
|
+
if (isLogPersistenceEnabled(cwd)) {
|
|
18451
|
+
const args = output?.args ?? null;
|
|
18452
|
+
const line = buildBeforeLogLine(input.tool, args);
|
|
18453
|
+
const file = subagentLogPath(cwd, rec.parentID, rec.childID);
|
|
18454
|
+
appendSubagentLog(file, line, log7);
|
|
18455
|
+
}
|
|
18456
|
+
}
|
|
18457
|
+
});
|
|
18458
|
+
},
|
|
18459
|
+
"tool.execute.after": async (input, _output) => {
|
|
18460
|
+
if (inflight3.size === 0)
|
|
18461
|
+
return;
|
|
18462
|
+
await safeAsync(PLUGIN_NAME13, "tool.execute.after", async () => {
|
|
18463
|
+
if (!input || typeof input.sessionID !== "string" || typeof input.tool !== "string")
|
|
18464
|
+
return;
|
|
18465
|
+
const rec = inflight3.get(input.sessionID);
|
|
18466
|
+
if (!rec)
|
|
18467
|
+
return;
|
|
18468
|
+
let durationMs = 0;
|
|
18469
|
+
if (rec.pendingCalls && typeof input.callID === "string") {
|
|
18470
|
+
const startedAt = rec.pendingCalls.get(input.callID);
|
|
18471
|
+
if (typeof startedAt === "number") {
|
|
18472
|
+
durationMs = Date.now() - startedAt;
|
|
18473
|
+
rec.pendingCalls.delete(input.callID);
|
|
18474
|
+
}
|
|
18475
|
+
}
|
|
18476
|
+
if (isLogPersistenceEnabled(cwd)) {
|
|
18477
|
+
const line = buildAfterLogLine(input.tool, true, durationMs);
|
|
18478
|
+
const file = subagentLogPath(cwd, rec.parentID, rec.childID);
|
|
18479
|
+
appendSubagentLog(file, line, log7);
|
|
18172
18480
|
}
|
|
18173
18481
|
});
|
|
18174
18482
|
}
|
|
@@ -18325,20 +18633,20 @@ function loadAgentToolsMap(rootDir, opts = {}) {
|
|
|
18325
18633
|
for (const entry of entries) {
|
|
18326
18634
|
if (!entry.endsWith(".md"))
|
|
18327
18635
|
continue;
|
|
18328
|
-
const
|
|
18636
|
+
const path19 = join15(dir, entry);
|
|
18329
18637
|
let content;
|
|
18330
18638
|
try {
|
|
18331
|
-
content = reader(
|
|
18639
|
+
content = reader(path19);
|
|
18332
18640
|
} catch (err) {
|
|
18333
18641
|
log8.warn(`agent.md 读取失败(已跳过)`, {
|
|
18334
|
-
path:
|
|
18642
|
+
path: path19,
|
|
18335
18643
|
error: err instanceof Error ? err.message : String(err)
|
|
18336
18644
|
});
|
|
18337
18645
|
continue;
|
|
18338
18646
|
}
|
|
18339
18647
|
const parsed = parseAgentFrontmatter(content);
|
|
18340
18648
|
if (!parsed) {
|
|
18341
|
-
log8.warn(`agent frontmatter 解析失败(已跳过)`, { path:
|
|
18649
|
+
log8.warn(`agent frontmatter 解析失败(已跳过)`, { path: path19 });
|
|
18342
18650
|
continue;
|
|
18343
18651
|
}
|
|
18344
18652
|
if (result.has(parsed.name))
|
|
@@ -18531,7 +18839,7 @@ var handler16 = async (_ctx3) => {
|
|
|
18531
18839
|
// lib/event-stream.ts
|
|
18532
18840
|
import { promises as fs15 } from "node:fs";
|
|
18533
18841
|
init_runtime_paths();
|
|
18534
|
-
import * as
|
|
18842
|
+
import * as path19 from "node:path";
|
|
18535
18843
|
async function loadSession(id, opts = {}) {
|
|
18536
18844
|
const file = resolveSessionFile(id, opts);
|
|
18537
18845
|
const raw = await fs15.readFile(file, "utf8");
|
|
@@ -18551,7 +18859,7 @@ async function listSessions(opts = {}) {
|
|
|
18551
18859
|
for (const e of entries) {
|
|
18552
18860
|
if (!e.isFile() || !e.name.endsWith(".jsonl"))
|
|
18553
18861
|
continue;
|
|
18554
|
-
const file =
|
|
18862
|
+
const file = path19.join(dir, e.name);
|
|
18555
18863
|
const id = e.name.replace(/\.jsonl$/, "");
|
|
18556
18864
|
try {
|
|
18557
18865
|
const stat = await fs15.stat(file);
|
|
@@ -18578,11 +18886,11 @@ async function listSessions(opts = {}) {
|
|
|
18578
18886
|
return out;
|
|
18579
18887
|
}
|
|
18580
18888
|
function resolveDir(opts = {}) {
|
|
18581
|
-
const root =
|
|
18582
|
-
return opts.sessions_dir ?
|
|
18889
|
+
const root = path19.resolve(opts.root ?? process.cwd());
|
|
18890
|
+
return opts.sessions_dir ? path19.resolve(root, opts.sessions_dir) : path19.join(runtimeDir(root), "sessions");
|
|
18583
18891
|
}
|
|
18584
18892
|
function resolveSessionFile(id, opts = {}) {
|
|
18585
|
-
return
|
|
18893
|
+
return path19.join(resolveDir(opts), `${id}.jsonl`);
|
|
18586
18894
|
}
|
|
18587
18895
|
function parseJsonl(id, raw) {
|
|
18588
18896
|
const events = [];
|
|
@@ -18844,6 +19152,92 @@ function isRecoveryWorthShowing(plan) {
|
|
|
18844
19152
|
return hasSignal;
|
|
18845
19153
|
}
|
|
18846
19154
|
|
|
19155
|
+
// lib/block-pending.ts
|
|
19156
|
+
init_runtime_paths();
|
|
19157
|
+
import { promises as fs16 } from "node:fs";
|
|
19158
|
+
import * as path20 from "node:path";
|
|
19159
|
+
function blockPendingFilePath(absRoot) {
|
|
19160
|
+
const rd = runtimeDir(absRoot, { ensure: false });
|
|
19161
|
+
return path20.join(rd, "sessions", "autonomous-blocks.ndjson");
|
|
19162
|
+
}
|
|
19163
|
+
function consumeLockPath(absRoot) {
|
|
19164
|
+
return blockPendingFilePath(absRoot) + ".consume.lock";
|
|
19165
|
+
}
|
|
19166
|
+
async function scanBlockPending(absRoot, filterSessionId) {
|
|
19167
|
+
const file = blockPendingFilePath(absRoot);
|
|
19168
|
+
let raw;
|
|
19169
|
+
try {
|
|
19170
|
+
raw = await fs16.readFile(file, "utf8");
|
|
19171
|
+
} catch {
|
|
19172
|
+
return [];
|
|
19173
|
+
}
|
|
19174
|
+
if (!raw)
|
|
19175
|
+
return [];
|
|
19176
|
+
const consumed = new Set;
|
|
19177
|
+
const entries = [];
|
|
19178
|
+
for (const line of raw.split(`
|
|
19179
|
+
`)) {
|
|
19180
|
+
const trimmed = line.trim();
|
|
19181
|
+
if (!trimmed)
|
|
19182
|
+
continue;
|
|
19183
|
+
let obj;
|
|
19184
|
+
try {
|
|
19185
|
+
obj = JSON.parse(trimmed);
|
|
19186
|
+
} catch {
|
|
19187
|
+
continue;
|
|
19188
|
+
}
|
|
19189
|
+
if (!obj || typeof obj !== "object")
|
|
19190
|
+
continue;
|
|
19191
|
+
if (obj["type"] === "consume") {
|
|
19192
|
+
const sid = obj["sessionId"];
|
|
19193
|
+
const ts = obj["timestamp"];
|
|
19194
|
+
if (typeof sid === "string" && typeof ts === "string") {
|
|
19195
|
+
consumed.add(`${sid}|${ts}`);
|
|
19196
|
+
}
|
|
19197
|
+
continue;
|
|
19198
|
+
}
|
|
19199
|
+
const sessionId = obj["sessionId"];
|
|
19200
|
+
const timestamp = obj["timestamp"];
|
|
19201
|
+
if (typeof sessionId !== "string" || typeof timestamp !== "string")
|
|
19202
|
+
continue;
|
|
19203
|
+
const entry = {
|
|
19204
|
+
sessionId,
|
|
19205
|
+
timestamp,
|
|
19206
|
+
reason: typeof obj["reason"] === "string" ? obj["reason"] : undefined,
|
|
19207
|
+
summary_excerpt: typeof obj["summary_excerpt"] === "string" ? obj["summary_excerpt"] : undefined,
|
|
19208
|
+
consumed_at: typeof obj["consumed_at"] === "string" ? obj["consumed_at"] : undefined
|
|
19209
|
+
};
|
|
19210
|
+
entries.push(entry);
|
|
19211
|
+
}
|
|
19212
|
+
return entries.filter((e) => {
|
|
19213
|
+
if (e.consumed_at)
|
|
19214
|
+
return false;
|
|
19215
|
+
if (consumed.has(`${e.sessionId}|${e.timestamp}`))
|
|
19216
|
+
return false;
|
|
19217
|
+
if (filterSessionId && e.sessionId !== filterSessionId)
|
|
19218
|
+
return false;
|
|
19219
|
+
return true;
|
|
19220
|
+
});
|
|
19221
|
+
}
|
|
19222
|
+
async function markBlocksConsumed(absRoot, entries) {
|
|
19223
|
+
if (entries.length === 0)
|
|
19224
|
+
return;
|
|
19225
|
+
const file = blockPendingFilePath(absRoot);
|
|
19226
|
+
await fs16.mkdir(path20.dirname(file), { recursive: true });
|
|
19227
|
+
const now = new Date().toISOString();
|
|
19228
|
+
const lines = entries.map((e) => ({
|
|
19229
|
+
type: "consume",
|
|
19230
|
+
sessionId: e.sessionId,
|
|
19231
|
+
timestamp: e.timestamp,
|
|
19232
|
+
consumed_at: now
|
|
19233
|
+
})).map((row) => JSON.stringify(row)).join(`
|
|
19234
|
+
`) + `
|
|
19235
|
+
`;
|
|
19236
|
+
await withFileLock(consumeLockPath(absRoot), async () => {
|
|
19237
|
+
await fs16.appendFile(file, lines, "utf8");
|
|
19238
|
+
});
|
|
19239
|
+
}
|
|
19240
|
+
|
|
18847
19241
|
// plugins/session-recovery.ts
|
|
18848
19242
|
var PLUGIN_NAME17 = "session-recovery";
|
|
18849
19243
|
logLifecycle(PLUGIN_NAME17, "import", {});
|
|
@@ -18860,10 +19254,23 @@ async function processSessionStart(currentSessionId, opts = {}) {
|
|
|
18860
19254
|
return { ok: false, injected: false, reason: "scan_error", error: r.error };
|
|
18861
19255
|
}
|
|
18862
19256
|
const plan = r.plan;
|
|
18863
|
-
|
|
18864
|
-
|
|
19257
|
+
let pendingBlocks = [];
|
|
19258
|
+
if (opts.root) {
|
|
19259
|
+
try {
|
|
19260
|
+
pendingBlocks = await scanBlockPending(opts.root);
|
|
19261
|
+
} catch (err) {
|
|
19262
|
+
opts.log?.warn?.(`[${PLUGIN_NAME17}] scanBlockPending 异常:${err instanceof Error ? err.message : String(err)}`);
|
|
19263
|
+
}
|
|
18865
19264
|
}
|
|
18866
|
-
const
|
|
19265
|
+
const hasPendingBlocks = pendingBlocks.length > 0;
|
|
19266
|
+
if (!isRecoveryWorthShowing(plan) && !hasPendingBlocks) {
|
|
19267
|
+
return { ok: true, injected: false, plan, reason: "no_signal", pendingBlocks };
|
|
19268
|
+
}
|
|
19269
|
+
const blockPrompt = hasPendingBlocks ? renderBlockPendingPrompt(pendingBlocks) : "";
|
|
19270
|
+
const recoveryPrompt = isRecoveryWorthShowing(plan) ? renderPrompt(plan) : "";
|
|
19271
|
+
const prompt = [blockPrompt, recoveryPrompt].filter((s) => s.length > 0).join(`
|
|
19272
|
+
|
|
19273
|
+
`);
|
|
18867
19274
|
const injection = { source: "session-recovery", plan, prompt };
|
|
18868
19275
|
if (opts.injectRecovery) {
|
|
18869
19276
|
try {
|
|
@@ -18871,10 +19278,36 @@ async function processSessionStart(currentSessionId, opts = {}) {
|
|
|
18871
19278
|
} catch (err) {
|
|
18872
19279
|
const msg = err instanceof Error ? err.message : String(err);
|
|
18873
19280
|
opts.log?.warn?.(`[${PLUGIN_NAME17}] injectRecovery 异常:${msg}`);
|
|
18874
|
-
return { ok: false, injected: false, plan, reason: "inject_error", error: msg };
|
|
19281
|
+
return { ok: false, injected: false, plan, reason: "inject_error", error: msg, pendingBlocks };
|
|
19282
|
+
}
|
|
19283
|
+
}
|
|
19284
|
+
if (hasPendingBlocks && opts.root) {
|
|
19285
|
+
try {
|
|
19286
|
+
await markBlocksConsumed(opts.root, pendingBlocks);
|
|
19287
|
+
} catch (err) {
|
|
19288
|
+
opts.log?.warn?.(`[${PLUGIN_NAME17}] markBlocksConsumed 异常:${err instanceof Error ? err.message : String(err)}`);
|
|
18875
19289
|
}
|
|
18876
19290
|
}
|
|
18877
|
-
return { ok: true, injected: true, plan, reason: "ok" };
|
|
19291
|
+
return { ok: true, injected: true, plan, reason: "ok", pendingBlocks };
|
|
19292
|
+
}
|
|
19293
|
+
function renderBlockPendingPrompt(entries) {
|
|
19294
|
+
const lines = [];
|
|
19295
|
+
lines.push(`⚠️ **${entries.length} 个未读 BLOCK 通知**(上次 auto 模式 reviewer 阻断、channels 通知失败):`);
|
|
19296
|
+
lines.push("");
|
|
19297
|
+
entries.forEach((e, i) => {
|
|
19298
|
+
const sidShort = e.sessionId.length > 12 ? e.sessionId.slice(0, 8) + "…" : e.sessionId;
|
|
19299
|
+
const reasonPart = e.reason ? `:${e.reason}` : "";
|
|
19300
|
+
lines.push(`${i + 1}. session \`${sidShort}\` @ ${e.timestamp}${reasonPart}`);
|
|
19301
|
+
if (e.summary_excerpt) {
|
|
19302
|
+
const excerpt = e.summary_excerpt.length > 200 ? e.summary_excerpt.slice(0, 200) + "…" : e.summary_excerpt;
|
|
19303
|
+
lines.push(` > ${excerpt.replace(/\n/g, `
|
|
19304
|
+
> `)}`);
|
|
19305
|
+
}
|
|
19306
|
+
});
|
|
19307
|
+
lines.push("");
|
|
19308
|
+
lines.push("请按用户偏好处理上述 BLOCK(重派 reviewer / 用户决策),再继续其它工作。");
|
|
19309
|
+
return lines.join(`
|
|
19310
|
+
`);
|
|
18878
19311
|
}
|
|
18879
19312
|
function renderPrompt(plan) {
|
|
18880
19313
|
const lines = [];
|
|
@@ -18926,10 +19359,11 @@ var sessionRecoveryServer = async (ctx) => {
|
|
|
18926
19359
|
ok: r.ok,
|
|
18927
19360
|
injected: r.injected,
|
|
18928
19361
|
reason: r.reason,
|
|
18929
|
-
last_session_id: r.plan?.last_session_id
|
|
19362
|
+
last_session_id: r.plan?.last_session_id,
|
|
19363
|
+
pending_blocks_count: r.pendingBlocks?.length ?? 0
|
|
18930
19364
|
});
|
|
18931
19365
|
if (r.injected && r.plan) {
|
|
18932
|
-
log9.info(`[${PLUGIN_NAME17}] 注入恢复提示(last=${r.plan.last_session_id?.slice(0, 8) ?? "?"}, reason=${r.plan.reason})`);
|
|
19366
|
+
log9.info(`[${PLUGIN_NAME17}] 注入恢复提示(last=${r.plan.last_session_id?.slice(0, 8) ?? "?"}, reason=${r.plan.reason}, pending_blocks=${r.pendingBlocks?.length ?? 0})`);
|
|
18933
19367
|
}
|
|
18934
19368
|
});
|
|
18935
19369
|
}
|
|
@@ -18938,8 +19372,8 @@ var sessionRecoveryServer = async (ctx) => {
|
|
|
18938
19372
|
var handler17 = sessionRecoveryServer;
|
|
18939
19373
|
|
|
18940
19374
|
// plugins/subtasks.ts
|
|
18941
|
-
import { promises as
|
|
18942
|
-
import * as
|
|
19375
|
+
import { promises as fs17 } from "node:fs";
|
|
19376
|
+
import * as path21 from "node:path";
|
|
18943
19377
|
|
|
18944
19378
|
// lib/parallel-merge.ts
|
|
18945
19379
|
init_autonomy();
|
|
@@ -19764,7 +20198,7 @@ function sleep2(ms) {
|
|
|
19764
20198
|
init_runtime_paths();
|
|
19765
20199
|
var PLUGIN_NAME18 = "subtasks";
|
|
19766
20200
|
function getLogFile(root = process.cwd()) {
|
|
19767
|
-
return
|
|
20201
|
+
return path21.join(runtimeDir(root), "logs", "subtasks.log");
|
|
19768
20202
|
}
|
|
19769
20203
|
var VERB_RE = /^([a-zA-Z]{3,12})/;
|
|
19770
20204
|
var CN_VERBS = [
|
|
@@ -20069,8 +20503,8 @@ async function writeLog(level, msg, data) {
|
|
|
20069
20503
|
`;
|
|
20070
20504
|
try {
|
|
20071
20505
|
const logFile = getLogFile();
|
|
20072
|
-
await
|
|
20073
|
-
await
|
|
20506
|
+
await fs17.mkdir(path21.dirname(logFile), { recursive: true });
|
|
20507
|
+
await fs17.appendFile(logFile, line, "utf8");
|
|
20074
20508
|
} catch {}
|
|
20075
20509
|
}
|
|
20076
20510
|
logLifecycle(PLUGIN_NAME18, "import");
|
|
@@ -20586,12 +21020,12 @@ var tokenManagerServer = async (ctx) => {
|
|
|
20586
21020
|
var handler20 = tokenManagerServer;
|
|
20587
21021
|
|
|
20588
21022
|
// plugins/tool-policy.ts
|
|
20589
|
-
import { promises as
|
|
20590
|
-
import * as
|
|
21023
|
+
import { promises as fs18 } from "node:fs";
|
|
21024
|
+
import * as path23 from "node:path";
|
|
20591
21025
|
init_autonomy();
|
|
20592
21026
|
|
|
20593
21027
|
// lib/file-regex-acl.ts
|
|
20594
|
-
import * as
|
|
21028
|
+
import * as path22 from "node:path";
|
|
20595
21029
|
function compileRule(r) {
|
|
20596
21030
|
if (r instanceof RegExp)
|
|
20597
21031
|
return r;
|
|
@@ -20657,7 +21091,7 @@ function normalizePath2(p) {
|
|
|
20657
21091
|
let s = p.replace(/\\/g, "/");
|
|
20658
21092
|
if (s.startsWith("./"))
|
|
20659
21093
|
s = s.slice(2);
|
|
20660
|
-
s =
|
|
21094
|
+
s = path22.posix.normalize(s);
|
|
20661
21095
|
return s;
|
|
20662
21096
|
}
|
|
20663
21097
|
function checkFileAccess(acl, file, op) {
|
|
@@ -20767,11 +21201,11 @@ function decideToolCall(ctx, cfg = {}, currentAgent) {
|
|
|
20767
21201
|
action = "deny";
|
|
20768
21202
|
return { action, reasons, autonomy: a, acl: aclResults };
|
|
20769
21203
|
}
|
|
20770
|
-
var POLICY_PATH =
|
|
21204
|
+
var POLICY_PATH = path23.join(".codeforge", "policy.json");
|
|
20771
21205
|
async function loadPolicy(root = process.cwd()) {
|
|
20772
|
-
const file =
|
|
21206
|
+
const file = path23.join(root, POLICY_PATH);
|
|
20773
21207
|
try {
|
|
20774
|
-
const raw = await
|
|
21208
|
+
const raw = await fs18.readFile(file, "utf8");
|
|
20775
21209
|
const data = JSON.parse(raw);
|
|
20776
21210
|
return data;
|
|
20777
21211
|
} catch {
|
|
@@ -20881,7 +21315,7 @@ var handler21 = toolPolicyServer;
|
|
|
20881
21315
|
// plugins/update-checker.ts
|
|
20882
21316
|
import { existsSync as existsSync5 } from "node:fs";
|
|
20883
21317
|
import { homedir as homedir8 } from "node:os";
|
|
20884
|
-
import { join as
|
|
21318
|
+
import { join as join21 } from "node:path";
|
|
20885
21319
|
|
|
20886
21320
|
// lib/update-checker-impl.ts
|
|
20887
21321
|
import { createHash as createHash5 } from "node:crypto";
|
|
@@ -20898,7 +21332,7 @@ import {
|
|
|
20898
21332
|
writeFileSync as writeFileSync2
|
|
20899
21333
|
} from "node:fs";
|
|
20900
21334
|
import { homedir as homedir7, tmpdir } from "node:os";
|
|
20901
|
-
import { dirname as
|
|
21335
|
+
import { dirname as dirname12, join as join20 } from "node:path";
|
|
20902
21336
|
import { fileURLToPath } from "node:url";
|
|
20903
21337
|
import * as https from "node:https";
|
|
20904
21338
|
import * as zlib from "node:zlib";
|
|
@@ -20906,7 +21340,7 @@ import * as zlib from "node:zlib";
|
|
|
20906
21340
|
// lib/version-injected.ts
|
|
20907
21341
|
function getInjectedVersion() {
|
|
20908
21342
|
try {
|
|
20909
|
-
const v = "0.5.
|
|
21343
|
+
const v = "0.5.5";
|
|
20910
21344
|
if (typeof v === "string" && /^\d+\.\d+\.\d+/.test(v)) {
|
|
20911
21345
|
return v;
|
|
20912
21346
|
}
|
|
@@ -20995,18 +21429,18 @@ function readLocalVersion() {
|
|
|
20995
21429
|
return injected;
|
|
20996
21430
|
try {
|
|
20997
21431
|
const here = fileURLToPath(import.meta.url);
|
|
20998
|
-
const root =
|
|
20999
|
-
const pkg = JSON.parse(readFileSync5(
|
|
21432
|
+
const root = dirname12(dirname12(here));
|
|
21433
|
+
const pkg = JSON.parse(readFileSync5(join20(root, "package.json"), "utf8"));
|
|
21000
21434
|
return typeof pkg.version === "string" ? pkg.version : "0.0.0";
|
|
21001
21435
|
} catch {
|
|
21002
21436
|
return "0.0.0";
|
|
21003
21437
|
}
|
|
21004
21438
|
}
|
|
21005
21439
|
function defaultCacheDir() {
|
|
21006
|
-
return process.env["CODEFORGE_CACHE_DIR"] ??
|
|
21440
|
+
return process.env["CODEFORGE_CACHE_DIR"] ?? join20(homedir7(), ".cache", "codeforge");
|
|
21007
21441
|
}
|
|
21008
21442
|
function defaultCacheFile() {
|
|
21009
|
-
return
|
|
21443
|
+
return join20(defaultCacheDir(), "update-check.json");
|
|
21010
21444
|
}
|
|
21011
21445
|
function readCache(file) {
|
|
21012
21446
|
try {
|
|
@@ -21024,7 +21458,7 @@ function readCache(file) {
|
|
|
21024
21458
|
}
|
|
21025
21459
|
function writeCache(file, entry) {
|
|
21026
21460
|
try {
|
|
21027
|
-
mkdirSync3(
|
|
21461
|
+
mkdirSync3(dirname12(file), { recursive: true });
|
|
21028
21462
|
writeFileSync2(file, JSON.stringify(entry, null, 2), "utf8");
|
|
21029
21463
|
} catch {}
|
|
21030
21464
|
}
|
|
@@ -21162,14 +21596,14 @@ function defaultHttpFetcher(url, timeoutMs) {
|
|
|
21162
21596
|
});
|
|
21163
21597
|
}
|
|
21164
21598
|
async function downloadAndExtractBundle(opts) {
|
|
21165
|
-
const tmpRoot = opts.tmpDir ?? mkdtempSync(
|
|
21599
|
+
const tmpRoot = opts.tmpDir ?? mkdtempSync(join20(tmpdir(), "codeforge-update-"));
|
|
21166
21600
|
mkdirSync3(tmpRoot, { recursive: true });
|
|
21167
21601
|
const fetcher = opts.tarballFetcher ?? defaultBinaryFetcher;
|
|
21168
21602
|
const tarballBuf = await fetcher(opts.tarballUrl);
|
|
21169
21603
|
verifyIntegrity(tarballBuf, opts.expectedIntegrity);
|
|
21170
21604
|
const tarBuf = zlib.gunzipSync(tarballBuf);
|
|
21171
21605
|
extractTarToDir(tarBuf, tmpRoot);
|
|
21172
|
-
const bundlePath =
|
|
21606
|
+
const bundlePath = join20(tmpRoot, "package", "dist", "index.js");
|
|
21173
21607
|
if (!existsSync4(bundlePath)) {
|
|
21174
21608
|
throw new Error(`bundle_not_found: ${bundlePath}`);
|
|
21175
21609
|
}
|
|
@@ -21209,11 +21643,11 @@ function extractTarToDir(tarBuf, destRoot) {
|
|
|
21209
21643
|
offset += 512;
|
|
21210
21644
|
if (typeFlag === "0" || typeFlag === "" || typeFlag === "\x00") {
|
|
21211
21645
|
const fileBuf = tarBuf.subarray(offset, offset + size);
|
|
21212
|
-
const dest =
|
|
21213
|
-
mkdirSync3(
|
|
21646
|
+
const dest = join20(destRoot, fullName);
|
|
21647
|
+
mkdirSync3(dirname12(dest), { recursive: true });
|
|
21214
21648
|
writeFileSync2(dest, fileBuf);
|
|
21215
21649
|
} else if (typeFlag === "5") {
|
|
21216
|
-
mkdirSync3(
|
|
21650
|
+
mkdirSync3(join20(destRoot, fullName), { recursive: true });
|
|
21217
21651
|
}
|
|
21218
21652
|
offset += Math.ceil(size / 512) * 512;
|
|
21219
21653
|
}
|
|
@@ -21266,7 +21700,7 @@ function atomicReplaceBundle(opts) {
|
|
|
21266
21700
|
if (!existsSync4(source)) {
|
|
21267
21701
|
throw new Error(`atomic_source_missing: ${source}`);
|
|
21268
21702
|
}
|
|
21269
|
-
mkdirSync3(
|
|
21703
|
+
mkdirSync3(dirname12(target), { recursive: true });
|
|
21270
21704
|
const newPath = `${target}.new`;
|
|
21271
21705
|
const backupPath = `${target}.bak.${oldVersion}`;
|
|
21272
21706
|
let strategy = "rename";
|
|
@@ -21318,11 +21752,11 @@ function cleanupOldBackups(target, keep) {
|
|
|
21318
21752
|
if (keep <= 0)
|
|
21319
21753
|
return;
|
|
21320
21754
|
try {
|
|
21321
|
-
const dir =
|
|
21755
|
+
const dir = dirname12(target);
|
|
21322
21756
|
const base = target.substring(dir.length + 1);
|
|
21323
21757
|
const prefix = `${base}.bak.`;
|
|
21324
21758
|
const all = readdirSync3(dir).filter((f) => f.startsWith(prefix)).map((f) => {
|
|
21325
|
-
const full =
|
|
21759
|
+
const full = join20(dir, f);
|
|
21326
21760
|
let mtimeMs = 0;
|
|
21327
21761
|
try {
|
|
21328
21762
|
mtimeMs = statSync5(full).mtimeMs;
|
|
@@ -21344,7 +21778,7 @@ function loadCompatibility(opts) {
|
|
|
21344
21778
|
const root = opts?.cwd ?? inferPluginRoot();
|
|
21345
21779
|
if (!root)
|
|
21346
21780
|
return null;
|
|
21347
|
-
file =
|
|
21781
|
+
file = join20(root, "compatibility.json");
|
|
21348
21782
|
}
|
|
21349
21783
|
if (!existsSync4(file))
|
|
21350
21784
|
return null;
|
|
@@ -21369,7 +21803,7 @@ function loadCompatibility(opts) {
|
|
|
21369
21803
|
function inferPluginRoot() {
|
|
21370
21804
|
try {
|
|
21371
21805
|
const here = fileURLToPath(import.meta.url);
|
|
21372
|
-
return
|
|
21806
|
+
return dirname12(dirname12(here));
|
|
21373
21807
|
} catch {
|
|
21374
21808
|
return null;
|
|
21375
21809
|
}
|
|
@@ -21564,14 +21998,14 @@ function detectOpencodeVersion() {
|
|
|
21564
21998
|
}
|
|
21565
21999
|
function getOpencodeBundlePath() {
|
|
21566
22000
|
const candidates = [];
|
|
21567
|
-
candidates.push(
|
|
22001
|
+
candidates.push(join21(homedir8(), ".config", "opencode", "codeforge", "index.js"));
|
|
21568
22002
|
if (process.platform === "win32") {
|
|
21569
22003
|
const appData = process.env["APPDATA"];
|
|
21570
22004
|
if (appData)
|
|
21571
|
-
candidates.push(
|
|
22005
|
+
candidates.push(join21(appData, "opencode", "codeforge", "index.js"));
|
|
21572
22006
|
const localAppData = process.env["LOCALAPPDATA"];
|
|
21573
22007
|
if (localAppData)
|
|
21574
|
-
candidates.push(
|
|
22008
|
+
candidates.push(join21(localAppData, "opencode", "codeforge", "index.js"));
|
|
21575
22009
|
}
|
|
21576
22010
|
for (const c of candidates) {
|
|
21577
22011
|
if (existsSync5(c))
|
|
@@ -21632,11 +22066,11 @@ async function postToast(ctx, message) {
|
|
|
21632
22066
|
var handler22 = updateCheckerServer;
|
|
21633
22067
|
|
|
21634
22068
|
// plugins/workflow-engine.ts
|
|
21635
|
-
import * as
|
|
22069
|
+
import * as path25 from "node:path";
|
|
21636
22070
|
|
|
21637
22071
|
// lib/workflow-loader.ts
|
|
21638
|
-
import { promises as
|
|
21639
|
-
import * as
|
|
22072
|
+
import { promises as fs19 } from "node:fs";
|
|
22073
|
+
import * as path24 from "node:path";
|
|
21640
22074
|
import { z as z32 } from "zod";
|
|
21641
22075
|
var ActionSchema = z32.object({
|
|
21642
22076
|
tool: z32.string().min(1, "action.tool 不能为空"),
|
|
@@ -21722,7 +22156,7 @@ function parseWorkflowYaml(yaml, sourcePath = "<inline>") {
|
|
|
21722
22156
|
async function loadWorkflowFromFile(filePath) {
|
|
21723
22157
|
let txt;
|
|
21724
22158
|
try {
|
|
21725
|
-
txt = await
|
|
22159
|
+
txt = await fs19.readFile(filePath, "utf8");
|
|
21726
22160
|
} catch (err) {
|
|
21727
22161
|
return {
|
|
21728
22162
|
ok: false,
|
|
@@ -21737,7 +22171,7 @@ async function loadWorkflowsFromDir(dir) {
|
|
|
21737
22171
|
const failed = [];
|
|
21738
22172
|
let entries;
|
|
21739
22173
|
try {
|
|
21740
|
-
entries = await
|
|
22174
|
+
entries = await fs19.readdir(dir);
|
|
21741
22175
|
} catch (err) {
|
|
21742
22176
|
const e = err;
|
|
21743
22177
|
if (e.code === "ENOENT")
|
|
@@ -21749,7 +22183,7 @@ async function loadWorkflowsFromDir(dir) {
|
|
|
21749
22183
|
continue;
|
|
21750
22184
|
if (!/\.ya?ml$/i.test(name))
|
|
21751
22185
|
continue;
|
|
21752
|
-
const full =
|
|
22186
|
+
const full = path24.join(dir, name);
|
|
21753
22187
|
const r = await loadWorkflowFromFile(full);
|
|
21754
22188
|
if (r.ok)
|
|
21755
22189
|
loaded.push(r);
|
|
@@ -22139,7 +22573,7 @@ async function handleCommandInvoked(raw, workflowsDir = "workflows") {
|
|
|
22139
22573
|
}
|
|
22140
22574
|
var workflowEngineServer = async (ctx) => {
|
|
22141
22575
|
const directory = ctx.directory ?? process.cwd();
|
|
22142
|
-
const workflowsDir =
|
|
22576
|
+
const workflowsDir = path25.join(directory, "workflows");
|
|
22143
22577
|
ensureRegistry(workflowsDir).catch((err) => fallbackLog2.warn(`[${PLUGIN_NAME23}] preload workflows failed`, {
|
|
22144
22578
|
error: err instanceof Error ? err.message : String(err)
|
|
22145
22579
|
}));
|
|
@@ -22183,7 +22617,7 @@ var workflowEngineServer = async (ctx) => {
|
|
|
22183
22617
|
var handler23 = workflowEngineServer;
|
|
22184
22618
|
|
|
22185
22619
|
// plugins/session-worktree-guard.ts
|
|
22186
|
-
import
|
|
22620
|
+
import path26 from "node:path";
|
|
22187
22621
|
var PLUGIN_NAME24 = "session-worktree-guard";
|
|
22188
22622
|
logLifecycle(PLUGIN_NAME24, "import", {});
|
|
22189
22623
|
var WRITE_INTENT_RE = />(?!=)|\btee\b|\brm\b|\bmv\b|\bcp\b|\bmkdir\b|\btouch\b|\bchmod\b|\bchown\b|\bln\b/;
|
|
@@ -22212,7 +22646,7 @@ var WRITE_TOOLS = new Set(["write", "edit", "ast_edit"]);
|
|
|
22212
22646
|
function rewritePath(value, mainRoot, worktreeRoot) {
|
|
22213
22647
|
if (!value)
|
|
22214
22648
|
return null;
|
|
22215
|
-
const resolved =
|
|
22649
|
+
const resolved = path26.isAbsolute(value) ? value : path26.resolve(mainRoot, value);
|
|
22216
22650
|
if (resolved === mainRoot)
|
|
22217
22651
|
return worktreeRoot;
|
|
22218
22652
|
const prefix = mainRoot.endsWith("/") ? mainRoot : mainRoot + "/";
|
|
@@ -22295,6 +22729,29 @@ var sessionWorktreeGuardPlugin = async (ctx) => {
|
|
|
22295
22729
|
if (!isWriteOperation(toolName, argsObj, mainRoot)) {
|
|
22296
22730
|
return;
|
|
22297
22731
|
}
|
|
22732
|
+
try {
|
|
22733
|
+
const parentId = lookupParentSessionId(sessionId);
|
|
22734
|
+
if (parentId) {
|
|
22735
|
+
const parentEntry = await getSessionWorktree(parentId, mainRoot);
|
|
22736
|
+
if (parentEntry && parentEntry.status === "active") {
|
|
22737
|
+
entry = parentEntry;
|
|
22738
|
+
log13.debug?.(`[child-inherit] session ${sessionId} 继承父 ${parentId} 的 worktree`, { parentSessionId: parentId, worktreePath: parentEntry.worktreePath });
|
|
22739
|
+
safeWriteLog(PLUGIN_NAME24, {
|
|
22740
|
+
hook: "tool.execute.before",
|
|
22741
|
+
tool: toolName,
|
|
22742
|
+
sessionID: input.sessionID,
|
|
22743
|
+
action: "child-inherit",
|
|
22744
|
+
parent_session_id: parentId,
|
|
22745
|
+
parent_branch: parentEntry.branch,
|
|
22746
|
+
parent_worktree: parentEntry.worktreePath
|
|
22747
|
+
});
|
|
22748
|
+
}
|
|
22749
|
+
}
|
|
22750
|
+
} catch (lookupErr) {
|
|
22751
|
+
log13.debug?.("[child-inherit] lookupParentSessionId 抛错(已隔离,退回 lazy-bind)", { error: lookupErr instanceof Error ? lookupErr.message : String(lookupErr) });
|
|
22752
|
+
}
|
|
22753
|
+
}
|
|
22754
|
+
if (!entry) {
|
|
22298
22755
|
try {
|
|
22299
22756
|
entry = await bindSessionWorktree({ sessionId, mainRoot });
|
|
22300
22757
|
log13.info(`[lazy-bind] auto-created worktree for session ${sessionId}`, { branch: entry.branch, path: entry.worktreePath });
|
|
@@ -22319,6 +22776,32 @@ var sessionWorktreeGuardPlugin = async (ctx) => {
|
|
|
22319
22776
|
}
|
|
22320
22777
|
}
|
|
22321
22778
|
const worktreePath = entry.worktreePath;
|
|
22779
|
+
if (toolName === "session_merge") {
|
|
22780
|
+
const action = argsObj["action"];
|
|
22781
|
+
if (action === "merge") {
|
|
22782
|
+
const caller = input.agent;
|
|
22783
|
+
if (caller !== undefined && caller !== "codeforge") {
|
|
22784
|
+
const reason = `[session-worktree-guard] DENIED: session_merge action=merge 仅 codeforge orchestrator 或用户可调;当前 caller=${caller}`;
|
|
22785
|
+
log13.warn(reason, {
|
|
22786
|
+
sessionId,
|
|
22787
|
+
tool: toolName,
|
|
22788
|
+
action,
|
|
22789
|
+
caller
|
|
22790
|
+
});
|
|
22791
|
+
safeWriteLog(PLUGIN_NAME24, {
|
|
22792
|
+
hook: "tool.execute.before",
|
|
22793
|
+
tool: toolName,
|
|
22794
|
+
sessionID: input.sessionID,
|
|
22795
|
+
action: "deny",
|
|
22796
|
+
source: "merge-caller-whitelist",
|
|
22797
|
+
caller,
|
|
22798
|
+
merge_action: "merge"
|
|
22799
|
+
});
|
|
22800
|
+
denied = new DeniedError(reason);
|
|
22801
|
+
return;
|
|
22802
|
+
}
|
|
22803
|
+
}
|
|
22804
|
+
}
|
|
22322
22805
|
if (toolName !== "plan_read" && entry.requiredPlanId && entry.planReadOk !== true) {
|
|
22323
22806
|
let isWriteOp = WRITE_TOOLS.has(toolName);
|
|
22324
22807
|
if (!isWriteOp && toolName === "bash") {
|
|
@@ -22337,8 +22820,16 @@ var sessionWorktreeGuardPlugin = async (ctx) => {
|
|
|
22337
22820
|
}
|
|
22338
22821
|
}
|
|
22339
22822
|
if (isWriteOp) {
|
|
22340
|
-
const
|
|
22341
|
-
|
|
22823
|
+
const inherited = entry.sessionId !== sessionId;
|
|
22824
|
+
const reasonBase = `[session-worktree-guard] DENIED: 当前 session 要求先调用 plan_read(plan_id="${entry.requiredPlanId}") 再执行写操作`;
|
|
22825
|
+
const reason = inherited ? `${reasonBase}
|
|
22826
|
+
[gate-deny] child session=${sessionId} 继承父 entry 但父 session planReadOk=false,父 session=${entry.sessionId} 需先调 plan_read(plan_id="${entry.requiredPlanId}")` : reasonBase;
|
|
22827
|
+
log13.warn(reason, {
|
|
22828
|
+
tool: toolName,
|
|
22829
|
+
sessionId,
|
|
22830
|
+
requiredPlanId: entry.requiredPlanId,
|
|
22831
|
+
inheritedFromParent: inherited ? entry.sessionId : undefined
|
|
22832
|
+
});
|
|
22342
22833
|
safeWriteLog(PLUGIN_NAME24, {
|
|
22343
22834
|
hook: "tool.execute.before",
|
|
22344
22835
|
tool: toolName,
|
|
@@ -22497,6 +22988,30 @@ var worktreeLifecyclePlugin = async (ctx) => {
|
|
|
22497
22988
|
});
|
|
22498
22989
|
return;
|
|
22499
22990
|
}
|
|
22991
|
+
try {
|
|
22992
|
+
const worktreeHead = await getCurrentWorktreeHead(entry.worktreePath);
|
|
22993
|
+
if (worktreeHead && worktreeHead === entry.baseSha) {
|
|
22994
|
+
const fastDirty = await isWorktreeDirty(entry.worktreePath);
|
|
22995
|
+
if (!fastDirty) {
|
|
22996
|
+
await discardSession({ sessionId: ended.sessionID, mainRoot });
|
|
22997
|
+
safeWriteLog(PLUGIN_NAME25, {
|
|
22998
|
+
hook: "event",
|
|
22999
|
+
type: ended.type,
|
|
23000
|
+
sessionID: ended.sessionID,
|
|
23001
|
+
action: "discard-empty-fast-path",
|
|
23002
|
+
branch: entry.branch,
|
|
23003
|
+
worktreePath: entry.worktreePath
|
|
23004
|
+
});
|
|
23005
|
+
lastIdleToastAt.delete(ended.sessionID);
|
|
23006
|
+
return;
|
|
23007
|
+
}
|
|
23008
|
+
}
|
|
23009
|
+
} catch (err) {
|
|
23010
|
+
log14.warn(`[lifecycle] empty-worktree fast-path 检测失败 (回退到常规路径)`, {
|
|
23011
|
+
sessionId: ended.sessionID,
|
|
23012
|
+
error: err instanceof Error ? err.message : String(err)
|
|
23013
|
+
});
|
|
23014
|
+
}
|
|
22500
23015
|
let hadDirty = false;
|
|
22501
23016
|
try {
|
|
22502
23017
|
hadDirty = await isWorktreeDirty(entry.worktreePath);
|
|
@@ -22646,6 +23161,7 @@ function createCodeforgeServer(opts) {
|
|
|
22646
23161
|
const commandExecuteBeforeBucket = [];
|
|
22647
23162
|
const chatParamsBucket = [];
|
|
22648
23163
|
const toolExecuteBeforeBucket = [];
|
|
23164
|
+
const toolExecuteAfterBucket = [];
|
|
22649
23165
|
const chatMessagesTransformBucket = [];
|
|
22650
23166
|
const chatSystemTransformBucket = [];
|
|
22651
23167
|
const eventBucket = [];
|
|
@@ -22659,6 +23175,8 @@ function createCodeforgeServer(opts) {
|
|
|
22659
23175
|
chatParamsBucket.push(h["chat.params"]);
|
|
22660
23176
|
if (h["tool.execute.before"])
|
|
22661
23177
|
toolExecuteBeforeBucket.push(h["tool.execute.before"]);
|
|
23178
|
+
if (h["tool.execute.after"])
|
|
23179
|
+
toolExecuteAfterBucket.push(h["tool.execute.after"]);
|
|
22662
23180
|
if (h["experimental.chat.messages.transform"]) {
|
|
22663
23181
|
chatMessagesTransformBucket.push(h["experimental.chat.messages.transform"]);
|
|
22664
23182
|
}
|
|
@@ -22675,6 +23193,7 @@ function createCodeforgeServer(opts) {
|
|
|
22675
23193
|
"command.execute.before": makeSerialHook("command.execute.before", commandExecuteBeforeBucket),
|
|
22676
23194
|
"chat.params": makeSerialHook("chat.params", chatParamsBucket),
|
|
22677
23195
|
"tool.execute.before": makeSerialHook("tool.execute.before", toolExecuteBeforeBucket),
|
|
23196
|
+
"tool.execute.after": makeSerialHook("tool.execute.after", toolExecuteAfterBucket),
|
|
22678
23197
|
"experimental.chat.messages.transform": makeSerialHook("experimental.chat.messages.transform", chatMessagesTransformBucket),
|
|
22679
23198
|
"experimental.chat.system.transform": makeSerialHook("experimental.chat.system.transform", chatSystemTransformBucket),
|
|
22680
23199
|
event: async (envelope) => {
|