okstra 0.180.0 → 0.184.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (233) hide show
  1. package/README.md +1 -1
  2. package/dist/cli-registry.mjs +16 -2
  3. package/dist/cli-registry.mjs.map +1 -1
  4. package/dist/commands/execute/render-bundle.d.mts +4 -2
  5. package/dist/commands/execute/render-bundle.mjs +46 -5
  6. package/dist/commands/execute/render-bundle.mjs.map +1 -1
  7. package/dist/commands/execute/run.mjs +11 -3
  8. package/dist/commands/execute/run.mjs.map +1 -1
  9. package/dist/commands/inspect/model-io.d.mts +1 -0
  10. package/dist/commands/inspect/model-io.mjs +25 -0
  11. package/dist/commands/inspect/model-io.mjs.map +1 -0
  12. package/dist/commands/inspect/stage-map.mjs +29 -8
  13. package/dist/commands/inspect/stage-map.mjs.map +1 -1
  14. package/dist/commands/inspect/task-list.mjs +52 -6
  15. package/dist/commands/inspect/task-list.mjs.map +1 -1
  16. package/dist/commands/inspect/user-response.mjs +14 -4
  17. package/dist/commands/inspect/user-response.mjs.map +1 -1
  18. package/dist/commands/lifecycle/check-project.d.mts +1 -0
  19. package/dist/commands/lifecycle/check-project.mjs +69 -50
  20. package/dist/commands/lifecycle/check-project.mjs.map +1 -1
  21. package/dist/commands/lifecycle/contract-check.d.mts +1 -0
  22. package/dist/commands/lifecycle/contract-check.mjs +18 -0
  23. package/dist/commands/lifecycle/contract-check.mjs.map +1 -0
  24. package/dist/commands/lifecycle/preflight.mjs +154 -51
  25. package/dist/commands/lifecycle/preflight.mjs.map +1 -1
  26. package/dist/commands/pr/pr.d.mts +1 -0
  27. package/dist/commands/pr/pr.mjs +19 -1
  28. package/dist/commands/pr/pr.mjs.map +1 -1
  29. package/dist/commands/report/agent-activity.mjs +2 -2
  30. package/dist/commands/report/translate.mjs +3 -0
  31. package/dist/commands/report/translate.mjs.map +1 -1
  32. package/dist/lib/host-registry-client.mjs +13 -9
  33. package/dist/lib/host-registry-client.mjs.map +1 -1
  34. package/docs/architecture.md +13 -2
  35. package/docs/cli.md +31 -15
  36. package/docs/container.md +6 -4
  37. package/docs/contributor-change-matrix.md +1 -1
  38. package/docs/for-ai/README.md +2 -2
  39. package/docs/for-ai/skills/okstra-brief-gen.md +5 -3
  40. package/docs/for-ai/skills/okstra-code-review.md +4 -4
  41. package/docs/for-ai/skills/okstra-container-build.md +20 -17
  42. package/docs/for-ai/skills/okstra-inspect.md +20 -23
  43. package/docs/for-ai/skills/okstra-manager.md +19 -18
  44. package/docs/for-ai/skills/okstra-memory.md +2 -2
  45. package/docs/for-ai/skills/okstra-pr-gen.md +3 -3
  46. package/docs/for-ai/skills/okstra-rollup.md +14 -13
  47. package/docs/for-ai/skills/okstra-run.md +7 -3
  48. package/docs/for-ai/skills/okstra-schedule-gen.md +15 -18
  49. package/docs/for-ai/skills/okstra-setup.md +7 -7
  50. package/docs/for-ai/skills/okstra-usage.md +5 -4
  51. package/docs/for-ai/skills/okstra-user-response.md +50 -32
  52. package/docs/project-structure-overview.md +30 -27
  53. package/docs/task-process/README.md +1 -1
  54. package/docs/task-process/common-flow.md +2 -3
  55. package/docs/task-process/error-analysis.md +3 -4
  56. package/docs/task-process/final-verification.md +2 -3
  57. package/docs/task-process/implementation-planning.md +2 -3
  58. package/docs/task-process/implementation.md +9 -7
  59. package/docs/task-process/release-handoff.md +3 -4
  60. package/docs/task-process/requirements-discovery.md +3 -4
  61. package/package.json +1 -1
  62. package/runtime/BUILD.json +2 -2
  63. package/runtime/agents/workers/claude-worker.md +4 -4
  64. package/runtime/agents/workers/report-writer-worker.md +3 -3
  65. package/runtime/agents/workers/translator-worker.md +5 -13
  66. package/runtime/bin/okstra-error-log.py +51 -11
  67. package/runtime/bin/okstra-report-translate.py +210 -23
  68. package/runtime/prompts/host-orchestration/implementation.md +1 -1
  69. package/runtime/prompts/launch.template.md +11 -14
  70. package/runtime/prompts/lead/context-loader.md +41 -141
  71. package/runtime/prompts/lead/convergence.md +8 -6
  72. package/runtime/prompts/lead/okstra-lead-contract.md +26 -36
  73. package/runtime/prompts/lead/plan-body-verification.md +211 -30
  74. package/runtime/prompts/lead/report-writer.md +23 -4
  75. package/runtime/prompts/lead/team-contract.md +8 -53
  76. package/runtime/prompts/profiles/_coding-conventions-preflight.md +3 -2
  77. package/runtime/prompts/profiles/_common-contract.md +1 -1
  78. package/runtime/prompts/profiles/_implementation-diff-review.md +1 -1
  79. package/runtime/prompts/profiles/_implementation-executor.md +1 -0
  80. package/runtime/prompts/profiles/_implementation-verifier.md +4 -4
  81. package/runtime/prompts/profiles/final-verification.md +1 -1
  82. package/runtime/prompts/profiles/implementation-planning.md +17 -13
  83. package/runtime/prompts/profiles/release-handoff.md +0 -1
  84. package/runtime/prompts/wizard/prompts.ko.json +7 -11
  85. package/runtime/python/okstra_ctl/adapters/hosts/capability_adapter.py +69 -17
  86. package/runtime/python/okstra_ctl/adapters/hosts/claude-code/adapter.py +13 -4
  87. package/runtime/python/okstra_ctl/adapters/hosts/claude-code/relay.md +6 -1
  88. package/runtime/python/okstra_ctl/adapters/hosts/codex/adapter.py +2 -2
  89. package/runtime/python/okstra_ctl/adapters/hosts/codex/relay.md +50 -5
  90. package/runtime/python/okstra_ctl/adapters/hosts/grok/adapter.py +2 -2
  91. package/runtime/python/okstra_ctl/adapters/hosts/grok/relay.md +66 -5
  92. package/runtime/python/okstra_ctl/adapters/providers/grok/adapter.py +70 -2
  93. package/runtime/python/okstra_ctl/agent_activity.py +118 -35
  94. package/runtime/python/okstra_ctl/agent_invocation.py +19 -6
  95. package/runtime/python/okstra_ctl/agent_prompt_cli.py +65 -18
  96. package/runtime/python/okstra_ctl/analysis_inputs.py +5 -4
  97. package/runtime/python/okstra_ctl/analysis_packet.py +81 -1
  98. package/runtime/python/okstra_ctl/approval_decisions.py +3 -2
  99. package/runtime/python/okstra_ctl/attempt_evidence.py +2 -2
  100. package/runtime/python/okstra_ctl/backfill.py +13 -10
  101. package/runtime/python/okstra_ctl/batch.py +2 -4
  102. package/runtime/python/okstra_ctl/build_tools.py +6 -3
  103. package/runtime/python/okstra_ctl/claim_reproduction.py +101 -0
  104. package/runtime/python/okstra_ctl/clarification_items.py +27 -13
  105. package/runtime/python/okstra_ctl/cmux.py +130 -52
  106. package/runtime/python/okstra_ctl/code_review_target.py +34 -8
  107. package/runtime/python/okstra_ctl/conformance.py +37 -1
  108. package/runtime/python/okstra_ctl/consumers.py +5 -4
  109. package/runtime/python/okstra_ctl/container.py +103 -8
  110. package/runtime/python/okstra_ctl/context_cost.py +2 -1
  111. package/runtime/python/okstra_ctl/contract_graph.py +497 -0
  112. package/runtime/python/okstra_ctl/contract_graph_cli.py +62 -0
  113. package/runtime/python/okstra_ctl/convergence.py +338 -17
  114. package/runtime/python/okstra_ctl/convergence_engine.py +10 -18
  115. package/runtime/python/okstra_ctl/convergence_provenance.py +58 -8
  116. package/runtime/python/okstra_ctl/convergence_store.py +55 -34
  117. package/runtime/python/okstra_ctl/design_prep.py +7 -4
  118. package/runtime/python/okstra_ctl/dispatch_core.py +35 -65
  119. package/runtime/python/okstra_ctl/dispatch_state.py +134 -59
  120. package/runtime/python/okstra_ctl/doctor.py +6 -3
  121. package/runtime/python/okstra_ctl/domain/worker_presentation.py +70 -9
  122. package/runtime/python/okstra_ctl/entrypoints/hosts.py +16 -30
  123. package/runtime/python/okstra_ctl/error_log_write.py +35 -30
  124. package/runtime/python/okstra_ctl/error_report.py +26 -1
  125. package/runtime/python/okstra_ctl/error_zip.py +27 -5
  126. package/runtime/python/okstra_ctl/execution_identity.py +3 -2
  127. package/runtime/python/okstra_ctl/execution_manifest.py +7 -4
  128. package/runtime/python/okstra_ctl/final_report_schema.py +2 -2
  129. package/runtime/python/okstra_ctl/fix_cycles.py +2 -2
  130. package/runtime/python/okstra_ctl/fixed_text.py +39 -0
  131. package/runtime/python/okstra_ctl/git_reconcile.py +41 -9
  132. package/runtime/python/okstra_ctl/handoff.py +5 -4
  133. package/runtime/python/okstra_ctl/i18n.py +4 -2
  134. package/runtime/python/okstra_ctl/implementation_direction.py +22 -14
  135. package/runtime/python/okstra_ctl/implementation_outcome.py +4 -7
  136. package/runtime/python/okstra_ctl/incremental_carry.py +2 -1
  137. package/runtime/python/okstra_ctl/incremental_scope.py +89 -39
  138. package/runtime/python/okstra_ctl/index.py +8 -11
  139. package/runtime/python/okstra_ctl/initial_prompt_materialization.py +79 -7
  140. package/runtime/python/okstra_ctl/invocation.py +3 -6
  141. package/runtime/python/okstra_ctl/json_boundary.py +366 -0
  142. package/runtime/python/okstra_ctl/json_registry.py +10 -12
  143. package/runtime/python/okstra_ctl/jsonl.py +19 -2
  144. package/runtime/python/okstra_ctl/lead_events.py +33 -1
  145. package/runtime/python/okstra_ctl/listing.py +3 -3
  146. package/runtime/python/okstra_ctl/log_report.py +24 -2
  147. package/runtime/python/okstra_ctl/manager_cli.py +92 -7
  148. package/runtime/python/okstra_ctl/manager_store.py +12 -10
  149. package/runtime/python/okstra_ctl/material.py +5 -1
  150. package/runtime/python/okstra_ctl/migrate.py +29 -25
  151. package/runtime/python/okstra_ctl/model_cli.py +3 -15
  152. package/runtime/python/okstra_ctl/model_io_cli.py +1051 -0
  153. package/runtime/python/okstra_ctl/mutation_probe.py +13 -4
  154. package/runtime/python/okstra_ctl/pane_reclaim.py +3 -2
  155. package/runtime/python/okstra_ctl/paths.py +9 -0
  156. package/runtime/python/okstra_ctl/plan_items.py +525 -5
  157. package/runtime/python/okstra_ctl/plan_items_cli.py +842 -32
  158. package/runtime/python/okstra_ctl/pr_template.py +3 -2
  159. package/runtime/python/okstra_ctl/project_meta.py +5 -7
  160. package/runtime/python/okstra_ctl/recap.py +5 -4
  161. package/runtime/python/okstra_ctl/reconcile.py +21 -27
  162. package/runtime/python/okstra_ctl/registry/host_discovery.py +3 -2
  163. package/runtime/python/okstra_ctl/registry/provider_registry.py +3 -2
  164. package/runtime/python/okstra_ctl/render.py +30 -15
  165. package/runtime/python/okstra_ctl/render_final_report.py +3 -2
  166. package/runtime/python/okstra_ctl/report_assembly.py +172 -17
  167. package/runtime/python/okstra_ctl/report_finalize.py +7 -10
  168. package/runtime/python/okstra_ctl/report_html/render.py +3 -2
  169. package/runtime/python/okstra_ctl/report_language.py +3 -2
  170. package/runtime/python/okstra_ctl/report_markdown.py +13 -1
  171. package/runtime/python/okstra_ctl/report_narrative.py +40 -8
  172. package/runtime/python/okstra_ctl/report_synthesis_packet.py +518 -0
  173. package/runtime/python/okstra_ctl/report_views.py +3 -2
  174. package/runtime/python/okstra_ctl/rollup.py +65 -4
  175. package/runtime/python/okstra_ctl/run.py +159 -56
  176. package/runtime/python/okstra_ctl/run_audit.py +3 -2
  177. package/runtime/python/okstra_ctl/run_context.py +6 -9
  178. package/runtime/python/okstra_ctl/run_index_row.py +2 -8
  179. package/runtime/python/okstra_ctl/schedule_semantics.py +5 -2
  180. package/runtime/python/okstra_ctl/schema_excerpt.py +4 -2
  181. package/runtime/python/okstra_ctl/session_transcript.py +27 -1
  182. package/runtime/python/okstra_ctl/set_work_status.py +64 -38
  183. package/runtime/python/okstra_ctl/stage_fix_carry.py +4 -2
  184. package/runtime/python/okstra_ctl/stage_map.py +26 -6
  185. package/runtime/python/okstra_ctl/stage_targets.py +3 -4
  186. package/runtime/python/okstra_ctl/team.py +2 -1
  187. package/runtime/python/okstra_ctl/team_reconcile.py +11 -2
  188. package/runtime/python/okstra_ctl/time_report.py +51 -4
  189. package/runtime/python/okstra_ctl/usage_identity.py +2 -1
  190. package/runtime/python/okstra_ctl/usage_report.py +58 -4
  191. package/runtime/python/okstra_ctl/user_response.py +1431 -66
  192. package/runtime/python/okstra_ctl/wizard.py +50 -117
  193. package/runtime/python/okstra_ctl/work_categories.py +3 -2
  194. package/runtime/python/okstra_ctl/worker_prompt_body.py +18 -7
  195. package/runtime/python/okstra_ctl/worker_prompt_contract.py +3 -2
  196. package/runtime/python/okstra_ctl/worker_runner.py +14 -12
  197. package/runtime/python/okstra_ctl/workflow.py +2 -1
  198. package/runtime/python/okstra_ctl/worktree.py +3 -2
  199. package/runtime/python/okstra_ctl/wrapper_status.py +4 -2
  200. package/runtime/python/okstra_ctl/write_policy.py +4 -2
  201. package/runtime/python/okstra_token_usage/antigravity.py +39 -12
  202. package/runtime/python/okstra_token_usage/collect.py +90 -38
  203. package/runtime/python/okstra_token_usage/grok.py +127 -0
  204. package/runtime/schemas/final-report-v2.0.schema.json +21 -0
  205. package/runtime/schemas/final-report-v3.0.schema.json +21 -0
  206. package/runtime/schemas/report-synthesis-packet-v1.0.schema.json +140 -0
  207. package/runtime/skills/okstra-brief-gen/SKILL.md +9 -7
  208. package/runtime/skills/okstra-code-review/SKILL.md +21 -11
  209. package/runtime/skills/okstra-container-build/SKILL.md +18 -18
  210. package/runtime/skills/okstra-inspect/SKILL.md +12 -11
  211. package/runtime/skills/okstra-inspect/facets/error-zip.md +8 -8
  212. package/runtime/skills/okstra-inspect/facets/errors.md +2 -2
  213. package/runtime/skills/okstra-inspect/facets/history.md +9 -14
  214. package/runtime/skills/okstra-inspect/facets/logs.md +2 -2
  215. package/runtime/skills/okstra-inspect/facets/recap.md +5 -5
  216. package/runtime/skills/okstra-inspect/facets/report.md +6 -10
  217. package/runtime/skills/okstra-inspect/facets/status.md +9 -8
  218. package/runtime/skills/okstra-inspect/facets/time.md +3 -3
  219. package/runtime/skills/okstra-manager/SKILL.md +16 -14
  220. package/runtime/skills/okstra-memory/SKILL.md +3 -3
  221. package/runtime/skills/okstra-pr-gen/SKILL.md +5 -4
  222. package/runtime/skills/okstra-rollup/SKILL.md +6 -16
  223. package/runtime/skills/okstra-run/SKILL.md +9 -9
  224. package/runtime/skills/okstra-schedule-gen/SKILL.md +21 -17
  225. package/runtime/skills/okstra-setup/SKILL.md +21 -13
  226. package/runtime/skills/okstra-setup/references/project-config.md +2 -2
  227. package/runtime/skills/okstra-usage/SKILL.md +10 -10
  228. package/runtime/skills/okstra-user-response/SKILL.md +78 -107
  229. package/runtime/templates/report-writer-prompt-preamble.md +17 -1
  230. package/runtime/templates/reports/schedule.template.md +4 -4
  231. package/runtime/templates/worker-error-contract.md +17 -29
  232. package/runtime/validators/validate-run.py +527 -113
  233. package/runtime/validators/validate_session_conformance.py +65 -10
