@dougharness/cli 1.0.2 → 1.1.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/bin.js +8 -0
- package/dist/bin.js.map +1 -1
- package/dist/board-serve.js +9 -3
- package/dist/board-serve.js.map +1 -1
- package/dist/board.js +10 -2
- package/dist/board.js.map +1 -1
- package/dist/detect/repo.js +14 -1
- package/dist/detect/repo.js.map +1 -1
- package/dist/detect/types.d.ts +2 -0
- package/dist/doctor.d.ts +46 -0
- package/dist/doctor.js +491 -0
- package/dist/doctor.js.map +1 -0
- package/dist/generate/agents.d.ts +7 -0
- package/dist/generate/agents.js +62 -12
- package/dist/generate/agents.js.map +1 -1
- package/dist/generate/claude-md.js +4 -0
- package/dist/generate/claude-md.js.map +1 -1
- package/dist/generate/config.d.ts +8 -0
- package/dist/generate/config.js +4 -1
- package/dist/generate/config.js.map +1 -1
- package/dist/generate/proposal.js +15 -6
- package/dist/generate/proposal.js.map +1 -1
- package/dist/generate/settings.d.ts +2 -0
- package/dist/generate/settings.js +17 -2
- package/dist/generate/settings.js.map +1 -1
- package/dist/init.d.ts +14 -0
- package/dist/init.js +94 -8
- package/dist/init.js.map +1 -1
- package/dist/memory-setup.d.ts +28 -0
- package/dist/memory-setup.js +323 -0
- package/dist/memory-setup.js.map +1 -0
- package/node_modules/@dougharness/flow/.claude-plugin/plugin.json +16 -4
- package/node_modules/@dougharness/flow/agents/planner.md +2 -2
- package/node_modules/@dougharness/flow/agents/researcher.md +2 -2
- package/node_modules/@dougharness/flow/hooks/hooks.json +16 -0
- package/node_modules/@dougharness/flow/lib/board.mjs +154 -12
- package/node_modules/@dougharness/flow/lib/code-index.mjs +18 -3
- package/node_modules/@dougharness/flow/lib/context.mjs +57 -0
- package/node_modules/@dougharness/flow/lib/cost.mjs +129 -4
- package/node_modules/@dougharness/flow/lib/embeddings.mjs +185 -23
- package/node_modules/@dougharness/flow/lib/land.mjs +69 -10
- package/node_modules/@dougharness/flow/lib/learn.mjs +4 -4
- package/node_modules/@dougharness/flow/lib/memory.mjs +326 -35
- package/node_modules/@dougharness/flow/lib/plan.mjs +9 -0
- package/node_modules/@dougharness/flow/package.json +2 -1
- package/node_modules/@dougharness/flow/scripts/board.mjs +60 -20
- package/node_modules/@dougharness/flow/scripts/context.mjs +48 -0
- package/node_modules/@dougharness/flow/scripts/cost.mjs +15 -3
- package/node_modules/@dougharness/flow/scripts/memory.mjs +190 -34
- package/node_modules/@dougharness/flow/scripts/plan.mjs +32 -64
- package/node_modules/@dougharness/flow/scripts/session-recall.mjs +132 -0
- package/node_modules/@dougharness/flow/skills/{core-next → doug-hand}/SKILL.md +31 -18
- package/node_modules/@dougharness/flow/skills/doug-learn/SKILL.md +1 -1
- package/node_modules/@dougharness/flow/skills/doug-next/SKILL.md +44 -10
- package/node_modules/@dougharness/flow/skills/doug-swarm/SKILL.md +5 -5
- package/node_modules/@dougharness/flow/skills/research/SKILL.md +4 -2
- package/node_modules/@dougharness/flow/skills/run-report/SKILL.md +8 -9
- package/node_modules/@dougharness/flow/skills/swarm-launch/SKILL.md +1 -1
- package/node_modules/@dougharness/flow/workflows/doug-implement.js +119 -72
- package/node_modules/@dougharness/gates/.claude-plugin/plugin.json +1 -1
- package/node_modules/@dougharness/gates/hooks/hooks.json +11 -0
- package/node_modules/@dougharness/gates/lib/baseline.mjs +166 -1
- package/node_modules/@dougharness/gates/lib/block-ledger.mjs +58 -1
- package/node_modules/@dougharness/gates/lib/checkpoint.mjs +1 -1
- package/node_modules/@dougharness/gates/lib/config.mjs +20 -0
- package/node_modules/@dougharness/gates/lib/docs-gate.mjs +95 -0
- package/node_modules/@dougharness/gates/lib/evidence.mjs +2 -0
- package/node_modules/@dougharness/gates/lib/run.mjs +4 -0
- package/node_modules/@dougharness/gates/lib/test-totals.mjs +40 -7
- package/node_modules/@dougharness/gates/lib/trace.mjs +24 -7
- package/node_modules/@dougharness/gates/lib/verified-tree.mjs +225 -0
- package/node_modules/@dougharness/gates/package.json +1 -1
- package/node_modules/@dougharness/gates/scripts/research-cap.mjs +133 -5
- package/node_modules/@dougharness/gates/scripts/stop-gate.mjs +551 -113
- package/node_modules/@dougharness/gates/scripts/trace.mjs +5 -1
- package/node_modules/@dougharness/gates/scripts/turn-start.mjs +17 -0
- package/node_modules/@dougharness/gates/scripts/verify.mjs +146 -0
- package/package.json +3 -3
- package/templates/board-app.js +2 -2
- package/node_modules/@dougharness/flow/lib/rehearse.mjs +0 -825
- package/node_modules/@dougharness/flow/scripts/rehearse.mjs +0 -129
- package/node_modules/@dougharness/flow/skills/harness-fix/SKILL.md +0 -85
|
@@ -95,53 +95,6 @@ function withDefaults(p, d) {
|
|
|
95
95
|
if (out.adversary !== false && !hasCommand && configAdversary(d)) out = { ...out, adversary: { ...(out.adversary && typeof out.adversary === "object" ? out.adversary : {}), command: configAdversary(d) } };
|
|
96
96
|
return out;
|
|
97
97
|
}
|
|
98
|
-
// Renders recalled lessons as one line per lesson, in rank order. Each lesson's own text gets a fixed budget
|
|
99
|
-
// (LESSON_CHAR_BUDGET): text over budget is cut at the last sentence boundary (". ", "! ", "? ", or a newline)
|
|
100
|
-
// within it, falling back to the last word boundary, then a hard cut, with an ellipsis ("…") appended; the
|
|
101
|
-
// citation (or kind, when there is none) stays on the line either way. A sentence/newline boundary this close
|
|
102
|
-
// to the start (MIN_CUT_KEPT) keeps too little to be worth it — a lesson opening with a heading-then-newline,
|
|
103
|
-
// or a stray early period, would otherwise collapse to almost nothing — so that case falls through to the word
|
|
104
|
-
// boundary (then the hard cut) instead, same as when no such boundary exists at all. That keeps any one lesson
|
|
105
|
-
// — the memory store holds whole files up to ~12k characters, not just short summaries — from ever crowding
|
|
106
|
-
// out the rest: the running total is kept under LESSONS_CHAR_CAP by skipping (not stopping at) a line that
|
|
107
|
-
// would overflow it, so a later, shorter lesson still renders even after an earlier one didn't fit. lessonIds
|
|
108
|
-
// carries exactly the ids of the lines rendered. { text: null, ids: [] } when nothing rendered, the caller's
|
|
109
|
-
// cue to omit the fields entirely.
|
|
110
|
-
const LESSONS_CHAR_CAP = 2000;
|
|
111
|
-
const LESSON_CHAR_BUDGET = 400;
|
|
112
|
-
const MIN_CUT_KEPT = 40;
|
|
113
|
-
function cutLessonText(text, budget) {
|
|
114
|
-
if (text.length <= budget) return text;
|
|
115
|
-
const slice = text.slice(0, budget);
|
|
116
|
-
let cut = -1;
|
|
117
|
-
for (const sep of [". ", "! ", "? "]) {
|
|
118
|
-
const idx = slice.lastIndexOf(sep);
|
|
119
|
-
if (idx !== -1) cut = Math.max(cut, idx + sep.length - 1);
|
|
120
|
-
}
|
|
121
|
-
const nl = slice.lastIndexOf("\n");
|
|
122
|
-
if (nl !== -1) cut = Math.max(cut, nl);
|
|
123
|
-
if (cut < MIN_CUT_KEPT) cut = -1;
|
|
124
|
-
if (cut === -1) {
|
|
125
|
-
const sp = slice.lastIndexOf(" ");
|
|
126
|
-
cut = sp !== -1 ? sp : budget;
|
|
127
|
-
}
|
|
128
|
-
return text.slice(0, cut).trimEnd() + "…";
|
|
129
|
-
}
|
|
130
|
-
function renderLessons(lessons) {
|
|
131
|
-
const text = [];
|
|
132
|
-
const ids = [];
|
|
133
|
-
let length = 0;
|
|
134
|
-
for (const lesson of Array.isArray(lessons) ? lessons : []) {
|
|
135
|
-
const line = `- [${lesson.id}] ${cutLessonText(lesson.text, LESSON_CHAR_BUDGET)} (${lesson.citation || lesson.kind})`;
|
|
136
|
-
const nextLength = length + (text.length ? 1 : 0) + line.length;
|
|
137
|
-
if (nextLength > LESSONS_CHAR_CAP) continue;
|
|
138
|
-
text.push(line);
|
|
139
|
-
ids.push(lesson.id);
|
|
140
|
-
length = nextLength;
|
|
141
|
-
}
|
|
142
|
-
return text.length ? { text: text.join("\n"), ids } : { text: null, ids: [] };
|
|
143
|
-
}
|
|
144
|
-
|
|
145
98
|
// Mirrors lib/memory.mjs's exported MEMORY_DB_RELPATH by value, not by import: importing that module loads
|
|
146
99
|
// node:sqlite at its top level (review MAJOR/MINOR 4), so the mere existence check below must not pull it in
|
|
147
100
|
// on a Node that lacks node:sqlite, or every plan.mjs json without a database would eat that module's load
|
|
@@ -149,14 +102,27 @@ function renderLessons(lessons) {
|
|
|
149
102
|
const MEMORY_DB_RELPATH = ".doug/.state/memory/memory.db";
|
|
150
103
|
|
|
151
104
|
// Wraps a provider so a down or slow embeddings endpoint is ever hit once per `plan.mjs json` run: the first
|
|
152
|
-
// embed() failure is cached and handed to every later caller with no further request
|
|
153
|
-
//
|
|
105
|
+
// embed() failure is cached and handed to every later caller with no further request (the degraded-recall
|
|
106
|
+
// WARNING for it comes from warnOnce below, once per distinct line). Without this, a plan with many tasks rediscovers the same dead endpoint
|
|
154
107
|
// once per task, each paying its own request or timeout (review MAJOR: measured 26.8s for 8 tasks against a
|
|
155
108
|
// black-holed baseUrl). null in, null out: recallLessons already treats a null provider as keyword-only.
|
|
109
|
+
// card recall-fallback-warning: each distinct degraded-recall warning is printed once per run, not once per task;
|
|
110
|
+
// and the local-provider probe (only when no provider is configured) runs once per run too.
|
|
111
|
+
const warnedLines = new Set();
|
|
112
|
+
function warnOnce(line) {
|
|
113
|
+
if (!line || warnedLines.has(line)) return;
|
|
114
|
+
warnedLines.add(line);
|
|
115
|
+
process.stderr.write(line + "\n");
|
|
116
|
+
}
|
|
117
|
+
let detectedOnce = null;
|
|
118
|
+
function detectOnce(provider) {
|
|
119
|
+
if (provider) return Promise.resolve(null);
|
|
120
|
+
if (!detectedOnce) detectedOnce = import("../lib/memory.mjs").then((mod) => mod.detectLocalProvider());
|
|
121
|
+
return detectedOnce;
|
|
122
|
+
}
|
|
156
123
|
function cachingProvider(provider) {
|
|
157
124
|
if (!provider) return null;
|
|
158
125
|
let failure = null;
|
|
159
|
-
let warned = false;
|
|
160
126
|
return {
|
|
161
127
|
name: provider.name,
|
|
162
128
|
model: provider.model,
|
|
@@ -164,13 +130,7 @@ function cachingProvider(provider) {
|
|
|
164
130
|
async embed(texts, opts) {
|
|
165
131
|
if (failure) return failure;
|
|
166
132
|
const result = await provider.embed(texts, opts);
|
|
167
|
-
if (!result.ok)
|
|
168
|
-
failure = result;
|
|
169
|
-
if (!warned) {
|
|
170
|
-
warned = true;
|
|
171
|
-
process.stderr.write(`[doug] memory recall: embeddings provider unavailable (${result.reason}); keyword-only\n`);
|
|
172
|
-
}
|
|
173
|
-
}
|
|
133
|
+
if (!result.ok) failure = result;
|
|
174
134
|
return result;
|
|
175
135
|
},
|
|
176
136
|
};
|
|
@@ -187,15 +147,20 @@ function cachingProvider(provider) {
|
|
|
187
147
|
async function attachLessons(plan, dir, provider) {
|
|
188
148
|
if (!existsSync(join(dir, MEMORY_DB_RELPATH))) return plan;
|
|
189
149
|
try {
|
|
190
|
-
const { openMemory, recallLessons } = await import("../lib/memory.mjs");
|
|
150
|
+
const { openMemory, recallLessons, degradedWarning, degradedReason, renderLessons } = await import("../lib/memory.mjs");
|
|
191
151
|
const staleDays = readMemoryStaleDays(dir);
|
|
192
152
|
const m = openMemory(dir);
|
|
193
153
|
try {
|
|
154
|
+
const detectedProvider = await detectOnce(provider);
|
|
155
|
+
// An empty lesson store has nothing to degrade (a code-index-only database, say): no warning for it.
|
|
156
|
+
const hasLessons = m.prepare("SELECT 1 FROM lessons WHERE superseded_by IS NULL AND stale IS NULL LIMIT 1").get() !== undefined;
|
|
194
157
|
const tasks = [];
|
|
195
158
|
for (const task of plan.tasks) {
|
|
196
|
-
const taskRecall = await recallLessons(m, `${task.title}\n${task.spec}`, { files: task.files, k: 8, checkoutDir: dir, provider, staleDays });
|
|
159
|
+
const taskRecall = await recallLessons(m, `${task.title}\n${task.spec}`, { files: task.files, k: 8, checkoutDir: dir, provider, staleDays, detectedProvider });
|
|
160
|
+
if (hasLessons) warnOnce(degradedWarning(taskRecall, { what: "memory recall", detectedProvider }));
|
|
197
161
|
const taskLessons = renderLessons(taskRecall.lessons);
|
|
198
|
-
|
|
162
|
+
const lessonsText = taskLessons.text !== null && taskRecall.degraded ? `(recall was keyword-only: ${degradedReason(taskRecall, detectedProvider)})\n${taskLessons.text}` : taskLessons.text;
|
|
163
|
+
tasks.push(taskLessons.text === null ? task : { ...task, lessons: lessonsText, lessonIds: taskLessons.ids });
|
|
199
164
|
}
|
|
200
165
|
return { ...plan, tasks };
|
|
201
166
|
} finally {
|
|
@@ -207,7 +172,7 @@ async function attachLessons(plan, dir, provider) {
|
|
|
207
172
|
}
|
|
208
173
|
}
|
|
209
174
|
|
|
210
|
-
// Renders code chunks the same skip-not-stop way renderLessons renders lessons, one line per chunk:
|
|
175
|
+
// Renders code chunks the same skip-not-stop way renderLessons (lib/memory.mjs) renders lessons, one line per chunk:
|
|
211
176
|
// "- <path>:<start>-<end> <first non-blank line of the chunk, trimmed to 120 chars>". codeContextIds carries
|
|
212
177
|
// exactly the ids of the lines kept, in rank order. { text: null, ids: [] } when nothing rendered, the caller's
|
|
213
178
|
// cue to omit the fields entirely.
|
|
@@ -243,17 +208,20 @@ async function attachCodeContext(plan, dir, provider) {
|
|
|
243
208
|
if (!readIndexConfig(dir).enabled) return plan;
|
|
244
209
|
if (!existsSync(join(dir, MEMORY_DB_RELPATH))) return plan;
|
|
245
210
|
try {
|
|
246
|
-
const { openMemory } = await import("../lib/memory.mjs");
|
|
211
|
+
const { openMemory, degradedWarning, degradedReason } = await import("../lib/memory.mjs");
|
|
247
212
|
const { searchIndex } = await import("../lib/code-index.mjs");
|
|
248
213
|
const m = openMemory(dir);
|
|
249
214
|
try {
|
|
250
215
|
const chunkCount = m.prepare("SELECT COUNT(*) AS n FROM code_chunks").get().n;
|
|
251
216
|
if (chunkCount === 0) return plan;
|
|
217
|
+
const detectedProvider = await detectOnce(provider);
|
|
252
218
|
const tasks = [];
|
|
253
219
|
for (const task of plan.tasks) {
|
|
254
|
-
const result = await searchIndex(m, `${task.title}\n${task.spec}`, { k: 6, provider });
|
|
220
|
+
const result = await searchIndex(m, `${task.title}\n${task.spec}`, { k: 6, provider, detectedProvider });
|
|
221
|
+
warnOnce(degradedWarning(result, { what: "code index search", code: true, detectedProvider }));
|
|
255
222
|
const rendered = renderCodeContext(result.chunks);
|
|
256
|
-
|
|
223
|
+
const codeText = rendered.text !== null && result.degraded ? `(code search was keyword-only: ${degradedReason(result, detectedProvider)})\n${rendered.text}` : rendered.text;
|
|
224
|
+
tasks.push(rendered.text === null ? task : { ...task, codeContext: codeText, codeContextIds: rendered.ids });
|
|
257
225
|
}
|
|
258
226
|
return { ...plan, tasks };
|
|
259
227
|
} finally {
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// SessionStart hook (card memory-every-session, brief A): puts the repository's top lessons into a fresh
|
|
3
|
+
// session's context. Runs on source startup|clear only. Query = title and goal of the card(s) in the board's
|
|
4
|
+
// flow column, else the query-free top lessons by helpful minus harmful. Keyword recall first; when an
|
|
5
|
+
// embeddings provider is configured a hybrid recall races a 3000 ms deadline and wins if it lands. Never
|
|
6
|
+
// creates the memory database, never exits non-zero: any exception is one stderr line plus a systemMessage.
|
|
7
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
8
|
+
import { join } from "node:path";
|
|
9
|
+
|
|
10
|
+
const DEADLINE_MS = 3000;
|
|
11
|
+
const PROVIDER_TIMEOUT_MS = 1500;
|
|
12
|
+
const K = 5;
|
|
13
|
+
|
|
14
|
+
function readStdin(ms) {
|
|
15
|
+
return new Promise((resolve) => {
|
|
16
|
+
let data = "";
|
|
17
|
+
let done = false;
|
|
18
|
+
const finish = () => {
|
|
19
|
+
if (done) return;
|
|
20
|
+
done = true;
|
|
21
|
+
resolve(data);
|
|
22
|
+
};
|
|
23
|
+
const timer = setTimeout(finish, ms);
|
|
24
|
+
process.stdin.setEncoding("utf8");
|
|
25
|
+
process.stdin.on("data", (d) => (data += d));
|
|
26
|
+
process.stdin.on("end", () => {
|
|
27
|
+
clearTimeout(timer);
|
|
28
|
+
finish();
|
|
29
|
+
});
|
|
30
|
+
process.stdin.on("error", finish);
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function parseInput(text) {
|
|
35
|
+
try {
|
|
36
|
+
const v = JSON.parse(text);
|
|
37
|
+
return v && typeof v === "object" && !Array.isArray(v) ? v : {};
|
|
38
|
+
} catch {
|
|
39
|
+
return {};
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function optedOut(dir) {
|
|
44
|
+
if (process.env.DOUG_SESSION_RECALL === "0") return true;
|
|
45
|
+
try {
|
|
46
|
+
const raw = JSON.parse(readFileSync(join(dir, ".doug", "config.json"), "utf8"));
|
|
47
|
+
return !!(raw && raw.memory && raw.memory.sessionRecall === false);
|
|
48
|
+
} catch {
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function write(obj) {
|
|
54
|
+
return new Promise((resolve) => process.stdout.write(JSON.stringify(obj) + "\n", resolve));
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async function run() {
|
|
58
|
+
const input = parseInput(await readStdin(1000));
|
|
59
|
+
const dir = process.env.CLAUDE_PROJECT_DIR || (typeof input.cwd === "string" && input.cwd) || process.cwd();
|
|
60
|
+
const source = input.source;
|
|
61
|
+
if (source !== "startup" && source !== "clear") return;
|
|
62
|
+
if (optedOut(dir)) return;
|
|
63
|
+
// Checked before anything opens it: a missing database is never created here.
|
|
64
|
+
if (!existsSync(join(dir, ".doug", ".state", "memory", "memory.db"))) return;
|
|
65
|
+
|
|
66
|
+
const { openMemory, recallLessons, topLessons, renderLessons, degradedReason } = await import("../lib/memory.mjs");
|
|
67
|
+
const { readMemoryConfig, readMemoryStaleDays, createProvider } = await import("../lib/embeddings.mjs");
|
|
68
|
+
const { loadBoard } = await import("../lib/board.mjs");
|
|
69
|
+
|
|
70
|
+
let cards = [];
|
|
71
|
+
try {
|
|
72
|
+
cards = loadBoard(dir).cards.filter((c) => c.column === "flow");
|
|
73
|
+
} catch {
|
|
74
|
+
cards = [];
|
|
75
|
+
}
|
|
76
|
+
const staleDays = readMemoryStaleDays(dir);
|
|
77
|
+
const m = openMemory(dir);
|
|
78
|
+
let lessons;
|
|
79
|
+
let header;
|
|
80
|
+
let scope;
|
|
81
|
+
try {
|
|
82
|
+
if (cards.length) {
|
|
83
|
+
const query = cards.map((c) => `${c.title}\n${c.goal || ""}`).join("\n");
|
|
84
|
+
scope = `for ${cards.map((c) => c.id).join(", ")}`;
|
|
85
|
+
const provider = createProvider(readMemoryConfig(dir), { timeoutMs: PROVIDER_TIMEOUT_MS });
|
|
86
|
+
let result = await recallLessons(m, query, { k: K, checkoutDir: dir, staleDays, provider: null });
|
|
87
|
+
if (provider) {
|
|
88
|
+
let timer;
|
|
89
|
+
const deadline = new Promise((resolve) => {
|
|
90
|
+
timer = setTimeout(() => resolve(null), DEADLINE_MS);
|
|
91
|
+
});
|
|
92
|
+
const hybrid = recallLessons(m, query, { k: K, checkoutDir: dir, staleDays, provider });
|
|
93
|
+
hybrid.catch(() => {});
|
|
94
|
+
const won = await Promise.race([hybrid, deadline]);
|
|
95
|
+
clearTimeout(timer);
|
|
96
|
+
if (won) result = won;
|
|
97
|
+
else result = { ...result, mode: "keyword-only", reason: "embeddings timed out" };
|
|
98
|
+
}
|
|
99
|
+
header =
|
|
100
|
+
result.mode === "hybrid"
|
|
101
|
+
? `recall: hybrid via ${result.provider} ${provider.model}/${provider.dims}`
|
|
102
|
+
: `recall: keyword-only (${degradedReason(result)})`;
|
|
103
|
+
lessons = result.lessons;
|
|
104
|
+
} else {
|
|
105
|
+
scope = "the repository: top by helpful-harmful";
|
|
106
|
+
header = null;
|
|
107
|
+
lessons = topLessons(m, { k: K, staleDays });
|
|
108
|
+
}
|
|
109
|
+
} finally {
|
|
110
|
+
// A raced-out hybrid recall may still be running; closing the handle makes it fail into its catch.
|
|
111
|
+
try {
|
|
112
|
+
m.close();
|
|
113
|
+
} catch {
|
|
114
|
+
// ignore
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
const rendered = renderLessons(lessons);
|
|
118
|
+
if (rendered.text === null) return;
|
|
119
|
+
const additionalContext =
|
|
120
|
+
`[doug] Lessons from this repository's memory (${header ? `${header}; ` : ""}${scope}):\n${rendered.text}\n` +
|
|
121
|
+
"Each line is [lesson id] text (citation). When you brief an agent, paste the lessons that apply and list their ids as memoryUsed.";
|
|
122
|
+
await write({ hookSpecificOutput: { hookEventName: "SessionStart", additionalContext } });
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
try {
|
|
126
|
+
await run();
|
|
127
|
+
} catch (err) {
|
|
128
|
+
const first = String((err && err.message) || err).split("\n")[0];
|
|
129
|
+
process.stderr.write(`[doug:session-recall] hook error, failing open: ${first}\n`);
|
|
130
|
+
await write({ systemMessage: `[doug:session-recall] hook error, failing open: ${first}` });
|
|
131
|
+
}
|
|
132
|
+
process.exit(0);
|
|
@@ -1,17 +1,18 @@
|
|
|
1
1
|
---
|
|
2
|
-
name:
|
|
3
|
-
description: Take the next hand-track board card (
|
|
4
|
-
disable-model-invocation: true
|
|
2
|
+
name: doug-hand
|
|
3
|
+
description: Take the next hand-track board card (a gated by-hand change) start to finish, or several together as one swarmed batch (`/doug-hand <id> <id>...` or `--batch <n>`): show its goal, ask before starting, build it by hand or, for a size M or L card, through the doug-implement workflow as a swarm, with tests and the full gate, commit without trailers, record the landing in docs/live-runs.md and on the board, and ask before the next card. Either way the session model leads only; implement and review work run on the Models table rows, a swarm's workers on the worker row. Invoked by `/doug-next` for a hand-track card, or directly with `/doug-hand`; its own start gate still blocks any work.
|
|
5
4
|
allowed-tools: AskUserQuestion, Agent, Workflow, Read, Edit, Write, Grep, Glob, Bash
|
|
6
5
|
---
|
|
7
6
|
|
|
8
|
-
#
|
|
7
|
+
# doug-hand
|
|
9
8
|
|
|
10
|
-
One hand-track card, start to finish, with two human gates: start and continue. A hand-track card is
|
|
9
|
+
One hand-track card, start to finish, with two human gates: start and continue. A hand-track card is a gated by-hand change, picked by size and risk: built directly in the checkout with tests and the project's own gate, or, for a size M or L card the user chooses to swarm, through the doug-implement workflow under an approved plan (section 2b). Nothing starts before the user says so, and the loop never continues to another card without being asked (except under autopilot, /doug-next --auto).
|
|
11
10
|
|
|
12
|
-
`/
|
|
11
|
+
`/doug-next <card-id>` hands a hand-track card to this loop; `/doug-hand` takes the first Ready hand-track card whose dependencies are Done, and `/doug-hand <card-id>` takes that card.
|
|
13
12
|
|
|
14
|
-
**Several cards in one swarmed batch.** `/
|
|
13
|
+
**Several cards in one swarmed batch.** `/doug-hand <id> <id>...` takes those hand-track cards together, and `/doug-hand --batch <n>` the first n runnable Ready hand-track cards (`board.mjs next --batch <n> --track hand`, board order). A batch runs only as a swarm: one planner per card in parallel writes `.doug/.state/drafts/<id>.json`, `plan.mjs merge` joins the drafts into one plan whose tasks carry their card, then one approval, one run, and one land, with each card keeping its own Done move and its own run entry. By-hand stays one card at a time: for the by-hand path a batch would only be step 6's loop with fewer gates, and the single gate run it would save is not worth collapsing n approvals for. Where the text below says "the card", read "each card of the batch" and follow the batch paragraphs marked as such.
|
|
14
|
+
|
|
15
|
+
Under autopilot (/doug-next --auto) the start gate, the shape question, and the continue gate are not asked; the queue approval stands for them, and the autopilot's stop conditions apply.
|
|
15
16
|
|
|
16
17
|
## 1. Pick the card
|
|
17
18
|
|
|
@@ -21,7 +22,7 @@ Start from a clean working tree on the base branch (`git status --porcelain` emp
|
|
|
21
22
|
- Without: `node "${CLAUDE_PLUGIN_ROOT}/scripts/board.mjs" next --track hand`. Show the `waiting on` skipping lines it prints (those hand-track cards wait on a dependency); the `flow track` lines need no comment.
|
|
22
23
|
- Batch: with several ids, `board.mjs card <id>` for each. With `--batch <n>`, `node "${CLAUDE_PLUGIN_ROOT}/scripts/board.mjs" next --batch <n> --track hand` prints the first n runnable Ready hand-track cards as a JSON array in board order (fewer than n when fewer are runnable; a batch left with one card, whether by a set-aside or by a drop, is a plain single-card run from here on). Show the `waiting on` skipping lines. Keep the ids in board order from here on.
|
|
23
24
|
|
|
24
|
-
Batch eligibility is checked here, on the card records alone, before the start gate and before any planner is spawned: a batch runs only as a swarm, so a card that step 2b would send by hand without asking (size S, or no size at all; or a goal naming
|
|
25
|
+
Batch eligibility is checked here, on the card records alone, before the start gate and before any planner is spawned: a batch runs only as a swarm, so a card that step 2b would send by hand without asking (size S, or no size at all; or a goal naming a file the running workflow or hooks execute, as 2b lists them) cannot be in a batch. Such a card is set aside, not the whole batch: it stays Ready, untouched, and the start-gate question below names it and the reason and says `/doug-hand <id>` takes it alone by hand. If eligibility sets every card of the batch aside, say which and why, and stop: nothing moves.
|
|
25
26
|
|
|
26
27
|
Put the card's title and goal in the chat verbatim, in a fenced code block: the goal is the acceptance list for the work. Note the wall clock. Batch: put each remaining card's title and goal in the chat the same way.
|
|
27
28
|
|
|
@@ -41,7 +42,7 @@ Batch: one `AskUserQuestion` names every remaining card of the batch (id, title,
|
|
|
41
42
|
|
|
42
43
|
Size S, or a card with no size at all: by hand, no question.
|
|
43
44
|
|
|
44
|
-
Size M or L: by hand without asking when the goal names a file the workflow
|
|
45
|
+
Size M or L: by hand without asking when the goal names a file the running workflow or hooks execute: the workflow file (`workflows/doug-implement.js`), anything under `.doug/hooks/scripts/`, the plugin's agents, plus any list the project's CLAUDE.md, or a procedure it names, gives. The workflow cannot rewrite itself, its gate hooks, or its agents while it runs them, so those stay by hand.
|
|
45
46
|
|
|
46
47
|
Batch: a batch runs only as a swarm, so for two or more cards this step does not ask swarm-or-by-hand; it says so and goes straight to spawning one planner per card in the same message so they run in parallel, on the `plan` row — the same `plan.mjs models` row and prompt shape below, each told to write its draft to `.doug/.state/drafts/<id>.json` (not `.doug/plan.json`) and validate it with `plan.mjs validate --file .doug/.state/drafts/<id>.json`, each also told this plan runs as a swarm: set `"swarm": true` and size every task M or L unless it is truly one bounded change. When every planner has returned, `node "${CLAUDE_PLUGIN_ROOT}/scripts/plan.mjs" merge <id> <id>...` joins the drafts into `.doug/plan.json` (its output in the chat verbatim), then `node "${CLAUDE_PLUGIN_ROOT}/scripts/plan.mjs" set swarm on`. Never `plan.mjs set card` for a batch: merge's `cards` replaces it. Then `plan.mjs show` (it prints `Cards:` and each task's card) and the approval gate below; Stop on a batch moves every card back to Ready with `board.mjs move <id> ready` (mirroring `/doug-next` section 3's Stop).
|
|
47
48
|
|
|
@@ -55,19 +56,27 @@ Then continue at step 4.
|
|
|
55
56
|
|
|
56
57
|
A dissolved batch (2b's approval gate went by hand after a failed run) builds only its first card here; the other cards of the batch already went back to Ready.
|
|
57
58
|
|
|
58
|
-
The session model is the lead and only the lead, on both tracks; it never implements, verifies, or reviews the change itself. The order is: research (when called for),
|
|
59
|
+
The session model is the lead and only the lead, on both tracks; it never implements, verifies, or reviews the change itself. The order is: research (when called for), the test file and the project's procedure, the brief, the tester, the coder, the reviewer, then the lead's own gate and commit; a lean-lane card has no tester.
|
|
59
60
|
|
|
60
61
|
First the research step when step 1 called for one: invoke the `research` skill. It names the agent (claude-code-guide for Claude Code questions, the plugin's read-only `researcher` agent otherwise), runs several researchers in parallel when there are several distinct questions, and writes one cited note to `.doug/.state/research/<id>.md`; read it before writing the brief, and treat a line marked unverified as a fact the code must not depend on.
|
|
61
62
|
|
|
62
|
-
|
|
63
|
+
Then recall memory for the card: `node "${CLAUDE_PLUGIN_ROOT}/scripts/memory.mjs" brief-lessons <id>`, and paste its output verbatim, header, lesson lines with their ids, and `memoryUsed` line, into every tester, coder, and reviewer brief. When it prints no lessons, or exits nonzero, say so in the chat and go on: recall never blocks a card. Save each brief to `.doug/.state/reports/<id>/briefs/<n>-<seat>.md` before spawning its agent, and each reviewer report to `.doug/.state/reports/<id>/reviews/<n>.md` when it returns.
|
|
64
|
+
|
|
65
|
+
On the lean lane and on a tests-only card, every brief carries the same `brief-lessons` block.
|
|
63
66
|
|
|
64
|
-
|
|
67
|
+
Then find the test file that covers each module the goal touches: the project's test skill when it has one, otherwise grep for the module's name under the test directories. Follow any procedure CLAUDE.md names for changes of this kind (its test table, its rules for generated or copied files, its type declarations), and put those rules into the brief: read the goal as the acceptance list, name the test file you found — when the project has a tester agent, the tester writes or extends that file first, from the goal; the coder runs only that file while iterating and does not write tests — and the mutation list step 3 demands (one mutation per case the card's goal names, each with the test that must fail). A card whose acceptance is that a mechanism exists is proved by mutating it: the coder applies each listed mutation, confirms the named test fails, reverts it, and reports which assertion failed; a card that only changes prose, a message, or a docs line needs none. The reviewer, on every card, reruns the brief's mutation list and adds its own. A tests-only card's tester never edits a production file in the live checkout. The coder and the reviewer apply each mutation in a scratch copy or git worktree under `.doug/.state`, run the one test file there, and remove the copy. Keep the change to what the card asks: a defect found on the way becomes its own hand-track card. On facts: a fact the brief states about the workflow, a hook contract, or a model row is copied verbatim from CLAUDE.md, README, docs, or the code with its path, never paraphrased; a claim the lead cannot cite goes into the brief as unverified.
|
|
68
|
+
|
|
69
|
+
Next: before the coder, spawn the project's `tester` agent (`.claude/agents/tester.md`, on the `implement` row) with the goal as the acceptance list and the test file step 3 found, so the failing tests are written from the card, not from the code — the Agent tool naming the project's `tester` agent, `model` set to the `implement` row's model, the same way the coder runs on that row. Tell the tester to end its report with the `tests_red_by_design` claim naming the test files it left red, so the SubagentStop gate lets it stop; the lead's own Stop stays red until the coder's change lands. The claim may sit in the tester's final plain-text message or in its hand-back call; the gate reads the plain text first, then the last hand-back. The tester is spawned in the foreground, never in the background: the coder starts from its red tests.
|
|
65
70
|
|
|
66
71
|
A tests-only card (its goal says tests only, no production change) runs as tester then reviewer, with no coder: the tester writes the test from the goal, runs the brief's mutation list itself in a scratch copy or git worktree under `.doug/.state`, and reports each result; the reviewer reruns the list and adds its own.
|
|
67
72
|
|
|
68
|
-
|
|
73
|
+
The lean lane: a size S card whose class is `prose` or `docs` runs as coder then reviewer, with no tester seat and no mutation list. One coder on the `implement` row writes the test and the change together, from the goal, and the reviewer reviews it as usual. The session model still never implements or reviews. On the lean lane the coder, not a tester, writes or extends the test file. The reviewer has no list to rerun, so it adds its own mutations only where the change turns out to be a mechanism.
|
|
74
|
+
|
|
75
|
+
A project with no `tester` agent (no `.claude/agents/tester.md`) has the coder write the failing test first, from the goal, before the change. The lead says so in the brief in place of the tester line, and a tests-only card runs as coder then reviewer.
|
|
69
76
|
|
|
70
|
-
|
|
77
|
+
Then spawn the implementation with the Agent tool on the `implement` row of the CLAUDE.md Models table (the project's `coder` agent with `model` set to that row's model, which executes the brief); the coder then makes them pass and may not edit a test except with a stated reason in its report; the reviewer checks that rule. Then a review on the `review` row (the project's `reviewer` agent); a swarm's workers run on the `worker` row. Verification on the hand track is the gate itself (the commands in `.doug/config.json` `stopGate.commands`), which the reviewer runs and the lead re-runs before the commit, says what ran, and records. Both run it through `node .doug/hooks/scripts/verify.mjs`, which credits a verified-tree record of the same tree instead of re-running the commands. A row that says `inherit` means the session model, and that is the user's choice in the table, not the lead's.
|
|
78
|
+
|
|
79
|
+
Before the commit the lead itself runs `node .doug/hooks/scripts/verify.mjs` (the commands in `.doug/config.json` `stopGate.commands`), records its last line as the gate line, and says what ran and the counts. The pre-commit hook always runs the commands itself, whatever a record says. Commit without attribution trailers (one commit per coherent change, with a message that says what changed and why), and leaves `.doug/plan.json` and `.doug/anchor.md` out unless the card is about them. If a hook blocks a step, report it; do not work around it.
|
|
71
80
|
|
|
72
81
|
Stay within the card's goal. A defect found on the way that the goal does not cover becomes a new hand-track card, not part of this commit.
|
|
73
82
|
|
|
@@ -75,17 +84,21 @@ Stay within the card's goal. A defect found on the way that the goal does not co
|
|
|
75
84
|
|
|
76
85
|
With the commit sha (the merge sha for a swarmed card) and the wall clock since step 1 (or since 2b's launch, for a swarmed card):
|
|
77
86
|
|
|
78
|
-
1. Invoke the `run-report` skill. By hand: its hand-track-landing form, `node "${CLAUDE_PLUGIN_ROOT}/scripts/board.mjs" record <id> --hand --commit <sha> --wall "<m> min" --gate "<gate line>"` (the gate line is run-report's own rule: one `<command> <exit>` clause per entry of `.doug/config.json` `stopGate.commands`, joined by `; `;
|
|
87
|
+
1. Invoke the `run-report` skill. By hand: its hand-track-landing form, `node "${CLAUDE_PLUGIN_ROOT}/scripts/board.mjs" record <id> --hand --commit <sha> --wall "<m> min" --gate "<gate line>"` (the gate line is run-report's own rule: one `<command> <exit>` clause per entry of `.doug/config.json` `stopGate.commands`, joined by `; `; for example `typecheck 0; test:unit 0 (<n> passed)`), and `--note "<one line>"` when something about the build is worth keeping (what was harder than the goal said, what was left out and why), Hand the form's step 4, `memory.mjs reflect --hand`, two more inputs; they go to that command only, never to `board.mjs record` or `memory.mjs record`, which reject them: `--rounds <n>` (coder rounds until the reviewer passed) and one `--review "R<n>=<real|marginal|false>: <reason>"` per reviewer blocker finding a later round fixed, classed by the lead. Swarmed: its workflow-run form, `node "${CLAUDE_PLUGIN_ROOT}/scripts/board.mjs" record <id> <report.json>` (the file 2b saved, `.doug/.state/last-report.json`) `--commit <merge sha> --wall "<m> min" --cost <usd> --codex-cost <usd>` (add --codex-cost <usd> when cost.mjs printed a Codex line, next to the run total) with one `--adversary "<id>=<real|marginal|false>: <reason>"` per adversary block the report carries (run-report requires every block classified), then `node "${CLAUDE_PLUGIN_ROOT}/scripts/board.mjs" summary <report.json> --wall "<m> min" --commit <merge sha>` and put its three lines in the chat verbatim. Batch: the `run-report` skill's batch form — `cost.mjs <run-id> --by-card` for each card's cost with the shared integration agents counted once under the first, then one `node "${CLAUDE_PLUGIN_ROOT}/scripts/board.mjs" record <id> <report.json> --commit <merge sha> --wall "<m> min" --cost <that card's usd> --shared-cost <shared usd> --adversary ...` per card in the plan's `cards` order, then `node "${CLAUDE_PLUGIN_ROOT}/scripts/board.mjs" summary <report.json> --card <id> --wall "<m> min" --commit <merge sha>` per card with each card's three lines in the chat verbatim.
|
|
79
88
|
2. `node "${CLAUDE_PLUGIN_ROOT}/scripts/board.mjs" move <id> done --source "commit <sha>"`. Batch: once per card of the batch, same merge sha.
|
|
80
89
|
3. `git add <record> docs/live-runs.md && git commit -m "Board: <id> done as <sha>"` (swarmed: add `.doug/plan.json` too); when step 1's `run-report` invocation printed that it promoted a research note, add `docs/research/<id>.md` too. No trailers. Batch: one commit `"Board: <id>, <id> done as <sha>"` including `.doug/plan.json`.
|
|
81
90
|
|
|
82
91
|
## 5. Follow-up cards
|
|
83
92
|
|
|
84
|
-
Before the continue gate, gather the follow-up candidates this landing produced: the coder's and reviewer's "left out", "not closed", and "found on the way" items, the landing note, and any defect the reviewer marked out of scope. Suggestions come only from what the run actually reported, never invented; each carries the one-line reason and cites the pass that found it. A reviewer's out-of-scope finding is
|
|
93
|
+
Before the continue gate, gather the follow-up candidates this landing produced: the coder's and reviewer's "left out", "not closed", and "found on the way" items, the landing note, and any defect the reviewer marked out of scope. Suggestions come only from what the run actually reported, never invented; each carries the one-line reason and cites the pass that found it. A reviewer's out-of-scope finding is filed only when it is realistic drift (a form a model or a person would plausibly write) or when one card would close the whole class; otherwise it goes in the landing note and is not filed. The card's goal text carries both: the goal names the pass that found it (the reviewer's finding, the coder's report, the adversary block id) and the landed commit.
|
|
94
|
+
|
|
95
|
+
Follow-ups are filed without a question. A landing with no candidates files nothing. Each realistic candidate goes straight to Backlog with `doug board add` (never a script that edits the record): `doug board add <id> --title "<title>" --goal "<goal>" --component <c> --size <S|M|L> --class <tests-only|prose|gate-script|code|docs|eval|decision> --track <hand|flow> --deps <landed-id> --column backlog`. Then the lead places it in Backlog by priority with `doug board reorder`: `--top` for a defect that blocks a commit, a gate, or a user; `--after <id>` of the last such card for drift a user would hit; `--bottom` otherwise. Only the user promotes a card to Ready; the lead never files a follow-up to Ready or Decide.
|
|
96
|
+
|
|
97
|
+
The lead says in the chat what it filed and where: each new card's id, title, and its Backlog position with the reason. Then one commit `"Board: <id> follow-ups added: <new-id>, <new-id>"`, whatever the count; the served page (`doug board serve`) shows the new cards without a republish. Batch: each card's deps name the card of the batch it came from; still one commit for the whole batch.
|
|
85
98
|
|
|
86
|
-
|
|
99
|
+
## Cards the user asks for
|
|
87
100
|
|
|
88
|
-
|
|
101
|
+
A card the user asks for is a mini ADR, not a one-line goal. Before filing it, the lead asks the scoping questions in one `AskUserQuestion` of up to 4 questions, each with a Recommended option, and writes the answers into the goal. The goal carries the fixed headings Context / Decision / Options / Questions; `docs/decisions` stays for real ADRs. Only a user-requested card takes this form: a follow-up the lead files keeps step 5's lighter form, so autopilot follow-ups stay question-free.
|
|
89
102
|
|
|
90
103
|
## 6. Human gate: continue
|
|
91
104
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: doug-learn
|
|
3
|
-
description: Log-then-propose
|
|
3
|
+
description: Log-then-propose self-improvement (card learn-signals). Counts signals from the outcomes/lessons store and the run trace, renders each candidate change - demote a violated prose rule to a hook, delete a rule nothing references, promote a repeated lesson, tighten a skill description - as a diff, and asks the user to approve each one before applying it. Only the user invokes this; it never applies a change on its own.
|
|
4
4
|
disable-model-invocation: true
|
|
5
5
|
allowed-tools: AskUserQuestion, Read, Bash(node *learn.mjs *), Bash(git *)
|
|
6
6
|
---
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: doug-next
|
|
3
|
-
description: Take the next board card (.doug/board.json), or several cards in one plan, through the whole flow in one command: plan it, stop for the user's approval, implement with the doug-implement workflow, replan a blocked run keeping the tasks that passed, land the integration branch, move the card to Done, log the run, and offer the next card. Only the user invokes this.
|
|
3
|
+
description: Take the next board card (.doug/board.json), or several cards in one plan, through the whole flow in one command (a hand-track card goes to the by-hand loop, the `doug-hand` skill; `--auto [n]` runs the queue of Ready cards on one approval): plan it, stop for the user's approval, implement with the doug-implement workflow, replan a blocked run keeping the tasks that passed, land the integration branch, move the card to Done, log the run, and offer the next card. Only the user invokes this.
|
|
4
4
|
disable-model-invocation: true
|
|
5
5
|
allowed-tools: Agent, AskUserQuestion, Workflow, Read, Bash(node *plan.mjs *), Bash(node *board.mjs *), Bash(node *cost.mjs *), Bash(git *), Bash(doug board *), Bash(node *bin.js board *)
|
|
6
6
|
---
|
|
7
7
|
|
|
8
8
|
# doug-next
|
|
9
9
|
|
|
10
|
-
One card, start to finish, with two human gates. Nothing is implemented before the user approves the plan, and the loop never continues to another card without being asked.
|
|
10
|
+
One card, start to finish, with two human gates. Nothing is implemented before the user approves the plan, and the loop never continues to another card without being asked (except under autopilot, below).
|
|
11
11
|
|
|
12
12
|
`/doug-next` takes the first Ready card whose dependencies are Done. `/doug-next <card-id>` takes that card.
|
|
13
13
|
|
|
@@ -15,15 +15,15 @@ One card, start to finish, with two human gates. Nothing is implemented before t
|
|
|
15
15
|
|
|
16
16
|
## Who does the work
|
|
17
17
|
|
|
18
|
-
The session model is the lead only, on both tracks (
|
|
18
|
+
The session model is the lead only, on both tracks (a hand-track card runs the by-hand loop, `doug-hand`, with the same rule). In this loop it picks the card, asks the human gates, launches the workflow, reads the report, replans, lands, and records; it never plans, researches, implements, verifies, reviews, or classifies itself. Planning and research run on the `plan` row (the `doug-plan` and `research` skills), the workflow's stages on the `implement`, `verify`, `review`, `adversary`, and `integrate` rows and a swarm's on the `lead` and `worker` rows, and adversary-block classification on the `review` row (`run-report`). A row that says `inherit` is the session model by the user's choice in the table.
|
|
19
19
|
|
|
20
20
|
## 1. Pick the card
|
|
21
21
|
|
|
22
22
|
Start from a clean working tree on the base branch (`git status --porcelain` empty apart from `.doug/` and `.claude/worktrees/`); land refuses otherwise, and the scope gate blocks uncommitted files the plan does not own.
|
|
23
23
|
|
|
24
|
-
- With an id: `node "${CLAUDE_PLUGIN_ROOT}/scripts/board.mjs" card <id>`. If the card carries `"track": "hand"`,
|
|
25
|
-
- Without: `node "${CLAUDE_PLUGIN_ROOT}/scripts/board.mjs" next`. Show any "skipping" lines it prints; those Ready cards are waiting on a dependency.
|
|
26
|
-
- Batch: with several ids, `board.mjs card <id>` for each (a hand-track card among them stops the run
|
|
24
|
+
- With an id: `node "${CLAUDE_PLUGIN_ROOT}/scripts/board.mjs" card <id>`. If the card carries `"track": "hand"`, it is a gated by-hand change, not a flow run: invoke the `doug-hand` skill with the card id and end this loop; doug-hand asks its own start gate before anything moves.
|
|
25
|
+
- Without: `node "${CLAUDE_PLUGIN_ROOT}/scripts/board.mjs" next --track any`. A plain /doug-next takes the next runnable Ready card of either track; a hand-track card goes to the doug-hand skill. Show any "skipping" lines it prints; those Ready cards are waiting on a dependency.
|
|
26
|
+
- Batch: with several ids, `board.mjs card <id>` for each (a hand-track card among them stops the run, saying `/doug-next <id>` takes it alone); with `--batch <n>`, `node "${CLAUDE_PLUGIN_ROOT}/scripts/board.mjs" next --batch <n>` prints the cards as a JSON array in board order (fewer than n when fewer are runnable; one card is a plain single-card run). Show the "skipping" lines. Keep the ids in board order from here on: the merge sequences overlapping files by it.
|
|
27
27
|
|
|
28
28
|
Move it: `node "${CLAUDE_PLUGIN_ROOT}/scripts/board.mjs" move <id> flow` prints `Moved <id> to flow in <record>.` (normally `<record>` is `.doug/board.json`; a repository that has not migrated prints its fallback). Batch: one move per card. Leave the move uncommitted: it is working state, the scope gate allows an uncommitted `.doug/board.json`, and land ignores `.doug/` changes; the served page (`doug board serve`, `--detach` to keep it running) shows the card In flow live. The record change lands in section 5's landing commit together with `docs/live-runs.md`.
|
|
29
29
|
|
|
@@ -48,7 +48,7 @@ Then `node "${CLAUDE_PLUGIN_ROOT}/scripts/plan.mjs" show` and put its complete o
|
|
|
48
48
|
|
|
49
49
|
## 3. Human gate: approval
|
|
50
50
|
|
|
51
|
-
Ask with `AskUserQuestion`, only after the full plan is in the chat: approve as written, edit first, or stop. Keep the question short; the plan is above it. Approval is never automatic and never inferred.
|
|
51
|
+
Ask with `AskUserQuestion`, only after the full plan is in the chat: approve as written, edit first, or stop. Keep the question short; the plan is above it. Approval is never automatic and never inferred, except that under autopilot the queue approval stands for it.
|
|
52
52
|
|
|
53
53
|
- **Approve**: first make the plan file complete, because the workflow and `plan.mjs land` both read the file, never the launch arguments: `install` and `adversary.command` are filled from `.doug/config.json` (`commands.install` and `commands.adversary`, where `{root}` stands for the project directory) when the plan has none; check with `node "${CLAUDE_PLUGIN_ROOT}/scripts/plan.mjs" json | grep -o '"adversary":{[^}]*}'` and, only if the config lacks one, set it with `plan.mjs set adversary.command "<command>"` (or `plan.mjs set install "<command>"`). `plan.mjs show` prints `Fix attempts per blocked task`; change it only if the user asks, with `plan.mjs set fixAttempts <n>` (0 disables the in-run fix loop). When the user asked for a swarm, `plan.mjs set swarm on` (a lead splits each full-shape task into worker briefs; `plan.mjs show` prints `Swarm: on`); `/doug-swarm` is the command that does this from the start. Then `node "${CLAUDE_PLUGIN_ROOT}/scripts/plan.mjs" approve` and `node "${CLAUDE_PLUGIN_ROOT}/scripts/plan.mjs" anchor`.
|
|
54
54
|
- **Edit**: stop here. Tell the user to edit `.doug/plan.json` and run `/doug-next <id>` again; the planner will not overwrite an edited draft unless asked.
|
|
@@ -76,12 +76,46 @@ Batch: every card of the batch moves to Done with the same `--source "commit <sh
|
|
|
76
76
|
|
|
77
77
|
## 6. Follow-up cards
|
|
78
78
|
|
|
79
|
-
Before the continue gate, gather the follow-up candidates this landing produced: the report's review issues, the adversary blocks classified marginal or false whose reason names a hole, the integrate stage's conflicts and failed verify tails, and the landing summary. Suggestions come only from what the run actually reported, never invented; each carries the one-line reason and cites the pass that found it. A reviewer's out-of-scope finding is
|
|
79
|
+
Before the continue gate, gather the follow-up candidates this landing produced: the report's review issues, the adversary blocks classified marginal or false whose reason names a hole, the integrate stage's conflicts and failed verify tails, and the landing summary. Suggestions come only from what the run actually reported, never invented; each carries the one-line reason and cites the pass that found it. A reviewer's out-of-scope finding is filed only when it is realistic drift (a form a model or a person would plausibly write) or when one card would close the whole class; otherwise it goes in the landing note and is not filed. The card's goal text carries both: the goal names the pass that found it (the reviewer's finding, the implementer's blockedReason or handoff, the verifier's findings, the adversary block id) and the landed commit.
|
|
80
80
|
|
|
81
|
-
A landing with no candidates
|
|
81
|
+
Follow-ups are filed without a question. A landing with no candidates files nothing. Each realistic candidate goes straight to Backlog with `doug board add` (never a script that edits the record): `doug board add <id> --title "<title>" --goal "<goal>" --component <c> --size <S|M|L> --class <tests-only|prose|gate-script|code|docs|eval|decision> --track <hand|flow> --deps <landed-id> --column backlog`. Then the lead places it in Backlog by priority with `doug board reorder`: `--top` for a defect that blocks a commit, a gate, or a user; `--after <id>` of the last such card for drift a user would hit; `--bottom` otherwise. Only the user promotes a card to Ready; the lead never files a follow-up to Ready or Decide.
|
|
82
82
|
|
|
83
|
-
|
|
83
|
+
The lead says in the chat what it filed and where: each new card's id, title, and its Backlog position with the reason. Then one commit `"Board: <id> follow-ups added: <new-id>, <new-id>"`, whatever the count; the served page (`doug board serve`) shows the new cards without a republish. Batch: each card's deps name the card of the batch it came from; still one commit for the whole batch.
|
|
84
|
+
|
|
85
|
+
## Cards the user asks for
|
|
86
|
+
|
|
87
|
+
A card the user asks for is a mini ADR, not a one-line goal. Before filing it, the lead asks the scoping questions in one `AskUserQuestion` of up to 4 questions, each with a Recommended option, and writes the answers into the goal. The goal carries the fixed headings Context / Decision / Options / Questions; `docs/decisions` stays for real ADRs. Only a user-requested card takes this form: a follow-up the lead files keeps section 6's lighter form, so autopilot follow-ups stay question-free.
|
|
84
88
|
|
|
85
89
|
## 7. Human gate: continue
|
|
86
90
|
|
|
87
91
|
Ask with `AskUserQuestion` whether to take the next Ready card. Yes: go to step 1 with no id. No: end. Each card is a fresh plan and a fresh set of worktrees; nothing carries over between cards except the board and the log. Between passes of the same card, `plan.mjs replan` carries the task branches that passed.
|
|
92
|
+
|
|
93
|
+
## Autopilot (--auto)
|
|
94
|
+
|
|
95
|
+
`/doug-next --auto [n]` takes the first n runnable Ready cards of either track in board order (`board.mjs next --batch <n> --track any`). With n, the queue is fixed at the approval. Without n, autopilot is unlimited: before each card it re-reads Ready with `board.mjs next --track any` and takes the next runnable Ready card in board order. It never takes a Backlog card: follow-ups still go to Backlog, and a card the user promotes to Ready during the run is picked up. It shows the queue once, each card's id, size, track, and class, with the caps: cards, wall clock, and USD. The caps default to the queue's length in cards, the wall cap that board.mjs next --batch prints (the sum of each card's budget from the size x class wall budget table in board.mjs, at the p80 of measured landings: a size/class cell with fewer than 5 landings falls back to its size's all-class figure, a card with no class uses its size's all-class figure, and a card with no size counts as M), and $40, and the user may change them at the approval. Unlimited autopilot shows the runnable Ready cards at the approval, and the approval covers every card it later takes from Ready. Unlimited autopilot has no card cap: its USD cap defaults to $40, and its wall cap grows as each card is taken, by that card's budget from the same size x class table (`board.mjs budgets` prints it); only the totals are checked between cards. When the measured walls drift, the lead reruns `board.mjs budgets --from docs/live-runs.md`, which recomputes the table and prints each cell's n, and replaces the table in the `board.mjs` source with the printed one. Autopilot asks one approval, for the whole queue, and no other gate: no start gate, no shape question, and no continue gate.
|
|
96
|
+
|
|
97
|
+
Each card still runs its track unchanged (research when needed, tester, coder, reviewer, mutations, gate, commit, record, Done; a lean-lane card has no tester and no mutation list). A hand card follows doug-hand from step 3 when it runs by hand, with step 2's start gate question, 2b's question, and step 6's continue gate skipped; a hand card that asks for a swarm takes 2b's swarm branch instead of step 3, as below. A flow card follows sections 2 to 6 above, with section 3's approval replaced as below and section 7 skipped. Only the questions are skipped, not the actions behind them. Under autopilot a card still gets everything the skipped gates' Start or Approve branches do: a hand card gets doug-hand step 2's class, move to flow, and condition open, and the wall clock is noted when the card starts; a flow card gets section 1's move and section 3's Approve steps.
|
|
98
|
+
|
|
99
|
+
The queue approval stands for each flow-track card's plan approval: the lead puts plan.mjs show in the chat and runs plan.mjs approve itself. A card runs by hand unless its goal asks for a swarm. A hand card whose goal asks for a swarm follows doug-hand's 2b swarm branch, with the queue approval standing for its plan approval. Follow-ups are filed to Backlog as section 6 says, without a question. Between cards the lead measures the session's cost with cost.mjs --session latest and checks every cap.
|
|
100
|
+
|
|
101
|
+
Stop conditions, one per line:
|
|
102
|
+
|
|
103
|
+
- It stops and asks when a card needs a design decision its goal does not answer.
|
|
104
|
+
- It stops and asks when the gate is red twice on one card.
|
|
105
|
+
- It stops and asks when a hook blocks a step.
|
|
106
|
+
- It stops and asks when a reviewer blocker stays open after one fix pass.
|
|
107
|
+
- It stops and asks when a cap is reached.
|
|
108
|
+
- It stops and asks when a workflow run's report is not ok, on either track.
|
|
109
|
+
- It stops and asks when a workflow run pauses at a human gate, on either track.
|
|
110
|
+
- It stops and asks when cost.mjs reports an unpriced transcript, because the USD cap cannot be checked.
|
|
111
|
+
|
|
112
|
+
It ends with the summary below, one condition per line:
|
|
113
|
+
|
|
114
|
+
- It ends when Ready has no runnable card, under unlimited autopilot.
|
|
115
|
+
- It ends when the user says stop: the current card is finished or left In flow.
|
|
116
|
+
- It ends when the lead's context is at or above autopilot.contextThreshold in .doug/config.json (default 60): between cards the lead runs `node "${CLAUDE_PLUGIN_ROOT}/scripts/context.mjs"`, which prints the context percentage of the lead's own session transcript, and treats a run that prints no reading as at the threshold.
|
|
117
|
+
- Before that summary it refreshes the anchor handoff: a `## Autopilot handoff` section in .doug/anchor.md naming the landed cards, any card left In flow, and that a new session resumes with /doug-next --auto.
|
|
118
|
+
|
|
119
|
+
An edge case or input the goal does not name is not a design decision: the lead takes the conservative behaviour, rejecting the input or, where rejecting is not possible, preserving it, never guessing, has whoever writes the test pin it, and names it in the card's landing note and the closing summary. A design decision is a choice, for inputs the goal names, between behaviours the goal's acceptance would both satisfy and a user would notice, or one that changes another card's or module's contract.
|
|
120
|
+
|
|
121
|
+
It waits for the answer, then resumes the queue at the card it stopped on. It ends with one summary: each landed card's commit, wall clock, and gate line; each stopped card and why; each edge case taken conservatively; and the follow-ups filed.
|