@dougharness/cli 1.0.2 → 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.
Files changed (88) hide show
  1. package/dist/bin.js +18 -3
  2. package/dist/bin.js.map +1 -1
  3. package/dist/board-serve.js +14 -7
  4. package/dist/board-serve.js.map +1 -1
  5. package/dist/board.d.ts +1 -1
  6. package/dist/board.js +42 -9
  7. package/dist/board.js.map +1 -1
  8. package/dist/detect/repo.js +14 -1
  9. package/dist/detect/repo.js.map +1 -1
  10. package/dist/detect/types.d.ts +2 -0
  11. package/dist/doctor.d.ts +46 -0
  12. package/dist/doctor.js +491 -0
  13. package/dist/doctor.js.map +1 -0
  14. package/dist/generate/agents.d.ts +7 -0
  15. package/dist/generate/agents.js +62 -12
  16. package/dist/generate/agents.js.map +1 -1
  17. package/dist/generate/claude-md.js +4 -0
  18. package/dist/generate/claude-md.js.map +1 -1
  19. package/dist/generate/config.d.ts +8 -0
  20. package/dist/generate/config.js +4 -1
  21. package/dist/generate/config.js.map +1 -1
  22. package/dist/generate/proposal.js +15 -6
  23. package/dist/generate/proposal.js.map +1 -1
  24. package/dist/generate/settings.d.ts +2 -0
  25. package/dist/generate/settings.js +17 -2
  26. package/dist/generate/settings.js.map +1 -1
  27. package/dist/init.d.ts +14 -0
  28. package/dist/init.js +94 -8
  29. package/dist/init.js.map +1 -1
  30. package/dist/memory-setup.d.ts +28 -0
  31. package/dist/memory-setup.js +326 -0
  32. package/dist/memory-setup.js.map +1 -0
  33. package/node_modules/@dougharness/flow/.claude-plugin/plugin.json +16 -4
  34. package/node_modules/@dougharness/flow/agents/lead.md +1 -1
  35. package/node_modules/@dougharness/flow/agents/planner.md +3 -3
  36. package/node_modules/@dougharness/flow/agents/researcher.md +2 -2
  37. package/node_modules/@dougharness/flow/hooks/hooks.json +16 -0
  38. package/node_modules/@dougharness/flow/lib/board.mjs +171 -13
  39. package/node_modules/@dougharness/flow/lib/code-index.mjs +18 -3
  40. package/node_modules/@dougharness/flow/lib/context.mjs +57 -0
  41. package/node_modules/@dougharness/flow/lib/cost.mjs +155 -7
  42. package/node_modules/@dougharness/flow/lib/embeddings.mjs +188 -24
  43. package/node_modules/@dougharness/flow/lib/land.mjs +74 -10
  44. package/node_modules/@dougharness/flow/lib/learn.mjs +4 -4
  45. package/node_modules/@dougharness/flow/lib/memory.mjs +368 -50
  46. package/node_modules/@dougharness/flow/lib/plan.mjs +11 -1
  47. package/node_modules/@dougharness/flow/package.json +2 -1
  48. package/node_modules/@dougharness/flow/scripts/board.mjs +86 -25
  49. package/node_modules/@dougharness/flow/scripts/context.mjs +50 -0
  50. package/node_modules/@dougharness/flow/scripts/cost.mjs +15 -3
  51. package/node_modules/@dougharness/flow/scripts/memory.mjs +197 -37
  52. package/node_modules/@dougharness/flow/scripts/plan.mjs +54 -64
  53. package/node_modules/@dougharness/flow/scripts/session-recall.mjs +132 -0
  54. package/node_modules/@dougharness/flow/skills/doug-hand/SKILL.md +105 -0
  55. package/node_modules/@dougharness/flow/skills/doug-learn/SKILL.md +1 -1
  56. package/node_modules/@dougharness/flow/skills/doug-next/SKILL.md +50 -14
  57. package/node_modules/@dougharness/flow/skills/doug-plan/SKILL.md +1 -1
  58. package/node_modules/@dougharness/flow/skills/doug-swarm/SKILL.md +7 -7
  59. package/node_modules/@dougharness/flow/skills/research/SKILL.md +4 -2
  60. package/node_modules/@dougharness/flow/skills/run-report/SKILL.md +14 -13
  61. package/node_modules/@dougharness/flow/skills/swarm-launch/SKILL.md +1 -1
  62. package/node_modules/@dougharness/flow/workflows/doug-implement.js +145 -86
  63. package/node_modules/@dougharness/gates/.claude-plugin/plugin.json +1 -1
  64. package/node_modules/@dougharness/gates/hooks/hooks.json +11 -0
  65. package/node_modules/@dougharness/gates/lib/baseline.mjs +166 -1
  66. package/node_modules/@dougharness/gates/lib/bash-rules.mjs +21 -14
  67. package/node_modules/@dougharness/gates/lib/block-ledger.mjs +58 -1
  68. package/node_modules/@dougharness/gates/lib/checkpoint.mjs +1 -1
  69. package/node_modules/@dougharness/gates/lib/config.mjs +20 -0
  70. package/node_modules/@dougharness/gates/lib/docs-gate.mjs +95 -0
  71. package/node_modules/@dougharness/gates/lib/evidence.mjs +2 -0
  72. package/node_modules/@dougharness/gates/lib/gate-lock.mjs +94 -0
  73. package/node_modules/@dougharness/gates/lib/run.mjs +4 -0
  74. package/node_modules/@dougharness/gates/lib/test-totals.mjs +40 -7
  75. package/node_modules/@dougharness/gates/lib/trace.mjs +24 -7
  76. package/node_modules/@dougharness/gates/lib/verified-tree.mjs +225 -0
  77. package/node_modules/@dougharness/gates/package.json +1 -1
  78. package/node_modules/@dougharness/gates/scripts/research-cap.mjs +133 -5
  79. package/node_modules/@dougharness/gates/scripts/stop-gate.mjs +558 -197
  80. package/node_modules/@dougharness/gates/scripts/trace.mjs +5 -1
  81. package/node_modules/@dougharness/gates/scripts/turn-start.mjs +17 -0
  82. package/node_modules/@dougharness/gates/scripts/verify.mjs +201 -0
  83. package/package.json +4 -4
  84. package/templates/board-app.js +4 -3
  85. package/node_modules/@dougharness/flow/lib/rehearse.mjs +0 -825
  86. package/node_modules/@dougharness/flow/scripts/rehearse.mjs +0 -129
  87. package/node_modules/@dougharness/flow/skills/core-next/SKILL.md +0 -92
  88. package/node_modules/@dougharness/flow/skills/harness-fix/SKILL.md +0 -85
