opencode-autoresearch 3.6.0 → 3.7.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/.opencode-plugin/plugin.json +1 -1
- package/CHANGELOG.md +166 -0
- package/INSTALL.md +2 -2
- package/README.md +79 -39
- package/VERSION +1 -1
- package/dist/badge.d.ts +9 -0
- package/dist/badge.d.ts.map +1 -0
- package/dist/badge.js +86 -0
- package/dist/badge.js.map +1 -0
- package/dist/cli.js +578 -26
- package/dist/cli.js.map +1 -1
- package/dist/constants.d.ts +7 -1
- package/dist/constants.d.ts.map +1 -1
- package/dist/constants.js +7 -1
- package/dist/constants.js.map +1 -1
- package/dist/goal-init.d.ts +30 -0
- package/dist/goal-init.d.ts.map +1 -0
- package/dist/goal-init.js +109 -0
- package/dist/goal-init.js.map +1 -0
- package/dist/helpers.d.ts +13 -2
- package/dist/helpers.d.ts.map +1 -1
- package/dist/helpers.js +152 -9
- package/dist/helpers.js.map +1 -1
- package/dist/index.d.ts +10 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -2
- package/dist/index.js.map +1 -1
- package/dist/memory-manager.d.ts +24 -0
- package/dist/memory-manager.d.ts.map +1 -0
- package/dist/memory-manager.js +336 -0
- package/dist/memory-manager.js.map +1 -0
- package/dist/run-manager.d.ts +27 -2
- package/dist/run-manager.d.ts.map +1 -1
- package/dist/run-manager.js +210 -15
- package/dist/run-manager.js.map +1 -1
- package/dist/score-parser.d.ts +56 -0
- package/dist/score-parser.d.ts.map +1 -0
- package/dist/score-parser.js +109 -0
- package/dist/score-parser.js.map +1 -0
- package/dist/subagent-pool.d.ts +8 -0
- package/dist/subagent-pool.d.ts.map +1 -1
- package/dist/subagent-pool.js +67 -0
- package/dist/subagent-pool.js.map +1 -1
- package/dist/task-schema.d.ts +62 -0
- package/dist/task-schema.d.ts.map +1 -0
- package/dist/task-schema.js +95 -0
- package/dist/task-schema.js.map +1 -0
- package/dist/translators/cli.d.ts +5 -0
- package/dist/translators/cli.d.ts.map +1 -0
- package/dist/translators/cli.js +85 -0
- package/dist/translators/cli.js.map +1 -0
- package/dist/translators/hermes.d.ts +28 -0
- package/dist/translators/hermes.d.ts.map +1 -0
- package/dist/translators/hermes.js +102 -0
- package/dist/translators/hermes.js.map +1 -0
- package/dist/types.d.ts +104 -0
- package/dist/types.d.ts.map +1 -1
- package/docs/ARCHITECTURE.md +3 -0
- package/docs/RELEASE.md +15 -36
- package/hooks/verify-package.sh +5 -1
- package/package.json +6 -6
package/dist/cli.js
CHANGED
|
@@ -1,18 +1,25 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { existsSync, readFileSync, readdirSync } from "fs";
|
|
2
|
+
import { closeSync, existsSync, fstatSync, openSync, readFileSync, readSync, readdirSync } from "fs";
|
|
3
3
|
import { resolve } from "path";
|
|
4
|
-
import {
|
|
4
|
+
import { execSync } from "child_process";
|
|
5
|
+
import { MAX_DRAFTS } from "./constants.js";
|
|
6
|
+
import { printJson, resolveRepo, parseRunState, parsePositiveInt, sanitizeForTerminal, getInstalledPackagePath, getInstalledPackageInfo, readUpdateCache, getGlobalNpmPrefix, readGoalDoc, atomicWriteTextInRepo } from "./helpers.js";
|
|
5
7
|
const VERSION_FLAGS = ["--version", "-v"];
|
|
6
8
|
const HELP_FLAGS = ["--help", "-h", "help"];
|
|
9
|
+
const BRANCH_POLICIES = ["best", "roulette", "diverse"];
|
|
7
10
|
const usage = () => {
|
|
8
11
|
console.error("Usage: autoresearch <command> [options]");
|
|
9
12
|
console.error("");
|
|
10
13
|
console.error("Commands:");
|
|
11
14
|
console.error(" init Initialize a run");
|
|
15
|
+
console.error(" goal Manage goal definitions (subcommands: init)");
|
|
12
16
|
console.error(" wizard Generate a setup summary");
|
|
13
17
|
console.error(" status Print run status");
|
|
14
18
|
console.error(" explain Human-readable run state");
|
|
19
|
+
console.error(" goal Show or validate the goal document");
|
|
15
20
|
console.error(" history Show recent iteration log");
|
|
21
|
+
console.error(" scores Show score trend history");
|
|
22
|
+
console.error(" score Run the configured scorer and show normalized output");
|
|
16
23
|
console.error(" config Show runtime configuration");
|
|
17
24
|
console.error(" summary Aggregate stats across runs");
|
|
18
25
|
console.error(" suggest Suggest next goal from memory");
|
|
@@ -27,18 +34,30 @@ const usage = () => {
|
|
|
27
34
|
console.error("Options:");
|
|
28
35
|
console.error(" --repo Repository root (default: current directory)");
|
|
29
36
|
console.error(" --goal Desired run outcome");
|
|
30
|
-
console.error(" --metric Metric name to track");
|
|
31
|
-
console.error(" --direction lower or higher");
|
|
37
|
+
console.error(" --metric Metric name to track (default outcome metric)");
|
|
38
|
+
console.error(" --direction lower or higher (for outcome metric)");
|
|
39
|
+
console.error(" --outcome-metric Primary metric for keep decisions");
|
|
40
|
+
console.error(" --outcome-direction Direction for outcome metric");
|
|
41
|
+
console.error(" --instrument-metric Measurement quality/risk metric (surfaced separately)");
|
|
42
|
+
console.error(" --instrument-direction Direction for instrument metric");
|
|
43
|
+
console.error(" --instrument-value Recorded value for the instrument metric");
|
|
44
|
+
console.error(" --scorer-status ok, ok-low-score, or scorer-broken (default: ok)");
|
|
32
45
|
console.error(" --verify Mechanical verification command");
|
|
33
46
|
console.error(" --guard Guard command for regression catch");
|
|
47
|
+
console.error(" --scorer Scorer command (outputs JSON with score and max fields)");
|
|
34
48
|
console.error(" --mode foreground or background");
|
|
35
49
|
console.error(" --scope In-scope files or subsystem");
|
|
36
50
|
console.error(" --iterations Iteration cap");
|
|
51
|
+
console.error(" --max-no-progress Max consecutive discards before stop");
|
|
37
52
|
console.error(" --duration Wall-clock cap (e.g., 5h or 300m)");
|
|
53
|
+
console.error(` --num-drafts Number of parallel drafts (default: 1, max: ${MAX_DRAFTS})`);
|
|
54
|
+
console.error(" --branch-policy Branch selection policy: best, roulette, diverse");
|
|
38
55
|
console.error(" --json Output raw JSON (default: human-readable)");
|
|
39
56
|
console.error(" --results-path Custom results TSV path");
|
|
40
57
|
console.error(" --state-path Custom state JSON path");
|
|
41
58
|
console.error(" --fresh-start Archive previous artifacts before starting");
|
|
59
|
+
console.error(" --goal-path Output path for GOAL.md (used by goal init)");
|
|
60
|
+
console.error(" --template Goal template: performance, quality, coverage, custom (used by goal init)");
|
|
42
61
|
console.error("");
|
|
43
62
|
console.error("Flags:");
|
|
44
63
|
console.error(" -h, --help Show this help");
|
|
@@ -49,9 +68,12 @@ const usage = () => {
|
|
|
49
68
|
console.error("Examples:");
|
|
50
69
|
console.error(" autoresearch wizard --goal \"optimize response time\"");
|
|
51
70
|
console.error(" autoresearch init --goal \"reduce errors\" --metric errors --direction lower --verify \"npm test\"");
|
|
71
|
+
console.error(" autoresearch goal init --goal \"reduce errors\" --metric errors --direction lower --verify \"npm test\"");
|
|
72
|
+
console.error(" autoresearch goal init --template performance");
|
|
52
73
|
console.error(" autoresearch status");
|
|
53
74
|
console.error(" autoresearch explain");
|
|
54
75
|
console.error(" autoresearch history");
|
|
76
|
+
console.error(" autoresearch score --scorer \"node score.js\"");
|
|
55
77
|
};
|
|
56
78
|
const parseArgs = (args) => {
|
|
57
79
|
const result = {};
|
|
@@ -70,6 +92,8 @@ const parseArgs = (args) => {
|
|
|
70
92
|
r: "repo", g: "goal", m: "metric", d: "direction",
|
|
71
93
|
v: "verify", n: "guard", o: "mode", s: "scope",
|
|
72
94
|
i: "iterations", t: "duration",
|
|
95
|
+
f: "num-drafts", b: "branch-policy",
|
|
96
|
+
p: "max-no-progress",
|
|
73
97
|
};
|
|
74
98
|
const key = shortToLong[args[i][1]] ?? args[i].slice(1);
|
|
75
99
|
if (i + 1 < args.length && !args[i + 1].startsWith("-")) {
|
|
@@ -82,6 +106,12 @@ const parseArgs = (args) => {
|
|
|
82
106
|
}
|
|
83
107
|
return result;
|
|
84
108
|
};
|
|
109
|
+
const tsvField = (headers, cols, field, legacyIndex) => {
|
|
110
|
+
const fieldIndex = headers.indexOf(field);
|
|
111
|
+
if (fieldIndex >= 0)
|
|
112
|
+
return cols[fieldIndex] ?? "";
|
|
113
|
+
return cols[legacyIndex] ?? "";
|
|
114
|
+
};
|
|
85
115
|
const markdownInlineEscapes = {
|
|
86
116
|
"\\": "\\\\",
|
|
87
117
|
"`": "\\`",
|
|
@@ -123,6 +153,21 @@ const formatDisplayValue = (val) => {
|
|
|
123
153
|
return "—";
|
|
124
154
|
return sanitizeForTerminal(val);
|
|
125
155
|
};
|
|
156
|
+
const parseMemoryPatternHeading = (heading) => {
|
|
157
|
+
const raw = heading.replace(/^### Pattern: /, "");
|
|
158
|
+
const trimmed = raw.trimEnd();
|
|
159
|
+
if (trimmed.startsWith('"')) {
|
|
160
|
+
try {
|
|
161
|
+
const parsed = JSON.parse(trimmed);
|
|
162
|
+
if (typeof parsed === "string")
|
|
163
|
+
return parsed;
|
|
164
|
+
}
|
|
165
|
+
catch {
|
|
166
|
+
// Fall through to the raw heading text for backward compatibility.
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
return trimmed;
|
|
170
|
+
};
|
|
126
171
|
const formatMetricValue = formatDisplayValue;
|
|
127
172
|
const formatTimestamp = (ts) => {
|
|
128
173
|
try {
|
|
@@ -133,6 +178,46 @@ const formatTimestamp = (ts) => {
|
|
|
133
178
|
return ts;
|
|
134
179
|
}
|
|
135
180
|
};
|
|
181
|
+
const readTailLines = (filePath, limit) => {
|
|
182
|
+
if (limit <= 0)
|
|
183
|
+
return [];
|
|
184
|
+
const fd = openSync(filePath, "r");
|
|
185
|
+
try {
|
|
186
|
+
const size = fstatSync(fd).size;
|
|
187
|
+
if (size === 0)
|
|
188
|
+
return [];
|
|
189
|
+
const chunkSize = 64 * 1024;
|
|
190
|
+
const lines = [];
|
|
191
|
+
let position = size;
|
|
192
|
+
let remainder = Buffer.alloc(0);
|
|
193
|
+
while (position > 0 && lines.length < limit) {
|
|
194
|
+
const bytesToRead = Math.min(chunkSize, position);
|
|
195
|
+
position -= bytesToRead;
|
|
196
|
+
const chunk = Buffer.alloc(bytesToRead);
|
|
197
|
+
const bytesRead = readSync(fd, chunk, 0, bytesToRead, position);
|
|
198
|
+
const data = Buffer.concat([chunk.subarray(0, bytesRead), remainder]);
|
|
199
|
+
let end = data.length;
|
|
200
|
+
for (let i = data.length - 1; i >= 0 && lines.length < limit; i -= 1) {
|
|
201
|
+
if (data[i] === 0x0a) {
|
|
202
|
+
const line = data.subarray(i + 1, end).toString("utf-8").trim();
|
|
203
|
+
if (line.length > 0)
|
|
204
|
+
lines.push(line);
|
|
205
|
+
end = i;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
remainder = data.subarray(0, end);
|
|
209
|
+
}
|
|
210
|
+
if (lines.length < limit) {
|
|
211
|
+
const line = remainder.toString("utf-8").trim();
|
|
212
|
+
if (line.length > 0)
|
|
213
|
+
lines.push(line);
|
|
214
|
+
}
|
|
215
|
+
return lines.reverse();
|
|
216
|
+
}
|
|
217
|
+
finally {
|
|
218
|
+
closeSync(fd);
|
|
219
|
+
}
|
|
220
|
+
};
|
|
136
221
|
const markdownEscapePattern = /([\\`*_{}[\]()#+\-.!|>])/g;
|
|
137
222
|
const terminalControlPattern = /\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~]|\][^\x07]*(?:\x07|\x1B\\))/g;
|
|
138
223
|
const controlCharacterPattern = /[\u0000-\u0008\u000B\u000C\u000E-\u001F\u007F-\u009F]/g;
|
|
@@ -148,6 +233,13 @@ const sanitizeMarkdownText = (value) => {
|
|
|
148
233
|
const formatMarkdownField = (value) => {
|
|
149
234
|
return sanitizeMarkdownText(value).replace(markdownEscapePattern, "\\$1");
|
|
150
235
|
};
|
|
236
|
+
const normalizeBranchPolicy = (value) => {
|
|
237
|
+
if (value == null || value === "")
|
|
238
|
+
return "best";
|
|
239
|
+
if (BRANCH_POLICIES.includes(value))
|
|
240
|
+
return value;
|
|
241
|
+
throw new Error(`Invalid branch policy: ${value}. Expected one of: ${BRANCH_POLICIES.join(", ")}`);
|
|
242
|
+
};
|
|
151
243
|
const main = async () => {
|
|
152
244
|
const args = process.argv.slice(2);
|
|
153
245
|
// Handle standalone flags
|
|
@@ -193,6 +285,7 @@ const main = async () => {
|
|
|
193
285
|
guard: grouped.guard,
|
|
194
286
|
mode: grouped.mode,
|
|
195
287
|
iterations: parsePositiveInt(grouped.iterations, "iterations"),
|
|
288
|
+
max_no_progress: parsePositiveInt(grouped["max-no-progress"], "max-no-progress"),
|
|
196
289
|
duration: grouped.duration,
|
|
197
290
|
memory_path: grouped["memory-path"],
|
|
198
291
|
required_keep_labels: grouped["required-keep-labels"],
|
|
@@ -219,13 +312,15 @@ const main = async () => {
|
|
|
219
312
|
const { initializeRun } = await import("./run-manager.js");
|
|
220
313
|
const config = {
|
|
221
314
|
goal: grouped.goal,
|
|
222
|
-
metric: grouped.metric,
|
|
223
|
-
direction: grouped.direction || "lower",
|
|
315
|
+
metric: (grouped.metric || grouped["outcome-metric"]),
|
|
316
|
+
direction: (grouped.direction || grouped["outcome-direction"]) || "lower",
|
|
224
317
|
verify: grouped.verify,
|
|
225
318
|
mode: grouped.mode || "foreground",
|
|
226
319
|
scope: grouped.scope,
|
|
227
320
|
guard: grouped.guard,
|
|
321
|
+
scorer: grouped.scorer,
|
|
228
322
|
iterations: parsePositiveInt(grouped.iterations, "iterations"),
|
|
323
|
+
max_no_progress: parsePositiveInt(grouped["max-no-progress"], "max-no-progress"),
|
|
229
324
|
duration: grouped.duration,
|
|
230
325
|
memory_path: grouped["memory-path"],
|
|
231
326
|
required_keep_labels: grouped["required-keep-labels"],
|
|
@@ -233,6 +328,12 @@ const main = async () => {
|
|
|
233
328
|
run_tag: grouped["run-tag"],
|
|
234
329
|
stop_condition: grouped["stop-condition"],
|
|
235
330
|
baseline: grouped.baseline,
|
|
331
|
+
num_drafts: parsePositiveInt(grouped["num-drafts"], "num_drafts", { max: MAX_DRAFTS }) ?? 1,
|
|
332
|
+
branch_selection_policy: normalizeBranchPolicy(grouped["branch-policy"]),
|
|
333
|
+
outcome_metric: grouped["outcome-metric"],
|
|
334
|
+
outcome_direction: grouped["outcome-direction"],
|
|
335
|
+
instrument_metric: grouped["instrument-metric"],
|
|
336
|
+
instrument_direction: grouped["instrument-direction"],
|
|
236
337
|
};
|
|
237
338
|
const state = await initializeRun(grouped.repo, grouped["results-path"], grouped["state-path"], config, grouped["fresh-start"] === "true");
|
|
238
339
|
printJson(state);
|
|
@@ -250,6 +351,7 @@ const main = async () => {
|
|
|
250
351
|
console.log(`Run: ${formatDisplayValue(s.run_id)}`);
|
|
251
352
|
console.log(`Status: ${formatDisplayValue(s.status)}`);
|
|
252
353
|
console.log(`Mode: ${formatDisplayValue(s.mode)}`);
|
|
354
|
+
console.log(`Op Mode: ${formatDisplayValue(s.operating_mode)}`);
|
|
253
355
|
console.log(`Goal: ${formatDisplayValue(s.goal)}`);
|
|
254
356
|
if (s.metric) {
|
|
255
357
|
const m = s.metric;
|
|
@@ -264,6 +366,13 @@ const main = async () => {
|
|
|
264
366
|
const lastIter = s.last_iteration;
|
|
265
367
|
if (lastIter && lastIter.iteration) {
|
|
266
368
|
console.log(`Last: iter ${formatDisplayValue(lastIter.iteration)} — ${formatDisplayValue(lastIter.decision)} (${formatMetricValue(lastIter.metric_value)})`);
|
|
369
|
+
if (lastIter.score_components != null && typeof lastIter.score_components === "object") {
|
|
370
|
+
const parts = Object.entries(lastIter.score_components)
|
|
371
|
+
.map(([k, v]) => `${formatDisplayValue(k)}:${typeof v === "number" ? v.toFixed(4) : formatDisplayValue(v)}`)
|
|
372
|
+
.join(", ");
|
|
373
|
+
if (parts.length > 0)
|
|
374
|
+
console.log(` Components: [${parts}]`);
|
|
375
|
+
}
|
|
267
376
|
}
|
|
268
377
|
const flags = s.flags;
|
|
269
378
|
if (flags?.needs_human)
|
|
@@ -296,6 +405,7 @@ const main = async () => {
|
|
|
296
405
|
console.log(` Goal: ${formatDisplayValue(s.goal)}`);
|
|
297
406
|
console.log(` Status: ${formatDisplayValue(s.status)}`);
|
|
298
407
|
console.log(` Mode: ${formatDisplayValue(s.mode)}`);
|
|
408
|
+
console.log(` Op Mode: ${formatDisplayValue(s.operating_mode)}`);
|
|
299
409
|
if (s.metric) {
|
|
300
410
|
const m = s.metric;
|
|
301
411
|
console.log(` Metric: ${formatDisplayValue(m.name)} → ${formatMetricValue(m.latest)} (best: ${formatMetricValue(m.best)}, dir: ${formatDisplayValue(m.direction)})`);
|
|
@@ -307,6 +417,13 @@ const main = async () => {
|
|
|
307
417
|
console.log(` Last iter: #${formatDisplayValue(lastIter.iteration)} — ${formatDisplayValue(lastIter.decision)}`);
|
|
308
418
|
if (lastIter.change_summary)
|
|
309
419
|
console.log(` Change: ${formatDisplayValue(lastIter.change_summary)}`);
|
|
420
|
+
if (lastIter.score_components != null && typeof lastIter.score_components === "object") {
|
|
421
|
+
const parts = Object.entries(lastIter.score_components)
|
|
422
|
+
.map(([k, v]) => `${formatDisplayValue(k)}:${typeof v === "number" ? v.toFixed(4) : formatDisplayValue(v)}`)
|
|
423
|
+
.join(", ");
|
|
424
|
+
if (parts.length > 0)
|
|
425
|
+
console.log(` Components: [${parts}]`);
|
|
426
|
+
}
|
|
310
427
|
}
|
|
311
428
|
if (flags?.needs_human)
|
|
312
429
|
console.log(" ⚠ Needs human review");
|
|
@@ -331,9 +448,9 @@ const main = async () => {
|
|
|
331
448
|
break;
|
|
332
449
|
}
|
|
333
450
|
const limit = parsePositiveInt(grouped.limit, "limit") ?? 10;
|
|
451
|
+
const headers = lines[0].split("\t");
|
|
334
452
|
const records = lines.slice(1).reverse().slice(0, limit);
|
|
335
453
|
if (useJson) {
|
|
336
|
-
const headers = lines[0].split("\t");
|
|
337
454
|
const parsed = records.map((r) => {
|
|
338
455
|
const cols = r.split("\t");
|
|
339
456
|
const obj = {};
|
|
@@ -347,14 +464,221 @@ const main = async () => {
|
|
|
347
464
|
}
|
|
348
465
|
for (const r of records) {
|
|
349
466
|
const cols = r.split("\t");
|
|
350
|
-
if (cols.length >=
|
|
351
|
-
const
|
|
352
|
-
|
|
467
|
+
if (cols.length >= 4) {
|
|
468
|
+
const decision = tsvField(headers, cols, "decision", 2);
|
|
469
|
+
const metricValue = tsvField(headers, cols, "metric_value", 3);
|
|
470
|
+
const emoji = decision === "keep" ? "✓" : decision === "discard" ? "✗" : "⚠";
|
|
471
|
+
const changeSummary = tsvField(headers, cols, "change_summary", 8);
|
|
472
|
+
console.log(`${emoji} #${formatDisplayValue(cols[1])} ${formatDisplayValue(decision)} (${formatMetricValue(metricValue)}) ${formatDisplayValue(changeSummary.substring(0, 60))}`);
|
|
353
473
|
}
|
|
354
474
|
}
|
|
355
475
|
console.log(`\nShowing ${Math.min(limit, records.length)} of ${lines.length - 1} records.`);
|
|
356
476
|
break;
|
|
357
477
|
}
|
|
478
|
+
case "scores": {
|
|
479
|
+
const { resolvePath } = await import("./helpers.js");
|
|
480
|
+
const { SCORE_HISTORY_DEFAULT } = await import("./constants.js");
|
|
481
|
+
const scoreHistoryPath = resolvePath(grouped.repo, grouped["score-history-path"], SCORE_HISTORY_DEFAULT);
|
|
482
|
+
if (!existsSync(scoreHistoryPath)) {
|
|
483
|
+
console.log("No score history found.");
|
|
484
|
+
break;
|
|
485
|
+
}
|
|
486
|
+
const limit = parsePositiveInt(grouped.limit, "limit") ?? 10;
|
|
487
|
+
const showTopComponents = grouped["top-components"] === "true";
|
|
488
|
+
if (showTopComponents) {
|
|
489
|
+
const allLines = readFileSync(scoreHistoryPath, "utf-8")
|
|
490
|
+
.split("\n")
|
|
491
|
+
.map((l) => l.trim())
|
|
492
|
+
.filter(Boolean);
|
|
493
|
+
const allParsed = allLines.map((r) => {
|
|
494
|
+
try {
|
|
495
|
+
return JSON.parse(r);
|
|
496
|
+
}
|
|
497
|
+
catch {
|
|
498
|
+
return null;
|
|
499
|
+
}
|
|
500
|
+
}).filter(Boolean);
|
|
501
|
+
if (allParsed.length === 0) {
|
|
502
|
+
console.log("No score records yet.");
|
|
503
|
+
break;
|
|
504
|
+
}
|
|
505
|
+
const { rankComponents } = await import("./score-parser.js");
|
|
506
|
+
const ranking = rankComponents(allParsed);
|
|
507
|
+
if (useJson) {
|
|
508
|
+
printJson({ count: allParsed.length, scores: allParsed.slice(-limit), ranking });
|
|
509
|
+
break;
|
|
510
|
+
}
|
|
511
|
+
console.log("Component Rankings:");
|
|
512
|
+
if (ranking.top_positive.length > 0) {
|
|
513
|
+
console.log(" Top improving components:");
|
|
514
|
+
for (const c of ranking.top_positive) {
|
|
515
|
+
console.log(` + ${formatDisplayValue(c.name)} Δ+${c.delta.toFixed(4)}`);
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
if (ranking.top_negative.length > 0) {
|
|
519
|
+
console.log(" Top declining components:");
|
|
520
|
+
for (const c of ranking.top_negative) {
|
|
521
|
+
console.log(` - ${formatDisplayValue(c.name)} Δ${c.delta.toFixed(4)}`);
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
if (ranking.top_positive.length === 0 && ranking.top_negative.length === 0) {
|
|
525
|
+
console.log(" No component data found in score history.");
|
|
526
|
+
}
|
|
527
|
+
console.log(`\nAnalyzed ${allParsed.length} score records.`);
|
|
528
|
+
break;
|
|
529
|
+
}
|
|
530
|
+
const records = readTailLines(scoreHistoryPath, limit);
|
|
531
|
+
if (records.length === 0) {
|
|
532
|
+
console.log("No score records yet.");
|
|
533
|
+
break;
|
|
534
|
+
}
|
|
535
|
+
if (useJson) {
|
|
536
|
+
const parsed = records.map((r) => {
|
|
537
|
+
try {
|
|
538
|
+
return JSON.parse(r);
|
|
539
|
+
}
|
|
540
|
+
catch {
|
|
541
|
+
return null;
|
|
542
|
+
}
|
|
543
|
+
}).filter(Boolean);
|
|
544
|
+
printJson({ count: parsed.length, scores: parsed });
|
|
545
|
+
break;
|
|
546
|
+
}
|
|
547
|
+
console.log("Score History (latest " + Math.min(limit, records.length) + "):");
|
|
548
|
+
const recordsOrdered = records.slice().reverse();
|
|
549
|
+
const parseMetricNumber = (value) => {
|
|
550
|
+
if (typeof value === "number") {
|
|
551
|
+
return Number.isFinite(value) ? value : null;
|
|
552
|
+
}
|
|
553
|
+
if (typeof value === "string") {
|
|
554
|
+
const parsed = Number(value);
|
|
555
|
+
return Number.isFinite(parsed) ? parsed : null;
|
|
556
|
+
}
|
|
557
|
+
return null;
|
|
558
|
+
};
|
|
559
|
+
for (let i = 0; i < recordsOrdered.length; i += 1) {
|
|
560
|
+
const r = recordsOrdered[i];
|
|
561
|
+
try {
|
|
562
|
+
const rec = JSON.parse(r);
|
|
563
|
+
let trend = "";
|
|
564
|
+
if (i + 1 < recordsOrdered.length) {
|
|
565
|
+
try {
|
|
566
|
+
const previousRec = JSON.parse(recordsOrdered[i + 1]);
|
|
567
|
+
const currentMetricValue = parseMetricNumber(rec.metric_value);
|
|
568
|
+
const previousMetricValue = parseMetricNumber(previousRec.metric_value);
|
|
569
|
+
if (currentMetricValue !== null && previousMetricValue !== null) {
|
|
570
|
+
if (currentMetricValue === previousMetricValue) {
|
|
571
|
+
trend = "→";
|
|
572
|
+
}
|
|
573
|
+
else if (rec.metric_direction === "higher") {
|
|
574
|
+
trend = currentMetricValue > previousMetricValue ? "↑" : "↓";
|
|
575
|
+
}
|
|
576
|
+
else {
|
|
577
|
+
trend = currentMetricValue < previousMetricValue ? "↑" : "↓";
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
catch {
|
|
582
|
+
trend = "";
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
let componentLine = "";
|
|
586
|
+
if (rec.score_components != null && typeof rec.score_components === "object") {
|
|
587
|
+
const parts = Object.entries(rec.score_components)
|
|
588
|
+
.map(([k, v]) => `${formatDisplayValue(k)}:${typeof v === "number" ? v.toFixed(4) : formatDisplayValue(v)}`)
|
|
589
|
+
.join(", ");
|
|
590
|
+
if (parts.length > 0)
|
|
591
|
+
componentLine = ` [${parts}]`;
|
|
592
|
+
}
|
|
593
|
+
let componentDeltaLine = "";
|
|
594
|
+
if (componentLine && i + 1 < recordsOrdered.length) {
|
|
595
|
+
try {
|
|
596
|
+
const prevRec = JSON.parse(recordsOrdered[i + 1]);
|
|
597
|
+
if (prevRec.score_components != null && typeof prevRec.score_components === "object") {
|
|
598
|
+
const deltas = [];
|
|
599
|
+
for (const [k, v] of Object.entries(rec.score_components)) {
|
|
600
|
+
const prev = prevRec.score_components[k];
|
|
601
|
+
if (typeof prev === "number" && typeof v === "number") {
|
|
602
|
+
const d = v - prev;
|
|
603
|
+
if (d !== 0) {
|
|
604
|
+
deltas.push(`${formatDisplayValue(k)}:${d > 0 ? "+" : ""}${d.toFixed(4)}`);
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
if (deltas.length > 0)
|
|
609
|
+
componentDeltaLine = ` Δ[${deltas.join(", ")}]`;
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
catch {
|
|
613
|
+
// ignore delta parse errors
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
console.log(` #${rec.iteration} ${trend} ${rec.metric_value ?? "—"} (${rec.decision}) ${rec.verify_status}${componentLine}${componentDeltaLine}`);
|
|
617
|
+
}
|
|
618
|
+
catch {
|
|
619
|
+
console.log(` [parse error]`);
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
console.log(`\nShowing ${records.length} score records.`);
|
|
623
|
+
break;
|
|
624
|
+
}
|
|
625
|
+
case "score": {
|
|
626
|
+
const { resolvePath, readJsonFile, AutoresearchError: AErr } = await import("./helpers.js");
|
|
627
|
+
const { STATE_DEFAULT } = await import("./constants.js");
|
|
628
|
+
const { parseScoreOutput } = await import("./score-parser.js");
|
|
629
|
+
// Resolve scorer: --scorer flag takes priority, else use state.scorer
|
|
630
|
+
let scorerCmd = grouped.scorer;
|
|
631
|
+
if (!scorerCmd) {
|
|
632
|
+
const statePath = resolvePath(grouped.repo, grouped["state-path"], STATE_DEFAULT);
|
|
633
|
+
if (existsSync(statePath)) {
|
|
634
|
+
const state = parseRunState(readJsonFile(statePath));
|
|
635
|
+
scorerCmd = state.scorer;
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
if (!scorerCmd) {
|
|
639
|
+
throw new AErr("No scorer configured. Provide --scorer <cmd> or configure a scorer via autoresearch init --scorer <cmd>.");
|
|
640
|
+
}
|
|
641
|
+
const repoBase = resolveRepo(grouped.repo);
|
|
642
|
+
let rawOutput;
|
|
643
|
+
try {
|
|
644
|
+
rawOutput = execSync(scorerCmd, { encoding: "utf-8", cwd: repoBase, stdio: ["ignore", "pipe", "pipe"] });
|
|
645
|
+
}
|
|
646
|
+
catch (err) {
|
|
647
|
+
const e = err;
|
|
648
|
+
const stderr = typeof e.stderr === "string" ? e.stderr.trim() : (Buffer.isBuffer(e.stderr) ? e.stderr.toString("utf-8").trim() : "");
|
|
649
|
+
const errMsg = stderr || (err instanceof Error ? err.message : String(err));
|
|
650
|
+
throw new AErr(`Scorer command failed: ${errMsg}`);
|
|
651
|
+
}
|
|
652
|
+
const scored = parseScoreOutput(rawOutput);
|
|
653
|
+
const normalized = scored.score / scored.max;
|
|
654
|
+
const percent = (normalized * 100).toFixed(1) + "%";
|
|
655
|
+
if (useJson) {
|
|
656
|
+
printJson({
|
|
657
|
+
score: scored.score,
|
|
658
|
+
max: scored.max,
|
|
659
|
+
normalized,
|
|
660
|
+
percent,
|
|
661
|
+
components: scored.components ?? null,
|
|
662
|
+
diagnostics: scored.diagnostics ?? null,
|
|
663
|
+
details: scored.details ?? null,
|
|
664
|
+
});
|
|
665
|
+
break;
|
|
666
|
+
}
|
|
667
|
+
console.log(`Score: ${scored.score} / ${scored.max} (${percent})`);
|
|
668
|
+
if (scored.components && Object.keys(scored.components).length > 0) {
|
|
669
|
+
console.log("Components:");
|
|
670
|
+
for (const [key, val] of Object.entries(scored.components)) {
|
|
671
|
+
console.log(` ${formatDisplayValue(key)}: ${formatDisplayValue(val)}`);
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
if (scored.diagnostics && Object.keys(scored.diagnostics).length > 0) {
|
|
675
|
+
console.log("Diagnostics:");
|
|
676
|
+
for (const [key, val] of Object.entries(scored.diagnostics)) {
|
|
677
|
+
console.log(` ${formatDisplayValue(key)}: ${formatDisplayValue(val)}`);
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
break;
|
|
681
|
+
}
|
|
358
682
|
case "config": {
|
|
359
683
|
const { resolvePath, readJsonFile } = await import("./helpers.js");
|
|
360
684
|
const { STATE_DEFAULT } = await import("./constants.js");
|
|
@@ -374,6 +698,7 @@ const main = async () => {
|
|
|
374
698
|
deadline_at: state.deadline_at,
|
|
375
699
|
verify: state.verify,
|
|
376
700
|
guard: state.guard,
|
|
701
|
+
scorer: state.scorer ?? null,
|
|
377
702
|
subagent_pool: state.subagent_pool ? "configured" : "none",
|
|
378
703
|
label_requirements: state.label_requirements,
|
|
379
704
|
});
|
|
@@ -382,6 +707,7 @@ const main = async () => {
|
|
|
382
707
|
console.log("Run Configuration:");
|
|
383
708
|
console.log(` Goal: ${formatDisplayValue(state.goal)}`);
|
|
384
709
|
console.log(` Mode: ${formatDisplayValue(state.mode)}`);
|
|
710
|
+
console.log(` Op Mode: ${formatDisplayValue(state.operating_mode)}`);
|
|
385
711
|
if (state.metric) {
|
|
386
712
|
const m = state.metric;
|
|
387
713
|
console.log(` Metric: ${formatDisplayValue(m.name)} (${formatDisplayValue(m.direction)})`);
|
|
@@ -391,6 +717,7 @@ const main = async () => {
|
|
|
391
717
|
console.log(` Deadline: ${formatDisplayValue(state.deadline_at ? formatTimestamp(state.deadline_at) : "—")}`);
|
|
392
718
|
console.log(` Verify: ${formatDisplayValue(state.verify)}`);
|
|
393
719
|
console.log(` Guard: ${formatDisplayValue(state.guard)}`);
|
|
720
|
+
console.log(` Scorer: ${formatDisplayValue(state.scorer ?? "—")}`);
|
|
394
721
|
console.log(` Pool: ${state.subagent_pool ? "configured" : "none"}`);
|
|
395
722
|
break;
|
|
396
723
|
}
|
|
@@ -445,8 +772,8 @@ const main = async () => {
|
|
|
445
772
|
const errors = [];
|
|
446
773
|
if (!grouped.goal)
|
|
447
774
|
errors.push("Missing required: --goal");
|
|
448
|
-
if (!grouped.metric)
|
|
449
|
-
errors.push("Missing required: --metric");
|
|
775
|
+
if (!grouped.metric && !grouped["outcome-metric"])
|
|
776
|
+
errors.push("Missing required: --metric or --outcome-metric");
|
|
450
777
|
try {
|
|
451
778
|
if (grouped.direction)
|
|
452
779
|
normalizeDirection(grouped.direction);
|
|
@@ -470,7 +797,7 @@ const main = async () => {
|
|
|
470
797
|
if (errors.length === 0) {
|
|
471
798
|
console.log("✓ Configuration is valid");
|
|
472
799
|
console.log(` Goal: ${grouped.goal}`);
|
|
473
|
-
console.log(` Metric: ${grouped.metric} (${grouped.direction || "lower"})`);
|
|
800
|
+
console.log(` Metric: ${grouped.metric || grouped["outcome-metric"]} (${grouped.direction || grouped["outcome-direction"] || "lower"})`);
|
|
474
801
|
console.log(` Verify: ${grouped.verify}`);
|
|
475
802
|
console.log(` Mode: ${grouped.mode || "foreground"}`);
|
|
476
803
|
}
|
|
@@ -494,9 +821,12 @@ const main = async () => {
|
|
|
494
821
|
}
|
|
495
822
|
const state = parseRunState(readJsonFile(statePath));
|
|
496
823
|
let results = [];
|
|
824
|
+
let resultHeaders = [];
|
|
497
825
|
if (existsSync(resultsPath)) {
|
|
498
826
|
const content = readFileSync(resultsPath, "utf-8");
|
|
499
|
-
|
|
827
|
+
const resultLines = content.trim().split("\n");
|
|
828
|
+
resultHeaders = resultLines[0]?.split("\t") ?? [];
|
|
829
|
+
results = resultLines.slice(1).filter(Boolean);
|
|
500
830
|
}
|
|
501
831
|
if (useJson) {
|
|
502
832
|
printJson({ state, results_count: results.length });
|
|
@@ -507,6 +837,7 @@ const main = async () => {
|
|
|
507
837
|
console.log(`**Goal:** ${formatMarkdownField(state.goal)}`);
|
|
508
838
|
console.log(`**Status:** ${formatMarkdownField(state.status)}`);
|
|
509
839
|
console.log(`**Mode:** ${formatMarkdownField(state.mode)}`);
|
|
840
|
+
console.log(`**Op Mode:** ${formatMarkdownField(state.operating_mode)}`);
|
|
510
841
|
if (state.metric) {
|
|
511
842
|
const m = state.metric;
|
|
512
843
|
console.log(`**Metric:** ${formatMarkdownField(m.name)} (${formatMarkdownField(m.direction)})`);
|
|
@@ -524,8 +855,11 @@ const main = async () => {
|
|
|
524
855
|
console.log(`\n## Iterations`);
|
|
525
856
|
for (const r of results) {
|
|
526
857
|
const cols = r.split("\t");
|
|
527
|
-
if (cols.length >=
|
|
528
|
-
|
|
858
|
+
if (cols.length >= 4) {
|
|
859
|
+
const decision = tsvField(resultHeaders, cols, "decision", 2);
|
|
860
|
+
const metricValue = tsvField(resultHeaders, cols, "metric_value", 3);
|
|
861
|
+
const changeSummary = tsvField(resultHeaders, cols, "change_summary", 8);
|
|
862
|
+
console.log(`- ${formatMarkdownField(cols[1])}: ${formatMarkdownField(decision)} (${formatMarkdownField(metricValue)}) — ${formatMarkdownField(changeSummary).substring(0, 60)}`);
|
|
529
863
|
}
|
|
530
864
|
}
|
|
531
865
|
}
|
|
@@ -541,15 +875,16 @@ const main = async () => {
|
|
|
541
875
|
}
|
|
542
876
|
const memory = readFileSync(memoryPath, "utf-8");
|
|
543
877
|
const patterns = memory.match(/### Pattern: [^\n]+/g) ?? [];
|
|
878
|
+
const suggestions = patterns.map(parseMemoryPatternHeading);
|
|
544
879
|
if (useJson) {
|
|
545
|
-
printJson({ patterns_found:
|
|
880
|
+
printJson({ patterns_found: suggestions.length, suggestions });
|
|
546
881
|
break;
|
|
547
882
|
}
|
|
548
883
|
console.log("Memory Patterns — candidate next goals:");
|
|
549
|
-
for (const
|
|
550
|
-
console.log(` → ${formatDisplayValue(
|
|
884
|
+
for (const suggestion of suggestions) {
|
|
885
|
+
console.log(` → ${formatDisplayValue(suggestion)}`);
|
|
551
886
|
}
|
|
552
|
-
console.log(`\n${
|
|
887
|
+
console.log(`\n${suggestions.length} patterns available. Use 'autoresearch init --goal "..."' to start a new run.`);
|
|
553
888
|
break;
|
|
554
889
|
}
|
|
555
890
|
case "export": {
|
|
@@ -611,8 +946,8 @@ const main = async () => {
|
|
|
611
946
|
}
|
|
612
947
|
case "completion": {
|
|
613
948
|
const shell = grouped.shell || "bash";
|
|
614
|
-
const commands = ["init", "wizard", "status", "explain", "history", "config", "summary", "suggest", "launch", "complete", "stop", "resume", "record", "doctor", "export", "completion", "help"];
|
|
615
|
-
const options = ["--repo", "--goal", "--metric", "--direction", "--verify", "--guard", "--mode", "--scope", "--iterations", "--duration", "--json", "--results-path", "--state-path", "--fresh-start", "--memory-path", "--format", "--shell"];
|
|
949
|
+
const commands = ["init", "goal", "wizard", "status", "explain", "history", "config", "summary", "suggest", "launch", "complete", "stop", "resume", "record", "doctor", "export", "completion", "help"];
|
|
950
|
+
const options = ["--repo", "--goal", "--metric", "--direction", "--verify", "--guard", "--mode", "--scope", "--iterations", "--duration", "--num-drafts", "--branch-policy", "--json", "--results-path", "--state-path", "--fresh-start", "--memory-path", "--format", "--shell", "--goal-path", "--template"];
|
|
616
951
|
if (shell === "bash" || shell === "zsh") {
|
|
617
952
|
console.log(`# Auto Research CLI completion for ${shell}`);
|
|
618
953
|
console.log(`_autoresearch() {`);
|
|
@@ -647,13 +982,15 @@ const main = async () => {
|
|
|
647
982
|
const { LAUNCH_DEFAULT } = await import("./constants.js");
|
|
648
983
|
const config = {
|
|
649
984
|
goal: grouped.goal,
|
|
650
|
-
metric: grouped.metric,
|
|
651
|
-
direction: grouped.direction || "lower",
|
|
985
|
+
metric: (grouped.metric || grouped["outcome-metric"]),
|
|
986
|
+
direction: (grouped.direction || grouped["outcome-direction"]) || "lower",
|
|
652
987
|
verify: grouped.verify,
|
|
653
988
|
mode: "background",
|
|
654
989
|
scope: grouped.scope,
|
|
655
990
|
guard: grouped.guard,
|
|
991
|
+
scorer: grouped.scorer,
|
|
656
992
|
iterations: parsePositiveInt(grouped.iterations, "iterations"),
|
|
993
|
+
max_no_progress: parsePositiveInt(grouped["max-no-progress"], "max-no-progress"),
|
|
657
994
|
duration: grouped.duration,
|
|
658
995
|
memory_path: grouped["memory-path"],
|
|
659
996
|
required_keep_labels: grouped["required-keep-labels"],
|
|
@@ -661,6 +998,12 @@ const main = async () => {
|
|
|
661
998
|
run_tag: grouped["run-tag"],
|
|
662
999
|
stop_condition: grouped["stop-condition"],
|
|
663
1000
|
baseline: grouped.baseline,
|
|
1001
|
+
num_drafts: parsePositiveInt(grouped["num-drafts"], "num_drafts", { max: MAX_DRAFTS }) ?? 1,
|
|
1002
|
+
branch_selection_policy: normalizeBranchPolicy(grouped["branch-policy"]),
|
|
1003
|
+
outcome_metric: grouped["outcome-metric"],
|
|
1004
|
+
outcome_direction: grouped["outcome-direction"],
|
|
1005
|
+
instrument_metric: grouped["instrument-metric"],
|
|
1006
|
+
instrument_direction: grouped["instrument-direction"],
|
|
664
1007
|
};
|
|
665
1008
|
const launchPath = resolvePath(grouped.repo, grouped["launch-path"], LAUNCH_DEFAULT);
|
|
666
1009
|
if (dryRun) {
|
|
@@ -706,15 +1049,32 @@ const main = async () => {
|
|
|
706
1049
|
break;
|
|
707
1050
|
}
|
|
708
1051
|
case "record": {
|
|
709
|
-
const { normalizeResultStatus } = await import("./helpers.js");
|
|
1052
|
+
const { normalizeResultStatus, normalizeScorerStatus } = await import("./helpers.js");
|
|
710
1053
|
const vs = grouped["verify-status"] || "pass";
|
|
711
1054
|
const gs = grouped["guard-status"] || "skip";
|
|
1055
|
+
const scorerStatus = normalizeScorerStatus(grouped["scorer-status"]);
|
|
712
1056
|
const iteration = parsePositiveInt(grouped.iteration, "iteration");
|
|
1057
|
+
let scoreComponents;
|
|
1058
|
+
if (grouped["score-components"]) {
|
|
1059
|
+
try {
|
|
1060
|
+
const parsed = JSON.parse(grouped["score-components"]);
|
|
1061
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
1062
|
+
throw new Error('score-components must be a JSON object with string keys and numeric values, e.g., {"accuracy": 0.8, "coverage": 0.6}');
|
|
1063
|
+
}
|
|
1064
|
+
scoreComponents = parsed;
|
|
1065
|
+
}
|
|
1066
|
+
catch (e) {
|
|
1067
|
+
console.error(`Invalid --score-components: ${e.message}`);
|
|
1068
|
+
return 1;
|
|
1069
|
+
}
|
|
1070
|
+
}
|
|
713
1071
|
if (dryRun) {
|
|
714
1072
|
console.log("[dry-run] Would record experiment result:");
|
|
715
1073
|
console.log(JSON.stringify({
|
|
716
1074
|
decision: grouped.decision,
|
|
717
1075
|
metric_value: grouped["metric-value"],
|
|
1076
|
+
instrument_value: grouped["instrument-value"],
|
|
1077
|
+
scorer_status: scorerStatus,
|
|
718
1078
|
verify_status: normalizeResultStatus(vs, "verify_status"),
|
|
719
1079
|
guard_status: normalizeResultStatus(gs, "guard_status"),
|
|
720
1080
|
hypothesis: grouped.hypothesis,
|
|
@@ -722,11 +1082,12 @@ const main = async () => {
|
|
|
722
1082
|
labels: grouped.labels ? (Array.isArray(grouped.labels) ? grouped.labels : [grouped.labels]) : undefined,
|
|
723
1083
|
note: grouped.note,
|
|
724
1084
|
iteration,
|
|
1085
|
+
score_components: scoreComponents,
|
|
725
1086
|
}, null, 2));
|
|
726
1087
|
return 0;
|
|
727
1088
|
}
|
|
728
1089
|
const { appendIteration } = await import("./run-manager.js");
|
|
729
|
-
const state = await appendIteration(grouped.repo, grouped["results-path"], grouped["state-path"], grouped.decision, grouped["metric-value"], normalizeResultStatus(vs, "verify_status"), normalizeResultStatus(gs, "guard_status"), grouped.hypothesis, grouped["change-summary"], grouped.labels ? (Array.isArray(grouped.labels) ? grouped.labels : [grouped.labels]) : undefined, grouped.note, iteration);
|
|
1090
|
+
const state = await appendIteration(grouped.repo, grouped["results-path"], grouped["state-path"], grouped.decision, grouped["metric-value"], grouped["instrument-value"], normalizeResultStatus(vs, "verify_status"), normalizeResultStatus(gs, "guard_status"), grouped.hypothesis, grouped["change-summary"], grouped.labels ? (Array.isArray(grouped.labels) ? grouped.labels : [grouped.labels]) : undefined, grouped.note, iteration, undefined, scorerStatus, scoreComponents);
|
|
730
1091
|
printJson(state);
|
|
731
1092
|
break;
|
|
732
1093
|
}
|
|
@@ -821,6 +1182,197 @@ const main = async () => {
|
|
|
821
1182
|
console.log(`\nAll ${checks.length} checks passed.`);
|
|
822
1183
|
break;
|
|
823
1184
|
}
|
|
1185
|
+
case "goal": {
|
|
1186
|
+
const rawSubCmd = cmdArgs[0];
|
|
1187
|
+
const subCmd = rawSubCmd && !rawSubCmd.startsWith("-") ? rawSubCmd : undefined;
|
|
1188
|
+
if ((!subCmd && cmdArgs.length === 0) || subCmd === "help" || (subCmd && HELP_FLAGS.includes(subCmd))) {
|
|
1189
|
+
console.error("Usage: autoresearch goal <subcommand> [options]");
|
|
1190
|
+
console.error("");
|
|
1191
|
+
console.error("Subcommands:");
|
|
1192
|
+
console.error(" init Create a GOAL.md goal definition file");
|
|
1193
|
+
console.error("");
|
|
1194
|
+
console.error("Options (goal init):");
|
|
1195
|
+
console.error(" --goal Goal description");
|
|
1196
|
+
console.error(" --metric Metric name to track");
|
|
1197
|
+
console.error(" --direction lower or higher (default: lower)");
|
|
1198
|
+
console.error(" --verify Mechanical verification command");
|
|
1199
|
+
console.error(" --guard Guard command for regression catch");
|
|
1200
|
+
console.error(" --mode foreground or background (default: foreground)");
|
|
1201
|
+
console.error(" --scope In-scope files or subsystem");
|
|
1202
|
+
console.error(" --iterations Iteration cap");
|
|
1203
|
+
console.error(" --duration Wall-clock cap (e.g., 5h or 300m)");
|
|
1204
|
+
console.error(" --template Preset template: performance, quality, coverage, custom");
|
|
1205
|
+
console.error(" --goal-path Output file path (default: .autoresearch/goal.md)");
|
|
1206
|
+
console.error(" --dry-run Preview without writing the file");
|
|
1207
|
+
console.error(" --json Output result as JSON");
|
|
1208
|
+
console.error("");
|
|
1209
|
+
console.error("Examples:");
|
|
1210
|
+
console.error(" autoresearch goal init --goal \"reduce errors\" --metric failures --direction lower --verify \"npm test\"");
|
|
1211
|
+
console.error(" autoresearch goal init --template performance");
|
|
1212
|
+
console.error(" autoresearch goal init # interactive wizard");
|
|
1213
|
+
return 0;
|
|
1214
|
+
}
|
|
1215
|
+
if (!subCmd) {
|
|
1216
|
+
const { GOAL_DEFAULT } = await import("./constants.js");
|
|
1217
|
+
const { resolvePath } = await import("./helpers.js");
|
|
1218
|
+
const goalPath = resolvePath(grouped.repo, grouped["goal-path"], GOAL_DEFAULT);
|
|
1219
|
+
if (!existsSync(goalPath)) {
|
|
1220
|
+
console.log("No goal document found. Run 'autoresearch init' first.");
|
|
1221
|
+
break;
|
|
1222
|
+
}
|
|
1223
|
+
const doc = readGoalDoc(goalPath);
|
|
1224
|
+
if (useJson) {
|
|
1225
|
+
printJson(doc);
|
|
1226
|
+
break;
|
|
1227
|
+
}
|
|
1228
|
+
console.log(`Goal: ${formatDisplayValue(doc.goal)}`);
|
|
1229
|
+
console.log(`Metric: ${formatDisplayValue(doc.metric)} (${formatDisplayValue(doc.direction)})`);
|
|
1230
|
+
console.log(`Verify: ${formatDisplayValue(doc.verify)}`);
|
|
1231
|
+
if (doc.guard)
|
|
1232
|
+
console.log(`Guard: ${formatDisplayValue(doc.guard)}`);
|
|
1233
|
+
if (doc.file_map)
|
|
1234
|
+
console.log(`File map: ${formatDisplayValue(doc.file_map)}`);
|
|
1235
|
+
if (doc.constraints)
|
|
1236
|
+
console.log(`Constraints: ${formatDisplayValue(doc.constraints)}`);
|
|
1237
|
+
if (doc.stop_conditions)
|
|
1238
|
+
console.log(`Stop conditions: ${formatDisplayValue(doc.stop_conditions)}`);
|
|
1239
|
+
break;
|
|
1240
|
+
}
|
|
1241
|
+
if (subCmd !== "init") {
|
|
1242
|
+
console.error(`Unknown goal subcommand: ${subCmd}`);
|
|
1243
|
+
console.error("Run 'autoresearch goal help' for usage.");
|
|
1244
|
+
return 1;
|
|
1245
|
+
}
|
|
1246
|
+
const goalArgs = cmdArgs.slice(1);
|
|
1247
|
+
const goalParsed = parseArgs(goalArgs);
|
|
1248
|
+
const goalGrouped = {};
|
|
1249
|
+
for (const [k, v] of Object.entries(goalParsed)) {
|
|
1250
|
+
goalGrouped[k] = v;
|
|
1251
|
+
}
|
|
1252
|
+
const useGoalJson = goalGrouped.json === "true";
|
|
1253
|
+
const isGoalDryRun = goalGrouped["dry-run"] === "true";
|
|
1254
|
+
const { GOAL_TEMPLATES, getGoalTemplate, buildGoalDocument, buildGoalInitResult } = await import("./goal-init.js");
|
|
1255
|
+
const { GOAL_DEFAULT } = await import("./constants.js");
|
|
1256
|
+
const { resolvePath } = await import("./helpers.js");
|
|
1257
|
+
const { existsSync: goalExistsSync } = await import("fs");
|
|
1258
|
+
const templateId = goalGrouped.template ?? "custom";
|
|
1259
|
+
if (!GOAL_TEMPLATES.find((t) => t.id === templateId)) {
|
|
1260
|
+
console.error(`Unknown template: ${templateId}. Valid templates: ${GOAL_TEMPLATES.map((t) => t.id).join(", ")}`);
|
|
1261
|
+
return 1;
|
|
1262
|
+
}
|
|
1263
|
+
const template = getGoalTemplate(templateId);
|
|
1264
|
+
const templateDefaults = template?.defaults ?? {};
|
|
1265
|
+
let config = {
|
|
1266
|
+
goal: goalGrouped.goal ?? templateDefaults.goal,
|
|
1267
|
+
metric: goalGrouped.metric ?? templateDefaults.metric,
|
|
1268
|
+
direction: goalGrouped.direction ?? templateDefaults.direction,
|
|
1269
|
+
verify: goalGrouped.verify ?? templateDefaults.verify,
|
|
1270
|
+
guard: goalGrouped.guard ?? templateDefaults.guard,
|
|
1271
|
+
mode: goalGrouped.mode ?? templateDefaults.mode,
|
|
1272
|
+
scope: goalGrouped.scope ?? templateDefaults.scope,
|
|
1273
|
+
iterations: goalGrouped.iterations ? parsePositiveInt(goalGrouped.iterations, "iterations") : templateDefaults.iterations,
|
|
1274
|
+
duration: goalGrouped.duration ?? templateDefaults.duration,
|
|
1275
|
+
stop_condition: goalGrouped["stop-condition"] ?? templateDefaults.stop_condition,
|
|
1276
|
+
rollback_strategy: goalGrouped["rollback-strategy"] ?? templateDefaults.rollback_strategy,
|
|
1277
|
+
template: templateId,
|
|
1278
|
+
};
|
|
1279
|
+
const isTTY = process.stdin.isTTY === true;
|
|
1280
|
+
const hasRequiredFlags = Boolean(config.goal && config.metric && config.verify);
|
|
1281
|
+
if (!hasRequiredFlags && !isTTY) {
|
|
1282
|
+
// Non-interactive stdin: try to read JSON from stdin
|
|
1283
|
+
let stdinData = "";
|
|
1284
|
+
try {
|
|
1285
|
+
stdinData = await new Promise((resolve, reject) => {
|
|
1286
|
+
const chunks = [];
|
|
1287
|
+
process.stdin.on("data", (chunk) => chunks.push(chunk));
|
|
1288
|
+
process.stdin.on("end", () => resolve(Buffer.concat(chunks).toString("utf-8")));
|
|
1289
|
+
process.stdin.on("error", reject);
|
|
1290
|
+
// Resolve immediately if stdin is closed / empty
|
|
1291
|
+
setTimeout(() => resolve(""), 200);
|
|
1292
|
+
});
|
|
1293
|
+
stdinData = stdinData.trim();
|
|
1294
|
+
}
|
|
1295
|
+
catch {
|
|
1296
|
+
stdinData = "";
|
|
1297
|
+
}
|
|
1298
|
+
if (stdinData) {
|
|
1299
|
+
try {
|
|
1300
|
+
const parsed = JSON.parse(stdinData);
|
|
1301
|
+
config = { ...config, ...parsed, template: templateId };
|
|
1302
|
+
}
|
|
1303
|
+
catch {
|
|
1304
|
+
console.error("Failed to parse stdin as JSON. Provide valid JSON or use --goal, --metric, --verify flags.");
|
|
1305
|
+
return 1;
|
|
1306
|
+
}
|
|
1307
|
+
}
|
|
1308
|
+
}
|
|
1309
|
+
if (!config.goal && isTTY) {
|
|
1310
|
+
// Interactive wizard
|
|
1311
|
+
const readline = await import("readline");
|
|
1312
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stderr });
|
|
1313
|
+
const ask = (prompt, defaultVal) => new Promise((resolve) => {
|
|
1314
|
+
const suffix = defaultVal ? ` [${defaultVal}]` : "";
|
|
1315
|
+
rl.question(`${prompt}${suffix}: `, (answer) => {
|
|
1316
|
+
resolve(answer.trim() || defaultVal || "");
|
|
1317
|
+
});
|
|
1318
|
+
});
|
|
1319
|
+
process.stderr.write("\nAutoresearch Goal Init — Interactive Wizard\n");
|
|
1320
|
+
process.stderr.write("Press Enter to accept default values shown in brackets.\n\n");
|
|
1321
|
+
if (!config.goal)
|
|
1322
|
+
config.goal = await ask("Goal (what outcome should this run optimize?)", config.goal);
|
|
1323
|
+
if (!config.metric)
|
|
1324
|
+
config.metric = await ask("Metric name", config.metric ?? "primary_metric");
|
|
1325
|
+
if (!config.direction)
|
|
1326
|
+
config.direction = await ask("Direction (lower/higher)", config.direction ?? "lower");
|
|
1327
|
+
if (!config.verify)
|
|
1328
|
+
config.verify = await ask("Verify command", config.verify);
|
|
1329
|
+
if (!config.guard) {
|
|
1330
|
+
const guard = await ask("Guard command (optional, press Enter to skip)");
|
|
1331
|
+
if (guard)
|
|
1332
|
+
config.guard = guard;
|
|
1333
|
+
}
|
|
1334
|
+
if (!config.scope)
|
|
1335
|
+
config.scope = await ask("Scope (files or subsystem)", config.scope ?? "current repository");
|
|
1336
|
+
if (!config.mode)
|
|
1337
|
+
config.mode = await ask("Mode (foreground/background)", config.mode ?? "foreground");
|
|
1338
|
+
rl.close();
|
|
1339
|
+
}
|
|
1340
|
+
const goalPath = resolvePath(goalGrouped.repo, goalGrouped["goal-path"], GOAL_DEFAULT);
|
|
1341
|
+
const document = buildGoalDocument(config);
|
|
1342
|
+
const result = buildGoalInitResult(goalPath, config, !hasRequiredFlags && isTTY);
|
|
1343
|
+
if (isGoalDryRun) {
|
|
1344
|
+
if (useGoalJson) {
|
|
1345
|
+
printJson({ ...result, dry_run: true });
|
|
1346
|
+
}
|
|
1347
|
+
else {
|
|
1348
|
+
console.log("[dry-run] Would write goal document to: " + goalPath);
|
|
1349
|
+
console.log("");
|
|
1350
|
+
console.log(document);
|
|
1351
|
+
}
|
|
1352
|
+
return 0;
|
|
1353
|
+
}
|
|
1354
|
+
if (goalExistsSync(goalPath) && !goalGrouped["force"]) {
|
|
1355
|
+
// Overwrite allowed by default (like init), but warn
|
|
1356
|
+
if (verbose)
|
|
1357
|
+
console.error(`[verbose] Overwriting existing ${goalPath}`);
|
|
1358
|
+
}
|
|
1359
|
+
atomicWriteTextInRepo(goalGrouped.repo, goalPath, document);
|
|
1360
|
+
if (useGoalJson) {
|
|
1361
|
+
printJson(result);
|
|
1362
|
+
}
|
|
1363
|
+
else {
|
|
1364
|
+
console.log(`✓ Goal definition written to ${goalPath}`);
|
|
1365
|
+
console.log(` Goal: ${result.goal ?? "(unset)"}`);
|
|
1366
|
+
console.log(` Metric: ${result.metric ?? "(unset)"} (${result.direction})`);
|
|
1367
|
+
console.log(` Verify: ${result.verify ?? "(unset)"}`);
|
|
1368
|
+
console.log(` Mode: ${result.mode}`);
|
|
1369
|
+
if (result.template !== "custom")
|
|
1370
|
+
console.log(` Template: ${result.template}`);
|
|
1371
|
+
console.log("");
|
|
1372
|
+
console.log(`Run 'autoresearch init --goal "..." --metric "..." --verify "..."' to start a run.`);
|
|
1373
|
+
}
|
|
1374
|
+
break;
|
|
1375
|
+
}
|
|
824
1376
|
default: {
|
|
825
1377
|
console.error(`Unknown command: ${cmd}`);
|
|
826
1378
|
console.error("Run 'autoresearch --help' for usage.");
|