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