executant 1.4.2 → 1.4.4

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 CHANGED
@@ -1292,6 +1292,73 @@ async function runPass3Judge(description, workflow2) {
1292
1292
  return { pass: true, feedback: "", skipped: true };
1293
1293
  }
1294
1294
  }
1295
+ function isNumericSequence(arr) {
1296
+ return arr.every((item, i) => item === String(i + 1));
1297
+ }
1298
+ function parseSeqCommand(cmd) {
1299
+ const t = cmd.trim();
1300
+ const shorthand = t.match(/^seq\s+(\d+)$/);
1301
+ if (shorthand) return parseInt(shorthand[1], 10);
1302
+ const explicit = t.match(/^seq\s+1\s+(\d+)$/);
1303
+ if (explicit) return parseInt(explicit[1], 10);
1304
+ return null;
1305
+ }
1306
+ function extractCountFromName(name) {
1307
+ const m = name.match(/_of_(\d+)/);
1308
+ return m ? parseInt(m[1], 10) : null;
1309
+ }
1310
+ function normalizeWorkflow(workflow2) {
1311
+ const steps = workflow2.steps.map((step) => {
1312
+ if (Array.isArray(step.forEach) && isNumericSequence(step.forEach)) {
1313
+ const { forEach, ...rest } = step;
1314
+ return { ...rest, repeat: forEach.length };
1315
+ }
1316
+ if (typeof step.forEach === "string") {
1317
+ const n = parseSeqCommand(step.forEach);
1318
+ if (n !== null) {
1319
+ const { forEach, ...rest } = step;
1320
+ return { ...rest, repeat: n };
1321
+ }
1322
+ }
1323
+ const prompt = typeof step.prompt === "string" ? step.prompt : "";
1324
+ if (prompt.includes("{{item}}") && step.forEach === void 0 && step.repeat === void 0) {
1325
+ const n = extractCountFromName(step.name);
1326
+ if (n !== null) return { ...step, repeat: n };
1327
+ }
1328
+ return step;
1329
+ });
1330
+ return { ...workflow2, steps: collapseSequentialSteps(steps) };
1331
+ }
1332
+ function collapseSequentialSteps(steps) {
1333
+ const result = [];
1334
+ let i = 0;
1335
+ while (i < steps.length) {
1336
+ const step = steps[i];
1337
+ if (step.forEach !== void 0 || step.repeat !== void 0) {
1338
+ result.push(step);
1339
+ i++;
1340
+ continue;
1341
+ }
1342
+ const m = step.name.match(/^(.+?)_1$/);
1343
+ if (!m) {
1344
+ result.push(step);
1345
+ i++;
1346
+ continue;
1347
+ }
1348
+ const prefix = m[1];
1349
+ let n = 1;
1350
+ while (i + n < steps.length && steps[i + n].name === `${prefix}_${n + 1}`) n++;
1351
+ if (n < 2) {
1352
+ result.push(step);
1353
+ i++;
1354
+ continue;
1355
+ }
1356
+ const { name, ...rest } = step;
1357
+ result.push({ ...rest, name: `${prefix}_{{item}}`, repeat: n });
1358
+ i += n;
1359
+ }
1360
+ return result;
1361
+ }
1295
1362
  async function* streamPlan(args) {
1296
1363
  const { description, taskFile } = args;
1297
1364
  const skipResearch = args.fast || isSimpleRequest(description);
@@ -1356,7 +1423,7 @@ async function* streamPlan(args) {
1356
1423
  ${basePrompt}` : basePrompt,
1357
1424
  allowedTools: [],
1358
1425
  permissionMode: "bypassPermissions",
1359
- model: "opus",
1426
+ model: skipResearch ? "sonnet" : "opus",
1360
1427
  appendSystemPrompt: PLAN_SYSTEM_RULES,
1361
1428
  jsonSchema: WORKFLOW_JSON_SCHEMA
1362
1429
  };
@@ -1414,7 +1481,7 @@ ${issues}` };
1414
1481
  if (!judgeResult.pass) {
1415
1482
  yield { type: "plan:warn", message: `Judge rejected plan but retries exhausted: ${judgeResult.feedback}` };
1416
1483
  }
1417
- const { goal, vars, steps, ...rest } = zodResult.data;
1484
+ const { goal, vars, steps, ...rest } = normalizeWorkflow(zodResult.data);
1418
1485
  const ordered = { goal, ...vars && { vars }, steps, ...rest };
1419
1486
  const yamlContent = dumpYaml(ordered, {
1420
1487
  lineWidth: -1,
@@ -97,6 +97,11 @@ Before finalising your JSON, scan every `prompt` and `command` field you wrote.
97
97
  For each field, ask: "Does this contain a file path or directory path as a string literal?"
98
98
  If yes, extract it to `vars` and replace with `{{var_name}}`.
99
99
 
100
+ **Pre-Output Self-Review — Repeat (MANDATORY):**
101
+ Scan every `forEach` field you wrote.
102
+ Ask: "Is this array just sequential numbers like `["1","2","3"]` with no meaningful items?"
103
+ If yes, replace the entire `forEach` with `repeat: N` where N is the count. Sequential-number forEach arrays are ALWAYS wrong — they are a misuse of forEach and must be converted to `repeat: N`.
104
+
100
105
  ## When to Use Each Step Type
101
106
 
102
107
  **Use `prompt` steps (AI-assisted) for:**
@@ -59,6 +59,15 @@ Are all file paths declared in `vars`?
59
59
  - Do any `prompt` or `command` fields contain hardcoded file paths or directory paths?
60
60
  - Hardcoded paths in steps (not in `vars`) are a violation
61
61
 
62
+ ### 5. Repeat Misuse (if applicable)
63
+ If the user's goal mentions "N times", "repeat N", "N iterations", or "N passes":
64
+
65
+ - Does any step use `forEach` with a sequential numeric array like `["1","2","3","4","5"]`?
66
+ - This is always wrong — `repeat: N` must be used instead of a numeric forEach array
67
+ - Reject and require the offending step be converted to `repeat: N`
68
+ - Does any single step's `prompt` describe doing something "N times" or "across N passes" inline, instead of using `repeat: N`? A step that says "do this 10 times" or "perform N passes" inside its prompt text rather than setting `repeat: N` is wrong — reject it and require it to be restructured as a single-pass prompt with `repeat: N` on the step
69
+ - Are there N consecutive steps with names like `step_1`, `step_2`, `step_3`? Sequential named steps are always wrong when they do the same thing — reject and require a single step with `repeat: N`
70
+
62
71
  ## Output Format
63
72
 
64
73
  Respond with ONLY a JSON object in this exact shape:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "executant",
3
- "version": "1.4.2",
3
+ "version": "1.4.4",
4
4
  "description": "Harness for YAML-defined workflows that enables stepping through Claude sessions and bash commands",
5
5
  "repository": {
6
6
  "type": "git",