@andyqiu/codeforge 0.5.0 → 0.5.2

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