executant 1.13.0 → 1.15.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 (3) hide show
  1. package/README.md +14 -0
  2. package/dist/index.js +304 -157
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -121,6 +121,20 @@ steps:
121
121
  - **`self_healing: true`** — on script failure, Claude diagnoses and repairs the command, then re-runs it, up to 5×
122
122
  - **`self_improve: true`** — after the workflow finishes, Claude analyzes execution highlights and saves an improved YAML to `tasks/backlog/`
123
123
 
124
+ ## Interjection
125
+
126
+ While a workflow is running, press **`i`** to open a text input at the bottom of the TUI. Type a correction and press **Enter** to send it; **Esc** cancels.
127
+
128
+ The message is queued and prepended to the **next Claude step's prompt** as `[User correction from a previous step]`. Claude sees your note before it starts and incorporates it into its work. If you interject while a script step is running, the correction waits for the next Claude step in the workflow.
129
+
130
+ ```
131
+ press i → ▷ don't delete that file, use git revert▌ esc to cancel
132
+ ```
133
+
134
+ **What it's good for:** steering the next Claude step while watching the current one run — leaving a note for the step that's about to start.
135
+
136
+ **What it can't do:** interrupt a Claude step mid-execution. The Claude CLI processes each invocation as a complete unit; there's no mechanism to inject a message partway through. To abort a runaway step immediately, press `q`.
137
+
124
138
  ## Examples
125
139
 
126
140
  | File | Demonstrates |
package/dist/index.js CHANGED
@@ -406,12 +406,11 @@ import { execSync, spawn as spawn2 } from "node:child_process";
406
406
  import { zodToJsonSchema } from "zod-to-json-schema";
407
407
  var METHODOLOGY = loadPrompt("development-methodology");
408
408
  var DEFAULT_TOOLS = ["Read", "Edit", "Write", "Bash", "Glob", "Grep"];
