okstra 0.165.0 → 0.165.2

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.165.0",
3
+ "version": "0.165.2",
4
4
  "description": "Host-aware multi-provider cross-verification orchestrator runtime and agent skills.",
5
5
  "license": "MIT",
6
6
  "author": "devonshin",
@@ -1,5 +1,5 @@
1
1
  {
2
- "package": "0.165.0",
3
- "builtAt": "2026-08-11T04:29:41.048Z",
2
+ "package": "0.165.2",
3
+ "builtAt": "2026-08-11T05:13:20.960Z",
4
4
  "repoRoot": "/home/runner/work/okstra/okstra"
5
5
  }
@@ -112,9 +112,26 @@ def _data_to_report_path(data_path: Path) -> Path:
112
112
  return final_report_markdown_path(data_path)
113
113
 
114
114
 
115
+ def _project_id_of(task_key: str) -> str:
116
+ """`project-id:task-group:task-id` 의 첫 세그먼트, 아니면 "".
117
+
118
+ A follow-up belongs to its parent's project by construction, so the new
119
+ task-key inherits that segment rather than re-deriving it from disk.
120
+ Omitting it is not a cosmetic difference: `okstra_project.state.
121
+ parse_task_key` raises on a two-segment key, and every catalog reader goes
122
+ through it, so one `<group>/<id>` entry took down `list_project_tasks` for
123
+ the whole project — the wizard could not render its first screen.
124
+ """
125
+ parts = (task_key or "").split(":")
126
+ if len(parts) != 3 or not all(parts):
127
+ return ""
128
+ return parts[0]
129
+
130
+
115
131
  def _spawn_one(
116
132
  *,
117
133
  project_root: Path,
134
+ project_id: str,
118
135
  task_group: str,
119
136
  parent_task_key: str,
120
137
  parent_report_relative: str,
@@ -151,7 +168,7 @@ def _spawn_one(
151
168
  origin = row["origin"].strip()
152
169
  priority = (row.get("priority") or "P1").strip()
153
170
  ticket_id = (row.get("ticketId") or "").strip()
154
- new_task_key = f"{task_group}/{new_task_id}"
171
+ new_task_key = f"{project_id}:{task_group}:{new_task_id}"
155
172
  now = dt.datetime.now(dt.timezone.utc).isoformat()
156
173
 
157
174
  spawned_meta: dict = {
@@ -257,6 +274,16 @@ def main(argv: list[str]) -> int:
257
274
  print(f"data.json not found: {args.data_file}", file=sys.stderr)
258
275
  return 1
259
276
 
277
+ project_id = _project_id_of(args.parent_task_key)
278
+ if not project_id:
279
+ print(
280
+ f"--parent-task-key must be project-id:task-group:task-id, got "
281
+ f"{args.parent_task_key!r} — a spawned task-key derived from it "
282
+ "would break every catalog read in this project.",
283
+ file=sys.stderr,
284
+ )
285
+ return 1
286
+
260
287
  try:
261
288
  data = json.loads(args.data_file.read_text(encoding="utf-8"))
262
289
  except json.JSONDecodeError as exc:
@@ -297,6 +324,7 @@ def main(argv: list[str]) -> int:
297
324
  continue
298
325
  status, info = _spawn_one(
299
326
  project_root=args.project_root,
327
+ project_id=project_id,
300
328
  task_group=args.task_group,
301
329
  parent_task_key=args.parent_task_key,
302
330
  parent_report_relative=parent_report_relative,
@@ -4658,10 +4658,16 @@ def _sim_advance(state: WizardState, prompt: Prompt) -> None:
4658
4658
  STEP_BY_ID[prompt.step].submit(state, _sim_answer(prompt))
4659
4659
  if prompt.step not in state.answered:
4660
4660
  state.answered.append(prompt.step)
4661
- except WizardError:
4662
- # 추천 기본답이 검증에서 막히는 드문 text 분기(WizardError)만 전진 처리한다.
4663
- # KeyError/AttributeError 실제 버그는 삼키지 않고 그대로 전파시켜
4664
- # progress 라벨이 그럴듯하게 틀리는 대신 테스트/실행에서 시끄럽게 실패한다.
4661
+ except (WizardError, PrepareError):
4662
+ # 기본답이 거부되는 경우만 전진 처리한다 입력 검증이 막는 드문 text
4663
+ # 분기(WizardError), 그리고 자체는 유효하나 대상의 도메인 상태가 그
4664
+ # 경로를 막는 경우(PrepareError). 후자가 빠져 있어 승인 게이트가
4665
+ # `blocked-by-disagreement` 인 계획이 최신 태스크이면 위저드가 첫 화면조차
4666
+ # 내지 못하고 죽었다: 시뮬레이터가 기본 경로를 따라가다 그 계획을 고르고,
4667
+ # 진행률 라벨 하나 때문에 run 전체가 시작 불가가 됐다. 시뮬레이션은 분모
4668
+ # 추정이므로 막힌 경로를 만나면 그 화면을 지났다고 치고 계속 세면 된다.
4669
+ # KeyError/AttributeError 등 실제 버그는 여전히 삼키지 않고 그대로
4670
+ # 전파시켜, progress 라벨이 그럴듯하게 틀리는 대신 시끄럽게 실패한다.
4665
4671
  members = prompt.questions if prompt.kind == "pick_group" else [prompt]
4666
4672
  for p in members:
4667
4673
  if p.step not in state.answered:
@@ -3983,6 +3983,29 @@ def _validate_gate_blocked_by(data: dict, failures: list[str]) -> None:
3983
3983
  )
3984
3984
  return
3985
3985
 
3986
+ if not actual_causes and _PLAN_GATE_RANK.get(declared_gate) == 0:
3987
+ # A blocking value with nothing left blocking it. The two checks around
3988
+ # this one both walk from a recorded cause outward, so a gate that
3989
+ # simply stopped being updated fell between them: a self-fix loop
3990
+ # resolved every majority-disagree item, `gateBlockedBy` emptied
3991
+ # correctly, and the gate token stayed at its round-1 value. The plan
3992
+ # was approvable and nothing said so — run-prep refused the approval,
3993
+ # and the refusal propagated far enough to take the run wizard down
3994
+ # with it, so no run could be started in that project at all. Rescoring
3995
+ # with `okstra plan-verify` and recording what it returns is the fix;
3996
+ # the round is not complete until that call agrees with the report.
3997
+ failures.append(
3998
+ "final-report data.json: implementationPlanning.planBodyVerification "
3999
+ f"`gateResult` is `{declared_gate}` but nothing blocks approval — "
4000
+ "no plan item is `majority-disagree`, no dispatch was a non-result, "
4001
+ "and no Requirement Coverage row blocks independently. A gate that "
4002
+ "withholds approval with no recorded cause is almost always a value "
4003
+ "left behind by an earlier round: rescore with `okstra plan-verify` "
4004
+ "and record its `gate.recomputed` "
4005
+ '(plan-body-verification.md §"Round protocol" step 5).'
4006
+ )
4007
+ return
4008
+
3986
4009
  if declared_causes != actual_causes:
3987
4010
  failures.append(
3988
4011
  "final-report data.json: implementationPlanning.planBodyVerification "