okstra 0.189.4 → 0.191.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 (156) hide show
  1. package/README.md +1 -1
  2. package/dist/cli-registry.mjs +9 -0
  3. package/dist/cli-registry.mjs.map +1 -1
  4. package/dist/commands/chat/chat.mjs +54 -4
  5. package/dist/commands/chat/chat.mjs.map +1 -1
  6. package/dist/commands/lifecycle/install.mjs +12 -5
  7. package/dist/commands/lifecycle/install.mjs.map +1 -1
  8. package/dist/commands/lifecycle/uninstall.mjs +2 -1
  9. package/dist/commands/lifecycle/uninstall.mjs.map +1 -1
  10. package/dist/lib/host-config.d.mts +5 -3
  11. package/dist/lib/host-config.mjs +89 -31
  12. package/dist/lib/host-config.mjs.map +1 -1
  13. package/docs/architecture/storage-model.md +4 -1
  14. package/docs/architecture.md +8 -4
  15. package/docs/cli.md +10 -8
  16. package/docs/for-ai/skills/okstra-brief-gen.md +1 -1
  17. package/docs/for-ai/skills/okstra-chat.md +3 -3
  18. package/docs/for-ai/skills/okstra-inspect.md +11 -1
  19. package/docs/for-ai/skills/okstra-rollup.md +1 -0
  20. package/docs/for-ai/skills/okstra-run.md +5 -4
  21. package/docs/for-ai/skills/okstra-user-response.md +1 -1
  22. package/docs/project-structure-overview.md +17 -5
  23. package/docs/task-process/README.md +1 -1
  24. package/docs/task-process/error-analysis.md +2 -2
  25. package/docs/task-process/final-verification.md +1 -1
  26. package/docs/task-process/implementation.md +1 -1
  27. package/docs/task-process/release-handoff.md +8 -6
  28. package/docs/task-process/requirements-discovery.md +1 -1
  29. package/package.json +1 -1
  30. package/runtime/BUILD.json +2 -2
  31. package/runtime/agents/workers/claude-worker.md +4 -0
  32. package/runtime/bin/okstra-report-translate.py +10 -4
  33. package/runtime/prompts/launch.template.md +11 -5
  34. package/runtime/prompts/lead/adapters/cmux.md +4 -7
  35. package/runtime/prompts/lead/context-loader.md +2 -0
  36. package/runtime/prompts/lead/convergence.md +20 -83
  37. package/runtime/prompts/lead/okstra-lead-contract.md +24 -6
  38. package/runtime/prompts/lead/plan-body-verification.md +10 -3
  39. package/runtime/prompts/lead/report-writer.md +7 -4
  40. package/runtime/prompts/lead/team-contract.md +15 -9
  41. package/runtime/prompts/profiles/_common-contract.md +1 -0
  42. package/runtime/prompts/profiles/_implementation-deliverable.md +2 -0
  43. package/runtime/prompts/profiles/_implementation-diff-review.md +4 -0
  44. package/runtime/prompts/profiles/_implementation-executor.md +9 -5
  45. package/runtime/prompts/profiles/_implementation-self-check.md +2 -0
  46. package/runtime/prompts/profiles/_implementation-verifier.md +3 -4
  47. package/runtime/prompts/profiles/_stage-discipline.md +12 -5
  48. package/runtime/prompts/profiles/final-verification.md +1 -1
  49. package/runtime/prompts/profiles/implementation.md +2 -0
  50. package/runtime/prompts/profiles/release-handoff.md +4 -3
  51. package/runtime/prompts/wizard/prompts.ko.json +2 -1
  52. package/runtime/python/okstra_ctl/adapters/dispatch/cmux.py +6 -0
  53. package/runtime/python/okstra_ctl/adapters/hosts/antigravity/adapter.py +1 -0
  54. package/runtime/python/okstra_ctl/adapters/hosts/antigravity/relay.md +2 -2
  55. package/runtime/python/okstra_ctl/adapters/hosts/capability_adapter.py +9 -0
  56. package/runtime/python/okstra_ctl/adapters/hosts/claude-code/adapter.py +1 -0
  57. package/runtime/python/okstra_ctl/adapters/hosts/claude-code/relay.md +3 -3
  58. package/runtime/python/okstra_ctl/adapters/hosts/codex/adapter.py +1 -0
  59. package/runtime/python/okstra_ctl/adapters/hosts/codex/relay.md +31 -3
  60. package/runtime/python/okstra_ctl/adapters/hosts/external/adapter.py +1 -0
  61. package/runtime/python/okstra_ctl/adapters/hosts/external/relay.md +3 -3
  62. package/runtime/python/okstra_ctl/adapters/hosts/grok/adapter.py +1 -0
  63. package/runtime/python/okstra_ctl/adapters/hosts/grok/relay.md +2 -2
  64. package/runtime/python/okstra_ctl/adapters/hosts/kimi/adapter.py +1 -0
  65. package/runtime/python/okstra_ctl/adapters/hosts/kimi/relay.md +2 -2
  66. package/runtime/python/okstra_ctl/adapters/providers/codex/adapter.py +13 -7
  67. package/runtime/python/okstra_ctl/adapters/providers/kimi/adapter.py +5 -2
  68. package/runtime/python/okstra_ctl/agent/invocation.py +107 -5
  69. package/runtime/python/okstra_ctl/agent/prompt_cli/cli.py +16 -0
  70. package/runtime/python/okstra_ctl/agent/prompt_cli/dynamic_verifier.py +3 -2
  71. package/runtime/python/okstra_ctl/agent/prompt_cli/jobs.py +87 -0
  72. package/runtime/python/okstra_ctl/agent/prompt_cli/materialize.py +72 -3
  73. package/runtime/python/okstra_ctl/analysis_packet.py +47 -6
  74. package/runtime/python/okstra_ctl/cmux.py +34 -13
  75. package/runtime/python/okstra_ctl/consumers.py +30 -7
  76. package/runtime/python/okstra_ctl/convergence.py +110 -21
  77. package/runtime/python/okstra_ctl/convergence_provenance.py +83 -0
  78. package/runtime/python/okstra_ctl/dispatch_core.py +127 -5
  79. package/runtime/python/okstra_ctl/dispatch_state.py +44 -13
  80. package/runtime/python/okstra_ctl/domain/host.py +5 -0
  81. package/runtime/python/okstra_ctl/domain/wizard/interaction.py +1 -0
  82. package/runtime/python/okstra_ctl/error_log_write.py +44 -4
  83. package/runtime/python/okstra_ctl/execution_mutation_audit.py +84 -7
  84. package/runtime/python/okstra_ctl/fixed_text.py +25 -7
  85. package/runtime/python/okstra_ctl/group_context.py +475 -10
  86. package/runtime/python/okstra_ctl/handoff.py +109 -32
  87. package/runtime/python/okstra_ctl/handoff_verification.py +80 -0
  88. package/runtime/python/okstra_ctl/initial_prompt_materialization.py +52 -10
  89. package/runtime/python/okstra_ctl/lead_progress.py +291 -0
  90. package/runtime/python/okstra_ctl/model_io/renderers.py +60 -1
  91. package/runtime/python/okstra_ctl/model_io_cli.py +6 -1
  92. package/runtime/python/okstra_ctl/ports/worker_dispatch.py +4 -0
  93. package/runtime/python/okstra_ctl/recap.py +179 -17
  94. package/runtime/python/okstra_ctl/reconcile.py +55 -4
  95. package/runtime/python/okstra_ctl/render.py +10 -1
  96. package/runtime/python/okstra_ctl/report_assembly.py +7 -6
  97. package/runtime/python/okstra_ctl/report_finalize.py +204 -11
  98. package/runtime/python/okstra_ctl/report_html/render.py +15 -4
  99. package/runtime/python/okstra_ctl/run.py +48 -14
  100. package/runtime/python/okstra_ctl/stage_targets.py +66 -0
  101. package/runtime/python/okstra_ctl/verdict_blocks.py +39 -12
  102. package/runtime/python/okstra_ctl/wizard/__init__.py +148 -0
  103. package/runtime/python/okstra_ctl/wizard/__main__.py +13 -0
  104. package/runtime/python/okstra_ctl/wizard/cli.py +176 -0
  105. package/runtime/python/okstra_ctl/wizard/confirmation.py +331 -0
  106. package/runtime/python/okstra_ctl/wizard/engine.py +384 -0
  107. package/runtime/python/okstra_ctl/wizard/ids.py +411 -0
  108. package/runtime/python/okstra_ctl/wizard/picker_navigation.py +75 -0
  109. package/runtime/python/okstra_ctl/wizard/prompts.py +202 -0
  110. package/runtime/python/okstra_ctl/wizard/registry.py +836 -0
  111. package/runtime/python/okstra_ctl/wizard/render.py +189 -0
  112. package/runtime/python/okstra_ctl/wizard/roles.py +737 -0
  113. package/runtime/python/okstra_ctl/wizard/sources.py +703 -0
  114. package/runtime/python/okstra_ctl/wizard/state.py +691 -0
  115. package/runtime/python/okstra_ctl/wizard/statefile.py +145 -0
  116. package/runtime/python/okstra_ctl/wizard/steps_analysis.py +558 -0
  117. package/runtime/python/okstra_ctl/wizard/steps_identity.py +828 -0
  118. package/runtime/python/okstra_ctl/wizard/steps_options.py +340 -0
  119. package/runtime/python/okstra_ctl/wizard/steps_plan.py +959 -0
  120. package/runtime/python/okstra_ctl/wizard/steps_roles.py +672 -0
  121. package/runtime/python/okstra_ctl/worker_prompt_contract.py +43 -3
  122. package/runtime/python/okstra_ctl/worker_prompt_headers.py +14 -0
  123. package/runtime/python/okstra_ctl/workflow.py +2 -2
  124. package/runtime/python/okstra_ctl/worktree/__init__.py +2 -0
  125. package/runtime/python/okstra_ctl/worktree/cleanliness.py +7 -4
  126. package/runtime/python/okstra_ctl/worktree/git_ops.py +7 -0
  127. package/runtime/python/okstra_ctl/worktree/naming.py +15 -6
  128. package/runtime/python/okstra_ctl/worktree/provision.py +2 -1
  129. package/runtime/python/okstra_ctl/worktree_registry.py +27 -0
  130. package/runtime/python/okstra_ctl/wrapper_status.py +4 -0
  131. package/runtime/python/okstra_token_usage/codex.py +87 -2
  132. package/runtime/python/okstra_token_usage/collect.py +224 -49
  133. package/runtime/python/okstra_token_usage/pricing.py +8 -4
  134. package/runtime/skills/okstra-brief-gen/SKILL.md +22 -3
  135. package/runtime/skills/okstra-chat/SKILL.md +8 -6
  136. package/runtime/skills/okstra-inspect/SKILL.md +2 -2
  137. package/runtime/skills/okstra-inspect/facets/recap.md +52 -4
  138. package/runtime/skills/okstra-rollup/SKILL.md +1 -1
  139. package/runtime/skills/okstra-run/SKILL.md +23 -9
  140. package/runtime/skills/okstra-schedule-gen/SKILL.md +2 -0
  141. package/runtime/skills/okstra-user-response/SKILL.md +1 -1
  142. package/runtime/templates/reports/brief.template.md +15 -7
  143. package/runtime/templates/reports/final-verification-input.template.md +2 -0
  144. package/runtime/templates/reports/group-context.template.md +7 -0
  145. package/runtime/templates/reports/html/tasks/release-handoff.template.html +2 -2
  146. package/runtime/templates/reports/implementation-planning-input.template.md +3 -0
  147. package/runtime/templates/reports/improvement-discovery-input.template.md +3 -0
  148. package/runtime/templates/reports/release-handoff-input.template.md +5 -3
  149. package/runtime/templates/reports/schedule.template.md +13 -4
  150. package/runtime/templates/reports/task-brief.template.md +1 -1
  151. package/runtime/templates/reverify-output-contract.md +5 -0
  152. package/runtime/templates/worker-error-contract.md +2 -0
  153. package/runtime/templates/worker-prompt-preamble.md +1 -1
  154. package/runtime/validators/validate-run.py +81 -22
  155. package/runtime/validators/validate_session_conformance.py +102 -2
  156. package/runtime/python/okstra_ctl/wizard.py +0 -7168
