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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "okstra",
3
- "version": "0.56.1",
3
+ "version": "0.57.0",
4
4
  "description": "Multi-agent cross-verification orchestrator runtime + Claude Code skills.",
5
5
  "license": "MIT",
6
6
  "author": "devonshin",
@@ -1,5 +1,5 @@
1
1
  {
2
- "package": "0.56.1",
3
- "builtAt": "2026-06-08T16:46:40.339Z",
2
+ "package": "0.57.0",
3
+ "builtAt": "2026-06-08T17:08:29.657Z",
4
4
  "repoRoot": "/home/runner/work/okstra/okstra"
5
5
  }
@@ -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.brief_path = str(p)
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.brief_path = str(p)
1022
- state.brief_path_pending_text = False
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__', or {PICK_TYPE_CUSTOM!r}, "
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.brief_path = str(p)
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 (with `유지 / 변경` for existing tasks),
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,