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.
- package/.agents/agents/test_agent_frontmatter.py +0 -72
- package/.agents/agents-archived/code-quality-agent.md +16 -16
- package/.agents/skills/_shared/end-of-run-gotcha-recommendations.md +5 -21
- package/.agents/skills/e-code-review/SKILL.md +1 -3
- package/.agents/skills/e-simplify/SKILL.md +6 -6
- package/.agents/skills/eli5/SKILL.md +4 -14
- package/.agents/skills/eli5/reference/task-seeds.md +3 -4
- package/.agents/skills/everything-search/SKILL.md +3 -3
- package/.agents/skills/fresh-branch/SKILL.md +2 -3
- package/.agents/skills/grok-spawn/SKILL.md +3 -8
- package/.agents/skills/issue-tracker/SKILL.md +4 -6
- package/.agents/skills/orchestrator-refresh/SKILL.md +7 -7
- package/.agents/skills/plugin-eval-standalone-skill/SKILL.md +3 -3
- package/.agents/skills/privacy-hygiene/SKILL.md +6 -10
- package/.agents/skills/privacy-hygiene/reference/sweep-procedure.md +1 -1
- package/.agents/skills/pull-request/SKILL.md +11 -14
- package/.agents/skills/pull-request/reference/publication-tasks.md +2 -4
- package/.agents/skills/recovering-codex-startup/SKILL.md +3 -6
- package/.agents/skills/recovering-codex-startup/reference/task-seeds.md +1 -1
- package/.agents/skills/repairing-hook-boundaries/SKILL.md +4 -7
- package/.agents/skills/repairing-hook-boundaries/reference/task-seeds.md +1 -2
- package/.agents/skills/second-account-workers/.claude/CLAUDE.md +1 -0
- package/.agents/skills/second-account-workers/AGENTS.md +1 -0
- package/.agents/skills/second-account-workers/SKILL.md +65 -0
- package/.agents/skills/syncing-submodules/SKILL.md +7 -17
- package/.agents/skills/syncing-submodules/reference/run-tasks.md +1 -1
- package/.agents/skills/task-build/SKILL.md +1 -1
- package/.agents/skills/task-build/reference/tool-routing.md +1 -1
- package/.agents/skills/team-advisor/SKILL.md +2 -4
- package/.agents/skills/usage-pause/SKILL.md +1 -1
- package/.agents/skills/windows-scheduled-task/SKILL.md +1 -1
- package/AGENTS.md +1 -1
- package/_shared/advisor/advisor-protocol.md +4 -4
- package/_shared/advisor/reference/consult-format.md +1 -1
- package/_shared/advisor/reference/identity.md +2 -2
- package/_shared/advisor/reference/warm-up.md +1 -1
- package/audit-rubrics/category_rubrics/category-o-docstring-vs-impl-drift.md +1 -1
- package/audit-rubrics/prompts/category-o-docstring-vs-impl-drift.md +2 -16
- package/bin/ever-shipped-skills.mjs +1 -0
- package/bin/install.mjs +1 -0
- package/bin/install.prompt-context.test.mjs +46 -0
- package/bin/install.test.mjs +3 -2
- package/commands/sr-loop.md +2 -2
- package/docs/CODE_RULES.md +2 -2
- package/docs/DJANGO_PATTERNS.md +5 -5
- package/docs/REACT_PATTERNS.md +6 -6
- package/docs/TEST_QUALITY.md +10 -28
- package/docs/agent-spawn-protocol.md +4 -4
- package/docs/codex-compatibility.md +4 -33
- package/docs/references/team-advisor-skill.md +0 -1
- package/docs/second-claude-account.md +4 -0
- package/docs/worker-completion-gate.md +0 -1
- package/hooks/advisory/migration_safety_advisor.py +3 -2
- package/hooks/advisory/refactor_guard.py +3 -3
- package/hooks/hooks.json +0 -5
- package/hooks/hooks_constants/issue_tracker_session_starter_constants.py +1 -2
- package/hooks/hooks_constants/orchestrator_auto_starter_constants.py +1 -2
- package/package.json +1 -1
- package/rules/explore-thoroughly.md +0 -1
- package/rules/filesystem-search.md +1 -1
- package/rules/git-workflow.md +5 -5
- package/rules/long-horizon-autonomy.md +1 -5
- package/rules/re-stage-before-commit.md +0 -2
- package/rules/research-mode.md +1 -1
- package/rules/shell-invocation.md +1 -1
- package/scripts/claude_account_worker.py +347 -0
- package/scripts/claude_account_worker_process.py +169 -0
- package/scripts/claude_account_worker_report.py +170 -0
- package/scripts/dev_env_scripts_constants/claude_account_worker_constants.py +139 -0
- package/scripts/dev_env_scripts_constants/grok_worker_constants.py +8 -2
- package/scripts/grok_headless_runner.py +2 -5
- package/scripts/policy_lint/config/constants.py +1 -0
- package/scripts/policy_lint/engine.py +5 -2
- package/scripts/repository_checks/config/constants.py +1 -1
- package/scripts/spawn_grok_batch.py +12 -1
- package/scripts/subprocess_stream_text.py +28 -0
- package/scripts/task_usage_events.py +387 -0
- package/scripts/task_usage_report.py +391 -0
- package/scripts/test_claude_account_worker.py +331 -0
- package/scripts/test_spawn_grok_batch.py +22 -0
- package/scripts/test_task_usage_events.py +47 -0
- package/scripts/test_task_usage_report.py +603 -0
- package/scripts/tests/test_banned_prose_words.py +1 -1
- package/scripts/tests/test_policy_lint_rules_archive.py +16 -0
- package/scripts/tests/test_policy_lint_rules_engine.py +2 -2
- package/scripts/tests/test_rules.py +9 -9
- package/system-prompts/software-engineer.xml +15 -30
- package/audit-rubrics/audit-categories.json +0 -700
- package/audit-rubrics/category_rubrics/category-a-api-contracts.md +0 -90
- package/audit-rubrics/category_rubrics/category-b-selector-engine-compat.md +0 -36
- package/audit-rubrics/category_rubrics/category-c-resource-cleanup.md +0 -35
- package/audit-rubrics/category_rubrics/category-d-scoping-and-ordering.md +0 -35
- package/audit-rubrics/category_rubrics/category-e-dead-code.md +0 -40
- package/audit-rubrics/category_rubrics/category-f-silent-failures.md +0 -41
- package/audit-rubrics/category_rubrics/category-g-bounds-and-overflow.md +0 -38
- package/audit-rubrics/category_rubrics/category-h-security-boundaries.md +0 -40
- package/audit-rubrics/category_rubrics/category-i-concurrency.md +0 -38
- package/audit-rubrics/category_rubrics/category-j-code-rules-compliance.md +0 -59
- package/audit-rubrics/category_rubrics/category-k-codebase-conflicts.md +0 -66
- package/audit-rubrics/category_rubrics/category-l-behavior-equivalence.md +0 -48
- package/audit-rubrics/category_rubrics/category-m-producer-consumer-cardinality.md +0 -44
- package/audit-rubrics/category_rubrics/category-n-test-name-scenario-verifier.md +0 -54
- package/audit-rubrics/category_rubrics/category-p-name-vs-behavior-contract.md +0 -59
- package/audit-rubrics/category_rubrics/category-q-cross-surface-claims.md +0 -46
- package/audit-rubrics/prompts/category-a-api-contracts.md +0 -407
- package/audit-rubrics/prompts/category-b-selector-engine-compat.md +0 -401
- package/audit-rubrics/prompts/category-c-resource-cleanup.md +0 -420
- package/audit-rubrics/prompts/category-d-scoping-and-ordering.md +0 -414
- package/audit-rubrics/prompts/category-e-dead-code.md +0 -434
- package/audit-rubrics/prompts/category-f-silent-failures.md +0 -432
- package/audit-rubrics/prompts/category-g-bounds-and-overflow.md +0 -383
- package/audit-rubrics/prompts/category-h-security-boundaries.md +0 -423
- package/audit-rubrics/prompts/category-i-concurrency.md +0 -429
- package/audit-rubrics/prompts/category-j-code-rules-compliance.md +0 -458
- package/audit-rubrics/prompts/category-k-codebase-conflicts.md +0 -338
- package/audit-rubrics/prompts/category-l-behavior-equivalence.md +0 -131
- package/audit-rubrics/prompts/category-m-producer-consumer-cardinality.md +0 -129
- package/audit-rubrics/prompts/category-n-test-name-scenario-verifier.md +0 -139
- package/audit-rubrics/prompts/category-p-name-vs-behavior-contract.md +0 -89
- package/audit-rubrics/prompts/category-q-cross-surface-claims.md +0 -51
- package/hooks/hooks_constants/style_reminder_prompt_constants.py +0 -9
- package/hooks/session/style_reminder_prompt.py +0 -41
- package/hooks/session/test_style_reminder_prompt.py +0 -32
|
@@ -0,0 +1,391 @@
|
|
|
1
|
+
"""Reduce vendor usage events to task costs without retaining event content.
|
|
2
|
+
|
|
3
|
+
::
|
|
4
|
+
|
|
5
|
+
claude -p --output-format json ... | python task_usage_report.py run \
|
|
6
|
+
--task-id task-1 --run-id run-1 --repository team/project \
|
|
7
|
+
--outcome pass --source-format claude-json
|
|
8
|
+
|
|
9
|
+
cat run-rows.jsonl | python task_usage_report.py summary
|
|
10
|
+
|
|
11
|
+
Each resumed invocation needs a new run ID under the same task ID.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import argparse
|
|
17
|
+
import json
|
|
18
|
+
import sys
|
|
19
|
+
from dataclasses import dataclass, field
|
|
20
|
+
from datetime import date
|
|
21
|
+
from decimal import Decimal
|
|
22
|
+
from enum import Enum
|
|
23
|
+
|
|
24
|
+
from task_usage_events import (
|
|
25
|
+
ModelRates,
|
|
26
|
+
UsageEvent,
|
|
27
|
+
UsageReportRunFatal,
|
|
28
|
+
_cost_basis,
|
|
29
|
+
_display_money,
|
|
30
|
+
_parse_claude_json,
|
|
31
|
+
_parse_codex_event,
|
|
32
|
+
_parse_otel_event,
|
|
33
|
+
_parse_rate,
|
|
34
|
+
_read_money,
|
|
35
|
+
_require_label,
|
|
36
|
+
_require_list,
|
|
37
|
+
_require_object,
|
|
38
|
+
_require_one_codex_thread,
|
|
39
|
+
_require_one_otel_session,
|
|
40
|
+
_safe_label,
|
|
41
|
+
_sum_costs,
|
|
42
|
+
_sum_known,
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@dataclass
|
|
47
|
+
class TaskRuns:
|
|
48
|
+
run_ids: list[str] = field(default_factory=list)
|
|
49
|
+
costs: list[Decimal | None] = field(default_factory=list)
|
|
50
|
+
outcomes: list[str | None] = field(default_factory=list)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class RunOutcome(str, Enum):
|
|
54
|
+
PASS = "pass"
|
|
55
|
+
FAIL = "fail"
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _rate_provenance(arguments: argparse.Namespace) -> tuple[str | None, str | None]:
|
|
59
|
+
if not arguments.rate:
|
|
60
|
+
if arguments.rate_source or arguments.rate_effective_date:
|
|
61
|
+
raise ValueError("rate source and date need a rate")
|
|
62
|
+
return None, None
|
|
63
|
+
if (
|
|
64
|
+
arguments.source_format != "codex-json"
|
|
65
|
+
or not arguments.rate_source
|
|
66
|
+
or not arguments.rate_effective_date
|
|
67
|
+
):
|
|
68
|
+
raise ValueError("Codex rates need a source and effective date")
|
|
69
|
+
rate_source = _require_label(arguments.rate_source, "rate source")
|
|
70
|
+
try:
|
|
71
|
+
effective_date = date.fromisoformat(arguments.rate_effective_date)
|
|
72
|
+
except ValueError as error:
|
|
73
|
+
raise ValueError("rate effective date needs YYYY-MM-DD") from error
|
|
74
|
+
return rate_source, effective_date.isoformat()
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _expand_otel_document(
|
|
78
|
+
all_document_fields: dict[str, object],
|
|
79
|
+
) -> list[dict[str, object]]:
|
|
80
|
+
if "resourceLogs" not in all_document_fields:
|
|
81
|
+
return [all_document_fields]
|
|
82
|
+
all_records: list[dict[str, object]] = []
|
|
83
|
+
for each_resource in _require_list(
|
|
84
|
+
all_document_fields["resourceLogs"], "resourceLogs"
|
|
85
|
+
):
|
|
86
|
+
resource = _require_object(each_resource, "resource log")
|
|
87
|
+
for each_scope in _require_list(resource.get("scopeLogs", []), "scopeLogs"):
|
|
88
|
+
scope = _require_object(each_scope, "scope log")
|
|
89
|
+
all_records.extend(
|
|
90
|
+
_require_object(each_record, "log record")
|
|
91
|
+
for each_record in _require_list(
|
|
92
|
+
scope.get("logRecords", []), "logRecords"
|
|
93
|
+
)
|
|
94
|
+
)
|
|
95
|
+
return all_records
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _parse_documents(stdin_text: str) -> list[dict[str, object]]:
|
|
99
|
+
if not stdin_text.strip():
|
|
100
|
+
raise ValueError("stdin needs vendor output")
|
|
101
|
+
try:
|
|
102
|
+
parsed_document = json.loads(stdin_text)
|
|
103
|
+
except json.JSONDecodeError:
|
|
104
|
+
return [
|
|
105
|
+
_require_object(json.loads(each_line), "event")
|
|
106
|
+
for each_line in stdin_text.splitlines()
|
|
107
|
+
if each_line.strip()
|
|
108
|
+
]
|
|
109
|
+
if isinstance(parsed_document, list):
|
|
110
|
+
return [
|
|
111
|
+
_require_object(each_document, "event") for each_document in parsed_document
|
|
112
|
+
]
|
|
113
|
+
return [_require_object(parsed_document, "event")]
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def _deduplicate_events(all_events: list[UsageEvent]) -> list[UsageEvent]:
|
|
117
|
+
event_by_id: dict[str, UsageEvent] = {}
|
|
118
|
+
unique_events: list[UsageEvent] = []
|
|
119
|
+
for each_event in all_events:
|
|
120
|
+
if each_event.event_id is None:
|
|
121
|
+
unique_events.append(each_event)
|
|
122
|
+
continue
|
|
123
|
+
previous_event = event_by_id.get(each_event.event_id)
|
|
124
|
+
if previous_event is not None and previous_event != each_event:
|
|
125
|
+
raise UsageReportRunFatal("duplicate event ID has conflicting usage")
|
|
126
|
+
if previous_event is None:
|
|
127
|
+
event_by_id[each_event.event_id] = each_event
|
|
128
|
+
unique_events.append(each_event)
|
|
129
|
+
return unique_events
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def _usage_totals(all_events: list[UsageEvent]) -> dict[str, object]:
|
|
133
|
+
return {
|
|
134
|
+
"uncached_input_tokens": _sum_known(
|
|
135
|
+
[each_event.uncached_input_tokens for each_event in all_events]
|
|
136
|
+
),
|
|
137
|
+
"cache_write_tokens": _sum_known(
|
|
138
|
+
[each_event.cache_write_tokens for each_event in all_events]
|
|
139
|
+
),
|
|
140
|
+
"cache_read_tokens": _sum_known(
|
|
141
|
+
[each_event.cache_read_tokens for each_event in all_events]
|
|
142
|
+
),
|
|
143
|
+
"output_tokens": _sum_known(
|
|
144
|
+
[each_event.generated_tokens for each_event in all_events]
|
|
145
|
+
),
|
|
146
|
+
"reasoning_output_tokens": _sum_known(
|
|
147
|
+
[each_event.reasoning_generated_tokens for each_event in all_events]
|
|
148
|
+
),
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def _collect_claude_events(
|
|
153
|
+
all_documents: list[dict[str, object]],
|
|
154
|
+
) -> tuple[list[UsageEvent], Decimal | None]:
|
|
155
|
+
if len(all_documents) != 1:
|
|
156
|
+
raise ValueError("Claude JSON needs one result object")
|
|
157
|
+
return _parse_claude_json(all_documents[0])
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def _collect_otel_events(
|
|
161
|
+
all_documents: list[dict[str, object]],
|
|
162
|
+
) -> tuple[list[UsageEvent], Decimal | None]:
|
|
163
|
+
all_records = [
|
|
164
|
+
each_record
|
|
165
|
+
for each_document in all_documents
|
|
166
|
+
for each_record in _expand_otel_document(each_document)
|
|
167
|
+
]
|
|
168
|
+
_require_one_otel_session(all_records)
|
|
169
|
+
all_events = [
|
|
170
|
+
each_event
|
|
171
|
+
for each_record in all_records
|
|
172
|
+
if (each_event := _parse_otel_event(each_record)) is not None
|
|
173
|
+
]
|
|
174
|
+
if not all_events:
|
|
175
|
+
raise ValueError("OTel input needs an API request")
|
|
176
|
+
unique_events = _deduplicate_events(all_events)
|
|
177
|
+
return unique_events, _sum_costs(
|
|
178
|
+
[each_event.cost_usd for each_event in unique_events]
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def _collect_codex_events(
|
|
183
|
+
all_documents: list[dict[str, object]],
|
|
184
|
+
fallback_model: str,
|
|
185
|
+
rates_by_model: dict[str, ModelRates],
|
|
186
|
+
) -> tuple[list[UsageEvent], Decimal | None]:
|
|
187
|
+
_require_one_codex_thread(all_documents)
|
|
188
|
+
all_events = [
|
|
189
|
+
each_event
|
|
190
|
+
for each_document in all_documents
|
|
191
|
+
if (
|
|
192
|
+
each_event := _parse_codex_event(
|
|
193
|
+
each_document, fallback_model, rates_by_model
|
|
194
|
+
)
|
|
195
|
+
)
|
|
196
|
+
is not None
|
|
197
|
+
]
|
|
198
|
+
unique_events = _deduplicate_events(all_events)
|
|
199
|
+
return unique_events, _sum_costs(
|
|
200
|
+
[each_event.cost_usd for each_event in unique_events]
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def _collect_run_events(
|
|
205
|
+
arguments: argparse.Namespace,
|
|
206
|
+
all_documents: list[dict[str, object]],
|
|
207
|
+
rates_by_model: dict[str, ModelRates],
|
|
208
|
+
) -> tuple[list[UsageEvent], Decimal | None]:
|
|
209
|
+
if arguments.source_format == "claude-json":
|
|
210
|
+
return _collect_claude_events(all_documents)
|
|
211
|
+
if arguments.source_format == "claude-otel":
|
|
212
|
+
return _collect_otel_events(all_documents)
|
|
213
|
+
return _collect_codex_events(
|
|
214
|
+
all_documents, _safe_label(arguments.model), rates_by_model
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def _build_run_report(
|
|
219
|
+
arguments: argparse.Namespace, stdin_text: str
|
|
220
|
+
) -> dict[str, object]:
|
|
221
|
+
all_documents = _parse_documents(stdin_text)
|
|
222
|
+
rate_source, rate_effective_date = _rate_provenance(arguments)
|
|
223
|
+
rates_by_model = dict(_parse_rate(each_rate) for each_rate in arguments.rate)
|
|
224
|
+
all_events, total_cost = _collect_run_events(
|
|
225
|
+
arguments, all_documents, rates_by_model
|
|
226
|
+
)
|
|
227
|
+
cost_basis = _cost_basis(arguments.source_format, total_cost)
|
|
228
|
+
return {
|
|
229
|
+
"task_id": _require_label(arguments.task_id, "task ID"),
|
|
230
|
+
"run_id": _require_label(arguments.run_id, "run ID"),
|
|
231
|
+
"repository": _require_label(arguments.repository, "repository"),
|
|
232
|
+
"source_format": arguments.source_format,
|
|
233
|
+
"outcome": arguments.outcome,
|
|
234
|
+
"models": sorted({each_event.model for each_event in all_events}),
|
|
235
|
+
"event_count": len(all_events),
|
|
236
|
+
**_usage_totals(all_events),
|
|
237
|
+
"cost_usd": _display_money(total_cost),
|
|
238
|
+
"cost_basis": cost_basis,
|
|
239
|
+
"rate_source": rate_source if cost_basis == "caller_rates" else None,
|
|
240
|
+
"rate_effective_date": (
|
|
241
|
+
rate_effective_date if cost_basis == "caller_rates" else None
|
|
242
|
+
),
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def _read_summary_row(
|
|
247
|
+
all_document_fields: dict[str, object],
|
|
248
|
+
) -> tuple[tuple[str, str, str], str | None, Decimal | None]:
|
|
249
|
+
repository = _require_summary_label(all_document_fields.get("repository"))
|
|
250
|
+
task_id = _require_summary_label(all_document_fields.get("task_id"))
|
|
251
|
+
run_id = _require_summary_label(all_document_fields.get("run_id"))
|
|
252
|
+
outcome = all_document_fields.get("outcome")
|
|
253
|
+
if outcome is not None and (
|
|
254
|
+
not isinstance(outcome, str)
|
|
255
|
+
or outcome not in tuple(each_outcome.value for each_outcome in RunOutcome)
|
|
256
|
+
):
|
|
257
|
+
raise ValueError("summary outcome must be pass, fail, or missing")
|
|
258
|
+
return (
|
|
259
|
+
(repository, task_id, run_id),
|
|
260
|
+
outcome,
|
|
261
|
+
_read_money(all_document_fields, "cost_usd"),
|
|
262
|
+
)
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
def _require_summary_label(candidate: object) -> str:
|
|
266
|
+
label = _safe_label(candidate)
|
|
267
|
+
if label == "unknown":
|
|
268
|
+
raise ValueError("summary row needs repository, task ID, and run ID")
|
|
269
|
+
return label
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
def _summarize_run_rows(all_documents: list[dict[str, object]]) -> dict[str, object]:
|
|
273
|
+
run_by_key: dict[tuple[str, str, str], tuple[str | None, Decimal | None]] = {}
|
|
274
|
+
for each_document in all_documents:
|
|
275
|
+
run_key, outcome, cost = _read_summary_row(each_document)
|
|
276
|
+
previous_run = run_by_key.get(run_key)
|
|
277
|
+
if previous_run is not None and previous_run != (outcome, cost):
|
|
278
|
+
raise UsageReportRunFatal(
|
|
279
|
+
"duplicate run ID has conflicting cost or outcome"
|
|
280
|
+
)
|
|
281
|
+
run_by_key[run_key] = (outcome, cost)
|
|
282
|
+
task_by_key: dict[tuple[str, str], TaskRuns] = {}
|
|
283
|
+
for (each_repository, each_task_id, each_run_id), (
|
|
284
|
+
each_outcome,
|
|
285
|
+
each_cost,
|
|
286
|
+
) in sorted(run_by_key.items()):
|
|
287
|
+
task_runs = task_by_key.setdefault((each_repository, each_task_id), TaskRuns())
|
|
288
|
+
task_runs.run_ids.append(each_run_id)
|
|
289
|
+
task_runs.outcomes.append(each_outcome)
|
|
290
|
+
task_runs.costs.append(each_cost)
|
|
291
|
+
return _build_summary(task_by_key)
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
def _task_summary_row(
|
|
295
|
+
repository: str, task_id: str, task_runs: TaskRuns
|
|
296
|
+
) -> tuple[dict[str, object], Decimal | None, bool | None]:
|
|
297
|
+
is_outcome_known = all(
|
|
298
|
+
each_outcome is not None for each_outcome in task_runs.outcomes
|
|
299
|
+
)
|
|
300
|
+
is_task_passed = RunOutcome.PASS.value in task_runs.outcomes
|
|
301
|
+
task_cost = _sum_costs(task_runs.costs)
|
|
302
|
+
return (
|
|
303
|
+
{
|
|
304
|
+
"repository": repository,
|
|
305
|
+
"task_id": task_id,
|
|
306
|
+
"run_ids": task_runs.run_ids,
|
|
307
|
+
"passed": is_task_passed if is_outcome_known else None,
|
|
308
|
+
"cost_usd": _display_money(task_cost),
|
|
309
|
+
},
|
|
310
|
+
task_cost,
|
|
311
|
+
is_task_passed if is_outcome_known else None,
|
|
312
|
+
)
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
def _build_summary(task_by_key: dict[tuple[str, str], TaskRuns]) -> dict[str, object]:
|
|
316
|
+
all_tasks: list[dict[str, object]] = []
|
|
317
|
+
all_costs: list[Decimal | None] = []
|
|
318
|
+
passed_task_count = 0
|
|
319
|
+
is_outcome_known = bool(task_by_key)
|
|
320
|
+
for (each_repository, each_task_id), each_task in sorted(task_by_key.items()):
|
|
321
|
+
task_row, task_cost, is_task_passed = _task_summary_row(
|
|
322
|
+
each_repository, each_task_id, each_task
|
|
323
|
+
)
|
|
324
|
+
is_outcome_known = is_outcome_known and is_task_passed is not None
|
|
325
|
+
passed_task_count += int(bool(is_task_passed))
|
|
326
|
+
all_costs.append(task_cost)
|
|
327
|
+
all_tasks.append(task_row)
|
|
328
|
+
total_cost = _sum_costs(all_costs)
|
|
329
|
+
is_complete = is_outcome_known and total_cost is not None and passed_task_count > 0
|
|
330
|
+
cost_per_pass = (
|
|
331
|
+
total_cost / passed_task_count
|
|
332
|
+
if is_complete and total_cost is not None
|
|
333
|
+
else None
|
|
334
|
+
)
|
|
335
|
+
return {
|
|
336
|
+
"status": "known" if is_complete else "unknown",
|
|
337
|
+
"task_count": len(all_tasks),
|
|
338
|
+
"passed_task_count": passed_task_count if is_outcome_known else None,
|
|
339
|
+
"total_cost_usd": _display_money(total_cost),
|
|
340
|
+
"cost_per_passed_task_usd": _display_money(cost_per_pass),
|
|
341
|
+
"tasks": all_tasks,
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
def _create_parser() -> argparse.ArgumentParser:
|
|
346
|
+
parser = argparse.ArgumentParser(description=__doc__)
|
|
347
|
+
commands = parser.add_subparsers(dest="command", required=True)
|
|
348
|
+
run_parser = commands.add_parser("run", help="reduce one vendor run from stdin")
|
|
349
|
+
run_parser.add_argument("--task-id", required=True)
|
|
350
|
+
run_parser.add_argument("--run-id", required=True)
|
|
351
|
+
run_parser.add_argument("--repository", required=True)
|
|
352
|
+
run_parser.add_argument(
|
|
353
|
+
"--outcome",
|
|
354
|
+
choices=tuple(each_outcome.value for each_outcome in RunOutcome),
|
|
355
|
+
required=True,
|
|
356
|
+
)
|
|
357
|
+
run_parser.add_argument(
|
|
358
|
+
"--source-format",
|
|
359
|
+
choices=("claude-json", "claude-otel", "codex-json"),
|
|
360
|
+
required=True,
|
|
361
|
+
)
|
|
362
|
+
run_parser.add_argument("--model", help="Codex model when turn events omit it")
|
|
363
|
+
run_parser.add_argument(
|
|
364
|
+
"--rate", action="append", default=[], metavar="MODEL=INPUT,WRITE,READ,OUTPUT"
|
|
365
|
+
)
|
|
366
|
+
run_parser.add_argument("--rate-source", help="source for caller-supplied prices")
|
|
367
|
+
run_parser.add_argument(
|
|
368
|
+
"--rate-effective-date", help="date for caller-supplied prices"
|
|
369
|
+
)
|
|
370
|
+
commands.add_parser("summary", help="group run reports from stdin")
|
|
371
|
+
return parser
|
|
372
|
+
|
|
373
|
+
|
|
374
|
+
def _main() -> int:
|
|
375
|
+
parser = _create_parser()
|
|
376
|
+
parsed_arguments = parser.parse_args()
|
|
377
|
+
try:
|
|
378
|
+
stdin_text = sys.stdin.read()
|
|
379
|
+
if parsed_arguments.command == "run":
|
|
380
|
+
report = _build_run_report(parsed_arguments, stdin_text)
|
|
381
|
+
else:
|
|
382
|
+
report = _summarize_run_rows(_parse_documents(stdin_text))
|
|
383
|
+
except (ValueError, json.JSONDecodeError) as error:
|
|
384
|
+
sys.stderr.write(f"invalid input: {error}\n")
|
|
385
|
+
return 2
|
|
386
|
+
sys.stdout.write(json.dumps(report, sort_keys=True) + "\n")
|
|
387
|
+
return 0
|
|
388
|
+
|
|
389
|
+
|
|
390
|
+
if __name__ == "__main__":
|
|
391
|
+
raise SystemExit(_main())
|