executant 1.7.0 → 1.9.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
@@ -113,22 +113,25 @@ function timestamp() {
113
113
 
114
114
  // src/load-workflow.ts
115
115
  import { z } from "zod";
116
- var RawStepSchema = z.object({
117
- name: z.string(),
118
- type: z.enum(["prompt", "script", "log", "command"]).optional(),
119
- prompt: z.string().optional(),
120
- command: z.string().optional(),
121
- message: z.string().optional(),
122
- continue_on_error: z.boolean().optional(),
123
- self_healing: z.boolean().optional(),
124
- max_healing_attempts: z.number().int().positive().optional(),
125
- output: z.string().optional(),
126
- llm_as_judge: z.boolean().optional(),
127
- allowed_tools: z.array(z.string()).optional(),
128
- forEach: z.union([z.array(z.string()), z.string()]).optional(),
129
- repeat: z.number().int().positive().optional(),
130
- context: z.array(z.string()).optional()
131
- });
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
+ );
132
135
  var RawWorkflowSchema = z.object({
133
136
  goal: z.string(),
134
137
  steps: z.array(RawStepSchema),
@@ -140,7 +143,9 @@ function loadWorkflow(filePath2) {
140
143
  try {
141
144
  raw = readFileSync2(filePath2, "utf8");
142
145
  } catch (err) {
143
- throw new Error(`Cannot read workflow file "${filePath2}": ${getErrorMessage(err)}`);
146
+ throw new Error(
147
+ `Cannot read workflow file "${filePath2}": ${getErrorMessage(err)}`
148
+ );
144
149
  }
145
150
  let doc;
146
151
  try {
@@ -164,16 +169,30 @@ function convertStep(step, vars) {
164
169
  throw new Error(`Step "${name}" cannot have both repeat and forEach`);
165
170
  }
166
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
+ }
167
177
  const forEachValue = step.repeat !== void 0 ? Array.from({ length: step.repeat }, (_, i) => String(i + 1)) : step.forEach;
168
- const { repeat: _r, forEach: _f, ...innerStep } = step;
178
+ const stepWithoutLoop = {
179
+ ...step,
180
+ repeat: void 0,
181
+ forEach: void 0,
182
+ steps: void 0
183
+ };
184
+ const inner = step.steps ? step.steps.map((s) => convertStep(s, vars)) : [convertInnerStep(stepWithoutLoop, vars, name, continueOnError)];
169
185
  return {
170
186
  type: "forEach",
171
187
  name,
172
188
  continueOnError,
173
189
  forEach: forEachValue,
174
- inner: convertInnerStep(innerStep, vars, name, continueOnError)
190
+ inner
175
191
  };
176
192
  }
193
+ if (step.steps) {
194
+ throw new Error(`Step "${name}" has steps but no forEach or repeat`);
195
+ }
177
196
  return convertInnerStep(step, vars, name, continueOnError);
178
197
  }
179
198
  function convertInnerStep(step, vars, name, continueOnError) {
@@ -181,7 +200,8 @@ function convertInnerStep(step, vars, name, continueOnError) {
181
200
  switch (effectiveType) {
182
201
  case "script":
183
202
  case "command": {
184
- 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`);
185
205
  return {
186
206
  type: "command",
187
207
  name,
@@ -189,7 +209,9 @@ function convertInnerStep(step, vars, name, continueOnError) {
189
209
  continueOnError,
190
210
  selfHealing: step.self_healing === true,
191
211
  maxHealingAttempts: step.max_healing_attempts,
192
- ...step.output && { output: resolveOutputFile(step.output, vars, name) }
212
+ ...step.output && {
213
+ output: resolveOutputFile(step.output, vars, name)
214
+ }
193
215
  };
194
216
  }
195
217
  case "log": {
@@ -202,7 +224,8 @@ function convertInnerStep(step, vars, name, continueOnError) {
202
224
  };
203
225
  }
204
226
  case "prompt": {
205
- 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`);
206
229
  const contextFiles = resolveContextFiles(step.context, vars, name);
207
230
  return {
208
231
  type: "claude",
@@ -225,13 +248,17 @@ function inferType(step) {
225
248
  }
226
249
  function resolveVarPath(varName, vars, stepName, label) {
227
250
  if (!(varName in vars)) {
228
- 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
+ );
229
254
  }
230
255
  return vars[varName];
231
256
  }
232
257
  function resolveContextFiles(contextVarNames, vars, stepName) {
233
258
  if (!contextVarNames || contextVarNames.length === 0) return [];
234
- return contextVarNames.map((varName) => resolveVarPath(varName, vars, stepName, "context"));
259
+ return contextVarNames.map(
260
+ (varName) => resolveVarPath(varName, vars, stepName, "context")
261
+ );
235
262
  }
236
263
  function resolveOutputFile(varName, vars, stepName) {
237
264
  return resolveVarPath(varName, vars, stepName, "output");
@@ -507,7 +534,7 @@ async function* runWorkflow(workflow2, options2 = {}) {
507
534
  yield { type: "step:start", index: i, name: task.name };
508
535
  try {
509
536
  for await (const event of runStep(task)) {
510
- 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") {
511
538
  yield { ...event, index: i };
512
539
  } else {
513
540
  yield event;
@@ -568,20 +595,55 @@ async function* runLog(task) {
568
595
  async function* runForEach(task) {
569
596
  const items = await resolveItems(task.forEach);
570
597
  const total = items.length;
598
+ const innerTotal = task.inner.length;
571
599
  for (const [i, item] of items.entries()) {
572
600
  yield { type: "step:iteration", index: -1, item, iteration: i + 1, total };
573
- const substituted = substituteItem(task.inner, item);
574
- 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
+ }
575
632
  }
576
633
  }
577
634
  async function resolveItems(forEach) {
578
635
  if (Array.isArray(forEach)) return forEach.filter(Boolean);
579
636
  try {
580
- const { stdout } = await execPromise(forEach, { shell: "/bin/sh", timeout: 3e4 });
637
+ const { stdout } = await execPromise(forEach, {
638
+ shell: "/bin/sh",
639
+ timeout: 3e4
640
+ });
581
641
  return stdout.split("\n").filter((l) => l.trim().length > 0);
582
642
  } catch (err) {
583
- throw new Error(`forEach shell command failed: ${getErrorMessage(err)}
584
- Command: ${forEach}`);
643
+ throw new Error(
644
+ `forEach shell command failed: ${getErrorMessage(err)}
645
+ Command: ${forEach}`
646
+ );
585
647
  }
586
648
  }
587
649
  function substituteItem(task, item) {
@@ -590,12 +652,24 @@ function substituteItem(task, item) {
590
652
  case "command":
591
653
  return { ...task, name: sub(task.name), command: sub(task.command) };
592
654
  case "claude":
593
- 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
+ };
594
661
  case "log":
595
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
+ };
596
670
  default: {
597
671
  const _ = task;
598
- throw new Error(`Unknown inner task type: ${JSON.stringify(_)}`);
672
+ throw new Error(`Unknown task type: ${JSON.stringify(_)}`);
599
673
  }
600
674
  }
601
675
  }
@@ -607,7 +681,11 @@ async function* runCommandWithHealing(task) {
607
681
  try {
608
682
  yield* collectLines(runCommand(task), lines);
609
683
  if (attempt > 0) {
610
- 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
+ };
611
689
  }
612
690
  return;
613
691
  } catch (err) {
@@ -615,7 +693,11 @@ async function* runCommandWithHealing(task) {
615
693
  const output = lines.join("\n");
616
694
  const remaining = maxAttempts - attempt - 1;
617
695
  if (remaining === 0) {
618
- 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
+ };
619
701
  throw new Error(
620
702
  `Step "${task.name}" failed after ${maxAttempts} self-healing attempts (last exit code: ${exitCode})`
621
703
  );
@@ -626,7 +708,12 @@ async function* runCommandWithHealing(task) {
626
708
  text: `[self-healing] Attempt ${attempt + 1}/${maxAttempts} failed (exit ${exitCode}), invoking Claude to fix\u2026`
627
709
  };
628
710
  const historyBlock = buildAttemptHistory(attemptHistory);
629
- const healPrompt = buildHealingPrompt(task.command, exitCode, output, historyBlock);
711
+ const healPrompt = buildHealingPrompt(
712
+ task.command,
713
+ exitCode,
714
+ output,
715
+ historyBlock
716
+ );
630
717
  const healTask = {
631
718
  type: "claude",
632
719
  name: `${task.name}:heal-${attempt + 1}`,
@@ -637,7 +724,8 @@ async function* runCommandWithHealing(task) {
637
724
  const claudeLines = [];
638
725
  for await (const event of runClaude(healTask)) {
639
726
  if (event.type === "output:text") claudeLines.push(event.text);
640
- 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));
641
729
  yield event;
642
730
  }
643
731
  attemptHistory.push({
@@ -645,7 +733,11 @@ async function* runCommandWithHealing(task) {
645
733
  exitCode,
646
734
  cmdOutput: output
647
735
  });
648
- 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
+ };
649
741
  }
650
742
  }
651
743
  }
@@ -657,21 +749,37 @@ async function* runClaudeWithJudge(task) {
657
749
  ${fillTemplate(JUDGE_RETRY_CONTEXT, { FEEDBACK: judgeContext })}`;
658
750
  const lines = [];
659
751
  yield* collectLines(runClaude({ ...task, prompt }), lines);
660
- yield { type: "log", level: "info", text: `[judge] Evaluating "${task.name}"\u2026` };
661
- 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
+ );
662
762
  if (verdict.pass) {
663
763
  yield { type: "log", level: "info", text: "[judge] PASS" };
664
764
  return;
665
765
  }
666
766
  judgeContext = verdict.feedback;
667
- 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
+ };
668
772
  const remaining = MAX_JUDGE_RETRIES - attempt - 1;
669
773
  if (remaining === 0) {
670
774
  throw new Error(
671
775
  `Step "${task.name}" failed judge evaluation after ${MAX_JUDGE_RETRIES} attempts`
672
776
  );
673
777
  }
674
- 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
+ };
675
783
  }
676
784
  }
677
785
  async function evaluateWithJudge(stepName, stepInstructions, output) {
@@ -698,7 +806,9 @@ function readContextFile(filePath2) {
698
806
  try {
699
807
  return readFileSync3(filePath2, "utf8");
700
808
  } catch (err) {
701
- throw new Error(`Context file "${filePath2}" could not be read: ${getErrorMessage(err)}`);
809
+ throw new Error(
810
+ `Context file "${filePath2}" could not be read: ${getErrorMessage(err)}`
811
+ );
702
812
  }
703
813
  }
704
814
  function expandContext(task) {
@@ -712,13 +822,23 @@ ${readContextFile(fp)}
712
822
  ${task.prompt}` };
713
823
  }
714
824
  function buildHealingPrompt(command, exitCode, output, attemptHistory) {
715
- return fillTemplate(SELF_HEALING_PROMPT, { COMMAND: command, EXIT_CODE: String(exitCode), OUTPUT: output, 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
+ });
716
831
  }
717
832
  function buildJudgePrompt(stepName, instructions, output) {
718
- return fillTemplate(JUDGE_EVALUATION_PROMPT, { STEP_NAME: stepName, STEP_INSTRUCTIONS: instructions, OUTPUT: output });
833
+ return fillTemplate(JUDGE_EVALUATION_PROMPT, {
834
+ STEP_NAME: stepName,
835
+ STEP_INSTRUCTIONS: instructions,
836
+ OUTPUT: output
837
+ });
719
838
  }
720
839
  function formatToolCall(tool, input) {
721
- if (tool === "Edit" || tool === "Write") return `${tool}(${String(input["file_path"] ?? "")})`;
840
+ if (tool === "Edit" || tool === "Write")
841
+ return `${tool}(${String(input["file_path"] ?? "")})`;
722
842
  if (tool === "Bash") return `Bash(${String(input["command"] ?? "")})`;
723
843
  return tool;
724
844
  }
@@ -743,7 +863,7 @@ init_update();
743
863
 
744
864
  // src/ui/App.tsx
745
865
  import { useEffect as useEffect2, useReducer, useState } from "react";
746
- import { Box as Box4, Text as Text4, useApp, useStdin } from "ink";
866
+ import { Box as Box5, Text as Text5, useApp, useStdin } from "ink";
747
867
 
748
868
  // src/ui/KeyboardHandler.tsx
749
869
  import { useInput } from "ink";
@@ -811,32 +931,70 @@ function reducer(state, event) {
811
931
  status: "running",
812
932
  startTime: Date.now()
813
933
  });
814
- case "step:complete":
934
+ case "step:complete": {
935
+ const prev = state.tasks[event.index]?.iterationHistory;
936
+ const iterationHistory = prev?.length ? prev.map(
937
+ (r) => r.status === "running" ? { ...r, status: "complete", endTime: Date.now() } : r
938
+ ) : void 0;
815
939
  return {
816
940
  ...updateTask(state, event.index, {
817
941
  status: "complete",
818
- endTime: Date.now()
942
+ endTime: Date.now(),
943
+ ...iterationHistory ? { iterationHistory } : {}
819
944
  }),
820
945
  currentIndex: event.index + 1
821
946
  };
822
- case "step:error":
947
+ }
948
+ case "step:error": {
949
+ const prev = state.tasks[event.index]?.iterationHistory;
950
+ const iterationHistory = prev?.length ? prev.map(
951
+ (r) => r.status === "running" ? { ...r, status: "error", endTime: Date.now() } : r
952
+ ) : void 0;
823
953
  return {
824
954
  ...updateTask(state, event.index, {
825
955
  status: "error",
826
956
  endTime: Date.now(),
827
- error: event.error
957
+ error: event.error,
958
+ ...iterationHistory ? { iterationHistory } : {}
828
959
  }),
829
960
  currentIndex: event.index + 1
830
961
  };
962
+ }
831
963
  case "step:skip":
832
964
  return {
833
965
  ...updateTask(state, event.index, { status: "skipped" }),
834
966
  currentIndex: event.index + 1
835
967
  };
836
- case "step:iteration":
968
+ case "step:iteration": {
969
+ const prev = (state.tasks[event.index]?.iterationHistory ?? []).map(
970
+ (r) => r.status === "running" ? { ...r, status: "complete", endTime: Date.now() } : r
971
+ );
837
972
  return updateTask(state, event.index, {
838
- iteration: { current: event.iteration, total: event.total, item: event.item }
973
+ iterationHistory: [
974
+ ...prev,
975
+ {
976
+ item: event.item,
977
+ iteration: event.iteration,
978
+ total: event.total,
979
+ status: "running",
980
+ startTime: Date.now()
981
+ }
982
+ ]
839
983
  });
984
+ }
985
+ case "step:inner": {
986
+ const iterationHistory = (state.tasks[event.index]?.iterationHistory ?? []).map(
987
+ (r) => r.status === "running" ? {
988
+ ...r,
989
+ inner: {
990
+ index: event.innerIndex,
991
+ total: event.innerTotal,
992
+ name: event.name
993
+ }
994
+ } : r
995
+ );
996
+ return updateTask(state, event.index, { iterationHistory });
997
+ }
840
998
  case "output:text": {
841
999
  const idx = event.index;
842
1000
  if (idx >= state.tasks.length) return state;
@@ -848,7 +1006,10 @@ function reducer(state, event) {
848
1006
  const formatted = formatToolCall2(event.tool, event.input);
849
1007
  const next = formatted ? appendLine(state, idx, formatted) : state;
850
1008
  if (event.tool === "Write" && typeof event.input["file_path"] === "string") {
851
- return { ...next, writtenFiles: [...next.writtenFiles, event.input["file_path"]] };
1009
+ return {
1010
+ ...next,
1011
+ writtenFiles: [...next.writtenFiles, event.input["file_path"]]
1012
+ };
852
1013
  }
853
1014
  return next;
854
1015
  }
@@ -871,7 +1032,9 @@ function reducer(state, event) {
871
1032
  }
872
1033
  }
873
1034
  function updateTask(state, index, patch) {
874
- const tasks = state.tasks.map((t, i) => i === index ? { ...t, ...patch } : t);
1035
+ const tasks = state.tasks.map(
1036
+ (t, i) => i === index ? { ...t, ...patch } : t
1037
+ );
875
1038
  return { ...state, tasks };
876
1039
  }
877
1040
  function appendLine(state, index, line) {
@@ -886,20 +1049,6 @@ function appendLine(state, index, line) {
886
1049
  // src/ui/TaskRow.tsx
887
1050
  import { Box, Text } from "ink";
888
1051
 
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
1052
  // src/ui/theme.ts
904
1053
  import { createRequire } from "node:module";
905
1054
  import { oklchToHex } from "@coston/design-tokens";
@@ -928,6 +1077,32 @@ var theme = {
928
1077
  // log pane border
929
1078
  };
930
1079
 
1080
+ // src/ui/utils.ts
1081
+ var SPINNER = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
1082
+ var STATUS_ICON = {
1083
+ complete: "\u2713",
1084
+ error: "\u2717",
1085
+ skipped: "\u2298",
1086
+ pending: "\xB7"
1087
+ };
1088
+ var STATUS_COLOR = {
1089
+ complete: theme.success,
1090
+ error: theme.error,
1091
+ pending: theme.muted
1092
+ };
1093
+ var EXIT_DELAY_MS = 300;
1094
+ function formatHeaderElapsed(start, end) {
1095
+ const ms = (end ?? Date.now()) - start;
1096
+ return ms < 1e3 ? `${ms}ms` : `${(ms / 1e3).toFixed(1)}s`;
1097
+ }
1098
+ function formatTaskElapsed(start, end, status) {
1099
+ if (!start) return "";
1100
+ const ms = (end ?? Date.now()) - start;
1101
+ if (status === "running" || status === "complete" || status === "error")
1102
+ return `${(ms / 1e3).toFixed(1)}s`;
1103
+ return "";
1104
+ }
1105
+
931
1106
  // src/ui/TaskRow.tsx
932
1107
  import { jsx, jsxs } from "react/jsx-runtime";
933
1108
  function TaskRow({ taskState, isActive, index, tick }) {
@@ -935,7 +1110,7 @@ function TaskRow({ taskState, isActive, index, tick }) {
935
1110
  const icon = statusIcon(status, tick);
936
1111
  const color = statusColor(status, isActive);
937
1112
  const elapsed = formatTaskElapsed(startTime, endTime, status);
938
- const iterInfo = taskState.iteration ? ` (${taskState.iteration.current}/${taskState.iteration.total}) ${taskState.iteration.item}` : "";
1113
+ const iterInfo = formatIterCount(taskState.iterationHistory);
939
1114
  const label = `${index + 1}. ${task.name}${iterInfo}`;
940
1115
  return /* @__PURE__ */ jsxs(Box, { children: [
941
1116
  /* @__PURE__ */ jsxs(Text, { color, children: [
@@ -949,17 +1124,6 @@ function TaskRow({ taskState, isActive, index, tick }) {
949
1124
  ] })
950
1125
  ] });
951
1126
  }
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
1127
  function statusIcon(status, tick) {
964
1128
  return status === "running" ? SPINNER[tick % SPINNER.length] : STATUS_ICON[status] ?? "\xB7";
965
1129
  }
@@ -967,16 +1131,74 @@ function statusColor(status, isActive) {
967
1131
  if (isActive && status === "running") return theme.primary;
968
1132
  return STATUS_COLOR[status] ?? theme.foreground;
969
1133
  }
1134
+ function formatIterCount(history) {
1135
+ if (!history?.length) return "";
1136
+ const total = history[0].total;
1137
+ const running = history.find((r) => r.status === "running");
1138
+ const current = running?.iteration ?? history.length;
1139
+ return ` (${current}/${total})`;
1140
+ }
970
1141
 
971
- // src/ui/LogPane.tsx
1142
+ // src/ui/IterationRow.tsx
972
1143
  import { Box as Box2, Text as Text2 } from "ink";
973
- import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
1144
+ import { Fragment, jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
1145
+ function IterationRow({ record, tick }) {
1146
+ const icon = record.status === "running" ? SPINNER[tick % SPINNER.length] : STATUS_ICON[record.status] ?? "\xB7";
1147
+ const color = STATUS_COLOR[record.status] ?? theme.primary;
1148
+ const innerText = record.inner ? ` \u2014 ${stripItem(record.inner.name, record.item)} [${record.inner.index + 1}/${record.inner.total}]` : "";
1149
+ const ms = (record.endTime ?? Date.now()) - record.startTime;
1150
+ const elapsed = `${(ms / 1e3).toFixed(1)}s`;
1151
+ return /* @__PURE__ */ jsxs2(Box2, { children: [
1152
+ /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: " " }),
1153
+ /* @__PURE__ */ jsx2(Text2, { color, children: icon }),
1154
+ /* @__PURE__ */ jsx2(Text2, { children: " " }),
1155
+ /* @__PURE__ */ jsxs2(
1156
+ Text2,
1157
+ {
1158
+ color: record.status === "running" ? theme.foreground : theme.muted,
1159
+ children: [
1160
+ record.item,
1161
+ innerText
1162
+ ]
1163
+ }
1164
+ ),
1165
+ /* @__PURE__ */ jsxs2(Text2, { dimColor: true, children: [
1166
+ " ",
1167
+ elapsed
1168
+ ] })
1169
+ ] });
1170
+ }
1171
+ function stripItem(name, item) {
1172
+ if (!name.includes(item)) return name;
1173
+ const stripped = name.replace(item, "").replace(/\s{2,}/g, " ").replace(/^[\s\-—–]+/, "").replace(/[\s\-—–]+$/, "").trim();
1174
+ return stripped || name;
1175
+ }
1176
+ function isRepeatStyle(history) {
1177
+ return history.every((r) => r.item === String(r.iteration));
1178
+ }
1179
+ function IterationList({
1180
+ iterationHistory,
1181
+ tick,
1182
+ maxVisible
1183
+ }) {
1184
+ if (isRepeatStyle(iterationHistory)) return null;
1185
+ const hidden = iterationHistory.length - maxVisible;
1186
+ const visible = iterationHistory.slice(-maxVisible);
1187
+ return /* @__PURE__ */ jsxs2(Fragment, { children: [
1188
+ hidden > 0 && /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: ` \u2026 ${hidden} earlier` }),
1189
+ visible.map((record) => /* @__PURE__ */ jsx2(IterationRow, { record, tick }, record.iteration))
1190
+ ] });
1191
+ }
1192
+
1193
+ // src/ui/LogPane.tsx
1194
+ import { Box as Box3, Text as Text3 } from "ink";
1195
+ import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
974
1196
  function LogPane({ lines, isActive = false, maxLines = 15 }) {
975
1197
  const visible = lines.slice(-maxLines);
976
1198
  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" }) });
1199
+ return /* @__PURE__ */ jsx3(Box3, { marginTop: 1, children: /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: isActive ? "\u2838 waiting for output\u2026" : "\u2014 no output yet \u2014" }) });
978
1200
  }
979
- return /* @__PURE__ */ jsx2(Box2, { flexDirection: "column", marginTop: 1, borderStyle: "single", borderColor: theme.border, paddingX: 1, children: visible.map((line, i) => /* @__PURE__ */ jsx2(
1201
+ return /* @__PURE__ */ jsx3(Box3, { flexDirection: "column", marginTop: 1, borderStyle: "single", borderColor: theme.border, paddingX: 1, children: visible.map((line, i) => /* @__PURE__ */ jsx3(
980
1202
  LogLine,
981
1203
  {
982
1204
  text: line,
@@ -986,29 +1208,29 @@ function LogPane({ lines, isActive = false, maxLines = 15 }) {
986
1208
  )) });
987
1209
  }
988
1210
  function LogLine({ text, cursor }) {
989
- const suffix = cursor ? /* @__PURE__ */ jsx2(Text2, { color: theme.primary, children: " \u258C" }) : null;
1211
+ const suffix = cursor ? /* @__PURE__ */ jsx3(Text3, { color: theme.primary, children: " \u258C" }) : null;
990
1212
  if (/^\[[\w:]+\]/.test(text)) {
991
1213
  const bracket = text.match(/^\[[\w:]+\]/)?.[0] ?? "";
992
1214
  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 }),
1215
+ return /* @__PURE__ */ jsxs3(Text3, { children: [
1216
+ /* @__PURE__ */ jsx3(Text3, { color: theme.primary, children: bracket }),
1217
+ /* @__PURE__ */ jsx3(Text3, { children: rest }),
996
1218
  suffix
997
1219
  ] });
998
1220
  }
999
- if (/^\s*\$\s/.test(text)) return /* @__PURE__ */ jsxs2(Text2, { color: theme.warning, children: [
1221
+ if (/^\s*\$\s/.test(text)) return /* @__PURE__ */ jsxs3(Text3, { color: theme.warning, children: [
1000
1222
  text,
1001
1223
  suffix
1002
1224
  ] });
1003
- if (text.startsWith("[warn]")) return /* @__PURE__ */ jsxs2(Text2, { color: theme.warning, children: [
1225
+ if (text.startsWith("[warn]")) return /* @__PURE__ */ jsxs3(Text3, { color: theme.warning, children: [
1004
1226
  text,
1005
1227
  suffix
1006
1228
  ] });
1007
- if (text.startsWith("[error]")) return /* @__PURE__ */ jsxs2(Text2, { color: theme.error, children: [
1229
+ if (text.startsWith("[error]")) return /* @__PURE__ */ jsxs3(Text3, { color: theme.error, children: [
1008
1230
  text,
1009
1231
  suffix
1010
1232
  ] });
1011
- return /* @__PURE__ */ jsxs2(Text2, { children: [
1233
+ return /* @__PURE__ */ jsxs3(Text3, { children: [
1012
1234
  text,
1013
1235
  suffix
1014
1236
  ] });
@@ -1028,8 +1250,8 @@ function useInterval(callback, delayMs) {
1028
1250
  }
1029
1251
 
1030
1252
  // src/ui/BrandMark.tsx
1031
- import { Box as Box3, Text as Text3 } from "ink";
1032
- import { jsx as jsx3 } from "react/jsx-runtime";
1253
+ import { Box as Box4, Text as Text4 } from "ink";
1254
+ import { jsx as jsx4 } from "react/jsx-runtime";
1033
1255
  var BRAND = "Executant";
1034
1256
  var SWEEP_TICKS = BRAND.length * 2;
1035
1257
  var GAP_TICKS = 30;
@@ -1042,11 +1264,12 @@ function charColor(charIndex, tick, isActive) {
1042
1264
  return charIndex === charPos ? theme.primaryLight : theme.primary;
1043
1265
  }
1044
1266
  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)) });
1267
+ return /* @__PURE__ */ jsx4(Box4, { children: [...BRAND].map((char, i) => /* @__PURE__ */ jsx4(Text4, { color: charColor(i, tick, isActive), bold: true, children: char }, i)) });
1046
1268
  }
1047
1269
 
1048
1270
  // src/ui/App.tsx
1049
- import { jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
1271
+ import { jsx as jsx5, jsxs as jsxs4 } from "react/jsx-runtime";
1272
+ var MAX_VISIBLE_ITERATIONS = 8;
1050
1273
  function App({ workflow: workflow2, events: events2, options: options2, updateCheck: updateCheck2 }) {
1051
1274
  const { exit } = useApp();
1052
1275
  const [state, dispatch] = useReducer(reducer, buildInitialState(workflow2));
@@ -1064,7 +1287,10 @@ function App({ workflow: workflow2, events: events2, options: options2, updateCh
1064
1287
  } catch (err) {
1065
1288
  if (!active) return;
1066
1289
  dispatch({ type: "log", level: "error", text: getErrorMessage(err) });
1067
- setTimeout(() => exit(err instanceof Error ? err : new Error(getErrorMessage(err))), EXIT_DELAY_MS);
1290
+ setTimeout(
1291
+ () => exit(err instanceof Error ? err : new Error(getErrorMessage(err))),
1292
+ EXIT_DELAY_MS
1293
+ );
1068
1294
  }
1069
1295
  })();
1070
1296
  return () => {
@@ -1084,14 +1310,16 @@ function App({ workflow: workflow2, events: events2, options: options2, updateCh
1084
1310
  }, [updateCheck2]);
1085
1311
  const elapsed = formatHeaderElapsed(state.startTime, state.endTime);
1086
1312
  const activeTask = state.tasks[state.currentIndex];
1087
- const completedCount = state.tasks.filter((t) => t.status === "complete").length;
1313
+ const completedCount = state.tasks.filter(
1314
+ (t) => t.status === "complete"
1315
+ ).length;
1088
1316
  const totalCount = state.tasks.length;
1089
1317
  const filterInfo = options2?.stepFilter ? ` [step: ${options2.stepFilter}]` : options2?.fromStep ? ` [from step: ${options2.fromStep}]` : "";
1090
- return /* @__PURE__ */ jsxs3(Box4, { flexDirection: "column", padding: 1, children: [
1091
- /* @__PURE__ */ jsx4(Box4, { marginBottom: 1, children: /* @__PURE__ */ jsx4(BrandMark, { tick, isActive: !state.endTime }) }),
1092
- /* @__PURE__ */ jsxs3(Box4, { marginBottom: 1, children: [
1093
- /* @__PURE__ */ jsx4(Text4, { bold: true, color: theme.primary, children: workflow2.goal }),
1094
- /* @__PURE__ */ jsxs3(Text4, { dimColor: true, children: [
1318
+ return /* @__PURE__ */ jsxs4(Box5, { flexDirection: "column", padding: 1, children: [
1319
+ /* @__PURE__ */ jsx5(Box5, { marginBottom: 1, children: /* @__PURE__ */ jsx5(BrandMark, { tick, isActive: !state.endTime }) }),
1320
+ /* @__PURE__ */ jsxs4(Box5, { marginBottom: 1, children: [
1321
+ /* @__PURE__ */ jsx5(Text5, { bold: true, color: theme.primary, children: workflow2.goal }),
1322
+ /* @__PURE__ */ jsxs4(Text5, { dimColor: true, children: [
1095
1323
  " ",
1096
1324
  completedCount,
1097
1325
  "/",
@@ -1101,33 +1329,48 @@ function App({ workflow: workflow2, events: events2, options: options2, updateCh
1101
1329
  filterInfo
1102
1330
  ] })
1103
1331
  ] }),
1104
- /* @__PURE__ */ jsx4(Box4, { flexDirection: "column", marginBottom: 1, children: state.tasks.map((taskState, i) => /* @__PURE__ */ jsx4(
1105
- TaskRow,
1332
+ /* @__PURE__ */ jsx5(Box5, { flexDirection: "column", marginBottom: 1, children: state.tasks.map((taskState, i) => /* @__PURE__ */ jsxs4(Box5, { flexDirection: "column", children: [
1333
+ /* @__PURE__ */ jsx5(
1334
+ TaskRow,
1335
+ {
1336
+ index: i,
1337
+ tick,
1338
+ taskState,
1339
+ isActive: i === state.currentIndex
1340
+ }
1341
+ ),
1342
+ taskState.status === "running" && taskState.iterationHistory?.length ? /* @__PURE__ */ jsx5(
1343
+ IterationList,
1344
+ {
1345
+ iterationHistory: taskState.iterationHistory,
1346
+ tick,
1347
+ maxVisible: MAX_VISIBLE_ITERATIONS
1348
+ }
1349
+ ) : null
1350
+ ] }, taskState.task.name)) }),
1351
+ activeTask && /* @__PURE__ */ jsx5(
1352
+ LogPane,
1106
1353
  {
1107
- index: i,
1108
- tick,
1109
- taskState,
1110
- isActive: i === state.currentIndex
1111
- },
1112
- taskState.task.name
1113
- )) }),
1114
- activeTask && /* @__PURE__ */ jsx4(LogPane, { lines: activeTask.lines, isActive: activeTask.status === "running" }),
1115
- state.endTime !== void 0 && state.writtenFiles.length > 0 && /* @__PURE__ */ jsxs3(Box4, { flexDirection: "column", marginTop: 1, children: [
1116
- /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: "files written:" }),
1117
- state.writtenFiles.map((f) => /* @__PURE__ */ jsxs3(Text4, { color: theme.primary, children: [
1354
+ lines: activeTask.lines,
1355
+ isActive: activeTask.status === "running"
1356
+ }
1357
+ ),
1358
+ state.endTime !== void 0 && state.writtenFiles.length > 0 && /* @__PURE__ */ jsxs4(Box5, { flexDirection: "column", marginTop: 1, children: [
1359
+ /* @__PURE__ */ jsx5(Text5, { dimColor: true, children: "files written:" }),
1360
+ state.writtenFiles.map((f) => /* @__PURE__ */ jsxs4(Text5, { color: theme.primary, children: [
1118
1361
  " ",
1119
1362
  f
1120
1363
  ] }, f))
1121
1364
  ] }),
1122
- /* @__PURE__ */ jsxs3(Box4, { marginTop: 1, flexDirection: "column", children: [
1123
- updateVersion && /* @__PURE__ */ jsxs3(Text4, { color: theme.warning, children: [
1365
+ /* @__PURE__ */ jsxs4(Box5, { marginTop: 1, flexDirection: "column", children: [
1366
+ updateVersion && /* @__PURE__ */ jsxs4(Text5, { color: theme.warning, children: [
1124
1367
  "v",
1125
1368
  updateVersion,
1126
1369
  " available \u2014 run: executant update"
1127
1370
  ] }),
1128
- /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: "press q to quit" })
1371
+ /* @__PURE__ */ jsx5(Text5, { dimColor: true, children: "press q to quit" })
1129
1372
  ] }),
1130
- isRawModeSupported && /* @__PURE__ */ jsx4(KeyboardHandler, { onExit: exit })
1373
+ isRawModeSupported && /* @__PURE__ */ jsx5(KeyboardHandler, { onExit: exit })
1131
1374
  ] });
1132
1375
  }
1133
1376
 
@@ -1146,25 +1389,9 @@ var PLAN_RETRY_SCHEMA_ERROR = loadPrompt("plan-retry-schema-error");
1146
1389
  var PLAN_RETRY_JUDGE = loadPrompt("plan-retry-judge");
1147
1390
  var MAX_PLAN_RETRIES = 3;
1148
1391
  var TOTAL_PLAN_STAGES = 3;
1149
- var StepSchema = z3.object({
1150
- name: z3.string(),
1151
- type: z3.enum(["prompt", "script", "log"]).optional(),
1152
- prompt: z3.string().optional(),
1153
- command: z3.string().optional(),
1154
- message: z3.string().optional(),
1155
- continue_on_error: z3.boolean().optional(),
1156
- self_healing: z3.boolean().optional(),
1157
- max_healing_attempts: z3.number().int().positive().optional(),
1158
- output: z3.string().optional(),
1159
- llm_as_judge: z3.boolean().optional(),
1160
- allowed_tools: z3.array(z3.string()).optional(),
1161
- forEach: z3.union([z3.array(z3.string()), z3.string()]).optional(),
1162
- repeat: z3.number().int().positive().optional(),
1163
- context: z3.array(z3.string()).optional()
1164
- });
1165
1392
  var WorkflowSchema = z3.object({
1166
1393
  goal: z3.string(),
1167
- steps: z3.array(StepSchema).min(1),
1394
+ steps: z3.array(RawStepSchema).min(1),
1168
1395
  vars: z3.record(z3.string()).optional(),
1169
1396
  self_improve: z3.boolean().optional()
1170
1397
  });
@@ -1184,7 +1411,10 @@ function walkUp(startDir, check) {
1184
1411
  }
1185
1412
  }
1186
1413
  function findGitRoot(startDir) {
1187
- return walkUp(startDir, (dir) => existsSync(join2(dir, ".git")) ? dir : null);
1414
+ return walkUp(
1415
+ startDir,
1416
+ (dir) => existsSync(join2(dir, ".git")) ? dir : null
1417
+ );
1188
1418
  }
1189
1419
  function findProjectRoot(startDir) {
1190
1420
  return walkUp(startDir, (dir) => {
@@ -1335,34 +1565,27 @@ function normalizeWorkflow(workflow2) {
1335
1565
  return { ...workflow2, steps: collapseSequentialSteps(steps) };
1336
1566
  }
1337
1567
  function collapseSequentialSteps(steps) {
1338
- const result = [];
1339
- let i = 0;
1340
- while (i < steps.length) {
1341
- const step = steps[i];
1342
- if (step.forEach !== void 0 || step.repeat !== void 0) {
1343
- result.push(step);
1344
- i++;
1345
- continue;
1346
- }
1347
- const m = step.name.match(/^(.+?)_1$/);
1348
- if (!m) {
1349
- result.push(step);
1350
- i++;
1351
- continue;
1352
- }
1353
- const prefix = m[1];
1354
- let n = 1;
1355
- while (i + n < steps.length && steps[i + n].name === `${prefix}_${n + 1}`) n++;
1356
- if (n < 2) {
1357
- result.push(step);
1358
- i++;
1359
- continue;
1360
- }
1361
- const { name: _name, ...rest } = step;
1362
- result.push({ ...rest, name: `${prefix}_{{item}}`, repeat: n });
1363
- i += n;
1364
- }
1365
- return result;
1568
+ return steps.reduce(
1569
+ ({ out, skip }, step, i, arr) => {
1570
+ if (skip > 0) return { out, skip: skip - 1 };
1571
+ if (step.forEach !== void 0 || step.repeat !== void 0 || step.steps !== void 0) {
1572
+ return { out: [...out, step], skip: 0 };
1573
+ }
1574
+ const m = step.name.match(/^(.+?)_1$/);
1575
+ if (!m) return { out: [...out, step], skip: 0 };
1576
+ const prefix = m[1];
1577
+ let n = 1;
1578
+ while (i + n < arr.length && arr[i + n].name === `${prefix}_${n + 1}`)
1579
+ n++;
1580
+ if (n < 2) return { out: [...out, step], skip: 0 };
1581
+ const { name: _name, ...rest } = step;
1582
+ return {
1583
+ out: [...out, { ...rest, name: `${prefix}_{{item}}`, repeat: n }],
1584
+ skip: n - 1
1585
+ };
1586
+ },
1587
+ { out: [], skip: 0 }
1588
+ ).out;
1366
1589
  }
1367
1590
  async function* streamPlan(args) {
1368
1591
  const { description, taskFile } = args;
@@ -1373,14 +1596,24 @@ async function* streamPlan(args) {
1373
1596
  yield { type: "plan:stages", names: ["Decompose to Steps", "Validate"] };
1374
1597
  researchDoc = "No codebase research performed \u2014 the task is self-contained. Work directly from the user's original goal.";
1375
1598
  } else {
1376
- yield { type: "plan:stages", names: ["Research & Planning", "Decompose to Steps", "Validate"] };
1377
- yield { type: "plan:stage", stage: 1, total: TOTAL_PLAN_STAGES, name: "Research & Planning" };
1599
+ yield {
1600
+ type: "plan:stages",
1601
+ names: ["Research & Planning", "Decompose to Steps", "Validate"]
1602
+ };
1603
+ yield {
1604
+ type: "plan:stage",
1605
+ stage: 1,
1606
+ total: TOTAL_PLAN_STAGES,
1607
+ name: "Research & Planning"
1608
+ };
1378
1609
  const researchLines = [];
1379
1610
  try {
1380
1611
  const researchTask = {
1381
1612
  type: "claude",
1382
1613
  name: "plan:research",
1383
- prompt: fillTemplate(PLAN_RESEARCH_PROMPT, { DESCRIPTION: description }),
1614
+ prompt: fillTemplate(PLAN_RESEARCH_PROMPT, {
1615
+ DESCRIPTION: description
1616
+ }),
1384
1617
  allowedTools: ["Read", "Glob", "Grep"],
1385
1618
  permissionMode: "bypassPermissions",
1386
1619
  model: "opus"
@@ -1394,17 +1627,28 @@ async function* streamPlan(args) {
1394
1627
  }
1395
1628
  }
1396
1629
  } catch (err) {
1397
- yield { type: "plan:error", message: `Research pass failed: ${getErrorMessage(err)}` };
1630
+ yield {
1631
+ type: "plan:error",
1632
+ message: `Research pass failed: ${getErrorMessage(err)}`
1633
+ };
1398
1634
  return;
1399
1635
  }
1400
1636
  researchDoc = researchLines.join("\n");
1401
1637
  if (!researchDoc.trim()) {
1402
- yield { type: "plan:error", message: "Research pass produced no output \u2014 cannot decompose" };
1638
+ yield {
1639
+ type: "plan:error",
1640
+ message: "Research pass produced no output \u2014 cannot decompose"
1641
+ };
1403
1642
  return;
1404
1643
  }
1405
1644
  }
1406
1645
  const stages = skipResearch ? { decompose: 1, validate: 2, total: 2 } : { decompose: 2, validate: 3, total: TOTAL_PLAN_STAGES };
1407
- yield { type: "plan:stage", stage: stages.decompose, total: stages.total, name: "Decompose to Steps" };
1646
+ yield {
1647
+ type: "plan:stage",
1648
+ stage: stages.decompose,
1649
+ total: stages.total,
1650
+ name: "Decompose to Steps"
1651
+ };
1408
1652
  let retryPrefix = "";
1409
1653
  for (let attempt = 0; attempt < MAX_PLAN_RETRIES; attempt++) {
1410
1654
  if (attempt > 0) {
@@ -1414,9 +1658,17 @@ async function* streamPlan(args) {
1414
1658
  maxAttempts: MAX_PLAN_RETRIES,
1415
1659
  reason: retryPrefix.replace(/\n/g, " ")
1416
1660
  };
1417
- yield { type: "plan:stage", stage: stages.decompose, total: stages.total, name: "Decompose to Steps" };
1661
+ yield {
1662
+ type: "plan:stage",
1663
+ stage: stages.decompose,
1664
+ total: stages.total,
1665
+ name: "Decompose to Steps"
1666
+ };
1418
1667
  }
1419
- const basePrompt = fillTemplate(PLAN_DECOMPOSE_PROMPT, { DESCRIPTION: description, RESEARCH_DOC: researchDoc });
1668
+ const basePrompt = fillTemplate(PLAN_DECOMPOSE_PROMPT, {
1669
+ DESCRIPTION: description,
1670
+ RESEARCH_DOC: researchDoc
1671
+ });
1420
1672
  const decomposeTask = {
1421
1673
  type: "claude",
1422
1674
  name: "plan:decompose",
@@ -1448,7 +1700,10 @@ ${basePrompt}` : basePrompt,
1448
1700
  yield { type: "plan:error", message: msg };
1449
1701
  return;
1450
1702
  }
1451
- retryPrefix = fillTemplate(PLAN_RETRY_PARSE_ERROR, { ERROR: msg, EXCERPT: decomposeTextLines.join("\n") });
1703
+ retryPrefix = fillTemplate(PLAN_RETRY_PARSE_ERROR, {
1704
+ ERROR: msg,
1705
+ EXCERPT: decomposeTextLines.join("\n")
1706
+ });
1452
1707
  continue;
1453
1708
  }
1454
1709
  if (structuredOutput === void 0) {
@@ -1464,24 +1719,40 @@ ${basePrompt}` : basePrompt,
1464
1719
  if (!zodResult.success) {
1465
1720
  const issues = formatZodIssues(zodResult.error.issues);
1466
1721
  if (attempt === MAX_PLAN_RETRIES - 1) {
1467
- yield { type: "plan:error", message: `Plan did not match expected schema:
1468
- ${issues}` };
1722
+ yield {
1723
+ type: "plan:error",
1724
+ message: `Plan did not match expected schema:
1725
+ ${issues}`
1726
+ };
1469
1727
  return;
1470
1728
  }
1471
1729
  retryPrefix = fillTemplate(PLAN_RETRY_SCHEMA_ERROR, { ISSUES: issues });
1472
1730
  continue;
1473
1731
  }
1474
- yield { type: "plan:stage", stage: stages.validate, total: stages.total, name: "Validate" };
1732
+ yield {
1733
+ type: "plan:stage",
1734
+ stage: stages.validate,
1735
+ total: stages.total,
1736
+ name: "Validate"
1737
+ };
1475
1738
  const judgeResult = await runPass3Judge(description, zodResult.data);
1476
1739
  if (judgeResult.skipped) {
1477
- yield { type: "plan:warn", message: "Judge skipped due to error \u2014 proceeding without validation" };
1740
+ yield {
1741
+ type: "plan:warn",
1742
+ message: "Judge skipped due to error \u2014 proceeding without validation"
1743
+ };
1478
1744
  }
1479
1745
  if (!judgeResult.pass && attempt < MAX_PLAN_RETRIES - 1) {
1480
- retryPrefix = fillTemplate(PLAN_RETRY_JUDGE, { FEEDBACK: judgeResult.feedback });
1746
+ retryPrefix = fillTemplate(PLAN_RETRY_JUDGE, {
1747
+ FEEDBACK: judgeResult.feedback
1748
+ });
1481
1749
  continue;
1482
1750
  }
1483
1751
  if (!judgeResult.pass) {
1484
- yield { type: "plan:warn", message: `Judge rejected plan but retries exhausted: ${judgeResult.feedback}` };
1752
+ yield {
1753
+ type: "plan:warn",
1754
+ message: `Judge rejected plan but retries exhausted: ${judgeResult.feedback}`
1755
+ };
1485
1756
  }
1486
1757
  const { goal, vars, steps, ...rest } = normalizeWorkflow(zodResult.data);
1487
1758
  const ordered = { goal, ...vars && { vars }, steps, ...rest };
@@ -1497,13 +1768,16 @@ ${issues}` };
1497
1768
  yield { type: "plan:complete", taskFile, preview };
1498
1769
  return;
1499
1770
  }
1500
- yield { type: "plan:error", message: "Plan generation failed after maximum retries" };
1771
+ yield {
1772
+ type: "plan:error",
1773
+ message: "Plan generation failed after maximum retries"
1774
+ };
1501
1775
  }
1502
1776
 
1503
1777
  // src/ui/PlanApp.tsx
1504
1778
  import { useEffect as useEffect3, useReducer as useReducer2, useState as useState2 } from "react";
1505
- import { Box as Box5, Text as Text5, useApp as useApp2, useStdin as useStdin2 } from "ink";
1506
- import { jsx as jsx5, jsxs as jsxs4 } from "react/jsx-runtime";
1779
+ import { Box as Box6, Text as Text6, useApp as useApp2, useStdin as useStdin2 } from "ink";
1780
+ import { jsx as jsx6, jsxs as jsxs5 } from "react/jsx-runtime";
1507
1781
  var truncate = (str, max) => str.length > max ? str.slice(0, max - 3) + "..." : str;
1508
1782
  function buildInitial(description) {
1509
1783
  return {
@@ -1579,7 +1853,7 @@ function StageProgress({ stage, totalStages, stageNames, tick, isActive, status
1579
1853
  }
1580
1854
  return { icon, color, name, bold, dim };
1581
1855
  });
1582
- 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: [
1856
+ return /* @__PURE__ */ jsx6(Box6, { flexDirection: "column", marginBottom: 1, children: rows.map(({ icon, color, name, bold, dim }, i) => /* @__PURE__ */ jsx6(Box6, { children: /* @__PURE__ */ jsxs5(Text6, { color, dimColor: dim, bold, children: [
1583
1857
  " ",
1584
1858
  icon,
1585
1859
  name ? ` ${name}` : ""
@@ -1612,19 +1886,19 @@ function PlanApp({ description, events: events2 }) {
1612
1886
  const elapsed = formatHeaderElapsed(state.startTime);
1613
1887
  const icon = isActive ? SPINNER[tick % SPINNER.length] : state.status === "complete" ? "\u2713" : "\u2717";
1614
1888
  const iconColor = state.status === "complete" ? theme.success : state.status === "error" ? theme.error : theme.primary;
1615
- return /* @__PURE__ */ jsxs4(Box5, { flexDirection: "column", padding: 1, children: [
1616
- /* @__PURE__ */ jsx5(Box5, { marginBottom: 1, children: /* @__PURE__ */ jsx5(BrandMark, { tick, isActive }) }),
1617
- /* @__PURE__ */ jsxs4(Box5, { marginBottom: 1, children: [
1618
- /* @__PURE__ */ jsxs4(Text5, { color: iconColor, children: [
1889
+ return /* @__PURE__ */ jsxs5(Box6, { flexDirection: "column", padding: 1, children: [
1890
+ /* @__PURE__ */ jsx6(Box6, { marginBottom: 1, children: /* @__PURE__ */ jsx6(BrandMark, { tick, isActive }) }),
1891
+ /* @__PURE__ */ jsxs5(Box6, { marginBottom: 1, children: [
1892
+ /* @__PURE__ */ jsxs5(Text6, { color: iconColor, children: [
1619
1893
  icon,
1620
1894
  " "
1621
1895
  ] }),
1622
- /* @__PURE__ */ jsx5(Text5, { bold: true, color: theme.primary, children: "Generating plan" }),
1623
- /* @__PURE__ */ jsxs4(Text5, { dimColor: true, children: [
1896
+ /* @__PURE__ */ jsx6(Text6, { bold: true, color: theme.primary, children: "Generating plan" }),
1897
+ /* @__PURE__ */ jsxs5(Text6, { dimColor: true, children: [
1624
1898
  " ",
1625
1899
  elapsed
1626
1900
  ] }),
1627
- state.status === "retrying" && /* @__PURE__ */ jsxs4(Text5, { color: theme.warning, children: [
1901
+ state.status === "retrying" && /* @__PURE__ */ jsxs5(Text6, { color: theme.warning, children: [
1628
1902
  " ",
1629
1903
  "(attempt ",
1630
1904
  state.attempt,
@@ -1633,11 +1907,11 @@ function PlanApp({ description, events: events2 }) {
1633
1907
  ")"
1634
1908
  ] })
1635
1909
  ] }),
1636
- /* @__PURE__ */ jsxs4(Box5, { marginBottom: 1, children: [
1637
- /* @__PURE__ */ jsx5(Text5, { dimColor: true, children: " " }),
1638
- /* @__PURE__ */ jsx5(Text5, { children: truncate(state.description, 80) })
1910
+ /* @__PURE__ */ jsxs5(Box6, { marginBottom: 1, children: [
1911
+ /* @__PURE__ */ jsx6(Text6, { dimColor: true, children: " " }),
1912
+ /* @__PURE__ */ jsx6(Text6, { children: truncate(state.description, 80) })
1639
1913
  ] }),
1640
- /* @__PURE__ */ jsx5(
1914
+ /* @__PURE__ */ jsx6(
1641
1915
  StageProgress,
1642
1916
  {
1643
1917
  stage: state.stage,
@@ -1648,23 +1922,23 @@ function PlanApp({ description, events: events2 }) {
1648
1922
  status: state.status
1649
1923
  }
1650
1924
  ),
1651
- state.lines.length > 0 && /* @__PURE__ */ jsx5(LogPane, { lines: state.lines, isActive, maxLines: 10 }),
1652
- state.status === "complete" && state.taskFile && /* @__PURE__ */ jsxs4(Box5, { flexDirection: "column", marginTop: 1, children: [
1653
- /* @__PURE__ */ jsxs4(Text5, { color: theme.success, children: [
1925
+ state.lines.length > 0 && /* @__PURE__ */ jsx6(LogPane, { lines: state.lines, isActive, maxLines: 10 }),
1926
+ state.status === "complete" && state.taskFile && /* @__PURE__ */ jsxs5(Box6, { flexDirection: "column", marginTop: 1, children: [
1927
+ /* @__PURE__ */ jsxs5(Text6, { color: theme.success, children: [
1654
1928
  "\u2705 Task plan saved: ",
1655
1929
  state.taskFile
1656
1930
  ] }),
1657
- state.preview && /* @__PURE__ */ jsxs4(Box5, { flexDirection: "column", marginTop: 1, children: [
1658
- /* @__PURE__ */ jsx5(Text5, { dimColor: true, children: "Preview:" }),
1659
- /* @__PURE__ */ jsx5(Text5, { children: state.preview })
1931
+ state.preview && /* @__PURE__ */ jsxs5(Box6, { flexDirection: "column", marginTop: 1, children: [
1932
+ /* @__PURE__ */ jsx6(Text6, { dimColor: true, children: "Preview:" }),
1933
+ /* @__PURE__ */ jsx6(Text6, { children: state.preview })
1660
1934
  ] })
1661
1935
  ] }),
1662
- state.status === "error" && /* @__PURE__ */ jsx5(Box5, { marginTop: 1, children: /* @__PURE__ */ jsxs4(Text5, { color: theme.error, children: [
1936
+ state.status === "error" && /* @__PURE__ */ jsx6(Box6, { marginTop: 1, children: /* @__PURE__ */ jsxs5(Text6, { color: theme.error, children: [
1663
1937
  "Error: ",
1664
1938
  state.errorMessage
1665
1939
  ] }) }),
1666
- isActive && /* @__PURE__ */ jsx5(Box5, { marginTop: 1, children: /* @__PURE__ */ jsx5(Text5, { dimColor: true, children: "press q to quit" }) }),
1667
- isRawModeSupported && /* @__PURE__ */ jsx5(KeyboardHandler, { onExit: exit })
1940
+ isActive && /* @__PURE__ */ jsx6(Box6, { marginTop: 1, children: /* @__PURE__ */ jsx6(Text6, { dimColor: true, children: "press q to quit" }) }),
1941
+ isRawModeSupported && /* @__PURE__ */ jsx6(KeyboardHandler, { onExit: exit })
1668
1942
  ] });
1669
1943
  }
1670
1944
 
@@ -1724,39 +1998,57 @@ function onWorkflowStart(ctx, s) {
1724
1998
  mkdirSync3(ctx.logDir, { recursive: true });
1725
1999
  mkdirSync3(ctx.highlightsDir, { recursive: true });
1726
2000
  const logFile = join3(ctx.logDir, `${ctx.ts}_${ctx.slug}.log`);
1727
- writeFileSync3(logFile, `# Execution Log
2001
+ writeFileSync3(
2002
+ logFile,
2003
+ `# Execution Log
1728
2004
  Task: ${ctx.slug}
1729
2005
  Started: ${(/* @__PURE__ */ new Date()).toISOString()}
1730
2006
  ${"\u2501".repeat(51)}
1731
2007
 
1732
- `);
2008
+ `
2009
+ );
1733
2010
  return { ...s, logFile };
1734
2011
  }
1735
2012
  function onStepStart(ctx, s, index, name) {
1736
- const next = { ...INIT_STATE, logFile: s.logFile, stepIndex: index, stepName: name, stepStartMs: Date.now() };
1737
- appendLog(next.logFile, `
2013
+ const next = {
2014
+ ...INIT_STATE,
2015
+ logFile: s.logFile,
2016
+ stepIndex: index,
2017
+ stepName: name,
2018
+ stepStartMs: Date.now()
2019
+ };
2020
+ appendLog(
2021
+ next.logFile,
2022
+ `
1738
2023
  ${"\u2501".repeat(51)}
1739
2024
  Step ${index + 1}: ${name}
1740
2025
  Started: ${(/* @__PURE__ */ new Date()).toISOString()}
1741
2026
  ${"\u2501".repeat(51)}
1742
- `);
2027
+ `
2028
+ );
1743
2029
  return next;
1744
2030
  }
1745
2031
  function finalizeComplexSequence(s) {
1746
2032
  if (s.toolCount >= 3 && s.complexSequenceFile) {
1747
- appendFileSync(s.complexSequenceFile, `
2033
+ appendFileSync(
2034
+ s.complexSequenceFile,
2035
+ `
1748
2036
  ---
1749
2037
 
1750
2038
  *Total tools used: ${s.toolCount}*
1751
2039
 
1752
2040
  *Captured by Executant Logger*
1753
- `);
2041
+ `
2042
+ );
1754
2043
  }
1755
2044
  }
1756
2045
  function onStepComplete(s) {
1757
- appendLog(s.logFile, `
2046
+ appendLog(
2047
+ s.logFile,
2048
+ `
1758
2049
  Step completed in ${((Date.now() - s.stepStartMs) / 1e3).toFixed(1)}s
1759
- `);
2050
+ `
2051
+ );
1760
2052
  finalizeComplexSequence(s);
1761
2053
  return s;
1762
2054
  }
@@ -1794,29 +2086,35 @@ function onTool(ctx, s, tool, input) {
1794
2086
  const toolCount = s.toolCount + 1;
1795
2087
  const complexSequenceFile = toolCount === 3 ? createComplexSequenceFile(ctx, s) : s.complexSequenceFile;
1796
2088
  if (toolCount >= 3 && complexSequenceFile) {
1797
- appendFileSync(complexSequenceFile, `${toolCount}. **${tool}** - ${desc}
1798
- `);
2089
+ appendFileSync(
2090
+ complexSequenceFile,
2091
+ `${toolCount}. **${tool}** - ${desc}
2092
+ `
2093
+ );
1799
2094
  }
1800
2095
  return { ...s, toolCount, complexSequenceFile };
1801
2096
  }
1802
2097
  function saveJudgeHighlight(ctx, s, verdict, text) {
1803
- writeFileSync3(highlightPath(ctx, s.stepIndex, `judge_${verdict}`), [
1804
- `# Judge Verdict: ${verdict}`,
1805
- "",
1806
- `**Task:** ${ctx.slug}`,
1807
- `**Step:** ${s.stepName}`,
1808
- `**Attempt:** ${s.judgeAttempt}`,
1809
- `**Timestamp:** ${(/* @__PURE__ */ new Date()).toISOString()}`,
1810
- "",
1811
- "---",
1812
- "",
1813
- text,
1814
- "",
1815
- "---",
1816
- "",
1817
- "*Auto-captured*",
1818
- ""
1819
- ].join("\n"));
2098
+ writeFileSync3(
2099
+ highlightPath(ctx, s.stepIndex, `judge_${verdict}`),
2100
+ [
2101
+ `# Judge Verdict: ${verdict}`,
2102
+ "",
2103
+ `**Task:** ${ctx.slug}`,
2104
+ `**Step:** ${s.stepName}`,
2105
+ `**Attempt:** ${s.judgeAttempt}`,
2106
+ `**Timestamp:** ${(/* @__PURE__ */ new Date()).toISOString()}`,
2107
+ "",
2108
+ "---",
2109
+ "",
2110
+ text,
2111
+ "",
2112
+ "---",
2113
+ "",
2114
+ "*Auto-captured*",
2115
+ ""
2116
+ ].join("\n")
2117
+ );
1820
2118
  }
1821
2119
  var LOG_MATCHERS = [
1822
2120
  {
@@ -1832,29 +2130,32 @@ var LOG_MATCHERS = [
1832
2130
  pattern: /\[self-healing\].*failed.*exit\s+(\d+)/i,
1833
2131
  apply: (ctx, s, text, match) => {
1834
2132
  const selfHealingFile = highlightPath(ctx, s.stepIndex, "self_healing");
1835
- writeFileSync3(selfHealingFile, [
1836
- "# Self-Healing Activation",
1837
- "",
1838
- `**Task:** ${ctx.slug}`,
1839
- `**Step:** ${s.stepName}`,
1840
- `**Timestamp:** ${(/* @__PURE__ */ new Date()).toISOString()}`,
1841
- "",
1842
- "---",
1843
- "",
1844
- "## \u274C Failure Detected",
1845
- "",
1846
- `**Exit Code:** ${match[1]}`,
1847
- "",
1848
- "**Recent Output:**",
1849
- "```",
1850
- s.recentOutput.join("\n"),
1851
- "```",
1852
- "",
1853
- "---",
1854
- "",
1855
- "## \u{1F527} Claude's Healing Process",
1856
- ""
1857
- ].join("\n"));
2133
+ writeFileSync3(
2134
+ selfHealingFile,
2135
+ [
2136
+ "# Self-Healing Activation",
2137
+ "",
2138
+ `**Task:** ${ctx.slug}`,
2139
+ `**Step:** ${s.stepName}`,
2140
+ `**Timestamp:** ${(/* @__PURE__ */ new Date()).toISOString()}`,
2141
+ "",
2142
+ "---",
2143
+ "",
2144
+ "## \u274C Failure Detected",
2145
+ "",
2146
+ `**Exit Code:** ${match[1]}`,
2147
+ "",
2148
+ "**Recent Output:**",
2149
+ "```",
2150
+ s.recentOutput.join("\n"),
2151
+ "```",
2152
+ "",
2153
+ "---",
2154
+ "",
2155
+ "## \u{1F527} Claude's Healing Process",
2156
+ ""
2157
+ ].join("\n")
2158
+ );
1858
2159
  return { ...s, selfHealingFile, recentOutput: [] };
1859
2160
  }
1860
2161
  },
@@ -1862,21 +2163,24 @@ var LOG_MATCHERS = [
1862
2163
  pattern: /\[self-healing\].*Re-running/i,
1863
2164
  apply: (_ctx, s) => {
1864
2165
  if (!s.selfHealingFile) return s;
1865
- appendFileSync(s.selfHealingFile, [
1866
- "",
1867
- "*(See full log for Claude's diagnostic process)*",
1868
- "",
1869
- "---",
1870
- "",
1871
- "## \u2705 Resolution Applied",
1872
- "",
1873
- "The self-healing process completed. Check the full execution log to see Claude's analysis and fix.",
1874
- "",
1875
- "---",
1876
- "",
1877
- "*Auto-captured*",
1878
- ""
1879
- ].join("\n"));
2166
+ appendFileSync(
2167
+ s.selfHealingFile,
2168
+ [
2169
+ "",
2170
+ "*(See full log for Claude's diagnostic process)*",
2171
+ "",
2172
+ "---",
2173
+ "",
2174
+ "## \u2705 Resolution Applied",
2175
+ "",
2176
+ "The self-healing process completed. Check the full execution log to see Claude's analysis and fix.",
2177
+ "",
2178
+ "---",
2179
+ "",
2180
+ "*Auto-captured*",
2181
+ ""
2182
+ ].join("\n")
2183
+ );
1880
2184
  return { ...s, selfHealingFile: "" };
1881
2185
  }
1882
2186
  }
@@ -1893,30 +2197,39 @@ function onLogMessage(ctx, s, level, text) {
1893
2197
  ).state;
1894
2198
  }
1895
2199
  function onWorkflowComplete(ctx, s) {
1896
- appendLog(s.logFile, `
2200
+ appendLog(
2201
+ s.logFile,
2202
+ `
1897
2203
  ${"\u2501".repeat(51)}
1898
2204
  Task Complete: ${ctx.slug}
1899
2205
  Finished: ${(/* @__PURE__ */ new Date()).toISOString()}
1900
2206
  ${"\u2501".repeat(51)}
1901
- `);
2207
+ `
2208
+ );
1902
2209
  const indexFile = join3(ctx.highlightsDir, "README.md");
1903
2210
  if (!existsSync2(indexFile)) {
1904
- writeFileSync3(indexFile, [
1905
- "# Execution Highlights",
1906
- "",
1907
- "This directory contains automatically extracted highlight moments from task executions.",
1908
- "",
1909
- "## Latest Highlights",
1910
- ""
1911
- ].join("\n"));
2211
+ writeFileSync3(
2212
+ indexFile,
2213
+ [
2214
+ "# Execution Highlights",
2215
+ "",
2216
+ "This directory contains automatically extracted highlight moments from task executions.",
2217
+ "",
2218
+ "## Latest Highlights",
2219
+ ""
2220
+ ].join("\n")
2221
+ );
1912
2222
  }
1913
2223
  const highlights = readdirSync(ctx.highlightsDir).filter((f) => f.startsWith(ctx.ts) && f.endsWith(".md")).sort();
1914
2224
  if (highlights.length > 0) {
1915
2225
  const entries = highlights.map((f) => `- [${f.replace(/\.md$/, "")}](./${f})`).join("\n");
1916
- appendFileSync(indexFile, `
2226
+ appendFileSync(
2227
+ indexFile,
2228
+ `
1917
2229
  ### ${ctx.slug} (${(/* @__PURE__ */ new Date()).toISOString()})
1918
2230
  ${entries}
1919
- `);
2231
+ `
2232
+ );
1920
2233
  }
1921
2234
  return s;
1922
2235
  }
@@ -1934,6 +2247,19 @@ function reduce(ctx, s, event) {
1934
2247
  return onStepComplete(s);
1935
2248
  case "step:error":
1936
2249
  return onStepError(s, event.error);
2250
+ case "step:iteration":
2251
+ appendLog(
2252
+ s.logFile,
2253
+ `
2254
+ \u2500\u2500 iteration ${event.iteration}/${event.total}: ${event.item}`
2255
+ );
2256
+ return s;
2257
+ case "step:inner":
2258
+ appendLog(
2259
+ s.logFile,
2260
+ ` \u21B3 [${event.innerIndex + 1}/${event.innerTotal}] ${event.name}`
2261
+ );
2262
+ return s;
1937
2263
  case "output:text":
1938
2264
  return onOutputText(s, event.text);
1939
2265
  case "output:tool":
@@ -2115,16 +2441,21 @@ function extractJson(text) {
2115
2441
 
2116
2442
  // src/index.ts
2117
2443
  var CURRENT_VERSION = JSON.parse(
2118
- readFileSync6(join5(dirname5(fileURLToPath2(import.meta.url)), "../package.json"), "utf-8")
2444
+ readFileSync6(
2445
+ join5(dirname5(fileURLToPath2(import.meta.url)), "../package.json"),
2446
+ "utf-8"
2447
+ )
2119
2448
  ).version;
2120
2449
  var rawArgs = process.argv.slice(2);
2121
2450
  if (rawArgs[0] === "plan") {
2122
2451
  const planArgs = parsePlanArgs(rawArgs.slice(1));
2123
2452
  const planEvents = streamPlan(planArgs);
2124
- const inkApp = render(React3.createElement(PlanApp, {
2125
- description: planArgs.description,
2126
- events: planEvents
2127
- }));
2453
+ const inkApp = render(
2454
+ React3.createElement(PlanApp, {
2455
+ description: planArgs.description,
2456
+ events: planEvents
2457
+ })
2458
+ );
2128
2459
  try {
2129
2460
  await inkApp.waitUntilExit();
2130
2461
  } catch {
@@ -2173,7 +2504,15 @@ YAML \u2014 step fields (all step types):
2173
2504
  forEach string or list
2174
2505
  Inline YAML array OR a shell command whose newline-split
2175
2506
  stdout provides the items. {{item}} is substituted per
2176
- iteration in the inner step's prompt or command.
2507
+ iteration in every child step's name, command, and prompt.
2508
+ repeat int Run this step N times; {{item}} is the 1-based
2509
+ iteration number. Mutually exclusive with forEach.
2510
+ steps list Multiple child steps to run per forEach/repeat
2511
+ iteration. Mutually exclusive with command/prompt on the
2512
+ parent step. Requires forEach or repeat.
2513
+ context list Var names whose file-path values are prepended to
2514
+ a prompt step's content at runtime.
2515
+ output string Var name; captures script stdout to that file path.
2177
2516
 
2178
2517
  YAML \u2014 prompt step fields (type: prompt, or inferred when prompt is present):
2179
2518
  prompt string (required) Instructions sent to Claude
@@ -2185,7 +2524,7 @@ YAML \u2014 prompt step fields (type: prompt, or inferred when prompt is present
2185
2524
  YAML \u2014 script step fields (type: script | command, or inferred when command is present):
2186
2525
  command string (required) Bash command to execute
2187
2526
  self_healing bool On failure, Claude diagnoses and fixes iteratively
2188
- up to 5 attempts with accumulated context (default: true)
2527
+ up to 5 attempts with accumulated context (default: false)
2189
2528
  max_healing_attempts int Override max self-healing retries (default: 5)
2190
2529
 
2191
2530
  YAML \u2014 log step fields (type: log, or inferred when message is present and prompt is absent):
@@ -2263,9 +2602,17 @@ function errorReplacer(_key, value) {
2263
2602
  async function maybeRunRetrospective(filePath2, workflow2, logger2) {
2264
2603
  if (!logger2) return;
2265
2604
  try {
2266
- await runRetrospective(filePath2, workflow2, logger2.getHighlightsDir(), logger2.getTimestamp());
2605
+ await runRetrospective(
2606
+ filePath2,
2607
+ workflow2,
2608
+ logger2.getHighlightsDir(),
2609
+ logger2.getTimestamp()
2610
+ );
2267
2611
  } catch (err) {
2268
- console.warn("[executant] retrospective failed (non-fatal):", getErrorMessage(err));
2612
+ console.warn(
2613
+ "[executant] retrospective failed (non-fatal):",
2614
+ getErrorMessage(err)
2615
+ );
2269
2616
  }
2270
2617
  }
2271
2618
  if (ciMode) {
@@ -2281,7 +2628,9 @@ if (ciMode) {
2281
2628
  process.exit(1);
2282
2629
  });
2283
2630
  } else {
2284
- const inkApp = render(React3.createElement(App, { workflow, events, options, updateCheck }));
2631
+ const inkApp = render(
2632
+ React3.createElement(App, { workflow, events, options, updateCheck })
2633
+ );
2285
2634
  if (workflow.selfImprove) {
2286
2635
  inkApp.waitUntilExit().then(() => maybeRunRetrospective(filePath, workflow, logger)).catch(() => {
2287
2636
  });