executant 1.10.1 → 1.12.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.
Files changed (2) hide show
  1. package/dist/index.js +107 -100
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -70,7 +70,9 @@ function stripPromptHeader(raw) {
70
70
  return raw.replace(/^(#[^\n]*\n)+\n?/, "").trim();
71
71
  }
72
72
  function loadPrompt(name) {
73
- return stripPromptHeader(readFileSync(join(PROMPTS_DIR, `${name}.txt`), "utf8"));
73
+ return stripPromptHeader(
74
+ readFileSync(join(PROMPTS_DIR, `${name}.txt`), "utf8")
75
+ );
74
76
  }
75
77
  function findOutermostBraces(text) {
76
78
  const start = text.indexOf("{");
@@ -110,6 +112,29 @@ function formatTimestamp(d) {
110
112
  function timestamp() {
111
113
  return formatTimestamp(/* @__PURE__ */ new Date());
112
114
  }
115
+ var ANSI_RE = /\x1B(?:\[[0-9;?]*[A-Za-z]|\][^\x07]*\x07)|[\r]/g;
116
+ function stripAnsi(s) {
117
+ return s.replace(ANSI_RE, "");
118
+ }
119
+ var TOOL_ARG = {
120
+ Read: (i) => String(i["file_path"] ?? i["path"] ?? ""),
121
+ Edit: (i) => String(i["file_path"] ?? ""),
122
+ Write: (i) => String(i["file_path"] ?? ""),
123
+ Bash: (i) => String(i["command"] ?? ""),
124
+ Glob: (i) => String(i["pattern"] ?? ""),
125
+ Grep: (i) => String(i["pattern"] ?? "")
126
+ };
127
+ function getToolArg(tool, input) {
128
+ const fn = TOOL_ARG[tool];
129
+ return fn ? fn(input) : JSON.stringify(input);
130
+ }
131
+ function formatToolCall(tool, input) {
132
+ const fn = TOOL_ARG[tool];
133
+ return fn ? `${tool}(${fn(input)})` : JSON.stringify({ tool, ...input });
134
+ }
135
+ function normalizeError(err) {
136
+ return err instanceof Error ? err : new Error(String(err));
137
+ }
113
138
 
114
139
  // src/load-workflow.ts
115
140
  import { z } from "zod";
@@ -487,10 +512,6 @@ function buildExitError(code, plainLines) {
487
512
  ${plainLines.join("\n")}` : "";
488
513
  return new Error(`claude exited with code ${code}${detail}`);
489
514
  }
490
- var ANSI_RE = /\x1B\[[0-9;]*[A-Za-z]|\x1B\][^\x07]*\x07|\r/g;
491
- function stripAnsi(s) {
492
- return s.replace(ANSI_RE, "");
493
- }
494
515
  function isObject(v) {
495
516
  return typeof v === "object" && v !== null && !Array.isArray(v);
496
517
  }
@@ -539,7 +560,7 @@ function shouldSkipStep(stepNumber, name, options2) {
539
560
  const matchByIndex = /^\d+$/.test(options2.stepFilter) && parseInt(options2.stepFilter, 10) === stepNumber;
540
561
  return !matchByIndex && name !== options2.stepFilter;
541
562
  }
542
- return options2.fromStep !== void 0 && stepNumber < options2.fromStep;
563
+ return options2.fromStep !== void 0 && stepNumber < options2.fromStep[0];
543
564
  }
544
565
  async function* runWorkflow(workflow2, options2 = {}) {
545
566
  const workflowStart = Date.now();
@@ -552,8 +573,9 @@ async function* runWorkflow(workflow2, options2 = {}) {
552
573
  }
553
574
  const stepStart = Date.now();
554
575
  yield { type: "step:start", index: i, name: task.name };
576
+ const from = options2.fromStep && options2.fromStep[0] === stepNumber ? options2.fromStep.slice(1) : void 0;
555
577
  try {
556
- for await (const event of runStep(task)) {
578
+ for await (const event of runStep(task, from)) {
557
579
  if (event.type === "step:iteration" || event.type === "step:inner" || event.type === "output:text" || event.type === "output:tool") {
558
580
  yield { ...event, index: i };
559
581
  } else {
@@ -567,7 +589,7 @@ async function* runWorkflow(workflow2, options2 = {}) {
567
589
  durationMs: Date.now() - stepStart
568
590
  };
569
591
  } catch (err) {
570
- const error = err instanceof Error ? err : new Error(String(err));
592
+ const error = normalizeError(err);
571
593
  yield { type: "step:error", index: i, name: task.name, error };
572
594
  if (!task.continueOnError) throw error;
573
595
  }
@@ -578,7 +600,7 @@ async function* runWorkflow(workflow2, options2 = {}) {
578
600
  durationMs: Date.now() - workflowStart
579
601
  };
580
602
  }
581
- async function* runStep(task) {
603
+ async function* runStep(task, from) {
582
604
  switch (task.type) {
583
605
  case "log":
584
606
  yield* runLog(task);
@@ -601,7 +623,7 @@ async function* runStep(task) {
601
623
  break;
602
624
  }
603
625
  case "forEach":
604
- yield* runForEach(task);
626
+ yield* runForEach(task, from);
605
627
  break;
606
628
  default: {
607
629
  const _ = task;
@@ -612,32 +634,48 @@ async function* runStep(task) {
612
634
  async function* runLog(task) {
613
635
  yield { type: "output:text", index: -1, text: task.message };
614
636
  }
615
- async function* runForEach(task) {
637
+ async function* runForEach(task, from) {
616
638
  const items = await resolveItems(task.forEach);
617
639
  const total = items.length;
618
640
  const innerTotal = task.inner.length;
641
+ const startIteration = from?.[0] ?? 1;
642
+ if (startIteration > 1 && startIteration > total) {
643
+ yield {
644
+ type: "log",
645
+ level: "warn",
646
+ text: `[from-step] No iterations to run: target iteration ${startIteration} exceeds total ${total} in "${task.name}"`
647
+ };
648
+ return;
649
+ }
619
650
  for (const [i, item] of items.entries()) {
620
- yield { type: "step:iteration", index: -1, item, iteration: i + 1, total };
651
+ const iteration = i + 1;
652
+ if (iteration < startIteration) continue;
653
+ yield { type: "step:iteration", index: -1, item, iteration, total };
654
+ const iterFrom = iteration === startIteration ? from?.slice(1) : void 0;
655
+ const startChild = iterFrom?.[0] ?? 1;
621
656
  for (const [j, innerTask] of task.inner.entries()) {
657
+ const childIdx = j + 1;
658
+ if (childIdx < startChild) continue;
622
659
  const substituted = substituteItem(innerTask, item);
623
660
  if (innerTotal > 1) {
624
661
  yield {
625
662
  type: "step:inner",
626
663
  index: -1,
627
- iteration: i + 1,
664
+ iteration,
628
665
  innerIndex: j,
629
666
  innerTotal,
630
667
  name: substituted.name
631
668
  };
632
669
  }
670
+ const childFrom = childIdx === startChild ? iterFrom?.slice(1) : void 0;
633
671
  try {
634
- for await (const event of runStep(substituted)) {
672
+ for await (const event of runStep(substituted, childFrom)) {
635
673
  if (event.type !== "step:iteration" && event.type !== "step:inner") {
636
674
  yield event;
637
675
  }
638
676
  }
639
677
  } catch (err) {
640
- const error = err instanceof Error ? err : new Error(String(err));
678
+ const error = normalizeError(err);
641
679
  if (!substituted.continueOnError) {
642
680
  yield {
643
681
  type: "log",
@@ -860,12 +898,6 @@ function buildJudgePrompt(stepName, instructions, output) {
860
898
  OUTPUT: output
861
899
  });
862
900
  }
863
- function formatToolCall(tool, input) {
864
- if (tool === "Edit" || tool === "Write")
865
- return `${tool}(${String(input["file_path"] ?? "")})`;
866
- if (tool === "Bash") return `Bash(${String(input["command"] ?? "")})`;
867
- return tool;
868
- }
869
901
  function buildFixSummary(toolCalls, claudeLines) {
870
902
  if (toolCalls.length > 0) return toolCalls.join(", ");
871
903
  return claudeLines.join(" ").trim() || "No changes made";
@@ -956,10 +988,10 @@ function reducer(state, event) {
956
988
  startTime: Date.now()
957
989
  });
958
990
  case "step:complete": {
959
- const prev = state.tasks[event.index]?.iterationHistory;
960
- const iterationHistory = prev?.length ? prev.map(
961
- (r) => r.status === "running" ? { ...r, status: "complete", endTime: Date.now() } : r
962
- ) : void 0;
991
+ const iterationHistory = finalizeIterations(
992
+ state.tasks[event.index]?.iterationHistory,
993
+ "complete"
994
+ );
963
995
  return {
964
996
  ...updateTask(state, event.index, {
965
997
  status: "complete",
@@ -970,10 +1002,10 @@ function reducer(state, event) {
970
1002
  };
971
1003
  }
972
1004
  case "step:error": {
973
- const prev = state.tasks[event.index]?.iterationHistory;
974
- const iterationHistory = prev?.length ? prev.map(
975
- (r) => r.status === "running" ? { ...r, status: "error", endTime: Date.now() } : r
976
- ) : void 0;
1005
+ const iterationHistory = finalizeIterations(
1006
+ state.tasks[event.index]?.iterationHistory,
1007
+ "error"
1008
+ );
977
1009
  return {
978
1010
  ...updateTask(state, event.index, {
979
1011
  status: "error",
@@ -990,9 +1022,10 @@ function reducer(state, event) {
990
1022
  currentIndex: event.index + 1
991
1023
  };
992
1024
  case "step:iteration": {
993
- const prev = (state.tasks[event.index]?.iterationHistory ?? []).map(
994
- (r) => r.status === "running" ? { ...r, status: "complete", endTime: Date.now() } : r
995
- );
1025
+ const prev = finalizeIterations(
1026
+ state.tasks[event.index]?.iterationHistory,
1027
+ "complete"
1028
+ ) ?? [];
996
1029
  return updateTask(state, event.index, {
997
1030
  iterationHistory: [
998
1031
  ...prev,
@@ -1028,7 +1061,7 @@ function reducer(state, event) {
1028
1061
  const idx = event.index;
1029
1062
  if (idx >= state.tasks.length) return state;
1030
1063
  const formatted = formatToolCall2(event.tool, event.input);
1031
- const next = formatted ? appendLine(state, idx, formatted) : state;
1064
+ const next = formatted ? appendLines(state, idx, formatted) : state;
1032
1065
  if (event.tool === "Write" && typeof event.input["file_path"] === "string") {
1033
1066
  return {
1034
1067
  ...next,
@@ -1055,10 +1088,15 @@ function reducer(state, event) {
1055
1088
  }
1056
1089
  }
1057
1090
  }
1058
- var ANSI_RE2 = /\x1B(?:\[[0-9;?]*[A-Za-z]|\][^\x07]*\x07)|[\r]/g;
1059
1091
  var MAX_LOG_LINES = 300;
1060
1092
  function normalizeLines(text) {
1061
- return text.replace(ANSI_RE2, "").split("\n");
1093
+ return stripAnsi(text).split("\n");
1094
+ }
1095
+ function finalizeIterations(prev, status) {
1096
+ if (!prev?.length) return void 0;
1097
+ return prev.map(
1098
+ (r) => r.status === "running" ? { ...r, status, endTime: Date.now() } : r
1099
+ );
1062
1100
  }
1063
1101
  function updateTask(state, index, patch) {
1064
1102
  const tasks = state.tasks.map(
@@ -1066,9 +1104,6 @@ function updateTask(state, index, patch) {
1066
1104
  );
1067
1105
  return { ...state, tasks };
1068
1106
  }
1069
- function appendLine(state, index, line) {
1070
- return appendLines(state, index, line);
1071
- }
1072
1107
  function appendLines(state, index, text) {
1073
1108
  const newLines = normalizeLines(text);
1074
1109
  const tasks = state.tasks.map((t, i) => {
@@ -1124,6 +1159,9 @@ var STATUS_COLOR = {
1124
1159
  error: theme.error,
1125
1160
  pending: theme.muted
1126
1161
  };
1162
+ function statusIcon(status, tick) {
1163
+ return status === "running" ? SPINNER[tick % SPINNER.length] : STATUS_ICON[status] ?? "\xB7";
1164
+ }
1127
1165
  var EXIT_DELAY_MS = 300;
1128
1166
  function formatHeaderElapsed(start, end) {
1129
1167
  const ms = (end ?? Date.now()) - start;
@@ -1158,9 +1196,6 @@ function TaskRow({ taskState, isActive, index, tick }) {
1158
1196
  ] })
1159
1197
  ] });
1160
1198
  }
1161
- function statusIcon(status, tick) {
1162
- return status === "running" ? SPINNER[tick % SPINNER.length] : STATUS_ICON[status] ?? "\xB7";
1163
- }
1164
1199
  function statusColor(status, isActive) {
1165
1200
  if (isActive && status === "running") return theme.primary;
1166
1201
  return STATUS_COLOR[status] ?? theme.foreground;
@@ -1177,7 +1212,7 @@ function formatIterCount(history) {
1177
1212
  import { Box as Box2, Text as Text2 } from "ink";
1178
1213
  import { Fragment, jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
1179
1214
  function IterationRow({ record, tick }) {
1180
- const icon = record.status === "running" ? SPINNER[tick % SPINNER.length] : STATUS_ICON[record.status] ?? "\xB7";
1215
+ const icon = statusIcon(record.status, tick);
1181
1216
  const color = STATUS_COLOR[record.status] ?? theme.primary;
1182
1217
  const innerText = record.inner ? ` \u2014 ${stripItem(record.inner.name, record.item)} [${record.inner.index + 1}/${record.inner.total}]` : "";
1183
1218
  const ms = (record.endTime ?? Date.now()) - record.startTime;
@@ -2056,17 +2091,6 @@ var INIT_STATE = {
2056
2091
  judgeAttempt: 0,
2057
2092
  recentOutput: []
2058
2093
  };
2059
- var TOOL_SUMMARY = {
2060
- Read: (i) => String(i["file_path"] ?? i["path"] ?? ""),
2061
- Edit: (i) => String(i["file_path"] ?? ""),
2062
- Write: (i) => String(i["file_path"] ?? ""),
2063
- Bash: (i) => String(i["command"] ?? ""),
2064
- Glob: (i) => String(i["pattern"] ?? ""),
2065
- Grep: (i) => String(i["pattern"] ?? "")
2066
- };
2067
- function toolSummary(tool, input) {
2068
- return (TOOL_SUMMARY[tool] ?? ((i) => JSON.stringify(i)))(input);
2069
- }
2070
2094
  function appendLog(logFile, text) {
2071
2095
  if (logFile) appendFileSync(logFile, text + "\n");
2072
2096
  }
@@ -2138,21 +2162,21 @@ Step failed: ${error.message}
2138
2162
  finalizeComplexSequence(s);
2139
2163
  return s;
2140
2164
  }
2141
- function complexSequenceHeader(ctx, s) {
2165
+ function buildHighlightHeader(ctx, s, title, extra = []) {
2142
2166
  return [
2143
- "# Complex Tool Sequence",
2167
+ `# ${title}`,
2144
2168
  "",
2145
2169
  `**Task:** ${ctx.slug}`,
2146
2170
  `**Step:** ${s.stepName}`,
2171
+ ...extra,
2147
2172
  `**Timestamp:** ${(/* @__PURE__ */ new Date()).toISOString()}`,
2148
2173
  "",
2149
2174
  "---",
2150
- "",
2151
- "## Claude's Tool Orchestration",
2152
- "",
2153
- "Claude used multiple tools to complete this step:",
2154
2175
  ""
2155
- ].join("\n");
2176
+ ].join("\n") + "\n";
2177
+ }
2178
+ function complexSequenceHeader(ctx, s) {
2179
+ return buildHighlightHeader(ctx, s, "Complex Tool Sequence") + "## Claude's Tool Orchestration\n\nClaude used multiple tools to complete this step:\n\n";
2156
2180
  }
2157
2181
  function createComplexSequenceFile(ctx, s) {
2158
2182
  const path = highlightPath(ctx, s.stepIndex, "complex_sequence");
@@ -2160,7 +2184,7 @@ function createComplexSequenceFile(ctx, s) {
2160
2184
  return path;
2161
2185
  }
2162
2186
  function onTool(ctx, s, tool, input) {
2163
- const desc = toolSummary(tool, input);
2187
+ const desc = getToolArg(tool, input);
2164
2188
  appendLog(s.logFile, ` [${tool}] ${desc}`);
2165
2189
  const toolCount = s.toolCount + 1;
2166
2190
  const complexSequenceFile = toolCount === 3 ? createComplexSequenceFile(ctx, s) : s.complexSequenceFile;
@@ -2176,23 +2200,9 @@ function onTool(ctx, s, tool, input) {
2176
2200
  function saveJudgeHighlight(ctx, s, verdict, text) {
2177
2201
  writeFileSync3(
2178
2202
  highlightPath(ctx, s.stepIndex, `judge_${verdict}`),
2179
- [
2180
- `# Judge Verdict: ${verdict}`,
2181
- "",
2182
- `**Task:** ${ctx.slug}`,
2183
- `**Step:** ${s.stepName}`,
2184
- `**Attempt:** ${s.judgeAttempt}`,
2185
- `**Timestamp:** ${(/* @__PURE__ */ new Date()).toISOString()}`,
2186
- "",
2187
- "---",
2188
- "",
2189
- text,
2190
- "",
2191
- "---",
2192
- "",
2193
- "*Auto-captured*",
2194
- ""
2195
- ].join("\n")
2203
+ buildHighlightHeader(ctx, s, `Judge Verdict: ${verdict}`, [
2204
+ `**Attempt:** ${s.judgeAttempt}`
2205
+ ]) + [text, "", "---", "", "*Auto-captured*", ""].join("\n")
2196
2206
  );
2197
2207
  }
2198
2208
  var LOG_MATCHERS = [
@@ -2207,19 +2217,11 @@ var LOG_MATCHERS = [
2207
2217
  },
2208
2218
  {
2209
2219
  pattern: /\[self-healing\].*failed.*exit\s+(\d+)/i,
2210
- apply: (ctx, s, text, match) => {
2220
+ apply: (ctx, s, _text, match) => {
2211
2221
  const selfHealingFile = highlightPath(ctx, s.stepIndex, "self_healing");
2212
2222
  writeFileSync3(
2213
2223
  selfHealingFile,
2214
- [
2215
- "# Self-Healing Activation",
2216
- "",
2217
- `**Task:** ${ctx.slug}`,
2218
- `**Step:** ${s.stepName}`,
2219
- `**Timestamp:** ${(/* @__PURE__ */ new Date()).toISOString()}`,
2220
- "",
2221
- "---",
2222
- "",
2224
+ buildHighlightHeader(ctx, s, "Self-Healing Activation") + [
2223
2225
  "## \u274C Failure Detected",
2224
2226
  "",
2225
2227
  `**Exit Code:** ${match[1]}`,
@@ -2266,14 +2268,15 @@ var LOG_MATCHERS = [
2266
2268
  ];
2267
2269
  function onLogMessage(ctx, s, level, text) {
2268
2270
  appendLog(s.logFile, `[${level}] ${text}`);
2269
- return LOG_MATCHERS.reduce(
2270
- ({ matched, state }, { pattern, apply }) => {
2271
- if (matched) return { matched, state };
2272
- const m = pattern.exec(text);
2273
- return m ? { matched: true, state: apply(ctx, state, text, m) } : { matched, state };
2274
- },
2275
- { matched: false, state: s }
2276
- ).state;
2271
+ let state = s;
2272
+ for (const { pattern, apply } of LOG_MATCHERS) {
2273
+ const m = pattern.exec(text);
2274
+ if (m) {
2275
+ state = apply(ctx, state, text, m);
2276
+ break;
2277
+ }
2278
+ }
2279
+ return state;
2277
2280
  }
2278
2281
  function onWorkflowComplete(ctx, s) {
2279
2282
  appendLog(
@@ -2596,7 +2599,7 @@ Version: ${CURRENT_VERSION}
2596
2599
  Options:
2597
2600
  --ci Headless mode \u2014 print events as NDJSON, no TUI
2598
2601
  --step <name|index> Run only the named step or step at 1-based index
2599
- --from-step <n> Skip steps before n and run from there (1-based)
2602
+ --from-step <n> Resume from step n (e.g. 3, 3.2, 2.5.4.3 \u2014 1-based path)
2600
2603
  --help, -h Show this help
2601
2604
 
2602
2605
  Commands:
@@ -2678,11 +2681,15 @@ for (let i = 0; i < rawArgs.length; i++) {
2678
2681
  console.error("--from-step requires a value");
2679
2682
  process.exit(1);
2680
2683
  }
2681
- fromStep = parseInt(rawArgs[++i], 10);
2682
- if (isNaN(fromStep)) {
2683
- console.error("--from-step must be a number");
2684
+ const raw = rawArgs[++i];
2685
+ const parts = raw.split(".").map(Number);
2686
+ if (parts.some(Number.isNaN) || parts.some((p) => p < 1)) {
2687
+ console.error(
2688
+ "--from-step must be N or N.M.K... (all 1-based, e.g. 3 or 3.2 or 2.5.4.3)"
2689
+ );
2684
2690
  process.exit(1);
2685
2691
  }
2692
+ fromStep = parts;
2686
2693
  } else {
2687
2694
  positional.push(a);
2688
2695
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "executant",
3
- "version": "1.10.1",
3
+ "version": "1.12.0",
4
4
  "description": "Harness for YAML-defined workflows that enables stepping through Claude sessions and bash commands",
5
5
  "repository": {
6
6
  "type": "git",