ralphctl 0.27.0 → 0.28.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.
@@ -0,0 +1,2772 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ AI_PROVIDERS,
4
+ AI_PROVIDERS_HINT,
5
+ ASSERT_SPRINT_STATUS,
6
+ AbortError,
7
+ AbsolutePath,
8
+ BUNDLED_SKILLS,
9
+ CLI_METADATA,
10
+ DEFAULT_SETTINGS,
11
+ DISPLAY_TEXT_MAX_CHARS,
12
+ FLOW_IDS,
13
+ IsoTimestamp,
14
+ PRESET_NAMES,
15
+ PROVIDER_BINARY,
16
+ ProjectId,
17
+ RALPHCTL_DEBUG_TRACE_ENV,
18
+ RALPHCTL_HOME_ENV,
19
+ Result,
20
+ SKILL_MOUNTING_FLOW_IDS,
21
+ SprintId,
22
+ StorageError,
23
+ TaskId,
24
+ TicketId,
25
+ ValidationError,
26
+ activateSprintUseCase,
27
+ addTicket,
28
+ applyPreset,
29
+ applySettingsKey,
30
+ approveTicketRequirements,
31
+ assertNoActivePeer,
32
+ assertSprintStatus,
33
+ bootstrapApp,
34
+ buildComposedSkillSource,
35
+ checkCli,
36
+ commandExists,
37
+ createAiProvider,
38
+ createAtomicWriteFile,
39
+ createBundledAgentDefinitionSource,
40
+ createCloseSprintFlow,
41
+ createCreatePrFlow,
42
+ createDoctorFlow,
43
+ createExportContextFlow,
44
+ createExportRequirementsFlow,
45
+ createFsProjectRepository,
46
+ createFsSprintExecutionRepository,
47
+ createFsSprintRepository,
48
+ createFsTaskRepository,
49
+ createJsonSettingsRepository,
50
+ createLastSelectionStore,
51
+ createOperatorAgentDefinitionSource,
52
+ createProject,
53
+ createRepository,
54
+ createRunner,
55
+ createSettingsApplyPresetFlow,
56
+ createSettingsSetFlow,
57
+ createSettingsSetProviderFlow,
58
+ createSettingsShowFlow,
59
+ createSkillsAdapter,
60
+ createSprintWithExecution,
61
+ createTask,
62
+ createTicket,
63
+ createTicketAddFlow,
64
+ createTicketPublishFlow,
65
+ createTicketRemoveFlow,
66
+ defaultAiSettingsForProvider,
67
+ detectInstalledProviders,
68
+ ensureCanonicalSprintDir,
69
+ ensureStorageRoots,
70
+ evaluationArtifactSprintPath,
71
+ evaluatorOutputContract,
72
+ flowRegistry,
73
+ foldOutcomeStats,
74
+ formatBytes,
75
+ formatDiffStat,
76
+ formatRelativeAge,
77
+ generatorOutputContract,
78
+ groupByFlow,
79
+ isAiProvider,
80
+ isPresetName,
81
+ latestRecordedEvaluation,
82
+ listRuns,
83
+ messageOf,
84
+ nameBlockedTasks,
85
+ parseDuration,
86
+ planSprint,
87
+ plural,
88
+ presetAdaptationLines,
89
+ probeTaskQuarantine,
90
+ quarantineStashMessage,
91
+ recommendedPriorWork,
92
+ reopenDoneSprint,
93
+ resolveEffort,
94
+ resolveSprintDir,
95
+ runCommand,
96
+ sanitizeDisplayText,
97
+ storagePathsFromRoot,
98
+ unblockTaskLocked,
99
+ warnIfVague
100
+ } from "./chunk-WAHQPVTG.mjs";
101
+
102
+ // src/application/ui/cli/cli.ts
103
+ import { Command } from "commander";
104
+
105
+ // src/application/ui/cli/parse-implement-role-overrides.ts
106
+ var parseRole = (role, provider, model) => {
107
+ if (provider === void 0 !== (model === void 0)) {
108
+ const [have, need] = provider === void 0 ? ["model", "provider"] : ["provider", "model"];
109
+ return {
110
+ ok: false,
111
+ error: `--implement-${role}-${have} requires --implement-${role}-${need} (both must be supplied together).`
112
+ };
113
+ }
114
+ if (provider === void 0 || model === void 0) {
115
+ return { ok: true };
116
+ }
117
+ if (!isAiProvider(provider)) {
118
+ return {
119
+ ok: false,
120
+ error: `--implement-${role}-provider: '${provider}' is not a supported provider (${AI_PROVIDERS_HINT}).`
121
+ };
122
+ }
123
+ const trimmedModel = model.trim();
124
+ if (trimmedModel.length === 0) {
125
+ return { ok: false, error: `--implement-${role}-model must be a non-empty string.` };
126
+ }
127
+ return { ok: true, row: { provider, model: trimmedModel } };
128
+ };
129
+ var parseImplementRoleOverrides = (input) => {
130
+ const generator = parseRole("generator", input.generatorProvider, input.generatorModel);
131
+ if (!generator.ok) return { ok: false, error: generator.error };
132
+ const evaluator = parseRole("evaluator", input.evaluatorProvider, input.evaluatorModel);
133
+ if (!evaluator.ok) return { ok: false, error: evaluator.error };
134
+ if (generator.row === void 0 && evaluator.row === void 0) {
135
+ return { ok: true, overrides: void 0 };
136
+ }
137
+ return {
138
+ ok: true,
139
+ overrides: {
140
+ ...generator.row !== void 0 ? { generator: generator.row } : {},
141
+ ...evaluator.row !== void 0 ? { evaluator: evaluator.row } : {}
142
+ }
143
+ };
144
+ };
145
+
146
+ // src/application/ui/cli/report-cli-error.ts
147
+ var fail = (message) => {
148
+ process.stderr.write(`error: ${message}
149
+ `);
150
+ process.exitCode = 1;
151
+ };
152
+ var okOrFail = (r, prefix = "") => {
153
+ if (!r.ok) fail(prefix + r.error.message);
154
+ return r.ok;
155
+ };
156
+ var EXIT_INTERRUPTED = 130;
157
+ var reportFatal = (err) => {
158
+ if (err instanceof AbortError) {
159
+ process.stderr.write("ralphctl: cancelled\n");
160
+ process.exitCode = EXIT_INTERRUPTED;
161
+ return;
162
+ }
163
+ const message = messageOf(err);
164
+ process.stderr.write(`ralphctl: ${message.trim()}
165
+ `);
166
+ const debug = process.env[RALPHCTL_DEBUG_TRACE_ENV];
167
+ if (typeof debug === "string" && debug.length > 0 && err instanceof Error && err.stack !== void 0) {
168
+ process.stderr.write(`${err.stack}
169
+ `);
170
+ } else {
171
+ process.stderr.write(`ralphctl: re-run with ${RALPHCTL_DEBUG_TRACE_ENV}=1 for the full stack
172
+ `);
173
+ }
174
+ process.exitCode = 1;
175
+ };
176
+
177
+ // src/application/ui/cli/resolve-sprint-selection.ts
178
+ var DEFAULT_MISSING_MESSAGE = "no sprint specified \u2014 pass --sprint <id> or pin one with `ralphctl sprint set-current <id>`";
179
+ var invalidId = (value, detail) => (
180
+ // Keep the long-established `invalid sprint id: …` stderr phrasing every command printed
181
+ // before this helper existed — scripts (and the e2e suite) match on it.
182
+ new ValidationError({ field: "sprint-id", value, message: `invalid sprint id: ${detail}` })
183
+ );
184
+ var resolveSprintId = async (raw, stateRoot, opts = {}) => {
185
+ if (raw !== void 0) {
186
+ const parsed2 = SprintId.parse(raw);
187
+ if (!parsed2.ok) return Result.error(invalidId(raw, parsed2.error.message));
188
+ return Result.ok({ sprintId: parsed2.value, fromPin: false });
189
+ }
190
+ const pinned = (await createLastSelectionStore(stateRoot).read())?.sprintId;
191
+ if (pinned === void 0) {
192
+ return Result.error(
193
+ new ValidationError({
194
+ field: "sprint",
195
+ value: void 0,
196
+ message: opts.missingMessage ?? DEFAULT_MISSING_MESSAGE
197
+ })
198
+ );
199
+ }
200
+ const parsed = SprintId.parse(String(pinned));
201
+ if (!parsed.ok) return Result.error(invalidId(pinned, `${parsed.error.message} (from the pinned selection)`));
202
+ return Result.ok({ sprintId: parsed.value, fromPin: true });
203
+ };
204
+ var pinFallbackNotice = (id) => `using current sprint ${String(id)} (from sprint set-current; pass --sprint to override)
205
+ `;
206
+ var SPRINT_OPTION_FLAGS = "-s, --sprint <id>";
207
+ var SPRINT_OPTION_DESC = "sprint id (defaults to the current sprint)";
208
+ var resolveSprintForCli = async (raw, stateRoot) => {
209
+ const resolved = await resolveSprintId(raw, stateRoot);
210
+ if (!okOrFail(resolved)) return void 0;
211
+ if (resolved.value.fromPin) process.stderr.write(pinFallbackNotice(resolved.value.sprintId));
212
+ return resolved.value.sprintId;
213
+ };
214
+ var resolveSprintAndIdForCli = async (rawSprint, stateRoot, rawId, parse, label) => {
215
+ const resolved = await resolveSprintId(rawSprint, stateRoot);
216
+ if (!okOrFail(resolved)) return void 0;
217
+ const id = parse(rawId);
218
+ if (!id.ok) {
219
+ fail(`invalid ${label} id: ${id.error.message}`);
220
+ return void 0;
221
+ }
222
+ if (resolved.value.fromPin) process.stderr.write(pinFallbackNotice(resolved.value.sprintId));
223
+ return { sprintId: resolved.value.sprintId, id: id.value };
224
+ };
225
+
226
+ // src/application/ui/cli/commands/export-requirements.ts
227
+ var exportRequirementsAction = async (opts) => {
228
+ const outputPath = AbsolutePath.parse(opts.output);
229
+ if (!okOrFail(outputPath, "--output: ")) return;
230
+ const { deps, storage } = await bootstrapApp();
231
+ const sprintId = await resolveSprintForCli(opts.sprint, storage.stateRoot);
232
+ if (sprintId === void 0) return;
233
+ const flow = createExportRequirementsFlow({
234
+ sprintRepo: deps.sprintRepo,
235
+ writeFile: deps.writeFile
236
+ });
237
+ const result = await flow.execute({
238
+ input: { sprintId, outputPath: outputPath.value }
239
+ });
240
+ if (!result.ok) {
241
+ fail(result.error.error.message);
242
+ return;
243
+ }
244
+ const out = result.value.ctx.output;
245
+ process.stdout.write(`wrote ${String(out.outputPath)} (${String(out.byteCount)} bytes)
246
+ `);
247
+ };
248
+ var registerExportRequirementsCommand = (program) => {
249
+ program.command("export-requirements").description("write the sprint's approved-ticket requirements to a markdown file").option("-s, --sprint <id>", "sprint id (defaults to the current sprint)").requiredOption("-o, --output <path>", "output markdown path").action(exportRequirementsAction);
250
+ };
251
+
252
+ // src/application/ui/cli/commands/export-context.ts
253
+ var exportContextAction = async (opts) => {
254
+ const outputPath = AbsolutePath.parse(opts.output);
255
+ if (!okOrFail(outputPath, "--output: ")) return;
256
+ let projectId;
257
+ if (opts.project !== void 0) {
258
+ const parsed = ProjectId.parse(opts.project);
259
+ if (!okOrFail(parsed, "invalid project id: ")) return;
260
+ projectId = parsed.value;
261
+ }
262
+ const { deps, storage } = await bootstrapApp();
263
+ const sprintId = await resolveSprintForCli(opts.sprint, storage.stateRoot);
264
+ if (sprintId === void 0) return;
265
+ const flow = createExportContextFlow({
266
+ sprintRepo: deps.sprintRepo,
267
+ projectRepo: deps.projectRepo,
268
+ taskRepo: deps.taskRepo,
269
+ writeFile: deps.writeFile
270
+ });
271
+ const result = await flow.execute({
272
+ input: {
273
+ sprintId,
274
+ ...projectId !== void 0 ? { projectId } : {},
275
+ outputPath: outputPath.value
276
+ }
277
+ });
278
+ if (!result.ok) {
279
+ fail(result.error.error.message);
280
+ return;
281
+ }
282
+ const out = result.value.ctx.output;
283
+ process.stdout.write(`wrote ${String(out.outputPath)} (${String(out.byteCount)} bytes)
284
+ `);
285
+ };
286
+ var registerExportContextCommand = (program) => {
287
+ program.command("export-context").description("render the harness-context markdown for a sprint").option("-s, --sprint <id>", "sprint id (defaults to the current sprint)").option("-p, --project <id>", "project id (defaults to the sprint's project)").requiredOption("-o, --output <path>", "output markdown path").action(exportContextAction);
288
+ };
289
+
290
+ // src/application/ui/cli/commands/create-pr.ts
291
+ var buildCreatePrFlow = (deps, storage, useAi) => {
292
+ const resolvedProvider = deps.settings.ai.createPr.provider;
293
+ const effort = resolveEffort("createPr", deps.settings);
294
+ const provider = createAiProvider({
295
+ flow: "createPr",
296
+ ai: deps.settings.ai,
297
+ harnessConfig: deps.settings.harness,
298
+ eventBus: deps.eventBus,
299
+ childRegistry: deps.childRegistry
300
+ });
301
+ const skillSource = buildComposedSkillSource(
302
+ { app: deps, storage },
303
+ {},
304
+ resolvedProvider,
305
+ "create-pr",
306
+ deps.settings,
307
+ {}
308
+ );
309
+ const skillsAdapter = createSkillsAdapter({ provider: resolvedProvider, logger: deps.logger });
310
+ return createCreatePrFlow(
311
+ {
312
+ sprintRepo: deps.sprintRepo,
313
+ sprintExecutionRepo: deps.sprintExecutionRepo,
314
+ taskRepo: deps.taskRepo,
315
+ pullRequestCreator: deps.pullRequestCreator,
316
+ gitRunner: deps.gitRunner,
317
+ eventBus: deps.eventBus,
318
+ clock: deps.clock,
319
+ provider,
320
+ templateLoader: deps.templateLoader,
321
+ writeFile: deps.writeFile,
322
+ logger: deps.logger,
323
+ model: deps.settings.ai.createPr.model,
324
+ ...effort !== void 0 ? { effort } : {},
325
+ skillSource,
326
+ skillsAdapter
327
+ },
328
+ { useAi, providerId: resolvedProvider }
329
+ );
330
+ };
331
+ var createPrAction = async (opts) => {
332
+ const cwdInput = opts.cwd ?? process.cwd();
333
+ const cwd = AbsolutePath.parse(cwdInput);
334
+ if (!okOrFail(cwd, "--cwd: ")) return;
335
+ const { deps, storage } = await bootstrapApp();
336
+ const sprintId = await resolveSprintForCli(opts.sprint, storage.stateRoot);
337
+ if (sprintId === void 0) return;
338
+ if (opts.ai) {
339
+ const gate = await checkCli("create-pr", deps.settings);
340
+ if (gate !== void 0 && !gate.ok) {
341
+ fail(gate.reason);
342
+ return;
343
+ }
344
+ }
345
+ const resolvedDir = await resolveSprintDir(storage.dataRoot, sprintId);
346
+ if (resolvedDir === void 0) {
347
+ fail("sprint dir: not found on disk");
348
+ return;
349
+ }
350
+ const sprintDir = AbsolutePath.parse(resolvedDir);
351
+ if (!okOrFail(sprintDir, "sprint dir: ")) return;
352
+ const flow = buildCreatePrFlow(deps, storage, opts.ai);
353
+ const result = await flow.execute({
354
+ input: {
355
+ sprintId,
356
+ cwd: cwd.value,
357
+ sprintDir: sprintDir.value,
358
+ base: opts.base,
359
+ draft: opts.draft,
360
+ ...opts.title !== void 0 ? { title: opts.title } : {},
361
+ ...opts.body !== void 0 ? { body: opts.body } : {}
362
+ }
363
+ });
364
+ if (!result.ok) {
365
+ fail(result.error.error.message);
366
+ return;
367
+ }
368
+ process.stdout.write(`opened PR ${result.value.ctx.output.url}
369
+ `);
370
+ };
371
+ var registerCreatePrCommand = (program) => {
372
+ program.command("create-pr").description("open a PR for the sprint's branch and persist the URL").option("-s, --sprint <id>", "sprint id (defaults to the current sprint)").option("--cwd <path>", "repository root (defaults to process.cwd())").option("-b, --base <branch>", "target branch", "main").option("--draft", "open as draft", false).option("-t, --title <title>", "override the derived PR title").option("--body <body>", "override the derived PR body").option("--no-ai", "skip AI content generation, use the template only").action(createPrAction);
373
+ };
374
+
375
+ // src/application/ui/cli/commands/doctor.ts
376
+ var PROBE_TAG = { pass: "OK ", warn: "WARN", unknown: "UNK ", fail: "FAIL" };
377
+ var printProbe = (probe) => {
378
+ const detail = probe.detail !== void 0 ? ` \u2014 ${probe.detail}` : "";
379
+ process.stdout.write(`${PROBE_TAG[probe.status]} ${probe.label}${detail}
380
+ `);
381
+ if (probe.hint !== void 0 && probe.status !== "pass") {
382
+ process.stdout.write(` hint: ${probe.hint}
383
+ `);
384
+ }
385
+ };
386
+ var doctorAction = async () => {
387
+ const { deps, storage } = await bootstrapApp();
388
+ const flow = createDoctorFlow({
389
+ projectRepo: deps.projectRepo,
390
+ sprintRepo: deps.sprintRepo,
391
+ sprintExecutionRepo: deps.sprintExecutionRepo,
392
+ settingsRepo: deps.settingsRepo,
393
+ commandExists,
394
+ runCommand,
395
+ nodeVersion: process.version
396
+ });
397
+ const result = await flow.execute({
398
+ input: { dataRoot: storage.dataRoot, configRoot: storage.configRoot }
399
+ });
400
+ if (!result.ok) {
401
+ fail(result.error.error.message);
402
+ return;
403
+ }
404
+ const report = result.value.ctx.output;
405
+ for (const probe of report.probes) printProbe(probe);
406
+ process.exitCode = report.hasFailures ? 1 : 0;
407
+ };
408
+ var registerDoctorCommand = (program) => {
409
+ program.command("doctor").description("run sanity probes against storage roots and core repositories").action(doctorAction);
410
+ };
411
+
412
+ // src/application/ui/cli/commands/demo.ts
413
+ import { randomUUID } from "crypto";
414
+ import { promises as fs2 } from "fs";
415
+ import { tmpdir } from "os";
416
+ import { join as join3 } from "path";
417
+
418
+ // src/application/demo/seed.ts
419
+ import { promises as fs } from "fs";
420
+ import { join } from "path";
421
+ var DEMO_MARKER_FILENAME = ".ralphctl-demo";
422
+ var unwrap = (r) => {
423
+ if (!r.ok) throw r.error;
424
+ return r.value;
425
+ };
426
+ var runGit = async (runCommand2, repoDir, args) => {
427
+ const r = await runCommand2("git", ["-C", repoDir, ...args]);
428
+ if (!r.ok) {
429
+ throw new StorageError({
430
+ subCode: "io",
431
+ message: `git ${args.join(" ")} failed in ${repoDir}: ${r.stderr || `exit ${String(r.code)}`}`,
432
+ path: repoDir
433
+ });
434
+ }
435
+ };
436
+ var seedMockRepo = async (runCommand2, repoDir) => {
437
+ await fs.mkdir(repoDir, { recursive: true });
438
+ await fs.writeFile(
439
+ join(repoDir, "hello.py"),
440
+ '# Placeholder greeting \u2014 the implement sprint changes this to print "Hello, world!".\nprint("hi from the mock project")\n'
441
+ );
442
+ await fs.writeFile(
443
+ join(repoDir, "README.md"),
444
+ "# hello-python (ralphctl demo)\n\nThrowaway project for exercising the refine / plan / implement flows.\n\nRun: `python3 hello.py`\n"
445
+ );
446
+ await runGit(runCommand2, repoDir, ["init", "--initial-branch=main"]);
447
+ await runGit(runCommand2, repoDir, ["config", "user.name", "ralphctl demo"]);
448
+ await runGit(runCommand2, repoDir, ["config", "user.email", "demo@ralphctl.local"]);
449
+ await runGit(runCommand2, repoDir, ["add", "-A"]);
450
+ await runGit(runCommand2, repoDir, ["commit", "-m", "chore: scaffold hello-python demo"]);
451
+ };
452
+ var seedRefineSprint = async (repos, project, token) => {
453
+ const s1 = unwrap(createSprintWithExecution({ name: `ready to refine \xB7 ${token}`, projectId: project.id }));
454
+ const s1Draft = unwrap(
455
+ addTicket(
456
+ s1.sprint,
457
+ unwrap(
458
+ createTicket({
459
+ title: "Greet the user by name",
460
+ description: "The greeting should address the person running the script. Details TBD \u2014 needs refinement."
461
+ })
462
+ )
463
+ )
464
+ );
465
+ unwrap(await repos.sprintRepo.save(s1Draft));
466
+ unwrap(await repos.executionRepo.save(s1.execution));
467
+ return { name: s1Draft.name, state: "draft \u2014 run Refine" };
468
+ };
469
+ var seedPlanSprint = async (repos, project, token) => {
470
+ const s2 = unwrap(createSprintWithExecution({ name: `ready to plan \xB7 ${token}`, projectId: project.id }));
471
+ const s2Ticket = unwrap(
472
+ approveTicketRequirements(
473
+ unwrap(createTicket({ title: "Add a --name CLI argument to the greeting" })),
474
+ [
475
+ "## Requirements",
476
+ "",
477
+ "- Accept an optional `--name <value>` argument.",
478
+ "- When provided, print `Hello, <value>!`.",
479
+ "- When omitted, fall back to `Hello, world!`.",
480
+ "- `python3 hello.py` must still exit 0 in both cases."
481
+ ].join("\n")
482
+ )
483
+ );
484
+ const s2Draft = unwrap(addTicket(s2.sprint, s2Ticket));
485
+ unwrap(await repos.sprintRepo.save(s2Draft));
486
+ unwrap(await repos.executionRepo.save(s2.execution));
487
+ return { name: s2Draft.name, state: "draft (ticket approved) \u2014 run Plan" };
488
+ };
489
+ var seedImplementSprint = async (repos, project, repo, token, now) => {
490
+ const s3 = unwrap(createSprintWithExecution({ name: `ready to implement \xB7 ${token}`, projectId: project.id }));
491
+ const s3Ticket = unwrap(
492
+ approveTicketRequirements(
493
+ unwrap(createTicket({ title: 'Print "Hello, world!" from hello.py' })),
494
+ [
495
+ "## Requirements",
496
+ "",
497
+ "- `hello.py` prints exactly `Hello, world!` (followed by a newline).",
498
+ "- `python3 hello.py` exits 0."
499
+ ].join("\n")
500
+ )
501
+ );
502
+ const s3Draft = unwrap(addTicket(s3.sprint, s3Ticket));
503
+ const s3Planned = unwrap(planSprint(s3Draft, now));
504
+ const task = unwrap(
505
+ createTask({
506
+ name: 'Make hello.py print "Hello, world!"',
507
+ description: "Replace the placeholder greeting with the canonical hello-world output.",
508
+ order: 1,
509
+ ticketId: s3Ticket.id,
510
+ repositoryId: repo.id,
511
+ steps: [
512
+ "Open hello.py.",
513
+ "Replace the print statement so it outputs exactly: Hello, world!",
514
+ "Confirm `python3 hello.py` exits 0."
515
+ ],
516
+ verificationCriteria: [
517
+ {
518
+ id: "C1",
519
+ assertion: 'Running hello.py prints "Hello, world!" and exits 0',
520
+ check: "auto",
521
+ command: "python3 hello.py"
522
+ }
523
+ ]
524
+ })
525
+ );
526
+ unwrap(await repos.sprintRepo.save(s3Planned));
527
+ unwrap(await repos.executionRepo.save(s3.execution));
528
+ unwrap(await repos.taskRepo.saveAll(s3Planned.id, [task]));
529
+ return { name: s3Planned.name, state: "planned (1 task queued) \u2014 run Implement" };
530
+ };
531
+ var seedDemoWorkspace = async (deps, input) => {
532
+ const { runCommand: runCommand2, writeFile } = deps;
533
+ const { homeDir, token } = input;
534
+ try {
535
+ const repoDirStr = join(String(homeDir), "demo-repos", `hello-python-${token}`);
536
+ const repoDir = unwrap(AbsolutePath.parse(repoDirStr));
537
+ const paths = unwrap(storagePathsFromRoot(homeDir));
538
+ unwrap(await ensureStorageRoots(paths));
539
+ await seedMockRepo(runCommand2, repoDirStr);
540
+ const repos = {
541
+ sprintRepo: createFsSprintRepository({ root: paths.dataRoot }),
542
+ executionRepo: createFsSprintExecutionRepository({ root: paths.dataRoot }),
543
+ taskRepo: createFsTaskRepository({ root: paths.dataRoot })
544
+ };
545
+ const projectRepo = createFsProjectRepository({ root: paths.dataRoot });
546
+ const now = IsoTimestamp.now();
547
+ const repo = unwrap(createRepository({ path: repoDir, name: "hello-python", verifyScript: "python3 hello.py" }));
548
+ const project = unwrap(
549
+ createProject({
550
+ displayName: `Hello Python (demo ${token})`,
551
+ description: "Throwaway sandbox project for exercising refine / plan / implement.",
552
+ repositories: [repo]
553
+ })
554
+ );
555
+ unwrap(await projectRepo.save(project));
556
+ const sprints = [
557
+ await seedRefineSprint(repos, project, token),
558
+ await seedPlanSprint(repos, project, token),
559
+ await seedImplementSprint(repos, project, repo, token, now)
560
+ ];
561
+ const markerPath = unwrap(AbsolutePath.parse(join(String(homeDir), DEMO_MARKER_FILENAME)));
562
+ unwrap(await writeFile(markerPath, `${String(now)}
563
+ `));
564
+ return Result.ok({ homeDir, repoDir, projectName: project.displayName, sprints });
565
+ } catch (cause) {
566
+ if (cause instanceof StorageError) return Result.error(cause);
567
+ return Result.error(cause);
568
+ }
569
+ };
570
+
571
+ // src/application/demo/seed-settings.ts
572
+ var PRESET_FOR_PROVIDER = {
573
+ "claude-code": "claude-only",
574
+ "github-copilot": "copilot-only",
575
+ "openai-codex": "codex-only",
576
+ opencode: "opencode-only",
577
+ "xai-grok": "grok-only"
578
+ };
579
+ var DEMO_PROVIDER_PREFERENCE = [
580
+ "claude-code",
581
+ "openai-codex",
582
+ "github-copilot",
583
+ "opencode",
584
+ "xai-grok"
585
+ ];
586
+ var pickDemoPreset = (installed) => {
587
+ const chosen = DEMO_PROVIDER_PREFERENCE.find((provider) => installed.has(provider)) ?? "claude-code";
588
+ return PRESET_FOR_PROVIDER[chosen];
589
+ };
590
+ var seedDemoSettings = async (input) => {
591
+ const paths = storagePathsFromRoot(input.homeDir);
592
+ if (!paths.ok) return Result.error(paths.error);
593
+ const installed = await (input.detect ?? detectInstalledProviders)();
594
+ const preset = pickDemoPreset(installed);
595
+ const settings = applyPreset(preset, DEFAULT_SETTINGS);
596
+ const saved = await createJsonSettingsRepository({ configRoot: paths.value.configRoot }).save(settings);
597
+ if (!saved.ok) return Result.error(saved.error);
598
+ return Result.ok({ preset, noCliDetected: installed.size === 0 });
599
+ };
600
+
601
+ // src/application/demo/scripted-spawn.ts
602
+ import { mkdirSync, writeFileSync } from "fs";
603
+ import { dirname, isAbsolute, join as join2 } from "path";
604
+
605
+ // src/integration/ai/providers/_engine/scripted-spawn.ts
606
+ import { EventEmitter } from "events";
607
+ var createScriptedStdin = (onStdin) => {
608
+ const stdin = new EventEmitter();
609
+ const record = (data) => {
610
+ if (typeof data === "string") onStdin?.(data);
611
+ else if (data instanceof Buffer) onStdin?.(data.toString("utf8"));
612
+ };
613
+ return Object.assign(stdin, {
614
+ write: (data) => {
615
+ record(data);
616
+ return true;
617
+ },
618
+ end: (data) => {
619
+ record(data);
620
+ },
621
+ destroy: () => {
622
+ }
623
+ });
624
+ };
625
+ var createScriptedStream = () => {
626
+ const stream = new EventEmitter();
627
+ return Object.assign(stream, {
628
+ setEncoding: () => {
629
+ },
630
+ destroy: () => {
631
+ }
632
+ });
633
+ };
634
+ var createScriptedChild = (source, hooks = {}) => {
635
+ const child = new EventEmitter();
636
+ const stdout = createScriptedStream();
637
+ const stderr = createScriptedStream();
638
+ const stdin = createScriptedStdin(hooks.onStdin);
639
+ let resolved;
640
+ let exited = false;
641
+ const finish = (code, signal, lateStdout = []) => {
642
+ if (exited) return;
643
+ exited = true;
644
+ child.emit("exit", code, signal);
645
+ setTimeout(() => {
646
+ for (const chunk of lateStdout) stdout.emit("data", chunk);
647
+ child.emit("close", code, signal);
648
+ }, 0);
649
+ };
650
+ Object.assign(child, {
651
+ stdout,
652
+ stderr,
653
+ stdin,
654
+ pid: 0,
655
+ killed: false,
656
+ kill: (signal) => {
657
+ const named = typeof signal === "number" || signal === void 0 ? "SIGTERM" : signal;
658
+ hooks.onKill?.(named);
659
+ if (named === "SIGTERM" && resolved?.trapsSigterm === true) return true;
660
+ setTimeout(() => finish(null, named), 0);
661
+ return true;
662
+ }
663
+ });
664
+ setTimeout(() => {
665
+ resolved = typeof source === "function" ? source() : source;
666
+ for (const chunk of resolved.stdoutChunks ?? []) stdout.emit("data", chunk);
667
+ for (const chunk of resolved.stderrChunks ?? []) stderr.emit("data", chunk);
668
+ if (resolved.hang === true) return;
669
+ const { exitCode = 0, exitSignal = null, exitDelayMs = 0, stdoutAfterExit } = resolved;
670
+ setTimeout(() => finish(exitCode, exitSignal, stdoutAfterExit), exitDelayMs);
671
+ }, 0);
672
+ return child;
673
+ };
674
+
675
+ // src/application/demo/transcript.ts
676
+ var DEMO_TARGET_FILE = "hello.py";
677
+ var DEMO_CRITERION_COMMAND = `node -e "const s=require('node:fs').readFileSync('hello.py','utf8'); if (!s.includes('Hello, world!')) process.exit(1)"`;
678
+ var DEMO_VERIFY_SCRIPT = `node -e "const s=require('node:fs').readFileSync('hello.py','utf8'); if (!s.includes('print(')) process.exit(1)"`;
679
+ var ROUND_1_SOURCE = ["# Greeting for the ralphctl demo sprint.", 'print("Hello world")', ""].join("\n");
680
+ var ROUND_2_SOURCE = ["# Greeting for the ralphctl demo sprint.", 'print("Hello, world!")', ""].join("\n");
681
+ var generatorSignals = (signals) => signals;
682
+ var evaluatorSignals = (signals) => signals;
683
+ var generatorRoundOne = (at) => ({
684
+ role: "generator",
685
+ round: 1,
686
+ files: [{ path: DEMO_TARGET_FILE, content: ROUND_1_SOURCE }],
687
+ schemaVersion: generatorOutputContract.schemaVersion,
688
+ signals: generatorSignals([
689
+ { type: "change", text: `rewrote the greeting in ${DEMO_TARGET_FILE}`, timestamp: at },
690
+ {
691
+ type: "decision",
692
+ text: "Kept the single print statement rather than introducing a helper \u2014 the task asks for one literal line of output.",
693
+ timestamp: at
694
+ },
695
+ {
696
+ type: "task-verified",
697
+ output: `$ ${DEMO_CRITERION_COMMAND}
698
+ Hello world`,
699
+ timestamp: at
700
+ },
701
+ {
702
+ type: "commit-message",
703
+ subject: "feat(demo): print a greeting from hello.py",
704
+ body: "Why: the placeholder greeting predates the demo task and says nothing about the sprint.",
705
+ timestamp: at
706
+ },
707
+ { type: "task-complete", timestamp: at }
708
+ ]),
709
+ body: `Updated ${DEMO_TARGET_FILE} to print a greeting and ran the acceptance command.`
710
+ });
711
+ var evaluatorRoundOne = (at) => ({
712
+ role: "evaluator",
713
+ round: 1,
714
+ files: [],
715
+ schemaVersion: evaluatorOutputContract.schemaVersion,
716
+ signals: evaluatorSignals([
717
+ {
718
+ type: "evaluation",
719
+ status: "failed",
720
+ dimensions: [
721
+ {
722
+ dimension: "correctness",
723
+ passed: false,
724
+ finding: `${DEMO_TARGET_FILE}:2 prints "Hello world" \u2014 the criterion requires the exact string "Hello, world!" (comma + exclamation mark).`,
725
+ executionEvidence: `$ ${DEMO_CRITERION_COMMAND}
726
+ exit 1`
727
+ },
728
+ { dimension: "completeness", passed: true, finding: "the only file the task names was edited" },
729
+ { dimension: "safety", passed: true, finding: "no shell, network, or dependency changes" },
730
+ {
731
+ dimension: "consistency",
732
+ passed: true,
733
+ finding: `the comment above the statement still describes ${DEMO_TARGET_FILE} accurately`
734
+ },
735
+ {
736
+ dimension: "robustness",
737
+ passed: true,
738
+ applicable: false,
739
+ finding: "a single literal print statement has no error path to harden"
740
+ }
741
+ ],
742
+ criteria: [
743
+ {
744
+ id: "C1",
745
+ passed: false,
746
+ evidence: `the acceptance command exited 1 \u2014 "Hello, world!" is absent from ${DEMO_TARGET_FILE}`
747
+ }
748
+ ],
749
+ critique: `Correctness: ${DEMO_TARGET_FILE}:2 must print exactly "Hello, world!" \u2014 add the comma and the exclamation mark, then re-run the acceptance command.`,
750
+ timestamp: at
751
+ },
752
+ {
753
+ type: "note",
754
+ text: "The generator reported the command output but not its exit status \u2014 the string mismatch is only visible in the exit code.",
755
+ timestamp: at
756
+ }
757
+ ]),
758
+ body: "FAILED \u2014 correctness: the printed string does not match the acceptance criterion."
759
+ });
760
+ var generatorRoundTwo = (at) => ({
761
+ role: "generator",
762
+ round: 2,
763
+ files: [{ path: DEMO_TARGET_FILE, content: ROUND_2_SOURCE }],
764
+ schemaVersion: generatorOutputContract.schemaVersion,
765
+ signals: generatorSignals([
766
+ {
767
+ type: "change",
768
+ text: `${DEMO_TARGET_FILE}:2 now prints the exact string "Hello, world!"`,
769
+ timestamp: at
770
+ },
771
+ {
772
+ type: "learning",
773
+ text: "Quote the acceptance string verbatim from the criterion instead of retyping it \u2014 punctuation is part of the contract.",
774
+ context: "the first round dropped the comma and the exclamation mark",
775
+ appliesTo: "tasks whose criteria assert exact output",
776
+ timestamp: at
777
+ },
778
+ {
779
+ type: "task-verified",
780
+ output: `$ ${DEMO_CRITERION_COMMAND}
781
+ exit 0`,
782
+ timestamp: at
783
+ },
784
+ {
785
+ type: "commit-message",
786
+ subject: "fix(demo): print the exact greeting the criterion asks for",
787
+ body: "Why: the first attempt dropped the comma and the exclamation mark, so the acceptance command exited 1.",
788
+ timestamp: at
789
+ },
790
+ { type: "task-complete", timestamp: at }
791
+ ]),
792
+ body: `Applied the critique \u2014 ${DEMO_TARGET_FILE} now prints "Hello, world!" and the acceptance command exits 0.`
793
+ });
794
+ var evaluatorRoundTwo = (at) => ({
795
+ role: "evaluator",
796
+ round: 2,
797
+ files: [],
798
+ schemaVersion: evaluatorOutputContract.schemaVersion,
799
+ signals: evaluatorSignals([
800
+ {
801
+ type: "evaluation",
802
+ status: "passed",
803
+ dimensions: [
804
+ {
805
+ dimension: "correctness",
806
+ passed: true,
807
+ finding: `${DEMO_TARGET_FILE}:2 prints "Hello, world!" exactly`,
808
+ executionEvidence: `$ ${DEMO_CRITERION_COMMAND}
809
+ exit 0`
810
+ },
811
+ {
812
+ dimension: "completeness",
813
+ passed: true,
814
+ finding: "every step the task lists is reflected in the diff"
815
+ },
816
+ { dimension: "safety", passed: true, finding: "no shell, network, or dependency changes" },
817
+ {
818
+ dimension: "consistency",
819
+ passed: true,
820
+ finding: "the surrounding comment still matches the statement below it"
821
+ },
822
+ {
823
+ dimension: "robustness",
824
+ passed: true,
825
+ applicable: false,
826
+ finding: "a single literal print statement has no error path to harden"
827
+ }
828
+ ],
829
+ criteria: [{ id: "C1", passed: true, evidence: "the acceptance command exited 0 on the committed tree" }],
830
+ critique: "No remaining findings \u2014 the prior round's correctness gap is closed.",
831
+ timestamp: at
832
+ }
833
+ ]),
834
+ body: "PASSED \u2014 the acceptance criterion holds on the committed tree."
835
+ });
836
+ var demoTranscript = (now = IsoTimestamp.now) => {
837
+ const at = now();
838
+ return {
839
+ beats: [generatorRoundOne(at), evaluatorRoundOne(at), generatorRoundTwo(at), evaluatorRoundTwo(at)]
840
+ };
841
+ };
842
+ var beatFor = (transcript, role, round) => {
843
+ const forRole = transcript.beats.filter((b) => b.role === role);
844
+ if (forRole.length === 0) return void 0;
845
+ const exact = forRole.find((b) => b.round === round);
846
+ if (exact !== void 0) return exact;
847
+ return round < 1 ? forRole[0] : forRole[forRole.length - 1];
848
+ };
849
+
850
+ // src/application/demo/scripted-spawn.ts
851
+ var ROUND_SIGNALS_PATH_RE = /(?:[A-Za-z]:)?[\\/][^\s`'"]*[\\/]rounds[\\/](\d+)[\\/](generator|evaluator)[\\/]signals\.json/g;
852
+ var resolveSpawnTarget = (prompt) => {
853
+ const matches = [...prompt.matchAll(ROUND_SIGNALS_PATH_RE)];
854
+ const match = matches[matches.length - 1];
855
+ if (match === void 0) return void 0;
856
+ const round = Number.parseInt(match[1] ?? "", 10);
857
+ const role = match[2];
858
+ return { signalsPath: match[0], role, round: Number.isInteger(round) ? round : 1 };
859
+ };
860
+ var writeFileAt = (path, content) => {
861
+ mkdirSync(dirname(path), { recursive: true });
862
+ writeFileSync(path, content, "utf8");
863
+ };
864
+ var applyBeat = (beat, target, cwd) => {
865
+ for (const file of beat.files) {
866
+ if (cwd === void 0) continue;
867
+ writeFileAt(isAbsolute(file.path) ? file.path : join2(cwd, file.path), file.content);
868
+ }
869
+ writeFileAt(
870
+ target.signalsPath,
871
+ `${JSON.stringify({ schemaVersion: beat.schemaVersion, signals: beat.signals }, null, 2)}
872
+ `
873
+ );
874
+ };
875
+ var flagValue = (args, flag) => {
876
+ const index = args.indexOf(flag);
877
+ return index === -1 ? void 0 : args[index + 1];
878
+ };
879
+ var streamLines = (beat, sessionId, model) => {
880
+ const lines = [
881
+ { type: "system", subtype: "init", session_id: sessionId, ...model !== void 0 ? { model } : {} },
882
+ {
883
+ type: "assistant",
884
+ session_id: sessionId,
885
+ message: { content: [{ type: "text", text: beat.body }] }
886
+ },
887
+ { type: "result", subtype: "success", session_id: sessionId, result: beat.body }
888
+ ];
889
+ return `${lines.map((line) => JSON.stringify(line)).join("\n")}
890
+ `;
891
+ };
892
+ var sessionIdFor = (role) => `demo-scripted-${role}`;
893
+ var unscriptedScript = (reason) => ({
894
+ stderrChunks: [`ralphctl demo: ${reason}
895
+ `],
896
+ exitCode: 1
897
+ });
898
+ var createDemoProviderSpawn = (transcript, { sessionDelayMs: exitDelayMs = 0 } = {}) => {
899
+ return (_command, args, options) => {
900
+ let prompt = "";
901
+ return createScriptedChild(
902
+ () => {
903
+ const target = resolveSpawnTarget(prompt);
904
+ if (target === void 0) {
905
+ return unscriptedScript("this session asked for an output path the demo transcript does not script");
906
+ }
907
+ const beat = beatFor(transcript, target.role, target.round);
908
+ if (beat === void 0) {
909
+ return unscriptedScript(`no scripted beat for ${target.role} round ${String(target.round)}`);
910
+ }
911
+ applyBeat(beat, target, options.cwd);
912
+ return {
913
+ stdoutChunks: [streamLines(beat, sessionIdFor(target.role), flagValue(args, "--model"))],
914
+ exitDelayMs
915
+ };
916
+ },
917
+ {
918
+ onStdin: (chunk) => {
919
+ prompt += chunk;
920
+ }
921
+ }
922
+ );
923
+ };
924
+ };
925
+
926
+ // src/application/demo/scripted-run.ts
927
+ var scriptedDemoSettings = () => ({
928
+ ...DEFAULT_SETTINGS,
929
+ ai: defaultAiSettingsForProvider("claude-code"),
930
+ harness: {
931
+ ...DEFAULT_SETTINGS.harness,
932
+ // The transcript converges on round 2; a rung that fires past it would spawn a session the
933
+ // transcript has no beat for.
934
+ bestOfNCandidates: 0,
935
+ escalateOnPlateau: false
936
+ }
937
+ });
938
+ var withDemoVerifyScript = (project) => ({
939
+ ...project,
940
+ repositories: project.repositories.map((repo) => ({ ...repo, verifyScript: DEMO_VERIFY_SCRIPT }))
941
+ });
942
+ var withDemoCriterionCommand = (task) => ({
943
+ ...task,
944
+ verificationCriteria: task.verificationCriteria.map(
945
+ (criterion) => criterion.check === "auto" ? { ...criterion, command: DEMO_CRITERION_COMMAND } : criterion
946
+ )
947
+ });
948
+ var prepareScriptedDemo = async (input) => {
949
+ const paths = storagePathsFromRoot(input.homeDir);
950
+ if (!paths.ok) return Result.error(paths.error);
951
+ const settings = scriptedDemoSettings();
952
+ const saved = await createJsonSettingsRepository({ configRoot: paths.value.configRoot }).save(settings);
953
+ if (!saved.ok) return Result.error(saved.error);
954
+ const projectRepo = createFsProjectRepository({ root: paths.value.dataRoot });
955
+ const projects = await projectRepo.list();
956
+ if (!projects.ok) return Result.error(projects.error);
957
+ for (const project of projects.value) {
958
+ const written = await projectRepo.save(withDemoVerifyScript(project));
959
+ if (!written.ok) return Result.error(written.error);
960
+ }
961
+ const sprintRepo = createFsSprintRepository({ root: paths.value.dataRoot });
962
+ const taskRepo = createFsTaskRepository({ root: paths.value.dataRoot });
963
+ const sprints = await sprintRepo.list();
964
+ if (!sprints.ok) return Result.error(sprints.error);
965
+ for (const sprint of sprints.value) {
966
+ const tasks = await taskRepo.findBySprintId(sprint.id);
967
+ if (!tasks.ok) return Result.error(tasks.error);
968
+ if (tasks.value.length === 0) continue;
969
+ const written = await taskRepo.saveAll(sprint.id, tasks.value.map(withDemoCriterionCommand));
970
+ if (!written.ok) return Result.error(written.error);
971
+ }
972
+ const transcript = input.now === void 0 ? demoTranscript() : demoTranscript(input.now);
973
+ const providerSpawn = createDemoProviderSpawn(transcript, { sessionDelayMs: input.sessionDelayMs });
974
+ return Result.ok({ providerSpawn, settings });
975
+ };
976
+
977
+ // src/application/ui/cli/commands/demo.ts
978
+ var DEFAULT_DEMO_HOME = join3(tmpdir(), "ralphctl-demo");
979
+ var pathExists = async (p) => fs2.access(p).then(() => true).catch(() => false);
980
+ var prepareSandbox = async (homeDirStr) => {
981
+ if (!await pathExists(homeDirStr)) return void 0;
982
+ const hasMarker = await pathExists(join3(homeDirStr, DEMO_MARKER_FILENAME));
983
+ if (!hasMarker) {
984
+ return `${homeDirStr} already exists and was not created by 'ralphctl demo' (no ${DEMO_MARKER_FILENAME} marker) \u2014 refusing to wipe it. Pass --home <dir> to pick an empty or demo-owned directory instead.`;
985
+ }
986
+ await fs2.rm(homeDirStr, { recursive: true, force: true });
987
+ return void 0;
988
+ };
989
+ var printSummary = (summary, homeDirStr, settingsLine) => {
990
+ const line = "\u2500".repeat(60);
991
+ process.stdout.write(
992
+ [
993
+ "",
994
+ line,
995
+ " Demo workspace seeded \u2713",
996
+ line,
997
+ ` home : ${homeDirStr}`,
998
+ ` repo : ${String(summary.repoDir)} (git-initialised, python3 hello.py)`,
999
+ ` project : ${summary.projectName}`,
1000
+ ...settingsLine === void 0 ? [] : [` ai : ${settingsLine}`],
1001
+ "",
1002
+ " Sprints:",
1003
+ ...summary.sprints.map((s) => ` \u2022 "${s.name}" ${s.state}`),
1004
+ line,
1005
+ ""
1006
+ ].join("\n")
1007
+ );
1008
+ };
1009
+ var describeSeededSettings = (seeded) => seeded.noCliDetected ? `${seeded.preset} preset (no AI CLI on PATH \u2014 placeholder; install one and run doctor)` : `${seeded.preset} preset (from the AI CLIs detected on PATH)`;
1010
+ var demoAction = async (opts) => {
1011
+ const homeDirStr = opts.home ?? DEFAULT_DEMO_HOME;
1012
+ const homeDir = AbsolutePath.parse(homeDirStr);
1013
+ if (!homeDir.ok) {
1014
+ fail(`--home must be an absolute path: ${homeDirStr}`);
1015
+ return;
1016
+ }
1017
+ const refusal = await prepareSandbox(homeDirStr);
1018
+ if (refusal !== void 0) {
1019
+ fail(refusal);
1020
+ return;
1021
+ }
1022
+ const seeded = await seedDemoWorkspace(
1023
+ { runCommand, writeFile: createAtomicWriteFile() },
1024
+ { homeDir: homeDir.value, token: randomUUID().slice(0, 8) }
1025
+ );
1026
+ if (!okOrFail(seeded, "seeding failed: ")) return;
1027
+ const seededSettings = await seedDemoSettings({ homeDir: homeDir.value });
1028
+ if (!okOrFail(seededSettings, "settings seeding failed: ")) return;
1029
+ let providerSpawn;
1030
+ if (opts.script === true) {
1031
+ const scripted = await prepareScriptedDemo({ homeDir: homeDir.value });
1032
+ if (!okOrFail(scripted, "scripted demo setup failed: ")) return;
1033
+ providerSpawn = scripted.value.providerSpawn;
1034
+ }
1035
+ printSummary(
1036
+ seeded.value,
1037
+ homeDirStr,
1038
+ opts.script === true ? void 0 : describeSeededSettings(seededSettings.value)
1039
+ );
1040
+ if (opts.script === true) {
1041
+ process.stdout.write(
1042
+ ' Scripted mode: every AI row is pinned to claude-code and replays a canned two-round\n generator \u2192 evaluator transcript. No provider CLI is spawned; the verify script and\n acceptance criterion are rewritten to portable node one-liners.\n The transcript covers the Implement flow \u2014 the "ready to implement" sprint is the one\n to launch. Interactive flows (refine / plan / ideate) still need a real, authenticated\n provider CLI, in demo mode too.\n\n'
1043
+ );
1044
+ }
1045
+ if (!opts.launch) {
1046
+ process.stdout.write(
1047
+ `Launch it with:
1048
+
1049
+ RALPHCTL_HOME=${homeDirStr} ralphctl
1050
+
1051
+ ` + (opts.script === true ? `Or reproduce the whole scripted run:
1052
+
1053
+ ralphctl demo --script --home ${homeDirStr}
1054
+
1055
+ ` : "")
1056
+ );
1057
+ return;
1058
+ }
1059
+ process.stdout.write(
1060
+ opts.script === true ? "Launching the TUI against the sandbox with the scripted transcript\u2026\n\n" : "Launching the TUI against the sandbox\u2026\n\n"
1061
+ );
1062
+ process.env[RALPHCTL_HOME_ENV] = homeDirStr;
1063
+ const { launchTui } = await import("./launch-VBRTQP4L.mjs");
1064
+ await launchTui({ ...providerSpawn !== void 0 ? { providerSpawn } : {} });
1065
+ };
1066
+ var registerDemoCommand = (program) => {
1067
+ program.command("demo").description("seed a throwaway sandbox project and launch the TUI against it \u2014 zero setup required").option("--home <dir>", "sandbox directory (default: a fixed path under the OS tmp dir)").option("--no-launch", "seed only \u2014 print the launch command instead of opening the TUI").option("--script", "replay a canned generator/evaluator transcript instead of spawning a real AI CLI").action(demoAction);
1068
+ };
1069
+
1070
+ // src/application/ui/cli/commands/settings.ts
1071
+ var parseProviderKey = (key2) => {
1072
+ const implementMatch = /^ai\.implement\.(generator|evaluator)\.provider$/.exec(key2);
1073
+ if (implementMatch !== null) return { flow: "implement", role: implementMatch[1] };
1074
+ const flatMatch = /^ai\.(refine|plan|readiness|ideate|createPr)\.provider$/.exec(key2);
1075
+ if (flatMatch !== null) return { flow: flatMatch[1] };
1076
+ return void 0;
1077
+ };
1078
+ var showSettingsAction = async () => {
1079
+ const { deps } = await bootstrapApp();
1080
+ const flow = createSettingsShowFlow({ settingsRepo: deps.settingsRepo });
1081
+ const result = await flow.execute({ input: void 0 });
1082
+ if (!result.ok) {
1083
+ fail(result.error.error.message);
1084
+ return;
1085
+ }
1086
+ process.stdout.write(`${JSON.stringify(result.value.ctx.output, null, 2)}
1087
+ `);
1088
+ };
1089
+ var setProviderKeyAction = async (key2, value, role, flowId, settingsRepo) => {
1090
+ if (!isAiProvider(value)) {
1091
+ fail(`'${value}' is not a recognised provider (expected one of: ${AI_PROVIDERS.join(", ")})`);
1092
+ return;
1093
+ }
1094
+ const providerFlow = createSettingsSetProviderFlow({ settingsRepo });
1095
+ const saved = await providerFlow.execute({
1096
+ input: {
1097
+ flow: flowId,
1098
+ provider: value,
1099
+ ...role !== void 0 ? { role } : {}
1100
+ }
1101
+ });
1102
+ if (!saved.ok) {
1103
+ const err = saved.error.error;
1104
+ const hint = "hint" in err && typeof err.hint === "string" ? ` (${err.hint})` : "";
1105
+ fail(`${err.message}${hint}`);
1106
+ return;
1107
+ }
1108
+ process.stdout.write(`${key2} = ${value}
1109
+ `);
1110
+ };
1111
+ var setSettingsAction = async (key2, value) => {
1112
+ const { deps } = await bootstrapApp();
1113
+ const providerKey = parseProviderKey(key2);
1114
+ if (providerKey !== void 0) {
1115
+ await setProviderKeyAction(key2, value, providerKey.role, providerKey.flow, deps.settingsRepo);
1116
+ return;
1117
+ }
1118
+ const showFlow = createSettingsShowFlow({ settingsRepo: deps.settingsRepo });
1119
+ const current = await showFlow.execute({ input: void 0 });
1120
+ if (!current.ok) {
1121
+ fail(current.error.error.message);
1122
+ return;
1123
+ }
1124
+ const next = applySettingsKey(current.value.ctx.output, key2, value);
1125
+ if (!okOrFail(next)) return;
1126
+ const setFlow = createSettingsSetFlow({ settingsRepo: deps.settingsRepo });
1127
+ const saved = await setFlow.execute({ input: { next: next.value } });
1128
+ if (!saved.ok) {
1129
+ fail(saved.error.error.message);
1130
+ return;
1131
+ }
1132
+ process.stdout.write(`${key2} = ${value}
1133
+ `);
1134
+ };
1135
+ var applyPresetAction = async (name) => {
1136
+ if (!isPresetName(name)) {
1137
+ fail(`unknown preset '${name}' \u2014 expected one of: ${PRESET_NAMES.join(", ")}`);
1138
+ return;
1139
+ }
1140
+ const { deps } = await bootstrapApp();
1141
+ const flow = createSettingsApplyPresetFlow({
1142
+ settingsRepo: deps.settingsRepo,
1143
+ availableModelsFor: deps.availableModelsFor
1144
+ });
1145
+ const result = await flow.execute({ input: { preset: name } });
1146
+ if (!result.ok) {
1147
+ fail(result.error.error.message);
1148
+ return;
1149
+ }
1150
+ const output = result.value.ctx.output;
1151
+ for (const w of output.warnings) {
1152
+ process.stderr.write(
1153
+ `warning: ${PROVIDER_BINARY[w.provider]} CLI not found on PATH; affects flows: ${w.flows.join(", ")}
1154
+ `
1155
+ );
1156
+ }
1157
+ for (const line of presetAdaptationLines(output)) process.stderr.write(`note: ${line}
1158
+ `);
1159
+ process.stdout.write(`applied preset ${name}
1160
+ `);
1161
+ };
1162
+ var registerSettingsCommand = (program) => {
1163
+ const settings = program.command("settings").description("inspect and mutate ralphctl settings");
1164
+ settings.command("show").description("print the current settings as JSON").action(showSettingsAction);
1165
+ settings.command("set <key> <value>").description("mutate one setting and persist (read-modify-write, schema-validated)").action(setSettingsAction);
1166
+ settings.command("apply-preset <name>").description(`stamp a preset onto ai.* (one of: ${PRESET_NAMES.join(", ")})`).action(applyPresetAction);
1167
+ };
1168
+
1169
+ // src/application/ui/cli/completion.ts
1170
+ var bashScript = (commands) => `# ralphctl bash completion \u2014 source from ~/.bashrc
1171
+ _ralphctl_complete() {
1172
+ local cur="\${COMP_WORDS[COMP_CWORD]}"
1173
+ local commands="${commands.join(" ")}"
1174
+ COMPREPLY=( $(compgen -W "$commands" -- "$cur") )
1175
+ }
1176
+ complete -F _ralphctl_complete ralphctl
1177
+ `;
1178
+ var zshScript = (commands) => `#compdef ralphctl
1179
+ # ralphctl zsh completion \u2014 source from ~/.zshrc (or place under $fpath)
1180
+ _ralphctl() {
1181
+ local -a commands
1182
+ commands=(${commands.map((c) => `'${c}'`).join(" ")})
1183
+ _describe 'command' commands
1184
+ }
1185
+ _ralphctl "$@"
1186
+ `;
1187
+ var generateCompletion = (shell, commands) => {
1188
+ const sorted = [...new Set(commands)].sort();
1189
+ switch (shell) {
1190
+ case "bash":
1191
+ return bashScript(sorted);
1192
+ case "zsh":
1193
+ return zshScript(sorted);
1194
+ }
1195
+ };
1196
+
1197
+ // src/application/ui/cli/commands/completion.ts
1198
+ var SUPPORTED = ["bash", "zsh"];
1199
+ var registerCompletionCommand = (program) => {
1200
+ program.command("completion <shell>").description(`print a shell-completion script (supported: ${SUPPORTED.join(", ")})`).action((shell) => {
1201
+ if (!SUPPORTED.includes(shell)) {
1202
+ process.stderr.write(`error: unsupported shell '${shell}' \u2014 supported: ${SUPPORTED.join(", ")}
1203
+ `);
1204
+ process.exitCode = 1;
1205
+ return;
1206
+ }
1207
+ const commands = program.commands.map((c) => c.name());
1208
+ process.stdout.write(generateCompletion(shell, commands));
1209
+ });
1210
+ };
1211
+
1212
+ // src/application/ui/cli/confirm-destructive.ts
1213
+ import { createInterface } from "readline";
1214
+ var confirmDestructive = async (opts) => {
1215
+ if (opts.yes) return true;
1216
+ if (process.stdin.isTTY !== true) {
1217
+ process.stderr.write(
1218
+ `error: refusing to ${opts.action} without confirmation on a non-TTY stdin \u2014 re-run with --yes to bypass${opts.nonTtyHint ?? ""}
1219
+ `
1220
+ );
1221
+ process.exitCode = 1;
1222
+ return false;
1223
+ }
1224
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
1225
+ try {
1226
+ const answer = await askLine(rl, opts.confirmPrompt);
1227
+ if (answer === void 0) {
1228
+ process.stdout.write("aborted\n");
1229
+ process.exitCode = 130;
1230
+ return false;
1231
+ }
1232
+ const confirmed = isYes(answer.trim());
1233
+ if (!confirmed) process.stdout.write("aborted\n");
1234
+ return confirmed;
1235
+ } finally {
1236
+ rl.close();
1237
+ }
1238
+ };
1239
+ var askLine = (rl, prompt) => new Promise((resolve) => {
1240
+ rl.once("close", () => resolve(void 0));
1241
+ rl.question(prompt, resolve);
1242
+ });
1243
+ var isYes = (answer) => {
1244
+ const lower = answer.toLowerCase();
1245
+ return lower === "y" || lower === "yes";
1246
+ };
1247
+
1248
+ // src/application/ui/cli/commands/project.ts
1249
+ var listProjectsAction = async () => {
1250
+ const { deps } = await bootstrapApp();
1251
+ const result = await deps.projectRepo.list();
1252
+ if (!okOrFail(result)) return;
1253
+ if (result.value.length === 0) {
1254
+ process.stdout.write("(no projects yet \u2014 create one in the TUI)\n");
1255
+ return;
1256
+ }
1257
+ for (const p of result.value) {
1258
+ process.stdout.write(`${formatProjectLine(p)}
1259
+ `);
1260
+ }
1261
+ };
1262
+ var showProjectAction = async (raw) => {
1263
+ const { deps, storage } = await bootstrapApp();
1264
+ let effectiveRaw = raw;
1265
+ if (effectiveRaw === void 0) {
1266
+ const pinned = await createLastSelectionStore(storage.stateRoot).read();
1267
+ if (pinned?.projectId === void 0) {
1268
+ fail("no current project \u2014 pick one in the TUI or pass an id");
1269
+ return;
1270
+ }
1271
+ effectiveRaw = String(pinned.projectId);
1272
+ }
1273
+ const id = ProjectId.parse(effectiveRaw);
1274
+ if (!okOrFail(id, "invalid project id: ")) return;
1275
+ const result = await deps.projectRepo.findById(id.value);
1276
+ if (!okOrFail(result)) return;
1277
+ process.stdout.write(`${JSON.stringify(result.value, null, 2)}
1278
+ `);
1279
+ };
1280
+ var removeProjectAction = async (raw, opts) => {
1281
+ const id = ProjectId.parse(raw);
1282
+ if (!okOrFail(id, "invalid project id: ")) return;
1283
+ const confirmed = await confirmDestructive({
1284
+ yes: opts.yes === true,
1285
+ action: `remove project ${String(id.value)}`,
1286
+ confirmPrompt: `remove project ${String(id.value)}? [y/N] `
1287
+ });
1288
+ if (!confirmed) return;
1289
+ const { deps, storage } = await bootstrapApp();
1290
+ const result = await deps.projectRepo.remove(id.value);
1291
+ if (!okOrFail(result)) return;
1292
+ const store = createLastSelectionStore(storage.stateRoot);
1293
+ const cur = await store.read();
1294
+ if (cur?.projectId === id.value) await store.write(void 0);
1295
+ process.stdout.write(`removed project ${String(id.value)}
1296
+ `);
1297
+ };
1298
+ var registerProjectCommand = (program) => {
1299
+ const project = program.command("project").description("inspect and manage projects");
1300
+ project.command("list").description("list all registered projects").action(listProjectsAction);
1301
+ project.command("show [id]").description("print a single project as JSON (defaults to the current project)").action(showProjectAction);
1302
+ project.command("remove <id>").description("delete a project (does not touch sprints or repository contents)").option("-y, --yes", "skip the interactive y/N confirmation").action(removeProjectAction);
1303
+ };
1304
+ var formatProjectLine = (p) => {
1305
+ const repos = p.repositories.length;
1306
+ return `${String(p.id)} ${String(p.slug).padEnd(24)} ${p.displayName} (${plural(repos, "repo")})`;
1307
+ };
1308
+
1309
+ // src/application/ui/cli/commands/sprint.ts
1310
+ import { join as join4 } from "path";
1311
+
1312
+ // src/business/sprint/reopen-sprint.ts
1313
+ var reopenDoneSprintUseCase = async (props) => {
1314
+ const log = props.logger.named("sprint.reopen-done");
1315
+ if (props.sprint.status === "review") {
1316
+ log.debug("already review, skipping", { sprintId: props.sprint.id });
1317
+ return Result.ok(props.sprint);
1318
+ }
1319
+ const conflictCheck = await assertNoActivePeer(props.sprint, props.sprintRepo, log, "reopen");
1320
+ if (!conflictCheck.ok) return Result.error(conflictCheck.error);
1321
+ log.debug("reopening sprint", { sprintId: props.sprint.id, from: props.sprint.status });
1322
+ const transitioned = reopenDoneSprint(props.sprint, props.clock());
1323
+ if (!transitioned.ok) {
1324
+ log.warn("invalid state transition", {
1325
+ sprintId: props.sprint.id,
1326
+ from: props.sprint.status,
1327
+ error: transitioned.error.message
1328
+ });
1329
+ return Result.error(transitioned.error);
1330
+ }
1331
+ const persisted = await props.sprintRepo.save(transitioned.value);
1332
+ if (!persisted.ok) {
1333
+ log.error("save failed", { sprintId: transitioned.value.id, error: persisted.error.message });
1334
+ return Result.error(persisted.error);
1335
+ }
1336
+ log.info(`reopened sprint '${transitioned.value.slug}' done \u2192 review`, {
1337
+ sprintId: transitioned.value.id,
1338
+ reviewAt: transitioned.value.reviewAt
1339
+ });
1340
+ return Result.ok(transitioned.value);
1341
+ };
1342
+
1343
+ // src/application/ui/cli/commands/sprint.ts
1344
+ var INVALID_SPRINT_ID = "invalid sprint id: ";
1345
+ var showName = (name) => sanitizeDisplayText(name, DISPLAY_TEXT_MAX_CHARS);
1346
+ var listSprintsAction = async () => {
1347
+ const { deps } = await bootstrapApp();
1348
+ const result = await deps.sprintRepo.list();
1349
+ if (!okOrFail(result)) return;
1350
+ if (result.value.length === 0) {
1351
+ process.stdout.write("(no sprints yet \u2014 create one in the TUI)\n");
1352
+ return;
1353
+ }
1354
+ for (const s of result.value) {
1355
+ process.stdout.write(`${formatSprintLine(s)}
1356
+ `);
1357
+ }
1358
+ };
1359
+ var showSprintAction = async (raw) => {
1360
+ const { deps, storage } = await bootstrapApp();
1361
+ const id = await resolveSprintId(raw, storage.stateRoot, {
1362
+ missingMessage: "no current sprint pinned \u2014 run `ralphctl sprint set-current <id>` or pass an id"
1363
+ });
1364
+ if (!okOrFail(id)) return;
1365
+ const result = await deps.sprintRepo.findById(id.value.sprintId);
1366
+ if (!okOrFail(result)) return;
1367
+ process.stdout.write(`${JSON.stringify(result.value, null, 2)}
1368
+ `);
1369
+ };
1370
+ var removeSprintAction = async (raw, opts) => {
1371
+ const id = SprintId.parse(raw);
1372
+ if (!okOrFail(id, INVALID_SPRINT_ID)) return;
1373
+ const confirmed = await confirmDestructive({
1374
+ yes: opts.yes === true,
1375
+ action: `remove sprint ${String(id.value)}`,
1376
+ confirmPrompt: `remove sprint ${String(id.value)} (cascades to its execution + tasks)? [y/N] `
1377
+ });
1378
+ if (!confirmed) return;
1379
+ const { deps, storage } = await bootstrapApp();
1380
+ const result = await deps.sprintRemoval.remove(id.value);
1381
+ if (!okOrFail(result)) return;
1382
+ const store = createLastSelectionStore(storage.stateRoot);
1383
+ const cur = await store.read();
1384
+ if (cur?.sprintId === id.value) {
1385
+ const { sprintId: _drop, ...rest } = cur;
1386
+ void _drop;
1387
+ await store.write(rest);
1388
+ }
1389
+ process.stdout.write(`removed sprint ${String(id.value)}
1390
+ `);
1391
+ };
1392
+ var activateSprintAction = async (raw) => {
1393
+ const id = SprintId.parse(raw);
1394
+ if (!okOrFail(id, INVALID_SPRINT_ID)) return;
1395
+ const { deps } = await bootstrapApp();
1396
+ const loaded = await deps.sprintRepo.findById(id.value);
1397
+ if (!okOrFail(loaded)) return;
1398
+ const result = await activateSprintUseCase({
1399
+ sprint: loaded.value,
1400
+ sprintRepo: deps.sprintRepo,
1401
+ clock: deps.clock,
1402
+ logger: deps.logger
1403
+ });
1404
+ if (!okOrFail(result)) return;
1405
+ process.stdout.write(`activated sprint '${result.value.slug}' (${String(result.value.id)})
1406
+ `);
1407
+ };
1408
+ var reopenSprintAction = async (raw) => {
1409
+ const id = SprintId.parse(raw);
1410
+ if (!okOrFail(id, INVALID_SPRINT_ID)) return;
1411
+ const { deps } = await bootstrapApp();
1412
+ const loaded = await deps.sprintRepo.findById(id.value);
1413
+ if (!okOrFail(loaded)) return;
1414
+ const wasAlreadyReview = loaded.value.status === "review";
1415
+ const result = await reopenDoneSprintUseCase({
1416
+ sprint: loaded.value,
1417
+ sprintRepo: deps.sprintRepo,
1418
+ clock: deps.clock,
1419
+ logger: deps.logger
1420
+ });
1421
+ if (!okOrFail(result)) return;
1422
+ const transitionLine = wasAlreadyReview ? `sprint '${result.value.slug}' (${String(result.value.id)}) is already in review \u2014 nothing to reopen
1423
+ ` : `reopened sprint '${result.value.slug}' (${String(result.value.id)}) done \u2192 review
1424
+ `;
1425
+ process.stdout.write(
1426
+ `${transitionLine} unblock its blocked tasks (ralphctl task unblock <id>) to make them runnable again
1427
+ `
1428
+ );
1429
+ };
1430
+ var confirmBlockedTasksOrStop = async (sprint, taskRepo, yes) => {
1431
+ const tasksLoaded = await taskRepo.findBySprintId(sprint.id);
1432
+ if (!tasksLoaded.ok) {
1433
+ fail(tasksLoaded.error.message);
1434
+ return void 0;
1435
+ }
1436
+ const blockedTasks = tasksLoaded.value.filter((t) => t.status === "blocked");
1437
+ if (blockedTasks.length === 0) return blockedTasks;
1438
+ const confirmed = await confirmDestructive({
1439
+ yes,
1440
+ action: `close sprint ${String(sprint.id)} with ${String(blockedTasks.length)} task(s) still blocked: ${nameBlockedTasks(blockedTasks)}`,
1441
+ confirmPrompt: `${String(blockedTasks.length)} task(s) are blocked: ${nameBlockedTasks(blockedTasks)}. Closing won't refuse, but a done sprint needs reopening (unblock one to do that) before they can run again. Close anyway? [y/N] `
1442
+ });
1443
+ return confirmed ? blockedTasks : void 0;
1444
+ };
1445
+ var closeSprintAction = async (raw, opts) => {
1446
+ const id = SprintId.parse(raw);
1447
+ if (!okOrFail(id, INVALID_SPRINT_ID)) return;
1448
+ const { deps, storage } = await bootstrapApp();
1449
+ const loaded = await deps.sprintRepo.findById(id.value);
1450
+ if (!okOrFail(loaded)) return;
1451
+ const sprint = loaded.value;
1452
+ const closable = assertSprintStatus(sprint, ["review"], ASSERT_SPRINT_STATUS);
1453
+ if (!okOrFail(closable)) return;
1454
+ const blockedTasks = await confirmBlockedTasksOrStop(sprint, deps.taskRepo, opts.yes === true);
1455
+ if (blockedTasks === void 0) return;
1456
+ const sprintDir = AbsolutePath.parse(await ensureCanonicalSprintDir(storage.dataRoot, sprint.id, sprint.slug));
1457
+ if (!okOrFail(sprintDir)) return;
1458
+ const progressPath = AbsolutePath.parse(join4(String(sprintDir.value), "progress.md"));
1459
+ if (!okOrFail(progressPath)) return;
1460
+ const project = await deps.projectRepo.findById(sprint.projectId);
1461
+ const memoryMirror = project.ok ? { writeFile: deps.writeFile, memoryRoot: storage.memoryRoot, projectId: String(project.value.id) } : void 0;
1462
+ const element = createCloseSprintFlow({
1463
+ sprintRepo: deps.sprintRepo,
1464
+ fileLocker: deps.fileLocker,
1465
+ locksRoot: storage.locksRoot,
1466
+ sprintDir: sprintDir.value,
1467
+ eventBus: deps.eventBus,
1468
+ clock: deps.clock,
1469
+ logger: deps.logger,
1470
+ appendFile: deps.appendFile,
1471
+ writeFile: deps.writeFile,
1472
+ progressFile: progressPath.value,
1473
+ ...memoryMirror !== void 0 ? { memoryMirror } : {}
1474
+ });
1475
+ const runner = createRunner({
1476
+ id: `cli-sprint-close-${String(id.value)}`,
1477
+ element,
1478
+ initialCtx: { sprintId: id.value, distillRequested: false }
1479
+ });
1480
+ let failure;
1481
+ const unsubscribe = runner.subscribe((event) => {
1482
+ if (event.type === "failed") failure = event.error;
1483
+ });
1484
+ await runner.start();
1485
+ unsubscribe();
1486
+ if (runner.status === "aborted") {
1487
+ fail("close was aborted internally \u2014 please retry");
1488
+ return;
1489
+ }
1490
+ if (runner.status === "failed") {
1491
+ fail(failure?.message ?? "sprint close failed");
1492
+ return;
1493
+ }
1494
+ const closed = runner.ctx.sprint;
1495
+ if (closed === void 0) {
1496
+ fail("close completed but returned no sprint \u2014 please retry");
1497
+ return;
1498
+ }
1499
+ process.stdout.write(`closed sprint '${closed.slug}' (${String(closed.id)})
1500
+ `);
1501
+ if (blockedTasks.length > 0) {
1502
+ process.stdout.write(
1503
+ ` note: ${String(blockedTasks.length)} task(s) stayed blocked: ${nameBlockedTasks(blockedTasks)}
1504
+ recover with: ralphctl task unblock <id> (reopens this sprint automatically)
1505
+ `
1506
+ );
1507
+ }
1508
+ };
1509
+ var setCurrentSprintAction = async (raw) => {
1510
+ const id = SprintId.parse(raw);
1511
+ if (!okOrFail(id, INVALID_SPRINT_ID)) return;
1512
+ const { deps, storage } = await bootstrapApp();
1513
+ const sprint = await deps.sprintRepo.findById(id.value);
1514
+ if (!okOrFail(sprint)) return;
1515
+ const project = await deps.projectRepo.findById(sprint.value.projectId);
1516
+ if (!okOrFail(project, "project lookup failed: ")) return;
1517
+ const store = createLastSelectionStore(storage.stateRoot);
1518
+ await store.write({
1519
+ projectId: project.value.id,
1520
+ projectLabel: project.value.displayName,
1521
+ sprintId: sprint.value.id
1522
+ });
1523
+ process.stdout.write(`pinned current sprint to '${sprint.value.slug}' (${String(sprint.value.id)})
1524
+ `);
1525
+ };
1526
+ var progressSprintAction = async (raw) => {
1527
+ const { deps, storage } = await bootstrapApp();
1528
+ const resolved = await resolveSprintId(raw, storage.stateRoot, {
1529
+ missingMessage: "no current sprint pinned \u2014 run `ralphctl sprint set-current <id>` or pass an id"
1530
+ });
1531
+ if (!okOrFail(resolved)) return;
1532
+ const sprintId = resolved.value.sprintId;
1533
+ const sprint = await deps.sprintRepo.findById(sprintId);
1534
+ if (!okOrFail(sprint)) return;
1535
+ const tasks = await deps.taskRepo.findBySprintId(sprintId);
1536
+ if (!okOrFail(tasks)) return;
1537
+ const execution = await deps.sprintExecutionRepo.findById(sprintId);
1538
+ const branchLine = execution.ok ? execution.value.branch !== null ? execution.value.branch : "(no branch assigned yet \u2014 first implement run will assign one)" : "(no execution record \u2014 sprint never started)";
1539
+ process.stdout.write(formatProgress(sprint.value, tasks.value, branchLine));
1540
+ };
1541
+ var registerSprintCommand = (program) => {
1542
+ const sprintCmd = program.command("sprint").description("inspect and manage sprints");
1543
+ sprintCmd.command("list").description("list all sprints").action(listSprintsAction);
1544
+ sprintCmd.command("show [id]").description("print a single sprint as JSON (defaults to the current sprint)").action(showSprintAction);
1545
+ sprintCmd.command("remove <id>").description("delete a sprint (cascades to its execution + tasks)").option("-y, --yes", "skip the interactive y/N confirmation").action(removeSprintAction);
1546
+ sprintCmd.command("activate <id>").description("transition a planned sprint to active (idempotent \u2014 already-active passes through)").action(activateSprintAction);
1547
+ sprintCmd.command("close <id>").description(
1548
+ "transition a review sprint to done (rejects any other status); confirms first if any task is still blocked"
1549
+ ).option("-y, --yes", "skip the blocked-task confirmation when any task is still blocked").action(closeSprintAction);
1550
+ sprintCmd.command("reopen <id>").description(
1551
+ "reopen a closed sprint back to review so its blocked work is reachable again (idempotent \u2014 already-review passes through)"
1552
+ ).action(reopenSprintAction);
1553
+ sprintCmd.command("set-current <id>").description(
1554
+ "pin a sprint as the user's current selection (read by the TUI on launch and used as the default sprint for CLI commands)"
1555
+ ).action(setCurrentSprintAction);
1556
+ sprintCmd.command("progress [id]").description("print task counts, blockers, and the sprint branch (defaults to the current sprint)").action(progressSprintAction);
1557
+ };
1558
+ var groupBlockedTasks = (allTasks) => {
1559
+ const byId = new Map(allTasks.map((t) => [t.id, t]));
1560
+ const rootIdOf = (task) => {
1561
+ const seen = /* @__PURE__ */ new Set([task.id]);
1562
+ let current = task;
1563
+ for (; ; ) {
1564
+ if (current.status !== "blocked") return task.id;
1565
+ if (current.blockKind !== "upstream") return current.id;
1566
+ const deps = current.dependsOn.map((depId) => byId.get(depId));
1567
+ const nextDep = deps.find((d) => d !== void 0 && d.status !== "done");
1568
+ if (nextDep === void 0 || seen.has(nextDep.id)) return current.id;
1569
+ seen.add(nextDep.id);
1570
+ current = nextDep;
1571
+ }
1572
+ };
1573
+ const groups = /* @__PURE__ */ new Map();
1574
+ for (const t of allTasks) {
1575
+ if (t.status !== "blocked") continue;
1576
+ const rootId = rootIdOf(t);
1577
+ const rootCandidate = byId.get(rootId);
1578
+ const root = rootCandidate !== void 0 && rootCandidate.status === "blocked" ? rootCandidate : t;
1579
+ const existing = groups.get(rootId);
1580
+ if (existing === void 0) groups.set(rootId, { root, waiting: t.id === rootId ? [] : [t] });
1581
+ else if (t.id !== rootId) groups.set(rootId, { ...existing, waiting: [...existing.waiting, t] });
1582
+ }
1583
+ return [...groups.values()];
1584
+ };
1585
+ var formatProgress = (sprint, tasks, branchLine) => {
1586
+ const total = tasks.length;
1587
+ const done = tasks.filter((t) => t.status === "done").length;
1588
+ const inProgress = tasks.filter((t) => t.status === "in_progress").length;
1589
+ const todo = tasks.filter((t) => t.status === "todo").length;
1590
+ const blocked = tasks.filter((t) => t.status === "blocked");
1591
+ const lines = [];
1592
+ lines.push(`Progress \u2014 ${sprint.name}`);
1593
+ lines.push(` id ${String(sprint.id)}`);
1594
+ lines.push(` status ${sprint.status}`);
1595
+ lines.push(` branch ${branchLine}`);
1596
+ lines.push(
1597
+ ` tasks ${String(done)}/${String(total)} done \xB7 ${String(inProgress)} in progress \xB7 ${String(todo)} todo \xB7 ${String(blocked.length)} blocked`
1598
+ );
1599
+ if (blocked.length > 0) {
1600
+ lines.push("");
1601
+ lines.push(`Blockers (${String(blocked.length)})`);
1602
+ for (const group of groupBlockedTasks(tasks)) {
1603
+ const reasonFirstLine = group.root.blockedReason.split("\n")[0] ?? group.root.blockedReason;
1604
+ lines.push(` \u2717 ${showName(group.root.name)}`);
1605
+ lines.push(` ${sanitizeDisplayText(reasonFirstLine, DISPLAY_TEXT_MAX_CHARS)}`);
1606
+ lines.push(` recover with: ralphctl task unblock ${String(group.root.id)}`);
1607
+ if (group.waiting.length > 0) {
1608
+ const names = group.waiting.map((t) => showName(t.name)).join(", ");
1609
+ lines.push(` \u21B3 ${String(group.waiting.length)} upstream-blocked task(s) waiting on this: ${names}`);
1610
+ }
1611
+ }
1612
+ }
1613
+ return `${lines.join("\n")}
1614
+ `;
1615
+ };
1616
+ var formatSprintLine = (s) => {
1617
+ const tickets = s.tickets.length;
1618
+ return `${String(s.id)} ${String(s.slug).padEnd(24)} [${s.status.padEnd(8)}] ${s.name} (${plural(tickets, "ticket")})`;
1619
+ };
1620
+
1621
+ // src/application/ui/cli/commands/ticket.ts
1622
+ var listTicketsAction = async (opts) => {
1623
+ const { deps, storage } = await bootstrapApp();
1624
+ const sprintId = await resolveSprintForCli(opts.sprint, storage.stateRoot);
1625
+ if (sprintId === void 0) return;
1626
+ const sprint = await deps.sprintRepo.findById(sprintId);
1627
+ if (!okOrFail(sprint)) return;
1628
+ if (sprint.value.tickets.length === 0) {
1629
+ process.stdout.write("(no tickets on this sprint yet \u2014 add one with `ralphctl ticket add`)\n");
1630
+ return;
1631
+ }
1632
+ for (const t of sprint.value.tickets) {
1633
+ process.stdout.write(`${formatTicketLine(t)}
1634
+ `);
1635
+ }
1636
+ };
1637
+ var showTicketAction = async (rawTicketId, opts) => {
1638
+ const { deps, storage } = await bootstrapApp();
1639
+ const ids = await resolveSprintAndIdForCli(opts.sprint, storage.stateRoot, rawTicketId, TicketId.parse, "ticket");
1640
+ if (ids === void 0) return;
1641
+ const { sprintId, id: ticketId } = ids;
1642
+ const sprint = await deps.sprintRepo.findById(sprintId);
1643
+ if (!okOrFail(sprint)) return;
1644
+ const found = sprint.value.tickets.find((t) => t.id === ticketId);
1645
+ if (!found) {
1646
+ fail(`ticket ${rawTicketId} not found on sprint ${String(sprintId)}`);
1647
+ return;
1648
+ }
1649
+ process.stdout.write(`${JSON.stringify(found, null, 2)}
1650
+ `);
1651
+ };
1652
+ var addTicketAction = async (opts) => {
1653
+ const { deps, storage } = await bootstrapApp();
1654
+ const sprintId = await resolveSprintForCli(opts.sprint, storage.stateRoot);
1655
+ if (sprintId === void 0) return;
1656
+ const flow = createTicketAddFlow({ sprintRepo: deps.sprintRepo });
1657
+ const result = await flow.execute({
1658
+ input: {
1659
+ sprintId,
1660
+ title: opts.title,
1661
+ ...opts.description !== void 0 ? { description: opts.description } : {},
1662
+ ...opts.link !== void 0 ? { link: opts.link } : {}
1663
+ }
1664
+ });
1665
+ if (!result.ok) {
1666
+ fail(result.error.error.message);
1667
+ return;
1668
+ }
1669
+ const ticket = result.value.ctx.output;
1670
+ process.stdout.write(`added ticket ${String(ticket.id)} to sprint ${String(sprintId)} \u2014 ${ticket.title}
1671
+ `);
1672
+ };
1673
+ var removeTicketAction = async (rawTicketId, opts) => {
1674
+ const { deps, storage } = await bootstrapApp();
1675
+ const ids = await resolveSprintAndIdForCli(opts.sprint, storage.stateRoot, rawTicketId, TicketId.parse, "ticket");
1676
+ if (ids === void 0) return;
1677
+ const { sprintId, id: ticketId } = ids;
1678
+ const confirmed = await confirmDestructive({
1679
+ yes: opts.yes === true,
1680
+ action: `remove ticket ${rawTicketId}`,
1681
+ confirmPrompt: `remove ticket ${rawTicketId} from sprint ${String(sprintId)}? [y/N] `
1682
+ });
1683
+ if (!confirmed) return;
1684
+ const flow = createTicketRemoveFlow({ sprintRepo: deps.sprintRepo });
1685
+ const result = await flow.execute({
1686
+ input: { sprintId, ticketId }
1687
+ });
1688
+ if (!result.ok) {
1689
+ fail(result.error.error.message);
1690
+ return;
1691
+ }
1692
+ const out = result.value.ctx.output;
1693
+ if (!out.removed) {
1694
+ fail(`ticket ${rawTicketId} not found on sprint ${String(sprintId)}`);
1695
+ return;
1696
+ }
1697
+ process.stdout.write(
1698
+ `removed ticket ${rawTicketId} from sprint ${String(sprintId)} (${plural(out.remainingTickets, "ticket")} remain)
1699
+ `
1700
+ );
1701
+ };
1702
+ var publishTicketAction = async (rawTicketId, opts) => {
1703
+ const { deps, storage } = await bootstrapApp();
1704
+ const ids = await resolveSprintAndIdForCli(opts.sprint, storage.stateRoot, rawTicketId, TicketId.parse, "ticket");
1705
+ if (ids === void 0) return;
1706
+ const { sprintId, id: ticketId } = ids;
1707
+ if (deps.issuePusher === void 0) {
1708
+ fail("issue tracker is unavailable");
1709
+ return;
1710
+ }
1711
+ const flow = createTicketPublishFlow({
1712
+ sprintRepo: deps.sprintRepo,
1713
+ projectRepo: deps.projectRepo,
1714
+ issuePusher: deps.issuePusher
1715
+ });
1716
+ const result = await flow.execute({
1717
+ input: { sprintId, ticketId }
1718
+ });
1719
+ if (!result.ok) {
1720
+ fail(result.error.error.message);
1721
+ return;
1722
+ }
1723
+ const out = result.value.ctx.output;
1724
+ process.stdout.write(`published ticket ${rawTicketId} on sprint ${String(sprintId)} \u2014 ${out.outcome}
1725
+ `);
1726
+ };
1727
+ var registerTicketCommand = (program) => {
1728
+ const ticketCmd = program.command("ticket").description("inspect and manage tickets within a sprint");
1729
+ ticketCmd.command("list").description("list every ticket on the sprint").option(SPRINT_OPTION_FLAGS, SPRINT_OPTION_DESC).action(listTicketsAction);
1730
+ ticketCmd.command("show <ticketId>").description("print a single ticket as JSON").option(SPRINT_OPTION_FLAGS, SPRINT_OPTION_DESC).action(showTicketAction);
1731
+ ticketCmd.command("add").description("append a pending ticket to a draft sprint").option(SPRINT_OPTION_FLAGS, SPRINT_OPTION_DESC).requiredOption("-t, --title <title>", "ticket title").option("-d, --description <text>", "optional description").option("-l, --link <url>", "optional issue link (http/https)").action(addTicketAction);
1732
+ ticketCmd.command("publish <ticketId>").description("create a tracker issue or post an approved-requirements comment").option(SPRINT_OPTION_FLAGS, SPRINT_OPTION_DESC).action(publishTicketAction);
1733
+ ticketCmd.command("remove <ticketId>").description("drop a ticket from a draft sprint").option(SPRINT_OPTION_FLAGS, SPRINT_OPTION_DESC).option("-y, --yes", "skip the interactive y/N confirmation").action(removeTicketAction);
1734
+ };
1735
+ var formatTicketLine = (t) => {
1736
+ const linkSuffix = t.link !== void 0 ? ` \u2014 ${String(t.link)}` : "";
1737
+ return `${String(t.id)} [${t.status.padEnd(8)}] ${t.title}${linkSuffix}`;
1738
+ };
1739
+
1740
+ // src/application/ui/cli/commands/task.ts
1741
+ import { promises as fs3 } from "fs";
1742
+ import { join as join5 } from "path";
1743
+ import { Option } from "commander";
1744
+ var listTasksAction = async (opts) => {
1745
+ const { deps, storage } = await bootstrapApp();
1746
+ const sprintId = await resolveSprintForCli(opts.sprint, storage.stateRoot);
1747
+ if (sprintId === void 0) return;
1748
+ const result = await deps.taskRepo.findBySprintId(sprintId);
1749
+ if (!okOrFail(result)) return;
1750
+ if (result.value.length === 0) {
1751
+ process.stdout.write("(no tasks yet \u2014 run plan to generate them)\n");
1752
+ return;
1753
+ }
1754
+ for (const t of result.value) {
1755
+ process.stdout.write(`${formatTaskLine(t)}
1756
+ `);
1757
+ }
1758
+ };
1759
+ var showTaskAction = async (rawTaskId, opts) => {
1760
+ const { deps, storage } = await bootstrapApp();
1761
+ const ids = await resolveSprintAndIdForCli(opts.sprint, storage.stateRoot, rawTaskId, TaskId.parse, "task");
1762
+ if (ids === void 0) return;
1763
+ const { sprintId, id: taskId } = ids;
1764
+ const result = await deps.taskRepo.findById(sprintId, taskId);
1765
+ if (!okOrFail(result)) return;
1766
+ process.stdout.write(`${JSON.stringify(result.value, null, 2)}
1767
+ `);
1768
+ };
1769
+ var evaluationTaskAction = async (rawTaskId, opts) => {
1770
+ const { deps, storage } = await bootstrapApp();
1771
+ const ids = await resolveSprintAndIdForCli(opts.sprint, storage.stateRoot, rawTaskId, TaskId.parse, "task");
1772
+ if (ids === void 0) return;
1773
+ const { sprintId, id: taskId } = ids;
1774
+ const loaded = await deps.taskRepo.findById(sprintId, taskId);
1775
+ if (!okOrFail(loaded)) return;
1776
+ const latest = latestRecordedEvaluation(loaded.value);
1777
+ if (latest === void 0) {
1778
+ process.stdout.write(`no evaluation recorded for task ${String(taskId)}
1779
+ `);
1780
+ return;
1781
+ }
1782
+ const relativePath = evaluationArtifactSprintPath(String(taskId), latest.file);
1783
+ if (relativePath === void 0) {
1784
+ process.stdout.write(`no evaluation artifact recorded for attempt ${String(latest.attemptN)} (legacy record)
1785
+ `);
1786
+ return;
1787
+ }
1788
+ const sprintDirPath = await resolveSprintDir(storage.dataRoot, sprintId);
1789
+ if (sprintDirPath === void 0) {
1790
+ process.stdout.write(`evaluation artifact not found on disk: ${relativePath}
1791
+ `);
1792
+ return;
1793
+ }
1794
+ try {
1795
+ const body = await fs3.readFile(join5(sprintDirPath, relativePath), "utf8");
1796
+ process.stderr.write(`# attempt ${String(latest.attemptN)} \xB7 eval ${latest.status} \xB7 ${relativePath}
1797
+ `);
1798
+ process.stdout.write(body.endsWith("\n") ? body : `${body}
1799
+ `);
1800
+ } catch (cause) {
1801
+ const code = cause?.code;
1802
+ if (code === "ENOENT") {
1803
+ process.stdout.write(`evaluation artifact not found on disk: ${relativePath}
1804
+ `);
1805
+ return;
1806
+ }
1807
+ fail(`could not read evaluation artifact: ${messageOf(cause)}`);
1808
+ }
1809
+ };
1810
+ var CAUSE_PHRASE = {
1811
+ "budget-exhausted": "an attempt-budget block",
1812
+ "generator-self-block": "a self-block",
1813
+ "operator-cancelled": "an operator cancel",
1814
+ "post-verify-regression": "a post-verify regression"
1815
+ };
1816
+ var defaultReason = (task) => task.status === "in_progress" ? "an interrupted run" : task.status === "blocked" ? CAUSE_PHRASE[task.blockCause ?? "unknown"] ?? "an unclassified block" : "the last block";
1817
+ var planPriorWork = async (deps, sprintId, task, flag) => {
1818
+ if (task.status === "todo" && flag === void 0) return { probe: { kind: "none" }, defaulted: true };
1819
+ const sprint = await deps.sprintRepo.findById(sprintId);
1820
+ const project = sprint.ok ? await deps.projectRepo.findById(sprint.value.projectId) : void 0;
1821
+ const probe = await probeTaskQuarantine(deps, project?.ok === true ? project.value : void 0, sprintId, task);
1822
+ const choice = flag ?? recommendedPriorWork(task);
1823
+ if (probe.kind === "present") {
1824
+ return {
1825
+ probe,
1826
+ defaulted: flag === void 0,
1827
+ decision: { choice, stashMessage: probe.stashMessage, stat: probe.stat, entries: probe.entries }
1828
+ };
1829
+ }
1830
+ if (probe.kind === "unknown" && flag !== void 0) {
1831
+ return { probe, defaulted: false, decision: { choice, stashMessage: quarantineStashMessage(sprintId, task.id) } };
1832
+ }
1833
+ return { probe, defaulted: flag === void 0 };
1834
+ };
1835
+ var STARTS_FRESH = "starts fresh";
1836
+ var CONTINUES = "continues from the rejected diff";
1837
+ var describeChoice = (fact) => fact?.nextAttempt === "fresh" ? STARTS_FRESH : fact?.nextAttempt === "continue" ? CONTINUES : "restores the rejected diff (no choice recorded)";
1838
+ var unblockTaskAction = async (rawTaskId, opts) => {
1839
+ const { deps, storage } = await bootstrapApp();
1840
+ const ids = await resolveSprintAndIdForCli(opts.sprint, storage.stateRoot, rawTaskId, TaskId.parse, "task");
1841
+ if (ids === void 0) return;
1842
+ const { sprintId, id: taskId } = ids;
1843
+ const loaded = await deps.taskRepo.findById(sprintId, taskId);
1844
+ if (!okOrFail(loaded)) return;
1845
+ const sprint = await deps.sprintRepo.findById(sprintId);
1846
+ if (!okOrFail(sprint)) return;
1847
+ const plan = await planPriorWork(deps, sprintId, loaded.value, opts.priorWork);
1848
+ const result = await unblockTaskLocked(deps, {
1849
+ sprint: sprint.value,
1850
+ taskId,
1851
+ ...plan.decision !== void 0 ? { priorWork: plan.decision } : {}
1852
+ });
1853
+ if (!okOrFail(result)) return;
1854
+ const taskRef = String(result.value.task.id);
1855
+ process.stdout.write(
1856
+ `unblocked task '${sanitizeDisplayText(result.value.task.name, DISPLAY_TEXT_MAX_CHARS)}' (${taskRef})
1857
+ `
1858
+ );
1859
+ const sprintRef = String(sprintId);
1860
+ writePriorWorkReport(plan, loaded.value, opts.priorWork, sprintRef, taskRef);
1861
+ const retry = `ralphctl task unblock --sprint ${sprintRef} ${taskRef}`;
1862
+ const reopened = result.value.sprintReopened;
1863
+ if (reopened !== void 0) {
1864
+ const moved = reopened.from !== reopened.sprint.status;
1865
+ process.stdout.write(
1866
+ moved ? `reopened sprint '${reopened.sprint.slug}' (${sprintRef}) ${reopened.from} \u2192 ${reopened.sprint.status}
1867
+ ` : `sprint '${reopened.sprint.slug}' (${sprintRef}) is still ${reopened.sprint.status}
1868
+ `
1869
+ );
1870
+ if (reopened.sprint.status !== "active") {
1871
+ process.stderr.write(`note: the review \u2192 active step did not persist \u2014 run '${retry}' again to finish it
1872
+ `);
1873
+ }
1874
+ }
1875
+ const conflict = result.value.sprintReopenConflict;
1876
+ if (conflict !== void 0) {
1877
+ process.stderr.write(`note: ${conflict.message}
1878
+ `);
1879
+ if (conflict.hint !== void 0) process.stderr.write(` ${conflict.hint}
1880
+ `);
1881
+ process.stderr.write(
1882
+ ` then 'ralphctl sprint reopen ${sprintRef}' and '${retry}' to make this task runnable
1883
+ `
1884
+ );
1885
+ }
1886
+ };
1887
+ var writePriorWorkReport = (plan, before, flag, sprintRef, taskRef) => {
1888
+ const { probe } = plan;
1889
+ if (probe.kind === "unknown") {
1890
+ process.stderr.write(
1891
+ flag !== void 0 ? `note: couldn't read git stash (${probe.error}) \u2014 recorded --prior-work ${flag} anyway
1892
+ ` : `note: couldn't read git stash (${probe.error}); a rejected diff there would be restored on the next attempt
1893
+ `
1894
+ );
1895
+ return;
1896
+ }
1897
+ if (probe.kind === "none") {
1898
+ if (flag !== void 0) {
1899
+ process.stderr.write(
1900
+ "note: no rejected diff in git stash for this task \u2014 --prior-work has nothing to apply to\n"
1901
+ );
1902
+ }
1903
+ return;
1904
+ }
1905
+ const choice = plan.decision?.choice ?? "fresh";
1906
+ process.stdout.write(`rejected diff in git stash: ${probe.stashMessage} \u2014 ${formatDiffStat(probe.stat)}
1907
+ `);
1908
+ if (before.status === "todo") {
1909
+ const now = choice === "fresh" ? STARTS_FRESH : CONTINUES;
1910
+ process.stdout.write(`next attempt: ${now} (was: ${describeChoice(before.quarantinedDiff)})
1911
+ `);
1912
+ return;
1913
+ }
1914
+ const how = plan.defaulted ? `default after ${defaultReason(before)}` : `--prior-work ${choice}`;
1915
+ const other = choice === "fresh" ? "continue" : "fresh";
1916
+ const effect = choice === "fresh" ? `${STARTS_FRESH} (${how}); the diff stays in the stash` : `${CONTINUES} (${how}); it is restored before the first AI turn`;
1917
+ process.stdout.write(`next attempt: ${effect}
1918
+ `);
1919
+ if (plan.defaulted) {
1920
+ const verb = other === "continue" ? "to continue from it instead" : "to start fresh instead";
1921
+ process.stdout.write(
1922
+ ` ${verb}: ralphctl task unblock --sprint ${sprintRef} ${taskRef} --prior-work ${other}
1923
+ `
1924
+ );
1925
+ }
1926
+ };
1927
+ var registerTaskCommand = (program) => {
1928
+ const task = program.command("task").description("inspect tasks for a sprint (planning generates them)");
1929
+ task.command("list").description("list every task on the sprint, in order").option(SPRINT_OPTION_FLAGS, SPRINT_OPTION_DESC).action(listTasksAction);
1930
+ task.command("show <taskId>").description("print a single task as JSON").option(SPRINT_OPTION_FLAGS, SPRINT_OPTION_DESC).action(showTaskAction);
1931
+ task.command("evaluation <taskId>").description("print the latest evaluator verdict (evaluation.md) for the task's most recent evaluated attempt").option(SPRINT_OPTION_FLAGS, SPRINT_OPTION_DESC).action(evaluationTaskAction);
1932
+ task.command("unblock <taskId>").description("flip a blocked task back to todo so the implement loop picks it up again").option(SPRINT_OPTION_FLAGS, SPRINT_OPTION_DESC).addOption(
1933
+ new Option(
1934
+ "--prior-work <choice>",
1935
+ "what the next attempt does with its quarantined rejected diff (default depends on why it stopped)"
1936
+ ).choices(["continue", "fresh"])
1937
+ ).action(unblockTaskAction);
1938
+ };
1939
+ var formatTaskLine = (t) => {
1940
+ const orderStr = String(t.order).padStart(3, " ");
1941
+ const show = (text) => sanitizeDisplayText(text, DISPLAY_TEXT_MAX_CHARS);
1942
+ const head = `${orderStr}. ${String(t.id)} [${t.status.padEnd(8)}] ${show(t.name)}`;
1943
+ const fact = t.quarantinedDiff;
1944
+ if (t.status === "todo" && fact !== void 0) {
1945
+ const next = fact.nextAttempt === "fresh" ? "starts fresh \u2014 rejected diff stays in git stash" : fact.nextAttempt === "continue" ? "continues from the rejected diff" : "restores the rejected diff in git stash (no choice recorded)";
1946
+ return `${head}
1947
+ next attempt: ${next}`;
1948
+ }
1949
+ if (t.status !== "blocked") return head;
1950
+ const reasonFirstLine = t.blockedReason.split("\n")[0] ?? t.blockedReason;
1951
+ const lines = [head, ` ${show(reasonFirstLine)}`];
1952
+ if (t.blockerClass !== void 0) lines.push(` blocker: ${t.blockerClass}`);
1953
+ if (t.question !== void 0) lines.push(` question: ${show(t.question)}`);
1954
+ if (t.whatUnblocksMe !== void 0) lines.push(` unblocks with: ${show(t.whatUnblocksMe)}`);
1955
+ if (fact !== void 0) {
1956
+ const stat = fact.stat !== void 0 ? ` (${formatDiffStat(fact.stat)})` : "";
1957
+ lines.push(` stash: ${fact.stashMessage}${stat}`);
1958
+ }
1959
+ const flagHint = fact !== void 0 ? " [--prior-work continue|fresh]" : "";
1960
+ lines.push(` recover with: ralphctl task unblock ${String(t.id)}${flagHint}`);
1961
+ return lines.join("\n");
1962
+ };
1963
+
1964
+ // src/application/ui/cli/commands/runs.ts
1965
+ import { promises as fs4 } from "fs";
1966
+ import { createInterface as createInterface2 } from "readline";
1967
+
1968
+ // src/application/ui/cli/commands/runs-stats-report.ts
1969
+ var STATUS_ORDER = ["done", "blocked", "in_progress", "todo"];
1970
+ var STATUS_LABELS = {
1971
+ done: "done",
1972
+ blocked: "blocked",
1973
+ in_progress: "in progress",
1974
+ todo: "todo"
1975
+ };
1976
+ var RUNG_ORDER = ["model", "effort", "evaluator-effort", "nudge", "best-of-n"];
1977
+ var PLATEAU_SOURCE_ORDER = ["threshold", "diversity", "entropy", "unspecified"];
1978
+ var ATTRIBUTION_LABEL = {
1979
+ regressed: "regressed",
1980
+ "baseline-broken": "baseline-broken",
1981
+ "fixed-baseline": "fixed-baseline",
1982
+ clean: "clean",
1983
+ unspecified: "unattributed"
1984
+ };
1985
+ var ATTRIBUTION_ORDER = Object.keys(ATTRIBUTION_LABEL);
1986
+ var WARNING_ORDER = [
1987
+ "verify-failed",
1988
+ "crashed",
1989
+ "malformed",
1990
+ "budget-exhausted",
1991
+ "plateau",
1992
+ "unknown"
1993
+ ];
1994
+ var ABORT_ORDER = [
1995
+ "rate-limit-exhausted",
1996
+ "watchdog-killed",
1997
+ "process-crash",
1998
+ "harness-interrupted",
1999
+ "sigterm",
2000
+ "user-cancel",
2001
+ "self-blocked",
2002
+ "unknown"
2003
+ ];
2004
+ var TOP_DIMENSIONS = 8;
2005
+ var LABEL_WIDTH = 15;
2006
+ var TAXONOMY_LABEL_WIDTH = 22;
2007
+ var pct = (rate) => `${(rate * 100).toFixed(1)}%`;
2008
+ var keyAt = (text, width) => ` ${text.padEnd(width)}`;
2009
+ var key = (text) => keyAt(text, LABEL_WIDTH);
2010
+ var taxonomyKey = (text) => keyAt(text, TAXONOMY_LABEL_WIDTH);
2011
+ var taxonomyRows = (counts, order) => {
2012
+ const tail = Object.keys(counts).filter((candidate) => !order.includes(candidate));
2013
+ return [...order, ...tail].filter((taxon) => (counts[taxon] ?? 0) > 0).map((taxon) => `${taxonomyKey(taxon)}${String(counts[taxon] ?? 0)}`);
2014
+ };
2015
+ var num = (n, width) => String(n).padStart(width);
2016
+ var denominator = (n, unit) => `(of ${plural(n, unit)})`;
2017
+ var headline = (stats) => `Harness outcomes \u2014 ${plural(stats.sprintCount, "sprint")} \xB7 ${plural(stats.totals.taskCount, "task")} \xB7 ${plural(stats.totals.attemptCount, "attempt")}`;
2018
+ var renderOutcomes = (rollup) => {
2019
+ const { byStatus, doneClean, doneWithWarning } = rollup.outcomes;
2020
+ const lines = [`Outcome mix ${denominator(rollup.taskCount, "task")}`];
2021
+ for (const status of STATUS_ORDER) {
2022
+ const suffix = status === "done" ? ` (clean ${String(doneClean)} \xB7 with warning ${String(doneWithWarning)})` : "";
2023
+ lines.push(`${key(STATUS_LABELS[status])}${String(byStatus[status])}${suffix}`);
2024
+ }
2025
+ return lines;
2026
+ };
2027
+ var regressionLine = (attribution) => {
2028
+ if (attribution.attributed === 0) return `${key("regressions")}\u2014 (no attempt carries an attribution verdict)`;
2029
+ const broke = `${plural(attribution.byVerdict.regressed, "attempt")} broke a green baseline`;
2030
+ return `${key("regressions")}${broke} (${pct(attribution.regressionRate)} of ${plural(attribution.attributed, "attributed attempt")})`;
2031
+ };
2032
+ var renderSummary = (rollup) => {
2033
+ const { doneOnFirstAttempt, doneTotal, rate } = rollup.firstPass;
2034
+ const criteria = rollup.criteria;
2035
+ return [
2036
+ "Summary",
2037
+ `${key("first pass")}${String(doneOnFirstAttempt)}/${String(doneTotal)} done on attempt 1 (${pct(rate)}) \u2014 of ${plural(doneTotal, "done task")}`,
2038
+ regressionLine(rollup.attribution),
2039
+ `${key("criteria")}${String(criteria.passed)}/${String(criteria.declared)} passed (${pct(criteria.passRate)}) \xB7 ${String(criteria.failed)} failed \xB7 ${String(criteria.unknown)} unknown \xB7 ${plural(criteria.tasksWithVerdicts, "task")} graded`
2040
+ ];
2041
+ };
2042
+ var renderAttempts = (rollup) => {
2043
+ const buckets = rollup.attemptsToDone;
2044
+ const lines = [`Attempts to done ${denominator(rollup.firstPass.doneTotal, "done task")}`];
2045
+ if (buckets.length === 0) {
2046
+ lines.push(" (no done task carries an attempt count yet)");
2047
+ return lines;
2048
+ }
2049
+ for (const bucket of buckets) {
2050
+ lines.push(`${key(plural(bucket.attempts, "attempt"))}${plural(bucket.tasks, "task")}`);
2051
+ }
2052
+ return lines;
2053
+ };
2054
+ var renderPlateau = (plateau, taskCount, attemptCount) => {
2055
+ const lines = [
2056
+ "Plateau",
2057
+ `${key("tasks")}${plural(plateau.tasksWithPlateau, "task")} (${pct(plateau.taskRate)}) \u2014 of ${plural(taskCount, "task")}`,
2058
+ `${key("attempts")}${String(plateau.attemptsWithPlateau)} \u2014 of ${plural(attemptCount, "attempt")}`
2059
+ ];
2060
+ for (const source of PLATEAU_SOURCE_ORDER) {
2061
+ const hits = plateau.bySource[source];
2062
+ if (hits > 0) lines.push(`${key(source)}${String(hits)}`);
2063
+ }
2064
+ return lines;
2065
+ };
2066
+ var renderAttribution = (attribution, attemptCount) => {
2067
+ const lines = [`Attribution ${denominator(attemptCount, "attempt")}`];
2068
+ if (attribution.attributed === 0) {
2069
+ lines.push(" (no attempt carries an attribution verdict)");
2070
+ return lines;
2071
+ }
2072
+ for (const verdict of ATTRIBUTION_ORDER) {
2073
+ const hits = attribution.byVerdict[verdict];
2074
+ if (hits > 0 || verdict === "regressed") lines.push(`${taxonomyKey(ATTRIBUTION_LABEL[verdict])}${String(hits)}`);
2075
+ }
2076
+ return lines;
2077
+ };
2078
+ var renderWarnings = (warnings, attemptCount) => {
2079
+ const lines = [`Warnings ${denominator(attemptCount, "attempt")}`];
2080
+ if (warnings.attemptsWithWarning === 0) {
2081
+ lines.push(" (no attempt carried a warning)");
2082
+ return lines;
2083
+ }
2084
+ return [...lines, ...taxonomyRows(warnings.byKind, WARNING_ORDER)];
2085
+ };
2086
+ var renderAborts = (aborts, attemptCount) => {
2087
+ const lines = [`Aborts ${denominator(attemptCount, "attempt")}`];
2088
+ if (aborts.attemptsAborted === 0) {
2089
+ lines.push(" (no attempt was aborted)");
2090
+ return lines;
2091
+ }
2092
+ return [...lines, ...taxonomyRows(aborts.byCause, ABORT_ORDER)];
2093
+ };
2094
+ var RUNG_HEADER = ` ${"rung".padEnd(18)}granted resolved fell-through unsettled`;
2095
+ var renderEscalation = (rollup) => {
2096
+ const rows = RUNG_ORDER.filter((rung) => rollup.escalation[rung].granted > 0);
2097
+ const lines = [`Escalation rungs ${denominator(rollup.taskCount, "task")}`];
2098
+ if (rows.length === 0) {
2099
+ lines.push(" (no escalation rung fired)");
2100
+ return lines;
2101
+ }
2102
+ lines.push(RUNG_HEADER);
2103
+ for (const rung of rows) {
2104
+ const efficacy = rollup.escalation[rung];
2105
+ lines.push(
2106
+ ` ${rung.padEnd(18)}${num(efficacy.granted, 7)}${num(efficacy.resolved, 10)}${num(efficacy.fellThrough, 14)}${num(efficacy.unsettled, 11)}`
2107
+ );
2108
+ }
2109
+ return lines;
2110
+ };
2111
+ var renderDimensions = (dimensions) => {
2112
+ const lines = ["Failed dimensions"];
2113
+ if (dimensions.length === 0) {
2114
+ lines.push(" (no plateau recorded a failed dimension)");
2115
+ return lines;
2116
+ }
2117
+ for (const entry of dimensions.slice(0, TOP_DIMENSIONS)) {
2118
+ lines.push(`${key(entry.dimension)}${String(entry.count)}`);
2119
+ }
2120
+ const hidden = dimensions.length - TOP_DIMENSIONS;
2121
+ if (hidden > 0) lines.push(` \u2026 ${plural(hidden, "more dimension")} (use --json for the full histogram)`);
2122
+ return lines;
2123
+ };
2124
+ var sprintLine = (entry) => {
2125
+ const { rollup } = entry;
2126
+ const regressed = rollup.attribution.tasksWithRegression;
2127
+ const parts = [
2128
+ plural(rollup.taskCount, "task"),
2129
+ `${String(rollup.outcomes.byStatus.done)} done`,
2130
+ `${String(rollup.outcomes.byStatus.blocked)} blocked`,
2131
+ `first-pass ${pct(rollup.firstPass.rate)}`,
2132
+ `plateau ${pct(rollup.plateau.taskRate)}`,
2133
+ // Appended only when it happened — the line is already five fields wide.
2134
+ ...regressed > 0 ? [`${String(regressed)} regressed`] : []
2135
+ ];
2136
+ return ` ${entry.sprintName} (${entry.sprintId})
2137
+ ${parts.join(" \xB7 ")}`;
2138
+ };
2139
+ var renderBySprint = (bySprint) => [
2140
+ "By sprint",
2141
+ ...bySprint.map(sprintLine)
2142
+ ];
2143
+ var renderOutcomeStats = (stats) => {
2144
+ const sections = [
2145
+ [headline(stats)],
2146
+ renderSummary(stats.totals),
2147
+ renderOutcomes(stats.totals),
2148
+ renderAttempts(stats.totals),
2149
+ renderPlateau(stats.totals.plateau, stats.totals.taskCount, stats.totals.attemptCount),
2150
+ renderAttribution(stats.totals.attribution, stats.totals.attemptCount),
2151
+ renderWarnings(stats.totals.warnings, stats.totals.attemptCount),
2152
+ renderAborts(stats.totals.aborts, stats.totals.attemptCount),
2153
+ renderEscalation(stats.totals),
2154
+ renderDimensions(stats.totals.failedDimensions),
2155
+ ...stats.bySprint.length > 1 ? [renderBySprint(stats.bySprint)] : []
2156
+ ];
2157
+ return `${sections.map((section) => section.join("\n")).join("\n\n")}
2158
+ `;
2159
+ };
2160
+
2161
+ // src/application/ui/cli/commands/runs-stats.ts
2162
+ var SINCE_DESC = "ISO date \u2014 keep sprints whose latest lifecycle stamp (done, else review, else activated, else planned) is on or after it; never-planned drafts have no stamp and drop out";
2163
+ var registerRunsStatsCommand = (runs) => {
2164
+ runs.command("stats").description("summarise harness outcomes across sprints (outcome mix, first-pass rate, escalation efficacy)").option("--json", "print the raw rollup as JSON (stable ordering \u2014 diff two runs to compare settings)").option("--since <date>", SINCE_DESC).option("-s, --sprint <id>", "restrict the rollup to a single sprint").option("-p, --project <id>", "restrict the rollup to one project's sprints").action(runStatsCommand);
2165
+ };
2166
+ var runStatsCommand = async (opts) => {
2167
+ if (opts.sprint !== void 0 && opts.project !== void 0) {
2168
+ fail("--sprint and --project are mutually exclusive \u2014 pass one, or neither to cover every sprint");
2169
+ return;
2170
+ }
2171
+ const since = parseSince(opts.since);
2172
+ if (!since.ok) return;
2173
+ const { deps } = await bootstrapApp();
2174
+ const scoped = await resolveScopedSprints(deps, opts);
2175
+ if (!scoped.ok) return;
2176
+ const inWindow = since.sinceMs === void 0 ? scoped.sprints : scoped.sprints.filter(activeSince(since.sinceMs));
2177
+ if (inWindow.length === 0 && opts.json !== true) {
2178
+ process.stdout.write(emptyNotice(opts));
2179
+ return;
2180
+ }
2181
+ const slices = await loadSlices(deps, inWindow);
2182
+ if (slices === void 0) return;
2183
+ const stats = foldOutcomeStats(slices);
2184
+ process.stdout.write(opts.json === true ? `${JSON.stringify(stats, null, 2)}
2185
+ ` : renderOutcomeStats(stats));
2186
+ };
2187
+ var parseSince = (raw) => {
2188
+ if (raw === void 0) return { ok: true, sinceMs: void 0 };
2189
+ const parsed = Date.parse(raw);
2190
+ if (Number.isNaN(parsed)) {
2191
+ fail(`--since must be an ISO date (e.g. 2026-01-31 or 2026-01-31T09:00:00Z) \u2014 got '${raw}'`);
2192
+ return { ok: false };
2193
+ }
2194
+ return { ok: true, sinceMs: parsed };
2195
+ };
2196
+ var lastActivityMs = (sprint) => {
2197
+ const stamp = sprint.doneAt ?? sprint.reviewAt ?? sprint.activatedAt ?? sprint.plannedAt;
2198
+ if (stamp === null) return void 0;
2199
+ const parsed = Date.parse(String(stamp));
2200
+ return Number.isNaN(parsed) ? void 0 : parsed;
2201
+ };
2202
+ var activeSince = (sinceMs) => (sprint) => {
2203
+ const activity = lastActivityMs(sprint);
2204
+ return activity !== void 0 && activity >= sinceMs;
2205
+ };
2206
+ var resolveScopedSprints = async (deps, opts) => {
2207
+ if (opts.sprint !== void 0) {
2208
+ const id = SprintId.parse(opts.sprint);
2209
+ if (!okOrFail(id, "invalid sprint id: ")) return { ok: false };
2210
+ const found = await deps.sprintRepo.findById(id.value);
2211
+ if (!okOrFail(found)) return { ok: false };
2212
+ return { ok: true, sprints: [found.value] };
2213
+ }
2214
+ const all = await deps.sprintRepo.list();
2215
+ if (!okOrFail(all)) return { ok: false };
2216
+ if (opts.project === void 0) return { ok: true, sprints: all.value };
2217
+ const projectId = ProjectId.parse(opts.project);
2218
+ if (!okOrFail(projectId, "invalid project id: ")) return { ok: false };
2219
+ const project = await deps.projectRepo.findById(projectId.value);
2220
+ if (!okOrFail(project)) return { ok: false };
2221
+ return { ok: true, sprints: all.value.filter((sprint) => sprint.projectId === projectId.value) };
2222
+ };
2223
+ var loadSlices = async (deps, sprints) => {
2224
+ const reads = await Promise.all(
2225
+ sprints.map(async (sprint) => ({ sprint, tasks: await deps.taskRepo.findBySprintId(sprint.id) }))
2226
+ );
2227
+ const slices = [];
2228
+ for (const { sprint, tasks } of reads) {
2229
+ if (!tasks.ok) {
2230
+ fail(`sprint ${String(sprint.id)}: ${tasks.error.message}`);
2231
+ return void 0;
2232
+ }
2233
+ slices.push({ sprint, tasks: tasks.value });
2234
+ }
2235
+ return slices;
2236
+ };
2237
+ var emptyNotice = (opts) => {
2238
+ const filtered = opts.since !== void 0 || opts.sprint !== void 0 || opts.project !== void 0;
2239
+ return filtered ? "(no sprints match this scope \u2014 widen or drop --since / --sprint / --project)\n" : "(no sprints yet \u2014 nothing to summarise)\n";
2240
+ };
2241
+
2242
+ // src/application/ui/cli/commands/runs.ts
2243
+ var registerRunsCommand = (program) => {
2244
+ const runs = program.command("runs").description("inspect and prune per-run forensic artifacts");
2245
+ runs.command("list").description("list per-run forensic artifacts grouped by flow").option("-f, --flow <name>", "restrict the listing to a single flow").action(runListCommand);
2246
+ runs.command("prune").description("delete per-run forensic artifacts (filters required unless invoked interactively)").option("--older-than <duration>", "delete runs older than this (h / d / w suffix, e.g. 7d)").option("--keep-last <n>", "retain the N most-recent runs per flow").option("-f, --flow <name>", "restrict pruning to a single flow").option("--dry-run", "list candidates without deleting (wins over --yes)").option("-y, --yes", "skip the interactive y/N confirmation").action(runPruneCommand);
2247
+ registerRunsStatsCommand(runs);
2248
+ };
2249
+ var runListCommand = async (opts) => {
2250
+ const { storage } = await bootstrapApp();
2251
+ const result = await listRuns(storage.runsRoot);
2252
+ if (!okOrFail(result)) return;
2253
+ const entries = opts.flow !== void 0 ? result.value.filter((r) => r.flow === opts.flow) : result.value;
2254
+ if (entries.length === 0) {
2255
+ if (opts.flow !== void 0) {
2256
+ process.stdout.write(
2257
+ `(no runs for flow '${opts.flow}' yet under ${String(storage.runsRoot)} \u2014 drop --flow to see all)
2258
+ `
2259
+ );
2260
+ } else {
2261
+ process.stdout.write(`(no runs yet under ${String(storage.runsRoot)} \u2014 run a flow to generate one)
2262
+ `);
2263
+ }
2264
+ return;
2265
+ }
2266
+ const groups = groupByFlow(entries);
2267
+ const now = /* @__PURE__ */ new Date();
2268
+ let grandTotalBytes = 0;
2269
+ let grandTotalRuns = 0;
2270
+ for (const [flow, runs] of groups) {
2271
+ const flowBytes = runs.reduce((acc, r) => acc + r.sizeBytes, 0);
2272
+ grandTotalBytes += flowBytes;
2273
+ grandTotalRuns += runs.length;
2274
+ process.stdout.write(`${flow} (${plural(runs.length, "run")}, ${formatBytes(flowBytes)})
2275
+ `);
2276
+ for (const run of runs) {
2277
+ process.stdout.write(` ${run.runId} ${formatRelativeAge(run.timestamp, now)} ${formatBytes(run.sizeBytes)}
2278
+ `);
2279
+ }
2280
+ }
2281
+ process.stdout.write(`total: ${plural(grandTotalRuns, "run")}, ${formatBytes(grandTotalBytes)}
2282
+ `);
2283
+ };
2284
+ var runPruneCommand = async (opts) => {
2285
+ const noFlags = opts.olderThan === void 0 && opts.keepLast === void 0 && opts.flow === void 0 && opts.dryRun !== true && opts.yes !== true;
2286
+ if (noFlags) {
2287
+ await runInteractivePrune();
2288
+ return;
2289
+ }
2290
+ const filters = parsePruneFilters(opts);
2291
+ if (!filters.ok) return;
2292
+ const scopedResult = await resolveScopedRuns(opts.flow);
2293
+ if (!scopedResult.ok) return;
2294
+ if (filters.olderThanMs === void 0 && filters.keepLast === void 0) {
2295
+ fail(
2296
+ "--older-than or --keep-last is required (or invoke `ralphctl runs prune` with no flags for the interactive picker)"
2297
+ );
2298
+ return;
2299
+ }
2300
+ const candidates = computeAndAnnouncePruneCandidates(scopedResult.grouped, {
2301
+ olderThanMs: filters.olderThanMs,
2302
+ keepLast: filters.keepLast
2303
+ });
2304
+ if (candidates === void 0) return;
2305
+ if (opts.dryRun === true) {
2306
+ return;
2307
+ }
2308
+ if (opts.yes !== true) {
2309
+ const confirmed = await confirmNonInteractivePrune(candidates.length);
2310
+ if (!confirmed) return;
2311
+ }
2312
+ await performPrune(candidates);
2313
+ };
2314
+ var parseKeepLast = (raw) => /^\d+$/u.test(raw.trim()) ? Number(raw.trim()) : void 0;
2315
+ var parsePruneFilters = (opts) => {
2316
+ let olderThanMs;
2317
+ if (opts.olderThan !== void 0) {
2318
+ const parsed = parseDuration(opts.olderThan);
2319
+ if (!okOrFail(parsed)) return { ok: false };
2320
+ olderThanMs = parsed.value;
2321
+ }
2322
+ let keepLast;
2323
+ if (opts.keepLast !== void 0) {
2324
+ keepLast = parseKeepLast(opts.keepLast);
2325
+ if (keepLast === void 0) {
2326
+ fail("--keep-last must be a non-negative integer");
2327
+ return { ok: false };
2328
+ }
2329
+ }
2330
+ return { ok: true, olderThanMs, keepLast };
2331
+ };
2332
+ var resolveScopedRuns = async (flow) => {
2333
+ const { storage } = await bootstrapApp();
2334
+ const result = await listRuns(storage.runsRoot);
2335
+ if (!okOrFail(result)) return { ok: false };
2336
+ const allEntries = result.value;
2337
+ if (flow !== void 0) {
2338
+ const flowExists = allEntries.some((r) => r.flow === flow);
2339
+ if (!flowExists) {
2340
+ fail(`no such flow '${flow}' under ${String(storage.runsRoot)}`);
2341
+ return { ok: false };
2342
+ }
2343
+ }
2344
+ const scoped = flow !== void 0 ? allEntries.filter((r) => r.flow === flow) : allEntries;
2345
+ return { ok: true, grouped: groupByFlow(scoped) };
2346
+ };
2347
+ var computeAndAnnouncePruneCandidates = (grouped, filters) => {
2348
+ const { candidates, unknownStampWarnings } = selectCandidates(grouped, filters);
2349
+ for (const warning of unknownStampWarnings) {
2350
+ process.stderr.write(`warning: skipping run with non-conforming dir name (no timestamp): ${warning}
2351
+ `);
2352
+ }
2353
+ if (candidates.length === 0) {
2354
+ process.stdout.write("nothing to prune\n");
2355
+ return void 0;
2356
+ }
2357
+ printCandidateSummary(candidates);
2358
+ return candidates;
2359
+ };
2360
+ var confirmNonInteractivePrune = async (count) => confirmDestructive({
2361
+ yes: false,
2362
+ action: "delete",
2363
+ confirmPrompt: `delete ${plural(count, "run")}? [y/N] `,
2364
+ nonTtyHint: ", or --dry-run to list candidates only"
2365
+ });
2366
+ var runInteractivePrune = async () => {
2367
+ if (process.stdin.isTTY !== true) {
2368
+ fail(
2369
+ "interactive prune requires a TTY \u2014 supply --older-than or --keep-last (plus --yes / --dry-run) on non-interactive stdin"
2370
+ );
2371
+ return;
2372
+ }
2373
+ const { storage } = await bootstrapApp();
2374
+ const listed = await listRuns(storage.runsRoot);
2375
+ if (!okOrFail(listed)) return;
2376
+ if (listed.value.length === 0) {
2377
+ process.stdout.write(`no runs to prune under ${String(storage.runsRoot)}
2378
+ `);
2379
+ return;
2380
+ }
2381
+ const groups = groupByFlow(listed.value);
2382
+ const flows = Array.from(groups.keys());
2383
+ printRunsOverview(groups);
2384
+ const rl = createInterface2({ input: process.stdin, output: process.stdout });
2385
+ try {
2386
+ const filterChoice = await promptPruneFilterChoice(rl);
2387
+ if (!filterChoice.ok) return;
2388
+ const flowChoice = await promptFlowFilterChoice(rl, flows);
2389
+ if (!flowChoice.ok) return;
2390
+ const scoped = flowChoice.flowFilter !== void 0 ? listed.value.filter((r) => r.flow === flowChoice.flowFilter) : listed.value;
2391
+ const grouped = groupByFlow(scoped);
2392
+ const candidates = computeAndAnnouncePruneCandidates(grouped, {
2393
+ olderThanMs: filterChoice.olderThanMs,
2394
+ keepLast: filterChoice.keepLast
2395
+ });
2396
+ if (candidates === void 0) return;
2397
+ const ok = await askLine(rl, `delete ${plural(candidates.length, "run")}? [y/N] `);
2398
+ if (ok === void 0) {
2399
+ reportPromptAborted();
2400
+ return;
2401
+ }
2402
+ if (!isYes(ok.trim())) {
2403
+ process.stdout.write("aborted\n");
2404
+ return;
2405
+ }
2406
+ await performPrune(candidates);
2407
+ } finally {
2408
+ rl.close();
2409
+ }
2410
+ };
2411
+ var printRunsOverview = (groups) => {
2412
+ process.stdout.write("current runs:\n");
2413
+ for (const [flow, runs] of groups) {
2414
+ const flowBytes = runs.reduce((acc, r) => acc + r.sizeBytes, 0);
2415
+ process.stdout.write(` ${flow} (${plural(runs.length, "run")}, ${formatBytes(flowBytes)})
2416
+ `);
2417
+ }
2418
+ process.stdout.write("\n");
2419
+ };
2420
+ var reportPromptAborted = () => {
2421
+ process.stdout.write("aborted\n");
2422
+ process.exitCode = 130;
2423
+ return { ok: false };
2424
+ };
2425
+ var promptPruneFilterChoice = async (rl) => {
2426
+ const criterion = (await askLine(rl, "prune by [1] age threshold or [2] keep-last N? "))?.trim();
2427
+ if (criterion === void 0) return reportPromptAborted();
2428
+ if (criterion === "1") {
2429
+ const duration = (await askLine(rl, "duration (e.g. 7d, 24h, 2w): "))?.trim();
2430
+ if (duration === void 0) return reportPromptAborted();
2431
+ const parsed = parseDuration(duration);
2432
+ if (!okOrFail(parsed)) return { ok: false };
2433
+ return { ok: true, olderThanMs: parsed.value, keepLast: void 0 };
2434
+ }
2435
+ if (criterion === "2") {
2436
+ const n = (await askLine(rl, "keep last N runs per flow: "))?.trim();
2437
+ if (n === void 0) return reportPromptAborted();
2438
+ const keepLast = parseKeepLast(n);
2439
+ if (keepLast === void 0) {
2440
+ fail("keep-last must be a non-negative integer");
2441
+ return { ok: false };
2442
+ }
2443
+ return { ok: true, olderThanMs: void 0, keepLast };
2444
+ }
2445
+ fail(`unrecognised choice '${criterion}' \u2014 expected 1 or 2`);
2446
+ return { ok: false };
2447
+ };
2448
+ var promptFlowFilterChoice = async (rl, flows) => {
2449
+ const flowAnswer = (await askLine(rl, `restrict to a flow? [enter for all, or one of: ${flows.join(", ")}] `))?.trim();
2450
+ if (flowAnswer === void 0) return reportPromptAborted();
2451
+ const flowFilter = flowAnswer.length === 0 ? void 0 : flowAnswer;
2452
+ if (flowFilter !== void 0 && !flows.includes(flowFilter)) {
2453
+ fail(`no such flow '${flowFilter}'`);
2454
+ return { ok: false };
2455
+ }
2456
+ return { ok: true, flowFilter };
2457
+ };
2458
+ var selectCandidates = (grouped, filters) => {
2459
+ const { olderThanMs, keepLast } = filters;
2460
+ const candidates = [];
2461
+ const unknownStampWarnings = [];
2462
+ const nowMs = Date.now();
2463
+ for (const [, runsForFlow] of grouped) {
2464
+ const keep = buildKeepSet(runsForFlow, keepLast);
2465
+ for (const run of runsForFlow) {
2466
+ const { qualifies, unknownStamp } = evaluatePruneQualification(run, keep, olderThanMs, nowMs);
2467
+ if (unknownStamp) unknownStampWarnings.push(run.path);
2468
+ if (qualifies) candidates.push(run);
2469
+ }
2470
+ }
2471
+ return { candidates, unknownStampWarnings };
2472
+ };
2473
+ var buildKeepSet = (runsForFlow, keepLast) => {
2474
+ if (keepLast === void 0) return void 0;
2475
+ const keep = /* @__PURE__ */ new Set();
2476
+ for (const r of runsForFlow.slice(0, keepLast)) keep.add(r.path);
2477
+ return keep;
2478
+ };
2479
+ var evaluatePruneQualification = (run, keepSet, olderThanMs, nowMs) => {
2480
+ let ageQualifies = true;
2481
+ let unknownStamp = false;
2482
+ if (olderThanMs !== void 0) {
2483
+ if (run.timestamp === null) {
2484
+ unknownStamp = true;
2485
+ ageQualifies = false;
2486
+ } else {
2487
+ ageQualifies = nowMs - run.timestamp.getTime() >= olderThanMs;
2488
+ }
2489
+ }
2490
+ const keepQualifies = keepSet === void 0 ? true : !keepSet.has(run.path);
2491
+ return { qualifies: ageQualifies && keepQualifies, unknownStamp };
2492
+ };
2493
+ var printCandidateSummary = (candidates) => {
2494
+ const grouped = groupByFlow(candidates);
2495
+ process.stdout.write("candidates:\n");
2496
+ let totalBytes = 0;
2497
+ for (const [flow, runs] of grouped) {
2498
+ const flowBytes = runs.reduce((acc, r) => acc + r.sizeBytes, 0);
2499
+ totalBytes += flowBytes;
2500
+ process.stdout.write(` ${flow}: ${plural(runs.length, "run")}, ${formatBytes(flowBytes)}
2501
+ `);
2502
+ }
2503
+ process.stdout.write(` total: ${plural(candidates.length, "run")}, ${formatBytes(totalBytes)}
2504
+ `);
2505
+ };
2506
+ var performPrune = async (candidates) => {
2507
+ const failures = [];
2508
+ let freedBytes = 0;
2509
+ let freedCount = 0;
2510
+ for (const candidate of candidates) {
2511
+ try {
2512
+ await fs4.rm(String(candidate.path), { recursive: true, force: false });
2513
+ freedBytes += candidate.sizeBytes;
2514
+ freedCount += 1;
2515
+ } catch (cause) {
2516
+ const reason = messageOf(cause);
2517
+ failures.push({ path: String(candidate.path), reason });
2518
+ }
2519
+ }
2520
+ process.stdout.write(`pruned ${plural(freedCount, "run")}, freed ${formatBytes(freedBytes)}
2521
+ `);
2522
+ if (failures.length > 0) {
2523
+ for (const failure of failures) {
2524
+ process.stderr.write(` failed: ${failure.path} \u2014 ${failure.reason}
2525
+ `);
2526
+ }
2527
+ fail(`${plural(failures.length, "run")} could not be deleted`);
2528
+ }
2529
+ };
2530
+
2531
+ // src/application/ui/cli/commands/agents.ts
2532
+ var IMPLEMENT_ROLES = ["generator", "evaluator"];
2533
+ var listAgentsAction = async () => {
2534
+ const { deps, storage } = await bootstrapApp();
2535
+ const bundledSource = createBundledAgentDefinitionSource();
2536
+ const operatorSource = createOperatorAgentDefinitionSource({
2537
+ operatorAgentDefinitionsRoot: storage.operatorAgentDefinitionsRoot,
2538
+ logger: deps.logger,
2539
+ warnIfVague: (definition) => warnIfVague(deps.logger, definition)
2540
+ });
2541
+ const [bundled, operator] = await Promise.all([bundledSource.list(), operatorSource.list()]);
2542
+ if (!okOrFail(bundled)) return;
2543
+ if (!okOrFail(operator)) return;
2544
+ const showFlow = createSettingsShowFlow({ settingsRepo: deps.settingsRepo });
2545
+ const current = await showFlow.execute({ input: void 0 });
2546
+ if (!current.ok) {
2547
+ fail(current.error.error.message);
2548
+ return;
2549
+ }
2550
+ const agentBindings = current.value.ctx.output.ai.implement.agents;
2551
+ const byName = /* @__PURE__ */ new Map();
2552
+ for (const definition of bundled.value) byName.set(definition.name, { tier: "bundled", definition });
2553
+ for (const definition of operator.value) byName.set(definition.name, { tier: "operator", definition });
2554
+ if (byName.size === 0) {
2555
+ process.stdout.write("(no agent definitions available)\n");
2556
+ return;
2557
+ }
2558
+ const listed = [...byName.values()].sort((a, b) => a.definition.name.localeCompare(b.definition.name));
2559
+ for (const { tier, definition } of listed) {
2560
+ const boundRoles = IMPLEMENT_ROLES.filter((role) => agentBindings?.[role] === definition.name);
2561
+ const boundLabel = boundRoles.length > 0 ? boundRoles.join(", ") : "-";
2562
+ process.stdout.write(
2563
+ `${definition.name.padEnd(28)} ${tier.padEnd(8)} bound: ${boundLabel.padEnd(20)} ${definition.description}
2564
+ `
2565
+ );
2566
+ }
2567
+ };
2568
+ var registerAgentsCommand = (program) => {
2569
+ const agents = program.command("agents").description("inspect portable agent definitions");
2570
+ agents.command("list").description("list bundled + operator agent definitions and which implement role each is bound to").action(listAgentsAction);
2571
+ };
2572
+
2573
+ // src/application/ui/cli/commands/skills.ts
2574
+ var savedDisabledFrom = (skills) => {
2575
+ const byFlow = /* @__PURE__ */ new Map();
2576
+ for (const flowId of FLOW_IDS) byFlow.set(flowId, new Set(skills?.[flowId]?.disabled ?? []));
2577
+ return (flowId) => byFlow.get(flowId) ?? /* @__PURE__ */ new Set();
2578
+ };
2579
+ var enabledFlowsFor = (entry, savedDisabled) => SKILL_MOUNTING_FLOW_IDS.filter((flowId) => {
2580
+ if (entry.defaultFor.includes(flowId)) return !savedDisabled(flowId).has(entry.name);
2581
+ const install = entry.installs.find((i) => i.flow === flowId);
2582
+ return install !== void 0 && install.status !== "broken";
2583
+ });
2584
+ var PROVENANCE_RANK = {
2585
+ "locally-modified": 5,
2586
+ broken: 4,
2587
+ "update-available": 3,
2588
+ "in-sync": 2,
2589
+ manual: 1
2590
+ };
2591
+ var provenanceLabel = (status) => {
2592
+ switch (status) {
2593
+ case "locally-modified":
2594
+ return "locally modified";
2595
+ case "broken":
2596
+ return "broken copy";
2597
+ case "update-available":
2598
+ return "update available";
2599
+ case "in-sync":
2600
+ return "in sync";
2601
+ case "manual":
2602
+ return "manual";
2603
+ }
2604
+ };
2605
+ var provenanceFor = (entry) => {
2606
+ if (entry.installs.length === 0) return "-";
2607
+ const worst = entry.installs.reduce((acc, i) => PROVENANCE_RANK[i.status] > PROVENANCE_RANK[acc.status] ? i : acc);
2608
+ return provenanceLabel(worst.status);
2609
+ };
2610
+ var formatSkillLine = (entry, bundledNames, savedDisabled) => {
2611
+ const tier = bundledNames.has(entry.name) ? "bundled" : "manual";
2612
+ const flowsLabel = enabledFlowsFor(entry, savedDisabled).join(", ") || "-";
2613
+ const provenance = provenanceFor(entry);
2614
+ return `${entry.name.padEnd(38)} ${tier.padEnd(8)} flows: ${flowsLabel.padEnd(44)} ${provenance.padEnd(18)} ${entry.description}`;
2615
+ };
2616
+ var listSkillsAction = async () => {
2617
+ const { deps } = await bootstrapApp();
2618
+ const catalogR = await deps.skillCatalog.list();
2619
+ if (!okOrFail(catalogR)) return;
2620
+ if (catalogR.value.length === 0) {
2621
+ process.stdout.write("(no skills bundled)\n");
2622
+ return;
2623
+ }
2624
+ const settingsR = await deps.settingsRepo.load();
2625
+ const savedDisabled = savedDisabledFrom(settingsR.ok ? settingsR.value.ai.skills : void 0);
2626
+ const bundledNames = new Set(BUNDLED_SKILLS.map((e) => e.name));
2627
+ const sorted = [...catalogR.value].sort((a, b) => a.name.localeCompare(b.name));
2628
+ for (const entry of sorted) {
2629
+ process.stdout.write(`${formatSkillLine(entry, bundledNames, savedDisabled)}
2630
+ `);
2631
+ }
2632
+ };
2633
+ var registerSkillsCommand = (program) => {
2634
+ const skills = program.command("skills").description("inspect the bundled skill catalog");
2635
+ skills.command("list").description("list bundled + manually-dropped skills, their tier, enabled flows, and provenance").action(listSkillsAction);
2636
+ };
2637
+
2638
+ // src/integration/ai/prompts/_engine/bundled-templates.ts
2639
+ var BUNDLED_PROMPT_TEMPLATES = [
2640
+ "apply-feedback",
2641
+ "create-pr",
2642
+ "detect-scripts",
2643
+ "detect-skills",
2644
+ "distill-learnings",
2645
+ "evaluate",
2646
+ "evaluate-continuation",
2647
+ "ideate",
2648
+ "implement",
2649
+ "implement-continuation",
2650
+ "implement-crash-resume",
2651
+ "plan",
2652
+ "readiness",
2653
+ "refine",
2654
+ "reproduce",
2655
+ "select-candidate"
2656
+ ];
2657
+ var BUNDLED_PROMPT_PARTIALS = [
2658
+ "approval-gate",
2659
+ "autonomous-operation",
2660
+ "conventions-agents-md",
2661
+ "conventions-claude-md",
2662
+ "conventions-copilot-instructions",
2663
+ "decisions",
2664
+ "evaluator-failure-modes",
2665
+ "evaluator-grading-rules",
2666
+ "evidence-bound",
2667
+ "git-boundary",
2668
+ "harness-context",
2669
+ "one-shot-operation",
2670
+ "parallel-tool-calls",
2671
+ "question-format",
2672
+ "skill-body",
2673
+ "task-blocked",
2674
+ "task-fields",
2675
+ "task-sizing",
2676
+ "validation-checklist"
2677
+ ];
2678
+
2679
+ // src/application/ui/cli/commands/prompts.ts
2680
+ var formatPromptLine = (loaded) => `${loaded.name.padEnd(26)} ${loaded.kind.padEnd(8)} ${String(loaded.bytes).padStart(6)} bytes`;
2681
+ var listPromptsAction = async () => {
2682
+ const { deps } = await bootstrapApp();
2683
+ const requested = [
2684
+ ...BUNDLED_PROMPT_TEMPLATES.map((name) => ({ name, kind: "template", bytes: 0 })),
2685
+ ...BUNDLED_PROMPT_PARTIALS.map((name) => ({ name, kind: "partial", bytes: 0 }))
2686
+ ];
2687
+ const loaded = [];
2688
+ for (const entry of requested) {
2689
+ const body = await deps.templateLoader.load(entry.name);
2690
+ if (!okOrFail(body)) return;
2691
+ if (body.value.trim().length === 0) {
2692
+ fail(`prompt template '${entry.name}' resolved to an empty file \u2014 the install is incomplete`);
2693
+ return;
2694
+ }
2695
+ loaded.push({ ...entry, bytes: Buffer.byteLength(body.value, "utf8") });
2696
+ }
2697
+ for (const entry of loaded.sort((a, b) => a.name.localeCompare(b.name))) {
2698
+ process.stdout.write(`${formatPromptLine(entry)}
2699
+ `);
2700
+ }
2701
+ };
2702
+ var registerPromptsCommand = (program) => {
2703
+ const prompts = program.command("prompts").description("inspect the bundled prompt templates");
2704
+ prompts.command("list").description("load every bundled prompt template + partial and list its name, kind and size").action(listPromptsAction);
2705
+ };
2706
+
2707
+ // src/application/ui/cli/cli.ts
2708
+ var runCli = async (argv) => {
2709
+ const program = new Command();
2710
+ program.name("ralphctl").description("ralphctl \u2014 interactive TUI and CLI").version(CLI_METADATA.currentVersion, "-v, --version", "show version").allowExcessArguments(true).option(
2711
+ "--implement-generator-provider <provider>",
2712
+ "override settings.ai.implement.generator.provider for this launch (requires --implement-generator-model)"
2713
+ ).option(
2714
+ "--implement-generator-model <model>",
2715
+ "override settings.ai.implement.generator.model for this launch (requires --implement-generator-provider)"
2716
+ ).option(
2717
+ "--implement-evaluator-provider <provider>",
2718
+ "override settings.ai.implement.evaluator.provider for this launch (requires --implement-evaluator-model)"
2719
+ ).option(
2720
+ "--implement-evaluator-model <model>",
2721
+ "override settings.ai.implement.evaluator.model for this launch (requires --implement-evaluator-provider)"
2722
+ ).action(async (opts, command) => {
2723
+ const verb = command.args[0];
2724
+ if (verb !== void 0) {
2725
+ const isFlow = flowRegistry.some((entry) => entry.manifest.id === verb);
2726
+ const flowHint = isFlow ? ` \u2014 '${verb}' is an interactive flow \u2014 launch the TUI with bare 'ralphctl'` : "";
2727
+ process.stderr.write(
2728
+ `error: unknown command '${verb}' \u2014 run 'ralphctl --help' for available commands${flowHint}
2729
+ `
2730
+ );
2731
+ process.exitCode = 1;
2732
+ return;
2733
+ }
2734
+ const parsed = parseImplementRoleOverrides({
2735
+ generatorProvider: opts.implementGeneratorProvider,
2736
+ generatorModel: opts.implementGeneratorModel,
2737
+ evaluatorProvider: opts.implementEvaluatorProvider,
2738
+ evaluatorModel: opts.implementEvaluatorModel
2739
+ });
2740
+ if (!parsed.ok) {
2741
+ process.stderr.write(`ralphctl: ${parsed.error}
2742
+ `);
2743
+ process.exitCode = 1;
2744
+ return;
2745
+ }
2746
+ const { launchTui } = await import("./launch-VBRTQP4L.mjs");
2747
+ await launchTui({ ...parsed.overrides !== void 0 ? { implementRoleOverrides: parsed.overrides } : {} });
2748
+ });
2749
+ registerExportRequirementsCommand(program);
2750
+ registerExportContextCommand(program);
2751
+ registerCreatePrCommand(program);
2752
+ registerDoctorCommand(program);
2753
+ registerDemoCommand(program);
2754
+ registerSettingsCommand(program);
2755
+ registerCompletionCommand(program);
2756
+ registerProjectCommand(program);
2757
+ registerSprintCommand(program);
2758
+ registerTicketCommand(program);
2759
+ registerTaskCommand(program);
2760
+ registerRunsCommand(program);
2761
+ registerAgentsCommand(program);
2762
+ registerSkillsCommand(program);
2763
+ registerPromptsCommand(program);
2764
+ try {
2765
+ await program.parseAsync([...argv]);
2766
+ } catch (err) {
2767
+ reportFatal(err);
2768
+ }
2769
+ };
2770
+ export {
2771
+ runCli
2772
+ };