opencode-autoresearch 3.18.0 → 3.18.2

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.
@@ -0,0 +1,1389 @@
1
+ import { existsSync, readFileSync, writeFileSync, readdirSync } from "fs";
2
+ import { resolve } from "path";
3
+ import { execSync } from "child_process";
4
+ import { printJson, printJsonEnvelope, resolveRepo, parseRunState, parsePositiveInt, sanitizeForTerminal, getInstalledPackagePath, getInstalledPackageInfo, readUpdateCache, getGlobalNpmPrefix, readGoalDoc, atomicWriteTextInRepo, resolvePath, readJsonFile, normalizeResultStatus, normalizeScorerStatus, AutoresearchError, normalizeDirection, normalizeMode, } from "./helpers.js";
5
+ import { initializeRun, appendIteration, setStopRequested, resumeBackgroundRun, completeRun, buildSupervisorSnapshot, buildRunDigest, } from "./run-manager.js";
6
+ import { VERSION, PACKAGE_NAME, SKILL_NAME, MAX_DRAFTS, RESULTS_DEFAULT, STATE_DEFAULT, LAUNCH_DEFAULT, MEMORY_DEFAULT, SCORE_HISTORY_DEFAULT, GOAL_DEFAULT, } from "./constants.js";
7
+ import { tsvField, escapeMarkdownInline, escapeMarkdownTableCell, formatDisplayValue, formatMetricValue, formatTimestamp, formatMarkdownField, parseMemoryPatternHeading, readScoreHistoryFile, readTailLines, normalizeBranchPolicy, parseBranchPolicyOverrides, parseArgs, shouldSkipUpdateCheck, HELP_FLAGS, } from "./cli-helpers.js";
8
+ /* ── wizard ── */
9
+ export async function handleWizard(grouped, _useJson) {
10
+ const { buildSetupSummary } = await import("./wizard.js");
11
+ const config = {
12
+ goal: grouped.goal,
13
+ scope: grouped.scope,
14
+ metric: grouped.metric,
15
+ direction: grouped.direction,
16
+ verify: grouped.verify,
17
+ guard: grouped.guard,
18
+ mode: grouped.mode,
19
+ iterations: parsePositiveInt(grouped.iterations, "iterations"),
20
+ max_no_progress: parsePositiveInt(grouped["max-no-progress"], "max-no-progress"),
21
+ duration: grouped.duration,
22
+ memory_path: grouped["memory-path"],
23
+ required_keep_labels: grouped["required-keep-labels"],
24
+ required_stop_labels: grouped["required-stop-labels"],
25
+ stop_condition: grouped["stop-condition"],
26
+ rollback_strategy: grouped["rollback-strategy"],
27
+ };
28
+ printJsonEnvelope("wizard", buildSetupSummary(grouped.repo, config));
29
+ return 0;
30
+ }
31
+ /* ── init ── */
32
+ export async function handleInit(grouped, verbose, dryRun, _useJson) {
33
+ if (verbose)
34
+ console.error(`[verbose] Initializing run with goal: ${formatDisplayValue(grouped.goal)}`);
35
+ if (dryRun) {
36
+ console.log("[dry-run] Would initialize run with config:");
37
+ console.log(JSON.stringify({
38
+ goal: grouped.goal,
39
+ metric: grouped.metric,
40
+ direction: grouped.direction || "lower",
41
+ mode: grouped.mode || "foreground",
42
+ }, null, 2));
43
+ return 0;
44
+ }
45
+ const config = {
46
+ goal: grouped.goal,
47
+ metric: (grouped.metric || grouped["outcome-metric"]),
48
+ direction: (grouped.direction || grouped["outcome-direction"]) || "lower",
49
+ verify: grouped.verify,
50
+ mode: grouped.mode || "foreground",
51
+ scope: grouped.scope,
52
+ guard: grouped.guard,
53
+ scorer: grouped.scorer,
54
+ iterations: parsePositiveInt(grouped.iterations, "iterations"),
55
+ max_no_progress: parsePositiveInt(grouped["max-no-progress"], "max-no-progress"),
56
+ duration: grouped.duration,
57
+ memory_path: grouped["memory-path"],
58
+ required_keep_labels: grouped["required-keep-labels"],
59
+ required_stop_labels: grouped["required-stop-labels"],
60
+ run_tag: grouped["run-tag"],
61
+ stop_condition: grouped["stop-condition"],
62
+ baseline: grouped.baseline,
63
+ num_drafts: parsePositiveInt(grouped["num-drafts"], "num_drafts", { max: MAX_DRAFTS }) ?? 1,
64
+ branch_selection_policy: normalizeBranchPolicy(grouped["branch-policy"]),
65
+ branch_policy_overrides: parseBranchPolicyOverrides(grouped["branch-policy-overrides"]),
66
+ outcome_metric: grouped["outcome-metric"],
67
+ outcome_direction: grouped["outcome-direction"],
68
+ instrument_metric: grouped["instrument-metric"],
69
+ instrument_direction: grouped["instrument-direction"],
70
+ max_debug_depth: parsePositiveInt(grouped["max-debug-depth"], "max_debug_depth"),
71
+ branch_failure_budget: parsePositiveInt(grouped["branch-failure-budget"], "branch_failure_budget"),
72
+ };
73
+ const state = await initializeRun(grouped.repo, grouped["results-path"], grouped["state-path"], config, grouped["fresh-start"] === "true");
74
+ printJson(state);
75
+ return 0;
76
+ }
77
+ /* ── status ── */
78
+ export async function handleStatus(grouped, useJson) {
79
+ const snapshot = await buildSupervisorSnapshot(grouped.repo, grouped["results-path"], grouped["state-path"]);
80
+ if (useJson) {
81
+ printJsonEnvelope("status", snapshot);
82
+ return 0;
83
+ }
84
+ const s = snapshot;
85
+ const stats = s.stats;
86
+ console.log(`Run: ${formatDisplayValue(s.run_id)}`);
87
+ console.log(`Status: ${formatDisplayValue(s.status)}`);
88
+ console.log(`Mode: ${formatDisplayValue(s.mode)}`);
89
+ console.log(`Op Mode: ${formatDisplayValue(s.operating_mode)}`);
90
+ console.log(`Goal: ${formatDisplayValue(s.goal)}`);
91
+ if (s.metric) {
92
+ const m = s.metric;
93
+ console.log(`Metric: ${formatDisplayValue(m.name)} (${formatDisplayValue(m.direction)})`);
94
+ console.log(` best: ${formatMetricValue(m.best)}`);
95
+ console.log(` latest: ${formatMetricValue(m.latest)}`);
96
+ }
97
+ if (stats) {
98
+ console.log(`Stats: ${stats.total_iterations} iterations, ${stats.kept} kept, ${stats.discarded} discarded`);
99
+ }
100
+ console.log(`Results: ${s.results_rows} rows`);
101
+ const lastIter = s.last_iteration;
102
+ if (lastIter && lastIter.iteration) {
103
+ const stageTag = lastIter.stage ? ` [${formatDisplayValue(lastIter.stage)}]` : "";
104
+ console.log(`Last: iter ${formatDisplayValue(lastIter.iteration)}${stageTag} \u2014 ${formatDisplayValue(lastIter.decision)} (${formatMetricValue(lastIter.metric_value)})`);
105
+ if (lastIter.score_components != null && typeof lastIter.score_components === "object") {
106
+ const parts = Object.entries(lastIter.score_components)
107
+ .map(([k, v]) => `${formatDisplayValue(k)}:${typeof v === "number" ? v.toFixed(4) : formatDisplayValue(v)}`)
108
+ .join(", ");
109
+ if (parts.length > 0)
110
+ console.log(` Components: [${parts}]`);
111
+ }
112
+ if (lastIter.selected_action)
113
+ console.log(` Action: ${formatDisplayValue(lastIter.selected_action)}`);
114
+ }
115
+ const flags = s.flags;
116
+ if (flags?.needs_human)
117
+ console.log("\u26a0 Needs human input");
118
+ if (flags?.stop_requested)
119
+ console.log("\u23f9 Stop requested");
120
+ if (flags?.context_pressure) {
121
+ const cp = flags.context_pressure;
122
+ const warnings = Array.isArray(cp.warnings) ? cp.warnings : [];
123
+ if (warnings.length > 0) {
124
+ for (const w of warnings)
125
+ console.log(`\u1f638 Context pressure: ${formatDisplayValue(w)}`);
126
+ }
127
+ }
128
+ return 0;
129
+ }
130
+ /* ── explain ── */
131
+ export async function handleExplain(grouped, useJson) {
132
+ const { buildSupervisorSnapshot } = await import("./run-manager.js");
133
+ const snapshot = await buildSupervisorSnapshot(grouped.repo, grouped["results-path"], grouped["state-path"]);
134
+ const s = snapshot;
135
+ const stats = s.stats;
136
+ const lastIter = s.last_iteration;
137
+ const flags = s.flags;
138
+ if (useJson) {
139
+ printJsonEnvelope("explain", snapshot);
140
+ return 0;
141
+ }
142
+ const statusEmoji = {
143
+ running: "\ud83d\udd04", completed: "\u2705", initialized: "\ud83d\udccb",
144
+ stopping: "\u23f9", stopped: "\u23f8",
145
+ };
146
+ const statusKey = typeof s.status === "string" ? s.status : "";
147
+ const candidateEmoji = Object.prototype.hasOwnProperty.call(statusEmoji, statusKey)
148
+ ? statusEmoji[statusKey] : undefined;
149
+ const emoji = typeof candidateEmoji === "string" ? candidateEmoji : "\u26aa";
150
+ console.log(`${emoji} Auto Research Run: ${formatDisplayValue(s.run_id)}`);
151
+ console.log(` Goal: ${formatDisplayValue(s.goal)}`);
152
+ console.log(` Status: ${formatDisplayValue(s.status)}`);
153
+ console.log(` Mode: ${formatDisplayValue(s.mode)}`);
154
+ console.log(` Op Mode: ${formatDisplayValue(s.operating_mode)}`);
155
+ if (s.metric) {
156
+ const m = s.metric;
157
+ console.log(` Metric: ${formatDisplayValue(m.name)} \u2192 ${formatMetricValue(m.latest)} (best: ${formatMetricValue(m.best)}, dir: ${formatDisplayValue(m.direction)})`);
158
+ }
159
+ if (stats) {
160
+ console.log(` Progress: ${stats.total_iterations} iterations | ${stats.kept} kept | ${stats.discarded} discarded`);
161
+ }
162
+ if (lastIter && lastIter.iteration) {
163
+ console.log(` Last iter: #${formatDisplayValue(lastIter.iteration)} \u2014 ${formatDisplayValue(lastIter.decision)}`);
164
+ if (lastIter.change_summary)
165
+ console.log(` Change: ${formatDisplayValue(lastIter.change_summary)}`);
166
+ if (lastIter.score_components != null && typeof lastIter.score_components === "object") {
167
+ const parts = Object.entries(lastIter.score_components)
168
+ .map(([k, v]) => `${formatDisplayValue(k)}:${typeof v === "number" ? v.toFixed(4) : formatDisplayValue(v)}`)
169
+ .join(", ");
170
+ if (parts.length > 0)
171
+ console.log(` Components: [${parts}]`);
172
+ }
173
+ }
174
+ if (flags?.needs_human)
175
+ console.log(" \u26a0 Needs human review");
176
+ if (flags?.stop_requested)
177
+ console.log(" \u23f9 Stop was requested");
178
+ if (flags?.background_active)
179
+ console.log(" \ud83d\udce1 Background active");
180
+ return 0;
181
+ }
182
+ /* ── history ── */
183
+ export async function handleHistory(grouped, useJson) {
184
+ const resultsPath = resolvePath(grouped.repo, grouped["results-path"], RESULTS_DEFAULT);
185
+ if (!existsSync(resultsPath)) {
186
+ console.log("No results file found.");
187
+ return 0;
188
+ }
189
+ const content = readFileSync(resultsPath, "utf-8");
190
+ const lines = content.trim().split("\n");
191
+ if (lines.length <= 1) {
192
+ console.log("No iteration records yet.");
193
+ return 0;
194
+ }
195
+ const limit = parsePositiveInt(grouped.limit, "limit") ?? 10;
196
+ const headers = lines[0].split("\t");
197
+ const records = lines.slice(1).reverse().slice(0, limit);
198
+ if (useJson) {
199
+ const parsed = records.map((r) => {
200
+ const cols = r.split("\t");
201
+ const obj = {};
202
+ for (let i = 0; i < headers.length; i++) {
203
+ obj[headers[i]] = cols[i] ?? "";
204
+ }
205
+ return obj;
206
+ });
207
+ printJsonEnvelope("history", { count: records.length, records: parsed });
208
+ return 0;
209
+ }
210
+ for (const r of records) {
211
+ const cols = r.split("\t");
212
+ if (cols.length >= 4) {
213
+ const decision = tsvField(headers, cols, "decision", 2);
214
+ const metricValue = tsvField(headers, cols, "metric_value", 3);
215
+ const emoji = decision === "keep" ? "\u2713" : decision === "discard" ? "\u2717" : "\u26a0";
216
+ const changeSummary = tsvField(headers, cols, "change_summary", 8);
217
+ console.log(`${emoji} #${formatDisplayValue(cols[1])} ${formatDisplayValue(decision)} (${formatMetricValue(metricValue)}) ${formatDisplayValue(changeSummary.substring(0, 60))}`);
218
+ }
219
+ }
220
+ console.log(`\nShowing ${Math.min(limit, records.length)} of ${lines.length - 1} records.`);
221
+ return 0;
222
+ }
223
+ /* ── scores ── */
224
+ export async function handleScores(grouped, useJson) {
225
+ const scoreHistoryPath = resolvePath(grouped.repo, grouped["score-history-path"], SCORE_HISTORY_DEFAULT);
226
+ if (!existsSync(scoreHistoryPath)) {
227
+ console.log("No score history found.");
228
+ return 0;
229
+ }
230
+ const limit = parsePositiveInt(grouped.limit, "limit") ?? 10;
231
+ const showTopComponents = grouped["top-components"] === "true";
232
+ if (showTopComponents) {
233
+ const allLines = readScoreHistoryFile(scoreHistoryPath)
234
+ .split("\n").map((l) => l.trim()).filter(Boolean);
235
+ const allParsed = allLines.map((r) => {
236
+ try {
237
+ return JSON.parse(r);
238
+ }
239
+ catch {
240
+ return null;
241
+ }
242
+ }).filter(Boolean);
243
+ if (allParsed.length === 0) {
244
+ console.log("No score records yet.");
245
+ return 0;
246
+ }
247
+ const { rankComponents } = await import("./score-parser.js");
248
+ const ranking = rankComponents(allParsed);
249
+ if (useJson) {
250
+ printJsonEnvelope("scores", { count: allParsed.length, scores: allParsed.slice(-limit), ranking });
251
+ return 0;
252
+ }
253
+ console.log("Component Rankings:");
254
+ if (ranking.top_positive.length > 0) {
255
+ console.log(" Top improving components:");
256
+ for (const c of ranking.top_positive) {
257
+ console.log(` + ${formatDisplayValue(c.name)} \u0394+${c.delta.toFixed(4)}`);
258
+ }
259
+ }
260
+ if (ranking.top_negative.length > 0) {
261
+ console.log(" Top declining components:");
262
+ for (const c of ranking.top_negative) {
263
+ console.log(` - ${formatDisplayValue(c.name)} \u0394${c.delta.toFixed(4)}`);
264
+ }
265
+ }
266
+ if (ranking.top_positive.length === 0 && ranking.top_negative.length === 0) {
267
+ console.log(" No component data found.");
268
+ }
269
+ console.log(`\nAnalyzed ${allParsed.length} score records.`);
270
+ return 0;
271
+ }
272
+ const records = readTailLines(scoreHistoryPath, limit);
273
+ if (records.length === 0) {
274
+ console.log("No score records yet.");
275
+ return 0;
276
+ }
277
+ if (useJson) {
278
+ const parsed = records.map((r) => {
279
+ try {
280
+ return JSON.parse(r);
281
+ }
282
+ catch {
283
+ return null;
284
+ }
285
+ }).filter(Boolean);
286
+ printJsonEnvelope("scores", { count: parsed.length, scores: parsed });
287
+ return 0;
288
+ }
289
+ console.log("Score History (latest " + Math.min(limit, records.length) + "):");
290
+ const recordsOrdered = records.slice().reverse();
291
+ for (let i = 0; i < recordsOrdered.length; i += 1) {
292
+ const r = recordsOrdered[i];
293
+ try {
294
+ const rec = JSON.parse(r);
295
+ let trend = "";
296
+ if (i + 1 < recordsOrdered.length) {
297
+ try {
298
+ const prevRec = JSON.parse(recordsOrdered[i + 1]);
299
+ const cv = typeof rec.metric_value === "number" ? rec.metric_value : Number(rec.metric_value);
300
+ const pv = typeof prevRec.metric_value === "number" ? prevRec.metric_value : Number(prevRec.metric_value);
301
+ if (Number.isFinite(cv) && Number.isFinite(pv)) {
302
+ if (cv === pv)
303
+ trend = "\u2192";
304
+ else if (rec.metric_direction === "higher")
305
+ trend = cv > pv ? "\u2191" : "\u2193";
306
+ else
307
+ trend = cv < pv ? "\u2191" : "\u2193";
308
+ }
309
+ }
310
+ catch { /* ignore */ }
311
+ }
312
+ let componentLine = "";
313
+ if (rec.score_components != null && typeof rec.score_components === "object") {
314
+ const parts = Object.entries(rec.score_components)
315
+ .map(([k, v]) => `${formatDisplayValue(k)}:${typeof v === "number" ? v.toFixed(4) : formatDisplayValue(v)}`)
316
+ .join(", ");
317
+ if (parts.length > 0)
318
+ componentLine = ` [${parts}]`;
319
+ }
320
+ let componentDeltaLine = "";
321
+ if (componentLine && i + 1 < recordsOrdered.length) {
322
+ try {
323
+ const prevRec = JSON.parse(recordsOrdered[i + 1]);
324
+ if (prevRec.score_components != null && typeof prevRec.score_components === "object") {
325
+ const deltas = [];
326
+ for (const [k, v] of Object.entries(rec.score_components)) {
327
+ const prev = prevRec.score_components[k];
328
+ if (typeof prev === "number" && typeof v === "number") {
329
+ const d = v - prev;
330
+ if (d !== 0)
331
+ deltas.push(`${formatDisplayValue(k)}:${d > 0 ? "+" : ""}${d.toFixed(4)}`);
332
+ }
333
+ }
334
+ if (deltas.length > 0)
335
+ componentDeltaLine = ` \u0394[${deltas.join(", ")}]`;
336
+ }
337
+ }
338
+ catch { /* ignore */ }
339
+ }
340
+ console.log(` #${rec.iteration} ${trend} ${rec.metric_value ?? "\u2014"} (${rec.decision}) ${rec.verify_status}${componentLine}${componentDeltaLine}`);
341
+ }
342
+ catch {
343
+ console.log(` [parse error]`);
344
+ }
345
+ }
346
+ console.log(`\nShowing ${records.length} score records.`);
347
+ return 0;
348
+ }
349
+ /* ── score ── */
350
+ export async function handleScore(grouped, useJson) {
351
+ const AErr = AutoresearchError;
352
+ const scorerCmd = grouped.scorer;
353
+ if (!scorerCmd) {
354
+ throw new AErr("No scorer provided. Pass --scorer <cmd> to run a scorer explicitly.");
355
+ }
356
+ const repoBase = resolveRepo(grouped.repo);
357
+ let rawOutput;
358
+ try {
359
+ rawOutput = execSync(scorerCmd, { encoding: "utf-8", cwd: repoBase, stdio: ["ignore", "pipe", "pipe"] });
360
+ }
361
+ catch (err) {
362
+ const e = err;
363
+ const stderr = typeof e.stderr === "string" ? e.stderr.trim() : (Buffer.isBuffer(e.stderr) ? e.stderr.toString("utf-8").trim() : "");
364
+ throw new AErr(stderr || (err instanceof Error ? err.message : String(err)));
365
+ }
366
+ const { parseScoreOutput } = await import("./score-parser.js");
367
+ const scored = parseScoreOutput(rawOutput);
368
+ const normalized = scored.score / scored.max;
369
+ const percent = (normalized * 100).toFixed(1) + "%";
370
+ if (useJson) {
371
+ printJsonEnvelope("score", {
372
+ score: scored.score, max: scored.max, normalized, percent,
373
+ components: scored.components ?? null,
374
+ diagnostics: scored.diagnostics ?? null,
375
+ details: scored.details ?? null,
376
+ });
377
+ return 0;
378
+ }
379
+ console.log(`Score: ${scored.score} / ${scored.max} (${percent})`);
380
+ if (scored.components && Object.keys(scored.components).length > 0) {
381
+ console.log("Components:");
382
+ for (const [key, val] of Object.entries(scored.components)) {
383
+ console.log(` ${formatDisplayValue(key)}: ${formatDisplayValue(val)}`);
384
+ }
385
+ }
386
+ if (scored.diagnostics && Object.keys(scored.diagnostics).length > 0) {
387
+ console.log("Diagnostics:");
388
+ for (const [key, val] of Object.entries(scored.diagnostics)) {
389
+ console.log(` ${formatDisplayValue(key)}: ${formatDisplayValue(val)}`);
390
+ }
391
+ }
392
+ return 0;
393
+ }
394
+ /* ── config ── */
395
+ export async function handleConfig(grouped, useJson) {
396
+ const statePath = resolvePath(grouped.repo, grouped["state-path"], STATE_DEFAULT);
397
+ if (!existsSync(statePath)) {
398
+ console.log("No run state found. Run 'autoresearch init' first.");
399
+ return 0;
400
+ }
401
+ const state = parseRunState(readJsonFile(statePath));
402
+ if (useJson) {
403
+ printJsonEnvelope("config", {
404
+ goal: state.goal, mode: state.mode, metric: state.metric, scope: state.scope,
405
+ iterations_cap: state.iterations_cap, deadline_at: state.deadline_at,
406
+ verify: state.verify, guard: state.guard, scorer: state.scorer ?? null,
407
+ subagent_pool: state.subagent_pool ? "configured" : "none",
408
+ label_requirements: state.label_requirements,
409
+ });
410
+ return 0;
411
+ }
412
+ console.log("Run Configuration:");
413
+ console.log(` Goal: ${formatDisplayValue(state.goal)}`);
414
+ console.log(` Mode: ${formatDisplayValue(state.mode)}`);
415
+ console.log(` Op Mode: ${formatDisplayValue(state.operating_mode)}`);
416
+ if (state.metric) {
417
+ console.log(` Metric: ${formatDisplayValue(state.metric.name)} (${formatDisplayValue(state.metric.direction)})`);
418
+ }
419
+ console.log(` Scope: ${formatDisplayValue(state.scope)}`);
420
+ console.log(` Iter cap: ${formatDisplayValue(state.iterations_cap)}`);
421
+ console.log(` Deadline: ${formatDisplayValue(state.deadline_at ? formatTimestamp(state.deadline_at) : "\u2014")}`);
422
+ console.log(` Verify: ${formatDisplayValue(state.verify)}`);
423
+ console.log(` Guard: ${formatDisplayValue(state.guard)}`);
424
+ console.log(` Scorer: ${formatDisplayValue(state.scorer ?? "\u2014")}`);
425
+ console.log(` Pool: ${state.subagent_pool ? "configured" : "none"}`);
426
+ return 0;
427
+ }
428
+ /* ── contract ── */
429
+ export async function handleContract(useJson) {
430
+ const schemas = {
431
+ schema_version: "1.0.0",
432
+ description: "Auto Research runtime contract schemas",
433
+ state: {
434
+ type: "object",
435
+ required: ["schema_version", "run_id", "created_at", "updated_at", "status", "mode", "operating_mode", "goal", "scope", "metric", "verify", "label_requirements", "artifact_paths", "stats", "flags"],
436
+ properties: {
437
+ schema_version: { type: "number", description: "State schema version" },
438
+ run_id: { type: "string", description: "Unique run identifier" },
439
+ created_at: { type: "string", format: "date-time", description: "Run creation timestamp" },
440
+ updated_at: { type: "string", format: "date-time", description: "Last update timestamp" },
441
+ status: { type: "string", enum: ["initialized", "running", "stopping", "stopped", "completed", "needs_human"], description: "Run status" },
442
+ mode: { type: "string", enum: ["foreground", "background"], description: "Execution mode" },
443
+ operating_mode: { type: "string", enum: ["converge", "continuous", "supervised"], description: "Operating mode" },
444
+ goal: { type: "string" }, scope: { type: "string" },
445
+ metric: { type: "object", required: ["name", "direction"], properties: { name: { type: "string" }, direction: { type: "string", enum: ["higher", "lower"] }, baseline: { type: "string" }, best: { type: "string" }, latest: { type: "string" } } },
446
+ instrument_metric: { type: "object" }, verify: { type: "string" }, guard: { type: "string" }, scorer: { type: "string" },
447
+ iterations_cap: { type: "number" }, duration: { type: "string" }, duration_seconds: { type: "number" }, deadline_at: { type: "string", format: "date-time" },
448
+ label_requirements: { type: "object", required: ["keep", "stop"], properties: { keep: { type: "array", items: { type: "string" } }, stop: { type: "array", items: { type: "string" } } } },
449
+ artifact_paths: { type: "object", required: ["results", "state"], properties: { results: { type: "string" }, state: { type: "string" } } },
450
+ stats: { type: "object", required: ["total_iterations", "kept", "discarded", "needs_human"], properties: { total_iterations: { type: "number" }, kept: { type: "number" }, discarded: { type: "number" }, needs_human: { type: "number" }, consecutive_discards: { type: "number" }, best_iteration: { type: "number" }, debug_depth: { type: "number" } } },
451
+ flags: { type: "object", required: ["stop_requested", "needs_human", "background_active", "stop_ready"], properties: { stop_requested: { type: "boolean" }, needs_human: { type: "boolean" }, background_active: { type: "boolean" }, stop_ready: { type: "boolean" } } },
452
+ last_iteration: { type: "object", properties: { iteration: { type: "number" }, decision: { type: "string", enum: ["keep", "discard", "needs_human"] }, metric_value: { type: "string" }, change_summary: { type: "string" }, labels: { type: "array", items: { type: "string" } }, timestamp: { type: "string", format: "date-time" } } },
453
+ draft_pool: { type: "object" }, lineage: { type: "object" }, budget_exhausted: { type: "boolean" }, budget_blocker_reason: { type: "string" },
454
+ },
455
+ },
456
+ result_row: { type: "object", properties: { iteration: { type: "number" }, decision: { type: "string" }, metric_value: { type: "string" }, verify_status: { type: "string" }, guard_status: { type: "string" }, change_summary: { type: "string" }, labels: { type: "array", items: { type: "string" } }, timestamp: { type: "string" }, note: { type: "string" } } },
457
+ goal_doc: { type: "object", required: ["goal", "metric", "direction", "verify"], properties: { goal: { type: "string" }, metric: { type: "string" }, direction: { type: "string", enum: ["higher", "lower"] }, verify: { type: "string" }, guard: { type: "string" }, constraints: { type: "string" }, file_map: { type: "string" }, stop_conditions: { type: "string" } } },
458
+ };
459
+ if (useJson) {
460
+ printJsonEnvelope("contract", schemas);
461
+ return 0;
462
+ }
463
+ console.log("Auto Research Contract Schemas");
464
+ console.log("==============================");
465
+ console.log("");
466
+ console.log("State Schema:");
467
+ console.log(` Version: ${schemas.state.properties.schema_version.type}`);
468
+ console.log(` Required: ${schemas.state.required.join(", ")}`);
469
+ console.log("");
470
+ console.log("Result Row Schema:");
471
+ console.log(` Properties: ${Object.keys(schemas.result_row.properties).join(", ")}`);
472
+ console.log("");
473
+ console.log("Goal Doc Schema:");
474
+ console.log(` Required: ${schemas.goal_doc.required.join(", ")}`);
475
+ console.log("");
476
+ console.log("Use --json for full machine-readable schema output.");
477
+ return 0;
478
+ }
479
+ /* ── summary ── */
480
+ export async function handleSummary(grouped, useJson) {
481
+ const resultsPath = resolvePath(grouped.repo, grouped["results-path"], RESULTS_DEFAULT);
482
+ if (!existsSync(resultsPath)) {
483
+ console.log("No results file found. No runs completed yet.");
484
+ return 0;
485
+ }
486
+ const content = readFileSync(resultsPath, "utf-8");
487
+ const lines = content.trim().split("\n");
488
+ const records = lines.slice(1).filter(Boolean);
489
+ let totalKept = 0, totalDiscarded = 0, totalNeedsHuman = 0;
490
+ const runIds = new Set();
491
+ for (const r of records) {
492
+ const cols = r.split("\t");
493
+ const dec = cols[2];
494
+ if (dec === "keep")
495
+ totalKept++;
496
+ else if (dec === "discard")
497
+ totalDiscarded++;
498
+ else if (dec === "needs_human")
499
+ totalNeedsHuman++;
500
+ const iterTags = cols[1].split(":");
501
+ if (iterTags.length >= 2)
502
+ runIds.add(iterTags[0]);
503
+ }
504
+ if (useJson) {
505
+ printJsonEnvelope("summary", {
506
+ total_records: records.length, total_kept: totalKept,
507
+ total_discarded: totalDiscarded, total_needs_human: totalNeedsHuman,
508
+ keep_rate: records.length > 0 ? (totalKept / records.length * 100).toFixed(1) + "%" : "0%",
509
+ distinct_run_ids: Array.from(runIds),
510
+ });
511
+ return 0;
512
+ }
513
+ console.log("Auto Research Summary");
514
+ console.log(` Total iterations: ${records.length}`);
515
+ console.log(` Kept: ${totalKept}`);
516
+ console.log(` Discarded: ${totalDiscarded}`);
517
+ console.log(` Needs human: ${totalNeedsHuman}`);
518
+ console.log(` Keep rate: ${records.length > 0 ? (totalKept / records.length * 100).toFixed(1) : 0}%`);
519
+ console.log(` Distinct runs: ${runIds.size}`);
520
+ return 0;
521
+ }
522
+ /* ── validate ── */
523
+ export async function handleValidate(grouped, useJson) {
524
+ const errors = [];
525
+ if (!grouped.goal)
526
+ errors.push("Missing required: --goal");
527
+ if (!grouped.metric && !grouped["outcome-metric"])
528
+ errors.push("Missing required: --metric or --outcome-metric");
529
+ try {
530
+ if (grouped.direction)
531
+ normalizeDirection(grouped.direction);
532
+ }
533
+ catch (e) {
534
+ errors.push(`Invalid direction: ${e.message}`);
535
+ }
536
+ try {
537
+ if (grouped.mode)
538
+ normalizeMode(grouped.mode);
539
+ }
540
+ catch (e) {
541
+ errors.push(`Invalid mode: ${e.message}`);
542
+ }
543
+ if (!grouped.verify)
544
+ errors.push("Missing required: --verify");
545
+ if (useJson) {
546
+ printJsonEnvelope("validate", { valid: errors.length === 0, errors });
547
+ return errors.length > 0 ? 1 : 0;
548
+ }
549
+ if (errors.length === 0) {
550
+ console.log("\u2713 Configuration is valid");
551
+ console.log(` Goal: ${grouped.goal}`);
552
+ console.log(` Metric: ${grouped.metric || grouped["outcome-metric"]} (${grouped.direction || grouped["outcome-direction"] || "lower"})`);
553
+ console.log(` Verify: ${grouped.verify}`);
554
+ console.log(` Mode: ${grouped.mode || "foreground"}`);
555
+ }
556
+ else {
557
+ console.error("\u2717 Configuration errors:");
558
+ for (const err of errors)
559
+ console.error(` - ${formatDisplayValue(err)}`);
560
+ return 1;
561
+ }
562
+ return 0;
563
+ }
564
+ /* ── report ── */
565
+ export async function handleReport(grouped, useJson) {
566
+ const statePath = resolvePath(grouped.repo, grouped["state-path"], STATE_DEFAULT);
567
+ const resultsPath = resolvePath(grouped.repo, grouped["results-path"], RESULTS_DEFAULT);
568
+ if (!existsSync(statePath)) {
569
+ console.log("No run state found.");
570
+ return 0;
571
+ }
572
+ const state = parseRunState(readJsonFile(statePath));
573
+ let results = [];
574
+ let resultHeaders = [];
575
+ if (existsSync(resultsPath)) {
576
+ const content = readFileSync(resultsPath, "utf-8");
577
+ const resultLines = content.trim().split("\n");
578
+ resultHeaders = resultLines[0]?.split("\t") ?? [];
579
+ results = resultLines.slice(1).filter(Boolean);
580
+ }
581
+ if (useJson) {
582
+ printJsonEnvelope("report", { state, results_count: results.length });
583
+ return 0;
584
+ }
585
+ console.log(`# Auto Research Report`);
586
+ console.log(`\n**Run:** ${formatMarkdownField(state.run_id)}`);
587
+ console.log(`**Goal:** ${formatMarkdownField(state.goal)}`);
588
+ console.log(`**Status:** ${formatMarkdownField(state.status)}`);
589
+ console.log(`**Mode:** ${formatMarkdownField(state.mode)}`);
590
+ console.log(`**Op Mode:** ${formatMarkdownField(state.operating_mode)}`);
591
+ if (state.metric) {
592
+ console.log(`**Metric:** ${formatMarkdownField(state.metric.name)} (${formatMarkdownField(state.metric.direction)})`);
593
+ console.log(`**Best:** ${formatMarkdownField(state.metric.best)} | **Latest:** ${formatMarkdownField(state.metric.latest)}`);
594
+ }
595
+ if (state.stats) {
596
+ const s = state.stats;
597
+ console.log(`\n## Stats`);
598
+ console.log(`- Iterations: ${formatMarkdownField(s.total_iterations)}`);
599
+ console.log(`- Kept: ${formatMarkdownField(s.kept)}`);
600
+ console.log(`- Discarded: ${formatMarkdownField(s.discarded)}`);
601
+ console.log(`- Needs human: ${formatMarkdownField(s.needs_human)}`);
602
+ if (s.best_iteration !== undefined && results.length > 0) {
603
+ const bestIterationResults = results.filter(r => {
604
+ const cols = r.split("\t");
605
+ return cols[1] === String(s.best_iteration);
606
+ });
607
+ if (bestIterationResults.length > 0) {
608
+ const bestCols = bestIterationResults[0].split("\t");
609
+ const bestChangeSummary = tsvField(resultHeaders, bestCols, "change_summary", 7);
610
+ console.log(`- Best attempt: iteration ${formatMarkdownField(String(s.best_iteration))} \u2014 ${formatMarkdownField(bestChangeSummary.substring(0, 60))}`);
611
+ }
612
+ }
613
+ }
614
+ console.log(`\n## Milestone Progress`);
615
+ if (state.stats) {
616
+ const s = state.stats;
617
+ const total = s.total_iterations;
618
+ const successRate = total > 0 ? ((s.kept / total) * 100).toFixed(1) : "0";
619
+ console.log(`- **Progress:** ${formatMarkdownField(s.kept)} kept / ${formatMarkdownField(total)} total iterations (${formatMarkdownField(successRate)}% success rate)`);
620
+ if (state.iterations_cap) {
621
+ const progressPct = ((total / state.iterations_cap) * 100).toFixed(1);
622
+ console.log(`- **Cap:** ${formatMarkdownField(total)} / ${formatMarkdownField(state.iterations_cap)} iterations (${formatMarkdownField(progressPct)}% of cap)`);
623
+ }
624
+ if (state.created_at) {
625
+ const startedAtMs = Date.parse(state.created_at);
626
+ const endedAtMs = state.updated_at ? Date.parse(state.updated_at) : Date.now();
627
+ if (!Number.isNaN(startedAtMs) && !Number.isNaN(endedAtMs) && endedAtMs >= startedAtMs) {
628
+ console.log(`- **Elapsed:** ${formatMarkdownField(Math.round((endedAtMs - startedAtMs) / 1000 / 60))} minutes`);
629
+ }
630
+ }
631
+ if (state.last_iteration && state.last_iteration.decision === "keep") {
632
+ console.log(`- **Next candidate:** Iteration ${formatMarkdownField(state.last_iteration.iteration)} (kept)`);
633
+ }
634
+ else if (s.best_iteration) {
635
+ console.log(`- **Best candidate:** Iteration ${formatMarkdownField(s.best_iteration)}`);
636
+ }
637
+ }
638
+ console.log(`\n## Artifacts`);
639
+ console.log(`- State: ${formatMarkdownField(state.artifact_paths?.state || ".autoresearch/state.json")}`);
640
+ console.log(`- Results: ${formatMarkdownField(state.artifact_paths?.results || "autoresearch-results.tsv")}`);
641
+ if (grouped.repo)
642
+ console.log(`- Repository: ${formatMarkdownField(grouped.repo)}`);
643
+ if (state.draft_pool && state.draft_pool.active_drafts) {
644
+ const failedBranches = state.draft_pool.active_drafts.filter((d) => d.status === "discarded");
645
+ if (failedBranches.length > 0) {
646
+ console.log(`\n## Failed Branches`);
647
+ for (const branch of failedBranches.slice(0, 5)) {
648
+ console.log(`- Branch ${formatMarkdownField(branch.branch_id)}: iteration ${formatMarkdownField(String(branch.iteration))} (parent: ${formatMarkdownField(String(branch.parent_iteration))}) \u2014 ${formatMarkdownField(branch.metric_value ?? "no metric")}`);
649
+ }
650
+ if (failedBranches.length > 5) {
651
+ console.log(` ... and ${formatMarkdownField(String(failedBranches.length - 5))} more failed branches`);
652
+ }
653
+ }
654
+ }
655
+ if (state.flags.needs_human) {
656
+ console.log(`\n## Blockers`);
657
+ console.log(`- Human input required: ${formatMarkdownField(state.last_iteration?.change_summary ?? "awaiting user decision")}`);
658
+ if (state.last_iteration?.note) {
659
+ console.log(`- Details: ${formatMarkdownField(state.last_iteration.note.substring(0, 100))}${state.last_iteration.note.length > 100 ? "..." : ""}`);
660
+ }
661
+ }
662
+ console.log(`\n## Next Actions`);
663
+ if (state.status === "running") {
664
+ if (state.flags.needs_human)
665
+ console.log("- Awaiting human input");
666
+ else if (state.flags.stop_requested)
667
+ console.log("- Stop requested, will complete current iteration");
668
+ else
669
+ console.log("- Continue with next iteration");
670
+ }
671
+ else if (state.status === "completed") {
672
+ console.log("- Run completed successfully");
673
+ }
674
+ else if (state.status === "stopped" || state.status === "stopping") {
675
+ console.log("- Run stopped; use 'autoresearch resume' to continue");
676
+ }
677
+ else {
678
+ console.log("- Initialize a new run with 'autoresearch init'");
679
+ }
680
+ if (results.length > 0) {
681
+ console.log(`\n## Iterations`);
682
+ for (const r of results) {
683
+ const cols = r.split("\t");
684
+ if (cols.length >= 4) {
685
+ const decision = tsvField(resultHeaders, cols, "decision", 2);
686
+ const metricValue = tsvField(resultHeaders, cols, "metric_value", 3);
687
+ const changeSummary = tsvField(resultHeaders, cols, "change_summary", 8);
688
+ console.log(`- ${formatMarkdownField(cols[1])}: ${formatMarkdownField(decision)} (${formatMarkdownField(metricValue)}) \u2014 ${formatMarkdownField(changeSummary).substring(0, 60)}`);
689
+ }
690
+ }
691
+ }
692
+ return 0;
693
+ }
694
+ /* ── suggest ── */
695
+ export async function handleSuggest(grouped, useJson) {
696
+ const evidenceGated = grouped.evidence === "true";
697
+ if (evidenceGated) {
698
+ const { generateIssueCandidate } = await import("./evidence.js");
699
+ const candidate = generateIssueCandidate(grouped.repo, grouped.goal, grouped.metric, grouped.verify, grouped["score-history-path"]);
700
+ if (!candidate) {
701
+ if (useJson) {
702
+ printJsonEnvelope("suggest", { candidates: [], reason: "insufficient_evidence" });
703
+ }
704
+ else {
705
+ console.log("No evidence-gated issue candidates found.");
706
+ }
707
+ return 0;
708
+ }
709
+ if (useJson) {
710
+ printJsonEnvelope("suggest", { candidates: [candidate], evidence_gated: true });
711
+ }
712
+ else {
713
+ console.log(`Evidence-Gated Issue Candidate:`);
714
+ console.log(` Title: ${candidate.title}`);
715
+ console.log(` Goal: ${candidate.goal}`);
716
+ console.log(` Metric: ${candidate.metric}`);
717
+ console.log(` Evidence: ${candidate.evidence.total_discards} discards in ${candidate.evidence.total_runs} cluster(s)`);
718
+ console.log(` Suggested command: ${candidate.suggest_command}`);
719
+ }
720
+ return 0;
721
+ }
722
+ const memoryPath = resolvePath(grouped.repo, grouped["memory-path"], MEMORY_DEFAULT);
723
+ if (!existsSync(memoryPath)) {
724
+ console.log("No memory file found.");
725
+ return 0;
726
+ }
727
+ const memory = readFileSync(memoryPath, "utf-8");
728
+ const patterns = memory.match(/^### Pattern: [^\n]+/gm) ?? [];
729
+ const suggestions = patterns.map(parseMemoryPatternHeading);
730
+ if (useJson) {
731
+ printJsonEnvelope("suggest", { patterns_found: suggestions.length, suggestions });
732
+ return 0;
733
+ }
734
+ console.log("Memory Patterns \u2014 candidate next goals:");
735
+ for (const suggestion of suggestions) {
736
+ console.log(` \u2192 ${formatDisplayValue(suggestion)}`);
737
+ }
738
+ console.log(`\n${suggestions.length} patterns available.`);
739
+ return 0;
740
+ }
741
+ /* ── export ── */
742
+ export async function handleExport(grouped) {
743
+ const resultsPath = resolvePath(grouped.repo, grouped["results-path"], RESULTS_DEFAULT);
744
+ const statePath = resolvePath(grouped.repo, grouped["state-path"], STATE_DEFAULT);
745
+ const format = grouped.format || "json";
746
+ if (!existsSync(resultsPath) || !existsSync(statePath)) {
747
+ console.error("No run data found.");
748
+ return 1;
749
+ }
750
+ const results = readFileSync(resultsPath, "utf-8");
751
+ const state = JSON.parse(readFileSync(statePath, "utf-8"));
752
+ const lines = results.trim().split("\n");
753
+ const headers = lines[0].split("\t");
754
+ const records = lines.slice(1).filter(Boolean).map((r) => {
755
+ const cols = r.split("\t");
756
+ const obj = {};
757
+ for (let i = 0; i < headers.length; i++)
758
+ obj[headers[i]] = cols[i] ?? "";
759
+ return obj;
760
+ });
761
+ const exportData = {
762
+ exported_at: new Date().toISOString(), state,
763
+ iterations: records,
764
+ summary: { total: records.length, kept: records.filter((r) => r.decision === "keep").length, discarded: records.filter((r) => r.decision === "discard").length },
765
+ };
766
+ if (format === "json") {
767
+ printJsonEnvelope("export", exportData);
768
+ }
769
+ else if (format === "md" || format === "markdown") {
770
+ console.log(`# Auto Research Export`);
771
+ console.log(`\n**Run:** ${escapeMarkdownInline(exportData.state.run_id) || "\u2014"}`);
772
+ console.log(`**Goal:** ${escapeMarkdownInline(exportData.state.goal) || "\u2014"}`);
773
+ console.log(`**Exported:** ${escapeMarkdownInline(exportData.exported_at)}`);
774
+ console.log(`\n## Summary`);
775
+ console.log(`- Total iterations: ${exportData.summary.total}`);
776
+ console.log(`- Kept: ${exportData.summary.kept}`);
777
+ console.log(`- Discarded: ${exportData.summary.discarded}`);
778
+ console.log(`\n## Iterations`);
779
+ console.log(`| # | Decision | Metric | Summary |`);
780
+ console.log(`|---|----------|--------|---------|`);
781
+ for (const r of records) {
782
+ console.log(`| ${escapeMarkdownTableCell(r.iteration)} | ${escapeMarkdownTableCell(r.decision)} | ${escapeMarkdownTableCell(r.metric_value)} | ${escapeMarkdownTableCell(r.change_summary?.substring(0, 50))} |`);
783
+ }
784
+ }
785
+ else {
786
+ console.error(`Unknown format: ${format}`);
787
+ return 1;
788
+ }
789
+ return 0;
790
+ }
791
+ /* ── completion ── */
792
+ export async function handleCompletion(grouped) {
793
+ const shell = grouped.shell || "bash";
794
+ const commands = ["init", "goal", "wizard", "status", "explain", "history", "config", "summary", "suggest", "launch", "complete", "stop", "resume", "record", "doctor", "pack", "export", "completion"];
795
+ 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"];
796
+ if (shell === "bash" || shell === "zsh") {
797
+ console.log(`# Completion for ${shell}`);
798
+ console.log(`_autoresearch() {`);
799
+ console.log(` local cur="\${COMP_WORDS[COMP_CWORD]}"`);
800
+ console.log(` local cmds="${commands.join(" ")}"`);
801
+ console.log(` local opts="${options.join(" ")}"`);
802
+ console.log(` if [ $COMP_CWORD -eq 1 ]; then`);
803
+ console.log(` COMPREPLY=($(compgen -W "$cmds" -- "$cur"))`);
804
+ console.log(` else`);
805
+ console.log(` COMPREPLY=($(compgen -W "$opts" -- "$cur"))`);
806
+ console.log(` fi`);
807
+ console.log(`}; complete -F _autoresearch autoresearch`);
808
+ }
809
+ else if (shell === "fish") {
810
+ console.log(`# Completion for fish`);
811
+ for (const cmd of commands)
812
+ console.log(`complete -c autoresearch -n '__fish_use_subcommand' -a '${cmd}'`);
813
+ for (const opt of options)
814
+ console.log(`complete -c autoresearch -n '__fish_seen_subcommand_from ${commands.join(" ")}' -l ${opt.slice(2)}`);
815
+ }
816
+ else {
817
+ console.error(`Unknown shell: ${shell}`);
818
+ return 1;
819
+ }
820
+ return 0;
821
+ }
822
+ /* ── launch ── */
823
+ export async function handleLaunch(grouped, dryRun) {
824
+ const config = {
825
+ goal: grouped.goal,
826
+ metric: (grouped.metric || grouped["outcome-metric"]),
827
+ direction: (grouped.direction || grouped["outcome-direction"]) || "lower",
828
+ verify: grouped.verify,
829
+ mode: "background",
830
+ scope: grouped.scope,
831
+ guard: grouped.guard,
832
+ scorer: grouped.scorer,
833
+ iterations: parsePositiveInt(grouped.iterations, "iterations"),
834
+ max_no_progress: parsePositiveInt(grouped["max-no-progress"], "max-no-progress"),
835
+ max_debug_depth: parsePositiveInt(grouped["max-debug-depth"], "max_debug_depth"),
836
+ branch_failure_budget: parsePositiveInt(grouped["branch-failure-budget"], "branch_failure_budget"),
837
+ duration: grouped.duration,
838
+ memory_path: grouped["memory-path"],
839
+ required_keep_labels: grouped["required-keep-labels"],
840
+ required_stop_labels: grouped["required-stop-labels"],
841
+ run_tag: grouped["run-tag"],
842
+ stop_condition: grouped["stop-condition"],
843
+ baseline: grouped.baseline,
844
+ num_drafts: parsePositiveInt(grouped["num-drafts"], "num_drafts", { max: MAX_DRAFTS }) ?? 1,
845
+ branch_selection_policy: normalizeBranchPolicy(grouped["branch-policy"]),
846
+ branch_policy_overrides: parseBranchPolicyOverrides(grouped["branch-policy-overrides"]),
847
+ outcome_metric: grouped["outcome-metric"],
848
+ outcome_direction: grouped["outcome-direction"],
849
+ instrument_metric: grouped["instrument-metric"],
850
+ instrument_direction: grouped["instrument-direction"],
851
+ };
852
+ const launchPath = resolvePath(grouped.repo, grouped["launch-path"], LAUNCH_DEFAULT);
853
+ if (dryRun) {
854
+ console.log("[dry-run] Would launch background run");
855
+ console.log(JSON.stringify({ ...config, launch_path: launchPath }, null, 2));
856
+ return 0;
857
+ }
858
+ const { initializeRun } = await import("./run-manager.js");
859
+ const state = await initializeRun(grouped.repo, grouped["results-path"], grouped["state-path"], config, grouped["fresh-start"] === "true");
860
+ writeFileSync(launchPath, JSON.stringify({ run_id: state.run_id, goal: state.goal, mode: "background" }, null, 2) + "\n", "utf-8");
861
+ printJsonEnvelope("launch", { status: "launched", run_id: state.run_id, launch_path: launchPath });
862
+ return 0;
863
+ }
864
+ /* ── complete ── */
865
+ export async function handleComplete(grouped, dryRun) {
866
+ if (dryRun) {
867
+ console.log("[dry-run] Would mark run complete");
868
+ return 0;
869
+ }
870
+ const state = await completeRun(grouped.repo, grouped["state-path"]);
871
+ printJsonEnvelope("complete", { status: "completed", run_id: state.run_id });
872
+ return 0;
873
+ }
874
+ /* ── stop ── */
875
+ export async function handleStop(grouped, dryRun) {
876
+ if (dryRun) {
877
+ console.log("[dry-run] Would request stop");
878
+ return 0;
879
+ }
880
+ const state = await setStopRequested(grouped.repo, grouped["state-path"]);
881
+ printJsonEnvelope("stop", { status: "stop_requested", run_id: state.run_id });
882
+ return 0;
883
+ }
884
+ /* ── resume ── */
885
+ export async function handleResume(grouped, dryRun) {
886
+ if (dryRun) {
887
+ console.log("[dry-run] Would resume background run");
888
+ return 0;
889
+ }
890
+ const state = await resumeBackgroundRun(grouped.repo, grouped["state-path"]);
891
+ printJsonEnvelope("resume", { status: "resumed", run_id: state.run_id });
892
+ return 0;
893
+ }
894
+ /* ── record ── */
895
+ export async function handleRecord(grouped, dryRun) {
896
+ const vs = grouped["verify-status"] || "pass";
897
+ const gs = grouped["guard-status"] || "skip";
898
+ const scorerStatus = normalizeScorerStatus(grouped["scorer-status"]);
899
+ const iteration = parsePositiveInt(grouped.iteration, "iteration");
900
+ let scoreComponents;
901
+ if (grouped["score-components"]) {
902
+ try {
903
+ const parsed = JSON.parse(grouped["score-components"]);
904
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
905
+ throw new Error('score-components must be a JSON object');
906
+ }
907
+ scoreComponents = parsed;
908
+ }
909
+ catch (e) {
910
+ console.error(`Invalid --score-components: ${e.message}`);
911
+ return 1;
912
+ }
913
+ }
914
+ if (dryRun) {
915
+ console.log("[dry-run] Would record result");
916
+ console.log(JSON.stringify({
917
+ decision: grouped.decision, metric_value: grouped["metric-value"],
918
+ scorer_status: scorerStatus, verify_status: normalizeResultStatus(vs, "verify_status"),
919
+ guard_status: normalizeResultStatus(gs, "guard_status"),
920
+ change_summary: grouped["change-summary"], iteration, score_components: scoreComponents,
921
+ }, null, 2));
922
+ return 0;
923
+ }
924
+ const lineage = {};
925
+ const stage = grouped.stage;
926
+ if (stage)
927
+ lineage.stage = stage;
928
+ const selectedAction = grouped["selected-action"];
929
+ if (selectedAction)
930
+ lineage.selected_action = selectedAction;
931
+ 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, Object.keys(lineage).length > 0 ? lineage : undefined);
932
+ printJsonEnvelope("record", state);
933
+ return 0;
934
+ }
935
+ /* ── digest ── */
936
+ export async function handleDigest(grouped, useJson, dryRun) {
937
+ if (dryRun) {
938
+ console.log("[dry-run] Would generate digest");
939
+ return 0;
940
+ }
941
+ const digest = await buildRunDigest(grouped.repo, grouped["results-path"], grouped["state-path"]);
942
+ if (useJson) {
943
+ printJsonEnvelope("digest", digest);
944
+ return 0;
945
+ }
946
+ console.log(`# Auto Research Digest`);
947
+ console.log(`\n**Run ID:** ${formatMarkdownField(digest.run_id || "\u2014")}`);
948
+ console.log(`**Status:** ${sanitizeForTerminal(digest.status || "\u2014")}`);
949
+ console.log(`**Mode:** ${formatMarkdownField(digest.mode || "\u2014")}`);
950
+ console.log(`**Goal:** ${formatMarkdownField(digest.goal || "\u2014")}`);
951
+ if (digest.metric) {
952
+ const m = digest.metric;
953
+ console.log(`**Metric:** ${sanitizeForTerminal(m.name)} (${sanitizeForTerminal(m.direction)})`);
954
+ console.log(` Best: ${formatMarkdownField(m.best || "\u2014")} | Latest: ${formatMarkdownField(m.latest || "\u2014")}`);
955
+ }
956
+ if (digest.stats) {
957
+ const s = digest.stats;
958
+ console.log(`\n## Stats`);
959
+ console.log(`- Iterations: ${formatMarkdownField(s.total_iterations || "\u2014")}`);
960
+ console.log(`- Kept: ${formatMarkdownField(s.kept || "\u2014")}`);
961
+ console.log(`- Discarded: ${formatMarkdownField(s.discarded || "\u2014")}`);
962
+ console.log(`- Needs human: ${formatMarkdownField(s.needs_human || "\u2014")}`);
963
+ }
964
+ if (digest.last_iteration) {
965
+ const li = digest.last_iteration;
966
+ console.log(`\n## Last Iteration`);
967
+ console.log(`- #${formatMarkdownField(li.iteration || "\u2014")}: ${formatMarkdownField(li.decision || "\u2014")} (${formatMarkdownField(li.metric_value || "\u2014")})`);
968
+ if (li.change_summary)
969
+ console.log(`- Change: ${formatMarkdownField(li.change_summary.substring(0, 100))}${li.change_summary.length > 100 ? "..." : ""}`);
970
+ }
971
+ console.log(`\n## Next Action`);
972
+ console.log(`${sanitizeForTerminal(digest.next_action || "No specific next action recommended")}`);
973
+ if (digest.blockers && digest.blockers.length > 0) {
974
+ console.log(`\n## Blockers`);
975
+ for (const blocker of digest.blockers)
976
+ console.log(`- ${formatMarkdownField(blocker)}`);
977
+ }
978
+ else {
979
+ console.log(`\n## Blockers\nNone identified`);
980
+ }
981
+ if (digest.flags && Object.keys(digest.flags).length > 0) {
982
+ console.log(`\n## Flags`);
983
+ for (const [key, value] of Object.entries(digest.flags))
984
+ console.log(`- ${formatMarkdownField(key)}: ${formatMarkdownField(value)}`);
985
+ }
986
+ return 0;
987
+ }
988
+ /* ── doctor ── */
989
+ export async function handleDoctor(grouped, useJson) {
990
+ const base = resolveRepo(grouped.repo);
991
+ const checks = [];
992
+ const cmdDir = resolve(base, "commands");
993
+ const skillsDir = resolve(base, "skills/autoresearch");
994
+ const hooksDir = resolve(base, "hooks");
995
+ const cmdFiles = existsSync(cmdDir) ? readdirSync(cmdDir).filter((f) => f.endsWith(".md")) : [];
996
+ const skillFiles = existsSync(skillsDir) ? readdirSync(skillsDir) : [];
997
+ const hookFiles = existsSync(hooksDir) ? readdirSync(hooksDir).filter((f) => f.endsWith(".sh")) : [];
998
+ checks.push({ name: "commands", ok: cmdFiles.length > 0, detail: `${cmdFiles.length} command files` });
999
+ checks.push({ name: "skills", ok: skillFiles.length > 0, detail: `${skillFiles.length} skill files` });
1000
+ checks.push({ name: "hooks", ok: hookFiles.length > 0, detail: `${hookFiles.length} hook scripts` });
1001
+ checks.push({ name: "dist", ok: existsSync(resolve(base, "dist/cli.js")), detail: "dist/cli.js" });
1002
+ checks.push({ name: "plugin", ok: existsSync(resolve(base, ".opencode-plugin/plugin.json")), detail: "plugin manifest" });
1003
+ checks.push({ name: "VERSION", ok: existsSync(resolve(base, "VERSION")), detail: "version marker" });
1004
+ const globalPrefix = getGlobalNpmPrefix();
1005
+ const installedPath = getInstalledPackagePath(PACKAGE_NAME);
1006
+ const installedInfo = installedPath ? getInstalledPackageInfo(PACKAGE_NAME) : null;
1007
+ const updateCache = readUpdateCache();
1008
+ const updateStatus = {
1009
+ cache_exists: updateCache !== null, last_check: updateCache?.last_check || null,
1010
+ current_version: updateCache?.current_version || null, latest_version: updateCache?.latest_version || null,
1011
+ update_available: updateCache?.update_available || false,
1012
+ update_disabled: process.env.AUTORESEARCH_NO_UPDATE === "1",
1013
+ skipped: shouldSkipUpdateCheck(process.argv.slice(2)).skip,
1014
+ skip_reason: shouldSkipUpdateCheck(process.argv.slice(2)).reason,
1015
+ };
1016
+ if (useJson) {
1017
+ const { getWhatsNew } = await import("./whats-new.js");
1018
+ const wn = getWhatsNew(base);
1019
+ printJsonEnvelope("doctor", {
1020
+ version: VERSION, skill_name: SKILL_NAME, runtime: `Node.js ${process.version}`,
1021
+ source: { package_name: PACKAGE_NAME, global_path: installedPath || null, global_prefix: globalPrefix || null, installed_version: installedInfo?.version || null },
1022
+ update: updateStatus, checks, checks_passed: checks.filter((c) => !c.ok).length === 0,
1023
+ whats_new: wn ? { features: wn.features, fixes: wn.fixes } : null,
1024
+ });
1025
+ return 0;
1026
+ }
1027
+ console.log(`${SKILL_NAME} ${VERSION} (${PACKAGE_NAME})`);
1028
+ console.log(`Runtime: Node.js ${process.version}\n`);
1029
+ console.log("Source:");
1030
+ console.log(` Package: ${PACKAGE_NAME}`);
1031
+ if (installedPath) {
1032
+ console.log(` Global: ${installedPath}`);
1033
+ if (globalPrefix)
1034
+ console.log(` Prefix: ${globalPrefix}`);
1035
+ }
1036
+ else {
1037
+ console.log(" Global: not found via npm -g");
1038
+ }
1039
+ console.log("\nUpdate:");
1040
+ if (updateStatus.skipped)
1041
+ console.log(` Skipped: yes (${updateStatus.skip_reason})`);
1042
+ else if (updateCache) {
1043
+ console.log(` Last check: ${updateCache.last_check}`);
1044
+ console.log(` Current: ${updateCache.current_version}`);
1045
+ console.log(` Latest: ${updateCache.latest_version}`);
1046
+ console.log(` Available: ${updateCache.update_available ? "yes" : "no"}`);
1047
+ }
1048
+ else {
1049
+ console.log(" Cache: no update check recorded");
1050
+ }
1051
+ console.log("\nInstallation Checks:");
1052
+ let maxNameLen = 0;
1053
+ for (const c of checks)
1054
+ maxNameLen = Math.max(maxNameLen, c.name.length);
1055
+ for (const c of checks) {
1056
+ console.log(` ${c.ok ? "\u2713" : "\u2717"} ${c.name.padEnd(maxNameLen + 2)}${c.detail ?? (c.ok ? "present" : "missing")}`);
1057
+ }
1058
+ const failed = checks.filter((c) => !c.ok).length;
1059
+ if (failed > 0) {
1060
+ console.error(`\n${failed} check(s) failed.`);
1061
+ return 1;
1062
+ }
1063
+ console.log(`\nAll ${checks.length} checks passed.`);
1064
+ if (grouped["whats-new"] === "true") {
1065
+ const { getWhatsNew, formatWhatsNew } = await import("./whats-new.js");
1066
+ const wn2 = getWhatsNew(base);
1067
+ if (wn2)
1068
+ console.log("\n" + formatWhatsNew(wn2));
1069
+ }
1070
+ return 0;
1071
+ }
1072
+ /* ── goal ── */
1073
+ export async function handleGoal(grouped, cmdArgs, useJson, _verbose, _dryRun) {
1074
+ const rawSubCmd = cmdArgs[0];
1075
+ const subCmd = rawSubCmd && !rawSubCmd.startsWith("-") ? rawSubCmd : undefined;
1076
+ if ((!subCmd && cmdArgs.length === 0) || subCmd === "help" || (subCmd && HELP_FLAGS.includes(subCmd))) {
1077
+ console.error("Usage: autoresearch goal <subcommand> [options]");
1078
+ console.error("Subcommands: init\tCreate a GOAL.md goal definition file");
1079
+ console.error("Options (goal init):");
1080
+ console.error(" --goal, --metric, --direction, --verify, --guard, --mode");
1081
+ console.error(" --scope, --iterations, --duration, --template");
1082
+ console.error(" --goal-path, --dry-run, --json");
1083
+ return 0;
1084
+ }
1085
+ if (!subCmd) {
1086
+ const goalPath = resolvePath(grouped.repo, grouped["goal-path"], GOAL_DEFAULT);
1087
+ if (!existsSync(goalPath)) {
1088
+ console.log("No goal document found.");
1089
+ return 0;
1090
+ }
1091
+ const doc = readGoalDoc(goalPath);
1092
+ if (useJson) {
1093
+ printJsonEnvelope("goal", doc);
1094
+ return 0;
1095
+ }
1096
+ console.log(`Goal: ${formatDisplayValue(doc.goal)}`);
1097
+ console.log(`Metric: ${formatDisplayValue(doc.metric)} (${formatDisplayValue(doc.direction)})`);
1098
+ console.log(`Verify: ${formatDisplayValue(doc.verify)}`);
1099
+ if (doc.guard)
1100
+ console.log(`Guard: ${formatDisplayValue(doc.guard)}`);
1101
+ if (doc.file_map)
1102
+ console.log(`File map: ${formatDisplayValue(doc.file_map)}`);
1103
+ if (doc.constraints)
1104
+ console.log(`Constraints: ${formatDisplayValue(doc.constraints)}`);
1105
+ if (doc.stop_conditions)
1106
+ console.log(`Stop conditions: ${formatDisplayValue(doc.stop_conditions)}`);
1107
+ return 0;
1108
+ }
1109
+ if (subCmd !== "init") {
1110
+ console.error(`Unknown goal subcommand: ${subCmd}`);
1111
+ return 1;
1112
+ }
1113
+ const goalArgs = cmdArgs.slice(1);
1114
+ const goalParsed = parseArgs(goalArgs);
1115
+ const goalGrouped = {};
1116
+ for (const [k, v] of Object.entries(goalParsed)) {
1117
+ goalGrouped[k] = v;
1118
+ }
1119
+ const useGoalJson = goalGrouped.json === "true";
1120
+ const isGoalDryRun = goalGrouped["dry-run"] === "true";
1121
+ const { GOAL_TEMPLATES, getGoalTemplate, buildGoalDocument, buildGoalInitResult } = await import("./goal-init.js");
1122
+ const templateId = goalGrouped.template ?? "custom";
1123
+ if (!GOAL_TEMPLATES.find((t) => t.id === templateId)) {
1124
+ console.error(`Unknown template: ${templateId}. Valid: ${GOAL_TEMPLATES.map((t) => t.id).join(", ")}`);
1125
+ return 1;
1126
+ }
1127
+ const template = getGoalTemplate(templateId);
1128
+ const templateDefaults = template?.defaults ?? {};
1129
+ let config = {
1130
+ goal: goalGrouped.goal ?? templateDefaults.goal,
1131
+ metric: goalGrouped.metric ?? templateDefaults.metric,
1132
+ direction: goalGrouped.direction ?? templateDefaults.direction,
1133
+ verify: goalGrouped.verify ?? templateDefaults.verify,
1134
+ guard: goalGrouped.guard ?? templateDefaults.guard,
1135
+ mode: goalGrouped.mode ?? templateDefaults.mode,
1136
+ scope: goalGrouped.scope ?? templateDefaults.scope,
1137
+ iterations: goalGrouped.iterations ? parsePositiveInt(goalGrouped.iterations, "iterations") : templateDefaults.iterations,
1138
+ duration: goalGrouped.duration ?? templateDefaults.duration,
1139
+ stop_condition: goalGrouped["stop-condition"] ?? templateDefaults.stop_condition,
1140
+ rollback_strategy: goalGrouped["rollback-strategy"] ?? templateDefaults.rollback_strategy,
1141
+ template: templateId,
1142
+ };
1143
+ const isTTY = process.stdin.isTTY === true;
1144
+ const hasRequiredFlags = Boolean(config.goal && config.metric && config.verify);
1145
+ if (!hasRequiredFlags && !isTTY) {
1146
+ let stdinData = "";
1147
+ try {
1148
+ stdinData = await new Promise((resolve, reject) => {
1149
+ const chunks = [];
1150
+ process.stdin.on("data", (chunk) => chunks.push(chunk));
1151
+ process.stdin.on("end", () => resolve(Buffer.concat(chunks).toString("utf-8")));
1152
+ process.stdin.on("error", reject);
1153
+ setTimeout(() => resolve(""), 200);
1154
+ });
1155
+ stdinData = stdinData.trim();
1156
+ }
1157
+ catch {
1158
+ stdinData = "";
1159
+ }
1160
+ if (stdinData) {
1161
+ try {
1162
+ config = { ...config, ...JSON.parse(stdinData), template: templateId };
1163
+ }
1164
+ catch {
1165
+ console.error("Failed to parse stdin as JSON.");
1166
+ return 1;
1167
+ }
1168
+ }
1169
+ }
1170
+ if (!config.goal && isTTY) {
1171
+ const readline = await import("readline");
1172
+ const rl = readline.createInterface({ input: process.stdin, output: process.stderr });
1173
+ const ask = (prompt, defaultVal) => new Promise((resolve) => {
1174
+ rl.question(`${prompt}${defaultVal ? ` [${defaultVal}]` : ""}: `, (answer) => {
1175
+ resolve(answer.trim() || defaultVal || "");
1176
+ });
1177
+ });
1178
+ process.stderr.write("\nAutoresearch Goal Init \u2014 Interactive Wizard\n\n");
1179
+ if (!config.goal)
1180
+ config.goal = await ask("Goal (what outcome should this run optimize?)", config.goal);
1181
+ if (!config.metric)
1182
+ config.metric = await ask("Metric name", config.metric ?? "primary_metric");
1183
+ if (!config.direction)
1184
+ config.direction = await ask("Direction (lower/higher)", config.direction ?? "lower");
1185
+ if (!config.verify)
1186
+ config.verify = await ask("Verify command", config.verify);
1187
+ if (!config.guard) {
1188
+ const guard = await ask("Guard command (optional)");
1189
+ if (guard)
1190
+ config.guard = guard;
1191
+ }
1192
+ if (!config.scope)
1193
+ config.scope = await ask("Scope", config.scope ?? "current repository");
1194
+ if (!config.mode)
1195
+ config.mode = await ask("Mode (foreground/background)", config.mode ?? "foreground");
1196
+ rl.close();
1197
+ }
1198
+ const goalPath = resolvePath(goalGrouped.repo, goalGrouped["goal-path"], GOAL_DEFAULT);
1199
+ const document = buildGoalDocument(config);
1200
+ const result = buildGoalInitResult(goalPath, config, !hasRequiredFlags && isTTY);
1201
+ if (isGoalDryRun) {
1202
+ if (useGoalJson) {
1203
+ printJsonEnvelope("goal", { ...result, dry_run: true });
1204
+ }
1205
+ else {
1206
+ console.log("[dry-run] Would write goal document to: " + goalPath + "\n");
1207
+ console.log(document);
1208
+ }
1209
+ return 0;
1210
+ }
1211
+ atomicWriteTextInRepo(goalGrouped.repo, goalPath, document);
1212
+ if (useGoalJson) {
1213
+ printJsonEnvelope("goal", result);
1214
+ }
1215
+ else {
1216
+ console.log(`\u2713 Goal definition written to ${goalPath}`);
1217
+ console.log(` Goal: ${result.goal ?? "(unset)"}`);
1218
+ console.log(` Metric: ${result.metric ?? "(unset)"} (${result.direction})`);
1219
+ console.log(` Verify: ${result.verify ?? "(unset)"}`);
1220
+ console.log(` Mode: ${result.mode}`);
1221
+ if (result.template !== "custom")
1222
+ console.log(` Template: ${result.template}`);
1223
+ }
1224
+ return 0;
1225
+ }
1226
+ /* ── queue ── */
1227
+ export async function handleQueue(grouped, cmdArgs, useJson, _dryRun) {
1228
+ const subCmd = cmdArgs[0] || "list";
1229
+ if (subCmd === "help") {
1230
+ console.error("Usage: autoresearch queue <subcommand> [options]");
1231
+ console.error("Subcommands: list, enqueue, clean");
1232
+ return 0;
1233
+ }
1234
+ if (subCmd === "enqueue") {
1235
+ if (!grouped.goal || !grouped.metric || !grouped.verify) {
1236
+ console.error("--goal, --metric, and --verify are required for enqueue");
1237
+ return 1;
1238
+ }
1239
+ const { enqueueTasks } = await import("./task-queue.js");
1240
+ const tasks = await enqueueTasks(grouped.repo, [{ goal: grouped.goal, metric: grouped.metric, verify: grouped.verify }]);
1241
+ if (useJson) {
1242
+ printJson({ enqueued: tasks });
1243
+ }
1244
+ else {
1245
+ for (const t of tasks)
1246
+ console.log(`Enqueued: ${t.id} - ${t.goal}`);
1247
+ }
1248
+ return 0;
1249
+ }
1250
+ if (subCmd === "clean") {
1251
+ const { listTasks, writeManifest, resolveQueuePath } = await import("./task-queue.js");
1252
+ const queuePath = resolveQueuePath(grouped.repo);
1253
+ const manifest = await listTasks(grouped.repo);
1254
+ const before = manifest.tasks.length;
1255
+ manifest.tasks = manifest.tasks.filter((t) => t.status === "pending" || t.status === "leased");
1256
+ manifest.updated_at = new Date().toISOString();
1257
+ await writeManifest(queuePath, manifest, grouped.repo);
1258
+ const removed = before - manifest.tasks.length;
1259
+ if (useJson) {
1260
+ printJson({ removed });
1261
+ }
1262
+ else {
1263
+ console.log(`Cleaned ${removed} completed/failed tasks. ${manifest.tasks.length} remain.`);
1264
+ }
1265
+ return 0;
1266
+ }
1267
+ const { listTasks } = await import("./task-queue.js");
1268
+ const manifest = await listTasks(grouped.repo);
1269
+ if (useJson) {
1270
+ printJson(manifest);
1271
+ }
1272
+ else {
1273
+ if (manifest.tasks.length === 0) {
1274
+ console.log("No tasks in queue.");
1275
+ }
1276
+ else {
1277
+ console.log(`Task Queue (${manifest.tasks.length} tasks):`);
1278
+ for (const task of manifest.tasks) {
1279
+ const icon = task.status === "completed" ? "v" : task.status === "failed" ? "x" : task.status === "leased" ? ">" : "*";
1280
+ console.log(` ${icon} ${task.id} [${task.status}] ${task.goal}`);
1281
+ }
1282
+ }
1283
+ }
1284
+ return 0;
1285
+ }
1286
+ /* ── pack ── */
1287
+ export async function handlePack(grouped, cmdArgs, useJson) {
1288
+ const subCmd = cmdArgs[0] || "help";
1289
+ if (subCmd === "help" || (subCmd !== "export" && subCmd !== "list" && subCmd !== "inspect")) {
1290
+ console.error("Usage: autoresearch pack <subcommand> [options]");
1291
+ console.error("Subcommands: export, list, inspect");
1292
+ return 0;
1293
+ }
1294
+ if (subCmd === "export") {
1295
+ const { exportPack } = await import("./strategy-pack.js");
1296
+ const result = exportPack(grouped.repo, grouped["state-path"]);
1297
+ if (!result) {
1298
+ console.error("No run state found.");
1299
+ return 1;
1300
+ }
1301
+ if (useJson) {
1302
+ printJsonEnvelope("pack", { exported: result.path, pack: result.pack });
1303
+ }
1304
+ else {
1305
+ console.log(`Strategy pack exported: ${result.path}`);
1306
+ console.log(` Goal: ${result.pack.goal}`);
1307
+ console.log(` Metric: ${result.pack.metric}`);
1308
+ console.log(` Success: ${result.pack.evidence.success_rate}`);
1309
+ }
1310
+ return 0;
1311
+ }
1312
+ if (subCmd === "list") {
1313
+ const { listPacks } = await import("./strategy-pack.js");
1314
+ const packs = listPacks(grouped.repo);
1315
+ if (useJson) {
1316
+ printJsonEnvelope("pack", { packs });
1317
+ }
1318
+ else {
1319
+ console.log(packs.length === 0 ? "No strategy packs found." : `Strategy Packs (${packs.length}):`);
1320
+ for (const p of packs)
1321
+ console.log(` ${p.name}`);
1322
+ }
1323
+ return 0;
1324
+ }
1325
+ if (subCmd === "inspect") {
1326
+ const name = cmdArgs[1];
1327
+ if (!name) {
1328
+ console.error("Usage: autoresearch pack inspect <name>");
1329
+ return 1;
1330
+ }
1331
+ const { readPack } = await import("./strategy-pack.js");
1332
+ const content = readPack(grouped.repo, name);
1333
+ if (!content) {
1334
+ console.error(`Pack not found: ${name}`);
1335
+ return 1;
1336
+ }
1337
+ console.log(content);
1338
+ return 0;
1339
+ }
1340
+ return 0;
1341
+ }
1342
+ /* ── leaderboard ── */
1343
+ export async function handleLeaderboard(grouped, useJson) {
1344
+ const { generateLeaderboard, formatLeaderboardMarkdown, formatLeaderboardText } = await import("./leaderboard.js");
1345
+ const repo = resolveRepo(grouped.repo);
1346
+ const leaderboard = generateLeaderboard(repo);
1347
+ if (useJson) {
1348
+ printJson(leaderboard);
1349
+ return 0;
1350
+ }
1351
+ if (leaderboard.entries.length === 0) {
1352
+ console.log("No runs found.");
1353
+ return 0;
1354
+ }
1355
+ if (grouped.format === "markdown") {
1356
+ console.log(formatLeaderboardMarkdown(leaderboard));
1357
+ }
1358
+ else {
1359
+ console.log(formatLeaderboardText(leaderboard));
1360
+ }
1361
+ return 0;
1362
+ }
1363
+ /* ── worker ── */
1364
+ export async function handleWorker(grouped, useJson) {
1365
+ const once = grouped.once === "true";
1366
+ if (!once) {
1367
+ console.error("worker requires --once flag");
1368
+ return 1;
1369
+ }
1370
+ const { workerOnce } = await import("./worker.js");
1371
+ const result = workerOnce(grouped.repo, grouped["state-path"], grouped["results-path"]);
1372
+ if (useJson) {
1373
+ printJsonEnvelope("worker", result);
1374
+ }
1375
+ else {
1376
+ if (result.ready) {
1377
+ console.log(`\u2713 Ready for iteration ${result.iteration}`);
1378
+ console.log(` Run ID: ${result.run_id}`);
1379
+ console.log(` Status: ${result.status}`);
1380
+ console.log(` Goal: ${result.goal}`);
1381
+ }
1382
+ else {
1383
+ console.log(`\u2717 Not ready: ${result.reason || "unknown"}`);
1384
+ console.log(` Run ID: ${result.run_id}`);
1385
+ }
1386
+ }
1387
+ return result.ready ? 0 : 1;
1388
+ }
1389
+ //# sourceMappingURL=cli-commands.js.map