claude-dev-env 8.14.0 → 8.15.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.
Files changed (123) hide show
  1. package/.agents/agents/test_agent_frontmatter.py +0 -72
  2. package/.agents/agents-archived/code-quality-agent.md +16 -16
  3. package/.agents/skills/_shared/end-of-run-gotcha-recommendations.md +5 -21
  4. package/.agents/skills/e-code-review/SKILL.md +1 -3
  5. package/.agents/skills/e-simplify/SKILL.md +6 -6
  6. package/.agents/skills/eli5/SKILL.md +4 -14
  7. package/.agents/skills/eli5/reference/task-seeds.md +3 -4
  8. package/.agents/skills/everything-search/SKILL.md +3 -3
  9. package/.agents/skills/fresh-branch/SKILL.md +2 -3
  10. package/.agents/skills/grok-spawn/SKILL.md +3 -8
  11. package/.agents/skills/issue-tracker/SKILL.md +4 -6
  12. package/.agents/skills/orchestrator-refresh/SKILL.md +7 -7
  13. package/.agents/skills/plugin-eval-standalone-skill/SKILL.md +3 -3
  14. package/.agents/skills/privacy-hygiene/SKILL.md +6 -10
  15. package/.agents/skills/privacy-hygiene/reference/sweep-procedure.md +1 -1
  16. package/.agents/skills/pull-request/SKILL.md +11 -14
  17. package/.agents/skills/pull-request/reference/publication-tasks.md +2 -4
  18. package/.agents/skills/recovering-codex-startup/SKILL.md +3 -6
  19. package/.agents/skills/recovering-codex-startup/reference/task-seeds.md +1 -1
  20. package/.agents/skills/repairing-hook-boundaries/SKILL.md +4 -7
  21. package/.agents/skills/repairing-hook-boundaries/reference/task-seeds.md +1 -2
  22. package/.agents/skills/second-account-workers/.claude/CLAUDE.md +1 -0
  23. package/.agents/skills/second-account-workers/AGENTS.md +1 -0
  24. package/.agents/skills/second-account-workers/SKILL.md +65 -0
  25. package/.agents/skills/syncing-submodules/SKILL.md +7 -17
  26. package/.agents/skills/syncing-submodules/reference/run-tasks.md +1 -1
  27. package/.agents/skills/task-build/SKILL.md +1 -1
  28. package/.agents/skills/task-build/reference/tool-routing.md +1 -1
  29. package/.agents/skills/team-advisor/SKILL.md +2 -4
  30. package/.agents/skills/usage-pause/SKILL.md +1 -1
  31. package/.agents/skills/windows-scheduled-task/SKILL.md +1 -1
  32. package/AGENTS.md +1 -1
  33. package/_shared/advisor/advisor-protocol.md +4 -4
  34. package/_shared/advisor/reference/consult-format.md +1 -1
  35. package/_shared/advisor/reference/identity.md +2 -2
  36. package/_shared/advisor/reference/warm-up.md +1 -1
  37. package/audit-rubrics/category_rubrics/category-o-docstring-vs-impl-drift.md +1 -1
  38. package/audit-rubrics/prompts/category-o-docstring-vs-impl-drift.md +2 -16
  39. package/bin/ever-shipped-skills.mjs +1 -0
  40. package/bin/install.mjs +1 -0
  41. package/bin/install.prompt-context.test.mjs +46 -0
  42. package/bin/install.test.mjs +3 -2
  43. package/commands/sr-loop.md +2 -2
  44. package/docs/CODE_RULES.md +2 -2
  45. package/docs/DJANGO_PATTERNS.md +5 -5
  46. package/docs/REACT_PATTERNS.md +6 -6
  47. package/docs/TEST_QUALITY.md +10 -28
  48. package/docs/agent-spawn-protocol.md +4 -4
  49. package/docs/codex-compatibility.md +4 -33
  50. package/docs/references/team-advisor-skill.md +0 -1
  51. package/docs/second-claude-account.md +4 -0
  52. package/docs/worker-completion-gate.md +0 -1
  53. package/hooks/advisory/migration_safety_advisor.py +3 -2
  54. package/hooks/advisory/refactor_guard.py +3 -3
  55. package/hooks/hooks.json +0 -5
  56. package/hooks/hooks_constants/issue_tracker_session_starter_constants.py +1 -2
  57. package/hooks/hooks_constants/orchestrator_auto_starter_constants.py +1 -2
  58. package/package.json +1 -1
  59. package/rules/explore-thoroughly.md +0 -1
  60. package/rules/filesystem-search.md +1 -1
  61. package/rules/git-workflow.md +5 -5
  62. package/rules/long-horizon-autonomy.md +1 -5
  63. package/rules/re-stage-before-commit.md +0 -2
  64. package/rules/research-mode.md +1 -1
  65. package/rules/shell-invocation.md +1 -1
  66. package/scripts/claude_account_worker.py +347 -0
  67. package/scripts/claude_account_worker_process.py +169 -0
  68. package/scripts/claude_account_worker_report.py +170 -0
  69. package/scripts/dev_env_scripts_constants/claude_account_worker_constants.py +139 -0
  70. package/scripts/dev_env_scripts_constants/grok_worker_constants.py +8 -2
  71. package/scripts/grok_headless_runner.py +2 -5
  72. package/scripts/policy_lint/config/constants.py +1 -0
  73. package/scripts/policy_lint/engine.py +5 -2
  74. package/scripts/repository_checks/config/constants.py +1 -1
  75. package/scripts/spawn_grok_batch.py +12 -1
  76. package/scripts/subprocess_stream_text.py +28 -0
  77. package/scripts/task_usage_events.py +387 -0
  78. package/scripts/task_usage_report.py +391 -0
  79. package/scripts/test_claude_account_worker.py +331 -0
  80. package/scripts/test_spawn_grok_batch.py +22 -0
  81. package/scripts/test_task_usage_events.py +47 -0
  82. package/scripts/test_task_usage_report.py +603 -0
  83. package/scripts/tests/test_banned_prose_words.py +1 -1
  84. package/scripts/tests/test_policy_lint_rules_archive.py +16 -0
  85. package/scripts/tests/test_policy_lint_rules_engine.py +2 -2
  86. package/scripts/tests/test_rules.py +9 -9
  87. package/system-prompts/software-engineer.xml +15 -30
  88. package/audit-rubrics/audit-categories.json +0 -700
  89. package/audit-rubrics/category_rubrics/category-a-api-contracts.md +0 -90
  90. package/audit-rubrics/category_rubrics/category-b-selector-engine-compat.md +0 -36
  91. package/audit-rubrics/category_rubrics/category-c-resource-cleanup.md +0 -35
  92. package/audit-rubrics/category_rubrics/category-d-scoping-and-ordering.md +0 -35
  93. package/audit-rubrics/category_rubrics/category-e-dead-code.md +0 -40
  94. package/audit-rubrics/category_rubrics/category-f-silent-failures.md +0 -41
  95. package/audit-rubrics/category_rubrics/category-g-bounds-and-overflow.md +0 -38
  96. package/audit-rubrics/category_rubrics/category-h-security-boundaries.md +0 -40
  97. package/audit-rubrics/category_rubrics/category-i-concurrency.md +0 -38
  98. package/audit-rubrics/category_rubrics/category-j-code-rules-compliance.md +0 -59
  99. package/audit-rubrics/category_rubrics/category-k-codebase-conflicts.md +0 -66
  100. package/audit-rubrics/category_rubrics/category-l-behavior-equivalence.md +0 -48
  101. package/audit-rubrics/category_rubrics/category-m-producer-consumer-cardinality.md +0 -44
  102. package/audit-rubrics/category_rubrics/category-n-test-name-scenario-verifier.md +0 -54
  103. package/audit-rubrics/category_rubrics/category-p-name-vs-behavior-contract.md +0 -59
  104. package/audit-rubrics/category_rubrics/category-q-cross-surface-claims.md +0 -46
  105. package/audit-rubrics/prompts/category-a-api-contracts.md +0 -407
  106. package/audit-rubrics/prompts/category-b-selector-engine-compat.md +0 -401
  107. package/audit-rubrics/prompts/category-c-resource-cleanup.md +0 -420
  108. package/audit-rubrics/prompts/category-d-scoping-and-ordering.md +0 -414
  109. package/audit-rubrics/prompts/category-e-dead-code.md +0 -434
  110. package/audit-rubrics/prompts/category-f-silent-failures.md +0 -432
  111. package/audit-rubrics/prompts/category-g-bounds-and-overflow.md +0 -383
  112. package/audit-rubrics/prompts/category-h-security-boundaries.md +0 -423
  113. package/audit-rubrics/prompts/category-i-concurrency.md +0 -429
  114. package/audit-rubrics/prompts/category-j-code-rules-compliance.md +0 -458
  115. package/audit-rubrics/prompts/category-k-codebase-conflicts.md +0 -338
  116. package/audit-rubrics/prompts/category-l-behavior-equivalence.md +0 -131
  117. package/audit-rubrics/prompts/category-m-producer-consumer-cardinality.md +0 -129
  118. package/audit-rubrics/prompts/category-n-test-name-scenario-verifier.md +0 -139
  119. package/audit-rubrics/prompts/category-p-name-vs-behavior-contract.md +0 -89
  120. package/audit-rubrics/prompts/category-q-cross-surface-claims.md +0 -51
  121. package/hooks/hooks_constants/style_reminder_prompt_constants.py +0 -9
  122. package/hooks/session/style_reminder_prompt.py +0 -41
  123. package/hooks/session/test_style_reminder_prompt.py +0 -32
