feat: add Tauri desktop app support and terminal management

This commit is contained in:
Mohamed Boudra
2026-01-20 10:09:35 +07:00
parent 12f617dd10
commit d9c487dd54
125 changed files with 8906 additions and 1049 deletions
+103 -66
View File
@@ -609,11 +609,28 @@ program
interface AgentConfig {
cli: "claude" | "codex";
model?: string;
effort?: string;
}
function getAgentConfig(model: ModelName): AgentConfig {
// Parse model string like "gpt-5.2-xhigh" into model and effort
function parseModelString(modelStr: string): { model: string; effort?: string } {
// GPT models with effort: gpt-5.2-low, gpt-5.2-medium, gpt-5.2-high, gpt-5.2-xhigh
const effortLevels = ["low", "medium", "high", "xhigh"];
for (const effort of effortLevels) {
if (modelStr.endsWith(`-${effort}`)) {
return {
model: modelStr.slice(0, -(effort.length + 1)),
effort,
};
}
}
return { model: modelStr };
}
function getAgentConfig(modelStr: string): AgentConfig {
const { model, effort } = parseModelString(modelStr);
if (model.startsWith("gpt-")) {
return { cli: "codex", model };
return { cli: "codex", model, effort };
}
// Claude CLI accepts aliases directly: haiku, sonnet, opus
return { cli: "claude", model };
@@ -621,10 +638,10 @@ function getAgentConfig(model: ModelName): AgentConfig {
function runAgentWithModel(
prompt: string,
model: ModelName,
modelStr: string,
logFile: string
): { success: boolean; output: string } {
const config = getAgentConfig(model);
const config = getAgentConfig(modelStr);
let args: string[];
if (config.cli === "claude") {
@@ -634,12 +651,13 @@ function runAgentWithModel(
}
args.push("-p", prompt);
} else {
const effort = config.effort ?? "medium";
args = [
"exec",
"--dangerously-bypass-approvals-and-sandbox",
"--skip-git-repo-check",
"-c",
"model_reasoning_effort=\"medium\"",
`model_reasoning_effort="${effort}"`,
];
if (config.model) {
args.push("--model", config.model);
@@ -686,28 +704,9 @@ async function buildTaskContext(task: Task, scopeId?: string): Promise<string> {
}
}
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}
# Task: ${task.title}
${task.body || "(no body)"}
${acceptanceCriteriaText}${notesText}`;
${task.raw}`;
}
function makePlannerPrompt(task: Task, context: string, reason: string): string {
@@ -917,36 +916,30 @@ 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)
## How to Work
Make MEANINGFUL progress toward the acceptance criteria:
- If the task can be completed in one iteration, do it all
- If it's too large, make the largest meaningful chunk of progress you can
- Read the notes from previous iterations - don't repeat work, BUILD on it
- A judge will verify your work against the acceptance criteria and leave feedback
The judge's feedback (in notes) tells you exactly what's still failing. Address it.
## Rules
- You CANNOT mark this task as done (a judge verifies completion)
- You MUST add a note documenting what you did: \`task note ${task.id} "WORKER: what you did"\`
- You CAN use \`task show ${task.id}\` to see full context
- You CAN use \`task children ${task.id}\` to see subtasks
- You CAN use \`task show ${task.id}\` to refresh context
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.
When you've made meaningful progress, 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}
${task.raw}
## Your Instructions
@@ -954,6 +947,28 @@ ${criteriaText}
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
## FAIL EARLY for NOT_DONE
For large tasks, the worker may yield after implementing incremental progress. Don't waste time
checking every criterion if it's obvious the task isn't complete.
**Fail early strategy:**
- Start with a quick smoke test (e.g., does \`npm run typecheck\` or \`npm test\` pass?)
- If the smoke test fails badly, you can immediately return NOT_DONE
- If you find ANY failing criterion, you can immediately return NOT_DONE
- No need to check all criteria if you already know the verdict is NOT_DONE
**Example:** If you run \`npm test\` and see 15 failures, don't methodically check each acceptance
criterion - just note the failures and return NOT_DONE.
## For DONE: Full Verification Required
Unlike NOT_DONE, you CANNOT shortcut DONE. To mark something DONE:
- You MUST verify EVERY acceptance criterion explicitly
- You MUST run the actual commands to check (not assume from context)
- You MUST confirm each criterion passes before concluding DONE
- No shortcuts, no assumptions, no "it probably works"
## CRITICAL: No Excuses Policy
You are evaluating RESULTS, not effort or intent. The following are NOT acceptable reasons to mark something DONE:
@@ -977,18 +992,38 @@ After verifying, you MUST leave feedback for the worker by adding a note. The wo
this note in the next iteration - it's their ONLY way to know what went wrong and what's missing.
\`\`\`
task note ${task.id} "JUDGE: [verdict] - [specific feedback for the worker]"
task note ${task.id} "JUDGE: [verdict] - [observed facts only]"
\`\`\`
Your feedback should tell the worker exactly what to fix:
- Which specific tests failed and with what error messages
- Which files or commands still need attention
- What's missing from the acceptance criteria
**CRITICAL: State ONLY observed facts. Do NOT provide solutions or debugging advice.**
BAD: "tests failed" (useless - worker doesn't know which ones)
GOOD: "3 tests fail: codex-mcp-agent.test.ts 'Session not found', opencode-agent.test.ts timeout after 60s, tool-calls.e2e.test.ts shell assertion"
You are a verification agent, not a debugging assistant. The worker has access to the task body
which contains the full context and instructions. Your job is to report WHAT failed, not HOW to fix it.
The worker cannot see your output - only the notes you leave.
DO:
- State which tests failed and their error messages
- State which commands returned non-zero exit codes
- State which files are missing or have wrong content
- Quote exact error output
DO NOT:
- Suggest fixes or solutions
- Explain why something might have failed
- Offer debugging strategies
- Recommend approaches or alternatives
BAD: "3 tests fail - try mocking the database connection"
BAD: "typecheck fails - you need to add the missing type annotation"
GOOD: "3 tests fail: auth.test.ts:42 'expected 200, got 401', user.test.ts:15 'timeout after 5000ms'"
GOOD: "npm run typecheck exit 1: src/api.ts:23 - Property 'foo' does not exist on type 'Bar'"
For non-test criteria, state observable facts about the code:
GOOD: "validateUser() in auth.ts:45-72 duplicates validateAdmin() in admin.ts:23-50 (criterion: no code duplication)"
GOOD: "processOrder() is 187 lines (criterion: functions under 50 lines)"
GOOD: "UserService calls database directly at line 34 (criterion: all DB access via repository layer)"
GOOD: "auth module exports JWT secret at line 12 (criterion: secrets not exported from modules)"
The worker has the task body with full instructions. Just tell them what's broken.
## Output Format - CRITICAL
@@ -1030,26 +1065,29 @@ program
.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("--judge <model>", "Judge model (default: haiku)", "haiku")
.option("--worker-model <model>", "Worker model (default: sonnet). For GPT models, append effort: gpt-5.2-high", "sonnet")
.option("--judge-model <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 enablePlanner = opts.plan;
const plannerModel = opts.planner as ModelName;
const judgeModel = opts.judge as ModelName;
const plannerModel = opts.planner as string;
const baseWorkerModel = opts.workerModel as string;
const judgeModel = opts.judgeModel as string;
const maxIterations = parseInt(opts.maxIterations, 10);
const watchMode = opts.watch;
const logFile = getLogFile();
process.stdout.write("Task Runner started (planner/worker/judge loop)\n");
process.stdout.write(`Planner: ${enablePlanner ? plannerModel : "disabled"}\n`);
process.stdout.write(`Worker: ${baseWorkerModel}\n`);
process.stdout.write(`Judge: ${judgeModel}\n`);
process.stdout.write(`Max iterations: ${maxIterations === 0 ? "unlimited" : maxIterations}\n`);
if (scopeId) process.stdout.write(`Scope: ${scopeId}\n`);
process.stdout.write(`Log: ${logFile}\n`);
process.stdout.write("\n");
log(logFile, `Started with planner=${enablePlanner ? plannerModel : "disabled"} judge=${judgeModel} maxIter=${maxIterations} scope=${scopeId || "all"}`);
log(logFile, `Started with planner=${enablePlanner ? plannerModel : "disabled"} worker=${baseWorkerModel} judge=${judgeModel} maxIter=${maxIterations} scope=${scopeId || "all"}`);
const runPlanner = async (task: Task, reason: string): Promise<boolean> => {
log(logFile, `[PLANNER] Running ${plannerModel} (${reason})...`);
@@ -1113,13 +1151,12 @@ program
}
const task = ready[0];
const baseModel = (
task.assignee === "codex" ? "gpt-5.2-codex" :
task.assignee ? task.assignee :
"sonnet"
) as ModelName;
let workerModel = baseModel;
const canUpgrade = baseModel === "sonnet"; // Only upgrade if starting from sonnet
// CLI --worker-model takes precedence, then task assignee, then default
let workerModel = baseWorkerModel;
if (!workerModel && task.assignee) {
workerModel = task.assignee === "codex" ? "gpt-5.2-codex" : task.assignee;
}
const canUpgrade = workerModel === "sonnet"; // Only upgrade if starting from sonnet
log(logFile, `\n=== Starting task: ${task.id} - ${task.title} ===`);
await store.start(task.id);
@@ -103,6 +103,20 @@ describe("FileTaskStore", () => {
expect(task.created >= before).toBe(true);
expect(task.created <= after).toBe(true);
});
it("returns task with raw content", async () => {
const task = await store.create("Test task", {
body: "Task body here",
acceptanceCriteria: ["criterion 1", "criterion 2"],
});
expect(task.raw).toContain("---");
expect(task.raw).toContain("title: Test task");
expect(task.raw).toContain("Task body here");
expect(task.raw).toContain("## Acceptance Criteria");
expect(task.raw).toContain("criterion 1");
expect(task.raw).toContain("criterion 2");
});
});
describe("get", () => {
@@ -125,6 +139,24 @@ describe("FileTaskStore", () => {
expect(retrieved?.assignee).toBe("codex");
});
it("returns raw content matching the file", async () => {
const created = await store.create("Task with body", {
body: "Some body content",
acceptanceCriteria: ["test passes", "lint passes"],
});
await store.addNote(created.id, "A note was added");
const retrieved = await store.get(created.id);
expect(retrieved?.raw).toContain("title: Task with body");
expect(retrieved?.raw).toContain("Some body content");
expect(retrieved?.raw).toContain("## Acceptance Criteria");
expect(retrieved?.raw).toContain("test passes");
expect(retrieved?.raw).toContain("lint passes");
expect(retrieved?.raw).toContain("## Notes");
expect(retrieved?.raw).toContain("A note was added");
});
});
describe("list", () => {
@@ -935,6 +967,29 @@ describe("FileTaskStore", () => {
expect(retrieved?.priority).toBe(3);
});
it("persists raw content across store instances", async () => {
const task = await store.create("Task with everything", {
body: "Detailed body",
acceptanceCriteria: ["tests pass", "build succeeds"],
assignee: "claude",
priority: 1,
});
await store.addNote(task.id, "Implementation note");
const store2 = new FileTaskStore(tempDir);
const retrieved = await store2.get(task.id);
expect(retrieved?.raw).toContain("title: Task with everything");
expect(retrieved?.raw).toContain("Detailed body");
expect(retrieved?.raw).toContain("## Acceptance Criteria");
expect(retrieved?.raw).toContain("tests pass");
expect(retrieved?.raw).toContain("build succeeds");
expect(retrieved?.raw).toContain("## Notes");
expect(retrieved?.raw).toContain("Implementation note");
expect(retrieved?.raw).toContain("assignee: claude");
expect(retrieved?.raw).toContain("priority: 1");
});
});
describe("parent-child hierarchy", () => {
+5 -1
View File
@@ -155,6 +155,7 @@ function parseTask(content: string): Task {
created: getValue("created") || new Date().toISOString(),
assignee: assignee || undefined,
priority,
raw: content,
};
}
@@ -387,10 +388,13 @@ export class FileTaskStore implements TaskStore {
created: new Date().toISOString(),
assignee: opts?.assignee,
priority: opts?.priority,
raw: "", // will be set after serialization
};
await this.writeTask(task);
return task;
// Re-read to get the raw content
const saved = await this.get(task.id);
return saved!;
}
async update(
+1
View File
@@ -25,6 +25,7 @@ export interface Task {
created: string; // ISO date
assignee?: AgentType; // optional agent override
priority?: number; // lower number = higher priority (1 is highest), tasks with priority sort before those without
raw: string; // the raw markdown file content
}
export interface CreateTaskOptions {