@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.
- package/agents/codeforge.md +2 -1
- package/commands/autonomy.md +30 -1
- package/dist/index.js +432 -108
- package/package.json +1 -1
package/agents/codeforge.md
CHANGED
|
@@ -59,7 +59,7 @@ fallback_models:
|
|
|
59
59
|
- ❌ 不允许**为并行而并行**:改同文件 / 步骤间有真实依赖时必须串行
|
|
60
60
|
- ❌ 不允许在父对话直接吐长交付物内容
|
|
61
61
|
- ❌ 不允许自动派 coder 修 BLOCK;REQUEST_CHANGES 允许自动派 coder 修,**最多 3 次**
|
|
62
|
-
-
|
|
62
|
+
- ✅ codeforge 可在 review-fix-review APPROVE 后调 `session_merge action=merge`;其他 sub-agent 调此操作会被 guard 拦截(ADR:codeforge-merge-permission);force=true 必须先告知用户并解释跳过 review 的理由
|
|
63
63
|
|
|
64
64
|
## 能力边界(场景分派表)
|
|
65
65
|
|
|
@@ -78,6 +78,7 @@ fallback_models:
|
|
|
78
78
|
| **reviewer 报 REQUEST_CHANGES(代码 review,`code_review_loop_count` < 3)** | **自动派 coder 修**(带具体到行的 reviewer 意见 + 原 plan_id + sessionId),loop +1 | ❌ 同时派多个 coder;❌ 不带 reviewer 意见 |
|
|
79
79
|
| **reviewer 报 REQUEST_CHANGES(loop = 3)** | 转告用户「reviewer 3 次仍 REQUEST_CHANGES」,问「接受 `/merge` / 手动改 / `/discard-session`」三选一 | ❌ 继续派 coder |
|
|
80
80
|
| **reviewer 报 BLOCK** | 转告用户 + 建议派 planner 重设计(带原 plan_id + BLOCK 理由),等用户拍板 | ❌ 派 coder 强行绕过 BLOCK |
|
|
81
|
+
| **review-fix-review 全部通过(APPROVE)** | codeforge orchestrator 自动调 `session_merge action=merge` 完成合入(ADR:codeforge-merge-permission);用户也可通过 `/merge` 命令触发 | ❌ force=true 不告知用户;❌ 派其他 sub-agent 调 session_merge action=merge(会被 guard 拦截) |
|
|
81
82
|
| **coder 回报「PRE 阻断、拒绝启动」** | 转告用户阻断点 + 解除路径,等用户拍板,**不自动派下一棒** | ❌ 自动重派 coder 并强塞 `pre_ack=` |
|
|
82
83
|
| 用户中途插入新需求(原 task 未结束) | 询问用户「先取消 / 等当前完 / 并行」三选一 | ❌ 默默丢弃;❌ 同时派多个不告知 |
|
|
83
84
|
| **可并行任务** | 自动判断依赖,无强依赖时自动并行调度 | ❌ 串行派 N 个独立 task |
|
package/commands/autonomy.md
CHANGED
|
@@ -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
|
-
|
|
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((
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
14661
|
-
ignored:
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
17869
|
-
|
|
17870
|
-
|
|
17871
|
-
|
|
17872
|
-
|
|
17873
|
-
|
|
17874
|
-
|
|
17875
|
-
|
|
17876
|
-
|
|
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
|
-
|
|
17880
|
-
|
|
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(
|
|
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
|
|
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 ??
|
|
18764
|
+
const homeAgentsDir = opts.homeAgentsDir ?? join16(homedir6(), ".config", "opencode", "agents");
|
|
18592
18765
|
const candidateDirs = [
|
|
18593
|
-
|
|
18594
|
-
|
|
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
|
|
18790
|
+
const path20 = join16(dir, entry);
|
|
18618
18791
|
let content;
|
|
18619
18792
|
try {
|
|
18620
|
-
content = reader(
|
|
18793
|
+
content = reader(path20);
|
|
18621
18794
|
} catch (err) {
|
|
18622
18795
|
log8.warn(`agent.md 读取失败(已跳过)`, {
|
|
18623
|
-
path:
|
|
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:
|
|
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
|
|
18994
|
+
import { promises as fs16 } from "node:fs";
|
|
18822
18995
|
init_runtime_paths();
|
|
18823
|
-
import * as
|
|
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
|
|
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
|
|
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 =
|
|
19016
|
+
const file = path20.join(dir, e.name);
|
|
18844
19017
|
const id = e.name.replace(/\.jsonl$/, "");
|
|
18845
19018
|
try {
|
|
18846
|
-
const stat = await
|
|
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 =
|
|
18871
|
-
return opts.sessions_dir ?
|
|
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
|
|
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
|
|
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
|
-
|
|
19153
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
19231
|
-
import * as
|
|
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((
|
|
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
|
|
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
|
|
20362
|
-
await
|
|
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
|
|
20879
|
-
import * as
|
|
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
|
|
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 =
|
|
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 =
|
|
21358
|
+
var POLICY_PATH = path24.join(".codeforge", "policy.json");
|
|
21060
21359
|
async function loadPolicy(root = process.cwd()) {
|
|
21061
|
-
const file =
|
|
21360
|
+
const file = path24.join(root, POLICY_PATH);
|
|
21062
21361
|
try {
|
|
21063
|
-
const raw = await
|
|
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
|
|
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
|
|
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.
|
|
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 =
|
|
21288
|
-
const pkg = JSON.parse(readFileSync5(
|
|
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"] ??
|
|
21594
|
+
return process.env["CODEFORGE_CACHE_DIR"] ?? join21(homedir7(), ".cache", "codeforge");
|
|
21296
21595
|
}
|
|
21297
21596
|
function defaultCacheFile() {
|
|
21298
|
-
return
|
|
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(
|
|
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((
|
|
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(
|
|
21658
|
+
getJsonWithRedirect(next, hopsLeft - 1).then(resolve16, reject);
|
|
21360
21659
|
return;
|
|
21361
21660
|
}
|
|
21362
21661
|
if (status === 404) {
|
|
21363
21662
|
res.resume();
|
|
21364
|
-
|
|
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", () =>
|
|
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((
|
|
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
|
-
|
|
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", () =>
|
|
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(
|
|
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 =
|
|
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 =
|
|
21502
|
-
mkdirSync3(
|
|
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(
|
|
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((
|
|
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(
|
|
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", () =>
|
|
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(
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
|
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(
|
|
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(
|
|
22159
|
+
candidates.push(join22(appData, "opencode", "codeforge", "index.js"));
|
|
21861
22160
|
const localAppData = process.env["LOCALAPPDATA"];
|
|
21862
22161
|
if (localAppData)
|
|
21863
|
-
candidates.push(
|
|
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
|
|
22223
|
+
import * as path26 from "node:path";
|
|
21925
22224
|
|
|
21926
22225
|
// lib/workflow-loader.ts
|
|
21927
|
-
import { promises as
|
|
21928
|
-
import * as
|
|
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
|
|
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
|
|
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 =
|
|
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 =
|
|
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
|
|
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 =
|
|
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
|
|
22822
|
-
|
|
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 });
|