@dougharness/cli 1.0.1 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bin.js +8 -0
- package/dist/bin.js.map +1 -1
- package/dist/board-serve.js +9 -3
- package/dist/board-serve.js.map +1 -1
- package/dist/board.js +10 -2
- package/dist/board.js.map +1 -1
- package/dist/detect/repo.js +14 -1
- package/dist/detect/repo.js.map +1 -1
- package/dist/detect/types.d.ts +2 -0
- package/dist/doctor.d.ts +46 -0
- package/dist/doctor.js +491 -0
- package/dist/doctor.js.map +1 -0
- package/dist/generate/agents.d.ts +7 -0
- package/dist/generate/agents.js +62 -12
- package/dist/generate/agents.js.map +1 -1
- package/dist/generate/claude-md.js +4 -0
- package/dist/generate/claude-md.js.map +1 -1
- package/dist/generate/config.d.ts +8 -1
- package/dist/generate/config.js +5 -12
- package/dist/generate/config.js.map +1 -1
- package/dist/generate/proposal.js +24 -6
- package/dist/generate/proposal.js.map +1 -1
- package/dist/generate/settings.d.ts +2 -0
- package/dist/generate/settings.js +17 -2
- package/dist/generate/settings.js.map +1 -1
- package/dist/init.d.ts +14 -0
- package/dist/init.js +94 -8
- package/dist/init.js.map +1 -1
- package/dist/memory-setup.d.ts +28 -0
- package/dist/memory-setup.js +323 -0
- package/dist/memory-setup.js.map +1 -0
- package/node_modules/@dougharness/flow/.claude-plugin/plugin.json +16 -4
- package/node_modules/@dougharness/flow/agents/planner.md +2 -2
- package/node_modules/@dougharness/flow/agents/researcher.md +2 -2
- package/node_modules/@dougharness/flow/hooks/hooks.json +16 -0
- package/node_modules/@dougharness/flow/lib/board.mjs +154 -12
- package/node_modules/@dougharness/flow/lib/code-index.mjs +18 -3
- package/node_modules/@dougharness/flow/lib/context.mjs +57 -0
- package/node_modules/@dougharness/flow/lib/cost.mjs +129 -4
- package/node_modules/@dougharness/flow/lib/data/pairs.json +103 -0
- package/node_modules/@dougharness/flow/lib/decisions.mjs +2 -2
- package/node_modules/@dougharness/flow/lib/embeddings.mjs +189 -27
- package/node_modules/@dougharness/flow/lib/land.mjs +69 -10
- package/node_modules/@dougharness/flow/lib/learn.mjs +61 -4
- package/node_modules/@dougharness/flow/lib/memory-import.mjs +22 -4
- package/node_modules/@dougharness/flow/lib/memory.mjs +326 -35
- package/node_modules/@dougharness/flow/lib/plan.mjs +9 -0
- package/node_modules/@dougharness/flow/package.json +2 -1
- package/node_modules/@dougharness/flow/scripts/board.mjs +60 -20
- package/node_modules/@dougharness/flow/scripts/context.mjs +48 -0
- package/node_modules/@dougharness/flow/scripts/cost.mjs +15 -3
- package/node_modules/@dougharness/flow/scripts/memory.mjs +196 -39
- package/node_modules/@dougharness/flow/scripts/plan.mjs +32 -64
- package/node_modules/@dougharness/flow/scripts/session-recall.mjs +132 -0
- package/node_modules/@dougharness/flow/skills/doug-hand/SKILL.md +105 -0
- package/node_modules/@dougharness/flow/skills/doug-learn/SKILL.md +1 -1
- package/node_modules/@dougharness/flow/skills/doug-next/SKILL.md +44 -10
- package/node_modules/@dougharness/flow/skills/doug-swarm/SKILL.md +5 -5
- package/node_modules/@dougharness/flow/skills/research/SKILL.md +4 -2
- package/node_modules/@dougharness/flow/skills/run-report/SKILL.md +10 -11
- package/node_modules/@dougharness/flow/skills/swarm-launch/SKILL.md +1 -1
- package/node_modules/@dougharness/flow/workflows/doug-implement.js +119 -72
- package/node_modules/@dougharness/gates/.claude-plugin/plugin.json +1 -1
- package/node_modules/@dougharness/gates/hooks/hooks.json +11 -0
- package/node_modules/@dougharness/gates/lib/baseline.mjs +166 -1
- package/node_modules/@dougharness/gates/lib/block-ledger.mjs +58 -1
- package/node_modules/@dougharness/gates/lib/checkpoint.mjs +1 -1
- package/node_modules/@dougharness/gates/lib/config.mjs +20 -0
- package/node_modules/@dougharness/gates/lib/docs-gate.mjs +95 -0
- package/node_modules/@dougharness/gates/lib/evidence.mjs +2 -0
- package/node_modules/@dougharness/gates/lib/run.mjs +4 -0
- package/node_modules/@dougharness/gates/lib/state.mjs +5 -0
- package/node_modules/@dougharness/gates/lib/test-totals.mjs +139 -0
- package/node_modules/@dougharness/gates/lib/trace.mjs +24 -7
- package/node_modules/@dougharness/gates/lib/verified-tree.mjs +225 -0
- package/node_modules/@dougharness/gates/package.json +1 -1
- package/node_modules/@dougharness/gates/scripts/research-cap.mjs +133 -5
- package/node_modules/@dougharness/gates/scripts/session-start-baseline.mjs +14 -0
- package/node_modules/@dougharness/gates/scripts/statusline.mjs +33 -7
- package/node_modules/@dougharness/gates/scripts/stop-gate.mjs +794 -67
- package/node_modules/@dougharness/gates/scripts/trace.mjs +5 -1
- package/node_modules/@dougharness/gates/scripts/turn-start.mjs +17 -0
- package/node_modules/@dougharness/gates/scripts/verify.mjs +146 -0
- package/package.json +3 -3
- package/templates/board-app.js +2 -2
- package/node_modules/@dougharness/flow/lib/rehearse.mjs +0 -825
- package/node_modules/@dougharness/flow/scripts/rehearse.mjs +0 -129
- package/node_modules/@dougharness/flow/skills/core-next/SKILL.md +0 -92
- package/node_modules/@dougharness/flow/skills/harness-fix/SKILL.md +0 -85
|
@@ -18,6 +18,29 @@ export const DONE = "done";
|
|
|
18
18
|
export const SIZES = ["S", "M", "L"];
|
|
19
19
|
export const TRACKS = ["flow", "hand"];
|
|
20
20
|
export const CLASSES = ["tests-only", "prose", "gate-script", "code", "docs", "eval", "decision"];
|
|
21
|
+
// Why a task stopped, as a closed set: every halt site in the doug-implement workflow sets one beside its prose
|
|
22
|
+
// stopReason. The workflow carries its own literal copy (it may not import); a template test keeps them equal.
|
|
23
|
+
export const STOP_CLASSES = [
|
|
24
|
+
"implementer-blocked",
|
|
25
|
+
"partial",
|
|
26
|
+
"stage-missing",
|
|
27
|
+
"spec-contradiction",
|
|
28
|
+
"environment",
|
|
29
|
+
"adversary-not-run",
|
|
30
|
+
"outside-owned",
|
|
31
|
+
"new-blockers-twice",
|
|
32
|
+
"fix-attempts-exhausted",
|
|
33
|
+
"stalled",
|
|
34
|
+
"budget",
|
|
35
|
+
"no-new-commit",
|
|
36
|
+
"dependency-skipped",
|
|
37
|
+
"stage-threw",
|
|
38
|
+
"level-adversary",
|
|
39
|
+
];
|
|
40
|
+
// "<id> [<class>]: "<reason>"" for a stopped task; an old report has no class and prints "<id>: "<reason>"".
|
|
41
|
+
function stopText(t) {
|
|
42
|
+
return `${t.id}${t.stopClass ? ` [${t.stopClass}]` : ""}: "${t.stopReason}"`;
|
|
43
|
+
}
|
|
21
44
|
export const DEFAULT_TAGS = ["bug", "feature", "chore", "docs", "refactor", "spike"];
|
|
22
45
|
|
|
23
46
|
export const DEFAULT_COLUMNS = [
|
|
@@ -28,6 +51,81 @@ export const DEFAULT_COLUMNS = [
|
|
|
28
51
|
{ id: "done", title: "Done", hint: "merged, gate green, documented" },
|
|
29
52
|
];
|
|
30
53
|
|
|
54
|
+
// Ready is capped at 7; only the user promotes cards to Ready. A line for stderr when it holds more, else null.
|
|
55
|
+
export const READY_CAP = 7;
|
|
56
|
+
export function readyCapWarning(board) {
|
|
57
|
+
const n = board.cards.filter((c) => c.column === "ready").length;
|
|
58
|
+
return n <= READY_CAP ? null : `warning: Ready holds ${n} cards, above the cap of ${READY_CAP}; only the user promotes cards to Ready\n`;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Wall-clock budget per card in minutes, a size x class table at p80 of measured landings (cards
|
|
62
|
+
// autopilot-wall-cap-by-size, wall-budget-size-class). Each size has `all`; a class key is present only when its
|
|
63
|
+
// cell had WALL_BUDGET_MIN_N or more landings. `board.mjs budgets --from docs/live-runs.md` recomputes it.
|
|
64
|
+
export const WALL_BUDGET_MIN_N = 5;
|
|
65
|
+
export const WALL_BUDGET_MIN = {"S":{"all":57,"code":53,"docs":29,"gate-script":116,"prose":50,"tests-only":66},"M":{"all":102,"code":112,"gate-script":289},"L":{"all":274}};
|
|
66
|
+
|
|
67
|
+
// A card's budget: its size's class cell when it has one, else the size's `all`. No or an unknown size counts as M.
|
|
68
|
+
export function cardBudgetMinutes(card, table = WALL_BUDGET_MIN) {
|
|
69
|
+
const size = card && (card.size === "S" || card.size === "M" || card.size === "L") ? card.size : "M";
|
|
70
|
+
const row = table[size];
|
|
71
|
+
const cls = card && card.class;
|
|
72
|
+
return typeof cls === "string" && Object.prototype.hasOwnProperty.call(row, cls) && cls !== "all" ? row[cls] : row.all;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function wallCapMinutes(cards, table = WALL_BUDGET_MIN) {
|
|
76
|
+
return cards.reduce((sum, c) => sum + cardBudgetMinutes(c, table), 0);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// The landings in a docs/live-runs.md text: one { id, minutes } per `## <date>, <id>: <title>` section that has a
|
|
80
|
+
// `| Wall clock | <n> min |` row (text after min is allowed; a decimal n is skipped) (the first such row). Older multi-column tables have no such row and are skipped.
|
|
81
|
+
export function parseLiveRunWalls(text) {
|
|
82
|
+
const out = [];
|
|
83
|
+
let id = null;
|
|
84
|
+
let seen = false;
|
|
85
|
+
for (const line of String(text).split("\n")) {
|
|
86
|
+
if (line.startsWith("## ")) {
|
|
87
|
+
const m = /^## \d{4}-\d{2}-\d{2}, ([^:\s]+): /.exec(line);
|
|
88
|
+
id = m ? m[1] : null;
|
|
89
|
+
seen = false;
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
if (id === null || seen) continue;
|
|
93
|
+
const w = /^\| Wall clock \| (\d+) min(?:\s[^|]*)? \|\s*$/.exec(line);
|
|
94
|
+
if (w) {
|
|
95
|
+
out.push({ id, minutes: Number(w[1]) });
|
|
96
|
+
seen = true;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return out;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// The table recomputed from samples joined to their cards: p80 (sorted ascending, index min(floor(0.8 n), n - 1)) per size and per size/class cell. A
|
|
103
|
+
// class cell under minN samples is left out (it falls back to its size's `all`); a size with no samples keeps the
|
|
104
|
+
// fallback's `all`. `cells` lists every cell with its n and p80 (cls "all" is the size row; p80 null when n is 0).
|
|
105
|
+
export function wallBudgetTable(samples, board, { minN = WALL_BUDGET_MIN_N, fallback = WALL_BUDGET_MIN } = {}) {
|
|
106
|
+
const byId = new Map(board.cards.map((c) => [c.id, c]));
|
|
107
|
+
const groups = { S: { all: [] }, M: { all: [] }, L: { all: [] } };
|
|
108
|
+
for (const s of samples) {
|
|
109
|
+
const card = byId.get(s.id);
|
|
110
|
+
if (!card || !(card.size === "S" || card.size === "M" || card.size === "L")) continue;
|
|
111
|
+
groups[card.size].all.push(s.minutes);
|
|
112
|
+
if (typeof card.class === "string" && card.class && card.class !== "all") (groups[card.size][card.class] ||= []).push(s.minutes);
|
|
113
|
+
}
|
|
114
|
+
const p80 = (xs) => [...xs].sort((a, b) => a - b)[Math.min(Math.floor(0.8 * xs.length), xs.length - 1)];
|
|
115
|
+
const table = {};
|
|
116
|
+
const cells = [];
|
|
117
|
+
for (const size of ["S", "M", "L"]) {
|
|
118
|
+
const g = groups[size];
|
|
119
|
+
table[size] = { all: g.all.length ? p80(g.all) : fallback[size].all };
|
|
120
|
+
cells.push({ size, cls: "all", n: g.all.length, p80: g.all.length ? p80(g.all) : null });
|
|
121
|
+
for (const cls of Object.keys(g).filter((k) => k !== "all").sort()) {
|
|
122
|
+
cells.push({ size, cls, n: g[cls].length, p80: p80(g[cls]) });
|
|
123
|
+
if (g[cls].length >= minN) table[size][cls] = p80(g[cls]);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
return { table, cells };
|
|
127
|
+
}
|
|
128
|
+
|
|
31
129
|
function today() {
|
|
32
130
|
return new Date().toISOString().slice(0, 10);
|
|
33
131
|
}
|
|
@@ -197,8 +295,9 @@ export function findCard(board, id) {
|
|
|
197
295
|
// The first Ready card, in board order, whose dependencies are all Done. Ready cards that are waiting
|
|
198
296
|
// on something are reported so the user can see why they were skipped.
|
|
199
297
|
// The first Ready card on the given track whose deps are Done. A card on the hand track is built directly in the
|
|
200
|
-
// checkout (tests, gate, commit)
|
|
201
|
-
// the other track is skipped with hand: true or flow: true so the skill can say so. Decision 0005.
|
|
298
|
+
// checkout (tests, gate, commit) through doug-hand, which /doug-next hands it to by id; a flow card the other way round. A Ready card on
|
|
299
|
+
// the other track is skipped with hand: true or flow: true so the skill can say so. Decision 0005. `track: "any"`
|
|
300
|
+
// takes a Ready card of either track (card autopilot): no track skip entries, the deps rule unchanged.
|
|
202
301
|
// `tag`, when given, filters to Ready cards carrying it: a card without it is skipped SILENTLY (not pushed onto
|
|
203
302
|
// `skipped`, since a caller-requested filter should not print a "skipping" line per card it filters out); a `tag`
|
|
204
303
|
// outside boardTags(board) throws.
|
|
@@ -210,7 +309,7 @@ export function nextReadyCard(board, { track = "flow", tag } = {}) {
|
|
|
210
309
|
if (c.column !== READY) continue;
|
|
211
310
|
if (tag !== undefined && !(c.tags || []).includes(tag)) continue;
|
|
212
311
|
const cardTrack = c.track === "hand" ? "hand" : "flow";
|
|
213
|
-
if (cardTrack !== track) {
|
|
312
|
+
if (track !== "any" && cardTrack !== track) {
|
|
214
313
|
skipped.push(cardTrack === "hand" ? { id: c.id, hand: true } : { id: c.id, flow: true });
|
|
215
314
|
continue;
|
|
216
315
|
}
|
|
@@ -237,7 +336,7 @@ export function nextReadyCards(board, { track = "flow", batch, tag } = {}) {
|
|
|
237
336
|
if (c.column !== READY || cards.length >= batch) continue;
|
|
238
337
|
if (tag !== undefined && !(c.tags || []).includes(tag)) continue;
|
|
239
338
|
const cardTrack = c.track === "hand" ? "hand" : "flow";
|
|
240
|
-
if (cardTrack !== track) {
|
|
339
|
+
if (track !== "any" && cardTrack !== track) {
|
|
241
340
|
skipped.push(cardTrack === "hand" ? { id: c.id, hand: true } : { id: c.id, flow: true });
|
|
242
341
|
continue;
|
|
243
342
|
}
|
|
@@ -579,7 +678,36 @@ function outcomeOf(report) {
|
|
|
579
678
|
return "not green";
|
|
580
679
|
}
|
|
581
680
|
|
|
582
|
-
|
|
681
|
+
// Card flow-wall-budget: the Stage wall row and the summary note from cost.mjs's runWall result (passed in; this
|
|
682
|
+
// library does not import cost.mjs). `keep` narrows the flags to one card's entry of a batch run.
|
|
683
|
+
const wallMin = (ms) => {
|
|
684
|
+
const m = ms / 60000;
|
|
685
|
+
return Number.isInteger(m) ? String(m) : m.toFixed(1);
|
|
686
|
+
};
|
|
687
|
+
function wallFlagText(f) {
|
|
688
|
+
const what = f.kind === "pass" ? `${f.label} pass ${f.pass ?? "?"}` : `${f.kind} ${f.label}`;
|
|
689
|
+
return `${what} ${wallMin(f.ms)} min > ${wallMin(f.limitMs)}`;
|
|
690
|
+
}
|
|
691
|
+
function wallFlagsFor(wall, report, batch, cardId) {
|
|
692
|
+
const flags = Array.isArray(wall.flags) ? wall.flags : [];
|
|
693
|
+
if (!Array.isArray(batch) || batch.length < 2) return flags;
|
|
694
|
+
const ids = (report.levels || []).flatMap((l) => (l.tasks || []).map((t) => t.id));
|
|
695
|
+
return flags.filter((f) => {
|
|
696
|
+
const label = String(f.label).replace(/:retry$/, "");
|
|
697
|
+
if (/(^|:)level-\d+$/.test(label)) return batch[0] === cardId;
|
|
698
|
+
return ids.some((id) => label === id || label.startsWith(`${id}:`) || label.includes(`:${id}:`) || label.endsWith(`:${id}`));
|
|
699
|
+
});
|
|
700
|
+
}
|
|
701
|
+
function wallRow(wall, flags) {
|
|
702
|
+
if (!wall) return "not measured";
|
|
703
|
+
if (!wall.measured) return `unverified: ${wall.reason || "no agent times"}`;
|
|
704
|
+
const unverified = Array.isArray(wall.unverified) && wall.unverified.length ? `; unverified tasks: ${wall.unverified.join(", ")}` : "";
|
|
705
|
+
if (flags.length) return `over: ${flags.map(wallFlagText).join("; ")}${unverified}`;
|
|
706
|
+
const longest = Math.max(0, ...(wall.tasks || []).map((t) => t.ms));
|
|
707
|
+
return `within budget (${wall.wallMinutes ?? wallMin(longest)} min a task)${unverified}`;
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
export function runEntry({ card, report, cost = null, codexCost = null, wallClock = null, wall = null, mergeCommit = null, record = null, adversary = [], date = new Date().toISOString().slice(0, 10), batch = null, sharedCost = null, taskIds = [], note = null, rehearsal = null }) {
|
|
583
711
|
const inBatch = Array.isArray(batch) && batch.length > 1;
|
|
584
712
|
if (inBatch) report = filterReportForCard(report, card.id, taskIds);
|
|
585
713
|
const lines = [];
|
|
@@ -588,7 +716,7 @@ export function runEntry({ card, report, cost = null, codexCost = null, wallCloc
|
|
|
588
716
|
const batchNote = inBatch ? `, in one plan with ${listOthers(batch, card.id)} (one approval, one run, one landing)` : "";
|
|
589
717
|
lines.push(
|
|
590
718
|
rehearsal
|
|
591
|
-
? `Rehearsal ${rehearsal} for card \`${card.id}\` on the ts-basic fixture (
|
|
719
|
+
? `Rehearsal ${rehearsal} for card \`${card.id}\` on the ts-basic fixture (scripts/rehearse.mjs); the commit and gate below are the fixture's, not this repository's.`
|
|
592
720
|
: `Ran through /doug-next on this repo from card \`${card.id}\`${record ? ` in \`${record}\`` : ""}${batchNote}. Plan "${report.plan}", integration branch \`${report.integrationBranch}\`${report.modelsSource ? `, models from ${report.modelsSource}` : ""}.`,
|
|
593
721
|
);
|
|
594
722
|
lines.push("");
|
|
@@ -609,6 +737,7 @@ export function runEntry({ card, report, cost = null, codexCost = null, wallCloc
|
|
|
609
737
|
}
|
|
610
738
|
if (inBatch) lines.push(`| Shared integration agents | ${sharedCost === null ? "not measured" : `$${Number(sharedCost).toFixed(2)}, counted once for the batch ${batch[0] === card.id ? "here" : `under ${batch[0]}`}`} |`);
|
|
611
739
|
lines.push(`| Wall clock | ${wallClock === null ? "not measured" : wallClock} |`);
|
|
740
|
+
lines.push(`| Stage wall | ${wallRow(wall, wall ? wallFlagsFor(wall, report, inBatch ? batch : null, card.id) : [])} |`);
|
|
612
741
|
const blocks = classifyBlocks(report, adversary);
|
|
613
742
|
const shownCommit = rehearsal && mergeCommit ? String(mergeCommit).slice(0, 7) : mergeCommit;
|
|
614
743
|
lines.push(`| Landed as | ${shownCommit ? `\`${shownCommit}\`` : "not landed"} |`);
|
|
@@ -634,6 +763,11 @@ export function runEntry({ card, report, cost = null, codexCost = null, wallCloc
|
|
|
634
763
|
}
|
|
635
764
|
lines.push(`| ${level.index} | integration | ${level.integration ? (level.integration.ok ? "ok" : "failed") : "not run"} | | | | ${describeTierCell({ implement: level.integrationModel })} | ${levelAdversaryCell(level.levelAdversary)} |`);
|
|
636
765
|
}
|
|
766
|
+
const stopped = (report.levels || []).flatMap((l) => (l.tasks || []).filter((t) => t.stopReason));
|
|
767
|
+
if (stopped.length) {
|
|
768
|
+
lines.push("");
|
|
769
|
+
lines.push(`Stopped: ${stopped.map(stopText).join("; ")}`);
|
|
770
|
+
}
|
|
637
771
|
if (blocks.length) {
|
|
638
772
|
lines.push("");
|
|
639
773
|
lines.push("Adversary blocks, classified (real: a defect a user would hit; marginal: true to the spec, no user impact; false: wrong):");
|
|
@@ -644,15 +778,15 @@ export function runEntry({ card, report, cost = null, codexCost = null, wallCloc
|
|
|
644
778
|
}
|
|
645
779
|
|
|
646
780
|
// The live-runs entry for a hand-track card (decision 0005): no workflow report and no levels, so it records what
|
|
647
|
-
//
|
|
781
|
+
// doug-hand can measure: the landing commit, the wall clock, and the gate result, plus an optional note.
|
|
648
782
|
export function handEntry({ card, commit = null, wallClock = null, gate = null, note = null, record = null, date = new Date().toISOString().slice(0, 10), rehearsal = null }) {
|
|
649
783
|
const lines = [];
|
|
650
784
|
lines.push(`## ${date}, ${card.id}: ${card.title}`);
|
|
651
785
|
lines.push("");
|
|
652
786
|
lines.push(
|
|
653
787
|
rehearsal
|
|
654
|
-
? `Rehearsal ${rehearsal} for card \`${card.id}\` on the ts-basic fixture (
|
|
655
|
-
: `Built by hand through
|
|
788
|
+
? `Rehearsal ${rehearsal} for card \`${card.id}\` on the ts-basic fixture (scripts/rehearse.mjs); the commit and gate below are the fixture's, not this repository's.`
|
|
789
|
+
: `Built by hand through doug-hand from card \`${card.id}\`${record ? ` in \`${record}\`` : ""} (hand track: a gated by-hand change); no workflow run.`,
|
|
656
790
|
);
|
|
657
791
|
lines.push("");
|
|
658
792
|
lines.push("| Measure | Value |");
|
|
@@ -764,13 +898,21 @@ function acceptanceUnmet(report, tasks) {
|
|
|
764
898
|
return { unmet, source: ` (acceptance read per task by [card] tag${asideNote})`, dirty: asideFailing > 0 };
|
|
765
899
|
}
|
|
766
900
|
|
|
901
|
+
function wallNote(wall, flags) {
|
|
902
|
+
if (!wall) return "";
|
|
903
|
+
if (!wall.measured) return " (stage times unverified)";
|
|
904
|
+
const n = Array.isArray(wall.unverified) ? wall.unverified.length : 0;
|
|
905
|
+
const unv = n ? ` (stage times unverified for ${n} task${n === 1 ? "" : "s"})` : "";
|
|
906
|
+
return (flags.length ? ` (over budget: ${flags.map(wallFlagText).join("; ")})` : "") + unv;
|
|
907
|
+
}
|
|
908
|
+
|
|
767
909
|
// Three lines for the chat after a run: the outcome, the tasks, and what stopped or was not met. Everything in it
|
|
768
910
|
// is read from the report; nothing is estimated.
|
|
769
|
-
export function runSummary({ report, cost = null, codexCost = null, wallClock = null, mergeCommit = null }) {
|
|
911
|
+
export function runSummary({ report, cost = null, codexCost = null, wallClock = null, wall = null, mergeCommit = null, batch = null, card = null }) {
|
|
770
912
|
const tasks = (report.levels || []).flatMap((l) => (l.tasks || []).map((t) => ({ level: l, task: t })));
|
|
771
913
|
const integrated = tasks.filter(({ level }) => level.integration && level.integration.ok).length;
|
|
772
914
|
const outcome = outcomeOf(report);
|
|
773
|
-
const first = `${report.plan}: ${outcome}; ${tasks.length} task${tasks.length === 1 ? "" : "s"}, ${integrated} integrated; wall ${wallClock === null ? "not measured" : wallClock}; cost ${cost === null ? "not measured" : `$${Number(cost).toFixed(2)}`}${codexCost === null ? "" : `; Codex $${Number(codexCost).toFixed(2)}`}${mergeCommit ? `; landed as ${mergeCommit}` : ""}.`;
|
|
915
|
+
const first = `${report.plan}: ${outcome}; ${tasks.length} task${tasks.length === 1 ? "" : "s"}, ${integrated} integrated; wall ${wallClock === null ? "not measured" : wallClock}; cost ${cost === null ? "not measured" : `$${Number(cost).toFixed(2)}`}${codexCost === null ? "" : `; Codex $${Number(codexCost).toFixed(2)}`}${mergeCommit ? `; landed as ${mergeCommit}` : ""}${wallNote(wall, wall ? wallFlagsFor(wall, report, batch, card) : [])}.`;
|
|
774
916
|
const second = tasks.length
|
|
775
917
|
? tasks.map(({ task: t }) => {
|
|
776
918
|
const passes = Array.isArray(t.attempts) && t.attempts.length ? t.attempts.length : 1;
|
|
@@ -780,7 +922,7 @@ export function runSummary({ report, cost = null, codexCost = null, wallClock =
|
|
|
780
922
|
return `${t.id}: ${impl}, ${passes} pass${passes === 1 ? "" : "es"}, verify ${yesNo(t.verified)}, review ${yesNo(t.reviewed)}, adversary ${adv}`;
|
|
781
923
|
}).join("; ")
|
|
782
924
|
: "no tasks";
|
|
783
|
-
const stops = tasks.filter(({ task: t }) => t.stopReason).map(({ task: t }) =>
|
|
925
|
+
const stops = tasks.filter(({ task: t }) => t.stopReason).map(({ task: t }) => stopText(t));
|
|
784
926
|
const { unmet, source, dirty } = acceptanceUnmet(report, tasks);
|
|
785
927
|
const pause = pausedAt(report) ? `Paused at the human gate after level ${pausedAt(report).level}; next: ${listNext(report)}. Open it with plan.mjs gate open ${pausedAt(report).level} and resume the run.` : "";
|
|
786
928
|
const third = pause || stops.length || unmet.length
|
|
@@ -545,7 +545,7 @@ function rowsByIdMap(m, ids) {
|
|
|
545
545
|
// the BM25 side (a precomputed allow-list of matching paths, so the ranking itself runs restricted), in memory
|
|
546
546
|
// for the vector side. Never throws for a provider problem; a query with no FTS tokens simply has no BM25 side.
|
|
547
547
|
export async function searchIndex(m, query, opts = {}) {
|
|
548
|
-
const { k = 8, files = [], provider = null } = opts;
|
|
548
|
+
const { k = 8, files = [], provider = null, detectedProvider = null } = opts;
|
|
549
549
|
|
|
550
550
|
const allowedPaths = files.length
|
|
551
551
|
? new Set(
|
|
@@ -576,14 +576,16 @@ export async function searchIndex(m, query, opts = {}) {
|
|
|
576
576
|
let reason = "no provider configured";
|
|
577
577
|
let providerName = null;
|
|
578
578
|
let vectorRanked = [];
|
|
579
|
+
let vectorsForProvider = 0;
|
|
579
580
|
if (provider) {
|
|
580
581
|
providerName = provider.name;
|
|
581
582
|
let vectorRows = m
|
|
582
583
|
.prepare("SELECT id, path, embedding FROM code_chunks WHERE embedding_model = ? AND embedding_dims = ?")
|
|
583
584
|
.all(provider.model, provider.dims);
|
|
585
|
+
vectorsForProvider = vectorRows.length;
|
|
584
586
|
if (allowedPaths) vectorRows = vectorRows.filter((r) => allowedPaths.has(r.path));
|
|
585
587
|
if (!vectorRows.length) {
|
|
586
|
-
reason = `no vectors stored for ${provider.model}/${provider.dims}`;
|
|
588
|
+
reason = vectorsForProvider > 0 ? "no vectors in the filtered paths" : `no vectors stored for ${provider.model}/${provider.dims}`;
|
|
587
589
|
} else {
|
|
588
590
|
const embedResult = await provider.embed([String(query || "")], { inputType: "query" });
|
|
589
591
|
if (!embedResult.ok) {
|
|
@@ -604,6 +606,19 @@ export async function searchIndex(m, query, opts = {}) {
|
|
|
604
606
|
}
|
|
605
607
|
}
|
|
606
608
|
|
|
609
|
+
// card recall-fallback-warning: a filter that excludes every vector while vectors exist for the provider is
|
|
610
|
+
// not degraded; hybrid counts chunks without a vector for the provider over the whole index.
|
|
611
|
+
const anyVectors = () => m.prepare("SELECT 1 FROM code_chunks WHERE embedding IS NOT NULL LIMIT 1").get() !== undefined;
|
|
612
|
+
const filteredOut = mode === "keyword-only" && !!provider && vectorsForProvider > 0 && reason === "no vectors in the filtered paths";
|
|
613
|
+
const degraded = mode === "keyword-only" && !filteredOut && (!!provider || anyVectors() || !!detectedProvider);
|
|
614
|
+
const coverage = {};
|
|
615
|
+
if (mode === "hybrid") {
|
|
616
|
+
coverage.total = m.prepare("SELECT COUNT(*) AS n FROM code_chunks").get().n;
|
|
617
|
+
coverage.unvectored = coverage.total - m
|
|
618
|
+
.prepare("SELECT COUNT(*) AS n FROM code_chunks WHERE embedding_model = ? AND embedding_dims = ?")
|
|
619
|
+
.get(provider.model, provider.dims).n;
|
|
620
|
+
}
|
|
621
|
+
|
|
607
622
|
const fused = new Map();
|
|
608
623
|
bm25Ranked.forEach((id, idx) => fused.set(id, (fused.get(id) || 0) + 1 / (60 + idx + 1)));
|
|
609
624
|
vectorRanked.forEach((id, idx) => fused.set(id, (fused.get(id) || 0) + 1 / (60 + idx + 1)));
|
|
@@ -625,5 +640,5 @@ export async function searchIndex(m, query, opts = {}) {
|
|
|
625
640
|
})
|
|
626
641
|
.filter(Boolean);
|
|
627
642
|
|
|
628
|
-
return { mode, reason: mode === "hybrid" ? null : reason, provider: providerName, chunks };
|
|
643
|
+
return { mode, reason: mode === "hybrid" ? null : reason, provider: providerName, degraded, ...coverage, chunks };
|
|
629
644
|
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
// The lead's own context reading (card autopilot-unlimited): scripts/context.mjs prints the last assistant line's
|
|
2
|
+
// usage in the session transcript against its model's window, and whether it is at autopilot.contextThreshold.
|
|
3
|
+
// windowFor and contextReading are copied from plugins/doug-gates/lib/agent-context.mjs: the plugins install
|
|
4
|
+
// separately, so doug-flow cannot import across the boundary (as learn.mjs keeps its own local glob copy).
|
|
5
|
+
|
|
6
|
+
import { readFileSync, existsSync } from "node:fs";
|
|
7
|
+
|
|
8
|
+
// 1M for a Fable/Opus/Sonnet 5 model, 200k otherwise (an unknown or missing model errs early, on the smaller window).
|
|
9
|
+
export function windowFor(model) {
|
|
10
|
+
if (typeof model !== "string") return 200000;
|
|
11
|
+
return /fable-5|opus-5|sonnet-5/.test(model) ? 1000000 : 200000;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// The last assistant line with usage in a transcript file, against its model's window. null when the file is
|
|
15
|
+
// missing, unreadable, or has no assistant line carrying usage.
|
|
16
|
+
export function contextReading(file, { windows = windowFor } = {}) {
|
|
17
|
+
if (!file || !existsSync(file)) return null;
|
|
18
|
+
let text;
|
|
19
|
+
try {
|
|
20
|
+
text = readFileSync(file, "utf8");
|
|
21
|
+
} catch {
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
let lastMessage = null;
|
|
25
|
+
for (const line of text.split("\n")) {
|
|
26
|
+
if (!line.trim()) continue;
|
|
27
|
+
let o;
|
|
28
|
+
try {
|
|
29
|
+
o = JSON.parse(line);
|
|
30
|
+
} catch {
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
if (o && o.type === "assistant" && o.message && o.message.usage) lastMessage = o.message;
|
|
34
|
+
}
|
|
35
|
+
if (!lastMessage) return null;
|
|
36
|
+
const u = lastMessage.usage;
|
|
37
|
+
const tokens = (u.input_tokens || 0) + (u.cache_creation_input_tokens || 0) + (u.cache_read_input_tokens || 0);
|
|
38
|
+
const model = lastMessage.model || null;
|
|
39
|
+
const window = windows(model);
|
|
40
|
+
return { tokens, model, window, pct: Math.round((100 * tokens) / window) };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export const CONTEXT_THRESHOLD_DEFAULT = 60;
|
|
44
|
+
|
|
45
|
+
// The configured autopilot.contextThreshold when it is a finite number in 1..100; the default silently when the key
|
|
46
|
+
// (or autopilot) is absent or null; the default plus one warn() call otherwise.
|
|
47
|
+
export function contextThreshold(config, warn = () => {}) {
|
|
48
|
+
const v = config && config.autopilot ? config.autopilot.contextThreshold : undefined;
|
|
49
|
+
if (v === undefined || v === null) return CONTEXT_THRESHOLD_DEFAULT;
|
|
50
|
+
if (typeof v === "number" && Number.isFinite(v) && v >= 1 && v <= 100) return v;
|
|
51
|
+
warn(`autopilot.contextThreshold must be a number from 1 to 100; using ${CONTEXT_THRESHOLD_DEFAULT}`);
|
|
52
|
+
return CONTEXT_THRESHOLD_DEFAULT;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function atThreshold(pct, threshold) {
|
|
56
|
+
return pct >= threshold;
|
|
57
|
+
}
|
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
// Real cost of one doug-implement run, from the local Claude Code transcripts (decision 0005, card cost-per-run).
|
|
2
2
|
// No network: the run's agent ids come from its journal under ~/.claude/projects/<slug>/<session>/subagents/workflows/<run>/,
|
|
3
3
|
// each agent's tokens are summed from its transcript there, and the price comes from the table below.
|
|
4
|
-
import { existsSync, readdirSync, readFileSync } from "node:fs";
|
|
4
|
+
import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
|
|
5
5
|
import { homedir } from "node:os";
|
|
6
|
-
import { join } from "node:path";
|
|
6
|
+
import { basename, join } from "node:path";
|
|
7
7
|
|
|
8
8
|
// USD per million tokens, first-party Claude API list prices. Cache reads are 0.1x input (0.025x on Fable 5.1,
|
|
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
|
-
// https://platform.claude.com/docs/en/about-claude/pricing, fetched 2026-
|
|
12
|
-
export const PRICES_DATED = "2026-
|
|
11
|
+
// https://platform.claude.com/docs/en/about-claude/pricing, fetched 2026-10-04. Update the date with the table.
|
|
12
|
+
export const PRICES_DATED = "2026-10-04";
|
|
13
13
|
export const PRICES = {
|
|
14
14
|
"claude-fable-5-1": { input: 10, output: 50, cacheRead: 0.25, cacheWrite5m: 12.5, cacheWrite1h: 20 },
|
|
15
15
|
"claude-fable-5": { input: 10, output: 50, cacheRead: 1, cacheWrite5m: 12.5, cacheWrite1h: 20 },
|
|
@@ -18,6 +18,7 @@ export const PRICES = {
|
|
|
18
18
|
"claude-opus-4-8": { input: 5, output: 25, cacheRead: 0.5, cacheWrite5m: 6.25, cacheWrite1h: 10 },
|
|
19
19
|
"claude-opus-4-7": { input: 5, output: 25, cacheRead: 0.5, cacheWrite5m: 6.25, cacheWrite1h: 10 },
|
|
20
20
|
"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.2, cacheWrite5m: 2.5, cacheWrite1h: 4 },
|
|
21
22
|
"claude-sonnet-5": { input: 2, output: 10, cacheRead: 0.2, cacheWrite5m: 2.5, cacheWrite1h: 4 },
|
|
22
23
|
"claude-sonnet-4-6": { input: 3, output: 15, cacheRead: 0.3, cacheWrite5m: 3.75, cacheWrite1h: 6 },
|
|
23
24
|
"claude-haiku-4-5": { input: 1, output: 5, cacheRead: 0.1, cacheWrite5m: 1.25, cacheWrite1h: 2 },
|
|
@@ -318,6 +319,64 @@ export function costRun({ runId, claudeDir, projectDir, report, reportPath } = {
|
|
|
318
319
|
};
|
|
319
320
|
}
|
|
320
321
|
|
|
322
|
+
// Card flow-wall-budget: slow tasks flagged after the run, from the runtime's run file (workflowProgress[] agents
|
|
323
|
+
// carry startedAt and durationMs). Detection only; the budget is the report's wallMinutes. A task's wall and a
|
|
324
|
+
// pass's wall are spans (latest end minus earliest start), never sums, since agents of a pass run alongside.
|
|
325
|
+
export const PASS_WALL_SHARE = 0.75;
|
|
326
|
+
export const STAGE_WALL_SHARE = 0.25;
|
|
327
|
+
|
|
328
|
+
export function runWall(run, { wallMinutes } = {}) {
|
|
329
|
+
const MIN = 60000;
|
|
330
|
+
const result = (extra) => ({ measured: false, reason: null, wallMinutes, unverified: [], tasks: [], flags: [], ...extra });
|
|
331
|
+
const agents = run && Array.isArray(run.workflowProgress) ? run.workflowProgress.filter((p) => p && p.type === "workflow_agent") : [];
|
|
332
|
+
if (!agents.length) return result({ reason: "no run file" });
|
|
333
|
+
const timed = (a) => typeof a.startedAt === "number" && typeof a.durationMs === "number";
|
|
334
|
+
if (!agents.some(timed)) return result({ reason: "no agent times" });
|
|
335
|
+
const taskLimit = wallMinutes * MIN;
|
|
336
|
+
const passLimit = PASS_WALL_SHARE * taskLimit;
|
|
337
|
+
const stageLimit = STAGE_WALL_SHARE * taskLimit;
|
|
338
|
+
const byTask = new Map();
|
|
339
|
+
const stageFlags = [];
|
|
340
|
+
for (const a of agents) {
|
|
341
|
+
const label = String(a.label || "");
|
|
342
|
+
const parsed = parseLabel(label.replace(/:retry$/, ""));
|
|
343
|
+
if (timed(a) && a.durationMs > stageLimit) stageFlags.push({ kind: "stage", label, ms: a.durationMs, limitMs: stageLimit });
|
|
344
|
+
if (!parsed.task || /^level-\d+$/.test(parsed.task)) continue;
|
|
345
|
+
const t = byTask.get(parsed.task) || { task: parsed.task, agents: [], missing: false };
|
|
346
|
+
if (timed(a)) t.agents.push({ pass: parsed.pass ?? 1, from: a.startedAt, to: a.startedAt + a.durationMs });
|
|
347
|
+
else t.missing = true;
|
|
348
|
+
byTask.set(parsed.task, t);
|
|
349
|
+
}
|
|
350
|
+
const span = (list) => Math.max(...list.map((x) => x.to)) - Math.min(...list.map((x) => x.from));
|
|
351
|
+
const unverified = [];
|
|
352
|
+
const tasks = [];
|
|
353
|
+
const flags = [];
|
|
354
|
+
for (const t of byTask.values()) {
|
|
355
|
+
if (t.missing || !t.agents.length) {
|
|
356
|
+
unverified.push(t.task);
|
|
357
|
+
continue;
|
|
358
|
+
}
|
|
359
|
+
const passNums = [...new Set(t.agents.map((x) => x.pass))].sort((x, y) => x - y);
|
|
360
|
+
const passes = passNums.map((n) => ({ pass: n, ms: span(t.agents.filter((x) => x.pass === n)) }));
|
|
361
|
+
const ms = span(t.agents);
|
|
362
|
+
tasks.push({ task: t.task, ms, passes, stages: [] });
|
|
363
|
+
if (ms > taskLimit) flags.push({ kind: "task", label: t.task, ms, limitMs: taskLimit });
|
|
364
|
+
for (const p of passes) if (p.ms > passLimit) flags.push({ kind: "pass", label: t.task, pass: p.pass, ms: p.ms, limitMs: passLimit });
|
|
365
|
+
}
|
|
366
|
+
return result({ measured: true, unverified, tasks, flags: [...flags, ...stageFlags] });
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
// runWall for a run id: finds the run file next to the journal; never throws, an unreadable run is not measured.
|
|
370
|
+
export function wallForRun({ runId, claudeDir, projectDir, wallMinutes } = {}) {
|
|
371
|
+
try {
|
|
372
|
+
const found = findRunDir({ runId, claudeDir, projectDir });
|
|
373
|
+
if (!found) return runWall(null, { wallMinutes });
|
|
374
|
+
return runWall(readJson(found.runFile), { wallMinutes });
|
|
375
|
+
} catch {
|
|
376
|
+
return runWall(null, { wallMinutes });
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
321
380
|
// A batch run's cost split per card (card parallel-cards): every agent whose task is one of a card's plan tasks
|
|
322
381
|
// counts for that card; the shared stages (integrate, the level adversary: task `level-<n>`) are listed once as
|
|
323
382
|
// `shared`; agents on no card's task are `other`. Nothing estimated: unpriced agents are counted, not summed.
|
|
@@ -359,6 +418,72 @@ export function renderCostByCard(byCard) {
|
|
|
359
418
|
const fmtInt = (n) => String(n).replace(/\B(?=(\d{3})+(?!\d))/g, ",");
|
|
360
419
|
const fmtUsd = (n) => (n === null ? "unpriced" : `$${n.toFixed(2)}`);
|
|
361
420
|
|
|
421
|
+
// The top-level transcript path for a session id or "latest" (the most recently modified one in the project's slug
|
|
422
|
+
// directory). Throws when none is found. Used by costSession and scripts/context.mjs.
|
|
423
|
+
export function sessionTranscriptPath({ session, claudeDir = join(homedir(), ".claude"), projectDir = process.cwd() } = {}) {
|
|
424
|
+
if (!session) throw new Error("a session id or latest is required");
|
|
425
|
+
const slugDir = join(claudeDir, "projects", projectSlug(projectDir));
|
|
426
|
+
let id = session;
|
|
427
|
+
if (session === "latest") {
|
|
428
|
+
let best = null;
|
|
429
|
+
if (existsSync(slugDir)) {
|
|
430
|
+
for (const e of readdirSync(slugDir, { withFileTypes: true })) {
|
|
431
|
+
if (!e.isFile() || !e.name.endsWith(".jsonl")) continue;
|
|
432
|
+
const mtime = statSync(join(slugDir, e.name)).mtimeMs;
|
|
433
|
+
if (!best || mtime > best.mtime) best = { id: e.name.slice(0, -".jsonl".length), mtime };
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
if (!best) throw new Error(`no session transcript under ${slugDir}`);
|
|
437
|
+
id = best.id;
|
|
438
|
+
}
|
|
439
|
+
const top = join(slugDir, `${id}.jsonl`);
|
|
440
|
+
if (!existsSync(top)) throw new Error(`no transcript for session ${id} under ${slugDir}`);
|
|
441
|
+
return top;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
// The cost of one Claude Code session (card autopilot): its top-level transcript plus every *.jsonl under
|
|
445
|
+
// <slug>/<session>/subagents/, recursively (plain agents and workflows/wf_*/ agents). `session` is an id or "latest",
|
|
446
|
+
// the most recently modified top-level transcript in the project's slug directory. Throws when none is found.
|
|
447
|
+
// Workflow journal.jsonl files carry no usage lines and add nothing.
|
|
448
|
+
export function costSession({ session, claudeDir = join(homedir(), ".claude"), projectDir = process.cwd() } = {}) {
|
|
449
|
+
const top = sessionTranscriptPath({ session, claudeDir, projectDir });
|
|
450
|
+
const slugDir = join(claudeDir, "projects", projectSlug(projectDir));
|
|
451
|
+
const id = basename(top, ".jsonl");
|
|
452
|
+
const files = [top];
|
|
453
|
+
const walk = (dir) => {
|
|
454
|
+
for (const e of readdirSync(dir, { withFileTypes: true })) {
|
|
455
|
+
const p = join(dir, e.name);
|
|
456
|
+
if (e.isDirectory()) walk(p);
|
|
457
|
+
else if (e.isFile() && e.name.endsWith(".jsonl")) files.push(p);
|
|
458
|
+
}
|
|
459
|
+
};
|
|
460
|
+
const subDir = join(slugDir, id, "subagents");
|
|
461
|
+
if (existsSync(subDir)) walk(subDir);
|
|
462
|
+
const usage = emptyUsage();
|
|
463
|
+
let usd = 0;
|
|
464
|
+
let unpriced = 0;
|
|
465
|
+
const notes = [];
|
|
466
|
+
for (const f of files) {
|
|
467
|
+
const t = transcriptUsage(f);
|
|
468
|
+
addUsage(usage, t.usage);
|
|
469
|
+
if (!t.messages) continue;
|
|
470
|
+
const price = t.model ? priceUsage(t.usage, t.model) : null;
|
|
471
|
+
if (price === null) {
|
|
472
|
+
unpriced++;
|
|
473
|
+
notes.push(`${f}: model ${t.model || "unknown"} is not in the price table; not priced`);
|
|
474
|
+
} else usd += price;
|
|
475
|
+
}
|
|
476
|
+
return { session: id, files: files.length, total: { usage, usd, unpriced, agents: files.length }, pricesDated: PRICES_DATED, notes };
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
export function renderSession(result) {
|
|
480
|
+
const t = result.total;
|
|
481
|
+
const lines = [`Session ${result.session}: ${result.files} transcript${result.files === 1 ? "" : "s"}.`];
|
|
482
|
+
lines.push(`Session total: ${fmtInt(t.usage.input)} input, ${fmtInt(t.usage.output)} output, ${fmtInt(t.usage.cacheRead)} cache read, ${fmtInt(t.usage.cacheWrite)} cache write tokens; ${fmtUsd(t.usd)}${t.unpriced ? ` (${t.unpriced} transcript(s) unpriced)` : ""} at list prices dated ${result.pricesDated}.`);
|
|
483
|
+
for (const n of result.notes) lines.push(`Note: ${n}.`);
|
|
484
|
+
return lines.join("\n") + "\n";
|
|
485
|
+
}
|
|
486
|
+
|
|
362
487
|
export function renderCost(result) {
|
|
363
488
|
const lines = [];
|
|
364
489
|
const when = result.startedAt ? `, ${result.startedAt}` : "";
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
{
|
|
2
|
+
"margin": 0.15,
|
|
3
|
+
"paraphrase": [
|
|
4
|
+
[
|
|
5
|
+
"The pre-commit hook re-runs the whole gate, so committing without waiting for tests wastes the developer's time.",
|
|
6
|
+
"Skipping ahead of the test run before a commit is pointless because the hook repeats every check anyway."
|
|
7
|
+
],
|
|
8
|
+
[
|
|
9
|
+
"Worktrees under .claude/worktrees are gitignored and safe to delete once a task lands.",
|
|
10
|
+
"Once a task merges, the per-task checkout folder can be removed since it was never tracked by git."
|
|
11
|
+
],
|
|
12
|
+
[
|
|
13
|
+
"Plan verify commands should call pnpm test:unit, never the full suite, because pnpm test spawns a live Codex call.",
|
|
14
|
+
"Use the unit-only test script for a plan's verify step; running the full suite triggers an actual Codex invocation."
|
|
15
|
+
],
|
|
16
|
+
[
|
|
17
|
+
"A worker nearing its context limit should stop at a clean boundary and report a partial result instead of guessing.",
|
|
18
|
+
"When an agent's context window is almost full, it ought to pause at a good stopping point and flag the work as unfinished."
|
|
19
|
+
],
|
|
20
|
+
[
|
|
21
|
+
"node:sqlite requires Node 22.16 or newer, so an older runtime must fail with one clear message rather than a stack trace.",
|
|
22
|
+
"Running on a Node version below 22.16 needs to produce a single readable error, not a crash dump, since the built-in SQLite module needs that version."
|
|
23
|
+
],
|
|
24
|
+
[
|
|
25
|
+
"Force-deleting a git branch is blocked by the bash guard; merge it first and use the plain delete flag.",
|
|
26
|
+
"The shell guard refuses a hard branch removal, so branches must be merged before a normal delete works."
|
|
27
|
+
],
|
|
28
|
+
[
|
|
29
|
+
"Lessons are append-only: a later insight can supersede an older one but the original text is never rewritten.",
|
|
30
|
+
"The store never edits a lesson's wording after the fact; a newer entry can mark an old one as replaced instead."
|
|
31
|
+
],
|
|
32
|
+
[
|
|
33
|
+
"Reviewers should flag a pull request that widens scope beyond what the plan's tasks actually own.",
|
|
34
|
+
"When a change touches files outside what a task was granted, that overreach is exactly what review should catch."
|
|
35
|
+
],
|
|
36
|
+
[
|
|
37
|
+
"The adversary review runs on Codex by default, falling back to an Opus pass when Codex can't be reached.",
|
|
38
|
+
"Codex normally does the adversarial check, but the workflow switches to Opus whenever Codex is unavailable."
|
|
39
|
+
],
|
|
40
|
+
[
|
|
41
|
+
"Auto memory import should be idempotent, never rewriting files it did not itself create in the memory directory.",
|
|
42
|
+
"Re-running the import of automatic memory must not touch files outside its own directory and must produce the same result each time."
|
|
43
|
+
],
|
|
44
|
+
[
|
|
45
|
+
"A swarm's lead splits one task into worker briefs and merges their results before verification starts.",
|
|
46
|
+
"In a swarm, the lead agent breaks a task into pieces for workers and combines their output ahead of the verify stage."
|
|
47
|
+
],
|
|
48
|
+
[
|
|
49
|
+
"Keep CLAUDE.md under sixty lines since hooks already enforce most of the guardrails it would otherwise restate.",
|
|
50
|
+
"The instructions file should stay short, under sixty lines, because the hook layer already covers most of what it would repeat."
|
|
51
|
+
]
|
|
52
|
+
],
|
|
53
|
+
"unrelated": [
|
|
54
|
+
[
|
|
55
|
+
"The pre-commit hook re-runs the whole test gate before allowing a commit to land.",
|
|
56
|
+
"Ollama serves an OpenAI-compatible /v1/embeddings endpoint on port 11434 by default."
|
|
57
|
+
],
|
|
58
|
+
[
|
|
59
|
+
"Worktrees under .claude/worktrees are gitignored and removed once a task merges.",
|
|
60
|
+
"A pull request title should stay under seventy characters so it reads well in a list view."
|
|
61
|
+
],
|
|
62
|
+
[
|
|
63
|
+
"pnpm test:unit skips the live Codex test so a plan's verify step never spends real usage.",
|
|
64
|
+
"Vectors returned by the embedding provider are L2-normalized before they are stored in the database."
|
|
65
|
+
],
|
|
66
|
+
[
|
|
67
|
+
"A worker close to its context limit should stop at a clean boundary and report a partial result.",
|
|
68
|
+
"The board's Ready column lists the next three small cards ahead of the larger context-window card."
|
|
69
|
+
],
|
|
70
|
+
[
|
|
71
|
+
"node:sqlite needs Node 22.16 or newer for the built-in FTS5 support the lessons table relies on.",
|
|
72
|
+
"The reflect command proposes at most three new lessons per report, filtered by a cheap-row model pass."
|
|
73
|
+
],
|
|
74
|
+
[
|
|
75
|
+
"Force-deleting a git branch is blocked by the bash guard; use a normal merge and delete instead.",
|
|
76
|
+
"Voyage AI's embeddings endpoint accepts an output_dimension parameter to control vector width."
|
|
77
|
+
],
|
|
78
|
+
[
|
|
79
|
+
"Lessons are append-only; an older entry can be superseded but its text is never rewritten.",
|
|
80
|
+
"The doctor subcommand probes a configured provider or a local Ollama installation and prints what it found."
|
|
81
|
+
],
|
|
82
|
+
[
|
|
83
|
+
"Review should catch a change that touches files outside what its task was granted to own.",
|
|
84
|
+
"A hand-track landing records the card id, commit, wall-clock time, and gate line, but has no report file."
|
|
85
|
+
],
|
|
86
|
+
[
|
|
87
|
+
"The adversary review defaults to Codex and falls back to an Opus pass when Codex cannot run.",
|
|
88
|
+
"Recall fuses BM25 keyword scores with cosine similarity using reciprocal rank fusion when a provider is configured."
|
|
89
|
+
],
|
|
90
|
+
[
|
|
91
|
+
"Importing auto memory is idempotent and never rewrites files outside its own memory directory.",
|
|
92
|
+
"The bash guard also blocks deleting a remote branch directly; ask first or merge it locally."
|
|
93
|
+
],
|
|
94
|
+
[
|
|
95
|
+
"A swarm's lead agent splits a task into worker briefs and merges their output before verification.",
|
|
96
|
+
"CLAUDE.md documents only what hooks cannot already enforce, and stays under sixty lines."
|
|
97
|
+
],
|
|
98
|
+
[
|
|
99
|
+
"Keep CLAUDE.md under sixty lines because the hook layer already enforces most of the guardrails.",
|
|
100
|
+
"A citation of the form path:line is re-checked against the checkout only when the directory contains a .doug folder."
|
|
101
|
+
]
|
|
102
|
+
]
|
|
103
|
+
}
|