executant 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js ADDED
@@ -0,0 +1,2252 @@
1
+ #!/usr/bin/env node
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropNames = Object.getOwnPropertyNames;
4
+ var __esm = (fn, res) => function __init() {
5
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
6
+ };
7
+ var __export = (target, all) => {
8
+ for (var name in all)
9
+ __defProp(target, name, { get: all[name], enumerable: true });
10
+ };
11
+
12
+ // src/update.ts
13
+ var update_exports = {};
14
+ __export(update_exports, {
15
+ checkForUpdate: () => checkForUpdate,
16
+ compareSemver: () => compareSemver,
17
+ doUpdate: () => doUpdate,
18
+ parseVersionsFromGitOutput: () => parseVersionsFromGitOutput
19
+ });
20
+ import { exec as exec2 } from "node:child_process";
21
+ import { promisify as promisify2 } from "node:util";
22
+ async function checkForUpdate(currentVersion) {
23
+ try {
24
+ const { stdout } = await execPromise2(
25
+ "git ls-remote --tags https://github.com/coston/executant.git",
26
+ { timeout: 5e3 }
27
+ );
28
+ const versions = parseVersionsFromGitOutput(stdout);
29
+ const latest = versions.sort(compareSemver).at(-1);
30
+ return latest && isNewer(latest, currentVersion) ? latest : null;
31
+ } catch {
32
+ return null;
33
+ }
34
+ }
35
+ async function doUpdate() {
36
+ await execPromise2("npm install -g github:coston/executant");
37
+ }
38
+ function parseVersionsFromGitOutput(stdout) {
39
+ const versions = [];
40
+ for (const line of stdout.split("\n")) {
41
+ const m = line.match(/refs\/tags\/v?(\d+\.\d+\.\d+)$/);
42
+ if (m) versions.push(m[1]);
43
+ }
44
+ return versions;
45
+ }
46
+ function compareSemver(a, b) {
47
+ const pa = a.split(".").map(Number);
48
+ const pb = b.split(".").map(Number);
49
+ for (let i = 0; i < 3; i++) {
50
+ if ((pa[i] ?? 0) !== (pb[i] ?? 0)) return (pa[i] ?? 0) - (pb[i] ?? 0);
51
+ }
52
+ return 0;
53
+ }
54
+ function isNewer(latest, current) {
55
+ return compareSemver(latest, current) > 0;
56
+ }
57
+ var execPromise2;
58
+ var init_update = __esm({
59
+ "src/update.ts"() {
60
+ "use strict";
61
+ execPromise2 = promisify2(exec2);
62
+ }
63
+ });
64
+
65
+ // src/index.ts
66
+ import React3 from "react";
67
+ import { render } from "ink";
68
+ import { readFileSync as readFileSync5 } from "node:fs";
69
+ import { dirname as dirname5, join as join5 } from "node:path";
70
+ import { fileURLToPath as fileURLToPath4 } from "node:url";
71
+
72
+ // src/load-workflow.ts
73
+ import { readFileSync } from "node:fs";
74
+ import { load as parseYaml } from "js-yaml";
75
+ import { z } from "zod";
76
+ var RawStepSchema = z.object({
77
+ name: z.string(),
78
+ type: z.enum(["prompt", "script", "log", "command"]).optional(),
79
+ prompt: z.string().optional(),
80
+ command: z.string().optional(),
81
+ message: z.string().optional(),
82
+ continue_on_error: z.boolean().optional(),
83
+ self_healing: z.boolean().optional(),
84
+ max_healing_attempts: z.number().int().positive().optional(),
85
+ output: z.string().optional(),
86
+ llm_as_judge: z.boolean().optional(),
87
+ allowed_tools: z.array(z.string()).optional(),
88
+ forEach: z.union([z.array(z.string()), z.string()]).optional(),
89
+ context: z.array(z.string()).optional()
90
+ });
91
+ var RawWorkflowSchema = z.object({
92
+ goal: z.string(),
93
+ steps: z.array(RawStepSchema),
94
+ vars: z.record(z.string(), z.string()).optional(),
95
+ self_improve: z.boolean().optional()
96
+ });
97
+ function loadWorkflow(filePath2) {
98
+ let raw;
99
+ try {
100
+ raw = readFileSync(filePath2, "utf8");
101
+ } catch (err) {
102
+ const msg = err instanceof Error ? err.message : String(err);
103
+ throw new Error(`Cannot read workflow file "${filePath2}": ${msg}`);
104
+ }
105
+ let doc;
106
+ try {
107
+ doc = RawWorkflowSchema.parse(parseYaml(raw));
108
+ } catch (err) {
109
+ const detail = err instanceof z.ZodError ? err.errors.map((e) => ` ${e.path.join(".")}: ${e.message}`).join("\n") : String(err);
110
+ throw new Error(`Invalid workflow file "${filePath2}":
111
+ ${detail}`);
112
+ }
113
+ const vars = doc.vars ?? {};
114
+ return {
115
+ goal: doc.goal,
116
+ vars,
117
+ selfImprove: doc.self_improve,
118
+ tasks: doc.steps.map((step) => convertStep(step, vars))
119
+ };
120
+ }
121
+ function convertStep(step, vars) {
122
+ const name = step.name;
123
+ const continueOnError = step.continue_on_error ?? false;
124
+ if (step.forEach !== void 0) {
125
+ const { forEach: _, ...innerStep } = step;
126
+ return {
127
+ type: "forEach",
128
+ name,
129
+ continueOnError,
130
+ forEach: step.forEach,
131
+ inner: convertInnerStep(innerStep, vars, name, continueOnError)
132
+ };
133
+ }
134
+ return convertInnerStep(step, vars, name, continueOnError);
135
+ }
136
+ function convertInnerStep(step, vars, name, continueOnError) {
137
+ const effectiveType = step.type ?? inferType(step);
138
+ switch (effectiveType) {
139
+ case "script":
140
+ case "command": {
141
+ if (!step.command) throw new Error(`Step "${name}" has type script but no command`);
142
+ return {
143
+ type: "command",
144
+ name,
145
+ command: substituteVars(step.command, vars, name, "command"),
146
+ continueOnError,
147
+ selfHealing: step.self_healing === true,
148
+ maxHealingAttempts: step.max_healing_attempts,
149
+ ...step.output && { output: resolveOutputFile(step.output, vars, name) }
150
+ };
151
+ }
152
+ case "log": {
153
+ const message = step.message ?? step.prompt ?? name;
154
+ return {
155
+ type: "log",
156
+ name,
157
+ message: substituteVars(message, vars, name, "message"),
158
+ continueOnError
159
+ };
160
+ }
161
+ case "prompt": {
162
+ if (!step.prompt) throw new Error(`Step "${name}" has type prompt but no prompt field`);
163
+ const contextFiles = resolveContextFiles(step.context, vars, name);
164
+ return {
165
+ type: "claude",
166
+ name,
167
+ prompt: substituteVars(step.prompt, vars, name, "prompt"),
168
+ continueOnError,
169
+ llmAsJudge: step.llm_as_judge,
170
+ allowedTools: step.allowed_tools,
171
+ ...contextFiles.length > 0 && { contextFiles }
172
+ };
173
+ }
174
+ default:
175
+ throw new Error(`Step "${name}" has unknown type: "${effectiveType}"`);
176
+ }
177
+ }
178
+ function inferType(step) {
179
+ if (step.command) return "script";
180
+ if (step.message && !step.prompt) return "log";
181
+ return "prompt";
182
+ }
183
+ function resolveVarPath(varName, vars, stepName, label) {
184
+ if (!(varName in vars)) {
185
+ throw new Error(`Step "${stepName}" ${label} references undefined var "${varName}" \u2014 add it to the vars section`);
186
+ }
187
+ return vars[varName];
188
+ }
189
+ function resolveContextFiles(contextVarNames, vars, stepName) {
190
+ if (!contextVarNames || contextVarNames.length === 0) return [];
191
+ return contextVarNames.map((varName) => resolveVarPath(varName, vars, stepName, "context"));
192
+ }
193
+ function resolveOutputFile(varName, vars, stepName) {
194
+ return resolveVarPath(varName, vars, stepName, "output");
195
+ }
196
+ function substituteVars(text, vars, stepName, field) {
197
+ const result = Object.entries(vars).reduce(
198
+ (acc, [key, value]) => acc.replaceAll(`{{${key}}}`, value),
199
+ text
200
+ );
201
+ const unknownTokens = [...result.matchAll(/\{\{(\w+)\}\}/g)].map((m) => m[1]).filter((key) => key !== "item");
202
+ if (unknownTokens.length > 0) {
203
+ throw new Error(
204
+ `Step "${stepName}" ${field} contains unknown placeholder "{{${unknownTokens[0]}}}" \u2014 add it to the vars section`
205
+ );
206
+ }
207
+ return result;
208
+ }
209
+
210
+ // src/runner.ts
211
+ import { exec } from "node:child_process";
212
+ import { mkdirSync, readFileSync as readFileSync2, writeFileSync } from "node:fs";
213
+ import { dirname, join } from "node:path";
214
+ import { fileURLToPath } from "node:url";
215
+ import { promisify } from "node:util";
216
+ import { z as z2 } from "zod";
217
+
218
+ // src/tasks/command.ts
219
+ import { spawn } from "node:child_process";
220
+
221
+ // src/tasks/stream.ts
222
+ var AsyncQueue = class {
223
+ buf = [];
224
+ waiter = null;
225
+ push(item) {
226
+ if (this.waiter) {
227
+ const w = this.waiter;
228
+ this.waiter = null;
229
+ w(item);
230
+ } else {
231
+ this.buf.push(item);
232
+ }
233
+ }
234
+ next() {
235
+ if (this.buf.length > 0) return Promise.resolve(this.buf.shift());
236
+ return new Promise((resolve4) => {
237
+ this.waiter = resolve4;
238
+ });
239
+ }
240
+ async *[Symbol.asyncIterator]() {
241
+ while (true) {
242
+ const item = await this.next();
243
+ if ("done" in item) return;
244
+ if ("error" in item) throw item.error;
245
+ yield item.value;
246
+ }
247
+ }
248
+ };
249
+ async function* mergeStreamsToLines(...streams) {
250
+ const q = new AsyncQueue();
251
+ let pending = streams.length;
252
+ for (const stream of streams) {
253
+ let buf = "";
254
+ stream.on("data", (chunk) => {
255
+ buf += chunk.toString();
256
+ const parts = buf.split("\n");
257
+ buf = parts.pop() ?? "";
258
+ for (const part of parts) q.push({ value: part });
259
+ });
260
+ stream.on("end", () => {
261
+ if (buf) {
262
+ q.push({ value: buf });
263
+ buf = "";
264
+ }
265
+ pending--;
266
+ if (pending === 0) q.push({ done: true });
267
+ });
268
+ stream.on("error", (err) => q.push({ error: err }));
269
+ }
270
+ yield* q;
271
+ }
272
+ function waitForExit(proc) {
273
+ return new Promise((resolve4, reject) => {
274
+ proc.on("close", (code) => resolve4(code ?? 0));
275
+ proc.on("error", reject);
276
+ });
277
+ }
278
+
279
+ // src/tasks/command.ts
280
+ var CommandError = class extends Error {
281
+ constructor(exitCode, command, message) {
282
+ super(message ?? `Command "${command}" exited with code ${exitCode}`);
283
+ this.exitCode = exitCode;
284
+ this.command = command;
285
+ this.name = "CommandError";
286
+ }
287
+ exitCode;
288
+ command;
289
+ };
290
+ async function* runCommand(task) {
291
+ yield { type: "log", level: "info", text: `$ ${task.command}` };
292
+ const proc = spawn("bash", ["-c", task.command], {
293
+ stdio: ["ignore", "pipe", "pipe"]
294
+ });
295
+ for await (const line of mergeStreamsToLines(proc.stdout, proc.stderr)) {
296
+ yield { type: "output:text", index: -1, text: line };
297
+ }
298
+ const code = await waitForExit(proc);
299
+ if (code !== 0) {
300
+ throw new CommandError(code, task.command, `Command "${task.name}" exited with code ${code}`);
301
+ }
302
+ }
303
+
304
+ // src/tasks/claude.ts
305
+ import { execSync, spawn as spawn2 } from "node:child_process";
306
+ import { zodToJsonSchema } from "zod-to-json-schema";
307
+
308
+ // src/lib/utils.ts
309
+ function findOutermostBraces(text) {
310
+ const start = text.indexOf("{");
311
+ if (start === -1) return null;
312
+ let depth = 0;
313
+ for (let i = start; i < text.length; i++) {
314
+ if (text[i] === "{") depth++;
315
+ else if (text[i] === "}" && --depth === 0) return { start, end: i };
316
+ }
317
+ return null;
318
+ }
319
+ function extractJsonObject(text) {
320
+ const fenceMatch = text.match(/```(?:json)?\s*\n([\s\S]*?)\n```/);
321
+ if (fenceMatch) return fenceMatch[1].trim();
322
+ const bounds = findOutermostBraces(text);
323
+ return bounds ? text.slice(bounds.start, bounds.end + 1) : text.trim();
324
+ }
325
+ function slugify(text, maxLen = 20) {
326
+ return text.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, maxLen).replace(/-+$/, "");
327
+ }
328
+ function formatTimestamp(d) {
329
+ const p = (n) => String(n).padStart(2, "0");
330
+ return `${d.getFullYear()}${p(d.getMonth() + 1)}${p(d.getDate())}-${p(d.getHours())}${p(d.getMinutes())}${p(d.getSeconds())}`;
331
+ }
332
+ function timestamp() {
333
+ return formatTimestamp(/* @__PURE__ */ new Date());
334
+ }
335
+
336
+ // src/tasks/claude.ts
337
+ var DEFAULT_TOOLS = ["Read", "Edit", "Write", "Bash", "Glob", "Grep"];
338
+ function resolveClaudePath() {
339
+ try {
340
+ return execSync("which claude", { env: process.env }).toString().trim();
341
+ } catch {
342
+ throw new Error(
343
+ "claude CLI not found. Ensure it is installed and in PATH.\n brew install claude OR npm install -g @anthropic-ai/claude-code"
344
+ );
345
+ }
346
+ }
347
+ async function* runClaude(task) {
348
+ const allowedTools = task.allowedTools ?? DEFAULT_TOOLS;
349
+ yield {
350
+ type: "log",
351
+ level: "info",
352
+ text: `claude -p "${task.prompt.slice(0, 60).replace(/\n/g, " ")}\u2026"`
353
+ };
354
+ const permissionMode = task.permissionMode ?? "bypassPermissions";
355
+ const args = [
356
+ "--print",
357
+ task.prompt,
358
+ "--output-format",
359
+ "stream-json",
360
+ "--verbose",
361
+ "--allowedTools",
362
+ allowedTools.join(","),
363
+ "--permission-mode",
364
+ permissionMode,
365
+ ...task.model ? ["--model", task.model] : [],
366
+ ...task.appendSystemPrompt ? ["--append-system-prompt", task.appendSystemPrompt] : [],
367
+ ...task.jsonSchema ? ["--json-schema", JSON.stringify(task.jsonSchema)] : []
368
+ ];
369
+ const claudeBin = resolveClaudePath();
370
+ let proc;
371
+ try {
372
+ proc = spawn2(claudeBin, args, {
373
+ stdio: ["ignore", "pipe", "pipe"],
374
+ env: { ...process.env }
375
+ });
376
+ } catch (err) {
377
+ const msg = err instanceof Error ? err.message : String(err);
378
+ throw new Error(`Failed to spawn claude (${claudeBin}): ${msg}`);
379
+ }
380
+ const cleanup = () => {
381
+ try {
382
+ proc.kill();
383
+ } catch {
384
+ }
385
+ };
386
+ process.once("SIGTERM", cleanup);
387
+ process.once("SIGHUP", cleanup);
388
+ const plainLines = [];
389
+ try {
390
+ for await (const line of mergeStreamsToLines(proc.stdout, proc.stderr)) {
391
+ if (!line.trim()) continue;
392
+ try {
393
+ const msg = JSON.parse(line);
394
+ yield* parseClaudeMessage(msg);
395
+ } catch {
396
+ const clean = stripAnsi(line);
397
+ if (clean.trim()) {
398
+ plainLines.push(clean);
399
+ yield { type: "output:text", index: -1, text: clean };
400
+ }
401
+ }
402
+ }
403
+ const code = await waitForExit(proc);
404
+ if (code !== 0) throw buildExitError(code, plainLines);
405
+ } finally {
406
+ process.off("SIGTERM", cleanup);
407
+ process.off("SIGHUP", cleanup);
408
+ }
409
+ }
410
+ function* parseClaudeMessage(msg) {
411
+ if (!isObject(msg)) return;
412
+ if (msg["type"] === "assistant") {
413
+ const content = getArray(msg, "message", "content");
414
+ for (const block of content) {
415
+ if (!isObject(block)) continue;
416
+ if (block["type"] === "text") {
417
+ const text = getString(block, "text");
418
+ if (text) yield { type: "output:text", index: -1, text };
419
+ } else if (block["type"] === "tool_use") {
420
+ const tool = getString(block, "name") ?? "Unknown";
421
+ const input = isObject(block["input"]) ? block["input"] : {};
422
+ yield { type: "output:tool", index: -1, tool, input };
423
+ }
424
+ }
425
+ } else if (msg["type"] === "result") {
426
+ const cost = msg["total_cost_usd"];
427
+ if (typeof cost === "number") {
428
+ yield { type: "output:cost", usd: cost };
429
+ }
430
+ if (msg["structured_output"] != null) {
431
+ yield { type: "output:structured", data: msg["structured_output"] };
432
+ }
433
+ }
434
+ }
435
+ function buildExitError(code, plainLines) {
436
+ const detail = plainLines.length > 0 ? `
437
+ ${plainLines.join("\n")}` : "";
438
+ return new Error(`claude exited with code ${code}${detail}`);
439
+ }
440
+ var ANSI_RE = /\x1B\[[0-9;]*[A-Za-z]|\x1B\][^\x07]*\x07|\r/g;
441
+ function stripAnsi(s) {
442
+ return s.replace(ANSI_RE, "");
443
+ }
444
+ function isObject(v) {
445
+ return typeof v === "object" && v !== null && !Array.isArray(v);
446
+ }
447
+ function getArray(obj, ...keys) {
448
+ const result = keys.reduce((cur, k) => isObject(cur) ? cur[k] : null, obj);
449
+ return Array.isArray(result) ? result : [];
450
+ }
451
+ function getString(obj, key) {
452
+ const v = obj[key];
453
+ return typeof v === "string" ? v : void 0;
454
+ }
455
+ async function runClaudeStructured(task, schema) {
456
+ const jsonSchema = zodToJsonSchema(schema);
457
+ let structuredOutput;
458
+ const lines = [];
459
+ for await (const event of runClaude({ ...task, jsonSchema })) {
460
+ if (event.type === "output:structured") structuredOutput = event.data;
461
+ else if (event.type === "output:text") lines.push(event.text);
462
+ }
463
+ if (structuredOutput === void 0 && process.env["NODE_ENV"] !== "test") {
464
+ console.warn("[executant] runClaudeStructured: no output:structured event \u2014 falling back to text parsing");
465
+ }
466
+ const data = structuredOutput ?? JSON.parse(extractJsonObject(lines.join("").trim()));
467
+ return schema.parse(data);
468
+ }
469
+
470
+ // src/runner.ts
471
+ var PROMPTS_DIR = join(dirname(fileURLToPath(import.meta.url)), "prompts");
472
+ var JUDGE_RETRY_CONTEXT = loadPrompt("judge-retry-context");
473
+ var SELF_HEALING_PROMPT = loadPrompt("self-healing-fix");
474
+ var JUDGE_EVALUATION_PROMPT = loadPrompt("judge-evaluation");
475
+ var execPromise = promisify(exec);
476
+ var MAX_JUDGE_RETRIES = 5;
477
+ var MAX_HEALING_ATTEMPTS = 5;
478
+ var JudgeOutputSchema = z2.object({
479
+ pass: z2.boolean(),
480
+ reasoning: z2.string().optional(),
481
+ feedback: z2.string()
482
+ });
483
+ async function* runWorkflow(workflow2, options2 = {}) {
484
+ const workflowStart = Date.now();
485
+ yield { type: "workflow:start", workflow: workflow2 };
486
+ for (const [i, task] of workflow2.tasks.entries()) {
487
+ const stepNumber = i + 1;
488
+ if (options2.stepFilter !== void 0) {
489
+ const matchByIndex = /^\d+$/.test(options2.stepFilter) && parseInt(options2.stepFilter, 10) === stepNumber;
490
+ const matchByName = task.name === options2.stepFilter;
491
+ if (!matchByIndex && !matchByName) {
492
+ yield { type: "step:skip", index: i, name: task.name };
493
+ continue;
494
+ }
495
+ }
496
+ if (options2.fromStep !== void 0 && stepNumber < options2.fromStep) {
497
+ yield { type: "step:skip", index: i, name: task.name };
498
+ continue;
499
+ }
500
+ const stepStart = Date.now();
501
+ yield { type: "step:start", index: i, name: task.name };
502
+ try {
503
+ for await (const event of runStep(task)) {
504
+ if (event.type === "step:iteration" || event.type === "output:text" || event.type === "output:tool") {
505
+ yield { ...event, index: i };
506
+ } else {
507
+ yield event;
508
+ }
509
+ }
510
+ yield {
511
+ type: "step:complete",
512
+ index: i,
513
+ name: task.name,
514
+ durationMs: Date.now() - stepStart
515
+ };
516
+ } catch (err) {
517
+ const error = err instanceof Error ? err : new Error(String(err));
518
+ yield { type: "step:error", index: i, name: task.name, error };
519
+ if (!task.continueOnError) throw error;
520
+ }
521
+ }
522
+ yield {
523
+ type: "workflow:complete",
524
+ workflow: workflow2,
525
+ durationMs: Date.now() - workflowStart
526
+ };
527
+ }
528
+ async function* runStep(task) {
529
+ switch (task.type) {
530
+ case "log":
531
+ yield* runLog(task);
532
+ break;
533
+ case "command": {
534
+ const gen = task.selfHealing ? runCommandWithHealing(task) : runCommand(task);
535
+ if (task.output) {
536
+ const lines = [];
537
+ yield* collectLines(gen, lines);
538
+ mkdirSync(dirname(task.output), { recursive: true });
539
+ writeFileSync(task.output, lines.join("\n"), "utf8");
540
+ } else {
541
+ yield* gen;
542
+ }
543
+ break;
544
+ }
545
+ case "claude": {
546
+ const expanded = expandContext(task);
547
+ yield* expanded.llmAsJudge ? runClaudeWithJudge(expanded) : runClaude(expanded);
548
+ break;
549
+ }
550
+ case "forEach":
551
+ yield* runForEach(task);
552
+ break;
553
+ default: {
554
+ const _ = task;
555
+ throw new Error(`Unknown task type: ${JSON.stringify(_)}`);
556
+ }
557
+ }
558
+ }
559
+ async function* runLog(task) {
560
+ yield { type: "output:text", index: -1, text: task.message };
561
+ }
562
+ async function* runForEach(task) {
563
+ const items = await resolveItems(task.forEach);
564
+ const total = items.length;
565
+ for (const [i, item] of items.entries()) {
566
+ yield { type: "step:iteration", index: -1, item, iteration: i + 1, total };
567
+ const substituted = substituteItem(task.inner, item);
568
+ yield* runStep(substituted);
569
+ }
570
+ }
571
+ async function resolveItems(forEach) {
572
+ if (Array.isArray(forEach)) return forEach.filter(Boolean);
573
+ try {
574
+ const { stdout } = await execPromise(forEach, { shell: "/bin/sh", timeout: 3e4 });
575
+ return stdout.split("\n").filter((l) => l.trim().length > 0);
576
+ } catch (err) {
577
+ const msg = err instanceof Error ? err.message : String(err);
578
+ throw new Error(`forEach shell command failed: ${msg}
579
+ Command: ${forEach}`);
580
+ }
581
+ }
582
+ function substituteItem(task, item) {
583
+ const sub = (s) => s.replace(/\{\{item\}\}/g, item);
584
+ switch (task.type) {
585
+ case "command":
586
+ return { ...task, name: sub(task.name), command: sub(task.command) };
587
+ case "claude":
588
+ return { ...task, name: sub(task.name), prompt: sub(task.prompt), allowedTools: task.allowedTools?.map(sub) };
589
+ case "log":
590
+ return { ...task, name: sub(task.name), message: sub(task.message) };
591
+ default: {
592
+ const _ = task;
593
+ throw new Error(`Unknown inner task type: ${JSON.stringify(_)}`);
594
+ }
595
+ }
596
+ }
597
+ async function* runCommandWithHealing(task) {
598
+ const maxAttempts = task.maxHealingAttempts ?? MAX_HEALING_ATTEMPTS;
599
+ const attemptHistory = [];
600
+ for (let attempt = 0; attempt < maxAttempts; attempt++) {
601
+ const lines = [];
602
+ try {
603
+ yield* collectLines(runCommand(task), lines);
604
+ if (attempt > 0) {
605
+ yield { type: "log", level: "info", text: `[self-healing] Command passed after ${attempt + 1} attempts` };
606
+ }
607
+ return;
608
+ } catch (err) {
609
+ const exitCode = err instanceof CommandError ? err.exitCode : 1;
610
+ const output = lines.join("\n");
611
+ const remaining = maxAttempts - attempt - 1;
612
+ if (remaining === 0) {
613
+ yield { type: "log", level: "warn", text: `[self-healing] Exhausted ${maxAttempts} attempts` };
614
+ throw new Error(
615
+ `Step "${task.name}" failed after ${maxAttempts} self-healing attempts (last exit code: ${exitCode})`
616
+ );
617
+ }
618
+ yield {
619
+ type: "log",
620
+ level: "warn",
621
+ text: `[self-healing] Attempt ${attempt + 1}/${maxAttempts} failed (exit ${exitCode}), invoking Claude to fix\u2026`
622
+ };
623
+ const historyBlock = buildAttemptHistory(attemptHistory);
624
+ const healPrompt = buildHealingPrompt(task.command, exitCode, output, historyBlock);
625
+ const healTask = {
626
+ type: "claude",
627
+ name: `${task.name}:heal-${attempt + 1}`,
628
+ prompt: healPrompt,
629
+ allowedTools: ["Bash", "Read", "Write", "Edit", "Glob", "Grep"]
630
+ };
631
+ const toolCalls = [];
632
+ const claudeLines = [];
633
+ for await (const event of runClaude(healTask)) {
634
+ if (event.type === "output:text") claudeLines.push(event.text);
635
+ else if (event.type === "output:tool") toolCalls.push(formatToolCall(event.tool, event.input));
636
+ yield event;
637
+ }
638
+ attemptHistory.push({
639
+ fixSummary: buildFixSummary(toolCalls, claudeLines),
640
+ exitCode,
641
+ cmdOutput: output
642
+ });
643
+ yield { type: "log", level: "info", text: `[self-healing] Re-running command (${remaining} attempt(s) left)\u2026` };
644
+ }
645
+ }
646
+ }
647
+ async function* runClaudeWithJudge(task) {
648
+ let judgeContext = "";
649
+ for (let attempt = 0; attempt < MAX_JUDGE_RETRIES; attempt++) {
650
+ const prompt = attempt === 0 ? task.prompt : `${task.prompt}
651
+
652
+ ${JUDGE_RETRY_CONTEXT.replace("{{FEEDBACK}}", judgeContext)}`;
653
+ const lines = [];
654
+ yield* collectLines(runClaude({ ...task, prompt }), lines);
655
+ yield { type: "log", level: "info", text: `[judge] Evaluating "${task.name}"\u2026` };
656
+ const verdict = await evaluateWithJudge(task.name, task.prompt, lines.join("\n"));
657
+ if (verdict.pass) {
658
+ yield { type: "log", level: "info", text: "[judge] PASS" };
659
+ return;
660
+ }
661
+ judgeContext = verdict.feedback;
662
+ yield { type: "log", level: "warn", text: `[judge] FAIL \u2014 ${verdict.feedback}` };
663
+ const remaining = MAX_JUDGE_RETRIES - attempt - 1;
664
+ if (remaining === 0) {
665
+ throw new Error(
666
+ `Step "${task.name}" failed judge evaluation after ${MAX_JUDGE_RETRIES} attempts`
667
+ );
668
+ }
669
+ yield { type: "log", level: "info", text: `[judge] Retrying (${remaining} attempt(s) left)\u2026` };
670
+ }
671
+ }
672
+ async function evaluateWithJudge(stepName, stepInstructions, output) {
673
+ const result = await runClaudeStructured(
674
+ {
675
+ type: "claude",
676
+ name: `judge:${stepName}`,
677
+ prompt: buildJudgePrompt(stepName, stepInstructions, output),
678
+ allowedTools: [],
679
+ permissionMode: "default"
680
+ // judge only reads text — no tool access needed
681
+ },
682
+ JudgeOutputSchema
683
+ );
684
+ return { pass: result.pass, feedback: result.feedback };
685
+ }
686
+ async function* collectLines(gen, lines) {
687
+ for await (const event of gen) {
688
+ if (event.type === "output:text") lines.push(event.text);
689
+ yield event;
690
+ }
691
+ }
692
+ function readContextFile(filePath2) {
693
+ try {
694
+ return readFileSync2(filePath2, "utf8");
695
+ } catch (err) {
696
+ const msg = err instanceof Error ? err.message : String(err);
697
+ throw new Error(`Context file "${filePath2}" could not be read: ${msg}`);
698
+ }
699
+ }
700
+ function expandContext(task) {
701
+ if (!task.contextFiles || task.contextFiles.length === 0) return task;
702
+ const header = task.contextFiles.map((fp) => `### ${fp}
703
+ \`\`\`
704
+ ${readContextFile(fp)}
705
+ \`\`\``).join("\n\n");
706
+ return { ...task, prompt: `${header}
707
+
708
+ ${task.prompt}` };
709
+ }
710
+ function loadPrompt(name) {
711
+ const raw = readFileSync2(join(PROMPTS_DIR, `${name}.txt`), "utf8");
712
+ return raw.replace(/^(#[^\n]*\n)+\n?/, "");
713
+ }
714
+ function buildHealingPrompt(command, exitCode, output, attemptHistory) {
715
+ return SELF_HEALING_PROMPT.replaceAll("{{COMMAND}}", command).replaceAll("{{EXIT_CODE}}", String(exitCode)).replaceAll("{{OUTPUT}}", output).replaceAll("{{ATTEMPT_HISTORY}}", attemptHistory);
716
+ }
717
+ function buildJudgePrompt(stepName, instructions, output) {
718
+ return JUDGE_EVALUATION_PROMPT.replace("{{STEP_NAME}}", stepName).replace("{{STEP_INSTRUCTIONS}}", instructions).replace("{{OUTPUT}}", output);
719
+ }
720
+ function formatToolCall(tool, input) {
721
+ if (tool === "Edit" || tool === "Write") return `${tool}(${String(input["file_path"] ?? "")})`;
722
+ if (tool === "Bash") return `Bash(${String(input["command"] ?? "")})`;
723
+ return tool;
724
+ }
725
+ function buildFixSummary(toolCalls, claudeLines) {
726
+ if (toolCalls.length > 0) return toolCalls.join(", ");
727
+ return claudeLines.join(" ").trim() || "No changes made";
728
+ }
729
+ function buildAttemptHistory(attempts) {
730
+ if (attempts.length === 0) return "";
731
+ const blocks = attempts.map(
732
+ (a, i) => `--- Attempt ${i + 1} ---
733
+ Fix applied: ${a.fixSummary}
734
+ Result: Failed with exit code ${a.exitCode}
735
+ Output: ${a.cmdOutput}`
736
+ );
737
+ return `PREVIOUS ATTEMPTS:
738
+ ${blocks.join("\n\n")}`;
739
+ }
740
+
741
+ // src/index.ts
742
+ init_update();
743
+
744
+ // src/ui/App.tsx
745
+ import { useEffect as useEffect2, useReducer, useState } from "react";
746
+ import { Box as Box4, Text as Text4, useApp, useStdin } from "ink";
747
+
748
+ // src/ui/KeyboardHandler.tsx
749
+ import { useInput } from "ink";
750
+ function KeyboardHandler({ onExit }) {
751
+ useInput((input, key) => {
752
+ if (input === "q" || key.ctrl && input === "c") onExit();
753
+ });
754
+ return null;
755
+ }
756
+
757
+ // src/ui/formatTool.ts
758
+ function formatToolCall2(tool, input) {
759
+ switch (tool) {
760
+ case "Read":
761
+ case "Edit":
762
+ case "Write":
763
+ return `[${tool}] ${input["file_path"] ?? JSON.stringify(input)}`;
764
+ case "Bash":
765
+ return `[Bash] ${input["description"] ?? ""}
766
+ $ ${String(input["command"] ?? "").slice(0, 120)}`;
767
+ case "Glob":
768
+ return `[Glob] ${input["pattern"] ?? JSON.stringify(input)}`;
769
+ case "Grep":
770
+ return `[Grep] ${input["pattern"] ?? JSON.stringify(input)}`;
771
+ case "TodoWrite": {
772
+ const todos = input["todos"];
773
+ if (Array.isArray(todos)) {
774
+ const inProgress = todos.filter((t) => typeof t === "object" && t !== null && t["status"] === "in_progress").map((t) => String(t["content"] ?? ""));
775
+ if (inProgress.length > 0) return `[Task] ${inProgress.join(", ")}`;
776
+ }
777
+ return "";
778
+ }
779
+ case "Agent":
780
+ return `[Agent:${input["subagent_type"] ?? "?"}] ${input["description"] ?? ""}`;
781
+ default:
782
+ if (process.env["EXECUTANT_DEBUG"] === "1") {
783
+ return `[${tool}] ${JSON.stringify(input)}`;
784
+ }
785
+ return "";
786
+ }
787
+ }
788
+
789
+ // src/ui/reducer.ts
790
+ function buildInitialState(workflow2) {
791
+ return {
792
+ workflow: workflow2,
793
+ tasks: workflow2.tasks.map((task) => ({
794
+ task,
795
+ status: "pending",
796
+ lines: []
797
+ })),
798
+ currentIndex: 0,
799
+ startTime: Date.now(),
800
+ writtenFiles: []
801
+ };
802
+ }
803
+ function reducer(state, event) {
804
+ switch (event.type) {
805
+ case "workflow:start":
806
+ return { ...state, startTime: Date.now() };
807
+ case "workflow:complete":
808
+ return { ...state, endTime: Date.now() };
809
+ case "step:start":
810
+ return updateTask(state, event.index, {
811
+ status: "running",
812
+ startTime: Date.now()
813
+ });
814
+ case "step:complete":
815
+ return {
816
+ ...updateTask(state, event.index, {
817
+ status: "complete",
818
+ endTime: Date.now()
819
+ }),
820
+ currentIndex: event.index + 1
821
+ };
822
+ case "step:error":
823
+ return {
824
+ ...updateTask(state, event.index, {
825
+ status: "error",
826
+ endTime: Date.now(),
827
+ error: event.error
828
+ }),
829
+ currentIndex: event.index + 1
830
+ };
831
+ case "step:skip":
832
+ return {
833
+ ...updateTask(state, event.index, { status: "skipped" }),
834
+ currentIndex: event.index + 1
835
+ };
836
+ case "step:iteration":
837
+ return updateTask(state, event.index, {
838
+ iteration: { current: event.iteration, total: event.total, item: event.item }
839
+ });
840
+ case "output:text": {
841
+ const idx = event.index;
842
+ if (idx >= state.tasks.length) return state;
843
+ return appendLine(state, idx, event.text);
844
+ }
845
+ case "output:tool": {
846
+ const idx = event.index;
847
+ if (idx >= state.tasks.length) return state;
848
+ const formatted = formatToolCall2(event.tool, event.input);
849
+ const next = formatted ? appendLine(state, idx, formatted) : state;
850
+ if (event.tool === "Write" && typeof event.input["file_path"] === "string") {
851
+ return { ...next, writtenFiles: [...next.writtenFiles, event.input["file_path"]] };
852
+ }
853
+ return next;
854
+ }
855
+ case "output:cost":
856
+ return state;
857
+ // cost events are intentionally not shown in the TUI
858
+ case "output:structured":
859
+ return state;
860
+ // structured output is consumed by callers, not shown in the TUI
861
+ case "log": {
862
+ const idx = state.currentIndex;
863
+ if (idx >= state.tasks.length) return state;
864
+ return appendLine(state, idx, `[${event.level}] ${event.text}`);
865
+ }
866
+ default: {
867
+ const _ = event;
868
+ void _;
869
+ return state;
870
+ }
871
+ }
872
+ }
873
+ function updateTask(state, index, patch) {
874
+ const tasks = state.tasks.map((t, i) => i === index ? { ...t, ...patch } : t);
875
+ return { ...state, tasks };
876
+ }
877
+ function appendLine(state, index, line) {
878
+ const tasks = state.tasks.map((t, i) => {
879
+ if (i !== index) return t;
880
+ const lines = [...t.lines, line];
881
+ return { ...t, lines };
882
+ });
883
+ return { ...state, tasks };
884
+ }
885
+
886
+ // src/ui/TaskRow.tsx
887
+ import { Box, Text } from "ink";
888
+
889
+ // src/ui/utils.ts
890
+ var SPINNER = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
891
+ var EXIT_DELAY_MS = 300;
892
+ function formatHeaderElapsed(start, end) {
893
+ const ms = (end ?? Date.now()) - start;
894
+ return ms < 1e3 ? `${ms}ms` : `${(ms / 1e3).toFixed(1)}s`;
895
+ }
896
+ function formatTaskElapsed(start, end, status) {
897
+ if (!start) return "";
898
+ const ms = (end ?? Date.now()) - start;
899
+ if (status === "running" || status === "complete" || status === "error") return `${(ms / 1e3).toFixed(1)}s`;
900
+ return "";
901
+ }
902
+
903
+ // src/ui/theme.ts
904
+ import { createRequire } from "node:module";
905
+ import { oklchToHex } from "@coston/design-tokens";
906
+ var THEME_NAME = "purple-dark";
907
+ var _require = createRequire(import.meta.url);
908
+ var { themes } = _require("@coston/design-tokens/tokens.json");
909
+ function hex(key) {
910
+ return oklchToHex(themes[THEME_NAME][key]);
911
+ }
912
+ var theme = {
913
+ foreground: hex("foreground"),
914
+ // primary text
915
+ muted: hex("muted-foreground"),
916
+ // dimmed / inactive text and borders
917
+ primary: hex("primary"),
918
+ // tool calls, cursor, active
919
+ primaryLight: hex("secondary-foreground"),
920
+ // lighter tint of primary (same hue, higher lightness)
921
+ success: hex("success"),
922
+ // completed steps
923
+ error: hex("destructive"),
924
+ // errors
925
+ warning: hex("warning"),
926
+ // warnings, retries, updates
927
+ border: hex("border")
928
+ // log pane border
929
+ };
930
+
931
+ // src/ui/TaskRow.tsx
932
+ import { jsx, jsxs } from "react/jsx-runtime";
933
+ function TaskRow({ taskState, isActive, index, tick }) {
934
+ const { task, status, startTime, endTime } = taskState;
935
+ const icon = statusIcon(status, tick);
936
+ const color = statusColor(status, isActive);
937
+ const elapsed = formatTaskElapsed(startTime, endTime, status);
938
+ const iterInfo = taskState.iteration ? ` (${taskState.iteration.current}/${taskState.iteration.total}) ${taskState.iteration.item}` : "";
939
+ const label = `${index + 1}. ${task.name}${iterInfo}`;
940
+ return /* @__PURE__ */ jsxs(Box, { children: [
941
+ /* @__PURE__ */ jsxs(Text, { color, children: [
942
+ icon,
943
+ " "
944
+ ] }),
945
+ /* @__PURE__ */ jsx(Text, { color: isActive ? theme.foreground : theme.muted, bold: isActive, children: label }),
946
+ /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
947
+ " ",
948
+ elapsed
949
+ ] })
950
+ ] });
951
+ }
952
+ var STATUS_ICON = {
953
+ complete: "\u2713",
954
+ error: "\u2717",
955
+ skipped: "\u2298",
956
+ pending: "\xB7"
957
+ };
958
+ var STATUS_COLOR = {
959
+ complete: theme.success,
960
+ error: theme.error,
961
+ pending: theme.muted
962
+ };
963
+ function statusIcon(status, tick) {
964
+ return status === "running" ? SPINNER[tick % SPINNER.length] : STATUS_ICON[status] ?? "\xB7";
965
+ }
966
+ function statusColor(status, isActive) {
967
+ if (isActive && status === "running") return theme.primary;
968
+ return STATUS_COLOR[status] ?? theme.foreground;
969
+ }
970
+
971
+ // src/ui/LogPane.tsx
972
+ import { Box as Box2, Text as Text2 } from "ink";
973
+ import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
974
+ function LogPane({ lines, isActive = false, maxLines = 15 }) {
975
+ const visible = lines.slice(-maxLines);
976
+ if (visible.length === 0) {
977
+ return /* @__PURE__ */ jsx2(Box2, { marginTop: 1, children: /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: isActive ? "\u2838 waiting for output\u2026" : "\u2014 no output yet \u2014" }) });
978
+ }
979
+ return /* @__PURE__ */ jsx2(Box2, { flexDirection: "column", marginTop: 1, borderStyle: "single", borderColor: theme.border, paddingX: 1, children: visible.map((line, i) => /* @__PURE__ */ jsx2(
980
+ LogLine,
981
+ {
982
+ text: line,
983
+ cursor: isActive && i === visible.length - 1
984
+ },
985
+ i
986
+ )) });
987
+ }
988
+ function LogLine({ text, cursor }) {
989
+ const suffix = cursor ? /* @__PURE__ */ jsx2(Text2, { color: theme.primary, children: " \u258C" }) : null;
990
+ if (/^\[[\w:]+\]/.test(text)) {
991
+ const bracket = text.match(/^\[[\w:]+\]/)?.[0] ?? "";
992
+ const rest = text.slice(bracket.length);
993
+ return /* @__PURE__ */ jsxs2(Text2, { children: [
994
+ /* @__PURE__ */ jsx2(Text2, { color: theme.primary, children: bracket }),
995
+ /* @__PURE__ */ jsx2(Text2, { children: rest }),
996
+ suffix
997
+ ] });
998
+ }
999
+ if (/^\s*\$\s/.test(text)) return /* @__PURE__ */ jsxs2(Text2, { color: theme.warning, children: [
1000
+ text,
1001
+ suffix
1002
+ ] });
1003
+ if (text.startsWith("[warn]")) return /* @__PURE__ */ jsxs2(Text2, { color: theme.warning, children: [
1004
+ text,
1005
+ suffix
1006
+ ] });
1007
+ if (text.startsWith("[error]")) return /* @__PURE__ */ jsxs2(Text2, { color: theme.error, children: [
1008
+ text,
1009
+ suffix
1010
+ ] });
1011
+ return /* @__PURE__ */ jsxs2(Text2, { children: [
1012
+ text,
1013
+ suffix
1014
+ ] });
1015
+ }
1016
+
1017
+ // src/ui/useInterval.ts
1018
+ import { useEffect, useRef } from "react";
1019
+ function useInterval(callback, delayMs) {
1020
+ const saved = useRef(callback);
1021
+ useEffect(() => {
1022
+ saved.current = callback;
1023
+ }, [callback]);
1024
+ useEffect(() => {
1025
+ const id = setInterval(() => saved.current(), delayMs);
1026
+ return () => clearInterval(id);
1027
+ }, [delayMs]);
1028
+ }
1029
+
1030
+ // src/ui/BrandMark.tsx
1031
+ import { Box as Box3, Text as Text3 } from "ink";
1032
+ import { jsx as jsx3 } from "react/jsx-runtime";
1033
+ var BRAND = "Executant";
1034
+ var SWEEP_TICKS = BRAND.length * 2;
1035
+ var GAP_TICKS = 30;
1036
+ var CYCLE = SWEEP_TICKS + GAP_TICKS;
1037
+ function charColor(charIndex, tick, isActive) {
1038
+ if (!isActive) return theme.primary;
1039
+ const pos = tick % CYCLE;
1040
+ if (pos >= SWEEP_TICKS) return theme.primary;
1041
+ const charPos = Math.floor(pos / 2);
1042
+ return charIndex === charPos ? theme.primaryLight : theme.primary;
1043
+ }
1044
+ function BrandMark({ tick, isActive }) {
1045
+ return /* @__PURE__ */ jsx3(Box3, { children: [...BRAND].map((char, i) => /* @__PURE__ */ jsx3(Text3, { color: charColor(i, tick, isActive), bold: true, children: char }, i)) });
1046
+ }
1047
+
1048
+ // src/ui/App.tsx
1049
+ import { jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
1050
+ function App({ workflow: workflow2, events: events2, options: options2, updateCheck: updateCheck2 }) {
1051
+ const { exit } = useApp();
1052
+ const [state, dispatch] = useReducer(reducer, buildInitialState(workflow2));
1053
+ useEffect2(() => {
1054
+ let active = true;
1055
+ (async () => {
1056
+ try {
1057
+ for await (const event of events2) {
1058
+ if (!active) break;
1059
+ dispatch(event);
1060
+ if (event.type === "workflow:complete") {
1061
+ setTimeout(() => exit(), EXIT_DELAY_MS);
1062
+ }
1063
+ }
1064
+ } catch (err) {
1065
+ if (!active) return;
1066
+ dispatch({
1067
+ type: "log",
1068
+ level: "error",
1069
+ text: err instanceof Error ? err.message : String(err)
1070
+ });
1071
+ setTimeout(() => exit(err instanceof Error ? err : new Error(String(err))), EXIT_DELAY_MS);
1072
+ }
1073
+ })();
1074
+ return () => {
1075
+ active = false;
1076
+ events2.return(void 0).catch(() => {
1077
+ });
1078
+ };
1079
+ }, [events2, exit]);
1080
+ const { isRawModeSupported } = useStdin();
1081
+ const [tick, setTick] = useState(0);
1082
+ useInterval(() => {
1083
+ if (!state.endTime) setTick((t) => t + 1);
1084
+ }, 100);
1085
+ const [updateVersion, setUpdateVersion] = useState(null);
1086
+ useEffect2(() => {
1087
+ updateCheck2.then(setUpdateVersion);
1088
+ }, [updateCheck2]);
1089
+ const elapsed = formatHeaderElapsed(state.startTime, state.endTime);
1090
+ const activeTask = state.tasks[state.currentIndex];
1091
+ const completedCount = state.tasks.filter((t) => t.status === "complete").length;
1092
+ const totalCount = state.tasks.length;
1093
+ const filterInfo = options2?.stepFilter ? ` [step: ${options2.stepFilter}]` : options2?.fromStep ? ` [from step: ${options2.fromStep}]` : "";
1094
+ return /* @__PURE__ */ jsxs3(Box4, { flexDirection: "column", padding: 1, children: [
1095
+ /* @__PURE__ */ jsx4(Box4, { marginBottom: 1, children: /* @__PURE__ */ jsx4(BrandMark, { tick, isActive: !state.endTime }) }),
1096
+ /* @__PURE__ */ jsxs3(Box4, { marginBottom: 1, children: [
1097
+ /* @__PURE__ */ jsx4(Text4, { bold: true, color: theme.primary, children: workflow2.goal }),
1098
+ /* @__PURE__ */ jsxs3(Text4, { dimColor: true, children: [
1099
+ " ",
1100
+ completedCount,
1101
+ "/",
1102
+ totalCount,
1103
+ " steps \xB7 ",
1104
+ elapsed,
1105
+ filterInfo
1106
+ ] })
1107
+ ] }),
1108
+ /* @__PURE__ */ jsx4(Box4, { flexDirection: "column", marginBottom: 1, children: state.tasks.map((taskState, i) => /* @__PURE__ */ jsx4(
1109
+ TaskRow,
1110
+ {
1111
+ index: i,
1112
+ tick,
1113
+ taskState,
1114
+ isActive: i === state.currentIndex
1115
+ },
1116
+ taskState.task.name
1117
+ )) }),
1118
+ activeTask && /* @__PURE__ */ jsx4(LogPane, { lines: activeTask.lines, isActive: activeTask.status === "running" }),
1119
+ state.endTime !== void 0 && state.writtenFiles.length > 0 && /* @__PURE__ */ jsxs3(Box4, { flexDirection: "column", marginTop: 1, children: [
1120
+ /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: "files written:" }),
1121
+ state.writtenFiles.map((f) => /* @__PURE__ */ jsxs3(Text4, { color: theme.primary, children: [
1122
+ " ",
1123
+ f
1124
+ ] }, f))
1125
+ ] }),
1126
+ /* @__PURE__ */ jsxs3(Box4, { marginTop: 1, flexDirection: "column", children: [
1127
+ updateVersion && /* @__PURE__ */ jsxs3(Text4, { color: theme.warning, children: [
1128
+ "v",
1129
+ updateVersion,
1130
+ " available \u2014 run: executant update"
1131
+ ] }),
1132
+ /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: "press q to quit" })
1133
+ ] }),
1134
+ isRawModeSupported && /* @__PURE__ */ jsx4(KeyboardHandler, { onExit: exit })
1135
+ ] });
1136
+ }
1137
+
1138
+ // src/plan.ts
1139
+ import { existsSync, mkdirSync as mkdirSync2, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "node:fs";
1140
+ import { dirname as dirname2, join as join2, resolve } from "node:path";
1141
+ import { fileURLToPath as fileURLToPath2 } from "node:url";
1142
+ import { dump as dumpYaml } from "js-yaml";
1143
+ import { z as z3 } from "zod";
1144
+ import { zodToJsonSchema as zodToJsonSchema2 } from "zod-to-json-schema";
1145
+ var PROMPTS_DIR2 = join2(dirname2(fileURLToPath2(import.meta.url)), "prompts");
1146
+ var PLAN_RESEARCH_PROMPT = readFileSync3(join2(PROMPTS_DIR2, "plan-research.txt"), "utf8");
1147
+ var PLAN_DECOMPOSE_PROMPT = readFileSync3(join2(PROMPTS_DIR2, "plan-decompose.txt"), "utf8");
1148
+ var PLAN_JUDGE_PROMPT = readFileSync3(join2(PROMPTS_DIR2, "plan-judge.txt"), "utf8");
1149
+ var PLAN_SYSTEM_RULES = readFileSync3(join2(PROMPTS_DIR2, "plan-system-rules.txt"), "utf8").trim();
1150
+ var PLAN_RETRY_PARSE_ERROR = readFileSync3(join2(PROMPTS_DIR2, "plan-retry-parse-error.txt"), "utf8").trim();
1151
+ var PLAN_RETRY_SCHEMA_ERROR = readFileSync3(join2(PROMPTS_DIR2, "plan-retry-schema-error.txt"), "utf8").trim();
1152
+ var PLAN_RETRY_JUDGE = readFileSync3(join2(PROMPTS_DIR2, "plan-retry-judge.txt"), "utf8").trim();
1153
+ var MAX_PLAN_RETRIES = 3;
1154
+ var TOTAL_PLAN_STAGES = 3;
1155
+ var StepSchema = z3.object({
1156
+ name: z3.string(),
1157
+ type: z3.enum(["prompt", "script", "log"]).optional(),
1158
+ prompt: z3.string().optional(),
1159
+ command: z3.string().optional(),
1160
+ message: z3.string().optional(),
1161
+ continue_on_error: z3.boolean().optional(),
1162
+ self_healing: z3.boolean().optional(),
1163
+ max_healing_attempts: z3.number().int().positive().optional(),
1164
+ output: z3.string().optional(),
1165
+ llm_as_judge: z3.boolean().optional(),
1166
+ allowed_tools: z3.array(z3.string()).optional(),
1167
+ forEach: z3.union([z3.array(z3.string()), z3.string()]).optional(),
1168
+ context: z3.array(z3.string()).optional()
1169
+ });
1170
+ var WorkflowSchema = z3.object({
1171
+ goal: z3.string(),
1172
+ steps: z3.array(StepSchema).min(1),
1173
+ vars: z3.record(z3.string()).optional(),
1174
+ self_improve: z3.boolean().optional()
1175
+ });
1176
+ var PlanJudgeOutputSchema = z3.object({
1177
+ pass: z3.boolean(),
1178
+ feedback: z3.string()
1179
+ });
1180
+ var WORKFLOW_JSON_SCHEMA = zodToJsonSchema2(WorkflowSchema);
1181
+ function walkUp(startDir, check) {
1182
+ let dir = startDir;
1183
+ while (true) {
1184
+ const found = check(dir);
1185
+ if (found !== null) return found;
1186
+ const parent = join2(dir, "..");
1187
+ if (resolve(parent) === resolve(dir)) return null;
1188
+ dir = parent;
1189
+ }
1190
+ }
1191
+ function findGitRoot(startDir) {
1192
+ return walkUp(startDir, (dir) => existsSync(join2(dir, ".git")) ? dir : null);
1193
+ }
1194
+ function findProjectRoot(startDir) {
1195
+ return walkUp(startDir, (dir) => {
1196
+ const candidate = join2(dir, ".claude", "executant.local", "tasks");
1197
+ return existsSync(candidate) ? candidate : null;
1198
+ });
1199
+ }
1200
+ function parsePlanArgs(rawArgs2) {
1201
+ let description = "";
1202
+ if (rawArgs2[0] === "-h" || rawArgs2[0] === "--help") {
1203
+ console.log(`Usage: executant plan [OPTIONS] [DESCRIPTION]
1204
+
1205
+ Generate a task plan from a description.
1206
+
1207
+ Options:
1208
+ -f, --file <path> Read prompt from file
1209
+ -h, --help Show this help message
1210
+
1211
+ Examples:
1212
+ executant plan "add user authentication"
1213
+ executant plan -f plan-prompt.txt
1214
+ cat prompt.txt | executant plan`);
1215
+ process.exit(0);
1216
+ }
1217
+ if (rawArgs2[0] === "-f" || rawArgs2[0] === "--file") {
1218
+ const filePath2 = rawArgs2[1];
1219
+ if (!filePath2) {
1220
+ console.error("Error: -f/--file requires a file path argument");
1221
+ process.exit(1);
1222
+ }
1223
+ if (!existsSync(filePath2)) {
1224
+ console.error(`Error: File not found: ${filePath2}`);
1225
+ process.exit(1);
1226
+ }
1227
+ try {
1228
+ description = readFileSync3(filePath2, "utf8").trim();
1229
+ } catch {
1230
+ console.error(`Error: Cannot read file: ${filePath2}`);
1231
+ process.exit(1);
1232
+ }
1233
+ } else if (rawArgs2.length > 0) {
1234
+ description = rawArgs2.join(" ").trim();
1235
+ } else if (!process.stdin.isTTY) {
1236
+ try {
1237
+ description = readFileSync3("/dev/stdin", "utf8").trim();
1238
+ } catch {
1239
+ }
1240
+ }
1241
+ if (!description) {
1242
+ console.error("Error: No task description provided");
1243
+ console.error("Usage: executant plan [OPTIONS] [DESCRIPTION]");
1244
+ console.error(" executant plan -f <filepath>");
1245
+ console.error(" cat prompt.txt | executant plan");
1246
+ process.exit(1);
1247
+ }
1248
+ let taskDir = findProjectRoot(process.cwd());
1249
+ if (!taskDir) {
1250
+ const base = findGitRoot(process.cwd()) ?? process.cwd();
1251
+ taskDir = join2(base, ".claude", "executant.local", "tasks");
1252
+ mkdirSync2(taskDir, { recursive: true });
1253
+ }
1254
+ const todoDir = join2(taskDir, "todo");
1255
+ mkdirSync2(todoDir, { recursive: true });
1256
+ const slug = slugify(description);
1257
+ const ts = timestamp();
1258
+ const taskFile = join2(todoDir, `${ts}-${slug}.yaml`);
1259
+ return { description, taskFile, todoDir };
1260
+ }
1261
+ async function runPass3Judge(description, workflow2) {
1262
+ try {
1263
+ const task = {
1264
+ type: "claude",
1265
+ name: "plan:judge",
1266
+ prompt: PLAN_JUDGE_PROMPT.replace("{{DESCRIPTION}}", description).replace("{{WORKFLOW_JSON}}", JSON.stringify(workflow2, null, 2)),
1267
+ allowedTools: [],
1268
+ permissionMode: "default",
1269
+ model: "sonnet"
1270
+ };
1271
+ return await runClaudeStructured(task, PlanJudgeOutputSchema);
1272
+ } catch {
1273
+ return { pass: true, feedback: "", skipped: true };
1274
+ }
1275
+ }
1276
+ async function* streamPlan(args) {
1277
+ const { description, taskFile } = args;
1278
+ yield { type: "plan:start", description };
1279
+ yield { type: "plan:stages", names: ["Research & Planning", "Decompose to Steps", "Validate"] };
1280
+ yield { type: "plan:stage", stage: 1, total: TOTAL_PLAN_STAGES, name: "Research & Planning" };
1281
+ const researchLines = [];
1282
+ try {
1283
+ const researchTask = {
1284
+ type: "claude",
1285
+ name: "plan:research",
1286
+ prompt: PLAN_RESEARCH_PROMPT.replace("{{DESCRIPTION}}", description),
1287
+ allowedTools: ["Read", "Glob", "Grep"],
1288
+ permissionMode: "bypassPermissions",
1289
+ model: "opus"
1290
+ };
1291
+ for await (const event of runClaude(researchTask)) {
1292
+ if (event.type === "output:tool") {
1293
+ yield { type: "plan:tool", tool: event.tool, input: event.input };
1294
+ } else if (event.type === "output:text") {
1295
+ researchLines.push(event.text);
1296
+ yield { type: "plan:text", text: event.text };
1297
+ }
1298
+ }
1299
+ } catch (err) {
1300
+ const msg = err instanceof Error ? err.message : String(err);
1301
+ yield { type: "plan:error", message: `Research pass failed: ${msg}` };
1302
+ return;
1303
+ }
1304
+ const researchDoc = researchLines.join("\n");
1305
+ if (!researchDoc.trim()) {
1306
+ yield { type: "plan:error", message: "Research pass produced no output \u2014 cannot decompose" };
1307
+ return;
1308
+ }
1309
+ yield { type: "plan:stage", stage: 2, total: TOTAL_PLAN_STAGES, name: "Decompose to Steps" };
1310
+ let retryPrefix = "";
1311
+ for (let attempt = 0; attempt < MAX_PLAN_RETRIES; attempt++) {
1312
+ if (attempt > 0) {
1313
+ yield {
1314
+ type: "plan:retry",
1315
+ attempt: attempt + 1,
1316
+ maxAttempts: MAX_PLAN_RETRIES,
1317
+ reason: retryPrefix.replace(/\n/g, " ")
1318
+ };
1319
+ yield { type: "plan:stage", stage: 2, total: TOTAL_PLAN_STAGES, name: "Decompose to Steps" };
1320
+ }
1321
+ const basePrompt = PLAN_DECOMPOSE_PROMPT.replace("{{DESCRIPTION}}", description).replace("{{RESEARCH_DOC}}", researchDoc);
1322
+ const decomposeTask = {
1323
+ type: "claude",
1324
+ name: "plan:decompose",
1325
+ prompt: retryPrefix ? `${retryPrefix}
1326
+
1327
+ ${basePrompt}` : basePrompt,
1328
+ allowedTools: [],
1329
+ permissionMode: "bypassPermissions",
1330
+ model: "opus",
1331
+ appendSystemPrompt: PLAN_SYSTEM_RULES,
1332
+ jsonSchema: WORKFLOW_JSON_SCHEMA
1333
+ };
1334
+ let structuredOutput;
1335
+ const decomposeTextLines = [];
1336
+ try {
1337
+ for await (const event of runClaude(decomposeTask)) {
1338
+ if (event.type === "output:tool") {
1339
+ yield { type: "plan:tool", tool: event.tool, input: event.input };
1340
+ } else if (event.type === "output:text") {
1341
+ decomposeTextLines.push(event.text);
1342
+ yield { type: "plan:text", text: event.text };
1343
+ } else if (event.type === "output:structured") {
1344
+ structuredOutput = event.data;
1345
+ }
1346
+ }
1347
+ } catch (err) {
1348
+ const msg = err instanceof Error ? err.message : String(err);
1349
+ if (attempt === MAX_PLAN_RETRIES - 1) {
1350
+ yield { type: "plan:error", message: msg };
1351
+ return;
1352
+ }
1353
+ retryPrefix = PLAN_RETRY_PARSE_ERROR.replace("{{ERROR}}", msg).replace("{{EXCERPT}}", decomposeTextLines.join("\n"));
1354
+ continue;
1355
+ }
1356
+ if (structuredOutput === void 0) {
1357
+ const issues = "No structured output returned \u2014 ensure the response is a JSON object";
1358
+ if (attempt === MAX_PLAN_RETRIES - 1) {
1359
+ yield { type: "plan:error", message: issues };
1360
+ return;
1361
+ }
1362
+ retryPrefix = PLAN_RETRY_SCHEMA_ERROR.replace("{{ISSUES}}", issues);
1363
+ continue;
1364
+ }
1365
+ const zodResult = WorkflowSchema.safeParse(structuredOutput);
1366
+ if (!zodResult.success) {
1367
+ const issues = zodResult.error.issues.map((i) => ` ${i.path.join(".")}: ${i.message}`).join("\n");
1368
+ if (attempt === MAX_PLAN_RETRIES - 1) {
1369
+ yield { type: "plan:error", message: `Plan did not match expected schema:
1370
+ ${issues}` };
1371
+ return;
1372
+ }
1373
+ retryPrefix = PLAN_RETRY_SCHEMA_ERROR.replace("{{ISSUES}}", issues);
1374
+ continue;
1375
+ }
1376
+ yield { type: "plan:stage", stage: 3, total: TOTAL_PLAN_STAGES, name: "Validate" };
1377
+ const judgeResult = await runPass3Judge(description, zodResult.data);
1378
+ if (judgeResult.skipped) {
1379
+ yield { type: "plan:warn", message: "Judge skipped due to error \u2014 proceeding without validation" };
1380
+ }
1381
+ if (!judgeResult.pass && attempt < MAX_PLAN_RETRIES - 1) {
1382
+ retryPrefix = PLAN_RETRY_JUDGE.replace("{{FEEDBACK}}", judgeResult.feedback);
1383
+ continue;
1384
+ }
1385
+ if (!judgeResult.pass) {
1386
+ yield { type: "plan:warn", message: `Judge rejected plan but retries exhausted: ${judgeResult.feedback}` };
1387
+ }
1388
+ const { goal, vars, steps, ...rest } = zodResult.data;
1389
+ const ordered = { goal, ...vars && { vars }, steps, ...rest };
1390
+ const yamlContent = dumpYaml(ordered, {
1391
+ lineWidth: -1,
1392
+ noRefs: true,
1393
+ quotingType: '"',
1394
+ forceQuotes: false
1395
+ }).trimEnd();
1396
+ writeFileSync2(taskFile, yamlContent + "\n", "utf8");
1397
+ const preview = yamlContent.split("\n").slice(0, 30).join("\n") + (yamlContent.split("\n").length > 30 ? "\n..." : "");
1398
+ yield { type: "plan:complete", taskFile, preview };
1399
+ return;
1400
+ }
1401
+ yield { type: "plan:error", message: "Plan generation failed after maximum retries" };
1402
+ }
1403
+
1404
+ // src/ui/PlanApp.tsx
1405
+ import { useEffect as useEffect3, useReducer as useReducer2, useState as useState2 } from "react";
1406
+ import { Box as Box5, Text as Text5, useApp as useApp2, useStdin as useStdin2 } from "ink";
1407
+ import { jsx as jsx5, jsxs as jsxs4 } from "react/jsx-runtime";
1408
+ var truncate = (str, max) => str.length > max ? str.slice(0, max - 3) + "..." : str;
1409
+ function buildInitial(description) {
1410
+ return {
1411
+ description,
1412
+ lines: [],
1413
+ status: "running",
1414
+ attempt: 1,
1415
+ maxAttempts: 3,
1416
+ startTime: Date.now(),
1417
+ stage: 0,
1418
+ totalStages: 0,
1419
+ stageName: "",
1420
+ stageNames: []
1421
+ };
1422
+ }
1423
+ function planReducer(state, event) {
1424
+ switch (event.type) {
1425
+ case "plan:start":
1426
+ return { ...state, description: event.description, startTime: Date.now() };
1427
+ case "plan:stages":
1428
+ return { ...state, stageNames: event.names, totalStages: event.names.length };
1429
+ case "plan:stage": {
1430
+ const lines = [...state.lines, `[${event.stage}/${event.total}] ${event.name}`];
1431
+ return { ...state, stage: event.stage, totalStages: event.total, stageName: event.name, status: "running", lines };
1432
+ }
1433
+ case "plan:tool": {
1434
+ const formatted = formatToolCall2(event.tool, event.input);
1435
+ if (!formatted) return state;
1436
+ return { ...state, lines: [...state.lines, formatted] };
1437
+ }
1438
+ case "plan:text":
1439
+ return state;
1440
+ // text output is collected separately for JSON parsing
1441
+ case "plan:warn": {
1442
+ const lines = [...state.lines, `[warn] ${event.message}`];
1443
+ return { ...state, lines };
1444
+ }
1445
+ case "plan:retry": {
1446
+ const lines = [...state.lines, `[retry] Attempt ${event.attempt}/${event.maxAttempts}: ${event.reason}`];
1447
+ return { ...state, status: "retrying", attempt: event.attempt, maxAttempts: event.maxAttempts, retryReason: event.reason, lines };
1448
+ }
1449
+ case "plan:complete":
1450
+ return { ...state, status: "complete", taskFile: event.taskFile, preview: event.preview };
1451
+ case "plan:error":
1452
+ return { ...state, status: "error", errorMessage: event.message };
1453
+ }
1454
+ }
1455
+ function StageProgress({ stage, totalStages, stageNames, tick, isActive, status }) {
1456
+ if (totalStages === 0 || stageNames.length === 0) return null;
1457
+ const rows = Array.from({ length: totalStages }, (_, i) => {
1458
+ const stageIndex = i + 1;
1459
+ const name = stageNames[i] ?? "";
1460
+ let icon;
1461
+ let color;
1462
+ let bold = false;
1463
+ let dim = false;
1464
+ if (stageIndex < stage) {
1465
+ icon = "\u2713";
1466
+ color = theme.success;
1467
+ } else if (stageIndex === stage && isActive) {
1468
+ icon = SPINNER[tick % SPINNER.length];
1469
+ color = theme.primary;
1470
+ bold = true;
1471
+ } else if (stageIndex === stage && !isActive) {
1472
+ icon = status === "complete" ? "\u2713" : status === "error" ? "\u2717" : "\xB7";
1473
+ color = status === "complete" ? theme.success : status === "error" ? theme.error : void 0;
1474
+ } else if (!isActive && status === "complete") {
1475
+ icon = "\u2713";
1476
+ color = theme.success;
1477
+ } else {
1478
+ icon = "\xB7";
1479
+ dim = true;
1480
+ }
1481
+ return { icon, color, name, bold, dim };
1482
+ });
1483
+ return /* @__PURE__ */ jsx5(Box5, { flexDirection: "column", marginBottom: 1, children: rows.map(({ icon, color, name, bold, dim }, i) => /* @__PURE__ */ jsx5(Box5, { children: /* @__PURE__ */ jsxs4(Text5, { color, dimColor: dim, bold, children: [
1484
+ " ",
1485
+ icon,
1486
+ name ? ` ${name}` : ""
1487
+ ] }) }, i)) });
1488
+ }
1489
+ function PlanApp({ description, events: events2 }) {
1490
+ const { exit } = useApp2();
1491
+ const [state, dispatch] = useReducer2(planReducer, buildInitial(description));
1492
+ useEffect3(() => {
1493
+ let active = true;
1494
+ (async () => {
1495
+ for await (const event of events2) {
1496
+ if (!active) break;
1497
+ dispatch(event);
1498
+ if (event.type === "plan:complete" || event.type === "plan:error") {
1499
+ setTimeout(() => exit(), EXIT_DELAY_MS);
1500
+ }
1501
+ }
1502
+ })();
1503
+ return () => {
1504
+ active = false;
1505
+ };
1506
+ }, [events2, exit]);
1507
+ const { isRawModeSupported } = useStdin2();
1508
+ const [tick, setTick] = useState2(0);
1509
+ const isActive = state.status === "running" || state.status === "retrying";
1510
+ useInterval(() => {
1511
+ if (isActive) setTick((t) => t + 1);
1512
+ }, 100);
1513
+ const elapsed = formatHeaderElapsed(state.startTime);
1514
+ const icon = isActive ? SPINNER[tick % SPINNER.length] : state.status === "complete" ? "\u2713" : "\u2717";
1515
+ const iconColor = state.status === "complete" ? theme.success : state.status === "error" ? theme.error : theme.primary;
1516
+ return /* @__PURE__ */ jsxs4(Box5, { flexDirection: "column", padding: 1, children: [
1517
+ /* @__PURE__ */ jsx5(Box5, { marginBottom: 1, children: /* @__PURE__ */ jsx5(BrandMark, { tick, isActive }) }),
1518
+ /* @__PURE__ */ jsxs4(Box5, { marginBottom: 1, children: [
1519
+ /* @__PURE__ */ jsxs4(Text5, { color: iconColor, children: [
1520
+ icon,
1521
+ " "
1522
+ ] }),
1523
+ /* @__PURE__ */ jsx5(Text5, { bold: true, color: theme.primary, children: "Generating plan" }),
1524
+ /* @__PURE__ */ jsxs4(Text5, { dimColor: true, children: [
1525
+ " ",
1526
+ elapsed
1527
+ ] }),
1528
+ state.status === "retrying" && /* @__PURE__ */ jsxs4(Text5, { color: theme.warning, children: [
1529
+ " ",
1530
+ "(attempt ",
1531
+ state.attempt,
1532
+ "/",
1533
+ state.maxAttempts,
1534
+ ")"
1535
+ ] })
1536
+ ] }),
1537
+ /* @__PURE__ */ jsxs4(Box5, { marginBottom: 1, children: [
1538
+ /* @__PURE__ */ jsx5(Text5, { dimColor: true, children: " " }),
1539
+ /* @__PURE__ */ jsx5(Text5, { children: truncate(state.description, 80) })
1540
+ ] }),
1541
+ /* @__PURE__ */ jsx5(
1542
+ StageProgress,
1543
+ {
1544
+ stage: state.stage,
1545
+ totalStages: state.totalStages,
1546
+ stageNames: state.stageNames,
1547
+ tick,
1548
+ isActive,
1549
+ status: state.status
1550
+ }
1551
+ ),
1552
+ state.lines.length > 0 && /* @__PURE__ */ jsx5(LogPane, { lines: state.lines, isActive, maxLines: 10 }),
1553
+ state.status === "complete" && state.taskFile && /* @__PURE__ */ jsxs4(Box5, { flexDirection: "column", marginTop: 1, children: [
1554
+ /* @__PURE__ */ jsxs4(Text5, { color: theme.success, children: [
1555
+ "\u2705 Task plan saved: ",
1556
+ state.taskFile
1557
+ ] }),
1558
+ state.preview && /* @__PURE__ */ jsxs4(Box5, { flexDirection: "column", marginTop: 1, children: [
1559
+ /* @__PURE__ */ jsx5(Text5, { dimColor: true, children: "Preview:" }),
1560
+ /* @__PURE__ */ jsx5(Text5, { children: state.preview })
1561
+ ] })
1562
+ ] }),
1563
+ state.status === "error" && /* @__PURE__ */ jsx5(Box5, { marginTop: 1, children: /* @__PURE__ */ jsxs4(Text5, { color: theme.error, children: [
1564
+ "Error: ",
1565
+ state.errorMessage
1566
+ ] }) }),
1567
+ isActive && /* @__PURE__ */ jsx5(Box5, { marginTop: 1, children: /* @__PURE__ */ jsx5(Text5, { dimColor: true, children: "press q to quit" }) }),
1568
+ isRawModeSupported && /* @__PURE__ */ jsx5(KeyboardHandler, { onExit: exit })
1569
+ ] });
1570
+ }
1571
+
1572
+ // src/logger.ts
1573
+ import {
1574
+ appendFileSync,
1575
+ existsSync as existsSync2,
1576
+ mkdirSync as mkdirSync3,
1577
+ readdirSync,
1578
+ writeFileSync as writeFileSync3
1579
+ } from "node:fs";
1580
+ import { dirname as dirname3, join as join3, resolve as resolve2 } from "node:path";
1581
+ var TOOL_SUMMARY = {
1582
+ Read: (i) => String(i["file_path"] ?? i["path"] ?? ""),
1583
+ Edit: (i) => String(i["file_path"] ?? ""),
1584
+ Write: (i) => String(i["file_path"] ?? ""),
1585
+ Bash: (i) => String(i["command"] ?? ""),
1586
+ Glob: (i) => String(i["pattern"] ?? ""),
1587
+ Grep: (i) => String(i["pattern"] ?? "")
1588
+ };
1589
+ function toolSummary(tool, input) {
1590
+ return (TOOL_SUMMARY[tool] ?? ((i) => JSON.stringify(i)))(input);
1591
+ }
1592
+ function findExecutantLocalDir(startDir) {
1593
+ let dir = resolve2(startDir);
1594
+ while (true) {
1595
+ const candidate = join3(dir, ".claude", "executant.local");
1596
+ if (existsSync2(candidate)) return candidate;
1597
+ const parent = dirname3(dir);
1598
+ if (parent === dir) return null;
1599
+ dir = parent;
1600
+ }
1601
+ }
1602
+ function resolveLogDir(workflowFilePath) {
1603
+ const startDir = dirname3(resolve2(workflowFilePath));
1604
+ const executantLocal = findExecutantLocalDir(startDir);
1605
+ if (executantLocal) return join3(executantLocal, "logs");
1606
+ return join3(startDir, "logs");
1607
+ }
1608
+ var Logger = class {
1609
+ enabled;
1610
+ logDir;
1611
+ highlightsDir;
1612
+ timestamp;
1613
+ taskName;
1614
+ logFile = "";
1615
+ // Per-step state
1616
+ stepIndex = -1;
1617
+ stepName = "";
1618
+ stepStartMs = 0;
1619
+ toolCount = 0;
1620
+ complexSequenceFile = "";
1621
+ selfHealingFile = "";
1622
+ judgeAttempt = 0;
1623
+ recentOutput = [];
1624
+ constructor(logDir, taskName) {
1625
+ this.enabled = process.env["EXECUTANT_LOG"] !== "0";
1626
+ this.logDir = logDir;
1627
+ this.highlightsDir = join3(logDir, "highlights");
1628
+ this.timestamp = formatTimestamp(/* @__PURE__ */ new Date());
1629
+ this.taskName = slugify(taskName, 40) || "task";
1630
+ }
1631
+ getHighlightsDir() {
1632
+ return this.highlightsDir;
1633
+ }
1634
+ getTimestamp() {
1635
+ return this.timestamp;
1636
+ }
1637
+ /** Feed each event from the runner into the logger. */
1638
+ observe(event) {
1639
+ if (!this.enabled) return;
1640
+ try {
1641
+ this.dispatch(event);
1642
+ } catch (err) {
1643
+ console.warn(`[logger] error: ${err instanceof Error ? err.message : String(err)}`);
1644
+ }
1645
+ }
1646
+ // --------------------------------------------------------------------------
1647
+ // Event dispatch
1648
+ // --------------------------------------------------------------------------
1649
+ dispatch(event) {
1650
+ switch (event.type) {
1651
+ case "workflow:start":
1652
+ this.initDirs();
1653
+ break;
1654
+ case "step:start":
1655
+ this.onStepStart(event.index, event.name);
1656
+ break;
1657
+ case "step:complete":
1658
+ this.onStepComplete();
1659
+ break;
1660
+ case "step:error":
1661
+ this.onStepError(event.error);
1662
+ break;
1663
+ case "output:text":
1664
+ this.appendLog(event.text);
1665
+ this.recentOutput.push(event.text);
1666
+ break;
1667
+ case "output:tool":
1668
+ this.onTool(event.tool, event.input);
1669
+ break;
1670
+ case "log":
1671
+ this.onLogMessage(event.level, event.text);
1672
+ break;
1673
+ case "workflow:complete":
1674
+ this.onWorkflowComplete();
1675
+ break;
1676
+ }
1677
+ }
1678
+ // --------------------------------------------------------------------------
1679
+ // Initialisation
1680
+ // --------------------------------------------------------------------------
1681
+ initDirs() {
1682
+ mkdirSync3(this.logDir, { recursive: true });
1683
+ mkdirSync3(this.highlightsDir, { recursive: true });
1684
+ this.logFile = join3(this.logDir, `${this.timestamp}_${this.taskName}.log`);
1685
+ writeFileSync3(
1686
+ this.logFile,
1687
+ `# Execution Log
1688
+ Task: ${this.taskName}
1689
+ Started: ${(/* @__PURE__ */ new Date()).toISOString()}
1690
+ ${"\u2501".repeat(51)}
1691
+
1692
+ `
1693
+ );
1694
+ }
1695
+ // --------------------------------------------------------------------------
1696
+ // Step lifecycle
1697
+ // --------------------------------------------------------------------------
1698
+ onStepStart(index, name) {
1699
+ Object.assign(this, {
1700
+ stepIndex: index,
1701
+ stepName: name,
1702
+ stepStartMs: Date.now(),
1703
+ toolCount: 0,
1704
+ complexSequenceFile: "",
1705
+ selfHealingFile: "",
1706
+ judgeAttempt: 0,
1707
+ recentOutput: []
1708
+ });
1709
+ this.appendLog(
1710
+ `
1711
+ ${"\u2501".repeat(51)}
1712
+ Step ${index + 1}: ${name}
1713
+ Started: ${(/* @__PURE__ */ new Date()).toISOString()}
1714
+ ${"\u2501".repeat(51)}
1715
+ `
1716
+ );
1717
+ }
1718
+ onStepComplete() {
1719
+ const durS = ((Date.now() - this.stepStartMs) / 1e3).toFixed(1);
1720
+ this.appendLog(`
1721
+ Step completed in ${durS}s
1722
+ `);
1723
+ this.finalizeComplexSequence();
1724
+ }
1725
+ onStepError(error) {
1726
+ this.appendLog(`
1727
+ Step failed: ${error.message}
1728
+ `);
1729
+ this.finalizeComplexSequence();
1730
+ }
1731
+ // --------------------------------------------------------------------------
1732
+ // Tool calls → complex sequence highlights
1733
+ // --------------------------------------------------------------------------
1734
+ onTool(tool, input) {
1735
+ const desc = toolSummary(tool, input);
1736
+ this.appendLog(` [${tool}] ${desc}`);
1737
+ this.toolCount++;
1738
+ if (this.toolCount === 3) {
1739
+ this.complexSequenceFile = join3(
1740
+ this.highlightsDir,
1741
+ `${this.timestamp}_step${this.stepIndex + 1}_complex_sequence.md`
1742
+ );
1743
+ writeFileSync3(
1744
+ this.complexSequenceFile,
1745
+ [
1746
+ "# Complex Tool Sequence",
1747
+ "",
1748
+ `**Task:** ${this.taskName}`,
1749
+ `**Step:** ${this.stepName}`,
1750
+ `**Timestamp:** ${(/* @__PURE__ */ new Date()).toISOString()}`,
1751
+ "",
1752
+ "---",
1753
+ "",
1754
+ "## Claude's Tool Orchestration",
1755
+ "",
1756
+ "Claude used multiple tools to complete this step:",
1757
+ ""
1758
+ ].join("\n")
1759
+ );
1760
+ }
1761
+ if (this.toolCount >= 3 && this.complexSequenceFile) {
1762
+ appendFileSync(this.complexSequenceFile, `${this.toolCount}. **${tool}** - ${desc}
1763
+ `);
1764
+ }
1765
+ }
1766
+ finalizeComplexSequence() {
1767
+ if (this.toolCount >= 3 && this.complexSequenceFile) {
1768
+ appendFileSync(
1769
+ this.complexSequenceFile,
1770
+ `
1771
+ ---
1772
+
1773
+ *Total tools used: ${this.toolCount}*
1774
+
1775
+ *Captured by Executant Logger*
1776
+ `
1777
+ );
1778
+ }
1779
+ }
1780
+ // --------------------------------------------------------------------------
1781
+ // Log events → judge / self-healing highlights
1782
+ // --------------------------------------------------------------------------
1783
+ onLogMessage(level, text) {
1784
+ this.appendLog(`[${level}] ${text}`);
1785
+ if (/\[judge\]\s+PASS/i.test(text)) {
1786
+ this.judgeAttempt++;
1787
+ this.saveJudgeHighlight("PASS", text);
1788
+ return;
1789
+ }
1790
+ if (/\[judge\]\s+FAIL/i.test(text)) {
1791
+ this.judgeAttempt++;
1792
+ this.saveJudgeHighlight("FAIL", text);
1793
+ return;
1794
+ }
1795
+ const healingMatch = text.match(/\[self-healing\].*failed.*exit\s+(\d+)/i);
1796
+ if (healingMatch) {
1797
+ this.startSelfHealingHighlight(healingMatch[1]);
1798
+ return;
1799
+ }
1800
+ if (/\[self-healing\].*Re-running/i.test(text)) {
1801
+ this.completeSelfHealingHighlight();
1802
+ }
1803
+ }
1804
+ // --------------------------------------------------------------------------
1805
+ // Highlight writers
1806
+ // --------------------------------------------------------------------------
1807
+ saveJudgeHighlight(verdict, output) {
1808
+ const file = join3(
1809
+ this.highlightsDir,
1810
+ `${this.timestamp}_step${this.stepIndex + 1}_judge_${verdict}.md`
1811
+ );
1812
+ writeFileSync3(
1813
+ file,
1814
+ [
1815
+ `# Judge Verdict: ${verdict}`,
1816
+ "",
1817
+ `**Task:** ${this.taskName}`,
1818
+ `**Step:** ${this.stepName}`,
1819
+ `**Attempt:** ${this.judgeAttempt}`,
1820
+ `**Timestamp:** ${(/* @__PURE__ */ new Date()).toISOString()}`,
1821
+ "",
1822
+ "---",
1823
+ "",
1824
+ output,
1825
+ "",
1826
+ "---",
1827
+ "",
1828
+ "*Auto-captured*",
1829
+ ""
1830
+ ].join("\n")
1831
+ );
1832
+ }
1833
+ startSelfHealingHighlight(exitCode) {
1834
+ this.selfHealingFile = join3(
1835
+ this.highlightsDir,
1836
+ `${this.timestamp}_step${this.stepIndex + 1}_self_healing.md`
1837
+ );
1838
+ const errorOutput = this.recentOutput.join("\n");
1839
+ this.recentOutput = [];
1840
+ writeFileSync3(
1841
+ this.selfHealingFile,
1842
+ [
1843
+ "# Self-Healing Activation",
1844
+ "",
1845
+ `**Task:** ${this.taskName}`,
1846
+ `**Step:** ${this.stepName}`,
1847
+ `**Timestamp:** ${(/* @__PURE__ */ new Date()).toISOString()}`,
1848
+ "",
1849
+ "---",
1850
+ "",
1851
+ "## \u274C Failure Detected",
1852
+ "",
1853
+ `**Exit Code:** ${exitCode}`,
1854
+ "",
1855
+ "**Recent Output:**",
1856
+ "```",
1857
+ errorOutput,
1858
+ "```",
1859
+ "",
1860
+ "---",
1861
+ "",
1862
+ "## \u{1F527} Claude's Healing Process",
1863
+ ""
1864
+ ].join("\n")
1865
+ );
1866
+ }
1867
+ completeSelfHealingHighlight() {
1868
+ if (!this.selfHealingFile) return;
1869
+ appendFileSync(
1870
+ this.selfHealingFile,
1871
+ [
1872
+ "",
1873
+ "*(See full log for Claude's diagnostic process)*",
1874
+ "",
1875
+ "---",
1876
+ "",
1877
+ "## \u2705 Resolution Applied",
1878
+ "",
1879
+ "The self-healing process completed. Check the full execution log to see Claude's analysis and fix.",
1880
+ "",
1881
+ "---",
1882
+ "",
1883
+ "*Auto-captured*",
1884
+ ""
1885
+ ].join("\n")
1886
+ );
1887
+ this.selfHealingFile = "";
1888
+ }
1889
+ // --------------------------------------------------------------------------
1890
+ // Workflow complete → index
1891
+ // --------------------------------------------------------------------------
1892
+ onWorkflowComplete() {
1893
+ this.appendLog(
1894
+ `
1895
+ ${"\u2501".repeat(51)}
1896
+ Task Complete: ${this.taskName}
1897
+ Finished: ${(/* @__PURE__ */ new Date()).toISOString()}
1898
+ ${"\u2501".repeat(51)}
1899
+ `
1900
+ );
1901
+ this.writeHighlightsIndex();
1902
+ }
1903
+ writeHighlightsIndex() {
1904
+ const indexFile = join3(this.highlightsDir, "README.md");
1905
+ if (!existsSync2(indexFile)) {
1906
+ writeFileSync3(
1907
+ indexFile,
1908
+ [
1909
+ "# Execution Highlights",
1910
+ "",
1911
+ "This directory contains automatically extracted highlight moments from task executions.",
1912
+ "",
1913
+ "## Latest Highlights",
1914
+ ""
1915
+ ].join("\n")
1916
+ );
1917
+ }
1918
+ const files = readdirSync(this.highlightsDir);
1919
+ const taskHighlights = files.filter((f) => f.startsWith(this.timestamp) && f.endsWith(".md")).sort();
1920
+ if (taskHighlights.length > 0) {
1921
+ const entries = taskHighlights.map((f) => `- [${f.replace(/\.md$/, "")}](./${f})`).join("\n");
1922
+ appendFileSync(indexFile, `
1923
+ ### ${this.taskName} (${(/* @__PURE__ */ new Date()).toISOString()})
1924
+ ${entries}
1925
+ `);
1926
+ }
1927
+ }
1928
+ // --------------------------------------------------------------------------
1929
+ // Log file writes
1930
+ // --------------------------------------------------------------------------
1931
+ appendLog(text) {
1932
+ if (!this.logFile) return;
1933
+ appendFileSync(this.logFile, text + "\n");
1934
+ }
1935
+ };
1936
+ async function* withLogger(gen, logger2) {
1937
+ for await (const event of gen) {
1938
+ logger2.observe(event);
1939
+ yield event;
1940
+ }
1941
+ }
1942
+
1943
+ // src/retrospective.ts
1944
+ import { existsSync as existsSync3, mkdirSync as mkdirSync4, readdirSync as readdirSync2, readFileSync as readFileSync4, writeFileSync as writeFileSync4 } from "node:fs";
1945
+ import { basename, dirname as dirname4, join as join4, resolve as resolve3 } from "node:path";
1946
+ import { fileURLToPath as fileURLToPath3 } from "node:url";
1947
+ import { spawnSync } from "node:child_process";
1948
+ import { load as parseYaml2 } from "js-yaml";
1949
+ import { z as z4 } from "zod";
1950
+ var RetrospectiveOutputSchema = z4.object({
1951
+ improved_yaml: z4.string(),
1952
+ changelog: z4.string()
1953
+ });
1954
+ var PROMPTS_DIR3 = join4(dirname4(fileURLToPath3(import.meta.url)), "prompts");
1955
+ var RETROSPECTIVE_PROMPT = readFileSync4(join4(PROMPTS_DIR3, "retrospective-analysis.txt"), "utf8");
1956
+ async function runRetrospective(workflowFilePath, workflow2, highlightsDir, runTimestamp) {
1957
+ try {
1958
+ await doRetrospective(workflowFilePath, workflow2, highlightsDir, runTimestamp);
1959
+ } catch (err) {
1960
+ const msg = err instanceof Error ? err.message : String(err);
1961
+ console.warn(`
1962
+ Self-improvement: retrospective failed: ${msg}`);
1963
+ }
1964
+ }
1965
+ async function doRetrospective(workflowFilePath, workflow2, highlightsDir, runTimestamp) {
1966
+ if (!existsSync3(highlightsDir)) {
1967
+ console.log("\nSelf-improvement: no highlights directory found, skipping.");
1968
+ return;
1969
+ }
1970
+ const allFiles = readdirSync2(highlightsDir);
1971
+ const runHighlights = allFiles.filter((f) => f.startsWith(runTimestamp) && f.endsWith(".md")).sort();
1972
+ if (runHighlights.length === 0) {
1973
+ console.log("\nSelf-improvement: no highlights for this run \u2014 task completed without issues, skipping.");
1974
+ return;
1975
+ }
1976
+ const divider = "\u2501".repeat(51);
1977
+ console.log(`
1978
+ ${divider}`);
1979
+ console.log("Self-Improvement: Analyzing execution and generating improvements...");
1980
+ console.log(`${divider}
1981
+ `);
1982
+ console.log(`Found ${runHighlights.length} highlight(s) to analyze`);
1983
+ const countByPattern = (pat) => runHighlights.filter((f) => f.includes(pat)).length;
1984
+ const judgeFailures = countByPattern("_judge_FAIL");
1985
+ const selfHealingCount = countByPattern("_self_healing");
1986
+ const complexSequences = countByPattern("_complex_sequence");
1987
+ const metrics = [
1988
+ `- Judge Failures: ${judgeFailures}`,
1989
+ `- Self-Healing Activations: ${selfHealingCount}`,
1990
+ `- Complex Tool Sequences: ${complexSequences}`,
1991
+ `- Total Highlights: ${runHighlights.length}`
1992
+ ].join("\n");
1993
+ console.log(`
1994
+ Execution Metrics:
1995
+ ${metrics}
1996
+ `);
1997
+ console.log("Analyzing execution and generating improvements...\n");
1998
+ const highlightContents = runHighlights.map((f) => {
1999
+ const content = readFileSync4(join4(highlightsDir, f), "utf8");
2000
+ return `### ${f}
2001
+
2002
+ ${content}`;
2003
+ }).join("\n\n---\n\n");
2004
+ const originalYaml = readFileSync4(workflowFilePath, "utf8");
2005
+ const taskName = basename(workflowFilePath, ".yaml");
2006
+ const prompt = RETROSPECTIVE_PROMPT.replaceAll("{{TASK_NAME}}", taskName).replaceAll("{{ORIGINAL_GOAL}}", workflow2.goal).replaceAll("{{ORIGINAL_YAML}}", originalYaml).replaceAll("{{HIGHLIGHTS}}", highlightContents).replaceAll("{{METRICS}}", metrics);
2007
+ const result = spawnSync(
2008
+ "claude",
2009
+ [
2010
+ "-p",
2011
+ prompt,
2012
+ "--allowedTools",
2013
+ "Read",
2014
+ "--permission-mode",
2015
+ "bypassPermissions",
2016
+ "--output-format",
2017
+ "text"
2018
+ ],
2019
+ { encoding: "utf8", maxBuffer: 10 * 1024 * 1024 }
2020
+ );
2021
+ if (result.error) {
2022
+ console.warn(`Self-improvement: failed to run claude: ${result.error.message}`);
2023
+ return;
2024
+ }
2025
+ if (result.status !== 0) {
2026
+ const stderr = result.stderr ?? "";
2027
+ console.warn(`Self-improvement: claude exited with code ${result.status}${stderr ? ": " + stderr : ""}`);
2028
+ return;
2029
+ }
2030
+ const response = result.stdout ?? "";
2031
+ let parsed;
2032
+ try {
2033
+ parsed = JSON.parse(extractJson(response));
2034
+ } catch {
2035
+ console.warn(`Self-improvement: could not parse Claude response as JSON.
2036
+ Response: ${response.trim()}`);
2037
+ return;
2038
+ }
2039
+ const zodResult = RetrospectiveOutputSchema.safeParse(parsed);
2040
+ if (!zodResult.success) {
2041
+ console.warn("Self-improvement: response schema mismatch \u2014 improved YAML not saved.");
2042
+ return;
2043
+ }
2044
+ const improvedYaml = zodResult.data.improved_yaml.trim();
2045
+ const changelog = zodResult.data.changelog.trim() || "No changelog generated.";
2046
+ try {
2047
+ parseYaml2(improvedYaml);
2048
+ } catch (err) {
2049
+ const msg = err instanceof Error ? err.message : String(err);
2050
+ console.warn(`Self-improvement: generated YAML is invalid (${msg}), skipping save.`);
2051
+ return;
2052
+ }
2053
+ const startDir = dirname4(resolve3(workflowFilePath));
2054
+ const executantLocal = findExecutantLocalDir(startDir);
2055
+ const backlogDir = executantLocal ? join4(executantLocal, "tasks", "backlog") : join4(startDir, "..", "backlog");
2056
+ mkdirSync4(backlogDir, { recursive: true });
2057
+ const ts = formatTimestamp(/* @__PURE__ */ new Date());
2058
+ const slug = slugify(taskName, 40);
2059
+ const improvedFile = join4(backlogDir, `${ts}-${slug}-improved.yaml`);
2060
+ const changelogFile = join4(backlogDir, `${ts}-${slug}-changelog.md`);
2061
+ writeFileSync4(improvedFile, improvedYaml + "\n", "utf8");
2062
+ writeFileSync4(changelogFile, changelog + "\n", "utf8");
2063
+ console.log(`\u2705 Improved task saved: ${improvedFile}`);
2064
+ console.log(`\u2705 Changelog saved: ${changelogFile}`);
2065
+ console.log(`
2066
+ ${divider}`);
2067
+ console.log("Improvement Summary");
2068
+ console.log(`${divider}
2069
+ `);
2070
+ console.log(changelog);
2071
+ }
2072
+ function extractJson(text) {
2073
+ const start = text.indexOf("{");
2074
+ const end = text.lastIndexOf("}");
2075
+ if (start === -1 || end === -1 || end <= start) throw new Error("no JSON object found in response");
2076
+ return text.slice(start, end + 1);
2077
+ }
2078
+
2079
+ // src/index.ts
2080
+ var CURRENT_VERSION = JSON.parse(
2081
+ readFileSync5(join5(dirname5(fileURLToPath4(import.meta.url)), "../package.json"), "utf-8")
2082
+ ).version;
2083
+ var rawArgs = process.argv.slice(2);
2084
+ if (rawArgs[0] === "plan") {
2085
+ const planArgs = parsePlanArgs(rawArgs.slice(1));
2086
+ const planEvents = streamPlan(planArgs);
2087
+ const inkApp = render(React3.createElement(PlanApp, {
2088
+ description: planArgs.description,
2089
+ events: planEvents
2090
+ }));
2091
+ try {
2092
+ await inkApp.waitUntilExit();
2093
+ } catch {
2094
+ }
2095
+ process.exit(0);
2096
+ }
2097
+ if (rawArgs[0] === "update") {
2098
+ const { checkForUpdate: checkForUpdate2, doUpdate: doUpdate2 } = await Promise.resolve().then(() => (init_update(), update_exports));
2099
+ const newer = await checkForUpdate2(CURRENT_VERSION);
2100
+ if (!newer) process.exit(0);
2101
+ console.log(`Updating to v${newer}...`);
2102
+ try {
2103
+ await doUpdate2();
2104
+ console.log("Done.");
2105
+ } catch (err) {
2106
+ console.error("Update failed:", err instanceof Error ? err.message : String(err));
2107
+ process.exit(1);
2108
+ }
2109
+ process.exit(0);
2110
+ }
2111
+ if (rawArgs.length === 0 || rawArgs[0] === "--help" || rawArgs[0] === "-h") {
2112
+ console.log(`Usage: executant [options] <workflow.yaml>
2113
+ executant update
2114
+
2115
+ Version: ${CURRENT_VERSION}
2116
+
2117
+ Options:
2118
+ --ci Headless mode \u2014 print events as NDJSON, no TUI
2119
+ --step <name|index> Run only the named step or step at 1-based index
2120
+ --from-step <n> Skip steps before n and run from there (1-based)
2121
+ --help, -h Show this help
2122
+
2123
+ Commands:
2124
+ plan <description> Generate a task YAML from a natural language description
2125
+ update Upgrade executant to the latest version
2126
+
2127
+ YAML \u2014 top-level fields:
2128
+ goal string (required) Description shown in the TUI header
2129
+ steps array (required) Ordered list of steps
2130
+ vars map Key/value pairs substituted as {{var_name}} in prompts/commands
2131
+
2132
+ YAML \u2014 step fields (all step types):
2133
+ name string (required) Unique identifier for the step
2134
+ type string prompt | script | command | log (inferred if omitted)
2135
+ continue_on_error bool Keep going if step fails (default: false)
2136
+ forEach string or list
2137
+ Inline YAML array OR a shell command whose newline-split
2138
+ stdout provides the items. {{item}} is substituted per
2139
+ iteration in the inner step's prompt or command.
2140
+
2141
+ YAML \u2014 prompt step fields (type: prompt, or inferred when prompt is present):
2142
+ prompt string (required) Instructions sent to Claude
2143
+ allowed_tools list Claude tools to permit
2144
+ (default: Read,Edit,Write,Bash,Glob,Grep)
2145
+ llm_as_judge bool After completion, Claude evaluates output quality;
2146
+ retries up to 5x on FAIL (default: false)
2147
+
2148
+ YAML \u2014 script step fields (type: script | command, or inferred when command is present):
2149
+ command string (required) Bash command to execute
2150
+ self_healing bool On failure, Claude diagnoses and fixes iteratively
2151
+ up to 5 attempts with accumulated context (default: true)
2152
+ max_healing_attempts int Override max self-healing retries (default: 5)
2153
+
2154
+ YAML \u2014 log step fields (type: log, or inferred when message is present and prompt is absent):
2155
+ message string Text to emit as a progress marker
2156
+
2157
+ Type inference (when type is omitted):
2158
+ has command field \u2192 script
2159
+ has message, no prompt \u2192 log
2160
+ otherwise \u2192 prompt
2161
+
2162
+ Example:
2163
+ goal: "Check and summarise recent changes"
2164
+ steps:
2165
+ - name: git-log
2166
+ type: script
2167
+ command: git log --oneline -10
2168
+
2169
+ - name: summarise
2170
+ prompt: |
2171
+ Summarise the 10 commits shown above in one short paragraph.
2172
+ Focus on the theme of changes, not individual commit messages.`);
2173
+ process.exit(0);
2174
+ }
2175
+ var ciMode = false;
2176
+ var stepFilter;
2177
+ var fromStep;
2178
+ var positional = [];
2179
+ for (let i = 0; i < rawArgs.length; i++) {
2180
+ const a = rawArgs[i];
2181
+ if (a === "--ci") {
2182
+ ciMode = true;
2183
+ } else if (a === "--step") {
2184
+ if (!rawArgs[i + 1]) {
2185
+ console.error("--step requires a value");
2186
+ process.exit(1);
2187
+ }
2188
+ stepFilter = rawArgs[++i];
2189
+ } else if (a === "--from-step") {
2190
+ if (!rawArgs[i + 1]) {
2191
+ console.error("--from-step requires a value");
2192
+ process.exit(1);
2193
+ }
2194
+ fromStep = parseInt(rawArgs[++i], 10);
2195
+ if (isNaN(fromStep)) {
2196
+ console.error("--from-step must be a number");
2197
+ process.exit(1);
2198
+ }
2199
+ } else {
2200
+ positional.push(a);
2201
+ }
2202
+ }
2203
+ var filePath = positional[0];
2204
+ if (!filePath) {
2205
+ console.error("Error: no workflow file specified");
2206
+ process.exit(1);
2207
+ }
2208
+ var workflow;
2209
+ try {
2210
+ workflow = loadWorkflow(filePath);
2211
+ } catch (err) {
2212
+ console.error(err instanceof Error ? err.message : String(err));
2213
+ process.exit(1);
2214
+ }
2215
+ var options = { stepFilter, fromStep };
2216
+ var rawEvents = runWorkflow(workflow, options);
2217
+ var logger = new Logger(resolveLogDir(filePath), workflow.goal);
2218
+ var events = withLogger(rawEvents, logger);
2219
+ var updateCheck = checkForUpdate(CURRENT_VERSION);
2220
+ function errorReplacer(_key, value) {
2221
+ if (value instanceof Error) {
2222
+ return { name: value.name, message: value.message, stack: value.stack };
2223
+ }
2224
+ return value;
2225
+ }
2226
+ async function maybeRunRetrospective(filePath2, workflow2, logger2) {
2227
+ if (!logger2) return;
2228
+ try {
2229
+ await runRetrospective(filePath2, workflow2, logger2.getHighlightsDir(), logger2.getTimestamp());
2230
+ } catch (err) {
2231
+ console.warn("[executant] retrospective failed (non-fatal):", err instanceof Error ? err.message : err);
2232
+ }
2233
+ }
2234
+ if (ciMode) {
2235
+ (async () => {
2236
+ for await (const event of events) {
2237
+ process.stdout.write(JSON.stringify(event, errorReplacer) + "\n");
2238
+ }
2239
+ if (workflow.selfImprove) {
2240
+ await maybeRunRetrospective(filePath, workflow, logger);
2241
+ }
2242
+ })().catch((err) => {
2243
+ console.error(err);
2244
+ process.exit(1);
2245
+ });
2246
+ } else {
2247
+ const inkApp = render(React3.createElement(App, { workflow, events, options, updateCheck }));
2248
+ if (workflow.selfImprove) {
2249
+ inkApp.waitUntilExit().then(() => maybeRunRetrospective(filePath, workflow, logger)).catch(() => {
2250
+ });
2251
+ }
2252
+ }