@andyqiu/codeforge 0.5.4 → 0.5.6

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 |
@@ -12,6 +12,7 @@ description: 查看或切换当前 session 的自主度档位(step/semi/full/a
12
12
  - `/autonomy semi` — 危险工具确认,安全工具自动(默认)
13
13
  - `/autonomy full` — 全自动工具执行(reviewer APPROVE 后仍需 `/merge` 手动触发)
14
14
  - `/autonomy auto` — 全自动 + APPROVE 自动 merge(**最小干预模式**)
15
+ - `/autonomy status` — **Phase 2.2** 显示实时状态(档位 / paused / 五维 budget / 全局今日 USD / block-pending)
15
16
 
16
17
  ## ⚠️ auto 模式须知
17
18
 
@@ -23,6 +24,7 @@ description: 查看或切换当前 session 的自主度档位(step/semi/full/a
23
24
  - **REQUEST_CHANGES** 自动循环(≤ 3 次),仍由 codeforge agent 派 coder 修
24
25
  - **BLOCK** 通过 channels 通知(需配置 `.codeforge/channels.json`)
25
26
  - channels 全失败时 fallback 写 `<runtimeDir>/sessions/autonomous-blocks.ndjson`
27
+ - **Phase 2.2**:下次 session.start 时 session-recovery plugin 自动扫描并提示用户处理
26
28
  - **仅根 session 生效**(子 session 一律跳过,防 orchestrator 嵌套)
27
29
 
28
30
  ### 默认预算上限(任一耗尽自动降回 semi)
@@ -34,6 +36,17 @@ description: 查看或切换当前 session 的自主度档位(step/semi/full/a
34
36
  | Wall clock | 30 分钟 |
35
37
  | auto merge 次数 | 5 |
36
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 信号)。
37
50
 
38
51
  ### 逃生口
39
52
 
@@ -41,6 +54,7 @@ description: 查看或切换当前 session 的自主度档位(step/semi/full/a
41
54
  - `/discard-session` — 不可逆,硬丢 worktree + branch
42
55
  - budget 耗尽 — 自动降回 semi
43
56
  - BLOCK — 强制通知 + 降档
57
+ - stuck 检测 — 5 轮无进展自动降档
44
58
 
45
59
  ## 行为对比
46
60
 
@@ -53,10 +67,25 @@ description: 查看或切换当前 session 的自主度档位(step/semi/full/a
53
67
 
54
68
  ## 配置存储
55
69
 
56
- `<runtimeDir>/autonomy/<sessionId>.json`(withFileLock 跨进程并发安全)
70
+ - per-session:`<runtimeDir>/autonomy/<sessionId>.json`(withFileLock 跨进程并发安全)
71
+ - 全局每日 USD:`<runtimeDir>/autonomy/global-budget-<YYYY-MM-DD>.json`(withFileLock)
57
72
 
58
73
  ## 关联 ADR
59
74
 
60
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 消费
61
77
  - [plandex-three-autonomy-modes](../docs/adr/plandex-three-autonomy-modes.md) — 三档基线
62
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 注册(零侵入约束)。
package/dist/index.js CHANGED
@@ -8205,11 +8205,11 @@ function shouldStopByStuck(history, cfg) {
8205
8205
  async function withTimeout4(p, timeoutMs) {
8206
8206
  if (timeoutMs <= 0)
8207
8207
  return await p;
8208
- return await new Promise((resolve15, reject) => {
8208
+ return await new Promise((resolve16, reject) => {
8209
8209
  const timer = setTimeout(() => reject(new Error(`timeout after ${timeoutMs}ms`)), timeoutMs);
8210
8210
  Promise.resolve(p).then((v) => {
8211
8211
  clearTimeout(timer);
8212
- resolve15(v);
8212
+ resolve16(v);
8213
8213
  }, (err) => {
8214
8214
  clearTimeout(timer);
8215
8215
  reject(err);
@@ -13393,13 +13393,6 @@ async function getCurrentWorktreeHead(worktreePath) {
13393
13393
  return "";
13394
13394
  }
13395
13395
  }
13396
- async function getCurrentMainHead(mainRoot) {
13397
- try {
13398
- return (await runGit2(path13.resolve(mainRoot), ["rev-parse", "HEAD"])).trim();
13399
- } catch {
13400
- return "";
13401
- }
13402
- }
13403
13396
  async function isWorktreeDirty(worktreePath) {
13404
13397
  try {
13405
13398
  const out = (await runGit2(worktreePath, ["status", "--porcelain"])).trim();
@@ -13437,6 +13430,27 @@ Codeforge-Base: ${baseSha.slice(0, 12)}`;
13437
13430
  return subject + body + footer;
13438
13431
  }
13439
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
+ }
13440
13454
  async function pruneOrphanWorktrees(mainRoot) {
13441
13455
  const resolved = path13.resolve(mainRoot);
13442
13456
  const cleaned = [];
@@ -13554,7 +13568,12 @@ async function pruneOrphanWorktrees(mainRoot) {
13554
13568
  failed.push({ worktreePath: candidate, error: lastError ?? "unknown" });
13555
13569
  }
13556
13570
  }
13557
- 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 };
13558
13577
  }
13559
13578
 
13560
13579
  // lib/merge-loop.ts
@@ -14657,8 +14676,8 @@ async function sendParentNotice(client, sessionID, text, opts = {}) {
14657
14676
  id: makePartId(),
14658
14677
  type: "text",
14659
14678
  text,
14660
- synthetic: false,
14661
- ignored: false
14679
+ synthetic: true,
14680
+ ignored: true
14662
14681
  }
14663
14682
  ]
14664
14683
  }
@@ -15860,7 +15879,12 @@ var codeforgeToolsServer = async (ctx) => {
15860
15879
  ...sid ? { currentSessionId: sid } : {},
15861
15880
  ...sid ? {
15862
15881
  sendProgress: async (state, detail) => {
15863
- await sendParentNotice(ctx.client, sid, `[merge-loop] ${state}: ${detail}`, ctx.directory ? { directory: ctx.directory } : {});
15882
+ const client = ctx.client;
15883
+ if (typeof client?.tui?.showToast === "function") {
15884
+ await client.tui.showToast({
15885
+ body: { message: `[merge-loop] ${state}: ${detail}`, variant: "default", duration: 4000, title: "CodeForge" }
15886
+ });
15887
+ }
15864
15888
  }
15865
15889
  } : {}
15866
15890
  });
@@ -17837,9 +17861,103 @@ var handler12 = modelFallbackServer;
17837
17861
 
17838
17862
  // plugins/subtask-heartbeat.ts
17839
17863
  import { promises as fsPromises } from "node:fs";
17840
- import * as path18 from "node:path";
17864
+ import * as path19 from "node:path";
17841
17865
  init_runtime_paths();
17842
17866
  init_global_config();
17867
+
17868
+ // lib/parent-map-store.ts
17869
+ init_runtime_paths();
17870
+ import { promises as fs15 } from "node:fs";
17871
+ import * as path18 from "node:path";
17872
+ var PARENT_MAP_VERSION = 1;
17873
+ var PARENT_MAP_LOCK_TIMEOUT_MS = 2000;
17874
+ function parentMapDir(mainRoot) {
17875
+ return path18.join(runtimeDir(path18.resolve(mainRoot), { ensure: false }), "session-worktrees");
17876
+ }
17877
+ function parentMapPath(mainRoot) {
17878
+ return path18.join(parentMapDir(mainRoot), "parent-map.json");
17879
+ }
17880
+ function parentMapLockPath(mainRoot) {
17881
+ return path18.join(parentMapDir(mainRoot), "parent-map.lock");
17882
+ }
17883
+ async function readParentMapFile(mainRoot) {
17884
+ const file = parentMapPath(mainRoot);
17885
+ try {
17886
+ const raw = await fs15.readFile(file, "utf8");
17887
+ const parsed = JSON.parse(raw);
17888
+ if (parsed.version !== PARENT_MAP_VERSION || !Array.isArray(parsed.entries)) {
17889
+ return { version: PARENT_MAP_VERSION, entries: [] };
17890
+ }
17891
+ return parsed;
17892
+ } catch (err) {
17893
+ const e = err;
17894
+ if (e.code === "ENOENT")
17895
+ return { version: PARENT_MAP_VERSION, entries: [] };
17896
+ return { version: PARENT_MAP_VERSION, entries: [] };
17897
+ }
17898
+ }
17899
+ async function writeParentMapFile(mainRoot, payload) {
17900
+ const file = parentMapPath(mainRoot);
17901
+ await fs15.mkdir(path18.dirname(file), { recursive: true });
17902
+ const tmp = `${file}.tmp-${process.pid}-${Date.now()}`;
17903
+ await fs15.writeFile(tmp, JSON.stringify(payload, null, 2), "utf8");
17904
+ await fs15.rename(tmp, file);
17905
+ }
17906
+ async function loadParentMap(mainRoot) {
17907
+ const out = new Map;
17908
+ let file;
17909
+ try {
17910
+ file = await readParentMapFile(mainRoot);
17911
+ } catch {
17912
+ return out;
17913
+ }
17914
+ for (const e of file.entries) {
17915
+ if (!e || typeof e.childID !== "string" || typeof e.parentID !== "string")
17916
+ continue;
17917
+ if (!e.childID || !e.parentID)
17918
+ continue;
17919
+ const ts = typeof e.ts === "number" ? e.ts : Date.now();
17920
+ out.set(e.childID, { parentID: e.parentID, ts });
17921
+ }
17922
+ return out;
17923
+ }
17924
+ async function writeParentMap(mainRoot, snapshot, opts = {}) {
17925
+ const lockPath = parentMapLockPath(mainRoot);
17926
+ await fs15.mkdir(path18.dirname(lockPath), { recursive: true });
17927
+ const lockOpts = {
17928
+ timeoutMs: opts.timeoutMs ?? PARENT_MAP_LOCK_TIMEOUT_MS,
17929
+ ...opts
17930
+ };
17931
+ await withFileLock(lockPath, async () => {
17932
+ const entries = [];
17933
+ for (const [childID, v] of snapshot.entries()) {
17934
+ entries.push({ childID, parentID: v.parentID, ts: v.ts });
17935
+ }
17936
+ await writeParentMapFile(mainRoot, { version: PARENT_MAP_VERSION, entries });
17937
+ }, lockOpts);
17938
+ }
17939
+ async function appendParentEntry(mainRoot, childID, parentID, ts, opts = {}) {
17940
+ if (!childID || !parentID)
17941
+ return;
17942
+ const lockPath = parentMapLockPath(mainRoot);
17943
+ await fs15.mkdir(path18.dirname(lockPath), { recursive: true });
17944
+ const lockOpts = {
17945
+ timeoutMs: opts.timeoutMs ?? PARENT_MAP_LOCK_TIMEOUT_MS,
17946
+ ...opts
17947
+ };
17948
+ await withFileLock(lockPath, async () => {
17949
+ const current = await readParentMapFile(mainRoot);
17950
+ const idx = current.entries.findIndex((e) => e.childID === childID);
17951
+ if (idx >= 0) {
17952
+ current.entries[idx] = { childID, parentID, ts };
17953
+ } else {
17954
+ current.entries.push({ childID, parentID, ts });
17955
+ }
17956
+ await writeParentMapFile(mainRoot, current);
17957
+ }, lockOpts);
17958
+ }
17959
+
17960
+ // plugins/subtask-heartbeat.ts
17843
17961
  var PLUGIN_NAME13 = "subtask-heartbeat";
17844
17962
  logLifecycle(PLUGIN_NAME13, "import", {});
17845
17963
  var HEARTBEAT_INTERVAL_MS2 = 30000;
@@ -17857,6 +17975,14 @@ var inflight3 = new Map;
17857
17975
  var pendingTask = new Map;
17858
17976
  var sessionParentMap = new Map;
17859
17977
  var _parentParseFailLogged = 0;
17978
+ var _persistRoot = null;
17979
+ function _bulkInjectSessionParentMap(entries) {
17980
+ for (const e of entries) {
17981
+ if (!e.childID || !e.parentID)
17982
+ continue;
17983
+ sessionParentMap.set(e.childID, { parentID: e.parentID, ts: e.ts });
17984
+ }
17985
+ }
17860
17986
  function _snapshotInflight() {
17861
17987
  return [...inflight3.values()].map((r) => ({ ...r }));
17862
17988
  }
@@ -17865,21 +17991,29 @@ function recordSessionParent(childID, parentID, now = Date.now()) {
17865
17991
  return;
17866
17992
  if (sessionParentMap.has(childID)) {
17867
17993
  sessionParentMap.set(childID, { parentID, ts: now });
17868
- return;
17869
- }
17870
- if (sessionParentMap.size >= SESSION_PARENT_MAP_MAX_SIZE) {
17871
- let oldestKey = null;
17872
- let oldestTs = Number.POSITIVE_INFINITY;
17873
- for (const [k, v] of sessionParentMap.entries()) {
17874
- if (v.ts < oldestTs) {
17875
- oldestTs = v.ts;
17876
- oldestKey = k;
17994
+ } else {
17995
+ if (sessionParentMap.size >= SESSION_PARENT_MAP_MAX_SIZE) {
17996
+ let oldestKey = null;
17997
+ let oldestTs = Number.POSITIVE_INFINITY;
17998
+ for (const [k, v] of sessionParentMap.entries()) {
17999
+ if (v.ts < oldestTs) {
18000
+ oldestTs = v.ts;
18001
+ oldestKey = k;
18002
+ }
17877
18003
  }
18004
+ if (oldestKey !== null)
18005
+ sessionParentMap.delete(oldestKey);
17878
18006
  }
17879
- if (oldestKey !== null)
17880
- sessionParentMap.delete(oldestKey);
18007
+ sessionParentMap.set(childID, { parentID, ts: now });
18008
+ }
18009
+ if (_persistRoot) {
18010
+ appendParentEntry(_persistRoot, childID, parentID, now).catch((err) => {
18011
+ log7.warn("appendParentEntry 失败(已隔离)", {
18012
+ error: err instanceof Error ? err.message : String(err),
18013
+ childID
18014
+ });
18015
+ });
17881
18016
  }
17882
- sessionParentMap.set(childID, { parentID, ts: now });
17883
18017
  }
17884
18018
  function lookupParentSessionId(childID, now = Date.now()) {
17885
18019
  const entry = sessionParentMap.get(childID);
@@ -17893,6 +18027,15 @@ function lookupParentSessionId(childID, now = Date.now()) {
17893
18027
  }
17894
18028
  function deleteSessionParent(childID) {
17895
18029
  sessionParentMap.delete(childID);
18030
+ if (_persistRoot) {
18031
+ const snapshot = new Map(sessionParentMap);
18032
+ writeParentMap(_persistRoot, snapshot).catch((err) => {
18033
+ log7.warn("writeParentMap (delete) 失败(已隔离)", {
18034
+ error: err instanceof Error ? err.message : String(err),
18035
+ childID
18036
+ });
18037
+ });
18038
+ }
17896
18039
  }
17897
18040
  function sweepExpiredSessionParents(now = Date.now()) {
17898
18041
  let removed = 0;
@@ -17902,6 +18045,15 @@ function sweepExpiredSessionParents(now = Date.now()) {
17902
18045
  removed++;
17903
18046
  }
17904
18047
  }
18048
+ if (removed > 0 && _persistRoot) {
18049
+ const snapshot = new Map(sessionParentMap);
18050
+ writeParentMap(_persistRoot, snapshot).catch((err) => {
18051
+ log7.warn("writeParentMap (sweep) 失败(已隔离)", {
18052
+ error: err instanceof Error ? err.message : String(err),
18053
+ removed
18054
+ });
18055
+ });
18056
+ }
17905
18057
  return removed;
17906
18058
  }
17907
18059
  function detectUnparsedParentID(event) {
@@ -18199,7 +18351,7 @@ function buildFailureNotice(r, endedType, logPath, worktreePath, now = Date.now(
18199
18351
  }
18200
18352
  async function appendSubagentLog(filePath, line, log7) {
18201
18353
  try {
18202
- await fsPromises.mkdir(path18.dirname(filePath), { recursive: true });
18354
+ await fsPromises.mkdir(path19.dirname(filePath), { recursive: true });
18203
18355
  await fsPromises.appendFile(filePath, line + `
18204
18356
  `, "utf8");
18205
18357
  } catch (err) {
@@ -18258,6 +18410,27 @@ var subtaskHeartbeatServer = async (ctx) => {
18258
18410
  });
18259
18411
  const client = ctx.client;
18260
18412
  const cwd = ctx.directory;
18413
+ _persistRoot = cwd;
18414
+ try {
18415
+ const restored = await loadParentMap(cwd);
18416
+ if (restored.size > 0) {
18417
+ const entries = [...restored.entries()].map(([childID, v]) => ({
18418
+ childID,
18419
+ parentID: v.parentID,
18420
+ ts: v.ts
18421
+ }));
18422
+ _bulkInjectSessionParentMap(entries);
18423
+ safeWriteLog(PLUGIN_NAME13, {
18424
+ hook: "activate",
18425
+ type: "parent-map.restore",
18426
+ restored: restored.size
18427
+ });
18428
+ }
18429
+ } catch (err) {
18430
+ log7.warn("loadParentMap 失败(已隔离),降级为空表", {
18431
+ error: err instanceof Error ? err.message : String(err)
18432
+ });
18433
+ }
18261
18434
  const interval = setInterval(() => {
18262
18435
  safeAsync(PLUGIN_NAME13, "interval", async () => {
18263
18436
  const swept = sweepExpiredPendingTasks();
@@ -18532,7 +18705,7 @@ var handler14 = parallelStatusServer;
18532
18705
 
18533
18706
  // plugins/parallel-tool-nudge.ts
18534
18707
  import { readFileSync as readFileSync4, readdirSync as readdirSync2, statSync as statSync4 } from "node:fs";
18535
- import { join as join15 } from "node:path";
18708
+ import { join as join16 } from "node:path";
18536
18709
  import { homedir as homedir6 } from "node:os";
18537
18710
  var PLUGIN_NAME15 = "parallel-tool-nudge";
18538
18711
  logLifecycle(PLUGIN_NAME15, "import", {});
@@ -18588,10 +18761,10 @@ function loadAgentToolsMap(rootDir, opts = {}) {
18588
18761
  const reader = opts.reader ?? defaultReader2;
18589
18762
  const dirReader = opts.dirReader ?? defaultDirReader2;
18590
18763
  const dirExists = opts.dirExists ?? defaultDirExists2;
18591
- const homeAgentsDir = opts.homeAgentsDir ?? join15(homedir6(), ".config", "opencode", "agents");
18764
+ const homeAgentsDir = opts.homeAgentsDir ?? join16(homedir6(), ".config", "opencode", "agents");
18592
18765
  const candidateDirs = [
18593
- join15(rootDir, ".codeforge", "agents"),
18594
- join15(rootDir, "agents"),
18766
+ join16(rootDir, ".codeforge", "agents"),
18767
+ join16(rootDir, "agents"),
18595
18768
  homeAgentsDir
18596
18769
  ];
18597
18770
  const result = new Map;
@@ -18614,20 +18787,20 @@ function loadAgentToolsMap(rootDir, opts = {}) {
18614
18787
  for (const entry of entries) {
18615
18788
  if (!entry.endsWith(".md"))
18616
18789
  continue;
18617
- const path19 = join15(dir, entry);
18790
+ const path20 = join16(dir, entry);
18618
18791
  let content;
18619
18792
  try {
18620
- content = reader(path19);
18793
+ content = reader(path20);
18621
18794
  } catch (err) {
18622
18795
  log8.warn(`agent.md 读取失败(已跳过)`, {
18623
- path: path19,
18796
+ path: path20,
18624
18797
  error: err instanceof Error ? err.message : String(err)
18625
18798
  });
18626
18799
  continue;
18627
18800
  }
18628
18801
  const parsed = parseAgentFrontmatter(content);
18629
18802
  if (!parsed) {
18630
- log8.warn(`agent frontmatter 解析失败(已跳过)`, { path: path19 });
18803
+ log8.warn(`agent frontmatter 解析失败(已跳过)`, { path: path20 });
18631
18804
  continue;
18632
18805
  }
18633
18806
  if (result.has(parsed.name))
@@ -18818,19 +18991,19 @@ var handler16 = async (_ctx3) => {
18818
18991
  };
18819
18992
 
18820
18993
  // lib/event-stream.ts
18821
- import { promises as fs15 } from "node:fs";
18994
+ import { promises as fs16 } from "node:fs";
18822
18995
  init_runtime_paths();
18823
- import * as path19 from "node:path";
18996
+ import * as path20 from "node:path";
18824
18997
  async function loadSession(id, opts = {}) {
18825
18998
  const file = resolveSessionFile(id, opts);
18826
- const raw = await fs15.readFile(file, "utf8");
18999
+ const raw = await fs16.readFile(file, "utf8");
18827
19000
  return parseJsonl(id, raw);
18828
19001
  }
18829
19002
  async function listSessions(opts = {}) {
18830
19003
  const dir = resolveDir(opts);
18831
19004
  let entries;
18832
19005
  try {
18833
- entries = await fs15.readdir(dir, { withFileTypes: true });
19006
+ entries = await fs16.readdir(dir, { withFileTypes: true });
18834
19007
  } catch (err) {
18835
19008
  if (err.code === "ENOENT")
18836
19009
  return [];
@@ -18840,10 +19013,10 @@ async function listSessions(opts = {}) {
18840
19013
  for (const e of entries) {
18841
19014
  if (!e.isFile() || !e.name.endsWith(".jsonl"))
18842
19015
  continue;
18843
- const file = path19.join(dir, e.name);
19016
+ const file = path20.join(dir, e.name);
18844
19017
  const id = e.name.replace(/\.jsonl$/, "");
18845
19018
  try {
18846
- const stat = await fs15.stat(file);
19019
+ const stat = await fs16.stat(file);
18847
19020
  const headerLine = await readFirstLine(file);
18848
19021
  let started_at = stat.birthtimeMs;
18849
19022
  if (headerLine) {
@@ -18867,11 +19040,11 @@ async function listSessions(opts = {}) {
18867
19040
  return out;
18868
19041
  }
18869
19042
  function resolveDir(opts = {}) {
18870
- const root = path19.resolve(opts.root ?? process.cwd());
18871
- return opts.sessions_dir ? path19.resolve(root, opts.sessions_dir) : path19.join(runtimeDir(root), "sessions");
19043
+ const root = path20.resolve(opts.root ?? process.cwd());
19044
+ return opts.sessions_dir ? path20.resolve(root, opts.sessions_dir) : path20.join(runtimeDir(root), "sessions");
18872
19045
  }
18873
19046
  function resolveSessionFile(id, opts = {}) {
18874
- return path19.join(resolveDir(opts), `${id}.jsonl`);
19047
+ return path20.join(resolveDir(opts), `${id}.jsonl`);
18875
19048
  }
18876
19049
  function parseJsonl(id, raw) {
18877
19050
  const events = [];
@@ -18906,7 +19079,7 @@ function isEvent(obj) {
18906
19079
  }
18907
19080
  async function readFirstLine(file) {
18908
19081
  const buf = Buffer.alloc(4096);
18909
- const fh = await fs15.open(file, "r");
19082
+ const fh = await fs16.open(file, "r");
18910
19083
  try {
18911
19084
  const { bytesRead } = await fh.read(buf, 0, buf.length, 0);
18912
19085
  const s = buf.subarray(0, bytesRead).toString("utf8");
@@ -19133,6 +19306,92 @@ function isRecoveryWorthShowing(plan) {
19133
19306
  return hasSignal;
19134
19307
  }
19135
19308
 
19309
+ // lib/block-pending.ts
19310
+ init_runtime_paths();
19311
+ import { promises as fs17 } from "node:fs";
19312
+ import * as path21 from "node:path";
19313
+ function blockPendingFilePath(absRoot) {
19314
+ const rd = runtimeDir(absRoot, { ensure: false });
19315
+ return path21.join(rd, "sessions", "autonomous-blocks.ndjson");
19316
+ }
19317
+ function consumeLockPath(absRoot) {
19318
+ return blockPendingFilePath(absRoot) + ".consume.lock";
19319
+ }
19320
+ async function scanBlockPending(absRoot, filterSessionId) {
19321
+ const file = blockPendingFilePath(absRoot);
19322
+ let raw;
19323
+ try {
19324
+ raw = await fs17.readFile(file, "utf8");
19325
+ } catch {
19326
+ return [];
19327
+ }
19328
+ if (!raw)
19329
+ return [];
19330
+ const consumed = new Set;
19331
+ const entries = [];
19332
+ for (const line of raw.split(`
19333
+ `)) {
19334
+ const trimmed = line.trim();
19335
+ if (!trimmed)
19336
+ continue;
19337
+ let obj;
19338
+ try {
19339
+ obj = JSON.parse(trimmed);
19340
+ } catch {
19341
+ continue;
19342
+ }
19343
+ if (!obj || typeof obj !== "object")
19344
+ continue;
19345
+ if (obj["type"] === "consume") {
19346
+ const sid = obj["sessionId"];
19347
+ const ts = obj["timestamp"];
19348
+ if (typeof sid === "string" && typeof ts === "string") {
19349
+ consumed.add(`${sid}|${ts}`);
19350
+ }
19351
+ continue;
19352
+ }
19353
+ const sessionId = obj["sessionId"];
19354
+ const timestamp = obj["timestamp"];
19355
+ if (typeof sessionId !== "string" || typeof timestamp !== "string")
19356
+ continue;
19357
+ const entry = {
19358
+ sessionId,
19359
+ timestamp,
19360
+ reason: typeof obj["reason"] === "string" ? obj["reason"] : undefined,
19361
+ summary_excerpt: typeof obj["summary_excerpt"] === "string" ? obj["summary_excerpt"] : undefined,
19362
+ consumed_at: typeof obj["consumed_at"] === "string" ? obj["consumed_at"] : undefined
19363
+ };
19364
+ entries.push(entry);
19365
+ }
19366
+ return entries.filter((e) => {
19367
+ if (e.consumed_at)
19368
+ return false;
19369
+ if (consumed.has(`${e.sessionId}|${e.timestamp}`))
19370
+ return false;
19371
+ if (filterSessionId && e.sessionId !== filterSessionId)
19372
+ return false;
19373
+ return true;
19374
+ });
19375
+ }
19376
+ async function markBlocksConsumed(absRoot, entries) {
19377
+ if (entries.length === 0)
19378
+ return;
19379
+ const file = blockPendingFilePath(absRoot);
19380
+ await fs17.mkdir(path21.dirname(file), { recursive: true });
19381
+ const now = new Date().toISOString();
19382
+ const lines = entries.map((e) => ({
19383
+ type: "consume",
19384
+ sessionId: e.sessionId,
19385
+ timestamp: e.timestamp,
19386
+ consumed_at: now
19387
+ })).map((row) => JSON.stringify(row)).join(`
19388
+ `) + `
19389
+ `;
19390
+ await withFileLock(consumeLockPath(absRoot), async () => {
19391
+ await fs17.appendFile(file, lines, "utf8");
19392
+ });
19393
+ }
19394
+
19136
19395
  // plugins/session-recovery.ts
19137
19396
  var PLUGIN_NAME17 = "session-recovery";
19138
19397
  logLifecycle(PLUGIN_NAME17, "import", {});
@@ -19149,10 +19408,23 @@ async function processSessionStart(currentSessionId, opts = {}) {
19149
19408
  return { ok: false, injected: false, reason: "scan_error", error: r.error };
19150
19409
  }
19151
19410
  const plan = r.plan;
19152
- if (!isRecoveryWorthShowing(plan)) {
19153
- return { ok: true, injected: false, plan, reason: "no_signal" };
19411
+ let pendingBlocks = [];
19412
+ if (opts.root) {
19413
+ try {
19414
+ pendingBlocks = await scanBlockPending(opts.root);
19415
+ } catch (err) {
19416
+ opts.log?.warn?.(`[${PLUGIN_NAME17}] scanBlockPending 异常:${err instanceof Error ? err.message : String(err)}`);
19417
+ }
19418
+ }
19419
+ const hasPendingBlocks = pendingBlocks.length > 0;
19420
+ if (!isRecoveryWorthShowing(plan) && !hasPendingBlocks) {
19421
+ return { ok: true, injected: false, plan, reason: "no_signal", pendingBlocks };
19154
19422
  }
19155
- const prompt = renderPrompt(plan);
19423
+ const blockPrompt = hasPendingBlocks ? renderBlockPendingPrompt(pendingBlocks) : "";
19424
+ const recoveryPrompt = isRecoveryWorthShowing(plan) ? renderPrompt(plan) : "";
19425
+ const prompt = [blockPrompt, recoveryPrompt].filter((s) => s.length > 0).join(`
19426
+
19427
+ `);
19156
19428
  const injection = { source: "session-recovery", plan, prompt };
19157
19429
  if (opts.injectRecovery) {
19158
19430
  try {
@@ -19160,10 +19432,36 @@ async function processSessionStart(currentSessionId, opts = {}) {
19160
19432
  } catch (err) {
19161
19433
  const msg = err instanceof Error ? err.message : String(err);
19162
19434
  opts.log?.warn?.(`[${PLUGIN_NAME17}] injectRecovery 异常:${msg}`);
19163
- return { ok: false, injected: false, plan, reason: "inject_error", error: msg };
19435
+ return { ok: false, injected: false, plan, reason: "inject_error", error: msg, pendingBlocks };
19164
19436
  }
19165
19437
  }
19166
- return { ok: true, injected: true, plan, reason: "ok" };
19438
+ if (hasPendingBlocks && opts.root) {
19439
+ try {
19440
+ await markBlocksConsumed(opts.root, pendingBlocks);
19441
+ } catch (err) {
19442
+ opts.log?.warn?.(`[${PLUGIN_NAME17}] markBlocksConsumed 异常:${err instanceof Error ? err.message : String(err)}`);
19443
+ }
19444
+ }
19445
+ return { ok: true, injected: true, plan, reason: "ok", pendingBlocks };
19446
+ }
19447
+ function renderBlockPendingPrompt(entries) {
19448
+ const lines = [];
19449
+ lines.push(`⚠️ **${entries.length} 个未读 BLOCK 通知**(上次 auto 模式 reviewer 阻断、channels 通知失败):`);
19450
+ lines.push("");
19451
+ entries.forEach((e, i) => {
19452
+ const sidShort = e.sessionId.length > 12 ? e.sessionId.slice(0, 8) + "…" : e.sessionId;
19453
+ const reasonPart = e.reason ? `:${e.reason}` : "";
19454
+ lines.push(`${i + 1}. session \`${sidShort}\` @ ${e.timestamp}${reasonPart}`);
19455
+ if (e.summary_excerpt) {
19456
+ const excerpt = e.summary_excerpt.length > 200 ? e.summary_excerpt.slice(0, 200) + "…" : e.summary_excerpt;
19457
+ lines.push(` > ${excerpt.replace(/\n/g, `
19458
+ > `)}`);
19459
+ }
19460
+ });
19461
+ lines.push("");
19462
+ lines.push("请按用户偏好处理上述 BLOCK(重派 reviewer / 用户决策),再继续其它工作。");
19463
+ return lines.join(`
19464
+ `);
19167
19465
  }
19168
19466
  function renderPrompt(plan) {
19169
19467
  const lines = [];
@@ -19215,10 +19513,11 @@ var sessionRecoveryServer = async (ctx) => {
19215
19513
  ok: r.ok,
19216
19514
  injected: r.injected,
19217
19515
  reason: r.reason,
19218
- last_session_id: r.plan?.last_session_id
19516
+ last_session_id: r.plan?.last_session_id,
19517
+ pending_blocks_count: r.pendingBlocks?.length ?? 0
19219
19518
  });
19220
19519
  if (r.injected && r.plan) {
19221
- log9.info(`[${PLUGIN_NAME17}] 注入恢复提示(last=${r.plan.last_session_id?.slice(0, 8) ?? "?"}, reason=${r.plan.reason})`);
19520
+ log9.info(`[${PLUGIN_NAME17}] 注入恢复提示(last=${r.plan.last_session_id?.slice(0, 8) ?? "?"}, reason=${r.plan.reason}, pending_blocks=${r.pendingBlocks?.length ?? 0})`);
19222
19521
  }
19223
19522
  });
19224
19523
  }
@@ -19227,8 +19526,8 @@ var sessionRecoveryServer = async (ctx) => {
19227
19526
  var handler17 = sessionRecoveryServer;
19228
19527
 
19229
19528
  // plugins/subtasks.ts
19230
- import { promises as fs16 } from "node:fs";
19231
- import * as path20 from "node:path";
19529
+ import { promises as fs18 } from "node:fs";
19530
+ import * as path22 from "node:path";
19232
19531
 
19233
19532
  // lib/parallel-merge.ts
19234
19533
  init_autonomy();
@@ -20046,14 +20345,14 @@ function describe8(err) {
20046
20345
  }
20047
20346
  }
20048
20347
  function sleep2(ms) {
20049
- return new Promise((resolve15) => setTimeout(resolve15, ms));
20348
+ return new Promise((resolve16) => setTimeout(resolve16, ms));
20050
20349
  }
20051
20350
 
20052
20351
  // plugins/subtasks.ts
20053
20352
  init_runtime_paths();
20054
20353
  var PLUGIN_NAME18 = "subtasks";
20055
20354
  function getLogFile(root = process.cwd()) {
20056
- return path20.join(runtimeDir(root), "logs", "subtasks.log");
20355
+ return path22.join(runtimeDir(root), "logs", "subtasks.log");
20057
20356
  }
20058
20357
  var VERB_RE = /^([a-zA-Z]{3,12})/;
20059
20358
  var CN_VERBS = [
@@ -20358,8 +20657,8 @@ async function writeLog(level, msg, data) {
20358
20657
  `;
20359
20658
  try {
20360
20659
  const logFile = getLogFile();
20361
- await fs16.mkdir(path20.dirname(logFile), { recursive: true });
20362
- await fs16.appendFile(logFile, line, "utf8");
20660
+ await fs18.mkdir(path22.dirname(logFile), { recursive: true });
20661
+ await fs18.appendFile(logFile, line, "utf8");
20363
20662
  } catch {}
20364
20663
  }
20365
20664
  logLifecycle(PLUGIN_NAME18, "import");
@@ -20875,12 +21174,12 @@ var tokenManagerServer = async (ctx) => {
20875
21174
  var handler20 = tokenManagerServer;
20876
21175
 
20877
21176
  // plugins/tool-policy.ts
20878
- import { promises as fs17 } from "node:fs";
20879
- import * as path22 from "node:path";
21177
+ import { promises as fs19 } from "node:fs";
21178
+ import * as path24 from "node:path";
20880
21179
  init_autonomy();
20881
21180
 
20882
21181
  // lib/file-regex-acl.ts
20883
- import * as path21 from "node:path";
21182
+ import * as path23 from "node:path";
20884
21183
  function compileRule(r) {
20885
21184
  if (r instanceof RegExp)
20886
21185
  return r;
@@ -20946,7 +21245,7 @@ function normalizePath2(p) {
20946
21245
  let s = p.replace(/\\/g, "/");
20947
21246
  if (s.startsWith("./"))
20948
21247
  s = s.slice(2);
20949
- s = path21.posix.normalize(s);
21248
+ s = path23.posix.normalize(s);
20950
21249
  return s;
20951
21250
  }
20952
21251
  function checkFileAccess(acl, file, op) {
@@ -21056,11 +21355,11 @@ function decideToolCall(ctx, cfg = {}, currentAgent) {
21056
21355
  action = "deny";
21057
21356
  return { action, reasons, autonomy: a, acl: aclResults };
21058
21357
  }
21059
- var POLICY_PATH = path22.join(".codeforge", "policy.json");
21358
+ var POLICY_PATH = path24.join(".codeforge", "policy.json");
21060
21359
  async function loadPolicy(root = process.cwd()) {
21061
- const file = path22.join(root, POLICY_PATH);
21360
+ const file = path24.join(root, POLICY_PATH);
21062
21361
  try {
21063
- const raw = await fs17.readFile(file, "utf8");
21362
+ const raw = await fs19.readFile(file, "utf8");
21064
21363
  const data = JSON.parse(raw);
21065
21364
  return data;
21066
21365
  } catch {
@@ -21170,7 +21469,7 @@ var handler21 = toolPolicyServer;
21170
21469
  // plugins/update-checker.ts
21171
21470
  import { existsSync as existsSync5 } from "node:fs";
21172
21471
  import { homedir as homedir8 } from "node:os";
21173
- import { join as join20 } from "node:path";
21472
+ import { join as join22 } from "node:path";
21174
21473
 
21175
21474
  // lib/update-checker-impl.ts
21176
21475
  import { createHash as createHash5 } from "node:crypto";
@@ -21187,7 +21486,7 @@ import {
21187
21486
  writeFileSync as writeFileSync2
21188
21487
  } from "node:fs";
21189
21488
  import { homedir as homedir7, tmpdir } from "node:os";
21190
- import { dirname as dirname11, join as join19 } from "node:path";
21489
+ import { dirname as dirname13, join as join21 } from "node:path";
21191
21490
  import { fileURLToPath } from "node:url";
21192
21491
  import * as https from "node:https";
21193
21492
  import * as zlib from "node:zlib";
@@ -21195,7 +21494,7 @@ import * as zlib from "node:zlib";
21195
21494
  // lib/version-injected.ts
21196
21495
  function getInjectedVersion() {
21197
21496
  try {
21198
- const v = "0.5.4";
21497
+ const v = "0.5.6";
21199
21498
  if (typeof v === "string" && /^\d+\.\d+\.\d+/.test(v)) {
21200
21499
  return v;
21201
21500
  }
@@ -21284,18 +21583,18 @@ function readLocalVersion() {
21284
21583
  return injected;
21285
21584
  try {
21286
21585
  const here = fileURLToPath(import.meta.url);
21287
- const root = dirname11(dirname11(here));
21288
- const pkg = JSON.parse(readFileSync5(join19(root, "package.json"), "utf8"));
21586
+ const root = dirname13(dirname13(here));
21587
+ const pkg = JSON.parse(readFileSync5(join21(root, "package.json"), "utf8"));
21289
21588
  return typeof pkg.version === "string" ? pkg.version : "0.0.0";
21290
21589
  } catch {
21291
21590
  return "0.0.0";
21292
21591
  }
21293
21592
  }
21294
21593
  function defaultCacheDir() {
21295
- return process.env["CODEFORGE_CACHE_DIR"] ?? join19(homedir7(), ".cache", "codeforge");
21594
+ return process.env["CODEFORGE_CACHE_DIR"] ?? join21(homedir7(), ".cache", "codeforge");
21296
21595
  }
21297
21596
  function defaultCacheFile() {
21298
- return join19(defaultCacheDir(), "update-check.json");
21597
+ return join21(defaultCacheDir(), "update-check.json");
21299
21598
  }
21300
21599
  function readCache(file) {
21301
21600
  try {
@@ -21313,7 +21612,7 @@ function readCache(file) {
21313
21612
  }
21314
21613
  function writeCache(file, entry) {
21315
21614
  try {
21316
- mkdirSync3(dirname11(file), { recursive: true });
21615
+ mkdirSync3(dirname13(file), { recursive: true });
21317
21616
  writeFileSync2(file, JSON.stringify(entry, null, 2), "utf8");
21318
21617
  } catch {}
21319
21618
  }
@@ -21332,7 +21631,7 @@ function fetchLatestTagFromGitHub(repo) {
21332
21631
  });
21333
21632
  }
21334
21633
  function getJsonWithRedirect(url, hopsLeft) {
21335
- return new Promise((resolve15, reject) => {
21634
+ return new Promise((resolve16, reject) => {
21336
21635
  const u = new URL(url);
21337
21636
  const headers = {
21338
21637
  "User-Agent": "codeforge-update-checker",
@@ -21356,12 +21655,12 @@ function getJsonWithRedirect(url, hopsLeft) {
21356
21655
  return;
21357
21656
  }
21358
21657
  const next = new URL(res.headers.location, url).toString();
21359
- getJsonWithRedirect(next, hopsLeft - 1).then(resolve15, reject);
21658
+ getJsonWithRedirect(next, hopsLeft - 1).then(resolve16, reject);
21360
21659
  return;
21361
21660
  }
21362
21661
  if (status === 404) {
21363
21662
  res.resume();
21364
- resolve15(null);
21663
+ resolve16(null);
21365
21664
  return;
21366
21665
  }
21367
21666
  if (status >= 400) {
@@ -21372,7 +21671,7 @@ function getJsonWithRedirect(url, hopsLeft) {
21372
21671
  let body = "";
21373
21672
  res.setEncoding("utf8");
21374
21673
  res.on("data", (chunk) => body += chunk);
21375
- res.on("end", () => resolve15(body));
21674
+ res.on("end", () => resolve16(body));
21376
21675
  });
21377
21676
  req.on("timeout", () => {
21378
21677
  req.destroy();
@@ -21412,7 +21711,7 @@ async function fetchLatestFromNpm(opts) {
21412
21711
  return { version, tarballUrl, integrity };
21413
21712
  }
21414
21713
  function defaultHttpFetcher(url, timeoutMs) {
21415
- return new Promise((resolve15, reject) => {
21714
+ return new Promise((resolve16, reject) => {
21416
21715
  const u = new URL(url);
21417
21716
  const headers = {
21418
21717
  "User-Agent": "codeforge-update-checker",
@@ -21429,7 +21728,7 @@ function defaultHttpFetcher(url, timeoutMs) {
21429
21728
  const status = res.statusCode ?? 0;
21430
21729
  if (status === 404) {
21431
21730
  res.resume();
21432
- resolve15(null);
21731
+ resolve16(null);
21433
21732
  return;
21434
21733
  }
21435
21734
  if (status >= 400) {
@@ -21440,7 +21739,7 @@ function defaultHttpFetcher(url, timeoutMs) {
21440
21739
  let body = "";
21441
21740
  res.setEncoding("utf8");
21442
21741
  res.on("data", (chunk) => body += chunk);
21443
- res.on("end", () => resolve15(body));
21742
+ res.on("end", () => resolve16(body));
21444
21743
  });
21445
21744
  req.on("timeout", () => {
21446
21745
  req.destroy();
@@ -21451,14 +21750,14 @@ function defaultHttpFetcher(url, timeoutMs) {
21451
21750
  });
21452
21751
  }
21453
21752
  async function downloadAndExtractBundle(opts) {
21454
- const tmpRoot = opts.tmpDir ?? mkdtempSync(join19(tmpdir(), "codeforge-update-"));
21753
+ const tmpRoot = opts.tmpDir ?? mkdtempSync(join21(tmpdir(), "codeforge-update-"));
21455
21754
  mkdirSync3(tmpRoot, { recursive: true });
21456
21755
  const fetcher = opts.tarballFetcher ?? defaultBinaryFetcher;
21457
21756
  const tarballBuf = await fetcher(opts.tarballUrl);
21458
21757
  verifyIntegrity(tarballBuf, opts.expectedIntegrity);
21459
21758
  const tarBuf = zlib.gunzipSync(tarballBuf);
21460
21759
  extractTarToDir(tarBuf, tmpRoot);
21461
- const bundlePath = join19(tmpRoot, "package", "dist", "index.js");
21760
+ const bundlePath = join21(tmpRoot, "package", "dist", "index.js");
21462
21761
  if (!existsSync4(bundlePath)) {
21463
21762
  throw new Error(`bundle_not_found: ${bundlePath}`);
21464
21763
  }
@@ -21498,11 +21797,11 @@ function extractTarToDir(tarBuf, destRoot) {
21498
21797
  offset += 512;
21499
21798
  if (typeFlag === "0" || typeFlag === "" || typeFlag === "\x00") {
21500
21799
  const fileBuf = tarBuf.subarray(offset, offset + size);
21501
- const dest = join19(destRoot, fullName);
21502
- mkdirSync3(dirname11(dest), { recursive: true });
21800
+ const dest = join21(destRoot, fullName);
21801
+ mkdirSync3(dirname13(dest), { recursive: true });
21503
21802
  writeFileSync2(dest, fileBuf);
21504
21803
  } else if (typeFlag === "5") {
21505
- mkdirSync3(join19(destRoot, fullName), { recursive: true });
21804
+ mkdirSync3(join21(destRoot, fullName), { recursive: true });
21506
21805
  }
21507
21806
  offset += Math.ceil(size / 512) * 512;
21508
21807
  }
@@ -21511,7 +21810,7 @@ function defaultBinaryFetcher(url) {
21511
21810
  return downloadBinary(url, 3);
21512
21811
  }
21513
21812
  function downloadBinary(url, hopsLeft) {
21514
- return new Promise((resolve15, reject) => {
21813
+ return new Promise((resolve16, reject) => {
21515
21814
  const u = new URL(url);
21516
21815
  const req = https.request({
21517
21816
  host: u.hostname,
@@ -21529,7 +21828,7 @@ function downloadBinary(url, hopsLeft) {
21529
21828
  return;
21530
21829
  }
21531
21830
  const next = new URL(res.headers.location, url).toString();
21532
- downloadBinary(next, hopsLeft - 1).then(resolve15, reject);
21831
+ downloadBinary(next, hopsLeft - 1).then(resolve16, reject);
21533
21832
  return;
21534
21833
  }
21535
21834
  if (status >= 400) {
@@ -21539,7 +21838,7 @@ function downloadBinary(url, hopsLeft) {
21539
21838
  }
21540
21839
  const chunks = [];
21541
21840
  res.on("data", (chunk) => chunks.push(chunk));
21542
- res.on("end", () => resolve15(Buffer.concat(chunks)));
21841
+ res.on("end", () => resolve16(Buffer.concat(chunks)));
21543
21842
  });
21544
21843
  req.on("timeout", () => {
21545
21844
  req.destroy();
@@ -21555,7 +21854,7 @@ function atomicReplaceBundle(opts) {
21555
21854
  if (!existsSync4(source)) {
21556
21855
  throw new Error(`atomic_source_missing: ${source}`);
21557
21856
  }
21558
- mkdirSync3(dirname11(target), { recursive: true });
21857
+ mkdirSync3(dirname13(target), { recursive: true });
21559
21858
  const newPath = `${target}.new`;
21560
21859
  const backupPath = `${target}.bak.${oldVersion}`;
21561
21860
  let strategy = "rename";
@@ -21607,11 +21906,11 @@ function cleanupOldBackups(target, keep) {
21607
21906
  if (keep <= 0)
21608
21907
  return;
21609
21908
  try {
21610
- const dir = dirname11(target);
21909
+ const dir = dirname13(target);
21611
21910
  const base = target.substring(dir.length + 1);
21612
21911
  const prefix = `${base}.bak.`;
21613
21912
  const all = readdirSync3(dir).filter((f) => f.startsWith(prefix)).map((f) => {
21614
- const full = join19(dir, f);
21913
+ const full = join21(dir, f);
21615
21914
  let mtimeMs = 0;
21616
21915
  try {
21617
21916
  mtimeMs = statSync5(full).mtimeMs;
@@ -21633,7 +21932,7 @@ function loadCompatibility(opts) {
21633
21932
  const root = opts?.cwd ?? inferPluginRoot();
21634
21933
  if (!root)
21635
21934
  return null;
21636
- file = join19(root, "compatibility.json");
21935
+ file = join21(root, "compatibility.json");
21637
21936
  }
21638
21937
  if (!existsSync4(file))
21639
21938
  return null;
@@ -21658,7 +21957,7 @@ function loadCompatibility(opts) {
21658
21957
  function inferPluginRoot() {
21659
21958
  try {
21660
21959
  const here = fileURLToPath(import.meta.url);
21661
- return dirname11(dirname11(here));
21960
+ return dirname13(dirname13(here));
21662
21961
  } catch {
21663
21962
  return null;
21664
21963
  }
@@ -21853,14 +22152,14 @@ function detectOpencodeVersion() {
21853
22152
  }
21854
22153
  function getOpencodeBundlePath() {
21855
22154
  const candidates = [];
21856
- candidates.push(join20(homedir8(), ".config", "opencode", "codeforge", "index.js"));
22155
+ candidates.push(join22(homedir8(), ".config", "opencode", "codeforge", "index.js"));
21857
22156
  if (process.platform === "win32") {
21858
22157
  const appData = process.env["APPDATA"];
21859
22158
  if (appData)
21860
- candidates.push(join20(appData, "opencode", "codeforge", "index.js"));
22159
+ candidates.push(join22(appData, "opencode", "codeforge", "index.js"));
21861
22160
  const localAppData = process.env["LOCALAPPDATA"];
21862
22161
  if (localAppData)
21863
- candidates.push(join20(localAppData, "opencode", "codeforge", "index.js"));
22162
+ candidates.push(join22(localAppData, "opencode", "codeforge", "index.js"));
21864
22163
  }
21865
22164
  for (const c of candidates) {
21866
22165
  if (existsSync5(c))
@@ -21921,11 +22220,11 @@ async function postToast(ctx, message) {
21921
22220
  var handler22 = updateCheckerServer;
21922
22221
 
21923
22222
  // plugins/workflow-engine.ts
21924
- import * as path24 from "node:path";
22223
+ import * as path26 from "node:path";
21925
22224
 
21926
22225
  // lib/workflow-loader.ts
21927
- import { promises as fs18 } from "node:fs";
21928
- import * as path23 from "node:path";
22226
+ import { promises as fs20 } from "node:fs";
22227
+ import * as path25 from "node:path";
21929
22228
  import { z as z32 } from "zod";
21930
22229
  var ActionSchema = z32.object({
21931
22230
  tool: z32.string().min(1, "action.tool 不能为空"),
@@ -22011,7 +22310,7 @@ function parseWorkflowYaml(yaml, sourcePath = "<inline>") {
22011
22310
  async function loadWorkflowFromFile(filePath) {
22012
22311
  let txt;
22013
22312
  try {
22014
- txt = await fs18.readFile(filePath, "utf8");
22313
+ txt = await fs20.readFile(filePath, "utf8");
22015
22314
  } catch (err) {
22016
22315
  return {
22017
22316
  ok: false,
@@ -22026,7 +22325,7 @@ async function loadWorkflowsFromDir(dir) {
22026
22325
  const failed = [];
22027
22326
  let entries;
22028
22327
  try {
22029
- entries = await fs18.readdir(dir);
22328
+ entries = await fs20.readdir(dir);
22030
22329
  } catch (err) {
22031
22330
  const e = err;
22032
22331
  if (e.code === "ENOENT")
@@ -22038,7 +22337,7 @@ async function loadWorkflowsFromDir(dir) {
22038
22337
  continue;
22039
22338
  if (!/\.ya?ml$/i.test(name))
22040
22339
  continue;
22041
- const full = path23.join(dir, name);
22340
+ const full = path25.join(dir, name);
22042
22341
  const r = await loadWorkflowFromFile(full);
22043
22342
  if (r.ok)
22044
22343
  loaded.push(r);
@@ -22428,7 +22727,7 @@ async function handleCommandInvoked(raw, workflowsDir = "workflows") {
22428
22727
  }
22429
22728
  var workflowEngineServer = async (ctx) => {
22430
22729
  const directory = ctx.directory ?? process.cwd();
22431
- const workflowsDir = path24.join(directory, "workflows");
22730
+ const workflowsDir = path26.join(directory, "workflows");
22432
22731
  ensureRegistry(workflowsDir).catch((err) => fallbackLog2.warn(`[${PLUGIN_NAME23}] preload workflows failed`, {
22433
22732
  error: err instanceof Error ? err.message : String(err)
22434
22733
  }));
@@ -22472,7 +22771,7 @@ var workflowEngineServer = async (ctx) => {
22472
22771
  var handler23 = workflowEngineServer;
22473
22772
 
22474
22773
  // plugins/session-worktree-guard.ts
22475
- import path25 from "node:path";
22774
+ import path27 from "node:path";
22476
22775
  var PLUGIN_NAME24 = "session-worktree-guard";
22477
22776
  logLifecycle(PLUGIN_NAME24, "import", {});
22478
22777
  var WRITE_INTENT_RE = />(?!=)|\btee\b|\brm\b|\bmv\b|\bcp\b|\bmkdir\b|\btouch\b|\bchmod\b|\bchown\b|\bln\b/;
@@ -22501,7 +22800,7 @@ var WRITE_TOOLS = new Set(["write", "edit", "ast_edit"]);
22501
22800
  function rewritePath(value, mainRoot, worktreeRoot) {
22502
22801
  if (!value)
22503
22802
  return null;
22504
- const resolved = path25.isAbsolute(value) ? value : path25.resolve(mainRoot, value);
22803
+ const resolved = path27.isAbsolute(value) ? value : path27.resolve(mainRoot, value);
22505
22804
  if (resolved === mainRoot)
22506
22805
  return worktreeRoot;
22507
22806
  const prefix = mainRoot.endsWith("/") ? mainRoot : mainRoot + "/";
@@ -22631,6 +22930,32 @@ var sessionWorktreeGuardPlugin = async (ctx) => {
22631
22930
  }
22632
22931
  }
22633
22932
  const worktreePath = entry.worktreePath;
22933
+ if (toolName === "session_merge") {
22934
+ const action = argsObj["action"];
22935
+ if (action === "merge") {
22936
+ const caller = input.agent;
22937
+ if (caller !== undefined && caller !== "codeforge") {
22938
+ const reason = `[session-worktree-guard] DENIED: session_merge action=merge 仅 codeforge orchestrator 或用户可调;当前 caller=${caller}`;
22939
+ log13.warn(reason, {
22940
+ sessionId,
22941
+ tool: toolName,
22942
+ action,
22943
+ caller
22944
+ });
22945
+ safeWriteLog(PLUGIN_NAME24, {
22946
+ hook: "tool.execute.before",
22947
+ tool: toolName,
22948
+ sessionID: input.sessionID,
22949
+ action: "deny",
22950
+ source: "merge-caller-whitelist",
22951
+ caller,
22952
+ merge_action: "merge"
22953
+ });
22954
+ denied = new DeniedError(reason);
22955
+ return;
22956
+ }
22957
+ }
22958
+ }
22634
22959
  if (toolName !== "plan_read" && entry.requiredPlanId && entry.planReadOk !== true) {
22635
22960
  let isWriteOp = WRITE_TOOLS.has(toolName);
22636
22961
  if (!isWriteOp && toolName === "bash") {
@@ -22818,9 +23143,8 @@ var worktreeLifecyclePlugin = async (ctx) => {
22818
23143
  return;
22819
23144
  }
22820
23145
  try {
22821
- const mainHead = await getCurrentMainHead(mainRoot);
22822
- const worktreeHead = mainHead ? await getCurrentWorktreeHead(entry.worktreePath) : "";
22823
- if (worktreeHead && worktreeHead === mainHead) {
23146
+ const worktreeHead = await getCurrentWorktreeHead(entry.worktreePath);
23147
+ if (worktreeHead && worktreeHead === entry.baseSha) {
22824
23148
  const fastDirty = await isWorktreeDirty(entry.worktreePath);
22825
23149
  if (!fastDirty) {
22826
23150
  await discardSession({ sessionId: ended.sessionID, mainRoot });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@andyqiu/codeforge",
3
- "version": "0.5.4",
3
+ "version": "0.5.6",
4
4
  "description": "CodeForge — opencode 的零侵入扩展包",
5
5
  "type": "module",
6
6
  "private": false,