@andyqiu/codeforge 0.5.4 → 0.5.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/agents/codeforge.md +2 -1
- package/commands/autonomy.md +30 -1
- package/dist/index.js +230 -60
- 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
|
@@ -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
|
|
@@ -19133,6 +19152,92 @@ function isRecoveryWorthShowing(plan) {
|
|
|
19133
19152
|
return hasSignal;
|
|
19134
19153
|
}
|
|
19135
19154
|
|
|
19155
|
+
// lib/block-pending.ts
|
|
19156
|
+
init_runtime_paths();
|
|
19157
|
+
import { promises as fs16 } from "node:fs";
|
|
19158
|
+
import * as path20 from "node:path";
|
|
19159
|
+
function blockPendingFilePath(absRoot) {
|
|
19160
|
+
const rd = runtimeDir(absRoot, { ensure: false });
|
|
19161
|
+
return path20.join(rd, "sessions", "autonomous-blocks.ndjson");
|
|
19162
|
+
}
|
|
19163
|
+
function consumeLockPath(absRoot) {
|
|
19164
|
+
return blockPendingFilePath(absRoot) + ".consume.lock";
|
|
19165
|
+
}
|
|
19166
|
+
async function scanBlockPending(absRoot, filterSessionId) {
|
|
19167
|
+
const file = blockPendingFilePath(absRoot);
|
|
19168
|
+
let raw;
|
|
19169
|
+
try {
|
|
19170
|
+
raw = await fs16.readFile(file, "utf8");
|
|
19171
|
+
} catch {
|
|
19172
|
+
return [];
|
|
19173
|
+
}
|
|
19174
|
+
if (!raw)
|
|
19175
|
+
return [];
|
|
19176
|
+
const consumed = new Set;
|
|
19177
|
+
const entries = [];
|
|
19178
|
+
for (const line of raw.split(`
|
|
19179
|
+
`)) {
|
|
19180
|
+
const trimmed = line.trim();
|
|
19181
|
+
if (!trimmed)
|
|
19182
|
+
continue;
|
|
19183
|
+
let obj;
|
|
19184
|
+
try {
|
|
19185
|
+
obj = JSON.parse(trimmed);
|
|
19186
|
+
} catch {
|
|
19187
|
+
continue;
|
|
19188
|
+
}
|
|
19189
|
+
if (!obj || typeof obj !== "object")
|
|
19190
|
+
continue;
|
|
19191
|
+
if (obj["type"] === "consume") {
|
|
19192
|
+
const sid = obj["sessionId"];
|
|
19193
|
+
const ts = obj["timestamp"];
|
|
19194
|
+
if (typeof sid === "string" && typeof ts === "string") {
|
|
19195
|
+
consumed.add(`${sid}|${ts}`);
|
|
19196
|
+
}
|
|
19197
|
+
continue;
|
|
19198
|
+
}
|
|
19199
|
+
const sessionId = obj["sessionId"];
|
|
19200
|
+
const timestamp = obj["timestamp"];
|
|
19201
|
+
if (typeof sessionId !== "string" || typeof timestamp !== "string")
|
|
19202
|
+
continue;
|
|
19203
|
+
const entry = {
|
|
19204
|
+
sessionId,
|
|
19205
|
+
timestamp,
|
|
19206
|
+
reason: typeof obj["reason"] === "string" ? obj["reason"] : undefined,
|
|
19207
|
+
summary_excerpt: typeof obj["summary_excerpt"] === "string" ? obj["summary_excerpt"] : undefined,
|
|
19208
|
+
consumed_at: typeof obj["consumed_at"] === "string" ? obj["consumed_at"] : undefined
|
|
19209
|
+
};
|
|
19210
|
+
entries.push(entry);
|
|
19211
|
+
}
|
|
19212
|
+
return entries.filter((e) => {
|
|
19213
|
+
if (e.consumed_at)
|
|
19214
|
+
return false;
|
|
19215
|
+
if (consumed.has(`${e.sessionId}|${e.timestamp}`))
|
|
19216
|
+
return false;
|
|
19217
|
+
if (filterSessionId && e.sessionId !== filterSessionId)
|
|
19218
|
+
return false;
|
|
19219
|
+
return true;
|
|
19220
|
+
});
|
|
19221
|
+
}
|
|
19222
|
+
async function markBlocksConsumed(absRoot, entries) {
|
|
19223
|
+
if (entries.length === 0)
|
|
19224
|
+
return;
|
|
19225
|
+
const file = blockPendingFilePath(absRoot);
|
|
19226
|
+
await fs16.mkdir(path20.dirname(file), { recursive: true });
|
|
19227
|
+
const now = new Date().toISOString();
|
|
19228
|
+
const lines = entries.map((e) => ({
|
|
19229
|
+
type: "consume",
|
|
19230
|
+
sessionId: e.sessionId,
|
|
19231
|
+
timestamp: e.timestamp,
|
|
19232
|
+
consumed_at: now
|
|
19233
|
+
})).map((row) => JSON.stringify(row)).join(`
|
|
19234
|
+
`) + `
|
|
19235
|
+
`;
|
|
19236
|
+
await withFileLock(consumeLockPath(absRoot), async () => {
|
|
19237
|
+
await fs16.appendFile(file, lines, "utf8");
|
|
19238
|
+
});
|
|
19239
|
+
}
|
|
19240
|
+
|
|
19136
19241
|
// plugins/session-recovery.ts
|
|
19137
19242
|
var PLUGIN_NAME17 = "session-recovery";
|
|
19138
19243
|
logLifecycle(PLUGIN_NAME17, "import", {});
|
|
@@ -19149,10 +19254,23 @@ async function processSessionStart(currentSessionId, opts = {}) {
|
|
|
19149
19254
|
return { ok: false, injected: false, reason: "scan_error", error: r.error };
|
|
19150
19255
|
}
|
|
19151
19256
|
const plan = r.plan;
|
|
19152
|
-
|
|
19153
|
-
|
|
19257
|
+
let pendingBlocks = [];
|
|
19258
|
+
if (opts.root) {
|
|
19259
|
+
try {
|
|
19260
|
+
pendingBlocks = await scanBlockPending(opts.root);
|
|
19261
|
+
} catch (err) {
|
|
19262
|
+
opts.log?.warn?.(`[${PLUGIN_NAME17}] scanBlockPending 异常:${err instanceof Error ? err.message : String(err)}`);
|
|
19263
|
+
}
|
|
19264
|
+
}
|
|
19265
|
+
const hasPendingBlocks = pendingBlocks.length > 0;
|
|
19266
|
+
if (!isRecoveryWorthShowing(plan) && !hasPendingBlocks) {
|
|
19267
|
+
return { ok: true, injected: false, plan, reason: "no_signal", pendingBlocks };
|
|
19154
19268
|
}
|
|
19155
|
-
const
|
|
19269
|
+
const blockPrompt = hasPendingBlocks ? renderBlockPendingPrompt(pendingBlocks) : "";
|
|
19270
|
+
const recoveryPrompt = isRecoveryWorthShowing(plan) ? renderPrompt(plan) : "";
|
|
19271
|
+
const prompt = [blockPrompt, recoveryPrompt].filter((s) => s.length > 0).join(`
|
|
19272
|
+
|
|
19273
|
+
`);
|
|
19156
19274
|
const injection = { source: "session-recovery", plan, prompt };
|
|
19157
19275
|
if (opts.injectRecovery) {
|
|
19158
19276
|
try {
|
|
@@ -19160,10 +19278,36 @@ async function processSessionStart(currentSessionId, opts = {}) {
|
|
|
19160
19278
|
} catch (err) {
|
|
19161
19279
|
const msg = err instanceof Error ? err.message : String(err);
|
|
19162
19280
|
opts.log?.warn?.(`[${PLUGIN_NAME17}] injectRecovery 异常:${msg}`);
|
|
19163
|
-
return { ok: false, injected: false, plan, reason: "inject_error", error: msg };
|
|
19281
|
+
return { ok: false, injected: false, plan, reason: "inject_error", error: msg, pendingBlocks };
|
|
19282
|
+
}
|
|
19283
|
+
}
|
|
19284
|
+
if (hasPendingBlocks && opts.root) {
|
|
19285
|
+
try {
|
|
19286
|
+
await markBlocksConsumed(opts.root, pendingBlocks);
|
|
19287
|
+
} catch (err) {
|
|
19288
|
+
opts.log?.warn?.(`[${PLUGIN_NAME17}] markBlocksConsumed 异常:${err instanceof Error ? err.message : String(err)}`);
|
|
19164
19289
|
}
|
|
19165
19290
|
}
|
|
19166
|
-
return { ok: true, injected: true, plan, reason: "ok" };
|
|
19291
|
+
return { ok: true, injected: true, plan, reason: "ok", pendingBlocks };
|
|
19292
|
+
}
|
|
19293
|
+
function renderBlockPendingPrompt(entries) {
|
|
19294
|
+
const lines = [];
|
|
19295
|
+
lines.push(`⚠️ **${entries.length} 个未读 BLOCK 通知**(上次 auto 模式 reviewer 阻断、channels 通知失败):`);
|
|
19296
|
+
lines.push("");
|
|
19297
|
+
entries.forEach((e, i) => {
|
|
19298
|
+
const sidShort = e.sessionId.length > 12 ? e.sessionId.slice(0, 8) + "…" : e.sessionId;
|
|
19299
|
+
const reasonPart = e.reason ? `:${e.reason}` : "";
|
|
19300
|
+
lines.push(`${i + 1}. session \`${sidShort}\` @ ${e.timestamp}${reasonPart}`);
|
|
19301
|
+
if (e.summary_excerpt) {
|
|
19302
|
+
const excerpt = e.summary_excerpt.length > 200 ? e.summary_excerpt.slice(0, 200) + "…" : e.summary_excerpt;
|
|
19303
|
+
lines.push(` > ${excerpt.replace(/\n/g, `
|
|
19304
|
+
> `)}`);
|
|
19305
|
+
}
|
|
19306
|
+
});
|
|
19307
|
+
lines.push("");
|
|
19308
|
+
lines.push("请按用户偏好处理上述 BLOCK(重派 reviewer / 用户决策),再继续其它工作。");
|
|
19309
|
+
return lines.join(`
|
|
19310
|
+
`);
|
|
19167
19311
|
}
|
|
19168
19312
|
function renderPrompt(plan) {
|
|
19169
19313
|
const lines = [];
|
|
@@ -19215,10 +19359,11 @@ var sessionRecoveryServer = async (ctx) => {
|
|
|
19215
19359
|
ok: r.ok,
|
|
19216
19360
|
injected: r.injected,
|
|
19217
19361
|
reason: r.reason,
|
|
19218
|
-
last_session_id: r.plan?.last_session_id
|
|
19362
|
+
last_session_id: r.plan?.last_session_id,
|
|
19363
|
+
pending_blocks_count: r.pendingBlocks?.length ?? 0
|
|
19219
19364
|
});
|
|
19220
19365
|
if (r.injected && r.plan) {
|
|
19221
|
-
log9.info(`[${PLUGIN_NAME17}] 注入恢复提示(last=${r.plan.last_session_id?.slice(0, 8) ?? "?"}, reason=${r.plan.reason})`);
|
|
19366
|
+
log9.info(`[${PLUGIN_NAME17}] 注入恢复提示(last=${r.plan.last_session_id?.slice(0, 8) ?? "?"}, reason=${r.plan.reason}, pending_blocks=${r.pendingBlocks?.length ?? 0})`);
|
|
19222
19367
|
}
|
|
19223
19368
|
});
|
|
19224
19369
|
}
|
|
@@ -19227,8 +19372,8 @@ var sessionRecoveryServer = async (ctx) => {
|
|
|
19227
19372
|
var handler17 = sessionRecoveryServer;
|
|
19228
19373
|
|
|
19229
19374
|
// plugins/subtasks.ts
|
|
19230
|
-
import { promises as
|
|
19231
|
-
import * as
|
|
19375
|
+
import { promises as fs17 } from "node:fs";
|
|
19376
|
+
import * as path21 from "node:path";
|
|
19232
19377
|
|
|
19233
19378
|
// lib/parallel-merge.ts
|
|
19234
19379
|
init_autonomy();
|
|
@@ -20053,7 +20198,7 @@ function sleep2(ms) {
|
|
|
20053
20198
|
init_runtime_paths();
|
|
20054
20199
|
var PLUGIN_NAME18 = "subtasks";
|
|
20055
20200
|
function getLogFile(root = process.cwd()) {
|
|
20056
|
-
return
|
|
20201
|
+
return path21.join(runtimeDir(root), "logs", "subtasks.log");
|
|
20057
20202
|
}
|
|
20058
20203
|
var VERB_RE = /^([a-zA-Z]{3,12})/;
|
|
20059
20204
|
var CN_VERBS = [
|
|
@@ -20358,8 +20503,8 @@ async function writeLog(level, msg, data) {
|
|
|
20358
20503
|
`;
|
|
20359
20504
|
try {
|
|
20360
20505
|
const logFile = getLogFile();
|
|
20361
|
-
await
|
|
20362
|
-
await
|
|
20506
|
+
await fs17.mkdir(path21.dirname(logFile), { recursive: true });
|
|
20507
|
+
await fs17.appendFile(logFile, line, "utf8");
|
|
20363
20508
|
} catch {}
|
|
20364
20509
|
}
|
|
20365
20510
|
logLifecycle(PLUGIN_NAME18, "import");
|
|
@@ -20875,12 +21020,12 @@ var tokenManagerServer = async (ctx) => {
|
|
|
20875
21020
|
var handler20 = tokenManagerServer;
|
|
20876
21021
|
|
|
20877
21022
|
// plugins/tool-policy.ts
|
|
20878
|
-
import { promises as
|
|
20879
|
-
import * as
|
|
21023
|
+
import { promises as fs18 } from "node:fs";
|
|
21024
|
+
import * as path23 from "node:path";
|
|
20880
21025
|
init_autonomy();
|
|
20881
21026
|
|
|
20882
21027
|
// lib/file-regex-acl.ts
|
|
20883
|
-
import * as
|
|
21028
|
+
import * as path22 from "node:path";
|
|
20884
21029
|
function compileRule(r) {
|
|
20885
21030
|
if (r instanceof RegExp)
|
|
20886
21031
|
return r;
|
|
@@ -20946,7 +21091,7 @@ function normalizePath2(p) {
|
|
|
20946
21091
|
let s = p.replace(/\\/g, "/");
|
|
20947
21092
|
if (s.startsWith("./"))
|
|
20948
21093
|
s = s.slice(2);
|
|
20949
|
-
s =
|
|
21094
|
+
s = path22.posix.normalize(s);
|
|
20950
21095
|
return s;
|
|
20951
21096
|
}
|
|
20952
21097
|
function checkFileAccess(acl, file, op) {
|
|
@@ -21056,11 +21201,11 @@ function decideToolCall(ctx, cfg = {}, currentAgent) {
|
|
|
21056
21201
|
action = "deny";
|
|
21057
21202
|
return { action, reasons, autonomy: a, acl: aclResults };
|
|
21058
21203
|
}
|
|
21059
|
-
var POLICY_PATH =
|
|
21204
|
+
var POLICY_PATH = path23.join(".codeforge", "policy.json");
|
|
21060
21205
|
async function loadPolicy(root = process.cwd()) {
|
|
21061
|
-
const file =
|
|
21206
|
+
const file = path23.join(root, POLICY_PATH);
|
|
21062
21207
|
try {
|
|
21063
|
-
const raw = await
|
|
21208
|
+
const raw = await fs18.readFile(file, "utf8");
|
|
21064
21209
|
const data = JSON.parse(raw);
|
|
21065
21210
|
return data;
|
|
21066
21211
|
} catch {
|
|
@@ -21170,7 +21315,7 @@ var handler21 = toolPolicyServer;
|
|
|
21170
21315
|
// plugins/update-checker.ts
|
|
21171
21316
|
import { existsSync as existsSync5 } from "node:fs";
|
|
21172
21317
|
import { homedir as homedir8 } from "node:os";
|
|
21173
|
-
import { join as
|
|
21318
|
+
import { join as join21 } from "node:path";
|
|
21174
21319
|
|
|
21175
21320
|
// lib/update-checker-impl.ts
|
|
21176
21321
|
import { createHash as createHash5 } from "node:crypto";
|
|
@@ -21187,7 +21332,7 @@ import {
|
|
|
21187
21332
|
writeFileSync as writeFileSync2
|
|
21188
21333
|
} from "node:fs";
|
|
21189
21334
|
import { homedir as homedir7, tmpdir } from "node:os";
|
|
21190
|
-
import { dirname as
|
|
21335
|
+
import { dirname as dirname12, join as join20 } from "node:path";
|
|
21191
21336
|
import { fileURLToPath } from "node:url";
|
|
21192
21337
|
import * as https from "node:https";
|
|
21193
21338
|
import * as zlib from "node:zlib";
|
|
@@ -21195,7 +21340,7 @@ import * as zlib from "node:zlib";
|
|
|
21195
21340
|
// lib/version-injected.ts
|
|
21196
21341
|
function getInjectedVersion() {
|
|
21197
21342
|
try {
|
|
21198
|
-
const v = "0.5.
|
|
21343
|
+
const v = "0.5.5";
|
|
21199
21344
|
if (typeof v === "string" && /^\d+\.\d+\.\d+/.test(v)) {
|
|
21200
21345
|
return v;
|
|
21201
21346
|
}
|
|
@@ -21284,18 +21429,18 @@ function readLocalVersion() {
|
|
|
21284
21429
|
return injected;
|
|
21285
21430
|
try {
|
|
21286
21431
|
const here = fileURLToPath(import.meta.url);
|
|
21287
|
-
const root =
|
|
21288
|
-
const pkg = JSON.parse(readFileSync5(
|
|
21432
|
+
const root = dirname12(dirname12(here));
|
|
21433
|
+
const pkg = JSON.parse(readFileSync5(join20(root, "package.json"), "utf8"));
|
|
21289
21434
|
return typeof pkg.version === "string" ? pkg.version : "0.0.0";
|
|
21290
21435
|
} catch {
|
|
21291
21436
|
return "0.0.0";
|
|
21292
21437
|
}
|
|
21293
21438
|
}
|
|
21294
21439
|
function defaultCacheDir() {
|
|
21295
|
-
return process.env["CODEFORGE_CACHE_DIR"] ??
|
|
21440
|
+
return process.env["CODEFORGE_CACHE_DIR"] ?? join20(homedir7(), ".cache", "codeforge");
|
|
21296
21441
|
}
|
|
21297
21442
|
function defaultCacheFile() {
|
|
21298
|
-
return
|
|
21443
|
+
return join20(defaultCacheDir(), "update-check.json");
|
|
21299
21444
|
}
|
|
21300
21445
|
function readCache(file) {
|
|
21301
21446
|
try {
|
|
@@ -21313,7 +21458,7 @@ function readCache(file) {
|
|
|
21313
21458
|
}
|
|
21314
21459
|
function writeCache(file, entry) {
|
|
21315
21460
|
try {
|
|
21316
|
-
mkdirSync3(
|
|
21461
|
+
mkdirSync3(dirname12(file), { recursive: true });
|
|
21317
21462
|
writeFileSync2(file, JSON.stringify(entry, null, 2), "utf8");
|
|
21318
21463
|
} catch {}
|
|
21319
21464
|
}
|
|
@@ -21451,14 +21596,14 @@ function defaultHttpFetcher(url, timeoutMs) {
|
|
|
21451
21596
|
});
|
|
21452
21597
|
}
|
|
21453
21598
|
async function downloadAndExtractBundle(opts) {
|
|
21454
|
-
const tmpRoot = opts.tmpDir ?? mkdtempSync(
|
|
21599
|
+
const tmpRoot = opts.tmpDir ?? mkdtempSync(join20(tmpdir(), "codeforge-update-"));
|
|
21455
21600
|
mkdirSync3(tmpRoot, { recursive: true });
|
|
21456
21601
|
const fetcher = opts.tarballFetcher ?? defaultBinaryFetcher;
|
|
21457
21602
|
const tarballBuf = await fetcher(opts.tarballUrl);
|
|
21458
21603
|
verifyIntegrity(tarballBuf, opts.expectedIntegrity);
|
|
21459
21604
|
const tarBuf = zlib.gunzipSync(tarballBuf);
|
|
21460
21605
|
extractTarToDir(tarBuf, tmpRoot);
|
|
21461
|
-
const bundlePath =
|
|
21606
|
+
const bundlePath = join20(tmpRoot, "package", "dist", "index.js");
|
|
21462
21607
|
if (!existsSync4(bundlePath)) {
|
|
21463
21608
|
throw new Error(`bundle_not_found: ${bundlePath}`);
|
|
21464
21609
|
}
|
|
@@ -21498,11 +21643,11 @@ function extractTarToDir(tarBuf, destRoot) {
|
|
|
21498
21643
|
offset += 512;
|
|
21499
21644
|
if (typeFlag === "0" || typeFlag === "" || typeFlag === "\x00") {
|
|
21500
21645
|
const fileBuf = tarBuf.subarray(offset, offset + size);
|
|
21501
|
-
const dest =
|
|
21502
|
-
mkdirSync3(
|
|
21646
|
+
const dest = join20(destRoot, fullName);
|
|
21647
|
+
mkdirSync3(dirname12(dest), { recursive: true });
|
|
21503
21648
|
writeFileSync2(dest, fileBuf);
|
|
21504
21649
|
} else if (typeFlag === "5") {
|
|
21505
|
-
mkdirSync3(
|
|
21650
|
+
mkdirSync3(join20(destRoot, fullName), { recursive: true });
|
|
21506
21651
|
}
|
|
21507
21652
|
offset += Math.ceil(size / 512) * 512;
|
|
21508
21653
|
}
|
|
@@ -21555,7 +21700,7 @@ function atomicReplaceBundle(opts) {
|
|
|
21555
21700
|
if (!existsSync4(source)) {
|
|
21556
21701
|
throw new Error(`atomic_source_missing: ${source}`);
|
|
21557
21702
|
}
|
|
21558
|
-
mkdirSync3(
|
|
21703
|
+
mkdirSync3(dirname12(target), { recursive: true });
|
|
21559
21704
|
const newPath = `${target}.new`;
|
|
21560
21705
|
const backupPath = `${target}.bak.${oldVersion}`;
|
|
21561
21706
|
let strategy = "rename";
|
|
@@ -21607,11 +21752,11 @@ function cleanupOldBackups(target, keep) {
|
|
|
21607
21752
|
if (keep <= 0)
|
|
21608
21753
|
return;
|
|
21609
21754
|
try {
|
|
21610
|
-
const dir =
|
|
21755
|
+
const dir = dirname12(target);
|
|
21611
21756
|
const base = target.substring(dir.length + 1);
|
|
21612
21757
|
const prefix = `${base}.bak.`;
|
|
21613
21758
|
const all = readdirSync3(dir).filter((f) => f.startsWith(prefix)).map((f) => {
|
|
21614
|
-
const full =
|
|
21759
|
+
const full = join20(dir, f);
|
|
21615
21760
|
let mtimeMs = 0;
|
|
21616
21761
|
try {
|
|
21617
21762
|
mtimeMs = statSync5(full).mtimeMs;
|
|
@@ -21633,7 +21778,7 @@ function loadCompatibility(opts) {
|
|
|
21633
21778
|
const root = opts?.cwd ?? inferPluginRoot();
|
|
21634
21779
|
if (!root)
|
|
21635
21780
|
return null;
|
|
21636
|
-
file =
|
|
21781
|
+
file = join20(root, "compatibility.json");
|
|
21637
21782
|
}
|
|
21638
21783
|
if (!existsSync4(file))
|
|
21639
21784
|
return null;
|
|
@@ -21658,7 +21803,7 @@ function loadCompatibility(opts) {
|
|
|
21658
21803
|
function inferPluginRoot() {
|
|
21659
21804
|
try {
|
|
21660
21805
|
const here = fileURLToPath(import.meta.url);
|
|
21661
|
-
return
|
|
21806
|
+
return dirname12(dirname12(here));
|
|
21662
21807
|
} catch {
|
|
21663
21808
|
return null;
|
|
21664
21809
|
}
|
|
@@ -21853,14 +21998,14 @@ function detectOpencodeVersion() {
|
|
|
21853
21998
|
}
|
|
21854
21999
|
function getOpencodeBundlePath() {
|
|
21855
22000
|
const candidates = [];
|
|
21856
|
-
candidates.push(
|
|
22001
|
+
candidates.push(join21(homedir8(), ".config", "opencode", "codeforge", "index.js"));
|
|
21857
22002
|
if (process.platform === "win32") {
|
|
21858
22003
|
const appData = process.env["APPDATA"];
|
|
21859
22004
|
if (appData)
|
|
21860
|
-
candidates.push(
|
|
22005
|
+
candidates.push(join21(appData, "opencode", "codeforge", "index.js"));
|
|
21861
22006
|
const localAppData = process.env["LOCALAPPDATA"];
|
|
21862
22007
|
if (localAppData)
|
|
21863
|
-
candidates.push(
|
|
22008
|
+
candidates.push(join21(localAppData, "opencode", "codeforge", "index.js"));
|
|
21864
22009
|
}
|
|
21865
22010
|
for (const c of candidates) {
|
|
21866
22011
|
if (existsSync5(c))
|
|
@@ -21921,11 +22066,11 @@ async function postToast(ctx, message) {
|
|
|
21921
22066
|
var handler22 = updateCheckerServer;
|
|
21922
22067
|
|
|
21923
22068
|
// plugins/workflow-engine.ts
|
|
21924
|
-
import * as
|
|
22069
|
+
import * as path25 from "node:path";
|
|
21925
22070
|
|
|
21926
22071
|
// lib/workflow-loader.ts
|
|
21927
|
-
import { promises as
|
|
21928
|
-
import * as
|
|
22072
|
+
import { promises as fs19 } from "node:fs";
|
|
22073
|
+
import * as path24 from "node:path";
|
|
21929
22074
|
import { z as z32 } from "zod";
|
|
21930
22075
|
var ActionSchema = z32.object({
|
|
21931
22076
|
tool: z32.string().min(1, "action.tool 不能为空"),
|
|
@@ -22011,7 +22156,7 @@ function parseWorkflowYaml(yaml, sourcePath = "<inline>") {
|
|
|
22011
22156
|
async function loadWorkflowFromFile(filePath) {
|
|
22012
22157
|
let txt;
|
|
22013
22158
|
try {
|
|
22014
|
-
txt = await
|
|
22159
|
+
txt = await fs19.readFile(filePath, "utf8");
|
|
22015
22160
|
} catch (err) {
|
|
22016
22161
|
return {
|
|
22017
22162
|
ok: false,
|
|
@@ -22026,7 +22171,7 @@ async function loadWorkflowsFromDir(dir) {
|
|
|
22026
22171
|
const failed = [];
|
|
22027
22172
|
let entries;
|
|
22028
22173
|
try {
|
|
22029
|
-
entries = await
|
|
22174
|
+
entries = await fs19.readdir(dir);
|
|
22030
22175
|
} catch (err) {
|
|
22031
22176
|
const e = err;
|
|
22032
22177
|
if (e.code === "ENOENT")
|
|
@@ -22038,7 +22183,7 @@ async function loadWorkflowsFromDir(dir) {
|
|
|
22038
22183
|
continue;
|
|
22039
22184
|
if (!/\.ya?ml$/i.test(name))
|
|
22040
22185
|
continue;
|
|
22041
|
-
const full =
|
|
22186
|
+
const full = path24.join(dir, name);
|
|
22042
22187
|
const r = await loadWorkflowFromFile(full);
|
|
22043
22188
|
if (r.ok)
|
|
22044
22189
|
loaded.push(r);
|
|
@@ -22428,7 +22573,7 @@ async function handleCommandInvoked(raw, workflowsDir = "workflows") {
|
|
|
22428
22573
|
}
|
|
22429
22574
|
var workflowEngineServer = async (ctx) => {
|
|
22430
22575
|
const directory = ctx.directory ?? process.cwd();
|
|
22431
|
-
const workflowsDir =
|
|
22576
|
+
const workflowsDir = path25.join(directory, "workflows");
|
|
22432
22577
|
ensureRegistry(workflowsDir).catch((err) => fallbackLog2.warn(`[${PLUGIN_NAME23}] preload workflows failed`, {
|
|
22433
22578
|
error: err instanceof Error ? err.message : String(err)
|
|
22434
22579
|
}));
|
|
@@ -22472,7 +22617,7 @@ var workflowEngineServer = async (ctx) => {
|
|
|
22472
22617
|
var handler23 = workflowEngineServer;
|
|
22473
22618
|
|
|
22474
22619
|
// plugins/session-worktree-guard.ts
|
|
22475
|
-
import
|
|
22620
|
+
import path26 from "node:path";
|
|
22476
22621
|
var PLUGIN_NAME24 = "session-worktree-guard";
|
|
22477
22622
|
logLifecycle(PLUGIN_NAME24, "import", {});
|
|
22478
22623
|
var WRITE_INTENT_RE = />(?!=)|\btee\b|\brm\b|\bmv\b|\bcp\b|\bmkdir\b|\btouch\b|\bchmod\b|\bchown\b|\bln\b/;
|
|
@@ -22501,7 +22646,7 @@ var WRITE_TOOLS = new Set(["write", "edit", "ast_edit"]);
|
|
|
22501
22646
|
function rewritePath(value, mainRoot, worktreeRoot) {
|
|
22502
22647
|
if (!value)
|
|
22503
22648
|
return null;
|
|
22504
|
-
const resolved =
|
|
22649
|
+
const resolved = path26.isAbsolute(value) ? value : path26.resolve(mainRoot, value);
|
|
22505
22650
|
if (resolved === mainRoot)
|
|
22506
22651
|
return worktreeRoot;
|
|
22507
22652
|
const prefix = mainRoot.endsWith("/") ? mainRoot : mainRoot + "/";
|
|
@@ -22631,6 +22776,32 @@ var sessionWorktreeGuardPlugin = async (ctx) => {
|
|
|
22631
22776
|
}
|
|
22632
22777
|
}
|
|
22633
22778
|
const worktreePath = entry.worktreePath;
|
|
22779
|
+
if (toolName === "session_merge") {
|
|
22780
|
+
const action = argsObj["action"];
|
|
22781
|
+
if (action === "merge") {
|
|
22782
|
+
const caller = input.agent;
|
|
22783
|
+
if (caller !== undefined && caller !== "codeforge") {
|
|
22784
|
+
const reason = `[session-worktree-guard] DENIED: session_merge action=merge 仅 codeforge orchestrator 或用户可调;当前 caller=${caller}`;
|
|
22785
|
+
log13.warn(reason, {
|
|
22786
|
+
sessionId,
|
|
22787
|
+
tool: toolName,
|
|
22788
|
+
action,
|
|
22789
|
+
caller
|
|
22790
|
+
});
|
|
22791
|
+
safeWriteLog(PLUGIN_NAME24, {
|
|
22792
|
+
hook: "tool.execute.before",
|
|
22793
|
+
tool: toolName,
|
|
22794
|
+
sessionID: input.sessionID,
|
|
22795
|
+
action: "deny",
|
|
22796
|
+
source: "merge-caller-whitelist",
|
|
22797
|
+
caller,
|
|
22798
|
+
merge_action: "merge"
|
|
22799
|
+
});
|
|
22800
|
+
denied = new DeniedError(reason);
|
|
22801
|
+
return;
|
|
22802
|
+
}
|
|
22803
|
+
}
|
|
22804
|
+
}
|
|
22634
22805
|
if (toolName !== "plan_read" && entry.requiredPlanId && entry.planReadOk !== true) {
|
|
22635
22806
|
let isWriteOp = WRITE_TOOLS.has(toolName);
|
|
22636
22807
|
if (!isWriteOp && toolName === "bash") {
|
|
@@ -22818,9 +22989,8 @@ var worktreeLifecyclePlugin = async (ctx) => {
|
|
|
22818
22989
|
return;
|
|
22819
22990
|
}
|
|
22820
22991
|
try {
|
|
22821
|
-
const
|
|
22822
|
-
|
|
22823
|
-
if (worktreeHead && worktreeHead === mainHead) {
|
|
22992
|
+
const worktreeHead = await getCurrentWorktreeHead(entry.worktreePath);
|
|
22993
|
+
if (worktreeHead && worktreeHead === entry.baseSha) {
|
|
22824
22994
|
const fastDirty = await isWorktreeDirty(entry.worktreePath);
|
|
22825
22995
|
if (!fastDirty) {
|
|
22826
22996
|
await discardSession({ sessionId: ended.sessionID, mainRoot });
|