@@ -1,11 +1,17 @@
1
1
  #!/usr/bin/env node
2
- // CLI for the development board, used by the /doug-next and /core-next skills. Deterministic; no agents.
3
- // board.mjs next [dir] [--track flow|hand] [--tag <tag>]
2
+ // CLI for the development board, used by the /doug-next and doug-hand skills. Deterministic; no agents.
3
+ // board.mjs next [dir] [--track flow|hand|any] [--tag <tag>]
4
4
  // the first Ready card on that track (default flow) whose deps are
5
5
  // Done, as JSON (exit 1 if none); --tag filters to cards carrying it
6
- // board.mjs next --batch <n> [dir] [--track flow|hand] [--tag <tag>]
6
+ // board.mjs next --batch <n> [dir] [--track flow|hand|any] [--tag <tag>]
7
7
  // the first n runnable Ready cards, in board order, as a JSON array
8
- // (exit 1 if none): the cards /doug-next plans together
8
+ // (exit 1 if none): the cards /doug-next plans together; stderr also
9
+ // carries a `wall cap: <m> min` line (the cards' budgets from the size x class table)
10
+ // board.mjs budgets [dir] [--from <live-runs file>]
11
+ // without --from, the shipped wall budget table as `table: <JSON>`;
12
+ // with --from, recomputes it (p80 per size/class cell) from the file's
13
+ // Wall clock rows joined to the board, one line per cell and a final
14
+ // `table:` line; writes nothing (replace WALL_BUDGET_MIN by hand)
9
15
  // board.mjs card <id> [dir] one card as JSON
