@defend-tech/opencode-optima 0.1.83 → 0.1.85
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/Agents_Common.md +1 -0
- package/Agents_Common.prompt.md +1 -0
- package/README.md +4 -0
- package/assets/agents/developer.md +4 -0
- package/assets/agents/qa_engineer.md +4 -0
- package/assets/agents/tech_lead.md +4 -0
- package/assets/agents/workflow_product_manager.md +5 -0
- package/assets/agents/workflow_runner.md +4 -0
- package/dist/index.js +1127 -231
- package/dist/sanitize_cli.js +1162 -266
- package/docs/core/task_model.md +1 -0
- package/docs/core/task_model.prompt.md +1 -1
- package/docs/guides/TOOLS.md +50 -0
- package/docs/setup/CONFIGURATION.md +11 -0
- package/package.json +3 -2
package/dist/sanitize_cli.js
CHANGED
|
@@ -109,17 +109,17 @@ var require_visit = __commonJS({
|
|
|
109
109
|
visit.BREAK = BREAK;
|
|
110
110
|
visit.SKIP = SKIP;
|
|
111
111
|
visit.REMOVE = REMOVE;
|
|
112
|
-
function visit_(key, node, visitor,
|
|
113
|
-
const ctrl = callVisitor(key, node, visitor,
|
|
112
|
+
function visit_(key, node, visitor, path23) {
|
|
113
|
+
const ctrl = callVisitor(key, node, visitor, path23);
|
|
114
114
|
if (identity.isNode(ctrl) || identity.isPair(ctrl)) {
|
|
115
|
-
replaceNode(key,
|
|
116
|
-
return visit_(key, ctrl, visitor,
|
|
115
|
+
replaceNode(key, path23, ctrl);
|
|
116
|
+
return visit_(key, ctrl, visitor, path23);
|
|
117
117
|
}
|
|
118
118
|
if (typeof ctrl !== "symbol") {
|
|
119
119
|
if (identity.isCollection(node)) {
|
|
120
|
-
|
|
120
|
+
path23 = Object.freeze(path23.concat(node));
|
|
121
121
|
for (let i = 0; i < node.items.length; ++i) {
|
|
122
|
-
const ci = visit_(i, node.items[i], visitor,
|
|
122
|
+
const ci = visit_(i, node.items[i], visitor, path23);
|
|
123
123
|
if (typeof ci === "number")
|
|
124
124
|
i = ci - 1;
|
|
125
125
|
else if (ci === BREAK)
|
|
@@ -130,13 +130,13 @@ var require_visit = __commonJS({
|
|
|
130
130
|
}
|
|
131
131
|
}
|
|
132
132
|
} else if (identity.isPair(node)) {
|
|
133
|
-
|
|
134
|
-
const ck = visit_("key", node.key, visitor,
|
|
133
|
+
path23 = Object.freeze(path23.concat(node));
|
|
134
|
+
const ck = visit_("key", node.key, visitor, path23);
|
|
135
135
|
if (ck === BREAK)
|
|
136
136
|
return BREAK;
|
|
137
137
|
else if (ck === REMOVE)
|
|
138
138
|
node.key = null;
|
|
139
|
-
const cv = visit_("value", node.value, visitor,
|
|
139
|
+
const cv = visit_("value", node.value, visitor, path23);
|
|
140
140
|
if (cv === BREAK)
|
|
141
141
|
return BREAK;
|
|
142
142
|
else if (cv === REMOVE)
|
|
@@ -157,17 +157,17 @@ var require_visit = __commonJS({
|
|
|
157
157
|
visitAsync.BREAK = BREAK;
|
|
158
158
|
visitAsync.SKIP = SKIP;
|
|
159
159
|
visitAsync.REMOVE = REMOVE;
|
|
160
|
-
async function visitAsync_(key, node, visitor,
|
|
161
|
-
const ctrl = await callVisitor(key, node, visitor,
|
|
160
|
+
async function visitAsync_(key, node, visitor, path23) {
|
|
161
|
+
const ctrl = await callVisitor(key, node, visitor, path23);
|
|
162
162
|
if (identity.isNode(ctrl) || identity.isPair(ctrl)) {
|
|
163
|
-
replaceNode(key,
|
|
164
|
-
return visitAsync_(key, ctrl, visitor,
|
|
163
|
+
replaceNode(key, path23, ctrl);
|
|
164
|
+
return visitAsync_(key, ctrl, visitor, path23);
|
|
165
165
|
}
|
|
166
166
|
if (typeof ctrl !== "symbol") {
|
|
167
167
|
if (identity.isCollection(node)) {
|
|
168
|
-
|
|
168
|
+
path23 = Object.freeze(path23.concat(node));
|
|
169
169
|
for (let i = 0; i < node.items.length; ++i) {
|
|
170
|
-
const ci = await visitAsync_(i, node.items[i], visitor,
|
|
170
|
+
const ci = await visitAsync_(i, node.items[i], visitor, path23);
|
|
171
171
|
if (typeof ci === "number")
|
|
172
172
|
i = ci - 1;
|
|
173
173
|
else if (ci === BREAK)
|
|
@@ -178,13 +178,13 @@ var require_visit = __commonJS({
|
|
|
178
178
|
}
|
|
179
179
|
}
|
|
180
180
|
} else if (identity.isPair(node)) {
|
|
181
|
-
|
|
182
|
-
const ck = await visitAsync_("key", node.key, visitor,
|
|
181
|
+
path23 = Object.freeze(path23.concat(node));
|
|
182
|
+
const ck = await visitAsync_("key", node.key, visitor, path23);
|
|
183
183
|
if (ck === BREAK)
|
|
184
184
|
return BREAK;
|
|
185
185
|
else if (ck === REMOVE)
|
|
186
186
|
node.key = null;
|
|
187
|
-
const cv = await visitAsync_("value", node.value, visitor,
|
|
187
|
+
const cv = await visitAsync_("value", node.value, visitor, path23);
|
|
188
188
|
if (cv === BREAK)
|
|
189
189
|
return BREAK;
|
|
190
190
|
else if (cv === REMOVE)
|
|
@@ -211,23 +211,23 @@ var require_visit = __commonJS({
|
|
|
211
211
|
}
|
|
212
212
|
return visitor;
|
|
213
213
|
}
|
|
214
|
-
function callVisitor(key, node, visitor,
|
|
214
|
+
function callVisitor(key, node, visitor, path23) {
|
|
215
215
|
if (typeof visitor === "function")
|
|
216
|
-
return visitor(key, node,
|
|
216
|
+
return visitor(key, node, path23);
|
|
217
217
|
if (identity.isMap(node))
|
|
218
|
-
return visitor.Map?.(key, node,
|
|
218
|
+
return visitor.Map?.(key, node, path23);
|
|
219
219
|
if (identity.isSeq(node))
|
|
220
|
-
return visitor.Seq?.(key, node,
|
|
220
|
+
return visitor.Seq?.(key, node, path23);
|
|
221
221
|
if (identity.isPair(node))
|
|
222
|
-
return visitor.Pair?.(key, node,
|
|
222
|
+
return visitor.Pair?.(key, node, path23);
|
|
223
223
|
if (identity.isScalar(node))
|
|
224
|
-
return visitor.Scalar?.(key, node,
|
|
224
|
+
return visitor.Scalar?.(key, node, path23);
|
|
225
225
|
if (identity.isAlias(node))
|
|
226
|
-
return visitor.Alias?.(key, node,
|
|
226
|
+
return visitor.Alias?.(key, node, path23);
|
|
227
227
|
return void 0;
|
|
228
228
|
}
|
|
229
|
-
function replaceNode(key,
|
|
230
|
-
const parent =
|
|
229
|
+
function replaceNode(key, path23, node) {
|
|
230
|
+
const parent = path23[path23.length - 1];
|
|
231
231
|
if (identity.isCollection(parent)) {
|
|
232
232
|
parent.items[key] = node;
|
|
233
233
|
} else if (identity.isPair(parent)) {
|
|
@@ -835,10 +835,10 @@ var require_Collection = __commonJS({
|
|
|
835
835
|
var createNode = require_createNode();
|
|
836
836
|
var identity = require_identity();
|
|
837
837
|
var Node = require_Node();
|
|
838
|
-
function collectionFromPath(schema,
|
|
838
|
+
function collectionFromPath(schema, path23, value) {
|
|
839
839
|
let v = value;
|
|
840
|
-
for (let i =
|
|
841
|
-
const k =
|
|
840
|
+
for (let i = path23.length - 1; i >= 0; --i) {
|
|
841
|
+
const k = path23[i];
|
|
842
842
|
if (typeof k === "number" && Number.isInteger(k) && k >= 0) {
|
|
843
843
|
const a = [];
|
|
844
844
|
a[k] = v;
|
|
@@ -857,7 +857,7 @@ var require_Collection = __commonJS({
|
|
|
857
857
|
sourceObjects: /* @__PURE__ */ new Map()
|
|
858
858
|
});
|
|
859
859
|
}
|
|
860
|
-
var isEmptyPath = (
|
|
860
|
+
var isEmptyPath = (path23) => path23 == null || typeof path23 === "object" && !!path23[Symbol.iterator]().next().done;
|
|
861
861
|
var Collection = class extends Node.NodeBase {
|
|
862
862
|
constructor(type, schema) {
|
|
863
863
|
super(type);
|
|
@@ -887,11 +887,11 @@ var require_Collection = __commonJS({
|
|
|
887
887
|
* be a Pair instance or a `{ key, value }` object, which may not have a key
|
|
888
888
|
* that already exists in the map.
|
|
889
889
|
*/
|
|
890
|
-
addIn(
|
|
891
|
-
if (isEmptyPath(
|
|
890
|
+
addIn(path23, value) {
|
|
891
|
+
if (isEmptyPath(path23))
|
|
892
892
|
this.add(value);
|
|
893
893
|
else {
|
|
894
|
-
const [key, ...rest] =
|
|
894
|
+
const [key, ...rest] = path23;
|
|
895
895
|
const node = this.get(key, true);
|
|
896
896
|
if (identity.isCollection(node))
|
|
897
897
|
node.addIn(rest, value);
|
|
@@ -905,8 +905,8 @@ var require_Collection = __commonJS({
|
|
|
905
905
|
* Removes a value from the collection.
|
|
906
906
|
* @returns `true` if the item was found and removed.
|
|
907
907
|
*/
|
|
908
|
-
deleteIn(
|
|
909
|
-
const [key, ...rest] =
|
|
908
|
+
deleteIn(path23) {
|
|
909
|
+
const [key, ...rest] = path23;
|
|
910
910
|
if (rest.length === 0)
|
|
911
911
|
return this.delete(key);
|
|
912
912
|
const node = this.get(key, true);
|
|
@@ -920,8 +920,8 @@ var require_Collection = __commonJS({
|
|
|
920
920
|
* scalar values from their surrounding node; to disable set `keepScalar` to
|
|
921
921
|
* `true` (collections are always returned intact).
|
|
922
922
|
*/
|
|
923
|
-
getIn(
|
|
924
|
-
const [key, ...rest] =
|
|
923
|
+
getIn(path23, keepScalar) {
|
|
924
|
+
const [key, ...rest] = path23;
|
|
925
925
|
const node = this.get(key, true);
|
|
926
926
|
if (rest.length === 0)
|
|
927
927
|
return !keepScalar && identity.isScalar(node) ? node.value : node;
|
|
@@ -939,8 +939,8 @@ var require_Collection = __commonJS({
|
|
|
939
939
|
/**
|
|
940
940
|
* Checks if the collection includes a value with the key `key`.
|
|
941
941
|
*/
|
|
942
|
-
hasIn(
|
|
943
|
-
const [key, ...rest] =
|
|
942
|
+
hasIn(path23) {
|
|
943
|
+
const [key, ...rest] = path23;
|
|
944
944
|
if (rest.length === 0)
|
|
945
945
|
return this.has(key);
|
|
946
946
|
const node = this.get(key, true);
|
|
@@ -950,8 +950,8 @@ var require_Collection = __commonJS({
|
|
|
950
950
|
* Sets a value in this collection. For `!!set`, `value` needs to be a
|
|
951
951
|
* boolean to add/remove the item from the set.
|
|
952
952
|
*/
|
|
953
|
-
setIn(
|
|
954
|
-
const [key, ...rest] =
|
|
953
|
+
setIn(path23, value) {
|
|
954
|
+
const [key, ...rest] = path23;
|
|
955
955
|
if (rest.length === 0) {
|
|
956
956
|
this.set(key, value);
|
|
957
957
|
} else {
|
|
@@ -3455,9 +3455,9 @@ var require_Document = __commonJS({
|
|
|
3455
3455
|
this.contents.add(value);
|
|
3456
3456
|
}
|
|
3457
3457
|
/** Adds a value to the document. */
|
|
3458
|
-
addIn(
|
|
3458
|
+
addIn(path23, value) {
|
|
3459
3459
|
if (assertCollection(this.contents))
|
|
3460
|
-
this.contents.addIn(
|
|
3460
|
+
this.contents.addIn(path23, value);
|
|
3461
3461
|
}
|
|
3462
3462
|
/**
|
|
3463
3463
|
* Create a new `Alias` node, ensuring that the target `node` has the required anchor.
|
|
@@ -3532,14 +3532,14 @@ var require_Document = __commonJS({
|
|
|
3532
3532
|
* Removes a value from the document.
|
|
3533
3533
|
* @returns `true` if the item was found and removed.
|
|
3534
3534
|
*/
|
|
3535
|
-
deleteIn(
|
|
3536
|
-
if (Collection.isEmptyPath(
|
|
3535
|
+
deleteIn(path23) {
|
|
3536
|
+
if (Collection.isEmptyPath(path23)) {
|
|
3537
3537
|
if (this.contents == null)
|
|
3538
3538
|
return false;
|
|
3539
3539
|
this.contents = null;
|
|
3540
3540
|
return true;
|
|
3541
3541
|
}
|
|
3542
|
-
return assertCollection(this.contents) ? this.contents.deleteIn(
|
|
3542
|
+
return assertCollection(this.contents) ? this.contents.deleteIn(path23) : false;
|
|
3543
3543
|
}
|
|
3544
3544
|
/**
|
|
3545
3545
|
* Returns item at `key`, or `undefined` if not found. By default unwraps
|
|
@@ -3554,10 +3554,10 @@ var require_Document = __commonJS({
|
|
|
3554
3554
|
* scalar values from their surrounding node; to disable set `keepScalar` to
|
|
3555
3555
|
* `true` (collections are always returned intact).
|
|
3556
3556
|
*/
|
|
3557
|
-
getIn(
|
|
3558
|
-
if (Collection.isEmptyPath(
|
|
3557
|
+
getIn(path23, keepScalar) {
|
|
3558
|
+
if (Collection.isEmptyPath(path23))
|
|
3559
3559
|
return !keepScalar && identity.isScalar(this.contents) ? this.contents.value : this.contents;
|
|
3560
|
-
return identity.isCollection(this.contents) ? this.contents.getIn(
|
|
3560
|
+
return identity.isCollection(this.contents) ? this.contents.getIn(path23, keepScalar) : void 0;
|
|
3561
3561
|
}
|
|
3562
3562
|
/**
|
|
3563
3563
|
* Checks if the document includes a value with the key `key`.
|
|
@@ -3568,10 +3568,10 @@ var require_Document = __commonJS({
|
|
|
3568
3568
|
/**
|
|
3569
3569
|
* Checks if the document includes a value at `path`.
|
|
3570
3570
|
*/
|
|
3571
|
-
hasIn(
|
|
3572
|
-
if (Collection.isEmptyPath(
|
|
3571
|
+
hasIn(path23) {
|
|
3572
|
+
if (Collection.isEmptyPath(path23))
|
|
3573
3573
|
return this.contents !== void 0;
|
|
3574
|
-
return identity.isCollection(this.contents) ? this.contents.hasIn(
|
|
3574
|
+
return identity.isCollection(this.contents) ? this.contents.hasIn(path23) : false;
|
|
3575
3575
|
}
|
|
3576
3576
|
/**
|
|
3577
3577
|
* Sets a value in this document. For `!!set`, `value` needs to be a
|
|
@@ -3588,13 +3588,13 @@ var require_Document = __commonJS({
|
|
|
3588
3588
|
* Sets a value in this document. For `!!set`, `value` needs to be a
|
|
3589
3589
|
* boolean to add/remove the item from the set.
|
|
3590
3590
|
*/
|
|
3591
|
-
setIn(
|
|
3592
|
-
if (Collection.isEmptyPath(
|
|
3591
|
+
setIn(path23, value) {
|
|
3592
|
+
if (Collection.isEmptyPath(path23)) {
|
|
3593
3593
|
this.contents = value;
|
|
3594
3594
|
} else if (this.contents == null) {
|
|
3595
|
-
this.contents = Collection.collectionFromPath(this.schema, Array.from(
|
|
3595
|
+
this.contents = Collection.collectionFromPath(this.schema, Array.from(path23), value);
|
|
3596
3596
|
} else if (assertCollection(this.contents)) {
|
|
3597
|
-
this.contents.setIn(
|
|
3597
|
+
this.contents.setIn(path23, value);
|
|
3598
3598
|
}
|
|
3599
3599
|
}
|
|
3600
3600
|
/**
|
|
@@ -5546,9 +5546,9 @@ var require_cst_visit = __commonJS({
|
|
|
5546
5546
|
visit.BREAK = BREAK;
|
|
5547
5547
|
visit.SKIP = SKIP;
|
|
5548
5548
|
visit.REMOVE = REMOVE;
|
|
5549
|
-
visit.itemAtPath = (cst,
|
|
5549
|
+
visit.itemAtPath = (cst, path23) => {
|
|
5550
5550
|
let item = cst;
|
|
5551
|
-
for (const [field, index] of
|
|
5551
|
+
for (const [field, index] of path23) {
|
|
5552
5552
|
const tok = item?.[field];
|
|
5553
5553
|
if (tok && "items" in tok) {
|
|
5554
5554
|
item = tok.items[index];
|
|
@@ -5557,23 +5557,23 @@ var require_cst_visit = __commonJS({
|
|
|
5557
5557
|
}
|
|
5558
5558
|
return item;
|
|
5559
5559
|
};
|
|
5560
|
-
visit.parentCollection = (cst,
|
|
5561
|
-
const parent = visit.itemAtPath(cst,
|
|
5562
|
-
const field =
|
|
5560
|
+
visit.parentCollection = (cst, path23) => {
|
|
5561
|
+
const parent = visit.itemAtPath(cst, path23.slice(0, -1));
|
|
5562
|
+
const field = path23[path23.length - 1][0];
|
|
5563
5563
|
const coll = parent?.[field];
|
|
5564
5564
|
if (coll && "items" in coll)
|
|
5565
5565
|
return coll;
|
|
5566
5566
|
throw new Error("Parent collection not found");
|
|
5567
5567
|
};
|
|
5568
|
-
function _visit(
|
|
5569
|
-
let ctrl = visitor(item,
|
|
5568
|
+
function _visit(path23, item, visitor) {
|
|
5569
|
+
let ctrl = visitor(item, path23);
|
|
5570
5570
|
if (typeof ctrl === "symbol")
|
|
5571
5571
|
return ctrl;
|
|
5572
5572
|
for (const field of ["key", "value"]) {
|
|
5573
5573
|
const token = item[field];
|
|
5574
5574
|
if (token && "items" in token) {
|
|
5575
5575
|
for (let i = 0; i < token.items.length; ++i) {
|
|
5576
|
-
const ci = _visit(Object.freeze(
|
|
5576
|
+
const ci = _visit(Object.freeze(path23.concat([[field, i]])), token.items[i], visitor);
|
|
5577
5577
|
if (typeof ci === "number")
|
|
5578
5578
|
i = ci - 1;
|
|
5579
5579
|
else if (ci === BREAK)
|
|
@@ -5584,10 +5584,10 @@ var require_cst_visit = __commonJS({
|
|
|
5584
5584
|
}
|
|
5585
5585
|
}
|
|
5586
5586
|
if (typeof ctrl === "function" && field === "key")
|
|
5587
|
-
ctrl = ctrl(item,
|
|
5587
|
+
ctrl = ctrl(item, path23);
|
|
5588
5588
|
}
|
|
5589
5589
|
}
|
|
5590
|
-
return typeof ctrl === "function" ? ctrl(item,
|
|
5590
|
+
return typeof ctrl === "function" ? ctrl(item, path23) : ctrl;
|
|
5591
5591
|
}
|
|
5592
5592
|
exports.visit = visit;
|
|
5593
5593
|
}
|
|
@@ -6872,14 +6872,14 @@ var require_parser = __commonJS({
|
|
|
6872
6872
|
case "scalar":
|
|
6873
6873
|
case "single-quoted-scalar":
|
|
6874
6874
|
case "double-quoted-scalar": {
|
|
6875
|
-
const
|
|
6875
|
+
const fs22 = this.flowScalar(this.type);
|
|
6876
6876
|
if (atNextItem || it.value) {
|
|
6877
|
-
map.items.push({ start, key:
|
|
6877
|
+
map.items.push({ start, key: fs22, sep: [] });
|
|
6878
6878
|
this.onKeyLine = true;
|
|
6879
6879
|
} else if (it.sep) {
|
|
6880
|
-
this.stack.push(
|
|
6880
|
+
this.stack.push(fs22);
|
|
6881
6881
|
} else {
|
|
6882
|
-
Object.assign(it, { key:
|
|
6882
|
+
Object.assign(it, { key: fs22, sep: [] });
|
|
6883
6883
|
this.onKeyLine = true;
|
|
6884
6884
|
}
|
|
6885
6885
|
return;
|
|
@@ -7007,13 +7007,13 @@ var require_parser = __commonJS({
|
|
|
7007
7007
|
case "scalar":
|
|
7008
7008
|
case "single-quoted-scalar":
|
|
7009
7009
|
case "double-quoted-scalar": {
|
|
7010
|
-
const
|
|
7010
|
+
const fs22 = this.flowScalar(this.type);
|
|
7011
7011
|
if (!it || it.value)
|
|
7012
|
-
fc.items.push({ start: [], key:
|
|
7012
|
+
fc.items.push({ start: [], key: fs22, sep: [] });
|
|
7013
7013
|
else if (it.sep)
|
|
7014
|
-
this.stack.push(
|
|
7014
|
+
this.stack.push(fs22);
|
|
7015
7015
|
else
|
|
7016
|
-
Object.assign(it, { key:
|
|
7016
|
+
Object.assign(it, { key: fs22, sep: [] });
|
|
7017
7017
|
return;
|
|
7018
7018
|
}
|
|
7019
7019
|
case "flow-map-end":
|
|
@@ -7551,17 +7551,17 @@ var require_ignore = __commonJS({
|
|
|
7551
7551
|
var throwError = (message, Ctor) => {
|
|
7552
7552
|
throw new Ctor(message);
|
|
7553
7553
|
};
|
|
7554
|
-
var checkPath = (
|
|
7555
|
-
if (!isString(
|
|
7554
|
+
var checkPath = (path23, originalPath, doThrow) => {
|
|
7555
|
+
if (!isString(path23)) {
|
|
7556
7556
|
return doThrow(
|
|
7557
7557
|
`path must be a string, but got \`${originalPath}\``,
|
|
7558
7558
|
TypeError
|
|
7559
7559
|
);
|
|
7560
7560
|
}
|
|
7561
|
-
if (!
|
|
7561
|
+
if (!path23) {
|
|
7562
7562
|
return doThrow(`path must not be empty`, TypeError);
|
|
7563
7563
|
}
|
|
7564
|
-
if (checkPath.isNotRelative(
|
|
7564
|
+
if (checkPath.isNotRelative(path23)) {
|
|
7565
7565
|
const r = "`path.relative()`d";
|
|
7566
7566
|
return doThrow(
|
|
7567
7567
|
`path should be a ${r} string, but got "${originalPath}"`,
|
|
@@ -7570,7 +7570,7 @@ var require_ignore = __commonJS({
|
|
|
7570
7570
|
}
|
|
7571
7571
|
return true;
|
|
7572
7572
|
};
|
|
7573
|
-
var isNotRelative = (
|
|
7573
|
+
var isNotRelative = (path23) => REGEX_TEST_INVALID_PATH.test(path23);
|
|
7574
7574
|
checkPath.isNotRelative = isNotRelative;
|
|
7575
7575
|
checkPath.convert = (p) => p;
|
|
7576
7576
|
var Ignore = class {
|
|
@@ -7629,7 +7629,7 @@ var require_ignore = __commonJS({
|
|
|
7629
7629
|
// setting `checkUnignored` to `false` could reduce additional
|
|
7630
7630
|
// path matching.
|
|
7631
7631
|
// @returns {TestResult} true if a file is ignored
|
|
7632
|
-
_testOne(
|
|
7632
|
+
_testOne(path23, checkUnignored) {
|
|
7633
7633
|
let ignored = false;
|
|
7634
7634
|
let unignored = false;
|
|
7635
7635
|
this._rules.forEach((rule) => {
|
|
@@ -7637,7 +7637,7 @@ var require_ignore = __commonJS({
|
|
|
7637
7637
|
if (unignored === negative && ignored !== unignored || negative && !ignored && !unignored && !checkUnignored) {
|
|
7638
7638
|
return;
|
|
7639
7639
|
}
|
|
7640
|
-
const matched = rule.regex.test(
|
|
7640
|
+
const matched = rule.regex.test(path23);
|
|
7641
7641
|
if (matched) {
|
|
7642
7642
|
ignored = !negative;
|
|
7643
7643
|
unignored = negative;
|
|
@@ -7650,24 +7650,24 @@ var require_ignore = __commonJS({
|
|
|
7650
7650
|
}
|
|
7651
7651
|
// @returns {TestResult}
|
|
7652
7652
|
_test(originalPath, cache, checkUnignored, slices) {
|
|
7653
|
-
const
|
|
7653
|
+
const path23 = originalPath && checkPath.convert(originalPath);
|
|
7654
7654
|
checkPath(
|
|
7655
|
-
|
|
7655
|
+
path23,
|
|
7656
7656
|
originalPath,
|
|
7657
7657
|
this._allowRelativePaths ? RETURN_FALSE : throwError
|
|
7658
7658
|
);
|
|
7659
|
-
return this._t(
|
|
7659
|
+
return this._t(path23, cache, checkUnignored, slices);
|
|
7660
7660
|
}
|
|
7661
|
-
_t(
|
|
7662
|
-
if (
|
|
7663
|
-
return cache[
|
|
7661
|
+
_t(path23, cache, checkUnignored, slices) {
|
|
7662
|
+
if (path23 in cache) {
|
|
7663
|
+
return cache[path23];
|
|
7664
7664
|
}
|
|
7665
7665
|
if (!slices) {
|
|
7666
|
-
slices =
|
|
7666
|
+
slices = path23.split(SLASH);
|
|
7667
7667
|
}
|
|
7668
7668
|
slices.pop();
|
|
7669
7669
|
if (!slices.length) {
|
|
7670
|
-
return cache[
|
|
7670
|
+
return cache[path23] = this._testOne(path23, checkUnignored);
|
|
7671
7671
|
}
|
|
7672
7672
|
const parent = this._t(
|
|
7673
7673
|
slices.join(SLASH) + SLASH,
|
|
@@ -7675,24 +7675,24 @@ var require_ignore = __commonJS({
|
|
|
7675
7675
|
checkUnignored,
|
|
7676
7676
|
slices
|
|
7677
7677
|
);
|
|
7678
|
-
return cache[
|
|
7678
|
+
return cache[path23] = parent.ignored ? parent : this._testOne(path23, checkUnignored);
|
|
7679
7679
|
}
|
|
7680
|
-
ignores(
|
|
7681
|
-
return this._test(
|
|
7680
|
+
ignores(path23) {
|
|
7681
|
+
return this._test(path23, this._ignoreCache, false).ignored;
|
|
7682
7682
|
}
|
|
7683
7683
|
createFilter() {
|
|
7684
|
-
return (
|
|
7684
|
+
return (path23) => !this.ignores(path23);
|
|
7685
7685
|
}
|
|
7686
7686
|
filter(paths) {
|
|
7687
7687
|
return makeArray(paths).filter(this.createFilter());
|
|
7688
7688
|
}
|
|
7689
7689
|
// @returns {TestResult}
|
|
7690
|
-
test(
|
|
7691
|
-
return this._test(
|
|
7690
|
+
test(path23) {
|
|
7691
|
+
return this._test(path23, this._testCache, true);
|
|
7692
7692
|
}
|
|
7693
7693
|
};
|
|
7694
7694
|
var factory = (options) => new Ignore(options);
|
|
7695
|
-
var isPathValid = (
|
|
7695
|
+
var isPathValid = (path23) => checkPath(path23 && checkPath.convert(path23), path23, RETURN_FALSE);
|
|
7696
7696
|
factory.isPathValid = isPathValid;
|
|
7697
7697
|
factory.default = factory;
|
|
7698
7698
|
module.exports = factory;
|
|
@@ -7703,22 +7703,22 @@ var require_ignore = __commonJS({
|
|
|
7703
7703
|
const makePosix = (str) => /^\\\\\?\\/.test(str) || /["<>|\u0000-\u001F]+/u.test(str) ? str : str.replace(/\\/g, "/");
|
|
7704
7704
|
checkPath.convert = makePosix;
|
|
7705
7705
|
const REGIX_IS_WINDOWS_PATH_ABSOLUTE = /^[a-z]:\//i;
|
|
7706
|
-
checkPath.isNotRelative = (
|
|
7706
|
+
checkPath.isNotRelative = (path23) => REGIX_IS_WINDOWS_PATH_ABSOLUTE.test(path23) || isNotRelative(path23);
|
|
7707
7707
|
}
|
|
7708
7708
|
}
|
|
7709
7709
|
});
|
|
7710
7710
|
|
|
7711
7711
|
// src/sanitize_cli.js
|
|
7712
|
-
import
|
|
7713
|
-
import
|
|
7714
|
-
import
|
|
7712
|
+
import fs21 from "node:fs";
|
|
7713
|
+
import os7 from "node:os";
|
|
7714
|
+
import path22 from "node:path";
|
|
7715
7715
|
import { execFileSync as execFileSync2, spawnSync } from "node:child_process";
|
|
7716
7716
|
|
|
7717
7717
|
// src/plugin.js
|
|
7718
|
-
import
|
|
7718
|
+
import fs20 from "node:fs";
|
|
7719
7719
|
var import_yaml6 = __toESM(require_dist(), 1);
|
|
7720
7720
|
var import_ignore3 = __toESM(require_ignore(), 1);
|
|
7721
|
-
import
|
|
7721
|
+
import path21 from "node:path";
|
|
7722
7722
|
import { tool } from "@opencode-ai/plugin/tool";
|
|
7723
7723
|
|
|
7724
7724
|
// src/agents.js
|
|
@@ -9962,8 +9962,8 @@ function clickUpWebhookLogPath(worktree) {
|
|
|
9962
9962
|
return path7.join(optimaRuntimeDir(worktree), "clickup-webhook.log.jsonl");
|
|
9963
9963
|
}
|
|
9964
9964
|
function clickUpWebhookAuditLogDir() {
|
|
9965
|
-
const
|
|
9966
|
-
return path7.join(
|
|
9965
|
+
const dataHome2 = process.env.XDG_DATA_HOME && path7.isAbsolute(process.env.XDG_DATA_HOME) ? process.env.XDG_DATA_HOME : path7.join(os2.homedir(), ".local", "share", "opencode");
|
|
9966
|
+
return path7.join(dataHome2, "opencode-optima");
|
|
9967
9967
|
}
|
|
9968
9968
|
function clickUpWebhookAuditLogPath(at = /* @__PURE__ */ new Date(), logDir = clickUpWebhookAuditLogDir()) {
|
|
9969
9969
|
return path7.join(logDir, `clickup-webhook-${at.toISOString().slice(0, 10)}.jsonl`);
|
|
@@ -13506,11 +13506,668 @@ function createGitHubApiClient(config = {}, fetchImpl = globalThis.fetch) {
|
|
|
13506
13506
|
};
|
|
13507
13507
|
}
|
|
13508
13508
|
|
|
13509
|
+
// src/qa/chrome.js
|
|
13510
|
+
import fs17 from "node:fs";
|
|
13511
|
+
import os6 from "node:os";
|
|
13512
|
+
import path18 from "node:path";
|
|
13513
|
+
|
|
13514
|
+
// src/qa/cdp.js
|
|
13515
|
+
import fs15 from "node:fs";
|
|
13516
|
+
import path16 from "node:path";
|
|
13517
|
+
var DEFAULT_CDP_URL = "http://127.0.0.1:9222";
|
|
13518
|
+
function normalizeBaseUrl(cdpUrl = "") {
|
|
13519
|
+
return String(cdpUrl || process.env.OPTIMA_QA_CHROME_CDP_URL || DEFAULT_CDP_URL).trim().replace(/\/+$/, "");
|
|
13520
|
+
}
|
|
13521
|
+
async function cdpJson(baseUrl, route, options = {}) {
|
|
13522
|
+
const response = await fetch(`${baseUrl}${route}`, options);
|
|
13523
|
+
if (!response.ok) throw new Error(`cdp_http_${response.status}: ${route}`);
|
|
13524
|
+
return response.json();
|
|
13525
|
+
}
|
|
13526
|
+
async function listCdpTargets({ cdpUrl = "" } = {}) {
|
|
13527
|
+
const baseUrl = normalizeBaseUrl(cdpUrl);
|
|
13528
|
+
return cdpJson(baseUrl, "/json/list");
|
|
13529
|
+
}
|
|
13530
|
+
async function cdpBrowserStatus({ cdpUrl = "", clickupTaskId = "", qaWindowName: qaWindowName2 = null } = {}) {
|
|
13531
|
+
const targets = await listCdpTargets({ cdpUrl });
|
|
13532
|
+
const pages = [];
|
|
13533
|
+
for (let index = 0; index < targets.length; index += 1) {
|
|
13534
|
+
const target = targets[index];
|
|
13535
|
+
if (target.type !== "page") continue;
|
|
13536
|
+
let windowName = "";
|
|
13537
|
+
if (target.webSocketDebuggerUrl) {
|
|
13538
|
+
const session = await CdpSession.connect(target.webSocketDebuggerUrl).catch(() => null);
|
|
13539
|
+
if (session) {
|
|
13540
|
+
try {
|
|
13541
|
+
const evaluated = await session.evaluate("window.name || ''");
|
|
13542
|
+
windowName = String(evaluated?.value || "");
|
|
13543
|
+
} finally {
|
|
13544
|
+
session.close();
|
|
13545
|
+
}
|
|
13546
|
+
}
|
|
13547
|
+
}
|
|
13548
|
+
pages.push({
|
|
13549
|
+
index,
|
|
13550
|
+
id: target.id,
|
|
13551
|
+
role: index === 0 ? "persistent_session_tab" : qaWindowName2 && windowName === qaWindowName2(clickupTaskId) ? "qa_task_tab" : "other",
|
|
13552
|
+
url: target.url,
|
|
13553
|
+
title: target.title || "",
|
|
13554
|
+
windowName
|
|
13555
|
+
});
|
|
13556
|
+
}
|
|
13557
|
+
return { ok: true, pages, serviceWorkers: targets.filter((target) => target.type === "service_worker").map((target) => target.url) };
|
|
13558
|
+
}
|
|
13559
|
+
var CdpSession = class _CdpSession {
|
|
13560
|
+
constructor(ws) {
|
|
13561
|
+
this.ws = ws;
|
|
13562
|
+
this.nextId = 1;
|
|
13563
|
+
this.pending = /* @__PURE__ */ new Map();
|
|
13564
|
+
this.ws.addEventListener("message", (event) => this.handleMessage(event));
|
|
13565
|
+
this.ws.addEventListener("close", () => this.rejectAll(new Error("cdp_websocket_closed")));
|
|
13566
|
+
this.ws.addEventListener("error", () => this.rejectAll(new Error("cdp_websocket_error")));
|
|
13567
|
+
}
|
|
13568
|
+
static connect(wsUrl, { timeoutMs = 1e4 } = {}) {
|
|
13569
|
+
return new Promise((resolve, reject) => {
|
|
13570
|
+
const ws = new WebSocket(wsUrl);
|
|
13571
|
+
const timer = setTimeout(() => {
|
|
13572
|
+
try {
|
|
13573
|
+
ws.close();
|
|
13574
|
+
} catch {
|
|
13575
|
+
}
|
|
13576
|
+
reject(new Error("cdp_websocket_connect_timeout"));
|
|
13577
|
+
}, timeoutMs);
|
|
13578
|
+
ws.addEventListener("open", () => {
|
|
13579
|
+
clearTimeout(timer);
|
|
13580
|
+
resolve(new _CdpSession(ws));
|
|
13581
|
+
}, { once: true });
|
|
13582
|
+
ws.addEventListener("error", () => {
|
|
13583
|
+
clearTimeout(timer);
|
|
13584
|
+
reject(new Error("cdp_websocket_connect_failed"));
|
|
13585
|
+
}, { once: true });
|
|
13586
|
+
});
|
|
13587
|
+
}
|
|
13588
|
+
handleMessage(event) {
|
|
13589
|
+
let message = null;
|
|
13590
|
+
try {
|
|
13591
|
+
message = JSON.parse(event.data);
|
|
13592
|
+
} catch {
|
|
13593
|
+
return;
|
|
13594
|
+
}
|
|
13595
|
+
if (!message?.id || !this.pending.has(message.id)) return;
|
|
13596
|
+
const pending = this.pending.get(message.id);
|
|
13597
|
+
this.pending.delete(message.id);
|
|
13598
|
+
clearTimeout(pending.timer);
|
|
13599
|
+
if (message.error) pending.reject(new Error(`${message.error.message || "cdp_error"}${message.error.data ? `: ${message.error.data}` : ""}`));
|
|
13600
|
+
else pending.resolve(message.result || {});
|
|
13601
|
+
}
|
|
13602
|
+
rejectAll(error) {
|
|
13603
|
+
for (const pending of this.pending.values()) {
|
|
13604
|
+
clearTimeout(pending.timer);
|
|
13605
|
+
pending.reject(error);
|
|
13606
|
+
}
|
|
13607
|
+
this.pending.clear();
|
|
13608
|
+
}
|
|
13609
|
+
send(method, params = {}, { timeoutMs = 3e4 } = {}) {
|
|
13610
|
+
const id = this.nextId;
|
|
13611
|
+
this.nextId += 1;
|
|
13612
|
+
return new Promise((resolve, reject) => {
|
|
13613
|
+
const timer = setTimeout(() => {
|
|
13614
|
+
this.pending.delete(id);
|
|
13615
|
+
reject(new Error(`cdp_command_timeout: ${method}`));
|
|
13616
|
+
}, timeoutMs);
|
|
13617
|
+
this.pending.set(id, { resolve, reject, timer });
|
|
13618
|
+
this.ws.send(JSON.stringify({ id, method, params }));
|
|
13619
|
+
});
|
|
13620
|
+
}
|
|
13621
|
+
async evaluate(expression, { timeoutMs = 3e4 } = {}) {
|
|
13622
|
+
const result = await this.send("Runtime.evaluate", {
|
|
13623
|
+
expression,
|
|
13624
|
+
awaitPromise: true,
|
|
13625
|
+
returnByValue: true
|
|
13626
|
+
}, { timeoutMs });
|
|
13627
|
+
if (result.exceptionDetails) throw new Error(result.exceptionDetails.text || "runtime_evaluate_failed");
|
|
13628
|
+
return result.result || null;
|
|
13629
|
+
}
|
|
13630
|
+
close() {
|
|
13631
|
+
try {
|
|
13632
|
+
this.ws.close();
|
|
13633
|
+
} catch {
|
|
13634
|
+
}
|
|
13635
|
+
}
|
|
13636
|
+
};
|
|
13637
|
+
async function newCdpTarget({ cdpUrl = "", startUrl = "about:blank" } = {}) {
|
|
13638
|
+
const baseUrl = normalizeBaseUrl(cdpUrl);
|
|
13639
|
+
return cdpJson(baseUrl, `/json/new?${encodeURIComponent(startUrl)}`, { method: "PUT" });
|
|
13640
|
+
}
|
|
13641
|
+
async function targetWindowName(target) {
|
|
13642
|
+
if (!target.webSocketDebuggerUrl) return "";
|
|
13643
|
+
const session = await CdpSession.connect(target.webSocketDebuggerUrl);
|
|
13644
|
+
try {
|
|
13645
|
+
const value = await session.evaluate("window.name || ''");
|
|
13646
|
+
return String(value?.value || "");
|
|
13647
|
+
} finally {
|
|
13648
|
+
session.close();
|
|
13649
|
+
}
|
|
13650
|
+
}
|
|
13651
|
+
async function ensureCdpQaTarget({ cdpUrl = "", clickupTaskId = "", startUrl = "about:blank", qaWindowName: qaWindowName2 } = {}) {
|
|
13652
|
+
const expected = qaWindowName2(clickupTaskId);
|
|
13653
|
+
const targets = (await listCdpTargets({ cdpUrl })).filter((target2) => target2.type === "page");
|
|
13654
|
+
for (let index = 1; index < targets.length; index += 1) {
|
|
13655
|
+
if (await targetWindowName(targets[index]).catch(() => "") === expected) return targets[index];
|
|
13656
|
+
}
|
|
13657
|
+
const target = await newCdpTarget({ cdpUrl, startUrl });
|
|
13658
|
+
const session = await CdpSession.connect(target.webSocketDebuggerUrl);
|
|
13659
|
+
try {
|
|
13660
|
+
await session.evaluate(`window.name = ${JSON.stringify(expected)}`);
|
|
13661
|
+
} finally {
|
|
13662
|
+
session.close();
|
|
13663
|
+
}
|
|
13664
|
+
return target;
|
|
13665
|
+
}
|
|
13666
|
+
function parseCdpCommand(commandJson = "") {
|
|
13667
|
+
if (!String(commandJson || "").trim()) return { action: "status" };
|
|
13668
|
+
try {
|
|
13669
|
+
const parsed = JSON.parse(commandJson);
|
|
13670
|
+
return parsed && typeof parsed === "object" ? parsed : { action: "invalid" };
|
|
13671
|
+
} catch {
|
|
13672
|
+
return { action: "script", script: String(commandJson || "") };
|
|
13673
|
+
}
|
|
13674
|
+
}
|
|
13675
|
+
async function pageSnapshot(session) {
|
|
13676
|
+
const evaluated = await session.evaluate("({ url: location.href, title: document.title })");
|
|
13677
|
+
return evaluated?.value || { url: "", title: "" };
|
|
13678
|
+
}
|
|
13679
|
+
async function cdpRunChromeCommand({ cdpUrl = "", clickupTaskId = "", commandJson = "", startUrl = "about:blank", artifactsDir, qaWindowName: qaWindowName2 } = {}) {
|
|
13680
|
+
const command = parseCdpCommand(commandJson);
|
|
13681
|
+
const target = await ensureCdpQaTarget({ cdpUrl, clickupTaskId, startUrl, qaWindowName: qaWindowName2 });
|
|
13682
|
+
const session = await CdpSession.connect(target.webSocketDebuggerUrl);
|
|
13683
|
+
try {
|
|
13684
|
+
const action = String(command.action || "status").toLowerCase();
|
|
13685
|
+
let result = null;
|
|
13686
|
+
if (action === "status") {
|
|
13687
|
+
result = { ok: true, page: await pageSnapshot(session) };
|
|
13688
|
+
} else if (action === "goto") {
|
|
13689
|
+
await session.send("Page.enable");
|
|
13690
|
+
await session.send("Page.navigate", { url: String(command.url || startUrl || "about:blank") });
|
|
13691
|
+
if (command.wait_ms !== false) await new Promise((resolve) => setTimeout(resolve, Number(command.wait_ms || 1e3)));
|
|
13692
|
+
result = { ok: true, page: await pageSnapshot(session) };
|
|
13693
|
+
} else if (action === "evaluate") {
|
|
13694
|
+
const evaluated = await session.evaluate(String(command.expression || "undefined"), { timeoutMs: Number(command.timeout_ms || 3e4) });
|
|
13695
|
+
result = { ok: true, result: evaluated?.value ?? evaluated };
|
|
13696
|
+
} else if (action === "screenshot") {
|
|
13697
|
+
await session.send("Page.enable");
|
|
13698
|
+
const captured = await session.send("Page.captureScreenshot", { format: command.format || "png", captureBeyondViewport: command.full_page !== false });
|
|
13699
|
+
const name = String(command.name || `screenshot-${Date.now()}.png`).replace(/[^a-zA-Z0-9._-]+/g, "-");
|
|
13700
|
+
const outPath = path16.join(artifactsDir, name);
|
|
13701
|
+
fs15.mkdirSync(path16.dirname(outPath), { recursive: true });
|
|
13702
|
+
fs15.writeFileSync(outPath, Buffer.from(captured.data || "", "base64"));
|
|
13703
|
+
result = { ok: true, path: outPath };
|
|
13704
|
+
} else if (action === "script") {
|
|
13705
|
+
const script = String(command.script || "");
|
|
13706
|
+
if (!script.trim()) return { ok: false, error: "script_required" };
|
|
13707
|
+
const cdp = { send: (method, params = {}, options = {}) => session.send(method, params, options), evaluate: (expression, options = {}) => session.evaluate(expression, options) };
|
|
13708
|
+
const fn = new Function("cdp", "target", "artifactsDir", "command", `return (async () => {
|
|
13709
|
+
${script}
|
|
13710
|
+
})()`);
|
|
13711
|
+
result = { ok: true, result: await fn(cdp, target, artifactsDir, command) };
|
|
13712
|
+
} else {
|
|
13713
|
+
result = { ok: false, error: `unsupported_cdp_command_action: ${action}` };
|
|
13714
|
+
}
|
|
13715
|
+
return {
|
|
13716
|
+
ok: result.ok !== false,
|
|
13717
|
+
command: command.action || "script",
|
|
13718
|
+
tab: { windowName: qaWindowName2(clickupTaskId), ...await pageSnapshot(session) },
|
|
13719
|
+
artifactsDir,
|
|
13720
|
+
result,
|
|
13721
|
+
transport: "cdp"
|
|
13722
|
+
};
|
|
13723
|
+
} finally {
|
|
13724
|
+
session.close();
|
|
13725
|
+
}
|
|
13726
|
+
}
|
|
13727
|
+
async function cdpPrepareExtension({ cdpUrl = "", extensionId = "", reset = true, reload = true } = {}) {
|
|
13728
|
+
const targets = await listCdpTargets({ cdpUrl });
|
|
13729
|
+
const normalized = String(extensionId || "").trim();
|
|
13730
|
+
const worker = targets.find((target) => target.type === "service_worker" && (!normalized || String(target.url || "").startsWith(`chrome-extension://${normalized}/`)));
|
|
13731
|
+
if (!worker?.webSocketDebuggerUrl) return { ok: false, error: "extension_service_worker_not_found", serviceWorkers: targets.filter((target) => target.type === "service_worker").map((target) => target.url) };
|
|
13732
|
+
const session = await CdpSession.connect(worker.webSocketDebuggerUrl);
|
|
13733
|
+
try {
|
|
13734
|
+
let resetResult = { ok: true, skipped: true };
|
|
13735
|
+
let reloadResult = { ok: true, skipped: true };
|
|
13736
|
+
if (reset) {
|
|
13737
|
+
const evaluated = await session.evaluate(`(async () => {
|
|
13738
|
+
const deletedCaches = [];
|
|
13739
|
+
const deletedDbs = [];
|
|
13740
|
+
if (globalThis.chrome?.storage?.local) await chrome.storage.local.clear();
|
|
13741
|
+
if (globalThis.chrome?.storage?.session?.clear) await chrome.storage.session.clear();
|
|
13742
|
+
if (globalThis.chrome?.storage?.sync) await chrome.storage.sync.clear();
|
|
13743
|
+
if (globalThis.caches?.keys) {
|
|
13744
|
+
for (const name of await caches.keys()) {
|
|
13745
|
+
await caches.delete(name);
|
|
13746
|
+
deletedCaches.push(name);
|
|
13747
|
+
}
|
|
13748
|
+
}
|
|
13749
|
+
if (globalThis.indexedDB?.databases) {
|
|
13750
|
+
for (const db of await indexedDB.databases()) {
|
|
13751
|
+
if (db.name) {
|
|
13752
|
+
indexedDB.deleteDatabase(db.name);
|
|
13753
|
+
deletedDbs.push(db.name);
|
|
13754
|
+
}
|
|
13755
|
+
}
|
|
13756
|
+
}
|
|
13757
|
+
return { ok: true, deletedCaches, deletedDbs };
|
|
13758
|
+
})()`);
|
|
13759
|
+
resetResult = evaluated?.value || { ok: true };
|
|
13760
|
+
}
|
|
13761
|
+
if (reload) {
|
|
13762
|
+
await session.evaluate("chrome.runtime.reload()");
|
|
13763
|
+
reloadResult = { ok: true };
|
|
13764
|
+
}
|
|
13765
|
+
return { ok: true, reset: resetResult, reload: reloadResult };
|
|
13766
|
+
} finally {
|
|
13767
|
+
session.close();
|
|
13768
|
+
}
|
|
13769
|
+
}
|
|
13770
|
+
|
|
13771
|
+
// src/qa/queue.js
|
|
13772
|
+
import fs16 from "node:fs";
|
|
13773
|
+
import os5 from "node:os";
|
|
13774
|
+
import path17 from "node:path";
|
|
13775
|
+
var DEFAULT_QA_LEASE_MS = 5 * 60 * 1e3;
|
|
13776
|
+
var DEFAULT_QA_PROVIDER = "chatgpt";
|
|
13777
|
+
function dataHome() {
|
|
13778
|
+
return process.env.XDG_DATA_HOME && path17.isAbsolute(process.env.XDG_DATA_HOME) ? process.env.XDG_DATA_HOME : path17.join(os5.homedir(), ".local", "share", "opencode");
|
|
13779
|
+
}
|
|
13780
|
+
function qaRuntimeDir() {
|
|
13781
|
+
return path17.join(dataHome(), "opencode-optima", "qa");
|
|
13782
|
+
}
|
|
13783
|
+
function normalizeQaProvider(provider = DEFAULT_QA_PROVIDER) {
|
|
13784
|
+
const normalized = String(provider || DEFAULT_QA_PROVIDER).trim().toLowerCase().replace(/[^a-z0-9_-]+/g, "-");
|
|
13785
|
+
return normalized || DEFAULT_QA_PROVIDER;
|
|
13786
|
+
}
|
|
13787
|
+
function qaQueueStatePath(provider = DEFAULT_QA_PROVIDER) {
|
|
13788
|
+
return path17.join(qaRuntimeDir(), `${normalizeQaProvider(provider)}-queue.json`);
|
|
13789
|
+
}
|
|
13790
|
+
function nowIso(now = /* @__PURE__ */ new Date()) {
|
|
13791
|
+
return now instanceof Date ? now.toISOString() : new Date(now).toISOString();
|
|
13792
|
+
}
|
|
13793
|
+
function normalizeClickUpTaskId(value = "") {
|
|
13794
|
+
return String(value || "").trim();
|
|
13795
|
+
}
|
|
13796
|
+
function emptyQaQueueState(provider = DEFAULT_QA_PROVIDER) {
|
|
13797
|
+
return {
|
|
13798
|
+
provider: normalizeQaProvider(provider),
|
|
13799
|
+
active: null,
|
|
13800
|
+
queue: [],
|
|
13801
|
+
history: []
|
|
13802
|
+
};
|
|
13803
|
+
}
|
|
13804
|
+
function readQaQueueState(provider = DEFAULT_QA_PROVIDER, statePath = qaQueueStatePath(provider)) {
|
|
13805
|
+
try {
|
|
13806
|
+
const raw = fs16.readFileSync(statePath, "utf8");
|
|
13807
|
+
const parsed = JSON.parse(raw);
|
|
13808
|
+
if (!isPlainObject(parsed)) return emptyQaQueueState(provider);
|
|
13809
|
+
return {
|
|
13810
|
+
...emptyQaQueueState(provider),
|
|
13811
|
+
...parsed,
|
|
13812
|
+
provider: normalizeQaProvider(parsed.provider || provider),
|
|
13813
|
+
queue: Array.isArray(parsed.queue) ? parsed.queue.filter(isPlainObject) : [],
|
|
13814
|
+
history: Array.isArray(parsed.history) ? parsed.history.filter(isPlainObject).slice(-50) : []
|
|
13815
|
+
};
|
|
13816
|
+
} catch {
|
|
13817
|
+
return emptyQaQueueState(provider);
|
|
13818
|
+
}
|
|
13819
|
+
}
|
|
13820
|
+
function writeQaQueueState(state, statePath = qaQueueStatePath(state?.provider)) {
|
|
13821
|
+
const normalized = {
|
|
13822
|
+
...emptyQaQueueState(state?.provider),
|
|
13823
|
+
...state,
|
|
13824
|
+
provider: normalizeQaProvider(state?.provider),
|
|
13825
|
+
queue: Array.isArray(state?.queue) ? state.queue.filter(isPlainObject) : [],
|
|
13826
|
+
history: Array.isArray(state?.history) ? state.history.filter(isPlainObject).slice(-50) : []
|
|
13827
|
+
};
|
|
13828
|
+
fs16.mkdirSync(path17.dirname(statePath), { recursive: true });
|
|
13829
|
+
fs16.writeFileSync(statePath, `${JSON.stringify(normalized, null, 2)}
|
|
13830
|
+
`, { mode: 384 });
|
|
13831
|
+
return normalized;
|
|
13832
|
+
}
|
|
13833
|
+
function activeExpired(active, nowMs, leaseMs) {
|
|
13834
|
+
if (!active?.clickupTaskId) return false;
|
|
13835
|
+
const last = Date.parse(active.lastActivityAt || active.grantedAt || "");
|
|
13836
|
+
return Number.isFinite(last) && nowMs - last > leaseMs;
|
|
13837
|
+
}
|
|
13838
|
+
function queuedIndex(state, clickupTaskId) {
|
|
13839
|
+
return state.queue.findIndex((entry) => entry.clickupTaskId === clickupTaskId);
|
|
13840
|
+
}
|
|
13841
|
+
function queueEntry({ clickupTaskId, sessionId = "", worktree = "", reason = "", now = /* @__PURE__ */ new Date() } = {}) {
|
|
13842
|
+
return {
|
|
13843
|
+
clickupTaskId,
|
|
13844
|
+
sessionId: String(sessionId || "").trim(),
|
|
13845
|
+
worktree: String(worktree || "").trim(),
|
|
13846
|
+
reason: String(reason || "").trim(),
|
|
13847
|
+
queuedAt: nowIso(now),
|
|
13848
|
+
lastRequestAt: nowIso(now)
|
|
13849
|
+
};
|
|
13850
|
+
}
|
|
13851
|
+
function activeEntry({ provider, clickupTaskId, sessionId = "", worktree = "", leaseMs = DEFAULT_QA_LEASE_MS, now = /* @__PURE__ */ new Date() } = {}) {
|
|
13852
|
+
return {
|
|
13853
|
+
provider: normalizeQaProvider(provider),
|
|
13854
|
+
clickupTaskId,
|
|
13855
|
+
sessionId: String(sessionId || "").trim(),
|
|
13856
|
+
worktree: String(worktree || "").trim(),
|
|
13857
|
+
grantedAt: nowIso(now),
|
|
13858
|
+
lastActivityAt: nowIso(now),
|
|
13859
|
+
leaseMs
|
|
13860
|
+
};
|
|
13861
|
+
}
|
|
13862
|
+
function releaseExpiredQaSlot(state, { now = /* @__PURE__ */ new Date(), leaseMs = DEFAULT_QA_LEASE_MS } = {}) {
|
|
13863
|
+
const normalized = { ...emptyQaQueueState(state?.provider), ...state, queue: Array.isArray(state?.queue) ? [...state.queue] : [] };
|
|
13864
|
+
if (!activeExpired(normalized.active, now.getTime(), leaseMs)) return { state: normalized, expired: null, promoted: null };
|
|
13865
|
+
const expired = normalized.active;
|
|
13866
|
+
normalized.history = [...normalized.history || [], { action: "expired", clickupTaskId: expired.clickupTaskId, at: nowIso(now) }].slice(-50);
|
|
13867
|
+
normalized.active = null;
|
|
13868
|
+
const promoted = normalized.queue.shift() || null;
|
|
13869
|
+
if (promoted) {
|
|
13870
|
+
normalized.active = activeEntry({ ...promoted, provider: normalized.provider, leaseMs, now });
|
|
13871
|
+
normalized.history = [...normalized.history || [], { action: "promoted", clickupTaskId: promoted.clickupTaskId, at: nowIso(now), reason: "expired_previous_slot" }].slice(-50);
|
|
13872
|
+
}
|
|
13873
|
+
return { state: normalized, expired, promoted };
|
|
13874
|
+
}
|
|
13875
|
+
function requestQaSlot(state, { clickupTaskId, sessionId = "", worktree = "", reason = "", now = /* @__PURE__ */ new Date(), leaseMs = DEFAULT_QA_LEASE_MS } = {}) {
|
|
13876
|
+
const taskId = normalizeClickUpTaskId(clickupTaskId);
|
|
13877
|
+
if (!taskId) return { state, ok: false, action: "invalid", reason: "clickup_task_id_required" };
|
|
13878
|
+
let next = { ...emptyQaQueueState(state?.provider), ...state, queue: Array.isArray(state?.queue) ? [...state.queue] : [] };
|
|
13879
|
+
const expiredResult = releaseExpiredQaSlot(next, { now, leaseMs });
|
|
13880
|
+
next = expiredResult.state;
|
|
13881
|
+
if (!next.active) {
|
|
13882
|
+
next.active = activeEntry({ provider: next.provider, clickupTaskId: taskId, sessionId, worktree, leaseMs, now });
|
|
13883
|
+
next.queue = next.queue.filter((entry) => entry.clickupTaskId !== taskId);
|
|
13884
|
+
next.history = [...next.history || [], { action: "granted", clickupTaskId: taskId, at: nowIso(now) }].slice(-50);
|
|
13885
|
+
return { state: next, ok: true, action: "granted", active: next.active, expired: expiredResult.expired, promoted: expiredResult.promoted };
|
|
13886
|
+
}
|
|
13887
|
+
if (next.active.clickupTaskId === taskId) {
|
|
13888
|
+
next.active = { ...next.active, sessionId: sessionId || next.active.sessionId || "", worktree: worktree || next.active.worktree || "", lastActivityAt: nowIso(now), leaseMs };
|
|
13889
|
+
next.history = [...next.history || [], { action: "renewed", clickupTaskId: taskId, at: nowIso(now) }].slice(-50);
|
|
13890
|
+
return { state: next, ok: true, action: "granted_existing", active: next.active, expired: expiredResult.expired, promoted: expiredResult.promoted };
|
|
13891
|
+
}
|
|
13892
|
+
const index = queuedIndex(next, taskId);
|
|
13893
|
+
if (index >= 0) {
|
|
13894
|
+
next.queue[index] = { ...next.queue[index], sessionId: sessionId || next.queue[index].sessionId || "", worktree: worktree || next.queue[index].worktree || "", reason: reason || next.queue[index].reason || "", lastRequestAt: nowIso(now) };
|
|
13895
|
+
} else {
|
|
13896
|
+
next.queue.push(queueEntry({ clickupTaskId: taskId, sessionId, worktree, reason, now }));
|
|
13897
|
+
}
|
|
13898
|
+
return {
|
|
13899
|
+
state: next,
|
|
13900
|
+
ok: false,
|
|
13901
|
+
action: "queued",
|
|
13902
|
+
active: next.active,
|
|
13903
|
+
position: queuedIndex(next, taskId) + 1,
|
|
13904
|
+
expired: expiredResult.expired,
|
|
13905
|
+
promoted: expiredResult.promoted
|
|
13906
|
+
};
|
|
13907
|
+
}
|
|
13908
|
+
function touchQaSlot(state, { clickupTaskId, now = /* @__PURE__ */ new Date(), leaseMs = DEFAULT_QA_LEASE_MS } = {}) {
|
|
13909
|
+
const taskId = normalizeClickUpTaskId(clickupTaskId);
|
|
13910
|
+
let next = { ...emptyQaQueueState(state?.provider), ...state, queue: Array.isArray(state?.queue) ? [...state.queue] : [] };
|
|
13911
|
+
const expiredResult = releaseExpiredQaSlot(next, { now, leaseMs });
|
|
13912
|
+
next = expiredResult.state;
|
|
13913
|
+
if (!next.active || next.active.clickupTaskId !== taskId) return { state: next, ok: false, action: "not_active_holder", active: next.active, expired: expiredResult.expired, promoted: expiredResult.promoted };
|
|
13914
|
+
next.active = { ...next.active, lastActivityAt: nowIso(now), leaseMs };
|
|
13915
|
+
return { state: next, ok: true, action: "touched", active: next.active, expired: expiredResult.expired, promoted: expiredResult.promoted };
|
|
13916
|
+
}
|
|
13917
|
+
function finishQaSlot(state, { clickupTaskId, now = /* @__PURE__ */ new Date(), leaseMs = DEFAULT_QA_LEASE_MS } = {}) {
|
|
13918
|
+
const taskId = normalizeClickUpTaskId(clickupTaskId);
|
|
13919
|
+
let next = { ...emptyQaQueueState(state?.provider), ...state, queue: Array.isArray(state?.queue) ? [...state.queue] : [] };
|
|
13920
|
+
const expiredResult = releaseExpiredQaSlot(next, { now, leaseMs });
|
|
13921
|
+
next = expiredResult.state;
|
|
13922
|
+
if (!next.active || next.active.clickupTaskId !== taskId) return { state: next, ok: false, action: "not_active_holder", active: next.active, expired: expiredResult.expired, promoted: expiredResult.promoted };
|
|
13923
|
+
const released = next.active;
|
|
13924
|
+
next.active = null;
|
|
13925
|
+
next.history = [...next.history || [], { action: "finished", clickupTaskId: taskId, at: nowIso(now) }].slice(-50);
|
|
13926
|
+
const promoted = next.queue.shift() || null;
|
|
13927
|
+
if (promoted) {
|
|
13928
|
+
next.active = activeEntry({ ...promoted, provider: next.provider, leaseMs, now });
|
|
13929
|
+
next.history = [...next.history || [], { action: "promoted", clickupTaskId: promoted.clickupTaskId, at: nowIso(now), reason: "previous_finished" }].slice(-50);
|
|
13930
|
+
}
|
|
13931
|
+
return { state: next, ok: true, action: "finished", released, promoted };
|
|
13932
|
+
}
|
|
13933
|
+
|
|
13934
|
+
// src/qa/chrome.js
|
|
13935
|
+
var DEFAULT_QA_CDP_URL = "http://127.0.0.1:9222";
|
|
13936
|
+
var DEFAULT_QA_PLAYWRIGHT_CONNECT_TIMEOUT_MS = 5e3;
|
|
13937
|
+
function providerDir(provider = DEFAULT_QA_PROVIDER) {
|
|
13938
|
+
return path18.join(qaRuntimeDir(), normalizeQaProvider(provider));
|
|
13939
|
+
}
|
|
13940
|
+
function defaultQaExtensionStageDir(provider = DEFAULT_QA_PROVIDER) {
|
|
13941
|
+
return path18.join(providerDir(provider), "extension-under-test");
|
|
13942
|
+
}
|
|
13943
|
+
function defaultQaArtifactsDir(provider = DEFAULT_QA_PROVIDER, clickupTaskId = "unknown") {
|
|
13944
|
+
return path18.join(providerDir(provider), "artifacts", String(clickupTaskId || "unknown"));
|
|
13945
|
+
}
|
|
13946
|
+
function normalizeCdpUrl(value = "") {
|
|
13947
|
+
return String(value || process.env.OPTIMA_QA_CHROME_CDP_URL || DEFAULT_QA_CDP_URL).trim() || DEFAULT_QA_CDP_URL;
|
|
13948
|
+
}
|
|
13949
|
+
async function loadPlaywrightChromium() {
|
|
13950
|
+
try {
|
|
13951
|
+
const mod = await import("playwright-core");
|
|
13952
|
+
return mod.chromium;
|
|
13953
|
+
} catch (error) {
|
|
13954
|
+
throw new Error(`playwright-core is required for Optima QA browser tools: ${error.message}`);
|
|
13955
|
+
}
|
|
13956
|
+
}
|
|
13957
|
+
function qaWindowName(clickupTaskId) {
|
|
13958
|
+
return `optima-qa:${String(clickupTaskId || "").trim()}`;
|
|
13959
|
+
}
|
|
13960
|
+
async function safePageWindowName(page) {
|
|
13961
|
+
try {
|
|
13962
|
+
return await page.evaluate(() => window.name || "");
|
|
13963
|
+
} catch {
|
|
13964
|
+
return "";
|
|
13965
|
+
}
|
|
13966
|
+
}
|
|
13967
|
+
async function findQaPage(context, clickupTaskId) {
|
|
13968
|
+
const expected = qaWindowName(clickupTaskId);
|
|
13969
|
+
const pages = context.pages();
|
|
13970
|
+
for (let index = 1; index < pages.length; index += 1) {
|
|
13971
|
+
const page = pages[index];
|
|
13972
|
+
if (await safePageWindowName(page) === expected) return page;
|
|
13973
|
+
}
|
|
13974
|
+
return null;
|
|
13975
|
+
}
|
|
13976
|
+
async function ensurePersistentTab(context, url = "https://chatgpt.com/") {
|
|
13977
|
+
const pages = context.pages();
|
|
13978
|
+
if (pages[0]) return pages[0];
|
|
13979
|
+
const page = await context.newPage();
|
|
13980
|
+
await page.goto(url, { waitUntil: "domcontentloaded", timeout: 3e4 }).catch(() => {
|
|
13981
|
+
});
|
|
13982
|
+
return page;
|
|
13983
|
+
}
|
|
13984
|
+
async function ensureQaPage(context, { clickupTaskId, startUrl = "https://chatgpt.com/" } = {}) {
|
|
13985
|
+
await ensurePersistentTab(context, startUrl);
|
|
13986
|
+
const existing = await findQaPage(context, clickupTaskId);
|
|
13987
|
+
if (existing) return existing;
|
|
13988
|
+
const page = await context.newPage();
|
|
13989
|
+
await page.goto(startUrl, { waitUntil: "domcontentloaded", timeout: 3e4 }).catch(() => {
|
|
13990
|
+
});
|
|
13991
|
+
await page.evaluate((name) => {
|
|
13992
|
+
window.name = name;
|
|
13993
|
+
}, qaWindowName(clickupTaskId)).catch(() => {
|
|
13994
|
+
});
|
|
13995
|
+
return page;
|
|
13996
|
+
}
|
|
13997
|
+
function copyExtensionSource(sourcePath, stageDir) {
|
|
13998
|
+
const source = String(sourcePath || "").trim();
|
|
13999
|
+
if (!source) return { ok: true, skipped: true, reason: "extension_path_not_provided", stageDir };
|
|
14000
|
+
const resolved = path18.resolve(source.replace(/^~(?=$|\/)/, os6.homedir()));
|
|
14001
|
+
if (!fs17.existsSync(resolved)) return { ok: false, error: `extension_path_not_found: ${resolved}`, stageDir };
|
|
14002
|
+
fs17.rmSync(stageDir, { recursive: true, force: true });
|
|
14003
|
+
fs17.mkdirSync(path18.dirname(stageDir), { recursive: true });
|
|
14004
|
+
fs17.cpSync(resolved, stageDir, {
|
|
14005
|
+
recursive: true,
|
|
14006
|
+
filter: (src) => !/[/\\](node_modules|\.git|dist|build|test-results)([/\\]|$)/.test(src)
|
|
14007
|
+
});
|
|
14008
|
+
return { ok: true, source: resolved, stageDir };
|
|
14009
|
+
}
|
|
14010
|
+
function extensionIdFromWorkerUrl(url = "") {
|
|
14011
|
+
const match = String(url || "").match(/^chrome-extension:\/\/([^/]+)/);
|
|
14012
|
+
return match?.[1] || "";
|
|
14013
|
+
}
|
|
14014
|
+
async function findExtensionWorker(context, extensionId = "") {
|
|
14015
|
+
const workers = context.serviceWorkers ? context.serviceWorkers() : [];
|
|
14016
|
+
const normalized = String(extensionId || "").trim();
|
|
14017
|
+
if (normalized) return workers.find((worker) => worker.url().startsWith(`chrome-extension://${normalized}/`)) || null;
|
|
14018
|
+
return workers.find((worker) => String(worker.url()).startsWith("chrome-extension://")) || null;
|
|
14019
|
+
}
|
|
14020
|
+
async function resetExtensionState(context, { extensionId = "" } = {}) {
|
|
14021
|
+
const worker = await findExtensionWorker(context, extensionId);
|
|
14022
|
+
if (!worker) return { ok: false, error: "extension_service_worker_not_found" };
|
|
14023
|
+
const id = extensionIdFromWorkerUrl(worker.url());
|
|
14024
|
+
const result = await worker.evaluate(async () => {
|
|
14025
|
+
const deletedCaches = [];
|
|
14026
|
+
const deletedDbs = [];
|
|
14027
|
+
if (globalThis.chrome?.storage?.local) await chrome.storage.local.clear();
|
|
14028
|
+
if (globalThis.chrome?.storage?.session?.clear) await chrome.storage.session.clear();
|
|
14029
|
+
if (globalThis.chrome?.storage?.sync) await chrome.storage.sync.clear();
|
|
14030
|
+
if (globalThis.caches?.keys) {
|
|
14031
|
+
for (const name of await caches.keys()) {
|
|
14032
|
+
await caches.delete(name);
|
|
14033
|
+
deletedCaches.push(name);
|
|
14034
|
+
}
|
|
14035
|
+
}
|
|
14036
|
+
if (globalThis.indexedDB?.databases) {
|
|
14037
|
+
for (const db of await indexedDB.databases()) {
|
|
14038
|
+
if (db.name) {
|
|
14039
|
+
indexedDB.deleteDatabase(db.name);
|
|
14040
|
+
deletedDbs.push(db.name);
|
|
14041
|
+
}
|
|
14042
|
+
}
|
|
14043
|
+
}
|
|
14044
|
+
return { ok: true, deletedCaches, deletedDbs };
|
|
14045
|
+
});
|
|
14046
|
+
return { extensionId: id, ...result };
|
|
14047
|
+
}
|
|
14048
|
+
async function reloadExtension(context, { extensionId = "" } = {}) {
|
|
14049
|
+
const worker = await findExtensionWorker(context, extensionId);
|
|
14050
|
+
if (!worker) return { ok: false, error: "extension_service_worker_not_found" };
|
|
14051
|
+
const id = extensionIdFromWorkerUrl(worker.url());
|
|
14052
|
+
await worker.evaluate(() => chrome.runtime.reload()).catch(() => {
|
|
14053
|
+
});
|
|
14054
|
+
return { ok: true, extensionId: id };
|
|
14055
|
+
}
|
|
14056
|
+
async function summarizeBrowser(context, { clickupTaskId = "" } = {}) {
|
|
14057
|
+
const pages = context.pages();
|
|
14058
|
+
const pageSummaries = [];
|
|
14059
|
+
for (let index = 0; index < pages.length; index += 1) {
|
|
14060
|
+
const page = pages[index];
|
|
14061
|
+
pageSummaries.push({
|
|
14062
|
+
index,
|
|
14063
|
+
role: index === 0 ? "persistent_session_tab" : await safePageWindowName(page) === qaWindowName(clickupTaskId) ? "qa_task_tab" : "other",
|
|
14064
|
+
url: page.url(),
|
|
14065
|
+
title: await page.title().catch(() => "")
|
|
14066
|
+
});
|
|
14067
|
+
}
|
|
14068
|
+
const workers = context.serviceWorkers ? context.serviceWorkers().map((worker) => worker.url()) : [];
|
|
14069
|
+
return { pages: pageSummaries, serviceWorkers: workers };
|
|
14070
|
+
}
|
|
14071
|
+
function parseQaCommand(commandJson = "") {
|
|
14072
|
+
if (!String(commandJson || "").trim()) return { action: "status" };
|
|
14073
|
+
try {
|
|
14074
|
+
const parsed = JSON.parse(commandJson);
|
|
14075
|
+
return parsed && typeof parsed === "object" ? parsed : { action: "invalid", error: "command_json_must_be_object" };
|
|
14076
|
+
} catch (error) {
|
|
14077
|
+
return { action: "script", script: String(commandJson || "") };
|
|
14078
|
+
}
|
|
14079
|
+
}
|
|
14080
|
+
async function runQaCommandOnPage(page, context, browser, command, { artifactsDir }) {
|
|
14081
|
+
const action = String(command.action || "status").trim().toLowerCase();
|
|
14082
|
+
if (action === "status") return { ok: true, page: { url: page.url(), title: await page.title().catch(() => "") } };
|
|
14083
|
+
if (action === "goto") {
|
|
14084
|
+
await page.goto(String(command.url || "https://chatgpt.com/"), { waitUntil: command.wait_until || "domcontentloaded", timeout: Number(command.timeout_ms || 3e4) });
|
|
14085
|
+
return { ok: true, url: page.url(), title: await page.title().catch(() => "") };
|
|
14086
|
+
}
|
|
14087
|
+
if (action === "evaluate") {
|
|
14088
|
+
const result = await page.evaluate(String(command.expression || "() => null"));
|
|
14089
|
+
return { ok: true, result };
|
|
14090
|
+
}
|
|
14091
|
+
if (action === "screenshot") {
|
|
14092
|
+
const name = String(command.name || `screenshot-${Date.now()}.png`).replace(/[^a-zA-Z0-9._-]+/g, "-");
|
|
14093
|
+
const outPath = path18.join(artifactsDir, name);
|
|
14094
|
+
fs17.mkdirSync(path18.dirname(outPath), { recursive: true });
|
|
14095
|
+
await page.screenshot({ path: outPath, fullPage: command.full_page !== false });
|
|
14096
|
+
return { ok: true, path: outPath };
|
|
14097
|
+
}
|
|
14098
|
+
if (action === "set_input_files") {
|
|
14099
|
+
const selector = String(command.selector || "input[type=file]");
|
|
14100
|
+
await page.setInputFiles(selector, command.files);
|
|
14101
|
+
return { ok: true, selector };
|
|
14102
|
+
}
|
|
14103
|
+
if (action === "script") {
|
|
14104
|
+
const script = String(command.script || "");
|
|
14105
|
+
if (!script.trim()) return { ok: false, error: "script_required" };
|
|
14106
|
+
const fn = new Function("page", "context", "browser", "artifactsDir", `return (async () => {
|
|
14107
|
+
${script}
|
|
14108
|
+
})()`);
|
|
14109
|
+
const result = await fn(page, context, browser, artifactsDir);
|
|
14110
|
+
return { ok: true, result };
|
|
14111
|
+
}
|
|
14112
|
+
return { ok: false, error: `unsupported_qa_command_action: ${action}` };
|
|
14113
|
+
}
|
|
14114
|
+
async function withQaBrowser({ provider = DEFAULT_QA_PROVIDER, cdpUrl = "", clickupTaskId = "", startUrl = "https://chatgpt.com/" } = {}, callback) {
|
|
14115
|
+
const chromium = await loadPlaywrightChromium();
|
|
14116
|
+
const browser = await chromium.connectOverCDP(normalizeCdpUrl(cdpUrl), {
|
|
14117
|
+
timeout: Number(process.env.OPTIMA_QA_PLAYWRIGHT_CONNECT_TIMEOUT_MS || DEFAULT_QA_PLAYWRIGHT_CONNECT_TIMEOUT_MS)
|
|
14118
|
+
});
|
|
14119
|
+
try {
|
|
14120
|
+
const context = browser.contexts()[0] || await browser.newContext();
|
|
14121
|
+
const page = clickupTaskId ? await ensureQaPage(context, { clickupTaskId, startUrl }) : null;
|
|
14122
|
+
return await callback({ browser, context, page });
|
|
14123
|
+
} finally {
|
|
14124
|
+
await browser.close().catch(() => {
|
|
14125
|
+
});
|
|
14126
|
+
}
|
|
14127
|
+
}
|
|
14128
|
+
async function qaBrowserStatus(options = {}) {
|
|
14129
|
+
return withQaBrowser(options, async ({ context }) => ({ ok: true, ...await summarizeBrowser(context, { clickupTaskId: options.clickupTaskId }) })).catch(async (error) => {
|
|
14130
|
+
const fallback = await cdpBrowserStatus({ cdpUrl: options.cdpUrl, clickupTaskId: options.clickupTaskId, qaWindowName }).catch((fallbackError) => ({ ok: false, error: fallbackError.message }));
|
|
14131
|
+
return fallback.ok ? { ...fallback, fallback: "cdp", playwrightError: error.message } : { ok: false, error: error.message, fallbackError: fallback.error, cdpUrl: normalizeCdpUrl(options.cdpUrl) };
|
|
14132
|
+
});
|
|
14133
|
+
}
|
|
14134
|
+
async function qaPrepareExtension({ provider = DEFAULT_QA_PROVIDER, cdpUrl = "", extensionPath = "", extensionId = "", reset = true, reload = true } = {}) {
|
|
14135
|
+
const stageDir = defaultQaExtensionStageDir(provider);
|
|
14136
|
+
const sync = copyExtensionSource(extensionPath, stageDir);
|
|
14137
|
+
const browserResult = await withQaBrowser({ provider, cdpUrl }, async ({ context }) => {
|
|
14138
|
+
const resetResult = reset ? await resetExtensionState(context, { extensionId }).catch((error) => ({ ok: false, error: error.message })) : { ok: true, skipped: true };
|
|
14139
|
+
const reloadResult = reload ? await reloadExtension(context, { extensionId }).catch((error) => ({ ok: false, error: error.message })) : { ok: true, skipped: true };
|
|
14140
|
+
return { reset: resetResult, reload: reloadResult, browser: await summarizeBrowser(context) };
|
|
14141
|
+
}).catch(async (error) => {
|
|
14142
|
+
const fallback = await cdpPrepareExtension({ cdpUrl, extensionId, reset, reload }).catch((fallbackError) => ({ ok: false, error: fallbackError.message }));
|
|
14143
|
+
return fallback.ok ? { ...fallback, fallback: "cdp", playwrightError: error.message } : { ok: false, error: error.message, fallbackError: fallback.error };
|
|
14144
|
+
});
|
|
14145
|
+
return { sync, ...browserResult };
|
|
14146
|
+
}
|
|
14147
|
+
async function qaRunChromeCommand({ provider = DEFAULT_QA_PROVIDER, cdpUrl = "", clickupTaskId = "", commandJson = "", startUrl = "https://chatgpt.com/" } = {}) {
|
|
14148
|
+
const command = parseQaCommand(commandJson);
|
|
14149
|
+
const artifactsDir = defaultQaArtifactsDir(provider, clickupTaskId);
|
|
14150
|
+
return withQaBrowser({ provider, cdpUrl, clickupTaskId, startUrl }, async ({ browser, context, page }) => {
|
|
14151
|
+
const result = await runQaCommandOnPage(page, context, browser, command, { artifactsDir });
|
|
14152
|
+
return { ok: result.ok !== false, command: command.action || "script", tab: { windowName: qaWindowName(clickupTaskId), url: page.url(), title: await page.title().catch(() => "") }, artifactsDir, result };
|
|
14153
|
+
}).catch(async (error) => {
|
|
14154
|
+
const fallback = await cdpRunChromeCommand({
|
|
14155
|
+
cdpUrl,
|
|
14156
|
+
clickupTaskId,
|
|
14157
|
+
commandJson,
|
|
14158
|
+
startUrl,
|
|
14159
|
+
artifactsDir,
|
|
14160
|
+
qaWindowName
|
|
14161
|
+
}).catch((fallbackError) => ({ ok: false, error: fallbackError.message }));
|
|
14162
|
+
return fallback.ok ? { ...fallback, fallback: "cdp", playwrightError: error.message } : { ok: false, error: error.message, fallbackError: fallback.error };
|
|
14163
|
+
});
|
|
14164
|
+
}
|
|
14165
|
+
|
|
13509
14166
|
// src/repair.js
|
|
13510
14167
|
var import_yaml4 = __toESM(require_dist(), 1);
|
|
13511
14168
|
var import_ignore = __toESM(require_ignore(), 1);
|
|
13512
|
-
import
|
|
13513
|
-
import
|
|
14169
|
+
import fs18 from "node:fs";
|
|
14170
|
+
import path19 from "node:path";
|
|
13514
14171
|
var CODEMAP_SOURCE_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
13515
14172
|
".js",
|
|
13516
14173
|
".ts",
|
|
@@ -13548,10 +14205,10 @@ var LEGACY_OPERATIONAL_ROOTS = /* @__PURE__ */ new Set([
|
|
|
13548
14205
|
]);
|
|
13549
14206
|
var OPTIMA_OPERATIONAL_FOLDERS = [".optima", "templates", "dist"];
|
|
13550
14207
|
function relPath(worktree, targetPath) {
|
|
13551
|
-
return
|
|
14208
|
+
return path19.relative(worktree, targetPath).split(path19.sep).join("/") || ".";
|
|
13552
14209
|
}
|
|
13553
14210
|
function normalizeCodemapRelPath(relPathValue) {
|
|
13554
|
-
return
|
|
14211
|
+
return path19.normalize(relPathValue).replace(/\\/g, "/").replace(/\/$/, "");
|
|
13555
14212
|
}
|
|
13556
14213
|
function firstPathSegment(relPathValue) {
|
|
13557
14214
|
return normalizeCodemapRelPath(relPathValue).split("/").filter(Boolean)[0] || "";
|
|
@@ -13570,8 +14227,8 @@ function isHiddenTree(relPathValue) {
|
|
|
13570
14227
|
function loadGitIgnoreMatcher(worktree) {
|
|
13571
14228
|
const ig = (0, import_ignore.default)();
|
|
13572
14229
|
ig.add(".git");
|
|
13573
|
-
const gitignorePath =
|
|
13574
|
-
if (
|
|
14230
|
+
const gitignorePath = path19.join(worktree, ".gitignore");
|
|
14231
|
+
if (fs18.existsSync(gitignorePath)) ig.add(fs18.readFileSync(gitignorePath, "utf8"));
|
|
13575
14232
|
return ig;
|
|
13576
14233
|
}
|
|
13577
14234
|
function codemapSectionEntries(value) {
|
|
@@ -13590,7 +14247,7 @@ function codemapIndexedPaths(map) {
|
|
|
13590
14247
|
}
|
|
13591
14248
|
function readCodemap(filePath, unresolved, worktree) {
|
|
13592
14249
|
try {
|
|
13593
|
-
const parsed = import_yaml4.default.parse(
|
|
14250
|
+
const parsed = import_yaml4.default.parse(fs18.readFileSync(filePath, "utf8"));
|
|
13594
14251
|
if (!parsed || typeof parsed !== "object") {
|
|
13595
14252
|
unresolved.push({ category: "codemap", path: relPath(worktree, filePath), message: "CodeMap is empty or invalid; manual repair required." });
|
|
13596
14253
|
return null;
|
|
@@ -13602,42 +14259,42 @@ function readCodemap(filePath, unresolved, worktree) {
|
|
|
13602
14259
|
}
|
|
13603
14260
|
}
|
|
13604
14261
|
function writeCodemap(filePath, map) {
|
|
13605
|
-
|
|
14262
|
+
fs18.writeFileSync(filePath, import_yaml4.default.stringify(map), "utf8");
|
|
13606
14263
|
}
|
|
13607
14264
|
function isIgnoredPath(ig, relativePath) {
|
|
13608
14265
|
const normalized = normalizeCodemapRelPath(relativePath);
|
|
13609
14266
|
return Boolean(normalized) && ig.ignores(normalized);
|
|
13610
14267
|
}
|
|
13611
14268
|
function hasImmediateSourceFile(dirPath, ig, worktree) {
|
|
13612
|
-
if (!
|
|
13613
|
-
for (const item of
|
|
13614
|
-
const childPath =
|
|
13615
|
-
const relative =
|
|
14269
|
+
if (!fs18.existsSync(dirPath)) return false;
|
|
14270
|
+
for (const item of fs18.readdirSync(dirPath, { withFileTypes: true })) {
|
|
14271
|
+
const childPath = path19.join(dirPath, item.name);
|
|
14272
|
+
const relative = path19.relative(worktree, childPath);
|
|
13616
14273
|
if (isIgnoredPath(ig, relative) || isOperationalRelPath(relative)) continue;
|
|
13617
|
-
if (item.isFile() && CODEMAP_SOURCE_EXTENSIONS.has(
|
|
14274
|
+
if (item.isFile() && CODEMAP_SOURCE_EXTENSIONS.has(path19.extname(item.name))) return true;
|
|
13618
14275
|
}
|
|
13619
14276
|
return false;
|
|
13620
14277
|
}
|
|
13621
14278
|
function containsSource(dirPath, ig, worktree) {
|
|
13622
|
-
if (!
|
|
13623
|
-
const dirRelPath =
|
|
14279
|
+
if (!fs18.existsSync(dirPath)) return false;
|
|
14280
|
+
const dirRelPath = path19.relative(worktree, dirPath);
|
|
13624
14281
|
if (isOperationalRelPath(dirRelPath) || isHiddenTree(dirRelPath)) return false;
|
|
13625
|
-
for (const item of
|
|
13626
|
-
const childPath =
|
|
13627
|
-
const relative =
|
|
14282
|
+
for (const item of fs18.readdirSync(dirPath, { withFileTypes: true })) {
|
|
14283
|
+
const childPath = path19.join(dirPath, item.name);
|
|
14284
|
+
const relative = path19.relative(worktree, childPath);
|
|
13628
14285
|
if (isIgnoredPath(ig, relative) || isOperationalRelPath(relative) || isHiddenTree(relative)) continue;
|
|
13629
|
-
if (item.isFile() && CODEMAP_SOURCE_EXTENSIONS.has(
|
|
14286
|
+
if (item.isFile() && CODEMAP_SOURCE_EXTENSIONS.has(path19.extname(item.name))) return true;
|
|
13630
14287
|
if (item.isDirectory() && containsSource(childPath, ig, worktree)) return true;
|
|
13631
14288
|
}
|
|
13632
14289
|
return false;
|
|
13633
14290
|
}
|
|
13634
14291
|
function createModuleCodemap(dirPath, worktree, deps) {
|
|
13635
|
-
const entries =
|
|
14292
|
+
const entries = fs18.readdirSync(dirPath, { withFileTypes: true });
|
|
13636
14293
|
const entrypoints = [];
|
|
13637
14294
|
const internals = [];
|
|
13638
|
-
const relToRoot =
|
|
14295
|
+
const relToRoot = path19.relative(dirPath, deps.optimaCodemapPath(worktree)).split(path19.sep).join("/");
|
|
13639
14296
|
for (const item of entries) {
|
|
13640
|
-
if (!item.isFile() || item.name === "codemap.yml" || !CODEMAP_SOURCE_EXTENSIONS.has(
|
|
14297
|
+
if (!item.isFile() || item.name === "codemap.yml" || !CODEMAP_SOURCE_EXTENSIONS.has(path19.extname(item.name))) continue;
|
|
13641
14298
|
const bucket = /^index\.(js|ts|tsx|jsx)$|^main\.(js|ts|tsx|jsx|py|go|rs|java)$/.test(item.name) ? entrypoints : internals;
|
|
13642
14299
|
bucket.push({ path: item.name });
|
|
13643
14300
|
}
|
|
@@ -13650,7 +14307,7 @@ function repairSingleCodemap(codemapPath, worktree, apply, registerAction, unres
|
|
|
13650
14307
|
const map = readCodemap(codemapPath, unresolved, worktree);
|
|
13651
14308
|
if (!map) return;
|
|
13652
14309
|
const isRootMap = codemapPath === deps.optimaCodemapPath(worktree);
|
|
13653
|
-
const mapDir =
|
|
14310
|
+
const mapDir = path19.dirname(codemapPath);
|
|
13654
14311
|
const pathBase = isRootMap ? worktree : mapDir;
|
|
13655
14312
|
let changed = false;
|
|
13656
14313
|
for (const section of ["modules", "entrypoints", "sources_of_truth", "links", "internals"]) {
|
|
@@ -13661,13 +14318,13 @@ function repairSingleCodemap(codemapPath, worktree, apply, registerAction, unres
|
|
|
13661
14318
|
}
|
|
13662
14319
|
const nextEntries = [];
|
|
13663
14320
|
for (const entry of originalEntries) {
|
|
13664
|
-
if (!entry?.path || entry.path.startsWith("http://") || entry.path.startsWith("https://") ||
|
|
14321
|
+
if (!entry?.path || entry.path.startsWith("http://") || entry.path.startsWith("https://") || path19.isAbsolute(entry.path)) {
|
|
13665
14322
|
nextEntries.push(entry);
|
|
13666
14323
|
continue;
|
|
13667
14324
|
}
|
|
13668
14325
|
const normalizedPath = normalizeCodemapRelPath(entry.path);
|
|
13669
|
-
const absPath =
|
|
13670
|
-
if (!
|
|
14326
|
+
const absPath = path19.join(pathBase, normalizedPath);
|
|
14327
|
+
if (!fs18.existsSync(absPath)) {
|
|
13671
14328
|
registerAction({ category: "codemap", action: apply ? "removed" : "would_remove", path: relPath(worktree, codemapPath), detail: `Remove broken ${section.slice(0, -1)} reference '${entry.path}'.` });
|
|
13672
14329
|
changed = true;
|
|
13673
14330
|
continue;
|
|
@@ -13676,8 +14333,8 @@ function repairSingleCodemap(codemapPath, worktree, apply, registerAction, unres
|
|
|
13676
14333
|
const maxParts = isRootMap ? 2 : 1;
|
|
13677
14334
|
if (parts.length > maxParts) {
|
|
13678
14335
|
const localPath = isRootMap ? parts.slice(0, 2).join("/") : parts[0];
|
|
13679
|
-
const localAbs =
|
|
13680
|
-
if (
|
|
14336
|
+
const localAbs = path19.join(pathBase, localPath);
|
|
14337
|
+
if (fs18.existsSync(localAbs)) {
|
|
13681
14338
|
nextEntries.push({ ...entry, path: localPath });
|
|
13682
14339
|
changed = true;
|
|
13683
14340
|
registerAction({ category: "codemap", action: apply ? "updated" : "would_update", path: relPath(worktree, codemapPath), detail: `Shorten '${entry.path}' to local path '${localPath}'.` });
|
|
@@ -13691,11 +14348,11 @@ function repairSingleCodemap(codemapPath, worktree, apply, registerAction, unres
|
|
|
13691
14348
|
}
|
|
13692
14349
|
if (Array.isArray(map[section])) map[section] = nextEntries;
|
|
13693
14350
|
}
|
|
13694
|
-
if (map.scope === "module" && !isOperationalRelPath(
|
|
14351
|
+
if (map.scope === "module" && !isOperationalRelPath(path19.relative(worktree, mapDir))) {
|
|
13695
14352
|
const indexed = codemapIndexedPaths(map);
|
|
13696
14353
|
const internals = Array.isArray(map.internals) ? map.internals : [];
|
|
13697
|
-
for (const item of
|
|
13698
|
-
if (!item.isFile() || item.name === "codemap.yml" || !CODEMAP_SOURCE_EXTENSIONS.has(
|
|
14354
|
+
for (const item of fs18.readdirSync(mapDir, { withFileTypes: true })) {
|
|
14355
|
+
if (!item.isFile() || item.name === "codemap.yml" || !CODEMAP_SOURCE_EXTENSIONS.has(path19.extname(item.name))) continue;
|
|
13699
14356
|
if (indexed.has(item.name)) continue;
|
|
13700
14357
|
internals.push({ path: item.name });
|
|
13701
14358
|
indexed.add(item.name);
|
|
@@ -13709,13 +14366,13 @@ function repairSingleCodemap(codemapPath, worktree, apply, registerAction, unres
|
|
|
13709
14366
|
function repairCodemaps(worktree, apply, actions, unresolved, deps) {
|
|
13710
14367
|
const ig = loadGitIgnoreMatcher(worktree);
|
|
13711
14368
|
const rootCodemapPath = deps.optimaCodemapPath(worktree);
|
|
13712
|
-
const rootCodemapMissing = !
|
|
14369
|
+
const rootCodemapMissing = !fs18.existsSync(rootCodemapPath);
|
|
13713
14370
|
if (rootCodemapMissing) {
|
|
13714
14371
|
actions.push({ category: "codemap", action: apply ? "created" : "would_create", path: ".optima/codemap.yml", detail: "Create missing root CodeMap from template." });
|
|
13715
14372
|
if (apply) deps.scaffoldOptimaRootCodemap(worktree);
|
|
13716
14373
|
}
|
|
13717
|
-
if (
|
|
13718
|
-
const rootMap =
|
|
14374
|
+
if (fs18.existsSync(rootCodemapPath)) repairSingleCodemap(rootCodemapPath, worktree, apply, (action) => actions.push(action), unresolved, deps);
|
|
14375
|
+
const rootMap = fs18.existsSync(rootCodemapPath) ? readCodemap(rootCodemapPath, unresolved, worktree) : null;
|
|
13719
14376
|
let rootChanged = false;
|
|
13720
14377
|
const rootModulesRepairable = rootMap && (rootMap.modules === void 0 || Array.isArray(rootMap.modules));
|
|
13721
14378
|
if (rootMap) {
|
|
@@ -13727,10 +14384,10 @@ function repairCodemaps(worktree, apply, actions, unresolved, deps) {
|
|
|
13727
14384
|
}
|
|
13728
14385
|
}
|
|
13729
14386
|
function walk(dirPath) {
|
|
13730
|
-
const relative =
|
|
14387
|
+
const relative = path19.relative(worktree, dirPath);
|
|
13731
14388
|
if (relative && (isIgnoredPath(ig, relative) || isOperationalRelPath(relative) || isHiddenTree(relative))) return;
|
|
13732
|
-
const codemapPath =
|
|
13733
|
-
const hasCodemap =
|
|
14389
|
+
const codemapPath = path19.join(dirPath, "codemap.yml");
|
|
14390
|
+
const hasCodemap = fs18.existsSync(codemapPath);
|
|
13734
14391
|
const sourceDir = relative && containsSource(dirPath, ig, worktree);
|
|
13735
14392
|
if (sourceDir && !hasCodemap && dirPath !== worktree) {
|
|
13736
14393
|
if (hasImmediateSourceFile(dirPath, ig, worktree)) {
|
|
@@ -13749,19 +14406,19 @@ function repairCodemaps(worktree, apply, actions, unresolved, deps) {
|
|
|
13749
14406
|
actions.push({ category: "codemap", action: apply ? "updated" : "would_update", path: ".optima/codemap.yml", detail: `Register top-level source module '${normalizeCodemapRelPath(relative)}'.` });
|
|
13750
14407
|
}
|
|
13751
14408
|
}
|
|
13752
|
-
if (
|
|
13753
|
-
for (const item of
|
|
13754
|
-
if (item.isDirectory()) walk(
|
|
14409
|
+
if (fs18.existsSync(codemapPath) && codemapPath !== rootCodemapPath) repairSingleCodemap(codemapPath, worktree, apply, (action) => actions.push(action), unresolved, deps);
|
|
14410
|
+
for (const item of fs18.readdirSync(dirPath, { withFileTypes: true })) {
|
|
14411
|
+
if (item.isDirectory()) walk(path19.join(dirPath, item.name));
|
|
13755
14412
|
}
|
|
13756
14413
|
}
|
|
13757
|
-
if (
|
|
14414
|
+
if (fs18.existsSync(worktree)) walk(worktree);
|
|
13758
14415
|
if (rootMap && rootChanged && apply) writeCodemap(rootCodemapPath, rootMap);
|
|
13759
14416
|
}
|
|
13760
14417
|
function walkMarkdownFiles(dirPath) {
|
|
13761
|
-
if (!
|
|
14418
|
+
if (!fs18.existsSync(dirPath)) return [];
|
|
13762
14419
|
const files = [];
|
|
13763
|
-
for (const entry of
|
|
13764
|
-
const entryPath =
|
|
14420
|
+
for (const entry of fs18.readdirSync(dirPath, { withFileTypes: true })) {
|
|
14421
|
+
const entryPath = path19.join(dirPath, entry.name);
|
|
13765
14422
|
if (entry.isDirectory()) {
|
|
13766
14423
|
files.push(...walkMarkdownFiles(entryPath));
|
|
13767
14424
|
} else if (entry.isFile() && entry.name.endsWith(".md")) {
|
|
@@ -13800,7 +14457,7 @@ function rewriteOptimaMarkdownReferences(content, evidenceBasePath = null) {
|
|
|
13800
14457
|
}
|
|
13801
14458
|
function optimaEvidenceBaseForFile(worktree, filePath, deps) {
|
|
13802
14459
|
const evidenceRoot = deps.optimaEvidencesDir(worktree);
|
|
13803
|
-
const relative =
|
|
14460
|
+
const relative = path19.relative(evidenceRoot, filePath).split(path19.sep).join("/");
|
|
13804
14461
|
const [taskId] = relative.split("/");
|
|
13805
14462
|
if (!taskId || taskId === ".." || relative.startsWith("../")) return null;
|
|
13806
14463
|
return `.optima/evidences/${taskId}`;
|
|
@@ -13813,11 +14470,11 @@ function normalizeOptimaMarkdownReferences(worktree, deps) {
|
|
|
13813
14470
|
];
|
|
13814
14471
|
const changed = [];
|
|
13815
14472
|
for (const filePath of scanRoots.flatMap(walkMarkdownFiles)) {
|
|
13816
|
-
const raw =
|
|
13817
|
-
const evidenceBasePath = filePath.startsWith(deps.optimaEvidencesDir(worktree) +
|
|
14473
|
+
const raw = fs18.readFileSync(filePath, "utf8");
|
|
14474
|
+
const evidenceBasePath = filePath.startsWith(deps.optimaEvidencesDir(worktree) + path19.sep) ? optimaEvidenceBaseForFile(worktree, filePath, deps) : null;
|
|
13818
14475
|
const rewritten = rewriteOptimaMarkdownReferences(raw, evidenceBasePath);
|
|
13819
14476
|
if (rewritten !== raw) {
|
|
13820
|
-
|
|
14477
|
+
fs18.writeFileSync(filePath, rewritten, "utf8");
|
|
13821
14478
|
changed.push(relPath(worktree, filePath));
|
|
13822
14479
|
}
|
|
13823
14480
|
}
|
|
@@ -13825,8 +14482,8 @@ function normalizeOptimaMarkdownReferences(worktree, deps) {
|
|
|
13825
14482
|
}
|
|
13826
14483
|
function missingOptimaGitignoreRules(worktree, deps) {
|
|
13827
14484
|
if (!deps.isGitRepository(worktree)) return [];
|
|
13828
|
-
const gitignorePath =
|
|
13829
|
-
const existing =
|
|
14485
|
+
const gitignorePath = path19.join(worktree, ".gitignore");
|
|
14486
|
+
const existing = fs18.existsSync(gitignorePath) ? fs18.readFileSync(gitignorePath, "utf8") : "";
|
|
13830
14487
|
const existingRules = new Set(existing.split(/\r?\n/).map((line) => line.trim()).filter(Boolean));
|
|
13831
14488
|
return deps.optimaGitignoreRules.filter((rule) => !existingRules.has(rule));
|
|
13832
14489
|
}
|
|
@@ -13843,11 +14500,11 @@ function planOptimaRepair(worktree, args = {}, deps) {
|
|
|
13843
14500
|
const apply = mode === "apply";
|
|
13844
14501
|
const actions = [];
|
|
13845
14502
|
const unresolved = [];
|
|
13846
|
-
const hadOptima =
|
|
14503
|
+
const hadOptima = fs18.existsSync(deps.optimaDir(worktree));
|
|
13847
14504
|
const missingGitignoreRules = missingOptimaGitignoreRules(worktree, deps);
|
|
13848
14505
|
const markdownBefore = apply ? [] : walkMarkdownFiles(deps.optimaDir(worktree)).filter((filePath) => {
|
|
13849
|
-
const raw =
|
|
13850
|
-
const evidenceBasePath = filePath.startsWith(deps.optimaEvidencesDir(worktree) +
|
|
14506
|
+
const raw = fs18.readFileSync(filePath, "utf8");
|
|
14507
|
+
const evidenceBasePath = filePath.startsWith(deps.optimaEvidencesDir(worktree) + path19.sep) ? optimaEvidenceBaseForFile(worktree, filePath, deps) : null;
|
|
13851
14508
|
return rewriteOptimaMarkdownReferences(raw, evidenceBasePath) !== raw;
|
|
13852
14509
|
}).map((filePath) => relPath(worktree, filePath));
|
|
13853
14510
|
if (!hadOptima) pushRepairAction(actions, "operational", apply ? "created" : "would_create", ".optima/", "Create Optima operational root.");
|
|
@@ -13856,31 +14513,31 @@ function planOptimaRepair(worktree, args = {}, deps) {
|
|
|
13856
14513
|
[".staticeng/", deps.legacyStaticEngDir(worktree)],
|
|
13857
14514
|
[".nomadwork/", deps.legacyNomadworkDir(worktree)],
|
|
13858
14515
|
[".nomadworks/", deps.legacyNomadworksDir(worktree)],
|
|
13859
|
-
["tasks/",
|
|
13860
|
-
["evidences/",
|
|
13861
|
-
["docs/scrs/",
|
|
13862
|
-
["codemap.yml",
|
|
13863
|
-
["codemap.yaml",
|
|
14516
|
+
["tasks/", path19.join(worktree, "tasks")],
|
|
14517
|
+
["evidences/", path19.join(worktree, "evidences")],
|
|
14518
|
+
["docs/scrs/", path19.join(worktree, "docs", "scrs")],
|
|
14519
|
+
["codemap.yml", path19.join(worktree, "codemap.yml")],
|
|
14520
|
+
["codemap.yaml", path19.join(worktree, "codemap.yaml")]
|
|
13864
14521
|
];
|
|
13865
|
-
if (!deps.isOptimaPluginPackageWorktree(worktree)) legacySources.push(["policies/",
|
|
14522
|
+
if (!deps.isOptimaPluginPackageWorktree(worktree)) legacySources.push(["policies/", path19.join(worktree, "policies")]);
|
|
13866
14523
|
for (const [display, sourcePath] of legacySources) {
|
|
13867
|
-
if (
|
|
14524
|
+
if (fs18.existsSync(sourcePath)) pushRepairAction(actions, "legacy", apply ? "migrated" : "would_migrate", display, "Move or merge legacy/root Optima artifact into .optima using preservation safeguards.");
|
|
13868
14525
|
}
|
|
13869
14526
|
if (apply) deps.migrateLegacyOptimaLayout(worktree);
|
|
13870
|
-
const configMissing = !
|
|
14527
|
+
const configMissing = !fs18.existsSync(deps.repoConfigPath(worktree));
|
|
13871
14528
|
if (configMissing) pushRepairAction(actions, "config", apply ? "created" : "would_create", ".optima/.config/optima.yaml", "Create missing local Optima config from template.");
|
|
13872
14529
|
if (apply) deps.scaffoldOptimaConfig(worktree, args.team_mode || "full");
|
|
13873
14530
|
const requiredFiles = [
|
|
13874
|
-
[
|
|
13875
|
-
[
|
|
13876
|
-
[
|
|
13877
|
-
[
|
|
13878
|
-
[
|
|
13879
|
-
[
|
|
13880
|
-
[
|
|
14531
|
+
[path19.join(deps.optimaTasksDir(worktree), "current.md"), "registry", ".optima/tasks/current.md", deps.currentTasksRegistryContent()],
|
|
14532
|
+
[path19.join(deps.optimaTasksDir(worktree), "done.md"), "registry", ".optima/tasks/done.md", deps.doneTasksRegistryContent()],
|
|
14533
|
+
[path19.join(deps.optimaScrsDir(worktree), "current.md"), "registry", ".optima/docs/scrs/current.md", deps.currentScrRegistryContent()],
|
|
14534
|
+
[path19.join(deps.optimaScrsDir(worktree), "done.md"), "registry", ".optima/docs/scrs/done.md", deps.doneScrRegistryContent()],
|
|
14535
|
+
[path19.join(deps.optimaTasksDir(worktree), "task-template.md"), "template", ".optima/tasks/task-template.md", deps.taskTemplateContent()],
|
|
14536
|
+
[path19.join(deps.optimaTasksDir(worktree), "subtask-template.md"), "template", ".optima/tasks/subtask-template.md", deps.subtaskTemplateContent()],
|
|
14537
|
+
[path19.join(deps.repoPoliciesDir(worktree), "README.md"), "policy", ".optima/policies/README.md", deps.repoLocalPoliciesReadme]
|
|
13881
14538
|
];
|
|
13882
14539
|
for (const [filePath, category, displayPath, content] of requiredFiles) {
|
|
13883
|
-
if (!
|
|
14540
|
+
if (!fs18.existsSync(filePath)) {
|
|
13884
14541
|
pushRepairAction(actions, category, apply ? "created" : "would_create", displayPath, "Create missing Optima scaffold file without overwriting existing content.");
|
|
13885
14542
|
if (apply) deps.ensureFileIfMissing(filePath, content);
|
|
13886
14543
|
}
|
|
@@ -13934,18 +14591,18 @@ function formatRepairResult(plan, validationResult = null, formatValidationResul
|
|
|
13934
14591
|
// src/validate_logic.js
|
|
13935
14592
|
var import_yaml5 = __toESM(require_dist(), 1);
|
|
13936
14593
|
var import_ignore2 = __toESM(require_ignore(), 1);
|
|
13937
|
-
import
|
|
13938
|
-
import
|
|
14594
|
+
import fs19 from "node:fs";
|
|
14595
|
+
import path20 from "node:path";
|
|
13939
14596
|
async function optima_validate_logic(worktree) {
|
|
13940
|
-
const rootCodemapPath =
|
|
13941
|
-
if (!
|
|
14597
|
+
const rootCodemapPath = path20.join(worktree, ".optima", "codemap.yml");
|
|
14598
|
+
if (!fs19.existsSync(rootCodemapPath)) return { ok: false, errors: [".optima/codemap.yml not found."], warnings: [] };
|
|
13942
14599
|
const errors = [];
|
|
13943
14600
|
const warnings = [];
|
|
13944
14601
|
const ig = (0, import_ignore2.default)();
|
|
13945
14602
|
ig.add(".git");
|
|
13946
|
-
const gitignorePath =
|
|
13947
|
-
if (
|
|
13948
|
-
ig.add(
|
|
14603
|
+
const gitignorePath = path20.join(worktree, ".gitignore");
|
|
14604
|
+
if (fs19.existsSync(gitignorePath)) {
|
|
14605
|
+
ig.add(fs19.readFileSync(gitignorePath, "utf8"));
|
|
13949
14606
|
}
|
|
13950
14607
|
const sourceExtensions = [
|
|
13951
14608
|
".js",
|
|
@@ -13985,30 +14642,30 @@ async function optima_validate_logic(worktree) {
|
|
|
13985
14642
|
const operationalFolders = [".optima", "templates", "dist"];
|
|
13986
14643
|
const isHiddenTree2 = (relPath2) => {
|
|
13987
14644
|
if (!relPath2) return false;
|
|
13988
|
-
return relPath2.split(
|
|
14645
|
+
return relPath2.split(path20.sep).some((part) => part.startsWith("."));
|
|
13989
14646
|
};
|
|
13990
|
-
const firstPathSegment2 = (relPath2) => relPath2.split(
|
|
14647
|
+
const firstPathSegment2 = (relPath2) => relPath2.split(path20.sep).filter(Boolean)[0] || "";
|
|
13991
14648
|
const isOperationalRelPath2 = (relPath2) => {
|
|
13992
14649
|
if (!relPath2) return false;
|
|
13993
14650
|
const firstSegment = firstPathSegment2(relPath2);
|
|
13994
14651
|
if (legacyOperationalRoots.has(firstSegment)) return true;
|
|
13995
|
-
return operationalFolders.some((f) => relPath2 === f || relPath2.startsWith(f +
|
|
14652
|
+
return operationalFolders.some((f) => relPath2 === f || relPath2.startsWith(f + path20.sep));
|
|
13996
14653
|
};
|
|
13997
14654
|
const getSectionEntries = (value) => {
|
|
13998
14655
|
if (Array.isArray(value)) return value;
|
|
13999
14656
|
if (value && typeof value === "object") return Object.values(value);
|
|
14000
14657
|
return [];
|
|
14001
14658
|
};
|
|
14002
|
-
const normalizeRelativePath = (relPath2) =>
|
|
14659
|
+
const normalizeRelativePath = (relPath2) => path20.normalize(relPath2).replace(/\\/g, "/").replace(/\/$/, "");
|
|
14003
14660
|
const isSourceDir = (dirPath) => {
|
|
14004
|
-
const dirRelPath =
|
|
14661
|
+
const dirRelPath = path20.relative(worktree, dirPath);
|
|
14005
14662
|
if (isOperationalRelPath2(dirRelPath)) return false;
|
|
14006
|
-
const items =
|
|
14663
|
+
const items = fs19.readdirSync(dirPath, { withFileTypes: true });
|
|
14007
14664
|
for (const item of items) {
|
|
14008
|
-
const childPath =
|
|
14009
|
-
const relPath2 =
|
|
14665
|
+
const childPath = path20.join(dirPath, item.name);
|
|
14666
|
+
const relPath2 = path20.relative(worktree, childPath);
|
|
14010
14667
|
if (ig.ignores(relPath2) || isOperationalRelPath2(relPath2)) continue;
|
|
14011
|
-
if (item.isFile() && sourceExtensions.includes(
|
|
14668
|
+
if (item.isFile() && sourceExtensions.includes(path20.extname(item.name))) return true;
|
|
14012
14669
|
if (item.isDirectory()) {
|
|
14013
14670
|
if (isSourceDir(childPath)) return true;
|
|
14014
14671
|
}
|
|
@@ -14016,7 +14673,7 @@ async function optima_validate_logic(worktree) {
|
|
|
14016
14673
|
return false;
|
|
14017
14674
|
};
|
|
14018
14675
|
function validateMap(filePath) {
|
|
14019
|
-
const content =
|
|
14676
|
+
const content = fs19.readFileSync(filePath, "utf8");
|
|
14020
14677
|
let map;
|
|
14021
14678
|
try {
|
|
14022
14679
|
map = import_yaml5.default.parse(content);
|
|
@@ -14028,32 +14685,32 @@ async function optima_validate_logic(worktree) {
|
|
|
14028
14685
|
errors.push(`${filePath}: Invalid YAML.`);
|
|
14029
14686
|
return;
|
|
14030
14687
|
}
|
|
14031
|
-
const dir =
|
|
14688
|
+
const dir = path20.dirname(filePath);
|
|
14032
14689
|
const pathBase = filePath === rootCodemapPath ? worktree : dir;
|
|
14033
14690
|
const indexedPaths = /* @__PURE__ */ new Set();
|
|
14034
14691
|
const sectionsToVerify = ["modules", "entrypoints", "sources_of_truth", "links", "internals"];
|
|
14035
14692
|
for (const section of sectionsToVerify) {
|
|
14036
14693
|
for (const item of getSectionEntries(map[section])) {
|
|
14037
14694
|
if (item?.path) {
|
|
14038
|
-
indexedPaths.add(
|
|
14695
|
+
indexedPaths.add(path20.normalize(item.path));
|
|
14039
14696
|
if (section === "links" && (item.path.startsWith("http://") || item.path.startsWith("https://"))) {
|
|
14040
14697
|
continue;
|
|
14041
14698
|
}
|
|
14042
|
-
const absPath =
|
|
14043
|
-
if (!
|
|
14699
|
+
const absPath = path20.isAbsolute(item.path) ? item.path : path20.join(pathBase, item.path);
|
|
14700
|
+
if (!fs19.existsSync(absPath)) {
|
|
14044
14701
|
errors.push(`${filePath}: ${section.slice(0, -1)} path does not exist: ${item.path}`);
|
|
14045
14702
|
}
|
|
14046
14703
|
}
|
|
14047
14704
|
}
|
|
14048
14705
|
}
|
|
14049
|
-
const relDir =
|
|
14706
|
+
const relDir = path20.relative(worktree, dir);
|
|
14050
14707
|
const isOperational = isOperationalRelPath2(relDir);
|
|
14051
14708
|
if (map.scope === "module" && !isOperational) {
|
|
14052
|
-
const items =
|
|
14709
|
+
const items = fs19.readdirSync(dir, { withFileTypes: true });
|
|
14053
14710
|
for (const item of items) {
|
|
14054
|
-
if (item.isFile() && sourceExtensions.includes(
|
|
14711
|
+
if (item.isFile() && sourceExtensions.includes(path20.extname(item.name))) {
|
|
14055
14712
|
if (item.name === "codemap.yml") continue;
|
|
14056
|
-
if (!indexedPaths.has(
|
|
14713
|
+
if (!indexedPaths.has(path20.normalize(item.name))) {
|
|
14057
14714
|
errors.push(`${filePath}: Unindexed source file found: '${item.name}'. Every source file must be categorized in a section (e.g., 'internals').`);
|
|
14058
14715
|
}
|
|
14059
14716
|
}
|
|
@@ -14063,7 +14720,7 @@ async function optima_validate_logic(worktree) {
|
|
|
14063
14720
|
for (const key of pathKeys) {
|
|
14064
14721
|
for (const entry of getSectionEntries(map[key])) {
|
|
14065
14722
|
if (!entry?.path || entry.path.startsWith("http://") || entry.path.startsWith("https://")) continue;
|
|
14066
|
-
if (
|
|
14723
|
+
if (path20.isAbsolute(entry.path)) continue;
|
|
14067
14724
|
const normalizedPath = normalizeRelativePath(entry.path);
|
|
14068
14725
|
if (!normalizedPath || normalizedPath === ".") continue;
|
|
14069
14726
|
const parts = normalizedPath.split("/").filter((p) => p && p !== ".");
|
|
@@ -14075,18 +14732,18 @@ async function optima_validate_logic(worktree) {
|
|
|
14075
14732
|
}
|
|
14076
14733
|
}
|
|
14077
14734
|
const walk = (dir) => {
|
|
14078
|
-
const relDir =
|
|
14735
|
+
const relDir = path20.relative(worktree, dir);
|
|
14079
14736
|
if (relDir && ig.ignores(relDir)) return;
|
|
14080
14737
|
if (isOperationalRelPath2(relDir)) return;
|
|
14081
14738
|
if (isHiddenTree2(relDir)) return;
|
|
14082
|
-
const hasCodemap =
|
|
14083
|
-
const items =
|
|
14084
|
-
if (!relDir.startsWith(
|
|
14739
|
+
const hasCodemap = fs19.existsSync(path20.join(dir, "codemap.yml"));
|
|
14740
|
+
const items = fs19.readdirSync(dir, { withFileTypes: true });
|
|
14741
|
+
if (!relDir.startsWith(path20.join(".optima", "tasks", "done"))) {
|
|
14085
14742
|
for (const item of items) {
|
|
14086
14743
|
if (item.isFile() && item.name.endsWith(".md")) {
|
|
14087
|
-
const content =
|
|
14744
|
+
const content = fs19.readFileSync(path20.join(dir, item.name), "utf8");
|
|
14088
14745
|
if (content.includes("[To be defined]") || content.includes("[Insert ")) {
|
|
14089
|
-
const relFilePath =
|
|
14746
|
+
const relFilePath = path20.join(relDir, item.name);
|
|
14090
14747
|
errors.push(`Documentation Placeholder found: '${relFilePath}' still contains [To be defined] or [Insert ...] placeholders.`);
|
|
14091
14748
|
}
|
|
14092
14749
|
}
|
|
@@ -14096,9 +14753,9 @@ async function optima_validate_logic(worktree) {
|
|
|
14096
14753
|
if (relDir !== "" && !hasCodemap && isSourceDir(dir) && !isOperational) {
|
|
14097
14754
|
errors.push(`Missing CodeMap: Directory '${relDir}' contains source but has no codemap.yml.`);
|
|
14098
14755
|
}
|
|
14099
|
-
if (hasCodemap) validateMap(
|
|
14756
|
+
if (hasCodemap) validateMap(path20.join(dir, "codemap.yml"));
|
|
14100
14757
|
for (const item of items) {
|
|
14101
|
-
if (item.isDirectory()) walk(
|
|
14758
|
+
if (item.isDirectory()) walk(path20.join(dir, item.name));
|
|
14102
14759
|
}
|
|
14103
14760
|
};
|
|
14104
14761
|
validateMap(rootCodemapPath);
|
|
@@ -14112,12 +14769,13 @@ async function optima_validate_logic(worktree) {
|
|
|
14112
14769
|
|
|
14113
14770
|
// src/plugin.js
|
|
14114
14771
|
var activeWorkflows = /* @__PURE__ */ new Map();
|
|
14772
|
+
var activeQaQueueSchedulers = /* @__PURE__ */ new Map();
|
|
14115
14773
|
function normalizeWorkflowTaskPath(taskPath) {
|
|
14116
14774
|
if (typeof taskPath !== "string") return { ok: false, message: "Error: task_path is required." };
|
|
14117
14775
|
const trimmed = taskPath.trim();
|
|
14118
14776
|
if (!trimmed) return { ok: false, message: "Error: task_path is required." };
|
|
14119
14777
|
const normalized = trimmed.replace(/\\/g, "/");
|
|
14120
|
-
if (
|
|
14778
|
+
if (path21.isAbsolute(trimmed)) return { ok: true, taskPath: trimmed };
|
|
14121
14779
|
if (normalized === "tasks" || normalized.startsWith("tasks/")) {
|
|
14122
14780
|
return {
|
|
14123
14781
|
ok: false,
|
|
@@ -14134,10 +14792,10 @@ function normalizeWorkflowTaskPath(taskPath) {
|
|
|
14134
14792
|
}
|
|
14135
14793
|
function readTaskMetadata(taskPath, worktree) {
|
|
14136
14794
|
if (!taskPath) return {};
|
|
14137
|
-
const absoluteTaskPath =
|
|
14138
|
-
if (!
|
|
14795
|
+
const absoluteTaskPath = path21.isAbsolute(taskPath) ? taskPath : path21.join(worktree, taskPath);
|
|
14796
|
+
if (!fs20.existsSync(absoluteTaskPath)) return {};
|
|
14139
14797
|
try {
|
|
14140
|
-
const raw =
|
|
14798
|
+
const raw = fs20.readFileSync(absoluteTaskPath, "utf8");
|
|
14141
14799
|
const { data } = parseFrontmatter(raw);
|
|
14142
14800
|
return {
|
|
14143
14801
|
complexity: typeof data.complexity === "string" ? data.complexity.trim().toLowerCase() : void 0,
|
|
@@ -14160,15 +14818,15 @@ function hasActiveImplementationWorkflow() {
|
|
|
14160
14818
|
function resolveSessionToolDirectory({ requestedDirectory, context, pluginWorktree, clickUpWebhookValidation } = {}) {
|
|
14161
14819
|
const safe = safeWorktreeOrFailure(context, pluginWorktree);
|
|
14162
14820
|
if (!safe.ok) return { ok: false, error: safe.message };
|
|
14163
|
-
const requested = String(requestedDirectory || "").trim() ?
|
|
14821
|
+
const requested = String(requestedDirectory || "").trim() ? path21.resolve(safe.worktree, String(requestedDirectory).trim()) : safe.worktree;
|
|
14164
14822
|
if (!isSafeWritableDirectory(requested)) return { ok: false, error: `Directory is not a safe writable directory: ${requested}` };
|
|
14165
14823
|
if (isSameOrNestedPath(requested, safe.worktree)) return { ok: true, directory: requested, safeWorktree: safe.worktree, scope: "context_worktree" };
|
|
14166
14824
|
const clickUpBasePath = clickUpWebhookValidation?.complete === true && clickUpWebhookValidation?.ok !== false ? clickUpWebhookValidation.config?.basePath : "";
|
|
14167
14825
|
if (clickUpBasePath && isSameOrNestedPath(requested, clickUpBasePath)) {
|
|
14168
|
-
return { ok: true, directory: requested, safeWorktree: safe.worktree, scope: "clickup_base_path", clickupBasePath:
|
|
14826
|
+
return { ok: true, directory: requested, safeWorktree: safe.worktree, scope: "clickup_base_path", clickupBasePath: path21.resolve(clickUpBasePath) };
|
|
14169
14827
|
}
|
|
14170
14828
|
if (clickUpBasePath && isClickUpDerivedWorktreeSibling(requested, clickUpBasePath)) {
|
|
14171
|
-
return { ok: true, directory: requested, safeWorktree: safe.worktree, scope: "clickup_derived_worktree", clickupBasePath:
|
|
14829
|
+
return { ok: true, directory: requested, safeWorktree: safe.worktree, scope: "clickup_derived_worktree", clickupBasePath: path21.resolve(clickUpBasePath) };
|
|
14172
14830
|
}
|
|
14173
14831
|
return {
|
|
14174
14832
|
ok: false,
|
|
@@ -14183,9 +14841,9 @@ async function OptimaPlugin(input = {}, pluginOptions = {}) {
|
|
|
14183
14841
|
const configPath = resolveConfigPath(worktree);
|
|
14184
14842
|
const discussionRegistry = loadDiscussionRegistry(worktree);
|
|
14185
14843
|
let repoCfg = { agents: {}, defaults: {}, features: {} };
|
|
14186
|
-
if (
|
|
14844
|
+
if (fs20.existsSync(configPath)) {
|
|
14187
14845
|
try {
|
|
14188
|
-
repoCfg = import_yaml6.default.parse(
|
|
14846
|
+
repoCfg = import_yaml6.default.parse(fs20.readFileSync(configPath, "utf8")) || repoCfg;
|
|
14189
14847
|
} catch (e) {
|
|
14190
14848
|
console.error(`[Optima] Failed to parse config at ${configPath}:`, e);
|
|
14191
14849
|
}
|
|
@@ -14346,6 +15004,134 @@ Evidencia: ${evidencePath}` : ""
|
|
|
14346
15004
|
applied
|
|
14347
15005
|
};
|
|
14348
15006
|
};
|
|
15007
|
+
const qaLeaseMs = Number(repoCfg.qa?.lease_ms || repoCfg.qa?.leaseMs || DEFAULT_QA_LEASE_MS) || DEFAULT_QA_LEASE_MS;
|
|
15008
|
+
const qaProvider = String(repoCfg.qa?.provider || DEFAULT_QA_PROVIDER);
|
|
15009
|
+
const qaCdpUrl = String(repoCfg.qa?.cdp_url || repoCfg.qa?.cdpUrl || process.env.OPTIMA_QA_CHROME_CDP_URL || "").trim();
|
|
15010
|
+
const qaTaskMetadata = async (taskId) => {
|
|
15011
|
+
if (!taskId || typeof runtimeClickUpClient?.getTask !== "function") return { task: null, metadata: {} };
|
|
15012
|
+
const task = await runtimeClickUpClient.getTask(taskId);
|
|
15013
|
+
const metadata = normalizeAgentMetadataJson(clickUpTaskAgentMetadata(task, clickUpWebhookValidation.config?.routing?.metadataFieldId));
|
|
15014
|
+
return { task, metadata };
|
|
15015
|
+
};
|
|
15016
|
+
const qaSessionFromMetadata = (metadata = {}) => {
|
|
15017
|
+
return metadata.optima?.sessions?.product_manager || metadata.task?.product_manager || metadata.sessions?.product_manager || "";
|
|
15018
|
+
};
|
|
15019
|
+
const qaWorktreeFromMetadata = (metadata = {}) => {
|
|
15020
|
+
return metadata.task?.worktree || metadata.worktree || "";
|
|
15021
|
+
};
|
|
15022
|
+
const applyQaQueuedClickUpState = async (taskId) => {
|
|
15023
|
+
const applied = [];
|
|
15024
|
+
const errors = [];
|
|
15025
|
+
const pmId = String(clickUpWebhookValidation.config?.routing?.productManagerAssigneeId || "").trim();
|
|
15026
|
+
if (!taskId || !clickUpWebhookValidation.complete || !runtimeClickUpClient) return { applied, errors };
|
|
15027
|
+
if (runtimeClickUpClient.updateTaskStatus) {
|
|
15028
|
+
try {
|
|
15029
|
+
await runtimeClickUpClient.updateTaskStatus({ taskId, status: "waiting QA slot" });
|
|
15030
|
+
applied.push("status:waiting QA slot");
|
|
15031
|
+
} catch (error) {
|
|
15032
|
+
errors.push(`status:${error.message}`);
|
|
15033
|
+
}
|
|
15034
|
+
}
|
|
15035
|
+
if (pmId && runtimeClickUpClient.updateTaskAssignees) {
|
|
15036
|
+
try {
|
|
15037
|
+
await runtimeClickUpClient.updateTaskAssignees({ taskId, remove: [pmId] });
|
|
15038
|
+
applied.push("remove_product_manager");
|
|
15039
|
+
} catch (error) {
|
|
15040
|
+
errors.push(`assignees:${error.message}`);
|
|
15041
|
+
}
|
|
15042
|
+
}
|
|
15043
|
+
return { applied, errors };
|
|
15044
|
+
};
|
|
15045
|
+
const wakePromotedQaTask = async (entry, { provider = qaProvider, reason = "qa_slot_available" } = {}) => {
|
|
15046
|
+
const taskId = String(entry?.clickupTaskId || "").trim();
|
|
15047
|
+
const applied = [];
|
|
15048
|
+
const errors = [];
|
|
15049
|
+
if (!taskId || !clickUpWebhookValidation.complete) return { ok: false, reason: "missing_task_or_clickup_config" };
|
|
15050
|
+
const pmId = String(clickUpWebhookValidation.config?.routing?.productManagerAssigneeId || "").trim();
|
|
15051
|
+
const humanIds = Object.values(clickUpWebhookValidation.config?.routing?.humanRoleClickUpIds || {}).map((id) => String(id || "").trim()).filter(Boolean);
|
|
15052
|
+
try {
|
|
15053
|
+
if (runtimeClickUpClient.updateTaskStatus) {
|
|
15054
|
+
await runtimeClickUpClient.updateTaskStatus({ taskId, status: "in progress" });
|
|
15055
|
+
applied.push("status:in progress");
|
|
15056
|
+
}
|
|
15057
|
+
} catch (error) {
|
|
15058
|
+
errors.push(`status:${error.message}`);
|
|
15059
|
+
}
|
|
15060
|
+
try {
|
|
15061
|
+
if (pmId && runtimeClickUpClient.updateTaskAssignees) {
|
|
15062
|
+
await runtimeClickUpClient.updateTaskAssignees({ taskId, add: [pmId], remove: humanIds });
|
|
15063
|
+
applied.push("assign_product_manager");
|
|
15064
|
+
}
|
|
15065
|
+
} catch (error) {
|
|
15066
|
+
errors.push(`assignees:${error.message}`);
|
|
15067
|
+
}
|
|
15068
|
+
let metadata = {};
|
|
15069
|
+
try {
|
|
15070
|
+
metadata = (await qaTaskMetadata(taskId)).metadata;
|
|
15071
|
+
} catch (error) {
|
|
15072
|
+
errors.push(`metadata:${error.message}`);
|
|
15073
|
+
}
|
|
15074
|
+
const sessionId = entry.sessionId || qaSessionFromMetadata(metadata);
|
|
15075
|
+
const directory = entry.worktree || qaWorktreeFromMetadata(metadata);
|
|
15076
|
+
if (sessionId && input.client && typeof sendOpenCodeSessionEvent === "function") {
|
|
15077
|
+
try {
|
|
15078
|
+
const prompt = [
|
|
15079
|
+
`[Optima QA Slot Granted]`,
|
|
15080
|
+
`Provider: ${provider}`,
|
|
15081
|
+
`ClickUp task: ${taskId}`,
|
|
15082
|
+
`Reason: ${reason}`,
|
|
15083
|
+
"",
|
|
15084
|
+
"Ya tienes plaza exclusiva para usar el Chrome QA. Debes llamar a `optima_qa_chrome_command` en menos de 5 minutos para conservar la plaza.",
|
|
15085
|
+
"Puedes conservarla mientras hagas al menos una llamada QA cada 5 minutos.",
|
|
15086
|
+
"Cuando termines, llama obligatoriamente a `optima_qa_finish` para resetear la extensi\xF3n y liberar la cola.",
|
|
15087
|
+
"La pesta\xF1a 0 es intocable: usa \xFAnicamente la pesta\xF1a QA que Optima crea para esta tarea."
|
|
15088
|
+
].join("\n");
|
|
15089
|
+
await sendOpenCodeSessionEvent(input.client, {
|
|
15090
|
+
sessionId,
|
|
15091
|
+
agent: clickUpWebhookValidation.config.routing?.targetAgent || "workflow_product_manager",
|
|
15092
|
+
text: prompt,
|
|
15093
|
+
directory,
|
|
15094
|
+
opencodeBaseUrl: clickUpWebhookValidation.config.opencode?.baseUrl,
|
|
15095
|
+
fetchImpl: input.fetch,
|
|
15096
|
+
directDelivery: "steer"
|
|
15097
|
+
});
|
|
15098
|
+
applied.push("session_prompt");
|
|
15099
|
+
} catch (error) {
|
|
15100
|
+
errors.push(`session_prompt:${error.message}`);
|
|
15101
|
+
}
|
|
15102
|
+
} else {
|
|
15103
|
+
errors.push("session_prompt:missing_session_or_client");
|
|
15104
|
+
}
|
|
15105
|
+
return { ok: errors.length === 0, taskId, sessionId, directory, applied, errors };
|
|
15106
|
+
};
|
|
15107
|
+
const persistQaState = async (provider, state, transitions = {}) => {
|
|
15108
|
+
const written = writeQaQueueState(state, qaQueueStatePath(provider));
|
|
15109
|
+
const notifications = [];
|
|
15110
|
+
const promoted = transitions.promoted || null;
|
|
15111
|
+
if (promoted?.clickupTaskId) notifications.push(await wakePromotedQaTask(promoted, { provider, reason: transitions.reason || "qa_slot_available" }));
|
|
15112
|
+
return { state: written, notifications };
|
|
15113
|
+
};
|
|
15114
|
+
const scheduleQaQueueReaper = () => {
|
|
15115
|
+
if (input.startQaQueueScheduler === false) return;
|
|
15116
|
+
const provider = qaProvider || DEFAULT_QA_PROVIDER;
|
|
15117
|
+
const schedulerKey = `${provider}:${clickUpWebhookValidation.config?.basePath || worktree}`;
|
|
15118
|
+
if (activeQaQueueSchedulers.has(schedulerKey)) return;
|
|
15119
|
+
const interval = setInterval(async () => {
|
|
15120
|
+
try {
|
|
15121
|
+
const statePath = qaQueueStatePath(provider);
|
|
15122
|
+
const current = readQaQueueState(provider, statePath);
|
|
15123
|
+
const result = releaseExpiredQaSlot(current, { leaseMs: qaLeaseMs });
|
|
15124
|
+
if (result.expired || result.promoted) {
|
|
15125
|
+
await persistQaState(provider, result.state, { promoted: result.promoted, reason: result.expired ? "previous_slot_expired" : "qa_slot_available" });
|
|
15126
|
+
}
|
|
15127
|
+
} catch (error) {
|
|
15128
|
+
appendClickUpWebhookLocalLog(worktree, { type: "qa_queue_reaper_failed", provider, message: error.message });
|
|
15129
|
+
}
|
|
15130
|
+
}, Math.min(6e4, Math.max(1e4, Math.floor(qaLeaseMs / 5))));
|
|
15131
|
+
interval.unref?.();
|
|
15132
|
+
activeQaQueueSchedulers.set(schedulerKey, interval);
|
|
15133
|
+
};
|
|
15134
|
+
scheduleQaQueueReaper();
|
|
14349
15135
|
const tools = {
|
|
14350
15136
|
optima_init: tool({
|
|
14351
15137
|
description: "Initialize the Optima workflow and CodeMap in the current repository",
|
|
@@ -14362,10 +15148,10 @@ Evidencia: ${evidencePath}` : ""
|
|
|
14362
15148
|
}
|
|
14363
15149
|
migrateLegacyOptimaLayout(toolWorktree);
|
|
14364
15150
|
const cfgDir = optimaConfigDir(toolWorktree);
|
|
14365
|
-
if (!
|
|
14366
|
-
const optimaTmplPath =
|
|
14367
|
-
const codemapTmplPath =
|
|
14368
|
-
if (!
|
|
15151
|
+
if (!fs20.existsSync(cfgDir)) fs20.mkdirSync(cfgDir, { recursive: true });
|
|
15152
|
+
const optimaTmplPath = path21.join(TEMPLATES_DIR, "optima.yaml.template");
|
|
15153
|
+
const codemapTmplPath = path21.join(TEMPLATES_DIR, "codemap.yml.template");
|
|
15154
|
+
if (!fs20.existsSync(optimaTmplPath) || !fs20.existsSync(codemapTmplPath)) {
|
|
14369
15155
|
return "Error: Initialization templates not found in plugin.";
|
|
14370
15156
|
}
|
|
14371
15157
|
scaffoldOptimaConfig(toolWorktree, requestedTeamMode);
|
|
@@ -14486,14 +15272,14 @@ Restart or reload OpenCode manually if the newly scaffolded config or agents are
|
|
|
14486
15272
|
async execute(args, context) {
|
|
14487
15273
|
const safe = safeWorktreeOrFailure(context, worktree);
|
|
14488
15274
|
if (!safe.ok) return safe.message;
|
|
14489
|
-
const summaryPath =
|
|
14490
|
-
if (!
|
|
14491
|
-
const taskPath = args.task_path ?
|
|
15275
|
+
const summaryPath = path21.resolve(safe.worktree, args.summary_path || "");
|
|
15276
|
+
if (!fs20.existsSync(summaryPath)) return `FAIL: summary_path not found: ${summaryPath}`;
|
|
15277
|
+
const taskPath = args.task_path ? path21.resolve(safe.worktree, args.task_path) : "";
|
|
14492
15278
|
const payload = buildClickUpSummaryPayload({
|
|
14493
|
-
summaryMarkdown:
|
|
14494
|
-
summaryPath:
|
|
14495
|
-
taskMarkdown: taskPath &&
|
|
14496
|
-
taskPath: taskPath ?
|
|
15279
|
+
summaryMarkdown: fs20.readFileSync(summaryPath, "utf8"),
|
|
15280
|
+
summaryPath: path21.relative(safe.worktree, summaryPath),
|
|
15281
|
+
taskMarkdown: taskPath && fs20.existsSync(taskPath) ? fs20.readFileSync(taskPath, "utf8") : "",
|
|
15282
|
+
taskPath: taskPath ? path21.relative(safe.worktree, taskPath) : "",
|
|
14497
15283
|
branch: args.branch,
|
|
14498
15284
|
worktree: args.worktree,
|
|
14499
15285
|
pr: args.pr
|
|
@@ -14586,6 +15372,116 @@ Restart or reload OpenCode manually if the newly scaffolded config or agents are
|
|
|
14586
15372
|
}
|
|
14587
15373
|
}
|
|
14588
15374
|
}),
|
|
15375
|
+
optima_qa_browser_status: tool({
|
|
15376
|
+
description: "Inspect the shared Optima QA browser and queue state for a provider such as chatgpt.",
|
|
15377
|
+
args: {
|
|
15378
|
+
provider: tool.schema.string().optional().describe("QA provider. Defaults to chatgpt."),
|
|
15379
|
+
clickup_task_id: tool.schema.string().optional().describe("Optional ClickUp task id to identify its QA tab."),
|
|
15380
|
+
cdp_url: tool.schema.string().optional().describe("Optional Chrome DevTools URL. Defaults to configured QA URL or http://127.0.0.1:9222.")
|
|
15381
|
+
},
|
|
15382
|
+
async execute(args = {}) {
|
|
15383
|
+
const provider = String(args.provider || qaProvider || DEFAULT_QA_PROVIDER);
|
|
15384
|
+
const state = readQaQueueState(provider);
|
|
15385
|
+
const browser = await qaBrowserStatus({ provider, cdpUrl: args.cdp_url || qaCdpUrl, clickupTaskId: args.clickup_task_id || "" });
|
|
15386
|
+
return JSON.stringify({ ok: browser.ok !== false, provider, leaseMs: qaLeaseMs, queue: state, browser }, null, 2);
|
|
15387
|
+
}
|
|
15388
|
+
}),
|
|
15389
|
+
optima_qa_request_slot: tool({
|
|
15390
|
+
description: "Request the exclusive QA browser slot for a ClickUp task. If busy, Optima queues the task, marks it waiting QA slot, and removes Product Manager assignment.",
|
|
15391
|
+
args: {
|
|
15392
|
+
clickup_task_id: tool.schema.string().describe("ClickUp task id requesting the QA slot"),
|
|
15393
|
+
provider: tool.schema.string().optional().describe("QA provider. Defaults to chatgpt."),
|
|
15394
|
+
session_id: tool.schema.string().optional().describe("Optional OpenCode session id to wake when the slot is granted later"),
|
|
15395
|
+
worktree: tool.schema.string().optional().describe("Optional task worktree/directory for session wakeups"),
|
|
15396
|
+
reason: tool.schema.string().optional().describe("Short reason/scenario for requesting the QA slot")
|
|
15397
|
+
},
|
|
15398
|
+
async execute(args = {}) {
|
|
15399
|
+
const provider = String(args.provider || qaProvider || DEFAULT_QA_PROVIDER);
|
|
15400
|
+
const pathToState = qaQueueStatePath(provider);
|
|
15401
|
+
const current = readQaQueueState(provider, pathToState);
|
|
15402
|
+
const result = requestQaSlot(current, {
|
|
15403
|
+
clickupTaskId: args.clickup_task_id,
|
|
15404
|
+
sessionId: args.session_id,
|
|
15405
|
+
worktree: args.worktree,
|
|
15406
|
+
reason: args.reason,
|
|
15407
|
+
leaseMs: qaLeaseMs
|
|
15408
|
+
});
|
|
15409
|
+
let queuedClickUp = null;
|
|
15410
|
+
if (result.action === "queued") queuedClickUp = await applyQaQueuedClickUpState(args.clickup_task_id);
|
|
15411
|
+
const persisted = await persistQaState(provider, result.state, { promoted: result.promoted, reason: result.expired ? "previous_slot_expired" : "qa_slot_available" });
|
|
15412
|
+
const guidance = result.ok ? "QA slot granted. Use optima_qa_chrome_command within 5 minutes and then at least once every 5 minutes. Use optima_qa_finish when done." : "QA slot busy. Task has been queued; stop work on QA, wait for Optima to reassign/wake the session when your slot is granted.";
|
|
15413
|
+
return JSON.stringify({ ...result, state: persisted.state, notifications: persisted.notifications, queuedClickUp, guidance }, null, 2);
|
|
15414
|
+
}
|
|
15415
|
+
}),
|
|
15416
|
+
optima_qa_chrome_command: tool({
|
|
15417
|
+
description: "Run one flexible Playwright/CDP command against the shared QA Chrome for the active ClickUp task. The persistent tab 0 is never used; Optima creates/reuses a task QA tab.",
|
|
15418
|
+
args: {
|
|
15419
|
+
clickup_task_id: tool.schema.string().describe("ClickUp task id that currently owns the QA slot"),
|
|
15420
|
+
provider: tool.schema.string().optional().describe("QA provider. Defaults to chatgpt."),
|
|
15421
|
+
command_json: tool.schema.string().describe("JSON command object, or raw async JS script. Actions: status, goto, evaluate, screenshot, set_input_files, script."),
|
|
15422
|
+
cdp_url: tool.schema.string().optional().describe("Optional Chrome DevTools URL"),
|
|
15423
|
+
start_url: tool.schema.string().optional().describe("Initial URL for a new QA tab. Defaults to https://chatgpt.com/")
|
|
15424
|
+
},
|
|
15425
|
+
async execute(args = {}) {
|
|
15426
|
+
const provider = String(args.provider || qaProvider || DEFAULT_QA_PROVIDER);
|
|
15427
|
+
const pathToState = qaQueueStatePath(provider);
|
|
15428
|
+
const current = readQaQueueState(provider, pathToState);
|
|
15429
|
+
const touched = touchQaSlot(current, { clickupTaskId: args.clickup_task_id, leaseMs: qaLeaseMs });
|
|
15430
|
+
const persisted = await persistQaState(provider, touched.state, { promoted: touched.promoted, reason: touched.expired ? "previous_slot_expired" : "qa_slot_available" });
|
|
15431
|
+
if (!touched.ok) {
|
|
15432
|
+
return JSON.stringify({
|
|
15433
|
+
ok: false,
|
|
15434
|
+
action: touched.action,
|
|
15435
|
+
active: persisted.state.active,
|
|
15436
|
+
queue: persisted.state.queue,
|
|
15437
|
+
notifications: persisted.notifications,
|
|
15438
|
+
guidance: "You do not own the QA slot. Call optima_qa_request_slot and stop if queued."
|
|
15439
|
+
}, null, 2);
|
|
15440
|
+
}
|
|
15441
|
+
try {
|
|
15442
|
+
const result = await qaRunChromeCommand({
|
|
15443
|
+
provider,
|
|
15444
|
+
cdpUrl: args.cdp_url || qaCdpUrl,
|
|
15445
|
+
clickupTaskId: args.clickup_task_id,
|
|
15446
|
+
commandJson: args.command_json,
|
|
15447
|
+
startUrl: args.start_url || "https://chatgpt.com/"
|
|
15448
|
+
});
|
|
15449
|
+
return JSON.stringify({ ok: result.ok !== false, lease: persisted.state.active, notifications: persisted.notifications, chrome: result }, null, 2);
|
|
15450
|
+
} catch (error) {
|
|
15451
|
+
return JSON.stringify({ ok: false, error: error.message, lease: persisted.state.active, notifications: persisted.notifications }, null, 2);
|
|
15452
|
+
}
|
|
15453
|
+
}
|
|
15454
|
+
}),
|
|
15455
|
+
optima_qa_finish: tool({
|
|
15456
|
+
description: "Finish the active QA browser slot, reset/reload the extension, release the slot, and wake the next queued ClickUp task if any.",
|
|
15457
|
+
args: {
|
|
15458
|
+
clickup_task_id: tool.schema.string().describe("ClickUp task id finishing QA"),
|
|
15459
|
+
provider: tool.schema.string().optional().describe("QA provider. Defaults to chatgpt."),
|
|
15460
|
+
cdp_url: tool.schema.string().optional().describe("Optional Chrome DevTools URL"),
|
|
15461
|
+
extension_path: tool.schema.string().optional().describe("Optional extension path to sync into the stable QA extension-under-test directory before reset/reload"),
|
|
15462
|
+
extension_id: tool.schema.string().optional().describe("Optional Chrome extension id when multiple extension service workers exist"),
|
|
15463
|
+
reset_extension: tool.schema.boolean().optional().describe("Defaults true. Clear extension storage/caches/IndexedDB."),
|
|
15464
|
+
reload_extension: tool.schema.boolean().optional().describe("Defaults true. Reload the extension service worker after reset.")
|
|
15465
|
+
},
|
|
15466
|
+
async execute(args = {}) {
|
|
15467
|
+
const provider = String(args.provider || qaProvider || DEFAULT_QA_PROVIDER);
|
|
15468
|
+
const reset = args.reset_extension !== false;
|
|
15469
|
+
const reload = args.reload_extension !== false;
|
|
15470
|
+
const extension = await qaPrepareExtension({
|
|
15471
|
+
provider,
|
|
15472
|
+
cdpUrl: args.cdp_url || qaCdpUrl,
|
|
15473
|
+
extensionPath: args.extension_path || "",
|
|
15474
|
+
extensionId: args.extension_id || "",
|
|
15475
|
+
reset,
|
|
15476
|
+
reload
|
|
15477
|
+
}).catch((error) => ({ ok: false, error: error.message }));
|
|
15478
|
+
const pathToState = qaQueueStatePath(provider);
|
|
15479
|
+
const current = readQaQueueState(provider, pathToState);
|
|
15480
|
+
const result = finishQaSlot(current, { clickupTaskId: args.clickup_task_id, leaseMs: qaLeaseMs });
|
|
15481
|
+
const persisted = await persistQaState(provider, result.state, { promoted: result.promoted, reason: "previous_finished" });
|
|
15482
|
+
return JSON.stringify({ ...result, state: persisted.state, notifications: persisted.notifications, extension }, null, 2);
|
|
15483
|
+
}
|
|
15484
|
+
}),
|
|
14589
15485
|
optima_clickup_create_subtasks: tool({
|
|
14590
15486
|
description: "Generate dry-run ClickUp subtask creation payloads from a strict Markdown ## Subtasks section",
|
|
14591
15487
|
args: {
|
|
@@ -14598,12 +15494,12 @@ Restart or reload OpenCode manually if the newly scaffolded config or agents are
|
|
|
14598
15494
|
async execute(args, context) {
|
|
14599
15495
|
const safe = safeWorktreeOrFailure(context, worktree);
|
|
14600
15496
|
if (!safe.ok) return safe.message;
|
|
14601
|
-
const markdownPath =
|
|
14602
|
-
if (!
|
|
15497
|
+
const markdownPath = path21.resolve(safe.worktree, args.markdown_path || "");
|
|
15498
|
+
if (!fs20.existsSync(markdownPath)) return `FAIL: markdown_path not found: ${markdownPath}`;
|
|
14603
15499
|
const payload = buildClickUpCreateSubtasksPayload({
|
|
14604
15500
|
parentTaskId: args.parent_task_id,
|
|
14605
|
-
markdown:
|
|
14606
|
-
sourcePath:
|
|
15501
|
+
markdown: fs20.readFileSync(markdownPath, "utf8"),
|
|
15502
|
+
sourcePath: path21.relative(safe.worktree, markdownPath),
|
|
14607
15503
|
parentBranch: args.parent_branch,
|
|
14608
15504
|
parentTaskType: args.parent_task_type || "Tarea",
|
|
14609
15505
|
apply: String(args.apply || "").toLowerCase() === "true"
|
|
@@ -14973,7 +15869,7 @@ Backfilled messages: ${backfilled}`;
|
|
|
14973
15869
|
if (!existing) {
|
|
14974
15870
|
return "FAIL: No active discussion exists for this session.";
|
|
14975
15871
|
}
|
|
14976
|
-
const discussionPath =
|
|
15872
|
+
const discussionPath = path21.join(toolWorktree, existing.transcriptPath);
|
|
14977
15873
|
setDiscussionStatus(discussionPath, "summarizing");
|
|
14978
15874
|
existing.status = "summarizing";
|
|
14979
15875
|
saveDiscussionRegistry(toolWorktree, toolRegistry);
|
|
@@ -15025,7 +15921,7 @@ Reason: ${err.message}`;
|
|
|
15025
15921
|
try {
|
|
15026
15922
|
const sessionResult = await client.session.create({
|
|
15027
15923
|
query: { directory: workflowDirectory },
|
|
15028
|
-
body: { title: `Workflow Run: ${
|
|
15924
|
+
body: { title: `Workflow Run: ${path21.basename(workflowTaskPath)}` }
|
|
15029
15925
|
});
|
|
15030
15926
|
const sessionId = sessionResult.data.id;
|
|
15031
15927
|
activeWorkflows.set(sessionId, { pmaSessionId, taskPath: workflowTaskPath, track: workflowTrack, directory: workflowDirectory });
|
|
@@ -15209,14 +16105,14 @@ function usage() {
|
|
|
15209
16105
|
].join("\n");
|
|
15210
16106
|
}
|
|
15211
16107
|
function expandHome(inputPath) {
|
|
15212
|
-
if (!inputPath || inputPath === "~") return
|
|
15213
|
-
if (inputPath.startsWith("~/")) return
|
|
16108
|
+
if (!inputPath || inputPath === "~") return os7.homedir();
|
|
16109
|
+
if (inputPath.startsWith("~/")) return path22.join(os7.homedir(), inputPath.slice(2));
|
|
15214
16110
|
return inputPath;
|
|
15215
16111
|
}
|
|
15216
16112
|
function resolveOpenCodeDbPath(inputPath = null) {
|
|
15217
16113
|
const expanded = expandHome(inputPath || process.env.OPTIMA_OPENCODE_DB_PATH || DEFAULT_DB_PATH);
|
|
15218
16114
|
try {
|
|
15219
|
-
if (
|
|
16115
|
+
if (fs21.existsSync(expanded) && fs21.statSync(expanded).isDirectory()) return path22.join(expanded, "opencode.db");
|
|
15220
16116
|
} catch {
|
|
15221
16117
|
return expanded;
|
|
15222
16118
|
}
|
|
@@ -15280,12 +16176,12 @@ function extractOpenedPathValue(value) {
|
|
|
15280
16176
|
}
|
|
15281
16177
|
function normalizePathCandidate(candidate) {
|
|
15282
16178
|
const expanded = expandHome(candidate);
|
|
15283
|
-
if (!
|
|
15284
|
-
return
|
|
16179
|
+
if (!path22.isAbsolute(expanded)) return null;
|
|
16180
|
+
return path22.resolve(expanded);
|
|
15285
16181
|
}
|
|
15286
16182
|
function discoverOpenCodePaths(dbPath) {
|
|
15287
16183
|
const resolvedDb = resolveOpenCodeDbPath(dbPath);
|
|
15288
|
-
if (!
|
|
16184
|
+
if (!fs21.existsSync(resolvedDb)) throw new Error(`OpenCode database not found: ${resolvedDb}`);
|
|
15289
16185
|
const tables = sqliteJson(resolvedDb, "SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%' ORDER BY name");
|
|
15290
16186
|
const discovered = /* @__PURE__ */ new Set();
|
|
15291
16187
|
for (const table of tables) {
|
|
@@ -15310,27 +16206,27 @@ function discoverOpenCodePaths(dbPath) {
|
|
|
15310
16206
|
function collectMarkdownOverridesFrom(baseDir) {
|
|
15311
16207
|
const files = [];
|
|
15312
16208
|
for (const dirName of OVERRIDE_DIRS) {
|
|
15313
|
-
const dirPath =
|
|
15314
|
-
if (!
|
|
15315
|
-
for (const entry of
|
|
16209
|
+
const dirPath = path22.join(baseDir, dirName);
|
|
16210
|
+
if (!fs21.existsSync(dirPath)) continue;
|
|
16211
|
+
for (const entry of fs21.readdirSync(dirPath, { withFileTypes: true })) {
|
|
15316
16212
|
if (!entry.isFile()) continue;
|
|
15317
16213
|
if (!entry.name.endsWith(".md")) continue;
|
|
15318
16214
|
if (entry.name.toLowerCase() === "readme.md") continue;
|
|
15319
|
-
files.push(
|
|
16215
|
+
files.push(path22.join(dirPath, entry.name));
|
|
15320
16216
|
}
|
|
15321
16217
|
}
|
|
15322
16218
|
return files;
|
|
15323
16219
|
}
|
|
15324
16220
|
function collectOverrideFiles(worktree) {
|
|
15325
|
-
return collectMarkdownOverridesFrom(
|
|
16221
|
+
return collectMarkdownOverridesFrom(path22.join(worktree, ".optima")).sort();
|
|
15326
16222
|
}
|
|
15327
16223
|
function collectPlannedOverrideFiles(worktree) {
|
|
15328
16224
|
return [
|
|
15329
16225
|
...collectOverrideFiles(worktree),
|
|
15330
|
-
...collectMarkdownOverridesFrom(
|
|
15331
|
-
...collectMarkdownOverridesFrom(
|
|
15332
|
-
...collectMarkdownOverridesFrom(
|
|
15333
|
-
...collectMarkdownOverridesFrom(
|
|
16226
|
+
...collectMarkdownOverridesFrom(path22.join(worktree, ".staticeng")),
|
|
16227
|
+
...collectMarkdownOverridesFrom(path22.join(worktree, ".orbita")),
|
|
16228
|
+
...collectMarkdownOverridesFrom(path22.join(worktree, ".nomadwork")),
|
|
16229
|
+
...collectMarkdownOverridesFrom(path22.join(worktree, ".nomadworks"))
|
|
15334
16230
|
].sort();
|
|
15335
16231
|
}
|
|
15336
16232
|
var CRC_TABLE = (() => {
|
|
@@ -15363,14 +16259,14 @@ function writeUInt16LE(value) {
|
|
|
15363
16259
|
return buffer;
|
|
15364
16260
|
}
|
|
15365
16261
|
function createZipBackup(files, backupPath) {
|
|
15366
|
-
|
|
16262
|
+
fs21.mkdirSync(path22.dirname(backupPath), { recursive: true });
|
|
15367
16263
|
const localParts = [];
|
|
15368
16264
|
const centralParts = [];
|
|
15369
16265
|
let offset = 0;
|
|
15370
16266
|
const now = dosTimeDate();
|
|
15371
16267
|
for (const filePath of files) {
|
|
15372
|
-
const data =
|
|
15373
|
-
const name =
|
|
16268
|
+
const data = fs21.readFileSync(filePath);
|
|
16269
|
+
const name = path22.resolve(filePath).replace(/^\//, "").split(path22.sep).join("/");
|
|
15374
16270
|
const nameBuffer = Buffer.from(name, "utf8");
|
|
15375
16271
|
const crc = crc32(data);
|
|
15376
16272
|
const local = Buffer.concat([
|
|
@@ -15422,7 +16318,7 @@ function createZipBackup(files, backupPath) {
|
|
|
15422
16318
|
writeUInt32LE(offset),
|
|
15423
16319
|
writeUInt16LE(0)
|
|
15424
16320
|
]);
|
|
15425
|
-
|
|
16321
|
+
fs21.writeFileSync(backupPath, Buffer.concat([...localParts, ...centralParts, end]));
|
|
15426
16322
|
}
|
|
15427
16323
|
function timestamp() {
|
|
15428
16324
|
return (/* @__PURE__ */ new Date()).toISOString().replace(/[-:]/g, "").replace(/\.\d{3}Z$/, "Z");
|
|
@@ -15432,17 +16328,17 @@ function validationStatus(result) {
|
|
|
15432
16328
|
return result.ok ? "passed" : "failed";
|
|
15433
16329
|
}
|
|
15434
16330
|
function hasLegacyMarker(worktree) {
|
|
15435
|
-
return
|
|
16331
|
+
return fs21.existsSync(path22.join(worktree, ".staticeng")) || fs21.existsSync(path22.join(worktree, ".orbita")) || fs21.existsSync(path22.join(worktree, ".nomadwork")) || fs21.existsSync(path22.join(worktree, ".nomadworks"));
|
|
15436
16332
|
}
|
|
15437
16333
|
function rootOptimaArtifacts(worktree) {
|
|
15438
16334
|
const artifacts = [
|
|
15439
|
-
["tasks/",
|
|
15440
|
-
["evidences/",
|
|
15441
|
-
["docs/scrs/",
|
|
15442
|
-
["codemap.yml",
|
|
15443
|
-
["codemap.yaml",
|
|
16335
|
+
["tasks/", path22.join(worktree, "tasks")],
|
|
16336
|
+
["evidences/", path22.join(worktree, "evidences")],
|
|
16337
|
+
["docs/scrs/", path22.join(worktree, "docs", "scrs")],
|
|
16338
|
+
["codemap.yml", path22.join(worktree, "codemap.yml")],
|
|
16339
|
+
["codemap.yaml", path22.join(worktree, "codemap.yaml")]
|
|
15444
16340
|
];
|
|
15445
|
-
return artifacts.map(([display, artifactPath]) => ({ display, path: artifactPath })).filter((item) =>
|
|
16341
|
+
return artifacts.map(([display, artifactPath]) => ({ display, path: artifactPath })).filter((item) => fs21.existsSync(item.path));
|
|
15446
16342
|
}
|
|
15447
16343
|
function hasSanitizableOverrides(worktree) {
|
|
15448
16344
|
return collectPlannedOverrideFiles(worktree).length > 0;
|
|
@@ -15468,7 +16364,7 @@ function planOptimaMigration(worktree, dryRun) {
|
|
|
15468
16364
|
...overrides.map((file) => ({
|
|
15469
16365
|
category: "override",
|
|
15470
16366
|
action: dryRun ? "would_remove_after_backup" : "removed_after_backup",
|
|
15471
|
-
path:
|
|
16367
|
+
path: path22.relative(worktree, file).split(path22.sep).join("/"),
|
|
15472
16368
|
detail: "Remove implicit agent/policy override after host-level backup."
|
|
15473
16369
|
}))
|
|
15474
16370
|
],
|
|
@@ -15489,12 +16385,12 @@ async function sanitizeHost(options = {}) {
|
|
|
15489
16385
|
const repoStates = [];
|
|
15490
16386
|
for (const candidate of paths) {
|
|
15491
16387
|
try {
|
|
15492
|
-
if (!
|
|
16388
|
+
if (!fs21.existsSync(candidate)) {
|
|
15493
16389
|
report.totals.skipped += 1;
|
|
15494
16390
|
report.repos.push({ path: candidate, status: "skipped", reason: "missing" });
|
|
15495
16391
|
continue;
|
|
15496
16392
|
}
|
|
15497
|
-
if (!
|
|
16393
|
+
if (!fs21.statSync(candidate).isDirectory()) {
|
|
15498
16394
|
report.totals.skipped += 1;
|
|
15499
16395
|
report.repos.push({ path: candidate, status: "skipped", reason: "not_directory" });
|
|
15500
16396
|
continue;
|
|
@@ -15516,7 +16412,7 @@ async function sanitizeHost(options = {}) {
|
|
|
15516
16412
|
const overrides = repoStates.flatMap((state) => state.overrideFiles.map((file) => ({ repo: state.repo, file })));
|
|
15517
16413
|
report.totals.overrideFiles = overrides.length;
|
|
15518
16414
|
if (!dryRun && overrides.length > 0) {
|
|
15519
|
-
const backupPath =
|
|
16415
|
+
const backupPath = path22.join(os7.homedir(), BACKUP_DIRNAME, `${timestamp()}.zip`);
|
|
15520
16416
|
try {
|
|
15521
16417
|
createZipBackup(overrides.map((item) => item.file), backupPath);
|
|
15522
16418
|
report.backupPath = backupPath;
|
|
@@ -15532,7 +16428,7 @@ async function sanitizeHost(options = {}) {
|
|
|
15532
16428
|
for (const state of repoStates) {
|
|
15533
16429
|
try {
|
|
15534
16430
|
if (!dryRun) {
|
|
15535
|
-
for (const file of state.overrideFiles)
|
|
16431
|
+
for (const file of state.overrideFiles) fs21.rmSync(file, { force: true });
|
|
15536
16432
|
}
|
|
15537
16433
|
const validation = !dryRun && hasLegacyMarker(state.repo) ? await optima_validate_logic(state.repo) : null;
|
|
15538
16434
|
const status = (!validation || validation.ok) && state.plan.unresolved.length === 0 ? "repaired" : "failed";
|
|
@@ -15545,7 +16441,7 @@ async function sanitizeHost(options = {}) {
|
|
|
15545
16441
|
repairActions: state.plan.actions.length,
|
|
15546
16442
|
repairSkipped: state.plan.skippedRepair === true,
|
|
15547
16443
|
overrideFiles: state.overrideFiles.length,
|
|
15548
|
-
overridePaths: state.overrideFiles.map((file) =>
|
|
16444
|
+
overridePaths: state.overrideFiles.map((file) => path22.relative(state.repo, file).split(path22.sep).join("/")),
|
|
15549
16445
|
validation: validation ? validationStatus(validation) : "skipped",
|
|
15550
16446
|
unresolved: state.plan.unresolved
|
|
15551
16447
|
});
|