okstra 0.200.0 → 0.201.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 (96) hide show
  1. package/README.md +4 -2
  2. package/dist/cli-registry.mjs +6 -0
  3. package/dist/cli-registry.mjs.map +1 -1
  4. package/docs/cli.md +14 -3
  5. package/package.json +1 -1
  6. package/runtime/BUILD.json +2 -2
  7. package/runtime/agents/workers/report-writer-worker.md +7 -3
  8. package/runtime/bin/okstra-spawn-followups.py +2 -2
  9. package/runtime/prompts/duties/technical-verification-worker.md +44 -0
  10. package/runtime/prompts/launch.template.md +7 -1
  11. package/runtime/prompts/lead/okstra-lead-contract.md +7 -2
  12. package/runtime/prompts/lead/plan-body-verification.md +3 -1
  13. package/runtime/prompts/lead/report-writer.md +11 -5
  14. package/runtime/prompts/lead/team-contract.md +6 -0
  15. package/runtime/prompts/profiles/_implementation-verifier.md +7 -1
  16. package/runtime/prompts/profiles/final-verification.md +5 -0
  17. package/runtime/prompts/profiles/forbidden-actions.json +6 -0
  18. package/runtime/prompts/profiles/implementation-option-selection.md +7 -1
  19. package/runtime/prompts/profiles/implementation-planning.md +1 -0
  20. package/runtime/prompts/profiles/technical-verification.md +53 -0
  21. package/runtime/prompts/wizard/prompts.ko.json +2 -1
  22. package/runtime/python/okstra_ctl/adapters/hosts/claude-code/relay.md +4 -4
  23. package/runtime/python/okstra_ctl/adapters/hosts/codex/relay.md +2 -0
  24. package/runtime/python/okstra_ctl/adapters/providers/zai/adapter.py +36 -5
  25. package/runtime/python/okstra_ctl/agent/invocation.py +14 -6
  26. package/runtime/python/okstra_ctl/agent/prompt_cli/cli.py +4 -3
  27. package/runtime/python/okstra_ctl/agent/prompt_cli/corrections.py +83 -22
  28. package/runtime/python/okstra_ctl/agent/prompt_cli/materialize.py +44 -2
  29. package/runtime/python/okstra_ctl/conformance.py +2 -20
  30. package/runtime/python/okstra_ctl/dispatch_core.py +25 -5
  31. package/runtime/python/okstra_ctl/dispatch_state.py +2 -0
  32. package/runtime/python/okstra_ctl/domain/provider.py +0 -1
  33. package/runtime/python/okstra_ctl/domain/role.py +1 -0
  34. package/runtime/python/okstra_ctl/execution_mutation_audit.py +6 -1
  35. package/runtime/python/okstra_ctl/implementation_direction.py +64 -7
  36. package/runtime/python/okstra_ctl/implementation_options.py +58 -45
  37. package/runtime/python/okstra_ctl/model_pool.py +2 -5
  38. package/runtime/python/okstra_ctl/next_phase.py +3 -0
  39. package/runtime/python/okstra_ctl/plan_items.py +15 -0
  40. package/runtime/python/okstra_ctl/plan_items_cli.py +9 -3
  41. package/runtime/python/okstra_ctl/qa_commands.py +30 -0
  42. package/runtime/python/okstra_ctl/registry/provider_registry.py +11 -8
  43. package/runtime/python/okstra_ctl/render.py +3 -0
  44. package/runtime/python/okstra_ctl/render_final_report.py +1 -0
  45. package/runtime/python/okstra_ctl/report_assembly.py +8 -2
  46. package/runtime/python/okstra_ctl/report_contract.py +3 -0
  47. package/runtime/python/okstra_ctl/report_corrections.py +209 -93
  48. package/runtime/python/okstra_ctl/report_finalize.py +25 -8
  49. package/runtime/python/okstra_ctl/report_html/router.py +2 -0
  50. package/runtime/python/okstra_ctl/report_html/view_models/technical_verification.py +21 -0
  51. package/runtime/python/okstra_ctl/report_projections.py +4 -3
  52. package/runtime/python/okstra_ctl/report_synthesis_packet.py +181 -47
  53. package/runtime/python/okstra_ctl/run.py +82 -0
  54. package/runtime/python/okstra_ctl/team.py +4 -1
  55. package/runtime/python/okstra_ctl/technical_verification.py +195 -0
  56. package/runtime/python/okstra_ctl/usage_identity.py +54 -0
  57. package/runtime/python/okstra_ctl/usage_report.py +22 -8
  58. package/runtime/python/okstra_ctl/verification_target.py +74 -0
  59. package/runtime/python/okstra_ctl/wizard/__init__.py +1 -1
  60. package/runtime/python/okstra_ctl/wizard/cli.py +2 -1
  61. package/runtime/python/okstra_ctl/wizard/confirmation.py +38 -2
  62. package/runtime/python/okstra_ctl/wizard/engine.py +3 -0
  63. package/runtime/python/okstra_ctl/wizard/ids.py +1 -0
  64. package/runtime/python/okstra_ctl/wizard/outcome.py +63 -0
  65. package/runtime/python/okstra_ctl/wizard/picker_navigation.py +2 -2
  66. package/runtime/python/okstra_ctl/wizard/registry.py +1 -1
  67. package/runtime/python/okstra_ctl/wizard/render.py +8 -55
  68. package/runtime/python/okstra_ctl/wizard/roles.py +11 -7
  69. package/runtime/python/okstra_ctl/wizard/sources.py +28 -2
  70. package/runtime/python/okstra_ctl/wizard/state.py +13 -6
  71. package/runtime/python/okstra_ctl/wizard/steps_plan.py +8 -0
  72. package/runtime/python/okstra_ctl/worker_liveness.py +52 -39
  73. package/runtime/python/okstra_ctl/worker_prompt_policy.py +2 -0
  74. package/runtime/python/okstra_ctl/workflow.py +8 -0
  75. package/runtime/python/okstra_ctl/write_policy.py +23 -0
  76. package/runtime/python/okstra_token_usage/blocks.py +50 -1
  77. package/runtime/python/okstra_token_usage/claude.py +42 -21
  78. package/runtime/python/okstra_token_usage/codex.py +17 -0
  79. package/runtime/python/okstra_token_usage/collect.py +299 -162
  80. package/runtime/python/okstra_token_usage/cursor.py +2 -3
  81. package/runtime/python/okstra_token_usage/report.py +35 -30
  82. package/runtime/python/okstra_token_usage/task_totals.py +3 -12
  83. package/runtime/schemas/final-report-v2.0.schema.json +298 -7
  84. package/runtime/schemas/final-report-v3.0.schema.json +298 -7
  85. package/runtime/schemas/report-narrative-v3.0.schema.json +1 -0
  86. package/runtime/schemas/report-synthesis-packet-v1.0.schema.json +1 -1
  87. package/runtime/schemas/report-writer-corrections-v1.0.schema.json +30 -3
  88. package/runtime/skills/okstra-run/SKILL.md +10 -2
  89. package/runtime/skills/okstra-setup/SKILL.md +42 -7
  90. package/runtime/templates/report-writer-prompt-preamble.md +7 -3
  91. package/runtime/templates/reports/html/i18n/en.json +11 -0
  92. package/runtime/templates/reports/html/i18n/ko.json +11 -0
  93. package/runtime/templates/reports/html/tasks/implementation-option-selection.template.html +7 -3
  94. package/runtime/templates/reports/html/tasks/technical-verification.template.html +35 -0
  95. package/runtime/templates/reports/md/tasks/technical-verification.template.md +5 -0
  96. package/runtime/validators/validate-run.py +9 -4
