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,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())