@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.
@@ -59,7 +59,7 @@ fallback_models:
59
59
  - ❌ 不允许**为并行而并行**:改同文件 / 步骤间有真实依赖时必须串行
60
60
  - ❌ 不允许在父对话直接吐长交付物内容
61
61
  - ❌ 不允许自动派 coder 修 BLOCK;REQUEST_CHANGES 允许自动派 coder 修,**最多 3 次**
62
- - **不允许自己主动调 `session_merge action=merge`** —— merge 由用户通过 `/merge` 命令触发(ADR:worktree-session-isolation)
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
- return { cleaned, failed, skipped };
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
- if (inflight3.has(input.sessionID)) {
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 path18 = join15(dir, entry);
18636
+ const path19 = join15(dir, entry);
18329
18637
  let content;
18330
18638
  try {
18331
- content = reader(path18);
18639
+ content = reader(path19);
18332
18640
  } catch (err) {
18333
18641
  log8.warn(`agent.md 读取失败(已跳过)`, {
18334
- path: path18,
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: path18 });
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 path18 from "node:path";
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 = path18.join(dir, e.name);
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 = path18.resolve(opts.root ?? process.cwd());
18582
- return opts.sessions_dir ? path18.resolve(root, opts.sessions_dir) : path18.join(runtimeDir(root), "sessions");
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 path18.join(resolveDir(opts), `${id}.jsonl`);
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
- if (!isRecoveryWorthShowing(plan)) {
18864
- return { ok: true, injected: false, plan, reason: "no_signal" };
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 prompt = renderPrompt(plan);
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 fs16 } from "node:fs";
18942
- import * as path19 from "node:path";
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 path19.join(runtimeDir(root), "logs", "subtasks.log");
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 fs16.mkdir(path19.dirname(logFile), { recursive: true });
20073
- await fs16.appendFile(logFile, line, "utf8");
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 fs17 } from "node:fs";
20590
- import * as path21 from "node:path";
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 path20 from "node:path";
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 = path20.posix.normalize(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 = path21.join(".codeforge", "policy.json");
21204
+ var POLICY_PATH = path23.join(".codeforge", "policy.json");
20771
21205
  async function loadPolicy(root = process.cwd()) {
20772
- const file = path21.join(root, POLICY_PATH);
21206
+ const file = path23.join(root, POLICY_PATH);
20773
21207
  try {
20774
- const raw = await fs17.readFile(file, "utf8");
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 join20 } from "node:path";
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 dirname10, join as join19 } from "node:path";
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.3";
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 = dirname10(dirname10(here));
20999
- const pkg = JSON.parse(readFileSync5(join19(root, "package.json"), "utf8"));
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"] ?? join19(homedir7(), ".cache", "codeforge");
21440
+ return process.env["CODEFORGE_CACHE_DIR"] ?? join20(homedir7(), ".cache", "codeforge");
21007
21441
  }
21008
21442
  function defaultCacheFile() {
21009
- return join19(defaultCacheDir(), "update-check.json");
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(dirname10(file), { recursive: true });
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(join19(tmpdir(), "codeforge-update-"));
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 = join19(tmpRoot, "package", "dist", "index.js");
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 = join19(destRoot, fullName);
21213
- mkdirSync3(dirname10(dest), { recursive: true });
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(join19(destRoot, fullName), { recursive: true });
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(dirname10(target), { recursive: true });
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 = dirname10(target);
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 = join19(dir, f);
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 = join19(root, "compatibility.json");
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 dirname10(dirname10(here));
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(join20(homedir8(), ".config", "opencode", "codeforge", "index.js"));
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(join20(appData, "opencode", "codeforge", "index.js"));
22005
+ candidates.push(join21(appData, "opencode", "codeforge", "index.js"));
21572
22006
  const localAppData = process.env["LOCALAPPDATA"];
21573
22007
  if (localAppData)
21574
- candidates.push(join20(localAppData, "opencode", "codeforge", "index.js"));
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 path23 from "node:path";
22069
+ import * as path25 from "node:path";
21636
22070
 
21637
22071
  // lib/workflow-loader.ts
21638
- import { promises as fs18 } from "node:fs";
21639
- import * as path22 from "node:path";
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 fs18.readFile(filePath, "utf8");
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 fs18.readdir(dir);
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 = path22.join(dir, name);
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 = path23.join(directory, "workflows");
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 path24 from "node:path";
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 = path24.isAbsolute(value) ? value : path24.resolve(mainRoot, value);
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 reason = `[session-worktree-guard] DENIED: 当前 session 要求先调用 plan_read(plan_id="${entry.requiredPlanId}") 再执行写操作`;
22341
- log13.warn(reason, { tool: toolName, sessionId, requiredPlanId: entry.requiredPlanId });
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) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@andyqiu/codeforge",
3
- "version": "0.5.3",
3
+ "version": "0.5.5",
4
4
  "description": "CodeForge — opencode 的零侵入扩展包",
5
5
  "type": "module",
6
6
  "private": false,