@andyqiu/codeforge 0.3.5 → 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
  }
@@ -8965,6 +9006,7 @@ import { promises as fs2 } from "node:fs";
8965
9006
  import * as path4 from "node:path";
8966
9007
 
8967
9008
  // lib/channels.ts
9009
+ import { createHmac } from "node:crypto";
8968
9010
  var TEMPLATE_RE = /\$\{([a-zA-Z_][a-zA-Z0-9_.]*)(?:\|([^}]*))?\}/g;
8969
9011
  function renderChannelTemplate(template, ev) {
8970
9012
  const missing = new Set;
@@ -9078,6 +9120,10 @@ async function sendOne(ev, ch, deps) {
9078
9120
  return await sendKh(ev, ch, deps);
9079
9121
  case "mcp":
9080
9122
  return await sendMcp(ev, ch, deps);
9123
+ case "slack":
9124
+ return await sendSlack(ev, ch, deps);
9125
+ case "lark":
9126
+ return await sendLark(ev, ch, deps);
9081
9127
  }
9082
9128
  }
9083
9129
  async function sendWebhook(ev, ch, deps) {
@@ -9217,6 +9263,224 @@ async function sendMcp(ev, ch, deps) {
9217
9263
  error: r.ok ? undefined : r.error ?? "mcp 调用失败"
9218
9264
  };
9219
9265
  }
