@dougharness/cli 1.3.0 → 1.5.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/ablate.js +2 -2
- package/dist/ablate.js.map +1 -1
- package/dist/bin.js +13 -4
- package/dist/bin.js.map +1 -1
- package/dist/board-page.d.ts +5 -1
- package/dist/board-page.js +19 -5
- package/dist/board-page.js.map +1 -1
- package/dist/board-serve.js +34 -10
- package/dist/board-serve.js.map +1 -1
- package/dist/board.d.ts +1 -1
- package/dist/board.js +160 -13
- package/dist/board.js.map +1 -1
- package/node_modules/@dougharness/flow/.claude-plugin/plugin.json +1 -1
- package/node_modules/@dougharness/flow/agents/planner.md +1 -0
- package/node_modules/@dougharness/flow/hooks/register.tsx +18 -2
- package/node_modules/@dougharness/flow/lib/autopilot-core.mjs +94 -7
- package/node_modules/@dougharness/flow/lib/autopilot-view.mjs +31 -4
- package/node_modules/@dougharness/flow/lib/board.mjs +375 -30
- package/node_modules/@dougharness/flow/lib/card-loop.mjs +237 -24
- package/node_modules/@dougharness/flow/lib/cost.mjs +9 -0
- package/node_modules/@dougharness/flow/lib/decisions.mjs +2 -4
- package/node_modules/@dougharness/flow/lib/learn.mjs +95 -54
- package/node_modules/@dougharness/flow/lib/memory.mjs +1403 -44
- package/node_modules/@dougharness/flow/lib/plan.mjs +40 -0
- package/node_modules/@dougharness/flow/package.json +1 -1
- package/node_modules/@dougharness/flow/scripts/autopilot.mjs +355 -13
- package/node_modules/@dougharness/flow/scripts/board.mjs +160 -27
- package/node_modules/@dougharness/flow/scripts/cost.mjs +6 -3
- package/node_modules/@dougharness/flow/scripts/learn.mjs +1 -1
- package/node_modules/@dougharness/flow/scripts/memory.mjs +668 -39
- package/node_modules/@dougharness/flow/scripts/plan.mjs +3 -3
- package/node_modules/@dougharness/flow/scripts/session-recall.mjs +11 -4
- package/node_modules/@dougharness/flow/skills/doug-hand/SKILL.md +7 -3
- package/node_modules/@dougharness/flow/skills/doug-learn/SKILL.md +1 -1
- package/node_modules/@dougharness/flow/skills/doug-next/SKILL.md +11 -2
- package/node_modules/@dougharness/flow/skills/doug-start/SKILL.md +9 -8
- package/node_modules/@dougharness/flow/skills/doug-swarm/SKILL.md +1 -1
- package/node_modules/@dougharness/flow/skills/research/SKILL.md +1 -1
- package/node_modules/@dougharness/flow/skills/run-report/SKILL.md +3 -3
- package/node_modules/@dougharness/flow/workflows/card-queen.js +238 -25
- package/node_modules/@dougharness/flow/workflows/doug-implement.js +55 -37
- package/node_modules/@dougharness/gates/.claude-plugin/plugin.json +1 -1
- package/node_modules/@dougharness/gates/lib/baseline.mjs +32 -0
- package/node_modules/@dougharness/gates/lib/block-ledger.mjs +45 -5
- package/node_modules/@dougharness/gates/lib/docs-gate.mjs +6 -2
- package/node_modules/@dougharness/gates/lib/verified-tree.mjs +33 -0
- package/node_modules/@dougharness/gates/package.json +1 -1
- package/node_modules/@dougharness/gates/scripts/stop-gate.mjs +130 -42
- package/node_modules/@dougharness/gates/scripts/verify.mjs +20 -4
- package/package.json +3 -3
- package/templates/board-app.js +240 -30
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
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>]
|
|
3
|
+
// board.mjs next [dir] [--track flow|hand|any] [--tag <tag>] [--epic <id>] [--milestone <id>]
|
|
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|any] [--tag <tag>]
|
|
6
|
+
// board.mjs next --batch <n> [dir] [--track flow|hand|any] [--tag <tag>] [--epic <id>] [--milestone <id>]
|
|
7
7
|
// the first n runnable Ready cards, in board order, as a JSON array
|
|
8
8
|
// (exit 1 if none): the cards /doug-next plans together; stderr also
|
|
9
9
|
// carries a `wall cap: <m> min` line (the cards' budgets from the size x class table)
|
|
@@ -14,28 +14,37 @@
|
|
|
14
14
|
// Wall clock rows joined to the board, one line per cell and a final
|
|
15
15
|
// `table:` line; writes nothing (replace WALL_BUDGET_MIN by hand). --usd does the same for the usd
|
|
16
16
|
// table (USD_BUDGET) from the Cost rows of flow and hand entries
|
|
17
|
+
// board.mjs rollup [dir] [--epic <id> | --milestone <id>] [--from <live-runs file>]
|
|
18
|
+
// per epic and milestone: columns, done/total, size-weighted progress,
|
|
19
|
+
// measured cost and wall of Done cards, not-Done budget, as JSON; --from
|
|
20
|
+
// defaults to docs/live-runs.md when present (else measured is null)
|
|
21
|
+
// board.mjs labels --check [dir] one line per label colour under 3:1 against #ffffff or #1c2022; exit 0
|
|
17
22
|
// board.mjs card <id> [dir] one card as JSON
|
|
18
23
|
// board.mjs reorder <id> [dir] --before <other> | --after <other> | --top | --bottom
|
|
19
24
|
// reorder a card within its column
|
|
20
25
|
// board.mjs move <id> <column> [dir] [--source <text>]
|
|
21
26
|
// move a card
|
|
22
27
|
// board.mjs edit <id> [dir] [--title <t>] [--goal <g> | --goal-file <path>] [--size S|M|L] [--component <c>]
|
|
23
|
-
// [--track flow|hand] [--class <c>] [--deps a,b] [--tag a,b] [--source <s>] [--force]
|
|
28
|
+
// [--track flow|hand] [--class <c>] [--deps a,b] [--tag a,b] [--source <s>] [--epic <id>] [--milestone <id>] [--force]
|
|
24
29
|
// changes a card's fields through the record's own validation
|
|
25
30
|
// (never the id or the column; move stays the way to change that);
|
|
26
|
-
// --tag "" clears a card's tags
|
|
31
|
+
// --tag "" clears a card's tags; --epic "" / --milestone "" clear those
|
|
32
|
+
// board.mjs epic add <id> --title <t> --goal <g> [dir] [--version <v>] [--date YYYY-MM-DD]
|
|
33
|
+
// board.mjs epic edit <id> [dir] [--title <t>] [--goal <g>] [--version <v>] [--date <d>] [--status planned|active|done] [--force]
|
|
34
|
+
// board.mjs epic list [dir] epics as a JSON array; milestone add|edit|list takes the same flags.
|
|
35
|
+
// --status done is refused while a member card is not Done (--force sets it, with a warning)
|
|
27
36
|
// board.mjs remove <id> [dir] [--force] removes a card from the record: refused always when another card
|
|
28
37
|
// lists it in deps; refused without --force when it is done, in
|
|
29
38
|
// flow, or named by .doug/plan.json (each with --force removes it
|
|
30
39
|
// and prints a warning to stderr); prints the removed card as JSON
|
|
31
40
|
// to stdout so a mistaken removal can be re-added by hand
|
|
32
|
-
// 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> --rehearsal-outcome <passed|failed>] [--run <run-id>] [--claude-dir <dir>]
|
|
41
|
+
// 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> --rehearsal-outcome <passed|failed> [--rehearsal-fixture <name>]] [--run <run-id>] [--claude-dir <dir>]
|
|
33
42
|
// append a run entry for the card to docs/live-runs.md; every adversary block
|
|
34
43
|
// in the report is classified real, marginal, or false (repeat the flag per block).
|
|
35
44
|
// When .doug/plan.json lists the card among its `cards` (a batch), the entry
|
|
36
45
|
// shows only that card's tasks, --cost is that card's tasks' cost, and
|
|
37
46
|
// --shared-cost <usd> the shared integration agents', counted once
|
|
38
|
-
// board.mjs record <id> --hand [dir] --summary "<one sentence>" [--commit <sha>] [--wall <text>] [--gate <text>] [--note <text>] [--rehearsal <scenario> --rehearsal-outcome <passed|failed>] [--cost <usd>]
|
|
47
|
+
// board.mjs record <id> --hand [dir] --summary "<one sentence>" [--commit <sha>] [--wall <text>] [--gate <text>] [--note <text>] [--rehearsal <scenario> --rehearsal-outcome <passed|failed> [--rehearsal-fixture <name>]] [--cost <usd>]
|
|
39
48
|
// append a hand-track entry (no report) for the card
|
|
40
49
|
// --rehearsal <scenario> (either form) marks the entry as a rehearse.mjs run on the ts-basic fixture (the start scenario: an empty git repo): the first
|
|
41
50
|
// prose line and the commit sha (abbreviated to 7 characters) read as the fixture's, not this repository's.
|
|
@@ -49,31 +58,44 @@
|
|
|
49
58
|
|
|
50
59
|
import { readFileSync, writeFileSync, mkdirSync, existsSync } from "node:fs";
|
|
51
60
|
import { join, relative, resolve } from "node:path";
|
|
52
|
-
import { loadBoard, saveBoard, findCard, nextReadyCard, nextReadyCards, moveCard, editCard, removeCard, reorderCard, readyCapWarning, wallCapMinutes, WALL_BUDGET_MIN, WALL_BUDGET_MIN_N, parseLiveRunWalls, wallBudgetTable, USD_BUDGET, USD_BUDGET_MIN_N, usdCapDollars, parseLiveRunCosts, usdBudgetTable, runEntry, handEntry, handWallVerdict, runSummary, boardPath, parseAdversaryClasses, filterReportForCard, recordLanding } from "../lib/board.mjs";
|
|
61
|
+
import { loadBoard, saveBoard, findCard, nextReadyCard, nextReadyCards, moveCard, editCard, editEpic, editMilestone, initLabelGroups, addEpic, addMilestone, removeCard, reorderCard, readyCapWarning, wallCapMinutes, WALL_BUDGET_MIN, WALL_BUDGET_MIN_N, parseLiveRunWalls, wallBudgetTable, USD_BUDGET, USD_BUDGET_MIN_N, usdCapDollars, parseLiveRunCosts, usdBudgetTable, rollup, checkFilters, labelContrastFailures, LIVE_RUNS_RELPATH, runEntry, handEntry, handWallVerdict, runSummary, boardPath, parseAdversaryClasses, filterReportForCard, recordLanding } from "../lib/board.mjs";
|
|
53
62
|
import { loadPlan, planFor, unwrapReport, DEFAULT_BUDGET } from "../lib/plan.mjs";
|
|
54
63
|
import { wallForRun } from "../lib/cost.mjs";
|
|
55
64
|
|
|
56
|
-
const EDIT_FIELD_ORDER = ["title", "goal", "size", "component", "track", "class", "tags", "deps", "source"];
|
|
57
|
-
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]";
|
|
65
|
+
const EDIT_FIELD_ORDER = ["title", "goal", "size", "component", "track", "class", "tags", "deps", "source", "epic", "milestone"];
|
|
66
|
+
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>] [--epic <id>] [--milestone <id>] [--force]";
|
|
58
67
|
|
|
59
68
|
const argv = process.argv.slice(2);
|
|
60
|
-
const FLAGS = new Set(["hand", "top", "bottom", "force", "usd"]);
|
|
69
|
+
const FLAGS = new Set(["hand", "top", "bottom", "force", "usd", "check"]);
|
|
61
70
|
const REPEATABLE = new Set(["adversary"]);
|
|
62
71
|
|
|
63
72
|
// Each subcommand's allowed flags, derived from what that case actually reads (card board-cli-unknown-flags).
|
|
64
73
|
// `record` has two forms (--hand or not) with different allowed sets, handled separately below.
|
|
65
74
|
const SUBCOMMAND_FLAGS = {
|
|
66
|
-
next: ["track", "batch", "tag"],
|
|
75
|
+
next: ["track", "batch", "tag", "epic", "milestone"],
|
|
67
76
|
card: [],
|
|
68
77
|
move: ["source"],
|
|
69
|
-
edit: ["title", "goal", "goal-file", "size", "component", "track", "class", "deps", "tag", "source", "force"],
|
|
78
|
+
edit: ["title", "goal", "goal-file", "size", "component", "track", "class", "deps", "tag", "source", "epic", "milestone", "force"],
|
|
70
79
|
remove: ["force"],
|
|
71
80
|
reorder: ["before", "after", "top", "bottom"],
|
|
72
81
|
budgets: ["from", "usd"],
|
|
82
|
+
rollup: ["epic", "milestone", "from"],
|
|
83
|
+
labels: ["check"],
|
|
73
84
|
summary: ["summary", "cost", "codex-cost", "wall", "commit", "card", "run", "claude-dir", "note"],
|
|
74
85
|
};
|
|
75
|
-
|
|
76
|
-
const
|
|
86
|
+
// epic and milestone: `<kind> add|edit|list`, each action with its own allowed flags.
|
|
87
|
+
const PLAN_ACTION_FLAGS = {
|
|
88
|
+
add: ["title", "goal", "version", "date"],
|
|
89
|
+
edit: ["title", "goal", "version", "date", "status", "force"],
|
|
90
|
+
list: [],
|
|
91
|
+
};
|
|
92
|
+
const planUsage = (kind) => [
|
|
93
|
+
`usage: board.mjs ${kind} add <id> --title <t> --goal <g> [dir] [--version <v>] [--date YYYY-MM-DD]`,
|
|
94
|
+
` board.mjs ${kind} edit <id> [dir] [--title <t>] [--goal <g>] [--version <v>] [--date YYYY-MM-DD] [--status planned|active|done] [--force]`,
|
|
95
|
+
` board.mjs ${kind} list [dir]`,
|
|
96
|
+
].join("\n");
|
|
97
|
+
const RECORD_HAND_FLAGS = ["summary", "hand", "commit", "wall", "gate", "note", "rehearsal", "rehearsal-outcome", "rehearsal-fixture", "cost"];
|
|
98
|
+
const RECORD_REPORT_FLAGS = ["summary", "cost", "codex-cost", "wall", "commit", "adversary", "shared-cost", "note", "rehearsal", "rehearsal-outcome", "rehearsal-fixture", "run", "claude-dir"];
|
|
77
99
|
const opts = {};
|
|
78
100
|
const positional = [];
|
|
79
101
|
for (let i = 0; i < argv.length; i++) {
|
|
@@ -98,17 +120,19 @@ function fail(msg, code = 1) {
|
|
|
98
120
|
}
|
|
99
121
|
|
|
100
122
|
const USAGE_FOR = {
|
|
101
|
-
next: "usage: board.mjs next [dir] [--track flow|hand|any] [--batch <n>] [--tag <tag>]",
|
|
123
|
+
next: "usage: board.mjs next [dir] [--track flow|hand|any] [--batch <n>] [--tag <tag>] [--epic <id>] [--milestone <id>]",
|
|
102
124
|
card: "usage: board.mjs card <id> [dir]",
|
|
103
125
|
move: "usage: board.mjs move <id> <column> [dir] [--source <text>]",
|
|
104
126
|
edit: EDIT_USAGE,
|
|
105
127
|
remove: "usage: board.mjs remove <id> [dir] [--force]",
|
|
106
128
|
reorder: "usage: board.mjs reorder <id> [dir] --before <other> | --after <other> | --top | --bottom",
|
|
107
129
|
budgets: "usage: board.mjs budgets [dir] [--usd] [--from <live-runs file>]",
|
|
130
|
+
rollup: "usage: board.mjs rollup [dir] [--epic <id> | --milestone <id>] [--from <live-runs file>]",
|
|
131
|
+
labels: "usage: board.mjs labels --check [dir] | labels init [dir]",
|
|
108
132
|
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>] [--note <text>]",
|
|
109
133
|
};
|
|
110
|
-
const RECORD_HAND_USAGE = "usage: board.mjs record <id> --hand [dir] --summary \"<one sentence>\" [--commit <sha>] [--wall <text>] [--gate <text>] [--note <text>] [--rehearsal <scenario> --rehearsal-outcome <passed|failed>] [--cost <usd>]";
|
|
111
|
-
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> --rehearsal-outcome <passed|failed>] [--run <run-id>] [--claude-dir <dir>]";
|
|
134
|
+
const RECORD_HAND_USAGE = "usage: board.mjs record <id> --hand [dir] --summary \"<one sentence>\" [--commit <sha>] [--wall <text>] [--gate <text>] [--note <text>] [--rehearsal <scenario> --rehearsal-outcome <passed|failed> [--rehearsal-fixture <name>]] [--cost <usd>]";
|
|
135
|
+
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> --rehearsal-outcome <passed|failed> [--rehearsal-fixture <name>]] [--run <run-id>] [--claude-dir <dir>]";
|
|
112
136
|
|
|
113
137
|
// An unknown --flag for the given subcommand: exit 2, name the flag and the subcommand's usage.
|
|
114
138
|
// Runs on flag names only (parse order doesn't matter), before any work or write.
|
|
@@ -124,6 +148,12 @@ function dirFrom(args, n) {
|
|
|
124
148
|
return resolve(process.env.CLAUDE_PROJECT_DIR || args[n] || process.cwd());
|
|
125
149
|
}
|
|
126
150
|
|
|
151
|
+
if (cmd === "epic" || cmd === "milestone") {
|
|
152
|
+
const action = rest[0];
|
|
153
|
+
if (!Object.prototype.hasOwnProperty.call(PLAN_ACTION_FLAGS, action)) fail(planUsage(cmd), 2);
|
|
154
|
+
const unknown = Object.keys(opts).find((name) => !PLAN_ACTION_FLAGS[action].includes(name));
|
|
155
|
+
if (unknown !== undefined) fail(`unknown option --${unknown} for board.mjs ${cmd}\n${planUsage(cmd)}`, 2);
|
|
156
|
+
}
|
|
127
157
|
if (cmd === "record" || Object.prototype.hasOwnProperty.call(SUBCOMMAND_FLAGS, cmd)) {
|
|
128
158
|
const unknown = unknownFlagFor(cmd, opts);
|
|
129
159
|
if (unknown !== undefined) {
|
|
@@ -147,6 +177,7 @@ if (cmd === "record" || cmd === "summary") {
|
|
|
147
177
|
if (cmd === "record") {
|
|
148
178
|
const ro = opts["rehearsal-outcome"];
|
|
149
179
|
if (opts.rehearsal !== undefined && ro !== "passed" && ro !== "failed") fail(`--rehearsal needs --rehearsal-outcome passed|failed, got ${ro === undefined ? "none" : JSON.stringify(ro)}\n${opts.hand ? RECORD_HAND_USAGE : RECORD_REPORT_USAGE}`, 2);
|
|
180
|
+
if (opts.rehearsal === undefined && opts["rehearsal-fixture"] !== undefined) fail(`--rehearsal-fixture is only valid with --rehearsal\n${opts.hand ? RECORD_HAND_USAGE : RECORD_REPORT_USAGE}`, 2);
|
|
150
181
|
if (opts.rehearsal === undefined && ro !== undefined) fail(`--rehearsal-outcome is only valid with --rehearsal\n${opts.hand ? RECORD_HAND_USAGE : RECORD_REPORT_USAGE}`, 2);
|
|
151
182
|
}
|
|
152
183
|
|
|
@@ -162,7 +193,8 @@ function landingBlock({ id, ok, commit, wall, gate, cost, codexCost, note, rehea
|
|
|
162
193
|
const learned = typeof note === "string" && note.trim() ? note.trim().replace(/\s+/g, " ") : "nothing new";
|
|
163
194
|
const lines = [`Changed: ${summarySentence}`, `Outcome: ${outcome}`, `Learned: ${learned}`];
|
|
164
195
|
const words = lines.join("\n").split(/\s+/).filter(Boolean).length;
|
|
165
|
-
|
|
196
|
+
// A rehearsal's block is print-only (no landing-block files, so the Stop gate checks nothing): no cap, so a failed stage's long note still records.
|
|
197
|
+
if (!rehearsal && words > LANDING_BLOCK_WORDS) fail(`landing block is ${words} words; the limit is ${LANDING_BLOCK_WORDS}. Shorten --summary or --note.`);
|
|
166
198
|
return lines;
|
|
167
199
|
}
|
|
168
200
|
// The pending marker holds the session that landed, so only that session's Stop checks it (empty when unset).
|
|
@@ -182,6 +214,25 @@ function writeLandingBlock(dir, id, lines, keepFile = false, printOnly = false)
|
|
|
182
214
|
}
|
|
183
215
|
process.stdout.write(lines.join("\n") + "\n");
|
|
184
216
|
}
|
|
217
|
+
// A plan landed after `plan.mjs done --force` (landed set, report not ok, --commit naming the land merge or absent,
|
|
218
|
+
// not a rehearsal) is a landed card whatever the report says; its gate is land's own acceptance. In a batch plan
|
|
219
|
+
// only the entries tagged `[<card>] ` count, or the whole list labelled as the plan's when none carries the tag.
|
|
220
|
+
function forcedLanding({ plan, report, opts, card }) {
|
|
221
|
+
const lc = plan && plan.landed && typeof plan.landed === "object" ? plan.landed : null;
|
|
222
|
+
const lm = lc && typeof lc.mergeCommit === "string" ? lc.mergeCommit : "";
|
|
223
|
+
const forced = report.ok !== true && !opts.rehearsal && lc !== null && (opts.commit === undefined || (lm !== "" && (lm.startsWith(String(opts.commit)) || String(opts.commit).startsWith(lm))));
|
|
224
|
+
if (!forced) return { forced: false, gate: null };
|
|
225
|
+
if (!Array.isArray(lc.acceptance)) return { forced, gate: "acceptance checked at land" };
|
|
226
|
+
const count = (list) => `${list.filter((a) => a && a.ok).length}/${list.length}`;
|
|
227
|
+
if (plan && Array.isArray(plan.cards) && plan.cards.includes(card)) {
|
|
228
|
+
const tag = `[${card}] `;
|
|
229
|
+
const own = lc.acceptance.filter((a) => a && (typeof a === "string" ? a : String(a.text ?? "")).startsWith(tag));
|
|
230
|
+
if (own.length) return { forced, gate: `acceptance ${count(own)} at land` };
|
|
231
|
+
return { forced, gate: `acceptance ${count(lc.acceptance)} at land (the plan's)` };
|
|
232
|
+
}
|
|
233
|
+
return { forced, gate: `acceptance ${count(lc.acceptance)} at land` };
|
|
234
|
+
}
|
|
235
|
+
|
|
185
236
|
function acceptanceGate(report) {
|
|
186
237
|
const levels = (report.levels || []).filter((l) => l && l.integration);
|
|
187
238
|
const acc = levels.length && Array.isArray(levels[levels.length - 1].integration.acceptance) ? levels[levels.length - 1].integration.acceptance : [];
|
|
@@ -198,7 +249,7 @@ try {
|
|
|
198
249
|
if (batch !== null && !(Number.isInteger(batch) && batch > 0)) fail(`--batch must be a positive integer, got ${JSON.stringify(opts.batch)}`, 2);
|
|
199
250
|
if (opts.tag !== undefined && opts.tag.includes(",")) fail(`--tag takes one tag here; got "${opts.tag}"`, 2);
|
|
200
251
|
const board = loadBoard(dir);
|
|
201
|
-
const { card, cards, skipped } = batch === null ? nextReadyCard(board, { track, tag: opts.tag }) : nextReadyCards(board, { track, batch, tag: opts.tag });
|
|
252
|
+
const { card, cards, skipped } = batch === null ? nextReadyCard(board, { track, tag: opts.tag, epic: opts.epic, milestone: opts.milestone }) : nextReadyCards(board, { track, batch, tag: opts.tag, epic: opts.epic, milestone: opts.milestone });
|
|
202
253
|
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`);
|
|
203
254
|
const capLine = readyCapWarning(board);
|
|
204
255
|
if (capLine) process.stderr.write(capLine);
|
|
@@ -236,6 +287,42 @@ try {
|
|
|
236
287
|
process.stdout.write(out.join("\n") + "\n");
|
|
237
288
|
break;
|
|
238
289
|
}
|
|
290
|
+
case "rollup": {
|
|
291
|
+
if (opts.epic !== undefined && opts.milestone !== undefined) fail(USAGE_FOR.rollup, 2);
|
|
292
|
+
const dir = dirFrom(rest, 0);
|
|
293
|
+
const board = loadBoard(dir);
|
|
294
|
+
try {
|
|
295
|
+
checkFilters(board, { epic: opts.epic, milestone: opts.milestone });
|
|
296
|
+
} catch (e) {
|
|
297
|
+
fail(e.message, 2);
|
|
298
|
+
}
|
|
299
|
+
let text = null;
|
|
300
|
+
if (opts.from !== undefined) {
|
|
301
|
+
try {
|
|
302
|
+
text = readFileSync(resolve(opts.from), "utf8");
|
|
303
|
+
} catch (e) {
|
|
304
|
+
fail(`cannot read --from file ${opts.from}: ${e.message}`, 2);
|
|
305
|
+
}
|
|
306
|
+
} else if (existsSync(join(dir, LIVE_RUNS_RELPATH))) {
|
|
307
|
+
text = readFileSync(join(dir, LIVE_RUNS_RELPATH), "utf8");
|
|
308
|
+
}
|
|
309
|
+
const r = rollup(board, text);
|
|
310
|
+
const out = opts.epic !== undefined ? r.epics.find((e) => e.id === opts.epic) : opts.milestone !== undefined ? r.milestones.find((m) => m.id === opts.milestone) : r;
|
|
311
|
+
process.stdout.write(JSON.stringify(out, null, 2) + "\n");
|
|
312
|
+
break;
|
|
313
|
+
}
|
|
314
|
+
case "labels": {
|
|
315
|
+
if (rest[0] === "init" && opts.check !== true) {
|
|
316
|
+
const dir = dirFrom(rest, 1);
|
|
317
|
+
saveBoard(dir, initLabelGroups(loadBoard(dir)));
|
|
318
|
+
process.stdout.write(`Added label groups type and priority to ${relative(dir, boardPath(dir))}.\n`);
|
|
319
|
+
break;
|
|
320
|
+
}
|
|
321
|
+
if (opts.check !== true) fail(USAGE_FOR.labels, 2);
|
|
322
|
+
const lines = labelContrastFailures(loadBoard(dirFrom(rest, 0)));
|
|
323
|
+
if (lines.length) process.stdout.write(lines.join("\n") + "\n");
|
|
324
|
+
break;
|
|
325
|
+
}
|
|
239
326
|
case "card": {
|
|
240
327
|
const dir = dirFrom(rest, 1);
|
|
241
328
|
process.stdout.write(JSON.stringify(findCard(loadBoard(dir), rest[0]), null, 2) + "\n");
|
|
@@ -272,6 +359,8 @@ try {
|
|
|
272
359
|
if (opts.deps !== undefined) fields.deps = opts.deps.split(",").map((d) => d.trim()).filter(Boolean);
|
|
273
360
|
if (opts.tag !== undefined) fields.tags = opts.tag.split(",").map((t) => t.trim()).filter(Boolean);
|
|
274
361
|
if (opts.source !== undefined) fields.source = opts.source;
|
|
362
|
+
if (opts.epic !== undefined) fields.epic = opts.epic === "" ? null : opts.epic;
|
|
363
|
+
if (opts.milestone !== undefined) fields.milestone = opts.milestone === "" ? null : opts.milestone;
|
|
275
364
|
const changed = EDIT_FIELD_ORDER.filter((k) => Object.prototype.hasOwnProperty.call(fields, k));
|
|
276
365
|
if (!changed.length) fail(EDIT_USAGE, 2);
|
|
277
366
|
const dir = dirFrom(rest, 1);
|
|
@@ -282,6 +371,47 @@ try {
|
|
|
282
371
|
process.stdout.write(`Edited ${id} (${changed.join(", ")}) in ${relative(dir, boardPath(dir))}.\n`);
|
|
283
372
|
break;
|
|
284
373
|
}
|
|
374
|
+
case "epic":
|
|
375
|
+
case "milestone": {
|
|
376
|
+
const [action, id] = rest;
|
|
377
|
+
const add = cmd === "epic" ? addEpic : addMilestone;
|
|
378
|
+
const edit = cmd === "epic" ? editEpic : editMilestone;
|
|
379
|
+
const targetOf = () => {
|
|
380
|
+
const t = {};
|
|
381
|
+
if (opts.version !== undefined) t.version = opts.version === "" ? null : opts.version;
|
|
382
|
+
if (opts.date !== undefined) t.date = opts.date === "" ? null : opts.date;
|
|
383
|
+
return t;
|
|
384
|
+
};
|
|
385
|
+
if (action === "list") {
|
|
386
|
+
const board = loadBoard(dirFrom(rest, 1));
|
|
387
|
+
process.stdout.write(JSON.stringify(board[`${cmd}s`] || [], null, 2) + "\n");
|
|
388
|
+
break;
|
|
389
|
+
}
|
|
390
|
+
if (!id) fail(planUsage(cmd), 2);
|
|
391
|
+
if (action === "add") {
|
|
392
|
+
if (!opts.title || !opts.goal) fail(planUsage(cmd), 2);
|
|
393
|
+
const dir = dirFrom(rest, 2);
|
|
394
|
+
const target = {};
|
|
395
|
+
if (opts.version) target.version = opts.version;
|
|
396
|
+
if (opts.date) target.date = opts.date;
|
|
397
|
+
saveBoard(dir, add(loadBoard(dir), { id, title: opts.title, goal: opts.goal, ...(Object.keys(target).length ? { target } : {}) }));
|
|
398
|
+
process.stdout.write(`Added ${cmd} ${id} in ${relative(dir, boardPath(dir))}.\n`);
|
|
399
|
+
break;
|
|
400
|
+
}
|
|
401
|
+
const fields = {};
|
|
402
|
+
if (opts.title !== undefined) fields.title = opts.title;
|
|
403
|
+
if (opts.goal !== undefined) fields.goal = opts.goal;
|
|
404
|
+
if (opts.status !== undefined) fields.status = opts.status;
|
|
405
|
+
const target = targetOf();
|
|
406
|
+
if (Object.keys(target).length) fields.target = target;
|
|
407
|
+
if (!Object.keys(fields).length) fail(planUsage(cmd), 2);
|
|
408
|
+
const dir = dirFrom(rest, 2);
|
|
409
|
+
const { board, warnings } = edit(loadBoard(dir), id, fields, { force: !!opts.force });
|
|
410
|
+
saveBoard(dir, board);
|
|
411
|
+
for (const w of warnings) process.stderr.write(`warning: ${w}\n`);
|
|
412
|
+
process.stdout.write(`Edited ${cmd} ${id} (${Object.keys(fields).join(", ")}) in ${relative(dir, boardPath(dir))}.\n`);
|
|
413
|
+
break;
|
|
414
|
+
}
|
|
285
415
|
case "remove": {
|
|
286
416
|
const [id] = rest;
|
|
287
417
|
if (!id) fail("usage: board.mjs remove <id> [dir] [--force]", 2);
|
|
@@ -325,11 +455,11 @@ try {
|
|
|
325
455
|
case "record": {
|
|
326
456
|
if (opts.hand) {
|
|
327
457
|
const [id] = rest;
|
|
328
|
-
if (!id) fail("usage: board.mjs record <id> --hand [dir] --summary \"<one sentence>\" [--commit <sha>] [--wall <text>] [--gate <text>] [--note <text>] [--rehearsal <scenario> --rehearsal-outcome <passed|failed>] [--cost <usd>]", 2);
|
|
458
|
+
if (!id) fail("usage: board.mjs record <id> --hand [dir] --summary \"<one sentence>\" [--commit <sha>] [--wall <text>] [--gate <text>] [--note <text>] [--rehearsal <scenario> --rehearsal-outcome <passed|failed> [--rehearsal-fixture <name>]] [--cost <usd>]", 2);
|
|
329
459
|
if (opts.cost !== undefined && !/^\d+(\.\d+)?$/.test(opts.cost)) fail(`--cost takes a non-negative number of dollars, got ${JSON.stringify(opts.cost)}`, 2);
|
|
330
460
|
const dir = dirFrom(rest, 1);
|
|
331
461
|
const card = findCard(loadBoard(dir), id);
|
|
332
|
-
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, rehearsalOutcome: opts["rehearsal-outcome"] ?? null, cost: opts.cost !== undefined ? Number(opts.cost) : null });
|
|
462
|
+
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, rehearsalOutcome: opts["rehearsal-outcome"] ?? null, fixture: opts["rehearsal-fixture"] ?? null, cost: opts.cost !== undefined ? Number(opts.cost) : null });
|
|
333
463
|
const block = landingBlock({ id, ok: true, commit: opts.commit, wall: opts.wall, gate: opts.gate, cost: opts.cost, note: opts.note, rehearsal: opts.rehearsal ?? null, rehearsalOutcome: opts["rehearsal-outcome"] ?? null });
|
|
334
464
|
const { file, promoted } = recordLanding(dir, id, entry);
|
|
335
465
|
process.stdout.write(`${summarySentence}\n`);
|
|
@@ -343,7 +473,7 @@ try {
|
|
|
343
473
|
break;
|
|
344
474
|
}
|
|
345
475
|
const [id, reportFile] = rest;
|
|
346
|
-
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> --rehearsal-outcome <passed|failed>] [--run <run-id>] [--claude-dir <dir>]", 2);
|
|
476
|
+
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> --rehearsal-outcome <passed|failed> [--rehearsal-fixture <name>]] [--run <run-id>] [--claude-dir <dir>]", 2);
|
|
347
477
|
const dir = dirFrom(rest, 2);
|
|
348
478
|
const card = findCard(loadBoard(dir), id);
|
|
349
479
|
const report = unwrapReport(JSON.parse(readFileSync(resolve(reportFile), "utf8")));
|
|
@@ -352,8 +482,10 @@ try {
|
|
|
352
482
|
const batch = plan && Array.isArray(plan.cards) && plan.cards.includes(id) ? plan.cards : null;
|
|
353
483
|
const taskIds = batch ? plan.tasks.filter((t) => t.card === id).map((t) => t.id) : [];
|
|
354
484
|
const wall = opts.run ? wallForRun({ runId: opts.run, claudeDir: opts["claude-dir"], projectDir: dir, wallMinutes: report.budget?.wallMinutes ?? DEFAULT_BUDGET.wallMinutes }) : null;
|
|
355
|
-
|
|
356
|
-
const
|
|
485
|
+
// A plan landed after `plan.mjs done --force` is recorded as landed, with land's own acceptance.
|
|
486
|
+
const { forced, gate: forcedGate } = forcedLanding({ plan, report, opts, card: id });
|
|
487
|
+
const entry = runEntry({ forcedLanding: forced, 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, rehearsalOutcome: opts["rehearsal-outcome"] ?? null, fixture: opts["rehearsal-fixture"] ?? null });
|
|
488
|
+
const block = landingBlock({ id, ok: report.ok === true || forced, commit: opts.commit, wall: opts.wall, gate: forced ? forcedGate : acceptanceGate(report), cost: opts.cost, codexCost: opts["codex-cost"], note: opts.note, rehearsal: opts.rehearsal ?? null, rehearsalOutcome: opts["rehearsal-outcome"] ?? null });
|
|
357
489
|
const { file, promoted } = recordLanding(dir, id, entry);
|
|
358
490
|
process.stdout.write(`${summarySentence}\n`);
|
|
359
491
|
if (promoted) process.stdout.write(`Promoted the research note for ${id} to ${promoted}.\n`);
|
|
@@ -382,14 +514,15 @@ try {
|
|
|
382
514
|
if (saved.length === 3) sumSaved = saved;
|
|
383
515
|
} catch {}
|
|
384
516
|
}
|
|
385
|
-
const
|
|
517
|
+
const sumForced = opts.card !== undefined ? forcedLanding({ plan: sumPlan, report: wholeReport, opts, card: opts.card }) : { forced: false, gate: null };
|
|
518
|
+
const sumBlock = sumSaved ? sumSaved : opts.card !== undefined ? landingBlock({ id: opts.card, ok: report.ok === true || sumForced.forced, commit: opts.commit, wall: opts.wall, gate: sumForced.forced ? sumForced.gate : acceptanceGate(wholeReport), cost: opts.cost, codexCost: opts["codex-cost"], note: opts.note }) : null;
|
|
386
519
|
process.stdout.write(`${summarySentence}\n`);
|
|
387
|
-
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 }));
|
|
520
|
+
process.stdout.write(runSummary({ report, forcedLanding: sumForced.forced, 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 }));
|
|
388
521
|
if (sumBlock) writeLandingBlock(dirFrom(rest, 1), opts.card, sumBlock, sumSaved !== null);
|
|
389
522
|
break;
|
|
390
523
|
}
|
|
391
524
|
default:
|
|
392
|
-
fail("usage: board.mjs <next|card|reorder|move|edit|remove|record|summary> ...", 2);
|
|
525
|
+
fail("usage: board.mjs <next|card|reorder|move|edit|epic|milestone|rollup|labels|remove|record|summary> ...", 2);
|
|
393
526
|
}
|
|
394
527
|
} catch (err) {
|
|
395
528
|
fail(err.message);
|
|
@@ -14,13 +14,14 @@
|
|
|
14
14
|
// --claude-dir <path> where ~/.claude lives (tests point it at a fixture)
|
|
15
15
|
// --report <path> a saved workflow report (card adversary-usage-in-report): prices its adversary relays'
|
|
16
16
|
// Codex usage as a separate line/`codex` key, never mixed into the Claude total; default
|
|
17
|
-
// <dir>/REPORT_RELPATH
|
|
18
|
-
//
|
|
17
|
+
// <dir>/REPORT_RELPATH when no --report is given and that file is readable. A report is
|
|
18
|
+
// attached only when its top-level runId equals the run asked for; a report with no runId
|
|
19
|
+
// is attached only when named with --report (card cost-run-codex-line-mismatch)
|
|
19
20
|
// Exit 1 when the run's journal cannot be found, or an explicit --report cannot be read/parsed; the text output ends
|
|
20
21
|
// with the run total and any unpriced agent. An unreadable/invalid default report is ignored, not a usage error.
|
|
21
22
|
import { readFileSync } from "node:fs";
|
|
22
23
|
import { resolve, join } from "node:path";
|
|
23
|
-
import { costRun, costSession, renderSession, renderCost, costByCard, renderCostByCard } from "../lib/cost.mjs";
|
|
24
|
+
import { reportForRun, costRun, costSession, renderSession, renderCost, costByCard, renderCostByCard } from "../lib/cost.mjs";
|
|
24
25
|
import { planFor, REPORT_RELPATH, unwrapReport } from "../lib/plan.mjs";
|
|
25
26
|
|
|
26
27
|
const argv = process.argv.slice(2);
|
|
@@ -81,6 +82,8 @@ try {
|
|
|
81
82
|
reportPath = null;
|
|
82
83
|
}
|
|
83
84
|
}
|
|
85
|
+
report = reportForRun(report, runId, { named: Boolean(opts.report) });
|
|
86
|
+
if (!report) reportPath = null;
|
|
84
87
|
const result = costRun({ runId, projectDir, claudeDir: opts["claude-dir"] ? resolve(opts["claude-dir"]) : undefined, report, reportPath });
|
|
85
88
|
const byCard = opts["by-card"] ? costByCard(result, planFor(projectDir, opts.card)) : null;
|
|
86
89
|
if (opts.json) process.stdout.write(JSON.stringify(byCard ? { ...result, byCard } : result, null, 2) + "\n");
|
|
@@ -95,7 +95,7 @@ function readOwnSkills() {
|
|
|
95
95
|
function signalsText(s) {
|
|
96
96
|
return [
|
|
97
97
|
`outcomes: ${s.outcomes.rows.length} rows, ${s.outcomes.gateFailures} gate failures, blocks real=${s.outcomes.blocksByClass.real} marginal=${s.outcomes.blocksByClass.marginal} false=${s.outcomes.blocksByClass.false}, fixPasses sum=${s.outcomes.fixPasses.sum} max=${s.outcomes.fixPasses.max}, usd sum=${s.outcomes.usd.sum.toFixed(2)} n=${s.outcomes.usd.n}, cards=${Object.keys(s.outcomes.byCard).length}`,
|
|
98
|
-
`lessons: ${s.lessons.total} total, ${s.lessons.live} live, ${s.lessons.helpful} helpful, ${s.lessons.harmful} harmful, ${s.lessons.stale} stale, ${s.lessons.
|
|
98
|
+
`lessons: ${s.lessons.total} total, ${s.lessons.live} live, ${s.lessons.helpful} helpful, ${s.lessons.harmful} harmful, ${s.lessons.stale} stale, ${s.lessons.pitfalls.length} pitfalls with occurrences`,
|
|
99
99
|
`trace: ${s.trace.files.length} files, ${s.trace.denials.length} denials, ${s.trace.unmatched.length} unmatched (reported only, never a proposal signal), ${s.trace.skills.length} skills invoked, ${s.trace.instructionsLoaded.length} instruction files loaded`,
|
|
100
100
|
].join("\n");
|
|
101
101
|
}
|