@dougharness/cli 1.1.0 → 1.1.1
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 +10 -3
- package/dist/bin.js.map +1 -1
- package/dist/board-serve.js +5 -4
- package/dist/board-serve.js.map +1 -1
- package/dist/board.d.ts +1 -1
- package/dist/board.js +33 -8
- package/dist/board.js.map +1 -1
- package/dist/memory-setup.js +5 -2
- package/dist/memory-setup.js.map +1 -1
- package/node_modules/@dougharness/flow/.claude-plugin/plugin.json +1 -1
- package/node_modules/@dougharness/flow/agents/lead.md +1 -1
- package/node_modules/@dougharness/flow/agents/planner.md +2 -2
- package/node_modules/@dougharness/flow/lib/board.mjs +17 -1
- package/node_modules/@dougharness/flow/lib/cost.mjs +29 -6
- package/node_modules/@dougharness/flow/lib/embeddings.mjs +3 -1
- package/node_modules/@dougharness/flow/lib/land.mjs +6 -1
- package/node_modules/@dougharness/flow/lib/memory.mjs +49 -22
- package/node_modules/@dougharness/flow/lib/plan.mjs +2 -1
- package/node_modules/@dougharness/flow/package.json +1 -1
- package/node_modules/@dougharness/flow/scripts/board.mjs +35 -14
- package/node_modules/@dougharness/flow/scripts/context.mjs +4 -2
- package/node_modules/@dougharness/flow/scripts/memory.mjs +10 -6
- package/node_modules/@dougharness/flow/scripts/plan.mjs +22 -0
- package/node_modules/@dougharness/flow/skills/doug-hand/SKILL.md +4 -4
- package/node_modules/@dougharness/flow/skills/doug-next/SKILL.md +8 -6
- package/node_modules/@dougharness/flow/skills/doug-plan/SKILL.md +1 -1
- package/node_modules/@dougharness/flow/skills/doug-swarm/SKILL.md +2 -2
- package/node_modules/@dougharness/flow/skills/run-report/SKILL.md +11 -9
- package/node_modules/@dougharness/flow/workflows/doug-implement.js +26 -14
- package/node_modules/@dougharness/gates/.claude-plugin/plugin.json +1 -1
- package/node_modules/@dougharness/gates/lib/bash-rules.mjs +21 -14
- package/node_modules/@dougharness/gates/lib/gate-lock.mjs +94 -0
- package/node_modules/@dougharness/gates/package.json +1 -1
- package/node_modules/@dougharness/gates/scripts/stop-gate.mjs +10 -87
- package/node_modules/@dougharness/gates/scripts/verify.mjs +58 -3
- package/package.json +4 -4
- package/templates/board-app.js +2 -1
|
@@ -9,7 +9,10 @@ import { basename, join } from "node:path";
|
|
|
9
9
|
// 0.05x on Opus 5.5); cache writes are 1.25x input for the 5-minute TTL and 2x for the 1-hour TTL. Opus 5.5:
|
|
10
10
|
// input 4, output 20, cacheRead 0.2, cacheWrite5m 5, cacheWrite1h 8; source
|
|
11
11
|
// https://platform.claude.com/docs/en/about-claude/pricing, fetched 2026-10-04. Update the date with the table.
|
|
12
|
-
|
|
12
|
+
// Haiku 5.5 is priced per request by prompt length (the same page, fetched 2026-10-10): a row may carry `over`, the
|
|
13
|
+
// prices for a request whose prompt (input + cacheRead + cacheWrite) is strictly over `over.above` tokens; a request
|
|
14
|
+
// at or under it pays the row's own prices. Sonnet 5.5 cacheRead is 0.1 (corrected 2026-10-10).
|
|
15
|
+
export const PRICES_DATED = "2026-10-10";
|
|
13
16
|
export const PRICES = {
|
|
14
17
|
"claude-fable-5-1": { input: 10, output: 50, cacheRead: 0.25, cacheWrite5m: 12.5, cacheWrite1h: 20 },
|
|
15
18
|
"claude-fable-5": { input: 10, output: 50, cacheRead: 1, cacheWrite5m: 12.5, cacheWrite1h: 20 },
|
|
@@ -18,9 +21,13 @@ export const PRICES = {
|
|
|
18
21
|
"claude-opus-4-8": { input: 5, output: 25, cacheRead: 0.5, cacheWrite5m: 6.25, cacheWrite1h: 10 },
|
|
19
22
|
"claude-opus-4-7": { input: 5, output: 25, cacheRead: 0.5, cacheWrite5m: 6.25, cacheWrite1h: 10 },
|
|
20
23
|
"claude-opus-4-6": { input: 5, output: 25, cacheRead: 0.5, cacheWrite5m: 6.25, cacheWrite1h: 10 },
|
|
21
|
-
"claude-sonnet-5-5": { input: 2, output: 10, cacheRead: 0.
|
|
24
|
+
"claude-sonnet-5-5": { input: 2, output: 10, cacheRead: 0.1, cacheWrite5m: 2.5, cacheWrite1h: 4 },
|
|
22
25
|
"claude-sonnet-5": { input: 2, output: 10, cacheRead: 0.2, cacheWrite5m: 2.5, cacheWrite1h: 4 },
|
|
23
26
|
"claude-sonnet-4-6": { input: 3, output: 15, cacheRead: 0.3, cacheWrite5m: 3.75, cacheWrite1h: 6 },
|
|
27
|
+
"claude-haiku-5-5": {
|
|
28
|
+
input: 0.1, output: 0.5, cacheRead: 0.01, cacheWrite5m: 0.125, cacheWrite1h: 0.2,
|
|
29
|
+
over: { above: 100_000, input: 0.5, output: 2.5, cacheRead: 0.05, cacheWrite5m: 0.625, cacheWrite1h: 1 },
|
|
30
|
+
},
|
|
24
31
|
"claude-haiku-4-5": { input: 1, output: 5, cacheRead: 0.1, cacheWrite5m: 1.25, cacheWrite1h: 2 },
|
|
25
32
|
};
|
|
26
33
|
|
|
@@ -61,8 +68,9 @@ function addUsage(a, b) {
|
|
|
61
68
|
|
|
62
69
|
// Price one usage record for a model; null when the model is not in the table.
|
|
63
70
|
export function priceUsage(usage, model) {
|
|
64
|
-
|
|
71
|
+
let p = PRICES[normalizeModel(model)];
|
|
65
72
|
if (!p) return null;
|
|
73
|
+
if (p.over && usage.input + usage.cacheRead + usage.cacheWrite > p.over.above) p = p.over;
|
|
66
74
|
const write5m = Math.max(0, usage.cacheWrite - usage.cacheWrite1h);
|
|
67
75
|
return (usage.input * p.input + usage.output * p.output + usage.cacheRead * p.cacheRead + write5m * p.cacheWrite5m + usage.cacheWrite1h * p.cacheWrite1h) / 1e6;
|
|
68
76
|
}
|
|
@@ -100,7 +108,22 @@ export function transcriptUsage(file) {
|
|
|
100
108
|
}
|
|
101
109
|
// The agent's model is the one most of its messages ran on (a transcript names one model in practice).
|
|
102
110
|
const model = [...models.entries()].sort((a, b) => b[1] - a[1]).map(([m]) => m)[0] || null;
|
|
103
|
-
|
|
111
|
+
// The transcript's USD: each message priced on its own model and tier (a tiered model is priced per request), null
|
|
112
|
+
// when any message's model is not in the table. A message without a model uses the transcript's.
|
|
113
|
+
let usd = model ? 0 : null;
|
|
114
|
+
if (model) {
|
|
115
|
+
for (const v of perMessage.values()) {
|
|
116
|
+
// An all-zero message (Claude Code's "<synthetic>" ones) costs nothing whatever its model.
|
|
117
|
+
if (!v.input && !v.output && !v.cacheRead && !v.cacheWrite && !v.cacheWrite1h) continue;
|
|
118
|
+
const price = priceUsage(v, v.model || model);
|
|
119
|
+
if (price === null) {
|
|
120
|
+
usd = null;
|
|
121
|
+
break;
|
|
122
|
+
}
|
|
123
|
+
usd += price;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
return { usage, model, messages: perMessage.size, usd };
|
|
104
127
|
}
|
|
105
128
|
|
|
106
129
|
// The workflow's label for an agent: `stage:task[:pass]` (`integrate:level-0`, `fix:agents-generator:2`). A swarm's
|
|
@@ -254,7 +277,7 @@ export function costRun({ runId, claudeDir, projectDir, report, reportPath } = {
|
|
|
254
277
|
agent.model = t.model;
|
|
255
278
|
agent.messages = t.messages;
|
|
256
279
|
agent.usage = t.usage;
|
|
257
|
-
agent.usd = t.
|
|
280
|
+
agent.usd = t.usd;
|
|
258
281
|
if (t.model && agent.usd === null) notes.push(`${j.agentId} (${agent.label || agent.stage}): model ${t.model} is not in the price table; not priced`);
|
|
259
282
|
if (!t.model && t.messages === 0) notes.push(`${j.agentId} (${agent.label || agent.stage}): transcript has no usage lines; not priced`);
|
|
260
283
|
} else {
|
|
@@ -467,7 +490,7 @@ export function costSession({ session, claudeDir = join(homedir(), ".claude"), p
|
|
|
467
490
|
const t = transcriptUsage(f);
|
|
468
491
|
addUsage(usage, t.usage);
|
|
469
492
|
if (!t.messages) continue;
|
|
470
|
-
const price = t.
|
|
493
|
+
const price = t.usd;
|
|
471
494
|
if (price === null) {
|
|
472
495
|
unpriced++;
|
|
473
496
|
notes.push(`${f}: model ${t.model || "unknown"} is not in the price table; not priced`);
|
|
@@ -264,7 +264,9 @@ export async function checkEmbeddingSanity(provider, pairs) {
|
|
|
264
264
|
|
|
265
265
|
// Task prefixes the provider layer must add; the server never adds them. Only the models below carry a
|
|
266
266
|
// prefix; every other model gets none. mxbai and qwen3 prefix queries only, documents get "".
|
|
267
|
-
|
|
267
|
+
export const PREFIXED_MODELS = ["embeddinggemma", "nomic-embed-text", "mxbai-embed-large", "qwen3-embedding"];
|
|
268
|
+
|
|
269
|
+
export function prefixFor(model, inputType) {
|
|
268
270
|
// Match on the lowercased basename after the last "/", so HF-style names (org/name) and Ollama names agree.
|
|
269
271
|
let m = String(model || "").split(/[/\\]/).pop().toLowerCase();
|
|
270
272
|
// LM Studio-style "text-embedding-<name>" ids: match on <name> when it is a prefixed family (not OpenAI's text-embedding-3-*).
|
|
@@ -90,10 +90,15 @@ function run(command, cwd, timeoutMs) {
|
|
|
90
90
|
// the MAIN checkout (--record-dir). Returns the entries land records under `verify` (command, ok, status, timedOut,
|
|
91
91
|
// durationMs, outputTail, plus credited: "verified-tree" when credited), stopping at the first failure like
|
|
92
92
|
// today's loop, or null when the output cannot be read (the caller then runs the loop unchanged).
|
|
93
|
+
// verify.mjs may wait timeoutMs x n on a held gate.lock and then run n commands of timeoutMs each.
|
|
94
|
+
export function vendoredVerifySpawnTimeoutMs(timeoutMs, n) {
|
|
95
|
+
return 2 * timeoutMs * n + 60000;
|
|
96
|
+
}
|
|
97
|
+
|
|
93
98
|
function verifyViaVendored(script, dir, work, commands, timeoutMs) {
|
|
94
99
|
try {
|
|
95
100
|
const args = [script, "--json", "--by", "land", "--cwd", work, "--record-dir", dir, "--timeout-ms", String(timeoutMs), ...commands.flatMap((c) => ["--command", c])];
|
|
96
|
-
const r = spawnSync(process.execPath, args, { cwd: work, encoding: "utf8", timeout: timeoutMs
|
|
101
|
+
const r = spawnSync(process.execPath, args, { cwd: work, encoding: "utf8", timeout: vendoredVerifySpawnTimeoutMs(timeoutMs, commands.length), maxBuffer: 64 * 1024 * 1024, env: commandEnv() });
|
|
97
102
|
let out = null;
|
|
98
103
|
try {
|
|
99
104
|
out = JSON.parse(String(r.stdout || "").trim().split("\n").pop());
|
|
@@ -18,7 +18,7 @@ import { homedir } from "node:os";
|
|
|
18
18
|
import { request as httpRequest } from "node:http";
|
|
19
19
|
import { request as httpsRequest } from "node:https";
|
|
20
20
|
import { classifyBlocks, adversaryBlocks } from "./board.mjs";
|
|
21
|
-
import { transcriptUsage,
|
|
21
|
+
import { transcriptUsage, projectSlug, CODEX_PRICES, CODEX_DEFAULT_MODEL } from "./cost.mjs";
|
|
22
22
|
import { MODELS_HEADING } from "./models.mjs";
|
|
23
23
|
|
|
24
24
|
let DatabaseSync;
|
|
@@ -958,18 +958,20 @@ export async function recallLessons(m, query, opts = {}) {
|
|
|
958
958
|
// stale and drops it, backfilling from the next candidate so k are returned when enough candidates exist,
|
|
959
959
|
// and a pass sets confirmed. Without one (checked: false), candidates are taken as-is — nothing is marked
|
|
960
960
|
// stale or confirmed, since there is nothing trustworthy to check them against.
|
|
961
|
+
// Every lessons column except the blob, derived from the live schema so a later column cannot drop out.
|
|
962
|
+
const loadCols = m
|
|
963
|
+
.prepare("PRAGMA table_info(lessons)")
|
|
964
|
+
.all()
|
|
965
|
+
.map((c) => c.name)
|
|
966
|
+
.filter((n) => n !== "embedding")
|
|
967
|
+
.join(", ");
|
|
968
|
+
const loadById = m.prepare(`SELECT ${loadCols}, embedding IS NOT NULL AS has_vec FROM lessons WHERE id = ?`);
|
|
961
969
|
const result = [];
|
|
962
970
|
let idx = 0;
|
|
963
971
|
while (result.length < k && idx < candidates.length) {
|
|
964
972
|
const cand = candidates[idx++];
|
|
965
973
|
// Every column except the blob (and the model/dims toRecallLesson strips anyway); hasVector comes from has_vec.
|
|
966
|
-
const { has_vec, ...full } =
|
|
967
|
-
.prepare(
|
|
968
|
-
`SELECT id, text, kind, scope, citation, source_agent, source_model, task, card, commit_sha, created, confirmed,
|
|
969
|
-
helpful, harmful, superseded_by, embedding_model, embedding_dims, stale, embedding IS NOT NULL AS has_vec
|
|
970
|
-
FROM lessons WHERE id = ?`
|
|
971
|
-
)
|
|
972
|
-
.get(cand.lesson.id);
|
|
974
|
+
const { has_vec, ...full } = loadById.get(cand.lesson.id);
|
|
973
975
|
cand.lesson = lessonRowToObject({ ...full, embedding: has_vec ? true : null });
|
|
974
976
|
if (!checked) {
|
|
975
977
|
result.push(toRecallLesson(cand.lesson, { score: cand.score, sides: cand.sides }));
|
|
@@ -1502,14 +1504,25 @@ function reflectionRowToObject(row) {
|
|
|
1502
1504
|
};
|
|
1503
1505
|
}
|
|
1504
1506
|
|
|
1505
|
-
// Deterministic, no LLM
|
|
1506
|
-
//
|
|
1507
|
-
//
|
|
1508
|
-
//
|
|
1509
|
-
|
|
1507
|
+
// Deterministic, no LLM. harmful is an explicit mark (card reflect-harmful-irrelevant-lessons): only an id in
|
|
1508
|
+
// `harmfulIds` (the lead's judgement that the lesson misled the work) gets harmful +1, once however often it is
|
|
1509
|
+
// named or used; a real block alone bumps nothing. For each task whose memoryUsed is non-empty, a real block earns
|
|
1510
|
+
// no helpful (real wins even when the task also verified), else a first-verified task bumps helpful on every used
|
|
1511
|
+
// lesson not named harmful; neither verified nor real does nothing. An id naming no lesson is reported in
|
|
1512
|
+
// `unknown`, never an error. apply:false (a dry run's preview) computes the same result without writing anything.
|
|
1513
|
+
function computeReflectCounters(m, blocks, report, { apply, harmfulIds = [] }) {
|
|
1510
1514
|
const helpful = [];
|
|
1511
1515
|
const harmful = [];
|
|
1512
1516
|
const unknown = [];
|
|
1517
|
+
const named = new Set(harmfulIds);
|
|
1518
|
+
for (const id of named) {
|
|
1519
|
+
if (getLesson(m, id)) {
|
|
1520
|
+
if (apply) m.prepare("UPDATE lessons SET harmful = harmful + 1 WHERE id = ?").run(id);
|
|
1521
|
+
harmful.push(id);
|
|
1522
|
+
} else {
|
|
1523
|
+
unknown.push(id);
|
|
1524
|
+
}
|
|
1525
|
+
}
|
|
1513
1526
|
for (const level of report.levels || []) {
|
|
1514
1527
|
for (const task of level.tasks || []) {
|
|
1515
1528
|
const used = Array.isArray(task.memoryUsed) ? task.memoryUsed : [];
|
|
@@ -1517,15 +1530,11 @@ function computeReflectCounters(m, blocks, report, { apply }) {
|
|
|
1517
1530
|
const real = blocks.some((b) => b.task === task.id && b.class === "real");
|
|
1518
1531
|
if (real) {
|
|
1519
1532
|
for (const id of used) {
|
|
1520
|
-
if (getLesson(m, id))
|
|
1521
|
-
if (apply) m.prepare("UPDATE lessons SET harmful = harmful + 1 WHERE id = ?").run(id);
|
|
1522
|
-
harmful.push(id);
|
|
1523
|
-
} else {
|
|
1524
|
-
unknown.push(id);
|
|
1525
|
-
}
|
|
1533
|
+
if (!getLesson(m, id) && !named.has(id)) unknown.push(id);
|
|
1526
1534
|
}
|
|
1527
1535
|
} else if (task.verified === true) {
|
|
1528
1536
|
for (const id of used) {
|
|
1537
|
+
if (named.has(id)) continue;
|
|
1529
1538
|
if (getLesson(m, id)) {
|
|
1530
1539
|
if (apply) m.prepare("UPDATE lessons SET helpful = helpful + 1 WHERE id = ?").run(id);
|
|
1531
1540
|
helpful.push(id);
|
|
@@ -1756,7 +1765,7 @@ function computeReflectCost({ dir, claudeDir, sessionId, totalCostUsd }) {
|
|
|
1756
1765
|
if (existsSync(file)) {
|
|
1757
1766
|
try {
|
|
1758
1767
|
const t = transcriptUsage(file);
|
|
1759
|
-
const usd = t.
|
|
1768
|
+
const usd = t.usd;
|
|
1760
1769
|
if (usd !== null) return { usd, usdSource: "transcript" };
|
|
1761
1770
|
} catch {
|
|
1762
1771
|
// fall through to the envelope's own number
|
|
@@ -1785,6 +1794,7 @@ export async function runReflect(m, report, opts = {}) {
|
|
|
1785
1794
|
model = null,
|
|
1786
1795
|
claudeDir,
|
|
1787
1796
|
dryRun = false,
|
|
1797
|
+
harmful = [],
|
|
1788
1798
|
propose,
|
|
1789
1799
|
now = nowIso(),
|
|
1790
1800
|
} = opts;
|
|
@@ -1798,13 +1808,30 @@ export async function runReflect(m, report, opts = {}) {
|
|
|
1798
1808
|
// happen before any write, so it is the very first thing after the idempotency check.
|
|
1799
1809
|
const blocks = classifyBlocks(report, classes);
|
|
1800
1810
|
|
|
1811
|
+
// A harmful id must be one a task recalled; refused before any write, naming it and the recalled ids.
|
|
1812
|
+
const harmfulIds = [...new Set(harmful)];
|
|
1813
|
+
if (harmfulIds.length) {
|
|
1814
|
+
const recalled = new Set();
|
|
1815
|
+
for (const level of report.levels || []) {
|
|
1816
|
+
for (const task of level.tasks || []) {
|
|
1817
|
+
if (Array.isArray(task.memoryUsed)) for (const id of task.memoryUsed) recalled.add(id);
|
|
1818
|
+
}
|
|
1819
|
+
}
|
|
1820
|
+
const outside = harmfulIds.filter((id) => !recalled.has(id));
|
|
1821
|
+
if (outside.length) {
|
|
1822
|
+
throw new Error(
|
|
1823
|
+
`--harmful id(s) not in any task's memoryUsed: ${outside.join(", ")}. memoryUsed ids: ${recalled.size ? [...recalled].join(", ") : "(none)"}.`
|
|
1824
|
+
);
|
|
1825
|
+
}
|
|
1826
|
+
}
|
|
1827
|
+
|
|
1801
1828
|
let counters;
|
|
1802
1829
|
if (dryRun) {
|
|
1803
|
-
counters = computeReflectCounters(m, blocks, report, { apply: false });
|
|
1830
|
+
counters = computeReflectCounters(m, blocks, report, { apply: false, harmfulIds });
|
|
1804
1831
|
} else {
|
|
1805
1832
|
m.exec("BEGIN");
|
|
1806
1833
|
try {
|
|
1807
|
-
counters = computeReflectCounters(m, blocks, report, { apply: true });
|
|
1834
|
+
counters = computeReflectCounters(m, blocks, report, { apply: true, harmfulIds });
|
|
1808
1835
|
m.prepare(
|
|
1809
1836
|
`INSERT INTO reflections (report_hash, recorded, run, card, commit_sha, model, counters, proposed, appended, bumped, rejected, usd, usd_source, session_id)
|
|
1810
1837
|
VALUES (?, ?, ?, ?, ?, ?, ?, NULL, NULL, NULL, NULL, NULL, NULL, NULL)`
|
|
@@ -42,7 +42,8 @@ export function crewOf(plan, task) {
|
|
|
42
42
|
// feature, never more than eight. The validator warns; it does not refuse, since the planner may have a reason.
|
|
43
43
|
export const FEATURE_TASKS = 5;
|
|
44
44
|
export const TASK_CAP = 8;
|
|
45
|
-
|
|
45
|
+
// agents 26 is the minimum that lets the default fixAttempts 5 run with one adversary seat when Codex is down and the Claude fallback runs on every pass (card flow-budget-after-parallel-adversary); a supervisor seat or a fix retry is not covered.
|
|
46
|
+
export const DEFAULT_BUDGET = { agents: 26, tokens: 400000, wallMinutes: 40 };
|
|
46
47
|
|
|
47
48
|
export function loadPlan(dir, relpath = PLAN_RELPATH) {
|
|
48
49
|
const file = join(dir, relpath);
|
|
@@ -27,25 +27,25 @@
|
|
|
27
27
|
// flow, or named by .doug/plan.json (each with --force removes it
|
|
28
28
|
// and prints a warning to stderr); prints the removed card as JSON
|
|
29
29
|
// to stdout so a mistaken removal can be re-added by hand
|
|
30
|
-
// board.mjs record <id> <report.json> [dir] [--cost <usd>] [--codex-cost <usd>] [--shared-cost <usd>] [--wall <text>] [--commit <sha>] [--adversary "<id>=<class>[: <reason>]"]... [--note <text>] [--rehearsal <scenario>] [--run <run-id>] [--claude-dir <dir>]
|
|
30
|
+
// board.mjs record <id> <report.json> [dir] --summary "<one sentence>" [--cost <usd>] [--codex-cost <usd>] [--shared-cost <usd>] [--wall <text>] [--commit <sha>] [--adversary "<id>=<class>[: <reason>]"]... [--note <text>] [--rehearsal <scenario>] [--run <run-id>] [--claude-dir <dir>]
|
|
31
31
|
// append a run entry for the card to docs/live-runs.md; every adversary block
|
|
32
32
|
// in the report is classified real, marginal, or false (repeat the flag per block).
|
|
33
33
|
// When .doug/plan.json lists the card among its `cards` (a batch), the entry
|
|
34
34
|
// shows only that card's tasks, --cost is that card's tasks' cost, and
|
|
35
35
|
// --shared-cost <usd> the shared integration agents', counted once
|
|
36
|
-
// board.mjs record <id> --hand [dir] [--commit <sha>] [--wall <text>] [--gate <text>] [--note <text>] [--rehearsal <scenario>]
|
|
36
|
+
// board.mjs record <id> --hand [dir] --summary "<one sentence>" [--commit <sha>] [--wall <text>] [--gate <text>] [--note <text>] [--rehearsal <scenario>]
|
|
37
37
|
// append a hand-track entry (no report) for the card
|
|
38
38
|
// --rehearsal <scenario> (either form) marks the entry as a rehearse.mjs run on the ts-basic fixture: the first
|
|
39
39
|
// prose line and the commit sha (abbreviated to 7 characters) read as the fixture's, not this repository's.
|
|
40
40
|
// Either record form also promotes .doug/.state/research/<id>.md to docs/research/<id>.md when that note
|
|
41
41
|
// exists and differs from the docs copy, copying it and printing one line saying so; a card with no note, or
|
|
42
42
|
// whose docs copy is already byte-identical, is untouched, silently.
|
|
43
|
-
// board.mjs summary <report.json> [--cost <usd>] [--codex-cost <usd>] [--wall <text>] [--commit <sha>] [--card <id>] [--run <run-id>] [--claude-dir <dir>]
|
|
43
|
+
// board.mjs summary <report.json> --summary "<one sentence>" [--cost <usd>] [--codex-cost <usd>] [--wall <text>] [--commit <sha>] [--card <id>] [--run <run-id>] [--claude-dir <dir>]
|
|
44
44
|
// the three-line chat summary of a report (--card: that card's tasks only)
|
|
45
45
|
|
|
46
46
|
import { readFileSync } from "node:fs";
|
|
47
47
|
import { relative, resolve } from "node:path";
|
|
48
|
-
import { loadBoard, saveBoard, findCard, nextReadyCard, nextReadyCards, moveCard, editCard, removeCard, reorderCard, readyCapWarning, wallCapMinutes, WALL_BUDGET_MIN, WALL_BUDGET_MIN_N, parseLiveRunWalls, wallBudgetTable, runEntry, handEntry, runSummary, boardPath, parseAdversaryClasses, filterReportForCard, recordLanding } from "../lib/board.mjs";
|
|
48
|
+
import { loadBoard, saveBoard, findCard, nextReadyCard, nextReadyCards, moveCard, editCard, removeCard, reorderCard, readyCapWarning, wallCapMinutes, WALL_BUDGET_MIN, WALL_BUDGET_MIN_N, parseLiveRunWalls, wallBudgetTable, runEntry, handEntry, handWallVerdict, runSummary, boardPath, parseAdversaryClasses, filterReportForCard, recordLanding } from "../lib/board.mjs";
|
|
49
49
|
import { loadPlan, unwrapReport, DEFAULT_BUDGET } from "../lib/plan.mjs";
|
|
50
50
|
import { wallForRun } from "../lib/cost.mjs";
|
|
51
51
|
|
|
@@ -66,10 +66,10 @@ const SUBCOMMAND_FLAGS = {
|
|
|
66
66
|
remove: ["force"],
|
|
67
67
|
reorder: ["before", "after", "top", "bottom"],
|
|
68
68
|
budgets: ["from"],
|
|
69
|
-
summary: ["cost", "codex-cost", "wall", "commit", "card", "run", "claude-dir"],
|
|
69
|
+
summary: ["summary", "cost", "codex-cost", "wall", "commit", "card", "run", "claude-dir"],
|
|
70
70
|
};
|
|
71
|
-
const RECORD_HAND_FLAGS = ["hand", "commit", "wall", "gate", "note", "rehearsal"];
|
|
72
|
-
const RECORD_REPORT_FLAGS = ["cost", "codex-cost", "wall", "commit", "adversary", "shared-cost", "note", "rehearsal", "run", "claude-dir"];
|
|
71
|
+
const RECORD_HAND_FLAGS = ["summary", "hand", "commit", "wall", "gate", "note", "rehearsal"];
|
|
72
|
+
const RECORD_REPORT_FLAGS = ["summary", "cost", "codex-cost", "wall", "commit", "adversary", "shared-cost", "note", "rehearsal", "run", "claude-dir"];
|
|
73
73
|
const opts = {};
|
|
74
74
|
const positional = [];
|
|
75
75
|
for (let i = 0; i < argv.length; i++) {
|
|
@@ -101,10 +101,10 @@ const USAGE_FOR = {
|
|
|
101
101
|
remove: "usage: board.mjs remove <id> [dir] [--force]",
|
|
102
102
|
reorder: "usage: board.mjs reorder <id> [dir] --before <other> | --after <other> | --top | --bottom",
|
|
103
103
|
budgets: "usage: board.mjs budgets [dir] [--from <live-runs file>]",
|
|
104
|
-
summary: "usage: board.mjs summary <report.json> [--cost <usd>] [--codex-cost <usd>] [--wall <text>] [--commit <sha>] [--card <id>] [--run <run-id>] [--claude-dir <dir>]",
|
|
104
|
+
summary: "usage: board.mjs summary <report.json> --summary \"<one sentence>\" [--cost <usd>] [--codex-cost <usd>] [--wall <text>] [--commit <sha>] [--card <id>] [--run <run-id>] [--claude-dir <dir>]",
|
|
105
105
|
};
|
|
106
|
-
const RECORD_HAND_USAGE = "usage: board.mjs record <id> --hand [dir] [--commit <sha>] [--wall <text>] [--gate <text>] [--note <text>] [--rehearsal <scenario>]";
|
|
107
|
-
const RECORD_REPORT_USAGE = "usage: board.mjs record <id> <report.json> [dir] [--cost <usd>] [--codex-cost <usd>] [--shared-cost <usd>] [--wall <text>] [--commit <sha>] [--adversary \"<id>=<class>[: <reason>]\"]... [--note <text>] [--rehearsal <scenario>] [--run <run-id>] [--claude-dir <dir>]";
|
|
106
|
+
const RECORD_HAND_USAGE = "usage: board.mjs record <id> --hand [dir] --summary \"<one sentence>\" [--commit <sha>] [--wall <text>] [--gate <text>] [--note <text>] [--rehearsal <scenario>]";
|
|
107
|
+
const RECORD_REPORT_USAGE = "usage: board.mjs record <id> <report.json> [dir] --summary \"<one sentence>\" [--cost <usd>] [--codex-cost <usd>] [--shared-cost <usd>] [--wall <text>] [--commit <sha>] [--adversary \"<id>=<class>[: <reason>]\"]... [--note <text>] [--rehearsal <scenario>] [--run <run-id>] [--claude-dir <dir>]";
|
|
108
108
|
|
|
109
109
|
// An unknown --flag for the given subcommand: exit 2, name the flag and the subcommand's usage.
|
|
110
110
|
// Runs on flag names only (parse order doesn't matter), before any work or write.
|
|
@@ -128,6 +128,17 @@ if (cmd === "record" || Object.prototype.hasOwnProperty.call(SUBCOMMAND_FLAGS, c
|
|
|
128
128
|
}
|
|
129
129
|
}
|
|
130
130
|
|
|
131
|
+
// --summary: the lead's one sentence of what changed for the user, required by record and summary (card
|
|
132
|
+
// landing-one-sentence-summary). Trimmed, non-empty, one line, no sentence end followed by more text.
|
|
133
|
+
// Checked before anything is appended or promoted; printed as stdout's first line, never written to a file.
|
|
134
|
+
let summarySentence = null;
|
|
135
|
+
if (cmd === "record" || cmd === "summary") {
|
|
136
|
+
const text = typeof opts.summary === "string" ? opts.summary.trim() : "";
|
|
137
|
+
if (!text) fail(`--summary "<one sentence>" is required for board.mjs ${cmd}: one sentence, in plain words, of what changed for the user`, 2);
|
|
138
|
+
if (/[\r\n]/.test(text) || /[.!?]\s+\S/.test(text)) fail(`--summary must be one sentence for board.mjs ${cmd}: no newline, and no sentence end followed by more text`, 2);
|
|
139
|
+
summarySentence = text;
|
|
140
|
+
}
|
|
141
|
+
|
|
131
142
|
try {
|
|
132
143
|
switch (cmd) {
|
|
133
144
|
case "next": {
|
|
@@ -180,8 +191,11 @@ try {
|
|
|
180
191
|
const [id, column] = rest;
|
|
181
192
|
if (!id || !column) fail("usage: board.mjs move <id> <column> [dir] [--source <text>]", 2);
|
|
182
193
|
const dir = dirFrom(rest, 2);
|
|
183
|
-
|
|
194
|
+
const moved = moveCard(loadBoard(dir), id, column, { source: opts.source });
|
|
195
|
+
saveBoard(dir, moved);
|
|
184
196
|
process.stdout.write(`Moved ${id} to ${column} in ${relative(dir, boardPath(dir))}.\n`);
|
|
197
|
+
const capLine = column === "ready" ? readyCapWarning(moved) : null;
|
|
198
|
+
if (capLine) process.stderr.write(capLine);
|
|
185
199
|
break;
|
|
186
200
|
}
|
|
187
201
|
case "edit": {
|
|
@@ -252,17 +266,22 @@ try {
|
|
|
252
266
|
case "record": {
|
|
253
267
|
if (opts.hand) {
|
|
254
268
|
const [id] = rest;
|
|
255
|
-
if (!id) fail("usage: board.mjs record <id> --hand [dir] [--commit <sha>] [--wall <text>] [--gate <text>] [--note <text>] [--rehearsal <scenario>]", 2);
|
|
269
|
+
if (!id) fail("usage: board.mjs record <id> --hand [dir] --summary \"<one sentence>\" [--commit <sha>] [--wall <text>] [--gate <text>] [--note <text>] [--rehearsal <scenario>]", 2);
|
|
256
270
|
const dir = dirFrom(rest, 1);
|
|
257
271
|
const card = findCard(loadBoard(dir), id);
|
|
258
272
|
const entry = handEntry({ card, commit: opts.commit ?? null, wallClock: opts.wall ?? null, gate: opts.gate ?? null, note: opts.note ?? null, record: relative(dir, boardPath(dir)), rehearsal: opts.rehearsal ?? null });
|
|
259
273
|
const { file, promoted } = recordLanding(dir, id, entry);
|
|
274
|
+
process.stdout.write(`${summarySentence}\n`);
|
|
275
|
+
if (!opts.rehearsal) {
|
|
276
|
+
const verdict = handWallVerdict(card, opts.wall ?? null);
|
|
277
|
+
if (verdict.over) process.stdout.write(`Wall budget: ${verdict.text}\n`);
|
|
278
|
+
}
|
|
260
279
|
if (promoted) process.stdout.write(`Promoted the research note for ${id} to ${promoted}.\n`);
|
|
261
280
|
process.stdout.write(`Appended a hand-track entry for ${id} to ${file}.\n`);
|
|
262
281
|
break;
|
|
263
282
|
}
|
|
264
283
|
const [id, reportFile] = rest;
|
|
265
|
-
if (!id || !reportFile) fail("usage: board.mjs record <id> <report.json> [dir] [--cost <usd>] [--codex-cost <usd>] [--shared-cost <usd>] [--wall <text>] [--commit <sha>] [--adversary \"<id>=<class>[: <reason>]\"]... [--note <text>] [--rehearsal <scenario>] [--run <run-id>] [--claude-dir <dir>]", 2);
|
|
284
|
+
if (!id || !reportFile) fail("usage: board.mjs record <id> <report.json> [dir] --summary \"<one sentence>\" [--cost <usd>] [--codex-cost <usd>] [--shared-cost <usd>] [--wall <text>] [--commit <sha>] [--adversary \"<id>=<class>[: <reason>]\"]... [--note <text>] [--rehearsal <scenario>] [--run <run-id>] [--claude-dir <dir>]", 2);
|
|
266
285
|
const dir = dirFrom(rest, 2);
|
|
267
286
|
const card = findCard(loadBoard(dir), id);
|
|
268
287
|
const report = unwrapReport(JSON.parse(readFileSync(resolve(reportFile), "utf8")));
|
|
@@ -273,13 +292,14 @@ try {
|
|
|
273
292
|
const wall = opts.run ? wallForRun({ runId: opts.run, claudeDir: opts["claude-dir"], projectDir: dir, wallMinutes: report.budget?.wallMinutes ?? DEFAULT_BUDGET.wallMinutes }) : null;
|
|
274
293
|
const entry = runEntry({ card, report, wall, cost: opts.cost !== undefined ? Number(opts.cost) : null, codexCost: opts["codex-cost"] !== undefined ? Number(opts["codex-cost"]) : null, wallClock: opts.wall ?? null, mergeCommit: opts.commit ?? null, record: relative(dir, boardPath(dir)), adversary: parseAdversaryClasses(opts.adversary || []), batch, taskIds, sharedCost: opts["shared-cost"] !== undefined ? Number(opts["shared-cost"]) : null, note: opts.note ?? null, rehearsal: opts.rehearsal ?? null });
|
|
275
294
|
const { file, promoted } = recordLanding(dir, id, entry);
|
|
295
|
+
process.stdout.write(`${summarySentence}\n`);
|
|
276
296
|
if (promoted) process.stdout.write(`Promoted the research note for ${id} to ${promoted}.\n`);
|
|
277
297
|
process.stdout.write(`Appended a run entry for ${id} to ${file}.\n`);
|
|
278
298
|
break;
|
|
279
299
|
}
|
|
280
300
|
case "summary": {
|
|
281
301
|
const [reportFile] = rest;
|
|
282
|
-
if (!reportFile) fail("usage: board.mjs summary <report.json> [--cost <usd>] [--codex-cost <usd>] [--wall <text>] [--commit <sha>] [--card <id>] [--run <run-id>] [--claude-dir <dir>]", 2);
|
|
302
|
+
if (!reportFile) fail("usage: board.mjs summary <report.json> --summary \"<one sentence>\" [--cost <usd>] [--codex-cost <usd>] [--wall <text>] [--commit <sha>] [--card <id>] [--run <run-id>] [--claude-dir <dir>]", 2);
|
|
283
303
|
let report = unwrapReport(JSON.parse(readFileSync(resolve(reportFile), "utf8")));
|
|
284
304
|
if (opts.card !== undefined) {
|
|
285
305
|
// With the plan at hand the filter also covers a report whose tasks carry no card.
|
|
@@ -289,6 +309,7 @@ try {
|
|
|
289
309
|
const wall = opts.run ? wallForRun({ runId: opts.run, claudeDir: opts["claude-dir"], projectDir: process.cwd(), wallMinutes: report.budget?.wallMinutes ?? DEFAULT_BUDGET.wallMinutes }) : null;
|
|
290
310
|
const sumPlan = opts.card !== undefined ? loadPlan(dirFrom(rest, 1)) : null;
|
|
291
311
|
const sumBatch = sumPlan && Array.isArray(sumPlan.cards) && sumPlan.cards.includes(opts.card) ? sumPlan.cards : null;
|
|
312
|
+
process.stdout.write(`${summarySentence}\n`);
|
|
292
313
|
process.stdout.write(runSummary({ report, wall, batch: sumBatch, card: opts.card ?? null, cost: opts.cost !== undefined ? Number(opts.cost) : null, codexCost: opts["codex-cost"] !== undefined ? Number(opts["codex-cost"]) : null, wallClock: opts.wall ?? null, mergeCommit: opts.commit ?? null }));
|
|
293
314
|
break;
|
|
294
315
|
}
|
|
@@ -2,7 +2,9 @@
|
|
|
2
2
|
// The lead's own context percentage (card autopilot-unlimited), read from the session transcript.
|
|
3
3
|
// context.mjs [dir] [--session <id|latest>] [--claude-dir <path>]
|
|
4
4
|
// dir the project directory (default cwd); <dir>/.doug/config.json supplies autopilot.contextThreshold
|
|
5
|
-
// --session a session id or latest
|
|
5
|
+
// --session a session id or latest, as cost.mjs --session. Default: the CLAUDE_CODE_SESSION_ID environment
|
|
6
|
+
// variable (the lead's own session, so a newer non-lead transcript, such as a reflect pass, is not read);
|
|
7
|
+
// latest only when it is unset or empty. A variable naming a session with no transcript exits 1.
|
|
6
8
|
// --claude-dir where ~/.claude lives (tests point it at a fixture)
|
|
7
9
|
// Prints one line and exits 0; exits 1 with a stderr message when there is no transcript or no usage line.
|
|
8
10
|
import { readFileSync } from "node:fs";
|
|
@@ -33,7 +35,7 @@ const threshold = contextThreshold(config, (m) => process.stderr.write(`[doug] $
|
|
|
33
35
|
|
|
34
36
|
let file;
|
|
35
37
|
try {
|
|
36
|
-
file = sessionTranscriptPath({ session: opts.session || "latest", ...(opts["claude-dir"] ? { claudeDir: resolve(opts["claude-dir"]) } : {}), projectDir: dir });
|
|
38
|
+
file = sessionTranscriptPath({ session: opts.session || process.env.CLAUDE_CODE_SESSION_ID || "latest", ...(opts["claude-dir"] ? { claudeDir: resolve(opts["claude-dir"]) } : {}), projectDir: dir });
|
|
37
39
|
} catch (err) {
|
|
38
40
|
process.stderr.write(`context: ${err.message}\n`);
|
|
39
41
|
process.exit(1);
|
|
@@ -79,11 +79,14 @@
|
|
|
79
79
|
// lesson's citation is re-checked against [dir] only when
|
|
80
80
|
// [dir] is a real project root (contains .doug)
|
|
81
81
|
// memory.mjs reflect <report.json> [dir] [--run <wf-id>] [--card <id>] [--commit <sha>]
|
|
82
|
-
// [--adversary "<id>=<class>[: <reason>]"]... [--claude-bin <path>] [--claude-dir <path>]
|
|
82
|
+
// [--adversary "<id>=<class>[: <reason>]"]... [--harmful <id>]... [--claude-bin <path>] [--claude-dir <path>]
|
|
83
83
|
// [--model <name>] [--max-usd <n>] [--dry-run] [--json]
|
|
84
84
|
// runs after record: applies deterministic helpful/harmful
|
|
85
85
|
// counters to lessons named in each task's memoryUsed (committed
|
|
86
|
-
// even if the LLM pass below fails
|
|
86
|
+
// even if the LLM pass below fails; a real block alone bumps no
|
|
87
|
+
// harmful: each --harmful <id>, a recalled lesson that misled the
|
|
88
|
+
// work, gets harmful +1 and no helpful; an id outside every
|
|
89
|
+
// task's memoryUsed exits 1 before any write), then a cheap-row `claude -p
|
|
87
90
|
// --json-schema` pass proposes at most 3 lessons from the report,
|
|
88
91
|
// filtered in code (a pitfall needs a block classed real, a
|
|
89
92
|
// pattern needs a first-pass verified task, the citation must be
|
|
@@ -96,7 +99,7 @@
|
|
|
96
99
|
// transcripts cost.mjs reads, falling back to the CLI's own
|
|
97
100
|
// cost estimate. Needs Node >=22.16 (node:sqlite).
|
|
98
101
|
// memory.mjs reflect --hand <card-id> [dir] --commit <sha> [--rounds <n>] [--review "R1=<class>[: <reason>]"]...
|
|
99
|
-
// [--note <text>] [--claude-bin <path>] [--model <name>] [--max-usd <n>] [--dry-run] [--json]
|
|
102
|
+
// [--harmful <id>]... [--note <text>] [--claude-bin <path>] [--model <name>] [--max-usd <n>] [--dry-run] [--json]
|
|
100
103
|
// (card memory-every-session) the same pass for a hand landing:
|
|
101
104
|
// assembles .doug/.state/reports/<card-id>/hand-report.json from
|
|
102
105
|
// the saved briefs/, reviews/ and lessons.json (memoryUsed = its
|
|
@@ -204,7 +207,7 @@ import {
|
|
|
204
207
|
|
|
205
208
|
const argv = process.argv.slice(2);
|
|
206
209
|
const FLAGS = new Set(["json", "all", "hand", "dry-run", "check", "no-check"]);
|
|
207
|
-
const REPEATABLE = new Set(["adversary", "review"]);
|
|
210
|
+
const REPEATABLE = new Set(["adversary", "review", "harmful"]);
|
|
208
211
|
const opts = {};
|
|
209
212
|
const positional = [];
|
|
210
213
|
for (let i = 0; i < argv.length; i++) {
|
|
@@ -612,7 +615,7 @@ try {
|
|
|
612
615
|
}
|
|
613
616
|
case "reflect": {
|
|
614
617
|
const usage =
|
|
615
|
-
'usage: memory.mjs reflect <report.json> [dir] [--run <wf-id>] [--card <id>] [--commit <sha>] [--adversary "<id>=<class>[: <reason>]"]... [--claude-bin <path>] [--claude-dir <path>] [--model <name>] [--max-usd <n>] [--dry-run] [--json]';
|
|
618
|
+
'usage: memory.mjs reflect <report.json> [dir] [--run <wf-id>] [--card <id>] [--commit <sha>] [--adversary "<id>=<class>[: <reason>]"]... [--harmful <id>]... [--claude-bin <path>] [--claude-dir <path>] [--model <name>] [--max-usd <n>] [--dry-run] [--json]';
|
|
616
619
|
let dir;
|
|
617
620
|
let reportPath;
|
|
618
621
|
let reportText;
|
|
@@ -621,7 +624,7 @@ try {
|
|
|
621
624
|
let handCard = null;
|
|
622
625
|
if (opts.hand) {
|
|
623
626
|
const handUsage =
|
|
624
|
-
'usage: memory.mjs reflect --hand <card-id> [dir] --commit <sha> [--rounds <n>] [--review "R1=<real|marginal|false>[: <reason>]"]... [--note <text>] [--claude-bin <path>] [--model <name>] [--max-usd <n>] [--dry-run] [--json]';
|
|
627
|
+
'usage: memory.mjs reflect --hand <card-id> [dir] --commit <sha> [--rounds <n>] [--review "R1=<real|marginal|false>[: <reason>]"]... [--harmful <id>]... [--note <text>] [--claude-bin <path>] [--model <name>] [--max-usd <n>] [--dry-run] [--json]';
|
|
625
628
|
const [id] = rest;
|
|
626
629
|
if (!id || !opts.commit) fail(handUsage, 2);
|
|
627
630
|
handCard = id;
|
|
@@ -724,6 +727,7 @@ try {
|
|
|
724
727
|
model,
|
|
725
728
|
claudeDir,
|
|
726
729
|
dryRun,
|
|
730
|
+
harmful: opts.harmful || [],
|
|
727
731
|
propose,
|
|
728
732
|
});
|
|
729
733
|
if (!dryRun && result.ok && result.appended && result.appended.length) {
|
|
@@ -51,6 +51,7 @@ import { landPlan, INTEGRATION_WORKTREE } from "../lib/land.mjs";
|
|
|
51
51
|
import { removeFinishedWorktrees, retireTaskWorktrees, retireIntegrationWorktree, branchHead, isGitRepo } from "../lib/worktrees.mjs";
|
|
52
52
|
import { readMemoryConfig, readMemoryStaleDays, readIndexConfig, createProvider } from "../lib/embeddings.mjs";
|
|
53
53
|
import { existsSync, readFileSync, statSync } from "node:fs";
|
|
54
|
+
import { execFileSync } from "node:child_process";
|
|
54
55
|
|
|
55
56
|
// The project's install command from .doug/config.json, used when the plan does not name one.
|
|
56
57
|
function configInstall(d) {
|
|
@@ -88,8 +89,29 @@ function configDeleteBranches(d) {
|
|
|
88
89
|
return false;
|
|
89
90
|
}
|
|
90
91
|
}
|
|
92
|
+
// card plan-base-branch-default: the branch checked out in d, or on a detached HEAD its full sha (noted on
|
|
93
|
+
// stderr); null when d is not a git repository or git fails.
|
|
94
|
+
function currentBase(d) {
|
|
95
|
+
const git = (...args) => execFileSync("git", args, { cwd: d, encoding: "utf8", stdio: "pipe" }).trim();
|
|
96
|
+
try {
|
|
97
|
+
const b = git("symbolic-ref", "--short", "-q", "HEAD");
|
|
98
|
+
if (b) return b;
|
|
99
|
+
} catch {}
|
|
100
|
+
try {
|
|
101
|
+
const sha = git("rev-parse", "--verify", "-q", "HEAD");
|
|
102
|
+
if (!sha) return null;
|
|
103
|
+
process.stderr.write(`[doug] plan.mjs: detached HEAD; baseBranch set to ${sha}\n`);
|
|
104
|
+
return sha;
|
|
105
|
+
} catch {
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
91
109
|
function withDefaults(p, d) {
|
|
92
110
|
let out = p;
|
|
111
|
+
if (typeof out.baseBranch !== "string" || !out.baseBranch.trim()) {
|
|
112
|
+
const base = currentBase(d);
|
|
113
|
+
if (base) out = { ...out, baseBranch: base };
|
|
114
|
+
}
|
|
93
115
|
if (!out.install && configInstall(d)) out = { ...out, install: configInstall(d) };
|
|
94
116
|
const hasCommand = out.adversary && typeof out.adversary === "object" && typeof out.adversary.command === "string" && out.adversary.command.trim();
|
|
95
117
|
if (out.adversary !== false && !hasCommand && configAdversary(d)) out = { ...out, adversary: { ...(out.adversary && typeof out.adversary === "object" ? out.adversary : {}), command: configAdversary(d) } };
|
|
@@ -44,9 +44,9 @@ Size S, or a card with no size at all: by hand, no question.
|
|
|
44
44
|
|
|
45
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.
|
|
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
|
+
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 (including its `Scripts: ${CLAUDE_PLUGIN_ROOT}/scripts.` sentence), 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).
|
|
48
48
|
|
|
49
|
-
Otherwise, for one card, ask with `AskUserQuestion`: run it as a swarm, or by hand. Swarm: spawn the planner the way `doug-plan` does, on the `plan` row: read it with the `plan.mjs models` command, `node "${CLAUDE_PLUGIN_ROOT}/scripts/plan.mjs" models` (`roles.plan.model`, `roles.plan.effort`), and pass that row's model (omit `model` when the row says `inherit`), but tell it this plan runs as a swarm the way `/doug-swarm` does: `Agent({ subagent_type: "doug-flow:planner", model: "<the plan row's model>", prompt: "Board card <id>, title \"<title>\": <goal>. This plan runs as a swarm: set \"swarm\": true in the plan file, and size every task M or L unless it is truly one bounded change (a size-S task keeps a single implementer). Write .doug/plan.json and return the rendered plan." })`. Then `node "${CLAUDE_PLUGIN_ROOT}/scripts/plan.mjs" set card <id>` and `plan.mjs set swarm on` (`node "${CLAUDE_PLUGIN_ROOT}/scripts/plan.mjs" set swarm on`) guarantee the card and the opt-in even if the planner forgot, put `plan.mjs show` in the chat, and ask the approval gate with `AskUserQuestion`: approve as written, edit first, or stop. Approval is never automatic.
|
|
49
|
+
Otherwise, for one card, ask with `AskUserQuestion`: run it as a swarm, or by hand. Swarm: spawn the planner the way `doug-plan` does, on the `plan` row: read it with the `plan.mjs models` command, `node "${CLAUDE_PLUGIN_ROOT}/scripts/plan.mjs" models` (`roles.plan.model`, `roles.plan.effort`), and pass that row's model (omit `model` when the row says `inherit`), but tell it this plan runs as a swarm the way `/doug-swarm` does: `Agent({ subagent_type: "doug-flow:planner", model: "<the plan row's model>", prompt: "Board card <id>, title \"<title>\": <goal>. This plan runs as a swarm: set \"swarm\": true in the plan file, and size every task M or L unless it is truly one bounded change (a size-S task keeps a single implementer). Scripts: ${CLAUDE_PLUGIN_ROOT}/scripts. Write .doug/plan.json and return the rendered plan." })`. Then `node "${CLAUDE_PLUGIN_ROOT}/scripts/plan.mjs" set card <id>` and `plan.mjs set swarm on` (`node "${CLAUDE_PLUGIN_ROOT}/scripts/plan.mjs" set swarm on`) guarantee the card and the opt-in even if the planner forgot, put `plan.mjs show` in the chat, and ask the approval gate with `AskUserQuestion`: approve as written, edit first, or stop. Approval is never automatic.
|
|
50
50
|
|
|
51
51
|
On approve: `plan.mjs approve`, `plan.mjs anchor`, invoke the `swarm-launch` skill. Note the wall clock when it starts and ends. When it returns, save the `result` object of the Workflow tool's output to `.doug/.state/last-report.json` with the Write tool and note the run id the Workflow tool printed (`wf_...`), the way `/doug-next` step 4 does: `plan.mjs replan`, `plan.mjs done`, `board.mjs record`, and `cost.mjs <run-id>` all read those two. If the report carries `paused`, follow `/doug-next` step 4's paused paragraph: name the tasks in `next` and ask with `AskUserQuestion` whether to open the gate; yes reopens with `plan.mjs gate open <level>` and a relaunch with `resumeFromRunId`, no leaves the card In flow and stops — `plan.mjs done` refuses while the last report is paused. A paused or failed batch changes nothing in this mechanism: the paused paragraph and `plan.mjs replan` both work from the plan file, which keeps `cards` through replan, and a second pass is detected the way `/doug-next` section 2 is, by the plan's `cards` matching this batch. Otherwise read the report as `swarm-launch` says, then `plan.mjs done` and `plan.mjs land`; a report that is not ok goes back through `plan.mjs replan` and the approval gate above, or by hand if the user says so — for a batch, "by hand" dissolves it: the first card continues by hand at step 3, and every other card of the batch goes back to Ready with `board.mjs move <id> ready`.
|
|
52
52
|
|
|
@@ -84,7 +84,7 @@ Stay within the card's goal. A defect found on the way that the goal does not co
|
|
|
84
84
|
|
|
85
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):
|
|
86
86
|
|
|
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`,
|
|
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>" --summary "<one sentence>"` (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`, three 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, and, third, `--harmful <id>` once per recalled lesson the lead judges actually misled the work (a real block no longer bumps harmful). 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> --summary "<one sentence>"` (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> --summary "<one sentence>"` and put its sentence, then 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> --summary "<one sentence>" --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> --summary "<one sentence>"` per card with each card's sentence and three lines in the chat verbatim. A batch ends with one overall sentence, in plain words, of what the batch completed, after each card's own summary line.
|
|
88
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.
|
|
89
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`.
|
|
90
90
|
|
|
@@ -98,7 +98,7 @@ The lead says in the chat what it filed and where: each new card's id, title, an
|
|
|
98
98
|
|
|
99
99
|
## Cards the user asks for
|
|
100
100
|
|
|
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.
|
|
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. A card the user asks for while autopilot runs is scoped at once with the same questions, filed to Backlog, and the queue resumes at the card it was on: the request is input, not a gate, and the running queue is unchanged.
|
|
102
102
|
|
|
103
103
|
## 6. Human gate: continue
|
|
104
104
|
|