@ofear/xdou 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js ADDED
@@ -0,0 +1,1112 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/cli.ts
4
+ import { Command, Flags } from "@oclif/core";
5
+ import fs5 from "fs-extra";
6
+ import pc2 from "picocolors";
7
+ import Table from "cli-table3";
8
+ import YAML2 from "yaml";
9
+ import { join as join6 } from "path";
10
+
11
+ // src/orchestrator.ts
12
+ import pc from "picocolors";
13
+ import fs4 from "fs-extra";
14
+ import { join as join5 } from "path";
15
+
16
+ // src/core/artifact-store.ts
17
+ import fs from "fs-extra";
18
+ import writeFileAtomic from "write-file-atomic";
19
+ import { join, resolve } from "path";
20
+ import { randomUUID } from "crypto";
21
+ var ArtifactStore = class {
22
+ root;
23
+ constructor(root) {
24
+ this.root = root;
25
+ }
26
+ runDir(runId) {
27
+ return join(this.root, "runs", runId);
28
+ }
29
+ async createRun(mission) {
30
+ const id = `${(/* @__PURE__ */ new Date()).toISOString().replace(/[-:.TZ]/g, "").slice(0, 14)}-${randomUUID().slice(0, 8)}`;
31
+ const dir = this.runDir(id);
32
+ await fs.ensureDir(dir);
33
+ const manifest = { id, mission, createdAt: (/* @__PURE__ */ new Date()).toISOString(), updatedAt: (/* @__PURE__ */ new Date()).toISOString(), status: "created", phase: "created", artifactDir: dir, events: 0 };
34
+ await this.writeJson(id, "manifest.json", manifest);
35
+ await this.writeText(id, "mission.md", `# Mission
36
+
37
+ ${mission}
38
+ `);
39
+ await this.appendEvent(id, { type: "run.created", by: "xdou", mission });
40
+ return manifest;
41
+ }
42
+ artifactPath(runId, relativePath) {
43
+ const root = resolve(this.runDir(runId));
44
+ const target = resolve(root, relativePath);
45
+ if (target !== root && !target.startsWith(`${root}\\`) && !target.startsWith(`${root}/`)) throw new Error(`Artifact path escapes run directory: ${relativePath}`);
46
+ return target;
47
+ }
48
+ async writeText(runId, relativePath, content) {
49
+ const path = this.artifactPath(runId, relativePath);
50
+ await fs.ensureDir(join(path, ".."));
51
+ await writeFileAtomic(path, content, "utf8");
52
+ return path;
53
+ }
54
+ async writeJson(runId, relativePath, value) {
55
+ return this.writeText(runId, relativePath, `${JSON.stringify(value, null, 2)}
56
+ `);
57
+ }
58
+ async readManifest(runId) {
59
+ return fs.readJson(join(this.runDir(runId), "manifest.json"));
60
+ }
61
+ async updateManifest(runId, patch) {
62
+ const current = await this.readManifest(runId);
63
+ const next = { ...current, ...patch, updatedAt: (/* @__PURE__ */ new Date()).toISOString() };
64
+ await this.writeJson(runId, "manifest.json", next);
65
+ return next;
66
+ }
67
+ async appendEvent(runId, event) {
68
+ const path = join(this.runDir(runId), "timeline.ndjson");
69
+ await fs.ensureDir(join(path, ".."));
70
+ const enriched = { time: (/* @__PURE__ */ new Date()).toISOString(), ...event };
71
+ await fs.appendFile(path, `${JSON.stringify(enriched)}
72
+ `, "utf8");
73
+ if (event.type !== "run.created") {
74
+ const manifestPath = join(this.runDir(runId), "manifest.json");
75
+ if (await fs.pathExists(manifestPath)) {
76
+ const manifest = await this.readManifest(runId);
77
+ await this.writeJson(runId, "manifest.json", { ...manifest, events: manifest.events + 1, updatedAt: (/* @__PURE__ */ new Date()).toISOString() });
78
+ }
79
+ }
80
+ }
81
+ async latestRunId() {
82
+ const runs = await this.listRuns();
83
+ return runs.at(-1)?.id;
84
+ }
85
+ async listRuns() {
86
+ const runsDir = join(this.root, "runs");
87
+ if (!await fs.pathExists(runsDir)) return [];
88
+ const entries = await fs.readdir(runsDir);
89
+ const manifests = [];
90
+ for (const entry of entries.sort()) {
91
+ const manifestPath = join(runsDir, entry, "manifest.json");
92
+ if (await fs.pathExists(manifestPath)) manifests.push(await fs.readJson(manifestPath));
93
+ }
94
+ return manifests;
95
+ }
96
+ async abortRun(runId, reason) {
97
+ await this.appendEvent(runId, { type: "run.aborted", by: "xdou", reason });
98
+ return this.updateManifest(runId, { status: "aborted", phase: "aborted", abortedReason: reason });
99
+ }
100
+ async recoverStaleRuns(staleAfterMs = 5 * 6e4) {
101
+ const now = Date.now();
102
+ const recovered = [];
103
+ for (const run of await this.listRuns()) {
104
+ if (run.status !== "running") continue;
105
+ const updatedAt = Date.parse(run.updatedAt);
106
+ const isStale = Number.isFinite(updatedAt) && now - updatedAt >= staleAfterMs;
107
+ const pidAlive = run.processPid ? this.isPidAlive(run.processPid) : false;
108
+ if (!pidAlive && isStale) recovered.push(await this.abortRun(run.id, run.processPid ? `process ${run.processPid} is not running` : "running manifest is stale and has no live process pid"));
109
+ }
110
+ return recovered;
111
+ }
112
+ isPidAlive(pid) {
113
+ try {
114
+ process.kill(pid, 0);
115
+ return true;
116
+ } catch {
117
+ return false;
118
+ }
119
+ }
120
+ };
121
+
122
+ // src/core/context-compiler.ts
123
+ function list(title, items) {
124
+ if (!items?.length) return [];
125
+ return [title, ...items.map((item) => `- ${item}`), ""];
126
+ }
127
+ function compileContextPacket(input) {
128
+ const lines = [
129
+ `XDOU CONTEXT PACKET`,
130
+ `RUN: ${input.runId}`,
131
+ `AGENT: ${input.agent}`,
132
+ `ROLE: ${input.role}`,
133
+ `BUDGET: ${input.budget ?? "balanced"}`,
134
+ "",
135
+ "MISSION:",
136
+ input.mission,
137
+ ""
138
+ ];
139
+ if (input.projectContext && input.budget !== "minimal") lines.push("PROJECT CONTEXT:", input.projectContext, "");
140
+ if (input.plan) lines.push("CANONICAL PLAN:", input.plan, "");
141
+ if (input.task) {
142
+ lines.push(`TASK ${input.task.id}: ${input.task.title}`, input.task.objective);
143
+ if (input.task.files?.length) lines.push("Files:", ...input.task.files.map((f) => `- ${f}`));
144
+ if (input.task.validation?.length) lines.push("Validation:", ...input.task.validation.map((v) => `- ${v}`));
145
+ lines.push("");
146
+ }
147
+ lines.push(...list("ACCEPTED DECISIONS:", input.decisions));
148
+ lines.push(...list("REJECTED APPROACHES:", input.rejected));
149
+ lines.push(...list("KNOWN RISKS:", input.risks));
150
+ if (input.diff) lines.push("DIFF TO REVIEW:", input.diff, "");
151
+ if (input.validation) lines.push("VALIDATION RESULT:", `Command: ${input.validation.command}`, `Status: ${input.validation.status}`, input.validation.output, "");
152
+ if (input.role === "reviewer") {
153
+ lines.push(
154
+ "SEMANTIC REVIEW CONTRACT:",
155
+ "You are the semantic completion gate. Decide whether the implementation satisfies the mission, not merely whether tests pass.",
156
+ "End your response with exactly one machine-readable verdict block:",
157
+ "REVIEW_VERDICT:",
158
+ '{"verdict":"approve|request_changes|blocked","confidence":0.0,"reason":"one sentence","missingRequirements":["requirement not satisfied"]}',
159
+ "Use request_changes when the diff/tests are green but the mission is semantically incomplete or incorrect.",
160
+ ""
161
+ );
162
+ }
163
+ if (input.budget === "full" && input.transcript) lines.push("RAW TRANSCRIPT FOR DEBUGGING ONLY:", input.transcript, "");
164
+ lines.push("OUTPUT CONTRACT:", "Return concise structured markdown with: Summary, Files changed/reviewed, Tests run, Issues/risks, Recommended next step.");
165
+ return lines.join("\n");
166
+ }
167
+
168
+ // src/core/repo.ts
169
+ import { execa } from "execa";
170
+ import fs2 from "fs-extra";
171
+ import { join as join2, dirname } from "path";
172
+ async function isGitRepo(cwd) {
173
+ const result = await execa("git", ["rev-parse", "--is-inside-work-tree"], { cwd, reject: false });
174
+ return result.exitCode === 0 && result.stdout.trim() === "true";
175
+ }
176
+ async function ensureGitRepo(cwd) {
177
+ if (!await isGitRepo(cwd)) throw new Error("xdou must run inside a git repository. Run git init first.");
178
+ }
179
+ async function isWorkingTreeClean(cwd) {
180
+ const result = await execa("git", ["status", "--porcelain"], { cwd, reject: false });
181
+ return result.exitCode === 0 && result.stdout.trim().length === 0;
182
+ }
183
+ async function ensureCleanWorkingTree(cwd) {
184
+ if (!await isWorkingTreeClean(cwd)) throw new Error("Refusing to run coding agents on a dirty working tree. Commit/stash changes first, or run planning/brainstorming only.");
185
+ }
186
+ async function gitDiff(cwd) {
187
+ const tracked = await execa("git", ["diff", "HEAD", "--", "."], { cwd, reject: false });
188
+ const untracked = await execa("git", ["ls-files", "--others", "--exclude-standard"], { cwd, reject: false });
189
+ const patches = [tracked.stdout].filter(Boolean);
190
+ for (const file of untracked.stdout.split(/\r?\n/).filter(Boolean)) {
191
+ const path = join2(cwd, file);
192
+ const content = await fs2.readFile(path, "utf8").catch(() => void 0);
193
+ if (content === void 0) continue;
194
+ const lines = content.split(/\r?\n/);
195
+ if (lines.at(-1) === "") lines.pop();
196
+ patches.push([
197
+ `diff --git a/${file} b/${file}`,
198
+ "new file mode 100644",
199
+ "index 0000000..0000000",
200
+ "--- /dev/null",
201
+ `+++ b/${file}`,
202
+ `@@ -0,0 +1,${lines.length} @@`,
203
+ ...lines.map((line) => `+${line}`)
204
+ ].join("\n"));
205
+ }
206
+ return patches.join("\n\n");
207
+ }
208
+ async function currentHead(cwd) {
209
+ const r = await execa("git", ["rev-parse", "HEAD"], { cwd });
210
+ return r.stdout.trim();
211
+ }
212
+ async function createRunWorktree(repoRoot, runId, artifactDir = ".xdou") {
213
+ const baseRef = await currentHead(repoRoot);
214
+ const worktreePath = join2(repoRoot, artifactDir, "worktrees", runId);
215
+ await fs2.remove(worktreePath);
216
+ await fs2.ensureDir(join2(worktreePath, ".."));
217
+ await execa("git", ["worktree", "add", "--detach", worktreePath, baseRef], { cwd: repoRoot });
218
+ return { cwd: worktreePath, worktreePath, baseRef };
219
+ }
220
+ async function createProjectSnapshot(repoRoot, snapshotPath) {
221
+ await fs2.remove(snapshotPath);
222
+ await fs2.ensureDir(snapshotPath);
223
+ const tracked = await execa("git", ["ls-files", "-z"], { cwd: repoRoot });
224
+ const files = tracked.stdout.split("\0").filter(Boolean);
225
+ for (const file of files) {
226
+ const source = join2(repoRoot, file);
227
+ const target = join2(snapshotPath, file);
228
+ await fs2.ensureDir(dirname(target));
229
+ await fs2.copyFile(source, target).catch(() => void 0);
230
+ }
231
+ return snapshotPath;
232
+ }
233
+ async function applyPatch(cwd, patch) {
234
+ if (!patch.trim() || patch.trim() === "No diff produced.") throw new Error("Run has no diff to apply.");
235
+ await ensureCleanWorkingTree(cwd);
236
+ const files = [...patch.matchAll(/^diff --git a\/(.*?) b\/(.*?)$/gm)].map((match) => match[2]).filter((file) => Boolean(file));
237
+ const normalizedPatch = patch.endsWith("\n") ? patch : `${patch}
238
+ `;
239
+ await execa("git", ["apply", "--check", "-"], { cwd, input: normalizedPatch });
240
+ await execa("git", ["apply", "-"], { cwd, input: normalizedPatch });
241
+ return { filesChanged: new Set(files).size, files: [...new Set(files)] };
242
+ }
243
+ async function repoSummary(cwd) {
244
+ const files = ["package.json", "pyproject.toml", "Cargo.toml", "go.mod", "README.md"];
245
+ const parts = [];
246
+ for (const file of files) if (await fs2.pathExists(join2(cwd, file))) parts.push(`## ${file}
247
+ ${await fs2.readFile(join2(cwd, file), "utf8")}`);
248
+ return parts.join("\n\n").slice(0, 24e3);
249
+ }
250
+
251
+ // src/core/mission-check.ts
252
+ var IGNORED_SYMBOLS = /* @__PURE__ */ new Set(["add", "update", "print", "test", "run", "export", "import"]);
253
+ function expectedSymbolsFromMission(mission) {
254
+ const symbols = /* @__PURE__ */ new Set();
255
+ for (const match of mission.matchAll(/\b([A-Za-z_$][\w$]*)\s*\(/g)) {
256
+ const symbol = match[1];
257
+ if (symbol && !IGNORED_SYMBOLS.has(symbol.toLowerCase())) symbols.add(symbol);
258
+ }
259
+ return [...symbols];
260
+ }
261
+ function checkMissionCompletion(mission, diff) {
262
+ const expectedSymbols = expectedSymbolsFromMission(mission);
263
+ const effectiveDiff = diff.trim() === "No diff produced." ? "" : diff;
264
+ if (!expectedSymbols.length) {
265
+ return { status: effectiveDiff.trim() ? "passed" : "skipped", expectedSymbols, missingSymbols: [], message: effectiveDiff.trim() ? "No explicit function symbols found in mission; non-empty diff produced." : "No explicit function symbols found in mission." };
266
+ }
267
+ const missingSymbols = expectedSymbols.filter((symbol) => !new RegExp(`\\b${escapeRegExp(symbol)}\\b`).test(effectiveDiff));
268
+ return {
269
+ status: missingSymbols.length ? "failed" : "passed",
270
+ expectedSymbols,
271
+ missingSymbols,
272
+ message: missingSymbols.length ? `Produced diff is missing mission symbol(s): ${missingSymbols.join(", ")}` : "Produced diff contains all explicit mission symbols."
273
+ };
274
+ }
275
+ function escapeRegExp(value) {
276
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
277
+ }
278
+
279
+ // src/core/acceptance-tests.ts
280
+ import { execa as execa2 } from "execa";
281
+ import { pathToFileURL } from "url";
282
+ import { join as join3, normalize } from "path";
283
+ var OPERATION_CASES = {
284
+ add: [{ args: [2, 3], expected: 5 }, { args: [-1, 4], expected: 3 }],
285
+ subtract: [{ args: [9, 4], expected: 5 }, { args: [1, 5], expected: -4 }],
286
+ multiply: [{ args: [3, 4], expected: 12 }, { args: [-2, 5], expected: -10 }],
287
+ divide: [{ args: [8, 2], expected: 4 }, { args: [9, 3], expected: 3 }],
288
+ modulo: [{ args: [9, 4], expected: 1 }, { args: [10, 5], expected: 0 }]
289
+ };
290
+ function generateAcceptanceTests(mission) {
291
+ const targetFile = extractTargetFile(mission);
292
+ const symbols = extractKnownOperationSymbols(mission);
293
+ if (!targetFile) return { status: "skipped", command: "xdou generated-acceptance", tests: [], reason: "No target source file detected in mission." };
294
+ if (!symbols.length) return { status: "skipped", command: "xdou generated-acceptance", targetFile, tests: [], reason: "No known behavior template detected in mission." };
295
+ const tests = symbols.flatMap((symbol) => OPERATION_CASES[symbol].map((testCase) => ({ symbol, ...testCase })));
296
+ return { status: "generated", command: `xdou-acceptance ${targetFile}`, targetFile, tests };
297
+ }
298
+ async function runGeneratedAcceptanceTests(cwd, mission) {
299
+ const plan = generateAcceptanceTests(mission);
300
+ if (plan.status === "skipped") return { command: plan.command, status: "skipped", output: plan.reason ?? "No generated acceptance tests." };
301
+ const script = buildAcceptanceScript(cwd, plan);
302
+ const result = await execa2("node", ["--input-type=module", "--eval", script], { cwd, reject: false, timeout: 2 * 6e4, all: true });
303
+ return {
304
+ command: plan.command,
305
+ status: result.exitCode === 0 ? "passed" : "failed",
306
+ output: (result.all ?? "").slice(-2e4),
307
+ ...typeof result.exitCode === "number" ? { exitCode: result.exitCode } : {}
308
+ };
309
+ }
310
+ function extractTargetFile(mission) {
311
+ const match = /export(?:ed)?\s+from\s+([\w./\\-]+\.(?:mjs|js|cjs|ts))/i.exec(mission);
312
+ if (!match?.[1]) return void 0;
313
+ return normalize(match[1]).replace(/\\/g, "/");
314
+ }
315
+ function extractKnownOperationSymbols(mission) {
316
+ const found = /* @__PURE__ */ new Set();
317
+ for (const symbol of Object.keys(OPERATION_CASES)) {
318
+ const pattern = new RegExp(`\\b${symbol}\\s*\\(`, "i");
319
+ if (pattern.test(mission)) found.add(symbol);
320
+ }
321
+ return [...found];
322
+ }
323
+ function buildAcceptanceScript(cwd, plan) {
324
+ const moduleUrl = pathToFileURL(join3(cwd, plan.targetFile)).href;
325
+ return `
326
+ const mod = await import(${JSON.stringify(moduleUrl)});
327
+ const tests = ${JSON.stringify(plan.tests)};
328
+ let failures = 0;
329
+ for (const test of tests) {
330
+ const fn = mod[test.symbol];
331
+ if (typeof fn !== 'function') {
332
+ console.error(test.symbol + ' is not exported as a function');
333
+ failures += 1;
334
+ continue;
335
+ }
336
+ const actual = fn(...test.args);
337
+ if (!Object.is(actual, test.expected)) {
338
+ console.error(test.symbol + '(' + test.args.join(', ') + ') expected ' + test.expected + ' but got ' + actual);
339
+ failures += 1;
340
+ }
341
+ }
342
+ if (failures > 0) process.exit(1);
343
+ console.log('generated acceptance tests passed: ' + tests.length);
344
+ `;
345
+ }
346
+
347
+ // src/core/review-verdict.ts
348
+ function extractReviewVerdict(output) {
349
+ const marker = /REVIEW_VERDICT\s*:/i.exec(output);
350
+ if (!marker) return blockedVerdict("Reviewer output missing REVIEW_VERDICT JSON block.");
351
+ const tail = output.slice(marker.index + marker[0].length).trim();
352
+ const json = extractFirstJsonObject(tail);
353
+ if (!json) return blockedVerdict("Reviewer output has REVIEW_VERDICT marker but no JSON object.");
354
+ try {
355
+ const parsed = JSON.parse(json);
356
+ if (!isReviewDecision(parsed.verdict)) return blockedVerdict("Reviewer verdict must be approve, request_changes, or blocked.");
357
+ return {
358
+ verdict: parsed.verdict,
359
+ confidence: clampConfidence(parsed.confidence),
360
+ reason: typeof parsed.reason === "string" && parsed.reason.trim() ? parsed.reason : "No reason provided.",
361
+ missingRequirements: Array.isArray(parsed.missingRequirements) ? parsed.missingRequirements.filter((item) => typeof item === "string") : []
362
+ };
363
+ } catch (error) {
364
+ return blockedVerdict(`Reviewer verdict JSON could not be parsed: ${error instanceof Error ? error.message : String(error)}`);
365
+ }
366
+ }
367
+ function reviewVerdictBlocks(verdict) {
368
+ return verdict.verdict === "request_changes" || verdict.verdict === "blocked";
369
+ }
370
+ function blockedVerdict(reason) {
371
+ return { verdict: "blocked", confidence: 1, reason, missingRequirements: ["structured semantic review verdict"] };
372
+ }
373
+ function isReviewDecision(value) {
374
+ return value === "approve" || value === "request_changes" || value === "blocked";
375
+ }
376
+ function clampConfidence(value) {
377
+ return typeof value === "number" && Number.isFinite(value) ? Math.min(1, Math.max(0, value)) : 0;
378
+ }
379
+ function extractFirstJsonObject(text) {
380
+ const start = text.indexOf("{");
381
+ if (start < 0) return void 0;
382
+ let depth = 0;
383
+ let inString = false;
384
+ let escaped = false;
385
+ for (let index = start; index < text.length; index += 1) {
386
+ const char = text[index];
387
+ if (escaped) {
388
+ escaped = false;
389
+ continue;
390
+ }
391
+ if (char === "\\") {
392
+ escaped = true;
393
+ continue;
394
+ }
395
+ if (char === '"') {
396
+ inString = !inString;
397
+ continue;
398
+ }
399
+ if (inString) continue;
400
+ if (char === "{") depth += 1;
401
+ if (char === "}") {
402
+ depth -= 1;
403
+ if (depth === 0) return text.slice(start, index + 1);
404
+ }
405
+ }
406
+ return void 0;
407
+ }
408
+
409
+ // src/core/validation.ts
410
+ import { execa as execa3 } from "execa";
411
+ import fs3 from "fs-extra";
412
+ import { join as join4 } from "path";
413
+ async function detectValidationCommands(cwd) {
414
+ const commands = [];
415
+ if (await fs3.pathExists(join4(cwd, "package.json"))) {
416
+ const pkg = await fs3.readJson(join4(cwd, "package.json"));
417
+ if (pkg.scripts?.test) commands.push("npm test");
418
+ if (pkg.scripts?.typecheck) commands.push("npm run typecheck");
419
+ if (pkg.scripts?.build) commands.push("npm run build");
420
+ }
421
+ if (await fs3.pathExists(join4(cwd, "pyproject.toml")) || await fs3.pathExists(join4(cwd, "pytest.ini"))) commands.push("python -m pytest -q");
422
+ if (await fs3.pathExists(join4(cwd, "Cargo.toml"))) commands.push("cargo test");
423
+ if (await fs3.pathExists(join4(cwd, "go.mod"))) commands.push("go test ./...");
424
+ return commands;
425
+ }
426
+ async function runValidation(cwd, commands) {
427
+ const commandsToRun = commands ?? await detectValidationCommands(cwd);
428
+ const results = [];
429
+ for (const command of commandsToRun) {
430
+ const result = await execa3(command, { cwd, shell: true, reject: false, timeout: 10 * 6e4, all: true });
431
+ const validationResult = { command, status: result.exitCode === 0 ? "passed" : "failed", output: (result.all ?? "").slice(-2e4) };
432
+ if (typeof result.exitCode === "number") validationResult.exitCode = result.exitCode;
433
+ results.push(validationResult);
434
+ }
435
+ if (!results.length) results.push({ command: "auto-detect", status: "skipped", output: "No validation command detected." });
436
+ return results;
437
+ }
438
+
439
+ // src/agents/base.ts
440
+ import { execa as execa4 } from "execa";
441
+ import which from "which";
442
+ var CliAgentAdapter = class {
443
+ command;
444
+ roles;
445
+ constructor(options) {
446
+ this.command = options.command;
447
+ this.roles = options.roles;
448
+ }
449
+ async detect() {
450
+ try {
451
+ const path = await which(this.command);
452
+ const version = await this.readVersion();
453
+ return { available: true, path, ...version ? { version } : {} };
454
+ } catch (error) {
455
+ return { available: false, error: error instanceof Error ? error.message : String(error) };
456
+ }
457
+ }
458
+ async run(input) {
459
+ const invocation = this.buildInvocation(input);
460
+ const started = Date.now();
461
+ try {
462
+ const options = {
463
+ cwd: invocation.cwd,
464
+ shell: invocation.shell,
465
+ timeout: input.timeoutMs ?? 30 * 6e4,
466
+ reject: false,
467
+ all: false,
468
+ ...invocation.stdin ? { input: invocation.stdin } : { stdin: "ignore" },
469
+ ...invocation.env ? { env: invocation.env } : {}
470
+ };
471
+ const result = await execa4(invocation.command, invocation.args, options);
472
+ return {
473
+ agent: this.id,
474
+ command: invocation.command,
475
+ args: invocation.args,
476
+ exitCode: result.exitCode ?? 0,
477
+ stdout: result.stdout,
478
+ stderr: result.stderr,
479
+ durationMs: Date.now() - started,
480
+ ok: (result.exitCode ?? 0) === 0
481
+ };
482
+ } catch (error) {
483
+ return {
484
+ agent: this.id,
485
+ command: invocation.command,
486
+ args: invocation.args,
487
+ exitCode: 1,
488
+ stdout: "",
489
+ stderr: error instanceof Error ? error.message : String(error),
490
+ durationMs: Date.now() - started,
491
+ ok: false
492
+ };
493
+ }
494
+ }
495
+ async readVersion() {
496
+ const result = await execa4(this.command, ["--version"], { reject: false, timeout: 1e4 });
497
+ const text = `${result.stdout}
498
+ ${result.stderr}`.trim();
499
+ return text || void 0;
500
+ }
501
+ };
502
+
503
+ // src/agents/claude-code.ts
504
+ var ClaudeCodeAdapter = class extends CliAgentAdapter {
505
+ id;
506
+ type = "claude-code";
507
+ maxTurns;
508
+ constructor(options = {}) {
509
+ super({ command: options.command ?? "claude", roles: options.roles ?? ["architect", "reviewer", "debugger"] });
510
+ this.id = options.id ?? "claude";
511
+ this.maxTurns = options.maxTurns ?? 10;
512
+ }
513
+ buildInvocation(input) {
514
+ return { command: this.command, args: ["-p", input.prompt, "--max-turns", String(this.maxTurns), "--output-format", "json"], cwd: input.cwd, shell: false };
515
+ }
516
+ };
517
+
518
+ // src/agents/codex.ts
519
+ var CodexAdapter = class extends CliAgentAdapter {
520
+ id;
521
+ type = "codex";
522
+ fullAuto;
523
+ constructor(options = {}) {
524
+ super({ command: options.command ?? "codex", roles: options.roles ?? ["implementer", "fixer", "critic"] });
525
+ this.id = options.id ?? "codex";
526
+ this.fullAuto = options.fullAuto ?? false;
527
+ }
528
+ buildInvocation(input) {
529
+ const args = ["exec", "--cd", input.cwd, "-"];
530
+ if (this.fullAuto) args.splice(3, 0, "--dangerously-bypass-approvals-and-sandbox");
531
+ return { command: this.command, args, cwd: input.cwd, shell: false, stdin: input.prompt };
532
+ }
533
+ };
534
+
535
+ // src/agents/opencode.ts
536
+ var OpenCodeAdapter = class extends CliAgentAdapter {
537
+ id;
538
+ type = "opencode";
539
+ model;
540
+ constructor(options = {}) {
541
+ super({ command: options.command ?? "opencode", roles: options.roles ?? ["implementer", "reviewer"] });
542
+ this.id = options.id ?? "opencode";
543
+ this.model = options.model;
544
+ }
545
+ buildInvocation(input) {
546
+ const args = ["run", input.prompt];
547
+ if (this.model) args.push("--model", this.model);
548
+ return { command: this.command, args, cwd: input.cwd, shell: false };
549
+ }
550
+ };
551
+
552
+ // src/agents/openrouter.ts
553
+ import { generateText } from "ai";
554
+ import { createOpenRouter } from "@openrouter/ai-sdk-provider";
555
+ var OpenRouterAdapter = class {
556
+ id;
557
+ type = "openrouter";
558
+ roles;
559
+ model;
560
+ apiKeyEnv;
561
+ constructor(options) {
562
+ this.id = options.id;
563
+ this.model = options.model;
564
+ this.roles = options.roles ?? ["brainstormer", "critic", "reviewer"];
565
+ this.apiKeyEnv = options.apiKeyEnv ?? "OPENROUTER_API_KEY";
566
+ }
567
+ buildInvocation(input) {
568
+ return { command: "openrouter-api", args: [this.model, input.prompt], cwd: input.cwd, shell: false };
569
+ }
570
+ detect() {
571
+ const key = process.env[this.apiKeyEnv];
572
+ return Promise.resolve(key ? { available: true, version: this.model } : { available: false, error: `${this.apiKeyEnv} is not set` });
573
+ }
574
+ async run(input) {
575
+ const started = Date.now();
576
+ const apiKey = process.env[this.apiKeyEnv];
577
+ if (!apiKey) {
578
+ return { agent: this.id, command: "openrouter-api", args: [this.model], exitCode: 1, stdout: "", stderr: `${this.apiKeyEnv} is not set`, durationMs: Date.now() - started, ok: false };
579
+ }
580
+ try {
581
+ const openrouter = createOpenRouter({ apiKey });
582
+ const result = await generateText({ model: openrouter.chat(this.model), prompt: input.prompt, abortSignal: AbortSignal.timeout(input.timeoutMs ?? 10 * 6e4) });
583
+ return { agent: this.id, command: "openrouter-api", args: [this.model], exitCode: 0, stdout: result.text, stderr: "", durationMs: Date.now() - started, ok: true };
584
+ } catch (error) {
585
+ return { agent: this.id, command: "openrouter-api", args: [this.model], exitCode: 1, stdout: "", stderr: error instanceof Error ? error.message : String(error), durationMs: Date.now() - started, ok: false };
586
+ }
587
+ }
588
+ };
589
+
590
+ // src/agents/registry.ts
591
+ var SAFE_ID = /^[a-zA-Z0-9][a-zA-Z0-9_-]{0,63}$/;
592
+ function assertSafeAgentId(id) {
593
+ if (!SAFE_ID.test(id)) throw new Error(`Invalid agent id "${id}". Use 1-64 letters, numbers, _ or -; no path separators.`);
594
+ }
595
+ function asRoles(roles, fallback) {
596
+ return roles?.length ? roles : fallback;
597
+ }
598
+ function defaultAgents(definitions = {}) {
599
+ const agents = {
600
+ claude: new ClaudeCodeAdapter({ id: "claude" }),
601
+ codex: new CodexAdapter({ id: "codex", fullAuto: false }),
602
+ opencode: new OpenCodeAdapter({ id: "opencode" })
603
+ };
604
+ for (const [id, def] of Object.entries(definitions)) {
605
+ assertSafeAgentId(id);
606
+ if (def.enabled === false) continue;
607
+ if (def.type === "claude-code") agents[id] = new ClaudeCodeAdapter({ id, ...def.command ? { command: def.command } : {}, roles: asRoles(def.roles, ["architect", "reviewer", "debugger"]) });
608
+ if (def.type === "codex") agents[id] = new CodexAdapter({ id, ...def.command ? { command: def.command } : {}, roles: asRoles(def.roles, ["implementer", "fixer", "critic"]), fullAuto: def.fullAuto ?? false });
609
+ if (def.type === "opencode") agents[id] = new OpenCodeAdapter({ id, ...def.command ? { command: def.command } : {}, roles: asRoles(def.roles, ["implementer", "reviewer"]), ...def.model ? { model: def.model } : {} });
610
+ if (def.type === "openrouter") {
611
+ if (!def.model) throw new Error(`OpenRouter agent "${id}" requires model`);
612
+ agents[id] = new OpenRouterAdapter({ id, model: def.model, roles: asRoles(def.roles, ["brainstormer", "critic", "reviewer"]) });
613
+ }
614
+ }
615
+ return agents;
616
+ }
617
+ function selectAgents(names, agents = defaultAgents()) {
618
+ return names.map((name) => {
619
+ const agent = agents[name];
620
+ if (!agent) throw new Error(`Unknown agent "${name}". Known agents: ${Object.keys(agents).join(", ")}`);
621
+ return agent;
622
+ });
623
+ }
624
+
625
+ // src/orchestrator.ts
626
+ var XdouOrchestrator = class {
627
+ cwd;
628
+ store;
629
+ agents;
630
+ constructor(cwd, artifactDir = ".xdou", agentDefinitions = {}, agentOverrides = {}) {
631
+ this.cwd = cwd;
632
+ this.store = new ArtifactStore(join5(cwd, artifactDir));
633
+ this.agents = { ...defaultAgents(agentDefinitions), ...agentOverrides };
634
+ }
635
+ async detectAgents() {
636
+ const entries = await Promise.all(Object.entries(this.agents).map(async ([name, agent]) => [name, await agent.detect()]));
637
+ return Object.fromEntries(entries);
638
+ }
639
+ async brainstorm(mission, names = ["claude", "codex"]) {
640
+ await ensureGitRepo(this.cwd);
641
+ const run = await this.store.createRun(mission);
642
+ await this.store.updateManifest(run.id, { status: "running", phase: "brainstorm" });
643
+ const project = await repoSummary(this.cwd);
644
+ const snapshotCwd = await createProjectSnapshot(this.cwd, join5(this.store.runDir(run.id), "project-snapshot"));
645
+ await this.store.writeText(run.id, "project.md", project || "No common project metadata found.");
646
+ const council = await this.runCouncil(run.id, mission, project, snapshotCwd, names, []);
647
+ const councilText = this.formatCouncil(council);
648
+ await this.store.writeText(run.id, "brainstorm.md", councilText);
649
+ await this.store.writeText(run.id, "council.md", councilText);
650
+ await this.store.updateManifest(run.id, { status: "completed", phase: "brainstormed" });
651
+ return run.id;
652
+ }
653
+ async run(options) {
654
+ await ensureGitRepo(this.cwd);
655
+ if (options.execute !== false) await ensureCleanWorkingTree(this.cwd);
656
+ const run = await this.store.createRun(options.mission);
657
+ const cleanupSignals = this.installAbortSignalHandlers(run.id);
658
+ await this.store.updateManifest(run.id, { status: "running", phase: "council", processPid: process.pid });
659
+ const project = await repoSummary(this.cwd);
660
+ const snapshotCwd = await createProjectSnapshot(this.cwd, join5(this.store.runDir(run.id), "project-snapshot"));
661
+ await this.store.writeText(run.id, "project.md", project || "No common project metadata found.");
662
+ const selected = selectAgents(options.team ?? ["claude", "codex", "claude"], this.agents);
663
+ const architect = selected[0];
664
+ const implementer = selected[1];
665
+ const fallbackReviewer = selected[2] ?? selected[0];
666
+ if (!architect || !implementer || !fallbackReviewer) throw new Error("A run needs at least architect, implementer, and reviewer agents.");
667
+ const brainstormers = options.brainstormers ?? [architect.id, implementer.id];
668
+ const critics = options.critics ?? [];
669
+ const reviewerNames = options.reviewers ?? [fallbackReviewer.id];
670
+ const council = await this.runCouncil(run.id, options.mission, project, snapshotCwd, brainstormers, critics, options.timeoutMs);
671
+ const councilText = this.formatCouncil(council);
672
+ await this.store.writeText(run.id, "council.md", councilText || "No council agents configured.");
673
+ await this.store.updateManifest(run.id, { phase: "planning" });
674
+ const synthesisPrompt = [
675
+ compileContextPacket({ runId: run.id, agent: architect.id, role: "architect", mission: options.mission, projectContext: project, budget: "balanced" }),
676
+ "",
677
+ "CO-DEVELOPMENT COUNCIL INPUTS:",
678
+ councilText || "No council inputs.",
679
+ "",
680
+ "SYNTHESIS CONTRACT:",
681
+ "Create one canonical plan that selects the strongest ideas, resolves disagreements, lists risks, and gives the implementer precise execution steps."
682
+ ].join("\n");
683
+ const planInput = { cwd: snapshotCwd, runDir: this.store.runDir(run.id), prompt: synthesisPrompt, ...options.timeoutMs ? { timeoutMs: options.timeoutMs } : {} };
684
+ const planResult = await architect.run(planInput);
685
+ const rawPlan = planResult.stdout || planResult.stderr;
686
+ const synthesis = this.formatSynthesis(architect.id, rawPlan, council);
687
+ await this.store.writeText(run.id, "plan.md", rawPlan);
688
+ await this.store.writeText(run.id, "synthesis.md", synthesis);
689
+ await this.store.writeJson(run.id, `agents/${architect.id}/plan-result.json`, planResult);
690
+ await this.store.appendEvent(run.id, { type: "plan.created", by: architect.id, ok: planResult.ok });
691
+ if (!planResult.ok) {
692
+ await this.store.updateManifest(run.id, { status: "blocked", phase: "planning_failed" });
693
+ throw new Error(`Architect failed: ${planResult.stderr}`);
694
+ }
695
+ if (options.execute === false) {
696
+ cleanupSignals();
697
+ await this.store.updateManifest(run.id, { status: "completed", phase: "planned" });
698
+ return run.id;
699
+ }
700
+ const workspace = options.isolated === false ? { cwd: this.cwd } : await createRunWorktree(this.cwd, run.id);
701
+ await this.store.updateManifest(run.id, { phase: "implementation", ...workspace.worktreePath ? { worktreePath: workspace.worktreePath, baseRef: workspace.baseRef } : {} });
702
+ const implPrompt = compileContextPacket({ runId: run.id, agent: implementer.id, role: "implementer", mission: options.mission, projectContext: project, plan: synthesis, budget: "balanced" });
703
+ const implInput = { cwd: workspace.cwd, runDir: this.store.runDir(run.id), prompt: implPrompt, ...options.timeoutMs ? { timeoutMs: options.timeoutMs } : {} };
704
+ const implResult = await implementer.run(implInput);
705
+ await this.store.writeJson(run.id, `agents/${implementer.id}/implementation-result.json`, implResult);
706
+ await this.store.appendEvent(run.id, { type: "implementation.finished", by: implementer.id, ok: implResult.ok });
707
+ let diff = await gitDiff(workspace.cwd);
708
+ await this.store.writeText(run.id, "diff.patch", diff || "No diff produced.");
709
+ let validation = await this.validateWorkspace(run.id, options.mission, workspace.cwd, diff);
710
+ await this.store.appendEvent(run.id, { type: "validation.finished", ok: !validation.some((v) => v.status === "failed") });
711
+ await this.store.updateManifest(run.id, { phase: "review" });
712
+ let reviewResults = await this.runReviews(run.id, snapshotCwd, options.mission, synthesis, diff, validation, reviewerNames, options.timeoutMs);
713
+ let failed = this.hasBlockers(implResult, validation, reviewResults);
714
+ const maxFixAttempts = options.maxFixAttempts ?? 1;
715
+ const fixerName = options.fixer ?? implementer.id;
716
+ for (let attempt = 1; failed && attempt <= maxFixAttempts; attempt += 1) {
717
+ const [fixer] = selectAgents([fixerName], this.agents);
718
+ if (!fixer) break;
719
+ await this.store.updateManifest(run.id, { phase: `fix_${attempt}`, fixAttempts: attempt });
720
+ await this.store.appendEvent(run.id, { type: "fix.started", by: fixer.id, attempt });
721
+ const lastValidation = validation.at(-1);
722
+ const fixPrompt = compileContextPacket({
723
+ runId: run.id,
724
+ agent: fixer.id,
725
+ role: "fixer",
726
+ mission: options.mission,
727
+ projectContext: project,
728
+ plan: synthesis,
729
+ diff,
730
+ budget: "balanced",
731
+ ...lastValidation ? { validation: lastValidation } : {}
732
+ });
733
+ await this.store.writeText(run.id, `fixes/attempt-${attempt}/inbox.md`, fixPrompt);
734
+ const fixInput = { cwd: workspace.cwd, runDir: this.store.runDir(run.id), prompt: fixPrompt, ...options.timeoutMs ? { timeoutMs: options.timeoutMs } : {} };
735
+ const fixResult = await fixer.run(fixInput);
736
+ await this.store.writeJson(run.id, `fixes/attempt-${attempt}/result.json`, fixResult);
737
+ await this.store.appendEvent(run.id, { type: "fix.finished", by: fixer.id, attempt, ok: fixResult.ok });
738
+ diff = await gitDiff(workspace.cwd);
739
+ await this.store.writeText(run.id, `fixes/attempt-${attempt}/diff.patch`, diff || "No diff produced.");
740
+ await this.store.writeText(run.id, "diff.patch", diff || "No diff produced.");
741
+ validation = await this.validateWorkspace(run.id, options.mission, workspace.cwd, diff);
742
+ await this.store.writeJson(run.id, `fixes/attempt-${attempt}/validation.json`, validation);
743
+ await this.store.appendEvent(run.id, { type: "validation.finished", attempt, ok: !validation.some((v) => v.status === "failed") });
744
+ reviewResults = await this.runReviews(run.id, snapshotCwd, options.mission, synthesis, diff, validation, reviewerNames, options.timeoutMs);
745
+ failed = this.hasBlockers(fixResult, validation, reviewResults);
746
+ }
747
+ if (failed && maxFixAttempts > 0) await this.store.appendEvent(run.id, { type: "fix.exhausted", attempts: maxFixAttempts });
748
+ await this.store.writeText(run.id, "final-summary.md", this.formatFinalSummary(options.mission, run.id, synthesis, validation, reviewResults, failed, workspace.worktreePath));
749
+ const finalManifest = await this.store.updateManifest(run.id, { status: failed ? "blocked" : "completed", phase: failed ? "needs_attention" : "done" });
750
+ await this.store.writeJson(run.id, "result.json", { runId: run.id, status: finalManifest.status, phase: finalManifest.phase, artifactDir: finalManifest.artifactDir, worktreePath: finalManifest.worktreePath, validation, reviews: reviewResults.map((review) => ({ agent: review.agent, ok: review.result.ok, verdict: review.verdict })) });
751
+ cleanupSignals();
752
+ if (failed) console.error(pc.yellow(`xdou run ${run.id} completed with blockers; inspect ${this.store.runDir(run.id)}`));
753
+ return run.id;
754
+ }
755
+ async applyRun(runId) {
756
+ await ensureGitRepo(this.cwd);
757
+ const manifest = await this.store.readManifest(runId);
758
+ if (manifest.status !== "completed") throw new Error(`Refusing to apply run ${runId} with status ${manifest.status}.`);
759
+ const diffPath = join5(this.store.runDir(runId), "diff.patch");
760
+ const diff = await fs4.readFile(diffPath, "utf8");
761
+ const applied = await applyPatch(this.cwd, diff);
762
+ const result = { runId, applied: true, filesChanged: applied.filesChanged, files: applied.files, artifactDir: manifest.artifactDir };
763
+ await this.store.writeJson(runId, "apply-result.json", result);
764
+ await this.store.appendEvent(runId, { type: "run.applied", by: "xdou", filesChanged: applied.filesChanged });
765
+ await this.store.updateManifest(runId, { appliedAt: (/* @__PURE__ */ new Date()).toISOString() });
766
+ return result;
767
+ }
768
+ async runCouncil(runId, mission, project, cwd, brainstormers, critics, timeoutMs) {
769
+ const specs = [
770
+ ...brainstormers.map((name) => ({ name, role: "brainstormer" })),
771
+ ...critics.map((name) => ({ name, role: "critic" }))
772
+ ];
773
+ const outputs = await Promise.all(specs.map(async (spec) => {
774
+ const [agent] = selectAgents([spec.name], this.agents);
775
+ if (!agent) return void 0;
776
+ const prompt = compileContextPacket({ runId, agent: agent.id, role: spec.role, mission, projectContext: project, budget: "balanced" });
777
+ await this.store.writeText(runId, `agents/${agent.id}/${spec.role}-inbox.md`, prompt);
778
+ const input = { cwd, runDir: this.store.runDir(runId), prompt, ...timeoutMs ? { timeoutMs } : {} };
779
+ const result = await agent.run(input);
780
+ await this.store.writeJson(runId, `agents/${agent.id}/${spec.role}-result.json`, result);
781
+ await this.store.appendEvent(runId, { type: "council.finished", by: agent.id, role: spec.role, ok: result.ok, exitCode: result.exitCode });
782
+ return { agent: agent.id, role: spec.role, result };
783
+ }));
784
+ return outputs.filter((output) => Boolean(output));
785
+ }
786
+ async runReviews(runId, cwd, mission, plan, diff, validation, reviewers, timeoutMs) {
787
+ const lastValidation = validation.at(-1);
788
+ const outputs = await Promise.all(selectAgents(reviewers, this.agents).map(async (reviewer) => {
789
+ const reviewContext = { runId, agent: reviewer.id, role: "reviewer", mission, plan, diff, budget: "minimal", ...lastValidation ? { validation: lastValidation } : {} };
790
+ const prompt = compileContextPacket(reviewContext);
791
+ await this.store.writeText(runId, `agents/${reviewer.id}/review-inbox.md`, prompt);
792
+ const input = { cwd, runDir: this.store.runDir(runId), prompt, ...timeoutMs ? { timeoutMs } : {} };
793
+ const result = await reviewer.run(input);
794
+ const verdict = extractReviewVerdict(result.stdout || result.stderr);
795
+ await this.store.writeJson(runId, `agents/${reviewer.id}/review-result.json`, result);
796
+ await this.store.writeJson(runId, `agents/${reviewer.id}/review-verdict.json`, verdict);
797
+ await this.store.appendEvent(runId, { type: "review.finished", by: reviewer.id, ok: result.ok, verdict: verdict.verdict });
798
+ return { agent: reviewer.id, result, verdict };
799
+ }));
800
+ await this.store.writeText(runId, "review.md", outputs.map((review) => `## ${review.agent}
801
+
802
+ ${review.result.stdout || review.result.stderr}`).join("\n\n---\n\n"));
803
+ await this.store.writeJson(runId, "review-verdicts.json", outputs.map((review) => ({ agent: review.agent, ...review.verdict })));
804
+ return outputs;
805
+ }
806
+ async validateWorkspace(runId, mission, cwd, diff) {
807
+ const validation = await runValidation(cwd);
808
+ const generatedAcceptance = await runGeneratedAcceptanceTests(cwd, mission);
809
+ await this.store.writeJson(runId, "generated-acceptance.json", generatedAcceptance);
810
+ const missionCheck = checkMissionCompletion(mission, diff || "No diff produced.");
811
+ await this.store.writeJson(runId, "mission-check.json", missionCheck);
812
+ const combined = [
813
+ ...validation,
814
+ generatedAcceptance,
815
+ {
816
+ command: "xdou mission-completion-check",
817
+ status: missionCheck.status === "failed" ? "failed" : "passed",
818
+ output: missionCheck.message,
819
+ exitCode: missionCheck.status === "failed" ? 1 : 0
820
+ }
821
+ ];
822
+ await this.store.writeJson(runId, "validation.json", combined);
823
+ return combined;
824
+ }
825
+ formatCouncil(council) {
826
+ return council.map((entry) => `## ${entry.agent} (${entry.role})
827
+
828
+ ${entry.result.stdout || entry.result.stderr}`).join("\n\n---\n\n");
829
+ }
830
+ formatSynthesis(architect, plan, council) {
831
+ const participants = council.map((entry) => `${entry.agent}:${entry.role}`).join(", ") || "none";
832
+ return [`# Synthesis`, "", `Architect: ${architect}`, `Council: ${participants}`, "", "## Selected implementation direction", "", plan, "", "## Collaboration rule", "", "Implementation follows this synthesized plan, then independent reviewers inspect the resulting diff and validation output."].join("\n");
833
+ }
834
+ hasBlockers(lastMutation, validation, reviews) {
835
+ return !lastMutation.ok || validation.some((v) => v.status === "failed") || reviews.some((review) => !review.result.ok || reviewVerdictBlocks(review.verdict));
836
+ }
837
+ installAbortSignalHandlers(runId) {
838
+ const handle = (signal) => {
839
+ void this.store.abortRun(runId, `received ${signal}`).finally(() => process.exit(130));
840
+ };
841
+ process.once("SIGINT", handle);
842
+ process.once("SIGTERM", handle);
843
+ return () => {
844
+ process.off("SIGINT", handle);
845
+ process.off("SIGTERM", handle);
846
+ };
847
+ }
848
+ formatFinalSummary(mission, runId, synthesis, validation, reviews, failed, worktreePath) {
849
+ return [
850
+ "# XDOU Run Summary",
851
+ "",
852
+ `Run: ${runId}`,
853
+ `Mission: ${mission}`,
854
+ `Status: ${failed ? "blocked" : "completed"}`,
855
+ ...worktreePath ? [`Worktree: ${worktreePath}`] : [],
856
+ `Reviewers: ${reviews.map((review) => review.agent).join(", ") || "none"}`,
857
+ "",
858
+ "## Validation",
859
+ ...validation.map((result) => `- ${result.status}: ${result.command}`),
860
+ "",
861
+ "## Synthesis",
862
+ synthesis,
863
+ "",
864
+ "## Review outcomes",
865
+ ...reviews.map((review) => `- ${review.agent}: ${review.result.ok ? "ok" : "failed"}`)
866
+ ].join("\n");
867
+ }
868
+ };
869
+
870
+ // src/config/schema.ts
871
+ import { z } from "zod";
872
+ var safeId = /^[a-zA-Z0-9][a-zA-Z0-9_-]{0,63}$/;
873
+ var agentSchema = z.object({
874
+ type: z.enum(["claude-code", "codex", "opencode", "openrouter"]),
875
+ command: z.string().optional(),
876
+ model: z.string().optional(),
877
+ roles: z.array(z.string()).default([]),
878
+ enabled: z.boolean().default(true),
879
+ fullAuto: z.boolean().default(false)
880
+ });
881
+ var teamSchema = z.object({
882
+ brainstormers: z.array(z.string().regex(safeId)).default(["claude", "codex"]),
883
+ architect: z.string().regex(safeId).default("claude"),
884
+ critic: z.string().regex(safeId).default("codex"),
885
+ implementer: z.string().regex(safeId).default("codex"),
886
+ reviewer: z.array(z.string().regex(safeId)).default(["claude"]),
887
+ fixer: z.string().regex(safeId).default("codex")
888
+ });
889
+ var configSchema = z.object({
890
+ artifactDir: z.string().default(".xdou"),
891
+ agents: z.record(z.string().regex(safeId), agentSchema).default({}),
892
+ teams: z.record(z.string().regex(safeId), teamSchema).default({ default: teamSchema.parse({}) })
893
+ });
894
+ function parseConfig(input) {
895
+ return configSchema.parse(input ?? {});
896
+ }
897
+ function defaultConfig() {
898
+ return parseConfig({});
899
+ }
900
+
901
+ // src/config/load.ts
902
+ import { cosmiconfig } from "cosmiconfig";
903
+ import YAML from "yaml";
904
+ async function loadConfig(cwd) {
905
+ const explorer = cosmiconfig("xdou", { searchPlaces: ["xdou.yaml", "xdou.yml", ".xdourc.yaml", "package.json"], loaders: { ".yaml": (_p, content) => YAML.parse(content), ".yml": (_p, content) => YAML.parse(content) } });
906
+ const result = await explorer.search(cwd);
907
+ if (!result) return { config: defaultConfig() };
908
+ return { config: parseConfig(result.config), filepath: result.filepath };
909
+ }
910
+
911
+ // src/cli.ts
912
+ var Xdou = class _Xdou extends Command {
913
+ static description = "xdou: multi-agent coding from your terminal";
914
+ static strict = false;
915
+ static flags = {
916
+ cwd: Flags.string({ default: process.cwd(), description: "Working directory" }),
917
+ json: Flags.boolean({ default: false }),
918
+ agents: Flags.string({ description: "Comma-separated agent ids for brainstorm/plan/run" }),
919
+ "max-fix-attempts": Flags.integer({ default: 1, description: "Maximum fixer iterations for run" })
920
+ };
921
+ async run() {
922
+ const { argv, flags } = await this.parse(_Xdou);
923
+ const [cmd, ...rest] = argv;
924
+ const cwd = flags.cwd;
925
+ const { config } = await loadConfig(cwd);
926
+ const orchestrator = new XdouOrchestrator(cwd, config.artifactDir, config.agents);
927
+ const fallbackTeam = defaultConfig().teams.default;
928
+ if (!fallbackTeam) throw new Error("Internal error: default team missing");
929
+ const team = config.teams.default ?? fallbackTeam;
930
+ switch (cmd) {
931
+ case "init":
932
+ await this.initProject(cwd);
933
+ break;
934
+ case "agents":
935
+ await this.agents(orchestrator, rest, flags.json);
936
+ break;
937
+ case "brainstorm":
938
+ await this.brainstorm(orchestrator, rest, team, flags.agents);
939
+ break;
940
+ case "plan":
941
+ await this.plan(orchestrator, rest, team, flags.agents);
942
+ break;
943
+ case "run":
944
+ await this.runMission(orchestrator, rest, team, flags.agents, flags["max-fix-attempts"], flags.json);
945
+ break;
946
+ case "apply":
947
+ await this.apply(orchestrator, rest, flags.json);
948
+ break;
949
+ case "status":
950
+ await this.status(orchestrator, rest, flags.json);
951
+ break;
952
+ case "runs":
953
+ await this.runs(orchestrator, rest, flags.json);
954
+ break;
955
+ case "context":
956
+ await this.context(orchestrator, rest);
957
+ break;
958
+ case "config":
959
+ await this.configCommand(cwd, rest);
960
+ break;
961
+ case void 0:
962
+ case "help":
963
+ case "--help":
964
+ case "-h":
965
+ this.log("xdou: multi-agent coding from your terminal\n\nCommands:\n init\n agents [list|detect]\n brainstorm <mission> [--agents a,b]\n plan <mission>\n run <mission> [--agents architect,implementer,reviewer] [--max-fix-attempts n] [--json]\n apply <run-id> [--json]\n status [run-id]\n runs list\n context [run-id]\n config validate");
966
+ break;
967
+ default:
968
+ throw new Error(`Unknown command: ${cmd}. Try: xdou init | agents detect | brainstorm | plan | run | status | runs list | context | config validate`);
969
+ }
970
+ }
971
+ async initProject(cwd) {
972
+ const configPath = join6(cwd, "xdou.yaml");
973
+ if (await fs5.pathExists(configPath)) throw new Error(`Config already exists: ${configPath}`);
974
+ await fs5.writeFile(configPath, YAML2.stringify(defaultConfig()), "utf8");
975
+ await fs5.ensureDir(join6(cwd, ".xdou", "runs"));
976
+ await this.ensureGitignore(cwd);
977
+ this.log(`${pc2.green("created")} ${configPath}`);
978
+ }
979
+ async ensureGitignore(cwd) {
980
+ const path = join6(cwd, ".gitignore");
981
+ const current = await fs5.readFile(path, "utf8").catch(() => "");
982
+ const required = [".xdou/runs/", ".xdou/worktrees/"];
983
+ const existing = current.split(/\r?\n/);
984
+ const missing = required.filter((line) => !existing.includes(line));
985
+ if (missing.length) await fs5.appendFile(path, `${current && !current.endsWith("\n") ? "\n" : ""}${missing.join("\n")}
986
+ `, "utf8");
987
+ }
988
+ async agents(orchestrator, args, json) {
989
+ const sub = args[0] ?? "list";
990
+ if (!["list", "detect"].includes(sub)) throw new Error("Usage: xdou agents [list|detect]");
991
+ const detected = await orchestrator.detectAgents();
992
+ if (json) {
993
+ this.log(JSON.stringify(detected, null, 2));
994
+ return;
995
+ }
996
+ const table = new Table({ head: ["agent", "available", "version/path"] });
997
+ for (const [name, info] of Object.entries(detected)) table.push([name, info.available ? pc2.green("yes") : pc2.red("no"), info.version ?? info.path ?? info.error ?? ""]);
998
+ this.log(table.toString());
999
+ }
1000
+ mission(args) {
1001
+ const text = args.join(" ").trim();
1002
+ if (!text) throw new Error('Mission is required. Example: xdou run "add oauth"');
1003
+ return text;
1004
+ }
1005
+ parseAgents(args, fallback, flagValue) {
1006
+ if (flagValue) return flagValue.split(",").map((s) => s.trim()).filter(Boolean);
1007
+ const idx = args.indexOf("--agents");
1008
+ const value = idx >= 0 ? args[idx + 1] : void 0;
1009
+ if (idx >= 0 && !value) throw new Error("--agents requires a comma-separated value");
1010
+ return value ? value.split(",").map((s) => s.trim()).filter(Boolean) : fallback;
1011
+ }
1012
+ cleanMissionArgs(args) {
1013
+ const idx = args.indexOf("--agents");
1014
+ return idx >= 0 ? args.slice(0, idx) : args;
1015
+ }
1016
+ numberFlag(args, name, fallback) {
1017
+ const idx = args.indexOf(name);
1018
+ if (idx < 0) return fallback;
1019
+ const value = Number(args[idx + 1]);
1020
+ if (!Number.isInteger(value) || value < 0) throw new Error(`${name} requires a non-negative integer`);
1021
+ return value;
1022
+ }
1023
+ cleanRunArgs(args) {
1024
+ let cleaned = this.cleanMissionArgs(args);
1025
+ const idx = cleaned.indexOf("--max-fix-attempts");
1026
+ if (idx >= 0) cleaned = [...cleaned.slice(0, idx), ...cleaned.slice(idx + 2)];
1027
+ return cleaned;
1028
+ }
1029
+ async brainstorm(orchestrator, args, team, agentsFlag) {
1030
+ const agents = this.parseAgents(args, team.brainstormers, agentsFlag);
1031
+ const runId = await orchestrator.brainstorm(this.mission(this.cleanMissionArgs(args)), agents);
1032
+ this.log(`${pc2.green("brainstorm complete")} run=${runId} artifacts=${orchestrator.store.runDir(runId)}`);
1033
+ }
1034
+ async plan(orchestrator, args, team, agentsFlag) {
1035
+ const agents = this.parseAgents(args, [team.architect, team.implementer, team.reviewer[0] ?? team.architect], agentsFlag);
1036
+ const runId = await orchestrator.run({
1037
+ cwd: orchestrator.cwd,
1038
+ mission: this.mission(this.cleanRunArgs(args)),
1039
+ execute: false,
1040
+ team: agents,
1041
+ brainstormers: team.brainstormers,
1042
+ critics: [team.critic],
1043
+ reviewers: team.reviewer
1044
+ });
1045
+ this.log(`${pc2.green("plan complete")} run=${runId} artifacts=${orchestrator.store.runDir(runId)}`);
1046
+ }
1047
+ async runMission(orchestrator, args, team, agentsFlag, maxFixAttempts = 1, json = false) {
1048
+ const agents = this.parseAgents(args, [team.architect, team.implementer, team.reviewer[0] ?? team.architect], agentsFlag);
1049
+ const runId = await orchestrator.run({
1050
+ cwd: orchestrator.cwd,
1051
+ mission: this.mission(this.cleanRunArgs(args)),
1052
+ team: agents,
1053
+ brainstormers: team.brainstormers,
1054
+ critics: [team.critic],
1055
+ reviewers: team.reviewer,
1056
+ fixer: team.fixer,
1057
+ maxFixAttempts
1058
+ });
1059
+ const manifest = await orchestrator.store.readManifest(runId);
1060
+ const payload = { runId, status: manifest.status, phase: manifest.phase, artifactDir: manifest.artifactDir, worktreePath: manifest.worktreePath };
1061
+ this.log(json ? JSON.stringify(payload, null, 2) : `${pc2.green("run complete")} run=${runId} artifacts=${orchestrator.store.runDir(runId)}`);
1062
+ }
1063
+ async apply(orchestrator, args, json) {
1064
+ const runId = args[0];
1065
+ if (!runId) throw new Error("Usage: xdou apply <run-id>");
1066
+ const result = await orchestrator.applyRun(runId);
1067
+ this.log(json ? JSON.stringify(result, null, 2) : `${pc2.green("applied")} run=${runId} files=${result.filesChanged}`);
1068
+ }
1069
+ async status(orchestrator, args, json) {
1070
+ await orchestrator.store.recoverStaleRuns();
1071
+ const runId = args[0] ?? await orchestrator.store.latestRunId();
1072
+ if (!runId) {
1073
+ this.log("No runs found.");
1074
+ return;
1075
+ }
1076
+ const manifest = await orchestrator.store.readManifest(runId);
1077
+ this.log(json ? JSON.stringify(manifest, null, 2) : `${manifest.id} ${manifest.status}/${manifest.phase}
1078
+ ${manifest.artifactDir}`);
1079
+ }
1080
+ async runs(orchestrator, args, json) {
1081
+ if ((args[0] ?? "list") !== "list") throw new Error("Usage: xdou runs list");
1082
+ await orchestrator.store.recoverStaleRuns();
1083
+ const runs = await orchestrator.store.listRuns();
1084
+ if (json) {
1085
+ this.log(JSON.stringify(runs, null, 2));
1086
+ return;
1087
+ }
1088
+ if (!runs.length) {
1089
+ this.log("No runs found.");
1090
+ return;
1091
+ }
1092
+ const table = new Table({ head: ["run", "status", "phase", "mission"] });
1093
+ for (const run of runs) table.push([run.id, run.status, run.phase, run.mission]);
1094
+ this.log(table.toString());
1095
+ }
1096
+ async context(orchestrator, args) {
1097
+ const runId = args[0] ?? await orchestrator.store.latestRunId();
1098
+ if (!runId) throw new Error("No run id supplied and no previous run found.");
1099
+ const inboxPath = join6(orchestrator.store.runDir(runId), "agents");
1100
+ this.log(inboxPath);
1101
+ }
1102
+ async configCommand(cwd, args) {
1103
+ if ((args[0] ?? "validate") !== "validate") throw new Error("Usage: xdou config validate");
1104
+ const loaded = await loadConfig(cwd);
1105
+ this.log(`${pc2.green("valid")} ${loaded.filepath ?? "defaults"}`);
1106
+ }
1107
+ };
1108
+ void Xdou.run().catch((error) => {
1109
+ console.error(pc2.red(error instanceof Error ? error.message : String(error)));
1110
+ process.exitCode = 1;
1111
+ });
1112
+ //# sourceMappingURL=cli.js.map