executant 1.6.0 → 1.8.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 +697 -524
- package/dist/prompts/plan-decompose.txt +40 -0
- package/dist/prompts/plan-judge.txt +3 -1
- package/package.json +6 -2
package/dist/index.js
CHANGED
|
@@ -54,28 +54,84 @@ import React3 from "react";
|
|
|
54
54
|
import { render } from "ink";
|
|
55
55
|
import { readFileSync as readFileSync6 } from "node:fs";
|
|
56
56
|
import { dirname as dirname5, join as join5 } from "node:path";
|
|
57
|
-
import { fileURLToPath as
|
|
57
|
+
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
58
58
|
|
|
59
59
|
// src/load-workflow.ts
|
|
60
|
-
import { readFileSync } from "node:fs";
|
|
60
|
+
import { readFileSync as readFileSync2 } from "node:fs";
|
|
61
61
|
import { load as parseYaml } from "js-yaml";
|
|
62
|
+
|
|
63
|
+
// src/lib/utils.ts
|
|
64
|
+
import { readFileSync } from "node:fs";
|
|
65
|
+
import { basename, dirname, join } from "node:path";
|
|
66
|
+
import { fileURLToPath } from "node:url";
|
|
67
|
+
var __dir = dirname(fileURLToPath(import.meta.url));
|
|
68
|
+
var PROMPTS_DIR = basename(__dir) === "lib" ? join(__dir, "..", "prompts") : join(__dir, "prompts");
|
|
69
|
+
function stripPromptHeader(raw) {
|
|
70
|
+
return raw.replace(/^(#[^\n]*\n)+\n?/, "").trim();
|
|
71
|
+
}
|
|
72
|
+
function loadPrompt(name) {
|
|
73
|
+
return stripPromptHeader(readFileSync(join(PROMPTS_DIR, `${name}.txt`), "utf8"));
|
|
74
|
+
}
|
|
75
|
+
function findOutermostBraces(text) {
|
|
76
|
+
const start = text.indexOf("{");
|
|
77
|
+
if (start === -1) return null;
|
|
78
|
+
let depth = 0;
|
|
79
|
+
for (let i = start; i < text.length; i++) {
|
|
80
|
+
if (text[i] === "{") depth++;
|
|
81
|
+
else if (text[i] === "}" && --depth === 0) return { start, end: i };
|
|
82
|
+
}
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
function extractJsonObject(text) {
|
|
86
|
+
const fenceMatch = text.match(/```(?:json)?\s*\n([\s\S]*?)\n```/);
|
|
87
|
+
if (fenceMatch) return fenceMatch[1].trim();
|
|
88
|
+
const bounds = findOutermostBraces(text);
|
|
89
|
+
return bounds ? text.slice(bounds.start, bounds.end + 1) : text.trim();
|
|
90
|
+
}
|
|
91
|
+
function getErrorMessage(err) {
|
|
92
|
+
return err instanceof Error ? err.message : String(err);
|
|
93
|
+
}
|
|
94
|
+
function fillTemplate(template, vars) {
|
|
95
|
+
return Object.entries(vars).reduce(
|
|
96
|
+
(acc, [key, val]) => acc.replaceAll(`{{${key}}}`, val),
|
|
97
|
+
template
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
function formatZodIssues(issues) {
|
|
101
|
+
return issues.map((i) => ` ${i.path.join(".")}: ${i.message}`).join("\n");
|
|
102
|
+
}
|
|
103
|
+
function slugify(text, maxLen = 20) {
|
|
104
|
+
return text.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, maxLen).replace(/-+$/, "");
|
|
105
|
+
}
|
|
106
|
+
function formatTimestamp(d) {
|
|
107
|
+
const p = (n) => String(n).padStart(2, "0");
|
|
108
|
+
return `${d.getFullYear()}${p(d.getMonth() + 1)}${p(d.getDate())}-${p(d.getHours())}${p(d.getMinutes())}${p(d.getSeconds())}`;
|
|
109
|
+
}
|
|
110
|
+
function timestamp() {
|
|
111
|
+
return formatTimestamp(/* @__PURE__ */ new Date());
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// src/load-workflow.ts
|
|
62
115
|
import { z } from "zod";
|
|
63
|
-
var RawStepSchema = z.
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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
|
+
);
|
|
79
135
|
var RawWorkflowSchema = z.object({
|
|
80
136
|
goal: z.string(),
|
|
81
137
|
steps: z.array(RawStepSchema),
|
|
@@ -85,16 +141,17 @@ var RawWorkflowSchema = z.object({
|
|
|
85
141
|
function loadWorkflow(filePath2) {
|
|
86
142
|
let raw;
|
|
87
143
|
try {
|
|
88
|
-
raw =
|
|
144
|
+
raw = readFileSync2(filePath2, "utf8");
|
|
89
145
|
} catch (err) {
|
|
90
|
-
|
|
91
|
-
|
|
146
|
+
throw new Error(
|
|
147
|
+
`Cannot read workflow file "${filePath2}": ${getErrorMessage(err)}`
|
|
148
|
+
);
|
|
92
149
|
}
|
|
93
150
|
let doc;
|
|
94
151
|
try {
|
|
95
152
|
doc = RawWorkflowSchema.parse(parseYaml(raw));
|
|
96
153
|
} catch (err) {
|
|
97
|
-
const detail = err instanceof z.ZodError ? err.errors
|
|
154
|
+
const detail = err instanceof z.ZodError ? formatZodIssues(err.errors) : String(err);
|
|
98
155
|
throw new Error(`Invalid workflow file "${filePath2}":
|
|
99
156
|
${detail}`);
|
|
100
157
|
}
|
|
@@ -107,32 +164,35 @@ ${detail}`);
|
|
|
107
164
|
};
|
|
108
165
|
}
|
|
109
166
|
function convertStep(step, vars) {
|
|
110
|
-
const name = step
|
|
111
|
-
const continueOnError = step.continue_on_error ?? false;
|
|
167
|
+
const { name, continue_on_error: continueOnError = false } = step;
|
|
112
168
|
if (step.repeat !== void 0 && step.forEach !== void 0) {
|
|
113
169
|
throw new Error(`Step "${name}" cannot have both repeat and forEach`);
|
|
114
170
|
}
|
|
115
|
-
if (step.repeat !== void 0) {
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
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
|
+
}
|
|
177
|
+
const forEachValue = step.repeat !== void 0 ? Array.from({ length: step.repeat }, (_, i) => String(i + 1)) : step.forEach;
|
|
178
|
+
const stepWithoutLoop = {
|
|
179
|
+
...step,
|
|
180
|
+
repeat: void 0,
|
|
181
|
+
forEach: void 0,
|
|
182
|
+
steps: void 0
|
|
124
183
|
};
|
|
125
|
-
|
|
126
|
-
if (step.forEach !== void 0) {
|
|
127
|
-
const { forEach: _, ...innerStep } = step;
|
|
184
|
+
const inner = step.steps ? step.steps.map((s) => convertStep(s, vars)) : [convertInnerStep(stepWithoutLoop, vars, name, continueOnError)];
|
|
128
185
|
return {
|
|
129
186
|
type: "forEach",
|
|
130
187
|
name,
|
|
131
188
|
continueOnError,
|
|
132
|
-
forEach:
|
|
133
|
-
inner
|
|
189
|
+
forEach: forEachValue,
|
|
190
|
+
inner
|
|
134
191
|
};
|
|
135
192
|
}
|
|
193
|
+
if (step.steps) {
|
|
194
|
+
throw new Error(`Step "${name}" has steps but no forEach or repeat`);
|
|
195
|
+
}
|
|
136
196
|
return convertInnerStep(step, vars, name, continueOnError);
|
|
137
197
|
}
|
|
138
198
|
function convertInnerStep(step, vars, name, continueOnError) {
|
|
@@ -140,7 +200,8 @@ function convertInnerStep(step, vars, name, continueOnError) {
|
|
|
140
200
|
switch (effectiveType) {
|
|
141
201
|
case "script":
|
|
142
202
|
case "command": {
|
|
143
|
-
if (!step.command)
|
|
203
|
+
if (!step.command)
|
|
204
|
+
throw new Error(`Step "${name}" has type script but no command`);
|
|
144
205
|
return {
|
|
145
206
|
type: "command",
|
|
146
207
|
name,
|
|
@@ -148,7 +209,9 @@ function convertInnerStep(step, vars, name, continueOnError) {
|
|
|
148
209
|
continueOnError,
|
|
149
210
|
selfHealing: step.self_healing === true,
|
|
150
211
|
maxHealingAttempts: step.max_healing_attempts,
|
|
151
|
-
...step.output && {
|
|
212
|
+
...step.output && {
|
|
213
|
+
output: resolveOutputFile(step.output, vars, name)
|
|
214
|
+
}
|
|
152
215
|
};
|
|
153
216
|
}
|
|
154
217
|
case "log": {
|
|
@@ -161,7 +224,8 @@ function convertInnerStep(step, vars, name, continueOnError) {
|
|
|
161
224
|
};
|
|
162
225
|
}
|
|
163
226
|
case "prompt": {
|
|
164
|
-
if (!step.prompt)
|
|
227
|
+
if (!step.prompt)
|
|
228
|
+
throw new Error(`Step "${name}" has type prompt but no prompt field`);
|
|
165
229
|
const contextFiles = resolveContextFiles(step.context, vars, name);
|
|
166
230
|
return {
|
|
167
231
|
type: "claude",
|
|
@@ -184,22 +248,23 @@ function inferType(step) {
|
|
|
184
248
|
}
|
|
185
249
|
function resolveVarPath(varName, vars, stepName, label) {
|
|
186
250
|
if (!(varName in vars)) {
|
|
187
|
-
throw new Error(
|
|
251
|
+
throw new Error(
|
|
252
|
+
`Step "${stepName}" ${label} references undefined var "${varName}" \u2014 add it to the vars section`
|
|
253
|
+
);
|
|
188
254
|
}
|
|
189
255
|
return vars[varName];
|
|
190
256
|
}
|
|
191
257
|
function resolveContextFiles(contextVarNames, vars, stepName) {
|
|
192
258
|
if (!contextVarNames || contextVarNames.length === 0) return [];
|
|
193
|
-
return contextVarNames.map(
|
|
259
|
+
return contextVarNames.map(
|
|
260
|
+
(varName) => resolveVarPath(varName, vars, stepName, "context")
|
|
261
|
+
);
|
|
194
262
|
}
|
|
195
263
|
function resolveOutputFile(varName, vars, stepName) {
|
|
196
264
|
return resolveVarPath(varName, vars, stepName, "output");
|
|
197
265
|
}
|
|
198
266
|
function substituteVars(text, vars, stepName, field) {
|
|
199
|
-
const result =
|
|
200
|
-
(acc, [key, value]) => acc.replaceAll(`{{${key}}}`, value),
|
|
201
|
-
text
|
|
202
|
-
);
|
|
267
|
+
const result = fillTemplate(text, vars);
|
|
203
268
|
const unknownTokens = [...result.matchAll(/\{\{(\w+)\}\}/g)].map((m) => m[1]).filter((key) => key !== "item");
|
|
204
269
|
if (unknownTokens.length > 0) {
|
|
205
270
|
throw new Error(
|
|
@@ -305,47 +370,6 @@ async function* runCommand(task) {
|
|
|
305
370
|
// src/tasks/claude.ts
|
|
306
371
|
import { execSync, spawn as spawn2 } from "node:child_process";
|
|
307
372
|
import { zodToJsonSchema } from "zod-to-json-schema";
|
|
308
|
-
|
|
309
|
-
// src/lib/utils.ts
|
|
310
|
-
import { readFileSync as readFileSync2 } from "node:fs";
|
|
311
|
-
import { basename, dirname, join } from "node:path";
|
|
312
|
-
import { fileURLToPath } from "node:url";
|
|
313
|
-
var __dir = dirname(fileURLToPath(import.meta.url));
|
|
314
|
-
var PROMPTS_DIR = basename(__dir) === "lib" ? join(__dir, "..", "prompts") : join(__dir, "prompts");
|
|
315
|
-
function stripPromptHeader(raw) {
|
|
316
|
-
return raw.replace(/^(#[^\n]*\n)+\n?/, "").trim();
|
|
317
|
-
}
|
|
318
|
-
function loadPrompt(name) {
|
|
319
|
-
return stripPromptHeader(readFileSync2(join(PROMPTS_DIR, `${name}.txt`), "utf8"));
|
|
320
|
-
}
|
|
321
|
-
function findOutermostBraces(text) {
|
|
322
|
-
const start = text.indexOf("{");
|
|
323
|
-
if (start === -1) return null;
|
|
324
|
-
let depth = 0;
|
|
325
|
-
for (let i = start; i < text.length; i++) {
|
|
326
|
-
if (text[i] === "{") depth++;
|
|
327
|
-
else if (text[i] === "}" && --depth === 0) return { start, end: i };
|
|
328
|
-
}
|
|
329
|
-
return null;
|
|
330
|
-
}
|
|
331
|
-
function extractJsonObject(text) {
|
|
332
|
-
const fenceMatch = text.match(/```(?:json)?\s*\n([\s\S]*?)\n```/);
|
|
333
|
-
if (fenceMatch) return fenceMatch[1].trim();
|
|
334
|
-
const bounds = findOutermostBraces(text);
|
|
335
|
-
return bounds ? text.slice(bounds.start, bounds.end + 1) : text.trim();
|
|
336
|
-
}
|
|
337
|
-
function slugify(text, maxLen = 20) {
|
|
338
|
-
return text.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, maxLen).replace(/-+$/, "");
|
|
339
|
-
}
|
|
340
|
-
function formatTimestamp(d) {
|
|
341
|
-
const p = (n) => String(n).padStart(2, "0");
|
|
342
|
-
return `${d.getFullYear()}${p(d.getMonth() + 1)}${p(d.getDate())}-${p(d.getHours())}${p(d.getMinutes())}${p(d.getSeconds())}`;
|
|
343
|
-
}
|
|
344
|
-
function timestamp() {
|
|
345
|
-
return formatTimestamp(/* @__PURE__ */ new Date());
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
// src/tasks/claude.ts
|
|
349
373
|
var DEFAULT_TOOLS = ["Read", "Edit", "Write", "Bash", "Glob", "Grep"];
|
|
350
374
|
function resolveClaudePath() {
|
|
351
375
|
try {
|
|
@@ -386,8 +410,7 @@ async function* runClaude(task) {
|
|
|
386
410
|
env: { ...process.env }
|
|
387
411
|
});
|
|
388
412
|
} catch (err) {
|
|
389
|
-
|
|
390
|
-
throw new Error(`Failed to spawn claude (${claudeBin}): ${msg}`);
|
|
413
|
+
throw new Error(`Failed to spawn claude (${claudeBin}): ${getErrorMessage(err)}`);
|
|
391
414
|
}
|
|
392
415
|
const cleanup = () => {
|
|
393
416
|
try {
|
|
@@ -491,20 +514,19 @@ var JudgeOutputSchema = z2.object({
|
|
|
491
514
|
reasoning: z2.string().optional(),
|
|
492
515
|
feedback: z2.string()
|
|
493
516
|
});
|
|
517
|
+
function shouldSkipStep(stepNumber, name, options2) {
|
|
518
|
+
if (options2.stepFilter !== void 0) {
|
|
519
|
+
const matchByIndex = /^\d+$/.test(options2.stepFilter) && parseInt(options2.stepFilter, 10) === stepNumber;
|
|
520
|
+
return !matchByIndex && name !== options2.stepFilter;
|
|
521
|
+
}
|
|
522
|
+
return options2.fromStep !== void 0 && stepNumber < options2.fromStep;
|
|
523
|
+
}
|
|
494
524
|
async function* runWorkflow(workflow2, options2 = {}) {
|
|
495
525
|
const workflowStart = Date.now();
|
|
496
526
|
yield { type: "workflow:start", workflow: workflow2 };
|
|
497
527
|
for (const [i, task] of workflow2.tasks.entries()) {
|
|
498
528
|
const stepNumber = i + 1;
|
|
499
|
-
if (
|
|
500
|
-
const matchByIndex = /^\d+$/.test(options2.stepFilter) && parseInt(options2.stepFilter, 10) === stepNumber;
|
|
501
|
-
const matchByName = task.name === options2.stepFilter;
|
|
502
|
-
if (!matchByIndex && !matchByName) {
|
|
503
|
-
yield { type: "step:skip", index: i, name: task.name };
|
|
504
|
-
continue;
|
|
505
|
-
}
|
|
506
|
-
}
|
|
507
|
-
if (options2.fromStep !== void 0 && stepNumber < options2.fromStep) {
|
|
529
|
+
if (shouldSkipStep(stepNumber, task.name, options2)) {
|
|
508
530
|
yield { type: "step:skip", index: i, name: task.name };
|
|
509
531
|
continue;
|
|
510
532
|
}
|
|
@@ -512,7 +534,7 @@ async function* runWorkflow(workflow2, options2 = {}) {
|
|
|
512
534
|
yield { type: "step:start", index: i, name: task.name };
|
|
513
535
|
try {
|
|
514
536
|
for await (const event of runStep(task)) {
|
|
515
|
-
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") {
|
|
516
538
|
yield { ...event, index: i };
|
|
517
539
|
} else {
|
|
518
540
|
yield event;
|
|
@@ -573,21 +595,55 @@ async function* runLog(task) {
|
|
|
573
595
|
async function* runForEach(task) {
|
|
574
596
|
const items = await resolveItems(task.forEach);
|
|
575
597
|
const total = items.length;
|
|
598
|
+
const innerTotal = task.inner.length;
|
|
576
599
|
for (const [i, item] of items.entries()) {
|
|
577
600
|
yield { type: "step:iteration", index: -1, item, iteration: i + 1, total };
|
|
578
|
-
const
|
|
579
|
-
|
|
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
|
+
}
|
|
580
632
|
}
|
|
581
633
|
}
|
|
582
634
|
async function resolveItems(forEach) {
|
|
583
635
|
if (Array.isArray(forEach)) return forEach.filter(Boolean);
|
|
584
636
|
try {
|
|
585
|
-
const { stdout } = await execPromise(forEach, {
|
|
637
|
+
const { stdout } = await execPromise(forEach, {
|
|
638
|
+
shell: "/bin/sh",
|
|
639
|
+
timeout: 3e4
|
|
640
|
+
});
|
|
586
641
|
return stdout.split("\n").filter((l) => l.trim().length > 0);
|
|
587
642
|
} catch (err) {
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
Command: ${forEach}`
|
|
643
|
+
throw new Error(
|
|
644
|
+
`forEach shell command failed: ${getErrorMessage(err)}
|
|
645
|
+
Command: ${forEach}`
|
|
646
|
+
);
|
|
591
647
|
}
|
|
592
648
|
}
|
|
593
649
|
function substituteItem(task, item) {
|
|
@@ -596,12 +652,24 @@ function substituteItem(task, item) {
|
|
|
596
652
|
case "command":
|
|
597
653
|
return { ...task, name: sub(task.name), command: sub(task.command) };
|
|
598
654
|
case "claude":
|
|
599
|
-
return {
|
|
655
|
+
return {
|
|
656
|
+
...task,
|
|
657
|
+
name: sub(task.name),
|
|
658
|
+
prompt: sub(task.prompt),
|
|
659
|
+
allowedTools: task.allowedTools?.map(sub)
|
|
660
|
+
};
|
|
600
661
|
case "log":
|
|
601
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
|
+
};
|
|
602
670
|
default: {
|
|
603
671
|
const _ = task;
|
|
604
|
-
throw new Error(`Unknown
|
|
672
|
+
throw new Error(`Unknown task type: ${JSON.stringify(_)}`);
|
|
605
673
|
}
|
|
606
674
|
}
|
|
607
675
|
}
|
|
@@ -613,7 +681,11 @@ async function* runCommandWithHealing(task) {
|
|
|
613
681
|
try {
|
|
614
682
|
yield* collectLines(runCommand(task), lines);
|
|
615
683
|
if (attempt > 0) {
|
|
616
|
-
yield {
|
|
684
|
+
yield {
|
|
685
|
+
type: "log",
|
|
686
|
+
level: "info",
|
|
687
|
+
text: `[self-healing] Command passed after ${attempt + 1} attempts`
|
|
688
|
+
};
|
|
617
689
|
}
|
|
618
690
|
return;
|
|
619
691
|
} catch (err) {
|
|
@@ -621,7 +693,11 @@ async function* runCommandWithHealing(task) {
|
|
|
621
693
|
const output = lines.join("\n");
|
|
622
694
|
const remaining = maxAttempts - attempt - 1;
|
|
623
695
|
if (remaining === 0) {
|
|
624
|
-
yield {
|
|
696
|
+
yield {
|
|
697
|
+
type: "log",
|
|
698
|
+
level: "warn",
|
|
699
|
+
text: `[self-healing] Exhausted ${maxAttempts} attempts`
|
|
700
|
+
};
|
|
625
701
|
throw new Error(
|
|
626
702
|
`Step "${task.name}" failed after ${maxAttempts} self-healing attempts (last exit code: ${exitCode})`
|
|
627
703
|
);
|
|
@@ -632,7 +708,12 @@ async function* runCommandWithHealing(task) {
|
|
|
632
708
|
text: `[self-healing] Attempt ${attempt + 1}/${maxAttempts} failed (exit ${exitCode}), invoking Claude to fix\u2026`
|
|
633
709
|
};
|
|
634
710
|
const historyBlock = buildAttemptHistory(attemptHistory);
|
|
635
|
-
const healPrompt = buildHealingPrompt(
|
|
711
|
+
const healPrompt = buildHealingPrompt(
|
|
712
|
+
task.command,
|
|
713
|
+
exitCode,
|
|
714
|
+
output,
|
|
715
|
+
historyBlock
|
|
716
|
+
);
|
|
636
717
|
const healTask = {
|
|
637
718
|
type: "claude",
|
|
638
719
|
name: `${task.name}:heal-${attempt + 1}`,
|
|
@@ -643,7 +724,8 @@ async function* runCommandWithHealing(task) {
|
|
|
643
724
|
const claudeLines = [];
|
|
644
725
|
for await (const event of runClaude(healTask)) {
|
|
645
726
|
if (event.type === "output:text") claudeLines.push(event.text);
|
|
646
|
-
else if (event.type === "output:tool")
|
|
727
|
+
else if (event.type === "output:tool")
|
|
728
|
+
toolCalls.push(formatToolCall(event.tool, event.input));
|
|
647
729
|
yield event;
|
|
648
730
|
}
|
|
649
731
|
attemptHistory.push({
|
|
@@ -651,7 +733,11 @@ async function* runCommandWithHealing(task) {
|
|
|
651
733
|
exitCode,
|
|
652
734
|
cmdOutput: output
|
|
653
735
|
});
|
|
654
|
-
yield {
|
|
736
|
+
yield {
|
|
737
|
+
type: "log",
|
|
738
|
+
level: "info",
|
|
739
|
+
text: `[self-healing] Re-running command (${remaining} attempt(s) left)\u2026`
|
|
740
|
+
};
|
|
655
741
|
}
|
|
656
742
|
}
|
|
657
743
|
}
|
|
@@ -660,24 +746,40 @@ async function* runClaudeWithJudge(task) {
|
|
|
660
746
|
for (let attempt = 0; attempt < MAX_JUDGE_RETRIES; attempt++) {
|
|
661
747
|
const prompt = attempt === 0 ? task.prompt : `${task.prompt}
|
|
662
748
|
|
|
663
|
-
${JUDGE_RETRY_CONTEXT
|
|
749
|
+
${fillTemplate(JUDGE_RETRY_CONTEXT, { FEEDBACK: judgeContext })}`;
|
|
664
750
|
const lines = [];
|
|
665
751
|
yield* collectLines(runClaude({ ...task, prompt }), lines);
|
|
666
|
-
yield {
|
|
667
|
-
|
|
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
|
+
);
|
|
668
762
|
if (verdict.pass) {
|
|
669
763
|
yield { type: "log", level: "info", text: "[judge] PASS" };
|
|
670
764
|
return;
|
|
671
765
|
}
|
|
672
766
|
judgeContext = verdict.feedback;
|
|
673
|
-
yield {
|
|
767
|
+
yield {
|
|
768
|
+
type: "log",
|
|
769
|
+
level: "warn",
|
|
770
|
+
text: `[judge] FAIL \u2014 ${verdict.feedback}`
|
|
771
|
+
};
|
|
674
772
|
const remaining = MAX_JUDGE_RETRIES - attempt - 1;
|
|
675
773
|
if (remaining === 0) {
|
|
676
774
|
throw new Error(
|
|
677
775
|
`Step "${task.name}" failed judge evaluation after ${MAX_JUDGE_RETRIES} attempts`
|
|
678
776
|
);
|
|
679
777
|
}
|
|
680
|
-
yield {
|
|
778
|
+
yield {
|
|
779
|
+
type: "log",
|
|
780
|
+
level: "info",
|
|
781
|
+
text: `[judge] Retrying (${remaining} attempt(s) left)\u2026`
|
|
782
|
+
};
|
|
681
783
|
}
|
|
682
784
|
}
|
|
683
785
|
async function evaluateWithJudge(stepName, stepInstructions, output) {
|
|
@@ -704,8 +806,9 @@ function readContextFile(filePath2) {
|
|
|
704
806
|
try {
|
|
705
807
|
return readFileSync3(filePath2, "utf8");
|
|
706
808
|
} catch (err) {
|
|
707
|
-
|
|
708
|
-
|
|
809
|
+
throw new Error(
|
|
810
|
+
`Context file "${filePath2}" could not be read: ${getErrorMessage(err)}`
|
|
811
|
+
);
|
|
709
812
|
}
|
|
710
813
|
}
|
|
711
814
|
function expandContext(task) {
|
|
@@ -719,13 +822,23 @@ ${readContextFile(fp)}
|
|
|
719
822
|
${task.prompt}` };
|
|
720
823
|
}
|
|
721
824
|
function buildHealingPrompt(command, exitCode, output, attemptHistory) {
|
|
722
|
-
return 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
|
+
});
|
|
723
831
|
}
|
|
724
832
|
function buildJudgePrompt(stepName, instructions, output) {
|
|
725
|
-
return JUDGE_EVALUATION_PROMPT
|
|
833
|
+
return fillTemplate(JUDGE_EVALUATION_PROMPT, {
|
|
834
|
+
STEP_NAME: stepName,
|
|
835
|
+
STEP_INSTRUCTIONS: instructions,
|
|
836
|
+
OUTPUT: output
|
|
837
|
+
});
|
|
726
838
|
}
|
|
727
839
|
function formatToolCall(tool, input) {
|
|
728
|
-
if (tool === "Edit" || tool === "Write")
|
|
840
|
+
if (tool === "Edit" || tool === "Write")
|
|
841
|
+
return `${tool}(${String(input["file_path"] ?? "")})`;
|
|
729
842
|
if (tool === "Bash") return `Bash(${String(input["command"] ?? "")})`;
|
|
730
843
|
return tool;
|
|
731
844
|
}
|
|
@@ -842,7 +955,20 @@ function reducer(state, event) {
|
|
|
842
955
|
};
|
|
843
956
|
case "step:iteration":
|
|
844
957
|
return updateTask(state, event.index, {
|
|
845
|
-
iteration: {
|
|
958
|
+
iteration: {
|
|
959
|
+
current: event.iteration,
|
|
960
|
+
total: event.total,
|
|
961
|
+
item: event.item
|
|
962
|
+
},
|
|
963
|
+
inner: void 0
|
|
964
|
+
});
|
|
965
|
+
case "step:inner":
|
|
966
|
+
return updateTask(state, event.index, {
|
|
967
|
+
inner: {
|
|
968
|
+
index: event.innerIndex,
|
|
969
|
+
total: event.innerTotal,
|
|
970
|
+
name: event.name
|
|
971
|
+
}
|
|
846
972
|
});
|
|
847
973
|
case "output:text": {
|
|
848
974
|
const idx = event.index;
|
|
@@ -855,7 +981,10 @@ function reducer(state, event) {
|
|
|
855
981
|
const formatted = formatToolCall2(event.tool, event.input);
|
|
856
982
|
const next = formatted ? appendLine(state, idx, formatted) : state;
|
|
857
983
|
if (event.tool === "Write" && typeof event.input["file_path"] === "string") {
|
|
858
|
-
return {
|
|
984
|
+
return {
|
|
985
|
+
...next,
|
|
986
|
+
writtenFiles: [...next.writtenFiles, event.input["file_path"]]
|
|
987
|
+
};
|
|
859
988
|
}
|
|
860
989
|
return next;
|
|
861
990
|
}
|
|
@@ -878,7 +1007,9 @@ function reducer(state, event) {
|
|
|
878
1007
|
}
|
|
879
1008
|
}
|
|
880
1009
|
function updateTask(state, index, patch) {
|
|
881
|
-
const tasks = state.tasks.map(
|
|
1010
|
+
const tasks = state.tasks.map(
|
|
1011
|
+
(t, i) => i === index ? { ...t, ...patch } : t
|
|
1012
|
+
);
|
|
882
1013
|
return { ...state, tasks };
|
|
883
1014
|
}
|
|
884
1015
|
function appendLine(state, index, line) {
|
|
@@ -943,7 +1074,8 @@ function TaskRow({ taskState, isActive, index, tick }) {
|
|
|
943
1074
|
const color = statusColor(status, isActive);
|
|
944
1075
|
const elapsed = formatTaskElapsed(startTime, endTime, status);
|
|
945
1076
|
const iterInfo = taskState.iteration ? ` (${taskState.iteration.current}/${taskState.iteration.total}) ${taskState.iteration.item}` : "";
|
|
946
|
-
const
|
|
1077
|
+
const innerInfo = taskState.inner ? ` \u2014 ${taskState.inner.name} [${taskState.inner.index + 1}/${taskState.inner.total}]` : "";
|
|
1078
|
+
const label = `${index + 1}. ${task.name}${iterInfo}${innerInfo}`;
|
|
947
1079
|
return /* @__PURE__ */ jsxs(Box, { children: [
|
|
948
1080
|
/* @__PURE__ */ jsxs(Text, { color, children: [
|
|
949
1081
|
icon,
|
|
@@ -1070,12 +1202,8 @@ function App({ workflow: workflow2, events: events2, options: options2, updateCh
|
|
|
1070
1202
|
}
|
|
1071
1203
|
} catch (err) {
|
|
1072
1204
|
if (!active) return;
|
|
1073
|
-
dispatch({
|
|
1074
|
-
|
|
1075
|
-
level: "error",
|
|
1076
|
-
text: err instanceof Error ? err.message : String(err)
|
|
1077
|
-
});
|
|
1078
|
-
setTimeout(() => exit(err instanceof Error ? err : new Error(String(err))), EXIT_DELAY_MS);
|
|
1205
|
+
dispatch({ type: "log", level: "error", text: getErrorMessage(err) });
|
|
1206
|
+
setTimeout(() => exit(err instanceof Error ? err : new Error(getErrorMessage(err))), EXIT_DELAY_MS);
|
|
1079
1207
|
}
|
|
1080
1208
|
})();
|
|
1081
1209
|
return () => {
|
|
@@ -1157,25 +1285,9 @@ var PLAN_RETRY_SCHEMA_ERROR = loadPrompt("plan-retry-schema-error");
|
|
|
1157
1285
|
var PLAN_RETRY_JUDGE = loadPrompt("plan-retry-judge");
|
|
1158
1286
|
var MAX_PLAN_RETRIES = 3;
|
|
1159
1287
|
var TOTAL_PLAN_STAGES = 3;
|
|
1160
|
-
var StepSchema = z3.object({
|
|
1161
|
-
name: z3.string(),
|
|
1162
|
-
type: z3.enum(["prompt", "script", "log"]).optional(),
|
|
1163
|
-
prompt: z3.string().optional(),
|
|
1164
|
-
command: z3.string().optional(),
|
|
1165
|
-
message: z3.string().optional(),
|
|
1166
|
-
continue_on_error: z3.boolean().optional(),
|
|
1167
|
-
self_healing: z3.boolean().optional(),
|
|
1168
|
-
max_healing_attempts: z3.number().int().positive().optional(),
|
|
1169
|
-
output: z3.string().optional(),
|
|
1170
|
-
llm_as_judge: z3.boolean().optional(),
|
|
1171
|
-
allowed_tools: z3.array(z3.string()).optional(),
|
|
1172
|
-
forEach: z3.union([z3.array(z3.string()), z3.string()]).optional(),
|
|
1173
|
-
repeat: z3.number().int().positive().optional(),
|
|
1174
|
-
context: z3.array(z3.string()).optional()
|
|
1175
|
-
});
|
|
1176
1288
|
var WorkflowSchema = z3.object({
|
|
1177
1289
|
goal: z3.string(),
|
|
1178
|
-
steps: z3.array(
|
|
1290
|
+
steps: z3.array(RawStepSchema).min(1),
|
|
1179
1291
|
vars: z3.record(z3.string()).optional(),
|
|
1180
1292
|
self_improve: z3.boolean().optional()
|
|
1181
1293
|
});
|
|
@@ -1195,7 +1307,10 @@ function walkUp(startDir, check) {
|
|
|
1195
1307
|
}
|
|
1196
1308
|
}
|
|
1197
1309
|
function findGitRoot(startDir) {
|
|
1198
|
-
return walkUp(
|
|
1310
|
+
return walkUp(
|
|
1311
|
+
startDir,
|
|
1312
|
+
(dir) => existsSync(join2(dir, ".git")) ? dir : null
|
|
1313
|
+
);
|
|
1199
1314
|
}
|
|
1200
1315
|
function findProjectRoot(startDir) {
|
|
1201
1316
|
return walkUp(startDir, (dir) => {
|
|
@@ -1283,7 +1398,10 @@ async function runPass3Judge(description, workflow2) {
|
|
|
1283
1398
|
const task = {
|
|
1284
1399
|
type: "claude",
|
|
1285
1400
|
name: "plan:judge",
|
|
1286
|
-
prompt: PLAN_JUDGE_PROMPT
|
|
1401
|
+
prompt: fillTemplate(PLAN_JUDGE_PROMPT, {
|
|
1402
|
+
DESCRIPTION: description,
|
|
1403
|
+
WORKFLOW_JSON: JSON.stringify(workflow2, null, 2)
|
|
1404
|
+
}),
|
|
1287
1405
|
allowedTools: [],
|
|
1288
1406
|
permissionMode: "default",
|
|
1289
1407
|
model: "sonnet"
|
|
@@ -1343,34 +1461,27 @@ function normalizeWorkflow(workflow2) {
|
|
|
1343
1461
|
return { ...workflow2, steps: collapseSequentialSteps(steps) };
|
|
1344
1462
|
}
|
|
1345
1463
|
function collapseSequentialSteps(steps) {
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
continue;
|
|
1368
|
-
}
|
|
1369
|
-
const { name: _name, ...rest } = step;
|
|
1370
|
-
result.push({ ...rest, name: `${prefix}_{{item}}`, repeat: n });
|
|
1371
|
-
i += n;
|
|
1372
|
-
}
|
|
1373
|
-
return result;
|
|
1464
|
+
return steps.reduce(
|
|
1465
|
+
({ out, skip }, step, i, arr) => {
|
|
1466
|
+
if (skip > 0) return { out, skip: skip - 1 };
|
|
1467
|
+
if (step.forEach !== void 0 || step.repeat !== void 0 || step.steps !== void 0) {
|
|
1468
|
+
return { out: [...out, step], skip: 0 };
|
|
1469
|
+
}
|
|
1470
|
+
const m = step.name.match(/^(.+?)_1$/);
|
|
1471
|
+
if (!m) return { out: [...out, step], skip: 0 };
|
|
1472
|
+
const prefix = m[1];
|
|
1473
|
+
let n = 1;
|
|
1474
|
+
while (i + n < arr.length && arr[i + n].name === `${prefix}_${n + 1}`)
|
|
1475
|
+
n++;
|
|
1476
|
+
if (n < 2) return { out: [...out, step], skip: 0 };
|
|
1477
|
+
const { name: _name, ...rest } = step;
|
|
1478
|
+
return {
|
|
1479
|
+
out: [...out, { ...rest, name: `${prefix}_{{item}}`, repeat: n }],
|
|
1480
|
+
skip: n - 1
|
|
1481
|
+
};
|
|
1482
|
+
},
|
|
1483
|
+
{ out: [], skip: 0 }
|
|
1484
|
+
).out;
|
|
1374
1485
|
}
|
|
1375
1486
|
async function* streamPlan(args) {
|
|
1376
1487
|
const { description, taskFile } = args;
|
|
@@ -1381,14 +1492,24 @@ async function* streamPlan(args) {
|
|
|
1381
1492
|
yield { type: "plan:stages", names: ["Decompose to Steps", "Validate"] };
|
|
1382
1493
|
researchDoc = "No codebase research performed \u2014 the task is self-contained. Work directly from the user's original goal.";
|
|
1383
1494
|
} else {
|
|
1384
|
-
yield {
|
|
1385
|
-
|
|
1495
|
+
yield {
|
|
1496
|
+
type: "plan:stages",
|
|
1497
|
+
names: ["Research & Planning", "Decompose to Steps", "Validate"]
|
|
1498
|
+
};
|
|
1499
|
+
yield {
|
|
1500
|
+
type: "plan:stage",
|
|
1501
|
+
stage: 1,
|
|
1502
|
+
total: TOTAL_PLAN_STAGES,
|
|
1503
|
+
name: "Research & Planning"
|
|
1504
|
+
};
|
|
1386
1505
|
const researchLines = [];
|
|
1387
1506
|
try {
|
|
1388
1507
|
const researchTask = {
|
|
1389
1508
|
type: "claude",
|
|
1390
1509
|
name: "plan:research",
|
|
1391
|
-
prompt: PLAN_RESEARCH_PROMPT
|
|
1510
|
+
prompt: fillTemplate(PLAN_RESEARCH_PROMPT, {
|
|
1511
|
+
DESCRIPTION: description
|
|
1512
|
+
}),
|
|
1392
1513
|
allowedTools: ["Read", "Glob", "Grep"],
|
|
1393
1514
|
permissionMode: "bypassPermissions",
|
|
1394
1515
|
model: "opus"
|
|
@@ -1402,20 +1523,28 @@ async function* streamPlan(args) {
|
|
|
1402
1523
|
}
|
|
1403
1524
|
}
|
|
1404
1525
|
} catch (err) {
|
|
1405
|
-
|
|
1406
|
-
|
|
1526
|
+
yield {
|
|
1527
|
+
type: "plan:error",
|
|
1528
|
+
message: `Research pass failed: ${getErrorMessage(err)}`
|
|
1529
|
+
};
|
|
1407
1530
|
return;
|
|
1408
1531
|
}
|
|
1409
1532
|
researchDoc = researchLines.join("\n");
|
|
1410
1533
|
if (!researchDoc.trim()) {
|
|
1411
|
-
yield {
|
|
1534
|
+
yield {
|
|
1535
|
+
type: "plan:error",
|
|
1536
|
+
message: "Research pass produced no output \u2014 cannot decompose"
|
|
1537
|
+
};
|
|
1412
1538
|
return;
|
|
1413
1539
|
}
|
|
1414
1540
|
}
|
|
1415
|
-
const
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1541
|
+
const stages = skipResearch ? { decompose: 1, validate: 2, total: 2 } : { decompose: 2, validate: 3, total: TOTAL_PLAN_STAGES };
|
|
1542
|
+
yield {
|
|
1543
|
+
type: "plan:stage",
|
|
1544
|
+
stage: stages.decompose,
|
|
1545
|
+
total: stages.total,
|
|
1546
|
+
name: "Decompose to Steps"
|
|
1547
|
+
};
|
|
1419
1548
|
let retryPrefix = "";
|
|
1420
1549
|
for (let attempt = 0; attempt < MAX_PLAN_RETRIES; attempt++) {
|
|
1421
1550
|
if (attempt > 0) {
|
|
@@ -1425,9 +1554,17 @@ async function* streamPlan(args) {
|
|
|
1425
1554
|
maxAttempts: MAX_PLAN_RETRIES,
|
|
1426
1555
|
reason: retryPrefix.replace(/\n/g, " ")
|
|
1427
1556
|
};
|
|
1428
|
-
yield {
|
|
1557
|
+
yield {
|
|
1558
|
+
type: "plan:stage",
|
|
1559
|
+
stage: stages.decompose,
|
|
1560
|
+
total: stages.total,
|
|
1561
|
+
name: "Decompose to Steps"
|
|
1562
|
+
};
|
|
1429
1563
|
}
|
|
1430
|
-
const basePrompt = PLAN_DECOMPOSE_PROMPT
|
|
1564
|
+
const basePrompt = fillTemplate(PLAN_DECOMPOSE_PROMPT, {
|
|
1565
|
+
DESCRIPTION: description,
|
|
1566
|
+
RESEARCH_DOC: researchDoc
|
|
1567
|
+
});
|
|
1431
1568
|
const decomposeTask = {
|
|
1432
1569
|
type: "claude",
|
|
1433
1570
|
name: "plan:decompose",
|
|
@@ -1454,12 +1591,15 @@ ${basePrompt}` : basePrompt,
|
|
|
1454
1591
|
}
|
|
1455
1592
|
}
|
|
1456
1593
|
} catch (err) {
|
|
1457
|
-
const msg =
|
|
1594
|
+
const msg = getErrorMessage(err);
|
|
1458
1595
|
if (attempt === MAX_PLAN_RETRIES - 1) {
|
|
1459
1596
|
yield { type: "plan:error", message: msg };
|
|
1460
1597
|
return;
|
|
1461
1598
|
}
|
|
1462
|
-
retryPrefix = PLAN_RETRY_PARSE_ERROR
|
|
1599
|
+
retryPrefix = fillTemplate(PLAN_RETRY_PARSE_ERROR, {
|
|
1600
|
+
ERROR: msg,
|
|
1601
|
+
EXCERPT: decomposeTextLines.join("\n")
|
|
1602
|
+
});
|
|
1463
1603
|
continue;
|
|
1464
1604
|
}
|
|
1465
1605
|
if (structuredOutput === void 0) {
|
|
@@ -1468,31 +1608,47 @@ ${basePrompt}` : basePrompt,
|
|
|
1468
1608
|
yield { type: "plan:error", message: issues };
|
|
1469
1609
|
return;
|
|
1470
1610
|
}
|
|
1471
|
-
retryPrefix = PLAN_RETRY_SCHEMA_ERROR
|
|
1611
|
+
retryPrefix = fillTemplate(PLAN_RETRY_SCHEMA_ERROR, { ISSUES: issues });
|
|
1472
1612
|
continue;
|
|
1473
1613
|
}
|
|
1474
1614
|
const zodResult = WorkflowSchema.safeParse(structuredOutput);
|
|
1475
1615
|
if (!zodResult.success) {
|
|
1476
|
-
const issues = zodResult.error.issues
|
|
1616
|
+
const issues = formatZodIssues(zodResult.error.issues);
|
|
1477
1617
|
if (attempt === MAX_PLAN_RETRIES - 1) {
|
|
1478
|
-
yield {
|
|
1479
|
-
|
|
1618
|
+
yield {
|
|
1619
|
+
type: "plan:error",
|
|
1620
|
+
message: `Plan did not match expected schema:
|
|
1621
|
+
${issues}`
|
|
1622
|
+
};
|
|
1480
1623
|
return;
|
|
1481
1624
|
}
|
|
1482
|
-
retryPrefix = PLAN_RETRY_SCHEMA_ERROR
|
|
1625
|
+
retryPrefix = fillTemplate(PLAN_RETRY_SCHEMA_ERROR, { ISSUES: issues });
|
|
1483
1626
|
continue;
|
|
1484
1627
|
}
|
|
1485
|
-
yield {
|
|
1628
|
+
yield {
|
|
1629
|
+
type: "plan:stage",
|
|
1630
|
+
stage: stages.validate,
|
|
1631
|
+
total: stages.total,
|
|
1632
|
+
name: "Validate"
|
|
1633
|
+
};
|
|
1486
1634
|
const judgeResult = await runPass3Judge(description, zodResult.data);
|
|
1487
1635
|
if (judgeResult.skipped) {
|
|
1488
|
-
yield {
|
|
1636
|
+
yield {
|
|
1637
|
+
type: "plan:warn",
|
|
1638
|
+
message: "Judge skipped due to error \u2014 proceeding without validation"
|
|
1639
|
+
};
|
|
1489
1640
|
}
|
|
1490
1641
|
if (!judgeResult.pass && attempt < MAX_PLAN_RETRIES - 1) {
|
|
1491
|
-
retryPrefix = PLAN_RETRY_JUDGE
|
|
1642
|
+
retryPrefix = fillTemplate(PLAN_RETRY_JUDGE, {
|
|
1643
|
+
FEEDBACK: judgeResult.feedback
|
|
1644
|
+
});
|
|
1492
1645
|
continue;
|
|
1493
1646
|
}
|
|
1494
1647
|
if (!judgeResult.pass) {
|
|
1495
|
-
yield {
|
|
1648
|
+
yield {
|
|
1649
|
+
type: "plan:warn",
|
|
1650
|
+
message: `Judge rejected plan but retries exhausted: ${judgeResult.feedback}`
|
|
1651
|
+
};
|
|
1496
1652
|
}
|
|
1497
1653
|
const { goal, vars, steps, ...rest } = normalizeWorkflow(zodResult.data);
|
|
1498
1654
|
const ordered = { goal, ...vars && { vars }, steps, ...rest };
|
|
@@ -1503,11 +1659,15 @@ ${issues}` };
|
|
|
1503
1659
|
forceQuotes: false
|
|
1504
1660
|
}).trimEnd();
|
|
1505
1661
|
writeFileSync2(taskFile, yamlContent + "\n", "utf8");
|
|
1506
|
-
const
|
|
1662
|
+
const yamlLines = yamlContent.split("\n");
|
|
1663
|
+
const preview = yamlLines.slice(0, 30).join("\n") + (yamlLines.length > 30 ? "\n..." : "");
|
|
1507
1664
|
yield { type: "plan:complete", taskFile, preview };
|
|
1508
1665
|
return;
|
|
1509
1666
|
}
|
|
1510
|
-
yield {
|
|
1667
|
+
yield {
|
|
1668
|
+
type: "plan:error",
|
|
1669
|
+
message: "Plan generation failed after maximum retries"
|
|
1670
|
+
};
|
|
1511
1671
|
}
|
|
1512
1672
|
|
|
1513
1673
|
// src/ui/PlanApp.tsx
|
|
@@ -1687,17 +1847,6 @@ import {
|
|
|
1687
1847
|
writeFileSync as writeFileSync3
|
|
1688
1848
|
} from "node:fs";
|
|
1689
1849
|
import { dirname as dirname3, join as join3, resolve as resolve2 } from "node:path";
|
|
1690
|
-
var TOOL_SUMMARY = {
|
|
1691
|
-
Read: (i) => String(i["file_path"] ?? i["path"] ?? ""),
|
|
1692
|
-
Edit: (i) => String(i["file_path"] ?? ""),
|
|
1693
|
-
Write: (i) => String(i["file_path"] ?? ""),
|
|
1694
|
-
Bash: (i) => String(i["command"] ?? ""),
|
|
1695
|
-
Glob: (i) => String(i["pattern"] ?? ""),
|
|
1696
|
-
Grep: (i) => String(i["pattern"] ?? "")
|
|
1697
|
-
};
|
|
1698
|
-
function toolSummary(tool, input) {
|
|
1699
|
-
return (TOOL_SUMMARY[tool] ?? ((i) => JSON.stringify(i)))(input);
|
|
1700
|
-
}
|
|
1701
1850
|
function findExecutantLocalDir(startDir) {
|
|
1702
1851
|
let dir = resolve2(startDir);
|
|
1703
1852
|
while (true) {
|
|
@@ -1711,337 +1860,336 @@ function findExecutantLocalDir(startDir) {
|
|
|
1711
1860
|
function resolveLogDir(workflowFilePath) {
|
|
1712
1861
|
const startDir = dirname3(resolve2(workflowFilePath));
|
|
1713
1862
|
const executantLocal = findExecutantLocalDir(startDir);
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
}
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
}
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
console.warn(`[logger] error: ${err instanceof Error ? err.message : String(err)}`);
|
|
1753
|
-
}
|
|
1754
|
-
}
|
|
1755
|
-
// --------------------------------------------------------------------------
|
|
1756
|
-
// Event dispatch
|
|
1757
|
-
// --------------------------------------------------------------------------
|
|
1758
|
-
dispatch(event) {
|
|
1759
|
-
switch (event.type) {
|
|
1760
|
-
case "workflow:start":
|
|
1761
|
-
this.initDirs();
|
|
1762
|
-
break;
|
|
1763
|
-
case "step:start":
|
|
1764
|
-
this.onStepStart(event.index, event.name);
|
|
1765
|
-
break;
|
|
1766
|
-
case "step:complete":
|
|
1767
|
-
this.onStepComplete();
|
|
1768
|
-
break;
|
|
1769
|
-
case "step:error":
|
|
1770
|
-
this.onStepError(event.error);
|
|
1771
|
-
break;
|
|
1772
|
-
case "output:text":
|
|
1773
|
-
this.appendLog(event.text);
|
|
1774
|
-
this.recentOutput.push(event.text);
|
|
1775
|
-
break;
|
|
1776
|
-
case "output:tool":
|
|
1777
|
-
this.onTool(event.tool, event.input);
|
|
1778
|
-
break;
|
|
1779
|
-
case "log":
|
|
1780
|
-
this.onLogMessage(event.level, event.text);
|
|
1781
|
-
break;
|
|
1782
|
-
case "workflow:complete":
|
|
1783
|
-
this.onWorkflowComplete();
|
|
1784
|
-
break;
|
|
1785
|
-
}
|
|
1786
|
-
}
|
|
1787
|
-
// --------------------------------------------------------------------------
|
|
1788
|
-
// Initialisation
|
|
1789
|
-
// --------------------------------------------------------------------------
|
|
1790
|
-
initDirs() {
|
|
1791
|
-
mkdirSync3(this.logDir, { recursive: true });
|
|
1792
|
-
mkdirSync3(this.highlightsDir, { recursive: true });
|
|
1793
|
-
this.logFile = join3(this.logDir, `${this.timestamp}_${this.taskName}.log`);
|
|
1794
|
-
writeFileSync3(
|
|
1795
|
-
this.logFile,
|
|
1796
|
-
`# Execution Log
|
|
1797
|
-
Task: ${this.taskName}
|
|
1863
|
+
return executantLocal ? join3(executantLocal, "logs") : join3(startDir, "logs");
|
|
1864
|
+
}
|
|
1865
|
+
var INIT_STATE = {
|
|
1866
|
+
logFile: "",
|
|
1867
|
+
stepIndex: -1,
|
|
1868
|
+
stepName: "",
|
|
1869
|
+
stepStartMs: 0,
|
|
1870
|
+
toolCount: 0,
|
|
1871
|
+
complexSequenceFile: "",
|
|
1872
|
+
selfHealingFile: "",
|
|
1873
|
+
judgeAttempt: 0,
|
|
1874
|
+
recentOutput: []
|
|
1875
|
+
};
|
|
1876
|
+
var TOOL_SUMMARY = {
|
|
1877
|
+
Read: (i) => String(i["file_path"] ?? i["path"] ?? ""),
|
|
1878
|
+
Edit: (i) => String(i["file_path"] ?? ""),
|
|
1879
|
+
Write: (i) => String(i["file_path"] ?? ""),
|
|
1880
|
+
Bash: (i) => String(i["command"] ?? ""),
|
|
1881
|
+
Glob: (i) => String(i["pattern"] ?? ""),
|
|
1882
|
+
Grep: (i) => String(i["pattern"] ?? "")
|
|
1883
|
+
};
|
|
1884
|
+
function toolSummary(tool, input) {
|
|
1885
|
+
return (TOOL_SUMMARY[tool] ?? ((i) => JSON.stringify(i)))(input);
|
|
1886
|
+
}
|
|
1887
|
+
function appendLog(logFile, text) {
|
|
1888
|
+
if (logFile) appendFileSync(logFile, text + "\n");
|
|
1889
|
+
}
|
|
1890
|
+
function highlightPath(ctx, stepIndex, suffix) {
|
|
1891
|
+
return join3(ctx.highlightsDir, `${ctx.ts}_step${stepIndex + 1}_${suffix}.md`);
|
|
1892
|
+
}
|
|
1893
|
+
function onWorkflowStart(ctx, s) {
|
|
1894
|
+
mkdirSync3(ctx.logDir, { recursive: true });
|
|
1895
|
+
mkdirSync3(ctx.highlightsDir, { recursive: true });
|
|
1896
|
+
const logFile = join3(ctx.logDir, `${ctx.ts}_${ctx.slug}.log`);
|
|
1897
|
+
writeFileSync3(
|
|
1898
|
+
logFile,
|
|
1899
|
+
`# Execution Log
|
|
1900
|
+
Task: ${ctx.slug}
|
|
1798
1901
|
Started: ${(/* @__PURE__ */ new Date()).toISOString()}
|
|
1799
1902
|
${"\u2501".repeat(51)}
|
|
1800
1903
|
|
|
1801
1904
|
`
|
|
1802
|
-
|
|
1803
|
-
}
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
recentOutput: []
|
|
1817
|
-
});
|
|
1818
|
-
this.appendLog(
|
|
1819
|
-
`
|
|
1905
|
+
);
|
|
1906
|
+
return { ...s, logFile };
|
|
1907
|
+
}
|
|
1908
|
+
function onStepStart(ctx, s, index, name) {
|
|
1909
|
+
const next = {
|
|
1910
|
+
...INIT_STATE,
|
|
1911
|
+
logFile: s.logFile,
|
|
1912
|
+
stepIndex: index,
|
|
1913
|
+
stepName: name,
|
|
1914
|
+
stepStartMs: Date.now()
|
|
1915
|
+
};
|
|
1916
|
+
appendLog(
|
|
1917
|
+
next.logFile,
|
|
1918
|
+
`
|
|
1820
1919
|
${"\u2501".repeat(51)}
|
|
1821
1920
|
Step ${index + 1}: ${name}
|
|
1822
1921
|
Started: ${(/* @__PURE__ */ new Date()).toISOString()}
|
|
1823
1922
|
${"\u2501".repeat(51)}
|
|
1923
|
+
`
|
|
1924
|
+
);
|
|
1925
|
+
return next;
|
|
1926
|
+
}
|
|
1927
|
+
function finalizeComplexSequence(s) {
|
|
1928
|
+
if (s.toolCount >= 3 && s.complexSequenceFile) {
|
|
1929
|
+
appendFileSync(
|
|
1930
|
+
s.complexSequenceFile,
|
|
1931
|
+
`
|
|
1932
|
+
---
|
|
1933
|
+
|
|
1934
|
+
*Total tools used: ${s.toolCount}*
|
|
1935
|
+
|
|
1936
|
+
*Captured by Executant Logger*
|
|
1824
1937
|
`
|
|
1825
1938
|
);
|
|
1826
1939
|
}
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
`
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1940
|
+
}
|
|
1941
|
+
function onStepComplete(s) {
|
|
1942
|
+
appendLog(
|
|
1943
|
+
s.logFile,
|
|
1944
|
+
`
|
|
1945
|
+
Step completed in ${((Date.now() - s.stepStartMs) / 1e3).toFixed(1)}s
|
|
1946
|
+
`
|
|
1947
|
+
);
|
|
1948
|
+
finalizeComplexSequence(s);
|
|
1949
|
+
return s;
|
|
1950
|
+
}
|
|
1951
|
+
function onStepError(s, error) {
|
|
1952
|
+
appendLog(s.logFile, `
|
|
1836
1953
|
Step failed: ${error.message}
|
|
1837
1954
|
`);
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
1955
|
+
finalizeComplexSequence(s);
|
|
1956
|
+
return s;
|
|
1957
|
+
}
|
|
1958
|
+
function complexSequenceHeader(ctx, s) {
|
|
1959
|
+
return [
|
|
1960
|
+
"# Complex Tool Sequence",
|
|
1961
|
+
"",
|
|
1962
|
+
`**Task:** ${ctx.slug}`,
|
|
1963
|
+
`**Step:** ${s.stepName}`,
|
|
1964
|
+
`**Timestamp:** ${(/* @__PURE__ */ new Date()).toISOString()}`,
|
|
1965
|
+
"",
|
|
1966
|
+
"---",
|
|
1967
|
+
"",
|
|
1968
|
+
"## Claude's Tool Orchestration",
|
|
1969
|
+
"",
|
|
1970
|
+
"Claude used multiple tools to complete this step:",
|
|
1971
|
+
""
|
|
1972
|
+
].join("\n");
|
|
1973
|
+
}
|
|
1974
|
+
function createComplexSequenceFile(ctx, s) {
|
|
1975
|
+
const path = highlightPath(ctx, s.stepIndex, "complex_sequence");
|
|
1976
|
+
writeFileSync3(path, complexSequenceHeader(ctx, s));
|
|
1977
|
+
return path;
|
|
1978
|
+
}
|
|
1979
|
+
function onTool(ctx, s, tool, input) {
|
|
1980
|
+
const desc = toolSummary(tool, input);
|
|
1981
|
+
appendLog(s.logFile, ` [${tool}] ${desc}`);
|
|
1982
|
+
const toolCount = s.toolCount + 1;
|
|
1983
|
+
const complexSequenceFile = toolCount === 3 ? createComplexSequenceFile(ctx, s) : s.complexSequenceFile;
|
|
1984
|
+
if (toolCount >= 3 && complexSequenceFile) {
|
|
1985
|
+
appendFileSync(
|
|
1986
|
+
complexSequenceFile,
|
|
1987
|
+
`${toolCount}. **${tool}** - ${desc}
|
|
1988
|
+
`
|
|
1989
|
+
);
|
|
1990
|
+
}
|
|
1991
|
+
return { ...s, toolCount, complexSequenceFile };
|
|
1992
|
+
}
|
|
1993
|
+
function saveJudgeHighlight(ctx, s, verdict, text) {
|
|
1994
|
+
writeFileSync3(
|
|
1995
|
+
highlightPath(ctx, s.stepIndex, `judge_${verdict}`),
|
|
1996
|
+
[
|
|
1997
|
+
`# Judge Verdict: ${verdict}`,
|
|
1998
|
+
"",
|
|
1999
|
+
`**Task:** ${ctx.slug}`,
|
|
2000
|
+
`**Step:** ${s.stepName}`,
|
|
2001
|
+
`**Attempt:** ${s.judgeAttempt}`,
|
|
2002
|
+
`**Timestamp:** ${(/* @__PURE__ */ new Date()).toISOString()}`,
|
|
2003
|
+
"",
|
|
2004
|
+
"---",
|
|
2005
|
+
"",
|
|
2006
|
+
text,
|
|
2007
|
+
"",
|
|
2008
|
+
"---",
|
|
2009
|
+
"",
|
|
2010
|
+
"*Auto-captured*",
|
|
2011
|
+
""
|
|
2012
|
+
].join("\n")
|
|
2013
|
+
);
|
|
2014
|
+
}
|
|
2015
|
+
var LOG_MATCHERS = [
|
|
2016
|
+
{
|
|
2017
|
+
pattern: /\[judge\]\s+(PASS|FAIL)/i,
|
|
2018
|
+
apply: (ctx, s, text, match) => {
|
|
2019
|
+
const verdict = match[1].toUpperCase();
|
|
2020
|
+
const judgeAttempt = s.judgeAttempt + 1;
|
|
2021
|
+
saveJudgeHighlight(ctx, { ...s, judgeAttempt }, verdict, text);
|
|
2022
|
+
return { ...s, judgeAttempt };
|
|
2023
|
+
}
|
|
2024
|
+
},
|
|
2025
|
+
{
|
|
2026
|
+
pattern: /\[self-healing\].*failed.*exit\s+(\d+)/i,
|
|
2027
|
+
apply: (ctx, s, text, match) => {
|
|
2028
|
+
const selfHealingFile = highlightPath(ctx, s.stepIndex, "self_healing");
|
|
1852
2029
|
writeFileSync3(
|
|
1853
|
-
|
|
2030
|
+
selfHealingFile,
|
|
1854
2031
|
[
|
|
1855
|
-
"#
|
|
2032
|
+
"# Self-Healing Activation",
|
|
1856
2033
|
"",
|
|
1857
|
-
`**Task:** ${
|
|
1858
|
-
`**Step:** ${
|
|
2034
|
+
`**Task:** ${ctx.slug}`,
|
|
2035
|
+
`**Step:** ${s.stepName}`,
|
|
1859
2036
|
`**Timestamp:** ${(/* @__PURE__ */ new Date()).toISOString()}`,
|
|
1860
2037
|
"",
|
|
1861
2038
|
"---",
|
|
1862
2039
|
"",
|
|
1863
|
-
"##
|
|
2040
|
+
"## \u274C Failure Detected",
|
|
2041
|
+
"",
|
|
2042
|
+
`**Exit Code:** ${match[1]}`,
|
|
2043
|
+
"",
|
|
2044
|
+
"**Recent Output:**",
|
|
2045
|
+
"```",
|
|
2046
|
+
s.recentOutput.join("\n"),
|
|
2047
|
+
"```",
|
|
1864
2048
|
"",
|
|
1865
|
-
"
|
|
2049
|
+
"---",
|
|
2050
|
+
"",
|
|
2051
|
+
"## \u{1F527} Claude's Healing Process",
|
|
1866
2052
|
""
|
|
1867
2053
|
].join("\n")
|
|
1868
2054
|
);
|
|
2055
|
+
return { ...s, selfHealingFile, recentOutput: [] };
|
|
1869
2056
|
}
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
finalizeComplexSequence() {
|
|
1876
|
-
if (this.toolCount >= 3 && this.complexSequenceFile) {
|
|
2057
|
+
},
|
|
2058
|
+
{
|
|
2059
|
+
pattern: /\[self-healing\].*Re-running/i,
|
|
2060
|
+
apply: (_ctx, s) => {
|
|
2061
|
+
if (!s.selfHealingFile) return s;
|
|
1877
2062
|
appendFileSync(
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
2063
|
+
s.selfHealingFile,
|
|
2064
|
+
[
|
|
2065
|
+
"",
|
|
2066
|
+
"*(See full log for Claude's diagnostic process)*",
|
|
2067
|
+
"",
|
|
2068
|
+
"---",
|
|
2069
|
+
"",
|
|
2070
|
+
"## \u2705 Resolution Applied",
|
|
2071
|
+
"",
|
|
2072
|
+
"The self-healing process completed. Check the full execution log to see Claude's analysis and fix.",
|
|
2073
|
+
"",
|
|
2074
|
+
"---",
|
|
2075
|
+
"",
|
|
2076
|
+
"*Auto-captured*",
|
|
2077
|
+
""
|
|
2078
|
+
].join("\n")
|
|
1886
2079
|
);
|
|
2080
|
+
return { ...s, selfHealingFile: "" };
|
|
1887
2081
|
}
|
|
1888
2082
|
}
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
}
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
// --------------------------------------------------------------------------
|
|
1914
|
-
// Highlight writers
|
|
1915
|
-
// --------------------------------------------------------------------------
|
|
1916
|
-
saveJudgeHighlight(verdict, output) {
|
|
1917
|
-
const file = join3(
|
|
1918
|
-
this.highlightsDir,
|
|
1919
|
-
`${this.timestamp}_step${this.stepIndex + 1}_judge_${verdict}.md`
|
|
1920
|
-
);
|
|
1921
|
-
writeFileSync3(
|
|
1922
|
-
file,
|
|
1923
|
-
[
|
|
1924
|
-
`# Judge Verdict: ${verdict}`,
|
|
1925
|
-
"",
|
|
1926
|
-
`**Task:** ${this.taskName}`,
|
|
1927
|
-
`**Step:** ${this.stepName}`,
|
|
1928
|
-
`**Attempt:** ${this.judgeAttempt}`,
|
|
1929
|
-
`**Timestamp:** ${(/* @__PURE__ */ new Date()).toISOString()}`,
|
|
1930
|
-
"",
|
|
1931
|
-
"---",
|
|
1932
|
-
"",
|
|
1933
|
-
output,
|
|
1934
|
-
"",
|
|
1935
|
-
"---",
|
|
1936
|
-
"",
|
|
1937
|
-
"*Auto-captured*",
|
|
1938
|
-
""
|
|
1939
|
-
].join("\n")
|
|
1940
|
-
);
|
|
1941
|
-
}
|
|
1942
|
-
startSelfHealingHighlight(exitCode) {
|
|
1943
|
-
this.selfHealingFile = join3(
|
|
1944
|
-
this.highlightsDir,
|
|
1945
|
-
`${this.timestamp}_step${this.stepIndex + 1}_self_healing.md`
|
|
1946
|
-
);
|
|
1947
|
-
const errorOutput = this.recentOutput.join("\n");
|
|
1948
|
-
this.recentOutput = [];
|
|
2083
|
+
];
|
|
2084
|
+
function onLogMessage(ctx, s, level, text) {
|
|
2085
|
+
appendLog(s.logFile, `[${level}] ${text}`);
|
|
2086
|
+
return LOG_MATCHERS.reduce(
|
|
2087
|
+
({ matched, state }, { pattern, apply }) => {
|
|
2088
|
+
if (matched) return { matched, state };
|
|
2089
|
+
const m = pattern.exec(text);
|
|
2090
|
+
return m ? { matched: true, state: apply(ctx, state, text, m) } : { matched, state };
|
|
2091
|
+
},
|
|
2092
|
+
{ matched: false, state: s }
|
|
2093
|
+
).state;
|
|
2094
|
+
}
|
|
2095
|
+
function onWorkflowComplete(ctx, s) {
|
|
2096
|
+
appendLog(
|
|
2097
|
+
s.logFile,
|
|
2098
|
+
`
|
|
2099
|
+
${"\u2501".repeat(51)}
|
|
2100
|
+
Task Complete: ${ctx.slug}
|
|
2101
|
+
Finished: ${(/* @__PURE__ */ new Date()).toISOString()}
|
|
2102
|
+
${"\u2501".repeat(51)}
|
|
2103
|
+
`
|
|
2104
|
+
);
|
|
2105
|
+
const indexFile = join3(ctx.highlightsDir, "README.md");
|
|
2106
|
+
if (!existsSync2(indexFile)) {
|
|
1949
2107
|
writeFileSync3(
|
|
1950
|
-
|
|
2108
|
+
indexFile,
|
|
1951
2109
|
[
|
|
1952
|
-
"#
|
|
1953
|
-
"",
|
|
1954
|
-
`**Task:** ${this.taskName}`,
|
|
1955
|
-
`**Step:** ${this.stepName}`,
|
|
1956
|
-
`**Timestamp:** ${(/* @__PURE__ */ new Date()).toISOString()}`,
|
|
1957
|
-
"",
|
|
1958
|
-
"---",
|
|
1959
|
-
"",
|
|
1960
|
-
"## \u274C Failure Detected",
|
|
1961
|
-
"",
|
|
1962
|
-
`**Exit Code:** ${exitCode}`,
|
|
1963
|
-
"",
|
|
1964
|
-
"**Recent Output:**",
|
|
1965
|
-
"```",
|
|
1966
|
-
errorOutput,
|
|
1967
|
-
"```",
|
|
2110
|
+
"# Execution Highlights",
|
|
1968
2111
|
"",
|
|
1969
|
-
"
|
|
2112
|
+
"This directory contains automatically extracted highlight moments from task executions.",
|
|
1970
2113
|
"",
|
|
1971
|
-
"##
|
|
2114
|
+
"## Latest Highlights",
|
|
1972
2115
|
""
|
|
1973
2116
|
].join("\n")
|
|
1974
2117
|
);
|
|
1975
2118
|
}
|
|
1976
|
-
|
|
1977
|
-
|
|
2119
|
+
const highlights = readdirSync(ctx.highlightsDir).filter((f) => f.startsWith(ctx.ts) && f.endsWith(".md")).sort();
|
|
2120
|
+
if (highlights.length > 0) {
|
|
2121
|
+
const entries = highlights.map((f) => `- [${f.replace(/\.md$/, "")}](./${f})`).join("\n");
|
|
1978
2122
|
appendFileSync(
|
|
1979
|
-
|
|
1980
|
-
[
|
|
1981
|
-
"",
|
|
1982
|
-
"*(See full log for Claude's diagnostic process)*",
|
|
1983
|
-
"",
|
|
1984
|
-
"---",
|
|
1985
|
-
"",
|
|
1986
|
-
"## \u2705 Resolution Applied",
|
|
1987
|
-
"",
|
|
1988
|
-
"The self-healing process completed. Check the full execution log to see Claude's analysis and fix.",
|
|
1989
|
-
"",
|
|
1990
|
-
"---",
|
|
1991
|
-
"",
|
|
1992
|
-
"*Auto-captured*",
|
|
1993
|
-
""
|
|
1994
|
-
].join("\n")
|
|
1995
|
-
);
|
|
1996
|
-
this.selfHealingFile = "";
|
|
1997
|
-
}
|
|
1998
|
-
// --------------------------------------------------------------------------
|
|
1999
|
-
// Workflow complete → index
|
|
2000
|
-
// --------------------------------------------------------------------------
|
|
2001
|
-
onWorkflowComplete() {
|
|
2002
|
-
this.appendLog(
|
|
2123
|
+
indexFile,
|
|
2003
2124
|
`
|
|
2004
|
-
${
|
|
2005
|
-
|
|
2006
|
-
Finished: ${(/* @__PURE__ */ new Date()).toISOString()}
|
|
2007
|
-
${"\u2501".repeat(51)}
|
|
2125
|
+
### ${ctx.slug} (${(/* @__PURE__ */ new Date()).toISOString()})
|
|
2126
|
+
${entries}
|
|
2008
2127
|
`
|
|
2009
2128
|
);
|
|
2010
|
-
this.writeHighlightsIndex();
|
|
2011
2129
|
}
|
|
2012
|
-
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
|
|
2023
|
-
|
|
2024
|
-
|
|
2130
|
+
return s;
|
|
2131
|
+
}
|
|
2132
|
+
function onOutputText(s, text) {
|
|
2133
|
+
appendLog(s.logFile, text);
|
|
2134
|
+
return { ...s, recentOutput: [...s.recentOutput, text] };
|
|
2135
|
+
}
|
|
2136
|
+
function reduce(ctx, s, event) {
|
|
2137
|
+
switch (event.type) {
|
|
2138
|
+
case "workflow:start":
|
|
2139
|
+
return onWorkflowStart(ctx, s);
|
|
2140
|
+
case "step:start":
|
|
2141
|
+
return onStepStart(ctx, s, event.index, event.name);
|
|
2142
|
+
case "step:complete":
|
|
2143
|
+
return onStepComplete(s);
|
|
2144
|
+
case "step:error":
|
|
2145
|
+
return onStepError(s, event.error);
|
|
2146
|
+
case "step:iteration":
|
|
2147
|
+
appendLog(
|
|
2148
|
+
s.logFile,
|
|
2149
|
+
`
|
|
2150
|
+
\u2500\u2500 iteration ${event.iteration}/${event.total}: ${event.item}`
|
|
2025
2151
|
);
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
|
|
2152
|
+
return s;
|
|
2153
|
+
case "step:inner":
|
|
2154
|
+
appendLog(
|
|
2155
|
+
s.logFile,
|
|
2156
|
+
` \u21B3 [${event.innerIndex + 1}/${event.innerTotal}] ${event.name}`
|
|
2157
|
+
);
|
|
2158
|
+
return s;
|
|
2159
|
+
case "output:text":
|
|
2160
|
+
return onOutputText(s, event.text);
|
|
2161
|
+
case "output:tool":
|
|
2162
|
+
return onTool(ctx, s, event.tool, event.input);
|
|
2163
|
+
case "log":
|
|
2164
|
+
return onLogMessage(ctx, s, event.level, event.text);
|
|
2165
|
+
case "workflow:complete":
|
|
2166
|
+
return onWorkflowComplete(ctx, s);
|
|
2167
|
+
default:
|
|
2168
|
+
return s;
|
|
2043
2169
|
}
|
|
2044
|
-
}
|
|
2170
|
+
}
|
|
2171
|
+
function createLogger(logDir, taskName) {
|
|
2172
|
+
const ctx = {
|
|
2173
|
+
logDir,
|
|
2174
|
+
highlightsDir: join3(logDir, "highlights"),
|
|
2175
|
+
ts: formatTimestamp(/* @__PURE__ */ new Date()),
|
|
2176
|
+
slug: slugify(taskName, 40) || "task"
|
|
2177
|
+
};
|
|
2178
|
+
const enabled = process.env["EXECUTANT_LOG"] !== "0";
|
|
2179
|
+
let state = INIT_STATE;
|
|
2180
|
+
return {
|
|
2181
|
+
getHighlightsDir: () => ctx.highlightsDir,
|
|
2182
|
+
getTimestamp: () => ctx.ts,
|
|
2183
|
+
observe(event) {
|
|
2184
|
+
if (!enabled) return;
|
|
2185
|
+
try {
|
|
2186
|
+
state = reduce(ctx, state, event);
|
|
2187
|
+
} catch (err) {
|
|
2188
|
+
console.warn(`[logger] error: ${getErrorMessage(err)}`);
|
|
2189
|
+
}
|
|
2190
|
+
}
|
|
2191
|
+
};
|
|
2192
|
+
}
|
|
2045
2193
|
async function* withLogger(gen, logger2) {
|
|
2046
2194
|
for await (const event of gen) {
|
|
2047
2195
|
logger2.observe(event);
|
|
@@ -2052,7 +2200,6 @@ async function* withLogger(gen, logger2) {
|
|
|
2052
2200
|
// src/retrospective.ts
|
|
2053
2201
|
import { existsSync as existsSync3, mkdirSync as mkdirSync4, readdirSync as readdirSync2, readFileSync as readFileSync5, writeFileSync as writeFileSync4 } from "node:fs";
|
|
2054
2202
|
import { basename as basename2, dirname as dirname4, join as join4, resolve as resolve3 } from "node:path";
|
|
2055
|
-
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
2056
2203
|
import { spawnSync } from "node:child_process";
|
|
2057
2204
|
import { load as parseYaml2 } from "js-yaml";
|
|
2058
2205
|
import { z as z4 } from "zod";
|
|
@@ -2060,15 +2207,13 @@ var RetrospectiveOutputSchema = z4.object({
|
|
|
2060
2207
|
improved_yaml: z4.string(),
|
|
2061
2208
|
changelog: z4.string()
|
|
2062
2209
|
});
|
|
2063
|
-
var
|
|
2064
|
-
var RETROSPECTIVE_PROMPT = readFileSync5(join4(PROMPTS_DIR2, "retrospective-analysis.txt"), "utf8");
|
|
2210
|
+
var RETROSPECTIVE_PROMPT = loadPrompt("retrospective-analysis");
|
|
2065
2211
|
async function runRetrospective(workflowFilePath, workflow2, highlightsDir, runTimestamp) {
|
|
2066
2212
|
try {
|
|
2067
2213
|
await doRetrospective(workflowFilePath, workflow2, highlightsDir, runTimestamp);
|
|
2068
2214
|
} catch (err) {
|
|
2069
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
2070
2215
|
console.warn(`
|
|
2071
|
-
Self-improvement: retrospective failed: ${
|
|
2216
|
+
Self-improvement: retrospective failed: ${getErrorMessage(err)}`);
|
|
2072
2217
|
}
|
|
2073
2218
|
}
|
|
2074
2219
|
async function doRetrospective(workflowFilePath, workflow2, highlightsDir, runTimestamp) {
|
|
@@ -2112,7 +2257,13 @@ ${content}`;
|
|
|
2112
2257
|
}).join("\n\n---\n\n");
|
|
2113
2258
|
const originalYaml = readFileSync5(workflowFilePath, "utf8");
|
|
2114
2259
|
const taskName = basename2(workflowFilePath, ".yaml");
|
|
2115
|
-
const prompt = RETROSPECTIVE_PROMPT
|
|
2260
|
+
const prompt = fillTemplate(RETROSPECTIVE_PROMPT, {
|
|
2261
|
+
TASK_NAME: taskName,
|
|
2262
|
+
ORIGINAL_GOAL: workflow2.goal,
|
|
2263
|
+
ORIGINAL_YAML: originalYaml,
|
|
2264
|
+
HIGHLIGHTS: highlightContents,
|
|
2265
|
+
METRICS: metrics
|
|
2266
|
+
});
|
|
2116
2267
|
const result = spawnSync(
|
|
2117
2268
|
"claude",
|
|
2118
2269
|
[
|
|
@@ -2155,8 +2306,7 @@ Response: ${response.trim()}`);
|
|
|
2155
2306
|
try {
|
|
2156
2307
|
parseYaml2(improvedYaml);
|
|
2157
2308
|
} catch (err) {
|
|
2158
|
-
|
|
2159
|
-
console.warn(`Self-improvement: generated YAML is invalid (${msg}), skipping save.`);
|
|
2309
|
+
console.warn(`Self-improvement: generated YAML is invalid (${getErrorMessage(err)}), skipping save.`);
|
|
2160
2310
|
return;
|
|
2161
2311
|
}
|
|
2162
2312
|
const startDir = dirname4(resolve3(workflowFilePath));
|
|
@@ -2187,16 +2337,21 @@ function extractJson(text) {
|
|
|
2187
2337
|
|
|
2188
2338
|
// src/index.ts
|
|
2189
2339
|
var CURRENT_VERSION = JSON.parse(
|
|
2190
|
-
readFileSync6(
|
|
2340
|
+
readFileSync6(
|
|
2341
|
+
join5(dirname5(fileURLToPath2(import.meta.url)), "../package.json"),
|
|
2342
|
+
"utf-8"
|
|
2343
|
+
)
|
|
2191
2344
|
).version;
|
|
2192
2345
|
var rawArgs = process.argv.slice(2);
|
|
2193
2346
|
if (rawArgs[0] === "plan") {
|
|
2194
2347
|
const planArgs = parsePlanArgs(rawArgs.slice(1));
|
|
2195
2348
|
const planEvents = streamPlan(planArgs);
|
|
2196
|
-
const inkApp = render(
|
|
2197
|
-
|
|
2198
|
-
|
|
2199
|
-
|
|
2349
|
+
const inkApp = render(
|
|
2350
|
+
React3.createElement(PlanApp, {
|
|
2351
|
+
description: planArgs.description,
|
|
2352
|
+
events: planEvents
|
|
2353
|
+
})
|
|
2354
|
+
);
|
|
2200
2355
|
try {
|
|
2201
2356
|
await inkApp.waitUntilExit();
|
|
2202
2357
|
} catch {
|
|
@@ -2212,7 +2367,7 @@ if (rawArgs[0] === "update") {
|
|
|
2212
2367
|
await doUpdate2();
|
|
2213
2368
|
console.log("Done.");
|
|
2214
2369
|
} catch (err) {
|
|
2215
|
-
console.error("Update failed:",
|
|
2370
|
+
console.error("Update failed:", getErrorMessage(err));
|
|
2216
2371
|
process.exit(1);
|
|
2217
2372
|
}
|
|
2218
2373
|
process.exit(0);
|
|
@@ -2245,7 +2400,15 @@ YAML \u2014 step fields (all step types):
|
|
|
2245
2400
|
forEach string or list
|
|
2246
2401
|
Inline YAML array OR a shell command whose newline-split
|
|
2247
2402
|
stdout provides the items. {{item}} is substituted per
|
|
2248
|
-
iteration in
|
|
2403
|
+
iteration in every child step's name, command, and prompt.
|
|
2404
|
+
repeat int Run this step N times; {{item}} is the 1-based
|
|
2405
|
+
iteration number. Mutually exclusive with forEach.
|
|
2406
|
+
steps list Multiple child steps to run per forEach/repeat
|
|
2407
|
+
iteration. Mutually exclusive with command/prompt on the
|
|
2408
|
+
parent step. Requires forEach or repeat.
|
|
2409
|
+
context list Var names whose file-path values are prepended to
|
|
2410
|
+
a prompt step's content at runtime.
|
|
2411
|
+
output string Var name; captures script stdout to that file path.
|
|
2249
2412
|
|
|
2250
2413
|
YAML \u2014 prompt step fields (type: prompt, or inferred when prompt is present):
|
|
2251
2414
|
prompt string (required) Instructions sent to Claude
|
|
@@ -2257,7 +2420,7 @@ YAML \u2014 prompt step fields (type: prompt, or inferred when prompt is present
|
|
|
2257
2420
|
YAML \u2014 script step fields (type: script | command, or inferred when command is present):
|
|
2258
2421
|
command string (required) Bash command to execute
|
|
2259
2422
|
self_healing bool On failure, Claude diagnoses and fixes iteratively
|
|
2260
|
-
up to 5 attempts with accumulated context (default:
|
|
2423
|
+
up to 5 attempts with accumulated context (default: false)
|
|
2261
2424
|
max_healing_attempts int Override max self-healing retries (default: 5)
|
|
2262
2425
|
|
|
2263
2426
|
YAML \u2014 log step fields (type: log, or inferred when message is present and prompt is absent):
|
|
@@ -2318,12 +2481,12 @@ var workflow;
|
|
|
2318
2481
|
try {
|
|
2319
2482
|
workflow = loadWorkflow(filePath);
|
|
2320
2483
|
} catch (err) {
|
|
2321
|
-
console.error(
|
|
2484
|
+
console.error(getErrorMessage(err));
|
|
2322
2485
|
process.exit(1);
|
|
2323
2486
|
}
|
|
2324
2487
|
var options = { stepFilter, fromStep };
|
|
2325
2488
|
var rawEvents = runWorkflow(workflow, options);
|
|
2326
|
-
var logger =
|
|
2489
|
+
var logger = createLogger(resolveLogDir(filePath), workflow.goal);
|
|
2327
2490
|
var events = withLogger(rawEvents, logger);
|
|
2328
2491
|
var updateCheck = checkForUpdate(CURRENT_VERSION);
|
|
2329
2492
|
function errorReplacer(_key, value) {
|
|
@@ -2335,9 +2498,17 @@ function errorReplacer(_key, value) {
|
|
|
2335
2498
|
async function maybeRunRetrospective(filePath2, workflow2, logger2) {
|
|
2336
2499
|
if (!logger2) return;
|
|
2337
2500
|
try {
|
|
2338
|
-
await runRetrospective(
|
|
2501
|
+
await runRetrospective(
|
|
2502
|
+
filePath2,
|
|
2503
|
+
workflow2,
|
|
2504
|
+
logger2.getHighlightsDir(),
|
|
2505
|
+
logger2.getTimestamp()
|
|
2506
|
+
);
|
|
2339
2507
|
} catch (err) {
|
|
2340
|
-
console.warn(
|
|
2508
|
+
console.warn(
|
|
2509
|
+
"[executant] retrospective failed (non-fatal):",
|
|
2510
|
+
getErrorMessage(err)
|
|
2511
|
+
);
|
|
2341
2512
|
}
|
|
2342
2513
|
}
|
|
2343
2514
|
if (ciMode) {
|
|
@@ -2353,7 +2524,9 @@ if (ciMode) {
|
|
|
2353
2524
|
process.exit(1);
|
|
2354
2525
|
});
|
|
2355
2526
|
} else {
|
|
2356
|
-
const inkApp = render(
|
|
2527
|
+
const inkApp = render(
|
|
2528
|
+
React3.createElement(App, { workflow, events, options, updateCheck })
|
|
2529
|
+
);
|
|
2357
2530
|
if (workflow.selfImprove) {
|
|
2358
2531
|
inkApp.waitUntilExit().then(() => maybeRunRetrospective(filePath, workflow, logger)).catch(() => {
|
|
2359
2532
|
});
|