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.
Files changed (61) hide show
  1. package/.opencode-plugin/plugin.json +1 -1
  2. package/CHANGELOG.md +166 -0
  3. package/INSTALL.md +2 -2
  4. package/README.md +79 -39
  5. package/VERSION +1 -1
  6. package/dist/badge.d.ts +9 -0
  7. package/dist/badge.d.ts.map +1 -0
  8. package/dist/badge.js +86 -0
  9. package/dist/badge.js.map +1 -0
  10. package/dist/cli.js +578 -26
  11. package/dist/cli.js.map +1 -1
  12. package/dist/constants.d.ts +7 -1
  13. package/dist/constants.d.ts.map +1 -1
  14. package/dist/constants.js +7 -1
  15. package/dist/constants.js.map +1 -1
  16. package/dist/goal-init.d.ts +30 -0
  17. package/dist/goal-init.d.ts.map +1 -0
  18. package/dist/goal-init.js +109 -0
  19. package/dist/goal-init.js.map +1 -0
  20. package/dist/helpers.d.ts +13 -2
  21. package/dist/helpers.d.ts.map +1 -1
  22. package/dist/helpers.js +152 -9
  23. package/dist/helpers.js.map +1 -1
  24. package/dist/index.d.ts +10 -4
  25. package/dist/index.d.ts.map +1 -1
  26. package/dist/index.js +6 -2
  27. package/dist/index.js.map +1 -1
  28. package/dist/memory-manager.d.ts +24 -0
  29. package/dist/memory-manager.d.ts.map +1 -0
  30. package/dist/memory-manager.js +336 -0
  31. package/dist/memory-manager.js.map +1 -0
  32. package/dist/run-manager.d.ts +27 -2
  33. package/dist/run-manager.d.ts.map +1 -1
  34. package/dist/run-manager.js +210 -15
  35. package/dist/run-manager.js.map +1 -1
  36. package/dist/score-parser.d.ts +56 -0
  37. package/dist/score-parser.d.ts.map +1 -0
  38. package/dist/score-parser.js +109 -0
  39. package/dist/score-parser.js.map +1 -0
  40. package/dist/subagent-pool.d.ts +8 -0
  41. package/dist/subagent-pool.d.ts.map +1 -1
  42. package/dist/subagent-pool.js +67 -0
  43. package/dist/subagent-pool.js.map +1 -1
  44. package/dist/task-schema.d.ts +62 -0
  45. package/dist/task-schema.d.ts.map +1 -0
  46. package/dist/task-schema.js +95 -0
  47. package/dist/task-schema.js.map +1 -0
  48. package/dist/translators/cli.d.ts +5 -0
  49. package/dist/translators/cli.d.ts.map +1 -0
  50. package/dist/translators/cli.js +85 -0
  51. package/dist/translators/cli.js.map +1 -0
  52. package/dist/translators/hermes.d.ts +28 -0
  53. package/dist/translators/hermes.d.ts.map +1 -0
  54. package/dist/translators/hermes.js +102 -0
  55. package/dist/translators/hermes.js.map +1 -0
  56. package/dist/types.d.ts +104 -0
  57. package/dist/types.d.ts.map +1 -1
  58. package/docs/ARCHITECTURE.md +3 -0
  59. package/docs/RELEASE.md +15 -36
  60. package/hooks/verify-package.sh +5 -1
  61. 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 { printJson, resolveRepo, parseRunState, parsePositiveInt, sanitizeForTerminal, getInstalledPackagePath, getInstalledPackageInfo, readUpdateCache, getGlobalNpmPrefix } from "./helpers.js";
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 >= 8) {
351
- const emoji = cols[2] === "keep" ? "✓" : cols[2] === "discard" ? "✗" : "⚠";
352
- console.log(`${emoji} #${formatDisplayValue(cols[1])} ${formatDisplayValue(cols[2])} (${formatMetricValue(cols[3])}) ${formatDisplayValue(cols[7].substring(0, 60))}`);
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
- results = content.trim().split("\n").slice(1).filter(Boolean);
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 >= 8) {
528
- console.log(`- ${formatMarkdownField(cols[1])}: ${formatMarkdownField(cols[2])} (${formatMarkdownField(cols[3])}) — ${formatMarkdownField(cols[7]).substring(0, 60)}`);
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: patterns.length, suggestions: patterns.map((p) => p.replace("### Pattern: ", "")) });
880
+ printJson({ patterns_found: suggestions.length, suggestions });
546
881
  break;
547
882
  }
548
883
  console.log("Memory Patterns — candidate next goals:");
549
- for (const p of patterns) {
550
- console.log(` → ${formatDisplayValue(p.replace("### Pattern: ", ""))}`);
884
+ for (const suggestion of suggestions) {
885
+ console.log(` → ${formatDisplayValue(suggestion)}`);
551
886
  }
552
- console.log(`\n${patterns.length} patterns available. Use 'autoresearch init --goal "..."' to start a new run.`);
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.");