10
16
  // board.mjs reorder <id> [dir] --before <other> | --after <other> | --top | --bottom
11
17
  // reorder a card within its column
@@ -21,26 +27,27 @@
21
27
  // flow, or named by .doug/plan.json (each with --force removes it
22
28
  // and prints a warning to stderr); prints the removed card as JSON
23
29
  // to stdout so a mistaken removal can be re-added by hand
24
- // 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>]
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>]
25
31
  // append a run entry for the card to docs/live-runs.md; every adversary block
26
32
  // in the report is classified real, marginal, or false (repeat the flag per block).
27
33
  // When .doug/plan.json lists the card among its `cards` (a batch), the entry
28
34
  // shows only that card's tasks, --cost is that card's tasks' cost, and
29
35
  // --shared-cost <usd> the shared integration agents', counted once
30
- // 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>]
31
37
  // append a hand-track entry (no report) for the card
32
38
  // --rehearsal <scenario> (either form) marks the entry as a rehearse.mjs run on the ts-basic fixture: the first
33
39
  // prose line and the commit sha (abbreviated to 7 characters) read as the fixture's, not this repository's.
34
40
  // Either record form also promotes .doug/.state/research/<id>.md to docs/research/<id>.md when that note
35
41
  // exists and differs from the docs copy, copying it and printing one line saying so; a card with no note, or
36
42
  // whose docs copy is already byte-identical, is untouched, silently.
37
- // board.mjs summary <report.json> [--cost <usd>] [--codex-cost <usd>] [--wall <text>] [--commit <sha>] [--card <id>]
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>]
38
44
  // the three-line chat summary of a report (--card: that card's tasks only)
39
45
 
40
46
  import { readFileSync } from "node:fs";
41
47
  import { relative, resolve } from "node:path";
42
- import { loadBoard, saveBoard, findCard, nextReadyCard, nextReadyCards, moveCard, editCard, removeCard, reorderCard, runEntry, handEntry, runSummary, boardPath, parseAdversaryClasses, filterReportForCard, recordLanding } from "../lib/board.mjs";
43
- import { loadPlan, unwrapReport } from "../lib/plan.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
+ import { loadPlan, unwrapReport, DEFAULT_BUDGET } from "../lib/plan.mjs";
50
+ import { wallForRun } from "../lib/cost.mjs";
44
51
 
45
52
  const EDIT_FIELD_ORDER = ["title", "goal", "size", "component", "track", "class", "tags", "deps", "source"];
46
53
  const EDIT_USAGE = "usage: board.mjs edit <id> [dir] [--title <t>] [--goal <g> | --goal-file <path>] [--size S|M|L] [--component <c>] [--track flow|hand] [--class <c>] [--tag a,b] [--deps a,b] [--source <s>] [--force]";
@@ -58,10 +65,11 @@ const SUBCOMMAND_FLAGS = {
58
65
  edit: ["title", "goal", "goal-file", "size", "component", "track", "class", "deps", "tag", "source", "force"],
59
66
  remove: ["force"],
60
67
  reorder: ["before", "after", "top", "bottom"],
61
- summary: ["cost", "codex-cost", "wall", "commit", "card"],
68
+ budgets: ["from"],
69
+ summary: ["summary", "cost", "codex-cost", "wall", "commit", "card", "run", "claude-dir"],
62
70
  };
63
- const RECORD_HAND_FLAGS = ["hand", "commit", "wall", "gate", "note", "rehearsal"];
64
- const RECORD_REPORT_FLAGS = ["cost", "codex-cost", "wall", "commit", "adversary", "shared-cost", "note", "rehearsal"];
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"];
65
73
  const opts = {};
