@dougharness/cli 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/dist/ablate.d.ts +96 -0
- package/dist/ablate.js +440 -0
- package/dist/ablate.js.map +1 -0
- package/dist/apply.d.ts +2 -0
- package/dist/apply.js +19 -0
- package/dist/apply.js.map +1 -0
- package/dist/bin.d.ts +2 -0
- package/dist/bin.js +80 -0
- package/dist/bin.js.map +1 -0
- package/dist/board-page.d.ts +15 -0
- package/dist/board-page.js +27 -0
- package/dist/board-page.js.map +1 -0
- package/dist/board-serve-process.d.ts +24 -0
- package/dist/board-serve-process.js +227 -0
- package/dist/board-serve-process.js.map +1 -0
- package/dist/board-serve.d.ts +9 -0
- package/dist/board-serve.js +302 -0
- package/dist/board-serve.js.map +1 -0
- package/dist/board.d.ts +6 -0
- package/dist/board.js +515 -0
- package/dist/board.js.map +1 -0
- package/dist/detect/fs.d.ts +6 -0
- package/dist/detect/fs.js +50 -0
- package/dist/detect/fs.js.map +1 -0
- package/dist/detect/index.d.ts +3 -0
- package/dist/detect/index.js +13 -0
- package/dist/detect/index.js.map +1 -0
- package/dist/detect/node.d.ts +3 -0
- package/dist/detect/node.js +179 -0
- package/dist/detect/node.js.map +1 -0
- package/dist/detect/python.d.ts +2 -0
- package/dist/detect/python.js +388 -0
- package/dist/detect/python.js.map +1 -0
- package/dist/detect/repo.d.ts +2 -0
- package/dist/detect/repo.js +102 -0
- package/dist/detect/repo.js.map +1 -0
- package/dist/detect/types.d.ts +98 -0
- package/dist/detect/types.js +2 -0
- package/dist/detect/types.js.map +1 -0
- package/dist/diff.d.ts +12 -0
- package/dist/diff.js +105 -0
- package/dist/diff.js.map +1 -0
- package/dist/generate/agents.d.ts +19 -0
- package/dist/generate/agents.js +184 -0
- package/dist/generate/agents.js.map +1 -0
- package/dist/generate/claude-md.d.ts +9 -0
- package/dist/generate/claude-md.js +111 -0
- package/dist/generate/claude-md.js.map +1 -0
- package/dist/generate/config.d.ts +51 -0
- package/dist/generate/config.js +155 -0
- package/dist/generate/config.js.map +1 -0
- package/dist/generate/proposal.d.ts +11 -0
- package/dist/generate/proposal.js +175 -0
- package/dist/generate/proposal.js.map +1 -0
- package/dist/generate/settings.d.ts +35 -0
- package/dist/generate/settings.js +213 -0
- package/dist/generate/settings.js.map +1 -0
- package/dist/generate/skills.d.ts +8 -0
- package/dist/generate/skills.js +264 -0
- package/dist/generate/skills.js.map +1 -0
- package/dist/init.d.ts +10 -0
- package/dist/init.js +69 -0
- package/dist/init.js.map +1 -0
- package/dist/trace.d.ts +9 -0
- package/dist/trace.js +106 -0
- package/dist/trace.js.map +1 -0
- package/node_modules/@dougharness/flow/.claude-plugin/plugin.json +10 -0
- package/node_modules/@dougharness/flow/LICENSE +21 -0
- package/node_modules/@dougharness/flow/agents/adversary-claude.md +33 -0
- package/node_modules/@dougharness/flow/agents/adversary.md +29 -0
- package/node_modules/@dougharness/flow/agents/implementer.md +23 -0
- package/node_modules/@dougharness/flow/agents/lead.md +35 -0
- package/node_modules/@dougharness/flow/agents/planner.md +70 -0
- package/node_modules/@dougharness/flow/agents/researcher.md +31 -0
- package/node_modules/@dougharness/flow/agents/reviewer.md +26 -0
- package/node_modules/@dougharness/flow/agents/verifier.md +24 -0
- package/node_modules/@dougharness/flow/lib/board.mjs +840 -0
- package/node_modules/@dougharness/flow/lib/code-index.mjs +629 -0
- package/node_modules/@dougharness/flow/lib/cost.mjs +392 -0
- package/node_modules/@dougharness/flow/lib/decisions.mjs +314 -0
- package/node_modules/@dougharness/flow/lib/embeddings.mjs +539 -0
- package/node_modules/@dougharness/flow/lib/land.mjs +219 -0
- package/node_modules/@dougharness/flow/lib/learn.mjs +742 -0
- package/node_modules/@dougharness/flow/lib/memory-import.mjs +145 -0
- package/node_modules/@dougharness/flow/lib/memory.mjs +1641 -0
- package/node_modules/@dougharness/flow/lib/models.mjs +148 -0
- package/node_modules/@dougharness/flow/lib/plan.mjs +773 -0
- package/node_modules/@dougharness/flow/lib/rehearse.mjs +825 -0
- package/node_modules/@dougharness/flow/lib/worktrees.mjs +215 -0
- package/node_modules/@dougharness/flow/package.json +19 -0
- package/node_modules/@dougharness/flow/scripts/board.mjs +260 -0
- package/node_modules/@dougharness/flow/scripts/cost.mjs +66 -0
- package/node_modules/@dougharness/flow/scripts/learn.mjs +161 -0
- package/node_modules/@dougharness/flow/scripts/memory.mjs +1274 -0
- package/node_modules/@dougharness/flow/scripts/plan.mjs +547 -0
- package/node_modules/@dougharness/flow/scripts/rehearse.mjs +129 -0
- package/node_modules/@dougharness/flow/skills/core-next/SKILL.md +92 -0
- package/node_modules/@dougharness/flow/skills/doug-approve/SKILL.md +15 -0
- package/node_modules/@dougharness/flow/skills/doug-decide/SKILL.md +26 -0
- package/node_modules/@dougharness/flow/skills/doug-implement/SKILL.md +22 -0
- package/node_modules/@dougharness/flow/skills/doug-learn/SKILL.md +20 -0
- package/node_modules/@dougharness/flow/skills/doug-next/SKILL.md +87 -0
- package/node_modules/@dougharness/flow/skills/doug-plan/SKILL.md +22 -0
- package/node_modules/@dougharness/flow/skills/doug-swarm/SKILL.md +55 -0
- package/node_modules/@dougharness/flow/skills/harness-fix/SKILL.md +85 -0
- package/node_modules/@dougharness/flow/skills/research/SKILL.md +41 -0
- package/node_modules/@dougharness/flow/skills/run-report/SKILL.md +51 -0
- package/node_modules/@dougharness/flow/skills/swarm-launch/SKILL.md +27 -0
- package/node_modules/@dougharness/flow/workflows/doug-implement.js +2687 -0
- package/node_modules/@dougharness/gates/.claude-plugin/plugin.json +10 -0
- package/node_modules/@dougharness/gates/LICENSE +21 -0
- package/node_modules/@dougharness/gates/hooks/hooks.json +184 -0
- package/node_modules/@dougharness/gates/lib/agent-context.mjs +111 -0
- package/node_modules/@dougharness/gates/lib/anchor.mjs +110 -0
- package/node_modules/@dougharness/gates/lib/baseline.mjs +160 -0
- package/node_modules/@dougharness/gates/lib/bash-rules.mjs +1522 -0
- package/node_modules/@dougharness/gates/lib/block-ledger.mjs +127 -0
- package/node_modules/@dougharness/gates/lib/checkpoint.mjs +97 -0
- package/node_modules/@dougharness/gates/lib/config.mjs +168 -0
- package/node_modules/@dougharness/gates/lib/evidence.mjs +59 -0
- package/node_modules/@dougharness/gates/lib/glob.mjs +86 -0
- package/node_modules/@dougharness/gates/lib/io.mjs +91 -0
- package/node_modules/@dougharness/gates/lib/paths.mjs +101 -0
- package/node_modules/@dougharness/gates/lib/proposals.mjs +201 -0
- package/node_modules/@dougharness/gates/lib/run.mjs +147 -0
- package/node_modules/@dougharness/gates/lib/scope.mjs +117 -0
- package/node_modules/@dougharness/gates/lib/secret-rules.mjs +268 -0
- package/node_modules/@dougharness/gates/lib/state.mjs +114 -0
- package/node_modules/@dougharness/gates/lib/trace.mjs +196 -0
- package/node_modules/@dougharness/gates/package.json +17 -0
- package/node_modules/@dougharness/gates/scripts/edit-loop.mjs +39 -0
- package/node_modules/@dougharness/gates/scripts/format-on-edit.mjs +45 -0
- package/node_modules/@dougharness/gates/scripts/guard-bash.mjs +32 -0
- package/node_modules/@dougharness/gates/scripts/protect-paths.mjs +49 -0
- package/node_modules/@dougharness/gates/scripts/reanchor.mjs +81 -0
- package/node_modules/@dougharness/gates/scripts/research-cap.mjs +295 -0
- package/node_modules/@dougharness/gates/scripts/secret-scan.mjs +26 -0
- package/node_modules/@dougharness/gates/scripts/session-start-baseline.mjs +57 -0
- package/node_modules/@dougharness/gates/scripts/statusline.mjs +133 -0
- package/node_modules/@dougharness/gates/scripts/stop-gate.mjs +1422 -0
- package/node_modules/@dougharness/gates/scripts/trace.mjs +75 -0
- package/package.json +33 -0
- package/templates/board-app.js +533 -0
|
@@ -0,0 +1,392 @@
|
|
|
1
|
+
// Real cost of one doug-implement run, from the local Claude Code transcripts (decision 0005, card cost-per-run).
|
|
2
|
+
// No network: the run's agent ids come from its journal under ~/.claude/projects/<slug>/<session>/subagents/workflows/<run>/,
|
|
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";
|
|
5
|
+
import { homedir } from "node:os";
|
|
6
|
+
import { join } from "node:path";
|
|
7
|
+
|
|
8
|
+
// USD per million tokens, first-party Claude API list prices. Cache reads are 0.1x input (0.025x on Fable 5.1,
|
|
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
|
+
// input 4, output 20, cacheRead 0.2, cacheWrite5m 5, cacheWrite1h 8; source
|
|
11
|
+
// https://platform.claude.com/docs/en/about-claude/pricing, fetched 2026-09-23. Update the date with the table.
|
|
12
|
+
export const PRICES_DATED = "2026-09-23";
|
|
13
|
+
export const PRICES = {
|
|
14
|
+
"claude-fable-5-1": { input: 10, output: 50, cacheRead: 0.25, cacheWrite5m: 12.5, cacheWrite1h: 20 },
|
|
15
|
+
"claude-fable-5": { input: 10, output: 50, cacheRead: 1, cacheWrite5m: 12.5, cacheWrite1h: 20 },
|
|
16
|
+
"claude-opus-5-5": { input: 4, output: 20, cacheRead: 0.2, cacheWrite5m: 5, cacheWrite1h: 8 },
|
|
17
|
+
"claude-opus-5": { input: 5, output: 25, cacheRead: 0.5, cacheWrite5m: 6.25, cacheWrite1h: 10 },
|
|
18
|
+
"claude-opus-4-8": { input: 5, output: 25, cacheRead: 0.5, cacheWrite5m: 6.25, cacheWrite1h: 10 },
|
|
19
|
+
"claude-opus-4-7": { input: 5, output: 25, cacheRead: 0.5, cacheWrite5m: 6.25, cacheWrite1h: 10 },
|
|
20
|
+
"claude-opus-4-6": { input: 5, output: 25, cacheRead: 0.5, cacheWrite5m: 6.25, cacheWrite1h: 10 },
|
|
21
|
+
"claude-sonnet-5": { input: 2, output: 10, cacheRead: 0.2, cacheWrite5m: 2.5, cacheWrite1h: 4 },
|
|
22
|
+
"claude-sonnet-4-6": { input: 3, output: 15, cacheRead: 0.3, cacheWrite5m: 3.75, cacheWrite1h: 6 },
|
|
23
|
+
"claude-haiku-4-5": { input: 1, output: 5, cacheRead: 0.1, cacheWrite5m: 1.25, cacheWrite1h: 2 },
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
// Card adversary-usage-in-report (decision 0007 follow-up): Codex's own tokens, from a workflow report's relayed
|
|
27
|
+
// ReviewResult.usage/durationMs (agents/adversary.md, doug-implement.js), priced separately from the Claude table
|
|
28
|
+
// above because they are a different vendor's tokens on a different model. USD per million tokens, short-context,
|
|
29
|
+
// uncached rates. Source: https://developers.openai.com/api/docs/pricing (fetched 2026-09-19; the page shows no
|
|
30
|
+
// last-updated date, so CODEX_PRICES_DATED is the fetch date). unverified: that the Codex CLI slug (e.g.
|
|
31
|
+
// "gpt-5.6-sol") is literally the API model id used by that pricing page - no documented mapping was found;
|
|
32
|
+
// unverified: the short/long context threshold for these rows, so a review's call is assumed short; unverified: no
|
|
33
|
+
// cached-input split exists in ReviewResult.usage, so all input is priced at the uncached rate, an upper bound.
|
|
34
|
+
export const CODEX_PRICES_DATED = "2026-09-19";
|
|
35
|
+
export const CODEX_PRICES = {
|
|
36
|
+
"gpt-5.6-sol": { input: 4, output: 20 },
|
|
37
|
+
"gpt-5.5": { input: 5, output: 30 },
|
|
38
|
+
"gpt-5.6-terra": { input: 2, output: 12 },
|
|
39
|
+
"gpt-5.6-luna": { input: 0.2, output: 1.2 },
|
|
40
|
+
"gpt-6-astra": { input: 10, output: 50 },
|
|
41
|
+
};
|
|
42
|
+
// ReviewResult.model (packages/doug-codex/src/schema.ts) is null unless codex-review was run with --model, which
|
|
43
|
+
// the workflow's command never passes, so the model a review actually ran on is not itself in the report; this is
|
|
44
|
+
// the assumption the printed Codex line names: the price assumes the Codex CLI's configured default model, gpt-5.6-sol.
|
|
45
|
+
export const CODEX_DEFAULT_MODEL = "gpt-5.6-sol";
|
|
46
|
+
|
|
47
|
+
// "claude-haiku-4-5-20251001" and "claude-opus-5[1m]" price as "claude-haiku-4-5" and "claude-opus-5".
|
|
48
|
+
export function normalizeModel(model) {
|
|
49
|
+
return String(model || "").replace(/\[[^\]]*\]$/, "").replace(/-\d{8}$/, "");
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function emptyUsage() {
|
|
53
|
+
return { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cacheWrite1h: 0 };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function addUsage(a, b) {
|
|
57
|
+
for (const k of Object.keys(a)) a[k] += b[k] || 0;
|
|
58
|
+
return a;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Price one usage record for a model; null when the model is not in the table.
|
|
62
|
+
export function priceUsage(usage, model) {
|
|
63
|
+
const p = PRICES[normalizeModel(model)];
|
|
64
|
+
if (!p) return null;
|
|
65
|
+
const write5m = Math.max(0, usage.cacheWrite - usage.cacheWrite1h);
|
|
66
|
+
return (usage.input * p.input + usage.output * p.output + usage.cacheRead * p.cacheRead + write5m * p.cacheWrite5m + usage.cacheWrite1h * p.cacheWrite1h) / 1e6;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// The transcript writes one line per content block of an API response, each carrying that response's usage (and
|
|
70
|
+
// the early blocks a partial output count), so usage is taken once per message id, the largest value seen per field.
|
|
71
|
+
export function transcriptUsage(file) {
|
|
72
|
+
const perMessage = new Map();
|
|
73
|
+
const models = new Map();
|
|
74
|
+
for (const line of readFileSync(file, "utf8").split("\n")) {
|
|
75
|
+
if (!line.trim()) continue;
|
|
76
|
+
let o;
|
|
77
|
+
try {
|
|
78
|
+
o = JSON.parse(line);
|
|
79
|
+
} catch {
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
const m = o && o.message;
|
|
83
|
+
if (o.type !== "assistant" || !m || !m.usage) continue;
|
|
84
|
+
const id = m.id || o.requestId || o.uuid;
|
|
85
|
+
const u = m.usage;
|
|
86
|
+
const cur = perMessage.get(id) || { ...emptyUsage(), model: m.model || null };
|
|
87
|
+
cur.input = Math.max(cur.input, u.input_tokens || 0);
|
|
88
|
+
cur.output = Math.max(cur.output, u.output_tokens || 0);
|
|
89
|
+
cur.cacheRead = Math.max(cur.cacheRead, u.cache_read_input_tokens || 0);
|
|
90
|
+
cur.cacheWrite = Math.max(cur.cacheWrite, u.cache_creation_input_tokens || 0);
|
|
91
|
+
cur.cacheWrite1h = Math.max(cur.cacheWrite1h, (u.cache_creation && u.cache_creation.ephemeral_1h_input_tokens) || 0);
|
|
92
|
+
if (m.model) cur.model = m.model;
|
|
93
|
+
perMessage.set(id, cur);
|
|
94
|
+
}
|
|
95
|
+
const usage = emptyUsage();
|
|
96
|
+
for (const v of perMessage.values()) {
|
|
97
|
+
addUsage(usage, v);
|
|
98
|
+
models.set(v.model, (models.get(v.model) || 0) + 1);
|
|
99
|
+
}
|
|
100
|
+
// The agent's model is the one most of its messages ran on (a transcript names one model in practice).
|
|
101
|
+
const model = [...models.entries()].sort((a, b) => b[1] - a[1]).map(([m]) => m)[0] || null;
|
|
102
|
+
return { usage, model, messages: perMessage.size };
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// The workflow's label for an agent: `stage:task[:pass]` (`integrate:level-0`, `fix:agents-generator:2`). A swarm's
|
|
106
|
+
// workers are `worker:task:n` (card swarm-lead): the trailing number is the worker, not a pass, so it is returned as
|
|
107
|
+
// `worker` and the agent still groups under its task.
|
|
108
|
+
export function parseLabel(label) {
|
|
109
|
+
const parts = String(label || "").split(":");
|
|
110
|
+
const stage = parts[0] || null;
|
|
111
|
+
let pass = null;
|
|
112
|
+
let worker = null;
|
|
113
|
+
if (parts.length > 2 && /^\d+$/.test(parts[parts.length - 1])) {
|
|
114
|
+
const n = Number(parts.pop());
|
|
115
|
+
if (stage === "worker") worker = n;
|
|
116
|
+
else pass = n;
|
|
117
|
+
}
|
|
118
|
+
const task = parts.slice(1).join(":") || null;
|
|
119
|
+
return stage === "worker" ? { stage, task, pass, worker } : { stage, task, pass };
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export function projectSlug(projectDir) {
|
|
123
|
+
return String(projectDir).replace(/[^A-Za-z0-9]/g, "-");
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Locate a run's journal directory: the project's own slug first, then every other project under ~/.claude/projects.
|
|
127
|
+
export function findRunDir({ runId, claudeDir = join(homedir(), ".claude"), projectDir = process.cwd() }) {
|
|
128
|
+
const projects = join(claudeDir, "projects");
|
|
129
|
+
if (!existsSync(projects)) return null;
|
|
130
|
+
const slug = projectSlug(projectDir);
|
|
131
|
+
const order = [slug, ...readdirSync(projects).filter((d) => d !== slug)];
|
|
132
|
+
for (const proj of order) {
|
|
133
|
+
const projDir = join(projects, proj);
|
|
134
|
+
if (!existsSync(projDir)) continue;
|
|
135
|
+
let sessions;
|
|
136
|
+
try {
|
|
137
|
+
sessions = readdirSync(projDir, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name);
|
|
138
|
+
} catch {
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
for (const session of sessions) {
|
|
142
|
+
const dir = join(projDir, session, "subagents", "workflows", runId);
|
|
143
|
+
if (existsSync(join(dir, "journal.jsonl"))) return { dir, session, project: proj, runFile: join(projDir, session, "workflows", `${runId}.json`) };
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
return null;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function readJournal(dir) {
|
|
150
|
+
const agents = new Map();
|
|
151
|
+
for (const line of readFileSync(join(dir, "journal.jsonl"), "utf8").split("\n")) {
|
|
152
|
+
if (!line.trim()) continue;
|
|
153
|
+
let o;
|
|
154
|
+
try {
|
|
155
|
+
o = JSON.parse(line);
|
|
156
|
+
} catch {
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
159
|
+
if (!o.agentId) continue;
|
|
160
|
+
const a = agents.get(o.agentId) || { agentId: o.agentId, taskId: null };
|
|
161
|
+
if (o.type === "result" && o.result && typeof o.result.taskId === "string") a.taskId = o.result.taskId;
|
|
162
|
+
agents.set(o.agentId, a);
|
|
163
|
+
}
|
|
164
|
+
return [...agents.values()];
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function readJson(file) {
|
|
168
|
+
try {
|
|
169
|
+
return JSON.parse(readFileSync(file, "utf8"));
|
|
170
|
+
} catch {
|
|
171
|
+
return null;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Card adversary-usage-in-report: walks a workflow report's levels (produced by doug-implement.js: each task's
|
|
176
|
+
// `adversary` and each level's `levelAdversary` carry usage/durationMs copied from ReviewResult) and sums the
|
|
177
|
+
// entries that actually carry Codex usage. `reviews` counts only entries with a real {inputTokens, outputTokens}
|
|
178
|
+
// object; `unpriced` counts an entry that ran and is not a fallback but carries no usage (codex-review printed
|
|
179
|
+
// none, or the relay predates this card) - never estimated, only counted.
|
|
180
|
+
export function codexUsageFromReport(report) {
|
|
181
|
+
let reviews = 0;
|
|
182
|
+
let input = 0;
|
|
183
|
+
let output = 0;
|
|
184
|
+
let durationMs = 0;
|
|
185
|
+
let unpriced = 0;
|
|
186
|
+
const consider = (adv) => {
|
|
187
|
+
if (!adv) return;
|
|
188
|
+
const usage = adv.usage;
|
|
189
|
+
if (usage && typeof usage === "object" && typeof usage.inputTokens === "number" && typeof usage.outputTokens === "number") {
|
|
190
|
+
reviews++;
|
|
191
|
+
input += usage.inputTokens;
|
|
192
|
+
output += usage.outputTokens;
|
|
193
|
+
if (typeof adv.durationMs === "number") durationMs += adv.durationMs;
|
|
194
|
+
} else if (adv.ran && !adv.fallback) {
|
|
195
|
+
unpriced++;
|
|
196
|
+
}
|
|
197
|
+
};
|
|
198
|
+
for (const level of (report && report.levels) || []) {
|
|
199
|
+
for (const t of level.tasks || []) consider(t.adversary);
|
|
200
|
+
consider(level.levelAdversary);
|
|
201
|
+
}
|
|
202
|
+
return { reviews, input, output, durationMs, unpriced };
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Prices a codexUsageFromReport() result at a CODEX_PRICES row; null when the model is not in the table (never
|
|
206
|
+
// estimated).
|
|
207
|
+
export function priceCodexUsage(codexUsage, model = CODEX_DEFAULT_MODEL) {
|
|
208
|
+
const p = CODEX_PRICES[model];
|
|
209
|
+
if (!p) return null;
|
|
210
|
+
return (codexUsage.input * p.input + codexUsage.output * p.output) / 1e6;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const CODEX_RELAY = "doug-flow:adversary";
|
|
214
|
+
|
|
215
|
+
// Sum and price every agent of one run. Throws when the run cannot be found; an agent without a transcript or on
|
|
216
|
+
// an unpriced model is reported in `notes`, never estimated.
|
|
217
|
+
// `report` (card adversary-usage-in-report): a parsed workflow report, or null/omitted. When given and it carries
|
|
218
|
+
// any Codex usage, the result's `codex` key prices it (see codexUsageFromReport/priceCodexUsage) and the relay note
|
|
219
|
+
// below is dropped, since the priced Codex line supersedes it; otherwise codex is null and the note is unchanged.
|
|
220
|
+
// `reportPath` is carried through into `codex.reportPath` only for display (renderCost); it is never read here.
|
|
221
|
+
export function costRun({ runId, claudeDir, projectDir, report, reportPath } = {}) {
|
|
222
|
+
if (!runId) throw new Error("a run id is required (wf_...)");
|
|
223
|
+
const found = findRunDir({ runId, claudeDir, projectDir });
|
|
224
|
+
if (!found) throw new Error(`no journal for run ${runId} under ${join(claudeDir || join(homedir(), ".claude"), "projects")}`);
|
|
225
|
+
const { dir, session, project, runFile } = found;
|
|
226
|
+
const run = readJson(runFile);
|
|
227
|
+
const labels = new Map();
|
|
228
|
+
for (const p of (run && run.workflowProgress) || []) if (p.type === "workflow_agent" && p.agentId) labels.set(p.agentId, p);
|
|
229
|
+
const notes = [];
|
|
230
|
+
const agents = [];
|
|
231
|
+
let codexRelays = 0;
|
|
232
|
+
for (const j of readJournal(dir)) {
|
|
233
|
+
const meta = readJson(join(dir, `agent-${j.agentId}.meta.json`)) || {};
|
|
234
|
+
const progress = labels.get(j.agentId);
|
|
235
|
+
const parsed = progress ? parseLabel(progress.label) : { stage: (meta.agentType || "").replace(/^doug-flow:/, "") || null, task: j.taskId, pass: null };
|
|
236
|
+
const agent = {
|
|
237
|
+
agentId: j.agentId,
|
|
238
|
+
label: progress ? progress.label : null,
|
|
239
|
+
stage: parsed.stage,
|
|
240
|
+
task: parsed.task || j.taskId,
|
|
241
|
+
pass: parsed.pass,
|
|
242
|
+
agentType: meta.agentType || null,
|
|
243
|
+
model: null,
|
|
244
|
+
messages: 0,
|
|
245
|
+
usage: emptyUsage(),
|
|
246
|
+
usd: null,
|
|
247
|
+
transcript: false,
|
|
248
|
+
};
|
|
249
|
+
const file = join(dir, `agent-${j.agentId}.jsonl`);
|
|
250
|
+
if (existsSync(file)) {
|
|
251
|
+
const t = transcriptUsage(file);
|
|
252
|
+
agent.transcript = true;
|
|
253
|
+
agent.model = t.model;
|
|
254
|
+
agent.messages = t.messages;
|
|
255
|
+
agent.usage = t.usage;
|
|
256
|
+
agent.usd = t.model ? priceUsage(t.usage, t.model) : null;
|
|
257
|
+
if (t.model && agent.usd === null) notes.push(`${j.agentId} (${agent.label || agent.stage}): model ${t.model} is not in the price table; not priced`);
|
|
258
|
+
if (!t.model && t.messages === 0) notes.push(`${j.agentId} (${agent.label || agent.stage}): transcript has no usage lines; not priced`);
|
|
259
|
+
} else {
|
|
260
|
+
notes.push(`${j.agentId} (${agent.label || agent.stage}): no transcript file; not priced`);
|
|
261
|
+
}
|
|
262
|
+
if (meta.agentType === CODEX_RELAY) codexRelays++;
|
|
263
|
+
agents.push(agent);
|
|
264
|
+
}
|
|
265
|
+
if (codexRelays) notes.push(`${codexRelays} adversary agent${codexRelays === 1 ? "" : "s"} relayed codex-review; Codex's own tokens are not in local transcripts and are not priced`);
|
|
266
|
+
let codex = null;
|
|
267
|
+
if (report) {
|
|
268
|
+
const codexUsage = codexUsageFromReport(report);
|
|
269
|
+
if (codexUsage.reviews > 0) {
|
|
270
|
+
codex = {
|
|
271
|
+
model: CODEX_DEFAULT_MODEL,
|
|
272
|
+
reviews: codexUsage.reviews,
|
|
273
|
+
input: codexUsage.input,
|
|
274
|
+
output: codexUsage.output,
|
|
275
|
+
durationMs: codexUsage.durationMs,
|
|
276
|
+
usd: priceCodexUsage(codexUsage, CODEX_DEFAULT_MODEL),
|
|
277
|
+
unpriced: codexUsage.unpriced,
|
|
278
|
+
pricesDated: CODEX_PRICES_DATED,
|
|
279
|
+
reportPath: reportPath || null,
|
|
280
|
+
};
|
|
281
|
+
const relayNoteIdx = notes.findIndex((n) => /relayed codex-review/.test(n));
|
|
282
|
+
if (relayNoteIdx !== -1) notes.splice(relayNoteIdx, 1);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
const group = (key) => {
|
|
286
|
+
const out = new Map();
|
|
287
|
+
for (const a of agents) {
|
|
288
|
+
const k = key(a) || "(unknown)";
|
|
289
|
+
const g = out.get(k) || { agents: 0, usage: emptyUsage(), usd: 0, unpriced: 0 };
|
|
290
|
+
g.agents++;
|
|
291
|
+
addUsage(g.usage, a.usage);
|
|
292
|
+
if (a.usd === null) g.unpriced++;
|
|
293
|
+
else g.usd += a.usd;
|
|
294
|
+
out.set(k, g);
|
|
295
|
+
}
|
|
296
|
+
return [...out.entries()].map(([name, g]) => ({ name, ...g }));
|
|
297
|
+
};
|
|
298
|
+
const total = { agents: agents.length, usage: emptyUsage(), usd: 0, unpriced: 0 };
|
|
299
|
+
for (const a of agents) {
|
|
300
|
+
addUsage(total.usage, a.usage);
|
|
301
|
+
if (a.usd === null) total.unpriced++;
|
|
302
|
+
else total.usd += a.usd;
|
|
303
|
+
}
|
|
304
|
+
return {
|
|
305
|
+
runId,
|
|
306
|
+
session,
|
|
307
|
+
project,
|
|
308
|
+
dir,
|
|
309
|
+
startedAt: run && run.timestamp ? run.timestamp : null,
|
|
310
|
+
durationMs: run && typeof run.durationMs === "number" ? run.durationMs : null,
|
|
311
|
+
pricesDated: PRICES_DATED,
|
|
312
|
+
agents,
|
|
313
|
+
tasks: group((a) => a.task),
|
|
314
|
+
stages: group((a) => a.stage),
|
|
315
|
+
total,
|
|
316
|
+
codex,
|
|
317
|
+
notes,
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// A batch run's cost split per card (card parallel-cards): every agent whose task is one of a card's plan tasks
|
|
322
|
+
// counts for that card; the shared stages (integrate, the level adversary: task `level-<n>`) are listed once as
|
|
323
|
+
// `shared`; agents on no card's task are `other`. Nothing estimated: unpriced agents are counted, not summed.
|
|
324
|
+
export function costByCard(result, plan) {
|
|
325
|
+
if (!plan || !Array.isArray(plan.cards) || !plan.cards.length) throw new Error("the plan has no cards; costByCard splits a batch plan's run (plan.mjs merge)");
|
|
326
|
+
const cardOf = new Map((plan.tasks || []).filter((t) => t && typeof t.card === "string").map((t) => [t.id, t.card]));
|
|
327
|
+
const bucket = () => ({ agents: 0, usd: 0, unpriced: 0 });
|
|
328
|
+
const add = (b, a) => {
|
|
329
|
+
b.agents++;
|
|
330
|
+
if (a.usd === null || a.usd === undefined) b.unpriced++;
|
|
331
|
+
else b.usd += a.usd;
|
|
332
|
+
};
|
|
333
|
+
const cards = plan.cards.map((card) => ({ card, ...bucket(), tasks: [...cardOf.entries()].filter(([, c]) => c === card).map(([id]) => id) }));
|
|
334
|
+
const shared = { ...bucket(), labels: [] };
|
|
335
|
+
const other = { ...bucket(), labels: [] };
|
|
336
|
+
for (const a of result.agents || []) {
|
|
337
|
+
const card = cardOf.get(a.task);
|
|
338
|
+
if (card) add(cards.find((c) => c.card === card), a);
|
|
339
|
+
else if (/^level-\d+$/.test(String(a.task || ""))) {
|
|
340
|
+
add(shared, a);
|
|
341
|
+
shared.labels.push(a.label || a.agentId);
|
|
342
|
+
} else {
|
|
343
|
+
add(other, a);
|
|
344
|
+
other.labels.push(a.label || a.agentId);
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
return { cards, shared, other };
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
export function renderCostByCard(byCard) {
|
|
351
|
+
const lines = ["| Per card | Agents | USD |", "|---|---|---|"];
|
|
352
|
+
const usd = (b) => `${fmtUsd(b.usd)}${b.unpriced ? ` (${b.unpriced} unpriced)` : ""}`;
|
|
353
|
+
for (const c of byCard.cards) lines.push(`| ${c.card} | ${c.agents} | ${usd(c)} |`);
|
|
354
|
+
lines.push(`| shared integration agents${byCard.shared.labels.length ? ` (${byCard.shared.labels.join(", ")})` : ""} | ${byCard.shared.agents} | ${usd(byCard.shared)} |`);
|
|
355
|
+
if (byCard.other.agents) lines.push(`| on no card's task (${byCard.other.labels.join(", ")}) | ${byCard.other.agents} | ${usd(byCard.other)} |`);
|
|
356
|
+
return lines.join("\n") + "\n";
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
const fmtInt = (n) => String(n).replace(/\B(?=(\d{3})+(?!\d))/g, ",");
|
|
360
|
+
const fmtUsd = (n) => (n === null ? "unpriced" : `$${n.toFixed(2)}`);
|
|
361
|
+
|
|
362
|
+
export function renderCost(result) {
|
|
363
|
+
const lines = [];
|
|
364
|
+
const when = result.startedAt ? `, ${result.startedAt}` : "";
|
|
365
|
+
const dur = result.durationMs !== null ? `, ${Math.round(result.durationMs / 60000)} min` : "";
|
|
366
|
+
lines.push(`Run ${result.runId}: ${result.agents.length} agent${result.agents.length === 1 ? "" : "s"} in session ${result.session}${when}${dur}.`);
|
|
367
|
+
lines.push("");
|
|
368
|
+
lines.push("| Agent | Stage | Task | Pass | Model | Input | Output | Cache read | Cache write | USD |");
|
|
369
|
+
lines.push("|---|---|---|---|---|---|---|---|---|---|");
|
|
370
|
+
for (const a of result.agents) {
|
|
371
|
+
lines.push(`| ${a.agentId} | ${a.stage || "?"} | ${a.task || "?"} | ${a.pass === null ? "" : a.pass} | ${a.model ? normalizeModel(a.model) : "?"} | ${fmtInt(a.usage.input)} | ${fmtInt(a.usage.output)} | ${fmtInt(a.usage.cacheRead)} | ${fmtInt(a.usage.cacheWrite)} | ${fmtUsd(a.usd)} |`);
|
|
372
|
+
}
|
|
373
|
+
for (const [title, rows] of [["Per task", result.tasks], ["Per stage", result.stages]]) {
|
|
374
|
+
lines.push("");
|
|
375
|
+
lines.push(`| ${title} | Agents | Input | Output | Cache read | Cache write | USD |`);
|
|
376
|
+
lines.push("|---|---|---|---|---|---|---|");
|
|
377
|
+
for (const g of rows) lines.push(`| ${g.name} | ${g.agents} | ${fmtInt(g.usage.input)} | ${fmtInt(g.usage.output)} | ${fmtInt(g.usage.cacheRead)} | ${fmtInt(g.usage.cacheWrite)} | ${fmtUsd(g.usd)}${g.unpriced ? ` (${g.unpriced} unpriced)` : ""} |`);
|
|
378
|
+
}
|
|
379
|
+
const t = result.total;
|
|
380
|
+
lines.push("");
|
|
381
|
+
lines.push(`Run 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 ? ` for ${t.agents - t.unpriced} of ${t.agents} agents (${t.unpriced} unpriced)` : ""} at list prices dated ${result.pricesDated}.`);
|
|
382
|
+
if (result.codex) {
|
|
383
|
+
const c = result.codex;
|
|
384
|
+
const secs = Math.round(c.durationMs / 1000);
|
|
385
|
+
const from = c.reportPath ? ` (from ${c.reportPath})` : "";
|
|
386
|
+
const unpriced = c.unpriced ? ` (${c.unpriced} review(s) printed no usage)` : "";
|
|
387
|
+
lines.push(`Codex: ${c.reviews} review(s), ${fmtInt(c.input)} input, ${fmtInt(c.output)} output tokens in ${secs} s; ${fmtUsd(c.usd)} at ${c.model} list price dated ${c.pricesDated}${from}; not included in the run total.${unpriced}`);
|
|
388
|
+
}
|
|
389
|
+
for (const n of result.notes) lines.push(`Note: ${n}.`);
|
|
390
|
+
return lines.join("\n") + "\n";
|
|
391
|
+
}
|
|
392
|
+
|