executant 1.5.0 → 1.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +341 -413
- package/package.json +6 -2
package/dist/index.js
CHANGED
|
@@ -54,11 +54,64 @@ 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
116
|
var RawStepSchema = z.object({
|
|
64
117
|
name: z.string(),
|
|
@@ -85,16 +138,15 @@ var RawWorkflowSchema = z.object({
|
|
|
85
138
|
function loadWorkflow(filePath2) {
|
|
86
139
|
let raw;
|
|
87
140
|
try {
|
|
88
|
-
raw =
|
|
141
|
+
raw = readFileSync2(filePath2, "utf8");
|
|
89
142
|
} catch (err) {
|
|
90
|
-
|
|
91
|
-
throw new Error(`Cannot read workflow file "${filePath2}": ${msg}`);
|
|
143
|
+
throw new Error(`Cannot read workflow file "${filePath2}": ${getErrorMessage(err)}`);
|
|
92
144
|
}
|
|
93
145
|
let doc;
|
|
94
146
|
try {
|
|
95
147
|
doc = RawWorkflowSchema.parse(parseYaml(raw));
|
|
96
148
|
} catch (err) {
|
|
97
|
-
const detail = err instanceof z.ZodError ? err.errors
|
|
149
|
+
const detail = err instanceof z.ZodError ? formatZodIssues(err.errors) : String(err);
|
|
98
150
|
throw new Error(`Invalid workflow file "${filePath2}":
|
|
99
151
|
${detail}`);
|
|
100
152
|
}
|
|
@@ -107,29 +159,18 @@ ${detail}`);
|
|
|
107
159
|
};
|
|
108
160
|
}
|
|
109
161
|
function convertStep(step, vars) {
|
|
110
|
-
const name = step
|
|
111
|
-
const continueOnError = step.continue_on_error ?? false;
|
|
162
|
+
const { name, continue_on_error: continueOnError = false } = step;
|
|
112
163
|
if (step.repeat !== void 0 && step.forEach !== void 0) {
|
|
113
164
|
throw new Error(`Step "${name}" cannot have both repeat and forEach`);
|
|
114
165
|
}
|
|
115
|
-
if (step.repeat !== void 0) {
|
|
116
|
-
const
|
|
117
|
-
const { repeat:
|
|
118
|
-
return {
|
|
119
|
-
type: "forEach",
|
|
120
|
-
name,
|
|
121
|
-
continueOnError,
|
|
122
|
-
forEach: items,
|
|
123
|
-
inner: convertInnerStep(innerStep, vars, name, continueOnError)
|
|
124
|
-
};
|
|
125
|
-
}
|
|
126
|
-
if (step.forEach !== void 0) {
|
|
127
|
-
const { forEach: _, ...innerStep } = step;
|
|
166
|
+
if (step.repeat !== void 0 || step.forEach !== void 0) {
|
|
167
|
+
const forEachValue = step.repeat !== void 0 ? Array.from({ length: step.repeat }, (_, i) => String(i + 1)) : step.forEach;
|
|
168
|
+
const { repeat: _r, forEach: _f, ...innerStep } = step;
|
|
128
169
|
return {
|
|
129
170
|
type: "forEach",
|
|
130
171
|
name,
|
|
131
172
|
continueOnError,
|
|
132
|
-
forEach:
|
|
173
|
+
forEach: forEachValue,
|
|
133
174
|
inner: convertInnerStep(innerStep, vars, name, continueOnError)
|
|
134
175
|
};
|
|
135
176
|
}
|
|
@@ -196,10 +237,7 @@ function resolveOutputFile(varName, vars, stepName) {
|
|
|
196
237
|
return resolveVarPath(varName, vars, stepName, "output");
|
|
197
238
|
}
|
|
198
239
|
function substituteVars(text, vars, stepName, field) {
|
|
199
|
-
const result =
|
|
200
|
-
(acc, [key, value]) => acc.replaceAll(`{{${key}}}`, value),
|
|
201
|
-
text
|
|
202
|
-
);
|
|
240
|
+
const result = fillTemplate(text, vars);
|
|
203
241
|
const unknownTokens = [...result.matchAll(/\{\{(\w+)\}\}/g)].map((m) => m[1]).filter((key) => key !== "item");
|
|
204
242
|
if (unknownTokens.length > 0) {
|
|
205
243
|
throw new Error(
|
|
@@ -305,47 +343,6 @@ async function* runCommand(task) {
|
|
|
305
343
|
// src/tasks/claude.ts
|
|
306
344
|
import { execSync, spawn as spawn2 } from "node:child_process";
|
|
307
345
|
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
346
|
var DEFAULT_TOOLS = ["Read", "Edit", "Write", "Bash", "Glob", "Grep"];
|
|
350
347
|
function resolveClaudePath() {
|
|
351
348
|
try {
|
|
@@ -386,8 +383,7 @@ async function* runClaude(task) {
|
|
|
386
383
|
env: { ...process.env }
|
|
387
384
|
});
|
|
388
385
|
} catch (err) {
|
|
389
|
-
|
|
390
|
-
throw new Error(`Failed to spawn claude (${claudeBin}): ${msg}`);
|
|
386
|
+
throw new Error(`Failed to spawn claude (${claudeBin}): ${getErrorMessage(err)}`);
|
|
391
387
|
}
|
|
392
388
|
const cleanup = () => {
|
|
393
389
|
try {
|
|
@@ -491,20 +487,19 @@ var JudgeOutputSchema = z2.object({
|
|
|
491
487
|
reasoning: z2.string().optional(),
|
|
492
488
|
feedback: z2.string()
|
|
493
489
|
});
|
|
490
|
+
function shouldSkipStep(stepNumber, name, options2) {
|
|
491
|
+
if (options2.stepFilter !== void 0) {
|
|
492
|
+
const matchByIndex = /^\d+$/.test(options2.stepFilter) && parseInt(options2.stepFilter, 10) === stepNumber;
|
|
493
|
+
return !matchByIndex && name !== options2.stepFilter;
|
|
494
|
+
}
|
|
495
|
+
return options2.fromStep !== void 0 && stepNumber < options2.fromStep;
|
|
496
|
+
}
|
|
494
497
|
async function* runWorkflow(workflow2, options2 = {}) {
|
|
495
498
|
const workflowStart = Date.now();
|
|
496
499
|
yield { type: "workflow:start", workflow: workflow2 };
|
|
497
500
|
for (const [i, task] of workflow2.tasks.entries()) {
|
|
498
501
|
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) {
|
|
502
|
+
if (shouldSkipStep(stepNumber, task.name, options2)) {
|
|
508
503
|
yield { type: "step:skip", index: i, name: task.name };
|
|
509
504
|
continue;
|
|
510
505
|
}
|
|
@@ -585,8 +580,7 @@ async function resolveItems(forEach) {
|
|
|
585
580
|
const { stdout } = await execPromise(forEach, { shell: "/bin/sh", timeout: 3e4 });
|
|
586
581
|
return stdout.split("\n").filter((l) => l.trim().length > 0);
|
|
587
582
|
} catch (err) {
|
|
588
|
-
|
|
589
|
-
throw new Error(`forEach shell command failed: ${msg}
|
|
583
|
+
throw new Error(`forEach shell command failed: ${getErrorMessage(err)}
|
|
590
584
|
Command: ${forEach}`);
|
|
591
585
|
}
|
|
592
586
|
}
|
|
@@ -660,7 +654,7 @@ async function* runClaudeWithJudge(task) {
|
|
|
660
654
|
for (let attempt = 0; attempt < MAX_JUDGE_RETRIES; attempt++) {
|
|
661
655
|
const prompt = attempt === 0 ? task.prompt : `${task.prompt}
|
|
662
656
|
|
|
663
|
-
${JUDGE_RETRY_CONTEXT
|
|
657
|
+
${fillTemplate(JUDGE_RETRY_CONTEXT, { FEEDBACK: judgeContext })}`;
|
|
664
658
|
const lines = [];
|
|
665
659
|
yield* collectLines(runClaude({ ...task, prompt }), lines);
|
|
666
660
|
yield { type: "log", level: "info", text: `[judge] Evaluating "${task.name}"\u2026` };
|
|
@@ -704,8 +698,7 @@ function readContextFile(filePath2) {
|
|
|
704
698
|
try {
|
|
705
699
|
return readFileSync3(filePath2, "utf8");
|
|
706
700
|
} catch (err) {
|
|
707
|
-
|
|
708
|
-
throw new Error(`Context file "${filePath2}" could not be read: ${msg}`);
|
|
701
|
+
throw new Error(`Context file "${filePath2}" could not be read: ${getErrorMessage(err)}`);
|
|
709
702
|
}
|
|
710
703
|
}
|
|
711
704
|
function expandContext(task) {
|
|
@@ -719,10 +712,10 @@ ${readContextFile(fp)}
|
|
|
719
712
|
${task.prompt}` };
|
|
720
713
|
}
|
|
721
714
|
function buildHealingPrompt(command, exitCode, output, attemptHistory) {
|
|
722
|
-
return SELF_HEALING_PROMPT
|
|
715
|
+
return fillTemplate(SELF_HEALING_PROMPT, { COMMAND: command, EXIT_CODE: String(exitCode), OUTPUT: output, ATTEMPT_HISTORY: attemptHistory });
|
|
723
716
|
}
|
|
724
717
|
function buildJudgePrompt(stepName, instructions, output) {
|
|
725
|
-
return JUDGE_EVALUATION_PROMPT
|
|
718
|
+
return fillTemplate(JUDGE_EVALUATION_PROMPT, { STEP_NAME: stepName, STEP_INSTRUCTIONS: instructions, OUTPUT: output });
|
|
726
719
|
}
|
|
727
720
|
function formatToolCall(tool, input) {
|
|
728
721
|
if (tool === "Edit" || tool === "Write") return `${tool}(${String(input["file_path"] ?? "")})`;
|
|
@@ -1070,12 +1063,8 @@ function App({ workflow: workflow2, events: events2, options: options2, updateCh
|
|
|
1070
1063
|
}
|
|
1071
1064
|
} catch (err) {
|
|
1072
1065
|
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);
|
|
1066
|
+
dispatch({ type: "log", level: "error", text: getErrorMessage(err) });
|
|
1067
|
+
setTimeout(() => exit(err instanceof Error ? err : new Error(getErrorMessage(err))), EXIT_DELAY_MS);
|
|
1079
1068
|
}
|
|
1080
1069
|
})();
|
|
1081
1070
|
return () => {
|
|
@@ -1283,7 +1272,10 @@ async function runPass3Judge(description, workflow2) {
|
|
|
1283
1272
|
const task = {
|
|
1284
1273
|
type: "claude",
|
|
1285
1274
|
name: "plan:judge",
|
|
1286
|
-
prompt: PLAN_JUDGE_PROMPT
|
|
1275
|
+
prompt: fillTemplate(PLAN_JUDGE_PROMPT, {
|
|
1276
|
+
DESCRIPTION: description,
|
|
1277
|
+
WORKFLOW_JSON: JSON.stringify(workflow2, null, 2)
|
|
1278
|
+
}),
|
|
1287
1279
|
allowedTools: [],
|
|
1288
1280
|
permissionMode: "default",
|
|
1289
1281
|
model: "sonnet"
|
|
@@ -1388,7 +1380,7 @@ async function* streamPlan(args) {
|
|
|
1388
1380
|
const researchTask = {
|
|
1389
1381
|
type: "claude",
|
|
1390
1382
|
name: "plan:research",
|
|
1391
|
-
prompt: PLAN_RESEARCH_PROMPT
|
|
1383
|
+
prompt: fillTemplate(PLAN_RESEARCH_PROMPT, { DESCRIPTION: description }),
|
|
1392
1384
|
allowedTools: ["Read", "Glob", "Grep"],
|
|
1393
1385
|
permissionMode: "bypassPermissions",
|
|
1394
1386
|
model: "opus"
|
|
@@ -1402,8 +1394,7 @@ async function* streamPlan(args) {
|
|
|
1402
1394
|
}
|
|
1403
1395
|
}
|
|
1404
1396
|
} catch (err) {
|
|
1405
|
-
|
|
1406
|
-
yield { type: "plan:error", message: `Research pass failed: ${msg}` };
|
|
1397
|
+
yield { type: "plan:error", message: `Research pass failed: ${getErrorMessage(err)}` };
|
|
1407
1398
|
return;
|
|
1408
1399
|
}
|
|
1409
1400
|
researchDoc = researchLines.join("\n");
|
|
@@ -1412,10 +1403,8 @@ async function* streamPlan(args) {
|
|
|
1412
1403
|
return;
|
|
1413
1404
|
}
|
|
1414
1405
|
}
|
|
1415
|
-
const
|
|
1416
|
-
|
|
1417
|
-
const totalStages = skipResearch ? 2 : TOTAL_PLAN_STAGES;
|
|
1418
|
-
yield { type: "plan:stage", stage: decomposeStage, total: totalStages, name: "Decompose to Steps" };
|
|
1406
|
+
const stages = skipResearch ? { decompose: 1, validate: 2, total: 2 } : { decompose: 2, validate: 3, total: TOTAL_PLAN_STAGES };
|
|
1407
|
+
yield { type: "plan:stage", stage: stages.decompose, total: stages.total, name: "Decompose to Steps" };
|
|
1419
1408
|
let retryPrefix = "";
|
|
1420
1409
|
for (let attempt = 0; attempt < MAX_PLAN_RETRIES; attempt++) {
|
|
1421
1410
|
if (attempt > 0) {
|
|
@@ -1425,9 +1414,9 @@ async function* streamPlan(args) {
|
|
|
1425
1414
|
maxAttempts: MAX_PLAN_RETRIES,
|
|
1426
1415
|
reason: retryPrefix.replace(/\n/g, " ")
|
|
1427
1416
|
};
|
|
1428
|
-
yield { type: "plan:stage", stage:
|
|
1417
|
+
yield { type: "plan:stage", stage: stages.decompose, total: stages.total, name: "Decompose to Steps" };
|
|
1429
1418
|
}
|
|
1430
|
-
const basePrompt = PLAN_DECOMPOSE_PROMPT
|
|
1419
|
+
const basePrompt = fillTemplate(PLAN_DECOMPOSE_PROMPT, { DESCRIPTION: description, RESEARCH_DOC: researchDoc });
|
|
1431
1420
|
const decomposeTask = {
|
|
1432
1421
|
type: "claude",
|
|
1433
1422
|
name: "plan:decompose",
|
|
@@ -1454,12 +1443,12 @@ ${basePrompt}` : basePrompt,
|
|
|
1454
1443
|
}
|
|
1455
1444
|
}
|
|
1456
1445
|
} catch (err) {
|
|
1457
|
-
const msg =
|
|
1446
|
+
const msg = getErrorMessage(err);
|
|
1458
1447
|
if (attempt === MAX_PLAN_RETRIES - 1) {
|
|
1459
1448
|
yield { type: "plan:error", message: msg };
|
|
1460
1449
|
return;
|
|
1461
1450
|
}
|
|
1462
|
-
retryPrefix = PLAN_RETRY_PARSE_ERROR
|
|
1451
|
+
retryPrefix = fillTemplate(PLAN_RETRY_PARSE_ERROR, { ERROR: msg, EXCERPT: decomposeTextLines.join("\n") });
|
|
1463
1452
|
continue;
|
|
1464
1453
|
}
|
|
1465
1454
|
if (structuredOutput === void 0) {
|
|
@@ -1468,27 +1457,27 @@ ${basePrompt}` : basePrompt,
|
|
|
1468
1457
|
yield { type: "plan:error", message: issues };
|
|
1469
1458
|
return;
|
|
1470
1459
|
}
|
|
1471
|
-
retryPrefix = PLAN_RETRY_SCHEMA_ERROR
|
|
1460
|
+
retryPrefix = fillTemplate(PLAN_RETRY_SCHEMA_ERROR, { ISSUES: issues });
|
|
1472
1461
|
continue;
|
|
1473
1462
|
}
|
|
1474
1463
|
const zodResult = WorkflowSchema.safeParse(structuredOutput);
|
|
1475
1464
|
if (!zodResult.success) {
|
|
1476
|
-
const issues = zodResult.error.issues
|
|
1465
|
+
const issues = formatZodIssues(zodResult.error.issues);
|
|
1477
1466
|
if (attempt === MAX_PLAN_RETRIES - 1) {
|
|
1478
1467
|
yield { type: "plan:error", message: `Plan did not match expected schema:
|
|
1479
1468
|
${issues}` };
|
|
1480
1469
|
return;
|
|
1481
1470
|
}
|
|
1482
|
-
retryPrefix = PLAN_RETRY_SCHEMA_ERROR
|
|
1471
|
+
retryPrefix = fillTemplate(PLAN_RETRY_SCHEMA_ERROR, { ISSUES: issues });
|
|
1483
1472
|
continue;
|
|
1484
1473
|
}
|
|
1485
|
-
yield { type: "plan:stage", stage:
|
|
1474
|
+
yield { type: "plan:stage", stage: stages.validate, total: stages.total, name: "Validate" };
|
|
1486
1475
|
const judgeResult = await runPass3Judge(description, zodResult.data);
|
|
1487
1476
|
if (judgeResult.skipped) {
|
|
1488
1477
|
yield { type: "plan:warn", message: "Judge skipped due to error \u2014 proceeding without validation" };
|
|
1489
1478
|
}
|
|
1490
1479
|
if (!judgeResult.pass && attempt < MAX_PLAN_RETRIES - 1) {
|
|
1491
|
-
retryPrefix = PLAN_RETRY_JUDGE
|
|
1480
|
+
retryPrefix = fillTemplate(PLAN_RETRY_JUDGE, { FEEDBACK: judgeResult.feedback });
|
|
1492
1481
|
continue;
|
|
1493
1482
|
}
|
|
1494
1483
|
if (!judgeResult.pass) {
|
|
@@ -1503,7 +1492,8 @@ ${issues}` };
|
|
|
1503
1492
|
forceQuotes: false
|
|
1504
1493
|
}).trimEnd();
|
|
1505
1494
|
writeFileSync2(taskFile, yamlContent + "\n", "utf8");
|
|
1506
|
-
const
|
|
1495
|
+
const yamlLines = yamlContent.split("\n");
|
|
1496
|
+
const preview = yamlLines.slice(0, 30).join("\n") + (yamlLines.length > 30 ? "\n..." : "");
|
|
1507
1497
|
yield { type: "plan:complete", taskFile, preview };
|
|
1508
1498
|
return;
|
|
1509
1499
|
}
|
|
@@ -1687,17 +1677,6 @@ import {
|
|
|
1687
1677
|
writeFileSync as writeFileSync3
|
|
1688
1678
|
} from "node:fs";
|
|
1689
1679
|
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
1680
|
function findExecutantLocalDir(startDir) {
|
|
1702
1681
|
let dir = resolve2(startDir);
|
|
1703
1682
|
while (true) {
|
|
@@ -1711,273 +1690,179 @@ function findExecutantLocalDir(startDir) {
|
|
|
1711
1690
|
function resolveLogDir(workflowFilePath) {
|
|
1712
1691
|
const startDir = dirname3(resolve2(workflowFilePath));
|
|
1713
1692
|
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
|
-
this.dispatch(event);
|
|
1751
|
-
} catch (err) {
|
|
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}
|
|
1693
|
+
return executantLocal ? join3(executantLocal, "logs") : join3(startDir, "logs");
|
|
1694
|
+
}
|
|
1695
|
+
var INIT_STATE = {
|
|
1696
|
+
logFile: "",
|
|
1697
|
+
stepIndex: -1,
|
|
1698
|
+
stepName: "",
|
|
1699
|
+
stepStartMs: 0,
|
|
1700
|
+
toolCount: 0,
|
|
1701
|
+
complexSequenceFile: "",
|
|
1702
|
+
selfHealingFile: "",
|
|
1703
|
+
judgeAttempt: 0,
|
|
1704
|
+
recentOutput: []
|
|
1705
|
+
};
|
|
1706
|
+
var TOOL_SUMMARY = {
|
|
1707
|
+
Read: (i) => String(i["file_path"] ?? i["path"] ?? ""),
|
|
1708
|
+
Edit: (i) => String(i["file_path"] ?? ""),
|
|
1709
|
+
Write: (i) => String(i["file_path"] ?? ""),
|
|
1710
|
+
Bash: (i) => String(i["command"] ?? ""),
|
|
1711
|
+
Glob: (i) => String(i["pattern"] ?? ""),
|
|
1712
|
+
Grep: (i) => String(i["pattern"] ?? "")
|
|
1713
|
+
};
|
|
1714
|
+
function toolSummary(tool, input) {
|
|
1715
|
+
return (TOOL_SUMMARY[tool] ?? ((i) => JSON.stringify(i)))(input);
|
|
1716
|
+
}
|
|
1717
|
+
function appendLog(logFile, text) {
|
|
1718
|
+
if (logFile) appendFileSync(logFile, text + "\n");
|
|
1719
|
+
}
|
|
1720
|
+
function highlightPath(ctx, stepIndex, suffix) {
|
|
1721
|
+
return join3(ctx.highlightsDir, `${ctx.ts}_step${stepIndex + 1}_${suffix}.md`);
|
|
1722
|
+
}
|
|
1723
|
+
function onWorkflowStart(ctx, s) {
|
|
1724
|
+
mkdirSync3(ctx.logDir, { recursive: true });
|
|
1725
|
+
mkdirSync3(ctx.highlightsDir, { recursive: true });
|
|
1726
|
+
const logFile = join3(ctx.logDir, `${ctx.ts}_${ctx.slug}.log`);
|
|
1727
|
+
writeFileSync3(logFile, `# Execution Log
|
|
1728
|
+
Task: ${ctx.slug}
|
|
1798
1729
|
Started: ${(/* @__PURE__ */ new Date()).toISOString()}
|
|
1799
1730
|
${"\u2501".repeat(51)}
|
|
1800
1731
|
|
|
1801
|
-
`
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
onStepStart(index, name) {
|
|
1808
|
-
Object.assign(this, {
|
|
1809
|
-
stepIndex: index,
|
|
1810
|
-
stepName: name,
|
|
1811
|
-
stepStartMs: Date.now(),
|
|
1812
|
-
toolCount: 0,
|
|
1813
|
-
complexSequenceFile: "",
|
|
1814
|
-
selfHealingFile: "",
|
|
1815
|
-
judgeAttempt: 0,
|
|
1816
|
-
recentOutput: []
|
|
1817
|
-
});
|
|
1818
|
-
this.appendLog(
|
|
1819
|
-
`
|
|
1732
|
+
`);
|
|
1733
|
+
return { ...s, logFile };
|
|
1734
|
+
}
|
|
1735
|
+
function onStepStart(ctx, s, index, name) {
|
|
1736
|
+
const next = { ...INIT_STATE, logFile: s.logFile, stepIndex: index, stepName: name, stepStartMs: Date.now() };
|
|
1737
|
+
appendLog(next.logFile, `
|
|
1820
1738
|
${"\u2501".repeat(51)}
|
|
1821
1739
|
Step ${index + 1}: ${name}
|
|
1822
1740
|
Started: ${(/* @__PURE__ */ new Date()).toISOString()}
|
|
1823
1741
|
${"\u2501".repeat(51)}
|
|
1824
|
-
`
|
|
1825
|
-
);
|
|
1826
|
-
}
|
|
1827
|
-
onStepComplete() {
|
|
1828
|
-
const durS = ((Date.now() - this.stepStartMs) / 1e3).toFixed(1);
|
|
1829
|
-
this.appendLog(`
|
|
1830
|
-
Step completed in ${durS}s
|
|
1831
|
-
`);
|
|
1832
|
-
this.finalizeComplexSequence();
|
|
1833
|
-
}
|
|
1834
|
-
onStepError(error) {
|
|
1835
|
-
this.appendLog(`
|
|
1836
|
-
Step failed: ${error.message}
|
|
1837
1742
|
`);
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
onTool(tool, input) {
|
|
1844
|
-
const desc = toolSummary(tool, input);
|
|
1845
|
-
this.appendLog(` [${tool}] ${desc}`);
|
|
1846
|
-
this.toolCount++;
|
|
1847
|
-
if (this.toolCount === 3) {
|
|
1848
|
-
this.complexSequenceFile = join3(
|
|
1849
|
-
this.highlightsDir,
|
|
1850
|
-
`${this.timestamp}_step${this.stepIndex + 1}_complex_sequence.md`
|
|
1851
|
-
);
|
|
1852
|
-
writeFileSync3(
|
|
1853
|
-
this.complexSequenceFile,
|
|
1854
|
-
[
|
|
1855
|
-
"# Complex Tool Sequence",
|
|
1856
|
-
"",
|
|
1857
|
-
`**Task:** ${this.taskName}`,
|
|
1858
|
-
`**Step:** ${this.stepName}`,
|
|
1859
|
-
`**Timestamp:** ${(/* @__PURE__ */ new Date()).toISOString()}`,
|
|
1860
|
-
"",
|
|
1861
|
-
"---",
|
|
1862
|
-
"",
|
|
1863
|
-
"## Claude's Tool Orchestration",
|
|
1864
|
-
"",
|
|
1865
|
-
"Claude used multiple tools to complete this step:",
|
|
1866
|
-
""
|
|
1867
|
-
].join("\n")
|
|
1868
|
-
);
|
|
1869
|
-
}
|
|
1870
|
-
if (this.toolCount >= 3 && this.complexSequenceFile) {
|
|
1871
|
-
appendFileSync(this.complexSequenceFile, `${this.toolCount}. **${tool}** - ${desc}
|
|
1872
|
-
`);
|
|
1873
|
-
}
|
|
1874
|
-
}
|
|
1875
|
-
finalizeComplexSequence() {
|
|
1876
|
-
if (this.toolCount >= 3 && this.complexSequenceFile) {
|
|
1877
|
-
appendFileSync(
|
|
1878
|
-
this.complexSequenceFile,
|
|
1879
|
-
`
|
|
1743
|
+
return next;
|
|
1744
|
+
}
|
|
1745
|
+
function finalizeComplexSequence(s) {
|
|
1746
|
+
if (s.toolCount >= 3 && s.complexSequenceFile) {
|
|
1747
|
+
appendFileSync(s.complexSequenceFile, `
|
|
1880
1748
|
---
|
|
1881
1749
|
|
|
1882
|
-
*Total tools used: ${
|
|
1750
|
+
*Total tools used: ${s.toolCount}*
|
|
1883
1751
|
|
|
1884
1752
|
*Captured by Executant Logger*
|
|
1885
|
-
`
|
|
1886
|
-
);
|
|
1887
|
-
}
|
|
1888
|
-
}
|
|
1889
|
-
// --------------------------------------------------------------------------
|
|
1890
|
-
// Log events → judge / self-healing highlights
|
|
1891
|
-
// --------------------------------------------------------------------------
|
|
1892
|
-
onLogMessage(level, text) {
|
|
1893
|
-
this.appendLog(`[${level}] ${text}`);
|
|
1894
|
-
if (/\[judge\]\s+PASS/i.test(text)) {
|
|
1895
|
-
this.judgeAttempt++;
|
|
1896
|
-
this.saveJudgeHighlight("PASS", text);
|
|
1897
|
-
return;
|
|
1898
|
-
}
|
|
1899
|
-
if (/\[judge\]\s+FAIL/i.test(text)) {
|
|
1900
|
-
this.judgeAttempt++;
|
|
1901
|
-
this.saveJudgeHighlight("FAIL", text);
|
|
1902
|
-
return;
|
|
1903
|
-
}
|
|
1904
|
-
const healingMatch = text.match(/\[self-healing\].*failed.*exit\s+(\d+)/i);
|
|
1905
|
-
if (healingMatch) {
|
|
1906
|
-
this.startSelfHealingHighlight(healingMatch[1]);
|
|
1907
|
-
return;
|
|
1908
|
-
}
|
|
1909
|
-
if (/\[self-healing\].*Re-running/i.test(text)) {
|
|
1910
|
-
this.completeSelfHealingHighlight();
|
|
1911
|
-
}
|
|
1753
|
+
`);
|
|
1912
1754
|
}
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
1755
|
+
}
|
|
1756
|
+
function onStepComplete(s) {
|
|
1757
|
+
appendLog(s.logFile, `
|
|
1758
|
+
Step completed in ${((Date.now() - s.stepStartMs) / 1e3).toFixed(1)}s
|
|
1759
|
+
`);
|
|
1760
|
+
finalizeComplexSequence(s);
|
|
1761
|
+
return s;
|
|
1762
|
+
}
|
|
1763
|
+
function onStepError(s, error) {
|
|
1764
|
+
appendLog(s.logFile, `
|
|
1765
|
+
Step failed: ${error.message}
|
|
1766
|
+
`);
|
|
1767
|
+
finalizeComplexSequence(s);
|
|
1768
|
+
return s;
|
|
1769
|
+
}
|
|
1770
|
+
function complexSequenceHeader(ctx, s) {
|
|
1771
|
+
return [
|
|
1772
|
+
"# Complex Tool Sequence",
|
|
1773
|
+
"",
|
|
1774
|
+
`**Task:** ${ctx.slug}`,
|
|
1775
|
+
`**Step:** ${s.stepName}`,
|
|
1776
|
+
`**Timestamp:** ${(/* @__PURE__ */ new Date()).toISOString()}`,
|
|
1777
|
+
"",
|
|
1778
|
+
"---",
|
|
1779
|
+
"",
|
|
1780
|
+
"## Claude's Tool Orchestration",
|
|
1781
|
+
"",
|
|
1782
|
+
"Claude used multiple tools to complete this step:",
|
|
1783
|
+
""
|
|
1784
|
+
].join("\n");
|
|
1785
|
+
}
|
|
1786
|
+
function createComplexSequenceFile(ctx, s) {
|
|
1787
|
+
const path = highlightPath(ctx, s.stepIndex, "complex_sequence");
|
|
1788
|
+
writeFileSync3(path, complexSequenceHeader(ctx, s));
|
|
1789
|
+
return path;
|
|
1790
|
+
}
|
|
1791
|
+
function onTool(ctx, s, tool, input) {
|
|
1792
|
+
const desc = toolSummary(tool, input);
|
|
1793
|
+
appendLog(s.logFile, ` [${tool}] ${desc}`);
|
|
1794
|
+
const toolCount = s.toolCount + 1;
|
|
1795
|
+
const complexSequenceFile = toolCount === 3 ? createComplexSequenceFile(ctx, s) : s.complexSequenceFile;
|
|
1796
|
+
if (toolCount >= 3 && complexSequenceFile) {
|
|
1797
|
+
appendFileSync(complexSequenceFile, `${toolCount}. **${tool}** - ${desc}
|
|
1798
|
+
`);
|
|
1941
1799
|
}
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
|
|
1800
|
+
return { ...s, toolCount, complexSequenceFile };
|
|
1801
|
+
}
|
|
1802
|
+
function saveJudgeHighlight(ctx, s, verdict, text) {
|
|
1803
|
+
writeFileSync3(highlightPath(ctx, s.stepIndex, `judge_${verdict}`), [
|
|
1804
|
+
`# Judge Verdict: ${verdict}`,
|
|
1805
|
+
"",
|
|
1806
|
+
`**Task:** ${ctx.slug}`,
|
|
1807
|
+
`**Step:** ${s.stepName}`,
|
|
1808
|
+
`**Attempt:** ${s.judgeAttempt}`,
|
|
1809
|
+
`**Timestamp:** ${(/* @__PURE__ */ new Date()).toISOString()}`,
|
|
1810
|
+
"",
|
|
1811
|
+
"---",
|
|
1812
|
+
"",
|
|
1813
|
+
text,
|
|
1814
|
+
"",
|
|
1815
|
+
"---",
|
|
1816
|
+
"",
|
|
1817
|
+
"*Auto-captured*",
|
|
1818
|
+
""
|
|
1819
|
+
].join("\n"));
|
|
1820
|
+
}
|
|
1821
|
+
var LOG_MATCHERS = [
|
|
1822
|
+
{
|
|
1823
|
+
pattern: /\[judge\]\s+(PASS|FAIL)/i,
|
|
1824
|
+
apply: (ctx, s, text, match) => {
|
|
1825
|
+
const verdict = match[1].toUpperCase();
|
|
1826
|
+
const judgeAttempt = s.judgeAttempt + 1;
|
|
1827
|
+
saveJudgeHighlight(ctx, { ...s, judgeAttempt }, verdict, text);
|
|
1828
|
+
return { ...s, judgeAttempt };
|
|
1829
|
+
}
|
|
1830
|
+
},
|
|
1831
|
+
{
|
|
1832
|
+
pattern: /\[self-healing\].*failed.*exit\s+(\d+)/i,
|
|
1833
|
+
apply: (ctx, s, text, match) => {
|
|
1834
|
+
const selfHealingFile = highlightPath(ctx, s.stepIndex, "self_healing");
|
|
1835
|
+
writeFileSync3(selfHealingFile, [
|
|
1952
1836
|
"# Self-Healing Activation",
|
|
1953
1837
|
"",
|
|
1954
|
-
`**Task:** ${
|
|
1955
|
-
`**Step:** ${
|
|
1838
|
+
`**Task:** ${ctx.slug}`,
|
|
1839
|
+
`**Step:** ${s.stepName}`,
|
|
1956
1840
|
`**Timestamp:** ${(/* @__PURE__ */ new Date()).toISOString()}`,
|
|
1957
1841
|
"",
|
|
1958
1842
|
"---",
|
|
1959
1843
|
"",
|
|
1960
1844
|
"## \u274C Failure Detected",
|
|
1961
1845
|
"",
|
|
1962
|
-
`**Exit Code:** ${
|
|
1846
|
+
`**Exit Code:** ${match[1]}`,
|
|
1963
1847
|
"",
|
|
1964
1848
|
"**Recent Output:**",
|
|
1965
1849
|
"```",
|
|
1966
|
-
|
|
1850
|
+
s.recentOutput.join("\n"),
|
|
1967
1851
|
"```",
|
|
1968
1852
|
"",
|
|
1969
1853
|
"---",
|
|
1970
1854
|
"",
|
|
1971
1855
|
"## \u{1F527} Claude's Healing Process",
|
|
1972
1856
|
""
|
|
1973
|
-
].join("\n")
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
|
|
1857
|
+
].join("\n"));
|
|
1858
|
+
return { ...s, selfHealingFile, recentOutput: [] };
|
|
1859
|
+
}
|
|
1860
|
+
},
|
|
1861
|
+
{
|
|
1862
|
+
pattern: /\[self-healing\].*Re-running/i,
|
|
1863
|
+
apply: (_ctx, s) => {
|
|
1864
|
+
if (!s.selfHealingFile) return s;
|
|
1865
|
+
appendFileSync(s.selfHealingFile, [
|
|
1981
1866
|
"",
|
|
1982
1867
|
"*(See full log for Claude's diagnostic process)*",
|
|
1983
1868
|
"",
|
|
@@ -1991,57 +1876,98 @@ Step failed: ${error.message}
|
|
|
1991
1876
|
"",
|
|
1992
1877
|
"*Auto-captured*",
|
|
1993
1878
|
""
|
|
1994
|
-
].join("\n")
|
|
1995
|
-
|
|
1996
|
-
|
|
1997
|
-
}
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
|
|
2001
|
-
|
|
2002
|
-
|
|
2003
|
-
|
|
1879
|
+
].join("\n"));
|
|
1880
|
+
return { ...s, selfHealingFile: "" };
|
|
1881
|
+
}
|
|
1882
|
+
}
|
|
1883
|
+
];
|
|
1884
|
+
function onLogMessage(ctx, s, level, text) {
|
|
1885
|
+
appendLog(s.logFile, `[${level}] ${text}`);
|
|
1886
|
+
return LOG_MATCHERS.reduce(
|
|
1887
|
+
({ matched, state }, { pattern, apply }) => {
|
|
1888
|
+
if (matched) return { matched, state };
|
|
1889
|
+
const m = pattern.exec(text);
|
|
1890
|
+
return m ? { matched: true, state: apply(ctx, state, text, m) } : { matched, state };
|
|
1891
|
+
},
|
|
1892
|
+
{ matched: false, state: s }
|
|
1893
|
+
).state;
|
|
1894
|
+
}
|
|
1895
|
+
function onWorkflowComplete(ctx, s) {
|
|
1896
|
+
appendLog(s.logFile, `
|
|
2004
1897
|
${"\u2501".repeat(51)}
|
|
2005
|
-
Task Complete: ${
|
|
1898
|
+
Task Complete: ${ctx.slug}
|
|
2006
1899
|
Finished: ${(/* @__PURE__ */ new Date()).toISOString()}
|
|
2007
1900
|
${"\u2501".repeat(51)}
|
|
2008
|
-
`
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
|
|
2012
|
-
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
|
|
2023
|
-
|
|
2024
|
-
|
|
2025
|
-
);
|
|
2026
|
-
}
|
|
2027
|
-
const files = readdirSync(this.highlightsDir);
|
|
2028
|
-
const taskHighlights = files.filter((f) => f.startsWith(this.timestamp) && f.endsWith(".md")).sort();
|
|
2029
|
-
if (taskHighlights.length > 0) {
|
|
2030
|
-
const entries = taskHighlights.map((f) => `- [${f.replace(/\.md$/, "")}](./${f})`).join("\n");
|
|
2031
|
-
appendFileSync(indexFile, `
|
|
2032
|
-
### ${this.taskName} (${(/* @__PURE__ */ new Date()).toISOString()})
|
|
1901
|
+
`);
|
|
1902
|
+
const indexFile = join3(ctx.highlightsDir, "README.md");
|
|
1903
|
+
if (!existsSync2(indexFile)) {
|
|
1904
|
+
writeFileSync3(indexFile, [
|
|
1905
|
+
"# Execution Highlights",
|
|
1906
|
+
"",
|
|
1907
|
+
"This directory contains automatically extracted highlight moments from task executions.",
|
|
1908
|
+
"",
|
|
1909
|
+
"## Latest Highlights",
|
|
1910
|
+
""
|
|
1911
|
+
].join("\n"));
|
|
1912
|
+
}
|
|
1913
|
+
const highlights = readdirSync(ctx.highlightsDir).filter((f) => f.startsWith(ctx.ts) && f.endsWith(".md")).sort();
|
|
1914
|
+
if (highlights.length > 0) {
|
|
1915
|
+
const entries = highlights.map((f) => `- [${f.replace(/\.md$/, "")}](./${f})`).join("\n");
|
|
1916
|
+
appendFileSync(indexFile, `
|
|
1917
|
+
### ${ctx.slug} (${(/* @__PURE__ */ new Date()).toISOString()})
|
|
2033
1918
|
${entries}
|
|
2034
1919
|
`);
|
|
2035
|
-
}
|
|
2036
1920
|
}
|
|
2037
|
-
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
appendLog(text)
|
|
2041
|
-
|
|
2042
|
-
|
|
1921
|
+
return s;
|
|
1922
|
+
}
|
|
1923
|
+
function onOutputText(s, text) {
|
|
1924
|
+
appendLog(s.logFile, text);
|
|
1925
|
+
return { ...s, recentOutput: [...s.recentOutput, text] };
|
|
1926
|
+
}
|
|
1927
|
+
function reduce(ctx, s, event) {
|
|
1928
|
+
switch (event.type) {
|
|
1929
|
+
case "workflow:start":
|
|
1930
|
+
return onWorkflowStart(ctx, s);
|
|
1931
|
+
case "step:start":
|
|
1932
|
+
return onStepStart(ctx, s, event.index, event.name);
|
|
1933
|
+
case "step:complete":
|
|
1934
|
+
return onStepComplete(s);
|
|
1935
|
+
case "step:error":
|
|
1936
|
+
return onStepError(s, event.error);
|
|
1937
|
+
case "output:text":
|
|
1938
|
+
return onOutputText(s, event.text);
|
|
1939
|
+
case "output:tool":
|
|
1940
|
+
return onTool(ctx, s, event.tool, event.input);
|
|
1941
|
+
case "log":
|
|
1942
|
+
return onLogMessage(ctx, s, event.level, event.text);
|
|
1943
|
+
case "workflow:complete":
|
|
1944
|
+
return onWorkflowComplete(ctx, s);
|
|
1945
|
+
default:
|
|
1946
|
+
return s;
|
|
2043
1947
|
}
|
|
2044
|
-
}
|
|
1948
|
+
}
|
|
1949
|
+
function createLogger(logDir, taskName) {
|
|
1950
|
+
const ctx = {
|
|
1951
|
+
logDir,
|
|
1952
|
+
highlightsDir: join3(logDir, "highlights"),
|
|
1953
|
+
ts: formatTimestamp(/* @__PURE__ */ new Date()),
|
|
1954
|
+
slug: slugify(taskName, 40) || "task"
|
|
1955
|
+
};
|
|
1956
|
+
const enabled = process.env["EXECUTANT_LOG"] !== "0";
|
|
1957
|
+
let state = INIT_STATE;
|
|
1958
|
+
return {
|
|
1959
|
+
getHighlightsDir: () => ctx.highlightsDir,
|
|
1960
|
+
getTimestamp: () => ctx.ts,
|
|
1961
|
+
observe(event) {
|
|
1962
|
+
if (!enabled) return;
|
|
1963
|
+
try {
|
|
1964
|
+
state = reduce(ctx, state, event);
|
|
1965
|
+
} catch (err) {
|
|
1966
|
+
console.warn(`[logger] error: ${getErrorMessage(err)}`);
|
|
1967
|
+
}
|
|
1968
|
+
}
|
|
1969
|
+
};
|
|
1970
|
+
}
|
|
2045
1971
|
async function* withLogger(gen, logger2) {
|
|
2046
1972
|
for await (const event of gen) {
|
|
2047
1973
|
logger2.observe(event);
|
|
@@ -2052,7 +1978,6 @@ async function* withLogger(gen, logger2) {
|
|
|
2052
1978
|
// src/retrospective.ts
|
|
2053
1979
|
import { existsSync as existsSync3, mkdirSync as mkdirSync4, readdirSync as readdirSync2, readFileSync as readFileSync5, writeFileSync as writeFileSync4 } from "node:fs";
|
|
2054
1980
|
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
1981
|
import { spawnSync } from "node:child_process";
|
|
2057
1982
|
import { load as parseYaml2 } from "js-yaml";
|
|
2058
1983
|
import { z as z4 } from "zod";
|
|
@@ -2060,15 +1985,13 @@ var RetrospectiveOutputSchema = z4.object({
|
|
|
2060
1985
|
improved_yaml: z4.string(),
|
|
2061
1986
|
changelog: z4.string()
|
|
2062
1987
|
});
|
|
2063
|
-
var
|
|
2064
|
-
var RETROSPECTIVE_PROMPT = readFileSync5(join4(PROMPTS_DIR2, "retrospective-analysis.txt"), "utf8");
|
|
1988
|
+
var RETROSPECTIVE_PROMPT = loadPrompt("retrospective-analysis");
|
|
2065
1989
|
async function runRetrospective(workflowFilePath, workflow2, highlightsDir, runTimestamp) {
|
|
2066
1990
|
try {
|
|
2067
1991
|
await doRetrospective(workflowFilePath, workflow2, highlightsDir, runTimestamp);
|
|
2068
1992
|
} catch (err) {
|
|
2069
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
2070
1993
|
console.warn(`
|
|
2071
|
-
Self-improvement: retrospective failed: ${
|
|
1994
|
+
Self-improvement: retrospective failed: ${getErrorMessage(err)}`);
|
|
2072
1995
|
}
|
|
2073
1996
|
}
|
|
2074
1997
|
async function doRetrospective(workflowFilePath, workflow2, highlightsDir, runTimestamp) {
|
|
@@ -2112,7 +2035,13 @@ ${content}`;
|
|
|
2112
2035
|
}).join("\n\n---\n\n");
|
|
2113
2036
|
const originalYaml = readFileSync5(workflowFilePath, "utf8");
|
|
2114
2037
|
const taskName = basename2(workflowFilePath, ".yaml");
|
|
2115
|
-
const prompt = RETROSPECTIVE_PROMPT
|
|
2038
|
+
const prompt = fillTemplate(RETROSPECTIVE_PROMPT, {
|
|
2039
|
+
TASK_NAME: taskName,
|
|
2040
|
+
ORIGINAL_GOAL: workflow2.goal,
|
|
2041
|
+
ORIGINAL_YAML: originalYaml,
|
|
2042
|
+
HIGHLIGHTS: highlightContents,
|
|
2043
|
+
METRICS: metrics
|
|
2044
|
+
});
|
|
2116
2045
|
const result = spawnSync(
|
|
2117
2046
|
"claude",
|
|
2118
2047
|
[
|
|
@@ -2155,8 +2084,7 @@ Response: ${response.trim()}`);
|
|
|
2155
2084
|
try {
|
|
2156
2085
|
parseYaml2(improvedYaml);
|
|
2157
2086
|
} catch (err) {
|
|
2158
|
-
|
|
2159
|
-
console.warn(`Self-improvement: generated YAML is invalid (${msg}), skipping save.`);
|
|
2087
|
+
console.warn(`Self-improvement: generated YAML is invalid (${getErrorMessage(err)}), skipping save.`);
|
|
2160
2088
|
return;
|
|
2161
2089
|
}
|
|
2162
2090
|
const startDir = dirname4(resolve3(workflowFilePath));
|
|
@@ -2187,7 +2115,7 @@ function extractJson(text) {
|
|
|
2187
2115
|
|
|
2188
2116
|
// src/index.ts
|
|
2189
2117
|
var CURRENT_VERSION = JSON.parse(
|
|
2190
|
-
readFileSync6(join5(dirname5(
|
|
2118
|
+
readFileSync6(join5(dirname5(fileURLToPath2(import.meta.url)), "../package.json"), "utf-8")
|
|
2191
2119
|
).version;
|
|
2192
2120
|
var rawArgs = process.argv.slice(2);
|
|
2193
2121
|
if (rawArgs[0] === "plan") {
|
|
@@ -2212,7 +2140,7 @@ if (rawArgs[0] === "update") {
|
|
|
2212
2140
|
await doUpdate2();
|
|
2213
2141
|
console.log("Done.");
|
|
2214
2142
|
} catch (err) {
|
|
2215
|
-
console.error("Update failed:",
|
|
2143
|
+
console.error("Update failed:", getErrorMessage(err));
|
|
2216
2144
|
process.exit(1);
|
|
2217
2145
|
}
|
|
2218
2146
|
process.exit(0);
|
|
@@ -2318,12 +2246,12 @@ var workflow;
|
|
|
2318
2246
|
try {
|
|
2319
2247
|
workflow = loadWorkflow(filePath);
|
|
2320
2248
|
} catch (err) {
|
|
2321
|
-
console.error(
|
|
2249
|
+
console.error(getErrorMessage(err));
|
|
2322
2250
|
process.exit(1);
|
|
2323
2251
|
}
|
|
2324
2252
|
var options = { stepFilter, fromStep };
|
|
2325
2253
|
var rawEvents = runWorkflow(workflow, options);
|
|
2326
|
-
var logger =
|
|
2254
|
+
var logger = createLogger(resolveLogDir(filePath), workflow.goal);
|
|
2327
2255
|
var events = withLogger(rawEvents, logger);
|
|
2328
2256
|
var updateCheck = checkForUpdate(CURRENT_VERSION);
|
|
2329
2257
|
function errorReplacer(_key, value) {
|
|
@@ -2337,7 +2265,7 @@ async function maybeRunRetrospective(filePath2, workflow2, logger2) {
|
|
|
2337
2265
|
try {
|
|
2338
2266
|
await runRetrospective(filePath2, workflow2, logger2.getHighlightsDir(), logger2.getTimestamp());
|
|
2339
2267
|
} catch (err) {
|
|
2340
|
-
console.warn("[executant] retrospective failed (non-fatal):", err
|
|
2268
|
+
console.warn("[executant] retrospective failed (non-fatal):", getErrorMessage(err));
|
|
2341
2269
|
}
|
|
2342
2270
|
}
|
|
2343
2271
|
if (ciMode) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "executant",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.7.0",
|
|
4
4
|
"description": "Harness for YAML-defined workflows that enables stepping through Claude sessions and bash commands",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -44,6 +44,7 @@
|
|
|
44
44
|
"husky": "^9.1.7",
|
|
45
45
|
"knip": "^5.88.1",
|
|
46
46
|
"lint-staged": "^16.4.0",
|
|
47
|
+
"prettier": "^3.8.3",
|
|
47
48
|
"semantic-release": "^24.2.9",
|
|
48
49
|
"tsx": "^4.15.7",
|
|
49
50
|
"typescript": "^5.4.5",
|
|
@@ -55,7 +56,10 @@
|
|
|
55
56
|
]
|
|
56
57
|
},
|
|
57
58
|
"lint-staged": {
|
|
58
|
-
"*.{ts,tsx}":
|
|
59
|
+
"*.{ts,tsx}": [
|
|
60
|
+
"prettier --write",
|
|
61
|
+
"eslint --fix"
|
|
62
|
+
]
|
|
59
63
|
},
|
|
60
64
|
"release": {
|
|
61
65
|
"plugins": [
|