@@ -4,12 +4,12 @@ from __future__ import annotations
4
4
  import json
5
5
  import sys
6
6
  import re
7
- from datetime import datetime
8
7
  from pathlib import Path
9
8
 
10
9
  from .cursor import MAX_NEEDLES, fresh_cache, load_cache, save_cache
11
10
  from .paths import claude_project_dir, ts_in_window
12
11
  from okstra_ctl.wrapper_status import read_wrapper_status
12
+ from okstra_ctl.usage_cells import duration_ms_from_bounds
13
13
 
14
14
  # lead 의 단계 체크포인트 라인(prompts/lead/okstra-lead-contract.md "Progress reporting") — phase id 가
15
15
  # `phase-<digit>` 로 시작하는 라인만 마커로 인정해 일반 대화의 오탐을 줄인다.
@@ -45,19 +45,13 @@ def claude_code_status_total(status_path: Path) -> dict:
45
45
  }
46
46
 
47
47
 
48
- def _event_from_record(rec: dict) -> dict | None:
49
- """jsonl 레코드 1개 압축 이벤트. 집계에 기여하지 않으면 None.
50
-
51
- 키: t=timestamp, i/o=input/output, c=cache_creation 합, c5/c1=ephemeral
52
- 5m/1h, r=cache_read, u=tool_use 수. 0/부재 필드는 생략(캐시 크기 절약).
53
- ts-only 레코드도 보존한다 — 임의 윈도우의 first/last ts 산출에 필요.
54
- """
55
- msg = rec.get("message")
56
- if not isinstance(msg, dict):
57
- msg = {}
48
+ def _usage_from_message(msg: dict) -> dict:
49
+ """응답 식별자를 남겨 여러 출력 조각의 사용량을 번만 집계한다."""
58
50
  ev: dict = {}
59
51
  usage = msg.get("usage")
60
- if usage:
52
+ if isinstance(usage, dict):
53
+ if isinstance(msg.get("id"), str) and msg["id"]:
54
+ ev["m"] = msg["id"]
61
55
  for src, key in (("input_tokens", "i"), ("output_tokens", "o"),
62
56
  ("cache_read_input_tokens", "r")):
63
57
  v = usage.get(src, 0) or 0
@@ -79,6 +73,15 @@ def _event_from_record(rec: dict) -> dict | None:
79
73
  elif cc_total:
80
74
  # API 분해가 없으면 전부 5m 티어로(1.25x — 더 싼 가정, 기존 동작).
81
75
  ev["c5"] = cc_total