@@ -19,6 +19,7 @@ from .codex import (
19
19
  codex_session_ids,
20
20
  codex_session_is_worker,
21
21
  codex_session_total,
22
+ codex_session_window_total,
22
23
  find_codex_sessions,
23
24
  )
24
25
  from .antigravity import (
@@ -32,9 +33,9 @@ from .grok import (
32
33
  grok_session_is_non_interactive,
33
34
  grok_session_total,
34
35
  )
35
- from .paths import claude_project_dir, utc_now
36
+ from .paths import claude_project_dir, find_session_jsonl, utc_now
36
37
  from .pricing import antigravity_cost_usd, provider_cost_usd
37
- from okstra_ctl.dispatch_state import worker_session_ids
38
+ from okstra_ctl.dispatch_state import worker_dispatch_records, worker_session_ids
38
39
  from okstra_ctl.models import provider_wrappers
39
40
  from okstra_ctl.wrapper_status import (
40
41
  log_path_for_prompt,
@@ -637,10 +638,18 @@ def _cli_sessions_for_windows(
637
638
  return sessions
638
639
 
639
640
 
640
- def _cli_session_totals(provider: str, session_paths: list[Path]) -> list[dict]:
641
+ def _cli_session_totals(
642
+ provider: str,
643
+ session_paths: list[Path],
644
+ *,
645
+ window: tuple[str, str] | None = None,
646
+ ) -> list[dict]:
647
+ """세션별 합계. `window` 는 codex 리드 전용 — run 보다 먼저 열린 세션을 창으로 자른다."""
641
648
  totals = []
642
649
  for session_path in session_paths:
643
- if provider == "codex":
650
+ if provider == "codex" and window is not None:
651
+ total = codex_session_window_total(session_path, *window)
652
+ elif provider == "codex":
644
653
  total = codex_session_total(session_path)
645
654
  elif provider == "antigravity":
646
655
  total = (
@@ -778,19 +787,50 @@ def collect_cli_usage(
778
787
  return block
779
788
 
780
789
 
781
- def _worker_cli_windows(
782
- project_root: Path,
783
- worker: dict,
784
- fallback_windows: list[tuple[str, str]],
785
- ) -> tuple[list[tuple[str, str]], Path | None, bool]:
786
- prompt_path = _resolve_project_path(project_root, str(worker.get("promptPath") or ""))
790
+ def _prompt_cli_window(
791
+ project_root: Path, prompt_path_raw: str,
792
+ ) -> tuple[tuple[str, str] | None, Path | None]:
793
+ """한 dispatch 의 래퍼 창 — 프롬프트 옆 status 사이드카의 started/ended."""
794
+ prompt_path = _resolve_project_path(project_root, prompt_path_raw)
787
795
  status_path = status_path_for_prompt(prompt_path) if prompt_path is not None else None
788
796
  execution = wrapper_execution(status_path)
789
797
  started_at = execution.get("startedAt")
790
- ended_at = execution.get("endedAt")
791
- if started_at:
792
- return [(started_at, ended_at or utc_now())], status_path, False
793
- return fallback_windows, status_path, True
798
+ if not started_at:
799
+ return None, status_path
800
+ return (started_at, execution.get("endedAt") or utc_now()), status_path
801
+
802
+
803
+ def _worker_prompt_paths(worker: dict, records: list[dict]) -> list[str]:
804
+ """이 워커가 띄운 dispatch 의 프롬프트 경로 전부 — 원장 행이 없으면(v1) 명부 행의 하나."""
805
+ paths: list[str] = []
806
+ for record in records:
807
+ raw = str(record.get("promptPath") or "").strip()
808
+ if raw and raw not in paths:
809
+ paths.append(raw)
810
+ if not paths:
811
+ raw = str(worker.get("promptPath") or "").strip()
812
+ if raw:
813
+ paths.append(raw)
814
+ return paths
815
+
816
+
817
+ def _worker_session_cwds(project_root: Path, records: list[dict]) -> list[Path]:
818
+ """세션 디렉터리가 인코딩된 cwd 후보 — grok·kimi 는 워크트리 안에서 돈다.
819
+
820
+ `providers/grok/adapter.py` 는 `request.worktree_path or request.project_root`
821
+ 를 cwd 로 넘기고 세션 디렉터리는 그 경로를 퍼센트 인코딩한 이름이다. 프로젝트
822
+ 루트로만 찾으면 워크트리 안에서 돈 세션은 0건이다(실측 2026-09-08 jobs
823
+ implementation-planning r01: grok critic 세션이 워크트리 이름 아래 있었다).
824
+ codex·claude 는 루트에서 돌므로 워크트리 후보는 빈 결과로 끝난다.
825
+ """
826
+ cwds: list[Path] = []
827
+ for record in records:
828
+ raw = str(record.get("worktreePath") or "").strip()
829
+ if raw and Path(raw) not in cwds:
830
+ cwds.append(Path(raw))
831
+ if project_root not in cwds:
832
+ cwds.append(project_root)
833
+ return cwds
794
834
 
795
835
 
796
836
  def _antigravity_usage_sources(
@@ -828,25 +868,114 @@ def _worker_cli_usage_block(
828
868
  provider: str,
829
869
  project_root: Path,
830
870
  worker: dict,
871
+ records: list[dict],
831
872
  fallback_windows: list[tuple[str, str]],
832
873
  ) -> dict:
833
- """공급자 프로세스 트랜스크립트와 실행 증거로 워커 사용량 블록을 만든다."""
834
- windows, status_path, used_fallback = _worker_cli_windows(
835
- project_root,
836
- worker,
837
- fallback_windows,
838
- )
839
- session_paths = _cli_sessions_for_windows(provider, project_root, windows)
874
+ """공급자 프로세스 트랜스크립트와 실행 증거로 워커 사용량 블록을 만든다.
875
+
876
+ dispatch 원장 행마다 래퍼 창을 하나씩 잡아 그 창의 세션을 전부 합산한다 —
877
+ 재검증·critic-gap·plan-verify 로 다시 띄운 실행이 각각 새 세션이므로
878
+ 첫 프롬프트 하나만 보면 나머지는 통째로 빠진다. 실행 상태는 마지막
879
+ dispatch 의 것, 소요 시간은 래퍼 창의 합이다.
880
+ """
881
+ windows: list[tuple[str, str]] = []
882
+ prompt_paths = _worker_prompt_paths(worker, records)
883
+ status_paths: list[Path | None] = []
884
+ for raw in prompt_paths:
885
+ window, status_path = _prompt_cli_window(project_root, raw)
886
+ status_paths.append(status_path)
887
+ if window is not None:
888
+ windows.append(window)
889
+ used_fallback = not windows
890
+ if used_fallback:
891
+ windows = fallback_windows
892
+ session_paths: list[Path] = []
893
+ for cwd in _worker_session_cwds(project_root, records):
894
+ for path in _cli_sessions_for_windows(provider, cwd, windows):
895
+ if path not in session_paths:
896
+ session_paths.append(path)
897
+ status_path = status_paths[-1] if status_paths else None
840
898
  if provider == "antigravity":
841
- for source in _antigravity_usage_sources(project_root, worker, status_path):
842
- if source not in session_paths:
843
- session_paths.append(source)
844
- return collect_cli_usage(
899
+ for raw, source_status in zip(prompt_paths, status_paths):
900
+ for source in _antigravity_usage_sources(
901
+ project_root, {"promptPath": raw}, source_status,
902
+ ):
903
+ if source not in session_paths:
904
+ session_paths.append(source)
905
+ block = collect_cli_usage(
845
906
  provider=provider,
846
907
  status_path=status_path,
847
908
  sessions=session_paths,
848
909
  fallback_window_used=used_fallback,
849
910
  )
911
+ durations = [
912
+ wrapper_execution(path).get("durationMs")
913
+ for path in status_paths
914
+ if path is not None
915
+ ]
916
+ measured = [value for value in durations if isinstance(value, int)]
917
+ if len(measured) > 1:
918
+ block["durationMs"] = sum(measured)
919
+ return block
920
+
921
+
922
+ def _wrapper_claude_worker(worker: dict) -> bool:
923
+ """호스트가 claude 가 아닐 때 래퍼로 띄운 claude 워커."""
924
+ provider = str(worker.get("provider") or worker.get("agent") or "").strip()
925
+ runner = str(worker.get("runner") or "").strip()
926
+ return provider in _SESSION_JSONL_PROVIDERS and runner in {"", "cli-wrapper"}
927
+
928
+
929
+ def _claude_wrapper_usage_block(
930
+ *,
931
+ project_root: Path,
932
+ worker_id: str,
933
+ state: dict,
934
+ window: tuple[str | None, str | None],
935
+ incremental: bool,
936
+ ) -> dict:
937
+ """래퍼로 띄운 claude 워커의 세션 jsonl — dispatch 가 발급한 세션 id 로 찾는다.
938
+
939
+ `okstra-claude-exec.sh` 는 `claude -p --session-id <id>` 로 돌고 그 id 는
940
+ `workerDispatches[].sessionId` 에 적힌다(`_dispatch_record`). 트랜스크립트는
941
+ `~/.claude/projects/<루트 인코딩>/<id>.jsonl` 에 있는데, claude 가 아닌
942
+ 호스트의 수집기는 이 워커를 "세션 jsonl 공급자" 라며 건너뛰어 항상
943
+ `unavailable` 이었다(실측 2026-09-08 jobs implementation-planning r01:
944
+ claude planner 5회·report-writer 2회 세션이 전부 디스크에 있었다). claude
945
+ 호스트의 `collect_claude_runtime_usage` 가 같은 id 로 하는 일을 여기서 한다.
946
+ """
947
+ since, until = window
948
+ session_ids = worker_session_ids(state, worker_id)
949
+ if not session_ids:
950
+ return na_block(
951
+ "claude wrapper worker has no dispatch session id recorded in workerDispatches"
952
+ )
953
+ totals: list[dict] = []
954
+ paths: list[Path] = []
955
+ attributed: list[str] = []
956
+ for session_id in session_ids:
957
+ path = find_session_jsonl(session_id, project_root)
958
+ if path is None:
959
+ continue
960
+ session_totals = claude_session_totals(
961
+ path, since=since, until=until, incremental=incremental,
962
+ )
963
+ # 창 밖 세션은 0 토큰 totals 가 되어 허위 0 으로 보고된다 — claude 호스트
964
+ # 경로와 같은 `startedAt` 검사로 거른다.
965
+ if not session_totals.get("startedAt"):
966
+ continue
967
+ totals.append(session_totals)
968
+ paths.append(path)
969
+ attributed.append(session_id)
970
+ if not totals:
971
+ return na_block(
972
+ "claude session jsonl not found under "
973
+ f"{claude_project_dir(project_root)} for dispatch session ids {session_ids}"
974
+ )
975
+ block = usage_block(_aggregate_totals(totals), source="claude-jsonl")
976
+ block["sessionIds"] = attributed
977
+ block["sessionPaths"] = [str(path) for path in paths]
978
+ return block
850
979
 
851
980
 
852
981
  def _attach_cli_usage(
@@ -886,27 +1015,46 @@ def _attach_cli_usage(
886
1015
 
887
1016
 
888
1017
  def _collect_cli_runtime_usage(
889
- state: dict, project_root: Path, team_state_path: Path | None = None,
1018
+ state: dict,
1019
+ project_root: Path,
1020
+ team_state_path: Path | None = None,
1021
+ *,
1022
+ incremental: bool = True,
890
1023
  ) -> dict:
891
1024
  windows_by_worker = _codex_worker_windows(project_root, state)
1025
+ run_window: tuple[str | None, str | None] = (None, None)
1026
+ if team_state_path is not None:
1027
+ run_window = resolve_run_window(team_state_path, state)
892
1028
  for worker in state.get("workers", []):
893
1029
  if not isinstance(worker, dict):
894
1030
  continue
1031
+ worker_id = str(worker.get("workerId") or "").strip()
1032
+ records = [
1033
+ dict(record) for record in worker_dispatch_records(state, worker_id)
1034
+ ] if worker_id else []
895
1035
  provider = _cli_assignment_provider(worker)
896
- if not provider:
1036
+ if provider:
1037
+ worker["usage"] = _worker_cli_usage_block(
1038
+ provider=provider,
1039
+ project_root=project_root,
1040
+ worker=worker,
1041
+ records=records,
1042
+ fallback_windows=windows_by_worker.get(worker_id, []),
1043
+ )
1044
+ elif worker_id and _wrapper_claude_worker(worker):
1045
+ worker["usage"] = _claude_wrapper_usage_block(
1046
+ project_root=project_root,
1047
+ worker_id=worker_id,
1048
+ state=state,
1049
+ window=run_window,
1050
+ incremental=incremental,
1051
+ )
1052
+ else:
897
1053
  worker["usage"] = na_block(
898
1054
  "worker usage is not read from a provider CLI transcript "
899
- "(host-native, session-jsonl provider, or no registered CLI provider): "
1055
+ "(host-native or no registered CLI provider): "
900
1056
  f"{worker.get('provider') or worker.get('agent') or worker.get('workerId')}"
901
1057
  )
902
- continue
903
- worker_id = str(worker.get("workerId") or "").strip()
904
- worker["usage"] = _worker_cli_usage_block(
905
- provider=provider,
906
- project_root=project_root,
907
- worker=worker,
908
- fallback_windows=windows_by_worker.get(worker_id, []),
909
- )
910
1058
  state["leadUsage"] = _cli_lead_usage(state, project_root, team_state_path)
911
1059
  _populate_usage_summary(state, team_name=resolve_team_name(state),
912
1060
  sessions_found=0, needle_source="none")
@@ -952,22 +1100,29 @@ def _cli_lead_usage(
952
1100
  if isinstance(worker, dict)
953
1101
  for path in ((worker.get("usage") or {}).get("cliSessionPaths") or [])
954
1102
  }
1103
+ # in-session codex 리드는 run 보다 먼저 열린 세션이다 — 창 안에서 시작한
1104
+ # 세션만 보면 없다고 나오고, 세션 전체를 더하면 다른 task 의 턴이 섞인다.
1105
+ # 창 안에서 활동한 세션을 후보에 넣고 토큰은 창으로 잘라 센다.
1106
+ window = (run_since, run_until) if provider == "codex" else None
1107
+ if provider == "codex":
1108
+ candidates = find_codex_sessions(
1109
+ project_root, run_since, run_until, active_before_start=True,
1110
+ )
1111
+ else:
1112
+ candidates = _cli_sessions_for_windows(
1113
+ provider, project_root, [(run_since, run_until)],
1114
+ )
955
1115
  sessions = _select_cli_lead_sessions(
956
1116
  provider,
957
- [
958
- path
959
- for path in _cli_sessions_for_windows(
960
- provider, project_root, [(run_since, run_until)],
961
- )
962
- if path not in worker_paths
963
- ],
1117
+ [path for path in candidates if path not in worker_paths],
964
1118
  state,
1119
+ window=window,
965
1120
  )
966
- totals = _cli_session_totals(provider, sessions)
1121
+ totals = _cli_session_totals(provider, sessions, window=window)
967
1122
  if not totals:
968
1123
  return na_block(
969
1124
  f"{provider} lead usage accounting is unavailable because no host session "
970
- "started in the run window."
1125
+ "was active in the run window."
971
1126
  )
972
1127
  return _cli_usage_block(provider, _aggregate_totals(totals), sessions)
973
1128
 
@@ -981,7 +1136,11 @@ def _cli_worker_session(provider: str, path: Path) -> bool:
981
1136
 
982
1137
 
983
1138
  def _select_cli_lead_sessions(
984
- provider: str, sessions: list[Path], state: dict,
1139
+ provider: str,
1140
+ sessions: list[Path],
1141
+ state: dict,
1142
+ *,
1143
+ window: tuple[str, str] | None = None,
985
1144
  ) -> list[Path]:
986
1145
  """워커 래퍼 세션을 빼고, 아이디가 있으면 그 세션, 없으면 벽시계가 가장 긴 대화."""
987
1146
  candidates = [
@@ -997,7 +1156,7 @@ def _select_cli_lead_sessions(
997
1156
  best = candidates[0]
998
1157
  best_ms = -1
999
1158
  for path in candidates:
1000
- totals = _cli_session_totals(provider, [path])
1159
+ totals = _cli_session_totals(provider, [path], window=window)
1001
1160
  total = totals[0] if totals else {}
1002
1161
  wall = _wall_ms(total.get("startedAt"), total.get("endedAt")) if total else None
1003
1162
  if wall is None:
@@ -1056,14 +1215,22 @@ def _populate_usage_summary(
1056
1215
  worker_cost += unattributed_usage.get("estimatedCostUsd", 0) or 0
1057
1216
 
1058
1217
  unmatched_models: list[str] = []
1059
- if lead.get("model") and lead.get("estimatedCostUsd") is None and (lead.get("totalTokens") or 0) > 0:
1218
+ if (
1219
+ lead.get("model")
1220
+ and lead.get("estimatedCostUsd") is None
1221
+ and lead.get("cliEstimatedCostUsd") is None
1222
+ and (lead.get("totalTokens") or 0) > 0
1223
+ ):
1060
1224
  unmatched_models.append(lead["model"])
1061
1225
  for w in workers:
1062
1226
  u = w.get("usage") or {}
1227
+ # CLI 블록의 가격은 `cliEstimatedCostUsd` 에 붙는다 — 그 키를 안 보면
1228
+ # 가격이 붙은 grok 도 미매칭으로 찍힌다.
1063
1229
  if (
1064
1230
  u.get("source") not in {"codex-cli", "agy-cli"}
1065
1231
  and u.get("model")
1066
1232
  and u.get("estimatedCostUsd") is None
1233
+ and u.get("cliEstimatedCostUsd") is None
1067
1234
  and (u.get("totalTokens") or 0) > 0
1068
1235
  ):
1069
1236
  unmatched_models.append(u["model"])
@@ -1269,6 +1436,12 @@ def collect_claude_runtime_usage(
1269
1436
  provider=provider,
1270
1437
  project_root=cwd,
1271
1438
  worker=worker,
1439
+ records=[
1440
+ dict(record)
1441
+ for record in worker_dispatch_records(
1442
+ state, str(worker_id or "").strip()
1443
+ )
1444
+ ] if worker_id else [],
1272
1445
  fallback_windows=cli_windows_by_worker.get(
1273
1446
  str(worker_id or "").strip(), []
1274
1447
  ),
@@ -1363,7 +1536,9 @@ def collect_cli_runtime_usage(
1363
1536
  ) -> dict:
1364
1537
  state = json.loads(team_state_path.read_text())
1365
1538
  cwd = project_root or _infer_project_root(team_state_path, state)
1366
- return _collect_cli_runtime_usage(state, cwd, team_state_path)
1539
+ return _collect_cli_runtime_usage(
1540
+ state, cwd, team_state_path, incremental=incremental,
1541
+ )
1367
1542
 
1368
1543
 
1369
1544
  def collect(
@@ -59,8 +59,12 @@ CLAUDE_PRICING = {
59
59
  # Claude Opus 5.
60
60
  "opus-5": (5.0, 6.25, 0.50, 25.0), # Opus 5 (cache prices derived from ratios)
61
61
 
62
- # Claude Sonnet 5.
63
- "sonnet-5": (3.0, 3.75, 0.30, 15.0), # Sonnet 5 (cache prices derived from ratios)
62
+ # Claude Sonnet 5 — $2/$10 launched as introductory pricing and became the
63
+ # standard price (the announced 2026-09-01 rise to $3/$15 was withdrawn;
64
+ # platform.claude.com/docs/en/about-claude/pricing, read 2026-09-08). The
65
+ # old (3.0, 3.75, 0.30, 15.0) row was the Sonnet 4.6 rate and overbilled
66
+ # every Sonnet 5 worker by 1.5x.
67
+ "sonnet-5": (2.0, 2.50, 0.20, 10.0), # Sonnet 5 (cache prices derived from ratios)
64
68
 
65
69
  # Claude 4 point releases (explicit so future divergence is easy to see).
66
70
  "opus-4-8": (5.0, 6.25, 0.50, 25.0), # Opus 4.8 (cache prices derived from ratios)
@@ -90,8 +94,8 @@ _LEGACY_CODEX_PRICING = {
90
94
 
91
95
  # GPT-5 series.
92
96
  # gpt-5.6 was dropped from the provider catalog; its rate stays so past runs
93
- # keep pricing. Substring matching means sol / terra / luna resolve here too
94
- # -- the 5.6 family rate, not a per-variant price we have a source for.
97
+ # keep pricing. sol / terra / luna no longer land here: their catalog rows
98
+ # carry per-variant prices, and `_match_pricing` prefers the longer key.
95
99
  "gpt-5.6": (5.00, 0.50, 30.0),
96
100
  "gpt-5.2-pro": (21.0, 2.10, 168.0),
97
101
  "gpt-5.1": (1.25, 0.125, 10.0),
@@ -222,6 +222,11 @@ Order of operations:
222
222
  questions into one. When you relay this preflight, tell the user in one
223
223
  line *why* task group comes first (it scopes where this ticket's brief —
224
224
  and any sub-ticket briefs — are written).
225
+
226
+ **Enforced:** `validators/validate-brief.py` requires the brief's
227
+ directory segment to equal the slugified frontmatter `task-group`, so a
228
+ brief written before the group is known — or under an invented placeholder
229
+ group — fails validation rather than landing in the wrong tree.
225
230
  1. `AskUserQuestion` (free text):
226
231
  `"Ticket key or URL (e.g. LIN-1234, PROJ-42, https://linear.app/..., https://your.atlassian.net/browse/...)"` →
227
232
  `ticket_ref`. Issue this only **after** Step 2a's task-group answer is in
@@ -451,6 +456,8 @@ to Source Material body, not to the filename):
451
456
  Validate the full `<ticket-id>-<file-title>` slug: must have at least one
452
457
  alphanumeric character after slugification. Apply Step 2c on collision.
453
458
 
459
+ **Enforced:** `validators/validate-brief.py` checks the brief's filename and its task-group directory segment against the slugified frontmatter `task-group`, and rejects a Source Material section whose entry does not match the source it names.
460
+
454
461
  ### 2c. Collision handling
455
462
 
456
463
  If a brief file already exists, `AskUserQuestion` with (`Skip` /
@@ -546,6 +553,8 @@ empty or trivially thin **after** reading Source Material verbatim, ask **at
546
553
  most one** `AskUserQuestion` to fill it. Never ask about sections already
547
554
  covered by the source material.
548
555
 
556
+ **Enforced:** `validators/validate-brief.py` fails a brief whose required section is missing, is left as a template placeholder (`is_placeholder`), or carries only the template's own example lines (`is_template_example`) — so a section skipped here does not pass as filled.
557
+
549
558
  ### Sharpening pass (bounded grill)
550
559
 
551
560
  The following rules absorb the useful parts of `grill-me` /
@@ -656,6 +665,8 @@ labels. Unlabelled augmentation is rejected as half-formed translation.
656
665
  **Inline form** — `> augmented: intent-inference — <one line>`.
657
666
  **Section form** — `- intent-inference: <one line>` under `Augmentation`.
658
667
 
668
+ **Enforced:** `validators/validate-brief.py` reads both forms (`inline_augmented_blockquotes`, `augmentation_entries`) and fails a brief whose entry carries a label outside this table (`parse_augmentation_label`).
669
+
659
670
  ### Auto-mirroring rule
660
671
 
661
672
  Every `intent-inference` entry must have a paired entry in `Open Questions`
@@ -1014,13 +1025,21 @@ per-page ratio instead of origin load). If the file is absent, ask once via
1014
1025
 
1015
1026
  Never fill the skeleton yourself from the tickets: a ticket summary decides
1016
1027
  nothing, and the sections ask for what the tickets do not say. If the file
1017
- exists, leave it untouched and mention its path in the hand-off block.
1028
+ exists, leave it untouched and mention its path in the hand-off block. The
1029
+ file's trailing `## Task Memory` region (between `<!-- okstra:task-memory:begin -->`
1030
+ and `end`) is okstra's: `report-finalize` rewrites it after every run with the
1031
+ group's start order and each task's latest conclusion, and a group whose
1032
+ runs finished before anyone created the file already has one holding that
1033
+ region alone — `init` then inserts the human sections above it.
1018
1034
 
1019
1035
  Then stop. Do not invoke `okstra-run` directly — the user chooses when to
1020
1036
  proceed, and they may want to edit the brief externally first. In the
1021
1037
  multi-brief case the user also decides the order in which tasks are
1022
- started. When a skeleton was created in 7a, add one line to the hand-off
1023
- block:
1038
+ started; the brief ordinal in each brief id (`<TICKET>-<n>-<slug>`) is what
1039
+ okstra reads as the group's start order (`Related Task Graph` edges are
1040
+ quoted beside it as `waits for`, never reordering), so number the briefs in
1041
+ the order the user wants them started. When a
1042
+ skeleton was created in 7a, add one line to the hand-off block:
1024
1043
 
1025
1044
  ```
1026
1045
  group context skeleton: <abs>/<task-group>/group-context.md (fill before /okstra-run)
@@ -48,10 +48,12 @@ If the host has a native picker, use it. Otherwise print a numbered list.
48
48
  1. Run `okstra chat rooms`.
49
49
  2. If the output is `no rooms`, say so and offer create.
50
50
  3. Otherwise pick a room from that list (picker, or numbered list if the picker limit is exceeded).
51
- 4. Ask for the display name as free input.
52
- 5. Run `okstra chat join --room <room> --name <display>`.
51
+ 4. Run `okstra chat members --room <room>`.
52
+ 5. Ask for the display name as free input.
53
+ 6. If that name is not in the members output, run `okstra chat join --room <room> --name <display>`.
54
+ 7. If that name is already in the members output, ask one question: is this session already in the room under that name (for example, it joined before the conversation was summarized)? If yes, do not run join; continue with `--as <display>`. If no, ask for a different display name and go back to 6.
53
55
 
54
- `--name` is required. An empty name, `all`, or a name already in the room fails.
56
+ `--name` is required. An empty name, `all`, or a name already in the room fails. `display name already taken` after a context loss means step 7, not a new name.
55
57
 
56
58
  ## Step 2: Unread
57
59
 
@@ -59,7 +61,7 @@ If the host has a native picker, use it. Otherwise print a numbered list.
59
61
  okstra chat unread --room <room> --as <display>
60
62
  ```
61
63
 
62
- Show the rows. Each row is `id @from YYYY-MM-DD HH:MM body`. Sender is always `@name`. The id is the first token (before the first space). A reply inserts `↑<parentId>` after the time. Recipient is not on the line.
64
+ Show the rows. Each row is `id @from YYYY-MM-DD HH:MM body`. Sender is always `@name`. The id is the first token (before the first space). A reply inserts `↑<parentId>` after the time. Recipient is not on the line. A body with several lines continues on the following rows; those rows start with two spaces and carry no id.
63
65
 
64
66
  If the output is `no unread`, do not ack.
65
67
 
@@ -69,7 +71,7 @@ If there are unread rows, always run:
69
71
  okstra chat ack --room <room> --as <display> --through <id>
70
72
  ```
71
73
 
72
- Use the last unread row's id. CLI unread does not move the cursor; this ack does.
74
+ Use the id of the last row that starts with an id. CLI unread does not move the cursor; this ack does. Ack never moves the cursor backwards.
73
75
 
74
76
  ## Step 3: Next action
75
77
 
@@ -81,7 +83,7 @@ There is no ack menu item. Choosing unread again shows the rows then acks, same
81
83
 
82
84
  1. Run `okstra chat members --room <room>`.
83
85
  2. Pick the recipient from `all` plus those names except the current display name (`--as`). Filter after `okstra chat members`. Do not pass `--as` to `members`. Recipient is required.
84
- 3. Ask for the body as free input.
86
+ 3. Ask for the body as free input. The body may span several lines; pass it as one `--body` argument.
85
87
  4. Run `okstra chat send --room <room> --as <display> --to <all|name> --body <text>`.
86
88
 
87
89
  ### Reply
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: okstra-inspect
3
3
  description: >-
4
- Use this for everything that happens AFTER a single okstra task has already run — inspecting it or light bookkeeping on it, never launching new work. The tell is usually a named task id (PROD-1623, dev-9184), often dropped without the word "okstra." Reach for it when the user wants one task's: status, current/next phase, blockers, or approval gate; its final report — where it is or whether it passed (verdict / pass); its elapsed time or context/read cost; its run history, re-run, or resume; to mark it done / in-progress / blocked / todo; or a failed run's error logs gathered into a report (error report). Also turns cross-project okstra errors into an anonymized zip (error feedback), and audits run health across tasks (run audit). NOT for starting a run (okstra-run), rollups/schedules (okstra-rollup / okstra-schedule-gen), a brief (okstra-brief-gen), setup (okstra-setup), or cross-project management (okstra-manager).
4
+ Use this for everything that happens AFTER an okstra task has already run — inspecting it or light bookkeeping, never launching new work. The tell is usually a named task id (PROD-1623, dev-9184) without the word "okstra." Reach for it when the user wants one task's: status, current/next phase, blockers, or approval gate; its final report — where it is or whether it passed; its elapsed time or context/read cost; its run history, re-run, or resume; to mark it done / in-progress / blocked / todo; or a failed run's error logs gathered into a report. The recap facet also takes a task-group: which briefs are done / in progress / not started, what is next, each task's latest conclusion. Also builds an anonymized cross-project error zip and audits run health. NOT for starting a run (okstra-run), a group's run/time/error totals (okstra-rollup), schedules (okstra-schedule-gen), a brief (okstra-brief-gen), setup (okstra-setup), or cross-project management (okstra-manager).
5
5
  ---
6
6
 
7
7
  # OKSTRA Inspect
@@ -19,7 +19,7 @@ Single read-side entry point for okstra runtime inspection plus the one status m
19
19
  | `errors` | `facets/errors.md` | Aggregate okstra-run error logs for a task into a timestamped markdown report; print a summary. |
20
20
  | `error-zip` | `facets/error-zip.md` | Collect cross-project okstra error logs into an anonymized zip (report + raw) and summarize clusters. |
21
21
  | `run-audit` | `facets/run-audit.md` | Check every run's artifacts against progress invariants; report what went wrong without an error ever being logged. |
22
- | `recap` | `facets/recap.md` | Summarize a task's run-to-run phase transitions, then answer free-form questions over its `.okstra` artifacts. Appends each summary/Q&A to `recap/recap-log.jsonl`, and writes agent-authored notes to `notes/` for feeding into later runs. |
22
+ | `recap` | `facets/recap.md` | Summarize a task's run-to-run phase transitions — or a task-group's start order, per-brief status, and each task's latest conclusion — then answer free-form questions over the `.okstra` artifacts. Appends each summary/Q&A to `recap-log.jsonl` (task `recap/`, group `.recap/`), and writes agent-authored notes to `notes/` for feeding into later runs. |
23
23
 
24
24
  ## Step 0: Preflight (shared)
25
25
 
@@ -6,13 +6,16 @@ Loaded lazily by the dispatch table in `SKILL.md` (core). Shared rules — Step
6
6
 
7
7
  Trigger phrases: "okstra recap", "recap", "work summary", "summarize this task", "before/after summary", "explain this work", "task question".
8
8
 
9
- On top of the `.okstra` artifacts accumulated for a single task-id, (a) produce a before/after summary and (b) answer free-form questions about that work. By default it reads only the `.okstra/` subtree (artifact mode). It expands to code mode only when the user explicitly asks to look at the code changes too. This sub-command performs only the `recap/recap-log.jsonl` append and the `notes/` note authoring (recap.5); it never mutates `task-manifest.json` / catalog / timeline.
9
+ On top of the `.okstra` artifacts accumulated for a single task-id — or for every task of one task-group — (a) produce a before/after summary and (b) answer free-form questions about that work. By default it reads only the `.okstra/` subtree (artifact mode). It expands to code mode only when the user explicitly asks to look at the code changes too. This sub-command performs only the `recap-log.jsonl` append and the `notes/` note authoring (recap.5); it never mutates `task-manifest.json` / catalog / timeline / `group-context.md`.
10
10
 
11
11
  ### recap.1 — Resolve target
12
12
 
13
- The same three forms as `cost` / `errors`: ① full task-key, ② bare token — the standard task-key resolution rule in `SKILL.md` (core), ③ task-root path.
13
+ Two scopes. Decide the scope before resolving anything:
14
14
 
15
- If the user asks for a recap without naming a task (e.g. "summarize this work"), apply the core no-task fallback as-is — do not list only a placeholder form like `<task-group>` and ask back.
15
+ - **Task scope** — the same three forms as `cost` / `errors`: ① full task-key, ② bare token — the standard task-key resolution rule in `SKILL.md` (core), ③ task-root path.
16
+ - **Group scope** — the user names a task-group ("recap the cache group", "how far is the `cache` group", "group progress") or the bare token resolves with every match `Matched via: taskGroup`. Do **not** drop to the N-match picker in that case; carry the group name as `<task-group>` and follow recap.2g / recap.4g below. A group-scope request still takes the one-task path when the user picks a single task from it.
17
+
18
+ If the user asks for a recap without naming a task or a group (e.g. "summarize this work"), apply the core no-task fallback as-is — do not list only a placeholder form like `<task-group>` and ask back.
16
19
 
17
20
  ### recap.2 — Assemble the before/after summary
18
21
 
@@ -28,11 +31,27 @@ Read the fixed-text `Run count` and repeated `Transition` blocks. Narrate each b
28
31
 
29
32
  For a transition that has a `Report`, run `okstra render-final-report <Report>` and read the rendered Markdown only when the user wants a deeper summary. Do not open the report data JSON directly and do not render every report automatically.
30
33
 
34
+ ### recap.2g — Assemble the group status (group scope)
35
+
36
+ ```bash
37
+ okstra model-io recap-input --project-root <projectRoot> --task-group <task-group>
38
+ ```
39
+
40
+ The projection joins three sources and is the only place to read them from — do not re-derive the queue from the brief directory, the catalog, or `group-context.md` by hand:
41
+
42
+ - the group's briefs give the start order (brief ordinal) and the `waits for` edges;
43
+ - the catalog / each `task-manifest.json` gives the real `Status` (`done` / `in progress` / `not started`), `Progress` (`<phase> (<state>)`), run count, next phase, and report path;
44
+ - the group document's Task Memory gives each recorded task's `Headline`, `Decision N`, `Watch out N`, `Follow-up N`. A task with `Memory run: -` finished before group memory existed — its conclusion lives only in its `Report`.
45
+
46
+ Read the head lines (`Brief count`, `Task count`, `Next in group`), then the repeated `Queue entry` blocks in order, then the repeated `Task` blocks. Narrate the group as: how many briefs, how many started, which are done, which is next (`Next in group` — `-` means every brief has started), and any `Waits for` that names a ticket not yet done. `Human sections: absent` means the group has no authored `group-context.md` context yet (only okstra's memory region) — say so once when the user asks why the group exists; do not offer to write it here (that is `okstra group-context init` via okstra-brief-gen).
47
+
48
+ Numbers beyond run count (CPU, wall clock, error totals) are okstra-rollup's; if the user asks for them, point to `/okstra-rollup --task-group <task-group>` instead of summing anything here.
49
+
31
50
  ### recap.3 — Free-form Q&A loop
32
51
 
33
52
  After the summary output, take the user's free-form questions.
34
53
 
35
- - **artifact mode (default)**: answer only from `.okstra/` artifacts (timeline, task-manifest, that run's final-report). Every factual claim is accompanied by a `path:line` citation to the `.okstra` file.
54
+ - **artifact mode (default)**: answer only from `.okstra/` artifacts (timeline, task-manifest, that run's final-report; in group scope also `.okstra/briefs/<task-group>/group-context.md` and each member task's artifacts). Every factual claim is accompanied by a `path:line` citation to the `.okstra` file.
36
55
  - **code mode (opt-in)**: only when the user **explicitly asks** — "look at the code changes / the diff too" — additionally read `git diff` · `git log` from that task's worktree/branch and answer at the code level. Entering recap never by itself switches to code mode.
37
56
 
38
57
  ### recap.4 — Persist each turn
@@ -48,6 +67,19 @@ okstra recap record <resolved-target> --project-root <projectRoot> \
48
67
 
49
68
  Put a one-or-two-line summary in `--answer`, not the full answer transcript. If there are multiple citations, repeat `--citation`. Do not silently swallow a recording failure (abnormal exit) — tell the user.
50
69
 
70
+ ### recap.4g — Persist each turn (group scope)
71
+
72
+ Same flags, but the group replaces the target and the log lands at `.okstra/tasks/<task-group>/.recap/recap-log.jsonl`:
73
+
74
+ ```bash
75
+ okstra recap record --task-group <task-group> --project-root <projectRoot> \
76
+ --kind <summary|qa> --mode <artifact|code> \
77
+ --question "<question or omit>" --answer "<one or two line summary>" \
78
+ --citation "<path:line>"
79
+ ```
80
+
81
+ `recap note` has no group form — a note belongs to the task whose next run will carry it, so write it against that task-key.
82
+
51
83
  ### recap — Output template
52
84
 
53
85
  ```markdown
@@ -62,6 +94,22 @@ Before/after summary (runs: <N>):
62
94
  <Free-form questions follow. To include code changes, ask "also show me the diff".>
63
95
  ```
64
96
 
97
+ Group scope:
98
+
99
+ ```markdown
100
+ ## okstra Recap — task-group <task-group> (briefs: <Brief count>, started: <Task count>)
101
+
102
+ | # | task | status | progress | waits for | latest conclusion |
103
+ |---|---|---|---|---|---|
104
+ | 1 | dev-10626-1-… | done | release-handoff (completed) | — | <Headline, or "report only" when Memory run is -> |
105
+ | 2 | dev-10627-2-… | in progress | implementation-option-selection (prepared) | DEV-10642 | <Headline> |
106
+ | 3 | dev-10628-3-… | not started | — | — | — |
107
+
108
+ Next in group: <Next in group or "every brief has started">
109
+
110
+ <Free-form questions follow.>
111
+ ```
112
+
65
113
  ### recap.5 — Agent-authored notes (`notes/`)
66
114
 
67
115
  Leave something in `notes/` only when, during recap, you produced an artifact **for the okstra task** — verification evidence, a design/decision draft, an analysis note — that may serve as grounding for future runs, and whose content is **neither a rendered report nor a user decision**.
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: okstra-rollup
3
3
  description: |
4
- Use when the user wants run results from MULTIPLE okstra tasks collected and summarized at once — a task-group digest or a whole-project roll-up, not a single task. Aggregates per-task run count, elapsed time, error count, and latest report path, plus group-level totals and status/category/phase tallies, then synthesizes a cross-task prose summary from the report files. Make sure to use this skill whenever the user mentions "okstra rollup", "rollup", "task-group summary", "group-level report", "collect multiple task results", "whole-project task status summary", "cross-task summary", "consolidated group report", "organize all tasks", "run results all at once", even if they don't say "rollup". For a SINGLE task's report/time/errors/recap use okstra-inspect instead; for a forward-looking work plan over non-done tasks use okstra-schedule-gen.
4
+ Use when the user wants run results from MULTIPLE okstra tasks collected and summarized at once — a task-group digest or a whole-project roll-up, not a single task. Aggregates per-task run count, elapsed time, error count, and latest report path, plus group-level totals and status/category/phase tallies, then synthesizes a cross-task prose summary from the report files. Make sure to use this skill whenever the user mentions "okstra rollup", "rollup", "task-group summary", "group-level report", "collect multiple task results", "whole-project task status summary", "cross-task summary", "consolidated group report", "organize all tasks", "run results all at once", even if they don't say "rollup". For a SINGLE task's report/time/errors/recap, or "where does the group stand" (per-brief status, what is next), use okstra-inspect; this skill is for the numbers and the cross-task digest. For a forward-looking work plan over non-done tasks use okstra-schedule-gen.
5
5
  ---
6
6
 
7
7
  # OKSTRA Rollup