66
74
  const positional = [];
67
75
  for (let i = 0; i < argv.length; i++) {
@@ -86,16 +94,17 @@ function fail(msg, code = 1) {
86
94
  }
87
95
 
88
96
  const USAGE_FOR = {
89
- next: "usage: board.mjs next [dir] [--track flow|hand] [--batch <n>] [--tag <tag>]",
97
+ next: "usage: board.mjs next [dir] [--track flow|hand|any] [--batch <n>] [--tag <tag>]",
90
98
  card: "usage: board.mjs card <id> [dir]",
91
99
  move: "usage: board.mjs move <id> <column> [dir] [--source <text>]",
92
100
  edit: EDIT_USAGE,
93
101
  remove: "usage: board.mjs remove <id> [dir] [--force]",
94
102
  reorder: "usage: board.mjs reorder <id> [dir] --before <other> | --after <other> | --top | --bottom",
95
- summary: "usage: board.mjs summary <report.json> [--cost <usd>] [--codex-cost <usd>] [--wall <text>] [--commit <sha>] [--card <id>]",
103
+ budgets: "usage: board.mjs budgets [dir] [--from <live-runs file>]",
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>]",
96
105
  };
97
- const RECORD_HAND_USAGE = "usage: board.mjs record <id> --hand [dir] [--commit <sha>] [--wall <text>] [--gate <text>] [--note <text>] [--rehearsal <scenario>]";
98
- 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>]";
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>]";
99
108
 
100
109
  // An unknown --flag for the given subcommand: exit 2, name the flag and the subcommand's usage.
101
110
  // Runs on flag names only (parse order doesn't matter), before any work or write.
@@ -119,22 +128,60 @@ if (cmd === "record" || Object.prototype.hasOwnProperty.call(SUBCOMMAND_FLAGS, c
119
128
  }
120
129
  }
