@andyqiu/codeforge 0.5.0 → 0.5.2
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 +3 -2
- package/agents/discover-challenger.md +2 -2
- package/agents/discover.md +7 -6
- package/bin/codeforge.mjs +49 -0
- package/commands/changes.md +62 -0
- package/commands/debug.md +4 -4
- package/commands/deep.md +11 -11
- package/commands/model-switch.md +4 -4
- package/commands/quick.md +11 -11
- package/commands/refactor.md +2 -2
- package/commands/review.md +2 -2
- package/commands/ship.md +6 -6
- package/commands/tdd.md +3 -3
- package/dist/index.js +794 -467
- package/install.ps1 +556 -556
- package/install.sh +0 -0
- package/package.json +1 -1
- package/workflows/bugfix.yaml +19 -22
- package/workflows/code-review.yaml +7 -6
- package/workflows/feature-dev.yaml +20 -25
- package/workflows/refactor.yaml +9 -12
- package/workflows/tdd.yaml +9 -12
package/dist/index.js
CHANGED
|
@@ -1112,6 +1112,60 @@ var init_autonomy = __esm(() => {
|
|
|
1112
1112
|
};
|
|
1113
1113
|
});
|
|
1114
1114
|
|
|
1115
|
+
// lib/decision-parser.ts
|
|
1116
|
+
var exports_decision_parser = {};
|
|
1117
|
+
__export(exports_decision_parser, {
|
|
1118
|
+
parseDecision: () => parseDecision
|
|
1119
|
+
});
|
|
1120
|
+
function parseDecision(markdown) {
|
|
1121
|
+
if (!markdown || typeof markdown !== "string") {
|
|
1122
|
+
return { token: null, reason: "empty input" };
|
|
1123
|
+
}
|
|
1124
|
+
const lines = markdown.split(/\r?\n/);
|
|
1125
|
+
let inDecision = false;
|
|
1126
|
+
let firstLine = "";
|
|
1127
|
+
for (const rawLine of lines) {
|
|
1128
|
+
const line = rawLine.trim();
|
|
1129
|
+
if (/^##\s+Decision\s*$/.test(line)) {
|
|
1130
|
+
inDecision = true;
|
|
1131
|
+
continue;
|
|
1132
|
+
}
|
|
1133
|
+
if (!inDecision)
|
|
1134
|
+
continue;
|
|
1135
|
+
if (/^##\s/.test(line))
|
|
1136
|
+
break;
|
|
1137
|
+
if (line === "")
|
|
1138
|
+
continue;
|
|
1139
|
+
if (line.startsWith(">"))
|
|
1140
|
+
continue;
|
|
1141
|
+
if (line.startsWith("<!--"))
|
|
1142
|
+
continue;
|
|
1143
|
+
firstLine = line;
|
|
1144
|
+
break;
|
|
1145
|
+
}
|
|
1146
|
+
if (!inDecision) {
|
|
1147
|
+
return { token: null, reason: "Decision section not found" };
|
|
1148
|
+
}
|
|
1149
|
+
if (!firstLine) {
|
|
1150
|
+
return { token: null, reason: "Decision section is empty" };
|
|
1151
|
+
}
|
|
1152
|
+
const cleaned = firstLine.replace(/`/g, "").trim().toUpperCase();
|
|
1153
|
+
if (VALID_TOKENS.has(cleaned)) {
|
|
1154
|
+
return {
|
|
1155
|
+
token: cleaned,
|
|
1156
|
+
reason: firstLine.length > 200 ? firstLine.slice(0, 200) + "…" : firstLine
|
|
1157
|
+
};
|
|
1158
|
+
}
|
|
1159
|
+
return {
|
|
1160
|
+
token: null,
|
|
1161
|
+
reason: `invalid token: "${firstLine.slice(0, 80)}"`
|
|
1162
|
+
};
|
|
1163
|
+
}
|
|
1164
|
+
var VALID_TOKENS;
|
|
1165
|
+
var init_decision_parser = __esm(() => {
|
|
1166
|
+
VALID_TOKENS = new Set(["APPROVE", "REQUEST_CHANGES", "BLOCK"]);
|
|
1167
|
+
});
|
|
1168
|
+
|
|
1115
1169
|
// node_modules/yaml/dist/nodes/identity.js
|
|
1116
1170
|
var require_identity = __commonJS((exports) => {
|
|
1117
1171
|
var ALIAS = Symbol.for("yaml.alias");
|
|
@@ -8033,60 +8087,6 @@ var require_public_api = __commonJS((exports) => {
|
|
|
8033
8087
|
exports.stringify = stringify;
|
|
8034
8088
|
});
|
|
8035
8089
|
|
|
8036
|
-
// lib/decision-parser.ts
|
|
8037
|
-
var exports_decision_parser = {};
|
|
8038
|
-
__export(exports_decision_parser, {
|
|
8039
|
-
parseDecision: () => parseDecision
|
|
8040
|
-
});
|
|
8041
|
-
function parseDecision(markdown) {
|
|
8042
|
-
if (!markdown || typeof markdown !== "string") {
|
|
8043
|
-
return { token: null, reason: "empty input" };
|
|
8044
|
-
}
|
|
8045
|
-
const lines = markdown.split(/\r?\n/);
|
|
8046
|
-
let inDecision = false;
|
|
8047
|
-
let firstLine = "";
|
|
8048
|
-
for (const rawLine of lines) {
|
|
8049
|
-
const line = rawLine.trim();
|
|
8050
|
-
if (/^##\s+Decision\s*$/.test(line)) {
|
|
8051
|
-
inDecision = true;
|
|
8052
|
-
continue;
|
|
8053
|
-
}
|
|
8054
|
-
if (!inDecision)
|
|
8055
|
-
continue;
|
|
8056
|
-
if (/^##\s/.test(line))
|
|
8057
|
-
break;
|
|
8058
|
-
if (line === "")
|
|
8059
|
-
continue;
|
|
8060
|
-
if (line.startsWith(">"))
|
|
8061
|
-
continue;
|
|
8062
|
-
if (line.startsWith("<!--"))
|
|
8063
|
-
continue;
|
|
8064
|
-
firstLine = line;
|
|
8065
|
-
break;
|
|
8066
|
-
}
|
|
8067
|
-
if (!inDecision) {
|
|
8068
|
-
return { token: null, reason: "Decision section not found" };
|
|
8069
|
-
}
|
|
8070
|
-
if (!firstLine) {
|
|
8071
|
-
return { token: null, reason: "Decision section is empty" };
|
|
8072
|
-
}
|
|
8073
|
-
const cleaned = firstLine.replace(/`/g, "").trim().toUpperCase();
|
|
8074
|
-
if (VALID_TOKENS.has(cleaned)) {
|
|
8075
|
-
return {
|
|
8076
|
-
token: cleaned,
|
|
8077
|
-
reason: firstLine.length > 200 ? firstLine.slice(0, 200) + "…" : firstLine
|
|
8078
|
-
};
|
|
8079
|
-
}
|
|
8080
|
-
return {
|
|
8081
|
-
token: null,
|
|
8082
|
-
reason: `invalid token: "${firstLine.slice(0, 80)}"`
|
|
8083
|
-
};
|
|
8084
|
-
}
|
|
8085
|
-
var VALID_TOKENS;
|
|
8086
|
-
var init_decision_parser = __esm(() => {
|
|
8087
|
-
VALID_TOKENS = new Set(["APPROVE", "REQUEST_CHANGES", "BLOCK"]);
|
|
8088
|
-
});
|
|
8089
|
-
|
|
8090
8090
|
// lib/auto-debug.ts
|
|
8091
8091
|
async function runWithAutoDebug(opts) {
|
|
8092
8092
|
const cfg = { ...DEFAULT_AUTO_DEBUG, ...opts.config ?? {} };
|
|
@@ -13571,7 +13571,8 @@ async function runMergeLoop(opts) {
|
|
|
13571
13571
|
worktreePath: entry.worktreePath,
|
|
13572
13572
|
round: loops,
|
|
13573
13573
|
maxRounds: config.maxReviewLoops,
|
|
13574
|
-
...lastReviewSummary ? { prevSummary: lastReviewSummary } : {}
|
|
13574
|
+
...lastReviewSummary ? { prevSummary: lastReviewSummary } : {},
|
|
13575
|
+
...opts.signal ? { signal: opts.signal } : {}
|
|
13575
13576
|
}), config.reviewTimeoutMs, `reviewer 第 ${loops} 轮`, opts.signal);
|
|
13576
13577
|
} catch (err) {
|
|
13577
13578
|
const e = err;
|
|
@@ -13639,7 +13640,8 @@ async function runMergeLoop(opts) {
|
|
|
13639
13640
|
const coderResult = await withTimeout2(opts.spawner.spawnCoder({
|
|
13640
13641
|
sessionId: opts.sessionId,
|
|
13641
13642
|
...opts.planId ? { planId: opts.planId } : {},
|
|
13642
|
-
reviewerSummary: reviewResult.summary
|
|
13643
|
+
reviewerSummary: reviewResult.summary,
|
|
13644
|
+
...opts.signal ? { signal: opts.signal } : {}
|
|
13643
13645
|
}), config.coderTimeoutMs, `coder round ${loops}`, opts.signal);
|
|
13644
13646
|
progress("wait_coder", `coder 完成: ${coderResult.ok ? "ok" : "fail"} - ${coderResult.summary}`);
|
|
13645
13647
|
if (!coderResult.ok) {
|
|
@@ -13763,6 +13765,7 @@ var description26 = [
|
|
|
13763
13765
|
"- action=status:查询当前 session worktree 状态(任意 agent 可调)",
|
|
13764
13766
|
"- action=merge:codeforge orchestrator 在用户触发 /merge 后调(**subagent 禁止**)",
|
|
13765
13767
|
"- action=discard:用户决定放弃当前 session 改动时调",
|
|
13768
|
+
"- action=diff:查看当前 session worktree 相对主仓(baseSha)的改动;stat=true 只看文件列表",
|
|
13766
13769
|
"**注意**:",
|
|
13767
13770
|
"- merge 走 review-fix-review 闭环(默认 3 轮);force=true 跳过 review 直接 squash",
|
|
13768
13771
|
"- subagent (coder/reviewer/planner) 调 action=merge 会被 Phase 2 guard plugin 拒绝",
|
|
@@ -13784,21 +13787,44 @@ var ArgsSchema26 = z27.discriminatedUnion("action", [
|
|
|
13784
13787
|
z27.object({
|
|
13785
13788
|
action: z27.literal("discard"),
|
|
13786
13789
|
session_id: z27.string().optional().describe("默认当前 session")
|
|
13790
|
+
}),
|
|
13791
|
+
z27.object({
|
|
13792
|
+
action: z27.literal("diff"),
|
|
13793
|
+
session_id: z27.string().optional().describe("默认当前 session"),
|
|
13794
|
+
stat: z27.boolean().optional().describe("true=只显示文件列表+统计,false=完整 diff(默认 false)")
|
|
13787
13795
|
})
|
|
13788
13796
|
]);
|
|
13789
13797
|
var _ctx = {};
|
|
13798
|
+
function __setContext(ctx) {
|
|
13799
|
+
_ctx = ctx;
|
|
13800
|
+
}
|
|
13790
13801
|
function getMainRoot() {
|
|
13791
13802
|
return _ctx.mainRoot ?? process.cwd();
|
|
13792
13803
|
}
|
|
13793
|
-
async function resolveSessionId(explicit) {
|
|
13804
|
+
async function resolveSessionId(explicit, strictWorktreeCheck = false) {
|
|
13794
13805
|
if (explicit)
|
|
13795
13806
|
return explicit;
|
|
13807
|
+
const mainRoot = getMainRoot();
|
|
13808
|
+
if (_ctx.currentSessionId) {
|
|
13809
|
+
if (!strictWorktreeCheck || await isSessionIdValid(_ctx.currentSessionId, mainRoot)) {
|
|
13810
|
+
return _ctx.currentSessionId;
|
|
13811
|
+
}
|
|
13812
|
+
}
|
|
13796
13813
|
if (_ctx.resolveCurrentSessionId) {
|
|
13797
13814
|
const sid = await _ctx.resolveCurrentSessionId();
|
|
13798
|
-
if (sid)
|
|
13815
|
+
if (sid && (!strictWorktreeCheck || await isSessionIdValid(sid, mainRoot))) {
|
|
13799
13816
|
return sid;
|
|
13817
|
+
}
|
|
13818
|
+
}
|
|
13819
|
+
throw new Error(strictWorktreeCheck ? "session_merge: 未指定 session_id 且无法反推一个有 active worktree 的 session" + "(plugin 注入的 currentSessionId 和 fallback resolver 都未命中或对应 worktree 非 active)" : "session_merge: 未指定 session_id 且无法反推当前 session(context resolver 缺失)");
|
|
13820
|
+
}
|
|
13821
|
+
async function isSessionIdValid(sid, mainRoot) {
|
|
13822
|
+
try {
|
|
13823
|
+
const entry = await getSessionWorktree(sid, mainRoot);
|
|
13824
|
+
return entry !== null && entry.status === "active";
|
|
13825
|
+
} catch {
|
|
13826
|
+
return false;
|
|
13800
13827
|
}
|
|
13801
|
-
throw new Error("session_merge: 未指定 session_id 且无法反推当前 session(context resolver 缺失)");
|
|
13802
13828
|
}
|
|
13803
13829
|
async function execute26(input) {
|
|
13804
13830
|
const parsed = ArgsSchema26.safeParse(input);
|
|
@@ -13833,19 +13859,84 @@ async function execute26(input) {
|
|
|
13833
13859
|
data: { discarded: true, sessionId }
|
|
13834
13860
|
};
|
|
13835
13861
|
}
|
|
13862
|
+
if (args.action === "diff") {
|
|
13863
|
+
const entry = await getSessionWorktree(sessionId, mainRoot);
|
|
13864
|
+
if (!entry) {
|
|
13865
|
+
return { ok: false, action: "diff", error: `session ${sessionId} 没有绑定 worktree` };
|
|
13866
|
+
}
|
|
13867
|
+
const { execFile: execFile4 } = await import("node:child_process");
|
|
13868
|
+
const { promisify: promisify2 } = await import("node:util");
|
|
13869
|
+
const execFileAsync = promisify2(execFile4);
|
|
13870
|
+
const diffArgs = args.stat ? ["diff", "--stat", entry.baseSha] : ["diff", entry.baseSha];
|
|
13871
|
+
let diffOutput = "";
|
|
13872
|
+
let changedFiles = [];
|
|
13873
|
+
try {
|
|
13874
|
+
const { stdout } = await execFileAsync("git", diffArgs, {
|
|
13875
|
+
cwd: entry.worktreePath,
|
|
13876
|
+
maxBuffer: 5242880
|
|
13877
|
+
});
|
|
13878
|
+
diffOutput = stdout;
|
|
13879
|
+
const { stdout: nameOnly } = await execFileAsync("git", ["diff", "--name-only", entry.baseSha], { cwd: entry.worktreePath, maxBuffer: 1048576 });
|
|
13880
|
+
changedFiles = nameOnly.trim().split(`
|
|
13881
|
+
`).filter(Boolean);
|
|
13882
|
+
} catch (e) {
|
|
13883
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
13884
|
+
if (!msg.includes("unknown revision") && !msg.includes("bad object")) {
|
|
13885
|
+
return { ok: false, action: "diff", error: `git diff 失败: ${msg}` };
|
|
13886
|
+
}
|
|
13887
|
+
try {
|
|
13888
|
+
const fallbackArgs = args.stat ? ["diff", "--stat", "HEAD"] : ["diff", "HEAD"];
|
|
13889
|
+
const { stdout } = await execFileAsync("git", fallbackArgs, {
|
|
13890
|
+
cwd: entry.worktreePath,
|
|
13891
|
+
maxBuffer: 5242880
|
|
13892
|
+
});
|
|
13893
|
+
diffOutput = stdout;
|
|
13894
|
+
const { stdout: nameOnly } = await execFileAsync("git", ["diff", "--name-only", "HEAD"], { cwd: entry.worktreePath, maxBuffer: 1048576 });
|
|
13895
|
+
changedFiles = nameOnly.trim().split(`
|
|
13896
|
+
`).filter(Boolean);
|
|
13897
|
+
} catch (e2) {
|
|
13898
|
+
return {
|
|
13899
|
+
ok: false,
|
|
13900
|
+
action: "diff",
|
|
13901
|
+
error: `git diff 失败: ${e2 instanceof Error ? e2.message : String(e2)}`
|
|
13902
|
+
};
|
|
13903
|
+
}
|
|
13904
|
+
}
|
|
13905
|
+
return {
|
|
13906
|
+
ok: true,
|
|
13907
|
+
action: "diff",
|
|
13908
|
+
data: {
|
|
13909
|
+
sessionId,
|
|
13910
|
+
worktreePath: entry.worktreePath,
|
|
13911
|
+
branch: entry.branch,
|
|
13912
|
+
baseSha: entry.baseSha,
|
|
13913
|
+
changedFiles,
|
|
13914
|
+
fileCount: changedFiles.length,
|
|
13915
|
+
diff: diffOutput
|
|
13916
|
+
}
|
|
13917
|
+
};
|
|
13918
|
+
}
|
|
13919
|
+
const mergeSessionId = await resolveSessionId(args.session_id, true);
|
|
13836
13920
|
if (!_ctx.spawner) {
|
|
13837
13921
|
return {
|
|
13838
13922
|
ok: false,
|
|
13839
13923
|
action: "merge",
|
|
13840
|
-
error: "session_merge: SubagentSpawner 未注入(
|
|
13924
|
+
error: "session_merge: SubagentSpawner 未注入(plugin activate 失败?" + "应由 plugins/codeforge-tools.ts 的 ProductionSpawner 注入 — ADR:spawner-production-wire-up)"
|
|
13841
13925
|
};
|
|
13842
13926
|
}
|
|
13927
|
+
const mergeArgs = args;
|
|
13928
|
+
const sendProgress = _ctx.sendProgress;
|
|
13843
13929
|
const result = await runMergeLoop({
|
|
13844
|
-
sessionId,
|
|
13930
|
+
sessionId: mergeSessionId,
|
|
13845
13931
|
mainRoot,
|
|
13846
|
-
...
|
|
13847
|
-
...
|
|
13848
|
-
spawner: _ctx.spawner
|
|
13932
|
+
...mergeArgs.plan_id ? { planId: mergeArgs.plan_id } : {},
|
|
13933
|
+
...mergeArgs.force ? { force: true } : {},
|
|
13934
|
+
spawner: _ctx.spawner,
|
|
13935
|
+
...sendProgress ? {
|
|
13936
|
+
onProgress: (state, detail) => {
|
|
13937
|
+
Promise.resolve(sendProgress(state, detail)).catch(() => {});
|
|
13938
|
+
}
|
|
13939
|
+
} : {}
|
|
13849
13940
|
});
|
|
13850
13941
|
return { ok: true, action: "merge", data: result };
|
|
13851
13942
|
} catch (err) {
|
|
@@ -14184,7 +14275,7 @@ var ArgsSchema28 = z29.object({
|
|
|
14184
14275
|
message: "plan_id 与 path 至少传一个"
|
|
14185
14276
|
});
|
|
14186
14277
|
var _ctx2 = {};
|
|
14187
|
-
function
|
|
14278
|
+
function __setContext2(ctx) {
|
|
14188
14279
|
_ctx2 = ctx;
|
|
14189
14280
|
}
|
|
14190
14281
|
function getStore2() {
|
|
@@ -14290,104 +14381,557 @@ async function execute28(input) {
|
|
|
14290
14381
|
};
|
|
14291
14382
|
}
|
|
14292
14383
|
}
|
|
14293
|
-
// lib/
|
|
14294
|
-
|
|
14295
|
-
|
|
14296
|
-
|
|
14297
|
-
|
|
14298
|
-
|
|
14299
|
-
|
|
14300
|
-
|
|
14301
|
-
|
|
14302
|
-
|
|
14303
|
-
|
|
14304
|
-
|
|
14305
|
-
|
|
14306
|
-
|
|
14307
|
-
|
|
14308
|
-
|
|
14309
|
-
|
|
14310
|
-
|
|
14311
|
-
|
|
14312
|
-
|
|
14313
|
-
|
|
14314
|
-
interval_hours: 24,
|
|
14315
|
-
repo: "andy-personal/code-forge",
|
|
14316
|
-
channel: "latest",
|
|
14317
|
-
package: "@andyqiu/codeforge",
|
|
14318
|
-
registry: "https://registry.npmjs.org",
|
|
14319
|
-
auto_install: true,
|
|
14320
|
-
backup_keep: 3
|
|
14321
|
-
}
|
|
14322
|
-
};
|
|
14323
|
-
function loadRuntimeSync(opts = {}) {
|
|
14324
|
-
const hit = findConfigFileSync(opts);
|
|
14325
|
-
if (!hit) {
|
|
14326
|
-
return emptyResult({ reason: `${CONFIG_FILE} not found (using built-in defaults)` });
|
|
14327
|
-
}
|
|
14328
|
-
const fsSync = __require("node:fs");
|
|
14329
|
-
let raw;
|
|
14330
|
-
try {
|
|
14331
|
-
raw = fsSync.readFileSync(hit, "utf8");
|
|
14332
|
-
} catch (e) {
|
|
14333
|
-
return emptyResult({ path: hit, reason: `read_failed: ${e.message}` });
|
|
14334
|
-
}
|
|
14335
|
-
return parseRuntime(raw, hit);
|
|
14336
|
-
}
|
|
14337
|
-
async function loadRuntime(opts = {}) {
|
|
14338
|
-
const root = opts.root ?? process.cwd();
|
|
14339
|
-
const abs = path16.resolve(root, opts.file ?? CONFIG_FILE);
|
|
14340
|
-
let raw;
|
|
14341
|
-
try {
|
|
14342
|
-
raw = await fs13.readFile(abs, "utf8");
|
|
14343
|
-
} catch (e) {
|
|
14344
|
-
const code = e.code;
|
|
14345
|
-
if (code === "ENOENT") {
|
|
14346
|
-
return emptyResult({ reason: `${CONFIG_FILE} not found at ${abs} (using built-in defaults)` });
|
|
14347
|
-
}
|
|
14348
|
-
return emptyResult({ path: abs, reason: `read_failed: ${e.message}` });
|
|
14349
|
-
}
|
|
14350
|
-
return parseRuntime(raw, abs);
|
|
14351
|
-
}
|
|
14352
|
-
function emptyResult(opts) {
|
|
14353
|
-
return {
|
|
14354
|
-
ok: false,
|
|
14355
|
-
path: opts.path ?? null,
|
|
14356
|
-
runtime: cloneDefaults(),
|
|
14357
|
-
warnings: [],
|
|
14358
|
-
error: opts.reason
|
|
14359
|
-
};
|
|
14360
|
-
}
|
|
14361
|
-
function cloneDefaults() {
|
|
14362
|
-
return JSON.parse(JSON.stringify(DEFAULT_RUNTIME));
|
|
14363
|
-
}
|
|
14364
|
-
var VALID_CHANNELS = ["stable", "prerelease", "latest", "next", "rc"];
|
|
14365
|
-
function parseRuntime(raw, abs) {
|
|
14366
|
-
let root = null;
|
|
14367
|
-
try {
|
|
14368
|
-
const parsed = JSON.parse(raw);
|
|
14369
|
-
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
14370
|
-
root = parsed;
|
|
14371
|
-
}
|
|
14372
|
-
} catch (e) {
|
|
14373
|
-
return emptyResult({ path: abs, reason: `invalid_json: ${e.message}` });
|
|
14374
|
-
}
|
|
14375
|
-
if (!root) {
|
|
14376
|
-
return emptyResult({ path: abs, reason: "config_root_must_be_object" });
|
|
14377
|
-
}
|
|
14378
|
-
const warnings = [];
|
|
14379
|
-
const cfg = cloneDefaults();
|
|
14380
|
-
if (root.autonomy && typeof root.autonomy === "object" && !Array.isArray(root.autonomy)) {
|
|
14381
|
-
const a = root.autonomy;
|
|
14382
|
-
if (a.default_mode !== undefined) {
|
|
14383
|
-
if (a.default_mode === "step" || a.default_mode === "semi" || a.default_mode === "full") {
|
|
14384
|
-
cfg.autonomy.default_mode = a.default_mode;
|
|
14385
|
-
} else {
|
|
14386
|
-
warnings.push(`autonomy.default_mode invalid: ${String(a.default_mode)} (keep ${cfg.autonomy.default_mode})`);
|
|
14384
|
+
// lib/opencode-runner.ts
|
|
14385
|
+
function makeOpencodeRunner(opts) {
|
|
14386
|
+
const log4 = opts.log ?? (() => {});
|
|
14387
|
+
return async (spec, runCtx) => {
|
|
14388
|
+
const startedAt = Date.now();
|
|
14389
|
+
let childSessionId;
|
|
14390
|
+
try {
|
|
14391
|
+
const created = await opts.client.session.create({
|
|
14392
|
+
body: {
|
|
14393
|
+
parentID: opts.parentSessionID,
|
|
14394
|
+
title: clip3(`subtask:${spec.id}`, 80)
|
|
14395
|
+
},
|
|
14396
|
+
query: opts.directory ? { directory: opts.directory } : undefined
|
|
14397
|
+
});
|
|
14398
|
+
if (created.error || !created.data?.id) {
|
|
14399
|
+
return {
|
|
14400
|
+
ok: false,
|
|
14401
|
+
summary: `session.create 失败:${describe4(created.error) || "no id"}`,
|
|
14402
|
+
status: "failed",
|
|
14403
|
+
error: describe4(created.error) || "session.create 无 id"
|
|
14404
|
+
};
|
|
14387
14405
|
}
|
|
14388
|
-
|
|
14389
|
-
|
|
14390
|
-
|
|
14406
|
+
childSessionId = created.data.id;
|
|
14407
|
+
log4("info", `[opencode-runner] subtask=${spec.id} session=${childSessionId} created`);
|
|
14408
|
+
const promptText = composePromptText(spec);
|
|
14409
|
+
const promptPromise = opts.client.session.prompt({
|
|
14410
|
+
path: { id: childSessionId },
|
|
14411
|
+
body: {
|
|
14412
|
+
agent: spec.agent ?? opts.defaultAgent,
|
|
14413
|
+
parts: [{ type: "text", text: promptText }]
|
|
14414
|
+
},
|
|
14415
|
+
query: opts.directory ? { directory: opts.directory } : undefined
|
|
14416
|
+
});
|
|
14417
|
+
const promptRes = await withTimeout3(Promise.resolve(promptPromise), opts.perTaskTimeoutMs, runCtx.signal);
|
|
14418
|
+
if (promptRes.kind === "timeout") {
|
|
14419
|
+
return {
|
|
14420
|
+
ok: false,
|
|
14421
|
+
summary: `subtask 超时(${opts.perTaskTimeoutMs ?? 0}ms)`,
|
|
14422
|
+
status: "timeout",
|
|
14423
|
+
error: "perTaskTimeoutMs 触发"
|
|
14424
|
+
};
|
|
14425
|
+
}
|
|
14426
|
+
if (promptRes.kind === "aborted") {
|
|
14427
|
+
return {
|
|
14428
|
+
ok: false,
|
|
14429
|
+
summary: "subtask 已被父任务取消",
|
|
14430
|
+
status: "cancelled"
|
|
14431
|
+
};
|
|
14432
|
+
}
|
|
14433
|
+
const r = promptRes.value;
|
|
14434
|
+
if (r.error || !r.data) {
|
|
14435
|
+
return {
|
|
14436
|
+
ok: false,
|
|
14437
|
+
summary: `session.prompt 失败:${describe4(r.error)}`,
|
|
14438
|
+
status: "failed",
|
|
14439
|
+
error: describe4(r.error) || "no data"
|
|
14440
|
+
};
|
|
14441
|
+
}
|
|
14442
|
+
const lastText = pickLastText(r.data.parts ?? []);
|
|
14443
|
+
const finishReason = (r.data.info?.finish ?? "").toLowerCase();
|
|
14444
|
+
const llmError = r.data.info?.error;
|
|
14445
|
+
let status;
|
|
14446
|
+
if (llmError)
|
|
14447
|
+
status = "failed";
|
|
14448
|
+
else if (finishReason === "length")
|
|
14449
|
+
status = "need_review";
|
|
14450
|
+
else
|
|
14451
|
+
status = "success";
|
|
14452
|
+
let changedFiles;
|
|
14453
|
+
try {
|
|
14454
|
+
const diff = await opts.client.session.diff({
|
|
14455
|
+
path: { id: childSessionId },
|
|
14456
|
+
query: opts.directory ? { directory: opts.directory } : undefined
|
|
14457
|
+
});
|
|
14458
|
+
changedFiles = pickDiffFiles(diff.data);
|
|
14459
|
+
} catch (err) {
|
|
14460
|
+
log4("warn", `[opencode-runner] diff 取失败 ${spec.id}`, { error: describe4(err) });
|
|
14461
|
+
}
|
|
14462
|
+
const elapsed = Date.now() - startedAt;
|
|
14463
|
+
const summary = lastText || `subtask ${spec.id} 完成(${elapsed}ms,无文本输出,finish=${finishReason || "unknown"})`;
|
|
14464
|
+
return {
|
|
14465
|
+
ok: status !== "failed",
|
|
14466
|
+
summary,
|
|
14467
|
+
status,
|
|
14468
|
+
changedFiles,
|
|
14469
|
+
error: llmError ? describe4(llmError) : undefined
|
|
14470
|
+
};
|
|
14471
|
+
} catch (err) {
|
|
14472
|
+
return {
|
|
14473
|
+
ok: false,
|
|
14474
|
+
summary: `runner 抛错:${describe4(err)}`,
|
|
14475
|
+
status: "failed",
|
|
14476
|
+
error: describe4(err)
|
|
14477
|
+
};
|
|
14478
|
+
} finally {
|
|
14479
|
+
if (childSessionId) {
|
|
14480
|
+
try {
|
|
14481
|
+
await opts.client.session.delete({
|
|
14482
|
+
path: { id: childSessionId },
|
|
14483
|
+
query: opts.directory ? { directory: opts.directory } : undefined
|
|
14484
|
+
});
|
|
14485
|
+
} catch (err) {
|
|
14486
|
+
log4("warn", `[opencode-runner] session.delete 失败 ${childSessionId}`, {
|
|
14487
|
+
error: describe4(err)
|
|
14488
|
+
});
|
|
14489
|
+
}
|
|
14490
|
+
}
|
|
14491
|
+
}
|
|
14492
|
+
};
|
|
14493
|
+
}
|
|
14494
|
+
function composePromptText(spec) {
|
|
14495
|
+
const lines = [spec.description.trim()];
|
|
14496
|
+
if (spec.args && Object.keys(spec.args).length > 0) {
|
|
14497
|
+
lines.push("", "<!-- subtask args -->");
|
|
14498
|
+
for (const [k, v] of Object.entries(spec.args)) {
|
|
14499
|
+
lines.push(`- ${k}: ${safeStringify(v)}`);
|
|
14500
|
+
}
|
|
14501
|
+
}
|
|
14502
|
+
return lines.join(`
|
|
14503
|
+
`);
|
|
14504
|
+
}
|
|
14505
|
+
function pickLastText(parts) {
|
|
14506
|
+
for (let i = parts.length - 1;i >= 0; i--) {
|
|
14507
|
+
const p = parts[i];
|
|
14508
|
+
if (p && p.type === "text" && typeof p.text === "string" && !p.synthetic && !p.ignored) {
|
|
14509
|
+
return p.text.trim();
|
|
14510
|
+
}
|
|
14511
|
+
}
|
|
14512
|
+
return "";
|
|
14513
|
+
}
|
|
14514
|
+
function pickDiffFiles(diffData) {
|
|
14515
|
+
if (!diffData || typeof diffData !== "object")
|
|
14516
|
+
return;
|
|
14517
|
+
const obj = diffData;
|
|
14518
|
+
if (!Array.isArray(obj.files))
|
|
14519
|
+
return;
|
|
14520
|
+
const out = [];
|
|
14521
|
+
for (const f of obj.files) {
|
|
14522
|
+
if (typeof f === "string")
|
|
14523
|
+
out.push(f);
|
|
14524
|
+
else if (f && typeof f.path === "string") {
|
|
14525
|
+
out.push(f.path);
|
|
14526
|
+
}
|
|
14527
|
+
}
|
|
14528
|
+
return out.length > 0 ? out : undefined;
|
|
14529
|
+
}
|
|
14530
|
+
async function withTimeout3(p, ms, signal) {
|
|
14531
|
+
if (signal?.aborted)
|
|
14532
|
+
return { kind: "aborted" };
|
|
14533
|
+
if (!ms || ms <= 0) {
|
|
14534
|
+
try {
|
|
14535
|
+
const value = await p;
|
|
14536
|
+
return { kind: "ok", value };
|
|
14537
|
+
} catch (err) {
|
|
14538
|
+
throw err;
|
|
14539
|
+
}
|
|
14540
|
+
}
|
|
14541
|
+
return await new Promise((resolve13, reject) => {
|
|
14542
|
+
let settled = false;
|
|
14543
|
+
const timer = setTimeout(() => {
|
|
14544
|
+
if (settled)
|
|
14545
|
+
return;
|
|
14546
|
+
settled = true;
|
|
14547
|
+
resolve13({ kind: "timeout" });
|
|
14548
|
+
}, ms);
|
|
14549
|
+
const onAbort = () => {
|
|
14550
|
+
if (settled)
|
|
14551
|
+
return;
|
|
14552
|
+
settled = true;
|
|
14553
|
+
clearTimeout(timer);
|
|
14554
|
+
resolve13({ kind: "aborted" });
|
|
14555
|
+
};
|
|
14556
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
14557
|
+
p.then((value) => {
|
|
14558
|
+
if (settled)
|
|
14559
|
+
return;
|
|
14560
|
+
settled = true;
|
|
14561
|
+
clearTimeout(timer);
|
|
14562
|
+
signal?.removeEventListener("abort", onAbort);
|
|
14563
|
+
resolve13({ kind: "ok", value });
|
|
14564
|
+
}, (err) => {
|
|
14565
|
+
if (settled)
|
|
14566
|
+
return;
|
|
14567
|
+
settled = true;
|
|
14568
|
+
clearTimeout(timer);
|
|
14569
|
+
signal?.removeEventListener("abort", onAbort);
|
|
14570
|
+
reject(err);
|
|
14571
|
+
});
|
|
14572
|
+
});
|
|
14573
|
+
}
|
|
14574
|
+
function describe4(err) {
|
|
14575
|
+
if (!err)
|
|
14576
|
+
return "";
|
|
14577
|
+
if (err instanceof Error)
|
|
14578
|
+
return err.message;
|
|
14579
|
+
if (typeof err === "string")
|
|
14580
|
+
return err;
|
|
14581
|
+
try {
|
|
14582
|
+
return JSON.stringify(err);
|
|
14583
|
+
} catch {
|
|
14584
|
+
return String(err);
|
|
14585
|
+
}
|
|
14586
|
+
}
|
|
14587
|
+
function safeStringify(v) {
|
|
14588
|
+
try {
|
|
14589
|
+
return JSON.stringify(v);
|
|
14590
|
+
} catch {
|
|
14591
|
+
return String(v);
|
|
14592
|
+
}
|
|
14593
|
+
}
|
|
14594
|
+
function clip3(s, max) {
|
|
14595
|
+
if (!s)
|
|
14596
|
+
return "";
|
|
14597
|
+
return s.length <= max ? s : s.slice(0, max - 1) + "…";
|
|
14598
|
+
}
|
|
14599
|
+
async function sendParentNotice(client, sessionID, text, opts = {}) {
|
|
14600
|
+
const log4 = opts.log ?? (() => {});
|
|
14601
|
+
if (!client?.session) {
|
|
14602
|
+
log4("warn", "[sendParentNotice] client.session 不可用,noop");
|
|
14603
|
+
return false;
|
|
14604
|
+
}
|
|
14605
|
+
const sessionAny = client.session;
|
|
14606
|
+
if (typeof sessionAny.promptAsync !== "function") {
|
|
14607
|
+
log4("warn", "[sendParentNotice] promptAsync 不可用(SDK 太老?),noop");
|
|
14608
|
+
return false;
|
|
14609
|
+
}
|
|
14610
|
+
try {
|
|
14611
|
+
const res = await sessionAny.promptAsync({
|
|
14612
|
+
path: { id: sessionID },
|
|
14613
|
+
query: opts.directory ? { directory: opts.directory } : undefined,
|
|
14614
|
+
body: {
|
|
14615
|
+
noReply: true,
|
|
14616
|
+
parts: [
|
|
14617
|
+
{
|
|
14618
|
+
id: makePartId(),
|
|
14619
|
+
type: "text",
|
|
14620
|
+
text,
|
|
14621
|
+
synthetic: false,
|
|
14622
|
+
ignored: false
|
|
14623
|
+
}
|
|
14624
|
+
]
|
|
14625
|
+
}
|
|
14626
|
+
});
|
|
14627
|
+
if (res && typeof res === "object" && "error" in res && res.error) {
|
|
14628
|
+
log4("warn", "[sendParentNotice] promptAsync 返回 error", { error: res.error });
|
|
14629
|
+
return false;
|
|
14630
|
+
}
|
|
14631
|
+
return true;
|
|
14632
|
+
} catch (err) {
|
|
14633
|
+
log4("warn", "[sendParentNotice] 抛错(已隔离)", {
|
|
14634
|
+
error: err instanceof Error ? err.message : String(err)
|
|
14635
|
+
});
|
|
14636
|
+
return false;
|
|
14637
|
+
}
|
|
14638
|
+
}
|
|
14639
|
+
|
|
14640
|
+
// lib/spawner-production.ts
|
|
14641
|
+
init_decision_parser();
|
|
14642
|
+
|
|
14643
|
+
class ProductionSpawner {
|
|
14644
|
+
opts;
|
|
14645
|
+
constructor(opts) {
|
|
14646
|
+
this.opts = opts;
|
|
14647
|
+
}
|
|
14648
|
+
async spawnReviewer(args) {
|
|
14649
|
+
const prompt = buildReviewerPrompt(args);
|
|
14650
|
+
let r;
|
|
14651
|
+
try {
|
|
14652
|
+
r = await this.runSubagent({
|
|
14653
|
+
agentName: this.opts.reviewerAgent ?? "reviewer",
|
|
14654
|
+
prompt,
|
|
14655
|
+
title: `[merge-review] sess=${args.sessionId.slice(0, 8)} r=${args.round}/${args.maxRounds}`,
|
|
14656
|
+
...args.signal ? { signal: args.signal } : {},
|
|
14657
|
+
timeoutMs: this.opts.reviewerTimeoutMs ?? 600000
|
|
14658
|
+
});
|
|
14659
|
+
} catch (err) {
|
|
14660
|
+
throw err;
|
|
14661
|
+
}
|
|
14662
|
+
if (r.llmError) {
|
|
14663
|
+
return {
|
|
14664
|
+
decision: "REQUEST_CHANGES",
|
|
14665
|
+
summary: `reviewer LLM error: ${describe5(r.llmError)}`
|
|
14666
|
+
};
|
|
14667
|
+
}
|
|
14668
|
+
const parsed = parseDecision(r.text);
|
|
14669
|
+
if (parsed.token === null) {
|
|
14670
|
+
return {
|
|
14671
|
+
decision: "REQUEST_CHANGES",
|
|
14672
|
+
summary: `decision parse failed: ${parsed.reason}
|
|
14673
|
+
|
|
14674
|
+
---
|
|
14675
|
+
${r.text.slice(0, 800)}`
|
|
14676
|
+
};
|
|
14677
|
+
}
|
|
14678
|
+
return { decision: parsed.token, summary: r.text };
|
|
14679
|
+
}
|
|
14680
|
+
async spawnCoder(args) {
|
|
14681
|
+
const prompt = buildCoderPrompt(args);
|
|
14682
|
+
let r;
|
|
14683
|
+
try {
|
|
14684
|
+
r = await this.runSubagent({
|
|
14685
|
+
agentName: this.opts.coderAgent ?? "coder",
|
|
14686
|
+
prompt,
|
|
14687
|
+
title: `[merge-fix] sess=${args.sessionId.slice(0, 8)}`,
|
|
14688
|
+
...args.signal ? { signal: args.signal } : {},
|
|
14689
|
+
timeoutMs: this.opts.coderTimeoutMs ?? 1800000
|
|
14690
|
+
});
|
|
14691
|
+
} catch (err) {
|
|
14692
|
+
throw err;
|
|
14693
|
+
}
|
|
14694
|
+
if (r.llmError)
|
|
14695
|
+
return { ok: false, summary: `coder error: ${describe5(r.llmError)}` };
|
|
14696
|
+
if (r.finishReason === "length") {
|
|
14697
|
+
return {
|
|
14698
|
+
ok: false,
|
|
14699
|
+
summary: `coder truncated (finish=length): ${r.text.slice(0, 500)}`
|
|
14700
|
+
};
|
|
14701
|
+
}
|
|
14702
|
+
return { ok: true, summary: r.text || "(coder 无文本输出)" };
|
|
14703
|
+
}
|
|
14704
|
+
async runSubagent(opts) {
|
|
14705
|
+
let childId;
|
|
14706
|
+
try {
|
|
14707
|
+
const created = await this.opts.client.session.create({
|
|
14708
|
+
body: { title: clip4(opts.title, 80) },
|
|
14709
|
+
query: this.opts.directory ? { directory: this.opts.directory } : undefined
|
|
14710
|
+
});
|
|
14711
|
+
if (created.error || !created.data?.id) {
|
|
14712
|
+
throw new Error(`session.create 失败: ${describe5(created.error) || "no id"}`);
|
|
14713
|
+
}
|
|
14714
|
+
childId = created.data.id;
|
|
14715
|
+
const promptPromise = Promise.resolve(this.opts.client.session.prompt({
|
|
14716
|
+
path: { id: childId },
|
|
14717
|
+
body: {
|
|
14718
|
+
agent: opts.agentName,
|
|
14719
|
+
parts: [{ type: "text", text: opts.prompt }]
|
|
14720
|
+
},
|
|
14721
|
+
query: this.opts.directory ? { directory: this.opts.directory } : undefined
|
|
14722
|
+
}));
|
|
14723
|
+
const res = await raceAbortTimeout(promptPromise, opts.signal, opts.timeoutMs, opts.title);
|
|
14724
|
+
if (res.error || !res.data) {
|
|
14725
|
+
throw new Error(`session.prompt 失败: ${describe5(res.error) || "no data"}`);
|
|
14726
|
+
}
|
|
14727
|
+
const text = pickLastText(res.data.parts ?? []);
|
|
14728
|
+
const finishReason = (res.data.info?.finish ?? "").toLowerCase();
|
|
14729
|
+
const llmError = res.data.info?.error;
|
|
14730
|
+
return llmError !== undefined ? { text, finishReason, llmError } : { text, finishReason };
|
|
14731
|
+
} finally {
|
|
14732
|
+
if (childId) {
|
|
14733
|
+
try {
|
|
14734
|
+
await this.opts.client.session.delete({
|
|
14735
|
+
path: { id: childId },
|
|
14736
|
+
query: this.opts.directory ? { directory: this.opts.directory } : undefined
|
|
14737
|
+
});
|
|
14738
|
+
} catch (err) {
|
|
14739
|
+
this.opts.log?.("warn", `[spawner] session.delete 失败 ${childId}`, {
|
|
14740
|
+
err: describe5(err)
|
|
14741
|
+
});
|
|
14742
|
+
}
|
|
14743
|
+
}
|
|
14744
|
+
}
|
|
14745
|
+
}
|
|
14746
|
+
}
|
|
14747
|
+
async function raceAbortTimeout(p, signal, timeoutMs, label) {
|
|
14748
|
+
if (signal?.aborted) {
|
|
14749
|
+
const e = new Error(`${label} aborted before start`);
|
|
14750
|
+
e.name = "AbortError";
|
|
14751
|
+
throw e;
|
|
14752
|
+
}
|
|
14753
|
+
return await new Promise((resolve13, reject) => {
|
|
14754
|
+
let settled = false;
|
|
14755
|
+
const timer = setTimeout(() => {
|
|
14756
|
+
if (settled)
|
|
14757
|
+
return;
|
|
14758
|
+
settled = true;
|
|
14759
|
+
signal?.removeEventListener("abort", onAbort);
|
|
14760
|
+
reject(new Error(`${label} 超时 (${timeoutMs}ms)`));
|
|
14761
|
+
}, timeoutMs);
|
|
14762
|
+
const onAbort = () => {
|
|
14763
|
+
if (settled)
|
|
14764
|
+
return;
|
|
14765
|
+
settled = true;
|
|
14766
|
+
clearTimeout(timer);
|
|
14767
|
+
const e = new Error(`${label} aborted by signal`);
|
|
14768
|
+
e.name = "AbortError";
|
|
14769
|
+
reject(e);
|
|
14770
|
+
};
|
|
14771
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
14772
|
+
p.then((v) => {
|
|
14773
|
+
if (settled)
|
|
14774
|
+
return;
|
|
14775
|
+
settled = true;
|
|
14776
|
+
clearTimeout(timer);
|
|
14777
|
+
signal?.removeEventListener("abort", onAbort);
|
|
14778
|
+
resolve13(v);
|
|
14779
|
+
}, (e) => {
|
|
14780
|
+
if (settled)
|
|
14781
|
+
return;
|
|
14782
|
+
settled = true;
|
|
14783
|
+
clearTimeout(timer);
|
|
14784
|
+
signal?.removeEventListener("abort", onAbort);
|
|
14785
|
+
reject(e);
|
|
14786
|
+
});
|
|
14787
|
+
});
|
|
14788
|
+
}
|
|
14789
|
+
function buildReviewerPrompt(args) {
|
|
14790
|
+
const lines = [
|
|
14791
|
+
"[Session Merge Review]",
|
|
14792
|
+
"",
|
|
14793
|
+
`session_id: ${args.sessionId}`,
|
|
14794
|
+
`worktree_path: ${args.worktreePath}`,
|
|
14795
|
+
`base_sha: ${args.baseSha}`
|
|
14796
|
+
];
|
|
14797
|
+
if (args.planId)
|
|
14798
|
+
lines.push(`plan_id: ${args.planId}`);
|
|
14799
|
+
lines.push(`round: ${args.round}/${args.maxRounds}`, "", "请按 reviewer.md 「worktree-session 审阅」模式执行:", "1. 若有 plan_id → 先 plan_read(plan_id=...) 拿方案", "2. 跑 `git -C <worktree_path> diff <base_sha>..HEAD` 看改动", '3. APPROVE 前必须先调 review_approval(verdict=APPROVE, pendingIds=["session:<session_id>"]) 写审批', "4. 输出 ## Decision 节,首行 APPROVE / REQUEST_CHANGES / BLOCK 之一");
|
|
14800
|
+
if (args.prevSummary) {
|
|
14801
|
+
lines.push("", "## 上一轮 reviewer 意见", "", args.prevSummary, "", "请确认 coder 是否已按意见修复");
|
|
14802
|
+
}
|
|
14803
|
+
return lines.join(`
|
|
14804
|
+
`);
|
|
14805
|
+
}
|
|
14806
|
+
function buildCoderPrompt(args) {
|
|
14807
|
+
const lines = [
|
|
14808
|
+
"[Session Merge Fix]",
|
|
14809
|
+
"",
|
|
14810
|
+
`session_id: ${args.sessionId}`
|
|
14811
|
+
];
|
|
14812
|
+
if (args.planId)
|
|
14813
|
+
lines.push(`plan_id: ${args.planId}`);
|
|
14814
|
+
lines.push("", "reviewer 在上一轮给出了 REQUEST_CHANGES,意见如下:", "", args.reviewerSummary, "", "请:", "1. 若有 plan_id → 先 plan_read(plan_id=...) 闭合 worktree-guard hard gate", "2. 按 reviewer 意见在当前 session worktree 内修改代码(直接 edit/write)", "3. 修完输出简短摘要(哪些文件改了什么),由下一轮 reviewer 再审");
|
|
14815
|
+
return lines.join(`
|
|
14816
|
+
`);
|
|
14817
|
+
}
|
|
14818
|
+
function describe5(err) {
|
|
14819
|
+
if (err === undefined || err === null)
|
|
14820
|
+
return "";
|
|
14821
|
+
if (err instanceof Error)
|
|
14822
|
+
return err.message;
|
|
14823
|
+
if (typeof err === "string")
|
|
14824
|
+
return err;
|
|
14825
|
+
try {
|
|
14826
|
+
return JSON.stringify(err);
|
|
14827
|
+
} catch {
|
|
14828
|
+
return String(err);
|
|
14829
|
+
}
|
|
14830
|
+
}
|
|
14831
|
+
function clip4(s, max) {
|
|
14832
|
+
if (!s)
|
|
14833
|
+
return "";
|
|
14834
|
+
return s.length <= max ? s : s.slice(0, max - 1) + "…";
|
|
14835
|
+
}
|
|
14836
|
+
|
|
14837
|
+
// lib/codeforge-runtime.ts
|
|
14838
|
+
import { promises as fs13 } from "node:fs";
|
|
14839
|
+
import * as path16 from "node:path";
|
|
14840
|
+
var DEFAULT_RUNTIME = {
|
|
14841
|
+
autonomy: {
|
|
14842
|
+
default_mode: "semi",
|
|
14843
|
+
downgrade_on_risky: true
|
|
14844
|
+
},
|
|
14845
|
+
runtime: {
|
|
14846
|
+
worktree_isolation: true,
|
|
14847
|
+
workspace_snapshot: true,
|
|
14848
|
+
auto_commit: false
|
|
14849
|
+
},
|
|
14850
|
+
tools: {
|
|
14851
|
+
browser: { enabled: false }
|
|
14852
|
+
},
|
|
14853
|
+
context: {
|
|
14854
|
+
condenser_threshold_ratio: 0.7
|
|
14855
|
+
},
|
|
14856
|
+
update: {
|
|
14857
|
+
auto_check_enabled: true,
|
|
14858
|
+
interval_hours: 24,
|
|
14859
|
+
repo: "andy-personal/code-forge",
|
|
14860
|
+
channel: "latest",
|
|
14861
|
+
package: "@andyqiu/codeforge",
|
|
14862
|
+
registry: "https://registry.npmjs.org",
|
|
14863
|
+
auto_install: true,
|
|
14864
|
+
backup_keep: 3
|
|
14865
|
+
}
|
|
14866
|
+
};
|
|
14867
|
+
function loadRuntimeSync(opts = {}) {
|
|
14868
|
+
const hit = findConfigFileSync(opts);
|
|
14869
|
+
if (!hit) {
|
|
14870
|
+
return emptyResult({ reason: `${CONFIG_FILE} not found (using built-in defaults)` });
|
|
14871
|
+
}
|
|
14872
|
+
const fsSync = __require("node:fs");
|
|
14873
|
+
let raw;
|
|
14874
|
+
try {
|
|
14875
|
+
raw = fsSync.readFileSync(hit, "utf8");
|
|
14876
|
+
} catch (e) {
|
|
14877
|
+
return emptyResult({ path: hit, reason: `read_failed: ${e.message}` });
|
|
14878
|
+
}
|
|
14879
|
+
return parseRuntime(raw, hit);
|
|
14880
|
+
}
|
|
14881
|
+
async function loadRuntime(opts = {}) {
|
|
14882
|
+
const root = opts.root ?? process.cwd();
|
|
14883
|
+
const abs = path16.resolve(root, opts.file ?? CONFIG_FILE);
|
|
14884
|
+
let raw;
|
|
14885
|
+
try {
|
|
14886
|
+
raw = await fs13.readFile(abs, "utf8");
|
|
14887
|
+
} catch (e) {
|
|
14888
|
+
const code = e.code;
|
|
14889
|
+
if (code === "ENOENT") {
|
|
14890
|
+
return emptyResult({ reason: `${CONFIG_FILE} not found at ${abs} (using built-in defaults)` });
|
|
14891
|
+
}
|
|
14892
|
+
return emptyResult({ path: abs, reason: `read_failed: ${e.message}` });
|
|
14893
|
+
}
|
|
14894
|
+
return parseRuntime(raw, abs);
|
|
14895
|
+
}
|
|
14896
|
+
function emptyResult(opts) {
|
|
14897
|
+
return {
|
|
14898
|
+
ok: false,
|
|
14899
|
+
path: opts.path ?? null,
|
|
14900
|
+
runtime: cloneDefaults(),
|
|
14901
|
+
warnings: [],
|
|
14902
|
+
error: opts.reason
|
|
14903
|
+
};
|
|
14904
|
+
}
|
|
14905
|
+
function cloneDefaults() {
|
|
14906
|
+
return JSON.parse(JSON.stringify(DEFAULT_RUNTIME));
|
|
14907
|
+
}
|
|
14908
|
+
var VALID_CHANNELS = ["stable", "prerelease", "latest", "next", "rc"];
|
|
14909
|
+
function parseRuntime(raw, abs) {
|
|
14910
|
+
let root = null;
|
|
14911
|
+
try {
|
|
14912
|
+
const parsed = JSON.parse(raw);
|
|
14913
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
14914
|
+
root = parsed;
|
|
14915
|
+
}
|
|
14916
|
+
} catch (e) {
|
|
14917
|
+
return emptyResult({ path: abs, reason: `invalid_json: ${e.message}` });
|
|
14918
|
+
}
|
|
14919
|
+
if (!root) {
|
|
14920
|
+
return emptyResult({ path: abs, reason: "config_root_must_be_object" });
|
|
14921
|
+
}
|
|
14922
|
+
const warnings = [];
|
|
14923
|
+
const cfg = cloneDefaults();
|
|
14924
|
+
if (root.autonomy && typeof root.autonomy === "object" && !Array.isArray(root.autonomy)) {
|
|
14925
|
+
const a = root.autonomy;
|
|
14926
|
+
if (a.default_mode !== undefined) {
|
|
14927
|
+
if (a.default_mode === "step" || a.default_mode === "semi" || a.default_mode === "full") {
|
|
14928
|
+
cfg.autonomy.default_mode = a.default_mode;
|
|
14929
|
+
} else {
|
|
14930
|
+
warnings.push(`autonomy.default_mode invalid: ${String(a.default_mode)} (keep ${cfg.autonomy.default_mode})`);
|
|
14931
|
+
}
|
|
14932
|
+
}
|
|
14933
|
+
if (a.downgrade_on_risky !== undefined) {
|
|
14934
|
+
if (typeof a.downgrade_on_risky === "boolean") {
|
|
14391
14935
|
cfg.autonomy.downgrade_on_risky = a.downgrade_on_risky;
|
|
14392
14936
|
} else {
|
|
14393
14937
|
warnings.push(`autonomy.downgrade_on_risky must be boolean (kept default)`);
|
|
@@ -15043,10 +15587,20 @@ var codeforgeToolsServer = async (ctx) => {
|
|
|
15043
15587
|
browser_enabled: browserEnabled,
|
|
15044
15588
|
config_source: rt.ok ? "codeforge.json" : "built-in"
|
|
15045
15589
|
});
|
|
15590
|
+
__setContext2({
|
|
15591
|
+
resolveCurrentSessionId: () => process.env["CODEFORGE_SESSION_ID"],
|
|
15592
|
+
resolveMainRoot: () => ctx.directory ?? process.cwd(),
|
|
15593
|
+
store: new PlanStore({ root: ctx.directory ?? process.cwd() })
|
|
15594
|
+
});
|
|
15595
|
+
const spawner = new ProductionSpawner({
|
|
15596
|
+
client: ctx.client,
|
|
15597
|
+
directory: ctx.directory ?? process.cwd(),
|
|
15598
|
+
log: (level, msg, data) => safeWriteLog(PLUGIN_NAME7, { level, msg, data })
|
|
15599
|
+
});
|
|
15046
15600
|
__setContext({
|
|
15047
|
-
|
|
15048
|
-
|
|
15049
|
-
|
|
15601
|
+
mainRoot: ctx.directory ?? process.cwd(),
|
|
15602
|
+
spawner,
|
|
15603
|
+
resolveCurrentSessionId: () => process.env["CODEFORGE_SESSION_ID"] ?? ""
|
|
15050
15604
|
});
|
|
15051
15605
|
const browserTools = browserEnabled ? buildBrowserTools() : {};
|
|
15052
15606
|
const khWriteTools = buildKhWriteTools();
|
|
@@ -15119,8 +15673,8 @@ var codeforgeToolsServer = async (ctx) => {
|
|
|
15119
15673
|
]).describe("编辑类型;不同 action 需要的字段不同"),
|
|
15120
15674
|
target: z30.string().min(1).describe("目标文件路径(相对 cwd 或绝对)"),
|
|
15121
15675
|
before_hash: z30.string().optional().describe("操作前的 sha256 hex(强烈建议传,新文件传 null/省略)"),
|
|
15122
|
-
auto_stage: z30.boolean().optional().describe("
|
|
15123
|
-
description: z30.string().optional().describe("
|
|
15676
|
+
auto_stage: z30.boolean().optional().describe("已废弃(保留兼容字段):Phase 5 后 ast_edit 直接写入 session worktree,无需独立 stage"),
|
|
15677
|
+
description: z30.string().optional().describe("可选变更说明(写入 audit log)"),
|
|
15124
15678
|
anchor: z30.string().optional().describe("anchor 类用:anchor 文本或 regex 源"),
|
|
15125
15679
|
regex: z30.boolean().optional().describe("anchor 是否按 RegExp 解释,默认 false"),
|
|
15126
15680
|
occurrence: z30.number().int().min(1).optional().describe("第几次匹配(1-based),默认 1"),
|
|
@@ -15254,11 +15808,23 @@ var codeforgeToolsServer = async (ctx) => {
|
|
|
15254
15808
|
plan_id: z30.string().optional().describe("关联的 plan_id(reviewer 校验时用),格式 plan-YYYYMMDD-HHmmss-NNN"),
|
|
15255
15809
|
force: z30.boolean().optional().describe("action=merge 时跳过 review 直接 squash merge(写审计)")
|
|
15256
15810
|
},
|
|
15257
|
-
async execute(args) {
|
|
15811
|
+
async execute(args, input) {
|
|
15258
15812
|
return await runSafe("session_merge", async () => {
|
|
15259
15813
|
const v = projectValidate("session_merge", ArgsSchema26, args);
|
|
15260
15814
|
if (!v.ok)
|
|
15261
15815
|
return wrap(JSON.parse(v.output));
|
|
15816
|
+
const sid = input.sessionID;
|
|
15817
|
+
__setContext({
|
|
15818
|
+
mainRoot: ctx.directory ?? process.cwd(),
|
|
15819
|
+
spawner,
|
|
15820
|
+
resolveCurrentSessionId: () => process.env["CODEFORGE_SESSION_ID"] ?? "",
|
|
15821
|
+
...sid ? { currentSessionId: sid } : {},
|
|
15822
|
+
...sid ? {
|
|
15823
|
+
sendProgress: async (state, detail) => {
|
|
15824
|
+
await sendParentNotice(ctx.client, sid, `[merge-loop] ${state}: ${detail}`, ctx.directory ? { directory: ctx.directory } : {});
|
|
15825
|
+
}
|
|
15826
|
+
} : {}
|
|
15827
|
+
});
|
|
15262
15828
|
const result = await execute26(v.data);
|
|
15263
15829
|
const meta = {
|
|
15264
15830
|
title: `session_merge: ${args.action}`,
|
|
@@ -16041,7 +16607,7 @@ function formatInjection(query, insights, mode = INJECTION_MODE) {
|
|
|
16041
16607
|
}
|
|
16042
16608
|
var CODEFORGE_CONSTRAINTS = [
|
|
16043
16609
|
{
|
|
16044
|
-
content: "本会话使用 CodeForge:触发词命中(部署/怎么/为什么/之前/历史等)必须先 smart_search
|
|
16610
|
+
content: "本会话使用 CodeForge:触发词命中(部署/怎么/为什么/之前/历史等)必须先 smart_search;写代码直接在 session worktree 内 edit/write/ast_edit(worktree 由 session-worktree-guard 隔离主仓),完成后由用户 /merge 拍板",
|
|
16045
16611
|
priority: 9
|
|
16046
16612
|
},
|
|
16047
16613
|
{
|
|
@@ -17840,7 +18406,7 @@ ${toolsBulleted}
|
|
|
17840
18406
|
Heuristics:
|
|
17841
18407
|
- Need to read 3 files? → emit 3 \`read\` calls in ONE response, not 3 sequential turns
|
|
17842
18408
|
- Need search + map? → emit \`smart_search\` + \`repo_map\` in ONE response
|
|
17843
|
-
- DO NOT batch write tools (
|
|
18409
|
+
- DO NOT batch write tools (edit / write / ast_edit / bash)
|
|
17844
18410
|
- DO NOT chain reads where each read's path depends on previous read's output
|
|
17845
18411
|
|
|
17846
18412
|
This directive is re-injected every turn — it is not optional advice.
|
|
@@ -18105,7 +18671,7 @@ function buildRecoveryPlan(events, meta = { id: null }, opts = {}) {
|
|
|
18105
18671
|
for (let i = events.length - 1;i >= 0; i--) {
|
|
18106
18672
|
const e = events[i];
|
|
18107
18673
|
if (e.type === "message" && e.role === "user") {
|
|
18108
|
-
lastUser =
|
|
18674
|
+
lastUser = clip5(e.content, o.excerptLimit);
|
|
18109
18675
|
break;
|
|
18110
18676
|
}
|
|
18111
18677
|
}
|
|
@@ -18123,7 +18689,7 @@ function buildRecoveryPlan(events, meta = { id: null }, opts = {}) {
|
|
|
18123
18689
|
const toolMap = new Map;
|
|
18124
18690
|
for (const t of window) {
|
|
18125
18691
|
const cur = toolMap.get(t.tool);
|
|
18126
|
-
const argsExcerpt =
|
|
18692
|
+
const argsExcerpt = clip5(safeJson(t.args), o.excerptLimit);
|
|
18127
18693
|
if (cur) {
|
|
18128
18694
|
cur.count++;
|
|
18129
18695
|
cur.last_ok = t.ok;
|
|
@@ -18212,7 +18778,7 @@ function formatIdle(ms) {
|
|
|
18212
18778
|
return `${Math.round(ms / 3600000)}h`;
|
|
18213
18779
|
return `${Math.round(ms / 86400000)}d`;
|
|
18214
18780
|
}
|
|
18215
|
-
function
|
|
18781
|
+
function clip5(s, max) {
|
|
18216
18782
|
if (!s)
|
|
18217
18783
|
return "";
|
|
18218
18784
|
if (s.length <= max)
|
|
@@ -18442,7 +19008,7 @@ async function mergeOneAttempt(r, opts, mergeFns, log10) {
|
|
|
18442
19008
|
});
|
|
18443
19009
|
} catch (commitErr) {
|
|
18444
19010
|
attempt.ok = false;
|
|
18445
|
-
attempt.message = `commitWorktreeIfDirty 失败:${
|
|
19011
|
+
attempt.message = `commitWorktreeIfDirty 失败:${describe6(commitErr)}`;
|
|
18446
19012
|
attempt.durationMs = Date.now() - t0;
|
|
18447
19013
|
return {
|
|
18448
19014
|
attempt,
|
|
@@ -18471,11 +19037,11 @@ async function mergeOneAttempt(r, opts, mergeFns, log10) {
|
|
|
18471
19037
|
abortFailed = true;
|
|
18472
19038
|
log10("error", `[parallel-merge] mergeAbort 失败(仓库可能锁死)`, {
|
|
18473
19039
|
id: r.id,
|
|
18474
|
-
error:
|
|
19040
|
+
error: describe6(abortErr)
|
|
18475
19041
|
});
|
|
18476
19042
|
}
|
|
18477
19043
|
attempt.ok = false;
|
|
18478
|
-
attempt.message = `mergeCommit 失败:${
|
|
19044
|
+
attempt.message = `mergeCommit 失败:${describe6(commitErr)}`;
|
|
18479
19045
|
attempt.durationMs = Date.now() - t0;
|
|
18480
19046
|
return {
|
|
18481
19047
|
attempt,
|
|
@@ -18496,7 +19062,7 @@ async function mergeOneAttempt(r, opts, mergeFns, log10) {
|
|
|
18496
19062
|
}
|
|
18497
19063
|
} catch (err) {
|
|
18498
19064
|
attempt.ok = false;
|
|
18499
|
-
attempt.message = `mergeWorktrees 异常:${
|
|
19065
|
+
attempt.message = `mergeWorktrees 异常:${describe6(err)}`;
|
|
18500
19066
|
attempt.durationMs = Date.now() - t0;
|
|
18501
19067
|
return {
|
|
18502
19068
|
attempt,
|
|
@@ -18639,7 +19205,7 @@ async function safeRemoveWorktree(fn, root, wt, log10, subtaskId) {
|
|
|
18639
19205
|
await fn({ root, worktree_path: wt, force: true });
|
|
18640
19206
|
} catch (err) {
|
|
18641
19207
|
log10("warn", `[parallel] removeWorktree 失败 ${subtaskId}`, {
|
|
18642
|
-
error:
|
|
19208
|
+
error: describe6(err),
|
|
18643
19209
|
worktree: wt
|
|
18644
19210
|
});
|
|
18645
19211
|
}
|
|
@@ -18652,11 +19218,11 @@ async function fireMergeAttempt(cb, attempt, idx, log10) {
|
|
|
18652
19218
|
} catch (err) {
|
|
18653
19219
|
log10("warn", `[parallel] onMergeAttempt 抛错(已隔离)`, {
|
|
18654
19220
|
id: attempt.subtaskId,
|
|
18655
|
-
error:
|
|
19221
|
+
error: describe6(err)
|
|
18656
19222
|
});
|
|
18657
19223
|
}
|
|
18658
19224
|
}
|
|
18659
|
-
function
|
|
19225
|
+
function describe6(err) {
|
|
18660
19226
|
if (err instanceof Error)
|
|
18661
19227
|
return err.message;
|
|
18662
19228
|
if (typeof err === "string")
|
|
@@ -18719,7 +19285,7 @@ async function schedule(opts) {
|
|
|
18719
19285
|
} catch (err) {
|
|
18720
19286
|
log10("warn", `[parallel] queue.enqueue 抛错(已隔离)`, {
|
|
18721
19287
|
id: res.id,
|
|
18722
|
-
error:
|
|
19288
|
+
error: describe7(err)
|
|
18723
19289
|
});
|
|
18724
19290
|
}
|
|
18725
19291
|
}
|
|
@@ -18730,7 +19296,7 @@ async function schedule(opts) {
|
|
|
18730
19296
|
} catch (err) {
|
|
18731
19297
|
log10("warn", `[parallel] onSubtaskFinish 抛错(已隔离)`, {
|
|
18732
19298
|
id: res.id,
|
|
18733
|
-
error:
|
|
19299
|
+
error: describe7(err)
|
|
18734
19300
|
});
|
|
18735
19301
|
}
|
|
18736
19302
|
};
|
|
@@ -18744,10 +19310,10 @@ async function schedule(opts) {
|
|
|
18744
19310
|
const res2 = {
|
|
18745
19311
|
id: spec.id,
|
|
18746
19312
|
ok: false,
|
|
18747
|
-
summary: clamp(`worktree 分配失败:${
|
|
19313
|
+
summary: clamp(`worktree 分配失败:${describe7(err)}`, limit),
|
|
18748
19314
|
status: "failed",
|
|
18749
19315
|
duration_ms: now() - subStart,
|
|
18750
|
-
error:
|
|
19316
|
+
error: describe7(err)
|
|
18751
19317
|
};
|
|
18752
19318
|
results[i] = res2;
|
|
18753
19319
|
await fireFinish(i, res2);
|
|
@@ -18760,7 +19326,7 @@ async function schedule(opts) {
|
|
|
18760
19326
|
} catch (err) {
|
|
18761
19327
|
log10("warn", `[parallel] onSubtaskStart 抛错(已隔离)`, {
|
|
18762
19328
|
id: spec.id,
|
|
18763
|
-
error:
|
|
19329
|
+
error: describe7(err)
|
|
18764
19330
|
});
|
|
18765
19331
|
}
|
|
18766
19332
|
}
|
|
@@ -18796,11 +19362,11 @@ async function schedule(opts) {
|
|
|
18796
19362
|
res = {
|
|
18797
19363
|
id: spec.id,
|
|
18798
19364
|
ok: false,
|
|
18799
|
-
summary: clamp(`runSubtask 抛错:${
|
|
19365
|
+
summary: clamp(`runSubtask 抛错:${describe7(err)}`, limit),
|
|
18800
19366
|
status: isAborted ? globalCtl.signal.aborted ? "cancelled" : "timeout" : "failed",
|
|
18801
19367
|
duration_ms: now() - subStart,
|
|
18802
19368
|
worktree: alloc?.path,
|
|
18803
|
-
error:
|
|
19369
|
+
error: describe7(err)
|
|
18804
19370
|
};
|
|
18805
19371
|
} finally {
|
|
18806
19372
|
clearTimeout(perTimer);
|
|
@@ -18810,7 +19376,7 @@ async function schedule(opts) {
|
|
|
18810
19376
|
await alloc.cleanup();
|
|
18811
19377
|
} catch (err) {
|
|
18812
19378
|
log10("warn", `[parallel] worktree 清理失败 ${spec.id}`, {
|
|
18813
|
-
error:
|
|
19379
|
+
error: describe7(err)
|
|
18814
19380
|
});
|
|
18815
19381
|
}
|
|
18816
19382
|
}
|
|
@@ -18934,7 +19500,7 @@ function buildDigest(results, conflicts) {
|
|
|
18934
19500
|
conflicts
|
|
18935
19501
|
};
|
|
18936
19502
|
}
|
|
18937
|
-
function
|
|
19503
|
+
function describe7(err) {
|
|
18938
19504
|
if (err instanceof Error)
|
|
18939
19505
|
return err.message;
|
|
18940
19506
|
if (typeof err === "string")
|
|
@@ -18966,262 +19532,6 @@ function pickStatus(r, taskAborted, globalAborted) {
|
|
|
18966
19532
|
return "failed";
|
|
18967
19533
|
}
|
|
18968
19534
|
|
|
18969
|
-
// lib/opencode-runner.ts
|
|
18970
|
-
function makeOpencodeRunner(opts) {
|
|
18971
|
-
const log10 = opts.log ?? (() => {});
|
|
18972
|
-
return async (spec, runCtx) => {
|
|
18973
|
-
const startedAt = Date.now();
|
|
18974
|
-
let childSessionId;
|
|
18975
|
-
try {
|
|
18976
|
-
const created = await opts.client.session.create({
|
|
18977
|
-
body: {
|
|
18978
|
-
parentID: opts.parentSessionID,
|
|
18979
|
-
title: clip4(`subtask:${spec.id}`, 80)
|
|
18980
|
-
},
|
|
18981
|
-
query: opts.directory ? { directory: opts.directory } : undefined
|
|
18982
|
-
});
|
|
18983
|
-
if (created.error || !created.data?.id) {
|
|
18984
|
-
return {
|
|
18985
|
-
ok: false,
|
|
18986
|
-
summary: `session.create 失败:${describe6(created.error) || "no id"}`,
|
|
18987
|
-
status: "failed",
|
|
18988
|
-
error: describe6(created.error) || "session.create 无 id"
|
|
18989
|
-
};
|
|
18990
|
-
}
|
|
18991
|
-
childSessionId = created.data.id;
|
|
18992
|
-
log10("info", `[opencode-runner] subtask=${spec.id} session=${childSessionId} created`);
|
|
18993
|
-
const promptText = composePromptText(spec);
|
|
18994
|
-
const promptPromise = opts.client.session.prompt({
|
|
18995
|
-
path: { id: childSessionId },
|
|
18996
|
-
body: {
|
|
18997
|
-
agent: spec.agent ?? opts.defaultAgent,
|
|
18998
|
-
parts: [{ type: "text", text: promptText }]
|
|
18999
|
-
},
|
|
19000
|
-
query: opts.directory ? { directory: opts.directory } : undefined
|
|
19001
|
-
});
|
|
19002
|
-
const promptRes = await withTimeout3(Promise.resolve(promptPromise), opts.perTaskTimeoutMs, runCtx.signal);
|
|
19003
|
-
if (promptRes.kind === "timeout") {
|
|
19004
|
-
return {
|
|
19005
|
-
ok: false,
|
|
19006
|
-
summary: `subtask 超时(${opts.perTaskTimeoutMs ?? 0}ms)`,
|
|
19007
|
-
status: "timeout",
|
|
19008
|
-
error: "perTaskTimeoutMs 触发"
|
|
19009
|
-
};
|
|
19010
|
-
}
|
|
19011
|
-
if (promptRes.kind === "aborted") {
|
|
19012
|
-
return {
|
|
19013
|
-
ok: false,
|
|
19014
|
-
summary: "subtask 已被父任务取消",
|
|
19015
|
-
status: "cancelled"
|
|
19016
|
-
};
|
|
19017
|
-
}
|
|
19018
|
-
const r = promptRes.value;
|
|
19019
|
-
if (r.error || !r.data) {
|
|
19020
|
-
return {
|
|
19021
|
-
ok: false,
|
|
19022
|
-
summary: `session.prompt 失败:${describe6(r.error)}`,
|
|
19023
|
-
status: "failed",
|
|
19024
|
-
error: describe6(r.error) || "no data"
|
|
19025
|
-
};
|
|
19026
|
-
}
|
|
19027
|
-
const lastText = pickLastText(r.data.parts ?? []);
|
|
19028
|
-
const finishReason = (r.data.info?.finish ?? "").toLowerCase();
|
|
19029
|
-
const llmError = r.data.info?.error;
|
|
19030
|
-
let status;
|
|
19031
|
-
if (llmError)
|
|
19032
|
-
status = "failed";
|
|
19033
|
-
else if (finishReason === "length")
|
|
19034
|
-
status = "need_review";
|
|
19035
|
-
else
|
|
19036
|
-
status = "success";
|
|
19037
|
-
let changedFiles;
|
|
19038
|
-
try {
|
|
19039
|
-
const diff = await opts.client.session.diff({
|
|
19040
|
-
path: { id: childSessionId },
|
|
19041
|
-
query: opts.directory ? { directory: opts.directory } : undefined
|
|
19042
|
-
});
|
|
19043
|
-
changedFiles = pickDiffFiles(diff.data);
|
|
19044
|
-
} catch (err) {
|
|
19045
|
-
log10("warn", `[opencode-runner] diff 取失败 ${spec.id}`, { error: describe6(err) });
|
|
19046
|
-
}
|
|
19047
|
-
const elapsed = Date.now() - startedAt;
|
|
19048
|
-
const summary = lastText || `subtask ${spec.id} 完成(${elapsed}ms,无文本输出,finish=${finishReason || "unknown"})`;
|
|
19049
|
-
return {
|
|
19050
|
-
ok: status !== "failed",
|
|
19051
|
-
summary,
|
|
19052
|
-
status,
|
|
19053
|
-
changedFiles,
|
|
19054
|
-
error: llmError ? describe6(llmError) : undefined
|
|
19055
|
-
};
|
|
19056
|
-
} catch (err) {
|
|
19057
|
-
return {
|
|
19058
|
-
ok: false,
|
|
19059
|
-
summary: `runner 抛错:${describe6(err)}`,
|
|
19060
|
-
status: "failed",
|
|
19061
|
-
error: describe6(err)
|
|
19062
|
-
};
|
|
19063
|
-
} finally {
|
|
19064
|
-
if (childSessionId) {
|
|
19065
|
-
try {
|
|
19066
|
-
await opts.client.session.delete({
|
|
19067
|
-
path: { id: childSessionId },
|
|
19068
|
-
query: opts.directory ? { directory: opts.directory } : undefined
|
|
19069
|
-
});
|
|
19070
|
-
} catch (err) {
|
|
19071
|
-
log10("warn", `[opencode-runner] session.delete 失败 ${childSessionId}`, {
|
|
19072
|
-
error: describe6(err)
|
|
19073
|
-
});
|
|
19074
|
-
}
|
|
19075
|
-
}
|
|
19076
|
-
}
|
|
19077
|
-
};
|
|
19078
|
-
}
|
|
19079
|
-
function composePromptText(spec) {
|
|
19080
|
-
const lines = [spec.description.trim()];
|
|
19081
|
-
if (spec.args && Object.keys(spec.args).length > 0) {
|
|
19082
|
-
lines.push("", "<!-- subtask args -->");
|
|
19083
|
-
for (const [k, v] of Object.entries(spec.args)) {
|
|
19084
|
-
lines.push(`- ${k}: ${safeStringify(v)}`);
|
|
19085
|
-
}
|
|
19086
|
-
}
|
|
19087
|
-
return lines.join(`
|
|
19088
|
-
`);
|
|
19089
|
-
}
|
|
19090
|
-
function pickLastText(parts) {
|
|
19091
|
-
for (let i = parts.length - 1;i >= 0; i--) {
|
|
19092
|
-
const p = parts[i];
|
|
19093
|
-
if (p && p.type === "text" && typeof p.text === "string" && !p.synthetic && !p.ignored) {
|
|
19094
|
-
return p.text.trim();
|
|
19095
|
-
}
|
|
19096
|
-
}
|
|
19097
|
-
return "";
|
|
19098
|
-
}
|
|
19099
|
-
function pickDiffFiles(diffData) {
|
|
19100
|
-
if (!diffData || typeof diffData !== "object")
|
|
19101
|
-
return;
|
|
19102
|
-
const obj = diffData;
|
|
19103
|
-
if (!Array.isArray(obj.files))
|
|
19104
|
-
return;
|
|
19105
|
-
const out = [];
|
|
19106
|
-
for (const f of obj.files) {
|
|
19107
|
-
if (typeof f === "string")
|
|
19108
|
-
out.push(f);
|
|
19109
|
-
else if (f && typeof f.path === "string") {
|
|
19110
|
-
out.push(f.path);
|
|
19111
|
-
}
|
|
19112
|
-
}
|
|
19113
|
-
return out.length > 0 ? out : undefined;
|
|
19114
|
-
}
|
|
19115
|
-
async function withTimeout3(p, ms, signal) {
|
|
19116
|
-
if (signal?.aborted)
|
|
19117
|
-
return { kind: "aborted" };
|
|
19118
|
-
if (!ms || ms <= 0) {
|
|
19119
|
-
try {
|
|
19120
|
-
const value = await p;
|
|
19121
|
-
return { kind: "ok", value };
|
|
19122
|
-
} catch (err) {
|
|
19123
|
-
throw err;
|
|
19124
|
-
}
|
|
19125
|
-
}
|
|
19126
|
-
return await new Promise((resolve15, reject) => {
|
|
19127
|
-
let settled = false;
|
|
19128
|
-
const timer = setTimeout(() => {
|
|
19129
|
-
if (settled)
|
|
19130
|
-
return;
|
|
19131
|
-
settled = true;
|
|
19132
|
-
resolve15({ kind: "timeout" });
|
|
19133
|
-
}, ms);
|
|
19134
|
-
const onAbort = () => {
|
|
19135
|
-
if (settled)
|
|
19136
|
-
return;
|
|
19137
|
-
settled = true;
|
|
19138
|
-
clearTimeout(timer);
|
|
19139
|
-
resolve15({ kind: "aborted" });
|
|
19140
|
-
};
|
|
19141
|
-
signal?.addEventListener("abort", onAbort, { once: true });
|
|
19142
|
-
p.then((value) => {
|
|
19143
|
-
if (settled)
|
|
19144
|
-
return;
|
|
19145
|
-
settled = true;
|
|
19146
|
-
clearTimeout(timer);
|
|
19147
|
-
signal?.removeEventListener("abort", onAbort);
|
|
19148
|
-
resolve15({ kind: "ok", value });
|
|
19149
|
-
}, (err) => {
|
|
19150
|
-
if (settled)
|
|
19151
|
-
return;
|
|
19152
|
-
settled = true;
|
|
19153
|
-
clearTimeout(timer);
|
|
19154
|
-
signal?.removeEventListener("abort", onAbort);
|
|
19155
|
-
reject(err);
|
|
19156
|
-
});
|
|
19157
|
-
});
|
|
19158
|
-
}
|
|
19159
|
-
function describe6(err) {
|
|
19160
|
-
if (!err)
|
|
19161
|
-
return "";
|
|
19162
|
-
if (err instanceof Error)
|
|
19163
|
-
return err.message;
|
|
19164
|
-
if (typeof err === "string")
|
|
19165
|
-
return err;
|
|
19166
|
-
try {
|
|
19167
|
-
return JSON.stringify(err);
|
|
19168
|
-
} catch {
|
|
19169
|
-
return String(err);
|
|
19170
|
-
}
|
|
19171
|
-
}
|
|
19172
|
-
function safeStringify(v) {
|
|
19173
|
-
try {
|
|
19174
|
-
return JSON.stringify(v);
|
|
19175
|
-
} catch {
|
|
19176
|
-
return String(v);
|
|
19177
|
-
}
|
|
19178
|
-
}
|
|
19179
|
-
function clip4(s, max) {
|
|
19180
|
-
if (!s)
|
|
19181
|
-
return "";
|
|
19182
|
-
return s.length <= max ? s : s.slice(0, max - 1) + "…";
|
|
19183
|
-
}
|
|
19184
|
-
async function sendParentNotice(client, sessionID, text, opts = {}) {
|
|
19185
|
-
const log10 = opts.log ?? (() => {});
|
|
19186
|
-
if (!client?.session) {
|
|
19187
|
-
log10("warn", "[sendParentNotice] client.session 不可用,noop");
|
|
19188
|
-
return false;
|
|
19189
|
-
}
|
|
19190
|
-
const sessionAny = client.session;
|
|
19191
|
-
if (typeof sessionAny.promptAsync !== "function") {
|
|
19192
|
-
log10("warn", "[sendParentNotice] promptAsync 不可用(SDK 太老?),noop");
|
|
19193
|
-
return false;
|
|
19194
|
-
}
|
|
19195
|
-
try {
|
|
19196
|
-
const res = await sessionAny.promptAsync({
|
|
19197
|
-
path: { id: sessionID },
|
|
19198
|
-
query: opts.directory ? { directory: opts.directory } : undefined,
|
|
19199
|
-
body: {
|
|
19200
|
-
noReply: true,
|
|
19201
|
-
parts: [
|
|
19202
|
-
{
|
|
19203
|
-
id: makePartId(),
|
|
19204
|
-
type: "text",
|
|
19205
|
-
text,
|
|
19206
|
-
synthetic: false,
|
|
19207
|
-
ignored: false
|
|
19208
|
-
}
|
|
19209
|
-
]
|
|
19210
|
-
}
|
|
19211
|
-
});
|
|
19212
|
-
if (res && typeof res === "object" && "error" in res && res.error) {
|
|
19213
|
-
log10("warn", "[sendParentNotice] promptAsync 返回 error", { error: res.error });
|
|
19214
|
-
return false;
|
|
19215
|
-
}
|
|
19216
|
-
return true;
|
|
19217
|
-
} catch (err) {
|
|
19218
|
-
log10("warn", "[sendParentNotice] 抛错(已隔离)", {
|
|
19219
|
-
error: err instanceof Error ? err.message : String(err)
|
|
19220
|
-
});
|
|
19221
|
-
return false;
|
|
19222
|
-
}
|
|
19223
|
-
}
|
|
19224
|
-
|
|
19225
19535
|
// plugins/subtasks.ts
|
|
19226
19536
|
init_autonomy();
|
|
19227
19537
|
|
|
@@ -19254,11 +19564,11 @@ async function decomposeTask(description29, opts) {
|
|
|
19254
19564
|
let childSessionId;
|
|
19255
19565
|
try {
|
|
19256
19566
|
const created = await opts.client.session.create({
|
|
19257
|
-
body: { title: `decompose:${
|
|
19567
|
+
body: { title: `decompose:${clip6(description29, 60)}` },
|
|
19258
19568
|
query: opts.directory ? { directory: opts.directory } : undefined
|
|
19259
19569
|
});
|
|
19260
19570
|
if (created.error || !created.data?.id) {
|
|
19261
|
-
log10("warn", "[decompose] session.create 失败", { error:
|
|
19571
|
+
log10("warn", "[decompose] session.create 失败", { error: describe8(created.error) });
|
|
19262
19572
|
return {
|
|
19263
19573
|
ok: false,
|
|
19264
19574
|
subtasks: [],
|
|
@@ -19285,7 +19595,7 @@ async function decomposeTask(description29, opts) {
|
|
|
19285
19595
|
}
|
|
19286
19596
|
const r = raced.value;
|
|
19287
19597
|
if (r.error || !r.data) {
|
|
19288
|
-
log10("warn", "[decompose] session.prompt 返回错误", { error:
|
|
19598
|
+
log10("warn", "[decompose] session.prompt 返回错误", { error: describe8(r.error) });
|
|
19289
19599
|
return { ok: false, subtasks: [], reason: "llm_unavailable" };
|
|
19290
19600
|
}
|
|
19291
19601
|
const rawText = pickLastText2(r.data.parts ?? []);
|
|
@@ -19295,7 +19605,7 @@ async function decomposeTask(description29, opts) {
|
|
|
19295
19605
|
}
|
|
19296
19606
|
const parsed = extractJson(rawText);
|
|
19297
19607
|
if (!parsed) {
|
|
19298
|
-
log10("warn", "[decompose] JSON 解析失败", { raw:
|
|
19608
|
+
log10("warn", "[decompose] JSON 解析失败", { raw: clip6(rawText, 200) });
|
|
19299
19609
|
return { ok: false, subtasks: [], reason: "parse_failed", raw: rawText };
|
|
19300
19610
|
}
|
|
19301
19611
|
if (parsed && typeof parsed === "object" && parsed.single_task === true) {
|
|
@@ -19324,7 +19634,7 @@ async function decomposeTask(description29, opts) {
|
|
|
19324
19634
|
}
|
|
19325
19635
|
return validateAndFinalize(normalized, rawText, log10, maxSubtasks);
|
|
19326
19636
|
} catch (err) {
|
|
19327
|
-
log10("warn", "[decompose] 抛错", { error:
|
|
19637
|
+
log10("warn", "[decompose] 抛错", { error: describe8(err) });
|
|
19328
19638
|
return { ok: false, subtasks: [], reason: "llm_unavailable" };
|
|
19329
19639
|
} finally {
|
|
19330
19640
|
if (childSessionId) {
|
|
@@ -19334,7 +19644,7 @@ async function decomposeTask(description29, opts) {
|
|
|
19334
19644
|
query: opts.directory ? { directory: opts.directory } : undefined
|
|
19335
19645
|
});
|
|
19336
19646
|
} catch (err) {
|
|
19337
|
-
log10("warn", "[decompose] session.delete 失败", { error:
|
|
19647
|
+
log10("warn", "[decompose] session.delete 失败", { error: describe8(err) });
|
|
19338
19648
|
}
|
|
19339
19649
|
}
|
|
19340
19650
|
}
|
|
@@ -19428,12 +19738,12 @@ function tryParse(s) {
|
|
|
19428
19738
|
return null;
|
|
19429
19739
|
}
|
|
19430
19740
|
}
|
|
19431
|
-
function
|
|
19741
|
+
function clip6(s, n) {
|
|
19432
19742
|
if (!s)
|
|
19433
19743
|
return "";
|
|
19434
19744
|
return s.length <= n ? s : s.slice(0, n - 1) + "…";
|
|
19435
19745
|
}
|
|
19436
|
-
function
|
|
19746
|
+
function describe8(err) {
|
|
19437
19747
|
if (!err)
|
|
19438
19748
|
return "";
|
|
19439
19749
|
if (err instanceof Error)
|
|
@@ -20019,7 +20329,7 @@ function parseTerminalOutput(ev, cfg = {}, rules = DEFAULT_RULES) {
|
|
|
20019
20329
|
`).length;
|
|
20020
20330
|
const lineFromStart = text.split(`
|
|
20021
20331
|
`)[lineIdx - 1] ?? m[0];
|
|
20022
|
-
const excerpt =
|
|
20332
|
+
const excerpt = clip7(lineFromStart.trim(), c.maxExcerpt);
|
|
20023
20333
|
if (!out.has(rule.kind)) {
|
|
20024
20334
|
out.set(rule.kind, {
|
|
20025
20335
|
severity: rule.severity,
|
|
@@ -20041,7 +20351,7 @@ function parseTerminalOutput(ev, cfg = {}, rules = DEFAULT_RULES) {
|
|
|
20041
20351
|
}
|
|
20042
20352
|
return [...out.values()].sort((a, b) => b.score - a.score);
|
|
20043
20353
|
}
|
|
20044
|
-
function
|
|
20354
|
+
function clip7(s, max) {
|
|
20045
20355
|
if (s.length <= max)
|
|
20046
20356
|
return s;
|
|
20047
20357
|
return s.slice(0, max - 1) + "…";
|
|
@@ -20100,7 +20410,7 @@ function shouldNotify(findings, ev, lru, cfg = {}, now = Date.now()) {
|
|
|
20100
20410
|
function buildSummary(ev, findings) {
|
|
20101
20411
|
const parts = [];
|
|
20102
20412
|
if (ev.cmd)
|
|
20103
|
-
parts.push(`\`${
|
|
20413
|
+
parts.push(`\`${clip7(ev.cmd, 60)}\``);
|
|
20104
20414
|
if (ev.type === "terminal.exit" && typeof ev.exit_code === "number") {
|
|
20105
20415
|
parts.push(`exit=${ev.exit_code}`);
|
|
20106
20416
|
}
|
|
@@ -20112,7 +20422,7 @@ function buildSummary(ev, findings) {
|
|
|
20112
20422
|
return parts.join(" · ");
|
|
20113
20423
|
}
|
|
20114
20424
|
const top = findings[0];
|
|
20115
|
-
parts.push(`${top.severity}/${top.kind}: ${
|
|
20425
|
+
parts.push(`${top.severity}/${top.kind}: ${clip7(top.excerpt, 80)}`);
|
|
20116
20426
|
if (findings.length > 1)
|
|
20117
20427
|
parts.push(`(+${findings.length - 1} 条)`);
|
|
20118
20428
|
return parts.join(" · ");
|
|
@@ -20596,7 +20906,7 @@ import * as zlib from "node:zlib";
|
|
|
20596
20906
|
// lib/version-injected.ts
|
|
20597
20907
|
function getInjectedVersion() {
|
|
20598
20908
|
try {
|
|
20599
|
-
const v = "0.5.
|
|
20909
|
+
const v = "0.5.2";
|
|
20600
20910
|
if (typeof v === "string" && /^\d+\.\d+\.\d+/.test(v)) {
|
|
20601
20911
|
return v;
|
|
20602
20912
|
}
|
|
@@ -21873,9 +22183,19 @@ var workflowEngineServer = async (ctx) => {
|
|
|
21873
22183
|
var handler23 = workflowEngineServer;
|
|
21874
22184
|
|
|
21875
22185
|
// plugins/session-worktree-guard.ts
|
|
22186
|
+
import path24 from "node:path";
|
|
21876
22187
|
var PLUGIN_NAME24 = "session-worktree-guard";
|
|
21877
22188
|
logLifecycle(PLUGIN_NAME24, "import", {});
|
|
21878
22189
|
var WRITE_INTENT_RE = />(?!=)|\btee\b|\brm\b|\bmv\b|\bcp\b|\bmkdir\b|\btouch\b|\bchmod\b|\bchown\b|\bln\b/;
|
|
22190
|
+
var READ_ONLY_COMMANDS = /^\s*(?:ls|cat|head|tail|grep|rg|find|fd|wc|stat|file|which|whereis|echo|pwd|cd|pushd|popd|env|printenv|type|less|more|sort|uniq|awk|tr|cut|jq|date|whoami|id|uname|git(?:\s+-C\s+\S+)?\s+(?:log|show|diff|status|branch|tag|remote|config\s+--get|rev-parse|rev-list|ls-files|ls-tree|cat-file|describe|reflog|blame|shortlog|name-rev|symbolic-ref|merge-base|worktree\s+list|stash\s+list|stash\s+show))\b/;
|
|
22191
|
+
var SIDE_EFFECT_TOKEN_RE = />(?!=)|\|\s*tee\b|\btee\b/;
|
|
22192
|
+
function isReadOnlyBashCommand(command) {
|
|
22193
|
+
if (!READ_ONLY_COMMANDS.test(command))
|
|
22194
|
+
return false;
|
|
22195
|
+
if (SIDE_EFFECT_TOKEN_RE.test(command))
|
|
22196
|
+
return false;
|
|
22197
|
+
return true;
|
|
22198
|
+
}
|
|
21879
22199
|
var INTERPRETER_WRITE_RES = [
|
|
21880
22200
|
/python.*open\s*\([^)]*['"]w['"]/,
|
|
21881
22201
|
/node.*writeFile/,
|
|
@@ -21890,12 +22210,15 @@ function buildGitVcsWriteRegex(mainRoot) {
|
|
|
21890
22210
|
}
|
|
21891
22211
|
var WRITE_TOOLS = new Set(["write", "edit", "ast_edit"]);
|
|
21892
22212
|
function rewritePath(value, mainRoot, worktreeRoot) {
|
|
21893
|
-
if (value
|
|
22213
|
+
if (!value)
|
|
22214
|
+
return null;
|
|
22215
|
+
const resolved = path24.isAbsolute(value) ? value : path24.resolve(mainRoot, value);
|
|
22216
|
+
if (resolved === mainRoot)
|
|
21894
22217
|
return worktreeRoot;
|
|
21895
22218
|
const prefix = mainRoot.endsWith("/") ? mainRoot : mainRoot + "/";
|
|
21896
|
-
if (
|
|
22219
|
+
if (resolved.startsWith(prefix)) {
|
|
21897
22220
|
const wtPrefix = worktreeRoot.endsWith("/") ? worktreeRoot : worktreeRoot + "/";
|
|
21898
|
-
return wtPrefix +
|
|
22221
|
+
return wtPrefix + resolved.slice(prefix.length);
|
|
21899
22222
|
}
|
|
21900
22223
|
return null;
|
|
21901
22224
|
}
|
|
@@ -21909,6 +22232,8 @@ function commandContainsMainRoot(command, mainRoot) {
|
|
|
21909
22232
|
return re.test(command);
|
|
21910
22233
|
}
|
|
21911
22234
|
function detectBashWriteIntent(command, mainRoot) {
|
|
22235
|
+
if (isReadOnlyBashCommand(command))
|
|
22236
|
+
return false;
|
|
21912
22237
|
if (WRITE_INTENT_RE.test(command))
|
|
21913
22238
|
return true;
|
|
21914
22239
|
for (const re of INTERPRETER_WRITE_RES) {
|
|
@@ -21927,6 +22252,8 @@ function isWriteOperation(toolName, argsObj, mainRoot) {
|
|
|
21927
22252
|
const command = argsObj["command"];
|
|
21928
22253
|
if (typeof command !== "string")
|
|
21929
22254
|
return false;
|
|
22255
|
+
if (isReadOnlyBashCommand(command))
|
|
22256
|
+
return false;
|
|
21930
22257
|
if (WRITE_INTENT_RE.test(command))
|
|
21931
22258
|
return true;
|
|
21932
22259
|
for (const re of INTERPRETER_WRITE_RES) {
|
|
@@ -21938,16 +22265,16 @@ function isWriteOperation(toolName, argsObj, mainRoot) {
|
|
|
21938
22265
|
return false;
|
|
21939
22266
|
}
|
|
21940
22267
|
var log13 = makePluginLogger(PLUGIN_NAME24);
|
|
21941
|
-
var sessionWorktreeGuardPlugin = async (
|
|
22268
|
+
var sessionWorktreeGuardPlugin = async (ctx) => {
|
|
22269
|
+
const mainRoot = ctx.directory ?? process.cwd();
|
|
21942
22270
|
logLifecycle(PLUGIN_NAME24, "activate", {
|
|
21943
|
-
|
|
21944
|
-
|
|
22271
|
+
mainRoot,
|
|
22272
|
+
CODEFORGE_SESSION_ID: process.env["CODEFORGE_SESSION_ID"] ?? "(not set)"
|
|
21945
22273
|
});
|
|
21946
22274
|
return {
|
|
21947
22275
|
"tool.execute.before": async (input, output) => {
|
|
21948
22276
|
const sessionId = input.sessionID ?? process.env["CODEFORGE_SESSION_ID"];
|
|
21949
|
-
|
|
21950
|
-
if (!sessionId || !mainRoot)
|
|
22277
|
+
if (!sessionId)
|
|
21951
22278
|
return;
|
|
21952
22279
|
let denied;
|
|
21953
22280
|
await safeAsync(PLUGIN_NAME24, "tool.execute.before", async () => {
|
|
@@ -22129,7 +22456,7 @@ var IDLE_TOAST_REMINDER_INTERVAL_MS = 30 * 60000;
|
|
|
22129
22456
|
var lastIdleToastAt = new Map;
|
|
22130
22457
|
var log14 = makePluginLogger(PLUGIN_NAME25);
|
|
22131
22458
|
var worktreeLifecyclePlugin = async (ctx) => {
|
|
22132
|
-
const mainRoot =
|
|
22459
|
+
const mainRoot = ctx.directory;
|
|
22133
22460
|
logLifecycle(PLUGIN_NAME25, "activate", {
|
|
22134
22461
|
mainRoot: mainRoot ?? "(not set)",
|
|
22135
22462
|
idle_threshold_ms: IDLE_TOAST_THROTTLE_MS
|