@@ -14,16 +14,17 @@ from .claude import (
14
14
  find_claude_team_sessions,
15
15
  )
16
16
  from .codex import codex_session_total, find_codex_sessions
17
- from .antigravity import (
18
- PRINT_MODE_USAGE_NOTE,
19
- antigravity_session_total,
20
- find_antigravity_sessions,
21
- )
17
+ from .antigravity import antigravity_session_total, find_antigravity_sessions
18
+ from .grok import find_grok_sessions, grok_session_total
22
19
  from .paths import claude_project_dir, utc_now
23
20
  from .pricing import antigravity_cost_usd, provider_cost_usd
24
21
  from okstra_ctl.dispatch_state import worker_session_ids
25
22
  from okstra_ctl.models import provider_wrappers
26
- from okstra_ctl.wrapper_status import read_wrapper_status, status_path_for_prompt
23
+ from okstra_ctl.wrapper_status import (
24
+ log_path_for_prompt,
25
+ read_wrapper_status,
26
+ status_path_for_prompt,
27
+ )
27
28
 
28
29
 
29
30
  def match_prefixes(worker_id: str) -> list[str]:
@@ -472,6 +473,8 @@ def _cli_sessions_for_windows(
472
473
  matches = find_codex_sessions(project_root, started_at, ended_at)
473
474
  elif provider == "antigravity":
474
475
  matches = find_antigravity_sessions(project_root, started_at, ended_at)
476
+ elif provider == "grok":
477
+ matches = find_grok_sessions(project_root, started_at, ended_at)
475
478
  else:
476
479
  matches = []
477
480
  for path in matches:
@@ -488,6 +491,8 @@ def _cli_session_totals(provider: str, session_paths: list[Path]) -> list[dict]:
488
491
  total = codex_session_total(session_path)
489
492
  elif provider == "antigravity":
490
493
  total = antigravity_session_total(session_path)
494
+ elif provider == "grok":
495
+ total = grok_session_total(session_path)
491
496
  else:
492
497
  continue
493
498
  if total.get("available"):
@@ -582,24 +587,20 @@ def collect_cli_usage(
582
587
  execution = wrapper_execution(status_path)
583
588
  execution_note = _execution_note(execution)
584
589
 
585
- if provider == "antigravity":
586
- block = na_block(f"{PRINT_MODE_USAGE_NOTE}; {execution_note}")
590
+ totals = _cli_session_totals(provider, sessions)
591
+ if not sessions:
592
+ block = na_block(
593
+ f"{execution_note}; CLI usage attribution unavailable because no transcript was found"
594
+ )
587
595
  block["cliNote"] = block["note"]
596
+ elif not totals:
597
+ block = na_block(
598
+ f"{execution_note}; transcript found but no final token snapshot was recorded"
599
+ )
600
+ block["cliNote"] = block["note"]
601
+ block["cliSessionPaths"] = [str(path) for path in sessions]
588
602
  else:
589
- totals = _cli_session_totals(provider, sessions)
590
- if not sessions:
591
- block = na_block(
592
- f"{execution_note}; CLI usage attribution unavailable because no transcript was found"
593
- )
594
- block["cliNote"] = block["note"]
595
- elif not totals:
596
- block = na_block(
597
- f"{execution_note}; transcript found but no final token snapshot was recorded"
598
- )
599
- block["cliNote"] = block["note"]
600
- block["cliSessionPaths"] = [str(path) for path in sessions]
601
- else:
602
- block = _cli_usage_block(provider, _aggregate_totals(totals), sessions)
603
+ block = _cli_usage_block(provider, _aggregate_totals(totals), sessions)
603
604
 
604
605
  block["cliExecutionStatus"] = execution["status"]
605
606
  if fallback_window_used:
@@ -624,6 +625,51 @@ def _worker_cli_windows(
624
625
  return fallback_windows, status_path, True
625
626
 
626
627
 
628
+ def _antigravity_log_paths(
629
+ project_root: Path,
630
+ worker: dict,
631
+ status_path: Path | None,
632
+ ) -> list[Path]:
633
+ """agy stream-json 은 워커 로그에 남는다. 홈 세션 디렉터리가 아니다."""
634
+ paths: list[Path] = []
635
+ prompt_path = _resolve_project_path(project_root, str(worker.get("promptPath") or ""))
636
+ if prompt_path is not None:
637
+ log_path = log_path_for_prompt(prompt_path)
638
+ if log_path.is_file():
639
+ paths.append(log_path)
640
+ status = read_wrapper_status(status_path) if status_path else None
641
+ if status is not None and status.log_path is not None and status.log_path.is_file():
642
+ if status.log_path not in paths:
643
+ paths.append(status.log_path)
644
+ return paths
645
+
646
+
647
+ def _worker_cli_usage_block(
648
+ *,
649
+ provider: str,
650
+ project_root: Path,
651
+ worker: dict,
652
+ fallback_windows: list[tuple[str, str]],
653
+ ) -> dict:
654
+ """공급자 프로세스 트랜스크립트와 실행 증거로 워커 사용량 블록을 만든다."""
655
+ windows, status_path, used_fallback = _worker_cli_windows(
656
+ project_root,
657
+ worker,
658
+ fallback_windows,
659
+ )
660
+ session_paths = _cli_sessions_for_windows(provider, project_root, windows)
661
+ if provider == "antigravity":
662
+ for log_path in _antigravity_log_paths(project_root, worker, status_path):
663
+ if log_path not in session_paths:
664
+ session_paths.append(log_path)
665
+ return collect_cli_usage(
666
+ provider=provider,
667
+ status_path=status_path,
668
+ sessions=session_paths,
669
+ fallback_window_used=used_fallback,
670
+ )
671
+
672
+
627
673
  def _attach_cli_usage(
628
674
  block: dict,
629
675
  provider: str,
@@ -638,8 +684,8 @@ def _attach_cli_usage(
638
684
  window produces several rollout jsonls; pricing only the latest (the old
639
685
  ``find_codex_session`` -> ``sessions[-1]`` behavior) dropped the earlier
640
686
  attempts' tokens. We sum every in-window session here so the redispatched
641
- CLI spend is fully reported. (agy print mode leaves no transcript, so the
642
- antigravity path still resolves to "session not found" → na cost.)
687
+ CLI spend is fully reported. antigravity 는 워커 로그의 stream-json
688
+ ``result.usage`` 를 같은 합산 경로에 붙인다.
643
689
  """
644
690
  session_paths = _cli_sessions_for_windows(provider, project_root, windows)
645
691
  cli = collect_cli_usage(
@@ -676,17 +722,11 @@ def _collect_cli_runtime_usage(state: dict, project_root: Path) -> dict:
676
722
  )
677
723
  continue
678
724
  worker_id = str(worker.get("workerId") or "").strip()
679
- windows, status_path, used_fallback = _worker_cli_windows(
680
- project_root,
681
- worker,
682
- windows_by_worker.get(worker_id, []),
683
- )
684
- session_paths = _cli_sessions_for_windows(provider, project_root, windows)
685
- worker["usage"] = collect_cli_usage(
725
+ worker["usage"] = _worker_cli_usage_block(
686
726
  provider=provider,
687
- status_path=status_path,
688
- sessions=session_paths,
689
- fallback_window_used=used_fallback,
727
+ project_root=project_root,
728
+ worker=worker,
729
+ fallback_windows=windows_by_worker.get(worker_id, []),
690
730
  )
691
731
  _populate_usage_summary(state, team_name=resolve_team_name(state),
692
732
  sessions_found=0, needle_source="none")
@@ -871,6 +911,7 @@ def collect_claude_runtime_usage(
871
911
  sessions_dir = claude_project_dir(cwd)
872
912
  # sid 로 귀속한 세션 id 전체 — 아래 unattributed 폴드에서 빼는 데 쓴다.
873
913
  attributed_sids: set[str] = set()
914
+ cli_windows_by_worker = _codex_worker_windows(cwd, state)
874
915
  for worker in state.get("workers", []):
875
916
  worker_id = worker.get("workerId")
876
917
  agent = worker.get("agent")
@@ -922,10 +963,21 @@ def collect_claude_runtime_usage(
922
963
  matched_sids.add(entry[0])
923
964
 
924
965
  if not matched:
925
- worker["usage"] = na_block(
926
- "no Claude session jsonl in the run window for dispatched "
927
- f"sessionIds {dispatched_sids} or agentName prefixes {prefixes}"
928
- )
966
+ provider = _cli_assignment_provider(worker)
967
+ if provider and str(worker.get("runner") or "") == "cli-wrapper":
968
+ worker["usage"] = _worker_cli_usage_block(
969
+ provider=provider,
970
+ project_root=cwd,
971
+ worker=worker,
972
+ fallback_windows=cli_windows_by_worker.get(
973
+ str(worker_id or "").strip(), []
974
+ ),
975
+ )
976
+ else:
977
+ worker["usage"] = na_block(
978
+ "no Claude session jsonl in the run window for dispatched "
979
+ f"sessionIds {dispatched_sids} or agentName prefixes {prefixes}"
980
+ )
929
981
  continue
930
982
 
931
983
  # Stable order by startedAt so the "primary" session is the first one.
@@ -0,0 +1,127 @@
1
+ """Grok Build session collectors.
2
+
3
+ 공식 세션 문서는 대화를 ``~/.grok/sessions/`` 에 둔다. cwd 는 퍼센트 인코딩된
4
+ 디렉터리 이름이다. 누적 사용량은 ``updates.jsonl`` 의
5
+ ``params.update.usage.modelUsage`` 마지막 스냅샷이다.
6
+ """
7
+ from __future__ import annotations
8
+
9
+ import os
10
+ from datetime import datetime, timezone
11
+ from pathlib import Path
12
+ from typing import Mapping
13
+ from urllib.parse import quote, unquote
14
+
15
+ from .jsonl_io import iter_jsonl
16
+ from .paths import ts_in_window
17
+
18
+
19
+ def grok_sessions_root(
20
+ home: Path | None = None,
21
+ env: Mapping[str, str] | None = None,
22
+ ) -> Path:
23
+ environ = env if env is not None else os.environ
24
+ configured = str(environ.get("GROK_HOME") or "").strip()
25
+ base = Path(configured).expanduser() if configured else (home or Path.home()) / ".grok"
26
+ return base / "sessions"
27
+
28
+
29
+ def _iso_from_unix(value: object) -> str | None:
30
+ if not isinstance(value, (int, float)) or isinstance(value, bool):
31
+ return None
32
+ return datetime.fromtimestamp(value, timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
33
+
34
+
35
+ def _usage_payload(record: dict) -> dict | None:
36
+ params = record.get("params")
37
+ if not isinstance(params, dict):
38
+ return None
39
+ update = params.get("update")
40
+ if not isinstance(update, dict):
41
+ return None
42
+ usage = update.get("usage")
43
+ return usage if isinstance(usage, dict) else None
44
+
45
+
46
+ def _model_snapshot(usage: dict) -> tuple[dict, str | None]:
47
+ models = usage.get("modelUsage")
48
+ if isinstance(models, dict) and len(models) == 1:
49
+ name, payload = next(iter(models.items()))
50
+ if isinstance(payload, dict):
51
+ return payload, name if isinstance(name, str) else None
52
+ return usage, None
53
+
54
+
55
+ def grok_session_total(updates_path: Path) -> dict:
56
+ """마지막 modelUsage 스냅샷. 세션 누적이라 더하지 않는다."""
57
+ last: dict | None = None
58
+ model: str | None = None
59
+ started: str | None = None
60
+ ended: str | None = None
61
+ for record in iter_jsonl(updates_path):
62
+ usage = _usage_payload(record)
63
+ if usage is None:
64
+ continue
65
+ iso = _iso_from_unix(record.get("timestamp"))
66
+ if iso and started is None:
67
+ started = iso
68
+ if iso:
69
+ ended = iso
70
+ snapshot, snapshot_model = _model_snapshot(usage)
71
+ last = snapshot
72
+ if snapshot_model:
73
+ model = snapshot_model
74
+ if last is None:
75
+ return {"totalTokens": 0, "available": False}
76
+ return {
77
+ "totalTokens": last.get("totalTokens", 0) or 0,
78
+ "inputTokens": last.get("inputTokens", 0) or 0,
79
+ "outputTokens": last.get("outputTokens", 0) or 0,
80
+ "cachedInputTokens": last.get("cachedReadTokens", 0) or 0,
81
+ "reasoningOutputTokens": last.get("reasoningTokens", 0) or 0,
82
+ "durationMs": last.get("apiDurationMs", 0) or 0,
83
+ "model": model,
84
+ "startedAt": started,
85
+ "endedAt": ended,
86
+ "available": True,
87
+ }
88
+
89
+
90
+ def _session_in_window(updates_path: Path, started_at: str, ended_at: str) -> bool:
91
+ for record in iter_jsonl(updates_path):
92
+ iso = _iso_from_unix(record.get("timestamp"))
93
+ if iso and ts_in_window(iso, started_at, ended_at):
94
+ return True
95
+ return False
96
+
97
+
98
+ def find_grok_sessions(
99
+ cwd: Path,
100
+ started_at: str,
101
+ ended_at: str,
102
+ *,
103
+ session_root: Path | None = None,
104
+ ) -> list[Path]:
105
+ """cwd 로 인코딩된 세션 중 창 안에 갱신이 있는 updates.jsonl."""
106
+ if not started_at or not ended_at:
107
+ return []
108
+ root = session_root or grok_sessions_root()
109
+ if not root.is_dir():
110
+ return []
111
+ target = str(cwd)
112
+ matches: list[Path] = []
113
+ for child in root.iterdir():
114
+ if not child.is_dir():
115
+ continue
116
+ decoded = unquote(child.name)
117
+ if decoded != target:
118
+ continue
119
+ for session in child.iterdir():
120
+ updates = session / "updates.jsonl"
121
+ if updates.is_file() and _session_in_window(updates, started_at, ended_at):
122
+ matches.append(updates)
123
+ return sorted(matches)
124
+
125
+
126
+ def grok_session_dir_name(cwd: Path) -> str:
127
+ return quote(str(cwd), safe="")
@@ -8943,6 +8943,7 @@
8943
8943
  ],
8944
8944
  "additionalProperties": false,
8945
8945
  "properties": {
8946
+ "stageRefs": {"type": "array", "minItems": 1, "uniqueItems": true, "items": {"type": "integer", "minimum": 1}},
8946
8947
  "id": {
8947
8948
  "type": "string",
8948
8949
  "pattern": "^VC-\\d{3,}$"
@@ -9142,6 +9143,14 @@
9142
9143
  ],
9143
9144
  "additionalProperties": false,
9144
9145
  "properties": {
9146
+ "setAside": {"type": "array", "items": {"type": "object", "required": ["id", "reason"], "additionalProperties": false, "properties": {"id": {"type": "string", "minLength": 1}, "reason": {"type": "string", "enum": ["record", "observed", "deferred"]}}}},
9147
+ "stageLedger": {"type": "object", "additionalProperties": {"type": "string", "enum": ["done", "active", "ready", "blocked"]}},
9148
+ "dispatchQueue": {
9149
+ "type": "array",
9150
+ "description": "Plan-item ids sent to verifiers this round. In-scope plus plan-wide; deferred and observed stages are omitted.",
9151
+ "items": {"type": "string", "minLength": 1},
9152
+ "uniqueItems": true
9153
+ },
9145
9154
  "uniformVerifiers": {
9146
9155
  "type": "array",
9147
9156
  "description": "Verifiers whose every vote this round was one verdict. Advisory: a unanimous round is legitimate, but the gate reads as a three-way cross-check unless this sits beside it.",
@@ -9169,6 +9178,10 @@
9169
9178
  }
9170
9179
  }
9171
9180
  },
9181
+ "gating": {
9182
+ "type": "boolean",
9183
+ "description": "If false, plan-body verification is advisory: majority-disagree does not block approval, the self-fix loop does not run, and only one automatic round is allowed. Legal only when designPreparation.mode is no-design-inputs and the Stage Map has exactly one row. Prepare emits true; plan-items prepare/seed flip it after those facts exist."
9184
+ },
9172
9185
  "roundCount": {
9173
9186
  "type": "integer",
9174
9187
  "minimum": 0
@@ -9242,6 +9255,10 @@
9242
9255
  ],
9243
9256
  "additionalProperties": false,
9244
9257
  "properties": {
9258
+ "block": {"type": "string", "enum": ["execution", "record"]},
9259
+ "stageScope": {"type": "array", "minItems": 1, "uniqueItems": true, "items": {"type": "integer", "minimum": 1}},
9260
+ "contentHash": {"type": "string", "pattern": "^[a-f0-9]{64}$", "description": "SHA-256 of subject+payload+block+stageScope for the current extract."},
9261
+ "verifiedContentHash": {"type": "string", "pattern": "^[a-f0-9]{64}$", "description": "contentHash at the last apply-verdicts. Matching hashes keep a pre-self-fix verdict."},
9245
9262
  "id": {
9246
9263
  "type": "string",
9247
9264
  "minLength": 1
@@ -9280,6 +9297,9 @@
9280
9297
  ],
9281
9298
  "additionalProperties": false,
9282
9299
  "properties": {
9300
+ "claimKind": {"description": "Set only on DISAGREE. `fact` claims are settled by reproduction, `judgement` by quorum.", "enum": ["fact", "judgement", null]},
9301
+ "reproduction": {"description": "Set only on a `fact` claim. The probe okstra runs; see okstra_ctl/claim_reproduction.py.", "type": ["object", "null"]},
9302
+ "reproductionResult": {"description": "Written by okstra, not by the worker.", "enum": ["reproduced", "not-reproduced", "not-runnable", null]},
9283
9303
  "worker": {
9284
9304
  "type": "string",
9285
9305
  "minLength": 1
@@ -9752,6 +9772,7 @@
9752
9772
  ],
9753
9773
  "additionalProperties": false,
9754
9774
  "properties": {
9775
+ "stageRefs": {"type": "array", "minItems": 1, "uniqueItems": true, "items": {"type": "integer", "minimum": 1}},
9755
9776
  "id": {
9756
9777
  "type": "string",
9757
9778
  "pattern": "^R-\\d{3,}$"
@@ -8938,6 +8938,7 @@
8938
8938
  ],
8939
8939
  "additionalProperties": false,
8940
8940
  "properties": {
8941
+ "stageRefs": {"type": "array", "minItems": 1, "uniqueItems": true, "items": {"type": "integer", "minimum": 1}},
8941
8942
  "id": {
8942
8943
  "type": "string",
8943
8944
  "pattern": "^VC-\\d{3,}$"
@@ -9137,6 +9138,14 @@
9137
9138
  ],
9138
9139
  "additionalProperties": false,
9139
9140
  "properties": {
9141
+ "setAside": {"type": "array", "items": {"type": "object", "required": ["id", "reason"], "additionalProperties": false, "properties": {"id": {"type": "string", "minLength": 1}, "reason": {"type": "string", "enum": ["record", "observed", "deferred"]}}}},
9142
+ "stageLedger": {"type": "object", "additionalProperties": {"type": "string", "enum": ["done", "active", "ready", "blocked"]}},
9143
+ "dispatchQueue": {
9144
+ "type": "array",
9145
+ "description": "Plan-item ids sent to verifiers this round. In-scope plus plan-wide; deferred and observed stages are omitted.",
9146
+ "items": {"type": "string", "minLength": 1},
9147
+ "uniqueItems": true
9148
+ },
9140
9149
  "uniformVerifiers": {
9141
9150
  "type": "array",
9142
9151
  "description": "Verifiers whose every vote this round was one verdict. Advisory: a unanimous round is legitimate, but the gate reads as a three-way cross-check unless this sits beside it.",
@@ -9164,6 +9173,10 @@
9164
9173
  }
9165
9174
  }
9166
9175
  },
9176
+ "gating": {
9177
+ "type": "boolean",
9178
+ "description": "If false, plan-body verification is advisory: majority-disagree does not block approval, the self-fix loop does not run, and only one automatic round is allowed. Legal only when designPreparation.mode is no-design-inputs and the Stage Map has exactly one row. Prepare emits true; plan-items prepare/seed flip it after those facts exist."
9179
+ },
9167
9180
  "roundCount": {
9168
9181
  "type": "integer",
9169
9182
  "minimum": 0
@@ -9237,6 +9250,10 @@
9237
9250
  ],
9238
9251
  "additionalProperties": false,
9239
9252
  "properties": {
9253
+ "block": {"type": "string", "enum": ["execution", "record"]},
9254
+ "stageScope": {"type": "array", "minItems": 1, "uniqueItems": true, "items": {"type": "integer", "minimum": 1}},
9255
+ "contentHash": {"type": "string", "pattern": "^[a-f0-9]{64}$", "description": "SHA-256 of subject+payload+block+stageScope for the current extract."},
9256
+ "verifiedContentHash": {"type": "string", "pattern": "^[a-f0-9]{64}$", "description": "contentHash at the last apply-verdicts. Matching hashes keep a pre-self-fix verdict."},
9240
9257
  "id": {
9241
9258
  "type": "string",
9242
9259
  "minLength": 1
@@ -9272,6 +9289,9 @@
9272
9289
  ],
9273
9290
  "additionalProperties": false,
9274
9291
  "properties": {
9292
+ "claimKind": {"description": "Set only on DISAGREE. `fact` claims are settled by reproduction, `judgement` by quorum.", "enum": ["fact", "judgement", null]},
9293
+ "reproduction": {"description": "Set only on a `fact` claim. The probe okstra runs; see okstra_ctl/claim_reproduction.py.", "type": ["object", "null"]},
9294
+ "reproductionResult": {"description": "Written by okstra, not by the worker.", "enum": ["reproduced", "not-reproduced", "not-runnable", null]},
9275
9295
  "worker": {
9276
9296
  "type": "string",
9277
9297
  "minLength": 1
@@ -9744,6 +9764,7 @@
9744
9764
  ],
9745
9765
  "additionalProperties": false,
9746
9766
  "properties": {
9767
+ "stageRefs": {"type": "array", "minItems": 1, "uniqueItems": true, "items": {"type": "integer", "minimum": 1}},
9747
9768
  "id": {
9748
9769
  "type": "string",
9749
9770
  "pattern": "^R-\\d{3,}$"
@@ -0,0 +1,140 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://okstra.dev/schemas/report-synthesis-packet-v1.0.schema.json",
4
+ "title": "Okstra report synthesis packet",
5
+ "type": "object",
6
+ "required": [
7
+ "schemaVersion",
8
+ "taskKey",
9
+ "taskType",
10
+ "authoringContract",
11
+ "accountingSnapshot",
12
+ "sources"
13
+ ],
14
+ "additionalProperties": false,
15
+ "properties": {
16
+ "schemaVersion": {
17
+ "const": "1.0"
18
+ },
19
+ "taskKey": {
20
+ "type": "string",
21
+ "minLength": 1
22
+ },
23
+ "taskType": {
24
+ "type": "string",
25
+ "minLength": 1
26
+ },
27
+ "authoringContract": {
28
+ "type": "object",
29
+ "required": [
30
+ "resultPath",
31
+ "format",
32
+ "sourcePolicy",
33
+ "instructions",
34
+ "runtimeOwnedContent",
35
+ "validationRules"
36
+ ],
37
+ "additionalProperties": false,
38
+ "properties": {
39
+ "resultPath": {
40
+ "type": "string",
41
+ "minLength": 1
42
+ },
43
+ "format": {
44
+ "const": "report-narrative-v3.0"
45
+ },
46
+ "sourcePolicy": {
47
+ "const": "read-only-synthesis-packet"
48
+ },
49
+ "instructions": {
50
+ "type": "array",
51
+ "minItems": 1,
52
+ "uniqueItems": true,
53
+ "items": {
54
+ "type": "string",
55
+ "minLength": 1
56
+ }
57
+ },
58
+ "runtimeOwnedContent": {
59
+ "type": "array",
60
+ "minItems": 1,
61
+ "uniqueItems": true,
62
+ "items": {
63
+ "type": "string",
64
+ "minLength": 1
65
+ }
66
+ },
67
+ "validationRules": {
68
+ "type": "array",
69
+ "minItems": 1,
70
+ "uniqueItems": true,
71
+ "items": {
72
+ "type": "string",
73
+ "minLength": 1
74
+ }
75
+ }
76
+ }
77
+ },
78
+ "accountingSnapshot": {
79
+ "type": "object",
80
+ "required": [
81
+ "leadSessionIds",
82
+ "leadUsage",
83
+ "workerUsage",
84
+ "usageSummary"
85
+ ],
86
+ "additionalProperties": false,
87
+ "properties": {
88
+ "leadSessionIds": {
89
+ "type": "array",
90
+ "items": {"type": "string"}
91
+ },
92
+ "leadUsage": {"type": "object"},
93
+ "workerUsage": {
94
+ "type": "array",
95
+ "items": {
96
+ "type": "object",
97
+ "required": ["workerId", "role", "usage"],
98
+ "additionalProperties": false,
99
+ "properties": {
100
+ "workerId": {"type": "string", "minLength": 1},
101
+ "role": {"type": "string"},
102
+ "usage": {"type": "object"}
103
+ }
104
+ }
105
+ },
106
+ "usageSummary": {"type": "object"}
107
+ }
108
+ },
109
+ "sources": {
110
+ "type": "array",
111
+ "minItems": 1,
112
+ "items": {
113
+ "type": "object",
114
+ "required": ["label", "owner", "path", "digest", "content"],
115
+ "additionalProperties": false,
116
+ "properties": {
117
+ "label": {
118
+ "type": "string",
119
+ "minLength": 1
120
+ },
121
+ "owner": {
122
+ "type": "string",
123
+ "minLength": 1
124
+ },
125
+ "path": {
126
+ "type": "string",
127
+ "minLength": 1
128
+ },
129
+ "digest": {
130
+ "type": "string",
131
+ "pattern": "^sha256:[0-9a-f]{64}$"
132
+ },
133
+ "content": {
134
+ "type": "string"
135
+ }
136
+ }
137
+ }
138
+ }
139
+ }
140
+ }
@@ -120,10 +120,10 @@ Run one Bash tool call, starting with the literal token `okstra` (never wrapped
120
120
  <!-- END FRAGMENT: bash-invocation-rule -->
121
121
 
122
122
  ```bash
123
- okstra preflight --runtime claude-code --json
123
+ okstra preflight --runtime claude-code
124
124
  ```
125
125
 
126
- Parse the stdout JSON. `ok: true` → carry `projectRoot` as a literal string into the steps below. `ok: false` (or `okstra` not on `PATH` at all) → tell the user: "okstra not set up — run `/okstra-setup` first." Then stop.
126
+ On `Okstra preflight: ready`, carry the fixed `Project root` line as a literal string into the steps below. On `Okstra preflight: failed` (or `okstra` not on `PATH` at all), show the fixed `Reason` and `Recovery` lines, then stop.
127
127
 
128
128
  <!-- BEGIN FRAGMENT: preflight-outdated-cli -->
129
129
  If the call fails with `unknown command: preflight`, the `okstra` binary on PATH predates this skill — tell the user to update it (`npm i -g okstra@latest`), then stop (`/okstra-setup` does not update the binary).
@@ -328,16 +328,18 @@ command is its own Bash call starting with the literal token `okstra`, per
328
328
  Step 0):
329
329
 
330
330
  ```bash
331
- okstra task-list
331
+ okstra task-list --text
332
332
  ```
333
333
 
334
- Parse the JSON `tasks[]` array. The `taskGroup` field is a **raw display
335
- name, not a slug** — `slugify` each value (lowercase + non-alphanumeric →
336
- `-`) and **dedupe by slug** so case/format variants collapse (e.g.
334
+ Read each fixed `Task group` and `Updated at` line. `Task group` is a **raw
335
+ display name, not a slug** — `slugify` each value (lowercase +
336
+ non-alphanumeric → `-`) and **dedupe by slug** so case/format variants collapse (e.g.
337
337
  `uploadFont` and `uploadfont` are one group, not two). Order distinct slugs
338
338
  by most-recent `updatedAt` and take the **2 most-recent** as recommendations,
339
339
  shown in slug form.
340
340
 
341
+ If `Status` is `error`, report `Failure stage` and `Failure reason`, then stop.
342
+
341
343
  `AskUserQuestion` (single-select):
342
344
 
343
345
  - **Label**: `"Task group?"`
@@ -349,7 +351,7 @@ shown in slug form.
349
351
  When the user picks an existing group, use its value verbatim. When they
350
352
  pick `Enter directly`, take a free-text value via a follow-up question.
351
353
 
352
- If `okstra task-list` returns an empty `tasks[]` (no prior tasks), skip the
354
+ If `okstra task-list --text` returns `Task count: 0` (no prior tasks), skip the
353
355
  picker and ask one free-text question:
354
356
  `"Task group (e.g. backend-api, INV-1234, refactor)"` → `task_group`.
355
357