76
+ return ev
77
+
78
+
79
+ def _event_from_record(rec: dict) -> dict | None:
80
+ """사용량과 별개로 각 조각의 도구·진행·시각 이벤트를 보존한다."""
81
+ msg = rec.get("message")
82
+ if not isinstance(msg, dict):
83
+ msg = {}
84
+ ev = _usage_from_message(msg)
82
85
  if rec.get("type") == "assistant":
83
86
  tools = sum(1 for b in (msg.get("content") or [])
84
87
  if isinstance(b, dict) and b.get("type") == "tool_use")
@@ -102,6 +105,31 @@ def _event_from_record(rec: dict) -> dict | None:
102
105
  return ev or None
103
106
 
104
107
 
108
+ def _response_usage_events(events: list[dict], since: str | None, until: str | None):
109
+ """응답별 누적 스냅샷의 창 안 증가량만 세며 도구 이벤트는 그대로 둔다."""
110
+ latest: dict[str, int] = {}
111
+ baseline: dict[str, dict] = {}
112
+ for index, event in enumerate(events):
113
+ message_id = event.get("m")
114
+ timestamp = event.get("t")
115
+ if message_id and (not timestamp or ts_in_window(timestamp, None, until)):
116
+ latest[message_id] = index
117
+ if timestamp and since and not ts_in_window(timestamp, since, None):
118
+ baseline[message_id] = event
119
+ usage_keys = {"i", "o", "c", "c5", "c1", "r"}
120
+ for index, event in enumerate(events):
121
+ message_id = event.get("m")
122
+ if not message_id:
123
+ yield event
124
+ continue
125
+ projected = {key: value for key, value in event.items() if key not in usage_keys}
126
+ if latest.get(message_id) == index:
127
+ previous = baseline.get(message_id) or {}
128
+ projected.update({key: max(0, event.get(key, 0) - previous.get(key, 0))
129
+ for key in usage_keys})
130
+ yield projected
131
+
132
+
105
133
  def _session_meta_from_record(rec: dict) -> tuple[str | None, str | None]:
106
134
  """레코드에서 (agentName, model) 후보 추출 — 둘 다 first-non-null 정책."""
107
135
  agent = rec.get("agentName") or None
@@ -213,7 +241,7 @@ def _totals_from_events(events: list[dict], agent_name: str | None,
213
241
  progress_markers: list[dict] = []
214
242
  first_ts: str | None = None
215
243
  last_ts: str | None = None
216
- for ev in events:
244
+ for ev in _response_usage_events(events, since, until):
217
245
  ts = ev.get("t")
218
246
  if ts and not ts_in_window(ts, since, until):
219
247
  continue
@@ -231,14 +259,7 @@ def _totals_from_events(events: list[dict], agent_name: str | None,
231
259
  first_ts = ts
232
260
  if last_ts is None or ts > last_ts:
233
261
  last_ts = ts
234
- duration_ms = 0
235
- if first_ts and last_ts:
236
- try:
237
- a = datetime.fromisoformat(first_ts.replace("Z", "+00:00"))
238
- b = datetime.fromisoformat(last_ts.replace("Z", "+00:00"))
239
- duration_ms = max(0, int((b - a).total_seconds() * 1000))
240
- except ValueError:
241
- duration_ms = 0
262
+ duration_ms = duration_ms_from_bounds(first_ts, last_ts) or 0
242
263
  # '처리 토큰' total 에서 cache_read 는 제외한다. claude 는 매 턴 직전까지의
243
264
  # 컨텍스트 전체를 캐시에서 재읽기(cache_read)하므로, 단순 합산하면 같은 토큰을
244
265
  # 턴 수만큼 중복 카운트해 처리량이 비현실적으로 부풀려진다(예: in-session
@@ -3,6 +3,7 @@ from __future__ import annotations
3
3
 
4
4
  import json
5
5
  import os
6
+ import re
6
7
  from datetime import datetime, timezone
7
8
  from pathlib import Path
8
9
  from .jsonl_io import iter_jsonl
@@ -12,6 +13,22 @@ from .paths import CODEX_SESSIONS, codex_session_roots, ts_in_window
12
13
  _DEFAULT_CODEX_SESSIONS = CODEX_SESSIONS
13
14
 
14
15
 
16
+ def codex_wrapper_session_ids(log_path: Path) -> set[str]:
17
+ """사용자 프롬프트 앞 실행기 헤더에서만 세션 ID를 읽는다."""
18
+ try:
19
+ with log_path.open(encoding="utf-8", errors="replace") as stream:
20
+ for line in stream:
21
+ clean = re.sub(r"\x1b\[[0-9;]*m", "", line).strip()
22
+ if clean == "user" or clean.endswith("[worker]user"):
23
+ break
24
+ match = re.search(r"(?:^|\[worker\])session id:\s*([\w-]+)\s*$", clean)
25
+ if match:
26
+ return {match.group(1)}
27
+ except OSError:
28
+ return set()
29
+ return set()
30
+
31
+
15
32
  def codex_session_total(jsonl_path: Path) -> dict:
16
33
  """Return last token_count snapshot from a codex rollout jsonl."""
17
34
  last: dict | None = None