ralphctl 0.2.3 → 0.2.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.
@@ -2,12 +2,12 @@
2
2
  import {
3
3
  addCheckScriptToRepository,
4
4
  projectAddCommand
5
- } from "./chunk-LAERLCL5.mjs";
5
+ } from "./chunk-UBPZHHCD.mjs";
6
6
  import "./chunk-7LZ6GOGN.mjs";
7
7
  import "./chunk-7TG3EAQ2.mjs";
8
- import "./chunk-7TBO6GOT.mjs";
8
+ import "./chunk-EUNAUHC3.mjs";
9
9
  import "./chunk-OEUJDSHY.mjs";
10
- import "./chunk-GLDPHKEW.mjs";
10
+ import "./chunk-IB6OCKZW.mjs";
11
11
  import "./chunk-EDJX7TT6.mjs";
12
12
  import "./chunk-QBXHAXHI.mjs";
13
13
  export {
@@ -2,12 +2,12 @@
2
2
  import {
3
3
  addSingleTicketInteractive,
4
4
  ticketAddCommand
5
- } from "./chunk-QYF7QIZJ.mjs";
5
+ } from "./chunk-742XQ7FL.mjs";
6
6
  import "./chunk-7TG3EAQ2.mjs";
7
- import "./chunk-7TBO6GOT.mjs";
8
- import "./chunk-ITRZMBLJ.mjs";
7
+ import "./chunk-EUNAUHC3.mjs";
8
+ import "./chunk-JRFOUFD3.mjs";
9
9
  import "./chunk-OEUJDSHY.mjs";
10
- import "./chunk-GLDPHKEW.mjs";
10
+ import "./chunk-IB6OCKZW.mjs";
11
11
  import "./chunk-EDJX7TT6.mjs";
12
12
  import "./chunk-QBXHAXHI.mjs";
