ralphctl 0.2.3 → 0.2.5

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 (32) hide show
  1. package/README.md +21 -9
  2. package/dist/{add-K7LNOYQ4.mjs → add-3T225IX5.mjs} +3 -3
  3. package/dist/{add-DWNLZQ7Q.mjs → add-6A5432U2.mjs} +4 -4
  4. package/dist/{chunk-QYF7QIZJ.mjs → chunk-742XQ7FL.mjs} +3 -3
  5. package/dist/{chunk-ORVGM6EV.mjs → chunk-CSICORGV.mjs} +583 -204
  6. package/dist/{chunk-V4ZUDZCG.mjs → chunk-DUU5346E.mjs} +1 -1
  7. package/dist/{chunk-7TBO6GOT.mjs → chunk-EUNAUHC3.mjs} +1 -1
  8. package/dist/{chunk-GLDPHKEW.mjs → chunk-IB6OCKZW.mjs} +15 -2
  9. package/dist/{chunk-ITRZMBLJ.mjs → chunk-JRFOUFD3.mjs} +1 -1
  10. package/dist/{chunk-LAERLCL5.mjs → chunk-UBPZHHCD.mjs} +2 -2
  11. package/dist/cli.mjs +29 -12
  12. package/dist/{create-5MILNF7E.mjs → create-MYGOWO2F.mjs} +3 -3
  13. package/dist/{handle-2BACSJLR.mjs → handle-TA4MYNQJ.mjs} +1 -1
  14. package/dist/{project-XC7AXA4B.mjs → project-YONEJICR.mjs} +2 -2
  15. package/dist/prompts/harness-context.md +5 -0
  16. package/dist/prompts/ideate-auto.md +34 -17
  17. package/dist/prompts/ideate.md +18 -2
  18. package/dist/prompts/plan-auto.md +7 -12
  19. package/dist/prompts/plan-common.md +18 -2
  20. package/dist/prompts/plan-interactive.md +8 -13
  21. package/dist/prompts/signals-evaluation.md +6 -0
  22. package/dist/prompts/signals-planning.md +5 -0
  23. package/dist/prompts/signals-task.md +7 -0
  24. package/dist/prompts/task-evaluation-resume.md +34 -0
  25. package/dist/prompts/task-evaluation.md +8 -0
  26. package/dist/prompts/task-execution.md +10 -19
  27. package/dist/prompts/validation-checklist.md +14 -0
  28. package/dist/{resolver-CFY6DIOP.mjs → resolver-RXEY6EJE.mjs} +2 -2
  29. package/dist/{sprint-F4VRAEWZ.mjs → sprint-FGLWYWKX.mjs} +2 -2
  30. package/dist/{wizard-RCQ4QQOL.mjs → wizard-XZ7OGBCJ.mjs} +6 -6
  31. package/package.json +1 -1
  32. package/schemas/tasks.schema.json +10 -1
