executant 1.8.0 → 1.10.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
@@ -156,6 +156,15 @@ function loadWorkflow(filePath2) {
156
156
  ${detail}`);
157
157
  }
158
158
  const vars = doc.vars ?? {};
159
+ const seen = /* @__PURE__ */ new Set();
160
+ for (const step of doc.steps) {
161
+ if (seen.has(step.name)) {
162
+ throw new Error(
163
+ `Duplicate step name "${step.name}" \u2014 step names must be unique within a workflow`
164
+ );
165
+ }
166
+ seen.add(step.name);
167
+ }
159
168
  return {
160
169
  goal: doc.goal,
161
170
  vars,
@@ -370,25 +379,12 @@ async function* runCommand(task) {
370
379
  // src/tasks/claude.ts
371
380
  import { execSync, spawn as spawn2 } from "node:child_process";
372
381
  import { zodToJsonSchema } from "zod-to-json-schema";
382
+ var METHODOLOGY = loadPrompt("development-methodology");
373
383
  var DEFAULT_TOOLS = ["Read", "Edit", "Write", "Bash", "Glob", "Grep"];
374
- function resolveClaudePath() {
375
- try {
376
- return execSync("which claude", { env: process.env }).toString().trim();
377
- } catch {
378
- throw new Error(
379
- "claude CLI not found. Ensure it is installed and in PATH.\n brew install claude OR npm install -g @anthropic-ai/claude-code"
380
- );
381
- }
382
- }
383
- async function* runClaude(task) {
384
+ function buildClaudeArgs(task) {
384
385
  const allowedTools = task.allowedTools ?? DEFAULT_TOOLS;
385
- yield {
386
- type: "log",
387
- level: "info",
388
- text: `claude -p "${task.prompt.slice(0, 60).replace(/\n/g, " ")}\u2026"`
389
- };
390
386
  const permissionMode = task.permissionMode ?? "bypassPermissions";
391
- const args = [
387
+ return [
392
388
  "--print",
393
389
  task.prompt,
394
390
  "--output-format",
@@ -402,6 +398,23 @@ async function* runClaude(task) {
402
398
  ...task.appendSystemPrompt ? ["--append-system-prompt", task.appendSystemPrompt] : [],
403
399
  ...task.jsonSchema ? ["--json-schema", JSON.stringify(task.jsonSchema)] : []
404
400
  ];
401
+ }
402
+ function resolveClaudePath() {
403
+ try {
404
+ return execSync("which claude", { env: process.env }).toString().trim();
405
+ } catch {
406
+ throw new Error(
407
+ "claude CLI not found. Ensure it is installed and in PATH.\n brew install claude OR npm install -g @anthropic-ai/claude-code"
408
+ );
409
+ }
410
+ }
411
+ async function* runClaude(task) {
412
+ yield {
413
+ type: "log",
414
+ level: "info",
415
+ text: `claude -p "${task.prompt.slice(0, 60).replace(/\n/g, " ")}\u2026"`
416
+ };
417
+ const args = buildClaudeArgs(task);
405
418
  const claudeBin = resolveClaudePath();
406
419
  let proc;
407
420
  try {
@@ -410,7 +423,9 @@ async function* runClaude(task) {
410
423
  env: { ...process.env }
411
424
  });
412
425
  } catch (err) {
413
- throw new Error(`Failed to spawn claude (${claudeBin}): ${getErrorMessage(err)}`);
426
+ throw new Error(
427
+ `Failed to spawn claude (${claudeBin}): ${getErrorMessage(err)}`
428
+ );
414
429
  }
415
430
  const cleanup = () => {
416
431
  try {
@@ -480,7 +495,10 @@ function isObject(v) {
480
495
  return typeof v === "object" && v !== null && !Array.isArray(v);
481
496
  }
482
497
  function getArray(obj, ...keys) {
483
- const result = keys.reduce((cur, k) => isObject(cur) ? cur[k] : null, obj);
498
+ const result = keys.reduce(
499
+ (cur, k) => isObject(cur) ? cur[k] : null,
500
+ obj
501
+ );
484
502
  return Array.isArray(result) ? result : [];
485
503
  }
486
504
  function getString(obj, key) {
@@ -496,7 +514,9 @@ async function runClaudeStructured(task, schema) {
496
514
  else if (event.type === "output:text") lines.push(event.text);
497
515
  }
498
516
  if (structuredOutput === void 0 && process.env["NODE_ENV"] !== "test") {
499
- console.warn("[executant] runClaudeStructured: no output:structured event \u2014 falling back to text parsing");
517
+ console.warn(
518
+ "[executant] runClaudeStructured: no output:structured event \u2014 falling back to text parsing"
519
+ );
500
520
  }
501
521
  const data = structuredOutput ?? JSON.parse(extractJsonObject(lines.join("").trim()));
502
522
  return schema.parse(data);
@@ -611,7 +631,11 @@ async function* runForEach(task) {
611
631
  };
612
632
  }
613
633
  try {
614
- yield* runStep(substituted);
634
+ for await (const event of runStep(substituted)) {
635
+ if (event.type !== "step:iteration" && event.type !== "step:inner") {
636
+ yield event;
637
+ }
638
+ }
615
639
  } catch (err) {
616
640
  const error = err instanceof Error ? err : new Error(String(err));
617
641
  if (!substituted.continueOnError) {
@@ -863,7 +887,7 @@ init_update();
863
887
 
864
888
  // src/ui/App.tsx
865
889
  import { useEffect as useEffect2, useReducer, useState } from "react";
866
- import { Box as Box4, Text as Text4, useApp, useStdin } from "ink";
890
+ import { Box as Box5, Text as Text5, useApp, useStdin } from "ink";
867
891
 
868
892
  // src/ui/KeyboardHandler.tsx
869
893
  import { useInput } from "ink";
@@ -931,45 +955,70 @@ function reducer(state, event) {
931
955
  status: "running",
932
956
  startTime: Date.now()
933
957
  });
934
- case "step:complete":
958
+ 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;
935
963
  return {
936
964
  ...updateTask(state, event.index, {
937
965
  status: "complete",
938
- endTime: Date.now()
966
+ endTime: Date.now(),
967
+ ...iterationHistory ? { iterationHistory } : {}
939
968
  }),
940
969
  currentIndex: event.index + 1
941
970
  };
942
- case "step:error":
971
+ }
972
+ 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;
943
977
  return {
944
978
  ...updateTask(state, event.index, {
945
979
  status: "error",
946
980
  endTime: Date.now(),
947
- error: event.error
981
+ error: event.error,
982
+ ...iterationHistory ? { iterationHistory } : {}
948
983
  }),
949
984
  currentIndex: event.index + 1
950
985
  };
986
+ }
951
987
  case "step:skip":
952
988
  return {
953
989
  ...updateTask(state, event.index, { status: "skipped" }),
954
990
  currentIndex: event.index + 1
955
991
  };
956
- case "step:iteration":
957
- return updateTask(state, event.index, {
958
- iteration: {
959
- current: event.iteration,
960
- total: event.total,
961
- item: event.item
962
- },
963
- inner: void 0
964
- });
965
- case "step:inner":
992
+ 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
+ );
966
996
  return updateTask(state, event.index, {
967
- inner: {
968
- index: event.innerIndex,
969
- total: event.innerTotal,
970
- name: event.name
971
- }
997
+ iterationHistory: [
998
+ ...prev,
999
+ {
1000
+ item: event.item,
1001
+ iteration: event.iteration,
1002
+ total: event.total,
1003
+ status: "running",
1004
+ startTime: Date.now()
1005
+ }
1006
+ ]
972
1007
  });
1008
+ }
1009
+ case "step:inner": {
1010
+ const iterationHistory = (state.tasks[event.index]?.iterationHistory ?? []).map(
1011
+ (r) => r.status === "running" ? {
1012
+ ...r,
1013
+ inner: {
1014
+ index: event.innerIndex,
1015
+ total: event.innerTotal,
1016
+ name: event.name
1017
+ }
1018
+ } : r
1019
+ );
1020
+ return updateTask(state, event.index, { iterationHistory });
1021
+ }
973
1022
  case "output:text": {
974
1023
  const idx = event.index;
975
1024
  if (idx >= state.tasks.length) return state;
@@ -1024,20 +1073,6 @@ function appendLine(state, index, line) {
1024
1073
  // src/ui/TaskRow.tsx
1025
1074
  import { Box, Text } from "ink";
1026
1075
 
1027
- // src/ui/utils.ts
1028
- var SPINNER = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
1029
- var EXIT_DELAY_MS = 300;
1030
- function formatHeaderElapsed(start, end) {
1031
- const ms = (end ?? Date.now()) - start;
1032
- return ms < 1e3 ? `${ms}ms` : `${(ms / 1e3).toFixed(1)}s`;
1033
- }
1034
- function formatTaskElapsed(start, end, status) {
1035
- if (!start) return "";
1036
- const ms = (end ?? Date.now()) - start;
1037
- if (status === "running" || status === "complete" || status === "error") return `${(ms / 1e3).toFixed(1)}s`;
1038
- return "";
1039
- }
1040
-
1041
1076
  // src/ui/theme.ts
1042
1077
  import { createRequire } from "node:module";
1043
1078
  import { oklchToHex } from "@coston/design-tokens";
@@ -1066,6 +1101,32 @@ var theme = {
1066
1101
  // log pane border
1067
1102
  };
1068
1103
 
1104
+ // src/ui/utils.ts
1105
+ var SPINNER = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
1106
+ var STATUS_ICON = {
1107
+ complete: "\u2713",
1108
+ error: "\u2717",
1109
+ skipped: "\u2298",
1110
+ pending: "\xB7"
1111
+ };
1112
+ var STATUS_COLOR = {
1113
+ complete: theme.success,
1114
+ error: theme.error,
1115
+ pending: theme.muted
1116
+ };
1117
+ var EXIT_DELAY_MS = 300;
1118
+ function formatHeaderElapsed(start, end) {
1119
+ const ms = (end ?? Date.now()) - start;
1120
+ return ms < 1e3 ? `${ms}ms` : `${(ms / 1e3).toFixed(1)}s`;
1121
+ }
1122
+ function formatTaskElapsed(start, end, status) {
1123
+ if (!start) return "";
1124
+ const ms = (end ?? Date.now()) - start;
1125
+ if (status === "running" || status === "complete" || status === "error")
1126
+ return `${(ms / 1e3).toFixed(1)}s`;
1127
+ return "";
1128
+ }
1129
+
1069
1130
  // src/ui/TaskRow.tsx
1070
1131
  import { jsx, jsxs } from "react/jsx-runtime";
1071
1132
  function TaskRow({ taskState, isActive, index, tick }) {
@@ -1073,9 +1134,8 @@ function TaskRow({ taskState, isActive, index, tick }) {
1073
1134
  const icon = statusIcon(status, tick);
1074
1135
  const color = statusColor(status, isActive);
1075
1136
  const elapsed = formatTaskElapsed(startTime, endTime, status);
1076
- const iterInfo = taskState.iteration ? ` (${taskState.iteration.current}/${taskState.iteration.total}) ${taskState.iteration.item}` : "";
1077
- const innerInfo = taskState.inner ? ` \u2014 ${taskState.inner.name} [${taskState.inner.index + 1}/${taskState.inner.total}]` : "";
1078
- const label = `${index + 1}. ${task.name}${iterInfo}${innerInfo}`;
1137
+ const iterInfo = formatIterCount(taskState.iterationHistory);
1138
+ const label = `${index + 1}. ${task.name}${iterInfo}`;
1079
1139
  return /* @__PURE__ */ jsxs(Box, { children: [
1080
1140
  /* @__PURE__ */ jsxs(Text, { color, children: [
1081
1141
  icon,
@@ -1088,17 +1148,6 @@ function TaskRow({ taskState, isActive, index, tick }) {
1088
1148
  ] })
1089
1149
  ] });
1090
1150
  }
1091
- var STATUS_ICON = {
1092
- complete: "\u2713",
1093
- error: "\u2717",
1094
- skipped: "\u2298",
1095
- pending: "\xB7"
1096
- };
1097
- var STATUS_COLOR = {
1098
- complete: theme.success,
1099
- error: theme.error,
1100
- pending: theme.muted
1101
- };
1102
1151
  function statusIcon(status, tick) {
1103
1152
  return status === "running" ? SPINNER[tick % SPINNER.length] : STATUS_ICON[status] ?? "\xB7";
1104
1153
  }
@@ -1106,16 +1155,74 @@ function statusColor(status, isActive) {
1106
1155
  if (isActive && status === "running") return theme.primary;
1107
1156
  return STATUS_COLOR[status] ?? theme.foreground;
1108
1157
  }
1158
+ function formatIterCount(history) {
1159
+ if (!history?.length) return "";
1160
+ const total = history[0].total;
1161
+ const running = history.find((r) => r.status === "running");
1162
+ const current = running?.iteration ?? history.length;
1163
+ return ` (${current}/${total})`;
1164
+ }
1109
1165
 
1110
- // src/ui/LogPane.tsx
1166
+ // src/ui/IterationRow.tsx
1111
1167
  import { Box as Box2, Text as Text2 } from "ink";
1112
- import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
1168
+ import { Fragment, jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
1169
+ function IterationRow({ record, tick }) {
1170
+ const icon = record.status === "running" ? SPINNER[tick % SPINNER.length] : STATUS_ICON[record.status] ?? "\xB7";
1171
+ const color = STATUS_COLOR[record.status] ?? theme.primary;
1172
+ const innerText = record.inner ? ` \u2014 ${stripItem(record.inner.name, record.item)} [${record.inner.index + 1}/${record.inner.total}]` : "";
1173
+ const ms = (record.endTime ?? Date.now()) - record.startTime;
1174
+ const elapsed = `${(ms / 1e3).toFixed(1)}s`;
1175
+ return /* @__PURE__ */ jsxs2(Box2, { children: [
1176
+ /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: " " }),
1177
+ /* @__PURE__ */ jsx2(Text2, { color, children: icon }),
1178
+ /* @__PURE__ */ jsx2(Text2, { children: " " }),
1179
+ /* @__PURE__ */ jsxs2(
1180
+ Text2,
1181
+ {
1182
+ color: record.status === "running" ? theme.foreground : theme.muted,
1183
+ children: [
1184
+ record.item,
1185
+ innerText
1186
+ ]
1187
+ }
1188
+ ),
1189
+ /* @__PURE__ */ jsxs2(Text2, { dimColor: true, children: [
1190
+ " ",
1191
+ elapsed
1192
+ ] })
1193
+ ] });
1194
+ }
1195
+ function stripItem(name, item) {
1196
+ if (!name.includes(item)) return name;
1197
+ const stripped = name.replace(item, "").replace(/\s{2,}/g, " ").replace(/^[\s\-—–]+/, "").replace(/[\s\-—–]+$/, "").trim();
1198
+ return stripped || name;
1199
+ }
1200
+ function isRepeatStyle(history) {
1201
+ return history.every((r) => r.item === String(r.iteration));
1202
+ }
1203
+ function IterationList({
1204
+ iterationHistory,
1205
+ tick,
1206
+ maxVisible
1207
+ }) {
1208
+ if (isRepeatStyle(iterationHistory)) return null;
1209
+ const hidden = iterationHistory.length - maxVisible;
1210
+ const visible = iterationHistory.slice(-maxVisible);
1211
+ return /* @__PURE__ */ jsxs2(Fragment, { children: [
1212
+ hidden > 0 && /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: ` \u2026 ${hidden} earlier` }),
1213
+ visible.map((record) => /* @__PURE__ */ jsx2(IterationRow, { record, tick }, record.iteration))
1214
+ ] });
1215
+ }
1216
+
1217
+ // src/ui/LogPane.tsx
1218
+ import { Box as Box3, Text as Text3 } from "ink";
1219
+ import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
1113
1220
  function LogPane({ lines, isActive = false, maxLines = 15 }) {
1114
1221
  const visible = lines.slice(-maxLines);
1115
1222
  if (visible.length === 0) {
1116
- return /* @__PURE__ */ jsx2(Box2, { marginTop: 1, children: /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: isActive ? "\u2838 waiting for output\u2026" : "\u2014 no output yet \u2014" }) });
1223
+ return /* @__PURE__ */ jsx3(Box3, { marginTop: 1, children: /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: isActive ? "\u2838 waiting for output\u2026" : "\u2014 no output yet \u2014" }) });
1117
1224
  }
1118
- return /* @__PURE__ */ jsx2(Box2, { flexDirection: "column", marginTop: 1, borderStyle: "single", borderColor: theme.border, paddingX: 1, children: visible.map((line, i) => /* @__PURE__ */ jsx2(
1225
+ return /* @__PURE__ */ jsx3(Box3, { flexDirection: "column", marginTop: 1, borderStyle: "single", borderColor: theme.border, paddingX: 1, children: visible.map((line, i) => /* @__PURE__ */ jsx3(
1119
1226
  LogLine,
1120
1227
  {
1121
1228
  text: line,
@@ -1125,29 +1232,29 @@ function LogPane({ lines, isActive = false, maxLines = 15 }) {
1125
1232
  )) });
1126
1233
  }
1127
1234
  function LogLine({ text, cursor }) {
1128
- const suffix = cursor ? /* @__PURE__ */ jsx2(Text2, { color: theme.primary, children: " \u258C" }) : null;
1235
+ const suffix = cursor ? /* @__PURE__ */ jsx3(Text3, { color: theme.primary, children: " \u258C" }) : null;
1129
1236
  if (/^\[[\w:]+\]/.test(text)) {
1130
1237
  const bracket = text.match(/^\[[\w:]+\]/)?.[0] ?? "";
1131
1238
  const rest = text.slice(bracket.length);
1132
- return /* @__PURE__ */ jsxs2(Text2, { children: [
1133
- /* @__PURE__ */ jsx2(Text2, { color: theme.primary, children: bracket }),
1134
- /* @__PURE__ */ jsx2(Text2, { children: rest }),
1239
+ return /* @__PURE__ */ jsxs3(Text3, { children: [
1240
+ /* @__PURE__ */ jsx3(Text3, { color: theme.primary, children: bracket }),
1241
+ /* @__PURE__ */ jsx3(Text3, { children: rest }),
1135
1242
  suffix
1136
1243
  ] });
1137
1244
  }
1138
- if (/^\s*\$\s/.test(text)) return /* @__PURE__ */ jsxs2(Text2, { color: theme.warning, children: [
1245
+ if (/^\s*\$\s/.test(text)) return /* @__PURE__ */ jsxs3(Text3, { color: theme.warning, children: [
1139
1246
  text,
1140
1247
  suffix
1141
1248
  ] });
1142
- if (text.startsWith("[warn]")) return /* @__PURE__ */ jsxs2(Text2, { color: theme.warning, children: [
1249
+ if (text.startsWith("[warn]")) return /* @__PURE__ */ jsxs3(Text3, { color: theme.warning, children: [
1143
1250
  text,
1144
1251
  suffix
1145
1252
  ] });
1146
- if (text.startsWith("[error]")) return /* @__PURE__ */ jsxs2(Text2, { color: theme.error, children: [
1253
+ if (text.startsWith("[error]")) return /* @__PURE__ */ jsxs3(Text3, { color: theme.error, children: [
1147
1254
  text,
1148
1255
  suffix
1149
1256
  ] });
1150
- return /* @__PURE__ */ jsxs2(Text2, { children: [
1257
+ return /* @__PURE__ */ jsxs3(Text3, { children: [
1151
1258
  text,
1152
1259
  suffix
1153
1260
  ] });
@@ -1167,8 +1274,8 @@ function useInterval(callback, delayMs) {
1167
1274
  }
1168
1275
 
1169
1276
  // src/ui/BrandMark.tsx
1170
- import { Box as Box3, Text as Text3 } from "ink";
1171
- import { jsx as jsx3 } from "react/jsx-runtime";
1277
+ import { Box as Box4, Text as Text4 } from "ink";
1278
+ import { jsx as jsx4 } from "react/jsx-runtime";
1172
1279
  var BRAND = "Executant";
1173
1280
  var SWEEP_TICKS = BRAND.length * 2;
1174
1281
  var GAP_TICKS = 30;
@@ -1181,11 +1288,12 @@ function charColor(charIndex, tick, isActive) {
1181
1288
  return charIndex === charPos ? theme.primaryLight : theme.primary;
1182
1289
  }
1183
1290
  function BrandMark({ tick, isActive }) {
1184
- return /* @__PURE__ */ jsx3(Box3, { children: [...BRAND].map((char, i) => /* @__PURE__ */ jsx3(Text3, { color: charColor(i, tick, isActive), bold: true, children: char }, i)) });
1291
+ return /* @__PURE__ */ jsx4(Box4, { children: [...BRAND].map((char, i) => /* @__PURE__ */ jsx4(Text4, { color: charColor(i, tick, isActive), bold: true, children: char }, i)) });
1185
1292
  }
1186
1293
 
1187
1294
  // src/ui/App.tsx
1188
- import { jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
1295
+ import { jsx as jsx5, jsxs as jsxs4 } from "react/jsx-runtime";
1296
+ var MAX_VISIBLE_ITERATIONS = 8;
1189
1297
  function App({ workflow: workflow2, events: events2, options: options2, updateCheck: updateCheck2 }) {
1190
1298
  const { exit } = useApp();
1191
1299
  const [state, dispatch] = useReducer(reducer, buildInitialState(workflow2));
@@ -1203,7 +1311,10 @@ function App({ workflow: workflow2, events: events2, options: options2, updateCh
1203
1311
  } catch (err) {
1204
1312
  if (!active) return;
1205
1313
  dispatch({ type: "log", level: "error", text: getErrorMessage(err) });
1206
- setTimeout(() => exit(err instanceof Error ? err : new Error(getErrorMessage(err))), EXIT_DELAY_MS);
1314
+ setTimeout(
1315
+ () => exit(err instanceof Error ? err : new Error(getErrorMessage(err))),
1316
+ EXIT_DELAY_MS
1317
+ );
1207
1318
  }
1208
1319
  })();
1209
1320
  return () => {
@@ -1223,14 +1334,16 @@ function App({ workflow: workflow2, events: events2, options: options2, updateCh
1223
1334
  }, [updateCheck2]);
1224
1335
  const elapsed = formatHeaderElapsed(state.startTime, state.endTime);
1225
1336
  const activeTask = state.tasks[state.currentIndex];
1226
- const completedCount = state.tasks.filter((t) => t.status === "complete").length;
1337
+ const completedCount = state.tasks.filter(
1338
+ (t) => t.status === "complete"
1339
+ ).length;
1227
1340
  const totalCount = state.tasks.length;
1228
1341
  const filterInfo = options2?.stepFilter ? ` [step: ${options2.stepFilter}]` : options2?.fromStep ? ` [from step: ${options2.fromStep}]` : "";
1229
- return /* @__PURE__ */ jsxs3(Box4, { flexDirection: "column", padding: 1, children: [
1230
- /* @__PURE__ */ jsx4(Box4, { marginBottom: 1, children: /* @__PURE__ */ jsx4(BrandMark, { tick, isActive: !state.endTime }) }),
1231
- /* @__PURE__ */ jsxs3(Box4, { marginBottom: 1, children: [
1232
- /* @__PURE__ */ jsx4(Text4, { bold: true, color: theme.primary, children: workflow2.goal }),
1233
- /* @__PURE__ */ jsxs3(Text4, { dimColor: true, children: [
1342
+ return /* @__PURE__ */ jsxs4(Box5, { flexDirection: "column", padding: 1, children: [
1343
+ /* @__PURE__ */ jsx5(Box5, { marginBottom: 1, children: /* @__PURE__ */ jsx5(BrandMark, { tick, isActive: !state.endTime }) }),
1344
+ /* @__PURE__ */ jsxs4(Box5, { marginBottom: 1, children: [
1345
+ /* @__PURE__ */ jsx5(Text5, { bold: true, color: theme.primary, children: workflow2.goal }),
1346
+ /* @__PURE__ */ jsxs4(Text5, { dimColor: true, children: [
1234
1347
  " ",
1235
1348
  completedCount,
1236
1349
  "/",
@@ -1240,33 +1353,48 @@ function App({ workflow: workflow2, events: events2, options: options2, updateCh
1240
1353
  filterInfo
1241
1354
  ] })
1242
1355
  ] }),
1243
- /* @__PURE__ */ jsx4(Box4, { flexDirection: "column", marginBottom: 1, children: state.tasks.map((taskState, i) => /* @__PURE__ */ jsx4(
1244
- TaskRow,
1356
+ /* @__PURE__ */ jsx5(Box5, { flexDirection: "column", marginBottom: 1, children: state.tasks.map((taskState, i) => /* @__PURE__ */ jsxs4(Box5, { flexDirection: "column", children: [
1357
+ /* @__PURE__ */ jsx5(
1358
+ TaskRow,
1359
+ {
1360
+ index: i,
1361
+ tick,
1362
+ taskState,
1363
+ isActive: i === state.currentIndex
1364
+ }
1365
+ ),
1366
+ taskState.status === "running" && taskState.iterationHistory?.length ? /* @__PURE__ */ jsx5(
1367
+ IterationList,
1368
+ {
1369
+ iterationHistory: taskState.iterationHistory,
1370
+ tick,
1371
+ maxVisible: MAX_VISIBLE_ITERATIONS
1372
+ }
1373
+ ) : null
1374
+ ] }, i)) }),
1375
+ activeTask && /* @__PURE__ */ jsx5(
1376
+ LogPane,
1245
1377
  {
1246
- index: i,
1247
- tick,
1248
- taskState,
1249
- isActive: i === state.currentIndex
1250
- },
1251
- taskState.task.name
1252
- )) }),
1253
- activeTask && /* @__PURE__ */ jsx4(LogPane, { lines: activeTask.lines, isActive: activeTask.status === "running" }),
1254
- state.endTime !== void 0 && state.writtenFiles.length > 0 && /* @__PURE__ */ jsxs3(Box4, { flexDirection: "column", marginTop: 1, children: [
1255
- /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: "files written:" }),
1256
- state.writtenFiles.map((f) => /* @__PURE__ */ jsxs3(Text4, { color: theme.primary, children: [
1378
+ lines: activeTask.lines,
1379
+ isActive: activeTask.status === "running"
1380
+ }
1381
+ ),
1382
+ state.endTime !== void 0 && state.writtenFiles.length > 0 && /* @__PURE__ */ jsxs4(Box5, { flexDirection: "column", marginTop: 1, children: [
1383
+ /* @__PURE__ */ jsx5(Text5, { dimColor: true, children: "files written:" }),
1384
+ state.writtenFiles.map((f) => /* @__PURE__ */ jsxs4(Text5, { color: theme.primary, children: [
1257
1385
  " ",
1258
1386
  f
1259
1387
  ] }, f))
1260
1388
  ] }),
1261
- /* @__PURE__ */ jsxs3(Box4, { marginTop: 1, flexDirection: "column", children: [
1262
- updateVersion && /* @__PURE__ */ jsxs3(Text4, { color: theme.warning, children: [
1389
+ /* @__PURE__ */ jsxs4(Box5, { marginTop: 1, flexDirection: "column", children: [
1390
+ updateVersion && /* @__PURE__ */ jsxs4(Text5, { color: theme.warning, children: [
1263
1391
  "v",
1264
1392
  updateVersion,
1265
1393
  " available \u2014 run: executant update"
1266
1394
  ] }),
1267
- /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: "press q to quit" })
1395
+ /* @__PURE__ */ jsx5(Text5, { dimColor: true, children: "press q to quit" })
1268
1396
  ] }),
1269
- isRawModeSupported && /* @__PURE__ */ jsx4(KeyboardHandler, { onExit: exit })
1397
+ isRawModeSupported && /* @__PURE__ */ jsx5(KeyboardHandler, { onExit: exit })
1270
1398
  ] });
1271
1399
  }
1272
1400
 
@@ -1404,7 +1532,8 @@ async function runPass3Judge(description, workflow2) {
1404
1532
  }),
1405
1533
  allowedTools: [],
1406
1534
  permissionMode: "default",
1407
- model: "sonnet"
1535
+ model: "sonnet",
1536
+ appendSystemPrompt: METHODOLOGY
1408
1537
  };
1409
1538
  return await runClaudeStructured(task, PlanJudgeOutputSchema);
1410
1539
  } catch {
@@ -1512,7 +1641,8 @@ async function* streamPlan(args) {
1512
1641
  }),
1513
1642
  allowedTools: ["Read", "Glob", "Grep"],
1514
1643
  permissionMode: "bypassPermissions",
1515
- model: "opus"
1644
+ model: "opus",
1645
+ appendSystemPrompt: METHODOLOGY
1516
1646
  };
1517
1647
  for await (const event of runClaude(researchTask)) {
1518
1648
  if (event.type === "output:tool") {
@@ -1574,7 +1704,9 @@ ${basePrompt}` : basePrompt,
1574
1704
  allowedTools: [],
1575
1705
  permissionMode: "bypassPermissions",
1576
1706
  model: skipResearch ? "sonnet" : "opus",
1577
- appendSystemPrompt: PLAN_SYSTEM_RULES,
1707
+ appendSystemPrompt: `${METHODOLOGY}
1708
+
1709
+ ${PLAN_SYSTEM_RULES}`,
1578
1710
  jsonSchema: WORKFLOW_JSON_SCHEMA
1579
1711
  };
