okstra 0.173.0 → 0.174.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 (62) hide show
  1. package/docs/architecture/storage-model.md +13 -3
  2. package/docs/architecture.md +5 -21
  3. package/docs/cli.md +3 -2
  4. package/docs/container.md +1 -1
  5. package/docs/contributor-change-matrix.md +1 -1
  6. package/docs/project-structure-overview.md +13 -13
  7. package/docs/task-process/README.md +1 -1
  8. package/docs/task-process/implementation-planning.md +1 -1
  9. package/package.json +1 -1
  10. package/runtime/BUILD.json +2 -2
  11. package/runtime/agents/workers/claude-worker.md +1 -1
  12. package/runtime/bin/lib/okstra/globals.sh +1 -1
  13. package/runtime/bin/okstra-provider-exec.py +29 -12
  14. package/runtime/bin/okstra-trace-cleanup.sh +58 -129
  15. package/runtime/prompts/lead/adapters/cmux.md +2 -0
  16. package/runtime/prompts/lead/okstra-lead-contract.md +1 -1
  17. package/runtime/prompts/lead/plan-body-verification.md +3 -3
  18. package/runtime/prompts/lead/report-writer.md +6 -6
  19. package/runtime/prompts/profiles/_common-contract.md +2 -2
  20. package/runtime/prompts/profiles/_implementation-executor.md +2 -0
  21. package/runtime/prompts/profiles/_implementation-verifier.md +2 -2
  22. package/runtime/prompts/profiles/error-analysis.md +1 -1
  23. package/runtime/prompts/profiles/implementation-planning.md +12 -9
  24. package/runtime/prompts/profiles/implementation.md +2 -1
  25. package/runtime/prompts/profiles/release-handoff.md +1 -1
  26. package/runtime/python/okstra_ctl/adapters/dispatch/__init__.py +1 -6
  27. package/runtime/python/okstra_ctl/adapters/hosts/external/relay.md +4 -4
  28. package/runtime/python/okstra_ctl/adapters/providers/claude/adapter.py +5 -0
  29. package/runtime/python/okstra_ctl/conformance.py +68 -0
  30. package/runtime/python/okstra_ctl/dispatch_core.py +89 -39
  31. package/runtime/python/okstra_ctl/dispatch_state.py +142 -14
  32. package/runtime/python/okstra_ctl/doctor.py +2 -2
  33. package/runtime/python/okstra_ctl/domain/worker_exec.py +5 -0
  34. package/runtime/python/okstra_ctl/final_report_schema.py +5 -4
  35. package/runtime/python/okstra_ctl/pane_reclaim.py +13 -22
  36. package/runtime/python/okstra_ctl/render_final_report.py +15 -19
  37. package/runtime/python/okstra_ctl/report_contract.py +0 -1
  38. package/runtime/python/okstra_ctl/report_finalize.py +68 -9
  39. package/runtime/python/okstra_ctl/run.py +43 -2
  40. package/runtime/python/okstra_ctl/schema_excerpt.py +1 -1
  41. package/runtime/python/okstra_ctl/scope_provenance.py +1 -1
  42. package/runtime/python/okstra_ctl/session.py +69 -12
  43. package/runtime/python/okstra_ctl/team.py +51 -25
  44. package/runtime/python/okstra_ctl/tmux.py +19 -149
  45. package/runtime/python/okstra_ctl/worker_request.py +2 -0
  46. package/runtime/python/okstra_ctl/worktree.py +69 -3
  47. package/runtime/python/okstra_token_usage/cli.py +1 -1
  48. package/runtime/python/okstra_token_usage/collect.py +66 -6
  49. package/runtime/skills/okstra-setup/references/project-config.md +11 -0
  50. package/runtime/templates/reports/settings.template.json +0 -24
  51. package/runtime/validators/lib/fixtures.sh +49 -17
  52. package/runtime/validators/validate-implementation-plan-stages.py +63 -3
  53. package/runtime/validators/validate-run.py +14 -473
  54. package/runtime/validators/validate_session_conformance.py +1 -1
  55. package/src/cli-registry.mjs +8 -1
  56. package/src/commands/execute/team.mjs +3 -3
  57. package/src/commands/execute/worktree-status.mjs +109 -0
  58. package/src/commands/lifecycle/install.mjs +0 -2
  59. package/src/commands/report/finalize.mjs +13 -6
  60. package/runtime/bin/okstra-subagent-reclaim.sh +0 -26
  61. package/runtime/schemas/final-report-v1.0.schema.json +0 -6366
  62. package/runtime/templates/reports/final-report.template.md +0 -1258