121
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
+
122
142
  try {
123
143
  switch (cmd) {
124
144
  case "next": {
125
145
  const dir = dirFrom(rest, 0);
126
- const track = opts.track === "hand" ? "hand" : "flow";
127
- if (opts.track !== undefined && opts.track !== "flow" && opts.track !== "hand") fail(`unknown track "${opts.track}"; tracks are flow, hand`, 2);
146
+ const track = opts.track === "hand" ? "hand" : opts.track === "any" ? "any" : "flow";
147
+ if (opts.track !== undefined && opts.track !== "flow" && opts.track !== "hand" && opts.track !== "any") fail(`unknown track "${opts.track}"; tracks are flow, hand, any`, 2);
128
148
  const batch = opts.batch !== undefined ? Number(opts.batch) : null;
129
149
  if (batch !== null && !(Number.isInteger(batch) && batch > 0)) fail(`--batch must be a positive integer, got ${JSON.stringify(opts.batch)}`, 2);
130
150
  if (opts.tag !== undefined && opts.tag.includes(",")) fail(`--tag takes one tag here; got "${opts.tag}"`, 2);
131
151
  const board = loadBoard(dir);
132
152
  const { card, cards, skipped } = batch === null ? nextReadyCard(board, { track, tag: opts.tag }) : nextReadyCards(board, { track, batch, tag: opts.tag });
133
- for (const s of skipped) process.stderr.write(s.hand ? `skipping ${s.id}: hand track (built by hand with /core-next, not by doug-next)\n` : s.flow ? `skipping ${s.id}: flow track (run it with /doug-next)\n` : `skipping ${s.id}: waiting on ${s.waitingOn.join(", ")}\n`);
153
+ for (const s of skipped) process.stderr.write(s.hand ? `skipping ${s.id}: hand track (a by-hand card; /doug-next ${s.id} takes it)\n` : s.flow ? `skipping ${s.id}: flow track (run it with /doug-next)\n` : `skipping ${s.id}: waiting on ${s.waitingOn.join(", ")}\n`);
154
+ const capLine = readyCapWarning(board);
155
+ if (capLine) process.stderr.write(capLine);
134
156
  if (batch === null ? !card : !cards.length) fail(track === "hand" ? "No Ready hand-track card whose dependencies are Done." : "No Ready card whose dependencies are Done.");
157
+ if (batch !== null) process.stderr.write(`wall cap: ${wallCapMinutes(cards)} min (size x class table at p80, board.mjs budgets; no size counts as M)\n`);
135
158
  process.stdout.write(JSON.stringify(batch === null ? card : cards, null, 2) + "\n");
136
159
  break;
137
160
  }
161
+ case "budgets": {
162
+ if (opts.from === undefined) {
163
+ process.stdout.write(`table: ${JSON.stringify(WALL_BUDGET_MIN)}\n`);
164
+ break;
165
+ }
166
+ const dir = dirFrom(rest, 0);
167
+ let text;
168
+ try {
169
+ text = readFileSync(resolve(opts.from), "utf8");
170
+ } catch (e) {
171
+ fail(`cannot read --from file ${opts.from}: ${e.message}`, 2);
172
+ }
173
+ const { table, cells } = wallBudgetTable(parseLiveRunWalls(text), loadBoard(dir));
174
+ const out = [];
175
+ for (const size of ["S", "M", "L"]) {
176
+ for (const c of cells.filter((x) => x.size === size)) {
177
+ if (c.cls === "all") out.push(c.n === 0 ? `${size}/all n=0 (no landings; keeps the shipped ${WALL_BUDGET_MIN[size].all})` : `${size}/all n=${c.n} p80 ${c.p80}`);
178
+ else out.push(`${size}/${c.cls} n=${c.n} p80 ${c.p80}${c.n < WALL_BUDGET_MIN_N ? ` (fewer than ${WALL_BUDGET_MIN_N}; falls back to ${size}/all ${table[size].all})` : ""}`);
179
+ }
180
+ }
181
+ out.push(`table: ${JSON.stringify(table)}`);
182
+ process.stdout.write(out.join("\n") + "\n");
183
+ break;
184
+ }
138
185
  case "card": {
139
186
  const dir = dirFrom(rest, 1);
140
187
  process.stdout.write(JSON.stringify(findCard(loadBoard(dir), rest[0]), null, 2) + "\n");
@@ -144,8 +191,11 @@ try {
144
191
  const [id, column] = rest;
145
192
  if (!id || !column) fail("usage: board.mjs move <id> <column> [dir] [--source <text>]", 2);
146
193
  const dir = dirFrom(rest, 2);
147
- saveBoard(dir, moveCard(loadBoard(dir), id, column, { source: opts.source }));
194
+ const moved = moveCard(loadBoard(dir), id, column, { source: opts.source });
195
+ saveBoard(dir, moved);
148
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);
149
199
  break;
150
200
  }
151
201
  case "edit": {
@@ -216,17 +266,22 @@ try {
216
266
  case "record": {
217
267
  if (opts.hand) {
218
268
  const [id] = rest;
219
- 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);
220
270
  const dir = dirFrom(rest, 1);
221
271
  const card = findCard(loadBoard(dir), id);
222
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 });
223
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
+ }
224
279
  if (promoted) process.stdout.write(`Promoted the research note for ${id} to ${promoted}.\n`);
225
280
  process.stdout.write(`Appended a hand-track entry for ${id} to ${file}.\n`);
226
281
  break;
227
282
  }
228
283
  const [id, reportFile] = rest;
229
- 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>]", 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);
230
285
  const dir = dirFrom(rest, 2);
231
286
  const card = findCard(loadBoard(dir), id);
232
287
  const report = unwrapReport(JSON.parse(readFileSync(resolve(reportFile), "utf8")));
