okstra 0.179.2 → 0.183.2

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 (258) hide show
  1. package/README.md +1 -1
  2. package/dist/cli-registry.mjs +30 -2
  3. package/dist/cli-registry.mjs.map +1 -1
  4. package/dist/commands/execute/incremental-carry.mjs +9 -8
  5. package/dist/commands/execute/incremental-carry.mjs.map +1 -1
  6. package/dist/commands/execute/plan-verify.mjs +3 -1
  7. package/dist/commands/execute/plan-verify.mjs.map +1 -1
  8. package/dist/commands/execute/render-bundle.d.mts +4 -2
  9. package/dist/commands/execute/render-bundle.mjs +46 -5
  10. package/dist/commands/execute/render-bundle.mjs.map +1 -1
  11. package/dist/commands/execute/run.mjs +11 -3
  12. package/dist/commands/execute/run.mjs.map +1 -1
  13. package/dist/commands/inspect/model-io.d.mts +1 -0
  14. package/dist/commands/inspect/model-io.mjs +25 -0
  15. package/dist/commands/inspect/model-io.mjs.map +1 -0
  16. package/dist/commands/inspect/stage-map.mjs +29 -8
  17. package/dist/commands/inspect/stage-map.mjs.map +1 -1
  18. package/dist/commands/inspect/task-list.mjs +52 -6
  19. package/dist/commands/inspect/task-list.mjs.map +1 -1
  20. package/dist/commands/inspect/user-response.mjs +14 -4
  21. package/dist/commands/inspect/user-response.mjs.map +1 -1
  22. package/dist/commands/lifecycle/check-project.d.mts +1 -0
  23. package/dist/commands/lifecycle/check-project.mjs +69 -50
  24. package/dist/commands/lifecycle/check-project.mjs.map +1 -1
  25. package/dist/commands/lifecycle/contract-check.d.mts +1 -0
  26. package/dist/commands/lifecycle/contract-check.mjs +18 -0
  27. package/dist/commands/lifecycle/contract-check.mjs.map +1 -0
  28. package/dist/commands/lifecycle/preflight.mjs +154 -51
  29. package/dist/commands/lifecycle/preflight.mjs.map +1 -1
  30. package/dist/commands/pr/pr.d.mts +1 -0
  31. package/dist/commands/pr/pr.mjs +19 -1
  32. package/dist/commands/pr/pr.mjs.map +1 -1
  33. package/dist/commands/report/agent-activity.mjs +2 -2
  34. package/dist/commands/report/approval-decision.d.mts +1 -0
  35. package/dist/commands/report/approval-decision.mjs +21 -0
  36. package/dist/commands/report/approval-decision.mjs.map +1 -0
  37. package/dist/commands/report/design-snapshot.d.mts +1 -0
  38. package/dist/commands/report/design-snapshot.mjs +19 -0
  39. package/dist/commands/report/design-snapshot.mjs.map +1 -0
  40. package/dist/commands/report/translate.mjs +3 -0
  41. package/dist/commands/report/translate.mjs.map +1 -1
  42. package/dist/lib/host-registry-client.mjs +13 -9
  43. package/dist/lib/host-registry-client.mjs.map +1 -1
  44. package/docs/architecture/storage-model.md +1 -1
  45. package/docs/architecture.md +21 -10
  46. package/docs/cli.md +36 -20
  47. package/docs/container.md +6 -4
  48. package/docs/contributor-change-matrix.md +1 -1
  49. package/docs/for-ai/README.md +2 -2
  50. package/docs/for-ai/skills/okstra-brief-gen.md +5 -3
  51. package/docs/for-ai/skills/okstra-code-review.md +4 -4
  52. package/docs/for-ai/skills/okstra-container-build.md +20 -17
  53. package/docs/for-ai/skills/okstra-inspect.md +20 -23
  54. package/docs/for-ai/skills/okstra-manager.md +19 -18
  55. package/docs/for-ai/skills/okstra-memory.md +2 -2
  56. package/docs/for-ai/skills/okstra-pr-gen.md +3 -3
  57. package/docs/for-ai/skills/okstra-rollup.md +14 -13
  58. package/docs/for-ai/skills/okstra-run.md +7 -3
  59. package/docs/for-ai/skills/okstra-schedule-gen.md +15 -18
  60. package/docs/for-ai/skills/okstra-setup.md +7 -7
  61. package/docs/for-ai/skills/okstra-usage.md +5 -4
  62. package/docs/for-ai/skills/okstra-user-response.md +50 -32
  63. package/docs/project-structure-overview.md +43 -31
  64. package/docs/task-process/implementation-planning.md +2 -2
  65. package/docs/task-process/implementation.md +7 -4
  66. package/package.json +1 -1
  67. package/runtime/BUILD.json +2 -2
  68. package/runtime/agents/workers/claude-worker.md +4 -4
  69. package/runtime/agents/workers/report-writer-worker.md +15 -164
  70. package/runtime/agents/workers/translator-worker.md +5 -13
  71. package/runtime/bin/okstra-error-log.py +51 -11
  72. package/runtime/bin/okstra-report-translate.py +210 -23
  73. package/runtime/prompts/host-orchestration/implementation.md +1 -1
  74. package/runtime/prompts/launch.template.md +9 -12
  75. package/runtime/prompts/lead/adapters/cmux.md +1 -1
  76. package/runtime/prompts/lead/context-loader.md +41 -141
  77. package/runtime/prompts/lead/convergence.md +10 -8
  78. package/runtime/prompts/lead/okstra-lead-contract.md +40 -49
  79. package/runtime/prompts/lead/plan-body-verification.md +217 -23
  80. package/runtime/prompts/lead/report-writer.md +81 -421
  81. package/runtime/prompts/lead/team-contract.md +9 -54
  82. package/runtime/prompts/profiles/_clarification-recommendation.md +5 -4
  83. package/runtime/prompts/profiles/_coding-conventions-preflight.md +3 -2
  84. package/runtime/prompts/profiles/_common-contract.md +4 -4
  85. package/runtime/prompts/profiles/_implementation-deliverable.md +1 -1
  86. package/runtime/prompts/profiles/_implementation-diff-review.md +1 -1
  87. package/runtime/prompts/profiles/_implementation-verifier.md +3 -3
  88. package/runtime/prompts/profiles/change-impact-analysis.md +1 -1
  89. package/runtime/prompts/profiles/error-analysis.md +1 -1
  90. package/runtime/prompts/profiles/feature-analysis.md +1 -1
  91. package/runtime/prompts/profiles/final-verification.md +1 -1
  92. package/runtime/prompts/profiles/implementation-planning.md +20 -12
  93. package/runtime/prompts/profiles/improvement-discovery.md +1 -1
  94. package/runtime/prompts/profiles/project-analysis.md +1 -1
  95. package/runtime/prompts/profiles/release-handoff.md +0 -1
  96. package/runtime/prompts/profiles/requirements-discovery.md +1 -1
  97. package/runtime/prompts/wizard/prompts.ko.json +0 -1
  98. package/runtime/python/okstra_ctl/adapters/hosts/capability_adapter.py +69 -17
  99. package/runtime/python/okstra_ctl/adapters/hosts/claude-code/adapter.py +13 -4
  100. package/runtime/python/okstra_ctl/adapters/hosts/claude-code/relay.md +8 -2
  101. package/runtime/python/okstra_ctl/adapters/hosts/codex/adapter.py +2 -2
  102. package/runtime/python/okstra_ctl/adapters/hosts/codex/relay.md +50 -5
  103. package/runtime/python/okstra_ctl/adapters/hosts/external/relay.md +1 -1
  104. package/runtime/python/okstra_ctl/adapters/hosts/grok/adapter.py +2 -2
  105. package/runtime/python/okstra_ctl/adapters/hosts/grok/relay.md +66 -5
  106. package/runtime/python/okstra_ctl/adapters/providers/grok/adapter.py +70 -2
  107. package/runtime/python/okstra_ctl/agent_activity.py +141 -38
  108. package/runtime/python/okstra_ctl/agent_invocation.py +19 -6
  109. package/runtime/python/okstra_ctl/agent_prompt_cli.py +70 -23
  110. package/runtime/python/okstra_ctl/analysis_inputs.py +5 -4
  111. package/runtime/python/okstra_ctl/analysis_packet.py +124 -3
  112. package/runtime/python/okstra_ctl/approval_decisions.py +328 -0
  113. package/runtime/python/okstra_ctl/attempt_evidence.py +2 -2
  114. package/runtime/python/okstra_ctl/backfill.py +13 -10
  115. package/runtime/python/okstra_ctl/batch.py +2 -4
  116. package/runtime/python/okstra_ctl/build_tools.py +6 -3
  117. package/runtime/python/okstra_ctl/claim_reproduction.py +101 -0
  118. package/runtime/python/okstra_ctl/clarification_items.py +27 -13
  119. package/runtime/python/okstra_ctl/cmux.py +130 -52
  120. package/runtime/python/okstra_ctl/code_review_target.py +34 -8
  121. package/runtime/python/okstra_ctl/consumers.py +5 -4
  122. package/runtime/python/okstra_ctl/container.py +103 -8
  123. package/runtime/python/okstra_ctl/context_cost.py +2 -1
  124. package/runtime/python/okstra_ctl/contract_graph.py +497 -0
  125. package/runtime/python/okstra_ctl/contract_graph_cli.py +62 -0
  126. package/runtime/python/okstra_ctl/convergence.py +338 -17
  127. package/runtime/python/okstra_ctl/convergence_engine.py +10 -18
  128. package/runtime/python/okstra_ctl/convergence_provenance.py +58 -8
  129. package/runtime/python/okstra_ctl/convergence_store.py +55 -34
  130. package/runtime/python/okstra_ctl/design_prep.py +7 -4
  131. package/runtime/python/okstra_ctl/design_snapshot.py +134 -0
  132. package/runtime/python/okstra_ctl/dispatch_core.py +97 -69
  133. package/runtime/python/okstra_ctl/dispatch_state.py +163 -63
  134. package/runtime/python/okstra_ctl/doctor.py +6 -3
  135. package/runtime/python/okstra_ctl/domain/worker_presentation.py +70 -9
  136. package/runtime/python/okstra_ctl/entrypoints/hosts.py +16 -30
  137. package/runtime/python/okstra_ctl/error_log_write.py +35 -30
  138. package/runtime/python/okstra_ctl/error_report.py +26 -1
  139. package/runtime/python/okstra_ctl/error_zip.py +27 -5
  140. package/runtime/python/okstra_ctl/execution_identity.py +3 -2
  141. package/runtime/python/okstra_ctl/execution_manifest.py +7 -4
  142. package/runtime/python/okstra_ctl/execution_mutation_audit.py +6 -2
  143. package/runtime/python/okstra_ctl/final_report_schema.py +26 -17
  144. package/runtime/python/okstra_ctl/fix_cycles.py +2 -2
  145. package/runtime/python/okstra_ctl/fixed_text.py +39 -0
  146. package/runtime/python/okstra_ctl/git_reconcile.py +41 -9
  147. package/runtime/python/okstra_ctl/handoff.py +5 -4
  148. package/runtime/python/okstra_ctl/i18n.py +4 -2
  149. package/runtime/python/okstra_ctl/implementation_direction.py +22 -14
  150. package/runtime/python/okstra_ctl/implementation_outcome.py +4 -7
  151. package/runtime/python/okstra_ctl/incremental_carry.py +130 -17
  152. package/runtime/python/okstra_ctl/incremental_scope.py +12 -4
  153. package/runtime/python/okstra_ctl/index.py +8 -11
  154. package/runtime/python/okstra_ctl/initial_prompt_materialization.py +79 -7
  155. package/runtime/python/okstra_ctl/invocation.py +3 -6
  156. package/runtime/python/okstra_ctl/json_boundary.py +366 -0
  157. package/runtime/python/okstra_ctl/json_registry.py +10 -12
  158. package/runtime/python/okstra_ctl/jsonl.py +19 -2
  159. package/runtime/python/okstra_ctl/lead_events.py +33 -1
  160. package/runtime/python/okstra_ctl/listing.py +3 -3
  161. package/runtime/python/okstra_ctl/log_report.py +24 -2
  162. package/runtime/python/okstra_ctl/manager_cli.py +92 -7
  163. package/runtime/python/okstra_ctl/manager_store.py +12 -10
  164. package/runtime/python/okstra_ctl/material.py +5 -1
  165. package/runtime/python/okstra_ctl/migrate.py +29 -25
  166. package/runtime/python/okstra_ctl/model_cli.py +3 -15
  167. package/runtime/python/okstra_ctl/model_io_cli.py +1051 -0
  168. package/runtime/python/okstra_ctl/mutation_probe.py +13 -4
  169. package/runtime/python/okstra_ctl/pane_reclaim.py +3 -2
  170. package/runtime/python/okstra_ctl/path_hints.py +12 -0
  171. package/runtime/python/okstra_ctl/paths.py +21 -0
  172. package/runtime/python/okstra_ctl/plan_items.py +115 -4
  173. package/runtime/python/okstra_ctl/plan_items_cli.py +626 -34
  174. package/runtime/python/okstra_ctl/ports/worker_dispatch.py +2 -1
  175. package/runtime/python/okstra_ctl/pr_template.py +3 -2
  176. package/runtime/python/okstra_ctl/project_meta.py +5 -7
  177. package/runtime/python/okstra_ctl/recap.py +5 -4
  178. package/runtime/python/okstra_ctl/reconcile.py +21 -27
  179. package/runtime/python/okstra_ctl/registry/host_discovery.py +3 -2
  180. package/runtime/python/okstra_ctl/registry/provider_registry.py +3 -2
  181. package/runtime/python/okstra_ctl/render.py +74 -16
  182. package/runtime/python/okstra_ctl/render_final_report.py +10 -8
  183. package/runtime/python/okstra_ctl/report_assembly.py +509 -0
  184. package/runtime/python/okstra_ctl/report_contract.py +2 -1
  185. package/runtime/python/okstra_ctl/report_finalize.py +67 -32
  186. package/runtime/python/okstra_ctl/report_html/render.py +3 -2
  187. package/runtime/python/okstra_ctl/report_inputs.py +72 -0
  188. package/runtime/python/okstra_ctl/report_language.py +3 -2
  189. package/runtime/python/okstra_ctl/report_markdown.py +81 -8
  190. package/runtime/python/okstra_ctl/report_narrative.py +351 -0
  191. package/runtime/python/okstra_ctl/report_projections.py +265 -0
  192. package/runtime/python/okstra_ctl/report_synthesis_packet.py +518 -0
  193. package/runtime/python/okstra_ctl/report_views.py +3 -2
  194. package/runtime/python/okstra_ctl/rollup.py +65 -4
  195. package/runtime/python/okstra_ctl/run.py +184 -65
  196. package/runtime/python/okstra_ctl/run_audit.py +3 -2
  197. package/runtime/python/okstra_ctl/run_context.py +6 -9
  198. package/runtime/python/okstra_ctl/run_index_row.py +2 -8
  199. package/runtime/python/okstra_ctl/schedule_semantics.py +5 -2
  200. package/runtime/python/okstra_ctl/schema_excerpt.py +15 -8
  201. package/runtime/python/okstra_ctl/session_transcript.py +27 -1
  202. package/runtime/python/okstra_ctl/set_work_status.py +64 -38
  203. package/runtime/python/okstra_ctl/stage_fix_carry.py +8 -6
  204. package/runtime/python/okstra_ctl/stage_ledger.py +132 -18
  205. package/runtime/python/okstra_ctl/stage_map.py +96 -28
  206. package/runtime/python/okstra_ctl/stage_targets.py +3 -4
  207. package/runtime/python/okstra_ctl/team.py +3 -2
  208. package/runtime/python/okstra_ctl/team_reconcile.py +11 -2
  209. package/runtime/python/okstra_ctl/time_report.py +51 -4
  210. package/runtime/python/okstra_ctl/usage_identity.py +2 -1
  211. package/runtime/python/okstra_ctl/usage_report.py +58 -4
  212. package/runtime/python/okstra_ctl/user_response.py +1431 -66
  213. package/runtime/python/okstra_ctl/wizard.py +11 -44
  214. package/runtime/python/okstra_ctl/work_categories.py +3 -2
  215. package/runtime/python/okstra_ctl/worker_dispatch.py +5 -2
  216. package/runtime/python/okstra_ctl/worker_prompt_body.py +49 -3
  217. package/runtime/python/okstra_ctl/worker_prompt_contract.py +3 -2
  218. package/runtime/python/okstra_ctl/worker_prompt_policy.py +31 -3
  219. package/runtime/python/okstra_ctl/worker_runner.py +14 -12
  220. package/runtime/python/okstra_ctl/workflow.py +2 -1
  221. package/runtime/python/okstra_ctl/worktree.py +3 -2
  222. package/runtime/python/okstra_ctl/wrapper_status.py +4 -2
  223. package/runtime/python/okstra_ctl/write_policy.py +4 -2
  224. package/runtime/python/okstra_token_usage/antigravity.py +39 -12
  225. package/runtime/python/okstra_token_usage/collect.py +90 -38
  226. package/runtime/python/okstra_token_usage/grok.py +127 -0
  227. package/runtime/schemas/final-report-v2.0.schema.json +9 -0
  228. package/runtime/schemas/final-report-v3.0.schema.json +10219 -0
  229. package/runtime/schemas/report-narrative-v3.0.schema.json +30 -0
  230. package/runtime/schemas/report-synthesis-packet-v1.0.schema.json +140 -0
  231. package/runtime/skills/okstra-brief-gen/SKILL.md +9 -7
  232. package/runtime/skills/okstra-code-review/SKILL.md +21 -11
  233. package/runtime/skills/okstra-container-build/SKILL.md +18 -18
  234. package/runtime/skills/okstra-inspect/SKILL.md +12 -11
  235. package/runtime/skills/okstra-inspect/facets/error-zip.md +8 -8
  236. package/runtime/skills/okstra-inspect/facets/errors.md +2 -2
  237. package/runtime/skills/okstra-inspect/facets/history.md +9 -14
  238. package/runtime/skills/okstra-inspect/facets/logs.md +2 -2
  239. package/runtime/skills/okstra-inspect/facets/recap.md +5 -5
  240. package/runtime/skills/okstra-inspect/facets/report.md +6 -10
  241. package/runtime/skills/okstra-inspect/facets/status.md +9 -8
  242. package/runtime/skills/okstra-inspect/facets/time.md +3 -3
  243. package/runtime/skills/okstra-manager/SKILL.md +16 -14
  244. package/runtime/skills/okstra-memory/SKILL.md +3 -3
  245. package/runtime/skills/okstra-pr-gen/SKILL.md +5 -4
  246. package/runtime/skills/okstra-rollup/SKILL.md +6 -16
  247. package/runtime/skills/okstra-run/SKILL.md +7 -7
  248. package/runtime/skills/okstra-schedule-gen/SKILL.md +21 -17
  249. package/runtime/skills/okstra-setup/SKILL.md +21 -13
  250. package/runtime/skills/okstra-setup/references/project-config.md +2 -2
  251. package/runtime/skills/okstra-usage/SKILL.md +10 -10
  252. package/runtime/skills/okstra-user-response/SKILL.md +78 -107
  253. package/runtime/templates/report-writer-prompt-preamble.md +29 -19
  254. package/runtime/templates/reports/html/macros/forms.html +6 -4
  255. package/runtime/templates/reports/schedule.template.md +4 -4
  256. package/runtime/templates/worker-error-contract.md +17 -29
  257. package/runtime/validators/validate-run.py +636 -72
  258. package/runtime/validators/validate_session_conformance.py +43 -7
