executant 1.14.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 +251 -135
  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"];
@@ -1182,7 +1235,7 @@ function formatTaskElapsed(start, end, status) {
1182
1235
  }
1183
1236
 
1184
1237
  // src/ui/TaskRow.tsx
1185
- import { jsx, jsxs } from "react/jsx-runtime";
1238
+ import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
1186
1239
  function TaskRow({ taskState, isActive, index, tick }) {
1187
1240
  const { task, status, startTime, endTime } = taskState;
1188
1241
  const icon = statusIcon(status, tick);
@@ -1190,13 +1243,13 @@ function TaskRow({ taskState, isActive, index, tick }) {
1190
1243
  const elapsed = formatTaskElapsed(startTime, endTime, status);
1191
1244
  const iterInfo = formatIterCount(taskState.iterationHistory);
1192
1245
  const label = `${index + 1}. ${task.name}${iterInfo}`;
1193
- return /* @__PURE__ */ jsxs(Box, { children: [
1194
- /* @__PURE__ */ jsxs(Text, { color, children: [
1246
+ return /* @__PURE__ */ jsxs2(Box2, { children: [
1247
+ /* @__PURE__ */ jsxs2(Text2, { color, children: [
1195
1248
  icon,
1196
1249
  " "
1197
1250
  ] }),
1198
- /* @__PURE__ */ jsx(Text, { color: isActive ? theme.foreground : theme.muted, bold: isActive, children: label }),
1199
- /* @__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: [
1200
1253
  " ",
1201
1254
  elapsed
1202
1255
  ] })
@@ -1215,22 +1268,22 @@ function formatIterCount(history) {
1215
1268
  }
1216
1269
 
1217
1270
  // src/ui/IterationRow.tsx
1218
- import { Box as Box2, Text as Text2 } from "ink";
1219
- 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";
1220
1273
  function IterationRow({ record, tick }) {
1221
1274
  const icon = statusIcon(record.status, tick);
1222
1275
  const color = STATUS_COLOR[record.status] ?? theme.primary;
1223
1276
  const innerText = record.inner ? ` \u2014 ${stripItem(record.inner.name, record.item)} [${record.inner.index + 1}/${record.inner.total}]` : "";
1224
1277
  const ms = (record.endTime ?? Date.now()) - record.startTime;
1225
1278
  const elapsed = `${(ms / 1e3).toFixed(1)}s`;
1226
- return /* @__PURE__ */ jsxs2(Box2, { children: [
1227
- /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: " " }),
1228
- /* @__PURE__ */ jsx2(Text2, { color, children: icon }),
1229
- /* @__PURE__ */ jsx2(Text2, { children: " " }),
1230
- /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: `[${record.iteration}/${record.total}]` }),
1231
- /* @__PURE__ */ jsx2(Text2, { children: " " }),
1232
- /* @__PURE__ */ jsxs2(
1233
- 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,
1234
1287
  {
1235
1288
  color: record.status === "running" ? theme.foreground : theme.muted,
1236
1289
  children: [
@@ -1239,7 +1292,7 @@ function IterationRow({ record, tick }) {
1239
1292
  ]
1240
1293
  }
1241
1294
  ),
1242
- /* @__PURE__ */ jsxs2(Text2, { dimColor: true, children: [
1295
+ /* @__PURE__ */ jsxs3(Text3, { dimColor: true, children: [
1243
1296
  " ",
1244
1297
  elapsed
1245
1298
  ] })
@@ -1261,29 +1314,29 @@ function IterationList({
1261
1314
  if (isRepeatStyle(iterationHistory)) return null;
1262
1315
  const hidden = iterationHistory.length - maxVisible;
1263
1316
  const visible = iterationHistory.slice(-maxVisible);
1264
- return /* @__PURE__ */ jsxs2(Fragment, { children: [
1265
- hidden > 0 && /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: ` \u2026 ${hidden} earlier` }),
1266
- 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))
1267
1320
  ] });
1268
1321
  }
1269
1322
 
1270
1323
  // src/ui/LogPane.tsx
1271
- import { Box as Box3, Text as Text3 } from "ink";
1272
- 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";
1273
1326
  function LogPane({ lines, isActive = false, maxLines = 15 }) {
1274
1327
  const visible = lines.slice(-maxLines);
1275
1328
  if (visible.length === 0) {
1276
- 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" }) });
1277
1330
  }
1278
- return /* @__PURE__ */ jsx3(
1279
- Box3,
1331
+ return /* @__PURE__ */ jsx4(
1332
+ Box4,
1280
1333
  {
1281
1334
  flexDirection: "column",
1282
1335
  marginTop: 1,
1283
1336
  borderStyle: "single",
1284
1337
  borderColor: theme.border,
1285
1338
  paddingX: 1,
1286
- children: visible.map((line, i) => /* @__PURE__ */ jsx3(
1339
+ children: visible.map((line, i) => /* @__PURE__ */ jsx4(
1287
1340
  LogLine,
1288
1341
  {
1289
1342
  text: line,
@@ -1295,52 +1348,52 @@ function LogPane({ lines, isActive = false, maxLines = 15 }) {
1295
1348
  );
1296
1349
  }
1297
1350
  function LogLine({ text, cursor }) {
1298
- 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;
1299
1352
  if (/^\[[\w:]+\]/.test(text)) {
1300
1353
  const bracket = text.match(/^\[[\w:]+\]/)?.[0] ?? "";
1301
1354
  const rest = text.slice(bracket.length);
1302
- return /* @__PURE__ */ jsxs3(Text3, { children: [
1303
- /* @__PURE__ */ jsx3(Text3, { color: theme.primary, children: bracket }),
1304
- /* @__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 }),
1305
1358
  suffix
1306
1359
  ] });
1307
1360
  }
1308
1361
  if (/^\s*\$\s/.test(text))
1309
- return /* @__PURE__ */ jsxs3(Text3, { color: theme.warning, children: [
1362
+ return /* @__PURE__ */ jsxs4(Text4, { color: theme.warning, children: [
1310
1363
  text,
1311
1364
  suffix
1312
1365
  ] });
1313
1366
  if (text.startsWith("[warn]"))
1314
- return /* @__PURE__ */ jsxs3(Text3, { color: theme.warning, children: [
1367
+ return /* @__PURE__ */ jsxs4(Text4, { color: theme.warning, children: [
1315
1368
  text,
1316
1369
  suffix
1317
1370
  ] });
1318
1371
  if (text.startsWith("[error]"))
1319
- return /* @__PURE__ */ jsxs3(Text3, { color: theme.error, children: [
1372
+ return /* @__PURE__ */ jsxs4(Text4, { color: theme.error, children: [
1320
1373
  text,
1321
1374
  suffix
1322
1375
  ] });
1323
1376
  if (/^[\s]*(✓|✔|✅|done|success|compiled|built|passed)/i.test(text) && !/\b(error|fail|failed|warn|warning)\b/i.test(text))
1324
- return /* @__PURE__ */ jsxs3(Text3, { color: theme.success, children: [
1377
+ return /* @__PURE__ */ jsxs4(Text4, { color: theme.success, children: [
1325
1378
  text,
1326
1379
  suffix
1327
1380
  ] });
1328
1381
  if (/\b(error|failed|fail)\b/i.test(text))
1329
- return /* @__PURE__ */ jsxs3(Text3, { color: theme.error, children: [
1382
+ return /* @__PURE__ */ jsxs4(Text4, { color: theme.error, children: [
1330
1383
  text,
1331
1384
  suffix
1332
1385
  ] });
1333
1386
  if (/\b(warn|warning)\b/i.test(text))
1334
- return /* @__PURE__ */ jsxs3(Text3, { color: theme.warning, children: [
1387
+ return /* @__PURE__ */ jsxs4(Text4, { color: theme.warning, children: [
1335
1388
  text,
1336
1389
  suffix
1337
1390
  ] });
1338
1391
  if (/^[·…⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏]/.test(text))
1339
- return /* @__PURE__ */ jsxs3(Text3, { color: theme.muted, children: [
1392
+ return /* @__PURE__ */ jsxs4(Text4, { color: theme.muted, children: [
1340
1393
  text,
1341
1394
  suffix
1342
1395
  ] });
1343
- return /* @__PURE__ */ jsxs3(Text3, { children: [
1396
+ return /* @__PURE__ */ jsxs4(Text4, { children: [
1344
1397
  text,
1345
1398
  suffix
1346
1399
  ] });
@@ -1360,8 +1413,8 @@ function useInterval(callback, delayMs) {
1360
1413
  }
1361
1414
 
1362
1415
  // src/ui/BrandMark.tsx
1363
- import { Box as Box4, Text as Text4 } from "ink";
1364
- 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";
1365
1418
  var BRAND = "Executant";
1366
1419
  var SWEEP_TICKS = BRAND.length * 2;
1367
1420
  var GAP_TICKS = 30;
@@ -1374,15 +1427,22 @@ function charColor(charIndex, tick, isActive) {
1374
1427
  return charIndex === charPos ? theme.primaryLight : theme.primary;
1375
1428
  }
1376
1429
  function BrandMark({ tick, isActive }) {
1377
- 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)) });
1378
1431
  }
1379
1432
 
1380
1433
  // src/ui/App.tsx
1381
- import { jsx as jsx5, jsxs as jsxs4 } from "react/jsx-runtime";
1434
+ import { jsx as jsx6, jsxs as jsxs5 } from "react/jsx-runtime";
1382
1435
  var MAX_VISIBLE_ITERATIONS = 8;
1383
- 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
+ }) {
1384
1443
  const { exit } = useApp();
1385
1444
  const [state, dispatch] = useReducer(reducer, buildInitialState(workflow2));
1445
+ const [isInterjecting, setIsInterjecting] = useState2(false);
1386
1446
  useEffect2(() => {
1387
1447
  let active = true;
1388
1448
  (async () => {
@@ -1411,11 +1471,11 @@ function App({ workflow: workflow2, events: events2, options: options2, updateCh
1411
1471
  }, [events2, exit]);
1412
1472
  const { isRawModeSupported } = useStdin();
1413
1473
  const { stdout } = useStdout();
1414
- const [updateVersion, setUpdateVersion] = useState(null);
1474
+ const [updateVersion, setUpdateVersion] = useState2(null);
1415
1475
  useEffect2(() => {
1416
1476
  updateCheck2.then(setUpdateVersion);
1417
1477
  }, [updateCheck2]);
1418
- const [tick, setTick] = useState(0);
1478
+ const [tick, setTick] = useState2(0);
1419
1479
  useInterval(() => {
1420
1480
  if (!state.endTime) setTick((t) => t + 1);
1421
1481
  }, 100);
@@ -1426,7 +1486,7 @@ function App({ workflow: workflow2, events: events2, options: options2, updateCh
1426
1486
  runningTask?.iterationHistory,
1427
1487
  MAX_VISIBLE_ITERATIONS
1428
1488
  );
1429
- const FIXED_OVERHEAD = 12 + (updateVersion ? 1 : 0);
1489
+ const FIXED_OVERHEAD = 12 + (updateVersion ? 1 : 0) + (isInterjecting ? 1 : 0);
1430
1490
  const availableForTaskSection = Math.max(
1431
1491
  1,
1432
1492
  terminalRows - FIXED_OVERHEAD - LOG_PANE_MIN - iterationRowCount
@@ -1446,11 +1506,11 @@ function App({ workflow: workflow2, events: events2, options: options2, updateCh
1446
1506
  ).length;
1447
1507
  const totalCount = state.tasks.length;
1448
1508
  const filterInfo = options2?.stepFilter ? ` [step: ${options2.stepFilter}]` : options2?.fromStep ? ` [from step: ${options2.fromStep}]` : "";
1449
- return /* @__PURE__ */ jsxs4(Box5, { flexDirection: "column", padding: 1, children: [
1450
- /* @__PURE__ */ jsx5(Box5, { marginBottom: 1, children: /* @__PURE__ */ jsx5(BrandMark, { tick, isActive: !state.endTime }) }),
1451
- /* @__PURE__ */ jsxs4(Box5, { marginBottom: 1, children: [
1452
- /* @__PURE__ */ jsx5(Text5, { bold: true, color: theme.primary, children: workflow2.goal }),
1453
- /* @__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: [
1454
1514
  " ",
1455
1515
  completedCount,
1456
1516
  "/",
@@ -1460,8 +1520,8 @@ function App({ workflow: workflow2, events: events2, options: options2, updateCh
1460
1520
  filterInfo
1461
1521
  ] })
1462
1522
  ] }),
1463
- /* @__PURE__ */ jsxs4(Box5, { flexDirection: "column", marginBottom: 1, children: [
1464
- hiddenTaskCount > 0 && /* @__PURE__ */ jsxs4(Text5, { dimColor: true, children: [
1523
+ /* @__PURE__ */ jsxs5(Box6, { flexDirection: "column", marginBottom: 1, children: [
1524
+ hiddenTaskCount > 0 && /* @__PURE__ */ jsxs5(Text6, { dimColor: true, children: [
1465
1525
  " ",
1466
1526
  "\xB7\xB7\xB7 ",
1467
1527
  hiddenTaskCount,
@@ -1469,8 +1529,8 @@ function App({ workflow: workflow2, events: events2, options: options2, updateCh
1469
1529
  ] }),
1470
1530
  taskSlice.map((taskState, i) => {
1471
1531
  const globalIndex = hiddenTaskCount + i;
1472
- return /* @__PURE__ */ jsxs4(Box5, { flexDirection: "column", children: [
1473
- /* @__PURE__ */ jsx5(
1532
+ return /* @__PURE__ */ jsxs5(Box6, { flexDirection: "column", children: [
1533
+ /* @__PURE__ */ jsx6(
1474
1534
  TaskRow,
1475
1535
  {
1476
1536
  index: globalIndex,
@@ -1479,7 +1539,7 @@ function App({ workflow: workflow2, events: events2, options: options2, updateCh
1479
1539
  isActive: globalIndex === state.currentIndex
1480
1540
  }
1481
1541
  ),
1482
- taskState.status === "running" && taskState.iterationHistory?.length ? /* @__PURE__ */ jsx5(
1542
+ taskState.status === "running" && taskState.iterationHistory?.length ? /* @__PURE__ */ jsx6(
1483
1543
  IterationList,
1484
1544
  {
1485
1545
  iterationHistory: taskState.iterationHistory,
@@ -1490,7 +1550,7 @@ function App({ workflow: workflow2, events: events2, options: options2, updateCh
1490
1550
  ] }, globalIndex);
1491
1551
  })
1492
1552
  ] }),
1493
- activeTask && /* @__PURE__ */ jsx5(
1553
+ activeTask && /* @__PURE__ */ jsx6(
1494
1554
  LogPane,
1495
1555
  {
1496
1556
  lines: activeTask.lines,
@@ -1498,22 +1558,44 @@ function App({ workflow: workflow2, events: events2, options: options2, updateCh
1498
1558
  maxLines: logPaneMaxLines
1499
1559
  }
1500
1560
  ),
1501
- state.endTime !== void 0 && state.writtenFiles.length > 0 && /* @__PURE__ */ jsxs4(Box5, { flexDirection: "column", marginTop: 1, children: [
1502
- /* @__PURE__ */ jsx5(Text5, { dimColor: true, children: "files written:" }),
1503
- 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: [
1504
1564
  " ",
1505
1565
  f
1506
1566
  ] }, f))
1507
1567
  ] }),
1508
- /* @__PURE__ */ jsxs4(Box5, { marginTop: 1, flexDirection: "column", children: [
1509
- 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: [
1510
1585
  "v",
1511
1586
  updateVersion,
1512
1587
  " available \u2014 run: executant update"
1513
1588
  ] }),
1514
- /* @__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" })
1515
1590
  ] }),
1516
- 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
+ )
1517
1599
  ] });
1518
1600
  }
1519
1601
 
@@ -1922,9 +2004,9 @@ ${issues}`
1922
2004
  }
1923
2005
 
1924
2006
  // src/ui/PlanApp.tsx
1925
- import { useEffect as useEffect3, useReducer as useReducer2, useState as useState2 } from "react";
1926
- import { Box as Box6, Text as Text6, useApp as useApp2, useStdin as useStdin2 } from "ink";
1927
- 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";
1928
2010
  var truncate = (str, max) => str.length > max ? str.slice(0, max - 3) + "..." : str;
1929
2011
  function buildInitial(description) {
1930
2012
  return {
@@ -2000,7 +2082,7 @@ function StageProgress({ stage, totalStages, stageNames, tick, isActive, status
2000
2082
  }
2001
2083
  return { icon, color, name, bold, dim };
2002
2084
  });
2003
- 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: [
2004
2086
  " ",
2005
2087
  icon,
2006
2088
  name ? ` ${name}` : ""
@@ -2025,7 +2107,7 @@ function PlanApp({ description, events: events2 }) {
2025
2107
  };
2026
2108
  }, [events2, exit]);
2027
2109
  const { isRawModeSupported } = useStdin2();
2028
- const [tick, setTick] = useState2(0);
2110
+ const [tick, setTick] = useState3(0);
2029
2111
  const isActive = state.status === "running" || state.status === "retrying";
2030
2112
  useInterval(() => {
2031
2113
  if (isActive) setTick((t) => t + 1);
@@ -2033,19 +2115,19 @@ function PlanApp({ description, events: events2 }) {
2033
2115
  const elapsed = formatHeaderElapsed(state.startTime);
2034
2116
  const icon = isActive ? SPINNER[tick % SPINNER.length] : state.status === "complete" ? "\u2713" : "\u2717";
2035
2117
  const iconColor = state.status === "complete" ? theme.success : state.status === "error" ? theme.error : theme.primary;
2036
- return /* @__PURE__ */ jsxs5(Box6, { flexDirection: "column", padding: 1, children: [
2037
- /* @__PURE__ */ jsx6(Box6, { marginBottom: 1, children: /* @__PURE__ */ jsx6(BrandMark, { tick, isActive }) }),
2038
- /* @__PURE__ */ jsxs5(Box6, { marginBottom: 1, children: [
2039
- /* @__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: [
2040
2122
  icon,
2041
2123
  " "
2042
2124
  ] }),
2043
- /* @__PURE__ */ jsx6(Text6, { bold: true, color: theme.primary, children: "Generating plan" }),
2044
- /* @__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: [
2045
2127
  " ",
2046
2128
  elapsed
2047
2129
  ] }),
2048
- state.status === "retrying" && /* @__PURE__ */ jsxs5(Text6, { color: theme.warning, children: [
2130
+ state.status === "retrying" && /* @__PURE__ */ jsxs6(Text7, { color: theme.warning, children: [
2049
2131
  " ",
2050
2132
  "(attempt ",
2051
2133
  state.attempt,
@@ -2054,11 +2136,11 @@ function PlanApp({ description, events: events2 }) {
2054
2136
  ")"
2055
2137
  ] })
2056
2138
  ] }),
2057
- /* @__PURE__ */ jsxs5(Box6, { marginBottom: 1, children: [
2058
- /* @__PURE__ */ jsx6(Text6, { dimColor: true, children: " " }),
2059
- /* @__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) })
2060
2142
  ] }),
2061
- /* @__PURE__ */ jsx6(
2143
+ /* @__PURE__ */ jsx7(
2062
2144
  StageProgress,
2063
2145
  {
2064
2146
  stage: state.stage,
@@ -2069,23 +2151,23 @@ function PlanApp({ description, events: events2 }) {
2069
2151
  status: state.status
2070
2152
  }
2071
2153
  ),
2072
- state.lines.length > 0 && /* @__PURE__ */ jsx6(LogPane, { lines: state.lines, isActive, maxLines: 10 }),
2073
- state.status === "complete" && state.taskFile && /* @__PURE__ */ jsxs5(Box6, { flexDirection: "column", marginTop: 1, children: [
2074
- /* @__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: [
2075
2157
  "\u2705 Task plan saved: ",
2076
2158
  state.taskFile
2077
2159
  ] }),
2078
- state.preview && /* @__PURE__ */ jsxs5(Box6, { flexDirection: "column", marginTop: 1, children: [
2079
- /* @__PURE__ */ jsx6(Text6, { dimColor: true, children: "Preview:" }),
2080
- /* @__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 })
2081
2163
  ] })
2082
2164
  ] }),
2083
- 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: [
2084
2166
  "Error: ",
2085
2167
  state.errorMessage
2086
2168
  ] }) }),
2087
- isActive && /* @__PURE__ */ jsx6(Box6, { marginTop: 1, children: /* @__PURE__ */ jsx6(Text6, { dimColor: true, children: "press q to quit" }) }),
2088
- 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 })
2089
2171
  ] });
2090
2172
  }
2091
2173
 
@@ -2586,6 +2668,33 @@ function extractJson(text) {
2586
2668
  return text.slice(start, end + 1);
2587
2669
  }
2588
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
+
2589
2698
  // src/index.ts
2590
2699
  var CURRENT_VERSION = JSON.parse(
2591
2700
  readFileSync6(
@@ -2740,7 +2849,8 @@ try {
2740
2849
  process.exit(1);
2741
2850
  }
2742
2851
  var options = { stepFilter, fromStep };
2743
- var rawEvents = runWorkflow(workflow, options);
2852
+ var channel = new InterjectChannel();
2853
+ var rawEvents = runWorkflow(workflow, options, channel);
2744
2854
  var logger = createLogger(resolveLogDir(filePath), workflow.goal);
2745
2855
  var events = withLogger(rawEvents, logger);
2746
2856
  var updateCheck = checkForUpdate(CURRENT_VERSION);
@@ -2780,7 +2890,13 @@ if (ciMode) {
2780
2890
  });
2781
2891
  } else {
2782
2892
  const inkApp = render(
2783
- React3.createElement(App, { workflow, events, options, updateCheck })
2893
+ React3.createElement(App, {
2894
+ workflow,
2895
+ events,
2896
+ options,
2897
+ updateCheck,
2898
+ interjectChannel: channel
2899
+ })
2784
2900
  );
2785
2901
  if (workflow.selfImprove) {
2786
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.14.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",