okstra 0.25.1 → 0.27.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.kr.md +16 -0
- package/README.md +16 -0
- package/docs/kr/architecture.md +3 -7
- package/docs/kr/cli.md +47 -4
- package/docs/kr/performance-improvement-plan-v2.md +23 -0
- package/docs/kr/performance-improvement-plan.md +22 -0
- package/docs/superpowers/specs/2026-05-15-implementation-plan-verification-design.md +254 -0
- package/package.json +1 -1
- package/runtime/BUILD.json +2 -2
- package/runtime/agents/SKILL.md +30 -2
- package/runtime/bin/okstra.sh +1 -1
- package/runtime/prompts/profiles/_common-contract.md +30 -1
- package/runtime/prompts/profiles/error-analysis.md +12 -0
- package/runtime/prompts/profiles/implementation-planning.md +23 -0
- package/runtime/prompts/profiles/requirements-discovery.md +20 -0
- package/runtime/python/lib/okstra/cli.sh +8 -7
- package/runtime/python/lib/okstra/globals.sh +3 -1
- package/runtime/python/lib/okstra/usage.sh +8 -4
- package/runtime/python/okstra_ctl/render.py +35 -0
- package/runtime/python/okstra_ctl/run.py +27 -6
- package/runtime/python/okstra_ctl/run_context.py +1 -1
- package/runtime/python/okstra_ctl/wizard.py +259 -10
- package/runtime/python/okstra_token_usage/blocks.py +5 -1
- package/runtime/python/okstra_token_usage/claude.py +16 -1
- package/runtime/python/okstra_token_usage/collect.py +17 -3
- package/runtime/python/okstra_token_usage/pricing.py +159 -24
- package/runtime/skills/okstra-brief/SKILL.md +532 -65
- package/runtime/skills/okstra-context-loader/SKILL.md +25 -11
- package/runtime/skills/okstra-convergence/SKILL.md +235 -8
- package/runtime/skills/okstra-history/SKILL.md +68 -37
- package/runtime/skills/okstra-logs/SKILL.md +26 -4
- package/runtime/skills/okstra-report-finder/SKILL.md +49 -22
- package/runtime/skills/okstra-report-writer/SKILL.md +59 -64
- package/runtime/skills/okstra-run/SKILL.md +53 -39
- package/runtime/skills/okstra-schedule/SKILL.md +51 -20
- package/runtime/skills/okstra-setup/SKILL.md +31 -12
- package/runtime/skills/okstra-status/SKILL.md +20 -8
- package/runtime/skills/okstra-team-contract/SKILL.md +27 -15
- package/runtime/skills/okstra-time-summary/SKILL.md +53 -16
- package/runtime/templates/reports/final-report.template.md +34 -0
- package/runtime/templates/reports/settings.template.json +7 -4
- package/runtime/validators/lib/fixtures.sh +10 -2
- package/runtime/validators/lib/validate-assets.sh +50 -24
- package/runtime/validators/validate-brief.py +385 -0
- package/runtime/validators/validate-brief.sh +35 -0
- package/runtime/validators/validate-run.py +71 -0
- package/runtime/validators/validate-workflow.sh +7 -33
- package/src/wizard.mjs +21 -5
|
@@ -84,6 +84,7 @@ S_BRIEF_KEEP = "brief_keep"
|
|
|
84
84
|
S_BRIEF_PATH = "brief_path"
|
|
85
85
|
S_BASE_REF_PICK = "base_ref_pick"
|
|
86
86
|
S_BASE_REF_TEXT = "base_ref_text"
|
|
87
|
+
S_APPROVED_PLAN_PICK = "approved_plan_pick"
|
|
87
88
|
S_APPROVED_PLAN = "approved_plan"
|
|
88
89
|
S_EXECUTOR = "executor"
|
|
89
90
|
S_DEFAULTS_OR_CUSTOM = "defaults_or_custom"
|
|
@@ -94,9 +95,13 @@ S_CLAUDE_MODEL = "claude_model"
|
|
|
94
95
|
S_CODEX_MODEL = "codex_model"
|
|
95
96
|
S_GEMINI_MODEL = "gemini_model"
|
|
96
97
|
S_REPORT_WRITER_MODEL = "report_writer_model"
|
|
98
|
+
S_DIRECTIVE_PICK = "directive_pick"
|
|
97
99
|
S_DIRECTIVE = "directive"
|
|
100
|
+
S_RELATED_TASKS_PICK = "related_tasks_pick"
|
|
98
101
|
S_RELATED_TASKS = "related_tasks"
|
|
102
|
+
S_CLARIFICATION_PICK = "clarification_pick"
|
|
99
103
|
S_CLARIFICATION = "clarification"
|
|
104
|
+
S_PR_TEMPLATE_PICK = "pr_template_pick"
|
|
100
105
|
S_PR_TEMPLATE = "pr_template"
|
|
101
106
|
S_PR_TEMPLATE_SCOPE = "pr_template_scope"
|
|
102
107
|
S_CONFIRM = "confirm"
|
|
@@ -134,6 +139,7 @@ class WizardState:
|
|
|
134
139
|
|
|
135
140
|
# impl extras
|
|
136
141
|
approved_plan_path: str = ""
|
|
142
|
+
approved_plan_pending_text: bool = False
|
|
137
143
|
executor: str = ""
|
|
138
144
|
|
|
139
145
|
# customize
|
|
@@ -145,9 +151,13 @@ class WizardState:
|
|
|
145
151
|
gemini_model: str = ""
|
|
146
152
|
report_writer_model: str = ""
|
|
147
153
|
directive: str = ""
|
|
154
|
+
directive_pending_text: bool = False
|
|
148
155
|
related_tasks_raw: str = ""
|
|
156
|
+
related_tasks_pending_text: bool = False
|
|
149
157
|
clarification_response_path: str = ""
|
|
158
|
+
clarification_pending_text: bool = False
|
|
150
159
|
pr_template_path: str = ""
|
|
160
|
+
pr_template_pending_text: bool = False
|
|
151
161
|
pr_template_scope: str = "" # "once" | "project" | "global"
|
|
152
162
|
|
|
153
163
|
# confirm / edit
|
|
@@ -533,6 +543,81 @@ def _submit_base_ref_text(state: WizardState, value: str) -> Optional[str]:
|
|
|
533
543
|
return f"base-ref: {ref}"
|
|
534
544
|
|
|
535
545
|
|
|
546
|
+
PICK_USE_DEFAULT = "__use_default__"
|
|
547
|
+
PICK_OTHER = "__other__"
|
|
548
|
+
PICK_SKIP = "__skip__"
|
|
549
|
+
PICK_ENTER = "__enter__"
|
|
550
|
+
|
|
551
|
+
|
|
552
|
+
def _latest_implementation_planning_report(state: WizardState) -> Optional[Path]:
|
|
553
|
+
"""Find the latest ``final-report-implementation-planning-<seq>.md`` under
|
|
554
|
+
the current task's runs directory.
|
|
555
|
+
|
|
556
|
+
Returns the path relative to ``project_root`` if found, otherwise ``None``.
|
|
557
|
+
"""
|
|
558
|
+
if not state.task_group or not state.task_id or not state.project_root:
|
|
559
|
+
return None
|
|
560
|
+
base = (Path(state.project_root) / ".project-docs" / "okstra" / "tasks"
|
|
561
|
+
/ slugify_task_segment(state.task_group)
|
|
562
|
+
/ slugify_task_segment(state.task_id)
|
|
563
|
+
/ "runs" / "implementation-planning")
|
|
564
|
+
if not base.is_dir():
|
|
565
|
+
return None
|
|
566
|
+
pat = re.compile(r"^final-report-implementation-planning-(\d+)\.md$")
|
|
567
|
+
best: tuple[int, Path] | None = None
|
|
568
|
+
for run_dir in base.iterdir():
|
|
569
|
+
reports = run_dir / "reports"
|
|
570
|
+
if not reports.is_dir():
|
|
571
|
+
continue
|
|
572
|
+
for child in reports.iterdir():
|
|
573
|
+
m = pat.match(child.name)
|
|
574
|
+
if not m:
|
|
575
|
+
continue
|
|
576
|
+
n = int(m.group(1))
|
|
577
|
+
if best is None or n > best[0]:
|
|
578
|
+
best = (n, child)
|
|
579
|
+
if best is None:
|
|
580
|
+
return None
|
|
581
|
+
try:
|
|
582
|
+
return best[1].relative_to(Path(state.project_root))
|
|
583
|
+
except ValueError:
|
|
584
|
+
return best[1]
|
|
585
|
+
|
|
586
|
+
|
|
587
|
+
def _build_approved_plan_pick(state: WizardState) -> Prompt:
|
|
588
|
+
default = _latest_implementation_planning_report(state)
|
|
589
|
+
options = [
|
|
590
|
+
_opt(PICK_USE_DEFAULT, f"기본 경로 사용: {default}"),
|
|
591
|
+
_opt(PICK_OTHER, "다른 경로 입력"),
|
|
592
|
+
]
|
|
593
|
+
return Prompt(
|
|
594
|
+
step=S_APPROVED_PLAN_PICK, kind="pick",
|
|
595
|
+
label=f"approved final-report 경로 (기본: {default})",
|
|
596
|
+
options=options,
|
|
597
|
+
echo_template="approved-plan(pick): {value}",
|
|
598
|
+
)
|
|
599
|
+
|
|
600
|
+
|
|
601
|
+
def _submit_approved_plan_pick(state: WizardState, value: str) -> Optional[str]:
|
|
602
|
+
if value == PICK_USE_DEFAULT:
|
|
603
|
+
default = _latest_implementation_planning_report(state)
|
|
604
|
+
if default is None:
|
|
605
|
+
raise WizardError(
|
|
606
|
+
"기본 approved-plan 경로를 찾을 수 없습니다. '다른 경로 입력'을 선택하세요."
|
|
607
|
+
)
|
|
608
|
+
p = _validate_approved_plan(str(default), Path(state.project_root))
|
|
609
|
+
state.approved_plan_path = str(p)
|
|
610
|
+
state.approved_plan_pending_text = False
|
|
611
|
+
return f"approved-plan: {p}"
|
|
612
|
+
if value == PICK_OTHER:
|
|
613
|
+
state.approved_plan_pending_text = True
|
|
614
|
+
state.approved_plan_path = ""
|
|
615
|
+
return None
|
|
616
|
+
raise WizardError(
|
|
617
|
+
f"expected '{PICK_USE_DEFAULT}' or '{PICK_OTHER}', got: {value!r}"
|
|
618
|
+
)
|
|
619
|
+
|
|
620
|
+
|
|
536
621
|
def _build_approved_plan(state: WizardState) -> Prompt:
|
|
537
622
|
return Prompt(
|
|
538
623
|
step=S_APPROVED_PLAN, kind="text",
|
|
@@ -544,9 +629,103 @@ def _build_approved_plan(state: WizardState) -> Prompt:
|
|
|
544
629
|
def _submit_approved_plan(state: WizardState, value: str) -> Optional[str]:
|
|
545
630
|
p = _validate_approved_plan(value, Path(state.project_root))
|
|
546
631
|
state.approved_plan_path = str(p)
|
|
632
|
+
state.approved_plan_pending_text = False
|
|
547
633
|
return f"approved-plan: {p}"
|
|
548
634
|
|
|
549
635
|
|
|
636
|
+
def _build_directive_pick(state: WizardState) -> Prompt:
|
|
637
|
+
return Prompt(
|
|
638
|
+
step=S_DIRECTIVE_PICK, kind="pick",
|
|
639
|
+
label="추가 directive 가 있나요?",
|
|
640
|
+
options=[
|
|
641
|
+
_opt(PICK_SKIP, "없음 (건너뛰기)"),
|
|
642
|
+
_opt(PICK_ENTER, "있음 (입력)"),
|
|
643
|
+
],
|
|
644
|
+
echo_template="directive(pick): {value}",
|
|
645
|
+
)
|
|
646
|
+
|
|
647
|
+
|
|
648
|
+
def _submit_directive_pick(state: WizardState, value: str) -> Optional[str]:
|
|
649
|
+
if value == PICK_SKIP:
|
|
650
|
+
state.directive = ""
|
|
651
|
+
state.directive_pending_text = False
|
|
652
|
+
return "directive: (none)"
|
|
653
|
+
if value == PICK_ENTER:
|
|
654
|
+
state.directive_pending_text = True
|
|
655
|
+
return None
|
|
656
|
+
raise WizardError(f"expected '{PICK_SKIP}' or '{PICK_ENTER}', got: {value!r}")
|
|
657
|
+
|
|
658
|
+
|
|
659
|
+
def _build_related_tasks_pick(state: WizardState) -> Prompt:
|
|
660
|
+
return Prompt(
|
|
661
|
+
step=S_RELATED_TASKS_PICK, kind="pick",
|
|
662
|
+
label="관련 task id 목록이 있나요?",
|
|
663
|
+
options=[
|
|
664
|
+
_opt(PICK_SKIP, "없음 (건너뛰기)"),
|
|
665
|
+
_opt(PICK_ENTER, "있음 (입력)"),
|
|
666
|
+
],
|
|
667
|
+
echo_template="related-tasks(pick): {value}",
|
|
668
|
+
)
|
|
669
|
+
|
|
670
|
+
|
|
671
|
+
def _submit_related_tasks_pick(state: WizardState, value: str) -> Optional[str]:
|
|
672
|
+
if value == PICK_SKIP:
|
|
673
|
+
state.related_tasks_raw = ""
|
|
674
|
+
state.related_tasks_pending_text = False
|
|
675
|
+
return "related-tasks: (none)"
|
|
676
|
+
if value == PICK_ENTER:
|
|
677
|
+
state.related_tasks_pending_text = True
|
|
678
|
+
return None
|
|
679
|
+
raise WizardError(f"expected '{PICK_SKIP}' or '{PICK_ENTER}', got: {value!r}")
|
|
680
|
+
|
|
681
|
+
|
|
682
|
+
def _build_clarification_pick(state: WizardState) -> Prompt:
|
|
683
|
+
return Prompt(
|
|
684
|
+
step=S_CLARIFICATION_PICK, kind="pick",
|
|
685
|
+
label="clarification-response 파일 경로가 있나요? (follow-up 시에만)",
|
|
686
|
+
options=[
|
|
687
|
+
_opt(PICK_SKIP, "없음 (건너뛰기)"),
|
|
688
|
+
_opt(PICK_ENTER, "있음 (입력)"),
|
|
689
|
+
],
|
|
690
|
+
echo_template="clarification(pick): {value}",
|
|
691
|
+
)
|
|
692
|
+
|
|
693
|
+
|
|
694
|
+
def _submit_clarification_pick(state: WizardState, value: str) -> Optional[str]:
|
|
695
|
+
if value == PICK_SKIP:
|
|
696
|
+
state.clarification_response_path = ""
|
|
697
|
+
state.clarification_pending_text = False
|
|
698
|
+
return "clarification: (none)"
|
|
699
|
+
if value == PICK_ENTER:
|
|
700
|
+
state.clarification_pending_text = True
|
|
701
|
+
return None
|
|
702
|
+
raise WizardError(f"expected '{PICK_SKIP}' or '{PICK_ENTER}', got: {value!r}")
|
|
703
|
+
|
|
704
|
+
|
|
705
|
+
def _build_pr_template_pick(state: WizardState) -> Prompt:
|
|
706
|
+
return Prompt(
|
|
707
|
+
step=S_PR_TEMPLATE_PICK, kind="pick",
|
|
708
|
+
label="PR 본문 템플릿 경로를 직접 지정할까요?",
|
|
709
|
+
options=[
|
|
710
|
+
_opt(PICK_SKIP, "자동 해석 (project.json → config → 기본)"),
|
|
711
|
+
_opt(PICK_ENTER, "직접 경로 입력 (1회성 override)"),
|
|
712
|
+
],
|
|
713
|
+
echo_template="pr-template(pick): {value}",
|
|
714
|
+
)
|
|
715
|
+
|
|
716
|
+
|
|
717
|
+
def _submit_pr_template_pick(state: WizardState, value: str) -> Optional[str]:
|
|
718
|
+
if value == PICK_SKIP:
|
|
719
|
+
state.pr_template_path = ""
|
|
720
|
+
state.pr_template_scope = ""
|
|
721
|
+
state.pr_template_pending_text = False
|
|
722
|
+
return "pr-template: (auto-resolve)"
|
|
723
|
+
if value == PICK_ENTER:
|
|
724
|
+
state.pr_template_pending_text = True
|
|
725
|
+
return None
|
|
726
|
+
raise WizardError(f"expected '{PICK_SKIP}' or '{PICK_ENTER}', got: {value!r}")
|
|
727
|
+
|
|
728
|
+
|
|
550
729
|
def _build_executor(state: WizardState) -> Prompt:
|
|
551
730
|
options = [_opt(e, e + (" (default)" if e == "claude" else ""))
|
|
552
731
|
for e in EXECUTORS]
|
|
@@ -689,6 +868,7 @@ def _build_directive(state: WizardState) -> Prompt:
|
|
|
689
868
|
|
|
690
869
|
def _submit_directive(state: WizardState, value: str) -> Optional[str]:
|
|
691
870
|
state.directive = (value or "").strip()
|
|
871
|
+
state.directive_pending_text = False
|
|
692
872
|
return f"directive: {state.directive or '(none)'}"
|
|
693
873
|
|
|
694
874
|
|
|
@@ -702,6 +882,7 @@ def _build_related_tasks(state: WizardState) -> Prompt:
|
|
|
702
882
|
|
|
703
883
|
def _submit_related_tasks(state: WizardState, value: str) -> Optional[str]:
|
|
704
884
|
state.related_tasks_raw = (value or "").strip()
|
|
885
|
+
state.related_tasks_pending_text = False
|
|
705
886
|
return f"related-tasks: {state.related_tasks_raw or '(none)'}"
|
|
706
887
|
|
|
707
888
|
|
|
@@ -715,6 +896,7 @@ def _build_clarification(state: WizardState) -> Prompt:
|
|
|
715
896
|
|
|
716
897
|
def _submit_clarification(state: WizardState, value: str) -> Optional[str]:
|
|
717
898
|
val = (value or "").strip()
|
|
899
|
+
state.clarification_pending_text = False
|
|
718
900
|
if not val:
|
|
719
901
|
state.clarification_response_path = ""
|
|
720
902
|
return "clarification: (none)"
|
|
@@ -734,6 +916,7 @@ def _build_pr_template(state: WizardState) -> Prompt:
|
|
|
734
916
|
|
|
735
917
|
def _submit_pr_template(state: WizardState, value: str) -> Optional[str]:
|
|
736
918
|
val = (value or "").strip()
|
|
919
|
+
state.pr_template_pending_text = False
|
|
737
920
|
if not val:
|
|
738
921
|
state.pr_template_path = ""
|
|
739
922
|
state.pr_template_scope = ""
|
|
@@ -860,15 +1043,29 @@ STEPS: list[Step] = [
|
|
|
860
1043
|
applies=lambda s: s.base_ref_pending_text,
|
|
861
1044
|
build=_build_base_ref_text, submit=_submit_base_ref_text,
|
|
862
1045
|
owns=("base_ref", "base_ref_pending_text")),
|
|
1046
|
+
Step(S_APPROVED_PLAN_PICK,
|
|
1047
|
+
applies=lambda s: (s.task_type == "implementation"
|
|
1048
|
+
and not s.approved_plan_path
|
|
1049
|
+
and not s.approved_plan_pending_text
|
|
1050
|
+
and S_APPROVED_PLAN_PICK not in s.answered
|
|
1051
|
+
and bool(s.brief_path)
|
|
1052
|
+
and (s.reuse_worktree is True
|
|
1053
|
+
or S_BASE_REF_PICK in s.answered)
|
|
1054
|
+
and not s.base_ref_pending_text
|
|
1055
|
+
and _latest_implementation_planning_report(s) is not None),
|
|
1056
|
+
build=_build_approved_plan_pick, submit=_submit_approved_plan_pick,
|
|
1057
|
+
owns=("approved_plan_path", "approved_plan_pending_text")),
|
|
863
1058
|
Step(S_APPROVED_PLAN,
|
|
864
1059
|
applies=lambda s: (s.task_type == "implementation"
|
|
865
1060
|
and not s.approved_plan_path
|
|
866
1061
|
and bool(s.brief_path)
|
|
867
1062
|
and (s.reuse_worktree is True
|
|
868
1063
|
or S_BASE_REF_PICK in s.answered)
|
|
869
|
-
and not s.base_ref_pending_text
|
|
1064
|
+
and not s.base_ref_pending_text
|
|
1065
|
+
and (s.approved_plan_pending_text
|
|
1066
|
+
or _latest_implementation_planning_report(s) is None)),
|
|
870
1067
|
build=_build_approved_plan, submit=_submit_approved_plan,
|
|
871
|
-
owns=("approved_plan_path",)),
|
|
1068
|
+
owns=("approved_plan_path", "approved_plan_pending_text")),
|
|
872
1069
|
Step(S_EXECUTOR,
|
|
873
1070
|
applies=lambda s: (s.task_type == "implementation"
|
|
874
1071
|
and bool(s.approved_plan_path)
|
|
@@ -927,27 +1124,52 @@ STEPS: list[Step] = [
|
|
|
927
1124
|
and S_REPORT_WRITER_MODEL not in s.answered),
|
|
928
1125
|
build=_build_report_writer_model, submit=_submit_report_writer_model,
|
|
929
1126
|
owns=("report_writer_model",)),
|
|
1127
|
+
Step(S_DIRECTIVE_PICK,
|
|
1128
|
+
applies=lambda s: (s.use_defaults is False
|
|
1129
|
+
and S_DIRECTIVE_PICK not in s.answered),
|
|
1130
|
+
build=_build_directive_pick, submit=_submit_directive_pick,
|
|
1131
|
+
owns=("directive", "directive_pending_text")),
|
|
930
1132
|
Step(S_DIRECTIVE,
|
|
931
1133
|
applies=lambda s: (s.use_defaults is False
|
|
1134
|
+
and s.directive_pending_text
|
|
932
1135
|
and S_DIRECTIVE not in s.answered),
|
|
933
1136
|
build=_build_directive, submit=_submit_directive,
|
|
934
|
-
owns=("directive",)),
|
|
1137
|
+
owns=("directive", "directive_pending_text")),
|
|
1138
|
+
Step(S_RELATED_TASKS_PICK,
|
|
1139
|
+
applies=lambda s: (s.use_defaults is False
|
|
1140
|
+
and S_RELATED_TASKS_PICK not in s.answered),
|
|
1141
|
+
build=_build_related_tasks_pick, submit=_submit_related_tasks_pick,
|
|
1142
|
+
owns=("related_tasks_raw", "related_tasks_pending_text")),
|
|
935
1143
|
Step(S_RELATED_TASKS,
|
|
936
1144
|
applies=lambda s: (s.use_defaults is False
|
|
1145
|
+
and s.related_tasks_pending_text
|
|
937
1146
|
and S_RELATED_TASKS not in s.answered),
|
|
938
1147
|
build=_build_related_tasks, submit=_submit_related_tasks,
|
|
939
|
-
owns=("related_tasks_raw",)),
|
|
1148
|
+
owns=("related_tasks_raw", "related_tasks_pending_text")),
|
|
1149
|
+
Step(S_CLARIFICATION_PICK,
|
|
1150
|
+
applies=lambda s: (s.use_defaults is False
|
|
1151
|
+
and S_CLARIFICATION_PICK not in s.answered),
|
|
1152
|
+
build=_build_clarification_pick, submit=_submit_clarification_pick,
|
|
1153
|
+
owns=("clarification_response_path", "clarification_pending_text")),
|
|
940
1154
|
Step(S_CLARIFICATION,
|
|
941
1155
|
applies=lambda s: (s.use_defaults is False
|
|
1156
|
+
and s.clarification_pending_text
|
|
942
1157
|
and S_CLARIFICATION not in s.answered),
|
|
943
1158
|
build=_build_clarification, submit=_submit_clarification,
|
|
944
|
-
owns=("clarification_response_path",)),
|
|
1159
|
+
owns=("clarification_response_path", "clarification_pending_text")),
|
|
1160
|
+
Step(S_PR_TEMPLATE_PICK,
|
|
1161
|
+
applies=lambda s: (s.use_defaults is False
|
|
1162
|
+
and s.task_type == "release-handoff"
|
|
1163
|
+
and S_PR_TEMPLATE_PICK not in s.answered),
|
|
1164
|
+
build=_build_pr_template_pick, submit=_submit_pr_template_pick,
|
|
1165
|
+
owns=("pr_template_path", "pr_template_scope", "pr_template_pending_text")),
|
|
945
1166
|
Step(S_PR_TEMPLATE,
|
|
946
1167
|
applies=lambda s: (s.use_defaults is False
|
|
947
1168
|
and s.task_type == "release-handoff"
|
|
1169
|
+
and s.pr_template_pending_text
|
|
948
1170
|
and S_PR_TEMPLATE not in s.answered),
|
|
949
1171
|
build=_build_pr_template, submit=_submit_pr_template,
|
|
950
|
-
owns=("pr_template_path", "pr_template_scope")),
|
|
1172
|
+
owns=("pr_template_path", "pr_template_scope", "pr_template_pending_text")),
|
|
951
1173
|
Step(S_PR_TEMPLATE_SCOPE,
|
|
952
1174
|
applies=lambda s: (s.use_defaults is False
|
|
953
1175
|
and s.task_type == "release-handoff"
|
|
@@ -1021,11 +1243,15 @@ _FIELD_DEFAULTS: dict[str, Any] = {
|
|
|
1021
1243
|
"profile_workers": [], "keep_existing_brief": None,
|
|
1022
1244
|
"brief_path": "", "reuse_worktree": None, "base_ref": "",
|
|
1023
1245
|
"base_ref_pending_text": False, "approved_plan_path": "",
|
|
1246
|
+
"approved_plan_pending_text": False,
|
|
1024
1247
|
"executor": "", "use_defaults": None, "workers_override": "",
|
|
1025
1248
|
"lead_model": "", "claude_model": "", "codex_model": "",
|
|
1026
1249
|
"gemini_model": "", "report_writer_model": "", "directive": "",
|
|
1027
|
-
"
|
|
1028
|
-
"
|
|
1250
|
+
"directive_pending_text": False,
|
|
1251
|
+
"related_tasks_raw": "", "related_tasks_pending_text": False,
|
|
1252
|
+
"clarification_response_path": "", "clarification_pending_text": False,
|
|
1253
|
+
"pr_template_path": "", "pr_template_pending_text": False,
|
|
1254
|
+
"pr_template_scope": "",
|
|
1029
1255
|
"confirmed": None, "edit_target": "",
|
|
1030
1256
|
}
|
|
1031
1257
|
|
|
@@ -1172,7 +1398,7 @@ def _cli(argv: list[str]) -> int:
|
|
|
1172
1398
|
|
|
1173
1399
|
Subcommands:
|
|
1174
1400
|
init --state-file PATH --workspace-root P --project-root P --project-id ID
|
|
1175
|
-
step --state-file PATH
|
|
1401
|
+
step --state-file PATH (--answer VALUE | --no-submit)
|
|
1176
1402
|
render-args --state-file PATH
|
|
1177
1403
|
confirmation --state-file PATH
|
|
1178
1404
|
"""
|
|
@@ -1190,6 +1416,11 @@ def _cli(argv: list[str]) -> int:
|
|
|
1190
1416
|
p_step = sub.add_parser("step")
|
|
1191
1417
|
p_step.add_argument("--state-file", required=True)
|
|
1192
1418
|
p_step.add_argument("--answer", default=None)
|
|
1419
|
+
p_step.add_argument(
|
|
1420
|
+
"--no-submit",
|
|
1421
|
+
action="store_true",
|
|
1422
|
+
help="Fetch the current prompt without submitting an answer.",
|
|
1423
|
+
)
|
|
1193
1424
|
|
|
1194
1425
|
p_render = sub.add_parser("render-args")
|
|
1195
1426
|
p_render.add_argument("--state-file", required=True)
|
|
@@ -1214,8 +1445,26 @@ def _cli(argv: list[str]) -> int:
|
|
|
1214
1445
|
|
|
1215
1446
|
if args.cmd == "step":
|
|
1216
1447
|
state = load_state_file(state_path)
|
|
1448
|
+
if args.no_submit and args.answer is not None:
|
|
1449
|
+
print(json.dumps(
|
|
1450
|
+
{"ok": False, "error": "--no-submit and --answer are mutually exclusive"},
|
|
1451
|
+
ensure_ascii=False, indent=2,
|
|
1452
|
+
))
|
|
1453
|
+
return 2
|
|
1454
|
+
if not args.no_submit and args.answer is None:
|
|
1455
|
+
print(json.dumps(
|
|
1456
|
+
{
|
|
1457
|
+
"ok": False,
|
|
1458
|
+
"error": (
|
|
1459
|
+
"step requires --answer VALUE (use --answer '' to submit an "
|
|
1460
|
+
"empty value, or --no-submit to peek at the current prompt)"
|
|
1461
|
+
),
|
|
1462
|
+
},
|
|
1463
|
+
ensure_ascii=False, indent=2,
|
|
1464
|
+
))
|
|
1465
|
+
return 2
|
|
1217
1466
|
try:
|
|
1218
|
-
if args.
|
|
1467
|
+
if args.no_submit:
|
|
1219
1468
|
result = {"echo": "", "next": next_prompt(state).to_json()}
|
|
1220
1469
|
else:
|
|
1221
1470
|
result = submit(state, args.answer)
|
|
@@ -18,18 +18,21 @@ def usage_block(totals: dict, source: str, note: str | None = None) -> dict:
|
|
|
18
18
|
"source": source,
|
|
19
19
|
"collectedAt": utc_now(),
|
|
20
20
|
}
|
|
21
|
-
for key in ("cacheCreationTokens", "
|
|
21
|
+
for key in ("cacheCreationTokens", "cacheCreation5mTokens", "cacheCreation1hTokens",
|
|
22
|
+
"cacheReadTokens", "cachedInputTokens",
|
|
22
23
|
"reasoningOutputTokens", "cachedTokens", "thoughtsTokens", "toolTokens"):
|
|
23
24
|
if totals.get(key):
|
|
24
25
|
block[key] = totals[key]
|
|
25
26
|
|
|
26
27
|
# Billable-equivalent + cost.
|
|
27
28
|
if source == "claude-jsonl":
|
|
29
|
+
cc_1h = totals.get("cacheCreation1hTokens", 0) or 0
|
|
28
30
|
be = claude_billable_equivalent(
|
|
29
31
|
totals.get("inputTokens", 0) or 0,
|
|
30
32
|
totals.get("cacheCreationTokens", 0) or 0,
|
|
31
33
|
totals.get("cacheReadTokens", 0) or 0,
|
|
32
34
|
totals.get("outputTokens", 0) or 0,
|
|
35
|
+
cache_create_1h_t=cc_1h,
|
|
33
36
|
)
|
|
34
37
|
block["billableEquivalentTokens"] = be
|
|
35
38
|
cost = claude_cost_usd(
|
|
@@ -38,6 +41,7 @@ def usage_block(totals: dict, source: str, note: str | None = None) -> dict:
|
|
|
38
41
|
totals.get("cacheCreationTokens", 0) or 0,
|
|
39
42
|
totals.get("cacheReadTokens", 0) or 0,
|
|
40
43
|
totals.get("outputTokens", 0) or 0,
|
|
44
|
+
cache_create_1h_t=cc_1h,
|
|
41
45
|
)
|
|
42
46
|
if cost is not None:
|
|
43
47
|
block["estimatedCostUsd"] = cost
|
|
@@ -10,6 +10,7 @@ from .paths import claude_project_dir
|
|
|
10
10
|
def claude_session_totals(jsonl_path: Path) -> dict:
|
|
11
11
|
"""Return totals + agentName + assistant model + time window for a Claude session jsonl."""
|
|
12
12
|
input_t = output_t = cache_create_t = cache_read_t = 0
|
|
13
|
+
cache_create_5m_t = cache_create_1h_t = 0
|
|
13
14
|
tool_uses = 0
|
|
14
15
|
agent_name: str | None = None
|
|
15
16
|
model: str | None = None
|
|
@@ -23,8 +24,20 @@ def claude_session_totals(jsonl_path: Path) -> dict:
|
|
|
23
24
|
if usage:
|
|
24
25
|
input_t += usage.get("input_tokens", 0) or 0
|
|
25
26
|
output_t += usage.get("output_tokens", 0) or 0
|
|
26
|
-
|
|
27
|
+
cc_total = usage.get("cache_creation_input_tokens", 0) or 0
|
|
28
|
+
cache_create_t += cc_total
|
|
27
29
|
cache_read_t += usage.get("cache_read_input_tokens", 0) or 0
|
|
30
|
+
# Split into 5m / 1h ephemeral tiers when the API breakdown is
|
|
31
|
+
# present. If only the aggregate is given, attribute all of it to
|
|
32
|
+
# the 5m tier (1.25x — the cheaper assumption, matches prior
|
|
33
|
+
# behavior).
|
|
34
|
+
cc_break = usage.get("cache_creation") or {}
|
|
35
|
+
if isinstance(cc_break, dict) and (cc_break.get("ephemeral_5m_input_tokens") is not None
|
|
36
|
+
or cc_break.get("ephemeral_1h_input_tokens") is not None):
|
|
37
|
+
cache_create_5m_t += cc_break.get("ephemeral_5m_input_tokens", 0) or 0
|
|
38
|
+
cache_create_1h_t += cc_break.get("ephemeral_1h_input_tokens", 0) or 0
|
|
39
|
+
else:
|
|
40
|
+
cache_create_5m_t += cc_total
|
|
28
41
|
if rec.get("type") == "assistant":
|
|
29
42
|
if model is None and msg.get("model"):
|
|
30
43
|
model = msg["model"]
|
|
@@ -51,6 +64,8 @@ def claude_session_totals(jsonl_path: Path) -> dict:
|
|
|
51
64
|
"inputTokens": input_t,
|
|
52
65
|
"outputTokens": output_t,
|
|
53
66
|
"cacheCreationTokens": cache_create_t,
|
|
67
|
+
"cacheCreation5mTokens": cache_create_5m_t,
|
|
68
|
+
"cacheCreation1hTokens": cache_create_1h_t,
|
|
54
69
|
"cacheReadTokens": cache_read_t,
|
|
55
70
|
"toolUses": tool_uses,
|
|
56
71
|
"durationMs": duration_ms,
|
|
@@ -54,14 +54,16 @@ def _aggregate_totals(items: list[dict]) -> dict:
|
|
|
54
54
|
"""
|
|
55
55
|
aggregate: dict = {
|
|
56
56
|
"totalTokens": 0, "inputTokens": 0, "outputTokens": 0,
|
|
57
|
-
"cacheCreationTokens": 0, "
|
|
57
|
+
"cacheCreationTokens": 0, "cacheCreation5mTokens": 0, "cacheCreation1hTokens": 0,
|
|
58
|
+
"cacheReadTokens": 0,
|
|
58
59
|
"toolUses": 0, "durationMs": 0,
|
|
59
60
|
"agentName": None, "model": None,
|
|
60
61
|
"startedAt": None, "endedAt": None,
|
|
61
62
|
}
|
|
62
63
|
for t in items:
|
|
63
64
|
for k in ("totalTokens", "inputTokens", "outputTokens",
|
|
64
|
-
"cacheCreationTokens", "
|
|
65
|
+
"cacheCreationTokens", "cacheCreation5mTokens", "cacheCreation1hTokens",
|
|
66
|
+
"cacheReadTokens", "toolUses"):
|
|
65
67
|
aggregate[k] += t.get(k, 0) or 0
|
|
66
68
|
if aggregate["agentName"] is None and t.get("agentName"):
|
|
67
69
|
aggregate["agentName"] = t["agentName"]
|
|
@@ -210,6 +212,17 @@ def collect(team_state_path: Path, project_root: Path | None = None) -> dict:
|
|
|
210
212
|
worker_billable = sum((w.get("usage") or {}).get("billableEquivalentTokens", 0) or 0 for w in workers)
|
|
211
213
|
worker_cost = sum((w.get("usage") or {}).get("estimatedCostUsd", 0) or 0 for w in workers)
|
|
212
214
|
cli_cost = sum((w.get("usage") or {}).get("cliEstimatedCostUsd", 0) or 0 for w in workers)
|
|
215
|
+
|
|
216
|
+
# Surface models whose pricing lookup failed so the silent-zero case is visible.
|
|
217
|
+
unmatched_models: list[str] = []
|
|
218
|
+
if lead.get("model") and lead.get("estimatedCostUsd") is None and (lead.get("totalTokens") or 0) > 0:
|
|
219
|
+
unmatched_models.append(lead["model"])
|
|
220
|
+
for w in workers:
|
|
221
|
+
u = w.get("usage") or {}
|
|
222
|
+
if u.get("model") and u.get("estimatedCostUsd") is None and (u.get("totalTokens") or 0) > 0:
|
|
223
|
+
unmatched_models.append(u["model"])
|
|
224
|
+
if u.get("cliModel") and u.get("cliEstimatedCostUsd") is None and (u.get("cliTotalTokens") or 0) > 0:
|
|
225
|
+
unmatched_models.append(u["cliModel"])
|
|
213
226
|
state["usageSummary"] = {
|
|
214
227
|
"leadTotalTokens": lead_total,
|
|
215
228
|
"workerTotalTokens": worker_total,
|
|
@@ -226,9 +239,10 @@ def collect(team_state_path: Path, project_root: Path | None = None) -> dict:
|
|
|
226
239
|
"collectedAt": utc_now(),
|
|
227
240
|
"teamName": team_name,
|
|
228
241
|
"sessionsFound": len(claude_sessions),
|
|
242
|
+
"unmatchedModels": sorted(set(unmatched_models)),
|
|
229
243
|
"definitions": {
|
|
230
244
|
"totalTokens": "Sum of input + output + cache_creation + cache_read tokens (raw processed volume; matches Anthropic API breakdown). Cache reads are 95%+ in long sessions.",
|
|
231
|
-
"billableEquivalentTokens": "Tokens normalized to base-input-price units (
|
|
245
|
+
"billableEquivalentTokens": "Tokens normalized to base-input-price units (cache_creation_5m x1.25, cache_creation_1h x2.0, cache_read x0.1, output x5). 5m vs 1h is split from usage.cache_creation when the API breakdown is present; otherwise all cache_creation falls into 5m.",
|
|
232
246
|
"estimatedCostUsd": "USD cost using public list pricing for the model recorded in the session. cliWorkers covers Codex/Gemini CLI calls billed under those providers.",
|
|
233
247
|
},
|
|
234
248
|
}
|