@@ -234,22 +289,28 @@ try {
234
289
  const plan = loadPlan(dir);
235
290
  const batch = plan && Array.isArray(plan.cards) && plan.cards.includes(id) ? plan.cards : null;
236
291
  const taskIds = batch ? plan.tasks.filter((t) => t.card === id).map((t) => t.id) : [];
237
- const entry = runEntry({ card, report, 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 });
292
+ const wall = opts.run ? wallForRun({ runId: opts.run, claudeDir: opts["claude-dir"], projectDir: dir, wallMinutes: report.budget?.wallMinutes ?? DEFAULT_BUDGET.wallMinutes }) : null;
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 });
238
294
  const { file, promoted } = recordLanding(dir, id, entry);
295
+ process.stdout.write(`${summarySentence}\n`);
239
296
  if (promoted) process.stdout.write(`Promoted the research note for ${id} to ${promoted}.\n`);
240
297
  process.stdout.write(`Appended a run entry for ${id} to ${file}.\n`);
241
298
  break;
242
299
  }
243
300
  case "summary": {
244
301
  const [reportFile] = rest;
245
- if (!reportFile) fail("usage: board.mjs summary <report.json> [--cost <usd>] [--codex-cost <usd>] [--wall <text>] [--commit <sha>] [--card <id>]", 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);
246
303
  let report = unwrapReport(JSON.parse(readFileSync(resolve(reportFile), "utf8")));
247
304
  if (opts.card !== undefined) {
248
305
  // With the plan at hand the filter also covers a report whose tasks carry no card.
249
306
  const plan = loadPlan(dirFrom(rest, 1));
250
307
  report = filterReportForCard(report, opts.card, plan && Array.isArray(plan.tasks) ? plan.tasks.filter((t) => t.card === opts.card).map((t) => t.id) : []);
251
308
  }
252
- process.stdout.write(runSummary({ report, 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 }));
309
+ const wall = opts.run ? wallForRun({ runId: opts.run, claudeDir: opts["claude-dir"], projectDir: process.cwd(), wallMinutes: report.budget?.wallMinutes ?? DEFAULT_BUDGET.wallMinutes }) : null;
310
+ const sumPlan = opts.card !== undefined ? loadPlan(dirFrom(rest, 1)) : null;
311
+ const sumBatch = sumPlan && Array.isArray(sumPlan.cards) && sumPlan.cards.includes(opts.card) ? sumPlan.cards : null;
312
+ process.stdout.write(`${summarySentence}\n`);
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 }));
253
314
  break;
254
315
  }
255
316
  default:
@@ -0,0 +1,50 @@
1
+ #!/usr/bin/env node
2
+ // The lead's own context percentage (card autopilot-unlimited), read from the session transcript.
3
+ // context.mjs [dir] [--session <id|latest>] [--claude-dir <path>]
4
+ // dir the project directory (default cwd); <dir>/.doug/config.json supplies autopilot.contextThreshold
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.
8
+ // --claude-dir where ~/.claude lives (tests point it at a fixture)
9
+ // Prints one line and exits 0; exits 1 with a stderr message when there is no transcript or no usage line.
10
+ import { readFileSync } from "node:fs";
11
+ import { resolve, join } from "node:path";
12
+ import { sessionTranscriptPath } from "../lib/cost.mjs";
13
+ import { atThreshold, contextReading, contextThreshold } from "../lib/context.mjs";
14
+
15
+ const argv = process.argv.slice(2);
16
+ const opts = {};
17
+ const positional = [];
18
+ for (let i = 0; i < argv.length; i++) {
19
+ if (!argv[i].startsWith("--")) {
20
+ positional.push(argv[i]);
21
+ continue;
22
+ }
23
+ opts[argv[i].slice(2)] = argv[i + 1] ?? "";
24
+ i++;
25
+ }
26
+
27
+ const dir = resolve(positional[0] || process.cwd());
28
+ let config = {};
29
+ try {
30
+ config = JSON.parse(readFileSync(join(dir, ".doug/config.json"), "utf8"));
31
+ } catch {
32
+ // absent or unreadable: defaults
33
+ }
34
+ const threshold = contextThreshold(config, (m) => process.stderr.write(`[doug] ${m}\n`));
35
+
36
+ let file;
37
+ try {
38
+ file = sessionTranscriptPath({ session: opts.session || process.env.CLAUDE_CODE_SESSION_ID || "latest", ...(opts["claude-dir"] ? { claudeDir: resolve(opts["claude-dir"]) } : {}), projectDir: dir });
39
+ } catch (err) {
40
+ process.stderr.write(`context: ${err.message}\n`);
41
+ process.exit(1);
42
+ }
43
+ const r = contextReading(file);
44
+ if (!r) {
45
+ process.stderr.write(`context: no usage line in ${file}\n`);
46
+ process.exit(1);
47
+ }
48
+ process.stdout.write(
49
+ `context: ${r.pct}% (${r.tokens} of ${r.window} tokens, ${r.model}); threshold ${threshold}% (autopilot.contextThreshold) -> ${atThreshold(r.pct, threshold) ? "stop" : "continue"}\n`,
50
+ );
@@ -1,5 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
  // Real cost of one doug-implement run from the local Claude Code transcripts; no network, nothing estimated.
