okstra 0.65.0 → 0.67.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/cli.md +1 -1
- package/package.json +1 -1
- package/runtime/BUILD.json +2 -2
- package/runtime/agents/SKILL.md +1 -1
- package/runtime/prompts/wizard/prompts.ko.json +13 -3
- package/runtime/python/okstra_ctl/models.py +5 -0
- package/runtime/python/okstra_ctl/render_final_report.py +2 -1
- package/runtime/python/okstra_ctl/wizard.py +117 -33
- package/runtime/python/okstra_token_usage/pricing.py +7 -3
- package/runtime/skills/okstra-run/SKILL.md +2 -2
- package/runtime/skills/okstra-schedule/SKILL.md +3 -3
- package/runtime/skills/okstra-team-contract/SKILL.md +1 -1
package/docs/kr/cli.md
CHANGED
|
@@ -296,7 +296,7 @@ scripts/okstra.sh --task-type implementation-planning --workers claude,codex --p
|
|
|
296
296
|
```
|
|
297
297
|
|
|
298
298
|
> 모든 `--*-model` 플래그는 `scripts/okstra_ctl/models.py` 의 provider 별 mapping 에 등록된 alias 만 허용합니다. 등록되지 않은 값은 `UnknownModelError` 로 즉시 거부됩니다 (manifest 의 `modelExecutionValue` 와 실제 실행값 불일치로 인한 contract-violation 을 사전에 차단). 허용값:
|
|
299
|
-
> - Claude (`--lead-model` / `--claude-model` / `--report-writer-model`): `opus`, `opus-4-7`, `claude-opus-4-7`, `opus-4-6`, `claude-opus-4-6`, `sonnet`, `sonnet-4-6`, `claude-sonnet-4-6`, `haiku`, `haiku-4-5`, `claude-haiku-4-5`, `claude-haiku-4-5-20251001`
|
|
299
|
+
> - Claude (`--lead-model` / `--claude-model` / `--report-writer-model`): `fable`, `fable-5`, `claude-fable-5`, `opus`, `opus-4-8`, `claude-opus-4-8`, `opus-4-7`, `claude-opus-4-7`, `opus-4-6`, `claude-opus-4-6`, `sonnet`, `sonnet-4-6`, `claude-sonnet-4-6`, `haiku`, `haiku-4-5`, `claude-haiku-4-5`, `claude-haiku-4-5-20251001`
|
|
300
300
|
> - Codex (`--codex-model`): `gpt-5.5`, `gpt-5.4`, `gpt-5.4-mini`, `gpt-5.3-codex`, `gpt-5.2`, `codex-auto-review`
|
|
301
301
|
> - Gemini (`--gemini-model`): `auto`, `pro`, `gemini-3-flash-preview`, `gemini-3-pro-preview` (그리고 `gemini auto` / `gemini pro` 별칭)
|
|
302
302
|
|
package/package.json
CHANGED
package/runtime/BUILD.json
CHANGED
package/runtime/agents/SKILL.md
CHANGED
|
@@ -225,7 +225,7 @@ Spawn **analysis workers only** in the same turn (Phase 4 in Teams mode; Phase 5
|
|
|
225
225
|
|
|
226
226
|
**Agent `name` on dispatch (BLOCKING — token-usage attribution depends on it).** Every analysis-worker `Agent(...)` call MUST set `name: "<workerId>-worker"` — `name: "claude-worker"` / `name: "codex-worker"` / `name: "gemini-worker"` — exactly as the report-writer dispatch sets `name: "report-writer"` ([okstra-report-writer](./skills/okstra-report-writer/SKILL.md)). The Agent harness records this `name` as `agentName` in the subagent session jsonl, and the Phase 7 token collector matches each worker's session by that `agentName` (`okstra_token_usage/collect.py`). A worker dispatched **without** `name` produces a session with no `agentName`; the collector cannot attribute it and records the worker as `source: "unavailable"` even though the session exists and is team-tagged (observed in `dev-9692` error-analysis: `claude`/`codex` workers dispatched without `name` → both `unavailable`, while the named `report-writer` collected normally). Convergence reverify dispatches keep the prefix (`<workerId>-worker-reverify-r<N>`); implementation executor/verifier variants keep `<workerId>-worker` / `<workerId>-executor`.
|
|
227
227
|
|
|
228
|
-
**Agent `model:` on dispatch (BLOCKING — assignment is otherwise ignored).** The `Claude worker` `Agent(...)` call MUST set `model: "<family token of that role's modelExecutionValue>"` (`opus` / `sonnet` / `haiku`), per [okstra-team-contract](./skills/okstra-team-contract/SKILL.md) "Model Assignment Rules" #3–#4. The claude-worker definition is `model: inherit`, so omitting this parameter makes the worker silently run on the lead's model instead of its manifest assignment — the assigned-vs-actual deviation. `Codex worker` / `Gemini worker` are exempt: their CLI model is applied via the wrapper's own `--model` argument, so leave their Agent `model:` at `inherit` (rule #5).
|
|
228
|
+
**Agent `model:` on dispatch (BLOCKING — assignment is otherwise ignored).** The `Claude worker` `Agent(...)` call MUST set `model: "<family token of that role's modelExecutionValue>"` (`fable` / `opus` / `sonnet` / `haiku`), per [okstra-team-contract](./skills/okstra-team-contract/SKILL.md) "Model Assignment Rules" #3–#4. The claude-worker definition is `model: inherit`, so omitting this parameter makes the worker silently run on the lead's model instead of its manifest assignment — the assigned-vs-actual deviation. `Codex worker` / `Gemini worker` are exempt: their CLI model is applied via the wrapper's own `--model` argument, so leave their Agent `model:` at `inherit` (rule #5).
|
|
229
229
|
|
|
230
230
|
The no-`team_name` fallback (Phase 5) is only legal when team-state's `teamCreate.status` is `"error"` for this run. If `teamCreate` is missing or `attempted: false`, the correct action when an Agent dispatch is rejected for a missing team is to GO BACK to Phase 3 and call `TeamCreate` — never to strip `team_name` and continue.
|
|
231
231
|
|
|
@@ -3,10 +3,10 @@
|
|
|
3
3
|
"locale": "ko",
|
|
4
4
|
"steps": {
|
|
5
5
|
"task_pick": {
|
|
6
|
-
"label": "어느 task?",
|
|
6
|
+
"label": "어느 task? (남은 작업 최신순 추천)",
|
|
7
7
|
"echo_template": "task: {value}",
|
|
8
8
|
"options": {
|
|
9
|
-
"__new__": "
|
|
9
|
+
"__new__": "직접 입력 (새 작업 또는 목록에 없는 task)",
|
|
10
10
|
"_LATEST_SUFFIX": " (latest)"
|
|
11
11
|
}
|
|
12
12
|
},
|
|
@@ -80,9 +80,19 @@
|
|
|
80
80
|
"label": "Task type?",
|
|
81
81
|
"echo_template": "task-type: {value}",
|
|
82
82
|
"options": {
|
|
83
|
-
"_RECOMMENDED_SUFFIX": " (recommended)"
|
|
83
|
+
"_RECOMMENDED_SUFFIX": " (recommended)",
|
|
84
|
+
"_RERUN_SUFFIX": " (현재 phase 재실행)",
|
|
85
|
+
"_NEXT_SUFFIX": " (다음 단계)",
|
|
86
|
+
"__free_input__": "직접 입력"
|
|
87
|
+
},
|
|
88
|
+
"echo_variants": {
|
|
89
|
+
"free_input": "task-type: (직접 입력)"
|
|
84
90
|
}
|
|
85
91
|
},
|
|
92
|
+
"task_type_text": {
|
|
93
|
+
"label": "Task type? (입력 가능: requirements-discovery, improvement-discovery, error-analysis, implementation-planning, implementation, final-verification, release-handoff)",
|
|
94
|
+
"echo_template": "task-type: {value}"
|
|
95
|
+
},
|
|
86
96
|
"brief_keep": {
|
|
87
97
|
"label": "기존 brief 경로 [{existing_brief_path}] 를 유지할까요?",
|
|
88
98
|
"echo_template": "brief: {value}",
|
|
@@ -8,7 +8,12 @@ from __future__ import annotations
|
|
|
8
8
|
from dataclasses import dataclass
|
|
9
9
|
|
|
10
10
|
CLAUDE_MAPPING = {
|
|
11
|
+
"fable": ("fable", "fable"),
|
|
12
|
+
"fable-5": ("fable-5", "claude-fable-5"),
|
|
13
|
+
"claude-fable-5": ("fable-5", "claude-fable-5"),
|
|
11
14
|
"opus": ("opus", "opus"),
|
|
15
|
+
"opus-4-8": ("opus-4-8", "claude-opus-4-8"),
|
|
16
|
+
"claude-opus-4-8": ("opus-4-8", "claude-opus-4-8"),
|
|
12
17
|
"opus-4-7": ("opus-4-7", "claude-opus-4-7"),
|
|
13
18
|
"claude-opus-4-7": ("opus-4-7", "claude-opus-4-7"),
|
|
14
19
|
"opus-4-6": ("opus-4-6", "claude-opus-4-6"),
|
|
@@ -358,7 +358,8 @@ def _enforce_schema(data: dict) -> None:
|
|
|
358
358
|
# 구체버전 계열로 돌았는지" 가독성만 준다. CLI 가 실제 고른 버전과 다를 수
|
|
359
359
|
# 있으나 표시이므로 실행에는 무해하다. 새 버전이 나오면 여기만 갱신한다.
|
|
360
360
|
_DISPLAY_CONCRETE_CLAUDE = {
|
|
361
|
-
"
|
|
361
|
+
"fable": "claude-fable-5",
|
|
362
|
+
"opus": "claude-opus-4-8",
|
|
362
363
|
"sonnet": "claude-sonnet-4-6",
|
|
363
364
|
"haiku": "claude-haiku-4-5",
|
|
364
365
|
}
|
|
@@ -49,6 +49,7 @@ from okstra_ctl.workers import (
|
|
|
49
49
|
resolve_profile_workers,
|
|
50
50
|
validate_workers_against_profile,
|
|
51
51
|
)
|
|
52
|
+
from okstra_ctl.workflow import PHASE_SEQUENCE
|
|
52
53
|
from okstra_ctl import worktree_registry
|
|
53
54
|
from okstra_ctl.worktree import (
|
|
54
55
|
is_git_work_tree,
|
|
@@ -90,13 +91,18 @@ _STAGE_SCOPED_TASK_TYPES = ("implementation", "final-verification")
|
|
|
90
91
|
CANONICAL_BASE_REFS = ["main", "dev", "staging", "preprod", "prod"]
|
|
91
92
|
BASE_REF_FREE_INPUT_TOKEN = "__free_input__"
|
|
92
93
|
|
|
93
|
-
CLAUDE_MODEL_OPTIONS = ["default", "opus", "sonnet", "haiku"]
|
|
94
|
+
CLAUDE_MODEL_OPTIONS = ["default", "fable", "opus", "sonnet", "haiku"]
|
|
94
95
|
CODEX_MODEL_OPTIONS = ["default", "gpt-5.5", "gpt-5.4", "gpt-5.4-mini"]
|
|
95
96
|
GEMINI_MODEL_OPTIONS = ["default", "gemini-3-pro-preview", "gemini-3-flash-preview", "auto"]
|
|
96
97
|
|
|
97
98
|
# special pick value: start a brand-new task
|
|
98
99
|
TASK_PICK_NEW_TOKEN = "__new__"
|
|
99
100
|
|
|
101
|
+
# AskUserQuestion renders at most 4 options per question; the last slot is
|
|
102
|
+
# always reserved for the direct-input option, so recommendation lists from
|
|
103
|
+
# dynamic sources (catalog, manifest) are capped at 3.
|
|
104
|
+
_RECOMMENDATION_CAP = 3
|
|
105
|
+
|
|
100
106
|
# Pick-vs-free-text tokens shared by suggestion-aware prompts.
|
|
101
107
|
PICK_USE_SUGGESTED = "__use_suggested__"
|
|
102
108
|
PICK_TYPE_CUSTOM = "__free_input__"
|
|
@@ -220,6 +226,7 @@ S_TASK_GROUP_TEXT = "task_group_text"
|
|
|
220
226
|
S_TASK_ID = "task_id"
|
|
221
227
|
S_TASK_ID_TEXT = "task_id_text"
|
|
222
228
|
S_TASK_TYPE = "task_type"
|
|
229
|
+
S_TASK_TYPE_TEXT = "task_type_text"
|
|
223
230
|
S_BRIEF_KEEP = "brief_keep"
|
|
224
231
|
S_BRIEF_PATH_PICK = "brief_path_pick"
|
|
225
232
|
S_BRIEF_PATH = "brief_path"
|
|
@@ -788,12 +795,15 @@ def _build_task_pick(state: WizardState) -> Prompt:
|
|
|
788
795
|
latest = read_latest_task(project_root) or {}
|
|
789
796
|
latest_key = latest.get("taskKey") or ""
|
|
790
797
|
latest_suffix = t["options"].get("_LATEST_SUFFIX", "")
|
|
798
|
+
remaining = [e for e in tasks if (e.get("workStatus") or "") != "done"]
|
|
791
799
|
options: list[Option] = []
|
|
792
|
-
for entry in
|
|
800
|
+
for entry in remaining[:_RECOMMENDATION_CAP]:
|
|
793
801
|
key = entry.get("taskKey") or ""
|
|
794
802
|
ttype = entry.get("taskType") or ""
|
|
795
|
-
|
|
796
|
-
|
|
803
|
+
# catalog entries are flat (render_task_catalog_discovery) — there is
|
|
804
|
+
# no nested "workflow" object here, unlike task-manifest.json.
|
|
805
|
+
phase = entry.get("currentPhase") or ttype
|
|
806
|
+
nxt = entry.get("nextRecommendedPhase") or ""
|
|
797
807
|
suffix = latest_suffix if key == latest_key else ""
|
|
798
808
|
label = f"{key} · {phase} · next: {nxt}{suffix}"
|
|
799
809
|
options.append(_opt(value=key, label=label))
|
|
@@ -838,7 +848,9 @@ def _submit_task_pick(state: WizardState, value: str) -> Optional[str]:
|
|
|
838
848
|
return f"task: {value}"
|
|
839
849
|
|
|
840
850
|
|
|
841
|
-
def _suggest_recent_task_groups(
|
|
851
|
+
def _suggest_recent_task_groups(
|
|
852
|
+
state: WizardState, limit: int = _RECOMMENDATION_CAP
|
|
853
|
+
) -> list[str]:
|
|
842
854
|
"""프로젝트 catalog 에서 최근 task-group 후보를 limit 개까지 반환."""
|
|
843
855
|
if not state.project_root:
|
|
844
856
|
return []
|
|
@@ -856,7 +868,9 @@ def _suggest_recent_task_groups(state: WizardState, limit: int = 2) -> list[str]
|
|
|
856
868
|
return seen
|
|
857
869
|
|
|
858
870
|
|
|
859
|
-
def _suggest_recent_task_ids(
|
|
871
|
+
def _suggest_recent_task_ids(
|
|
872
|
+
state: WizardState, limit: int = _RECOMMENDATION_CAP
|
|
873
|
+
) -> list[str]:
|
|
860
874
|
"""현재 task_group 내의 최근 task-id 후보를 limit 개까지 반환."""
|
|
861
875
|
if not state.project_root or not state.task_group:
|
|
862
876
|
return []
|
|
@@ -925,7 +939,7 @@ def _build_task_group(state: WizardState) -> Prompt:
|
|
|
925
939
|
echo_template=t["echo_template"],
|
|
926
940
|
)
|
|
927
941
|
# suggestion 이 없으면 catalog 의 최근 task-group 을 후보로 노출 + 직접 입력
|
|
928
|
-
recent = _suggest_recent_task_groups(state
|
|
942
|
+
recent = _suggest_recent_task_groups(state)
|
|
929
943
|
t = _p(state.workspace_root, "task_group_no_suggestion")
|
|
930
944
|
recent_prefix = t.get("recent_label_prefix", "")
|
|
931
945
|
options: list[Option] = []
|
|
@@ -999,7 +1013,7 @@ def _build_task_id(state: WizardState) -> Prompt:
|
|
|
999
1013
|
label=t["label"], options=options,
|
|
1000
1014
|
echo_template=t["echo_template"],
|
|
1001
1015
|
)
|
|
1002
|
-
recent = _suggest_recent_task_ids(state
|
|
1016
|
+
recent = _suggest_recent_task_ids(state)
|
|
1003
1017
|
t = _p(state.workspace_root, "task_id_no_suggestion")
|
|
1004
1018
|
recent_prefix = t.get("recent_label_prefix", "")
|
|
1005
1019
|
options: list[Option] = []
|
|
@@ -1058,48 +1072,88 @@ def _submit_task_id_text(state: WizardState, value: str) -> Optional[str]:
|
|
|
1058
1072
|
return f"task-id: {state.task_id}"
|
|
1059
1073
|
|
|
1060
1074
|
|
|
1061
|
-
def
|
|
1062
|
-
"""
|
|
1063
|
-
그 기존 manifest 의 nextRecommendedPhase 를 반환한다. 없으면 ''.
|
|
1075
|
+
def _existing_task_workflow(state: WizardState) -> dict:
|
|
1076
|
+
"""현재 task-key 가 이미 존재하면 그 manifest 의 workflow dict 를 반환한다.
|
|
1064
1077
|
|
|
1065
|
-
|
|
1066
|
-
task-group/task-id 를 다시 입력한
|
|
1067
|
-
하는 안전장치."""
|
|
1078
|
+
picker 로 기존 task 를 고른 경우뿐 아니라, new-task 흐름으로 같은
|
|
1079
|
+
task-group/task-id 를 다시 입력한 경우(=사실상 이어가기)에도 직전 phase
|
|
1080
|
+
기반 추천이 끊기지 않게 하는 안전장치. 없으면 {}."""
|
|
1068
1081
|
if not (state.project_id and state.task_group and state.task_id):
|
|
1069
|
-
return
|
|
1082
|
+
return {}
|
|
1070
1083
|
key = f"{state.project_id}:{state.task_group}:{state.task_id}"
|
|
1071
1084
|
root = find_task_root(Path(state.project_root), key)
|
|
1072
1085
|
if root is None:
|
|
1073
|
-
return
|
|
1086
|
+
return {}
|
|
1074
1087
|
workflow = (read_task_manifest(root) or {}).get("workflow") or {}
|
|
1075
|
-
|
|
1076
|
-
|
|
1088
|
+
return workflow if isinstance(workflow, dict) else {}
|
|
1089
|
+
|
|
1090
|
+
|
|
1091
|
+
def _phase_after(task_type: str) -> str:
|
|
1092
|
+
"""라이프사이클(PHASE_SEQUENCE) 상 task_type 바로 다음 단계. 없으면 ''."""
|
|
1093
|
+
try:
|
|
1094
|
+
idx = PHASE_SEQUENCE.index(task_type)
|
|
1095
|
+
except ValueError:
|
|
1096
|
+
return ""
|
|
1097
|
+
return PHASE_SEQUENCE[idx + 1] if idx + 1 < len(PHASE_SEQUENCE) else ""
|
|
1098
|
+
|
|
1099
|
+
|
|
1100
|
+
def _recent_task_types(state: WizardState) -> list[str]:
|
|
1101
|
+
"""catalog 최신순으로 이 프로젝트에서 최근 사용된 task-type 목록(중복 제거)."""
|
|
1102
|
+
if not state.project_root:
|
|
1103
|
+
return []
|
|
1104
|
+
try:
|
|
1105
|
+
tasks = list_project_tasks(Path(state.project_root))
|
|
1106
|
+
except (OSError, StateError):
|
|
1107
|
+
return []
|
|
1108
|
+
out: list[str] = []
|
|
1109
|
+
for entry in tasks:
|
|
1110
|
+
tt = entry.get("taskType") or ""
|
|
1111
|
+
if tt and tt not in out:
|
|
1112
|
+
out.append(tt)
|
|
1113
|
+
return out
|
|
1077
1114
|
|
|
1078
1115
|
|
|
1079
1116
|
def _build_task_type(state: WizardState) -> Prompt:
|
|
1080
1117
|
t = _p(state.workspace_root, "task_type")
|
|
1081
1118
|
recommended_suffix = t["options"].get("_RECOMMENDED_SUFFIX", "")
|
|
1119
|
+
rerun_suffix = t["options"].get("_RERUN_SUFFIX", "")
|
|
1120
|
+
next_suffix = t["options"].get("_NEXT_SUFFIX", "")
|
|
1121
|
+
description_by_type = dict(TASK_TYPES)
|
|
1082
1122
|
options: list[Option] = []
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
options
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1123
|
+
|
|
1124
|
+
def add(task_type: str, suffix: str = "") -> None:
|
|
1125
|
+
if task_type not in description_by_type:
|
|
1126
|
+
return
|
|
1127
|
+
if any(o.value == task_type for o in options):
|
|
1128
|
+
return
|
|
1129
|
+
if len(options) >= _RECOMMENDATION_CAP:
|
|
1130
|
+
return
|
|
1131
|
+
options.append(_opt(task_type, f"{task_type}{suffix}",
|
|
1132
|
+
description_by_type[task_type]))
|
|
1133
|
+
|
|
1134
|
+
workflow = _existing_task_workflow(state)
|
|
1135
|
+
recommended = state.task_type or workflow.get("nextRecommendedPhase") or ""
|
|
1136
|
+
if not recommended and not workflow:
|
|
1137
|
+
recommended = TASK_TYPE_VALUES[0]
|
|
1138
|
+
add(recommended, recommended_suffix)
|
|
1139
|
+
add(workflow.get("currentPhase") or "", rerun_suffix)
|
|
1140
|
+
add(_phase_after(recommended), next_suffix)
|
|
1141
|
+
for tt in _recent_task_types(state):
|
|
1142
|
+
add(tt)
|
|
1143
|
+
for tt in TASK_TYPE_VALUES:
|
|
1144
|
+
add(tt)
|
|
1145
|
+
options.append(_opt(PICK_TYPE_CUSTOM, t["options"][PICK_TYPE_CUSTOM]))
|
|
1095
1146
|
return Prompt(step=S_TASK_TYPE, kind="pick",
|
|
1096
1147
|
label=t["label"], options=options,
|
|
1097
1148
|
echo_template=t["echo_template"])
|
|
1098
1149
|
|
|
1099
1150
|
|
|
1100
|
-
def
|
|
1151
|
+
def _apply_task_type(state: WizardState, value: str) -> str:
|
|
1101
1152
|
if value not in TASK_TYPE_VALUES:
|
|
1102
|
-
raise WizardError(
|
|
1153
|
+
raise WizardError(
|
|
1154
|
+
f"unknown task-type: {value!r} "
|
|
1155
|
+
f"(expected one of: {', '.join(TASK_TYPE_VALUES)})"
|
|
1156
|
+
)
|
|
1103
1157
|
state.task_type = value
|
|
1104
1158
|
state.profile_workers = _load_profile_workers(
|
|
1105
1159
|
Path(state.workspace_root), value
|
|
@@ -1113,6 +1167,27 @@ def _submit_task_type(state: WizardState, value: str) -> Optional[str]:
|
|
|
1113
1167
|
return f"task-type: {value}"
|
|
1114
1168
|
|
|
1115
1169
|
|
|
1170
|
+
def _submit_task_type(state: WizardState, value: str) -> Optional[str]:
|
|
1171
|
+
# PICK_TYPE_CUSTOM leaves task_type empty while S_TASK_TYPE gets marked
|
|
1172
|
+
# answered — that combination is what gates S_TASK_TYPE_TEXT on.
|
|
1173
|
+
if value == PICK_TYPE_CUSTOM:
|
|
1174
|
+
state.task_type = ""
|
|
1175
|
+
t = _p(state.workspace_root, "task_type")
|
|
1176
|
+
return t["echo_variants"]["free_input"]
|
|
1177
|
+
return _apply_task_type(state, value)
|
|
1178
|
+
|
|
1179
|
+
|
|
1180
|
+
def _build_task_type_text(state: WizardState) -> Prompt:
|
|
1181
|
+
t = _p(state.workspace_root, "task_type_text")
|
|
1182
|
+
return Prompt(step=S_TASK_TYPE_TEXT, kind="text",
|
|
1183
|
+
label=t["label"],
|
|
1184
|
+
echo_template=t["echo_template"])
|
|
1185
|
+
|
|
1186
|
+
|
|
1187
|
+
def _submit_task_type_text(state: WizardState, value: str) -> Optional[str]:
|
|
1188
|
+
return _apply_task_type(state, value.strip())
|
|
1189
|
+
|
|
1190
|
+
|
|
1116
1191
|
def _build_brief_keep(state: WizardState) -> Prompt:
|
|
1117
1192
|
t = _p(state.workspace_root, "brief_keep",
|
|
1118
1193
|
existing_brief_path=state.existing_brief_path)
|
|
@@ -2216,6 +2291,7 @@ STEPS: list[Step] = [
|
|
|
2216
2291
|
(s.is_new_task is True and bool(s.task_group))
|
|
2217
2292
|
or (s.is_new_task is False
|
|
2218
2293
|
and S_TASK_TYPE in s.answered
|
|
2294
|
+
and bool(s.task_type)
|
|
2219
2295
|
and (s.keep_existing_brief is False
|
|
2220
2296
|
or not s.existing_brief_path))
|
|
2221
2297
|
)
|
|
@@ -2250,11 +2326,19 @@ STEPS: list[Step] = [
|
|
|
2250
2326
|
build=_build_task_type, submit=_submit_task_type,
|
|
2251
2327
|
owns=("task_type", "profile_workers", "profile_optional_workers",
|
|
2252
2328
|
"reuse_worktree")),
|
|
2329
|
+
Step(S_TASK_TYPE_TEXT,
|
|
2330
|
+
applies=lambda s: (S_TASK_TYPE in s.answered
|
|
2331
|
+
and not s.task_type
|
|
2332
|
+
and S_TASK_TYPE_TEXT not in s.answered),
|
|
2333
|
+
build=_build_task_type_text, submit=_submit_task_type_text,
|
|
2334
|
+
owns=("task_type", "profile_workers", "profile_optional_workers",
|
|
2335
|
+
"reuse_worktree")),
|
|
2253
2336
|
Step(S_BRIEF_KEEP,
|
|
2254
2337
|
applies=lambda s: (not s.is_new_task
|
|
2255
2338
|
and bool(s.existing_brief_path)
|
|
2256
2339
|
and s.keep_existing_brief is None
|
|
2257
|
-
and S_TASK_TYPE in s.answered
|
|
2340
|
+
and S_TASK_TYPE in s.answered
|
|
2341
|
+
and bool(s.task_type)),
|
|
2258
2342
|
build=_build_brief_keep, submit=_submit_brief_keep,
|
|
2259
2343
|
owns=("keep_existing_brief",)),
|
|
2260
2344
|
Step(S_BASE_REF_PICK,
|
|
@@ -4,9 +4,9 @@ Pricing is matched by substring against the model id recorded in the session
|
|
|
4
4
|
transcript, so keys must reflect the *actual* model id form emitted by each
|
|
5
5
|
provider:
|
|
6
6
|
|
|
7
|
-
* Anthropic — `claude-
|
|
8
|
-
`claude-
|
|
9
|
-
`claude-3-haiku-*`.
|
|
7
|
+
* Anthropic — `claude-fable-5*`, `claude-opus-4-*`, `claude-sonnet-4-*`,
|
|
8
|
+
`claude-haiku-4-5-*`, `claude-3-5-sonnet-*`, `claude-3-5-haiku-*`,
|
|
9
|
+
`claude-3-opus-*`, `claude-3-haiku-*`.
|
|
10
10
|
* OpenAI / Codex — `gpt-5*`, `gpt-4o*`, `gpt-4*`.
|
|
11
11
|
* Google / Gemini — `gemini-2.5-pro*`, `gemini-2.5-flash*`, `gemini-2.0-flash*`.
|
|
12
12
|
|
|
@@ -45,7 +45,11 @@ CLAUDE_PRICING = {
|
|
|
45
45
|
"3-sonnet": (3.0, 3.75, 0.30, 15.0), # legacy 3 Sonnet
|
|
46
46
|
"3-haiku": (0.25, 0.30, 0.03, 1.25), # Haiku 3
|
|
47
47
|
|
|
48
|
+
# Claude Fable 5 (tier above Opus).
|
|
49
|
+
"fable-5": (10.0, 12.5, 1.0, 50.0), # Fable 5 (cache prices derived from ratios)
|
|
50
|
+
|
|
48
51
|
# Claude 4 point releases (explicit so future divergence is easy to see).
|
|
52
|
+
"opus-4-8": (5.0, 6.25, 0.50, 25.0), # Opus 4.8 (cache prices derived from ratios)
|
|
49
53
|
"opus-4-7": (5.0, 6.25, 0.50, 25.0), # Opus 4.7 (cache prices derived from ratios)
|
|
50
54
|
"opus-4-6": (5.0, 6.25, 0.50, 25.0), # Opus 4.6 (legacy; pricing matches 4.7 per Anthropic)
|
|
51
55
|
"sonnet-4-6": (3.0, 3.75, 0.30, 15.0), # Sonnet 4.6 (cache prices derived from ratios)
|
|
@@ -118,8 +118,8 @@ Repeat until `next.kind == "done"`:
|
|
|
118
118
|
|
|
119
119
|
That is the entire interactive flow. The wizard handles:
|
|
120
120
|
|
|
121
|
-
- new-vs-existing task split, task-group / task-id slug validation,
|
|
122
|
-
- task-type pick (
|
|
121
|
+
- new-vs-existing task split (남은 작업 — `workStatus != done` — 최신순 3개 추천 + 직접 입력), task-group / task-id slug validation (각각 최근 후보 3개 추천 + 직접 입력),
|
|
122
|
+
- task-type pick (추천 3개 — `nextRecommendedPhase` recommended / 현재 phase 재실행 / 라이프사이클 다음 단계 — + 직접 입력; 직접 입력은 후속 `text` 단계에서 전체 task-type 화이트리스트로 검증),
|
|
123
123
|
- brief path after task-group selection (same-group `.okstra/briefs/<task-group>/**/*.md` candidates first, direct input last; `유지 / 변경` for existing tasks),
|
|
124
124
|
- base-ref pick + git rev-parse validation (skipped when reusing an active worktree),
|
|
125
125
|
- `implementation`-only sub-flow: approved-plan path (frontmatter `approved: true` check) + stage pick (`auto` = 의존성 충족된 가장 빠른 미완료 stage, 또는 특정 stage 번호) + executor pick,
|
|
@@ -56,15 +56,15 @@ Subsequent `okstra <subcmd>` calls self-bootstrap their Python path, so this ski
|
|
|
56
56
|
|
|
57
57
|
This skill performs cross-task synthesis (multi-task classification, dependency reasoning, phase placement, Gantt/timeline assembly) which benefits substantially from Opus-class reasoning. The frontmatter `model: opus` field above instructs supporting Claude Code harness versions to switch automatically; if the harness ignores it, this gate catches the case explicitly.
|
|
58
58
|
|
|
59
|
-
1. Inspect the active session model. The model is shown in the status line, accessible via `/model`, and embedded in the runtime context as the model name (e.g. `claude-opus-4-7`, `claude-sonnet-4-6`, `claude-haiku-4-5-*`).
|
|
60
|
-
2. If the active model is **Opus-class** (`claude-opus-*`): proceed to Step 1.
|
|
59
|
+
1. Inspect the active session model. The model is shown in the status line, accessible via `/model`, and embedded in the runtime context as the model name (e.g. `claude-fable-5`, `claude-opus-4-7`, `claude-sonnet-4-6`, `claude-haiku-4-5-*`).
|
|
60
|
+
2. If the active model is **Opus-class or above** (`claude-opus-*`, `claude-fable-*`): proceed to Step 1.
|
|
61
61
|
3. If the active model is **Sonnet or Haiku-class**: STOP and output the following message verbatim, then wait for user response:
|
|
62
62
|
```
|
|
63
63
|
okstra-schedule는 Opus-class 모델에서 실행하는 것을 권장합니다 (현재: <active-model>).
|
|
64
64
|
/model opus 로 전환 후 다시 호출하시거나, 'sonnet으로 진행' 이라고 명시하시면 그대로 실행합니다.
|
|
65
65
|
```
|
|
66
66
|
4. If the user explicitly insists on the lower model ("sonnet으로 진행", "그대로 진행", "force", or similar): proceed to Step 1, but prepend a single-line warning at the top of the generated schedule file: `> ⚠️ Generated with <model> (not Opus). Cross-task synthesis quality may be reduced.`
|
|
67
|
-
5. Skip this gate ONLY when the harness has clearly enforced `model: opus` from the frontmatter — verifiable by the active model already being Opus-class without manual switching.
|
|
67
|
+
5. Skip this gate ONLY when the harness has clearly enforced `model: opus` from the frontmatter — verifiable by the active model already being Opus-class or above without manual switching.
|
|
68
68
|
|
|
69
69
|
### Step 1: Resolve task-group and collect tasks
|
|
70
70
|
|
|
@@ -38,7 +38,7 @@ okstra tasks are always operated using the `Claude lead` + required worker team
|
|
|
38
38
|
1. `resultContract.requiredWorkerRoles` in `task-manifest.json` (and the lead model metadata) is the canonical source. There is no role-level fallback — a missing assignment is a manifest defect, not a license to invent one.
|
|
39
39
|
2. If `modelExecutionValue` differs from `model`, use `modelExecutionValue` during execution.
|
|
40
40
|
3. **Spawn-time enforcement for in-process Claude subagents (BLOCKING).** `Claude worker` and `Report writer worker` are in-process Claude subagents whose agent definitions declare `model: inherit` (`agents/workers/claude-worker.md`, `agents/workers/report-writer-worker.md`). `inherit` follows the **lead's** runtime model, NOT the role's assignment — so an opus assignment silently runs on a sonnet lead. To make the assignment binding (not merely declared), lead MUST pass an explicit `model:` parameter on every `Agent(...)` dispatch for these two roles, derived from that role's `modelExecutionValue`. The dispatch `model:` parameter overrides the `inherit` frontmatter; the frontmatter remains only as the fallback when no parameter is supplied. Omitting `model:` on a Claude-side dispatch is a contract violation that reproduces the assigned-vs-actual model deviation.
|
|
41
|
-
4. **`modelExecutionValue` → Agent `model:` family token.** The Agent tool's `model` parameter accepts family tokens only — `opus` / `sonnet` / `haiku` (an exact version such as `claude-opus-4-7` is NOT a valid value). Map by prefix: a `modelExecutionValue` of `opus*` / `claude-opus*` → `"opus"`, `sonnet*` / `claude-sonnet*` → `"sonnet"`, `haiku*` / `claude-haiku*` → `"haiku"`. This enforces the assignment at **family granularity** (opus vs sonnet vs haiku); the exact version within a family is still inherited from the lead session and cannot be pinned via this parameter.
|
|
41
|
+
4. **`modelExecutionValue` → Agent `model:` family token.** The Agent tool's `model` parameter accepts family tokens only — `fable` / `opus` / `sonnet` / `haiku` (an exact version such as `claude-opus-4-7` is NOT a valid value). Map by prefix: a `modelExecutionValue` of `fable*` / `claude-fable*` → `"fable"`, `opus*` / `claude-opus*` → `"opus"`, `sonnet*` / `claude-sonnet*` → `"sonnet"`, `haiku*` / `claude-haiku*` → `"haiku"`. This enforces the assignment at **family granularity** (fable vs opus vs sonnet vs haiku); the exact version within a family is still inherited from the lead session and cannot be pinned via this parameter.
|
|
42
42
|
5. **Codex / Gemini wrappers are out of scope for the Agent `model:` rule.** `Codex worker` / `Gemini worker` subagents are Claude wrappers that shell out to an external CLI; the role's `modelExecutionValue` is already applied via the CLI's own `--model <modelExecutionValue>` argument (see `agents/workers/_cli-wrapper-template.md`). The Agent `model:` parameter for these wrappers would only set the wrapper's own orchestration model, not the external CLI's model — leave it at `inherit` and do NOT map it from `modelExecutionValue`.
|
|
43
43
|
|
|
44
44
|
### Dynamic Worker Role Determination
|