okstra 0.205.1 → 0.206.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.
Files changed (50) hide show
  1. package/dist/commands/lifecycle/install.mjs +1 -0
  2. package/dist/commands/lifecycle/install.mjs.map +1 -1
  3. package/docs/architecture.md +6 -6
  4. package/docs/contributor-change-matrix.md +2 -2
  5. package/docs/project-structure-overview.md +8 -5
  6. package/package.json +2 -3
  7. package/runtime/BUILD.json +2 -2
  8. package/runtime/prompts/lead/okstra-lead-contract.md +1 -1
  9. package/runtime/prompts/lead/phase-routing.md +18 -0
  10. package/runtime/python/okstra_ctl/contract_graph.py +75 -10
  11. package/runtime/python/okstra_ctl/doctor.py +13 -3
  12. package/runtime/python/okstra_ctl/implementation_direction.py +9 -8
  13. package/runtime/python/okstra_ctl/next_phase.py +2 -2
  14. package/runtime/python/okstra_ctl/paths.py +14 -0
  15. package/runtime/python/okstra_ctl/phases/__init__.py +4 -0
  16. package/runtime/python/okstra_ctl/phases/catalog.py +216 -0
  17. package/runtime/python/okstra_ctl/phases/final_verification/__init__.py +4 -0
  18. package/runtime/python/okstra_ctl/phases/final_verification/entry.py +166 -0
  19. package/runtime/{prompts/profiles/final-verification.md → python/okstra_ctl/phases/final_verification/profile.md} +4 -4
  20. package/runtime/python/okstra_ctl/{report_html/view_models/final_verification.py → phases/final_verification/report.py} +12 -3
  21. package/{docs/task-process/final-verification.md → runtime/python/okstra_ctl/phases/final_verification/spec.md} +42 -25
  22. package/runtime/python/okstra_ctl/phases/final_verification/target.py +296 -0
  23. package/runtime/python/okstra_ctl/phases/final_verification/validation.py +190 -0
  24. package/runtime/python/okstra_ctl/phases/final_verification/wizard.py +38 -0
  25. package/runtime/python/okstra_ctl/profile_show.py +7 -1
  26. package/runtime/python/okstra_ctl/render_final_report.py +3 -2
  27. package/runtime/python/okstra_ctl/report_assembly.py +3 -3
  28. package/runtime/python/okstra_ctl/report_html/render.py +3 -2
  29. package/runtime/python/okstra_ctl/report_html/router.py +9 -33
  30. package/runtime/python/okstra_ctl/report_template_loader.py +35 -0
  31. package/runtime/python/okstra_ctl/report_views.py +17 -1
  32. package/runtime/python/okstra_ctl/run.py +46 -154
  33. package/runtime/python/okstra_ctl/stage_targets.py +9 -286
  34. package/runtime/python/okstra_ctl/user_response.py +199 -4
  35. package/runtime/python/okstra_ctl/verification_target.py +1 -1
  36. package/runtime/python/okstra_ctl/wizard/state.py +6 -2
  37. package/runtime/python/okstra_ctl/wizard/steps_plan.py +17 -15
  38. package/runtime/skills/okstra-user-response/SKILL.md +23 -4
  39. package/runtime/validators/validate-run.py +22 -185
  40. package/docs/task-process/README.md +0 -82
  41. package/docs/task-process/common-flow.md +0 -173
  42. package/docs/task-process/error-analysis.md +0 -103
  43. package/docs/task-process/implementation-option-selection.md +0 -70
  44. package/docs/task-process/implementation-planning.md +0 -180
  45. package/docs/task-process/implementation.md +0 -226
  46. package/docs/task-process/release-handoff.md +0 -220
  47. package/docs/task-process/requirements-discovery.md +0 -113
  48. /package/runtime/{prompts/profiles/final-verification.json → python/okstra_ctl/phases/final_verification/profile.json} +0 -0
  49. /package/runtime/{templates/reports/html/tasks → python/okstra_ctl/phases/final_verification/report_assets}/final-verification.template.html +0 -0
  50. /package/runtime/{templates/reports/md/tasks → python/okstra_ctl/phases/final_verification/report_assets}/final-verification.template.md +0 -0
