okstra 0.125.5 → 0.125.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json
CHANGED
package/runtime/BUILD.json
CHANGED
|
@@ -87,7 +87,7 @@ Rules (the schema enforces most of these — they are listed here so you know *w
|
|
|
87
87
|
|
|
88
88
|
- `header.reportAuthor` is `"Report writer worker"`; `header.reportOwner` is `"Claude lead"`. Set author to `"Claude lead"` only for `release-handoff` runs (single-lead by design) or a recorded report-writer dispatch failure fallback.
|
|
89
89
|
- **Source items (worker:item) preservation.** Every `consensus[].sourceItems`, `differences[].workersPosition[].itemId`, and `evidence.primary[].sourceItems` entry MUST carry the worker:item-id pair (e.g. `claude:F-001`, `codex:1.1`, `antigravity:F-3`, or `lead:mcp-1` for lead-only evidence). The schema enforces this via the `SourceItem` regex; bare worker-name lists no longer parse.
|
|
90
|
-
- **Verdict Card consistency.** `verdictCard.verdictToken`
|
|
90
|
+
- **Verdict Card consistency.** `verdictCard.verdictToken` and `verdictCard.direction` MUST byte-match `finalVerdict.verdictToken` / `.direction`; `validators/validate-run.py` diffs both and fails the run on divergence. `verdictCard.nextStep` names the same action as `finalVerdict.nextStep` and `recommendedNextSteps[0].text` but is written as the actionable command the reader runs (e.g. `/okstra-run task-key=… task-type=release-handoff`) where the other two are prose — it is deliberately not a byte copy. Duplicating the compared values across `verdictCard` and `finalVerdict` is intentional so the validator can diff them.
|
|
91
91
|
- **Reader Summary.** Populate `readerSummary` when the schema excerpt exposes it. It is the human-first entrypoint for both Markdown and HTML: one sentence for the decision, one for the human action required, one for blockers, one for audit sections safe to skip on first read, and one runnable recommended command. Do not duplicate raw evidence tables here.
|
|
92
92
|
- **§7 phase-continuation row (mandatory for non-terminal task-types).** When `header.taskType` is one of `requirements-discovery` / `implementation-planning` / `error-analysis` / `implementation` / `final-verification`, `followUpTasks` MUST contain at least one row whose `origin` is `phase-continuation`, `suggestedTaskType` equals the next phase (byte-identical to `finalVerdict.nextStep`'s referenced phase), `newTaskId` reuses the current task-id, `autoSpawn` is `"no"`, and `priority` is `"P0"`. For `release-handoff` runs, omit the phase-continuation row. Schema `allOf` clause enforces this via `contains`.
|
|
93
93
|
- **No deprecated sections.** The schema has no `4.5.8 User Approval Request` body field, no `4.5.9 Open Questions`, no `5.1 Additional Material Request`, no `5.2 User Confirmation Questions` — clarifications go under the unified `clarificationItems[]` array.
|
|
@@ -70,7 +70,7 @@ profile document.
|
|
|
70
70
|
- the same `final-report.md` file is the canonical artifact carried into the next run; the user appends answers inline before rerunning. The preferred turn-around is `scripts/okstra.sh --resume-clarification --task-key <project-id>:<task-group>:<task-id>` (opens the latest report in `$EDITOR`, then auto-reruns the same phase with `--clarification-response` carry-in). The lower-level form `--clarification-response <path>` remains available for scripted runs.
|
|
71
71
|
- if a clarification response was carried in for this run, render the conditional `## 0. Clarification Response Carried In From Previous Run` section (the template's `RENDER_IF` guard activates it), walk every `C-*` row of the prior report's `## 1. Clarification Items` table, reconcile each one against new evidence, and update its `Status` to `resolved` or `obsolete` before issuing the next decision/verdict. When no carry-in path was provided, omit the `## 0.` heading entirely — the validator fails reports that emit an empty Section 0 stub (e.g. "No prior clarification response was provided for this run.").
|
|
72
72
|
- Verdict Card (shared — applies to every final-report regardless of profile):
|
|
73
|
-
- The top-of-report `## Verdict Card` block is mandatory in every final-report. Its `Verdict Token
|
|
73
|
+
- The top-of-report `## Verdict Card` block is mandatory in every final-report. Its `Verdict Token` and `Direction` cells MUST byte-match the corresponding cells in `## 7. Final Verdict` — the card is a non-authoritative index and divergence makes the run `contract-violated`. **Enforced:** `validators/validate-run.py` `_validate_verdict_card_consistency` (token, on the rendered markdown) and `_validate_verdict_card_fields` (direction, on data.json). The `Next Step` cell points at the SAME action as `## 7. Final Verdict` and the first item of `## 3. Recommended Next Steps`, but is written as the actionable command the reader runs (`/okstra-run task-type=release-handoff`) where §7 states it as prose — it is not a byte copy and is not compared.
|
|
74
74
|
- Cross-worker traceability (shared — applies to every analysis worker output and to the lead's `## 6.` / `## 2.` tables in the final-report):
|
|
75
75
|
- **Worker-side item IDs (free-form but unique within the worker).** Every row item in sections 1–5 (and any optional section 6) of an analysis worker's output MUST carry an item ID that is unique within that one worker's result file. The ID convention is the worker's choice — `F-001` / `F-002` per the suggested schema, `1.1` / `1.2` / `1.3` as Codex tends to use, or any other shape — but it MUST appear as the leading column of the row (for table-form items) or as a `[<ID>]` prefix (for bullet/numbered items). Workers that emit findings without IDs make cross-worker reconciliation impossible.
|
|
76
76
|
- **Lead-side ID assignment + source preservation.** When the lead (or `report-writer-worker`) synthesises `## 6.1 Consensus` / `## 6.2 Differences` / `## 2.1 Primary Evidence` rows from worker outputs, the lead assigns a fresh `C-NNN` / `D-NNN` / `E-NNN` row ID. The `Source items` column (or, where the template still calls it `Supporting workers` / `Workers (position)` / `Source`, that same column) MUST list every contributing worker:item pair (e.g. `claude:F-001, codex:1.1, antigravity:F-3`) so a reviewer can trace the synthesised row back to each worker's original wording without re-reading every worker-results file. Bare worker names without item IDs (e.g. `claude, codex, antigravity`) are deprecated for these tables; the validator does not yet fail on them but the readability pass treats it as a contract violation.
|
|
@@ -1529,6 +1529,32 @@ def _validate_verdict_card_consistency(content: str, failures: list[str]) -> Non
|
|
|
1529
1529
|
)
|
|
1530
1530
|
|
|
1531
1531
|
|
|
1532
|
+
def _validate_verdict_card_fields(data: dict, failures: list[str]) -> None:
|
|
1533
|
+
"""`verdictCard.direction` must byte-match its authoritative home in §7.
|
|
1534
|
+
|
|
1535
|
+
The markdown check above compares only the Verdict Token, so a Card whose
|
|
1536
|
+
Direction contradicted §7 shipped silently. `nextStep` is deliberately NOT
|
|
1537
|
+
compared: every shipped fixture authors the Card's cell as the actionable
|
|
1538
|
+
command (`/okstra-run task-type=release-handoff`) while §7 states the same
|
|
1539
|
+
action as prose ("Proceed to release-handoff."), so a byte-match rule there
|
|
1540
|
+
would reject the reference reports.
|
|
1541
|
+
"""
|
|
1542
|
+
card = data.get("verdictCard")
|
|
1543
|
+
final = data.get("finalVerdict")
|
|
1544
|
+
if not isinstance(card, dict) or not isinstance(final, dict):
|
|
1545
|
+
return
|
|
1546
|
+
card_value = str(card.get("direction") or "").strip()
|
|
1547
|
+
final_value = str(final.get("direction") or "").strip()
|
|
1548
|
+
if not card_value or not final_value or card_value == final_value:
|
|
1549
|
+
return
|
|
1550
|
+
failures.append(
|
|
1551
|
+
f"final-report data.json: verdictCard.direction `{card_value}` does not "
|
|
1552
|
+
f"match finalVerdict.direction `{final_value}` — the Card is a "
|
|
1553
|
+
"non-authoritative index and MUST byte-match §7 "
|
|
1554
|
+
"(_common-contract.md Verdict Card)."
|
|
1555
|
+
)
|
|
1556
|
+
|
|
1557
|
+
|
|
1532
1558
|
def _load_final_report_data(report_path: Path) -> dict:
|
|
1533
1559
|
"""Best-effort parse of the final-report data.json sibling. Returns {} when
|
|
1534
1560
|
absent or unparseable — those conditions are already surfaced as failures by
|
|
@@ -1596,6 +1622,7 @@ def validate_final_report_data(
|
|
|
1596
1622
|
|
|
1597
1623
|
_validate_rationale_evidence(data, failures)
|
|
1598
1624
|
_validate_no_opaque_id_references(data, failures)
|
|
1625
|
+
_validate_verdict_card_fields(data, failures)
|
|
1599
1626
|
# Phase-agnostic: the coverage critic runs in every finding-producing phase.
|
|
1600
1627
|
_validate_unverified_critic_gaps_recorded(data, failures)
|
|
1601
1628
|
|
|
@@ -1907,6 +1934,27 @@ _PLAN_ITEM_SOURCES = (
|
|
|
1907
1934
|
)
|
|
1908
1935
|
|
|
1909
1936
|
|
|
1937
|
+
def _plan_item_source_count(planning: dict, field: str) -> int:
|
|
1938
|
+
"""Row count for one plan-body deliverable array.
|
|
1939
|
+
|
|
1940
|
+
`stepwiseExecution` has two homes: the top-level array is the legacy flat
|
|
1941
|
+
summary (schema: "Legacy flat summary kept for compatibility only. New
|
|
1942
|
+
reports use stageMap/stages") and is absent from `implementationPlanning.
|
|
1943
|
+
required`, while a current report carries the real steps per stage. Counting
|
|
1944
|
+
only the legacy field made this whole check a no-op for the largest
|
|
1945
|
+
category — a modern plan could drop every `P-Step-*` item and still pass.
|
|
1946
|
+
"""
|
|
1947
|
+
rows = [r for r in (planning.get(field) or []) if isinstance(r, dict)]
|
|
1948
|
+
if field != "stepwiseExecution":
|
|
1949
|
+
return len(rows)
|
|
1950
|
+
per_stage = sum(
|
|
1951
|
+
len([s for s in (stage.get("stepwiseExecution") or []) if isinstance(s, dict)])
|
|
1952
|
+
for stage in (planning.get("stages") or [])
|
|
1953
|
+
if isinstance(stage, dict)
|
|
1954
|
+
)
|
|
1955
|
+
return per_stage or len(rows)
|
|
1956
|
+
|
|
1957
|
+
|
|
1910
1958
|
def _validate_plan_item_extraction_completeness(
|
|
1911
1959
|
data: dict,
|
|
1912
1960
|
failures: list[str],
|
|
@@ -1934,7 +1982,7 @@ def _validate_plan_item_extraction_completeness(
|
|
|
1934
1982
|
if isinstance(it, dict)
|
|
1935
1983
|
]
|
|
1936
1984
|
for field, prefix, label in _PLAN_ITEM_SOURCES:
|
|
1937
|
-
source_count =
|
|
1985
|
+
source_count = _plan_item_source_count(ip, field)
|
|
1938
1986
|
if source_count == 0:
|
|
1939
1987
|
continue
|
|
1940
1988
|
extracted = len(
|
|
@@ -3041,6 +3089,65 @@ def _validate_forbidden_actions(
|
|
|
3041
3089
|
failures.extend(f"forbidden-action: {v}" for v in violations)
|
|
3042
3090
|
|
|
3043
3091
|
|
|
3092
|
+
CLASSIFICATION_COUNT_KEYS = {
|
|
3093
|
+
"full-consensus": "fullConsensus",
|
|
3094
|
+
"partial-consensus": "partialConsensus",
|
|
3095
|
+
"contested": "contested",
|
|
3096
|
+
"worker-unique": "workerUnique",
|
|
3097
|
+
}
|
|
3098
|
+
|
|
3099
|
+
|
|
3100
|
+
def classification_counts_from_findings(findings) -> dict:
|
|
3101
|
+
"""Recompute `finalClassificationCounts` from `findings[].classification`.
|
|
3102
|
+
|
|
3103
|
+
Shared with `tests/contract/test_convergence_state_contract.py` so the
|
|
3104
|
+
invariant has one definition — it used to live only in that test, which
|
|
3105
|
+
meant it was checked against four hand-written fixtures and never against
|
|
3106
|
+
the state a lead actually writes.
|
|
3107
|
+
"""
|
|
3108
|
+
counts = {value: 0 for value in CLASSIFICATION_COUNT_KEYS.values()}
|
|
3109
|
+
for finding in findings or []:
|
|
3110
|
+
if not isinstance(finding, dict):
|
|
3111
|
+
continue
|
|
3112
|
+
key = CLASSIFICATION_COUNT_KEYS.get(finding.get("classification"))
|
|
3113
|
+
if key:
|
|
3114
|
+
counts[key] += 1
|
|
3115
|
+
return counts
|
|
3116
|
+
|
|
3117
|
+
|
|
3118
|
+
def _validate_convergence_state_counts(run_dir, failures) -> None:
|
|
3119
|
+
"""A run's convergence state must agree with its own findings list.
|
|
3120
|
+
|
|
3121
|
+
`finalClassificationCounts` is a lead-authored summary of
|
|
3122
|
+
`findings[].classification`. Nothing recomputed it against the real
|
|
3123
|
+
artifact, so a miscount survived into the report as two different truths
|
|
3124
|
+
(observed on dev-6901 planning 009: declared partial 6 / worker-unique 4
|
|
3125
|
+
against an actual 5 / 5). Downstream readers pick one arbitrarily.
|
|
3126
|
+
"""
|
|
3127
|
+
from pathlib import Path as _Path
|
|
3128
|
+
|
|
3129
|
+
state_dir = _Path(run_dir) / "state"
|
|
3130
|
+
if not state_dir.is_dir():
|
|
3131
|
+
return
|
|
3132
|
+
for state_path in sorted(state_dir.glob("convergence-*.json")):
|
|
3133
|
+
try:
|
|
3134
|
+
state = json.loads(state_path.read_text(encoding="utf-8"))
|
|
3135
|
+
except (OSError, json.JSONDecodeError) as exc:
|
|
3136
|
+
failures.append(f"convergence state {state_path.name} unreadable: {exc}")
|
|
3137
|
+
continue
|
|
3138
|
+
declared = state.get("finalClassificationCounts")
|
|
3139
|
+
if not isinstance(declared, dict):
|
|
3140
|
+
continue
|
|
3141
|
+
actual = classification_counts_from_findings(state.get("findings"))
|
|
3142
|
+
if declared != actual:
|
|
3143
|
+
failures.append(
|
|
3144
|
+
f"convergence state {state_path.name}: finalClassificationCounts "
|
|
3145
|
+
f"{declared} does not match findings[].classification {actual}. "
|
|
3146
|
+
"The counts are a summary of the findings list — a mismatch means "
|
|
3147
|
+
"the run reports two different classifications for the same work."
|
|
3148
|
+
)
|
|
3149
|
+
|
|
3150
|
+
|
|
3044
3151
|
def _validate_requirements_discovery_fanout(run_dir, failures) -> None:
|
|
3045
3152
|
"""requirements-discovery run 에 fan-out/ 이 있으면 packet+index 를 검증해
|
|
3046
3153
|
실패를 ``requirements-discovery: `` 접두로 folding 한다. fan-out 이 없으면 no-op.
|
|
@@ -3478,6 +3585,8 @@ def main() -> int:
|
|
|
3478
3585
|
if task_type == "requirements-discovery":
|
|
3479
3586
|
run_dir = report_path.parent.parent
|
|
3480
3587
|
_validate_requirements_discovery_fanout(run_dir, failures)
|
|
3588
|
+
# Phase-agnostic: convergence runs in every finding-producing phase.
|
|
3589
|
+
_validate_convergence_state_counts(report_path.parent.parent, failures)
|
|
3481
3590
|
validate_report_views(report_path, failures)
|
|
3482
3591
|
|
|
3483
3592
|
validation_status = "passed" if not failures else "failed"
|