@@ -43,14 +43,16 @@ from okstra_project.dirs import tasks_root as _okstra_tasks_root # noqa: E402
43
43
  from okstra_project.resolver import resolve_architecture # noqa: E402
44
44
 
45
45
  from okstra_ctl.conformance import ( # noqa: E402
46
- CAPABILITY_WHITELIST,
47
46
  detect_surfaces,
48
47
  evaluate_conformance,
49
48
  manifest_required_surfaces,
49
+ normalize_conformance_script as _normalize_conformance_script,
50
+ parse_conformance_tests as _parse_conformance_tests,
50
51
  qa_result_from_dict,
51
52
  validate_conformance_manifest,
52
53
  )
53
54
  from okstra_ctl.paths import RunRef # noqa: E402
55
+ from okstra_ctl.report_contract import CURRENT_REPORT_SCHEMA_VERSION # noqa: E402
54
56
  from okstra_ctl.domain.host import HostNotRegistered # noqa: E402
55
57
  from okstra_ctl.registry.host_registry import default_host_registry # noqa: E402
56
58
  from okstra_ctl.build_tools import ( # noqa: E402
@@ -141,7 +143,7 @@ from okstra_ctl.convergence_provenance import ( # noqa: E402
141
143
 
142
144
  TERMINAL_STATUSES = {"completed", "timeout", "error", "not-run"}
143
145
  ATTEMPTED_STATUSES = {"completed", "timeout", "error"}
144
- WORKER_DISPATCH_MODES = {"cli-wrapper", "mixed", "tmux-pane"}
146
+ WORKER_DISPATCH_MODES = {"cli-wrapper", "mixed"}
145
147
  _AGENT_DISPATCH_DIGEST_KEYS = (
146
148
  "catalogDigest",
147
149
  "assignmentDigest",
@@ -1682,35 +1684,6 @@ def _approved_plan_stage_entry(
1682
1684
  }
1683
1685
 
1684
1686
 
1685
- _CONFORMANCE_TESTS_RE = re.compile(
1686
- r"^(?P<script>\S+)\s+\(requires=\[(?P<requires>[^\]]+)\]\)$"
1687
- )
1688
-
1689
-
1690
- def _normalize_conformance_script(script: str) -> str:
1691
- prefix = "<task_root>/"
1692
- return script[len(prefix):] if script.startswith(prefix) else script
1693
-
1694
-
1695
- def _parse_conformance_tests(value: object) -> tuple[str, frozenset[str]] | None:
1696
- """Parse `<script> (requires=[cap,...])` from a stage declaration."""
1697
- if not isinstance(value, str):
1698
- return None
1699
- match = _CONFORMANCE_TESTS_RE.fullmatch(value.strip())
1700
- if match is None:
1701
- return None
1702
- script = _normalize_conformance_script(match.group("script"))
1703
- capabilities = [part.strip() for part in match.group("requires").split(",")]
1704
- if (
1705
- not script
1706
- or any(not capability for capability in capabilities)
1707
- or len(set(capabilities)) != len(capabilities)
1708
- or any(capability not in CAPABILITY_WHITELIST for capability in capabilities)
1709
- ):
1710
- return None
1711
- return script, frozenset(capabilities)
1712
-
1713
-
1714
1687
  def _approved_plan_conformance_manifest(
1715
1688
  approved_plan_path: Path,
1716
1689
  task_root: Path,
@@ -2355,7 +2328,7 @@ def _validate_selfmock(report_path: Path, failures: list[str]) -> None:
2355
2328
  read the flat `self-mock.json`.
2356
2329
 
2357
2330
  Only the implementation template renders the §5.7.3 diff summary
2358
- (`templates/reports/final-report.template.md:589`); a final-verification report
2331
+ (`templates/reports/final-report-v2.template.md:589`); a final-verification report
2359
2332
  quotes the diff as a blockquote instead, so this gate is vacuous there by
2360
2333
  design — self-mock is enforced at the implementation stage, and
2361
2334
  final-verification is a read-only re-verify that adds no test files.
@@ -2717,65 +2690,6 @@ def validate_team_state_usage(team_state: dict, failures: list[str]) -> None:
2717
2690
  )
2718
2691
 
2719
2692
 
2720
- PLANNING_REQUIRED_SECTIONS = (
2721
- "Option Candidates",
2722
- "Trade-off",
2723
- "Recommended Option",
2724
- "Stage Map",
2725
- "Stepwise Execution Order",
2726
- "Dependency",
2727
- "Validation Checklist",
2728
- "Rollback",
2729
- "Requirement Coverage",
2730
- "Plan Body Verification",
2731
- "Cross-Project Dependencies",
2732
- "Decision Drafts",
2733
- )
2734
-
2735
- # §5.7 implementation deliverables — substring scan against report body.
2736
- IMPLEMENTATION_REQUIRED_SECTIONS = (
2737
- "Approved Plan Reference",
2738
- "Commit List",
2739
- "Diff Summary",
2740
- "Out-of-plan Edits",
2741
- "Stage Sidecar Evidence",
2742
- "Validation Evidence",
2743
- "Verifier Results",
2744
- "Rollback Verification",
2745
- "Manual User Test (Draft)",
2746
- "Routing Recommendation",
2747
- )
2748
-
2749
- # §5.8 final-verification deliverables — substring scan against report body.
2750
- FINAL_VERIFICATION_REQUIRED_SECTIONS = (
2751
- "Source Implementation Report",
2752
- "Acceptance Blockers",
2753
- "Residual Risk",
2754
- "Validation Evidence",
2755
- "Read-only Command Log",
2756
- "Conditional Acceptance Conditions",
2757
- "Manual User Test Results",
2758
- "Routing Recommendation",
2759
- "Could-Not-Verify Roll-up",
2760
- )
2761
-
2762
- # Allowed Verdict Token vocabulary, by task-type. `release-handoff` is
2763
- # author-tagged but reads its entry gate from final-verification's report
2764
- # and renders `not-applicable` itself.
2765
- FINAL_VERIFICATION_VERDICT_TOKENS = (
2766
- "accepted",
2767
- "conditional-accept",
2768
- "blocked",
2769
- )
2770
-
2771
- # `## 7. Final Verdict` Verdict Token cell — captures the value between
2772
- # backticks on the `Verdict Token` row. Tolerant to extra column whitespace
2773
- # and to leading bold/italic markers in the label cell.
2774
- _FINAL_VERDICT_TOKEN_RE = re.compile(
2775
- r"^\|[ \t]*\*{0,2}Verdict Token\*{0,2}[ \t]*\|[ \t]*`(?P<value>[^`\n]*)`",
2776
- re.MULTILINE,
2777
- )
2778
-
2779
2693
  # Verdict Card Verdict Token row (top-of-report at-a-glance). Same shape
2780
2694
  # as `_FINAL_VERDICT_TOKEN_RE` but matched against the first occurrence in
2781
2695
  # the Verdict Card block, scoped to the body between `## Verdict Card`
@@ -2786,23 +2700,6 @@ _VERDICT_CARD_BLOCK_RE = re.compile(
2786
2700
  re.DOTALL | re.MULTILINE,
2787
2701
  )
2788
2702
 
2789
- # `## 7. Final Verdict` block scope — used to scope the Verdict Token
2790
- # regex so that we don't accidentally match a Verdict Token row that
2791
- # lives in the Verdict Card or anywhere else.
2792
- _FINAL_VERDICT_BLOCK_RE = re.compile(
2793
- r"^##[ \t]+7\.[ \t]+Final Verdict" + _HEADING_TAIL
2794
- + r"\n(?P<body>.*?)(?=^##[ \t]|\Z)",
2795
- re.DOTALL | re.MULTILINE,
2796
- )
2797
-
2798
- # `## 5.6 Release Handoff Deliverables` and `## 5.6.6 Merge Conflict
2799
- # Probe` are required when task-type == release-handoff. The probe sub-
2800
- # section was retro-added to the template; old runs that predate it ship
2801
- # without it, but new runs must include it.
2802
- _MERGE_CONFLICT_PROBE_HEADING_RE = re.compile(
2803
- r"^###[ \t]+5\.6\.6[ \t]+Merge Conflict Probe\b", re.MULTILINE
2804
- )
2805
-
2806
2703
  PLAN_VERIFY_GATE_VALUES = (
2807
2704
  "passed",
2808
2705
  "passed-with-dissent",
@@ -2810,207 +2707,10 @@ PLAN_VERIFY_GATE_VALUES = (
2810
2707
  "aborted-non-result",
2811
2708
  )
2812
2709
 
2813
- # `Gate result:` line in §5.5.9 of the final report — captures the value
2814
- # token that follows. Tolerates arbitrary markdown formatting between the
2815
- # label and the value (backticks for inline code, double-asterisks for
2816
- # bold, colons, hyphens, whitespace). The captured value is then
2817
- # validated against `PLAN_VERIFY_GATE_VALUES` below so typo'd or unknown
2818
- # values surface as their own failure rather than silently no-matching.
2819
- _GATE_RESULT_RE = re.compile(
2820
- r"Gate result[^A-Za-z\n]+(?P<value>[a-z][a-z\-]+)",
2821
- re.IGNORECASE,
2822
- )
2823
-
2824
- # §5.5.9 Plan Body Verification section body — scopes the gate-result search
2825
- # so an earlier prose mention of "Gate result" elsewhere in the report cannot
2826
- # hijack the authoritative value. Matched from the `Plan Body Verification`
2827
- # heading to the next heading (or end-of-file).
2828
- _PLAN_BODY_VERIFICATION_BLOCK_RE = re.compile(
2829
- r"^#{2,}[ \t][^\n]*\bPlan Body Verification\b[^\n]*\n"
2830
- r"(?P<body>.*?)(?=^#{2,}[ \t]|\Z)",
2831
- re.DOTALL | re.MULTILINE,
2832
- )
2833
-
2834
- # Frontmatter approval flag — `approved: true|false` line inside the
2835
- # leading `---` YAML block. Mirrors `APPROVED_FRONTMATTER_PATTERN` in
2836
- # scripts/okstra_ctl/run.py.
2837
- _APPROVED_FRONTMATTER_RE = re.compile(
2838
- r"^approved:[ \t]+(true|false)[ \t]*$",
2839
- re.IGNORECASE | re.MULTILINE,
2840
- )
2841
2710
  # Tolerate a leading UTF-8 BOM and/or blank lines before the opening `---`
2842
2711
  # so a hand-edited or differently-rendered report does not silently bypass
2843
2712
  # the approved-frontmatter gate (the `.match` would otherwise return None).
2844
2713
  _FRONTMATTER_BLOCK_RE = re.compile(r"\A\ufeff?\s*---\n(.*?)\n---\n", re.DOTALL)
2845
- _REQUIREMENT_COVERAGE_HEADING_RE = re.compile(
2846
- r"^###[ \t]+(?:5\.5\.8[ \t]+)?Requirement Coverage\b",
2847
- re.IGNORECASE | re.MULTILINE,
2848
- )
2849
- _NEXT_THIRD_LEVEL_HEADING_RE = re.compile(r"^###[ \t]+", re.MULTILINE)
2850
-
2851
-
2852
- def _extract_final_verdict_token(content: str) -> str | None:
2853
- """Return the `Verdict Token` cell value from the `## 7. Final Verdict`
2854
- block, or None when the row is absent. Scoped to §7 so the Verdict
2855
- Card row (which has the same shape) does not shadow the authoritative
2856
- value.
2857
- """
2858
- block = _FINAL_VERDICT_BLOCK_RE.search(content)
2859
- if block is None:
2860
- return None
2861
- match = _FINAL_VERDICT_TOKEN_RE.search(block.group("body"))
2862
- if match is None:
2863
- return None
2864
- return match.group("value")
2865
-
2866
-
2867
- # 렌더러는 ID 정의 셀(`<a id="r-001"></a>R-001`)에도 스크롤 앵커를 넣는다.
2868
- # 셀 정규화 때 그 빈 앵커를 벗겨야 ID 컬럼이 bare 토큰으로 읽힌다
2869
- # (clarification_items._CELL_ANCHOR_RE 와 동형).
2870
- _CELL_ANCHOR_RE = re.compile(r'<a id="[^"]*"></a>')
2871
-
2872
-
2873
- def _split_markdown_row(line: str) -> list[str]:
2874
- return [
2875
- _CELL_ANCHOR_RE.sub("", cell).strip().strip("`").strip()
2876
- for cell in _split_pipe_row(line)
2877
- ]
2878
-
2879
-
2880
- def _append_requirement_coverage_failures(
2881
- content: str,
2882
- gate_value: str,
2883
- failures: list[str],
2884
- ) -> None:
2885
- """Validate implementation-planning §5.5.8 requirement coverage.
2886
-
2887
- The table is intentionally lightweight: it cannot prove semantic truth, but
2888
- it makes requirement-to-plan mapping explicit and blocks publishable plans
2889
- that admit uncovered requirements.
2890
- """
2891
- heading = _REQUIREMENT_COVERAGE_HEADING_RE.search(content)
2892
- if heading is None:
2893
- failures.append(
2894
- "implementation-planning report is missing `Requirement Coverage` "
2895
- "section — every task-brief requirement must map to option/stage/step."
2896
- )
2897
- return
2898
-
2899
- rest = content[heading.end():]
2900
- next_heading = _NEXT_THIRD_LEVEL_HEADING_RE.search(rest)
2901
- section = rest[: next_heading.start()] if next_heading else rest
2902
- lines = section.splitlines()
2903
-
2904
- header_idx = -1
2905
- headers: list[str] = []
2906
- for idx, line in enumerate(lines):
2907
- if not line.lstrip().startswith("|"):
2908
- continue
2909
- cells = [c.lower() for c in _split_markdown_row(line)]
2910
- if "id" in cells and "requirement" in cells and "status" in cells:
2911
- header_idx = idx
2912
- headers = cells
2913
- break
2914
- if header_idx < 0:
2915
- failures.append(
2916
- "implementation-planning Requirement Coverage section has no table "
2917
- "with `ID`, `Requirement`, and `Status` columns."
2918
- )
2919
- return
2920
-
2921
- id_col = headers.index("id")
2922
- status_col = headers.index("status")
2923
- rows: list[tuple[str, str]] = []
2924
- body_started = False
2925
- for line in lines[header_idx + 1:]:
2926
- if not line.lstrip().startswith("|"):
2927
- if body_started:
2928
- break
2929
- continue
2930
- if _is_markdown_separator(line):
2931
- body_started = True
2932
- continue
2933
- if not body_started:
2934
- continue
2935
- cells = _split_markdown_row(line)
2936
- if max(id_col, status_col) >= len(cells):
2937
- failures.append(
2938
- "implementation-planning Requirement Coverage table has a "
2939
- f"malformed row: `{line.strip()}`"
2940
- )
2941
- continue
2942
- rows.append((cells[id_col], cells[status_col].lower()))
2943
-
2944
- if not rows:
2945
- failures.append(
2946
- "implementation-planning Requirement Coverage table has no data rows."
2947
- )
2948
- return
2949
-
2950
- for row_id, status in rows:
2951
- # `status` is already lower-cased at parse time, so the blocker form
2952
- # arrives as `blocked c-001`; match lower-case here even though the
2953
- # canonical authored form (and the remedy message) is `blocked C-NNN`.
2954
- if not re.fullmatch(
2955
- r"covered|gap|blocked c-\d{3,}|"
2956
- r"documented-deviation — refs: (?:c-\d{3,}|d-\d{4,})"
2957
- r"(?:, (?:c-\d{3,}|d-\d{4,}))*; approval: "
2958
- r"(?:accepted|blocked c-\d{3,})",
2959
- status,
2960
- ):
2961
- failures.append(
2962
- "implementation-planning Requirement Coverage row "
2963
- f"`{row_id}` has invalid Status `{status}`; expected "
2964
- "`covered`, `gap`, `blocked C-NNN`, or a rendered "
2965
- "`documented-deviation` disposition."
2966
- )
2967
-
2968
- if gate_value in ("passed", "passed-with-dissent"):
2969
- uncovered = [
2970
- f"{row_id} ({status})"
2971
- for row_id, status in rows
2972
- if status != "covered"
2973
- and not status.endswith("; approval: accepted")
2974
- ]
2975
- if uncovered:
2976
- failures.append(
2977
- "implementation-planning Gate result is publishable but "
2978
- "Requirement Coverage has uncovered row(s): "
2979
- + ", ".join(uncovered)
2980
- )
2981
- def _extract_verdict_card_token(content: str) -> str | None:
2982
- """Return the `Verdict Token` cell from the Verdict Card block."""
2983
- block = _VERDICT_CARD_BLOCK_RE.search(content)
2984
- if block is None:
2985
- return None
2986
- match = _FINAL_VERDICT_TOKEN_RE.search(block.group("body"))
2987
- if match is None:
2988
- return None
2989
- return match.group("value")
2990
-
2991
-
2992
- def _validate_verdict_card_consistency(content: str, failures: list[str]) -> None:
2993
- """Verdict Card is a non-authoritative index of §7. If both blocks
2994
- carry a Verdict Token row, the values MUST byte-match (modulo case
2995
- and surrounding whitespace) — divergence is a contract violation per
2996
- `report-writer` SKILL.md "Authoring Contract".
2997
- """
2998
- card_value = _extract_verdict_card_token(content)
2999
- final_value = _extract_final_verdict_token(content)
3000
- if card_value is None or final_value is None:
3001
- # Missing-Card and missing-§7 are surfaced by other checks; this
3002
- # function only enforces the consistency contract between the two.
3003
- return
3004
- if card_value.strip().lower() != final_value.strip().lower():
3005
- failures.append(
3006
- "Verdict Card `Verdict Token` value "
3007
- f"`{card_value}` does not match `## 7. Final Verdict` value "
3008
- f"`{final_value}` — the Card is a non-authoritative index and "
3009
- "MUST byte-match §7. Either fix the Card or update §7; do not "
3010
- "ship divergent values."
3011
- )
3012
-
3013
-
3014
2714
  def _validate_verdict_card_fields(data: dict, failures: list[str]) -> None:
3015
2715
  """`verdictCard.direction` must byte-match its authoritative home in §7.
3016
2716
 
@@ -3337,9 +3037,8 @@ def validate_final_report_data(
3337
3037
  The data.json is the source-of-truth that the renderer reads to
3338
3038
  produce the markdown. If schema validation passes here, the rendered
3339
3039
  markdown is guaranteed to contain every section / row the contract
3340
- requires (the template loops over the data). The downstream
3341
- substring checks in ``validate_phase_boundary`` are kept as a safety
3342
- net but are expected to be redundant.
3040
+ requires (the template loops over the data), so the schema is the
3041
+ only place the deliverable contract is enforced.
3343
3042
 
3344
3043
  Missing data.json is reported as a single failure rather than a
3345
3044
  cascade of substring failures — that points the writer at the right
@@ -7394,7 +7093,7 @@ def _validate_unverified_critic_gaps_recorded(data: dict, failures: list[str]) -
7394
7093
 
7395
7094
 
7396
7095
  # Allowed `fixability` values, mirroring the schema enum
7397
- # (schemas/final-report-v1.0.schema.json planItems[].verdicts[].fixability).
7096
+ # (schemas/final-report-v2.0.schema.json planItems[].verdicts[].fixability).
7398
7097
  _FIXABILITY_VALUES = frozenset({"planner-fixable", "needs-user-input"})
7399
7098
 
7400
7099
 
@@ -8015,14 +7714,13 @@ def _load_stage_validator():
8015
7714
  def _append_stage_data_failures(data: Mapping[str, Any], failures: list[str]) -> None:
8016
7715
  """Run the stage relationship checks that schema v2 cannot express.
8017
7716
 
8018
- `_append_stage_structure_failures` scans rendered Markdown and sits after
8019
- the v2 early return in `validate_phase_boundary`, so for a v2 report the
8020
- depends-on DAG, parallel-stage file safety, RED→GREEN ordering, and the
8021
- TDD-exemption vocabulary had nothing enforcing them. The same validator
8022
- owns both modes so the rule vocabulary stays defined once.
7717
+ The depends-on DAG, parallel-stage file safety, RED→GREEN ordering, and
7718
+ the TDD-exemption vocabulary are relationships between stages, which a
7719
+ JSON Schema cannot state. They are enforced here, against the data.json,
7720
+ by the same validator that owns the rule vocabulary.
8023
7721
  """
8024
- if (data or {}).get("schemaVersion") != "2.0":
8025
- return # v1 reports are covered by the Markdown scan.
7722
+ if (data or {}).get("schemaVersion") != CURRENT_REPORT_SCHEMA_VERSION:
7723
+ return # Schema validation already rejected an unknown version.
8026
7724
  planning = (data or {}).get("implementationPlanning")
8027
7725
  if not isinstance(planning, Mapping):
8028
7726
  return # Schema validation already reported the missing block.
@@ -8037,157 +7735,6 @@ def _append_stage_data_failures(data: Mapping[str, Any], failures: list[str]) ->
8037
7735
  )
8038
7736
 
8039
7737
 
8040
- def _append_stage_structure_failures(content: str, failures: list[str]) -> None:
8041
- """Enforce the Stage Map structural contract at the implementation-planning
8042
- boundary. Without this, a plan missing `## 5.5 Stage Map` passes the
8043
- planning gate, gets approved, and only fails later at the `implementation`
8044
- entry (validators/validate-implementation-plan-stages.py via
8045
- prepare_task_bundle). Running the same validator here moves the failure to
8046
- produce-time."""
8047
- mod = _load_stage_validator()
8048
- if mod is None: # pragma: no cover — repo/runtime always ship the file
8049
- failures.append(f"cannot load Stage Map validator at {_STAGE_VALIDATOR_PATH}")
8050
- return
8051
- for e in mod.collect_validation_errors(content):
8052
- failures.append(
8053
- f"implementation-planning Stage Map structure invalid "
8054
- f"[{e.code} stage={e.stage}]: {e.message}"
8055
- )
8056
-
8057
-
8058
- def validate_phase_boundary(
8059
- task_type: str,
8060
- report_path: Path,
8061
- failures: list[str],
8062
- *,
8063
- report_data: Mapping[str, Any] | None = None,
8064
- ) -> None:
8065
- """Phase-specific contract checks.
8066
-
8067
- For `implementation-planning` runs, the final report must contain the
8068
- required deliverable sections; absence indicates a planning run that
8069
- skipped its core outputs (or an implementation run that ran under the
8070
- wrong task type).
8071
-
8072
- Additionally enforces the Plan Body Verification gate (§5.5.9):
8073
- - gate ∈ {passed, passed-with-dissent} → top-level Approval checkbox
8074
- MUST be present.
8075
- - gate ∈ {blocked-by-disagreement, aborted-non-result} → checkbox
8076
- MUST be absent (lead converted findings into Clarification rows
8077
- instead of opening the gate).
8078
- """
8079
- if (report_data or {}).get("schemaVersion") == "2.0":
8080
- return
8081
- if not report_path.exists():
8082
- return
8083
- content = report_path.read_text()
8084
-
8085
- # Verdict Card vs §7. Final Verdict Verdict Token consistency. The Card
8086
- # is a non-authoritative index; divergence is a contract violation.
8087
- _validate_verdict_card_consistency(content, failures)
8088
-
8089
- if task_type == "implementation":
8090
- for needle in IMPLEMENTATION_REQUIRED_SECTIONS:
8091
- if needle not in content:
8092
- failures.append(
8093
- "implementation report is missing required §5.7 "
8094
- f"deliverable section: `{needle}`"
8095
- )
8096
-
8097
- if task_type == "final-verification":
8098
- for needle in FINAL_VERIFICATION_REQUIRED_SECTIONS:
8099
- if needle not in content:
8100
- failures.append(
8101
- "final-verification report is missing required §5.8 "
8102
- f"deliverable section: `{needle}`"
8103
- )
8104
- token_value = _extract_final_verdict_token(content)
8105
- if token_value is None:
8106
- failures.append(
8107
- "final-verification report `## 7. Final Verdict` table is "
8108
- "missing the `Verdict Token` row — required by the release-"
8109
- "handoff entry gate."
8110
- )
8111
- elif token_value.strip().lower() not in FINAL_VERIFICATION_VERDICT_TOKENS:
8112
- failures.append(
8113
- "final-verification report `Verdict Token` value "
8114
- f"`{token_value}` is not one of "
8115
- f"{', '.join(FINAL_VERIFICATION_VERDICT_TOKENS)}."
8116
- )
8117
-
8118
- if task_type == "release-handoff":
8119
- if _MERGE_CONFLICT_PROBE_HEADING_RE.search(content) is None:
8120
- failures.append(
8121
- "release-handoff report is missing `### 5.6.6 Merge Conflict "
8122
- "Probe` sub-section — required by the release-handoff profile "
8123
- "(self-review 6, merge-conflict probe audit). When the run is "
8124
- "`local checkout` / `skip`, record the single line `- Not run "
8125
- "(user picked local checkout or skip).` under the heading."
8126
- )
8127
-
8128
- if task_type != "implementation-planning":
8129
- return
8130
- for needle in PLANNING_REQUIRED_SECTIONS:
8131
- if needle not in content:
8132
- failures.append(
8133
- "implementation-planning report is missing required section: "
8134
- f"`{needle}`"
8135
- )
8136
-
8137
- # Scope the gate-result search to the §5.5.9 Plan Body Verification block
8138
- # so an earlier prose mention of "Gate result" cannot hijack the value.
8139
- pbv_block = _PLAN_BODY_VERIFICATION_BLOCK_RE.search(content)
8140
- gate_match = (
8141
- _GATE_RESULT_RE.search(pbv_block.group("body")) if pbv_block else None
8142
- )
8143
- if gate_match is None:
8144
- # The `Plan Body Verification` heading check above already covers
8145
- # the wholly-missing case; a heading present without a `Gate result`
8146
- # line is its own contract violation.
8147
- if "Plan Body Verification" in content:
8148
- failures.append(
8149
- "implementation-planning report has `Plan Body Verification` "
8150
- "section but no `Gate result:` line — required by §5.5.9."
8151
- )
8152
- return
8153
- gate_value = gate_match.group("value").strip().lower()
8154
- if gate_value not in PLAN_VERIFY_GATE_VALUES:
8155
- failures.append(
8156
- "implementation-planning report `Gate result` value "
8157
- f"`{gate_value}` is not one of "
8158
- f"{', '.join(PLAN_VERIFY_GATE_VALUES)}."
8159
- )
8160
- return
8161
- fm_block = _FRONTMATTER_BLOCK_RE.match(content)
8162
- fm_match = (
8163
- _APPROVED_FRONTMATTER_RE.search(fm_block.group(1)) if fm_block else None
8164
- )
8165
- if gate_value in ("passed", "passed-with-dissent") and fm_match is None:
8166
- failures.append(
8167
- "implementation-planning report Gate result is "
8168
- f"`{gate_value}` but the frontmatter `approved:` field is missing — "
8169
- "render `approved: false` so the user (or `--approve`) can flip it."
8170
- )
8171
- if (
8172
- gate_value in ("blocked-by-disagreement", "aborted-non-result")
8173
- and fm_match is not None
8174
- and fm_match.group(1).lower() == "true"
8175
- ):
8176
- failures.append(
8177
- "implementation-planning report Gate result is "
8178
- f"`{gate_value}` but the frontmatter has `approved: true` — gate "
8179
- "must NOT publish a pre-approved plan when verification did not pass."
8180
- )
8181
-
8182
- _append_requirement_coverage_failures(content, gate_value, failures)
8183
-
8184
- # Only a publishable plan (gate passed) can be flipped to `approved: true`
8185
- # and reach the `implementation` entry, so the Stage Map structure is
8186
- # enforced only here — a blocked/aborted plan may legitimately be incomplete.
8187
- if gate_value in ("passed", "passed-with-dissent"):
8188
- _append_stage_structure_failures(content, failures)
8189
-
8190
-
8191
7738
  def _brief_path_from_manifest(task_manifest: dict, project_root: Path) -> Path:
8192
7739
  """This run's brief, or a non-existent path when the manifest names none.
8193
7740
 
@@ -9190,12 +8737,6 @@ def main() -> int:
9190
8737
  )
9191
8738
  validate_team_state_usage(team_state, failures)
9192
8739
 
9193
- validate_phase_boundary(
9194
- task_type,
9195
- report_path,
9196
- failures,
9197
- report_data=validation_data,
9198
- )
9199
8740
  _validate_phase_boundary_error_log(
9200
8741
  report_path.parent.parent,
9201
8742
  task_type,
@@ -50,7 +50,7 @@ from okstra_ctl.registry.host_registry import default_host_registry # noqa: E40
50
50
  from okstra_ctl.wrapper_status import read_wrapper_status # noqa: E402
51
51
 
52
52
  _DISPATCHED_STATUSES = {"completed", "timeout", "error", "in-progress"}
53
- _WORKER_DISPATCH_MODES = {"cli-wrapper", "mixed", "tmux-pane"}
53
+ _WORKER_DISPATCH_MODES = {"cli-wrapper", "mixed"}
54
54
  _ATTEMPTED_STATUSES = {"completed", "timeout", "error"}
55
55
  _LIVENESS_AUDIT_HEARTBEAT = "audit-heartbeat"
56
56
  _LIVENESS_WRAPPER_STATUS = "wrapper-status"
@@ -288,6 +288,13 @@ export const COMMAND_REGISTRY = [
288
288
  category: "introspection",
289
289
  summary: ["Look up registered worktree for a task-key"],
290
290
  },
291
+ {
292
+ name: "worktree-status",
293
+ module: "./commands/execute/worktree-status.mjs",
294
+ export: "run",
295
+ category: "introspection",
296
+ summary: ["Clean-worktree check that ignores okstra's own scaffolding"],
297
+ },
291
298
  {
292
299
  name: "plan-validate",
293
300
  module: "./commands/execute/plan-validate.mjs",
@@ -351,7 +358,7 @@ export const COMMAND_REGISTRY = [
351
358
  module: "./commands/execute/team.mjs",
352
359
  export: "run",
353
360
  category: "introspection",
354
- summary: ["Dispatch and await tmux-pane workers for external leads"],
361
+ summary: ["Dispatch and await pane-backed workers for external leads"],
355
362
  },
356
363
  {
357
364
  name: "wizard",
@@ -1,7 +1,7 @@
1
1
  import { runPythonModule } from "../../lib/python-helper.mjs";
2
2
  import { resolvePaths } from "../../lib/paths.mjs";
3
3
 
4
- const USAGE = `okstra team — dispatch and await tmux-pane workers for external leads
4
+ const USAGE = `okstra team — dispatch and await pane-backed workers for external leads
5
5
 
6
6
  Usage:
7
7
  okstra team dispatch --project-root <dir> --run-manifest <path> \
@@ -14,8 +14,8 @@ Usage:
14
14
  [--dry-run] [--json]
15
15
 
16
16
  This command is for leadRuntime=external bundles. It dispatches okstra-owned
17
- worker panes through tmux when possible and degrades to CLI wrappers when tmux is
18
- unavailable to the lead harness.
17
+ worker surfaces through cmux when the lead is inside it, and runs CLI wrapper
18
+ subprocesses everywhere else.
19
19
 
20
20
  --workspace-root and --okstra-bin are owned by this command.
21
21
  `;