okstra 0.30.3 → 0.32.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/docs/kr/architecture.md +2 -2
- package/docs/kr/cli.md +2 -2
- package/package.json +1 -1
- package/runtime/BUILD.json +2 -2
- package/runtime/agents/SKILL.md +7 -5
- package/runtime/agents/workers/claude-worker.md +1 -1
- package/runtime/agents/workers/codex-worker.md +23 -6
- package/runtime/agents/workers/gemini-worker.md +23 -6
- package/runtime/agents/workers/report-writer-worker.md +45 -66
- package/runtime/bin/okstra-codex-exec.sh +31 -0
- package/runtime/bin/okstra-gemini-exec.sh +26 -0
- package/runtime/bin/okstra-render-final-report.py +101 -0
- package/runtime/bin/okstra-render-report-views.py +17 -10
- package/runtime/bin/okstra-token-usage.py +3 -1
- package/runtime/python/lib/okstra/globals.sh +1 -1
- package/runtime/python/lib/okstra/usage.sh +2 -2
- package/runtime/python/okstra_ctl/final_report_schema.py +253 -0
- package/runtime/python/okstra_ctl/models.py +2 -0
- package/runtime/python/okstra_ctl/render_final_report.py +201 -0
- package/runtime/python/okstra_ctl/report_views.py +276 -297
- package/runtime/python/okstra_ctl/run.py +1 -1
- package/runtime/python/okstra_ctl/wizard.py +53 -14
- package/runtime/python/okstra_ctl/workers.py +45 -11
- package/runtime/python/okstra_token_usage/__init__.py +5 -1
- package/runtime/python/okstra_token_usage/cli.py +66 -36
- package/runtime/python/okstra_token_usage/pricing.py +1 -0
- package/runtime/python/okstra_token_usage/report.py +148 -65
- package/runtime/python/okstra_vendor/__init__.py +37 -0
- package/runtime/python/okstra_vendor/jinja2/__init__.py +38 -0
- package/runtime/python/okstra_vendor/jinja2/_identifier.py +6 -0
- package/runtime/python/okstra_vendor/jinja2/async_utils.py +99 -0
- package/runtime/python/okstra_vendor/jinja2/bccache.py +408 -0
- package/runtime/python/okstra_vendor/jinja2/compiler.py +1998 -0
- package/runtime/python/okstra_vendor/jinja2/constants.py +20 -0
- package/runtime/python/okstra_vendor/jinja2/debug.py +191 -0
- package/runtime/python/okstra_vendor/jinja2/defaults.py +48 -0
- package/runtime/python/okstra_vendor/jinja2/environment.py +1672 -0
- package/runtime/python/okstra_vendor/jinja2/exceptions.py +166 -0
- package/runtime/python/okstra_vendor/jinja2/ext.py +870 -0
- package/runtime/python/okstra_vendor/jinja2/filters.py +1873 -0
- package/runtime/python/okstra_vendor/jinja2/idtracking.py +318 -0
- package/runtime/python/okstra_vendor/jinja2/lexer.py +868 -0
- package/runtime/python/okstra_vendor/jinja2/loaders.py +693 -0
- package/runtime/python/okstra_vendor/jinja2/meta.py +112 -0
- package/runtime/python/okstra_vendor/jinja2/nativetypes.py +130 -0
- package/runtime/python/okstra_vendor/jinja2/nodes.py +1206 -0
- package/runtime/python/okstra_vendor/jinja2/optimizer.py +48 -0
- package/runtime/python/okstra_vendor/jinja2/parser.py +1049 -0
- package/runtime/python/okstra_vendor/jinja2/py.typed +0 -0
- package/runtime/python/okstra_vendor/jinja2/runtime.py +1062 -0
- package/runtime/python/okstra_vendor/jinja2/sandbox.py +436 -0
- package/runtime/python/okstra_vendor/jinja2/tests.py +256 -0
- package/runtime/python/okstra_vendor/jinja2/utils.py +766 -0
- package/runtime/python/okstra_vendor/jinja2/visitor.py +92 -0
- package/runtime/python/okstra_vendor/markupsafe/__init__.py +396 -0
- package/runtime/python/okstra_vendor/markupsafe/_native.py +8 -0
- package/runtime/python/okstra_vendor/markupsafe/py.typed +0 -0
- package/runtime/schemas/final-report-v1.0.schema.json +1391 -0
- package/runtime/skills/okstra-report-writer/SKILL.md +31 -30
- package/runtime/skills/okstra-run/SKILL.md +6 -4
- package/runtime/skills/okstra-team-contract/SKILL.md +27 -3
- package/runtime/templates/reports/final-report.template.md +370 -405
- package/runtime/templates/reports/report.css +57 -4
- package/runtime/templates/reports/report.js +63 -7
- package/runtime/templates/reports/settings.template.json +1 -0
- package/runtime/validators/lib/fixtures.sh +7 -7
- package/runtime/validators/validate-report-views.py +24 -153
- package/runtime/validators/validate-run.py +102 -19
- package/src/install.mjs +21 -1
|
@@ -549,7 +549,7 @@ def prepare_task_bundle(inp: PrepareInputs) -> PrepareOutputs:
|
|
|
549
549
|
pr_template_source = resolved_tpl.source
|
|
550
550
|
|
|
551
551
|
# ---- model assignments ----
|
|
552
|
-
lead_default = _default("OKSTRA_DEFAULT_LEAD_MODEL", "opus")
|
|
552
|
+
lead_default = _default("OKSTRA_DEFAULT_LEAD_MODEL", "opus-4-6")
|
|
553
553
|
claude_default = _default("OKSTRA_DEFAULT_CLAUDE_MODEL", "sonnet")
|
|
554
554
|
codex_default = _default("OKSTRA_DEFAULT_CODEX_MODEL", "gpt-5.5")
|
|
555
555
|
gemini_default = _default("OKSTRA_DEFAULT_GEMINI_MODEL", "auto")
|
|
@@ -37,6 +37,7 @@ from okstra_ctl.workers import (
|
|
|
37
37
|
ALLOWED_WORKERS,
|
|
38
38
|
WorkersError,
|
|
39
39
|
normalize_workers,
|
|
40
|
+
resolve_optional_workers,
|
|
40
41
|
resolve_profile_workers,
|
|
41
42
|
validate_workers_against_profile,
|
|
42
43
|
)
|
|
@@ -215,6 +216,7 @@ class WizardState:
|
|
|
215
216
|
# task-type + dependents
|
|
216
217
|
task_type: str = ""
|
|
217
218
|
profile_workers: list[str] = field(default_factory=list)
|
|
219
|
+
profile_optional_workers: list[str] = field(default_factory=list)
|
|
218
220
|
|
|
219
221
|
# brief
|
|
220
222
|
keep_existing_brief: Optional[bool] = None
|
|
@@ -278,6 +280,7 @@ class Prompt:
|
|
|
278
280
|
options: list[Option] = field(default_factory=list)
|
|
279
281
|
help: str = ""
|
|
280
282
|
echo_template: str = "" # e.g. "task-group: {value}"
|
|
283
|
+
multi: bool = False # only meaningful when kind == "pick"
|
|
281
284
|
|
|
282
285
|
def to_json(self) -> dict[str, Any]:
|
|
283
286
|
return {
|
|
@@ -287,6 +290,7 @@ class Prompt:
|
|
|
287
290
|
"options": [asdict(o) for o in self.options],
|
|
288
291
|
"help": self.help,
|
|
289
292
|
"echoTemplate": self.echo_template,
|
|
293
|
+
"multi": self.multi,
|
|
290
294
|
}
|
|
291
295
|
|
|
292
296
|
|
|
@@ -401,6 +405,12 @@ def _load_profile_workers(workspace_root: Path, task_type: str) -> list[str]:
|
|
|
401
405
|
return resolve_profile_workers(_profile_path(workspace_root, task_type))
|
|
402
406
|
|
|
403
407
|
|
|
408
|
+
def _load_profile_optional_workers(
|
|
409
|
+
workspace_root: Path, task_type: str
|
|
410
|
+
) -> list[str]:
|
|
411
|
+
return resolve_optional_workers(_profile_path(workspace_root, task_type))
|
|
412
|
+
|
|
413
|
+
|
|
404
414
|
def _resolved_roster(state: WizardState) -> list[str]:
|
|
405
415
|
"""Effective worker list AFTER override. Implementation: profile default
|
|
406
416
|
(caller never asks for override). Others: override or profile default."""
|
|
@@ -628,6 +638,9 @@ def _submit_task_type(state: WizardState, value: str) -> Optional[str]:
|
|
|
628
638
|
state.profile_workers = _load_profile_workers(
|
|
629
639
|
Path(state.workspace_root), value
|
|
630
640
|
)
|
|
641
|
+
state.profile_optional_workers = _load_profile_optional_workers(
|
|
642
|
+
Path(state.workspace_root), value
|
|
643
|
+
)
|
|
631
644
|
# Reuse-worktree is decided once identity is final. Recompute here so
|
|
632
645
|
# subsequent base-ref step knows whether to apply.
|
|
633
646
|
state.reuse_worktree = _resolve_reuse_worktree(state)
|
|
@@ -932,25 +945,43 @@ def _submit_defaults_or_custom(state: WizardState, value: str) -> Optional[str]:
|
|
|
932
945
|
|
|
933
946
|
|
|
934
947
|
def _build_workers_override(state: WizardState) -> Prompt:
|
|
935
|
-
|
|
948
|
+
"""분석 워커 멀티픽. report-writer 는 옵션에서 빼고 항상 결과에 강제
|
|
949
|
+
포함시킨다(프로필이 report-writer 를 Required 로 가질 때)."""
|
|
950
|
+
analyser_choices = [
|
|
951
|
+
w for w in (state.profile_workers + state.profile_optional_workers)
|
|
952
|
+
if w != "report-writer"
|
|
953
|
+
]
|
|
954
|
+
options: list[Option] = []
|
|
955
|
+
for w in analyser_choices:
|
|
956
|
+
is_optional = w in state.profile_optional_workers
|
|
957
|
+
label = f"{w} (옵션)" if is_optional else w
|
|
958
|
+
options.append(_opt(value=w, label=label))
|
|
936
959
|
return Prompt(
|
|
937
|
-
step=S_WORKERS_OVERRIDE, kind="
|
|
938
|
-
label=(
|
|
939
|
-
|
|
940
|
-
|
|
960
|
+
step=S_WORKERS_OVERRIDE, kind="pick", multi=True,
|
|
961
|
+
label=("참여시킬 분석 워커를 선택해주세요 (최소 1개). "
|
|
962
|
+
"report-writer 는 항상 포함됩니다."),
|
|
963
|
+
options=options,
|
|
941
964
|
echo_template="workers: {value}",
|
|
942
965
|
)
|
|
943
966
|
|
|
944
967
|
|
|
945
968
|
def _submit_workers_override(state: WizardState, value: str) -> Optional[str]:
|
|
946
|
-
|
|
947
|
-
state.workers_override = ""
|
|
948
|
-
return f"workers: (profile default: {','.join(state.profile_workers)})"
|
|
969
|
+
raw = (value or "").strip()
|
|
949
970
|
try:
|
|
950
|
-
chosen = normalize_workers(
|
|
951
|
-
|
|
971
|
+
chosen = normalize_workers(raw) if raw else []
|
|
972
|
+
if not chosen:
|
|
973
|
+
raise WizardError("워커를 최소 1개 선택해주세요")
|
|
974
|
+
validate_workers_against_profile(
|
|
975
|
+
chosen,
|
|
976
|
+
state.profile_workers,
|
|
977
|
+
state.profile_optional_workers,
|
|
978
|
+
)
|
|
952
979
|
except WorkersError as exc:
|
|
953
980
|
raise WizardError(str(exc))
|
|
981
|
+
# report-writer 는 프로필이 Required 로 선언했을 때만 강제 포함.
|
|
982
|
+
if ("report-writer" in state.profile_workers
|
|
983
|
+
and "report-writer" not in chosen):
|
|
984
|
+
chosen.append("report-writer")
|
|
954
985
|
state.workers_override = ",".join(chosen)
|
|
955
986
|
return f"workers: {state.workers_override}"
|
|
956
987
|
|
|
@@ -1173,6 +1204,7 @@ STEPS: list[Step] = [
|
|
|
1173
1204
|
build=_build_task_pick, submit=_submit_task_pick,
|
|
1174
1205
|
owns=("is_new_task", "task_group", "task_id", "task_type",
|
|
1175
1206
|
"existing_brief_path", "profile_workers",
|
|
1207
|
+
"profile_optional_workers",
|
|
1176
1208
|
"task_group_suggestion", "task_id_suggestion",
|
|
1177
1209
|
"task_group_pending_text", "task_id_pending_text")),
|
|
1178
1210
|
Step(S_BRIEF_PATH,
|
|
@@ -1228,7 +1260,8 @@ STEPS: list[Step] = [
|
|
|
1228
1260
|
and (s.is_new_task is False or bool(s.task_id))
|
|
1229
1261
|
and S_TASK_TYPE not in s.answered),
|
|
1230
1262
|
build=_build_task_type, submit=_submit_task_type,
|
|
1231
|
-
owns=("task_type", "profile_workers", "
|
|
1263
|
+
owns=("task_type", "profile_workers", "profile_optional_workers",
|
|
1264
|
+
"reuse_worktree")),
|
|
1232
1265
|
Step(S_BRIEF_KEEP,
|
|
1233
1266
|
applies=lambda s: (not s.is_new_task
|
|
1234
1267
|
and bool(s.existing_brief_path)
|
|
@@ -1281,11 +1314,16 @@ STEPS: list[Step] = [
|
|
|
1281
1314
|
and s.use_defaults is None),
|
|
1282
1315
|
build=_build_defaults_or_custom, submit=_submit_defaults_or_custom,
|
|
1283
1316
|
owns=("use_defaults",)),
|
|
1284
|
-
# Customize branch — workers override only when
|
|
1317
|
+
# Customize branch — workers override only when the profile actually
|
|
1318
|
+
# has analyser-candidate workers (required ∪ optional, minus report-writer).
|
|
1285
1319
|
Step(S_WORKERS_OVERRIDE,
|
|
1286
1320
|
applies=lambda s: (s.use_defaults is False
|
|
1287
1321
|
and s.task_type != "implementation"
|
|
1288
|
-
and
|
|
1322
|
+
and any(
|
|
1323
|
+
w != "report-writer"
|
|
1324
|
+
for w in (s.profile_workers
|
|
1325
|
+
+ s.profile_optional_workers)
|
|
1326
|
+
)
|
|
1289
1327
|
and S_WORKERS_OVERRIDE not in s.answered),
|
|
1290
1328
|
build=_build_workers_override, submit=_submit_workers_override,
|
|
1291
1329
|
owns=("workers_override",)),
|
|
@@ -1446,7 +1484,8 @@ _FIELD_DEFAULTS: dict[str, Any] = {
|
|
|
1446
1484
|
"existing_brief_path": "", "task_type": "",
|
|
1447
1485
|
"task_group_suggestion": "", "task_id_suggestion": "",
|
|
1448
1486
|
"task_group_pending_text": False, "task_id_pending_text": False,
|
|
1449
|
-
"profile_workers": [], "
|
|
1487
|
+
"profile_workers": [], "profile_optional_workers": [],
|
|
1488
|
+
"keep_existing_brief": None,
|
|
1450
1489
|
"brief_path": "", "reuse_worktree": None, "base_ref": "",
|
|
1451
1490
|
"base_ref_pending_text": False, "approved_plan_path": "",
|
|
1452
1491
|
"approved_plan_pending_text": False,
|
|
@@ -16,16 +16,20 @@ PROFILE_BULLET_HEADERS = {
|
|
|
16
16
|
"- Reviewers:",
|
|
17
17
|
"- Analysers:",
|
|
18
18
|
}
|
|
19
|
+
PROFILE_BULLET_HEADERS_OPTIONAL = {
|
|
20
|
+
"- Optional workers:",
|
|
21
|
+
"- Optional workers (opt-in via `--workers`):",
|
|
22
|
+
}
|
|
19
23
|
|
|
20
24
|
|
|
21
25
|
class WorkersError(Exception):
|
|
22
26
|
"""invalid worker selection — surface to user."""
|
|
23
27
|
|
|
24
28
|
|
|
25
|
-
def
|
|
26
|
-
"""`
|
|
27
|
-
|
|
28
|
-
|
|
29
|
+
def _resolve_workers_under(profile_path: Path, headers: set[str]) -> list[str]:
|
|
30
|
+
"""Generic parser: collect ` - <id> …` sub-bullets under any of `headers`
|
|
31
|
+
until the next top-level bullet. Returns first token (before any
|
|
32
|
+
` — ` / ` -- ` / whitespace) of each captured line.
|
|
29
33
|
"""
|
|
30
34
|
if not Path(profile_path).is_file():
|
|
31
35
|
return []
|
|
@@ -33,7 +37,7 @@ def resolve_profile_workers(profile_path: Path) -> list[str]:
|
|
|
33
37
|
out: list[str] = []
|
|
34
38
|
for line in Path(profile_path).read_text(encoding="utf-8").splitlines():
|
|
35
39
|
stripped = line.strip()
|
|
36
|
-
if stripped in
|
|
40
|
+
if stripped in headers:
|
|
37
41
|
capturing = True
|
|
38
42
|
continue
|
|
39
43
|
if not capturing:
|
|
@@ -41,13 +45,40 @@ def resolve_profile_workers(profile_path: Path) -> list[str]:
|
|
|
41
45
|
if line.startswith("- "):
|
|
42
46
|
break
|
|
43
47
|
if line.startswith(" - "):
|
|
44
|
-
|
|
48
|
+
body = line[4:].strip()
|
|
49
|
+
token = body.split(" — ", 1)[0].split(" -- ", 1)[0].split()[0]
|
|
50
|
+
out.append(token)
|
|
45
51
|
continue
|
|
46
52
|
if stripped:
|
|
47
53
|
break
|
|
48
54
|
return out
|
|
49
55
|
|
|
50
56
|
|
|
57
|
+
def resolve_profile_workers(profile_path: Path) -> list[str]:
|
|
58
|
+
"""`prompts/profiles/<task-type>.md` 본문의 `- Required workers:` (또는
|
|
59
|
+
`- Workers:` / `- Reviewers:` / `- Analysers:`) 섹션 아래 sub-bullet 들을
|
|
60
|
+
worker id 리스트로 돌려준다. profile 파일이 없거나 섹션이 없으면 빈 리스트.
|
|
61
|
+
"""
|
|
62
|
+
return _resolve_workers_under(profile_path, PROFILE_BULLET_HEADERS)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def resolve_optional_workers(profile_path: Path) -> list[str]:
|
|
66
|
+
r"""`- Optional workers (opt-in via \`--workers\`):` 섹션 아래 sub-bullet
|
|
67
|
+
들에서 worker id 만 추출한다. (` - gemini — when added …` → `gemini`)
|
|
68
|
+
Required 와 중복되는 항목은 제거. ALLOWED_WORKERS 밖 토큰도 제거.
|
|
69
|
+
"""
|
|
70
|
+
raw = _resolve_workers_under(profile_path, PROFILE_BULLET_HEADERS_OPTIONAL)
|
|
71
|
+
required = set(resolve_profile_workers(profile_path))
|
|
72
|
+
seen: set[str] = set()
|
|
73
|
+
out: list[str] = []
|
|
74
|
+
for w in raw:
|
|
75
|
+
if w in required or w in seen or w not in ALLOWED_WORKERS:
|
|
76
|
+
continue
|
|
77
|
+
seen.add(w)
|
|
78
|
+
out.append(w)
|
|
79
|
+
return out
|
|
80
|
+
|
|
81
|
+
|
|
51
82
|
def normalize_workers(value: str) -> list[str]:
|
|
52
83
|
"""CSV 입력을 정규화한다.
|
|
53
84
|
|
|
@@ -71,20 +102,23 @@ def normalize_workers(value: str) -> list[str]:
|
|
|
71
102
|
|
|
72
103
|
|
|
73
104
|
def validate_workers_against_profile(
|
|
74
|
-
workers: list[str],
|
|
105
|
+
workers: list[str],
|
|
106
|
+
profile_workers: list[str],
|
|
107
|
+
optional_workers: list[str] | None = None,
|
|
75
108
|
) -> None:
|
|
76
109
|
"""프로파일이 `Required workers:` 로 로스터를 선언했다면, 사용자
|
|
77
|
-
override 가
|
|
110
|
+
override 가 (required ∪ optional) 의 부분집합인지 검증한다.
|
|
78
111
|
|
|
79
112
|
`profile_workers` 가 비어 있으면(프로파일이 로스터를 선언하지 않은
|
|
80
|
-
구버전) 검증을
|
|
113
|
+
구버전) 검증을 건너뛴다.
|
|
81
114
|
"""
|
|
82
115
|
if not profile_workers:
|
|
83
116
|
return
|
|
84
|
-
allowed = set(profile_workers)
|
|
117
|
+
allowed = set(profile_workers) | set(optional_workers or [])
|
|
85
118
|
extras = [w for w in workers if w not in allowed]
|
|
86
119
|
if extras:
|
|
120
|
+
allow_str = ",".join(profile_workers + list(optional_workers or []))
|
|
87
121
|
raise WorkersError(
|
|
88
122
|
"workers not allowed by profile roster: "
|
|
89
|
-
f"{','.join(extras)} (profile allows: {
|
|
123
|
+
f"{','.join(extras)} (profile allows: {allow_str})"
|
|
90
124
|
)
|
|
@@ -5,25 +5,53 @@ import argparse
|
|
|
5
5
|
import json
|
|
6
6
|
import sys
|
|
7
7
|
from pathlib import Path
|
|
8
|
+
|
|
8
9
|
from .collect import collect
|
|
9
|
-
from .report import
|
|
10
|
+
from .report import (
|
|
11
|
+
SubstituteRefusedError,
|
|
12
|
+
populate_and_render,
|
|
13
|
+
)
|
|
10
14
|
|
|
11
15
|
|
|
12
16
|
def main() -> int:
|
|
13
17
|
parser = argparse.ArgumentParser(description=__doc__)
|
|
14
|
-
parser.add_argument(
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
parser.add_argument(
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
18
|
+
parser.add_argument(
|
|
19
|
+
"team_state",
|
|
20
|
+
type=Path,
|
|
21
|
+
help="Path to team-state JSON file",
|
|
22
|
+
)
|
|
23
|
+
parser.add_argument(
|
|
24
|
+
"--project-root",
|
|
25
|
+
type=Path,
|
|
26
|
+
default=None,
|
|
27
|
+
help="Override project root (default: inferred from team-state path)",
|
|
28
|
+
)
|
|
29
|
+
parser.add_argument(
|
|
30
|
+
"--write",
|
|
31
|
+
action="store_true",
|
|
32
|
+
help=(
|
|
33
|
+
"Write the updated team-state back to the same path "
|
|
34
|
+
"(default: print to stdout)"
|
|
35
|
+
),
|
|
36
|
+
)
|
|
37
|
+
parser.add_argument(
|
|
38
|
+
"--summary",
|
|
39
|
+
action="store_true",
|
|
40
|
+
help="Also print a one-line summary to stderr",
|
|
41
|
+
)
|
|
42
|
+
parser.add_argument(
|
|
43
|
+
"--substitute-data",
|
|
44
|
+
type=Path,
|
|
45
|
+
default=None,
|
|
46
|
+
help=(
|
|
47
|
+
"After collecting usage, populate the final-report data.json "
|
|
48
|
+
"at this path with concrete token / cost / duration values "
|
|
49
|
+
"from the freshly computed usageSummary, then re-render the "
|
|
50
|
+
"sibling final-report markdown via the renderer. The data.json "
|
|
51
|
+
"is the SSOT; the markdown is regenerated from it. "
|
|
52
|
+
"See schemas/final-report-v1.0.schema.json for the data shape."
|
|
53
|
+
),
|
|
54
|
+
)
|
|
27
55
|
args = parser.parse_args()
|
|
28
56
|
|
|
29
57
|
if not args.team_state.is_file():
|
|
@@ -33,7 +61,9 @@ def main() -> int:
|
|
|
33
61
|
updated = collect(args.team_state, args.project_root)
|
|
34
62
|
|
|
35
63
|
if args.write:
|
|
36
|
-
args.team_state.write_text(
|
|
64
|
+
args.team_state.write_text(
|
|
65
|
+
json.dumps(updated, indent=2, ensure_ascii=False) + "\n",
|
|
66
|
+
)
|
|
37
67
|
else:
|
|
38
68
|
json.dump(updated, sys.stdout, indent=2, ensure_ascii=False)
|
|
39
69
|
sys.stdout.write("\n")
|
|
@@ -42,7 +72,8 @@ def main() -> int:
|
|
|
42
72
|
s = updated.get("usageSummary") or {}
|
|
43
73
|
cost = s.get("estimatedCostUsd") or {}
|
|
44
74
|
print(
|
|
45
|
-
f"raw: lead={s.get('leadTotalTokens', 0):,}
|
|
75
|
+
f"raw: lead={s.get('leadTotalTokens', 0):,} "
|
|
76
|
+
f"workers={s.get('workerTotalTokens', 0):,} "
|
|
46
77
|
f"grand={s.get('grandTotalTokens', 0):,}",
|
|
47
78
|
file=sys.stderr,
|
|
48
79
|
)
|
|
@@ -53,36 +84,35 @@ def main() -> int:
|
|
|
53
84
|
file=sys.stderr,
|
|
54
85
|
)
|
|
55
86
|
print(
|
|
56
|
-
f"cost USD: lead=${cost.get('lead', 0):.2f}
|
|
57
|
-
f"
|
|
87
|
+
f"cost USD: lead=${cost.get('lead', 0):.2f} "
|
|
88
|
+
f"claude-workers=${cost.get('claudeWorkers', 0):.2f} "
|
|
89
|
+
f"cli-workers=${cost.get('cliWorkers', 0):.2f} "
|
|
90
|
+
f"grand=${cost.get('grandTotal', 0):.2f}",
|
|
91
|
+
file=sys.stderr,
|
|
92
|
+
)
|
|
93
|
+
print(
|
|
94
|
+
f"sessions={s.get('sessionsFound', 0)} "
|
|
95
|
+
f"team={s.get('teamName', '')}",
|
|
58
96
|
file=sys.stderr,
|
|
59
97
|
)
|
|
60
|
-
print(f"sessions={s.get('sessionsFound', 0)} team={s.get('teamName', '')}", file=sys.stderr)
|
|
61
98
|
|
|
62
|
-
if args.
|
|
99
|
+
if args.substitute_data is not None:
|
|
63
100
|
try:
|
|
64
|
-
|
|
101
|
+
cells_changed, bytes_written = populate_and_render(
|
|
102
|
+
args.substitute_data,
|
|
103
|
+
updated,
|
|
104
|
+
)
|
|
65
105
|
except SubstituteRefusedError as exc:
|
|
66
106
|
print(
|
|
67
107
|
f"final-report substitution REFUSED: {exc}",
|
|
68
108
|
file=sys.stderr,
|
|
69
109
|
)
|
|
70
110
|
return 2
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
elif replaced == 0:
|
|
77
|
-
print(
|
|
78
|
-
f"final-report substitution: no token placeholders found at {args.substitute_final_report}",
|
|
79
|
-
file=sys.stderr,
|
|
80
|
-
)
|
|
81
|
-
else:
|
|
82
|
-
print(
|
|
83
|
-
f"final-report substitution: replaced {replaced} placeholder occurrence(s) in {args.substitute_final_report}",
|
|
84
|
-
file=sys.stderr,
|
|
85
|
-
)
|
|
111
|
+
print(
|
|
112
|
+
f"final-report substitution: populated {cells_changed} cell(s) in "
|
|
113
|
+
f"{args.substitute_data}, re-rendered {bytes_written} bytes of markdown.",
|
|
114
|
+
file=sys.stderr,
|
|
115
|
+
)
|
|
86
116
|
|
|
87
117
|
return 0
|
|
88
118
|
|
|
@@ -47,6 +47,7 @@ CLAUDE_PRICING = {
|
|
|
47
47
|
|
|
48
48
|
# Claude 4 point releases (explicit so future divergence is easy to see).
|
|
49
49
|
"opus-4-7": (5.0, 6.25, 0.50, 25.0), # Opus 4.7 (cache prices derived from ratios)
|
|
50
|
+
"opus-4-6": (5.0, 6.25, 0.50, 25.0), # Opus 4.6 (legacy; pricing matches 4.7 per Anthropic)
|
|
50
51
|
"sonnet-4-6": (3.0, 3.75, 0.30, 15.0), # Sonnet 4.6 (cache prices derived from ratios)
|
|
51
52
|
"haiku-4-5": (1.0, 1.25, 0.10, 5.0), # Haiku 4.5 (cache prices derived from ratios)
|
|
52
53
|
|