@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.
Files changed (112) hide show
  1. package/CODE_OF_CONDUCT.md +83 -0
  2. package/README.md +102 -63
  3. package/compose.mjs +8 -2
  4. package/composition.mjs +11 -3
  5. package/config-view.mjs +18 -3
  6. package/config.mjs +74 -13
  7. package/docs/assets/overview.png +0 -0
  8. package/docs/configuration.md +85 -9
  9. package/package.json +4 -2
  10. package/rules/core.md +118 -209
  11. package/rules/modifiers/backlog-outline.md +25 -0
  12. package/rules/modifiers/backlog-visual.md +16 -0
  13. package/rules/modifiers/coded-offers.md +47 -17
  14. package/rules/modifiers/cross-session-relay.md +23 -0
  15. package/rules/modifiers/low-quota-handoff.md +4 -3
  16. package/rules/modifiers/model-default-questions.md +33 -0
  17. package/rules/modifiers/multi-agent.md +6 -2
  18. package/rules/modifiers/no-attribution.md +7 -1
  19. package/rules/modifiers/periodic-updates.md +25 -0
  20. package/rules/modifiers/plan-files.md +35 -3
  21. package/rules/modifiers/release-batching.md +4 -2
  22. package/rules/modifiers/shared-host-load.md +13 -42
  23. package/rules/modifiers/squash-landing.md +1 -1
  24. package/rules/modifiers/upstream-contributions.md +45 -0
  25. package/rules/modifiers/web-image-formats.md +35 -0
  26. package/rules/references/backlog-view.md +154 -0
  27. package/rules/references/landing.md +14 -14
  28. package/rules/references/multi-agent.md +6 -2
  29. package/rules/references/release-batching.md +37 -4
  30. package/rules/references/squash-landing.md +10 -4
  31. package/rules/references/thread-visuals.md +45 -0
  32. package/setup.mjs +87 -17
  33. package/skills/hr-agent-guidance-audit/SKILL.md +10 -5
  34. package/skills/hr-agent-guidance-audit/references/prune-workflow.md +72 -0
  35. package/skills/hr-agent-guidance-audit/references/records-and-lessons.md +1 -1
  36. package/skills/hr-agent-guidance-audit/references/retirement-verdicts.md +49 -0
  37. package/skills/hr-agent-guidance-refresh/SKILL.md +25 -57
  38. package/skills/hr-agent-guidance-refresh/references/verification.md +1 -1
  39. package/skills/hr-audit-choices/SKILL.md +1 -1
  40. package/skills/hr-benchmarking/SKILL.md +122 -159
  41. package/skills/hr-benchmarking/references/javascript.md +113 -0
  42. package/skills/hr-benchmarking/references/tools.md +62 -57
  43. package/skills/hr-better-accessibility/SKILL.md +1 -1
  44. package/skills/hr-better-accessibility/screen-readers.md +1 -0
  45. package/skills/hr-code-review/SKILL.md +19 -3
  46. package/skills/hr-code-review/agents/openai.yaml +1 -1
  47. package/skills/hr-code-review/references/acp-client.md +43 -0
  48. package/skills/{hr-change-impact/SKILL.md → hr-code-review/references/impact.md} +2 -12
  49. package/skills/hr-code-review/references/panel-adjudication.md +58 -0
  50. package/skills/hr-code-review/references/panel-plan.md +61 -0
  51. package/skills/hr-code-review/references/panel-recommendations.json +105 -0
  52. package/skills/hr-code-review/references/panel-recommendations.md +46 -0
  53. package/skills/hr-code-review/references/panel-setup.md +54 -0
  54. package/skills/hr-code-review/references/reviewer-role.md +31 -0
  55. package/skills/hr-code-review/scripts/acp-panel-client.mjs +299 -0
  56. package/skills/hr-code-review/scripts/check-receipts.mjs +263 -0
  57. package/skills/hr-code-review/scripts/panel-plan.mjs +69 -0
  58. package/skills/hr-code-review/scripts/panel-preferences.mjs +220 -0
  59. package/skills/hr-design-exploration/SKILL.md +24 -1
  60. package/skills/hr-design-exploration/agents/openai.yaml +2 -2
  61. package/skills/hr-diagnosing-bugs/SKILL.md +2 -2
  62. package/skills/hr-diagnosing-bugs/references/reporting.md +29 -4
  63. package/skills/hr-diagnosing-bugs/scripts/check-pr-offer.mjs +47 -0
  64. package/skills/hr-explain-code/SKILL.md +1 -1
  65. package/skills/hr-extract-shared-steps/SKILL.md +1 -1
  66. package/skills/hr-forgotten-work-audit/SKILL.md +130 -0
  67. package/skills/hr-forgotten-work-audit/references/brief-template.md +56 -0
  68. package/skills/hr-forgotten-work-audit/references/result-format.md +46 -0
  69. package/skills/hr-handoff/LICENSE +21 -0
  70. package/skills/hr-handoff/NOTICE.md +29 -0
  71. package/skills/hr-handoff/SKILL.md +24 -1
  72. package/skills/hr-handoff/references/reconciliation.md +25 -6
  73. package/skills/hr-handoff/references/successor-thread.md +76 -0
  74. package/skills/hr-handoff/scripts/check-reconciliation.mjs +48 -6
  75. package/skills/hr-house-rules-setup/SKILL.md +22 -8
  76. package/skills/hr-lean-plan/SKILL.md +4 -0
  77. package/skills/hr-maintainability-review/SKILL.md +14 -4
  78. package/skills/hr-maintainability-review/agents/openai.yaml +1 -1
  79. package/skills/hr-manual-qa/SKILL.md +1 -1
  80. package/skills/hr-ordering-tests/SKILL.md +1 -1
  81. package/skills/hr-plain-prose/SKILL.md +7 -4
  82. package/skills/hr-pr-followup/SKILL.md +6 -3
  83. package/skills/hr-pr-followup/references/review-helpers.md +40 -7
  84. package/skills/hr-pr-followup/scripts/check-landing.py +204 -0
  85. package/skills/hr-pr-followup/scripts/check-pr-review.ts +19 -8
  86. package/skills/hr-pr-followup/scripts/merge-pr.sh +100 -0
  87. package/skills/hr-pr-followup/scripts/verify-landed.sh +184 -0
  88. package/skills/hr-read-reddit/SKILL.md +1 -1
  89. package/skills/hr-research-shared-links/SKILL.md +103 -0
  90. package/skills/hr-sqlite/SKILL.md +174 -0
  91. package/skills/hr-sqlite/agents/openai.yaml +2 -0
  92. package/skills/hr-sqlite/references/evidence.md +94 -0
  93. package/skills/hr-sqlite/references/tool-decisions.md +39 -0
  94. package/skills/hr-sqlite/scripts/inspect-sqlite.py +245 -0
  95. package/skills/hr-stock-ui-audit/SKILL.md +1 -1
  96. package/skills/hr-test-audit/LICENSE +21 -0
  97. package/skills/hr-test-audit/NOTICE.md +67 -0
  98. package/skills/hr-usage-report/SKILL.md +90 -0
  99. package/skills/hr-usage-report/agents/openai.yaml +2 -0
  100. package/skills/hr-usage-report/assets/example-report.json +125 -0
  101. package/skills/hr-usage-report/assets/report-template.html +377 -0
  102. package/skills/hr-usage-report/scripts/render-report.ts +86 -0
  103. package/skills/hr-usage-report/scripts/usage-by-model.ts +499 -0
  104. package/skills/hr-what-next/SKILL.md +100 -10
  105. package/skills/hr-what-next/references/reporting.md +13 -22
  106. package/skills/hr-what-next/scripts/check-open-work.mjs +711 -0
  107. package/skills/hr-writing-pr/SKILL.md +29 -14
  108. package/skills/hr-writing-pr/scripts/benefit-chart.py +200 -0
  109. package/docs/npm-release.md +0 -45
  110. package/rules/modifiers/swarmail.md +0 -12
  111. package/skills/hr-benchmarking/references/priority-findings-20261006.md +0 -115
  112. 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 owned uncommitted file,
21
- unpushed commit or held checkout, and the number of other live ledger items
22
- (deferred, unselected, blocked or follow-up) with a link to the ledger.
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 as an offer instead. When asked
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 their latest disposition. Reconcile duplicate, superseded
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, unselected,
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. Preserve schedules
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 context-only dispositions. If `hr-handoff` and its reconciliation
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.