@dougharness/cli 1.1.1 → 1.3.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/board-serve.js +40 -6
- package/dist/board-serve.js.map +1 -1
- package/dist/init.js +1 -1
- package/dist/init.js.map +1 -1
- package/dist/memory-setup.d.ts +7 -0
- package/dist/memory-setup.js +70 -46
- package/dist/memory-setup.js.map +1 -1
- package/node_modules/@dougharness/flow/.claude-plugin/plugin.json +6 -2
- package/node_modules/@dougharness/flow/agents/coder.md +23 -0
- package/node_modules/@dougharness/flow/agents/implementer.md +1 -1
- package/node_modules/@dougharness/flow/agents/planner.md +1 -1
- package/node_modules/@dougharness/flow/agents/researcher.md +2 -1
- package/node_modules/@dougharness/flow/agents/reviewer.md +3 -1
- package/node_modules/@dougharness/flow/agents/spec-writer.md +23 -0
- package/node_modules/@dougharness/flow/agents/tester.md +21 -0
- package/node_modules/@dougharness/flow/hooks/hooks.json +4 -1
- package/node_modules/@dougharness/flow/hooks/register.tsx +94 -0
- package/node_modules/@dougharness/flow/lib/autopilot-core.mjs +117 -0
- package/node_modules/@dougharness/flow/lib/autopilot-view.mjs +17 -0
- package/node_modules/@dougharness/flow/lib/board.mjs +91 -10
- package/node_modules/@dougharness/flow/lib/card-land.mjs +115 -0
- package/node_modules/@dougharness/flow/lib/card-loop.mjs +778 -0
- package/node_modules/@dougharness/flow/lib/coordinator.mjs +393 -0
- package/node_modules/@dougharness/flow/lib/cost.mjs +15 -5
- package/node_modules/@dougharness/flow/lib/decisions.mjs +4 -2
- package/node_modules/@dougharness/flow/lib/embeddings.mjs +144 -3
- package/node_modules/@dougharness/flow/lib/land.mjs +27 -8
- package/node_modules/@dougharness/flow/lib/memory.mjs +406 -76
- package/node_modules/@dougharness/flow/lib/plan.mjs +33 -5
- package/node_modules/@dougharness/flow/lib/worktrees.mjs +3 -3
- package/node_modules/@dougharness/flow/package.json +1 -1
- package/node_modules/@dougharness/flow/scripts/autopilot.mjs +440 -0
- package/node_modules/@dougharness/flow/scripts/board.mjs +107 -32
- package/node_modules/@dougharness/flow/scripts/cost.mjs +16 -3
- package/node_modules/@dougharness/flow/scripts/memory.mjs +33 -10
- package/node_modules/@dougharness/flow/scripts/plan.mjs +104 -30
- package/node_modules/@dougharness/flow/skills/doug-hand/SKILL.md +8 -2
- package/node_modules/@dougharness/flow/skills/doug-next/SKILL.md +30 -10
- package/node_modules/@dougharness/flow/skills/doug-start/SKILL.md +80 -0
- package/node_modules/@dougharness/flow/skills/doug-swarm/SKILL.md +1 -1
- package/node_modules/@dougharness/flow/skills/research/SKILL.md +1 -1
- package/node_modules/@dougharness/flow/skills/run-report/SKILL.md +6 -6
- package/node_modules/@dougharness/flow/workflows/card-queen.js +846 -0
- package/node_modules/@dougharness/flow/workflows/doug-implement.js +63 -23
- package/node_modules/@dougharness/gates/.claude-plugin/plugin.json +1 -1
- package/node_modules/@dougharness/gates/lib/anchor.mjs +29 -4
- package/node_modules/@dougharness/gates/lib/bash-rules.mjs +196 -6
- package/node_modules/@dougharness/gates/lib/gate-lock.mjs +40 -8
- package/node_modules/@dougharness/gates/lib/proposals.mjs +41 -5
- package/node_modules/@dougharness/gates/lib/scope.mjs +43 -4
- package/node_modules/@dougharness/gates/package.json +1 -1
- package/node_modules/@dougharness/gates/scripts/reanchor.mjs +2 -1
- package/node_modules/@dougharness/gates/scripts/research-cap.mjs +119 -15
- package/node_modules/@dougharness/gates/scripts/stop-gate.mjs +156 -14
- package/node_modules/@dougharness/gates/scripts/verify.mjs +8 -4
- package/package.json +3 -3
- package/templates/board-app.js +46 -0
|
@@ -9,6 +9,13 @@ import { isModelName, EFFORTS } from "./models.mjs";
|
|
|
9
9
|
|
|
10
10
|
export const PLAN_RELPATH = ".doug/plan.json";
|
|
11
11
|
export const REPORT_RELPATH = ".doug/.state/last-report.json";
|
|
12
|
+
// Per-card plans (card parallel-card-queens): a card run as its own queen keeps its plan at the tracked
|
|
13
|
+
// .doug/plans/<id>.json and its report at .doug/.state/reports/<id>/last-report.json.
|
|
14
|
+
export const PLANS_RELPATH = ".doug/plans";
|
|
15
|
+
export const REPORTS_RELPATH = ".doug/.state/reports";
|
|
16
|
+
export const DEFAULT_INTEGRATION_WORKTREE = ".claude/worktrees/doug-integration";
|
|
17
|
+
export const planPathFor = (card) => `${PLANS_RELPATH}/${card}.json`;
|
|
18
|
+
export const reportPathFor = (card) => `${REPORTS_RELPATH}/${card}/last-report.json`;
|
|
12
19
|
// One draft per card of a batch (card parallel-cards): the planner for card <id> writes .doug/.state/drafts/<id>.json
|
|
13
20
|
// and `plan.mjs merge` joins them into the plan file.
|
|
14
21
|
export const DRAFTS_RELPATH = ".doug/.state/drafts";
|
|
@@ -17,7 +24,7 @@ export const SIZES = ["S", "M", "L"];
|
|
|
17
24
|
export const GATES = ["auto", "human"];
|
|
18
25
|
// Crew per role (card crew-sizing): researchers before planning, reviewers and adversaries after implementation,
|
|
19
26
|
// default one each. More coders is a split into more tasks, so there is no coders row.
|
|
20
|
-
export const CREW_ROLES = ["researchers", "reviewers", "adversaries"];
|
|
27
|
+
export const CREW_ROLES = ["researchers", "reviewers", "adversaries", "testers"];
|
|
21
28
|
|
|
22
29
|
// Error strings for a crew object at `where` (the plan or a task); [] when valid or absent.
|
|
23
30
|
function crewErrors(crew, where) {
|
|
@@ -55,8 +62,24 @@ export function draftPath(card) {
|
|
|
55
62
|
return `${DRAFTS_RELPATH}/${card}.json`;
|
|
56
63
|
}
|
|
57
64
|
|
|
58
|
-
|
|
59
|
-
|
|
65
|
+
// The plan a card's commands read: its per-card plan when one exists; else the single .doug/plan.json, but only
|
|
66
|
+
// when no card is given or that plan is this card's (plan.card equals it, or plan.cards includes it), so a stale
|
|
67
|
+
// plan for another card is never read. Null otherwise, and when neither exists.
|
|
68
|
+
export function planFor(dir, card) {
|
|
69
|
+
const hasCard = typeof card === "string" && card !== "";
|
|
70
|
+
if (hasCard && existsSync(join(dir, planPathFor(card)))) return loadPlan(dir, planPathFor(card));
|
|
71
|
+
const plan = loadPlan(dir);
|
|
72
|
+
if (!plan || !hasCard) return plan;
|
|
73
|
+
return plan.card === card || (Array.isArray(plan.cards) && plan.cards.includes(card)) ? plan : null;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// The plan's integration worktree: its own when set (a per-card run), else the fixed shared path.
|
|
77
|
+
export function integrationWorktreeFor(plan) {
|
|
78
|
+
return (plan && plan.integrationWorktree) || DEFAULT_INTEGRATION_WORKTREE;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function savePlan(dir, plan, relpath = PLAN_RELPATH) {
|
|
82
|
+
const file = join(dir, relpath);
|
|
60
83
|
mkdirSync(dirname(file), { recursive: true });
|
|
61
84
|
const tmp = file + "." + process.pid + ".tmp";
|
|
62
85
|
writeFileSync(tmp, JSON.stringify(plan, null, 2) + "\n");
|
|
@@ -76,8 +99,8 @@ export function withSpecHashes(plan) {
|
|
|
76
99
|
return { ...plan, tasks: plan.tasks.map((t) => ({ ...t, specHash: specHash(t) })) };
|
|
77
100
|
}
|
|
78
101
|
|
|
79
|
-
export function loadReport(dir) {
|
|
80
|
-
const file = join(dir,
|
|
102
|
+
export function loadReport(dir, relpath = REPORT_RELPATH) {
|
|
103
|
+
const file = join(dir, relpath);
|
|
81
104
|
if (!existsSync(file)) return null;
|
|
82
105
|
try {
|
|
83
106
|
return unwrapReport(JSON.parse(readFileSync(file, "utf8")));
|
|
@@ -396,6 +419,11 @@ export function validatePlan(plan) {
|
|
|
396
419
|
});
|
|
397
420
|
}
|
|
398
421
|
if (!Array.isArray(plan.verify) || !plan.verify.every((v) => typeof v === "string" && v.trim())) errors.push("verify must be an array of command strings");
|
|
422
|
+
if (plan.integrationWorktree !== undefined) {
|
|
423
|
+
const w = plan.integrationWorktree;
|
|
424
|
+
const under = typeof w === "string" && w.startsWith(".claude/worktrees/") && w.length > ".claude/worktrees/".length && !w.split("/").includes("..");
|
|
425
|
+
if (!under) errors.push("integrationWorktree must be a path under .claude/worktrees/ (the gate only recognises worktrees there)");
|
|
426
|
+
}
|
|
399
427
|
if (plan.card !== undefined && (typeof plan.card !== "string" || !plan.card.trim())) errors.push("card must be a non-empty string when present");
|
|
400
428
|
// A batch (several cards in one plan) lists its cards; every task then names the card it belongs to.
|
|
401
429
|
let cards = null;
|
|
@@ -198,15 +198,15 @@ export function retireTaskWorktrees(dir, { tasks = [], saveDir }) {
|
|
|
198
198
|
return result;
|
|
199
199
|
}
|
|
200
200
|
|
|
201
|
-
// Retires the integration worktree at .claude/worktrees/doug-integration, if one is registered (card
|
|
201
|
+
// Retires the integration worktree at `path` (default .claude/worktrees/doug-integration; a per-card run passes its own), if one is registered (card
|
|
202
202
|
// integration-worktree-stale, 2026-09-08): a leftover from an abandoned run must not survive into the next one,
|
|
203
203
|
// since the workflow's own leftover-vs-this-run's-own check (integratePrompt's firstIntegration) only runs inside
|
|
204
204
|
// a run already underway. Removed with --force; the branch is never renamed or deleted (the workflow's
|
|
205
205
|
// `git worktree add -B` on the next run's first integration resets it from the base branch). A worktree that is
|
|
206
206
|
// not there is not an error.
|
|
207
207
|
// Returns { removed: string | null } - the path removed, or null when none was registered.
|
|
208
|
-
export function retireIntegrationWorktree(dir) {
|
|
209
|
-
const target =
|
|
208
|
+
export function retireIntegrationWorktree(dir, path = ".claude/worktrees/doug-integration") {
|
|
209
|
+
const target = resolve(realpathOrSelf(dir), path);
|
|
210
210
|
const match = taskWorktrees(dir).find((wt) => realpathOrSelf(wt.path) === realpathOrSelf(target));
|
|
211
211
|
if (!match) return { removed: null };
|
|
212
212
|
const removal = tryGit(dir, ["worktree", "remove", "--force", match.path]);
|
|
@@ -0,0 +1,440 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// The lead's autopilot state file writer (card autopilot-live-queue): .doug/.state/autopilot.json, which the live pane
|
|
3
|
+
// and the served board's autopilot panel read. Each subcommand prints one line `autopilot: <what>`.
|
|
4
|
+
// autopilot.mjs start --cards <id,id,...> --usd-cap <n> --wall-cap <min> [--context-threshold <pct>]
|
|
5
|
+
// autopilot.mjs add <id> [--usd-cap <n>] [--wall-cap <min>]
|
|
6
|
+
// autopilot.mjs card <id>
|
|
7
|
+
// autopilot.mjs step <id> <research|tester|coder|reviewer|gate|commit|record>
|
|
8
|
+
// autopilot.mjs land <id> --commit <sha>
|
|
9
|
+
// autopilot.mjs stop <id> --reason "<why>"
|
|
10
|
+
// autopilot.mjs filed <id> --kind <follow-up|user> [--from <card>] --position "<text>"
|
|
11
|
+
// autopilot.mjs caps [--usd <n>] [--wall <min>] [--context <pct>]
|
|
12
|
+
// autopilot.mjs pick [--max <n>] JSON { launch, needsPlan, held, alone }; a held line per held card on stderr
|
|
13
|
+
// autopilot.mjs queen-args <id> --kind <flow|hand> [--with <id,id,...>] writes .doug/.state/reports/<id>/queen-args.json and the per-card queen.js beside it, prints both paths (args, then queen.js)
|
|
14
|
+
// autopilot.mjs agent-types --kind <flow|hand> prints the agent types that queen spawns, one per line
|
|
15
|
+
// autopilot.mjs launched <id> --run <wf_id> --kind <flow|hand> --worktree <abs> --args-file <path>
|
|
16
|
+
// autopilot.mjs report --run <wf_id> --file <result.json> saves the queen's report, prints land|resume|ask
|
|
17
|
+
// autopilot.mjs resumed <id> --run <wf_id>
|
|
18
|
+
// autopilot.mjs lock-wait <id> --ms <n>
|
|
19
|
+
// autopilot.mjs end
|
|
20
|
+
// --dir <d> names the project (default cwd). Exit 0 done; 1 no run or unknown card; 2 bad usage.
|
|
21
|
+
import { execFileSync } from "node:child_process";
|
|
22
|
+
import { copyFileSync, mkdirSync, readFileSync, readdirSync, renameSync, rmSync, writeFileSync } from "node:fs";
|
|
23
|
+
import { dirname, join, resolve } from "node:path";
|
|
24
|
+
import { fileURLToPath } from "node:url";
|
|
25
|
+
import { loadModels } from "../lib/models.mjs";
|
|
26
|
+
import { AUTOPILOT_FILE, STEPS, readAutopilot } from "../lib/autopilot-view.mjs";
|
|
27
|
+
import { contextThreshold } from "../lib/context.mjs";
|
|
28
|
+
import { classifyNotification, pickCards, queenLabel, renameQueenMeta, workersMax } from "../lib/coordinator.mjs";
|
|
29
|
+
import { nextReadyCards } from "../lib/board.mjs";
|
|
30
|
+
import { planFor, reportPathFor } from "../lib/plan.mjs";
|
|
31
|
+
|
|
32
|
+
const argv = process.argv.slice(2);
|
|
33
|
+
const opts = {};
|
|
34
|
+
const pos = [];
|
|
35
|
+
for (let i = 0; i < argv.length; i++) {
|
|
36
|
+
if (argv[i].startsWith("--")) {
|
|
37
|
+
opts[argv[i].slice(2)] = argv[i + 1] ?? "";
|
|
38
|
+
i++;
|
|
39
|
+
} else pos.push(argv[i]);
|
|
40
|
+
}
|
|
41
|
+
const [cmd, id, arg2] = pos;
|
|
42
|
+
const dir = resolve(opts.dir || process.cwd());
|
|
43
|
+
const file = join(dir, AUTOPILOT_FILE);
|
|
44
|
+
|
|
45
|
+
const usage = (msg) => {
|
|
46
|
+
process.stderr.write(`autopilot: ${msg}\n`);
|
|
47
|
+
process.exit(2);
|
|
48
|
+
};
|
|
49
|
+
const fail = (msg) => {
|
|
50
|
+
process.stderr.write(`autopilot: ${msg}\n`);
|
|
51
|
+
process.exit(1);
|
|
52
|
+
};
|
|
53
|
+
const number = (flag, v) => {
|
|
54
|
+
const n = Number(v);
|
|
55
|
+
if (v === undefined || v === "" || !Number.isFinite(n) || n < 0) usage(`--${flag} needs a non-negative number`);
|
|
56
|
+
return n;
|
|
57
|
+
};
|
|
58
|
+
const need = (flag) => {
|
|
59
|
+
if (opts[flag] === undefined || opts[flag] === "") usage(`--${flag} is required`);
|
|
60
|
+
return opts[flag];
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
// tmp + rename, so a reader never sees a half-written file; the tmp name is not left behind.
|
|
64
|
+
function save(state) {
|
|
65
|
+
state.updatedAt = new Date().toISOString();
|
|
66
|
+
mkdirSync(dirname(file), { recursive: true });
|
|
67
|
+
const tmp = `${file}.${process.pid}.tmp`;
|
|
68
|
+
writeFileSync(tmp, JSON.stringify(state, null, 2) + "\n");
|
|
69
|
+
renameSync(tmp, file);
|
|
70
|
+
}
|
|
71
|
+
const say = (what) => process.stdout.write(`autopilot: ${what}\n`);
|
|
72
|
+
const NO_RUN = "no autopilot run; run autopilot.mjs start first";
|
|
73
|
+
|
|
74
|
+
function load() {
|
|
75
|
+
const s = readAutopilot(dir);
|
|
76
|
+
if (!s) fail(NO_RUN);
|
|
77
|
+
if (!Array.isArray(s.cards)) s.cards = [];
|
|
78
|
+
return s;
|
|
79
|
+
}
|
|
80
|
+
function cardOf(s, cardId) {
|
|
81
|
+
const c = s.cards.find((x) => x.id === cardId);
|
|
82
|
+
if (!c) fail(`unknown card ${cardId}`);
|
|
83
|
+
return c;
|
|
84
|
+
}
|
|
85
|
+
const clearStep = (c) => {
|
|
86
|
+
delete c.step;
|
|
87
|
+
delete c.stepStartedAt;
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
function boardCard(cardId) {
|
|
91
|
+
try {
|
|
92
|
+
const b = JSON.parse(readFileSync(join(dir, ".doug/board.json"), "utf8"));
|
|
93
|
+
return b.cards.find((c) => c.id === cardId) || null;
|
|
94
|
+
} catch {
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
// A swarm card carries the tag "swarm", a field swarm: true, or an explicit request in its goal ("run it as a
|
|
99
|
+
// swarm", "swarm: on"); the word swarm alone in a goal does not make one.
|
|
100
|
+
function isSwarmCard(c) {
|
|
101
|
+
if (c.swarm === true || (Array.isArray(c.tags) && c.tags.includes("swarm"))) return true;
|
|
102
|
+
return /\b(?:run|build|land)s?\b[^.]{0,20}\bas a swarm\b|\bswarm:\s*on\b/i.test(c.goal ?? "");
|
|
103
|
+
}
|
|
104
|
+
function queued(cardId) {
|
|
105
|
+
const b = boardCard(cardId);
|
|
106
|
+
const c = { id: cardId };
|
|
107
|
+
for (const k of ["title", "size", "class"]) if (b && b[k] !== undefined) c[k] = b[k];
|
|
108
|
+
c.state = "queued";
|
|
109
|
+
return c;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
switch (cmd) {
|
|
113
|
+
case "start": {
|
|
114
|
+
const ids = need("cards").split(",").map((x) => x.trim()).filter(Boolean);
|
|
115
|
+
if (!ids.length) usage("--cards names no card");
|
|
116
|
+
const usdCap = number("usd-cap", opts["usd-cap"]);
|
|
117
|
+
const wallCap = number("wall-cap", opts["wall-cap"]);
|
|
118
|
+
let threshold;
|
|
119
|
+
if (opts["context-threshold"] !== undefined) threshold = number("context-threshold", opts["context-threshold"]);
|
|
120
|
+
else {
|
|
121
|
+
let config = {};
|
|
122
|
+
try {
|
|
123
|
+
config = JSON.parse(readFileSync(join(dir, ".doug/config.json"), "utf8"));
|
|
124
|
+
} catch {
|
|
125
|
+
// absent or unreadable: the default
|
|
126
|
+
}
|
|
127
|
+
threshold = contextThreshold(config);
|
|
128
|
+
}
|
|
129
|
+
const now = new Date().toISOString();
|
|
130
|
+
save({
|
|
131
|
+
version: 1,
|
|
132
|
+
status: "running",
|
|
133
|
+
startedAt: now,
|
|
134
|
+
updatedAt: now,
|
|
135
|
+
endedAt: null,
|
|
136
|
+
caps: { usd: usdCap, wallMin: wallCap, contextPct: threshold },
|
|
137
|
+
cards: ids.map(queued),
|
|
138
|
+
filed: [],
|
|
139
|
+
});
|
|
140
|
+
say(`started with ${ids.length} card${ids.length === 1 ? "" : "s"}`);
|
|
141
|
+
break;
|
|
142
|
+
}
|
|
143
|
+
case "add": {
|
|
144
|
+
if (!id) usage("add needs a card id");
|
|
145
|
+
const s = load();
|
|
146
|
+
if (!s.caps || typeof s.caps !== "object") s.caps = {};
|
|
147
|
+
if (opts["usd-cap"] !== undefined) s.caps.usd = number("usd-cap", opts["usd-cap"]);
|
|
148
|
+
if (opts["wall-cap"] !== undefined) s.caps.wallMin = number("wall-cap", opts["wall-cap"]);
|
|
149
|
+
const dup = s.cards.some((c) => c.id === id);
|
|
150
|
+
if (!dup) s.cards.push(queued(id));
|
|
151
|
+
save(s);
|
|
152
|
+
say(dup ? `${id} already queued` : `added ${id}`);
|
|
153
|
+
break;
|
|
154
|
+
}
|
|
155
|
+
case "card": {
|
|
156
|
+
if (!id) usage("card needs a card id");
|
|
157
|
+
const s = load();
|
|
158
|
+
const c = cardOf(s, id);
|
|
159
|
+
c.state = "running";
|
|
160
|
+
clearStep(c);
|
|
161
|
+
save(s);
|
|
162
|
+
say(`${id} running`);
|
|
163
|
+
break;
|
|
164
|
+
}
|
|
165
|
+
case "step": {
|
|
166
|
+
if (!id || !arg2) usage("step needs a card id and a step");
|
|
167
|
+
if (!STEPS.includes(arg2)) usage(`step must be one of ${STEPS.join(", ")}`);
|
|
168
|
+
const s = load();
|
|
169
|
+
const c = cardOf(s, id);
|
|
170
|
+
c.step = arg2;
|
|
171
|
+
c.stepStartedAt = new Date().toISOString();
|
|
172
|
+
save(s);
|
|
173
|
+
say(`${id} ${arg2}`);
|
|
174
|
+
break;
|
|
175
|
+
}
|
|
176
|
+
case "land": {
|
|
177
|
+
if (!id) usage("land needs a card id");
|
|
178
|
+
const commit = need("commit");
|
|
179
|
+
const s = load();
|
|
180
|
+
const c = cardOf(s, id);
|
|
181
|
+
c.state = "landed";
|
|
182
|
+
c.commit = commit;
|
|
183
|
+
delete c.reason;
|
|
184
|
+
clearStep(c);
|
|
185
|
+
save(s);
|
|
186
|
+
say(`${id} landed ${commit}`);
|
|
187
|
+
break;
|
|
188
|
+
}
|
|
189
|
+
case "stop": {
|
|
190
|
+
if (!id) usage("stop needs a card id");
|
|
191
|
+
const reason = need("reason");
|
|
192
|
+
const s = load();
|
|
193
|
+
const c = cardOf(s, id);
|
|
194
|
+
c.state = "stopped";
|
|
195
|
+
c.reason = reason;
|
|
196
|
+
clearStep(c);
|
|
197
|
+
save(s);
|
|
198
|
+
say(`${id} stopped`);
|
|
199
|
+
break;
|
|
200
|
+
}
|
|
201
|
+
case "filed": {
|
|
202
|
+
if (!id) usage("filed needs a card id");
|
|
203
|
+
const kind = need("kind");
|
|
204
|
+
if (kind !== "follow-up" && kind !== "user") usage("--kind must be follow-up or user");
|
|
205
|
+
const position = need("position");
|
|
206
|
+
const s = load();
|
|
207
|
+
if (!Array.isArray(s.filed)) s.filed = [];
|
|
208
|
+
s.filed.push({ id, kind, ...(opts.from ? { from: opts.from } : {}), position });
|
|
209
|
+
save(s);
|
|
210
|
+
say(`filed ${id} (${kind})`);
|
|
211
|
+
break;
|
|
212
|
+
}
|
|
213
|
+
case "caps": {
|
|
214
|
+
const keys = ["usd", "wall", "context"].filter((k) => opts[k] !== undefined);
|
|
215
|
+
if (!keys.length) usage("caps needs --usd, --wall, or --context");
|
|
216
|
+
const vals = Object.fromEntries(keys.map((k) => [k, number(k, opts[k])]));
|
|
217
|
+
const s = load();
|
|
218
|
+
s.used = {
|
|
219
|
+
...(s.used || {}),
|
|
220
|
+
...(vals.usd !== undefined ? { usd: vals.usd } : {}),
|
|
221
|
+
...(vals.wall !== undefined ? { wallMin: vals.wall } : {}),
|
|
222
|
+
...(vals.context !== undefined ? { contextPct: vals.context } : {}),
|
|
223
|
+
checkedAt: new Date().toISOString(),
|
|
224
|
+
};
|
|
225
|
+
save(s);
|
|
226
|
+
say("caps recorded");
|
|
227
|
+
break;
|
|
228
|
+
}
|
|
229
|
+
case "pick": {
|
|
230
|
+
const s = load();
|
|
231
|
+
let board;
|
|
232
|
+
try {
|
|
233
|
+
board = JSON.parse(readFileSync(join(dir, ".doug/board.json"), "utf8"));
|
|
234
|
+
} catch {
|
|
235
|
+
fail("no readable .doug/board.json");
|
|
236
|
+
}
|
|
237
|
+
let config = {};
|
|
238
|
+
try {
|
|
239
|
+
config = JSON.parse(readFileSync(join(dir, ".doug/config.json"), "utf8"));
|
|
240
|
+
} catch {
|
|
241
|
+
// absent or unreadable: the default
|
|
242
|
+
}
|
|
243
|
+
const max = opts.max !== undefined ? Math.floor(number("max", opts.max)) : workersMax(config, (m) => process.stderr.write(`autopilot: ${m}\n`));
|
|
244
|
+
const trackOf = (c) => (c.track === "hand" ? "hand" : "flow");
|
|
245
|
+
const shape = (c) => ({
|
|
246
|
+
id: c.id,
|
|
247
|
+
goal: c.goal ?? "",
|
|
248
|
+
component: c.component,
|
|
249
|
+
track: trackOf(c),
|
|
250
|
+
class: c.class,
|
|
251
|
+
swarm: isSwarmCard(c),
|
|
252
|
+
});
|
|
253
|
+
const flying = s.cards.filter((c) => ["running", "done", "landing"].includes(c.state));
|
|
254
|
+
const flyingIds = new Set(flying.map((c) => c.id));
|
|
255
|
+
const inFlight = flying.map((c) => {
|
|
256
|
+
const b = boardCard(c.id);
|
|
257
|
+
return b ? shape(b) : { id: c.id, goal: "", track: c.kind === "flow" ? "flow" : "hand" };
|
|
258
|
+
});
|
|
259
|
+
const ready = nextReadyCards(board, { track: "any", batch: Math.max(1, board.cards.length) })
|
|
260
|
+
.cards.filter((c) => !flyingIds.has(c.id))
|
|
261
|
+
.map(shape);
|
|
262
|
+
const plans = {};
|
|
263
|
+
for (const c of [...ready, ...inFlight]) {
|
|
264
|
+
if (c.track !== "flow") continue;
|
|
265
|
+
let p = null;
|
|
266
|
+
try {
|
|
267
|
+
p = planFor(dir, c.id);
|
|
268
|
+
} catch {
|
|
269
|
+
// an unreadable plan counts as no plan
|
|
270
|
+
}
|
|
271
|
+
if (p) plans[c.id] = p;
|
|
272
|
+
}
|
|
273
|
+
const roots = readdirSync(dir, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
|
|
274
|
+
const out = pickCards({ ready, inFlight, slots: Math.max(0, max - inFlight.length), plans, roots });
|
|
275
|
+
for (const h of out.held) process.stderr.write(`held ${h.id}: overlaps ${h.with} (${h.why})\n`);
|
|
276
|
+
process.stdout.write(JSON.stringify(out) + "\n");
|
|
277
|
+
break;
|
|
278
|
+
}
|
|
279
|
+
case "queen-args": {
|
|
280
|
+
if (!id) usage("queen-args needs a card id");
|
|
281
|
+
const kind = need("kind");
|
|
282
|
+
if (kind !== "flow" && kind !== "hand") usage("--kind must be flow or hand");
|
|
283
|
+
let args;
|
|
284
|
+
if (kind === "flow") {
|
|
285
|
+
try {
|
|
286
|
+
args = JSON.parse(execFileSync(process.execPath, [join(dirname(fileURLToPath(import.meta.url)), "plan.mjs"), "json", "--card", id], { cwd: dir, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"], maxBuffer: 64 * 1024 * 1024 }));
|
|
287
|
+
} catch (e) {
|
|
288
|
+
fail(`plan.mjs json --card ${id} failed: ${String(e.stderr || e.message).trim()}`);
|
|
289
|
+
}
|
|
290
|
+
} else {
|
|
291
|
+
const b = boardCard(id);
|
|
292
|
+
if (!b) fail(`unknown card ${id}`);
|
|
293
|
+
let config = {};
|
|
294
|
+
try {
|
|
295
|
+
config = JSON.parse(readFileSync(join(dir, ".doug/config.json"), "utf8"));
|
|
296
|
+
} catch {
|
|
297
|
+
// absent or unreadable: no install command
|
|
298
|
+
}
|
|
299
|
+
args = {
|
|
300
|
+
card: { id: b.id, title: b.title ?? null, size: b.size ?? null, class: b.class ?? null, goal: b.goal ?? "" },
|
|
301
|
+
options: {
|
|
302
|
+
testerAgent: true,
|
|
303
|
+
research: false,
|
|
304
|
+
install: (config.commands && config.commands.install) || null,
|
|
305
|
+
gateCommand: "node .doug/hooks/scripts/verify.mjs",
|
|
306
|
+
branch: `card/${id}`,
|
|
307
|
+
base: "main",
|
|
308
|
+
},
|
|
309
|
+
models: loadModels(dir),
|
|
310
|
+
repoRoot: dir,
|
|
311
|
+
worktreePath: join(dir, ".claude/worktrees", `card-${id}`),
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
const out = join(dir, ".doug/.state/reports", id, "queen-args.json");
|
|
315
|
+
mkdirSync(dirname(out), { recursive: true });
|
|
316
|
+
writeFileSync(out, JSON.stringify(args) + "\n");
|
|
317
|
+
const flying = (readAutopilot(dir)?.cards ?? []).filter((c) => ["running", "done", "landing"].includes(c.state)).map((c) => c.id);
|
|
318
|
+
const batch = String(opts.with ?? "").split(",").map((x) => x.trim()).filter(Boolean);
|
|
319
|
+
const srcFile = join(dirname(fileURLToPath(import.meta.url)), "..", "workflows", kind === "hand" ? "card-queen.js" : "doug-implement.js");
|
|
320
|
+
let renamed;
|
|
321
|
+
try {
|
|
322
|
+
renamed = renameQueenMeta(readFileSync(srcFile, "utf8"), { kind, label: queenLabel(id, [...flying, ...batch]) });
|
|
323
|
+
} catch (e) {
|
|
324
|
+
fail(`cannot build the per-card queen from ${srcFile}: ${e.message}`);
|
|
325
|
+
}
|
|
326
|
+
const script = join(dirname(out), "queen.js");
|
|
327
|
+
rmSync(script, { force: true });
|
|
328
|
+
writeFileSync(script, renamed);
|
|
329
|
+
process.stdout.write(out + "\n" + script + "\n");
|
|
330
|
+
break;
|
|
331
|
+
}
|
|
332
|
+
case "agent-types": {
|
|
333
|
+
const kind = need("kind");
|
|
334
|
+
if (kind !== "flow" && kind !== "hand") usage("--kind must be flow or hand");
|
|
335
|
+
const wf = join(dirname(fileURLToPath(import.meta.url)), "..", "workflows", kind === "hand" ? "card-queen.js" : "doug-implement.js");
|
|
336
|
+
let src;
|
|
337
|
+
try {
|
|
338
|
+
src = readFileSync(wf, "utf8");
|
|
339
|
+
} catch (e) {
|
|
340
|
+
fail(`cannot read ${wf}: ${e.message}`);
|
|
341
|
+
}
|
|
342
|
+
if (kind === "hand") {
|
|
343
|
+
const m = src.match(/const AGENT_TYPES = \{([^}]*)\}/);
|
|
344
|
+
if (!m) fail("AGENT_TYPES not found in card-queen.js");
|
|
345
|
+
src = m[1];
|
|
346
|
+
}
|
|
347
|
+
const types = [...new Set([...src.matchAll(kind === "hand" ? /'(doug-flow:[\w-]+)'/g : /agentType: '([^']+)'/g)].map((x) => x[1]))];
|
|
348
|
+
if (types.length === 0) fail("no agent types found");
|
|
349
|
+
process.stdout.write(types.join("\n") + "\n");
|
|
350
|
+
break;
|
|
351
|
+
}
|
|
352
|
+
case "launched": {
|
|
353
|
+
if (!id) usage("launched needs a card id");
|
|
354
|
+
const run = need("run");
|
|
355
|
+
const kind = need("kind");
|
|
356
|
+
if (kind !== "flow" && kind !== "hand") usage("--kind must be flow or hand");
|
|
357
|
+
const worktree = need("worktree");
|
|
358
|
+
const argsFile = need("args-file");
|
|
359
|
+
const s = load();
|
|
360
|
+
const c = cardOf(s, id);
|
|
361
|
+
const reportDir = join(dir, ".doug/.state/reports", id);
|
|
362
|
+
mkdirSync(reportDir, { recursive: true });
|
|
363
|
+
try {
|
|
364
|
+
copyFileSync(argsFile, join(reportDir, "queen-args.json"));
|
|
365
|
+
} catch (e) {
|
|
366
|
+
fail(`cannot read --args-file ${argsFile}: ${e.code || e.message}`);
|
|
367
|
+
}
|
|
368
|
+
c.state = "running";
|
|
369
|
+
c.runId = run;
|
|
370
|
+
c.kind = kind;
|
|
371
|
+
c.worktree = worktree;
|
|
372
|
+
clearStep(c);
|
|
373
|
+
save(s);
|
|
374
|
+
say(`${id} launched as ${run}`);
|
|
375
|
+
break;
|
|
376
|
+
}
|
|
377
|
+
case "report": {
|
|
378
|
+
const run = need("run");
|
|
379
|
+
const resultFile = need("file");
|
|
380
|
+
const s = load();
|
|
381
|
+
const c = s.cards.find((x) => x.runId === run);
|
|
382
|
+
if (!c) fail(`unknown run ${run}`);
|
|
383
|
+
let text;
|
|
384
|
+
try {
|
|
385
|
+
text = readFileSync(resultFile, "utf8");
|
|
386
|
+
} catch (e) {
|
|
387
|
+
fail(`cannot read --file ${resultFile}: ${e.code || e.message}`);
|
|
388
|
+
}
|
|
389
|
+
let parsed = null;
|
|
390
|
+
try {
|
|
391
|
+
parsed = JSON.parse(text);
|
|
392
|
+
} catch {
|
|
393
|
+
// unreadable: classify says resume
|
|
394
|
+
}
|
|
395
|
+
const kind = c.kind === "flow" ? "flow" : "hand";
|
|
396
|
+
if (parsed !== null) {
|
|
397
|
+
const dest = kind === "flow" ? join(dir, reportPathFor(c.id)) : join(dir, ".doug/.state/reports", c.id, "card-report.json");
|
|
398
|
+
try {
|
|
399
|
+
mkdirSync(dirname(dest), { recursive: true });
|
|
400
|
+
writeFileSync(dest, JSON.stringify(kind === "hand" && parsed.report ? parsed.report : parsed, null, 2) + "\n");
|
|
401
|
+
} catch (e) {
|
|
402
|
+
process.stderr.write(`autopilot: warning: cannot write ${dest}: ${e.code || e.message}\n`);
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
const v = classifyNotification(kind, { status: "completed", result: parsed === null ? text : parsed });
|
|
406
|
+
say(`${c.id} ${v.action}${v.reason ? ` ${v.reason}` : ""}`);
|
|
407
|
+
break;
|
|
408
|
+
}
|
|
409
|
+
case "resumed": {
|
|
410
|
+
if (!id) usage("resumed needs a card id");
|
|
411
|
+
const run = need("run");
|
|
412
|
+
const s = load();
|
|
413
|
+
const c = cardOf(s, id);
|
|
414
|
+
c.resumes = (c.resumes || 0) + 1;
|
|
415
|
+
c.runId = run;
|
|
416
|
+
save(s);
|
|
417
|
+
say(`${id} resumed as ${run}`);
|
|
418
|
+
break;
|
|
419
|
+
}
|
|
420
|
+
case "lock-wait": {
|
|
421
|
+
if (!id) usage("lock-wait needs a card id");
|
|
422
|
+
const ms = number("ms", opts.ms);
|
|
423
|
+
const s = load();
|
|
424
|
+
const c = cardOf(s, id);
|
|
425
|
+
c.lockWaitMs = (c.lockWaitMs || 0) + ms;
|
|
426
|
+
save(s);
|
|
427
|
+
say(`${id} lock wait ${c.lockWaitMs}ms`);
|
|
428
|
+
break;
|
|
429
|
+
}
|
|
430
|
+
case "end": {
|
|
431
|
+
const s = load();
|
|
432
|
+
s.status = "ended";
|
|
433
|
+
s.endedAt = new Date().toISOString();
|
|
434
|
+
save(s);
|
|
435
|
+
say("ended");
|
|
436
|
+
break;
|
|
437
|
+
}
|
|
438
|
+
default:
|
|
439
|
+
usage(`unknown subcommand${cmd ? ` ${cmd}` : ""}; use start, add, card, step, land, stop, filed, caps, pick, launched, report, resumed, lock-wait, or end`);
|
|
440
|
+
}
|