@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/LICENSE +21 -0
- package/README.md +192 -0
- package/dist/cli.js +2585 -0
- package/dist/cli.js.map +1 -0
- package/dist/index.d.ts +397 -0
- package/dist/index.js +2328 -0
- package/dist/index.js.map +1 -0
- package/package.json +71 -0
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
|