409
- function buildClaudeArgs(task) {
409
+ function buildClaudeArgs(task, interactive = false) {
410
410
  const allowedTools = task.allowedTools ?? DEFAULT_TOOLS;
411
411
  const permissionMode = task.permissionMode ?? "bypassPermissions";
412
412
  return [
413
- "--print",
414
- task.prompt,
413
+ ...interactive ? [] : ["--print", task.prompt],
415
414
  "--output-format",
416
415
  "stream-json",
417
416
  "--verbose",
@@ -433,7 +432,7 @@ function resolveClaudePath() {
433
432
  );
434
433
  }
435
434
  }
436
- async function* runClaude(task) {
435
+ async function* runClaude(task, _channel) {
437
436
  yield {
438
437
  type: "log",
439
438
  level: "info",
@@ -562,7 +561,7 @@ function shouldSkipStep(stepNumber, name, options2) {
562
561
  }
563
562
  return options2.fromStep !== void 0 && stepNumber < options2.fromStep[0];
564
563
  }
565
- async function* runWorkflow(workflow2, options2 = {}) {
564
+ async function* runWorkflow(workflow2, options2 = {}, channel2) {
566
565
  const workflowStart = Date.now();
567
566
  yield { type: "workflow:start", workflow: workflow2 };
568
567
  for (const [i, task] of workflow2.tasks.entries()) {
@@ -575,7 +574,7 @@ async function* runWorkflow(workflow2, options2 = {}) {
575
574
  yield { type: "step:start", index: i, name: task.name };
576
575
  const from = options2.fromStep && options2.fromStep[0] === stepNumber ? options2.fromStep.slice(1) : void 0;
577
576
  try {
578
- for await (const event of runStep(task, from)) {
577
+ for await (const event of runStep(task, from, channel2)) {
579
578
  if (event.type === "step:iteration" || event.type === "step:inner" || event.type === "output:text" || event.type === "output:tool") {
580
579
  yield { ...event, index: i };
581
580
  } else {
@@ -600,7 +599,7 @@ async function* runWorkflow(workflow2, options2 = {}) {
600
599
  durationMs: Date.now() - workflowStart
601
600
  };
602
601
  }
603
- async function* runStep(task, from) {
602
+ async function* runStep(task, from, channel2) {
604
603
  switch (task.type) {
605
604
  case "log":
606
605
  yield* runLog(task);
@@ -619,11 +618,20 @@ async function* runStep(task, from) {
619
618
  }
620
619
  case "claude": {
621
620
  const expanded = expandContext(task);
622
- yield* expanded.llmAsJudge ? runClaudeWithJudge(expanded) : runClaude(expanded);
621
+ const queued = channel2?.consumeQueue() ?? [];
622
+ const enriched = queued.length > 0 ? {
623
+ ...expanded,
624
+ prompt: `[User correction from a previous step]
625
+ ${queued.join("\n")}
626
+
627
+ ---
628
+ ${expanded.prompt}`
629
+ } : expanded;
630
+ yield* enriched.llmAsJudge ? runClaudeWithJudge(enriched, channel2) : runClaude(enriched, channel2);
623
631
  break;
624
632
  }
625
633
  case "forEach":
626
- yield* runForEach(task, from);
634
+ yield* runForEach(task, from, channel2);
627
635
  break;
628
636
  default: {
629
637
  const _ = task;
@@ -634,7 +642,7 @@ async function* runStep(task, from) {
634
642
  async function* runLog(task) {
635
643
  yield { type: "output:text", index: -1, text: task.message };
636
644
  }
637
- async function* runForEach(task, from) {
645
+ async function* runForEach(task, from, channel2) {
638
646
  const items = await resolveItems(task.forEach);
639
647
  const total = items.length;
640
648
  const innerTotal = task.inner.length;
@@ -669,7 +677,7 @@ async function* runForEach(task, from) {
669
677
  }
670
678
  const childFrom = childIdx === startChild ? iterFrom?.slice(1) : void 0;
671
679
  try {
672
- for await (const event of runStep(substituted, childFrom)) {
680
+ for await (const event of runStep(substituted, childFrom, channel2)) {
673
681
  if (event.type !== "step:iteration" && event.type !== "step:inner") {
674
682
  yield event;
675
683
  }
@@ -803,14 +811,14 @@ async function* runCommandWithHealing(task) {
803
811
  }
804
812
  }
805
813
  }
806
- async function* runClaudeWithJudge(task) {
814
+ async function* runClaudeWithJudge(task, channel2) {
807
815
  let judgeContext = "";
808
816
  for (let attempt = 0; attempt < MAX_JUDGE_RETRIES; attempt++) {
809
817
  const prompt = attempt === 0 ? task.prompt : `${task.prompt}
810
818
 
811
819
  ${fillTemplate(JUDGE_RETRY_CONTEXT, { FEEDBACK: judgeContext })}`;
812
820
  const lines = [];
813
- yield* collectLines(runClaude({ ...task, prompt }), lines);
821
+ yield* collectLines(runClaude({ ...task, prompt }, channel2), lines);
814
822
  yield {
815
823
  type: "log",
816
824
  level: "info",
@@ -918,18 +926,86 @@ ${blocks.join("\n\n")}`;
918
926
  init_update();
919
927
 
920
928
  // src/ui/App.tsx
921
- import { useEffect as useEffect2, useReducer, useState } from "react";
922
- import { Box as Box5, Text as Text5, useApp, useStdin, useStdout } from "ink";
929
+ import { useEffect as useEffect2, useReducer, useState as useState2 } from "react";
930
+ import { Box as Box6, Text as Text6, useApp, useStdin, useStdout } from "ink";
923
931
 
924
932
  // src/ui/KeyboardHandler.tsx
925
933
  import { useInput } from "ink";
926
- function KeyboardHandler({ onExit }) {
934
+ function KeyboardHandler({
935
+ onExit,
936
+ onInterject,
937
+ isInterjecting
938
+ }) {
927
939
  useInput((input, key) => {
940
+ if (isInterjecting) return;
928
941
  if (input === "q" || key.ctrl && input === "c") onExit();
942
+ if (input === "i" && onInterject) onInterject();
929
943
  });
930
944
  return null;
931
945
  }
932
946
 
947
+ // src/ui/InterjectInput.tsx
948
+ import { useState } from "react";
949
+ import { Box, Text, useInput as useInput2 } from "ink";
950
+
951
+ // src/ui/theme.ts
952
+ import { createRequire } from "node:module";
953
+ import { oklchToHex } from "@coston/design-tokens";
954
+ var THEME_NAME = "purple-dark";
955
+ var _require = createRequire(import.meta.url);
956
+ var { themes } = _require("@coston/design-tokens/tokens.json");
957
+ function hex(key) {
958
+ return oklchToHex(themes[THEME_NAME][key]);
959
+ }
960
+ var theme = {
961
+ foreground: hex("foreground"),
962
+ // primary text
963
+ muted: hex("muted-foreground"),
964
+ // dimmed / inactive text and borders
965
+ primary: hex("primary"),
966
+ // tool calls, cursor, active
967
+ primaryLight: hex("secondary-foreground"),
968
+ // lighter tint of primary (same hue, higher lightness)
969
+ success: hex("success"),
970
+ // completed steps
971
+ error: hex("destructive"),
972
+ // errors
973
+ warning: hex("warning"),
974
+ // warnings, retries, updates
975
+ border: hex("border")
976
+ // log pane border
977
+ };
978
+
979
+ // src/ui/InterjectInput.tsx
980
+ import { jsx, jsxs } from "react/jsx-runtime";
981
+ function InterjectInput({ onSubmit, onCancel }) {
982
+ const [value, setValue] = useState("");
983
+ useInput2((input, key) => {
984
+ if (key.escape) {
985
+ onCancel();
986
+ return;
987
+ }
988
+ if (key.return) {
989
+ if (value.trim()) onSubmit(value.trim());
990
+ else onCancel();
991
+ return;
992
+ }
993
+ if (key.backspace || key.delete) {
994
+ setValue((v) => v.slice(0, -1));
995
+ return;
996
+ }
997
+ if (input && !key.ctrl && !key.meta) {
998
+ setValue((v) => v + input);
999
+ }
1000
+ });
1001
+ return /* @__PURE__ */ jsxs(Box, { marginTop: 1, flexDirection: "row", children: [
1002
+ /* @__PURE__ */ jsx(Text, { color: theme.primary, bold: true, children: "\u25B7 " }),
1003
+ /* @__PURE__ */ jsx(Text, { children: value }),
1004
+ /* @__PURE__ */ jsx(Text, { color: theme.primary, children: "\u258C" }),
1005
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: " esc to cancel" })
1006
+ ] });
1007
+ }
1008
+
933
1009
  // src/ui/formatTool.ts
934
1010
  function formatToolCall2(tool, input) {
935
1011
  switch (tool) {
@@ -1081,6 +1157,11 @@ function reducer(state, event) {
1081
1157
  if (idx >= state.tasks.length) return state;
1082
1158
  return appendLines(state, idx, `[${event.level}] ${event.text}`);
1083
1159
  }
1160
+ case "step:interjection": {
1161
+ const idx = event.index;
1162
+ if (idx >= state.tasks.length) return state;
1163
+ return appendLines(state, idx, `[interjection] ${event.message}`);
1164
+ }
1084
1165
  default: {
1085
1166
  const _ = event;
1086
1167
  void _;
@@ -1116,35 +1197,7 @@ function appendLines(state, index, text) {
1116
1197
  }
1117
1198
 
1118
1199
  // src/ui/TaskRow.tsx
1119
- import { Box, Text } from "ink";
1120
-
1121
- // src/ui/theme.ts
1122
- import { createRequire } from "node:module";
1123
- import { oklchToHex } from "@coston/design-tokens";
1124
- var THEME_NAME = "purple-dark";
1125
- var _require = createRequire(import.meta.url);
1126
- var { themes } = _require("@coston/design-tokens/tokens.json");
1127
- function hex(key) {
1128
- return oklchToHex(themes[THEME_NAME][key]);
1129
- }
1130
- var theme = {
1131
- foreground: hex("foreground"),
1132
- // primary text
1133
- muted: hex("muted-foreground"),
1134
- // dimmed / inactive text and borders
1135
- primary: hex("primary"),
1136
- // tool calls, cursor, active
1137
- primaryLight: hex("secondary-foreground"),
1138
- // lighter tint of primary (same hue, higher lightness)
1139
- success: hex("success"),
1140
- // completed steps
1141
- error: hex("destructive"),
1142
- // errors
1143
- warning: hex("warning"),
1144
- // warnings, retries, updates
1145
- border: hex("border")
1146
- // log pane border
1147
- };
1200
+ import { Box as Box2, Text as Text2 } from "ink";
1148
1201
 
1149
1202
  // src/ui/utils.ts
1150
1203
  var SPINNER = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
@@ -1162,6 +1215,12 @@ var STATUS_COLOR = {
1162
1215
  function statusIcon(status, tick) {
1163
1216
  return status === "running" ? SPINNER[tick % SPINNER.length] : STATUS_ICON[status] ?? "\xB7";
1164
1217
  }
1218
+ function countIterationRows(iterationHistory, maxVisible) {
1219
+ if (!iterationHistory?.length) return 0;
1220
+ if (iterationHistory.every((r) => r.item === String(r.iteration))) return 0;
1221
+ const visible = Math.min(iterationHistory.length, maxVisible);
1222
+ return visible + (iterationHistory.length > maxVisible ? 1 : 0);
1223
+ }
1165
1224
  var EXIT_DELAY_MS = 300;
1166
1225
  function formatHeaderElapsed(start, end) {
1167
1226
  const ms = (end ?? Date.now()) - start;
@@ -1176,7 +1235,7 @@ function formatTaskElapsed(start, end, status) {
1176
1235
  }
1177
1236
 
1178
1237
  // src/ui/TaskRow.tsx
1179
- import { jsx, jsxs } from "react/jsx-runtime";
1238
+ import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
1180
1239
  function TaskRow({ taskState, isActive, index, tick }) {
1181
1240
  const { task, status, startTime, endTime } = taskState;
1182
1241
  const icon = statusIcon(status, tick);
@@ -1184,13 +1243,13 @@ function TaskRow({ taskState, isActive, index, tick }) {
1184
1243
  const elapsed = formatTaskElapsed(startTime, endTime, status);
1185
1244
  const iterInfo = formatIterCount(taskState.iterationHistory);
1186
1245
  const label = `${index + 1}. ${task.name}${iterInfo}`;
1187
- return /* @__PURE__ */ jsxs(Box, { children: [
1188
- /* @__PURE__ */ jsxs(Text, { color, children: [
1246
+ return /* @__PURE__ */ jsxs2(Box2, { children: [
1247
+ /* @__PURE__ */ jsxs2(Text2, { color, children: [
1189
1248
  icon,
1190
1249
  " "
1191
1250
  ] }),
1192
- /* @__PURE__ */ jsx(Text, { color: isActive ? theme.foreground : theme.muted, bold: isActive, children: label }),
1193
- /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
1251
+ /* @__PURE__ */ jsx2(Text2, { color: isActive ? theme.foreground : theme.muted, bold: isActive, children: label }),
1252
+ /* @__PURE__ */ jsxs2(Text2, { dimColor: true, children: [
1194
1253
  " ",
1195
1254
  elapsed
1196
1255
  ] })
@@ -1209,22 +1268,22 @@ function formatIterCount(history) {
1209
1268
  }
1210
1269
 
1211
1270
  // src/ui/IterationRow.tsx
1212
- import { Box as Box2, Text as Text2 } from "ink";
1213
- import { Fragment, jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
1271
+ import { Box as Box3, Text as Text3 } from "ink";
1272
+ import { Fragment, jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
1214
1273
  function IterationRow({ record, tick }) {
1215
1274
  const icon = statusIcon(record.status, tick);
1216
1275
  const color = STATUS_COLOR[record.status] ?? theme.primary;
1217
1276
  const innerText = record.inner ? ` \u2014 ${stripItem(record.inner.name, record.item)} [${record.inner.index + 1}/${record.inner.total}]` : "";
1218
1277
  const ms = (record.endTime ?? Date.now()) - record.startTime;
1219
1278
  const elapsed = `${(ms / 1e3).toFixed(1)}s`;
1220
- return /* @__PURE__ */ jsxs2(Box2, { children: [
1221
- /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: " " }),
1222
- /* @__PURE__ */ jsx2(Text2, { color, children: icon }),
1223
- /* @__PURE__ */ jsx2(Text2, { children: " " }),
1224
- /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: `[${record.iteration}/${record.total}]` }),
1225
- /* @__PURE__ */ jsx2(Text2, { children: " " }),
1226
- /* @__PURE__ */ jsxs2(
1227
- Text2,
1279
+ return /* @__PURE__ */ jsxs3(Box3, { children: [
1280
+ /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: " " }),
1281
+ /* @__PURE__ */ jsx3(Text3, { color, children: icon }),
1282
+ /* @__PURE__ */ jsx3(Text3, { children: " " }),
1283
+ /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: `[${record.iteration}/${record.total}]` }),
1284
+ /* @__PURE__ */ jsx3(Text3, { children: " " }),
1285
+ /* @__PURE__ */ jsxs3(
1286
+ Text3,
1228
1287
  {
1229
1288
  color: record.status === "running" ? theme.foreground : theme.muted,
1230
1289
  children: [
@@ -1233,7 +1292,7 @@ function IterationRow({ record, tick }) {
1233
1292
  ]
1234
1293
  }
1235
1294
  ),
1236
- /* @__PURE__ */ jsxs2(Text2, { dimColor: true, children: [
1295
+ /* @__PURE__ */ jsxs3(Text3, { dimColor: true, children: [
1237
1296
  " ",
1238
1297
  elapsed
1239
1298
  ] })
@@ -1255,29 +1314,29 @@ function IterationList({
1255
1314
  if (isRepeatStyle(iterationHistory)) return null;
1256
1315
  const hidden = iterationHistory.length - maxVisible;
1257
1316
  const visible = iterationHistory.slice(-maxVisible);
1258
- return /* @__PURE__ */ jsxs2(Fragment, { children: [
1259
- hidden > 0 && /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: ` \u2026 ${hidden} earlier` }),
1260
- visible.map((record) => /* @__PURE__ */ jsx2(IterationRow, { record, tick }, record.iteration))
1317
+ return /* @__PURE__ */ jsxs3(Fragment, { children: [
1318
+ hidden > 0 && /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: ` \u2026 ${hidden} earlier` }),
1319
+ visible.map((record) => /* @__PURE__ */ jsx3(IterationRow, { record, tick }, record.iteration))
1261
1320
  ] });
1262
1321
  }
1263
1322
 
1264
1323
  // src/ui/LogPane.tsx
1265
- import { Box as Box3, Text as Text3 } from "ink";
1266
- import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
1324
+ import { Box as Box4, Text as Text4 } from "ink";
1325
+ import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
1267
1326
  function LogPane({ lines, isActive = false, maxLines = 15 }) {
1268
1327
  const visible = lines.slice(-maxLines);
1269
1328
  if (visible.length === 0) {
1270
- return /* @__PURE__ */ jsx3(Box3, { marginTop: 1, children: /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: isActive ? "\u2838 waiting for output\u2026" : "\u2014 no output yet \u2014" }) });
1329
+ return /* @__PURE__ */ jsx4(Box4, { marginTop: 1, children: /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: isActive ? "\u2838 waiting for output\u2026" : "\u2014 no output yet \u2014" }) });
1271
1330
  }
1272
- return /* @__PURE__ */ jsx3(
1273
- Box3,
1331
+ return /* @__PURE__ */ jsx4(
1332
+ Box4,
1274
1333
  {
1275
1334
  flexDirection: "column",
1276
1335
  marginTop: 1,
1277
1336
  borderStyle: "single",
1278
1337
  borderColor: theme.border,
1279
1338
  paddingX: 1,
1280
- children: visible.map((line, i) => /* @__PURE__ */ jsx3(
1339
+ children: visible.map((line, i) => /* @__PURE__ */ jsx4(
1281
1340
  LogLine,
1282
1341
  {
1283
1342
  text: line,
@@ -1289,52 +1348,52 @@ function LogPane({ lines, isActive = false, maxLines = 15 }) {
1289
1348
  );
1290
1349
  }
1291
1350
  function LogLine({ text, cursor }) {
1292
- const suffix = cursor ? /* @__PURE__ */ jsx3(Text3, { color: theme.primary, children: " \u258C" }) : null;
1351
+ const suffix = cursor ? /* @__PURE__ */ jsx4(Text4, { color: theme.primary, children: " \u258C" }) : null;
1293
1352
  if (/^\[[\w:]+\]/.test(text)) {
1294
1353
  const bracket = text.match(/^\[[\w:]+\]/)?.[0] ?? "";
1295
1354
  const rest = text.slice(bracket.length);
1296
- return /* @__PURE__ */ jsxs3(Text3, { children: [
1297
- /* @__PURE__ */ jsx3(Text3, { color: theme.primary, children: bracket }),
1298
- /* @__PURE__ */ jsx3(Text3, { children: rest }),
1355
+ return /* @__PURE__ */ jsxs4(Text4, { children: [
1356
+ /* @__PURE__ */ jsx4(Text4, { color: theme.primary, children: bracket }),
1357
+ /* @__PURE__ */ jsx4(Text4, { children: rest }),
1299
1358
  suffix
1300
1359
  ] });
1301
1360
  }
1302
1361
  if (/^\s*\$\s/.test(text))
1303
- return /* @__PURE__ */ jsxs3(Text3, { color: theme.warning, children: [
1362
+ return /* @__PURE__ */ jsxs4(Text4, { color: theme.warning, children: [
1304
1363
  text,
1305
1364
  suffix
1306
1365
  ] });
1307
1366
  if (text.startsWith("[warn]"))
1308
- return /* @__PURE__ */ jsxs3(Text3, { color: theme.warning, children: [
1367
+ return /* @__PURE__ */ jsxs4(Text4, { color: theme.warning, children: [
1309
1368
  text,
1310
1369
  suffix
1311
1370
  ] });
1312
1371
  if (text.startsWith("[error]"))
1313
- return /* @__PURE__ */ jsxs3(Text3, { color: theme.error, children: [
1372
+ return /* @__PURE__ */ jsxs4(Text4, { color: theme.error, children: [
1314
1373
  text,
1315
1374
  suffix
1316
1375
  ] });
1317
1376
  if (/^[\s]*(✓|✔|✅|done|success|compiled|built|passed)/i.test(text) && !/\b(error|fail|failed|warn|warning)\b/i.test(text))
1318
- return /* @__PURE__ */ jsxs3(Text3, { color: theme.success, children: [
1377
+ return /* @__PURE__ */ jsxs4(Text4, { color: theme.success, children: [
1319
1378
  text,
1320
1379
  suffix
1321
1380
  ] });
1322
1381
  if (/\b(error|failed|fail)\b/i.test(text))
1323
- return /* @__PURE__ */ jsxs3(Text3, { color: theme.error, children: [
1382
+ return /* @__PURE__ */ jsxs4(Text4, { color: theme.error, children: [
1324
1383
  text,
1325
1384
  suffix
1326
1385
  ] });
1327
1386
  if (/\b(warn|warning)\b/i.test(text))
1328
- return /* @__PURE__ */ jsxs3(Text3, { color: theme.warning, children: [
1387
+ return /* @__PURE__ */ jsxs4(Text4, { color: theme.warning, children: [
1329
1388
  text,
1330
1389
  suffix
1331
1390
  ] });
1332
1391
  if (/^[·…⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏]/.test(text))
1333
- return /* @__PURE__ */ jsxs3(Text3, { color: theme.muted, children: [
1392
+ return /* @__PURE__ */ jsxs4(Text4, { color: theme.muted, children: [
1334
1393
  text,
1335
1394
  suffix
1336
1395
  ] });
1337
- return /* @__PURE__ */ jsxs3(Text3, { children: [
1396
+ return /* @__PURE__ */ jsxs4(Text4, { children: [
1338
1397
  text,
1339
1398
  suffix
1340
1399
  ] });
@@ -1354,8 +1413,8 @@ function useInterval(callback, delayMs) {
1354
1413
  }
1355
1414
 
1356
1415
  // src/ui/BrandMark.tsx
1357
- import { Box as Box4, Text as Text4 } from "ink";
1358
- import { jsx as jsx4 } from "react/jsx-runtime";
1416
+ import { Box as Box5, Text as Text5 } from "ink";
1417
+ import { jsx as jsx5 } from "react/jsx-runtime";
1359
1418
  var BRAND = "Executant";
1360
1419
  var SWEEP_TICKS = BRAND.length * 2;
1361
1420
  var GAP_TICKS = 30;
@@ -1368,15 +1427,22 @@ function charColor(charIndex, tick, isActive) {
1368
1427
  return charIndex === charPos ? theme.primaryLight : theme.primary;
1369
1428
  }
1370
1429
  function BrandMark({ tick, isActive }) {
1371
- return /* @__PURE__ */ jsx4(Box4, { children: [...BRAND].map((char, i) => /* @__PURE__ */ jsx4(Text4, { color: charColor(i, tick, isActive), bold: true, children: char }, i)) });
1430
+ return /* @__PURE__ */ jsx5(Box5, { children: [...BRAND].map((char, i) => /* @__PURE__ */ jsx5(Text5, { color: charColor(i, tick, isActive), bold: true, children: char }, i)) });
1372
1431
  }
1373
1432
 
1374
1433
  // src/ui/App.tsx
1375
- import { jsx as jsx5, jsxs as jsxs4 } from "react/jsx-runtime";
1434
+ import { jsx as jsx6, jsxs as jsxs5 } from "react/jsx-runtime";
1376
1435
  var MAX_VISIBLE_ITERATIONS = 8;
1377
- function App({ workflow: workflow2, events: events2, options: options2, updateCheck: updateCheck2 }) {
1436
+ function App({
1437
+ workflow: workflow2,
1438
+ events: events2,
1439
+ options: options2,
1440
+ updateCheck: updateCheck2,
1441
+ interjectChannel
1442
+ }) {
1378
1443
  const { exit } = useApp();
1379
1444
  const [state, dispatch] = useReducer(reducer, buildInitialState(workflow2));
1445
+ const [isInterjecting, setIsInterjecting] = useState2(false);
1380
1446
  useEffect2(() => {
1381
1447
  let active = true;
1382
1448
  (async () => {
@@ -1405,20 +1471,34 @@ function App({ workflow: workflow2, events: events2, options: options2, updateCh
1405
1471
  }, [events2, exit]);
1406
1472
  const { isRawModeSupported } = useStdin();
1407
1473
  const { stdout } = useStdout();
1408
- const terminalRows = stdout?.rows ?? 24;
1409
- const FIXED_OVERHEAD = 12;
1410
- const logPaneMaxLines = Math.max(
1411
- 5,
1412
- terminalRows - FIXED_OVERHEAD - state.tasks.length
1413
- );
1414
- const [tick, setTick] = useState(0);
1415
- useInterval(() => {
1416
- if (!state.endTime) setTick((t) => t + 1);
1417
- }, 100);
1418
- const [updateVersion, setUpdateVersion] = useState(null);
1474
+ const [updateVersion, setUpdateVersion] = useState2(null);
1419
1475
  useEffect2(() => {
1420
1476
  updateCheck2.then(setUpdateVersion);
1421
1477
  }, [updateCheck2]);
1478
+ const [tick, setTick] = useState2(0);
1479
+ useInterval(() => {
1480
+ if (!state.endTime) setTick((t) => t + 1);
1481
+ }, 100);
1482
+ const terminalRows = stdout?.rows ?? 24;
1483
+ const LOG_PANE_MIN = 5;
1484
+ const runningTask = state.tasks.find((t) => t.status === "running");
1485
+ const iterationRowCount = countIterationRows(
1486
+ runningTask?.iterationHistory,
1487
+ MAX_VISIBLE_ITERATIONS
1488
+ );
1489
+ const FIXED_OVERHEAD = 12 + (updateVersion ? 1 : 0) + (isInterjecting ? 1 : 0);
1490
+ const availableForTaskSection = Math.max(
1491
+ 1,
1492
+ terminalRows - FIXED_OVERHEAD - LOG_PANE_MIN - iterationRowCount
1493
+ );
1494
+ const visibleTaskCount = state.tasks.length > availableForTaskSection ? availableForTaskSection - 1 : state.tasks.length;
1495
+ const taskSlice = state.tasks.slice(-visibleTaskCount);
1496
+ const hiddenTaskCount = state.tasks.length - taskSlice.length;
1497
+ const taskRowsUsed = visibleTaskCount + (hiddenTaskCount > 0 ? 1 : 0);
1498
+ const logPaneMaxLines = Math.max(
1499
+ LOG_PANE_MIN,
1500
+ terminalRows - FIXED_OVERHEAD - taskRowsUsed - iterationRowCount
1501
+ );
1422
1502
  const elapsed = formatHeaderElapsed(state.startTime, state.endTime);
1423
1503
  const activeTask = state.tasks[state.currentIndex];
1424
1504
  const completedCount = state.tasks.filter(
@@ -1426,11 +1506,11 @@ function App({ workflow: workflow2, events: events2, options: options2, updateCh
1426
1506
  ).length;
1427
1507
  const totalCount = state.tasks.length;
1428
1508
  const filterInfo = options2?.stepFilter ? ` [step: ${options2.stepFilter}]` : options2?.fromStep ? ` [from step: ${options2.fromStep}]` : "";
1429
- return /* @__PURE__ */ jsxs4(Box5, { flexDirection: "column", padding: 1, children: [
1430
- /* @__PURE__ */ jsx5(Box5, { marginBottom: 1, children: /* @__PURE__ */ jsx5(BrandMark, { tick, isActive: !state.endTime }) }),
1431
- /* @__PURE__ */ jsxs4(Box5, { marginBottom: 1, children: [
1432
- /* @__PURE__ */ jsx5(Text5, { bold: true, color: theme.primary, children: workflow2.goal }),
1433
- /* @__PURE__ */ jsxs4(Text5, { dimColor: true, children: [
1509
+ return /* @__PURE__ */ jsxs5(Box6, { flexDirection: "column", padding: 1, children: [
1510
+ /* @__PURE__ */ jsx6(Box6, { marginBottom: 1, children: /* @__PURE__ */ jsx6(BrandMark, { tick, isActive: !state.endTime }) }),
1511
+ /* @__PURE__ */ jsxs5(Box6, { marginBottom: 1, children: [
1512
+ /* @__PURE__ */ jsx6(Text6, { bold: true, color: theme.primary, children: workflow2.goal }),
1513
+ /* @__PURE__ */ jsxs5(Text6, { dimColor: true, children: [
1434
1514
  " ",
1435
1515
  completedCount,
1436
1516
  "/",
@@ -1440,26 +1520,37 @@ function App({ workflow: workflow2, events: events2, options: options2, updateCh
1440
1520
  filterInfo
1441
1521
  ] })
1442
1522
  ] }),
1443
- /* @__PURE__ */ jsx5(Box5, { flexDirection: "column", marginBottom: 1, children: state.tasks.map((taskState, i) => /* @__PURE__ */ jsxs4(Box5, { flexDirection: "column", children: [
1444
- /* @__PURE__ */ jsx5(
1445
- TaskRow,
1446
- {
1447
- index: i,
1448
- tick,
1449
- taskState,
1450
- isActive: i === state.currentIndex
1451
- }
1452
- ),
1453
- taskState.status === "running" && taskState.iterationHistory?.length ? /* @__PURE__ */ jsx5(
1454
- IterationList,
1455
- {
1456
- iterationHistory: taskState.iterationHistory,
1457
- tick,
1458
- maxVisible: MAX_VISIBLE_ITERATIONS
1459
- }
1460
- ) : null
1461
- ] }, i)) }),
1462
- activeTask && /* @__PURE__ */ jsx5(
1523
+ /* @__PURE__ */ jsxs5(Box6, { flexDirection: "column", marginBottom: 1, children: [
1524
+ hiddenTaskCount > 0 && /* @__PURE__ */ jsxs5(Text6, { dimColor: true, children: [
1525
+ " ",
1526
+ "\xB7\xB7\xB7 ",
1527
+ hiddenTaskCount,
1528
+ " earlier"
1529
+ ] }),
1530
+ taskSlice.map((taskState, i) => {
1531
+ const globalIndex = hiddenTaskCount + i;
1532
+ return /* @__PURE__ */ jsxs5(Box6, { flexDirection: "column", children: [
1533
+ /* @__PURE__ */ jsx6(
1534
+ TaskRow,
1535
+ {
1536
+ index: globalIndex,
1537
+ tick,
1538
+ taskState,
1539
+ isActive: globalIndex === state.currentIndex
1540
+ }
1541
+ ),
1542
+ taskState.status === "running" && taskState.iterationHistory?.length ? /* @__PURE__ */ jsx6(
1543
+ IterationList,
1544
+ {
1545
+ iterationHistory: taskState.iterationHistory,
1546
+ tick,
1547
+ maxVisible: MAX_VISIBLE_ITERATIONS
1548
+ }
1549
+ ) : null
1550
+ ] }, globalIndex);
1551
+ })
1552
+ ] }),
1553
+ activeTask && /* @__PURE__ */ jsx6(
1463
1554
  LogPane,
1464
1555
  {
1465
1556
  lines: activeTask.lines,
@@ -1467,22 +1558,44 @@ function App({ workflow: workflow2, events: events2, options: options2, updateCh
1467
1558
  maxLines: logPaneMaxLines
1468
1559
  }
1469
1560
  ),
1470
- state.endTime !== void 0 && state.writtenFiles.length > 0 && /* @__PURE__ */ jsxs4(Box5, { flexDirection: "column", marginTop: 1, children: [
1471
- /* @__PURE__ */ jsx5(Text5, { dimColor: true, children: "files written:" }),
1472
- state.writtenFiles.map((f) => /* @__PURE__ */ jsxs4(Text5, { color: theme.primary, children: [
1561
+ state.endTime !== void 0 && state.writtenFiles.length > 0 && /* @__PURE__ */ jsxs5(Box6, { flexDirection: "column", marginTop: 1, children: [
1562
+ /* @__PURE__ */ jsx6(Text6, { dimColor: true, children: "files written:" }),
1563
+ state.writtenFiles.map((f) => /* @__PURE__ */ jsxs5(Text6, { color: theme.primary, children: [
1473
1564
  " ",
1474
1565
  f
1475
1566
  ] }, f))
1476
1567
  ] }),
1477
- /* @__PURE__ */ jsxs4(Box5, { marginTop: 1, flexDirection: "column", children: [
1478
- updateVersion && /* @__PURE__ */ jsxs4(Text5, { color: theme.warning, children: [
1568
+ isInterjecting && interjectChannel && /* @__PURE__ */ jsx6(
1569
+ InterjectInput,
1570
+ {
1571
+ onSubmit: (msg) => {
1572
+ interjectChannel.interject(msg);
1573
+ dispatch({
1574
+ type: "step:interjection",
1575
+ index: state.currentIndex,
1576
+ message: msg
1577
+ });
1578
+ setIsInterjecting(false);
1579
+ },
1580
+ onCancel: () => setIsInterjecting(false)
1581
+ }
1582
+ ),
1583
+ /* @__PURE__ */ jsxs5(Box6, { marginTop: 1, flexDirection: "column", children: [
1584
+ updateVersion && /* @__PURE__ */ jsxs5(Text6, { color: theme.warning, children: [
1479
1585
  "v",
1480
1586
  updateVersion,
1481
1587
  " available \u2014 run: executant update"
1482
1588
  ] }),
1483
- /* @__PURE__ */ jsx5(Text5, { dimColor: true, children: "press q to quit" })
1589
+ /* @__PURE__ */ jsx6(Text6, { dimColor: true, children: isInterjecting ? "typing interjection\u2026" : "press q to quit \xB7 i to interject" })
1484
1590
  ] }),
1485
- isRawModeSupported && /* @__PURE__ */ jsx5(KeyboardHandler, { onExit: exit })
1591
+ isRawModeSupported && /* @__PURE__ */ jsx6(
1592
+ KeyboardHandler,
1593
+ {
1594
+ onExit: exit,
1595
+ onInterject: interjectChannel ? () => setIsInterjecting(true) : void 0,
1596
+ isInterjecting
1597
+ }
1598
+ )
1486
1599
  ] });
1487
1600
  }
1488
1601
 
@@ -1891,9 +2004,9 @@ ${issues}`
1891
2004
  }
1892
2005
 
1893
2006
  // src/ui/PlanApp.tsx
1894
- import { useEffect as useEffect3, useReducer as useReducer2, useState as useState2 } from "react";
1895
- import { Box as Box6, Text as Text6, useApp as useApp2, useStdin as useStdin2 } from "ink";
1896
- import { jsx as jsx6, jsxs as jsxs5 } from "react/jsx-runtime";
2007
+ import { useEffect as useEffect3, useReducer as useReducer2, useState as useState3 } from "react";
2008
+ import { Box as Box7, Text as Text7, useApp as useApp2, useStdin as useStdin2 } from "ink";
2009
+ import { jsx as jsx7, jsxs as jsxs6 } from "react/jsx-runtime";
1897
2010
  var truncate = (str, max) => str.length > max ? str.slice(0, max - 3) + "..." : str;
1898
2011
  function buildInitial(description) {
1899
2012
  return {
@@ -1969,7 +2082,7 @@ function StageProgress({ stage, totalStages, stageNames, tick, isActive, status
1969
2082
  }
1970
2083
  return { icon, color, name, bold, dim };
1971
2084
  });
1972
- 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: [
2085
+ return /* @__PURE__ */ jsx7(Box7, { flexDirection: "column", marginBottom: 1, children: rows.map(({ icon, color, name, bold, dim }, i) => /* @__PURE__ */ jsx7(Box7, { children: /* @__PURE__ */ jsxs6(Text7, { color, dimColor: dim, bold, children: [
1973
2086
  " ",
1974
2087
  icon,
1975
2088
  name ? ` ${name}` : ""
@@ -1994,7 +2107,7 @@ function PlanApp({ description, events: events2 }) {
1994
2107
  };
1995
2108
  }, [events2, exit]);
1996
2109
  const { isRawModeSupported } = useStdin2();
1997
- const [tick, setTick] = useState2(0);
2110
+ const [tick, setTick] = useState3(0);
1998
2111
  const isActive = state.status === "running" || state.status === "retrying";
1999
2112
  useInterval(() => {
2000
2113
  if (isActive) setTick((t) => t + 1);
@@ -2002,19 +2115,19 @@ function PlanApp({ description, events: events2 }) {
2002
2115
  const elapsed = formatHeaderElapsed(state.startTime);
2003
2116
  const icon = isActive ? SPINNER[tick % SPINNER.length] : state.status === "complete" ? "\u2713" : "\u2717";
2004
2117
  const iconColor = state.status === "complete" ? theme.success : state.status === "error" ? theme.error : theme.primary;
2005
- return /* @__PURE__ */ jsxs5(Box6, { flexDirection: "column", padding: 1, children: [
2006
- /* @__PURE__ */ jsx6(Box6, { marginBottom: 1, children: /* @__PURE__ */ jsx6(BrandMark, { tick, isActive }) }),
2007
- /* @__PURE__ */ jsxs5(Box6, { marginBottom: 1, children: [
2008
- /* @__PURE__ */ jsxs5(Text6, { color: iconColor, children: [
2118
+ return /* @__PURE__ */ jsxs6(Box7, { flexDirection: "column", padding: 1, children: [
2119
+ /* @__PURE__ */ jsx7(Box7, { marginBottom: 1, children: /* @__PURE__ */ jsx7(BrandMark, { tick, isActive }) }),
2120
+ /* @__PURE__ */ jsxs6(Box7, { marginBottom: 1, children: [
2121
+ /* @__PURE__ */ jsxs6(Text7, { color: iconColor, children: [
2009
2122
  icon,
2010
2123
  " "
2011
2124
  ] }),
2012
- /* @__PURE__ */ jsx6(Text6, { bold: true, color: theme.primary, children: "Generating plan" }),
2013
- /* @__PURE__ */ jsxs5(Text6, { dimColor: true, children: [
2125
+ /* @__PURE__ */ jsx7(Text7, { bold: true, color: theme.primary, children: "Generating plan" }),
2126
+ /* @__PURE__ */ jsxs6(Text7, { dimColor: true, children: [
2014
2127
  " ",
2015
2128
  elapsed
2016
2129
  ] }),
2017
- state.status === "retrying" && /* @__PURE__ */ jsxs5(Text6, { color: theme.warning, children: [
2130
+ state.status === "retrying" && /* @__PURE__ */ jsxs6(Text7, { color: theme.warning, children: [
2018
2131
  " ",
2019
2132
  "(attempt ",
2020
2133
  state.attempt,
@@ -2023,11 +2136,11 @@ function PlanApp({ description, events: events2 }) {
2023
2136
  ")"
2024
2137
  ] })
2025
2138
  ] }),
2026
- /* @__PURE__ */ jsxs5(Box6, { marginBottom: 1, children: [
2027
- /* @__PURE__ */ jsx6(Text6, { dimColor: true, children: " " }),
2028
- /* @__PURE__ */ jsx6(Text6, { children: truncate(state.description, 80) })
2139
+ /* @__PURE__ */ jsxs6(Box7, { marginBottom: 1, children: [
2140
+ /* @__PURE__ */ jsx7(Text7, { dimColor: true, children: " " }),
2141
+ /* @__PURE__ */ jsx7(Text7, { children: truncate(state.description, 80) })
2029
2142
  ] }),
2030
- /* @__PURE__ */ jsx6(
2143
+ /* @__PURE__ */ jsx7(
2031
2144
  StageProgress,
2032
2145
  {
2033
2146
  stage: state.stage,
@@ -2038,23 +2151,23 @@ function PlanApp({ description, events: events2 }) {
2038
2151
  status: state.status
2039
2152
  }
2040
2153
  ),
2041
- state.lines.length > 0 && /* @__PURE__ */ jsx6(LogPane, { lines: state.lines, isActive, maxLines: 10 }),
2042
- state.status === "complete" && state.taskFile && /* @__PURE__ */ jsxs5(Box6, { flexDirection: "column", marginTop: 1, children: [
2043
- /* @__PURE__ */ jsxs5(Text6, { color: theme.success, children: [
2154
+ state.lines.length > 0 && /* @__PURE__ */ jsx7(LogPane, { lines: state.lines, isActive, maxLines: 10 }),
2155
+ state.status === "complete" && state.taskFile && /* @__PURE__ */ jsxs6(Box7, { flexDirection: "column", marginTop: 1, children: [
2156
+ /* @__PURE__ */ jsxs6(Text7, { color: theme.success, children: [
2044
2157
  "\u2705 Task plan saved: ",
2045
2158
  state.taskFile
2046
2159
  ] }),
2047
- state.preview && /* @__PURE__ */ jsxs5(Box6, { flexDirection: "column", marginTop: 1, children: [
2048
- /* @__PURE__ */ jsx6(Text6, { dimColor: true, children: "Preview:" }),
2049
- /* @__PURE__ */ jsx6(Text6, { children: state.preview })
2160
+ state.preview && /* @__PURE__ */ jsxs6(Box7, { flexDirection: "column", marginTop: 1, children: [
2161
+ /* @__PURE__ */ jsx7(Text7, { dimColor: true, children: "Preview:" }),
2162
+ /* @__PURE__ */ jsx7(Text7, { children: state.preview })
2050
2163
  ] })
2051
2164
  ] }),
2052
- state.status === "error" && /* @__PURE__ */ jsx6(Box6, { marginTop: 1, children: /* @__PURE__ */ jsxs5(Text6, { color: theme.error, children: [
2165
+ state.status === "error" && /* @__PURE__ */ jsx7(Box7, { marginTop: 1, children: /* @__PURE__ */ jsxs6(Text7, { color: theme.error, children: [
2053
2166
  "Error: ",
2054
2167
  state.errorMessage
2055
2168
  ] }) }),
2056
- isActive && /* @__PURE__ */ jsx6(Box6, { marginTop: 1, children: /* @__PURE__ */ jsx6(Text6, { dimColor: true, children: "press q to quit" }) }),
2057
- isRawModeSupported && /* @__PURE__ */ jsx6(KeyboardHandler, { onExit: exit })
2169
+ isActive && /* @__PURE__ */ jsx7(Box7, { marginTop: 1, children: /* @__PURE__ */ jsx7(Text7, { dimColor: true, children: "press q to quit" }) }),
2170
+ isRawModeSupported && /* @__PURE__ */ jsx7(KeyboardHandler, { onExit: exit })
2058
2171
  ] });
2059
2172
  }
2060
2173
 
@@ -2555,6 +2668,33 @@ function extractJson(text) {
2555
2668
  return text.slice(start, end + 1);
2556
2669
  }
2557
2670
 
2671
+ // src/types.ts
2672
+ var InterjectChannel = class {
2673
+ sender = null;
2674
+ _queue = [];
2675
+ /** Called by runClaude when a Claude step starts to activate direct delivery. */
2676
+ register(sender) {
2677
+ this.sender = sender;
2678
+ for (const msg of this._queue) sender(msg);
2679
+ this._queue = [];
2680
+ }
2681
+ /** Called by runClaude when a Claude step ends. */
2682
+ unregister() {
2683
+ this.sender = null;
2684
+ }
2685
+ /** Called by the TUI. Delivers immediately if a Claude step is running, else queues. */
2686
+ interject(message) {
2687
+ if (this.sender) this.sender(message);
2688
+ else this._queue.push(message);
2689
+ }
2690
+ /** Drains and returns any queued messages (for non-Claude steps to consume). */
2691
+ consumeQueue() {
2692
+ const q = this._queue.slice();
2693
+ this._queue = [];
2694
+ return q;
2695
+ }
2696
+ };
2697
+
2558
2698
  // src/index.ts
2559
2699
  var CURRENT_VERSION = JSON.parse(
2560
2700
  readFileSync6(
@@ -2709,7 +2849,8 @@ try {
2709
2849
  process.exit(1);
2710
2850
  }
2711
2851
  var options = { stepFilter, fromStep };
2712
- var rawEvents = runWorkflow(workflow, options);
2852
+ var channel = new InterjectChannel();
2853
+ var rawEvents = runWorkflow(workflow, options, channel);
2713
2854
  var logger = createLogger(resolveLogDir(filePath), workflow.goal);
2714
2855
  var events = withLogger(rawEvents, logger);
2715
2856
  var updateCheck = checkForUpdate(CURRENT_VERSION);
@@ -2749,7 +2890,13 @@ if (ciMode) {
2749
2890
  });
2750
2891
  } else {
2751
2892
  const inkApp = render(
2752
- React3.createElement(App, { workflow, events, options, updateCheck })
2893
+ React3.createElement(App, {
2894
+ workflow,
2895
+ events,
2896
+ options,
2897
+ updateCheck,
2898
+ interjectChannel: channel
2899
+ })
2753
2900
  );
2754
2901
  if (workflow.selfImprove) {
2755
2902
  inkApp.waitUntilExit().then(() => maybeRunRetrospective(filePath, workflow, logger)).catch(() => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "executant",
3
- "version": "1.13.0",
3
+ "version": "1.15.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",