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 +266 -134
- package/dist/prompts/dev-approach.txt +16 -0
- package/dist/prompts/development-methodology.txt +71 -0
- package/package.json +1 -1
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
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
958
|
-
|
|
959
|
-
|
|
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
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
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 =
|
|
1077
|
-
const
|
|
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/
|
|
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__ */
|
|
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__ */
|
|
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__ */
|
|
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__ */
|
|
1133
|
-
/* @__PURE__ */
|
|
1134
|
-
/* @__PURE__ */
|
|
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__ */
|
|
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__ */
|
|
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__ */
|
|
1253
|
+
if (text.startsWith("[error]")) return /* @__PURE__ */ jsxs3(Text3, { color: theme.error, children: [
|
|
1147
1254
|
text,
|
|
1148
1255
|
suffix
|
|
1149
1256
|
] });
|
|
1150
|
-
return /* @__PURE__ */
|
|
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
|
|
1171
|
-
import { jsx as
|
|
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__ */
|
|
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
|
|
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(
|
|
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(
|
|
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__ */
|
|
1230
|
-
/* @__PURE__ */
|
|
1231
|
-
/* @__PURE__ */
|
|
1232
|
-
/* @__PURE__ */
|
|
1233
|
-
/* @__PURE__ */
|
|
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__ */
|
|
1244
|
-
|
|
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
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
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__ */
|
|
1262
|
-
updateVersion && /* @__PURE__ */
|
|
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__ */
|
|
1395
|
+
/* @__PURE__ */ jsx5(Text5, { dimColor: true, children: "press q to quit" })
|
|
1268
1396
|
] }),
|
|
1269
|
-
isRawModeSupported && /* @__PURE__ */
|
|
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:
|
|
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
|
|
1676
|
-
import { jsx as
|
|
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__ */
|
|
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__ */
|
|
1786
|
-
/* @__PURE__ */
|
|
1787
|
-
/* @__PURE__ */
|
|
1788
|
-
/* @__PURE__ */
|
|
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__ */
|
|
1793
|
-
/* @__PURE__ */
|
|
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__ */
|
|
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__ */
|
|
1807
|
-
/* @__PURE__ */
|
|
1808
|
-
/* @__PURE__ */
|
|
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__ */
|
|
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__ */
|
|
1822
|
-
state.status === "complete" && state.taskFile && /* @__PURE__ */
|
|
1823
|
-
/* @__PURE__ */
|
|
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__ */
|
|
1828
|
-
/* @__PURE__ */
|
|
1829
|
-
/* @__PURE__ */
|
|
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__ */
|
|
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__ */
|
|
1837
|
-
isRawModeSupported && /* @__PURE__ */
|
|
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.
|