executant 1.7.0 → 1.9.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/README.md +19 -0
- package/dist/index.js +642 -293
- package/dist/prompts/plan-decompose.txt +40 -0
- package/dist/prompts/plan-judge.txt +3 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -113,22 +113,25 @@ function timestamp() {
|
|
|
113
113
|
|
|
114
114
|
// src/load-workflow.ts
|
|
115
115
|
import { z } from "zod";
|
|
116
|
-
var RawStepSchema = z.
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
116
|
+
var RawStepSchema = z.lazy(
|
|
117
|
+
() => z.object({
|
|
118
|
+
name: z.string(),
|
|
119
|
+
type: z.enum(["prompt", "script", "log", "command"]).optional(),
|
|
120
|
+
prompt: z.string().optional(),
|
|
121
|
+
command: z.string().optional(),
|
|
122
|
+
message: z.string().optional(),
|
|
123
|
+
continue_on_error: z.boolean().optional(),
|
|
124
|
+
self_healing: z.boolean().optional(),
|
|
125
|
+
max_healing_attempts: z.number().int().positive().optional(),
|
|
126
|
+
output: z.string().optional(),
|
|
127
|
+
llm_as_judge: z.boolean().optional(),
|
|
128
|
+
allowed_tools: z.array(z.string()).optional(),
|
|
129
|
+
forEach: z.union([z.array(z.string()), z.string()]).optional(),
|
|
130
|
+
repeat: z.number().int().positive().optional(),
|
|
131
|
+
context: z.array(z.string()).optional(),
|
|
132
|
+
steps: z.array(RawStepSchema).min(1).optional()
|
|
133
|
+
})
|
|
134
|
+
);
|
|
132
135
|
var RawWorkflowSchema = z.object({
|
|
133
136
|
goal: z.string(),
|
|
134
137
|
steps: z.array(RawStepSchema),
|
|
@@ -140,7 +143,9 @@ function loadWorkflow(filePath2) {
|
|
|
140
143
|
try {
|
|
141
144
|
raw = readFileSync2(filePath2, "utf8");
|
|
142
145
|
} catch (err) {
|
|
143
|
-
throw new Error(
|
|
146
|
+
throw new Error(
|
|
147
|
+
`Cannot read workflow file "${filePath2}": ${getErrorMessage(err)}`
|
|
148
|
+
);
|
|
144
149
|
}
|
|
145
150
|
let doc;
|
|
146
151
|
try {
|
|
@@ -164,16 +169,30 @@ function convertStep(step, vars) {
|
|
|
164
169
|
throw new Error(`Step "${name}" cannot have both repeat and forEach`);
|
|
165
170
|
}
|
|
166
171
|
if (step.repeat !== void 0 || step.forEach !== void 0) {
|
|
172
|
+
if (step.steps && (step.command || step.prompt || step.message)) {
|
|
173
|
+
throw new Error(
|
|
174
|
+
`Step "${name}" cannot have both steps and command/prompt/message`
|
|
175
|
+
);
|
|
176
|
+
}
|
|
167
177
|
const forEachValue = step.repeat !== void 0 ? Array.from({ length: step.repeat }, (_, i) => String(i + 1)) : step.forEach;
|
|
168
|
-
const
|
|
178
|
+
const stepWithoutLoop = {
|
|
179
|
+
...step,
|
|
180
|
+
repeat: void 0,
|
|
181
|
+
forEach: void 0,
|
|
182
|
+
steps: void 0
|
|
183
|
+
};
|
|
184
|
+
const inner = step.steps ? step.steps.map((s) => convertStep(s, vars)) : [convertInnerStep(stepWithoutLoop, vars, name, continueOnError)];
|
|
169
185
|
return {
|
|
170
186
|
type: "forEach",
|
|
171
187
|
name,
|
|
172
188
|
continueOnError,
|
|
173
189
|
forEach: forEachValue,
|
|
174
|
-
inner
|
|
190
|
+
inner
|
|
175
191
|
};
|
|
176
192
|
}
|
|
193
|
+
if (step.steps) {
|
|
194
|
+
throw new Error(`Step "${name}" has steps but no forEach or repeat`);
|
|
195
|
+
}
|
|
177
196
|
return convertInnerStep(step, vars, name, continueOnError);
|
|
178
197
|
}
|
|
179
198
|
function convertInnerStep(step, vars, name, continueOnError) {
|
|
@@ -181,7 +200,8 @@ function convertInnerStep(step, vars, name, continueOnError) {
|
|
|
181
200
|
switch (effectiveType) {
|
|
182
201
|
case "script":
|
|
183
202
|
case "command": {
|
|
184
|
-
if (!step.command)
|
|
203
|
+
if (!step.command)
|
|
204
|
+
throw new Error(`Step "${name}" has type script but no command`);
|
|
185
205
|
return {
|
|
186
206
|
type: "command",
|
|
187
207
|
name,
|
|
@@ -189,7 +209,9 @@ function convertInnerStep(step, vars, name, continueOnError) {
|
|
|
189
209
|
continueOnError,
|
|
190
210
|
selfHealing: step.self_healing === true,
|
|
191
211
|
maxHealingAttempts: step.max_healing_attempts,
|
|
192
|
-
...step.output && {
|
|
212
|
+
...step.output && {
|
|
213
|
+
output: resolveOutputFile(step.output, vars, name)
|
|
214
|
+
}
|
|
193
215
|
};
|
|
194
216
|
}
|
|
195
217
|
case "log": {
|
|
@@ -202,7 +224,8 @@ function convertInnerStep(step, vars, name, continueOnError) {
|
|
|
202
224
|
};
|
|
203
225
|
}
|
|
204
226
|
case "prompt": {
|
|
205
|
-
if (!step.prompt)
|
|
227
|
+
if (!step.prompt)
|
|
228
|
+
throw new Error(`Step "${name}" has type prompt but no prompt field`);
|
|
206
229
|
const contextFiles = resolveContextFiles(step.context, vars, name);
|
|
207
230
|
return {
|
|
208
231
|
type: "claude",
|
|
@@ -225,13 +248,17 @@ function inferType(step) {
|
|
|
225
248
|
}
|
|
226
249
|
function resolveVarPath(varName, vars, stepName, label) {
|
|
227
250
|
if (!(varName in vars)) {
|
|
228
|
-
throw new Error(
|
|
251
|
+
throw new Error(
|
|
252
|
+
`Step "${stepName}" ${label} references undefined var "${varName}" \u2014 add it to the vars section`
|
|
253
|
+
);
|
|
229
254
|
}
|
|
230
255
|
return vars[varName];
|
|
231
256
|
}
|
|
232
257
|
function resolveContextFiles(contextVarNames, vars, stepName) {
|
|
233
258
|
if (!contextVarNames || contextVarNames.length === 0) return [];
|
|
234
|
-
return contextVarNames.map(
|
|
259
|
+
return contextVarNames.map(
|
|
260
|
+
(varName) => resolveVarPath(varName, vars, stepName, "context")
|
|
261
|
+
);
|
|
235
262
|
}
|
|
236
263
|
function resolveOutputFile(varName, vars, stepName) {
|
|
237
264
|
return resolveVarPath(varName, vars, stepName, "output");
|
|
@@ -507,7 +534,7 @@ async function* runWorkflow(workflow2, options2 = {}) {
|
|
|
507
534
|
yield { type: "step:start", index: i, name: task.name };
|
|
508
535
|
try {
|
|
509
536
|
for await (const event of runStep(task)) {
|
|
510
|
-
if (event.type === "step:iteration" || event.type === "output:text" || event.type === "output:tool") {
|
|
537
|
+
if (event.type === "step:iteration" || event.type === "step:inner" || event.type === "output:text" || event.type === "output:tool") {
|
|
511
538
|
yield { ...event, index: i };
|
|
512
539
|
} else {
|
|
513
540
|
yield event;
|
|
@@ -568,20 +595,55 @@ async function* runLog(task) {
|
|
|
568
595
|
async function* runForEach(task) {
|
|
569
596
|
const items = await resolveItems(task.forEach);
|
|
570
597
|
const total = items.length;
|
|
598
|
+
const innerTotal = task.inner.length;
|
|
571
599
|
for (const [i, item] of items.entries()) {
|
|
572
600
|
yield { type: "step:iteration", index: -1, item, iteration: i + 1, total };
|
|
573
|
-
const
|
|
574
|
-
|
|
601
|
+
for (const [j, innerTask] of task.inner.entries()) {
|
|
602
|
+
const substituted = substituteItem(innerTask, item);
|
|
603
|
+
if (innerTotal > 1) {
|
|
604
|
+
yield {
|
|
605
|
+
type: "step:inner",
|
|
606
|
+
index: -1,
|
|
607
|
+
iteration: i + 1,
|
|
608
|
+
innerIndex: j,
|
|
609
|
+
innerTotal,
|
|
610
|
+
name: substituted.name
|
|
611
|
+
};
|
|
612
|
+
}
|
|
613
|
+
try {
|
|
614
|
+
yield* runStep(substituted);
|
|
615
|
+
} catch (err) {
|
|
616
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
617
|
+
if (!substituted.continueOnError) {
|
|
618
|
+
yield {
|
|
619
|
+
type: "log",
|
|
620
|
+
level: "warn",
|
|
621
|
+
text: `[forEach] Step "${substituted.name}" failed \u2014 aborting remaining children and iterations`
|
|
622
|
+
};
|
|
623
|
+
throw error;
|
|
624
|
+
}
|
|
625
|
+
yield {
|
|
626
|
+
type: "log",
|
|
627
|
+
level: "warn",
|
|
628
|
+
text: `[forEach] Step "${substituted.name}" failed (continuing): ${error.message}`
|
|
629
|
+
};
|
|
630
|
+
}
|
|
631
|
+
}
|
|
575
632
|
}
|
|
576
633
|
}
|
|
577
634
|
async function resolveItems(forEach) {
|
|
578
635
|
if (Array.isArray(forEach)) return forEach.filter(Boolean);
|
|
579
636
|
try {
|
|
580
|
-
const { stdout } = await execPromise(forEach, {
|
|
637
|
+
const { stdout } = await execPromise(forEach, {
|
|
638
|
+
shell: "/bin/sh",
|
|
639
|
+
timeout: 3e4
|
|
640
|
+
});
|
|
581
641
|
return stdout.split("\n").filter((l) => l.trim().length > 0);
|
|
582
642
|
} catch (err) {
|
|
583
|
-
throw new Error(
|
|
584
|
-
|
|
643
|
+
throw new Error(
|
|
644
|
+
`forEach shell command failed: ${getErrorMessage(err)}
|
|
645
|
+
Command: ${forEach}`
|
|
646
|
+
);
|
|
585
647
|
}
|
|
586
648
|
}
|
|
587
649
|
function substituteItem(task, item) {
|
|
@@ -590,12 +652,24 @@ function substituteItem(task, item) {
|
|
|
590
652
|
case "command":
|
|
591
653
|
return { ...task, name: sub(task.name), command: sub(task.command) };
|
|
592
654
|
case "claude":
|
|
593
|
-
return {
|
|
655
|
+
return {
|
|
656
|
+
...task,
|
|
657
|
+
name: sub(task.name),
|
|
658
|
+
prompt: sub(task.prompt),
|
|
659
|
+
allowedTools: task.allowedTools?.map(sub)
|
|
660
|
+
};
|
|
594
661
|
case "log":
|
|
595
662
|
return { ...task, name: sub(task.name), message: sub(task.message) };
|
|
663
|
+
case "forEach":
|
|
664
|
+
return {
|
|
665
|
+
...task,
|
|
666
|
+
name: sub(task.name),
|
|
667
|
+
forEach: Array.isArray(task.forEach) ? task.forEach : sub(task.forEach),
|
|
668
|
+
inner: task.inner.map((t) => substituteItem(t, item))
|
|
669
|
+
};
|
|
596
670
|
default: {
|
|
597
671
|
const _ = task;
|
|
598
|
-
throw new Error(`Unknown
|
|
672
|
+
throw new Error(`Unknown task type: ${JSON.stringify(_)}`);
|
|
599
673
|
}
|
|
600
674
|
}
|
|
601
675
|
}
|
|
@@ -607,7 +681,11 @@ async function* runCommandWithHealing(task) {
|
|
|
607
681
|
try {
|
|
608
682
|
yield* collectLines(runCommand(task), lines);
|
|
609
683
|
if (attempt > 0) {
|
|
610
|
-
yield {
|
|
684
|
+
yield {
|
|
685
|
+
type: "log",
|
|
686
|
+
level: "info",
|
|
687
|
+
text: `[self-healing] Command passed after ${attempt + 1} attempts`
|
|
688
|
+
};
|
|
611
689
|
}
|
|
612
690
|
return;
|
|
613
691
|
} catch (err) {
|
|
@@ -615,7 +693,11 @@ async function* runCommandWithHealing(task) {
|
|
|
615
693
|
const output = lines.join("\n");
|
|
616
694
|
const remaining = maxAttempts - attempt - 1;
|
|
617
695
|
if (remaining === 0) {
|
|
618
|
-
yield {
|
|
696
|
+
yield {
|
|
697
|
+
type: "log",
|
|
698
|
+
level: "warn",
|
|
699
|
+
text: `[self-healing] Exhausted ${maxAttempts} attempts`
|
|
700
|
+
};
|
|
619
701
|
throw new Error(
|
|
620
702
|
`Step "${task.name}" failed after ${maxAttempts} self-healing attempts (last exit code: ${exitCode})`
|
|
621
703
|
);
|
|
@@ -626,7 +708,12 @@ async function* runCommandWithHealing(task) {
|
|
|
626
708
|
text: `[self-healing] Attempt ${attempt + 1}/${maxAttempts} failed (exit ${exitCode}), invoking Claude to fix\u2026`
|
|
627
709
|
};
|
|
628
710
|
const historyBlock = buildAttemptHistory(attemptHistory);
|
|
629
|
-
const healPrompt = buildHealingPrompt(
|
|
711
|
+
const healPrompt = buildHealingPrompt(
|
|
712
|
+
task.command,
|
|
713
|
+
exitCode,
|
|
714
|
+
output,
|
|
715
|
+
historyBlock
|
|
716
|
+
);
|
|
630
717
|
const healTask = {
|
|
631
718
|
type: "claude",
|
|
632
719
|
name: `${task.name}:heal-${attempt + 1}`,
|
|
@@ -637,7 +724,8 @@ async function* runCommandWithHealing(task) {
|
|
|
637
724
|
const claudeLines = [];
|
|
638
725
|
for await (const event of runClaude(healTask)) {
|
|
639
726
|
if (event.type === "output:text") claudeLines.push(event.text);
|
|
640
|
-
else if (event.type === "output:tool")
|
|
727
|
+
else if (event.type === "output:tool")
|
|
728
|
+
toolCalls.push(formatToolCall(event.tool, event.input));
|
|
641
729
|
yield event;
|
|
642
730
|
}
|
|
643
731
|
attemptHistory.push({
|
|
@@ -645,7 +733,11 @@ async function* runCommandWithHealing(task) {
|
|
|
645
733
|
exitCode,
|
|
646
734
|
cmdOutput: output
|
|
647
735
|
});
|
|
648
|
-
yield {
|
|
736
|
+
yield {
|
|
737
|
+
type: "log",
|
|
738
|
+
level: "info",
|
|
739
|
+
text: `[self-healing] Re-running command (${remaining} attempt(s) left)\u2026`
|
|
740
|
+
};
|
|
649
741
|
}
|
|
650
742
|
}
|
|
651
743
|
}
|
|
@@ -657,21 +749,37 @@ async function* runClaudeWithJudge(task) {
|
|
|
657
749
|
${fillTemplate(JUDGE_RETRY_CONTEXT, { FEEDBACK: judgeContext })}`;
|
|
658
750
|
const lines = [];
|
|
659
751
|
yield* collectLines(runClaude({ ...task, prompt }), lines);
|
|
660
|
-
yield {
|
|
661
|
-
|
|
752
|
+
yield {
|
|
753
|
+
type: "log",
|
|
754
|
+
level: "info",
|
|
755
|
+
text: `[judge] Evaluating "${task.name}"\u2026`
|
|
756
|
+
};
|
|
757
|
+
const verdict = await evaluateWithJudge(
|
|
758
|
+
task.name,
|
|
759
|
+
task.prompt,
|
|
760
|
+
lines.join("\n")
|
|
761
|
+
);
|
|
662
762
|
if (verdict.pass) {
|
|
663
763
|
yield { type: "log", level: "info", text: "[judge] PASS" };
|
|
664
764
|
return;
|
|
665
765
|
}
|
|
666
766
|
judgeContext = verdict.feedback;
|
|
667
|
-
yield {
|
|
767
|
+
yield {
|
|
768
|
+
type: "log",
|
|
769
|
+
level: "warn",
|
|
770
|
+
text: `[judge] FAIL \u2014 ${verdict.feedback}`
|
|
771
|
+
};
|
|
668
772
|
const remaining = MAX_JUDGE_RETRIES - attempt - 1;
|
|
669
773
|
if (remaining === 0) {
|
|
670
774
|
throw new Error(
|
|
671
775
|
`Step "${task.name}" failed judge evaluation after ${MAX_JUDGE_RETRIES} attempts`
|
|
672
776
|
);
|
|
673
777
|
}
|
|
674
|
-
yield {
|
|
778
|
+
yield {
|
|
779
|
+
type: "log",
|
|
780
|
+
level: "info",
|
|
781
|
+
text: `[judge] Retrying (${remaining} attempt(s) left)\u2026`
|
|
782
|
+
};
|
|
675
783
|
}
|
|
676
784
|
}
|
|
677
785
|
async function evaluateWithJudge(stepName, stepInstructions, output) {
|
|
@@ -698,7 +806,9 @@ function readContextFile(filePath2) {
|
|
|
698
806
|
try {
|
|
699
807
|
return readFileSync3(filePath2, "utf8");
|
|
700
808
|
} catch (err) {
|
|
701
|
-
throw new Error(
|
|
809
|
+
throw new Error(
|
|
810
|
+
`Context file "${filePath2}" could not be read: ${getErrorMessage(err)}`
|
|
811
|
+
);
|
|
702
812
|
}
|
|
703
813
|
}
|
|
704
814
|
function expandContext(task) {
|
|
@@ -712,13 +822,23 @@ ${readContextFile(fp)}
|
|
|
712
822
|
${task.prompt}` };
|
|
713
823
|
}
|
|
714
824
|
function buildHealingPrompt(command, exitCode, output, attemptHistory) {
|
|
715
|
-
return fillTemplate(SELF_HEALING_PROMPT, {
|
|
825
|
+
return fillTemplate(SELF_HEALING_PROMPT, {
|
|
826
|
+
COMMAND: command,
|
|
827
|
+
EXIT_CODE: String(exitCode),
|
|
828
|
+
OUTPUT: output,
|
|
829
|
+
ATTEMPT_HISTORY: attemptHistory
|
|
830
|
+
});
|
|
716
831
|
}
|
|
717
832
|
function buildJudgePrompt(stepName, instructions, output) {
|
|
718
|
-
return fillTemplate(JUDGE_EVALUATION_PROMPT, {
|
|
833
|
+
return fillTemplate(JUDGE_EVALUATION_PROMPT, {
|
|
834
|
+
STEP_NAME: stepName,
|
|
835
|
+
STEP_INSTRUCTIONS: instructions,
|
|
836
|
+
OUTPUT: output
|
|
837
|
+
});
|
|
719
838
|
}
|
|
720
839
|
function formatToolCall(tool, input) {
|
|
721
|
-
if (tool === "Edit" || tool === "Write")
|
|
840
|
+
if (tool === "Edit" || tool === "Write")
|
|
841
|
+
return `${tool}(${String(input["file_path"] ?? "")})`;
|
|
722
842
|
if (tool === "Bash") return `Bash(${String(input["command"] ?? "")})`;
|
|
723
843
|
return tool;
|
|
724
844
|
}
|
|
@@ -743,7 +863,7 @@ init_update();
|
|
|
743
863
|
|
|
744
864
|
// src/ui/App.tsx
|
|
745
865
|
import { useEffect as useEffect2, useReducer, useState } from "react";
|
|
746
|
-
import { Box as
|
|
866
|
+
import { Box as Box5, Text as Text5, useApp, useStdin } from "ink";
|
|
747
867
|
|
|
748
868
|
// src/ui/KeyboardHandler.tsx
|
|
749
869
|
import { useInput } from "ink";
|
|
@@ -811,32 +931,70 @@ function reducer(state, event) {
|
|
|
811
931
|
status: "running",
|
|
812
932
|
startTime: Date.now()
|
|
813
933
|
});
|
|
814
|
-
case "step:complete":
|
|
934
|
+
case "step:complete": {
|
|
935
|
+
const prev = state.tasks[event.index]?.iterationHistory;
|
|
936
|
+
const iterationHistory = prev?.length ? prev.map(
|
|
937
|
+
(r) => r.status === "running" ? { ...r, status: "complete", endTime: Date.now() } : r
|
|
938
|
+
) : void 0;
|
|
815
939
|
return {
|
|
816
940
|
...updateTask(state, event.index, {
|
|
817
941
|
status: "complete",
|
|
818
|
-
endTime: Date.now()
|
|
942
|
+
endTime: Date.now(),
|
|
943
|
+
...iterationHistory ? { iterationHistory } : {}
|
|
819
944
|
}),
|
|
820
945
|
currentIndex: event.index + 1
|
|
821
946
|
};
|
|
822
|
-
|
|
947
|
+
}
|
|
948
|
+
case "step:error": {
|
|
949
|
+
const prev = state.tasks[event.index]?.iterationHistory;
|
|
950
|
+
const iterationHistory = prev?.length ? prev.map(
|
|
951
|
+
(r) => r.status === "running" ? { ...r, status: "error", endTime: Date.now() } : r
|
|
952
|
+
) : void 0;
|
|
823
953
|
return {
|
|
824
954
|
...updateTask(state, event.index, {
|
|
825
955
|
status: "error",
|
|
826
956
|
endTime: Date.now(),
|
|
827
|
-
error: event.error
|
|
957
|
+
error: event.error,
|
|
958
|
+
...iterationHistory ? { iterationHistory } : {}
|
|
828
959
|
}),
|
|
829
960
|
currentIndex: event.index + 1
|
|
830
961
|
};
|
|
962
|
+
}
|
|
831
963
|
case "step:skip":
|
|
832
964
|
return {
|
|
833
965
|
...updateTask(state, event.index, { status: "skipped" }),
|
|
834
966
|
currentIndex: event.index + 1
|
|
835
967
|
};
|
|
836
|
-
case "step:iteration":
|
|
968
|
+
case "step:iteration": {
|
|
969
|
+
const prev = (state.tasks[event.index]?.iterationHistory ?? []).map(
|
|
970
|
+
(r) => r.status === "running" ? { ...r, status: "complete", endTime: Date.now() } : r
|
|
971
|
+
);
|
|
837
972
|
return updateTask(state, event.index, {
|
|
838
|
-
|
|
973
|
+
iterationHistory: [
|
|
974
|
+
...prev,
|
|
975
|
+
{
|
|
976
|
+
item: event.item,
|
|
977
|
+
iteration: event.iteration,
|
|
978
|
+
total: event.total,
|
|
979
|
+
status: "running",
|
|
980
|
+
startTime: Date.now()
|
|
981
|
+
}
|
|
982
|
+
]
|
|
839
983
|
});
|
|
984
|
+
}
|
|
985
|
+
case "step:inner": {
|
|
986
|
+
const iterationHistory = (state.tasks[event.index]?.iterationHistory ?? []).map(
|
|
987
|
+
(r) => r.status === "running" ? {
|
|
988
|
+
...r,
|
|
989
|
+
inner: {
|
|
990
|
+
index: event.innerIndex,
|
|
991
|
+
total: event.innerTotal,
|
|
992
|
+
name: event.name
|
|
993
|
+
}
|
|
994
|
+
} : r
|
|
995
|
+
);
|
|
996
|
+
return updateTask(state, event.index, { iterationHistory });
|
|
997
|
+
}
|
|
840
998
|
case "output:text": {
|
|
841
999
|
const idx = event.index;
|
|
842
1000
|
if (idx >= state.tasks.length) return state;
|
|
@@ -848,7 +1006,10 @@ function reducer(state, event) {
|
|
|
848
1006
|
const formatted = formatToolCall2(event.tool, event.input);
|
|
849
1007
|
const next = formatted ? appendLine(state, idx, formatted) : state;
|
|
850
1008
|
if (event.tool === "Write" && typeof event.input["file_path"] === "string") {
|
|
851
|
-
return {
|
|
1009
|
+
return {
|
|
1010
|
+
...next,
|
|
1011
|
+
writtenFiles: [...next.writtenFiles, event.input["file_path"]]
|
|
1012
|
+
};
|
|
852
1013
|
}
|
|
853
1014
|
return next;
|
|
854
1015
|
}
|
|
@@ -871,7 +1032,9 @@ function reducer(state, event) {
|
|
|
871
1032
|
}
|
|
872
1033
|
}
|
|
873
1034
|
function updateTask(state, index, patch) {
|
|
874
|
-
const tasks = state.tasks.map(
|
|
1035
|
+
const tasks = state.tasks.map(
|
|
1036
|
+
(t, i) => i === index ? { ...t, ...patch } : t
|
|
1037
|
+
);
|
|
875
1038
|
return { ...state, tasks };
|
|
876
1039
|
}
|
|
877
1040
|
function appendLine(state, index, line) {
|
|
@@ -886,20 +1049,6 @@ function appendLine(state, index, line) {
|
|
|
886
1049
|
// src/ui/TaskRow.tsx
|
|
887
1050
|
import { Box, Text } from "ink";
|
|
888
1051
|
|
|
889
|
-
// src/ui/utils.ts
|
|
890
|
-
var SPINNER = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
|
|
891
|
-
var EXIT_DELAY_MS = 300;
|
|
892
|
-
function formatHeaderElapsed(start, end) {
|
|
893
|
-
const ms = (end ?? Date.now()) - start;
|
|
894
|
-
return ms < 1e3 ? `${ms}ms` : `${(ms / 1e3).toFixed(1)}s`;
|
|
895
|
-
}
|
|
896
|
-
function formatTaskElapsed(start, end, status) {
|
|
897
|
-
if (!start) return "";
|
|
898
|
-
const ms = (end ?? Date.now()) - start;
|
|
899
|
-
if (status === "running" || status === "complete" || status === "error") return `${(ms / 1e3).toFixed(1)}s`;
|
|
900
|
-
return "";
|
|
901
|
-
}
|
|
902
|
-
|
|
903
1052
|
// src/ui/theme.ts
|
|
904
1053
|
import { createRequire } from "node:module";
|
|
905
1054
|
import { oklchToHex } from "@coston/design-tokens";
|
|
@@ -928,6 +1077,32 @@ var theme = {
|
|
|
928
1077
|
// log pane border
|
|
929
1078
|
};
|
|
930
1079
|
|
|
1080
|
+
// src/ui/utils.ts
|
|
1081
|
+
var SPINNER = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
|
|
1082
|
+
var STATUS_ICON = {
|
|
1083
|
+
complete: "\u2713",
|
|
1084
|
+
error: "\u2717",
|
|
1085
|
+
skipped: "\u2298",
|
|
1086
|
+
pending: "\xB7"
|
|
1087
|
+
};
|
|
1088
|
+
var STATUS_COLOR = {
|
|
1089
|
+
complete: theme.success,
|
|
1090
|
+
error: theme.error,
|
|
1091
|
+
pending: theme.muted
|
|
1092
|
+
};
|
|
1093
|
+
var EXIT_DELAY_MS = 300;
|
|
1094
|
+
function formatHeaderElapsed(start, end) {
|
|
1095
|
+
const ms = (end ?? Date.now()) - start;
|
|
1096
|
+
return ms < 1e3 ? `${ms}ms` : `${(ms / 1e3).toFixed(1)}s`;
|
|
1097
|
+
}
|
|
1098
|
+
function formatTaskElapsed(start, end, status) {
|
|
1099
|
+
if (!start) return "";
|
|
1100
|
+
const ms = (end ?? Date.now()) - start;
|
|
1101
|
+
if (status === "running" || status === "complete" || status === "error")
|
|
1102
|
+
return `${(ms / 1e3).toFixed(1)}s`;
|
|
1103
|
+
return "";
|
|
1104
|
+
}
|
|
1105
|
+
|
|
931
1106
|
// src/ui/TaskRow.tsx
|
|
932
1107
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
933
1108
|
function TaskRow({ taskState, isActive, index, tick }) {
|
|
@@ -935,7 +1110,7 @@ function TaskRow({ taskState, isActive, index, tick }) {
|
|
|
935
1110
|
const icon = statusIcon(status, tick);
|
|
936
1111
|
const color = statusColor(status, isActive);
|
|
937
1112
|
const elapsed = formatTaskElapsed(startTime, endTime, status);
|
|
938
|
-
const iterInfo =
|
|
1113
|
+
const iterInfo = formatIterCount(taskState.iterationHistory);
|
|
939
1114
|
const label = `${index + 1}. ${task.name}${iterInfo}`;
|
|
940
1115
|
return /* @__PURE__ */ jsxs(Box, { children: [
|
|
941
1116
|
/* @__PURE__ */ jsxs(Text, { color, children: [
|
|
@@ -949,17 +1124,6 @@ function TaskRow({ taskState, isActive, index, tick }) {
|
|
|
949
1124
|
] })
|
|
950
1125
|
] });
|
|
951
1126
|
}
|
|
952
|
-
var STATUS_ICON = {
|
|
953
|
-
complete: "\u2713",
|
|
954
|
-
error: "\u2717",
|
|
955
|
-
skipped: "\u2298",
|
|
956
|
-
pending: "\xB7"
|
|
957
|
-
};
|
|
958
|
-
var STATUS_COLOR = {
|
|
959
|
-
complete: theme.success,
|
|
960
|
-
error: theme.error,
|
|
961
|
-
pending: theme.muted
|
|
962
|
-
};
|
|
963
1127
|
function statusIcon(status, tick) {
|
|
964
1128
|
return status === "running" ? SPINNER[tick % SPINNER.length] : STATUS_ICON[status] ?? "\xB7";
|
|
965
1129
|
}
|
|
@@ -967,16 +1131,74 @@ function statusColor(status, isActive) {
|
|
|
967
1131
|
if (isActive && status === "running") return theme.primary;
|
|
968
1132
|
return STATUS_COLOR[status] ?? theme.foreground;
|
|
969
1133
|
}
|
|
1134
|
+
function formatIterCount(history) {
|
|
1135
|
+
if (!history?.length) return "";
|
|
1136
|
+
const total = history[0].total;
|
|
1137
|
+
const running = history.find((r) => r.status === "running");
|
|
1138
|
+
const current = running?.iteration ?? history.length;
|
|
1139
|
+
return ` (${current}/${total})`;
|
|
1140
|
+
}
|
|
970
1141
|
|
|
971
|
-
// src/ui/
|
|
1142
|
+
// src/ui/IterationRow.tsx
|
|
972
1143
|
import { Box as Box2, Text as Text2 } from "ink";
|
|
973
|
-
import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
1144
|
+
import { Fragment, jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
1145
|
+
function IterationRow({ record, tick }) {
|
|
1146
|
+
const icon = record.status === "running" ? SPINNER[tick % SPINNER.length] : STATUS_ICON[record.status] ?? "\xB7";
|
|
1147
|
+
const color = STATUS_COLOR[record.status] ?? theme.primary;
|
|
1148
|
+
const innerText = record.inner ? ` \u2014 ${stripItem(record.inner.name, record.item)} [${record.inner.index + 1}/${record.inner.total}]` : "";
|
|
1149
|
+
const ms = (record.endTime ?? Date.now()) - record.startTime;
|
|
1150
|
+
const elapsed = `${(ms / 1e3).toFixed(1)}s`;
|
|
1151
|
+
return /* @__PURE__ */ jsxs2(Box2, { children: [
|
|
1152
|
+
/* @__PURE__ */ jsx2(Text2, { dimColor: true, children: " " }),
|
|
1153
|
+
/* @__PURE__ */ jsx2(Text2, { color, children: icon }),
|
|
1154
|
+
/* @__PURE__ */ jsx2(Text2, { children: " " }),
|
|
1155
|
+
/* @__PURE__ */ jsxs2(
|
|
1156
|
+
Text2,
|
|
1157
|
+
{
|
|
1158
|
+
color: record.status === "running" ? theme.foreground : theme.muted,
|
|
1159
|
+
children: [
|
|
1160
|
+
record.item,
|
|
1161
|
+
innerText
|
|
1162
|
+
]
|
|
1163
|
+
}
|
|
1164
|
+
),
|
|
1165
|
+
/* @__PURE__ */ jsxs2(Text2, { dimColor: true, children: [
|
|
1166
|
+
" ",
|
|
1167
|
+
elapsed
|
|
1168
|
+
] })
|
|
1169
|
+
] });
|
|
1170
|
+
}
|
|
1171
|
+
function stripItem(name, item) {
|
|
1172
|
+
if (!name.includes(item)) return name;
|
|
1173
|
+
const stripped = name.replace(item, "").replace(/\s{2,}/g, " ").replace(/^[\s\-—–]+/, "").replace(/[\s\-—–]+$/, "").trim();
|
|
1174
|
+
return stripped || name;
|
|
1175
|
+
}
|
|
1176
|
+
function isRepeatStyle(history) {
|
|
1177
|
+
return history.every((r) => r.item === String(r.iteration));
|
|
1178
|
+
}
|
|
1179
|
+
function IterationList({
|
|
1180
|
+
iterationHistory,
|
|
1181
|
+
tick,
|
|
1182
|
+
maxVisible
|
|
1183
|
+
}) {
|
|
1184
|
+
if (isRepeatStyle(iterationHistory)) return null;
|
|
1185
|
+
const hidden = iterationHistory.length - maxVisible;
|
|
1186
|
+
const visible = iterationHistory.slice(-maxVisible);
|
|
1187
|
+
return /* @__PURE__ */ jsxs2(Fragment, { children: [
|
|
1188
|
+
hidden > 0 && /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: ` \u2026 ${hidden} earlier` }),
|
|
1189
|
+
visible.map((record) => /* @__PURE__ */ jsx2(IterationRow, { record, tick }, record.iteration))
|
|
1190
|
+
] });
|
|
1191
|
+
}
|
|
1192
|
+
|
|
1193
|
+
// src/ui/LogPane.tsx
|
|
1194
|
+
import { Box as Box3, Text as Text3 } from "ink";
|
|
1195
|
+
import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
|
|
974
1196
|
function LogPane({ lines, isActive = false, maxLines = 15 }) {
|
|
975
1197
|
const visible = lines.slice(-maxLines);
|
|
976
1198
|
if (visible.length === 0) {
|
|
977
|
-
return /* @__PURE__ */
|
|
1199
|
+
return /* @__PURE__ */ jsx3(Box3, { marginTop: 1, children: /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: isActive ? "\u2838 waiting for output\u2026" : "\u2014 no output yet \u2014" }) });
|
|
978
1200
|
}
|
|
979
|
-
return /* @__PURE__ */
|
|
1201
|
+
return /* @__PURE__ */ jsx3(Box3, { flexDirection: "column", marginTop: 1, borderStyle: "single", borderColor: theme.border, paddingX: 1, children: visible.map((line, i) => /* @__PURE__ */ jsx3(
|
|
980
1202
|
LogLine,
|
|
981
1203
|
{
|
|
982
1204
|
text: line,
|
|
@@ -986,29 +1208,29 @@ function LogPane({ lines, isActive = false, maxLines = 15 }) {
|
|
|
986
1208
|
)) });
|
|
987
1209
|
}
|
|
988
1210
|
function LogLine({ text, cursor }) {
|
|
989
|
-
const suffix = cursor ? /* @__PURE__ */
|
|
1211
|
+
const suffix = cursor ? /* @__PURE__ */ jsx3(Text3, { color: theme.primary, children: " \u258C" }) : null;
|
|
990
1212
|
if (/^\[[\w:]+\]/.test(text)) {
|
|
991
1213
|
const bracket = text.match(/^\[[\w:]+\]/)?.[0] ?? "";
|
|
992
1214
|
const rest = text.slice(bracket.length);
|
|
993
|
-
return /* @__PURE__ */
|
|
994
|
-
/* @__PURE__ */
|
|
995
|
-
/* @__PURE__ */
|
|
1215
|
+
return /* @__PURE__ */ jsxs3(Text3, { children: [
|
|
1216
|
+
/* @__PURE__ */ jsx3(Text3, { color: theme.primary, children: bracket }),
|
|
1217
|
+
/* @__PURE__ */ jsx3(Text3, { children: rest }),
|
|
996
1218
|
suffix
|
|
997
1219
|
] });
|
|
998
1220
|
}
|
|
999
|
-
if (/^\s*\$\s/.test(text)) return /* @__PURE__ */
|
|
1221
|
+
if (/^\s*\$\s/.test(text)) return /* @__PURE__ */ jsxs3(Text3, { color: theme.warning, children: [
|
|
1000
1222
|
text,
|
|
1001
1223
|
suffix
|
|
1002
1224
|
] });
|
|
1003
|
-
if (text.startsWith("[warn]")) return /* @__PURE__ */
|
|
1225
|
+
if (text.startsWith("[warn]")) return /* @__PURE__ */ jsxs3(Text3, { color: theme.warning, children: [
|
|
1004
1226
|
text,
|
|
1005
1227
|
suffix
|
|
1006
1228
|
] });
|
|
1007
|
-
if (text.startsWith("[error]")) return /* @__PURE__ */
|
|
1229
|
+
if (text.startsWith("[error]")) return /* @__PURE__ */ jsxs3(Text3, { color: theme.error, children: [
|
|
1008
1230
|
text,
|
|
1009
1231
|
suffix
|
|
1010
1232
|
] });
|
|
1011
|
-
return /* @__PURE__ */
|
|
1233
|
+
return /* @__PURE__ */ jsxs3(Text3, { children: [
|
|
1012
1234
|
text,
|
|
1013
1235
|
suffix
|
|
1014
1236
|
] });
|
|
@@ -1028,8 +1250,8 @@ function useInterval(callback, delayMs) {
|
|
|
1028
1250
|
}
|
|
1029
1251
|
|
|
1030
1252
|
// src/ui/BrandMark.tsx
|
|
1031
|
-
import { Box as
|
|
1032
|
-
import { jsx as
|
|
1253
|
+
import { Box as Box4, Text as Text4 } from "ink";
|
|
1254
|
+
import { jsx as jsx4 } from "react/jsx-runtime";
|
|
1033
1255
|
var BRAND = "Executant";
|
|
1034
1256
|
var SWEEP_TICKS = BRAND.length * 2;
|
|
1035
1257
|
var GAP_TICKS = 30;
|
|
@@ -1042,11 +1264,12 @@ function charColor(charIndex, tick, isActive) {
|
|
|
1042
1264
|
return charIndex === charPos ? theme.primaryLight : theme.primary;
|
|
1043
1265
|
}
|
|
1044
1266
|
function BrandMark({ tick, isActive }) {
|
|
1045
|
-
return /* @__PURE__ */
|
|
1267
|
+
return /* @__PURE__ */ jsx4(Box4, { children: [...BRAND].map((char, i) => /* @__PURE__ */ jsx4(Text4, { color: charColor(i, tick, isActive), bold: true, children: char }, i)) });
|
|
1046
1268
|
}
|
|
1047
1269
|
|
|
1048
1270
|
// src/ui/App.tsx
|
|
1049
|
-
import { jsx as
|
|
1271
|
+
import { jsx as jsx5, jsxs as jsxs4 } from "react/jsx-runtime";
|
|
1272
|
+
var MAX_VISIBLE_ITERATIONS = 8;
|
|
1050
1273
|
function App({ workflow: workflow2, events: events2, options: options2, updateCheck: updateCheck2 }) {
|
|
1051
1274
|
const { exit } = useApp();
|
|
1052
1275
|
const [state, dispatch] = useReducer(reducer, buildInitialState(workflow2));
|
|
@@ -1064,7 +1287,10 @@ function App({ workflow: workflow2, events: events2, options: options2, updateCh
|
|
|
1064
1287
|
} catch (err) {
|
|
1065
1288
|
if (!active) return;
|
|
1066
1289
|
dispatch({ type: "log", level: "error", text: getErrorMessage(err) });
|
|
1067
|
-
setTimeout(
|
|
1290
|
+
setTimeout(
|
|
1291
|
+
() => exit(err instanceof Error ? err : new Error(getErrorMessage(err))),
|
|
1292
|
+
EXIT_DELAY_MS
|
|
1293
|
+
);
|
|
1068
1294
|
}
|
|
1069
1295
|
})();
|
|
1070
1296
|
return () => {
|
|
@@ -1084,14 +1310,16 @@ function App({ workflow: workflow2, events: events2, options: options2, updateCh
|
|
|
1084
1310
|
}, [updateCheck2]);
|
|
1085
1311
|
const elapsed = formatHeaderElapsed(state.startTime, state.endTime);
|
|
1086
1312
|
const activeTask = state.tasks[state.currentIndex];
|
|
1087
|
-
const completedCount = state.tasks.filter(
|
|
1313
|
+
const completedCount = state.tasks.filter(
|
|
1314
|
+
(t) => t.status === "complete"
|
|
1315
|
+
).length;
|
|
1088
1316
|
const totalCount = state.tasks.length;
|
|
1089
1317
|
const filterInfo = options2?.stepFilter ? ` [step: ${options2.stepFilter}]` : options2?.fromStep ? ` [from step: ${options2.fromStep}]` : "";
|
|
1090
|
-
return /* @__PURE__ */
|
|
1091
|
-
/* @__PURE__ */
|
|
1092
|
-
/* @__PURE__ */
|
|
1093
|
-
/* @__PURE__ */
|
|
1094
|
-
/* @__PURE__ */
|
|
1318
|
+
return /* @__PURE__ */ jsxs4(Box5, { flexDirection: "column", padding: 1, children: [
|
|
1319
|
+
/* @__PURE__ */ jsx5(Box5, { marginBottom: 1, children: /* @__PURE__ */ jsx5(BrandMark, { tick, isActive: !state.endTime }) }),
|
|
1320
|
+
/* @__PURE__ */ jsxs4(Box5, { marginBottom: 1, children: [
|
|
1321
|
+
/* @__PURE__ */ jsx5(Text5, { bold: true, color: theme.primary, children: workflow2.goal }),
|
|
1322
|
+
/* @__PURE__ */ jsxs4(Text5, { dimColor: true, children: [
|
|
1095
1323
|
" ",
|
|
1096
1324
|
completedCount,
|
|
1097
1325
|
"/",
|
|
@@ -1101,33 +1329,48 @@ function App({ workflow: workflow2, events: events2, options: options2, updateCh
|
|
|
1101
1329
|
filterInfo
|
|
1102
1330
|
] })
|
|
1103
1331
|
] }),
|
|
1104
|
-
/* @__PURE__ */
|
|
1105
|
-
|
|
1332
|
+
/* @__PURE__ */ jsx5(Box5, { flexDirection: "column", marginBottom: 1, children: state.tasks.map((taskState, i) => /* @__PURE__ */ jsxs4(Box5, { flexDirection: "column", children: [
|
|
1333
|
+
/* @__PURE__ */ jsx5(
|
|
1334
|
+
TaskRow,
|
|
1335
|
+
{
|
|
1336
|
+
index: i,
|
|
1337
|
+
tick,
|
|
1338
|
+
taskState,
|
|
1339
|
+
isActive: i === state.currentIndex
|
|
1340
|
+
}
|
|
1341
|
+
),
|
|
1342
|
+
taskState.status === "running" && taskState.iterationHistory?.length ? /* @__PURE__ */ jsx5(
|
|
1343
|
+
IterationList,
|
|
1344
|
+
{
|
|
1345
|
+
iterationHistory: taskState.iterationHistory,
|
|
1346
|
+
tick,
|
|
1347
|
+
maxVisible: MAX_VISIBLE_ITERATIONS
|
|
1348
|
+
}
|
|
1349
|
+
) : null
|
|
1350
|
+
] }, taskState.task.name)) }),
|
|
1351
|
+
activeTask && /* @__PURE__ */ jsx5(
|
|
1352
|
+
LogPane,
|
|
1106
1353
|
{
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
activeTask && /* @__PURE__ */ jsx4(LogPane, { lines: activeTask.lines, isActive: activeTask.status === "running" }),
|
|
1115
|
-
state.endTime !== void 0 && state.writtenFiles.length > 0 && /* @__PURE__ */ jsxs3(Box4, { flexDirection: "column", marginTop: 1, children: [
|
|
1116
|
-
/* @__PURE__ */ jsx4(Text4, { dimColor: true, children: "files written:" }),
|
|
1117
|
-
state.writtenFiles.map((f) => /* @__PURE__ */ jsxs3(Text4, { color: theme.primary, children: [
|
|
1354
|
+
lines: activeTask.lines,
|
|
1355
|
+
isActive: activeTask.status === "running"
|
|
1356
|
+
}
|
|
1357
|
+
),
|
|
1358
|
+
state.endTime !== void 0 && state.writtenFiles.length > 0 && /* @__PURE__ */ jsxs4(Box5, { flexDirection: "column", marginTop: 1, children: [
|
|
1359
|
+
/* @__PURE__ */ jsx5(Text5, { dimColor: true, children: "files written:" }),
|
|
1360
|
+
state.writtenFiles.map((f) => /* @__PURE__ */ jsxs4(Text5, { color: theme.primary, children: [
|
|
1118
1361
|
" ",
|
|
1119
1362
|
f
|
|
1120
1363
|
] }, f))
|
|
1121
1364
|
] }),
|
|
1122
|
-
/* @__PURE__ */
|
|
1123
|
-
updateVersion && /* @__PURE__ */
|
|
1365
|
+
/* @__PURE__ */ jsxs4(Box5, { marginTop: 1, flexDirection: "column", children: [
|
|
1366
|
+
updateVersion && /* @__PURE__ */ jsxs4(Text5, { color: theme.warning, children: [
|
|
1124
1367
|
"v",
|
|
1125
1368
|
updateVersion,
|
|
1126
1369
|
" available \u2014 run: executant update"
|
|
1127
1370
|
] }),
|
|
1128
|
-
/* @__PURE__ */
|
|
1371
|
+
/* @__PURE__ */ jsx5(Text5, { dimColor: true, children: "press q to quit" })
|
|
1129
1372
|
] }),
|
|
1130
|
-
isRawModeSupported && /* @__PURE__ */
|
|
1373
|
+
isRawModeSupported && /* @__PURE__ */ jsx5(KeyboardHandler, { onExit: exit })
|
|
1131
1374
|
] });
|
|
1132
1375
|
}
|
|
1133
1376
|
|
|
@@ -1146,25 +1389,9 @@ var PLAN_RETRY_SCHEMA_ERROR = loadPrompt("plan-retry-schema-error");
|
|
|
1146
1389
|
var PLAN_RETRY_JUDGE = loadPrompt("plan-retry-judge");
|
|
1147
1390
|
var MAX_PLAN_RETRIES = 3;
|
|
1148
1391
|
var TOTAL_PLAN_STAGES = 3;
|
|
1149
|
-
var StepSchema = z3.object({
|
|
1150
|
-
name: z3.string(),
|
|
1151
|
-
type: z3.enum(["prompt", "script", "log"]).optional(),
|
|
1152
|
-
prompt: z3.string().optional(),
|
|
1153
|
-
command: z3.string().optional(),
|
|
1154
|
-
message: z3.string().optional(),
|
|
1155
|
-
continue_on_error: z3.boolean().optional(),
|
|
1156
|
-
self_healing: z3.boolean().optional(),
|
|
1157
|
-
max_healing_attempts: z3.number().int().positive().optional(),
|
|
1158
|
-
output: z3.string().optional(),
|
|
1159
|
-
llm_as_judge: z3.boolean().optional(),
|
|
1160
|
-
allowed_tools: z3.array(z3.string()).optional(),
|
|
1161
|
-
forEach: z3.union([z3.array(z3.string()), z3.string()]).optional(),
|
|
1162
|
-
repeat: z3.number().int().positive().optional(),
|
|
1163
|
-
context: z3.array(z3.string()).optional()
|
|
1164
|
-
});
|
|
1165
1392
|
var WorkflowSchema = z3.object({
|
|
1166
1393
|
goal: z3.string(),
|
|
1167
|
-
steps: z3.array(
|
|
1394
|
+
steps: z3.array(RawStepSchema).min(1),
|
|
1168
1395
|
vars: z3.record(z3.string()).optional(),
|
|
1169
1396
|
self_improve: z3.boolean().optional()
|
|
1170
1397
|
});
|
|
@@ -1184,7 +1411,10 @@ function walkUp(startDir, check) {
|
|
|
1184
1411
|
}
|
|
1185
1412
|
}
|
|
1186
1413
|
function findGitRoot(startDir) {
|
|
1187
|
-
return walkUp(
|
|
1414
|
+
return walkUp(
|
|
1415
|
+
startDir,
|
|
1416
|
+
(dir) => existsSync(join2(dir, ".git")) ? dir : null
|
|
1417
|
+
);
|
|
1188
1418
|
}
|
|
1189
1419
|
function findProjectRoot(startDir) {
|
|
1190
1420
|
return walkUp(startDir, (dir) => {
|
|
@@ -1335,34 +1565,27 @@ function normalizeWorkflow(workflow2) {
|
|
|
1335
1565
|
return { ...workflow2, steps: collapseSequentialSteps(steps) };
|
|
1336
1566
|
}
|
|
1337
1567
|
function collapseSequentialSteps(steps) {
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
continue;
|
|
1360
|
-
}
|
|
1361
|
-
const { name: _name, ...rest } = step;
|
|
1362
|
-
result.push({ ...rest, name: `${prefix}_{{item}}`, repeat: n });
|
|
1363
|
-
i += n;
|
|
1364
|
-
}
|
|
1365
|
-
return result;
|
|
1568
|
+
return steps.reduce(
|
|
1569
|
+
({ out, skip }, step, i, arr) => {
|
|
1570
|
+
if (skip > 0) return { out, skip: skip - 1 };
|
|
1571
|
+
if (step.forEach !== void 0 || step.repeat !== void 0 || step.steps !== void 0) {
|
|
1572
|
+
return { out: [...out, step], skip: 0 };
|
|
1573
|
+
}
|
|
1574
|
+
const m = step.name.match(/^(.+?)_1$/);
|
|
1575
|
+
if (!m) return { out: [...out, step], skip: 0 };
|
|
1576
|
+
const prefix = m[1];
|
|
1577
|
+
let n = 1;
|
|
1578
|
+
while (i + n < arr.length && arr[i + n].name === `${prefix}_${n + 1}`)
|
|
1579
|
+
n++;
|
|
1580
|
+
if (n < 2) return { out: [...out, step], skip: 0 };
|
|
1581
|
+
const { name: _name, ...rest } = step;
|
|
1582
|
+
return {
|
|
1583
|
+
out: [...out, { ...rest, name: `${prefix}_{{item}}`, repeat: n }],
|
|
1584
|
+
skip: n - 1
|
|
1585
|
+
};
|
|
1586
|
+
},
|
|
1587
|
+
{ out: [], skip: 0 }
|
|
1588
|
+
).out;
|
|
1366
1589
|
}
|
|
1367
1590
|
async function* streamPlan(args) {
|
|
1368
1591
|
const { description, taskFile } = args;
|
|
@@ -1373,14 +1596,24 @@ async function* streamPlan(args) {
|
|
|
1373
1596
|
yield { type: "plan:stages", names: ["Decompose to Steps", "Validate"] };
|
|
1374
1597
|
researchDoc = "No codebase research performed \u2014 the task is self-contained. Work directly from the user's original goal.";
|
|
1375
1598
|
} else {
|
|
1376
|
-
yield {
|
|
1377
|
-
|
|
1599
|
+
yield {
|
|
1600
|
+
type: "plan:stages",
|
|
1601
|
+
names: ["Research & Planning", "Decompose to Steps", "Validate"]
|
|
1602
|
+
};
|
|
1603
|
+
yield {
|
|
1604
|
+
type: "plan:stage",
|
|
1605
|
+
stage: 1,
|
|
1606
|
+
total: TOTAL_PLAN_STAGES,
|
|
1607
|
+
name: "Research & Planning"
|
|
1608
|
+
};
|
|
1378
1609
|
const researchLines = [];
|
|
1379
1610
|
try {
|
|
1380
1611
|
const researchTask = {
|
|
1381
1612
|
type: "claude",
|
|
1382
1613
|
name: "plan:research",
|
|
1383
|
-
prompt: fillTemplate(PLAN_RESEARCH_PROMPT, {
|
|
1614
|
+
prompt: fillTemplate(PLAN_RESEARCH_PROMPT, {
|
|
1615
|
+
DESCRIPTION: description
|
|
1616
|
+
}),
|
|
1384
1617
|
allowedTools: ["Read", "Glob", "Grep"],
|
|
1385
1618
|
permissionMode: "bypassPermissions",
|
|
1386
1619
|
model: "opus"
|
|
@@ -1394,17 +1627,28 @@ async function* streamPlan(args) {
|
|
|
1394
1627
|
}
|
|
1395
1628
|
}
|
|
1396
1629
|
} catch (err) {
|
|
1397
|
-
yield {
|
|
1630
|
+
yield {
|
|
1631
|
+
type: "plan:error",
|
|
1632
|
+
message: `Research pass failed: ${getErrorMessage(err)}`
|
|
1633
|
+
};
|
|
1398
1634
|
return;
|
|
1399
1635
|
}
|
|
1400
1636
|
researchDoc = researchLines.join("\n");
|
|
1401
1637
|
if (!researchDoc.trim()) {
|
|
1402
|
-
yield {
|
|
1638
|
+
yield {
|
|
1639
|
+
type: "plan:error",
|
|
1640
|
+
message: "Research pass produced no output \u2014 cannot decompose"
|
|
1641
|
+
};
|
|
1403
1642
|
return;
|
|
1404
1643
|
}
|
|
1405
1644
|
}
|
|
1406
1645
|
const stages = skipResearch ? { decompose: 1, validate: 2, total: 2 } : { decompose: 2, validate: 3, total: TOTAL_PLAN_STAGES };
|
|
1407
|
-
yield {
|
|
1646
|
+
yield {
|
|
1647
|
+
type: "plan:stage",
|
|
1648
|
+
stage: stages.decompose,
|
|
1649
|
+
total: stages.total,
|
|
1650
|
+
name: "Decompose to Steps"
|
|
1651
|
+
};
|
|
1408
1652
|
let retryPrefix = "";
|
|
1409
1653
|
for (let attempt = 0; attempt < MAX_PLAN_RETRIES; attempt++) {
|
|
1410
1654
|
if (attempt > 0) {
|
|
@@ -1414,9 +1658,17 @@ async function* streamPlan(args) {
|
|
|
1414
1658
|
maxAttempts: MAX_PLAN_RETRIES,
|
|
1415
1659
|
reason: retryPrefix.replace(/\n/g, " ")
|
|
1416
1660
|
};
|
|
1417
|
-
yield {
|
|
1661
|
+
yield {
|
|
1662
|
+
type: "plan:stage",
|
|
1663
|
+
stage: stages.decompose,
|
|
1664
|
+
total: stages.total,
|
|
1665
|
+
name: "Decompose to Steps"
|
|
1666
|
+
};
|
|
1418
1667
|
}
|
|
1419
|
-
const basePrompt = fillTemplate(PLAN_DECOMPOSE_PROMPT, {
|
|
1668
|
+
const basePrompt = fillTemplate(PLAN_DECOMPOSE_PROMPT, {
|
|
1669
|
+
DESCRIPTION: description,
|
|
1670
|
+
RESEARCH_DOC: researchDoc
|
|
1671
|
+
});
|
|
1420
1672
|
const decomposeTask = {
|
|
1421
1673
|
type: "claude",
|
|
1422
1674
|
name: "plan:decompose",
|
|
@@ -1448,7 +1700,10 @@ ${basePrompt}` : basePrompt,
|
|
|
1448
1700
|
yield { type: "plan:error", message: msg };
|
|
1449
1701
|
return;
|
|
1450
1702
|
}
|
|
1451
|
-
retryPrefix = fillTemplate(PLAN_RETRY_PARSE_ERROR, {
|
|
1703
|
+
retryPrefix = fillTemplate(PLAN_RETRY_PARSE_ERROR, {
|
|
1704
|
+
ERROR: msg,
|
|
1705
|
+
EXCERPT: decomposeTextLines.join("\n")
|
|
1706
|
+
});
|
|
1452
1707
|
continue;
|
|
1453
1708
|
}
|
|
1454
1709
|
if (structuredOutput === void 0) {
|
|
@@ -1464,24 +1719,40 @@ ${basePrompt}` : basePrompt,
|
|
|
1464
1719
|
if (!zodResult.success) {
|
|
1465
1720
|
const issues = formatZodIssues(zodResult.error.issues);
|
|
1466
1721
|
if (attempt === MAX_PLAN_RETRIES - 1) {
|
|
1467
|
-
yield {
|
|
1468
|
-
|
|
1722
|
+
yield {
|
|
1723
|
+
type: "plan:error",
|
|
1724
|
+
message: `Plan did not match expected schema:
|
|
1725
|
+
${issues}`
|
|
1726
|
+
};
|
|
1469
1727
|
return;
|
|
1470
1728
|
}
|
|
1471
1729
|
retryPrefix = fillTemplate(PLAN_RETRY_SCHEMA_ERROR, { ISSUES: issues });
|
|
1472
1730
|
continue;
|
|
1473
1731
|
}
|
|
1474
|
-
yield {
|
|
1732
|
+
yield {
|
|
1733
|
+
type: "plan:stage",
|
|
1734
|
+
stage: stages.validate,
|
|
1735
|
+
total: stages.total,
|
|
1736
|
+
name: "Validate"
|
|
1737
|
+
};
|
|
1475
1738
|
const judgeResult = await runPass3Judge(description, zodResult.data);
|
|
1476
1739
|
if (judgeResult.skipped) {
|
|
1477
|
-
yield {
|
|
1740
|
+
yield {
|
|
1741
|
+
type: "plan:warn",
|
|
1742
|
+
message: "Judge skipped due to error \u2014 proceeding without validation"
|
|
1743
|
+
};
|
|
1478
1744
|
}
|
|
1479
1745
|
if (!judgeResult.pass && attempt < MAX_PLAN_RETRIES - 1) {
|
|
1480
|
-
retryPrefix = fillTemplate(PLAN_RETRY_JUDGE, {
|
|
1746
|
+
retryPrefix = fillTemplate(PLAN_RETRY_JUDGE, {
|
|
1747
|
+
FEEDBACK: judgeResult.feedback
|
|
1748
|
+
});
|
|
1481
1749
|
continue;
|
|
1482
1750
|
}
|
|
1483
1751
|
if (!judgeResult.pass) {
|
|
1484
|
-
yield {
|
|
1752
|
+
yield {
|
|
1753
|
+
type: "plan:warn",
|
|
1754
|
+
message: `Judge rejected plan but retries exhausted: ${judgeResult.feedback}`
|
|
1755
|
+
};
|
|
1485
1756
|
}
|
|
1486
1757
|
const { goal, vars, steps, ...rest } = normalizeWorkflow(zodResult.data);
|
|
1487
1758
|
const ordered = { goal, ...vars && { vars }, steps, ...rest };
|
|
@@ -1497,13 +1768,16 @@ ${issues}` };
|
|
|
1497
1768
|
yield { type: "plan:complete", taskFile, preview };
|
|
1498
1769
|
return;
|
|
1499
1770
|
}
|
|
1500
|
-
yield {
|
|
1771
|
+
yield {
|
|
1772
|
+
type: "plan:error",
|
|
1773
|
+
message: "Plan generation failed after maximum retries"
|
|
1774
|
+
};
|
|
1501
1775
|
}
|
|
1502
1776
|
|
|
1503
1777
|
// src/ui/PlanApp.tsx
|
|
1504
1778
|
import { useEffect as useEffect3, useReducer as useReducer2, useState as useState2 } from "react";
|
|
1505
|
-
import { Box as
|
|
1506
|
-
import { jsx as
|
|
1779
|
+
import { Box as Box6, Text as Text6, useApp as useApp2, useStdin as useStdin2 } from "ink";
|
|
1780
|
+
import { jsx as jsx6, jsxs as jsxs5 } from "react/jsx-runtime";
|
|
1507
1781
|
var truncate = (str, max) => str.length > max ? str.slice(0, max - 3) + "..." : str;
|
|
1508
1782
|
function buildInitial(description) {
|
|
1509
1783
|
return {
|
|
@@ -1579,7 +1853,7 @@ function StageProgress({ stage, totalStages, stageNames, tick, isActive, status
|
|
|
1579
1853
|
}
|
|
1580
1854
|
return { icon, color, name, bold, dim };
|
|
1581
1855
|
});
|
|
1582
|
-
return /* @__PURE__ */
|
|
1856
|
+
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: [
|
|
1583
1857
|
" ",
|
|
1584
1858
|
icon,
|
|
1585
1859
|
name ? ` ${name}` : ""
|
|
@@ -1612,19 +1886,19 @@ function PlanApp({ description, events: events2 }) {
|
|
|
1612
1886
|
const elapsed = formatHeaderElapsed(state.startTime);
|
|
1613
1887
|
const icon = isActive ? SPINNER[tick % SPINNER.length] : state.status === "complete" ? "\u2713" : "\u2717";
|
|
1614
1888
|
const iconColor = state.status === "complete" ? theme.success : state.status === "error" ? theme.error : theme.primary;
|
|
1615
|
-
return /* @__PURE__ */
|
|
1616
|
-
/* @__PURE__ */
|
|
1617
|
-
/* @__PURE__ */
|
|
1618
|
-
/* @__PURE__ */
|
|
1889
|
+
return /* @__PURE__ */ jsxs5(Box6, { flexDirection: "column", padding: 1, children: [
|
|
1890
|
+
/* @__PURE__ */ jsx6(Box6, { marginBottom: 1, children: /* @__PURE__ */ jsx6(BrandMark, { tick, isActive }) }),
|
|
1891
|
+
/* @__PURE__ */ jsxs5(Box6, { marginBottom: 1, children: [
|
|
1892
|
+
/* @__PURE__ */ jsxs5(Text6, { color: iconColor, children: [
|
|
1619
1893
|
icon,
|
|
1620
1894
|
" "
|
|
1621
1895
|
] }),
|
|
1622
|
-
/* @__PURE__ */
|
|
1623
|
-
/* @__PURE__ */
|
|
1896
|
+
/* @__PURE__ */ jsx6(Text6, { bold: true, color: theme.primary, children: "Generating plan" }),
|
|
1897
|
+
/* @__PURE__ */ jsxs5(Text6, { dimColor: true, children: [
|
|
1624
1898
|
" ",
|
|
1625
1899
|
elapsed
|
|
1626
1900
|
] }),
|
|
1627
|
-
state.status === "retrying" && /* @__PURE__ */
|
|
1901
|
+
state.status === "retrying" && /* @__PURE__ */ jsxs5(Text6, { color: theme.warning, children: [
|
|
1628
1902
|
" ",
|
|
1629
1903
|
"(attempt ",
|
|
1630
1904
|
state.attempt,
|
|
@@ -1633,11 +1907,11 @@ function PlanApp({ description, events: events2 }) {
|
|
|
1633
1907
|
")"
|
|
1634
1908
|
] })
|
|
1635
1909
|
] }),
|
|
1636
|
-
/* @__PURE__ */
|
|
1637
|
-
/* @__PURE__ */
|
|
1638
|
-
/* @__PURE__ */
|
|
1910
|
+
/* @__PURE__ */ jsxs5(Box6, { marginBottom: 1, children: [
|
|
1911
|
+
/* @__PURE__ */ jsx6(Text6, { dimColor: true, children: " " }),
|
|
1912
|
+
/* @__PURE__ */ jsx6(Text6, { children: truncate(state.description, 80) })
|
|
1639
1913
|
] }),
|
|
1640
|
-
/* @__PURE__ */
|
|
1914
|
+
/* @__PURE__ */ jsx6(
|
|
1641
1915
|
StageProgress,
|
|
1642
1916
|
{
|
|
1643
1917
|
stage: state.stage,
|
|
@@ -1648,23 +1922,23 @@ function PlanApp({ description, events: events2 }) {
|
|
|
1648
1922
|
status: state.status
|
|
1649
1923
|
}
|
|
1650
1924
|
),
|
|
1651
|
-
state.lines.length > 0 && /* @__PURE__ */
|
|
1652
|
-
state.status === "complete" && state.taskFile && /* @__PURE__ */
|
|
1653
|
-
/* @__PURE__ */
|
|
1925
|
+
state.lines.length > 0 && /* @__PURE__ */ jsx6(LogPane, { lines: state.lines, isActive, maxLines: 10 }),
|
|
1926
|
+
state.status === "complete" && state.taskFile && /* @__PURE__ */ jsxs5(Box6, { flexDirection: "column", marginTop: 1, children: [
|
|
1927
|
+
/* @__PURE__ */ jsxs5(Text6, { color: theme.success, children: [
|
|
1654
1928
|
"\u2705 Task plan saved: ",
|
|
1655
1929
|
state.taskFile
|
|
1656
1930
|
] }),
|
|
1657
|
-
state.preview && /* @__PURE__ */
|
|
1658
|
-
/* @__PURE__ */
|
|
1659
|
-
/* @__PURE__ */
|
|
1931
|
+
state.preview && /* @__PURE__ */ jsxs5(Box6, { flexDirection: "column", marginTop: 1, children: [
|
|
1932
|
+
/* @__PURE__ */ jsx6(Text6, { dimColor: true, children: "Preview:" }),
|
|
1933
|
+
/* @__PURE__ */ jsx6(Text6, { children: state.preview })
|
|
1660
1934
|
] })
|
|
1661
1935
|
] }),
|
|
1662
|
-
state.status === "error" && /* @__PURE__ */
|
|
1936
|
+
state.status === "error" && /* @__PURE__ */ jsx6(Box6, { marginTop: 1, children: /* @__PURE__ */ jsxs5(Text6, { color: theme.error, children: [
|
|
1663
1937
|
"Error: ",
|
|
1664
1938
|
state.errorMessage
|
|
1665
1939
|
] }) }),
|
|
1666
|
-
isActive && /* @__PURE__ */
|
|
1667
|
-
isRawModeSupported && /* @__PURE__ */
|
|
1940
|
+
isActive && /* @__PURE__ */ jsx6(Box6, { marginTop: 1, children: /* @__PURE__ */ jsx6(Text6, { dimColor: true, children: "press q to quit" }) }),
|
|
1941
|
+
isRawModeSupported && /* @__PURE__ */ jsx6(KeyboardHandler, { onExit: exit })
|
|
1668
1942
|
] });
|
|
1669
1943
|
}
|
|
1670
1944
|
|
|
@@ -1724,39 +1998,57 @@ function onWorkflowStart(ctx, s) {
|
|
|
1724
1998
|
mkdirSync3(ctx.logDir, { recursive: true });
|
|
1725
1999
|
mkdirSync3(ctx.highlightsDir, { recursive: true });
|
|
1726
2000
|
const logFile = join3(ctx.logDir, `${ctx.ts}_${ctx.slug}.log`);
|
|
1727
|
-
writeFileSync3(
|
|
2001
|
+
writeFileSync3(
|
|
2002
|
+
logFile,
|
|
2003
|
+
`# Execution Log
|
|
1728
2004
|
Task: ${ctx.slug}
|
|
1729
2005
|
Started: ${(/* @__PURE__ */ new Date()).toISOString()}
|
|
1730
2006
|
${"\u2501".repeat(51)}
|
|
1731
2007
|
|
|
1732
|
-
`
|
|
2008
|
+
`
|
|
2009
|
+
);
|
|
1733
2010
|
return { ...s, logFile };
|
|
1734
2011
|
}
|
|
1735
2012
|
function onStepStart(ctx, s, index, name) {
|
|
1736
|
-
const next = {
|
|
1737
|
-
|
|
2013
|
+
const next = {
|
|
2014
|
+
...INIT_STATE,
|
|
2015
|
+
logFile: s.logFile,
|
|
2016
|
+
stepIndex: index,
|
|
2017
|
+
stepName: name,
|
|
2018
|
+
stepStartMs: Date.now()
|
|
2019
|
+
};
|
|
2020
|
+
appendLog(
|
|
2021
|
+
next.logFile,
|
|
2022
|
+
`
|
|
1738
2023
|
${"\u2501".repeat(51)}
|
|
1739
2024
|
Step ${index + 1}: ${name}
|
|
1740
2025
|
Started: ${(/* @__PURE__ */ new Date()).toISOString()}
|
|
1741
2026
|
${"\u2501".repeat(51)}
|
|
1742
|
-
`
|
|
2027
|
+
`
|
|
2028
|
+
);
|
|
1743
2029
|
return next;
|
|
1744
2030
|
}
|
|
1745
2031
|
function finalizeComplexSequence(s) {
|
|
1746
2032
|
if (s.toolCount >= 3 && s.complexSequenceFile) {
|
|
1747
|
-
appendFileSync(
|
|
2033
|
+
appendFileSync(
|
|
2034
|
+
s.complexSequenceFile,
|
|
2035
|
+
`
|
|
1748
2036
|
---
|
|
1749
2037
|
|
|
1750
2038
|
*Total tools used: ${s.toolCount}*
|
|
1751
2039
|
|
|
1752
2040
|
*Captured by Executant Logger*
|
|
1753
|
-
`
|
|
2041
|
+
`
|
|
2042
|
+
);
|
|
1754
2043
|
}
|
|
1755
2044
|
}
|
|
1756
2045
|
function onStepComplete(s) {
|
|
1757
|
-
appendLog(
|
|
2046
|
+
appendLog(
|
|
2047
|
+
s.logFile,
|
|
2048
|
+
`
|
|
1758
2049
|
Step completed in ${((Date.now() - s.stepStartMs) / 1e3).toFixed(1)}s
|
|
1759
|
-
`
|
|
2050
|
+
`
|
|
2051
|
+
);
|
|
1760
2052
|
finalizeComplexSequence(s);
|
|
1761
2053
|
return s;
|
|
1762
2054
|
}
|
|
@@ -1794,29 +2086,35 @@ function onTool(ctx, s, tool, input) {
|
|
|
1794
2086
|
const toolCount = s.toolCount + 1;
|
|
1795
2087
|
const complexSequenceFile = toolCount === 3 ? createComplexSequenceFile(ctx, s) : s.complexSequenceFile;
|
|
1796
2088
|
if (toolCount >= 3 && complexSequenceFile) {
|
|
1797
|
-
appendFileSync(
|
|
1798
|
-
|
|
2089
|
+
appendFileSync(
|
|
2090
|
+
complexSequenceFile,
|
|
2091
|
+
`${toolCount}. **${tool}** - ${desc}
|
|
2092
|
+
`
|
|
2093
|
+
);
|
|
1799
2094
|
}
|
|
1800
2095
|
return { ...s, toolCount, complexSequenceFile };
|
|
1801
2096
|
}
|
|
1802
2097
|
function saveJudgeHighlight(ctx, s, verdict, text) {
|
|
1803
|
-
writeFileSync3(
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
2098
|
+
writeFileSync3(
|
|
2099
|
+
highlightPath(ctx, s.stepIndex, `judge_${verdict}`),
|
|
2100
|
+
[
|
|
2101
|
+
`# Judge Verdict: ${verdict}`,
|
|
2102
|
+
"",
|
|
2103
|
+
`**Task:** ${ctx.slug}`,
|
|
2104
|
+
`**Step:** ${s.stepName}`,
|
|
2105
|
+
`**Attempt:** ${s.judgeAttempt}`,
|
|
2106
|
+
`**Timestamp:** ${(/* @__PURE__ */ new Date()).toISOString()}`,
|
|
2107
|
+
"",
|
|
2108
|
+
"---",
|
|
2109
|
+
"",
|
|
2110
|
+
text,
|
|
2111
|
+
"",
|
|
2112
|
+
"---",
|
|
2113
|
+
"",
|
|
2114
|
+
"*Auto-captured*",
|
|
2115
|
+
""
|
|
2116
|
+
].join("\n")
|
|
2117
|
+
);
|
|
1820
2118
|
}
|
|
1821
2119
|
var LOG_MATCHERS = [
|
|
1822
2120
|
{
|
|
@@ -1832,29 +2130,32 @@ var LOG_MATCHERS = [
|
|
|
1832
2130
|
pattern: /\[self-healing\].*failed.*exit\s+(\d+)/i,
|
|
1833
2131
|
apply: (ctx, s, text, match) => {
|
|
1834
2132
|
const selfHealingFile = highlightPath(ctx, s.stepIndex, "self_healing");
|
|
1835
|
-
writeFileSync3(
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
2133
|
+
writeFileSync3(
|
|
2134
|
+
selfHealingFile,
|
|
2135
|
+
[
|
|
2136
|
+
"# Self-Healing Activation",
|
|
2137
|
+
"",
|
|
2138
|
+
`**Task:** ${ctx.slug}`,
|
|
2139
|
+
`**Step:** ${s.stepName}`,
|
|
2140
|
+
`**Timestamp:** ${(/* @__PURE__ */ new Date()).toISOString()}`,
|
|
2141
|
+
"",
|
|
2142
|
+
"---",
|
|
2143
|
+
"",
|
|
2144
|
+
"## \u274C Failure Detected",
|
|
2145
|
+
"",
|
|
2146
|
+
`**Exit Code:** ${match[1]}`,
|
|
2147
|
+
"",
|
|
2148
|
+
"**Recent Output:**",
|
|
2149
|
+
"```",
|
|
2150
|
+
s.recentOutput.join("\n"),
|
|
2151
|
+
"```",
|
|
2152
|
+
"",
|
|
2153
|
+
"---",
|
|
2154
|
+
"",
|
|
2155
|
+
"## \u{1F527} Claude's Healing Process",
|
|
2156
|
+
""
|
|
2157
|
+
].join("\n")
|
|
2158
|
+
);
|
|
1858
2159
|
return { ...s, selfHealingFile, recentOutput: [] };
|
|
1859
2160
|
}
|
|
1860
2161
|
},
|
|
@@ -1862,21 +2163,24 @@ var LOG_MATCHERS = [
|
|
|
1862
2163
|
pattern: /\[self-healing\].*Re-running/i,
|
|
1863
2164
|
apply: (_ctx, s) => {
|
|
1864
2165
|
if (!s.selfHealingFile) return s;
|
|
1865
|
-
appendFileSync(
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
2166
|
+
appendFileSync(
|
|
2167
|
+
s.selfHealingFile,
|
|
2168
|
+
[
|
|
2169
|
+
"",
|
|
2170
|
+
"*(See full log for Claude's diagnostic process)*",
|
|
2171
|
+
"",
|
|
2172
|
+
"---",
|
|
2173
|
+
"",
|
|
2174
|
+
"## \u2705 Resolution Applied",
|
|
2175
|
+
"",
|
|
2176
|
+
"The self-healing process completed. Check the full execution log to see Claude's analysis and fix.",
|
|
2177
|
+
"",
|
|
2178
|
+
"---",
|
|
2179
|
+
"",
|
|
2180
|
+
"*Auto-captured*",
|
|
2181
|
+
""
|
|
2182
|
+
].join("\n")
|
|
2183
|
+
);
|
|
1880
2184
|
return { ...s, selfHealingFile: "" };
|
|
1881
2185
|
}
|
|
1882
2186
|
}
|
|
@@ -1893,30 +2197,39 @@ function onLogMessage(ctx, s, level, text) {
|
|
|
1893
2197
|
).state;
|
|
1894
2198
|
}
|
|
1895
2199
|
function onWorkflowComplete(ctx, s) {
|
|
1896
|
-
appendLog(
|
|
2200
|
+
appendLog(
|
|
2201
|
+
s.logFile,
|
|
2202
|
+
`
|
|
1897
2203
|
${"\u2501".repeat(51)}
|
|
1898
2204
|
Task Complete: ${ctx.slug}
|
|
1899
2205
|
Finished: ${(/* @__PURE__ */ new Date()).toISOString()}
|
|
1900
2206
|
${"\u2501".repeat(51)}
|
|
1901
|
-
`
|
|
2207
|
+
`
|
|
2208
|
+
);
|
|
1902
2209
|
const indexFile = join3(ctx.highlightsDir, "README.md");
|
|
1903
2210
|
if (!existsSync2(indexFile)) {
|
|
1904
|
-
writeFileSync3(
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
|
|
2211
|
+
writeFileSync3(
|
|
2212
|
+
indexFile,
|
|
2213
|
+
[
|
|
2214
|
+
"# Execution Highlights",
|
|
2215
|
+
"",
|
|
2216
|
+
"This directory contains automatically extracted highlight moments from task executions.",
|
|
2217
|
+
"",
|
|
2218
|
+
"## Latest Highlights",
|
|
2219
|
+
""
|
|
2220
|
+
].join("\n")
|
|
2221
|
+
);
|
|
1912
2222
|
}
|
|
1913
2223
|
const highlights = readdirSync(ctx.highlightsDir).filter((f) => f.startsWith(ctx.ts) && f.endsWith(".md")).sort();
|
|
1914
2224
|
if (highlights.length > 0) {
|
|
1915
2225
|
const entries = highlights.map((f) => `- [${f.replace(/\.md$/, "")}](./${f})`).join("\n");
|
|
1916
|
-
appendFileSync(
|
|
2226
|
+
appendFileSync(
|
|
2227
|
+
indexFile,
|
|
2228
|
+
`
|
|
1917
2229
|
### ${ctx.slug} (${(/* @__PURE__ */ new Date()).toISOString()})
|
|
1918
2230
|
${entries}
|
|
1919
|
-
`
|
|
2231
|
+
`
|
|
2232
|
+
);
|
|
1920
2233
|
}
|
|
1921
2234
|
return s;
|
|
1922
2235
|
}
|
|
@@ -1934,6 +2247,19 @@ function reduce(ctx, s, event) {
|
|
|
1934
2247
|
return onStepComplete(s);
|
|
1935
2248
|
case "step:error":
|
|
1936
2249
|
return onStepError(s, event.error);
|
|
2250
|
+
case "step:iteration":
|
|
2251
|
+
appendLog(
|
|
2252
|
+
s.logFile,
|
|
2253
|
+
`
|
|
2254
|
+
\u2500\u2500 iteration ${event.iteration}/${event.total}: ${event.item}`
|
|
2255
|
+
);
|
|
2256
|
+
return s;
|
|
2257
|
+
case "step:inner":
|
|
2258
|
+
appendLog(
|
|
2259
|
+
s.logFile,
|
|
2260
|
+
` \u21B3 [${event.innerIndex + 1}/${event.innerTotal}] ${event.name}`
|
|
2261
|
+
);
|
|
2262
|
+
return s;
|
|
1937
2263
|
case "output:text":
|
|
1938
2264
|
return onOutputText(s, event.text);
|
|
1939
2265
|
case "output:tool":
|
|
@@ -2115,16 +2441,21 @@ function extractJson(text) {
|
|
|
2115
2441
|
|
|
2116
2442
|
// src/index.ts
|
|
2117
2443
|
var CURRENT_VERSION = JSON.parse(
|
|
2118
|
-
readFileSync6(
|
|
2444
|
+
readFileSync6(
|
|
2445
|
+
join5(dirname5(fileURLToPath2(import.meta.url)), "../package.json"),
|
|
2446
|
+
"utf-8"
|
|
2447
|
+
)
|
|
2119
2448
|
).version;
|
|
2120
2449
|
var rawArgs = process.argv.slice(2);
|
|
2121
2450
|
if (rawArgs[0] === "plan") {
|
|
2122
2451
|
const planArgs = parsePlanArgs(rawArgs.slice(1));
|
|
2123
2452
|
const planEvents = streamPlan(planArgs);
|
|
2124
|
-
const inkApp = render(
|
|
2125
|
-
|
|
2126
|
-
|
|
2127
|
-
|
|
2453
|
+
const inkApp = render(
|
|
2454
|
+
React3.createElement(PlanApp, {
|
|
2455
|
+
description: planArgs.description,
|
|
2456
|
+
events: planEvents
|
|
2457
|
+
})
|
|
2458
|
+
);
|
|
2128
2459
|
try {
|
|
2129
2460
|
await inkApp.waitUntilExit();
|
|
2130
2461
|
} catch {
|
|
@@ -2173,7 +2504,15 @@ YAML \u2014 step fields (all step types):
|
|
|
2173
2504
|
forEach string or list
|
|
2174
2505
|
Inline YAML array OR a shell command whose newline-split
|
|
2175
2506
|
stdout provides the items. {{item}} is substituted per
|
|
2176
|
-
iteration in
|
|
2507
|
+
iteration in every child step's name, command, and prompt.
|
|
2508
|
+
repeat int Run this step N times; {{item}} is the 1-based
|
|
2509
|
+
iteration number. Mutually exclusive with forEach.
|
|
2510
|
+
steps list Multiple child steps to run per forEach/repeat
|
|
2511
|
+
iteration. Mutually exclusive with command/prompt on the
|
|
2512
|
+
parent step. Requires forEach or repeat.
|
|
2513
|
+
context list Var names whose file-path values are prepended to
|
|
2514
|
+
a prompt step's content at runtime.
|
|
2515
|
+
output string Var name; captures script stdout to that file path.
|
|
2177
2516
|
|
|
2178
2517
|
YAML \u2014 prompt step fields (type: prompt, or inferred when prompt is present):
|
|
2179
2518
|
prompt string (required) Instructions sent to Claude
|
|
@@ -2185,7 +2524,7 @@ YAML \u2014 prompt step fields (type: prompt, or inferred when prompt is present
|
|
|
2185
2524
|
YAML \u2014 script step fields (type: script | command, or inferred when command is present):
|
|
2186
2525
|
command string (required) Bash command to execute
|
|
2187
2526
|
self_healing bool On failure, Claude diagnoses and fixes iteratively
|
|
2188
|
-
up to 5 attempts with accumulated context (default:
|
|
2527
|
+
up to 5 attempts with accumulated context (default: false)
|
|
2189
2528
|
max_healing_attempts int Override max self-healing retries (default: 5)
|
|
2190
2529
|
|
|
2191
2530
|
YAML \u2014 log step fields (type: log, or inferred when message is present and prompt is absent):
|
|
@@ -2263,9 +2602,17 @@ function errorReplacer(_key, value) {
|
|
|
2263
2602
|
async function maybeRunRetrospective(filePath2, workflow2, logger2) {
|
|
2264
2603
|
if (!logger2) return;
|
|
2265
2604
|
try {
|
|
2266
|
-
await runRetrospective(
|
|
2605
|
+
await runRetrospective(
|
|
2606
|
+
filePath2,
|
|
2607
|
+
workflow2,
|
|
2608
|
+
logger2.getHighlightsDir(),
|
|
2609
|
+
logger2.getTimestamp()
|
|
2610
|
+
);
|
|
2267
2611
|
} catch (err) {
|
|
2268
|
-
console.warn(
|
|
2612
|
+
console.warn(
|
|
2613
|
+
"[executant] retrospective failed (non-fatal):",
|
|
2614
|
+
getErrorMessage(err)
|
|
2615
|
+
);
|
|
2269
2616
|
}
|
|
2270
2617
|
}
|
|
2271
2618
|
if (ciMode) {
|
|
@@ -2281,7 +2628,9 @@ if (ciMode) {
|
|
|
2281
2628
|
process.exit(1);
|
|
2282
2629
|
});
|
|
2283
2630
|
} else {
|
|
2284
|
-
const inkApp = render(
|
|
2631
|
+
const inkApp = render(
|
|
2632
|
+
React3.createElement(App, { workflow, events, options, updateCheck })
|
|
2633
|
+
);
|
|
2285
2634
|
if (workflow.selfImprove) {
|
|
2286
2635
|
inkApp.waitUntilExit().then(() => maybeRunRetrospective(filePath, workflow, logger)).catch(() => {
|
|
2287
2636
|
});
|