@andyqiu/codeforge 0.3.10 → 0.3.12

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
@@ -1034,17 +1034,17 @@ var require_visit = __commonJS((exports) => {
1034
1034
  visit.BREAK = BREAK;
1035
1035
  visit.SKIP = SKIP;
1036
1036
  visit.REMOVE = REMOVE;
1037
- function visit_(key, node, visitor, path16) {
1038
- const ctrl = callVisitor(key, node, visitor, path16);
1037
+ function visit_(key, node, visitor, path13) {
1038
+ const ctrl = callVisitor(key, node, visitor, path13);
1039
1039
  if (identity.isNode(ctrl) || identity.isPair(ctrl)) {
1040
- replaceNode(key, path16, ctrl);
1041
- return visit_(key, ctrl, visitor, path16);
1040
+ replaceNode(key, path13, ctrl);
1041
+ return visit_(key, ctrl, visitor, path13);
1042
1042
  }
1043
1043
  if (typeof ctrl !== "symbol") {
1044
1044
  if (identity.isCollection(node)) {
1045
- path16 = Object.freeze(path16.concat(node));
1045
+ path13 = Object.freeze(path13.concat(node));
1046
1046
  for (let i = 0;i < node.items.length; ++i) {
1047
- const ci = visit_(i, node.items[i], visitor, path16);
1047
+ const ci = visit_(i, node.items[i], visitor, path13);
1048
1048
  if (typeof ci === "number")
1049
1049
  i = ci - 1;
1050
1050
  else if (ci === BREAK)
@@ -1055,13 +1055,13 @@ var require_visit = __commonJS((exports) => {
1055
1055
  }
1056
1056
  }
1057
1057
  } else if (identity.isPair(node)) {
1058
- path16 = Object.freeze(path16.concat(node));
1059
- const ck = visit_("key", node.key, visitor, path16);
1058
+ path13 = Object.freeze(path13.concat(node));
1059
+ const ck = visit_("key", node.key, visitor, path13);
1060
1060
  if (ck === BREAK)
1061
1061
  return BREAK;
1062
1062
  else if (ck === REMOVE)
1063
1063
  node.key = null;
1064
- const cv = visit_("value", node.value, visitor, path16);
1064
+ const cv = visit_("value", node.value, visitor, path13);
1065
1065
  if (cv === BREAK)
1066
1066
  return BREAK;
1067
1067
  else if (cv === REMOVE)
@@ -1082,17 +1082,17 @@ var require_visit = __commonJS((exports) => {
1082
1082
  visitAsync.BREAK = BREAK;
1083
1083
  visitAsync.SKIP = SKIP;
1084
1084
  visitAsync.REMOVE = REMOVE;
1085
- async function visitAsync_(key, node, visitor, path16) {
1086
- const ctrl = await callVisitor(key, node, visitor, path16);
1085
+ async function visitAsync_(key, node, visitor, path13) {
1086
+ const ctrl = await callVisitor(key, node, visitor, path13);
1087
1087
  if (identity.isNode(ctrl) || identity.isPair(ctrl)) {
1088
- replaceNode(key, path16, ctrl);
1089
- return visitAsync_(key, ctrl, visitor, path16);
1088
+ replaceNode(key, path13, ctrl);
1089
+ return visitAsync_(key, ctrl, visitor, path13);
1090
1090
  }
1091
1091
  if (typeof ctrl !== "symbol") {
1092
1092
  if (identity.isCollection(node)) {
1093
- path16 = Object.freeze(path16.concat(node));
1093
+ path13 = Object.freeze(path13.concat(node));
1094
1094
  for (let i = 0;i < node.items.length; ++i) {
1095
- const ci = await visitAsync_(i, node.items[i], visitor, path16);
1095
+ const ci = await visitAsync_(i, node.items[i], visitor, path13);
1096
1096
  if (typeof ci === "number")
1097
1097
  i = ci - 1;
1098
1098
  else if (ci === BREAK)
@@ -1103,13 +1103,13 @@ var require_visit = __commonJS((exports) => {
1103
1103
  }
1104
1104
  }
1105
1105
  } else if (identity.isPair(node)) {
1106
- path16 = Object.freeze(path16.concat(node));
1107
- const ck = await visitAsync_("key", node.key, visitor, path16);
1106
+ path13 = Object.freeze(path13.concat(node));
1107
+ const ck = await visitAsync_("key", node.key, visitor, path13);
1108
1108
  if (ck === BREAK)
1109
1109
  return BREAK;
1110
1110
  else if (ck === REMOVE)
1111
1111
  node.key = null;
1112
- const cv = await visitAsync_("value", node.value, visitor, path16);
1112
+ const cv = await visitAsync_("value", node.value, visitor, path13);
1113
1113
  if (cv === BREAK)
1114
1114
  return BREAK;
1115
1115
  else if (cv === REMOVE)
@@ -1136,23 +1136,23 @@ var require_visit = __commonJS((exports) => {
1136
1136
  }
1137
1137
  return visitor;
1138
1138
  }
1139
- function callVisitor(key, node, visitor, path16) {
1139
+ function callVisitor(key, node, visitor, path13) {
1140
1140
  if (typeof visitor === "function")
1141
- return visitor(key, node, path16);
1141
+ return visitor(key, node, path13);
1142
1142
  if (identity.isMap(node))
1143
- return visitor.Map?.(key, node, path16);
1143
+ return visitor.Map?.(key, node, path13);
1144
1144
  if (identity.isSeq(node))
1145
- return visitor.Seq?.(key, node, path16);
1145
+ return visitor.Seq?.(key, node, path13);
1146
1146
  if (identity.isPair(node))
1147
- return visitor.Pair?.(key, node, path16);
1147
+ return visitor.Pair?.(key, node, path13);
1148
1148
  if (identity.isScalar(node))
1149
- return visitor.Scalar?.(key, node, path16);
1149
+ return visitor.Scalar?.(key, node, path13);
1150
1150
  if (identity.isAlias(node))
1151
- return visitor.Alias?.(key, node, path16);
1151
+ return visitor.Alias?.(key, node, path13);
1152
1152
  return;
1153
1153
  }
1154
- function replaceNode(key, path16, node) {
1155
- const parent = path16[path16.length - 1];
1154
+ function replaceNode(key, path13, node) {
1155
+ const parent = path13[path13.length - 1];
1156
1156
  if (identity.isCollection(parent)) {
1157
1157
  parent.items[key] = node;
1158
1158
  } else if (identity.isPair(parent)) {
@@ -1711,10 +1711,10 @@ var require_Collection = __commonJS((exports) => {
1711
1711
  var createNode = require_createNode();
1712
1712
  var identity = require_identity();
1713
1713
  var Node = require_Node();
1714
- function collectionFromPath(schema, path16, value) {
1714
+ function collectionFromPath(schema, path13, value) {
1715
1715
  let v = value;
1716
- for (let i = path16.length - 1;i >= 0; --i) {
1717
- const k = path16[i];
1716
+ for (let i = path13.length - 1;i >= 0; --i) {
1717
+ const k = path13[i];
1718
1718
  if (typeof k === "number" && Number.isInteger(k) && k >= 0) {
1719
1719
  const a = [];
1720
1720
  a[k] = v;
@@ -1733,7 +1733,7 @@ var require_Collection = __commonJS((exports) => {
1733
1733
  sourceObjects: new Map
1734
1734
  });
1735
1735
  }
1736
- var isEmptyPath = (path16) => path16 == null || typeof path16 === "object" && !!path16[Symbol.iterator]().next().done;
1736
+ var isEmptyPath = (path13) => path13 == null || typeof path13 === "object" && !!path13[Symbol.iterator]().next().done;
1737
1737
 
1738
1738
  class Collection extends Node.NodeBase {
1739
1739
  constructor(type, schema) {
@@ -1754,11 +1754,11 @@ var require_Collection = __commonJS((exports) => {
1754
1754
  copy.range = this.range.slice();
1755
1755
  return copy;
1756
1756
  }
1757
- addIn(path16, value) {
1758
- if (isEmptyPath(path16))
1757
+ addIn(path13, value) {
1758
+ if (isEmptyPath(path13))
1759
1759
  this.add(value);
1760
1760
  else {
1761
- const [key, ...rest] = path16;
1761
+ const [key, ...rest] = path13;
1762
1762
  const node = this.get(key, true);
1763
1763
  if (identity.isCollection(node))
1764
1764
  node.addIn(rest, value);
@@ -1768,8 +1768,8 @@ var require_Collection = __commonJS((exports) => {
1768
1768
  throw new Error(`Expected YAML collection at ${key}. Remaining path: ${rest}`);
1769
1769
  }
1770
1770
  }
1771
- deleteIn(path16) {
1772
- const [key, ...rest] = path16;
1771
+ deleteIn(path13) {
1772
+ const [key, ...rest] = path13;
1773
1773
  if (rest.length === 0)
1774
1774
  return this.delete(key);
1775
1775
  const node = this.get(key, true);
@@ -1778,8 +1778,8 @@ var require_Collection = __commonJS((exports) => {
1778
1778
  else
1779
1779
  throw new Error(`Expected YAML collection at ${key}. Remaining path: ${rest}`);
1780
1780
  }
1781
- getIn(path16, keepScalar) {
1782
- const [key, ...rest] = path16;
1781
+ getIn(path13, keepScalar) {
1782
+ const [key, ...rest] = path13;
1783
1783
  const node = this.get(key, true);
1784
1784
  if (rest.length === 0)
1785
1785
  return !keepScalar && identity.isScalar(node) ? node.value : node;
@@ -1794,15 +1794,15 @@ var require_Collection = __commonJS((exports) => {
1794
1794
  return n == null || allowScalar && identity.isScalar(n) && n.value == null && !n.commentBefore && !n.comment && !n.tag;
1795
1795
  });
1796
1796
  }
1797
- hasIn(path16) {
1798
- const [key, ...rest] = path16;
1797
+ hasIn(path13) {
1798
+ const [key, ...rest] = path13;
1799
1799
  if (rest.length === 0)
1800
1800
  return this.has(key);
1801
1801
  const node = this.get(key, true);
1802
1802
  return identity.isCollection(node) ? node.hasIn(rest) : false;
1803
1803
  }
1804
- setIn(path16, value) {
1805
- const [key, ...rest] = path16;
1804
+ setIn(path13, value) {
1805
+ const [key, ...rest] = path13;
1806
1806
  if (rest.length === 0) {
1807
1807
  this.set(key, value);
1808
1808
  } else {
@@ -2608,7 +2608,7 @@ var require_merge = __commonJS((exports) => {
2608
2608
 
2609
2609
  // node_modules/yaml/dist/nodes/addPairToJSMap.js
2610
2610
  var require_addPairToJSMap = __commonJS((exports) => {
2611
- var log12 = require_log();
2611
+ var log8 = require_log();
2612
2612
  var merge = require_merge();
2613
2613
  var stringify = require_stringify();
2614
2614
  var identity = require_identity();
@@ -2657,7 +2657,7 @@ var require_addPairToJSMap = __commonJS((exports) => {
2657
2657
  let jsonStr = JSON.stringify(strKey);
2658
2658
  if (jsonStr.length > 40)
2659
2659
  jsonStr = jsonStr.substring(0, 36) + '..."';
2660
- log12.warn(ctx.doc.options.logLevel, `Keys with collection values will be stringified due to JS Object restrictions: ${jsonStr}. Set mapAsMap: true to use object keys.`);
2660
+ log8.warn(ctx.doc.options.logLevel, `Keys with collection values will be stringified due to JS Object restrictions: ${jsonStr}. Set mapAsMap: true to use object keys.`);
2661
2661
  ctx.mapKeyWarned = true;
2662
2662
  }
2663
2663
  return strKey;
@@ -4195,9 +4195,9 @@ var require_Document = __commonJS((exports) => {
4195
4195
  if (assertCollection(this.contents))
4196
4196
  this.contents.add(value);
4197
4197
  }
4198
- addIn(path16, value) {
4198
+ addIn(path13, value) {
4199
4199
  if (assertCollection(this.contents))
4200
- this.contents.addIn(path16, value);
4200
+ this.contents.addIn(path13, value);
4201
4201
  }
4202
4202
  createAlias(node, name) {
4203
4203
  if (!node.anchor) {
@@ -4246,30 +4246,30 @@ var require_Document = __commonJS((exports) => {
4246
4246
  delete(key) {
4247
4247
  return assertCollection(this.contents) ? this.contents.delete(key) : false;
4248
4248
  }
4249
- deleteIn(path16) {
4250
- if (Collection.isEmptyPath(path16)) {
4249
+ deleteIn(path13) {
4250
+ if (Collection.isEmptyPath(path13)) {
4251
4251
  if (this.contents == null)
4252
4252
  return false;
4253
4253
  this.contents = null;
4254
4254
  return true;
4255
4255
  }
4256
- return assertCollection(this.contents) ? this.contents.deleteIn(path16) : false;
4256
+ return assertCollection(this.contents) ? this.contents.deleteIn(path13) : false;
4257
4257
  }
4258
4258
  get(key, keepScalar) {
4259
4259
  return identity.isCollection(this.contents) ? this.contents.get(key, keepScalar) : undefined;
4260
4260
  }
4261
- getIn(path16, keepScalar) {
4262
- if (Collection.isEmptyPath(path16))
4261
+ getIn(path13, keepScalar) {
4262
+ if (Collection.isEmptyPath(path13))
4263
4263
  return !keepScalar && identity.isScalar(this.contents) ? this.contents.value : this.contents;
4264
- return identity.isCollection(this.contents) ? this.contents.getIn(path16, keepScalar) : undefined;
4264
+ return identity.isCollection(this.contents) ? this.contents.getIn(path13, keepScalar) : undefined;
4265
4265
  }
4266
4266
  has(key) {
4267
4267
  return identity.isCollection(this.contents) ? this.contents.has(key) : false;
4268
4268
  }
4269
- hasIn(path16) {
4270
- if (Collection.isEmptyPath(path16))
4269
+ hasIn(path13) {
4270
+ if (Collection.isEmptyPath(path13))
4271
4271
  return this.contents !== undefined;
4272
- return identity.isCollection(this.contents) ? this.contents.hasIn(path16) : false;
4272
+ return identity.isCollection(this.contents) ? this.contents.hasIn(path13) : false;
4273
4273
  }
4274
4274
  set(key, value) {
4275
4275
  if (this.contents == null) {
@@ -4278,13 +4278,13 @@ var require_Document = __commonJS((exports) => {
4278
4278
  this.contents.set(key, value);
4279
4279
  }
4280
4280
  }
4281
- setIn(path16, value) {
4282
- if (Collection.isEmptyPath(path16)) {
4281
+ setIn(path13, value) {
4282
+ if (Collection.isEmptyPath(path13)) {
4283
4283
  this.contents = value;
4284
4284
  } else if (this.contents == null) {
4285
- this.contents = Collection.collectionFromPath(this.schema, Array.from(path16), value);
4285
+ this.contents = Collection.collectionFromPath(this.schema, Array.from(path13), value);
4286
4286
  } else if (assertCollection(this.contents)) {
4287
- this.contents.setIn(path16, value);
4287
+ this.contents.setIn(path13, value);
4288
4288
  }
4289
4289
  }
4290
4290
  setSchema(version, options = {}) {
@@ -6179,9 +6179,9 @@ var require_cst_visit = __commonJS((exports) => {
6179
6179
  visit.BREAK = BREAK;
6180
6180
  visit.SKIP = SKIP;
6181
6181
  visit.REMOVE = REMOVE;
6182
- visit.itemAtPath = (cst, path16) => {
6182
+ visit.itemAtPath = (cst, path13) => {
6183
6183
  let item = cst;
6184
- for (const [field, index] of path16) {
6184
+ for (const [field, index] of path13) {
6185
6185
  const tok = item?.[field];
6186
6186
  if (tok && "items" in tok) {
6187
6187
  item = tok.items[index];
@@ -6190,23 +6190,23 @@ var require_cst_visit = __commonJS((exports) => {
6190
6190
  }
6191
6191
  return item;
6192
6192
  };
6193
- visit.parentCollection = (cst, path16) => {
6194
- const parent = visit.itemAtPath(cst, path16.slice(0, -1));
6195
- const field = path16[path16.length - 1][0];
6193
+ visit.parentCollection = (cst, path13) => {
6194
+ const parent = visit.itemAtPath(cst, path13.slice(0, -1));
6195
+ const field = path13[path13.length - 1][0];
6196
6196
  const coll = parent?.[field];
6197
6197
  if (coll && "items" in coll)
6198
6198
  return coll;
6199
6199
  throw new Error("Parent collection not found");
6200
6200
  };
6201
- function _visit(path16, item, visitor) {
6202
- let ctrl = visitor(item, path16);
6201
+ function _visit(path13, item, visitor) {
6202
+ let ctrl = visitor(item, path13);
6203
6203
  if (typeof ctrl === "symbol")
6204
6204
  return ctrl;
6205
6205
  for (const field of ["key", "value"]) {
6206
6206
  const token = item[field];
6207
6207
  if (token && "items" in token) {
6208
6208
  for (let i = 0;i < token.items.length; ++i) {
6209
- const ci = _visit(Object.freeze(path16.concat([[field, i]])), token.items[i], visitor);
6209
+ const ci = _visit(Object.freeze(path13.concat([[field, i]])), token.items[i], visitor);
6210
6210
  if (typeof ci === "number")
6211
6211
  i = ci - 1;
6212
6212
  else if (ci === BREAK)
@@ -6217,10 +6217,10 @@ var require_cst_visit = __commonJS((exports) => {
6217
6217
  }
6218
6218
  }
6219
6219
  if (typeof ctrl === "function" && field === "key")
6220
- ctrl = ctrl(item, path16);
6220
+ ctrl = ctrl(item, path13);
6221
6221
  }
6222
6222
  }
6223
- return typeof ctrl === "function" ? ctrl(item, path16) : ctrl;
6223
+ return typeof ctrl === "function" ? ctrl(item, path13) : ctrl;
6224
6224
  }
6225
6225
  exports.visit = visit;
6226
6226
  });
@@ -7489,14 +7489,14 @@ var require_parser = __commonJS((exports) => {
7489
7489
  case "scalar":
7490
7490
  case "single-quoted-scalar":
7491
7491
  case "double-quoted-scalar": {
7492
- const fs12 = this.flowScalar(this.type);
7492
+ const fs10 = this.flowScalar(this.type);
7493
7493
  if (atNextItem || it.value) {
7494
- map.items.push({ start, key: fs12, sep: [] });
7494
+ map.items.push({ start, key: fs10, sep: [] });
7495
7495
  this.onKeyLine = true;
7496
7496
  } else if (it.sep) {
7497
- this.stack.push(fs12);
7497
+ this.stack.push(fs10);
7498
7498
  } else {
7499
- Object.assign(it, { key: fs12, sep: [] });
7499
+ Object.assign(it, { key: fs10, sep: [] });
7500
7500
  this.onKeyLine = true;
7501
7501
  }
7502
7502
  return;
@@ -7624,13 +7624,13 @@ var require_parser = __commonJS((exports) => {
7624
7624
  case "scalar":
7625
7625
  case "single-quoted-scalar":
7626
7626
  case "double-quoted-scalar": {
7627
- const fs12 = this.flowScalar(this.type);
7627
+ const fs10 = this.flowScalar(this.type);
7628
7628
  if (!it || it.value)
7629
- fc.items.push({ start: [], key: fs12, sep: [] });
7629
+ fc.items.push({ start: [], key: fs10, sep: [] });
7630
7630
  else if (it.sep)
7631
- this.stack.push(fs12);
7631
+ this.stack.push(fs10);
7632
7632
  else
7633
- Object.assign(it, { key: fs12, sep: [] });
7633
+ Object.assign(it, { key: fs10, sep: [] });
7634
7634
  return;
7635
7635
  }
7636
7636
  case "flow-map-end":
@@ -7794,7 +7794,7 @@ var require_public_api = __commonJS((exports) => {
7794
7794
  var composer = require_composer();
7795
7795
  var Document = require_Document();
7796
7796
  var errors = require_errors();
7797
- var log12 = require_log();
7797
+ var log8 = require_log();
7798
7798
  var identity = require_identity();
7799
7799
  var lineCounter = require_line_counter();
7800
7800
  var parser = require_parser();
@@ -7846,7 +7846,7 @@ var require_public_api = __commonJS((exports) => {
7846
7846
  const doc = parseDocument(src, options);
7847
7847
  if (!doc)
7848
7848
  return null;
7849
- doc.warnings.forEach((warning) => log12.warn(doc.options.logLevel, warning));
7849
+ doc.warnings.forEach((warning) => log8.warn(doc.options.logLevel, warning));
7850
7850
  if (doc.errors.length > 0) {
7851
7851
  if (doc.options.logLevel !== "silent")
7852
7852
  throw doc.errors[0];
@@ -8030,11 +8030,11 @@ function shouldStopByStuck(history, cfg) {
8030
8030
  async function withTimeout3(p, timeoutMs) {
8031
8031
  if (timeoutMs <= 0)
8032
8032
  return await p;
8033
- return await new Promise((resolve11, reject) => {
8033
+ return await new Promise((resolve13, reject) => {
8034
8034
  const timer = setTimeout(() => reject(new Error(`timeout after ${timeoutMs}ms`)), timeoutMs);
8035
8035
  Promise.resolve(p).then((v) => {
8036
8036
  clearTimeout(timer);
8037
- resolve11(v);
8037
+ resolve13(v);
8038
8038
  }, (err) => {
8039
8039
  clearTimeout(timer);
8040
8040
  reject(err);
@@ -8066,7 +8066,7 @@ async function runAutoFeedback(cfg, ctx) {
8066
8066
  if (cmds.length === 0) {
8067
8067
  throw new Error("auto-feedback: 至少需要 test_cmd 或 lint_cmd 之一");
8068
8068
  }
8069
- const log12 = ctx.log ?? (() => {});
8069
+ const log13 = ctx.log ?? (() => {});
8070
8070
  const attempt = async () => {
8071
8071
  let last = {
8072
8072
  cmd: "",
@@ -8103,7 +8103,7 @@ ${excerpt}
8103
8103
 
8104
8104
  请基于错误信息修复 pending-changes(用 ast-edit / pending-changes.stage),完成后我会重新跑测试验证。`;
8105
8105
  const result2 = await ctx.dispatchToAgent("coder", prompt);
8106
- log12("info", `auto-feedback round ${round} dispatch summary: ${result2.summary.slice(0, 200)}`);
8106
+ log13("info", `auto-feedback round ${round} dispatch summary: ${result2.summary.slice(0, 200)}`);
8107
8107
  };
8108
8108
  const debugResult = await runWithAutoDebug({
8109
8109
  attempt,
@@ -8124,7 +8124,7 @@ ${excerpt}
8124
8124
  history: debugResult.history
8125
8125
  };
8126
8126
  if (!debugResult.ok && debugResult.stopReason === "max-rounds") {
8127
- log12("warn", `auto-feedback 达 max_retries=${cfg.max_retries},escalate to ${cfg.escalate_to}`);
8127
+ log13("warn", `auto-feedback 达 max_retries=${cfg.max_retries},escalate to ${cfg.escalate_to}`);
8128
8128
  const lastFail = debugResult.history[debugResult.history.length - 1];
8129
8129
  const excerpt = lastFail?.result ? extractErrorExcerpt(lastFail.result, cfg.error_excerpt_lines) : "(无失败结果)";
8130
8130
  const escalatePrompt = `coder 连续 ${cfg.max_retries} 轮自纠失败,错误片段:
@@ -9007,7 +9007,7 @@ var handler5 = autoLearningServer;
9007
9007
 
9008
9008
  // plugins/channels.ts
9009
9009
  init_opencode_plugin_helpers();
9010
- import { promises as fs2 } from "node:fs";
9010
+ import { promises as fs2, statSync as statSync2 } from "node:fs";
9011
9011
  import * as path4 from "node:path";
9012
9012
 
9013
9013
  // lib/channels.ts
@@ -9285,6 +9285,13 @@ function transformSlackToWebhook(ch, ev) {
9285
9285
  text: { type: "plain_text", text: titleText.slice(0, 150), emoji: true }
9286
9286
  }
9287
9287
  ];
9288
+ const sevLabel = ch.show_severity_label !== false ? severityToLabel(ev.severity) : "";
9289
+ if (sevLabel) {
9290
+ blocks.push({
9291
+ type: "section",
9292
+ text: { type: "mrkdwn", text: sevLabel }
9293
+ });
9294
+ }
9288
9295
  if (message.rendered.trim()) {
9289
9296
  blocks.push({
9290
9297
  type: "section",
@@ -9297,6 +9304,56 @@ function transformSlackToWebhook(ch, ev) {
9297
9304
  elements: [{ type: "mrkdwn", text: mentionText }]
9298
9305
  });
9299
9306
  }
9307
+ const showFields = ch.show_fields ?? [...DEFAULT_SHOW_FIELDS];
9308
+ const labels = { ...DEFAULT_FIELD_LABELS, ...ch.field_labels ?? {} };
9309
+ const fieldLines = pickFieldLines(ev.data, showFields, labels);
9310
+ if (fieldLines.length > 0) {
9311
+ blocks.push({ type: "divider" });
9312
+ blocks.push({
9313
+ type: "section",
9314
+ text: { type: "mrkdwn", text: fieldLines.join(`
9315
+ `) }
9316
+ });
9317
+ }
9318
+ if (ev.severity !== undefined && ev.severity >= 20) {
9319
+ const errMsg = ev.data?.["error"];
9320
+ const stack = ev.data?.["stack"];
9321
+ if (typeof errMsg === "string" && errMsg) {
9322
+ let errContent = `**❌ Error**: ${errMsg.slice(0, 500)}`;
9323
+ if (typeof stack === "string" && stack) {
9324
+ const stackHead = stack.split(`
9325
+ `).slice(0, 5).join(`
9326
+ `);
9327
+ errContent += "\n```\n" + stackHead.slice(0, 1000) + "\n```";
9328
+ }
9329
+ blocks.push({ type: "divider" });
9330
+ blocks.push({
9331
+ type: "section",
9332
+ text: { type: "mrkdwn", text: errContent }
9333
+ });
9334
+ }
9335
+ }
9336
+ const sessionUrl = ev.data?.["session_url"];
9337
+ const logsUrl = ev.data?.["logs_url"];
9338
+ const slackActions = [];
9339
+ if (isValidCtaUrl(sessionUrl)) {
9340
+ slackActions.push({
9341
+ type: "button",
9342
+ text: { type: "plain_text", text: "查看会话", emoji: true },
9343
+ url: sessionUrl,
9344
+ style: "primary"
9345
+ });
9346
+ }
9347
+ if (isValidCtaUrl(logsUrl)) {
9348
+ slackActions.push({
9349
+ type: "button",
9350
+ text: { type: "plain_text", text: "查看日志", emoji: true },
9351
+ url: logsUrl
9352
+ });
9353
+ }
9354
+ if (slackActions.length > 0) {
9355
+ blocks.push({ type: "actions", elements: slackActions });
9356
+ }
9300
9357
  const footerParts = [`event=\`${ev.event}\``];
9301
9358
  if (ev.session_id)
9302
9359
  footerParts.push(`session=\`${ev.session_id.slice(0, 8)}\``);
@@ -9387,6 +9444,13 @@ function transformLarkToWebhook(ch, ev) {
9387
9444
  }).join(" ");
9388
9445
  const headerTemplate = severityToLarkHeader(ev.severity);
9389
9446
  const elements = [];
9447
+ const sevLabel = ch.show_severity_label !== false ? severityToLabel(ev.severity) : "";
9448
+ if (sevLabel) {
9449
+ elements.push({
9450
+ tag: "div",
9451
+ text: { tag: "lark_md", content: sevLabel }
9452
+ });
9453
+ }
9390
9454
  if (messageText || mentionMarkdown) {
9391
9455
  const fullMsg = [messageText, mentionMarkdown].filter(Boolean).join(`
9392
9456
 
@@ -9396,6 +9460,57 @@ function transformLarkToWebhook(ch, ev) {
9396
9460
  text: { tag: "lark_md", content: fullMsg.slice(0, 3000) }
9397
9461
  });
9398
9462
  }
9463
+ const showFields = ch.show_fields ?? [...DEFAULT_SHOW_FIELDS];
9464
+ const labels = { ...DEFAULT_FIELD_LABELS, ...ch.field_labels ?? {} };
9465
+ const fieldLines = pickFieldLines(ev.data, showFields, labels);
9466
+ if (fieldLines.length > 0) {
9467
+ elements.push({ tag: "hr" });
9468
+ elements.push({
9469
+ tag: "div",
9470
+ text: { tag: "lark_md", content: fieldLines.join(`
9471
+ `) }
9472
+ });
9473
+ }
9474
+ if (ev.severity !== undefined && ev.severity >= 20) {
9475
+ const errMsg = ev.data?.["error"];
9476
+ const stack = ev.data?.["stack"];
9477
+ if (typeof errMsg === "string" && errMsg) {
9478
+ let errContent = `**❌ Error**: ${errMsg.slice(0, 500)}`;
9479
+ if (typeof stack === "string" && stack) {
9480
+ const stackHead = stack.split(`
9481
+ `).slice(0, 5).join(`
9482
+ `);
9483
+ errContent += "\n```\n" + stackHead.slice(0, 1000) + "\n```";
9484
+ }
9485
+ elements.push({ tag: "hr" });
9486
+ elements.push({
9487
+ tag: "div",
9488
+ text: { tag: "lark_md", content: errContent }
9489
+ });
9490
+ }
9491
+ }
9492
+ const sessionUrl = ev.data?.["session_url"];
9493
+ const logsUrl = ev.data?.["logs_url"];
9494
+ const larkActions = [];
9495
+ if (isValidCtaUrl(sessionUrl)) {
9496
+ larkActions.push({
9497
+ tag: "button",
9498
+ text: { tag: "plain_text", content: "查看会话" },
9499
+ type: "primary",
9500
+ url: sessionUrl
9501
+ });
9502
+ }
9503
+ if (isValidCtaUrl(logsUrl)) {
9504
+ larkActions.push({
9505
+ tag: "button",
9506
+ text: { tag: "plain_text", content: "查看日志" },
9507
+ type: "default",
9508
+ url: logsUrl
9509
+ });
9510
+ }
9511
+ if (larkActions.length > 0) {
9512
+ elements.push({ tag: "action", actions: larkActions });
9513
+ }
9399
9514
  const footer = [
9400
9515
  `**event**: \`${ev.event}\``,
9401
9516
  ev.session_id ? `**session**: \`${ev.session_id.slice(0, 8)}\`` : null,
@@ -9431,6 +9546,60 @@ function severityToLarkHeader(sev) {
9431
9546
  return "blue";
9432
9547
  return "grey";
9433
9548
  }
9549
+ function severityToLabel(sev) {
9550
+ if (sev === undefined)
9551
+ return "";
9552
+ if (sev >= 40)
9553
+ return "\uD83D\uDEA8 CRITICAL";
9554
+ if (sev >= 30)
9555
+ return "\uD83D\uDD34 ERROR";
9556
+ if (sev >= 20)
9557
+ return "\uD83D\uDFE0 WARN";
9558
+ if (sev >= 10)
9559
+ return "\uD83D\uDD35 INFO";
9560
+ return "";
9561
+ }
9562
+ var DEFAULT_SHOW_FIELDS = [
9563
+ "agent",
9564
+ "model",
9565
+ "duration_ms",
9566
+ "cost",
9567
+ "files_changed",
9568
+ "status"
9569
+ ];
9570
+ var DEFAULT_FIELD_LABELS = {
9571
+ agent: "Agent",
9572
+ model: "Model",
9573
+ duration_ms: "耗时",
9574
+ cost: "费用",
9575
+ files_changed: "改动文件数",
9576
+ status: "状态",
9577
+ error: "错误"
9578
+ };
9579
+ function pickFieldLines(data, showFields, labels) {
9580
+ if (!data)
9581
+ return [];
9582
+ const out = [];
9583
+ for (const key of showFields) {
9584
+ const v = data[key];
9585
+ if (v === undefined || v === null || v === "")
9586
+ continue;
9587
+ const label = labels[key] ?? key;
9588
+ let valueStr;
9589
+ if (key === "duration_ms" && typeof v === "number") {
9590
+ valueStr = v >= 60000 ? `${(v / 60000).toFixed(1)}m` : `${(v / 1000).toFixed(1)}s`;
9591
+ } else if (key === "cost" && typeof v === "number") {
9592
+ valueStr = `$${v.toFixed(4)}`;
9593
+ } else {
9594
+ valueStr = String(v);
9595
+ }
9596
+ out.push(`**${label}**: ${valueStr}`);
9597
+ }
9598
+ return out;
9599
+ }
9600
+ function isValidCtaUrl(url) {
9601
+ return typeof url === "string" && url.startsWith("https://");
9602
+ }
9434
9603
  function computeLarkSign(secret, timestampSec) {
9435
9604
  const stringToSign = `${timestampSec}
9436
9605
  ${secret}`;
@@ -9524,11 +9693,88 @@ function makeChannelEvent(event, data = {}) {
9524
9693
  }
9525
9694
 
9526
9695
  // plugins/channels.ts
9696
+ init_global_config();
9527
9697
  var PLUGIN_NAME6 = "channels";
9528
9698
  logLifecycle(PLUGIN_NAME6, "import", {});
9529
9699
  var fallbackLog = makePluginLogger(PLUGIN_NAME6);
9530
9700
  var _channelsCache = null;
9531
- function loadChannelsFromEnv() {
9701
+ var _activatedDirectory;
9702
+ var KNOWN_TYPES = new Set([
9703
+ "webhook",
9704
+ "file",
9705
+ "exec",
9706
+ "kh",
9707
+ "mcp",
9708
+ "slack",
9709
+ "lark"
9710
+ ]);
9711
+ var REQUIRED_FIELDS = {
9712
+ webhook: ["url"],
9713
+ file: ["path"],
9714
+ exec: [],
9715
+ kh: [],
9716
+ mcp: ["server", "tool"],
9717
+ slack: ["webhook_url"],
9718
+ lark: ["webhook_url"]
9719
+ };
9720
+ function safePeek(o) {
9721
+ const out = {};
9722
+ for (const k of ["type", "name"]) {
9723
+ if (k in o)
9724
+ out[k] = o[k];
9725
+ }
9726
+ return out;
9727
+ }
9728
+ function isChannel(x, log4) {
9729
+ if (!x || typeof x !== "object")
9730
+ return false;
9731
+ const o = x;
9732
+ const t = o["type"];
9733
+ const n = o["name"];
9734
+ if (typeof n !== "string" || n.length === 0) {
9735
+ log4?.warn(`[channels] 配置被忽略:缺少 name 字段`, { sample: safePeek(o) });
9736
+ return false;
9737
+ }
9738
+ if (typeof t !== "string" || !KNOWN_TYPES.has(t)) {
9739
+ log4?.warn(`[channels] 配置 '${n}' 被忽略:未知 type='${String(t)}',合法值=${[...KNOWN_TYPES].join(",")}`);
9740
+ return false;
9741
+ }
9742
+ const required = REQUIRED_FIELDS[t] ?? [];
9743
+ for (const f of required) {
9744
+ const v = o[f];
9745
+ if (typeof v !== "string" || v.length === 0) {
9746
+ log4?.warn(`[channels] 配置 '${n}' (type=${t}) 被忽略:缺少必填字段 '${f}'`);
9747
+ return false;
9748
+ }
9749
+ }
9750
+ if (t === "exec") {
9751
+ const hasArgv = Array.isArray(o["argv"]) && o["argv"].length > 0;
9752
+ const hasCmd = typeof o["command"] === "string" && o["command"].length > 0;
9753
+ if (!hasArgv && !hasCmd) {
9754
+ log4?.warn(`[channels] 配置 '${n}' (type=exec) 被忽略:argv 与 command 均为空`);
9755
+ return false;
9756
+ }
9757
+ }
9758
+ return true;
9759
+ }
9760
+ function resolveGlobalConfigPath() {
9761
+ return path4.join(globalConfigDir(), "channels.json");
9762
+ }
9763
+ function resolveProjectConfigPaths(root) {
9764
+ const r = root ?? _activatedDirectory ?? process.cwd();
9765
+ return {
9766
+ recommended: path4.join(projectConfigDir(r), "channels.json"),
9767
+ legacy: path4.join(projectConfigDir(r), "config", "channels.json")
9768
+ };
9769
+ }
9770
+ var _warnedKeys2 = new Set;
9771
+ function warnOnce3(key, msg, log4) {
9772
+ if (_warnedKeys2.has(key))
9773
+ return;
9774
+ _warnedKeys2.add(key);
9775
+ log4.warn(msg);
9776
+ }
9777
+ function loadChannelsFromEnv(log4 = fallbackLog) {
9532
9778
  const raw = process.env.CODEFORGE_CHANNELS_JSON;
9533
9779
  if (!raw)
9534
9780
  return [];
@@ -9536,22 +9782,103 @@ function loadChannelsFromEnv() {
9536
9782
  const parsed = JSON.parse(raw);
9537
9783
  if (!Array.isArray(parsed))
9538
9784
  return [];
9539
- return parsed.filter((c) => isChannel(c));
9785
+ return parsed.filter((c) => isChannel(c, log4));
9540
9786
  } catch {
9787
+ log4.warn(`[channels] CODEFORGE_CHANNELS_JSON JSON 解析失败,忽略整段 env 配置`);
9541
9788
  return [];
9542
9789
  }
9543
9790
  }
9544
- function isChannel(x) {
9545
- if (!x || typeof x !== "object")
9546
- return false;
9547
- const t = x.type;
9548
- const n = x.name;
9549
- return typeof n === "string" && (t === "webhook" || t === "file" || t === "exec" || t === "kh" || t === "mcp");
9791
+ function loadChannelsFromGlobal(log4 = fallbackLog) {
9792
+ const filePath = resolveGlobalConfigPath();
9793
+ const raw = loadJsonIfExists(filePath);
9794
+ if (!raw)
9795
+ return [];
9796
+ try {
9797
+ const st = statSync2(filePath);
9798
+ const perm = st.mode & 511;
9799
+ if ((perm & 63) !== 0) {
9800
+ warnOnce3(`global-perm:${filePath}`, `[channels] 全局 channels.json 权限 0${perm.toString(8)} 含 group/other 可读位,` + `含 webhook 时建议 chmod 600 ${filePath}`, log4);
9801
+ }
9802
+ } catch {}
9803
+ const arr = Array.isArray(raw) ? raw : raw.channels;
9804
+ if (!Array.isArray(arr))
9805
+ return [];
9806
+ return arr.filter((c) => isChannel(c, log4));
9807
+ }
9808
+ function loadChannelsFromFile(root, log4 = fallbackLog) {
9809
+ const { recommended, legacy } = resolveProjectConfigPaths(root);
9810
+ let raw = loadJsonIfExists(recommended);
9811
+ if (raw === null) {
9812
+ const legacyRaw = loadJsonIfExists(legacy);
9813
+ if (legacyRaw !== null) {
9814
+ log4.warn(`[channels] 检测到 0.3.10 兼容路径 ${legacy},建议迁移到 0.3.11 推荐路径 ${recommended}` + `(与 KH 配置惯例统一)。两条路径同时存在时只读推荐路径。`);
9815
+ raw = legacyRaw;
9816
+ }
9817
+ }
9818
+ if (!raw)
9819
+ return [];
9820
+ const arr = Array.isArray(raw) ? raw : raw.channels;
9821
+ if (!Array.isArray(arr))
9822
+ return [];
9823
+ return arr.filter((c) => isChannel(c, log4));
9824
+ }
9825
+ function parseRawEnvLength(raw) {
9826
+ if (raw === undefined || raw === "")
9827
+ return "not-set";
9828
+ try {
9829
+ const p = JSON.parse(raw);
9830
+ return Array.isArray(p) ? p.length : "invalid";
9831
+ } catch {
9832
+ return "invalid";
9833
+ }
9834
+ }
9835
+ function mergeByName(layers, log4) {
9836
+ const map = new Map;
9837
+ for (const { layer, items } of layers) {
9838
+ for (const c of items) {
9839
+ const prev = map.get(c.name);
9840
+ if (prev) {
9841
+ const typeNote = prev.channel.type !== c.type ? `(type: ${prev.channel.type} → ${c.type})` : "";
9842
+ log4.warn(`[channels] channel '${c.name}' (${layer} 层) 覆盖 ${prev.layer} 层同名配置${typeNote}`);
9843
+ }
9844
+ map.set(c.name, { layer, channel: c });
9845
+ }
9846
+ }
9847
+ return [...map.values()].map((e) => e.channel);
9550
9848
  }
9551
- function ensureChannels() {
9849
+ function ensureChannels(log4 = fallbackLog) {
9552
9850
  if (_channelsCache)
9553
9851
  return _channelsCache;
9554
- _channelsCache = loadChannelsFromEnv();
9852
+ const rawEnv = process.env.CODEFORGE_CHANNELS_JSON;
9853
+ const rawLen = parseRawEnvLength(rawEnv);
9854
+ if (rawLen === 0) {
9855
+ log4.warn(`[channels] CODEFORGE_CHANNELS_JSON 显式为空数组,跳过 global + project 两层(全部通知已禁用)`);
9856
+ _channelsCache = [];
9857
+ return _channelsCache;
9858
+ }
9859
+ const fromGlobal = loadChannelsFromGlobal(log4);
9860
+ const fromFile = loadChannelsFromFile(undefined, log4);
9861
+ const fromEnv = rawLen === "not-set" ? [] : loadChannelsFromEnv(log4);
9862
+ if (rawLen === "not-set") {
9863
+ _channelsCache = mergeByName([
9864
+ { layer: "global", items: fromGlobal },
9865
+ { layer: "project", items: fromFile }
9866
+ ], log4);
9867
+ return _channelsCache;
9868
+ }
9869
+ if (fromEnv.length === 0) {
9870
+ log4.warn(`[channels] CODEFORGE_CHANNELS_JSON 含 ${rawLen === "invalid" ? "非数组" : rawLen} 项但全部被过滤,` + `退回 global + project 两层(global ${fromGlobal.length} 项 + project ${fromFile.length} 项)`);
9871
+ _channelsCache = mergeByName([
9872
+ { layer: "global", items: fromGlobal },
9873
+ { layer: "project", items: fromFile }
9874
+ ], log4);
9875
+ return _channelsCache;
9876
+ }
9877
+ _channelsCache = mergeByName([
9878
+ { layer: "global", items: fromGlobal },
9879
+ { layer: "project", items: fromFile },
9880
+ { layer: "env", items: fromEnv }
9881
+ ], log4);
9555
9882
  return _channelsCache;
9556
9883
  }
9557
9884
  var _limiter = null;
@@ -9698,6 +10025,7 @@ var TRIGGER_EVENT_TYPES3 = new Set([
9698
10025
  "subtasks.completed"
9699
10026
  ]);
9700
10027
  var channelsServer = async (ctx) => {
10028
+ _activatedDirectory = ctx.directory;
9701
10029
  logLifecycle(PLUGIN_NAME6, "activate", {
9702
10030
  directory: ctx.directory,
9703
10031
  triggerEventTypes: [...TRIGGER_EVENT_TYPES3],
@@ -10401,7 +10729,7 @@ class PendingChangesStore {
10401
10729
  const currentNormHash = crypto3.createHash("sha256").update(normalize3(current)).digest("hex");
10402
10730
  const origNormHash = crypto3.createHash("sha256").update(normalize3(origStored)).digest("hex");
10403
10731
  if (currentNormHash === origNormHash) {
10404
- console.warn(`[pending-changes] ADR-0044: ${id} 仅 EOL 漂移,已接受 apply ` + `(raw hash mismatch: ${data.meta.source_hash?.slice(0, 8)}… → ${currentHash.slice(0, 8)}…)`);
10732
+ console.warn(`[pending-changes] ADR:pending-changes-eol-tolerant-apply: ${id} 仅 EOL 漂移,已接受 apply ` + `(raw hash mismatch: ${data.meta.source_hash?.slice(0, 8)}… → ${currentHash.slice(0, 8)}…)`);
10405
10733
  } else {
10406
10734
  throw new Error(`apply: 目标文件已被修改(漂移)。原 hash=${data.meta.source_hash?.slice(0, 8)}… → 现 hash=${currentHash.slice(0, 8)}…,` + `请重新 stage 后再 apply`);
10407
10735
  }
@@ -10547,7 +10875,7 @@ var ArgsSchema15 = z16.discriminatedUnion("action", [
10547
10875
  target: z16.string().min(1).describe("相对路径(相对仓库根),如 src/foo.ts"),
10548
10876
  content: z16.string().describe("整文件新内容(UTF-8 文本)"),
10549
10877
  description: z16.string().optional().describe("一句话说明这次改动的意图(建议 ≤ 80 字)"),
10550
- force_eol: z16.enum(["lf", "crlf", "preserve"]).optional().describe("强制 EOL 转换(ADR-0033):lf/crlf 显式覆盖,preserve=C-EOL 智能保留(默认行为)。" + "严格期望某 EOL 的场景(脚本 shebang / lint 严格的 markdown 等)建议传 lf;" + "不确定时不传,让默认 C-EOL 处理。docs/adr/*.md 自动判定 lf。")
10878
+ force_eol: z16.enum(["lf", "crlf", "preserve"]).optional().describe("强制 EOL 转换(ADR:pending-changes-force-eol):lf/crlf 显式覆盖,preserve=C-EOL 智能保留(默认行为)。" + "严格期望某 EOL 的场景(脚本 shebang / lint 严格的 markdown 等)建议传 lf;" + "不确定时不传,让默认 C-EOL 处理。docs/adr/*.md 自动判定 lf。")
10551
10879
  }),
10552
10880
  z16.object({
10553
10881
  action: z16.literal("list"),
@@ -12211,6 +12539,11 @@ import { z as z26 } from "zod";
12211
12539
  // lib/model-config.ts
12212
12540
  import { promises as fs7 } from "node:fs";
12213
12541
  import * as path10 from "node:path";
12542
+
12543
+ // lib/model-tier.ts
12544
+ var TIER_ORDER = ["quick", "balanced", "deep", "ultra"];
12545
+
12546
+ // lib/model-config.ts
12214
12547
  var CONFIG_FILE = "codeforge.json";
12215
12548
  var DEFAULT_RUNTIME_FALLBACK = {
12216
12549
  enabled: true,
@@ -12315,11 +12648,29 @@ function validateConfig(input) {
12315
12648
  return { ok: false, warnings, error: r.error };
12316
12649
  runtime = r.cfg;
12317
12650
  }
12651
+ let tiers;
12652
+ if (obj.tiers !== undefined) {
12653
+ const r = normalizeTiers(obj.tiers);
12654
+ if (!r.ok)
12655
+ return { ok: false, warnings, error: r.error };
12656
+ tiers = r.cfg;
12657
+ }
12318
12658
  for (const [name, agent] of Object.entries(agents)) {
12319
12659
  if (agent.category && !categories?.[agent.category]) {
12320
12660
  warnings.push(`agent[${name}].category="${agent.category}" 未在 categories 定义,将忽略 category 链`);
12321
12661
  }
12322
12662
  }
12663
+ for (const [name, agent] of Object.entries(agents)) {
12664
+ if (!agent.tier)
12665
+ continue;
12666
+ const mappedCat = tiers?.category_map?.[agent.tier];
12667
+ const hasOverride = agent.tier_overrides?.[agent.tier] !== undefined;
12668
+ if (!mappedCat && !hasOverride) {
12669
+ warnings.push(`agent[${name}].tier="${agent.tier}" 既无 models.tiers.category_map["${agent.tier}"] 映射,` + `也无 tier_overrides["${agent.tier}"];该 agent 将不参与 tier 体系(adapter 返 null)。`);
12670
+ } else if (mappedCat && !categories?.[mappedCat] && !hasOverride) {
12671
+ warnings.push(`agent[${name}].tier="${agent.tier}" 通过 category_map 映射到 "${mappedCat}",` + `但 categories.${mappedCat} 不存在;该 agent 将不参与 tier 体系。`);
12672
+ }
12673
+ }
12323
12674
  return {
12324
12675
  ok: true,
12325
12676
  warnings,
@@ -12328,7 +12679,8 @@ function validateConfig(input) {
12328
12679
  _doc: typeof obj._doc === "string" ? obj._doc : undefined,
12329
12680
  agents,
12330
12681
  categories,
12331
- runtime_fallback: runtime
12682
+ runtime_fallback: runtime,
12683
+ tiers
12332
12684
  }
12333
12685
  };
12334
12686
  }
@@ -12349,6 +12701,23 @@ function normalizeAgent(name, raw) {
12349
12701
  const thinking = o.thinking !== undefined ? normalizeThinking(`agent[${name}]`, o.thinking) : undefined;
12350
12702
  if (thinking && !thinking.ok)
12351
12703
  return { ok: false, error: thinking.error };
12704
+ let tier;
12705
+ if (o.tier !== undefined) {
12706
+ if (typeof o.tier !== "string" || !isValidTierLevel(o.tier)) {
12707
+ return {
12708
+ ok: false,
12709
+ error: `agent[${name}].tier="${String(o.tier)}" 不是合法 TierLevel (期望 ${TIER_ORDER.join("/")})`
12710
+ };
12711
+ }
12712
+ tier = o.tier;
12713
+ }
12714
+ let tierOverrides;
12715
+ if (o.tier_overrides !== undefined) {
12716
+ const r = normalizeTierOverrides(name, o.tier_overrides);
12717
+ if (!r.ok)
12718
+ return { ok: false, error: r.error };
12719
+ tierOverrides = r.value;
12720
+ }
12352
12721
  return {
12353
12722
  ok: true,
12354
12723
  binding: {
@@ -12357,6 +12726,8 @@ function normalizeAgent(name, raw) {
12357
12726
  category: typeof o.category === "string" ? o.category : undefined,
12358
12727
  thinking: thinking?.value,
12359
12728
  fallback_models: fallbacks.value,
12729
+ tier,
12730
+ tier_overrides: tierOverrides,
12360
12731
  _doc: typeof o._doc === "string" ? o._doc : undefined
12361
12732
  }
12362
12733
  };
@@ -12453,6 +12824,99 @@ function normalizeRuntime(raw) {
12453
12824
  cfg._doc = o._doc;
12454
12825
  return { ok: true, cfg };
12455
12826
  }
12827
+ function isValidTierLevel(x) {
12828
+ return typeof x === "string" && TIER_ORDER.includes(x);
12829
+ }
12830
+ function normalizeTierOverrides(agentName, raw) {
12831
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
12832
+ return {
12833
+ ok: false,
12834
+ error: `agent[${agentName}].tier_overrides 必须是 object(不是 array / null / 其他类型)`
12835
+ };
12836
+ }
12837
+ const o = raw;
12838
+ const result = {};
12839
+ for (const [key, value] of Object.entries(o)) {
12840
+ if (!isValidTierLevel(key)) {
12841
+ return {
12842
+ ok: false,
12843
+ error: `agent[${agentName}].tier_overrides.${key}: 非法 TierLevel key (期望 ${TIER_ORDER.join("/")})`
12844
+ };
12845
+ }
12846
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
12847
+ return {
12848
+ ok: false,
12849
+ error: `agent[${agentName}].tier_overrides.${key}: 必须是 object`
12850
+ };
12851
+ }
12852
+ const ov = value;
12853
+ const partial = {};
12854
+ if (ov.level !== undefined) {
12855
+ if (ov.level !== key) {
12856
+ return {
12857
+ ok: false,
12858
+ error: `agent[${agentName}].tier_overrides.${key}.level="${String(ov.level)}" 与 key "${key}" 错配`
12859
+ };
12860
+ }
12861
+ partial.level = key;
12862
+ }
12863
+ if (ov.model !== undefined) {
12864
+ if (typeof ov.model !== "string" || !PROVIDER_MODEL_RE.test(ov.model)) {
12865
+ return {
12866
+ ok: false,
12867
+ error: `agent[${agentName}].tier_overrides.${key}.model="${String(ov.model)}" 格式非法 (期望 <provider>/<id>)`
12868
+ };
12869
+ }
12870
+ partial.model = ov.model;
12871
+ }
12872
+ if (ov.thinking !== undefined) {
12873
+ const t = normalizeThinking(`agent[${agentName}].tier_overrides.${key}`, ov.thinking);
12874
+ if (!t.ok)
12875
+ return { ok: false, error: t.error };
12876
+ partial.thinking = t.value;
12877
+ }
12878
+ if (ov.fallback_models !== undefined) {
12879
+ const f = normalizeFallbackList(`agent[${agentName}].tier_overrides.${key}.fallback_models`, ov.fallback_models);
12880
+ if (!f.ok)
12881
+ return { ok: false, error: f.error };
12882
+ partial.fallback_models = f.value;
12883
+ }
12884
+ result[key] = partial;
12885
+ }
12886
+ return { ok: true, value: result };
12887
+ }
12888
+ function normalizeTiers(raw) {
12889
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
12890
+ return { ok: false, error: "models.tiers 必须是 object(不是 array / null)" };
12891
+ }
12892
+ const o = raw;
12893
+ const cfg = {};
12894
+ if (o.category_map !== undefined) {
12895
+ if (!o.category_map || typeof o.category_map !== "object" || Array.isArray(o.category_map)) {
12896
+ return { ok: false, error: "models.tiers.category_map 必须是 object" };
12897
+ }
12898
+ const map = {};
12899
+ for (const [key, value] of Object.entries(o.category_map)) {
12900
+ if (!isValidTierLevel(key)) {
12901
+ return {
12902
+ ok: false,
12903
+ error: `models.tiers.category_map.${key}: 非法 TierLevel key (期望 ${TIER_ORDER.join("/")})`
12904
+ };
12905
+ }
12906
+ if (typeof value !== "string" || value.length === 0) {
12907
+ return {
12908
+ ok: false,
12909
+ error: `models.tiers.category_map.${key}: 必须是非空字符串(category 名)`
12910
+ };
12911
+ }
12912
+ map[key] = value;
12913
+ }
12914
+ cfg.category_map = map;
12915
+ }
12916
+ if (typeof o._doc === "string")
12917
+ cfg._doc = o._doc;
12918
+ return { ok: true, cfg };
12919
+ }
12456
12920
  function resolveAgentModel(config, agent) {
12457
12921
  const a = config.agents[agent];
12458
12922
  if (!a)
@@ -12576,6 +13040,8 @@ function toEntry(r) {
12576
13040
  init_opencode_plugin_helpers();
12577
13041
 
12578
13042
  // lib/codeforge-runtime.ts
13043
+ import { promises as fs8 } from "node:fs";
13044
+ import * as path11 from "node:path";
12579
13045
  var DEFAULT_RUNTIME = {
12580
13046
  autonomy: {
12581
13047
  default_mode: "semi",
@@ -12617,6 +13083,21 @@ function loadRuntimeSync(opts = {}) {
12617
13083
  }
12618
13084
  return parseRuntime(raw, hit);
12619
13085
  }
13086
+ async function loadRuntime(opts = {}) {
13087
+ const root = opts.root ?? process.cwd();
13088
+ const abs = path11.resolve(root, opts.file ?? CONFIG_FILE);
13089
+ let raw;
13090
+ try {
13091
+ raw = await fs8.readFile(abs, "utf8");
13092
+ } catch (e) {
13093
+ const code = e.code;
13094
+ if (code === "ENOENT") {
13095
+ return emptyResult({ reason: `${CONFIG_FILE} not found at ${abs} (using built-in defaults)` });
13096
+ }
13097
+ return emptyResult({ path: abs, reason: `read_failed: ${e.message}` });
13098
+ }
13099
+ return parseRuntime(raw, abs);
13100
+ }
12620
13101
  function emptyResult(opts) {
12621
13102
  return {
12622
13103
  ok: false,
@@ -13384,7 +13865,7 @@ var codeforgeToolsServer = async (ctx) => {
13384
13865
  target: z27.string().optional().describe("仅 stage:相对仓库根的目标文件路径"),
13385
13866
  content: z27.string().optional().describe("仅 stage:整文件新内容"),
13386
13867
  description: z27.string().optional().describe("仅 stage:一句话说明改动意图"),
13387
- force_eol: z27.enum(["lf", "crlf", "preserve"]).optional().describe("仅 stage:强制 EOL 转换(ADR-0033)。lf/crlf 显式覆盖,preserve=C-EOL 智能保留(默认)。不确定时不传"),
13868
+ force_eol: z27.enum(["lf", "crlf", "preserve"]).optional().describe("仅 stage:强制 EOL 转换(ADR:pending-changes-force-eol)。lf/crlf 显式覆盖,preserve=C-EOL 智能保留(默认)。不确定时不传"),
13388
13869
  id: z27.string().optional().describe("show / diff / apply / discard:pending change ID"),
13389
13870
  status: z27.enum(["pending", "applied", "discarded"]).optional().describe("仅 list:按状态过滤")
13390
13871
  },
@@ -14366,14 +14847,14 @@ var khReminderServer = async (ctx) => {
14366
14847
  var handler10 = khReminderServer;
14367
14848
 
14368
14849
  // lib/memories.ts
14369
- import { promises as fs8 } from "node:fs";
14370
- import * as path11 from "node:path";
14850
+ import { promises as fs9 } from "node:fs";
14851
+ import * as path12 from "node:path";
14371
14852
  import * as os4 from "node:os";
14372
14853
  function resolveConfig(c) {
14373
14854
  return {
14374
14855
  projectRoot: c.projectRoot,
14375
14856
  homeDir: c.homeDir ?? os4.homedir(),
14376
- projectName: c.projectName ?? path11.basename(c.projectRoot),
14857
+ projectName: c.projectName ?? path12.basename(c.projectRoot),
14377
14858
  kh: c.kh,
14378
14859
  now: c.now ?? Date.now,
14379
14860
  log: c.log ?? (() => {}),
@@ -14382,13 +14863,13 @@ function resolveConfig(c) {
14382
14863
  }
14383
14864
  function fileFor(scope, cfg) {
14384
14865
  if (scope === "project") {
14385
- return path11.join(cfg.projectRoot, ".codeforge", "memories.json");
14866
+ return path12.join(cfg.projectRoot, ".codeforge", "memories.json");
14386
14867
  }
14387
- return path11.join(cfg.homeDir, ".codeforge", "memories.json");
14868
+ return path12.join(cfg.homeDir, ".codeforge", "memories.json");
14388
14869
  }
14389
14870
  async function readBank(p) {
14390
14871
  try {
14391
- const raw = await fs8.readFile(p, "utf8");
14872
+ const raw = await fs9.readFile(p, "utf8");
14392
14873
  const arr = JSON.parse(raw);
14393
14874
  if (!Array.isArray(arr))
14394
14875
  return [];
@@ -14398,10 +14879,10 @@ async function readBank(p) {
14398
14879
  }
14399
14880
  }
14400
14881
  async function writeBank(p, items) {
14401
- await fs8.mkdir(path11.dirname(p), { recursive: true });
14882
+ await fs9.mkdir(path12.dirname(p), { recursive: true });
14402
14883
  const tmp = `${p}.tmp`;
14403
- await fs8.writeFile(tmp, JSON.stringify(items, null, 2), "utf8");
14404
- await fs8.rename(tmp, p);
14884
+ await fs9.writeFile(tmp, JSON.stringify(items, null, 2), "utf8");
14885
+ await fs9.rename(tmp, p);
14405
14886
  }
14406
14887
  function isMemory(x) {
14407
14888
  if (!x || typeof x !== "object")
@@ -15002,25 +15483,17 @@ function extractEndedSessionID(event) {
15002
15483
  }
15003
15484
  return null;
15004
15485
  }
15005
- function extractSubtaskPart(event) {
15006
- if (!event || typeof event !== "object")
15007
- return null;
15008
- const e = event;
15009
- if (e.type !== "message.part.updated")
15486
+ function extractTaskArgs(args) {
15487
+ if (!args || typeof args !== "object")
15010
15488
  return null;
15011
- const part = e.properties?.part;
15012
- if (!part || typeof part !== "object")
15489
+ const a = args;
15490
+ const rawDesc = typeof a["description"] === "string" ? a["description"] : null;
15491
+ const rawPrompt = typeof a["prompt"] === "string" ? a["prompt"] : null;
15492
+ const description26 = rawDesc ?? (rawPrompt ? rawPrompt.slice(0, 60) : null);
15493
+ const subagentType = typeof a["subagent_type"] === "string" && a["subagent_type"] || typeof a["agent"] === "string" && a["agent"] || typeof a["agentType"] === "string" && a["agentType"] || typeof a["agent_type"] === "string" && a["agent_type"] || null;
15494
+ if (!description26 && !subagentType)
15013
15495
  return null;
15014
- const p = part;
15015
- if (p.type !== "subtask")
15016
- return null;
15017
- if (typeof p.sessionID !== "string" || p.sessionID === "")
15018
- return null;
15019
- if (typeof p.agent !== "string" || p.agent === "")
15020
- return null;
15021
- if (typeof p.description !== "string" || p.description === "")
15022
- return null;
15023
- return { parentID: p.sessionID, agent: p.agent, description: p.description };
15496
+ return { description: description26, subagentType };
15024
15497
  }
15025
15498
  function enqueuePendingTask(parentID, entry, now = Date.now()) {
15026
15499
  const ts = entry.ts ?? now;
@@ -15249,21 +15722,6 @@ var subtaskHeartbeatServer = async (ctx) => {
15249
15722
  return {
15250
15723
  event: async ({ event }) => {
15251
15724
  await safeAsync(PLUGIN_NAME13, "event", async () => {
15252
- const subtask = extractSubtaskPart(event);
15253
- if (subtask) {
15254
- enqueuePendingTask(subtask.parentID, {
15255
- agent: subtask.agent,
15256
- description: subtask.description
15257
- });
15258
- safeWriteLog(PLUGIN_NAME13, {
15259
- hook: "event",
15260
- type: "message.part.updated.subtask",
15261
- parent: subtask.parentID,
15262
- agent: subtask.agent,
15263
- description_len: subtask.description.length
15264
- });
15265
- return;
15266
- }
15267
15725
  const created = extractCreatedChild(event);
15268
15726
  if (created) {
15269
15727
  const pending = dequeuePendingTask(created.parentID);
@@ -15279,7 +15737,8 @@ var subtaskHeartbeatServer = async (ctx) => {
15279
15737
  child: created.childID,
15280
15738
  parent: created.parentID,
15281
15739
  pending_task_matched: pending !== null,
15282
- agent: record.agent
15740
+ agent: record.agent,
15741
+ description_len: record.description?.length ?? 0
15283
15742
  });
15284
15743
  const startToast = buildStartToast(record);
15285
15744
  const sent = await showToast3(client, { ...startToast, duration: START_TOAST_DURATION_MS }, log7);
@@ -15310,13 +15769,38 @@ var subtaskHeartbeatServer = async (ctx) => {
15310
15769
  }
15311
15770
  });
15312
15771
  },
15313
- "tool.execute.before": async (input) => {
15314
- if (inflight3.size === 0)
15772
+ "tool.execute.before": async (input, output) => {
15773
+ const isTaskTool = input?.tool === "task";
15774
+ if (inflight3.size === 0 && !isTaskTool)
15315
15775
  return;
15316
15776
  await safeAsync(PLUGIN_NAME13, "tool.execute.before", async () => {
15317
15777
  if (!input || typeof input.sessionID !== "string" || typeof input.tool !== "string")
15318
15778
  return;
15319
- recordToolBeat(input.sessionID, input.tool);
15779
+ if (isTaskTool) {
15780
+ const args = output?.args ?? null;
15781
+ safeWriteLog(PLUGIN_NAME13, {
15782
+ hook: "tool.execute.before.task",
15783
+ sessionID: input.sessionID,
15784
+ args_keys: args && typeof args === "object" ? Object.keys(args) : null,
15785
+ args_raw: args
15786
+ });
15787
+ const extracted = extractTaskArgs(args);
15788
+ if (extracted) {
15789
+ enqueuePendingTask(input.sessionID, {
15790
+ agent: extracted.subagentType,
15791
+ description: extracted.description
15792
+ });
15793
+ safeWriteLog(PLUGIN_NAME13, {
15794
+ hook: "tool.execute.before.task.enqueued",
15795
+ parent: input.sessionID,
15796
+ agent: extracted.subagentType,
15797
+ description_len: extracted.description?.length ?? 0
15798
+ });
15799
+ }
15800
+ }
15801
+ if (inflight3.has(input.sessionID)) {
15802
+ recordToolBeat(input.sessionID, input.tool);
15803
+ }
15320
15804
  });
15321
15805
  }
15322
15806
  };
@@ -15389,69 +15873,360 @@ var parallelStatusServer = async (ctx) => {
15389
15873
  };
15390
15874
  var handler14 = parallelStatusServer;
15391
15875
 
15392
- // plugins/pwsh-utf8.ts
15393
- init_opencode_plugin_helpers();
15394
- var PLUGIN_NAME15 = "pwsh-utf8";
15395
- logLifecycle(PLUGIN_NAME15, "import", {});
15396
- var PRELUDE = "chcp 65001 *> $null; " + "[Console]::OutputEncoding = [System.Text.UTF8Encoding]::new(); " + "$OutputEncoding = [System.Text.UTF8Encoding]::new(); ";
15397
- function prependUtf8Prelude(command) {
15398
- if (typeof command !== "string")
15399
- return;
15400
- if (command.length === 0)
15401
- return command;
15402
- if (/chcp\s+65001/i.test(command))
15403
- return command;
15404
- if (/\[Console\]::OutputEncoding\s*=/i.test(command))
15405
- return command;
15406
- return PRELUDE + command;
15407
- }
15408
- var handler15 = async (_ctx) => {
15409
- const enabled = process.platform === "win32" && process.env.CODEFORGE_DISABLE_PWSH_UTF8 !== "1";
15410
- const reason = enabled ? "win32" : process.platform !== "win32" ? "non-win32" : "disabled-by-env";
15411
- logLifecycle(PLUGIN_NAME15, "activate", { enabled, platform: process.platform, reason });
15412
- if (!enabled)
15413
- return {};
15414
- return {
15415
- "tool.execute.before": async (input, output) => {
15416
- await safeAsync(PLUGIN_NAME15, "tool.execute.before", async () => {
15417
- if (input.tool !== "bash")
15418
- return;
15419
- const args = output.args ?? {};
15420
- const original = args["command"];
15421
- const next = prependUtf8Prelude(original);
15422
- if (next !== undefined && next !== original) {
15423
- args["command"] = next;
15424
- output.args = args;
15425
- safeWriteLog(PLUGIN_NAME15, {
15426
- hook: "tool.execute.before",
15427
- tool: input.tool,
15428
- callID: input.callID,
15429
- sessionID: input.sessionID,
15430
- injected: true
15431
- });
15432
- }
15433
- });
15434
- }
15435
- };
15436
- };
15437
-
15438
- // plugins/session-recovery.ts
15439
- init_opencode_plugin_helpers();
15876
+ // plugins/parallel-tool-nudge.ts
15877
+ import { readFileSync as readFileSync4, readdirSync, statSync as statSync3 } from "node:fs";
15878
+ import { join as join12 } from "node:path";
15879
+ import { homedir as homedir6 } from "node:os";
15440
15880
 
15441
- // lib/event-stream.ts
15442
- import { promises as fs9 } from "node:fs";
15881
+ // node_modules/yaml/dist/index.js
15882
+ var composer = require_composer();
15883
+ var Document = require_Document();
15884
+ var Schema = require_Schema();
15885
+ var errors = require_errors();
15886
+ var Alias = require_Alias();
15887
+ var identity = require_identity();
15888
+ var Pair = require_Pair();
15889
+ var Scalar = require_Scalar();
15890
+ var YAMLMap = require_YAMLMap();
15891
+ var YAMLSeq = require_YAMLSeq();
15892
+ var cst = require_cst();
15893
+ var lexer = require_lexer();
15894
+ var lineCounter = require_line_counter();
15895
+ var parser = require_parser();
15896
+ var publicApi = require_public_api();
15897
+ var visit = require_visit();
15898
+ var $Composer = composer.Composer;
15899
+ var $Document = Document.Document;
15900
+ var $Schema = Schema.Schema;
15901
+ var $YAMLError = errors.YAMLError;
15902
+ var $YAMLParseError = errors.YAMLParseError;
15903
+ var $YAMLWarning = errors.YAMLWarning;
15904
+ var $Alias = Alias.Alias;
15905
+ var $isAlias = identity.isAlias;
15906
+ var $isCollection = identity.isCollection;
15907
+ var $isDocument = identity.isDocument;
15908
+ var $isMap = identity.isMap;
15909
+ var $isNode = identity.isNode;
15910
+ var $isPair = identity.isPair;
15911
+ var $isScalar = identity.isScalar;
15912
+ var $isSeq = identity.isSeq;
15913
+ var $Pair = Pair.Pair;
15914
+ var $Scalar = Scalar.Scalar;
15915
+ var $YAMLMap = YAMLMap.YAMLMap;
15916
+ var $YAMLSeq = YAMLSeq.YAMLSeq;
15917
+ var $Lexer = lexer.Lexer;
15918
+ var $LineCounter = lineCounter.LineCounter;
15919
+ var $Parser = parser.Parser;
15920
+ var $parse = publicApi.parse;
15921
+ var $parseAllDocuments = publicApi.parseAllDocuments;
15922
+ var $parseDocument = publicApi.parseDocument;
15923
+ var $stringify = publicApi.stringify;
15924
+ var $visit = visit.visit;
15925
+ var $visitAsync = visit.visitAsync;
15926
+
15927
+ // plugins/parallel-tool-nudge.ts
15928
+ init_opencode_plugin_helpers();
15929
+ var PLUGIN_NAME15 = "parallel-tool-nudge";
15930
+ logLifecycle(PLUGIN_NAME15, "import", {});
15931
+ var PARALLEL_SAFE_TOOLS = [
15932
+ "read",
15933
+ "smart_search",
15934
+ "repo_map",
15935
+ "webfetch",
15936
+ "list_recent",
15937
+ "get_working_memory"
15938
+ ];
15939
+ var DEFAULT_AGENT_KEY = "__default__";
15940
+ var NUDGE_MAX_LEN = 800;
15941
+ var agentToolsMap = new Map;
15942
+ function defaultReader(p) {
15943
+ return readFileSync4(p, "utf8");
15944
+ }
15945
+ function defaultDirReader(p) {
15946
+ return readdirSync(p);
15947
+ }
15948
+ function defaultDirExists(p) {
15949
+ try {
15950
+ return statSync3(p).isDirectory();
15951
+ } catch {
15952
+ return false;
15953
+ }
15954
+ }
15955
+ function parseAgentFrontmatter(content) {
15956
+ const fmMatch = /^---\r?\n([\s\S]*?)\r?\n---/.exec(content);
15957
+ if (!fmMatch || !fmMatch[1])
15958
+ return null;
15959
+ let parsed;
15960
+ try {
15961
+ parsed = $parse(fmMatch[1]);
15962
+ } catch {
15963
+ return null;
15964
+ }
15965
+ if (!parsed || typeof parsed !== "object")
15966
+ return null;
15967
+ const obj = parsed;
15968
+ const name = typeof obj["name"] === "string" ? obj["name"] : null;
15969
+ const tools = obj["allowed_tools"];
15970
+ if (!name || !Array.isArray(tools))
15971
+ return null;
15972
+ const allowedTools = [];
15973
+ for (const t of tools) {
15974
+ if (typeof t === "string")
15975
+ allowedTools.push(t);
15976
+ }
15977
+ return { name, allowedTools };
15978
+ }
15979
+ function loadAgentToolsMap(rootDir, opts = {}) {
15980
+ const reader = opts.reader ?? defaultReader;
15981
+ const dirReader = opts.dirReader ?? defaultDirReader;
15982
+ const dirExists = opts.dirExists ?? defaultDirExists;
15983
+ const homeAgentsDir = opts.homeAgentsDir ?? join12(homedir6(), ".config", "opencode", "agents");
15984
+ const candidateDirs = [
15985
+ join12(rootDir, ".codeforge", "agents"),
15986
+ join12(rootDir, "agents"),
15987
+ homeAgentsDir
15988
+ ];
15989
+ const result = new Map;
15990
+ const safeSet = new Set(PARALLEL_SAFE_TOOLS);
15991
+ const unionTools = new Set;
15992
+ const log8 = makePluginLogger(PLUGIN_NAME15);
15993
+ for (const dir of candidateDirs) {
15994
+ if (!dirExists(dir))
15995
+ continue;
15996
+ let entries;
15997
+ try {
15998
+ entries = dirReader(dir);
15999
+ } catch (err) {
16000
+ log8.warn(`agents 目录读取失败(已跳过)`, {
16001
+ dir,
16002
+ error: err instanceof Error ? err.message : String(err)
16003
+ });
16004
+ continue;
16005
+ }
16006
+ for (const entry of entries) {
16007
+ if (!entry.endsWith(".md"))
16008
+ continue;
16009
+ const path13 = join12(dir, entry);
16010
+ let content;
16011
+ try {
16012
+ content = reader(path13);
16013
+ } catch (err) {
16014
+ log8.warn(`agent.md 读取失败(已跳过)`, {
16015
+ path: path13,
16016
+ error: err instanceof Error ? err.message : String(err)
16017
+ });
16018
+ continue;
16019
+ }
16020
+ const parsed = parseAgentFrontmatter(content);
16021
+ if (!parsed) {
16022
+ log8.warn(`agent frontmatter 解析失败(已跳过)`, { path: path13 });
16023
+ continue;
16024
+ }
16025
+ if (result.has(parsed.name))
16026
+ continue;
16027
+ const intersect = [];
16028
+ const seen = new Set;
16029
+ for (const t of parsed.allowedTools) {
16030
+ if (safeSet.has(t) && !seen.has(t)) {
16031
+ intersect.push(t);
16032
+ seen.add(t);
16033
+ unionTools.add(t);
16034
+ }
16035
+ }
16036
+ result.set(parsed.name, intersect);
16037
+ }
16038
+ }
16039
+ result.set(DEFAULT_AGENT_KEY, Array.from(unionTools));
16040
+ return result;
16041
+ }
16042
+ var sessionAgentMap = new Map;
16043
+ var SESSION_CAP = 200;
16044
+ var SESSION_TTL_MS = 24 * 60 * 60 * 1000;
16045
+ function pruneIfOversize() {
16046
+ while (sessionAgentMap.size > SESSION_CAP) {
16047
+ const oldestKey = sessionAgentMap.keys().next().value;
16048
+ if (oldestKey === undefined)
16049
+ break;
16050
+ sessionAgentMap.delete(oldestKey);
16051
+ }
16052
+ }
16053
+ function isExpired(entry, now) {
16054
+ return now - entry.ts > SESSION_TTL_MS;
16055
+ }
16056
+ function resolveAgent(sessionID, nowFn = Date.now) {
16057
+ if (!sessionID)
16058
+ return "unknown";
16059
+ const entry = sessionAgentMap.get(sessionID);
16060
+ if (!entry)
16061
+ return "unknown";
16062
+ const now = nowFn();
16063
+ if (isExpired(entry, now)) {
16064
+ sessionAgentMap.delete(sessionID);
16065
+ return "unknown";
16066
+ }
16067
+ sessionAgentMap.delete(sessionID);
16068
+ sessionAgentMap.set(sessionID, entry);
16069
+ return entry.agent;
16070
+ }
16071
+ function renderNudge(agent, tools) {
16072
+ const isUnknown = agent === "unknown";
16073
+ const agentLine = isUnknown ? `You are running as a CodeForge agent (specific agent unknown for this turn).` : `You are the \`${agent}\` agent.`;
16074
+ const toolsBulleted = tools.length > 0 ? tools.map((t) => ` - \`${t}\``).join(`
16075
+ `) : ` - (no parallel-safe tools registered for this agent)`;
16076
+ const body = `────────────────────────────────────────
16077
+ PARALLEL TOOL CALL DIRECTIVE (auto-injected by parallel-tool-nudge plugin)
16078
+ ────────────────────────────────────────
16079
+ ${agentLine} The opencode runtime executes tool calls in your single
16080
+ response **truly in parallel** (verified: up to 22 tools in 72ms).
16081
+ You MUST batch-emit independent read-only tool calls in one response
16082
+ whenever possible.
16083
+
16084
+ Parallel-safe tools available to you:
16085
+ ${toolsBulleted}
16086
+
16087
+ Heuristics:
16088
+ - Need to read 3 files? → emit 3 \`read\` calls in ONE response, not 3 sequential turns
16089
+ - Need search + map? → emit \`smart_search\` + \`repo_map\` in ONE response
16090
+ - DO NOT batch write tools (ast_edit / pending_changes.apply / bash)
16091
+ - DO NOT chain reads where each read's path depends on previous read's output
16092
+
16093
+ This directive is re-injected every turn — it is not optional advice.
16094
+ ────────────────────────────────────────`;
16095
+ if (body.length <= NUDGE_MAX_LEN)
16096
+ return body;
16097
+ return body.slice(0, NUDGE_MAX_LEN - 4) + `
16098
+ …`;
16099
+ }
16100
+ var log8 = makePluginLogger(PLUGIN_NAME15);
16101
+ var parallelToolNudgeServer = async (ctx) => {
16102
+ try {
16103
+ const loaded = loadAgentToolsMap(ctx.directory ?? process.cwd());
16104
+ agentToolsMap.clear();
16105
+ for (const [k, v] of loaded.entries())
16106
+ agentToolsMap.set(k, v);
16107
+ } catch (err) {
16108
+ log8.warn(`loadAgentToolsMap 失败(plugin 将 no-op)`, {
16109
+ error: err instanceof Error ? err.message : String(err)
16110
+ });
16111
+ }
16112
+ const realAgentCount = Math.max(0, agentToolsMap.size - (agentToolsMap.has(DEFAULT_AGENT_KEY) ? 1 : 0));
16113
+ if (realAgentCount === 0) {
16114
+ agentToolsMap.clear();
16115
+ log8.warn(`0 real agents loaded; plugin will be no-op for this session`, {
16116
+ directory: ctx.directory
16117
+ });
16118
+ }
16119
+ const defaultTools = agentToolsMap.get(DEFAULT_AGENT_KEY) ?? [];
16120
+ logLifecycle(PLUGIN_NAME15, "activate", {
16121
+ directory: ctx.directory,
16122
+ real_agents_loaded: realAgentCount,
16123
+ default_tools_union: defaultTools,
16124
+ parallel_safe_whitelist: PARALLEL_SAFE_TOOLS,
16125
+ session_cap: SESSION_CAP,
16126
+ session_ttl_ms: SESSION_TTL_MS,
16127
+ no_op: agentToolsMap.size === 0
16128
+ });
16129
+ return {
16130
+ "chat.params": async (input, _output) => {
16131
+ await safeAsync(PLUGIN_NAME15, "chat.params", async () => {
16132
+ if (agentToolsMap.size === 0)
16133
+ return;
16134
+ const sid = input?.sessionID;
16135
+ const agent = input?.agent;
16136
+ if (!sid || !agent)
16137
+ return;
16138
+ sessionAgentMap.set(sid, { agent, ts: Date.now() });
16139
+ pruneIfOversize();
16140
+ });
16141
+ },
16142
+ "experimental.chat.system.transform": async (input, output) => {
16143
+ await safeAsync(PLUGIN_NAME15, "experimental.chat.system.transform", async () => {
16144
+ if (agentToolsMap.size === 0)
16145
+ return;
16146
+ if (!output || !Array.isArray(output.system))
16147
+ return;
16148
+ const sid = input?.sessionID;
16149
+ const agent = resolveAgent(sid);
16150
+ const tools = agent !== "unknown" ? agentToolsMap.get(agent) ?? agentToolsMap.get(DEFAULT_AGENT_KEY) ?? [] : agentToolsMap.get(DEFAULT_AGENT_KEY) ?? [];
16151
+ const nudge = renderNudge(agent, tools);
16152
+ output.system.push(nudge);
16153
+ safeWriteLog(PLUGIN_NAME15, {
16154
+ hook: "experimental.chat.system.transform",
16155
+ sessionID: sid,
16156
+ agent,
16157
+ injected_tools: tools,
16158
+ injected_chars: nudge.length,
16159
+ fallback: agent === "unknown"
16160
+ });
16161
+ });
16162
+ }
16163
+ };
16164
+ };
16165
+ var handler15 = parallelToolNudgeServer;
16166
+
16167
+ // plugins/pwsh-utf8.ts
16168
+ init_opencode_plugin_helpers();
16169
+ var PLUGIN_NAME16 = "pwsh-utf8";
16170
+ logLifecycle(PLUGIN_NAME16, "import", {});
16171
+ var PRELUDE = "chcp 65001 *> $null; " + "[Console]::OutputEncoding = [System.Text.UTF8Encoding]::new(); " + "$OutputEncoding = [System.Text.UTF8Encoding]::new(); ";
16172
+ function prependUtf8Prelude(command) {
16173
+ if (typeof command !== "string")
16174
+ return;
16175
+ if (command.length === 0)
16176
+ return command;
16177
+ if (/chcp\s+65001/i.test(command))
16178
+ return command;
16179
+ if (/\[Console\]::OutputEncoding\s*=/i.test(command))
16180
+ return command;
16181
+ return PRELUDE + command;
16182
+ }
16183
+ var handler16 = async (_ctx) => {
16184
+ const enabled = process.platform === "win32" && process.env.CODEFORGE_DISABLE_PWSH_UTF8 !== "1";
16185
+ const reason = enabled ? "win32" : process.platform !== "win32" ? "non-win32" : "disabled-by-env";
16186
+ logLifecycle(PLUGIN_NAME16, "activate", { enabled, platform: process.platform, reason });
16187
+ if (!enabled)
16188
+ return {};
16189
+ return {
16190
+ "tool.execute.before": async (input, output) => {
16191
+ await safeAsync(PLUGIN_NAME16, "tool.execute.before", async () => {
16192
+ if (input.tool !== "bash")
16193
+ return;
16194
+ const args = output.args ?? {};
16195
+ const original = args["command"];
16196
+ const next = prependUtf8Prelude(original);
16197
+ if (next !== undefined && next !== original) {
16198
+ args["command"] = next;
16199
+ output.args = args;
16200
+ safeWriteLog(PLUGIN_NAME16, {
16201
+ hook: "tool.execute.before",
16202
+ tool: input.tool,
16203
+ callID: input.callID,
16204
+ sessionID: input.sessionID,
16205
+ injected: true
16206
+ });
16207
+ }
16208
+ });
16209
+ }
16210
+ };
16211
+ };
16212
+
16213
+ // plugins/session-recovery.ts
16214
+ init_opencode_plugin_helpers();
16215
+
16216
+ // lib/event-stream.ts
16217
+ import { promises as fs10 } from "node:fs";
15443
16218
  init_runtime_paths();
15444
- import * as path12 from "node:path";
16219
+ import * as path13 from "node:path";
15445
16220
  async function loadSession(id, opts = {}) {
15446
16221
  const file = resolveSessionFile(id, opts);
15447
- const raw = await fs9.readFile(file, "utf8");
16222
+ const raw = await fs10.readFile(file, "utf8");
15448
16223
  return parseJsonl(id, raw);
15449
16224
  }
15450
16225
  async function listSessions(opts = {}) {
15451
16226
  const dir = resolveDir(opts);
15452
16227
  let entries;
15453
16228
  try {
15454
- entries = await fs9.readdir(dir, { withFileTypes: true });
16229
+ entries = await fs10.readdir(dir, { withFileTypes: true });
15455
16230
  } catch (err) {
15456
16231
  if (err.code === "ENOENT")
15457
16232
  return [];
@@ -15461,10 +16236,10 @@ async function listSessions(opts = {}) {
15461
16236
  for (const e of entries) {
15462
16237
  if (!e.isFile() || !e.name.endsWith(".jsonl"))
15463
16238
  continue;
15464
- const file = path12.join(dir, e.name);
16239
+ const file = path13.join(dir, e.name);
15465
16240
  const id = e.name.replace(/\.jsonl$/, "");
15466
16241
  try {
15467
- const stat = await fs9.stat(file);
16242
+ const stat = await fs10.stat(file);
15468
16243
  const headerLine = await readFirstLine(file);
15469
16244
  let started_at = stat.birthtimeMs;
15470
16245
  if (headerLine) {
@@ -15488,11 +16263,11 @@ async function listSessions(opts = {}) {
15488
16263
  return out;
15489
16264
  }
15490
16265
  function resolveDir(opts = {}) {
15491
- const root = path12.resolve(opts.root ?? process.cwd());
15492
- return opts.sessions_dir ? path12.resolve(root, opts.sessions_dir) : path12.join(runtimeDir(root), "sessions");
16266
+ const root = path13.resolve(opts.root ?? process.cwd());
16267
+ return opts.sessions_dir ? path13.resolve(root, opts.sessions_dir) : path13.join(runtimeDir(root), "sessions");
15493
16268
  }
15494
16269
  function resolveSessionFile(id, opts = {}) {
15495
- return path12.join(resolveDir(opts), `${id}.jsonl`);
16270
+ return path13.join(resolveDir(opts), `${id}.jsonl`);
15496
16271
  }
15497
16272
  function parseJsonl(id, raw) {
15498
16273
  const events = [];
@@ -15527,7 +16302,7 @@ function isEvent(obj) {
15527
16302
  }
15528
16303
  async function readFirstLine(file) {
15529
16304
  const buf = Buffer.alloc(4096);
15530
- const fh = await fs9.open(file, "r");
16305
+ const fh = await fs10.open(file, "r");
15531
16306
  try {
15532
16307
  const { bytesRead } = await fh.read(buf, 0, buf.length, 0);
15533
16308
  const s = buf.subarray(0, bytesRead).toString("utf8");
@@ -15755,8 +16530,8 @@ function isRecoveryWorthShowing(plan) {
15755
16530
  }
15756
16531
 
15757
16532
  // plugins/session-recovery.ts
15758
- var PLUGIN_NAME16 = "session-recovery";
15759
- logLifecycle(PLUGIN_NAME16, "import", {});
16533
+ var PLUGIN_NAME17 = "session-recovery";
16534
+ logLifecycle(PLUGIN_NAME17, "import", {});
15760
16535
  async function processSessionStart(currentSessionId, opts = {}) {
15761
16536
  if (opts.disabled) {
15762
16537
  return { ok: true, injected: false, reason: "disabled" };
@@ -15766,97 +16541,661 @@ async function processSessionStart(currentSessionId, opts = {}) {
15766
16541
  excludeIds.add(currentSessionId);
15767
16542
  const r = await scanLastSession({ ...opts, excludeIds: [...excludeIds] });
15768
16543
  if (!r.ok) {
15769
- opts.log?.warn?.(`[${PLUGIN_NAME16}] 扫描失败:${r.error}`);
16544
+ opts.log?.warn?.(`[${PLUGIN_NAME17}] 扫描失败:${r.error}`);
15770
16545
  return { ok: false, injected: false, reason: "scan_error", error: r.error };
15771
16546
  }
15772
- const plan = r.plan;
15773
- if (!isRecoveryWorthShowing(plan)) {
15774
- return { ok: true, injected: false, plan, reason: "no_signal" };
16547
+ const plan = r.plan;
16548
+ if (!isRecoveryWorthShowing(plan)) {
16549
+ return { ok: true, injected: false, plan, reason: "no_signal" };
16550
+ }
16551
+ const prompt = renderPrompt(plan);
16552
+ const injection = { source: "session-recovery", plan, prompt };
16553
+ if (opts.injectRecovery) {
16554
+ try {
16555
+ await opts.injectRecovery(injection);
16556
+ } catch (err) {
16557
+ const msg = err instanceof Error ? err.message : String(err);
16558
+ opts.log?.warn?.(`[${PLUGIN_NAME17}] injectRecovery 异常:${msg}`);
16559
+ return { ok: false, injected: false, plan, reason: "inject_error", error: msg };
16560
+ }
16561
+ }
16562
+ return { ok: true, injected: true, plan, reason: "ok" };
16563
+ }
16564
+ function renderPrompt(plan) {
16565
+ const lines = [];
16566
+ lines.push("【会话恢复提示】");
16567
+ lines.push(plan.summary);
16568
+ lines.push("");
16569
+ lines.push("如果用户接下来未明确提及,请优先:");
16570
+ if (plan.pending_changes_likely) {
16571
+ lines.push(" • 询问是否要 review 上次的暂存区改动并 apply / 丢弃");
16572
+ }
16573
+ if (plan.open_subtasks_likely) {
16574
+ lines.push(" • 询问是否要继续上次未完成的子任务");
16575
+ }
16576
+ if (plan.last_user_intent) {
16577
+ lines.push(` • 确认是否要继续推进上次的目标:"${plan.last_user_intent}"`);
16578
+ }
16579
+ if (!plan.pending_changes_likely && !plan.open_subtasks_likely && !plan.last_user_intent) {
16580
+ lines.push(" • 询问是否要恢复上次的工作(信号较弱,可能不需要)");
16581
+ }
16582
+ lines.push("");
16583
+ lines.push("(如果用户明确开了新话题,本提示可忽略)");
16584
+ return lines.join(`
16585
+ `);
16586
+ }
16587
+ var log9 = makePluginLogger(PLUGIN_NAME17);
16588
+ var _lastInjection = null;
16589
+ var sessionRecoveryServer = async (ctx) => {
16590
+ logLifecycle(PLUGIN_NAME17, "activate", { directory: ctx.directory });
16591
+ return {
16592
+ event: async ({ event }) => {
16593
+ await safeAsync(PLUGIN_NAME17, "event", async () => {
16594
+ const e = event;
16595
+ if (!e || typeof e.type !== "string")
16596
+ return;
16597
+ if (e.type !== "session.start")
16598
+ return;
16599
+ const sid = typeof e.properties?.["session_id"] === "string" ? e.properties["session_id"] : undefined;
16600
+ const root = typeof e.properties?.["root"] === "string" ? e.properties["root"] : ctx.directory ?? process.cwd();
16601
+ const r = await processSessionStart(sid, {
16602
+ root,
16603
+ log: log9,
16604
+ injectRecovery: (inj) => {
16605
+ _lastInjection = inj;
16606
+ }
16607
+ });
16608
+ safeWriteLog(PLUGIN_NAME17, {
16609
+ hook: "event",
16610
+ type: "session.start",
16611
+ ok: r.ok,
16612
+ injected: r.injected,
16613
+ reason: r.reason,
16614
+ last_session_id: r.plan?.last_session_id
16615
+ });
16616
+ if (r.injected && r.plan) {
16617
+ log9.info(`[${PLUGIN_NAME17}] 注入恢复提示(last=${r.plan.last_session_id?.slice(0, 8) ?? "?"}, reason=${r.plan.reason})`);
16618
+ }
16619
+ });
16620
+ }
16621
+ };
16622
+ };
16623
+ var handler17 = sessionRecoveryServer;
16624
+
16625
+ // plugins/subtasks.ts
16626
+ import { promises as fs12 } from "node:fs";
16627
+ import * as path15 from "node:path";
16628
+
16629
+ // lib/autonomy.ts
16630
+ import { execFile as execFile2 } from "node:child_process";
16631
+ import { promises as fs11 } from "node:fs";
16632
+ import * as path14 from "node:path";
16633
+ var DEFAULT_POLICIES = {
16634
+ step: {
16635
+ bash: "confirm",
16636
+ edit: "confirm",
16637
+ webfetch: "confirm",
16638
+ read: "confirm",
16639
+ search: "confirm",
16640
+ other: "confirm"
16641
+ },
16642
+ semi: {
16643
+ bash: "confirm",
16644
+ edit: "confirm",
16645
+ webfetch: "confirm",
16646
+ read: "auto",
16647
+ search: "auto",
16648
+ other: "auto"
16649
+ },
16650
+ full: {
16651
+ bash: "auto",
16652
+ edit: "auto",
16653
+ webfetch: "auto",
16654
+ read: "auto",
16655
+ search: "auto",
16656
+ other: "auto"
16657
+ }
16658
+ };
16659
+ var RISK_PATTERNS = [
16660
+ {
16661
+ tag: "rm_rf_root",
16662
+ kinds: ["bash"],
16663
+ re: /rm\s+-[rRf]+[^|;`\n]*\s+(\/(?:\s|$|[^/\w*.-])|~|\$HOME(?:\b|\/)|\/(?:bin|etc|usr|var|opt|home|root|boot|lib|sbin)\b)/
16664
+ },
16665
+ {
16666
+ tag: "rm_rf_wildcard",
16667
+ kinds: ["bash"],
16668
+ re: /rm\s+-[rRf]+\s+(?:\.\*|\*)/
16669
+ },
16670
+ {
16671
+ tag: "force_push_protected",
16672
+ kinds: ["bash"],
16673
+ re: /git\s+push\s+(?:--force\b|-f\b)[\s\S]*\b(main|master|release(?:\/[\w.-]+)?)\b/
16674
+ },
16675
+ {
16676
+ tag: "git_reset_hard_main",
16677
+ kinds: ["bash"],
16678
+ re: /git\s+reset\s+--hard\s+(origin\/)?(main|master)/
16679
+ },
16680
+ { tag: "sudo", kinds: ["bash"], re: /(?<![\w-])sudo(?![\w-])/ },
16681
+ { tag: "su_root", kinds: ["bash"], re: /(?<![\w-])su\s+(?:-\s+)?root\b/ },
16682
+ { tag: "mkfs", kinds: ["bash"], re: /\bmkfs(\.[\w]+)?\b/ },
16683
+ { tag: "dd_disk", kinds: ["bash"], re: /\bdd\s+if=[^|;\s]+\s+of=\/dev\// },
16684
+ { tag: "chmod_777", kinds: ["bash"], re: /chmod\s+[-+]?[0-7]?777/ },
16685
+ {
16686
+ tag: "curl_pipe_sh",
16687
+ kinds: ["bash"],
16688
+ re: /\b(?:curl|wget)\b[^|]*\|\s*(?:sh|bash|zsh)\b/
16689
+ },
16690
+ {
16691
+ tag: "drop_database",
16692
+ kinds: ["bash", "other"],
16693
+ re: /\b(DROP\s+(DATABASE|TABLE)|TRUNCATE\s+TABLE|DROP\s+SCHEMA)\b/i
16694
+ },
16695
+ { tag: "write_secrets", re: /(\.env(?:\.\w+)?|id_[edr]sa|\.ssh\/id_|\.pem|\.p12|secret\.json)/i },
16696
+ { tag: "write_etc", re: /(?:^|\s|"|')\/etc\// },
16697
+ { tag: "write_usr", re: /(?:^|\s|"|')\/usr\// },
16698
+ { tag: "write_root_home", re: /(?:^|\s|"|')(\/root|\/home\/root)\// },
16699
+ {
16700
+ tag: "internal_url",
16701
+ kinds: ["webfetch"],
16702
+ re: /https?:\/\/(localhost|127\.0\.0\.1|0\.0\.0\.0|169\.254\.|10\.|192\.168\.|172\.(?:1[6-9]|2\d|3[01])\.)/i
16703
+ },
16704
+ {
16705
+ tag: "kubectl_delete",
16706
+ kinds: ["bash"],
16707
+ re: /kubectl\s+(?:delete|destroy)\s+/
16708
+ },
16709
+ {
16710
+ tag: "terraform_destroy",
16711
+ kinds: ["bash"],
16712
+ re: /terraform\s+destroy/
16713
+ }
16714
+ ];
16715
+ function evaluate2(mode, intent, opts = {}) {
16716
+ const ignore = new Set(opts.ignore_risks ?? []);
16717
+ const allRisks = [...RISK_PATTERNS, ...opts.extra_risks ?? []];
16718
+ const haystack = buildHaystack(intent.args);
16719
+ const hits = allRisks.filter((p) => !ignore.has(p.tag)).filter((p) => !p.kinds || p.kinds.includes(intent.kind)).filter((p) => p.re.test(haystack)).map((p) => p.tag);
16720
+ let effective = mode;
16721
+ let downgraded = false;
16722
+ let reason;
16723
+ if (hits.length > 0 && mode === "full") {
16724
+ effective = "semi";
16725
+ downgraded = true;
16726
+ reason = `检测到风险操作 [${hits.join(", ")}],自动从 full 降级到 semi`;
16727
+ } else if (hits.length > 0 && mode !== "step") {}
16728
+ const policy = mergePolicy(effective, opts.policies);
16729
+ const baseAction = policy[intent.kind] ?? "confirm";
16730
+ const action = hits.length > 0 && effective !== "step" ? "confirm" : baseAction;
16731
+ return {
16732
+ effective_mode: effective,
16733
+ action,
16734
+ downgraded,
16735
+ reason,
16736
+ detected_risks: hits
16737
+ };
16738
+ }
16739
+ function mergePolicy(mode, override) {
16740
+ const base = DEFAULT_POLICIES[mode];
16741
+ if (!override?.[mode])
16742
+ return base;
16743
+ return { ...base, ...override[mode] };
16744
+ }
16745
+ function buildHaystack(args) {
16746
+ if (typeof args === "string")
16747
+ return args;
16748
+ try {
16749
+ return JSON.stringify(args);
16750
+ } catch {
16751
+ return String(args);
16752
+ }
16753
+ }
16754
+ async function ensureWorktree(opts) {
16755
+ const root = path14.resolve(opts.root);
16756
+ const dir = opts.worktrees_dir ?? path14.join(root, ".git", "codeforge-worktrees");
16757
+ await fs11.mkdir(dir, { recursive: true });
16758
+ const wtPath = path14.join(dir, sanitizeBranch(opts.branch));
16759
+ try {
16760
+ const stat = await fs11.stat(wtPath);
16761
+ if (stat.isDirectory()) {
16762
+ return { path: wtPath, branch: opts.branch, created: false };
16763
+ }
16764
+ } catch {}
16765
+ await runGit(root, ["rev-parse", "--is-inside-work-tree"], opts.git_timeout_ms ?? 3000);
16766
+ let branchExists = false;
16767
+ try {
16768
+ await runGit(root, ["rev-parse", "--verify", `refs/heads/${opts.branch}`], opts.git_timeout_ms ?? 3000);
16769
+ branchExists = true;
16770
+ } catch {
16771
+ branchExists = false;
16772
+ }
16773
+ const args = branchExists ? ["worktree", "add", wtPath, opts.branch] : ["worktree", "add", "-b", opts.branch, wtPath, opts.base ?? "HEAD"];
16774
+ await runGit(root, args, opts.git_timeout_ms ?? 1e4);
16775
+ return { path: wtPath, branch: opts.branch, created: true };
16776
+ }
16777
+ async function removeWorktree(opts) {
16778
+ const args = ["worktree", "remove", opts.worktree_path];
16779
+ if (opts.force)
16780
+ args.push("--force");
16781
+ await runGit(opts.root, args, opts.git_timeout_ms ?? 5000);
16782
+ }
16783
+ function sanitizeBranch(name) {
16784
+ return name.replace(/[/\\:*?"<>|]/g, "-");
16785
+ }
16786
+ function runGitAllowFail(cwd, args, timeout) {
16787
+ return new Promise((resolve13, reject) => {
16788
+ execFile2("git", args, { cwd, timeout, windowsHide: true, encoding: "utf8" }, (err, stdout, stderr) => {
16789
+ if (err) {
16790
+ const errCode = err.code;
16791
+ if (typeof errCode === "string") {
16792
+ reject(new Error(`git ${args.join(" ")} 失败: ${stderr || err.message}`));
16793
+ return;
16794
+ }
16795
+ resolve13({
16796
+ code: typeof errCode === "number" ? errCode : 1,
16797
+ stdout: stdout ?? "",
16798
+ stderr: stderr ?? err.message ?? ""
16799
+ });
16800
+ return;
16801
+ }
16802
+ resolve13({ code: 0, stdout, stderr: stderr ?? "" });
16803
+ });
16804
+ });
16805
+ }
16806
+ function runGit(cwd, args, timeout) {
16807
+ return new Promise((resolve13, reject) => {
16808
+ execFile2("git", args, { cwd, timeout, windowsHide: true, encoding: "utf8" }, (err, stdout, stderr) => {
16809
+ if (err) {
16810
+ reject(new Error(`git ${args.join(" ")} 失败: ${stderr || err.message}`));
16811
+ return;
16812
+ }
16813
+ resolve13(stdout);
16814
+ });
16815
+ });
16816
+ }
16817
+ async function tryMerge(opts) {
16818
+ const root = path14.resolve(opts.root);
16819
+ const timeout = opts.git_timeout_ms ?? 1e4;
16820
+ const status = await runGitAllowFail(root, ["status", "--porcelain"], 3000);
16821
+ if (status.code !== 0) {
16822
+ return {
16823
+ ok: false,
16824
+ conflicts: [],
16825
+ hadChanges: false,
16826
+ message: `git status 失败: ${status.stderr.trim() || "unknown"}`
16827
+ };
16828
+ }
16829
+ if (status.stdout.trim().length > 0) {
16830
+ return {
16831
+ ok: false,
16832
+ conflicts: [],
16833
+ hadChanges: false,
16834
+ message: "main repo dirty"
16835
+ };
16836
+ }
16837
+ const diff = await runGitAllowFail(root, ["diff", "--name-only", "HEAD", opts.branch], 3000);
16838
+ if (diff.code !== 0) {
16839
+ return {
16840
+ ok: false,
16841
+ conflicts: [],
16842
+ hadChanges: false,
16843
+ message: `git diff HEAD ${opts.branch} 失败: ${diff.stderr.trim() || "unknown"}`
16844
+ };
16845
+ }
16846
+ const hadChanges = diff.stdout.trim().length > 0;
16847
+ if (!hadChanges) {
16848
+ return { ok: true, conflicts: [], hadChanges: false };
15775
16849
  }
15776
- const prompt = renderPrompt(plan);
15777
- const injection = { source: "session-recovery", plan, prompt };
15778
- if (opts.injectRecovery) {
15779
- try {
15780
- await opts.injectRecovery(injection);
15781
- } catch (err) {
15782
- const msg = err instanceof Error ? err.message : String(err);
15783
- opts.log?.warn?.(`[${PLUGIN_NAME16}] injectRecovery 异常:${msg}`);
15784
- return { ok: false, injected: false, plan, reason: "inject_error", error: msg };
15785
- }
16850
+ const merge = await runGitAllowFail(root, ["merge", "--no-commit", "--no-ff", opts.branch], timeout);
16851
+ if (merge.code === 0) {
16852
+ return { ok: true, conflicts: [], hadChanges: true };
15786
16853
  }
15787
- return { ok: true, injected: true, plan, reason: "ok" };
16854
+ const conflicts = await getMergeConflicts({ root, git_timeout_ms: 3000 });
16855
+ await runGitAllowFail(root, ["merge", "--abort"], 5000);
16856
+ return {
16857
+ ok: false,
16858
+ conflicts,
16859
+ hadChanges: true,
16860
+ message: merge.stderr.trim() || `merge exit code ${merge.code}`
16861
+ };
15788
16862
  }
15789
- function renderPrompt(plan) {
15790
- const lines = [];
15791
- lines.push("【会话恢复提示】");
15792
- lines.push(plan.summary);
15793
- lines.push("");
15794
- lines.push("如果用户接下来未明确提及,请优先:");
15795
- if (plan.pending_changes_likely) {
15796
- lines.push(" • 询问是否要 review 上次的暂存区改动并 apply / 丢弃");
16863
+ async function mergeCommit(opts) {
16864
+ const root = path14.resolve(opts.root);
16865
+ await runGit(root, ["commit", "-m", opts.message], opts.git_timeout_ms ?? 1e4);
16866
+ }
16867
+ async function mergeAbort(opts) {
16868
+ const root = path14.resolve(opts.root);
16869
+ await runGitAllowFail(root, ["merge", "--abort"], opts.git_timeout_ms ?? 5000);
16870
+ }
16871
+ async function getMergeConflicts(opts) {
16872
+ const out = await runGitAllowFail(path14.resolve(opts.root), ["diff", "--name-only", "--diff-filter=U"], opts.git_timeout_ms ?? 3000);
16873
+ if (out.code !== 0)
16874
+ return [];
16875
+ return out.stdout.split(/\r?\n/).map((s) => s.trim()).filter(Boolean);
16876
+ }
16877
+ async function worktreeHasChanges(opts) {
16878
+ const out = await runGitAllowFail(path14.resolve(opts.worktree_path), ["status", "--porcelain"], opts.git_timeout_ms ?? 3000);
16879
+ if (out.code !== 0)
16880
+ return false;
16881
+ return out.stdout.trim().length > 0;
16882
+ }
16883
+ async function commitWorktreeIfDirty(opts) {
16884
+ const wt = path14.resolve(opts.worktree_path);
16885
+ const timeout = opts.git_timeout_ms ?? 1e4;
16886
+ const status = await runGitAllowFail(wt, ["status", "--porcelain"], 3000);
16887
+ if (status.code !== 0) {
16888
+ return { committed: false };
15797
16889
  }
15798
- if (plan.open_subtasks_likely) {
15799
- lines.push(" • 询问是否要继续上次未完成的子任务");
16890
+ if (status.stdout.trim().length === 0) {
16891
+ return { committed: false };
15800
16892
  }
15801
- if (plan.last_user_intent) {
15802
- lines.push(` 确认是否要继续推进上次的目标:"${plan.last_user_intent}"`);
16893
+ await runGit(wt, ["add", "-A"], timeout);
16894
+ await runGit(wt, ["commit", "-m", opts.message], timeout);
16895
+ return { committed: true };
16896
+ }
16897
+
16898
+ // lib/parallel-merge.ts
16899
+ function resolveMergeFns(deps) {
16900
+ return {
16901
+ commitWorktreeIfDirtyFn: deps.commitWorktreeIfDirty ?? commitWorktreeIfDirty,
16902
+ tryMergeFn: deps.tryMerge ?? tryMerge,
16903
+ mergeCommitFn: deps.mergeCommit ?? mergeCommit,
16904
+ mergeAbortFn: deps.mergeAbort ?? mergeAbort,
16905
+ removeWorktreeFn: deps.removeWorktree ?? removeWorktree,
16906
+ worktreeHasChangesFn: deps.worktreeHasChanges ?? worktreeHasChanges
16907
+ };
16908
+ }
16909
+ async function mergeOneAttempt(r, opts, mergeFns, log10) {
16910
+ const t0 = Date.now();
16911
+ const root = opts.mergeRoot;
16912
+ const {
16913
+ commitWorktreeIfDirtyFn,
16914
+ tryMergeFn,
16915
+ mergeCommitFn,
16916
+ mergeAbortFn,
16917
+ removeWorktreeFn
16918
+ } = mergeFns;
16919
+ const eligibleStatus = r.status === "success" || r.status === "need_review";
16920
+ if (!eligibleStatus) {
16921
+ return {
16922
+ attempt: {
16923
+ subtaskId: r.id,
16924
+ branch: r.id,
16925
+ worktree: r.worktree ?? null,
16926
+ ok: false,
16927
+ conflicts: [],
16928
+ skippedReason: "subtask_failed",
16929
+ durationMs: 0
16930
+ }
16931
+ };
15803
16932
  }
15804
- if (!plan.pending_changes_likely && !plan.open_subtasks_likely && !plan.last_user_intent) {
15805
- lines.push(" • 询问是否要恢复上次的工作(信号较弱,可能不需要)");
16933
+ if (!r.worktree) {
16934
+ return {
16935
+ attempt: {
16936
+ subtaskId: r.id,
16937
+ branch: r.id,
16938
+ worktree: null,
16939
+ ok: false,
16940
+ conflicts: [],
16941
+ skippedReason: "no_worktree",
16942
+ durationMs: 0
16943
+ }
16944
+ };
16945
+ }
16946
+ const wt = r.worktree;
16947
+ const branch = r.id;
16948
+ const attempt = {
16949
+ subtaskId: r.id,
16950
+ branch,
16951
+ worktree: wt,
16952
+ ok: false,
16953
+ conflicts: [],
16954
+ durationMs: 0
16955
+ };
16956
+ try {
16957
+ try {
16958
+ await commitWorktreeIfDirtyFn({
16959
+ worktree_path: wt,
16960
+ message: `parallel: auto-commit ${r.id} before merge`
16961
+ });
16962
+ } catch (commitErr) {
16963
+ attempt.ok = false;
16964
+ attempt.message = `commitWorktreeIfDirty 失败:${describe4(commitErr)}`;
16965
+ attempt.durationMs = Date.now() - t0;
16966
+ return {
16967
+ attempt,
16968
+ pendingItem: { subtaskId: r.id, path: wt, conflicts: [] }
16969
+ };
16970
+ }
16971
+ const merge = await tryMergeFn({ root, branch });
16972
+ if (!merge.ok) {
16973
+ attempt.ok = false;
16974
+ attempt.conflicts = merge.conflicts;
16975
+ attempt.message = merge.message;
16976
+ attempt.durationMs = Date.now() - t0;
16977
+ return {
16978
+ attempt,
16979
+ pendingItem: { subtaskId: r.id, path: wt, conflicts: merge.conflicts }
16980
+ };
16981
+ }
16982
+ if (merge.hadChanges) {
16983
+ try {
16984
+ await mergeCommitFn({ root, message: `parallel: merge ${r.id}` });
16985
+ } catch (commitErr) {
16986
+ let abortFailed = false;
16987
+ try {
16988
+ await mergeAbortFn({ root });
16989
+ } catch (abortErr) {
16990
+ abortFailed = true;
16991
+ log10("error", `[parallel-merge] mergeAbort 失败(仓库可能锁死)`, {
16992
+ id: r.id,
16993
+ error: describe4(abortErr)
16994
+ });
16995
+ }
16996
+ attempt.ok = false;
16997
+ attempt.message = `mergeCommit 失败:${describe4(commitErr)}`;
16998
+ attempt.durationMs = Date.now() - t0;
16999
+ return {
17000
+ attempt,
17001
+ abortFailed,
17002
+ pendingItem: { subtaskId: r.id, path: wt, conflicts: [] }
17003
+ };
17004
+ }
17005
+ attempt.ok = true;
17006
+ attempt.durationMs = Date.now() - t0;
17007
+ await safeRemoveWorktree(removeWorktreeFn, root, wt, log10, r.id);
17008
+ return { attempt };
17009
+ } else {
17010
+ attempt.ok = true;
17011
+ attempt.skippedReason = "no_changes";
17012
+ attempt.durationMs = Date.now() - t0;
17013
+ await safeRemoveWorktree(removeWorktreeFn, root, wt, log10, r.id);
17014
+ return { attempt };
17015
+ }
17016
+ } catch (err) {
17017
+ attempt.ok = false;
17018
+ attempt.message = `mergeWorktrees 异常:${describe4(err)}`;
17019
+ attempt.durationMs = Date.now() - t0;
17020
+ return {
17021
+ attempt,
17022
+ pendingItem: { subtaskId: r.id, path: wt, conflicts: [] }
17023
+ };
15806
17024
  }
15807
- lines.push("");
15808
- lines.push("(如果用户明确开了新话题,本提示可忽略)");
15809
- return lines.join(`
15810
- `);
15811
17025
  }
15812
- var log8 = makePluginLogger(PLUGIN_NAME16);
15813
- var _lastInjection = null;
15814
- var sessionRecoveryServer = async (ctx) => {
15815
- logLifecycle(PLUGIN_NAME16, "activate", { directory: ctx.directory });
17026
+ async function mergeWorktrees(results, opts, deps) {
17027
+ const log10 = deps.log ?? (() => {});
17028
+ const root = opts.mergeRoot;
17029
+ const mergeFns = resolveMergeFns(deps);
17030
+ const report = {
17031
+ attempts: [],
17032
+ merged: 0,
17033
+ skipped: 0,
17034
+ conflicted: 0,
17035
+ pendingWorktrees: []
17036
+ };
17037
+ const mergeable = [];
17038
+ const skipped = [];
17039
+ for (const r of results) {
17040
+ const eligibleStatus = r.status === "success" || r.status === "need_review";
17041
+ if (!eligibleStatus) {
17042
+ skipped.push({ res: r, reason: "subtask_failed" });
17043
+ continue;
17044
+ }
17045
+ if (!r.worktree) {
17046
+ skipped.push({ res: r, reason: "no_worktree" });
17047
+ continue;
17048
+ }
17049
+ mergeable.push(r);
17050
+ }
17051
+ mergeable.sort((a, b) => {
17052
+ const la = a.changedFiles?.length ?? 0;
17053
+ const lb = b.changedFiles?.length ?? 0;
17054
+ return la - lb;
17055
+ });
17056
+ let idx = 0;
17057
+ for (const s of skipped) {
17058
+ const attempt = {
17059
+ subtaskId: s.res.id,
17060
+ branch: s.res.id,
17061
+ worktree: s.res.worktree ?? null,
17062
+ ok: false,
17063
+ conflicts: [],
17064
+ skippedReason: s.reason,
17065
+ durationMs: 0
17066
+ };
17067
+ report.attempts.push(attempt);
17068
+ report.skipped++;
17069
+ await fireMergeAttempt(opts.onMergeAttempt, attempt, idx++, log10);
17070
+ }
17071
+ for (const r of mergeable) {
17072
+ const { attempt, pendingItem } = await mergeOneAttempt(r, { mergeRoot: root }, mergeFns, log10);
17073
+ report.attempts.push(attempt);
17074
+ if (attempt.ok && !attempt.skippedReason)
17075
+ report.merged++;
17076
+ else if (attempt.skippedReason === "no_changes")
17077
+ report.skipped++;
17078
+ else
17079
+ report.conflicted++;
17080
+ if (pendingItem)
17081
+ report.pendingWorktrees.push(pendingItem);
17082
+ await fireMergeAttempt(opts.onMergeAttempt, attempt, idx++, log10);
17083
+ }
17084
+ return report;
17085
+ }
17086
+ function createMergeQueue(opts, deps = {}) {
17087
+ const log10 = deps.log ?? (() => {});
17088
+ const mergeFns = {
17089
+ commitWorktreeIfDirtyFn: deps.commitWorktreeIfDirtyFn ?? commitWorktreeIfDirty,
17090
+ tryMergeFn: deps.tryMergeFn ?? tryMerge,
17091
+ mergeCommitFn: deps.mergeCommitFn ?? mergeCommit,
17092
+ mergeAbortFn: deps.mergeAbortFn ?? mergeAbort,
17093
+ removeWorktreeFn: deps.removeWorktreeFn ?? removeWorktree,
17094
+ worktreeHasChangesFn: deps.worktreeHasChangesFn ?? worktreeHasChanges
17095
+ };
17096
+ const report = {
17097
+ attempts: [],
17098
+ merged: 0,
17099
+ skipped: 0,
17100
+ conflicted: 0,
17101
+ pendingWorktrees: []
17102
+ };
17103
+ let tail = Promise.resolve();
17104
+ let idx = 0;
17105
+ let queueAborted = false;
15816
17106
  return {
15817
- event: async ({ event }) => {
15818
- await safeAsync(PLUGIN_NAME16, "event", async () => {
15819
- const e = event;
15820
- if (!e || typeof e.type !== "string")
15821
- return;
15822
- if (e.type !== "session.start")
15823
- return;
15824
- const sid = typeof e.properties?.["session_id"] === "string" ? e.properties["session_id"] : undefined;
15825
- const root = typeof e.properties?.["root"] === "string" ? e.properties["root"] : ctx.directory ?? process.cwd();
15826
- const r = await processSessionStart(sid, {
15827
- root,
15828
- log: log8,
15829
- injectRecovery: (inj) => {
15830
- _lastInjection = inj;
17107
+ enqueue(result) {
17108
+ tail = tail.then(async () => {
17109
+ if (queueAborted) {
17110
+ const attempt2 = {
17111
+ subtaskId: result.id,
17112
+ branch: result.id,
17113
+ worktree: result.worktree ?? null,
17114
+ ok: false,
17115
+ conflicts: [],
17116
+ skippedReason: "queue_aborted",
17117
+ durationMs: 0
17118
+ };
17119
+ report.attempts.push(attempt2);
17120
+ report.skipped++;
17121
+ if (result.worktree) {
17122
+ report.pendingWorktrees.push({
17123
+ subtaskId: result.id,
17124
+ path: result.worktree,
17125
+ conflicts: []
17126
+ });
15831
17127
  }
15832
- });
15833
- safeWriteLog(PLUGIN_NAME16, {
15834
- hook: "event",
15835
- type: "session.start",
15836
- ok: r.ok,
15837
- injected: r.injected,
15838
- reason: r.reason,
15839
- last_session_id: r.plan?.last_session_id
15840
- });
15841
- if (r.injected && r.plan) {
15842
- log8.info(`[${PLUGIN_NAME16}] 注入恢复提示(last=${r.plan.last_session_id?.slice(0, 8) ?? "?"}, reason=${r.plan.reason})`);
17128
+ await fireMergeAttempt(opts.onMergeAttempt, attempt2, idx++, log10);
17129
+ return;
17130
+ }
17131
+ const { attempt, abortFailed, pendingItem } = await mergeOneAttempt(result, { mergeRoot: opts.mergeRoot, summaryCharLimit: opts.summaryCharLimit }, mergeFns, log10);
17132
+ report.attempts.push(attempt);
17133
+ if (attempt.ok && !attempt.skippedReason)
17134
+ report.merged++;
17135
+ else if (attempt.skippedReason === "no_changes" || attempt.skippedReason === "subtask_failed" || attempt.skippedReason === "no_worktree" || attempt.skippedReason === "autonomy_off")
17136
+ report.skipped++;
17137
+ else
17138
+ report.conflicted++;
17139
+ if (pendingItem)
17140
+ report.pendingWorktrees.push(pendingItem);
17141
+ if (abortFailed) {
17142
+ queueAborted = true;
17143
+ log10("error", `[parallel-merge] queue aborted (mergeAbort failed)`, {
17144
+ id: result.id
17145
+ });
15843
17146
  }
17147
+ await fireMergeAttempt(opts.onMergeAttempt, attempt, idx++, log10);
15844
17148
  });
17149
+ },
17150
+ async flushAndReport() {
17151
+ await tail;
17152
+ return report;
15845
17153
  }
15846
17154
  };
15847
- };
15848
- var handler16 = sessionRecoveryServer;
15849
-
15850
- // plugins/subtasks.ts
15851
- import { promises as fs10 } from "node:fs";
15852
- import * as path13 from "node:path";
17155
+ }
17156
+ async function safeRemoveWorktree(fn, root, wt, log10, subtaskId) {
17157
+ try {
17158
+ await fn({ root, worktree_path: wt, force: true });
17159
+ } catch (err) {
17160
+ log10("warn", `[parallel] removeWorktree 失败 ${subtaskId}`, {
17161
+ error: describe4(err),
17162
+ worktree: wt
17163
+ });
17164
+ }
17165
+ }
17166
+ async function fireMergeAttempt(cb, attempt, idx, log10) {
17167
+ if (!cb)
17168
+ return;
17169
+ try {
17170
+ await cb(attempt, idx);
17171
+ } catch (err) {
17172
+ log10("warn", `[parallel] onMergeAttempt 抛错(已隔离)`, {
17173
+ id: attempt.subtaskId,
17174
+ error: describe4(err)
17175
+ });
17176
+ }
17177
+ }
17178
+ function describe4(err) {
17179
+ if (err instanceof Error)
17180
+ return err.message;
17181
+ if (typeof err === "string")
17182
+ return err;
17183
+ try {
17184
+ return JSON.stringify(err);
17185
+ } catch {
17186
+ return String(err);
17187
+ }
17188
+ }
15853
17189
 
15854
17190
  // lib/parallel.ts
15855
17191
  async function schedule(opts) {
15856
17192
  validateSpecs(opts.subtasks);
17193
+ if (opts.autoMerge && !opts.mergeRoot) {
17194
+ throw new Error("mergeRoot is required when autoMerge=true");
17195
+ }
15857
17196
  const now = opts.deps.now ?? Date.now;
15858
17197
  const start = now();
15859
- const log9 = opts.deps.log ?? (() => {});
17198
+ const log10 = opts.deps.log ?? (() => {});
15860
17199
  const concurrency = Math.max(1, opts.maxConcurrency ?? 4);
15861
17200
  const totalTimeout = opts.totalTimeout_ms ?? 30 * 60000;
15862
17201
  const limit = Math.max(1, opts.summaryCharLimit ?? 500);
@@ -15875,15 +17214,42 @@ async function schedule(opts) {
15875
17214
  const results = new Array(opts.subtasks.length);
15876
17215
  let nextIdx = 0;
15877
17216
  const workers = [];
17217
+ const pipelineEnabled = Boolean(opts.autoMerge && opts.pipelineMerge);
17218
+ let queue;
17219
+ if (pipelineEnabled) {
17220
+ queue = createMergeQueue({
17221
+ mergeRoot: opts.mergeRoot,
17222
+ summaryCharLimit: opts.summaryCharLimit,
17223
+ onMergeAttempt: opts.onMergeAttempt
17224
+ }, {
17225
+ commitWorktreeIfDirtyFn: opts.deps.commitWorktreeIfDirty,
17226
+ tryMergeFn: opts.deps.tryMerge,
17227
+ mergeCommitFn: opts.deps.mergeCommit,
17228
+ mergeAbortFn: opts.deps.mergeAbort,
17229
+ removeWorktreeFn: opts.deps.removeWorktree,
17230
+ worktreeHasChangesFn: opts.deps.worktreeHasChanges,
17231
+ log: log10
17232
+ });
17233
+ }
15878
17234
  const fireFinish = async (i, res) => {
17235
+ if (queue) {
17236
+ try {
17237
+ queue.enqueue(res);
17238
+ } catch (err) {
17239
+ log10("warn", `[parallel] queue.enqueue 抛错(已隔离)`, {
17240
+ id: res.id,
17241
+ error: describe5(err)
17242
+ });
17243
+ }
17244
+ }
15879
17245
  if (!opts.onSubtaskFinish)
15880
17246
  return;
15881
17247
  try {
15882
17248
  await opts.onSubtaskFinish(res, i);
15883
17249
  } catch (err) {
15884
- log9("warn", `[parallel] onSubtaskFinish 抛错(已隔离)`, {
17250
+ log10("warn", `[parallel] onSubtaskFinish 抛错(已隔离)`, {
15885
17251
  id: res.id,
15886
- error: describe4(err)
17252
+ error: describe5(err)
15887
17253
  });
15888
17254
  }
15889
17255
  };
@@ -15897,10 +17263,10 @@ async function schedule(opts) {
15897
17263
  const res2 = {
15898
17264
  id: spec.id,
15899
17265
  ok: false,
15900
- summary: clamp(`worktree 分配失败:${describe4(err)}`, limit),
17266
+ summary: clamp(`worktree 分配失败:${describe5(err)}`, limit),
15901
17267
  status: "failed",
15902
17268
  duration_ms: now() - subStart,
15903
- error: describe4(err)
17269
+ error: describe5(err)
15904
17270
  };
15905
17271
  results[i] = res2;
15906
17272
  await fireFinish(i, res2);
@@ -15911,9 +17277,9 @@ async function schedule(opts) {
15911
17277
  try {
15912
17278
  await opts.onSubtaskStart(spec, i);
15913
17279
  } catch (err) {
15914
- log9("warn", `[parallel] onSubtaskStart 抛错(已隔离)`, {
17280
+ log10("warn", `[parallel] onSubtaskStart 抛错(已隔离)`, {
15915
17281
  id: spec.id,
15916
- error: describe4(err)
17282
+ error: describe5(err)
15917
17283
  });
15918
17284
  }
15919
17285
  }
@@ -15949,21 +17315,21 @@ async function schedule(opts) {
15949
17315
  res = {
15950
17316
  id: spec.id,
15951
17317
  ok: false,
15952
- summary: clamp(`runSubtask 抛错:${describe4(err)}`, limit),
17318
+ summary: clamp(`runSubtask 抛错:${describe5(err)}`, limit),
15953
17319
  status: isAborted ? globalCtl.signal.aborted ? "cancelled" : "timeout" : "failed",
15954
17320
  duration_ms: now() - subStart,
15955
17321
  worktree: alloc?.path,
15956
- error: describe4(err)
17322
+ error: describe5(err)
15957
17323
  };
15958
17324
  } finally {
15959
17325
  clearTimeout(perTimer);
15960
17326
  globalCtl.signal.removeEventListener("abort", cascade);
15961
- if (alloc) {
17327
+ if (alloc && !opts.autoMerge) {
15962
17328
  try {
15963
17329
  await alloc.cleanup();
15964
17330
  } catch (err) {
15965
- log9("warn", `[parallel] worktree 清理失败 ${spec.id}`, {
15966
- error: describe4(err)
17331
+ log10("warn", `[parallel] worktree 清理失败 ${spec.id}`, {
17332
+ error: describe5(err)
15967
17333
  });
15968
17334
  }
15969
17335
  }
@@ -16001,12 +17367,21 @@ async function schedule(opts) {
16001
17367
  opts.signal.removeEventListener("abort", onParentAbort);
16002
17368
  const conflicts = detectConflicts(results);
16003
17369
  const digest = buildDigest(results, conflicts);
17370
+ let mergeReport;
17371
+ if (opts.autoMerge) {
17372
+ if (queue) {
17373
+ mergeReport = await queue.flushAndReport();
17374
+ } else {
17375
+ mergeReport = await mergeWorktrees(results, opts, opts.deps);
17376
+ }
17377
+ }
16004
17378
  return {
16005
17379
  parentId: opts.parentId,
16006
17380
  results,
16007
17381
  conflicts,
16008
17382
  digest,
16009
- duration_ms: now() - start
17383
+ duration_ms: now() - start,
17384
+ mergeReport
16010
17385
  };
16011
17386
  }
16012
17387
  var SUBTASK_ID_RE = /^[a-zA-Z0-9._-]+$/;
@@ -16078,7 +17453,7 @@ function buildDigest(results, conflicts) {
16078
17453
  conflicts
16079
17454
  };
16080
17455
  }
16081
- function describe4(err) {
17456
+ function describe5(err) {
16082
17457
  if (err instanceof Error)
16083
17458
  return err.message;
16084
17459
  if (typeof err === "string")
@@ -16113,7 +17488,7 @@ function pickStatus(r, taskAborted, globalAborted) {
16113
17488
  // lib/opencode-runner.ts
16114
17489
  init_opencode_plugin_helpers();
16115
17490
  function makeOpencodeRunner(opts) {
16116
- const log9 = opts.log ?? (() => {});
17491
+ const log10 = opts.log ?? (() => {});
16117
17492
  return async (spec, runCtx) => {
16118
17493
  const startedAt = Date.now();
16119
17494
  let childSessionId;
@@ -16128,13 +17503,13 @@ function makeOpencodeRunner(opts) {
16128
17503
  if (created.error || !created.data?.id) {
16129
17504
  return {
16130
17505
  ok: false,
16131
- summary: `session.create 失败:${describe5(created.error) || "no id"}`,
17506
+ summary: `session.create 失败:${describe6(created.error) || "no id"}`,
16132
17507
  status: "failed",
16133
- error: describe5(created.error) || "session.create 无 id"
17508
+ error: describe6(created.error) || "session.create 无 id"
16134
17509
  };
16135
17510
  }
16136
17511
  childSessionId = created.data.id;
16137
- log9("info", `[opencode-runner] subtask=${spec.id} session=${childSessionId} created`);
17512
+ log10("info", `[opencode-runner] subtask=${spec.id} session=${childSessionId} created`);
16138
17513
  const promptText = composePromptText(spec);
16139
17514
  const promptPromise = opts.client.session.prompt({
16140
17515
  path: { id: childSessionId },
@@ -16164,9 +17539,9 @@ function makeOpencodeRunner(opts) {
16164
17539
  if (r.error || !r.data) {
16165
17540
  return {
16166
17541
  ok: false,
16167
- summary: `session.prompt 失败:${describe5(r.error)}`,
17542
+ summary: `session.prompt 失败:${describe6(r.error)}`,
16168
17543
  status: "failed",
16169
- error: describe5(r.error) || "no data"
17544
+ error: describe6(r.error) || "no data"
16170
17545
  };
16171
17546
  }
16172
17547
  const lastText = pickLastText(r.data.parts ?? []);
@@ -16187,7 +17562,7 @@ function makeOpencodeRunner(opts) {
16187
17562
  });
16188
17563
  changedFiles = pickDiffFiles(diff.data);
16189
17564
  } catch (err) {
16190
- log9("warn", `[opencode-runner] diff 取失败 ${spec.id}`, { error: describe5(err) });
17565
+ log10("warn", `[opencode-runner] diff 取失败 ${spec.id}`, { error: describe6(err) });
16191
17566
  }
16192
17567
  const elapsed = Date.now() - startedAt;
16193
17568
  const summary = lastText || `subtask ${spec.id} 完成(${elapsed}ms,无文本输出,finish=${finishReason || "unknown"})`;
@@ -16196,14 +17571,14 @@ function makeOpencodeRunner(opts) {
16196
17571
  summary,
16197
17572
  status,
16198
17573
  changedFiles,
16199
- error: llmError ? describe5(llmError) : undefined
17574
+ error: llmError ? describe6(llmError) : undefined
16200
17575
  };
16201
17576
  } catch (err) {
16202
17577
  return {
16203
17578
  ok: false,
16204
- summary: `runner 抛错:${describe5(err)}`,
17579
+ summary: `runner 抛错:${describe6(err)}`,
16205
17580
  status: "failed",
16206
- error: describe5(err)
17581
+ error: describe6(err)
16207
17582
  };
16208
17583
  } finally {
16209
17584
  if (childSessionId) {
@@ -16213,8 +17588,8 @@ function makeOpencodeRunner(opts) {
16213
17588
  query: opts.directory ? { directory: opts.directory } : undefined
16214
17589
  });
16215
17590
  } catch (err) {
16216
- log9("warn", `[opencode-runner] session.delete 失败 ${childSessionId}`, {
16217
- error: describe5(err)
17591
+ log10("warn", `[opencode-runner] session.delete 失败 ${childSessionId}`, {
17592
+ error: describe6(err)
16218
17593
  });
16219
17594
  }
16220
17595
  }
@@ -16268,20 +17643,20 @@ async function withTimeout2(p, ms, signal) {
16268
17643
  throw err;
16269
17644
  }
16270
17645
  }
16271
- return await new Promise((resolve11, reject) => {
17646
+ return await new Promise((resolve13, reject) => {
16272
17647
  let settled = false;
16273
17648
  const timer = setTimeout(() => {
16274
17649
  if (settled)
16275
17650
  return;
16276
17651
  settled = true;
16277
- resolve11({ kind: "timeout" });
17652
+ resolve13({ kind: "timeout" });
16278
17653
  }, ms);
16279
17654
  const onAbort = () => {
16280
17655
  if (settled)
16281
17656
  return;
16282
17657
  settled = true;
16283
17658
  clearTimeout(timer);
16284
- resolve11({ kind: "aborted" });
17659
+ resolve13({ kind: "aborted" });
16285
17660
  };
16286
17661
  signal?.addEventListener("abort", onAbort, { once: true });
16287
17662
  p.then((value) => {
@@ -16290,7 +17665,7 @@ async function withTimeout2(p, ms, signal) {
16290
17665
  settled = true;
16291
17666
  clearTimeout(timer);
16292
17667
  signal?.removeEventListener("abort", onAbort);
16293
- resolve11({ kind: "ok", value });
17668
+ resolve13({ kind: "ok", value });
16294
17669
  }, (err) => {
16295
17670
  if (settled)
16296
17671
  return;
@@ -16301,7 +17676,7 @@ async function withTimeout2(p, ms, signal) {
16301
17676
  });
16302
17677
  });
16303
17678
  }
16304
- function describe5(err) {
17679
+ function describe6(err) {
16305
17680
  if (!err)
16306
17681
  return "";
16307
17682
  if (err instanceof Error)
@@ -16327,14 +17702,14 @@ function clip4(s, max) {
16327
17702
  return s.length <= max ? s : s.slice(0, max - 1) + "…";
16328
17703
  }
16329
17704
  async function sendParentNotice(client, sessionID, text, opts = {}) {
16330
- const log9 = opts.log ?? (() => {});
17705
+ const log10 = opts.log ?? (() => {});
16331
17706
  if (!client?.session) {
16332
- log9("warn", "[sendParentNotice] client.session 不可用,noop");
17707
+ log10("warn", "[sendParentNotice] client.session 不可用,noop");
16333
17708
  return false;
16334
17709
  }
16335
17710
  const sessionAny = client.session;
16336
17711
  if (typeof sessionAny.promptAsync !== "function") {
16337
- log9("warn", "[sendParentNotice] promptAsync 不可用(SDK 太老?),noop");
17712
+ log10("warn", "[sendParentNotice] promptAsync 不可用(SDK 太老?),noop");
16338
17713
  return false;
16339
17714
  }
16340
17715
  try {
@@ -16355,12 +17730,12 @@ async function sendParentNotice(client, sessionID, text, opts = {}) {
16355
17730
  }
16356
17731
  });
16357
17732
  if (res && typeof res === "object" && "error" in res && res.error) {
16358
- log9("warn", "[sendParentNotice] promptAsync 返回 error", { error: res.error });
17733
+ log10("warn", "[sendParentNotice] promptAsync 返回 error", { error: res.error });
16359
17734
  return false;
16360
17735
  }
16361
17736
  return true;
16362
17737
  } catch (err) {
16363
- log9("warn", "[sendParentNotice] 抛错(已隔离)", {
17738
+ log10("warn", "[sendParentNotice] 抛错(已隔离)", {
16364
17739
  error: err instanceof Error ? err.message : String(err)
16365
17740
  });
16366
17741
  return false;
@@ -16368,11 +17743,238 @@ async function sendParentNotice(client, sessionID, text, opts = {}) {
16368
17743
  }
16369
17744
 
16370
17745
  // plugins/subtasks.ts
16371
- init_opencode_plugin_helpers();
17746
+ init_opencode_plugin_helpers();
17747
+
17748
+ // lib/decompose.ts
17749
+ var DEFAULT_MAX_SUBTASKS = 8;
17750
+ var DEFAULT_TIMEOUT_MS2 = 30000;
17751
+ function buildSystemPrompt(maxSubtasks) {
17752
+ return [
17753
+ `你是任务拆分助手。把用户描述拆成 2 到 ${maxSubtasks} 个【可并行执行】的独立子任务。`,
17754
+ `硬约束:`,
17755
+ `(a) 子任务之间不互相依赖(不能"先 A 再 B"的串行关系)`,
17756
+ `(b) 不同子任务尽量改不同文件(hintFiles 填你预测会改的文件路径,用于冲突预检)`,
17757
+ `(c) 每个子任务自包含(单独给一个 agent 执行即可完成)`,
17758
+ `(d) 如果任务不能拆(如"先实现再测试"这种强依赖链),返回 {"single_task": true, "reason": "..."}`,
17759
+ ``,
17760
+ `输出格式(必须是合法 JSON,不要加 markdown 代码围栏之外的内容):`,
17761
+ `{"subtasks": [{"description": "...", "hintFiles": ["path/to/file.ts"]}]}`,
17762
+ `或`,
17763
+ `{"single_task": true, "reason": "无法并行,因为..."}`
17764
+ ].join(`
17765
+ `);
17766
+ }
17767
+ async function decomposeTask(description26, opts) {
17768
+ const log10 = opts.log ?? (() => {});
17769
+ if (opts.mockResponse) {
17770
+ return validateAndFinalize(opts.mockResponse, undefined, log10, opts.maxSubtasks ?? DEFAULT_MAX_SUBTASKS);
17771
+ }
17772
+ const maxSubtasks = opts.maxSubtasks ?? DEFAULT_MAX_SUBTASKS;
17773
+ const timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS2;
17774
+ let childSessionId;
17775
+ try {
17776
+ const created = await opts.client.session.create({
17777
+ body: { title: `decompose:${clip5(description26, 60)}` },
17778
+ query: opts.directory ? { directory: opts.directory } : undefined
17779
+ });
17780
+ if (created.error || !created.data?.id) {
17781
+ log10("warn", "[decompose] session.create 失败", { error: describe7(created.error) });
17782
+ return {
17783
+ ok: false,
17784
+ subtasks: [],
17785
+ reason: "llm_unavailable"
17786
+ };
17787
+ }
17788
+ childSessionId = created.data.id;
17789
+ const systemText = buildSystemPrompt(maxSubtasks);
17790
+ const promptPromise = Promise.resolve(opts.client.session.prompt({
17791
+ path: { id: childSessionId },
17792
+ body: {
17793
+ system: systemText,
17794
+ parts: [{ type: "text", text: description26 }]
17795
+ },
17796
+ query: opts.directory ? { directory: opts.directory } : undefined
17797
+ }));
17798
+ const raced = await Promise.race([
17799
+ promptPromise.then((v) => ({ kind: "ok", value: v })),
17800
+ sleep(timeoutMs).then(() => ({ kind: "timeout" }))
17801
+ ]);
17802
+ if (raced.kind === "timeout") {
17803
+ log10("warn", "[decompose] LLM 调用超时", { timeoutMs });
17804
+ return { ok: false, subtasks: [], reason: "llm_unavailable" };
17805
+ }
17806
+ const r = raced.value;
17807
+ if (r.error || !r.data) {
17808
+ log10("warn", "[decompose] session.prompt 返回错误", { error: describe7(r.error) });
17809
+ return { ok: false, subtasks: [], reason: "llm_unavailable" };
17810
+ }
17811
+ const rawText = pickLastText2(r.data.parts ?? []);
17812
+ if (!rawText) {
17813
+ log10("warn", "[decompose] LLM 输出为空");
17814
+ return { ok: false, subtasks: [], reason: "parse_failed", raw: "" };
17815
+ }
17816
+ const parsed = extractJson(rawText);
17817
+ if (!parsed) {
17818
+ log10("warn", "[decompose] JSON 解析失败", { raw: clip5(rawText, 200) });
17819
+ return { ok: false, subtasks: [], reason: "parse_failed", raw: rawText };
17820
+ }
17821
+ if (parsed && typeof parsed === "object" && parsed.single_task === true) {
17822
+ return { ok: false, subtasks: [], reason: "single_task", raw: rawText };
17823
+ }
17824
+ const subtasksRaw = parsed.subtasks;
17825
+ if (!Array.isArray(subtasksRaw)) {
17826
+ log10("warn", "[decompose] JSON 缺 subtasks 数组");
17827
+ return { ok: false, subtasks: [], reason: "parse_failed", raw: rawText };
17828
+ }
17829
+ const normalized = [];
17830
+ for (const item of subtasksRaw) {
17831
+ if (!item || typeof item !== "object")
17832
+ continue;
17833
+ const o = item;
17834
+ const desc = typeof o.description === "string" ? o.description.trim() : "";
17835
+ if (!desc) {
17836
+ return { ok: false, subtasks: [], reason: "parse_failed", raw: rawText };
17837
+ }
17838
+ const hintFiles = Array.isArray(o.hintFiles) ? o.hintFiles.filter((x) => typeof x === "string" && x.trim().length > 0) : undefined;
17839
+ normalized.push({ description: desc, hintFiles });
17840
+ }
17841
+ if (normalized.length > maxSubtasks) {
17842
+ log10("warn", "[decompose] LLM 返回子任务超过上限", { count: normalized.length, maxSubtasks });
17843
+ return { ok: false, subtasks: [], reason: "parse_failed", raw: rawText };
17844
+ }
17845
+ return validateAndFinalize(normalized, rawText, log10, maxSubtasks);
17846
+ } catch (err) {
17847
+ log10("warn", "[decompose] 抛错", { error: describe7(err) });
17848
+ return { ok: false, subtasks: [], reason: "llm_unavailable" };
17849
+ } finally {
17850
+ if (childSessionId) {
17851
+ try {
17852
+ await opts.client.session.delete({
17853
+ path: { id: childSessionId },
17854
+ query: opts.directory ? { directory: opts.directory } : undefined
17855
+ });
17856
+ } catch (err) {
17857
+ log10("warn", "[decompose] session.delete 失败", { error: describe7(err) });
17858
+ }
17859
+ }
17860
+ }
17861
+ }
17862
+ function validateAndFinalize(subtasks, raw, log10, maxSubtasks) {
17863
+ if (subtasks.length < 2) {
17864
+ return { ok: false, subtasks: [], reason: "single_task", raw };
17865
+ }
17866
+ for (const t of subtasks) {
17867
+ if (!t.description || !t.description.trim()) {
17868
+ return { ok: false, subtasks: [], reason: "parse_failed", raw };
17869
+ }
17870
+ }
17871
+ if (subtasks.length > maxSubtasks) {
17872
+ log10("warn", "[decompose] LLM 返回子任务超过上限", { count: subtasks.length, maxSubtasks });
17873
+ return { ok: false, subtasks: [], reason: "parse_failed", raw };
17874
+ }
17875
+ for (let i = 0;i < subtasks.length; i++) {
17876
+ const a = subtasks[i].hintFiles;
17877
+ if (!a || a.length === 0)
17878
+ continue;
17879
+ const setA = new Set(a.map((p) => p.trim()).filter(Boolean));
17880
+ for (let j = i + 1;j < subtasks.length; j++) {
17881
+ const b = subtasks[j].hintFiles;
17882
+ if (!b || b.length === 0)
17883
+ continue;
17884
+ for (const f of b) {
17885
+ if (setA.has(f.trim())) {
17886
+ log10("info", "[decompose] hintFiles 有交集,降级 single_task", {
17887
+ a: subtasks[i].description,
17888
+ b: subtasks[j].description,
17889
+ file: f
17890
+ });
17891
+ return { ok: false, subtasks: [], reason: "single_task", raw };
17892
+ }
17893
+ }
17894
+ }
17895
+ }
17896
+ return { ok: true, subtasks, reason: "ok", raw: undefined };
17897
+ }
17898
+ function pickLastText2(parts) {
17899
+ for (let i = parts.length - 1;i >= 0; i--) {
17900
+ const p = parts[i];
17901
+ if (p && p.type === "text" && typeof p.text === "string" && !p.synthetic && !p.ignored) {
17902
+ return p.text.trim();
17903
+ }
17904
+ }
17905
+ return "";
17906
+ }
17907
+ function extractJson(text) {
17908
+ if (!text)
17909
+ return null;
17910
+ const fenceMatch = text.match(/```(?:json)?\s*\n?([\s\S]*?)\n?```/i);
17911
+ if (fenceMatch && fenceMatch[1]) {
17912
+ const inner = fenceMatch[1].trim();
17913
+ const parsed = tryParse(inner);
17914
+ if (parsed !== null)
17915
+ return parsed;
17916
+ }
17917
+ const direct = tryParse(text.trim());
17918
+ if (direct !== null)
17919
+ return direct;
17920
+ for (let end = text.length - 1;end >= 0; end--) {
17921
+ if (text[end] !== "}")
17922
+ continue;
17923
+ let depth = 0;
17924
+ for (let start = end;start >= 0; start--) {
17925
+ const ch = text[start];
17926
+ if (ch === "}")
17927
+ depth++;
17928
+ else if (ch === "{") {
17929
+ depth--;
17930
+ if (depth === 0) {
17931
+ const slice = text.slice(start, end + 1);
17932
+ const parsed = tryParse(slice);
17933
+ if (parsed !== null)
17934
+ return parsed;
17935
+ break;
17936
+ }
17937
+ }
17938
+ }
17939
+ }
17940
+ return null;
17941
+ }
17942
+ function tryParse(s) {
17943
+ if (!s)
17944
+ return null;
17945
+ try {
17946
+ return JSON.parse(s);
17947
+ } catch {
17948
+ return null;
17949
+ }
17950
+ }
17951
+ function clip5(s, n) {
17952
+ if (!s)
17953
+ return "";
17954
+ return s.length <= n ? s : s.slice(0, n - 1) + "…";
17955
+ }
17956
+ function describe7(err) {
17957
+ if (!err)
17958
+ return "";
17959
+ if (err instanceof Error)
17960
+ return err.message;
17961
+ if (typeof err === "string")
17962
+ return err;
17963
+ try {
17964
+ return JSON.stringify(err);
17965
+ } catch {
17966
+ return String(err);
17967
+ }
17968
+ }
17969
+ function sleep(ms) {
17970
+ return new Promise((resolve13) => setTimeout(resolve13, ms));
17971
+ }
17972
+
17973
+ // plugins/subtasks.ts
16372
17974
  init_runtime_paths();
16373
- var PLUGIN_NAME17 = "subtasks";
17975
+ var PLUGIN_NAME18 = "subtasks";
16374
17976
  function getLogFile(root = process.cwd()) {
16375
- return path13.join(runtimeDir(root), "logs", "subtasks.log");
17977
+ return path15.join(runtimeDir(root), "logs", "subtasks.log");
16376
17978
  }
16377
17979
  var VERB_RE = /^([a-zA-Z]{3,12})/;
16378
17980
  var CN_VERBS = [
@@ -16436,9 +18038,10 @@ var mockRunner = async (spec) => {
16436
18038
  };
16437
18039
  async function handleParallelCommand(raw) {
16438
18040
  const ctx = raw ?? {};
16439
- const log9 = ctx.log;
18041
+ const log10 = ctx.log;
16440
18042
  const args = ctx.args ?? {};
16441
18043
  const parentId = (args.parentId ?? `task-${Date.now().toString(36)}`).replace(/[^a-zA-Z0-9._-]/g, "-");
18044
+ const rawDescription = typeof args.description === "string" ? args.description.trim() : "";
16442
18045
  let specs = [];
16443
18046
  if (Array.isArray(args.subtasks) && args.subtasks.length > 0) {
16444
18047
  specs = args.subtasks.map((s, i) => ({
@@ -16449,16 +18052,80 @@ async function handleParallelCommand(raw) {
16449
18052
  specs = splitDescriptions(args.description, { parentId });
16450
18053
  }
16451
18054
  if (specs.length === 0) {
16452
- log9?.warn(`[${PLUGIN_NAME17}] /parallel 缺有效子任务`);
18055
+ log10?.warn(`[${PLUGIN_NAME18}] /parallel 缺有效子任务`);
16453
18056
  await safeReply2(ctx, "⚠ /parallel 需要至少 1 个子任务(用 ; 或换行分隔)");
16454
18057
  return { ok: false, reason: "no_subtasks" };
16455
18058
  }
16456
18059
  const maxConcurrency = clampInt(args.maxConcurrency, 1, 16, 4);
16457
18060
  const totalTimeoutMs = clampInt(args.totalTimeout_ms, 1000, 60 * 60000, 30 * 60000);
18061
+ let autoMerge = ctx.autoMerge;
18062
+ if (autoMerge === undefined) {
18063
+ if (ctx.allocateWorktree !== undefined) {
18064
+ autoMerge = false;
18065
+ } else {
18066
+ try {
18067
+ const rt = await loadRuntime({ root: ctx.directory });
18068
+ autoMerge = rt.runtime.runtime.worktree_isolation === true;
18069
+ } catch {
18070
+ autoMerge = false;
18071
+ }
18072
+ }
18073
+ }
18074
+ let downgradedAutoMerge = false;
18075
+ const mergeRoot = autoMerge ? ctx.directory ?? process.cwd() : undefined;
18076
+ if (autoMerge && mergeRoot) {
18077
+ const gitOk = await isGitRepo({ root: mergeRoot }).catch(() => false);
18078
+ if (!gitOk) {
18079
+ autoMerge = false;
18080
+ downgradedAutoMerge = true;
18081
+ log10?.warn("[subtasks] mergeRoot 非 git 仓库,降级 autoMerge=false");
18082
+ const canNoticeEarly = Boolean(ctx.client && ctx.parentSessionID);
18083
+ if (canNoticeEarly) {
18084
+ await sendParentNotice(ctx.client, ctx.parentSessionID, "ℹ 当前目录不是 git 仓库,worktree 隔离已自动关闭,以单目录模式运行", { directory: ctx.directory });
18085
+ }
18086
+ }
18087
+ }
16458
18088
  const canNotice = Boolean(ctx.client && ctx.parentSessionID);
18089
+ if (specs.length === 1 && rawDescription.length >= 30 && ctx.client) {
18090
+ log10?.info(`[${PLUGIN_NAME18}] 触发 AI 拆分(描述长度=${rawDescription.length})`);
18091
+ const dec = await decomposeTask(rawDescription, {
18092
+ client: ctx.client,
18093
+ directory: ctx.directory,
18094
+ log: (lvl, msg, data) => {
18095
+ if (lvl === "error")
18096
+ log10?.error(msg, data);
18097
+ else if (lvl === "warn")
18098
+ log10?.warn(msg, data);
18099
+ else
18100
+ log10?.info(msg, data);
18101
+ },
18102
+ mockResponse: ctx.decomposeMockResponse
18103
+ });
18104
+ if (dec.ok && dec.subtasks.length >= 2) {
18105
+ specs = dec.subtasks.map((d, i) => ({
18106
+ id: `${parentId}-${i + 1}-${pickVerb(d.description, "task")}`,
18107
+ description: d.description,
18108
+ timeout_ms: undefined,
18109
+ args: d.hintFiles && d.hintFiles.length > 0 ? { hintFiles: d.hintFiles } : undefined
18110
+ }));
18111
+ log10?.info(`[${PLUGIN_NAME18}] AI 拆分成功 → ${specs.length} 个子任务`);
18112
+ if (canNotice) {
18113
+ const lines = [
18114
+ `\uD83E\uDD16 AI 已自动拆为 ${specs.length} 个并行子任务:`,
18115
+ ...specs.map((s, i) => ` ${i + 1}. ${s.description}`)
18116
+ ];
18117
+ await sendParentNotice(ctx.client, ctx.parentSessionID, lines.join(`
18118
+ `), {
18119
+ directory: ctx.directory
18120
+ });
18121
+ }
18122
+ } else if (dec.reason === "llm_unavailable" || dec.reason === "parse_failed") {
18123
+ log10?.warn(`[${PLUGIN_NAME18}] AI 拆分失败(${dec.reason}),按单任务执行`);
18124
+ }
18125
+ }
16459
18126
  if (canNotice) {
16460
18127
  const lines = [
16461
- `\uD83D\uDE80 /parallel 已派出 ${specs.length} 个子任务(并发=${maxConcurrency}):`,
18128
+ `\uD83D\uDE80 /parallel 已派出 ${specs.length} 个子任务(并发=${maxConcurrency}, worktree 隔离=${autoMerge ? "ON" : "OFF"}):`,
16462
18129
  ...specs.map((s, i) => ` ${i + 1}. \`${s.id}\` — ${s.description}`),
16463
18130
  "",
16464
18131
  "\uD83D\uDCA1 用 /parallel-status 随时查进度;TUI 按 Ctrl+→ 进子 session 看实时。"
@@ -16468,11 +18135,11 @@ async function handleParallelCommand(raw) {
16468
18135
  directory: ctx.directory,
16469
18136
  log: (lvl, msg, data) => {
16470
18137
  if (lvl === "error")
16471
- log9?.error(msg, data);
18138
+ log10?.error(msg, data);
16472
18139
  else if (lvl === "warn")
16473
- log9?.warn(msg, data);
18140
+ log10?.warn(msg, data);
16474
18141
  else
16475
- log9?.info(msg, data);
18142
+ log10?.info(msg, data);
16476
18143
  }
16477
18144
  });
16478
18145
  }
@@ -16484,6 +18151,9 @@ async function handleParallelCommand(raw) {
16484
18151
  subtasks: specs,
16485
18152
  maxConcurrency,
16486
18153
  totalTimeout_ms: totalTimeoutMs,
18154
+ autoMerge,
18155
+ mergeRoot: autoMerge ? ctx.directory ?? process.cwd() : undefined,
18156
+ pipelineMerge: true,
16487
18157
  onSubtaskStart: canNotice ? async (spec, idx) => {
16488
18158
  await sendParentNotice(ctx.client, ctx.parentSessionID, `▶ 子任务 ${idx + 1}/${specs.length} 启动: \`${spec.id}\` — ${spec.description}`, { directory: ctx.directory });
16489
18159
  } : undefined,
@@ -16492,40 +18162,75 @@ async function handleParallelCommand(raw) {
16492
18162
  const fileCount = res.changedFiles?.length ?? 0;
16493
18163
  await sendParentNotice(ctx.client, ctx.parentSessionID, `${emoji} 子任务 ${idx + 1}/${specs.length} 完成 [${res.status}] \`${res.id}\` (${res.duration_ms}ms, files=${fileCount})`, { directory: ctx.directory });
16494
18164
  } : undefined,
18165
+ onMergeAttempt: canNotice ? async (attempt) => {
18166
+ if (attempt.skippedReason) {
18167
+ if (attempt.skippedReason === "no_changes") {
18168
+ await sendParentNotice(ctx.client, ctx.parentSessionID, `⏭ merge \`${attempt.subtaskId}\`: 无改动,跳过`, { directory: ctx.directory });
18169
+ } else if (attempt.skippedReason === "queue_aborted") {
18170
+ await sendParentNotice(ctx.client, ctx.parentSessionID, `\uD83D\uDED1 merge \`${attempt.subtaskId}\`: 队列已锁死,跳过(worktree 保留)`, { directory: ctx.directory });
18171
+ }
18172
+ return;
18173
+ }
18174
+ if (attempt.ok) {
18175
+ await sendParentNotice(ctx.client, ctx.parentSessionID, `\uD83D\uDD00 merge \`${attempt.subtaskId}\`: ✅ 成功`, { directory: ctx.directory });
18176
+ } else {
18177
+ const n = attempt.conflicts.length;
18178
+ await sendParentNotice(ctx.client, ctx.parentSessionID, `\uD83D\uDD00 merge \`${attempt.subtaskId}\`: ⚠ 冲突 ${n} 个文件`, { directory: ctx.directory });
18179
+ }
18180
+ } : undefined,
16495
18181
  deps: {
16496
18182
  runSubtask: runner,
16497
- allocateWorktree: ctx.allocateWorktree,
18183
+ allocateWorktree: downgradedAutoMerge ? undefined : ctx.allocateWorktree,
16498
18184
  log: (lvl, msg, data) => {
16499
18185
  if (lvl === "error")
16500
- log9?.error(msg, data);
18186
+ log10?.error(msg, data);
16501
18187
  else if (lvl === "warn")
16502
- log9?.warn(msg, data);
18188
+ log10?.warn(msg, data);
16503
18189
  else
16504
- log9?.info(msg, data);
16505
- }
18190
+ log10?.info(msg, data);
18191
+ },
18192
+ tryMerge: ctx.tryMerge,
18193
+ mergeCommit: ctx.mergeCommit,
18194
+ mergeAbort: ctx.mergeAbort,
18195
+ worktreeHasChanges: ctx.worktreeHasChanges,
18196
+ removeWorktree: ctx.removeWorktree,
18197
+ commitWorktreeIfDirty: ctx.commitWorktreeIfDirty
16506
18198
  }
16507
18199
  });
16508
18200
  } catch (err) {
16509
18201
  const msg = err instanceof Error ? err.message : String(err);
16510
- log9?.error(`[${PLUGIN_NAME17}] schedule 抛错`, { error: msg });
18202
+ log10?.error(`[${PLUGIN_NAME18}] schedule 抛错`, { error: msg });
16511
18203
  await safeReply2(ctx, `❌ 并发调度失败:${msg}`);
16512
18204
  return { ok: false, reason: msg };
16513
18205
  }
16514
18206
  const head = `\uD83D\uDCCA 并发完成 ${result.results.length} 个子任务(${result.duration_ms}ms)`;
16515
18207
  const summaryLines = [head, "", "```", result.digest.text, "```"];
18208
+ if (result.mergeReport && result.mergeReport.pendingWorktrees.length > 0) {
18209
+ const repoCwd = mergeRoot ?? ctx.directory ?? process.cwd();
18210
+ summaryLines.push("", `⚠ ${result.mergeReport.pendingWorktrees.length} 个 worktree 有冲突,请手动解决:`);
18211
+ for (const p of result.mergeReport.pendingWorktrees) {
18212
+ const n = p.conflicts.length;
18213
+ const hint = n > 0 ? `(${n} files)` : "";
18214
+ summaryLines.push(` • ${p.path} ${hint} → cd ${repoCwd} && git merge ${p.subtaskId}`);
18215
+ }
18216
+ summaryLines.push("", `(解决冲突后在主仓库执行上面的 \`git merge\` 命令;冲突 worktree 路径已保留,可 cd 进去查看 status)`);
18217
+ }
16516
18218
  await safeReply2(ctx, summaryLines.join(`
16517
18219
  `));
16518
- log9?.info(`[${PLUGIN_NAME17}] schedule 完成`, {
18220
+ log10?.info(`[${PLUGIN_NAME18}] schedule 完成`, {
16519
18221
  parentId,
16520
18222
  success: result.digest.success,
16521
18223
  failed: result.digest.failed,
16522
- conflicts: result.conflicts.hasConflict
18224
+ conflicts: result.conflicts.hasConflict,
18225
+ autoMerge,
18226
+ merged: result.mergeReport?.merged ?? 0,
18227
+ conflicted: result.mergeReport?.conflicted ?? 0
16523
18228
  });
16524
18229
  if (typeof ctx.onCompleted === "function") {
16525
18230
  try {
16526
18231
  await ctx.onCompleted(result);
16527
18232
  } catch (err) {
16528
- log9?.warn(`[${PLUGIN_NAME17}] onCompleted hook 抛错(已隔离)`, {
18233
+ log10?.warn(`[${PLUGIN_NAME18}] onCompleted hook 抛错(已隔离)`, {
16529
18234
  error: err instanceof Error ? err.message : String(err)
16530
18235
  });
16531
18236
  }
@@ -16559,29 +18264,30 @@ async function maybeHandleMessage(raw) {
16559
18264
  log: ctx.log,
16560
18265
  client: ctx.client,
16561
18266
  parentSessionID: ctx.parentSessionID,
16562
- directory: ctx.directory
18267
+ directory: ctx.directory,
18268
+ autoMerge: ctx.autoMerge
16563
18269
  });
16564
18270
  }
16565
18271
  async function writeLog(level, msg, data) {
16566
18272
  const line = JSON.stringify({
16567
18273
  ts: new Date().toISOString(),
16568
18274
  level,
16569
- plugin: PLUGIN_NAME17,
18275
+ plugin: PLUGIN_NAME18,
16570
18276
  msg,
16571
18277
  data
16572
18278
  }) + `
16573
18279
  `;
16574
18280
  try {
16575
18281
  const logFile = getLogFile();
16576
- await fs10.mkdir(path13.dirname(logFile), { recursive: true });
16577
- await fs10.appendFile(logFile, line, "utf8");
18282
+ await fs12.mkdir(path15.dirname(logFile), { recursive: true });
18283
+ await fs12.appendFile(logFile, line, "utf8");
16578
18284
  } catch {}
16579
18285
  }
16580
- logLifecycle(PLUGIN_NAME17, "import");
18286
+ logLifecycle(PLUGIN_NAME18, "import");
16581
18287
  var subtasksServer = async (ctx) => {
16582
- const log9 = makePluginLogger(PLUGIN_NAME17);
18288
+ const log10 = makePluginLogger(PLUGIN_NAME18);
16583
18289
  const client = ctx?.client ?? undefined;
16584
- logLifecycle(PLUGIN_NAME17, "activate", {
18290
+ logLifecycle(PLUGIN_NAME18, "activate", {
16585
18291
  directory: ctx.directory,
16586
18292
  hasClient: Boolean(client)
16587
18293
  });
@@ -16594,6 +18300,40 @@ var subtasksServer = async (ctx) => {
16594
18300
  if (!description26) {
16595
18301
  return;
16596
18302
  }
18303
+ let autoMerge = false;
18304
+ try {
18305
+ const rt = await loadRuntime({ root: ctx.directory });
18306
+ autoMerge = rt.runtime.runtime.worktree_isolation === true;
18307
+ } catch {
18308
+ autoMerge = false;
18309
+ }
18310
+ const repoRoot = ctx.directory ?? process.cwd();
18311
+ if (autoMerge) {
18312
+ const gitOk = await isGitRepo({ root: repoRoot }).catch(() => false);
18313
+ if (!gitOk) {
18314
+ autoMerge = false;
18315
+ log10?.warn("[subtasks] worktree_isolation=true 但非 git 仓库,自动降级 autoMerge=false");
18316
+ const canNotice = Boolean(client);
18317
+ if (canNotice) {
18318
+ await sendParentNotice(client, input.sessionID, "ℹ 当前目录不是 git 仓库,worktree 隔离已自动关闭,以单目录模式运行", { directory: ctx.directory });
18319
+ }
18320
+ }
18321
+ }
18322
+ const allocateWorktree = autoMerge ? async (subtaskId) => {
18323
+ const wt = await ensureWorktree({ root: repoRoot, branch: subtaskId });
18324
+ return {
18325
+ path: wt.path,
18326
+ cleanup: async () => {
18327
+ try {
18328
+ await removeWorktree({
18329
+ root: repoRoot,
18330
+ worktree_path: wt.path,
18331
+ force: true
18332
+ });
18333
+ } catch {}
18334
+ }
18335
+ };
18336
+ } : undefined;
16597
18337
  const replyLines = [];
16598
18338
  const messageCtx = {
16599
18339
  content: `/parallel ${description26}`,
@@ -16601,10 +18341,12 @@ var subtasksServer = async (ctx) => {
16601
18341
  replyLines.push(s);
16602
18342
  return Promise.resolve();
16603
18343
  },
16604
- log: log9,
18344
+ log: log10,
16605
18345
  client,
16606
18346
  parentSessionID: input.sessionID,
16607
18347
  directory: ctx.directory,
18348
+ allocateWorktree,
18349
+ autoMerge,
16608
18350
  runner: client ? makeOpencodeRunner({
16609
18351
  client,
16610
18352
  parentSessionID: input.sessionID,
@@ -16612,11 +18354,11 @@ var subtasksServer = async (ctx) => {
16612
18354
  perTaskTimeoutMs: 5 * 60000,
16613
18355
  log: (lvl, msg, data) => {
16614
18356
  if (lvl === "error")
16615
- log9.error(msg, data);
18357
+ log10.error(msg, data);
16616
18358
  else if (lvl === "warn")
16617
- log9.warn(msg, data);
18359
+ log10.warn(msg, data);
16618
18360
  else
16619
- log9.info(msg, data);
18361
+ log10.info(msg, data);
16620
18362
  }
16621
18363
  }) : undefined
16622
18364
  };
@@ -16625,7 +18367,8 @@ var subtasksServer = async (ctx) => {
16625
18367
  sessionID: input.sessionID,
16626
18368
  ok: result?.ok,
16627
18369
  subtasks: result?.scheduled?.results?.length ?? 0,
16628
- usedMockRunner: !client
18370
+ usedMockRunner: !client,
18371
+ autoMerge
16629
18372
  });
16630
18373
  if (replyLines.length > 0 && Array.isArray(output?.parts)) {
16631
18374
  output.parts.length = 0;
@@ -16641,20 +18384,20 @@ var subtasksServer = async (ctx) => {
16641
18384
  });
16642
18385
  }
16643
18386
  } catch (err) {
16644
- log9.error(`[${PLUGIN_NAME17}] command.execute.before 异常(已隔离)`, {
18387
+ log10.error(`[${PLUGIN_NAME18}] command.execute.before 异常(已隔离)`, {
16645
18388
  error: err instanceof Error ? err.message : String(err)
16646
18389
  });
16647
18390
  }
16648
18391
  }
16649
18392
  };
16650
18393
  };
16651
- var handler17 = subtasksServer;
18394
+ var handler18 = subtasksServer;
16652
18395
 
16653
18396
  // plugins/terminal-monitor.ts
16654
18397
  init_opencode_plugin_helpers();
16655
18398
  import * as crypto5 from "node:crypto";
16656
- var PLUGIN_NAME18 = "terminal-monitor";
16657
- logLifecycle(PLUGIN_NAME18, "import", {});
18399
+ var PLUGIN_NAME19 = "terminal-monitor";
18400
+ logLifecycle(PLUGIN_NAME19, "import", {});
16658
18401
  var DEFAULT_CONFIG7 = {
16659
18402
  minScore: 0.6,
16660
18403
  cooldownMs: 30000,
@@ -16797,7 +18540,7 @@ function parseTerminalOutput(ev, cfg = {}, rules = DEFAULT_RULES) {
16797
18540
  `).length;
16798
18541
  const lineFromStart = text.split(`
16799
18542
  `)[lineIdx - 1] ?? m[0];
16800
- const excerpt = clip5(lineFromStart.trim(), c.maxExcerpt);
18543
+ const excerpt = clip6(lineFromStart.trim(), c.maxExcerpt);
16801
18544
  if (!out.has(rule.kind)) {
16802
18545
  out.set(rule.kind, {
16803
18546
  severity: rule.severity,
@@ -16819,7 +18562,7 @@ function parseTerminalOutput(ev, cfg = {}, rules = DEFAULT_RULES) {
16819
18562
  }
16820
18563
  return [...out.values()].sort((a, b) => b.score - a.score);
16821
18564
  }
16822
- function clip5(s, max) {
18565
+ function clip6(s, max) {
16823
18566
  if (s.length <= max)
16824
18567
  return s;
16825
18568
  return s.slice(0, max - 1) + "…";
@@ -16878,7 +18621,7 @@ function shouldNotify(findings, ev, lru, cfg = {}, now = Date.now()) {
16878
18621
  function buildSummary(ev, findings) {
16879
18622
  const parts = [];
16880
18623
  if (ev.cmd)
16881
- parts.push(`\`${clip5(ev.cmd, 60)}\``);
18624
+ parts.push(`\`${clip6(ev.cmd, 60)}\``);
16882
18625
  if (ev.type === "terminal.exit" && typeof ev.exit_code === "number") {
16883
18626
  parts.push(`exit=${ev.exit_code}`);
16884
18627
  }
@@ -16890,7 +18633,7 @@ function buildSummary(ev, findings) {
16890
18633
  return parts.join(" · ");
16891
18634
  }
16892
18635
  const top = findings[0];
16893
- parts.push(`${top.severity}/${top.kind}: ${clip5(top.excerpt, 80)}`);
18636
+ parts.push(`${top.severity}/${top.kind}: ${clip6(top.excerpt, 80)}`);
16894
18637
  if (findings.length > 1)
16895
18638
  parts.push(`(+${findings.length - 1} 条)`);
16896
18639
  return parts.join(" · ");
@@ -16936,17 +18679,17 @@ function describeError(err) {
16936
18679
  return String(err);
16937
18680
  }
16938
18681
  }
16939
- var log9 = makePluginLogger(PLUGIN_NAME18);
18682
+ var log10 = makePluginLogger(PLUGIN_NAME19);
16940
18683
  var lru = new FingerprintLRU2;
16941
18684
  var _lastNotification = null;
16942
18685
  var terminalMonitorServer = async (ctx) => {
16943
- logLifecycle(PLUGIN_NAME18, "activate", {
18686
+ logLifecycle(PLUGIN_NAME19, "activate", {
16944
18687
  directory: ctx.directory,
16945
18688
  triggerEventTypes: ["terminal.output", "terminal.exit"]
16946
18689
  });
16947
18690
  return {
16948
18691
  event: async ({ event }) => {
16949
- await safeAsync(PLUGIN_NAME18, "event", async () => {
18692
+ await safeAsync(PLUGIN_NAME19, "event", async () => {
16950
18693
  const e = event;
16951
18694
  if (!e || typeof e.type !== "string")
16952
18695
  return;
@@ -16955,12 +18698,12 @@ var terminalMonitorServer = async (ctx) => {
16955
18698
  const ev = { type: e.type, ...e.properties ?? {} };
16956
18699
  const r = await processTerminalEvent(ev, {
16957
18700
  lru,
16958
- log: log9,
18701
+ log: log10,
16959
18702
  notifyAgent: (msg) => {
16960
18703
  _lastNotification = msg;
16961
18704
  }
16962
18705
  });
16963
- safeWriteLog(PLUGIN_NAME18, {
18706
+ safeWriteLog(PLUGIN_NAME19, {
16964
18707
  hook: "event",
16965
18708
  type: e.type,
16966
18709
  notified: r.notified,
@@ -16968,19 +18711,19 @@ var terminalMonitorServer = async (ctx) => {
16968
18711
  findings: r.notification?.findings.length ?? 0
16969
18712
  });
16970
18713
  if (r.notified && r.notification) {
16971
- log9.info(`[${PLUGIN_NAME18}] 反馈 ${r.notification.findings.length} 条 → ${r.notification.summary}`);
18714
+ log10.info(`[${PLUGIN_NAME19}] 反馈 ${r.notification.findings.length} 条 → ${r.notification.summary}`);
16972
18715
  }
16973
18716
  });
16974
18717
  }
16975
18718
  };
16976
18719
  };
16977
- var handler18 = terminalMonitorServer;
18720
+ var handler19 = terminalMonitorServer;
16978
18721
 
16979
18722
  // plugins/token-manager.ts
16980
18723
  init_opencode_plugin_helpers();
16981
- var PLUGIN_NAME19 = "token-manager";
16982
- logLifecycle(PLUGIN_NAME19, "import", {});
16983
- async function handleMessageBefore(raw, log10, defaults) {
18724
+ var PLUGIN_NAME20 = "token-manager";
18725
+ logLifecycle(PLUGIN_NAME20, "import", {});
18726
+ async function handleMessageBefore(raw, log11, defaults) {
16984
18727
  const ctx = raw ?? {};
16985
18728
  if (!Array.isArray(ctx.messages) || ctx.messages.length === 0)
16986
18729
  return null;
@@ -16999,21 +18742,21 @@ async function handleMessageBefore(raw, log10, defaults) {
16999
18742
  };
17000
18743
  if (r.compressed) {
17001
18744
  ctx.messages = r.messages;
17002
- log10?.info(`[${PLUGIN_NAME19}] 压缩 ${r.before.count}→${r.after.count} 条 / ${r.before.tokens}→${r.after.tokens} tokens`);
18745
+ log11?.info(`[${PLUGIN_NAME20}] 压缩 ${r.before.count}→${r.after.count} 条 / ${r.before.tokens}→${r.after.tokens} tokens`);
17003
18746
  }
17004
18747
  return r;
17005
18748
  } catch (err) {
17006
- log10?.warn(`[${PLUGIN_NAME19}] 压缩异常(已隔离)`, {
18749
+ log11?.warn(`[${PLUGIN_NAME20}] 压缩异常(已隔离)`, {
17007
18750
  error: err instanceof Error ? err.message : String(err)
17008
18751
  });
17009
18752
  return null;
17010
18753
  }
17011
18754
  }
17012
- var log10 = makePluginLogger(PLUGIN_NAME19);
18755
+ var log11 = makePluginLogger(PLUGIN_NAME20);
17013
18756
  var tokenManagerServer = async (ctx) => {
17014
18757
  const rt = loadRuntimeSync();
17015
18758
  const threshold = rt.runtime.context.condenser_threshold_ratio;
17016
- logLifecycle(PLUGIN_NAME19, "activate", {
18759
+ logLifecycle(PLUGIN_NAME20, "activate", {
17017
18760
  directory: ctx.directory,
17018
18761
  threshold,
17019
18762
  target: DEFAULT_CONDENSE.target,
@@ -17022,7 +18765,7 @@ var tokenManagerServer = async (ctx) => {
17022
18765
  });
17023
18766
  return {
17024
18767
  "experimental.chat.messages.transform": async (_input, output) => {
17025
- await safeAsync(PLUGIN_NAME19, "experimental.chat.messages.transform", async () => {
18768
+ await safeAsync(PLUGIN_NAME20, "experimental.chat.messages.transform", async () => {
17026
18769
  const list = output.messages;
17027
18770
  if (!Array.isArray(list) || list.length === 0)
17028
18771
  return;
@@ -17032,10 +18775,10 @@ var tokenManagerServer = async (ctx) => {
17032
18775
  `);
17033
18776
  return { role, content };
17034
18777
  });
17035
- const r = await handleMessageBefore({ messages: flat }, log10, { threshold });
18778
+ const r = await handleMessageBefore({ messages: flat }, log11, { threshold });
17036
18779
  if (!r)
17037
18780
  return;
17038
- safeWriteLog(PLUGIN_NAME19, {
18781
+ safeWriteLog(PLUGIN_NAME20, {
17039
18782
  hook: "experimental.chat.messages.transform",
17040
18783
  mode: "observe-only",
17041
18784
  before_msgs: r.before.count,
@@ -17046,144 +18789,21 @@ var tokenManagerServer = async (ctx) => {
17046
18789
  reason: r.reason
17047
18790
  });
17048
18791
  if (r.compressed) {
17049
- 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)`);
18792
+ log11.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)`);
17050
18793
  }
17051
18794
  });
17052
18795
  }
17053
18796
  };
17054
18797
  };
17055
- var handler19 = tokenManagerServer;
18798
+ var handler20 = tokenManagerServer;
17056
18799
 
17057
18800
  // plugins/tool-policy.ts
17058
18801
  init_opencode_plugin_helpers();
17059
- import { promises as fs11 } from "node:fs";
17060
- import * as path15 from "node:path";
17061
-
17062
- // lib/autonomy.ts
17063
- var DEFAULT_POLICIES = {
17064
- step: {
17065
- bash: "confirm",
17066
- edit: "confirm",
17067
- webfetch: "confirm",
17068
- read: "confirm",
17069
- search: "confirm",
17070
- other: "confirm"
17071
- },
17072
- semi: {
17073
- bash: "confirm",
17074
- edit: "confirm",
17075
- webfetch: "confirm",
17076
- read: "auto",
17077
- search: "auto",
17078
- other: "auto"
17079
- },
17080
- full: {
17081
- bash: "auto",
17082
- edit: "auto",
17083
- webfetch: "auto",
17084
- read: "auto",
17085
- search: "auto",
17086
- other: "auto"
17087
- }
17088
- };
17089
- var RISK_PATTERNS = [
17090
- {
17091
- tag: "rm_rf_root",
17092
- kinds: ["bash"],
17093
- re: /rm\s+-[rRf]+[^|;`\n]*\s+(\/(?:\s|$|[^/\w*.-])|~|\$HOME(?:\b|\/)|\/(?:bin|etc|usr|var|opt|home|root|boot|lib|sbin)\b)/
17094
- },
17095
- {
17096
- tag: "rm_rf_wildcard",
17097
- kinds: ["bash"],
17098
- re: /rm\s+-[rRf]+\s+(?:\.\*|\*)/
17099
- },
17100
- {
17101
- tag: "force_push_protected",
17102
- kinds: ["bash"],
17103
- re: /git\s+push\s+(?:--force\b|-f\b)[\s\S]*\b(main|master|release(?:\/[\w.-]+)?)\b/
17104
- },
17105
- {
17106
- tag: "git_reset_hard_main",
17107
- kinds: ["bash"],
17108
- re: /git\s+reset\s+--hard\s+(origin\/)?(main|master)/
17109
- },
17110
- { tag: "sudo", kinds: ["bash"], re: /(?<![\w-])sudo(?![\w-])/ },
17111
- { tag: "su_root", kinds: ["bash"], re: /(?<![\w-])su\s+(?:-\s+)?root\b/ },
17112
- { tag: "mkfs", kinds: ["bash"], re: /\bmkfs(\.[\w]+)?\b/ },
17113
- { tag: "dd_disk", kinds: ["bash"], re: /\bdd\s+if=[^|;\s]+\s+of=\/dev\// },
17114
- { tag: "chmod_777", kinds: ["bash"], re: /chmod\s+[-+]?[0-7]?777/ },
17115
- {
17116
- tag: "curl_pipe_sh",
17117
- kinds: ["bash"],
17118
- re: /\b(?:curl|wget)\b[^|]*\|\s*(?:sh|bash|zsh)\b/
17119
- },
17120
- {
17121
- tag: "drop_database",
17122
- kinds: ["bash", "other"],
17123
- re: /\b(DROP\s+(DATABASE|TABLE)|TRUNCATE\s+TABLE|DROP\s+SCHEMA)\b/i
17124
- },
17125
- { tag: "write_secrets", re: /(\.env(?:\.\w+)?|id_[edr]sa|\.ssh\/id_|\.pem|\.p12|secret\.json)/i },
17126
- { tag: "write_etc", re: /(?:^|\s|"|')\/etc\// },
17127
- { tag: "write_usr", re: /(?:^|\s|"|')\/usr\// },
17128
- { tag: "write_root_home", re: /(?:^|\s|"|')(\/root|\/home\/root)\// },
17129
- {
17130
- tag: "internal_url",
17131
- kinds: ["webfetch"],
17132
- re: /https?:\/\/(localhost|127\.0\.0\.1|0\.0\.0\.0|169\.254\.|10\.|192\.168\.|172\.(?:1[6-9]|2\d|3[01])\.)/i
17133
- },
17134
- {
17135
- tag: "kubectl_delete",
17136
- kinds: ["bash"],
17137
- re: /kubectl\s+(?:delete|destroy)\s+/
17138
- },
17139
- {
17140
- tag: "terraform_destroy",
17141
- kinds: ["bash"],
17142
- re: /terraform\s+destroy/
17143
- }
17144
- ];
17145
- function evaluate2(mode, intent, opts = {}) {
17146
- const ignore = new Set(opts.ignore_risks ?? []);
17147
- const allRisks = [...RISK_PATTERNS, ...opts.extra_risks ?? []];
17148
- const haystack = buildHaystack(intent.args);
17149
- const hits = allRisks.filter((p) => !ignore.has(p.tag)).filter((p) => !p.kinds || p.kinds.includes(intent.kind)).filter((p) => p.re.test(haystack)).map((p) => p.tag);
17150
- let effective = mode;
17151
- let downgraded = false;
17152
- let reason;
17153
- if (hits.length > 0 && mode === "full") {
17154
- effective = "semi";
17155
- downgraded = true;
17156
- reason = `检测到风险操作 [${hits.join(", ")}],自动从 full 降级到 semi`;
17157
- } else if (hits.length > 0 && mode !== "step") {}
17158
- const policy = mergePolicy(effective, opts.policies);
17159
- const baseAction = policy[intent.kind] ?? "confirm";
17160
- const action = hits.length > 0 && effective !== "step" ? "confirm" : baseAction;
17161
- return {
17162
- effective_mode: effective,
17163
- action,
17164
- downgraded,
17165
- reason,
17166
- detected_risks: hits
17167
- };
17168
- }
17169
- function mergePolicy(mode, override) {
17170
- const base = DEFAULT_POLICIES[mode];
17171
- if (!override?.[mode])
17172
- return base;
17173
- return { ...base, ...override[mode] };
17174
- }
17175
- function buildHaystack(args) {
17176
- if (typeof args === "string")
17177
- return args;
17178
- try {
17179
- return JSON.stringify(args);
17180
- } catch {
17181
- return String(args);
17182
- }
17183
- }
18802
+ import { promises as fs13 } from "node:fs";
18803
+ import * as path17 from "node:path";
17184
18804
 
17185
18805
  // lib/file-regex-acl.ts
17186
- import * as path14 from "node:path";
18806
+ import * as path16 from "node:path";
17187
18807
  function compileRule(r) {
17188
18808
  if (r instanceof RegExp)
17189
18809
  return r;
@@ -17249,7 +18869,7 @@ function normalizePath2(p) {
17249
18869
  let s = p.replace(/\\/g, "/");
17250
18870
  if (s.startsWith("./"))
17251
18871
  s = s.slice(2);
17252
- s = path14.posix.normalize(s);
18872
+ s = path16.posix.normalize(s);
17253
18873
  return s;
17254
18874
  }
17255
18875
  function checkFileAccess(acl, file, op) {
@@ -17295,8 +18915,8 @@ function checkFileAccess(acl, file, op) {
17295
18915
  }
17296
18916
 
17297
18917
  // plugins/tool-policy.ts
17298
- var PLUGIN_NAME20 = "tool-policy";
17299
- logLifecycle(PLUGIN_NAME20, "import", {});
18918
+ var PLUGIN_NAME21 = "tool-policy";
18919
+ logLifecycle(PLUGIN_NAME21, "import", {});
17300
18920
  var EMPTY_ACL = { whitelistMode: false };
17301
18921
  var SUBAGENT_APPLY_DENY_LIST = new Set([
17302
18922
  "coder",
@@ -17319,7 +18939,7 @@ function decideToolCall(ctx, cfg = {}, currentAgent) {
17319
18939
  return {
17320
18940
  action: "deny",
17321
18941
  reasons: [
17322
- `[ADR-0061] subagent '${currentAgent}' 禁止调 pending_changes.${action2}` + ` — apply 必须由 codeforge orchestrator 或用户拍板`,
18942
+ `[ADR:prevent-subagent-self-apply] subagent '${currentAgent}' 禁止调 pending_changes.${action2}` + ` — apply 必须由 codeforge orchestrator 或用户拍板`,
17323
18943
  `替代路径:stage 完成后回报 codeforge,由其决定 apply 后通过 task_id 复用启动你跑测试`
17324
18944
  ],
17325
18945
  autonomy: {
@@ -17327,7 +18947,7 @@ function decideToolCall(ctx, cfg = {}, currentAgent) {
17327
18947
  action: "confirm",
17328
18948
  downgraded: false,
17329
18949
  detected_risks: ["subagent_apply_blocked"],
17330
- reason: `subagent '${currentAgent}' 自 apply 越权 (ADR-0061)`
18950
+ reason: `subagent '${currentAgent}' 自 apply 越权 (ADR:prevent-subagent-self-apply)`
17331
18951
  }
17332
18952
  };
17333
18953
  }
@@ -17359,11 +18979,11 @@ function decideToolCall(ctx, cfg = {}, currentAgent) {
17359
18979
  action = "deny";
17360
18980
  return { action, reasons, autonomy: a, acl: aclResults };
17361
18981
  }
17362
- var POLICY_PATH = path15.join(".codeforge", "policy.json");
18982
+ var POLICY_PATH = path17.join(".codeforge", "policy.json");
17363
18983
  async function loadPolicy(root = process.cwd()) {
17364
- const file = path15.join(root, POLICY_PATH);
18984
+ const file = path17.join(root, POLICY_PATH);
17365
18985
  try {
17366
- const raw = await fs11.readFile(file, "utf8");
18986
+ const raw = await fs13.readFile(file, "utf8");
17367
18987
  const data = JSON.parse(raw);
17368
18988
  return data;
17369
18989
  } catch {
@@ -17380,18 +19000,18 @@ function classifyToolKind(toolName) {
17380
19000
  return "webfetch";
17381
19001
  return "other";
17382
19002
  }
17383
- async function resolveCurrentAgent(client, sessionID, log11) {
19003
+ async function resolveCurrentAgent(client, sessionID, log12) {
17384
19004
  try {
17385
19005
  const sessionApi = client?.session;
17386
19006
  if (!sessionApi || typeof sessionApi.get !== "function") {
17387
- log11.warn(`client.session.get unavailable`, { sessionID });
19007
+ log12.warn(`client.session.get unavailable`, { sessionID });
17388
19008
  return;
17389
19009
  }
17390
19010
  const res = await sessionApi.get({ path: { id: sessionID } });
17391
19011
  const data = res?.data ?? res;
17392
19012
  const rawAgent = data?.agent;
17393
19013
  if (typeof rawAgent !== "string" || rawAgent === "") {
17394
- log11.warn(`client.session.get returned no string agent (保守放行)`, {
19014
+ log12.warn(`client.session.get returned no string agent (保守放行)`, {
17395
19015
  sessionID,
17396
19016
  dataKeys: data && typeof data === "object" ? Object.keys(data) : null
17397
19017
  });
@@ -17399,20 +19019,20 @@ async function resolveCurrentAgent(client, sessionID, log11) {
17399
19019
  }
17400
19020
  return rawAgent;
17401
19021
  } catch (err) {
17402
- log11.warn(`client.session.get failed (保守放行)`, {
19022
+ log12.warn(`client.session.get failed (保守放行)`, {
17403
19023
  sessionID,
17404
19024
  error: err instanceof Error ? err.message : String(err)
17405
19025
  });
17406
19026
  return;
17407
19027
  }
17408
19028
  }
17409
- var log11 = makePluginLogger(PLUGIN_NAME20);
19029
+ var log12 = makePluginLogger(PLUGIN_NAME21);
17410
19030
  var toolPolicyServer = async (ctx) => {
17411
19031
  const directory = ctx.directory ?? process.cwd();
17412
19032
  const cfg = await loadPolicy(directory);
17413
19033
  const rt = loadRuntimeSync();
17414
19034
  cfg.defaultMode = rt.runtime.autonomy.default_mode;
17415
- logLifecycle(PLUGIN_NAME20, "activate", {
19035
+ logLifecycle(PLUGIN_NAME21, "activate", {
17416
19036
  directory,
17417
19037
  acl_loaded: !!cfg.acl,
17418
19038
  default_mode: cfg.defaultMode,
@@ -17421,13 +19041,13 @@ var toolPolicyServer = async (ctx) => {
17421
19041
  return {
17422
19042
  "tool.execute.before": async (input, output) => {
17423
19043
  let denied;
17424
- await safeAsync(PLUGIN_NAME20, "tool.execute.before", async () => {
19044
+ await safeAsync(PLUGIN_NAME21, "tool.execute.before", async () => {
17425
19045
  const toolName = input.tool;
17426
19046
  const argsObj = output.args ?? {};
17427
19047
  const needsAgentDetection = toolName === "pending_changes" && (argsObj.action === "apply" || argsObj.action === "apply_all");
17428
19048
  let currentAgent = undefined;
17429
19049
  if (needsAgentDetection) {
17430
- currentAgent = await resolveCurrentAgent(ctx.client, input.sessionID, log11);
19050
+ currentAgent = await resolveCurrentAgent(ctx.client, input.sessionID, log12);
17431
19051
  }
17432
19052
  const files = [];
17433
19053
  const filePath = argsObj["path"] ?? argsObj["filePath"] ?? argsObj["file"];
@@ -17442,7 +19062,7 @@ var toolPolicyServer = async (ctx) => {
17442
19062
  mode: cfg.defaultMode,
17443
19063
  files: files.length ? files : undefined
17444
19064
  }, cfg, currentAgent);
17445
- safeWriteLog(PLUGIN_NAME20, {
19065
+ safeWriteLog(PLUGIN_NAME21, {
17446
19066
  hook: "tool.execute.before",
17447
19067
  tool: toolName,
17448
19068
  callID: input.callID,
@@ -17452,14 +19072,14 @@ var toolPolicyServer = async (ctx) => {
17452
19072
  currentAgent
17453
19073
  });
17454
19074
  if (decision.action === "deny") {
17455
- log11.warn(`[${PLUGIN_NAME20}] DENY ${toolName}`, {
19075
+ log12.warn(`[${PLUGIN_NAME21}] DENY ${toolName}`, {
17456
19076
  action: argsObj.action,
17457
19077
  currentAgent,
17458
19078
  reasons: decision.reasons
17459
19079
  });
17460
19080
  denied = new Error(`[tool-policy] DENIED: ${decision.reasons.join(" / ")}`);
17461
19081
  } else if (decision.action === "confirm") {
17462
- log11.info(`[${PLUGIN_NAME20}] CONFIRM ${toolName}: ${decision.reasons.join("; ")}`);
19082
+ log12.info(`[${PLUGIN_NAME21}] CONFIRM ${toolName}: ${decision.reasons.join("; ")}`);
17463
19083
  }
17464
19084
  });
17465
19085
  if (denied)
@@ -17467,13 +19087,13 @@ var toolPolicyServer = async (ctx) => {
17467
19087
  }
17468
19088
  };
17469
19089
  };
17470
- var handler20 = toolPolicyServer;
19090
+ var handler21 = toolPolicyServer;
17471
19091
 
17472
19092
  // plugins/update-checker.ts
17473
19093
  init_opencode_plugin_helpers();
17474
19094
  import { existsSync as existsSync5 } from "node:fs";
17475
- import { homedir as homedir7 } from "node:os";
17476
- import { join as join15 } from "node:path";
19095
+ import { homedir as homedir8 } from "node:os";
19096
+ import { join as join18 } from "node:path";
17477
19097
 
17478
19098
  // lib/update-checker-impl.ts
17479
19099
  import { createHash as createHash6 } from "node:crypto";
@@ -17482,15 +19102,15 @@ import {
17482
19102
  existsSync as existsSync4,
17483
19103
  mkdirSync as mkdirSync3,
17484
19104
  mkdtempSync,
17485
- readFileSync as readFileSync4,
17486
- readdirSync,
19105
+ readFileSync as readFileSync5,
19106
+ readdirSync as readdirSync2,
17487
19107
  renameSync,
17488
- statSync as statSync2,
19108
+ statSync as statSync4,
17489
19109
  unlinkSync,
17490
19110
  writeFileSync as writeFileSync2
17491
19111
  } from "node:fs";
17492
- import { homedir as homedir6, tmpdir } from "node:os";
17493
- import { dirname as dirname7, join as join14 } from "node:path";
19112
+ import { homedir as homedir7, tmpdir } from "node:os";
19113
+ import { dirname as dirname7, join as join17 } from "node:path";
17494
19114
  import { fileURLToPath } from "node:url";
17495
19115
  import * as https from "node:https";
17496
19116
  import * as zlib from "node:zlib";
@@ -17498,7 +19118,7 @@ import * as zlib from "node:zlib";
17498
19118
  // lib/version-injected.ts
17499
19119
  function getInjectedVersion() {
17500
19120
  try {
17501
- const v = "0.3.10";
19121
+ const v = "0.3.12";
17502
19122
  if (typeof v === "string" && /^\d+\.\d+\.\d+/.test(v)) {
17503
19123
  return v;
17504
19124
  }
@@ -17588,23 +19208,23 @@ function readLocalVersion() {
17588
19208
  try {
17589
19209
  const here = fileURLToPath(import.meta.url);
17590
19210
  const root = dirname7(dirname7(here));
17591
- const pkg = JSON.parse(readFileSync4(join14(root, "package.json"), "utf8"));
19211
+ const pkg = JSON.parse(readFileSync5(join17(root, "package.json"), "utf8"));
17592
19212
  return typeof pkg.version === "string" ? pkg.version : "0.0.0";
17593
19213
  } catch {
17594
19214
  return "0.0.0";
17595
19215
  }
17596
19216
  }
17597
19217
  function defaultCacheDir() {
17598
- return process.env["CODEFORGE_CACHE_DIR"] ?? join14(homedir6(), ".cache", "codeforge");
19218
+ return process.env["CODEFORGE_CACHE_DIR"] ?? join17(homedir7(), ".cache", "codeforge");
17599
19219
  }
17600
19220
  function defaultCacheFile() {
17601
- return join14(defaultCacheDir(), "update-check.json");
19221
+ return join17(defaultCacheDir(), "update-check.json");
17602
19222
  }
17603
19223
  function readCache(file) {
17604
19224
  try {
17605
19225
  if (!existsSync4(file))
17606
19226
  return null;
17607
- const raw = readFileSync4(file, "utf8");
19227
+ const raw = readFileSync5(file, "utf8");
17608
19228
  const obj = JSON.parse(raw);
17609
19229
  if (obj && typeof obj === "object" && typeof obj.checkedAt === "number" && typeof obj.remote === "string" && typeof obj.repo === "string") {
17610
19230
  return obj;
@@ -17635,7 +19255,7 @@ function fetchLatestTagFromGitHub(repo) {
17635
19255
  });
17636
19256
  }
17637
19257
  function getJsonWithRedirect(url, hopsLeft) {
17638
- return new Promise((resolve11, reject) => {
19258
+ return new Promise((resolve13, reject) => {
17639
19259
  const u = new URL(url);
17640
19260
  const headers = {
17641
19261
  "User-Agent": "codeforge-update-checker",
@@ -17659,12 +19279,12 @@ function getJsonWithRedirect(url, hopsLeft) {
17659
19279
  return;
17660
19280
  }
17661
19281
  const next = new URL(res.headers.location, url).toString();
17662
- getJsonWithRedirect(next, hopsLeft - 1).then(resolve11, reject);
19282
+ getJsonWithRedirect(next, hopsLeft - 1).then(resolve13, reject);
17663
19283
  return;
17664
19284
  }
17665
19285
  if (status === 404) {
17666
19286
  res.resume();
17667
- resolve11(null);
19287
+ resolve13(null);
17668
19288
  return;
17669
19289
  }
17670
19290
  if (status >= 400) {
@@ -17675,7 +19295,7 @@ function getJsonWithRedirect(url, hopsLeft) {
17675
19295
  let body = "";
17676
19296
  res.setEncoding("utf8");
17677
19297
  res.on("data", (chunk) => body += chunk);
17678
- res.on("end", () => resolve11(body));
19298
+ res.on("end", () => resolve13(body));
17679
19299
  });
17680
19300
  req.on("timeout", () => {
17681
19301
  req.destroy();
@@ -17715,7 +19335,7 @@ async function fetchLatestFromNpm(opts) {
17715
19335
  return { version, tarballUrl, integrity };
17716
19336
  }
17717
19337
  function defaultHttpFetcher(url, timeoutMs) {
17718
- return new Promise((resolve11, reject) => {
19338
+ return new Promise((resolve13, reject) => {
17719
19339
  const u = new URL(url);
17720
19340
  const headers = {
17721
19341
  "User-Agent": "codeforge-update-checker",
@@ -17732,7 +19352,7 @@ function defaultHttpFetcher(url, timeoutMs) {
17732
19352
  const status = res.statusCode ?? 0;
17733
19353
  if (status === 404) {
17734
19354
  res.resume();
17735
- resolve11(null);
19355
+ resolve13(null);
17736
19356
  return;
17737
19357
  }
17738
19358
  if (status >= 400) {
@@ -17743,7 +19363,7 @@ function defaultHttpFetcher(url, timeoutMs) {
17743
19363
  let body = "";
17744
19364
  res.setEncoding("utf8");
17745
19365
  res.on("data", (chunk) => body += chunk);
17746
- res.on("end", () => resolve11(body));
19366
+ res.on("end", () => resolve13(body));
17747
19367
  });
17748
19368
  req.on("timeout", () => {
17749
19369
  req.destroy();
@@ -17754,14 +19374,14 @@ function defaultHttpFetcher(url, timeoutMs) {
17754
19374
  });
17755
19375
  }
17756
19376
  async function downloadAndExtractBundle(opts) {
17757
- const tmpRoot = opts.tmpDir ?? mkdtempSync(join14(tmpdir(), "codeforge-update-"));
19377
+ const tmpRoot = opts.tmpDir ?? mkdtempSync(join17(tmpdir(), "codeforge-update-"));
17758
19378
  mkdirSync3(tmpRoot, { recursive: true });
17759
19379
  const fetcher = opts.tarballFetcher ?? defaultBinaryFetcher;
17760
19380
  const tarballBuf = await fetcher(opts.tarballUrl);
17761
19381
  verifyIntegrity(tarballBuf, opts.expectedIntegrity);
17762
19382
  const tarBuf = zlib.gunzipSync(tarballBuf);
17763
19383
  extractTarToDir(tarBuf, tmpRoot);
17764
- const bundlePath = join14(tmpRoot, "package", "dist", "index.js");
19384
+ const bundlePath = join17(tmpRoot, "package", "dist", "index.js");
17765
19385
  if (!existsSync4(bundlePath)) {
17766
19386
  throw new Error(`bundle_not_found: ${bundlePath}`);
17767
19387
  }
@@ -17801,11 +19421,11 @@ function extractTarToDir(tarBuf, destRoot) {
17801
19421
  offset += 512;
17802
19422
  if (typeFlag === "0" || typeFlag === "" || typeFlag === "\x00") {
17803
19423
  const fileBuf = tarBuf.subarray(offset, offset + size);
17804
- const dest = join14(destRoot, fullName);
19424
+ const dest = join17(destRoot, fullName);
17805
19425
  mkdirSync3(dirname7(dest), { recursive: true });
17806
19426
  writeFileSync2(dest, fileBuf);
17807
19427
  } else if (typeFlag === "5") {
17808
- mkdirSync3(join14(destRoot, fullName), { recursive: true });
19428
+ mkdirSync3(join17(destRoot, fullName), { recursive: true });
17809
19429
  }
17810
19430
  offset += Math.ceil(size / 512) * 512;
17811
19431
  }
@@ -17814,7 +19434,7 @@ function defaultBinaryFetcher(url) {
17814
19434
  return downloadBinary(url, 3);
17815
19435
  }
17816
19436
  function downloadBinary(url, hopsLeft) {
17817
- return new Promise((resolve11, reject) => {
19437
+ return new Promise((resolve13, reject) => {
17818
19438
  const u = new URL(url);
17819
19439
  const req = https.request({
17820
19440
  host: u.hostname,
@@ -17832,7 +19452,7 @@ function downloadBinary(url, hopsLeft) {
17832
19452
  return;
17833
19453
  }
17834
19454
  const next = new URL(res.headers.location, url).toString();
17835
- downloadBinary(next, hopsLeft - 1).then(resolve11, reject);
19455
+ downloadBinary(next, hopsLeft - 1).then(resolve13, reject);
17836
19456
  return;
17837
19457
  }
17838
19458
  if (status >= 400) {
@@ -17842,7 +19462,7 @@ function downloadBinary(url, hopsLeft) {
17842
19462
  }
17843
19463
  const chunks = [];
17844
19464
  res.on("data", (chunk) => chunks.push(chunk));
17845
- res.on("end", () => resolve11(Buffer.concat(chunks)));
19465
+ res.on("end", () => resolve13(Buffer.concat(chunks)));
17846
19466
  });
17847
19467
  req.on("timeout", () => {
17848
19468
  req.destroy();
@@ -17913,11 +19533,11 @@ function cleanupOldBackups(target, keep) {
17913
19533
  const dir = dirname7(target);
17914
19534
  const base = target.substring(dir.length + 1);
17915
19535
  const prefix = `${base}.bak.`;
17916
- const all = readdirSync(dir).filter((f) => f.startsWith(prefix)).map((f) => {
17917
- const full = join14(dir, f);
19536
+ const all = readdirSync2(dir).filter((f) => f.startsWith(prefix)).map((f) => {
19537
+ const full = join17(dir, f);
17918
19538
  let mtimeMs = 0;
17919
19539
  try {
17920
- mtimeMs = statSync2(full).mtimeMs;
19540
+ mtimeMs = statSync4(full).mtimeMs;
17921
19541
  } catch {}
17922
19542
  return { full, mtimeMs };
17923
19543
  }).sort((a, b) => b.mtimeMs - a.mtimeMs);
@@ -17936,11 +19556,11 @@ function loadCompatibility(opts) {
17936
19556
  const root = opts?.cwd ?? inferPluginRoot();
17937
19557
  if (!root)
17938
19558
  return null;
17939
- file = join14(root, "compatibility.json");
19559
+ file = join17(root, "compatibility.json");
17940
19560
  }
17941
19561
  if (!existsSync4(file))
17942
19562
  return null;
17943
- const raw = readFileSync4(file, "utf8");
19563
+ const raw = readFileSync5(file, "utf8");
17944
19564
  const obj = JSON.parse(raw);
17945
19565
  if (!obj || typeof obj !== "object")
17946
19566
  return null;
@@ -18001,20 +19621,20 @@ function compareOpencodeVersion(opts) {
18001
19621
  }
18002
19622
 
18003
19623
  // plugins/update-checker.ts
18004
- var PLUGIN_NAME21 = "update-checker";
19624
+ var PLUGIN_NAME22 = "update-checker";
18005
19625
  var PLUGIN_VERSION = "2.0.0";
18006
- logLifecycle(PLUGIN_NAME21, "import", { version: PLUGIN_VERSION });
19626
+ logLifecycle(PLUGIN_NAME22, "import", { version: PLUGIN_VERSION });
18007
19627
  var updateCheckerServer = async (ctx) => {
18008
19628
  const yieldResult = shouldYieldToLocalPlugin({ directory: ctx.directory });
18009
19629
  if (yieldResult.yield) {
18010
- safeWriteLog(PLUGIN_NAME21, {
19630
+ safeWriteLog(PLUGIN_NAME22, {
18011
19631
  level: "info",
18012
19632
  msg: "dev_mode_yield_skip",
18013
19633
  reason: yieldResult.reason,
18014
19634
  markerPath: yieldResult.markerPath,
18015
19635
  detail: formatYieldLog(yieldResult)
18016
19636
  });
18017
- logLifecycle(PLUGIN_NAME21, "activate", {
19637
+ logLifecycle(PLUGIN_NAME22, "activate", {
18018
19638
  yield_to_local: true,
18019
19639
  yield_reason: yieldResult.reason,
18020
19640
  skipped: "auto_install + background_check"
@@ -18023,7 +19643,7 @@ var updateCheckerServer = async (ctx) => {
18023
19643
  }
18024
19644
  const rt = loadRuntimeSync();
18025
19645
  const u = rt.runtime.update;
18026
- logLifecycle(PLUGIN_NAME21, "activate", {
19646
+ logLifecycle(PLUGIN_NAME22, "activate", {
18027
19647
  version: PLUGIN_VERSION,
18028
19648
  auto_check_enabled: u.auto_check_enabled,
18029
19649
  interval_hours: u.interval_hours,
@@ -18035,14 +19655,14 @@ var updateCheckerServer = async (ctx) => {
18035
19655
  repo_fallback: u.repo,
18036
19656
  config_source: "codeforge.json"
18037
19657
  });
18038
- await safeAsync(PLUGIN_NAME21, "opencode_version_check", async () => {
19658
+ await safeAsync(PLUGIN_NAME22, "opencode_version_check", async () => {
18039
19659
  const compat = loadCompatibility();
18040
19660
  const opencodeVer = detectOpencodeVersion();
18041
19661
  const verdict = compareOpencodeVersion({
18042
19662
  currentOpencodeVer: opencodeVer,
18043
19663
  compat
18044
19664
  });
18045
- safeWriteLog(PLUGIN_NAME21, {
19665
+ safeWriteLog(PLUGIN_NAME22, {
18046
19666
  level: "info",
18047
19667
  msg: "opencode_version_check",
18048
19668
  opencodeVer,
@@ -18059,14 +19679,14 @@ var updateCheckerServer = async (ctx) => {
18059
19679
  }
18060
19680
  });
18061
19681
  if (!u.auto_check_enabled) {
18062
- safeWriteLog(PLUGIN_NAME21, {
19682
+ safeWriteLog(PLUGIN_NAME22, {
18063
19683
  level: "info",
18064
19684
  msg: "auto-check disabled (codeforge.json update.auto_check_enabled=false)"
18065
19685
  });
18066
19686
  return {};
18067
19687
  }
18068
19688
  setImmediate(() => {
18069
- safeAsync(PLUGIN_NAME21, "checkAndMaybeUpdate", async () => {
19689
+ safeAsync(PLUGIN_NAME22, "checkAndMaybeUpdate", async () => {
18070
19690
  const local = readLocalVersion();
18071
19691
  let npmResult = null;
18072
19692
  try {
@@ -18077,7 +19697,7 @@ var updateCheckerServer = async (ctx) => {
18077
19697
  timeoutMs: 5000
18078
19698
  });
18079
19699
  } catch (e) {
18080
- safeWriteLog(PLUGIN_NAME21, {
19700
+ safeWriteLog(PLUGIN_NAME22, {
18081
19701
  level: "warn",
18082
19702
  msg: "npm_fetch_failed",
18083
19703
  error: e.message
@@ -18086,7 +19706,7 @@ var updateCheckerServer = async (ctx) => {
18086
19706
  return;
18087
19707
  }
18088
19708
  if (!npmResult) {
18089
- safeWriteLog(PLUGIN_NAME21, {
19709
+ safeWriteLog(PLUGIN_NAME22, {
18090
19710
  level: "info",
18091
19711
  msg: "npm_no_release",
18092
19712
  package: u.package,
@@ -18095,7 +19715,7 @@ var updateCheckerServer = async (ctx) => {
18095
19715
  return;
18096
19716
  }
18097
19717
  const hasUpdate = cmpVersion(local, npmResult.version) < 0;
18098
- safeWriteLog(PLUGIN_NAME21, {
19718
+ safeWriteLog(PLUGIN_NAME22, {
18099
19719
  level: "info",
18100
19720
  msg: "npm_check_result",
18101
19721
  local,
@@ -18110,10 +19730,10 @@ var updateCheckerServer = async (ctx) => {
18110
19730
  更新命令:npx ${u.package} install --global`);
18111
19731
  return;
18112
19732
  }
18113
- await safeAsync(PLUGIN_NAME21, "auto_install_bundle", async () => {
19733
+ await safeAsync(PLUGIN_NAME22, "auto_install_bundle", async () => {
18114
19734
  const target = getOpencodeBundlePath();
18115
19735
  if (!target) {
18116
- safeWriteLog(PLUGIN_NAME21, {
19736
+ safeWriteLog(PLUGIN_NAME22, {
18117
19737
  level: "warn",
18118
19738
  msg: "auto_install_skip",
18119
19739
  reason: "无法定位 opencode bundle 路径"
@@ -18132,7 +19752,7 @@ var updateCheckerServer = async (ctx) => {
18132
19752
  oldVersion: local,
18133
19753
  keepBackups: u.backup_keep
18134
19754
  });
18135
- safeWriteLog(PLUGIN_NAME21, {
19755
+ safeWriteLog(PLUGIN_NAME22, {
18136
19756
  level: "info",
18137
19757
  msg: "auto_install_success",
18138
19758
  local,
@@ -18156,14 +19776,14 @@ function detectOpencodeVersion() {
18156
19776
  }
18157
19777
  function getOpencodeBundlePath() {
18158
19778
  const candidates = [];
18159
- candidates.push(join15(homedir7(), ".config", "opencode", "codeforge", "index.js"));
19779
+ candidates.push(join18(homedir8(), ".config", "opencode", "codeforge", "index.js"));
18160
19780
  if (process.platform === "win32") {
18161
19781
  const appData = process.env["APPDATA"];
18162
19782
  if (appData)
18163
- candidates.push(join15(appData, "opencode", "codeforge", "index.js"));
19783
+ candidates.push(join18(appData, "opencode", "codeforge", "index.js"));
18164
19784
  const localAppData = process.env["LOCALAPPDATA"];
18165
19785
  if (localAppData)
18166
- candidates.push(join15(localAppData, "opencode", "codeforge", "index.js"));
19786
+ candidates.push(join18(localAppData, "opencode", "codeforge", "index.js"));
18167
19787
  }
18168
19788
  for (const c of candidates) {
18169
19789
  if (existsSync5(c))
@@ -18172,7 +19792,7 @@ function getOpencodeBundlePath() {
18172
19792
  return candidates[0] ?? null;
18173
19793
  }
18174
19794
  async function fallbackToGitHubReleases(ctx, u) {
18175
- await safeAsync(PLUGIN_NAME21, "github_fallback", async () => {
19795
+ await safeAsync(PLUGIN_NAME22, "github_fallback", async () => {
18176
19796
  const result = await checkUpdateOnce({
18177
19797
  repo: u.repo,
18178
19798
  intervalMs: u.interval_hours * 3600 * 1000
@@ -18182,7 +19802,7 @@ async function fallbackToGitHubReleases(ctx, u) {
18182
19802
  }
18183
19803
  async function reportLegacyResult(ctx, result, repo) {
18184
19804
  if (result.error && !result.fromCache) {
18185
- safeWriteLog(PLUGIN_NAME21, {
19805
+ safeWriteLog(PLUGIN_NAME22, {
18186
19806
  level: "warn",
18187
19807
  msg: `update check failed: ${result.error}`,
18188
19808
  ...result
@@ -18190,7 +19810,7 @@ async function reportLegacyResult(ctx, result, repo) {
18190
19810
  return;
18191
19811
  }
18192
19812
  if (!result.hasUpdate) {
18193
- safeWriteLog(PLUGIN_NAME21, {
19813
+ safeWriteLog(PLUGIN_NAME22, {
18194
19814
  level: "info",
18195
19815
  msg: `up-to-date (local=${result.local}, remote=${result.remote}${result.fromCache ? ", from_cache" : ""})`,
18196
19816
  ...result
@@ -18200,7 +19820,7 @@ async function reportLegacyResult(ctx, result, repo) {
18200
19820
  const updateCmd = `bunx --bun github:${repo} install`;
18201
19821
  const toast = `[CodeForge] 有新版本:${result.local} → ${result.remote}
18202
19822
  更新命令:${updateCmd}`;
18203
- safeWriteLog(PLUGIN_NAME21, {
19823
+ safeWriteLog(PLUGIN_NAME22, {
18204
19824
  level: "info",
18205
19825
  msg: "new_version_available_github_fallback",
18206
19826
  local: result.local,
@@ -18211,74 +19831,26 @@ async function reportLegacyResult(ctx, result, repo) {
18211
19831
  await postToast(ctx, toast);
18212
19832
  }
18213
19833
  async function postToast(ctx, message) {
18214
- await safeAsync(PLUGIN_NAME21, "client.app.log", async () => {
19834
+ await safeAsync(PLUGIN_NAME22, "client.app.log", async () => {
18215
19835
  await ctx.client.app.log({
18216
19836
  body: {
18217
- service: PLUGIN_NAME21,
19837
+ service: PLUGIN_NAME22,
18218
19838
  level: "info",
18219
19839
  message
18220
19840
  }
18221
19841
  });
18222
19842
  });
18223
19843
  }
18224
- var handler21 = updateCheckerServer;
19844
+ var handler22 = updateCheckerServer;
18225
19845
 
18226
19846
  // plugins/workflow-engine.ts
18227
19847
  init_opencode_plugin_helpers();
18228
- import * as path17 from "node:path";
19848
+ import * as path19 from "node:path";
18229
19849
 
18230
19850
  // lib/workflow-loader.ts
18231
- import { promises as fs12 } from "node:fs";
18232
- import * as path16 from "node:path";
19851
+ import { promises as fs14 } from "node:fs";
19852
+ import * as path18 from "node:path";
18233
19853
  import { z as z28 } from "zod";
18234
-
18235
- // node_modules/yaml/dist/index.js
18236
- var composer = require_composer();
18237
- var Document = require_Document();
18238
- var Schema = require_Schema();
18239
- var errors = require_errors();
18240
- var Alias = require_Alias();
18241
- var identity = require_identity();
18242
- var Pair = require_Pair();
18243
- var Scalar = require_Scalar();
18244
- var YAMLMap = require_YAMLMap();
18245
- var YAMLSeq = require_YAMLSeq();
18246
- var cst = require_cst();
18247
- var lexer = require_lexer();
18248
- var lineCounter = require_line_counter();
18249
- var parser = require_parser();
18250
- var publicApi = require_public_api();
18251
- var visit = require_visit();
18252
- var $Composer = composer.Composer;
18253
- var $Document = Document.Document;
18254
- var $Schema = Schema.Schema;
18255
- var $YAMLError = errors.YAMLError;
18256
- var $YAMLParseError = errors.YAMLParseError;
18257
- var $YAMLWarning = errors.YAMLWarning;
18258
- var $Alias = Alias.Alias;
18259
- var $isAlias = identity.isAlias;
18260
- var $isCollection = identity.isCollection;
18261
- var $isDocument = identity.isDocument;
18262
- var $isMap = identity.isMap;
18263
- var $isNode = identity.isNode;
18264
- var $isPair = identity.isPair;
18265
- var $isScalar = identity.isScalar;
18266
- var $isSeq = identity.isSeq;
18267
- var $Pair = Pair.Pair;
18268
- var $Scalar = Scalar.Scalar;
18269
- var $YAMLMap = YAMLMap.YAMLMap;
18270
- var $YAMLSeq = YAMLSeq.YAMLSeq;
18271
- var $Lexer = lexer.Lexer;
18272
- var $LineCounter = lineCounter.LineCounter;
18273
- var $Parser = parser.Parser;
18274
- var $parse = publicApi.parse;
18275
- var $parseAllDocuments = publicApi.parseAllDocuments;
18276
- var $parseDocument = publicApi.parseDocument;
18277
- var $stringify = publicApi.stringify;
18278
- var $visit = visit.visit;
18279
- var $visitAsync = visit.visitAsync;
18280
-
18281
- // lib/workflow-loader.ts
18282
19854
  var ActionSchema = z28.object({
18283
19855
  tool: z28.string().min(1, "action.tool 不能为空"),
18284
19856
  args: z28.record(z28.string(), z28.unknown()).optional().default({}),
@@ -18363,7 +19935,7 @@ function parseWorkflowYaml(yaml, sourcePath = "<inline>") {
18363
19935
  async function loadWorkflowFromFile(filePath) {
18364
19936
  let txt;
18365
19937
  try {
18366
- txt = await fs12.readFile(filePath, "utf8");
19938
+ txt = await fs14.readFile(filePath, "utf8");
18367
19939
  } catch (err) {
18368
19940
  return {
18369
19941
  ok: false,
@@ -18378,7 +19950,7 @@ async function loadWorkflowsFromDir(dir) {
18378
19950
  const failed = [];
18379
19951
  let entries;
18380
19952
  try {
18381
- entries = await fs12.readdir(dir);
19953
+ entries = await fs14.readdir(dir);
18382
19954
  } catch (err) {
18383
19955
  const e = err;
18384
19956
  if (e.code === "ENOENT")
@@ -18390,7 +19962,7 @@ async function loadWorkflowsFromDir(dir) {
18390
19962
  continue;
18391
19963
  if (!/\.ya?ml$/i.test(name))
18392
19964
  continue;
18393
- const full = path16.join(dir, name);
19965
+ const full = path18.join(dir, name);
18394
19966
  const r = await loadWorkflowFromFile(full);
18395
19967
  if (r.ok)
18396
19968
  loaded.push(r);
@@ -18593,7 +20165,7 @@ async function run(workflow, opts = {}) {
18593
20165
  } else {
18594
20166
  const output = await adapter.getLastAgentOutput();
18595
20167
  if (output === null || output === "") {
18596
- adapter.log("warn", `[${step.name}] getLastAgentOutput 返回空 → 降级 abort (ADR-0036)`);
20168
+ adapter.log("warn", `[${step.name}] getLastAgentOutput 返回空 → 降级 abort (ADR:on-decision-empty-output-abort)`);
18597
20169
  const last = results[results.length - 1];
18598
20170
  results[results.length - 1] = {
18599
20171
  ...last,
@@ -18725,9 +20297,9 @@ async function runStepAutoFeedback(step, adapter) {
18725
20297
  }
18726
20298
 
18727
20299
  // plugins/workflow-engine.ts
18728
- var PLUGIN_NAME22 = "workflow-engine";
18729
- logLifecycle(PLUGIN_NAME22, "import", {});
18730
- var fallbackLog2 = makePluginLogger(PLUGIN_NAME22);
20300
+ var PLUGIN_NAME23 = "workflow-engine";
20301
+ logLifecycle(PLUGIN_NAME23, "import", {});
20302
+ var fallbackLog2 = makePluginLogger(PLUGIN_NAME23);
18731
20303
  var _registry = null;
18732
20304
  async function loadRegistry(workflowsDir) {
18733
20305
  const { loaded, failed } = await loadWorkflowsFromDir(workflowsDir);
@@ -18744,35 +20316,35 @@ async function ensureRegistry(workflowsDir = "workflows") {
18744
20316
  }
18745
20317
  async function handleCommandInvoked(raw, workflowsDir = "workflows") {
18746
20318
  const ctx = raw ?? {};
18747
- const log12 = ctx.log ?? fallbackLog2;
20319
+ const log13 = ctx.log ?? fallbackLog2;
18748
20320
  const command = typeof ctx.command === "string" ? ctx.command : null;
18749
20321
  if (!command) {
18750
- log12.warn(`[${PLUGIN_NAME22}] command.invoked 缺 command 字段`, ctx);
20322
+ log13.warn(`[${PLUGIN_NAME23}] command.invoked 缺 command 字段`, ctx);
18751
20323
  return null;
18752
20324
  }
18753
20325
  const reg = await ensureRegistry(workflowsDir);
18754
20326
  if (reg.errors.length) {
18755
- log12.warn(`[${PLUGIN_NAME22}] 有 ${reg.errors.length} 个 workflow 加载失败`, reg.errors);
20327
+ log13.warn(`[${PLUGIN_NAME23}] 有 ${reg.errors.length} 个 workflow 加载失败`, reg.errors);
18756
20328
  }
18757
20329
  const wf = reg.workflows.find((w) => matchesTrigger(w, command));
18758
20330
  if (!wf) {
18759
- log12.info(`[${PLUGIN_NAME22}] no workflow matches "${command}"`);
20331
+ log13.info(`[${PLUGIN_NAME23}] no workflow matches "${command}"`);
18760
20332
  return null;
18761
20333
  }
18762
- log12.info(`[${PLUGIN_NAME22}] dispatch "${command}" → workflow "${wf.name}"`);
20334
+ log13.info(`[${PLUGIN_NAME23}] dispatch "${command}" → workflow "${wf.name}"`);
18763
20335
  try {
18764
20336
  const result = await run(wf, {
18765
20337
  mode: ctx.adapter ? "real" : "dry_run",
18766
20338
  autonomy: ctx.autonomy ?? "semi",
18767
20339
  adapter: ctx.adapter
18768
20340
  });
18769
- log12.info(`[${PLUGIN_NAME22}] workflow "${wf.name}" 完成 (${result.plan.mode})`, {
20341
+ log13.info(`[${PLUGIN_NAME23}] workflow "${wf.name}" 完成 (${result.plan.mode})`, {
18770
20342
  steps: result.plan.steps.length,
18771
20343
  results: result.results.length
18772
20344
  });
18773
20345
  return result;
18774
20346
  } catch (err) {
18775
- log12.error(`[${PLUGIN_NAME22}] workflow "${wf.name}" 执行失败`, {
20347
+ log13.error(`[${PLUGIN_NAME23}] workflow "${wf.name}" 执行失败`, {
18776
20348
  error: err instanceof Error ? err.message : String(err)
18777
20349
  });
18778
20350
  throw err;
@@ -18780,16 +20352,16 @@ async function handleCommandInvoked(raw, workflowsDir = "workflows") {
18780
20352
  }
18781
20353
  var workflowEngineServer = async (ctx) => {
18782
20354
  const directory = ctx.directory ?? process.cwd();
18783
- const workflowsDir = path17.join(directory, "workflows");
18784
- ensureRegistry(workflowsDir).catch((err) => fallbackLog2.warn(`[${PLUGIN_NAME22}] preload workflows failed`, {
20355
+ const workflowsDir = path19.join(directory, "workflows");
20356
+ ensureRegistry(workflowsDir).catch((err) => fallbackLog2.warn(`[${PLUGIN_NAME23}] preload workflows failed`, {
18785
20357
  error: err instanceof Error ? err.message : String(err)
18786
20358
  }));
18787
- logLifecycle(PLUGIN_NAME22, "activate", { directory, workflowsDir });
20359
+ logLifecycle(PLUGIN_NAME23, "activate", { directory, workflowsDir });
18788
20360
  return {
18789
20361
  "command.execute.before": async (input, output) => {
18790
- await safeAsync(PLUGIN_NAME22, "command.execute.before", async () => {
20362
+ await safeAsync(PLUGIN_NAME23, "command.execute.before", async () => {
18791
20363
  const cmd = input.command.startsWith("/") ? input.command : `/${input.command}`;
18792
- safeWriteLog(PLUGIN_NAME22, {
20364
+ safeWriteLog(PLUGIN_NAME23, {
18793
20365
  hook: "command.execute.before",
18794
20366
  command: cmd,
18795
20367
  sessionID: input.sessionID
@@ -18804,14 +20376,14 @@ var workflowEngineServer = async (ctx) => {
18804
20376
  });
18805
20377
  },
18806
20378
  "chat.message": async (input, output) => {
18807
- await safeAsync(PLUGIN_NAME22, "chat.message", async () => {
20379
+ await safeAsync(PLUGIN_NAME23, "chat.message", async () => {
18808
20380
  const text = extractUserText(output).trim();
18809
20381
  if (!text.startsWith("/"))
18810
20382
  return;
18811
20383
  const cmd = text.split(/\s+/)[0];
18812
20384
  if (!cmd)
18813
20385
  return;
18814
- safeWriteLog(PLUGIN_NAME22, {
20386
+ safeWriteLog(PLUGIN_NAME23, {
18815
20387
  hook: "chat.message",
18816
20388
  command: cmd,
18817
20389
  sessionID: input.sessionID
@@ -18821,11 +20393,11 @@ var workflowEngineServer = async (ctx) => {
18821
20393
  }
18822
20394
  };
18823
20395
  };
18824
- var handler22 = workflowEngineServer;
20396
+ var handler23 = workflowEngineServer;
18825
20397
 
18826
20398
  // src/index.ts
18827
20399
  var PLUGIN_ID = "codeforge";
18828
- var log12 = makePluginLogger(PLUGIN_ID);
20400
+ var log13 = makePluginLogger(PLUGIN_ID);
18829
20401
  logLifecycle(PLUGIN_ID, "import", { entry: "src/index.ts" });
18830
20402
  var HANDLERS = [
18831
20403
  { name: "agent-router", init: handler },
@@ -18839,17 +20411,18 @@ var HANDLERS = [
18839
20411
  { name: "kh-reminder", init: handler10 },
18840
20412
  { name: "memories-context", init: handler11 },
18841
20413
  { name: "model-fallback", init: handler12 },
18842
- { name: "pwsh-utf8", init: handler15 },
18843
- { name: "session-recovery", init: handler16 },
20414
+ { name: "parallel-tool-nudge", init: handler15 },
20415
+ { name: "pwsh-utf8", init: handler16 },
20416
+ { name: "session-recovery", init: handler17 },
18844
20417
  { name: "subtask-heartbeat", init: handler13 },
18845
- { name: "subtasks", init: handler17 },
20418
+ { name: "subtasks", init: handler18 },
18846
20419
  { name: "parallel-status", init: handler14 },
18847
- { name: "terminal-monitor", init: handler18 },
18848
- { name: "token-manager", init: handler19 },
20420
+ { name: "terminal-monitor", init: handler19 },
20421
+ { name: "token-manager", init: handler20 },
18849
20422
  { name: "tool-heartbeat", init: handler7 },
18850
- { name: "tool-policy", init: handler20 },
18851
- { name: "update-checker", init: handler21 },
18852
- { name: "workflow-engine", init: handler22 }
20423
+ { name: "tool-policy", init: handler21 },
20424
+ { name: "update-checker", init: handler22 },
20425
+ { name: "workflow-engine", init: handler23 }
18853
20426
  ];
18854
20427
  function makeSerialHook(hookName, fns) {
18855
20428
  return async (input, output) => {
@@ -18857,7 +20430,7 @@ function makeSerialHook(hookName, fns) {
18857
20430
  try {
18858
20431
  await fn(input, output);
18859
20432
  } catch (err) {
18860
- log12.warn(`[${PLUGIN_ID}] ${hookName} handler 异常(已隔离)`, {
20433
+ log13.warn(`[${PLUGIN_ID}] ${hookName} handler 异常(已隔离)`, {
18861
20434
  error: err instanceof Error ? err.message : String(err)
18862
20435
  });
18863
20436
  }
@@ -18870,7 +20443,7 @@ function createCodeforgeServer(opts) {
18870
20443
  const yieldResult = shouldYieldToLocalPlugin({ directory: input.directory });
18871
20444
  if (yieldResult.yield) {
18872
20445
  const msg = formatYieldLog(yieldResult);
18873
- log12.info(msg, { reason: yieldResult.reason, markerPath: yieldResult.markerPath });
20446
+ log13.info(msg, { reason: yieldResult.reason, markerPath: yieldResult.markerPath });
18874
20447
  logLifecycle(PLUGIN_ID, "activate", {
18875
20448
  yield_to_local: true,
18876
20449
  yield_reason: yieldResult.reason,
@@ -18888,7 +20461,7 @@ function createCodeforgeServer(opts) {
18888
20461
  if (r.status === "fulfilled" && r.value && typeof r.value === "object") {
18889
20462
  hooksList.push(r.value);
18890
20463
  } else if (r.status === "rejected") {
18891
- log12.warn(`[${PLUGIN_ID}] handler ${HANDLERS[i].name} init failed (隔离,其他 handler 继续)`, { error: r.reason instanceof Error ? r.reason.message : String(r.reason) });
20464
+ log13.warn(`[${PLUGIN_ID}] handler ${HANDLERS[i].name} init failed (隔离,其他 handler 继续)`, { error: r.reason instanceof Error ? r.reason.message : String(r.reason) });
18892
20465
  }
18893
20466
  });
18894
20467
  logLifecycle(PLUGIN_ID, "activate", {
@@ -18902,6 +20475,7 @@ function createCodeforgeServer(opts) {
18902
20475
  const chatParamsBucket = [];
18903
20476
  const toolExecuteBeforeBucket = [];
18904
20477
  const chatMessagesTransformBucket = [];
20478
+ const chatSystemTransformBucket = [];
18905
20479
  const eventBucket = [];
18906
20480
  const toolMerged = {};
18907
20481
  for (const h of hooksList) {
@@ -18916,6 +20490,9 @@ function createCodeforgeServer(opts) {
18916
20490
  if (h["experimental.chat.messages.transform"]) {
18917
20491
  chatMessagesTransformBucket.push(h["experimental.chat.messages.transform"]);
18918
20492
  }
20493
+ if (h["experimental.chat.system.transform"]) {
20494
+ chatSystemTransformBucket.push(h["experimental.chat.system.transform"]);
20495
+ }
18919
20496
  if (h.event)
18920
20497
  eventBucket.push(h.event);
18921
20498
  if (h.tool)
@@ -18927,12 +20504,13 @@ function createCodeforgeServer(opts) {
18927
20504
  "chat.params": makeSerialHook("chat.params", chatParamsBucket),
18928
20505
  "tool.execute.before": makeSerialHook("tool.execute.before", toolExecuteBeforeBucket),
18929
20506
  "experimental.chat.messages.transform": makeSerialHook("experimental.chat.messages.transform", chatMessagesTransformBucket),
20507
+ "experimental.chat.system.transform": makeSerialHook("experimental.chat.system.transform", chatSystemTransformBucket),
18930
20508
  event: async (envelope) => {
18931
20509
  await Promise.all(eventBucket.map(async (fn) => {
18932
20510
  try {
18933
20511
  await fn(envelope);
18934
20512
  } catch (err) {
18935
- log12.warn(`[${PLUGIN_ID}] event handler 异常(已隔离)`, {
20513
+ log13.warn(`[${PLUGIN_ID}] event handler 异常(已隔离)`, {
18936
20514
  error: err instanceof Error ? err.message : String(err)
18937
20515
  });
18938
20516
  }