@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/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 未注入(Phase 1 runtime 暂未接 wire-up;" + "Phase 3 commands/merge.md 落地后会通过 __setContext 注入)"
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 __setContext(ctx) {
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/codeforge-runtime.ts
14358
- import { promises as fs13 } from "node:fs";
14359
- import * as path16 from "node:path";
14360
- var DEFAULT_RUNTIME = {
14361
- autonomy: {
14362
- default_mode: "semi",
14363
- downgrade_on_risky: true
14364
- },
14365
- runtime: {
14366
- worktree_isolation: true,
14367
- workspace_snapshot: true,
14368
- auto_commit: false
14369
- },
14370
- tools: {
14371
- browser: { enabled: false }
14372
- },
14373
- context: {
14374
- condenser_threshold_ratio: 0.7
14375
- },
14376
- update: {
14377
- auto_check_enabled: true,
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
- if (a.downgrade_on_risky !== undefined) {
14454
- if (typeof a.downgrade_on_risky === "boolean") {
14455
- cfg.autonomy.downgrade_on_risky = a.downgrade_on_risky;
14456
- } else {
14457
- warnings.push(`autonomy.downgrade_on_risky must be boolean (kept default)`);
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
- resolveCurrentSessionId: () => process.env["CODEFORGE_SESSION_ID"],
15112
- resolveMainRoot: () => ctx.directory ?? process.cwd(),
15113
- store: new PlanStore({ root: ctx.directory ?? process.cwd() })
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("默认 true:直接调 pending-changes 暂存"),
15187
- description: z30.string().optional().describe("auto_stage=true 时附带的变更说明"),
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;写代码必须 ast_edit + pending_changes,禁止直接 edit 工作区",
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 (ast_edit / pending_changes.apply / bash)
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 = clip3(e.content, o.excerptLimit);
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 = clip3(safeJson(t.args), o.excerptLimit);
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 clip3(s, max) {
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 失败:${describe4(commitErr)}`;
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: describe4(abortErr)
19040
+ error: describe6(abortErr)
18539
19041
  });
18540
19042
  }
18541
19043
  attempt.ok = false;
18542
- attempt.message = `mergeCommit 失败:${describe4(commitErr)}`;
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 异常:${describe4(err)}`;
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: describe4(err),
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: describe4(err)
19221
+ error: describe6(err)
18720
19222
  });
18721
19223
  }
18722
19224
  }
18723
- function describe4(err) {
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: describe5(err)
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: describe5(err)
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 分配失败:${describe5(err)}`, limit),
19313
+ summary: clamp(`worktree 分配失败:${describe7(err)}`, limit),
18812
19314
  status: "failed",
18813
19315
  duration_ms: now() - subStart,
18814
- error: describe5(err)
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: describe5(err)
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 抛错:${describe5(err)}`, limit),
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: describe5(err)
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: describe5(err)
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 describe5(err) {
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:${clip5(description29, 60)}` },
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: describe7(created.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: describe7(r.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: clip5(rawText, 200) });
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: describe7(err) });
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: describe7(err) });
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 clip5(s, n) {
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 describe7(err) {
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 = clip6(lineFromStart.trim(), c.maxExcerpt);
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 clip6(s, max) {
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(`\`${clip6(ev.cmd, 60)}\``);
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}: ${clip6(top.excerpt, 80)}`);
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.1";
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 === mainRoot)
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 (value.startsWith(prefix)) {
22219
+ if (resolved.startsWith(prefix)) {
21961
22220
  const wtPrefix = worktreeRoot.endsWith("/") ? worktreeRoot : worktreeRoot + "/";
21962
- return wtPrefix + value.slice(prefix.length);
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 (_ctx3) => {
22268
+ var sessionWorktreeGuardPlugin = async (ctx) => {
22269
+ const mainRoot = ctx.directory ?? process.cwd();
22006
22270
  logLifecycle(PLUGIN_NAME24, "activate", {
22007
- CODEFORGE_SESSION_ID: process.env["CODEFORGE_SESSION_ID"] ?? "(not set)",
22008
- CODEFORGE_MAIN_ROOT: process.env["CODEFORGE_MAIN_ROOT"] ?? "(not set)"
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
- const mainRoot = process.env["CODEFORGE_MAIN_ROOT"];
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 = process.env["CODEFORGE_MAIN_ROOT"] ?? ctx.directory;
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