@@ -0,0 +1,603 @@
1
+ """Check task usage reporting through its command line interface."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import subprocess
7
+ import sys
8
+ from decimal import Decimal
9
+ from pathlib import Path
10
+
11
+ import pytest
12
+
13
+ SCRIPT_PATH = Path(__file__).with_name("task_usage_report.py")
14
+ CODEX_STREAM_FIXTURE = (
15
+ Path(__file__).resolve().parent.parent
16
+ / "_shared"
17
+ / "pr-loop"
18
+ / "scripts"
19
+ / "fixtures"
20
+ / "success_stream_v0.144.3.jsonl"
21
+ )
22
+ RUN_ARGUMENTS = [
23
+ "run",
24
+ "--task-id",
25
+ "task-7",
26
+ "--run-id",
27
+ "run-1",
28
+ "--repository",
29
+ "team/project",
30
+ "--outcome",
31
+ "pass",
32
+ ]
33
+
34
+
35
+ def invoke_cli(
36
+ arguments: list[str], stdin_text: str
37
+ ) -> subprocess.CompletedProcess[str]:
38
+ return subprocess.run(
39
+ [sys.executable, str(SCRIPT_PATH), *arguments],
40
+ input=stdin_text,
41
+ capture_output=True,
42
+ text=True,
43
+ check=False,
44
+ )
45
+
46
+
47
+ def report_for(source_format: str, source_payload: object) -> dict[str, object]:
48
+ if source_format == "codex-json":
49
+ stdin_text = "\n".join(
50
+ map(
51
+ json.dumps,
52
+ [{"type": "thread.started", "thread_id": "thread-1"}, source_payload],
53
+ )
54
+ )
55
+ else:
56
+ stdin_text = json.dumps(source_payload)
57
+ completed = invoke_cli(
58
+ [*RUN_ARGUMENTS, "--source-format", source_format], stdin_text
59
+ )
60
+ assert completed.returncode == 0, completed.stderr
61
+ return json.loads(completed.stdout)
62
+
63
+
64
+ def test_claude_json_emits_only_allowlisted_fields_and_uses_model_usage_once() -> None:
65
+ source_payload = {
66
+ "type": "result",
67
+ "result": "SECRET_PROMPT_AND_ANSWER",
68
+ "session_id": "SECRET_SESSION",
69
+ "total_cost_usd": 0.125,
70
+ "usage": {
71
+ "input_tokens": 17,
72
+ "cache_creation_input_tokens": 11,
73
+ "cache_read_input_tokens": 31,
74
+ "output_tokens": 7,
75
+ },
76
+ "modelUsage": {
77
+ "claude-opus": {
78
+ "inputTokens": 17,
79
+ "cacheCreationInputTokens": 11,
80
+ "cacheReadInputTokens": 31,
81
+ "outputTokens": 7,
82
+ "costUSD": 0.125,
83
+ }
84
+ },
85
+ }
86
+ report = report_for("claude-json", source_payload)
87
+ serialized_report = json.dumps(report)
88
+ assert "SECRET_PROMPT_AND_ANSWER" not in serialized_report
89
+ assert "SECRET_SESSION" not in serialized_report
90
+ assert report["uncached_input_tokens"] == 17
91
+ assert report["cache_write_tokens"] == 11
92
+ assert report["cache_read_tokens"] == 31
93
+ assert report["output_tokens"] == 7
94
+ assert report["cost_usd"] == "0.125"
95
+ assert report["cost_basis"] == "provider_estimate"
96
+ assert report["models"] == ["claude-opus"]
97
+ assert report["event_count"] == 1
98
+
99
+
100
+ def test_claude_json_keeps_missing_usage_unknown() -> None:
101
+ report = report_for(
102
+ "claude-json", {"type": "result", "total_cost_usd": 0.03, "result": "secret"}
103
+ )
104
+ assert report["uncached_input_tokens"] is None
105
+ assert report["cache_write_tokens"] is None
106
+ assert report["cost_usd"] == "0.03"
107
+
108
+
109
+ def test_claude_otel_deduplicates_request_ids_and_keeps_models_separate() -> None:
110
+ first_event = {
111
+ "body": "claude_code.api_request",
112
+ "attributes": {
113
+ "request_id": "request-1",
114
+ "session.id": "session-1",
115
+ "model": "claude-opus",
116
+ "input_tokens": 2,
117
+ "cache_creation_tokens": 3,
118
+ "cache_read_tokens": 5,
119
+ "output_tokens": 7,
120
+ "cost_usd_micros": 12_500,
121
+ "prompt": "SECRET_PROMPT",
122
+ },
123
+ }
124
+ second_event = {
125
+ "body": "claude_code.api_request",
126
+ "attributes": {
127
+ "request_id": "request-2",
128
+ "session.id": "session-1",
129
+ "model": "claude-sonnet",
130
+ "input_tokens": 11,
131
+ "cache_creation_tokens": 13,
132
+ "cache_read_tokens": 17,
133
+ "output_tokens": 19,
134
+ "cost_usd": 0.0375,
135
+ },
136
+ }
137
+ stdin_text = "\n".join(map(json.dumps, [first_event, first_event, second_event]))
138
+ completed = invoke_cli(
139
+ [*RUN_ARGUMENTS, "--source-format", "claude-otel"], stdin_text
140
+ )
141
+ assert completed.returncode == 0, completed.stderr
142
+ report = json.loads(completed.stdout)
143
+ assert report["event_count"] == 2
144
+ assert report["uncached_input_tokens"] == 13
145
+ assert report["cache_write_tokens"] == 16
146
+ assert report["cache_read_tokens"] == 22
147
+ assert report["output_tokens"] == 26
148
+ assert Decimal(report["cost_usd"]) == Decimal("0.0500")
149
+ assert report["models"] == ["claude-opus", "claude-sonnet"]
150
+ assert "SECRET_PROMPT" not in completed.stdout
151
+
152
+
153
+ def test_claude_otel_accepts_typed_attributes_and_ignores_other_events() -> None:
154
+ source_payload = {
155
+ "resourceLogs": [
156
+ {
157
+ "scopeLogs": [
158
+ {
159
+ "logRecords": [
160
+ {
161
+ "body": {"stringValue": "claude_code.user_prompt"},
162
+ "attributes": [
163
+ {
164
+ "key": "session.id",
165
+ "value": {"stringValue": "session-1"},
166
+ },
167
+ {
168
+ "key": "prompt",
169
+ "value": {"stringValue": "SECRET_PROMPT"},
170
+ },
171
+ ],
172
+ },
173
+ {
174
+ "body": {"stringValue": "claude_code.api_request"},
175
+ "attributes": [
176
+ {
177
+ "key": "request_id",
178
+ "value": {"stringValue": "r1"},
179
+ },
180
+ {"key": "model", "value": {"stringValue": "opus"}},
181
+ {
182
+ "key": "session.id",
183
+ "value": {"stringValue": "session-1"},
184
+ },
185
+ {"key": "input_tokens", "value": {"intValue": "4"}},
186
+ {
187
+ "key": "cache_creation_tokens",
188
+ "value": {"intValue": "0"},
189
+ },
190
+ {
191
+ "key": "cache_read_tokens",
192
+ "value": {"intValue": "8"},
193
+ },
194
+ {
195
+ "key": "output_tokens",
196
+ "value": {"intValue": "2"},
197
+ },
198
+ ],
199
+ },
200
+ ]
201
+ }
202
+ ]
203
+ }
204
+ ]
205
+ }
206
+ report = report_for("claude-otel", source_payload)
207
+ assert report["event_count"] == 1
208
+ assert report["uncached_input_tokens"] == 4
209
+ assert report["cache_read_tokens"] == 8
210
+ assert report["cost_usd"] is None
211
+ assert "SECRET_PROMPT" not in json.dumps(report)
212
+ assert "session-1" not in json.dumps(report)
213
+
214
+
215
+ def test_codex_uses_cached_and_reasoning_counts_as_subsets() -> None:
216
+ source_payload = [
217
+ {"type": "thread.started", "thread_id": "SECRET_THREAD"},
218
+ {
219
+ "type": "turn.completed",
220
+ "turn_id": "turn-1",
221
+ "model": "gpt-x",
222
+ "usage": {
223
+ "input_tokens": 100,
224
+ "cached_input_tokens": 20,
225
+ "cache_write_input_tokens": 5,
226
+ "output_tokens": 30,
227
+ "reasoning_output_tokens": 10,
228
+ },
229
+ "text": "SECRET_ANSWER",
230
+ },
231
+ ]
232
+ stdin_text = "\n".join(map(json.dumps, source_payload))
233
+ completed = invoke_cli(
234
+ [
235
+ *RUN_ARGUMENTS,
236
+ "--source-format",
237
+ "codex-json",
238
+ "--rate",
239
+ "gpt-x=1,2,0.1,4",
240
+ "--rate-source",
241
+ "vendor-price-card",
242
+ "--rate-effective-date",
243
+ "2026-09-24",
244
+ ],
245
+ stdin_text,
246
+ )
247
+ assert completed.returncode == 0, completed.stderr
248
+ report = json.loads(completed.stdout)
249
+ assert report["uncached_input_tokens"] == 75
250
+ assert report["cache_write_tokens"] == 5
251
+ assert report["cache_read_tokens"] == 20
252
+ assert report["output_tokens"] == 30
253
+ assert report["reasoning_output_tokens"] == 10
254
+ assert report["cost_usd"] == "0.000207"
255
+ assert report["cost_basis"] == "caller_rates"
256
+ assert report["rate_source"] == "vendor-price-card"
257
+ assert report["rate_effective_date"] == "2026-09-24"
258
+ assert "SECRET_THREAD" not in completed.stdout
259
+ assert "SECRET_ANSWER" not in completed.stdout
260
+
261
+
262
+ def test_codex_deduplicates_turn_ids_and_requires_rates_for_cost() -> None:
263
+ turn_event = {
264
+ "type": "turn.completed",
265
+ "turn_id": "turn-1",
266
+ "usage": {
267
+ "input_tokens": 20,
268
+ "cached_input_tokens": 5,
269
+ "cache_write_input_tokens": 0,
270
+ "output_tokens": 3,
271
+ },
272
+ }
273
+ stdin_text = "\n".join(
274
+ map(
275
+ json.dumps,
276
+ [
277
+ {"type": "thread.started", "thread_id": "thread-1"},
278
+ turn_event,
279
+ turn_event,
280
+ ],
281
+ )
282
+ )
283
+ completed = invoke_cli(
284
+ [*RUN_ARGUMENTS, "--source-format", "codex-json"], stdin_text
285
+ )
286
+ assert completed.returncode == 0, completed.stderr
287
+ report = json.loads(completed.stdout)
288
+ assert report["event_count"] == 1
289
+ assert report["uncached_input_tokens"] == 15
290
+ assert report["cost_usd"] is None
291
+ assert report["cost_basis"] == "unknown"
292
+
293
+
294
+ def test_codex_missing_usage_does_not_become_zero() -> None:
295
+ report = report_for("codex-json", {"type": "turn.completed"})
296
+ assert report["event_count"] == 1
297
+ assert report["uncached_input_tokens"] is None
298
+ assert report["output_tokens"] is None
299
+ assert report["cost_usd"] is None
300
+
301
+
302
+ @pytest.mark.parametrize("bad_count", [-1, 1.5, "oops", None, True])
303
+ def test_malformed_numeric_usage_fails_without_echoing_event(bad_count: object) -> None:
304
+ source_payload = {
305
+ "type": "turn.completed",
306
+ "usage": {
307
+ "input_tokens": bad_count,
308
+ "cached_input_tokens": 0,
309
+ "cache_write_input_tokens": 0,
310
+ "output_tokens": 1,
311
+ },
312
+ "text": "SECRET_ANSWER",
313
+ }
314
+ completed = invoke_cli(
315
+ [*RUN_ARGUMENTS, "--source-format", "codex-json"],
316
+ "\n".join(
317
+ map(
318
+ json.dumps,
319
+ [{"type": "thread.started", "thread_id": "thread-1"}, source_payload],
320
+ )
321
+ ),
322
+ )
323
+ assert completed.returncode != 0
324
+ assert completed.stdout == ""
325
+ assert "SECRET_ANSWER" not in completed.stderr
326
+
327
+
328
+ def test_codex_rejects_cached_input_exceeding_total_input() -> None:
329
+ source_payload = {
330
+ "type": "turn.completed",
331
+ "usage": {
332
+ "input_tokens": 10,
333
+ "cached_input_tokens": 11,
334
+ "cache_write_input_tokens": 0,
335
+ "output_tokens": 1,
336
+ },
337
+ }
338
+ completed = invoke_cli(
339
+ [*RUN_ARGUMENTS, "--source-format", "codex-json"],
340
+ "\n".join(
341
+ map(
342
+ json.dumps,
343
+ [{"type": "thread.started", "thread_id": "thread-1"}, source_payload],
344
+ )
345
+ ),
346
+ )
347
+ assert completed.returncode != 0
348
+
349
+
350
+ def test_summary_groups_resumed_runs_and_includes_failed_attempt_cost() -> None:
351
+ first_report = {
352
+ "task_id": "task-7",
353
+ "run_id": "run-1",
354
+ "repository": "team/project",
355
+ "outcome": "fail",
356
+ "cost_usd": "0.20",
357
+ }
358
+ second_report = {
359
+ "task_id": "task-7",
360
+ "run_id": "run-2",
361
+ "repository": "team/project",
362
+ "outcome": "pass",
363
+ "cost_usd": "0.30",
364
+ }
365
+ stdin_text = "\n".join(map(json.dumps, [first_report, second_report]))
366
+ completed = invoke_cli(["summary"], stdin_text)
367
+ assert completed.returncode == 0, completed.stderr
368
+ summary = json.loads(completed.stdout)
369
+ assert summary["task_count"] == 1
370
+ assert summary["passed_task_count"] == 1
371
+ assert Decimal(summary["total_cost_usd"]) == Decimal("0.50")
372
+ assert Decimal(summary["cost_per_passed_task_usd"]) == Decimal("0.50")
373
+ assert summary["tasks"][0]["run_ids"] == ["run-1", "run-2"]
374
+
375
+
376
+ @pytest.mark.parametrize("missing_field", ["cost_usd", "outcome"])
377
+ def test_summary_marks_missing_cost_or_outcome_unknown(missing_field: str) -> None:
378
+ source_report = {
379
+ "task_id": "task-7",
380
+ "run_id": "run-1",
381
+ "repository": "team/project",
382
+ "outcome": "pass",
383
+ "cost_usd": "0.20",
384
+ }
385
+ del source_report[missing_field]
386
+ completed = invoke_cli(["summary"], json.dumps(source_report))
387
+ assert completed.returncode == 0, completed.stderr
388
+ summary = json.loads(completed.stdout)
389
+ assert summary["cost_per_passed_task_usd"] is None
390
+ assert summary["status"] == "unknown"
391
+
392
+
393
+ def test_summary_deduplicates_identical_run_rows() -> None:
394
+ source_report = {
395
+ "task_id": "task-7",
396
+ "run_id": "run-1",
397
+ "repository": "team/project",
398
+ "outcome": "pass",
399
+ "cost_usd": "0.20",
400
+ }
401
+ completed = invoke_cli(
402
+ ["summary"], "\n".join(map(json.dumps, [source_report, source_report]))
403
+ )
404
+ assert completed.returncode == 0, completed.stderr
405
+ summary = json.loads(completed.stdout)
406
+ assert Decimal(summary["total_cost_usd"]) == Decimal("0.20")
407
+ assert summary["tasks"][0]["run_ids"] == ["run-1"]
408
+
409
+
410
+ def test_claude_otel_accepts_flat_event_attributes() -> None:
411
+ source_payload = {
412
+ "event.name": "api_request",
413
+ "session.id": "session-1",
414
+ "model": "claude-sonnet",
415
+ "input_tokens": 4,
416
+ "cache_creation_tokens": 2,
417
+ "cache_read_tokens": 3,
418
+ "output_tokens": 1,
419
+ "cost_usd": 0.002,
420
+ }
421
+ report = report_for("claude-otel", source_payload)
422
+ assert report["uncached_input_tokens"] == 4
423
+ assert report["cache_write_tokens"] == 2
424
+ assert report["cost_usd"] == "0.002"
425
+
426
+
427
+ def test_codex_mixed_models_need_each_models_rate() -> None:
428
+ all_events = [
429
+ {
430
+ "type": "turn.completed",
431
+ "turn_id": f"turn-{each_index}",
432
+ "model": each_model,
433
+ "usage": {
434
+ "input_tokens": 1_000_000,
435
+ "cached_input_tokens": 0,
436
+ "cache_write_input_tokens": 0,
437
+ "output_tokens": 0,
438
+ },
439
+ }
440
+ for each_index, each_model in enumerate(("gpt-small", "gpt-large"))
441
+ ]
442
+ stdin_text = "\n".join(
443
+ map(
444
+ json.dumps,
445
+ [{"type": "thread.started", "thread_id": "thread-1"}, *all_events],
446
+ )
447
+ )
448
+ base_arguments = [*RUN_ARGUMENTS, "--source-format", "codex-json"]
449
+ rate_provenance = [
450
+ "--rate-source",
451
+ "vendor-price-card",
452
+ "--rate-effective-date",
453
+ "2026-09-24",
454
+ ]
455
+ partial = invoke_cli(
456
+ [*base_arguments, "--rate", "gpt-small=1,0,0,0", *rate_provenance],
457
+ stdin_text,
458
+ )
459
+ complete = invoke_cli(
460
+ [
461
+ *base_arguments,
462
+ "--rate",
463
+ "gpt-small=1,0,0,0",
464
+ "--rate",
465
+ "gpt-large=3,0,0,0",
466
+ *rate_provenance,
467
+ ],
468
+ stdin_text,
469
+ )
470
+ assert partial.returncode == 0, partial.stderr
471
+ assert complete.returncode == 0, complete.stderr
472
+ assert json.loads(partial.stdout)["cost_usd"] is None
473
+ complete_report = json.loads(complete.stdout)
474
+ assert complete_report["cost_usd"] == "4"
475
+ assert complete_report["models"] == ["gpt-large", "gpt-small"]
476
+
477
+
478
+ def test_otel_rejects_mixed_sessions_without_leaking_identifiers() -> None:
479
+ all_events = [
480
+ {
481
+ "body": "claude_code.api_request",
482
+ "attributes": {
483
+ "session.id": each_session,
484
+ "request_id": f"request-{each_index}",
485
+ "model": "claude-opus",
486
+ "input_tokens": 1,
487
+ "cache_creation_tokens": 0,
488
+ "cache_read_tokens": 0,
489
+ "output_tokens": 1,
490
+ "cost_usd": 0.01,
491
+ },
492
+ }
493
+ for each_index, each_session in enumerate(
494
+ ("SECRET_SESSION_A", "SECRET_SESSION_B")
495
+ )
496
+ ]
497
+ completed = invoke_cli(
498
+ [*RUN_ARGUMENTS, "--source-format", "claude-otel"],
499
+ "\n".join(map(json.dumps, all_events)),
500
+ )
501
+ assert completed.returncode != 0
502
+ assert completed.stdout == ""
503
+ assert "SECRET_SESSION" not in completed.stderr
504
+
505
+
506
+ def test_otel_rejects_non_api_record_from_another_session() -> None:
507
+ all_records = [
508
+ {
509
+ "body": "claude_code.api_request",
510
+ "attributes": {"session.id": "SECRET_SESSION_A"},
511
+ },
512
+ {
513
+ "body": "claude_code.user_prompt",
514
+ "attributes": {
515
+ "session.id": "SECRET_SESSION_B",
516
+ "prompt": "SECRET_PROMPT",
517
+ },
518
+ },
519
+ ]
520
+ completed = invoke_cli(
521
+ [*RUN_ARGUMENTS, "--source-format", "claude-otel"], json.dumps(all_records)
522
+ )
523
+ assert completed.returncode != 0
524
+ assert completed.stdout == ""
525
+ assert "one session ID" in completed.stderr
526
+ assert "SECRET_SESSION" not in completed.stderr
527
+ assert "SECRET_PROMPT" not in completed.stderr
528
+
529
+
530
+ def test_otel_rejects_capture_without_api_request() -> None:
531
+ source_payload = {
532
+ "body": "claude_code.user_prompt",
533
+ "attributes": {"session.id": "session-1"},
534
+ }
535
+ completed = invoke_cli(
536
+ [*RUN_ARGUMENTS, "--source-format", "claude-otel"], json.dumps(source_payload)
537
+ )
538
+ assert completed.returncode != 0
539
+ assert completed.stdout == ""
540
+
541
+
542
+ def test_codex_rejects_concatenated_threads() -> None:
543
+ all_events = [
544
+ {"type": "thread.started", "thread_id": "thread-1"},
545
+ {"type": "turn.completed", "usage": {"input_tokens": 1}},
546
+ {"type": "thread.started", "thread_id": "thread-2"},
547
+ {"type": "turn.completed", "usage": {"input_tokens": 1}},
548
+ ]
549
+ completed = invoke_cli(
550
+ [*RUN_ARGUMENTS, "--source-format", "codex-json"],
551
+ "\n".join(map(json.dumps, all_events)),
552
+ )
553
+ assert completed.returncode != 0
554
+ assert completed.stdout == ""
555
+
556
+
557
+ def test_codex_rate_requires_source_and_effective_date() -> None:
558
+ all_events = [
559
+ {"type": "thread.started", "thread_id": "thread-1"},
560
+ {
561
+ "type": "turn.completed",
562
+ "model": "gpt-x",
563
+ "usage": {
564
+ "input_tokens": 1,
565
+ "cached_input_tokens": 0,
566
+ "cache_write_input_tokens": 0,
567
+ "output_tokens": 1,
568
+ },
569
+ },
570
+ ]
571
+ completed = invoke_cli(
572
+ [*RUN_ARGUMENTS, "--source-format", "codex-json", "--rate", "gpt-x=1,0,0,1"],
573
+ "\n".join(map(json.dumps, all_events)),
574
+ )
575
+ assert completed.returncode != 0
576
+ assert completed.stdout == ""
577
+
578
+
579
+ def test_codex_fixture_without_cache_write_field_uses_zero_write_cost() -> None:
580
+ completed = invoke_cli(
581
+ [
582
+ *RUN_ARGUMENTS,
583
+ "--source-format",
584
+ "codex-json",
585
+ "--model",
586
+ "gpt-x",
587
+ "--rate",
588
+ "gpt-x=1,2,0.1,4",
589
+ "--rate-source",
590
+ "vendor-price-card",
591
+ "--rate-effective-date",
592
+ "2026-09-24",
593
+ ],
594
+ CODEX_STREAM_FIXTURE.read_text(encoding="utf-8"),
595
+ )
596
+ assert completed.returncode == 0, completed.stderr
597
+ report = json.loads(completed.stdout)
598
+ assert report["uncached_input_tokens"] == 12
599
+ assert report["cache_write_tokens"] == 0
600
+ assert report["cache_read_tokens"] == 0
601
+ assert report["output_tokens"] == 6
602
+ assert report["cost_usd"] == "0.000036"
603
+ assert report["cost_basis"] == "caller_rates"
@@ -86,7 +86,7 @@ def test_governed_paths_lists_the_shipped_surfaces(tmp_path: Path) -> None:
86
86
  def test_governed_paths_cover_instruction_surfaces_and_skip_archives() -> None:
87
87
  assert governs_path("rules/git-workflow.md")
88
88
  assert governs_path(".agents/skills/eli5/SKILL.md")
89
- assert governs_path("audit-rubrics/prompts/category-e-dead-code.md")
89
+ assert governs_path("audit-rubrics/prompts/category-o-docstring-vs-impl-drift.md")
90
90
  assert governs_path("system-prompts/software-engineer.xml")
91
91
  assert not governs_path("rules-archived/manifest.md")
92
92
  assert not governs_path("scripts/banned_prose_words.py")
@@ -134,3 +134,19 @@ def test_archived_hook_moves_leave_the_automatic_checks(tmp_path: Path) -> None:
134
134
  assert runtime_set.documents == ()
135
135
  assert runtime_set.deleted_paths == (live_hook_path,)
136
136
  assert runtime_set.renamed_paths == ()
137
+
138
+
139
+ def test_archived_audit_rubric_moves_leave_the_automatic_checks(tmp_path: Path) -> None:
140
+ live_rubric_path = PurePosixPath("packages/tool/audit-rubrics/prompts/category-k.md")
141
+ archived_rubric_path = PurePosixPath("packages/tool/audit-rubrics-archived/prompts/category-k.md")
142
+ document_set = DocumentSet(
143
+ (Document.from_text(archived_rubric_path, "archived"),),
144
+ SelectionKind.BASE,
145
+ tmp_path,
146
+ (),
147
+ ((live_rubric_path, archived_rubric_path),),
148
+ )
149
+ runtime_set = _runtime_document_set(document_set)
150
+ assert runtime_set.documents == ()
151
+ assert runtime_set.deleted_paths == (live_rubric_path,)
152
+ assert runtime_set.renamed_paths == ()
@@ -209,7 +209,7 @@ def test_changed_line_filter_drops_other_located_line(tmp_path: Path) -> None:
209
209
  assert lint_report.diagnostics == ()
210
210
 
211
211
 
212
- def test_lint_keeps_located_findings_when_changed_lines_are_empty(
212
+ def test_lint_drops_located_findings_when_a_change_only_deletes_lines(
213
213
  tmp_path: Path,
214
214
  ) -> None:
215
215
  repository_root = tmp_path.resolve()
@@ -233,7 +233,7 @@ def test_lint_keeps_located_findings_when_changed_lines_are_empty(
233
233
  (document,),
234
234
  (_changed_document_rule("probe", collect_diagnostics),),
235
235
  )
236
- assert len(lint_report.diagnostics) == 1
236
+ assert lint_report.diagnostics == ()
237
237
 
238
238
 
239
239
  def test_code_rule_diagnostics_includes_comment_policy_without_prior_text(