executant 1.6.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.
Files changed (2) hide show
  1. package/dist/index.js +341 -413
  2. 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 fileURLToPath3 } from "node:url";
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 = readFileSync(filePath2, "utf8");
141
+ raw = readFileSync2(filePath2, "utf8");
89
142
  } catch (err) {
90
- const msg = err instanceof Error ? err.message : String(err);
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.map((e) => ` ${e.path.join(".")}: ${e.message}`).join("\n") : String(err);
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.name;
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 items = Array.from({ length: step.repeat }, (_2, i) => String(i + 1));
117
- const { repeat: _, ...innerStep } = step;
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: step.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 = Object.entries(vars).reduce(
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
- const msg = err instanceof Error ? err.message : String(err);
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 (options2.stepFilter !== void 0) {
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
- const msg = err instanceof Error ? err.message : String(err);
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.replace("{{FEEDBACK}}", judgeContext)}`;
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
- const msg = err instanceof Error ? err.message : String(err);
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.replaceAll("{{COMMAND}}", command).replaceAll("{{EXIT_CODE}}", String(exitCode)).replaceAll("{{OUTPUT}}", output).replaceAll("{{ATTEMPT_HISTORY}}", attemptHistory);
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.replace("{{STEP_NAME}}", stepName).replace("{{STEP_INSTRUCTIONS}}", instructions).replace("{{OUTPUT}}", output);
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
- type: "log",
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.replace("{{DESCRIPTION}}", description).replace("{{WORKFLOW_JSON}}", JSON.stringify(workflow2, null, 2)),
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.replace("{{DESCRIPTION}}", description),
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
- const msg = err instanceof Error ? err.message : String(err);
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 decomposeStage = skipResearch ? 1 : 2;
1416
- const validateStage = skipResearch ? 2 : 3;
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: decomposeStage, total: totalStages, name: "Decompose to Steps" };
1417
+ yield { type: "plan:stage", stage: stages.decompose, total: stages.total, name: "Decompose to Steps" };
1429
1418
  }
1430
- const basePrompt = PLAN_DECOMPOSE_PROMPT.replace("{{DESCRIPTION}}", description).replace("{{RESEARCH_DOC}}", researchDoc);
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 = err instanceof Error ? err.message : String(err);
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.replace("{{ERROR}}", msg).replace("{{EXCERPT}}", decomposeTextLines.join("\n"));
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.replace("{{ISSUES}}", issues);
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.map((i) => ` ${i.path.join(".")}: ${i.message}`).join("\n");
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.replace("{{ISSUES}}", issues);
1471
+ retryPrefix = fillTemplate(PLAN_RETRY_SCHEMA_ERROR, { ISSUES: issues });
1483
1472
  continue;
1484
1473
  }
1485
- yield { type: "plan:stage", stage: validateStage, total: totalStages, name: "Validate" };
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.replace("{{FEEDBACK}}", judgeResult.feedback);
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 preview = yamlContent.split("\n").slice(0, 30).join("\n") + (yamlContent.split("\n").length > 30 ? "\n..." : "");
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
- if (executantLocal) return join3(executantLocal, "logs");
1715
- return join3(startDir, "logs");
1716
- }
1717
- var Logger = class {
1718
- enabled;
1719
- logDir;
1720
- highlightsDir;
1721
- timestamp;
1722
- taskName;
1723
- logFile = "";
1724
- // Per-step state
1725
- stepIndex = -1;
1726
- stepName = "";
1727
- stepStartMs = 0;
1728
- toolCount = 0;
1729
- complexSequenceFile = "";
1730
- selfHealingFile = "";
1731
- judgeAttempt = 0;
1732
- recentOutput = [];
1733
- constructor(logDir, taskName) {
1734
- this.enabled = process.env["EXECUTANT_LOG"] !== "0";
1735
- this.logDir = logDir;
1736
- this.highlightsDir = join3(logDir, "highlights");
1737
- this.timestamp = formatTimestamp(/* @__PURE__ */ new Date());
1738
- this.taskName = slugify(taskName, 40) || "task";
1739
- }
1740
- getHighlightsDir() {
1741
- return this.highlightsDir;
1742
- }
1743
- getTimestamp() {
1744
- return this.timestamp;
1745
- }
1746
- /** Feed each event from the runner into the logger. */
1747
- observe(event) {
1748
- if (!this.enabled) return;
1749
- try {
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
- // Step lifecycle
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
- this.finalizeComplexSequence();
1839
- }
1840
- // --------------------------------------------------------------------------
1841
- // Tool calls complex sequence highlights
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: ${this.toolCount}*
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
- // 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
- );
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
- 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 = [];
1949
- writeFileSync3(
1950
- this.selfHealingFile,
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:** ${this.taskName}`,
1955
- `**Step:** ${this.stepName}`,
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:** ${exitCode}`,
1846
+ `**Exit Code:** ${match[1]}`,
1963
1847
  "",
1964
1848
  "**Recent Output:**",
1965
1849
  "```",
1966
- errorOutput,
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
- completeSelfHealingHighlight() {
1977
- if (!this.selfHealingFile) return;
1978
- appendFileSync(
1979
- this.selfHealingFile,
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
- this.selfHealingFile = "";
1997
- }
1998
- // --------------------------------------------------------------------------
1999
- // Workflow complete index
2000
- // --------------------------------------------------------------------------
2001
- onWorkflowComplete() {
2002
- this.appendLog(
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: ${this.taskName}
1898
+ Task Complete: ${ctx.slug}
2006
1899
  Finished: ${(/* @__PURE__ */ new Date()).toISOString()}
2007
1900
  ${"\u2501".repeat(51)}
2008
- `
2009
- );
2010
- this.writeHighlightsIndex();
2011
- }
2012
- writeHighlightsIndex() {
2013
- const indexFile = join3(this.highlightsDir, "README.md");
2014
- if (!existsSync2(indexFile)) {
2015
- writeFileSync3(
2016
- indexFile,
2017
- [
2018
- "# Execution Highlights",
2019
- "",
2020
- "This directory contains automatically extracted highlight moments from task executions.",
2021
- "",
2022
- "## Latest Highlights",
2023
- ""
2024
- ].join("\n")
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
- // Log file writes
2039
- // --------------------------------------------------------------------------
2040
- appendLog(text) {
2041
- if (!this.logFile) return;
2042
- appendFileSync(this.logFile, text + "\n");
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 PROMPTS_DIR2 = join4(dirname4(fileURLToPath2(import.meta.url)), "prompts");
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: ${msg}`);
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.replaceAll("{{TASK_NAME}}", taskName).replaceAll("{{ORIGINAL_GOAL}}", workflow2.goal).replaceAll("{{ORIGINAL_YAML}}", originalYaml).replaceAll("{{HIGHLIGHTS}}", highlightContents).replaceAll("{{METRICS}}", metrics);
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
- const msg = err instanceof Error ? err.message : String(err);
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(fileURLToPath3(import.meta.url)), "../package.json"), "utf-8")
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:", err instanceof Error ? err.message : String(err));
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(err instanceof Error ? err.message : String(err));
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 = new Logger(resolveLogDir(filePath), workflow.goal);
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 instanceof Error ? err.message : 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.6.0",
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}": "eslint --fix"
59
+ "*.{ts,tsx}": [
60
+ "prettier --write",
61
+ "eslint --fix"
62
+ ]
59
63
  },
60
64
  "release": {
61
65
  "plugins": [