@@ -1,6 +1,6 @@
1
1
  """Read the prepared final-verification target snapshot.
2
2
 
3
- `run.write_verification_target_snapshot` writes it; two consumers read it —
3
+ `phases.final_verification.entry.write_verification_target_snapshot` writes it; two consumers read it —
4
4
  report assembly, which records the run's `verificationScope`, and `validate-run`,
5
5
  which re-checks the published report against it. The digest rule lived only in
6
6
  the validator, so a second reader would have been a second implementation of it;
@@ -14,6 +14,7 @@ from okstra_ctl.role_requirements import (
14
14
  load_role_profile,
15
15
  )
16
16
  from okstra_ctl.ids import slugify_task_segment
17
+ from okstra_ctl.phases.catalog import PhaseAssetError, profile_markdown
17
18
 
18
19
  from .ids import (
19
20
  PICK_TYPE_CUSTOM,
@@ -307,7 +308,10 @@ def _slug_or_die(value: str, field_name: str) -> str:
307
308
  # ---- Roster / profile helpers -------------------------------------------
308
309
 
309
310
  def _profile_path(workspace_root: Path, task_type: str) -> Path:
310
- return workspace_root / "prompts" / "profiles" / f"{task_type}.md"
311
+ try:
312
+ return profile_markdown(workspace_root, task_type)
313
+ except PhaseAssetError as exc:
314
+ raise WizardError(str(exc)) from exc
311
315
 
312
316
 
313
317
  _V2_STATE_FIELDS = (
@@ -366,7 +370,7 @@ def _role_selection_enabled(state: WizardState) -> bool:
366
370
  return False
367
371
  try:
368
372
  load_role_profile(_profile_path(Path(state.workspace_root), state.task_type))
369
- except (OSError, RoleProfileError):
373
+ except (OSError, RoleProfileError, WizardError):
370
374
  return False
371
375
  return True
372
376
 
@@ -29,6 +29,11 @@ from okstra_ctl.user_response import PlanDecisionRecord, parse_plan_decision
29
29
  from okstra_ctl.wizard_stage_intent import WHOLE_TASK_STAGE
30
30
  from okstra_ctl import fix_cycles
31
31
  from okstra_ctl.paths import task_dir, task_runs_dir
32
+ from okstra_ctl.phases.final_verification.wizard import (
33
+ StageAnswerError,
34
+ validate_stage_answer,
35
+ whole_task_verification_allowed,
36
+ )
32
37
  from okstra_project.state import read_task_manifest
33
38
 
34
39
  from .ids import (
@@ -422,7 +427,10 @@ def _whole_task_allowed(
422
427
  return False
423
428
  if done is None:
424
429
  done = _stage_lifecycle_snapshot(state, stages).done_stages
425
- return all(s.stage_number in done for s in stages)
430
+ return whole_task_verification_allowed(
431
+ stage_numbers=tuple(stage.stage_number for stage in stages),
432
+ done_stages=done,
433
+ )
426
434
 
427
435
 
428
436
  def _build_approved_plan_pick(state: WizardState) -> Prompt:
@@ -623,20 +631,14 @@ def _impl_stage_marker(t, lifecycle) -> str:
623
631
  def _submit_stage_pick(state: WizardState, answer: str) -> Optional[str]:
624
632
  if state.task_type == "implementation":
625
633
  return _submit_impl_stage_pick(state, answer)
626
- # final-verification: 단일선택 유지 (whole-task 또는 단일 정수; auto 불가)
627
- if not answer:
628
- raise WizardError("value required")
629
- if answer == WHOLE_TASK_STAGE:
630
- if not _whole_task_allowed(state):
631
- raise WizardError(
632
- "whole-task verification requires final-verification "
633
- "with all stages done")
634
- else:
635
- try:
636
- int(answer)
637
- except ValueError:
638
- raise WizardError(
639
- f"answer must be whole-task or a stage number, got {answer!r}")
634
+ try:
635
+ validate_stage_answer(
636
+ answer,
637
+ whole_task_token=WHOLE_TASK_STAGE,
638
+ whole_task_allowed=answer == WHOLE_TASK_STAGE and _whole_task_allowed(state),
639
+ )
640
+ except StageAnswerError as exc:
641
+ raise WizardError(str(exc)) from exc
640
642
  state.selected_stage = answer
641
643
  return f"stage: {answer}"
642
644
 
@@ -6,7 +6,7 @@ description: >-
6
6
 
7
7
  # OKSTRA User Response
8
8
 
9
- Use this skill for open `C-*` clarification items and explicit plan decisions. The user alone selects or writes every answer. Never infer an answer or approval.
9
+ Use this skill for open `C-*` clarification items, explicit plan decisions, and the implementation direction a finished `implementation-option-selection` comparison awaits. The user alone selects or writes every answer. Never infer an answer or approval.
10
10
 
11
11
  The model-facing commands are fixed text reads and typed transaction writes:
12
12
 
@@ -17,6 +17,7 @@ The model-facing commands are fixed text reads and typed transaction writes:
17
17
  | `user-response begin` | Open a sidecar transaction for one report identity. |
18
18
  | `user-response answer` | Add or replace one validated clarification answer. |
19
19
  | `user-response plan-decision` | Record an explicit plan decision in the transaction. |
20
+ | `user-response direction` | Record the implementation direction the user picked in the transaction. |
20
21
  | `user-response legacy-report-authoring` | Record legacy report-authoring permission for report contract 2.0 only. |
21
22
  | `user-response finalize` | Atomically merge and publish the user-owned sidecar. |
22
23
 
@@ -71,7 +72,7 @@ Never invent a picker function. Never ask the user to type a number when the nat
71
72
  okstra user-response list-view --home <resolved-home> --project <projectId> --limit 3
72
73
  ```
73
74
 
74
- The view gives `Task key`, `Task type`, `Report`, open-item counts, and readability status. If the count is zero, answer `No task has open clarification items.` and stop. Do not continue with an unreadable entry.
75
+ The view gives `Task key`, `Task type`, `Report`, open-item counts, `Direction selection required`, and readability status. If the count is zero, answer `No task has open clarification items.` and stop. Do not continue with an unreadable entry.
75
76
 
76
77
  Present up to three task choices through the host picker. A host free-text row or unmatched next message is the report path or task key.
77
78
 
@@ -89,6 +90,8 @@ Contract 3.0 options also expose `reach` and `scopeEffects`. Contract 3.0 approv
89
90
 
90
91
  When an axis says `not stated in the report`, repeat that text. Do not infer missing report-owned impact. The skill must **never invent it**.
91
92
 
93
+ When the view prints `Direction candidates:`, the comparison awaits a direction. Each `Direction option N:` row gives the candidate id and name, `Recommended`, `Goal`, `Core mechanism`, and `Selectable`. The `Direction picker:` block lists only selectable candidates, recommended first, each with its `Option number`.
94
+
92
95
  ## Step 2b: Investigate cited context before asking
93
96
 
94
97
  Do not present a picker from the raw field dump. For each still-open item, read the investigation list the view printed:
@@ -127,9 +130,15 @@ Use the displayed values to confirm the user's choice. Do not copy a predefined
127
130
 
128
131
  Copy `kind` from the view. A `reframe` does not satisfy the gate. If the user asks what an item means, explain from the view plus the cited files already read, then ask the same item again.
129
132
 
133
+ ## Step 3b: Ask for the direction
134
+
135
+ Only when `Current direction selection: none` and `Direction picker:` has rows. Ask one single-select question through the host picker, after any clarification whose answer would change the choice. The body says that the comparison is finished, that the chosen candidate becomes the input of `implementation-planning`, and that planning cannot start until one is chosen. Copy each `Direction picker:` `- Label:` / `Description:` pair in order. Remember the `Option number` of the picked row. A `Selectable: no - <reason>` candidate is never offered; when the user asks for it, state the reason.
136
+
137
+ When the user adds a note or a constraint for the planner, keep their words verbatim for Step 6.
138
+
130
139
  ## Step 4: Confirm the complete response
131
140
 
132
- Echo each clarification ID, kind, disposition, value, and rationale. Include any explicit plan decision or legacy report-authoring decision. Ask through the host picker, two options:
141
+ Echo each clarification ID, kind, disposition, value, and rationale. Include any explicit plan decision, direction, or legacy report-authoring decision. Ask through the host picker, two options:
133
142
 
134
143
  1. `Record as shown` (Recommended)
135
144
  2. `Change an answer`
@@ -150,7 +159,7 @@ For a predefined option, pass only its one-based number from the fixed view:
150
159
  okstra user-response answer --transaction <transaction> --clarification-id <C-NNN> --kind <kind> --option-number <N>
151
160
  ```
152
161
 
153
- Every value, rationale, and reason body file must be a regular file under `<projectRoot>/.okstra/tmp/user-response/`; do not use an external file or a symbolic link. For a direct user answer, write the exact value there. Write the rationale to a separate Markdown file only when present. Then run:
162
+ Every value, rationale, and reason body file — and every direction note or constraints file — must be a regular file under `<projectRoot>/.okstra/tmp/user-response/`; do not use an external file or a symbolic link. For a direct user answer, write the exact value there. Write the rationale to a separate Markdown file only when present. Then run:
154
163
 
155
164
  ```bash
156
165
  okstra user-response answer --transaction <transaction> --clarification-id <C-NNN> --kind <kind> --disposition <disposition> --value-file <value.md> [--rationale-file <rationale.md>]
@@ -174,6 +183,14 @@ okstra user-response plan-decision --transaction <transaction> --status <revisio
174
183
 
175
184
  Never infer a plan decision from the user's tone.
176
185
 
186
+ When the user picked a direction, pass the picked row's `Option number`. Write a note or constraints the user stated to separate Markdown files in that same temporary directory, one constraint per line:
187
+
188
+ ```bash
189
+ okstra user-response direction --transaction <transaction> --option-number <N> [--note-file <note.md>] [--constraints-file <constraints.md>]
190
+ ```
191
+
192
+ The command refuses a candidate the planning gate would refuse and a report that does not await a direction.
193
+
177
194
  Only for a report whose fixed view says `Report contract: 2.0`, an explicit legacy report-authoring decision may be recorded. A reason file in that same temporary directory is always required:
178
195
 
179
196
  ```bash
@@ -194,6 +211,8 @@ Leave this guidance in the final answer:
194
211
 
195
212
  > This answer was recorded in the `user-responses/` sidecar (`<sidecar path>`). Re-running this task with `/okstra-run` attaches the answer to the next eligible phase.
196
213
 
214
+ When a direction was recorded, add: start `implementation-planning` with `/okstra-run` and pick this report in the wizard's direction-report step.
215
+
197
216
  ## Output rules
198
217
 
199
218
  - Keep responses in the user's language.
@@ -101,6 +101,10 @@ from okstra_ctl.incremental_scope import ( # noqa: E402
101
101
  stages_for_clarification,
102
102
  )
103
103
  from okstra_ctl import next_phase # noqa: E402
104
+ from okstra_ctl.phases.final_verification.validation import ( # noqa: E402
105
+ validate_final_verification_content,
106
+ validate_verification_target_match,
107
+ )
104
108
  from okstra_ctl.clarification_items import ( # noqa: E402
105
109
  APPROVAL_BLOCKS,
106
110
  clarification_disposition,
@@ -5376,19 +5380,6 @@ def _consumers_rows(report_path: Path) -> list[dict] | None:
5376
5380
  return rows
5377
5381
 
5378
5382
 
5379
- # 이 스냅샷의 다이제스트 규칙과 파싱은 `okstra_ctl.verification_target` 하나가
5380
- # 쥔다. 조립도 같은 파일을 읽어 `verificationScope` 를 기록하므로, 사본을 두면
5381
- # 규칙이 갈리는 순간 한쪽이 정상 target 을 변조로 판정한다.
5382
- from okstra_ctl.verification_target import ( # noqa: E402
5383
- TARGET_FIELD_RES as _TARGET_FIELD_RES,
5384
- read_verification_target as _read_verification_target_impl,
5385
- )
5386
-
5387
-
5388
- def _read_verification_target(project_root: Path, relative: str) -> dict | None:
5389
- return _read_verification_target_impl(project_root, relative)
5390
-
5391
-
5392
5383
  _PLAN_BODY_STATE_KEYS = ("schemaVersion", "planItems", "roundHistory")
5393
5384
 
5394
5385
 
@@ -5748,66 +5739,6 @@ def _validate_verifier_command_log_is_read_only(
5748
5739
 
5749
5740
 
5750
5741
 
5751
- def _validate_verification_target_match(
5752
- data: dict,
5753
- run_manifest: dict,
5754
- project_root: Path,
5755
- failures: list[str],
5756
- ) -> None:
5757
- """The verification report must mirror the target it was prepared against.
5758
-
5759
- `verificationScope`, the worktree, and the base/head refs were entirely
5760
- self-declared: the schema required the fields to exist but nothing compared
5761
- them to the digest-verified snapshot written at prep time. That matters
5762
- because both `handoff.compute_eligibility` and the `release-handoff`
5763
- routing check read `verificationScope` — a single-stage run that writes
5764
- `whole-task` passes both, and an `accepted` verdict can be rendered against
5765
- a worktree or head nobody verified.
5766
- """
5767
- # 최상위 `verificationTargetPath` 가 run 매니페스트의 실물 키다(render.py).
5768
- # `instructionSet` 블록은 active-run-context 의 것이라 여기서 읽으면 검사가
5769
- # 통째로 건너뛰어졌다(실측 2026-09-06, dev-10626 final-verification 001).
5770
- relative = str(run_manifest.get("verificationTargetPath") or "").strip()
5771
- if not relative:
5772
- return
5773
- target = _read_verification_target(project_root, relative)
5774
- if target is None:
5775
- return
5776
-
5777
- source = (data.get("finalVerification") or {}).get("sourceImplementationReport") or {}
5778
- declared = {
5779
- "scope": str(data.get("verificationScope") or "").strip(),
5780
- "worktree": str(source.get("worktreePath") or "").strip(),
5781
- "base": str(source.get("implementationBaseRef") or "").strip(),
5782
- "head": str(source.get("capturedHeadSha") or "").strip(),
5783
- }
5784
- for key, expected in ((k, target[k]) for k in _TARGET_FIELD_RES):
5785
- actual = declared[key]
5786
- if expected and actual and actual != expected:
5787
- failures.append(
5788
- f"final-verification report declares {key} `{actual}` but the "
5789
- f"prepared verification target says `{expected}` "
5790
- f"(`{relative}`). The report must mirror the target it was "
5791
- "prepared against — `verificationScope` in particular gates "
5792
- "both stage-group eligibility and release-handoff routing, so "
5793
- "a self-declared value lets a run be judged as something it "
5794
- "was not."
5795
- )
5796
-
5797
- declared_stages = {
5798
- row.get("stage")
5799
- for row in ((data.get("finalVerification") or {}).get("stageReports") or [])
5800
- if isinstance(row, dict) and isinstance(row.get("stage"), int)
5801
- }
5802
- if target["stages"] and declared_stages and declared_stages != target["stages"]:
5803
- failures.append(
5804
- f"final-verification report covers stages {sorted(declared_stages)} "
5805
- f"but the prepared target names {sorted(target['stages'])} "
5806
- f"(`{relative}`). A verdict must not be rendered for a stage set "
5807
- "nobody prepared evidence for."
5808
- )
5809
-
5810
-
5811
5742
  def _validate_verified_row_recorded(
5812
5743
  data: dict,
5813
5744
  report_path: Path,
@@ -8025,98 +7956,19 @@ def _validate_stage_has_requirement(data: dict, failures: list[str]) -> None:
8025
7956
  )
8026
7957
 
8027
7958
 
8028
- _ADDED_SURFACE_NO_CALLER_RE = re.compile(r"^\s*none\b", re.IGNORECASE)
8029
-
8030
-
8031
- def _validate_added_surface_audit(data: dict, failures: list[str]) -> None:
8032
- """Every surface the diff added is traced to a requirement, exempted, or paid for.
8033
-
8034
- The coverage table proves each requirement reached the diff. Nothing proved
8035
- the reverse — that each thing the diff added answers a requirement — so work
8036
- nobody asked for passed every gate. This check reads the reverse table and
8037
- refuses a row that calls itself over-delivery without the blocker or
8038
- condition it became: a caller-less surface is an acceptance blocker, and a
8039
- surface with callers but no requirement is a conditional-acceptance
8040
- condition (ADR-0009 grades the two differently on purpose).
8041
- """
8042
- fv = data.get("finalVerification")
8043
- if not isinstance(fv, Mapping):
8044
- return
8045
- rows = fv.get("addedSurfaceAudit")
8046
- if not isinstance(rows, list):
8047
- return
8048
- blocker_ids = {
8049
- str(row.get("id"))
8050
- for row in (fv.get("acceptanceBlockers") or [])
8051
- if isinstance(row, Mapping)
8052
- }
8053
- condition_ids = {
8054
- str(row.get("id"))
8055
- for row in ((data.get("finalVerdict") or {}).get(
8056
- "conditionalAcceptanceConditions") or [])
8057
- if isinstance(row, Mapping)
8058
- }
8059
- for row in rows:
8060
- if not isinstance(row, Mapping):
8061
- continue
8062
- row_id = str(row.get("id") or "<id 없음>")
8063
- disposition = str(row.get("disposition") or "")
8064
- note = str(row.get("note") or "")
8065
- if disposition == "traced" and not str(row.get("requirement") or "").strip():
8066
- failures.append(
8067
- f"final-verification: addedSurfaceAudit {row_id} is `traced` but "
8068
- "names no requirement — a surface is traced to something the "
8069
- "brief asked for, or it is not traced."
8070
- )
8071
- continue
8072
- if disposition != "over-delivery":
8073
- continue
8074
- caller_less = bool(
8075
- _ADDED_SURFACE_NO_CALLER_RE.match(str(row.get("callers") or ""))
8076
- )
8077
- expected, known = (
8078
- ("AB", blocker_ids) if caller_less else ("CA", condition_ids)
8079
- )
8080
- cited = set(re.findall(rf"\b{expected}-\d{{3,}}\b", note))
8081
- if not cited:
8082
- failures.append(
8083
- f"final-verification: addedSurfaceAudit {row_id} is "
8084
- f"`over-delivery` with callers "
8085
- f"{'none' if caller_less else 'recorded'}, so its note MUST cite "
8086
- f"the `{expected}-NNN` row it became — "
8087
- + (
8088
- "a caller-less surface is an acceptance blocker"
8089
- if caller_less
8090
- else "a surface with callers but no requirement is a "
8091
- "conditional-acceptance condition"
8092
- )
8093
- + "."
8094
- )
8095
- continue
8096
- missing = sorted(cited - known)
8097
- if missing:
8098
- failures.append(
8099
- f"final-verification: addedSurfaceAudit {row_id} cites "
8100
- f"{missing}, which the report does not carry."
8101
- )
8102
-
8103
-
8104
7959
  def _validate_final_verification_consistency(data: dict, failures: list[str]) -> None:
8105
- """Enforce verdict ↔ blocker/condition/routing consistency on the
8106
- final-verification data.json (SSOT). The schema guarantees field SHAPE;
8107
- these are the cross-field invariants the release-handoff gate depends on.
8108
-
8109
- No-op for non-final-verification data so the caller's gate stays defensive.
8110
- """
7960
+ """단계 내용 판정 뒤에 이동 적합성을 본다. 다른 작업 유형은 건너뛴다."""
8111
7961
  if (data.get("header") or {}).get("taskType") != "final-verification":
8112
7962
  return
8113
- _validate_added_surface_audit(data, failures)
7963
+ validate_final_verification_content(data, failures)
7964
+ _validate_final_verification_routing(data, failures)
7965
+
7966
+
7967
+ def _validate_final_verification_routing(data: dict, failures: list[str]) -> None:
7968
+ """다음 단계 이름이 판정과 맞는지 본다. 대상 선택은 추론하지 않는다."""
8114
7969
  verdict = data.get("finalVerdict") or {}
8115
7970
  token = (verdict.get("verdictToken") or "").strip().lower()
8116
- fv = data.get("finalVerification") or {}
8117
- blockers = fv.get("acceptanceBlockers") or []
8118
- conditions = verdict.get("conditionalAcceptanceConditions") or []
8119
- routing_value = fv.get("routingRecommendation")
7971
+ routing_value = (data.get("finalVerification") or {}).get("routingRecommendation")
8120
7972
  routing_token = ""
8121
7973
  if isinstance(routing_value, dict):
8122
7974
  routing_token = str(routing_value.get("target") or "")
@@ -8125,23 +7977,16 @@ def _validate_final_verification_consistency(data: dict, failures: list[str]) ->
8125
7977
  "final-verification: routingRecommendation.target must name exactly one "
8126
7978
  "supported routing target."
8127
7979
  )
8128
- routing_token = None
7980
+ return
7981
+ _refuse_unsuitable_final_verification_route(data, failures, token, routing_token)
8129
7982
 
8130
- if token == "accepted" and blockers:
8131
- failures.append(
8132
- "final-verification: verdict `accepted` but acceptanceBlockers is "
8133
- "non-empty — an accepted verdict must have zero blockers."
8134
- )
8135
- if token == "blocked" and not blockers:
8136
- failures.append(
8137
- "final-verification: verdict `blocked` but acceptanceBlockers is "
8138
- "empty — a blocked verdict must list at least one blocker."
8139
- )
8140
- if token == "conditional-accept" and not conditions:
8141
- failures.append(
8142
- "final-verification: verdict `conditional-accept` but "
8143
- "conditionalAcceptanceConditions is empty — list every condition."
8144
- )
7983
+
7984
+ def _refuse_unsuitable_final_verification_route(
7985
+ data: dict,
7986
+ failures: list[str],
7987
+ token: str,
7988
+ routing_token: str,
7989
+ ) -> None:
8145
7990
  if routing_token in RELEASE_HANDOFF_TARGETS and not release_handoff_allowed(data):
8146
7991
  blocking = blocking_condition_ids(data)
8147
7992
  reason = (
@@ -8156,7 +8001,6 @@ def _validate_final_verification_consistency(data: dict, failures: list[str]) ->
8156
8001
  "or a `conditional-accept` whose every condition declares "
8157
8002
  "`blocksReleaseHandoff: false`."
8158
8003
  )
8159
-
8160
8004
  if routing_token == "final-verification" and token == "accepted":
8161
8005
  failures.append(
8162
8006
  "final-verification: routingRecommendation cites `final-verification` "
@@ -8164,13 +8008,6 @@ def _validate_final_verification_consistency(data: dict, failures: list[str]) ->
8164
8008
  "to re-verify. Route to release-handoff or done."
8165
8009
  )
8166
8010
 
8167
- scope = data.get("verificationScope", "whole-task")
8168
- if scope not in ("whole-task", "single-stage"):
8169
- failures.append(
8170
- f"final-verification: verificationScope must be `whole-task` or "
8171
- f"`single-stage`, got {scope!r}."
8172
- )
8173
-
8174
8011
 
8175
8012
  def validate_report_views(report_path: Path, failures: list[str]) -> None:
8176
8013
  """Enforce Phase 7 step 1.5 (BLOCKING) — the self-contained HTML
