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.
- package/README.md +1 -1
- package/dist/cli-registry.mjs +9 -0
- package/dist/cli-registry.mjs.map +1 -1
- package/dist/commands/chat/chat.mjs +54 -4
- package/dist/commands/chat/chat.mjs.map +1 -1
- package/dist/commands/lifecycle/install.mjs +12 -5
- package/dist/commands/lifecycle/install.mjs.map +1 -1
- package/dist/commands/lifecycle/uninstall.mjs +2 -1
- package/dist/commands/lifecycle/uninstall.mjs.map +1 -1
- package/dist/lib/host-config.d.mts +5 -3
- package/dist/lib/host-config.mjs +89 -31
- package/dist/lib/host-config.mjs.map +1 -1
- package/docs/architecture/storage-model.md +4 -1
- package/docs/architecture.md +8 -4
- package/docs/cli.md +10 -8
- package/docs/for-ai/skills/okstra-brief-gen.md +1 -1
- package/docs/for-ai/skills/okstra-chat.md +3 -3
- package/docs/for-ai/skills/okstra-inspect.md +11 -1
- package/docs/for-ai/skills/okstra-rollup.md +1 -0
- package/docs/for-ai/skills/okstra-run.md +5 -4
- package/docs/for-ai/skills/okstra-user-response.md +1 -1
- package/docs/project-structure-overview.md +17 -5
- package/docs/task-process/README.md +1 -1
- package/docs/task-process/error-analysis.md +2 -2
- package/docs/task-process/final-verification.md +1 -1
- package/docs/task-process/implementation.md +1 -1
- package/docs/task-process/release-handoff.md +8 -6
- package/docs/task-process/requirements-discovery.md +1 -1
- package/package.json +1 -1
- package/runtime/BUILD.json +2 -2
- package/runtime/agents/workers/claude-worker.md +4 -0
- package/runtime/bin/okstra-report-translate.py +10 -4
- package/runtime/prompts/launch.template.md +11 -5
- package/runtime/prompts/lead/adapters/cmux.md +4 -7
- package/runtime/prompts/lead/context-loader.md +2 -0
- package/runtime/prompts/lead/convergence.md +20 -83
- package/runtime/prompts/lead/okstra-lead-contract.md +24 -6
- package/runtime/prompts/lead/plan-body-verification.md +10 -3
- package/runtime/prompts/lead/report-writer.md +7 -4
- package/runtime/prompts/lead/team-contract.md +15 -9
- package/runtime/prompts/profiles/_common-contract.md +1 -0
- package/runtime/prompts/profiles/_implementation-deliverable.md +2 -0
- package/runtime/prompts/profiles/_implementation-diff-review.md +4 -0
- package/runtime/prompts/profiles/_implementation-executor.md +9 -5
- package/runtime/prompts/profiles/_implementation-self-check.md +2 -0
- package/runtime/prompts/profiles/_implementation-verifier.md +3 -4
- package/runtime/prompts/profiles/_stage-discipline.md +12 -5
- package/runtime/prompts/profiles/final-verification.md +1 -1
- package/runtime/prompts/profiles/implementation.md +2 -0
- package/runtime/prompts/profiles/release-handoff.md +4 -3
- package/runtime/prompts/wizard/prompts.ko.json +2 -1
- package/runtime/python/okstra_ctl/adapters/dispatch/cmux.py +6 -0
- package/runtime/python/okstra_ctl/adapters/hosts/antigravity/adapter.py +1 -0
- package/runtime/python/okstra_ctl/adapters/hosts/antigravity/relay.md +2 -2
- package/runtime/python/okstra_ctl/adapters/hosts/capability_adapter.py +9 -0
- package/runtime/python/okstra_ctl/adapters/hosts/claude-code/adapter.py +1 -0
- package/runtime/python/okstra_ctl/adapters/hosts/claude-code/relay.md +3 -3
- package/runtime/python/okstra_ctl/adapters/hosts/codex/adapter.py +1 -0
- package/runtime/python/okstra_ctl/adapters/hosts/codex/relay.md +31 -3
- package/runtime/python/okstra_ctl/adapters/hosts/external/adapter.py +1 -0
- package/runtime/python/okstra_ctl/adapters/hosts/external/relay.md +3 -3
- package/runtime/python/okstra_ctl/adapters/hosts/grok/adapter.py +1 -0
- package/runtime/python/okstra_ctl/adapters/hosts/grok/relay.md +2 -2
- package/runtime/python/okstra_ctl/adapters/hosts/kimi/adapter.py +1 -0
- package/runtime/python/okstra_ctl/adapters/hosts/kimi/relay.md +2 -2
- package/runtime/python/okstra_ctl/adapters/providers/codex/adapter.py +13 -7
- package/runtime/python/okstra_ctl/adapters/providers/kimi/adapter.py +5 -2
- package/runtime/python/okstra_ctl/agent/invocation.py +107 -5
- package/runtime/python/okstra_ctl/agent/prompt_cli/cli.py +16 -0
- package/runtime/python/okstra_ctl/agent/prompt_cli/dynamic_verifier.py +3 -2
- package/runtime/python/okstra_ctl/agent/prompt_cli/jobs.py +87 -0
- package/runtime/python/okstra_ctl/agent/prompt_cli/materialize.py +72 -3
- package/runtime/python/okstra_ctl/analysis_packet.py +47 -6
- package/runtime/python/okstra_ctl/cmux.py +34 -13
- package/runtime/python/okstra_ctl/consumers.py +30 -7
- package/runtime/python/okstra_ctl/convergence.py +110 -21
- package/runtime/python/okstra_ctl/convergence_provenance.py +83 -0
- package/runtime/python/okstra_ctl/dispatch_core.py +127 -5
- package/runtime/python/okstra_ctl/dispatch_state.py +44 -13
- package/runtime/python/okstra_ctl/domain/host.py +5 -0
- package/runtime/python/okstra_ctl/domain/wizard/interaction.py +1 -0
- package/runtime/python/okstra_ctl/error_log_write.py +44 -4
- package/runtime/python/okstra_ctl/execution_mutation_audit.py +84 -7
- package/runtime/python/okstra_ctl/fixed_text.py +25 -7
- package/runtime/python/okstra_ctl/group_context.py +475 -10
- package/runtime/python/okstra_ctl/handoff.py +109 -32
- package/runtime/python/okstra_ctl/handoff_verification.py +80 -0
- package/runtime/python/okstra_ctl/initial_prompt_materialization.py +52 -10
- package/runtime/python/okstra_ctl/lead_progress.py +291 -0
- package/runtime/python/okstra_ctl/model_io/renderers.py +60 -1
- package/runtime/python/okstra_ctl/model_io_cli.py +6 -1
- package/runtime/python/okstra_ctl/ports/worker_dispatch.py +4 -0
- package/runtime/python/okstra_ctl/recap.py +179 -17
- package/runtime/python/okstra_ctl/reconcile.py +55 -4
- package/runtime/python/okstra_ctl/render.py +10 -1
- package/runtime/python/okstra_ctl/report_assembly.py +7 -6
- package/runtime/python/okstra_ctl/report_finalize.py +204 -11
- package/runtime/python/okstra_ctl/report_html/render.py +15 -4
- package/runtime/python/okstra_ctl/run.py +48 -14
- package/runtime/python/okstra_ctl/stage_targets.py +66 -0
- package/runtime/python/okstra_ctl/verdict_blocks.py +39 -12
- package/runtime/python/okstra_ctl/wizard/__init__.py +148 -0
- package/runtime/python/okstra_ctl/wizard/__main__.py +13 -0
- package/runtime/python/okstra_ctl/wizard/cli.py +176 -0
- package/runtime/python/okstra_ctl/wizard/confirmation.py +331 -0
- package/runtime/python/okstra_ctl/wizard/engine.py +384 -0
- package/runtime/python/okstra_ctl/wizard/ids.py +411 -0
- package/runtime/python/okstra_ctl/wizard/picker_navigation.py +75 -0
- package/runtime/python/okstra_ctl/wizard/prompts.py +202 -0
- package/runtime/python/okstra_ctl/wizard/registry.py +836 -0
- package/runtime/python/okstra_ctl/wizard/render.py +189 -0
- package/runtime/python/okstra_ctl/wizard/roles.py +737 -0
- package/runtime/python/okstra_ctl/wizard/sources.py +703 -0
- package/runtime/python/okstra_ctl/wizard/state.py +691 -0
- package/runtime/python/okstra_ctl/wizard/statefile.py +145 -0
- package/runtime/python/okstra_ctl/wizard/steps_analysis.py +558 -0
- package/runtime/python/okstra_ctl/wizard/steps_identity.py +828 -0
- package/runtime/python/okstra_ctl/wizard/steps_options.py +340 -0
- package/runtime/python/okstra_ctl/wizard/steps_plan.py +959 -0
- package/runtime/python/okstra_ctl/wizard/steps_roles.py +672 -0
- package/runtime/python/okstra_ctl/worker_prompt_contract.py +43 -3
- package/runtime/python/okstra_ctl/worker_prompt_headers.py +14 -0
- package/runtime/python/okstra_ctl/workflow.py +2 -2
- package/runtime/python/okstra_ctl/worktree/__init__.py +2 -0
- package/runtime/python/okstra_ctl/worktree/cleanliness.py +7 -4
- package/runtime/python/okstra_ctl/worktree/git_ops.py +7 -0
- package/runtime/python/okstra_ctl/worktree/naming.py +15 -6
- package/runtime/python/okstra_ctl/worktree/provision.py +2 -1
- package/runtime/python/okstra_ctl/worktree_registry.py +27 -0
- package/runtime/python/okstra_ctl/wrapper_status.py +4 -0
- package/runtime/python/okstra_token_usage/codex.py +87 -2
- package/runtime/python/okstra_token_usage/collect.py +224 -49
- package/runtime/python/okstra_token_usage/pricing.py +8 -4
- package/runtime/skills/okstra-brief-gen/SKILL.md +22 -3
- package/runtime/skills/okstra-chat/SKILL.md +8 -6
- package/runtime/skills/okstra-inspect/SKILL.md +2 -2
- package/runtime/skills/okstra-inspect/facets/recap.md +52 -4
- package/runtime/skills/okstra-rollup/SKILL.md +1 -1
- package/runtime/skills/okstra-run/SKILL.md +23 -9
- package/runtime/skills/okstra-schedule-gen/SKILL.md +2 -0
- package/runtime/skills/okstra-user-response/SKILL.md +1 -1
- package/runtime/templates/reports/brief.template.md +15 -7
- package/runtime/templates/reports/final-verification-input.template.md +2 -0
- package/runtime/templates/reports/group-context.template.md +7 -0
- package/runtime/templates/reports/html/tasks/release-handoff.template.html +2 -2
- package/runtime/templates/reports/implementation-planning-input.template.md +3 -0
- package/runtime/templates/reports/improvement-discovery-input.template.md +3 -0
- package/runtime/templates/reports/release-handoff-input.template.md +5 -3
- package/runtime/templates/reports/schedule.template.md +13 -4
- package/runtime/templates/reports/task-brief.template.md +1 -1
- package/runtime/templates/reverify-output-contract.md +5 -0
- package/runtime/templates/worker-error-contract.md +2 -0
- package/runtime/templates/worker-prompt-preamble.md +1 -1
- package/runtime/validators/validate-run.py +81 -22
- package/runtime/validators/validate_session_conformance.py +102 -2
- 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(
|
|
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
|
|
782
|
-
project_root: Path,
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
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
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
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
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
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
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
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,
|
|
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
|
|
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
|
|
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
|
-
"
|
|
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,
|
|
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
|
|
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(
|
|
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
|
-
|
|
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.
|
|
94
|
-
#
|
|
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
|
|
1023
|
-
|
|
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.
|
|
52
|
-
5.
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
13
|
+
Two scopes. Decide the scope before resolving anything:
|
|
14
14
|
|
|
15
|
-
|
|
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
|
|
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
|