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.
- package/docs/architecture/storage-model.md +13 -3
- package/docs/architecture.md +5 -21
- package/docs/cli.md +3 -2
- package/docs/container.md +1 -1
- package/docs/contributor-change-matrix.md +1 -1
- package/docs/project-structure-overview.md +13 -13
- package/docs/task-process/README.md +1 -1
- package/docs/task-process/implementation-planning.md +1 -1
- package/package.json +1 -1
- package/runtime/BUILD.json +2 -2
- package/runtime/agents/workers/claude-worker.md +1 -1
- package/runtime/bin/lib/okstra/globals.sh +1 -1
- package/runtime/bin/okstra-provider-exec.py +29 -12
- package/runtime/bin/okstra-trace-cleanup.sh +58 -129
- package/runtime/prompts/lead/adapters/cmux.md +2 -0
- package/runtime/prompts/lead/okstra-lead-contract.md +1 -1
- package/runtime/prompts/lead/plan-body-verification.md +3 -3
- package/runtime/prompts/lead/report-writer.md +6 -6
- package/runtime/prompts/profiles/_common-contract.md +2 -2
- package/runtime/prompts/profiles/_implementation-executor.md +2 -0
- package/runtime/prompts/profiles/_implementation-verifier.md +2 -2
- package/runtime/prompts/profiles/error-analysis.md +1 -1
- package/runtime/prompts/profiles/implementation-planning.md +12 -9
- package/runtime/prompts/profiles/implementation.md +2 -1
- package/runtime/prompts/profiles/release-handoff.md +1 -1
- package/runtime/python/okstra_ctl/adapters/dispatch/__init__.py +1 -6
- package/runtime/python/okstra_ctl/adapters/hosts/external/relay.md +4 -4
- package/runtime/python/okstra_ctl/adapters/providers/claude/adapter.py +5 -0
- package/runtime/python/okstra_ctl/conformance.py +68 -0
- package/runtime/python/okstra_ctl/dispatch_core.py +89 -39
- package/runtime/python/okstra_ctl/dispatch_state.py +142 -14
- package/runtime/python/okstra_ctl/doctor.py +2 -2
- package/runtime/python/okstra_ctl/domain/worker_exec.py +5 -0
- package/runtime/python/okstra_ctl/final_report_schema.py +5 -4
- package/runtime/python/okstra_ctl/pane_reclaim.py +13 -22
- package/runtime/python/okstra_ctl/render_final_report.py +15 -19
- package/runtime/python/okstra_ctl/report_contract.py +0 -1
- package/runtime/python/okstra_ctl/report_finalize.py +68 -9
- package/runtime/python/okstra_ctl/run.py +43 -2
- package/runtime/python/okstra_ctl/schema_excerpt.py +1 -1
- package/runtime/python/okstra_ctl/scope_provenance.py +1 -1
- package/runtime/python/okstra_ctl/session.py +69 -12
- package/runtime/python/okstra_ctl/team.py +51 -25
- package/runtime/python/okstra_ctl/tmux.py +19 -149
- package/runtime/python/okstra_ctl/worker_request.py +2 -0
- package/runtime/python/okstra_ctl/worktree.py +69 -3
- package/runtime/python/okstra_token_usage/cli.py +1 -1
- package/runtime/python/okstra_token_usage/collect.py +66 -6
- package/runtime/skills/okstra-setup/references/project-config.md +11 -0
- package/runtime/templates/reports/settings.template.json +0 -24
- package/runtime/validators/lib/fixtures.sh +49 -17
- package/runtime/validators/validate-implementation-plan-stages.py +63 -3
- package/runtime/validators/validate-run.py +14 -473
- package/runtime/validators/validate_session_conformance.py +1 -1
- package/src/cli-registry.mjs +8 -1
- package/src/commands/execute/team.mjs +3 -3
- package/src/commands/execute/worktree-status.mjs +109 -0
- package/src/commands/lifecycle/install.mjs +0 -2
- package/src/commands/report/finalize.mjs +13 -6
- package/runtime/bin/okstra-subagent-reclaim.sh +0 -26
- package/runtime/schemas/final-report-v1.0.schema.json +0 -6366
- 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"
|
|
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)
|
|
3341
|
-
|
|
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-
|
|
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
|
-
|
|
8019
|
-
the
|
|
8020
|
-
|
|
8021
|
-
|
|
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") !=
|
|
8025
|
-
return #
|
|
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"
|
|
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"
|
package/src/cli-registry.mjs
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
18
|
-
|
|
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
|
`;
|