@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/README.md +150 -5
- package/agents/codeforge.md +40 -5
- package/agents/coder-deep.md +96 -0
- package/agents/coder-quick.md +95 -0
- package/agents/coder.md +8 -4
- package/agents/planner.md +13 -8
- package/agents/reviewer.md +4 -0
- package/assets/adr-init/.github/pull_request_template.md +22 -0
- package/assets/adr-init/docs/adr/README.md +105 -0
- package/assets/adr-init/docs/adr/template.md +83 -0
- package/assets/adr-init/githooks/pre-commit.sh +46 -0
- package/assets/adr-init/githooks/pre-push.sh +23 -0
- package/assets/adr-init/scripts/adr-check.mjs +428 -0
- package/assets/adr-init/scripts/adr-index-sync.mjs +151 -0
- package/bin/codeforge.mjs +96 -3
- package/codeforge.json +28 -2
- package/commands/adr-init.md +67 -0
- package/commands/deep.md +87 -0
- package/commands/quick.md +92 -0
- package/dist/adr-init.js +207 -0
- package/dist/index.js +2232 -654
- package/install.sh +8 -0
- package/package.json +16 -5
- package/schemas/codeforge.schema.json +230 -224
- package/scripts/sync-agent-models.mjs +22 -3
- package/workflows/bugfix.yaml +3 -3
- package/workflows/feature-dev.yaml +3 -3
- package/workflows/parallel-explore.yaml +1 -1
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,
|
|
1038
|
-
const ctrl = callVisitor(key, node, visitor,
|
|
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,
|
|
1041
|
-
return visit_(key, ctrl, visitor,
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
1059
|
-
const ck = visit_("key", node.key, visitor,
|
|
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,
|
|
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,
|
|
1086
|
-
const ctrl = await callVisitor(key, node, visitor,
|
|
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,
|
|
1089
|
-
return visitAsync_(key, ctrl, visitor,
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
1107
|
-
const ck = await visitAsync_("key", node.key, visitor,
|
|
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,
|
|
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,
|
|
1139
|
+
function callVisitor(key, node, visitor, path13) {
|
|
1140
1140
|
if (typeof visitor === "function")
|
|
1141
|
-
return visitor(key, node,
|
|
1141
|
+
return visitor(key, node, path13);
|
|
1142
1142
|
if (identity.isMap(node))
|
|
1143
|
-
return visitor.Map?.(key, node,
|
|
1143
|
+
return visitor.Map?.(key, node, path13);
|
|
1144
1144
|
if (identity.isSeq(node))
|
|
1145
|
-
return visitor.Seq?.(key, node,
|
|
1145
|
+
return visitor.Seq?.(key, node, path13);
|
|
1146
1146
|
if (identity.isPair(node))
|
|
1147
|
-
return visitor.Pair?.(key, node,
|
|
1147
|
+
return visitor.Pair?.(key, node, path13);
|
|
1148
1148
|
if (identity.isScalar(node))
|
|
1149
|
-
return visitor.Scalar?.(key, node,
|
|
1149
|
+
return visitor.Scalar?.(key, node, path13);
|
|
1150
1150
|
if (identity.isAlias(node))
|
|
1151
|
-
return visitor.Alias?.(key, node,
|
|
1151
|
+
return visitor.Alias?.(key, node, path13);
|
|
1152
1152
|
return;
|
|
1153
1153
|
}
|
|
1154
|
-
function replaceNode(key,
|
|
1155
|
-
const parent =
|
|
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,
|
|
1714
|
+
function collectionFromPath(schema, path13, value) {
|
|
1715
1715
|
let v = value;
|
|
1716
|
-
for (let i =
|
|
1717
|
-
const k =
|
|
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 = (
|
|
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(
|
|
1758
|
-
if (isEmptyPath(
|
|
1757
|
+
addIn(path13, value) {
|
|
1758
|
+
if (isEmptyPath(path13))
|
|
1759
1759
|
this.add(value);
|
|
1760
1760
|
else {
|
|
1761
|
-
const [key, ...rest] =
|
|
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(
|
|
1772
|
-
const [key, ...rest] =
|
|
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(
|
|
1782
|
-
const [key, ...rest] =
|
|
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(
|
|
1798
|
-
const [key, ...rest] =
|
|
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(
|
|
1805
|
-
const [key, ...rest] =
|
|
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
|
|
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
|
-
|
|
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(
|
|
4198
|
+
addIn(path13, value) {
|
|
4199
4199
|
if (assertCollection(this.contents))
|
|
4200
|
-
this.contents.addIn(
|
|
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(
|
|
4250
|
-
if (Collection.isEmptyPath(
|
|
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(
|
|
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(
|
|
4262
|
-
if (Collection.isEmptyPath(
|
|
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(
|
|
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(
|
|
4270
|
-
if (Collection.isEmptyPath(
|
|
4269
|
+
hasIn(path13) {
|
|
4270
|
+
if (Collection.isEmptyPath(path13))
|
|
4271
4271
|
return this.contents !== undefined;
|
|
4272
|
-
return identity.isCollection(this.contents) ? this.contents.hasIn(
|
|
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(
|
|
4282
|
-
if (Collection.isEmptyPath(
|
|
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(
|
|
4285
|
+
this.contents = Collection.collectionFromPath(this.schema, Array.from(path13), value);
|
|
4286
4286
|
} else if (assertCollection(this.contents)) {
|
|
4287
|
-
this.contents.setIn(
|
|
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,
|
|
6182
|
+
visit.itemAtPath = (cst, path13) => {
|
|
6183
6183
|
let item = cst;
|
|
6184
|
-
for (const [field, index] of
|
|
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,
|
|
6194
|
-
const parent = visit.itemAtPath(cst,
|
|
6195
|
-
const field =
|
|
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(
|
|
6202
|
-
let ctrl = visitor(item,
|
|
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(
|
|
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,
|
|
6220
|
+
ctrl = ctrl(item, path13);
|
|
6221
6221
|
}
|
|
6222
6222
|
}
|
|
6223
|
-
return typeof ctrl === "function" ? ctrl(item,
|
|
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
|
|
7492
|
+
const fs10 = this.flowScalar(this.type);
|
|
7493
7493
|
if (atNextItem || it.value) {
|
|
7494
|
-
map.items.push({ start, key:
|
|
7494
|
+
map.items.push({ start, key: fs10, sep: [] });
|
|
7495
7495
|
this.onKeyLine = true;
|
|
7496
7496
|
} else if (it.sep) {
|
|
7497
|
-
this.stack.push(
|
|
7497
|
+
this.stack.push(fs10);
|
|
7498
7498
|
} else {
|
|
7499
|
-
Object.assign(it, { key:
|
|
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
|
|
7627
|
+
const fs10 = this.flowScalar(this.type);
|
|
7628
7628
|
if (!it || it.value)
|
|
7629
|
-
fc.items.push({ start: [], key:
|
|
7629
|
+
fc.items.push({ start: [], key: fs10, sep: [] });
|
|
7630
7630
|
else if (it.sep)
|
|
7631
|
-
this.stack.push(
|
|
7631
|
+
this.stack.push(fs10);
|
|
7632
7632
|
else
|
|
7633
|
-
Object.assign(it, { key:
|
|
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
|
|
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) =>
|
|
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((
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
9545
|
-
|
|
9546
|
-
|
|
9547
|
-
|
|
9548
|
-
|
|
9549
|
-
|
|
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
|
-
|
|
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-
|
|
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-
|
|
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-
|
|
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
|
|
14370
|
-
import * as
|
|
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 ??
|
|
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
|
|
14866
|
+
return path12.join(cfg.projectRoot, ".codeforge", "memories.json");
|
|
14386
14867
|
}
|
|
14387
|
-
return
|
|
14868
|
+
return path12.join(cfg.homeDir, ".codeforge", "memories.json");
|
|
14388
14869
|
}
|
|
14389
14870
|
async function readBank(p) {
|
|
14390
14871
|
try {
|
|
14391
|
-
const raw = await
|
|
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
|
|
14882
|
+
await fs9.mkdir(path12.dirname(p), { recursive: true });
|
|
14402
14883
|
const tmp = `${p}.tmp`;
|
|
14403
|
-
await
|
|
14404
|
-
await
|
|
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
|
|
15006
|
-
if (!
|
|
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
|
|
15012
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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/
|
|
15393
|
-
|
|
15394
|
-
|
|
15395
|
-
|
|
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
|
-
//
|
|
15442
|
-
|
|
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
|
|
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
|
|
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
|
|
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 =
|
|
16239
|
+
const file = path13.join(dir, e.name);
|
|
15465
16240
|
const id = e.name.replace(/\.jsonl$/, "");
|
|
15466
16241
|
try {
|
|
15467
|
-
const stat = await
|
|
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 =
|
|
15492
|
-
return opts.sessions_dir ?
|
|
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
|
|
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
|
|
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
|
|
15759
|
-
logLifecycle(
|
|
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?.(`[${
|
|
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
|
|
15777
|
-
|
|
15778
|
-
|
|
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
|
-
|
|
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
|
|
15790
|
-
const
|
|
15791
|
-
|
|
15792
|
-
|
|
15793
|
-
|
|
15794
|
-
|
|
15795
|
-
|
|
15796
|
-
|
|
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 (
|
|
15799
|
-
|
|
16890
|
+
if (status.stdout.trim().length === 0) {
|
|
16891
|
+
return { committed: false };
|
|
15800
16892
|
}
|
|
15801
|
-
|
|
15802
|
-
|
|
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 (!
|
|
15805
|
-
|
|
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
|
-
|
|
15813
|
-
|
|
15814
|
-
|
|
15815
|
-
|
|
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
|
-
|
|
15818
|
-
|
|
15819
|
-
|
|
15820
|
-
|
|
15821
|
-
|
|
15822
|
-
|
|
15823
|
-
|
|
15824
|
-
|
|
15825
|
-
|
|
15826
|
-
|
|
15827
|
-
|
|
15828
|
-
|
|
15829
|
-
|
|
15830
|
-
|
|
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
|
-
|
|
15834
|
-
|
|
15835
|
-
|
|
15836
|
-
|
|
15837
|
-
|
|
15838
|
-
|
|
15839
|
-
|
|
15840
|
-
|
|
15841
|
-
|
|
15842
|
-
|
|
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
|
-
|
|
15849
|
-
|
|
15850
|
-
|
|
15851
|
-
|
|
15852
|
-
|
|
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
|
|
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
|
-
|
|
17250
|
+
log10("warn", `[parallel] onSubtaskFinish 抛错(已隔离)`, {
|
|
15885
17251
|
id: res.id,
|
|
15886
|
-
error:
|
|
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 分配失败:${
|
|
17266
|
+
summary: clamp(`worktree 分配失败:${describe5(err)}`, limit),
|
|
15901
17267
|
status: "failed",
|
|
15902
17268
|
duration_ms: now() - subStart,
|
|
15903
|
-
error:
|
|
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
|
-
|
|
17280
|
+
log10("warn", `[parallel] onSubtaskStart 抛错(已隔离)`, {
|
|
15915
17281
|
id: spec.id,
|
|
15916
|
-
error:
|
|
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 抛错:${
|
|
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:
|
|
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
|
-
|
|
15966
|
-
error:
|
|
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
|
|
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
|
|
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 失败:${
|
|
17506
|
+
summary: `session.create 失败:${describe6(created.error) || "no id"}`,
|
|
16132
17507
|
status: "failed",
|
|
16133
|
-
error:
|
|
17508
|
+
error: describe6(created.error) || "session.create 无 id"
|
|
16134
17509
|
};
|
|
16135
17510
|
}
|
|
16136
17511
|
childSessionId = created.data.id;
|
|
16137
|
-
|
|
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 失败:${
|
|
17542
|
+
summary: `session.prompt 失败:${describe6(r.error)}`,
|
|
16168
17543
|
status: "failed",
|
|
16169
|
-
error:
|
|
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
|
-
|
|
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 ?
|
|
17574
|
+
error: llmError ? describe6(llmError) : undefined
|
|
16200
17575
|
};
|
|
16201
17576
|
} catch (err) {
|
|
16202
17577
|
return {
|
|
16203
17578
|
ok: false,
|
|
16204
|
-
summary: `runner 抛错:${
|
|
17579
|
+
summary: `runner 抛错:${describe6(err)}`,
|
|
16205
17580
|
status: "failed",
|
|
16206
|
-
error:
|
|
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
|
-
|
|
16217
|
-
error:
|
|
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((
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
17705
|
+
const log10 = opts.log ?? (() => {});
|
|
16331
17706
|
if (!client?.session) {
|
|
16332
|
-
|
|
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
|
-
|
|
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
|
-
|
|
17733
|
+
log10("warn", "[sendParentNotice] promptAsync 返回 error", { error: res.error });
|
|
16359
17734
|
return false;
|
|
16360
17735
|
}
|
|
16361
17736
|
return true;
|
|
16362
17737
|
} catch (err) {
|
|
16363
|
-
|
|
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
|
|
17975
|
+
var PLUGIN_NAME18 = "subtasks";
|
|
16374
17976
|
function getLogFile(root = process.cwd()) {
|
|
16375
|
-
return
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
18138
|
+
log10?.error(msg, data);
|
|
16472
18139
|
else if (lvl === "warn")
|
|
16473
|
-
|
|
18140
|
+
log10?.warn(msg, data);
|
|
16474
18141
|
else
|
|
16475
|
-
|
|
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
|
-
|
|
18186
|
+
log10?.error(msg, data);
|
|
16501
18187
|
else if (lvl === "warn")
|
|
16502
|
-
|
|
18188
|
+
log10?.warn(msg, data);
|
|
16503
18189
|
else
|
|
16504
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
18275
|
+
plugin: PLUGIN_NAME18,
|
|
16570
18276
|
msg,
|
|
16571
18277
|
data
|
|
16572
18278
|
}) + `
|
|
16573
18279
|
`;
|
|
16574
18280
|
try {
|
|
16575
18281
|
const logFile = getLogFile();
|
|
16576
|
-
await
|
|
16577
|
-
await
|
|
18282
|
+
await fs12.mkdir(path15.dirname(logFile), { recursive: true });
|
|
18283
|
+
await fs12.appendFile(logFile, line, "utf8");
|
|
16578
18284
|
} catch {}
|
|
16579
18285
|
}
|
|
16580
|
-
logLifecycle(
|
|
18286
|
+
logLifecycle(PLUGIN_NAME18, "import");
|
|
16581
18287
|
var subtasksServer = async (ctx) => {
|
|
16582
|
-
const
|
|
18288
|
+
const log10 = makePluginLogger(PLUGIN_NAME18);
|
|
16583
18289
|
const client = ctx?.client ?? undefined;
|
|
16584
|
-
logLifecycle(
|
|
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:
|
|
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
|
-
|
|
18357
|
+
log10.error(msg, data);
|
|
16616
18358
|
else if (lvl === "warn")
|
|
16617
|
-
|
|
18359
|
+
log10.warn(msg, data);
|
|
16618
18360
|
else
|
|
16619
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
16657
|
-
logLifecycle(
|
|
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 =
|
|
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
|
|
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(`\`${
|
|
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}: ${
|
|
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
|
|
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(
|
|
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(
|
|
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:
|
|
18701
|
+
log: log10,
|
|
16959
18702
|
notifyAgent: (msg) => {
|
|
16960
18703
|
_lastNotification = msg;
|
|
16961
18704
|
}
|
|
16962
18705
|
});
|
|
16963
|
-
safeWriteLog(
|
|
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
|
-
|
|
18714
|
+
log10.info(`[${PLUGIN_NAME19}] 反馈 ${r.notification.findings.length} 条 → ${r.notification.summary}`);
|
|
16972
18715
|
}
|
|
16973
18716
|
});
|
|
16974
18717
|
}
|
|
16975
18718
|
};
|
|
16976
18719
|
};
|
|
16977
|
-
var
|
|
18720
|
+
var handler19 = terminalMonitorServer;
|
|
16978
18721
|
|
|
16979
18722
|
// plugins/token-manager.ts
|
|
16980
18723
|
init_opencode_plugin_helpers();
|
|
16981
|
-
var
|
|
16982
|
-
logLifecycle(
|
|
16983
|
-
async function handleMessageBefore(raw,
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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(
|
|
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(
|
|
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 },
|
|
18778
|
+
const r = await handleMessageBefore({ messages: flat }, log11, { threshold });
|
|
17036
18779
|
if (!r)
|
|
17037
18780
|
return;
|
|
17038
|
-
safeWriteLog(
|
|
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
|
-
|
|
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
|
|
18798
|
+
var handler20 = tokenManagerServer;
|
|
17056
18799
|
|
|
17057
18800
|
// plugins/tool-policy.ts
|
|
17058
18801
|
init_opencode_plugin_helpers();
|
|
17059
|
-
import { promises as
|
|
17060
|
-
import * as
|
|
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
|
|
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 =
|
|
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
|
|
17299
|
-
logLifecycle(
|
|
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-
|
|
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-
|
|
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 =
|
|
18982
|
+
var POLICY_PATH = path17.join(".codeforge", "policy.json");
|
|
17363
18983
|
async function loadPolicy(root = process.cwd()) {
|
|
17364
|
-
const file =
|
|
18984
|
+
const file = path17.join(root, POLICY_PATH);
|
|
17365
18985
|
try {
|
|
17366
|
-
const raw = await
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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(
|
|
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(
|
|
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,
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
17476
|
-
import { join as
|
|
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
|
|
17486
|
-
readdirSync,
|
|
19105
|
+
readFileSync as readFileSync5,
|
|
19106
|
+
readdirSync as readdirSync2,
|
|
17487
19107
|
renameSync,
|
|
17488
|
-
statSync as
|
|
19108
|
+
statSync as statSync4,
|
|
17489
19109
|
unlinkSync,
|
|
17490
19110
|
writeFileSync as writeFileSync2
|
|
17491
19111
|
} from "node:fs";
|
|
17492
|
-
import { homedir as
|
|
17493
|
-
import { dirname as dirname7, join as
|
|
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.
|
|
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(
|
|
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"] ??
|
|
19218
|
+
return process.env["CODEFORGE_CACHE_DIR"] ?? join17(homedir7(), ".cache", "codeforge");
|
|
17599
19219
|
}
|
|
17600
19220
|
function defaultCacheFile() {
|
|
17601
|
-
return
|
|
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 =
|
|
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((
|
|
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(
|
|
19282
|
+
getJsonWithRedirect(next, hopsLeft - 1).then(resolve13, reject);
|
|
17663
19283
|
return;
|
|
17664
19284
|
}
|
|
17665
19285
|
if (status === 404) {
|
|
17666
19286
|
res.resume();
|
|
17667
|
-
|
|
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", () =>
|
|
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((
|
|
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
|
-
|
|
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", () =>
|
|
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(
|
|
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 =
|
|
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 =
|
|
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(
|
|
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((
|
|
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(
|
|
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", () =>
|
|
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 =
|
|
17917
|
-
const full =
|
|
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 =
|
|
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 =
|
|
19559
|
+
file = join17(root, "compatibility.json");
|
|
17940
19560
|
}
|
|
17941
19561
|
if (!existsSync4(file))
|
|
17942
19562
|
return null;
|
|
17943
|
-
const raw =
|
|
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
|
|
19624
|
+
var PLUGIN_NAME22 = "update-checker";
|
|
18005
19625
|
var PLUGIN_VERSION = "2.0.0";
|
|
18006
|
-
logLifecycle(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
19733
|
+
await safeAsync(PLUGIN_NAME22, "auto_install_bundle", async () => {
|
|
18114
19734
|
const target = getOpencodeBundlePath();
|
|
18115
19735
|
if (!target) {
|
|
18116
|
-
safeWriteLog(
|
|
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(
|
|
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(
|
|
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(
|
|
19783
|
+
candidates.push(join18(appData, "opencode", "codeforge", "index.js"));
|
|
18164
19784
|
const localAppData = process.env["LOCALAPPDATA"];
|
|
18165
19785
|
if (localAppData)
|
|
18166
|
-
candidates.push(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
19834
|
+
await safeAsync(PLUGIN_NAME22, "client.app.log", async () => {
|
|
18215
19835
|
await ctx.client.app.log({
|
|
18216
19836
|
body: {
|
|
18217
|
-
service:
|
|
19837
|
+
service: PLUGIN_NAME22,
|
|
18218
19838
|
level: "info",
|
|
18219
19839
|
message
|
|
18220
19840
|
}
|
|
18221
19841
|
});
|
|
18222
19842
|
});
|
|
18223
19843
|
}
|
|
18224
|
-
var
|
|
19844
|
+
var handler22 = updateCheckerServer;
|
|
18225
19845
|
|
|
18226
19846
|
// plugins/workflow-engine.ts
|
|
18227
19847
|
init_opencode_plugin_helpers();
|
|
18228
|
-
import * as
|
|
19848
|
+
import * as path19 from "node:path";
|
|
18229
19849
|
|
|
18230
19850
|
// lib/workflow-loader.ts
|
|
18231
|
-
import { promises as
|
|
18232
|
-
import * as
|
|
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
|
|
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
|
|
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 =
|
|
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-
|
|
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
|
|
18729
|
-
logLifecycle(
|
|
18730
|
-
var fallbackLog2 = makePluginLogger(
|
|
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
|
|
20319
|
+
const log13 = ctx.log ?? fallbackLog2;
|
|
18748
20320
|
const command = typeof ctx.command === "string" ? ctx.command : null;
|
|
18749
20321
|
if (!command) {
|
|
18750
|
-
|
|
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
|
-
|
|
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
|
-
|
|
20331
|
+
log13.info(`[${PLUGIN_NAME23}] no workflow matches "${command}"`);
|
|
18760
20332
|
return null;
|
|
18761
20333
|
}
|
|
18762
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
18784
|
-
ensureRegistry(workflowsDir).catch((err) => fallbackLog2.warn(`[${
|
|
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(
|
|
20359
|
+
logLifecycle(PLUGIN_NAME23, "activate", { directory, workflowsDir });
|
|
18788
20360
|
return {
|
|
18789
20361
|
"command.execute.before": async (input, output) => {
|
|
18790
|
-
await safeAsync(
|
|
20362
|
+
await safeAsync(PLUGIN_NAME23, "command.execute.before", async () => {
|
|
18791
20363
|
const cmd = input.command.startsWith("/") ? input.command : `/${input.command}`;
|
|
18792
|
-
safeWriteLog(
|
|
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(
|
|
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(
|
|
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
|
|
20396
|
+
var handler23 = workflowEngineServer;
|
|
18825
20397
|
|
|
18826
20398
|
// src/index.ts
|
|
18827
20399
|
var PLUGIN_ID = "codeforge";
|
|
18828
|
-
var
|
|
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: "
|
|
18843
|
-
{ name: "
|
|
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:
|
|
20418
|
+
{ name: "subtasks", init: handler18 },
|
|
18846
20419
|
{ name: "parallel-status", init: handler14 },
|
|
18847
|
-
{ name: "terminal-monitor", init:
|
|
18848
|
-
{ name: "token-manager", init:
|
|
20420
|
+
{ name: "terminal-monitor", init: handler19 },
|
|
20421
|
+
{ name: "token-manager", init: handler20 },
|
|
18849
20422
|
{ name: "tool-heartbeat", init: handler7 },
|
|
18850
|
-
{ name: "tool-policy", init:
|
|
18851
|
-
{ name: "update-checker", init:
|
|
18852
|
-
{ name: "workflow-engine", init:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
20513
|
+
log13.warn(`[${PLUGIN_ID}] event handler 异常(已隔离)`, {
|
|
18936
20514
|
error: err instanceof Error ? err.message : String(err)
|
|
18937
20515
|
});
|
|
18938
20516
|
}
|