okstra 0.56.1 → 0.58.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/package.json
CHANGED
package/runtime/BUILD.json
CHANGED
|
@@ -95,13 +95,16 @@
|
|
|
95
95
|
}
|
|
96
96
|
},
|
|
97
97
|
"brief_path_pick": {
|
|
98
|
-
"label": "task brief markdown
|
|
98
|
+
"label": "task brief markdown 을 선택해주세요",
|
|
99
99
|
"echo_template": "brief(pick): {value}",
|
|
100
100
|
"options": {
|
|
101
101
|
"__existing__": "기존 brief 사용: {existing}",
|
|
102
102
|
"__standard__": "표준 경로 사용: {standard}",
|
|
103
103
|
"__free_input__": "직접 입력"
|
|
104
104
|
},
|
|
105
|
+
"labels": {
|
|
106
|
+
"brief_candidate": "같은 task-group brief: {path}"
|
|
107
|
+
},
|
|
105
108
|
"errors": {
|
|
106
109
|
"existing_missing": "기존 brief 가 없습니다. 다른 옵션을 선택하세요."
|
|
107
110
|
}
|
|
@@ -233,14 +236,20 @@
|
|
|
233
236
|
"label": "추가 critic 패스를 돌릴까요? (놓친 finding/blocker 를 캐는 검증 패스 — opt-in)",
|
|
234
237
|
"echo_template": "critic: {value}",
|
|
235
238
|
"options": {
|
|
236
|
-
"off": "사용 안 함 (기본·추천)"
|
|
237
|
-
|
|
238
|
-
|
|
239
|
+
"off": "사용 안 함 (기본·추천)"
|
|
240
|
+
},
|
|
241
|
+
"labels": {
|
|
242
|
+
"provider_recommended": "{provider} critic (추천)",
|
|
243
|
+
"provider": "{provider} critic"
|
|
239
244
|
}
|
|
240
245
|
},
|
|
241
246
|
"critic_text": {
|
|
242
|
-
"label": "critic provider 를
|
|
243
|
-
"echo_template": "critic: {value}"
|
|
247
|
+
"label": "critic provider 를 선택하세요",
|
|
248
|
+
"echo_template": "critic: {value}",
|
|
249
|
+
"labels": {
|
|
250
|
+
"provider_recommended": "{provider} critic (추천)",
|
|
251
|
+
"provider": "{provider} critic"
|
|
252
|
+
}
|
|
244
253
|
},
|
|
245
254
|
"defaults_or_custom": {
|
|
246
255
|
"label": "역할별로 어떤 모델을 쓸지 정하는 단계입니다 (참여 워커 구성을 바꾸는 게 아닙니다).\n· 기본값으로 진행 — lead·실행자/워커·report-writer 를 모두 추천 모델로 두고 바로 진행합니다.\n· 커스터마이즈 — 역할별 모델을 직접 고르고, 추가 directive·관련 task 도 지정합니다.",
|
|
@@ -99,6 +99,7 @@ PICK_USE_SUGGESTED = "__use_suggested__"
|
|
|
99
99
|
PICK_TYPE_CUSTOM = "__free_input__"
|
|
100
100
|
_RECENT_PREFIX = "__recent:"
|
|
101
101
|
_REPORT_PREFIX = "__report:"
|
|
102
|
+
_BRIEF_PREFIX = "__brief:"
|
|
102
103
|
|
|
103
104
|
# Lines of `key: value` we pull from a brief markdown frontmatter. The
|
|
104
105
|
# parser is intentionally lightweight (no yaml dep) and tolerant — a
|
|
@@ -176,6 +177,38 @@ def _brief_suggestions(path: Path) -> tuple[str, str]:
|
|
|
176
177
|
return tg, tid
|
|
177
178
|
|
|
178
179
|
|
|
180
|
+
def _project_relative_path(path: Path, project_root: Path) -> str:
|
|
181
|
+
try:
|
|
182
|
+
return str(path.relative_to(project_root))
|
|
183
|
+
except ValueError:
|
|
184
|
+
return str(path)
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def _accept_brief_path(state: WizardState, path: Path) -> None:
|
|
188
|
+
"""Record a validated brief and derive safe identity suggestions.
|
|
189
|
+
|
|
190
|
+
New-task runs now ask for ``task_group`` before the brief so the wizard
|
|
191
|
+
can offer same-group brief candidates. If the selected brief carries a
|
|
192
|
+
conflicting frontmatter ``task-group``, fail fast; otherwise keep using
|
|
193
|
+
the brief's ``brief-id`` as the task-id suggestion.
|
|
194
|
+
"""
|
|
195
|
+
tg_suggestion, tid_suggestion = _brief_suggestions(path)
|
|
196
|
+
if state.task_group and tg_suggestion:
|
|
197
|
+
suggested_group = _slug_or_die(tg_suggestion, "task_group")
|
|
198
|
+
if suggested_group != state.task_group:
|
|
199
|
+
raise WizardError(
|
|
200
|
+
"brief task-group does not match selected task-group: "
|
|
201
|
+
f"{tg_suggestion!r} != {state.task_group!r} ({path})"
|
|
202
|
+
)
|
|
203
|
+
state.brief_path = str(path)
|
|
204
|
+
state.brief_path_pending_text = False
|
|
205
|
+
if state.is_new_task:
|
|
206
|
+
if not state.task_group:
|
|
207
|
+
state.task_group_suggestion = tg_suggestion
|
|
208
|
+
if not state.task_id:
|
|
209
|
+
state.task_id_suggestion = tid_suggestion
|
|
210
|
+
|
|
211
|
+
|
|
179
212
|
# ---- Step IDs ------------------------------------------------------------
|
|
180
213
|
|
|
181
214
|
S_TASK_PICK = "task_pick"
|
|
@@ -771,6 +804,39 @@ def _suggest_recent_task_ids(state: WizardState, limit: int = 2) -> list[str]:
|
|
|
771
804
|
return seen
|
|
772
805
|
|
|
773
806
|
|
|
807
|
+
def _suggest_group_briefs(state: WizardState, limit: int = 6) -> list[str]:
|
|
808
|
+
"""Return recent okstra-brief outputs for the selected task-group.
|
|
809
|
+
|
|
810
|
+
``okstra-brief`` writes to ``.okstra/briefs/<task-group>/**/*.md``. The
|
|
811
|
+
wizard exposes those paths after task-group selection so users can pick a
|
|
812
|
+
generated brief instead of typing its path. Paths are project-relative.
|
|
813
|
+
"""
|
|
814
|
+
if not state.project_root or not state.task_group:
|
|
815
|
+
return []
|
|
816
|
+
project_root = Path(state.project_root)
|
|
817
|
+
root = project_root / ".okstra" / "briefs" / state.task_group
|
|
818
|
+
if not root.is_dir():
|
|
819
|
+
return []
|
|
820
|
+
candidates: list[tuple[float, str]] = []
|
|
821
|
+
for path in root.rglob("*.md"):
|
|
822
|
+
if not path.is_file():
|
|
823
|
+
continue
|
|
824
|
+
tg_suggestion, _ = _brief_suggestions(path)
|
|
825
|
+
if tg_suggestion:
|
|
826
|
+
try:
|
|
827
|
+
if _slug_or_die(tg_suggestion, "task_group") != state.task_group:
|
|
828
|
+
continue
|
|
829
|
+
except WizardError:
|
|
830
|
+
continue
|
|
831
|
+
try:
|
|
832
|
+
mtime = path.stat().st_mtime
|
|
833
|
+
except OSError:
|
|
834
|
+
mtime = 0.0
|
|
835
|
+
candidates.append((mtime, _project_relative_path(path, project_root)))
|
|
836
|
+
candidates.sort(key=lambda item: (-item[0], item[1]))
|
|
837
|
+
return [rel for _, rel in candidates[:limit]]
|
|
838
|
+
|
|
839
|
+
|
|
774
840
|
def _build_task_group(state: WizardState) -> Prompt:
|
|
775
841
|
sugg = state.task_group_suggestion
|
|
776
842
|
if sugg:
|
|
@@ -1000,6 +1066,12 @@ def _build_brief_path_pick(state: WizardState) -> Prompt:
|
|
|
1000
1066
|
if standard and standard != existing:
|
|
1001
1067
|
options.append(_opt("__standard__",
|
|
1002
1068
|
t["options"]["__standard__"].format(standard=standard)))
|
|
1069
|
+
brief_label = t["labels"].get("brief_candidate", "{path}")
|
|
1070
|
+
for relpath in _suggest_group_briefs(state):
|
|
1071
|
+
if relpath in (existing, standard):
|
|
1072
|
+
continue
|
|
1073
|
+
options.append(_opt(f"{_BRIEF_PREFIX}{relpath}",
|
|
1074
|
+
brief_label.format(path=relpath)))
|
|
1003
1075
|
options.append(_opt(PICK_TYPE_CUSTOM, t["options"][PICK_TYPE_CUSTOM]))
|
|
1004
1076
|
return Prompt(step=S_BRIEF_PATH_PICK, kind="pick",
|
|
1005
1077
|
label=t["label"], options=options,
|
|
@@ -1013,20 +1085,24 @@ def _submit_brief_path_pick(state: WizardState, value: str) -> Optional[str]:
|
|
|
1013
1085
|
t = _p(state.workspace_root, "brief_path_pick")
|
|
1014
1086
|
raise WizardError(t["errors"]["existing_missing"])
|
|
1015
1087
|
p = _require_file(existing, Path(state.project_root), "task brief")
|
|
1016
|
-
state
|
|
1017
|
-
state.brief_path_pending_text = False
|
|
1088
|
+
_accept_brief_path(state, p)
|
|
1018
1089
|
return f"brief: {p}"
|
|
1019
1090
|
if value == "__standard__":
|
|
1020
1091
|
p = _require_file(standard, Path(state.project_root), "task brief")
|
|
1021
|
-
state
|
|
1022
|
-
|
|
1092
|
+
_accept_brief_path(state, p)
|
|
1093
|
+
return f"brief: {p}"
|
|
1094
|
+
if value.startswith(_BRIEF_PREFIX):
|
|
1095
|
+
relpath = value[len(_BRIEF_PREFIX):]
|
|
1096
|
+
p = _require_file(relpath, Path(state.project_root), "task brief")
|
|
1097
|
+
_accept_brief_path(state, p)
|
|
1023
1098
|
return f"brief: {p}"
|
|
1024
1099
|
if value == PICK_TYPE_CUSTOM:
|
|
1025
1100
|
state.brief_path_pending_text = True
|
|
1026
1101
|
state.brief_path = ""
|
|
1027
1102
|
return None
|
|
1028
1103
|
raise WizardError(
|
|
1029
|
-
f"expected '__existing__', '__standard__',
|
|
1104
|
+
f"expected '__existing__', '__standard__', '{_BRIEF_PREFIX}<path>', "
|
|
1105
|
+
f"or {PICK_TYPE_CUSTOM!r}, "
|
|
1030
1106
|
f"got: {value!r}"
|
|
1031
1107
|
)
|
|
1032
1108
|
|
|
@@ -1042,16 +1118,7 @@ def _build_brief_path(state: WizardState) -> Prompt:
|
|
|
1042
1118
|
|
|
1043
1119
|
def _submit_brief_path(state: WizardState, value: str) -> Optional[str]:
|
|
1044
1120
|
p = _require_file(value, Path(state.project_root), "task brief")
|
|
1045
|
-
state
|
|
1046
|
-
state.brief_path_pending_text = False
|
|
1047
|
-
# When the user is starting a brand-new task, pull task-group /
|
|
1048
|
-
# task-id candidates from the brief frontmatter so the next two
|
|
1049
|
-
# prompts can offer them as a one-click pick instead of forcing a
|
|
1050
|
-
# free-text retype of what the brief already declares.
|
|
1051
|
-
if state.is_new_task and not state.task_group and not state.task_id:
|
|
1052
|
-
tg, tid = _brief_suggestions(p)
|
|
1053
|
-
state.task_group_suggestion = tg
|
|
1054
|
-
state.task_id_suggestion = tid
|
|
1121
|
+
_accept_brief_path(state, p)
|
|
1055
1122
|
return f"brief: {p}"
|
|
1056
1123
|
|
|
1057
1124
|
|
|
@@ -1505,17 +1572,29 @@ def _submit_pr_template_pick(state: WizardState, value: str) -> Optional[str]:
|
|
|
1505
1572
|
return _submit_optional_cached_pick(state, value, _PR_TEMPLATE_PICK_SPEC)
|
|
1506
1573
|
|
|
1507
1574
|
|
|
1508
|
-
|
|
1575
|
+
def _critic_provider_choices() -> list[str]:
|
|
1576
|
+
return list(EXECUTORS)
|
|
1577
|
+
|
|
1578
|
+
|
|
1579
|
+
def _critic_choices() -> list[str]:
|
|
1580
|
+
return ["off", *_critic_provider_choices()]
|
|
1581
|
+
|
|
1582
|
+
|
|
1583
|
+
def _critic_provider_label(provider: str, t: dict) -> str:
|
|
1584
|
+
labels = t.get("labels", {})
|
|
1585
|
+
if provider == "claude":
|
|
1586
|
+
template = labels.get("provider_recommended", "{provider} critic (recommended)")
|
|
1587
|
+
else:
|
|
1588
|
+
template = labels.get("provider", "{provider} critic")
|
|
1589
|
+
return template.format(provider=provider)
|
|
1509
1590
|
|
|
1510
1591
|
|
|
1511
1592
|
def _build_critic_pick(state: WizardState) -> Prompt:
|
|
1512
1593
|
t = _p(state.workspace_root, "critic_pick")
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
custom_label = t["options"].get(PICK_TYPE_CUSTOM, PICK_TYPE_CUSTOM)
|
|
1518
|
-
options.append(_opt(PICK_TYPE_CUSTOM, custom_label))
|
|
1594
|
+
off_label = t["options"].get("off", "off")
|
|
1595
|
+
options = [_opt("off", off_label)]
|
|
1596
|
+
for provider in _critic_provider_choices():
|
|
1597
|
+
options.append(_opt(provider, _critic_provider_label(provider, t)))
|
|
1519
1598
|
return Prompt(
|
|
1520
1599
|
step=S_CRITIC_PICK, kind="pick",
|
|
1521
1600
|
label=t["label"],
|
|
@@ -1525,12 +1604,10 @@ def _build_critic_pick(state: WizardState) -> Prompt:
|
|
|
1525
1604
|
|
|
1526
1605
|
|
|
1527
1606
|
def _submit_critic_pick(state: WizardState, value: str) -> Optional[str]:
|
|
1528
|
-
if value == PICK_TYPE_CUSTOM:
|
|
1529
|
-
state.critic_pending_text = True
|
|
1530
|
-
return None
|
|
1531
1607
|
choice = (value or "").strip().lower()
|
|
1532
|
-
|
|
1533
|
-
|
|
1608
|
+
choices = _critic_choices()
|
|
1609
|
+
if choice not in choices:
|
|
1610
|
+
raise WizardError(f"critic must be one of {choices}, got: {value!r}")
|
|
1534
1611
|
state.critic = choice
|
|
1535
1612
|
state.critic_pending_text = False
|
|
1536
1613
|
return f"critic: {choice}"
|
|
@@ -1538,17 +1615,23 @@ def _submit_critic_pick(state: WizardState, value: str) -> Optional[str]:
|
|
|
1538
1615
|
|
|
1539
1616
|
def _build_critic_text(state: WizardState) -> Prompt:
|
|
1540
1617
|
t = _p(state.workspace_root, "critic_text")
|
|
1618
|
+
options = [
|
|
1619
|
+
_opt(provider, _critic_provider_label(provider, t))
|
|
1620
|
+
for provider in _critic_provider_choices()
|
|
1621
|
+
]
|
|
1541
1622
|
return Prompt(
|
|
1542
|
-
step=S_CRITIC_TEXT, kind="
|
|
1623
|
+
step=S_CRITIC_TEXT, kind="pick",
|
|
1543
1624
|
label=t["label"],
|
|
1625
|
+
options=options,
|
|
1544
1626
|
echo_template=t["echo_template"],
|
|
1545
1627
|
)
|
|
1546
1628
|
|
|
1547
1629
|
|
|
1548
1630
|
def _submit_critic_text(state: WizardState, value: str) -> Optional[str]:
|
|
1549
1631
|
choice = (value or "").strip().lower()
|
|
1550
|
-
|
|
1551
|
-
|
|
1632
|
+
providers = _critic_provider_choices()
|
|
1633
|
+
if choice not in providers:
|
|
1634
|
+
raise WizardError(f"critic must be one of {providers}, got: {value!r}")
|
|
1552
1635
|
state.critic = choice
|
|
1553
1636
|
state.critic_pending_text = False
|
|
1554
1637
|
return f"critic: {choice}"
|
|
@@ -1907,13 +1990,25 @@ STEPS: list[Step] = [
|
|
|
1907
1990
|
"profile_optional_workers",
|
|
1908
1991
|
"task_group_suggestion", "task_id_suggestion",
|
|
1909
1992
|
"task_group_pending_text", "task_id_pending_text")),
|
|
1993
|
+
Step(S_TASK_GROUP,
|
|
1994
|
+
applies=lambda s: (bool(s.is_new_task)
|
|
1995
|
+
and not s.task_group
|
|
1996
|
+
and not s.task_group_pending_text),
|
|
1997
|
+
build=_build_task_group, submit=_submit_task_group,
|
|
1998
|
+
owns=("task_group", "task_group_pending_text")),
|
|
1999
|
+
Step(S_TASK_GROUP_TEXT,
|
|
2000
|
+
applies=lambda s: (bool(s.is_new_task)
|
|
2001
|
+
and not s.task_group
|
|
2002
|
+
and s.task_group_pending_text),
|
|
2003
|
+
build=_build_task_group_text, submit=_submit_task_group_text,
|
|
2004
|
+
owns=("task_group", "task_group_pending_text")),
|
|
1910
2005
|
Step(S_BRIEF_PATH_PICK,
|
|
1911
2006
|
applies=lambda s: (
|
|
1912
2007
|
not s.brief_path
|
|
1913
2008
|
and not s.brief_path_pending_text
|
|
1914
2009
|
and S_BRIEF_PATH_PICK not in s.answered
|
|
1915
2010
|
and (
|
|
1916
|
-
(s.is_new_task is True)
|
|
2011
|
+
(s.is_new_task is True and bool(s.task_group))
|
|
1917
2012
|
or (s.is_new_task is False
|
|
1918
2013
|
and S_TASK_TYPE in s.answered
|
|
1919
2014
|
and (s.keep_existing_brief is False
|
|
@@ -1927,20 +2022,6 @@ STEPS: list[Step] = [
|
|
|
1927
2022
|
and S_BRIEF_PATH not in s.answered,
|
|
1928
2023
|
build=_build_brief_path, submit=_submit_brief_path,
|
|
1929
2024
|
owns=("brief_path", "task_group_suggestion", "task_id_suggestion")),
|
|
1930
|
-
Step(S_TASK_GROUP,
|
|
1931
|
-
applies=lambda s: (bool(s.is_new_task)
|
|
1932
|
-
and bool(s.brief_path)
|
|
1933
|
-
and not s.task_group
|
|
1934
|
-
and not s.task_group_pending_text),
|
|
1935
|
-
build=_build_task_group, submit=_submit_task_group,
|
|
1936
|
-
owns=("task_group", "task_group_pending_text")),
|
|
1937
|
-
Step(S_TASK_GROUP_TEXT,
|
|
1938
|
-
applies=lambda s: (bool(s.is_new_task)
|
|
1939
|
-
and bool(s.brief_path)
|
|
1940
|
-
and not s.task_group
|
|
1941
|
-
and s.task_group_pending_text),
|
|
1942
|
-
build=_build_task_group_text, submit=_submit_task_group_text,
|
|
1943
|
-
owns=("task_group", "task_group_pending_text")),
|
|
1944
2025
|
Step(S_TASK_ID,
|
|
1945
2026
|
applies=lambda s: (bool(s.is_new_task)
|
|
1946
2027
|
and bool(s.brief_path)
|
|
@@ -120,7 +120,7 @@ That is the entire interactive flow. The wizard handles:
|
|
|
120
120
|
|
|
121
121
|
- new-vs-existing task split, task-group / task-id slug validation,
|
|
122
122
|
- task-type pick (with `nextRecommendedPhase` surfaced as recommended for existing tasks),
|
|
123
|
-
- brief path (
|
|
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,
|
|
126
126
|
- `Use defaults / Customize` branch with profile-aware worker/model questions,
|