executant 1.10.1 → 1.11.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 +99 -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,40 @@ 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;
619
642
  for (const [i, item] of items.entries()) {
620
- yield { type: "step:iteration", index: -1, item, iteration: i + 1, total };
643
+ const iteration = i + 1;
644
+ if (iteration < startIteration) continue;
645
+ yield { type: "step:iteration", index: -1, item, iteration, total };
646
+ const iterFrom = iteration === startIteration ? from?.slice(1) : void 0;
647
+ const startChild = iterFrom?.[0] ?? 1;
621
648
  for (const [j, innerTask] of task.inner.entries()) {
649
+ const childIdx = j + 1;
650
+ if (childIdx < startChild) continue;
622
651
  const substituted = substituteItem(innerTask, item);
623
652
  if (innerTotal > 1) {
624
653
  yield {
625
654
  type: "step:inner",
626
655
  index: -1,
627
- iteration: i + 1,
656
+ iteration,
628
657
  innerIndex: j,
629
658
  innerTotal,
630
659
  name: substituted.name
631
660
  };
632
661
  }
662
+ const childFrom = childIdx === startChild ? iterFrom?.slice(1) : void 0;
633
663
  try {
634
- for await (const event of runStep(substituted)) {
664
+ for await (const event of runStep(substituted, childFrom)) {
635
665
  if (event.type !== "step:iteration" && event.type !== "step:inner") {
636
666
  yield event;
637
667
  }
638
668
  }
639
669
  } catch (err) {
640
- const error = err instanceof Error ? err : new Error(String(err));
670
+ const error = normalizeError(err);
641
671
  if (!substituted.continueOnError) {
642
672
  yield {
643
673
  type: "log",
@@ -860,12 +890,6 @@ function buildJudgePrompt(stepName, instructions, output) {
860
890
  OUTPUT: output
861
891
  });
862
892
  }
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
893
  function buildFixSummary(toolCalls, claudeLines) {
870
894
  if (toolCalls.length > 0) return toolCalls.join(", ");
871
895
  return claudeLines.join(" ").trim() || "No changes made";
@@ -956,10 +980,10 @@ function reducer(state, event) {
956
980
  startTime: Date.now()
957
981
  });
958
982
  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;
983
+ const iterationHistory = finalizeIterations(
984
+ state.tasks[event.index]?.iterationHistory,
985
+ "complete"
986
+ );
963
987
  return {
964
988
  ...updateTask(state, event.index, {
965
989
  status: "complete",
@@ -970,10 +994,10 @@ function reducer(state, event) {
970
994
  };
971
995
  }
972
996
  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;
997
+ const iterationHistory = finalizeIterations(
998
+ state.tasks[event.index]?.iterationHistory,
999
+ "error"
1000
+ );
977
1001
  return {
978
1002
  ...updateTask(state, event.index, {
979
1003
  status: "error",
@@ -990,9 +1014,10 @@ function reducer(state, event) {
990
1014
  currentIndex: event.index + 1
991
1015
  };
992
1016
  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
- );
1017
+ const prev = finalizeIterations(
1018
+ state.tasks[event.index]?.iterationHistory,
1019
+ "complete"
1020
+ ) ?? [];
996
1021
  return updateTask(state, event.index, {
997
1022
  iterationHistory: [
998
1023
  ...prev,
@@ -1028,7 +1053,7 @@ function reducer(state, event) {
1028
1053
  const idx = event.index;
1029
1054
  if (idx >= state.tasks.length) return state;
1030
1055
  const formatted = formatToolCall2(event.tool, event.input);
1031
- const next = formatted ? appendLine(state, idx, formatted) : state;
1056
+ const next = formatted ? appendLines(state, idx, formatted) : state;
1032
1057
  if (event.tool === "Write" && typeof event.input["file_path"] === "string") {
1033
1058
  return {
1034
1059
  ...next,
@@ -1055,10 +1080,15 @@ function reducer(state, event) {
1055
1080
  }
1056
1081
  }
1057
1082
  }
1058
- var ANSI_RE2 = /\x1B(?:\[[0-9;?]*[A-Za-z]|\][^\x07]*\x07)|[\r]/g;
1059
1083
  var MAX_LOG_LINES = 300;
1060
1084
  function normalizeLines(text) {
1061
- return text.replace(ANSI_RE2, "").split("\n");
1085
+ return stripAnsi(text).split("\n");
1086
+ }
1087
+ function finalizeIterations(prev, status) {
1088
+ if (!prev?.length) return void 0;
1089
+ return prev.map(
1090
+ (r) => r.status === "running" ? { ...r, status, endTime: Date.now() } : r
1091
+ );
1062
1092
  }
1063
1093
  function updateTask(state, index, patch) {
1064
1094
  const tasks = state.tasks.map(
@@ -1066,9 +1096,6 @@ function updateTask(state, index, patch) {
1066
1096
  );
1067
1097
  return { ...state, tasks };
1068
1098
  }
1069
- function appendLine(state, index, line) {
1070
- return appendLines(state, index, line);
1071
- }
1072
1099
  function appendLines(state, index, text) {
1073
1100
  const newLines = normalizeLines(text);
1074
1101
  const tasks = state.tasks.map((t, i) => {
@@ -1124,6 +1151,9 @@ var STATUS_COLOR = {
1124
1151
  error: theme.error,
1125
1152
  pending: theme.muted
1126
1153
  };
1154
+ function statusIcon(status, tick) {
1155
+ return status === "running" ? SPINNER[tick % SPINNER.length] : STATUS_ICON[status] ?? "\xB7";
1156
+ }
1127
1157
  var EXIT_DELAY_MS = 300;
1128
1158
  function formatHeaderElapsed(start, end) {
1129
1159
  const ms = (end ?? Date.now()) - start;
@@ -1158,9 +1188,6 @@ function TaskRow({ taskState, isActive, index, tick }) {
1158
1188
  ] })
1159
1189
  ] });
1160
1190
  }
1161
- function statusIcon(status, tick) {
1162
- return status === "running" ? SPINNER[tick % SPINNER.length] : STATUS_ICON[status] ?? "\xB7";
1163
- }
1164
1191
  function statusColor(status, isActive) {
1165
1192
  if (isActive && status === "running") return theme.primary;
1166
1193
  return STATUS_COLOR[status] ?? theme.foreground;
@@ -1177,7 +1204,7 @@ function formatIterCount(history) {
1177
1204
  import { Box as Box2, Text as Text2 } from "ink";
1178
1205
  import { Fragment, jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
1179
1206
  function IterationRow({ record, tick }) {
1180
- const icon = record.status === "running" ? SPINNER[tick % SPINNER.length] : STATUS_ICON[record.status] ?? "\xB7";
1207
+ const icon = statusIcon(record.status, tick);
1181
1208
  const color = STATUS_COLOR[record.status] ?? theme.primary;
1182
1209
  const innerText = record.inner ? ` \u2014 ${stripItem(record.inner.name, record.item)} [${record.inner.index + 1}/${record.inner.total}]` : "";
1183
1210
  const ms = (record.endTime ?? Date.now()) - record.startTime;
@@ -2056,17 +2083,6 @@ var INIT_STATE = {
2056
2083
  judgeAttempt: 0,
2057
2084
  recentOutput: []
2058
2085
  };
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
2086
  function appendLog(logFile, text) {
2071
2087
  if (logFile) appendFileSync(logFile, text + "\n");
2072
2088
  }
@@ -2138,21 +2154,21 @@ Step failed: ${error.message}
2138
2154
  finalizeComplexSequence(s);
2139
2155
  return s;
2140
2156
  }
2141
- function complexSequenceHeader(ctx, s) {
2157
+ function buildHighlightHeader(ctx, s, title, extra = []) {
2142
2158
  return [
2143
- "# Complex Tool Sequence",
2159
+ `# ${title}`,
2144
2160
  "",
2145
2161
  `**Task:** ${ctx.slug}`,
2146
2162
  `**Step:** ${s.stepName}`,
2163
+ ...extra,
2147
2164
  `**Timestamp:** ${(/* @__PURE__ */ new Date()).toISOString()}`,
2148
2165
  "",
2149
2166
  "---",
2150
- "",
2151
- "## Claude's Tool Orchestration",
2152
- "",
2153
- "Claude used multiple tools to complete this step:",
2154
2167
  ""
2155
- ].join("\n");
2168
+ ].join("\n") + "\n";
2169
+ }
2170
+ function complexSequenceHeader(ctx, s) {
2171
+ return buildHighlightHeader(ctx, s, "Complex Tool Sequence") + "## Claude's Tool Orchestration\n\nClaude used multiple tools to complete this step:\n\n";
2156
2172
  }
2157
2173
  function createComplexSequenceFile(ctx, s) {
2158
2174
  const path = highlightPath(ctx, s.stepIndex, "complex_sequence");
@@ -2160,7 +2176,7 @@ function createComplexSequenceFile(ctx, s) {
2160
2176
  return path;
2161
2177
  }
2162
2178
  function onTool(ctx, s, tool, input) {
2163
- const desc = toolSummary(tool, input);
2179
+ const desc = getToolArg(tool, input);
2164
2180
  appendLog(s.logFile, ` [${tool}] ${desc}`);
2165
2181
  const toolCount = s.toolCount + 1;
2166
2182
  const complexSequenceFile = toolCount === 3 ? createComplexSequenceFile(ctx, s) : s.complexSequenceFile;
@@ -2176,23 +2192,9 @@ function onTool(ctx, s, tool, input) {
2176
2192
  function saveJudgeHighlight(ctx, s, verdict, text) {
2177
2193
  writeFileSync3(
2178
2194
  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")
2195
+ buildHighlightHeader(ctx, s, `Judge Verdict: ${verdict}`, [
2196
+ `**Attempt:** ${s.judgeAttempt}`
2197
+ ]) + [text, "", "---", "", "*Auto-captured*", ""].join("\n")
2196
2198
  );
2197
2199
  }
2198
2200
  var LOG_MATCHERS = [
@@ -2207,19 +2209,11 @@ var LOG_MATCHERS = [
2207
2209
  },
2208
2210
  {
2209
2211
  pattern: /\[self-healing\].*failed.*exit\s+(\d+)/i,
2210
- apply: (ctx, s, text, match) => {
2212
+ apply: (ctx, s, _text, match) => {
2211
2213
  const selfHealingFile = highlightPath(ctx, s.stepIndex, "self_healing");
2212
2214
  writeFileSync3(
2213
2215
  selfHealingFile,
2214
- [
2215
- "# Self-Healing Activation",
2216
- "",
2217
- `**Task:** ${ctx.slug}`,
2218
- `**Step:** ${s.stepName}`,
2219
- `**Timestamp:** ${(/* @__PURE__ */ new Date()).toISOString()}`,
2220
- "",
2221
- "---",
2222
- "",
2216
+ buildHighlightHeader(ctx, s, "Self-Healing Activation") + [
2223
2217
  "## \u274C Failure Detected",
2224
2218
  "",
2225
2219
  `**Exit Code:** ${match[1]}`,
@@ -2266,14 +2260,15 @@ var LOG_MATCHERS = [
2266
2260
  ];
2267
2261
  function onLogMessage(ctx, s, level, text) {
2268
2262
  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;
2263
+ let state = s;
2264
+ for (const { pattern, apply } of LOG_MATCHERS) {
2265
+ const m = pattern.exec(text);
2266
+ if (m) {
2267
+ state = apply(ctx, state, text, m);
2268
+ break;
2269
+ }
2270
+ }
2271
+ return state;
2277
2272
  }
2278
2273
  function onWorkflowComplete(ctx, s) {
2279
2274
  appendLog(
@@ -2596,7 +2591,7 @@ Version: ${CURRENT_VERSION}
2596
2591
  Options:
2597
2592
  --ci Headless mode \u2014 print events as NDJSON, no TUI
2598
2593
  --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)
2594
+ --from-step <n> Resume from step n (e.g. 3, 3.2, 2.5.4.3 \u2014 1-based path)
2600
2595
  --help, -h Show this help
2601
2596
 
2602
2597
  Commands:
@@ -2678,11 +2673,15 @@ for (let i = 0; i < rawArgs.length; i++) {
2678
2673
  console.error("--from-step requires a value");
2679
2674
  process.exit(1);
2680
2675
  }
2681
- fromStep = parseInt(rawArgs[++i], 10);
2682
- if (isNaN(fromStep)) {
2683
- console.error("--from-step must be a number");
2676
+ const raw = rawArgs[++i];
2677
+ const parts = raw.split(".").map(Number);
2678
+ if (parts.some(Number.isNaN) || parts.some((p) => p < 1)) {
2679
+ console.error(
2680
+ "--from-step must be N or N.M.K... (all 1-based, e.g. 3 or 3.2 or 2.5.4.3)"
2681
+ );
2684
2682
  process.exit(1);
2685
2683
  }
2684
+ fromStep = parts;
2686
2685
  } else {
2687
2686
  positional.push(a);
2688
2687
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "executant",
3
- "version": "1.10.1",
3
+ "version": "1.11.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",