@@ -9848,7 +9685,7 @@ def main() -> int:
9848
9685
  Path(args.state).resolve() if args.state else None,
9849
9686
  )
9850
9687
  if task_type == "final-verification":
9851
- _validate_verification_target_match(
9688
+ validate_verification_target_match(
9852
9689
  validation_data,
9853
9690
  run_manifest,
9854
9691
  project_root,
@@ -1,82 +0,0 @@
1
- # okstra-run task process
2
-
3
- ## Index
4
-
5
- - [1. Reading order](#1-reading-order)
6
- - [2. Big picture](#2-big-picture)
7
- - [3. task-type documents](#3-task-type-documents)
8
- - [4. Key code locations](#4-key-code-locations)
9
- - [5. Quick comparison table](#5-quick-comparison-table)
10
-
11
- ## 1. Reading order
12
-
13
- `okstra-run` is the path that starts a task inside a supported Claude Code, Codex, or Antigravity host session. This folder organizes that execution flow into two layers.
14
-
15
- 1. First read [common-flow.md](common-flow.md). It is the wizard, render-bundle, lead phase, and artifact flow shared by every task-type.
16
- 2. Then read the document for the task-type you want to run.
17
- 3. Check the per-task-type differences across three places: "what the wizard additionally asks", "what `prepare_task_bundle()` blocks in the runtime", and "what the lead profile enforces within the phase".
18
-
19
- ## 2. Big picture
20
-
21
- ```mermaid
22
- flowchart TD
23
- U[User in supported host] --> S[okstra-run skill]
24
- S --> R[Step 1<br/>ensure-installed / paths / check-project]
25
- R --> W[okstra wizard<br/>state machine]
26
- W --> A[render-args]
27
- A --> B[okstra render-bundle<br/>--render-only]
28
- B --> P[prepare_task_bundle()]
29
- P --> I["runs/task-type/prompts<br/>lead-execution-prompt-*.md"]
30
- I --> L[Current host session<br/>takes over as Okstra lead]
31
- L --> F[Phase 1-7 lead workflow]
32
- F --> O[final-report + manifests + status]
33
- ```
34
-
35
- `okstra-run` does not call `scripts/okstra.sh`. Instead it goes through `okstra wizard` and `okstra render-bundle` and converges on the same single Python entrypoint, `prepare_task_bundle()`.
36
-
37
- Launch selection is role slots and model refs, not a provider roster. The wizard asks one screen per static role: a checkbox of candidate models for a role that runs several instances (the number checked is the instance count, rendered as `--role-count <role>=<N>` plus one `--role-model <role>=<provider>/<model>` per checked model; the label states the profile range and recommended count), a single pick for a fixed single-instance role. current-session lead is this session and is listed on the confirmation summary. Roles with `min = 0` stay closed unless the user adds them. There is no provider multi-pick and no `Use defaults / Customize` fork for worker selection. `--workers` is compatibility-only. `lead` is a compatibility alias for `leader`. `executor` is a compatibility alias for `implementer`. New records write `leader` and `implementer`.
38
-
39
- ## 3. task-type documents
40
-
41
- | task-type | Document | One-line purpose |
42
- |---|---|---|
43
- | `requirements-discovery` | [requirements-discovery.md](requirements-discovery.md) | Classify the request and choose the next safe phase. |
44
- | `error-analysis` | [error-analysis.md](error-analysis.md) | Find cause candidates and validation paths from symptoms and evidence. |
45
- | `implementation-option-selection` | [implementation-option-selection.md](implementation-option-selection.md) | Compare or validate exact-coverage directions before detailed planning. |
46
- | `implementation-planning` | [implementation-planning.md](implementation-planning.md) | Realize one selected direction as an exact-coverage plan with a separate approval gate. |
47
- | `implementation` | [implementation.md](implementation.md) | The executor implements the approved plan and the verifier verifies it independently. |
48
- | `final-verification` | [final-verification.md](final-verification.md) | Judge whole-task or single-stage acceptance of the implementation result. |
49
- | `release-handoff` | [release-handoff.md](release-handoff.md) | Perform the push/PR handoff lead-only after an accepted verdict. |
50
-
51
- ## 4. Key code locations
52
-
53
- | Concern | Source of truth |
54
- |---|---|
55
- | okstra-run skill procedure | [`skills/okstra-run/SKILL.md`](../../skills/okstra-run/SKILL.md) |
56
- | wizard state machine | [`scripts/okstra_ctl/wizard/`](../../scripts/okstra_ctl/wizard/) |
57
- | wizard prompt text | [`prompts/wizard/prompts.ko.json`](../../prompts/wizard/prompts.ko.json) |
58
- | render-bundle Node shim | [`src/commands/execute/render-bundle.mts`](../../src/commands/execute/render-bundle.mts) |
59
- | single entrypoint for bundle creation | [`scripts/okstra_ctl/run.py`](../../scripts/okstra_ctl/run.py) |
60
- | implementation stage selection/provisioning | [`scripts/okstra_ctl/implementation_stage.py`](../../scripts/okstra_ctl/implementation_stage.py) |
61
- | Stage Lifecycle Snapshot + stage target/base/verification policy | [`scripts/okstra_ctl/stage_targets.py`](../../scripts/okstra_ctl/stage_targets.py) |
62
- | phase boundary | [`scripts/okstra_ctl/workflow.py`](../../scripts/okstra_ctl/workflow.py) |
63
- | task worktree | [`scripts/okstra_ctl/worktree/`](../../scripts/okstra_ctl/worktree/) |
64
- | stage-group handoff | [`scripts/okstra_ctl/handoff.py`](../../scripts/okstra_ctl/handoff.py) |
65
- | worker roster parser | [`scripts/okstra_ctl/workers.py`](../../scripts/okstra_ctl/workers.py) |
66
- | lead operating contract | [`prompts/lead/okstra-lead-contract.md`](../../prompts/lead/okstra-lead-contract.md) |
67
- | phase profiles | [`prompts/profiles/`](../../prompts/profiles/) |
68
- | final report shape / HTML view | [`templates/reports/final-report-v2.template.md`](../../templates/reports/final-report-v2.template.md), [`scripts/okstra_ctl/report_views.py`](../../scripts/okstra_ctl/report_views.py) |
69
-
70
- ## 5. Quick comparison table
71
-
72
- The last column is the `workflow.nextRecommendedPhase` pointer Phase 7 leaves behind — an object `{phase, status, rationale}`, projected from the report's own routing field. There is no static default: a run that settles no route ends `pending` with no phase. The routing field's `rationale` is carried into the pointer, and the lead's closeout quotes it. The rule for authoring that field is stated once, in the Phase 6 checklist of [`prompts/lead/report-writer.md`](../../prompts/lead/report-writer.md).
73
-
74
- | task-type | wizard special question | runtime prepare gate | lead/worker mode | next-phase pointer |
75
- |---|---|---|---|---|
76
- | `requirements-discovery` | common questions only | profile/brief/base-ref exist | multi-worker analysis, convergence 1 round default | `ready` at `error-analysis` or `implementation-option-selection`; `pending` when neither is settled |
77
- | `error-analysis` | common questions only | profile/brief/base-ref exist | multi-worker analysis, convergence 2 rounds default | `ready` at `implementation-option-selection`, or at `error-analysis` while the investigation continues |
78
- | `implementation-option-selection` | comparison or preselected-validation context | stable brief IDs and at least three analysers | read-only candidate validation, exact coverage, separate direction confirmation | `ready` at `implementation-planning` (a confirmed direction, or ranked candidates the planning wizard lets the user pick from), `pending` when no candidate was ranked, or `blocked` |
79
- | `implementation-planning` | selected-direction report for a new plan | selection report/sidecar/digest or same-task planning rerun | one-direction realization + Phase 6 plan-body verification | `ready` at `implementation` on approvable `plan-ready` (`awaitingApproval` until the user flips `approved`); `blocked` when the gate is blocking or a `Blocks=approval` row is open; `ready` at `implementation-option-selection` on `direction-invalidated` |
80
- | `implementation` | approved plan, stage multi-pick, executor | approved marker, Stage Lifecycle Snapshot, stage-key reservation, QA command deny-list | one run = one stage; executor writes in isolated stage worktree, verifiers read-only | `ready` at the stage report's `routingRecommendation.target` — `final-verification` on a clean stage |
81
- | `final-verification` | approved plan, stage pick (whole-task or single-stage) | `VERIFICATION_TARGET` resolved; whole-task auto integration/teardown or single-stage worktree reuse | whole-task may integrate stages first; analyser verification itself is read-only | `ready` at `release-handoff` on an `accepted` verdict, otherwise at the phase owning the defect; `terminal` on `done` |
82
- | `release-handoff` | handoff scope (stage-group or whole-task), PR template override/scope | Stage Lifecycle Snapshot eligibility, generated `release-handoff-input.md`, empty worker roster | single-lead; whole-task PR or stage-group collector branch/PR | always `terminal` |