@p10i/rundown 1.0.0-rc.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js ADDED
@@ -0,0 +1,2585 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/presentation/cli.ts
4
+ import { Command } from "commander";
5
+
6
+ // src/application/run-task.ts
7
+ import path from "path";
8
+
9
+ // src/domain/defaults.ts
10
+ var DEFAULT_TEMPLATE_SHARED_PREFIX = `{{context}}
11
+
12
+ ---
13
+
14
+ The Markdown above is the source document up to but not including the selected unchecked task.
15
+
16
+ ## Source file
17
+
18
+ \`{{file}}\` (line {{taskLine}})
19
+
20
+ ## Selected task
21
+
22
+ {{task}}
23
+ `;
24
+ var DEFAULT_TASK_TEMPLATE = `${DEFAULT_TEMPLATE_SHARED_PREFIX}
25
+
26
+ ## Phase
27
+
28
+ Execute the selected task.
29
+
30
+ Complete the task described above. Make the necessary changes to the project, but do not edit the source Markdown task file as part of completion tracking.
31
+
32
+ - Do not change the checkbox in the source Markdown file.
33
+ - Do not rewrite the task item to make it look completed.
34
+ - Do not treat editing the TODO file itself as evidence that the task is done unless the task explicitly requires documentation changes in that file.
35
+ - rundown is responsible for marking the task complete after validation succeeds.
36
+ `;
37
+ var DEFAULT_VALIDATE_TEMPLATE = `${DEFAULT_TEMPLATE_SHARED_PREFIX}
38
+
39
+ ## Phase
40
+
41
+ Verify whether the selected task is complete.
42
+
43
+ Evaluate whether the task above has been completed.
44
+
45
+ Write your result to a file named \`{{file}}.{{taskIndex}}.validation\` next to the source file.
46
+
47
+ - If the task is complete, write exactly: OK
48
+ - If the task is not complete, write a short explanation of what is still missing.
49
+
50
+ Do not modify the source Markdown task file or change its checkbox state. Validation is determined only by the actual project state and the sidecar file above.
51
+
52
+ Do not write anything else.
53
+ `;
54
+ var DEFAULT_CORRECT_TEMPLATE = `${DEFAULT_TEMPLATE_SHARED_PREFIX}
55
+
56
+ ## Phase
57
+
58
+ Repair the selected task after a failed verification pass.
59
+
60
+ ## Previous validation result
61
+
62
+ {{validationResult}}
63
+
64
+ Please fix what is missing or incorrect. The validation above explains what still needs to be done.
65
+
66
+ - Do not change the checkbox in the source Markdown file.
67
+ - Do not mark the task complete yourself.
68
+ - rundown will update task completion only after validation succeeds.
69
+
70
+ After making corrections, the task will be validated again.
71
+ `;
72
+ var DEFAULT_VARS_FILE_CONTENT = `{
73
+ "branch": "main",
74
+ "ticket": "ENG-42"
75
+ }
76
+ `;
77
+ var DEFAULT_PLAN_TEMPLATE = `${DEFAULT_TEMPLATE_SHARED_PREFIX}
78
+
79
+ ## Phase
80
+
81
+ Plan the selected task by decomposing it into concrete subtasks.
82
+
83
+ Break this task into smaller, actionable subtasks.
84
+
85
+ Return ONLY a Markdown list of unchecked task items using \`- [ ]\` syntax, one per subtask.
86
+
87
+ Rules:
88
+ - Each subtask should be a single clear action.
89
+ - Together the subtasks should fully cover the parent task.
90
+ - Do not include the parent task itself.
91
+ - Do not include any other text, headings, or explanation.
92
+ - Do not modify the source Markdown file.
93
+
94
+ Example output format:
95
+
96
+ - [ ] First concrete step
97
+ - [ ] Second concrete step
98
+ - [ ] Third concrete step
99
+ `;
100
+
101
+ // src/domain/checkbox.ts
102
+ function markChecked(source, task) {
103
+ const eol = source.includes("\r\n") ? "\r\n" : "\n";
104
+ const lines = source.split(/\r?\n/);
105
+ const lineIndex = task.line - 1;
106
+ if (lineIndex < 0 || lineIndex >= lines.length) {
107
+ throw new Error(`Task line ${task.line} is out of range in ${task.file}`);
108
+ }
109
+ const line = lines[lineIndex];
110
+ const updated = line.replace(/\[ \]/, "[x]");
111
+ if (updated === line) {
112
+ throw new Error(`Could not find unchecked checkbox on line ${task.line} in ${task.file}`);
113
+ }
114
+ lines[lineIndex] = updated;
115
+ return lines.join(eol);
116
+ }
117
+
118
+ // src/domain/run-options.ts
119
+ function resolveRunBehavior(input) {
120
+ const maxRetries = Number.isFinite(input.retries) && input.retries > 0 ? Math.floor(input.retries) : 0;
121
+ const onlyValidate = input.onlyValidate;
122
+ const shouldValidate = input.validate || onlyValidate;
123
+ const allowCorrection = !input.noCorrect && maxRetries > 0;
124
+ return {
125
+ shouldValidate,
126
+ onlyValidate,
127
+ allowCorrection,
128
+ maxRetries
129
+ };
130
+ }
131
+ function requiresWorkerCommand(input) {
132
+ if (input.workerCommand.length > 0) {
133
+ return false;
134
+ }
135
+ if (input.onlyValidate) {
136
+ return true;
137
+ }
138
+ if (!input.isInlineCli) {
139
+ return true;
140
+ }
141
+ return input.shouldValidate;
142
+ }
143
+
144
+ // src/domain/template.ts
145
+ function renderTemplate(template, vars) {
146
+ return template.replace(/\{\{(\w+)\}\}/g, (_match, key) => {
147
+ if (key in vars) {
148
+ return String(vars[key] ?? "");
149
+ }
150
+ return `{{${key}}}`;
151
+ });
152
+ }
153
+
154
+ // src/domain/template-vars.ts
155
+ var DEFAULT_TEMPLATE_VARS_FILE = ".rundown/vars.json";
156
+ var TEMPLATE_VAR_KEY = /^[A-Za-z_]\w*$/;
157
+ function parseCliTemplateVars(entries) {
158
+ const vars = {};
159
+ for (const entry of entries) {
160
+ const equalsIndex = entry.indexOf("=");
161
+ if (equalsIndex <= 0) {
162
+ throw new Error(`Invalid template variable "${entry}". Use key=value.`);
163
+ }
164
+ const key = entry.slice(0, equalsIndex).trim();
165
+ const value = entry.slice(equalsIndex + 1);
166
+ if (!TEMPLATE_VAR_KEY.test(key)) {
167
+ throw new Error(`Invalid template variable name "${key}". Use letters, numbers, and underscores only.`);
168
+ }
169
+ vars[key] = value;
170
+ }
171
+ return vars;
172
+ }
173
+ function resolveTemplateVarsFilePath(option) {
174
+ if (option === true) {
175
+ return DEFAULT_TEMPLATE_VARS_FILE;
176
+ }
177
+ return typeof option === "string" ? option : void 0;
178
+ }
179
+
180
+ // src/application/run-task.ts
181
+ function createRunTask(dependencies) {
182
+ const emit = dependencies.output.emit.bind(dependencies.output);
183
+ return async function runTask(options) {
184
+ const {
185
+ source,
186
+ mode,
187
+ transport,
188
+ sortMode,
189
+ verify,
190
+ onlyVerify,
191
+ noRepair,
192
+ retries,
193
+ dryRun,
194
+ printPrompt,
195
+ keepArtifacts,
196
+ varsFileOption,
197
+ cliTemplateVarArgs,
198
+ workerCommand,
199
+ commitAfterComplete,
200
+ commitMessageTemplate,
201
+ onCompleteCommand
202
+ } = options;
203
+ const runBehavior = resolveRunBehavior({
204
+ validate: verify,
205
+ onlyValidate: onlyVerify,
206
+ noCorrect: noRepair,
207
+ retries
208
+ });
209
+ const shouldValidate = runBehavior.shouldValidate;
210
+ const onlyValidate = runBehavior.onlyValidate;
211
+ const allowCorrection = runBehavior.allowCorrection;
212
+ const maxRetries = runBehavior.maxRetries;
213
+ const varsFilePath = resolveTemplateVarsFilePath(varsFileOption);
214
+ const fileTemplateVars = varsFilePath ? loadTemplateVarsFileFromPorts(varsFilePath, dependencies.workingDirectory.cwd(), dependencies.fileSystem) : {};
215
+ const cliTemplateVars = parseCliTemplateVars(cliTemplateVarArgs);
216
+ const extraTemplateVars = {
217
+ ...fileTemplateVars,
218
+ ...cliTemplateVars
219
+ };
220
+ let artifactContext = null;
221
+ let artifactsFinalized = false;
222
+ const finalizeArtifacts = (status, preserve = keepArtifacts) => {
223
+ if (!artifactContext || artifactsFinalized) {
224
+ return;
225
+ }
226
+ finalizeRunArtifacts(dependencies.artifactStore, artifactContext, preserve, status, emit);
227
+ artifactsFinalized = true;
228
+ };
229
+ const finishRun = (code, status, preserve = keepArtifacts) => {
230
+ finalizeArtifacts(status, preserve);
231
+ return code;
232
+ };
233
+ try {
234
+ const files = await dependencies.sourceResolver.resolveSources(source);
235
+ if (files.length === 0) {
236
+ emit({ kind: "warn", message: "No Markdown files found matching: " + source });
237
+ return 3;
238
+ }
239
+ const result = dependencies.taskSelector.selectNextTask(files, sortMode);
240
+ if (!result) {
241
+ emit({ kind: "info", message: "No unchecked tasks found." });
242
+ return 3;
243
+ }
244
+ const { task, source: fileSource, contextBefore } = result;
245
+ emit({ kind: "info", message: "Next task: " + formatTaskLabel(task) });
246
+ const automationCommand = getAutomationWorkerCommand(workerCommand, mode);
247
+ const templates = loadProjectTemplatesFromPorts(
248
+ dependencies.workingDirectory.cwd(),
249
+ dependencies.templateLoader
250
+ );
251
+ const vars = {
252
+ ...extraTemplateVars,
253
+ task: task.text,
254
+ file: task.file,
255
+ context: contextBefore,
256
+ taskIndex: task.index,
257
+ taskLine: task.line,
258
+ source: fileSource
259
+ };
260
+ const prompt = renderTemplate(templates.task, vars);
261
+ const validationPrompt = shouldValidate ? renderTemplate(templates.validate, vars) : "";
262
+ if (printPrompt && onlyValidate) {
263
+ emit({ kind: "text", text: validationPrompt });
264
+ return 0;
265
+ }
266
+ if (dryRun && onlyValidate) {
267
+ emit({ kind: "info", message: "Dry run \u2014 would run verification with: " + automationCommand.join(" ") });
268
+ emit({ kind: "info", message: "Prompt length: " + validationPrompt.length + " chars" });
269
+ return 0;
270
+ }
271
+ if (requiresWorkerCommand({
272
+ workerCommand,
273
+ isInlineCli: task.isInlineCli,
274
+ shouldValidate,
275
+ onlyValidate
276
+ })) {
277
+ emit({ kind: "error", message: "No worker command specified. Use --worker <command...> or -- <command>." });
278
+ return 1;
279
+ }
280
+ if (!onlyValidate && !task.isInlineCli) {
281
+ if (printPrompt) {
282
+ emit({ kind: "text", text: prompt });
283
+ return 0;
284
+ }
285
+ if (dryRun) {
286
+ emit({ kind: "info", message: "Dry run \u2014 would run: " + workerCommand.join(" ") });
287
+ emit({ kind: "info", message: "Prompt length: " + prompt.length + " chars" });
288
+ return 0;
289
+ }
290
+ }
291
+ if (!onlyValidate && task.isInlineCli && dryRun) {
292
+ emit({ kind: "info", message: "Dry run \u2014 would execute inline CLI: " + task.cliCommand });
293
+ return 0;
294
+ }
295
+ artifactContext = dependencies.artifactStore.createContext({
296
+ cwd: dependencies.workingDirectory.cwd(),
297
+ commandName: "run",
298
+ workerCommand: onlyValidate ? automationCommand : workerCommand,
299
+ mode,
300
+ transport,
301
+ source,
302
+ task: toRuntimeTaskMetadata(task, fileSource),
303
+ keepArtifacts
304
+ });
305
+ if (onlyValidate) {
306
+ emit({ kind: "info", message: "Only verify mode \u2014 skipping task execution." });
307
+ const valid = await runValidation(
308
+ dependencies,
309
+ task,
310
+ fileSource,
311
+ contextBefore,
312
+ templates,
313
+ automationCommand,
314
+ transport,
315
+ maxRetries,
316
+ allowCorrection,
317
+ extraTemplateVars,
318
+ artifactContext
319
+ );
320
+ if (!valid) {
321
+ emit({ kind: "error", message: "Verification failed after all retries. Task not checked." });
322
+ return finishRun(2, "verification-failed");
323
+ }
324
+ checkTaskUsingFileSystem(task, dependencies.fileSystem);
325
+ emit({ kind: "success", message: "Task checked: " + task.text });
326
+ await afterTaskComplete(
327
+ dependencies,
328
+ task,
329
+ source,
330
+ commitAfterComplete,
331
+ commitMessageTemplate,
332
+ onCompleteCommand
333
+ );
334
+ return finishRun(0, "completed");
335
+ }
336
+ if (task.isInlineCli) {
337
+ const inlineCliCwd = path.dirname(path.resolve(task.file));
338
+ emit({ kind: "info", message: "Executing inline CLI: " + task.cliCommand + " [cwd=" + inlineCliCwd + "]" });
339
+ const cliResult = await dependencies.workerExecutor.executeInlineCli(task.cliCommand, inlineCliCwd, {
340
+ artifactContext,
341
+ keepArtifacts,
342
+ artifactExtra: { taskType: "inline-cli" }
343
+ });
344
+ if (cliResult.stdout) emit({ kind: "text", text: cliResult.stdout });
345
+ if (cliResult.stderr) emit({ kind: "stderr", text: cliResult.stderr });
346
+ if (cliResult.exitCode !== 0) {
347
+ emit({ kind: "error", message: "Inline CLI exited with code " + cliResult.exitCode });
348
+ return finishRun(1, "execution-failed");
349
+ }
350
+ if (shouldValidate) {
351
+ const valid = await runValidation(
352
+ dependencies,
353
+ task,
354
+ fileSource,
355
+ contextBefore,
356
+ templates,
357
+ automationCommand,
358
+ transport,
359
+ maxRetries,
360
+ allowCorrection,
361
+ extraTemplateVars,
362
+ artifactContext
363
+ );
364
+ if (!valid) {
365
+ emit({ kind: "error", message: "Verification failed. Task not checked." });
366
+ return finishRun(2, "verification-failed");
367
+ }
368
+ }
369
+ checkTaskUsingFileSystem(task, dependencies.fileSystem);
370
+ emit({ kind: "success", message: "Task checked: " + task.text });
371
+ await afterTaskComplete(
372
+ dependencies,
373
+ task,
374
+ source,
375
+ commitAfterComplete,
376
+ commitMessageTemplate,
377
+ onCompleteCommand
378
+ );
379
+ return finishRun(0, "completed");
380
+ }
381
+ emit({ kind: "info", message: "Running: " + workerCommand.join(" ") + " [mode=" + mode + ", transport=" + transport + "]" });
382
+ const runResult = await dependencies.workerExecutor.runWorker({
383
+ command: workerCommand,
384
+ prompt,
385
+ mode,
386
+ transport,
387
+ cwd: dependencies.workingDirectory.cwd(),
388
+ artifactContext,
389
+ artifactPhase: "execute"
390
+ });
391
+ if (mode === "wait") {
392
+ if (runResult.stdout) emit({ kind: "text", text: runResult.stdout });
393
+ if (runResult.stderr) emit({ kind: "stderr", text: runResult.stderr });
394
+ }
395
+ if (mode !== "detached" && runResult.exitCode !== 0 && runResult.exitCode !== null) {
396
+ emit({ kind: "error", message: "Worker exited with code " + runResult.exitCode + "." });
397
+ return finishRun(1, "execution-failed");
398
+ }
399
+ if (mode === "detached") {
400
+ emit({ kind: "info", message: "Detached mode \u2014 skipping immediate verification and leaving the task unchecked." });
401
+ return finishRun(0, "detached", true);
402
+ }
403
+ if (shouldValidate) {
404
+ const valid = await runValidation(
405
+ dependencies,
406
+ task,
407
+ fileSource,
408
+ contextBefore,
409
+ templates,
410
+ automationCommand,
411
+ transport,
412
+ maxRetries,
413
+ allowCorrection,
414
+ extraTemplateVars,
415
+ artifactContext
416
+ );
417
+ if (!valid) {
418
+ emit({ kind: "error", message: "Verification failed after all retries. Task not checked." });
419
+ return finishRun(2, "verification-failed");
420
+ }
421
+ }
422
+ checkTaskUsingFileSystem(task, dependencies.fileSystem);
423
+ emit({ kind: "success", message: "Task checked: " + task.text });
424
+ await afterTaskComplete(
425
+ dependencies,
426
+ task,
427
+ source,
428
+ commitAfterComplete,
429
+ commitMessageTemplate,
430
+ onCompleteCommand
431
+ );
432
+ return finishRun(0, "completed");
433
+ } catch (error) {
434
+ finalizeArtifacts("failed", keepArtifacts || mode === "detached");
435
+ throw error;
436
+ }
437
+ };
438
+ }
439
+ async function runValidation(dependencies, task, fileSource, contextBefore, templates, workerCommand, transport, maxRetries, allowCorrection, extraTemplateVars, artifactContext) {
440
+ const emit = dependencies.output.emit.bind(dependencies.output);
441
+ emit({ kind: "info", message: "Running verification..." });
442
+ const valid = await dependencies.taskValidation.validate({
443
+ task,
444
+ source: fileSource,
445
+ contextBefore,
446
+ template: templates.validate,
447
+ command: workerCommand,
448
+ mode: "wait",
449
+ transport,
450
+ templateVars: extraTemplateVars,
451
+ artifactContext
452
+ });
453
+ if (valid) {
454
+ dependencies.validationSidecar.remove(task);
455
+ emit({ kind: "success", message: "Verification passed." });
456
+ return true;
457
+ }
458
+ if (allowCorrection) {
459
+ emit({ kind: "warn", message: "Verification failed. Running repair (" + maxRetries + " retries)..." });
460
+ const result = await dependencies.taskCorrection.correct({
461
+ task,
462
+ source: fileSource,
463
+ contextBefore,
464
+ correctTemplate: templates.correct,
465
+ validateTemplate: templates.validate,
466
+ command: workerCommand,
467
+ maxRetries,
468
+ mode: "wait",
469
+ transport,
470
+ templateVars: extraTemplateVars,
471
+ artifactContext
472
+ });
473
+ if (result.valid) {
474
+ dependencies.validationSidecar.remove(task);
475
+ emit({ kind: "success", message: "Repair succeeded after " + result.attempts + " attempt(s)." });
476
+ return true;
477
+ }
478
+ }
479
+ return false;
480
+ }
481
+ async function afterTaskComplete(dependencies, task, source, commit, commitMessageTemplate, onCompleteCommand) {
482
+ const cwd = dependencies.workingDirectory.cwd();
483
+ const emit = dependencies.output.emit.bind(dependencies.output);
484
+ if (commit) {
485
+ try {
486
+ const inGitRepo = await isGitRepoWithGitClient(dependencies.gitClient, cwd);
487
+ if (!inGitRepo) {
488
+ emit({ kind: "warn", message: "--commit: not inside a git repository, skipping." });
489
+ } else {
490
+ const message = buildCommitMessage(task, cwd, commitMessageTemplate);
491
+ await commitCheckedTaskWithGitClient(dependencies.gitClient, task, cwd, message);
492
+ emit({ kind: "success", message: "Committed: " + message });
493
+ }
494
+ } catch (error) {
495
+ emit({ kind: "warn", message: "--commit failed: " + String(error) });
496
+ }
497
+ }
498
+ if (onCompleteCommand) {
499
+ try {
500
+ const result = await runOnCompleteHookWithProcessRunner(
501
+ dependencies.processRunner,
502
+ onCompleteCommand,
503
+ task,
504
+ source,
505
+ cwd
506
+ );
507
+ if (result.stdout) emit({ kind: "text", text: result.stdout });
508
+ if (result.stderr) emit({ kind: "stderr", text: result.stderr });
509
+ if (!result.success) {
510
+ emit({ kind: "warn", message: "--on-complete hook exited with code " + result.exitCode });
511
+ }
512
+ } catch (error) {
513
+ emit({ kind: "warn", message: "--on-complete hook failed: " + String(error) });
514
+ }
515
+ }
516
+ }
517
+ function getAutomationWorkerCommand(workerCommand, mode) {
518
+ if (mode !== "tui") {
519
+ return workerCommand;
520
+ }
521
+ if (!isOpenCodeWorkerCommand(workerCommand)) {
522
+ return workerCommand;
523
+ }
524
+ return workerCommand.length > 1 ? workerCommand : [workerCommand[0], "run"];
525
+ }
526
+ function finalizeRunArtifacts(artifactStore, artifactContext, preserve, status, emit) {
527
+ artifactStore.finalize(artifactContext, { status, preserve });
528
+ if (preserve) {
529
+ emit({ kind: "info", message: "Runtime artifacts saved at " + artifactStore.displayPath(artifactContext) + "." });
530
+ }
531
+ }
532
+ var TEMPLATE_VAR_KEY2 = /^[A-Za-z_]\w*$/;
533
+ var DEFAULT_COMMIT_MESSAGE_TEMPLATE = 'rundown: complete "{{task}}" in {{file}}';
534
+ function loadTemplateVarsFileFromPorts(filePath, cwd, fileSystem) {
535
+ const resolvedPath = path.resolve(cwd, filePath);
536
+ let parsed;
537
+ try {
538
+ parsed = JSON.parse(fileSystem.readText(resolvedPath));
539
+ } catch (error) {
540
+ throw new Error(`Failed to read template vars file "${filePath}": ${String(error)}`);
541
+ }
542
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
543
+ throw new Error(`Template vars file "${filePath}" must contain a JSON object.`);
544
+ }
545
+ const vars = {};
546
+ for (const [key, value] of Object.entries(parsed)) {
547
+ if (!TEMPLATE_VAR_KEY2.test(key)) {
548
+ throw new Error(`Invalid template variable name "${key}" in "${filePath}". Use letters, numbers, and underscores only.`);
549
+ }
550
+ if (value === null || value === void 0) {
551
+ vars[key] = "";
552
+ continue;
553
+ }
554
+ if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
555
+ vars[key] = String(value);
556
+ continue;
557
+ }
558
+ throw new Error(`Template variable "${key}" in "${filePath}" must be a string, number, boolean, or null.`);
559
+ }
560
+ return vars;
561
+ }
562
+ function loadProjectTemplatesFromPorts(cwd, templateLoader) {
563
+ const dir = path.join(cwd, ".rundown");
564
+ return {
565
+ task: templateLoader.load(path.join(dir, "execute.md")) ?? DEFAULT_TASK_TEMPLATE,
566
+ validate: templateLoader.load(path.join(dir, "verify.md")) ?? DEFAULT_VALIDATE_TEMPLATE,
567
+ correct: templateLoader.load(path.join(dir, "repair.md")) ?? DEFAULT_CORRECT_TEMPLATE,
568
+ plan: templateLoader.load(path.join(dir, "plan.md")) ?? DEFAULT_PLAN_TEMPLATE
569
+ };
570
+ }
571
+ function checkTaskUsingFileSystem(task, fileSystem) {
572
+ const source = fileSystem.readText(task.file);
573
+ const updated = markChecked(source, task);
574
+ fileSystem.writeText(task.file, updated);
575
+ }
576
+ async function isGitRepoWithGitClient(gitClient, cwd) {
577
+ try {
578
+ await gitClient.run(["rev-parse", "--is-inside-work-tree"], cwd);
579
+ return true;
580
+ } catch {
581
+ return false;
582
+ }
583
+ }
584
+ async function commitCheckedTaskWithGitClient(gitClient, task, cwd, message) {
585
+ const relativePath = path.relative(cwd, task.file).replace(/\\/g, "/");
586
+ await gitClient.run(["add", "--", relativePath], cwd);
587
+ await gitClient.run(["commit", "-m", message], cwd);
588
+ }
589
+ function buildCommitMessage(task, cwd, messageTemplate) {
590
+ const relativePath = path.relative(cwd, task.file).replace(/\\/g, "/");
591
+ return renderTemplate(messageTemplate ?? DEFAULT_COMMIT_MESSAGE_TEMPLATE, {
592
+ task: task.text,
593
+ file: relativePath,
594
+ context: "",
595
+ taskIndex: task.index,
596
+ taskLine: task.line,
597
+ source: ""
598
+ });
599
+ }
600
+ async function runOnCompleteHookWithProcessRunner(processRunner, command, task, source, cwd) {
601
+ try {
602
+ const result = await processRunner.run({
603
+ command,
604
+ args: [],
605
+ cwd,
606
+ mode: "wait",
607
+ shell: true,
608
+ timeoutMs: 6e4,
609
+ env: {
610
+ ...process.env,
611
+ RUNDOWN_TASK: task.text,
612
+ RUNDOWN_FILE: path.resolve(task.file),
613
+ RUNDOWN_LINE: String(task.line),
614
+ RUNDOWN_INDEX: String(task.index),
615
+ RUNDOWN_SOURCE: source
616
+ }
617
+ });
618
+ return {
619
+ success: result.exitCode === 0,
620
+ exitCode: result.exitCode,
621
+ stdout: result.stdout,
622
+ stderr: result.stderr
623
+ };
624
+ } catch (error) {
625
+ return {
626
+ success: false,
627
+ exitCode: null,
628
+ stdout: "",
629
+ stderr: error instanceof Error ? error.message : String(error)
630
+ };
631
+ }
632
+ }
633
+ function formatTaskLabel(task) {
634
+ return `${task.file}:${task.line} [#${task.index}] ${task.text}`;
635
+ }
636
+ function toRuntimeTaskMetadata(task, source) {
637
+ return {
638
+ text: task.text,
639
+ file: task.file,
640
+ line: task.line,
641
+ index: task.index,
642
+ source
643
+ };
644
+ }
645
+ function isOpenCodeWorkerCommand(workerCommand) {
646
+ if (workerCommand.length === 0) {
647
+ return false;
648
+ }
649
+ const command = workerCommand[0].toLowerCase();
650
+ return command === "opencode" || command.endsWith("/opencode") || command.endsWith("\\opencode") || command.endsWith("/opencode.cmd") || command.endsWith("\\opencode.cmd") || command.endsWith("/opencode.exe") || command.endsWith("\\opencode.exe") || command.endsWith("/opencode.ps1") || command.endsWith("\\opencode.ps1");
651
+ }
652
+
653
+ // src/application/plan-task.ts
654
+ import path2 from "path";
655
+
656
+ // src/domain/planner.ts
657
+ function parsePlannerOutput(output) {
658
+ const lines = output.split(/\r?\n/);
659
+ const taskPattern = /^\s*[-*+]\s+\[ \]\s+\S/;
660
+ return lines.filter((line) => taskPattern.test(line)).map((line) => line.replace(/^\s+/, ""));
661
+ }
662
+ function computeChildIndent(parentLine) {
663
+ const leadingWhitespace = parentLine.match(/^(\s*)/)?.[1] ?? "";
664
+ const indentUnit = " ";
665
+ return leadingWhitespace + indentUnit;
666
+ }
667
+ function insertSubitems(source, task, subitemLines) {
668
+ if (subitemLines.length === 0) return source;
669
+ const eol = source.includes("\r\n") ? "\r\n" : "\n";
670
+ const lines = source.split(/\r?\n/);
671
+ const parentLineIndex = task.line - 1;
672
+ if (parentLineIndex < 0 || parentLineIndex >= lines.length) {
673
+ throw new Error(`Task line ${task.line} is out of range.`);
674
+ }
675
+ const parentLine = lines[parentLineIndex];
676
+ const indent = computeChildIndent(parentLine);
677
+ const indented = subitemLines.map((item) => {
678
+ const text = item.replace(/^[-*+]\s+/, "");
679
+ return `${indent}- ${text}`;
680
+ });
681
+ lines.splice(parentLineIndex + 1, 0, ...indented);
682
+ return lines.join(eol);
683
+ }
684
+
685
+ // src/application/plan-task.ts
686
+ function createPlanTask(dependencies) {
687
+ const emit = dependencies.output.emit.bind(dependencies.output);
688
+ return async function planTask(options) {
689
+ const {
690
+ source,
691
+ at,
692
+ mode,
693
+ transport,
694
+ sortMode,
695
+ dryRun,
696
+ printPrompt,
697
+ keepArtifacts,
698
+ varsFileOption,
699
+ cliTemplateVarArgs,
700
+ workerCommand
701
+ } = options;
702
+ const varsFilePath = resolveTemplateVarsFilePath(varsFileOption);
703
+ const cwd = dependencies.workingDirectory.cwd();
704
+ const fileTemplateVars = varsFilePath ? loadTemplateVarsFileFromPorts2(varsFilePath, cwd, dependencies.fileSystem) : {};
705
+ const cliTemplateVars = parseCliTemplateVars(cliTemplateVarArgs);
706
+ const extraTemplateVars = {
707
+ ...fileTemplateVars,
708
+ ...cliTemplateVars
709
+ };
710
+ const selection = await selectPlanTask(source, at, sortMode, dependencies, emit);
711
+ if (!selection.result) {
712
+ return selection.exitCode;
713
+ }
714
+ const { task, source: fileSource, contextBefore } = selection.result;
715
+ emit({
716
+ kind: "info",
717
+ message: "Planning task: " + formatTaskLabel2(task)
718
+ });
719
+ if (workerCommand.length === 0) {
720
+ emit({
721
+ kind: "error",
722
+ message: "No worker command specified. Use --worker <command...> or -- <command>."
723
+ });
724
+ return 1;
725
+ }
726
+ const planTemplate = loadPlanTemplateFromPorts(cwd, dependencies.templateLoader);
727
+ const vars = {
728
+ ...extraTemplateVars,
729
+ task: task.text,
730
+ file: task.file,
731
+ context: contextBefore,
732
+ taskIndex: task.index,
733
+ taskLine: task.line,
734
+ source: fileSource
735
+ };
736
+ const prompt = renderTemplate(planTemplate, vars);
737
+ if (printPrompt) {
738
+ emit({ kind: "text", text: prompt });
739
+ return 0;
740
+ }
741
+ if (dryRun) {
742
+ emit({ kind: "info", message: "Dry run \u2014 would plan: " + workerCommand.join(" ") });
743
+ emit({ kind: "info", message: "Prompt length: " + prompt.length + " chars" });
744
+ return 0;
745
+ }
746
+ const artifactContext = dependencies.artifactStore.createContext({
747
+ cwd,
748
+ commandName: "plan",
749
+ workerCommand,
750
+ mode,
751
+ transport,
752
+ source,
753
+ task: {
754
+ text: task.text,
755
+ file: task.file,
756
+ line: task.line,
757
+ index: task.index,
758
+ source: fileSource
759
+ },
760
+ keepArtifacts
761
+ });
762
+ let artifactsFinalized = false;
763
+ let artifactStatus = "running";
764
+ const finishPlan = (code, status) => {
765
+ artifactStatus = status;
766
+ finalizePlanArtifacts(dependencies.artifactStore, artifactContext, keepArtifacts, artifactStatus, emit);
767
+ artifactsFinalized = true;
768
+ return code;
769
+ };
770
+ try {
771
+ emit({
772
+ kind: "info",
773
+ message: "Running planner: " + workerCommand.join(" ") + " [mode=" + mode + ", transport=" + transport + "]"
774
+ });
775
+ const runResult = await dependencies.workerExecutor.runWorker({
776
+ command: workerCommand,
777
+ prompt,
778
+ mode,
779
+ transport,
780
+ cwd,
781
+ artifactContext,
782
+ artifactPhase: "plan"
783
+ });
784
+ if (mode === "wait" && runResult.stderr) {
785
+ emit({ kind: "stderr", text: runResult.stderr });
786
+ }
787
+ if (runResult.exitCode !== 0 && runResult.exitCode !== null) {
788
+ emit({ kind: "error", message: "Planner worker exited with code " + runResult.exitCode + "." });
789
+ return finishPlan(1, "execution-failed");
790
+ }
791
+ if (!runResult.stdout || runResult.stdout.trim().length === 0) {
792
+ emit({ kind: "warn", message: "Planner produced no output. No subtasks created." });
793
+ return finishPlan(0, "completed");
794
+ }
795
+ const count = applyPlannerOutputWithFileSystem(task, runResult.stdout, dependencies.fileSystem);
796
+ if (count === 0) {
797
+ emit({ kind: "warn", message: "Planner output contained no valid task items. No subtasks created." });
798
+ return finishPlan(0, "completed");
799
+ }
800
+ emit({
801
+ kind: "success",
802
+ message: "Inserted " + count + " subtask" + (count === 1 ? "" : "s") + " under: " + task.text
803
+ });
804
+ return finishPlan(0, "completed");
805
+ } finally {
806
+ if (!artifactsFinalized) {
807
+ finalizePlanArtifacts(dependencies.artifactStore, artifactContext, keepArtifacts, artifactStatus, emit);
808
+ artifactsFinalized = true;
809
+ }
810
+ }
811
+ };
812
+ }
813
+ async function selectPlanTask(source, at, sortMode, dependencies, emit) {
814
+ if (at) {
815
+ const parsed = parseTaskLocation(at);
816
+ if (parsed.kind === "invalid-format") {
817
+ emit({ kind: "error", message: "Invalid --at format. Expected file:line (e.g. roadmap.md:12)." });
818
+ return { result: null, exitCode: 1 };
819
+ }
820
+ if (parsed.kind === "invalid-line") {
821
+ emit({ kind: "error", message: "Invalid line number in --at: " + parsed.lineRaw });
822
+ return { result: null, exitCode: 1 };
823
+ }
824
+ const { filePath, lineNum } = parsed;
825
+ const selected2 = dependencies.taskSelector.selectTaskByLocation(filePath, lineNum);
826
+ if (!selected2) {
827
+ emit({ kind: "error", message: "No task found at " + filePath + ":" + lineNum });
828
+ return { result: null, exitCode: 3 };
829
+ }
830
+ return { result: selected2, exitCode: 0 };
831
+ }
832
+ const files = await dependencies.sourceResolver.resolveSources(source);
833
+ if (files.length === 0) {
834
+ emit({ kind: "warn", message: "No Markdown files found matching: " + source });
835
+ return { result: null, exitCode: 3 };
836
+ }
837
+ const selected = dependencies.taskSelector.selectNextTask(files, sortMode);
838
+ if (!selected) {
839
+ emit({ kind: "info", message: "No unchecked tasks found." });
840
+ return { result: null, exitCode: 3 };
841
+ }
842
+ return { result: selected, exitCode: 0 };
843
+ }
844
+ function formatTaskLabel2(task) {
845
+ return `${task.file}:${task.line} [#${task.index}] ${task.text}`;
846
+ }
847
+ function parseTaskLocation(value) {
848
+ const colonIdx = value.lastIndexOf(":");
849
+ if (colonIdx === -1) {
850
+ return { kind: "invalid-format" };
851
+ }
852
+ const filePath = value.slice(0, colonIdx);
853
+ const lineRaw = value.slice(colonIdx + 1);
854
+ const lineNum = Number.parseInt(lineRaw, 10);
855
+ if (!Number.isFinite(lineNum) || lineNum < 1) {
856
+ return { kind: "invalid-line", lineRaw };
857
+ }
858
+ return { kind: "ok", filePath, lineNum };
859
+ }
860
+ function finalizePlanArtifacts(artifactStore, artifactContext, preserve, status, emit) {
861
+ artifactStore.finalize(artifactContext, {
862
+ status,
863
+ preserve
864
+ });
865
+ if (preserve) {
866
+ emit({
867
+ kind: "info",
868
+ message: "Runtime artifacts saved at " + artifactStore.displayPath(artifactContext) + "."
869
+ });
870
+ }
871
+ }
872
+ var TEMPLATE_VAR_KEY3 = /^[A-Za-z_]\w*$/;
873
+ function loadTemplateVarsFileFromPorts2(filePath, cwd, fileSystem) {
874
+ const resolvedPath = path2.resolve(cwd, filePath);
875
+ let parsed;
876
+ try {
877
+ parsed = JSON.parse(fileSystem.readText(resolvedPath));
878
+ } catch (error) {
879
+ throw new Error(`Failed to read template vars file "${filePath}": ${String(error)}`);
880
+ }
881
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
882
+ throw new Error(`Template vars file "${filePath}" must contain a JSON object.`);
883
+ }
884
+ const vars = {};
885
+ for (const [key, value] of Object.entries(parsed)) {
886
+ if (!TEMPLATE_VAR_KEY3.test(key)) {
887
+ throw new Error(`Invalid template variable name "${key}" in "${filePath}". Use letters, numbers, and underscores only.`);
888
+ }
889
+ if (value === null || value === void 0) {
890
+ vars[key] = "";
891
+ continue;
892
+ }
893
+ if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
894
+ vars[key] = String(value);
895
+ continue;
896
+ }
897
+ throw new Error(`Template variable "${key}" in "${filePath}" must be a string, number, boolean, or null.`);
898
+ }
899
+ return vars;
900
+ }
901
+ function loadPlanTemplateFromPorts(cwd, templateLoader) {
902
+ return templateLoader.load(path2.join(cwd, ".rundown", "plan.md")) ?? DEFAULT_PLAN_TEMPLATE;
903
+ }
904
+ function applyPlannerOutputWithFileSystem(task, plannerOutput, fileSystem) {
905
+ const subitemLines = parsePlannerOutput(plannerOutput);
906
+ if (subitemLines.length === 0) {
907
+ return 0;
908
+ }
909
+ const source = fileSystem.readText(task.file);
910
+ const updated = insertSubitems(source, task, subitemLines);
911
+ fileSystem.writeText(task.file, updated);
912
+ return subitemLines.length;
913
+ }
914
+
915
+ // src/domain/parser.ts
916
+ import { fromMarkdown } from "mdast-util-from-markdown";
917
+ import {
918
+ gfmTaskListItem
919
+ } from "micromark-extension-gfm-task-list-item";
920
+ import {
921
+ gfmTaskListItemFromMarkdown
922
+ } from "mdast-util-gfm-task-list-item";
923
+ var CLI_PREFIX = /^cli:\s*/i;
924
+ function parseTasks(source, file = "") {
925
+ const tree = fromMarkdown(source, {
926
+ extensions: [gfmTaskListItem()],
927
+ mdastExtensions: [gfmTaskListItemFromMarkdown()]
928
+ });
929
+ const tasks = [];
930
+ walkForTasks(tree, tasks, file, 0);
931
+ return tasks;
932
+ }
933
+ function walkForTasks(node, tasks, file, depth) {
934
+ if (isListItem(node) && node.checked !== null && node.checked !== void 0) {
935
+ const text = extractText(node);
936
+ const pos = node.position;
937
+ const isInlineCli = CLI_PREFIX.test(text);
938
+ const task = {
939
+ text,
940
+ checked: node.checked === true,
941
+ index: tasks.length,
942
+ line: pos?.start.line ?? 0,
943
+ column: pos?.start.column ?? 0,
944
+ offsetStart: pos?.start.offset ?? 0,
945
+ offsetEnd: pos?.end?.offset ?? 0,
946
+ file,
947
+ isInlineCli,
948
+ depth
949
+ };
950
+ if (isInlineCli) {
951
+ task.cliCommand = text.replace(CLI_PREFIX, "").trim();
952
+ }
953
+ tasks.push(task);
954
+ }
955
+ if ("children" in node) {
956
+ const nextDepth = isListItem(node) ? depth + 1 : depth;
957
+ for (const child of node.children) {
958
+ walkForTasks(child, tasks, file, nextDepth);
959
+ }
960
+ }
961
+ }
962
+ function isListItem(node) {
963
+ return node.type === "listItem";
964
+ }
965
+ function extractText(node) {
966
+ const parts = [];
967
+ for (const child of node.children) {
968
+ if (child.type === "paragraph") {
969
+ collectText(child, parts);
970
+ }
971
+ }
972
+ return parts.join("").trim();
973
+ }
974
+ function collectText(node, parts) {
975
+ if (node.type === "text" || node.type === "inlineCode") {
976
+ parts.push(node.value);
977
+ }
978
+ if ("children" in node) {
979
+ for (const child of node.children) {
980
+ collectText(child, parts);
981
+ }
982
+ }
983
+ }
984
+
985
+ // src/domain/task-selection.ts
986
+ function hasUncheckedDescendants(task, allTasks) {
987
+ const startIdx = allTasks.indexOf(task);
988
+ if (startIdx === -1) return false;
989
+ for (let i = startIdx + 1; i < allTasks.length; i++) {
990
+ const candidate = allTasks[i];
991
+ if (candidate.depth <= task.depth) break;
992
+ if (!candidate.checked) return true;
993
+ }
994
+ return false;
995
+ }
996
+ function filterRunnable(tasks) {
997
+ return tasks.filter((task) => !task.checked && !hasUncheckedDescendants(task, tasks));
998
+ }
999
+
1000
+ // src/domain/sorting.ts
1001
+ import path3 from "path";
1002
+ function sortFiles(files, mode = "name-sort", options = {}) {
1003
+ switch (mode) {
1004
+ case "none":
1005
+ return files;
1006
+ case "name-sort":
1007
+ return [...files].sort((a, b) => naturalCompare(path3.basename(a), path3.basename(b)));
1008
+ case "old-first":
1009
+ return [...files].sort((a, b) => getBirthtime(a, options) - getBirthtime(b, options));
1010
+ case "new-first":
1011
+ return [...files].sort((a, b) => getBirthtime(b, options) - getBirthtime(a, options));
1012
+ default:
1013
+ return files;
1014
+ }
1015
+ }
1016
+ function naturalCompare(a, b) {
1017
+ const ax = tokenize(a);
1018
+ const bx = tokenize(b);
1019
+ for (let i = 0; i < Math.max(ax.length, bx.length); i++) {
1020
+ const ai = ax[i];
1021
+ const bi = bx[i];
1022
+ if (ai === void 0) return -1;
1023
+ if (bi === void 0) return 1;
1024
+ const an = typeof ai === "number";
1025
+ const bn = typeof bi === "number";
1026
+ if (an && bn) {
1027
+ if (ai !== bi) return ai - bi;
1028
+ } else if (an) {
1029
+ return -1;
1030
+ } else if (bn) {
1031
+ return 1;
1032
+ } else {
1033
+ const cmp = ai.localeCompare(bi, void 0, { sensitivity: "base" });
1034
+ if (cmp !== 0) return cmp;
1035
+ }
1036
+ }
1037
+ return 0;
1038
+ }
1039
+ function tokenize(s) {
1040
+ const tokens = [];
1041
+ const re = /(\d+)|(\D+)/g;
1042
+ let m;
1043
+ while ((m = re.exec(s)) !== null) {
1044
+ if (m[1] !== void 0) {
1045
+ tokens.push(parseInt(m[1], 10));
1046
+ } else {
1047
+ tokens.push(m[2]);
1048
+ }
1049
+ }
1050
+ return tokens;
1051
+ }
1052
+ function getBirthtime(filePath, options) {
1053
+ if (!options.getBirthtimeMs) return 0;
1054
+ return options.getBirthtimeMs(filePath);
1055
+ }
1056
+
1057
+ // src/application/list-tasks.ts
1058
+ function createListTasks(dependencies) {
1059
+ const emit = dependencies.output.emit.bind(dependencies.output);
1060
+ return async function listTasks(options) {
1061
+ const { source, sortMode, includeAll } = options;
1062
+ const files = await dependencies.sourceResolver.resolveSources(source);
1063
+ if (files.length === 0) {
1064
+ emit({ kind: "warn", message: "No Markdown files found matching: " + source });
1065
+ return 3;
1066
+ }
1067
+ const sorted = sortFiles(files, sortMode, {
1068
+ getBirthtimeMs: (filePath) => {
1069
+ const stats = dependencies.fileSystem.stat(filePath);
1070
+ if (!stats) {
1071
+ throw new Error(`ENOENT: no such file or directory, stat '${filePath}'`);
1072
+ }
1073
+ if (stats.birthtimeMs !== void 0 && Number.isFinite(stats.birthtimeMs)) {
1074
+ return stats.birthtimeMs;
1075
+ }
1076
+ if (stats.mtimeMs !== void 0 && Number.isFinite(stats.mtimeMs)) {
1077
+ return stats.mtimeMs;
1078
+ }
1079
+ throw new Error(`birthtime unavailable for '${filePath}'`);
1080
+ }
1081
+ });
1082
+ let count = 0;
1083
+ for (const file of sorted) {
1084
+ const content = dependencies.fileSystem.readText(file);
1085
+ const tasks = parseTasks(content, file);
1086
+ const filtered = includeAll ? tasks : tasks.filter((task) => !task.checked);
1087
+ for (const task of filtered) {
1088
+ const blocked = !task.checked && hasUncheckedDescendants(task, tasks);
1089
+ emit({ kind: "task", task, blocked });
1090
+ count++;
1091
+ }
1092
+ }
1093
+ if (count === 0) {
1094
+ emit({ kind: "info", message: "No tasks found." });
1095
+ }
1096
+ return 0;
1097
+ };
1098
+ }
1099
+
1100
+ // src/application/next-task.ts
1101
+ function createNextTask(dependencies) {
1102
+ const emit = dependencies.output.emit.bind(dependencies.output);
1103
+ return async function nextTask(options) {
1104
+ const { source, sortMode } = options;
1105
+ const files = await dependencies.sourceResolver.resolveSources(source);
1106
+ if (files.length === 0) {
1107
+ emit({ kind: "warn", message: "No Markdown files found matching: " + source });
1108
+ return 3;
1109
+ }
1110
+ const result = dependencies.taskSelector.selectNextTask(files, sortMode);
1111
+ if (!result) {
1112
+ emit({ kind: "info", message: "No unchecked tasks found." });
1113
+ return 3;
1114
+ }
1115
+ emit({ kind: "task", task: result.task });
1116
+ return 0;
1117
+ };
1118
+ }
1119
+
1120
+ // src/application/init-project.ts
1121
+ var CONFIG_DIR = ".rundown";
1122
+ function createInitProject(dependencies) {
1123
+ const emit = dependencies.output.emit.bind(dependencies.output);
1124
+ return async function initProject() {
1125
+ if (!dependencies.fileSystem.exists(CONFIG_DIR)) {
1126
+ dependencies.fileSystem.mkdir(CONFIG_DIR, { recursive: true });
1127
+ }
1128
+ const write = (name, content) => {
1129
+ const filePath = `${CONFIG_DIR}/${name}`;
1130
+ if (dependencies.fileSystem.exists(filePath)) {
1131
+ emit({ kind: "warn", message: `${filePath} already exists, skipping.` });
1132
+ return;
1133
+ }
1134
+ dependencies.fileSystem.writeText(filePath, content);
1135
+ emit({ kind: "success", message: `Created ${filePath}` });
1136
+ };
1137
+ write("execute.md", DEFAULT_TASK_TEMPLATE);
1138
+ write("verify.md", DEFAULT_VALIDATE_TEMPLATE);
1139
+ write("repair.md", DEFAULT_CORRECT_TEMPLATE);
1140
+ write("plan.md", DEFAULT_PLAN_TEMPLATE);
1141
+ write("vars.json", DEFAULT_VARS_FILE_CONTENT);
1142
+ emit({ kind: "success", message: "Initialized .rundown/ with default templates." });
1143
+ return 0;
1144
+ };
1145
+ }
1146
+
1147
+ // src/application/manage-artifacts.ts
1148
+ function createManageArtifacts(dependencies) {
1149
+ const emit = dependencies.output.emit.bind(dependencies.output);
1150
+ return function manageArtifacts(options) {
1151
+ const shouldClean = options.clean;
1152
+ const shouldPrintJson = options.json;
1153
+ const onlyFailed = options.failed;
1154
+ const runToOpen = options.open;
1155
+ const cwd = options.cwd ?? dependencies.workingDirectory.cwd();
1156
+ if (shouldClean && (shouldPrintJson || runToOpen)) {
1157
+ emit({ kind: "error", message: "--clean cannot be combined with --json or --open." });
1158
+ return 1;
1159
+ }
1160
+ if (runToOpen && (shouldPrintJson || onlyFailed)) {
1161
+ emit({ kind: "error", message: "--open cannot be combined with --json or --failed." });
1162
+ return 1;
1163
+ }
1164
+ if (runToOpen) {
1165
+ const run = runToOpen === "latest" ? dependencies.artifactStore.latest(cwd) : dependencies.artifactStore.find(runToOpen, cwd);
1166
+ if (!run) {
1167
+ emit({ kind: "error", message: "No saved runtime artifact run found for: " + runToOpen });
1168
+ return 3;
1169
+ }
1170
+ dependencies.directoryOpener.openDirectory(run.rootDir);
1171
+ emit({ kind: "success", message: "Opened runtime artifacts: " + run.relativePath });
1172
+ return 0;
1173
+ }
1174
+ if (shouldClean) {
1175
+ const removed = onlyFailed ? dependencies.artifactStore.removeFailed(cwd) : dependencies.artifactStore.removeSaved(cwd);
1176
+ if (removed === 0) {
1177
+ emit({ kind: "info", message: onlyFailed ? "No failed runtime artifacts found." : "No saved runtime artifacts found." });
1178
+ return 0;
1179
+ }
1180
+ emit({
1181
+ kind: "success",
1182
+ message: "Removed " + removed + " " + (onlyFailed ? "failed " : "") + "runtime artifact run" + (removed === 1 ? "" : "s") + "."
1183
+ });
1184
+ return 0;
1185
+ }
1186
+ const runs = onlyFailed ? dependencies.artifactStore.listFailed(cwd) : dependencies.artifactStore.listSaved(cwd);
1187
+ if (runs.length === 0) {
1188
+ emit({ kind: "info", message: onlyFailed ? "No failed runtime artifacts found." : "No saved runtime artifacts found." });
1189
+ return 0;
1190
+ }
1191
+ if (shouldPrintJson) {
1192
+ emit({ kind: "text", text: JSON.stringify(runs, null, 2) });
1193
+ return 0;
1194
+ }
1195
+ for (const run of runs) {
1196
+ const worker = run.workerCommand?.join(" ") ?? run.commandName;
1197
+ const summary = [
1198
+ run.runId,
1199
+ `[status=${run.status ?? "unknown"}]`,
1200
+ `[command=${run.commandName}]`,
1201
+ `[mode=${run.mode ?? "n/a"}]`,
1202
+ `[transport=${run.transport ?? "n/a"}]`,
1203
+ run.relativePath
1204
+ ].join(" ");
1205
+ emit({ kind: "text", text: summary });
1206
+ if (run.task) {
1207
+ emit({ kind: "text", text: " task: " + run.task.text + " \u2014 " + run.task.file + ":" + run.task.line });
1208
+ }
1209
+ emit({ kind: "text", text: " worker: " + worker });
1210
+ emit({ kind: "text", text: " started: " + run.startedAt });
1211
+ }
1212
+ return 0;
1213
+ };
1214
+ }
1215
+
1216
+ // src/infrastructure/adapters/fs-file-system.ts
1217
+ import fs from "fs";
1218
+ function createNodeFileSystem() {
1219
+ return {
1220
+ exists(filePath) {
1221
+ return fs.existsSync(filePath);
1222
+ },
1223
+ readText(filePath) {
1224
+ return fs.readFileSync(filePath, "utf-8");
1225
+ },
1226
+ writeText(filePath, content) {
1227
+ fs.writeFileSync(filePath, content, "utf-8");
1228
+ },
1229
+ mkdir(dirPath, options) {
1230
+ fs.mkdirSync(dirPath, options);
1231
+ },
1232
+ readdir(dirPath) {
1233
+ const entries = fs.readdirSync(dirPath, { withFileTypes: true });
1234
+ return entries.map((entry) => ({
1235
+ name: entry.name,
1236
+ isFile: entry.isFile(),
1237
+ isDirectory: entry.isDirectory()
1238
+ }));
1239
+ },
1240
+ stat(filePath) {
1241
+ try {
1242
+ const stats = fs.statSync(filePath);
1243
+ const value = {
1244
+ isFile: stats.isFile(),
1245
+ isDirectory: stats.isDirectory(),
1246
+ birthtimeMs: stats.birthtimeMs,
1247
+ mtimeMs: stats.mtimeMs
1248
+ };
1249
+ return value;
1250
+ } catch {
1251
+ return null;
1252
+ }
1253
+ },
1254
+ unlink(filePath) {
1255
+ fs.unlinkSync(filePath);
1256
+ },
1257
+ rm(filePath, options) {
1258
+ fs.rmSync(filePath, options);
1259
+ }
1260
+ };
1261
+ }
1262
+
1263
+ // src/infrastructure/adapters/crossspawn-process-runner.ts
1264
+ import spawn from "cross-spawn";
1265
+ function createCrossSpawnProcessRunner() {
1266
+ return {
1267
+ run(options) {
1268
+ return runWithCrossSpawn(options);
1269
+ }
1270
+ };
1271
+ }
1272
+ function runWithCrossSpawn(options) {
1273
+ return new Promise((resolve, reject) => {
1274
+ const { command, args, cwd, mode, shell, env, timeoutMs } = options;
1275
+ if (mode === "detached") {
1276
+ const child2 = spawn(command, args, {
1277
+ cwd,
1278
+ shell: shell ?? false,
1279
+ env,
1280
+ stdio: "ignore",
1281
+ detached: true
1282
+ });
1283
+ child2.on("error", reject);
1284
+ child2.unref();
1285
+ resolve({ exitCode: null, stdout: "", stderr: "" });
1286
+ return;
1287
+ }
1288
+ if (mode === "tui") {
1289
+ const child2 = spawn(command, args, {
1290
+ cwd,
1291
+ shell: shell ?? false,
1292
+ env,
1293
+ stdio: "inherit"
1294
+ });
1295
+ child2.on("close", (exitCode) => {
1296
+ resolve({ exitCode, stdout: "", stderr: "" });
1297
+ });
1298
+ child2.on("error", reject);
1299
+ return;
1300
+ }
1301
+ const child = spawn(command, args, {
1302
+ cwd,
1303
+ shell: shell ?? false,
1304
+ env,
1305
+ stdio: ["inherit", "pipe", "pipe"]
1306
+ });
1307
+ const stdout = [];
1308
+ const stderr = [];
1309
+ let timeoutHandle = null;
1310
+ if (typeof timeoutMs === "number" && timeoutMs > 0) {
1311
+ timeoutHandle = setTimeout(() => {
1312
+ child.kill("SIGTERM");
1313
+ }, timeoutMs);
1314
+ }
1315
+ child.stdout?.on("data", (chunk) => stdout.push(chunk));
1316
+ child.stderr?.on("data", (chunk) => stderr.push(chunk));
1317
+ child.on("error", reject);
1318
+ child.on("close", (exitCode) => {
1319
+ if (timeoutHandle) {
1320
+ clearTimeout(timeoutHandle);
1321
+ }
1322
+ resolve({
1323
+ exitCode,
1324
+ stdout: Buffer.concat(stdout).toString("utf-8"),
1325
+ stderr: Buffer.concat(stderr).toString("utf-8")
1326
+ });
1327
+ });
1328
+ });
1329
+ }
1330
+
1331
+ // src/infrastructure/adapters/execfile-git-client.ts
1332
+ import { execFile } from "child_process";
1333
+ function createExecFileGitClient() {
1334
+ return {
1335
+ run(args, cwd, options) {
1336
+ return runGit(args, cwd, options?.timeoutMs);
1337
+ }
1338
+ };
1339
+ }
1340
+ function runGit(args, cwd, timeoutMs = 3e4) {
1341
+ return new Promise((resolve, reject) => {
1342
+ execFile("git", args, { cwd, timeout: timeoutMs }, (error, stdout, stderr) => {
1343
+ if (error) {
1344
+ const message = stderr?.trim() || stdout?.trim() || error.message;
1345
+ reject(new Error(`git ${args[0]}: ${message}`));
1346
+ return;
1347
+ }
1348
+ resolve(stdout.trim());
1349
+ });
1350
+ });
1351
+ }
1352
+
1353
+ // src/infrastructure/adapters/fs-template-loader.ts
1354
+ import fs2 from "fs";
1355
+ function createFsTemplateLoader() {
1356
+ return {
1357
+ load(filePath) {
1358
+ try {
1359
+ return fs2.readFileSync(filePath, "utf-8");
1360
+ } catch {
1361
+ return null;
1362
+ }
1363
+ }
1364
+ };
1365
+ }
1366
+
1367
+ // src/infrastructure/adapters/fs-validation-sidecar.ts
1368
+ import fs3 from "fs";
1369
+ function createFsValidationSidecar() {
1370
+ return {
1371
+ filePath(task) {
1372
+ return validationFilePath(task);
1373
+ },
1374
+ read(task) {
1375
+ const filePath = validationFilePath(task);
1376
+ try {
1377
+ return fs3.readFileSync(filePath, "utf-8").trim();
1378
+ } catch {
1379
+ return null;
1380
+ }
1381
+ },
1382
+ remove(task) {
1383
+ const filePath = validationFilePath(task);
1384
+ try {
1385
+ fs3.unlinkSync(filePath);
1386
+ } catch {
1387
+ }
1388
+ }
1389
+ };
1390
+ }
1391
+ function validationFilePath(task) {
1392
+ return `${task.file}.${task.index}.validation`;
1393
+ }
1394
+
1395
+ // src/infrastructure/runtime-artifacts.ts
1396
+ import fs4 from "fs";
1397
+ import path4 from "path";
1398
+ import { randomBytes } from "crypto";
1399
+ function createRuntimeArtifactsContext(options) {
1400
+ const cwd = options.cwd ?? process.cwd();
1401
+ const rootBase = path4.join(cwd, ".rundown", "runs");
1402
+ fs4.mkdirSync(rootBase, { recursive: true });
1403
+ const runId = buildRunId();
1404
+ const rootDir = path4.join(rootBase, runId);
1405
+ fs4.mkdirSync(rootDir, { recursive: true });
1406
+ const context = {
1407
+ runId,
1408
+ rootDir,
1409
+ cwd,
1410
+ keepArtifacts: options.keepArtifacts ?? false,
1411
+ commandName: options.commandName,
1412
+ workerCommand: options.workerCommand,
1413
+ mode: options.mode,
1414
+ transport: options.transport,
1415
+ task: options.task,
1416
+ sequence: 0
1417
+ };
1418
+ const metadata = {
1419
+ runId,
1420
+ commandName: options.commandName,
1421
+ workerCommand: options.workerCommand,
1422
+ mode: options.mode,
1423
+ transport: options.transport,
1424
+ source: options.source,
1425
+ task: options.task,
1426
+ keepArtifacts: context.keepArtifacts,
1427
+ startedAt: (/* @__PURE__ */ new Date()).toISOString()
1428
+ };
1429
+ writeJson(path4.join(rootDir, "run.json"), metadata);
1430
+ return context;
1431
+ }
1432
+ function runtimeArtifactsRootDir(cwd = process.cwd()) {
1433
+ return path4.join(cwd, ".rundown", "runs");
1434
+ }
1435
+ function listSavedRuntimeArtifacts(cwd = process.cwd()) {
1436
+ const rootDir = runtimeArtifactsRootDir(cwd);
1437
+ if (!fs4.existsSync(rootDir)) {
1438
+ return [];
1439
+ }
1440
+ const runs = [];
1441
+ for (const entry of fs4.readdirSync(rootDir, { withFileTypes: true })) {
1442
+ if (!entry.isDirectory()) {
1443
+ continue;
1444
+ }
1445
+ const runDir = path4.join(rootDir, entry.name);
1446
+ const metadata = readJson(path4.join(runDir, "run.json"));
1447
+ if (!metadata) {
1448
+ continue;
1449
+ }
1450
+ runs.push({
1451
+ runId: metadata.runId,
1452
+ rootDir: runDir,
1453
+ relativePath: path4.relative(cwd, runDir).split(path4.sep).join("/"),
1454
+ commandName: metadata.commandName,
1455
+ workerCommand: metadata.workerCommand,
1456
+ mode: metadata.mode,
1457
+ transport: metadata.transport,
1458
+ source: metadata.source,
1459
+ task: metadata.task,
1460
+ keepArtifacts: metadata.keepArtifacts,
1461
+ startedAt: metadata.startedAt,
1462
+ completedAt: metadata.completedAt,
1463
+ status: metadata.status
1464
+ });
1465
+ }
1466
+ runs.sort((a, b) => b.startedAt.localeCompare(a.startedAt));
1467
+ return runs;
1468
+ }
1469
+ function listFailedRuntimeArtifacts(cwd = process.cwd()) {
1470
+ return listSavedRuntimeArtifacts(cwd).filter((run) => isFailedRuntimeArtifactStatus(run.status));
1471
+ }
1472
+ function latestSavedRuntimeArtifact(cwd = process.cwd()) {
1473
+ return listSavedRuntimeArtifacts(cwd)[0] ?? null;
1474
+ }
1475
+ function findSavedRuntimeArtifact(runId, cwd = process.cwd()) {
1476
+ const runs = listSavedRuntimeArtifacts(cwd);
1477
+ const exact = runs.find((run) => run.runId === runId);
1478
+ if (exact) {
1479
+ return exact;
1480
+ }
1481
+ const prefixMatches = runs.filter((run) => run.runId.startsWith(runId));
1482
+ if (prefixMatches.length === 1) {
1483
+ return prefixMatches[0] ?? null;
1484
+ }
1485
+ return null;
1486
+ }
1487
+ function removeSavedRuntimeArtifacts(cwd = process.cwd()) {
1488
+ return removeRuntimeArtifactsMatching(() => true, cwd);
1489
+ }
1490
+ function removeFailedRuntimeArtifacts(cwd = process.cwd()) {
1491
+ return removeRuntimeArtifactsMatching((run) => isFailedRuntimeArtifactStatus(run.status), cwd);
1492
+ }
1493
+ function removeRuntimeArtifactsMatching(predicate, cwd) {
1494
+ const rootDir = runtimeArtifactsRootDir(cwd);
1495
+ if (!fs4.existsSync(rootDir)) {
1496
+ return 0;
1497
+ }
1498
+ const runs = listSavedRuntimeArtifacts(cwd);
1499
+ let removed = 0;
1500
+ for (const run of runs) {
1501
+ if (!predicate(run)) {
1502
+ continue;
1503
+ }
1504
+ fs4.rmSync(run.rootDir, { recursive: true, force: true });
1505
+ removed += 1;
1506
+ }
1507
+ return removed;
1508
+ }
1509
+ function isFailedRuntimeArtifactStatus(status) {
1510
+ if (!status) {
1511
+ return false;
1512
+ }
1513
+ return status.includes("failed");
1514
+ }
1515
+ function beginRuntimePhase(context, options) {
1516
+ context.sequence += 1;
1517
+ const sequence = context.sequence;
1518
+ const dirName = `${String(sequence).padStart(2, "0")}-${options.phase}`;
1519
+ const dir = path4.join(context.rootDir, dirName);
1520
+ fs4.mkdirSync(dir, { recursive: true });
1521
+ const promptFile = options.prompt === void 0 ? null : path4.join(dir, "prompt.md");
1522
+ if (promptFile) {
1523
+ fs4.writeFileSync(promptFile, options.prompt ?? "", "utf-8");
1524
+ }
1525
+ const metadata = {
1526
+ runId: context.runId,
1527
+ sequence,
1528
+ phase: options.phase,
1529
+ command: options.command,
1530
+ mode: options.mode,
1531
+ transport: options.transport,
1532
+ task: context.task,
1533
+ promptFile: promptFile ? "prompt.md" : null,
1534
+ stdoutFile: null,
1535
+ stderrFile: null,
1536
+ outputCaptured: false,
1537
+ startedAt: (/* @__PURE__ */ new Date()).toISOString(),
1538
+ notes: options.notes,
1539
+ extra: options.extra
1540
+ };
1541
+ const metadataFile = path4.join(dir, "metadata.json");
1542
+ writeJson(metadataFile, metadata);
1543
+ return {
1544
+ context,
1545
+ phase: options.phase,
1546
+ sequence,
1547
+ dir,
1548
+ promptFile,
1549
+ metadataFile,
1550
+ metadata
1551
+ };
1552
+ }
1553
+ function completeRuntimePhase(handle, options) {
1554
+ handle.metadata.exitCode = options.exitCode;
1555
+ handle.metadata.outputCaptured = options.outputCaptured;
1556
+ handle.metadata.completedAt = (/* @__PURE__ */ new Date()).toISOString();
1557
+ if (options.stdout !== void 0 && options.stdout.length > 0) {
1558
+ fs4.writeFileSync(path4.join(handle.dir, "stdout.log"), options.stdout, "utf-8");
1559
+ handle.metadata.stdoutFile = "stdout.log";
1560
+ }
1561
+ if (options.stderr !== void 0 && options.stderr.length > 0) {
1562
+ fs4.writeFileSync(path4.join(handle.dir, "stderr.log"), options.stderr, "utf-8");
1563
+ handle.metadata.stderrFile = "stderr.log";
1564
+ }
1565
+ if (options.notes !== void 0) {
1566
+ handle.metadata.notes = options.notes;
1567
+ }
1568
+ if (options.extra !== void 0) {
1569
+ handle.metadata.extra = {
1570
+ ...handle.metadata.extra ?? {},
1571
+ ...options.extra
1572
+ };
1573
+ }
1574
+ writeJson(handle.metadataFile, handle.metadata);
1575
+ }
1576
+ function finalizeRuntimeArtifacts(context, options) {
1577
+ const metadataFile = path4.join(context.rootDir, "run.json");
1578
+ const metadata = readJson(metadataFile) ?? {
1579
+ runId: context.runId,
1580
+ commandName: context.commandName,
1581
+ workerCommand: context.workerCommand,
1582
+ mode: context.mode,
1583
+ transport: context.transport,
1584
+ task: context.task,
1585
+ keepArtifacts: context.keepArtifacts,
1586
+ startedAt: (/* @__PURE__ */ new Date()).toISOString()
1587
+ };
1588
+ metadata.completedAt = (/* @__PURE__ */ new Date()).toISOString();
1589
+ metadata.status = options.status;
1590
+ writeJson(metadataFile, metadata);
1591
+ if (!options.preserve) {
1592
+ fs4.rmSync(context.rootDir, { recursive: true, force: true });
1593
+ }
1594
+ }
1595
+ function displayArtifactsPath(context) {
1596
+ const relative = path4.relative(context.cwd, context.rootDir);
1597
+ return relative === "" ? path4.basename(context.rootDir) : relative.split(path4.sep).join("/");
1598
+ }
1599
+ function buildRunId() {
1600
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[-:.]/g, "").replace("Z", "Z");
1601
+ const suffix = randomBytes(4).toString("hex");
1602
+ return `run-${timestamp}-${suffix}`;
1603
+ }
1604
+ function writeJson(filePath, value) {
1605
+ fs4.writeFileSync(filePath, `${JSON.stringify(value, null, 2)}
1606
+ `, "utf-8");
1607
+ }
1608
+ function readJson(filePath) {
1609
+ try {
1610
+ return JSON.parse(fs4.readFileSync(filePath, "utf-8"));
1611
+ } catch {
1612
+ return null;
1613
+ }
1614
+ }
1615
+
1616
+ // src/infrastructure/adapters/fs-artifact-store.ts
1617
+ var toRuntimePhase = (phase) => phase;
1618
+ function createFsArtifactStore() {
1619
+ return {
1620
+ createContext(options) {
1621
+ return createRuntimeArtifactsContext(options);
1622
+ },
1623
+ beginPhase(context, options) {
1624
+ const runtimeOptions = {
1625
+ ...options,
1626
+ phase: toRuntimePhase(options.phase)
1627
+ };
1628
+ return beginRuntimePhase(context, runtimeOptions);
1629
+ },
1630
+ completePhase(handle, options) {
1631
+ const runtimeOptions = options;
1632
+ completeRuntimePhase(handle, runtimeOptions);
1633
+ },
1634
+ finalize(context, options) {
1635
+ finalizeRuntimeArtifacts(context, {
1636
+ status: options.status,
1637
+ preserve: options.preserve
1638
+ });
1639
+ },
1640
+ displayPath(context) {
1641
+ return displayArtifactsPath(context);
1642
+ },
1643
+ rootDir(cwd) {
1644
+ return runtimeArtifactsRootDir(cwd);
1645
+ },
1646
+ listSaved(cwd) {
1647
+ return listSavedRuntimeArtifacts(cwd);
1648
+ },
1649
+ listFailed(cwd) {
1650
+ return listFailedRuntimeArtifacts(cwd);
1651
+ },
1652
+ latest(cwd) {
1653
+ return latestSavedRuntimeArtifact(cwd);
1654
+ },
1655
+ find(runId, cwd) {
1656
+ return findSavedRuntimeArtifact(runId, cwd);
1657
+ },
1658
+ removeSaved(cwd) {
1659
+ return removeSavedRuntimeArtifacts(cwd);
1660
+ },
1661
+ removeFailed(cwd) {
1662
+ return removeFailedRuntimeArtifacts(cwd);
1663
+ },
1664
+ isFailedStatus(status) {
1665
+ return isFailedRuntimeArtifactStatus(status);
1666
+ }
1667
+ };
1668
+ }
1669
+
1670
+ // src/infrastructure/adapters/system-clock.ts
1671
+ function createSystemClock() {
1672
+ return {
1673
+ now() {
1674
+ return /* @__PURE__ */ new Date();
1675
+ },
1676
+ nowIsoString() {
1677
+ return (/* @__PURE__ */ new Date()).toISOString();
1678
+ }
1679
+ };
1680
+ }
1681
+
1682
+ // src/infrastructure/sources.ts
1683
+ import fs5 from "fs";
1684
+ import path5 from "path";
1685
+ import fg from "fast-glob";
1686
+ async function resolveSources(source) {
1687
+ const resolved = path5.resolve(source);
1688
+ if (isFile(resolved)) {
1689
+ return [resolved];
1690
+ }
1691
+ if (isDirectory(resolved)) {
1692
+ const pattern = path5.join(resolved, "**/*.md").replace(/\\/g, "/");
1693
+ return await fg(pattern, { absolute: true, onlyFiles: true });
1694
+ }
1695
+ const files = await fg(source.replace(/\\/g, "/"), {
1696
+ absolute: true,
1697
+ onlyFiles: true
1698
+ });
1699
+ return files.filter((f) => f.endsWith(".md"));
1700
+ }
1701
+ function isFile(p) {
1702
+ try {
1703
+ return fs5.statSync(p).isFile();
1704
+ } catch {
1705
+ return false;
1706
+ }
1707
+ }
1708
+ function isDirectory(p) {
1709
+ try {
1710
+ return fs5.statSync(p).isDirectory();
1711
+ } catch {
1712
+ return false;
1713
+ }
1714
+ }
1715
+
1716
+ // src/infrastructure/adapters/source-resolver-adapter.ts
1717
+ function createSourceResolverAdapter() {
1718
+ return {
1719
+ resolveSources(source) {
1720
+ return resolveSources(source);
1721
+ }
1722
+ };
1723
+ }
1724
+
1725
+ // src/infrastructure/selector.ts
1726
+ import fs6 from "fs";
1727
+
1728
+ // src/infrastructure/file-birthtime.ts
1729
+ function getFileBirthtimeMs(filePath, fileSystem) {
1730
+ try {
1731
+ return fileSystem.stat(filePath)?.birthtimeMs ?? 0;
1732
+ } catch {
1733
+ return 0;
1734
+ }
1735
+ }
1736
+
1737
+ // src/infrastructure/selector.ts
1738
+ var selectorFileSystem = {
1739
+ stat(filePath) {
1740
+ const stats = fs6.statSync(filePath);
1741
+ return {
1742
+ isFile: stats.isFile(),
1743
+ isDirectory: stats.isDirectory(),
1744
+ birthtimeMs: stats.birthtimeMs,
1745
+ mtimeMs: stats.mtimeMs
1746
+ };
1747
+ }
1748
+ };
1749
+ function selectNextTask(files, sortMode = "name-sort") {
1750
+ const sorted = sortFiles(files, sortMode, {
1751
+ getBirthtimeMs: (filePath) => getFileBirthtimeMs(filePath, selectorFileSystem)
1752
+ });
1753
+ for (const file of sorted) {
1754
+ const source = fs6.readFileSync(file, "utf-8");
1755
+ const tasks = parseTasks(source, file);
1756
+ const runnable = filterRunnable(tasks);
1757
+ for (const task of runnable) {
1758
+ const lines = source.split("\n");
1759
+ const contextBefore = lines.slice(0, task.line - 1).join("\n");
1760
+ return { task, source, contextBefore };
1761
+ }
1762
+ }
1763
+ return null;
1764
+ }
1765
+ function selectTaskByLocation(file, line) {
1766
+ const source = fs6.readFileSync(file, "utf-8");
1767
+ const tasks = parseTasks(source, file);
1768
+ const task = tasks.find((t) => t.line === line);
1769
+ if (!task) {
1770
+ return null;
1771
+ }
1772
+ const lines = source.split("\n");
1773
+ const contextBefore = lines.slice(0, task.line - 1).join("\n");
1774
+ return { task, source, contextBefore };
1775
+ }
1776
+
1777
+ // src/infrastructure/adapters/task-selector-adapter.ts
1778
+ function createTaskSelectorAdapter() {
1779
+ return {
1780
+ selectNextTask(files, sortMode) {
1781
+ return selectNextTask(files, sortMode);
1782
+ },
1783
+ selectTaskByLocation(filePath, line) {
1784
+ return selectTaskByLocation(filePath, line);
1785
+ }
1786
+ };
1787
+ }
1788
+
1789
+ // src/infrastructure/inline-cli.ts
1790
+ import { spawn as spawn2 } from "child_process";
1791
+ async function executeInlineCli(command, cwd = process.cwd(), options) {
1792
+ let ownedArtifactContext = null;
1793
+ let artifactContext;
1794
+ if (options?.artifactContext) {
1795
+ artifactContext = options.artifactContext;
1796
+ } else {
1797
+ ownedArtifactContext = createRuntimeArtifactsContext({
1798
+ cwd,
1799
+ commandName: "inline-cli",
1800
+ workerCommand: [command],
1801
+ mode: "wait",
1802
+ transport: "inline-cli",
1803
+ keepArtifacts: options?.keepArtifacts ?? false
1804
+ });
1805
+ artifactContext = ownedArtifactContext;
1806
+ }
1807
+ const phase = beginRuntimePhase(artifactContext, {
1808
+ phase: "inline-cli",
1809
+ command: [command],
1810
+ mode: "wait",
1811
+ transport: "inline-cli",
1812
+ extra: options?.artifactExtra
1813
+ });
1814
+ return new Promise((resolve, reject) => {
1815
+ const child = spawn2(command, {
1816
+ stdio: ["inherit", "pipe", "pipe"],
1817
+ cwd,
1818
+ shell: true
1819
+ });
1820
+ const stdout = [];
1821
+ const stderr = [];
1822
+ child.stdout?.on("data", (chunk) => stdout.push(chunk));
1823
+ child.stderr?.on("data", (chunk) => stderr.push(chunk));
1824
+ child.on("close", (code) => {
1825
+ const result = {
1826
+ exitCode: code,
1827
+ stdout: Buffer.concat(stdout).toString("utf-8"),
1828
+ stderr: Buffer.concat(stderr).toString("utf-8")
1829
+ };
1830
+ completeRuntimePhase(phase, {
1831
+ exitCode: code,
1832
+ stdout: result.stdout,
1833
+ stderr: result.stderr,
1834
+ outputCaptured: true
1835
+ });
1836
+ if (ownedArtifactContext) {
1837
+ finalizeRuntimeArtifacts(ownedArtifactContext, {
1838
+ status: code === 0 ? "completed" : "failed",
1839
+ preserve: options?.keepArtifacts ?? false
1840
+ });
1841
+ }
1842
+ resolve(result);
1843
+ });
1844
+ child.on("error", (error) => {
1845
+ completeRuntimePhase(phase, {
1846
+ exitCode: null,
1847
+ outputCaptured: true,
1848
+ notes: error.message,
1849
+ extra: { error: true }
1850
+ });
1851
+ if (ownedArtifactContext) {
1852
+ finalizeRuntimeArtifacts(ownedArtifactContext, {
1853
+ status: "failed",
1854
+ preserve: options?.keepArtifacts ?? false
1855
+ });
1856
+ }
1857
+ reject(error);
1858
+ });
1859
+ });
1860
+ }
1861
+
1862
+ // src/infrastructure/runner.ts
1863
+ import os from "os";
1864
+ import path6 from "path";
1865
+ import spawn3 from "cross-spawn";
1866
+ async function runWorker(options) {
1867
+ const mode = options.mode ?? "wait";
1868
+ const transport = options.transport ?? "file";
1869
+ const cwd = options.cwd ?? process.cwd();
1870
+ let ownedArtifactContext = null;
1871
+ let artifactContext;
1872
+ if (options.artifactContext) {
1873
+ artifactContext = options.artifactContext;
1874
+ } else {
1875
+ ownedArtifactContext = createRuntimeArtifactsContext({
1876
+ cwd,
1877
+ commandName: "worker",
1878
+ workerCommand: options.command,
1879
+ mode,
1880
+ transport,
1881
+ keepArtifacts: options.keepArtifacts ?? false
1882
+ });
1883
+ artifactContext = ownedArtifactContext;
1884
+ }
1885
+ const phase = beginRuntimePhase(artifactContext, {
1886
+ phase: options.artifactPhase ?? "worker",
1887
+ prompt: options.prompt,
1888
+ command: options.command,
1889
+ mode,
1890
+ transport,
1891
+ notes: buildCaptureNotes(mode),
1892
+ extra: options.artifactExtra
1893
+ });
1894
+ const transportPromptFile = transport === "file" ? phase.promptFile : null;
1895
+ const args = buildWorkerArgs(options.command, options.prompt, transport, transportPromptFile, cwd);
1896
+ const [cmd, ...cmdArgs] = args;
1897
+ if (!cmd) {
1898
+ throw new Error("No command specified after --");
1899
+ }
1900
+ try {
1901
+ const result = await executeCommand(cmd, cmdArgs, mode, cwd);
1902
+ completeRuntimePhase(phase, {
1903
+ exitCode: result.exitCode,
1904
+ stdout: result.stdout,
1905
+ stderr: result.stderr,
1906
+ outputCaptured: mode === "wait"
1907
+ });
1908
+ return result;
1909
+ } catch (error) {
1910
+ completeRuntimePhase(phase, {
1911
+ exitCode: null,
1912
+ outputCaptured: mode === "wait",
1913
+ notes: error instanceof Error ? error.message : String(error),
1914
+ extra: { error: true }
1915
+ });
1916
+ throw error;
1917
+ } finally {
1918
+ if (ownedArtifactContext) {
1919
+ finalizeRuntimeArtifacts(ownedArtifactContext, {
1920
+ status: mode === "detached" ? "detached" : "completed",
1921
+ preserve: (options.keepArtifacts ?? false) || mode === "detached"
1922
+ });
1923
+ }
1924
+ }
1925
+ }
1926
+ function buildWorkerArgs(command, prompt, transport, promptFile, cwd) {
1927
+ if (command.length === 0) {
1928
+ return [];
1929
+ }
1930
+ if (isOpenCodeCommand(command[0])) {
1931
+ return buildOpenCodeArgs(command, prompt, promptFile, cwd);
1932
+ }
1933
+ const args = [...command];
1934
+ if (transport === "file") {
1935
+ if (!promptFile) {
1936
+ throw new Error("Prompt file transport requested but no prompt file was created.");
1937
+ }
1938
+ args.push(promptFile);
1939
+ } else {
1940
+ args.push(prompt);
1941
+ }
1942
+ return args;
1943
+ }
1944
+ function isOpenCodeCommand(command) {
1945
+ const normalized = path6.basename(command).toLowerCase();
1946
+ return normalized === "opencode" || normalized === "opencode.cmd" || normalized === "opencode.exe" || normalized === "opencode.ps1";
1947
+ }
1948
+ function buildOpenCodeArgs(command, prompt, promptFile, cwd) {
1949
+ const [cmd, ...rest] = command;
1950
+ if (rest[0] === "run") {
1951
+ const args = [cmd, ...rest];
1952
+ if (promptFile) {
1953
+ args.push(buildOpenCodeRunBootstrapPrompt());
1954
+ args.push("--file", promptFile);
1955
+ return args;
1956
+ }
1957
+ args.push(prompt);
1958
+ return args;
1959
+ }
1960
+ if (promptFile) {
1961
+ return [cmd, ...rest, buildOpenCodeTuiPromptArg(buildOpenCodeTuiBootstrapPrompt(promptFile, cwd))];
1962
+ }
1963
+ return [cmd, ...rest, buildOpenCodeTuiPromptArg(prompt)];
1964
+ }
1965
+ function buildOpenCodeRunBootstrapPrompt() {
1966
+ return "Read the attached Markdown file first. It contains the full task instructions and context for this run.";
1967
+ }
1968
+ function buildOpenCodeTuiBootstrapPrompt(tempFile, cwd) {
1969
+ const displayPath = path6.relative(cwd, tempFile) || path6.basename(tempFile);
1970
+ const normalizedPath = displayPath.split(path6.sep).join("/");
1971
+ return `The full rendered rundown task prompt is staged in ${normalizedPath}. Open and read that file completely before taking any action, then continue the work in this session.`;
1972
+ }
1973
+ function buildOpenCodeTuiPromptArg(prompt) {
1974
+ return `--prompt=${prompt}`;
1975
+ }
1976
+ function executeCommand(cmd, args, mode, cwd) {
1977
+ return new Promise((resolve, reject) => {
1978
+ if (mode === "tui") {
1979
+ if (os.platform() === "win32") {
1980
+ const child3 = spawn3(
1981
+ "cmd",
1982
+ ["/c", "start", "/wait", '""', cmd, ...args],
1983
+ { stdio: "ignore", cwd, shell: false }
1984
+ );
1985
+ child3.on("close", (code) => {
1986
+ resolve({ exitCode: code, stdout: "", stderr: "" });
1987
+ });
1988
+ child3.on("error", reject);
1989
+ return;
1990
+ }
1991
+ const child2 = spawn3(cmd, args, {
1992
+ stdio: "inherit",
1993
+ cwd,
1994
+ shell: false
1995
+ });
1996
+ child2.on("close", (code) => {
1997
+ resolve({ exitCode: code, stdout: "", stderr: "" });
1998
+ });
1999
+ child2.on("error", reject);
2000
+ return;
2001
+ }
2002
+ if (mode === "detached") {
2003
+ const child2 = spawn3(cmd, args, {
2004
+ stdio: "ignore",
2005
+ cwd,
2006
+ shell: false,
2007
+ detached: true
2008
+ });
2009
+ child2.unref();
2010
+ resolve({ exitCode: null, stdout: "", stderr: "" });
2011
+ return;
2012
+ }
2013
+ const child = spawn3(cmd, args, {
2014
+ stdio: ["inherit", "pipe", "pipe"],
2015
+ cwd,
2016
+ shell: false
2017
+ });
2018
+ const stdout = [];
2019
+ const stderr = [];
2020
+ child.stdout?.on("data", (chunk) => stdout.push(chunk));
2021
+ child.stderr?.on("data", (chunk) => stderr.push(chunk));
2022
+ child.on("close", (code) => {
2023
+ resolve({
2024
+ exitCode: code,
2025
+ stdout: Buffer.concat(stdout).toString("utf-8"),
2026
+ stderr: Buffer.concat(stderr).toString("utf-8")
2027
+ });
2028
+ });
2029
+ child.on("error", reject);
2030
+ });
2031
+ }
2032
+ function buildCaptureNotes(mode) {
2033
+ if (mode === "wait") {
2034
+ return void 0;
2035
+ }
2036
+ if (mode === "tui") {
2037
+ return "Interactive TUI mode does not capture worker stdout/stderr transcripts.";
2038
+ }
2039
+ return "Detached mode does not capture worker stdout/stderr and leaves runtime artifacts in place.";
2040
+ }
2041
+
2042
+ // src/infrastructure/adapters/worker-executor-adapter.ts
2043
+ function createWorkerExecutorAdapter() {
2044
+ return {
2045
+ async runWorker(options) {
2046
+ return runWorker({
2047
+ command: options.command,
2048
+ prompt: options.prompt,
2049
+ mode: options.mode,
2050
+ transport: options.transport,
2051
+ cwd: options.cwd,
2052
+ artifactContext: options.artifactContext,
2053
+ artifactPhase: options.artifactPhase,
2054
+ artifactExtra: options.artifactExtra
2055
+ });
2056
+ },
2057
+ async executeInlineCli(command, cwd, options) {
2058
+ return executeInlineCli(command, cwd, {
2059
+ artifactContext: options?.artifactContext,
2060
+ keepArtifacts: options?.keepArtifacts,
2061
+ artifactExtra: options?.artifactExtra
2062
+ });
2063
+ }
2064
+ };
2065
+ }
2066
+
2067
+ // src/infrastructure/validation.ts
2068
+ import fs7 from "fs";
2069
+ function validationFilePath2(task) {
2070
+ return `${task.file}.${task.index}.validation`;
2071
+ }
2072
+ function readValidationFile(task) {
2073
+ const p = validationFilePath2(task);
2074
+ try {
2075
+ return fs7.readFileSync(p, "utf-8").trim();
2076
+ } catch {
2077
+ return null;
2078
+ }
2079
+ }
2080
+ function removeValidationFile(task) {
2081
+ const p = validationFilePath2(task);
2082
+ try {
2083
+ fs7.unlinkSync(p);
2084
+ } catch {
2085
+ }
2086
+ }
2087
+ function isValidationOk(task) {
2088
+ const content = readValidationFile(task);
2089
+ return content !== null && content.toUpperCase() === "OK";
2090
+ }
2091
+ async function validate(options) {
2092
+ const vars = {
2093
+ ...options.templateVars,
2094
+ task: options.task.text,
2095
+ file: options.task.file,
2096
+ context: options.contextBefore,
2097
+ taskIndex: options.task.index,
2098
+ taskLine: options.task.line,
2099
+ source: options.source
2100
+ };
2101
+ const prompt = renderTemplate(options.template, vars);
2102
+ removeValidationFile(options.task);
2103
+ const runResult = await runWorker({
2104
+ command: options.command,
2105
+ prompt,
2106
+ mode: options.mode ?? "wait",
2107
+ transport: options.transport ?? "file",
2108
+ cwd: options.cwd,
2109
+ artifactContext: options.artifactContext,
2110
+ artifactPhase: "verify"
2111
+ });
2112
+ if (runResult.exitCode !== 0) {
2113
+ return false;
2114
+ }
2115
+ return isValidationOk(options.task);
2116
+ }
2117
+
2118
+ // src/infrastructure/adapters/task-validation-adapter.ts
2119
+ function createTaskValidationAdapter() {
2120
+ return {
2121
+ validate(options) {
2122
+ return validate({
2123
+ ...options,
2124
+ templateVars: options.templateVars,
2125
+ artifactContext: options.artifactContext
2126
+ });
2127
+ }
2128
+ };
2129
+ }
2130
+
2131
+ // src/infrastructure/correction.ts
2132
+ async function correct(options) {
2133
+ let attempts = 0;
2134
+ for (let i = 0; i < options.maxRetries; i++) {
2135
+ attempts++;
2136
+ const validationResult = readValidationFile(options.task) ?? "Validation failed (no details).";
2137
+ const vars = {
2138
+ ...options.templateVars,
2139
+ task: options.task.text,
2140
+ file: options.task.file,
2141
+ context: options.contextBefore,
2142
+ taskIndex: options.task.index,
2143
+ taskLine: options.task.line,
2144
+ source: options.source,
2145
+ validationResult
2146
+ };
2147
+ const prompt = renderTemplate(options.correctTemplate, vars);
2148
+ await runWorker({
2149
+ command: options.command,
2150
+ prompt,
2151
+ mode: options.mode ?? "wait",
2152
+ transport: options.transport ?? "file",
2153
+ cwd: options.cwd,
2154
+ artifactContext: options.artifactContext,
2155
+ artifactPhase: "repair",
2156
+ artifactExtra: { attempt: attempts }
2157
+ });
2158
+ const valid = await validate({
2159
+ task: options.task,
2160
+ source: options.source,
2161
+ contextBefore: options.contextBefore,
2162
+ template: options.validateTemplate,
2163
+ command: options.command,
2164
+ mode: options.mode,
2165
+ transport: options.transport,
2166
+ cwd: options.cwd,
2167
+ templateVars: options.templateVars,
2168
+ artifactContext: options.artifactContext
2169
+ });
2170
+ if (valid) {
2171
+ return { valid: true, attempts };
2172
+ }
2173
+ }
2174
+ return { valid: false, attempts };
2175
+ }
2176
+
2177
+ // src/infrastructure/adapters/task-correction-adapter.ts
2178
+ function createTaskCorrectionAdapter() {
2179
+ return {
2180
+ correct(options) {
2181
+ return correct({
2182
+ ...options,
2183
+ templateVars: options.templateVars,
2184
+ artifactContext: options.artifactContext
2185
+ });
2186
+ }
2187
+ };
2188
+ }
2189
+
2190
+ // src/infrastructure/adapters/working-directory-adapter.ts
2191
+ function createWorkingDirectoryAdapter() {
2192
+ return {
2193
+ cwd() {
2194
+ return process.cwd();
2195
+ }
2196
+ };
2197
+ }
2198
+
2199
+ // src/infrastructure/open-directory.ts
2200
+ import { spawn as spawn4 } from "child_process";
2201
+ function openDirectory(dirPath) {
2202
+ if (process.platform === "win32") {
2203
+ const child2 = spawn4("explorer", [dirPath], {
2204
+ detached: true,
2205
+ stdio: "ignore",
2206
+ shell: false
2207
+ });
2208
+ child2.unref();
2209
+ return;
2210
+ }
2211
+ if (process.platform === "darwin") {
2212
+ const child2 = spawn4("open", [dirPath], {
2213
+ detached: true,
2214
+ stdio: "ignore",
2215
+ shell: false
2216
+ });
2217
+ child2.unref();
2218
+ return;
2219
+ }
2220
+ const child = spawn4("xdg-open", [dirPath], {
2221
+ detached: true,
2222
+ stdio: "ignore",
2223
+ shell: false
2224
+ });
2225
+ child.unref();
2226
+ }
2227
+
2228
+ // src/infrastructure/adapters/directory-opener-adapter.ts
2229
+ function createDirectoryOpenerAdapter() {
2230
+ return {
2231
+ openDirectory
2232
+ };
2233
+ }
2234
+
2235
+ // src/create-app.ts
2236
+ function createAppPorts(overrides = {}) {
2237
+ return {
2238
+ fileSystem: overrides.fileSystem ?? createNodeFileSystem(),
2239
+ processRunner: overrides.processRunner ?? createCrossSpawnProcessRunner(),
2240
+ gitClient: overrides.gitClient ?? createExecFileGitClient(),
2241
+ templateLoader: overrides.templateLoader ?? createFsTemplateLoader(),
2242
+ validationSidecar: overrides.validationSidecar ?? createFsValidationSidecar(),
2243
+ artifactStore: overrides.artifactStore ?? createFsArtifactStore(),
2244
+ clock: overrides.clock ?? createSystemClock(),
2245
+ directoryOpener: overrides.directoryOpener ?? createDirectoryOpenerAdapter(),
2246
+ sourceResolver: overrides.sourceResolver ?? createSourceResolverAdapter(),
2247
+ taskSelector: overrides.taskSelector ?? createTaskSelectorAdapter(),
2248
+ workerExecutor: overrides.workerExecutor ?? createWorkerExecutorAdapter(),
2249
+ taskValidation: overrides.taskValidation ?? createTaskValidationAdapter(),
2250
+ taskCorrection: overrides.taskCorrection ?? createTaskCorrectionAdapter(),
2251
+ workingDirectory: overrides.workingDirectory ?? createWorkingDirectoryAdapter(),
2252
+ output: overrides.output ?? createNoopOutputPort()
2253
+ };
2254
+ }
2255
+ function createNoopOutputPort() {
2256
+ return {
2257
+ emit() {
2258
+ }
2259
+ };
2260
+ }
2261
+ function createDefaultUseCaseFactories() {
2262
+ return {
2263
+ runTask: (ports) => createRunTask({
2264
+ sourceResolver: ports.sourceResolver,
2265
+ taskSelector: ports.taskSelector,
2266
+ workerExecutor: ports.workerExecutor,
2267
+ taskValidation: ports.taskValidation,
2268
+ taskCorrection: ports.taskCorrection,
2269
+ workingDirectory: ports.workingDirectory,
2270
+ fileSystem: ports.fileSystem,
2271
+ templateLoader: ports.templateLoader,
2272
+ validationSidecar: ports.validationSidecar,
2273
+ artifactStore: ports.artifactStore,
2274
+ gitClient: ports.gitClient,
2275
+ processRunner: ports.processRunner,
2276
+ output: ports.output
2277
+ }),
2278
+ planTask: (ports) => createPlanTask({
2279
+ sourceResolver: ports.sourceResolver,
2280
+ taskSelector: ports.taskSelector,
2281
+ workerExecutor: ports.workerExecutor,
2282
+ workingDirectory: ports.workingDirectory,
2283
+ fileSystem: ports.fileSystem,
2284
+ templateLoader: ports.templateLoader,
2285
+ artifactStore: ports.artifactStore,
2286
+ output: ports.output
2287
+ }),
2288
+ listTasks: (ports) => createListTasks({
2289
+ fileSystem: ports.fileSystem,
2290
+ sourceResolver: ports.sourceResolver,
2291
+ output: ports.output
2292
+ }),
2293
+ nextTask: (ports) => createNextTask({
2294
+ sourceResolver: ports.sourceResolver,
2295
+ taskSelector: ports.taskSelector,
2296
+ output: ports.output
2297
+ }),
2298
+ initProject: (ports) => createInitProject({
2299
+ fileSystem: ports.fileSystem,
2300
+ output: ports.output
2301
+ }),
2302
+ manageArtifacts: (ports) => createManageArtifacts({
2303
+ artifactStore: ports.artifactStore,
2304
+ directoryOpener: ports.directoryOpener,
2305
+ workingDirectory: ports.workingDirectory,
2306
+ output: ports.output
2307
+ })
2308
+ };
2309
+ }
2310
+ function createAppFromFactories(ports, factoryOverrides = {}) {
2311
+ const factories = {
2312
+ ...createDefaultUseCaseFactories(),
2313
+ ...factoryOverrides
2314
+ };
2315
+ return {
2316
+ runTask: factories.runTask(ports),
2317
+ planTask: factories.planTask(ports),
2318
+ listTasks: factories.listTasks(ports),
2319
+ nextTask: factories.nextTask(ports),
2320
+ initProject: factories.initProject(ports),
2321
+ manageArtifacts: factories.manageArtifacts(ports)
2322
+ };
2323
+ }
2324
+ function createApp(dependencies = {}) {
2325
+ const portOverrides = dependencies.ports ?? {};
2326
+ const useCaseFactoryOverrides = dependencies.useCaseFactories ?? {};
2327
+ const ports = createAppPorts(portOverrides);
2328
+ return createAppFromFactories(ports, useCaseFactoryOverrides);
2329
+ }
2330
+
2331
+ // src/presentation/output-port.ts
2332
+ import pc from "picocolors";
2333
+ function dim(message) {
2334
+ return pc.dim(message);
2335
+ }
2336
+ function taskLabel(task) {
2337
+ return `${pc.cyan(task.file)}:${pc.yellow(String(task.line))} ${pc.dim(`[#${task.index}]`)} ${task.text}`;
2338
+ }
2339
+ var cliOutputPort = {
2340
+ emit(event) {
2341
+ switch (event.kind) {
2342
+ case "info":
2343
+ console.log(pc.blue("\u2139") + " " + event.message);
2344
+ return;
2345
+ case "warn":
2346
+ console.log(pc.yellow("\u26A0") + " " + event.message);
2347
+ return;
2348
+ case "error":
2349
+ console.error(pc.red("\u2716") + " " + event.message);
2350
+ return;
2351
+ case "success":
2352
+ console.log(pc.green("\u2714") + " " + event.message);
2353
+ return;
2354
+ case "task":
2355
+ console.log(
2356
+ taskLabel(event.task) + (event.blocked ? dim(" (blocked \u2014 has unchecked subtasks)") : "")
2357
+ );
2358
+ return;
2359
+ case "text":
2360
+ console.log(event.text);
2361
+ return;
2362
+ case "stderr":
2363
+ process.stderr.write(event.text);
2364
+ return;
2365
+ default:
2366
+ return;
2367
+ }
2368
+ }
2369
+ };
2370
+
2371
+ // src/presentation/cli.ts
2372
+ import fs8 from "fs";
2373
+ import pc2 from "picocolors";
2374
+ var RUNNER_MODES = ["wait", "tui", "detached"];
2375
+ var PLANNER_MODES = ["wait"];
2376
+ var PROMPT_TRANSPORTS = ["file", "arg"];
2377
+ var SORT_MODES = ["name-sort", "none", "old-first", "new-first"];
2378
+ var EXIT_TEST_MODE_ENV = "RUNDOWN_TEST_MODE";
2379
+ var CliExitSignal = class extends Error {
2380
+ code;
2381
+ constructor(code) {
2382
+ super(`CLI exited with code ${code}`);
2383
+ this.code = code;
2384
+ }
2385
+ };
2386
+ function readCliVersion() {
2387
+ try {
2388
+ const packageJson = JSON.parse(
2389
+ fs8.readFileSync(new URL("../package.json", import.meta.url), "utf-8")
2390
+ );
2391
+ return packageJson.version ?? "0.0.0";
2392
+ } catch {
2393
+ return "0.0.0";
2394
+ }
2395
+ }
2396
+ var workerFromSeparator = [];
2397
+ var program = new Command();
2398
+ var app = createApp({
2399
+ ports: {
2400
+ output: cliOutputPort
2401
+ }
2402
+ });
2403
+ program.name("rundown").description("A Markdown-native task runtime for agentic workflows.").version(readCliVersion());
2404
+ program.command("run").description("Find the next unchecked TODO and execute it.").argument("<source>", "File, directory, or glob to scan for Markdown tasks").option("--mode <mode>", "Runner execution mode: wait, tui, detached", "wait").option("--transport <transport>", "Prompt transport: file, arg", "file").option("--sort <sort>", "File sort mode: name-sort, none, old-first, new-first", "name-sort").option("--verify", "Run verification after task execution (default)").option("--no-verify", "Disable verification after task execution").option("--only-verify", "Skip execution and run verification directly", false).option("--retries <n>", "Max repair retries on verification failure", "0").option("--no-repair", "Disable repair even when retries are set", false).option("--dry-run", "Show what would be executed without running it", false).option("--print-prompt", "Print the rendered prompt and exit", false).option("--keep-artifacts", "Preserve runtime prompts, logs, and metadata under .rundown/runs", false).option("--vars-file [path]", "Load extra template variables from a JSON file (default: .rundown/vars.json)").option("--var <key=value>", "Template variable to inject into prompts (repeatable)", collectOption, []).option("--commit", "Auto-commit checked task file after successful completion", false).option("--commit-message <template>", "Commit message template (supports {{task}} and {{file}})").option("--on-complete <command>", "Run a shell command after successful task completion").option("--worker <command...>", "Worker command to run (alternative to -- <command>)").allowUnknownOption(false).action(withCliAction(async (source, opts) => {
2405
+ const mode = parseRunnerMode(opts.mode, RUNNER_MODES);
2406
+ const transport = parsePromptTransport(opts.transport);
2407
+ const sortMode = parseSortMode(opts.sort);
2408
+ const verify = resolveVerifyFlag(opts);
2409
+ const onlyVerify = Boolean(opts.onlyVerify);
2410
+ const noRepair = Boolean(opts.noRepair);
2411
+ const retries = parseRetries(opts.retries);
2412
+ const dryRun = opts.dryRun;
2413
+ const printPrompt = opts.printPrompt;
2414
+ const keepArtifacts = opts.keepArtifacts;
2415
+ const varsFileOption = opts.varsFile;
2416
+ const cliTemplateVarArgs = opts.var ?? [];
2417
+ const workerCommand = Array.isArray(opts.worker) ? opts.worker : typeof opts.worker === "string" ? [opts.worker] : workerFromSeparator;
2418
+ const commitAfterComplete = Boolean(opts.commit);
2419
+ const commitMessageTemplate = normalizeOptionalString(opts.commitMessage);
2420
+ const onCompleteCommand = normalizeOptionalString(opts.onComplete);
2421
+ return app.runTask({
2422
+ source,
2423
+ mode,
2424
+ transport,
2425
+ sortMode,
2426
+ verify,
2427
+ onlyVerify,
2428
+ noRepair,
2429
+ retries,
2430
+ dryRun,
2431
+ printPrompt,
2432
+ keepArtifacts,
2433
+ varsFileOption,
2434
+ cliTemplateVarArgs,
2435
+ workerCommand,
2436
+ commitAfterComplete,
2437
+ commitMessageTemplate,
2438
+ onCompleteCommand
2439
+ });
2440
+ }));
2441
+ program.command("next").description("Show the next unchecked task without executing it.").argument("<source>", "File, directory, or glob to scan for Markdown tasks").option("--sort <sort>", "File sort mode: name-sort, none, old-first, new-first", "name-sort").action(withCliAction((source, opts) => {
2442
+ return app.nextTask({
2443
+ source,
2444
+ sortMode: parseSortMode(opts.sort)
2445
+ });
2446
+ }));
2447
+ program.command("list").description("List all unchecked tasks across the source.").argument("<source>", "File, directory, or glob to scan for Markdown tasks").option("--sort <sort>", "File sort mode: name-sort, none, old-first, new-first", "name-sort").option("--all", "Show all tasks including checked ones", false).action(withCliAction((source, opts) => {
2448
+ return app.listTasks({
2449
+ source,
2450
+ sortMode: parseSortMode(opts.sort),
2451
+ includeAll: opts.all
2452
+ });
2453
+ }));
2454
+ program.command("artifacts").description("List or clean saved runtime artifact runs under .rundown/runs.").option("--clean", "Remove all saved runtime artifact runs", false).option("--json", "Print saved runtime artifacts as JSON", false).option("--failed", "Show only failed runtime artifact runs", false).option("--open <runId>", "Open a saved runtime artifact folder by run id, unique prefix, or 'latest'").action(withCliAction((opts) => {
2455
+ return app.manageArtifacts({
2456
+ clean: opts.clean,
2457
+ json: opts.json,
2458
+ failed: opts.failed,
2459
+ open: typeof opts.open === "string" ? opts.open : ""
2460
+ });
2461
+ }));
2462
+ program.command("plan").description("Decompose a task into subtasks using a worker command.").argument("<source>", "File, directory, or glob to scan for Markdown tasks").option("--at <file:line>", "Target a specific task by file path and line number").option("--mode <mode>", "Planner mode: wait", "wait").option("--transport <transport>", "Prompt transport: file, arg", "file").option("--sort <sort>", "File sort mode: name-sort, none, old-first, new-first", "name-sort").option("--dry-run", "Show what would be planned without executing", false).option("--print-prompt", "Print the rendered plan prompt and exit", false).option("--keep-artifacts", "Preserve runtime prompts, logs, and metadata under .rundown/runs", false).option("--vars-file [path]", "Load extra template variables from a JSON file (default: .rundown/vars.json)").option("--var <key=value>", "Template variable to inject into prompts (repeatable)", collectOption, []).option("--worker <command...>", "Worker command to run (alternative to -- <command>)").allowUnknownOption(false).action(withCliAction((source, opts) => {
2463
+ const mode = parseRunnerMode(opts.mode, PLANNER_MODES);
2464
+ const transport = parsePromptTransport(opts.transport);
2465
+ const sortMode = parseSortMode(opts.sort);
2466
+ const dryRun = opts.dryRun;
2467
+ const printPrompt = opts.printPrompt;
2468
+ const keepArtifacts = opts.keepArtifacts;
2469
+ const varsFileOption = opts.varsFile;
2470
+ const cliTemplateVarArgs = opts.var ?? [];
2471
+ const workerCommand = Array.isArray(opts.worker) ? opts.worker : typeof opts.worker === "string" ? [opts.worker] : workerFromSeparator;
2472
+ return app.planTask({
2473
+ source,
2474
+ at: opts.at,
2475
+ mode,
2476
+ transport,
2477
+ sortMode,
2478
+ dryRun,
2479
+ printPrompt,
2480
+ keepArtifacts,
2481
+ varsFileOption,
2482
+ cliTemplateVarArgs,
2483
+ workerCommand
2484
+ });
2485
+ }));
2486
+ program.command("init").description("Create a .rundown/ directory with default templates (plan, execute, verify, repair).").action(withCliAction(() => app.initProject()));
2487
+ function collectOption(value, previous) {
2488
+ return [...previous, value];
2489
+ }
2490
+ function parseRunnerMode(value, allowed) {
2491
+ const mode = value ?? "wait";
2492
+ if (!allowed.includes(mode)) {
2493
+ throw new Error(`Invalid --mode value: ${value}. Allowed: ${allowed.join(", ")}.`);
2494
+ }
2495
+ return mode;
2496
+ }
2497
+ function parsePromptTransport(value) {
2498
+ const transport = value ?? "file";
2499
+ if (!PROMPT_TRANSPORTS.includes(transport)) {
2500
+ throw new Error(`Invalid --transport value: ${value}. Allowed: ${PROMPT_TRANSPORTS.join(", ")}.`);
2501
+ }
2502
+ return transport;
2503
+ }
2504
+ function parseSortMode(value) {
2505
+ const sortMode = value ?? "name-sort";
2506
+ if (!SORT_MODES.includes(sortMode)) {
2507
+ throw new Error(`Invalid --sort value: ${value}. Allowed: ${SORT_MODES.join(", ")}.`);
2508
+ }
2509
+ return sortMode;
2510
+ }
2511
+ function parseRetries(value) {
2512
+ const raw = value ?? "0";
2513
+ if (!/^\d+$/.test(raw)) {
2514
+ throw new Error(`Invalid --retries value: ${raw}. Must be a non-negative integer.`);
2515
+ }
2516
+ const parsed = Number(raw);
2517
+ if (!Number.isSafeInteger(parsed)) {
2518
+ throw new Error(`Invalid --retries value: ${raw}. Must be a safe non-negative integer.`);
2519
+ }
2520
+ return parsed;
2521
+ }
2522
+ function normalizeOptionalString(value) {
2523
+ if (typeof value !== "string") {
2524
+ return void 0;
2525
+ }
2526
+ return value.trim() === "" ? void 0 : value;
2527
+ }
2528
+ function resolveVerifyFlag(opts) {
2529
+ const verifyOpt = opts.verify;
2530
+ if (verifyOpt === false) {
2531
+ return false;
2532
+ }
2533
+ if (verifyOpt === true) {
2534
+ return true;
2535
+ }
2536
+ return true;
2537
+ }
2538
+ function withCliAction(action) {
2539
+ return async (...args) => {
2540
+ try {
2541
+ const exitCode = await action(...args);
2542
+ terminate(exitCode);
2543
+ } catch (err) {
2544
+ if (isCliExitSignal(err)) {
2545
+ throw err;
2546
+ }
2547
+ console.error(pc2.red("\u2716") + " " + String(err));
2548
+ terminate(1);
2549
+ }
2550
+ };
2551
+ }
2552
+ async function parseCliArgs(argv) {
2553
+ const { rundownArgs, workerFromSeparator: workerCommandArgs } = splitWorkerFromSeparator(argv);
2554
+ workerFromSeparator = workerCommandArgs;
2555
+ await program.parseAsync(rundownArgs, { from: "user" });
2556
+ }
2557
+ if (process.env.RUNDOWN_DISABLE_AUTO_PARSE !== "1") {
2558
+ parseCliArgs(process.argv.slice(2)).catch((err) => {
2559
+ if (isCliExitSignal(err)) {
2560
+ process.exit(err.code);
2561
+ }
2562
+ console.error(pc2.red("\u2716") + " " + String(err));
2563
+ process.exit(1);
2564
+ });
2565
+ }
2566
+ function splitWorkerFromSeparator(argv) {
2567
+ const sepIndex = argv.indexOf("--");
2568
+ return {
2569
+ rundownArgs: sepIndex !== -1 ? argv.slice(0, sepIndex) : argv,
2570
+ workerFromSeparator: sepIndex !== -1 ? argv.slice(sepIndex + 1) : []
2571
+ };
2572
+ }
2573
+ function terminate(code) {
2574
+ if (process.env[EXIT_TEST_MODE_ENV] === "1") {
2575
+ throw new CliExitSignal(code);
2576
+ }
2577
+ process.exit(code);
2578
+ }
2579
+ function isCliExitSignal(error) {
2580
+ return error instanceof CliExitSignal;
2581
+ }
2582
+ export {
2583
+ parseCliArgs
2584
+ };
2585
+ //# sourceMappingURL=cli.js.map