@andyqiu/codeforge 0.8.11 → 0.8.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 +1 -1
- package/agents/codeforge.md +2 -2
- package/agents/reviewer-lite.md +11 -0
- package/agents/reviewer.md +11 -0
- package/dist/index.js +1087 -383
- package/package.json +1 -1
- /package/workflows/{parallel-explore.yaml → _parallel-explore.yaml} +0 -0
package/dist/index.js
CHANGED
|
@@ -372,17 +372,17 @@ var require_visit = __commonJS((exports) => {
|
|
|
372
372
|
visit.BREAK = BREAK;
|
|
373
373
|
visit.SKIP = SKIP;
|
|
374
374
|
visit.REMOVE = REMOVE;
|
|
375
|
-
function visit_(key, node, visitor,
|
|
376
|
-
const ctrl = callVisitor(key, node, visitor,
|
|
375
|
+
function visit_(key, node, visitor, path22) {
|
|
376
|
+
const ctrl = callVisitor(key, node, visitor, path22);
|
|
377
377
|
if (identity.isNode(ctrl) || identity.isPair(ctrl)) {
|
|
378
|
-
replaceNode(key,
|
|
379
|
-
return visit_(key, ctrl, visitor,
|
|
378
|
+
replaceNode(key, path22, ctrl);
|
|
379
|
+
return visit_(key, ctrl, visitor, path22);
|
|
380
380
|
}
|
|
381
381
|
if (typeof ctrl !== "symbol") {
|
|
382
382
|
if (identity.isCollection(node)) {
|
|
383
|
-
|
|
383
|
+
path22 = Object.freeze(path22.concat(node));
|
|
384
384
|
for (let i = 0;i < node.items.length; ++i) {
|
|
385
|
-
const ci = visit_(i, node.items[i], visitor,
|
|
385
|
+
const ci = visit_(i, node.items[i], visitor, path22);
|
|
386
386
|
if (typeof ci === "number")
|
|
387
387
|
i = ci - 1;
|
|
388
388
|
else if (ci === BREAK)
|
|
@@ -393,13 +393,13 @@ var require_visit = __commonJS((exports) => {
|
|
|
393
393
|
}
|
|
394
394
|
}
|
|
395
395
|
} else if (identity.isPair(node)) {
|
|
396
|
-
|
|
397
|
-
const ck = visit_("key", node.key, visitor,
|
|
396
|
+
path22 = Object.freeze(path22.concat(node));
|
|
397
|
+
const ck = visit_("key", node.key, visitor, path22);
|
|
398
398
|
if (ck === BREAK)
|
|
399
399
|
return BREAK;
|
|
400
400
|
else if (ck === REMOVE)
|
|
401
401
|
node.key = null;
|
|
402
|
-
const cv = visit_("value", node.value, visitor,
|
|
402
|
+
const cv = visit_("value", node.value, visitor, path22);
|
|
403
403
|
if (cv === BREAK)
|
|
404
404
|
return BREAK;
|
|
405
405
|
else if (cv === REMOVE)
|
|
@@ -420,17 +420,17 @@ var require_visit = __commonJS((exports) => {
|
|
|
420
420
|
visitAsync.BREAK = BREAK;
|
|
421
421
|
visitAsync.SKIP = SKIP;
|
|
422
422
|
visitAsync.REMOVE = REMOVE;
|
|
423
|
-
async function visitAsync_(key, node, visitor,
|
|
424
|
-
const ctrl = await callVisitor(key, node, visitor,
|
|
423
|
+
async function visitAsync_(key, node, visitor, path22) {
|
|
424
|
+
const ctrl = await callVisitor(key, node, visitor, path22);
|
|
425
425
|
if (identity.isNode(ctrl) || identity.isPair(ctrl)) {
|
|
426
|
-
replaceNode(key,
|
|
427
|
-
return visitAsync_(key, ctrl, visitor,
|
|
426
|
+
replaceNode(key, path22, ctrl);
|
|
427
|
+
return visitAsync_(key, ctrl, visitor, path22);
|
|
428
428
|
}
|
|
429
429
|
if (typeof ctrl !== "symbol") {
|
|
430
430
|
if (identity.isCollection(node)) {
|
|
431
|
-
|
|
431
|
+
path22 = Object.freeze(path22.concat(node));
|
|
432
432
|
for (let i = 0;i < node.items.length; ++i) {
|
|
433
|
-
const ci = await visitAsync_(i, node.items[i], visitor,
|
|
433
|
+
const ci = await visitAsync_(i, node.items[i], visitor, path22);
|
|
434
434
|
if (typeof ci === "number")
|
|
435
435
|
i = ci - 1;
|
|
436
436
|
else if (ci === BREAK)
|
|
@@ -441,13 +441,13 @@ var require_visit = __commonJS((exports) => {
|
|
|
441
441
|
}
|
|
442
442
|
}
|
|
443
443
|
} else if (identity.isPair(node)) {
|
|
444
|
-
|
|
445
|
-
const ck = await visitAsync_("key", node.key, visitor,
|
|
444
|
+
path22 = Object.freeze(path22.concat(node));
|
|
445
|
+
const ck = await visitAsync_("key", node.key, visitor, path22);
|
|
446
446
|
if (ck === BREAK)
|
|
447
447
|
return BREAK;
|
|
448
448
|
else if (ck === REMOVE)
|
|
449
449
|
node.key = null;
|
|
450
|
-
const cv = await visitAsync_("value", node.value, visitor,
|
|
450
|
+
const cv = await visitAsync_("value", node.value, visitor, path22);
|
|
451
451
|
if (cv === BREAK)
|
|
452
452
|
return BREAK;
|
|
453
453
|
else if (cv === REMOVE)
|
|
@@ -474,23 +474,23 @@ var require_visit = __commonJS((exports) => {
|
|
|
474
474
|
}
|
|
475
475
|
return visitor;
|
|
476
476
|
}
|
|
477
|
-
function callVisitor(key, node, visitor,
|
|
477
|
+
function callVisitor(key, node, visitor, path22) {
|
|
478
478
|
if (typeof visitor === "function")
|
|
479
|
-
return visitor(key, node,
|
|
479
|
+
return visitor(key, node, path22);
|
|
480
480
|
if (identity.isMap(node))
|
|
481
|
-
return visitor.Map?.(key, node,
|
|
481
|
+
return visitor.Map?.(key, node, path22);
|
|
482
482
|
if (identity.isSeq(node))
|
|
483
|
-
return visitor.Seq?.(key, node,
|
|
483
|
+
return visitor.Seq?.(key, node, path22);
|
|
484
484
|
if (identity.isPair(node))
|
|
485
|
-
return visitor.Pair?.(key, node,
|
|
485
|
+
return visitor.Pair?.(key, node, path22);
|
|
486
486
|
if (identity.isScalar(node))
|
|
487
|
-
return visitor.Scalar?.(key, node,
|
|
487
|
+
return visitor.Scalar?.(key, node, path22);
|
|
488
488
|
if (identity.isAlias(node))
|
|
489
|
-
return visitor.Alias?.(key, node,
|
|
489
|
+
return visitor.Alias?.(key, node, path22);
|
|
490
490
|
return;
|
|
491
491
|
}
|
|
492
|
-
function replaceNode(key,
|
|
493
|
-
const parent =
|
|
492
|
+
function replaceNode(key, path22, node) {
|
|
493
|
+
const parent = path22[path22.length - 1];
|
|
494
494
|
if (identity.isCollection(parent)) {
|
|
495
495
|
parent.items[key] = node;
|
|
496
496
|
} else if (identity.isPair(parent)) {
|
|
@@ -1049,10 +1049,10 @@ var require_Collection = __commonJS((exports) => {
|
|
|
1049
1049
|
var createNode = require_createNode();
|
|
1050
1050
|
var identity = require_identity();
|
|
1051
1051
|
var Node = require_Node();
|
|
1052
|
-
function collectionFromPath(schema,
|
|
1052
|
+
function collectionFromPath(schema, path22, value) {
|
|
1053
1053
|
let v = value;
|
|
1054
|
-
for (let i =
|
|
1055
|
-
const k =
|
|
1054
|
+
for (let i = path22.length - 1;i >= 0; --i) {
|
|
1055
|
+
const k = path22[i];
|
|
1056
1056
|
if (typeof k === "number" && Number.isInteger(k) && k >= 0) {
|
|
1057
1057
|
const a = [];
|
|
1058
1058
|
a[k] = v;
|
|
@@ -1071,7 +1071,7 @@ var require_Collection = __commonJS((exports) => {
|
|
|
1071
1071
|
sourceObjects: new Map
|
|
1072
1072
|
});
|
|
1073
1073
|
}
|
|
1074
|
-
var isEmptyPath = (
|
|
1074
|
+
var isEmptyPath = (path22) => path22 == null || typeof path22 === "object" && !!path22[Symbol.iterator]().next().done;
|
|
1075
1075
|
|
|
1076
1076
|
class Collection extends Node.NodeBase {
|
|
1077
1077
|
constructor(type, schema) {
|
|
@@ -1092,11 +1092,11 @@ var require_Collection = __commonJS((exports) => {
|
|
|
1092
1092
|
copy.range = this.range.slice();
|
|
1093
1093
|
return copy;
|
|
1094
1094
|
}
|
|
1095
|
-
addIn(
|
|
1096
|
-
if (isEmptyPath(
|
|
1095
|
+
addIn(path22, value) {
|
|
1096
|
+
if (isEmptyPath(path22))
|
|
1097
1097
|
this.add(value);
|
|
1098
1098
|
else {
|
|
1099
|
-
const [key, ...rest] =
|
|
1099
|
+
const [key, ...rest] = path22;
|
|
1100
1100
|
const node = this.get(key, true);
|
|
1101
1101
|
if (identity.isCollection(node))
|
|
1102
1102
|
node.addIn(rest, value);
|
|
@@ -1106,8 +1106,8 @@ var require_Collection = __commonJS((exports) => {
|
|
|
1106
1106
|
throw new Error(`Expected YAML collection at ${key}. Remaining path: ${rest}`);
|
|
1107
1107
|
}
|
|
1108
1108
|
}
|
|
1109
|
-
deleteIn(
|
|
1110
|
-
const [key, ...rest] =
|
|
1109
|
+
deleteIn(path22) {
|
|
1110
|
+
const [key, ...rest] = path22;
|
|
1111
1111
|
if (rest.length === 0)
|
|
1112
1112
|
return this.delete(key);
|
|
1113
1113
|
const node = this.get(key, true);
|
|
@@ -1116,8 +1116,8 @@ var require_Collection = __commonJS((exports) => {
|
|
|
1116
1116
|
else
|
|
1117
1117
|
throw new Error(`Expected YAML collection at ${key}. Remaining path: ${rest}`);
|
|
1118
1118
|
}
|
|
1119
|
-
getIn(
|
|
1120
|
-
const [key, ...rest] =
|
|
1119
|
+
getIn(path22, keepScalar) {
|
|
1120
|
+
const [key, ...rest] = path22;
|
|
1121
1121
|
const node = this.get(key, true);
|
|
1122
1122
|
if (rest.length === 0)
|
|
1123
1123
|
return !keepScalar && identity.isScalar(node) ? node.value : node;
|
|
@@ -1132,15 +1132,15 @@ var require_Collection = __commonJS((exports) => {
|
|
|
1132
1132
|
return n == null || allowScalar && identity.isScalar(n) && n.value == null && !n.commentBefore && !n.comment && !n.tag;
|
|
1133
1133
|
});
|
|
1134
1134
|
}
|
|
1135
|
-
hasIn(
|
|
1136
|
-
const [key, ...rest] =
|
|
1135
|
+
hasIn(path22) {
|
|
1136
|
+
const [key, ...rest] = path22;
|
|
1137
1137
|
if (rest.length === 0)
|
|
1138
1138
|
return this.has(key);
|
|
1139
1139
|
const node = this.get(key, true);
|
|
1140
1140
|
return identity.isCollection(node) ? node.hasIn(rest) : false;
|
|
1141
1141
|
}
|
|
1142
|
-
setIn(
|
|
1143
|
-
const [key, ...rest] =
|
|
1142
|
+
setIn(path22, value) {
|
|
1143
|
+
const [key, ...rest] = path22;
|
|
1144
1144
|
if (rest.length === 0) {
|
|
1145
1145
|
this.set(key, value);
|
|
1146
1146
|
} else {
|
|
@@ -3533,9 +3533,9 @@ var require_Document = __commonJS((exports) => {
|
|
|
3533
3533
|
if (assertCollection(this.contents))
|
|
3534
3534
|
this.contents.add(value);
|
|
3535
3535
|
}
|
|
3536
|
-
addIn(
|
|
3536
|
+
addIn(path22, value) {
|
|
3537
3537
|
if (assertCollection(this.contents))
|
|
3538
|
-
this.contents.addIn(
|
|
3538
|
+
this.contents.addIn(path22, value);
|
|
3539
3539
|
}
|
|
3540
3540
|
createAlias(node, name) {
|
|
3541
3541
|
if (!node.anchor) {
|
|
@@ -3584,30 +3584,30 @@ var require_Document = __commonJS((exports) => {
|
|
|
3584
3584
|
delete(key) {
|
|
3585
3585
|
return assertCollection(this.contents) ? this.contents.delete(key) : false;
|
|
3586
3586
|
}
|
|
3587
|
-
deleteIn(
|
|
3588
|
-
if (Collection.isEmptyPath(
|
|
3587
|
+
deleteIn(path22) {
|
|
3588
|
+
if (Collection.isEmptyPath(path22)) {
|
|
3589
3589
|
if (this.contents == null)
|
|
3590
3590
|
return false;
|
|
3591
3591
|
this.contents = null;
|
|
3592
3592
|
return true;
|
|
3593
3593
|
}
|
|
3594
|
-
return assertCollection(this.contents) ? this.contents.deleteIn(
|
|
3594
|
+
return assertCollection(this.contents) ? this.contents.deleteIn(path22) : false;
|
|
3595
3595
|
}
|
|
3596
3596
|
get(key, keepScalar) {
|
|
3597
3597
|
return identity.isCollection(this.contents) ? this.contents.get(key, keepScalar) : undefined;
|
|
3598
3598
|
}
|
|
3599
|
-
getIn(
|
|
3600
|
-
if (Collection.isEmptyPath(
|
|
3599
|
+
getIn(path22, keepScalar) {
|
|
3600
|
+
if (Collection.isEmptyPath(path22))
|
|
3601
3601
|
return !keepScalar && identity.isScalar(this.contents) ? this.contents.value : this.contents;
|
|
3602
|
-
return identity.isCollection(this.contents) ? this.contents.getIn(
|
|
3602
|
+
return identity.isCollection(this.contents) ? this.contents.getIn(path22, keepScalar) : undefined;
|
|
3603
3603
|
}
|
|
3604
3604
|
has(key) {
|
|
3605
3605
|
return identity.isCollection(this.contents) ? this.contents.has(key) : false;
|
|
3606
3606
|
}
|
|
3607
|
-
hasIn(
|
|
3608
|
-
if (Collection.isEmptyPath(
|
|
3607
|
+
hasIn(path22) {
|
|
3608
|
+
if (Collection.isEmptyPath(path22))
|
|
3609
3609
|
return this.contents !== undefined;
|
|
3610
|
-
return identity.isCollection(this.contents) ? this.contents.hasIn(
|
|
3610
|
+
return identity.isCollection(this.contents) ? this.contents.hasIn(path22) : false;
|
|
3611
3611
|
}
|
|
3612
3612
|
set(key, value) {
|
|
3613
3613
|
if (this.contents == null) {
|
|
@@ -3616,13 +3616,13 @@ var require_Document = __commonJS((exports) => {
|
|
|
3616
3616
|
this.contents.set(key, value);
|
|
3617
3617
|
}
|
|
3618
3618
|
}
|
|
3619
|
-
setIn(
|
|
3620
|
-
if (Collection.isEmptyPath(
|
|
3619
|
+
setIn(path22, value) {
|
|
3620
|
+
if (Collection.isEmptyPath(path22)) {
|
|
3621
3621
|
this.contents = value;
|
|
3622
3622
|
} else if (this.contents == null) {
|
|
3623
|
-
this.contents = Collection.collectionFromPath(this.schema, Array.from(
|
|
3623
|
+
this.contents = Collection.collectionFromPath(this.schema, Array.from(path22), value);
|
|
3624
3624
|
} else if (assertCollection(this.contents)) {
|
|
3625
|
-
this.contents.setIn(
|
|
3625
|
+
this.contents.setIn(path22, value);
|
|
3626
3626
|
}
|
|
3627
3627
|
}
|
|
3628
3628
|
setSchema(version2, options = {}) {
|
|
@@ -5517,9 +5517,9 @@ var require_cst_visit = __commonJS((exports) => {
|
|
|
5517
5517
|
visit.BREAK = BREAK;
|
|
5518
5518
|
visit.SKIP = SKIP;
|
|
5519
5519
|
visit.REMOVE = REMOVE;
|
|
5520
|
-
visit.itemAtPath = (cst,
|
|
5520
|
+
visit.itemAtPath = (cst, path22) => {
|
|
5521
5521
|
let item = cst;
|
|
5522
|
-
for (const [field, index] of
|
|
5522
|
+
for (const [field, index] of path22) {
|
|
5523
5523
|
const tok = item?.[field];
|
|
5524
5524
|
if (tok && "items" in tok) {
|
|
5525
5525
|
item = tok.items[index];
|
|
@@ -5528,23 +5528,23 @@ var require_cst_visit = __commonJS((exports) => {
|
|
|
5528
5528
|
}
|
|
5529
5529
|
return item;
|
|
5530
5530
|
};
|
|
5531
|
-
visit.parentCollection = (cst,
|
|
5532
|
-
const parent = visit.itemAtPath(cst,
|
|
5533
|
-
const field =
|
|
5531
|
+
visit.parentCollection = (cst, path22) => {
|
|
5532
|
+
const parent = visit.itemAtPath(cst, path22.slice(0, -1));
|
|
5533
|
+
const field = path22[path22.length - 1][0];
|
|
5534
5534
|
const coll = parent?.[field];
|
|
5535
5535
|
if (coll && "items" in coll)
|
|
5536
5536
|
return coll;
|
|
5537
5537
|
throw new Error("Parent collection not found");
|
|
5538
5538
|
};
|
|
5539
|
-
function _visit(
|
|
5540
|
-
let ctrl = visitor(item,
|
|
5539
|
+
function _visit(path22, item, visitor) {
|
|
5540
|
+
let ctrl = visitor(item, path22);
|
|
5541
5541
|
if (typeof ctrl === "symbol")
|
|
5542
5542
|
return ctrl;
|
|
5543
5543
|
for (const field of ["key", "value"]) {
|
|
5544
5544
|
const token = item[field];
|
|
5545
5545
|
if (token && "items" in token) {
|
|
5546
5546
|
for (let i = 0;i < token.items.length; ++i) {
|
|
5547
|
-
const ci = _visit(Object.freeze(
|
|
5547
|
+
const ci = _visit(Object.freeze(path22.concat([[field, i]])), token.items[i], visitor);
|
|
5548
5548
|
if (typeof ci === "number")
|
|
5549
5549
|
i = ci - 1;
|
|
5550
5550
|
else if (ci === BREAK)
|
|
@@ -5555,10 +5555,10 @@ var require_cst_visit = __commonJS((exports) => {
|
|
|
5555
5555
|
}
|
|
5556
5556
|
}
|
|
5557
5557
|
if (typeof ctrl === "function" && field === "key")
|
|
5558
|
-
ctrl = ctrl(item,
|
|
5558
|
+
ctrl = ctrl(item, path22);
|
|
5559
5559
|
}
|
|
5560
5560
|
}
|
|
5561
|
-
return typeof ctrl === "function" ? ctrl(item,
|
|
5561
|
+
return typeof ctrl === "function" ? ctrl(item, path22) : ctrl;
|
|
5562
5562
|
}
|
|
5563
5563
|
exports.visit = visit;
|
|
5564
5564
|
});
|
|
@@ -6827,14 +6827,14 @@ var require_parser = __commonJS((exports) => {
|
|
|
6827
6827
|
case "scalar":
|
|
6828
6828
|
case "single-quoted-scalar":
|
|
6829
6829
|
case "double-quoted-scalar": {
|
|
6830
|
-
const
|
|
6830
|
+
const fs17 = this.flowScalar(this.type);
|
|
6831
6831
|
if (atNextItem || it.value) {
|
|
6832
|
-
map2.items.push({ start, key:
|
|
6832
|
+
map2.items.push({ start, key: fs17, sep: [] });
|
|
6833
6833
|
this.onKeyLine = true;
|
|
6834
6834
|
} else if (it.sep) {
|
|
6835
|
-
this.stack.push(
|
|
6835
|
+
this.stack.push(fs17);
|
|
6836
6836
|
} else {
|
|
6837
|
-
Object.assign(it, { key:
|
|
6837
|
+
Object.assign(it, { key: fs17, sep: [] });
|
|
6838
6838
|
this.onKeyLine = true;
|
|
6839
6839
|
}
|
|
6840
6840
|
return;
|
|
@@ -6962,13 +6962,13 @@ var require_parser = __commonJS((exports) => {
|
|
|
6962
6962
|
case "scalar":
|
|
6963
6963
|
case "single-quoted-scalar":
|
|
6964
6964
|
case "double-quoted-scalar": {
|
|
6965
|
-
const
|
|
6965
|
+
const fs17 = this.flowScalar(this.type);
|
|
6966
6966
|
if (!it || it.value)
|
|
6967
|
-
fc.items.push({ start: [], key:
|
|
6967
|
+
fc.items.push({ start: [], key: fs17, sep: [] });
|
|
6968
6968
|
else if (it.sep)
|
|
6969
|
-
this.stack.push(
|
|
6969
|
+
this.stack.push(fs17);
|
|
6970
6970
|
else
|
|
6971
|
-
Object.assign(it, { key:
|
|
6971
|
+
Object.assign(it, { key: fs17, sep: [] });
|
|
6972
6972
|
return;
|
|
6973
6973
|
}
|
|
6974
6974
|
case "flow-map-end":
|
|
@@ -24224,6 +24224,9 @@ async function reconcileOneStaleTransitional(mainRoot, snap, result) {
|
|
|
24224
24224
|
} catch (err) {
|
|
24225
24225
|
debugLog(`stale salvage 后清理 worktree 失败 (${snap.sessionId}): ${err.message}`);
|
|
24226
24226
|
}
|
|
24227
|
+
await deleteBranchIfExists({ root: mainRoot, branch: snap.branch }).catch((err) => {
|
|
24228
|
+
debugLog(`stale salvage 后删分支失败 (${snap.sessionId}): ${err.message}`);
|
|
24229
|
+
});
|
|
24227
24230
|
result.staleSalvaged.push(snap.sessionId);
|
|
24228
24231
|
}
|
|
24229
24232
|
function summarizeReconcileDigest(result, prune, maxList = 3) {
|
|
@@ -24819,7 +24822,11 @@ var description4 = [
|
|
|
24819
24822
|
" reviewTarget 缺失时默认 'code'([Session Merge Review] 流按合同即 code review)。",
|
|
24820
24823
|
" 显式传入的值优先,不覆盖。plan:/decision:/pc- id 不补全。",
|
|
24821
24824
|
"**何时不调**:REQUEST_CHANGES / BLOCK 不调(无 APPROVE = 无审批记录)。",
|
|
24822
|
-
"**fallback**:codeforge 解析 reviewer boomerang 见 APPROVE 但无记录 → 自动以 source='codeforge-fallback' 补写。"
|
|
24825
|
+
"**fallback**:codeforge 解析 reviewer boomerang 见 APPROVE 但无记录 → 自动以 source='codeforge-fallback' 补写。",
|
|
24826
|
+
"**⚠️ 承诺语义(辅助提示,非安全保证)**(ADR:reviewer-approval-decision-consistency):",
|
|
24827
|
+
" 调此工具 = 你已决定 APPROVE/APPROVE_WITH_NOTES,**不可**再输出 REQUEST_CHANGES/BLOCK。",
|
|
24828
|
+
" 工具层无法强制校验(不能作为关键控制点);语义防线由 reviewer.md 文案约束承担(Phase 1),",
|
|
24829
|
+
" Phase 2 将在 review decision collection boundary 层自动作废同轮 stale approval。"
|
|
24823
24830
|
].join(`
|
|
24824
24831
|
`);
|
|
24825
24832
|
var ArgsSchema4 = exports_external.object({
|
|
@@ -25916,50 +25923,218 @@ function toEntry(r) {
|
|
|
25916
25923
|
}
|
|
25917
25924
|
// lib/merge-gate.ts
|
|
25918
25925
|
import { promises as fs13 } from "node:fs";
|
|
25926
|
+
import * as os5 from "node:os";
|
|
25919
25927
|
import * as path16 from "node:path";
|
|
25920
25928
|
var DEFAULT_MERGE_GATE_CONFIG = {
|
|
25921
25929
|
enabled: true,
|
|
25922
25930
|
approvalPreCheck: true,
|
|
25923
|
-
preCheckTtlSeconds: 3600
|
|
25931
|
+
preCheckTtlSeconds: 3600,
|
|
25932
|
+
requireUserConfirm: true
|
|
25924
25933
|
};
|
|
25925
|
-
var
|
|
25926
|
-
|
|
25927
|
-
|
|
25934
|
+
var PROJECT_CONFIG_REL = ".codeforge/merge-gate.json";
|
|
25935
|
+
var GLOBAL_CONFIG_PATH = path16.join(os5.homedir(), ".config", "codeforge", "merge-gate.json");
|
|
25936
|
+
async function readOptionalJson(filePath, label) {
|
|
25928
25937
|
let raw;
|
|
25929
25938
|
try {
|
|
25930
|
-
raw = await fs13.readFile(
|
|
25939
|
+
raw = await fs13.readFile(filePath, "utf8");
|
|
25931
25940
|
} catch (err) {
|
|
25932
25941
|
const e = err;
|
|
25933
25942
|
if (e.code === "ENOENT")
|
|
25934
|
-
return
|
|
25935
|
-
console.warn(`[merge-gate] 读取 ${
|
|
25936
|
-
return
|
|
25943
|
+
return null;
|
|
25944
|
+
console.warn(`[merge-gate] 读取 ${label} 失败,跳过该层配置: ${e.message}`);
|
|
25945
|
+
return null;
|
|
25937
25946
|
}
|
|
25938
25947
|
let parsed;
|
|
25939
25948
|
try {
|
|
25940
25949
|
parsed = JSON.parse(raw);
|
|
25941
25950
|
} catch (err) {
|
|
25942
|
-
console.warn(`[merge-gate] ${
|
|
25943
|
-
return
|
|
25951
|
+
console.warn(`[merge-gate] ${label} JSON 解析失败,跳过该层配置: ${err instanceof Error ? err.message : String(err)}`);
|
|
25952
|
+
return null;
|
|
25944
25953
|
}
|
|
25945
|
-
if (!parsed || typeof parsed !== "object") {
|
|
25946
|
-
console.warn(`[merge-gate] ${
|
|
25947
|
-
return
|
|
25954
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
25955
|
+
console.warn(`[merge-gate] ${label} 顶层非 object,跳过该层配置`);
|
|
25956
|
+
return null;
|
|
25948
25957
|
}
|
|
25949
|
-
|
|
25950
|
-
|
|
25951
|
-
|
|
25952
|
-
|
|
25953
|
-
const
|
|
25954
|
-
|
|
25955
|
-
|
|
25956
|
-
|
|
25957
|
-
|
|
25958
|
-
|
|
25959
|
-
|
|
25958
|
+
return parsed;
|
|
25959
|
+
}
|
|
25960
|
+
async function loadMergeGateWithPaths(mainRoot, globalConfigPath = GLOBAL_CONFIG_PATH) {
|
|
25961
|
+
const projectConfigPath = path16.join(mainRoot, PROJECT_CONFIG_REL);
|
|
25962
|
+
const [globalObj, projectObj] = await Promise.all([
|
|
25963
|
+
readOptionalJson(globalConfigPath, "~/.config/codeforge/merge-gate.json"),
|
|
25964
|
+
readOptionalJson(projectConfigPath, PROJECT_CONFIG_REL)
|
|
25965
|
+
]);
|
|
25966
|
+
const enabled = (() => {
|
|
25967
|
+
if (projectObj && typeof projectObj["enabled"] === "boolean")
|
|
25968
|
+
return projectObj["enabled"];
|
|
25969
|
+
if (globalObj && typeof globalObj["enabled"] === "boolean")
|
|
25970
|
+
return globalObj["enabled"];
|
|
25971
|
+
return DEFAULT_MERGE_GATE_CONFIG.enabled;
|
|
25972
|
+
})();
|
|
25973
|
+
const approvalPreCheck = (() => {
|
|
25974
|
+
if (projectObj && typeof projectObj["approvalPreCheck"] === "boolean")
|
|
25975
|
+
return projectObj["approvalPreCheck"];
|
|
25976
|
+
if (globalObj && typeof globalObj["approvalPreCheck"] === "boolean")
|
|
25977
|
+
return globalObj["approvalPreCheck"];
|
|
25978
|
+
return DEFAULT_MERGE_GATE_CONFIG.approvalPreCheck ?? true;
|
|
25979
|
+
})();
|
|
25980
|
+
const preCheckTtlSeconds = (() => {
|
|
25981
|
+
const rawTtl = projectObj?.["preCheckTtlSeconds"] ?? globalObj?.["preCheckTtlSeconds"];
|
|
25982
|
+
if (typeof rawTtl === "number" && rawTtl > 0) {
|
|
25983
|
+
if (rawTtl > 86400) {
|
|
25984
|
+
console.warn(`[merge-gate] preCheckTtlSeconds=${rawTtl} 超过 24h 上界,截断为 86400`);
|
|
25985
|
+
return 86400;
|
|
25986
|
+
}
|
|
25987
|
+
return rawTtl;
|
|
25988
|
+
}
|
|
25989
|
+
return DEFAULT_MERGE_GATE_CONFIG.preCheckTtlSeconds ?? 3600;
|
|
25990
|
+
})();
|
|
25991
|
+
const requireUserConfirm = (() => {
|
|
25992
|
+
if (projectObj && projectObj["requireUserConfirm"] === false) {
|
|
25993
|
+
console.warn("[merge-gate] 项目级配置不允许设 requireUserConfirm: false(安全锁定)。" + "若需关闭门 B,请在全局配置 ~/.config/codeforge/merge-gate.json 中设置。" + "项目级 false 被静默忽略,门 B 保持开启。");
|
|
25994
|
+
}
|
|
25995
|
+
if (globalObj?.["requireUserConfirm"] === false)
|
|
25996
|
+
return false;
|
|
25997
|
+
return DEFAULT_MERGE_GATE_CONFIG.requireUserConfirm ?? true;
|
|
25998
|
+
})();
|
|
25999
|
+
return { enabled, approvalPreCheck, preCheckTtlSeconds, requireUserConfirm };
|
|
26000
|
+
}
|
|
26001
|
+
async function loadMergeGate(mainRoot) {
|
|
26002
|
+
return loadMergeGateWithPaths(mainRoot, GLOBAL_CONFIG_PATH);
|
|
26003
|
+
}
|
|
26004
|
+
|
|
26005
|
+
// lib/confirm-store.ts
|
|
26006
|
+
import { promises as fs14 } from "node:fs";
|
|
26007
|
+
import * as path17 from "node:path";
|
|
26008
|
+
var CONFIRM_TTL_MS = 30 * 60 * 1000;
|
|
26009
|
+
function confirmRoot(mainRoot) {
|
|
26010
|
+
return path17.join(runtimeDir(mainRoot, { ensure: false }), "confirm");
|
|
26011
|
+
}
|
|
26012
|
+
function pendingFile(mainRoot, sid) {
|
|
26013
|
+
return path17.join(confirmRoot(mainRoot), "pending", `${sid}.json`);
|
|
26014
|
+
}
|
|
26015
|
+
function consumingFile(mainRoot, sid) {
|
|
26016
|
+
return path17.join(confirmRoot(mainRoot), "consuming", `${sid}.json`);
|
|
26017
|
+
}
|
|
26018
|
+
function consumedFile(mainRoot, sid) {
|
|
26019
|
+
return path17.join(confirmRoot(mainRoot), "consumed", `${sid}.json`);
|
|
26020
|
+
}
|
|
26021
|
+
async function ensureConfirmDirs(mainRoot) {
|
|
26022
|
+
const root = confirmRoot(mainRoot);
|
|
26023
|
+
await Promise.all([
|
|
26024
|
+
fs14.mkdir(path17.join(root, "pending"), { recursive: true }),
|
|
26025
|
+
fs14.mkdir(path17.join(root, "consuming"), { recursive: true }),
|
|
26026
|
+
fs14.mkdir(path17.join(root, "consumed"), { recursive: true })
|
|
26027
|
+
]);
|
|
26028
|
+
}
|
|
26029
|
+
async function readJsonSafe(file2) {
|
|
26030
|
+
let raw;
|
|
26031
|
+
try {
|
|
26032
|
+
raw = await fs14.readFile(file2, "utf8");
|
|
26033
|
+
} catch (e) {
|
|
26034
|
+
if (e.code === "ENOENT")
|
|
26035
|
+
return null;
|
|
26036
|
+
return null;
|
|
26037
|
+
}
|
|
26038
|
+
try {
|
|
26039
|
+
const parsed = JSON.parse(raw);
|
|
26040
|
+
if (typeof parsed.sessionId !== "string" || typeof parsed.worktreeHeadSha !== "string" || typeof parsed.approvalSha !== "string" || typeof parsed.ts !== "number" || typeof parsed.text !== "string" || parsed.status !== "PENDING" && parsed.status !== "CONSUMING" && parsed.status !== "CONSUMED") {
|
|
26041
|
+
return null;
|
|
25960
26042
|
}
|
|
26043
|
+
return parsed;
|
|
26044
|
+
} catch {
|
|
26045
|
+
return null;
|
|
26046
|
+
}
|
|
26047
|
+
}
|
|
26048
|
+
async function writeJsonAtomic(file2, rec) {
|
|
26049
|
+
const tmp = file2 + ".tmp." + Date.now();
|
|
26050
|
+
try {
|
|
26051
|
+
await fs14.writeFile(tmp, JSON.stringify(rec, null, 2), "utf8");
|
|
26052
|
+
await fs14.rename(tmp, file2);
|
|
26053
|
+
} catch (e) {
|
|
26054
|
+
await fs14.unlink(tmp).catch(() => {});
|
|
26055
|
+
throw e;
|
|
26056
|
+
}
|
|
26057
|
+
}
|
|
26058
|
+
async function writeConfirmRecord(mainRoot, rec) {
|
|
26059
|
+
await ensureConfirmDirs(mainRoot);
|
|
26060
|
+
const full = { ...rec, status: "PENDING" };
|
|
26061
|
+
await writeJsonAtomic(pendingFile(mainRoot, rec.sessionId), full);
|
|
26062
|
+
}
|
|
26063
|
+
async function readPendingConfirm(mainRoot, sid) {
|
|
26064
|
+
return readJsonSafe(pendingFile(mainRoot, sid));
|
|
26065
|
+
}
|
|
26066
|
+
function isHeadDrifted(rec, curHead) {
|
|
26067
|
+
if (!curHead)
|
|
26068
|
+
return true;
|
|
26069
|
+
return rec.worktreeHeadSha !== curHead;
|
|
26070
|
+
}
|
|
26071
|
+
function isConfirmValid(rec, curHead, approval, now = Date.now()) {
|
|
26072
|
+
if (!rec)
|
|
26073
|
+
return false;
|
|
26074
|
+
if (rec.status !== "PENDING")
|
|
26075
|
+
return false;
|
|
26076
|
+
if (isHeadDrifted(rec, curHead))
|
|
26077
|
+
return false;
|
|
26078
|
+
if (!approval)
|
|
26079
|
+
return false;
|
|
26080
|
+
if (approval.verdict !== "APPROVE" && approval.verdict !== "APPROVE_WITH_NOTES")
|
|
26081
|
+
return false;
|
|
26082
|
+
if (!approval.coveredSha)
|
|
26083
|
+
return false;
|
|
26084
|
+
if (rec.approvalSha !== approval.coveredSha)
|
|
26085
|
+
return false;
|
|
26086
|
+
if (now - rec.ts > CONFIRM_TTL_MS)
|
|
26087
|
+
return false;
|
|
26088
|
+
return true;
|
|
26089
|
+
}
|
|
26090
|
+
async function claimConfirmIfValid(mainRoot, sid, currentHeadSha, approval, now = Date.now()) {
|
|
26091
|
+
const rec = await readPendingConfirm(mainRoot, sid);
|
|
26092
|
+
if (!rec)
|
|
26093
|
+
return { ok: false, reason: "no_pending_record" };
|
|
26094
|
+
if (!isConfirmValid(rec, currentHeadSha, approval, now)) {
|
|
26095
|
+
return { ok: false, reason: "invalid_or_drifted" };
|
|
26096
|
+
}
|
|
26097
|
+
await ensureConfirmDirs(mainRoot);
|
|
26098
|
+
try {
|
|
26099
|
+
await fs14.rename(pendingFile(mainRoot, sid), consumingFile(mainRoot, sid));
|
|
26100
|
+
} catch (e) {
|
|
26101
|
+
const err = e;
|
|
26102
|
+
if (err.code === "ENOENT") {
|
|
26103
|
+
return { ok: false, reason: "already_claimed" };
|
|
26104
|
+
}
|
|
26105
|
+
throw e;
|
|
26106
|
+
}
|
|
26107
|
+
const claimed = { ...rec, status: "CONSUMING" };
|
|
26108
|
+
await writeJsonAtomic(consumingFile(mainRoot, sid), claimed);
|
|
26109
|
+
return { ok: true, rec: claimed };
|
|
26110
|
+
}
|
|
26111
|
+
async function finalizeConfirmConsumed(mainRoot, sid) {
|
|
26112
|
+
try {
|
|
26113
|
+
await ensureConfirmDirs(mainRoot);
|
|
26114
|
+
const rec = await readJsonSafe(consumingFile(mainRoot, sid));
|
|
26115
|
+
const consumed = {
|
|
26116
|
+
...rec ?? { sessionId: sid, worktreeHeadSha: "", approvalSha: "", ts: Date.now(), text: "" },
|
|
26117
|
+
status: "CONSUMED"
|
|
26118
|
+
};
|
|
26119
|
+
await writeJsonAtomic(consumedFile(mainRoot, sid), consumed);
|
|
26120
|
+
await fs14.unlink(consumingFile(mainRoot, sid)).catch(() => {});
|
|
26121
|
+
} catch (err) {
|
|
26122
|
+
console.warn(`[confirm-store] finalizeConfirmConsumed 失败(sid=${sid}),已合入,不阻断:${err instanceof Error ? err.message : String(err)}`);
|
|
26123
|
+
}
|
|
26124
|
+
}
|
|
26125
|
+
async function failConfirmConsumed(mainRoot, sid) {
|
|
26126
|
+
try {
|
|
26127
|
+
await ensureConfirmDirs(mainRoot);
|
|
26128
|
+
const rec = await readJsonSafe(consumingFile(mainRoot, sid));
|
|
26129
|
+
const consumed = {
|
|
26130
|
+
...rec ?? { sessionId: sid, worktreeHeadSha: "", approvalSha: "", ts: Date.now(), text: "" },
|
|
26131
|
+
status: "CONSUMED"
|
|
26132
|
+
};
|
|
26133
|
+
await writeJsonAtomic(consumedFile(mainRoot, sid), consumed);
|
|
26134
|
+
await fs14.unlink(consumingFile(mainRoot, sid)).catch(() => {});
|
|
26135
|
+
} catch (err) {
|
|
26136
|
+
console.warn(`[confirm-store] failConfirmConsumed 失败(sid=${sid}),fail-closed 已记录:${err instanceof Error ? err.message : String(err)}`);
|
|
25961
26137
|
}
|
|
25962
|
-
return { enabled, approvalPreCheck, preCheckTtlSeconds };
|
|
25963
26138
|
}
|
|
25964
26139
|
|
|
25965
26140
|
// lib/merge-loop.ts
|
|
@@ -26000,7 +26175,8 @@ async function runMergeLoop(opts) {
|
|
|
26000
26175
|
const { sha } = await mergeSessionBack({
|
|
26001
26176
|
sessionId: opts.sessionId,
|
|
26002
26177
|
mainRoot: opts.mainRoot,
|
|
26003
|
-
commitMessage: message
|
|
26178
|
+
commitMessage: message,
|
|
26179
|
+
...opts.planStore ? { planStore: opts.planStore } : {}
|
|
26004
26180
|
});
|
|
26005
26181
|
return { status: "force_merged", commitSha: sha, loops: 0 };
|
|
26006
26182
|
}
|
|
@@ -26027,14 +26203,50 @@ async function runMergeLoop(opts) {
|
|
|
26027
26203
|
});
|
|
26028
26204
|
}
|
|
26029
26205
|
progress("approval_pre_check", `skip_review | reviewTarget=${hit.reviewTarget} | coveredSha=${hit.coveredSha.slice(0, 12)} | ttlOk`);
|
|
26030
|
-
|
|
26031
|
-
|
|
26032
|
-
|
|
26033
|
-
|
|
26034
|
-
|
|
26206
|
+
{
|
|
26207
|
+
const preCheckApproval = (() => {
|
|
26208
|
+
return {
|
|
26209
|
+
verdict: "APPROVE",
|
|
26210
|
+
coveredSha: hit.coveredSha,
|
|
26211
|
+
pendingId: `session:${opts.sessionId}`,
|
|
26212
|
+
createdAt: new Date().toISOString()
|
|
26213
|
+
};
|
|
26214
|
+
})();
|
|
26215
|
+
const gate = await claimConfirmGate({
|
|
26216
|
+
mainRoot: opts.mainRoot,
|
|
26217
|
+
sessionId: opts.sessionId,
|
|
26218
|
+
worktreePath: entry.worktreePath,
|
|
26219
|
+
approval: preCheckApproval,
|
|
26220
|
+
mergeGate,
|
|
26221
|
+
getCurrentWorktreeHeadFn: opts.__testHooks?.getCurrentWorktreeHead
|
|
26222
|
+
});
|
|
26223
|
+
if (!gate.ok) {
|
|
26224
|
+
progress("block_pause", "门 B 对话确认未通过(pre-check 路径)");
|
|
26225
|
+
return {
|
|
26226
|
+
status: "blocked",
|
|
26227
|
+
loops: 0,
|
|
26228
|
+
finalDecision: "APPROVE",
|
|
26229
|
+
blockReason: gate.blockReason,
|
|
26230
|
+
lastReviewSummary: `approval-pre-check 命中但门 B 阻断:${gate.blockReason}`
|
|
26231
|
+
};
|
|
26232
|
+
}
|
|
26233
|
+
}
|
|
26234
|
+
let preCheckSha;
|
|
26235
|
+
try {
|
|
26236
|
+
({ sha: preCheckSha } = await mergeSessionBack({
|
|
26237
|
+
sessionId: opts.sessionId,
|
|
26238
|
+
mainRoot: opts.mainRoot,
|
|
26239
|
+
...opts.summary ? { summary: opts.summary } : {},
|
|
26240
|
+
...opts.planStore ? { planStore: opts.planStore } : {}
|
|
26241
|
+
}));
|
|
26242
|
+
} catch (mergeErr) {
|
|
26243
|
+
await failConfirmConsumed(opts.mainRoot, opts.sessionId);
|
|
26244
|
+
throw mergeErr;
|
|
26245
|
+
}
|
|
26246
|
+
await finalizeConfirmConsumed(opts.mainRoot, opts.sessionId);
|
|
26035
26247
|
return {
|
|
26036
26248
|
status: "skipped_by_approval",
|
|
26037
|
-
commitSha:
|
|
26249
|
+
commitSha: preCheckSha,
|
|
26038
26250
|
loops: 0,
|
|
26039
26251
|
finalDecision: "APPROVE",
|
|
26040
26252
|
lastReviewSummary: `approval-pre-check 命中:coveredSha=${hit.coveredSha}, reviewTarget=${hit.reviewTarget}`
|
|
@@ -26119,15 +26331,43 @@ async function runMergeLoop(opts) {
|
|
|
26119
26331
|
...lastReviewSummary ? { lastReviewSummary } : {}
|
|
26120
26332
|
};
|
|
26121
26333
|
}
|
|
26334
|
+
{
|
|
26335
|
+
const gate = await claimConfirmGate({
|
|
26336
|
+
mainRoot: opts.mainRoot,
|
|
26337
|
+
sessionId: opts.sessionId,
|
|
26338
|
+
worktreePath: entry.worktreePath,
|
|
26339
|
+
approval: approval ?? undefined,
|
|
26340
|
+
mergeGate,
|
|
26341
|
+
getCurrentWorktreeHeadFn: opts.__testHooks?.getCurrentWorktreeHead
|
|
26342
|
+
});
|
|
26343
|
+
if (!gate.ok) {
|
|
26344
|
+
progress("block_pause", "门 B 对话确认未通过(S6 路径)");
|
|
26345
|
+
return {
|
|
26346
|
+
status: "blocked",
|
|
26347
|
+
loops,
|
|
26348
|
+
finalDecision: "APPROVE",
|
|
26349
|
+
blockReason: gate.blockReason,
|
|
26350
|
+
...lastReviewSummary ? { lastReviewSummary } : {}
|
|
26351
|
+
};
|
|
26352
|
+
}
|
|
26353
|
+
}
|
|
26122
26354
|
}
|
|
26123
|
-
|
|
26124
|
-
|
|
26125
|
-
|
|
26126
|
-
|
|
26127
|
-
|
|
26355
|
+
let s6Sha;
|
|
26356
|
+
try {
|
|
26357
|
+
({ sha: s6Sha } = await mergeSessionBack({
|
|
26358
|
+
sessionId: opts.sessionId,
|
|
26359
|
+
mainRoot: opts.mainRoot,
|
|
26360
|
+
...opts.summary ? { summary: opts.summary } : {},
|
|
26361
|
+
...opts.planStore ? { planStore: opts.planStore } : {}
|
|
26362
|
+
}));
|
|
26363
|
+
} catch (mergeErr) {
|
|
26364
|
+
await failConfirmConsumed(opts.mainRoot, opts.sessionId);
|
|
26365
|
+
throw mergeErr;
|
|
26366
|
+
}
|
|
26367
|
+
await finalizeConfirmConsumed(opts.mainRoot, opts.sessionId);
|
|
26128
26368
|
return {
|
|
26129
26369
|
status: "merged",
|
|
26130
|
-
commitSha:
|
|
26370
|
+
commitSha: s6Sha,
|
|
26131
26371
|
loops,
|
|
26132
26372
|
finalDecision: "APPROVE",
|
|
26133
26373
|
lastReviewSummary
|
|
@@ -26203,6 +26443,26 @@ async function runMergeLoop(opts) {
|
|
|
26203
26443
|
}
|
|
26204
26444
|
}
|
|
26205
26445
|
}
|
|
26446
|
+
async function claimConfirmGate(args) {
|
|
26447
|
+
const { mainRoot, sessionId, worktreePath, approval, mergeGate } = args;
|
|
26448
|
+
if (!mergeGate.requireUserConfirm) {
|
|
26449
|
+
return { ok: true, skip: true };
|
|
26450
|
+
}
|
|
26451
|
+
const headOf = args.getCurrentWorktreeHeadFn ?? getCurrentWorktreeHead;
|
|
26452
|
+
const curHead = await headOf(worktreePath).catch(() => "");
|
|
26453
|
+
const approvalArg = approval ? { verdict: approval.verdict, coveredSha: approval.coveredSha ?? "" } : null;
|
|
26454
|
+
const claim = await claimConfirmIfValid(mainRoot, sessionId, curHead, approvalArg).catch((e) => ({
|
|
26455
|
+
ok: false,
|
|
26456
|
+
reason: `io_error:${e instanceof Error ? e.message : String(e)}`
|
|
26457
|
+
}));
|
|
26458
|
+
if (!claim.ok) {
|
|
26459
|
+
return {
|
|
26460
|
+
ok: false,
|
|
26461
|
+
blockReason: `对话确认门(门 B)未通过(${claim.reason}):请重新说「确认合入」。`
|
|
26462
|
+
};
|
|
26463
|
+
}
|
|
26464
|
+
return { ok: true, claimed: true };
|
|
26465
|
+
}
|
|
26206
26466
|
function buildForceMergeMessage(sessionId, entry) {
|
|
26207
26467
|
return `session(${sessionId}): merge ${entry.branch} [force-merge: skipped review]
|
|
26208
26468
|
|
|
@@ -26658,12 +26918,14 @@ async function execute12(input) {
|
|
|
26658
26918
|
...mergeArgs.plan_id ? { planId: mergeArgs.plan_id } : {},
|
|
26659
26919
|
...mergeArgs.force ? { force: true } : {},
|
|
26660
26920
|
...mergeArgs.summary ? { summary: mergeArgs.summary } : {},
|
|
26921
|
+
..._ctx.planStore ? { planStore: _ctx.planStore } : {},
|
|
26661
26922
|
spawner: _ctx.spawner,
|
|
26662
26923
|
...sendProgress ? {
|
|
26663
26924
|
onProgress: (state, detail) => {
|
|
26664
26925
|
Promise.resolve(sendProgress(state, detail)).catch(() => {});
|
|
26665
26926
|
}
|
|
26666
|
-
} : {}
|
|
26927
|
+
} : {},
|
|
26928
|
+
..._ctx.__testHooks ? { __testHooks: _ctx.__testHooks } : {}
|
|
26667
26929
|
});
|
|
26668
26930
|
return { ok: true, action: "merge", data: result };
|
|
26669
26931
|
} catch (err) {
|
|
@@ -26675,8 +26937,8 @@ async function execute12(input) {
|
|
|
26675
26937
|
}
|
|
26676
26938
|
}
|
|
26677
26939
|
// lib/plan-store.ts
|
|
26678
|
-
import { promises as
|
|
26679
|
-
import * as
|
|
26940
|
+
import { promises as fs15 } from "node:fs";
|
|
26941
|
+
import * as path18 from "node:path";
|
|
26680
26942
|
var INDEX_VERSION = 1;
|
|
26681
26943
|
|
|
26682
26944
|
class PlanStore {
|
|
@@ -26685,8 +26947,8 @@ class PlanStore {
|
|
|
26685
26947
|
now;
|
|
26686
26948
|
secondCounters = new Map;
|
|
26687
26949
|
constructor(opts = {}) {
|
|
26688
|
-
this.root =
|
|
26689
|
-
this.base = opts.base ?
|
|
26950
|
+
this.root = path18.resolve(opts.root ?? process.cwd());
|
|
26951
|
+
this.base = opts.base ? path18.resolve(opts.base) : plansDir(this.root);
|
|
26690
26952
|
this.now = opts.now ?? (() => new Date);
|
|
26691
26953
|
}
|
|
26692
26954
|
async write(input) {
|
|
@@ -26696,14 +26958,14 @@ class PlanStore {
|
|
|
26696
26958
|
if (typeof input.content !== "string" || input.content.length === 0) {
|
|
26697
26959
|
throw new Error("PlanStore.write: content 不能为空");
|
|
26698
26960
|
}
|
|
26699
|
-
await
|
|
26961
|
+
await fs15.mkdir(this.base, { recursive: true });
|
|
26700
26962
|
const lockPath = this.lockPath();
|
|
26701
26963
|
return await withFileLock(lockPath, async () => {
|
|
26702
26964
|
const index = await this.readIndexLocked();
|
|
26703
26965
|
const now = this.now();
|
|
26704
26966
|
const planId = this.allocPlanId(now, index);
|
|
26705
26967
|
const filename = this.composeFilename(planId, input.title);
|
|
26706
|
-
const absFile =
|
|
26968
|
+
const absFile = path18.join(this.base, filename);
|
|
26707
26969
|
await this.atomicWriteFile(absFile, input.content);
|
|
26708
26970
|
const entry = {
|
|
26709
26971
|
plan_id: planId,
|
|
@@ -26727,9 +26989,9 @@ class PlanStore {
|
|
|
26727
26989
|
const entry = index.entries.find((e) => e.plan_id === planId);
|
|
26728
26990
|
if (!entry)
|
|
26729
26991
|
return null;
|
|
26730
|
-
const abs =
|
|
26992
|
+
const abs = path18.join(this.base, entry.path);
|
|
26731
26993
|
try {
|
|
26732
|
-
const content = await
|
|
26994
|
+
const content = await fs15.readFile(abs, "utf8");
|
|
26733
26995
|
return { entry, content };
|
|
26734
26996
|
} catch (err) {
|
|
26735
26997
|
const e = err;
|
|
@@ -26788,7 +27050,7 @@ class PlanStore {
|
|
|
26788
27050
|
else if (e.status === "orphan")
|
|
26789
27051
|
shouldDelete = true;
|
|
26790
27052
|
if (shouldDelete) {
|
|
26791
|
-
await
|
|
27053
|
+
await fs15.rm(path18.join(this.base, e.path), { force: true }).catch(() => {});
|
|
26792
27054
|
removed++;
|
|
26793
27055
|
} else {
|
|
26794
27056
|
keep.push(e);
|
|
@@ -26810,9 +27072,9 @@ class PlanStore {
|
|
|
26810
27072
|
knownPaths.add(e.path);
|
|
26811
27073
|
if (e.status !== "active")
|
|
26812
27074
|
continue;
|
|
26813
|
-
const abs =
|
|
27075
|
+
const abs = path18.join(this.base, e.path);
|
|
26814
27076
|
try {
|
|
26815
|
-
await
|
|
27077
|
+
await fs15.stat(abs);
|
|
26816
27078
|
} catch {
|
|
26817
27079
|
e.status = "orphan";
|
|
26818
27080
|
markedOrphan++;
|
|
@@ -26822,23 +27084,23 @@ class PlanStore {
|
|
|
26822
27084
|
await this.writeIndexLocked(index);
|
|
26823
27085
|
let unindexedFiles = [];
|
|
26824
27086
|
try {
|
|
26825
|
-
const all = await
|
|
27087
|
+
const all = await fs15.readdir(this.base);
|
|
26826
27088
|
unindexedFiles = all.filter((f) => f.endsWith(".md")).filter((f) => !knownPaths.has(f));
|
|
26827
27089
|
} catch {}
|
|
26828
27090
|
return { markedOrphan, unindexedFiles };
|
|
26829
27091
|
});
|
|
26830
27092
|
}
|
|
26831
27093
|
indexPath() {
|
|
26832
|
-
return
|
|
27094
|
+
return path18.join(this.base, "index.json");
|
|
26833
27095
|
}
|
|
26834
27096
|
lockPath() {
|
|
26835
|
-
return
|
|
27097
|
+
return path18.join(this.base, "index.lock");
|
|
26836
27098
|
}
|
|
26837
27099
|
async readIndexLocked() {
|
|
26838
27100
|
const file2 = this.indexPath();
|
|
26839
27101
|
let raw;
|
|
26840
27102
|
try {
|
|
26841
|
-
raw = await
|
|
27103
|
+
raw = await fs15.readFile(file2, "utf8");
|
|
26842
27104
|
} catch (err) {
|
|
26843
27105
|
const e = err;
|
|
26844
27106
|
if (e.code === "ENOENT")
|
|
@@ -26860,7 +27122,7 @@ class PlanStore {
|
|
|
26860
27122
|
async archiveCorruptIndex(file2) {
|
|
26861
27123
|
const ts = this.now().toISOString().replace(/[-:.TZ]/g, "").slice(0, 14);
|
|
26862
27124
|
const dst = `${file2}.corrupt-${ts}`;
|
|
26863
|
-
await
|
|
27125
|
+
await fs15.rename(file2, dst).catch(() => {});
|
|
26864
27126
|
}
|
|
26865
27127
|
async readIndex() {
|
|
26866
27128
|
return this.readIndexLocked();
|
|
@@ -26905,15 +27167,15 @@ class PlanStore {
|
|
|
26905
27167
|
const tsPart = m ? `${m[1]}-${m[2]}` : planId;
|
|
26906
27168
|
const nnn = m ? m[3] : "000";
|
|
26907
27169
|
const sample = planFilePath(this.root, title);
|
|
26908
|
-
const base =
|
|
27170
|
+
const base = path18.basename(sample, ".md");
|
|
26909
27171
|
const slug = base.replace(/^\d{8}-\d{6}-?/, "") || "untitled";
|
|
26910
27172
|
return `${tsPart}-${nnn}-${slug}.md`;
|
|
26911
27173
|
}
|
|
26912
27174
|
async atomicWriteFile(file2, data) {
|
|
26913
|
-
await
|
|
27175
|
+
await fs15.mkdir(path18.dirname(file2), { recursive: true });
|
|
26914
27176
|
const tmp = `${file2}.tmp-${process.pid}-${Date.now()}`;
|
|
26915
|
-
await
|
|
26916
|
-
await
|
|
27177
|
+
await fs15.writeFile(tmp, data, "utf8");
|
|
27178
|
+
await fs15.rename(tmp, file2);
|
|
26917
27179
|
}
|
|
26918
27180
|
}
|
|
26919
27181
|
function formatTimestamp(d) {
|
|
@@ -26976,8 +27238,8 @@ async function execute13(input) {
|
|
|
26976
27238
|
}
|
|
26977
27239
|
}
|
|
26978
27240
|
// tools/plan-read.ts
|
|
26979
|
-
import { promises as
|
|
26980
|
-
import * as
|
|
27241
|
+
import { promises as fs16 } from "node:fs";
|
|
27242
|
+
import * as path19 from "node:path";
|
|
26981
27243
|
var description14 = [
|
|
26982
27244
|
"读取方案文档内容,支持按 plan_id 或绝对路径查询。",
|
|
26983
27245
|
"**何时调用**:",
|
|
@@ -27082,9 +27344,9 @@ async function execute14(input) {
|
|
|
27082
27344
|
};
|
|
27083
27345
|
}
|
|
27084
27346
|
}
|
|
27085
|
-
const abs =
|
|
27347
|
+
const abs = path19.resolve(args.path);
|
|
27086
27348
|
try {
|
|
27087
|
-
const content = await
|
|
27349
|
+
const content = await fs16.readFile(abs, "utf8");
|
|
27088
27350
|
return {
|
|
27089
27351
|
ok: true,
|
|
27090
27352
|
content,
|
|
@@ -27106,16 +27368,16 @@ async function execute14(input) {
|
|
|
27106
27368
|
// lib/adr-init.ts
|
|
27107
27369
|
import { spawnSync } from "node:child_process";
|
|
27108
27370
|
import { existsSync as existsSync4, promises as fsp } from "node:fs";
|
|
27109
|
-
import * as
|
|
27371
|
+
import * as path20 from "node:path";
|
|
27110
27372
|
import * as url2 from "node:url";
|
|
27111
27373
|
function resolveAssetsRoot() {
|
|
27112
|
-
const here =
|
|
27374
|
+
const here = path20.dirname(url2.fileURLToPath(import.meta.url));
|
|
27113
27375
|
let dir = here;
|
|
27114
27376
|
for (let i = 0;i < 6; i++) {
|
|
27115
|
-
if (existsSync4(
|
|
27116
|
-
return
|
|
27377
|
+
if (existsSync4(path20.join(dir, "package.json")) && existsSync4(path20.join(dir, "assets", "adr-init"))) {
|
|
27378
|
+
return path20.join(dir, "assets", "adr-init");
|
|
27117
27379
|
}
|
|
27118
|
-
const parent =
|
|
27380
|
+
const parent = path20.dirname(dir);
|
|
27119
27381
|
if (parent === dir)
|
|
27120
27382
|
break;
|
|
27121
27383
|
dir = parent;
|
|
@@ -27123,13 +27385,13 @@ function resolveAssetsRoot() {
|
|
|
27123
27385
|
const xdgConfig = process.env["XDG_CONFIG_HOME"];
|
|
27124
27386
|
const homeDir = process.env["HOME"] ?? process.env["USERPROFILE"] ?? "";
|
|
27125
27387
|
const fallbackRoots = [
|
|
27126
|
-
xdgConfig ?
|
|
27127
|
-
|
|
27128
|
-
process.env["APPDATA"] ?
|
|
27129
|
-
process.env["LOCALAPPDATA"] ?
|
|
27388
|
+
xdgConfig ? path20.join(xdgConfig, "opencode") : null,
|
|
27389
|
+
path20.join(homeDir, ".config", "opencode"),
|
|
27390
|
+
process.env["APPDATA"] ? path20.join(process.env["APPDATA"], "opencode") : null,
|
|
27391
|
+
process.env["LOCALAPPDATA"] ? path20.join(process.env["LOCALAPPDATA"], "opencode") : null
|
|
27130
27392
|
].filter(Boolean);
|
|
27131
27393
|
for (const root of fallbackRoots) {
|
|
27132
|
-
const candidate =
|
|
27394
|
+
const candidate = path20.join(root, "assets", "adr-init");
|
|
27133
27395
|
if (existsSync4(candidate)) {
|
|
27134
27396
|
return candidate;
|
|
27135
27397
|
}
|
|
@@ -27166,7 +27428,7 @@ function runGitConfigHooksPath(cwd) {
|
|
|
27166
27428
|
}
|
|
27167
27429
|
}
|
|
27168
27430
|
async function runAdrInit(opts = {}) {
|
|
27169
|
-
const cwd =
|
|
27431
|
+
const cwd = path20.resolve(opts.cwd ?? process.cwd());
|
|
27170
27432
|
const force = !!opts.force;
|
|
27171
27433
|
const dryRun = !!opts.dryRun;
|
|
27172
27434
|
const writePrepare = !!opts.writePrepare;
|
|
@@ -27215,8 +27477,8 @@ async function runAdrInit(opts = {}) {
|
|
|
27215
27477
|
});
|
|
27216
27478
|
}
|
|
27217
27479
|
for (const item of plan) {
|
|
27218
|
-
const srcAbs =
|
|
27219
|
-
const dstAbs =
|
|
27480
|
+
const srcAbs = path20.join(assetsRoot, item.src);
|
|
27481
|
+
const dstAbs = path20.join(cwd, item.dst);
|
|
27220
27482
|
if (!existsSync4(srcAbs)) {
|
|
27221
27483
|
result.warnings.push(`资产缺失:${item.src}(跳过 ${item.dst})`);
|
|
27222
27484
|
continue;
|
|
@@ -27229,7 +27491,7 @@ async function runAdrInit(opts = {}) {
|
|
|
27229
27491
|
const bakRel = `${item.dst}.bak.${ts}`;
|
|
27230
27492
|
if (!dryRun) {
|
|
27231
27493
|
try {
|
|
27232
|
-
await fsp.copyFile(dstAbs,
|
|
27494
|
+
await fsp.copyFile(dstAbs, path20.join(cwd, bakRel));
|
|
27233
27495
|
} catch (e) {
|
|
27234
27496
|
result.ok = false;
|
|
27235
27497
|
result.reason = "io_error";
|
|
@@ -27241,7 +27503,7 @@ async function runAdrInit(opts = {}) {
|
|
|
27241
27503
|
}
|
|
27242
27504
|
if (!dryRun) {
|
|
27243
27505
|
try {
|
|
27244
|
-
await fsp.mkdir(
|
|
27506
|
+
await fsp.mkdir(path20.dirname(dstAbs), { recursive: true });
|
|
27245
27507
|
await fsp.copyFile(srcAbs, dstAbs);
|
|
27246
27508
|
if (item.chmod !== undefined) {
|
|
27247
27509
|
try {
|
|
@@ -27271,7 +27533,7 @@ async function runAdrInit(opts = {}) {
|
|
|
27271
27533
|
} else {
|
|
27272
27534
|
result.suggestions.push("[dry-run] 将运行:git config core.hooksPath .githooks");
|
|
27273
27535
|
}
|
|
27274
|
-
const pkgPath =
|
|
27536
|
+
const pkgPath = path20.join(cwd, "package.json");
|
|
27275
27537
|
const isNpm = existsSync4(pkgPath);
|
|
27276
27538
|
if (isNpm) {
|
|
27277
27539
|
if (writePrepare) {
|
|
@@ -27286,7 +27548,7 @@ async function runAdrInit(opts = {}) {
|
|
|
27286
27548
|
const bakRel = `package.json.bak.${ts}`;
|
|
27287
27549
|
if (!dryRun) {
|
|
27288
27550
|
try {
|
|
27289
|
-
await fsp.copyFile(pkgPath,
|
|
27551
|
+
await fsp.copyFile(pkgPath, path20.join(cwd, bakRel));
|
|
27290
27552
|
} catch (e) {
|
|
27291
27553
|
result.warnings.push(`备份 package.json 失败:${e instanceof Error ? e.message : String(e)}`);
|
|
27292
27554
|
}
|
|
@@ -27406,12 +27668,12 @@ async function execute15(input) {
|
|
|
27406
27668
|
}
|
|
27407
27669
|
}
|
|
27408
27670
|
// lib/opencode-session-probe.ts
|
|
27409
|
-
import * as
|
|
27410
|
-
import * as
|
|
27671
|
+
import * as path21 from "node:path";
|
|
27672
|
+
import * as os6 from "node:os";
|
|
27411
27673
|
import { createRequire as createRequire2 } from "node:module";
|
|
27412
27674
|
var requireFromHere = createRequire2(import.meta.url);
|
|
27413
27675
|
var DEFAULT_LIVENESS_MS = 6 * 60 * 60000;
|
|
27414
|
-
var DEFAULT_DB_PATH =
|
|
27676
|
+
var DEFAULT_DB_PATH = path21.join(os6.homedir(), ".local/share/opencode/opencode.db");
|
|
27415
27677
|
function createSessionProbe(opts = {}) {
|
|
27416
27678
|
const dbPath = opts.dbPath ?? DEFAULT_DB_PATH;
|
|
27417
27679
|
const httpBaseUrl = opts.httpBaseUrl ?? process.env["OPENCODE_SERVER_URL"];
|
|
@@ -27566,6 +27828,241 @@ async function execute16(input) {
|
|
|
27566
27828
|
probe.close();
|
|
27567
27829
|
}
|
|
27568
27830
|
}
|
|
27831
|
+
// lib/llm-retry.ts
|
|
27832
|
+
var DEFAULT_RETRY_CONFIG = {
|
|
27833
|
+
maxRetries: 2,
|
|
27834
|
+
baseDelayMs: 1000,
|
|
27835
|
+
maxDelayMs: 8000,
|
|
27836
|
+
jitterRatio: 0.25,
|
|
27837
|
+
minAttemptWindowMs: 30000
|
|
27838
|
+
};
|
|
27839
|
+
function classifyError(err) {
|
|
27840
|
+
const e = err;
|
|
27841
|
+
if (e instanceof Error && e.name === "AbortError" || e && e.name === "AbortError" || e && e.name === "MessageAbortedError") {
|
|
27842
|
+
return "fatal";
|
|
27843
|
+
}
|
|
27844
|
+
if (e && e.__cfTimeout === true) {
|
|
27845
|
+
return "fatal";
|
|
27846
|
+
}
|
|
27847
|
+
const errName = e ? e.name : undefined;
|
|
27848
|
+
if (errName === "MessageOutputLengthError")
|
|
27849
|
+
return "fatal";
|
|
27850
|
+
const msg = getMsg(err);
|
|
27851
|
+
if (/finish[=\s]+length/i.test(msg))
|
|
27852
|
+
return "fatal";
|
|
27853
|
+
if (errName === "APIError") {
|
|
27854
|
+
const data = e.data ?? {};
|
|
27855
|
+
const isRetryable = data.isRetryable;
|
|
27856
|
+
const statusCode = data.statusCode;
|
|
27857
|
+
if (typeof isRetryable === "boolean") {
|
|
27858
|
+
return isRetryable ? "retryable" : "fatal";
|
|
27859
|
+
}
|
|
27860
|
+
if (typeof statusCode === "number") {
|
|
27861
|
+
if (statusCode === 429 || statusCode === 529 || statusCode >= 500 && statusCode < 600) {
|
|
27862
|
+
return "retryable";
|
|
27863
|
+
}
|
|
27864
|
+
if (statusCode >= 400 && statusCode < 500)
|
|
27865
|
+
return "fatal";
|
|
27866
|
+
}
|
|
27867
|
+
return "retryable";
|
|
27868
|
+
}
|
|
27869
|
+
const NETWORK_KEYWORDS = [
|
|
27870
|
+
"http2",
|
|
27871
|
+
"client connection lost",
|
|
27872
|
+
"econnreset",
|
|
27873
|
+
"econnrefused",
|
|
27874
|
+
"etimedout",
|
|
27875
|
+
"epipe",
|
|
27876
|
+
"socket hang up",
|
|
27877
|
+
"fetch failed",
|
|
27878
|
+
"network"
|
|
27879
|
+
];
|
|
27880
|
+
const msgLower = msg.toLowerCase();
|
|
27881
|
+
if (NETWORK_KEYWORDS.some((kw) => msgLower.includes(kw))) {
|
|
27882
|
+
return "retryable";
|
|
27883
|
+
}
|
|
27884
|
+
if (/\b(5\d{2}|529|429)\b/.test(msg))
|
|
27885
|
+
return "retryable";
|
|
27886
|
+
if (errName === "ProviderAuthError" || errName === "ContextOverflowError")
|
|
27887
|
+
return "fatal";
|
|
27888
|
+
if (/\b4[0-9]{2}\b/.test(msg) && !/\b429\b/.test(msg))
|
|
27889
|
+
return "fatal";
|
|
27890
|
+
return "fatal";
|
|
27891
|
+
}
|
|
27892
|
+
function extractRetryAfterMs(err) {
|
|
27893
|
+
const headers = getResponseHeaders(err) ?? getResponseHeaders(err?.cause);
|
|
27894
|
+
if (!headers)
|
|
27895
|
+
return;
|
|
27896
|
+
const headerValue = findHeader(headers, "retry-after");
|
|
27897
|
+
if (!headerValue)
|
|
27898
|
+
return;
|
|
27899
|
+
return parseRetryAfterHeader(headerValue);
|
|
27900
|
+
}
|
|
27901
|
+
function getResponseHeaders(src) {
|
|
27902
|
+
if (!src || typeof src !== "object")
|
|
27903
|
+
return;
|
|
27904
|
+
const e = src;
|
|
27905
|
+
const data = e["data"];
|
|
27906
|
+
if (data && typeof data === "object") {
|
|
27907
|
+
const rh = data["responseHeaders"];
|
|
27908
|
+
if (rh && typeof rh === "object" && !Array.isArray(rh)) {
|
|
27909
|
+
return rh;
|
|
27910
|
+
}
|
|
27911
|
+
}
|
|
27912
|
+
return;
|
|
27913
|
+
}
|
|
27914
|
+
function findHeader(headers, name) {
|
|
27915
|
+
const lower = name.toLowerCase();
|
|
27916
|
+
for (const [k, v] of Object.entries(headers)) {
|
|
27917
|
+
if (k.toLowerCase() === lower)
|
|
27918
|
+
return v;
|
|
27919
|
+
}
|
|
27920
|
+
return;
|
|
27921
|
+
}
|
|
27922
|
+
function parseRetryAfterHeader(value) {
|
|
27923
|
+
const trimmed = value.trim();
|
|
27924
|
+
const seconds = parseFloat(trimmed);
|
|
27925
|
+
if (!isNaN(seconds) && seconds >= 0) {
|
|
27926
|
+
return Math.ceil(seconds * 1000);
|
|
27927
|
+
}
|
|
27928
|
+
const d = new Date(trimmed);
|
|
27929
|
+
if (!isNaN(d.getTime())) {
|
|
27930
|
+
const ms = d.getTime() - Date.now();
|
|
27931
|
+
return ms > 0 ? ms : 0;
|
|
27932
|
+
}
|
|
27933
|
+
return;
|
|
27934
|
+
}
|
|
27935
|
+
async function withLlmRetry(fn, opts = {}) {
|
|
27936
|
+
const maxRetries = opts.maxRetries ?? DEFAULT_RETRY_CONFIG.maxRetries;
|
|
27937
|
+
const baseDelayMs = opts.baseDelayMs ?? DEFAULT_RETRY_CONFIG.baseDelayMs;
|
|
27938
|
+
const maxDelayMs = opts.maxDelayMs ?? DEFAULT_RETRY_CONFIG.maxDelayMs;
|
|
27939
|
+
const jitterRatio = opts.jitterRatio ?? DEFAULT_RETRY_CONFIG.jitterRatio;
|
|
27940
|
+
const minAttemptWindowMs = opts.minAttemptWindowMs ?? DEFAULT_RETRY_CONFIG.minAttemptWindowMs;
|
|
27941
|
+
const maxTotalRetryMs = opts.maxTotalRetryMs;
|
|
27942
|
+
const maxAttempts = maxRetries + 1;
|
|
27943
|
+
const startedAt = Date.now();
|
|
27944
|
+
let lastError;
|
|
27945
|
+
let attempt = 0;
|
|
27946
|
+
while (attempt < maxAttempts) {
|
|
27947
|
+
if (attempt > 0 && opts.signal?.aborted) {
|
|
27948
|
+
const e = new Error("withLlmRetry: aborted by signal");
|
|
27949
|
+
e.name = "AbortError";
|
|
27950
|
+
throw e;
|
|
27951
|
+
}
|
|
27952
|
+
if (attempt > 0 && maxTotalRetryMs != null) {
|
|
27953
|
+
const elapsed = Date.now() - startedAt;
|
|
27954
|
+
const remaining = maxTotalRetryMs - elapsed;
|
|
27955
|
+
const backoff = calcBackoff(attempt, baseDelayMs, maxDelayMs, jitterRatio);
|
|
27956
|
+
const gate = Math.max(backoff, minAttemptWindowMs);
|
|
27957
|
+
if (remaining < gate) {
|
|
27958
|
+
opts.log?.("warn", `[llm-retry] budget gate: remaining=${remaining}ms < gate=${gate}ms, stopping after attempt ${attempt}`);
|
|
27959
|
+
throw lastError ?? new Error("withLlmRetry: budget exhausted");
|
|
27960
|
+
}
|
|
27961
|
+
}
|
|
27962
|
+
attempt++;
|
|
27963
|
+
try {
|
|
27964
|
+
const result = await fn();
|
|
27965
|
+
if (opts.isRetryableResult) {
|
|
27966
|
+
const failErr = opts.isRetryableResult(result);
|
|
27967
|
+
if (failErr) {
|
|
27968
|
+
const cls = classifyError(failErr);
|
|
27969
|
+
if (cls === "retryable" && attempt < maxAttempts) {
|
|
27970
|
+
lastError = failErr;
|
|
27971
|
+
const delayMs = await handleRetryDelay(failErr, attempt, maxAttempts, baseDelayMs, maxDelayMs, jitterRatio, opts);
|
|
27972
|
+
if (delayMs < 0)
|
|
27973
|
+
throw lastError;
|
|
27974
|
+
continue;
|
|
27975
|
+
}
|
|
27976
|
+
throw failErr;
|
|
27977
|
+
}
|
|
27978
|
+
}
|
|
27979
|
+
return result;
|
|
27980
|
+
} catch (err) {
|
|
27981
|
+
if (err.name === "AbortError")
|
|
27982
|
+
throw err;
|
|
27983
|
+
const cls = classifyError(err);
|
|
27984
|
+
if (cls === "fatal" || attempt >= maxAttempts) {
|
|
27985
|
+
throw err;
|
|
27986
|
+
}
|
|
27987
|
+
lastError = err;
|
|
27988
|
+
const delayMs = await handleRetryDelay(err, attempt, maxAttempts, baseDelayMs, maxDelayMs, jitterRatio, opts);
|
|
27989
|
+
if (delayMs < 0) {
|
|
27990
|
+
const e = new Error("withLlmRetry: aborted during retry sleep");
|
|
27991
|
+
e.name = "AbortError";
|
|
27992
|
+
throw e;
|
|
27993
|
+
}
|
|
27994
|
+
}
|
|
27995
|
+
}
|
|
27996
|
+
throw lastError ?? new Error("withLlmRetry: unexpected loop exit");
|
|
27997
|
+
}
|
|
27998
|
+
async function handleRetryDelay(err, attempt, maxAttempts, baseDelayMs, maxDelayMs, jitterRatio, opts) {
|
|
27999
|
+
const getRetryAfterMs = opts.getRetryAfterMs ?? extractRetryAfterMs;
|
|
28000
|
+
const backoff = calcBackoff(attempt, baseDelayMs, maxDelayMs, jitterRatio);
|
|
28001
|
+
const retryAfterMs = getRetryAfterMs(err);
|
|
28002
|
+
let delayMs;
|
|
28003
|
+
if (retryAfterMs != null) {
|
|
28004
|
+
delayMs = retryAfterMs;
|
|
28005
|
+
} else {
|
|
28006
|
+
if (isLikelyRateLimitError(err)) {
|
|
28007
|
+
opts.log?.("warn", `[llm-retry] 429/rate-limit 但拿不到 Retry-After, 使用默认 backoff=${backoff}ms`, {
|
|
28008
|
+
err: getMsg(err)
|
|
28009
|
+
});
|
|
28010
|
+
}
|
|
28011
|
+
delayMs = backoff;
|
|
28012
|
+
}
|
|
28013
|
+
opts.onRetry?.(attempt, maxAttempts, err, delayMs);
|
|
28014
|
+
opts.log?.("warn", `[llm-retry] attempt ${attempt}/${maxAttempts} failed: ${getMsg(err)}, retry in ${delayMs}ms`, { err: getMsg(err), delayMs });
|
|
28015
|
+
return sleep2(delayMs, opts.signal);
|
|
28016
|
+
}
|
|
28017
|
+
function calcBackoff(attempt, baseMs, maxMs, jitterRatio) {
|
|
28018
|
+
const expo = Math.min(baseMs * Math.pow(2, attempt - 1), maxMs);
|
|
28019
|
+
const jitter = expo * jitterRatio * (Math.random() * 2 - 1);
|
|
28020
|
+
return Math.max(0, Math.round(expo + jitter));
|
|
28021
|
+
}
|
|
28022
|
+
function sleep2(ms, signal) {
|
|
28023
|
+
if (signal?.aborted)
|
|
28024
|
+
return Promise.resolve(-1);
|
|
28025
|
+
if (ms <= 0)
|
|
28026
|
+
return Promise.resolve(0);
|
|
28027
|
+
return new Promise((resolve16) => {
|
|
28028
|
+
let settled = false;
|
|
28029
|
+
const timer = setTimeout(() => {
|
|
28030
|
+
if (settled)
|
|
28031
|
+
return;
|
|
28032
|
+
settled = true;
|
|
28033
|
+
signal?.removeEventListener("abort", onAbort);
|
|
28034
|
+
resolve16(ms);
|
|
28035
|
+
}, ms);
|
|
28036
|
+
const onAbort = () => {
|
|
28037
|
+
if (settled)
|
|
28038
|
+
return;
|
|
28039
|
+
settled = true;
|
|
28040
|
+
clearTimeout(timer);
|
|
28041
|
+
resolve16(-1);
|
|
28042
|
+
};
|
|
28043
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
28044
|
+
});
|
|
28045
|
+
}
|
|
28046
|
+
function isLikelyRateLimitError(err) {
|
|
28047
|
+
if (!err)
|
|
28048
|
+
return false;
|
|
28049
|
+
const msg = getMsg(err).toLowerCase();
|
|
28050
|
+
return msg.includes("429") || msg.includes("rate limit") || msg.includes("too many requests");
|
|
28051
|
+
}
|
|
28052
|
+
function getMsg(err) {
|
|
28053
|
+
if (!err)
|
|
28054
|
+
return "";
|
|
28055
|
+
if (err instanceof Error)
|
|
28056
|
+
return err.message;
|
|
28057
|
+
if (typeof err === "string")
|
|
28058
|
+
return err;
|
|
28059
|
+
try {
|
|
28060
|
+
return JSON.stringify(err);
|
|
28061
|
+
} catch {
|
|
28062
|
+
return String(err);
|
|
28063
|
+
}
|
|
28064
|
+
}
|
|
28065
|
+
|
|
27569
28066
|
// lib/opencode-runner.ts
|
|
27570
28067
|
function pickLastText(parts) {
|
|
27571
28068
|
for (let i = parts.length - 1;i >= 0; i--) {
|
|
@@ -27699,66 +28196,85 @@ ${r.text.slice(0, 800)}`
|
|
|
27699
28196
|
return { ok: true, summary: r.text || "(coder 无文本输出)" };
|
|
27700
28197
|
}
|
|
27701
28198
|
async runSubagent(opts, parentSessionIdOverride) {
|
|
27702
|
-
|
|
27703
|
-
|
|
27704
|
-
|
|
27705
|
-
|
|
27706
|
-
|
|
27707
|
-
|
|
27708
|
-
|
|
27709
|
-
|
|
28199
|
+
const retryCfg = this.opts.retry;
|
|
28200
|
+
const retryOpts = {
|
|
28201
|
+
maxRetries: retryCfg?.maxRetries ?? DEFAULT_RETRY_CONFIG.maxRetries,
|
|
28202
|
+
baseDelayMs: retryCfg?.baseDelayMs ?? DEFAULT_RETRY_CONFIG.baseDelayMs,
|
|
28203
|
+
maxDelayMs: retryCfg?.maxDelayMs ?? DEFAULT_RETRY_CONFIG.maxDelayMs,
|
|
28204
|
+
jitterRatio: retryCfg?.jitterRatio ?? DEFAULT_RETRY_CONFIG.jitterRatio,
|
|
28205
|
+
minAttemptWindowMs: retryCfg?.minAttemptWindowMs ?? DEFAULT_RETRY_CONFIG.minAttemptWindowMs,
|
|
28206
|
+
maxTotalRetryMs: retryCfg?.maxTotalRetryMs ?? opts.timeoutMs,
|
|
28207
|
+
signal: opts.signal,
|
|
28208
|
+
log: this.opts.log,
|
|
28209
|
+
isRetryableResult: (result) => {
|
|
28210
|
+
if (result.llmError == null)
|
|
28211
|
+
return;
|
|
28212
|
+
const cls = classifyError(result.llmError);
|
|
28213
|
+
return cls === "retryable" ? new Error(describe4(result.llmError)) : undefined;
|
|
27710
28214
|
}
|
|
27711
|
-
|
|
27712
|
-
|
|
27713
|
-
|
|
27714
|
-
|
|
27715
|
-
|
|
27716
|
-
|
|
27717
|
-
this.opts.
|
|
27718
|
-
|
|
27719
|
-
|
|
27720
|
-
});
|
|
28215
|
+
};
|
|
28216
|
+
return withLlmRetry(async () => {
|
|
28217
|
+
let childId;
|
|
28218
|
+
try {
|
|
28219
|
+
const created = await this.opts.client.session.create({
|
|
28220
|
+
body: { title: clip2(opts.title, 80) },
|
|
28221
|
+
query: this.opts.directory ? { directory: this.opts.directory } : undefined
|
|
28222
|
+
});
|
|
28223
|
+
if (created.error || !created.data?.id) {
|
|
28224
|
+
throw new Error(`session.create 失败: ${describe4(created.error) || "no id"}`);
|
|
27721
28225
|
}
|
|
27722
|
-
|
|
27723
|
-
|
|
27724
|
-
|
|
27725
|
-
|
|
27726
|
-
|
|
27727
|
-
|
|
27728
|
-
|
|
27729
|
-
|
|
27730
|
-
|
|
27731
|
-
|
|
27732
|
-
|
|
27733
|
-
throw new Error(`session.prompt 失败: ${describe4(res.error) || "no data"}`);
|
|
27734
|
-
}
|
|
27735
|
-
const text = pickLastText(res.data.parts ?? []);
|
|
27736
|
-
const finishReason = (res.data.info?.finish ?? "").toLowerCase();
|
|
27737
|
-
const llmError = res.data.info?.error;
|
|
27738
|
-
return llmError !== undefined ? { text, finishReason, llmError } : { text, finishReason };
|
|
27739
|
-
} finally {
|
|
27740
|
-
if (childId) {
|
|
27741
|
-
try {
|
|
27742
|
-
await this.opts.client.session.delete({
|
|
27743
|
-
path: { id: childId },
|
|
27744
|
-
query: this.opts.directory ? { directory: this.opts.directory } : undefined
|
|
27745
|
-
});
|
|
27746
|
-
} catch (err) {
|
|
27747
|
-
this.opts.log?.("warn", `[spawner] session.delete 失败 ${childId}`, {
|
|
27748
|
-
err: describe4(err)
|
|
27749
|
-
});
|
|
28226
|
+
childId = created.data.id;
|
|
28227
|
+
const parentSessionId = parentSessionIdOverride ?? this.opts.parentSessionId ?? "";
|
|
28228
|
+
if (parentSessionId) {
|
|
28229
|
+
try {
|
|
28230
|
+
recordSessionParent(childId, parentSessionId);
|
|
28231
|
+
} catch (err) {
|
|
28232
|
+
this.opts.log?.("warn", `[spawner] recordSessionParent 失败 child=${childId}`, {
|
|
28233
|
+
err: describe4(err),
|
|
28234
|
+
parentSessionId
|
|
28235
|
+
});
|
|
28236
|
+
}
|
|
27750
28237
|
}
|
|
27751
|
-
const
|
|
27752
|
-
|
|
27753
|
-
|
|
27754
|
-
|
|
27755
|
-
|
|
28238
|
+
const promptPromise = Promise.resolve(this.opts.client.session.prompt({
|
|
28239
|
+
path: { id: childId },
|
|
28240
|
+
body: {
|
|
28241
|
+
agent: opts.agentName,
|
|
28242
|
+
parts: [{ type: "text", text: opts.prompt }]
|
|
28243
|
+
},
|
|
28244
|
+
query: this.opts.directory ? { directory: this.opts.directory } : undefined
|
|
28245
|
+
}));
|
|
28246
|
+
const res = await raceAbortTimeout(promptPromise, opts.signal, opts.timeoutMs, opts.title);
|
|
28247
|
+
if (res.error || !res.data) {
|
|
28248
|
+
throw new Error(`session.prompt 失败: ${describe4(res.error) || "no data"}`);
|
|
28249
|
+
}
|
|
28250
|
+
const text = pickLastText(res.data.parts ?? []);
|
|
28251
|
+
const finishReason = (res.data.info?.finish ?? "").toLowerCase();
|
|
28252
|
+
const llmError = res.data.info?.error;
|
|
28253
|
+
return llmError !== undefined ? { text, finishReason, llmError } : { text, finishReason };
|
|
28254
|
+
} finally {
|
|
28255
|
+
if (childId) {
|
|
28256
|
+
try {
|
|
28257
|
+
await this.opts.client.session.delete({
|
|
28258
|
+
path: { id: childId },
|
|
28259
|
+
query: this.opts.directory ? { directory: this.opts.directory } : undefined
|
|
28260
|
+
});
|
|
28261
|
+
} catch (err) {
|
|
28262
|
+
this.opts.log?.("warn", `[spawner] session.delete 失败 ${childId}`, {
|
|
27756
28263
|
err: describe4(err)
|
|
27757
28264
|
});
|
|
27758
|
-
}
|
|
28265
|
+
}
|
|
28266
|
+
const mainRoot = this.opts.mainRoot ?? this.opts.directory;
|
|
28267
|
+
if (mainRoot) {
|
|
28268
|
+
const cid = childId;
|
|
28269
|
+
discardSession({ sessionId: cid, mainRoot }).catch((err) => {
|
|
28270
|
+
this.opts.log?.("warn", `[spawner] auto-discard 失败 ${cid}`, {
|
|
28271
|
+
err: describe4(err)
|
|
28272
|
+
});
|
|
28273
|
+
});
|
|
28274
|
+
}
|
|
27759
28275
|
}
|
|
27760
28276
|
}
|
|
27761
|
-
}
|
|
28277
|
+
}, retryOpts);
|
|
27762
28278
|
}
|
|
27763
28279
|
}
|
|
27764
28280
|
async function raceAbortTimeout(p, signal, timeoutMs, label) {
|
|
@@ -27774,7 +28290,9 @@ async function raceAbortTimeout(p, signal, timeoutMs, label) {
|
|
|
27774
28290
|
return;
|
|
27775
28291
|
settled = true;
|
|
27776
28292
|
signal?.removeEventListener("abort", onAbort);
|
|
27777
|
-
|
|
28293
|
+
const timeoutErr = new Error(`${label} 超时 (${timeoutMs}ms)`);
|
|
28294
|
+
timeoutErr["__cfTimeout"] = true;
|
|
28295
|
+
reject(timeoutErr);
|
|
27778
28296
|
}, timeoutMs);
|
|
27779
28297
|
const onAbort = () => {
|
|
27780
28298
|
if (settled)
|
|
@@ -28335,10 +28853,11 @@ var codeforgeToolsServer = async (ctx) => {
|
|
|
28335
28853
|
browser_enabled: browserEnabled,
|
|
28336
28854
|
config_source: rt.ok ? "codeforge.json" : "built-in"
|
|
28337
28855
|
});
|
|
28856
|
+
const planStore = new PlanStore({ root: ctx.directory ?? process.cwd() });
|
|
28338
28857
|
__setContext2({
|
|
28339
28858
|
resolveCurrentSessionId: () => process.env["CODEFORGE_SESSION_ID"],
|
|
28340
28859
|
resolveMainRoot: () => ctx.directory ?? process.cwd(),
|
|
28341
|
-
store:
|
|
28860
|
+
store: planStore
|
|
28342
28861
|
});
|
|
28343
28862
|
const spawner = new ProductionSpawner({
|
|
28344
28863
|
client: ctx.client,
|
|
@@ -28349,7 +28868,8 @@ var codeforgeToolsServer = async (ctx) => {
|
|
|
28349
28868
|
__setContext({
|
|
28350
28869
|
mainRoot: ctx.directory ?? process.cwd(),
|
|
28351
28870
|
spawner,
|
|
28352
|
-
resolveCurrentSessionId: () => process.env["CODEFORGE_SESSION_ID"] ?? ""
|
|
28871
|
+
resolveCurrentSessionId: () => process.env["CODEFORGE_SESSION_ID"] ?? "",
|
|
28872
|
+
planStore
|
|
28353
28873
|
});
|
|
28354
28874
|
__setContext3({ mainRoot: ctx.directory ?? process.cwd() });
|
|
28355
28875
|
const browserTools = browserEnabled ? buildBrowserTools() : {};
|
|
@@ -28627,7 +29147,7 @@ var handler7 = codeforgeToolsServer;
|
|
|
28627
29147
|
|
|
28628
29148
|
// plugins/discover-spec-suggest.ts
|
|
28629
29149
|
import { readFileSync as readFileSync3, readdirSync, statSync as statSync2 } from "node:fs";
|
|
28630
|
-
import { join as
|
|
29150
|
+
import { join as join19 } from "node:path";
|
|
28631
29151
|
|
|
28632
29152
|
// node_modules/yaml/dist/index.js
|
|
28633
29153
|
var composer = require_composer();
|
|
@@ -28791,9 +29311,9 @@ function validateHandoff(rawYaml, fileSize) {
|
|
|
28791
29311
|
const result = HandoffSchema.safeParse(parsed);
|
|
28792
29312
|
if (!result.success) {
|
|
28793
29313
|
const first = result.error.issues[0];
|
|
28794
|
-
const
|
|
29314
|
+
const path22 = first?.path?.join(".") ?? "(root)";
|
|
28795
29315
|
const msg = first?.message ?? "unknown";
|
|
28796
|
-
return { ok: false, reason: `schema 校验失败:${
|
|
29316
|
+
return { ok: false, reason: `schema 校验失败:${path22}: ${msg}` };
|
|
28797
29317
|
}
|
|
28798
29318
|
return { ok: true, data: result.data, schemaVersion: result.data.schema_version };
|
|
28799
29319
|
}
|
|
@@ -28810,7 +29330,7 @@ var SESSION_TTL_MS2 = 24 * 60 * 60 * 1000;
|
|
|
28810
29330
|
var MATCH_THRESHOLD = 0.15;
|
|
28811
29331
|
var MAX_CANDIDATES = 3;
|
|
28812
29332
|
var NUDGE_MAX_LEN = 1500;
|
|
28813
|
-
var SPECS_REL_DIR =
|
|
29333
|
+
var SPECS_REL_DIR = join19("docs", "specs");
|
|
28814
29334
|
var sessionMap = new Map;
|
|
28815
29335
|
function pruneIfOversize2() {
|
|
28816
29336
|
while (sessionMap.size > SESSION_CAP2) {
|
|
@@ -28917,7 +29437,7 @@ function loadSpecs(rootDir, opts = {}) {
|
|
|
28917
29437
|
const dirExists = opts.dirExists ?? defaultDirExists;
|
|
28918
29438
|
const statReader = opts.statReader ?? defaultStatReader;
|
|
28919
29439
|
const log6 = makePluginLogger(PLUGIN_NAME8);
|
|
28920
|
-
const specsRoot =
|
|
29440
|
+
const specsRoot = join19(rootDir, SPECS_REL_DIR);
|
|
28921
29441
|
const records = [];
|
|
28922
29442
|
if (!dirExists(specsRoot)) {
|
|
28923
29443
|
log6.info(`specs 目录不存在,plugin 将 no-op`, { specsRoot });
|
|
@@ -28938,7 +29458,7 @@ function loadSpecs(rootDir, opts = {}) {
|
|
|
28938
29458
|
log6.info(`跳过非合法 slug 命名的条目`, { entry });
|
|
28939
29459
|
continue;
|
|
28940
29460
|
}
|
|
28941
|
-
const specDir =
|
|
29461
|
+
const specDir = join19(specsRoot, entry);
|
|
28942
29462
|
let dirStat;
|
|
28943
29463
|
try {
|
|
28944
29464
|
dirStat = statReader(specDir);
|
|
@@ -28951,7 +29471,7 @@ function loadSpecs(rootDir, opts = {}) {
|
|
|
28951
29471
|
}
|
|
28952
29472
|
if (!dirStat.isDirectory)
|
|
28953
29473
|
continue;
|
|
28954
|
-
const handoffPath =
|
|
29474
|
+
const handoffPath = join19(specDir, "handoff.yaml");
|
|
28955
29475
|
let fileStat;
|
|
28956
29476
|
try {
|
|
28957
29477
|
fileStat = statReader(handoffPath);
|
|
@@ -29123,14 +29643,14 @@ var discoverSpecSuggestServer = async (ctx) => {
|
|
|
29123
29643
|
var handler8 = discoverSpecSuggestServer;
|
|
29124
29644
|
|
|
29125
29645
|
// lib/memories.ts
|
|
29126
|
-
import { promises as
|
|
29127
|
-
import * as
|
|
29128
|
-
import * as
|
|
29646
|
+
import { promises as fs17 } from "node:fs";
|
|
29647
|
+
import * as path22 from "node:path";
|
|
29648
|
+
import * as os7 from "node:os";
|
|
29129
29649
|
function resolveConfig(c) {
|
|
29130
29650
|
return {
|
|
29131
29651
|
projectRoot: c.projectRoot,
|
|
29132
|
-
homeDir: c.homeDir ??
|
|
29133
|
-
projectName: c.projectName ??
|
|
29652
|
+
homeDir: c.homeDir ?? os7.homedir(),
|
|
29653
|
+
projectName: c.projectName ?? path22.basename(c.projectRoot),
|
|
29134
29654
|
now: c.now ?? Date.now,
|
|
29135
29655
|
log: c.log ?? (() => {}),
|
|
29136
29656
|
maxPerScope: c.maxPerScope ?? 1000
|
|
@@ -29138,13 +29658,13 @@ function resolveConfig(c) {
|
|
|
29138
29658
|
}
|
|
29139
29659
|
function fileFor(scope, cfg) {
|
|
29140
29660
|
if (scope === "project") {
|
|
29141
|
-
return
|
|
29661
|
+
return path22.join(cfg.projectRoot, ".codeforge", "memories.json");
|
|
29142
29662
|
}
|
|
29143
|
-
return
|
|
29663
|
+
return path22.join(cfg.homeDir, ".codeforge", "memories.json");
|
|
29144
29664
|
}
|
|
29145
29665
|
async function readBank(p) {
|
|
29146
29666
|
try {
|
|
29147
|
-
const raw = await
|
|
29667
|
+
const raw = await fs17.readFile(p, "utf8");
|
|
29148
29668
|
const arr = JSON.parse(raw);
|
|
29149
29669
|
if (!Array.isArray(arr))
|
|
29150
29670
|
return [];
|
|
@@ -29154,10 +29674,10 @@ async function readBank(p) {
|
|
|
29154
29674
|
}
|
|
29155
29675
|
}
|
|
29156
29676
|
async function writeBank(p, items) {
|
|
29157
|
-
await
|
|
29677
|
+
await fs17.mkdir(path22.dirname(p), { recursive: true });
|
|
29158
29678
|
const tmp = `${p}.tmp`;
|
|
29159
|
-
await
|
|
29160
|
-
await
|
|
29679
|
+
await fs17.writeFile(tmp, JSON.stringify(items, null, 2), "utf8");
|
|
29680
|
+
await fs17.rename(tmp, p);
|
|
29161
29681
|
}
|
|
29162
29682
|
function isMemory(x) {
|
|
29163
29683
|
if (!x || typeof x !== "object")
|
|
@@ -29675,8 +30195,8 @@ var handler10 = modelFallbackServer;
|
|
|
29675
30195
|
|
|
29676
30196
|
// plugins/parallel-tool-nudge.ts
|
|
29677
30197
|
import { readFileSync as readFileSync4, readdirSync as readdirSync2, statSync as statSync3 } from "node:fs";
|
|
29678
|
-
import { join as
|
|
29679
|
-
import { homedir as
|
|
30198
|
+
import { join as join21 } from "node:path";
|
|
30199
|
+
import { homedir as homedir8 } from "node:os";
|
|
29680
30200
|
var PLUGIN_NAME11 = "parallel-tool-nudge";
|
|
29681
30201
|
logLifecycle(PLUGIN_NAME11, "import", {});
|
|
29682
30202
|
var PARALLEL_SAFE_TOOLS = [
|
|
@@ -29728,10 +30248,10 @@ function loadAgentToolsMap(rootDir, opts = {}) {
|
|
|
29728
30248
|
const reader = opts.reader ?? defaultReader2;
|
|
29729
30249
|
const dirReader = opts.dirReader ?? defaultDirReader2;
|
|
29730
30250
|
const dirExists = opts.dirExists ?? defaultDirExists2;
|
|
29731
|
-
const homeAgentsDir = opts.homeAgentsDir ??
|
|
30251
|
+
const homeAgentsDir = opts.homeAgentsDir ?? join21(homedir8(), ".config", "opencode", "agents");
|
|
29732
30252
|
const candidateDirs = [
|
|
29733
|
-
|
|
29734
|
-
|
|
30253
|
+
join21(rootDir, ".codeforge", "agents"),
|
|
30254
|
+
join21(rootDir, "agents"),
|
|
29735
30255
|
homeAgentsDir
|
|
29736
30256
|
];
|
|
29737
30257
|
const result = new Map;
|
|
@@ -29754,20 +30274,20 @@ function loadAgentToolsMap(rootDir, opts = {}) {
|
|
|
29754
30274
|
for (const entry of entries) {
|
|
29755
30275
|
if (!entry.endsWith(".md"))
|
|
29756
30276
|
continue;
|
|
29757
|
-
const
|
|
30277
|
+
const path23 = join21(dir, entry);
|
|
29758
30278
|
let content;
|
|
29759
30279
|
try {
|
|
29760
|
-
content = reader(
|
|
30280
|
+
content = reader(path23);
|
|
29761
30281
|
} catch (err) {
|
|
29762
30282
|
log7.warn(`agent.md 读取失败(已跳过)`, {
|
|
29763
|
-
path:
|
|
30283
|
+
path: path23,
|
|
29764
30284
|
error: err instanceof Error ? err.message : String(err)
|
|
29765
30285
|
});
|
|
29766
30286
|
continue;
|
|
29767
30287
|
}
|
|
29768
30288
|
const parsed = parseAgentFrontmatter(content);
|
|
29769
30289
|
if (!parsed) {
|
|
29770
|
-
log7.warn(`agent frontmatter 解析失败(已跳过)`, { path:
|
|
30290
|
+
log7.warn(`agent frontmatter 解析失败(已跳过)`, { path: path23 });
|
|
29771
30291
|
continue;
|
|
29772
30292
|
}
|
|
29773
30293
|
if (result.has(parsed.name))
|
|
@@ -29958,18 +30478,18 @@ var handler12 = async (_ctx4) => {
|
|
|
29958
30478
|
};
|
|
29959
30479
|
|
|
29960
30480
|
// lib/event-stream.ts
|
|
29961
|
-
import { promises as
|
|
29962
|
-
import * as
|
|
30481
|
+
import { promises as fs18 } from "node:fs";
|
|
30482
|
+
import * as path23 from "node:path";
|
|
29963
30483
|
async function loadSession(id, opts = {}) {
|
|
29964
30484
|
const file2 = resolveSessionFile(id, opts);
|
|
29965
|
-
const raw = await
|
|
30485
|
+
const raw = await fs18.readFile(file2, "utf8");
|
|
29966
30486
|
return parseJsonl(id, raw);
|
|
29967
30487
|
}
|
|
29968
30488
|
async function listSessions(opts = {}) {
|
|
29969
30489
|
const dir = resolveDir(opts);
|
|
29970
30490
|
let entries;
|
|
29971
30491
|
try {
|
|
29972
|
-
entries = await
|
|
30492
|
+
entries = await fs18.readdir(dir, { withFileTypes: true });
|
|
29973
30493
|
} catch (err) {
|
|
29974
30494
|
if (err.code === "ENOENT")
|
|
29975
30495
|
return [];
|
|
@@ -29979,10 +30499,10 @@ async function listSessions(opts = {}) {
|
|
|
29979
30499
|
for (const e of entries) {
|
|
29980
30500
|
if (!e.isFile() || !e.name.endsWith(".jsonl"))
|
|
29981
30501
|
continue;
|
|
29982
|
-
const file2 =
|
|
30502
|
+
const file2 = path23.join(dir, e.name);
|
|
29983
30503
|
const id = e.name.replace(/\.jsonl$/, "");
|
|
29984
30504
|
try {
|
|
29985
|
-
const stat = await
|
|
30505
|
+
const stat = await fs18.stat(file2);
|
|
29986
30506
|
const headerLine = await readFirstLine(file2);
|
|
29987
30507
|
let started_at = stat.birthtimeMs;
|
|
29988
30508
|
if (headerLine) {
|
|
@@ -30006,11 +30526,11 @@ async function listSessions(opts = {}) {
|
|
|
30006
30526
|
return out;
|
|
30007
30527
|
}
|
|
30008
30528
|
function resolveDir(opts = {}) {
|
|
30009
|
-
const root =
|
|
30010
|
-
return opts.sessions_dir ?
|
|
30529
|
+
const root = path23.resolve(opts.root ?? process.cwd());
|
|
30530
|
+
return opts.sessions_dir ? path23.resolve(root, opts.sessions_dir) : path23.join(runtimeDir(root), "sessions");
|
|
30011
30531
|
}
|
|
30012
30532
|
function resolveSessionFile(id, opts = {}) {
|
|
30013
|
-
return
|
|
30533
|
+
return path23.join(resolveDir(opts), `${id}.jsonl`);
|
|
30014
30534
|
}
|
|
30015
30535
|
function parseJsonl(id, raw) {
|
|
30016
30536
|
const events = [];
|
|
@@ -30045,7 +30565,7 @@ function isEvent(obj) {
|
|
|
30045
30565
|
}
|
|
30046
30566
|
async function readFirstLine(file2) {
|
|
30047
30567
|
const buf = Buffer.alloc(4096);
|
|
30048
|
-
const fh = await
|
|
30568
|
+
const fh = await fs18.open(file2, "r");
|
|
30049
30569
|
try {
|
|
30050
30570
|
const { bytesRead } = await fh.read(buf, 0, buf.length, 0);
|
|
30051
30571
|
const s = buf.subarray(0, bytesRead).toString("utf8");
|
|
@@ -30273,11 +30793,11 @@ function isRecoveryWorthShowing(plan) {
|
|
|
30273
30793
|
}
|
|
30274
30794
|
|
|
30275
30795
|
// lib/block-pending.ts
|
|
30276
|
-
import { promises as
|
|
30277
|
-
import * as
|
|
30796
|
+
import { promises as fs19 } from "node:fs";
|
|
30797
|
+
import * as path24 from "node:path";
|
|
30278
30798
|
function blockPendingFilePath(absRoot) {
|
|
30279
30799
|
const rd = runtimeDir(absRoot, { ensure: false });
|
|
30280
|
-
return
|
|
30800
|
+
return path24.join(rd, "sessions", "autonomous-blocks.ndjson");
|
|
30281
30801
|
}
|
|
30282
30802
|
function consumeLockPath(absRoot) {
|
|
30283
30803
|
return blockPendingFilePath(absRoot) + ".consume.lock";
|
|
@@ -30286,7 +30806,7 @@ async function scanBlockPending(absRoot, filterSessionId) {
|
|
|
30286
30806
|
const file2 = blockPendingFilePath(absRoot);
|
|
30287
30807
|
let raw;
|
|
30288
30808
|
try {
|
|
30289
|
-
raw = await
|
|
30809
|
+
raw = await fs19.readFile(file2, "utf8");
|
|
30290
30810
|
} catch {
|
|
30291
30811
|
return [];
|
|
30292
30812
|
}
|
|
@@ -30342,7 +30862,7 @@ async function markBlocksConsumed(absRoot, entries) {
|
|
|
30342
30862
|
if (entries.length === 0)
|
|
30343
30863
|
return;
|
|
30344
30864
|
const file2 = blockPendingFilePath(absRoot);
|
|
30345
|
-
await
|
|
30865
|
+
await fs19.mkdir(path24.dirname(file2), { recursive: true });
|
|
30346
30866
|
const now = new Date().toISOString();
|
|
30347
30867
|
const lines = entries.map((e) => ({
|
|
30348
30868
|
type: "consume",
|
|
@@ -30353,7 +30873,7 @@ async function markBlocksConsumed(absRoot, entries) {
|
|
|
30353
30873
|
`) + `
|
|
30354
30874
|
`;
|
|
30355
30875
|
await withFileLock(consumeLockPath(absRoot), async () => {
|
|
30356
|
-
await
|
|
30876
|
+
await fs19.appendFile(file2, lines, "utf8");
|
|
30357
30877
|
});
|
|
30358
30878
|
}
|
|
30359
30879
|
|
|
@@ -30492,7 +31012,7 @@ var handler13 = sessionRecoveryServer;
|
|
|
30492
31012
|
|
|
30493
31013
|
// plugins/subtask-heartbeat.ts
|
|
30494
31014
|
import { promises as fsPromises } from "node:fs";
|
|
30495
|
-
import * as
|
|
31015
|
+
import * as path25 from "node:path";
|
|
30496
31016
|
var recordSessionParent2 = recordSessionParent;
|
|
30497
31017
|
var lookupParentSessionId2 = lookupParentSessionId;
|
|
30498
31018
|
var deleteSessionParent2 = deleteSessionParent;
|
|
@@ -30833,7 +31353,7 @@ function buildErrorLogLine(errorReason, elapsedMs, now = Date.now()) {
|
|
|
30833
31353
|
}
|
|
30834
31354
|
async function appendSubagentLog(filePath, line, log9) {
|
|
30835
31355
|
try {
|
|
30836
|
-
await fsPromises.mkdir(
|
|
31356
|
+
await fsPromises.mkdir(path25.dirname(filePath), { recursive: true });
|
|
30837
31357
|
await fsPromises.appendFile(filePath, line + `
|
|
30838
31358
|
`, "utf8");
|
|
30839
31359
|
} catch (err) {
|
|
@@ -31135,8 +31655,8 @@ var subtaskHeartbeatServer = async (ctx) => {
|
|
|
31135
31655
|
var handler14 = subtaskHeartbeatServer;
|
|
31136
31656
|
|
|
31137
31657
|
// plugins/subtasks.ts
|
|
31138
|
-
import { promises as
|
|
31139
|
-
import * as
|
|
31658
|
+
import { promises as fs20 } from "node:fs";
|
|
31659
|
+
import * as path26 from "node:path";
|
|
31140
31660
|
|
|
31141
31661
|
// lib/parallel-merge.ts
|
|
31142
31662
|
init_worktree_ops();
|
|
@@ -31144,7 +31664,7 @@ init_worktree_ops();
|
|
|
31144
31664
|
// plugins/subtasks.ts
|
|
31145
31665
|
var PLUGIN_NAME15 = "subtasks";
|
|
31146
31666
|
function getLogFile(root = process.cwd()) {
|
|
31147
|
-
return
|
|
31667
|
+
return path26.join(runtimeDir(root), "logs", "subtasks.log");
|
|
31148
31668
|
}
|
|
31149
31669
|
async function writeLog(level, msg, data) {
|
|
31150
31670
|
const line = JSON.stringify({
|
|
@@ -31157,8 +31677,8 @@ async function writeLog(level, msg, data) {
|
|
|
31157
31677
|
`;
|
|
31158
31678
|
try {
|
|
31159
31679
|
const logFile = getLogFile();
|
|
31160
|
-
await
|
|
31161
|
-
await
|
|
31680
|
+
await fs20.mkdir(path26.dirname(logFile), { recursive: true });
|
|
31681
|
+
await fs20.appendFile(logFile, line, "utf8");
|
|
31162
31682
|
} catch {}
|
|
31163
31683
|
}
|
|
31164
31684
|
logLifecycle(PLUGIN_NAME15, "import");
|
|
@@ -31732,8 +32252,8 @@ var tokenManagerServer = async (ctx) => {
|
|
|
31732
32252
|
var handler17 = tokenManagerServer;
|
|
31733
32253
|
|
|
31734
32254
|
// plugins/tool-policy.ts
|
|
31735
|
-
import { promises as
|
|
31736
|
-
import * as
|
|
32255
|
+
import { promises as fs21 } from "node:fs";
|
|
32256
|
+
import * as path28 from "node:path";
|
|
31737
32257
|
|
|
31738
32258
|
// lib/tool-risk.ts
|
|
31739
32259
|
var RISK_PATTERNS = [
|
|
@@ -31887,7 +32407,7 @@ function buildHaystackFor(args, matchOn) {
|
|
|
31887
32407
|
}
|
|
31888
32408
|
|
|
31889
32409
|
// lib/file-regex-acl.ts
|
|
31890
|
-
import * as
|
|
32410
|
+
import * as path27 from "node:path";
|
|
31891
32411
|
function compileRule(r) {
|
|
31892
32412
|
if (r instanceof RegExp)
|
|
31893
32413
|
return r;
|
|
@@ -31953,7 +32473,7 @@ function normalizePath(p) {
|
|
|
31953
32473
|
let s = p.replace(/\\/g, "/");
|
|
31954
32474
|
if (s.startsWith("./"))
|
|
31955
32475
|
s = s.slice(2);
|
|
31956
|
-
s =
|
|
32476
|
+
s = path27.posix.normalize(s);
|
|
31957
32477
|
return s;
|
|
31958
32478
|
}
|
|
31959
32479
|
function checkFileAccess(acl, file2, op) {
|
|
@@ -32056,11 +32576,11 @@ function decideToolCall(ctx, cfg = {}, currentAgent) {
|
|
|
32056
32576
|
const action = risks.length > 0 || worstAcl === "deny" ? "deny" : "allow";
|
|
32057
32577
|
return { action, reasons, risks, acl: aclResults };
|
|
32058
32578
|
}
|
|
32059
|
-
var POLICY_PATH =
|
|
32579
|
+
var POLICY_PATH = path28.join(".codeforge", "policy.json");
|
|
32060
32580
|
async function loadPolicy(root = process.cwd()) {
|
|
32061
|
-
const file2 =
|
|
32581
|
+
const file2 = path28.join(root, POLICY_PATH);
|
|
32062
32582
|
try {
|
|
32063
|
-
const raw = await
|
|
32583
|
+
const raw = await fs21.readFile(file2, "utf8");
|
|
32064
32584
|
const data = JSON.parse(raw);
|
|
32065
32585
|
return data;
|
|
32066
32586
|
} catch {
|
|
@@ -32157,20 +32677,20 @@ var handler18 = toolPolicyServer;
|
|
|
32157
32677
|
|
|
32158
32678
|
// plugins/update-checker.ts
|
|
32159
32679
|
import { closeSync, existsSync as existsSync5, mkdirSync as mkdirSync3, openSync, readFileSync as readFileSync6, rmSync, statSync as statSync4, writeFileSync as writeFileSync2, writeSync } from "node:fs";
|
|
32160
|
-
import { homedir as
|
|
32161
|
-
import { dirname as dirname16, join as
|
|
32680
|
+
import { homedir as homedir9 } from "node:os";
|
|
32681
|
+
import { dirname as dirname16, join as join27 } from "node:path";
|
|
32162
32682
|
import { spawn } from "node:child_process";
|
|
32163
32683
|
|
|
32164
32684
|
// lib/update-checker-impl.ts
|
|
32165
32685
|
import { readFileSync as readFileSync5 } from "node:fs";
|
|
32166
|
-
import { dirname as dirname15, join as
|
|
32686
|
+
import { dirname as dirname15, join as join26 } from "node:path";
|
|
32167
32687
|
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
32168
32688
|
import * as https from "node:https";
|
|
32169
32689
|
|
|
32170
32690
|
// lib/version-injected.ts
|
|
32171
32691
|
function getInjectedVersion() {
|
|
32172
32692
|
try {
|
|
32173
|
-
const v = "0.8.
|
|
32693
|
+
const v = "0.8.12";
|
|
32174
32694
|
if (typeof v === "string" && /^\d+\.\d+\.\d+/.test(v)) {
|
|
32175
32695
|
return v;
|
|
32176
32696
|
}
|
|
@@ -32218,7 +32738,7 @@ function readLocalVersion() {
|
|
|
32218
32738
|
try {
|
|
32219
32739
|
const here = fileURLToPath2(import.meta.url);
|
|
32220
32740
|
const root = dirname15(dirname15(here));
|
|
32221
|
-
const pkg = JSON.parse(readFileSync5(
|
|
32741
|
+
const pkg = JSON.parse(readFileSync5(join26(root, "package.json"), "utf8"));
|
|
32222
32742
|
return typeof pkg.version === "string" ? pkg.version : "0.0.0";
|
|
32223
32743
|
} catch {
|
|
32224
32744
|
return "0.0.0";
|
|
@@ -32298,7 +32818,7 @@ var PLUGIN_NAME19 = "update-checker";
|
|
|
32298
32818
|
var PLUGIN_VERSION = "3.0.0";
|
|
32299
32819
|
var _updateCheckStarted = false;
|
|
32300
32820
|
function getCacheFile() {
|
|
32301
|
-
return
|
|
32821
|
+
return join27(process.env["CODEFORGE_CACHE_DIR"] ?? join27(homedir9(), ".cache", "codeforge"), "update-check.json");
|
|
32302
32822
|
}
|
|
32303
32823
|
function readLastInstalledVersion() {
|
|
32304
32824
|
try {
|
|
@@ -32319,7 +32839,7 @@ function writeLastInstalledVersion(v) {
|
|
|
32319
32839
|
} catch {}
|
|
32320
32840
|
}
|
|
32321
32841
|
function getLockFile() {
|
|
32322
|
-
return
|
|
32842
|
+
return join27(process.env["CODEFORGE_CACHE_DIR"] ?? join27(homedir9(), ".cache", "codeforge"), "install.lock");
|
|
32323
32843
|
}
|
|
32324
32844
|
function tryAcquireInstallLock() {
|
|
32325
32845
|
try {
|
|
@@ -32512,7 +33032,7 @@ var updateCheckerServer = async (ctx) => {
|
|
|
32512
33032
|
try {
|
|
32513
33033
|
const npmRoot2 = await getNpmGlobalRoot(npmBin);
|
|
32514
33034
|
if (npmRoot2) {
|
|
32515
|
-
const oldPkgDir =
|
|
33035
|
+
const oldPkgDir = join27(npmRoot2, ...u.package.split("/"));
|
|
32516
33036
|
if (existsSync5(oldPkgDir)) {
|
|
32517
33037
|
rmSync(oldPkgDir, { recursive: true, force: true });
|
|
32518
33038
|
safeWriteLog(PLUGIN_NAME19, { level: "info", msg: "npm_install_old_dir_removed", path: oldPkgDir });
|
|
@@ -32530,8 +33050,8 @@ var updateCheckerServer = async (ctx) => {
|
|
|
32530
33050
|
}
|
|
32531
33051
|
safeWriteLog(PLUGIN_NAME19, { level: "info", msg: "npm_install_success", remote });
|
|
32532
33052
|
try {
|
|
32533
|
-
const cacheRoot = process.env["XDG_CACHE_HOME"] ??
|
|
32534
|
-
const opencodeCache =
|
|
33053
|
+
const cacheRoot = process.env["XDG_CACHE_HOME"] ?? join27(homedir9(), ".cache");
|
|
33054
|
+
const opencodeCache = join27(cacheRoot, "opencode", "packages", "@andyqiu", "codeforge@latest");
|
|
32535
33055
|
if (existsSync5(opencodeCache)) {
|
|
32536
33056
|
rmSync(opencodeCache, { recursive: true, force: true });
|
|
32537
33057
|
safeWriteLog(PLUGIN_NAME19, { level: "info", msg: "opencode_plugin_cache_cleared", path: opencodeCache });
|
|
@@ -32540,8 +33060,8 @@ var updateCheckerServer = async (ctx) => {
|
|
|
32540
33060
|
safeWriteLog(PLUGIN_NAME19, { level: "warn", msg: "opencode_plugin_cache_clear_failed", error: e.message });
|
|
32541
33061
|
}
|
|
32542
33062
|
const npmRoot = await getNpmGlobalRoot(npmBin);
|
|
32543
|
-
const pkgRoot = npmRoot ?
|
|
32544
|
-
const installMjs = pkgRoot ?
|
|
33063
|
+
const pkgRoot = npmRoot ? join27(npmRoot, "@andyqiu", "codeforge") : null;
|
|
33064
|
+
const installMjs = pkgRoot ? join27(pkgRoot, "install.mjs") : null;
|
|
32545
33065
|
if (!installMjs || !existsSync5(installMjs)) {
|
|
32546
33066
|
safeWriteLog(PLUGIN_NAME19, { level: "warn", msg: "install_mjs_not_found", path: installMjs ?? "null" });
|
|
32547
33067
|
await postToast(ctx, `[codeforge] ⚠ npm 包已升级 ${local} → ${remote},但资产部署未完成。下次启动将重试,或手动运行:codeforge upgrade`);
|
|
@@ -32580,12 +33100,195 @@ async function postToast(ctx, message) {
|
|
|
32580
33100
|
}
|
|
32581
33101
|
var handler19 = updateCheckerServer;
|
|
32582
33102
|
|
|
32583
|
-
// plugins/
|
|
33103
|
+
// plugins/user-merge-confirm.ts
|
|
33104
|
+
import { execFile as execFile4 } from "node:child_process";
|
|
33105
|
+
import { promisify as promisify2 } from "node:util";
|
|
33106
|
+
|
|
33107
|
+
// lib/merge-confirm-semantics.ts
|
|
33108
|
+
import { promises as fs22 } from "node:fs";
|
|
32584
33109
|
import * as path29 from "node:path";
|
|
33110
|
+
var RULES_REL_PATH = "workflows/_merge-confirm-semantics.yaml";
|
|
33111
|
+
async function loadMergeConfirmSemantics(repoRoot) {
|
|
33112
|
+
const file2 = path29.join(repoRoot, RULES_REL_PATH);
|
|
33113
|
+
const txt = await fs22.readFile(file2, "utf8");
|
|
33114
|
+
return $parse(txt);
|
|
33115
|
+
}
|
|
33116
|
+
function includesAny(haystack, needles) {
|
|
33117
|
+
const lower = haystack.toLowerCase();
|
|
33118
|
+
return needles.some((n) => lower.includes(n.toLowerCase()));
|
|
33119
|
+
}
|
|
33120
|
+
function stripAffirmatives(text, affirmatives) {
|
|
33121
|
+
let lower = text.toLowerCase();
|
|
33122
|
+
let matched = false;
|
|
33123
|
+
const sorted = [...affirmatives].sort((a, b) => b.length - a.length);
|
|
33124
|
+
for (const phrase of sorted) {
|
|
33125
|
+
const p = phrase.toLowerCase();
|
|
33126
|
+
if (lower.includes(p)) {
|
|
33127
|
+
matched = true;
|
|
33128
|
+
lower = lower.split(p).join(" ");
|
|
33129
|
+
}
|
|
33130
|
+
}
|
|
33131
|
+
return { matched, remainder: lower };
|
|
33132
|
+
}
|
|
33133
|
+
function classifyMergeConfirm(text, rules) {
|
|
33134
|
+
if (!text || !text.trim())
|
|
33135
|
+
return "NOT_CONFIRMED";
|
|
33136
|
+
const { matched, remainder } = stripAffirmatives(text, rules.affirmative);
|
|
33137
|
+
if (!matched)
|
|
33138
|
+
return "NOT_CONFIRMED";
|
|
33139
|
+
if (includesAny(remainder, rules.ambiguous_or_negative))
|
|
33140
|
+
return "NOT_CONFIRMED";
|
|
33141
|
+
if (rules.confirm_plus_new_request_is_not_confirmed && includesAny(remainder, rules.new_request_signals)) {
|
|
33142
|
+
return "NOT_CONFIRMED";
|
|
33143
|
+
}
|
|
33144
|
+
return "CONFIRMED";
|
|
33145
|
+
}
|
|
33146
|
+
|
|
33147
|
+
// plugins/user-merge-confirm.ts
|
|
33148
|
+
var PLUGIN_NAME20 = "user-merge-confirm";
|
|
33149
|
+
var execFileAsync = promisify2(execFile4);
|
|
33150
|
+
logLifecycle(PLUGIN_NAME20, "import", {});
|
|
33151
|
+
async function getWorktreeHead(worktreePath) {
|
|
33152
|
+
if (!worktreePath)
|
|
33153
|
+
return "";
|
|
33154
|
+
try {
|
|
33155
|
+
const { stdout } = await execFileAsync("git", ["rev-parse", "HEAD"], {
|
|
33156
|
+
cwd: worktreePath,
|
|
33157
|
+
timeout: 5000
|
|
33158
|
+
});
|
|
33159
|
+
return stdout.trim();
|
|
33160
|
+
} catch {
|
|
33161
|
+
return "";
|
|
33162
|
+
}
|
|
33163
|
+
}
|
|
33164
|
+
var userMergeConfirmPlugin = async (ctx) => {
|
|
33165
|
+
const mainRoot = ctx.directory;
|
|
33166
|
+
logLifecycle(PLUGIN_NAME20, "activate", { directory: mainRoot });
|
|
33167
|
+
return {
|
|
33168
|
+
"chat.message": async (input, _output) => {
|
|
33169
|
+
await safeAsync(PLUGIN_NAME20, "chat.message", async () => {
|
|
33170
|
+
if (input?.role !== "user")
|
|
33171
|
+
return;
|
|
33172
|
+
const sid = input?.sessionID;
|
|
33173
|
+
if (typeof sid !== "string" || !sid)
|
|
33174
|
+
return;
|
|
33175
|
+
const owner = await resolveWorktreeOwner({
|
|
33176
|
+
sessionId: sid,
|
|
33177
|
+
mainRoot
|
|
33178
|
+
}).catch(() => null);
|
|
33179
|
+
if (!owner || !owner.ok) {
|
|
33180
|
+
safeWriteLog(PLUGIN_NAME20, {
|
|
33181
|
+
hook: "chat.message",
|
|
33182
|
+
sid,
|
|
33183
|
+
action: "skip_registry_unreadable",
|
|
33184
|
+
via: owner?.via
|
|
33185
|
+
});
|
|
33186
|
+
return;
|
|
33187
|
+
}
|
|
33188
|
+
const targetRootSessionId = owner.ownerSessionId;
|
|
33189
|
+
if (sid !== targetRootSessionId) {
|
|
33190
|
+
safeWriteLog(PLUGIN_NAME20, {
|
|
33191
|
+
hook: "chat.message",
|
|
33192
|
+
sid,
|
|
33193
|
+
action: "skip_child_session",
|
|
33194
|
+
ownerSessionId: targetRootSessionId
|
|
33195
|
+
});
|
|
33196
|
+
return;
|
|
33197
|
+
}
|
|
33198
|
+
const text = extractUserText(input);
|
|
33199
|
+
if (!text?.trim())
|
|
33200
|
+
return;
|
|
33201
|
+
const entry = owner.entry;
|
|
33202
|
+
if (!entry || entry.status !== "active") {
|
|
33203
|
+
return;
|
|
33204
|
+
}
|
|
33205
|
+
if (entry.kind === "parallel-lane" || entry.kind === "merge-fix")
|
|
33206
|
+
return;
|
|
33207
|
+
let approval;
|
|
33208
|
+
try {
|
|
33209
|
+
const store = ApprovalStore.forProject(mainRoot);
|
|
33210
|
+
approval = await store.getLatest(`session:${targetRootSessionId}`);
|
|
33211
|
+
} catch {
|
|
33212
|
+
return;
|
|
33213
|
+
}
|
|
33214
|
+
if (!approval)
|
|
33215
|
+
return;
|
|
33216
|
+
if (approval.verdict !== "APPROVE" && approval.verdict !== "APPROVE_WITH_NOTES")
|
|
33217
|
+
return;
|
|
33218
|
+
if (!approval.coveredSha) {
|
|
33219
|
+
safeWriteLog(PLUGIN_NAME20, {
|
|
33220
|
+
hook: "chat.message",
|
|
33221
|
+
sid: targetRootSessionId,
|
|
33222
|
+
action: "no_covered_sha_early_exit"
|
|
33223
|
+
});
|
|
33224
|
+
return;
|
|
33225
|
+
}
|
|
33226
|
+
let mergeConfirmRules;
|
|
33227
|
+
try {
|
|
33228
|
+
const semantics = await loadMergeConfirmSemantics(mainRoot);
|
|
33229
|
+
mergeConfirmRules = semantics.merge_confirm;
|
|
33230
|
+
} catch {
|
|
33231
|
+
safeWriteLog(PLUGIN_NAME20, {
|
|
33232
|
+
hook: "chat.message",
|
|
33233
|
+
sid: targetRootSessionId,
|
|
33234
|
+
action: "skip_rules_load_failed"
|
|
33235
|
+
});
|
|
33236
|
+
return;
|
|
33237
|
+
}
|
|
33238
|
+
const verdict = classifyMergeConfirm(text, mergeConfirmRules);
|
|
33239
|
+
if (verdict !== "CONFIRMED") {
|
|
33240
|
+
safeWriteLog(PLUGIN_NAME20, {
|
|
33241
|
+
hook: "chat.message",
|
|
33242
|
+
sid: targetRootSessionId,
|
|
33243
|
+
action: "not_confirmed",
|
|
33244
|
+
text_preview: text.slice(0, 50)
|
|
33245
|
+
});
|
|
33246
|
+
return;
|
|
33247
|
+
}
|
|
33248
|
+
const currentHead = await getWorktreeHead(entry.worktreePath);
|
|
33249
|
+
if (!currentHead) {
|
|
33250
|
+
safeWriteLog(PLUGIN_NAME20, {
|
|
33251
|
+
hook: "chat.message",
|
|
33252
|
+
sid: targetRootSessionId,
|
|
33253
|
+
action: "skip_head_unavailable",
|
|
33254
|
+
worktreePath: entry.worktreePath
|
|
33255
|
+
});
|
|
33256
|
+
return;
|
|
33257
|
+
}
|
|
33258
|
+
await writeConfirmRecord(mainRoot, {
|
|
33259
|
+
sessionId: targetRootSessionId,
|
|
33260
|
+
worktreeHeadSha: currentHead,
|
|
33261
|
+
approvalSha: approval.coveredSha,
|
|
33262
|
+
ts: Date.now(),
|
|
33263
|
+
text,
|
|
33264
|
+
status: "PENDING"
|
|
33265
|
+
});
|
|
33266
|
+
safeWriteLog(PLUGIN_NAME20, {
|
|
33267
|
+
hook: "chat.message",
|
|
33268
|
+
sid: targetRootSessionId,
|
|
33269
|
+
action: "confirm_recorded",
|
|
33270
|
+
head: currentHead.slice(0, 8),
|
|
33271
|
+
approvalSha: approval.coveredSha.slice(0, 8)
|
|
33272
|
+
});
|
|
33273
|
+
safeWriteLog(PLUGIN_NAME20, {
|
|
33274
|
+
hook: "chat.message",
|
|
33275
|
+
sid: targetRootSessionId,
|
|
33276
|
+
action: "anchor_notice",
|
|
33277
|
+
anchor: `[CODEFORGE_CONFIRM_PENDING sid=${targetRootSessionId}]`
|
|
33278
|
+
});
|
|
33279
|
+
console.log(`[CODEFORGE_CONFIRM_PENDING sid=${targetRootSessionId}] ` + `已记录合入确认,codeforge 将执行合入。`);
|
|
33280
|
+
});
|
|
33281
|
+
}
|
|
33282
|
+
};
|
|
33283
|
+
};
|
|
33284
|
+
var handler20 = userMergeConfirmPlugin;
|
|
33285
|
+
|
|
33286
|
+
// plugins/workflow-engine.ts
|
|
33287
|
+
import * as path31 from "node:path";
|
|
32585
33288
|
|
|
32586
33289
|
// lib/workflow-loader.ts
|
|
32587
|
-
import { promises as
|
|
32588
|
-
import * as
|
|
33290
|
+
import { promises as fs23 } from "node:fs";
|
|
33291
|
+
import * as path30 from "node:path";
|
|
32589
33292
|
var ActionSchema = exports_external.object({
|
|
32590
33293
|
tool: exports_external.string().min(1, "action.tool 不能为空"),
|
|
32591
33294
|
args: exports_external.record(exports_external.string(), exports_external.unknown()).optional().default({}),
|
|
@@ -32670,7 +33373,7 @@ function parseWorkflowYaml(yaml, sourcePath = "<inline>") {
|
|
|
32670
33373
|
async function loadWorkflowFromFile(filePath) {
|
|
32671
33374
|
let txt;
|
|
32672
33375
|
try {
|
|
32673
|
-
txt = await
|
|
33376
|
+
txt = await fs23.readFile(filePath, "utf8");
|
|
32674
33377
|
} catch (err) {
|
|
32675
33378
|
return {
|
|
32676
33379
|
ok: false,
|
|
@@ -32685,7 +33388,7 @@ async function loadWorkflowsFromDir(dir) {
|
|
|
32685
33388
|
const failed = [];
|
|
32686
33389
|
let entries;
|
|
32687
33390
|
try {
|
|
32688
|
-
entries = await
|
|
33391
|
+
entries = await fs23.readdir(dir);
|
|
32689
33392
|
} catch (err) {
|
|
32690
33393
|
const e = err;
|
|
32691
33394
|
if (e.code === "ENOENT")
|
|
@@ -32697,7 +33400,7 @@ async function loadWorkflowsFromDir(dir) {
|
|
|
32697
33400
|
continue;
|
|
32698
33401
|
if (!/\.ya?ml$/i.test(name))
|
|
32699
33402
|
continue;
|
|
32700
|
-
const full =
|
|
33403
|
+
const full = path30.join(dir, name);
|
|
32701
33404
|
const r = await loadWorkflowFromFile(full);
|
|
32702
33405
|
if (r.ok)
|
|
32703
33406
|
loaded.push(r);
|
|
@@ -33032,9 +33735,9 @@ async function runStepAutoFeedback(step, adapter) {
|
|
|
33032
33735
|
}
|
|
33033
33736
|
|
|
33034
33737
|
// plugins/workflow-engine.ts
|
|
33035
|
-
var
|
|
33036
|
-
logLifecycle(
|
|
33037
|
-
var fallbackLog2 = makePluginLogger(
|
|
33738
|
+
var PLUGIN_NAME21 = "workflow-engine";
|
|
33739
|
+
logLifecycle(PLUGIN_NAME21, "import", {});
|
|
33740
|
+
var fallbackLog2 = makePluginLogger(PLUGIN_NAME21);
|
|
33038
33741
|
var _registry = null;
|
|
33039
33742
|
async function loadRegistry(workflowsDir) {
|
|
33040
33743
|
const { loaded, failed } = await loadWorkflowsFromDir(workflowsDir);
|
|
@@ -33054,32 +33757,32 @@ async function handleCommandInvoked(raw, workflowsDir = "workflows") {
|
|
|
33054
33757
|
const log13 = ctx.log ?? fallbackLog2;
|
|
33055
33758
|
const command = typeof ctx.command === "string" ? ctx.command : null;
|
|
33056
33759
|
if (!command) {
|
|
33057
|
-
log13.warn(`[${
|
|
33760
|
+
log13.warn(`[${PLUGIN_NAME21}] command.invoked 缺 command 字段`, ctx);
|
|
33058
33761
|
return null;
|
|
33059
33762
|
}
|
|
33060
33763
|
const reg = await ensureRegistry(workflowsDir);
|
|
33061
33764
|
if (reg.errors.length) {
|
|
33062
|
-
log13.warn(`[${
|
|
33765
|
+
log13.warn(`[${PLUGIN_NAME21}] 有 ${reg.errors.length} 个 workflow 加载失败`, reg.errors);
|
|
33063
33766
|
}
|
|
33064
33767
|
const wf = reg.workflows.find((w) => matchesTrigger(w, command));
|
|
33065
33768
|
if (!wf) {
|
|
33066
|
-
log13.info(`[${
|
|
33769
|
+
log13.info(`[${PLUGIN_NAME21}] no workflow matches "${command}"`);
|
|
33067
33770
|
return null;
|
|
33068
33771
|
}
|
|
33069
|
-
log13.info(`[${
|
|
33772
|
+
log13.info(`[${PLUGIN_NAME21}] dispatch "${command}" → workflow "${wf.name}"`);
|
|
33070
33773
|
try {
|
|
33071
33774
|
const result = await run(wf, {
|
|
33072
33775
|
mode: ctx.adapter ? "real" : "dry_run",
|
|
33073
33776
|
autonomy: ctx.autonomy ?? "semi",
|
|
33074
33777
|
adapter: ctx.adapter
|
|
33075
33778
|
});
|
|
33076
|
-
log13.info(`[${
|
|
33779
|
+
log13.info(`[${PLUGIN_NAME21}] workflow "${wf.name}" 完成 (${result.plan.mode})`, {
|
|
33077
33780
|
steps: result.plan.steps.length,
|
|
33078
33781
|
results: result.results.length
|
|
33079
33782
|
});
|
|
33080
33783
|
return result;
|
|
33081
33784
|
} catch (err) {
|
|
33082
|
-
log13.error(`[${
|
|
33785
|
+
log13.error(`[${PLUGIN_NAME21}] workflow "${wf.name}" 执行失败`, {
|
|
33083
33786
|
error: err instanceof Error ? err.message : String(err)
|
|
33084
33787
|
});
|
|
33085
33788
|
throw err;
|
|
@@ -33087,16 +33790,16 @@ async function handleCommandInvoked(raw, workflowsDir = "workflows") {
|
|
|
33087
33790
|
}
|
|
33088
33791
|
var workflowEngineServer = async (ctx) => {
|
|
33089
33792
|
const directory = ctx.directory ?? process.cwd();
|
|
33090
|
-
const workflowsDir =
|
|
33091
|
-
ensureRegistry(workflowsDir).catch((err) => fallbackLog2.warn(`[${
|
|
33793
|
+
const workflowsDir = path31.join(directory, "workflows");
|
|
33794
|
+
ensureRegistry(workflowsDir).catch((err) => fallbackLog2.warn(`[${PLUGIN_NAME21}] preload workflows failed`, {
|
|
33092
33795
|
error: err instanceof Error ? err.message : String(err)
|
|
33093
33796
|
}));
|
|
33094
|
-
logLifecycle(
|
|
33797
|
+
logLifecycle(PLUGIN_NAME21, "activate", { directory, workflowsDir });
|
|
33095
33798
|
return {
|
|
33096
33799
|
"command.execute.before": async (input, output) => {
|
|
33097
|
-
await safeAsync(
|
|
33800
|
+
await safeAsync(PLUGIN_NAME21, "command.execute.before", async () => {
|
|
33098
33801
|
const cmd = input.command.startsWith("/") ? input.command : `/${input.command}`;
|
|
33099
|
-
safeWriteLog(
|
|
33802
|
+
safeWriteLog(PLUGIN_NAME21, {
|
|
33100
33803
|
hook: "command.execute.before",
|
|
33101
33804
|
command: cmd,
|
|
33102
33805
|
sessionID: input.sessionID
|
|
@@ -33111,14 +33814,14 @@ var workflowEngineServer = async (ctx) => {
|
|
|
33111
33814
|
});
|
|
33112
33815
|
},
|
|
33113
33816
|
"chat.message": async (input, output) => {
|
|
33114
|
-
await safeAsync(
|
|
33817
|
+
await safeAsync(PLUGIN_NAME21, "chat.message", async () => {
|
|
33115
33818
|
const text = extractUserText(output).trim();
|
|
33116
33819
|
if (!text.startsWith("/"))
|
|
33117
33820
|
return;
|
|
33118
33821
|
const cmd = text.split(/\s+/)[0];
|
|
33119
33822
|
if (!cmd)
|
|
33120
33823
|
return;
|
|
33121
|
-
safeWriteLog(
|
|
33824
|
+
safeWriteLog(PLUGIN_NAME21, {
|
|
33122
33825
|
hook: "chat.message",
|
|
33123
33826
|
command: cmd,
|
|
33124
33827
|
sessionID: input.sessionID
|
|
@@ -33128,13 +33831,13 @@ var workflowEngineServer = async (ctx) => {
|
|
|
33128
33831
|
}
|
|
33129
33832
|
};
|
|
33130
33833
|
};
|
|
33131
|
-
var
|
|
33834
|
+
var handler21 = workflowEngineServer;
|
|
33132
33835
|
|
|
33133
33836
|
// plugins/session-worktree-guard.ts
|
|
33134
|
-
import
|
|
33837
|
+
import path32 from "node:path";
|
|
33135
33838
|
import { stat } from "node:fs/promises";
|
|
33136
|
-
var
|
|
33137
|
-
logLifecycle(
|
|
33839
|
+
var PLUGIN_NAME22 = "session-worktree-guard";
|
|
33840
|
+
logLifecycle(PLUGIN_NAME22, "import", {});
|
|
33138
33841
|
var WRITE_INTENT_RE = />(?![=&])(?!\s*\/dev\/(?:null|stdout|stderr|fd\/\d+)\b)|\btee\b|\brm\b|\bmv\b|\bcp\b|\bmkdir\b|\btouch\b|\bchmod\b|\bchown\b|\bln\b/;
|
|
33139
33842
|
var READ_ONLY_COMMANDS = /^\s*(?:ls|cat|head|tail|grep|rg|find|fd|wc|stat|file|which|whereis|echo|pwd|cd|pushd|popd|env|printenv|type|less|more|sort|uniq|awk|tr|cut|jq|date|whoami|id|uname|node|npx|tsc|diff|python3?|git(?:\s+-C\s+\S+)?\s+(?:log|show|diff|status|branch|tag|remote|config\s+--get|rev-parse|rev-list|ls-files|ls-tree|cat-file|describe|reflog|blame|shortlog|name-rev|symbolic-ref|merge-base|worktree\s+list|stash\s+list|stash\s+show))\b/;
|
|
33140
33843
|
var SIDE_EFFECT_TOKEN_RE = />(?![=&])(?!\s*\/dev\/(?:null|stdout|stderr|fd\/\d+)\b)|\|\s*tee\b|\btee\b/;
|
|
@@ -33269,28 +33972,28 @@ var MERGE_CALLER_WHITELIST = new Set([
|
|
|
33269
33972
|
var FORCE_MERGE_CALLER_WHITELIST = new Set([
|
|
33270
33973
|
"codeforge"
|
|
33271
33974
|
]);
|
|
33272
|
-
var CODEFORGE_WORKTREE_DIR_NAME =
|
|
33975
|
+
var CODEFORGE_WORKTREE_DIR_NAME = path32.join(".git", "codeforge-worktrees");
|
|
33273
33976
|
function worktreesRoot(mainRoot) {
|
|
33274
|
-
return
|
|
33977
|
+
return path32.join(mainRoot, CODEFORGE_WORKTREE_DIR_NAME);
|
|
33275
33978
|
}
|
|
33276
33979
|
function isInsideAnyWorktreeDir(absPath, mainRoot) {
|
|
33277
|
-
if (!
|
|
33980
|
+
if (!path32.isAbsolute(absPath))
|
|
33278
33981
|
return false;
|
|
33279
33982
|
const root = worktreesRoot(mainRoot);
|
|
33280
33983
|
if (absPath === root)
|
|
33281
33984
|
return false;
|
|
33282
|
-
const prefix = root.endsWith(
|
|
33985
|
+
const prefix = root.endsWith(path32.sep) ? root : root + path32.sep;
|
|
33283
33986
|
return absPath.startsWith(prefix);
|
|
33284
33987
|
}
|
|
33285
33988
|
function worktreeRootOf(absPath, mainRoot) {
|
|
33286
33989
|
const root = worktreesRoot(mainRoot);
|
|
33287
|
-
const prefix = root.endsWith(
|
|
33990
|
+
const prefix = root.endsWith(path32.sep) ? root : root + path32.sep;
|
|
33288
33991
|
if (!absPath.startsWith(prefix))
|
|
33289
33992
|
return null;
|
|
33290
|
-
const seg = absPath.slice(prefix.length).split(
|
|
33993
|
+
const seg = absPath.slice(prefix.length).split(path32.sep)[0];
|
|
33291
33994
|
if (!seg || seg === "..")
|
|
33292
33995
|
return null;
|
|
33293
|
-
return
|
|
33996
|
+
return path32.join(root, seg);
|
|
33294
33997
|
}
|
|
33295
33998
|
function stripPairedQuotes(raw) {
|
|
33296
33999
|
if (raw.length >= 2) {
|
|
@@ -33321,7 +34024,7 @@ function resolveExplicitWorktreeTarget(argsObj, mainRoot) {
|
|
|
33321
34024
|
for (const cand of candidates) {
|
|
33322
34025
|
if (!cand)
|
|
33323
34026
|
continue;
|
|
33324
|
-
const abs =
|
|
34027
|
+
const abs = path32.resolve(mainRoot, cand);
|
|
33325
34028
|
if (isInsideAnyWorktreeDir(abs, mainRoot)) {
|
|
33326
34029
|
const wtRoot = worktreeRootOf(abs, mainRoot);
|
|
33327
34030
|
if (wtRoot)
|
|
@@ -33334,14 +34037,14 @@ function resolveExplicitWorktreeTarget(argsObj, mainRoot) {
|
|
|
33334
34037
|
}
|
|
33335
34038
|
}
|
|
33336
34039
|
function stripSharedGitDirRef(command, mainRoot) {
|
|
33337
|
-
const escGitDir = escapeRegex2(
|
|
34040
|
+
const escGitDir = escapeRegex2(path32.join(mainRoot, ".git"));
|
|
33338
34041
|
const re = new RegExp(`--git-dir(?:=|\\s+)['"]?${escGitDir}(?:/[^\\s'"\\x60)]*)?['"]?`, "g");
|
|
33339
34042
|
return command.replace(re, " ");
|
|
33340
34043
|
}
|
|
33341
34044
|
function rewritePath(value, mainRoot, worktreeRoot) {
|
|
33342
34045
|
if (!value)
|
|
33343
34046
|
return null;
|
|
33344
|
-
const resolved =
|
|
34047
|
+
const resolved = path32.isAbsolute(value) ? value : path32.resolve(mainRoot, value);
|
|
33345
34048
|
const wtPrefix2 = worktreeRoot.endsWith("/") ? worktreeRoot : worktreeRoot + "/";
|
|
33346
34049
|
if (resolved === worktreeRoot || resolved.startsWith(wtPrefix2)) {
|
|
33347
34050
|
return null;
|
|
@@ -33379,7 +34082,7 @@ function commandContainsMainRootExcludingWorktree(command, mainRoot, worktreePat
|
|
|
33379
34082
|
}
|
|
33380
34083
|
}
|
|
33381
34084
|
const wtRoot = worktreesRoot(mainRoot);
|
|
33382
|
-
const wtRootPrefix = wtRoot +
|
|
34085
|
+
const wtRootPrefix = wtRoot + path32.sep;
|
|
33383
34086
|
const escapedWtRootPrefix = escapeRegex2(wtRootPrefix);
|
|
33384
34087
|
const wtPathPattern = escapedWtRootPrefix + `[^\\s'"\\x60)]*`;
|
|
33385
34088
|
const allWorktreePathsReForEscape = new RegExp(wtPathPattern, "g");
|
|
@@ -33434,19 +34137,19 @@ function collectWritePaths(toolName, argsObj, worktreeRoot) {
|
|
|
33434
34137
|
const candidate = toolName === "write" || toolName === "edit" ? argsObj["filePath"] : toolName === "ast_edit" ? argsObj["target"] : undefined;
|
|
33435
34138
|
if (typeof candidate !== "string" || candidate.length === 0)
|
|
33436
34139
|
return out;
|
|
33437
|
-
const abs =
|
|
33438
|
-
const rel =
|
|
34140
|
+
const abs = path32.isAbsolute(candidate) ? candidate : path32.resolve(worktreeRoot, candidate);
|
|
34141
|
+
const rel = path32.relative(worktreeRoot, abs).split(path32.sep).join("/");
|
|
33439
34142
|
out.push(rel);
|
|
33440
34143
|
return out;
|
|
33441
34144
|
}
|
|
33442
|
-
var log13 = makePluginLogger(
|
|
34145
|
+
var log13 = makePluginLogger(PLUGIN_NAME22);
|
|
33443
34146
|
async function isCodeforgeManagedProject(mainRoot, opts = {}) {
|
|
33444
34147
|
const env = opts.env ?? process.env;
|
|
33445
34148
|
const force = env["CODEFORGE_FORCE_WORKTREE_GUARD"];
|
|
33446
34149
|
if (force === "1" || force === "true" || force === "yes")
|
|
33447
34150
|
return true;
|
|
33448
34151
|
try {
|
|
33449
|
-
const st = await stat(
|
|
34152
|
+
const st = await stat(path32.join(mainRoot, ".codeforge"));
|
|
33450
34153
|
return st.isDirectory();
|
|
33451
34154
|
} catch {
|
|
33452
34155
|
return false;
|
|
@@ -33476,11 +34179,11 @@ var STALE_MERGE_HEAD_MS = 24 * 60 * 60000;
|
|
|
33476
34179
|
var _staleMergeHeadWarned = false;
|
|
33477
34180
|
var _mergeBypassToastShown = false;
|
|
33478
34181
|
async function isMainRepoMidMerge(mainRoot, opts = {}) {
|
|
33479
|
-
const gitDir =
|
|
34182
|
+
const gitDir = path32.join(mainRoot, ".git");
|
|
33480
34183
|
const markers = [
|
|
33481
|
-
|
|
33482
|
-
|
|
33483
|
-
|
|
34184
|
+
path32.join(gitDir, "MERGE_HEAD"),
|
|
34185
|
+
path32.join(gitDir, "rebase-merge"),
|
|
34186
|
+
path32.join(gitDir, "CHERRY_PICK_HEAD")
|
|
33484
34187
|
];
|
|
33485
34188
|
let freshest = -1;
|
|
33486
34189
|
for (const m of markers) {
|
|
@@ -33507,25 +34210,25 @@ var sessionWorktreeGuardPlugin = async (ctx) => {
|
|
|
33507
34210
|
const disableEnv = process.env["CODEFORGE_DISABLE_WORKTREE_GUARD"];
|
|
33508
34211
|
if (disableEnv === "1" || disableEnv === "true" || disableEnv === "yes") {
|
|
33509
34212
|
log13.warn("[guard] CODEFORGE_DISABLE_WORKTREE_GUARD 已启用,session-worktree-guard 全部 hook 跳过;" + "本次 opencode 会话所有写操作将直接落到主工作区(失去隔离保护)", { env: disableEnv });
|
|
33510
|
-
safeWriteLog(
|
|
34213
|
+
safeWriteLog(PLUGIN_NAME22, {
|
|
33511
34214
|
hook: "activate",
|
|
33512
34215
|
action: "skip",
|
|
33513
34216
|
source: "disable-env",
|
|
33514
34217
|
env_value: disableEnv
|
|
33515
34218
|
});
|
|
33516
|
-
logLifecycle(
|
|
34219
|
+
logLifecycle(PLUGIN_NAME22, "activate", { disabled_by_env: true });
|
|
33517
34220
|
return {};
|
|
33518
34221
|
}
|
|
33519
34222
|
const mainRoot = resolveMainRoot2(ctx.directory ?? process.cwd());
|
|
33520
34223
|
if (!await isCodeforgeManagedProject(mainRoot)) {
|
|
33521
34224
|
log13.info("[guard] 当前项目无 .codeforge/ 目录,session-worktree-guard 不启用;" + "如需在此项目启用 worktree 隔离:mkdir .codeforge", { mainRoot });
|
|
33522
|
-
safeWriteLog(
|
|
34225
|
+
safeWriteLog(PLUGIN_NAME22, {
|
|
33523
34226
|
hook: "activate",
|
|
33524
34227
|
action: "skip",
|
|
33525
34228
|
source: "non-codeforge-project",
|
|
33526
34229
|
mainRoot
|
|
33527
34230
|
});
|
|
33528
|
-
logLifecycle(
|
|
34231
|
+
logLifecycle(PLUGIN_NAME22, "activate", { skipped: "non-codeforge-project", mainRoot });
|
|
33529
34232
|
return {};
|
|
33530
34233
|
}
|
|
33531
34234
|
let policyCfg = {};
|
|
@@ -33538,7 +34241,7 @@ var sessionWorktreeGuardPlugin = async (ctx) => {
|
|
|
33538
34241
|
});
|
|
33539
34242
|
}
|
|
33540
34243
|
const perAgentEnabled = !!policyCfg.per_agent && Object.keys(policyCfg.per_agent).length > 0;
|
|
33541
|
-
logLifecycle(
|
|
34244
|
+
logLifecycle(PLUGIN_NAME22, "activate", {
|
|
33542
34245
|
mainRoot,
|
|
33543
34246
|
CODEFORGE_SESSION_ID: process.env["CODEFORGE_SESSION_ID"] ?? "(not set)"
|
|
33544
34247
|
});
|
|
@@ -33554,7 +34257,7 @@ var sessionWorktreeGuardPlugin = async (ctx) => {
|
|
|
33554
34257
|
opencode_version_hint: "需 opencode >= 0.x 才会在 tool.execute.before 注入 input.sessionID"
|
|
33555
34258
|
});
|
|
33556
34259
|
}
|
|
33557
|
-
safeWriteLog(
|
|
34260
|
+
safeWriteLog(PLUGIN_NAME22, {
|
|
33558
34261
|
hook: "tool.execute.before",
|
|
33559
34262
|
tool: input.tool,
|
|
33560
34263
|
action: "skip",
|
|
@@ -33564,12 +34267,12 @@ var sessionWorktreeGuardPlugin = async (ctx) => {
|
|
|
33564
34267
|
return;
|
|
33565
34268
|
}
|
|
33566
34269
|
let denied;
|
|
33567
|
-
await safeAsync(
|
|
34270
|
+
await safeAsync(PLUGIN_NAME22, "tool.execute.before", async () => {
|
|
33568
34271
|
const toolName = input.tool;
|
|
33569
34272
|
const argsObj = output.args ?? {};
|
|
33570
34273
|
const midMerge = isWriteOperation(toolName, argsObj, mainRoot) || toolName === "bash" ? await isMainRepoMidMerge(mainRoot, { log: log13 }) : false;
|
|
33571
34274
|
const emitMergeBypassNotice = (subClass, detail) => {
|
|
33572
|
-
safeWriteLog(
|
|
34275
|
+
safeWriteLog(PLUGIN_NAME22, {
|
|
33573
34276
|
hook: "tool.execute.before",
|
|
33574
34277
|
tool: toolName,
|
|
33575
34278
|
sessionID: input.sessionID,
|
|
@@ -33602,7 +34305,7 @@ var sessionWorktreeGuardPlugin = async (ctx) => {
|
|
|
33602
34305
|
tool: toolName,
|
|
33603
34306
|
error: errMsg
|
|
33604
34307
|
});
|
|
33605
|
-
safeWriteLog(
|
|
34308
|
+
safeWriteLog(PLUGIN_NAME22, {
|
|
33606
34309
|
hook: "tool.execute.before",
|
|
33607
34310
|
tool: toolName,
|
|
33608
34311
|
sessionID: input.sessionID,
|
|
@@ -33619,7 +34322,7 @@ var sessionWorktreeGuardPlugin = async (ctx) => {
|
|
|
33619
34322
|
tool: toolName,
|
|
33620
34323
|
error: errMsg
|
|
33621
34324
|
});
|
|
33622
|
-
safeWriteLog(
|
|
34325
|
+
safeWriteLog(PLUGIN_NAME22, {
|
|
33623
34326
|
hook: "tool.execute.before",
|
|
33624
34327
|
tool: toolName,
|
|
33625
34328
|
sessionID: input.sessionID,
|
|
@@ -33646,7 +34349,7 @@ var sessionWorktreeGuardPlugin = async (ctx) => {
|
|
|
33646
34349
|
if (parentEntry && parentEntry.status === "active") {
|
|
33647
34350
|
entry = parentEntry;
|
|
33648
34351
|
log13.debug?.(`[child-inherit] session ${sessionId} 继承父 ${parentId} 的 worktree`, { parentSessionId: parentId, worktreePath: parentEntry.worktreePath });
|
|
33649
|
-
safeWriteLog(
|
|
34352
|
+
safeWriteLog(PLUGIN_NAME22, {
|
|
33650
34353
|
hook: "tool.execute.before",
|
|
33651
34354
|
tool: toolName,
|
|
33652
34355
|
sessionID: input.sessionID,
|
|
@@ -33658,7 +34361,7 @@ var sessionWorktreeGuardPlugin = async (ctx) => {
|
|
|
33658
34361
|
} else if (!parentEntry) {
|
|
33659
34362
|
entry = await bindSessionWorktree({ sessionId: parentId, mainRoot });
|
|
33660
34363
|
log13.info(`[child-inherit-bind-root] session ${sessionId} 触发父 ${parentId} 的 worktree lazy-bind`, { parentSessionId: parentId, branch: entry.branch, worktreePath: entry.worktreePath });
|
|
33661
|
-
safeWriteLog(
|
|
34364
|
+
safeWriteLog(PLUGIN_NAME22, {
|
|
33662
34365
|
hook: "tool.execute.before",
|
|
33663
34366
|
tool: toolName,
|
|
33664
34367
|
sessionID: input.sessionID,
|
|
@@ -33677,7 +34380,7 @@ var sessionWorktreeGuardPlugin = async (ctx) => {
|
|
|
33677
34380
|
try {
|
|
33678
34381
|
entry = await bindSessionWorktree({ sessionId, mainRoot });
|
|
33679
34382
|
log13.info(`[lazy-bind] auto-created worktree for session ${sessionId}`, { branch: entry.branch, path: entry.worktreePath });
|
|
33680
|
-
safeWriteLog(
|
|
34383
|
+
safeWriteLog(PLUGIN_NAME22, {
|
|
33681
34384
|
hook: "tool.execute.before",
|
|
33682
34385
|
tool: toolName,
|
|
33683
34386
|
sessionID: input.sessionID,
|
|
@@ -33702,7 +34405,7 @@ var sessionWorktreeGuardPlugin = async (ctx) => {
|
|
|
33702
34405
|
error: errMsg,
|
|
33703
34406
|
throttled: alreadyNotified
|
|
33704
34407
|
});
|
|
33705
|
-
safeWriteLog(
|
|
34408
|
+
safeWriteLog(PLUGIN_NAME22, {
|
|
33706
34409
|
hook: "tool.execute.before",
|
|
33707
34410
|
tool: toolName,
|
|
33708
34411
|
sessionID: input.sessionID,
|
|
@@ -33746,7 +34449,7 @@ var sessionWorktreeGuardPlugin = async (ctx) => {
|
|
|
33746
34449
|
caller: null,
|
|
33747
34450
|
force: true
|
|
33748
34451
|
});
|
|
33749
|
-
safeWriteLog(
|
|
34452
|
+
safeWriteLog(PLUGIN_NAME22, {
|
|
33750
34453
|
hook: "tool.execute.before",
|
|
33751
34454
|
tool: toolName,
|
|
33752
34455
|
sessionID: input.sessionID,
|
|
@@ -33767,7 +34470,7 @@ var sessionWorktreeGuardPlugin = async (ctx) => {
|
|
|
33767
34470
|
action,
|
|
33768
34471
|
caller
|
|
33769
34472
|
});
|
|
33770
|
-
safeWriteLog(
|
|
34473
|
+
safeWriteLog(PLUGIN_NAME22, {
|
|
33771
34474
|
hook: "tool.execute.before",
|
|
33772
34475
|
tool: toolName,
|
|
33773
34476
|
sessionID: input.sessionID,
|
|
@@ -33788,7 +34491,7 @@ var sessionWorktreeGuardPlugin = async (ctx) => {
|
|
|
33788
34491
|
caller,
|
|
33789
34492
|
force: true
|
|
33790
34493
|
});
|
|
33791
|
-
safeWriteLog(
|
|
34494
|
+
safeWriteLog(PLUGIN_NAME22, {
|
|
33792
34495
|
hook: "tool.execute.before",
|
|
33793
34496
|
tool: toolName,
|
|
33794
34497
|
sessionID: input.sessionID,
|
|
@@ -33812,7 +34515,7 @@ var sessionWorktreeGuardPlugin = async (ctx) => {
|
|
|
33812
34515
|
tool: toolName,
|
|
33813
34516
|
source: "per-agent-acl-fail-closed"
|
|
33814
34517
|
});
|
|
33815
|
-
safeWriteLog(
|
|
34518
|
+
safeWriteLog(PLUGIN_NAME22, {
|
|
33816
34519
|
hook: "tool.execute.before",
|
|
33817
34520
|
tool: toolName,
|
|
33818
34521
|
sessionID: input.sessionID,
|
|
@@ -33837,7 +34540,7 @@ var sessionWorktreeGuardPlugin = async (ctx) => {
|
|
|
33837
34540
|
path: relPath,
|
|
33838
34541
|
acl_decision: dec
|
|
33839
34542
|
});
|
|
33840
|
-
safeWriteLog(
|
|
34543
|
+
safeWriteLog(PLUGIN_NAME22, {
|
|
33841
34544
|
hook: "tool.execute.before",
|
|
33842
34545
|
tool: toolName,
|
|
33843
34546
|
sessionID: input.sessionID,
|
|
@@ -33881,7 +34584,7 @@ var sessionWorktreeGuardPlugin = async (ctx) => {
|
|
|
33881
34584
|
requiredPlanId: entry.requiredPlanId,
|
|
33882
34585
|
inheritedFromParent: inherited ? entry.sessionId : undefined
|
|
33883
34586
|
});
|
|
33884
|
-
safeWriteLog(
|
|
34587
|
+
safeWriteLog(PLUGIN_NAME22, {
|
|
33885
34588
|
hook: "tool.execute.before",
|
|
33886
34589
|
tool: toolName,
|
|
33887
34590
|
sessionID: input.sessionID,
|
|
@@ -33903,7 +34606,7 @@ var sessionWorktreeGuardPlugin = async (ctx) => {
|
|
|
33903
34606
|
if (!stillHit) {
|
|
33904
34607
|
hitMainRoot = false;
|
|
33905
34608
|
if (detectBashWriteIntent(command, mainRoot)) {
|
|
33906
|
-
safeWriteLog(
|
|
34609
|
+
safeWriteLog(PLUGIN_NAME22, {
|
|
33907
34610
|
hook: "tool.execute.before",
|
|
33908
34611
|
tool: toolName,
|
|
33909
34612
|
sessionID: input.sessionID,
|
|
@@ -33924,7 +34627,7 @@ var sessionWorktreeGuardPlugin = async (ctx) => {
|
|
|
33924
34627
|
const caller = await resolveAgentForGuard({ sessionID: input.sessionID, agent: input.agent }, ctx.client, log13);
|
|
33925
34628
|
if (caller !== null && CLASS_B_CALLER_WHITELIST.has(caller)) {
|
|
33926
34629
|
log13.debug?.(`[class-b-whitelist] allow caller=${caller}`, { sessionId, tool: toolName, command: command.slice(0, 200) });
|
|
33927
|
-
safeWriteLog(
|
|
34630
|
+
safeWriteLog(PLUGIN_NAME22, {
|
|
33928
34631
|
hook: "tool.execute.before",
|
|
33929
34632
|
tool: toolName,
|
|
33930
34633
|
sessionID: input.sessionID,
|
|
@@ -33938,7 +34641,7 @@ var sessionWorktreeGuardPlugin = async (ctx) => {
|
|
|
33938
34641
|
const snippet = command.length > 60 ? command.slice(0, 60) + "…" : command;
|
|
33939
34642
|
const reason = `[session-worktree-guard] DENIED: bash.command 含主仓绝对路径写操作 (${snippet}) [caller=${callerTag}],请在当前 session worktree (${worktreePath}) 内操作`;
|
|
33940
34643
|
log13.warn(reason, { sessionId, caller: callerTag, command: command.slice(0, 200) });
|
|
33941
|
-
safeWriteLog(
|
|
34644
|
+
safeWriteLog(PLUGIN_NAME22, {
|
|
33942
34645
|
hook: "tool.execute.before",
|
|
33943
34646
|
tool: toolName,
|
|
33944
34647
|
sessionID: input.sessionID,
|
|
@@ -33962,7 +34665,7 @@ var sessionWorktreeGuardPlugin = async (ctx) => {
|
|
|
33962
34665
|
return;
|
|
33963
34666
|
}
|
|
33964
34667
|
log13.info(`rewrote ${toolName}.${field}: ${value} → ${newPath}`);
|
|
33965
|
-
safeWriteLog(
|
|
34668
|
+
safeWriteLog(PLUGIN_NAME22, {
|
|
33966
34669
|
hook: "tool.execute.before",
|
|
33967
34670
|
tool: toolName,
|
|
33968
34671
|
field,
|
|
@@ -33993,7 +34696,7 @@ var sessionWorktreeGuardPlugin = async (ctx) => {
|
|
|
33993
34696
|
} else if (!midMerge && entry.status === "active") {
|
|
33994
34697
|
output.args["workdir"] = worktreePath;
|
|
33995
34698
|
log13.info(`injected default bash.workdir → ${worktreePath}`);
|
|
33996
|
-
safeWriteLog(
|
|
34699
|
+
safeWriteLog(PLUGIN_NAME22, {
|
|
33997
34700
|
hook: "tool.execute.before",
|
|
33998
34701
|
tool: toolName,
|
|
33999
34702
|
field: "workdir",
|
|
@@ -34009,7 +34712,7 @@ var sessionWorktreeGuardPlugin = async (ctx) => {
|
|
|
34009
34712
|
const newReadPath = rewritePath(filePath, mainRoot, worktreePath);
|
|
34010
34713
|
if (newReadPath !== null) {
|
|
34011
34714
|
output.args["filePath"] = newReadPath;
|
|
34012
|
-
safeWriteLog(
|
|
34715
|
+
safeWriteLog(PLUGIN_NAME22, {
|
|
34013
34716
|
hook: "tool.execute.before",
|
|
34014
34717
|
tool: toolName,
|
|
34015
34718
|
field: "filePath",
|
|
@@ -34033,7 +34736,7 @@ var sessionWorktreeGuardPlugin = async (ctx) => {
|
|
|
34033
34736
|
throw denied;
|
|
34034
34737
|
},
|
|
34035
34738
|
"tool.execute.after": async (input, output) => {
|
|
34036
|
-
await safeAsync(
|
|
34739
|
+
await safeAsync(PLUGIN_NAME22, "tool.execute.after", async () => {
|
|
34037
34740
|
const callID = input.callID;
|
|
34038
34741
|
if (!callID)
|
|
34039
34742
|
return;
|
|
@@ -34045,7 +34748,7 @@ var sessionWorktreeGuardPlugin = async (ctx) => {
|
|
|
34045
34748
|
output.output = warning + `
|
|
34046
34749
|
|
|
34047
34750
|
` + prev;
|
|
34048
|
-
safeWriteLog(
|
|
34751
|
+
safeWriteLog(PLUGIN_NAME22, {
|
|
34049
34752
|
hook: "tool.execute.after",
|
|
34050
34753
|
tool: notice.tool,
|
|
34051
34754
|
sessionID: input.sessionID,
|
|
@@ -34056,12 +34759,12 @@ var sessionWorktreeGuardPlugin = async (ctx) => {
|
|
|
34056
34759
|
}
|
|
34057
34760
|
};
|
|
34058
34761
|
};
|
|
34059
|
-
var
|
|
34762
|
+
var handler22 = sessionWorktreeGuardPlugin;
|
|
34060
34763
|
|
|
34061
34764
|
// plugins/worktree-lifecycle.ts
|
|
34062
34765
|
init_worktree_ops();
|
|
34063
|
-
var
|
|
34064
|
-
logLifecycle(
|
|
34766
|
+
var PLUGIN_NAME23 = "worktree-lifecycle";
|
|
34767
|
+
logLifecycle(PLUGIN_NAME23, "import", {});
|
|
34065
34768
|
var IDLE_TOAST_THROTTLE_MS = 60 * 60000;
|
|
34066
34769
|
var IDLE_TOAST_DURATION_MS = 8000;
|
|
34067
34770
|
var IDLE_TOAST_REMINDER_INTERVAL_MS = 30 * 60000;
|
|
@@ -34078,7 +34781,7 @@ async function maybeNotifyRegistryUnreadable(client, rec, prune) {
|
|
|
34078
34781
|
_registryUnreadableNotified = false;
|
|
34079
34782
|
return;
|
|
34080
34783
|
}
|
|
34081
|
-
safeWriteLog(
|
|
34784
|
+
safeWriteLog(PLUGIN_NAME23, {
|
|
34082
34785
|
hook: "registry-unreadable",
|
|
34083
34786
|
reason: unreadable.reason,
|
|
34084
34787
|
detail: unreadable.detail,
|
|
@@ -34097,11 +34800,11 @@ async function maybeNotifyRegistryUnreadable(client, rec, prune) {
|
|
|
34097
34800
|
return;
|
|
34098
34801
|
await showToast2(client, { message: msg, variant: "warning", duration: 1e4, title: "CodeForge" }, log14).catch(() => {});
|
|
34099
34802
|
}
|
|
34100
|
-
var log14 = makePluginLogger(
|
|
34803
|
+
var log14 = makePluginLogger(PLUGIN_NAME23);
|
|
34101
34804
|
var worktreeLifecyclePlugin = async (ctx) => {
|
|
34102
34805
|
const rawDir = ctx.directory ?? process.cwd();
|
|
34103
34806
|
const mainRoot = resolveMainRoot2(rawDir);
|
|
34104
|
-
logLifecycle(
|
|
34807
|
+
logLifecycle(PLUGIN_NAME23, "activate", {
|
|
34105
34808
|
mainRoot: mainRoot ?? "(not set)",
|
|
34106
34809
|
idle_threshold_ms: IDLE_TOAST_THROTTLE_MS
|
|
34107
34810
|
});
|
|
@@ -34115,13 +34818,13 @@ var worktreeLifecyclePlugin = async (ctx) => {
|
|
|
34115
34818
|
}
|
|
34116
34819
|
if (!await shouldRunWorktreeGc(mainRoot)) {
|
|
34117
34820
|
log14.info("[lifecycle] 当前项目无 .codeforge/ 目录且 registry 无条目,worktree-lifecycle 不启用", { mainRoot });
|
|
34118
|
-
safeWriteLog(
|
|
34821
|
+
safeWriteLog(PLUGIN_NAME23, {
|
|
34119
34822
|
hook: "activate",
|
|
34120
34823
|
action: "skip",
|
|
34121
34824
|
source: "non-codeforge-project",
|
|
34122
34825
|
mainRoot
|
|
34123
34826
|
});
|
|
34124
|
-
logLifecycle(
|
|
34827
|
+
logLifecycle(PLUGIN_NAME23, "activate", { skipped: "non-codeforge-project", mainRoot });
|
|
34125
34828
|
return {};
|
|
34126
34829
|
}
|
|
34127
34830
|
if (myGeneration !== _activateGeneration)
|
|
@@ -34134,14 +34837,14 @@ var worktreeLifecyclePlugin = async (ctx) => {
|
|
|
34134
34837
|
}
|
|
34135
34838
|
_probe = createSessionProbe();
|
|
34136
34839
|
setImmediate(() => {
|
|
34137
|
-
safeAsync(
|
|
34840
|
+
safeAsync(PLUGIN_NAME23, "activate.pruneOrphan", async () => {
|
|
34138
34841
|
const rec = await reconcileTransitionalEntries(mainRoot);
|
|
34139
34842
|
if (rec.cleanedCreating.length > 0 || rec.finishedRemoving.length > 0 || rec.keptConservative.length > 0) {
|
|
34140
|
-
safeWriteLog(
|
|
34843
|
+
safeWriteLog(PLUGIN_NAME23, { hook: "activate.reconcile", ...rec });
|
|
34141
34844
|
}
|
|
34142
34845
|
const merc = await reconcileInterruptedMerges(mainRoot);
|
|
34143
34846
|
if (merc.recoveredMerged.length > 0 || merc.rolledBack.length > 0 || merc.conflicts.length > 0 || merc.registryUnreadable) {
|
|
34144
|
-
safeWriteLog(
|
|
34847
|
+
safeWriteLog(PLUGIN_NAME23, { hook: "activate.reconcileMerge", ...merc });
|
|
34145
34848
|
}
|
|
34146
34849
|
const result = await pruneOrphanWorktrees(mainRoot, {
|
|
34147
34850
|
isSessionAlive: _probe.isSessionAlive
|
|
@@ -34149,7 +34852,7 @@ var worktreeLifecyclePlugin = async (ctx) => {
|
|
|
34149
34852
|
await maybeNotifyRegistryUnreadable(client, rec, result);
|
|
34150
34853
|
if (result.cleaned.length > 0 || result.failed.length > 0) {
|
|
34151
34854
|
log14.info(`[pruneOrphan] cleaned=${result.cleaned.length} failed=${result.failed.length} skipped=${result.skipped}`);
|
|
34152
|
-
safeWriteLog(
|
|
34855
|
+
safeWriteLog(PLUGIN_NAME23, {
|
|
34153
34856
|
hook: "activate.pruneOrphan",
|
|
34154
34857
|
cleaned: result.cleaned,
|
|
34155
34858
|
failed: result.failed,
|
|
@@ -34160,7 +34863,7 @@ var worktreeLifecyclePlugin = async (ctx) => {
|
|
|
34160
34863
|
});
|
|
34161
34864
|
_pruneTimer = setInterval(() => {
|
|
34162
34865
|
if (pruneRunning) {
|
|
34163
|
-
safeWriteLog(
|
|
34866
|
+
safeWriteLog(PLUGIN_NAME23, {
|
|
34164
34867
|
hook: "interval.pruneOrphan",
|
|
34165
34868
|
action: "skip",
|
|
34166
34869
|
reason: "previous prune still running"
|
|
@@ -34168,15 +34871,15 @@ var worktreeLifecyclePlugin = async (ctx) => {
|
|
|
34168
34871
|
return;
|
|
34169
34872
|
}
|
|
34170
34873
|
pruneRunning = true;
|
|
34171
|
-
safeAsync(
|
|
34874
|
+
safeAsync(PLUGIN_NAME23, "interval.pruneOrphan", async () => {
|
|
34172
34875
|
try {
|
|
34173
34876
|
const rec = await reconcileTransitionalEntries(mainRoot);
|
|
34174
34877
|
if (rec.cleanedCreating.length > 0 || rec.finishedRemoving.length > 0 || rec.keptConservative.length > 0) {
|
|
34175
|
-
safeWriteLog(
|
|
34878
|
+
safeWriteLog(PLUGIN_NAME23, { hook: "interval.reconcile", ...rec });
|
|
34176
34879
|
}
|
|
34177
34880
|
const merc = await reconcileInterruptedMerges(mainRoot);
|
|
34178
34881
|
if (merc.recoveredMerged.length > 0 || merc.rolledBack.length > 0 || merc.conflicts.length > 0 || merc.registryUnreadable) {
|
|
34179
|
-
safeWriteLog(
|
|
34882
|
+
safeWriteLog(PLUGIN_NAME23, { hook: "interval.reconcileMerge", ...merc });
|
|
34180
34883
|
}
|
|
34181
34884
|
const result = await pruneOrphanWorktrees(mainRoot, {
|
|
34182
34885
|
isSessionAlive: _probe.isSessionAlive
|
|
@@ -34184,7 +34887,7 @@ var worktreeLifecyclePlugin = async (ctx) => {
|
|
|
34184
34887
|
await maybeNotifyRegistryUnreadable(client, rec, result);
|
|
34185
34888
|
if (result.cleaned.length > 0 || result.failed.length > 0) {
|
|
34186
34889
|
log14.info(`[pruneOrphan interval] cleaned=${result.cleaned.length} failed=${result.failed.length} skipped=${result.skipped}`);
|
|
34187
|
-
safeWriteLog(
|
|
34890
|
+
safeWriteLog(PLUGIN_NAME23, {
|
|
34188
34891
|
hook: "interval.pruneOrphan",
|
|
34189
34892
|
cleaned: result.cleaned,
|
|
34190
34893
|
failed: result.failed,
|
|
@@ -34199,14 +34902,14 @@ var worktreeLifecyclePlugin = async (ctx) => {
|
|
|
34199
34902
|
_pruneTimer.unref();
|
|
34200
34903
|
return {
|
|
34201
34904
|
event: async ({ event }) => {
|
|
34202
|
-
await safeAsync(
|
|
34905
|
+
await safeAsync(PLUGIN_NAME23, "event", async () => {
|
|
34203
34906
|
const ended = extractEndedSessionID(event);
|
|
34204
34907
|
if (!ended)
|
|
34205
34908
|
return;
|
|
34206
34909
|
if (ended.type === "session.deleted") {
|
|
34207
34910
|
const entry = await getSessionWorktree(ended.sessionID, mainRoot);
|
|
34208
34911
|
if (!entry || entry.status !== "active") {
|
|
34209
|
-
safeWriteLog(
|
|
34912
|
+
safeWriteLog(PLUGIN_NAME23, {
|
|
34210
34913
|
hook: "event",
|
|
34211
34914
|
type: ended.type,
|
|
34212
34915
|
sessionID: ended.sessionID,
|
|
@@ -34221,7 +34924,7 @@ var worktreeLifecyclePlugin = async (ctx) => {
|
|
|
34221
34924
|
const fastDirty = await isWorktreeDirty(entry.worktreePath);
|
|
34222
34925
|
if (!fastDirty) {
|
|
34223
34926
|
await discardSession({ sessionId: ended.sessionID, mainRoot });
|
|
34224
|
-
safeWriteLog(
|
|
34927
|
+
safeWriteLog(PLUGIN_NAME23, {
|
|
34225
34928
|
hook: "event",
|
|
34226
34929
|
type: ended.type,
|
|
34227
34930
|
sessionID: ended.sessionID,
|
|
@@ -34256,7 +34959,7 @@ var worktreeLifecyclePlugin = async (ctx) => {
|
|
|
34256
34959
|
}
|
|
34257
34960
|
try {
|
|
34258
34961
|
await discardSession({ sessionId: ended.sessionID, mainRoot });
|
|
34259
|
-
safeWriteLog(
|
|
34962
|
+
safeWriteLog(PLUGIN_NAME23, {
|
|
34260
34963
|
hook: "event",
|
|
34261
34964
|
type: ended.type,
|
|
34262
34965
|
sessionID: ended.sessionID,
|
|
@@ -34297,7 +35000,7 @@ var worktreeLifecyclePlugin = async (ctx) => {
|
|
|
34297
35000
|
const idleMin = Math.round(idleMs / 60000);
|
|
34298
35001
|
const msg = `\uD83D\uDCA4 Session ${ended.sessionID.slice(0, 8)} worktree 已空闲 ${idleMin}min,` + `用 /merge 收尾或 /discard-session 放弃`;
|
|
34299
35002
|
const sent = await showToast2(client, { message: msg, variant: "default", duration: IDLE_TOAST_DURATION_MS, title: "CodeForge" }, log14);
|
|
34300
|
-
safeWriteLog(
|
|
35003
|
+
safeWriteLog(PLUGIN_NAME23, {
|
|
34301
35004
|
hook: "event",
|
|
34302
35005
|
type: ended.type,
|
|
34303
35006
|
sessionID: ended.sessionID,
|
|
@@ -34310,7 +35013,7 @@ var worktreeLifecyclePlugin = async (ctx) => {
|
|
|
34310
35013
|
}
|
|
34311
35014
|
};
|
|
34312
35015
|
};
|
|
34313
|
-
var
|
|
35016
|
+
var handler23 = worktreeLifecyclePlugin;
|
|
34314
35017
|
|
|
34315
35018
|
// src/index.ts
|
|
34316
35019
|
var PLUGIN_ID = "codeforge";
|
|
@@ -34336,9 +35039,10 @@ var HANDLERS = [
|
|
|
34336
35039
|
{ name: "tool-heartbeat", init: handler6 },
|
|
34337
35040
|
{ name: "tool-policy", init: handler18 },
|
|
34338
35041
|
{ name: "update-checker", init: handler19 },
|
|
34339
|
-
{ name: "
|
|
34340
|
-
{ name: "
|
|
34341
|
-
{ name: "worktree-
|
|
35042
|
+
{ name: "user-merge-confirm", init: handler20 },
|
|
35043
|
+
{ name: "workflow-engine", init: handler21 },
|
|
35044
|
+
{ name: "session-worktree-guard", init: handler22 },
|
|
35045
|
+
{ name: "worktree-lifecycle", init: handler23 }
|
|
34342
35046
|
];
|
|
34343
35047
|
function makeSerialHook(hookName, fns) {
|
|
34344
35048
|
return async (input, output) => {
|