3
+ // cost.mjs --session <id|latest> [dir] [--json] [--claude-dir <path>]
4
+ // --session price one Claude Code session: its transcript plus every subagent transcript under it
5
+ // (latest = the most recently modified one in the project); exit 1 when none is found
3
6
  // cost.mjs <run-id> [dir] [--json] [--claude-dir <path>]
4
7
  // <run-id> the Workflow run id (wf_...)
5
8
  // [dir] the project directory whose ~/.claude/projects slug is searched first (default cwd)
@@ -15,7 +18,7 @@
15
18
  // with the run total and any unpriced agent. An unreadable/invalid default report is ignored, not a usage error.
16
19
  import { readFileSync } from "node:fs";
17
20
  import { resolve, join } from "node:path";
18
- import { costRun, renderCost, costByCard, renderCostByCard } from "../lib/cost.mjs";
21
+ import { costRun, costSession, renderSession, renderCost, costByCard, renderCostByCard } from "../lib/cost.mjs";
19
22
  import { loadPlan, REPORT_RELPATH, unwrapReport } from "../lib/plan.mjs";
20
23
 
21
24
  const argv = process.argv.slice(2);
@@ -35,13 +38,22 @@ for (let i = 0; i < argv.length; i++) {
35
38
  opts[name] = argv[i + 1] ?? "";
36
39
  i++;
37
40
  }
38
- const [runId, dirArg] = positional;
39
- if (!runId) {
41
+ const [runId, dirArg] = opts.session !== undefined ? [null, positional[0]] : positional;
42
+ if (opts.session === "") {
43
+ process.stderr.write("usage: cost.mjs --session <id|latest> [dir] [--json] [--claude-dir <path>]\n");
44
+ process.exit(2);
45
+ }
46
+ if (!runId && opts.session === undefined) {
40
47
  process.stderr.write("usage: cost.mjs <run-id> [dir] [--json] [--claude-dir <path>]\n");
41
48
  process.exit(2);
42
49
  }
43
50
  try {
44
51
  const projectDir = resolve(process.env.CLAUDE_PROJECT_DIR || dirArg || process.cwd());
52
+ if (opts.session !== undefined) {
53
+ const result = costSession({ session: opts.session, projectDir, claudeDir: opts["claude-dir"] ? resolve(opts["claude-dir"]) : undefined });
54
+ process.stdout.write(opts.json ? JSON.stringify(result, null, 2) + "\n" : renderSession(result));
55
+ process.exit(0);
56
+ }
45
57
  let reportPath = opts.report ? resolve(opts.report) : join(projectDir, REPORT_RELPATH);
46
58
  let report = null;
47
59
  if (opts.report) {