executant 1.6.0 → 1.8.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 CHANGED
@@ -54,28 +54,84 @@ import React3 from "react";
54
54
  import { render } from "ink";
55
55
  import { readFileSync as readFileSync6 } from "node:fs";
56
56
  import { dirname as dirname5, join as join5 } from "node:path";
57
- import { fileURLToPath as fileURLToPath3 } from "node:url";
57
+ import { fileURLToPath as fileURLToPath2 } from "node:url";
58
58
 
59
59
  // src/load-workflow.ts
60
- import { readFileSync } from "node:fs";
60
+ import { readFileSync as readFileSync2 } from "node:fs";
61
61
  import { load as parseYaml } from "js-yaml";
62
+
63
+ // src/lib/utils.ts
64
+ import { readFileSync } from "node:fs";
65
+ import { basename, dirname, join } from "node:path";
66
+ import { fileURLToPath } from "node:url";
67
+ var __dir = dirname(fileURLToPath(import.meta.url));
68
+ var PROMPTS_DIR = basename(__dir) === "lib" ? join(__dir, "..", "prompts") : join(__dir, "prompts");
69
+ function stripPromptHeader(raw) {
70
+ return raw.replace(/^(#[^\n]*\n)+\n?/, "").trim();
71
+ }
72
+ function loadPrompt(name) {
73
+ return stripPromptHeader(readFileSync(join(PROMPTS_DIR, `${name}.txt`), "utf8"));
74
+ }
75
+ function findOutermostBraces(text) {
76
+ const start = text.indexOf("{");
77
+ if (start === -1) return null;
78
+ let depth = 0;
79
+ for (let i = start; i < text.length; i++) {
80
+ if (text[i] === "{") depth++;
81
+ else if (text[i] === "}" && --depth === 0) return { start, end: i };
82
+ }
83
+ return null;
84
+ }
85
+ function extractJsonObject(text) {
86
+ const fenceMatch = text.match(/```(?:json)?\s*\n([\s\S]*?)\n```/);
87
+ if (fenceMatch) return fenceMatch[1].trim();
88
+ const bounds = findOutermostBraces(text);
89
+ return bounds ? text.slice(bounds.start, bounds.end + 1) : text.trim();
90
+ }
91
+ function getErrorMessage(err) {
92
+ return err instanceof Error ? err.message : String(err);
93
+ }
94
+ function fillTemplate(template, vars) {
95
+ return Object.entries(vars).reduce(
96
+ (acc, [key, val]) => acc.replaceAll(`{{${key}}}`, val),
97
+ template
98
+ );
99
+ }
100
+ function formatZodIssues(issues) {
101
+ return issues.map((i) => ` ${i.path.join(".")}: ${i.message}`).join("\n");
102
+ }
103
+ function slugify(text, maxLen = 20) {
104
+ return text.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, maxLen).replace(/-+$/, "");
105
+ }
106
+ function formatTimestamp(d) {
107
+ const p = (n) => String(n).padStart(2, "0");
108
+ return `${d.getFullYear()}${p(d.getMonth() + 1)}${p(d.getDate())}-${p(d.getHours())}${p(d.getMinutes())}${p(d.getSeconds())}`;
109
+ }
110
+ function timestamp() {
111
+ return formatTimestamp(/* @__PURE__ */ new Date());
112
+ }
113
+
114
+ // src/load-workflow.ts
62
115
  import { z } from "zod";
63
- var RawStepSchema = z.object({
64
- name: z.string(),
65
- type: z.enum(["prompt", "script", "log", "command"]).optional(),
66
- prompt: z.string().optional(),
67
- command: z.string().optional(),
68
- message: z.string().optional(),
69
- continue_on_error: z.boolean().optional(),
70
- self_healing: z.boolean().optional(),
71
- max_healing_attempts: z.number().int().positive().optional(),
72
- output: z.string().optional(),
73
- llm_as_judge: z.boolean().optional(),
74
- allowed_tools: z.array(z.string()).optional(),
75
- forEach: z.union([z.array(z.string()), z.string()]).optional(),
76
- repeat: z.number().int().positive().optional(),
77
- context: z.array(z.string()).optional()
78
- });
116
+ var RawStepSchema = z.lazy(
117
+ () => z.object({
118
+ name: z.string(),
119
+ type: z.enum(["prompt", "script", "log", "command"]).optional(),
120
+ prompt: z.string().optional(),
121
+ command: z.string().optional(),
122
+ message: z.string().optional(),
123
+ continue_on_error: z.boolean().optional(),
124
+ self_healing: z.boolean().optional(),
125
+ max_healing_attempts: z.number().int().positive().optional(),
126
+ output: z.string().optional(),
127
+ llm_as_judge: z.boolean().optional(),
128
+ allowed_tools: z.array(z.string()).optional(),
129
+ forEach: z.union([z.array(z.string()), z.string()]).optional(),
130
+ repeat: z.number().int().positive().optional(),
131
+ context: z.array(z.string()).optional(),
132
+ steps: z.array(RawStepSchema).min(1).optional()
133
+ })
134
+ );
79
135
  var RawWorkflowSchema = z.object({
80
136
  goal: z.string(),
81
137
  steps: z.array(RawStepSchema),
@@ -85,16 +141,17 @@ var RawWorkflowSchema = z.object({
85
141
  function loadWorkflow(filePath2) {
86
142
  let raw;
87
143
  try {
88
- raw = readFileSync(filePath2, "utf8");
144
+ raw = readFileSync2(filePath2, "utf8");
89
145
  } catch (err) {
90
- const msg = err instanceof Error ? err.message : String(err);
91
- throw new Error(`Cannot read workflow file "${filePath2}": ${msg}`);
146
+ throw new Error(
147
+ `Cannot read workflow file "${filePath2}": ${getErrorMessage(err)}`
148
+ );
92
149
  }
93
150
  let doc;
94
151
  try {
95
152
  doc = RawWorkflowSchema.parse(parseYaml(raw));
96
153
  } catch (err) {
97
- const detail = err instanceof z.ZodError ? err.errors.map((e) => ` ${e.path.join(".")}: ${e.message}`).join("\n") : String(err);
154
+ const detail = err instanceof z.ZodError ? formatZodIssues(err.errors) : String(err);
98
155
  throw new Error(`Invalid workflow file "${filePath2}":
99
156
  ${detail}`);
100
157
  }
@@ -107,32 +164,35 @@ ${detail}`);
107
164
  };
108
165
  }
109
166
  function convertStep(step, vars) {
110
- const name = step.name;
111
- const continueOnError = step.continue_on_error ?? false;
167
+ const { name, continue_on_error: continueOnError = false } = step;
112
168
  if (step.repeat !== void 0 && step.forEach !== void 0) {
113
169
  throw new Error(`Step "${name}" cannot have both repeat and forEach`);
114
170
  }
115
- if (step.repeat !== void 0) {
116
- const items = Array.from({ length: step.repeat }, (_2, i) => String(i + 1));
117
- const { repeat: _, ...innerStep } = step;
118
- return {
119
- type: "forEach",
120
- name,
121
- continueOnError,
122
- forEach: items,
123
- inner: convertInnerStep(innerStep, vars, name, continueOnError)
171
+ if (step.repeat !== void 0 || step.forEach !== void 0) {
172
+ if (step.steps && (step.command || step.prompt || step.message)) {
173
+ throw new Error(
174
+ `Step "${name}" cannot have both steps and command/prompt/message`
175
+ );
176
+ }
177
+ const forEachValue = step.repeat !== void 0 ? Array.from({ length: step.repeat }, (_, i) => String(i + 1)) : step.forEach;
178
+ const stepWithoutLoop = {
179
+ ...step,
180
+ repeat: void 0,
181
+ forEach: void 0,
182
+ steps: void 0
124
183
  };
125
- }
126
- if (step.forEach !== void 0) {
127
- const { forEach: _, ...innerStep } = step;
184
+ const inner = step.steps ? step.steps.map((s) => convertStep(s, vars)) : [convertInnerStep(stepWithoutLoop, vars, name, continueOnError)];
128
185
  return {
129
186
  type: "forEach",
130
187
  name,
131
188
  continueOnError,
132
- forEach: step.forEach,
133
- inner: convertInnerStep(innerStep, vars, name, continueOnError)
189
+ forEach: forEachValue,
190
+ inner
134
191
  };
135
192
  }
193
+ if (step.steps) {
194
+ throw new Error(`Step "${name}" has steps but no forEach or repeat`);
195
+ }
136
196
  return convertInnerStep(step, vars, name, continueOnError);
137
197
  }
138
198
  function convertInnerStep(step, vars, name, continueOnError) {
@@ -140,7 +200,8 @@ function convertInnerStep(step, vars, name, continueOnError) {
140
200
  switch (effectiveType) {
141
201
  case "script":
142
202
  case "command": {
143
- if (!step.command) throw new Error(`Step "${name}" has type script but no command`);
203
+ if (!step.command)
204
+ throw new Error(`Step "${name}" has type script but no command`);
144
205
  return {
145
206
  type: "command",
146
207
  name,
@@ -148,7 +209,9 @@ function convertInnerStep(step, vars, name, continueOnError) {
148
209
  continueOnError,
149
210
  selfHealing: step.self_healing === true,
150
211
  maxHealingAttempts: step.max_healing_attempts,
151
- ...step.output && { output: resolveOutputFile(step.output, vars, name) }
212
+ ...step.output && {
213
+ output: resolveOutputFile(step.output, vars, name)
214
+ }
152
215
  };
153
216
  }
154
217
  case "log": {
@@ -161,7 +224,8 @@ function convertInnerStep(step, vars, name, continueOnError) {
161
224
  };
162
225
  }
163
226
  case "prompt": {
164
- if (!step.prompt) throw new Error(`Step "${name}" has type prompt but no prompt field`);
227
+ if (!step.prompt)
228
+ throw new Error(`Step "${name}" has type prompt but no prompt field`);
165
229
  const contextFiles = resolveContextFiles(step.context, vars, name);
166
230
  return {
167
231
  type: "claude",
@@ -184,22 +248,23 @@ function inferType(step) {
184
248
  }
185
249
  function resolveVarPath(varName, vars, stepName, label) {
186
250
  if (!(varName in vars)) {
187
- throw new Error(`Step "${stepName}" ${label} references undefined var "${varName}" \u2014 add it to the vars section`);
251
+ throw new Error(
252
+ `Step "${stepName}" ${label} references undefined var "${varName}" \u2014 add it to the vars section`
253
+ );
188
254
  }
189
255
  return vars[varName];
190
256
  }
191
257
  function resolveContextFiles(contextVarNames, vars, stepName) {
192
258
  if (!contextVarNames || contextVarNames.length === 0) return [];
193
- return contextVarNames.map((varName) => resolveVarPath(varName, vars, stepName, "context"));
259
+ return contextVarNames.map(
260
+ (varName) => resolveVarPath(varName, vars, stepName, "context")
261
+ );
194
262
  }
195
263
  function resolveOutputFile(varName, vars, stepName) {
196
264
  return resolveVarPath(varName, vars, stepName, "output");
197
265
  }
198
266
  function substituteVars(text, vars, stepName, field) {
199
- const result = Object.entries(vars).reduce(
200
- (acc, [key, value]) => acc.replaceAll(`{{${key}}}`, value),
201
- text
202
- );
267
+ const result = fillTemplate(text, vars);
203
268
  const unknownTokens = [...result.matchAll(/\{\{(\w+)\}\}/g)].map((m) => m[1]).filter((key) => key !== "item");
204
269
  if (unknownTokens.length > 0) {
205
270
  throw new Error(
@@ -305,47 +370,6 @@ async function* runCommand(task) {
305
370
  // src/tasks/claude.ts
306
371
  import { execSync, spawn as spawn2 } from "node:child_process";
307
372
  import { zodToJsonSchema } from "zod-to-json-schema";
308
-
309
- // src/lib/utils.ts
310
- import { readFileSync as readFileSync2 } from "node:fs";
311
- import { basename, dirname, join } from "node:path";
312
- import { fileURLToPath } from "node:url";
313
- var __dir = dirname(fileURLToPath(import.meta.url));
314
- var PROMPTS_DIR = basename(__dir) === "lib" ? join(__dir, "..", "prompts") : join(__dir, "prompts");
315
- function stripPromptHeader(raw) {
316
- return raw.replace(/^(#[^\n]*\n)+\n?/, "").trim();
317
- }
318
- function loadPrompt(name) {
319
- return stripPromptHeader(readFileSync2(join(PROMPTS_DIR, `${name}.txt`), "utf8"));
320
- }
321
- function findOutermostBraces(text) {
322
- const start = text.indexOf("{");
323
- if (start === -1) return null;
324
- let depth = 0;
325
- for (let i = start; i < text.length; i++) {
326
- if (text[i] === "{") depth++;
327
- else if (text[i] === "}" && --depth === 0) return { start, end: i };
328
- }
329
- return null;
330
- }
331
- function extractJsonObject(text) {
332
- const fenceMatch = text.match(/```(?:json)?\s*\n([\s\S]*?)\n```/);
333
- if (fenceMatch) return fenceMatch[1].trim();
334
- const bounds = findOutermostBraces(text);
335
- return bounds ? text.slice(bounds.start, bounds.end + 1) : text.trim();
336
- }
337
- function slugify(text, maxLen = 20) {
338
- return text.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, maxLen).replace(/-+$/, "");
339
- }
340
- function formatTimestamp(d) {
341
- const p = (n) => String(n).padStart(2, "0");
342
- return `${d.getFullYear()}${p(d.getMonth() + 1)}${p(d.getDate())}-${p(d.getHours())}${p(d.getMinutes())}${p(d.getSeconds())}`;
343
- }
344
- function timestamp() {
345
- return formatTimestamp(/* @__PURE__ */ new Date());
346
- }
347
-
348
- // src/tasks/claude.ts
349
373
  var DEFAULT_TOOLS = ["Read", "Edit", "Write", "Bash", "Glob", "Grep"];
350
374
  function resolveClaudePath() {
351
375
  try {
@@ -386,8 +410,7 @@ async function* runClaude(task) {
386
410
  env: { ...process.env }
387
411
  });
388
412
  } catch (err) {
389
- const msg = err instanceof Error ? err.message : String(err);
390
- throw new Error(`Failed to spawn claude (${claudeBin}): ${msg}`);
413
+ throw new Error(`Failed to spawn claude (${claudeBin}): ${getErrorMessage(err)}`);
391
414
  }
392
415
  const cleanup = () => {
393
416
  try {
@@ -491,20 +514,19 @@ var JudgeOutputSchema = z2.object({
491
514
  reasoning: z2.string().optional(),
492
515
  feedback: z2.string()
493
516
  });
517
+ function shouldSkipStep(stepNumber, name, options2) {
518
+ if (options2.stepFilter !== void 0) {
519
+ const matchByIndex = /^\d+$/.test(options2.stepFilter) && parseInt(options2.stepFilter, 10) === stepNumber;
520
+ return !matchByIndex && name !== options2.stepFilter;
521
+ }
522
+ return options2.fromStep !== void 0 && stepNumber < options2.fromStep;
523
+ }
494
524
  async function* runWorkflow(workflow2, options2 = {}) {
495
525
  const workflowStart = Date.now();
496
526
  yield { type: "workflow:start", workflow: workflow2 };
497
527
  for (const [i, task] of workflow2.tasks.entries()) {
498
528
  const stepNumber = i + 1;
499
- if (options2.stepFilter !== void 0) {
500
- const matchByIndex = /^\d+$/.test(options2.stepFilter) && parseInt(options2.stepFilter, 10) === stepNumber;
501
- const matchByName = task.name === options2.stepFilter;
502
- if (!matchByIndex && !matchByName) {
503
- yield { type: "step:skip", index: i, name: task.name };
504
- continue;
505
- }
506
- }
507
- if (options2.fromStep !== void 0 && stepNumber < options2.fromStep) {
529
+ if (shouldSkipStep(stepNumber, task.name, options2)) {
508
530
  yield { type: "step:skip", index: i, name: task.name };
509
531
  continue;
510
532
  }
@@ -512,7 +534,7 @@ async function* runWorkflow(workflow2, options2 = {}) {
512
534
  yield { type: "step:start", index: i, name: task.name };
513
535
  try {
514
536
  for await (const event of runStep(task)) {
515
- if (event.type === "step:iteration" || event.type === "output:text" || event.type === "output:tool") {
537
+ if (event.type === "step:iteration" || event.type === "step:inner" || event.type === "output:text" || event.type === "output:tool") {
516
538
  yield { ...event, index: i };
517
539
  } else {
518
540
  yield event;
@@ -573,21 +595,55 @@ async function* runLog(task) {
573
595
  async function* runForEach(task) {
574
596
  const items = await resolveItems(task.forEach);
575
597
  const total = items.length;
598
+ const innerTotal = task.inner.length;
576
599
  for (const [i, item] of items.entries()) {
577
600
  yield { type: "step:iteration", index: -1, item, iteration: i + 1, total };
578
- const substituted = substituteItem(task.inner, item);
579
- yield* runStep(substituted);
601
+ for (const [j, innerTask] of task.inner.entries()) {
602
+ const substituted = substituteItem(innerTask, item);
603
+ if (innerTotal > 1) {
604
+ yield {
605
+ type: "step:inner",
606
+ index: -1,
607
+ iteration: i + 1,
608
+ innerIndex: j,
609
+ innerTotal,
610
+ name: substituted.name
611
+ };
612
+ }
613
+ try {
614
+ yield* runStep(substituted);
615
+ } catch (err) {
616
+ const error = err instanceof Error ? err : new Error(String(err));
617
+ if (!substituted.continueOnError) {
618
+ yield {
619
+ type: "log",
620
+ level: "warn",
621
+ text: `[forEach] Step "${substituted.name}" failed \u2014 aborting remaining children and iterations`
622
+ };
623
+ throw error;
624
+ }
625
+ yield {
626
+ type: "log",
627
+ level: "warn",
628
+ text: `[forEach] Step "${substituted.name}" failed (continuing): ${error.message}`
629
+ };
630
+ }
631
+ }
580
632
  }
581
633
  }
582
634
  async function resolveItems(forEach) {
583
635
  if (Array.isArray(forEach)) return forEach.filter(Boolean);
584
636
  try {
585
- const { stdout } = await execPromise(forEach, { shell: "/bin/sh", timeout: 3e4 });
637
+ const { stdout } = await execPromise(forEach, {
638
+ shell: "/bin/sh",
639
+ timeout: 3e4
640
+ });
586
641
  return stdout.split("\n").filter((l) => l.trim().length > 0);
587
642
  } catch (err) {
588
- const msg = err instanceof Error ? err.message : String(err);
589
- throw new Error(`forEach shell command failed: ${msg}
590
- Command: ${forEach}`);
643
+ throw new Error(
644
+ `forEach shell command failed: ${getErrorMessage(err)}
645
+ Command: ${forEach}`
646
+ );
591
647
  }
592
648
  }
593
649
  function substituteItem(task, item) {
@@ -596,12 +652,24 @@ function substituteItem(task, item) {
596
652
  case "command":
597
653
  return { ...task, name: sub(task.name), command: sub(task.command) };
598
654
  case "claude":
599
- return { ...task, name: sub(task.name), prompt: sub(task.prompt), allowedTools: task.allowedTools?.map(sub) };
655
+ return {
656
+ ...task,
657
+ name: sub(task.name),
658
+ prompt: sub(task.prompt),
659
+ allowedTools: task.allowedTools?.map(sub)
660
+ };
600
661
  case "log":
601
662
  return { ...task, name: sub(task.name), message: sub(task.message) };
663
+ case "forEach":
664
+ return {
665
+ ...task,
666
+ name: sub(task.name),
667
+ forEach: Array.isArray(task.forEach) ? task.forEach : sub(task.forEach),
668
+ inner: task.inner.map((t) => substituteItem(t, item))
669
+ };
602
670
  default: {
603
671
  const _ = task;
604
- throw new Error(`Unknown inner task type: ${JSON.stringify(_)}`);
672
+ throw new Error(`Unknown task type: ${JSON.stringify(_)}`);
605
673
  }
606
674
  }
607
675
  }
@@ -613,7 +681,11 @@ async function* runCommandWithHealing(task) {
613
681
  try {
614
682
  yield* collectLines(runCommand(task), lines);
615
683
  if (attempt > 0) {
616
- yield { type: "log", level: "info", text: `[self-healing] Command passed after ${attempt + 1} attempts` };
684
+ yield {
685
+ type: "log",
686
+ level: "info",
687
+ text: `[self-healing] Command passed after ${attempt + 1} attempts`
688
+ };
617
689
  }
618
690
  return;
619
691
  } catch (err) {
@@ -621,7 +693,11 @@ async function* runCommandWithHealing(task) {
621
693
  const output = lines.join("\n");
622
694
  const remaining = maxAttempts - attempt - 1;
623
695
  if (remaining === 0) {
624
- yield { type: "log", level: "warn", text: `[self-healing] Exhausted ${maxAttempts} attempts` };
696
+ yield {
697
+ type: "log",
698
+ level: "warn",
699
+ text: `[self-healing] Exhausted ${maxAttempts} attempts`
700
+ };
625
701
  throw new Error(
626
702
  `Step "${task.name}" failed after ${maxAttempts} self-healing attempts (last exit code: ${exitCode})`
627
703
  );
@@ -632,7 +708,12 @@ async function* runCommandWithHealing(task) {
632
708
  text: `[self-healing] Attempt ${attempt + 1}/${maxAttempts} failed (exit ${exitCode}), invoking Claude to fix\u2026`
633
709
  };
634
710
  const historyBlock = buildAttemptHistory(attemptHistory);
635
- const healPrompt = buildHealingPrompt(task.command, exitCode, output, historyBlock);
711
+ const healPrompt = buildHealingPrompt(
712
+ task.command,
713
+ exitCode,
714
+ output,
715
+ historyBlock
716
+ );
636
717
  const healTask = {
637
718
  type: "claude",
638
719
  name: `${task.name}:heal-${attempt + 1}`,
@@ -643,7 +724,8 @@ async function* runCommandWithHealing(task) {
643
724
  const claudeLines = [];
644
725
  for await (const event of runClaude(healTask)) {
645
726
  if (event.type === "output:text") claudeLines.push(event.text);
646
- else if (event.type === "output:tool") toolCalls.push(formatToolCall(event.tool, event.input));
727
+ else if (event.type === "output:tool")
728
+ toolCalls.push(formatToolCall(event.tool, event.input));
647
729
  yield event;
648
730
  }
649
731
  attemptHistory.push({
@@ -651,7 +733,11 @@ async function* runCommandWithHealing(task) {
651
733
  exitCode,
652
734
  cmdOutput: output
653
735
  });
654
- yield { type: "log", level: "info", text: `[self-healing] Re-running command (${remaining} attempt(s) left)\u2026` };
736
+ yield {
737
+ type: "log",
738
+ level: "info",
739
+ text: `[self-healing] Re-running command (${remaining} attempt(s) left)\u2026`
740
+ };
655
741
  }
656
742
  }
657
743
  }
@@ -660,24 +746,40 @@ async function* runClaudeWithJudge(task) {
660
746
  for (let attempt = 0; attempt < MAX_JUDGE_RETRIES; attempt++) {
661
747
  const prompt = attempt === 0 ? task.prompt : `${task.prompt}
662
748
 
663
- ${JUDGE_RETRY_CONTEXT.replace("{{FEEDBACK}}", judgeContext)}`;
749
+ ${fillTemplate(JUDGE_RETRY_CONTEXT, { FEEDBACK: judgeContext })}`;
664
750
  const lines = [];
665
751
  yield* collectLines(runClaude({ ...task, prompt }), lines);
666
- yield { type: "log", level: "info", text: `[judge] Evaluating "${task.name}"\u2026` };
667
- const verdict = await evaluateWithJudge(task.name, task.prompt, lines.join("\n"));
752
+ yield {
753
+ type: "log",
754
+ level: "info",
755
+ text: `[judge] Evaluating "${task.name}"\u2026`
756
+ };
757
+ const verdict = await evaluateWithJudge(
758
+ task.name,
759
+ task.prompt,
760
+ lines.join("\n")
761
+ );
668
762
  if (verdict.pass) {
669
763
  yield { type: "log", level: "info", text: "[judge] PASS" };
670
764
  return;
671
765
  }
672
766
  judgeContext = verdict.feedback;
673
- yield { type: "log", level: "warn", text: `[judge] FAIL \u2014 ${verdict.feedback}` };
767
+ yield {
768
+ type: "log",
769
+ level: "warn",
770
+ text: `[judge] FAIL \u2014 ${verdict.feedback}`
771
+ };
674
772
  const remaining = MAX_JUDGE_RETRIES - attempt - 1;
675
773
  if (remaining === 0) {
676
774
  throw new Error(
677
775
  `Step "${task.name}" failed judge evaluation after ${MAX_JUDGE_RETRIES} attempts`
678
776
  );
679
777
  }
680
- yield { type: "log", level: "info", text: `[judge] Retrying (${remaining} attempt(s) left)\u2026` };
778
+ yield {
779
+ type: "log",
780
+ level: "info",
781
+ text: `[judge] Retrying (${remaining} attempt(s) left)\u2026`
782
+ };
681
783
  }
682
784
  }
683
785
  async function evaluateWithJudge(stepName, stepInstructions, output) {
@@ -704,8 +806,9 @@ function readContextFile(filePath2) {
704
806
  try {
705
807
  return readFileSync3(filePath2, "utf8");
706
808
  } catch (err) {
707
- const msg = err instanceof Error ? err.message : String(err);
708
- throw new Error(`Context file "${filePath2}" could not be read: ${msg}`);
809
+ throw new Error(
810
+ `Context file "${filePath2}" could not be read: ${getErrorMessage(err)}`
811
+ );
709
812
  }
710
813
  }
711
814
  function expandContext(task) {
@@ -719,13 +822,23 @@ ${readContextFile(fp)}
719
822
  ${task.prompt}` };
720
823
  }
721
824
  function buildHealingPrompt(command, exitCode, output, attemptHistory) {
722
- return SELF_HEALING_PROMPT.replaceAll("{{COMMAND}}", command).replaceAll("{{EXIT_CODE}}", String(exitCode)).replaceAll("{{OUTPUT}}", output).replaceAll("{{ATTEMPT_HISTORY}}", attemptHistory);
825
+ return fillTemplate(SELF_HEALING_PROMPT, {
826
+ COMMAND: command,
827
+ EXIT_CODE: String(exitCode),
828
+ OUTPUT: output,
829
+ ATTEMPT_HISTORY: attemptHistory
830
+ });
723
831
  }
724
832
  function buildJudgePrompt(stepName, instructions, output) {
725
- return JUDGE_EVALUATION_PROMPT.replace("{{STEP_NAME}}", stepName).replace("{{STEP_INSTRUCTIONS}}", instructions).replace("{{OUTPUT}}", output);
833
+ return fillTemplate(JUDGE_EVALUATION_PROMPT, {
834
+ STEP_NAME: stepName,
835
+ STEP_INSTRUCTIONS: instructions,
836
+ OUTPUT: output
837
+ });
726
838
  }
727
839
  function formatToolCall(tool, input) {
728
- if (tool === "Edit" || tool === "Write") return `${tool}(${String(input["file_path"] ?? "")})`;
840
+ if (tool === "Edit" || tool === "Write")
841
+ return `${tool}(${String(input["file_path"] ?? "")})`;
729
842
  if (tool === "Bash") return `Bash(${String(input["command"] ?? "")})`;
730
843
  return tool;
731
844
  }
@@ -842,7 +955,20 @@ function reducer(state, event) {
842
955
  };
843
956
  case "step:iteration":
844
957
  return updateTask(state, event.index, {
845
- iteration: { current: event.iteration, total: event.total, item: event.item }
958
+ iteration: {
959
+ current: event.iteration,
960
+ total: event.total,
961
+ item: event.item
962
+ },
963
+ inner: void 0
964
+ });
965
+ case "step:inner":
966
+ return updateTask(state, event.index, {
967
+ inner: {
968
+ index: event.innerIndex,
969
+ total: event.innerTotal,
970
+ name: event.name
971
+ }
846
972
  });
847
973
  case "output:text": {
848
974
  const idx = event.index;
@@ -855,7 +981,10 @@ function reducer(state, event) {
855
981
  const formatted = formatToolCall2(event.tool, event.input);
856
982
  const next = formatted ? appendLine(state, idx, formatted) : state;
857
983
  if (event.tool === "Write" && typeof event.input["file_path"] === "string") {
858
- return { ...next, writtenFiles: [...next.writtenFiles, event.input["file_path"]] };
984
+ return {
985
+ ...next,
986
+ writtenFiles: [...next.writtenFiles, event.input["file_path"]]
987
+ };
859
988
  }
860
989
  return next;
861
990
  }
@@ -878,7 +1007,9 @@ function reducer(state, event) {
878
1007
  }
879
1008
  }
880
1009
  function updateTask(state, index, patch) {
881
- const tasks = state.tasks.map((t, i) => i === index ? { ...t, ...patch } : t);
1010
+ const tasks = state.tasks.map(
1011
+ (t, i) => i === index ? { ...t, ...patch } : t
1012
+ );
882
1013
  return { ...state, tasks };
883
1014
  }
884
1015
  function appendLine(state, index, line) {
@@ -943,7 +1074,8 @@ function TaskRow({ taskState, isActive, index, tick }) {
943
1074
  const color = statusColor(status, isActive);
944
1075
  const elapsed = formatTaskElapsed(startTime, endTime, status);
945
1076
  const iterInfo = taskState.iteration ? ` (${taskState.iteration.current}/${taskState.iteration.total}) ${taskState.iteration.item}` : "";
946
- const label = `${index + 1}. ${task.name}${iterInfo}`;
1077
+ const innerInfo = taskState.inner ? ` \u2014 ${taskState.inner.name} [${taskState.inner.index + 1}/${taskState.inner.total}]` : "";
1078
+ const label = `${index + 1}. ${task.name}${iterInfo}${innerInfo}`;
947
1079
  return /* @__PURE__ */ jsxs(Box, { children: [
948
1080
  /* @__PURE__ */ jsxs(Text, { color, children: [
949
1081
  icon,
@@ -1070,12 +1202,8 @@ function App({ workflow: workflow2, events: events2, options: options2, updateCh
1070
1202
  }
1071
1203
  } catch (err) {
1072
1204
  if (!active) return;
1073
- dispatch({
1074
- type: "log",
1075
- level: "error",
1076
- text: err instanceof Error ? err.message : String(err)
1077
- });
1078
- setTimeout(() => exit(err instanceof Error ? err : new Error(String(err))), EXIT_DELAY_MS);
1205
+ dispatch({ type: "log", level: "error", text: getErrorMessage(err) });
1206
+ setTimeout(() => exit(err instanceof Error ? err : new Error(getErrorMessage(err))), EXIT_DELAY_MS);
1079
1207
  }
1080
1208
  })();
1081
1209
  return () => {
@@ -1157,25 +1285,9 @@ var PLAN_RETRY_SCHEMA_ERROR = loadPrompt("plan-retry-schema-error");
1157
1285
  var PLAN_RETRY_JUDGE = loadPrompt("plan-retry-judge");
1158
1286
  var MAX_PLAN_RETRIES = 3;
1159
1287
  var TOTAL_PLAN_STAGES = 3;
1160
- var StepSchema = z3.object({
1161
- name: z3.string(),
1162
- type: z3.enum(["prompt", "script", "log"]).optional(),
1163
- prompt: z3.string().optional(),
1164
- command: z3.string().optional(),
1165
- message: z3.string().optional(),
1166
- continue_on_error: z3.boolean().optional(),
1167
- self_healing: z3.boolean().optional(),
1168
- max_healing_attempts: z3.number().int().positive().optional(),
1169
- output: z3.string().optional(),
1170
- llm_as_judge: z3.boolean().optional(),
1171
- allowed_tools: z3.array(z3.string()).optional(),
1172
- forEach: z3.union([z3.array(z3.string()), z3.string()]).optional(),
1173
- repeat: z3.number().int().positive().optional(),
1174
- context: z3.array(z3.string()).optional()
1175
- });
1176
1288
  var WorkflowSchema = z3.object({
1177
1289
  goal: z3.string(),
1178
- steps: z3.array(StepSchema).min(1),
1290
+ steps: z3.array(RawStepSchema).min(1),
1179
1291
  vars: z3.record(z3.string()).optional(),
1180
1292
  self_improve: z3.boolean().optional()
1181
1293
  });
@@ -1195,7 +1307,10 @@ function walkUp(startDir, check) {
1195
1307
  }
1196
1308
  }
1197
1309
  function findGitRoot(startDir) {
1198
- return walkUp(startDir, (dir) => existsSync(join2(dir, ".git")) ? dir : null);
1310
+ return walkUp(
1311
+ startDir,
1312
+ (dir) => existsSync(join2(dir, ".git")) ? dir : null
1313
+ );
1199
1314
  }
1200
1315
  function findProjectRoot(startDir) {
1201
1316
  return walkUp(startDir, (dir) => {
@@ -1283,7 +1398,10 @@ async function runPass3Judge(description, workflow2) {
1283
1398
  const task = {
1284
1399
  type: "claude",
1285
1400
  name: "plan:judge",
1286
- prompt: PLAN_JUDGE_PROMPT.replace("{{DESCRIPTION}}", description).replace("{{WORKFLOW_JSON}}", JSON.stringify(workflow2, null, 2)),
1401
+ prompt: fillTemplate(PLAN_JUDGE_PROMPT, {
1402
+ DESCRIPTION: description,
1403
+ WORKFLOW_JSON: JSON.stringify(workflow2, null, 2)
1404
+ }),
1287
1405
  allowedTools: [],
1288
1406
  permissionMode: "default",
1289
1407
  model: "sonnet"
@@ -1343,34 +1461,27 @@ function normalizeWorkflow(workflow2) {
1343
1461
  return { ...workflow2, steps: collapseSequentialSteps(steps) };
1344
1462
  }
1345
1463
  function collapseSequentialSteps(steps) {
1346
- const result = [];
1347
- let i = 0;
1348
- while (i < steps.length) {
1349
- const step = steps[i];
1350
- if (step.forEach !== void 0 || step.repeat !== void 0) {
1351
- result.push(step);
1352
- i++;
1353
- continue;
1354
- }
1355
- const m = step.name.match(/^(.+?)_1$/);
1356
- if (!m) {
1357
- result.push(step);
1358
- i++;
1359
- continue;
1360
- }
1361
- const prefix = m[1];
1362
- let n = 1;
1363
- while (i + n < steps.length && steps[i + n].name === `${prefix}_${n + 1}`) n++;
1364
- if (n < 2) {
1365
- result.push(step);
1366
- i++;
1367
- continue;
1368
- }
1369
- const { name: _name, ...rest } = step;
1370
- result.push({ ...rest, name: `${prefix}_{{item}}`, repeat: n });
1371
- i += n;
1372
- }
1373
- return result;
1464
+ return steps.reduce(
1465
+ ({ out, skip }, step, i, arr) => {
1466
+ if (skip > 0) return { out, skip: skip - 1 };
1467
+ if (step.forEach !== void 0 || step.repeat !== void 0 || step.steps !== void 0) {
1468
+ return { out: [...out, step], skip: 0 };
1469
+ }
1470
+ const m = step.name.match(/^(.+?)_1$/);
1471
+ if (!m) return { out: [...out, step], skip: 0 };
1472
+ const prefix = m[1];
1473
+ let n = 1;
1474
+ while (i + n < arr.length && arr[i + n].name === `${prefix}_${n + 1}`)
1475
+ n++;
1476
+ if (n < 2) return { out: [...out, step], skip: 0 };
1477
+ const { name: _name, ...rest } = step;
1478
+ return {
1479
+ out: [...out, { ...rest, name: `${prefix}_{{item}}`, repeat: n }],
1480
+ skip: n - 1
1481
+ };
1482
+ },
1483
+ { out: [], skip: 0 }
1484
+ ).out;
1374
1485
  }
1375
1486
  async function* streamPlan(args) {
1376
1487
  const { description, taskFile } = args;
@@ -1381,14 +1492,24 @@ async function* streamPlan(args) {
1381
1492
  yield { type: "plan:stages", names: ["Decompose to Steps", "Validate"] };
1382
1493
  researchDoc = "No codebase research performed \u2014 the task is self-contained. Work directly from the user's original goal.";
1383
1494
  } else {
1384
- yield { type: "plan:stages", names: ["Research & Planning", "Decompose to Steps", "Validate"] };
1385
- yield { type: "plan:stage", stage: 1, total: TOTAL_PLAN_STAGES, name: "Research & Planning" };
1495
+ yield {
1496
+ type: "plan:stages",
1497
+ names: ["Research & Planning", "Decompose to Steps", "Validate"]
1498
+ };
1499
+ yield {
1500
+ type: "plan:stage",
1501
+ stage: 1,
1502
+ total: TOTAL_PLAN_STAGES,
1503
+ name: "Research & Planning"
1504
+ };
1386
1505
  const researchLines = [];
1387
1506
  try {
1388
1507
  const researchTask = {
1389
1508
  type: "claude",
1390
1509
  name: "plan:research",
1391
- prompt: PLAN_RESEARCH_PROMPT.replace("{{DESCRIPTION}}", description),
1510
+ prompt: fillTemplate(PLAN_RESEARCH_PROMPT, {
1511
+ DESCRIPTION: description
1512
+ }),
1392
1513
  allowedTools: ["Read", "Glob", "Grep"],
1393
1514
  permissionMode: "bypassPermissions",
1394
1515
  model: "opus"
@@ -1402,20 +1523,28 @@ async function* streamPlan(args) {
1402
1523
  }
1403
1524
  }
1404
1525
  } catch (err) {
1405
- const msg = err instanceof Error ? err.message : String(err);
1406
- yield { type: "plan:error", message: `Research pass failed: ${msg}` };
1526
+ yield {
1527
+ type: "plan:error",
1528
+ message: `Research pass failed: ${getErrorMessage(err)}`
1529
+ };
1407
1530
  return;
1408
1531
  }
1409
1532
  researchDoc = researchLines.join("\n");
1410
1533
  if (!researchDoc.trim()) {
1411
- yield { type: "plan:error", message: "Research pass produced no output \u2014 cannot decompose" };
1534
+ yield {
1535
+ type: "plan:error",
1536
+ message: "Research pass produced no output \u2014 cannot decompose"
1537
+ };
1412
1538
  return;
1413
1539
  }
1414
1540
  }
1415
- const decomposeStage = skipResearch ? 1 : 2;
1416
- const validateStage = skipResearch ? 2 : 3;
1417
- const totalStages = skipResearch ? 2 : TOTAL_PLAN_STAGES;
1418
- yield { type: "plan:stage", stage: decomposeStage, total: totalStages, name: "Decompose to Steps" };
1541
+ const stages = skipResearch ? { decompose: 1, validate: 2, total: 2 } : { decompose: 2, validate: 3, total: TOTAL_PLAN_STAGES };
1542
+ yield {
1543
+ type: "plan:stage",
1544
+ stage: stages.decompose,
1545
+ total: stages.total,
1546
+ name: "Decompose to Steps"
1547
+ };
1419
1548
  let retryPrefix = "";
1420
1549
  for (let attempt = 0; attempt < MAX_PLAN_RETRIES; attempt++) {
1421
1550
  if (attempt > 0) {
@@ -1425,9 +1554,17 @@ async function* streamPlan(args) {
1425
1554
  maxAttempts: MAX_PLAN_RETRIES,
1426
1555
  reason: retryPrefix.replace(/\n/g, " ")
1427
1556
  };
1428
- yield { type: "plan:stage", stage: decomposeStage, total: totalStages, name: "Decompose to Steps" };
1557
+ yield {
1558
+ type: "plan:stage",
1559
+ stage: stages.decompose,
1560
+ total: stages.total,
1561
+ name: "Decompose to Steps"
1562
+ };
1429
1563
  }
1430
- const basePrompt = PLAN_DECOMPOSE_PROMPT.replace("{{DESCRIPTION}}", description).replace("{{RESEARCH_DOC}}", researchDoc);
1564
+ const basePrompt = fillTemplate(PLAN_DECOMPOSE_PROMPT, {
1565
+ DESCRIPTION: description,
1566
+ RESEARCH_DOC: researchDoc
1567
+ });
1431
1568
  const decomposeTask = {
1432
1569
  type: "claude",
1433
1570
  name: "plan:decompose",
@@ -1454,12 +1591,15 @@ ${basePrompt}` : basePrompt,
1454
1591
  }
1455
1592
  }
1456
1593
  } catch (err) {
1457
- const msg = err instanceof Error ? err.message : String(err);
1594
+ const msg = getErrorMessage(err);
1458
1595
  if (attempt === MAX_PLAN_RETRIES - 1) {
1459
1596
  yield { type: "plan:error", message: msg };
1460
1597
  return;
1461
1598
  }
1462
- retryPrefix = PLAN_RETRY_PARSE_ERROR.replace("{{ERROR}}", msg).replace("{{EXCERPT}}", decomposeTextLines.join("\n"));
1599
+ retryPrefix = fillTemplate(PLAN_RETRY_PARSE_ERROR, {
1600
+ ERROR: msg,
1601
+ EXCERPT: decomposeTextLines.join("\n")
1602
+ });
1463
1603
  continue;
1464
1604
  }
1465
1605
  if (structuredOutput === void 0) {
@@ -1468,31 +1608,47 @@ ${basePrompt}` : basePrompt,
1468
1608
  yield { type: "plan:error", message: issues };
1469
1609
  return;
1470
1610
  }
1471
- retryPrefix = PLAN_RETRY_SCHEMA_ERROR.replace("{{ISSUES}}", issues);
1611
+ retryPrefix = fillTemplate(PLAN_RETRY_SCHEMA_ERROR, { ISSUES: issues });
1472
1612
  continue;
1473
1613
  }
1474
1614
  const zodResult = WorkflowSchema.safeParse(structuredOutput);
1475
1615
  if (!zodResult.success) {
1476
- const issues = zodResult.error.issues.map((i) => ` ${i.path.join(".")}: ${i.message}`).join("\n");
1616
+ const issues = formatZodIssues(zodResult.error.issues);
1477
1617
  if (attempt === MAX_PLAN_RETRIES - 1) {
1478
- yield { type: "plan:error", message: `Plan did not match expected schema:
1479
- ${issues}` };
1618
+ yield {
1619
+ type: "plan:error",
1620
+ message: `Plan did not match expected schema:
1621
+ ${issues}`
1622
+ };
1480
1623
  return;
1481
1624
  }
1482
- retryPrefix = PLAN_RETRY_SCHEMA_ERROR.replace("{{ISSUES}}", issues);
1625
+ retryPrefix = fillTemplate(PLAN_RETRY_SCHEMA_ERROR, { ISSUES: issues });
1483
1626
  continue;
1484
1627
  }
1485
- yield { type: "plan:stage", stage: validateStage, total: totalStages, name: "Validate" };
1628
+ yield {
1629
+ type: "plan:stage",
1630
+ stage: stages.validate,
1631
+ total: stages.total,
1632
+ name: "Validate"
1633
+ };
1486
1634
  const judgeResult = await runPass3Judge(description, zodResult.data);
1487
1635
  if (judgeResult.skipped) {
1488
- yield { type: "plan:warn", message: "Judge skipped due to error \u2014 proceeding without validation" };
1636
+ yield {
1637
+ type: "plan:warn",
1638
+ message: "Judge skipped due to error \u2014 proceeding without validation"
1639
+ };
1489
1640
  }
1490
1641
  if (!judgeResult.pass && attempt < MAX_PLAN_RETRIES - 1) {
1491
- retryPrefix = PLAN_RETRY_JUDGE.replace("{{FEEDBACK}}", judgeResult.feedback);
1642
+ retryPrefix = fillTemplate(PLAN_RETRY_JUDGE, {
1643
+ FEEDBACK: judgeResult.feedback
1644
+ });
1492
1645
  continue;
1493
1646
  }
1494
1647
  if (!judgeResult.pass) {
1495
- yield { type: "plan:warn", message: `Judge rejected plan but retries exhausted: ${judgeResult.feedback}` };
1648
+ yield {
1649
+ type: "plan:warn",
1650
+ message: `Judge rejected plan but retries exhausted: ${judgeResult.feedback}`
1651
+ };
1496
1652
  }
1497
1653
  const { goal, vars, steps, ...rest } = normalizeWorkflow(zodResult.data);
1498
1654
  const ordered = { goal, ...vars && { vars }, steps, ...rest };
@@ -1503,11 +1659,15 @@ ${issues}` };
1503
1659
  forceQuotes: false
1504
1660
  }).trimEnd();
1505
1661
  writeFileSync2(taskFile, yamlContent + "\n", "utf8");
1506
- const preview = yamlContent.split("\n").slice(0, 30).join("\n") + (yamlContent.split("\n").length > 30 ? "\n..." : "");
1662
+ const yamlLines = yamlContent.split("\n");
1663
+ const preview = yamlLines.slice(0, 30).join("\n") + (yamlLines.length > 30 ? "\n..." : "");
1507
1664
  yield { type: "plan:complete", taskFile, preview };
1508
1665
  return;
1509
1666
  }
