@andyqiu/codeforge 0.3.7 → 0.3.8

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
@@ -262,7 +262,7 @@ var init_auto_review_trigger = __esm(() => {
262
262
  });
263
263
 
264
264
  // lib/runtime-paths.ts
265
- import { mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync, existsSync } from "node:fs";
265
+ import { mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync, existsSync as existsSync2 } from "node:fs";
266
266
  import * as path2 from "node:path";
267
267
  import * as os from "node:os";
268
268
  import * as crypto from "node:crypto";
@@ -296,7 +296,7 @@ function runtimeDir(absRoot, opts = {}) {
296
296
  return dir;
297
297
  mkdirSync2(dir, { recursive: true });
298
298
  const metaFile = path2.join(dir, ".meta.json");
299
- if (existsSync(metaFile)) {
299
+ if (existsSync2(metaFile)) {
300
300
  const existing = readMetaSafe(metaFile);
301
301
  if (existing && existing.absPath && existing.absPath !== resolvedRoot) {
302
302
  throw new Error(`runtime dir hash collision: ${projectKey(resolvedRoot)} already used by ${existing.absPath}, current is ${resolvedRoot}`);
@@ -335,7 +335,7 @@ function readMetaSafe(file) {
335
335
  var init_runtime_paths = () => {};
336
336
 
337
337
  // lib/global-config.ts
338
- import { readFileSync as readFileSync3, existsSync as existsSync2, statSync } from "node:fs";
338
+ import { readFileSync as readFileSync3, existsSync as existsSync3, statSync } from "node:fs";
339
339
  import * as path3 from "node:path";
340
340
  import * as os2 from "node:os";
341
341
  function __resetGlobalConfigCache() {
@@ -360,7 +360,7 @@ function loadJsonIfExists(filePath) {
360
360
  const cached = cacheGet(cacheKey);
361
361
  if (cached !== undefined)
362
362
  return cached;
363
- if (!existsSync2(filePath)) {
363
+ if (!existsSync3(filePath)) {
364
364
  cacheSet(cacheKey, null);
365
365
  return null;
366
366
  }
@@ -641,7 +641,7 @@ function isAbortError(err) {
641
641
  return name === "AbortError" || name === "TimeoutError";
642
642
  }
643
643
  function defaultSleep(ms) {
644
- return new Promise((resolve4) => setTimeout(resolve4, ms));
644
+ return new Promise((resolve5) => setTimeout(resolve5, ms));
645
645
  }
646
646
  function errorMessage2(err) {
647
647
  return err instanceof Error ? err.message : String(err);
@@ -8025,11 +8025,11 @@ function shouldStopByStuck(history, cfg) {
8025
8025
  async function withTimeout3(p, timeoutMs) {
8026
8026
  if (timeoutMs <= 0)
8027
8027
  return await p;
8028
- return await new Promise((resolve10, reject) => {
8028
+ return await new Promise((resolve11, reject) => {
8029
8029
  const timer = setTimeout(() => reject(new Error(`timeout after ${timeoutMs}ms`)), timeoutMs);
8030
8030
  Promise.resolve(p).then((v) => {
8031
8031
  clearTimeout(timer);
8032
- resolve10(v);
8032
+ resolve11(v);
8033
8033
  }, (err) => {
8034
8034
  clearTimeout(timer);
8035
8035
  reject(err);
@@ -8151,6 +8151,47 @@ var init_auto_feedback = __esm(() => {
8151
8151
  // src/index.ts
8152
8152
  init_opencode_plugin_helpers();
8153
8153
 
8154
+ // lib/dev-isolation.ts
8155
+ import { existsSync } from "node:fs";
8156
+ import { resolve, dirname } from "node:path";
8157
+ var MARKER_REL = ".codeforge/.dev-marker";
8158
+ var ENV_KEY = "CODEFORGE_DEV";
8159
+ function shouldYieldToLocalPlugin(opts = {}) {
8160
+ const env = opts.env ?? process.env;
8161
+ const fileExists = opts.fileExists ?? existsSync;
8162
+ const envVal = env[ENV_KEY];
8163
+ if (envVal === "1" || envVal === "true" || envVal === "yes") {
8164
+ return { yield: true, reason: "env" };
8165
+ }
8166
+ const startDir = opts.directory ? resolve(opts.directory) : process.cwd();
8167
+ const maxDepth = Math.max(1, opts.maxDepth ?? 20);
8168
+ let cur = startDir;
8169
+ for (let i = 0;i < maxDepth; i++) {
8170
+ const markerPath = resolve(cur, MARKER_REL);
8171
+ try {
8172
+ if (fileExists(markerPath)) {
8173
+ return { yield: true, reason: "marker", markerPath };
8174
+ }
8175
+ } catch {}
8176
+ const parent = dirname(cur);
8177
+ if (parent === cur)
8178
+ break;
8179
+ cur = parent;
8180
+ }
8181
+ return { yield: false, reason: null };
8182
+ }
8183
+ function formatYieldLog(result) {
8184
+ if (!result.yield)
8185
+ return "[codeforge] stable plugin 正常加载";
8186
+ if (result.reason === "env") {
8187
+ return "[codeforge] 检测到 CODEFORGE_DEV env,stable plugin 让位";
8188
+ }
8189
+ if (result.reason === "marker") {
8190
+ return `[codeforge] 检测到 dev marker (${result.markerPath}),stable plugin 让位`;
8191
+ }
8192
+ return "[codeforge] stable plugin 让位(未知原因)";
8193
+ }
8194
+
8154
8195
  // plugins/agent-router.ts
8155
8196
  init_opencode_plugin_helpers();
8156
8197
  var PLUGIN_NAME = "agent-router";
@@ -8317,7 +8358,7 @@ init_opencode_plugin_helpers();
8317
8358
  // lib/arena.ts
8318
8359
  var DEFAULT_TIMEOUT = 90000;
8319
8360
  async function withTimeout(p, timeoutMs, signal) {
8320
- return new Promise((resolve, reject) => {
8361
+ return new Promise((resolve2, reject) => {
8321
8362
  if (signal?.aborted)
8322
8363
  return reject(new Error("aborted"));
8323
8364
  const onAbort = () => {
@@ -8335,7 +8376,7 @@ async function withTimeout(p, timeoutMs, signal) {
8335
8376
  }
8336
8377
  Promise.resolve().then(() => p).then((v) => {
8337
8378
  cleanup();
8338
- resolve(v);
8379
+ resolve2(v);
8339
8380
  }, (err) => {
8340
8381
  cleanup();
8341
8382
  reject(err);
@@ -8561,14 +8602,14 @@ async function git(opts, args) {
8561
8602
  };
8562
8603
  }
8563
8604
  }
8564
- function resolve2(o) {
8605
+ function resolve3(o) {
8565
8606
  return {
8566
8607
  root: path.resolve(o?.root ?? process.cwd()),
8567
8608
  timeoutMs: o?.timeoutMs ?? DEFAULTS.timeoutMs
8568
8609
  };
8569
8610
  }
8570
8611
  async function isGitRepo(o) {
8571
- const opts = resolve2(o);
8612
+ const opts = resolve3(o);
8572
8613
  try {
8573
8614
  await fs.access(path.join(opts.root, ".git"));
8574
8615
  return true;
@@ -8577,7 +8618,7 @@ async function isGitRepo(o) {
8577
8618
  }
8578
8619
  }
8579
8620
  async function listChanged(o) {
8580
- const opts = resolve2(o);
8621
+ const opts = resolve3(o);
8581
8622
  if (!await isGitRepo(opts))
8582
8623
  return [];
8583
8624
  const r = await git(opts, ["status", "--porcelain"]);
@@ -8604,7 +8645,7 @@ ${input.body.trim()}
8604
8645
  `;
8605
8646
  }
8606
8647
  async function commit(input, o) {
8607
- const opts = resolve2(o);
8648
+ const opts = resolve3(o);
8608
8649
  if (!await isGitRepo(opts)) {
8609
8650
  return { ok: false, reason: "not-a-git-repo" };
8610
8651
  }
@@ -14731,10 +14772,303 @@ var modelFallbackServer = async (ctx) => {
14731
14772
  };
14732
14773
  var handler13 = modelFallbackServer;
14733
14774
 
14734
- // plugins/pwsh-utf8.ts
14775
+ // plugins/subtask-heartbeat.ts
14735
14776
  init_opencode_plugin_helpers();
14736
- var PLUGIN_NAME14 = "pwsh-utf8";
14777
+ var PLUGIN_NAME14 = "subtask-heartbeat";
14737
14778
  logLifecycle(PLUGIN_NAME14, "import", {});
14779
+ var HEARTBEAT_INTERVAL_MS2 = 30000;
14780
+ var HEARTBEAT_DEBOUNCE_MS = 25000;
14781
+ var TOAST_DURATION_MS3 = 5000;
14782
+ var START_TOAST_DURATION_MS = 2000;
14783
+ var inflight2 = new Map;
14784
+ function _snapshotInflight() {
14785
+ return [...inflight2.values()].map((r) => ({ ...r }));
14786
+ }
14787
+ function getInflightSnapshot() {
14788
+ return _snapshotInflight();
14789
+ }
14790
+ function extractCreatedChild(event) {
14791
+ if (!event || typeof event !== "object")
14792
+ return null;
14793
+ const e = event;
14794
+ if (e.type !== "session.created")
14795
+ return null;
14796
+ const info = e.properties?.info;
14797
+ if (!info || typeof info !== "object")
14798
+ return null;
14799
+ const session = info;
14800
+ if (typeof session.id !== "string")
14801
+ return null;
14802
+ if (typeof session.parentID !== "string" || session.parentID === "")
14803
+ return null;
14804
+ return { childID: session.id, parentID: session.parentID, agent: null };
14805
+ }
14806
+ function extractEndedSessionID(event) {
14807
+ if (!event || typeof event !== "object")
14808
+ return null;
14809
+ const e = event;
14810
+ if (typeof e.type !== "string")
14811
+ return null;
14812
+ if (e.type !== "session.idle" && e.type !== "session.deleted" && e.type !== "session.error") {
14813
+ return null;
14814
+ }
14815
+ const props = e.properties ?? {};
14816
+ const direct = props["sessionID"];
14817
+ if (typeof direct === "string" && direct)
14818
+ return { type: e.type, sessionID: direct };
14819
+ const info = props["info"];
14820
+ if (info && typeof info === "object") {
14821
+ const sid = info.id;
14822
+ if (typeof sid === "string" && sid)
14823
+ return { type: e.type, sessionID: sid };
14824
+ }
14825
+ return null;
14826
+ }
14827
+ function registerInflight(payload, now = Date.now()) {
14828
+ const r = {
14829
+ childID: payload.childID,
14830
+ parentID: payload.parentID,
14831
+ agent: payload.agent,
14832
+ startedAt: now,
14833
+ lastBeatAt: now,
14834
+ lastTool: null
14835
+ };
14836
+ inflight2.set(payload.childID, r);
14837
+ return r;
14838
+ }
14839
+ function recordToolBeat(sessionID, tool2, now = Date.now()) {
14840
+ const r = inflight2.get(sessionID);
14841
+ if (!r)
14842
+ return null;
14843
+ r.lastBeatAt = now;
14844
+ r.lastTool = tool2;
14845
+ return r;
14846
+ }
14847
+ function clearInflight2(sessionID) {
14848
+ const r = inflight2.get(sessionID);
14849
+ if (!r)
14850
+ return null;
14851
+ inflight2.delete(sessionID);
14852
+ return r;
14853
+ }
14854
+ function pickHeartbeats(now = Date.now()) {
14855
+ const out = [];
14856
+ for (const r of inflight2.values()) {
14857
+ if (now - r.lastBeatAt >= HEARTBEAT_DEBOUNCE_MS)
14858
+ out.push(r);
14859
+ }
14860
+ return out;
14861
+ }
14862
+ function fmtElapsed(ms) {
14863
+ const total = Math.max(0, Math.floor(ms / 1000));
14864
+ const m = Math.floor(total / 60);
14865
+ const s = total % 60;
14866
+ return m > 0 ? `${m}m${s.toString().padStart(2, "0")}s` : `${s}s`;
14867
+ }
14868
+ function buildStartToast(r) {
14869
+ const who = r.agent ?? "subagent";
14870
+ return {
14871
+ message: `\uD83D\uDE80 子 session 启动: ${who}`,
14872
+ variant: "info"
14873
+ };
14874
+ }
14875
+ function buildHeartbeatToast(r, now = Date.now()) {
14876
+ const who = r.agent ?? "subagent";
14877
+ const tool2 = r.lastTool ?? "thinking";
14878
+ return {
14879
+ message: `⏳ ${who} 仍在运行 ${fmtElapsed(now - r.startedAt)} | 当前: ${tool2}`,
14880
+ variant: "info"
14881
+ };
14882
+ }
14883
+ function buildEndToast(r, type, now = Date.now()) {
14884
+ const who = r.agent ?? "subagent";
14885
+ const elapsed = fmtElapsed(now - r.startedAt);
14886
+ if (type === "session.error") {
14887
+ return { message: `❌ ${who} 失败 (${elapsed})`, variant: "error" };
14888
+ }
14889
+ if (type === "session.deleted") {
14890
+ return { message: `\uD83D\uDDD1️ ${who} 被取消 (${elapsed})`, variant: "error" };
14891
+ }
14892
+ return { message: `✅ ${who} 完成 (${elapsed})`, variant: "success" };
14893
+ }
14894
+ function normalizeVariant3(raw) {
14895
+ if (raw === "info" || raw === "warning")
14896
+ return "default";
14897
+ return raw;
14898
+ }
14899
+ async function showToast3(client, payload, log7) {
14900
+ if (typeof client?.tui?.showToast !== "function") {
14901
+ log7?.debug?.("tui.showToast 不可用,noop");
14902
+ return false;
14903
+ }
14904
+ try {
14905
+ await client.tui.showToast({
14906
+ body: {
14907
+ message: payload.message,
14908
+ variant: normalizeVariant3(payload.variant),
14909
+ duration: payload.duration ?? TOAST_DURATION_MS3,
14910
+ title: payload.title ?? "CodeForge"
14911
+ }
14912
+ });
14913
+ return true;
14914
+ } catch (err) {
14915
+ log7?.warn("tui.showToast 抛错(已隔离)", {
14916
+ error: err instanceof Error ? err.message : String(err)
14917
+ });
14918
+ return false;
14919
+ }
14920
+ }
14921
+ var log7 = makePluginLogger(PLUGIN_NAME14);
14922
+ var subtaskHeartbeatServer = async (ctx) => {
14923
+ logLifecycle(PLUGIN_NAME14, "activate", {
14924
+ directory: ctx.directory,
14925
+ intervalMs: HEARTBEAT_INTERVAL_MS2
14926
+ });
14927
+ const client = ctx.client;
14928
+ const interval = setInterval(() => {
14929
+ safeAsync(PLUGIN_NAME14, "interval", async () => {
14930
+ const beats = pickHeartbeats();
14931
+ if (beats.length === 0)
14932
+ return;
14933
+ for (const r of beats) {
14934
+ const t = buildHeartbeatToast(r);
14935
+ const sent = await showToast3(client, t, log7);
14936
+ safeWriteLog(PLUGIN_NAME14, {
14937
+ hook: "interval",
14938
+ child: r.childID,
14939
+ parent: r.parentID,
14940
+ tool: r.lastTool,
14941
+ elapsed_ms: Date.now() - r.startedAt,
14942
+ toast_sent: sent
14943
+ });
14944
+ r.lastBeatAt = Date.now();
14945
+ }
14946
+ });
14947
+ }, HEARTBEAT_INTERVAL_MS2);
14948
+ if (typeof interval.unref === "function") {
14949
+ interval.unref();
14950
+ }
14951
+ return {
14952
+ event: async ({ event }) => {
14953
+ await safeAsync(PLUGIN_NAME14, "event", async () => {
14954
+ const created = extractCreatedChild(event);
14955
+ if (created) {
14956
+ const record = registerInflight(created);
14957
+ safeWriteLog(PLUGIN_NAME14, {
14958
+ hook: "event",
14959
+ type: "session.created",
14960
+ child: created.childID,
14961
+ parent: created.parentID
14962
+ });
14963
+ const startToast = buildStartToast(record);
14964
+ const sent = await showToast3(client, { ...startToast, duration: START_TOAST_DURATION_MS }, log7);
14965
+ safeWriteLog(PLUGIN_NAME14, {
14966
+ hook: "event",
14967
+ type: "session.created.toast",
14968
+ child: created.childID,
14969
+ toast_sent: sent
14970
+ });
14971
+ return;
14972
+ }
14973
+ const ended = extractEndedSessionID(event);
14974
+ if (ended) {
14975
+ const r = clearInflight2(ended.sessionID);
14976
+ if (r) {
14977
+ const t = buildEndToast(r, ended.type);
14978
+ const sent = await showToast3(client, t, log7);
14979
+ safeWriteLog(PLUGIN_NAME14, {
14980
+ hook: "event",
14981
+ type: ended.type,
14982
+ child: r.childID,
14983
+ elapsed_ms: Date.now() - r.startedAt,
14984
+ toast_sent: sent,
14985
+ end_toast_message: t.message
14986
+ });
14987
+ }
14988
+ }
14989
+ });
14990
+ },
14991
+ "tool.execute.before": async (input) => {
14992
+ await safeAsync(PLUGIN_NAME14, "tool.execute.before", async () => {
14993
+ if (!input || typeof input.sessionID !== "string" || typeof input.tool !== "string")
14994
+ return;
14995
+ recordToolBeat(input.sessionID, input.tool);
14996
+ });
14997
+ }
14998
+ };
14999
+ };
15000
+ var handler14 = subtaskHeartbeatServer;
15001
+
15002
+ // plugins/parallel-status.ts
15003
+ init_opencode_plugin_helpers();
15004
+ var PLUGIN_NAME15 = "parallel-status";
15005
+ logLifecycle(PLUGIN_NAME15, "import");
15006
+ var ID_MAX_LEN = 16;
15007
+ var ID_KEEP_LEN = 13;
15008
+ function shortId(s) {
15009
+ return s.length > ID_MAX_LEN ? s.slice(0, ID_KEEP_LEN) + "..." : s;
15010
+ }
15011
+ function formatElapsed(ms) {
15012
+ const total = Math.max(0, Math.floor(ms / 1000));
15013
+ const m = Math.floor(total / 60);
15014
+ const s = total % 60;
15015
+ return m > 0 ? `${m}m${s.toString().padStart(2, "0")}s` : `${s}s`;
15016
+ }
15017
+ function formatInflightMarkdown(snapshot, now = Date.now()) {
15018
+ if (snapshot.length === 0) {
15019
+ return "✅ 当前无 inflight subagent";
15020
+ }
15021
+ const lines = [];
15022
+ lines.push(`\uD83D\uDCCA 当前 ${snapshot.length} 个 subagent 在跑:`, "");
15023
+ lines.push("| # | child id | parent id | agent | 已跑 | 最近工具 |");
15024
+ lines.push("|---|----------|-----------|-------|------|----------|");
15025
+ snapshot.forEach((r, i) => {
15026
+ const elapsed = formatElapsed(now - r.startedAt);
15027
+ const agent = r.agent ?? "(unknown)";
15028
+ const tool2 = r.lastTool ?? "(thinking)";
15029
+ const child = shortId(r.childID);
15030
+ const parent = shortId(r.parentID);
15031
+ lines.push(`| ${i + 1} | \`${child}\` | \`${parent}\` | ${agent} | ${elapsed} | ${tool2} |`);
15032
+ });
15033
+ return lines.join(`
15034
+ `);
15035
+ }
15036
+ var parallelStatusServer = async (ctx) => {
15037
+ const log8 = makePluginLogger(PLUGIN_NAME15);
15038
+ logLifecycle(PLUGIN_NAME15, "activate", { directory: ctx.directory });
15039
+ return {
15040
+ "command.execute.before": async (input, output) => {
15041
+ try {
15042
+ if (!input || input.command !== "parallel-status")
15043
+ return;
15044
+ const snapshot = getInflightSnapshot();
15045
+ const text = formatInflightMarkdown(snapshot);
15046
+ if (Array.isArray(output?.parts)) {
15047
+ output.parts.length = 0;
15048
+ output.parts.push({
15049
+ id: `parallel-status-${Date.now()}`,
15050
+ sessionID: input.sessionID,
15051
+ messageID: "",
15052
+ type: "text",
15053
+ text,
15054
+ synthetic: false
15055
+ });
15056
+ }
15057
+ log8.info(`[${PLUGIN_NAME15}] 已回写 ${snapshot.length} 条 inflight`);
15058
+ } catch (err) {
15059
+ log8.error(`[${PLUGIN_NAME15}] command.execute.before 异常(已隔离)`, {
15060
+ error: err instanceof Error ? err.message : String(err)
15061
+ });
15062
+ }
15063
+ }
15064
+ };
15065
+ };
15066
+ var handler15 = parallelStatusServer;
15067
+
15068
+ // plugins/pwsh-utf8.ts
15069
+ init_opencode_plugin_helpers();
15070
+ var PLUGIN_NAME16 = "pwsh-utf8";
15071
+ logLifecycle(PLUGIN_NAME16, "import", {});
14738
15072
  var PRELUDE = "chcp 65001 *> $null; " + "[Console]::OutputEncoding = [System.Text.UTF8Encoding]::new(); " + "$OutputEncoding = [System.Text.UTF8Encoding]::new(); ";
14739
15073
  function prependUtf8Prelude(command) {
14740
15074
  if (typeof command !== "string")
@@ -14747,14 +15081,14 @@ function prependUtf8Prelude(command) {
14747
15081
  return command;
14748
15082
  return PRELUDE + command;
14749
15083
  }
14750
- var handler14 = async (_ctx) => {
15084
+ var handler16 = async (_ctx) => {
14751
15085
  const enabled = process.platform === "win32" && process.env.CODEFORGE_DISABLE_PWSH_UTF8 !== "1";
14752
- logLifecycle(PLUGIN_NAME14, "activate", { enabled, platform: process.platform });
15086
+ logLifecycle(PLUGIN_NAME16, "activate", { enabled, platform: process.platform });
14753
15087
  if (!enabled)
14754
15088
  return {};
14755
15089
  return {
14756
15090
  "tool.execute.before": async (input, output) => {
14757
- await safeAsync(PLUGIN_NAME14, "tool.execute.before", async () => {
15091
+ await safeAsync(PLUGIN_NAME16, "tool.execute.before", async () => {
14758
15092
  if (input.tool !== "bash")
14759
15093
  return;
14760
15094
  const args = output.args ?? {};
@@ -14763,7 +15097,7 @@ var handler14 = async (_ctx) => {
14763
15097
  if (next !== undefined && next !== original) {
14764
15098
  args["command"] = next;
14765
15099
  output.args = args;
14766
- safeWriteLog(PLUGIN_NAME14, {
15100
+ safeWriteLog(PLUGIN_NAME16, {
14767
15101
  hook: "tool.execute.before",
14768
15102
  tool: input.tool,
14769
15103
  callID: input.callID,
@@ -14956,7 +15290,7 @@ function buildRecoveryPlan(events, meta = { id: null }, opts = {}) {
14956
15290
  });
14957
15291
  }
14958
15292
  }
14959
- const inflight2 = [...toolMap.values()].sort((a, b) => b.last_ts - a.last_ts);
15293
+ const inflight3 = [...toolMap.values()].sort((a, b) => b.last_ts - a.last_ts);
14960
15294
  const proposed = window.some((t) => PENDING_CHANGES_TOOLS.has(t.tool));
14961
15295
  const applied = window.some((t) => APPLY_TOOLS.has(t.tool) && t.ok);
14962
15296
  const pending_changes_likely = proposed && !applied;
@@ -14980,7 +15314,7 @@ function buildRecoveryPlan(events, meta = { id: null }, opts = {}) {
14980
15314
  idleMs,
14981
15315
  lastUser,
14982
15316
  lastAgent,
14983
- inflight: inflight2,
15317
+ inflight: inflight3,
14984
15318
  pending_changes_likely,
14985
15319
  open_subtasks_likely,
14986
15320
  reason
@@ -14991,7 +15325,7 @@ function buildRecoveryPlan(events, meta = { id: null }, opts = {}) {
14991
15325
  idle_ms: idleMs,
14992
15326
  last_user_intent: lastUser,
14993
15327
  last_agent: lastAgent,
14994
- inflight_tools: inflight2,
15328
+ inflight_tools: inflight3,
14995
15329
  pending_changes_likely,
14996
15330
  open_subtasks_likely,
14997
15331
  summary
@@ -15096,8 +15430,8 @@ function isRecoveryWorthShowing(plan) {
15096
15430
  }
15097
15431
 
15098
15432
  // plugins/session-recovery.ts
15099
- var PLUGIN_NAME15 = "session-recovery";
15100
- logLifecycle(PLUGIN_NAME15, "import", {});
15433
+ var PLUGIN_NAME17 = "session-recovery";
15434
+ logLifecycle(PLUGIN_NAME17, "import", {});
15101
15435
  async function processSessionStart(currentSessionId, opts = {}) {
15102
15436
  if (opts.disabled) {
15103
15437
  return { ok: true, injected: false, reason: "disabled" };
@@ -15107,7 +15441,7 @@ async function processSessionStart(currentSessionId, opts = {}) {
15107
15441
  excludeIds.add(currentSessionId);
15108
15442
  const r = await scanLastSession({ ...opts, excludeIds: [...excludeIds] });
15109
15443
  if (!r.ok) {
15110
- opts.log?.warn?.(`[${PLUGIN_NAME15}] 扫描失败:${r.error}`);
15444
+ opts.log?.warn?.(`[${PLUGIN_NAME17}] 扫描失败:${r.error}`);
15111
15445
  return { ok: false, injected: false, reason: "scan_error", error: r.error };
15112
15446
  }
15113
15447
  const plan = r.plan;
@@ -15121,7 +15455,7 @@ async function processSessionStart(currentSessionId, opts = {}) {
15121
15455
  await opts.injectRecovery(injection);
15122
15456
  } catch (err) {
15123
15457
  const msg = err instanceof Error ? err.message : String(err);
15124
- opts.log?.warn?.(`[${PLUGIN_NAME15}] injectRecovery 异常:${msg}`);
15458
+ opts.log?.warn?.(`[${PLUGIN_NAME17}] injectRecovery 异常:${msg}`);
15125
15459
  return { ok: false, injected: false, plan, reason: "inject_error", error: msg };
15126
15460
  }
15127
15461
  }
@@ -15150,13 +15484,13 @@ function renderPrompt(plan) {
15150
15484
  return lines.join(`
15151
15485
  `);
15152
15486
  }
15153
- var log7 = makePluginLogger(PLUGIN_NAME15);
15487
+ var log8 = makePluginLogger(PLUGIN_NAME17);
15154
15488
  var _lastInjection = null;
15155
15489
  var sessionRecoveryServer = async (ctx) => {
15156
- logLifecycle(PLUGIN_NAME15, "activate", { directory: ctx.directory });
15490
+ logLifecycle(PLUGIN_NAME17, "activate", { directory: ctx.directory });
15157
15491
  return {
15158
15492
  event: async ({ event }) => {
15159
- await safeAsync(PLUGIN_NAME15, "event", async () => {
15493
+ await safeAsync(PLUGIN_NAME17, "event", async () => {
15160
15494
  const e = event;
15161
15495
  if (!e || typeof e.type !== "string")
15162
15496
  return;
@@ -15166,12 +15500,12 @@ var sessionRecoveryServer = async (ctx) => {
15166
15500
  const root = typeof e.properties?.["root"] === "string" ? e.properties["root"] : ctx.directory ?? process.cwd();
15167
15501
  const r = await processSessionStart(sid, {
15168
15502
  root,
15169
- log: log7,
15503
+ log: log8,
15170
15504
  injectRecovery: (inj) => {
15171
15505
  _lastInjection = inj;
15172
15506
  }
15173
15507
  });
15174
- safeWriteLog(PLUGIN_NAME15, {
15508
+ safeWriteLog(PLUGIN_NAME17, {
15175
15509
  hook: "event",
15176
15510
  type: "session.start",
15177
15511
  ok: r.ok,
@@ -15180,234 +15514,13 @@ var sessionRecoveryServer = async (ctx) => {
15180
15514
  last_session_id: r.plan?.last_session_id
15181
15515
  });
15182
15516
  if (r.injected && r.plan) {
15183
- log7.info(`[${PLUGIN_NAME15}] 注入恢复提示(last=${r.plan.last_session_id?.slice(0, 8) ?? "?"}, reason=${r.plan.reason})`);
15184
- }
15185
- });
15186
- }
15187
- };
15188
- };
15189
- var handler15 = sessionRecoveryServer;
15190
-
15191
- // plugins/subtask-heartbeat.ts
15192
- init_opencode_plugin_helpers();
15193
- var PLUGIN_NAME16 = "subtask-heartbeat";
15194
- logLifecycle(PLUGIN_NAME16, "import", {});
15195
- var HEARTBEAT_INTERVAL_MS2 = 30000;
15196
- var HEARTBEAT_DEBOUNCE_MS = 25000;
15197
- var TOAST_DURATION_MS3 = 5000;
15198
- var START_TOAST_DURATION_MS = 2000;
15199
- var inflight2 = new Map;
15200
- function extractCreatedChild(event) {
15201
- if (!event || typeof event !== "object")
15202
- return null;
15203
- const e = event;
15204
- if (e.type !== "session.created")
15205
- return null;
15206
- const info = e.properties?.info;
15207
- if (!info || typeof info !== "object")
15208
- return null;
15209
- const session = info;
15210
- if (typeof session.id !== "string")
15211
- return null;
15212
- if (typeof session.parentID !== "string" || session.parentID === "")
15213
- return null;
15214
- return { childID: session.id, parentID: session.parentID, agent: null };
15215
- }
15216
- function extractEndedSessionID(event) {
15217
- if (!event || typeof event !== "object")
15218
- return null;
15219
- const e = event;
15220
- if (typeof e.type !== "string")
15221
- return null;
15222
- if (e.type !== "session.idle" && e.type !== "session.deleted" && e.type !== "session.error") {
15223
- return null;
15224
- }
15225
- const props = e.properties ?? {};
15226
- const direct = props["sessionID"];
15227
- if (typeof direct === "string" && direct)
15228
- return { type: e.type, sessionID: direct };
15229
- const info = props["info"];
15230
- if (info && typeof info === "object") {
15231
- const sid = info.id;
15232
- if (typeof sid === "string" && sid)
15233
- return { type: e.type, sessionID: sid };
15234
- }
15235
- return null;
15236
- }
15237
- function registerInflight(payload, now = Date.now()) {
15238
- const r = {
15239
- childID: payload.childID,
15240
- parentID: payload.parentID,
15241
- agent: payload.agent,
15242
- startedAt: now,
15243
- lastBeatAt: now,
15244
- lastTool: null
15245
- };
15246
- inflight2.set(payload.childID, r);
15247
- return r;
15248
- }
15249
- function recordToolBeat(sessionID, tool2, now = Date.now()) {
15250
- const r = inflight2.get(sessionID);
15251
- if (!r)
15252
- return null;
15253
- r.lastBeatAt = now;
15254
- r.lastTool = tool2;
15255
- return r;
15256
- }
15257
- function clearInflight2(sessionID) {
15258
- const r = inflight2.get(sessionID);
15259
- if (!r)
15260
- return null;
15261
- inflight2.delete(sessionID);
15262
- return r;
15263
- }
15264
- function pickHeartbeats(now = Date.now()) {
15265
- const out = [];
15266
- for (const r of inflight2.values()) {
15267
- if (now - r.lastBeatAt >= HEARTBEAT_DEBOUNCE_MS)
15268
- out.push(r);
15269
- }
15270
- return out;
15271
- }
15272
- function fmtElapsed(ms) {
15273
- const total = Math.max(0, Math.floor(ms / 1000));
15274
- const m = Math.floor(total / 60);
15275
- const s = total % 60;
15276
- return m > 0 ? `${m}m${s.toString().padStart(2, "0")}s` : `${s}s`;
15277
- }
15278
- function buildStartToast(r) {
15279
- const who = r.agent ?? "subagent";
15280
- return {
15281
- message: `\uD83D\uDE80 子 session 启动: ${who}`,
15282
- variant: "info"
15283
- };
15284
- }
15285
- function buildHeartbeatToast(r, now = Date.now()) {
15286
- const who = r.agent ?? "subagent";
15287
- const tool2 = r.lastTool ?? "thinking";
15288
- return {
15289
- message: `⏳ ${who} 仍在运行 ${fmtElapsed(now - r.startedAt)} | 当前: ${tool2}`,
15290
- variant: "info"
15291
- };
15292
- }
15293
- function buildEndToast(r, type, now = Date.now()) {
15294
- const who = r.agent ?? "subagent";
15295
- const elapsed = fmtElapsed(now - r.startedAt);
15296
- if (type === "session.error") {
15297
- return { message: `❌ ${who} 失败 (${elapsed})`, variant: "error" };
15298
- }
15299
- if (type === "session.deleted") {
15300
- return { message: `\uD83D\uDDD1️ ${who} 被取消 (${elapsed})`, variant: "error" };
15301
- }
15302
- return { message: `✅ ${who} 完成 (${elapsed})`, variant: "success" };
15303
- }
15304
- function normalizeVariant3(raw) {
15305
- if (raw === "info" || raw === "warning")
15306
- return "default";
15307
- return raw;
15308
- }
15309
- async function showToast3(client, payload, log8) {
15310
- if (typeof client?.tui?.showToast !== "function") {
15311
- log8?.debug?.("tui.showToast 不可用,noop");
15312
- return false;
15313
- }
15314
- try {
15315
- await client.tui.showToast({
15316
- body: {
15317
- message: payload.message,
15318
- variant: normalizeVariant3(payload.variant),
15319
- duration: payload.duration ?? TOAST_DURATION_MS3,
15320
- title: payload.title ?? "CodeForge"
15321
- }
15322
- });
15323
- return true;
15324
- } catch (err) {
15325
- log8?.warn("tui.showToast 抛错(已隔离)", {
15326
- error: err instanceof Error ? err.message : String(err)
15327
- });
15328
- return false;
15329
- }
15330
- }
15331
- var log8 = makePluginLogger(PLUGIN_NAME16);
15332
- var subtaskHeartbeatServer = async (ctx) => {
15333
- logLifecycle(PLUGIN_NAME16, "activate", {
15334
- directory: ctx.directory,
15335
- intervalMs: HEARTBEAT_INTERVAL_MS2
15336
- });
15337
- const client = ctx.client;
15338
- const interval = setInterval(() => {
15339
- safeAsync(PLUGIN_NAME16, "interval", async () => {
15340
- const beats = pickHeartbeats();
15341
- if (beats.length === 0)
15342
- return;
15343
- for (const r of beats) {
15344
- const t = buildHeartbeatToast(r);
15345
- const sent = await showToast3(client, t, log8);
15346
- safeWriteLog(PLUGIN_NAME16, {
15347
- hook: "interval",
15348
- child: r.childID,
15349
- parent: r.parentID,
15350
- tool: r.lastTool,
15351
- elapsed_ms: Date.now() - r.startedAt,
15352
- toast_sent: sent
15353
- });
15354
- r.lastBeatAt = Date.now();
15355
- }
15356
- });
15357
- }, HEARTBEAT_INTERVAL_MS2);
15358
- if (typeof interval.unref === "function") {
15359
- interval.unref();
15360
- }
15361
- return {
15362
- event: async ({ event }) => {
15363
- await safeAsync(PLUGIN_NAME16, "event", async () => {
15364
- const created = extractCreatedChild(event);
15365
- if (created) {
15366
- const record = registerInflight(created);
15367
- safeWriteLog(PLUGIN_NAME16, {
15368
- hook: "event",
15369
- type: "session.created",
15370
- child: created.childID,
15371
- parent: created.parentID
15372
- });
15373
- const startToast = buildStartToast(record);
15374
- const sent = await showToast3(client, { ...startToast, duration: START_TOAST_DURATION_MS }, log8);
15375
- safeWriteLog(PLUGIN_NAME16, {
15376
- hook: "event",
15377
- type: "session.created.toast",
15378
- child: created.childID,
15379
- toast_sent: sent
15380
- });
15381
- return;
15517
+ log8.info(`[${PLUGIN_NAME17}] 注入恢复提示(last=${r.plan.last_session_id?.slice(0, 8) ?? "?"}, reason=${r.plan.reason})`);
15382
15518
  }
15383
- const ended = extractEndedSessionID(event);
15384
- if (ended) {
15385
- const r = clearInflight2(ended.sessionID);
15386
- if (r) {
15387
- const t = buildEndToast(r, ended.type);
15388
- const sent = await showToast3(client, t, log8);
15389
- safeWriteLog(PLUGIN_NAME16, {
15390
- hook: "event",
15391
- type: ended.type,
15392
- child: r.childID,
15393
- elapsed_ms: Date.now() - r.startedAt,
15394
- toast_sent: sent,
15395
- end_toast_message: t.message
15396
- });
15397
- }
15398
- }
15399
- });
15400
- },
15401
- "tool.execute.before": async (input) => {
15402
- await safeAsync(PLUGIN_NAME16, "tool.execute.before", async () => {
15403
- if (!input || typeof input.sessionID !== "string" || typeof input.tool !== "string")
15404
- return;
15405
- recordToolBeat(input.sessionID, input.tool);
15406
15519
  });
15407
15520
  }
15408
15521
  };
15409
15522
  };
15410
- var handler16 = subtaskHeartbeatServer;
15523
+ var handler17 = sessionRecoveryServer;
15411
15524
 
15412
15525
  // plugins/subtasks.ts
15413
15526
  import { promises as fs10 } from "node:fs";
@@ -15437,6 +15550,18 @@ async function schedule(opts) {
15437
15550
  const results = new Array(opts.subtasks.length);
15438
15551
  let nextIdx = 0;
15439
15552
  const workers = [];
15553
+ const fireFinish = async (i, res) => {
15554
+ if (!opts.onSubtaskFinish)
15555
+ return;
15556
+ try {
15557
+ await opts.onSubtaskFinish(res, i);
15558
+ } catch (err) {
15559
+ log9("warn", `[parallel] onSubtaskFinish 抛错(已隔离)`, {
15560
+ id: res.id,
15561
+ error: describe4(err)
15562
+ });
15563
+ }
15564
+ };
15440
15565
  const runOne = async (i, spec) => {
15441
15566
  const subStart = now();
15442
15567
  let alloc;
@@ -15444,7 +15569,7 @@ async function schedule(opts) {
15444
15569
  try {
15445
15570
  alloc = await opts.deps.allocateWorktree(spec.id);
15446
15571
  } catch (err) {
15447
- results[i] = {
15572
+ const res2 = {
15448
15573
  id: spec.id,
15449
15574
  ok: false,
15450
15575
  summary: clamp(`worktree 分配失败:${describe4(err)}`, limit),
@@ -15452,9 +15577,21 @@ async function schedule(opts) {
15452
15577
  duration_ms: now() - subStart,
15453
15578
  error: describe4(err)
15454
15579
  };
15580
+ results[i] = res2;
15581
+ await fireFinish(i, res2);
15455
15582
  return;
15456
15583
  }
15457
15584
  }
15585
+ if (opts.onSubtaskStart) {
15586
+ try {
15587
+ await opts.onSubtaskStart(spec, i);
15588
+ } catch (err) {
15589
+ log9("warn", `[parallel] onSubtaskStart 抛错(已隔离)`, {
15590
+ id: spec.id,
15591
+ error: describe4(err)
15592
+ });
15593
+ }
15594
+ }
15458
15595
  const ctl = new AbortController;
15459
15596
  const cascade = () => ctl.abort();
15460
15597
  if (globalCtl.signal.aborted)
@@ -15507,6 +15644,7 @@ async function schedule(opts) {
15507
15644
  }
15508
15645
  }
15509
15646
  results[i] = res;
15647
+ await fireFinish(i, res);
15510
15648
  };
15511
15649
  const launch = async () => {
15512
15650
  while (true) {
@@ -15515,13 +15653,15 @@ async function schedule(opts) {
15515
15653
  return;
15516
15654
  if (globalCtl.signal.aborted) {
15517
15655
  const spec = opts.subtasks[i];
15518
- results[i] = {
15656
+ const res = {
15519
15657
  id: spec.id,
15520
15658
  ok: false,
15521
15659
  summary: clamp("调度器已取消,未启动", limit),
15522
15660
  status: "cancelled",
15523
15661
  duration_ms: 0
15524
15662
  };
15663
+ results[i] = res;
15664
+ await fireFinish(i, res);
15525
15665
  continue;
15526
15666
  }
15527
15667
  await runOne(i, opts.subtasks[i]);
@@ -15802,20 +15942,20 @@ async function withTimeout2(p, ms, signal) {
15802
15942
  throw err;
15803
15943
  }
15804
15944
  }
15805
- return await new Promise((resolve10, reject) => {
15945
+ return await new Promise((resolve11, reject) => {
15806
15946
  let settled = false;
15807
15947
  const timer = setTimeout(() => {
15808
15948
  if (settled)
15809
15949
  return;
15810
15950
  settled = true;
15811
- resolve10({ kind: "timeout" });
15951
+ resolve11({ kind: "timeout" });
15812
15952
  }, ms);
15813
15953
  const onAbort = () => {
15814
15954
  if (settled)
15815
15955
  return;
15816
15956
  settled = true;
15817
15957
  clearTimeout(timer);
15818
- resolve10({ kind: "aborted" });
15958
+ resolve11({ kind: "aborted" });
15819
15959
  };
15820
15960
  signal?.addEventListener("abort", onAbort, { once: true });
15821
15961
  p.then((value) => {
@@ -15824,7 +15964,7 @@ async function withTimeout2(p, ms, signal) {
15824
15964
  settled = true;
15825
15965
  clearTimeout(timer);
15826
15966
  signal?.removeEventListener("abort", onAbort);
15827
- resolve10({ kind: "ok", value });
15967
+ resolve11({ kind: "ok", value });
15828
15968
  }, (err) => {
15829
15969
  if (settled)
15830
15970
  return;
@@ -15860,11 +16000,48 @@ function clip4(s, max) {
15860
16000
  return "";
15861
16001
  return s.length <= max ? s : s.slice(0, max - 1) + "…";
15862
16002
  }
16003
+ async function sendParentNotice(client, sessionID, text, opts = {}) {
16004
+ const log9 = opts.log ?? (() => {});
16005
+ if (!client?.session) {
16006
+ log9("warn", "[sendParentNotice] client.session 不可用,noop");
16007
+ return false;
16008
+ }
16009
+ const sessionAny = client.session;
16010
+ if (typeof sessionAny.promptAsync !== "function") {
16011
+ log9("warn", "[sendParentNotice] promptAsync 不可用(SDK 太老?),noop");
16012
+ return false;
16013
+ }
16014
+ try {
16015
+ const res = await sessionAny.promptAsync({
16016
+ sessionID,
16017
+ directory: opts.directory,
16018
+ noReply: true,
16019
+ parts: [
16020
+ {
16021
+ type: "text",
16022
+ text,
16023
+ synthetic: true,
16024
+ ignored: true
16025
+ }
16026
+ ]
16027
+ });
16028
+ if (res && typeof res === "object" && "error" in res && res.error) {
16029
+ log9("warn", "[sendParentNotice] promptAsync 返回 error", { error: res.error });
16030
+ return false;
16031
+ }
16032
+ return true;
16033
+ } catch (err) {
16034
+ log9("warn", "[sendParentNotice] 抛错(已隔离)", {
16035
+ error: err instanceof Error ? err.message : String(err)
16036
+ });
16037
+ return false;
16038
+ }
16039
+ }
15863
16040
 
15864
16041
  // plugins/subtasks.ts
15865
16042
  init_opencode_plugin_helpers();
15866
16043
  init_runtime_paths();
15867
- var PLUGIN_NAME17 = "subtasks";
16044
+ var PLUGIN_NAME18 = "subtasks";
15868
16045
  function getLogFile(root = process.cwd()) {
15869
16046
  return path13.join(runtimeDir(root), "logs", "subtasks.log");
15870
16047
  }
@@ -15943,18 +16120,49 @@ async function handleParallelCommand(raw) {
15943
16120
  specs = splitDescriptions(args.description, { parentId });
15944
16121
  }
15945
16122
  if (specs.length === 0) {
15946
- log9?.warn(`[${PLUGIN_NAME17}] /parallel 缺有效子任务`);
16123
+ log9?.warn(`[${PLUGIN_NAME18}] /parallel 缺有效子任务`);
15947
16124
  await safeReply2(ctx, "⚠ /parallel 需要至少 1 个子任务(用 ; 或换行分隔)");
15948
16125
  return { ok: false, reason: "no_subtasks" };
15949
16126
  }
16127
+ const maxConcurrency = clampInt(args.maxConcurrency, 1, 16, 4);
16128
+ const totalTimeoutMs = clampInt(args.totalTimeout_ms, 1000, 60 * 60000, 30 * 60000);
16129
+ const canNotice = Boolean(ctx.client && ctx.parentSessionID);
16130
+ if (canNotice) {
16131
+ const lines = [
16132
+ `\uD83D\uDE80 /parallel 已派出 ${specs.length} 个子任务(并发=${maxConcurrency}):`,
16133
+ ...specs.map((s, i) => ` ${i + 1}. \`${s.id}\` — ${s.description}`),
16134
+ "",
16135
+ "\uD83D\uDCA1 用 /parallel-status 随时查进度;TUI 按 Ctrl+→ 进子 session 看实时。"
16136
+ ];
16137
+ await sendParentNotice(ctx.client, ctx.parentSessionID, lines.join(`
16138
+ `), {
16139
+ directory: ctx.directory,
16140
+ log: (lvl, msg, data) => {
16141
+ if (lvl === "error")
16142
+ log9?.error(msg, data);
16143
+ else if (lvl === "warn")
16144
+ log9?.warn(msg, data);
16145
+ else
16146
+ log9?.info(msg, data);
16147
+ }
16148
+ });
16149
+ }
15950
16150
  const runner = ctx.runner ?? mockRunner;
15951
16151
  let result;
15952
16152
  try {
15953
16153
  result = await schedule({
15954
16154
  parentId,
15955
16155
  subtasks: specs,
15956
- maxConcurrency: clampInt(args.maxConcurrency, 1, 16, 4),
15957
- totalTimeout_ms: clampInt(args.totalTimeout_ms, 1000, 60 * 60000, 30 * 60000),
16156
+ maxConcurrency,
16157
+ totalTimeout_ms: totalTimeoutMs,
16158
+ onSubtaskStart: canNotice ? async (spec, idx) => {
16159
+ await sendParentNotice(ctx.client, ctx.parentSessionID, `▶ 子任务 ${idx + 1}/${specs.length} 启动: \`${spec.id}\` — ${spec.description}`, { directory: ctx.directory });
16160
+ } : undefined,
16161
+ onSubtaskFinish: canNotice ? async (res, idx) => {
16162
+ const emoji = res.status === "success" ? "✅" : res.status === "need_review" ? "⚠" : res.status === "timeout" ? "⏱" : res.status === "cancelled" ? "\uD83D\uDDD1" : "❌";
16163
+ const fileCount = res.changedFiles?.length ?? 0;
16164
+ await sendParentNotice(ctx.client, ctx.parentSessionID, `${emoji} 子任务 ${idx + 1}/${specs.length} 完成 [${res.status}] \`${res.id}\` (${res.duration_ms}ms, files=${fileCount})`, { directory: ctx.directory });
16165
+ } : undefined,
15958
16166
  deps: {
15959
16167
  runSubtask: runner,
15960
16168
  allocateWorktree: ctx.allocateWorktree,
@@ -15970,7 +16178,7 @@ async function handleParallelCommand(raw) {
15970
16178
  });
15971
16179
  } catch (err) {
15972
16180
  const msg = err instanceof Error ? err.message : String(err);
15973
- log9?.error(`[${PLUGIN_NAME17}] schedule 抛错`, { error: msg });
16181
+ log9?.error(`[${PLUGIN_NAME18}] schedule 抛错`, { error: msg });
15974
16182
  await safeReply2(ctx, `❌ 并发调度失败:${msg}`);
15975
16183
  return { ok: false, reason: msg };
15976
16184
  }
@@ -15978,7 +16186,7 @@ async function handleParallelCommand(raw) {
15978
16186
  const summaryLines = [head, "", "```", result.digest.text, "```"];
15979
16187
  await safeReply2(ctx, summaryLines.join(`
15980
16188
  `));
15981
- log9?.info(`[${PLUGIN_NAME17}] schedule 完成`, {
16189
+ log9?.info(`[${PLUGIN_NAME18}] schedule 完成`, {
15982
16190
  parentId,
15983
16191
  success: result.digest.success,
15984
16192
  failed: result.digest.failed,
@@ -15988,7 +16196,7 @@ async function handleParallelCommand(raw) {
15988
16196
  try {
15989
16197
  await ctx.onCompleted(result);
15990
16198
  } catch (err) {
15991
- log9?.warn(`[${PLUGIN_NAME17}] onCompleted hook 抛错(已隔离)`, {
16199
+ log9?.warn(`[${PLUGIN_NAME18}] onCompleted hook 抛错(已隔离)`, {
15992
16200
  error: err instanceof Error ? err.message : String(err)
15993
16201
  });
15994
16202
  }
@@ -16019,14 +16227,17 @@ async function maybeHandleMessage(raw) {
16019
16227
  reply: ctx.reply,
16020
16228
  runner: ctx.runner,
16021
16229
  allocateWorktree: ctx.allocateWorktree,
16022
- log: ctx.log
16230
+ log: ctx.log,
16231
+ client: ctx.client,
16232
+ parentSessionID: ctx.parentSessionID,
16233
+ directory: ctx.directory
16023
16234
  });
16024
16235
  }
16025
16236
  async function writeLog(level, msg, data) {
16026
16237
  const line = JSON.stringify({
16027
16238
  ts: new Date().toISOString(),
16028
16239
  level,
16029
- plugin: PLUGIN_NAME17,
16240
+ plugin: PLUGIN_NAME18,
16030
16241
  msg,
16031
16242
  data
16032
16243
  }) + `
@@ -16037,11 +16248,11 @@ async function writeLog(level, msg, data) {
16037
16248
  await fs10.appendFile(logFile, line, "utf8");
16038
16249
  } catch {}
16039
16250
  }
16040
- logLifecycle(PLUGIN_NAME17, "import");
16251
+ logLifecycle(PLUGIN_NAME18, "import");
16041
16252
  var subtasksServer = async (ctx) => {
16042
- const log9 = makePluginLogger(PLUGIN_NAME17);
16253
+ const log9 = makePluginLogger(PLUGIN_NAME18);
16043
16254
  const client = ctx?.client ?? undefined;
16044
- logLifecycle(PLUGIN_NAME17, "activate", {
16255
+ logLifecycle(PLUGIN_NAME18, "activate", {
16045
16256
  directory: ctx.directory,
16046
16257
  hasClient: Boolean(client)
16047
16258
  });
@@ -16062,6 +16273,9 @@ var subtasksServer = async (ctx) => {
16062
16273
  return Promise.resolve();
16063
16274
  },
16064
16275
  log: log9,
16276
+ client,
16277
+ parentSessionID: input.sessionID,
16278
+ directory: ctx.directory,
16065
16279
  runner: client ? makeOpencodeRunner({
16066
16280
  client,
16067
16281
  parentSessionID: input.sessionID,
@@ -16098,20 +16312,20 @@ var subtasksServer = async (ctx) => {
16098
16312
  });
16099
16313
  }
16100
16314
  } catch (err) {
16101
- log9.error(`[${PLUGIN_NAME17}] command.execute.before 异常(已隔离)`, {
16315
+ log9.error(`[${PLUGIN_NAME18}] command.execute.before 异常(已隔离)`, {
16102
16316
  error: err instanceof Error ? err.message : String(err)
16103
16317
  });
16104
16318
  }
16105
16319
  }
16106
16320
  };
16107
16321
  };
16108
- var handler17 = subtasksServer;
16322
+ var handler18 = subtasksServer;
16109
16323
 
16110
16324
  // plugins/terminal-monitor.ts
16111
16325
  init_opencode_plugin_helpers();
16112
16326
  import * as crypto5 from "node:crypto";
16113
- var PLUGIN_NAME18 = "terminal-monitor";
16114
- logLifecycle(PLUGIN_NAME18, "import", {});
16327
+ var PLUGIN_NAME19 = "terminal-monitor";
16328
+ logLifecycle(PLUGIN_NAME19, "import", {});
16115
16329
  var DEFAULT_CONFIG7 = {
16116
16330
  minScore: 0.6,
16117
16331
  cooldownMs: 30000,
@@ -16393,17 +16607,17 @@ function describeError(err) {
16393
16607
  return String(err);
16394
16608
  }
16395
16609
  }
16396
- var log9 = makePluginLogger(PLUGIN_NAME18);
16610
+ var log9 = makePluginLogger(PLUGIN_NAME19);
16397
16611
  var lru = new FingerprintLRU2;
16398
16612
  var _lastNotification = null;
16399
16613
  var terminalMonitorServer = async (ctx) => {
16400
- logLifecycle(PLUGIN_NAME18, "activate", {
16614
+ logLifecycle(PLUGIN_NAME19, "activate", {
16401
16615
  directory: ctx.directory,
16402
16616
  triggerEventTypes: ["terminal.output", "terminal.exit"]
16403
16617
  });
16404
16618
  return {
16405
16619
  event: async ({ event }) => {
16406
- await safeAsync(PLUGIN_NAME18, "event", async () => {
16620
+ await safeAsync(PLUGIN_NAME19, "event", async () => {
16407
16621
  const e = event;
16408
16622
  if (!e || typeof e.type !== "string")
16409
16623
  return;
@@ -16417,7 +16631,7 @@ var terminalMonitorServer = async (ctx) => {
16417
16631
  _lastNotification = msg;
16418
16632
  }
16419
16633
  });
16420
- safeWriteLog(PLUGIN_NAME18, {
16634
+ safeWriteLog(PLUGIN_NAME19, {
16421
16635
  hook: "event",
16422
16636
  type: e.type,
16423
16637
  notified: r.notified,
@@ -16425,18 +16639,18 @@ var terminalMonitorServer = async (ctx) => {
16425
16639
  findings: r.notification?.findings.length ?? 0
16426
16640
  });
16427
16641
  if (r.notified && r.notification) {
16428
- log9.info(`[${PLUGIN_NAME18}] 反馈 ${r.notification.findings.length} 条 → ${r.notification.summary}`);
16642
+ log9.info(`[${PLUGIN_NAME19}] 反馈 ${r.notification.findings.length} 条 → ${r.notification.summary}`);
16429
16643
  }
16430
16644
  });
16431
16645
  }
16432
16646
  };
16433
16647
  };
16434
- var handler18 = terminalMonitorServer;
16648
+ var handler19 = terminalMonitorServer;
16435
16649
 
16436
16650
  // plugins/token-manager.ts
16437
16651
  init_opencode_plugin_helpers();
16438
- var PLUGIN_NAME19 = "token-manager";
16439
- logLifecycle(PLUGIN_NAME19, "import", {});
16652
+ var PLUGIN_NAME20 = "token-manager";
16653
+ logLifecycle(PLUGIN_NAME20, "import", {});
16440
16654
  async function handleMessageBefore(raw, log10, defaults) {
16441
16655
  const ctx = raw ?? {};
16442
16656
  if (!Array.isArray(ctx.messages) || ctx.messages.length === 0)
@@ -16456,21 +16670,21 @@ async function handleMessageBefore(raw, log10, defaults) {
16456
16670
  };
16457
16671
  if (r.compressed) {
16458
16672
  ctx.messages = r.messages;
16459
- log10?.info(`[${PLUGIN_NAME19}] 压缩 ${r.before.count}→${r.after.count} 条 / ${r.before.tokens}→${r.after.tokens} tokens`);
16673
+ log10?.info(`[${PLUGIN_NAME20}] 压缩 ${r.before.count}→${r.after.count} 条 / ${r.before.tokens}→${r.after.tokens} tokens`);
16460
16674
  }
16461
16675
  return r;
16462
16676
  } catch (err) {
16463
- log10?.warn(`[${PLUGIN_NAME19}] 压缩异常(已隔离)`, {
16677
+ log10?.warn(`[${PLUGIN_NAME20}] 压缩异常(已隔离)`, {
16464
16678
  error: err instanceof Error ? err.message : String(err)
16465
16679
  });
16466
16680
  return null;
16467
16681
  }
16468
16682
  }
16469
- var log10 = makePluginLogger(PLUGIN_NAME19);
16683
+ var log10 = makePluginLogger(PLUGIN_NAME20);
16470
16684
  var tokenManagerServer = async (ctx) => {
16471
16685
  const rt = loadRuntimeSync();
16472
16686
  const threshold = rt.runtime.context.condenser_threshold_ratio;
16473
- logLifecycle(PLUGIN_NAME19, "activate", {
16687
+ logLifecycle(PLUGIN_NAME20, "activate", {
16474
16688
  directory: ctx.directory,
16475
16689
  threshold,
16476
16690
  target: DEFAULT_CONDENSE.target,
@@ -16479,7 +16693,7 @@ var tokenManagerServer = async (ctx) => {
16479
16693
  });
16480
16694
  return {
16481
16695
  "experimental.chat.messages.transform": async (_input, output) => {
16482
- await safeAsync(PLUGIN_NAME19, "experimental.chat.messages.transform", async () => {
16696
+ await safeAsync(PLUGIN_NAME20, "experimental.chat.messages.transform", async () => {
16483
16697
  const list = output.messages;
16484
16698
  if (!Array.isArray(list) || list.length === 0)
16485
16699
  return;
@@ -16492,7 +16706,7 @@ var tokenManagerServer = async (ctx) => {
16492
16706
  const r = await handleMessageBefore({ messages: flat }, log10, { threshold });
16493
16707
  if (!r)
16494
16708
  return;
16495
- safeWriteLog(PLUGIN_NAME19, {
16709
+ safeWriteLog(PLUGIN_NAME20, {
16496
16710
  hook: "experimental.chat.messages.transform",
16497
16711
  mode: "observe-only",
16498
16712
  before_msgs: r.before.count,
@@ -16503,13 +16717,13 @@ var tokenManagerServer = async (ctx) => {
16503
16717
  reason: r.reason
16504
16718
  });
16505
16719
  if (r.compressed) {
16506
- log10.warn(`[${PLUGIN_NAME19}] advise condense: ${r.before.count}→${r.after.count} msgs / ${r.before.tokens}→${r.after.tokens} tokens (observe-only, no write-back to avoid opencode message schema mismatch)`);
16720
+ log10.warn(`[${PLUGIN_NAME20}] advise condense: ${r.before.count}→${r.after.count} msgs / ${r.before.tokens}→${r.after.tokens} tokens (observe-only, no write-back to avoid opencode message schema mismatch)`);
16507
16721
  }
16508
16722
  });
16509
16723
  }
16510
16724
  };
16511
16725
  };
16512
- var handler19 = tokenManagerServer;
16726
+ var handler20 = tokenManagerServer;
16513
16727
 
16514
16728
  // plugins/tool-policy.ts
16515
16729
  init_opencode_plugin_helpers();
@@ -16752,8 +16966,8 @@ function checkFileAccess(acl, file, op) {
16752
16966
  }
16753
16967
 
16754
16968
  // plugins/tool-policy.ts
16755
- var PLUGIN_NAME20 = "tool-policy";
16756
- logLifecycle(PLUGIN_NAME20, "import", {});
16969
+ var PLUGIN_NAME21 = "tool-policy";
16970
+ logLifecycle(PLUGIN_NAME21, "import", {});
16757
16971
  var EMPTY_ACL = { whitelistMode: false };
16758
16972
  function decideToolCall(ctx, cfg = {}) {
16759
16973
  const fallbackMode = cfg.defaultMode ?? DEFAULT_RUNTIME.autonomy.default_mode;
@@ -16805,13 +17019,13 @@ function classifyToolKind(toolName) {
16805
17019
  return "webfetch";
16806
17020
  return "other";
16807
17021
  }
16808
- var log11 = makePluginLogger(PLUGIN_NAME20);
17022
+ var log11 = makePluginLogger(PLUGIN_NAME21);
16809
17023
  var toolPolicyServer = async (ctx) => {
16810
17024
  const directory = ctx.directory ?? process.cwd();
16811
17025
  const cfg = await loadPolicy(directory);
16812
17026
  const rt = loadRuntimeSync();
16813
17027
  cfg.defaultMode = rt.runtime.autonomy.default_mode;
16814
- logLifecycle(PLUGIN_NAME20, "activate", {
17028
+ logLifecycle(PLUGIN_NAME21, "activate", {
16815
17029
  directory,
16816
17030
  acl_loaded: !!cfg.acl,
16817
17031
  default_mode: cfg.defaultMode,
@@ -16819,7 +17033,7 @@ var toolPolicyServer = async (ctx) => {
16819
17033
  });
16820
17034
  return {
16821
17035
  "tool.execute.before": async (input, output) => {
16822
- await safeAsync(PLUGIN_NAME20, "tool.execute.before", async () => {
17036
+ await safeAsync(PLUGIN_NAME21, "tool.execute.before", async () => {
16823
17037
  const toolName = input.tool;
16824
17038
  const argsObj = output.args ?? {};
16825
17039
  const files = [];
@@ -16835,7 +17049,7 @@ var toolPolicyServer = async (ctx) => {
16835
17049
  mode: cfg.defaultMode,
16836
17050
  files: files.length ? files : undefined
16837
17051
  }, cfg);
16838
- safeWriteLog(PLUGIN_NAME20, {
17052
+ safeWriteLog(PLUGIN_NAME21, {
16839
17053
  hook: "tool.execute.before",
16840
17054
  tool: toolName,
16841
17055
  callID: input.callID,
@@ -16844,19 +17058,19 @@ var toolPolicyServer = async (ctx) => {
16844
17058
  reasons: decision.reasons
16845
17059
  });
16846
17060
  if (decision.action === "deny") {
16847
- log11.warn(`[${PLUGIN_NAME20}] DENY ${toolName}: ${decision.reasons.join("; ")}`);
17061
+ log11.warn(`[${PLUGIN_NAME21}] DENY ${toolName}: ${decision.reasons.join("; ")}`);
16848
17062
  } else if (decision.action === "confirm") {
16849
- log11.info(`[${PLUGIN_NAME20}] CONFIRM ${toolName}: ${decision.reasons.join("; ")}`);
17063
+ log11.info(`[${PLUGIN_NAME21}] CONFIRM ${toolName}: ${decision.reasons.join("; ")}`);
16850
17064
  }
16851
17065
  });
16852
17066
  }
16853
17067
  };
16854
17068
  };
16855
- var handler20 = toolPolicyServer;
17069
+ var handler21 = toolPolicyServer;
16856
17070
 
16857
17071
  // plugins/update-checker.ts
16858
17072
  init_opencode_plugin_helpers();
16859
- import { existsSync as existsSync4 } from "node:fs";
17073
+ import { existsSync as existsSync5 } from "node:fs";
16860
17074
  import { homedir as homedir7 } from "node:os";
16861
17075
  import { join as join15 } from "node:path";
16862
17076
 
@@ -16864,7 +17078,7 @@ import { join as join15 } from "node:path";
16864
17078
  import { createHash as createHash6 } from "node:crypto";
16865
17079
  import {
16866
17080
  copyFileSync,
16867
- existsSync as existsSync3,
17081
+ existsSync as existsSync4,
16868
17082
  mkdirSync as mkdirSync3,
16869
17083
  mkdtempSync,
16870
17084
  readFileSync as readFileSync4,
@@ -16875,7 +17089,7 @@ import {
16875
17089
  writeFileSync as writeFileSync2
16876
17090
  } from "node:fs";
16877
17091
  import { homedir as homedir6, tmpdir } from "node:os";
16878
- import { dirname as dirname6, join as join14 } from "node:path";
17092
+ import { dirname as dirname7, join as join14 } from "node:path";
16879
17093
  import { fileURLToPath } from "node:url";
16880
17094
  import * as https from "node:https";
16881
17095
  import * as zlib from "node:zlib";
@@ -16883,7 +17097,7 @@ import * as zlib from "node:zlib";
16883
17097
  // lib/version-injected.ts
16884
17098
  function getInjectedVersion() {
16885
17099
  try {
16886
- const v = "0.3.7";
17100
+ const v = "0.3.8";
16887
17101
  if (typeof v === "string" && /^\d+\.\d+\.\d+/.test(v)) {
16888
17102
  return v;
16889
17103
  }
@@ -16972,7 +17186,7 @@ function readLocalVersion() {
16972
17186
  return injected;
16973
17187
  try {
16974
17188
  const here = fileURLToPath(import.meta.url);
16975
- const root = dirname6(dirname6(here));
17189
+ const root = dirname7(dirname7(here));
16976
17190
  const pkg = JSON.parse(readFileSync4(join14(root, "package.json"), "utf8"));
16977
17191
  return typeof pkg.version === "string" ? pkg.version : "0.0.0";
16978
17192
  } catch {
@@ -16987,7 +17201,7 @@ function defaultCacheFile() {
16987
17201
  }
16988
17202
  function readCache(file) {
16989
17203
  try {
16990
- if (!existsSync3(file))
17204
+ if (!existsSync4(file))
16991
17205
  return null;
16992
17206
  const raw = readFileSync4(file, "utf8");
16993
17207
  const obj = JSON.parse(raw);
@@ -17001,7 +17215,7 @@ function readCache(file) {
17001
17215
  }
17002
17216
  function writeCache(file, entry) {
17003
17217
  try {
17004
- mkdirSync3(dirname6(file), { recursive: true });
17218
+ mkdirSync3(dirname7(file), { recursive: true });
17005
17219
  writeFileSync2(file, JSON.stringify(entry, null, 2), "utf8");
17006
17220
  } catch {}
17007
17221
  }
@@ -17020,7 +17234,7 @@ function fetchLatestTagFromGitHub(repo) {
17020
17234
  });
17021
17235
  }
17022
17236
  function getJsonWithRedirect(url, hopsLeft) {
17023
- return new Promise((resolve10, reject) => {
17237
+ return new Promise((resolve11, reject) => {
17024
17238
  const u = new URL(url);
17025
17239
  const headers = {
17026
17240
  "User-Agent": "codeforge-update-checker",
@@ -17044,12 +17258,12 @@ function getJsonWithRedirect(url, hopsLeft) {
17044
17258
  return;
17045
17259
  }
17046
17260
  const next = new URL(res.headers.location, url).toString();
17047
- getJsonWithRedirect(next, hopsLeft - 1).then(resolve10, reject);
17261
+ getJsonWithRedirect(next, hopsLeft - 1).then(resolve11, reject);
17048
17262
  return;
17049
17263
  }
17050
17264
  if (status === 404) {
17051
17265
  res.resume();
17052
- resolve10(null);
17266
+ resolve11(null);
17053
17267
  return;
17054
17268
  }
17055
17269
  if (status >= 400) {
@@ -17060,7 +17274,7 @@ function getJsonWithRedirect(url, hopsLeft) {
17060
17274
  let body = "";
17061
17275
  res.setEncoding("utf8");
17062
17276
  res.on("data", (chunk) => body += chunk);
17063
- res.on("end", () => resolve10(body));
17277
+ res.on("end", () => resolve11(body));
17064
17278
  });
17065
17279
  req.on("timeout", () => {
17066
17280
  req.destroy();
@@ -17100,7 +17314,7 @@ async function fetchLatestFromNpm(opts) {
17100
17314
  return { version, tarballUrl, integrity };
17101
17315
  }
17102
17316
  function defaultHttpFetcher(url, timeoutMs) {
17103
- return new Promise((resolve10, reject) => {
17317
+ return new Promise((resolve11, reject) => {
17104
17318
  const u = new URL(url);
17105
17319
  const headers = {
17106
17320
  "User-Agent": "codeforge-update-checker",
@@ -17117,7 +17331,7 @@ function defaultHttpFetcher(url, timeoutMs) {
17117
17331
  const status = res.statusCode ?? 0;
17118
17332
  if (status === 404) {
17119
17333
  res.resume();
17120
- resolve10(null);
17334
+ resolve11(null);
17121
17335
  return;
17122
17336
  }
17123
17337
  if (status >= 400) {
@@ -17128,7 +17342,7 @@ function defaultHttpFetcher(url, timeoutMs) {
17128
17342
  let body = "";
17129
17343
  res.setEncoding("utf8");
17130
17344
  res.on("data", (chunk) => body += chunk);
17131
- res.on("end", () => resolve10(body));
17345
+ res.on("end", () => resolve11(body));
17132
17346
  });
17133
17347
  req.on("timeout", () => {
17134
17348
  req.destroy();
@@ -17147,7 +17361,7 @@ async function downloadAndExtractBundle(opts) {
17147
17361
  const tarBuf = zlib.gunzipSync(tarballBuf);
17148
17362
  extractTarToDir(tarBuf, tmpRoot);
17149
17363
  const bundlePath = join14(tmpRoot, "package", "dist", "index.js");
17150
- if (!existsSync3(bundlePath)) {
17364
+ if (!existsSync4(bundlePath)) {
17151
17365
  throw new Error(`bundle_not_found: ${bundlePath}`);
17152
17366
  }
17153
17367
  return { bundlePath, extractDir: tmpRoot };
@@ -17187,7 +17401,7 @@ function extractTarToDir(tarBuf, destRoot) {
17187
17401
  if (typeFlag === "0" || typeFlag === "" || typeFlag === "\x00") {
17188
17402
  const fileBuf = tarBuf.subarray(offset, offset + size);
17189
17403
  const dest = join14(destRoot, fullName);
17190
- mkdirSync3(dirname6(dest), { recursive: true });
17404
+ mkdirSync3(dirname7(dest), { recursive: true });
17191
17405
  writeFileSync2(dest, fileBuf);
17192
17406
  } else if (typeFlag === "5") {
17193
17407
  mkdirSync3(join14(destRoot, fullName), { recursive: true });
@@ -17199,7 +17413,7 @@ function defaultBinaryFetcher(url) {
17199
17413
  return downloadBinary(url, 3);
17200
17414
  }
17201
17415
  function downloadBinary(url, hopsLeft) {
17202
- return new Promise((resolve10, reject) => {
17416
+ return new Promise((resolve11, reject) => {
17203
17417
  const u = new URL(url);
17204
17418
  const req = https.request({
17205
17419
  host: u.hostname,
@@ -17217,7 +17431,7 @@ function downloadBinary(url, hopsLeft) {
17217
17431
  return;
17218
17432
  }
17219
17433
  const next = new URL(res.headers.location, url).toString();
17220
- downloadBinary(next, hopsLeft - 1).then(resolve10, reject);
17434
+ downloadBinary(next, hopsLeft - 1).then(resolve11, reject);
17221
17435
  return;
17222
17436
  }
17223
17437
  if (status >= 400) {
@@ -17227,7 +17441,7 @@ function downloadBinary(url, hopsLeft) {
17227
17441
  }
17228
17442
  const chunks = [];
17229
17443
  res.on("data", (chunk) => chunks.push(chunk));
17230
- res.on("end", () => resolve10(Buffer.concat(chunks)));
17444
+ res.on("end", () => resolve11(Buffer.concat(chunks)));
17231
17445
  });
17232
17446
  req.on("timeout", () => {
17233
17447
  req.destroy();
@@ -17240,16 +17454,16 @@ function downloadBinary(url, hopsLeft) {
17240
17454
  function atomicReplaceBundle(opts) {
17241
17455
  const { source, target, oldVersion } = opts;
17242
17456
  const keep = opts.keepBackups ?? 3;
17243
- if (!existsSync3(source)) {
17457
+ if (!existsSync4(source)) {
17244
17458
  throw new Error(`atomic_source_missing: ${source}`);
17245
17459
  }
17246
- mkdirSync3(dirname6(target), { recursive: true });
17460
+ mkdirSync3(dirname7(target), { recursive: true });
17247
17461
  const newPath = `${target}.new`;
17248
17462
  const backupPath = `${target}.bak.${oldVersion}`;
17249
17463
  let strategy = "rename";
17250
17464
  try {
17251
17465
  copyFileSync(source, newPath);
17252
- if (existsSync3(target)) {
17466
+ if (existsSync4(target)) {
17253
17467
  try {
17254
17468
  renameSync(target, backupPath);
17255
17469
  } catch (e) {
@@ -17285,7 +17499,7 @@ function atomicReplaceBundle(opts) {
17285
17499
  return { backupPath, strategy };
17286
17500
  } catch (e) {
17287
17501
  try {
17288
- if (existsSync3(newPath))
17502
+ if (existsSync4(newPath))
17289
17503
  unlinkSync(newPath);
17290
17504
  } catch {}
17291
17505
  throw e;
@@ -17295,7 +17509,7 @@ function cleanupOldBackups(target, keep) {
17295
17509
  if (keep <= 0)
17296
17510
  return;
17297
17511
  try {
17298
- const dir = dirname6(target);
17512
+ const dir = dirname7(target);
17299
17513
  const base = target.substring(dir.length + 1);
17300
17514
  const prefix = `${base}.bak.`;
17301
17515
  const all = readdirSync(dir).filter((f) => f.startsWith(prefix)).map((f) => {
@@ -17323,7 +17537,7 @@ function loadCompatibility(opts) {
17323
17537
  return null;
17324
17538
  file = join14(root, "compatibility.json");
17325
17539
  }
17326
- if (!existsSync3(file))
17540
+ if (!existsSync4(file))
17327
17541
  return null;
17328
17542
  const raw = readFileSync4(file, "utf8");
17329
17543
  const obj = JSON.parse(raw);
@@ -17346,7 +17560,7 @@ function loadCompatibility(opts) {
17346
17560
  function inferPluginRoot() {
17347
17561
  try {
17348
17562
  const here = fileURLToPath(import.meta.url);
17349
- return dirname6(dirname6(here));
17563
+ return dirname7(dirname7(here));
17350
17564
  } catch {
17351
17565
  return null;
17352
17566
  }
@@ -17386,13 +17600,29 @@ function compareOpencodeVersion(opts) {
17386
17600
  }
17387
17601
 
17388
17602
  // plugins/update-checker.ts
17389
- var PLUGIN_NAME21 = "update-checker";
17603
+ var PLUGIN_NAME22 = "update-checker";
17390
17604
  var PLUGIN_VERSION2 = "2.0.0";
17391
- logLifecycle(PLUGIN_NAME21, "import", { version: PLUGIN_VERSION2 });
17605
+ logLifecycle(PLUGIN_NAME22, "import", { version: PLUGIN_VERSION2 });
17392
17606
  var updateCheckerServer = async (ctx) => {
17607
+ const yieldResult = shouldYieldToLocalPlugin({ directory: ctx.directory });
17608
+ if (yieldResult.yield) {
17609
+ safeWriteLog(PLUGIN_NAME22, {
17610
+ level: "info",
17611
+ msg: "dev_mode_yield_skip",
17612
+ reason: yieldResult.reason,
17613
+ markerPath: yieldResult.markerPath,
17614
+ detail: formatYieldLog(yieldResult)
17615
+ });
17616
+ logLifecycle(PLUGIN_NAME22, "activate", {
17617
+ yield_to_local: true,
17618
+ yield_reason: yieldResult.reason,
17619
+ skipped: "auto_install + background_check"
17620
+ });
17621
+ return {};
17622
+ }
17393
17623
  const rt = loadRuntimeSync();
17394
17624
  const u = rt.runtime.update;
17395
- logLifecycle(PLUGIN_NAME21, "activate", {
17625
+ logLifecycle(PLUGIN_NAME22, "activate", {
17396
17626
  version: PLUGIN_VERSION2,
17397
17627
  auto_check_enabled: u.auto_check_enabled,
17398
17628
  interval_hours: u.interval_hours,
@@ -17404,14 +17634,14 @@ var updateCheckerServer = async (ctx) => {
17404
17634
  repo_fallback: u.repo,
17405
17635
  config_source: "codeforge.json"
17406
17636
  });
17407
- await safeAsync(PLUGIN_NAME21, "opencode_version_check", async () => {
17637
+ await safeAsync(PLUGIN_NAME22, "opencode_version_check", async () => {
17408
17638
  const compat = loadCompatibility();
17409
17639
  const opencodeVer = detectOpencodeVersion();
17410
17640
  const verdict = compareOpencodeVersion({
17411
17641
  currentOpencodeVer: opencodeVer,
17412
17642
  compat
17413
17643
  });
17414
- safeWriteLog(PLUGIN_NAME21, {
17644
+ safeWriteLog(PLUGIN_NAME22, {
17415
17645
  level: "info",
17416
17646
  msg: "opencode_version_check",
17417
17647
  opencodeVer,
@@ -17428,14 +17658,14 @@ var updateCheckerServer = async (ctx) => {
17428
17658
  }
17429
17659
  });
17430
17660
  if (!u.auto_check_enabled) {
17431
- safeWriteLog(PLUGIN_NAME21, {
17661
+ safeWriteLog(PLUGIN_NAME22, {
17432
17662
  level: "info",
17433
17663
  msg: "auto-check disabled (codeforge.json update.auto_check_enabled=false)"
17434
17664
  });
17435
17665
  return {};
17436
17666
  }
17437
17667
  setImmediate(() => {
17438
- safeAsync(PLUGIN_NAME21, "checkAndMaybeUpdate", async () => {
17668
+ safeAsync(PLUGIN_NAME22, "checkAndMaybeUpdate", async () => {
17439
17669
  const local = readLocalVersion();
17440
17670
  let npmResult = null;
17441
17671
  try {
@@ -17446,7 +17676,7 @@ var updateCheckerServer = async (ctx) => {
17446
17676
  timeoutMs: 5000
17447
17677
  });
17448
17678
  } catch (e) {
17449
- safeWriteLog(PLUGIN_NAME21, {
17679
+ safeWriteLog(PLUGIN_NAME22, {
17450
17680
  level: "warn",
17451
17681
  msg: "npm_fetch_failed",
17452
17682
  error: e.message
@@ -17455,7 +17685,7 @@ var updateCheckerServer = async (ctx) => {
17455
17685
  return;
17456
17686
  }
17457
17687
  if (!npmResult) {
17458
- safeWriteLog(PLUGIN_NAME21, {
17688
+ safeWriteLog(PLUGIN_NAME22, {
17459
17689
  level: "info",
17460
17690
  msg: "npm_no_release",
17461
17691
  package: u.package,
@@ -17464,7 +17694,7 @@ var updateCheckerServer = async (ctx) => {
17464
17694
  return;
17465
17695
  }
17466
17696
  const hasUpdate = cmpVersion(local, npmResult.version) < 0;
17467
- safeWriteLog(PLUGIN_NAME21, {
17697
+ safeWriteLog(PLUGIN_NAME22, {
17468
17698
  level: "info",
17469
17699
  msg: "npm_check_result",
17470
17700
  local,
@@ -17479,10 +17709,10 @@ var updateCheckerServer = async (ctx) => {
17479
17709
  更新命令:npx ${u.package} install --global`);
17480
17710
  return;
17481
17711
  }
17482
- await safeAsync(PLUGIN_NAME21, "auto_install_bundle", async () => {
17712
+ await safeAsync(PLUGIN_NAME22, "auto_install_bundle", async () => {
17483
17713
  const target = getOpencodeBundlePath();
17484
17714
  if (!target) {
17485
- safeWriteLog(PLUGIN_NAME21, {
17715
+ safeWriteLog(PLUGIN_NAME22, {
17486
17716
  level: "warn",
17487
17717
  msg: "auto_install_skip",
17488
17718
  reason: "无法定位 opencode bundle 路径"
@@ -17501,7 +17731,7 @@ var updateCheckerServer = async (ctx) => {
17501
17731
  oldVersion: local,
17502
17732
  keepBackups: u.backup_keep
17503
17733
  });
17504
- safeWriteLog(PLUGIN_NAME21, {
17734
+ safeWriteLog(PLUGIN_NAME22, {
17505
17735
  level: "info",
17506
17736
  msg: "auto_install_success",
17507
17737
  local,
@@ -17535,13 +17765,13 @@ function getOpencodeBundlePath() {
17535
17765
  candidates.push(join15(localAppData, "opencode", "codeforge", "index.js"));
17536
17766
  }
17537
17767
  for (const c of candidates) {
17538
- if (existsSync4(c))
17768
+ if (existsSync5(c))
17539
17769
  return c;
17540
17770
  }
17541
17771
  return candidates[0] ?? null;
17542
17772
  }
17543
17773
  async function fallbackToGitHubReleases(ctx, u) {
17544
- await safeAsync(PLUGIN_NAME21, "github_fallback", async () => {
17774
+ await safeAsync(PLUGIN_NAME22, "github_fallback", async () => {
17545
17775
  const result = await checkUpdateOnce({
17546
17776
  repo: u.repo,
17547
17777
  intervalMs: u.interval_hours * 3600 * 1000
@@ -17551,7 +17781,7 @@ async function fallbackToGitHubReleases(ctx, u) {
17551
17781
  }
17552
17782
  async function reportLegacyResult(ctx, result, repo) {
17553
17783
  if (result.error && !result.fromCache) {
17554
- safeWriteLog(PLUGIN_NAME21, {
17784
+ safeWriteLog(PLUGIN_NAME22, {
17555
17785
  level: "warn",
17556
17786
  msg: `update check failed: ${result.error}`,
17557
17787
  ...result
@@ -17559,7 +17789,7 @@ async function reportLegacyResult(ctx, result, repo) {
17559
17789
  return;
17560
17790
  }
17561
17791
  if (!result.hasUpdate) {
17562
- safeWriteLog(PLUGIN_NAME21, {
17792
+ safeWriteLog(PLUGIN_NAME22, {
17563
17793
  level: "info",
17564
17794
  msg: `up-to-date (local=${result.local}, remote=${result.remote}${result.fromCache ? ", from_cache" : ""})`,
17565
17795
  ...result
@@ -17569,7 +17799,7 @@ async function reportLegacyResult(ctx, result, repo) {
17569
17799
  const updateCmd = `bunx --bun github:${repo} install`;
17570
17800
  const toast = `[CodeForge] 有新版本:${result.local} → ${result.remote}
17571
17801
  更新命令:${updateCmd}`;
17572
- safeWriteLog(PLUGIN_NAME21, {
17802
+ safeWriteLog(PLUGIN_NAME22, {
17573
17803
  level: "info",
17574
17804
  msg: "new_version_available_github_fallback",
17575
17805
  local: result.local,
@@ -17580,17 +17810,17 @@ async function reportLegacyResult(ctx, result, repo) {
17580
17810
  await postToast(ctx, toast);
17581
17811
  }
17582
17812
  async function postToast(ctx, message) {
17583
- await safeAsync(PLUGIN_NAME21, "client.app.log", async () => {
17813
+ await safeAsync(PLUGIN_NAME22, "client.app.log", async () => {
17584
17814
  await ctx.client.app.log({
17585
17815
  body: {
17586
- service: PLUGIN_NAME21,
17816
+ service: PLUGIN_NAME22,
17587
17817
  level: "info",
17588
17818
  message
17589
17819
  }
17590
17820
  });
17591
17821
  });
17592
17822
  }
17593
- var handler21 = updateCheckerServer;
17823
+ var handler22 = updateCheckerServer;
17594
17824
 
17595
17825
  // plugins/workflow-engine.ts
17596
17826
  init_opencode_plugin_helpers();
@@ -18094,9 +18324,9 @@ async function runStepAutoFeedback(step, adapter) {
18094
18324
  }
18095
18325
 
18096
18326
  // plugins/workflow-engine.ts
18097
- var PLUGIN_NAME22 = "workflow-engine";
18098
- logLifecycle(PLUGIN_NAME22, "import", {});
18099
- var fallbackLog2 = makePluginLogger(PLUGIN_NAME22);
18327
+ var PLUGIN_NAME23 = "workflow-engine";
18328
+ logLifecycle(PLUGIN_NAME23, "import", {});
18329
+ var fallbackLog2 = makePluginLogger(PLUGIN_NAME23);
18100
18330
  var _registry = null;
18101
18331
  async function loadRegistry(workflowsDir) {
18102
18332
  const { loaded, failed } = await loadWorkflowsFromDir(workflowsDir);
@@ -18116,32 +18346,32 @@ async function handleCommandInvoked(raw, workflowsDir = "workflows") {
18116
18346
  const log12 = ctx.log ?? fallbackLog2;
18117
18347
  const command = typeof ctx.command === "string" ? ctx.command : null;
18118
18348
  if (!command) {
18119
- log12.warn(`[${PLUGIN_NAME22}] command.invoked 缺 command 字段`, ctx);
18349
+ log12.warn(`[${PLUGIN_NAME23}] command.invoked 缺 command 字段`, ctx);
18120
18350
  return null;
18121
18351
  }
18122
18352
  const reg = await ensureRegistry(workflowsDir);
18123
18353
  if (reg.errors.length) {
18124
- log12.warn(`[${PLUGIN_NAME22}] 有 ${reg.errors.length} 个 workflow 加载失败`, reg.errors);
18354
+ log12.warn(`[${PLUGIN_NAME23}] 有 ${reg.errors.length} 个 workflow 加载失败`, reg.errors);
18125
18355
  }
18126
18356
  const wf = reg.workflows.find((w) => matchesTrigger(w, command));
18127
18357
  if (!wf) {
18128
- log12.info(`[${PLUGIN_NAME22}] no workflow matches "${command}"`);
18358
+ log12.info(`[${PLUGIN_NAME23}] no workflow matches "${command}"`);
18129
18359
  return null;
18130
18360
  }
18131
- log12.info(`[${PLUGIN_NAME22}] dispatch "${command}" → workflow "${wf.name}"`);
18361
+ log12.info(`[${PLUGIN_NAME23}] dispatch "${command}" → workflow "${wf.name}"`);
18132
18362
  try {
18133
18363
  const result = await run(wf, {
18134
18364
  mode: ctx.adapter ? "real" : "dry_run",
18135
18365
  autonomy: ctx.autonomy ?? "semi",
18136
18366
  adapter: ctx.adapter
18137
18367
  });
18138
- log12.info(`[${PLUGIN_NAME22}] workflow "${wf.name}" 完成 (${result.plan.mode})`, {
18368
+ log12.info(`[${PLUGIN_NAME23}] workflow "${wf.name}" 完成 (${result.plan.mode})`, {
18139
18369
  steps: result.plan.steps.length,
18140
18370
  results: result.results.length
18141
18371
  });
18142
18372
  return result;
18143
18373
  } catch (err) {
18144
- log12.error(`[${PLUGIN_NAME22}] workflow "${wf.name}" 执行失败`, {
18374
+ log12.error(`[${PLUGIN_NAME23}] workflow "${wf.name}" 执行失败`, {
18145
18375
  error: err instanceof Error ? err.message : String(err)
18146
18376
  });
18147
18377
  throw err;
@@ -18150,15 +18380,15 @@ async function handleCommandInvoked(raw, workflowsDir = "workflows") {
18150
18380
  var workflowEngineServer = async (ctx) => {
18151
18381
  const directory = ctx.directory ?? process.cwd();
18152
18382
  const workflowsDir = path17.join(directory, "workflows");
18153
- ensureRegistry(workflowsDir).catch((err) => fallbackLog2.warn(`[${PLUGIN_NAME22}] preload workflows failed`, {
18383
+ ensureRegistry(workflowsDir).catch((err) => fallbackLog2.warn(`[${PLUGIN_NAME23}] preload workflows failed`, {
18154
18384
  error: err instanceof Error ? err.message : String(err)
18155
18385
  }));
18156
- logLifecycle(PLUGIN_NAME22, "activate", { directory, workflowsDir });
18386
+ logLifecycle(PLUGIN_NAME23, "activate", { directory, workflowsDir });
18157
18387
  return {
18158
18388
  "command.execute.before": async (input, output) => {
18159
- await safeAsync(PLUGIN_NAME22, "command.execute.before", async () => {
18389
+ await safeAsync(PLUGIN_NAME23, "command.execute.before", async () => {
18160
18390
  const cmd = input.command.startsWith("/") ? input.command : `/${input.command}`;
18161
- safeWriteLog(PLUGIN_NAME22, {
18391
+ safeWriteLog(PLUGIN_NAME23, {
18162
18392
  hook: "command.execute.before",
18163
18393
  command: cmd,
18164
18394
  sessionID: input.sessionID
@@ -18173,14 +18403,14 @@ var workflowEngineServer = async (ctx) => {
18173
18403
  });
18174
18404
  },
18175
18405
  "chat.message": async (input, output) => {
18176
- await safeAsync(PLUGIN_NAME22, "chat.message", async () => {
18406
+ await safeAsync(PLUGIN_NAME23, "chat.message", async () => {
18177
18407
  const text = extractUserText(output).trim();
18178
18408
  if (!text.startsWith("/"))
18179
18409
  return;
18180
18410
  const cmd = text.split(/\s+/)[0];
18181
18411
  if (!cmd)
18182
18412
  return;
18183
- safeWriteLog(PLUGIN_NAME22, {
18413
+ safeWriteLog(PLUGIN_NAME23, {
18184
18414
  hook: "chat.message",
18185
18415
  command: cmd,
18186
18416
  sessionID: input.sessionID
@@ -18190,7 +18420,7 @@ var workflowEngineServer = async (ctx) => {
18190
18420
  }
18191
18421
  };
18192
18422
  };
18193
- var handler22 = workflowEngineServer;
18423
+ var handler23 = workflowEngineServer;
18194
18424
 
18195
18425
  // src/index.ts
18196
18426
  var PLUGIN_ID = "codeforge";
@@ -18209,16 +18439,17 @@ var HANDLERS = [
18209
18439
  { name: "kh-reminder", init: handler11 },
18210
18440
  { name: "memories-context", init: handler12 },
18211
18441
  { name: "model-fallback", init: handler13 },
18212
- { name: "pwsh-utf8", init: handler14 },
18213
- { name: "session-recovery", init: handler15 },
18214
- { name: "subtask-heartbeat", init: handler16 },
18215
- { name: "subtasks", init: handler17 },
18216
- { name: "terminal-monitor", init: handler18 },
18217
- { name: "token-manager", init: handler19 },
18442
+ { name: "pwsh-utf8", init: handler16 },
18443
+ { name: "session-recovery", init: handler17 },
18444
+ { name: "subtask-heartbeat", init: handler14 },
18445
+ { name: "subtasks", init: handler18 },
18446
+ { name: "parallel-status", init: handler15 },
18447
+ { name: "terminal-monitor", init: handler19 },
18448
+ { name: "token-manager", init: handler20 },
18218
18449
  { name: "tool-heartbeat", init: handler7 },
18219
- { name: "tool-policy", init: handler20 },
18220
- { name: "update-checker", init: handler21 },
18221
- { name: "workflow-engine", init: handler22 }
18450
+ { name: "tool-policy", init: handler21 },
18451
+ { name: "update-checker", init: handler22 },
18452
+ { name: "workflow-engine", init: handler23 }
18222
18453
  ];
18223
18454
  function makeSerialHook(hookName, fns) {
18224
18455
  return async (input, output) => {
@@ -18233,69 +18464,91 @@ function makeSerialHook(hookName, fns) {
18233
18464
  }
18234
18465
  };
18235
18466
  }
18236
- var codeforgeServer = async (input) => {
18237
- const results = await Promise.allSettled(HANDLERS.map((h) => h.init(input)));
18238
- const hooksList = [];
18239
- results.forEach((r, i) => {
18240
- if (r.status === "fulfilled" && r.value && typeof r.value === "object") {
18241
- hooksList.push(r.value);
18242
- } else if (r.status === "rejected") {
18243
- log12.warn(`[${PLUGIN_ID}] handler ${HANDLERS[i].name} init failed (隔离,其他 handler 继续)`, { error: r.reason instanceof Error ? r.reason.message : String(r.reason) });
18467
+ function createCodeforgeServer(opts) {
18468
+ return async (input) => {
18469
+ if (opts.enableDevIsolation) {
18470
+ const yieldResult = shouldYieldToLocalPlugin({ directory: input.directory });
18471
+ if (yieldResult.yield) {
18472
+ const msg = formatYieldLog(yieldResult);
18473
+ log12.info(msg, { reason: yieldResult.reason, markerPath: yieldResult.markerPath });
18474
+ logLifecycle(PLUGIN_ID, "activate", {
18475
+ yield_to_local: true,
18476
+ yield_reason: yieldResult.reason,
18477
+ yield_marker_path: yieldResult.markerPath,
18478
+ directory: input.directory,
18479
+ handlers_total: HANDLERS.length,
18480
+ handlers_active: 0
18481
+ });
18482
+ return {};
18483
+ }
18244
18484
  }
18245
- });
18246
- logLifecycle(PLUGIN_ID, "activate", {
18247
- directory: input.directory,
18248
- handlers_total: HANDLERS.length,
18249
- handlers_active: hooksList.length,
18250
- handlers_failed: HANDLERS.length - hooksList.length
18251
- });
18252
- const chatMessageBucket = [];
18253
- const commandExecuteBeforeBucket = [];
18254
- const chatParamsBucket = [];
18255
- const toolExecuteBeforeBucket = [];
18256
- const chatMessagesTransformBucket = [];
18257
- const eventBucket = [];
18258
- const toolMerged = {};
18259
- for (const h of hooksList) {
18260
- if (h["chat.message"])
18261
- chatMessageBucket.push(h["chat.message"]);
18262
- if (h["command.execute.before"])
18263
- commandExecuteBeforeBucket.push(h["command.execute.before"]);
18264
- if (h["chat.params"])
18265
- chatParamsBucket.push(h["chat.params"]);
18266
- if (h["tool.execute.before"])
18267
- toolExecuteBeforeBucket.push(h["tool.execute.before"]);
18268
- if (h["experimental.chat.messages.transform"]) {
18269
- chatMessagesTransformBucket.push(h["experimental.chat.messages.transform"]);
18270
- }
18271
- if (h.event)
18272
- eventBucket.push(h.event);
18273
- if (h.tool)
18274
- Object.assign(toolMerged, h.tool);
18275
- }
18276
- return {
18277
- "chat.message": makeSerialHook("chat.message", chatMessageBucket),
18278
- "command.execute.before": makeSerialHook("command.execute.before", commandExecuteBeforeBucket),
18279
- "chat.params": makeSerialHook("chat.params", chatParamsBucket),
18280
- "tool.execute.before": makeSerialHook("tool.execute.before", toolExecuteBeforeBucket),
18281
- "experimental.chat.messages.transform": makeSerialHook("experimental.chat.messages.transform", chatMessagesTransformBucket),
18282
- event: async (envelope) => {
18283
- await Promise.all(eventBucket.map(async (fn) => {
18284
- try {
18285
- await fn(envelope);
18286
- } catch (err) {
18287
- log12.warn(`[${PLUGIN_ID}] event handler 异常(已隔离)`, {
18288
- error: err instanceof Error ? err.message : String(err)
18289
- });
18290
- }
18291
- }));
18292
- },
18293
- tool: toolMerged
18485
+ const results = await Promise.allSettled(HANDLERS.map((h) => h.init(input)));
18486
+ const hooksList = [];
18487
+ results.forEach((r, i) => {
18488
+ if (r.status === "fulfilled" && r.value && typeof r.value === "object") {
18489
+ hooksList.push(r.value);
18490
+ } else if (r.status === "rejected") {
18491
+ log12.warn(`[${PLUGIN_ID}] handler ${HANDLERS[i].name} init failed (隔离,其他 handler 继续)`, { error: r.reason instanceof Error ? r.reason.message : String(r.reason) });
18492
+ }
18493
+ });
18494
+ logLifecycle(PLUGIN_ID, "activate", {
18495
+ directory: input.directory,
18496
+ handlers_total: HANDLERS.length,
18497
+ handlers_active: hooksList.length,
18498
+ handlers_failed: HANDLERS.length - hooksList.length
18499
+ });
18500
+ const chatMessageBucket = [];
18501
+ const commandExecuteBeforeBucket = [];
18502
+ const chatParamsBucket = [];
18503
+ const toolExecuteBeforeBucket = [];
18504
+ const chatMessagesTransformBucket = [];
18505
+ const eventBucket = [];
18506
+ const toolMerged = {};
18507
+ for (const h of hooksList) {
18508
+ if (h["chat.message"])
18509
+ chatMessageBucket.push(h["chat.message"]);
18510
+ if (h["command.execute.before"])
18511
+ commandExecuteBeforeBucket.push(h["command.execute.before"]);
18512
+ if (h["chat.params"])
18513
+ chatParamsBucket.push(h["chat.params"]);
18514
+ if (h["tool.execute.before"])
18515
+ toolExecuteBeforeBucket.push(h["tool.execute.before"]);
18516
+ if (h["experimental.chat.messages.transform"]) {
18517
+ chatMessagesTransformBucket.push(h["experimental.chat.messages.transform"]);
18518
+ }
18519
+ if (h.event)
18520
+ eventBucket.push(h.event);
18521
+ if (h.tool)
18522
+ Object.assign(toolMerged, h.tool);
18523
+ }
18524
+ return {
18525
+ "chat.message": makeSerialHook("chat.message", chatMessageBucket),
18526
+ "command.execute.before": makeSerialHook("command.execute.before", commandExecuteBeforeBucket),
18527
+ "chat.params": makeSerialHook("chat.params", chatParamsBucket),
18528
+ "tool.execute.before": makeSerialHook("tool.execute.before", toolExecuteBeforeBucket),
18529
+ "experimental.chat.messages.transform": makeSerialHook("experimental.chat.messages.transform", chatMessagesTransformBucket),
18530
+ event: async (envelope) => {
18531
+ await Promise.all(eventBucket.map(async (fn) => {
18532
+ try {
18533
+ await fn(envelope);
18534
+ } catch (err) {
18535
+ log12.warn(`[${PLUGIN_ID}] event handler 异常(已隔离)`, {
18536
+ error: err instanceof Error ? err.message : String(err)
18537
+ });
18538
+ }
18539
+ }));
18540
+ },
18541
+ tool: toolMerged
18542
+ };
18294
18543
  };
18295
- };
18544
+ }
18545
+ var codeforgeServer = createCodeforgeServer({ enableDevIsolation: true });
18546
+ var codeforgeDevServer = createCodeforgeServer({ enableDevIsolation: false });
18296
18547
  var pluginModule = { id: PLUGIN_ID, server: codeforgeServer };
18297
18548
  var src_default = pluginModule;
18298
18549
  export {
18299
18550
  src_default as default,
18300
- codeforgeServer
18551
+ createCodeforgeServer,
18552
+ codeforgeServer,
18553
+ codeforgeDevServer
18301
18554
  };