okstra 0.56.1 → 0.57.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
|
}
|
|
@@ -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
|
|
|
@@ -1907,13 +1974,25 @@ STEPS: list[Step] = [
|
|
|
1907
1974
|
"profile_optional_workers",
|
|
1908
1975
|
"task_group_suggestion", "task_id_suggestion",
|
|
1909
1976
|
"task_group_pending_text", "task_id_pending_text")),
|
|
1977
|
+
Step(S_TASK_GROUP,
|
|
1978
|
+
applies=lambda s: (bool(s.is_new_task)
|
|
1979
|
+
and not s.task_group
|
|
1980
|
+
and not s.task_group_pending_text),
|
|
1981
|
+
build=_build_task_group, submit=_submit_task_group,
|
|
1982
|
+
owns=("task_group", "task_group_pending_text")),
|
|
1983
|
+
Step(S_TASK_GROUP_TEXT,
|
|
1984
|
+
applies=lambda s: (bool(s.is_new_task)
|
|
1985
|
+
and not s.task_group
|
|
1986
|
+
and s.task_group_pending_text),
|
|
1987
|
+
build=_build_task_group_text, submit=_submit_task_group_text,
|
|
1988
|
+
owns=("task_group", "task_group_pending_text")),
|
|
1910
1989
|
Step(S_BRIEF_PATH_PICK,
|
|
1911
1990
|
applies=lambda s: (
|
|
1912
1991
|
not s.brief_path
|
|
1913
1992
|
and not s.brief_path_pending_text
|
|
1914
1993
|
and S_BRIEF_PATH_PICK not in s.answered
|
|
1915
1994
|
and (
|
|
1916
|
-
(s.is_new_task is True)
|
|
1995
|
+
(s.is_new_task is True and bool(s.task_group))
|
|
1917
1996
|
or (s.is_new_task is False
|
|
1918
1997
|
and S_TASK_TYPE in s.answered
|
|
1919
1998
|
and (s.keep_existing_brief is False
|
|
@@ -1927,20 +2006,6 @@ STEPS: list[Step] = [
|
|
|
1927
2006
|
and S_BRIEF_PATH not in s.answered,
|
|
1928
2007
|
build=_build_brief_path, submit=_submit_brief_path,
|
|
1929
2008
|
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
2009
|
Step(S_TASK_ID,
|
|
1945
2010
|
applies=lambda s: (bool(s.is_new_task)
|
|
1946
2011
|
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,
|