1510
- yield { type: "plan:error", message: "Plan generation failed after maximum retries" };
1667
+ yield {
1668
+ type: "plan:error",
1669
+ message: "Plan generation failed after maximum retries"
1670
+ };
1511
1671
  }
1512
1672
 
1513
1673
  // src/ui/PlanApp.tsx
@@ -1687,17 +1847,6 @@ import {
1687
1847
  writeFileSync as writeFileSync3
1688
1848
  } from "node:fs";
1689
1849
  import { dirname as dirname3, join as join3, resolve as resolve2 } from "node:path";
1690
- var TOOL_SUMMARY = {
1691
- Read: (i) => String(i["file_path"] ?? i["path"] ?? ""),
1692
- Edit: (i) => String(i["file_path"] ?? ""),
1693
- Write: (i) => String(i["file_path"] ?? ""),
1694
- Bash: (i) => String(i["command"] ?? ""),
1695
- Glob: (i) => String(i["pattern"] ?? ""),
1696
- Grep: (i) => String(i["pattern"] ?? "")
1697
- };
1698
- function toolSummary(tool, input) {
1699
- return (TOOL_SUMMARY[tool] ?? ((i) => JSON.stringify(i)))(input);
1700
- }
1701
1850
  function findExecutantLocalDir(startDir) {
1702
1851
  let dir = resolve2(startDir);
1703
1852
  while (true) {
@@ -1711,337 +1860,336 @@ function findExecutantLocalDir(startDir) {
1711
1860
  function resolveLogDir(workflowFilePath) {
1712
1861
  const startDir = dirname3(resolve2(workflowFilePath));
1713
1862
  const executantLocal = findExecutantLocalDir(startDir);
1714
- if (executantLocal) return join3(executantLocal, "logs");
1715
- return join3(startDir, "logs");
1716
- }
1717
- var Logger = class {
1718
- enabled;
1719
- logDir;
1720
- highlightsDir;
1721
- timestamp;
1722
- taskName;
1723
- logFile = "";
1724
- // Per-step state
1725
- stepIndex = -1;
1726
- stepName = "";
1727
- stepStartMs = 0;
1728
- toolCount = 0;
1729
- complexSequenceFile = "";
1730
- selfHealingFile = "";
1731
- judgeAttempt = 0;
1732
- recentOutput = [];
1733
- constructor(logDir, taskName) {
1734
- this.enabled = process.env["EXECUTANT_LOG"] !== "0";
1735
- this.logDir = logDir;
1736
- this.highlightsDir = join3(logDir, "highlights");
1737
- this.timestamp = formatTimestamp(/* @__PURE__ */ new Date());
1738
- this.taskName = slugify(taskName, 40) || "task";
1739
- }
1740
- getHighlightsDir() {
1741
- return this.highlightsDir;
1742
- }
1743
- getTimestamp() {
1744
- return this.timestamp;
1745
- }
1746
- /** Feed each event from the runner into the logger. */
1747
- observe(event) {
1748
- if (!this.enabled) return;
1749
- try {
1750
- this.dispatch(event);
1751
- } catch (err) {
1752
- console.warn(`[logger] error: ${err instanceof Error ? err.message : String(err)}`);
1753
- }
1754
- }
1755
- // --------------------------------------------------------------------------
1756
- // Event dispatch
1757
- // --------------------------------------------------------------------------
1758
- dispatch(event) {
1759
- switch (event.type) {
1760
- case "workflow:start":
1761
- this.initDirs();
1762
- break;
1763
- case "step:start":
1764
- this.onStepStart(event.index, event.name);
1765
- break;
1766
- case "step:complete":
1767
- this.onStepComplete();
1768
- break;
1769
- case "step:error":
1770
- this.onStepError(event.error);
1771
- break;
1772
- case "output:text":
1773
- this.appendLog(event.text);
1774
- this.recentOutput.push(event.text);
1775
- break;
1776
- case "output:tool":
1777
- this.onTool(event.tool, event.input);
1778
- break;
1779
- case "log":
1780
- this.onLogMessage(event.level, event.text);
1781
- break;
1782
- case "workflow:complete":
1783
- this.onWorkflowComplete();
1784
- break;
1785
- }
1786
- }
1787
- // --------------------------------------------------------------------------
1788
- // Initialisation
1789
- // --------------------------------------------------------------------------
1790
- initDirs() {
1791
- mkdirSync3(this.logDir, { recursive: true });
1792
- mkdirSync3(this.highlightsDir, { recursive: true });
1793
- this.logFile = join3(this.logDir, `${this.timestamp}_${this.taskName}.log`);
1794
- writeFileSync3(
1795
- this.logFile,
1796
- `# Execution Log
1797
- Task: ${this.taskName}
1863
+ return executantLocal ? join3(executantLocal, "logs") : join3(startDir, "logs");
1864
+ }
1865
+ var INIT_STATE = {
1866
+ logFile: "",
1867
+ stepIndex: -1,
1868
+ stepName: "",
1869
+ stepStartMs: 0,
1870
+ toolCount: 0,
1871
+ complexSequenceFile: "",
1872
+ selfHealingFile: "",
1873
+ judgeAttempt: 0,
1874
+ recentOutput: []
1875
+ };
1876
+ var TOOL_SUMMARY = {
1877
+ Read: (i) => String(i["file_path"] ?? i["path"] ?? ""),
1878
+ Edit: (i) => String(i["file_path"] ?? ""),
1879
+ Write: (i) => String(i["file_path"] ?? ""),
1880
+ Bash: (i) => String(i["command"] ?? ""),
1881
+ Glob: (i) => String(i["pattern"] ?? ""),
1882
+ Grep: (i) => String(i["pattern"] ?? "")
1883
+ };
1884
+ function toolSummary(tool, input) {
1885
+ return (TOOL_SUMMARY[tool] ?? ((i) => JSON.stringify(i)))(input);
1886
+ }
1887
+ function appendLog(logFile, text) {
1888
+ if (logFile) appendFileSync(logFile, text + "\n");
1889
+ }
1890
+ function highlightPath(ctx, stepIndex, suffix) {
1891
+ return join3(ctx.highlightsDir, `${ctx.ts}_step${stepIndex + 1}_${suffix}.md`);
1892
+ }
1893
+ function onWorkflowStart(ctx, s) {
1894
+ mkdirSync3(ctx.logDir, { recursive: true });
1895
+ mkdirSync3(ctx.highlightsDir, { recursive: true });
1896
+ const logFile = join3(ctx.logDir, `${ctx.ts}_${ctx.slug}.log`);
1897
+ writeFileSync3(
1898
+ logFile,
1899
+ `# Execution Log
1900
+ Task: ${ctx.slug}
1798
1901
  Started: ${(/* @__PURE__ */ new Date()).toISOString()}
1799
1902
  ${"\u2501".repeat(51)}
1800
1903
 
1801
1904
  `
1802
- );
1803
- }
1804
- // --------------------------------------------------------------------------
1805
- // Step lifecycle
1806
- // --------------------------------------------------------------------------
1807
- onStepStart(index, name) {
1808
- Object.assign(this, {
1809
- stepIndex: index,
1810
- stepName: name,
1811
- stepStartMs: Date.now(),
1812
- toolCount: 0,
1813
- complexSequenceFile: "",
1814
- selfHealingFile: "",
1815
- judgeAttempt: 0,
1816
- recentOutput: []
1817
- });
1818
- this.appendLog(
1819
- `
1905
+ );
1906
+ return { ...s, logFile };
1907
+ }
1908
+ function onStepStart(ctx, s, index, name) {
1909
+ const next = {
1910
+ ...INIT_STATE,
1911
+ logFile: s.logFile,
1912
+ stepIndex: index,
1913
+ stepName: name,
1914
+ stepStartMs: Date.now()
1915
+ };
1916
+ appendLog(
1917
+ next.logFile,
1918
+ `
1820
1919
  ${"\u2501".repeat(51)}
1821
1920
  Step ${index + 1}: ${name}
1822
1921
  Started: ${(/* @__PURE__ */ new Date()).toISOString()}
1823
1922
  ${"\u2501".repeat(51)}
1923
+ `
1924
+ );
1925
+ return next;
1926
+ }
1927
+ function finalizeComplexSequence(s) {
1928
+ if (s.toolCount >= 3 && s.complexSequenceFile) {
1929
+ appendFileSync(
1930
+ s.complexSequenceFile,
1931
+ `
1932
+ ---
1933
+
1934
+ *Total tools used: ${s.toolCount}*
1935
+
1936
+ *Captured by Executant Logger*
1824
1937
  `
1825
1938
  );
1826
1939
  }
1827
- onStepComplete() {
1828
- const durS = ((Date.now() - this.stepStartMs) / 1e3).toFixed(1);
1829
- this.appendLog(`
1830
- Step completed in ${durS}s
1831
- `);
1832
- this.finalizeComplexSequence();
1833
- }
1834
- onStepError(error) {
1835
- this.appendLog(`
1940
+ }
1941
+ function onStepComplete(s) {
1942
+ appendLog(
1943
+ s.logFile,
1944
+ `
1945
+ Step completed in ${((Date.now() - s.stepStartMs) / 1e3).toFixed(1)}s
1946
+ `
1947
+ );
1948
+ finalizeComplexSequence(s);
1949
+ return s;
1950
+ }
1951
+ function onStepError(s, error) {
1952
+ appendLog(s.logFile, `
1836
1953
  Step failed: ${error.message}
1837
1954
  `);
1838
- this.finalizeComplexSequence();
1839
- }
1840
- // --------------------------------------------------------------------------
1841
- // Tool calls → complex sequence highlights
1842
- // --------------------------------------------------------------------------
1843
- onTool(tool, input) {
1844
- const desc = toolSummary(tool, input);
1845
- this.appendLog(` [${tool}] ${desc}`);
1846
- this.toolCount++;
1847
- if (this.toolCount === 3) {
1848
- this.complexSequenceFile = join3(
1849
- this.highlightsDir,
1850
- `${this.timestamp}_step${this.stepIndex + 1}_complex_sequence.md`
1851
- );
1955
+ finalizeComplexSequence(s);
1956
+ return s;
1957
+ }
1958
+ function complexSequenceHeader(ctx, s) {
1959
+ return [
1960
+ "# Complex Tool Sequence",
1961
+ "",
1962
+ `**Task:** ${ctx.slug}`,
1963
+ `**Step:** ${s.stepName}`,
1964
+ `**Timestamp:** ${(/* @__PURE__ */ new Date()).toISOString()}`,
1965
+ "",
1966
+ "---",
1967
+ "",
1968
+ "## Claude's Tool Orchestration",
1969
+ "",
1970
+ "Claude used multiple tools to complete this step:",
1971
+ ""
1972
+ ].join("\n");
1973
+ }
1974
+ function createComplexSequenceFile(ctx, s) {
1975
+ const path = highlightPath(ctx, s.stepIndex, "complex_sequence");
1976
+ writeFileSync3(path, complexSequenceHeader(ctx, s));
1977
+ return path;
1978
+ }
1979
+ function onTool(ctx, s, tool, input) {
1980
+ const desc = toolSummary(tool, input);
1981
+ appendLog(s.logFile, ` [${tool}] ${desc}`);
1982
+ const toolCount = s.toolCount + 1;
1983
+ const complexSequenceFile = toolCount === 3 ? createComplexSequenceFile(ctx, s) : s.complexSequenceFile;
1984
+ if (toolCount >= 3 && complexSequenceFile) {
1985
+ appendFileSync(
1986
+ complexSequenceFile,
1987
+ `${toolCount}. **${tool}** - ${desc}
1988
+ `
1989
+ );
1990
+ }
1991
+ return { ...s, toolCount, complexSequenceFile };
1992
+ }
1993
+ function saveJudgeHighlight(ctx, s, verdict, text) {
1994
+ writeFileSync3(
1995
+ highlightPath(ctx, s.stepIndex, `judge_${verdict}`),
1996
+ [
1997
+ `# Judge Verdict: ${verdict}`,
1998
+ "",
1999
+ `**Task:** ${ctx.slug}`,
2000
+ `**Step:** ${s.stepName}`,
2001
+ `**Attempt:** ${s.judgeAttempt}`,
2002
+ `**Timestamp:** ${(/* @__PURE__ */ new Date()).toISOString()}`,
2003
+ "",
2004
+ "---",
2005
+ "",
2006
+ text,
2007
+ "",
2008
+ "---",
2009
+ "",
2010
+ "*Auto-captured*",
2011
+ ""
2012
+ ].join("\n")
2013
+ );
2014
+ }
2015
+ var LOG_MATCHERS = [
2016
+ {
2017
+ pattern: /\[judge\]\s+(PASS|FAIL)/i,
2018
+ apply: (ctx, s, text, match) => {
2019
+ const verdict = match[1].toUpperCase();
2020
+ const judgeAttempt = s.judgeAttempt + 1;
2021
+ saveJudgeHighlight(ctx, { ...s, judgeAttempt }, verdict, text);
2022
+ return { ...s, judgeAttempt };
2023
+ }
2024
+ },
2025
+ {
2026
+ pattern: /\[self-healing\].*failed.*exit\s+(\d+)/i,
2027
+ apply: (ctx, s, text, match) => {
2028
+ const selfHealingFile = highlightPath(ctx, s.stepIndex, "self_healing");
1852
2029
  writeFileSync3(
1853
- this.complexSequenceFile,
2030
+ selfHealingFile,
1854
2031
  [
1855
- "# Complex Tool Sequence",
2032
+ "# Self-Healing Activation",
1856
2033
  "",
1857
- `**Task:** ${this.taskName}`,
1858
- `**Step:** ${this.stepName}`,
2034
+ `**Task:** ${ctx.slug}`,
2035
+ `**Step:** ${s.stepName}`,
1859
2036
  `**Timestamp:** ${(/* @__PURE__ */ new Date()).toISOString()}`,
1860
2037
  "",
1861
2038
  "---",
1862
2039
  "",
1863
- "## Claude's Tool Orchestration",
2040
+ "## \u274C Failure Detected",
2041
+ "",
2042
+ `**Exit Code:** ${match[1]}`,
2043
+ "",
2044
+ "**Recent Output:**",
2045
+ "```",
2046
+ s.recentOutput.join("\n"),
2047
+ "```",
1864
2048
  "",
1865
- "Claude used multiple tools to complete this step:",
2049
+ "---",
2050
+ "",
2051
+ "## \u{1F527} Claude's Healing Process",
1866
2052
  ""
1867
2053
  ].join("\n")
1868
2054
  );
2055
+ return { ...s, selfHealingFile, recentOutput: [] };
1869
2056
  }
1870
- if (this.toolCount >= 3 && this.complexSequenceFile) {
1871
- appendFileSync(this.complexSequenceFile, `${this.toolCount}. **${tool}** - ${desc}
1872
- `);
1873
- }
1874
- }
1875
- finalizeComplexSequence() {
1876
- if (this.toolCount >= 3 && this.complexSequenceFile) {
2057
+ },
2058
+ {
2059
+ pattern: /\[self-healing\].*Re-running/i,
2060
+ apply: (_ctx, s) => {
2061
+ if (!s.selfHealingFile) return s;
1877
2062
  appendFileSync(
1878
- this.complexSequenceFile,
1879
- `
1880
- ---
1881
-
1882
- *Total tools used: ${this.toolCount}*
1883
-
1884
- *Captured by Executant Logger*
1885
- `
2063
+ s.selfHealingFile,
2064
+ [
2065
+ "",
2066
+ "*(See full log for Claude's diagnostic process)*",
2067
+ "",
2068
+ "---",
2069
+ "",
2070
+ "## \u2705 Resolution Applied",
2071
+ "",
2072
+ "The self-healing process completed. Check the full execution log to see Claude's analysis and fix.",
2073
+ "",
2074
+ "---",
2075
+ "",
2076
+ "*Auto-captured*",
2077
+ ""
2078
+ ].join("\n")
1886
2079
  );
2080
+ return { ...s, selfHealingFile: "" };
1887
2081
  }
1888
2082
  }
1889
- // --------------------------------------------------------------------------
1890
- // Log events judge / self-healing highlights
1891
- // --------------------------------------------------------------------------
1892
- onLogMessage(level, text) {
1893
- this.appendLog(`[${level}] ${text}`);
1894
- if (/\[judge\]\s+PASS/i.test(text)) {
1895
- this.judgeAttempt++;
1896
- this.saveJudgeHighlight("PASS", text);
1897
- return;
1898
- }
1899
- if (/\[judge\]\s+FAIL/i.test(text)) {
1900
- this.judgeAttempt++;
1901
- this.saveJudgeHighlight("FAIL", text);
1902
- return;
1903
- }
1904
- const healingMatch = text.match(/\[self-healing\].*failed.*exit\s+(\d+)/i);
1905
- if (healingMatch) {
1906
- this.startSelfHealingHighlight(healingMatch[1]);
1907
- return;
1908
- }
1909
- if (/\[self-healing\].*Re-running/i.test(text)) {
1910
- this.completeSelfHealingHighlight();
1911
- }
1912
- }
1913
- // --------------------------------------------------------------------------
1914
- // Highlight writers
1915
- // --------------------------------------------------------------------------
1916
- saveJudgeHighlight(verdict, output) {
1917
- const file = join3(
1918
- this.highlightsDir,
1919
- `${this.timestamp}_step${this.stepIndex + 1}_judge_${verdict}.md`
1920
- );
1921
- writeFileSync3(
1922
- file,
1923
- [
1924
- `# Judge Verdict: ${verdict}`,
1925
- "",
1926
- `**Task:** ${this.taskName}`,
1927
- `**Step:** ${this.stepName}`,
1928
- `**Attempt:** ${this.judgeAttempt}`,
1929
- `**Timestamp:** ${(/* @__PURE__ */ new Date()).toISOString()}`,
1930
- "",
1931
- "---",
1932
- "",
1933
- output,
1934
- "",
1935
- "---",
1936
- "",
1937
- "*Auto-captured*",
1938
- ""
1939
- ].join("\n")
1940
- );
1941
- }
1942
- startSelfHealingHighlight(exitCode) {
1943
- this.selfHealingFile = join3(
1944
- this.highlightsDir,
1945
- `${this.timestamp}_step${this.stepIndex + 1}_self_healing.md`
1946
- );
1947
- const errorOutput = this.recentOutput.join("\n");
1948
- this.recentOutput = [];
2083
+ ];
2084
+ function onLogMessage(ctx, s, level, text) {
2085
+ appendLog(s.logFile, `[${level}] ${text}`);
2086
+ return LOG_MATCHERS.reduce(
2087
+ ({ matched, state }, { pattern, apply }) => {
2088
+ if (matched) return { matched, state };
2089
+ const m = pattern.exec(text);
2090
+ return m ? { matched: true, state: apply(ctx, state, text, m) } : { matched, state };
2091
+ },
2092
+ { matched: false, state: s }
2093
+ ).state;
2094
+ }
2095
+ function onWorkflowComplete(ctx, s) {
2096
+ appendLog(
2097
+ s.logFile,
2098
+ `
2099
+ ${"\u2501".repeat(51)}
2100
+ Task Complete: ${ctx.slug}
2101
+ Finished: ${(/* @__PURE__ */ new Date()).toISOString()}
2102
+ ${"\u2501".repeat(51)}
2103
+ `
2104
+ );
2105
+ const indexFile = join3(ctx.highlightsDir, "README.md");
2106
+ if (!existsSync2(indexFile)) {
1949
2107
  writeFileSync3(
1950
- this.selfHealingFile,
2108
+ indexFile,
1951
2109
  [
1952
- "# Self-Healing Activation",
1953
- "",
1954
- `**Task:** ${this.taskName}`,
1955
- `**Step:** ${this.stepName}`,
1956
- `**Timestamp:** ${(/* @__PURE__ */ new Date()).toISOString()}`,
1957
- "",
1958
- "---",
1959
- "",
1960
- "## \u274C Failure Detected",
1961
- "",
1962
- `**Exit Code:** ${exitCode}`,
1963
- "",
1964
- "**Recent Output:**",
1965
- "```",
1966
- errorOutput,
1967
- "```",
2110
+ "# Execution Highlights",
1968
2111
  "",
1969
- "---",
2112
+ "This directory contains automatically extracted highlight moments from task executions.",
1970
2113
  "",
1971
- "## \u{1F527} Claude's Healing Process",
2114
+ "## Latest Highlights",
1972
2115
  ""
1973
2116
  ].join("\n")
1974
2117
  );
1975
2118
  }
1976
- completeSelfHealingHighlight() {
1977
- if (!this.selfHealingFile) return;
2119
+ const highlights = readdirSync(ctx.highlightsDir).filter((f) => f.startsWith(ctx.ts) && f.endsWith(".md")).sort();
2120
+ if (highlights.length > 0) {
2121
+ const entries = highlights.map((f) => `- [${f.replace(/\.md$/, "")}](./${f})`).join("\n");
1978
2122
  appendFileSync(
1979
- this.selfHealingFile,
1980
- [
1981
- "",
1982
- "*(See full log for Claude's diagnostic process)*",
1983
- "",
1984
- "---",
1985
- "",
1986
- "## \u2705 Resolution Applied",
1987
- "",
1988
- "The self-healing process completed. Check the full execution log to see Claude's analysis and fix.",
1989
- "",
1990
- "---",
1991
- "",
1992
- "*Auto-captured*",
1993
- ""
1994
- ].join("\n")
1995
- );
1996
- this.selfHealingFile = "";
1997
- }
1998
- // --------------------------------------------------------------------------
1999
- // Workflow complete → index
2000
- // --------------------------------------------------------------------------
2001
- onWorkflowComplete() {
2002
- this.appendLog(
2123
+ indexFile,
2003
2124
  `
2004
- ${"\u2501".repeat(51)}
2005
- Task Complete: ${this.taskName}
2006
- Finished: ${(/* @__PURE__ */ new Date()).toISOString()}
2007
- ${"\u2501".repeat(51)}
2125
+ ### ${ctx.slug} (${(/* @__PURE__ */ new Date()).toISOString()})
2126
+ ${entries}
2008
2127
  `
2009
2128
  );
2010
- this.writeHighlightsIndex();
2011
2129
  }
2012
- writeHighlightsIndex() {
2013
- const indexFile = join3(this.highlightsDir, "README.md");
2014
- if (!existsSync2(indexFile)) {
2015
- writeFileSync3(
2016
- indexFile,
2017
- [
2018
- "# Execution Highlights",
2019
- "",
2020
- "This directory contains automatically extracted highlight moments from task executions.",
2021
- "",
2022
- "## Latest Highlights",
2023
- ""
2024
- ].join("\n")
2130
+ return s;
2131
+ }
2132
+ function onOutputText(s, text) {
2133
+ appendLog(s.logFile, text);
2134
+ return { ...s, recentOutput: [...s.recentOutput, text] };
2135
+ }
2136
+ function reduce(ctx, s, event) {
2137
+ switch (event.type) {
2138
+ case "workflow:start":
2139
+ return onWorkflowStart(ctx, s);
2140
+ case "step:start":
2141
+ return onStepStart(ctx, s, event.index, event.name);
2142
+ case "step:complete":
2143
+ return onStepComplete(s);
2144
+ case "step:error":
2145
+ return onStepError(s, event.error);
2146
+ case "step:iteration":
2147
+ appendLog(
2148
+ s.logFile,
2149
+ `
2150
+ \u2500\u2500 iteration ${event.iteration}/${event.total}: ${event.item}`
2025
2151
  );
2026
- }
2027
- const files = readdirSync(this.highlightsDir);
2028
- const taskHighlights = files.filter((f) => f.startsWith(this.timestamp) && f.endsWith(".md")).sort();
2029
- if (taskHighlights.length > 0) {
2030
- const entries = taskHighlights.map((f) => `- [${f.replace(/\.md$/, "")}](./${f})`).join("\n");
2031
- appendFileSync(indexFile, `
2032
- ### ${this.taskName} (${(/* @__PURE__ */ new Date()).toISOString()})
2033
- ${entries}
2034
- `);
2035
- }
2036
- }
2037
- // --------------------------------------------------------------------------
2038
- // Log file writes
2039
- // --------------------------------------------------------------------------
2040
- appendLog(text) {
2041
- if (!this.logFile) return;
2042
- appendFileSync(this.logFile, text + "\n");
2152
+ return s;
2153
+ case "step:inner":
2154
+ appendLog(
2155
+ s.logFile,
2156
+ ` \u21B3 [${event.innerIndex + 1}/${event.innerTotal}] ${event.name}`
2157
+ );
2158
+ return s;
2159
+ case "output:text":
2160
+ return onOutputText(s, event.text);
2161
+ case "output:tool":
2162
+ return onTool(ctx, s, event.tool, event.input);
2163
+ case "log":
2164
+ return onLogMessage(ctx, s, event.level, event.text);
2165
+ case "workflow:complete":
2166
+ return onWorkflowComplete(ctx, s);
2167
+ default:
2168
+ return s;
2043
2169
  }
2044
- };
2170
+ }
2171
+ function createLogger(logDir, taskName) {
2172
+ const ctx = {
2173
+ logDir,
2174
+ highlightsDir: join3(logDir, "highlights"),
2175
+ ts: formatTimestamp(/* @__PURE__ */ new Date()),
2176
+ slug: slugify(taskName, 40) || "task"
2177
+ };
2178
+ const enabled = process.env["EXECUTANT_LOG"] !== "0";
2179
+ let state = INIT_STATE;
2180
+ return {
2181
+ getHighlightsDir: () => ctx.highlightsDir,
2182
+ getTimestamp: () => ctx.ts,
2183
+ observe(event) {
2184
+ if (!enabled) return;
2185
+ try {
2186
+ state = reduce(ctx, state, event);
2187
+ } catch (err) {
2188
+ console.warn(`[logger] error: ${getErrorMessage(err)}`);
2189
+ }
2190
+ }
2191
+ };
2192
+ }
2045
2193
  async function* withLogger(gen, logger2) {
2046
2194
  for await (const event of gen) {
2047
2195
  logger2.observe(event);
@@ -2052,7 +2200,6 @@ async function* withLogger(gen, logger2) {
2052
2200
  // src/retrospective.ts
2053
2201
  import { existsSync as existsSync3, mkdirSync as mkdirSync4, readdirSync as readdirSync2, readFileSync as readFileSync5, writeFileSync as writeFileSync4 } from "node:fs";
2054
2202
  import { basename as basename2, dirname as dirname4, join as join4, resolve as resolve3 } from "node:path";
2055
- import { fileURLToPath as fileURLToPath2 } from "node:url";
2056
2203
  import { spawnSync } from "node:child_process";
2057
2204
  import { load as parseYaml2 } from "js-yaml";
2058
2205
  import { z as z4 } from "zod";
@@ -2060,15 +2207,13 @@ var RetrospectiveOutputSchema = z4.object({
2060
2207
  improved_yaml: z4.string(),
2061
2208
  changelog: z4.string()
2062
2209
  });
2063
- var PROMPTS_DIR2 = join4(dirname4(fileURLToPath2(import.meta.url)), "prompts");
2064
- var RETROSPECTIVE_PROMPT = readFileSync5(join4(PROMPTS_DIR2, "retrospective-analysis.txt"), "utf8");
2210
+ var RETROSPECTIVE_PROMPT = loadPrompt("retrospective-analysis");
2065
2211
  async function runRetrospective(workflowFilePath, workflow2, highlightsDir, runTimestamp) {
2066
2212
  try {
2067
2213
  await doRetrospective(workflowFilePath, workflow2, highlightsDir, runTimestamp);
2068
2214
  } catch (err) {
2069
- const msg = err instanceof Error ? err.message : String(err);
2070
2215
  console.warn(`
2071
- Self-improvement: retrospective failed: ${msg}`);
2216
+ Self-improvement: retrospective failed: ${getErrorMessage(err)}`);
2072
2217
  }
2073
2218
  }
2074
2219
  async function doRetrospective(workflowFilePath, workflow2, highlightsDir, runTimestamp) {
@@ -2112,7 +2257,13 @@ ${content}`;
2112
2257
  }).join("\n\n---\n\n");
2113
2258
  const originalYaml = readFileSync5(workflowFilePath, "utf8");
2114
2259
  const taskName = basename2(workflowFilePath, ".yaml");
2115
- const prompt = RETROSPECTIVE_PROMPT.replaceAll("{{TASK_NAME}}", taskName).replaceAll("{{ORIGINAL_GOAL}}", workflow2.goal).replaceAll("{{ORIGINAL_YAML}}", originalYaml).replaceAll("{{HIGHLIGHTS}}", highlightContents).replaceAll("{{METRICS}}", metrics);
2260
+ const prompt = fillTemplate(RETROSPECTIVE_PROMPT, {
2261
+ TASK_NAME: taskName,
2262
+ ORIGINAL_GOAL: workflow2.goal,
2263
+ ORIGINAL_YAML: originalYaml,
2264
+ HIGHLIGHTS: highlightContents,
2265
+ METRICS: metrics
2266
+ });
2116
2267
  const result = spawnSync(
2117
2268
  "claude",
2118
2269
  [
@@ -2155,8 +2306,7 @@ Response: ${response.trim()}`);
2155
2306
  try {
2156
2307
  parseYaml2(improvedYaml);
2157
2308
  } catch (err) {
2158
- const msg = err instanceof Error ? err.message : String(err);
2159
- console.warn(`Self-improvement: generated YAML is invalid (${msg}), skipping save.`);
2309
+ console.warn(`Self-improvement: generated YAML is invalid (${getErrorMessage(err)}), skipping save.`);
2160
2310
  return;
2161
2311
  }
2162
2312
  const startDir = dirname4(resolve3(workflowFilePath));
@@ -2187,16 +2337,21 @@ function extractJson(text) {
2187
2337
 
2188
2338
  // src/index.ts
2189
2339
  var CURRENT_VERSION = JSON.parse(
2190
- readFileSync6(join5(dirname5(fileURLToPath3(import.meta.url)), "../package.json"), "utf-8")
2340
+ readFileSync6(
2341
+ join5(dirname5(fileURLToPath2(import.meta.url)), "../package.json"),
2342
+ "utf-8"
2343
+ )
2191
2344
  ).version;
2192
2345
  var rawArgs = process.argv.slice(2);
2193
2346
  if (rawArgs[0] === "plan") {
2194
2347
  const planArgs = parsePlanArgs(rawArgs.slice(1));
2195
2348
  const planEvents = streamPlan(planArgs);
2196
- const inkApp = render(React3.createElement(PlanApp, {
2197
- description: planArgs.description,
2198
- events: planEvents
2199
- }));
2349
+ const inkApp = render(
2350
+ React3.createElement(PlanApp, {
2351
+ description: planArgs.description,
2352
+ events: planEvents
2353
+ })
2354
+ );
2200
2355
  try {
2201
2356
  await inkApp.waitUntilExit();
2202
2357
  } catch {
@@ -2212,7 +2367,7 @@ if (rawArgs[0] === "update") {
2212
2367
  await doUpdate2();
2213
2368
  console.log("Done.");
2214
2369
  } catch (err) {
2215
- console.error("Update failed:", err instanceof Error ? err.message : String(err));
2370
+ console.error("Update failed:", getErrorMessage(err));
2216
2371
  process.exit(1);
2217
2372
  }
2218
2373
  process.exit(0);
@@ -2245,7 +2400,15 @@ YAML \u2014 step fields (all step types):
2245
2400
  forEach string or list
2246
2401
  Inline YAML array OR a shell command whose newline-split
2247
2402
  stdout provides the items. {{item}} is substituted per
2248
- iteration in the inner step's prompt or command.
2403
+ iteration in every child step's name, command, and prompt.
2404
+ repeat int Run this step N times; {{item}} is the 1-based
2405
+ iteration number. Mutually exclusive with forEach.
2406
+ steps list Multiple child steps to run per forEach/repeat
2407
+ iteration. Mutually exclusive with command/prompt on the
2408
+ parent step. Requires forEach or repeat.
2409
+ context list Var names whose file-path values are prepended to
2410
+ a prompt step's content at runtime.
2411
+ output string Var name; captures script stdout to that file path.
2249
2412
 
2250
2413
  YAML \u2014 prompt step fields (type: prompt, or inferred when prompt is present):
2251
2414
  prompt string (required) Instructions sent to Claude
@@ -2257,7 +2420,7 @@ YAML \u2014 prompt step fields (type: prompt, or inferred when prompt is present
2257
2420
  YAML \u2014 script step fields (type: script | command, or inferred when command is present):
2258
2421
  command string (required) Bash command to execute
2259
2422
  self_healing bool On failure, Claude diagnoses and fixes iteratively
2260
- up to 5 attempts with accumulated context (default: true)
2423
+ up to 5 attempts with accumulated context (default: false)
2261
2424
  max_healing_attempts int Override max self-healing retries (default: 5)
2262
2425
 
2263
2426
  YAML \u2014 log step fields (type: log, or inferred when message is present and prompt is absent):
@@ -2318,12 +2481,12 @@ var workflow;
2318
2481
  try {
2319
2482
  workflow = loadWorkflow(filePath);
2320
2483
  } catch (err) {
2321
- console.error(err instanceof Error ? err.message : String(err));
2484
+ console.error(getErrorMessage(err));
2322
2485
  process.exit(1);
2323
2486
  }
2324
2487
  var options = { stepFilter, fromStep };
2325
2488
  var rawEvents = runWorkflow(workflow, options);
2326
- var logger = new Logger(resolveLogDir(filePath), workflow.goal);
2489
+ var logger = createLogger(resolveLogDir(filePath), workflow.goal);
2327
2490
  var events = withLogger(rawEvents, logger);
2328
2491
  var updateCheck = checkForUpdate(CURRENT_VERSION);
2329
2492
  function errorReplacer(_key, value) {
@@ -2335,9 +2498,17 @@ function errorReplacer(_key, value) {
2335
2498
  async function maybeRunRetrospective(filePath2, workflow2, logger2) {
2336
2499
  if (!logger2) return;
2337
2500
  try {
2338
- await runRetrospective(filePath2, workflow2, logger2.getHighlightsDir(), logger2.getTimestamp());
2501
+ await runRetrospective(
2502
+ filePath2,
2503
+ workflow2,
2504
+ logger2.getHighlightsDir(),
2505
+ logger2.getTimestamp()
2506
+ );
2339
2507
  } catch (err) {
2340
- console.warn("[executant] retrospective failed (non-fatal):", err instanceof Error ? err.message : err);
2508
+ console.warn(
2509
+ "[executant] retrospective failed (non-fatal):",
2510
+ getErrorMessage(err)
2511
+ );
2341
2512
  }
2342
2513
  }
2343
2514
  if (ciMode) {
@@ -2353,7 +2524,9 @@ if (ciMode) {
2353
2524
  process.exit(1);
2354
2525
  });
2355
2526
  } else {
2356
- const inkApp = render(React3.createElement(App, { workflow, events, options, updateCheck }));
2527
+ const inkApp = render(
2528
+ React3.createElement(App, { workflow, events, options, updateCheck })
2529
+ );
2357
2530
  if (workflow.selfImprove) {
2358
2531
  inkApp.waitUntilExit().then(() => maybeRunRetrospective(filePath, workflow, logger)).catch(() => {
2359
2532
  });