@glrs-dev/cli 2.1.0 → 2.3.0
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/CHANGELOG.md +4 -0
- package/dist/{chunk-SB3MLROC.js → chunk-MIWZLETC.js} +7 -2
- package/dist/cli.js +1 -1
- package/dist/lib/auto-update.js +1 -1
- package/dist/vendor/harness-opencode/dist/agents/prompts/build.md +34 -4
- package/dist/vendor/harness-opencode/dist/agents/prompts/build.open.md +18 -4
- package/dist/vendor/harness-opencode/dist/agents/prompts/code-reviewer-thorough.md +77 -0
- package/dist/vendor/harness-opencode/dist/agents/prompts/code-reviewer.md +80 -0
- package/dist/vendor/harness-opencode/dist/agents/prompts/code-reviewer.open.md +68 -0
- package/dist/vendor/harness-opencode/dist/agents/prompts/debriefer.md +55 -0
- package/dist/vendor/harness-opencode/dist/agents/prompts/gap-analyzer.md +2 -0
- package/dist/vendor/harness-opencode/dist/agents/prompts/plan-reviewer.md +5 -1
- package/dist/vendor/harness-opencode/dist/agents/prompts/plan.md +119 -10
- package/dist/vendor/harness-opencode/dist/agents/prompts/prime.md +149 -88
- package/dist/vendor/harness-opencode/dist/agents/prompts/research-auto.md +1 -1
- package/dist/vendor/harness-opencode/dist/agents/prompts/research-local.md +1 -1
- package/dist/vendor/harness-opencode/dist/agents/prompts/research-web.md +1 -1
- package/dist/vendor/harness-opencode/dist/agents/prompts/research.md +2 -0
- package/dist/vendor/harness-opencode/dist/agents/prompts/scoper.md +129 -0
- package/dist/vendor/harness-opencode/dist/agents/prompts/spec-reviewer.md +53 -0
- package/dist/vendor/harness-opencode/dist/agents/prompts/spec-reviewer.open.md +56 -0
- package/dist/vendor/harness-opencode/dist/agents/shared/index.ts +1 -0
- package/dist/vendor/harness-opencode/dist/agents/shared/ui-evaluation-ladder.md +50 -0
- package/dist/vendor/harness-opencode/dist/agents/shared/workflow-mechanics.md +5 -5
- package/dist/vendor/harness-opencode/dist/autopilot/prompt-template.md +104 -0
- package/dist/vendor/harness-opencode/dist/chunk-GCWHRUOK.js +259 -0
- package/dist/vendor/harness-opencode/dist/chunk-MJSMBY2Y.js +87 -0
- package/dist/vendor/harness-opencode/dist/chunk-NIFAVPNN.js +544 -0
- package/dist/vendor/harness-opencode/dist/{chunk-VJUETC6A.js → chunk-PDMXYZM4.js} +53 -1
- package/dist/vendor/harness-opencode/dist/cli.js +1596 -1964
- package/dist/vendor/harness-opencode/dist/commands/prompts/fresh.md +27 -24
- package/dist/vendor/harness-opencode/dist/commands/prompts/review.md +3 -3
- package/dist/vendor/harness-opencode/dist/commands/prompts/ship.md +2 -0
- package/dist/vendor/harness-opencode/dist/index.js +188 -633
- package/dist/vendor/harness-opencode/dist/loop-session-J35NILUZ.js +30 -0
- package/dist/vendor/harness-opencode/dist/opencode-server-KPCDFYAX.js +22 -0
- package/dist/vendor/harness-opencode/dist/plan-parser-TMHEKT22.js +6 -0
- package/dist/vendor/harness-opencode/dist/plan-session-7VS32P52.js +117 -0
- package/dist/vendor/harness-opencode/dist/scoper-S77SOK7X.js +326 -0
- package/dist/vendor/harness-opencode/dist/skills/adversarial-review-rubric/SKILL.md +47 -0
- package/dist/vendor/harness-opencode/dist/skills/code-quality/SKILL.md +1 -1
- package/dist/vendor/harness-opencode/dist/skills/root-cause-diagnosis/SKILL.md +24 -0
- package/dist/vendor/harness-opencode/dist/skills/spear-protocol/SKILL.md +167 -0
- package/dist/vendor/harness-opencode/package.json +1 -1
- package/package.json +3 -1
- package/dist/vendor/harness-opencode/dist/agents/prompts/pilot-assessor.md +0 -77
- package/dist/vendor/harness-opencode/dist/agents/prompts/pilot-builder.md +0 -40
- package/dist/vendor/harness-opencode/dist/agents/prompts/pilot-planner.md +0 -56
- package/dist/vendor/harness-opencode/dist/agents/prompts/pilot-scoper.md +0 -58
- package/dist/vendor/harness-opencode/dist/agents/prompts/qa-reviewer.md +0 -68
- package/dist/vendor/harness-opencode/dist/agents/prompts/qa-reviewer.open.md +0 -58
- package/dist/vendor/harness-opencode/dist/agents/prompts/qa-thorough.md +0 -63
- package/dist/vendor/harness-opencode/dist/bin/plan-check.sh +0 -255
- package/dist/vendor/harness-opencode/dist/chunk-6CZPRUMJ.js +0 -869
- package/dist/vendor/harness-opencode/dist/chunk-DZG4D3OH.js +0 -54
- package/dist/vendor/harness-opencode/dist/chunk-OYRKOEXK.js +0 -88
- package/dist/vendor/harness-opencode/dist/commands/prompts/autopilot.md +0 -96
- package/dist/vendor/harness-opencode/dist/install-6775ZBDG.js +0 -13
- package/dist/vendor/harness-opencode/dist/paths-WZ23ZQOV.js +0 -18
|
@@ -1,2082 +1,1763 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
import {
|
|
3
|
+
getOpenCodeCachePackageDir,
|
|
4
|
+
inspectCachePin,
|
|
5
|
+
readOurPackageVersion,
|
|
6
|
+
refreshPluginCache,
|
|
3
7
|
validateModelOverride
|
|
4
|
-
} from "./chunk-
|
|
8
|
+
} from "./chunk-PDMXYZM4.js";
|
|
5
9
|
import {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
10
|
+
MAX_ITERATIONS,
|
|
11
|
+
TIMEOUT_MS,
|
|
12
|
+
runRalphLoop
|
|
13
|
+
} from "./chunk-NIFAVPNN.js";
|
|
14
|
+
import "./chunk-MJSMBY2Y.js";
|
|
9
15
|
import {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
getScopeArtifactPath,
|
|
15
|
-
getStateDbPath
|
|
16
|
-
} from "./chunk-OYRKOEXK.js";
|
|
16
|
+
createSession,
|
|
17
|
+
getLastAssistantMessage,
|
|
18
|
+
sendAndWait
|
|
19
|
+
} from "./chunk-GCWHRUOK.js";
|
|
17
20
|
|
|
18
21
|
// src/cli.ts
|
|
19
22
|
import {
|
|
20
23
|
binary,
|
|
21
|
-
command as
|
|
24
|
+
command as command3,
|
|
22
25
|
flag as flag2,
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
positional,
|
|
26
|
-
restPositionals as restPositionals2,
|
|
27
|
-
string as string2,
|
|
28
|
-
subcommands as subcommands2,
|
|
26
|
+
positional as positional2,
|
|
27
|
+
subcommands,
|
|
29
28
|
run
|
|
30
29
|
} from "cmd-ts";
|
|
31
30
|
|
|
32
|
-
// src/cli/
|
|
31
|
+
// src/cli/install.ts
|
|
32
|
+
import * as fs3 from "fs";
|
|
33
|
+
import * as path3 from "path";
|
|
34
|
+
import * as os2 from "os";
|
|
35
|
+
import { fileURLToPath } from "url";
|
|
36
|
+
|
|
37
|
+
// src/cli/merge-config.ts
|
|
33
38
|
import * as fs from "fs";
|
|
34
39
|
import * as path from "path";
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
const
|
|
43
|
-
const
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
if (
|
|
57
|
-
|
|
58
|
-
return;
|
|
40
|
+
var UNION_ALLOWLIST = /* @__PURE__ */ new Set(["plugin"]);
|
|
41
|
+
function isPlainObject(v) {
|
|
42
|
+
return typeof v === "object" && v !== null && !Array.isArray(v) && Object.prototype.toString.call(v) === "[object Object]";
|
|
43
|
+
}
|
|
44
|
+
function deepClone(v) {
|
|
45
|
+
if (v === null || typeof v !== "object") return v;
|
|
46
|
+
if (Array.isArray(v)) return v.map(deepClone);
|
|
47
|
+
const out = {};
|
|
48
|
+
for (const k of Object.keys(v)) {
|
|
49
|
+
out[k] = deepClone(v[k]);
|
|
50
|
+
}
|
|
51
|
+
return out;
|
|
52
|
+
}
|
|
53
|
+
function fmtPath(parts) {
|
|
54
|
+
return parts.map((p) => /^[A-Za-z_$][\w$]*$/.test(p) ? p : `["${p.replace(/"/g, '\\"')}"]`).reduce((acc, part) => {
|
|
55
|
+
if (acc === "") return part;
|
|
56
|
+
if (part.startsWith("[")) return acc + part;
|
|
57
|
+
return acc + "." + part;
|
|
58
|
+
}, "");
|
|
59
|
+
}
|
|
60
|
+
function pluginName(entry) {
|
|
61
|
+
if (typeof entry === "string") {
|
|
62
|
+
const atIdx = entry.indexOf("@", 1);
|
|
63
|
+
return atIdx > 0 ? entry.slice(0, atIdx) : entry;
|
|
64
|
+
}
|
|
65
|
+
if (Array.isArray(entry) && typeof entry[0] === "string") {
|
|
66
|
+
const name = entry[0];
|
|
67
|
+
const atIdx = name.indexOf("@", 1);
|
|
68
|
+
return atIdx > 0 ? name.slice(0, atIdx) : name;
|
|
69
|
+
}
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
function mergeWalk(src, dst, pathParts, additions, warnings) {
|
|
73
|
+
for (const key of Object.keys(src)) {
|
|
74
|
+
const sv = src[key];
|
|
75
|
+
const newPath = pathParts.concat([key]);
|
|
76
|
+
const pathStr = fmtPath(newPath);
|
|
77
|
+
if (!Object.prototype.hasOwnProperty.call(dst, key)) {
|
|
78
|
+
dst[key] = deepClone(sv);
|
|
79
|
+
additions.push(`added: ${pathStr}`);
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
const dv = dst[key];
|
|
83
|
+
if (isPlainObject(sv) && isPlainObject(dv)) {
|
|
84
|
+
mergeWalk(sv, dv, newPath, additions, warnings);
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
if (isPlainObject(sv) && !isPlainObject(dv)) {
|
|
88
|
+
warnings.push(
|
|
89
|
+
`WARN: scalar-vs-object: user has non-object at ${pathStr} where we ship an object; not migrating. To adopt: ${JSON.stringify(sv)}`
|
|
90
|
+
);
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
if (Array.isArray(sv)) {
|
|
94
|
+
if (!Array.isArray(dv)) {
|
|
95
|
+
warnings.push(
|
|
96
|
+
`WARN: scalar-vs-array: user has non-array at ${pathStr} where we ship an array; not migrating. To adopt: ${JSON.stringify(sv)}`
|
|
97
|
+
);
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
const joined = newPath.join(".");
|
|
101
|
+
if (UNION_ALLOWLIST.has(joined)) {
|
|
102
|
+
for (const item of sv) {
|
|
103
|
+
const srcName = pluginName(item);
|
|
104
|
+
if (srcName) {
|
|
105
|
+
const dstIdx = dv.findIndex(
|
|
106
|
+
(x) => pluginName(x) === srcName
|
|
107
|
+
);
|
|
108
|
+
if (dstIdx >= 0) {
|
|
109
|
+
const srcIsTuple = Array.isArray(item) && item.length >= 2;
|
|
110
|
+
const dstIsTuple = Array.isArray(dv[dstIdx]) && dv[dstIdx].length >= 2;
|
|
111
|
+
if (srcIsTuple && !dstIsTuple) {
|
|
112
|
+
dv[dstIdx] = deepClone(item);
|
|
113
|
+
additions.push(`upgraded: ${pathStr}[${JSON.stringify(srcName)}] to tuple form`);
|
|
114
|
+
}
|
|
115
|
+
} else {
|
|
116
|
+
dv.push(deepClone(item));
|
|
117
|
+
additions.push(`appended: ${pathStr}[${JSON.stringify(item)}]`);
|
|
118
|
+
}
|
|
119
|
+
} else {
|
|
120
|
+
const needle = JSON.stringify(item);
|
|
121
|
+
const alreadyPresent = dv.some(
|
|
122
|
+
(x) => JSON.stringify(x) === needle
|
|
123
|
+
);
|
|
124
|
+
if (!alreadyPresent) {
|
|
125
|
+
dv.push(deepClone(item));
|
|
126
|
+
additions.push(`appended: ${pathStr}[${JSON.stringify(item)}]`);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
59
133
|
}
|
|
60
|
-
|
|
134
|
+
}
|
|
135
|
+
function mergeConfig(srcJson, dstPath, dryRun = false) {
|
|
136
|
+
let dstText;
|
|
61
137
|
try {
|
|
62
|
-
|
|
138
|
+
dstText = fs.readFileSync(dstPath, "utf8");
|
|
63
139
|
} catch (e) {
|
|
64
|
-
|
|
65
|
-
process.exit(1);
|
|
140
|
+
throw new Error(`Failed to read dst ${dstPath}: ${e.message}`);
|
|
66
141
|
}
|
|
67
|
-
let
|
|
142
|
+
let dst;
|
|
68
143
|
try {
|
|
69
|
-
|
|
144
|
+
dst = JSON.parse(dstText);
|
|
70
145
|
} catch (e) {
|
|
71
|
-
|
|
72
|
-
|
|
146
|
+
throw new Error(
|
|
147
|
+
`User config at ${dstPath} has invalid JSON: ${e.message}. Not touching the file.`
|
|
148
|
+
);
|
|
73
149
|
}
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
150
|
+
if (!isPlainObject(dst)) {
|
|
151
|
+
throw new Error(
|
|
152
|
+
`User config at ${dstPath} is not a JSON object at the top level.`
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
const additions = [];
|
|
156
|
+
const warnings = [];
|
|
157
|
+
mergeWalk(srcJson, dst, [], additions, warnings);
|
|
158
|
+
if (additions.length === 0) {
|
|
159
|
+
return { changed: false, warnings };
|
|
82
160
|
}
|
|
83
161
|
if (dryRun) {
|
|
84
|
-
|
|
85
|
-
return;
|
|
162
|
+
return { changed: true, bakPath: "(dry-run)", additions, warnings };
|
|
86
163
|
}
|
|
87
|
-
const
|
|
164
|
+
const suffix = `${Date.now()}-${process.pid}`;
|
|
165
|
+
const bakPath = `${dstPath}.bak.${suffix}`;
|
|
166
|
+
const tmpPath = `${dstPath}.merge.tmp.${suffix}`;
|
|
88
167
|
try {
|
|
89
|
-
fs.copyFileSync(
|
|
168
|
+
fs.copyFileSync(dstPath, bakPath);
|
|
90
169
|
} catch (e) {
|
|
91
|
-
|
|
92
|
-
|
|
170
|
+
throw new Error(`Failed to write backup ${bakPath}: ${e.message}`);
|
|
171
|
+
}
|
|
172
|
+
const serialized = JSON.stringify(dst, null, 2) + "\n";
|
|
173
|
+
try {
|
|
174
|
+
fs.writeFileSync(tmpPath, serialized);
|
|
175
|
+
} catch (e) {
|
|
176
|
+
try {
|
|
177
|
+
fs.unlinkSync(bakPath);
|
|
178
|
+
} catch {
|
|
179
|
+
}
|
|
180
|
+
throw new Error(`Failed to write tempfile ${tmpPath}: ${e.message}`);
|
|
93
181
|
}
|
|
94
|
-
config.plugin = filtered;
|
|
95
|
-
const tmpPath = `${configPath}.uninstall.tmp.${Date.now()}-${process.pid}`;
|
|
96
182
|
try {
|
|
97
|
-
fs.
|
|
98
|
-
fs.renameSync(tmpPath, configPath);
|
|
183
|
+
fs.renameSync(tmpPath, dstPath);
|
|
99
184
|
} catch (e) {
|
|
100
185
|
try {
|
|
101
186
|
fs.unlinkSync(tmpPath);
|
|
102
187
|
} catch {
|
|
103
188
|
}
|
|
104
|
-
|
|
105
|
-
|
|
189
|
+
try {
|
|
190
|
+
fs.unlinkSync(bakPath);
|
|
191
|
+
} catch {
|
|
192
|
+
}
|
|
193
|
+
throw new Error(`Failed to rename ${tmpPath} \u2192 ${dstPath}: ${e.message}`);
|
|
106
194
|
}
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
195
|
+
return { changed: true, bakPath, additions, warnings };
|
|
196
|
+
}
|
|
197
|
+
function seedConfig(srcJson, dstPath) {
|
|
198
|
+
fs.mkdirSync(path.dirname(dstPath), { recursive: true });
|
|
199
|
+
fs.writeFileSync(dstPath, JSON.stringify(srcJson, null, 2) + "\n");
|
|
112
200
|
}
|
|
113
201
|
|
|
114
|
-
// src/cli/
|
|
202
|
+
// src/cli/plugin-check.ts
|
|
115
203
|
import * as fs2 from "fs";
|
|
116
204
|
import * as path2 from "path";
|
|
117
|
-
import * as
|
|
118
|
-
import {
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
const
|
|
122
|
-
|
|
205
|
+
import * as os from "os";
|
|
206
|
+
import { select, checkbox, confirm } from "@inquirer/prompts";
|
|
207
|
+
async function promptChoice(question, choices, defaultIndex = 0) {
|
|
208
|
+
if (!process.stdin.isTTY) return defaultIndex;
|
|
209
|
+
const answer = await select({
|
|
210
|
+
message: question,
|
|
211
|
+
choices: choices.map((label, i) => ({
|
|
212
|
+
name: label,
|
|
213
|
+
value: i
|
|
214
|
+
})),
|
|
215
|
+
default: defaultIndex
|
|
216
|
+
});
|
|
217
|
+
return answer;
|
|
123
218
|
}
|
|
124
|
-
function
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
219
|
+
async function promptMulti(question, choices) {
|
|
220
|
+
if (!process.stdin.isTTY) {
|
|
221
|
+
const defaults = /* @__PURE__ */ new Set();
|
|
222
|
+
choices.forEach((c3, i) => {
|
|
223
|
+
if (c3.defaultOn) defaults.add(i);
|
|
224
|
+
});
|
|
225
|
+
return defaults;
|
|
226
|
+
}
|
|
227
|
+
const answers = await checkbox({
|
|
228
|
+
message: question,
|
|
229
|
+
choices: choices.map((c3, i) => ({
|
|
230
|
+
name: c3.label,
|
|
231
|
+
value: i,
|
|
232
|
+
checked: c3.defaultOn
|
|
233
|
+
}))
|
|
234
|
+
});
|
|
235
|
+
return new Set(answers);
|
|
130
236
|
}
|
|
131
|
-
|
|
132
|
-
|
|
237
|
+
|
|
238
|
+
// src/cli/models-dev.ts
|
|
239
|
+
var MODELS_DEV_URL = "https://models.dev/api.json";
|
|
240
|
+
var FETCH_TIMEOUT_MS = 5e3;
|
|
241
|
+
function combinedCost(m) {
|
|
242
|
+
const input = m.cost?.input ?? 0;
|
|
243
|
+
const output = m.cost?.output ?? 0;
|
|
244
|
+
return input + output;
|
|
245
|
+
}
|
|
246
|
+
function suggestTiersFromModelsDev(provider) {
|
|
247
|
+
const models = Object.values(provider.models).sort(
|
|
248
|
+
(a, b) => combinedCost(b) - combinedCost(a)
|
|
249
|
+
);
|
|
250
|
+
if (models.length === 0) {
|
|
251
|
+
throw new Error(`Provider "${provider.id}" has no models`);
|
|
252
|
+
}
|
|
253
|
+
const deep = models[0];
|
|
254
|
+
const fast = models[models.length - 1];
|
|
255
|
+
let mid;
|
|
256
|
+
if (models.length <= 2) {
|
|
257
|
+
mid = models.length === 1 ? deep : fast;
|
|
258
|
+
} else {
|
|
259
|
+
const midCost = (combinedCost(deep) + combinedCost(fast)) / 2;
|
|
260
|
+
const candidates = models.filter(
|
|
261
|
+
(m) => m.id !== deep.id && m.id !== fast.id
|
|
262
|
+
);
|
|
263
|
+
mid = candidates.reduce(
|
|
264
|
+
(best, m) => Math.abs(combinedCost(m) - midCost) < Math.abs(combinedCost(best) - midCost) ? m : best
|
|
265
|
+
);
|
|
266
|
+
}
|
|
267
|
+
const ref = (m) => `${provider.id}/${m.id}`;
|
|
268
|
+
return {
|
|
269
|
+
deep: ref(deep),
|
|
270
|
+
mid: ref(mid),
|
|
271
|
+
fast: ref(fast)
|
|
272
|
+
};
|
|
133
273
|
}
|
|
134
|
-
function
|
|
135
|
-
const
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
274
|
+
function pickBedrockTierIds(provider) {
|
|
275
|
+
const models = Object.values(provider.models);
|
|
276
|
+
const mostRecent = (candidates) => {
|
|
277
|
+
if (candidates.length === 0) return null;
|
|
278
|
+
return [...candidates].sort((a, b) => {
|
|
279
|
+
const aDate = a.last_updated ?? "";
|
|
280
|
+
const bDate = b.last_updated ?? "";
|
|
281
|
+
if (aDate !== bDate) return bDate.localeCompare(aDate);
|
|
282
|
+
return b.id.localeCompare(a.id);
|
|
283
|
+
})[0];
|
|
141
284
|
};
|
|
142
|
-
const
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
285
|
+
const pickFamily = (familyKeyword) => {
|
|
286
|
+
const globalCandidates = models.filter(
|
|
287
|
+
(m) => m.id.startsWith(`global.anthropic.claude-${familyKeyword}-`)
|
|
288
|
+
);
|
|
289
|
+
const globalPick = mostRecent(globalCandidates);
|
|
290
|
+
if (globalPick) return globalPick;
|
|
291
|
+
const nonPrefixedCandidates = models.filter(
|
|
292
|
+
(m) => m.id.startsWith(`anthropic.claude-${familyKeyword}-`)
|
|
293
|
+
);
|
|
294
|
+
return mostRecent(nonPrefixedCandidates);
|
|
295
|
+
};
|
|
296
|
+
const opus = pickFamily("opus");
|
|
297
|
+
const sonnet = pickFamily("sonnet");
|
|
298
|
+
const haiku = pickFamily("haiku");
|
|
299
|
+
if (!opus || !sonnet || !haiku) {
|
|
300
|
+
return suggestTiersFromModelsDev(provider);
|
|
153
301
|
}
|
|
154
|
-
const
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
const modelSources = [];
|
|
180
|
-
if (pluginOptions && typeof pluginOptions.models === "object") {
|
|
181
|
-
modelSources.push({
|
|
182
|
-
label: "plugin options.models",
|
|
183
|
-
block: pluginOptions.models
|
|
184
|
-
});
|
|
185
|
-
}
|
|
186
|
-
const legacyHarness = config.harness;
|
|
187
|
-
if (legacyHarness && typeof legacyHarness.models === "object") {
|
|
188
|
-
modelSources.push({
|
|
189
|
-
label: "harness.models (legacy)",
|
|
190
|
-
block: legacyHarness.models
|
|
191
|
-
});
|
|
192
|
-
}
|
|
193
|
-
if (modelSources.length > 0) {
|
|
194
|
-
const invalid = [];
|
|
195
|
-
for (const { label, block } of modelSources) {
|
|
196
|
-
if (!block || typeof block !== "object") continue;
|
|
197
|
-
for (const [key, rawValue] of Object.entries(block)) {
|
|
198
|
-
const candidate = Array.isArray(rawValue) ? rawValue[0] : rawValue;
|
|
199
|
-
if (typeof candidate !== "string") continue;
|
|
200
|
-
const result = validateModelOverride(candidate);
|
|
201
|
-
if (!result.valid) {
|
|
202
|
-
invalid.push({
|
|
203
|
-
keyPath: `${label}.${key}`,
|
|
204
|
-
value: candidate,
|
|
205
|
-
suggestion: result.suggestion,
|
|
206
|
-
reason: result.reason
|
|
207
|
-
});
|
|
208
|
-
}
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
|
-
if (invalid.length === 0) {
|
|
212
|
-
ok("model overrides look valid");
|
|
213
|
-
} else {
|
|
214
|
-
for (const entry of invalid) {
|
|
215
|
-
fail(`invalid model override at ${entry.keyPath}: "${entry.value}"`);
|
|
216
|
-
if (entry.reason) {
|
|
217
|
-
console.log(` ${c2.yellow}reason:${c2.reset} ${entry.reason}`);
|
|
218
|
-
}
|
|
219
|
-
if (entry.suggestion) {
|
|
220
|
-
console.log(
|
|
221
|
-
` ${c2.yellow}fix:${c2.reset} remove this key, or replace with \`${entry.suggestion}\``
|
|
222
|
-
);
|
|
223
|
-
} else {
|
|
224
|
-
console.log(
|
|
225
|
-
` ${c2.yellow}fix:${c2.reset} remove this key, or run \`bunx ${PLUGIN_NAME2} install\` to pick a current preset`
|
|
226
|
-
);
|
|
227
|
-
}
|
|
228
|
-
}
|
|
229
|
-
}
|
|
230
|
-
}
|
|
231
|
-
} catch {
|
|
232
|
-
fail(`opencode.json at ${configPath} has invalid JSON`);
|
|
302
|
+
const ref = (m) => `${provider.id}/${m.id}`;
|
|
303
|
+
return {
|
|
304
|
+
deep: ref(opus),
|
|
305
|
+
mid: ref(sonnet),
|
|
306
|
+
fast: ref(haiku)
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
async function fetchModelsDevProviders() {
|
|
310
|
+
const controller = new AbortController();
|
|
311
|
+
const timer = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
|
|
312
|
+
try {
|
|
313
|
+
const res = await fetch(MODELS_DEV_URL, { signal: controller.signal });
|
|
314
|
+
if (!res.ok) return null;
|
|
315
|
+
const data = await res.json();
|
|
316
|
+
if (!data || typeof data !== "object" || Array.isArray(data)) return null;
|
|
317
|
+
const providers = [];
|
|
318
|
+
for (const [key, rawValue] of Object.entries(
|
|
319
|
+
data
|
|
320
|
+
)) {
|
|
321
|
+
if (!rawValue || typeof rawValue !== "object") continue;
|
|
322
|
+
const value = rawValue;
|
|
323
|
+
if (typeof value.id !== "string" || value.id !== key) continue;
|
|
324
|
+
if (typeof value.name !== "string") continue;
|
|
325
|
+
if (!value.models || typeof value.models !== "object") continue;
|
|
326
|
+
providers.push(value);
|
|
233
327
|
}
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
warn("uvx not found \u2014 serena and git MCPs won't work. Install: brew install uv");
|
|
241
|
-
}
|
|
242
|
-
if (which("node") && which("npx")) {
|
|
243
|
-
ok(`node ${cmd("node --version") ?? ""} + npx (memory MCP)`);
|
|
244
|
-
} else {
|
|
245
|
-
warn("node/npx not found \u2014 memory MCP won't work");
|
|
246
|
-
}
|
|
247
|
-
const planCheckResult = cmd(`bunx ${PLUGIN_NAME2} plan-check --help 2>/dev/null`);
|
|
248
|
-
if (planCheckResult !== null) {
|
|
249
|
-
ok("plan-check CLI invokable");
|
|
250
|
-
} else {
|
|
251
|
-
warn("plan-check CLI not invokable \u2014 try: bun install");
|
|
252
|
-
}
|
|
253
|
-
if (which("bun")) {
|
|
254
|
-
ok(`bun ${cmd("bun --version") ?? ""}`);
|
|
255
|
-
} else if (which("npm")) {
|
|
256
|
-
ok(`npm ${cmd("npm --version") ?? ""} (install bun for faster installs)`);
|
|
257
|
-
} else {
|
|
258
|
-
fail("Neither bun nor npm found \u2014 cannot install plugins");
|
|
328
|
+
if (providers.length === 0) return null;
|
|
329
|
+
return providers;
|
|
330
|
+
} catch {
|
|
331
|
+
return null;
|
|
332
|
+
} finally {
|
|
333
|
+
clearTimeout(timer);
|
|
259
334
|
}
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// src/cli/install.ts
|
|
338
|
+
var PLUGIN_NAME = "@glrs-dev/harness-plugin-opencode";
|
|
339
|
+
var c = {
|
|
340
|
+
reset: "\x1B[0m",
|
|
341
|
+
green: "\x1B[32m",
|
|
342
|
+
yellow: "\x1B[33m",
|
|
343
|
+
blue: "\x1B[34m",
|
|
344
|
+
dim: "\x1B[2m",
|
|
345
|
+
bold: "\x1B[1m"
|
|
346
|
+
};
|
|
347
|
+
var ok = (msg) => console.log(`${c.green}\u2713${c.reset} ${msg}`);
|
|
348
|
+
var info = (msg) => console.log(`${c.blue}\u2022${c.reset} ${msg}`);
|
|
349
|
+
var warn = (msg) => console.log(`${c.yellow}!${c.reset} ${msg}`);
|
|
350
|
+
var MODEL_PRESETS = [
|
|
351
|
+
{
|
|
352
|
+
label: "Anthropic API (direct)",
|
|
353
|
+
providerId: "anthropic",
|
|
354
|
+
deep: "anthropic/claude-opus-4-7",
|
|
355
|
+
mid: "anthropic/claude-sonnet-4-6",
|
|
356
|
+
fast: "anthropic/claude-haiku-4-5-20251001"
|
|
357
|
+
},
|
|
358
|
+
{
|
|
359
|
+
label: "AWS Bedrock",
|
|
360
|
+
providerId: "amazon-bedrock",
|
|
361
|
+
deep: "amazon-bedrock/global.anthropic.claude-opus-4-7",
|
|
362
|
+
mid: "amazon-bedrock/global.anthropic.claude-sonnet-4-6",
|
|
363
|
+
fast: "amazon-bedrock/global.anthropic.claude-haiku-4-5-20251001-v1:0"
|
|
364
|
+
},
|
|
365
|
+
{
|
|
366
|
+
label: "Google Vertex AI (Claude)",
|
|
367
|
+
providerId: "google-vertex-anthropic",
|
|
368
|
+
deep: "google-vertex-anthropic/claude-opus-4-7@default",
|
|
369
|
+
mid: "google-vertex-anthropic/claude-sonnet-4-6@default",
|
|
370
|
+
fast: "google-vertex-anthropic/claude-haiku-4-5@20251001"
|
|
267
371
|
}
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
372
|
+
];
|
|
373
|
+
var MCP_TOGGLES = [
|
|
374
|
+
{ name: "playwright", label: "Playwright \u2014 browser automation + visual UI verification (requires Chromium)", defaultOn: false },
|
|
375
|
+
{ name: "linear", label: "Linear \u2014 issue tracker integration", defaultOn: false }
|
|
376
|
+
];
|
|
377
|
+
var PLUGIN_TOGGLES = [
|
|
378
|
+
{
|
|
379
|
+
name: "opencode-snip",
|
|
380
|
+
label: "Token reduction \u2014 opencode-snip (requires Go snip binary)",
|
|
381
|
+
defaultOn: false
|
|
272
382
|
}
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
);
|
|
282
|
-
}
|
|
383
|
+
];
|
|
384
|
+
function extractPluginOptions(config) {
|
|
385
|
+
if (!config) return null;
|
|
386
|
+
const plugins = config.plugin;
|
|
387
|
+
if (!Array.isArray(plugins)) return null;
|
|
388
|
+
for (const entry of plugins) {
|
|
389
|
+
if (Array.isArray(entry) && entry.length >= 2 && (entry[0] === PLUGIN_NAME || String(entry[0]).startsWith(`${PLUGIN_NAME}@`))) {
|
|
390
|
+
return entry[1];
|
|
283
391
|
}
|
|
284
|
-
} else {
|
|
285
|
-
warn(
|
|
286
|
-
"could not run `opencode agent list` \u2014 skipping pilot agent registration check"
|
|
287
|
-
);
|
|
288
392
|
}
|
|
289
|
-
|
|
393
|
+
return null;
|
|
290
394
|
}
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
import { execFileSync } from "child_process";
|
|
294
|
-
import { fileURLToPath } from "url";
|
|
295
|
-
import { dirname, join as join3 } from "path";
|
|
296
|
-
function planCheck(args) {
|
|
297
|
-
const here = dirname(fileURLToPath(import.meta.url));
|
|
395
|
+
function readPackageVersion() {
|
|
396
|
+
const here = path3.dirname(fileURLToPath(import.meta.url));
|
|
298
397
|
const candidates = [
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
join3(here, "bin", "plan-check.sh")
|
|
302
|
-
// dist: dist/ → dist/bin/plan-check.sh
|
|
398
|
+
path3.join(here, "..", "package.json"),
|
|
399
|
+
path3.join(here, "..", "..", "package.json")
|
|
303
400
|
];
|
|
304
|
-
|
|
305
|
-
for (const p of candidates) {
|
|
401
|
+
for (const candidate of candidates) {
|
|
306
402
|
try {
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
403
|
+
const raw = fs3.readFileSync(candidate, "utf8");
|
|
404
|
+
const parsed = JSON.parse(raw);
|
|
405
|
+
if (parsed.name === PLUGIN_NAME && typeof parsed.version === "string") {
|
|
406
|
+
return parsed.version;
|
|
407
|
+
}
|
|
310
408
|
} catch {
|
|
311
409
|
}
|
|
312
410
|
}
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
411
|
+
throw new Error(
|
|
412
|
+
`Could not locate ${PLUGIN_NAME}'s package.json to read version`
|
|
413
|
+
);
|
|
414
|
+
}
|
|
415
|
+
function getOpencodeConfigPath() {
|
|
416
|
+
const configHome = process.env["XDG_CONFIG_HOME"] ?? path3.join(os2.homedir(), ".config");
|
|
417
|
+
return path3.join(configHome, "opencode", "opencode.json");
|
|
418
|
+
}
|
|
419
|
+
async function refreshPluginCacheIfStale() {
|
|
420
|
+
try {
|
|
421
|
+
const cacheDir = getOpenCodeCachePackageDir();
|
|
422
|
+
const pin = await inspectCachePin(cacheDir);
|
|
423
|
+
if (pin.kind !== "exact") return;
|
|
424
|
+
const ourVersion = readOurPackageVersion(import.meta.url);
|
|
425
|
+
if (pin.version === ourVersion) return;
|
|
426
|
+
const result = await refreshPluginCache(pin.version, ourVersion);
|
|
427
|
+
if (result.outcome === "refreshed") {
|
|
428
|
+
ok(`Plugin cache updated: ${result.fromVersion} \u2192 ${result.toVersion}`);
|
|
429
|
+
}
|
|
430
|
+
} catch {
|
|
316
431
|
}
|
|
432
|
+
}
|
|
433
|
+
function readExistingConfig(configPath) {
|
|
434
|
+
if (!fs3.existsSync(configPath)) return null;
|
|
317
435
|
try {
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
});
|
|
322
|
-
} catch (e) {
|
|
323
|
-
process.exit(e.status ?? 1);
|
|
436
|
+
return JSON.parse(fs3.readFileSync(configPath, "utf8"));
|
|
437
|
+
} catch {
|
|
438
|
+
return null;
|
|
324
439
|
}
|
|
325
440
|
}
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
return;
|
|
346
|
-
}
|
|
347
|
-
resolve2(stdout ?? "");
|
|
348
|
-
}
|
|
349
|
-
);
|
|
350
|
-
});
|
|
351
|
-
}
|
|
352
|
-
function expandTilde(p) {
|
|
353
|
-
if (p === "~") return os3.homedir();
|
|
354
|
-
if (p.startsWith("~/")) return path3.join(os3.homedir(), p.slice(2));
|
|
355
|
-
return p;
|
|
356
|
-
}
|
|
357
|
-
async function getRepoFolder(worktreeDir) {
|
|
358
|
-
let stdout;
|
|
359
|
-
try {
|
|
360
|
-
stdout = await execFileP(
|
|
361
|
-
"git",
|
|
362
|
-
["rev-parse", "--git-common-dir"],
|
|
363
|
-
{ cwd: worktreeDir }
|
|
364
|
-
);
|
|
365
|
-
} catch (err) {
|
|
366
|
-
const msg = err instanceof Error ? err.message : "unknown error running `git rev-parse --git-common-dir`";
|
|
367
|
-
throw new Error(
|
|
368
|
-
`getRepoFolder: failed to resolve git-common-dir for ${worktreeDir}: ${msg}`
|
|
369
|
-
);
|
|
370
|
-
}
|
|
371
|
-
const gitCommonDir = stdout.trim();
|
|
372
|
-
if (!gitCommonDir) {
|
|
373
|
-
throw new Error(
|
|
374
|
-
`getRepoFolder: \`git rev-parse --git-common-dir\` returned empty for ${worktreeDir}`
|
|
375
|
-
);
|
|
441
|
+
function detectModelProvider(existing) {
|
|
442
|
+
const opts = extractPluginOptions(existing);
|
|
443
|
+
const models = opts?.models ?? existing?.harness?.models;
|
|
444
|
+
if (!models) return null;
|
|
445
|
+
const deep = Array.isArray(models.deep) ? models.deep[0] : models.deep;
|
|
446
|
+
if (typeof deep !== "string") return null;
|
|
447
|
+
for (const preset of MODEL_PRESETS) {
|
|
448
|
+
if (deep === preset.deep) return preset.label;
|
|
449
|
+
}
|
|
450
|
+
return `custom (${deep})`;
|
|
451
|
+
}
|
|
452
|
+
function detectEnabledMcps(existing) {
|
|
453
|
+
const enabled = /* @__PURE__ */ new Set();
|
|
454
|
+
const mcp = existing?.mcp;
|
|
455
|
+
if (!mcp || typeof mcp !== "object") return enabled;
|
|
456
|
+
for (const toggle of MCP_TOGGLES) {
|
|
457
|
+
if (mcp[toggle.name]?.enabled === true) {
|
|
458
|
+
enabled.add(toggle.name);
|
|
459
|
+
}
|
|
376
460
|
}
|
|
377
|
-
|
|
378
|
-
const repoRoot = path3.dirname(absCommonDir);
|
|
379
|
-
return path3.basename(repoRoot);
|
|
380
|
-
}
|
|
381
|
-
async function getPlanDir(worktreeDir) {
|
|
382
|
-
const override = process.env.GLORIOUS_PLAN_DIR;
|
|
383
|
-
const base = override ? expandTilde(override) : path3.join(os3.homedir(), ".glorious", "opencode");
|
|
384
|
-
const repoFolder = await getRepoFolder(worktreeDir);
|
|
385
|
-
const planDir = path3.join(base, repoFolder, "plans");
|
|
386
|
-
await fs3.mkdir(planDir, { recursive: true });
|
|
387
|
-
return planDir;
|
|
461
|
+
return enabled;
|
|
388
462
|
}
|
|
389
|
-
|
|
390
|
-
const
|
|
391
|
-
const
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
try {
|
|
398
|
-
await fs3.stat(marker);
|
|
399
|
-
return;
|
|
400
|
-
} catch {
|
|
401
|
-
}
|
|
402
|
-
let entries;
|
|
403
|
-
try {
|
|
404
|
-
entries = await fs3.readdir(oldDir);
|
|
405
|
-
} catch {
|
|
406
|
-
return;
|
|
407
|
-
}
|
|
408
|
-
const planFiles = entries.filter(
|
|
409
|
-
(name) => name.endsWith(".md") && !name.startsWith(".")
|
|
410
|
-
);
|
|
411
|
-
await fs3.mkdir(planDir, { recursive: true });
|
|
412
|
-
for (const name of planFiles) {
|
|
413
|
-
const src = path3.join(oldDir, name);
|
|
414
|
-
const dst = path3.join(planDir, name);
|
|
415
|
-
let dstExists = false;
|
|
416
|
-
try {
|
|
417
|
-
await fs3.stat(dst);
|
|
418
|
-
dstExists = true;
|
|
419
|
-
} catch {
|
|
420
|
-
dstExists = false;
|
|
421
|
-
}
|
|
422
|
-
if (!dstExists) {
|
|
423
|
-
await fs3.rename(src, dst);
|
|
424
|
-
continue;
|
|
425
|
-
}
|
|
426
|
-
const [srcBuf, dstBuf] = await Promise.all([
|
|
427
|
-
fs3.readFile(src),
|
|
428
|
-
fs3.readFile(dst)
|
|
429
|
-
]);
|
|
430
|
-
if (srcBuf.equals(dstBuf)) {
|
|
431
|
-
await fs3.unlink(src);
|
|
432
|
-
continue;
|
|
463
|
+
function detectEnabledPluginToggles(existing) {
|
|
464
|
+
const enabled = /* @__PURE__ */ new Set();
|
|
465
|
+
const plugins = Array.isArray(existing?.plugin) ? existing.plugin : [];
|
|
466
|
+
const toggleNames = new Set(PLUGIN_TOGGLES.map((t) => t.name));
|
|
467
|
+
for (const entry of plugins) {
|
|
468
|
+
const name = typeof entry === "string" ? entry : Array.isArray(entry) ? entry[0] : null;
|
|
469
|
+
if (typeof name === "string" && toggleNames.has(name)) {
|
|
470
|
+
enabled.add(name);
|
|
433
471
|
}
|
|
434
|
-
process.stderr.write(
|
|
435
|
-
`[harness-opencode] migratePlans: conflict on ${name} \u2014 destination ${dst} exists with different content; leaving source ${src} in place. Resolve manually.
|
|
436
|
-
`
|
|
437
|
-
);
|
|
438
472
|
}
|
|
439
|
-
|
|
473
|
+
return enabled;
|
|
440
474
|
}
|
|
441
|
-
|
|
442
|
-
// src/pilot/cli/index.ts
|
|
443
|
-
import { subcommands } from "cmd-ts";
|
|
444
|
-
|
|
445
|
-
// src/pilot/cli/configure.ts
|
|
446
|
-
import { command } from "cmd-ts";
|
|
447
|
-
import { input, select, confirm, number } from "@inquirer/prompts";
|
|
448
|
-
|
|
449
|
-
// src/pilot/config.ts
|
|
450
|
-
import * as fs4 from "fs";
|
|
451
|
-
var DEFAULT_MODEL = "anthropic/claude-sonnet-4-6";
|
|
452
|
-
var DEFAULT_CONFIG = {
|
|
453
|
-
models: {
|
|
454
|
-
scope: DEFAULT_MODEL,
|
|
455
|
-
plan: DEFAULT_MODEL,
|
|
456
|
-
execute: DEFAULT_MODEL,
|
|
457
|
-
assess: DEFAULT_MODEL
|
|
458
|
-
},
|
|
459
|
-
verify: {
|
|
460
|
-
baseline: [],
|
|
461
|
-
after_each: []
|
|
462
|
-
},
|
|
463
|
-
max_assess_cycles: 3,
|
|
464
|
-
playwright: {
|
|
465
|
-
enabled: false,
|
|
466
|
-
base_url: "http://localhost:3000"
|
|
467
|
-
}
|
|
468
|
-
};
|
|
469
|
-
function loadPilotConfig(cwd) {
|
|
470
|
-
const configPath = getPilotConfigPath(cwd);
|
|
471
|
-
if (!fs4.existsSync(configPath)) {
|
|
472
|
-
return { ...DEFAULT_CONFIG };
|
|
473
|
-
}
|
|
474
|
-
let raw;
|
|
475
|
+
function migrateHarnessKeyToPluginOptions(configPath) {
|
|
475
476
|
try {
|
|
476
|
-
|
|
477
|
+
if (!fs3.existsSync(configPath)) return;
|
|
478
|
+
const raw = fs3.readFileSync(configPath, "utf8");
|
|
479
|
+
const config = JSON.parse(raw);
|
|
480
|
+
if (!config.harness || typeof config.harness !== "object") return;
|
|
481
|
+
const plugins = Array.isArray(config.plugin) ? config.plugin : [];
|
|
482
|
+
const pluginIdx = plugins.findIndex((entry) => {
|
|
483
|
+
const name = typeof entry === "string" ? entry : Array.isArray(entry) ? entry[0] : null;
|
|
484
|
+
return name === PLUGIN_NAME || String(name ?? "").startsWith(`${PLUGIN_NAME}@`);
|
|
485
|
+
});
|
|
486
|
+
if (pluginIdx < 0) return;
|
|
487
|
+
const current = plugins[pluginIdx];
|
|
488
|
+
const existingName = typeof current === "string" ? current : Array.isArray(current) ? current[0] : PLUGIN_NAME;
|
|
489
|
+
const existingOpts = Array.isArray(current) && current.length >= 2 ? current[1] : {};
|
|
490
|
+
const merged = { ...config.harness, ...existingOpts };
|
|
491
|
+
plugins[pluginIdx] = [existingName, merged];
|
|
492
|
+
delete config.harness;
|
|
493
|
+
const bakPath = `${configPath}.bak.${Date.now()}-${process.pid}`;
|
|
494
|
+
fs3.copyFileSync(configPath, bakPath);
|
|
495
|
+
fs3.writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
|
|
496
|
+
ok("Migrated legacy `harness` config into plugin options");
|
|
497
|
+
info(`Backup: ${bakPath}`);
|
|
477
498
|
} catch {
|
|
478
|
-
process.stderr.write(
|
|
479
|
-
`[pilot] Warning: .glrs/pilot.json has invalid JSON \u2014 using defaults
|
|
480
|
-
`
|
|
481
|
-
);
|
|
482
|
-
return { ...DEFAULT_CONFIG };
|
|
483
|
-
}
|
|
484
|
-
if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
|
|
485
|
-
return { ...DEFAULT_CONFIG };
|
|
486
|
-
}
|
|
487
|
-
const obj = raw;
|
|
488
|
-
if ("baseline" in obj || "after_each" in obj) {
|
|
489
|
-
process.stderr.write(
|
|
490
|
-
`[pilot] Warning: .glrs/pilot.json appears to be in the old pilot v1 format.
|
|
491
|
-
Run \`pilot configure\` to set up the new v2 configuration.
|
|
492
|
-
Using defaults for now.
|
|
493
|
-
`
|
|
494
|
-
);
|
|
495
|
-
return { ...DEFAULT_CONFIG };
|
|
496
499
|
}
|
|
497
|
-
const models = mergeModels(obj["models"]);
|
|
498
|
-
const verify = mergeVerify(obj["verify"]);
|
|
499
|
-
const playwright = mergePlaywright(obj["playwright"]);
|
|
500
|
-
const max_assess_cycles = typeof obj["max_assess_cycles"] === "number" && obj["max_assess_cycles"] > 0 ? obj["max_assess_cycles"] : DEFAULT_CONFIG.max_assess_cycles;
|
|
501
|
-
return { models, verify, max_assess_cycles, playwright };
|
|
502
|
-
}
|
|
503
|
-
function mergeModels(raw) {
|
|
504
|
-
const d = DEFAULT_CONFIG.models;
|
|
505
|
-
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return { ...d };
|
|
506
|
-
const obj = raw;
|
|
507
|
-
return {
|
|
508
|
-
scope: typeof obj["scope"] === "string" ? obj["scope"] : d.scope,
|
|
509
|
-
plan: typeof obj["plan"] === "string" ? obj["plan"] : d.plan,
|
|
510
|
-
execute: typeof obj["execute"] === "string" ? obj["execute"] : d.execute,
|
|
511
|
-
assess: typeof obj["assess"] === "string" ? obj["assess"] : d.assess
|
|
512
|
-
};
|
|
513
|
-
}
|
|
514
|
-
function mergeVerify(raw) {
|
|
515
|
-
const d = DEFAULT_CONFIG.verify;
|
|
516
|
-
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return { ...d };
|
|
517
|
-
const obj = raw;
|
|
518
|
-
return {
|
|
519
|
-
baseline: Array.isArray(obj["baseline"]) ? obj["baseline"].filter((x) => typeof x === "string") : d.baseline,
|
|
520
|
-
after_each: Array.isArray(obj["after_each"]) ? obj["after_each"].filter((x) => typeof x === "string") : d.after_each
|
|
521
|
-
};
|
|
522
500
|
}
|
|
523
|
-
function
|
|
524
|
-
|
|
525
|
-
if (
|
|
526
|
-
|
|
527
|
-
return
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
const
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
fs4.mkdirSync(dir, { recursive: true });
|
|
501
|
+
function deepEqual(a, b) {
|
|
502
|
+
if (a === b) return true;
|
|
503
|
+
if (typeof a !== typeof b) return false;
|
|
504
|
+
if (a === null || b === null) return a === b;
|
|
505
|
+
if (typeof a !== "object") return false;
|
|
506
|
+
const aObj = a;
|
|
507
|
+
const bObj = b;
|
|
508
|
+
const aKeys = Object.keys(aObj);
|
|
509
|
+
const bKeys = Object.keys(bObj);
|
|
510
|
+
if (aKeys.length !== bKeys.length) return false;
|
|
511
|
+
for (const key of aKeys) {
|
|
512
|
+
if (!bKeys.includes(key)) return false;
|
|
513
|
+
if (!deepEqual(aObj[key], bObj[key])) return false;
|
|
537
514
|
}
|
|
538
|
-
|
|
539
|
-
}
|
|
540
|
-
|
|
541
|
-
// src/pilot/cli/configure.ts
|
|
542
|
-
var MODEL_SUGGESTIONS = [
|
|
543
|
-
// Anthropic
|
|
544
|
-
"anthropic/claude-opus-4-7",
|
|
545
|
-
"anthropic/claude-sonnet-4-6",
|
|
546
|
-
"anthropic/claude-haiku-4-5",
|
|
547
|
-
// Amazon Bedrock
|
|
548
|
-
"amazon-bedrock/global.anthropic.claude-opus-4-7",
|
|
549
|
-
"amazon-bedrock/global.anthropic.claude-sonnet-4-6",
|
|
550
|
-
"amazon-bedrock/global.anthropic.claude-haiku-4-5",
|
|
551
|
-
// OpenAI
|
|
552
|
-
"openai/gpt-4o",
|
|
553
|
-
"openai/gpt-4o-mini",
|
|
554
|
-
"openai/o3",
|
|
555
|
-
"openai/o4-mini",
|
|
556
|
-
// Google
|
|
557
|
-
"google/gemini-2.5-pro",
|
|
558
|
-
"google/gemini-2.5-flash",
|
|
559
|
-
// DeepSeek
|
|
560
|
-
"deepseek/deepseek-chat",
|
|
561
|
-
// Qwen
|
|
562
|
-
"qwen/qwen3-coder"
|
|
563
|
-
];
|
|
564
|
-
async function promptModel(phase, current) {
|
|
565
|
-
const choices = MODEL_SUGGESTIONS.includes(current) ? MODEL_SUGGESTIONS.map((m) => ({ name: m, value: m })) : [
|
|
566
|
-
{ name: `${current} (current)`, value: current },
|
|
567
|
-
...MODEL_SUGGESTIONS.map((m) => ({ name: m, value: m }))
|
|
568
|
-
];
|
|
569
|
-
return select({
|
|
570
|
-
message: `Model for ${phase} phase:`,
|
|
571
|
-
choices,
|
|
572
|
-
default: current
|
|
573
|
-
});
|
|
515
|
+
return true;
|
|
574
516
|
}
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
}
|
|
584
|
-
var configureCmd = command({
|
|
585
|
-
name: "configure",
|
|
586
|
-
description: "Interactively configure pilot v2 for this repo (.glrs/pilot.json).",
|
|
587
|
-
args: {},
|
|
588
|
-
handler: async () => {
|
|
589
|
-
const cwd = process.cwd();
|
|
590
|
-
if (!process.stdin.isTTY) {
|
|
591
|
-
process.stderr.write(
|
|
592
|
-
"pilot configure: requires an interactive terminal (TTY).\n Edit .glrs/pilot.json directly for non-interactive configuration.\n"
|
|
593
|
-
);
|
|
594
|
-
process.exit(1);
|
|
517
|
+
function writePluginOption(configPath, subKey, value, opts) {
|
|
518
|
+
try {
|
|
519
|
+
if (!fs3.existsSync(configPath)) {
|
|
520
|
+
return { changed: false };
|
|
521
|
+
}
|
|
522
|
+
const raw = fs3.readFileSync(configPath, "utf8");
|
|
523
|
+
const config = JSON.parse(raw);
|
|
524
|
+
if (!Array.isArray(config.plugin)) {
|
|
525
|
+
return { changed: false };
|
|
595
526
|
}
|
|
596
|
-
const
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
console.log("\x1B[2m\u2500\u2500 Models \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\x1B[0m");
|
|
600
|
-
const scopeModel = await promptModel("scope", current.models.scope);
|
|
601
|
-
const planModel = await promptModel("plan", current.models.plan);
|
|
602
|
-
const executeModel = await promptModel("execute", current.models.execute);
|
|
603
|
-
const assessModel = await promptModel("assess", current.models.assess);
|
|
604
|
-
console.log("\n\x1B[2m\u2500\u2500 Verify commands \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\x1B[0m");
|
|
605
|
-
const baseline = await promptVerifyCommands("Baseline (run before execution)", current.verify.baseline);
|
|
606
|
-
const after_each = await promptVerifyCommands("After-each (run after each task)", current.verify.after_each);
|
|
607
|
-
console.log("\n\x1B[2m\u2500\u2500 Assess loop \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\x1B[0m");
|
|
608
|
-
const max_assess_cycles = await number({
|
|
609
|
-
message: "Max assess cycles (how many times to re-plan on failure):",
|
|
610
|
-
default: current.max_assess_cycles,
|
|
611
|
-
min: 1,
|
|
612
|
-
max: 10
|
|
613
|
-
}) ?? current.max_assess_cycles;
|
|
614
|
-
console.log("\n\x1B[2m\u2500\u2500 Playwright (optional visual testing) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\x1B[0m");
|
|
615
|
-
const playwrightEnabled = await confirm({
|
|
616
|
-
message: "Enable Playwright MCP for visual verification in Assess?",
|
|
617
|
-
default: current.playwright.enabled
|
|
527
|
+
const pluginIdx = config.plugin.findIndex((entry) => {
|
|
528
|
+
const name = typeof entry === "string" ? entry : Array.isArray(entry) ? entry[0] : null;
|
|
529
|
+
return name === PLUGIN_NAME || String(name ?? "").startsWith(`${PLUGIN_NAME}@`);
|
|
618
530
|
});
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
playwrightBaseUrl = await input({
|
|
622
|
-
message: "Playwright base URL:",
|
|
623
|
-
default: current.playwright.base_url
|
|
624
|
-
});
|
|
531
|
+
if (pluginIdx < 0) {
|
|
532
|
+
return { changed: false };
|
|
625
533
|
}
|
|
626
|
-
const
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
assess: assessModel
|
|
632
|
-
},
|
|
633
|
-
verify: { baseline, after_each },
|
|
634
|
-
max_assess_cycles,
|
|
635
|
-
playwright: { enabled: playwrightEnabled, base_url: playwrightBaseUrl }
|
|
636
|
-
};
|
|
637
|
-
writePilotConfig(cwd, config);
|
|
638
|
-
console.log("\n\x1B[32m\u2713\x1B[0m Configuration saved to .glrs/pilot.json");
|
|
639
|
-
console.log(' Run \x1B[1mpilot scope "<goal>"\x1B[0m to start a new workflow.\n');
|
|
640
|
-
process.exit(0);
|
|
641
|
-
}
|
|
642
|
-
});
|
|
643
|
-
|
|
644
|
-
// src/pilot/cli/scope.ts
|
|
645
|
-
import { command as command2, restPositionals, string } from "cmd-ts";
|
|
646
|
-
import { input as input2 } from "@inquirer/prompts";
|
|
647
|
-
|
|
648
|
-
// src/pilot/scope.ts
|
|
649
|
-
import * as fs5 from "fs";
|
|
650
|
-
|
|
651
|
-
// src/pilot/state.ts
|
|
652
|
-
import { Database } from "bun:sqlite";
|
|
653
|
-
import { ulid } from "ulid";
|
|
654
|
-
var SCHEMA_SQL = `
|
|
655
|
-
CREATE TABLE IF NOT EXISTS workflows (
|
|
656
|
-
id TEXT NOT NULL PRIMARY KEY,
|
|
657
|
-
goal TEXT NOT NULL,
|
|
658
|
-
scope_path TEXT,
|
|
659
|
-
plan_path TEXT,
|
|
660
|
-
status TEXT NOT NULL CHECK (status IN (
|
|
661
|
-
'pending','scoped','planned','executing','assessing','completed','failed'
|
|
662
|
-
)),
|
|
663
|
-
started_at INTEGER NOT NULL,
|
|
664
|
-
finished_at INTEGER,
|
|
665
|
-
config TEXT
|
|
666
|
-
);
|
|
667
|
-
|
|
668
|
-
CREATE TABLE IF NOT EXISTS events (
|
|
669
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
670
|
-
workflow_id TEXT NOT NULL REFERENCES workflows(id) ON DELETE CASCADE,
|
|
671
|
-
ts INTEGER NOT NULL,
|
|
672
|
-
phase TEXT NOT NULL,
|
|
673
|
-
kind TEXT NOT NULL,
|
|
674
|
-
task_id TEXT,
|
|
675
|
-
payload TEXT NOT NULL,
|
|
676
|
-
session_id TEXT
|
|
677
|
-
);
|
|
678
|
-
|
|
679
|
-
CREATE INDEX IF NOT EXISTS idx_events_workflow ON events(workflow_id, id);
|
|
680
|
-
CREATE INDEX IF NOT EXISTS idx_events_workflow_phase ON events(workflow_id, phase, id);
|
|
681
|
-
`.trim();
|
|
682
|
-
function openStateDb(dbPath) {
|
|
683
|
-
const db = new Database(dbPath, { create: true });
|
|
684
|
-
try {
|
|
685
|
-
db.run("PRAGMA foreign_keys = ON");
|
|
686
|
-
if (dbPath !== ":memory:") {
|
|
687
|
-
db.run("PRAGMA journal_mode = WAL");
|
|
688
|
-
db.run("PRAGMA synchronous = NORMAL");
|
|
534
|
+
const current = config.plugin[pluginIdx];
|
|
535
|
+
const existingName = typeof current === "string" ? current : Array.isArray(current) ? current[0] : PLUGIN_NAME;
|
|
536
|
+
const existingOpts = Array.isArray(current) && current.length >= 2 ? current[1] : {};
|
|
537
|
+
if (deepEqual(existingOpts[subKey], value)) {
|
|
538
|
+
return { changed: false };
|
|
689
539
|
}
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
return { db, close: () => db.close() };
|
|
703
|
-
}
|
|
704
|
-
function createWorkflow(db, opts) {
|
|
705
|
-
const id = ulid();
|
|
706
|
-
const now = opts.now ?? Date.now();
|
|
707
|
-
db.prepare(
|
|
708
|
-
`INSERT INTO workflows (id, goal, status, started_at, config)
|
|
709
|
-
VALUES (?, ?, 'pending', ?, ?)`
|
|
710
|
-
).run(id, opts.goal, now, opts.config ?? null);
|
|
711
|
-
return id;
|
|
712
|
-
}
|
|
713
|
-
function getWorkflow(db, id) {
|
|
714
|
-
return db.prepare(
|
|
715
|
-
`SELECT * FROM workflows WHERE id = ?`
|
|
716
|
-
).get(id);
|
|
717
|
-
}
|
|
718
|
-
function latestWorkflow(db) {
|
|
719
|
-
return db.prepare(
|
|
720
|
-
`SELECT * FROM workflows ORDER BY started_at DESC LIMIT 1`
|
|
721
|
-
).get();
|
|
722
|
-
}
|
|
723
|
-
function updateWorkflowStatus(db, id, status, opts = {}) {
|
|
724
|
-
const now = opts.now ?? Date.now();
|
|
725
|
-
const terminal = status === "completed" || status === "failed";
|
|
726
|
-
db.prepare(
|
|
727
|
-
`UPDATE workflows
|
|
728
|
-
SET status = ?,
|
|
729
|
-
scope_path = COALESCE(?, scope_path),
|
|
730
|
-
plan_path = COALESCE(?, plan_path),
|
|
731
|
-
finished_at = CASE WHEN ? THEN ? ELSE finished_at END
|
|
732
|
-
WHERE id = ?`
|
|
733
|
-
).run(
|
|
734
|
-
status,
|
|
735
|
-
opts.scopePath ?? null,
|
|
736
|
-
opts.planPath ?? null,
|
|
737
|
-
terminal ? 1 : 0,
|
|
738
|
-
terminal ? now : null,
|
|
739
|
-
id
|
|
740
|
-
);
|
|
741
|
-
}
|
|
742
|
-
function appendEvent(db, opts) {
|
|
743
|
-
const ts = opts.now ?? Date.now();
|
|
744
|
-
let payloadStr;
|
|
745
|
-
try {
|
|
746
|
-
payloadStr = JSON.stringify(opts.payload);
|
|
747
|
-
} catch {
|
|
748
|
-
payloadStr = JSON.stringify({ _serializationError: true });
|
|
749
|
-
}
|
|
750
|
-
db.prepare(
|
|
751
|
-
`INSERT INTO events (workflow_id, ts, phase, kind, task_id, payload, session_id)
|
|
752
|
-
VALUES (?, ?, ?, ?, ?, ?, ?)`
|
|
753
|
-
).run(
|
|
754
|
-
opts.workflowId,
|
|
755
|
-
ts,
|
|
756
|
-
opts.phase,
|
|
757
|
-
opts.kind,
|
|
758
|
-
opts.taskId ?? null,
|
|
759
|
-
payloadStr,
|
|
760
|
-
opts.sessionId ?? null
|
|
761
|
-
);
|
|
762
|
-
}
|
|
763
|
-
function readEvents(db, opts) {
|
|
764
|
-
if (opts.phase) {
|
|
765
|
-
return db.prepare(
|
|
766
|
-
`SELECT * FROM events WHERE workflow_id = ? AND phase = ? ORDER BY id LIMIT ?`
|
|
767
|
-
).all(opts.workflowId, opts.phase, opts.limit ?? 1e3);
|
|
768
|
-
}
|
|
769
|
-
return db.prepare(
|
|
770
|
-
`SELECT * FROM events WHERE workflow_id = ? ORDER BY id LIMIT ?`
|
|
771
|
-
).all(opts.workflowId, opts.limit ?? 1e3);
|
|
772
|
-
}
|
|
773
|
-
function logEvent(db, opts) {
|
|
774
|
-
appendEvent(db, opts);
|
|
775
|
-
const indent = " ".repeat(opts.indent ?? 0);
|
|
776
|
-
const kvPairs = Object.entries(opts.payload).map(([k, v]) => {
|
|
777
|
-
const val = typeof v === "string" && v.includes(" ") ? `"${v}"` : String(v);
|
|
778
|
-
return `${k}=${val}`;
|
|
779
|
-
}).join(" ");
|
|
780
|
-
const line = `${indent}[pilot] ${opts.kind.padEnd(32)} ${kvPairs}
|
|
781
|
-
`;
|
|
782
|
-
process.stderr.write(line);
|
|
783
|
-
}
|
|
784
|
-
|
|
785
|
-
// src/pilot/safety.ts
|
|
786
|
-
import { execFile as execFile2 } from "child_process";
|
|
787
|
-
import { promisify } from "util";
|
|
788
|
-
var execFileP2 = promisify(execFile2);
|
|
789
|
-
var PROTECTED_BRANCHES = /* @__PURE__ */ new Set(["main", "master", "develop", "trunk"]);
|
|
790
|
-
async function checkSafety(cwd) {
|
|
791
|
-
try {
|
|
792
|
-
await execFileP2("git", ["rev-parse", "--is-inside-work-tree"], { cwd });
|
|
793
|
-
} catch {
|
|
794
|
-
return { ok: false, reason: "Not inside a git repository." };
|
|
795
|
-
}
|
|
796
|
-
let branch;
|
|
797
|
-
try {
|
|
798
|
-
const { stdout } = await execFileP2("git", ["branch", "--show-current"], { cwd });
|
|
799
|
-
branch = stdout.trim();
|
|
800
|
-
} catch {
|
|
801
|
-
return { ok: false, reason: "Could not determine current branch." };
|
|
802
|
-
}
|
|
803
|
-
if (PROTECTED_BRANCHES.has(branch)) {
|
|
804
|
-
return {
|
|
805
|
-
ok: false,
|
|
806
|
-
reason: `Refusing to run pilot on protected branch "${branch}". Create a feature branch first (e.g. git checkout -b feat/my-feature).`
|
|
807
|
-
};
|
|
808
|
-
}
|
|
809
|
-
let status;
|
|
810
|
-
try {
|
|
811
|
-
const { stdout } = await execFileP2("git", ["status", "--porcelain"], { cwd });
|
|
812
|
-
status = stdout.trim();
|
|
540
|
+
const newOpts = { ...existingOpts, [subKey]: value };
|
|
541
|
+
if (opts.dryRun) {
|
|
542
|
+
info(`[dry-run] Would reconfigure ${subKey} in plugin options`);
|
|
543
|
+
return { changed: true };
|
|
544
|
+
}
|
|
545
|
+
const bakPath = `${configPath}.bak.${Date.now()}-${process.pid}`;
|
|
546
|
+
fs3.copyFileSync(configPath, bakPath);
|
|
547
|
+
config.plugin[pluginIdx] = [existingName, newOpts];
|
|
548
|
+
fs3.writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
|
|
549
|
+
ok(`Reconfigured ${subKey}`);
|
|
550
|
+
info(`Backup: ${bakPath}`);
|
|
551
|
+
return { changed: true, bakPath };
|
|
813
552
|
} catch {
|
|
814
|
-
return {
|
|
815
|
-
}
|
|
816
|
-
if (status.length > 0) {
|
|
817
|
-
const lines = status.split("\n").slice(0, 5);
|
|
818
|
-
const preview = lines.join("\n ");
|
|
819
|
-
return {
|
|
820
|
-
ok: false,
|
|
821
|
-
reason: `Working tree is dirty. Commit or stash changes before running pilot.
|
|
822
|
-
${preview}`
|
|
823
|
-
};
|
|
824
|
-
}
|
|
825
|
-
return { ok: true };
|
|
826
|
-
}
|
|
827
|
-
|
|
828
|
-
// src/pilot/scope.ts
|
|
829
|
-
function parseScopeArtifact(raw) {
|
|
830
|
-
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return null;
|
|
831
|
-
const obj = raw;
|
|
832
|
-
if (typeof obj["workflow_id"] !== "string") return null;
|
|
833
|
-
if (typeof obj["goal"] !== "string") return null;
|
|
834
|
-
if (typeof obj["framing"] !== "string") return null;
|
|
835
|
-
if (!Array.isArray(obj["acceptance_criteria"])) return null;
|
|
836
|
-
const acs = [];
|
|
837
|
-
for (const ac of obj["acceptance_criteria"]) {
|
|
838
|
-
if (!ac || typeof ac !== "object") return null;
|
|
839
|
-
const a = ac;
|
|
840
|
-
if (typeof a["id"] !== "string") return null;
|
|
841
|
-
if (typeof a["description"] !== "string") return null;
|
|
842
|
-
const verifiable = a["verifiable"];
|
|
843
|
-
if (verifiable !== "shell" && verifiable !== "llm" && verifiable !== "manual") return null;
|
|
844
|
-
acs.push({ id: a["id"], description: a["description"], verifiable });
|
|
553
|
+
return { changed: false };
|
|
845
554
|
}
|
|
846
|
-
return {
|
|
847
|
-
workflow_id: obj["workflow_id"],
|
|
848
|
-
goal: obj["goal"],
|
|
849
|
-
framing: obj["framing"],
|
|
850
|
-
acceptance_criteria: acs,
|
|
851
|
-
non_goals: Array.isArray(obj["non_goals"]) ? obj["non_goals"].filter((x) => typeof x === "string") : [],
|
|
852
|
-
context: typeof obj["context"] === "string" ? obj["context"] : void 0
|
|
853
|
-
};
|
|
854
555
|
}
|
|
855
|
-
|
|
856
|
-
const { goal, cwd } = opts;
|
|
857
|
-
const safety = await checkSafety(cwd);
|
|
858
|
-
if (!safety.ok) {
|
|
859
|
-
return { ok: false, reason: safety.reason };
|
|
860
|
-
}
|
|
861
|
-
const config = loadPilotConfig(cwd);
|
|
862
|
-
const dbPath = await getStateDbPath(cwd);
|
|
863
|
-
const { db, close: closeDb } = openStateDb(dbPath);
|
|
864
|
-
const workflowId = createWorkflow(db, {
|
|
865
|
-
goal,
|
|
866
|
-
config: JSON.stringify(config)
|
|
867
|
-
});
|
|
868
|
-
const scopePath = await getScopeArtifactPath(cwd, workflowId);
|
|
869
|
-
logEvent(db, {
|
|
870
|
-
workflowId,
|
|
871
|
-
phase: "scope",
|
|
872
|
-
kind: "workflow.started",
|
|
873
|
-
payload: { id: workflowId, goal }
|
|
874
|
-
});
|
|
875
|
-
logEvent(db, {
|
|
876
|
-
workflowId,
|
|
877
|
-
phase: "scope",
|
|
878
|
-
kind: "task.scope.started",
|
|
879
|
-
payload: { scopePath }
|
|
880
|
-
});
|
|
881
|
-
const scoperPrompt = buildScopePrompt({ goal, scopePath, workflowId });
|
|
882
|
-
logEvent(db, {
|
|
883
|
-
workflowId,
|
|
884
|
-
phase: "scope",
|
|
885
|
-
kind: "task.scope.tui.spawning",
|
|
886
|
-
payload: { agent: "pilot-scoper" }
|
|
887
|
-
});
|
|
888
|
-
closeDb();
|
|
556
|
+
function writeMcpToggles(configPath, enabledSet, opts) {
|
|
889
557
|
try {
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
558
|
+
if (!fs3.existsSync(configPath)) {
|
|
559
|
+
return { changed: false };
|
|
560
|
+
}
|
|
561
|
+
const raw = fs3.readFileSync(configPath, "utf8");
|
|
562
|
+
const config = JSON.parse(raw);
|
|
563
|
+
const toggleNames = new Set(MCP_TOGGLES.map((t) => t.name));
|
|
564
|
+
const existingMcp = config.mcp && typeof config.mcp === "object" ? { ...config.mcp } : {};
|
|
565
|
+
const newMcp = {};
|
|
566
|
+
let hasChanges = false;
|
|
567
|
+
for (const [key, val] of Object.entries(existingMcp)) {
|
|
568
|
+
if (!toggleNames.has(key)) {
|
|
569
|
+
newMcp[key] = val;
|
|
900
570
|
}
|
|
901
|
-
);
|
|
902
|
-
const exitCode = await new Promise((resolve2) => {
|
|
903
|
-
child.on("close", (code) => resolve2(code ?? 1));
|
|
904
|
-
child.on("error", () => resolve2(1));
|
|
905
|
-
});
|
|
906
|
-
if (exitCode !== 0) {
|
|
907
|
-
return {
|
|
908
|
-
ok: false,
|
|
909
|
-
reason: `OpenCode TUI exited with code ${exitCode}. Scope session may have been interrupted.`
|
|
910
|
-
};
|
|
911
571
|
}
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
phase: "scope",
|
|
924
|
-
kind: "task.scope.failed",
|
|
925
|
-
payload: { reason: "scope.json not produced" }
|
|
926
|
-
});
|
|
927
|
-
closeDb2();
|
|
928
|
-
return {
|
|
929
|
-
ok: false,
|
|
930
|
-
reason: `Scoper did not produce scope.json at ${scopePath}. The session may have ended without completing.`
|
|
931
|
-
};
|
|
572
|
+
for (const toggleName of toggleNames) {
|
|
573
|
+
if (enabledSet.has(toggleName)) {
|
|
574
|
+
newMcp[toggleName] = { enabled: true };
|
|
575
|
+
if (!deepEqual(existingMcp[toggleName], { enabled: true })) {
|
|
576
|
+
hasChanges = true;
|
|
577
|
+
}
|
|
578
|
+
} else {
|
|
579
|
+
if (existingMcp[toggleName] !== void 0) {
|
|
580
|
+
hasChanges = true;
|
|
581
|
+
}
|
|
582
|
+
}
|
|
932
583
|
}
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
584
|
+
if (!hasChanges && Object.keys(newMcp).length === Object.keys(existingMcp).length) {
|
|
585
|
+
const allKeysMatch = Object.keys(newMcp).every(
|
|
586
|
+
(k) => deepEqual(newMcp[k], existingMcp[k])
|
|
587
|
+
);
|
|
588
|
+
if (allKeysMatch) {
|
|
589
|
+
return { changed: false };
|
|
590
|
+
}
|
|
940
591
|
}
|
|
941
|
-
if (
|
|
942
|
-
|
|
943
|
-
return {
|
|
592
|
+
if (opts.dryRun) {
|
|
593
|
+
info(`[dry-run] Would reconfigure MCP toggles`);
|
|
594
|
+
return { changed: true };
|
|
944
595
|
}
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
goal: artifact.goal,
|
|
959
|
-
ac_count: artifact.acceptance_criteria.length
|
|
960
|
-
}
|
|
961
|
-
});
|
|
962
|
-
return { ok: true, workflowId, scopePath, artifact };
|
|
963
|
-
} finally {
|
|
964
|
-
closeDb2();
|
|
596
|
+
const bakPath = `${configPath}.bak.${Date.now()}-${process.pid}`;
|
|
597
|
+
fs3.copyFileSync(configPath, bakPath);
|
|
598
|
+
if (Object.keys(newMcp).length > 0) {
|
|
599
|
+
config.mcp = newMcp;
|
|
600
|
+
} else {
|
|
601
|
+
delete config.mcp;
|
|
602
|
+
}
|
|
603
|
+
fs3.writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
|
|
604
|
+
ok("Reconfigured MCPs");
|
|
605
|
+
info(`Backup: ${bakPath}`);
|
|
606
|
+
return { changed: true, bakPath };
|
|
607
|
+
} catch {
|
|
608
|
+
return { changed: false };
|
|
965
609
|
}
|
|
966
610
|
}
|
|
967
|
-
function
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
{
|
|
980
|
-
|
|
981
|
-
"goal": "one sentence",
|
|
982
|
-
"framing": "2-4 sentences: why this matters, what success looks like",
|
|
983
|
-
"acceptance_criteria": [
|
|
984
|
-
{ "id": "AC-001", "description": "behavioral, verifiable statement", "verifiable": "shell|llm|manual" }
|
|
985
|
-
],
|
|
986
|
-
"non_goals": ["what we are NOT doing"],
|
|
987
|
-
"context": "optional: key patterns, constraints, background for the planner"
|
|
988
|
-
}
|
|
989
|
-
|
|
990
|
-
Start by asking the user to tell you more about their goal. Then explore the codebase. Then draft acceptance criteria and confirm with the user before writing scope.json.`;
|
|
991
|
-
}
|
|
992
|
-
|
|
993
|
-
// src/pilot/cli/scope.ts
|
|
994
|
-
var scopeCmd = command2({
|
|
995
|
-
name: "scope",
|
|
996
|
-
description: "Start a new pilot workflow with interactive scoping. Produces scope.json for `pilot go`.",
|
|
997
|
-
args: {
|
|
998
|
-
goalWords: restPositionals({
|
|
999
|
-
type: string,
|
|
1000
|
-
displayName: "goal",
|
|
1001
|
-
description: "What you want to build (optional \u2014 will prompt if not provided)"
|
|
1002
|
-
})
|
|
1003
|
-
},
|
|
1004
|
-
handler: async ({ goalWords }) => {
|
|
1005
|
-
const cwd = process.cwd();
|
|
1006
|
-
let goal = goalWords.join(" ").trim();
|
|
1007
|
-
if (!goal) {
|
|
1008
|
-
if (!process.stdin.isTTY) {
|
|
1009
|
-
process.stderr.write("pilot scope: no goal provided and not running in a TTY.\n");
|
|
1010
|
-
process.stderr.write(' Usage: pilot scope "<what you want to build>"\n');
|
|
1011
|
-
process.exit(1);
|
|
611
|
+
function writePluginToggles(configPath, enabledSet, opts) {
|
|
612
|
+
try {
|
|
613
|
+
if (!fs3.existsSync(configPath)) {
|
|
614
|
+
return { changed: false };
|
|
615
|
+
}
|
|
616
|
+
const raw = fs3.readFileSync(configPath, "utf8");
|
|
617
|
+
const config = JSON.parse(raw);
|
|
618
|
+
const toggleNames = new Set(PLUGIN_TOGGLES.map((t) => t.name));
|
|
619
|
+
const existingPlugins = Array.isArray(config.plugin) ? config.plugin : [];
|
|
620
|
+
const currentlyPresent = /* @__PURE__ */ new Set();
|
|
621
|
+
for (const entry of existingPlugins) {
|
|
622
|
+
const name = typeof entry === "string" ? entry : Array.isArray(entry) ? entry[0] : null;
|
|
623
|
+
if (typeof name === "string" && toggleNames.has(name)) {
|
|
624
|
+
currentlyPresent.add(name);
|
|
1012
625
|
}
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
626
|
+
}
|
|
627
|
+
const toAdd = [];
|
|
628
|
+
const toRemove = /* @__PURE__ */ new Set();
|
|
629
|
+
for (const toggleName of toggleNames) {
|
|
630
|
+
if (enabledSet.has(toggleName) && !currentlyPresent.has(toggleName)) {
|
|
631
|
+
toAdd.push(toggleName);
|
|
632
|
+
} else if (!enabledSet.has(toggleName) && currentlyPresent.has(toggleName)) {
|
|
633
|
+
toRemove.add(toggleName);
|
|
1019
634
|
}
|
|
1020
635
|
}
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
console.log(`Goal: ${goal}
|
|
1024
|
-
`);
|
|
1025
|
-
console.log("Starting interactive scoping session...");
|
|
1026
|
-
console.log("The scoper will interview you and explore the codebase.");
|
|
1027
|
-
console.log("When done, it will produce scope.json for `pilot go`.\n");
|
|
1028
|
-
const result = await runScopePhase({ goal, cwd });
|
|
1029
|
-
if (!result.ok) {
|
|
1030
|
-
process.stderr.write(`
|
|
1031
|
-
\x1B[31m\u2717\x1B[0m Scope phase failed: ${result.reason}
|
|
1032
|
-
`);
|
|
1033
|
-
process.exit(1);
|
|
636
|
+
if (toAdd.length === 0 && toRemove.size === 0) {
|
|
637
|
+
return { changed: false };
|
|
1034
638
|
}
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
// src/pilot/server.ts
|
|
1055
|
-
import { execFile as execFile3 } from "child_process";
|
|
1056
|
-
import { promisify as promisify2 } from "util";
|
|
1057
|
-
import {
|
|
1058
|
-
createOpencodeServer,
|
|
1059
|
-
createOpencodeClient
|
|
1060
|
-
} from "@opencode-ai/sdk";
|
|
1061
|
-
var execFileP3 = promisify2(execFile3);
|
|
1062
|
-
var DEFAULT_STARTUP_TIMEOUT_MS = 3e4;
|
|
1063
|
-
async function ensureOpencodeOnPath() {
|
|
1064
|
-
try {
|
|
1065
|
-
await execFileP3("opencode", ["--version"]);
|
|
639
|
+
if (opts.dryRun) {
|
|
640
|
+
info(`[dry-run] Would reconfigure plugin toggles`);
|
|
641
|
+
return { changed: true };
|
|
642
|
+
}
|
|
643
|
+
const bakPath = `${configPath}.bak.${Date.now()}-${process.pid}`;
|
|
644
|
+
fs3.copyFileSync(configPath, bakPath);
|
|
645
|
+
const newPlugins = existingPlugins.filter((entry) => {
|
|
646
|
+
const name = typeof entry === "string" ? entry : Array.isArray(entry) ? entry[0] : null;
|
|
647
|
+
return !(typeof name === "string" && toRemove.has(name));
|
|
648
|
+
});
|
|
649
|
+
for (const name of toAdd) {
|
|
650
|
+
newPlugins.push(name);
|
|
651
|
+
}
|
|
652
|
+
config.plugin = newPlugins;
|
|
653
|
+
fs3.writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
|
|
654
|
+
ok("Reconfigured plugin add-ons");
|
|
655
|
+
info(`Backup: ${bakPath}`);
|
|
656
|
+
return { changed: true, bakPath };
|
|
1066
657
|
} catch {
|
|
1067
|
-
|
|
1068
|
-
"opencode CLI not found on PATH.\n Install: https://opencode.ai\n Or: bunx opencode upgrade"
|
|
1069
|
-
);
|
|
658
|
+
return { changed: false };
|
|
1070
659
|
}
|
|
1071
660
|
}
|
|
1072
|
-
async function
|
|
1073
|
-
|
|
1074
|
-
const
|
|
1075
|
-
const
|
|
1076
|
-
const
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
const shutdown = async () => {
|
|
1083
|
-
if (shutdownCalled) return;
|
|
1084
|
-
shutdownCalled = true;
|
|
1085
|
-
try {
|
|
1086
|
-
await server.close();
|
|
1087
|
-
} catch {
|
|
661
|
+
async function install(opts = {}) {
|
|
662
|
+
const { dryRun = false, pin = false, nonInteractive = false } = opts;
|
|
663
|
+
const configPath = getOpencodeConfigPath();
|
|
664
|
+
const pluginEntry = pin ? `${PLUGIN_NAME}@${readPackageVersion()}` : PLUGIN_NAME;
|
|
665
|
+
const interactive = !nonInteractive && process.stdin.isTTY === true;
|
|
666
|
+
const existing = readExistingConfig(configPath);
|
|
667
|
+
const hasPlugin = existing ? (Array.isArray(existing.plugin) ? existing.plugin : []).some(
|
|
668
|
+
(p) => {
|
|
669
|
+
const name = typeof p === "string" ? p : Array.isArray(p) ? p[0] : null;
|
|
670
|
+
return name === PLUGIN_NAME || String(name ?? "").startsWith(`${PLUGIN_NAME}@`);
|
|
1088
671
|
}
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
672
|
+
) : false;
|
|
673
|
+
const existingProvider = detectModelProvider(existing);
|
|
674
|
+
const existingMcps = detectEnabledMcps(existing);
|
|
675
|
+
const existingPluginToggles = detectEnabledPluginToggles(existing);
|
|
676
|
+
const existingOpts = extractPluginOptions(existing);
|
|
677
|
+
let hasModels = !!(existingOpts?.models ?? existing?.harness?.models);
|
|
678
|
+
console.log(`
|
|
679
|
+
${c.bold}${c.blue}@glrs-dev/harness-plugin-opencode${c.reset} setup
|
|
680
|
+
`);
|
|
681
|
+
if (hasPlugin) {
|
|
682
|
+
ok("Plugin already registered");
|
|
683
|
+
}
|
|
684
|
+
if (existingProvider) {
|
|
685
|
+
ok(`Models: ${existingProvider}`);
|
|
686
|
+
}
|
|
687
|
+
if (existingMcps.size > 0) {
|
|
688
|
+
ok(`MCPs: ${[...existingMcps].join(", ")} enabled`);
|
|
689
|
+
}
|
|
690
|
+
let reconfigureModels = false;
|
|
691
|
+
let reconfigureMcps = false;
|
|
692
|
+
let reconfigurePluginToggles = false;
|
|
693
|
+
let newModelsValue = null;
|
|
694
|
+
let newMcpEnabledSet = /* @__PURE__ */ new Set();
|
|
695
|
+
let newPluginToggleEnabledSet = new Set(existingPluginToggles);
|
|
696
|
+
if (hasPlugin && (existingProvider || hasModels)) {
|
|
697
|
+
const unconfiguredMcps = MCP_TOGGLES.filter(
|
|
698
|
+
(t) => !existingMcps.has(t.name) && !existing?.mcp?.[t.name]
|
|
1100
699
|
);
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
return session.id;
|
|
1111
|
-
}
|
|
1112
|
-
async function sendAndWait(client, opts) {
|
|
1113
|
-
const stallMs = opts.stallMs ?? 60 * 60 * 1e3;
|
|
1114
|
-
await client.session.chat({
|
|
1115
|
-
sessionID: opts.sessionId,
|
|
1116
|
-
body: { content: [{ type: "text", text: opts.message }] }
|
|
1117
|
-
});
|
|
1118
|
-
return waitForIdle(client, {
|
|
1119
|
-
sessionId: opts.sessionId,
|
|
1120
|
-
stallMs,
|
|
1121
|
-
abortSignal: opts.abortSignal
|
|
1122
|
-
});
|
|
1123
|
-
}
|
|
1124
|
-
async function waitForIdle(client, opts) {
|
|
1125
|
-
const stallMs = opts.stallMs ?? 60 * 60 * 1e3;
|
|
1126
|
-
return new Promise((resolve2) => {
|
|
1127
|
-
let stallTimer = null;
|
|
1128
|
-
let unsubscribe = null;
|
|
1129
|
-
let settled = false;
|
|
1130
|
-
const settle = (result) => {
|
|
1131
|
-
if (settled) return;
|
|
1132
|
-
settled = true;
|
|
1133
|
-
if (stallTimer) clearTimeout(stallTimer);
|
|
1134
|
-
if (unsubscribe) unsubscribe();
|
|
1135
|
-
resolve2(result);
|
|
1136
|
-
};
|
|
1137
|
-
const resetStall = () => {
|
|
1138
|
-
if (stallTimer) clearTimeout(stallTimer);
|
|
1139
|
-
stallTimer = setTimeout(() => settle({ kind: "stall", stallMs }), stallMs);
|
|
1140
|
-
};
|
|
1141
|
-
if (opts.abortSignal) {
|
|
1142
|
-
if (opts.abortSignal.aborted) {
|
|
1143
|
-
settle({ kind: "abort" });
|
|
1144
|
-
return;
|
|
700
|
+
if (interactive) {
|
|
701
|
+
const reconfigure = await promptChoice(
|
|
702
|
+
" Reconfigure models?",
|
|
703
|
+
["No, keep current config", "Yes, reconfigure models"],
|
|
704
|
+
0
|
|
705
|
+
);
|
|
706
|
+
if (reconfigure === 1) {
|
|
707
|
+
reconfigureModels = true;
|
|
708
|
+
hasModels = false;
|
|
1145
709
|
}
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
if (settled) break;
|
|
1155
|
-
const props = event.properties ?? {};
|
|
1156
|
-
const eventSessionId = props["sessionID"];
|
|
1157
|
-
if (eventSessionId !== opts.sessionId) continue;
|
|
1158
|
-
resetStall();
|
|
1159
|
-
const type = event.type ?? "";
|
|
1160
|
-
if (type === "session.idle") {
|
|
1161
|
-
settle({ kind: "idle" });
|
|
1162
|
-
break;
|
|
1163
|
-
}
|
|
1164
|
-
if (type === "session.error") {
|
|
1165
|
-
const msg = props["message"] ?? "session error";
|
|
1166
|
-
settle({ kind: "error", message: msg });
|
|
1167
|
-
break;
|
|
1168
|
-
}
|
|
1169
|
-
}
|
|
1170
|
-
} catch (err) {
|
|
1171
|
-
if (!settled) {
|
|
1172
|
-
settle({ kind: "error", message: err instanceof Error ? err.message : String(err) });
|
|
710
|
+
if (existingMcps.size > 0) {
|
|
711
|
+
const reconfigureMcpChoice = await promptChoice(
|
|
712
|
+
" Reconfigure MCPs?",
|
|
713
|
+
["No, keep current config", "Yes, reconfigure MCPs"],
|
|
714
|
+
0
|
|
715
|
+
);
|
|
716
|
+
if (reconfigureMcpChoice === 1) {
|
|
717
|
+
reconfigureMcps = true;
|
|
1173
718
|
}
|
|
1174
|
-
} finally {
|
|
1175
|
-
streamDone = true;
|
|
1176
719
|
}
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
tasks.push({
|
|
1198
|
-
id: task["id"],
|
|
1199
|
-
title: task["title"],
|
|
1200
|
-
prompt: task["prompt"],
|
|
1201
|
-
addresses: Array.isArray(task["addresses"]) ? task["addresses"].filter((x) => typeof x === "string") : [],
|
|
1202
|
-
verify: Array.isArray(task["verify"]) ? task["verify"].filter((x) => typeof x === "string") : []
|
|
1203
|
-
});
|
|
720
|
+
const reconfigurePluginToggleChoice = await promptChoice(
|
|
721
|
+
" Reconfigure plugin add-ons?",
|
|
722
|
+
["No, keep current config", "Yes, reconfigure plugin add-ons"],
|
|
723
|
+
0
|
|
724
|
+
);
|
|
725
|
+
if (reconfigurePluginToggleChoice === 1) {
|
|
726
|
+
reconfigurePluginToggles = true;
|
|
727
|
+
}
|
|
728
|
+
if (!reconfigureModels && !reconfigureMcps && !reconfigurePluginToggles && unconfiguredMcps.length === 0) {
|
|
729
|
+
console.log(`
|
|
730
|
+
${c.bold}Ready.${c.reset} Run ${c.green}opencode${c.reset} to start.
|
|
731
|
+
`);
|
|
732
|
+
return;
|
|
733
|
+
}
|
|
734
|
+
} else if (unconfiguredMcps.length === 0) {
|
|
735
|
+
console.log(`
|
|
736
|
+
${c.bold}Ready.${c.reset} Run ${c.green}opencode${c.reset} to start.
|
|
737
|
+
`);
|
|
738
|
+
return;
|
|
739
|
+
}
|
|
1204
740
|
}
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
741
|
+
const pluginOpts = {};
|
|
742
|
+
if (interactive && !hasModels) {
|
|
743
|
+
console.log();
|
|
744
|
+
console.log(`${c.dim}Models${c.reset}`);
|
|
745
|
+
info("Fetching available providers\u2026");
|
|
746
|
+
const modelsDevProviders = await fetchModelsDevProviders();
|
|
747
|
+
let preset = null;
|
|
748
|
+
if (modelsDevProviders && modelsDevProviders.length > 0) {
|
|
749
|
+
const providerChoices = modelsDevProviders.map((p) => p.name);
|
|
750
|
+
providerChoices.push("Keep defaults (no model config)");
|
|
751
|
+
providerChoices.push("Custom (enter model IDs manually)");
|
|
752
|
+
const keepDefaultsIdx = providerChoices.length - 2;
|
|
753
|
+
const providerIdx = await promptChoice(
|
|
754
|
+
" Which model provider?",
|
|
755
|
+
providerChoices,
|
|
756
|
+
keepDefaultsIdx
|
|
757
|
+
);
|
|
758
|
+
if (providerIdx < modelsDevProviders.length) {
|
|
759
|
+
const provider = modelsDevProviders[providerIdx];
|
|
760
|
+
ok(`Provider: ${provider.name}`);
|
|
761
|
+
const suggested = provider.id === "amazon-bedrock" ? pickBedrockTierIds(provider) : suggestTiersFromModelsDev(provider);
|
|
762
|
+
const modelChoices = Object.keys(provider.models).map(
|
|
763
|
+
(modelId) => `${provider.id}/${modelId}`
|
|
764
|
+
);
|
|
765
|
+
const tiers = [
|
|
766
|
+
{ tier: "deep", suggested: suggested.deep },
|
|
767
|
+
{ tier: "mid", suggested: suggested.mid },
|
|
768
|
+
{ tier: "fast", suggested: suggested.fast }
|
|
769
|
+
];
|
|
770
|
+
const picked = {};
|
|
771
|
+
for (const { tier, suggested: suggestedModel } of tiers) {
|
|
772
|
+
const defaultIdx = modelChoices.indexOf(suggestedModel);
|
|
773
|
+
const idx = await promptChoice(
|
|
774
|
+
` ${tier} model?`,
|
|
775
|
+
modelChoices,
|
|
776
|
+
defaultIdx >= 0 ? defaultIdx : 0
|
|
777
|
+
);
|
|
778
|
+
picked[tier] = modelChoices[idx];
|
|
779
|
+
info(` ${tier} \u2192 ${picked[tier]}`);
|
|
780
|
+
}
|
|
781
|
+
preset = {
|
|
782
|
+
label: provider.name,
|
|
783
|
+
providerId: provider.id,
|
|
784
|
+
deep: picked["deep"],
|
|
785
|
+
mid: picked["mid"],
|
|
786
|
+
fast: picked["fast"]
|
|
787
|
+
};
|
|
788
|
+
} else if (providerIdx === modelsDevProviders.length) {
|
|
789
|
+
ok("Models: OpenCode defaults");
|
|
790
|
+
pluginOpts._skipModels = true;
|
|
791
|
+
}
|
|
792
|
+
} else {
|
|
793
|
+
warn("Could not reach Models.dev API \u2014 using built-in presets");
|
|
794
|
+
const presetLabels = [...MODEL_PRESETS.map((p) => p.label), "Keep defaults (no model config)", "Custom (enter model IDs manually)"];
|
|
795
|
+
const keepDefaultsOfflineIdx = presetLabels.length - 2;
|
|
796
|
+
const choice = await promptChoice(
|
|
797
|
+
" Which model provider?",
|
|
798
|
+
presetLabels,
|
|
799
|
+
keepDefaultsOfflineIdx
|
|
800
|
+
);
|
|
801
|
+
if (choice < MODEL_PRESETS.length) {
|
|
802
|
+
preset = MODEL_PRESETS[choice];
|
|
803
|
+
ok(`Provider: ${preset.label}`);
|
|
804
|
+
} else if (choice === MODEL_PRESETS.length) {
|
|
805
|
+
ok("Models: OpenCode defaults");
|
|
806
|
+
pluginOpts._skipModels = true;
|
|
807
|
+
}
|
|
1246
808
|
}
|
|
1247
|
-
if (
|
|
1248
|
-
|
|
809
|
+
if (preset) {
|
|
810
|
+
pluginOpts.models = {
|
|
811
|
+
deep: [preset.deep],
|
|
812
|
+
mid: [preset.mid],
|
|
813
|
+
fast: [preset.fast]
|
|
814
|
+
};
|
|
815
|
+
newModelsValue = {
|
|
816
|
+
deep: [preset.deep],
|
|
817
|
+
mid: [preset.mid],
|
|
818
|
+
fast: [preset.fast]
|
|
819
|
+
};
|
|
820
|
+
ok(`Models configured`);
|
|
821
|
+
const midExecIdx = await promptChoice(
|
|
822
|
+
" Use a strict executor for build agents? (recommended for Kimi/Qwen/DeepSeek)",
|
|
823
|
+
["No (use mid model as reasoning builder)", "Yes (configure mid-execute model)"],
|
|
824
|
+
0
|
|
825
|
+
);
|
|
826
|
+
if (midExecIdx === 1) {
|
|
827
|
+
const { input } = await import("@inquirer/prompts");
|
|
828
|
+
const midExecModel = await input({
|
|
829
|
+
message: " mid-execute model ID:",
|
|
830
|
+
default: preset.mid
|
|
831
|
+
});
|
|
832
|
+
if (midExecModel) {
|
|
833
|
+
pluginOpts.models["mid-execute"] = [midExecModel];
|
|
834
|
+
newModelsValue["mid-execute"] = [midExecModel];
|
|
835
|
+
info(` mid-execute \u2192 ${midExecModel} (strict executor prompts)`);
|
|
836
|
+
}
|
|
837
|
+
} else {
|
|
838
|
+
info(` mid-execute: skipped (build agents use mid model with reasoning prompts)`);
|
|
839
|
+
}
|
|
840
|
+
} else if (!pluginOpts._skipModels) {
|
|
841
|
+
info("Enter model IDs in <provider>/<model-id> format (e.g. amazon-bedrock/global.anthropic.claude-opus-4-7)");
|
|
842
|
+
const { input } = await import("@inquirer/prompts");
|
|
843
|
+
const deepModel = await input({ message: " deep (most capable):" });
|
|
844
|
+
const midModel = await input({ message: " mid (balanced):" });
|
|
845
|
+
const fastModel = await input({ message: " fast (cheapest):" });
|
|
846
|
+
if (deepModel) {
|
|
847
|
+
const resolvedMid = midModel || deepModel;
|
|
848
|
+
pluginOpts.models = {
|
|
849
|
+
deep: [deepModel],
|
|
850
|
+
mid: [resolvedMid],
|
|
851
|
+
fast: [fastModel || midModel || deepModel]
|
|
852
|
+
};
|
|
853
|
+
newModelsValue = {
|
|
854
|
+
deep: [deepModel],
|
|
855
|
+
mid: [resolvedMid],
|
|
856
|
+
fast: [fastModel || midModel || deepModel]
|
|
857
|
+
};
|
|
858
|
+
ok("Models: custom");
|
|
859
|
+
const midExecModel = await input({ message: " mid-execute (optional strict executor, press Enter to skip):" });
|
|
860
|
+
if (midExecModel) {
|
|
861
|
+
pluginOpts.models["mid-execute"] = [midExecModel];
|
|
862
|
+
newModelsValue["mid-execute"] = [midExecModel];
|
|
863
|
+
info(` mid-execute \u2192 ${midExecModel} (strict executor prompts)`);
|
|
864
|
+
} else {
|
|
865
|
+
info(` mid-execute: skipped (build agents use mid model with reasoning prompts)`);
|
|
866
|
+
}
|
|
867
|
+
} else {
|
|
868
|
+
ok("Models: OpenCode defaults");
|
|
869
|
+
}
|
|
1249
870
|
}
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
}
|
|
1255
|
-
|
|
871
|
+
delete pluginOpts._skipModels;
|
|
872
|
+
console.log();
|
|
873
|
+
}
|
|
874
|
+
if (interactive && reconfigureMcps) {
|
|
875
|
+
console.log(`${c.dim}Reconfigure MCP servers${c.reset}`);
|
|
876
|
+
const currentEnabled = new Set(existingMcps);
|
|
877
|
+
const selected = await promptMulti(
|
|
878
|
+
" Select MCPs to enable:",
|
|
879
|
+
MCP_TOGGLES.map((t) => ({ label: t.label, defaultOn: currentEnabled.has(t.name) }))
|
|
880
|
+
);
|
|
881
|
+
newMcpEnabledSet = new Set([...selected].map((i) => MCP_TOGGLES[i].name));
|
|
882
|
+
const names = [...newMcpEnabledSet].join(", ");
|
|
883
|
+
if (newMcpEnabledSet.size > 0) {
|
|
884
|
+
ok(`MCPs to enable: ${names}`);
|
|
885
|
+
} else {
|
|
886
|
+
ok("MCPs: all disabled");
|
|
1256
887
|
}
|
|
1257
|
-
|
|
1258
|
-
|
|
888
|
+
console.log();
|
|
889
|
+
}
|
|
890
|
+
if (interactive && reconfigurePluginToggles) {
|
|
891
|
+
console.log(`${c.dim}Plugin add-ons${c.reset}`);
|
|
892
|
+
const currentEnabled = new Set(existingPluginToggles);
|
|
893
|
+
const selected = await promptMulti(
|
|
894
|
+
" Enable plugin add-ons?",
|
|
895
|
+
PLUGIN_TOGGLES.map((t) => ({ label: t.label, defaultOn: currentEnabled.has(t.name) }))
|
|
896
|
+
);
|
|
897
|
+
newPluginToggleEnabledSet = new Set([...selected].map((i) => PLUGIN_TOGGLES[i].name));
|
|
898
|
+
const names = [...newPluginToggleEnabledSet].join(", ");
|
|
899
|
+
if (newPluginToggleEnabledSet.size > 0) {
|
|
900
|
+
ok(`Plugin add-ons enabled: ${names}`);
|
|
901
|
+
} else {
|
|
902
|
+
ok("Plugin add-ons: none");
|
|
1259
903
|
}
|
|
1260
|
-
|
|
1261
|
-
logEvent(db, {
|
|
1262
|
-
workflowId,
|
|
1263
|
-
phase: "plan",
|
|
1264
|
-
kind: "task.plan.completed",
|
|
1265
|
-
payload: { planPath, task_count: artifact.tasks.length },
|
|
1266
|
-
sessionId
|
|
1267
|
-
});
|
|
1268
|
-
return { ok: true, planPath, artifact };
|
|
1269
|
-
} finally {
|
|
1270
|
-
closeDb();
|
|
904
|
+
console.log();
|
|
1271
905
|
}
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
"tasks": [
|
|
1301
|
-
{
|
|
1302
|
-
"id": "TASK-001",
|
|
1303
|
-
"title": "Short title",
|
|
1304
|
-
"prompt": "Detailed self-contained instructions for the builder",
|
|
1305
|
-
"addresses": ["AC-001"],
|
|
1306
|
-
"verify": ["bun test src/specific.test.ts"]
|
|
906
|
+
const pluginValue = Object.keys(pluginOpts).length > 0 ? [pluginEntry, pluginOpts] : pluginEntry;
|
|
907
|
+
const config = {
|
|
908
|
+
$schema: "https://opencode.ai/config.json",
|
|
909
|
+
plugin: [pluginValue]
|
|
910
|
+
};
|
|
911
|
+
if (interactive) {
|
|
912
|
+
const unconfigured = MCP_TOGGLES.filter(
|
|
913
|
+
(t) => !existingMcps.has(t.name) && !existing?.mcp?.[t.name]
|
|
914
|
+
);
|
|
915
|
+
if (unconfigured.length > 0) {
|
|
916
|
+
console.log(`${c.dim}Optional MCP servers (serena, memory, git are always on)${c.reset}`);
|
|
917
|
+
const selected = await promptMulti(
|
|
918
|
+
" Enable additional MCPs?",
|
|
919
|
+
unconfigured.map((t) => ({ label: t.label, defaultOn: t.defaultOn }))
|
|
920
|
+
);
|
|
921
|
+
if (selected.size > 0) {
|
|
922
|
+
const mcp = {};
|
|
923
|
+
for (const idx of selected) {
|
|
924
|
+
const toggle = unconfigured[idx];
|
|
925
|
+
mcp[toggle.name] = { enabled: true };
|
|
926
|
+
}
|
|
927
|
+
config.mcp = mcp;
|
|
928
|
+
const names = [...selected].map((i) => unconfigured[i].name).join(", ");
|
|
929
|
+
ok(`MCPs enabled: ${names}`);
|
|
930
|
+
} else {
|
|
931
|
+
ok("MCPs: defaults only");
|
|
932
|
+
}
|
|
933
|
+
console.log();
|
|
1307
934
|
}
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
updateWorkflowStatus(db, workflowId, "executing");
|
|
1328
|
-
logEvent(db, {
|
|
1329
|
-
workflowId,
|
|
1330
|
-
phase: "execute",
|
|
1331
|
-
kind: "task.execute.phase.started",
|
|
1332
|
-
payload: { task_count: plan.tasks.length }
|
|
1333
|
-
});
|
|
1334
|
-
const taskResults = [];
|
|
1335
|
-
try {
|
|
1336
|
-
for (let i = 0; i < plan.tasks.length; i++) {
|
|
1337
|
-
const task = plan.tasks[i];
|
|
1338
|
-
const taskNum = `${i + 1}/${plan.tasks.length}`;
|
|
1339
|
-
logEvent(db, {
|
|
1340
|
-
workflowId,
|
|
1341
|
-
phase: "execute",
|
|
1342
|
-
kind: "task.execute.started",
|
|
1343
|
-
payload: { task: taskNum, id: task.id, title: task.title },
|
|
1344
|
-
taskId: task.id
|
|
1345
|
-
});
|
|
1346
|
-
const result = await runOneTask({
|
|
1347
|
-
workflowId,
|
|
1348
|
-
task,
|
|
1349
|
-
taskNum,
|
|
1350
|
-
scope,
|
|
1351
|
-
cwd,
|
|
1352
|
-
server,
|
|
1353
|
-
db
|
|
1354
|
-
});
|
|
1355
|
-
taskResults.push(result);
|
|
1356
|
-
if (!result.ok) {
|
|
1357
|
-
logEvent(db, {
|
|
1358
|
-
workflowId,
|
|
1359
|
-
phase: "execute",
|
|
1360
|
-
kind: "task.execute.phase.failed",
|
|
1361
|
-
payload: { failed_task: task.id, reason: result.reason },
|
|
1362
|
-
taskId: task.id
|
|
1363
|
-
});
|
|
1364
|
-
return { ok: false, reason: `Task ${task.id} failed: ${result.reason}`, taskResults };
|
|
935
|
+
const unconfiguredPluginToggles = PLUGIN_TOGGLES.filter(
|
|
936
|
+
(t) => !existingPluginToggles.has(t.name)
|
|
937
|
+
);
|
|
938
|
+
if (unconfiguredPluginToggles.length > 0) {
|
|
939
|
+
console.log(`${c.dim}Plugin add-ons${c.reset}`);
|
|
940
|
+
const selected = await promptMulti(
|
|
941
|
+
" Enable plugin add-ons?",
|
|
942
|
+
unconfiguredPluginToggles.map((t) => ({ label: t.label, defaultOn: t.defaultOn }))
|
|
943
|
+
);
|
|
944
|
+
if (selected.size > 0) {
|
|
945
|
+
for (const idx of selected) {
|
|
946
|
+
const toggle = unconfiguredPluginToggles[idx];
|
|
947
|
+
config.plugin.push(toggle.name);
|
|
948
|
+
newPluginToggleEnabledSet.add(toggle.name);
|
|
949
|
+
}
|
|
950
|
+
const names = [...selected].map((i) => unconfiguredPluginToggles[i].name).join(", ");
|
|
951
|
+
ok(`Plugin add-ons enabled: ${names}`);
|
|
952
|
+
} else {
|
|
953
|
+
ok("Plugin add-ons: none");
|
|
1365
954
|
}
|
|
1366
|
-
|
|
1367
|
-
workflowId,
|
|
1368
|
-
phase: "execute",
|
|
1369
|
-
kind: "task.execute.completed",
|
|
1370
|
-
payload: { task: taskNum, id: task.id, commit: result.commitSha },
|
|
1371
|
-
taskId: task.id
|
|
1372
|
-
});
|
|
955
|
+
console.log();
|
|
1373
956
|
}
|
|
1374
|
-
logEvent(db, {
|
|
1375
|
-
workflowId,
|
|
1376
|
-
phase: "execute",
|
|
1377
|
-
kind: "task.execute.phase.completed",
|
|
1378
|
-
payload: { task_count: plan.tasks.length }
|
|
1379
|
-
});
|
|
1380
|
-
return { ok: true, taskResults };
|
|
1381
|
-
} finally {
|
|
1382
|
-
closeDb();
|
|
1383
957
|
}
|
|
958
|
+
if (reconfigureModels && newModelsValue) {
|
|
959
|
+
writePluginOption(configPath, "models", newModelsValue, { dryRun });
|
|
960
|
+
}
|
|
961
|
+
if (reconfigureMcps) {
|
|
962
|
+
writeMcpToggles(configPath, newMcpEnabledSet, { dryRun });
|
|
963
|
+
}
|
|
964
|
+
if (reconfigurePluginToggles) {
|
|
965
|
+
writePluginToggles(configPath, newPluginToggleEnabledSet, { dryRun });
|
|
966
|
+
}
|
|
967
|
+
if (!fs3.existsSync(configPath)) {
|
|
968
|
+
if (dryRun) {
|
|
969
|
+
info(`[dry-run] Would create ${configPath}`);
|
|
970
|
+
info(`[dry-run] Config: ${JSON.stringify(config, null, 2)}`);
|
|
971
|
+
} else {
|
|
972
|
+
seedConfig(config, configPath);
|
|
973
|
+
ok(`Created ${configPath}`);
|
|
974
|
+
}
|
|
975
|
+
} else {
|
|
976
|
+
try {
|
|
977
|
+
const result = mergeConfig(config, configPath, dryRun);
|
|
978
|
+
if (!result.changed) {
|
|
979
|
+
ok("opencode.json is up to date");
|
|
980
|
+
for (const w of result.warnings) warn(w);
|
|
981
|
+
} else {
|
|
982
|
+
if (dryRun) {
|
|
983
|
+
info(`[dry-run] Would merge into ${configPath}:`);
|
|
984
|
+
for (const a of result.additions) info(` ${a}`);
|
|
985
|
+
} else {
|
|
986
|
+
ok(`Updated ${configPath}`);
|
|
987
|
+
info(`Backup: ${result.bakPath}`);
|
|
988
|
+
for (const a of result.additions) info(` ${a}`);
|
|
989
|
+
}
|
|
990
|
+
for (const w of result.warnings) warn(w);
|
|
991
|
+
}
|
|
992
|
+
} catch (e) {
|
|
993
|
+
console.error(`\x1B[31m\u2717\x1B[0m ${e.message}`);
|
|
994
|
+
process.exit(1);
|
|
995
|
+
}
|
|
996
|
+
}
|
|
997
|
+
if (!dryRun) {
|
|
998
|
+
migrateHarnessKeyToPluginOptions(configPath);
|
|
999
|
+
}
|
|
1000
|
+
if (!dryRun) {
|
|
1001
|
+
await refreshPluginCacheIfStale();
|
|
1002
|
+
}
|
|
1003
|
+
if (newPluginToggleEnabledSet.has("opencode-snip")) {
|
|
1004
|
+
warn("opencode-snip requires the Go snip binary. Install: brew install vhardouin/opencode-snip/snip");
|
|
1005
|
+
}
|
|
1006
|
+
console.log(`
|
|
1007
|
+
${c.bold}Ready.${c.reset} Run ${c.green}opencode${c.reset} to start.
|
|
1008
|
+
`);
|
|
1384
1009
|
}
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1010
|
+
|
|
1011
|
+
// src/cli/uninstall.ts
|
|
1012
|
+
import * as fs4 from "fs";
|
|
1013
|
+
import * as path4 from "path";
|
|
1014
|
+
import * as os3 from "os";
|
|
1015
|
+
var PLUGIN_NAME2 = "@glrs-dev/harness-plugin-opencode";
|
|
1016
|
+
function getOpencodeConfigPath2() {
|
|
1017
|
+
const configHome = process.env["XDG_CONFIG_HOME"] ?? path4.join(os3.homedir(), ".config");
|
|
1018
|
+
return path4.join(configHome, "opencode", "opencode.json");
|
|
1019
|
+
}
|
|
1020
|
+
function uninstall(opts = {}) {
|
|
1021
|
+
const { dryRun = false } = opts;
|
|
1022
|
+
const configPath = getOpencodeConfigPath2();
|
|
1023
|
+
const c3 = {
|
|
1024
|
+
reset: "\x1B[0m",
|
|
1025
|
+
green: "\x1B[32m",
|
|
1026
|
+
yellow: "\x1B[33m",
|
|
1027
|
+
blue: "\x1B[34m"
|
|
1028
|
+
};
|
|
1029
|
+
const ok2 = (msg) => console.log(`${c3.green}\u2713${c3.reset} ${msg}`);
|
|
1030
|
+
const info2 = (msg) => console.log(`${c3.blue}\u2022${c3.reset} ${msg}`);
|
|
1031
|
+
const warn2 = (msg) => console.log(`${c3.yellow}!${c3.reset} ${msg}`);
|
|
1032
|
+
console.log(`
|
|
1033
|
+
${c3.blue}Uninstalling ${PLUGIN_NAME2}${c3.reset}
|
|
1034
|
+
`);
|
|
1035
|
+
if (!fs4.existsSync(configPath)) {
|
|
1036
|
+
warn2(`No opencode.json found at ${configPath} \u2014 nothing to do`);
|
|
1037
|
+
return;
|
|
1038
|
+
}
|
|
1039
|
+
let raw;
|
|
1388
1040
|
try {
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1041
|
+
raw = fs4.readFileSync(configPath, "utf8");
|
|
1042
|
+
} catch (e) {
|
|
1043
|
+
console.error(`\x1B[31m\u2717\x1B[0m Failed to read ${configPath}: ${e.message}`);
|
|
1044
|
+
process.exit(1);
|
|
1393
1045
|
}
|
|
1394
|
-
let
|
|
1046
|
+
let config;
|
|
1395
1047
|
try {
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
} catch (err) {
|
|
1401
|
-
return {
|
|
1402
|
-
ok: false,
|
|
1403
|
-
taskId: task.id,
|
|
1404
|
-
reason: `Failed to create builder session: ${err instanceof Error ? err.message : String(err)}`
|
|
1405
|
-
};
|
|
1048
|
+
config = JSON.parse(raw);
|
|
1049
|
+
} catch (e) {
|
|
1050
|
+
console.error(`\x1B[31m\u2717\x1B[0m Invalid JSON in ${configPath}: ${e.message}`);
|
|
1051
|
+
process.exit(1);
|
|
1406
1052
|
}
|
|
1407
|
-
const
|
|
1408
|
-
const
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
stallMs: 15 * 60 * 1e3
|
|
1412
|
-
// 15 min per task
|
|
1053
|
+
const plugins = Array.isArray(config.plugin) ? config.plugin : [];
|
|
1054
|
+
const filtered = plugins.filter((p) => {
|
|
1055
|
+
const name = typeof p === "string" ? p : Array.isArray(p) ? p[0] : null;
|
|
1056
|
+
return name !== PLUGIN_NAME2 && !String(name ?? "").startsWith(`${PLUGIN_NAME2}@`);
|
|
1413
1057
|
});
|
|
1414
|
-
if (
|
|
1415
|
-
|
|
1416
|
-
return
|
|
1417
|
-
ok: false,
|
|
1418
|
-
taskId: task.id,
|
|
1419
|
-
reason: `Builder session ended unexpectedly: ${result.kind}`
|
|
1420
|
-
};
|
|
1058
|
+
if (filtered.length === plugins.length) {
|
|
1059
|
+
warn2(`"${PLUGIN_NAME2}" not found in plugin array \u2014 nothing to remove`);
|
|
1060
|
+
return;
|
|
1421
1061
|
}
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
return {
|
|
1426
|
-
ok: false,
|
|
1427
|
-
taskId: task.id,
|
|
1428
|
-
reason: `Verify failed: ${verifyResult.reason}`
|
|
1429
|
-
};
|
|
1062
|
+
if (dryRun) {
|
|
1063
|
+
info2(`[dry-run] Would remove "${PLUGIN_NAME2}" from plugin array in ${configPath}`);
|
|
1064
|
+
return;
|
|
1430
1065
|
}
|
|
1431
|
-
|
|
1066
|
+
const bakPath = `${configPath}.bak.${Date.now()}-${process.pid}`;
|
|
1432
1067
|
try {
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
const allFiles = [...modifiedFiles, ...untrackedFiles];
|
|
1438
|
-
if (allFiles.length > 20) {
|
|
1439
|
-
process.stderr.write(
|
|
1440
|
-
` [pilot] \u26A0\uFE0F Task ${task.id} modified ${allFiles.length} files \u2014 review the commit carefully
|
|
1441
|
-
`
|
|
1442
|
-
);
|
|
1443
|
-
}
|
|
1444
|
-
await execFileP4("git", ["add", "-A"], { cwd });
|
|
1445
|
-
await execFileP4("git", ["commit", "-m", `pilot: ${task.title} (${task.id})`], { cwd });
|
|
1446
|
-
const { stdout } = await execFileP4("git", ["rev-parse", "HEAD"], { cwd });
|
|
1447
|
-
commitSha = stdout.trim();
|
|
1448
|
-
} catch (err) {
|
|
1449
|
-
await cleanWorkingTree(cwd);
|
|
1450
|
-
return {
|
|
1451
|
-
ok: false,
|
|
1452
|
-
taskId: task.id,
|
|
1453
|
-
reason: `Commit failed: ${err instanceof Error ? err.message : String(err)}`
|
|
1454
|
-
};
|
|
1068
|
+
fs4.copyFileSync(configPath, bakPath);
|
|
1069
|
+
} catch (e) {
|
|
1070
|
+
console.error(`\x1B[31m\u2717\x1B[0m Failed to write backup: ${e.message}`);
|
|
1071
|
+
process.exit(1);
|
|
1455
1072
|
}
|
|
1456
|
-
|
|
1457
|
-
}
|
|
1458
|
-
|
|
1459
|
-
|
|
1073
|
+
config.plugin = filtered;
|
|
1074
|
+
const tmpPath = `${configPath}.uninstall.tmp.${Date.now()}-${process.pid}`;
|
|
1075
|
+
try {
|
|
1076
|
+
fs4.writeFileSync(tmpPath, JSON.stringify(config, null, 2) + "\n");
|
|
1077
|
+
fs4.renameSync(tmpPath, configPath);
|
|
1078
|
+
} catch (e) {
|
|
1460
1079
|
try {
|
|
1461
|
-
|
|
1462
|
-
} catch
|
|
1463
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
1464
|
-
return { ok: false, reason: `Command "${cmd2}" failed: ${msg}` };
|
|
1080
|
+
fs4.unlinkSync(tmpPath);
|
|
1081
|
+
} catch {
|
|
1465
1082
|
}
|
|
1083
|
+
console.error(`\x1B[31m\u2717\x1B[0m Failed to write config: ${e.message}`);
|
|
1084
|
+
process.exit(1);
|
|
1466
1085
|
}
|
|
1467
|
-
|
|
1086
|
+
ok2(`Removed "${PLUGIN_NAME2}" from ${configPath}`);
|
|
1087
|
+
info2(`Backup: ${bakPath}`);
|
|
1088
|
+
console.log(`
|
|
1089
|
+
To fully remove the package: bun remove @glrs-dev/harness-plugin-opencode
|
|
1090
|
+
`);
|
|
1468
1091
|
}
|
|
1469
|
-
|
|
1092
|
+
|
|
1093
|
+
// src/cli/doctor.ts
|
|
1094
|
+
import * as fs5 from "fs";
|
|
1095
|
+
import * as path5 from "path";
|
|
1096
|
+
import * as os4 from "os";
|
|
1097
|
+
import { execSync } from "child_process";
|
|
1098
|
+
var PLUGIN_NAME3 = "@glrs-dev/harness-plugin-opencode";
|
|
1099
|
+
function getOpencodeConfigPath3() {
|
|
1100
|
+
const configHome = process.env["XDG_CONFIG_HOME"] ?? path5.join(os4.homedir(), ".config");
|
|
1101
|
+
return path5.join(configHome, "opencode", "opencode.json");
|
|
1102
|
+
}
|
|
1103
|
+
function cmd(command4) {
|
|
1470
1104
|
try {
|
|
1471
|
-
|
|
1472
|
-
await execFileP4("git", ["clean", "-fd"], { cwd });
|
|
1105
|
+
return execSync(command4, { encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] }).trim();
|
|
1473
1106
|
} catch {
|
|
1107
|
+
return null;
|
|
1108
|
+
}
|
|
1109
|
+
}
|
|
1110
|
+
function which(bin) {
|
|
1111
|
+
return cmd(`which ${bin}`) !== null;
|
|
1112
|
+
}
|
|
1113
|
+
function doctor() {
|
|
1114
|
+
const c3 = {
|
|
1115
|
+
reset: "\x1B[0m",
|
|
1116
|
+
green: "\x1B[32m",
|
|
1117
|
+
yellow: "\x1B[33m",
|
|
1118
|
+
red: "\x1B[31m",
|
|
1119
|
+
bold: "\x1B[1m"
|
|
1120
|
+
};
|
|
1121
|
+
const ok2 = (msg) => console.log(`${c3.green}\u2713${c3.reset} ${msg}`);
|
|
1122
|
+
const warn2 = (msg) => console.log(`${c3.yellow}!${c3.reset} ${msg}`);
|
|
1123
|
+
const fail = (msg) => console.log(`${c3.red}\u2717${c3.reset} ${msg}`);
|
|
1124
|
+
console.log(`
|
|
1125
|
+
${c3.bold}Doctor \u2014 ${PLUGIN_NAME3}${c3.reset}
|
|
1126
|
+
`);
|
|
1127
|
+
const ocVersion = cmd("opencode --version 2>/dev/null | head -1");
|
|
1128
|
+
if (ocVersion) {
|
|
1129
|
+
ok2(`opencode ${ocVersion}`);
|
|
1130
|
+
} else {
|
|
1131
|
+
fail("opencode CLI not found \u2014 install from https://opencode.ai");
|
|
1132
|
+
}
|
|
1133
|
+
const configPath = getOpencodeConfigPath3();
|
|
1134
|
+
if (fs5.existsSync(configPath)) {
|
|
1135
|
+
try {
|
|
1136
|
+
const config = JSON.parse(fs5.readFileSync(configPath, "utf8"));
|
|
1137
|
+
const plugins = Array.isArray(config.plugin) ? config.plugin : [];
|
|
1138
|
+
let pluginOptions = null;
|
|
1139
|
+
const hasPlugin = plugins.some((p) => {
|
|
1140
|
+
if (typeof p === "string") {
|
|
1141
|
+
return p === PLUGIN_NAME3 || p.startsWith(`${PLUGIN_NAME3}@`);
|
|
1142
|
+
}
|
|
1143
|
+
if (Array.isArray(p)) {
|
|
1144
|
+
const [name, opts] = p;
|
|
1145
|
+
const match = name === PLUGIN_NAME3 || String(name ?? "").startsWith(`${PLUGIN_NAME3}@`);
|
|
1146
|
+
if (match && opts && typeof opts === "object") {
|
|
1147
|
+
pluginOptions = opts;
|
|
1148
|
+
}
|
|
1149
|
+
return match;
|
|
1150
|
+
}
|
|
1151
|
+
return false;
|
|
1152
|
+
});
|
|
1153
|
+
if (hasPlugin) {
|
|
1154
|
+
ok2(`"${PLUGIN_NAME3}" present in opencode.json plugin array`);
|
|
1155
|
+
} else {
|
|
1156
|
+
warn2(`"${PLUGIN_NAME3}" NOT in opencode.json plugin array \u2014 run: bunx ${PLUGIN_NAME3} install`);
|
|
1157
|
+
}
|
|
1158
|
+
const modelSources = [];
|
|
1159
|
+
if (pluginOptions && typeof pluginOptions.models === "object") {
|
|
1160
|
+
modelSources.push({
|
|
1161
|
+
label: "plugin options.models",
|
|
1162
|
+
block: pluginOptions.models
|
|
1163
|
+
});
|
|
1164
|
+
}
|
|
1165
|
+
const legacyHarness = config.harness;
|
|
1166
|
+
if (legacyHarness && typeof legacyHarness.models === "object") {
|
|
1167
|
+
modelSources.push({
|
|
1168
|
+
label: "harness.models (legacy)",
|
|
1169
|
+
block: legacyHarness.models
|
|
1170
|
+
});
|
|
1171
|
+
}
|
|
1172
|
+
if (modelSources.length > 0) {
|
|
1173
|
+
const invalid = [];
|
|
1174
|
+
for (const { label, block } of modelSources) {
|
|
1175
|
+
if (!block || typeof block !== "object") continue;
|
|
1176
|
+
for (const [key, rawValue] of Object.entries(block)) {
|
|
1177
|
+
const candidate = Array.isArray(rawValue) ? rawValue[0] : rawValue;
|
|
1178
|
+
if (typeof candidate !== "string") continue;
|
|
1179
|
+
const result = validateModelOverride(candidate);
|
|
1180
|
+
if (!result.valid) {
|
|
1181
|
+
invalid.push({
|
|
1182
|
+
keyPath: `${label}.${key}`,
|
|
1183
|
+
value: candidate,
|
|
1184
|
+
suggestion: result.suggestion,
|
|
1185
|
+
reason: result.reason
|
|
1186
|
+
});
|
|
1187
|
+
}
|
|
1188
|
+
}
|
|
1189
|
+
}
|
|
1190
|
+
if (invalid.length === 0) {
|
|
1191
|
+
ok2("model overrides look valid");
|
|
1192
|
+
} else {
|
|
1193
|
+
for (const entry of invalid) {
|
|
1194
|
+
fail(`invalid model override at ${entry.keyPath}: "${entry.value}"`);
|
|
1195
|
+
if (entry.reason) {
|
|
1196
|
+
console.log(` ${c3.yellow}reason:${c3.reset} ${entry.reason}`);
|
|
1197
|
+
}
|
|
1198
|
+
if (entry.suggestion) {
|
|
1199
|
+
console.log(
|
|
1200
|
+
` ${c3.yellow}fix:${c3.reset} remove this key, or replace with \`${entry.suggestion}\``
|
|
1201
|
+
);
|
|
1202
|
+
} else {
|
|
1203
|
+
console.log(
|
|
1204
|
+
` ${c3.yellow}fix:${c3.reset} remove this key, or run \`bunx ${PLUGIN_NAME3} install\` to pick a current preset`
|
|
1205
|
+
);
|
|
1206
|
+
}
|
|
1207
|
+
}
|
|
1208
|
+
}
|
|
1209
|
+
}
|
|
1210
|
+
} catch {
|
|
1211
|
+
fail(`opencode.json at ${configPath} has invalid JSON`);
|
|
1212
|
+
}
|
|
1213
|
+
} else {
|
|
1214
|
+
warn2(`No opencode.json at ${configPath} \u2014 run: bunx ${PLUGIN_NAME3} install`);
|
|
1215
|
+
}
|
|
1216
|
+
if (which("uvx")) {
|
|
1217
|
+
ok2("uvx (serena + git MCPs)");
|
|
1218
|
+
} else {
|
|
1219
|
+
warn2("uvx not found \u2014 serena and git MCPs won't work. Install: brew install uv");
|
|
1220
|
+
}
|
|
1221
|
+
if (which("node") && which("npx")) {
|
|
1222
|
+
ok2(`node ${cmd("node --version") ?? ""} + npx (memory MCP)`);
|
|
1223
|
+
} else {
|
|
1224
|
+
warn2("node/npx not found \u2014 memory MCP won't work");
|
|
1474
1225
|
}
|
|
1226
|
+
if (which("bun")) {
|
|
1227
|
+
ok2(`bun ${cmd("bun --version") ?? ""}`);
|
|
1228
|
+
} else if (which("npm")) {
|
|
1229
|
+
ok2(`npm ${cmd("npm --version") ?? ""} (install bun for faster installs)`);
|
|
1230
|
+
} else {
|
|
1231
|
+
fail("Neither bun nor npm found \u2014 cannot install plugins");
|
|
1232
|
+
}
|
|
1233
|
+
console.log();
|
|
1475
1234
|
}
|
|
1476
|
-
function buildTaskPrompt(opts) {
|
|
1477
|
-
const { task, scope, workflowId } = opts;
|
|
1478
|
-
const verifyText = task.verify.length > 0 ? task.verify.map((v) => ` - ${v}`).join("\n") : " (no verify commands \u2014 just make the changes)";
|
|
1479
|
-
const addressesText = task.addresses.length > 0 ? task.addresses.join(", ") : "(none specified)";
|
|
1480
|
-
return `You are executing a pilot task.
|
|
1481
|
-
|
|
1482
|
-
Workflow: ${workflowId}
|
|
1483
|
-
Task: ${task.id} \u2014 ${task.title}
|
|
1484
|
-
Addresses: ${addressesText}
|
|
1485
|
-
|
|
1486
|
-
${task.prompt}
|
|
1487
1235
|
|
|
1488
|
-
|
|
1489
|
-
|
|
1236
|
+
// src/autopilot/cli.ts
|
|
1237
|
+
import { command, option, positional, string as stringType, optional, number as numberType, flag } from "cmd-ts";
|
|
1490
1238
|
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
}
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
const result = r;
|
|
1512
|
-
if (typeof result["id"] !== "string") return null;
|
|
1513
|
-
if (!["met", "unmet", "partial"].includes(result["status"])) return null;
|
|
1514
|
-
if (typeof result["evidence"] !== "string") return null;
|
|
1515
|
-
acResults.push({
|
|
1516
|
-
id: result["id"],
|
|
1517
|
-
status: result["status"],
|
|
1518
|
-
evidence: result["evidence"],
|
|
1519
|
-
gap: typeof result["gap"] === "string" ? result["gap"] : void 0
|
|
1520
|
-
});
|
|
1521
|
-
}
|
|
1522
|
-
const risks = [];
|
|
1523
|
-
if (Array.isArray(obj["deployment_risks"])) {
|
|
1524
|
-
for (const r of obj["deployment_risks"]) {
|
|
1525
|
-
if (!r || typeof r !== "object") continue;
|
|
1526
|
-
const risk = r;
|
|
1527
|
-
if (!["high", "medium", "low"].includes(risk["severity"])) continue;
|
|
1528
|
-
if (typeof risk["description"] !== "string") continue;
|
|
1529
|
-
risks.push({
|
|
1530
|
-
severity: risk["severity"],
|
|
1531
|
-
description: risk["description"],
|
|
1532
|
-
actionable: Boolean(risk["actionable"]),
|
|
1533
|
-
suggested_fix: typeof risk["suggested_fix"] === "string" ? risk["suggested_fix"] : void 0
|
|
1534
|
-
});
|
|
1239
|
+
// src/autopilot/debrief.ts
|
|
1240
|
+
function shouldRunDebrief(opts) {
|
|
1241
|
+
if (opts.noDebrief) return false;
|
|
1242
|
+
const envVal = opts.env["GLRS_AUTOPILOT_DEBRIEF"];
|
|
1243
|
+
if (envVal !== void 0 && envVal.toLowerCase() === "off") return false;
|
|
1244
|
+
return true;
|
|
1245
|
+
}
|
|
1246
|
+
async function defaultExecGitDiffStat(cwd) {
|
|
1247
|
+
const { execFile: execFileCb } = await import("child_process");
|
|
1248
|
+
const { promisify } = await import("util");
|
|
1249
|
+
const execFile2 = promisify(execFileCb);
|
|
1250
|
+
try {
|
|
1251
|
+
const { stdout } = await execFile2("git", ["diff", "--stat", "HEAD~1", "HEAD"], { cwd });
|
|
1252
|
+
return stdout.trim();
|
|
1253
|
+
} catch {
|
|
1254
|
+
try {
|
|
1255
|
+
const { stdout } = await execFile2("git", ["diff", "--stat"], { cwd });
|
|
1256
|
+
return stdout.trim() || "(no uncommitted changes)";
|
|
1257
|
+
} catch {
|
|
1258
|
+
return "(git diff unavailable)";
|
|
1535
1259
|
}
|
|
1536
1260
|
}
|
|
1537
|
-
return {
|
|
1538
|
-
workflow_id: obj["workflow_id"],
|
|
1539
|
-
verdict: obj["verdict"],
|
|
1540
|
-
ac_results: acResults,
|
|
1541
|
-
deployment_risks: risks,
|
|
1542
|
-
replan_guidance: typeof obj["replan_guidance"] === "string" ? obj["replan_guidance"] : void 0
|
|
1543
|
-
};
|
|
1544
1261
|
}
|
|
1545
|
-
|
|
1546
|
-
const
|
|
1547
|
-
const
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1262
|
+
function buildContextMessage(loopResult, prompt, gitDiffStat) {
|
|
1263
|
+
const cost = loopResult.cumulativeCostUsd !== void 0 ? `$${loopResult.cumulativeCostUsd.toFixed(4)}` : "not available";
|
|
1264
|
+
const sessionId = loopResult.sessionId ?? "not available";
|
|
1265
|
+
return [
|
|
1266
|
+
"## Autopilot session context",
|
|
1267
|
+
"",
|
|
1268
|
+
`**Exit reason:** ${loopResult.exitReason}`,
|
|
1269
|
+
`**Iterations completed:** ${loopResult.iterations}`,
|
|
1270
|
+
`**Exit message:** ${loopResult.message}`,
|
|
1271
|
+
`**Cumulative cost:** ${cost}`,
|
|
1272
|
+
`**Session ID:** ${sessionId}`,
|
|
1273
|
+
"",
|
|
1274
|
+
"## Original prompt",
|
|
1275
|
+
"",
|
|
1276
|
+
prompt,
|
|
1277
|
+
"",
|
|
1278
|
+
"## Git diff stat (last commit vs HEAD~1)",
|
|
1279
|
+
"",
|
|
1280
|
+
gitDiffStat || "(no changes)",
|
|
1281
|
+
"",
|
|
1282
|
+
"---",
|
|
1283
|
+
"",
|
|
1284
|
+
"Please produce the five-section debrief as instructed in your system prompt."
|
|
1285
|
+
].join("\n");
|
|
1286
|
+
}
|
|
1287
|
+
async function runDebrief(opts) {
|
|
1288
|
+
const _createSession = opts._deps?.createSession ?? createSession;
|
|
1289
|
+
const _sendAndWait = opts._deps?.sendAndWait ?? sendAndWait;
|
|
1290
|
+
const _getLastAssistantMessage = opts._deps?.getLastAssistantMessage ?? getLastAssistantMessage;
|
|
1291
|
+
const _execGitDiffStat = opts._deps?.execGitDiffStat ?? defaultExecGitDiffStat;
|
|
1557
1292
|
try {
|
|
1558
|
-
const
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
workflowId,
|
|
1564
|
-
phase: "assess",
|
|
1565
|
-
kind: "task.assess.session.created",
|
|
1566
|
-
payload: { sessionId, cycle },
|
|
1567
|
-
sessionId
|
|
1293
|
+
const gitDiffStat = await _execGitDiffStat(opts.cwd).catch(() => "(git diff unavailable)");
|
|
1294
|
+
const contextMessage = buildContextMessage(opts.loopResult, opts.prompt, gitDiffStat);
|
|
1295
|
+
const sessionId = await _createSession(opts.server.client, {
|
|
1296
|
+
cwd: opts.cwd,
|
|
1297
|
+
agentName: "debriefer"
|
|
1568
1298
|
});
|
|
1569
|
-
|
|
1570
|
-
const result = await sendAndWait(server.client, {
|
|
1299
|
+
await _sendAndWait(opts.server.client, {
|
|
1571
1300
|
sessionId,
|
|
1572
|
-
message:
|
|
1573
|
-
stallMs:
|
|
1574
|
-
//
|
|
1301
|
+
message: contextMessage,
|
|
1302
|
+
stallMs: 5 * 60 * 1e3
|
|
1303
|
+
// 5 min stall timeout for debrief
|
|
1575
1304
|
});
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
payload: { reason: result.kind, cycle },
|
|
1582
|
-
sessionId
|
|
1583
|
-
});
|
|
1584
|
-
return { ok: false, reason: `Assessor session ended unexpectedly: ${result.kind}` };
|
|
1585
|
-
}
|
|
1586
|
-
if (!fs7.existsSync(assessPath)) {
|
|
1587
|
-
return { ok: false, reason: `Assessor did not produce assessment report at ${assessPath}` };
|
|
1588
|
-
}
|
|
1589
|
-
let artifact;
|
|
1590
|
-
try {
|
|
1591
|
-
const raw = JSON.parse(fs7.readFileSync(assessPath, "utf8"));
|
|
1592
|
-
artifact = parseAssessmentArtifact(raw);
|
|
1593
|
-
} catch {
|
|
1594
|
-
return { ok: false, reason: `Assessment report has invalid JSON` };
|
|
1595
|
-
}
|
|
1596
|
-
if (!artifact) {
|
|
1597
|
-
return { ok: false, reason: `Assessment report has invalid schema` };
|
|
1598
|
-
}
|
|
1599
|
-
for (const acResult of artifact.ac_results) {
|
|
1600
|
-
const kind = acResult.status === "met" ? "task.assess.gate.passed" : "task.assess.gate.failed";
|
|
1601
|
-
logEvent(db, {
|
|
1602
|
-
workflowId,
|
|
1603
|
-
phase: "assess",
|
|
1604
|
-
kind,
|
|
1605
|
-
payload: {
|
|
1606
|
-
gate: acResult.id,
|
|
1607
|
-
status: acResult.status,
|
|
1608
|
-
...acResult.gap ? { reason: acResult.gap } : {}
|
|
1609
|
-
},
|
|
1610
|
-
sessionId
|
|
1611
|
-
});
|
|
1305
|
+
const debriefOutput = await _getLastAssistantMessage(opts.server.client, sessionId);
|
|
1306
|
+
if (debriefOutput) {
|
|
1307
|
+
process.stdout.write("\n\x1B[1m\u2500\u2500\u2500 Autopilot Debrief \u2500\u2500\u2500\x1B[0m\n\n");
|
|
1308
|
+
process.stdout.write(debriefOutput);
|
|
1309
|
+
process.stdout.write("\n\n");
|
|
1612
1310
|
}
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
phase: "assess",
|
|
1618
|
-
kind: "task.assess.risk_check",
|
|
1619
|
-
payload: { risks: highRisks.map((r) => r.description) },
|
|
1620
|
-
sessionId
|
|
1621
|
-
});
|
|
1622
|
-
}
|
|
1623
|
-
if (artifact.verdict === "pass") {
|
|
1624
|
-
logEvent(db, {
|
|
1625
|
-
workflowId,
|
|
1626
|
-
phase: "assess",
|
|
1627
|
-
kind: "task.assess.passed",
|
|
1628
|
-
payload: { all_acs_met: true, cycle },
|
|
1629
|
-
sessionId
|
|
1630
|
-
});
|
|
1631
|
-
return { ok: true, verdict: "pass", artifact };
|
|
1632
|
-
} else {
|
|
1633
|
-
const unmetAcs = artifact.ac_results.filter((r) => r.status !== "met").map((r) => r.id);
|
|
1634
|
-
logEvent(db, {
|
|
1635
|
-
workflowId,
|
|
1636
|
-
phase: "assess",
|
|
1637
|
-
kind: "task.assess.failed",
|
|
1638
|
-
payload: { unmet: unmetAcs, cycle },
|
|
1639
|
-
sessionId
|
|
1640
|
-
});
|
|
1641
|
-
return {
|
|
1642
|
-
ok: true,
|
|
1643
|
-
verdict: "fail",
|
|
1644
|
-
artifact,
|
|
1645
|
-
replanGuidance: artifact.replan_guidance ?? `Unmet ACs: ${unmetAcs.join(", ")}`
|
|
1646
|
-
};
|
|
1647
|
-
}
|
|
1648
|
-
} finally {
|
|
1649
|
-
closeDb();
|
|
1311
|
+
} catch (err) {
|
|
1312
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1313
|
+
process.stderr.write(`\x1B[33m\u26A0 Debrief failed (non-fatal): ${msg}\x1B[0m
|
|
1314
|
+
`);
|
|
1650
1315
|
}
|
|
1651
1316
|
}
|
|
1652
|
-
async function getAssessArtifactPath(cwd, workflowId, cycle) {
|
|
1653
|
-
const base = await getPilotDir(cwd);
|
|
1654
|
-
const dir = path4.join(base, "scopes", workflowId);
|
|
1655
|
-
fs7.mkdirSync(dir, { recursive: true });
|
|
1656
|
-
return path4.join(dir, `assessment-cycle-${cycle}.json`);
|
|
1657
|
-
}
|
|
1658
|
-
function buildAssessorPrompt(opts) {
|
|
1659
|
-
const { workflowId, scope, plan, assessPath, cycle } = opts;
|
|
1660
|
-
const acsText = scope.acceptance_criteria.map((ac) => ` - ${ac.id}: ${ac.description} (verifiable: ${ac.verifiable})`).join("\n");
|
|
1661
|
-
return `You are assessing a pilot workflow.
|
|
1662
1317
|
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1318
|
+
// src/autopilot/cli.ts
|
|
1319
|
+
var loopCmd = command({
|
|
1320
|
+
name: "loop",
|
|
1321
|
+
description: "Run the Ralph loop: send a prompt to PRIME repeatedly until it emits <autopilot-done> or a budget is exhausted.",
|
|
1322
|
+
args: {
|
|
1323
|
+
prompt: positional({
|
|
1324
|
+
type: stringType,
|
|
1325
|
+
displayName: "prompt",
|
|
1326
|
+
description: "The prompt to send to PRIME each iteration (e.g. a Linear issue ref or free-form task)."
|
|
1327
|
+
}),
|
|
1328
|
+
maxIterations: option({
|
|
1329
|
+
long: "max-iterations",
|
|
1330
|
+
type: optional(numberType),
|
|
1331
|
+
description: `Maximum number of loop iterations (default: ${MAX_ITERATIONS}).`
|
|
1332
|
+
}),
|
|
1333
|
+
timeout: option({
|
|
1334
|
+
long: "timeout",
|
|
1335
|
+
type: optional(numberType),
|
|
1336
|
+
description: `Total wall-clock timeout in milliseconds (default: ${TIMEOUT_MS} = 4 hours).`
|
|
1337
|
+
}),
|
|
1338
|
+
noDebrief: flag({
|
|
1339
|
+
long: "no-debrief",
|
|
1340
|
+
description: "Skip the post-run debrief session."
|
|
1341
|
+
})
|
|
1342
|
+
},
|
|
1343
|
+
handler: async ({ prompt, maxIterations, timeout, noDebrief }) => {
|
|
1344
|
+
const cwd = process.cwd();
|
|
1345
|
+
process.stdout.write("\n\x1B[1mAutopilot \u2014 Ralph loop\x1B[0m\n");
|
|
1346
|
+
process.stdout.write(`Prompt: ${prompt.slice(0, 80)}${prompt.length > 80 ? "..." : ""}
|
|
1347
|
+
`);
|
|
1348
|
+
process.stdout.write(`Max iterations: ${maxIterations ?? MAX_ITERATIONS}
|
|
1349
|
+
`);
|
|
1350
|
+
process.stdout.write(`Timeout: ${((timeout ?? TIMEOUT_MS) / 36e5).toFixed(1)}h
|
|
1666
1351
|
|
|
1667
|
-
|
|
1668
|
-
|
|
1352
|
+
`);
|
|
1353
|
+
const result = await runRalphLoop({
|
|
1354
|
+
prompt,
|
|
1355
|
+
cwd,
|
|
1356
|
+
maxIterations: maxIterations ?? void 0,
|
|
1357
|
+
timeoutMs: timeout ?? void 0
|
|
1358
|
+
});
|
|
1359
|
+
const icon = result.exitReason === "sentinel" ? "\x1B[32m\u2713\x1B[0m" : result.exitReason === "kill-switch" ? "\x1B[33m\u2298\x1B[0m" : "\x1B[31m\u2717\x1B[0m";
|
|
1360
|
+
process.stdout.write(`
|
|
1361
|
+
${icon} ${result.message}
|
|
1362
|
+
`);
|
|
1363
|
+
process.stdout.write(` Iterations: ${result.iterations}
|
|
1669
1364
|
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1365
|
+
`);
|
|
1366
|
+
if (shouldRunDebrief({ noDebrief, env: process.env })) {
|
|
1367
|
+
const { startServer } = await import("./opencode-server-KPCDFYAX.js");
|
|
1368
|
+
let debriefServer;
|
|
1369
|
+
try {
|
|
1370
|
+
debriefServer = await startServer({ cwd });
|
|
1371
|
+
await runDebrief({
|
|
1372
|
+
server: debriefServer,
|
|
1373
|
+
loopResult: result,
|
|
1374
|
+
prompt,
|
|
1375
|
+
cwd
|
|
1376
|
+
});
|
|
1377
|
+
} catch {
|
|
1378
|
+
process.stderr.write("\x1B[33m\u26A0 Debrief server failed to start (non-fatal)\x1B[0m\n");
|
|
1379
|
+
} finally {
|
|
1380
|
+
await debriefServer?.shutdown().catch(() => {
|
|
1381
|
+
});
|
|
1382
|
+
}
|
|
1383
|
+
}
|
|
1384
|
+
if (result.exitReason !== "sentinel" && result.exitReason !== "kill-switch") {
|
|
1385
|
+
process.exit(1);
|
|
1386
|
+
}
|
|
1387
|
+
process.exit(0);
|
|
1388
|
+
}
|
|
1389
|
+
});
|
|
1675
1390
|
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
- Check the git diff to see what changed.
|
|
1679
|
-
- For shell-verifiable ACs, run the commands.
|
|
1680
|
-
- For llm-verifiable ACs, use your judgment.
|
|
1391
|
+
// src/autopilot/autopilot-cmd.ts
|
|
1392
|
+
import { command as command2, option as option2, optional as optional2, string as stringType2 } from "cmd-ts";
|
|
1681
1393
|
|
|
1682
|
-
|
|
1394
|
+
// src/autopilot/interactive.ts
|
|
1395
|
+
import * as fs7 from "fs";
|
|
1396
|
+
import * as path7 from "path";
|
|
1683
1397
|
|
|
1684
|
-
|
|
1685
|
-
{
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1398
|
+
// src/plan-paths.ts
|
|
1399
|
+
import { execFile } from "child_process";
|
|
1400
|
+
import * as fs6 from "fs/promises";
|
|
1401
|
+
import * as os5 from "os";
|
|
1402
|
+
import * as path6 from "path";
|
|
1403
|
+
function execFileP(file, args, opts = {}) {
|
|
1404
|
+
const { cwd, timeoutMs = 5e3 } = opts;
|
|
1405
|
+
return new Promise((resolve2, reject) => {
|
|
1406
|
+
const controller = new AbortController();
|
|
1407
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
1408
|
+
execFile(
|
|
1409
|
+
file,
|
|
1410
|
+
args,
|
|
1411
|
+
{ signal: controller.signal, cwd, encoding: "utf8" },
|
|
1412
|
+
(err, stdout) => {
|
|
1413
|
+
clearTimeout(timer);
|
|
1414
|
+
if (err) {
|
|
1415
|
+
reject(err);
|
|
1416
|
+
return;
|
|
1417
|
+
}
|
|
1418
|
+
resolve2(stdout ?? "");
|
|
1419
|
+
}
|
|
1420
|
+
);
|
|
1421
|
+
});
|
|
1695
1422
|
}
|
|
1696
|
-
|
|
1697
|
-
|
|
1423
|
+
function expandTilde(p) {
|
|
1424
|
+
if (p === "~") return os5.homedir();
|
|
1425
|
+
if (p.startsWith("~/")) return path6.join(os5.homedir(), p.slice(2));
|
|
1426
|
+
return p;
|
|
1427
|
+
}
|
|
1428
|
+
async function getRepoFolder(worktreeDir) {
|
|
1429
|
+
let stdout;
|
|
1430
|
+
try {
|
|
1431
|
+
stdout = await execFileP(
|
|
1432
|
+
"git",
|
|
1433
|
+
["rev-parse", "--git-common-dir"],
|
|
1434
|
+
{ cwd: worktreeDir }
|
|
1435
|
+
);
|
|
1436
|
+
} catch (err) {
|
|
1437
|
+
const msg = err instanceof Error ? err.message : "unknown error running `git rev-parse --git-common-dir`";
|
|
1438
|
+
throw new Error(
|
|
1439
|
+
`getRepoFolder: failed to resolve git-common-dir for ${worktreeDir}: ${msg}`
|
|
1440
|
+
);
|
|
1441
|
+
}
|
|
1442
|
+
const gitCommonDir = stdout.trim();
|
|
1443
|
+
if (!gitCommonDir) {
|
|
1444
|
+
throw new Error(
|
|
1445
|
+
`getRepoFolder: \`git rev-parse --git-common-dir\` returned empty for ${worktreeDir}`
|
|
1446
|
+
);
|
|
1447
|
+
}
|
|
1448
|
+
const absCommonDir = path6.isAbsolute(gitCommonDir) ? gitCommonDir : path6.resolve(worktreeDir, gitCommonDir);
|
|
1449
|
+
const repoRoot = path6.dirname(absCommonDir);
|
|
1450
|
+
return path6.basename(repoRoot);
|
|
1451
|
+
}
|
|
1452
|
+
async function getPlanDir(worktreeDir) {
|
|
1453
|
+
const override = process.env.GLORIOUS_PLAN_DIR;
|
|
1454
|
+
const base = override ? expandTilde(override) : path6.join(os5.homedir(), ".glorious", "opencode");
|
|
1455
|
+
const repoFolder = await getRepoFolder(worktreeDir);
|
|
1456
|
+
const planDir = path6.join(base, repoFolder, "plans");
|
|
1457
|
+
await fs6.mkdir(planDir, { recursive: true });
|
|
1458
|
+
return planDir;
|
|
1698
1459
|
}
|
|
1699
1460
|
|
|
1700
|
-
// src/
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1461
|
+
// src/autopilot/interactive.ts
|
|
1462
|
+
function defaultBanner(message) {
|
|
1463
|
+
process.stdout.write(`
|
|
1464
|
+
${message}
|
|
1465
|
+
`);
|
|
1466
|
+
}
|
|
1467
|
+
async function orchestrateAutopilot(opts, deps) {
|
|
1468
|
+
const banner = deps.onBanner ?? defaultBanner;
|
|
1469
|
+
const cwd = opts.cwd ?? process.cwd();
|
|
1470
|
+
banner("\u2192 Phase 1/3: Scoping (interactive)...");
|
|
1471
|
+
const scoperResult = await deps.runScoper({
|
|
1472
|
+
planDir: opts.planDir,
|
|
1473
|
+
slug: opts.slug,
|
|
1474
|
+
initialGoal: opts.initialGoal
|
|
1710
1475
|
});
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
});
|
|
1719
|
-
}
|
|
1720
|
-
const durationMs = Date.now() - startedAt;
|
|
1721
|
-
updateWorkflowStatus(db, workflowId, "completed");
|
|
1722
|
-
logEvent(db, {
|
|
1723
|
-
workflowId,
|
|
1724
|
-
phase: "resolve",
|
|
1725
|
-
kind: "task.resolve.completed",
|
|
1726
|
-
payload: { acknowledged_risks: acknowledgedRisks.length }
|
|
1476
|
+
banner(`\u2713 Scope captured at ${scoperResult.scopePath}`);
|
|
1477
|
+
const actualSlug = path7.basename(path7.dirname(scoperResult.scopePath));
|
|
1478
|
+
banner("\u2192 Phase 2/3: Planning (headless)...");
|
|
1479
|
+
const planResult = await deps.runPlan({
|
|
1480
|
+
scopePath: scoperResult.scopePath,
|
|
1481
|
+
planDir: opts.planDir,
|
|
1482
|
+
slug: actualSlug || opts.slug
|
|
1727
1483
|
});
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
duration: `${Math.round(durationMs / 1e3)}s`
|
|
1734
|
-
}
|
|
1484
|
+
banner(`\u2713 Plan written at ${planResult.planPath}`);
|
|
1485
|
+
banner("\u2192 Phase 3/3: Executing (headless loop)...");
|
|
1486
|
+
const loopResult = await deps.runLoop({
|
|
1487
|
+
planPath: planResult.planPath,
|
|
1488
|
+
cwd
|
|
1735
1489
|
});
|
|
1736
|
-
closeDb();
|
|
1737
1490
|
return {
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
acknowledgedRisks
|
|
1491
|
+
scopePath: scoperResult.scopePath,
|
|
1492
|
+
planPath: planResult.planPath,
|
|
1493
|
+
loopResult
|
|
1742
1494
|
};
|
|
1743
1495
|
}
|
|
1496
|
+
function deriveSlug(goal) {
|
|
1497
|
+
const slug = goal.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 40);
|
|
1498
|
+
return slug.length > 0 ? slug : `feature-${Date.now()}`;
|
|
1499
|
+
}
|
|
1500
|
+
async function browsePlansDir(planDir, _readdirSync) {
|
|
1501
|
+
const { select: select2 } = await import("@inquirer/prompts");
|
|
1502
|
+
const readdir2 = _readdirSync ?? ((p, o) => fs7.readdirSync(p, o));
|
|
1503
|
+
let currentDir = planDir;
|
|
1504
|
+
while (true) {
|
|
1505
|
+
const entries = readdir2(currentDir, { withFileTypes: true });
|
|
1506
|
+
const dirs = entries.filter((e) => e.isDirectory()).map((e) => e.name).sort();
|
|
1507
|
+
const files = entries.filter((e) => e.isFile() && e.name.endsWith(".md")).map((e) => e.name).sort();
|
|
1508
|
+
if (dirs.length === 0 && files.length === 0) {
|
|
1509
|
+
process.stderr.write(`
|
|
1510
|
+
No plans found in ${currentDir}
|
|
1744
1511
|
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1512
|
+
`);
|
|
1513
|
+
return null;
|
|
1514
|
+
}
|
|
1515
|
+
const choices = [];
|
|
1516
|
+
for (const d of dirs) {
|
|
1517
|
+
const dirPath = path7.join(currentDir, d);
|
|
1518
|
+
const hasMain = fs7.existsSync(path7.join(dirPath, "main.md"));
|
|
1519
|
+
const fileCount = readdir2(dirPath, { withFileTypes: true }).filter((e) => e.isFile()).length;
|
|
1520
|
+
choices.push({
|
|
1521
|
+
name: hasMain ? `${d}/ (multi-file plan \u2014 ${fileCount} files)` : `${d}/ (${fileCount} files)`,
|
|
1522
|
+
value: `dir:${dirPath}`
|
|
1523
|
+
});
|
|
1524
|
+
}
|
|
1525
|
+
for (const f of files) {
|
|
1526
|
+
choices.push({
|
|
1527
|
+
name: `${f}`,
|
|
1528
|
+
value: `file:${path7.join(currentDir, f)}`
|
|
1529
|
+
});
|
|
1530
|
+
}
|
|
1531
|
+
if (currentDir !== planDir) {
|
|
1532
|
+
choices.push({ name: "\u21A9 Back", value: "back" });
|
|
1533
|
+
}
|
|
1534
|
+
choices.push({ name: "\u2715 Cancel (scope a new feature instead)", value: "cancel" });
|
|
1535
|
+
const answer = await select2({
|
|
1536
|
+
message: "Select a plan:",
|
|
1537
|
+
choices
|
|
1538
|
+
});
|
|
1539
|
+
if (answer === "cancel") return null;
|
|
1540
|
+
if (answer === "back") {
|
|
1541
|
+
currentDir = path7.dirname(currentDir);
|
|
1542
|
+
continue;
|
|
1543
|
+
}
|
|
1544
|
+
if (answer.startsWith("file:")) {
|
|
1545
|
+
return answer.slice("file:".length);
|
|
1546
|
+
}
|
|
1547
|
+
if (answer.startsWith("dir:")) {
|
|
1548
|
+
const dirPath = answer.slice("dir:".length);
|
|
1549
|
+
const hasMain = fs7.existsSync(path7.join(dirPath, "main.md"));
|
|
1550
|
+
if (hasMain) {
|
|
1551
|
+
const dirAction = await select2({
|
|
1552
|
+
message: `${path7.basename(dirPath)}/ has a main.md. What do you want?`,
|
|
1553
|
+
choices: [
|
|
1554
|
+
{ name: "Select this as a multi-file plan", value: "select" },
|
|
1555
|
+
{ name: "Browse files inside", value: "browse" },
|
|
1556
|
+
{ name: "\u21A9 Back", value: "back" }
|
|
1557
|
+
]
|
|
1558
|
+
});
|
|
1559
|
+
if (dirAction === "select") return dirPath;
|
|
1560
|
+
if (dirAction === "browse") {
|
|
1561
|
+
currentDir = dirPath;
|
|
1562
|
+
continue;
|
|
1563
|
+
}
|
|
1564
|
+
continue;
|
|
1763
1565
|
}
|
|
1764
|
-
|
|
1566
|
+
currentDir = dirPath;
|
|
1567
|
+
continue;
|
|
1765
1568
|
}
|
|
1766
1569
|
}
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1570
|
+
}
|
|
1571
|
+
async function runInteractiveAutopilot(cwd, _deps) {
|
|
1572
|
+
const _getPlanDir = _deps?.getPlanDir ?? getPlanDir;
|
|
1573
|
+
const planDir = await _getPlanDir(cwd);
|
|
1574
|
+
let hasExistingPlan;
|
|
1575
|
+
if (_deps?.promptExistingPlan) {
|
|
1576
|
+
hasExistingPlan = await _deps.promptExistingPlan();
|
|
1577
|
+
} else {
|
|
1578
|
+
const { confirm: confirm2 } = await import("@inquirer/prompts");
|
|
1579
|
+
hasExistingPlan = await confirm2({
|
|
1580
|
+
message: "Do you have an existing plan?",
|
|
1581
|
+
default: false
|
|
1582
|
+
});
|
|
1773
1583
|
}
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
const
|
|
1777
|
-
const
|
|
1778
|
-
|
|
1779
|
-
|
|
1584
|
+
if (hasExistingPlan) {
|
|
1585
|
+
const repoLocalPlansDir = path7.join(cwd, "plans");
|
|
1586
|
+
const hasRepoLocal = fs7.existsSync(repoLocalPlansDir) && fs7.statSync(repoLocalPlansDir).isDirectory();
|
|
1587
|
+
const hasShared = fs7.existsSync(planDir) && fs7.statSync(planDir).isDirectory();
|
|
1588
|
+
let browseRoot;
|
|
1589
|
+
if (hasRepoLocal && hasShared) {
|
|
1590
|
+
const { select: select2 } = await import("@inquirer/prompts");
|
|
1591
|
+
const which2 = await select2({
|
|
1592
|
+
message: "Where are your plans?",
|
|
1593
|
+
choices: [
|
|
1594
|
+
{ name: `./plans/ (repo-local)`, value: repoLocalPlansDir },
|
|
1595
|
+
{ name: `${planDir} (harness-shared)`, value: planDir }
|
|
1596
|
+
]
|
|
1597
|
+
});
|
|
1598
|
+
browseRoot = which2;
|
|
1599
|
+
} else if (hasRepoLocal) {
|
|
1600
|
+
browseRoot = repoLocalPlansDir;
|
|
1601
|
+
} else {
|
|
1602
|
+
browseRoot = planDir;
|
|
1780
1603
|
}
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
const dbPath = await getStateDbPath(cwd);
|
|
1787
|
-
const { db, close: closeDb } = openStateDb(dbPath);
|
|
1788
|
-
logEvent(db, {
|
|
1789
|
-
workflowId,
|
|
1790
|
-
phase: "plan",
|
|
1791
|
-
kind: "workflow.go.started",
|
|
1792
|
-
payload: { goal: scope.goal, scopePath }
|
|
1793
|
-
});
|
|
1794
|
-
closeDb();
|
|
1795
|
-
let server;
|
|
1796
|
-
try {
|
|
1797
|
-
server = await startServer({ cwd });
|
|
1798
|
-
await selfTest(server.client);
|
|
1799
|
-
} catch (err) {
|
|
1800
|
-
return {
|
|
1801
|
-
ok: false,
|
|
1802
|
-
reason: `Failed to start OpenCode server: ${err instanceof Error ? err.message : String(err)}`,
|
|
1803
|
-
workflowId
|
|
1804
|
-
};
|
|
1805
|
-
}
|
|
1806
|
-
try {
|
|
1807
|
-
const planResult = await runPlanPhase({ workflowId, scope, cwd, server });
|
|
1808
|
-
if (!planResult.ok) {
|
|
1809
|
-
return { ok: false, reason: `Plan phase failed: ${planResult.reason}`, workflowId };
|
|
1604
|
+
let selectedPlan;
|
|
1605
|
+
if (_deps?.browsePlans) {
|
|
1606
|
+
selectedPlan = await _deps.browsePlans(browseRoot);
|
|
1607
|
+
} else {
|
|
1608
|
+
selectedPlan = await browsePlansDir(browseRoot, _deps?.readdirSync);
|
|
1810
1609
|
}
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
const
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
plan: currentPlan,
|
|
1828
|
-
cwd,
|
|
1829
|
-
cycle,
|
|
1830
|
-
server
|
|
1831
|
-
});
|
|
1832
|
-
if (!assessResult.ok) {
|
|
1833
|
-
return { ok: false, reason: `Assess phase failed: ${assessResult.reason}`, workflowId };
|
|
1834
|
-
}
|
|
1835
|
-
if (assessResult.verdict === "pass") {
|
|
1836
|
-
const resolveResult = await runResolvePhase({
|
|
1837
|
-
workflowId,
|
|
1838
|
-
scope,
|
|
1839
|
-
assessment: assessResult.artifact,
|
|
1840
|
-
cwd,
|
|
1841
|
-
startedAt
|
|
1610
|
+
if (!selectedPlan) {
|
|
1611
|
+
process.stderr.write("\n No plan selected. Starting new feature scoping.\n\n");
|
|
1612
|
+
} else {
|
|
1613
|
+
const isDir = fs7.statSync(selectedPlan).isDirectory();
|
|
1614
|
+
const planPath = isDir ? selectedPlan : selectedPlan;
|
|
1615
|
+
const { parsePlanState } = await import("./plan-parser-TMHEKT22.js");
|
|
1616
|
+
const planState = parsePlanState(planPath);
|
|
1617
|
+
if (planState.totalItems > 0 && planState.checkedItems === planState.totalItems) {
|
|
1618
|
+
const { select: selectAction } = await import("@inquirer/prompts");
|
|
1619
|
+
const action = await selectAction({
|
|
1620
|
+
message: `All ${planState.totalItems} items in this plan are already checked. What do you want to do?`,
|
|
1621
|
+
choices: [
|
|
1622
|
+
{ name: "Uncheck all items and run from scratch", value: "uncheck" },
|
|
1623
|
+
{ name: "Run anyway (agent will verify/audit the checked items)", value: "run" },
|
|
1624
|
+
{ name: "Cancel and pick a different plan", value: "cancel" }
|
|
1625
|
+
]
|
|
1842
1626
|
});
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
1627
|
+
if (action === "cancel") {
|
|
1628
|
+
process.stderr.write("\n Cancelled. Starting new feature scoping.\n\n");
|
|
1629
|
+
} else {
|
|
1630
|
+
if (action === "uncheck") {
|
|
1631
|
+
const uncheckFiles = isDir ? fs7.readdirSync(planPath).filter((f) => f.endsWith(".md")).map((f) => path7.join(planPath, f)) : [planPath];
|
|
1632
|
+
for (const file of uncheckFiles) {
|
|
1633
|
+
const content = fs7.readFileSync(file, "utf-8");
|
|
1634
|
+
const unchecked = content.replace(/- \[x\]/g, "- [ ]");
|
|
1635
|
+
fs7.writeFileSync(file, unchecked);
|
|
1636
|
+
}
|
|
1637
|
+
process.stderr.write(`
|
|
1638
|
+
\u2713 Unchecked all items in ${uncheckFiles.length} file(s).
|
|
1639
|
+
|
|
1640
|
+
`);
|
|
1641
|
+
}
|
|
1642
|
+
const banner = _deps?.onBanner ?? ((msg) => process.stdout.write(`
|
|
1643
|
+
${msg}
|
|
1644
|
+
`));
|
|
1645
|
+
banner(`\u2192 Running loop against plan: ${planPath}`);
|
|
1646
|
+
const { runLoopSession: runLoopSession2 } = await import("./loop-session-J35NILUZ.js");
|
|
1647
|
+
const _runLoop = _deps?.runLoop ?? runLoopSession2;
|
|
1648
|
+
const loopResult = await _runLoop({ planPath, cwd });
|
|
1649
|
+
return {
|
|
1650
|
+
scopePath: "",
|
|
1651
|
+
planPath,
|
|
1652
|
+
loopResult
|
|
1653
|
+
};
|
|
1654
|
+
}
|
|
1655
|
+
} else {
|
|
1656
|
+
const unchecked = planState.totalItems - planState.checkedItems;
|
|
1853
1657
|
process.stderr.write(
|
|
1854
1658
|
`
|
|
1855
|
-
|
|
1856
|
-
Gap: ${assessResult.replanGuidance}
|
|
1857
|
-
Elapsed: ${elapsedSec}s
|
|
1659
|
+
Plan: ${planState.totalItems} items, ${unchecked} remaining.
|
|
1858
1660
|
|
|
1859
1661
|
`
|
|
1860
1662
|
);
|
|
1861
|
-
const
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
});
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
context: `${scope.context ?? ""}
|
|
1874
|
-
|
|
1875
|
-
Previous attempt failed. Gap to address:
|
|
1876
|
-
${assessResult.replanGuidance}`
|
|
1877
|
-
},
|
|
1878
|
-
cwd,
|
|
1879
|
-
server
|
|
1880
|
-
});
|
|
1881
|
-
if (!replanResult.ok) {
|
|
1882
|
-
return { ok: false, reason: `Re-plan failed: ${replanResult.reason}`, workflowId };
|
|
1883
|
-
}
|
|
1884
|
-
currentPlan = replanResult.artifact;
|
|
1663
|
+
const banner = _deps?.onBanner ?? ((msg) => process.stdout.write(`
|
|
1664
|
+
${msg}
|
|
1665
|
+
`));
|
|
1666
|
+
banner(`\u2192 Running loop against plan: ${planPath}`);
|
|
1667
|
+
const { runLoopSession: runLoopSession2 } = await import("./loop-session-J35NILUZ.js");
|
|
1668
|
+
const _runLoop = _deps?.runLoop ?? runLoopSession2;
|
|
1669
|
+
const loopResult = await _runLoop({ planPath, cwd });
|
|
1670
|
+
return {
|
|
1671
|
+
scopePath: "",
|
|
1672
|
+
planPath,
|
|
1673
|
+
loopResult
|
|
1674
|
+
};
|
|
1885
1675
|
}
|
|
1886
1676
|
}
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
|
|
1677
|
+
}
|
|
1678
|
+
let goal;
|
|
1679
|
+
let ticketRef;
|
|
1680
|
+
if (_deps?.promptGoal) {
|
|
1681
|
+
goal = await _deps.promptGoal();
|
|
1682
|
+
} else {
|
|
1683
|
+
const { input } = await import("@inquirer/prompts");
|
|
1684
|
+
goal = await input({
|
|
1685
|
+
message: "What do you want to build? (one sentence, free-form)",
|
|
1686
|
+
validate: (v) => v.trim().length > 0 ? true : "Please describe what you want to build."
|
|
1894
1687
|
});
|
|
1895
|
-
closeFailDb();
|
|
1896
|
-
return {
|
|
1897
|
-
ok: false,
|
|
1898
|
-
reason: `Assess failed after ${maxCycles} cycles. Manual intervention required.`,
|
|
1899
|
-
workflowId
|
|
1900
|
-
};
|
|
1901
|
-
} finally {
|
|
1902
|
-
await server.shutdown();
|
|
1903
1688
|
}
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
const
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
return null;
|
|
1689
|
+
if (_deps?.promptTicketRef) {
|
|
1690
|
+
ticketRef = await _deps.promptTicketRef();
|
|
1691
|
+
} else {
|
|
1692
|
+
const { input } = await import("@inquirer/prompts");
|
|
1693
|
+
ticketRef = await input({
|
|
1694
|
+
message: "Optional ticket or issue ref (Linear ID, GitHub issue URL, etc.)",
|
|
1695
|
+
default: ""
|
|
1696
|
+
});
|
|
1913
1697
|
}
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
process.exit(1);
|
|
1941
|
-
}
|
|
1942
|
-
const durationSec = Math.round(result.durationMs / 1e3);
|
|
1943
|
-
const durationStr = durationSec >= 60 ? `${Math.floor(durationSec / 60)}m ${durationSec % 60}s` : `${durationSec}s`;
|
|
1944
|
-
console.log(`
|
|
1945
|
-
\x1B[32m\u2713\x1B[0m Workflow complete`);
|
|
1946
|
-
console.log(` Goal: ${result.goal}`);
|
|
1947
|
-
console.log(` Duration: ${durationStr}`);
|
|
1948
|
-
if (result.acknowledgedRisks.length > 0) {
|
|
1949
|
-
console.log(`
|
|
1950
|
-
Acknowledged risks (non-blocking):`);
|
|
1951
|
-
for (const risk of result.acknowledgedRisks) {
|
|
1952
|
-
console.log(` \u2022 ${risk}`);
|
|
1953
|
-
}
|
|
1698
|
+
const slug = deriveSlug(goal);
|
|
1699
|
+
const seedDir = path7.join(planDir, slug);
|
|
1700
|
+
const seedPath = path7.join(seedDir, "scope-seed.md");
|
|
1701
|
+
const _mkdirSync = _deps?.mkdirSync ?? ((p, o) => fs7.mkdirSync(p, o));
|
|
1702
|
+
const _writeFileSync = _deps?.writeFileSync ?? fs7.writeFileSync;
|
|
1703
|
+
_mkdirSync(seedDir, { recursive: true });
|
|
1704
|
+
const seedContent = [
|
|
1705
|
+
`# Scope Seed: ${slug}`,
|
|
1706
|
+
"",
|
|
1707
|
+
`## Goal`,
|
|
1708
|
+
"",
|
|
1709
|
+
goal,
|
|
1710
|
+
"",
|
|
1711
|
+
...ticketRef.trim() ? [`## Ticket / Issue Ref`, "", ticketRef.trim(), ""] : []
|
|
1712
|
+
].join("\n");
|
|
1713
|
+
_writeFileSync(seedPath, seedContent);
|
|
1714
|
+
const { runScoperSession } = await import("./scoper-S77SOK7X.js");
|
|
1715
|
+
const { runPlanSession } = await import("./plan-session-7VS32P52.js");
|
|
1716
|
+
const { runLoopSession } = await import("./loop-session-J35NILUZ.js");
|
|
1717
|
+
return orchestrateAutopilot(
|
|
1718
|
+
{ slug, planDir, cwd, initialGoal: goal },
|
|
1719
|
+
{
|
|
1720
|
+
runScoper: _deps?.runScoper ?? runScoperSession,
|
|
1721
|
+
runPlan: _deps?.runPlan ?? runPlanSession,
|
|
1722
|
+
runLoop: _deps?.runLoop ?? runLoopSession,
|
|
1723
|
+
onBanner: _deps?.onBanner
|
|
1954
1724
|
}
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
}
|
|
1958
|
-
});
|
|
1725
|
+
);
|
|
1726
|
+
}
|
|
1959
1727
|
|
|
1960
|
-
// src/
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
description: "Show pilot workflow status.",
|
|
1728
|
+
// src/autopilot/autopilot-cmd.ts
|
|
1729
|
+
var autopilotInteractiveCmd = command2({
|
|
1730
|
+
name: "autopilot",
|
|
1731
|
+
description: "Interactive three-phase autopilot: scope with @scoper, plan with @plan, then execute with the Ralph loop. Produces a structured plan before running.",
|
|
1965
1732
|
args: {
|
|
1966
|
-
|
|
1967
|
-
long: "
|
|
1733
|
+
slug: option2({
|
|
1734
|
+
long: "slug",
|
|
1968
1735
|
type: optional2(stringType2),
|
|
1969
|
-
description: "
|
|
1970
|
-
}),
|
|
1971
|
-
json: flag({
|
|
1972
|
-
long: "json",
|
|
1973
|
-
description: "Output JSON"
|
|
1736
|
+
description: "Plan slug (kebab-case, \u22645 words). If omitted, you will be prompted during the scoping session."
|
|
1974
1737
|
})
|
|
1975
1738
|
},
|
|
1976
|
-
handler: async ({
|
|
1977
|
-
const
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
process.exit(1);
|
|
1985
|
-
}
|
|
1986
|
-
const events = readEvents(db, { workflowId: wf.id, limit: 100 });
|
|
1987
|
-
if (json) {
|
|
1988
|
-
process.stdout.write(JSON.stringify({ workflow: wf, events }, null, 2) + "\n");
|
|
1989
|
-
process.exit(0);
|
|
1990
|
-
}
|
|
1991
|
-
const started = new Date(wf.started_at).toLocaleString();
|
|
1992
|
-
const finished = wf.finished_at ? new Date(wf.finished_at).toLocaleString() : "--";
|
|
1993
|
-
const statusColor = wf.status === "completed" ? "\x1B[32m" : wf.status === "failed" ? "\x1B[31m" : "\x1B[33m";
|
|
1994
|
-
console.log(`
|
|
1995
|
-
Workflow ${wf.id}`);
|
|
1996
|
-
console.log(` Goal: ${wf.goal}`);
|
|
1997
|
-
console.log(` Status: ${statusColor}${wf.status}\x1B[0m`);
|
|
1998
|
-
console.log(` Started: ${started}`);
|
|
1999
|
-
console.log(` Ended: ${finished}`);
|
|
2000
|
-
console.log(`
|
|
2001
|
-
Recent events (${events.length}):`);
|
|
2002
|
-
for (const event of events.slice(-20)) {
|
|
2003
|
-
const ts = new Date(event.ts).toLocaleTimeString();
|
|
2004
|
-
const payload = (() => {
|
|
2005
|
-
try {
|
|
2006
|
-
const p = JSON.parse(event.payload);
|
|
2007
|
-
return Object.entries(p).map(([k, v]) => `${k}=${JSON.stringify(v)}`).join(" ");
|
|
2008
|
-
} catch {
|
|
2009
|
-
return event.payload;
|
|
2010
|
-
}
|
|
2011
|
-
})();
|
|
2012
|
-
console.log(` ${ts} [${event.phase}] ${event.kind} ${payload}`);
|
|
2013
|
-
}
|
|
2014
|
-
console.log();
|
|
2015
|
-
process.exit(0);
|
|
2016
|
-
} finally {
|
|
2017
|
-
close();
|
|
2018
|
-
}
|
|
2019
|
-
}
|
|
2020
|
-
});
|
|
2021
|
-
|
|
2022
|
-
// src/pilot/cli/shims.ts
|
|
2023
|
-
import { command as command5 } from "cmd-ts";
|
|
2024
|
-
function removedCommand(name, replacement) {
|
|
2025
|
-
return command5({
|
|
2026
|
-
name,
|
|
2027
|
-
description: `[removed] Use \`${replacement}\` instead.`,
|
|
2028
|
-
args: {},
|
|
2029
|
-
handler: async () => {
|
|
2030
|
-
process.stderr.write(
|
|
2031
|
-
`
|
|
2032
|
-
\x1B[33m!\x1B[0m \`pilot ${name}\` was removed in pilot v2.
|
|
2033
|
-
Use \x1B[1m${replacement}\x1B[0m instead.
|
|
2034
|
-
|
|
2035
|
-
Migration guide:
|
|
2036
|
-
pilot scope "<goal>" \u2014 interactive scoping (replaces pilot plan)
|
|
2037
|
-
pilot go \u2014 autonomous execution (replaces pilot build)
|
|
2038
|
-
pilot configure \u2014 set up models and verify commands
|
|
2039
|
-
pilot status \u2014 check workflow status
|
|
2040
|
-
|
|
1739
|
+
handler: async ({ slug: _slug }) => {
|
|
1740
|
+
const result = await runInteractiveAutopilot(process.cwd());
|
|
1741
|
+
process.stdout.write(
|
|
1742
|
+
`
|
|
1743
|
+
\x1B[1m\u2713 Autopilot complete\x1B[0m
|
|
1744
|
+
Scope: ${result.scopePath}
|
|
1745
|
+
Plan: ${result.planPath}
|
|
1746
|
+
Loop: ${result.loopResult.exitReason} after ${result.loopResult.iterations} iteration(s)
|
|
2041
1747
|
`
|
|
2042
|
-
|
|
2043
|
-
process.exit(1);
|
|
2044
|
-
}
|
|
2045
|
-
});
|
|
2046
|
-
}
|
|
2047
|
-
var buildShim = removedCommand("build", "pilot go");
|
|
2048
|
-
var validateShim = removedCommand("validate", "pilot configure");
|
|
2049
|
-
var logsShim = removedCommand("logs", "pilot status --json");
|
|
2050
|
-
var costShim = removedCommand("cost", "pilot status --json");
|
|
2051
|
-
var buildResumeShim = removedCommand("build-resume", "pilot go");
|
|
2052
|
-
|
|
2053
|
-
// src/pilot/cli/index.ts
|
|
2054
|
-
var pilotSubcommand = subcommands({
|
|
2055
|
-
name: "pilot",
|
|
2056
|
-
description: "Pilot v2 \u2014 SPEAR-based autonomous execution (scope \u2192 plan \u2192 execute \u2192 assess \u2192 resolve).",
|
|
2057
|
-
cmds: {
|
|
2058
|
-
scope: scopeCmd,
|
|
2059
|
-
go: goCmd,
|
|
2060
|
-
configure: configureCmd,
|
|
2061
|
-
status: statusCmd,
|
|
2062
|
-
// Shims for removed v1 commands (print migration message)
|
|
2063
|
-
build: buildShim,
|
|
2064
|
-
validate: validateShim,
|
|
2065
|
-
logs: logsShim,
|
|
2066
|
-
cost: costShim,
|
|
2067
|
-
"build-resume": buildResumeShim
|
|
1748
|
+
);
|
|
2068
1749
|
}
|
|
2069
1750
|
});
|
|
2070
1751
|
|
|
2071
1752
|
// src/cli/cli-update.ts
|
|
2072
|
-
import * as
|
|
2073
|
-
import * as
|
|
2074
|
-
import * as
|
|
1753
|
+
import * as fs8 from "fs";
|
|
1754
|
+
import * as path8 from "path";
|
|
1755
|
+
import * as os6 from "os";
|
|
2075
1756
|
import { spawn } from "child_process";
|
|
2076
1757
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
2077
1758
|
var PACKAGE_NAME = "@glrs-dev/harness-plugin-opencode";
|
|
2078
1759
|
var CHECK_INTERVAL_MS = 24 * 60 * 60 * 1e3;
|
|
2079
|
-
var
|
|
1760
|
+
var c2 = {
|
|
2080
1761
|
reset: "\x1B[0m",
|
|
2081
1762
|
green: "\x1B[32m",
|
|
2082
1763
|
yellow: "\x1B[33m",
|
|
@@ -2098,12 +1779,12 @@ function isMajorBump(current, latest) {
|
|
|
2098
1779
|
return latest.major > current.major;
|
|
2099
1780
|
}
|
|
2100
1781
|
function getStateFilePath() {
|
|
2101
|
-
const cacheHome = process.env["XDG_CACHE_HOME"] ??
|
|
2102
|
-
return
|
|
1782
|
+
const cacheHome = process.env["XDG_CACHE_HOME"] ?? path8.join(os6.homedir(), ".cache");
|
|
1783
|
+
return path8.join(cacheHome, "harness-opencode", "cli-update.json");
|
|
2103
1784
|
}
|
|
2104
1785
|
function readState() {
|
|
2105
1786
|
try {
|
|
2106
|
-
const raw =
|
|
1787
|
+
const raw = fs8.readFileSync(getStateFilePath(), "utf8");
|
|
2107
1788
|
return JSON.parse(raw);
|
|
2108
1789
|
} catch {
|
|
2109
1790
|
return null;
|
|
@@ -2112,21 +1793,21 @@ function readState() {
|
|
|
2112
1793
|
function writeState(state) {
|
|
2113
1794
|
try {
|
|
2114
1795
|
const statePath = getStateFilePath();
|
|
2115
|
-
|
|
2116
|
-
|
|
1796
|
+
fs8.mkdirSync(path8.dirname(statePath), { recursive: true });
|
|
1797
|
+
fs8.writeFileSync(statePath, JSON.stringify(state));
|
|
2117
1798
|
} catch {
|
|
2118
1799
|
}
|
|
2119
1800
|
}
|
|
2120
1801
|
function readInstalledVersion() {
|
|
2121
|
-
const here =
|
|
1802
|
+
const here = path8.dirname(fileURLToPath2(import.meta.url));
|
|
2122
1803
|
const candidates = [
|
|
2123
|
-
|
|
2124
|
-
|
|
2125
|
-
|
|
1804
|
+
path8.join(here, "..", "package.json"),
|
|
1805
|
+
path8.join(here, "..", "..", "package.json"),
|
|
1806
|
+
path8.join(here, "package.json")
|
|
2126
1807
|
];
|
|
2127
1808
|
for (const candidate of candidates) {
|
|
2128
1809
|
try {
|
|
2129
|
-
const raw =
|
|
1810
|
+
const raw = fs8.readFileSync(candidate, "utf8");
|
|
2130
1811
|
const parsed = JSON.parse(raw);
|
|
2131
1812
|
if (parsed.name === PACKAGE_NAME && typeof parsed.version === "string") {
|
|
2132
1813
|
return parsed.version;
|
|
@@ -2197,7 +1878,7 @@ function startUpdateCheck() {
|
|
|
2197
1878
|
action = () => {
|
|
2198
1879
|
process.stderr.write(
|
|
2199
1880
|
`
|
|
2200
|
-
${
|
|
1881
|
+
${c2.blue}\u2022${c2.reset} Updating ${PACKAGE_NAME} ${c2.dim}${currentVersionStr}${c2.reset} \u2192 ${c2.green}${latestStr}${c2.reset} in the background...
|
|
2201
1882
|
`
|
|
2202
1883
|
);
|
|
2203
1884
|
spawnBackgroundUpdate();
|
|
@@ -2212,8 +1893,8 @@ ${c.blue}\u2022${c.reset} Updating ${PACKAGE_NAME} ${c.dim}${currentVersionStr}$
|
|
|
2212
1893
|
function printMajorNotice(current, latest) {
|
|
2213
1894
|
process.stderr.write(
|
|
2214
1895
|
`
|
|
2215
|
-
${
|
|
2216
|
-
${
|
|
1896
|
+
${c2.yellow}${c2.bold}Major update available:${c2.reset} ${current} \u2192 ${c2.green}${latest}${c2.reset}
|
|
1897
|
+
${c2.dim}Review the changelog before upgrading:${c2.reset}
|
|
2217
1898
|
bun update -g ${PACKAGE_NAME}
|
|
2218
1899
|
`
|
|
2219
1900
|
);
|
|
@@ -2252,7 +1933,7 @@ Upgrade Node or run via a compatible Bun runtime. See the "engines" field in pac
|
|
|
2252
1933
|
}
|
|
2253
1934
|
}
|
|
2254
1935
|
var VERSION = "0.1.0";
|
|
2255
|
-
var installCmd =
|
|
1936
|
+
var installCmd = command3({
|
|
2256
1937
|
name: "install",
|
|
2257
1938
|
description: 'Add "@glrs-dev/harness-plugin-opencode" to your opencode.json plugin array.',
|
|
2258
1939
|
args: {
|
|
@@ -2269,7 +1950,7 @@ var installCmd = command6({
|
|
|
2269
1950
|
await install({ dryRun, pin });
|
|
2270
1951
|
}
|
|
2271
1952
|
});
|
|
2272
|
-
var uninstallCmd =
|
|
1953
|
+
var uninstallCmd = command3({
|
|
2273
1954
|
name: "uninstall",
|
|
2274
1955
|
description: 'Remove "@glrs-dev/harness-plugin-opencode" from your opencode.json plugin array.',
|
|
2275
1956
|
args: {
|
|
@@ -2282,7 +1963,7 @@ var uninstallCmd = command6({
|
|
|
2282
1963
|
uninstall({ dryRun });
|
|
2283
1964
|
}
|
|
2284
1965
|
});
|
|
2285
|
-
var doctorCmd =
|
|
1966
|
+
var doctorCmd = command3({
|
|
2286
1967
|
name: "doctor",
|
|
2287
1968
|
description: "Check installation health (OpenCode CLI, plugin registration, MCP backends).",
|
|
2288
1969
|
args: {},
|
|
@@ -2290,58 +1971,7 @@ var doctorCmd = command6({
|
|
|
2290
1971
|
doctor();
|
|
2291
1972
|
}
|
|
2292
1973
|
});
|
|
2293
|
-
var
|
|
2294
|
-
name: "plan-check",
|
|
2295
|
-
description: "Parse a plan file's plan-state fence (legacy markdown plans).",
|
|
2296
|
-
args: {
|
|
2297
|
-
run: option3({
|
|
2298
|
-
long: "run",
|
|
2299
|
-
type: optional3(string2),
|
|
2300
|
-
description: "Print verify commands for pending items, one per line."
|
|
2301
|
-
}),
|
|
2302
|
-
check: option3({
|
|
2303
|
-
long: "check",
|
|
2304
|
-
type: optional3(string2),
|
|
2305
|
-
description: "Structural validation; exits 1 if any item is invalid."
|
|
2306
|
-
}),
|
|
2307
|
-
rest: restPositionals2({
|
|
2308
|
-
type: string2,
|
|
2309
|
-
displayName: "plan-path",
|
|
2310
|
-
description: "Path to a plan markdown file. Required unless --run / --check is given."
|
|
2311
|
-
})
|
|
2312
|
-
},
|
|
2313
|
-
handler: ({ run: run2, check, rest }) => {
|
|
2314
|
-
const legacy = [];
|
|
2315
|
-
if (run2 !== void 0) {
|
|
2316
|
-
legacy.push("--run", run2);
|
|
2317
|
-
} else if (check !== void 0) {
|
|
2318
|
-
legacy.push("--check", check);
|
|
2319
|
-
} else {
|
|
2320
|
-
legacy.push(...rest);
|
|
2321
|
-
}
|
|
2322
|
-
planCheck(legacy);
|
|
2323
|
-
}
|
|
2324
|
-
});
|
|
2325
|
-
var planDirCmd = command6({
|
|
2326
|
-
name: "plan-dir",
|
|
2327
|
-
description: "Print the repo-shared plan directory for the current worktree (resolves + creates + migrates legacy).",
|
|
2328
|
-
args: {},
|
|
2329
|
-
handler: async () => {
|
|
2330
|
-
try {
|
|
2331
|
-
const cwd = process.cwd();
|
|
2332
|
-
const planDir = await getPlanDir(cwd);
|
|
2333
|
-
await migratePlans(cwd, planDir);
|
|
2334
|
-
process.stdout.write(planDir + "\n");
|
|
2335
|
-
process.exit(0);
|
|
2336
|
-
} catch (err) {
|
|
2337
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
2338
|
-
process.stderr.write(`plan-dir: ${msg}
|
|
2339
|
-
`);
|
|
2340
|
-
process.exit(1);
|
|
2341
|
-
}
|
|
2342
|
-
}
|
|
2343
|
-
});
|
|
2344
|
-
var installPluginCmd = command6({
|
|
1974
|
+
var installPluginCmd = command3({
|
|
2345
1975
|
name: "install-plugin",
|
|
2346
1976
|
description: 'Add "@glrs-dev/harness-plugin-opencode" to your opencode.json plugin array.',
|
|
2347
1977
|
args: {
|
|
@@ -2358,7 +1988,7 @@ var installPluginCmd = command6({
|
|
|
2358
1988
|
await install({ dryRun, pin });
|
|
2359
1989
|
}
|
|
2360
1990
|
});
|
|
2361
|
-
var cli =
|
|
1991
|
+
var cli = subcommands({
|
|
2362
1992
|
name: "glrs-oc",
|
|
2363
1993
|
description: "OpenCode agent harness CLI.",
|
|
2364
1994
|
version: VERSION,
|
|
@@ -2367,9 +1997,11 @@ var cli = subcommands2({
|
|
|
2367
1997
|
install: installCmd,
|
|
2368
1998
|
uninstall: uninstallCmd,
|
|
2369
1999
|
doctor: doctorCmd,
|
|
2370
|
-
|
|
2371
|
-
|
|
2372
|
-
|
|
2000
|
+
// `loop` is the raw-prompt Ralph loop runner.
|
|
2001
|
+
// `autopilot` is the interactive three-phase orchestrator (scope → plan → loop).
|
|
2002
|
+
// PR 3 diverged them: they are now separate subcommands.
|
|
2003
|
+
loop: loopCmd,
|
|
2004
|
+
autopilot: autopilotInteractiveCmd
|
|
2373
2005
|
}
|
|
2374
2006
|
});
|
|
2375
2007
|
var printUpdate = startUpdateCheck();
|