@@ -11,7 +11,7 @@ import {
11
11
  getPendingRequirements,
12
12
  groupTicketsByProject,
13
13
  listTickets
14
- } from "./chunk-QYF7QIZJ.mjs";
14
+ } from "./chunk-742XQ7FL.mjs";
15
15
  import {
16
16
  EXIT_ALL_BLOCKED,
17
17
  EXIT_ERROR,
@@ -23,7 +23,7 @@ import {
23
23
  import {
24
24
  getProject,
25
25
  listProjects
26
- } from "./chunk-7TBO6GOT.mjs";
26
+ } from "./chunk-EUNAUHC3.mjs";
27
27
  import {
28
28
  activateSprint,
29
29
  assertSprintStatus,
@@ -40,7 +40,7 @@ import {
40
40
  setAiProvider,
41
41
  summarizeProgressForContext,
42
42
  withFileLock
43
- } from "./chunk-ITRZMBLJ.mjs";
43
+ } from "./chunk-JRFOUFD3.mjs";
44
44
  import {
45
45
  ensureError,
46
46
  unwrapOrThrow,
@@ -50,9 +50,11 @@ import {
50
50
  ImportTasksSchema,
51
51
  RefinedRequirementsSchema,
52
52
  TasksSchema,
53
+ appendToFile,
53
54
  assertSafeCwd,
54
55
  ensureDir,
55
56
  fileExists,
57
+ getEvaluationFilePath,
56
58
  getPlanningDir,
57
59
  getProgressFilePath,
58
60
  getRefinementDir,
@@ -61,7 +63,7 @@ import {
61
63
  getTasksFilePath,
62
64
  readValidatedJson,
63
65
  writeValidatedJson
64
- } from "./chunk-GLDPHKEW.mjs";
66
+ } from "./chunk-IB6OCKZW.mjs";
65
67
  import {
66
68
  DependencyCycleError,
67
69
  IOError,
@@ -123,37 +125,91 @@ var promptDir = getPromptDir();
123
125
  function loadTemplate(name) {
124
126
  return readFileSync(join(promptDir, `${name}.md`), "utf-8");
125
127
  }
126
- function buildPlanPrompt(template, context, schema) {
127
- const common = loadTemplate("plan-common");
128
- return template.replace("{{COMMON}}", common).replace("{{CONTEXT}}", context).replace("{{SCHEMA}}", schema);
128
+ function loadPartial(name) {
129
+ return loadTemplate(name).replace(/\s+$/, "");
129
130
  }
130
- function buildInteractivePrompt(context, outputFile, schema) {
131
- const template = loadTemplate("plan-interactive");
132
- return buildPlanPrompt(template, context, schema).replace("{{OUTPUT_FILE}}", outputFile);
131
+ var UNREPLACED_TOKEN_RE = /\{\{[A-Z_]+\}\}/g;
132
+ function composePrompt(template, substitutions) {
133
+ let result = template;
134
+ for (const [key, value] of Object.entries(substitutions)) {
135
+ result = result.replaceAll(`{{${key}}}`, value);
136
+ }
137
+ const remaining = result.match(UNREPLACED_TOKEN_RE);
138
+ if (remaining) {
139
+ throw new Error(`composePrompt: unreplaced placeholders: ${[...new Set(remaining)].join(", ")}`);
140
+ }
141
+ return result;
142
+ }
143
+ function buildPlanCommon(projectToolingSection) {
144
+ return composePrompt(loadPartial("plan-common"), {
145
+ PROJECT_TOOLING: projectToolingSection
146
+ });
147
+ }
148
+ function buildPlannerBase(projectToolingSection) {
149
+ return {
150
+ HARNESS_CONTEXT: loadPartial("harness-context"),
151
+ COMMON: buildPlanCommon(projectToolingSection),
152
+ VALIDATION: loadPartial("validation-checklist"),
153
+ SIGNALS: loadPartial("signals-planning")
154
+ };
155
+ }
156
+ function buildInteractivePrompt(context, outputFile, schema, projectToolingSection) {
157
+ return composePrompt(loadTemplate("plan-interactive"), {
158
+ ...buildPlannerBase(projectToolingSection),
159
+ CONTEXT: context,
160
+ OUTPUT_FILE: outputFile,
161
+ SCHEMA: schema
162
+ });
133
163
  }
134
- function buildAutoPrompt(context, schema) {
135
- const template = loadTemplate("plan-auto");
136
- return buildPlanPrompt(template, context, schema);
164
+ function buildAutoPrompt(context, schema, projectToolingSection) {
165
+ return composePrompt(loadTemplate("plan-auto"), {
166
+ ...buildPlannerBase(projectToolingSection),
167
+ CONTEXT: context,
168
+ SCHEMA: schema
169
+ });
137
170
  }
138
171
  function buildTaskExecutionPrompt(progressFilePath, noCommit, contextFileName) {
139
172
  const template = loadTemplate("task-execution");
140
- const commitStep = noCommit ? "" : "\n> **Before continuing:** Create a git commit with a descriptive message for the changes made.\n";
173
+ const commitStep = noCommit ? "" : "\n - **Before continuing:** Create a git commit with a descriptive message for the changes made.";
141
174
  const commitConstraint = noCommit ? "" : "- **Must commit** \u2014 Create a git commit before signaling completion.\n";
142
- return template.replaceAll("{{PROGRESS_FILE}}", progressFilePath).replaceAll("{{COMMIT_STEP}}", commitStep).replaceAll("{{COMMIT_CONSTRAINT}}", commitConstraint).replaceAll("{{CONTEXT_FILE}}", contextFileName);
175
+ return composePrompt(template, {
176
+ HARNESS_CONTEXT: loadPartial("harness-context"),
177
+ SIGNALS: loadPartial("signals-task"),
178
+ PROGRESS_FILE: progressFilePath,
179
+ COMMIT_STEP: commitStep,
180
+ COMMIT_CONSTRAINT: commitConstraint,
181
+ CONTEXT_FILE: contextFileName
182
+ });
143
183
  }
144
184
  function buildTicketRefinePrompt(ticketContent, outputFile, schema, issueContext = "") {
145
185
  const template = loadTemplate("ticket-refine");
146
- return template.replace("{{TICKET}}", ticketContent).replace("{{OUTPUT_FILE}}", outputFile).replace("{{SCHEMA}}", schema).replace("{{ISSUE_CONTEXT}}", issueContext);
186
+ return composePrompt(template, {
187
+ TICKET: ticketContent,
188
+ OUTPUT_FILE: outputFile,
189
+ SCHEMA: schema,
190
+ ISSUE_CONTEXT: issueContext
191
+ });
147
192
  }
148
- function buildIdeatePrompt(ideaTitle, ideaDescription, projectName, repositories, outputFile, schema) {
149
- const template = loadTemplate("ideate");
150
- const common = loadTemplate("plan-common");
151
- return template.replace("{{IDEA_TITLE}}", ideaTitle).replace("{{IDEA_DESCRIPTION}}", ideaDescription).replace("{{PROJECT_NAME}}", projectName).replace("{{REPOSITORIES}}", repositories).replace("{{OUTPUT_FILE}}", outputFile).replace("{{SCHEMA}}", schema).replace("{{COMMON}}", common);
193
+ function buildIdeatePrompt(ideaTitle, ideaDescription, projectName, repositories, outputFile, schema, projectToolingSection) {
194
+ return composePrompt(loadTemplate("ideate"), {
195
+ ...buildPlannerBase(projectToolingSection),
196
+ IDEA_TITLE: ideaTitle,
197
+ IDEA_DESCRIPTION: ideaDescription,
198
+ PROJECT_NAME: projectName,
199
+ REPOSITORIES: repositories,
200
+ OUTPUT_FILE: outputFile,
201
+ SCHEMA: schema
202
+ });
152
203
  }
153
- function buildIdeateAutoPrompt(ideaTitle, ideaDescription, projectName, repositories, schema) {
154
- const template = loadTemplate("ideate-auto");
155
- const common = loadTemplate("plan-common");
156
- return template.replace("{{IDEA_TITLE}}", ideaTitle).replace("{{IDEA_DESCRIPTION}}", ideaDescription).replace("{{PROJECT_NAME}}", projectName).replace("{{REPOSITORIES}}", repositories).replace("{{SCHEMA}}", schema).replace("{{COMMON}}", common);
204
+ function buildIdeateAutoPrompt(ideaTitle, ideaDescription, projectName, repositories, schema, projectToolingSection) {
205
+ return composePrompt(loadTemplate("ideate-auto"), {
206
+ ...buildPlannerBase(projectToolingSection),
207
+ IDEA_TITLE: ideaTitle,
208
+ IDEA_DESCRIPTION: ideaDescription,
209
+ PROJECT_NAME: projectName,
210
+ REPOSITORIES: repositories,
211
+ SCHEMA: schema
212
+ });
157
213
  }
158
214
  function buildEvaluatorPrompt(ctx) {
159
215
  const template = loadTemplate("task-evaluation");
@@ -168,7 +224,27 @@ ${ctx.verificationCriteria.map((c) => `- ${c}`).join("\n")}` : "";
168
224
  const checkSection = ctx.checkScriptSection ? `
169
225
 
170
226
  ${ctx.checkScriptSection}` : "";
171
- return template.replaceAll("{{TASK_NAME}}", ctx.taskName).replace("{{TASK_DESCRIPTION_SECTION}}", descriptionSection).replace("{{TASK_STEPS_SECTION}}", stepsSection).replace("{{VERIFICATION_CRITERIA_SECTION}}", criteriaSection).replace("{{PROJECT_PATH}}", ctx.projectPath).replace("{{CHECK_SCRIPT_SECTION}}", checkSection);
227
+ return composePrompt(template, {
228
+ HARNESS_CONTEXT: loadPartial("harness-context"),
229
+ SIGNALS: loadPartial("signals-evaluation"),
230
+ TASK_NAME: ctx.taskName,
231
+ TASK_DESCRIPTION_SECTION: descriptionSection,
232
+ TASK_STEPS_SECTION: stepsSection,
233
+ VERIFICATION_CRITERIA_SECTION: criteriaSection,
234
+ PROJECT_PATH: ctx.projectPath,
235
+ CHECK_SCRIPT_SECTION: checkSection,
236
+ PROJECT_TOOLING: ctx.projectToolingSection
237
+ });
238
+ }
239
+ function buildEvaluationResumePrompt(ctx) {
240
+ const template = loadTemplate("task-evaluation-resume");
241
+ const commitInstruction = ctx.needsCommit ? "\n - **Then commit the fix** with a descriptive message before signaling completion." : "";
242
+ return composePrompt(template, {
243
+ HARNESS_CONTEXT: loadPartial("harness-context"),
244
+ SIGNALS: loadPartial("signals-task"),
245
+ CRITIQUE: ctx.critique,
246
+ COMMIT_INSTRUCTION: commitInstruction
247
+ });
172
248
  }
173
249
 
174
250
  // src/utils/requirements-export.ts
@@ -1053,7 +1129,7 @@ ${text}`;
1053
1129
 
1054
1130
  // src/commands/sprint/plan.ts
1055
1131
  import { mkdir as mkdir2, readFile as readFile3, writeFile as writeFile3 } from "fs/promises";
1056
- import { join as join5 } from "path";
1132
+ import { join as join6 } from "path";
1057
1133
  import { confirm as confirm3 } from "@inquirer/prompts";
1058
1134
  import { Result as Result5 } from "typescript-result";
1059
1135
 
@@ -1163,6 +1239,12 @@ async function updateTask(taskId, updates, sprintId) {
1163
1239
  if (updates.evaluationOutput !== void 0) {
1164
1240
  task.evaluationOutput = updates.evaluationOutput;
1165
1241
  }
1242
+ if (updates.evaluationStatus !== void 0) {
1243
+ task.evaluationStatus = updates.evaluationStatus;
1244
+ }
1245
+ if (updates.evaluationFile !== void 0) {
1246
+ task.evaluationFile = updates.evaluationFile;
1247
+ }
1166
1248
  await saveTasks(tasks, id);
1167
1249
  return task;
1168
1250
  });
@@ -1349,6 +1431,172 @@ function validateImportTasks(importTasks2, existingTasks, ticketIds) {
1349
1431
  return errors;
1350
1432
  }
1351
1433
 
1434
+ // src/ai/project-tooling.ts
1435
+ import { existsSync as existsSync2, readdirSync, readFileSync as readFileSync2 } from "fs";
1436
+ import { join as join5 } from "path";
1437
+ var EMPTY_TOOLING = {
1438
+ agents: [],
1439
+ skills: [],
1440
+ mcpServers: [],
1441
+ hasClaudeMd: false,
1442
+ hasAgentsMd: false,
1443
+ hasCopilotInstructions: false
1444
+ };
1445
+ function safeListDir(path, predicate) {
1446
+ try {
1447
+ if (!existsSync2(path)) return [];
1448
+ return readdirSync(path).filter(predicate).sort();
1449
+ } catch {
1450
+ return [];
1451
+ }
1452
+ }
1453
+ var EVALUATOR_DENYLISTED_AGENTS = /* @__PURE__ */ new Set(["implementer", "planner"]);
1454
+ function detectAgents(projectPath) {
1455
+ const agentsDir = join5(projectPath, ".claude", "agents");
1456
+ return safeListDir(agentsDir, (name) => name.endsWith(".md")).map((name) => name.replace(/\.md$/, "")).filter((name) => !EVALUATOR_DENYLISTED_AGENTS.has(name));
1457
+ }
1458
+ function detectSkills(projectPath) {
1459
+ const skillsDir = join5(projectPath, ".claude", "skills");
1460
+ try {
1461
+ if (!existsSync2(skillsDir)) return [];
1462
+ return readdirSync(skillsDir, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => entry.name).sort();
1463
+ } catch {
1464
+ return [];
1465
+ }
1466
+ }
1467
+ function detectMcpServers(projectPath) {
1468
+ const mcpFile = join5(projectPath, ".mcp.json");
1469
+ if (!existsSync2(mcpFile)) return [];
1470
+ try {
1471
+ const raw = readFileSync2(mcpFile, "utf-8");
1472
+ const parsed = JSON.parse(raw);
1473
+ const servers = parsed.mcpServers;
1474
+ if (!servers || typeof servers !== "object") return [];
1475
+ return Object.keys(servers).sort();
1476
+ } catch {
1477
+ return [];
1478
+ }
1479
+ }
1480
+ function detectProjectTooling(projectPath) {
1481
+ if (!projectPath || !existsSync2(projectPath)) {
1482
+ return EMPTY_TOOLING;
1483
+ }
1484
+ return {
1485
+ agents: detectAgents(projectPath),
1486
+ skills: detectSkills(projectPath),
1487
+ mcpServers: detectMcpServers(projectPath),
1488
+ hasClaudeMd: existsSync2(join5(projectPath, "CLAUDE.md")),
1489
+ hasAgentsMd: existsSync2(join5(projectPath, "AGENTS.md")),
1490
+ hasCopilotInstructions: existsSync2(join5(projectPath, ".github", "copilot-instructions.md"))
1491
+ };
1492
+ }
1493
+ function detectProjectToolingAcrossPaths(projectPaths) {
1494
+ if (projectPaths.length === 0) {
1495
+ return EMPTY_TOOLING;
1496
+ }
1497
+ const agents = /* @__PURE__ */ new Set();
1498
+ const skills = /* @__PURE__ */ new Set();
1499
+ const mcpServers = /* @__PURE__ */ new Set();
1500
+ let hasClaudeMd = false;
1501
+ let hasAgentsMd = false;
1502
+ let hasCopilotInstructions = false;
1503
+ for (const path of projectPaths) {
1504
+ const tooling = detectProjectTooling(path);
1505
+ for (const agent of tooling.agents) agents.add(agent);
1506
+ for (const skill of tooling.skills) skills.add(skill);
1507
+ for (const server of tooling.mcpServers) mcpServers.add(server);
1508
+ hasClaudeMd = hasClaudeMd || tooling.hasClaudeMd;
1509
+ hasAgentsMd = hasAgentsMd || tooling.hasAgentsMd;
1510
+ hasCopilotInstructions = hasCopilotInstructions || tooling.hasCopilotInstructions;
1511
+ }
1512
+ return {
1513
+ agents: [...agents].sort(),
1514
+ skills: [...skills].sort(),
1515
+ mcpServers: [...mcpServers].sort(),
1516
+ hasClaudeMd,
1517
+ hasAgentsMd,
1518
+ hasCopilotInstructions
1519
+ };
1520
+ }
1521
+ function buildProjectToolingSection(paths) {
1522
+ const tooling = typeof paths === "string" ? detectProjectTooling(paths) : detectProjectToolingAcrossPaths([...paths]);
1523
+ return renderProjectToolingSection(tooling);
1524
+ }
1525
+ function renderProjectToolingSection(tooling) {
1526
+ const hasAny = tooling.agents.length > 0 || tooling.skills.length > 0 || tooling.mcpServers.length > 0 || tooling.hasClaudeMd || tooling.hasAgentsMd || tooling.hasCopilotInstructions;
1527
+ if (!hasAny) return "";
1528
+ const lines = [];
1529
+ lines.push("## Project Tooling (use these \u2014 they exist for a reason)");
1530
+ lines.push("");
1531
+ lines.push(
1532
+ "This project ships with tooling that you should prefer over generic approaches. Verification and evaluation must adapt to the project\u2019s actual stack and the agents, skills, and MCP servers it has installed."
1533
+ );
1534
+ lines.push("");
1535
+ if (tooling.agents.length > 0) {
1536
+ lines.push("### Subagents available");
1537
+ lines.push("");
1538
+ lines.push("Delegate via the Task tool with `subagent_type=<name>` when the diff matches a specialty:");
1539
+ for (const agent of tooling.agents) {
1540
+ const hint = describeAgentHint(agent);
1541
+ lines.push(`- \`${agent}\`${hint ? ` \u2014 ${hint}` : ""}`);
1542
+ }
1543
+ lines.push("");
1544
+ }
1545
+ if (tooling.skills.length > 0) {
1546
+ lines.push("### Skills available");
1547
+ lines.push("");
1548
+ lines.push("Invoke via the Skill tool when the skill name matches the work in front of you:");
1549
+ for (const skill of tooling.skills) {
1550
+ lines.push(`- \`${skill}\``);
1551
+ }
1552
+ lines.push("");
1553
+ }
1554
+ if (tooling.mcpServers.length > 0) {
1555
+ lines.push("### MCP servers available");
1556
+ lines.push("");
1557
+ lines.push(
1558
+ "These give you tools beyond the filesystem. Use them to **interact with the running system**, not just read its source."
1559
+ );
1560
+ for (const server of tooling.mcpServers) {
1561
+ const hint = describeMcpHint(server);
1562
+ lines.push(`- \`${server}\`${hint ? ` \u2014 ${hint}` : ""}`);
1563
+ }
1564
+ lines.push("");
1565
+ }
1566
+ const instructionFiles = [];
1567
+ if (tooling.hasClaudeMd) instructionFiles.push("`CLAUDE.md`");
1568
+ if (tooling.hasAgentsMd) instructionFiles.push("`AGENTS.md`");
1569
+ if (tooling.hasCopilotInstructions) instructionFiles.push("`.github/copilot-instructions.md`");
1570
+ if (instructionFiles.length > 0) {
1571
+ lines.push("### Project instructions");
1572
+ lines.push("");
1573
+ lines.push(
1574
+ `Read ${instructionFiles.join(" / ")} for project-specific verification commands, conventions, and constraints. If no check script is configured, derive verification commands from these files (e.g. \`package.json\` scripts referenced there).`
1575
+ );
1576
+ lines.push("");
1577
+ }
1578
+ return lines.join("\n");
1579
+ }
1580
+ function describeAgentHint(name) {
1581
+ const hints = {
1582
+ auditor: "use for security-sensitive diffs (auth, input handling, file IO, secrets)",
1583
+ reviewer: "use for general code-quality review of the diff",
1584
+ tester: "use to assess test coverage and quality of new tests",
1585
+ designer: "use for UI/UX/theming changes"
1586
+ };
1587
+ return hints[name] ?? null;
1588
+ }
1589
+ function describeMcpHint(name) {
1590
+ const lower = name.toLowerCase();
1591
+ if (lower.includes("playwright")) return "use for any UI/frontend task \u2014 click through the changed flow";
1592
+ if (lower.includes("puppeteer")) return "use for browser automation on UI changes";
1593
+ if (lower.includes("github")) return "use to inspect related PRs/issues for context";
1594
+ if (lower.includes("postgres") || lower.includes("mysql") || lower.includes("sqlite")) {
1595
+ return "use to verify database schema/migration changes against a real DB";
1596
+ }
1597
+ return null;
1598
+ }
1599
+
1352
1600
  // src/interactive/selectors.ts
1353
1601
  import { checkbox, confirm as confirm2, input } from "@inquirer/prompts";
1354
1602
  async function selectProject(message = "Select project:") {
@@ -1360,7 +1608,7 @@ async function selectProject(message = "Select project:") {
1360
1608
  default: true
1361
1609
  });
1362
1610
  if (create) {
1363
- const { projectAddCommand } = await import("./add-K7LNOYQ4.mjs");
1611
+ const { projectAddCommand } = await import("./add-3T225IX5.mjs");
1364
1612
  await projectAddCommand({ interactive: true });
1365
1613
  const updated = await listProjects();
1366
1614
  if (updated.length === 0) return null;
@@ -1433,7 +1681,7 @@ async function selectSprint(message = "Select sprint:", filter) {
1433
1681
  default: true
1434
1682
  });
1435
1683
  if (create) {
1436
- const { sprintCreateCommand } = await import("./create-5MILNF7E.mjs");
1684
+ const { sprintCreateCommand } = await import("./create-MYGOWO2F.mjs");
1437
1685
  await sprintCreateCommand({ interactive: true });
1438
1686
  const updated = await listSprints();
1439
1687
  const refiltered = filter ? updated.filter((s) => filter.includes(s.status)) : updated;
@@ -1468,7 +1716,7 @@ async function selectTicket(message = "Select ticket:", filter) {
1468
1716
  default: true
1469
1717
  });
1470
1718
  if (create) {
1471
- const { ticketAddCommand } = await import("./add-DWNLZQ7Q.mjs");
1719
+ const { ticketAddCommand } = await import("./add-6A5432U2.mjs");
1472
1720
  await ticketAddCommand({ interactive: true });
1473
1721
  const updated = await listTickets();
1474
1722
  const refiltered = filter ? updated.filter(filter) : updated;
@@ -1762,7 +2010,7 @@ async function getSprintContext(sprintName, ticketsByProject, existingTasks) {
1762
2010
  return lines.join("\n");
1763
2011
  }
1764
2012
  async function invokeAiInteractive(prompt, repoPaths, planDir) {
1765
- const contextFile = join5(planDir, "planning-context.md");
2013
+ const contextFile = join6(planDir, "planning-context.md");
1766
2014
  await writeFile3(contextFile, prompt, "utf-8");
1767
2015
  const provider = await getActiveProvider();
1768
2016
  const ticketCount = (prompt.match(/^####/gm) ?? []).length;
@@ -1930,8 +2178,9 @@ async function sprintPlanCommand(args) {
1930
2178
  const planDir = getPlanningDir(id);
1931
2179
  await mkdir2(planDir, { recursive: true });
1932
2180
  const ticketIds = new Set(sprint.tickets.map((t) => t.id));
2181
+ const projectToolingSection = buildProjectToolingSection(selectedPaths);
1933
2182
  if (options.auto) {
1934
- const prompt = buildAutoPrompt(context, schema);
2183
+ const prompt = buildAutoPrompt(context, schema, projectToolingSection);
1935
2184
  const spinner = createSpinner(`${providerName} is planning tasks...`);
1936
2185
  spinner.start();
1937
2186
  const outputR = await wrapAsync(() => invokeAiAuto(prompt, selectedPaths, planDir), ensureError);
@@ -1987,8 +2236,8 @@ async function sprintPlanCommand(args) {
1987
2236
  showSuccess(`Imported ${String(imported)}/${String(parsedTasks.length)} tasks.`);
1988
2237
  log.newline();
1989
2238
  } else {
1990
- const outputFile = join5(planDir, "tasks.json");
1991
- const prompt = buildInteractivePrompt(context, outputFile, schema);
2239
+ const outputFile = join6(planDir, "tasks.json");
2240
+ const prompt = buildInteractivePrompt(context, outputFile, schema, projectToolingSection);
1992
2241
  showInfo(`Starting interactive ${providerName} session...`);
1993
2242
  console.log(
1994
2243
  muted(
@@ -2059,15 +2308,165 @@ async function sprintPlanCommand(args) {
2059
2308
  }
2060
2309
 
2061
2310
  // src/commands/sprint/start.ts
2062
- import { Result as Result9 } from "typescript-result";
2311
+ import { Result as Result10 } from "typescript-result";
2063
2312
 
2064
2313
  // src/ai/runner.ts
2065
2314
  import { confirm as confirm5, input as input2, select as select2 } from "@inquirer/prompts";
2066
- import { Result as Result8 } from "typescript-result";
2315
+ import { Result as Result9 } from "typescript-result";
2067
2316
 
2068
2317
  // src/ai/executor.ts
2069
2318
  import { confirm as confirm4 } from "@inquirer/prompts";
2070
2319
  import { readFile as readFile4, unlink as unlink2 } from "fs/promises";
2320
+ import { Result as Result8 } from "typescript-result";
2321
+
2322
+ // src/utils/git.ts
2323
+ import { spawnSync as spawnSync2 } from "child_process";
2324
+ var BRANCH_NAME_RE = /^[a-zA-Z0-9/_.-]+$/;
2325
+ var BRANCH_NAME_INVALID_PATTERNS = [/\.\./, /\.$/, /\/$/, /\.lock$/, /^-/, /\/\//];
2326
+ function isValidBranchName(name) {
2327
+ if (!name || name.length > 250) return false;
2328
+ if (!BRANCH_NAME_RE.test(name)) return false;
2329
+ for (const pattern of BRANCH_NAME_INVALID_PATTERNS) {
2330
+ if (pattern.test(name)) return false;
2331
+ }
2332
+ return true;
2333
+ }
2334
+ function getCurrentBranch(cwd) {
2335
+ assertSafeCwd(cwd);
2336
+ const result = spawnSync2("git", ["rev-parse", "--abbrev-ref", "HEAD"], {
2337
+ cwd,
2338
+ encoding: "utf-8",
2339
+ stdio: ["pipe", "pipe", "pipe"]
2340
+ });
2341
+ if (result.status !== 0) {
2342
+ throw new Error(`Failed to get current branch in ${cwd}: ${result.stderr.trim()}`);
2343
+ }
2344
+ return result.stdout.trim();
2345
+ }
2346
+ function branchExists(cwd, name) {
2347
+ assertSafeCwd(cwd);
2348
+ if (!isValidBranchName(name)) {
2349
+ throw new Error(`Invalid branch name: ${name}`);
2350
+ }
2351
+ const result = spawnSync2("git", ["show-ref", "--verify", `refs/heads/${name}`], {
2352
+ cwd,
2353
+ encoding: "utf-8",
2354
+ stdio: ["pipe", "pipe", "pipe"]
2355
+ });
2356
+ return result.status === 0;
2357
+ }
2358
+ function createAndCheckoutBranch(cwd, name) {
2359
+ assertSafeCwd(cwd);
2360
+ if (!isValidBranchName(name)) {
2361
+ throw new Error(`Invalid branch name: ${name}`);
2362
+ }
2363
+ const current = getCurrentBranch(cwd);
2364
+ if (current === name) {
2365
+ return;
2366
+ }
2367
+ if (branchExists(cwd, name)) {
2368
+ const result = spawnSync2("git", ["checkout", name], {
2369
+ cwd,
2370
+ encoding: "utf-8",
2371
+ stdio: ["pipe", "pipe", "pipe"]
2372
+ });
2373
+ if (result.status !== 0) {
2374
+ throw new Error(`Failed to checkout branch '${name}' in ${cwd}: ${result.stderr.trim()}`);
2375
+ }
2376
+ } else {
2377
+ const result = spawnSync2("git", ["checkout", "-b", name], {
2378
+ cwd,
2379
+ encoding: "utf-8",
2380
+ stdio: ["pipe", "pipe", "pipe"]
2381
+ });
2382
+ if (result.status !== 0) {
2383
+ throw new Error(`Failed to create branch '${name}' in ${cwd}: ${result.stderr.trim()}`);
2384
+ }
2385
+ }
2386
+ }
2387
+ function verifyCurrentBranch(cwd, expected) {
2388
+ const current = getCurrentBranch(cwd);
2389
+ return current === expected;
2390
+ }
2391
+ function getDefaultBranch(cwd) {
2392
+ assertSafeCwd(cwd);
2393
+ const result = spawnSync2("git", ["symbolic-ref", "refs/remotes/origin/HEAD"], {
2394
+ cwd,
2395
+ encoding: "utf-8",
2396
+ stdio: ["pipe", "pipe", "pipe"]
2397
+ });
2398
+ if (result.status === 0) {
2399
+ const ref = result.stdout.trim();
2400
+ const parts = ref.split("/");
2401
+ return parts[parts.length - 1] ?? "main";
2402
+ }
2403
+ const stderr = result.stderr.trim();
2404
+ if (stderr.includes("is not a symbolic ref") || stderr.includes("No such ref")) {
2405
+ if (branchExists(cwd, "main")) return "main";
2406
+ if (branchExists(cwd, "master")) return "master";
2407
+ return "main";
2408
+ }
2409
+ throw new Error(`Failed to detect default branch in ${cwd}: ${stderr}`);
2410
+ }
2411
+ function getHeadSha(cwd) {
2412
+ try {
2413
+ assertSafeCwd(cwd);
2414
+ const result = spawnSync2("git", ["rev-parse", "HEAD"], {
2415
+ cwd,
2416
+ encoding: "utf-8",
2417
+ stdio: ["pipe", "pipe", "pipe"]
2418
+ });
2419
+ if (result.status !== 0) return null;
2420
+ return result.stdout.trim() || null;
2421
+ } catch {
2422
+ return null;
2423
+ }
2424
+ }
2425
+ function hasUncommittedChanges(cwd) {
2426
+ assertSafeCwd(cwd);
2427
+ const result = spawnSync2("git", ["status", "--porcelain"], {
2428
+ cwd,
2429
+ encoding: "utf-8",
2430
+ stdio: ["pipe", "pipe", "pipe"]
2431
+ });
2432
+ if (result.status !== 0) {
2433
+ throw new Error(`Failed to check git status in ${cwd}: ${result.stderr.trim()}`);
2434
+ }
2435
+ return result.stdout.trim().length > 0;
2436
+ }
2437
+ function generateBranchName(sprintId) {
2438
+ return `ralphctl/${sprintId}`;
2439
+ }
2440
+ function isGhAvailable() {
2441
+ const result = spawnSync2("gh", ["--version"], {
2442
+ encoding: "utf-8",
2443
+ stdio: ["pipe", "pipe", "pipe"]
2444
+ });
2445
+ return result.status === 0;
2446
+ }
2447
+ function isGlabAvailable() {
2448
+ const result = spawnSync2("glab", ["--version"], {
2449
+ encoding: "utf-8",
2450
+ stdio: ["pipe", "pipe", "pipe"]
2451
+ });
2452
+ return result.status === 0;
2453
+ }
2454
+
2455
+ // src/store/evaluation.ts
2456
+ async function writeEvaluation(sprintId, taskId, iteration, status, body) {
2457
+ const filePath = getEvaluationFilePath(sprintId, taskId);
2458
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString();
2459
+ const header = `## ${timestamp} \u2014 Iteration ${String(iteration)} \u2014 ${status.toUpperCase()}
2460
+
2461
+ `;
2462
+ const entry = `${header}${body.trimEnd()}
2463
+
2464
+ ---
2465
+
2466
+ `;
2467
+ unwrapOrThrow(await appendToFile(filePath, entry));
2468
+ return filePath;
2469
+ }
2071
2470
 
2072
2471
  // src/ai/parser.ts
2073
2472
  function parseExecutionResult(output) {
@@ -2164,12 +2563,12 @@ var RateLimitCoordinator = class {
2164
2563
  // src/ai/task-context.ts
2165
2564
  import { execSync } from "child_process";
2166
2565
  import { writeFile as writeFile4 } from "fs/promises";
2167
- import { join as join7 } from "path";
2566
+ import { join as join8 } from "path";
2168
2567
  import { Result as Result7 } from "typescript-result";
2169
2568
 
2170
2569
  // src/ai/permissions.ts
2171
- import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
2172
- import { join as join6 } from "path";
2570
+ import { existsSync as existsSync3, readFileSync as readFileSync3 } from "fs";
2571
+ import { join as join7 } from "path";
2173
2572
  import { homedir } from "os";
2174
2573
  import { Result as Result6 } from "typescript-result";
2175
2574
  function getProviderPermissions(projectPath, provider) {
@@ -2180,10 +2579,10 @@ function getProviderPermissions(projectPath, provider) {
2180
2579
  if (provider === "copilot") {
2181
2580
  return permissions;
2182
2581
  }
2183
- const projectSettingsPath = join6(projectPath, ".claude", "settings.local.json");
2184
- if (existsSync2(projectSettingsPath)) {
2582
+ const projectSettingsPath = join7(projectPath, ".claude", "settings.local.json");
2583
+ if (existsSync3(projectSettingsPath)) {
2185
2584
  const projectResult = Result6.try(() => {
2186
- const content = readFileSync2(projectSettingsPath, "utf-8");
2585
+ const content = readFileSync3(projectSettingsPath, "utf-8");
2187
2586
  return JSON.parse(content);
2188
2587
  });
2189
2588
  if (projectResult.ok) {
@@ -2196,10 +2595,10 @@ function getProviderPermissions(projectPath, provider) {
2196
2595
  }
2197
2596
  }
2198
2597
  }
2199
- const userSettingsPath = join6(homedir(), ".claude", "settings.json");
2200
- if (existsSync2(userSettingsPath)) {
2598
+ const userSettingsPath = join7(homedir(), ".claude", "settings.json");
2599
+ if (existsSync3(userSettingsPath)) {
2201
2600
  const userResult = Result6.try(() => {
2202
- const content = readFileSync2(userSettingsPath, "utf-8");
2601
+ const content = readFileSync3(userSettingsPath, "utf-8");
2203
2602
  return JSON.parse(content);
2204
2603
  });
2205
2604
  if (userResult.ok) {
@@ -2413,7 +2812,7 @@ function getContextFileName(sprintId, taskId) {
2413
2812
  return `.ralphctl-sprint-${sprintId}-task-${taskId}-context.md`;
2414
2813
  }
2415
2814
  async function writeTaskContextFile(projectPath, taskContent, instructions, sprintId, taskId) {
2416
- const contextFile = join7(projectPath, getContextFileName(sprintId, taskId));
2815
+ const contextFile = join8(projectPath, getContextFileName(sprintId, taskId));
2417
2816
  const warning2 = `<!-- TEMPORARY FILE - DO NOT COMMIT -->
2418
2817
  <!-- This file is auto-generated by ralphctl for task execution context -->
2419
2818
  <!-- It will be automatically cleaned up after task completion -->
@@ -2455,7 +2854,7 @@ function runPermissionCheck(ctx, noCommit, provider) {
2455
2854
  }
2456
2855
 
2457
2856
  // src/ai/lifecycle.ts
2458
- import { spawnSync as spawnSync2 } from "child_process";
2857
+ import { spawnSync as spawnSync3 } from "child_process";
2459
2858
  var DEFAULT_HOOK_TIMEOUT_MS = 5 * 60 * 1e3;
2460
2859
  function getHookTimeoutMs() {
2461
2860
  const envVal = process.env["RALPHCTL_SETUP_TIMEOUT_MS"];
@@ -2468,7 +2867,7 @@ function getHookTimeoutMs() {
2468
2867
  function runLifecycleHook(projectPath, script, event, timeoutOverrideMs) {
2469
2868
  assertSafeCwd(projectPath);
2470
2869
  const timeoutMs = timeoutOverrideMs ?? getHookTimeoutMs();
2471
- const result = spawnSync2(script, {
2870
+ const result = spawnSync3(script, {
2472
2871
  cwd: projectPath,
2473
2872
  shell: true,
2474
2873
  stdio: ["pipe", "pipe", "pipe"],
@@ -2481,6 +2880,7 @@ function runLifecycleHook(projectPath, script, event, timeoutOverrideMs) {
2481
2880
  }
2482
2881
 
2483
2882
  // src/ai/evaluator.ts
2883
+ var EVALUATOR_MAX_TURNS = 100;
2484
2884
  function getEvaluatorModel(generatorModel, provider) {
2485
2885
  if (provider.name !== "claude" || !generatorModel) return null;
2486
2886
  const modelLower = generatorModel.toLowerCase();
@@ -2512,13 +2912,16 @@ function parseDimensionScores(output) {
2512
2912
  function parseEvaluationResult(output) {
2513
2913
  const dimensions = parseDimensionScores(output);
2514
2914
  if (output.includes("<evaluation-passed>")) {
2515
- return { passed: true, output, dimensions };
2915
+ return { passed: true, status: "passed", output, dimensions };
2516
2916
  }
2517
2917
  const failedMatch = /<evaluation-failed>([\s\S]*?)<\/evaluation-failed>/.exec(output);
2518
2918
  if (failedMatch) {
2519
- return { passed: false, output: failedMatch[1]?.trim() ?? output, dimensions };
2919
+ return { passed: false, status: "failed", output: failedMatch[1]?.trim() ?? output, dimensions };
2920
+ }
2921
+ if (dimensions.length > 0) {
2922
+ return { passed: false, status: "failed", output, dimensions };
2520
2923
  }
2521
- return { passed: false, output, dimensions };
2924
+ return { passed: false, status: "malformed", output, dimensions };
2522
2925
  }
2523
2926
  function buildEvaluatorContext(task, checkScript) {
2524
2927
  const checkScriptSection = checkScript ? `## Check Script (Computational Gate)
@@ -2530,31 +2933,41 @@ ${checkScript}
2530
2933
  \`\`\`
2531
2934
 
2532
2935
  If this script fails, the implementation fails regardless of code quality. Record the full output.` : null;
2936
+ const projectToolingSection = buildProjectToolingSection(task.projectPath);
2533
2937
  return {
2534
2938
  taskName: task.name,
2535
2939
  taskDescription: task.description ?? "",
2536
2940
  taskSteps: task.steps,
2537
2941
  verificationCriteria: task.verificationCriteria,
2538
2942
  projectPath: task.projectPath,
2539
- checkScriptSection
2943
+ checkScriptSection,
2944
+ projectToolingSection
2540
2945
  };
2541
2946
  }
2542
- async function runEvaluation(task, generatorModel, checkScript, sprintId, provider) {
2947
+ async function runEvaluation(task, generatorModel, checkScript, sprintId, provider, options) {
2543
2948
  const p = provider ?? await getActiveProvider();
2544
2949
  const evaluatorModel = getEvaluatorModel(generatorModel, p);
2545
2950
  const sprintDir = getSprintDir(sprintId);
2546
2951
  const ctx = buildEvaluatorContext(task, checkScript);
2547
2952
  const prompt = buildEvaluatorPrompt(ctx);
2548
2953
  const providerArgs = ["--add-dir", sprintDir];
2549
- if (evaluatorModel && p.name === "claude") {
2550
- providerArgs.push("--model", evaluatorModel);
2551
- }
2552
- const result = await spawnWithRetry({
2553
- cwd: task.projectPath,
2554
- args: providerArgs,
2555
- prompt,
2556
- env: p.getSpawnEnv()
2557
- });
2954
+ if (p.name === "claude") {
2955
+ if (evaluatorModel) {
2956
+ providerArgs.push("--model", evaluatorModel);
2957
+ }
2958
+ providerArgs.push("--max-turns", String(EVALUATOR_MAX_TURNS));
2959
+ }
2960
+ await options?.coordinator?.waitIfPaused();
2961
+ const result = await spawnWithRetry(
2962
+ {
2963
+ cwd: task.projectPath,
2964
+ args: providerArgs,
2965
+ prompt,
2966
+ env: p.getSpawnEnv()
2967
+ },
2968
+ { maxRetries: options?.maxRetries },
2969
+ p
2970
+ );
2558
2971
  return parseEvaluationResult(result.stdout);
2559
2972
  }
2560
2973
 
@@ -2701,6 +3114,31 @@ async function executeTask(ctx, options, sprintId, resumeSessionId, provider, ch
2701
3114
  return { ...parsed, sessionId: spawnResult.sessionId, model: spawnResult.model };
2702
3115
  }
2703
3116
  var MAX_EVAL_OUTPUT = 2e3;
3117
+ var EVAL_SPAWN_FAILURE_PREFIX = "Evaluator spawn failed:";
3118
+ function isEvalSpawnFailure(output) {
3119
+ return output.startsWith(EVAL_SPAWN_FAILURE_PREFIX);
3120
+ }
3121
+ async function runEvaluationSafely(task, generatorModel, checkScript, sprintId, provider, options, coordinator) {
3122
+ const evalR = await wrapAsync(
3123
+ () => runEvaluation(task, generatorModel, checkScript, sprintId, provider, {
3124
+ coordinator,
3125
+ maxRetries: options.maxRetries
3126
+ }),
3127
+ ensureError
3128
+ );
3129
+ if (evalR.ok) return evalR.value;
3130
+ const err = evalR.error;
3131
+ if (err instanceof SpawnError && err.rateLimited && coordinator) {
3132
+ coordinator.pause(err.retryAfterMs ?? 6e4);
3133
+ }
3134
+ console.log(warning(`Evaluator spawn failed for ${task.name}: ${err.message} \u2014 marking malformed`));
3135
+ return {
3136
+ passed: false,
3137
+ status: "malformed",
3138
+ output: `${EVAL_SPAWN_FAILURE_PREFIX} ${err.message}`,
3139
+ dimensions: []
3140
+ };
3141
+ }
2704
3142
  async function runEvaluationLoop(params) {
2705
3143
  const {
2706
3144
  task,
@@ -2711,30 +3149,37 @@ async function runEvaluationLoop(params) {
2711
3149
  options,
2712
3150
  evalIterations,
2713
3151
  checkTimeout,
2714
- useSpinner = false
3152
+ useSpinner = false,
3153
+ coordinator
2715
3154
  } = params;
2716
3155
  const evalCheckScript = getEffectiveCheckScript(project, task.projectPath);
2717
3156
  const sprintDir = getSprintDir(sprintId);
2718
- let evalResult = await runEvaluation(task, result.model, evalCheckScript, sprintId, provider);
3157
+ let evalResult = await runEvaluationSafely(
3158
+ task,
3159
+ result.model,
3160
+ evalCheckScript,
3161
+ sprintId,
3162
+ provider,
3163
+ options,
3164
+ coordinator
3165
+ );
3166
+ let evaluationFile = await tryWriteEvaluationEntry(sprintId, task, 1, evalResult);
2719
3167
  let currentSessionId = result.sessionId;
2720
3168
  let currentModel = result.model;
2721
- for (let i = 0; i < evalIterations && !evalResult.passed; i++) {
2722
- console.log(warning(`Evaluation failed for ${task.name} (iteration ${String(i + 1)}/${String(evalIterations)})`));
3169
+ for (let i = 0; i < evalIterations && !evalResult.passed && evalResult.status !== "malformed"; i++) {
3170
+ console.log(warning(`Evaluation failed for ${task.name} \u2014 fix attempt ${String(i + 1)}/${String(evalIterations)}`));
2723
3171
  console.log(muted(evalResult.output.slice(0, 500)));
3172
+ const headBefore = getHeadSha(task.projectPath);
3173
+ const resumePrompt = buildEvaluationResumePrompt({
3174
+ critique: evalResult.output,
3175
+ needsCommit: !options.noCommit
3176
+ });
2724
3177
  const resumeSpinner = useSpinner ? createSpinner(`Fixing evaluation issues: ${task.name}`).start() : null;
2725
3178
  const resumeResult = await spawnWithRetry(
2726
3179
  {
2727
3180
  cwd: task.projectPath,
2728
3181
  args: ["--add-dir", sprintDir, ...buildProviderArgs(options, provider)],
2729
- prompt: `The evaluator found issues with your implementation:
2730
-
2731
- ${evalResult.output}
2732
-
2733
- Review the critique carefully. Fix each identified issue in the code, then:
2734
- 1. Re-run verification commands to confirm the fix
2735
- ${options.noCommit ? "" : "2. Commit the fix with a descriptive message\n"}${options.noCommit ? "2" : "3"}. Signal completion with <task-verified> and <task-complete>
2736
-
2737
- If the critique is about something outside your task scope, fix only what is within scope and signal completion.`,
3182
+ prompt: resumePrompt,
2738
3183
  resumeSessionId: currentSessionId ?? void 0,
2739
3184
  env: provider.getSpawnEnv()
2740
3185
  },
@@ -2753,35 +3198,84 @@ If the critique is about something outside your task scope, fix only what is wit
2753
3198
  if (resumeResult.model) currentModel = resumeResult.model;
2754
3199
  const fixResult = parseExecutionResult(resumeResult.stdout);
2755
3200
  if (!fixResult.success) {
2756
- console.log(warning(`Generator could not fix issues after feedback: ${task.name}`));
3201
+ const reason = `Generator could not fix issues after feedback (no <task-complete> signal)`;
3202
+ console.log(warning(`${reason}: ${task.name}`));
3203
+ const stubPath = await tryWriteEvaluationStub(sprintId, task, i + 2, reason);
3204
+ if (stubPath) evaluationFile = stubPath;
3205
+ break;
3206
+ }
3207
+ const headAfter = getHeadSha(task.projectPath);
3208
+ const dirtyR = Result8.try(() => hasUncommittedChanges(task.projectPath));
3209
+ const dirty = dirtyR.ok ? dirtyR.value : false;
3210
+ if (headBefore !== null && headAfter === headBefore && !dirty) {
3211
+ const reason = "Generator no-op (HEAD unchanged, no uncommitted changes)";
3212
+ console.log(warning(`${reason}: ${task.name}`));
3213
+ const stubPath = await tryWriteEvaluationStub(sprintId, task, i + 2, reason);
3214
+ if (stubPath) evaluationFile = stubPath;
2757
3215
  break;
2758
3216
  }
2759
3217
  const recheckScript = getEffectiveCheckScript(project, task.projectPath);
2760
3218
  if (recheckScript) {
2761
3219
  const recheckResult = runLifecycleHook(task.projectPath, recheckScript, "taskComplete", checkTimeout);
2762
3220
  if (!recheckResult.passed) {
3221
+ const reason = `Post-task check failed after generator fix: ${recheckResult.output.slice(0, 200)}`;
2763
3222
  console.log(warning(`Post-task check failed after generator fix: ${task.name}`));
3223
+ const stubPath = await tryWriteEvaluationStub(sprintId, task, i + 2, reason);
3224
+ if (stubPath) evaluationFile = stubPath;
2764
3225
  break;
2765
3226
  }
2766
3227
  }
2767
- evalResult = await runEvaluation(task, currentModel, evalCheckScript, sprintId, provider);
3228
+ evalResult = await runEvaluationSafely(
3229
+ task,
3230
+ currentModel,
3231
+ evalCheckScript,
3232
+ sprintId,
3233
+ provider,
3234
+ options,
3235
+ coordinator
3236
+ );
3237
+ const entryPath = await tryWriteEvaluationEntry(sprintId, task, i + 2, evalResult);
3238
+ if (entryPath) evaluationFile = entryPath;
2768
3239
  }
2769
3240
  await updateTask(
2770
3241
  task.id,
2771
3242
  {
2772
3243
  evaluated: true,
2773
- evaluationOutput: evalResult.output.slice(0, MAX_EVAL_OUTPUT)
3244
+ evaluationStatus: evalResult.status,
3245
+ evaluationOutput: evalResult.output.slice(0, MAX_EVAL_OUTPUT),
3246
+ ...evaluationFile ? { evaluationFile } : {}
2774
3247
  },
2775
3248
  sprintId
2776
3249
  );
2777
- if (!evalResult.passed) {
3250
+ if (evalResult.status === "malformed") {
3251
+ const cause = isEvalSpawnFailure(evalResult.output) ? evalResult.output : "no signal, no dimensions";
3252
+ console.log(warning(`Evaluator output was malformed for ${task.name} (${cause}) \u2014 marking done`));
3253
+ } else if (!evalResult.passed) {
2778
3254
  console.log(
2779
- warning(`Evaluation did not pass after ${String(evalIterations)} iteration(s) \u2014 marking done: ${task.name}`)
3255
+ warning(`Evaluation did not pass after ${String(evalIterations)} fix attempt(s) \u2014 marking done: ${task.name}`)
2780
3256
  );
2781
3257
  } else {
2782
3258
  console.log(success(`Evaluation passed: ${task.name}`));
2783
3259
  }
2784
3260
  }
3261
+ async function tryWriteEvaluationEntry(sprintId, task, iteration, evalResult) {
3262
+ let body;
3263
+ if (evalResult.status === "malformed") {
3264
+ body = isEvalSpawnFailure(evalResult.output) ? evalResult.output : "_(evaluator output had no parseable signal \u2014 see executor stdout)_";
3265
+ } else {
3266
+ body = evalResult.output;
3267
+ }
3268
+ return tryWriteEvaluationRaw(sprintId, task, iteration, evalResult.status, body);
3269
+ }
3270
+ async function tryWriteEvaluationStub(sprintId, task, iteration, reason) {
3271
+ return tryWriteEvaluationRaw(sprintId, task, iteration, "failed", `_(no re-evaluation: ${reason})_`);
3272
+ }
3273
+ async function tryWriteEvaluationRaw(sprintId, task, iteration, status, body) {
3274
+ const writeR = await wrapAsync(() => writeEvaluation(sprintId, task.id, iteration, status, body), ensureError);
3275
+ if (writeR.ok) return writeR.value;
3276
+ console.log(warning(`Could not persist evaluation sidecar for ${task.name}: ${writeR.error.message}`));
3277
+ return null;
3278
+ }
2785
3279
  async function areAllRemainingBlocked(sprintId) {
2786
3280
  const remaining = await getRemainingTasks(sprintId);
2787
3281
  if (remaining.length === 0) return false;
@@ -2926,9 +3420,10 @@ Starting ${label} in ${task.projectPath} (session)...
2926
3420
  console.log(success("Verification: passed"));
2927
3421
  }
2928
3422
  const checkScript = getEffectiveCheckScript(project, task.projectPath);
3423
+ const sequentialRepo = project?.repositories.find((r) => r.path === task.projectPath);
2929
3424
  if (checkScript) {
2930
3425
  console.log(muted(`Running post-task check: ${checkScript}`));
2931
- const hookResult = runLifecycleHook(task.projectPath, checkScript, "taskComplete");
3426
+ const hookResult = runLifecycleHook(task.projectPath, checkScript, "taskComplete", sequentialRepo?.checkTimeout);
2932
3427
  if (!hookResult.passed) {
2933
3428
  console.log(warning(`
2934
3429
  Post-task check failed for: ${task.name}`));
@@ -2956,6 +3451,7 @@ Post-task check failed for: ${task.name}`));
2956
3451
  provider,
2957
3452
  options,
2958
3453
  evalIterations,
3454
+ checkTimeout: sequentialRepo?.checkTimeout,
2959
3455
  useSpinner: true
2960
3456
  });
2961
3457
  }
@@ -3289,7 +3785,8 @@ Post-task check failed for: ${settled.task.name}`));
3289
3785
  provider,
3290
3786
  options,
3291
3787
  evalIterations,
3292
- checkTimeout: taskRepo?.checkTimeout
3788
+ checkTimeout: taskRepo?.checkTimeout,
3789
+ coordinator
3293
3790
  });
3294
3791
  }
3295
3792
  await updateTaskStatus(settled.task.id, "done", sprintId);
@@ -3368,125 +3865,6 @@ Waiting for ${String(running.size)} remaining task(s)...`));
3368
3865
  };
3369
3866
  }
3370
3867
 
3371
- // src/utils/git.ts
3372
- import { spawnSync as spawnSync3 } from "child_process";
3373
- var BRANCH_NAME_RE = /^[a-zA-Z0-9/_.-]+$/;
3374
- var BRANCH_NAME_INVALID_PATTERNS = [/\.\./, /\.$/, /\/$/, /\.lock$/, /^-/, /\/\//];
3375
- function isValidBranchName(name) {
3376
- if (!name || name.length > 250) return false;
3377
- if (!BRANCH_NAME_RE.test(name)) return false;
3378
- for (const pattern of BRANCH_NAME_INVALID_PATTERNS) {
3379
- if (pattern.test(name)) return false;
3380
- }
3381
- return true;
3382
- }
3383
- function getCurrentBranch(cwd) {
3384
- assertSafeCwd(cwd);
3385
- const result = spawnSync3("git", ["rev-parse", "--abbrev-ref", "HEAD"], {
3386
- cwd,
3387
- encoding: "utf-8",
3388
- stdio: ["pipe", "pipe", "pipe"]
3389
- });
3390
- if (result.status !== 0) {
3391
- throw new Error(`Failed to get current branch in ${cwd}: ${result.stderr.trim()}`);
3392
- }
3393
- return result.stdout.trim();
3394
- }
3395
- function branchExists(cwd, name) {
3396
- assertSafeCwd(cwd);
3397
- if (!isValidBranchName(name)) {
3398
- throw new Error(`Invalid branch name: ${name}`);
3399
- }
3400
- const result = spawnSync3("git", ["show-ref", "--verify", `refs/heads/${name}`], {
3401
- cwd,
3402
- encoding: "utf-8",
3403
- stdio: ["pipe", "pipe", "pipe"]
3404
- });
3405
- return result.status === 0;
3406
- }
3407
- function createAndCheckoutBranch(cwd, name) {
3408
- assertSafeCwd(cwd);
3409
- if (!isValidBranchName(name)) {
3410
- throw new Error(`Invalid branch name: ${name}`);
3411
- }
3412
- const current = getCurrentBranch(cwd);
3413
- if (current === name) {
3414
- return;
3415
- }
3416
- if (branchExists(cwd, name)) {
3417
- const result = spawnSync3("git", ["checkout", name], {
3418
- cwd,
3419
- encoding: "utf-8",
3420
- stdio: ["pipe", "pipe", "pipe"]
3421
- });
3422
- if (result.status !== 0) {
3423
- throw new Error(`Failed to checkout branch '${name}' in ${cwd}: ${result.stderr.trim()}`);
3424
- }
3425
- } else {
3426
- const result = spawnSync3("git", ["checkout", "-b", name], {
3427
- cwd,
3428
- encoding: "utf-8",
3429
- stdio: ["pipe", "pipe", "pipe"]
3430
- });
3431
- if (result.status !== 0) {
3432
- throw new Error(`Failed to create branch '${name}' in ${cwd}: ${result.stderr.trim()}`);
3433
- }
3434
- }
3435
- }
3436
- function verifyCurrentBranch(cwd, expected) {
3437
- const current = getCurrentBranch(cwd);
3438
- return current === expected;
3439
- }
3440
- function getDefaultBranch(cwd) {
3441
- assertSafeCwd(cwd);
3442
- const result = spawnSync3("git", ["symbolic-ref", "refs/remotes/origin/HEAD"], {
3443
- cwd,
3444
- encoding: "utf-8",
3445
- stdio: ["pipe", "pipe", "pipe"]
3446
- });
3447
- if (result.status === 0) {
3448
- const ref = result.stdout.trim();
3449
- const parts = ref.split("/");
3450
- return parts[parts.length - 1] ?? "main";
3451
- }
3452
- const stderr = result.stderr.trim();
3453
- if (stderr.includes("is not a symbolic ref") || stderr.includes("No such ref")) {
3454
- if (branchExists(cwd, "main")) return "main";
3455
- if (branchExists(cwd, "master")) return "master";
3456
- return "main";
3457
- }
3458
- throw new Error(`Failed to detect default branch in ${cwd}: ${stderr}`);
3459
- }
3460
- function hasUncommittedChanges(cwd) {
3461
- assertSafeCwd(cwd);
3462
- const result = spawnSync3("git", ["status", "--porcelain"], {
3463
- cwd,
3464
- encoding: "utf-8",
3465
- stdio: ["pipe", "pipe", "pipe"]
3466
- });
3467
- if (result.status !== 0) {
3468
- throw new Error(`Failed to check git status in ${cwd}: ${result.stderr.trim()}`);
3469
- }
3470
- return result.stdout.trim().length > 0;
3471
- }
3472
- function generateBranchName(sprintId) {
3473
- return `ralphctl/${sprintId}`;
3474
- }
3475
- function isGhAvailable() {
3476
- const result = spawnSync3("gh", ["--version"], {
3477
- encoding: "utf-8",
3478
- stdio: ["pipe", "pipe", "pipe"]
3479
- });
3480
- return result.status === 0;
3481
- }
3482
- function isGlabAvailable() {
3483
- const result = spawnSync3("glab", ["--version"], {
3484
- encoding: "utf-8",
3485
- stdio: ["pipe", "pipe", "pipe"]
3486
- });
3487
- return result.status === 0;
3488
- }
3489
-
3490
3868
  // src/ai/runner.ts
3491
3869
  async function promptBranchStrategy(sprintId) {
3492
3870
  const autoBranch = generateBranchName(sprintId);
@@ -3536,7 +3914,7 @@ async function ensureSprintBranches(sprintId, sprint, branchName) {
3536
3914
  const uniquePaths = [...new Set(remainingTasks.map((t) => t.projectPath))];
3537
3915
  if (uniquePaths.length === 0) return;
3538
3916
  for (const projectPath of uniquePaths) {
3539
- const uncommittedR = Result8.try(() => hasUncommittedChanges(projectPath));
3917
+ const uncommittedR = Result9.try(() => hasUncommittedChanges(projectPath));
3540
3918
  if (!uncommittedR.ok) {
3541
3919
  log.dim(` Skipping ${projectPath} \u2014 not a git repository`);
3542
3920
  continue;
@@ -3548,7 +3926,7 @@ async function ensureSprintBranches(sprintId, sprint, branchName) {
3548
3926
  }
3549
3927
  }
3550
3928
  for (const projectPath of uniquePaths) {
3551
- const branchR = Result8.try(() => {
3929
+ const branchR = Result9.try(() => {
3552
3930
  const currentBranch = getCurrentBranch(projectPath);
3553
3931
  if (currentBranch === branchName) {
3554
3932
  log.dim(` Already on branch '${branchName}' in ${projectPath}`);
@@ -3569,7 +3947,7 @@ async function ensureSprintBranches(sprintId, sprint, branchName) {
3569
3947
  }
3570
3948
  }
3571
3949
  function verifySprintBranch(projectPath, expectedBranch) {
3572
- const r = Result8.try(() => {
3950
+ const r = Result9.try(() => {
3573
3951
  if (verifyCurrentBranch(projectPath, expectedBranch)) return true;
3574
3952
  log.dim(` Branch mismatch in ${projectPath} \u2014 checking out '${expectedBranch}'`);
3575
3953
  createAndCheckoutBranch(projectPath, expectedBranch);
@@ -3868,7 +4246,7 @@ function parseArgs3(args) {
3868
4246
  return { sprintId, options };
3869
4247
  }
3870
4248
  async function sprintStartCommand(args) {
3871
- const parseR = Result9.try(() => parseArgs3(args));
4249
+ const parseR = Result10.try(() => parseArgs3(args));
3872
4250
  if (!parseR.ok) {
3873
4251
  showError(parseR.error.message);
3874
4252
  log.newline();
@@ -3938,6 +4316,7 @@ export {
3938
4316
  parseRequirementsFile,
3939
4317
  runAiSession,
3940
4318
  sprintRefineCommand,
4319
+ buildProjectToolingSection,
3941
4320
  getTaskImportSchema,
3942
4321
  parsePlanningBlocked,
3943
4322
  buildHeadlessAiRequest,