feat(task): add acceptance criteria, planner/worker/judge loop
- Add acceptanceCriteria field to tasks (structured checklist) - Add --accept flag to create/update commands (repeatable, append-only) - Add failed status for catastrophically stuck tasks - Implement planner/worker/judge agent loop in run command: - Planner (gpt-5.2): breaks down tasks, reorganizes tree, writes criteria - Worker (sonnet/codex): implements tasks, cannot mark done - Judge (haiku): verifies acceptance criteria with no-excuses policy - Hybrid replanning: after NOT_DONE and every N completions - Parent tasks blocked until all children are done - Add getDescendants method for children tree traversal - Update getReady/getBlocked/getClosed to use children tree for scope 🤖 Generated with [Claude Code](https://claude.com/claude-code)
This commit is contained in:
@@ -4,7 +4,7 @@ import { spawnSync } from "node:child_process";
|
||||
import { appendFileSync, existsSync, openSync, readFileSync } from "node:fs";
|
||||
import { resolve } from "node:path";
|
||||
import { FileTaskStore } from "./task-store.js";
|
||||
import type { AgentType, Task } from "./types.js";
|
||||
import type { AgentType, ModelName, Task } from "./types.js";
|
||||
|
||||
const TASKS_DIR = resolve(process.cwd(), ".tasks");
|
||||
const store = new FileTaskStore(TASKS_DIR);
|
||||
@@ -88,6 +88,7 @@ program
|
||||
.option("--parent <id>", "Parent task ID (for hierarchy)")
|
||||
.option("--assignee <agent>", "Agent to assign (claude or codex)")
|
||||
.option("--draft", "Create as draft (not actionable)")
|
||||
.option("-a, --accept <criterion>", "Acceptance criterion (repeatable)", (val: string, prev: string[]) => prev.concat(val), [] as string[])
|
||||
.action(async (title, opts) => {
|
||||
let body = opts.body ?? "";
|
||||
if (body === "-") {
|
||||
@@ -102,6 +103,7 @@ program
|
||||
parentId: opts.parent,
|
||||
status: opts.draft ? "draft" : "open",
|
||||
assignee: opts.assignee as AgentType | undefined,
|
||||
acceptanceCriteria: opts.accept,
|
||||
});
|
||||
|
||||
console.log(task.id);
|
||||
@@ -174,6 +176,12 @@ program
|
||||
if (task.body) {
|
||||
console.log(`\n${task.body}`);
|
||||
}
|
||||
if (task.acceptanceCriteria.length) {
|
||||
console.log("\n## Acceptance Criteria\n");
|
||||
for (const criterion of task.acceptanceCriteria) {
|
||||
console.log(`- [ ] ${criterion}`);
|
||||
}
|
||||
}
|
||||
if (task.notes.length) {
|
||||
console.log("\n## Notes");
|
||||
for (const note of task.notes) {
|
||||
@@ -301,6 +309,7 @@ program
|
||||
.option("-t, --title <text>", "New title")
|
||||
.option("-b, --body <text>", "New body (use '-' to read from stdin)")
|
||||
.option("--assignee <agent>", "New assignee (claude or codex)")
|
||||
.option("-a, --accept <criterion>", "Add acceptance criterion (repeatable, append-only)", (val: string, prev: string[]) => prev.concat(val), [] as string[])
|
||||
.action(async (id, opts) => {
|
||||
const task = await store.get(id);
|
||||
if (!task) {
|
||||
@@ -322,12 +331,19 @@ program
|
||||
changes.assignee = opts.assignee as AgentType;
|
||||
}
|
||||
|
||||
if (Object.keys(changes).length === 0) {
|
||||
// Add new acceptance criteria (append only)
|
||||
for (const criterion of opts.accept) {
|
||||
await store.addAcceptanceCriteria(id, criterion);
|
||||
}
|
||||
|
||||
if (Object.keys(changes).length === 0 && opts.accept.length === 0) {
|
||||
console.error("No changes specified");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
await store.update(id, changes);
|
||||
if (Object.keys(changes).length > 0) {
|
||||
await store.update(id, changes);
|
||||
}
|
||||
console.log(`Updated: ${id}`);
|
||||
});
|
||||
|
||||
@@ -410,14 +426,69 @@ program
|
||||
console.log(`${id} -> done`);
|
||||
});
|
||||
|
||||
// Agent runner
|
||||
async function makePrompt(
|
||||
task: Task,
|
||||
scopeId: string | undefined
|
||||
): Promise<string> {
|
||||
const scopeArg = scopeId ? ` --scope ${scopeId}` : "";
|
||||
program
|
||||
.command("fail <id>")
|
||||
.description("Mark as failed (catastrophically stuck)")
|
||||
.action(async (id) => {
|
||||
await store.fail(id);
|
||||
console.log(`${id} -> failed`);
|
||||
});
|
||||
|
||||
// Build parent context from ancestor chain
|
||||
// Agent runner
|
||||
|
||||
interface AgentConfig {
|
||||
cli: "claude" | "codex";
|
||||
model?: string;
|
||||
}
|
||||
|
||||
function getAgentConfig(model: ModelName): AgentConfig {
|
||||
if (model.startsWith("gpt-")) {
|
||||
return { cli: "codex", model };
|
||||
}
|
||||
// Claude CLI accepts aliases directly: haiku, sonnet, opus
|
||||
return { cli: "claude", model };
|
||||
}
|
||||
|
||||
function runAgentWithModel(
|
||||
prompt: string,
|
||||
model: ModelName,
|
||||
logFile: string
|
||||
): { success: boolean; output: string } {
|
||||
const config = getAgentConfig(model);
|
||||
let args: string[];
|
||||
|
||||
if (config.cli === "claude") {
|
||||
args = ["--dangerously-skip-permissions"];
|
||||
if (config.model) {
|
||||
args.push("--model", config.model);
|
||||
}
|
||||
args.push("-p", prompt);
|
||||
} else {
|
||||
args = [
|
||||
"exec",
|
||||
"--dangerously-bypass-approvals-and-sandbox",
|
||||
"--skip-git-repo-check",
|
||||
];
|
||||
if (config.model) {
|
||||
args.push("--model", config.model);
|
||||
}
|
||||
args.push(prompt);
|
||||
}
|
||||
|
||||
const fd = openSync(logFile, "a");
|
||||
const result = spawnSync(config.cli, args, {
|
||||
stdio: ["inherit", fd, fd],
|
||||
cwd: process.cwd(),
|
||||
maxBuffer: 50 * 1024 * 1024,
|
||||
});
|
||||
|
||||
// Read the output from the log file to check for judge verdict
|
||||
const output = existsSync(logFile) ? readFileSync(logFile, "utf-8") : "";
|
||||
|
||||
return { success: result.status === 0, output };
|
||||
}
|
||||
|
||||
async function buildTaskContext(task: Task, scopeId?: string): Promise<string> {
|
||||
const ancestors = await store.getAncestors(task.id);
|
||||
let parentContext = "";
|
||||
if (ancestors.length > 0) {
|
||||
@@ -436,54 +507,177 @@ async function makePrompt(
|
||||
if (scopeId && !ancestors.some((a) => a.id === scopeId)) {
|
||||
const scope = await store.get(scopeId);
|
||||
if (scope) {
|
||||
scopeContext = `Scope: ${scope.title} (${scopeId})
|
||||
${scope.body ? `\n${scope.body}\n` : ""}`;
|
||||
scopeContext = `Scope: ${scope.title} (${scopeId})\n${scope.body ? `\n${scope.body}\n` : ""}`;
|
||||
}
|
||||
}
|
||||
|
||||
let acceptanceCriteriaText = "";
|
||||
if (task.acceptanceCriteria.length > 0) {
|
||||
acceptanceCriteriaText = "\n## Acceptance Criteria\n\n";
|
||||
for (const criterion of task.acceptanceCriteria) {
|
||||
acceptanceCriteriaText += `- [ ] ${criterion}\n`;
|
||||
}
|
||||
}
|
||||
|
||||
let notesText = "";
|
||||
if (task.notes.length > 0) {
|
||||
notesText = "\n## Notes\n";
|
||||
for (const note of task.notes) {
|
||||
notesText += `\n**${note.timestamp}**\n${note.content}\n`;
|
||||
}
|
||||
}
|
||||
|
||||
return `Working directory: ${process.cwd()}
|
||||
${scopeContext}${parentContext}
|
||||
# YOUR TASK (${task.id}): ${task.title}
|
||||
# Task: ${task.title}
|
||||
|
||||
${task.body || "(no body)"}
|
||||
${acceptanceCriteriaText}${notesText}`;
|
||||
}
|
||||
|
||||
function makePlannerPrompt(task: Task, context: string, iteration: number): string {
|
||||
return `${context}
|
||||
|
||||
---
|
||||
|
||||
STEPS:
|
||||
1. UNDERSTAND CONTEXT FIRST - Before any implementation:
|
||||
- Run \`task show ${task.id}\` to see full context with parent chain
|
||||
- Run \`task children ${task.id}\` to see subtasks if any
|
||||
- Run \`task closed${scopeArg}\` to see completed sibling tasks
|
||||
- Understand what's been done, what decisions were made, what's planned
|
||||
2. Implement the task described above
|
||||
3. Add a note documenting what you did: \`task note ${task.id} "what you did"\`
|
||||
4. Mark complete: \`task close ${task.id}\`
|
||||
You are a PLANNER agent. Your job is to analyze the task tree and organize work to achieve the top-level goal.
|
||||
|
||||
COMMANDS:
|
||||
- \`task show <id>\` - view task details with parent context
|
||||
- \`task children <id>\` - list subtasks
|
||||
- \`task update <id> --body "..."\` - update task body (for grooming/clarifying)
|
||||
- \`task closed${scopeArg}\` - list completed tasks in scope
|
||||
- \`task note ${task.id} "content"\` - add a note to your task
|
||||
- \`task close ${task.id}\` - mark your task done
|
||||
Current iteration: ${iteration}
|
||||
|
||||
You MUST run \`task close ${task.id}\` when finished.
|
||||
You have FULL ACCESS to the task CLI and can reorganize the ENTIRE task tree under the scope:
|
||||
- \`task show ${task.id}\` - view task details with parent context
|
||||
- \`task list\` - see all tasks
|
||||
- \`task tree ${task.id}\` - see full task hierarchy
|
||||
- \`task children ${task.id}\` - list subtasks
|
||||
- \`task ready\` - see what's ready to work on
|
||||
- \`task create "title" --parent <id> --body "..." --accept "criterion"\` - create task (any level)
|
||||
- \`task update <id> --title "..." --body "..." --accept "criterion"\` - update task
|
||||
- \`task note <id> "content"\` - add planning notes
|
||||
- \`task delete <id>\` - remove a task that's no longer needed
|
||||
- \`task fail <id>\` - mark task as failed if catastrophically stuck
|
||||
|
||||
## Your Scope
|
||||
|
||||
You can reorganize ANY task under this scope. The TOP-LEVEL task's acceptance criteria are IMMUTABLE - they are the north star. Everything else can be:
|
||||
- Broken down into subtasks
|
||||
- Deleted if no longer relevant
|
||||
- Reordered/reprioritized
|
||||
- Updated with better acceptance criteria
|
||||
|
||||
Always keep the top-level goal in mind when reorganizing.
|
||||
|
||||
## Writing Good Acceptance Criteria
|
||||
|
||||
Acceptance criteria MUST be:
|
||||
- **Verifiable**: Can be checked programmatically or with a clear command (e.g., "npm run test passes", "file X exists", "API returns 200")
|
||||
- **Objective**: No subjective judgments like "code is clean" or "good performance" - use measurable thresholds
|
||||
- **Specific**: Reference exact files, functions, endpoints, or behaviors
|
||||
- **Complete**: Cover edge cases, error handling, and integration points
|
||||
|
||||
Bad examples:
|
||||
- "Code is well-written" (subjective)
|
||||
- "Feature works" (vague)
|
||||
- "Tests pass" (which tests?)
|
||||
|
||||
Good examples:
|
||||
- "npm run test passes with 0 failures"
|
||||
- "GET /api/users returns 200 with JSON array"
|
||||
- "File src/utils/parser.ts exports parseConfig function"
|
||||
- "Running \`node cli.js --help\` prints usage information"
|
||||
|
||||
## Your Options
|
||||
|
||||
1. If the current task is simple enough to implement directly, add a note explaining why and exit
|
||||
2. If it needs breakdown, create subtasks with clear, verifiable acceptance criteria
|
||||
3. If other tasks in the tree need reorganization based on what's been learned, do that
|
||||
4. If a task is catastrophically stuck with no clear path forward (repeated failures WITHOUT progress), mark it failed
|
||||
|
||||
Note: Multiple iterations are fine if there's progress. Only mark failed if truly stuck with no way forward.
|
||||
|
||||
DO NOT implement tasks. Only plan and organize.
|
||||
When done planning, simply exit. Do not mark tasks as done.
|
||||
`;
|
||||
}
|
||||
|
||||
function runAgent(prompt: string, agent: AgentType, logFile: string): boolean {
|
||||
const args =
|
||||
agent === "claude"
|
||||
? ["--dangerously-skip-permissions", "-p", prompt]
|
||||
: ["exec", "--dangerously-bypass-approvals-and-sandbox", prompt];
|
||||
function makeWorkerPrompt(task: Task, context: string, iteration: number): string {
|
||||
return `${context}
|
||||
|
||||
const fd = openSync(logFile, "a");
|
||||
const result = spawnSync(agent, args, {
|
||||
stdio: ["inherit", fd, fd],
|
||||
cwd: process.cwd(),
|
||||
});
|
||||
---
|
||||
|
||||
return result.status === 0;
|
||||
You are a WORKER agent implementing this task.
|
||||
|
||||
Current iteration: ${iteration}
|
||||
|
||||
IMPORTANT RULES:
|
||||
- You CANNOT mark this task as done (task close is forbidden for you)
|
||||
- You MUST add a note documenting what you did: \`task note ${task.id} "what you did"\`
|
||||
- You CAN use \`task show ${task.id}\` to see full context
|
||||
- You CAN use \`task children ${task.id}\` to see subtasks
|
||||
|
||||
Focus on implementing the task. The acceptance criteria will be verified by a separate judge agent.
|
||||
|
||||
When you've done your best work, add a note explaining what you did and exit.
|
||||
`;
|
||||
}
|
||||
|
||||
function makeJudgePrompt(task: Task): string {
|
||||
let criteriaText = "";
|
||||
if (task.acceptanceCriteria.length > 0) {
|
||||
criteriaText = "## Acceptance Criteria to Verify\n\n";
|
||||
for (let i = 0; i < task.acceptanceCriteria.length; i++) {
|
||||
criteriaText += `${i + 1}. ${task.acceptanceCriteria[i]}\n`;
|
||||
}
|
||||
} else {
|
||||
criteriaText = "No explicit acceptance criteria defined. Verify the task is reasonably complete based on the title and body.\n";
|
||||
}
|
||||
|
||||
return `You are a JUDGE agent. Your ONLY job is to verify if acceptance criteria are met.
|
||||
|
||||
# Task: ${task.title}
|
||||
|
||||
${task.body || "(no body)"}
|
||||
|
||||
${criteriaText}
|
||||
|
||||
## Your Instructions
|
||||
|
||||
1. For EACH criterion, verify if it is satisfied by running commands and checking actual state
|
||||
2. You may run commands to check (e.g., \`npm run test\`, \`npm run typecheck\`, check file existence, run the code)
|
||||
3. After checking all criteria, output your verdict
|
||||
|
||||
## CRITICAL: No Excuses Policy
|
||||
|
||||
You are evaluating RESULTS, not effort or intent. The following are NOT acceptable reasons to mark something DONE:
|
||||
|
||||
- "The agent tried their best" - IRRELEVANT
|
||||
- "It mostly works" - NOT_DONE
|
||||
- "The environment wasn't set up correctly" - NOT_DONE (setup is part of the task)
|
||||
- "This feature requires X which isn't available" - NOT_DONE (making it available is part of the task)
|
||||
- "The agent documented why it couldn't complete" - NOT_DONE
|
||||
- "It's a good start" - NOT_DONE
|
||||
- "The core functionality works" - Check ALL criteria, not just "core"
|
||||
- "This is blocked by external factors" - NOT_DONE
|
||||
|
||||
The ONLY question: Does the acceptance criterion pass when verified? Yes or No.
|
||||
|
||||
If an agent left notes explaining why something couldn't be done, IGNORE THE EXPLANATION. Just check: is the criterion met?
|
||||
|
||||
## Output Format
|
||||
|
||||
After verification, output EXACTLY one of these XML tags:
|
||||
|
||||
If ALL criteria pass verification:
|
||||
<VERDICT>DONE</VERDICT>
|
||||
|
||||
If ANY criterion fails verification:
|
||||
<VERDICT>NOT_DONE</VERDICT>
|
||||
<FAILED_CRITERIA>
|
||||
- criterion that failed: what you checked and what you found
|
||||
</FAILED_CRITERIA>
|
||||
|
||||
Then add a note to the task with your findings:
|
||||
\`task note ${task.id} "Judge verdict: [DONE/NOT_DONE]. Details: ..."\`
|
||||
`;
|
||||
}
|
||||
|
||||
function getLogFile(): string {
|
||||
@@ -497,77 +691,171 @@ function getLogFile(): string {
|
||||
function log(logFile: string, message: string): void {
|
||||
const timestamp = new Date().toISOString();
|
||||
appendFileSync(logFile, `[${timestamp}] ${message}\n`);
|
||||
console.log(`[${timestamp}] ${message}`);
|
||||
}
|
||||
|
||||
function parseJudgeVerdict(output: string): "DONE" | "NOT_DONE" | null {
|
||||
const match = output.match(/<VERDICT>(DONE|NOT_DONE)<\/VERDICT>/);
|
||||
return match ? (match[1] as "DONE" | "NOT_DONE") : null;
|
||||
}
|
||||
|
||||
program
|
||||
.command("run [scope]")
|
||||
.description("Run agent loop on tasks")
|
||||
.option("--agent <type>", "Agent to use (claude or codex)", "claude")
|
||||
.description("Run agent loop on tasks with planner/worker/judge")
|
||||
.option("--plan", "Enable planner agent")
|
||||
.option("--planner <model>", "Planner model (default: gpt-5.2)", "gpt-5.2")
|
||||
.option("--replan <n>", "Run planner every N completed tasks (default: 3)", "3")
|
||||
.option("--judge <model>", "Judge model (default: haiku)", "haiku")
|
||||
.option("--max-iterations <n>", "Max worker/judge iterations per task (0 = no limit)", "0")
|
||||
.option("-w, --watch", "Keep running and wait for new tasks")
|
||||
.action(async (scopeId: string | undefined, opts) => {
|
||||
const defaultAgent = opts.agent as AgentType;
|
||||
const enablePlanner = opts.plan;
|
||||
const plannerModel = opts.planner as ModelName;
|
||||
const replanInterval = parseInt(opts.replan, 10);
|
||||
const judgeModel = opts.judge as ModelName;
|
||||
const maxIterations = parseInt(opts.maxIterations, 10);
|
||||
const watchMode = opts.watch;
|
||||
const logFile = getLogFile();
|
||||
|
||||
console.log("Task Runner started");
|
||||
console.log(`Agent: ${defaultAgent}`);
|
||||
console.log("Task Runner started (planner/worker/judge loop)");
|
||||
console.log(`Planner: ${enablePlanner ? `${plannerModel} (replan every ${replanInterval} tasks)` : "disabled"}`);
|
||||
console.log(`Judge: ${judgeModel}`);
|
||||
console.log(`Max iterations: ${maxIterations === 0 ? "unlimited" : maxIterations}`);
|
||||
if (scopeId) console.log(`Scope: ${scopeId}`);
|
||||
console.log(`Log: ${logFile}`);
|
||||
console.log("");
|
||||
|
||||
log(logFile, `Started with agent=${defaultAgent} scope=${scopeId || "all"}`);
|
||||
log(logFile, `Started with planner=${enablePlanner ? plannerModel : "disabled"} replan=${replanInterval} judge=${judgeModel} maxIter=${maxIterations} scope=${scopeId || "all"}`);
|
||||
|
||||
const MAX_RETRIES = 3;
|
||||
let completedSinceReplan = 0;
|
||||
|
||||
const runLoop = async (): Promise<void> => {
|
||||
const runPlanner = async (task: Task, reason: string): Promise<boolean> => {
|
||||
log(logFile, `[PLANNER] Running ${plannerModel} (${reason})...`);
|
||||
const context = await buildTaskContext(task, scopeId);
|
||||
const plannerPrompt = makePlannerPrompt(task, context, 1);
|
||||
runAgentWithModel(plannerPrompt, plannerModel, logFile);
|
||||
|
||||
// Check if planner created subtasks for this task
|
||||
const children = await store.getChildren(task.id);
|
||||
if (children.some((c) => c.status !== "done")) {
|
||||
log(logFile, `[PLANNER] Task has pending children, will process those first`);
|
||||
return true; // Signal to restart loop
|
||||
}
|
||||
|
||||
// Check if planner marked task as failed
|
||||
const updatedTask = await store.get(task.id);
|
||||
if (updatedTask?.status === "failed") {
|
||||
log(logFile, `[PLANNER] Marked task as failed`);
|
||||
return true; // Signal to continue to next task
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
const runTaskLoop = async (): Promise<void> => {
|
||||
while (true) {
|
||||
// Find ready tasks
|
||||
const ready = await store.getReady(scopeId);
|
||||
if (ready.length === 0) break;
|
||||
|
||||
const task = ready[0];
|
||||
const agent = task.assignee || defaultAgent;
|
||||
|
||||
console.log(`⏳ ${task.title} [${agent}]`);
|
||||
log(logFile, `Starting: ${task.id} - ${task.title} [${agent}]`);
|
||||
const workerModel = (task.assignee === "codex" ? "gpt-5.2" : "sonnet") as ModelName;
|
||||
|
||||
log(logFile, `\n=== Starting task: ${task.id} - ${task.title} ===`);
|
||||
await store.start(task.id);
|
||||
const prompt = await makePrompt(task, scopeId);
|
||||
|
||||
let attempt = 1;
|
||||
let success = false;
|
||||
|
||||
while (attempt <= MAX_RETRIES) {
|
||||
success = runAgent(prompt, agent, logFile);
|
||||
if (success) break;
|
||||
|
||||
if (attempt < MAX_RETRIES) {
|
||||
const backoff = attempt * 10;
|
||||
console.log(`⚠️ Attempt ${attempt} failed, retrying in ${backoff}s...`);
|
||||
log(logFile, `Attempt ${attempt} failed, retrying in ${backoff}s`);
|
||||
await new Promise((r) => setTimeout(r, backoff * 1000));
|
||||
// Step 1: Initial planner run (if enabled)
|
||||
if (enablePlanner) {
|
||||
const shouldRestart = await runPlanner(task, "initial planning");
|
||||
if (shouldRestart) {
|
||||
// Reset task to open so we process children first
|
||||
const currentTask = await store.get(task.id);
|
||||
if (currentTask && currentTask.status === "in_progress") {
|
||||
await store.update(task.id, { status: "open" });
|
||||
}
|
||||
continue;
|
||||
}
|
||||
attempt++;
|
||||
}
|
||||
|
||||
if (!success) {
|
||||
console.log(`❌ ${task.title} (failed after ${MAX_RETRIES} attempts)`);
|
||||
log(logFile, `Failed: ${task.id} after ${MAX_RETRIES} attempts`);
|
||||
process.exit(1);
|
||||
// Step 2: Worker/Judge loop
|
||||
let iteration = 1;
|
||||
let taskDone = false;
|
||||
|
||||
while ((maxIterations === 0 || iteration <= maxIterations) && !taskDone) {
|
||||
const iterLabel = maxIterations === 0 ? `${iteration}` : `${iteration}/${maxIterations}`;
|
||||
log(logFile, `[WORKER] Iteration ${iterLabel} with ${workerModel}...`);
|
||||
|
||||
// Refresh task context (notes may have been added)
|
||||
const freshTask = await store.get(task.id);
|
||||
if (!freshTask) break;
|
||||
|
||||
const freshContext = await buildTaskContext(freshTask, scopeId);
|
||||
const workerPrompt = makeWorkerPrompt(freshTask, freshContext, iteration);
|
||||
runAgentWithModel(workerPrompt, workerModel, logFile);
|
||||
|
||||
// Step 3: Judge
|
||||
log(logFile, `[JUDGE] Verifying with ${judgeModel}...`);
|
||||
const judgeTask = await store.get(task.id);
|
||||
if (!judgeTask) break;
|
||||
|
||||
const judgePrompt = makeJudgePrompt(judgeTask);
|
||||
const judgeResult = runAgentWithModel(judgePrompt, judgeModel, logFile);
|
||||
|
||||
const verdict = parseJudgeVerdict(judgeResult.output);
|
||||
log(logFile, `[JUDGE] Verdict: ${verdict || "UNKNOWN"}`);
|
||||
|
||||
if (verdict === "DONE") {
|
||||
await store.close(task.id);
|
||||
log(logFile, `✅ Task ${task.id} completed`);
|
||||
taskDone = true;
|
||||
completedSinceReplan++;
|
||||
|
||||
// Periodic replanning after N completions
|
||||
if (enablePlanner && completedSinceReplan >= replanInterval) {
|
||||
completedSinceReplan = 0;
|
||||
const nextReady = await store.getReady(scopeId);
|
||||
if (nextReady.length > 0) {
|
||||
log(logFile, `[PLANNER] Periodic replan after ${replanInterval} completions`);
|
||||
// Run planner on the scope root or next ready task to reassess
|
||||
const scopeTask = scopeId ? await store.get(scopeId) : nextReady[0];
|
||||
if (scopeTask) {
|
||||
await runPlanner(scopeTask, "periodic reassessment");
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// NOT_DONE - run planner to reassess before retry
|
||||
if (enablePlanner) {
|
||||
log(logFile, `[JUDGE] NOT_DONE - triggering planner reassessment`);
|
||||
const currentTask = await store.get(task.id);
|
||||
if (currentTask) {
|
||||
const shouldRestart = await runPlanner(currentTask, `NOT_DONE iteration ${iteration}`);
|
||||
if (shouldRestart) {
|
||||
// Planner created subtasks or marked failed, restart the main loop
|
||||
const updatedTask = await store.get(task.id);
|
||||
if (updatedTask && updatedTask.status === "in_progress") {
|
||||
await store.update(task.id, { status: "open" });
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
iteration++;
|
||||
}
|
||||
}
|
||||
|
||||
// Check if agent closed the task
|
||||
const updated = await store.get(task.id);
|
||||
if (updated?.status !== "done") {
|
||||
console.log(`⚠️ Agent did not close task ${task.id}`);
|
||||
log(logFile, `Warning: agent did not close task ${task.id}`);
|
||||
if (!taskDone && maxIterations > 0) {
|
||||
const finalTask = await store.get(task.id);
|
||||
if (finalTask && finalTask.status === "in_progress") {
|
||||
log(logFile, `⚠️ Task ${task.id} not completed after ${maxIterations} iterations`);
|
||||
// Reset to open so it can be picked up again (planner may have adjusted things)
|
||||
await store.update(task.id, { status: "open" });
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`✅ ${task.title}`);
|
||||
log(logFile, `Completed: ${task.id}`);
|
||||
}
|
||||
};
|
||||
|
||||
await runLoop();
|
||||
await runTaskLoop();
|
||||
|
||||
if (watchMode) {
|
||||
console.log("💤 Waiting for new tasks...");
|
||||
@@ -575,7 +863,7 @@ program
|
||||
await new Promise((r) => setTimeout(r, 5000));
|
||||
const ready = await store.getReady(scopeId);
|
||||
if (ready.length > 0) {
|
||||
await runLoop();
|
||||
await runTaskLoop();
|
||||
console.log("💤 Waiting for new tasks...");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user