1580
1712
  let structuredOutput;
@@ -1672,8 +1804,8 @@ ${issues}`
1672
1804
 
1673
1805
  // src/ui/PlanApp.tsx
1674
1806
  import { useEffect as useEffect3, useReducer as useReducer2, useState as useState2 } from "react";
1675
- import { Box as Box5, Text as Text5, useApp as useApp2, useStdin as useStdin2 } from "ink";
1676
- import { jsx as jsx5, jsxs as jsxs4 } from "react/jsx-runtime";
1807
+ import { Box as Box6, Text as Text6, useApp as useApp2, useStdin as useStdin2 } from "ink";
1808
+ import { jsx as jsx6, jsxs as jsxs5 } from "react/jsx-runtime";
1677
1809
  var truncate = (str, max) => str.length > max ? str.slice(0, max - 3) + "..." : str;
1678
1810
  function buildInitial(description) {
1679
1811
  return {
@@ -1749,7 +1881,7 @@ function StageProgress({ stage, totalStages, stageNames, tick, isActive, status
1749
1881
  }
1750
1882
  return { icon, color, name, bold, dim };
1751
1883
  });
1752
- 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: [
1884
+ 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: [
1753
1885
  " ",
1754
1886
  icon,
1755
1887
  name ? ` ${name}` : ""
@@ -1782,19 +1914,19 @@ function PlanApp({ description, events: events2 }) {
1782
1914
  const elapsed = formatHeaderElapsed(state.startTime);
1783
1915
  const icon = isActive ? SPINNER[tick % SPINNER.length] : state.status === "complete" ? "\u2713" : "\u2717";
1784
1916
  const iconColor = state.status === "complete" ? theme.success : state.status === "error" ? theme.error : theme.primary;
1785
- return /* @__PURE__ */ jsxs4(Box5, { flexDirection: "column", padding: 1, children: [
1786
- /* @__PURE__ */ jsx5(Box5, { marginBottom: 1, children: /* @__PURE__ */ jsx5(BrandMark, { tick, isActive }) }),
1787
- /* @__PURE__ */ jsxs4(Box5, { marginBottom: 1, children: [
1788
- /* @__PURE__ */ jsxs4(Text5, { color: iconColor, children: [
1917
+ return /* @__PURE__ */ jsxs5(Box6, { flexDirection: "column", padding: 1, children: [
1918
+ /* @__PURE__ */ jsx6(Box6, { marginBottom: 1, children: /* @__PURE__ */ jsx6(BrandMark, { tick, isActive }) }),
1919
+ /* @__PURE__ */ jsxs5(Box6, { marginBottom: 1, children: [
1920
+ /* @__PURE__ */ jsxs5(Text6, { color: iconColor, children: [
1789
1921
  icon,
1790
1922
  " "
1791
1923
  ] }),
1792
- /* @__PURE__ */ jsx5(Text5, { bold: true, color: theme.primary, children: "Generating plan" }),
1793
- /* @__PURE__ */ jsxs4(Text5, { dimColor: true, children: [
1924
+ /* @__PURE__ */ jsx6(Text6, { bold: true, color: theme.primary, children: "Generating plan" }),
1925
+ /* @__PURE__ */ jsxs5(Text6, { dimColor: true, children: [
1794
1926
  " ",
1795
1927
  elapsed
1796
1928
  ] }),
1797
- state.status === "retrying" && /* @__PURE__ */ jsxs4(Text5, { color: theme.warning, children: [
1929
+ state.status === "retrying" && /* @__PURE__ */ jsxs5(Text6, { color: theme.warning, children: [
1798
1930
  " ",
1799
1931
  "(attempt ",
1800
1932
  state.attempt,
@@ -1803,11 +1935,11 @@ function PlanApp({ description, events: events2 }) {
1803
1935
  ")"
1804
1936
  ] })
1805
1937
  ] }),
1806
- /* @__PURE__ */ jsxs4(Box5, { marginBottom: 1, children: [
1807
- /* @__PURE__ */ jsx5(Text5, { dimColor: true, children: " " }),
1808
- /* @__PURE__ */ jsx5(Text5, { children: truncate(state.description, 80) })
1938
+ /* @__PURE__ */ jsxs5(Box6, { marginBottom: 1, children: [
1939
+ /* @__PURE__ */ jsx6(Text6, { dimColor: true, children: " " }),
1940
+ /* @__PURE__ */ jsx6(Text6, { children: truncate(state.description, 80) })
1809
1941
  ] }),
1810
- /* @__PURE__ */ jsx5(
1942
+ /* @__PURE__ */ jsx6(
1811
1943
  StageProgress,
1812
1944
  {
1813
1945
  stage: state.stage,
@@ -1818,23 +1950,23 @@ function PlanApp({ description, events: events2 }) {
1818
1950
  status: state.status
1819
1951
  }
1820
1952
  ),
1821
- state.lines.length > 0 && /* @__PURE__ */ jsx5(LogPane, { lines: state.lines, isActive, maxLines: 10 }),
1822
- state.status === "complete" && state.taskFile && /* @__PURE__ */ jsxs4(Box5, { flexDirection: "column", marginTop: 1, children: [
1823
- /* @__PURE__ */ jsxs4(Text5, { color: theme.success, children: [
1953
+ state.lines.length > 0 && /* @__PURE__ */ jsx6(LogPane, { lines: state.lines, isActive, maxLines: 10 }),
1954
+ state.status === "complete" && state.taskFile && /* @__PURE__ */ jsxs5(Box6, { flexDirection: "column", marginTop: 1, children: [
1955
+ /* @__PURE__ */ jsxs5(Text6, { color: theme.success, children: [
1824
1956
  "\u2705 Task plan saved: ",
1825
1957
  state.taskFile
1826
1958
  ] }),
1827
- state.preview && /* @__PURE__ */ jsxs4(Box5, { flexDirection: "column", marginTop: 1, children: [
1828
- /* @__PURE__ */ jsx5(Text5, { dimColor: true, children: "Preview:" }),
1829
- /* @__PURE__ */ jsx5(Text5, { children: state.preview })
1959
+ state.preview && /* @__PURE__ */ jsxs5(Box6, { flexDirection: "column", marginTop: 1, children: [
1960
+ /* @__PURE__ */ jsx6(Text6, { dimColor: true, children: "Preview:" }),
1961
+ /* @__PURE__ */ jsx6(Text6, { children: state.preview })
1830
1962
  ] })
1831
1963
  ] }),
1832
- state.status === "error" && /* @__PURE__ */ jsx5(Box5, { marginTop: 1, children: /* @__PURE__ */ jsxs4(Text5, { color: theme.error, children: [
1964
+ state.status === "error" && /* @__PURE__ */ jsx6(Box6, { marginTop: 1, children: /* @__PURE__ */ jsxs5(Text6, { color: theme.error, children: [
1833
1965
  "Error: ",
1834
1966
  state.errorMessage
1835
1967
  ] }) }),
1836
- isActive && /* @__PURE__ */ jsx5(Box5, { marginTop: 1, children: /* @__PURE__ */ jsx5(Text5, { dimColor: true, children: "press q to quit" }) }),
1837
- isRawModeSupported && /* @__PURE__ */ jsx5(KeyboardHandler, { onExit: exit })
1968
+ isActive && /* @__PURE__ */ jsx6(Box6, { marginTop: 1, children: /* @__PURE__ */ jsx6(Text6, { dimColor: true, children: "press q to quit" }) }),
1969
+ isRawModeSupported && /* @__PURE__ */ jsx6(KeyboardHandler, { onExit: exit })
1838
1970
  ] });
1839
1971
  }
1840
1972
 
@@ -0,0 +1,16 @@
1
+ # ============================================================================
2
+ # DEV APPROACH PROMPT
3
+ # ============================================================================
4
+ # Purpose: Eval-only template for testing development methodology adherence.
5
+ # Asks Claude to verbalize its process so behavioral criteria can be
6
+ # judged against the injected methodology system prompt.
7
+ # Used by: evals/development-methodology.eval.yaml
8
+ # Triggered when: npm run eval evals/development-methodology.eval.yaml
9
+ #
10
+ # Placeholders:
11
+ # {{TASK}} - The programming task to reason about
12
+ # ============================================================================
13
+
14
+ {{TASK}}
15
+
16
+ Before writing any code, briefly describe: what is still unclear and any assumptions you're making, what you need to learn or inspect first, how you would break this into slices, and how you would verify the implementation is correct.
@@ -0,0 +1,71 @@
1
+ # ============================================================================
2
+ # DEVELOPMENT METHODOLOGY
3
+ # ============================================================================
4
+ # Purpose: Defines the software development loop injected into every Claude
5
+ # step that executant runs.
6
+ # Used by: src/tasks/claude.ts via --append-system-prompt
7
+ # Triggered when: Every Claude step invocation
8
+ # ============================================================================
9
+
10
+ Critical rules — these apply to every task, always:
11
+
12
+ 1. TESTS FIRST: Never write implementation code before writing at least one failing test for it.
13
+ Wrong: create rate-limiter.ts → create rate-limiter.test.ts
14
+ Right: create rate-limiter.test.ts (failing) → create rate-limiter.ts to make it pass
15
+ Wrong slice order: Slice 1: write counter → Slice 2: write middleware → Slice 3: write tests
16
+ Right slice order: Slice 1: [test for counter, then counter] → Slice 2: [test for middleware, then middleware]
17
+ The test file always exists and fails before the implementation code for that feature is written.
18
+
19
+ 2. VERIFICATION SEQUENCE: After every meaningful code change, run these four steps in exact order and fix all failures before continuing:
20
+ lint → typecheck → test → build
21
+ Never say "run tests" as your only verification step. Always name all four.
22
+
23
+ 3. ASSUMPTIONS NOT QUESTIONS: If the goal or bug report is ambiguous and you cannot interactively ask for clarification, you MUST explicitly state your assumptions before proceeding. Write "I'm assuming X means Y" or "Assuming the bug refers to Z" — then act on that assumption. Do not proceed silently on an implicit assumption.
24
+
25
+ 4. COMPLEXITY VS AMBIGUITY: A complex task with clear requirements should be decomposed immediately into slices — do not treat complexity as ambiguity. A vague or underspecified task requires explicit assumptions (rule 3), not decomposition into unknown slices.
26
+
27
+ ---
28
+
29
+ Knowledge loop — repeat until sufficient knowledge is acquired. Always in this order:
30
+ - Inspect existing code
31
+ - Inspect architecture and module boundaries
32
+ - Inspect APIs/contracts
33
+ - Inspect similar implementations and conventions/patterns
34
+ - Identify unknowns/risks
35
+ - Read external documentation only when internal inspection is insufficient
36
+
37
+ If uncertainty remains: build experiments/spikes and validate assumptions.
38
+
39
+ Decomposition loop — repeat until solid:
40
+ - Split into independently shippable slices
41
+ - Order by dependency and risk (riskiest first)
42
+ - Choose next smallest shippable slice
43
+
44
+ For each slice:
45
+
46
+ Spec loop — repeat until precise:
47
+ - Write behavior spec
48
+ - Define inputs, outputs, edge cases, failure modes, acceptance criteria
49
+
50
+ Test loop — apply rule 1. Repeat until tests express the full spec:
51
+ - Write failing tests before any implementation code
52
+ - Review coverage against spec
53
+ - Add missing cases
54
+
55
+ Implementation loop — build the smallest implementation that makes the tests pass.
56
+
57
+ Verification loop — apply rule 2 after every meaningful change:
58
+ 1. lint
59
+ 2. typecheck
60
+ 3. test
61
+ 4. build
62
+
63
+ Spec-check loop — repeat until implementation matches spec:
64
+ - Compare code against acceptance criteria
65
+ - Add test for any gap → fix gap → rerun lint → typecheck → test → build
66
+
67
+ Refactor loop — repeat until maintainable:
68
+ - Simplify names, remove duplication, improve boundaries
69
+ - Rerun lint → typecheck → test → build after every change
70
+
71
+ Commit — one slice = one commit.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "executant",
3
- "version": "1.8.0",
3
+ "version": "1.10.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",