9266
+ function transformSlackToWebhook(ch, ev) {
9267
+ const missing = new Set;
9268
+ const titleTpl = ch.title_template ?? "${title|}";
9269
+ const msgTpl = ch.message_template ?? "${message|}";
9270
+ const title = renderChannelTemplate(titleTpl, ev);
9271
+ const message = renderChannelTemplate(msgTpl, ev);
9272
+ title.missing.forEach((k) => missing.add(k));
9273
+ message.missing.forEach((k) => missing.add(k));
9274
+ const titleText = title.rendered.trim() ? title.rendered : ev.event;
9275
+ const mentionText = (ch.mentions ?? []).map((id) => id.startsWith("@") || id.startsWith("<") ? id : `<@${id}>`).join(" ");
9276
+ const color = severityToSlackColor(ev.severity);
9277
+ const blocks = [
9278
+ {
9279
+ type: "header",
9280
+ text: { type: "plain_text", text: titleText.slice(0, 150), emoji: true }
9281
+ }
9282
+ ];
9283
+ if (message.rendered.trim()) {
9284
+ blocks.push({
9285
+ type: "section",
9286
+ text: { type: "mrkdwn", text: message.rendered.slice(0, 3000) }
9287
+ });
9288
+ }
9289
+ if (mentionText) {
9290
+ blocks.push({
9291
+ type: "context",
9292
+ elements: [{ type: "mrkdwn", text: mentionText }]
9293
+ });
9294
+ }
9295
+ const footerParts = [`event=\`${ev.event}\``];
9296
+ if (ev.session_id)
9297
+ footerParts.push(`session=\`${ev.session_id.slice(0, 8)}\``);
9298
+ footerParts.push(`ts=<!date^${Math.floor(ev.timestamp / 1000)}^{date_short_pretty} {time}|${new Date(ev.timestamp).toISOString()}>`);
9299
+ blocks.push({
9300
+ type: "context",
9301
+ elements: [{ type: "mrkdwn", text: footerParts.join(" · ") }]
9302
+ });
9303
+ const payload = {
9304
+ attachments: [{ color, blocks }]
9305
+ };
9306
+ if (ch.channel)
9307
+ payload.channel = ch.channel;
9308
+ if (ch.username)
9309
+ payload.username = ch.username;
9310
+ if (ch.icon_emoji)
9311
+ payload.icon_emoji = ch.icon_emoji;
9312
+ return { body: JSON.stringify(payload), missing: [...missing] };
9313
+ }
9314
+ function severityToSlackColor(sev) {
9315
+ if (sev === undefined)
9316
+ return "#cccccc";
9317
+ if (sev >= 40)
9318
+ return "#d50000";
9319
+ if (sev >= 30)
9320
+ return "#e91e63";
9321
+ if (sev >= 20)
9322
+ return "#ff9800";
9323
+ if (sev >= 10)
9324
+ return "#36a64f";
9325
+ return "#cccccc";
9326
+ }
9327
+ async function sendSlack(ev, ch, deps) {
9328
+ if (!deps.fetch) {
9329
+ return { name: ch.name, type: "slack", status: "error", error: "deps.fetch 未注入" };
9330
+ }
9331
+ const { body } = transformSlackToWebhook(ch, ev);
9332
+ try {
9333
+ const resp = await deps.fetch(ch.webhook_url, {
9334
+ method: "POST",
9335
+ headers: { "Content-Type": "application/json" },
9336
+ body
9337
+ });
9338
+ const ok = resp.status >= 200 && resp.status < 300;
9339
+ if (ok) {
9340
+ return {
9341
+ name: ch.name,
9342
+ type: "slack",
9343
+ status: "sent",
9344
+ http_status: resp.status,
9345
+ rendered: body
9346
+ };
9347
+ }
9348
+ return {
9349
+ name: ch.name,
9350
+ type: "slack",
9351
+ status: "error",
9352
+ http_status: resp.status,
9353
+ error: `slack returned ${resp.status}: ${resp.body?.slice(0, 200) ?? ""}`,
9354
+ rendered: body
9355
+ };
9356
+ } catch (err) {
9357
+ return {
9358
+ name: ch.name,
9359
+ type: "slack",
9360
+ status: "error",
9361
+ error: describe2(err),
9362
+ rendered: body
9363
+ };
9364
+ }
9365
+ }
9366
+ function transformLarkToWebhook(ch, ev) {
9367
+ const missing = new Set;
9368
+ const titleTpl = ch.title_template ?? "${title|}";
9369
+ const msgTpl = ch.message_template ?? "${message|}";
9370
+ const titleR = renderChannelTemplate(titleTpl, ev);
9371
+ const msgR = renderChannelTemplate(msgTpl, ev);
9372
+ titleR.missing.forEach((k) => missing.add(k));
9373
+ msgR.missing.forEach((k) => missing.add(k));
9374
+ const titleText = titleR.rendered.trim() || ev.event;
9375
+ const messageText = msgR.rendered.trim();
9376
+ const mentionMarkdown = (ch.mentions ?? []).map((id) => {
9377
+ if (id === "@all" || id === "all")
9378
+ return '<at user_id="all"></at>';
9379
+ if (id.startsWith("<at"))
9380
+ return id;
9381
+ return `<at user_id="${id}"></at>`;
9382
+ }).join(" ");
9383
+ const headerTemplate = severityToLarkHeader(ev.severity);
9384
+ const elements = [];
9385
+ if (messageText || mentionMarkdown) {
9386
+ const fullMsg = [messageText, mentionMarkdown].filter(Boolean).join(`
9387
+
9388
+ `);
9389
+ elements.push({
9390
+ tag: "div",
9391
+ text: { tag: "lark_md", content: fullMsg.slice(0, 3000) }
9392
+ });
9393
+ }
9394
+ const footer = [
9395
+ `**event**: \`${ev.event}\``,
9396
+ ev.session_id ? `**session**: \`${ev.session_id.slice(0, 8)}\`` : null,
9397
+ `**ts**: ${new Date(ev.timestamp).toISOString()}`
9398
+ ].filter(Boolean).join(" · ");
9399
+ elements.push({
9400
+ tag: "note",
9401
+ elements: [{ tag: "lark_md", content: footer }]
9402
+ });
9403
+ const payload = {
9404
+ msg_type: "interactive",
9405
+ card: {
9406
+ config: { wide_screen_mode: true },
9407
+ header: {
9408
+ template: headerTemplate,
9409
+ title: { tag: "plain_text", content: titleText.slice(0, 150) }
9410
+ },
9411
+ elements
9412
+ }
9413
+ };
9414
+ return { body: JSON.stringify(payload), missing: [...missing] };
9415
+ }
9416
+ function severityToLarkHeader(sev) {
9417
+ if (sev === undefined)
9418
+ return "grey";
9419
+ if (sev >= 40)
9420
+ return "carmine";
9421
+ if (sev >= 30)
9422
+ return "red";
9423
+ if (sev >= 20)
9424
+ return "orange";
9425
+ if (sev >= 10)
9426
+ return "blue";
9427
+ return "grey";
9428
+ }
9429
+ function computeLarkSign(secret, timestampSec) {
9430
+ const stringToSign = `${timestampSec}
9431
+ ${secret}`;
9432
+ const hmac = createHmac("sha256", stringToSign);
9433
+ hmac.update("");
9434
+ return hmac.digest("base64");
9435
+ }
9436
+ async function sendLark(ev, ch, deps) {
9437
+ if (!deps.fetch) {
9438
+ return { name: ch.name, type: "lark", status: "error", error: "deps.fetch 未注入" };
9439
+ }
9440
+ const { body: cardBody } = transformLarkToWebhook(ch, ev);
9441
+ let body = cardBody;
9442
+ if (ch.secret) {
9443
+ const tsSec = Math.floor((deps.now ? deps.now() : Date.now()) / 1000);
9444
+ const sign = computeLarkSign(ch.secret, tsSec);
9445
+ const parsed = JSON.parse(cardBody);
9446
+ parsed.timestamp = String(tsSec);
9447
+ parsed.sign = sign;
9448
+ body = JSON.stringify(parsed);
9449
+ }
9450
+ try {
9451
+ const resp = await deps.fetch(ch.webhook_url, {
9452
+ method: "POST",
9453
+ headers: { "Content-Type": "application/json" },
9454
+ body
9455
+ });
9456
+ const ok = resp.status >= 200 && resp.status < 300;
9457
+ if (ok) {
9458
+ return {
9459
+ name: ch.name,
9460
+ type: "lark",
9461
+ status: "sent",
9462
+ http_status: resp.status,
9463
+ rendered: body
9464
+ };
9465
+ }
9466
+ return {
9467
+ name: ch.name,
9468
+ type: "lark",
9469
+ status: "error",
9470
+ http_status: resp.status,
9471
+ error: `lark returned ${resp.status}: ${resp.body?.slice(0, 200) ?? ""}`,
9472
+ rendered: body
9473
+ };
9474
+ } catch (err) {
9475
+ return {
9476
+ name: ch.name,
9477
+ type: "lark",
9478
+ status: "error",
9479
+ error: describe2(err),
9480
+ rendered: body
9481
+ };
9482
+ }
9483
+ }
9220
9484
  function dedupeTags(defaults, evTags) {
9221
9485
  const set = new Set;
9222
9486
  if (defaults) {
@@ -11141,6 +11405,51 @@ function applyFocus(ranked, focus) {
11141
11405
  const rest = others.filter((n) => !isAdj(n.rel));
11142
11406
  return [center, ...adj, ...rest];
11143
11407
  }
11408
+ function renderMermaid(map, opts = {}) {
11409
+ const top = opts.top ?? 20;
11410
+ const dir = opts.direction ?? "LR";
11411
+ const focus = opts.focus ? toPosix(opts.focus.replace(/^\.\//, "")) : undefined;
11412
+ const sorted = [...map.ranked].sort((a, b) => b.score - a.score).slice(0, top);
11413
+ const idMap = new Map;
11414
+ for (const f of sorted) {
11415
+ idMap.set(f.rel, "n" + sha1Short(f.rel));
11416
+ }
11417
+ const lines = [];
11418
+ lines.push(`flowchart ${dir}`);
11419
+ for (const f of sorted) {
11420
+ const id = idMap.get(f.rel);
11421
+ const basename = f.rel.split(/[\\/]/).pop() ?? f.rel;
11422
+ const label = `${escapeLabel(basename)}<br/><small>${f.score.toFixed(2)}</small>`;
11423
+ lines.push(` ${id}["${label}"]`);
11424
+ }
11425
+ const topSet = new Set(sorted.map((f) => f.rel));
11426
+ for (const f of sorted) {
11427
+ const from = idMap.get(f.rel);
11428
+ for (const dep of f.deps ?? []) {
11429
+ if (topSet.has(dep)) {
11430
+ const to = idMap.get(dep);
11431
+ lines.push(` ${from} --> ${to}`);
11432
+ }
11433
+ }
11434
+ }
11435
+ if (focus && idMap.has(focus)) {
11436
+ lines.push(` classDef focus fill:#ff9800,stroke:#e65100,color:#fff`);
11437
+ lines.push(` ${idMap.get(focus)}:::focus`);
11438
+ }
11439
+ return lines.join(`
11440
+ `);
11441
+ }
11442
+ function sha1Short(s) {
11443
+ let h = 2166136261 >>> 0;
11444
+ for (let i = 0;i < s.length; i++) {
11445
+ h ^= s.charCodeAt(i);
11446
+ h = Math.imul(h, 16777619) >>> 0;
11447
+ }
11448
+ return h.toString(16).padStart(8, "0");
11449
+ }
11450
+ function escapeLabel(s) {
11451
+ return s.replace(/["`]/g, "'");
11452
+ }
11144
11453
 
11145
11454
  // tools/repo-map.ts
11146
11455
  var description17 = [
@@ -11149,6 +11458,7 @@ var description17 = [
11149
11458
  "- planner agent 接到新需求 → 先 smart_search → 再 repo-map 找代码入口",
11150
11459
  "- 用户问「这个项目怎么组织的 / 入口在哪 / 哪些是核心模块」",
11151
11460
  "- 跨多个文件的重构前,先用 focus= 看清依赖关系",
11461
+ '- 想要可视化依赖图时传 format="mermaid"(Markdown 渲染器会自动出图)',
11152
11462
  "**何时不需要**:",
11153
11463
  "- 用户已指明确切文件,且只在该文件内改动",
11154
11464
  "- 项目地图本会话已生成且未发生大改"
@@ -11159,6 +11469,7 @@ var ArgsSchema17 = z18.object({
11159
11469
  top: z18.number().int().min(1).max(100).optional().describe("展示 top N 文件,默认 20;想要全图可传 100"),
11160
11470
  focus: z18.string().optional().describe("聚焦文件(仓内 POSIX 相对路径):把它和它的直接依赖 / 反向依赖排在前面"),
11161
11471
  max_files: z18.number().int().min(10).max(5000).optional().describe("扫描文件数上限,默认 500;超过自动截断"),
11472
+ format: z18.enum(["markdown", "mermaid", "both"]).optional().describe("输出格式:markdown(默认,文字 + 星级评分)/mermaid(flowchart 流程图,可贴 mermaid.live 渲染)/both(两者,方便对照)"),
11162
11473
  _raw: z18.boolean().optional()
11163
11474
  });
11164
11475
  async function execute17(input) {
@@ -11171,19 +11482,32 @@ async function execute17(input) {
11171
11482
  };
11172
11483
  }
11173
11484
  const args = parsed.data;
11485
+ const format = args.format ?? "markdown";
11174
11486
  try {
11175
11487
  const map = await buildRepoMap({
11176
11488
  root: args.root,
11177
11489
  maxFiles: args.max_files
11178
11490
  });
11179
- const md = renderMarkdown(map, {
11180
- top: args.top,
11181
- focus: args.focus
11182
- });
11491
+ let body;
11492
+ if (format === "markdown") {
11493
+ body = renderMarkdown(map, { top: args.top, focus: args.focus });
11494
+ } else if (format === "mermaid") {
11495
+ const mmd = renderMermaid(map, { top: args.top, focus: args.focus });
11496
+ body = "```mermaid\n" + mmd + "\n```";
11497
+ } else {
11498
+ const md = renderMarkdown(map, { top: args.top, focus: args.focus });
11499
+ const mmd = renderMermaid(map, { top: args.top, focus: args.focus });
11500
+ body = md + `
11501
+
11502
+ ## Dependency Graph
11503
+
11504
+ \`\`\`mermaid
11505
+ ` + mmd + "\n```";
11506
+ }
11183
11507
  const truncated = args.max_files ? map.totalFiles >= args.max_files : map.totalFiles >= 500;
11184
11508
  return {
11185
11509
  ok: true,
11186
- markdown: md,
11510
+ markdown: body,
11187
11511
  raw: args._raw ? map : undefined,
11188
11512
  stats: {
11189
11513
  totalFiles: map.totalFiles,
@@ -14448,112 +14772,405 @@ var modelFallbackServer = async (ctx) => {
14448
14772
  };
14449
14773
  var handler13 = modelFallbackServer;
14450
14774
 
14451
- // plugins/pwsh-utf8.ts
14775
+ // plugins/subtask-heartbeat.ts
14452
14776
  init_opencode_plugin_helpers();
14453
- var PLUGIN_NAME14 = "pwsh-utf8";
14777
+ var PLUGIN_NAME14 = "subtask-heartbeat";
14454
14778
  logLifecycle(PLUGIN_NAME14, "import", {});
14455
- var PRELUDE = "chcp 65001 *> $null; " + "[Console]::OutputEncoding = [System.Text.UTF8Encoding]::new(); " + "$OutputEncoding = [System.Text.UTF8Encoding]::new(); ";
14456
- function prependUtf8Prelude(command) {
14457
- if (typeof command !== "string")
14458
- return;
14459
- if (command.length === 0)
14460
- return command;
14461
- if (/chcp\s+65001/i.test(command))
14462
- return command;
14463
- if (/\[Console\]::OutputEncoding\s*=/i.test(command))
14464
- return command;
14465
- return PRELUDE + command;
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 }));
14466
14786
  }
14467
- var handler14 = async (_ctx) => {
14468
- const enabled = process.platform === "win32" && process.env.CODEFORGE_DISABLE_PWSH_UTF8 !== "1";
14469
- logLifecycle(PLUGIN_NAME14, "activate", { enabled, platform: process.platform });
14470
- if (!enabled)
14471
- return {};
14472
- return {
14473
- "tool.execute.before": async (input, output) => {
14474
- await safeAsync(PLUGIN_NAME14, "tool.execute.before", async () => {
14475
- if (input.tool !== "bash")
14476
- return;
14477
- const args = output.args ?? {};
14478
- const original = args["command"];
14479
- const next = prependUtf8Prelude(original);
14480
- if (next !== undefined && next !== original) {
14481
- args["command"] = next;
14482
- output.args = args;
14483
- safeWriteLog(PLUGIN_NAME14, {
14484
- hook: "tool.execute.before",
14485
- tool: input.tool,
14486
- callID: input.callID,
14487
- sessionID: input.sessionID,
14488
- injected: true
14489
- });
14490
- }
14491
- });
14492
- }
14493
- };
14494
- };
14495
-
14496
- // plugins/session-recovery.ts
14497
- init_opencode_plugin_helpers();
14498
-
14499
- // lib/event-stream.ts
14500
- import { promises as fs9 } from "node:fs";
14501
- init_runtime_paths();
14502
- import * as path12 from "node:path";
14503
- async function loadSession(id, opts = {}) {
14504
- const file = resolveSessionFile(id, opts);
14505
- const raw = await fs9.readFile(file, "utf8");
14506
- return parseJsonl(id, raw);
14787
+ function getInflightSnapshot() {
14788
+ return _snapshotInflight();
14507
14789
  }
14508
- async function listSessions(opts = {}) {
14509
- const dir = resolveDir(opts);
14510
- let entries;
14511
- try {
14512
- entries = await fs9.readdir(dir, { withFileTypes: true });
14513
- } catch (err) {
14514
- if (err.code === "ENOENT")
14515
- return [];
14516
- throw err;
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 };
14517
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()) {
14518
14855
  const out = [];
14519
- for (const e of entries) {
14520
- if (!e.isFile() || !e.name.endsWith(".jsonl"))
14521
- continue;
14522
- const file = path12.join(dir, e.name);
14523
- const id = e.name.replace(/\.jsonl$/, "");
14524
- try {
14525
- const stat = await fs9.stat(file);
14526
- const headerLine = await readFirstLine(file);
14527
- let started_at = stat.birthtimeMs;
14528
- if (headerLine) {
14529
- try {
14530
- const h = JSON.parse(headerLine);
14531
- if (h.__header && typeof h.started_at === "number") {
14532
- started_at = h.started_at;
14533
- }
14534
- } catch {}
14535
- }
14536
- out.push({
14537
- id,
14538
- file,
14539
- started_at,
14540
- size: stat.size,
14541
- mtime_ms: stat.mtimeMs
14542
- });
14543
- } catch {}
14856
+ for (const r of inflight2.values()) {
14857
+ if (now - r.lastBeatAt >= HEARTBEAT_DEBOUNCE_MS)
14858
+ out.push(r);
14544
14859
  }
14545
- out.sort((a, b) => b.started_at - a.started_at);
14546
14860
  return out;
14547
14861
  }
14548
- function resolveDir(opts = {}) {
14549
- const root = path12.resolve(opts.root ?? process.cwd());
14550
- return opts.sessions_dir ? path12.resolve(root, opts.sessions_dir) : path12.join(runtimeDir(root), "sessions");
14551
- }
14552
- function resolveSessionFile(id, opts = {}) {
14553
- return path12.join(resolveDir(opts), `${id}.jsonl`);
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`;
14554
14867
  }
14555
- function parseJsonl(id, raw) {
14556
- const events = [];
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", {});
15072
+ var PRELUDE = "chcp 65001 *> $null; " + "[Console]::OutputEncoding = [System.Text.UTF8Encoding]::new(); " + "$OutputEncoding = [System.Text.UTF8Encoding]::new(); ";
15073
+ function prependUtf8Prelude(command) {
15074
+ if (typeof command !== "string")
15075
+ return;
15076
+ if (command.length === 0)
15077
+ return command;
15078
+ if (/chcp\s+65001/i.test(command))
15079
+ return command;
15080
+ if (/\[Console\]::OutputEncoding\s*=/i.test(command))
15081
+ return command;
15082
+ return PRELUDE + command;
15083
+ }
15084
+ var handler16 = async (_ctx) => {
15085
+ const enabled = process.platform === "win32" && process.env.CODEFORGE_DISABLE_PWSH_UTF8 !== "1";
15086
+ logLifecycle(PLUGIN_NAME16, "activate", { enabled, platform: process.platform });
15087
+ if (!enabled)
15088
+ return {};
15089
+ return {
15090
+ "tool.execute.before": async (input, output) => {
15091
+ await safeAsync(PLUGIN_NAME16, "tool.execute.before", async () => {
15092
+ if (input.tool !== "bash")
15093
+ return;
15094
+ const args = output.args ?? {};
15095
+ const original = args["command"];
15096
+ const next = prependUtf8Prelude(original);
15097
+ if (next !== undefined && next !== original) {
15098
+ args["command"] = next;
15099
+ output.args = args;
15100
+ safeWriteLog(PLUGIN_NAME16, {
15101
+ hook: "tool.execute.before",
15102
+ tool: input.tool,
15103
+ callID: input.callID,
15104
+ sessionID: input.sessionID,
15105
+ injected: true
15106
+ });
15107
+ }
15108
+ });
15109
+ }
15110
+ };
15111
+ };
15112
+
15113
+ // plugins/session-recovery.ts
15114
+ init_opencode_plugin_helpers();
15115
+
15116
+ // lib/event-stream.ts
15117
+ import { promises as fs9 } from "node:fs";
15118
+ init_runtime_paths();
15119
+ import * as path12 from "node:path";
15120
+ async function loadSession(id, opts = {}) {
15121
+ const file = resolveSessionFile(id, opts);
15122
+ const raw = await fs9.readFile(file, "utf8");
15123
+ return parseJsonl(id, raw);
15124
+ }
15125
+ async function listSessions(opts = {}) {
15126
+ const dir = resolveDir(opts);
15127
+ let entries;
15128
+ try {
15129
+ entries = await fs9.readdir(dir, { withFileTypes: true });
15130
+ } catch (err) {
15131
+ if (err.code === "ENOENT")
15132
+ return [];
15133
+ throw err;
15134
+ }
15135
+ const out = [];
15136
+ for (const e of entries) {
15137
+ if (!e.isFile() || !e.name.endsWith(".jsonl"))
15138
+ continue;
15139
+ const file = path12.join(dir, e.name);
15140
+ const id = e.name.replace(/\.jsonl$/, "");
15141
+ try {
15142
+ const stat = await fs9.stat(file);
15143
+ const headerLine = await readFirstLine(file);
15144
+ let started_at = stat.birthtimeMs;
15145
+ if (headerLine) {
15146
+ try {
15147
+ const h = JSON.parse(headerLine);
15148
+ if (h.__header && typeof h.started_at === "number") {
15149
+ started_at = h.started_at;
15150
+ }
15151
+ } catch {}
15152
+ }
15153
+ out.push({
15154
+ id,
15155
+ file,
15156
+ started_at,
15157
+ size: stat.size,
15158
+ mtime_ms: stat.mtimeMs
15159
+ });
15160
+ } catch {}
15161
+ }
15162
+ out.sort((a, b) => b.started_at - a.started_at);
15163
+ return out;
15164
+ }
15165
+ function resolveDir(opts = {}) {
15166
+ const root = path12.resolve(opts.root ?? process.cwd());
15167
+ return opts.sessions_dir ? path12.resolve(root, opts.sessions_dir) : path12.join(runtimeDir(root), "sessions");
15168
+ }
15169
+ function resolveSessionFile(id, opts = {}) {
15170
+ return path12.join(resolveDir(opts), `${id}.jsonl`);
15171
+ }
15172
+ function parseJsonl(id, raw) {
15173
+ const events = [];
14557
15174
  let started_at = null;
14558
15175
  for (const line of raw.split(/\r?\n/)) {
14559
15176
  if (!line.trim())
@@ -14673,7 +15290,7 @@ function buildRecoveryPlan(events, meta = { id: null }, opts = {}) {
14673
15290
  });
14674
15291
  }
14675
15292
  }
14676
- 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);
14677
15294
  const proposed = window.some((t) => PENDING_CHANGES_TOOLS.has(t.tool));
14678
15295
  const applied = window.some((t) => APPLY_TOOLS.has(t.tool) && t.ok);
14679
15296
  const pending_changes_likely = proposed && !applied;
@@ -14697,7 +15314,7 @@ function buildRecoveryPlan(events, meta = { id: null }, opts = {}) {
14697
15314
  idleMs,
14698
15315
  lastUser,
14699
15316
  lastAgent,
14700
- inflight: inflight2,
15317
+ inflight: inflight3,
14701
15318
  pending_changes_likely,
14702
15319
  open_subtasks_likely,
14703
15320
  reason
@@ -14708,7 +15325,7 @@ function buildRecoveryPlan(events, meta = { id: null }, opts = {}) {
14708
15325
  idle_ms: idleMs,
14709
15326
  last_user_intent: lastUser,
14710
15327
  last_agent: lastAgent,
14711
- inflight_tools: inflight2,
15328
+ inflight_tools: inflight3,
14712
15329
  pending_changes_likely,
14713
15330
  open_subtasks_likely,
14714
15331
  summary
@@ -14803,328 +15420,107 @@ function emptyPlan(reason) {
14803
15420
  summary: "无历史会话,无需恢复。"
14804
15421
  };
14805
15422
  }
14806
- function isRecoveryWorthShowing(plan) {
14807
- if (!plan.last_session_id)
14808
- return false;
14809
- if (plan.reason === "no_session")
14810
- return false;
14811
- const hasSignal = plan.pending_changes_likely || plan.open_subtasks_likely || plan.inflight_tools.length > 0 || plan.reason === "explicit_marker";
14812
- return hasSignal;
14813
- }
14814
-
14815
- // plugins/session-recovery.ts
14816
- var PLUGIN_NAME15 = "session-recovery";
14817
- logLifecycle(PLUGIN_NAME15, "import", {});
14818
- async function processSessionStart(currentSessionId, opts = {}) {
14819
- if (opts.disabled) {
14820
- return { ok: true, injected: false, reason: "disabled" };
14821
- }
14822
- const excludeIds = new Set(opts.excludeIds ?? []);
14823
- if (currentSessionId)
14824
- excludeIds.add(currentSessionId);
14825
- const r = await scanLastSession({ ...opts, excludeIds: [...excludeIds] });
14826
- if (!r.ok) {
14827
- opts.log?.warn?.(`[${PLUGIN_NAME15}] 扫描失败:${r.error}`);
14828
- return { ok: false, injected: false, reason: "scan_error", error: r.error };
14829
- }
14830
- const plan = r.plan;
14831
- if (!isRecoveryWorthShowing(plan)) {
14832
- return { ok: true, injected: false, plan, reason: "no_signal" };
14833
- }
14834
- const prompt = renderPrompt(plan);
14835
- const injection = { source: "session-recovery", plan, prompt };
14836
- if (opts.injectRecovery) {
14837
- try {
14838
- await opts.injectRecovery(injection);
14839
- } catch (err) {
14840
- const msg = err instanceof Error ? err.message : String(err);
14841
- opts.log?.warn?.(`[${PLUGIN_NAME15}] injectRecovery 异常:${msg}`);
14842
- return { ok: false, injected: false, plan, reason: "inject_error", error: msg };
14843
- }
14844
- }
14845
- return { ok: true, injected: true, plan, reason: "ok" };
14846
- }
14847
- function renderPrompt(plan) {
14848
- const lines = [];
14849
- lines.push("【会话恢复提示】");
14850
- lines.push(plan.summary);
14851
- lines.push("");
14852
- lines.push("如果用户接下来未明确提及,请优先:");
14853
- if (plan.pending_changes_likely) {
14854
- lines.push(" • 询问是否要 review 上次的暂存区改动并 apply / 丢弃");
14855
- }
14856
- if (plan.open_subtasks_likely) {
14857
- lines.push(" • 询问是否要继续上次未完成的子任务");
14858
- }
14859
- if (plan.last_user_intent) {
14860
- lines.push(` • 确认是否要继续推进上次的目标:"${plan.last_user_intent}"`);
14861
- }
14862
- if (!plan.pending_changes_likely && !plan.open_subtasks_likely && !plan.last_user_intent) {
14863
- lines.push(" • 询问是否要恢复上次的工作(信号较弱,可能不需要)");
14864
- }
14865
- lines.push("");
14866
- lines.push("(如果用户明确开了新话题,本提示可忽略)");
14867
- return lines.join(`
14868
- `);
14869
- }
14870
- var log7 = makePluginLogger(PLUGIN_NAME15);
14871
- var _lastInjection = null;
14872
- var sessionRecoveryServer = async (ctx) => {
14873
- logLifecycle(PLUGIN_NAME15, "activate", { directory: ctx.directory });
14874
- return {
14875
- event: async ({ event }) => {
14876
- await safeAsync(PLUGIN_NAME15, "event", async () => {
14877
- const e = event;
14878
- if (!e || typeof e.type !== "string")
14879
- return;
14880
- if (e.type !== "session.start")
14881
- return;
14882
- const sid = typeof e.properties?.["session_id"] === "string" ? e.properties["session_id"] : undefined;
14883
- const root = typeof e.properties?.["root"] === "string" ? e.properties["root"] : ctx.directory ?? process.cwd();
14884
- const r = await processSessionStart(sid, {
14885
- root,
14886
- log: log7,
14887
- injectRecovery: (inj) => {
14888
- _lastInjection = inj;
14889
- }
14890
- });
14891
- safeWriteLog(PLUGIN_NAME15, {
14892
- hook: "event",
14893
- type: "session.start",
14894
- ok: r.ok,
14895
- injected: r.injected,
14896
- reason: r.reason,
14897
- last_session_id: r.plan?.last_session_id
14898
- });
14899
- if (r.injected && r.plan) {
14900
- log7.info(`[${PLUGIN_NAME15}] 注入恢复提示(last=${r.plan.last_session_id?.slice(0, 8) ?? "?"}, reason=${r.plan.reason})`);
14901
- }
14902
- });
14903
- }
14904
- };
14905
- };
14906
- var handler15 = sessionRecoveryServer;
14907
-
14908
- // plugins/subtask-heartbeat.ts
14909
- init_opencode_plugin_helpers();
14910
- var PLUGIN_NAME16 = "subtask-heartbeat";
14911
- logLifecycle(PLUGIN_NAME16, "import", {});
14912
- var HEARTBEAT_INTERVAL_MS2 = 30000;
14913
- var HEARTBEAT_DEBOUNCE_MS = 25000;
14914
- var TOAST_DURATION_MS3 = 5000;
14915
- var START_TOAST_DURATION_MS = 2000;
14916
- var inflight2 = new Map;
14917
- function extractCreatedChild(event) {
14918
- if (!event || typeof event !== "object")
14919
- return null;
14920
- const e = event;
14921
- if (e.type !== "session.created")
14922
- return null;
14923
- const info = e.properties?.info;
14924
- if (!info || typeof info !== "object")
14925
- return null;
14926
- const session = info;
14927
- if (typeof session.id !== "string")
14928
- return null;
14929
- if (typeof session.parentID !== "string" || session.parentID === "")
14930
- return null;
14931
- return { childID: session.id, parentID: session.parentID, agent: null };
14932
- }
14933
- function extractEndedSessionID(event) {
14934
- if (!event || typeof event !== "object")
14935
- return null;
14936
- const e = event;
14937
- if (typeof e.type !== "string")
14938
- return null;
14939
- if (e.type !== "session.idle" && e.type !== "session.deleted" && e.type !== "session.error") {
14940
- return null;
14941
- }
14942
- const props = e.properties ?? {};
14943
- const direct = props["sessionID"];
14944
- if (typeof direct === "string" && direct)
14945
- return { type: e.type, sessionID: direct };
14946
- const info = props["info"];
14947
- if (info && typeof info === "object") {
14948
- const sid = info.id;
14949
- if (typeof sid === "string" && sid)
14950
- return { type: e.type, sessionID: sid };
14951
- }
14952
- return null;
14953
- }
14954
- function registerInflight(payload, now = Date.now()) {
14955
- const r = {
14956
- childID: payload.childID,
14957
- parentID: payload.parentID,
14958
- agent: payload.agent,
14959
- startedAt: now,
14960
- lastBeatAt: now,
14961
- lastTool: null
14962
- };
14963
- inflight2.set(payload.childID, r);
14964
- return r;
14965
- }
14966
- function recordToolBeat(sessionID, tool2, now = Date.now()) {
14967
- const r = inflight2.get(sessionID);
14968
- if (!r)
14969
- return null;
14970
- r.lastBeatAt = now;
14971
- r.lastTool = tool2;
14972
- return r;
14973
- }
14974
- function clearInflight2(sessionID) {
14975
- const r = inflight2.get(sessionID);
14976
- if (!r)
14977
- return null;
14978
- inflight2.delete(sessionID);
14979
- return r;
14980
- }
14981
- function pickHeartbeats(now = Date.now()) {
14982
- const out = [];
14983
- for (const r of inflight2.values()) {
14984
- if (now - r.lastBeatAt >= HEARTBEAT_DEBOUNCE_MS)
14985
- out.push(r);
14986
- }
14987
- return out;
14988
- }
14989
- function fmtElapsed(ms) {
14990
- const total = Math.max(0, Math.floor(ms / 1000));
14991
- const m = Math.floor(total / 60);
14992
- const s = total % 60;
14993
- return m > 0 ? `${m}m${s.toString().padStart(2, "0")}s` : `${s}s`;
14994
- }
14995
- function buildStartToast(r) {
14996
- const who = r.agent ?? "subagent";
14997
- return {
14998
- message: `\uD83D\uDE80 子 session 启动: ${who}`,
14999
- variant: "info"
15000
- };
15001
- }
15002
- function buildHeartbeatToast(r, now = Date.now()) {
15003
- const who = r.agent ?? "subagent";
15004
- const tool2 = r.lastTool ?? "thinking";
15005
- return {
15006
- message: `⏳ ${who} 仍在运行 ${fmtElapsed(now - r.startedAt)} | 当前: ${tool2}`,
15007
- variant: "info"
15008
- };
15009
- }
15010
- function buildEndToast(r, type, now = Date.now()) {
15011
- const who = r.agent ?? "subagent";
15012
- const elapsed = fmtElapsed(now - r.startedAt);
15013
- if (type === "session.error") {
15014
- return { message: `❌ ${who} 失败 (${elapsed})`, variant: "error" };
15423
+ function isRecoveryWorthShowing(plan) {
15424
+ if (!plan.last_session_id)
15425
+ return false;
15426
+ if (plan.reason === "no_session")
15427
+ return false;
15428
+ const hasSignal = plan.pending_changes_likely || plan.open_subtasks_likely || plan.inflight_tools.length > 0 || plan.reason === "explicit_marker";
15429
+ return hasSignal;
15430
+ }
15431
+
15432
+ // plugins/session-recovery.ts
15433
+ var PLUGIN_NAME17 = "session-recovery";
15434
+ logLifecycle(PLUGIN_NAME17, "import", {});
15435
+ async function processSessionStart(currentSessionId, opts = {}) {
15436
+ if (opts.disabled) {
15437
+ return { ok: true, injected: false, reason: "disabled" };
15015
15438
  }
15016
- if (type === "session.deleted") {
15017
- return { message: `\uD83D\uDDD1️ ${who} 被取消 (${elapsed})`, variant: "error" };
15439
+ const excludeIds = new Set(opts.excludeIds ?? []);
15440
+ if (currentSessionId)
15441
+ excludeIds.add(currentSessionId);
15442
+ const r = await scanLastSession({ ...opts, excludeIds: [...excludeIds] });
15443
+ if (!r.ok) {
15444
+ opts.log?.warn?.(`[${PLUGIN_NAME17}] 扫描失败:${r.error}`);
15445
+ return { ok: false, injected: false, reason: "scan_error", error: r.error };
15018
15446
  }
15019
- return { message: `✅ ${who} 完成 (${elapsed})`, variant: "success" };
15020
- }
15021
- function normalizeVariant3(raw) {
15022
- if (raw === "info" || raw === "warning")
15023
- return "default";
15024
- return raw;
15025
- }
15026
- async function showToast3(client, payload, log8) {
15027
- if (typeof client?.tui?.showToast !== "function") {
15028
- log8?.debug?.("tui.showToast 不可用,noop");
15029
- return false;
15447
+ const plan = r.plan;
15448
+ if (!isRecoveryWorthShowing(plan)) {
15449
+ return { ok: true, injected: false, plan, reason: "no_signal" };
15030
15450
  }
15031
- try {
15032
- await client.tui.showToast({
15033
- body: {
15034
- message: payload.message,
15035
- variant: normalizeVariant3(payload.variant),
15036
- duration: payload.duration ?? TOAST_DURATION_MS3,
15037
- title: payload.title ?? "CodeForge"
15038
- }
15039
- });
15040
- return true;
15041
- } catch (err) {
15042
- log8?.warn("tui.showToast 抛错(已隔离)", {
15043
- error: err instanceof Error ? err.message : String(err)
15044
- });
15045
- return false;
15451
+ const prompt = renderPrompt(plan);
15452
+ const injection = { source: "session-recovery", plan, prompt };
15453
+ if (opts.injectRecovery) {
15454
+ try {
15455
+ await opts.injectRecovery(injection);
15456
+ } catch (err) {
15457
+ const msg = err instanceof Error ? err.message : String(err);
15458
+ opts.log?.warn?.(`[${PLUGIN_NAME17}] injectRecovery 异常:${msg}`);
15459
+ return { ok: false, injected: false, plan, reason: "inject_error", error: msg };
15460
+ }
15046
15461
  }
15462
+ return { ok: true, injected: true, plan, reason: "ok" };
15047
15463
  }
15048
- var log8 = makePluginLogger(PLUGIN_NAME16);
15049
- var subtaskHeartbeatServer = async (ctx) => {
15050
- logLifecycle(PLUGIN_NAME16, "activate", {
15051
- directory: ctx.directory,
15052
- intervalMs: HEARTBEAT_INTERVAL_MS2
15053
- });
15054
- const client = ctx.client;
15055
- const interval = setInterval(() => {
15056
- safeAsync(PLUGIN_NAME16, "interval", async () => {
15057
- const beats = pickHeartbeats();
15058
- if (beats.length === 0)
15059
- return;
15060
- for (const r of beats) {
15061
- const t = buildHeartbeatToast(r);
15062
- const sent = await showToast3(client, t, log8);
15063
- safeWriteLog(PLUGIN_NAME16, {
15064
- hook: "interval",
15065
- child: r.childID,
15066
- parent: r.parentID,
15067
- tool: r.lastTool,
15068
- elapsed_ms: Date.now() - r.startedAt,
15069
- toast_sent: sent
15070
- });
15071
- r.lastBeatAt = Date.now();
15072
- }
15073
- });
15074
- }, HEARTBEAT_INTERVAL_MS2);
15075
- if (typeof interval.unref === "function") {
15076
- interval.unref();
15464
+ function renderPrompt(plan) {
15465
+ const lines = [];
15466
+ lines.push("【会话恢复提示】");
15467
+ lines.push(plan.summary);
15468
+ lines.push("");
15469
+ lines.push("如果用户接下来未明确提及,请优先:");
15470
+ if (plan.pending_changes_likely) {
15471
+ lines.push(" 询问是否要 review 上次的暂存区改动并 apply / 丢弃");
15472
+ }
15473
+ if (plan.open_subtasks_likely) {
15474
+ lines.push(" • 询问是否要继续上次未完成的子任务");
15475
+ }
15476
+ if (plan.last_user_intent) {
15477
+ lines.push(` • 确认是否要继续推进上次的目标:"${plan.last_user_intent}"`);
15077
15478
  }
15479
+ if (!plan.pending_changes_likely && !plan.open_subtasks_likely && !plan.last_user_intent) {
15480
+ lines.push(" • 询问是否要恢复上次的工作(信号较弱,可能不需要)");
15481
+ }
15482
+ lines.push("");
15483
+ lines.push("(如果用户明确开了新话题,本提示可忽略)");
15484
+ return lines.join(`
15485
+ `);
15486
+ }
15487
+ var log8 = makePluginLogger(PLUGIN_NAME17);
15488
+ var _lastInjection = null;
15489
+ var sessionRecoveryServer = async (ctx) => {
15490
+ logLifecycle(PLUGIN_NAME17, "activate", { directory: ctx.directory });
15078
15491
  return {
15079
15492
  event: async ({ event }) => {
15080
- await safeAsync(PLUGIN_NAME16, "event", async () => {
15081
- const created = extractCreatedChild(event);
15082
- if (created) {
15083
- const record = registerInflight(created);
15084
- safeWriteLog(PLUGIN_NAME16, {
15085
- hook: "event",
15086
- type: "session.created",
15087
- child: created.childID,
15088
- parent: created.parentID
15089
- });
15090
- const startToast = buildStartToast(record);
15091
- const sent = await showToast3(client, { ...startToast, duration: START_TOAST_DURATION_MS }, log8);
15092
- safeWriteLog(PLUGIN_NAME16, {
15093
- hook: "event",
15094
- type: "session.created.toast",
15095
- child: created.childID,
15096
- toast_sent: sent
15097
- });
15493
+ await safeAsync(PLUGIN_NAME17, "event", async () => {
15494
+ const e = event;
15495
+ if (!e || typeof e.type !== "string")
15098
15496
  return;
15099
- }
15100
- const ended = extractEndedSessionID(event);
15101
- if (ended) {
15102
- const r = clearInflight2(ended.sessionID);
15103
- if (r) {
15104
- const t = buildEndToast(r, ended.type);
15105
- const sent = await showToast3(client, t, log8);
15106
- safeWriteLog(PLUGIN_NAME16, {
15107
- hook: "event",
15108
- type: ended.type,
15109
- child: r.childID,
15110
- elapsed_ms: Date.now() - r.startedAt,
15111
- toast_sent: sent,
15112
- end_toast_message: t.message
15113
- });
15497
+ if (e.type !== "session.start")
15498
+ return;
15499
+ const sid = typeof e.properties?.["session_id"] === "string" ? e.properties["session_id"] : undefined;
15500
+ const root = typeof e.properties?.["root"] === "string" ? e.properties["root"] : ctx.directory ?? process.cwd();
15501
+ const r = await processSessionStart(sid, {
15502
+ root,
15503
+ log: log8,
15504
+ injectRecovery: (inj) => {
15505
+ _lastInjection = inj;
15114
15506
  }
15507
+ });
15508
+ safeWriteLog(PLUGIN_NAME17, {
15509
+ hook: "event",
15510
+ type: "session.start",
15511
+ ok: r.ok,
15512
+ injected: r.injected,
15513
+ reason: r.reason,
15514
+ last_session_id: r.plan?.last_session_id
15515
+ });
15516
+ if (r.injected && r.plan) {
15517
+ log8.info(`[${PLUGIN_NAME17}] 注入恢复提示(last=${r.plan.last_session_id?.slice(0, 8) ?? "?"}, reason=${r.plan.reason})`);
15115
15518
  }
15116
15519
  });
15117
- },
15118
- "tool.execute.before": async (input) => {
15119
- await safeAsync(PLUGIN_NAME16, "tool.execute.before", async () => {
15120
- if (!input || typeof input.sessionID !== "string" || typeof input.tool !== "string")
15121
- return;
15122
- recordToolBeat(input.sessionID, input.tool);
15123
- });
15124
15520
  }
15125
15521
  };
15126
15522
  };
15127
- var handler16 = subtaskHeartbeatServer;
15523
+ var handler17 = sessionRecoveryServer;
15128
15524
 
15129
15525
  // plugins/subtasks.ts
15130
15526
  import { promises as fs10 } from "node:fs";
@@ -15154,6 +15550,18 @@ async function schedule(opts) {
15154
15550
  const results = new Array(opts.subtasks.length);
15155
15551
  let nextIdx = 0;
15156
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
+ };
15157
15565
  const runOne = async (i, spec) => {
15158
15566
  const subStart = now();
15159
15567
  let alloc;
@@ -15161,7 +15569,7 @@ async function schedule(opts) {
15161
15569
  try {
15162
15570
  alloc = await opts.deps.allocateWorktree(spec.id);
15163
15571
  } catch (err) {
15164
- results[i] = {
15572
+ const res2 = {
15165
15573
  id: spec.id,
15166
15574
  ok: false,
15167
15575
  summary: clamp(`worktree 分配失败:${describe4(err)}`, limit),
@@ -15169,9 +15577,21 @@ async function schedule(opts) {
15169
15577
  duration_ms: now() - subStart,
15170
15578
  error: describe4(err)
15171
15579
  };
15580
+ results[i] = res2;
15581
+ await fireFinish(i, res2);
15172
15582
  return;
15173
15583
  }
15174
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
+ }
15175
15595
  const ctl = new AbortController;
15176
15596
  const cascade = () => ctl.abort();
15177
15597
  if (globalCtl.signal.aborted)
@@ -15224,6 +15644,7 @@ async function schedule(opts) {
15224
15644
  }
15225
15645
  }
15226
15646
  results[i] = res;
15647
+ await fireFinish(i, res);
15227
15648
  };
15228
15649
  const launch = async () => {
15229
15650
  while (true) {
@@ -15232,13 +15653,15 @@ async function schedule(opts) {
15232
15653
  return;
15233
15654
  if (globalCtl.signal.aborted) {
15234
15655
  const spec = opts.subtasks[i];
15235
- results[i] = {
15656
+ const res = {
15236
15657
  id: spec.id,
15237
15658
  ok: false,
15238
15659
  summary: clamp("调度器已取消,未启动", limit),
15239
15660
  status: "cancelled",
15240
15661
  duration_ms: 0
15241
15662
  };
15663
+ results[i] = res;
15664
+ await fireFinish(i, res);
15242
15665
  continue;
15243
15666
  }
15244
15667
  await runOne(i, opts.subtasks[i]);
@@ -15519,20 +15942,20 @@ async function withTimeout2(p, ms, signal) {
15519
15942
  throw err;
15520
15943
  }
15521
15944
  }
15522
- return await new Promise((resolve10, reject) => {
15945
+ return await new Promise((resolve11, reject) => {
15523
15946
  let settled = false;
15524
15947
  const timer = setTimeout(() => {
15525
15948
  if (settled)
15526
15949
  return;
15527
15950
  settled = true;
15528
- resolve10({ kind: "timeout" });
15951
+ resolve11({ kind: "timeout" });
15529
15952
  }, ms);
15530
15953
  const onAbort = () => {
15531
15954
  if (settled)
15532
15955
  return;
15533
15956
  settled = true;
15534
15957
  clearTimeout(timer);
15535
- resolve10({ kind: "aborted" });
15958
+ resolve11({ kind: "aborted" });
15536
15959
  };
15537
15960
  signal?.addEventListener("abort", onAbort, { once: true });
15538
15961
  p.then((value) => {
@@ -15541,7 +15964,7 @@ async function withTimeout2(p, ms, signal) {
15541
15964
  settled = true;
15542
15965
  clearTimeout(timer);
15543
15966
  signal?.removeEventListener("abort", onAbort);
15544
- resolve10({ kind: "ok", value });
15967
+ resolve11({ kind: "ok", value });
15545
15968
  }, (err) => {
15546
15969
  if (settled)
15547
15970
  return;
@@ -15577,11 +16000,48 @@ function clip4(s, max) {
15577
16000
  return "";
15578
16001
  return s.length <= max ? s : s.slice(0, max - 1) + "…";
15579
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
+ }
15580
16040
 
15581
16041
  // plugins/subtasks.ts
15582
16042
  init_opencode_plugin_helpers();
15583
16043
  init_runtime_paths();
15584
- var PLUGIN_NAME17 = "subtasks";
16044
+ var PLUGIN_NAME18 = "subtasks";
15585
16045
  function getLogFile(root = process.cwd()) {
15586
16046
  return path13.join(runtimeDir(root), "logs", "subtasks.log");
15587
16047
  }
@@ -15660,18 +16120,49 @@ async function handleParallelCommand(raw) {
15660
16120
  specs = splitDescriptions(args.description, { parentId });
15661
16121
  }
15662
16122
  if (specs.length === 0) {
15663
- log9?.warn(`[${PLUGIN_NAME17}] /parallel 缺有效子任务`);
16123
+ log9?.warn(`[${PLUGIN_NAME18}] /parallel 缺有效子任务`);
15664
16124
  await safeReply2(ctx, "⚠ /parallel 需要至少 1 个子任务(用 ; 或换行分隔)");
15665
16125
  return { ok: false, reason: "no_subtasks" };
15666
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
+ }
15667
16150
  const runner = ctx.runner ?? mockRunner;
15668
16151
  let result;
15669
16152
  try {
15670
16153
  result = await schedule({
15671
16154
  parentId,
15672
16155
  subtasks: specs,
15673
- maxConcurrency: clampInt(args.maxConcurrency, 1, 16, 4),
15674
- 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,
15675
16166
  deps: {
15676
16167
  runSubtask: runner,
15677
16168
  allocateWorktree: ctx.allocateWorktree,
@@ -15687,7 +16178,7 @@ async function handleParallelCommand(raw) {
15687
16178
  });
15688
16179
  } catch (err) {
15689
16180
  const msg = err instanceof Error ? err.message : String(err);
15690
- log9?.error(`[${PLUGIN_NAME17}] schedule 抛错`, { error: msg });
16181
+ log9?.error(`[${PLUGIN_NAME18}] schedule 抛错`, { error: msg });
15691
16182
  await safeReply2(ctx, `❌ 并发调度失败:${msg}`);
15692
16183
  return { ok: false, reason: msg };
15693
16184
  }
@@ -15695,7 +16186,7 @@ async function handleParallelCommand(raw) {
15695
16186
  const summaryLines = [head, "", "```", result.digest.text, "```"];
15696
16187
  await safeReply2(ctx, summaryLines.join(`
15697
16188
  `));
15698
- log9?.info(`[${PLUGIN_NAME17}] schedule 完成`, {
16189
+ log9?.info(`[${PLUGIN_NAME18}] schedule 完成`, {
15699
16190
  parentId,
15700
16191
  success: result.digest.success,
15701
16192
  failed: result.digest.failed,
@@ -15705,7 +16196,7 @@ async function handleParallelCommand(raw) {
15705
16196
  try {
15706
16197
  await ctx.onCompleted(result);
15707
16198
  } catch (err) {
15708
- log9?.warn(`[${PLUGIN_NAME17}] onCompleted hook 抛错(已隔离)`, {
16199
+ log9?.warn(`[${PLUGIN_NAME18}] onCompleted hook 抛错(已隔离)`, {
15709
16200
  error: err instanceof Error ? err.message : String(err)
15710
16201
  });
15711
16202
  }
@@ -15736,14 +16227,17 @@ async function maybeHandleMessage(raw) {
15736
16227
  reply: ctx.reply,
15737
16228
  runner: ctx.runner,
15738
16229
  allocateWorktree: ctx.allocateWorktree,
15739
- log: ctx.log
16230
+ log: ctx.log,
16231
+ client: ctx.client,
16232
+ parentSessionID: ctx.parentSessionID,
16233
+ directory: ctx.directory
15740
16234
  });
15741
16235
  }
15742
16236
  async function writeLog(level, msg, data) {
15743
16237
  const line = JSON.stringify({
15744
16238
  ts: new Date().toISOString(),
15745
16239
  level,
15746
- plugin: PLUGIN_NAME17,
16240
+ plugin: PLUGIN_NAME18,
15747
16241
  msg,
15748
16242
  data
15749
16243
  }) + `
@@ -15754,11 +16248,11 @@ async function writeLog(level, msg, data) {
15754
16248
  await fs10.appendFile(logFile, line, "utf8");
15755
16249
  } catch {}
15756
16250
  }
15757
- logLifecycle(PLUGIN_NAME17, "import");
16251
+ logLifecycle(PLUGIN_NAME18, "import");
15758
16252
  var subtasksServer = async (ctx) => {
15759
- const log9 = makePluginLogger(PLUGIN_NAME17);
16253
+ const log9 = makePluginLogger(PLUGIN_NAME18);
15760
16254
  const client = ctx?.client ?? undefined;
15761
- logLifecycle(PLUGIN_NAME17, "activate", {
16255
+ logLifecycle(PLUGIN_NAME18, "activate", {
15762
16256
  directory: ctx.directory,
15763
16257
  hasClient: Boolean(client)
15764
16258
  });
@@ -15779,6 +16273,9 @@ var subtasksServer = async (ctx) => {
15779
16273
  return Promise.resolve();
15780
16274
  },
15781
16275
  log: log9,
16276
+ client,
16277
+ parentSessionID: input.sessionID,
16278
+ directory: ctx.directory,
15782
16279
  runner: client ? makeOpencodeRunner({
15783
16280
  client,
15784
16281
  parentSessionID: input.sessionID,
@@ -15815,20 +16312,20 @@ var subtasksServer = async (ctx) => {
15815
16312
  });
15816
16313
  }
15817
16314
  } catch (err) {
15818
- log9.error(`[${PLUGIN_NAME17}] command.execute.before 异常(已隔离)`, {
16315
+ log9.error(`[${PLUGIN_NAME18}] command.execute.before 异常(已隔离)`, {
15819
16316
  error: err instanceof Error ? err.message : String(err)
15820
16317
  });
15821
16318
  }
15822
16319
  }
15823
16320
  };
15824
16321
  };
15825
- var handler17 = subtasksServer;
16322
+ var handler18 = subtasksServer;
15826
16323
 
15827
16324
  // plugins/terminal-monitor.ts
15828
16325
  init_opencode_plugin_helpers();
15829
16326
  import * as crypto5 from "node:crypto";
15830
- var PLUGIN_NAME18 = "terminal-monitor";
15831
- logLifecycle(PLUGIN_NAME18, "import", {});
16327
+ var PLUGIN_NAME19 = "terminal-monitor";
16328
+ logLifecycle(PLUGIN_NAME19, "import", {});
15832
16329
  var DEFAULT_CONFIG7 = {
15833
16330
  minScore: 0.6,
15834
16331
  cooldownMs: 30000,
@@ -16110,17 +16607,17 @@ function describeError(err) {
16110
16607
  return String(err);
16111
16608
  }
16112
16609
  }
16113
- var log9 = makePluginLogger(PLUGIN_NAME18);
16610
+ var log9 = makePluginLogger(PLUGIN_NAME19);
16114
16611
  var lru = new FingerprintLRU2;
16115
16612
  var _lastNotification = null;
16116
16613
  var terminalMonitorServer = async (ctx) => {
16117
- logLifecycle(PLUGIN_NAME18, "activate", {
16614
+ logLifecycle(PLUGIN_NAME19, "activate", {
16118
16615
  directory: ctx.directory,
16119
16616
  triggerEventTypes: ["terminal.output", "terminal.exit"]
16120
16617
  });
16121
16618
  return {
16122
16619
  event: async ({ event }) => {
16123
- await safeAsync(PLUGIN_NAME18, "event", async () => {
16620
+ await safeAsync(PLUGIN_NAME19, "event", async () => {
16124
16621
  const e = event;
16125
16622
  if (!e || typeof e.type !== "string")
16126
16623
  return;
@@ -16134,7 +16631,7 @@ var terminalMonitorServer = async (ctx) => {
16134
16631
  _lastNotification = msg;
16135
16632
  }
16136
16633
  });
16137
- safeWriteLog(PLUGIN_NAME18, {
16634
+ safeWriteLog(PLUGIN_NAME19, {
16138
16635
  hook: "event",
16139
16636
  type: e.type,
16140
16637
  notified: r.notified,
@@ -16142,18 +16639,18 @@ var terminalMonitorServer = async (ctx) => {
16142
16639
  findings: r.notification?.findings.length ?? 0
16143
16640
  });
16144
16641
  if (r.notified && r.notification) {
16145
- log9.info(`[${PLUGIN_NAME18}] 反馈 ${r.notification.findings.length} 条 → ${r.notification.summary}`);
16642
+ log9.info(`[${PLUGIN_NAME19}] 反馈 ${r.notification.findings.length} 条 → ${r.notification.summary}`);
16146
16643
  }
16147
16644
  });
16148
16645
  }
16149
16646
  };
16150
16647
  };
16151
- var handler18 = terminalMonitorServer;
16648
+ var handler19 = terminalMonitorServer;
16152
16649
 
16153
16650
  // plugins/token-manager.ts
16154
16651
  init_opencode_plugin_helpers();
16155
- var PLUGIN_NAME19 = "token-manager";
16156
- logLifecycle(PLUGIN_NAME19, "import", {});
16652
+ var PLUGIN_NAME20 = "token-manager";
16653
+ logLifecycle(PLUGIN_NAME20, "import", {});
16157
16654
  async function handleMessageBefore(raw, log10, defaults) {
16158
16655
  const ctx = raw ?? {};
16159
16656
  if (!Array.isArray(ctx.messages) || ctx.messages.length === 0)
@@ -16173,21 +16670,21 @@ async function handleMessageBefore(raw, log10, defaults) {
16173
16670
  };
16174
16671
  if (r.compressed) {
16175
16672
  ctx.messages = r.messages;
16176
- 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`);
16177
16674
  }
16178
16675
  return r;
16179
16676
  } catch (err) {
16180
- log10?.warn(`[${PLUGIN_NAME19}] 压缩异常(已隔离)`, {
16677
+ log10?.warn(`[${PLUGIN_NAME20}] 压缩异常(已隔离)`, {
16181
16678
  error: err instanceof Error ? err.message : String(err)
16182
16679
  });
16183
16680
  return null;
16184
16681
  }
16185
16682
  }
16186
- var log10 = makePluginLogger(PLUGIN_NAME19);
16683
+ var log10 = makePluginLogger(PLUGIN_NAME20);
16187
16684
  var tokenManagerServer = async (ctx) => {
16188
16685
  const rt = loadRuntimeSync();
16189
16686
  const threshold = rt.runtime.context.condenser_threshold_ratio;
16190
- logLifecycle(PLUGIN_NAME19, "activate", {
16687
+ logLifecycle(PLUGIN_NAME20, "activate", {
16191
16688
  directory: ctx.directory,
16192
16689
  threshold,
16193
16690
  target: DEFAULT_CONDENSE.target,
@@ -16196,7 +16693,7 @@ var tokenManagerServer = async (ctx) => {
16196
16693
  });
16197
16694
  return {
16198
16695
  "experimental.chat.messages.transform": async (_input, output) => {
16199
- await safeAsync(PLUGIN_NAME19, "experimental.chat.messages.transform", async () => {
16696
+ await safeAsync(PLUGIN_NAME20, "experimental.chat.messages.transform", async () => {
16200
16697
  const list = output.messages;
16201
16698
  if (!Array.isArray(list) || list.length === 0)
16202
16699
  return;
@@ -16209,7 +16706,7 @@ var tokenManagerServer = async (ctx) => {
16209
16706
  const r = await handleMessageBefore({ messages: flat }, log10, { threshold });
16210
16707
  if (!r)
16211
16708
  return;
16212
- safeWriteLog(PLUGIN_NAME19, {
16709
+ safeWriteLog(PLUGIN_NAME20, {
16213
16710
  hook: "experimental.chat.messages.transform",
16214
16711
  mode: "observe-only",
16215
16712
  before_msgs: r.before.count,
@@ -16220,13 +16717,13 @@ var tokenManagerServer = async (ctx) => {
16220
16717
  reason: r.reason
16221
16718
  });
16222
16719
  if (r.compressed) {
16223
- 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)`);
16224
16721
  }
16225
16722
  });
16226
16723
  }
16227
16724
  };
16228
16725
  };
16229
- var handler19 = tokenManagerServer;
16726
+ var handler20 = tokenManagerServer;
16230
16727
 
16231
16728
  // plugins/tool-policy.ts
16232
16729
  init_opencode_plugin_helpers();
@@ -16469,8 +16966,8 @@ function checkFileAccess(acl, file, op) {
16469
16966
  }
16470
16967
 
16471
16968
  // plugins/tool-policy.ts
16472
- var PLUGIN_NAME20 = "tool-policy";
16473
- logLifecycle(PLUGIN_NAME20, "import", {});
16969
+ var PLUGIN_NAME21 = "tool-policy";
16970
+ logLifecycle(PLUGIN_NAME21, "import", {});
16474
16971
  var EMPTY_ACL = { whitelistMode: false };
16475
16972
  function decideToolCall(ctx, cfg = {}) {
16476
16973
  const fallbackMode = cfg.defaultMode ?? DEFAULT_RUNTIME.autonomy.default_mode;
@@ -16522,13 +17019,13 @@ function classifyToolKind(toolName) {
16522
17019
  return "webfetch";
16523
17020
  return "other";
16524
17021
  }
16525
- var log11 = makePluginLogger(PLUGIN_NAME20);
17022
+ var log11 = makePluginLogger(PLUGIN_NAME21);
16526
17023
  var toolPolicyServer = async (ctx) => {
16527
17024
  const directory = ctx.directory ?? process.cwd();
16528
17025
  const cfg = await loadPolicy(directory);
16529
17026
  const rt = loadRuntimeSync();
16530
17027
  cfg.defaultMode = rt.runtime.autonomy.default_mode;
16531
- logLifecycle(PLUGIN_NAME20, "activate", {
17028
+ logLifecycle(PLUGIN_NAME21, "activate", {
16532
17029
  directory,
16533
17030
  acl_loaded: !!cfg.acl,
16534
17031
  default_mode: cfg.defaultMode,
@@ -16536,7 +17033,7 @@ var toolPolicyServer = async (ctx) => {
16536
17033
  });
16537
17034
  return {
16538
17035
  "tool.execute.before": async (input, output) => {
16539
- await safeAsync(PLUGIN_NAME20, "tool.execute.before", async () => {
17036
+ await safeAsync(PLUGIN_NAME21, "tool.execute.before", async () => {
16540
17037
  const toolName = input.tool;
16541
17038
  const argsObj = output.args ?? {};
16542
17039
  const files = [];
@@ -16552,7 +17049,7 @@ var toolPolicyServer = async (ctx) => {
16552
17049
  mode: cfg.defaultMode,
16553
17050
  files: files.length ? files : undefined
16554
17051
  }, cfg);
16555
- safeWriteLog(PLUGIN_NAME20, {
17052
+ safeWriteLog(PLUGIN_NAME21, {
16556
17053
  hook: "tool.execute.before",
16557
17054
  tool: toolName,
16558
17055
  callID: input.callID,
@@ -16561,19 +17058,19 @@ var toolPolicyServer = async (ctx) => {
16561
17058
  reasons: decision.reasons
16562
17059
  });
16563
17060
  if (decision.action === "deny") {
16564
- log11.warn(`[${PLUGIN_NAME20}] DENY ${toolName}: ${decision.reasons.join("; ")}`);
17061
+ log11.warn(`[${PLUGIN_NAME21}] DENY ${toolName}: ${decision.reasons.join("; ")}`);
16565
17062
  } else if (decision.action === "confirm") {
16566
- log11.info(`[${PLUGIN_NAME20}] CONFIRM ${toolName}: ${decision.reasons.join("; ")}`);
17063
+ log11.info(`[${PLUGIN_NAME21}] CONFIRM ${toolName}: ${decision.reasons.join("; ")}`);
16567
17064
  }
16568
17065
  });
16569
17066
  }
16570
17067
  };
16571
17068
  };
16572
- var handler20 = toolPolicyServer;
17069
+ var handler21 = toolPolicyServer;
16573
17070
 
16574
17071
  // plugins/update-checker.ts
16575
17072
  init_opencode_plugin_helpers();
16576
- import { existsSync as existsSync4 } from "node:fs";
17073
+ import { existsSync as existsSync5 } from "node:fs";
16577
17074
  import { homedir as homedir7 } from "node:os";
16578
17075
  import { join as join15 } from "node:path";
16579
17076
 
@@ -16581,7 +17078,7 @@ import { join as join15 } from "node:path";
16581
17078
  import { createHash as createHash6 } from "node:crypto";
16582
17079
  import {
16583
17080
  copyFileSync,
16584
- existsSync as existsSync3,
17081
+ existsSync as existsSync4,
16585
17082
  mkdirSync as mkdirSync3,
16586
17083
  mkdtempSync,
16587
17084
  readFileSync as readFileSync4,
@@ -16592,7 +17089,7 @@ import {
16592
17089
  writeFileSync as writeFileSync2
16593
17090
  } from "node:fs";
16594
17091
  import { homedir as homedir6, tmpdir } from "node:os";
16595
- import { dirname as dirname6, join as join14 } from "node:path";
17092
+ import { dirname as dirname7, join as join14 } from "node:path";
16596
17093
  import { fileURLToPath } from "node:url";
16597
17094
  import * as https from "node:https";
16598
17095
  import * as zlib from "node:zlib";
@@ -16600,7 +17097,7 @@ import * as zlib from "node:zlib";
16600
17097
  // lib/version-injected.ts
16601
17098
  function getInjectedVersion() {
16602
17099
  try {
16603
- const v = "0.3.5";
17100
+ const v = "0.3.8";
16604
17101
  if (typeof v === "string" && /^\d+\.\d+\.\d+/.test(v)) {
16605
17102
  return v;
16606
17103
  }
@@ -16689,7 +17186,7 @@ function readLocalVersion() {
16689
17186
  return injected;
16690
17187
  try {
16691
17188
  const here = fileURLToPath(import.meta.url);
16692
- const root = dirname6(dirname6(here));
17189
+ const root = dirname7(dirname7(here));
16693
17190
  const pkg = JSON.parse(readFileSync4(join14(root, "package.json"), "utf8"));
16694
17191
  return typeof pkg.version === "string" ? pkg.version : "0.0.0";
16695
17192
  } catch {
@@ -16704,7 +17201,7 @@ function defaultCacheFile() {
16704
17201
  }
16705
17202
  function readCache(file) {
16706
17203
  try {
16707
- if (!existsSync3(file))
17204
+ if (!existsSync4(file))
16708
17205
  return null;
16709
17206
  const raw = readFileSync4(file, "utf8");
16710
17207
  const obj = JSON.parse(raw);
@@ -16718,7 +17215,7 @@ function readCache(file) {
16718
17215
  }
16719
17216
  function writeCache(file, entry) {
16720
17217
  try {
16721
- mkdirSync3(dirname6(file), { recursive: true });
17218
+ mkdirSync3(dirname7(file), { recursive: true });
16722
17219
  writeFileSync2(file, JSON.stringify(entry, null, 2), "utf8");
16723
17220
  } catch {}
16724
17221
  }
@@ -16737,7 +17234,7 @@ function fetchLatestTagFromGitHub(repo) {
16737
17234
  });
16738
17235
  }
16739
17236
  function getJsonWithRedirect(url, hopsLeft) {
16740
- return new Promise((resolve10, reject) => {
17237
+ return new Promise((resolve11, reject) => {
16741
17238
  const u = new URL(url);
16742
17239
  const headers = {
16743
17240
  "User-Agent": "codeforge-update-checker",
@@ -16761,12 +17258,12 @@ function getJsonWithRedirect(url, hopsLeft) {
16761
17258
  return;
16762
17259
  }
16763
17260
  const next = new URL(res.headers.location, url).toString();
16764
- getJsonWithRedirect(next, hopsLeft - 1).then(resolve10, reject);
17261
+ getJsonWithRedirect(next, hopsLeft - 1).then(resolve11, reject);
16765
17262
  return;
16766
17263
  }
16767
17264
  if (status === 404) {
16768
17265
  res.resume();
16769
- resolve10(null);
17266
+ resolve11(null);
16770
17267
  return;
16771
17268
  }
16772
17269
  if (status >= 400) {
@@ -16777,7 +17274,7 @@ function getJsonWithRedirect(url, hopsLeft) {
16777
17274
  let body = "";
16778
17275
  res.setEncoding("utf8");
16779
17276
  res.on("data", (chunk) => body += chunk);
16780
- res.on("end", () => resolve10(body));
17277
+ res.on("end", () => resolve11(body));
16781
17278
  });
16782
17279
  req.on("timeout", () => {
16783
17280
  req.destroy();
@@ -16817,7 +17314,7 @@ async function fetchLatestFromNpm(opts) {
16817
17314
  return { version, tarballUrl, integrity };
16818
17315
  }
16819
17316
  function defaultHttpFetcher(url, timeoutMs) {
16820
- return new Promise((resolve10, reject) => {
17317
+ return new Promise((resolve11, reject) => {
16821
17318
  const u = new URL(url);
16822
17319
  const headers = {
16823
17320
  "User-Agent": "codeforge-update-checker",
@@ -16834,7 +17331,7 @@ function defaultHttpFetcher(url, timeoutMs) {
16834
17331
  const status = res.statusCode ?? 0;
16835
17332
  if (status === 404) {
16836
17333
  res.resume();
16837
- resolve10(null);
17334
+ resolve11(null);
16838
17335
  return;
16839
17336
  }
16840
17337
  if (status >= 400) {
@@ -16845,7 +17342,7 @@ function defaultHttpFetcher(url, timeoutMs) {
16845
17342
  let body = "";
16846
17343
  res.setEncoding("utf8");
16847
17344
  res.on("data", (chunk) => body += chunk);
16848
- res.on("end", () => resolve10(body));
17345
+ res.on("end", () => resolve11(body));
16849
17346
  });
16850
17347
  req.on("timeout", () => {
16851
17348
  req.destroy();
@@ -16864,7 +17361,7 @@ async function downloadAndExtractBundle(opts) {
16864
17361
  const tarBuf = zlib.gunzipSync(tarballBuf);
16865
17362
  extractTarToDir(tarBuf, tmpRoot);
16866
17363
  const bundlePath = join14(tmpRoot, "package", "dist", "index.js");
16867
- if (!existsSync3(bundlePath)) {
17364
+ if (!existsSync4(bundlePath)) {
16868
17365
  throw new Error(`bundle_not_found: ${bundlePath}`);
16869
17366
  }
16870
17367
  return { bundlePath, extractDir: tmpRoot };
@@ -16904,7 +17401,7 @@ function extractTarToDir(tarBuf, destRoot) {
16904
17401
  if (typeFlag === "0" || typeFlag === "" || typeFlag === "\x00") {
16905
17402
  const fileBuf = tarBuf.subarray(offset, offset + size);
16906
17403
  const dest = join14(destRoot, fullName);
16907
- mkdirSync3(dirname6(dest), { recursive: true });
17404
+ mkdirSync3(dirname7(dest), { recursive: true });
16908
17405
  writeFileSync2(dest, fileBuf);
16909
17406
  } else if (typeFlag === "5") {
16910
17407
  mkdirSync3(join14(destRoot, fullName), { recursive: true });
@@ -16916,7 +17413,7 @@ function defaultBinaryFetcher(url) {
16916
17413
  return downloadBinary(url, 3);
16917
17414
  }
16918
17415
  function downloadBinary(url, hopsLeft) {
16919
- return new Promise((resolve10, reject) => {
17416
+ return new Promise((resolve11, reject) => {
16920
17417
  const u = new URL(url);
16921
17418
  const req = https.request({
16922
17419
  host: u.hostname,
@@ -16934,7 +17431,7 @@ function downloadBinary(url, hopsLeft) {
16934
17431
  return;
16935
17432
  }
16936
17433
  const next = new URL(res.headers.location, url).toString();
16937
- downloadBinary(next, hopsLeft - 1).then(resolve10, reject);
17434
+ downloadBinary(next, hopsLeft - 1).then(resolve11, reject);
16938
17435
  return;
16939
17436
  }
16940
17437
  if (status >= 400) {
@@ -16944,7 +17441,7 @@ function downloadBinary(url, hopsLeft) {
16944
17441
  }
16945
17442
  const chunks = [];
16946
17443
  res.on("data", (chunk) => chunks.push(chunk));
16947
- res.on("end", () => resolve10(Buffer.concat(chunks)));
17444
+ res.on("end", () => resolve11(Buffer.concat(chunks)));
16948
17445
  });
16949
17446
  req.on("timeout", () => {
16950
17447
  req.destroy();
@@ -16957,16 +17454,16 @@ function downloadBinary(url, hopsLeft) {
16957
17454
  function atomicReplaceBundle(opts) {
16958
17455
  const { source, target, oldVersion } = opts;
16959
17456
  const keep = opts.keepBackups ?? 3;
16960
- if (!existsSync3(source)) {
17457
+ if (!existsSync4(source)) {
16961
17458
  throw new Error(`atomic_source_missing: ${source}`);
16962
17459
  }
16963
- mkdirSync3(dirname6(target), { recursive: true });
17460
+ mkdirSync3(dirname7(target), { recursive: true });
16964
17461
  const newPath = `${target}.new`;
16965
17462
  const backupPath = `${target}.bak.${oldVersion}`;
16966
17463
  let strategy = "rename";
16967
17464
  try {
16968
17465
  copyFileSync(source, newPath);
16969
- if (existsSync3(target)) {
17466
+ if (existsSync4(target)) {
16970
17467
  try {
16971
17468
  renameSync(target, backupPath);
16972
17469
  } catch (e) {
@@ -17002,7 +17499,7 @@ function atomicReplaceBundle(opts) {
17002
17499
  return { backupPath, strategy };
17003
17500
  } catch (e) {
17004
17501
  try {
17005
- if (existsSync3(newPath))
17502
+ if (existsSync4(newPath))
17006
17503
  unlinkSync(newPath);
17007
17504
  } catch {}
17008
17505
  throw e;
@@ -17012,7 +17509,7 @@ function cleanupOldBackups(target, keep) {
17012
17509
  if (keep <= 0)
17013
17510
  return;
17014
17511
  try {
17015
- const dir = dirname6(target);
17512
+ const dir = dirname7(target);
17016
17513
  const base = target.substring(dir.length + 1);
17017
17514
  const prefix = `${base}.bak.`;
17018
17515
  const all = readdirSync(dir).filter((f) => f.startsWith(prefix)).map((f) => {
@@ -17040,7 +17537,7 @@ function loadCompatibility(opts) {
17040
17537
  return null;
17041
17538
  file = join14(root, "compatibility.json");
17042
17539
  }
17043
- if (!existsSync3(file))
17540
+ if (!existsSync4(file))
17044
17541
  return null;
17045
17542
  const raw = readFileSync4(file, "utf8");
17046
17543
  const obj = JSON.parse(raw);
@@ -17063,7 +17560,7 @@ function loadCompatibility(opts) {
17063
17560
  function inferPluginRoot() {
17064
17561
  try {
17065
17562
  const here = fileURLToPath(import.meta.url);
17066
- return dirname6(dirname6(here));
17563
+ return dirname7(dirname7(here));
17067
17564
  } catch {
17068
17565
  return null;
17069
17566
  }
@@ -17103,13 +17600,29 @@ function compareOpencodeVersion(opts) {
17103
17600
  }
17104
17601
 
17105
17602
  // plugins/update-checker.ts
17106
- var PLUGIN_NAME21 = "update-checker";
17603
+ var PLUGIN_NAME22 = "update-checker";
17107
17604
  var PLUGIN_VERSION2 = "2.0.0";
17108
- logLifecycle(PLUGIN_NAME21, "import", { version: PLUGIN_VERSION2 });
17605
+ logLifecycle(PLUGIN_NAME22, "import", { version: PLUGIN_VERSION2 });
17109
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
+ }
17110
17623
  const rt = loadRuntimeSync();
17111
17624
  const u = rt.runtime.update;
17112
- logLifecycle(PLUGIN_NAME21, "activate", {
17625
+ logLifecycle(PLUGIN_NAME22, "activate", {
17113
17626
  version: PLUGIN_VERSION2,
17114
17627
  auto_check_enabled: u.auto_check_enabled,
17115
17628
  interval_hours: u.interval_hours,
@@ -17121,14 +17634,14 @@ var updateCheckerServer = async (ctx) => {
17121
17634
  repo_fallback: u.repo,
17122
17635
  config_source: "codeforge.json"
17123
17636
  });
17124
- await safeAsync(PLUGIN_NAME21, "opencode_version_check", async () => {
17637
+ await safeAsync(PLUGIN_NAME22, "opencode_version_check", async () => {
17125
17638
  const compat = loadCompatibility();
17126
17639
  const opencodeVer = detectOpencodeVersion();
17127
17640
  const verdict = compareOpencodeVersion({
17128
17641
  currentOpencodeVer: opencodeVer,
17129
17642
  compat
17130
17643
  });
17131
- safeWriteLog(PLUGIN_NAME21, {
17644
+ safeWriteLog(PLUGIN_NAME22, {
17132
17645
  level: "info",
17133
17646
  msg: "opencode_version_check",
17134
17647
  opencodeVer,
@@ -17145,14 +17658,14 @@ var updateCheckerServer = async (ctx) => {
17145
17658
  }
17146
17659
  });
17147
17660
  if (!u.auto_check_enabled) {
17148
- safeWriteLog(PLUGIN_NAME21, {
17661
+ safeWriteLog(PLUGIN_NAME22, {
17149
17662
  level: "info",
17150
17663
  msg: "auto-check disabled (codeforge.json update.auto_check_enabled=false)"
17151
17664
  });
17152
17665
  return {};
17153
17666
  }
17154
17667
  setImmediate(() => {
17155
- safeAsync(PLUGIN_NAME21, "checkAndMaybeUpdate", async () => {
17668
+ safeAsync(PLUGIN_NAME22, "checkAndMaybeUpdate", async () => {
17156
17669
  const local = readLocalVersion();
17157
17670
  let npmResult = null;
17158
17671
  try {
@@ -17163,7 +17676,7 @@ var updateCheckerServer = async (ctx) => {
17163
17676
  timeoutMs: 5000
17164
17677
  });
17165
17678
  } catch (e) {
17166
- safeWriteLog(PLUGIN_NAME21, {
17679
+ safeWriteLog(PLUGIN_NAME22, {
17167
17680
  level: "warn",
17168
17681
  msg: "npm_fetch_failed",
17169
17682
  error: e.message
@@ -17172,7 +17685,7 @@ var updateCheckerServer = async (ctx) => {
17172
17685
  return;
17173
17686
  }
17174
17687
  if (!npmResult) {
17175
- safeWriteLog(PLUGIN_NAME21, {
17688
+ safeWriteLog(PLUGIN_NAME22, {
17176
17689
  level: "info",
17177
17690
  msg: "npm_no_release",
17178
17691
  package: u.package,
@@ -17181,7 +17694,7 @@ var updateCheckerServer = async (ctx) => {
17181
17694
  return;
17182
17695
  }
17183
17696
  const hasUpdate = cmpVersion(local, npmResult.version) < 0;
17184
- safeWriteLog(PLUGIN_NAME21, {
17697
+ safeWriteLog(PLUGIN_NAME22, {
17185
17698
  level: "info",
17186
17699
  msg: "npm_check_result",
17187
17700
  local,
@@ -17196,10 +17709,10 @@ var updateCheckerServer = async (ctx) => {
17196
17709
  更新命令:npx ${u.package} install --global`);
17197
17710
  return;
17198
17711
  }
17199
- await safeAsync(PLUGIN_NAME21, "auto_install_bundle", async () => {
17712
+ await safeAsync(PLUGIN_NAME22, "auto_install_bundle", async () => {
17200
17713
  const target = getOpencodeBundlePath();
17201
17714
  if (!target) {
17202
- safeWriteLog(PLUGIN_NAME21, {
17715
+ safeWriteLog(PLUGIN_NAME22, {
17203
17716
  level: "warn",
17204
17717
  msg: "auto_install_skip",
17205
17718
  reason: "无法定位 opencode bundle 路径"
@@ -17218,7 +17731,7 @@ var updateCheckerServer = async (ctx) => {
17218
17731
  oldVersion: local,
17219
17732
  keepBackups: u.backup_keep
17220
17733
  });
17221
- safeWriteLog(PLUGIN_NAME21, {
17734
+ safeWriteLog(PLUGIN_NAME22, {
17222
17735
  level: "info",
17223
17736
  msg: "auto_install_success",
17224
17737
  local,
@@ -17252,13 +17765,13 @@ function getOpencodeBundlePath() {
17252
17765
  candidates.push(join15(localAppData, "opencode", "codeforge", "index.js"));
17253
17766
  }
17254
17767
  for (const c of candidates) {
17255
- if (existsSync4(c))
17768
+ if (existsSync5(c))
17256
17769
  return c;
17257
17770
  }
17258
17771
  return candidates[0] ?? null;
17259
17772
  }
17260
17773
  async function fallbackToGitHubReleases(ctx, u) {
17261
- await safeAsync(PLUGIN_NAME21, "github_fallback", async () => {
17774
+ await safeAsync(PLUGIN_NAME22, "github_fallback", async () => {
17262
17775
  const result = await checkUpdateOnce({
17263
17776
  repo: u.repo,
17264
17777
  intervalMs: u.interval_hours * 3600 * 1000
@@ -17268,7 +17781,7 @@ async function fallbackToGitHubReleases(ctx, u) {
17268
17781
  }
17269
17782
  async function reportLegacyResult(ctx, result, repo) {
17270
17783
  if (result.error && !result.fromCache) {
17271
- safeWriteLog(PLUGIN_NAME21, {
17784
+ safeWriteLog(PLUGIN_NAME22, {
17272
17785
  level: "warn",
17273
17786
  msg: `update check failed: ${result.error}`,
17274
17787
  ...result
@@ -17276,7 +17789,7 @@ async function reportLegacyResult(ctx, result, repo) {
17276
17789
  return;
17277
17790
  }
17278
17791
  if (!result.hasUpdate) {
17279
- safeWriteLog(PLUGIN_NAME21, {
17792
+ safeWriteLog(PLUGIN_NAME22, {
17280
17793
  level: "info",
17281
17794
  msg: `up-to-date (local=${result.local}, remote=${result.remote}${result.fromCache ? ", from_cache" : ""})`,
17282
17795
  ...result
@@ -17286,7 +17799,7 @@ async function reportLegacyResult(ctx, result, repo) {
17286
17799
  const updateCmd = `bunx --bun github:${repo} install`;
17287
17800
  const toast = `[CodeForge] 有新版本:${result.local} → ${result.remote}
17288
17801
  更新命令:${updateCmd}`;
17289
- safeWriteLog(PLUGIN_NAME21, {
17802
+ safeWriteLog(PLUGIN_NAME22, {
17290
17803
  level: "info",
17291
17804
  msg: "new_version_available_github_fallback",
17292
17805
  local: result.local,
@@ -17297,17 +17810,17 @@ async function reportLegacyResult(ctx, result, repo) {
17297
17810
  await postToast(ctx, toast);
17298
17811
  }
17299
17812
  async function postToast(ctx, message) {
17300
- await safeAsync(PLUGIN_NAME21, "client.app.log", async () => {
17813
+ await safeAsync(PLUGIN_NAME22, "client.app.log", async () => {
17301
17814
  await ctx.client.app.log({
17302
17815
  body: {
17303
- service: PLUGIN_NAME21,
17816
+ service: PLUGIN_NAME22,
17304
17817
  level: "info",
17305
17818
  message
17306
17819
  }
17307
17820
  });
17308
17821
  });
17309
17822
  }
17310
- var handler21 = updateCheckerServer;
17823
+ var handler22 = updateCheckerServer;
17311
17824
 
17312
17825
  // plugins/workflow-engine.ts
17313
17826
  init_opencode_plugin_helpers();
@@ -17811,9 +18324,9 @@ async function runStepAutoFeedback(step, adapter) {
17811
18324
  }
17812
18325
 
17813
18326
  // plugins/workflow-engine.ts
17814
- var PLUGIN_NAME22 = "workflow-engine";
17815
- logLifecycle(PLUGIN_NAME22, "import", {});
17816
- var fallbackLog2 = makePluginLogger(PLUGIN_NAME22);
18327
+ var PLUGIN_NAME23 = "workflow-engine";
18328
+ logLifecycle(PLUGIN_NAME23, "import", {});
18329
+ var fallbackLog2 = makePluginLogger(PLUGIN_NAME23);
17817
18330
  var _registry = null;
17818
18331
  async function loadRegistry(workflowsDir) {
17819
18332
  const { loaded, failed } = await loadWorkflowsFromDir(workflowsDir);
@@ -17833,32 +18346,32 @@ async function handleCommandInvoked(raw, workflowsDir = "workflows") {
17833
18346
  const log12 = ctx.log ?? fallbackLog2;
17834
18347
  const command = typeof ctx.command === "string" ? ctx.command : null;
17835
18348
  if (!command) {
17836
- log12.warn(`[${PLUGIN_NAME22}] command.invoked 缺 command 字段`, ctx);
18349
+ log12.warn(`[${PLUGIN_NAME23}] command.invoked 缺 command 字段`, ctx);
17837
18350
  return null;
17838
18351
  }
17839
18352
  const reg = await ensureRegistry(workflowsDir);
17840
18353
  if (reg.errors.length) {
17841
- log12.warn(`[${PLUGIN_NAME22}] 有 ${reg.errors.length} 个 workflow 加载失败`, reg.errors);
18354
+ log12.warn(`[${PLUGIN_NAME23}] 有 ${reg.errors.length} 个 workflow 加载失败`, reg.errors);
17842
18355
  }
17843
18356
  const wf = reg.workflows.find((w) => matchesTrigger(w, command));
17844
18357
  if (!wf) {
17845
- log12.info(`[${PLUGIN_NAME22}] no workflow matches "${command}"`);
18358
+ log12.info(`[${PLUGIN_NAME23}] no workflow matches "${command}"`);
17846
18359
  return null;
17847
18360
  }
17848
- log12.info(`[${PLUGIN_NAME22}] dispatch "${command}" → workflow "${wf.name}"`);
18361
+ log12.info(`[${PLUGIN_NAME23}] dispatch "${command}" → workflow "${wf.name}"`);
17849
18362
  try {
17850
18363
  const result = await run(wf, {
17851
18364
  mode: ctx.adapter ? "real" : "dry_run",
17852
18365
  autonomy: ctx.autonomy ?? "semi",
17853
18366
  adapter: ctx.adapter
17854
18367
  });
17855
- log12.info(`[${PLUGIN_NAME22}] workflow "${wf.name}" 完成 (${result.plan.mode})`, {
18368
+ log12.info(`[${PLUGIN_NAME23}] workflow "${wf.name}" 完成 (${result.plan.mode})`, {
17856
18369
  steps: result.plan.steps.length,
17857
18370
  results: result.results.length
17858
18371
  });
17859
18372
  return result;
17860
18373
  } catch (err) {
17861
- log12.error(`[${PLUGIN_NAME22}] workflow "${wf.name}" 执行失败`, {
18374
+ log12.error(`[${PLUGIN_NAME23}] workflow "${wf.name}" 执行失败`, {
17862
18375
  error: err instanceof Error ? err.message : String(err)
17863
18376
  });
17864
18377
  throw err;
@@ -17867,15 +18380,15 @@ async function handleCommandInvoked(raw, workflowsDir = "workflows") {
17867
18380
  var workflowEngineServer = async (ctx) => {
17868
18381
  const directory = ctx.directory ?? process.cwd();
17869
18382
  const workflowsDir = path17.join(directory, "workflows");
17870
- ensureRegistry(workflowsDir).catch((err) => fallbackLog2.warn(`[${PLUGIN_NAME22}] preload workflows failed`, {
18383
+ ensureRegistry(workflowsDir).catch((err) => fallbackLog2.warn(`[${PLUGIN_NAME23}] preload workflows failed`, {
17871
18384
  error: err instanceof Error ? err.message : String(err)
17872
18385
  }));
17873
- logLifecycle(PLUGIN_NAME22, "activate", { directory, workflowsDir });
18386
+ logLifecycle(PLUGIN_NAME23, "activate", { directory, workflowsDir });
17874
18387
  return {
17875
18388
  "command.execute.before": async (input, output) => {
17876
- await safeAsync(PLUGIN_NAME22, "command.execute.before", async () => {
18389
+ await safeAsync(PLUGIN_NAME23, "command.execute.before", async () => {
17877
18390
  const cmd = input.command.startsWith("/") ? input.command : `/${input.command}`;
17878
- safeWriteLog(PLUGIN_NAME22, {
18391
+ safeWriteLog(PLUGIN_NAME23, {
17879
18392
  hook: "command.execute.before",
17880
18393
  command: cmd,
17881
18394
  sessionID: input.sessionID
@@ -17890,14 +18403,14 @@ var workflowEngineServer = async (ctx) => {
17890
18403
  });
17891
18404
  },
17892
18405
  "chat.message": async (input, output) => {
17893
- await safeAsync(PLUGIN_NAME22, "chat.message", async () => {
18406
+ await safeAsync(PLUGIN_NAME23, "chat.message", async () => {
17894
18407
  const text = extractUserText(output).trim();
17895
18408
  if (!text.startsWith("/"))
17896
18409
  return;
17897
18410
  const cmd = text.split(/\s+/)[0];
17898
18411
  if (!cmd)
17899
18412
  return;
17900
- safeWriteLog(PLUGIN_NAME22, {
18413
+ safeWriteLog(PLUGIN_NAME23, {
17901
18414
  hook: "chat.message",
17902
18415
  command: cmd,
17903
18416
  sessionID: input.sessionID
@@ -17907,7 +18420,7 @@ var workflowEngineServer = async (ctx) => {
17907
18420
  }
17908
18421
  };
17909
18422
  };
17910
- var handler22 = workflowEngineServer;
18423
+ var handler23 = workflowEngineServer;
17911
18424
 
17912
18425
  // src/index.ts
17913
18426
  var PLUGIN_ID = "codeforge";
@@ -17926,16 +18439,17 @@ var HANDLERS = [
17926
18439
  { name: "kh-reminder", init: handler11 },
17927
18440
  { name: "memories-context", init: handler12 },
17928
18441
  { name: "model-fallback", init: handler13 },
17929
- { name: "pwsh-utf8", init: handler14 },
17930
- { name: "session-recovery", init: handler15 },
17931
- { name: "subtask-heartbeat", init: handler16 },
17932
- { name: "subtasks", init: handler17 },
17933
- { name: "terminal-monitor", init: handler18 },
17934
- { 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 },
17935
18449
  { name: "tool-heartbeat", init: handler7 },
17936
- { name: "tool-policy", init: handler20 },
17937
- { name: "update-checker", init: handler21 },
17938
- { name: "workflow-engine", init: handler22 }
18450
+ { name: "tool-policy", init: handler21 },
18451
+ { name: "update-checker", init: handler22 },
18452
+ { name: "workflow-engine", init: handler23 }
17939
18453
  ];
17940
18454
  function makeSerialHook(hookName, fns) {
17941
18455
  return async (input, output) => {
@@ -17950,69 +18464,91 @@ function makeSerialHook(hookName, fns) {
17950
18464
  }
17951
18465
  };
17952
18466
  }
17953
- var codeforgeServer = async (input) => {
17954
- const results = await Promise.allSettled(HANDLERS.map((h) => h.init(input)));
17955
- const hooksList = [];
17956
- results.forEach((r, i) => {
17957
- if (r.status === "fulfilled" && r.value && typeof r.value === "object") {
17958
- hooksList.push(r.value);
17959
- } else if (r.status === "rejected") {
17960
- 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
+ }
17961
18484
  }
17962
- });
17963
- logLifecycle(PLUGIN_ID, "activate", {
17964
- directory: input.directory,
17965
- handlers_total: HANDLERS.length,
17966
- handlers_active: hooksList.length,
17967
- handlers_failed: HANDLERS.length - hooksList.length
17968
- });
17969
- const chatMessageBucket = [];
17970
- const commandExecuteBeforeBucket = [];
17971
- const chatParamsBucket = [];
17972
- const toolExecuteBeforeBucket = [];
17973
- const chatMessagesTransformBucket = [];
17974
- const eventBucket = [];
17975
- const toolMerged = {};
17976
- for (const h of hooksList) {
17977
- if (h["chat.message"])
17978
- chatMessageBucket.push(h["chat.message"]);
17979
- if (h["command.execute.before"])
17980
- commandExecuteBeforeBucket.push(h["command.execute.before"]);
17981
- if (h["chat.params"])
17982
- chatParamsBucket.push(h["chat.params"]);
17983
- if (h["tool.execute.before"])
17984
- toolExecuteBeforeBucket.push(h["tool.execute.before"]);
17985
- if (h["experimental.chat.messages.transform"]) {
17986
- chatMessagesTransformBucket.push(h["experimental.chat.messages.transform"]);
17987
- }
17988
- if (h.event)
17989
- eventBucket.push(h.event);
17990
- if (h.tool)
17991
- Object.assign(toolMerged, h.tool);
17992
- }
17993
- return {
17994
- "chat.message": makeSerialHook("chat.message", chatMessageBucket),
17995
- "command.execute.before": makeSerialHook("command.execute.before", commandExecuteBeforeBucket),
17996
- "chat.params": makeSerialHook("chat.params", chatParamsBucket),
17997
- "tool.execute.before": makeSerialHook("tool.execute.before", toolExecuteBeforeBucket),
17998
- "experimental.chat.messages.transform": makeSerialHook("experimental.chat.messages.transform", chatMessagesTransformBucket),
17999
- event: async (envelope) => {
18000
- await Promise.all(eventBucket.map(async (fn) => {
18001
- try {
18002
- await fn(envelope);
18003
- } catch (err) {
18004
- log12.warn(`[${PLUGIN_ID}] event handler 异常(已隔离)`, {
18005
- error: err instanceof Error ? err.message : String(err)
18006
- });
18007
- }
18008
- }));
18009
- },
18010
- 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
+ };
18011
18543
  };
18012
- };
18544
+ }
18545
+ var codeforgeServer = createCodeforgeServer({ enableDevIsolation: true });
18546
+ var codeforgeDevServer = createCodeforgeServer({ enableDevIsolation: false });
18013
18547
  var pluginModule = { id: PLUGIN_ID, server: codeforgeServer };
18014
18548
  var src_default = pluginModule;
18015
18549
  export {
18016
18550
  src_default as default,
18017
- codeforgeServer
18551
+ createCodeforgeServer,
18552
+ codeforgeServer,
18553
+ codeforgeDevServer
18018
18554
  };