@@ -36,6 +36,11 @@ import tempfile
36
36
  from pathlib import Path
37
37
 
38
38
  from okstra_ctl.session import resolve_inproc_lead_session_id
39
+ from okstra_ctl.json_boundary import (
40
+ JsonBoundaryError,
41
+ external_claude_team_json_source,
42
+ load_external_json,
43
+ )
39
44
 
40
45
 
41
46
  def live_pane_ids() -> tuple[bool, set[str]]:
@@ -148,8 +153,12 @@ def _reconcile_config(cfg_path: Path, dry_run: bool, print_members: bool) -> int
148
153
  line per non-lead member so the lead knows whom to ``shutdown_request``.
149
154
  """
150
155
  try:
151
- config = json.loads(cfg_path.read_text())
152
- except (OSError, json.JSONDecodeError) as exc:
156
+ # 외부 입력: Claude Code가 소유하는 team roster 설정이다.
157
+ config = load_external_json(
158
+ external_claude_team_json_source(cfg_path),
159
+ artifact="Claude team configuration",
160
+ )
161
+ except JsonBoundaryError as exc:
153
162
  print(f"team-reconcile: cannot read team config at {cfg_path}: {exc}",
154
163
  file=sys.stderr)
155
164
  return 1
@@ -14,7 +14,10 @@ import json
14
14
  import sys
15
15
  from pathlib import Path
16
16
 
17
+ from .fixed_text import line as _text_line
18
+
17
19
  from okstra_ctl.paths import resolve_under_root, task_timeline_file
20
+ from okstra_ctl.json_boundary import load_owned_object
18
21
  from okstra_project import read_task_key
19
22
  from okstra_ctl.task_target import resolve_task_root
20
23
 
@@ -25,7 +28,7 @@ def time_rows_with_identity(run_root: Path) -> list[dict]:
25
28
 
26
29
  state_path = run_root / "state" / "team-state.json"
27
30
  try:
28
- state = json.loads(state_path.read_text(encoding="utf-8"))
31
+ state = load_owned_object(state_path, artifact="team state")
29
32
  except (OSError, ValueError):
30
33
  return []
31
34
  return identity_rows_from_state(state if isinstance(state, dict) else {})
@@ -34,7 +37,7 @@ def time_rows_with_identity(run_root: Path) -> list[dict]:
34
37
  def load_runs(task_root: Path) -> tuple[list[dict], bool]:
35
38
  path = task_timeline_file(task_root)
36
39
  try:
37
- data = json.loads(path.read_text(encoding="utf-8"))
40
+ data = load_owned_object(path, artifact="task timeline")
38
41
  except (OSError, ValueError):
39
42
  return [], True
40
43
  runs = data.get("runs", []) if isinstance(data, dict) else []
@@ -96,7 +99,7 @@ def _read_state(project_root: Path, rel: str) -> dict | None:
96
99
  if target is None:
97
100
  return None
98
101
  try:
99
- state = json.loads(target.read_text(encoding="utf-8"))
102
+ state = load_owned_object(target, artifact="team state")
100
103
  except (OSError, ValueError):
101
104
  return None
102
105
  return state if isinstance(state, dict) else None
@@ -211,6 +214,48 @@ def aggregate_time(runs: list[dict], project_root: Path) -> dict:
211
214
  }
212
215
 
213
216
 
217
+ def _worker_text(result: dict) -> str:
218
+ parts: list[str] = []
219
+ workers = result.get("perWorker")
220
+ if not isinstance(workers, dict):
221
+ return ""
222
+ for task_type, rows in workers.items():
223
+ for row in rows if isinstance(rows, list) else []:
224
+ if isinstance(row, dict):
225
+ parts.extend(("\n## Worker\n\n", _text_line("Task type", task_type), _text_line("Worker ID", row.get("workerId")), _text_line("Agents", ",".join(row.get("agents", [])) if isinstance(row.get("agents"), list) else None), _text_line("Runs", row.get("runs")), _text_line("Total ms", row.get("totalMs")), _text_line("Average ms", row.get("avgMs"))))
226
+ return "".join(parts)
227
+
228
+
229
+ def _timeline_text(result: dict) -> str:
230
+ parts: list[str] = []
231
+ for row in result.get("perRunWallClock", []):
232
+ if isinstance(row, dict):
233
+ parts.extend(("\n## Run wall clock\n\n", _text_line("Run timestamp", row.get("runTimestamp")), _text_line("Task type", row.get("taskType")), _text_line("Wall clock ms", row.get("wallClockMs"))))
234
+ for timeline in result.get("phaseTimelines", []):
235
+ if not isinstance(timeline, dict):
236
+ continue
237
+ for phase in timeline.get("phases", []):
238
+ if isinstance(phase, dict):
239
+ parts.extend(("\n## Phase\n\n", _text_line("Run timestamp", timeline.get("runTimestamp")), _text_line("Task type", timeline.get("taskType")), _text_line("Phase", phase.get("phase")), _text_line("First at", phase.get("firstAt")), _text_line("Wall ms to next", phase.get("wallMsToNext"))))
240
+ return "".join(parts)
241
+
242
+
243
+ def render_result_text(result: dict) -> str:
244
+ parts = ["# Okstra Time Input\n\n", _text_line("Task key", result.get("taskKey"))]
245
+ total = result.get("grandTotal") if isinstance(result.get("grandTotal"), dict) else {}
246
+ for key, label in (("runs", "Total runs"), ("leadMs", "Total lead ms"), ("workersMs", "Total workers ms"), ("cpuSumMs", "Total CPU sum ms")):
247
+ parts.append(_text_line(label, total.get(key)))
248
+ for row in result.get("byTaskType", []):
249
+ if not isinstance(row, dict):
250
+ continue
251
+ parts.extend(("\n## Task type\n\n", _text_line("Task type", row.get("taskType")), _text_line("Runs", row.get("runs")), _text_line("Lead ms", row.get("leadMs")), _text_line("Workers ms", row.get("workersMs")), _text_line("CPU sum ms", row.get("cpuSumMs"))))
252
+ parts.extend((_worker_text(result), _timeline_text(result)))
253
+ for row in result.get("unavailable", []):
254
+ if isinstance(row, dict):
255
+ parts.extend(("\n## Unavailable run\n\n", _text_line("Run timestamp", row.get("runTimestamp")), _text_line("Task type", row.get("taskType")), _text_line("Reason", row.get("reason"))))
256
+ return "".join(parts)
257
+
258
+
214
259
  def main(argv: list[str] | None = None) -> int:
215
260
  parser = argparse.ArgumentParser(
216
261
  prog="okstra time-report",
@@ -219,13 +264,15 @@ def main(argv: list[str] | None = None) -> int:
219
264
  parser.add_argument("--project-root", default="")
220
265
  parser.add_argument("--cwd", default=".")
221
266
  parser.add_argument("--json", action="store_true", help="emit JSON (always on)")
267
+ parser.add_argument("--text", action="store_true", help="emit fixed text fields")
222
268
  args = parser.parse_args(argv)
223
269
 
224
270
  task_root, project_root = resolve_task_root(args.target, args.project_root, args.cwd)
225
271
  runs, _ = load_runs(task_root)
226
272
  result = {"ok": True, "taskKey": read_task_key(task_root)}
227
273
  result.update(aggregate_time(runs, project_root))
228
- print(json.dumps(result, ensure_ascii=False, indent=2))
274
+ output = render_result_text(result) if args.text else json.dumps(result, ensure_ascii=False, indent=2)
275
+ print(output, end="" if args.text else "\n")
229
276
  return 0
230
277
 
231
278
 
@@ -13,6 +13,7 @@ from .execution_identity import (
13
13
  stored_identity,
14
14
  )
15
15
  from .execution_manifest import read_execution_manifest
16
+ from .json_boundary import load_owned_object
16
17
 
17
18
 
18
19
  def run_execution_manifest(run_root: Path) -> ExecutionManifest:
@@ -83,4 +84,4 @@ def _manifest_path(run_root: Path) -> Path:
83
84
 
84
85
 
85
86
  def _load_json(path: Path) -> dict[str, Any]:
86
- return json.loads(path.read_text(encoding="utf-8"))
87
+ return load_owned_object(path, artifact="usage identity source")
@@ -14,6 +14,8 @@ from okstra_ctl.time_report import (
14
14
  wall_clock_ms,
15
15
  load_runs,
16
16
  )
17
+ from okstra_ctl.json_boundary import load_owned_object
18
+ from okstra_ctl.fixed_text import line
17
19
  from okstra_project import ResolverError, list_project_tasks, resolve_project_root
18
20
 
19
21
  UTC = dt.timezone.utc
@@ -43,8 +45,8 @@ def _parse_timestamp(value: Any) -> dt.datetime | None:
43
45
 
44
46
  def _project_id(project_root: Path) -> str:
45
47
  try:
46
- data = json.loads(
47
- (project_root / ".okstra" / "project.json").read_text(encoding="utf-8")
48
+ data = load_owned_object(
49
+ project_root / ".okstra" / "project.json", artifact="project config"
48
50
  )
49
51
  except (OSError, ValueError):
50
52
  return ""
@@ -214,6 +216,9 @@ def build_usage_report(project_root: Path, days: int, now: dt.datetime) -> dict:
214
216
  rows = sorted(buckets.values(), key=lambda row: row["firstRunAt"])
215
217
  for row in rows:
216
218
  row["costUsd"] = round(_float_value(row["costUsd"]), 4)
219
+ row["collectionRate"] = (
220
+ row["collectedRuns"] / row["runs"] if row["runs"] else 0.0
221
+ )
217
222
  totals = {
218
223
  "runs": total_runs,
219
224
  "collectedRuns": collected_runs,
@@ -264,18 +269,67 @@ def main(argv: list[str] | None = None) -> int:
264
269
  parser.add_argument("--project-root", default="")
265
270
  parser.add_argument("--cwd", default=".")
266
271
  parser.add_argument("--json", action="store_true", help="emit JSON (always on)")
272
+ parser.add_argument("--text", action="store_true", help="emit fixed text fields")
267
273
  args = parser.parse_args(argv)
268
274
  try:
269
275
  project_root = Path(
270
276
  resolve_project_root(explicit_root=args.project_root, cwd=args.cwd)
271
277
  ).resolve()
272
278
  except ResolverError as exc:
273
- print(json.dumps({"ok": False, "stage": "resolve", "reason": str(exc)}))
279
+ payload = {"ok": False, "stage": "resolve", "reason": str(exc)}
280
+ print(render_usage_text(payload) if args.text else json.dumps(payload))
274
281
  return 2
275
282
  result = build_usage_report(project_root, args.days, utc_now())
276
- print(json.dumps(result, ensure_ascii=False, indent=2))
283
+ print(
284
+ render_usage_text(result)
285
+ if args.text
286
+ else json.dumps(result, ensure_ascii=False, indent=2)
287
+ )
277
288
  return 0
278
289
 
279
290
 
291
+ def render_usage_text(payload: dict) -> str:
292
+ """usage 모델 표면에 승인된 집계만 고정 순서로 투영한다."""
293
+ rows = ["Okstra usage\n"]
294
+ rows.append(line("Status", "ready" if payload.get("ok") else "error"))
295
+ rows.append(line("Project ID", payload.get("projectId")))
296
+ period = payload.get("range") if isinstance(payload.get("range"), dict) else {}
297
+ rows.append(line("Range days", period.get("days")))
298
+ runs = payload.get("runs") if isinstance(payload.get("runs"), dict) else {}
299
+ rows.append(line("Runs total", runs.get("total")))
300
+ rows.append(line("Runs collected", runs.get("collected")))
301
+ rows.append(line("Runs collection rate", runs.get("collectionRate")))
302
+ task_types = payload.get("byTaskType") if isinstance(payload.get("byTaskType"), list) else []
303
+ for index, item in enumerate(task_types, 1):
304
+ if not isinstance(item, dict):
305
+ continue
306
+ rows.append(line(f"Task type {index} name", item.get("taskType")))
307
+ rows.append(line(f"Task type {index} runs", item.get("runs")))
308
+ rows.append(line(f"Task type {index} collected runs", item.get("collectedRuns")))
309
+ rows.append(line(f"Task type {index} collection rate", item.get("collectionRate")))
310
+ rows.append(line(f"Task type {index} raw tokens", item.get("rawTokens")))
311
+ rows.append(line(f"Task type {index} billable tokens", item.get("billableTokens")))
312
+ rows.append(line(f"Task type {index} cost USD", item.get("costUsd")))
313
+ rows.append(line(f"Task type {index} CPU sum ms", item.get("cpuSumMs")))
314
+ rows.append(line(f"Task type {index} wall clock ms", item.get("wallClockMs")))
315
+ totals = payload.get("totals") if isinstance(payload.get("totals"), dict) else {}
316
+ for label, key in (("Raw tokens", "rawTokens"), ("Billable tokens", "billableTokens"),
317
+ ("Cost USD", "costUsd"), ("CPU sum ms", "cpuSumMs"),
318
+ ("Wall clock ms", "wallClockMs")):
319
+ rows.append(line(label, totals.get(key)))
320
+ for prefix, key in (("Unavailable", "unavailable"), ("Warning", "warnings")):
321
+ values = payload.get(key) if isinstance(payload.get(key), dict) else {}
322
+ for index, (name, count) in enumerate(sorted(values.items()), 1):
323
+ rows.append(line(f"{prefix} {index} reason", name))
324
+ rows.append(line(f"{prefix} {index} count", count))
325
+ models = payload.get("unmatchedModels") if isinstance(payload.get("unmatchedModels"), list) else []
326
+ for index, model in enumerate(models, 1):
327
+ rows.append(line(f"Unmatched model {index} name", model))
328
+ if not payload.get("ok"):
329
+ rows.append(line("Failure stage", payload.get("stage")))
330
+ rows.append(line("Failure reason", payload.get("reason")))
331
+ return "".join(rows)
332
+
333
+
280
334
  if __name__ == "__main__":
281
335
  raise SystemExit(main(sys.argv[1:]))