@andyqiu/codeforge 0.5.1 → 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/discover-challenger.md +2 -2
- package/agents/discover.md +7 -6
- package/bin/codeforge.mjs +49 -0
- package/commands/{diff.md → changes.md} +10 -10
- 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 +732 -469
- 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) {
|
|
@@ -13793,18 +13795,36 @@ var ArgsSchema26 = z27.discriminatedUnion("action", [
|
|
|
13793
13795
|
})
|
|
13794
13796
|
]);
|
|
13795
13797
|
var _ctx = {};
|
|
13798
|
+
function __setContext(ctx) {
|
|
13799
|
+
_ctx = ctx;
|
|
13800
|
+
}
|
|
13796
13801
|
function getMainRoot() {
|
|
13797
13802
|
return _ctx.mainRoot ?? process.cwd();
|
|
13798
13803
|
}
|
|
13799
|
-
async function resolveSessionId(explicit) {
|
|
13804
|
+
async function resolveSessionId(explicit, strictWorktreeCheck = false) {
|
|
13800
13805
|
if (explicit)
|
|
13801
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
|
+
}
|
|
13802
13813
|
if (_ctx.resolveCurrentSessionId) {
|
|
13803
13814
|
const sid = await _ctx.resolveCurrentSessionId();
|
|
13804
|
-
if (sid)
|
|
13815
|
+
if (sid && (!strictWorktreeCheck || await isSessionIdValid(sid, mainRoot))) {
|
|
13805
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;
|
|
13806
13827
|
}
|
|
13807
|
-
throw new Error("session_merge: 未指定 session_id 且无法反推当前 session(context resolver 缺失)");
|
|
13808
13828
|
}
|
|
13809
13829
|
async function execute26(input) {
|
|
13810
13830
|
const parsed = ArgsSchema26.safeParse(input);
|
|
@@ -13896,20 +13916,27 @@ async function execute26(input) {
|
|
|
13896
13916
|
}
|
|
13897
13917
|
};
|
|
13898
13918
|
}
|
|
13919
|
+
const mergeSessionId = await resolveSessionId(args.session_id, true);
|
|
13899
13920
|
if (!_ctx.spawner) {
|
|
13900
13921
|
return {
|
|
13901
13922
|
ok: false,
|
|
13902
13923
|
action: "merge",
|
|
13903
|
-
error: "session_merge: SubagentSpawner 未注入(
|
|
13924
|
+
error: "session_merge: SubagentSpawner 未注入(plugin activate 失败?" + "应由 plugins/codeforge-tools.ts 的 ProductionSpawner 注入 — ADR:spawner-production-wire-up)"
|
|
13904
13925
|
};
|
|
13905
13926
|
}
|
|
13906
13927
|
const mergeArgs = args;
|
|
13928
|
+
const sendProgress = _ctx.sendProgress;
|
|
13907
13929
|
const result = await runMergeLoop({
|
|
13908
|
-
sessionId,
|
|
13930
|
+
sessionId: mergeSessionId,
|
|
13909
13931
|
mainRoot,
|
|
13910
13932
|
...mergeArgs.plan_id ? { planId: mergeArgs.plan_id } : {},
|
|
13911
13933
|
...mergeArgs.force ? { force: true } : {},
|
|
13912
|
-
spawner: _ctx.spawner
|
|
13934
|
+
spawner: _ctx.spawner,
|
|
13935
|
+
...sendProgress ? {
|
|
13936
|
+
onProgress: (state, detail) => {
|
|
13937
|
+
Promise.resolve(sendProgress(state, detail)).catch(() => {});
|
|
13938
|
+
}
|
|
13939
|
+
} : {}
|
|
13913
13940
|
});
|
|
13914
13941
|
return { ok: true, action: "merge", data: result };
|
|
13915
13942
|
} catch (err) {
|
|
@@ -14248,7 +14275,7 @@ var ArgsSchema28 = z29.object({
|
|
|
14248
14275
|
message: "plan_id 与 path 至少传一个"
|
|
14249
14276
|
});
|
|
14250
14277
|
var _ctx2 = {};
|
|
14251
|
-
function
|
|
14278
|
+
function __setContext2(ctx) {
|
|
14252
14279
|
_ctx2 = ctx;
|
|
14253
14280
|
}
|
|
14254
14281
|
function getStore2() {
|
|
@@ -14354,109 +14381,562 @@ async function execute28(input) {
|
|
|
14354
14381
|
};
|
|
14355
14382
|
}
|
|
14356
14383
|
}
|
|
14357
|
-
// lib/
|
|
14358
|
-
|
|
14359
|
-
|
|
14360
|
-
|
|
14361
|
-
|
|
14362
|
-
|
|
14363
|
-
|
|
14364
|
-
|
|
14365
|
-
|
|
14366
|
-
|
|
14367
|
-
|
|
14368
|
-
|
|
14369
|
-
|
|
14370
|
-
|
|
14371
|
-
|
|
14372
|
-
|
|
14373
|
-
|
|
14374
|
-
|
|
14375
|
-
|
|
14376
|
-
|
|
14377
|
-
|
|
14378
|
-
interval_hours: 24,
|
|
14379
|
-
repo: "andy-personal/code-forge",
|
|
14380
|
-
channel: "latest",
|
|
14381
|
-
package: "@andyqiu/codeforge",
|
|
14382
|
-
registry: "https://registry.npmjs.org",
|
|
14383
|
-
auto_install: true,
|
|
14384
|
-
backup_keep: 3
|
|
14385
|
-
}
|
|
14386
|
-
};
|
|
14387
|
-
function loadRuntimeSync(opts = {}) {
|
|
14388
|
-
const hit = findConfigFileSync(opts);
|
|
14389
|
-
if (!hit) {
|
|
14390
|
-
return emptyResult({ reason: `${CONFIG_FILE} not found (using built-in defaults)` });
|
|
14391
|
-
}
|
|
14392
|
-
const fsSync = __require("node:fs");
|
|
14393
|
-
let raw;
|
|
14394
|
-
try {
|
|
14395
|
-
raw = fsSync.readFileSync(hit, "utf8");
|
|
14396
|
-
} catch (e) {
|
|
14397
|
-
return emptyResult({ path: hit, reason: `read_failed: ${e.message}` });
|
|
14398
|
-
}
|
|
14399
|
-
return parseRuntime(raw, hit);
|
|
14400
|
-
}
|
|
14401
|
-
async function loadRuntime(opts = {}) {
|
|
14402
|
-
const root = opts.root ?? process.cwd();
|
|
14403
|
-
const abs = path16.resolve(root, opts.file ?? CONFIG_FILE);
|
|
14404
|
-
let raw;
|
|
14405
|
-
try {
|
|
14406
|
-
raw = await fs13.readFile(abs, "utf8");
|
|
14407
|
-
} catch (e) {
|
|
14408
|
-
const code = e.code;
|
|
14409
|
-
if (code === "ENOENT") {
|
|
14410
|
-
return emptyResult({ reason: `${CONFIG_FILE} not found at ${abs} (using built-in defaults)` });
|
|
14411
|
-
}
|
|
14412
|
-
return emptyResult({ path: abs, reason: `read_failed: ${e.message}` });
|
|
14413
|
-
}
|
|
14414
|
-
return parseRuntime(raw, abs);
|
|
14415
|
-
}
|
|
14416
|
-
function emptyResult(opts) {
|
|
14417
|
-
return {
|
|
14418
|
-
ok: false,
|
|
14419
|
-
path: opts.path ?? null,
|
|
14420
|
-
runtime: cloneDefaults(),
|
|
14421
|
-
warnings: [],
|
|
14422
|
-
error: opts.reason
|
|
14423
|
-
};
|
|
14424
|
-
}
|
|
14425
|
-
function cloneDefaults() {
|
|
14426
|
-
return JSON.parse(JSON.stringify(DEFAULT_RUNTIME));
|
|
14427
|
-
}
|
|
14428
|
-
var VALID_CHANNELS = ["stable", "prerelease", "latest", "next", "rc"];
|
|
14429
|
-
function parseRuntime(raw, abs) {
|
|
14430
|
-
let root = null;
|
|
14431
|
-
try {
|
|
14432
|
-
const parsed = JSON.parse(raw);
|
|
14433
|
-
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
14434
|
-
root = parsed;
|
|
14435
|
-
}
|
|
14436
|
-
} catch (e) {
|
|
14437
|
-
return emptyResult({ path: abs, reason: `invalid_json: ${e.message}` });
|
|
14438
|
-
}
|
|
14439
|
-
if (!root) {
|
|
14440
|
-
return emptyResult({ path: abs, reason: "config_root_must_be_object" });
|
|
14441
|
-
}
|
|
14442
|
-
const warnings = [];
|
|
14443
|
-
const cfg = cloneDefaults();
|
|
14444
|
-
if (root.autonomy && typeof root.autonomy === "object" && !Array.isArray(root.autonomy)) {
|
|
14445
|
-
const a = root.autonomy;
|
|
14446
|
-
if (a.default_mode !== undefined) {
|
|
14447
|
-
if (a.default_mode === "step" || a.default_mode === "semi" || a.default_mode === "full") {
|
|
14448
|
-
cfg.autonomy.default_mode = a.default_mode;
|
|
14449
|
-
} else {
|
|
14450
|
-
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
|
+
};
|
|
14451
14405
|
}
|
|
14452
|
-
|
|
14453
|
-
|
|
14454
|
-
|
|
14455
|
-
|
|
14456
|
-
|
|
14457
|
-
|
|
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
|
+
};
|
|
14458
14425
|
}
|
|
14459
|
-
|
|
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") {
|
|
14935
|
+
cfg.autonomy.downgrade_on_risky = a.downgrade_on_risky;
|
|
14936
|
+
} else {
|
|
14937
|
+
warnings.push(`autonomy.downgrade_on_risky must be boolean (kept default)`);
|
|
14938
|
+
}
|
|
14939
|
+
}
|
|
14460
14940
|
if (typeof a._doc === "string")
|
|
14461
14941
|
cfg.autonomy._doc = a._doc;
|
|
14462
14942
|
}
|
|
@@ -15107,10 +15587,20 @@ var codeforgeToolsServer = async (ctx) => {
|
|
|
15107
15587
|
browser_enabled: browserEnabled,
|
|
15108
15588
|
config_source: rt.ok ? "codeforge.json" : "built-in"
|
|
15109
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
|
+
});
|
|
15110
15600
|
__setContext({
|
|
15111
|
-
|
|
15112
|
-
|
|
15113
|
-
|
|
15601
|
+
mainRoot: ctx.directory ?? process.cwd(),
|
|
15602
|
+
spawner,
|
|
15603
|
+
resolveCurrentSessionId: () => process.env["CODEFORGE_SESSION_ID"] ?? ""
|
|
15114
15604
|
});
|
|
15115
15605
|
const browserTools = browserEnabled ? buildBrowserTools() : {};
|
|
15116
15606
|
const khWriteTools = buildKhWriteTools();
|
|
@@ -15183,8 +15673,8 @@ var codeforgeToolsServer = async (ctx) => {
|
|
|
15183
15673
|
]).describe("编辑类型;不同 action 需要的字段不同"),
|
|
15184
15674
|
target: z30.string().min(1).describe("目标文件路径(相对 cwd 或绝对)"),
|
|
15185
15675
|
before_hash: z30.string().optional().describe("操作前的 sha256 hex(强烈建议传,新文件传 null/省略)"),
|
|
15186
|
-
auto_stage: z30.boolean().optional().describe("
|
|
15187
|
-
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)"),
|
|
15188
15678
|
anchor: z30.string().optional().describe("anchor 类用:anchor 文本或 regex 源"),
|
|
15189
15679
|
regex: z30.boolean().optional().describe("anchor 是否按 RegExp 解释,默认 false"),
|
|
15190
15680
|
occurrence: z30.number().int().min(1).optional().describe("第几次匹配(1-based),默认 1"),
|
|
@@ -15318,11 +15808,23 @@ var codeforgeToolsServer = async (ctx) => {
|
|
|
15318
15808
|
plan_id: z30.string().optional().describe("关联的 plan_id(reviewer 校验时用),格式 plan-YYYYMMDD-HHmmss-NNN"),
|
|
15319
15809
|
force: z30.boolean().optional().describe("action=merge 时跳过 review 直接 squash merge(写审计)")
|
|
15320
15810
|
},
|
|
15321
|
-
async execute(args) {
|
|
15811
|
+
async execute(args, input) {
|
|
15322
15812
|
return await runSafe("session_merge", async () => {
|
|
15323
15813
|
const v = projectValidate("session_merge", ArgsSchema26, args);
|
|
15324
15814
|
if (!v.ok)
|
|
15325
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
|
+
});
|
|
15326
15828
|
const result = await execute26(v.data);
|
|
15327
15829
|
const meta = {
|
|
15328
15830
|
title: `session_merge: ${args.action}`,
|
|
@@ -16105,7 +16607,7 @@ function formatInjection(query, insights, mode = INJECTION_MODE) {
|
|
|
16105
16607
|
}
|
|
16106
16608
|
var CODEFORGE_CONSTRAINTS = [
|
|
16107
16609
|
{
|
|
16108
|
-
content: "本会话使用 CodeForge:触发词命中(部署/怎么/为什么/之前/历史等)必须先 smart_search
|
|
16610
|
+
content: "本会话使用 CodeForge:触发词命中(部署/怎么/为什么/之前/历史等)必须先 smart_search;写代码直接在 session worktree 内 edit/write/ast_edit(worktree 由 session-worktree-guard 隔离主仓),完成后由用户 /merge 拍板",
|
|
16109
16611
|
priority: 9
|
|
16110
16612
|
},
|
|
16111
16613
|
{
|
|
@@ -17904,7 +18406,7 @@ ${toolsBulleted}
|
|
|
17904
18406
|
Heuristics:
|
|
17905
18407
|
- Need to read 3 files? → emit 3 \`read\` calls in ONE response, not 3 sequential turns
|
|
17906
18408
|
- Need search + map? → emit \`smart_search\` + \`repo_map\` in ONE response
|
|
17907
|
-
- DO NOT batch write tools (
|
|
18409
|
+
- DO NOT batch write tools (edit / write / ast_edit / bash)
|
|
17908
18410
|
- DO NOT chain reads where each read's path depends on previous read's output
|
|
17909
18411
|
|
|
17910
18412
|
This directive is re-injected every turn — it is not optional advice.
|
|
@@ -18169,7 +18671,7 @@ function buildRecoveryPlan(events, meta = { id: null }, opts = {}) {
|
|
|
18169
18671
|
for (let i = events.length - 1;i >= 0; i--) {
|
|
18170
18672
|
const e = events[i];
|
|
18171
18673
|
if (e.type === "message" && e.role === "user") {
|
|
18172
|
-
lastUser =
|
|
18674
|
+
lastUser = clip5(e.content, o.excerptLimit);
|
|
18173
18675
|
break;
|
|
18174
18676
|
}
|
|
18175
18677
|
}
|
|
@@ -18187,7 +18689,7 @@ function buildRecoveryPlan(events, meta = { id: null }, opts = {}) {
|
|
|
18187
18689
|
const toolMap = new Map;
|
|
18188
18690
|
for (const t of window) {
|
|
18189
18691
|
const cur = toolMap.get(t.tool);
|
|
18190
|
-
const argsExcerpt =
|
|
18692
|
+
const argsExcerpt = clip5(safeJson(t.args), o.excerptLimit);
|
|
18191
18693
|
if (cur) {
|
|
18192
18694
|
cur.count++;
|
|
18193
18695
|
cur.last_ok = t.ok;
|
|
@@ -18276,7 +18778,7 @@ function formatIdle(ms) {
|
|
|
18276
18778
|
return `${Math.round(ms / 3600000)}h`;
|
|
18277
18779
|
return `${Math.round(ms / 86400000)}d`;
|
|
18278
18780
|
}
|
|
18279
|
-
function
|
|
18781
|
+
function clip5(s, max) {
|
|
18280
18782
|
if (!s)
|
|
18281
18783
|
return "";
|
|
18282
18784
|
if (s.length <= max)
|
|
@@ -18506,7 +19008,7 @@ async function mergeOneAttempt(r, opts, mergeFns, log10) {
|
|
|
18506
19008
|
});
|
|
18507
19009
|
} catch (commitErr) {
|
|
18508
19010
|
attempt.ok = false;
|
|
18509
|
-
attempt.message = `commitWorktreeIfDirty 失败:${
|
|
19011
|
+
attempt.message = `commitWorktreeIfDirty 失败:${describe6(commitErr)}`;
|
|
18510
19012
|
attempt.durationMs = Date.now() - t0;
|
|
18511
19013
|
return {
|
|
18512
19014
|
attempt,
|
|
@@ -18535,11 +19037,11 @@ async function mergeOneAttempt(r, opts, mergeFns, log10) {
|
|
|
18535
19037
|
abortFailed = true;
|
|
18536
19038
|
log10("error", `[parallel-merge] mergeAbort 失败(仓库可能锁死)`, {
|
|
18537
19039
|
id: r.id,
|
|
18538
|
-
error:
|
|
19040
|
+
error: describe6(abortErr)
|
|
18539
19041
|
});
|
|
18540
19042
|
}
|
|
18541
19043
|
attempt.ok = false;
|
|
18542
|
-
attempt.message = `mergeCommit 失败:${
|
|
19044
|
+
attempt.message = `mergeCommit 失败:${describe6(commitErr)}`;
|
|
18543
19045
|
attempt.durationMs = Date.now() - t0;
|
|
18544
19046
|
return {
|
|
18545
19047
|
attempt,
|
|
@@ -18560,7 +19062,7 @@ async function mergeOneAttempt(r, opts, mergeFns, log10) {
|
|
|
18560
19062
|
}
|
|
18561
19063
|
} catch (err) {
|
|
18562
19064
|
attempt.ok = false;
|
|
18563
|
-
attempt.message = `mergeWorktrees 异常:${
|
|
19065
|
+
attempt.message = `mergeWorktrees 异常:${describe6(err)}`;
|
|
18564
19066
|
attempt.durationMs = Date.now() - t0;
|
|
18565
19067
|
return {
|
|
18566
19068
|
attempt,
|
|
@@ -18703,7 +19205,7 @@ async function safeRemoveWorktree(fn, root, wt, log10, subtaskId) {
|
|
|
18703
19205
|
await fn({ root, worktree_path: wt, force: true });
|
|
18704
19206
|
} catch (err) {
|
|
18705
19207
|
log10("warn", `[parallel] removeWorktree 失败 ${subtaskId}`, {
|
|
18706
|
-
error:
|
|
19208
|
+
error: describe6(err),
|
|
18707
19209
|
worktree: wt
|
|
18708
19210
|
});
|
|
18709
19211
|
}
|
|
@@ -18716,11 +19218,11 @@ async function fireMergeAttempt(cb, attempt, idx, log10) {
|
|
|
18716
19218
|
} catch (err) {
|
|
18717
19219
|
log10("warn", `[parallel] onMergeAttempt 抛错(已隔离)`, {
|
|
18718
19220
|
id: attempt.subtaskId,
|
|
18719
|
-
error:
|
|
19221
|
+
error: describe6(err)
|
|
18720
19222
|
});
|
|
18721
19223
|
}
|
|
18722
19224
|
}
|
|
18723
|
-
function
|
|
19225
|
+
function describe6(err) {
|
|
18724
19226
|
if (err instanceof Error)
|
|
18725
19227
|
return err.message;
|
|
18726
19228
|
if (typeof err === "string")
|
|
@@ -18783,7 +19285,7 @@ async function schedule(opts) {
|
|
|
18783
19285
|
} catch (err) {
|
|
18784
19286
|
log10("warn", `[parallel] queue.enqueue 抛错(已隔离)`, {
|
|
18785
19287
|
id: res.id,
|
|
18786
|
-
error:
|
|
19288
|
+
error: describe7(err)
|
|
18787
19289
|
});
|
|
18788
19290
|
}
|
|
18789
19291
|
}
|
|
@@ -18794,7 +19296,7 @@ async function schedule(opts) {
|
|
|
18794
19296
|
} catch (err) {
|
|
18795
19297
|
log10("warn", `[parallel] onSubtaskFinish 抛错(已隔离)`, {
|
|
18796
19298
|
id: res.id,
|
|
18797
|
-
error:
|
|
19299
|
+
error: describe7(err)
|
|
18798
19300
|
});
|
|
18799
19301
|
}
|
|
18800
19302
|
};
|
|
@@ -18808,10 +19310,10 @@ async function schedule(opts) {
|
|
|
18808
19310
|
const res2 = {
|
|
18809
19311
|
id: spec.id,
|
|
18810
19312
|
ok: false,
|
|
18811
|
-
summary: clamp(`worktree 分配失败:${
|
|
19313
|
+
summary: clamp(`worktree 分配失败:${describe7(err)}`, limit),
|
|
18812
19314
|
status: "failed",
|
|
18813
19315
|
duration_ms: now() - subStart,
|
|
18814
|
-
error:
|
|
19316
|
+
error: describe7(err)
|
|
18815
19317
|
};
|
|
18816
19318
|
results[i] = res2;
|
|
18817
19319
|
await fireFinish(i, res2);
|
|
@@ -18824,7 +19326,7 @@ async function schedule(opts) {
|
|
|
18824
19326
|
} catch (err) {
|
|
18825
19327
|
log10("warn", `[parallel] onSubtaskStart 抛错(已隔离)`, {
|
|
18826
19328
|
id: spec.id,
|
|
18827
|
-
error:
|
|
19329
|
+
error: describe7(err)
|
|
18828
19330
|
});
|
|
18829
19331
|
}
|
|
18830
19332
|
}
|
|
@@ -18860,11 +19362,11 @@ async function schedule(opts) {
|
|
|
18860
19362
|
res = {
|
|
18861
19363
|
id: spec.id,
|
|
18862
19364
|
ok: false,
|
|
18863
|
-
summary: clamp(`runSubtask 抛错:${
|
|
19365
|
+
summary: clamp(`runSubtask 抛错:${describe7(err)}`, limit),
|
|
18864
19366
|
status: isAborted ? globalCtl.signal.aborted ? "cancelled" : "timeout" : "failed",
|
|
18865
19367
|
duration_ms: now() - subStart,
|
|
18866
19368
|
worktree: alloc?.path,
|
|
18867
|
-
error:
|
|
19369
|
+
error: describe7(err)
|
|
18868
19370
|
};
|
|
18869
19371
|
} finally {
|
|
18870
19372
|
clearTimeout(perTimer);
|
|
@@ -18874,7 +19376,7 @@ async function schedule(opts) {
|
|
|
18874
19376
|
await alloc.cleanup();
|
|
18875
19377
|
} catch (err) {
|
|
18876
19378
|
log10("warn", `[parallel] worktree 清理失败 ${spec.id}`, {
|
|
18877
|
-
error:
|
|
19379
|
+
error: describe7(err)
|
|
18878
19380
|
});
|
|
18879
19381
|
}
|
|
18880
19382
|
}
|
|
@@ -18998,7 +19500,7 @@ function buildDigest(results, conflicts) {
|
|
|
18998
19500
|
conflicts
|
|
18999
19501
|
};
|
|
19000
19502
|
}
|
|
19001
|
-
function
|
|
19503
|
+
function describe7(err) {
|
|
19002
19504
|
if (err instanceof Error)
|
|
19003
19505
|
return err.message;
|
|
19004
19506
|
if (typeof err === "string")
|
|
@@ -19030,262 +19532,6 @@ function pickStatus(r, taskAborted, globalAborted) {
|
|
|
19030
19532
|
return "failed";
|
|
19031
19533
|
}
|
|
19032
19534
|
|
|
19033
|
-
// lib/opencode-runner.ts
|
|
19034
|
-
function makeOpencodeRunner(opts) {
|
|
19035
|
-
const log10 = opts.log ?? (() => {});
|
|
19036
|
-
return async (spec, runCtx) => {
|
|
19037
|
-
const startedAt = Date.now();
|
|
19038
|
-
let childSessionId;
|
|
19039
|
-
try {
|
|
19040
|
-
const created = await opts.client.session.create({
|
|
19041
|
-
body: {
|
|
19042
|
-
parentID: opts.parentSessionID,
|
|
19043
|
-
title: clip4(`subtask:${spec.id}`, 80)
|
|
19044
|
-
},
|
|
19045
|
-
query: opts.directory ? { directory: opts.directory } : undefined
|
|
19046
|
-
});
|
|
19047
|
-
if (created.error || !created.data?.id) {
|
|
19048
|
-
return {
|
|
19049
|
-
ok: false,
|
|
19050
|
-
summary: `session.create 失败:${describe6(created.error) || "no id"}`,
|
|
19051
|
-
status: "failed",
|
|
19052
|
-
error: describe6(created.error) || "session.create 无 id"
|
|
19053
|
-
};
|
|
19054
|
-
}
|
|
19055
|
-
childSessionId = created.data.id;
|
|
19056
|
-
log10("info", `[opencode-runner] subtask=${spec.id} session=${childSessionId} created`);
|
|
19057
|
-
const promptText = composePromptText(spec);
|
|
19058
|
-
const promptPromise = opts.client.session.prompt({
|
|
19059
|
-
path: { id: childSessionId },
|
|
19060
|
-
body: {
|
|
19061
|
-
agent: spec.agent ?? opts.defaultAgent,
|
|
19062
|
-
parts: [{ type: "text", text: promptText }]
|
|
19063
|
-
},
|
|
19064
|
-
query: opts.directory ? { directory: opts.directory } : undefined
|
|
19065
|
-
});
|
|
19066
|
-
const promptRes = await withTimeout3(Promise.resolve(promptPromise), opts.perTaskTimeoutMs, runCtx.signal);
|
|
19067
|
-
if (promptRes.kind === "timeout") {
|
|
19068
|
-
return {
|
|
19069
|
-
ok: false,
|
|
19070
|
-
summary: `subtask 超时(${opts.perTaskTimeoutMs ?? 0}ms)`,
|
|
19071
|
-
status: "timeout",
|
|
19072
|
-
error: "perTaskTimeoutMs 触发"
|
|
19073
|
-
};
|
|
19074
|
-
}
|
|
19075
|
-
if (promptRes.kind === "aborted") {
|
|
19076
|
-
return {
|
|
19077
|
-
ok: false,
|
|
19078
|
-
summary: "subtask 已被父任务取消",
|
|
19079
|
-
status: "cancelled"
|
|
19080
|
-
};
|
|
19081
|
-
}
|
|
19082
|
-
const r = promptRes.value;
|
|
19083
|
-
if (r.error || !r.data) {
|
|
19084
|
-
return {
|
|
19085
|
-
ok: false,
|
|
19086
|
-
summary: `session.prompt 失败:${describe6(r.error)}`,
|
|
19087
|
-
status: "failed",
|
|
19088
|
-
error: describe6(r.error) || "no data"
|
|
19089
|
-
};
|
|
19090
|
-
}
|
|
19091
|
-
const lastText = pickLastText(r.data.parts ?? []);
|
|
19092
|
-
const finishReason = (r.data.info?.finish ?? "").toLowerCase();
|
|
19093
|
-
const llmError = r.data.info?.error;
|
|
19094
|
-
let status;
|
|
19095
|
-
if (llmError)
|
|
19096
|
-
status = "failed";
|
|
19097
|
-
else if (finishReason === "length")
|
|
19098
|
-
status = "need_review";
|
|
19099
|
-
else
|
|
19100
|
-
status = "success";
|
|
19101
|
-
let changedFiles;
|
|
19102
|
-
try {
|
|
19103
|
-
const diff = await opts.client.session.diff({
|
|
19104
|
-
path: { id: childSessionId },
|
|
19105
|
-
query: opts.directory ? { directory: opts.directory } : undefined
|
|
19106
|
-
});
|
|
19107
|
-
changedFiles = pickDiffFiles(diff.data);
|
|
19108
|
-
} catch (err) {
|
|
19109
|
-
log10("warn", `[opencode-runner] diff 取失败 ${spec.id}`, { error: describe6(err) });
|
|
19110
|
-
}
|
|
19111
|
-
const elapsed = Date.now() - startedAt;
|
|
19112
|
-
const summary = lastText || `subtask ${spec.id} 完成(${elapsed}ms,无文本输出,finish=${finishReason || "unknown"})`;
|
|
19113
|
-
return {
|
|
19114
|
-
ok: status !== "failed",
|
|
19115
|
-
summary,
|
|
19116
|
-
status,
|
|
19117
|
-
changedFiles,
|
|
19118
|
-
error: llmError ? describe6(llmError) : undefined
|
|
19119
|
-
};
|
|
19120
|
-
} catch (err) {
|
|
19121
|
-
return {
|
|
19122
|
-
ok: false,
|
|
19123
|
-
summary: `runner 抛错:${describe6(err)}`,
|
|
19124
|
-
status: "failed",
|
|
19125
|
-
error: describe6(err)
|
|
19126
|
-
};
|
|
19127
|
-
} finally {
|
|
19128
|
-
if (childSessionId) {
|
|
19129
|
-
try {
|
|
19130
|
-
await opts.client.session.delete({
|
|
19131
|
-
path: { id: childSessionId },
|
|
19132
|
-
query: opts.directory ? { directory: opts.directory } : undefined
|
|
19133
|
-
});
|
|
19134
|
-
} catch (err) {
|
|
19135
|
-
log10("warn", `[opencode-runner] session.delete 失败 ${childSessionId}`, {
|
|
19136
|
-
error: describe6(err)
|
|
19137
|
-
});
|
|
19138
|
-
}
|
|
19139
|
-
}
|
|
19140
|
-
}
|
|
19141
|
-
};
|
|
19142
|
-
}
|
|
19143
|
-
function composePromptText(spec) {
|
|
19144
|
-
const lines = [spec.description.trim()];
|
|
19145
|
-
if (spec.args && Object.keys(spec.args).length > 0) {
|
|
19146
|
-
lines.push("", "<!-- subtask args -->");
|
|
19147
|
-
for (const [k, v] of Object.entries(spec.args)) {
|
|
19148
|
-
lines.push(`- ${k}: ${safeStringify(v)}`);
|
|
19149
|
-
}
|
|
19150
|
-
}
|
|
19151
|
-
return lines.join(`
|
|
19152
|
-
`);
|
|
19153
|
-
}
|
|
19154
|
-
function pickLastText(parts) {
|
|
19155
|
-
for (let i = parts.length - 1;i >= 0; i--) {
|
|
19156
|
-
const p = parts[i];
|
|
19157
|
-
if (p && p.type === "text" && typeof p.text === "string" && !p.synthetic && !p.ignored) {
|
|
19158
|
-
return p.text.trim();
|
|
19159
|
-
}
|
|
19160
|
-
}
|
|
19161
|
-
return "";
|
|
19162
|
-
}
|
|
19163
|
-
function pickDiffFiles(diffData) {
|
|
19164
|
-
if (!diffData || typeof diffData !== "object")
|
|
19165
|
-
return;
|
|
19166
|
-
const obj = diffData;
|
|
19167
|
-
if (!Array.isArray(obj.files))
|
|
19168
|
-
return;
|
|
19169
|
-
const out = [];
|
|
19170
|
-
for (const f of obj.files) {
|
|
19171
|
-
if (typeof f === "string")
|
|
19172
|
-
out.push(f);
|
|
19173
|
-
else if (f && typeof f.path === "string") {
|
|
19174
|
-
out.push(f.path);
|
|
19175
|
-
}
|
|
19176
|
-
}
|
|
19177
|
-
return out.length > 0 ? out : undefined;
|
|
19178
|
-
}
|
|
19179
|
-
async function withTimeout3(p, ms, signal) {
|
|
19180
|
-
if (signal?.aborted)
|
|
19181
|
-
return { kind: "aborted" };
|
|
19182
|
-
if (!ms || ms <= 0) {
|
|
19183
|
-
try {
|
|
19184
|
-
const value = await p;
|
|
19185
|
-
return { kind: "ok", value };
|
|
19186
|
-
} catch (err) {
|
|
19187
|
-
throw err;
|
|
19188
|
-
}
|
|
19189
|
-
}
|
|
19190
|
-
return await new Promise((resolve15, reject) => {
|
|
19191
|
-
let settled = false;
|
|
19192
|
-
const timer = setTimeout(() => {
|
|
19193
|
-
if (settled)
|
|
19194
|
-
return;
|
|
19195
|
-
settled = true;
|
|
19196
|
-
resolve15({ kind: "timeout" });
|
|
19197
|
-
}, ms);
|
|
19198
|
-
const onAbort = () => {
|
|
19199
|
-
if (settled)
|
|
19200
|
-
return;
|
|
19201
|
-
settled = true;
|
|
19202
|
-
clearTimeout(timer);
|
|
19203
|
-
resolve15({ kind: "aborted" });
|
|
19204
|
-
};
|
|
19205
|
-
signal?.addEventListener("abort", onAbort, { once: true });
|
|
19206
|
-
p.then((value) => {
|
|
19207
|
-
if (settled)
|
|
19208
|
-
return;
|
|
19209
|
-
settled = true;
|
|
19210
|
-
clearTimeout(timer);
|
|
19211
|
-
signal?.removeEventListener("abort", onAbort);
|
|
19212
|
-
resolve15({ kind: "ok", value });
|
|
19213
|
-
}, (err) => {
|
|
19214
|
-
if (settled)
|
|
19215
|
-
return;
|
|
19216
|
-
settled = true;
|
|
19217
|
-
clearTimeout(timer);
|
|
19218
|
-
signal?.removeEventListener("abort", onAbort);
|
|
19219
|
-
reject(err);
|
|
19220
|
-
});
|
|
19221
|
-
});
|
|
19222
|
-
}
|
|
19223
|
-
function describe6(err) {
|
|
19224
|
-
if (!err)
|
|
19225
|
-
return "";
|
|
19226
|
-
if (err instanceof Error)
|
|
19227
|
-
return err.message;
|
|
19228
|
-
if (typeof err === "string")
|
|
19229
|
-
return err;
|
|
19230
|
-
try {
|
|
19231
|
-
return JSON.stringify(err);
|
|
19232
|
-
} catch {
|
|
19233
|
-
return String(err);
|
|
19234
|
-
}
|
|
19235
|
-
}
|
|
19236
|
-
function safeStringify(v) {
|
|
19237
|
-
try {
|
|
19238
|
-
return JSON.stringify(v);
|
|
19239
|
-
} catch {
|
|
19240
|
-
return String(v);
|
|
19241
|
-
}
|
|
19242
|
-
}
|
|
19243
|
-
function clip4(s, max) {
|
|
19244
|
-
if (!s)
|
|
19245
|
-
return "";
|
|
19246
|
-
return s.length <= max ? s : s.slice(0, max - 1) + "…";
|
|
19247
|
-
}
|
|
19248
|
-
async function sendParentNotice(client, sessionID, text, opts = {}) {
|
|
19249
|
-
const log10 = opts.log ?? (() => {});
|
|
19250
|
-
if (!client?.session) {
|
|
19251
|
-
log10("warn", "[sendParentNotice] client.session 不可用,noop");
|
|
19252
|
-
return false;
|
|
19253
|
-
}
|
|
19254
|
-
const sessionAny = client.session;
|
|
19255
|
-
if (typeof sessionAny.promptAsync !== "function") {
|
|
19256
|
-
log10("warn", "[sendParentNotice] promptAsync 不可用(SDK 太老?),noop");
|
|
19257
|
-
return false;
|
|
19258
|
-
}
|
|
19259
|
-
try {
|
|
19260
|
-
const res = await sessionAny.promptAsync({
|
|
19261
|
-
path: { id: sessionID },
|
|
19262
|
-
query: opts.directory ? { directory: opts.directory } : undefined,
|
|
19263
|
-
body: {
|
|
19264
|
-
noReply: true,
|
|
19265
|
-
parts: [
|
|
19266
|
-
{
|
|
19267
|
-
id: makePartId(),
|
|
19268
|
-
type: "text",
|
|
19269
|
-
text,
|
|
19270
|
-
synthetic: false,
|
|
19271
|
-
ignored: false
|
|
19272
|
-
}
|
|
19273
|
-
]
|
|
19274
|
-
}
|
|
19275
|
-
});
|
|
19276
|
-
if (res && typeof res === "object" && "error" in res && res.error) {
|
|
19277
|
-
log10("warn", "[sendParentNotice] promptAsync 返回 error", { error: res.error });
|
|
19278
|
-
return false;
|
|
19279
|
-
}
|
|
19280
|
-
return true;
|
|
19281
|
-
} catch (err) {
|
|
19282
|
-
log10("warn", "[sendParentNotice] 抛错(已隔离)", {
|
|
19283
|
-
error: err instanceof Error ? err.message : String(err)
|
|
19284
|
-
});
|
|
19285
|
-
return false;
|
|
19286
|
-
}
|
|
19287
|
-
}
|
|
19288
|
-
|
|
19289
19535
|
// plugins/subtasks.ts
|
|
19290
19536
|
init_autonomy();
|
|
19291
19537
|
|
|
@@ -19318,11 +19564,11 @@ async function decomposeTask(description29, opts) {
|
|
|
19318
19564
|
let childSessionId;
|
|
19319
19565
|
try {
|
|
19320
19566
|
const created = await opts.client.session.create({
|
|
19321
|
-
body: { title: `decompose:${
|
|
19567
|
+
body: { title: `decompose:${clip6(description29, 60)}` },
|
|
19322
19568
|
query: opts.directory ? { directory: opts.directory } : undefined
|
|
19323
19569
|
});
|
|
19324
19570
|
if (created.error || !created.data?.id) {
|
|
19325
|
-
log10("warn", "[decompose] session.create 失败", { error:
|
|
19571
|
+
log10("warn", "[decompose] session.create 失败", { error: describe8(created.error) });
|
|
19326
19572
|
return {
|
|
19327
19573
|
ok: false,
|
|
19328
19574
|
subtasks: [],
|
|
@@ -19349,7 +19595,7 @@ async function decomposeTask(description29, opts) {
|
|
|
19349
19595
|
}
|
|
19350
19596
|
const r = raced.value;
|
|
19351
19597
|
if (r.error || !r.data) {
|
|
19352
|
-
log10("warn", "[decompose] session.prompt 返回错误", { error:
|
|
19598
|
+
log10("warn", "[decompose] session.prompt 返回错误", { error: describe8(r.error) });
|
|
19353
19599
|
return { ok: false, subtasks: [], reason: "llm_unavailable" };
|
|
19354
19600
|
}
|
|
19355
19601
|
const rawText = pickLastText2(r.data.parts ?? []);
|
|
@@ -19359,7 +19605,7 @@ async function decomposeTask(description29, opts) {
|
|
|
19359
19605
|
}
|
|
19360
19606
|
const parsed = extractJson(rawText);
|
|
19361
19607
|
if (!parsed) {
|
|
19362
|
-
log10("warn", "[decompose] JSON 解析失败", { raw:
|
|
19608
|
+
log10("warn", "[decompose] JSON 解析失败", { raw: clip6(rawText, 200) });
|
|
19363
19609
|
return { ok: false, subtasks: [], reason: "parse_failed", raw: rawText };
|
|
19364
19610
|
}
|
|
19365
19611
|
if (parsed && typeof parsed === "object" && parsed.single_task === true) {
|
|
@@ -19388,7 +19634,7 @@ async function decomposeTask(description29, opts) {
|
|
|
19388
19634
|
}
|
|
19389
19635
|
return validateAndFinalize(normalized, rawText, log10, maxSubtasks);
|
|
19390
19636
|
} catch (err) {
|
|
19391
|
-
log10("warn", "[decompose] 抛错", { error:
|
|
19637
|
+
log10("warn", "[decompose] 抛错", { error: describe8(err) });
|
|
19392
19638
|
return { ok: false, subtasks: [], reason: "llm_unavailable" };
|
|
19393
19639
|
} finally {
|
|
19394
19640
|
if (childSessionId) {
|
|
@@ -19398,7 +19644,7 @@ async function decomposeTask(description29, opts) {
|
|
|
19398
19644
|
query: opts.directory ? { directory: opts.directory } : undefined
|
|
19399
19645
|
});
|
|
19400
19646
|
} catch (err) {
|
|
19401
|
-
log10("warn", "[decompose] session.delete 失败", { error:
|
|
19647
|
+
log10("warn", "[decompose] session.delete 失败", { error: describe8(err) });
|
|
19402
19648
|
}
|
|
19403
19649
|
}
|
|
19404
19650
|
}
|
|
@@ -19492,12 +19738,12 @@ function tryParse(s) {
|
|
|
19492
19738
|
return null;
|
|
19493
19739
|
}
|
|
19494
19740
|
}
|
|
19495
|
-
function
|
|
19741
|
+
function clip6(s, n) {
|
|
19496
19742
|
if (!s)
|
|
19497
19743
|
return "";
|
|
19498
19744
|
return s.length <= n ? s : s.slice(0, n - 1) + "…";
|
|
19499
19745
|
}
|
|
19500
|
-
function
|
|
19746
|
+
function describe8(err) {
|
|
19501
19747
|
if (!err)
|
|
19502
19748
|
return "";
|
|
19503
19749
|
if (err instanceof Error)
|
|
@@ -20083,7 +20329,7 @@ function parseTerminalOutput(ev, cfg = {}, rules = DEFAULT_RULES) {
|
|
|
20083
20329
|
`).length;
|
|
20084
20330
|
const lineFromStart = text.split(`
|
|
20085
20331
|
`)[lineIdx - 1] ?? m[0];
|
|
20086
|
-
const excerpt =
|
|
20332
|
+
const excerpt = clip7(lineFromStart.trim(), c.maxExcerpt);
|
|
20087
20333
|
if (!out.has(rule.kind)) {
|
|
20088
20334
|
out.set(rule.kind, {
|
|
20089
20335
|
severity: rule.severity,
|
|
@@ -20105,7 +20351,7 @@ function parseTerminalOutput(ev, cfg = {}, rules = DEFAULT_RULES) {
|
|
|
20105
20351
|
}
|
|
20106
20352
|
return [...out.values()].sort((a, b) => b.score - a.score);
|
|
20107
20353
|
}
|
|
20108
|
-
function
|
|
20354
|
+
function clip7(s, max) {
|
|
20109
20355
|
if (s.length <= max)
|
|
20110
20356
|
return s;
|
|
20111
20357
|
return s.slice(0, max - 1) + "…";
|
|
@@ -20164,7 +20410,7 @@ function shouldNotify(findings, ev, lru, cfg = {}, now = Date.now()) {
|
|
|
20164
20410
|
function buildSummary(ev, findings) {
|
|
20165
20411
|
const parts = [];
|
|
20166
20412
|
if (ev.cmd)
|
|
20167
|
-
parts.push(`\`${
|
|
20413
|
+
parts.push(`\`${clip7(ev.cmd, 60)}\``);
|
|
20168
20414
|
if (ev.type === "terminal.exit" && typeof ev.exit_code === "number") {
|
|
20169
20415
|
parts.push(`exit=${ev.exit_code}`);
|
|
20170
20416
|
}
|
|
@@ -20176,7 +20422,7 @@ function buildSummary(ev, findings) {
|
|
|
20176
20422
|
return parts.join(" · ");
|
|
20177
20423
|
}
|
|
20178
20424
|
const top = findings[0];
|
|
20179
|
-
parts.push(`${top.severity}/${top.kind}: ${
|
|
20425
|
+
parts.push(`${top.severity}/${top.kind}: ${clip7(top.excerpt, 80)}`);
|
|
20180
20426
|
if (findings.length > 1)
|
|
20181
20427
|
parts.push(`(+${findings.length - 1} 条)`);
|
|
20182
20428
|
return parts.join(" · ");
|
|
@@ -20660,7 +20906,7 @@ import * as zlib from "node:zlib";
|
|
|
20660
20906
|
// lib/version-injected.ts
|
|
20661
20907
|
function getInjectedVersion() {
|
|
20662
20908
|
try {
|
|
20663
|
-
const v = "0.5.
|
|
20909
|
+
const v = "0.5.2";
|
|
20664
20910
|
if (typeof v === "string" && /^\d+\.\d+\.\d+/.test(v)) {
|
|
20665
20911
|
return v;
|
|
20666
20912
|
}
|
|
@@ -21937,9 +22183,19 @@ var workflowEngineServer = async (ctx) => {
|
|
|
21937
22183
|
var handler23 = workflowEngineServer;
|
|
21938
22184
|
|
|
21939
22185
|
// plugins/session-worktree-guard.ts
|
|
22186
|
+
import path24 from "node:path";
|
|
21940
22187
|
var PLUGIN_NAME24 = "session-worktree-guard";
|
|
21941
22188
|
logLifecycle(PLUGIN_NAME24, "import", {});
|
|
21942
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
|
+
}
|
|
21943
22199
|
var INTERPRETER_WRITE_RES = [
|
|
21944
22200
|
/python.*open\s*\([^)]*['"]w['"]/,
|
|
21945
22201
|
/node.*writeFile/,
|
|
@@ -21954,12 +22210,15 @@ function buildGitVcsWriteRegex(mainRoot) {
|
|
|
21954
22210
|
}
|
|
21955
22211
|
var WRITE_TOOLS = new Set(["write", "edit", "ast_edit"]);
|
|
21956
22212
|
function rewritePath(value, mainRoot, worktreeRoot) {
|
|
21957
|
-
if (value
|
|
22213
|
+
if (!value)
|
|
22214
|
+
return null;
|
|
22215
|
+
const resolved = path24.isAbsolute(value) ? value : path24.resolve(mainRoot, value);
|
|
22216
|
+
if (resolved === mainRoot)
|
|
21958
22217
|
return worktreeRoot;
|
|
21959
22218
|
const prefix = mainRoot.endsWith("/") ? mainRoot : mainRoot + "/";
|
|
21960
|
-
if (
|
|
22219
|
+
if (resolved.startsWith(prefix)) {
|
|
21961
22220
|
const wtPrefix = worktreeRoot.endsWith("/") ? worktreeRoot : worktreeRoot + "/";
|
|
21962
|
-
return wtPrefix +
|
|
22221
|
+
return wtPrefix + resolved.slice(prefix.length);
|
|
21963
22222
|
}
|
|
21964
22223
|
return null;
|
|
21965
22224
|
}
|
|
@@ -21973,6 +22232,8 @@ function commandContainsMainRoot(command, mainRoot) {
|
|
|
21973
22232
|
return re.test(command);
|
|
21974
22233
|
}
|
|
21975
22234
|
function detectBashWriteIntent(command, mainRoot) {
|
|
22235
|
+
if (isReadOnlyBashCommand(command))
|
|
22236
|
+
return false;
|
|
21976
22237
|
if (WRITE_INTENT_RE.test(command))
|
|
21977
22238
|
return true;
|
|
21978
22239
|
for (const re of INTERPRETER_WRITE_RES) {
|
|
@@ -21991,6 +22252,8 @@ function isWriteOperation(toolName, argsObj, mainRoot) {
|
|
|
21991
22252
|
const command = argsObj["command"];
|
|
21992
22253
|
if (typeof command !== "string")
|
|
21993
22254
|
return false;
|
|
22255
|
+
if (isReadOnlyBashCommand(command))
|
|
22256
|
+
return false;
|
|
21994
22257
|
if (WRITE_INTENT_RE.test(command))
|
|
21995
22258
|
return true;
|
|
21996
22259
|
for (const re of INTERPRETER_WRITE_RES) {
|
|
@@ -22002,16 +22265,16 @@ function isWriteOperation(toolName, argsObj, mainRoot) {
|
|
|
22002
22265
|
return false;
|
|
22003
22266
|
}
|
|
22004
22267
|
var log13 = makePluginLogger(PLUGIN_NAME24);
|
|
22005
|
-
var sessionWorktreeGuardPlugin = async (
|
|
22268
|
+
var sessionWorktreeGuardPlugin = async (ctx) => {
|
|
22269
|
+
const mainRoot = ctx.directory ?? process.cwd();
|
|
22006
22270
|
logLifecycle(PLUGIN_NAME24, "activate", {
|
|
22007
|
-
|
|
22008
|
-
|
|
22271
|
+
mainRoot,
|
|
22272
|
+
CODEFORGE_SESSION_ID: process.env["CODEFORGE_SESSION_ID"] ?? "(not set)"
|
|
22009
22273
|
});
|
|
22010
22274
|
return {
|
|
22011
22275
|
"tool.execute.before": async (input, output) => {
|
|
22012
22276
|
const sessionId = input.sessionID ?? process.env["CODEFORGE_SESSION_ID"];
|
|
22013
|
-
|
|
22014
|
-
if (!sessionId || !mainRoot)
|
|
22277
|
+
if (!sessionId)
|
|
22015
22278
|
return;
|
|
22016
22279
|
let denied;
|
|
22017
22280
|
await safeAsync(PLUGIN_NAME24, "tool.execute.before", async () => {
|
|
@@ -22193,7 +22456,7 @@ var IDLE_TOAST_REMINDER_INTERVAL_MS = 30 * 60000;
|
|
|
22193
22456
|
var lastIdleToastAt = new Map;
|
|
22194
22457
|
var log14 = makePluginLogger(PLUGIN_NAME25);
|
|
22195
22458
|
var worktreeLifecyclePlugin = async (ctx) => {
|
|
22196
|
-
const mainRoot =
|
|
22459
|
+
const mainRoot = ctx.directory;
|
|
22197
22460
|
logLifecycle(PLUGIN_NAME25, "activate", {
|
|
22198
22461
|
mainRoot: mainRoot ?? "(not set)",
|
|
22199
22462
|
idle_threshold_ms: IDLE_TOAST_THROTTLE_MS
|