13
13
  export {
@@ -6,14 +6,14 @@ import {
6
6
  import {
7
7
  listProjects,
8
8
  projectExists
9
- } from "./chunk-7TBO6GOT.mjs";
9
+ } from "./chunk-EUNAUHC3.mjs";
10
10
  import {
11
11
  assertSprintStatus,
12
12
  generateUuid8,
13
13
  getEditor,
14
14
  resolveSprintId,
15
15
  setEditor
16
- } from "./chunk-ITRZMBLJ.mjs";
16
+ } from "./chunk-JRFOUFD3.mjs";
17
17
  import {
18
18
  ensureError,
19
19
  unwrapOrThrow,
@@ -24,7 +24,7 @@ import {
24
24
  getSprintFilePath,
25
25
  readValidatedJson,
26
26
  writeValidatedJson
27
- } from "./chunk-GLDPHKEW.mjs";
27
+ } from "./chunk-IB6OCKZW.mjs";
28
28
  import {
29
29
  IOError,
30
30
  IssueFetchError,
@@ -2,7 +2,7 @@
2
2
  import {
3
3
  createSprint,
4
4
  setCurrentSprint
5
- } from "./chunk-ITRZMBLJ.mjs";
5
+ } from "./chunk-JRFOUFD3.mjs";
6
6
  import {
7
7
  emoji,
8
8
  field,
@@ -7,7 +7,7 @@ import {
7
7
  readValidatedJson,
8
8
  validateProjectPath,
9
9
  writeValidatedJson
10
- } from "./chunk-GLDPHKEW.mjs";
10
+ } from "./chunk-IB6OCKZW.mjs";
11
11
  import {
12
12
  ParseError,
13
13
  ProjectExistsError,
@@ -53,6 +53,13 @@ function getTasksFilePath(sprintId) {
53
53
  function getProgressFilePath(sprintId) {
54
54
  return join(getSprintDir(sprintId), "progress.md");
55
55
  }
56
+ function getEvaluationsDir(sprintId) {
57
+ return join(getSprintDir(sprintId), "evaluations");
58
+ }
59
+ function getEvaluationFilePath(sprintId, taskId) {
60
+ assertSafeSegment(taskId, "task ID");
61
+ return join(getEvaluationsDir(sprintId), `${taskId}.md`);
62
+ }
56
63
  function assertSafeSegment(segment, label) {
57
64
  if (!segment || segment.includes("/") || segment.includes("\\") || segment.includes("..") || segment.includes("\0")) {
58
65
  throw new Error(`Path traversal detected in ${label}: ${segment}`);
@@ -203,6 +210,7 @@ import { z } from "zod";
203
210
  var SprintStatusSchema = z.enum(["draft", "active", "closed"]);
204
211
  var TaskStatusSchema = z.enum(["todo", "in_progress", "done"]);
205
212
  var RequirementStatusSchema = z.enum(["pending", "approved"]);
213
+ var EvaluationStatusSchema = z.enum(["passed", "failed", "malformed"]);
206
214
  var RepositorySchema = z.object({
207
215
  name: z.string().min(1),
208
216
  // Auto-derived from basename(path)
@@ -254,8 +262,12 @@ var TaskSchema = z.object({
254
262
  // Output from verification run
255
263
  evaluated: z.boolean().default(false),
256
264
  // Whether evaluation passed
257
- evaluationOutput: z.string().optional()
258
- // Output from evaluation run
265
+ evaluationOutput: z.string().optional(),
266
+ // Truncated output from evaluation run (full critique lives in evaluationFile)
267
+ evaluationStatus: EvaluationStatusSchema.optional(),
268
+ // Discriminator: 'passed' | 'failed' | 'malformed'
269
+ evaluationFile: z.string().optional()
270
+ // Sidecar file path containing the full untruncated critique
259
271
  });
260
272
  var TasksSchema = z.array(TaskSchema);
261
273
  var ImportTaskSchema = z.object({
@@ -309,6 +321,7 @@ export {
309
321
  getSprintFilePath,
310
322
  getTasksFilePath,
311
323
  getProgressFilePath,
324
+ getEvaluationFilePath,
312
325
  getRefinementDir,
313
326
  getPlanningDir,
314
327
  getIdeateDir,
@@ -21,7 +21,7 @@ import {
21
21
  readValidatedJson,
22
22
  removeDir,
23
23
  writeValidatedJson
24
- } from "./chunk-GLDPHKEW.mjs";
24
+ } from "./chunk-IB6OCKZW.mjs";
25
25
  import {
26
26
  LockError,
27
27
  NoCurrentSprintError,
@@ -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,
@@ -168,7 +170,12 @@ ${ctx.verificationCriteria.map((c) => `- ${c}`).join("\n")}` : "";
168
170
  const checkSection = ctx.checkScriptSection ? `
169
171
 
170
172
  ${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);
173
+ 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).replace("{{PROJECT_TOOLING_SECTION}}", ctx.projectToolingSection);
174
+ }
175
+ function buildEvaluationResumePrompt(ctx) {
176
+ const template = loadTemplate("task-evaluation-resume");
177
+ const commitInstruction = ctx.needsCommit ? "\n - **Then commit the fix** with a descriptive message before signaling completion." : "";
178
+ return template.replace("{{CRITIQUE}}", ctx.critique).replace("{{COMMIT_INSTRUCTION}}", commitInstruction);
172
179
  }
173
180
 
174
181
  // src/utils/requirements-export.ts
@@ -1163,6 +1170,12 @@ async function updateTask(taskId, updates, sprintId) {
1163
1170
  if (updates.evaluationOutput !== void 0) {
1164
1171
  task.evaluationOutput = updates.evaluationOutput;
1165
1172
  }
1173
+ if (updates.evaluationStatus !== void 0) {
1174
+ task.evaluationStatus = updates.evaluationStatus;
1175
+ }
1176
+ if (updates.evaluationFile !== void 0) {
1177
+ task.evaluationFile = updates.evaluationFile;
1178
+ }
1166
1179
  await saveTasks(tasks, id);
1167
1180
  return task;
1168
1181
  });
@@ -1360,7 +1373,7 @@ async function selectProject(message = "Select project:") {
1360
1373
  default: true
1361
1374
  });
1362
1375
  if (create) {
1363
- const { projectAddCommand } = await import("./add-K7LNOYQ4.mjs");
1376
+ const { projectAddCommand } = await import("./add-3T225IX5.mjs");
1364
1377
  await projectAddCommand({ interactive: true });
1365
1378
  const updated = await listProjects();
1366
1379
  if (updated.length === 0) return null;
@@ -1433,7 +1446,7 @@ async function selectSprint(message = "Select sprint:", filter) {
1433
1446
  default: true
1434
1447
  });
1435
1448
  if (create) {
1436
- const { sprintCreateCommand } = await import("./create-5MILNF7E.mjs");
1449
+ const { sprintCreateCommand } = await import("./create-MYGOWO2F.mjs");
1437
1450
  await sprintCreateCommand({ interactive: true });
1438
1451
  const updated = await listSprints();
1439
1452
  const refiltered = filter ? updated.filter((s) => filter.includes(s.status)) : updated;
@@ -1468,7 +1481,7 @@ async function selectTicket(message = "Select ticket:", filter) {
1468
1481
  default: true
1469
1482
  });
1470
1483
  if (create) {
1471
- const { ticketAddCommand } = await import("./add-DWNLZQ7Q.mjs");
1484
+ const { ticketAddCommand } = await import("./add-6A5432U2.mjs");
1472
1485
  await ticketAddCommand({ interactive: true });
1473
1486
  const updated = await listTickets();
1474
1487
  const refiltered = filter ? updated.filter(filter) : updated;
@@ -2059,15 +2072,165 @@ async function sprintPlanCommand(args) {
2059
2072
  }
2060
2073
 
2061
2074
  // src/commands/sprint/start.ts
2062
- import { Result as Result9 } from "typescript-result";
2075
+ import { Result as Result10 } from "typescript-result";
2063
2076
 
2064
2077
  // src/ai/runner.ts
2065
2078
  import { confirm as confirm5, input as input2, select as select2 } from "@inquirer/prompts";
2066
- import { Result as Result8 } from "typescript-result";
2079
+ import { Result as Result9 } from "typescript-result";
2067
2080
 
2068
2081
  // src/ai/executor.ts
2069
2082
  import { confirm as confirm4 } from "@inquirer/prompts";
2070
2083
  import { readFile as readFile4, unlink as unlink2 } from "fs/promises";
2084
+ import { Result as Result8 } from "typescript-result";
2085
+
2086
+ // src/utils/git.ts
2087
+ import { spawnSync as spawnSync2 } from "child_process";
2088
+ var BRANCH_NAME_RE = /^[a-zA-Z0-9/_.-]+$/;
2089
+ var BRANCH_NAME_INVALID_PATTERNS = [/\.\./, /\.$/, /\/$/, /\.lock$/, /^-/, /\/\//];
2090
+ function isValidBranchName(name) {
2091
+ if (!name || name.length > 250) return false;
2092
+ if (!BRANCH_NAME_RE.test(name)) return false;
2093
+ for (const pattern of BRANCH_NAME_INVALID_PATTERNS) {
2094
+ if (pattern.test(name)) return false;
2095
+ }
2096
+ return true;
2097
+ }
2098
+ function getCurrentBranch(cwd) {
2099
+ assertSafeCwd(cwd);
2100
+ const result = spawnSync2("git", ["rev-parse", "--abbrev-ref", "HEAD"], {
2101
+ cwd,
2102
+ encoding: "utf-8",
2103
+ stdio: ["pipe", "pipe", "pipe"]
2104
+ });
2105
+ if (result.status !== 0) {
2106
+ throw new Error(`Failed to get current branch in ${cwd}: ${result.stderr.trim()}`);
2107
+ }
2108
+ return result.stdout.trim();
2109
+ }
2110
+ function branchExists(cwd, name) {
2111
+ assertSafeCwd(cwd);
2112
+ if (!isValidBranchName(name)) {
2113
+ throw new Error(`Invalid branch name: ${name}`);
2114
+ }
2115
+ const result = spawnSync2("git", ["show-ref", "--verify", `refs/heads/${name}`], {
2116
+ cwd,
2117
+ encoding: "utf-8",
2118
+ stdio: ["pipe", "pipe", "pipe"]
2119
+ });
2120
+ return result.status === 0;
2121
+ }
2122
+ function createAndCheckoutBranch(cwd, name) {
2123
+ assertSafeCwd(cwd);
2124
+ if (!isValidBranchName(name)) {
2125
+ throw new Error(`Invalid branch name: ${name}`);
2126
+ }
2127
+ const current = getCurrentBranch(cwd);
2128
+ if (current === name) {
2129
+ return;
2130
+ }
2131
+ if (branchExists(cwd, name)) {
2132
+ const result = spawnSync2("git", ["checkout", name], {
2133
+ cwd,
2134
+ encoding: "utf-8",
2135
+ stdio: ["pipe", "pipe", "pipe"]
2136
+ });
2137
+ if (result.status !== 0) {
2138
+ throw new Error(`Failed to checkout branch '${name}' in ${cwd}: ${result.stderr.trim()}`);
2139
+ }
2140
+ } else {
2141
+ const result = spawnSync2("git", ["checkout", "-b", name], {
2142
+ cwd,
2143
+ encoding: "utf-8",
2144
+ stdio: ["pipe", "pipe", "pipe"]
2145
+ });
2146
+ if (result.status !== 0) {
2147
+ throw new Error(`Failed to create branch '${name}' in ${cwd}: ${result.stderr.trim()}`);
2148
+ }
2149
+ }
2150
+ }
2151
+ function verifyCurrentBranch(cwd, expected) {
2152
+ const current = getCurrentBranch(cwd);
2153
+ return current === expected;
2154
+ }
2155
+ function getDefaultBranch(cwd) {
2156
+ assertSafeCwd(cwd);
2157
+ const result = spawnSync2("git", ["symbolic-ref", "refs/remotes/origin/HEAD"], {
2158
+ cwd,
2159
+ encoding: "utf-8",
2160
+ stdio: ["pipe", "pipe", "pipe"]
2161
+ });
2162
+ if (result.status === 0) {
2163
+ const ref = result.stdout.trim();
2164
+ const parts = ref.split("/");
2165
+ return parts[parts.length - 1] ?? "main";
2166
+ }
2167
+ const stderr = result.stderr.trim();
2168
+ if (stderr.includes("is not a symbolic ref") || stderr.includes("No such ref")) {
2169
+ if (branchExists(cwd, "main")) return "main";
2170
+ if (branchExists(cwd, "master")) return "master";
2171
+ return "main";
2172
+ }
2173
+ throw new Error(`Failed to detect default branch in ${cwd}: ${stderr}`);
2174
+ }
2175
+ function getHeadSha(cwd) {
2176
+ try {
2177
+ assertSafeCwd(cwd);
2178
+ const result = spawnSync2("git", ["rev-parse", "HEAD"], {
2179
+ cwd,
2180
+ encoding: "utf-8",
2181
+ stdio: ["pipe", "pipe", "pipe"]
2182
+ });
2183
+ if (result.status !== 0) return null;
2184
+ return result.stdout.trim() || null;
2185
+ } catch {
2186
+ return null;
2187
+ }
2188
+ }
2189
+ function hasUncommittedChanges(cwd) {
2190
+ assertSafeCwd(cwd);
2191
+ const result = spawnSync2("git", ["status", "--porcelain"], {
2192
+ cwd,
2193
+ encoding: "utf-8",
2194
+ stdio: ["pipe", "pipe", "pipe"]
2195
+ });
2196
+ if (result.status !== 0) {
2197
+ throw new Error(`Failed to check git status in ${cwd}: ${result.stderr.trim()}`);
2198
+ }
2199
+ return result.stdout.trim().length > 0;
2200
+ }
2201
+ function generateBranchName(sprintId) {
2202
+ return `ralphctl/${sprintId}`;
2203
+ }
2204
+ function isGhAvailable() {
2205
+ const result = spawnSync2("gh", ["--version"], {
2206
+ encoding: "utf-8",
2207
+ stdio: ["pipe", "pipe", "pipe"]
2208
+ });
2209
+ return result.status === 0;
2210
+ }
2211
+ function isGlabAvailable() {
2212
+ const result = spawnSync2("glab", ["--version"], {
2213
+ encoding: "utf-8",
2214
+ stdio: ["pipe", "pipe", "pipe"]
2215
+ });
2216
+ return result.status === 0;
2217
+ }
2218
+
2219
+ // src/store/evaluation.ts
2220
+ async function writeEvaluation(sprintId, taskId, iteration, status, body) {
2221
+ const filePath = getEvaluationFilePath(sprintId, taskId);
2222
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString();
2223
+ const header = `## ${timestamp} \u2014 Iteration ${String(iteration)} \u2014 ${status.toUpperCase()}
2224
+
2225
+ `;
2226
+ const entry = `${header}${body.trimEnd()}
2227
+
2228
+ ---
2229
+
2230
+ `;
2231
+ unwrapOrThrow(await appendToFile(filePath, entry));
2232
+ return filePath;
2233
+ }
2071
2234
 
2072
2235
  // src/ai/parser.ts
2073
2236
  function parseExecutionResult(output) {
@@ -2455,7 +2618,7 @@ function runPermissionCheck(ctx, noCommit, provider) {
2455
2618
  }
2456
2619
 
2457
2620
  // src/ai/lifecycle.ts
2458
- import { spawnSync as spawnSync2 } from "child_process";
2621
+ import { spawnSync as spawnSync3 } from "child_process";
2459
2622
  var DEFAULT_HOOK_TIMEOUT_MS = 5 * 60 * 1e3;
2460
2623
  function getHookTimeoutMs() {
2461
2624
  const envVal = process.env["RALPHCTL_SETUP_TIMEOUT_MS"];
@@ -2468,7 +2631,7 @@ function getHookTimeoutMs() {
2468
2631
  function runLifecycleHook(projectPath, script, event, timeoutOverrideMs) {
2469
2632
  assertSafeCwd(projectPath);
2470
2633
  const timeoutMs = timeoutOverrideMs ?? getHookTimeoutMs();
2471
- const result = spawnSync2(script, {
2634
+ const result = spawnSync3(script, {
2472
2635
  cwd: projectPath,
2473
2636
  shell: true,
2474
2637
  stdio: ["pipe", "pipe", "pipe"],
@@ -2480,7 +2643,142 @@ function runLifecycleHook(projectPath, script, event, timeoutOverrideMs) {
2480
2643
  return { passed: result.status === 0, output };
2481
2644
  }
2482
2645
 
2646
+ // src/ai/project-tooling.ts
2647
+ import { existsSync as existsSync3, readdirSync, readFileSync as readFileSync3 } from "fs";
2648
+ import { join as join8 } from "path";
2649
+ var EMPTY_TOOLING = {
2650
+ agents: [],
2651
+ skills: [],
2652
+ mcpServers: [],
2653
+ hasClaudeMd: false,
2654
+ hasAgentsMd: false,
2655
+ hasCopilotInstructions: false
2656
+ };
2657
+ function safeListDir(path, predicate) {
2658
+ try {
2659
+ if (!existsSync3(path)) return [];
2660
+ return readdirSync(path).filter(predicate).sort();
2661
+ } catch {
2662
+ return [];
2663
+ }
2664
+ }
2665
+ var EVALUATOR_DENYLISTED_AGENTS = /* @__PURE__ */ new Set(["implementer", "planner"]);
2666
+ function detectAgents(projectPath) {
2667
+ const agentsDir = join8(projectPath, ".claude", "agents");
2668
+ return safeListDir(agentsDir, (name) => name.endsWith(".md")).map((name) => name.replace(/\.md$/, "")).filter((name) => !EVALUATOR_DENYLISTED_AGENTS.has(name));
2669
+ }
2670
+ function detectSkills(projectPath) {
2671
+ const skillsDir = join8(projectPath, ".claude", "skills");
2672
+ try {
2673
+ if (!existsSync3(skillsDir)) return [];
2674
+ return readdirSync(skillsDir, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => entry.name).sort();
2675
+ } catch {
2676
+ return [];
2677
+ }
2678
+ }
2679
+ function detectMcpServers(projectPath) {
2680
+ const mcpFile = join8(projectPath, ".mcp.json");
2681
+ if (!existsSync3(mcpFile)) return [];
2682
+ try {
2683
+ const raw = readFileSync3(mcpFile, "utf-8");
2684
+ const parsed = JSON.parse(raw);
2685
+ const servers = parsed.mcpServers;
2686
+ if (!servers || typeof servers !== "object") return [];
2687
+ return Object.keys(servers).sort();
2688
+ } catch {
2689
+ return [];
2690
+ }
2691
+ }
2692
+ function detectProjectTooling(projectPath) {
2693
+ if (!projectPath || !existsSync3(projectPath)) {
2694
+ return EMPTY_TOOLING;
2695
+ }
2696
+ return {
2697
+ agents: detectAgents(projectPath),
2698
+ skills: detectSkills(projectPath),
2699
+ mcpServers: detectMcpServers(projectPath),
2700
+ hasClaudeMd: existsSync3(join8(projectPath, "CLAUDE.md")),
2701
+ hasAgentsMd: existsSync3(join8(projectPath, "AGENTS.md")),
2702
+ hasCopilotInstructions: existsSync3(join8(projectPath, ".github", "copilot-instructions.md"))
2703
+ };
2704
+ }
2705
+ function renderProjectToolingSection(tooling) {
2706
+ const hasAny = tooling.agents.length > 0 || tooling.skills.length > 0 || tooling.mcpServers.length > 0 || tooling.hasClaudeMd || tooling.hasAgentsMd || tooling.hasCopilotInstructions;
2707
+ if (!hasAny) return "";
2708
+ const lines = [];
2709
+ lines.push("## Project Tooling (use these \u2014 they exist for a reason)");
2710
+ lines.push("");
2711
+ lines.push(
2712
+ "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."
2713
+ );
2714
+ lines.push("");
2715
+ if (tooling.agents.length > 0) {
2716
+ lines.push("### Subagents available");
2717
+ lines.push("");
2718
+ lines.push("Delegate via the Task tool with `subagent_type=<name>` when the diff matches a specialty:");
2719
+ for (const agent of tooling.agents) {
2720
+ const hint = describeAgentHint(agent);
2721
+ lines.push(`- \`${agent}\`${hint ? ` \u2014 ${hint}` : ""}`);
2722
+ }
2723
+ lines.push("");
2724
+ }
2725
+ if (tooling.skills.length > 0) {
2726
+ lines.push("### Skills available");
2727
+ lines.push("");
2728
+ lines.push("Invoke via the Skill tool when the skill name matches the work in front of you:");
2729
+ for (const skill of tooling.skills) {
2730
+ lines.push(`- \`${skill}\``);
2731
+ }
2732
+ lines.push("");
2733
+ }
2734
+ if (tooling.mcpServers.length > 0) {
2735
+ lines.push("### MCP servers available");
2736
+ lines.push("");
2737
+ lines.push(
2738
+ "These give you tools beyond the filesystem. Use them to **interact with the running system**, not just read its source."
2739
+ );
2740
+ for (const server of tooling.mcpServers) {
2741
+ const hint = describeMcpHint(server);
2742
+ lines.push(`- \`${server}\`${hint ? ` \u2014 ${hint}` : ""}`);
2743
+ }
2744
+ lines.push("");
2745
+ }
2746
+ const instructionFiles = [];
2747
+ if (tooling.hasClaudeMd) instructionFiles.push("`CLAUDE.md`");
2748
+ if (tooling.hasAgentsMd) instructionFiles.push("`AGENTS.md`");
2749
+ if (tooling.hasCopilotInstructions) instructionFiles.push("`.github/copilot-instructions.md`");
2750
+ if (instructionFiles.length > 0) {
2751
+ lines.push("### Project instructions");
2752
+ lines.push("");
2753
+ lines.push(
2754
+ `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).`
2755
+ );
2756
+ lines.push("");
2757
+ }
2758
+ return lines.join("\n");
2759
+ }
2760
+ function describeAgentHint(name) {
2761
+ const hints = {
2762
+ auditor: "use for security-sensitive diffs (auth, input handling, file IO, secrets)",
2763
+ reviewer: "use for general code-quality review of the diff",
2764
+ tester: "use to assess test coverage and quality of new tests",
2765
+ designer: "use for UI/UX/theming changes"
2766
+ };
2767
+ return hints[name] ?? null;
2768
+ }
2769
+ function describeMcpHint(name) {
2770
+ const lower = name.toLowerCase();
2771
+ if (lower.includes("playwright")) return "use for any UI/frontend task \u2014 click through the changed flow";
2772
+ if (lower.includes("puppeteer")) return "use for browser automation on UI changes";
2773
+ if (lower.includes("github")) return "use to inspect related PRs/issues for context";
2774
+ if (lower.includes("postgres") || lower.includes("mysql") || lower.includes("sqlite")) {
2775
+ return "use to verify database schema/migration changes against a real DB";
2776
+ }
2777
+ return null;
2778
+ }
2779
+
2483
2780
  // src/ai/evaluator.ts
2781
+ var EVALUATOR_MAX_TURNS = 100;
2484
2782
  function getEvaluatorModel(generatorModel, provider) {
2485
2783
  if (provider.name !== "claude" || !generatorModel) return null;
2486
2784
  const modelLower = generatorModel.toLowerCase();
@@ -2512,13 +2810,16 @@ function parseDimensionScores(output) {
2512
2810
  function parseEvaluationResult(output) {
2513
2811
  const dimensions = parseDimensionScores(output);
2514
2812
  if (output.includes("<evaluation-passed>")) {
2515
- return { passed: true, output, dimensions };
2813
+ return { passed: true, status: "passed", output, dimensions };
2516
2814
  }
2517
2815
  const failedMatch = /<evaluation-failed>([\s\S]*?)<\/evaluation-failed>/.exec(output);
2518
2816
  if (failedMatch) {
2519
- return { passed: false, output: failedMatch[1]?.trim() ?? output, dimensions };
2817
+ return { passed: false, status: "failed", output: failedMatch[1]?.trim() ?? output, dimensions };
2520
2818
  }
2521
- return { passed: false, output, dimensions };
2819
+ if (dimensions.length > 0) {
2820
+ return { passed: false, status: "failed", output, dimensions };
2821
+ }
2822
+ return { passed: false, status: "malformed", output, dimensions };
2522
2823
  }
2523
2824
  function buildEvaluatorContext(task, checkScript) {
2524
2825
  const checkScriptSection = checkScript ? `## Check Script (Computational Gate)
@@ -2530,31 +2831,42 @@ ${checkScript}
2530
2831
  \`\`\`
2531
2832
 
2532
2833
  If this script fails, the implementation fails regardless of code quality. Record the full output.` : null;
2834
+ const tooling = detectProjectTooling(task.projectPath);
2835
+ const projectToolingSection = renderProjectToolingSection(tooling);
2533
2836
  return {
2534
2837
  taskName: task.name,
2535
2838
  taskDescription: task.description ?? "",
2536
2839
  taskSteps: task.steps,
2537
2840
  verificationCriteria: task.verificationCriteria,
2538
2841
  projectPath: task.projectPath,
2539
- checkScriptSection
2842
+ checkScriptSection,
2843
+ projectToolingSection
2540
2844
  };
2541
2845
  }
2542
- async function runEvaluation(task, generatorModel, checkScript, sprintId, provider) {
2846
+ async function runEvaluation(task, generatorModel, checkScript, sprintId, provider, options) {
2543
2847
  const p = provider ?? await getActiveProvider();
2544
2848
  const evaluatorModel = getEvaluatorModel(generatorModel, p);
2545
2849
  const sprintDir = getSprintDir(sprintId);
2546
2850
  const ctx = buildEvaluatorContext(task, checkScript);
2547
2851
  const prompt = buildEvaluatorPrompt(ctx);
2548
2852
  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
- });
2853
+ if (p.name === "claude") {
2854
+ if (evaluatorModel) {
2855
+ providerArgs.push("--model", evaluatorModel);
2856
+ }
2857
+ providerArgs.push("--max-turns", String(EVALUATOR_MAX_TURNS));
2858
+ }
2859
+ await options?.coordinator?.waitIfPaused();
2860
+ const result = await spawnWithRetry(
2861
+ {
2862
+ cwd: task.projectPath,
2863
+ args: providerArgs,
2864
+ prompt,
2865
+ env: p.getSpawnEnv()
2866
+ },
2867
+ { maxRetries: options?.maxRetries },
2868
+ p
2869
+ );
2558
2870
  return parseEvaluationResult(result.stdout);
2559
2871
  }
2560
2872
 
@@ -2701,6 +3013,31 @@ async function executeTask(ctx, options, sprintId, resumeSessionId, provider, ch
2701
3013
  return { ...parsed, sessionId: spawnResult.sessionId, model: spawnResult.model };
2702
3014
  }
2703
3015
  var MAX_EVAL_OUTPUT = 2e3;
3016
+ var EVAL_SPAWN_FAILURE_PREFIX = "Evaluator spawn failed:";
3017
+ function isEvalSpawnFailure(output) {
3018
+ return output.startsWith(EVAL_SPAWN_FAILURE_PREFIX);
3019
+ }
3020
+ async function runEvaluationSafely(task, generatorModel, checkScript, sprintId, provider, options, coordinator) {
3021
+ const evalR = await wrapAsync(
3022
+ () => runEvaluation(task, generatorModel, checkScript, sprintId, provider, {
3023
+ coordinator,
3024
+ maxRetries: options.maxRetries
3025
+ }),
3026
+ ensureError
3027
+ );
3028
+ if (evalR.ok) return evalR.value;
3029
+ const err = evalR.error;
3030
+ if (err instanceof SpawnError && err.rateLimited && coordinator) {
3031
+ coordinator.pause(err.retryAfterMs ?? 6e4);
3032
+ }
3033
+ console.log(warning(`Evaluator spawn failed for ${task.name}: ${err.message} \u2014 marking malformed`));
3034
+ return {
3035
+ passed: false,
3036
+ status: "malformed",
3037
+ output: `${EVAL_SPAWN_FAILURE_PREFIX} ${err.message}`,
3038
+ dimensions: []
3039
+ };
3040
+ }
2704
3041
  async function runEvaluationLoop(params) {
2705
3042
  const {
2706
3043
  task,
@@ -2711,30 +3048,37 @@ async function runEvaluationLoop(params) {
2711
3048
  options,
2712
3049
  evalIterations,
2713
3050
  checkTimeout,
2714
- useSpinner = false
3051
+ useSpinner = false,
3052
+ coordinator
2715
3053
  } = params;
2716
3054
  const evalCheckScript = getEffectiveCheckScript(project, task.projectPath);
2717
3055
  const sprintDir = getSprintDir(sprintId);
2718
- let evalResult = await runEvaluation(task, result.model, evalCheckScript, sprintId, provider);
3056
+ let evalResult = await runEvaluationSafely(
3057
+ task,
3058
+ result.model,
3059
+ evalCheckScript,
3060
+ sprintId,
3061
+ provider,
3062
+ options,
3063
+ coordinator
3064
+ );
3065
+ let evaluationFile = await tryWriteEvaluationEntry(sprintId, task, 1, evalResult);
2719
3066
  let currentSessionId = result.sessionId;
2720
3067
  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)})`));
3068
+ for (let i = 0; i < evalIterations && !evalResult.passed && evalResult.status !== "malformed"; i++) {
3069
+ console.log(warning(`Evaluation failed for ${task.name} \u2014 fix attempt ${String(i + 1)}/${String(evalIterations)}`));
2723
3070
  console.log(muted(evalResult.output.slice(0, 500)));
3071
+ const headBefore = getHeadSha(task.projectPath);
3072
+ const resumePrompt = buildEvaluationResumePrompt({
3073
+ critique: evalResult.output,
3074
+ needsCommit: !options.noCommit
3075
+ });
2724
3076
  const resumeSpinner = useSpinner ? createSpinner(`Fixing evaluation issues: ${task.name}`).start() : null;
2725
3077
  const resumeResult = await spawnWithRetry(
2726
3078
  {
2727
3079
  cwd: task.projectPath,
2728
3080
  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.`,
3081
+ prompt: resumePrompt,
2738
3082
  resumeSessionId: currentSessionId ?? void 0,
2739
3083
  env: provider.getSpawnEnv()
2740
3084
  },
@@ -2753,35 +3097,84 @@ If the critique is about something outside your task scope, fix only what is wit
2753
3097
  if (resumeResult.model) currentModel = resumeResult.model;
2754
3098
  const fixResult = parseExecutionResult(resumeResult.stdout);
2755
3099
  if (!fixResult.success) {
2756
- console.log(warning(`Generator could not fix issues after feedback: ${task.name}`));
3100
+ const reason = `Generator could not fix issues after feedback (no <task-complete> signal)`;
3101
+ console.log(warning(`${reason}: ${task.name}`));
3102
+ const stubPath = await tryWriteEvaluationStub(sprintId, task, i + 2, reason);
3103
+ if (stubPath) evaluationFile = stubPath;
3104
+ break;
3105
+ }
3106
+ const headAfter = getHeadSha(task.projectPath);
3107
+ const dirtyR = Result8.try(() => hasUncommittedChanges(task.projectPath));
3108
+ const dirty = dirtyR.ok ? dirtyR.value : false;
3109
+ if (headBefore !== null && headAfter === headBefore && !dirty) {
3110
+ const reason = "Generator no-op (HEAD unchanged, no uncommitted changes)";
3111
+ console.log(warning(`${reason}: ${task.name}`));
3112
+ const stubPath = await tryWriteEvaluationStub(sprintId, task, i + 2, reason);
3113
+ if (stubPath) evaluationFile = stubPath;
2757
3114
  break;
2758
3115
  }
2759
3116
  const recheckScript = getEffectiveCheckScript(project, task.projectPath);
2760
3117
  if (recheckScript) {
2761
3118
  const recheckResult = runLifecycleHook(task.projectPath, recheckScript, "taskComplete", checkTimeout);
2762
3119
  if (!recheckResult.passed) {
3120
+ const reason = `Post-task check failed after generator fix: ${recheckResult.output.slice(0, 200)}`;
2763
3121
  console.log(warning(`Post-task check failed after generator fix: ${task.name}`));
3122
+ const stubPath = await tryWriteEvaluationStub(sprintId, task, i + 2, reason);
3123
+ if (stubPath) evaluationFile = stubPath;
2764
3124
  break;
2765
3125
  }
2766
3126
  }
2767
- evalResult = await runEvaluation(task, currentModel, evalCheckScript, sprintId, provider);
3127
+ evalResult = await runEvaluationSafely(
3128
+ task,
3129
+ currentModel,
3130
+ evalCheckScript,
3131
+ sprintId,
3132
+ provider,
3133
+ options,
3134
+ coordinator
3135
+ );
3136
+ const entryPath = await tryWriteEvaluationEntry(sprintId, task, i + 2, evalResult);
3137
+ if (entryPath) evaluationFile = entryPath;
2768
3138
  }
2769
3139
  await updateTask(
2770
3140
  task.id,
2771
3141
  {
2772
3142
  evaluated: true,
2773
- evaluationOutput: evalResult.output.slice(0, MAX_EVAL_OUTPUT)
3143
+ evaluationStatus: evalResult.status,
3144
+ evaluationOutput: evalResult.output.slice(0, MAX_EVAL_OUTPUT),
3145
+ ...evaluationFile ? { evaluationFile } : {}
2774
3146
  },
2775
3147
  sprintId
2776
3148
  );
2777
- if (!evalResult.passed) {
3149
+ if (evalResult.status === "malformed") {
3150
+ const cause = isEvalSpawnFailure(evalResult.output) ? evalResult.output : "no signal, no dimensions";
3151
+ console.log(warning(`Evaluator output was malformed for ${task.name} (${cause}) \u2014 marking done`));
3152
+ } else if (!evalResult.passed) {
2778
3153
  console.log(
2779
- warning(`Evaluation did not pass after ${String(evalIterations)} iteration(s) \u2014 marking done: ${task.name}`)
3154
+ warning(`Evaluation did not pass after ${String(evalIterations)} fix attempt(s) \u2014 marking done: ${task.name}`)
2780
3155
  );
2781
3156
  } else {
2782
3157
  console.log(success(`Evaluation passed: ${task.name}`));
2783
3158
  }
2784
3159
  }
3160
+ async function tryWriteEvaluationEntry(sprintId, task, iteration, evalResult) {
3161
+ let body;
3162
+ if (evalResult.status === "malformed") {
3163
+ body = isEvalSpawnFailure(evalResult.output) ? evalResult.output : "_(evaluator output had no parseable signal \u2014 see executor stdout)_";
3164
+ } else {
3165
+ body = evalResult.output;
3166
+ }
3167
+ return tryWriteEvaluationRaw(sprintId, task, iteration, evalResult.status, body);
3168
+ }
3169
+ async function tryWriteEvaluationStub(sprintId, task, iteration, reason) {
3170
+ return tryWriteEvaluationRaw(sprintId, task, iteration, "failed", `_(no re-evaluation: ${reason})_`);
3171
+ }
3172
+ async function tryWriteEvaluationRaw(sprintId, task, iteration, status, body) {
3173
+ const writeR = await wrapAsync(() => writeEvaluation(sprintId, task.id, iteration, status, body), ensureError);
3174
+ if (writeR.ok) return writeR.value;
3175
+ console.log(warning(`Could not persist evaluation sidecar for ${task.name}: ${writeR.error.message}`));
3176
+ return null;
3177
+ }
2785
3178
  async function areAllRemainingBlocked(sprintId) {
2786
3179
  const remaining = await getRemainingTasks(sprintId);
2787
3180
  if (remaining.length === 0) return false;
@@ -2926,9 +3319,10 @@ Starting ${label} in ${task.projectPath} (session)...
2926
3319
  console.log(success("Verification: passed"));
2927
3320
  }
2928
3321
  const checkScript = getEffectiveCheckScript(project, task.projectPath);
3322
+ const sequentialRepo = project?.repositories.find((r) => r.path === task.projectPath);
2929
3323
  if (checkScript) {
2930
3324
  console.log(muted(`Running post-task check: ${checkScript}`));
2931
- const hookResult = runLifecycleHook(task.projectPath, checkScript, "taskComplete");
3325
+ const hookResult = runLifecycleHook(task.projectPath, checkScript, "taskComplete", sequentialRepo?.checkTimeout);
2932
3326
  if (!hookResult.passed) {
2933
3327
  console.log(warning(`
2934
3328
  Post-task check failed for: ${task.name}`));
@@ -2956,6 +3350,7 @@ Post-task check failed for: ${task.name}`));
2956
3350
  provider,
2957
3351
  options,
2958
3352
  evalIterations,
3353
+ checkTimeout: sequentialRepo?.checkTimeout,
2959
3354
  useSpinner: true
2960
3355
  });
2961
3356
  }
@@ -3289,7 +3684,8 @@ Post-task check failed for: ${settled.task.name}`));
3289
3684
  provider,
3290
3685
  options,
3291
3686
  evalIterations,
3292
- checkTimeout: taskRepo?.checkTimeout
3687
+ checkTimeout: taskRepo?.checkTimeout,
3688
+ coordinator
3293
3689
  });
3294
3690
  }
3295
3691
  await updateTaskStatus(settled.task.id, "done", sprintId);
@@ -3368,125 +3764,6 @@ Waiting for ${String(running.size)} remaining task(s)...`));
3368
3764
  };
3369
3765
  }
3370
3766
 
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
3767
  // src/ai/runner.ts
3491
3768
  async function promptBranchStrategy(sprintId) {
3492
3769
  const autoBranch = generateBranchName(sprintId);
@@ -3536,7 +3813,7 @@ async function ensureSprintBranches(sprintId, sprint, branchName) {
3536
3813
  const uniquePaths = [...new Set(remainingTasks.map((t) => t.projectPath))];
3537
3814
  if (uniquePaths.length === 0) return;
3538
3815
  for (const projectPath of uniquePaths) {
3539
- const uncommittedR = Result8.try(() => hasUncommittedChanges(projectPath));
3816
+ const uncommittedR = Result9.try(() => hasUncommittedChanges(projectPath));
3540
3817
  if (!uncommittedR.ok) {
3541
3818
  log.dim(` Skipping ${projectPath} \u2014 not a git repository`);
3542
3819
  continue;
@@ -3548,7 +3825,7 @@ async function ensureSprintBranches(sprintId, sprint, branchName) {
3548
3825
  }
3549
3826
  }
3550
3827
  for (const projectPath of uniquePaths) {
3551
- const branchR = Result8.try(() => {
3828
+ const branchR = Result9.try(() => {
3552
3829
  const currentBranch = getCurrentBranch(projectPath);
3553
3830
  if (currentBranch === branchName) {
3554
3831
  log.dim(` Already on branch '${branchName}' in ${projectPath}`);
@@ -3569,7 +3846,7 @@ async function ensureSprintBranches(sprintId, sprint, branchName) {
3569
3846
  }
3570
3847
  }
3571
3848
  function verifySprintBranch(projectPath, expectedBranch) {
3572
- const r = Result8.try(() => {
3849
+ const r = Result9.try(() => {
3573
3850
  if (verifyCurrentBranch(projectPath, expectedBranch)) return true;
3574
3851
  log.dim(` Branch mismatch in ${projectPath} \u2014 checking out '${expectedBranch}'`);
3575
3852
  createAndCheckoutBranch(projectPath, expectedBranch);
@@ -3868,7 +4145,7 @@ function parseArgs3(args) {
3868
4145
  return { sprintId, options };
3869
4146
  }
3870
4147
  async function sprintStartCommand(args) {
3871
- const parseR = Result9.try(() => parseArgs3(args));
4148
+ const parseR = Result10.try(() => parseArgs3(args));
3872
4149
  if (!parseR.ok) {
3873
4150
  showError(parseR.error.message);
3874
4151
  log.newline();
@@ -8,7 +8,7 @@ import {
8
8
  } from "./chunk-7TG3EAQ2.mjs";
9
9
  import {
10
10
  createProject
11
- } from "./chunk-7TBO6GOT.mjs";
11
+ } from "./chunk-EUNAUHC3.mjs";
12
12
  import {
13
13
  ensureError,
14
14
  wrapAsync
@@ -16,7 +16,7 @@ import {
16
16
  import {
17
17
  expandTilde,
18
18
  validateProjectPath
19
- } from "./chunk-GLDPHKEW.mjs";
19
+ } from "./chunk-IB6OCKZW.mjs";
20
20
  import {
21
21
  IOError,
22
22
  ProjectExistsError
package/dist/cli.mjs CHANGED
@@ -2,7 +2,7 @@
2
2
  import {
3
3
  addCheckScriptToRepository,
4
4
  projectAddCommand
5
- } from "./chunk-LAERLCL5.mjs";
5
+ } from "./chunk-UBPZHHCD.mjs";
6
6
  import {
7
7
  addTask,
8
8
  areAllTasksDone,
@@ -52,13 +52,13 @@ import {
52
52
  sprintStartCommand,
53
53
  updateTaskStatus,
54
54
  validateImportTasks
55
- } from "./chunk-ORVGM6EV.mjs";
55
+ } from "./chunk-U62BX47C.mjs";
56
56
  import {
57
57
  escapableSelect
58
58
  } from "./chunk-7LZ6GOGN.mjs";
59
59
  import {
60
60
  sprintCreateCommand
61
- } from "./chunk-V4ZUDZCG.mjs";
61
+ } from "./chunk-DUU5346E.mjs";
62
62
  import {
63
63
  addTicket,
64
64
  allRequirementsApproved,
@@ -73,7 +73,7 @@ import {
73
73
  removeTicket,
74
74
  ticketAddCommand,
75
75
  updateTicket
76
- } from "./chunk-QYF7QIZJ.mjs";
76
+ } from "./chunk-742XQ7FL.mjs";
77
77
  import {
78
78
  EXIT_ERROR,
79
79
  exitWithCode
@@ -84,7 +84,7 @@ import {
84
84
  listProjects,
85
85
  removeProject,
86
86
  removeProjectRepo
87
- } from "./chunk-7TBO6GOT.mjs";
87
+ } from "./chunk-EUNAUHC3.mjs";
88
88
  import {
89
89
  DEFAULT_EVALUATION_ITERATIONS,
90
90
  assertSprintStatus,
@@ -107,7 +107,7 @@ import {
107
107
  setEditor,
108
108
  setEvaluationIterations,
109
109
  withFileLock
110
- } from "./chunk-ITRZMBLJ.mjs";
110
+ } from "./chunk-JRFOUFD3.mjs";
111
111
  import {
112
112
  ensureError,
113
113
  wrapAsync
@@ -134,7 +134,7 @@ import {
134
134
  getTasksFilePath,
135
135
  readValidatedJson,
136
136
  validateProjectPath
137
- } from "./chunk-GLDPHKEW.mjs";
137
+ } from "./chunk-IB6OCKZW.mjs";
138
138
  import {
139
139
  DomainError,
140
140
  NoCurrentSprintError,
@@ -3764,7 +3764,7 @@ async function interactiveMode() {
3764
3764
  continue;
3765
3765
  }
3766
3766
  if (command === "wizard") {
3767
- const { runWizard } = await import("./wizard-RCQ4QQOL.mjs");
3767
+ const { runWizard } = await import("./wizard-HWOH2HPV.mjs");
3768
3768
  await runWizard();
3769
3769
  continue;
3770
3770
  }
@@ -4323,7 +4323,7 @@ Checks performed:
4323
4323
  // package.json
4324
4324
  var package_default = {
4325
4325
  name: "ralphctl",
4326
- version: "0.2.3",
4326
+ version: "0.2.4",
4327
4327
  description: "Agent harness for long-running AI coding tasks \u2014 orchestrates Claude Code & GitHub Copilot across repositories",
4328
4328
  homepage: "https://github.com/lukas-grigis/ralphctl",
4329
4329
  type: "module",
@@ -4445,7 +4445,7 @@ registerCompletionCommands(program);
4445
4445
  registerDoctorCommands(program);
4446
4446
  async function main() {
4447
4447
  if (process.env["COMP_CWORD"] && process.env["COMP_POINT"] && process.env["COMP_LINE"]) {
4448
- const { handleCompletionRequest } = await import("./handle-2BACSJLR.mjs");
4448
+ const { handleCompletionRequest } = await import("./handle-TA4MYNQJ.mjs");
4449
4449
  if (await handleCompletionRequest(program)) return;
4450
4450
  }
4451
4451
  if (process.argv.length <= 2 || process.argv[2] === "interactive") {
@@ -1,10 +1,10 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  sprintCreateCommand
4
- } from "./chunk-V4ZUDZCG.mjs";
5
- import "./chunk-ITRZMBLJ.mjs";
4
+ } from "./chunk-DUU5346E.mjs";
5
+ import "./chunk-JRFOUFD3.mjs";
6
6
  import "./chunk-OEUJDSHY.mjs";
7
- import "./chunk-GLDPHKEW.mjs";
7
+ import "./chunk-IB6OCKZW.mjs";
8
8
  import "./chunk-EDJX7TT6.mjs";
9
9
  import "./chunk-QBXHAXHI.mjs";
10
10
  export {
@@ -7,7 +7,7 @@ async function handleCompletionRequest(program) {
7
7
  return false;
8
8
  }
9
9
  const tabtab = (await import("tabtab")).default;
10
- const { resolveCompletions } = await import("./resolver-CFY6DIOP.mjs");
10
+ const { resolveCompletions } = await import("./resolver-RXEY6EJE.mjs");
11
11
  const tabEnv = tabtab.parseEnv(env);
12
12
  const completions = await resolveCompletions(program, {
13
13
  line: tabEnv.line,
@@ -9,8 +9,8 @@ import {
9
9
  removeProject,
10
10
  removeProjectRepo,
11
11
  updateProject
12
- } from "./chunk-7TBO6GOT.mjs";
13
- import "./chunk-GLDPHKEW.mjs";
12
+ } from "./chunk-EUNAUHC3.mjs";
13
+ import "./chunk-IB6OCKZW.mjs";
14
14
  import {
15
15
  ProjectExistsError,
16
16
  ProjectNotFoundError
@@ -0,0 +1,22 @@
1
+ # Evaluator Feedback — Fix and Re-verify
2
+
3
+ The independent code reviewer found issues with your implementation. Treat this as ground truth — do not argue with
4
+ it. Read the critique carefully, fix each identified issue, then re-verify and signal completion.
5
+
6
+ ## Critique
7
+
8
+ {{CRITIQUE}}
9
+
10
+ ## What to do now
11
+
12
+ 1. **Fix each issue in the critique above.** Reference the file:line locations the reviewer cited. If a citation is
13
+ wrong, find the actually-affected location and fix that.
14
+ 2. **Stay in scope.** If the critique calls out something outside your task scope, fix only what is within scope and
15
+ note the rest. Do not expand the task.
16
+ 3. **Re-run verification commands.** Run the project's check script (or the equivalent verification commands) and
17
+ confirm they pass.{{COMMIT_INSTRUCTION}}
18
+ 4. **Re-output verification results** wrapped in `<task-verified>...</task-verified>`.
19
+ 5. **Signal completion** with `<task-complete>` ONLY after all of the above pass.
20
+
21
+ If the critique is unfixable (e.g. it asks for something that contradicts the spec, or requires changes you cannot
22
+ make), signal `<task-blocked>reason</task-blocked>` instead of completing.
@@ -22,6 +22,8 @@ You are working in this project directory:
22
22
  {{PROJECT_PATH}}
23
23
  ```
24
24
 
25
+ {{PROJECT_TOOLING_SECTION}}
26
+
25
27
  ### Phase 1: Computational Verification (run before reasoning)
26
28
 
27
29
  Run deterministic checks first — these are cheap, fast, and authoritative.
@@ -11,7 +11,7 @@ var dynamicResolvers = {
11
11
  "--project": async () => {
12
12
  const result = await wrapAsync(
13
13
  async () => {
14
- const { listProjects } = await import("./project-XC7AXA4B.mjs");
14
+ const { listProjects } = await import("./project-YONEJICR.mjs");
15
15
  return listProjects();
16
16
  },
17
17
  (err) => new IOError("Failed to load projects for completion", err instanceof Error ? err : void 0)
@@ -45,7 +45,7 @@ var configValueCompletions = {
45
45
  async function getSprintCompletions() {
46
46
  const result = await wrapAsync(
47
47
  async () => {
48
- const { listSprints } = await import("./sprint-F4VRAEWZ.mjs");
48
+ const { listSprints } = await import("./sprint-FGLWYWKX.mjs");
49
49
  return listSprints();
50
50
  },
51
51
  (err) => new IOError("Failed to load sprints for completion", err instanceof Error ? err : void 0)
@@ -12,9 +12,9 @@ import {
12
12
  listSprints,
13
13
  resolveSprintId,
14
14
  saveSprint
15
- } from "./chunk-ITRZMBLJ.mjs";
15
+ } from "./chunk-JRFOUFD3.mjs";
16
16
  import "./chunk-OEUJDSHY.mjs";
17
- import "./chunk-GLDPHKEW.mjs";
17
+ import "./chunk-IB6OCKZW.mjs";
18
18
  import {
19
19
  NoCurrentSprintError,
20
20
  SprintNotFoundError,
@@ -3,25 +3,25 @@ import {
3
3
  sprintPlanCommand,
4
4
  sprintRefineCommand,
5
5
  sprintStartCommand
6
- } from "./chunk-ORVGM6EV.mjs";
6
+ } from "./chunk-U62BX47C.mjs";
7
7
  import "./chunk-7LZ6GOGN.mjs";
8
8
  import {
9
9
  sprintCreateCommand
10
- } from "./chunk-V4ZUDZCG.mjs";
10
+ } from "./chunk-DUU5346E.mjs";
11
11
  import {
12
12
  addSingleTicketInteractive
13
- } from "./chunk-QYF7QIZJ.mjs";
13
+ } from "./chunk-742XQ7FL.mjs";
14
14
  import "./chunk-7TG3EAQ2.mjs";
15
- import "./chunk-7TBO6GOT.mjs";
15
+ import "./chunk-EUNAUHC3.mjs";
16
16
  import {
17
17
  getCurrentSprint,
18
18
  getSprint
19
- } from "./chunk-ITRZMBLJ.mjs";
19
+ } from "./chunk-JRFOUFD3.mjs";
20
20
  import {
21
21
  ensureError,
22
22
  wrapAsync
23
23
  } from "./chunk-OEUJDSHY.mjs";
24
- import "./chunk-GLDPHKEW.mjs";
24
+ import "./chunk-IB6OCKZW.mjs";
25
25
  import "./chunk-EDJX7TT6.mjs";
26
26
  import {
27
27
  colors,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ralphctl",
3
- "version": "0.2.3",
3
+ "version": "0.2.4",
4
4
  "description": "Agent harness for long-running AI coding tasks — orchestrates Claude Code & GitHub Copilot across repositories",
5
5
  "homepage": "https://github.com/lukas-grigis/ralphctl",
6
6
  "type": "module",
@@ -82,7 +82,16 @@
82
82
  },
83
83
  "evaluationOutput": {
84
84
  "type": "string",
85
- "description": "Output from the evaluation run (truncated to 2000 chars)"
85
+ "description": "Output from the evaluation run (truncated to 2000 chars; full critique in evaluationFile)"
86
+ },
87
+ "evaluationStatus": {
88
+ "type": "string",
89
+ "enum": ["passed", "failed", "malformed"],
90
+ "description": "Evaluation outcome discriminator. 'malformed' = evaluator output had no parseable signal."
91
+ },
92
+ "evaluationFile": {
93
+ "type": "string",
94
+ "description": "Sidecar file path containing the full untruncated critique"
86
95
  }
87
96
  }
88
97
  }