executant 1.20.0 → 1.21.1
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/README.md +44 -0
- package/dist/index.js +193 -60
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -115,10 +115,43 @@ steps:
|
|
|
115
115
|
This is pass {{item}} of 5. Review src/runner.ts for untested edge cases.
|
|
116
116
|
```
|
|
117
117
|
|
|
118
|
+
## Variables at Runtime
|
|
119
|
+
|
|
120
|
+
Pass `--var KEY=VALUE` on the command line to override or supply workflow vars without editing the YAML:
|
|
121
|
+
|
|
122
|
+
```bash
|
|
123
|
+
executant --var env=staging --var region=eu-west-1 deploy.yaml
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
CLI vars override any same-named vars in the workflow's `vars:` section. Multiple `--var` flags are accepted.
|
|
127
|
+
|
|
118
128
|
## Quality Controls
|
|
119
129
|
|
|
120
130
|
- **`llm_as_judge: true`** — after a step completes, Claude evaluates the output; retries with feedback on FAIL, up to 5×
|
|
121
131
|
- **`self_healing: true`** — on script failure, Claude diagnoses and repairs the command, then re-runs it, up to 5×
|
|
132
|
+
- **`timeout_seconds: N`** — kill the step after N seconds and fail with exit code 3. Works for both script and prompt steps.
|
|
133
|
+
|
|
134
|
+
```yaml
|
|
135
|
+
steps:
|
|
136
|
+
- name: install
|
|
137
|
+
command: npm ci
|
|
138
|
+
timeout_seconds: 120 # fail if install takes longer than 2 min
|
|
139
|
+
|
|
140
|
+
- name: implement
|
|
141
|
+
prompt: Implement the feature described above.
|
|
142
|
+
timeout_seconds: 1800 # 30 min ceiling for the Claude step
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
## Cancellation
|
|
146
|
+
|
|
147
|
+
Write a `.executant-cancel` file in the **same directory as the workflow YAML** to stop the workflow cleanly **between steps**:
|
|
148
|
+
|
|
149
|
+
```bash
|
|
150
|
+
executant long-workflow.yaml &
|
|
151
|
+
touch .executant-cancel # workflow stops at the next step boundary; exits 4
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
The file is deleted automatically. This is a cooperative, process-safe alternative to SIGTERM — no mid-step git state corruption. The cancel file is always resolved relative to the workflow file, so the location is predictable regardless of which directory you invoked executant from.
|
|
122
155
|
|
|
123
156
|
## Interjection
|
|
124
157
|
|
|
@@ -162,9 +195,20 @@ executant workflow.yaml # run a workflow
|
|
|
162
195
|
executant --ci workflow.yaml # headless, NDJSON to stdout
|
|
163
196
|
executant --step <name|n> wf.yaml # run one step by name or index
|
|
164
197
|
executant --from-step <n> wf.yaml # resume from step n
|
|
198
|
+
executant --var KEY=VALUE wf.yaml # override a workflow var at runtime
|
|
165
199
|
executant update # upgrade to latest version
|
|
166
200
|
```
|
|
167
201
|
|
|
202
|
+
### Exit codes
|
|
203
|
+
|
|
204
|
+
| Code | Meaning |
|
|
205
|
+
|------|---------|
|
|
206
|
+
| `0` | All steps completed successfully |
|
|
207
|
+
| `1` | A step failed at runtime |
|
|
208
|
+
| `2` | YAML or variable validation error |
|
|
209
|
+
| `3` | A step timed out (`timeout_seconds` exceeded) |
|
|
210
|
+
| `4` | Cancelled via `.executant-cancel` file |
|
|
211
|
+
|
|
168
212
|
## Development
|
|
169
213
|
|
|
170
214
|
```bash
|
package/dist/index.js
CHANGED
|
@@ -52,8 +52,8 @@ var init_update = __esm({
|
|
|
52
52
|
// src/index.ts
|
|
53
53
|
import React3 from "react";
|
|
54
54
|
import { render } from "ink";
|
|
55
|
-
import { readFileSync as readFileSync6 } from "node:fs";
|
|
56
|
-
import { dirname as dirname4, join as
|
|
55
|
+
import { mkdirSync as mkdirSync4, readFileSync as readFileSync6 } from "node:fs";
|
|
56
|
+
import { dirname as dirname4, join as join5, resolve as resolve3 } from "node:path";
|
|
57
57
|
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
58
58
|
|
|
59
59
|
// src/load-workflow.ts
|
|
@@ -154,7 +154,8 @@ var RawStepSchema = z.lazy(
|
|
|
154
154
|
forEach: z.union([z.array(z.string()), z.string()]).optional(),
|
|
155
155
|
repeat: z.number().int().positive().optional(),
|
|
156
156
|
context: z.array(z.string()).optional(),
|
|
157
|
-
steps: z.array(RawStepSchema).min(1).optional()
|
|
157
|
+
steps: z.array(RawStepSchema).min(1).optional(),
|
|
158
|
+
timeout_seconds: z.number().positive().optional()
|
|
158
159
|
})
|
|
159
160
|
);
|
|
160
161
|
var RawWorkflowSchema = z.object({
|
|
@@ -162,7 +163,7 @@ var RawWorkflowSchema = z.object({
|
|
|
162
163
|
steps: z.array(RawStepSchema),
|
|
163
164
|
vars: z.record(z.string(), z.string()).optional()
|
|
164
165
|
});
|
|
165
|
-
function loadWorkflow(filePath2) {
|
|
166
|
+
function loadWorkflow(filePath2, cliVars2 = {}) {
|
|
166
167
|
let raw;
|
|
167
168
|
try {
|
|
168
169
|
raw = readFileSync2(filePath2, "utf8");
|
|
@@ -179,7 +180,7 @@ function loadWorkflow(filePath2) {
|
|
|
179
180
|
throw new Error(`Invalid workflow file "${filePath2}":
|
|
180
181
|
${detail}`);
|
|
181
182
|
}
|
|
182
|
-
const vars = doc.vars ?? {};
|
|
183
|
+
const vars = { ...doc.vars ?? {}, ...cliVars2 };
|
|
183
184
|
const seen = /* @__PURE__ */ new Set();
|
|
184
185
|
for (const step of doc.steps) {
|
|
185
186
|
if (seen.has(step.name)) {
|
|
@@ -243,6 +244,9 @@ function convertInnerStep(step, vars, name, continueOnError) {
|
|
|
243
244
|
maxHealingAttempts: step.max_healing_attempts,
|
|
244
245
|
...step.output && {
|
|
245
246
|
output: resolveOutputFile(step.output, vars, name)
|
|
247
|
+
},
|
|
248
|
+
...step.timeout_seconds !== void 0 && {
|
|
249
|
+
timeoutSeconds: step.timeout_seconds
|
|
246
250
|
}
|
|
247
251
|
};
|
|
248
252
|
}
|
|
@@ -267,7 +271,10 @@ function convertInnerStep(step, vars, name, continueOnError) {
|
|
|
267
271
|
llmAsJudge: step.llm_as_judge,
|
|
268
272
|
allowedTools: step.allowed_tools,
|
|
269
273
|
model: "sonnet",
|
|
270
|
-
...contextFiles.length > 0 && { contextFiles }
|
|
274
|
+
...contextFiles.length > 0 && { contextFiles },
|
|
275
|
+
...step.timeout_seconds !== void 0 && {
|
|
276
|
+
timeoutSeconds: step.timeout_seconds
|
|
277
|
+
}
|
|
271
278
|
};
|
|
272
279
|
}
|
|
273
280
|
default:
|
|
@@ -309,14 +316,42 @@ function substituteVars(text, vars, stepName, field) {
|
|
|
309
316
|
|
|
310
317
|
// src/runner.ts
|
|
311
318
|
import { exec } from "node:child_process";
|
|
312
|
-
import {
|
|
313
|
-
|
|
319
|
+
import {
|
|
320
|
+
existsSync,
|
|
321
|
+
mkdirSync,
|
|
322
|
+
readFileSync as readFileSync3,
|
|
323
|
+
unlinkSync,
|
|
324
|
+
writeFileSync
|
|
325
|
+
} from "node:fs";
|
|
326
|
+
import { dirname as dirname2, join as join2 } from "node:path";
|
|
314
327
|
import { promisify } from "node:util";
|
|
315
328
|
import { z as z2 } from "zod";
|
|
316
329
|
|
|
317
330
|
// src/tasks/command.ts
|
|
318
331
|
import { spawn } from "node:child_process";
|
|
319
332
|
|
|
333
|
+
// src/types.ts
|
|
334
|
+
var InterjectChannel = class {
|
|
335
|
+
_queue = [];
|
|
336
|
+
/** Called by the TUI when the user submits an interjection message. */
|
|
337
|
+
interject(message) {
|
|
338
|
+
this._queue.push(message);
|
|
339
|
+
}
|
|
340
|
+
/** Drains and returns any queued messages (for non-Claude steps to consume). */
|
|
341
|
+
consumeQueue() {
|
|
342
|
+
const q = this._queue.slice();
|
|
343
|
+
this._queue = [];
|
|
344
|
+
return q;
|
|
345
|
+
}
|
|
346
|
+
};
|
|
347
|
+
var TimeoutError = class extends Error {
|
|
348
|
+
exitCode = 3;
|
|
349
|
+
constructor(stepName, seconds) {
|
|
350
|
+
super(`Step "${stepName}" timed out after ${seconds}s`);
|
|
351
|
+
this.name = "TimeoutError";
|
|
352
|
+
}
|
|
353
|
+
};
|
|
354
|
+
|
|
320
355
|
// src/tasks/stream.ts
|
|
321
356
|
var AsyncQueue = class {
|
|
322
357
|
buf = [];
|
|
@@ -332,8 +367,8 @@ var AsyncQueue = class {
|
|
|
332
367
|
}
|
|
333
368
|
next() {
|
|
334
369
|
if (this.buf.length > 0) return Promise.resolve(this.buf.shift());
|
|
335
|
-
return new Promise((
|
|
336
|
-
this.waiter =
|
|
370
|
+
return new Promise((resolve4) => {
|
|
371
|
+
this.waiter = resolve4;
|
|
337
372
|
});
|
|
338
373
|
}
|
|
339
374
|
async *[Symbol.asyncIterator]() {
|
|
@@ -369,11 +404,30 @@ async function* mergeStreamsToLines(...streams) {
|
|
|
369
404
|
yield* q;
|
|
370
405
|
}
|
|
371
406
|
function waitForExit(proc) {
|
|
372
|
-
return new Promise((
|
|
373
|
-
proc.on("close", (code) =>
|
|
407
|
+
return new Promise((resolve4, reject) => {
|
|
408
|
+
proc.on("close", (code) => resolve4(code ?? 0));
|
|
374
409
|
proc.on("error", reject);
|
|
375
410
|
});
|
|
376
411
|
}
|
|
412
|
+
function startTimeout(proc, taskName, timeoutSeconds) {
|
|
413
|
+
if (timeoutSeconds == null) return { check: () => {
|
|
414
|
+
}, cancel: () => {
|
|
415
|
+
} };
|
|
416
|
+
let timedOut = false;
|
|
417
|
+
const timer = setTimeout(() => {
|
|
418
|
+
timedOut = true;
|
|
419
|
+
try {
|
|
420
|
+
proc.kill();
|
|
421
|
+
} catch {
|
|
422
|
+
}
|
|
423
|
+
}, timeoutSeconds * 1e3);
|
|
424
|
+
return {
|
|
425
|
+
check: () => {
|
|
426
|
+
if (timedOut) throw new TimeoutError(taskName, timeoutSeconds);
|
|
427
|
+
},
|
|
428
|
+
cancel: () => clearTimeout(timer)
|
|
429
|
+
};
|
|
430
|
+
}
|
|
377
431
|
|
|
378
432
|
// src/tasks/command.ts
|
|
379
433
|
var CommandError = class extends Error {
|
|
@@ -391,12 +445,22 @@ async function* runCommand(task) {
|
|
|
391
445
|
const proc = spawn("bash", ["-c", task.command], {
|
|
392
446
|
stdio: ["ignore", "pipe", "pipe"]
|
|
393
447
|
});
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
448
|
+
const timeout = startTimeout(proc, task.name, task.timeoutSeconds);
|
|
449
|
+
try {
|
|
450
|
+
for await (const line of mergeStreamsToLines(proc.stdout, proc.stderr)) {
|
|
451
|
+
yield { type: "output:text", index: -1, text: line };
|
|
452
|
+
}
|
|
453
|
+
const code = await waitForExit(proc);
|
|
454
|
+
timeout.check();
|
|
455
|
+
if (code !== 0) {
|
|
456
|
+
throw new CommandError(
|
|
457
|
+
code,
|
|
458
|
+
task.command,
|
|
459
|
+
`Command "${task.name}" exited with code ${code}`
|
|
460
|
+
);
|
|
461
|
+
}
|
|
462
|
+
} finally {
|
|
463
|
+
timeout.cancel();
|
|
400
464
|
}
|
|
401
465
|
}
|
|
402
466
|
|
|
@@ -458,6 +522,7 @@ async function* runClaude(task) {
|
|
|
458
522
|
};
|
|
459
523
|
process.once("SIGTERM", cleanup);
|
|
460
524
|
process.once("SIGHUP", cleanup);
|
|
525
|
+
const timeout = startTimeout(proc, task.name, task.timeoutSeconds);
|
|
461
526
|
const plainLines = [];
|
|
462
527
|
try {
|
|
463
528
|
for await (const line of mergeStreamsToLines(proc.stdout, proc.stderr)) {
|
|
@@ -474,8 +539,10 @@ async function* runClaude(task) {
|
|
|
474
539
|
}
|
|
475
540
|
}
|
|
476
541
|
const code = await waitForExit(proc);
|
|
542
|
+
timeout.check();
|
|
477
543
|
if (code !== 0) throw buildExitError(code, plainLines);
|
|
478
544
|
} finally {
|
|
545
|
+
timeout.cancel();
|
|
479
546
|
process.off("SIGTERM", cleanup);
|
|
480
547
|
process.off("SIGHUP", cleanup);
|
|
481
548
|
}
|
|
@@ -560,10 +627,28 @@ function shouldSkipStep(stepNumber, name, options2) {
|
|
|
560
627
|
}
|
|
561
628
|
return options2.fromStep !== void 0 && stepNumber < options2.fromStep[0];
|
|
562
629
|
}
|
|
630
|
+
var LAST_OUTPUT_MAX_LINES = 100;
|
|
563
631
|
async function* runWorkflow(workflow2, options2 = {}, channel2) {
|
|
564
632
|
const workflowStart = Date.now();
|
|
633
|
+
const cancelFile = join2(
|
|
634
|
+
options2.workDir ?? process.cwd(),
|
|
635
|
+
".executant-cancel"
|
|
636
|
+
);
|
|
565
637
|
yield { type: "workflow:start", workflow: workflow2 };
|
|
638
|
+
let lastStepOutput;
|
|
566
639
|
for (const [i, task] of workflow2.tasks.entries()) {
|
|
640
|
+
if (existsSync(cancelFile)) {
|
|
641
|
+
try {
|
|
642
|
+
unlinkSync(cancelFile);
|
|
643
|
+
} catch {
|
|
644
|
+
}
|
|
645
|
+
yield {
|
|
646
|
+
type: "workflow:cancelled",
|
|
647
|
+
workflow: workflow2,
|
|
648
|
+
durationMs: Date.now() - workflowStart
|
|
649
|
+
};
|
|
650
|
+
return;
|
|
651
|
+
}
|
|
567
652
|
const stepNumber = i + 1;
|
|
568
653
|
if (shouldSkipStep(stepNumber, task.name, options2)) {
|
|
569
654
|
yield { type: "step:skip", index: i, name: task.name };
|
|
@@ -572,14 +657,20 @@ async function* runWorkflow(workflow2, options2 = {}, channel2) {
|
|
|
572
657
|
const stepStart = Date.now();
|
|
573
658
|
yield { type: "step:start", index: i, name: task.name };
|
|
574
659
|
const from = options2.fromStep && options2.fromStep[0] === stepNumber ? options2.fromStep.slice(1) : void 0;
|
|
660
|
+
const lines = [];
|
|
575
661
|
try {
|
|
576
662
|
for await (const event of runStep(task, from, channel2)) {
|
|
577
663
|
if (event.type === "step:iteration" || event.type === "step:inner" || event.type === "output:text" || event.type === "output:tool") {
|
|
664
|
+
if (event.type === "output:text") {
|
|
665
|
+
if (lines.length >= LAST_OUTPUT_MAX_LINES) lines.shift();
|
|
666
|
+
lines.push(event.text);
|
|
667
|
+
}
|
|
578
668
|
yield { ...event, index: i };
|
|
579
669
|
} else {
|
|
580
670
|
yield event;
|
|
581
671
|
}
|
|
582
672
|
}
|
|
673
|
+
lastStepOutput = lines.join("\n") || void 0;
|
|
583
674
|
yield {
|
|
584
675
|
type: "step:complete",
|
|
585
676
|
index: i,
|
|
@@ -588,14 +679,23 @@ async function* runWorkflow(workflow2, options2 = {}, channel2) {
|
|
|
588
679
|
};
|
|
589
680
|
} catch (err) {
|
|
590
681
|
const error = normalizeError(err);
|
|
591
|
-
|
|
682
|
+
const lastOutput = lines.join("\n") || void 0;
|
|
683
|
+
lastStepOutput = lastOutput;
|
|
684
|
+
yield {
|
|
685
|
+
type: "step:error",
|
|
686
|
+
index: i,
|
|
687
|
+
name: task.name,
|
|
688
|
+
error,
|
|
689
|
+
lastOutput
|
|
690
|
+
};
|
|
592
691
|
if (!task.continueOnError) throw error;
|
|
593
692
|
}
|
|
594
693
|
}
|
|
595
694
|
yield {
|
|
596
695
|
type: "workflow:complete",
|
|
597
696
|
workflow: workflow2,
|
|
598
|
-
durationMs: Date.now() - workflowStart
|
|
697
|
+
durationMs: Date.now() - workflowStart,
|
|
698
|
+
lastOutput: lastStepOutput
|
|
599
699
|
};
|
|
600
700
|
}
|
|
601
701
|
async function* runStep(task, from, channel2) {
|
|
@@ -1058,6 +1158,7 @@ function reducer(state, event) {
|
|
|
1058
1158
|
case "workflow:start":
|
|
1059
1159
|
return { ...state, startTime: Date.now() };
|
|
1060
1160
|
case "workflow:complete":
|
|
1161
|
+
case "workflow:cancelled":
|
|
1061
1162
|
return { ...state, endTime: Date.now() };
|
|
1062
1163
|
case "step:start":
|
|
1063
1164
|
return updateTask(state, event.index, {
|
|
@@ -1454,10 +1555,15 @@ function App({
|
|
|
1454
1555
|
if (event.type === "workflow:complete") {
|
|
1455
1556
|
setTimeout(() => exit(), EXIT_DELAY_MS);
|
|
1456
1557
|
}
|
|
1558
|
+
if (event.type === "workflow:cancelled") {
|
|
1559
|
+
process.exitCode = 4;
|
|
1560
|
+
setTimeout(() => exit(), EXIT_DELAY_MS);
|
|
1561
|
+
}
|
|
1457
1562
|
}
|
|
1458
1563
|
} catch (err) {
|
|
1459
1564
|
if (!active) return;
|
|
1460
1565
|
dispatch({ type: "log", level: "error", text: getErrorMessage(err) });
|
|
1566
|
+
process.exitCode = err instanceof TimeoutError ? 3 : 1;
|
|
1461
1567
|
setTimeout(
|
|
1462
1568
|
() => exit(err instanceof Error ? err : new Error(getErrorMessage(err))),
|
|
1463
1569
|
EXIT_DELAY_MS
|
|
@@ -1601,8 +1707,8 @@ function App({
|
|
|
1601
1707
|
}
|
|
1602
1708
|
|
|
1603
1709
|
// src/plan.ts
|
|
1604
|
-
import { existsSync, mkdirSync as mkdirSync2, readFileSync as readFileSync4, writeFileSync as writeFileSync2 } from "node:fs";
|
|
1605
|
-
import { join as
|
|
1710
|
+
import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync4, writeFileSync as writeFileSync2 } from "node:fs";
|
|
1711
|
+
import { join as join3, resolve } from "node:path";
|
|
1606
1712
|
import { dump as dumpYaml } from "js-yaml";
|
|
1607
1713
|
import { z as z3 } from "zod";
|
|
1608
1714
|
import { zodToJsonSchema as zodToJsonSchema2 } from "zod-to-json-schema";
|
|
@@ -1630,7 +1736,7 @@ function walkUp(startDir, check) {
|
|
|
1630
1736
|
while (true) {
|
|
1631
1737
|
const found = check(dir);
|
|
1632
1738
|
if (found !== null) return found;
|
|
1633
|
-
const parent =
|
|
1739
|
+
const parent = join3(dir, "..");
|
|
1634
1740
|
if (resolve(parent) === resolve(dir)) return null;
|
|
1635
1741
|
dir = parent;
|
|
1636
1742
|
}
|
|
@@ -1638,13 +1744,13 @@ function walkUp(startDir, check) {
|
|
|
1638
1744
|
function findGitRoot(startDir) {
|
|
1639
1745
|
return walkUp(
|
|
1640
1746
|
startDir,
|
|
1641
|
-
(dir) =>
|
|
1747
|
+
(dir) => existsSync2(join3(dir, ".git")) ? dir : null
|
|
1642
1748
|
);
|
|
1643
1749
|
}
|
|
1644
1750
|
function findProjectRoot(startDir) {
|
|
1645
1751
|
return walkUp(startDir, (dir) => {
|
|
1646
|
-
const candidate =
|
|
1647
|
-
return
|
|
1752
|
+
const candidate = join3(dir, ".claude", "executant.local", "tasks");
|
|
1753
|
+
return existsSync2(candidate) ? candidate : null;
|
|
1648
1754
|
});
|
|
1649
1755
|
}
|
|
1650
1756
|
function isSimpleRequest(description) {
|
|
@@ -1684,7 +1790,7 @@ Examples:
|
|
|
1684
1790
|
console.error("Error: -f/--file requires a file path argument");
|
|
1685
1791
|
process.exit(1);
|
|
1686
1792
|
}
|
|
1687
|
-
if (!
|
|
1793
|
+
if (!existsSync2(filePath2)) {
|
|
1688
1794
|
console.error(`Error: File not found: ${filePath2}`);
|
|
1689
1795
|
process.exit(1);
|
|
1690
1796
|
}
|
|
@@ -1712,14 +1818,14 @@ Examples:
|
|
|
1712
1818
|
let taskDir = findProjectRoot(process.cwd());
|
|
1713
1819
|
if (!taskDir) {
|
|
1714
1820
|
const base = findGitRoot(process.cwd()) ?? process.cwd();
|
|
1715
|
-
taskDir =
|
|
1821
|
+
taskDir = join3(base, ".claude", "executant.local", "tasks");
|
|
1716
1822
|
mkdirSync2(taskDir, { recursive: true });
|
|
1717
1823
|
}
|
|
1718
|
-
const todoDir =
|
|
1824
|
+
const todoDir = join3(taskDir, "todo");
|
|
1719
1825
|
mkdirSync2(todoDir, { recursive: true });
|
|
1720
1826
|
const slug = slugify(description);
|
|
1721
1827
|
const ts = timestamp();
|
|
1722
|
-
const taskFile =
|
|
1828
|
+
const taskFile = join3(todoDir, `${ts}-${slug}.yaml`);
|
|
1723
1829
|
return { description, taskFile, todoDir, fast };
|
|
1724
1830
|
}
|
|
1725
1831
|
async function runPass3Judge(description, workflow2) {
|
|
@@ -2037,7 +2143,7 @@ ${PLAN_SYSTEM_RULES}`,
|
|
|
2037
2143
|
}
|
|
2038
2144
|
|
|
2039
2145
|
// src/refine.ts
|
|
2040
|
-
import { existsSync as
|
|
2146
|
+
import { existsSync as existsSync3, readFileSync as readFileSync5 } from "node:fs";
|
|
2041
2147
|
import { load as loadYaml } from "js-yaml";
|
|
2042
2148
|
var PLAN_REFINE_PROMPT = loadPrompt("plan-refine");
|
|
2043
2149
|
var PLAN_SYSTEM_RULES2 = loadPrompt("plan-system-rules");
|
|
@@ -2064,7 +2170,7 @@ Examples:
|
|
|
2064
2170
|
console.error("Usage: executant refine <task-file> [INSTRUCTIONS]");
|
|
2065
2171
|
process.exit(1);
|
|
2066
2172
|
}
|
|
2067
|
-
if (!
|
|
2173
|
+
if (!existsSync3(taskFile)) {
|
|
2068
2174
|
console.error(`Error: File not found: ${taskFile}`);
|
|
2069
2175
|
process.exit(1);
|
|
2070
2176
|
}
|
|
@@ -2089,7 +2195,7 @@ Examples:
|
|
|
2089
2195
|
console.error("Error: -f/--file requires a file path argument");
|
|
2090
2196
|
process.exit(1);
|
|
2091
2197
|
}
|
|
2092
|
-
if (!
|
|
2198
|
+
if (!existsSync3(filePath2)) {
|
|
2093
2199
|
console.error(`Error: File not found: ${filePath2}`);
|
|
2094
2200
|
process.exit(1);
|
|
2095
2201
|
}
|
|
@@ -2325,13 +2431,13 @@ function PlanApp({ description, events: events2 }) {
|
|
|
2325
2431
|
}
|
|
2326
2432
|
|
|
2327
2433
|
// src/logger.ts
|
|
2328
|
-
import { appendFileSync, existsSync as
|
|
2329
|
-
import { dirname as dirname3, join as
|
|
2434
|
+
import { appendFileSync, existsSync as existsSync4, mkdirSync as mkdirSync3, writeFileSync as writeFileSync3 } from "node:fs";
|
|
2435
|
+
import { dirname as dirname3, join as join4, resolve as resolve2 } from "node:path";
|
|
2330
2436
|
function findExecutantLocalDir(startDir) {
|
|
2331
2437
|
let dir = resolve2(startDir);
|
|
2332
2438
|
while (true) {
|
|
2333
|
-
const candidate =
|
|
2334
|
-
if (
|
|
2439
|
+
const candidate = join4(dir, ".claude", "executant.local");
|
|
2440
|
+
if (existsSync4(candidate)) return candidate;
|
|
2335
2441
|
const parent = dirname3(dir);
|
|
2336
2442
|
if (parent === dir) return null;
|
|
2337
2443
|
dir = parent;
|
|
@@ -2340,7 +2446,7 @@ function findExecutantLocalDir(startDir) {
|
|
|
2340
2446
|
function resolveLogDir(workflowFilePath) {
|
|
2341
2447
|
const startDir = dirname3(resolve2(workflowFilePath));
|
|
2342
2448
|
const executantLocal = findExecutantLocalDir(startDir);
|
|
2343
|
-
return executantLocal ?
|
|
2449
|
+
return executantLocal ? join4(executantLocal, "logs") : join4(startDir, "logs");
|
|
2344
2450
|
}
|
|
2345
2451
|
var INIT_STATE = {
|
|
2346
2452
|
logFile: "",
|
|
@@ -2353,7 +2459,7 @@ function appendLog(logFile, text) {
|
|
|
2353
2459
|
}
|
|
2354
2460
|
function onWorkflowStart(ctx, s) {
|
|
2355
2461
|
mkdirSync3(ctx.logDir, { recursive: true });
|
|
2356
|
-
const logFile =
|
|
2462
|
+
const logFile = join4(ctx.logDir, `${ctx.ts}_${ctx.slug}.log`);
|
|
2357
2463
|
writeFileSync3(
|
|
2358
2464
|
logFile,
|
|
2359
2465
|
`# Execution Log
|
|
@@ -2450,6 +2556,7 @@ function reduce(ctx, s, event) {
|
|
|
2450
2556
|
case "log":
|
|
2451
2557
|
return onLogMessage(s, event.level, event.text);
|
|
2452
2558
|
case "workflow:complete":
|
|
2559
|
+
case "workflow:cancelled":
|
|
2453
2560
|
return onWorkflowComplete(ctx, s);
|
|
2454
2561
|
default:
|
|
2455
2562
|
return s;
|
|
@@ -2481,25 +2588,10 @@ async function* withLogger(gen, logger2) {
|
|
|
2481
2588
|
}
|
|
2482
2589
|
}
|
|
2483
2590
|
|
|
2484
|
-
// src/types.ts
|
|
2485
|
-
var InterjectChannel = class {
|
|
2486
|
-
_queue = [];
|
|
2487
|
-
/** Called by the TUI when the user submits an interjection message. */
|
|
2488
|
-
interject(message) {
|
|
2489
|
-
this._queue.push(message);
|
|
2490
|
-
}
|
|
2491
|
-
/** Drains and returns any queued messages (for non-Claude steps to consume). */
|
|
2492
|
-
consumeQueue() {
|
|
2493
|
-
const q = this._queue.slice();
|
|
2494
|
-
this._queue = [];
|
|
2495
|
-
return q;
|
|
2496
|
-
}
|
|
2497
|
-
};
|
|
2498
|
-
|
|
2499
2591
|
// src/index.ts
|
|
2500
2592
|
var CURRENT_VERSION = JSON.parse(
|
|
2501
2593
|
readFileSync6(
|
|
2502
|
-
|
|
2594
|
+
join5(dirname4(fileURLToPath2(import.meta.url)), "../package.json"),
|
|
2503
2595
|
"utf-8"
|
|
2504
2596
|
)
|
|
2505
2597
|
).version;
|
|
@@ -2558,6 +2650,7 @@ Options:
|
|
|
2558
2650
|
--ci Headless mode \u2014 print events as NDJSON, no TUI
|
|
2559
2651
|
--step <name|index> Run only the named step or step at 1-based index
|
|
2560
2652
|
--from-step <n> Resume from step n (e.g. 3, 3.2, 2.5.4.3 \u2014 1-based path)
|
|
2653
|
+
--var KEY=VALUE Override or supply a workflow var at runtime (repeatable)
|
|
2561
2654
|
--help, -h Show this help
|
|
2562
2655
|
|
|
2563
2656
|
Commands:
|
|
@@ -2599,6 +2692,18 @@ YAML \u2014 script step fields (type: script | command, or inferred when command
|
|
|
2599
2692
|
self_healing bool On failure, Claude diagnoses and fixes iteratively
|
|
2600
2693
|
up to 5 attempts with accumulated context (default: false)
|
|
2601
2694
|
max_healing_attempts int Override max self-healing retries (default: 5)
|
|
2695
|
+
timeout_seconds number Kill the step and fail with exit code 3 after N seconds
|
|
2696
|
+
|
|
2697
|
+
Cancellation:
|
|
2698
|
+
Write a .executant-cancel file in the working directory to stop execution
|
|
2699
|
+
cleanly between steps (exit code 4). The file is deleted automatically.
|
|
2700
|
+
|
|
2701
|
+
Exit codes:
|
|
2702
|
+
0 All steps completed successfully
|
|
2703
|
+
1 A step failed at runtime
|
|
2704
|
+
2 YAML or variable validation error
|
|
2705
|
+
3 A step timed out (timeout_seconds exceeded)
|
|
2706
|
+
4 Cancelled via .executant-cancel file
|
|
2602
2707
|
|
|
2603
2708
|
YAML \u2014 log step fields (type: log, or inferred when message is present and prompt is absent):
|
|
2604
2709
|
message string Text to emit as a progress marker
|
|
@@ -2624,6 +2729,7 @@ Example:
|
|
|
2624
2729
|
var ciMode = false;
|
|
2625
2730
|
var stepFilter;
|
|
2626
2731
|
var fromStep;
|
|
2732
|
+
var cliVars = {};
|
|
2627
2733
|
var positional = [];
|
|
2628
2734
|
for (let i = 0; i < rawArgs.length; i++) {
|
|
2629
2735
|
const a = rawArgs[i];
|
|
@@ -2649,6 +2755,18 @@ for (let i = 0; i < rawArgs.length; i++) {
|
|
|
2649
2755
|
process.exit(1);
|
|
2650
2756
|
}
|
|
2651
2757
|
fromStep = parts;
|
|
2758
|
+
} else if (a === "--var") {
|
|
2759
|
+
if (!rawArgs[i + 1]) {
|
|
2760
|
+
console.error("--var requires a KEY=VALUE argument");
|
|
2761
|
+
process.exit(1);
|
|
2762
|
+
}
|
|
2763
|
+
const pair = rawArgs[++i];
|
|
2764
|
+
const eq = pair.indexOf("=");
|
|
2765
|
+
if (eq <= 0) {
|
|
2766
|
+
console.error(`--var value must be KEY=VALUE, got: ${pair}`);
|
|
2767
|
+
process.exit(1);
|
|
2768
|
+
}
|
|
2769
|
+
cliVars[pair.slice(0, eq)] = pair.slice(eq + 1);
|
|
2652
2770
|
} else {
|
|
2653
2771
|
positional.push(a);
|
|
2654
2772
|
}
|
|
@@ -2660,12 +2778,21 @@ if (!filePath) {
|
|
|
2660
2778
|
}
|
|
2661
2779
|
var workflow;
|
|
2662
2780
|
try {
|
|
2663
|
-
workflow = loadWorkflow(filePath);
|
|
2781
|
+
workflow = loadWorkflow(filePath, cliVars);
|
|
2664
2782
|
} catch (err) {
|
|
2665
2783
|
console.error(getErrorMessage(err));
|
|
2666
|
-
process.exit(
|
|
2784
|
+
process.exit(2);
|
|
2667
2785
|
}
|
|
2668
|
-
var
|
|
2786
|
+
var localDir = findExecutantLocalDir(dirname4(resolve3(filePath)));
|
|
2787
|
+
if (localDir) {
|
|
2788
|
+
mkdirSync4(join5(localDir, "tasks", "todo"), { recursive: true });
|
|
2789
|
+
mkdirSync4(join5(localDir, "tasks", "done"), { recursive: true });
|
|
2790
|
+
}
|
|
2791
|
+
var options = {
|
|
2792
|
+
stepFilter,
|
|
2793
|
+
fromStep,
|
|
2794
|
+
workDir: dirname4(resolve3(filePath))
|
|
2795
|
+
};
|
|
2669
2796
|
var channel = new InterjectChannel();
|
|
2670
2797
|
var rawEvents = runWorkflow(workflow, options, channel);
|
|
2671
2798
|
var logger = createLogger(resolveLogDir(filePath), workflow.goal);
|
|
@@ -2680,11 +2807,17 @@ function errorReplacer(_key, value) {
|
|
|
2680
2807
|
if (ciMode) {
|
|
2681
2808
|
(async () => {
|
|
2682
2809
|
for await (const event of events) {
|
|
2683
|
-
|
|
2810
|
+
const line = JSON.stringify(event, errorReplacer) + "\n";
|
|
2811
|
+
if (event.type === "workflow:cancelled") {
|
|
2812
|
+
process.stdout.write(line, () => process.exit(4));
|
|
2813
|
+
return;
|
|
2814
|
+
}
|
|
2815
|
+
process.stdout.write(line);
|
|
2684
2816
|
}
|
|
2685
2817
|
})().catch((err) => {
|
|
2818
|
+
const code = err instanceof TimeoutError ? 3 : 1;
|
|
2686
2819
|
console.error(err);
|
|
2687
|
-
process.exit(
|
|
2820
|
+
process.exit(code);
|
|
2688
2821
|
});
|
|
2689
2822
|
} else {
|
|
2690
2823
|
render(
|