@bompus/house-rules 0.11.0 → 0.14.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/CODE_OF_CONDUCT.md +83 -0
- package/README.md +102 -63
- package/compose.mjs +8 -2
- package/composition.mjs +11 -3
- package/config-view.mjs +18 -3
- package/config.mjs +74 -13
- package/docs/assets/overview.png +0 -0
- package/docs/configuration.md +85 -9
- package/package.json +4 -2
- package/rules/core.md +118 -209
- package/rules/modifiers/backlog-outline.md +25 -0
- package/rules/modifiers/backlog-visual.md +16 -0
- package/rules/modifiers/coded-offers.md +47 -17
- package/rules/modifiers/cross-session-relay.md +23 -0
- package/rules/modifiers/low-quota-handoff.md +4 -3
- package/rules/modifiers/model-default-questions.md +33 -0
- package/rules/modifiers/multi-agent.md +6 -2
- package/rules/modifiers/no-attribution.md +7 -1
- package/rules/modifiers/periodic-updates.md +25 -0
- package/rules/modifiers/plan-files.md +35 -3
- package/rules/modifiers/release-batching.md +4 -2
- package/rules/modifiers/shared-host-load.md +13 -42
- package/rules/modifiers/squash-landing.md +1 -1
- package/rules/modifiers/upstream-contributions.md +45 -0
- package/rules/modifiers/web-image-formats.md +35 -0
- package/rules/references/backlog-view.md +154 -0
- package/rules/references/landing.md +14 -14
- package/rules/references/multi-agent.md +6 -2
- package/rules/references/release-batching.md +37 -4
- package/rules/references/squash-landing.md +10 -4
- package/rules/references/thread-visuals.md +45 -0
- package/setup.mjs +87 -17
- package/skills/hr-agent-guidance-audit/SKILL.md +10 -5
- package/skills/hr-agent-guidance-audit/references/prune-workflow.md +72 -0
- package/skills/hr-agent-guidance-audit/references/records-and-lessons.md +1 -1
- package/skills/hr-agent-guidance-audit/references/retirement-verdicts.md +49 -0
- package/skills/hr-agent-guidance-refresh/SKILL.md +25 -57
- package/skills/hr-agent-guidance-refresh/references/verification.md +1 -1
- package/skills/hr-audit-choices/SKILL.md +1 -1
- package/skills/hr-benchmarking/SKILL.md +122 -159
- package/skills/hr-benchmarking/references/javascript.md +113 -0
- package/skills/hr-benchmarking/references/tools.md +62 -57
- package/skills/hr-better-accessibility/SKILL.md +1 -1
- package/skills/hr-better-accessibility/screen-readers.md +1 -0
- package/skills/hr-code-review/SKILL.md +19 -3
- package/skills/hr-code-review/agents/openai.yaml +1 -1
- package/skills/hr-code-review/references/acp-client.md +43 -0
- package/skills/{hr-change-impact/SKILL.md → hr-code-review/references/impact.md} +2 -12
- package/skills/hr-code-review/references/panel-adjudication.md +58 -0
- package/skills/hr-code-review/references/panel-plan.md +61 -0
- package/skills/hr-code-review/references/panel-recommendations.json +105 -0
- package/skills/hr-code-review/references/panel-recommendations.md +46 -0
- package/skills/hr-code-review/references/panel-setup.md +54 -0
- package/skills/hr-code-review/references/reviewer-role.md +31 -0
- package/skills/hr-code-review/scripts/acp-panel-client.mjs +299 -0
- package/skills/hr-code-review/scripts/check-receipts.mjs +263 -0
- package/skills/hr-code-review/scripts/panel-plan.mjs +69 -0
- package/skills/hr-code-review/scripts/panel-preferences.mjs +220 -0
- package/skills/hr-design-exploration/SKILL.md +24 -1
- package/skills/hr-design-exploration/agents/openai.yaml +2 -2
- package/skills/hr-diagnosing-bugs/SKILL.md +2 -2
- package/skills/hr-diagnosing-bugs/references/reporting.md +29 -4
- package/skills/hr-diagnosing-bugs/scripts/check-pr-offer.mjs +47 -0
- package/skills/hr-explain-code/SKILL.md +1 -1
- package/skills/hr-extract-shared-steps/SKILL.md +1 -1
- package/skills/hr-forgotten-work-audit/SKILL.md +130 -0
- package/skills/hr-forgotten-work-audit/references/brief-template.md +56 -0
- package/skills/hr-forgotten-work-audit/references/result-format.md +46 -0
- package/skills/hr-handoff/LICENSE +21 -0
- package/skills/hr-handoff/NOTICE.md +29 -0
- package/skills/hr-handoff/SKILL.md +24 -1
- package/skills/hr-handoff/references/reconciliation.md +25 -6
- package/skills/hr-handoff/references/successor-thread.md +76 -0
- package/skills/hr-handoff/scripts/check-reconciliation.mjs +48 -6
- package/skills/hr-house-rules-setup/SKILL.md +22 -8
- package/skills/hr-lean-plan/SKILL.md +4 -0
- package/skills/hr-maintainability-review/SKILL.md +14 -4
- package/skills/hr-maintainability-review/agents/openai.yaml +1 -1
- package/skills/hr-manual-qa/SKILL.md +1 -1
- package/skills/hr-ordering-tests/SKILL.md +1 -1
- package/skills/hr-plain-prose/SKILL.md +7 -4
- package/skills/hr-pr-followup/SKILL.md +6 -3
- package/skills/hr-pr-followup/references/review-helpers.md +40 -7
- package/skills/hr-pr-followup/scripts/check-landing.py +204 -0
- package/skills/hr-pr-followup/scripts/check-pr-review.ts +19 -8
- package/skills/hr-pr-followup/scripts/merge-pr.sh +100 -0
- package/skills/hr-pr-followup/scripts/verify-landed.sh +184 -0
- package/skills/hr-read-reddit/SKILL.md +1 -1
- package/skills/hr-research-shared-links/SKILL.md +103 -0
- package/skills/hr-sqlite/SKILL.md +174 -0
- package/skills/hr-sqlite/agents/openai.yaml +2 -0
- package/skills/hr-sqlite/references/evidence.md +94 -0
- package/skills/hr-sqlite/references/tool-decisions.md +39 -0
- package/skills/hr-sqlite/scripts/inspect-sqlite.py +245 -0
- package/skills/hr-stock-ui-audit/SKILL.md +1 -1
- package/skills/hr-test-audit/LICENSE +21 -0
- package/skills/hr-test-audit/NOTICE.md +67 -0
- package/skills/hr-usage-report/SKILL.md +90 -0
- package/skills/hr-usage-report/agents/openai.yaml +2 -0
- package/skills/hr-usage-report/assets/example-report.json +125 -0
- package/skills/hr-usage-report/assets/report-template.html +377 -0
- package/skills/hr-usage-report/scripts/render-report.ts +86 -0
- package/skills/hr-usage-report/scripts/usage-by-model.ts +499 -0
- package/skills/hr-what-next/SKILL.md +100 -10
- package/skills/hr-what-next/references/reporting.md +13 -22
- package/skills/hr-what-next/scripts/check-open-work.mjs +711 -0
- package/skills/hr-writing-pr/SKILL.md +29 -14
- package/skills/hr-writing-pr/scripts/benefit-chart.py +200 -0
- package/docs/npm-release.md +0 -45
- package/rules/modifiers/swarmail.md +0 -12
- package/skills/hr-benchmarking/references/priority-findings-20261006.md +0 -115
- package/skills/hr-change-impact/agents/openai.yaml +0 -5
|
@@ -0,0 +1,499 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
// Where did model usage go? Totals local transcript usage per provider, model and
|
|
3
|
+
// role (main session, subagent, advisor) over a window, priced at models.dev list
|
|
4
|
+
// rates, and reports context size per request and at session start.
|
|
5
|
+
//
|
|
6
|
+
// bun usage-by-model.ts [--days N | --since ISO] [--providers claude,codex,opencode]
|
|
7
|
+
// [--prices models-dev.json] [--json]
|
|
8
|
+
//
|
|
9
|
+
// List price stands in for subscription quota; providers do not publish the
|
|
10
|
+
// conversion. Read the shares beside the provider's live quota readings, not as an
|
|
11
|
+
// invoice. Method and report shape: ../SKILL.md.
|
|
12
|
+
import { Database } from "bun:sqlite";
|
|
13
|
+
import { existsSync, readdirSync, readFileSync, realpathSync, statSync } from "node:fs";
|
|
14
|
+
import { homedir } from "node:os";
|
|
15
|
+
import { join } from "node:path";
|
|
16
|
+
|
|
17
|
+
export type Tokens = { fresh: number; output: number; cacheRead: number; cacheWrite: number };
|
|
18
|
+
export type Request = Tokens & {
|
|
19
|
+
provider: string;
|
|
20
|
+
priceProvider?: string;
|
|
21
|
+
model: string;
|
|
22
|
+
role: string;
|
|
23
|
+
session: string;
|
|
24
|
+
t: number;
|
|
25
|
+
};
|
|
26
|
+
type Rate = { input: number; output: number; cache_read?: number; cache_write?: number };
|
|
27
|
+
type PriceEntry = Rate & { tiers?: (Rate & { tier: { type: string; size: number } })[] };
|
|
28
|
+
export type Prices = Record<string, { models?: Record<string, { cost?: PriceEntry }> }>;
|
|
29
|
+
|
|
30
|
+
const PRICE_ORDER = ["anthropic", "openai", "google", "xai", "opencode", "opencode-go"];
|
|
31
|
+
|
|
32
|
+
/** models.dev entry for a model id, trying the recorded provider first, then known vendors. */
|
|
33
|
+
export function priceFor(prices: Prices, model: string, provider?: string): PriceEntry | undefined {
|
|
34
|
+
const id = model.replace(/\[.*\]$/, "").replace(/-\d{8}$/, "");
|
|
35
|
+
for (const p of [provider, ...PRICE_ORDER]) {
|
|
36
|
+
const cost = p ? prices[p]?.models?.[id]?.cost : undefined;
|
|
37
|
+
if (cost) {
|
|
38
|
+
return cost;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return undefined;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** List cost of one request; long-context tiers apply when the request's context exceeds their size. */
|
|
45
|
+
export function requestCost(price: PriceEntry, r: Tokens): number {
|
|
46
|
+
const ctx = r.fresh + r.cacheRead + r.cacheWrite;
|
|
47
|
+
let rate: Rate = price;
|
|
48
|
+
// The largest exceeded tier applies, whatever order the price list gives.
|
|
49
|
+
let size = -1;
|
|
50
|
+
for (const tier of price.tiers ?? []) {
|
|
51
|
+
if (tier.tier.type === "context" && ctx > tier.tier.size && tier.tier.size > size) {
|
|
52
|
+
rate = tier;
|
|
53
|
+
size = tier.tier.size;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
const cr = rate.cache_read ?? rate.input * 0.1;
|
|
57
|
+
const cw = rate.cache_write ?? rate.input * 1.25;
|
|
58
|
+
return (
|
|
59
|
+
(r.fresh * rate.input + r.output * rate.output + r.cacheRead * cr + r.cacheWrite * cw) / 1e6
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function* jsonlFiles(dir: string, since: number): Generator<string> {
|
|
64
|
+
if (!existsSync(dir)) {
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
for (const name of readdirSync(dir)) {
|
|
68
|
+
const path = join(dir, name);
|
|
69
|
+
const st = statSync(path);
|
|
70
|
+
if (st.isDirectory()) {
|
|
71
|
+
yield* jsonlFiles(path, since);
|
|
72
|
+
} else if (name.endsWith(".jsonl") && st.mtimeMs >= since) {
|
|
73
|
+
yield path;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function lines(path: string): unknown[] {
|
|
79
|
+
const out: unknown[] = [];
|
|
80
|
+
for (const line of readFileSync(path, "utf8").split("\n")) {
|
|
81
|
+
if (!line.includes("usage")) {
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
try {
|
|
85
|
+
out.push(JSON.parse(line));
|
|
86
|
+
} catch {
|
|
87
|
+
// A partially written last line is skipped.
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return out;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
type ClaudeUsage = {
|
|
94
|
+
input_tokens?: number;
|
|
95
|
+
output_tokens?: number;
|
|
96
|
+
cache_read_input_tokens?: number;
|
|
97
|
+
cache_creation_input_tokens?: number;
|
|
98
|
+
iterations?: (ClaudeUsage & { type?: string; model?: string })[];
|
|
99
|
+
};
|
|
100
|
+
type ClaudeEntry = {
|
|
101
|
+
timestamp?: string;
|
|
102
|
+
requestId?: string;
|
|
103
|
+
isSidechain?: boolean;
|
|
104
|
+
sessionId?: string;
|
|
105
|
+
message?: { id?: string; model?: string; usage?: ClaudeUsage };
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
const claudeTokens = (u: ClaudeUsage): Tokens => ({
|
|
109
|
+
fresh: u.input_tokens ?? 0,
|
|
110
|
+
output: u.output_tokens ?? 0,
|
|
111
|
+
cacheRead: u.cache_read_input_tokens ?? 0,
|
|
112
|
+
cacheWrite: u.cache_creation_input_tokens ?? 0,
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Claude Code transcript roots under a home directory: the default `~/.claude/projects` and the
|
|
117
|
+
* `projects` directory of every other `~/.claude*` configuration directory, such as a separate
|
|
118
|
+
* `CLAUDE_CONFIG_DIR` used for a gateway. A directory reached through a link counts once.
|
|
119
|
+
*/
|
|
120
|
+
export function claudeProjectRoots(home: string): string[] {
|
|
121
|
+
const roots = new Map<string, string>();
|
|
122
|
+
for (const name of readdirSync(home).sort()) {
|
|
123
|
+
if (name !== ".claude" && !name.startsWith(".claude_") && !name.startsWith(".claude-")) {
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
const root = join(home, name, "projects");
|
|
127
|
+
if (existsSync(root)) {
|
|
128
|
+
roots.set(realpathSync(root), root);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
return [...roots.values()];
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
type ClaudeMessage = NonNullable<ClaudeEntry["message"]>;
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Claude Code transcripts: one row per API response, advisor calls split out. A response is
|
|
138
|
+
* written once per streamed block: the first line carries the usage at the start of the stream
|
|
139
|
+
* (a few output tokens, no advisor iterations) and later lines carry the final figures, so the
|
|
140
|
+
* line with the most output tokens is kept, and advisor iterations from whichever line has them.
|
|
141
|
+
*/
|
|
142
|
+
export function claudeRequests(roots: string | string[], since: number): Request[] {
|
|
143
|
+
type Seen = {
|
|
144
|
+
model: string;
|
|
145
|
+
role: string;
|
|
146
|
+
session: string;
|
|
147
|
+
t: number;
|
|
148
|
+
usage: ClaudeUsage;
|
|
149
|
+
iterations: NonNullable<ClaudeUsage["iterations"]>;
|
|
150
|
+
};
|
|
151
|
+
const responses = new Map<string, Seen>();
|
|
152
|
+
const files = (Array.isArray(roots) ? roots : [roots]).flatMap((root) =>
|
|
153
|
+
Array.from(jsonlFiles(root, since)),
|
|
154
|
+
);
|
|
155
|
+
for (const file of files) {
|
|
156
|
+
const fileRole = file.includes("/subagents/") ? "subagent" : "main";
|
|
157
|
+
for (const raw of lines(file)) {
|
|
158
|
+
const e = raw as ClaudeEntry;
|
|
159
|
+
const m: ClaudeMessage | undefined = e.message;
|
|
160
|
+
const t = Date.parse(e.timestamp ?? "");
|
|
161
|
+
// `<synthetic>` marks Claude Code's locally generated replies; they carry no usage.
|
|
162
|
+
if (!m?.id || !m.usage || !m.model || m.model === "<synthetic>" || !(t >= since)) {
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
const key = `${m.id}|${e.requestId ?? ""}`;
|
|
166
|
+
const seen = responses.get(key);
|
|
167
|
+
const iterations = m.usage.iterations ?? [];
|
|
168
|
+
if (!seen) {
|
|
169
|
+
responses.set(key, {
|
|
170
|
+
model: m.model,
|
|
171
|
+
role: e.isSidechain ? "subagent" : fileRole,
|
|
172
|
+
session: e.sessionId ?? file,
|
|
173
|
+
t,
|
|
174
|
+
usage: m.usage,
|
|
175
|
+
iterations,
|
|
176
|
+
});
|
|
177
|
+
continue;
|
|
178
|
+
}
|
|
179
|
+
if ((m.usage.output_tokens ?? 0) > (seen.usage.output_tokens ?? 0)) {
|
|
180
|
+
seen.usage = m.usage;
|
|
181
|
+
}
|
|
182
|
+
if (iterations.length > 0) {
|
|
183
|
+
seen.iterations = iterations;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
const out: Request[] = [];
|
|
188
|
+
for (const r of responses.values()) {
|
|
189
|
+
out.push({
|
|
190
|
+
provider: "claude",
|
|
191
|
+
priceProvider: "anthropic",
|
|
192
|
+
model: r.model,
|
|
193
|
+
role: r.role,
|
|
194
|
+
session: r.session,
|
|
195
|
+
t: r.t,
|
|
196
|
+
...claudeTokens(r.usage),
|
|
197
|
+
});
|
|
198
|
+
for (const it of r.iterations) {
|
|
199
|
+
if (it.type !== "advisor_message" || !it.model) {
|
|
200
|
+
continue;
|
|
201
|
+
}
|
|
202
|
+
out.push({
|
|
203
|
+
provider: "claude",
|
|
204
|
+
priceProvider: "anthropic",
|
|
205
|
+
model: it.model,
|
|
206
|
+
role: "advisor",
|
|
207
|
+
session: r.session,
|
|
208
|
+
t: r.t,
|
|
209
|
+
...claudeTokens(it),
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
return out;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
type CodexUsage = {
|
|
217
|
+
input_tokens?: number;
|
|
218
|
+
cached_input_tokens?: number;
|
|
219
|
+
cache_write_input_tokens?: number;
|
|
220
|
+
output_tokens?: number;
|
|
221
|
+
total_tokens?: number;
|
|
222
|
+
};
|
|
223
|
+
type CodexLine = {
|
|
224
|
+
type?: string;
|
|
225
|
+
timestamp?: string;
|
|
226
|
+
payload?: {
|
|
227
|
+
type?: string;
|
|
228
|
+
model?: string;
|
|
229
|
+
usage?: CodexUsage;
|
|
230
|
+
info?: { last_token_usage?: CodexUsage; total_token_usage?: CodexUsage } | null;
|
|
231
|
+
source?: unknown;
|
|
232
|
+
id?: string;
|
|
233
|
+
};
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
/** Codex rollouts: one row per response record; input tokens include the cached ones. */
|
|
237
|
+
// Codex writes one token_usage_record per response. Builds without it still write
|
|
238
|
+
// token_count events, which repeat when only rate limits change; those count only
|
|
239
|
+
// when the session's running total advances, and only for files with no records.
|
|
240
|
+
export function codexRequests(root: string, since: number): Request[] {
|
|
241
|
+
const out: Request[] = [];
|
|
242
|
+
for (const file of jsonlFiles(root, since)) {
|
|
243
|
+
let model = "unknown";
|
|
244
|
+
let role = "main";
|
|
245
|
+
let session = file;
|
|
246
|
+
let total = 0;
|
|
247
|
+
const records: Request[] = [];
|
|
248
|
+
const events: Request[] = [];
|
|
249
|
+
const request = (u: CodexUsage, t: number): Request => {
|
|
250
|
+
const cached = u.cached_input_tokens ?? 0;
|
|
251
|
+
return {
|
|
252
|
+
provider: "codex",
|
|
253
|
+
priceProvider: "openai",
|
|
254
|
+
model,
|
|
255
|
+
role,
|
|
256
|
+
session,
|
|
257
|
+
t,
|
|
258
|
+
fresh: Math.max(0, (u.input_tokens ?? 0) - cached),
|
|
259
|
+
cacheRead: cached,
|
|
260
|
+
cacheWrite: u.cache_write_input_tokens ?? 0,
|
|
261
|
+
output: u.output_tokens ?? 0,
|
|
262
|
+
};
|
|
263
|
+
};
|
|
264
|
+
for (const raw of readFileSync(file, "utf8").split("\n")) {
|
|
265
|
+
if (!raw) {
|
|
266
|
+
continue;
|
|
267
|
+
}
|
|
268
|
+
let e: CodexLine;
|
|
269
|
+
try {
|
|
270
|
+
e = JSON.parse(raw);
|
|
271
|
+
} catch {
|
|
272
|
+
continue;
|
|
273
|
+
}
|
|
274
|
+
const p = e.payload ?? {};
|
|
275
|
+
const t = Date.parse(e.timestamp ?? "");
|
|
276
|
+
if (e.type === "session_meta") {
|
|
277
|
+
session = p.id ?? file;
|
|
278
|
+
if (p.source && typeof p.source === "object" && "subagent" in p.source) {
|
|
279
|
+
role = "subagent";
|
|
280
|
+
}
|
|
281
|
+
} else if (e.type === "turn_context" && p.model) {
|
|
282
|
+
model = p.model;
|
|
283
|
+
} else if (e.type === "token_usage_record" && p.usage) {
|
|
284
|
+
if (t >= since) {
|
|
285
|
+
records.push(request(p.usage, t));
|
|
286
|
+
}
|
|
287
|
+
} else if (e.type === "event_msg" && p.type === "token_count" && p.info?.last_token_usage) {
|
|
288
|
+
const running = p.info.total_token_usage?.total_tokens ?? 0;
|
|
289
|
+
if (running > total) {
|
|
290
|
+
total = running;
|
|
291
|
+
if (t >= since) {
|
|
292
|
+
events.push(request(p.info.last_token_usage, t));
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
out.push(...(records.length ? records : events));
|
|
298
|
+
}
|
|
299
|
+
return out;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
type OpenCodeData = {
|
|
303
|
+
role?: string;
|
|
304
|
+
modelID?: string;
|
|
305
|
+
providerID?: string;
|
|
306
|
+
tokens?: {
|
|
307
|
+
input?: number;
|
|
308
|
+
output?: number;
|
|
309
|
+
reasoning?: number;
|
|
310
|
+
cache?: { read?: number; write?: number };
|
|
311
|
+
};
|
|
312
|
+
};
|
|
313
|
+
|
|
314
|
+
/** OpenCode's database: assistant messages; a session with a parent is a subagent. */
|
|
315
|
+
export function opencodeRequests(dbPath: string, since: number): Request[] {
|
|
316
|
+
if (!existsSync(dbPath)) {
|
|
317
|
+
return [];
|
|
318
|
+
}
|
|
319
|
+
const db = new Database(dbPath, { readonly: true });
|
|
320
|
+
try {
|
|
321
|
+
const rows = db
|
|
322
|
+
.query(
|
|
323
|
+
"select m.session_id as s, m.time_created as t, m.data as d, s.parent_id as parent from message m join session s on s.id = m.session_id where m.time_created >= ?",
|
|
324
|
+
)
|
|
325
|
+
.all(since) as { s: string; t: number; d: string; parent: string | null }[];
|
|
326
|
+
const out: Request[] = [];
|
|
327
|
+
for (const row of rows) {
|
|
328
|
+
const d = JSON.parse(row.d) as OpenCodeData;
|
|
329
|
+
if (d.role !== "assistant" || !d.modelID || !d.tokens) {
|
|
330
|
+
continue;
|
|
331
|
+
}
|
|
332
|
+
out.push({
|
|
333
|
+
provider: "opencode",
|
|
334
|
+
priceProvider: d.providerID,
|
|
335
|
+
model: d.modelID,
|
|
336
|
+
role: row.parent ? "subagent" : "main",
|
|
337
|
+
session: row.s,
|
|
338
|
+
t: row.t,
|
|
339
|
+
fresh: d.tokens.input ?? 0,
|
|
340
|
+
output: (d.tokens.output ?? 0) + (d.tokens.reasoning ?? 0),
|
|
341
|
+
cacheRead: d.tokens.cache?.read ?? 0,
|
|
342
|
+
cacheWrite: d.tokens.cache?.write ?? 0,
|
|
343
|
+
});
|
|
344
|
+
}
|
|
345
|
+
return out;
|
|
346
|
+
} finally {
|
|
347
|
+
db.close();
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
const quantile = (xs: number[], q: number) =>
|
|
352
|
+
xs.length ? (xs[Math.min(xs.length - 1, Math.floor(q * xs.length))] ?? 0) : 0;
|
|
353
|
+
|
|
354
|
+
export function summarize(reqs: Request[], prices: Prices) {
|
|
355
|
+
const groups = new Map<
|
|
356
|
+
string,
|
|
357
|
+
Tokens & {
|
|
358
|
+
provider: string;
|
|
359
|
+
model: string;
|
|
360
|
+
role: string;
|
|
361
|
+
requests: number;
|
|
362
|
+
cost: number | null;
|
|
363
|
+
}
|
|
364
|
+
>();
|
|
365
|
+
const unpriced = new Set<string>();
|
|
366
|
+
for (const r of reqs) {
|
|
367
|
+
const key = `${r.provider}|${r.model}|${r.role}`;
|
|
368
|
+
const g = groups.get(key) ?? {
|
|
369
|
+
provider: r.provider,
|
|
370
|
+
model: r.model,
|
|
371
|
+
role: r.role,
|
|
372
|
+
requests: 0,
|
|
373
|
+
fresh: 0,
|
|
374
|
+
output: 0,
|
|
375
|
+
cacheRead: 0,
|
|
376
|
+
cacheWrite: 0,
|
|
377
|
+
cost: 0,
|
|
378
|
+
};
|
|
379
|
+
g.requests += 1;
|
|
380
|
+
g.fresh += r.fresh;
|
|
381
|
+
g.output += r.output;
|
|
382
|
+
g.cacheRead += r.cacheRead;
|
|
383
|
+
g.cacheWrite += r.cacheWrite;
|
|
384
|
+
const price = priceFor(prices, r.model, r.priceProvider);
|
|
385
|
+
if (price && g.cost !== null) {
|
|
386
|
+
g.cost += requestCost(price, r);
|
|
387
|
+
} else if (!price) {
|
|
388
|
+
g.cost = null;
|
|
389
|
+
unpriced.add(r.model);
|
|
390
|
+
}
|
|
391
|
+
groups.set(key, g);
|
|
392
|
+
}
|
|
393
|
+
const total = [...groups.values()].reduce((s, g) => s + (g.cost ?? 0), 0);
|
|
394
|
+
const rows = [...groups.values()]
|
|
395
|
+
.map((g) => ({
|
|
396
|
+
...g,
|
|
397
|
+
cost: g.cost === null ? null : Math.round(g.cost * 100) / 100,
|
|
398
|
+
sharePct: g.cost === null || !total ? null : Math.round((1000 * g.cost) / total) / 10,
|
|
399
|
+
}))
|
|
400
|
+
.sort((a, b) => (b.cost ?? -1) - (a.cost ?? -1));
|
|
401
|
+
const context: Record<
|
|
402
|
+
string,
|
|
403
|
+
{ requestP50: number; requestP90: number; sessionStartP50: number; sessions: number }
|
|
404
|
+
> = {};
|
|
405
|
+
for (const provider of new Set(reqs.map((r) => r.provider))) {
|
|
406
|
+
const main = reqs
|
|
407
|
+
.filter((r) => r.provider === provider && r.role === "main")
|
|
408
|
+
.sort((a, b) => a.t - b.t);
|
|
409
|
+
const ctx = main.map((r) => r.fresh + r.cacheRead + r.cacheWrite).sort((a, b) => a - b);
|
|
410
|
+
const first = new Map<string, number>();
|
|
411
|
+
for (const r of main) {
|
|
412
|
+
if (!first.has(r.session)) {
|
|
413
|
+
first.set(r.session, r.fresh + r.cacheRead + r.cacheWrite);
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
const starts = [...first.values()].sort((a, b) => a - b);
|
|
417
|
+
context[provider] = {
|
|
418
|
+
requestP50: quantile(ctx, 0.5),
|
|
419
|
+
requestP90: quantile(ctx, 0.9),
|
|
420
|
+
sessionStartP50: quantile(starts, 0.5),
|
|
421
|
+
sessions: starts.length,
|
|
422
|
+
};
|
|
423
|
+
}
|
|
424
|
+
return { listCostTotal: Math.round(total * 100) / 100, rows, context, unpriced: [...unpriced] };
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
function arg(args: string[], name: string): string | undefined {
|
|
428
|
+
const i = args.indexOf(name);
|
|
429
|
+
return i >= 0 ? args[i + 1] : undefined;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
async function fetchPrices(): Promise<unknown> {
|
|
433
|
+
const res = await fetch("https://models.dev/api.json");
|
|
434
|
+
if (!res.ok) {
|
|
435
|
+
throw new Error(`models.dev price list returned ${res.status}; pass --prices <file>`);
|
|
436
|
+
}
|
|
437
|
+
return res.json();
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
async function main(args: string[]) {
|
|
441
|
+
const since = arg(args, "--since")
|
|
442
|
+
? Date.parse(arg(args, "--since")!)
|
|
443
|
+
: Date.now() - Number(arg(args, "--days") ?? 7) * 86_400_000;
|
|
444
|
+
if (!Number.isFinite(since)) {
|
|
445
|
+
throw new Error("--since needs an ISO time; --days a number");
|
|
446
|
+
}
|
|
447
|
+
const providers = (arg(args, "--providers") ?? "claude,codex,opencode").split(",");
|
|
448
|
+
const pricePath = arg(args, "--prices");
|
|
449
|
+
const prices = (
|
|
450
|
+
pricePath ? JSON.parse(readFileSync(pricePath, "utf8")) : await fetchPrices()
|
|
451
|
+
) as Prices;
|
|
452
|
+
const home = homedir();
|
|
453
|
+
const reqs = [
|
|
454
|
+
...(providers.includes("claude")
|
|
455
|
+
? claudeRequests(
|
|
456
|
+
arg(args, "--claude-projects")?.split(",") ?? claudeProjectRoots(home),
|
|
457
|
+
since,
|
|
458
|
+
)
|
|
459
|
+
: []),
|
|
460
|
+
...(providers.includes("codex") ? codexRequests(join(home, ".codex", "sessions"), since) : []),
|
|
461
|
+
...(providers.includes("opencode")
|
|
462
|
+
? opencodeRequests(join(home, ".local", "share", "opencode", "opencode.db"), since)
|
|
463
|
+
: []),
|
|
464
|
+
];
|
|
465
|
+
const report = {
|
|
466
|
+
since: new Date(since).toISOString(),
|
|
467
|
+
providers,
|
|
468
|
+
requests: reqs.length,
|
|
469
|
+
...summarize(reqs, prices),
|
|
470
|
+
};
|
|
471
|
+
if (args.includes("--json")) {
|
|
472
|
+
console.log(JSON.stringify(report, null, 1));
|
|
473
|
+
return;
|
|
474
|
+
}
|
|
475
|
+
const M = (n: number) => (n / 1e6).toFixed(2);
|
|
476
|
+
console.log(
|
|
477
|
+
`since ${report.since}; ${report.requests} requests; list total $${report.listCostTotal}`,
|
|
478
|
+
);
|
|
479
|
+
console.log(
|
|
480
|
+
"provider model role reqs fresh(M) out(M) cacheR(M) cacheW(M) list$ share",
|
|
481
|
+
);
|
|
482
|
+
for (const r of report.rows) {
|
|
483
|
+
console.log(
|
|
484
|
+
`${r.provider.padEnd(9)} ${r.model.slice(0, 30).padEnd(30)} ${r.role.padEnd(9)} ${String(r.requests).padStart(6)} ${M(r.fresh).padStart(9)} ${M(r.output).padStart(6)} ${M(r.cacheRead).padStart(9)} ${M(r.cacheWrite).padStart(9)} ${String(r.cost ?? "n/a").padStart(8)} ${r.sharePct === null ? " n/a" : `${r.sharePct}%`.padStart(6)}`,
|
|
485
|
+
);
|
|
486
|
+
}
|
|
487
|
+
for (const [p, c] of Object.entries(report.context)) {
|
|
488
|
+
console.log(
|
|
489
|
+
`${p}: main-request context p50 ${c.requestP50}, p90 ${c.requestP90}; session start p50 ${c.sessionStartP50} over ${c.sessions} sessions`,
|
|
490
|
+
);
|
|
491
|
+
}
|
|
492
|
+
if (report.unpriced.length) {
|
|
493
|
+
console.log(`no models.dev price: ${report.unpriced.join(", ")}`);
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
if (import.meta.main) {
|
|
498
|
+
await main(process.argv.slice(2));
|
|
499
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: hr-what-next
|
|
3
|
-
description: "Reconcile remaining work and recommend priorities. Use for next, next?, what's next, what remains, backlog reviews, full-session audits of tasks and unanswered or past questions, and equivalent requests. Do not use for go or continue, which retain their acceptance/continuation meaning, or progress/ETA-only requests."
|
|
3
|
+
description: "Reconcile remaining work and recommend priorities. Use for next, next?, what's next, what remains, backlog reviews, walk-throughs of the backlog item by item, full-session audits of tasks and unanswered or past questions, and equivalent requests. Do not use for go or continue, which retain their acceptance/continuation meaning, or progress/ETA-only requests."
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# What next
|
|
@@ -17,25 +17,55 @@ completed state before the completion message; a report alone does not do so.
|
|
|
17
17
|
1. Choose the review scope from the request and its context. For a short
|
|
18
18
|
"next?", check the current task, its ledger and latest results, plus this
|
|
19
19
|
floor whatever the scope: every question from an earlier offer in this
|
|
20
|
-
conversation that the user has not answered, every
|
|
21
|
-
|
|
22
|
-
|
|
20
|
+
conversation that the user has not answered, every item deferred with only the
|
|
21
|
+
user's selection as its trigger, every owned uncommitted file, unpushed commit
|
|
22
|
+
or held checkout, and the number of other live ledger items (deferred,
|
|
23
|
+
awaiting selection, blocked or follow-up) with a link to the ledger.
|
|
24
|
+
When the session keeps a plan file, `scripts/check-open-work.mjs <plan>`
|
|
25
|
+
computes this floor (add `--checkout <path>` for each checkout touched, or
|
|
26
|
+
`--app-managed <path>` for one an app retires, which is reported but not a
|
|
27
|
+
finding): run it and quote its result instead of recalling the ledger. Its
|
|
28
|
+
"awaiting your yes/no" group lists the user-trigger deferrals by a text match.
|
|
29
|
+
Its "possibly finished" group lists items whose named pull requests or
|
|
30
|
+
issues are all merged or closed (add `--repo <owner/repo>` for bare numbers),
|
|
31
|
+
unless a named commit sits outside a `--checkout`'s default branch,
|
|
32
|
+
and "re-check due" lists `recheck:` dates that are past or within a week.
|
|
33
|
+
Look at each before reporting it as done or still waiting.
|
|
34
|
+
It also prints `unanswered offers: N`, the number of offer IDs ("Offer
|
|
35
|
+
ABC-12") on open ledger lines that no line records as answered, dropped or
|
|
36
|
+
superseded, and warns at three. Plans that number offers without an ID are
|
|
37
|
+
not counted.
|
|
38
|
+
When the plan's header has a `Project: <name>` line, it also warns when that
|
|
39
|
+
project's last `hr-forgotten-work-audit` record is missing or over 30 days
|
|
40
|
+
old; offer the audit then.
|
|
23
41
|
Report each floor item, or state that the floor is empty. While any floor
|
|
24
42
|
item remains, do not say that nothing is left or that nothing needs the
|
|
25
|
-
user; re-present the unanswered questions
|
|
43
|
+
user; re-present the unanswered questions and ask the undecided items (step 6). When three
|
|
44
|
+
or more offers from this conversation are open and unanswered, do not
|
|
45
|
+
re-present them one by one: start a walk-through over them. When asked
|
|
26
46
|
for all current and past items, review the available session transcript,
|
|
27
47
|
relevant plans, task lists, handoffs and linked follow-ups. Name source
|
|
28
48
|
ranges and missing, inaccessible or truncated records. Sweep other
|
|
29
49
|
sessions' records only within the requested project and scope.
|
|
30
50
|
2. Trace requests, unanswered questions, commitments, offered alternatives
|
|
31
|
-
and findings to
|
|
51
|
+
and findings to what was last decided or done about them. Reconcile duplicate, superseded
|
|
32
52
|
and stale rows with completion or replacement evidence. Keep owners,
|
|
33
53
|
deferred triggers and other sessions' context visible. An old task
|
|
34
54
|
description alone does not establish current activity or ownership.
|
|
35
55
|
3. Preserve the latest selected scope and valid unanswered offer in the
|
|
36
|
-
durable ledger. Distinguish done, authorized unfinished,
|
|
56
|
+
durable ledger. Distinguish done, authorized unfinished, awaiting selection, dropped,
|
|
37
57
|
blocked, deferred and other-owner items. A completed implementation does
|
|
38
|
-
not complete required checks or landing; deferral does not mean done.
|
|
58
|
+
not complete required checks or landing; deferral does not mean done. A
|
|
59
|
+
deferral is not a yes or no: an item deferred with only the user's selection
|
|
60
|
+
as its trigger is undecided, so it sorts under needs your decision.
|
|
61
|
+
Sort every open item by what it waits on: **actionable now** (this session
|
|
62
|
+
can advance it under existing authority), **needs your decision** (a
|
|
63
|
+
selection or answer only the user can give) or **waiting on others** (an
|
|
64
|
+
external release, issue or pull request, another project or owner, a date,
|
|
65
|
+
or an app that retires it). Keep each waiting item's trigger and owner. A
|
|
66
|
+
waiting item stays in the ledger and in the report with its trigger, but it
|
|
67
|
+
is not work this session can do yet: do not recommend it as a next step
|
|
68
|
+
before its trigger fires.
|
|
39
69
|
4. Rank remaining items toward the current goal and explicit priorities.
|
|
40
70
|
Explain the objective and weigh expected impact, next-decision value,
|
|
41
71
|
urgency, dependencies, effort and evidence confidence. Keep priority,
|
|
@@ -47,15 +77,75 @@ completed state before the completion message; a report alone does not do so.
|
|
|
47
77
|
For a full audit, put the full individual table in a durable report
|
|
48
78
|
and link it beside the immediate decisions. Answer whether items are
|
|
49
79
|
accounted for separately from whether selected work is complete.
|
|
80
|
+
Where the user's rules have a backlog view (§ Backlog view, an optional
|
|
81
|
+
rule), show it beside the table whenever any item remains, in the format that
|
|
82
|
+
rule gives. Without one, the table alone is the report. The offer itself
|
|
83
|
+
follows the offer rules, which make an offer visual only when the user asks
|
|
84
|
+
for one.
|
|
50
85
|
6. Continue work that can advance under existing authorization. Ask only for
|
|
51
|
-
unresolved choices using the governing offer format.
|
|
86
|
+
unresolved choices using the governing offer format. Invoking this skill is
|
|
87
|
+
asking for the backlog (§ Session purpose), so present every needs-your-decision
|
|
88
|
+
item as an offer, or as a walk-through when three or more questions are open,
|
|
89
|
+
instead of only counting it. Record each answer as yes, no
|
|
90
|
+
(dropped) or defer with a trigger other than the user's selection, so the
|
|
91
|
+
item is not asked again on every run. A no is dropped, even when it names
|
|
92
|
+
what would make the user reconsider. Defer only work the user wants done later. Items waiting on others stay listed
|
|
93
|
+
with their trigger and are not asked. Preserve schedules
|
|
52
94
|
and ownership; this audit grants no authority to start, transfer, publish
|
|
53
95
|
or clean up work merely because it appears in the inventory.
|
|
54
96
|
|
|
97
|
+
## Walk-through
|
|
98
|
+
|
|
99
|
+
When the user asks to go through the backlog item by item, or three or more
|
|
100
|
+
offers are open and unanswered, run it in groups:
|
|
101
|
+
|
|
102
|
+
1. Rank and save the report as above. Where the user's rules have a backlog
|
|
103
|
+
view (§ Backlog view), show it for the whole ranked backlog with the
|
|
104
|
+
current group marked and placed last among the decisions. The decisions
|
|
105
|
+
stay in the text offer. When
|
|
106
|
+
stacked offers start the walk-through, mark each superseded in the ledger,
|
|
107
|
+
keep its option text, and queue its questions as groups.
|
|
108
|
+
2. Put items in one group when one answer can cover them: the same owner, the
|
|
109
|
+
same action or one dependency. Ask one group per reply, one numbered
|
|
110
|
+
question per item with one combined acceptance line. Ask first the group
|
|
111
|
+
whose answers change or unblock the others, then one with a date, then the
|
|
112
|
+
rest; a decision whose prerequisite is still open waits for a later group.
|
|
113
|
+
Say how many groups remain and name the next.
|
|
114
|
+
3. Before asking a group, look up what one read-only call can answer (why a
|
|
115
|
+
run failed, whether a setting already exists) and show the finding beside
|
|
116
|
+
the question. Ask the user only the decision.
|
|
117
|
+
4. Record each answer in the ledger before acting, then close that group's
|
|
118
|
+
offer. Show the backlog view again, when there is one, with the next group
|
|
119
|
+
marked before asking it.
|
|
120
|
+
`go` selects the recommendation in the latest open group only. A result or
|
|
121
|
+
handoff that brings its own offer while the queue is open joins the queue
|
|
122
|
+
with one line; it does not become another standalone offer.
|
|
123
|
+
5. Stop when no decision remains that the user can make now. Record each
|
|
124
|
+
leftover with the condition that would reopen it. Say what closed in the
|
|
125
|
+
text, and show the backlog view, when there is one, with what still waits;
|
|
126
|
+
the view lists open items only.
|
|
127
|
+
|
|
55
128
|
For a full audit, save a mapping from reviewed source records to items or
|
|
56
|
-
reviewed
|
|
129
|
+
reviewed records kept only as context. When the report rebuilds a ledger or
|
|
130
|
+
inventory from an earlier one, treat the earlier one as a source. Carry each of
|
|
131
|
+
its open or deferred lines over as an item, marked done with its outcome when it
|
|
132
|
+
has closed. Several lines may share one item only when the same next action
|
|
133
|
+
and completion evidence satisfy every line. A summary row such as "the rest,
|
|
134
|
+
reopen only if asked" loses those lines. If `hr-handoff` and its reconciliation
|
|
57
135
|
resources are available, reuse its `references/reconciliation.md` procedure
|
|
58
136
|
and `scripts/check-reconciliation.mjs` checker. Otherwise retain the mapping
|
|
59
137
|
and coverage gaps without claiming a mechanical check. A passing checker
|
|
60
138
|
accounts for supplied records; it does not prove every promise was understood
|
|
61
139
|
or that unavailable history contains no other work.
|
|
140
|
+
|
|
141
|
+
## Machine reconciliation
|
|
142
|
+
|
|
143
|
+
For a host that checks the reply boundary, add `--json --session <id> --turn <id>`
|
|
144
|
+
to the existing floor command. It emits a bound `reconciliation` receipt alongside
|
|
145
|
+
the usual report. An invalid plan exits `1`, even when displayed groups are empty.
|
|
146
|
+
Exit `2` reports open work or findings and does not classify readiness or authority.
|
|
147
|
+
|
|
148
|
+
The house-rules repository's `evals/end-of-reply/README.md`, Adapter migration,
|
|
149
|
+
defines version-2 records, exact item coverage and adapter migration. The floor
|
|
150
|
+
only supplies snapshot identities; the producer must explicitly classify each
|
|
151
|
+
item. Source availability does not establish host enforcement.
|