okstra 0.115.0 → 0.116.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/package.json +1 -1
- package/runtime/BUILD.json +2 -2
- package/runtime/prompts/launch.template.md +34 -0
- package/runtime/prompts/lead/convergence.md +2 -0
- package/runtime/prompts/lead/report-writer.md +1 -1
- package/runtime/prompts/profiles/implementation-planning.md +1 -0
- package/runtime/python/okstra_ctl/incremental_carry.py +70 -0
- package/runtime/python/okstra_ctl/incremental_scope.py +89 -0
- package/runtime/python/okstra_ctl/stage_targets.py +26 -0
- package/runtime/schemas/final-report-v1.0.schema.json +19 -0
- package/runtime/templates/reports/final-report.template.md +13 -0
- package/runtime/validators/validate-run.py +50 -0
- package/src/cli-registry.mjs +14 -0
- package/src/commands/execute/incremental-carry.mjs +20 -0
- package/src/commands/execute/incremental-scope.mjs +20 -0
package/package.json
CHANGED
package/runtime/BUILD.json
CHANGED
|
@@ -85,3 +85,37 @@ Emit one `PROGRESS: <phase-id> <verb-phrase>` line as plain user-facing text at
|
|
|
85
85
|
- Source path: `{{CLARIFICATION_RESPONSE_RELATIVE_PATH}}`
|
|
86
86
|
- If the source path above is empty, no prior clarification response was attached to this run.
|
|
87
87
|
- If the source path is set, a copy is staged at `{{INSTRUCTION_SET_RELATIVE_PATH}}/clarification-response.md`. Read it before running workers; reconcile each `C-*` row in section 1 (`## 1. Clarification Items`) of the prior report against new evidence and record the outcome in the conditional `## 0. Clarification Response Carried In From Previous Run` section of this run's final report (render that heading only when carry-in is non-empty — the validator fails empty Section 0 stubs).
|
|
88
|
+
|
|
89
|
+
### Incremental re-verification (implementation-planning clarification re-runs only)
|
|
90
|
+
|
|
91
|
+
The **default is full re-verification**. Only narrow this re-run to the impacted stages when the deterministic `okstra incremental-scope` CLI returns `mode == "incremental"`; on any doubt, stay full. This procedure fires ONLY when this run's task-type is `implementation-planning` AND a prior final report exists for this task-key (its data.json at `runs/implementation-planning/reports/final-report-implementation-planning-<prev-seq>.data.json`, where `<prev-seq>` is the most recent prior implementation-planning run's seq). For every other task-type, ignore this block and re-verify normally. This branches on the CLI's `mode` output only — it does NOT re-implement the safety logic in the prompt.
|
|
92
|
+
|
|
93
|
+
1. **Resolve the impacted stage set (safety condition C2 — your discretionary judgement).** For each answered `C-*` row you reconciled in §1, decide which **Stage Map stage numbers** (from the prior plan's `## 5.5 Stage Map`) the answer touches. Rules:
|
|
94
|
+
- Pass ONLY stage numbers that appear in that Stage Map. Never invent or guess a stage number — a number absent from the graph must never enter `--impacted` (it would leak into `reverify_stages`).
|
|
95
|
+
- If you cannot map a touched item to a real stage with confidence, leave the impacted set **EMPTY** — an empty set forces `mode == "full"`. Widening to full is always the safe choice; guessing is not.
|
|
96
|
+
- If any answer overturns the selected Option, restructures the stages, or changes the recommended approach (rather than a localized detail), also leave the impacted set **EMPTY**.
|
|
97
|
+
2. **Resolve the two base SHAs (safety condition C1 — code-unchanged, decided by the CLI, not by you).**
|
|
98
|
+
- Current base SHA: `{{EXECUTOR_WORKTREE_BASE_REF}}` (this run's resolved worktree base commit).
|
|
99
|
+
- Prior base SHA: read the prior run's active-run-context at `runs/implementation-planning/state/active-run-context-implementation-planning-<prev-seq>.json`, field `executorWorktree.baseRef` (a resolved commit SHA). The run-manifest does NOT carry this field — use the active-run-context. If that file or field cannot be located, treat the run as **full** and skip the rest of this procedure.
|
|
100
|
+
3. **Call the CLI** (it is pure — same inputs always yield the same decision):
|
|
101
|
+
```
|
|
102
|
+
okstra incremental-scope \
|
|
103
|
+
--prev-data runs/implementation-planning/reports/final-report-implementation-planning-<prev-seq>.data.json \
|
|
104
|
+
--cur-base-sha {{EXECUTOR_WORKTREE_BASE_REF}} \
|
|
105
|
+
--prev-base-sha <prior baseRef from step 2> \
|
|
106
|
+
--impacted <csv of impacted stage numbers, empty for full>
|
|
107
|
+
```
|
|
108
|
+
The CLI reads the plan's dependency graph from the top-level `## 5.5 Stage Map` (`implementationPlanning.stageMap`), which is authoritative for the impacted stage numbers — there is no per-option stage graph. The CLI prints JSON `{mode, reverify_stages, carry_stages, reason}`. Instruct the report-writer to record this JSON verbatim into this run's data.json as `implementationPlanning.incrementalDecision` (keys `mode`, `reverifyStages`, `carryStages`, `reason`) — the renderer turns it into the `### 0.1 Incremental Re-Verification Scope` audit block, and the validator fails an `incremental`-mode run whose Section 0 omits that block.
|
|
109
|
+
4. **`mode == "full"`** → run the existing full re-verification path unchanged; ignore `reverify_stages` / `carry_stages`.
|
|
110
|
+
5. **`mode == "incremental"`** → scope every worker dispatch prompt to `reverify_stages` only (the downstream closure of the impacted stages). Do NOT re-analyze `carry_stages` — their prior plan-item verdicts are carried forward verbatim (see `prompts/profiles/implementation-planning.md` "Cross-verification mode" and `prompts/lead/convergence.md` "Convergence scope").
|
|
111
|
+
6. **Merge carried-forward verdicts.** In `incremental` mode, after this run's report-writer authors its data.json, instruct it to merge the prior plan-item verdicts for `carry_stages` into it:
|
|
112
|
+
```
|
|
113
|
+
okstra incremental-carry \
|
|
114
|
+
--prev-data runs/implementation-planning/reports/final-report-implementation-planning-<prev-seq>.data.json \
|
|
115
|
+
--cur-data <this run's data.json> \
|
|
116
|
+
--prev-seq <prev-seq> \
|
|
117
|
+
--out <this run's data.json>
|
|
118
|
+
```
|
|
119
|
+
A non-zero exit (`CarryError` — schema drift between the two runs) means the carry is unsafe: fall back to **full** — discard the incremental result and re-verify every stage. Note: `verdictCard` / `finalVerdict` are NEVER carried — this run re-computes them from the re-verified plus carried plan items.
|
|
120
|
+
|
|
121
|
+
**Carry completeness (BLOCKING).** In incremental mode, this run's `planItems` MUST contain every plan-item id from the re-verified stages, each carried forward with its updated verdict. If re-verification concludes a plan item should be REMOVED, that is a signal the answer's blast radius is NOT local — abandon incremental and re-route to a FULL re-verification. The carry merge only ever ADDS prior items whose id is absent from this run; it cannot distinguish a legitimate deletion from an untouched carry, so it would resurrect a stale verdict.
|
|
@@ -66,6 +66,8 @@ Read the worker result files generated in Phase 4/5 and extract individual findi
|
|
|
66
66
|
|
|
67
67
|
**Convergence scope.** Convergence operates on sections 1–5 of the worker output (the common core, see `team-contract` "Worker Output Contract"). Section 6 ("Specialization Lens") is additive worker-specific depth and MUST NOT be fed into the consensus grouping, the verification queue, or the round-N reverify prompts. Carry Section 6 forward into the final report verbatim through the report-writer worker — do not let it inflate `unique` counts or trigger spurious `verification-error` statuses.
|
|
68
68
|
|
|
69
|
+
**Incremental re-verification scope (implementation-planning clarification re-runs).** When the lead's `okstra incremental-scope` decision is `mode == "incremental"` (procedure in `prompts/launch.template.md` §"Clarification Response Carried In"), only findings the lead attributes to a stage in `reverify_stages` enter the verification queue. Findings and plan-item verdicts carried forward for `carry_stages` are NOT re-queued — they skip the re-verification rounds entirely and are merged verbatim into this run's data.json via `okstra incremental-carry`. When the decision is `mode == "full"` (the default), every finding enters the queue as usual.
|
|
70
|
+
|
|
69
71
|
1. In the "Findings" section of each worker's results, identify individual items by number (F-001, F-002, ...) and parse the ticket identifier attached to each item:
|
|
70
72
|
- For table-form findings, read the `Ticket ID` column.
|
|
71
73
|
- For bullet/numbered findings, parse `[TICKETID: <id>]` from the item title.
|
|
@@ -281,7 +281,7 @@ Every field MUST anchor its claim with at least one evidence reference — a `pa
|
|
|
281
281
|
|
|
282
282
|
**Reader-facing prose MUST NOT cite a bare brief/worker-internal ID that this report never surfaces** — `RC-*` (reporter confirmations, defined in the brief), `RF-*` / `F-*` (findings, defined in worker-results) have no anchor in the final report, so a reader hits an opaque token with nothing to click. Either expand it inline (`the confirmed version target 1.27.47→1.27.48`) or, for an audit trail, namespace it (`claude:F-005`). **Enforced:** `_validate_no_opaque_id_references` fails a bare `RC-*` / `RF-*` / non-namespaced `F-*` appearing in `verdictCard` / `finalVerdict` / `rationale` / `clarificationItems[].statement`. (The renderer anchors + links in-report IDs of any digit width, so a surfaced `RC-4`-style id does resolve.) Do NOT restate the Verdict Card or the §5.4 trade-off matrix verbatim — this section is the *why*, in connected prose, that those tables compress.
|
|
283
283
|
|
|
284
|
-
0. **Clarification Response Carried In** — render this `## 0.` heading ONLY when `{{CLARIFICATION_RESPONSE_RELATIVE_PATH}}` is non-empty. Walk every `C-*` row of the prior report's `## 1. Clarification Items` table, reconcile against new evidence, and record the outcome (`resolved` / `obsolete`) with citation before drafting the verdict. When no carry-in path was provided, OMIT the `## 0.` heading entirely — the validator fails an empty Section 0 stub.
|
|
284
|
+
0. **Clarification Response Carried In** — render this `## 0.` heading ONLY when `{{CLARIFICATION_RESPONSE_RELATIVE_PATH}}` is non-empty. Walk every `C-*` row of the prior report's `## 1. Clarification Items` table, reconcile against new evidence, and record the outcome (`resolved` / `obsolete`) with citation before drafting the verdict. When no carry-in path was provided, OMIT the `## 0.` heading entirely — the validator fails an empty Section 0 stub. When the lead ran `okstra incremental-scope` for this re-run, record its JSON verbatim into `implementationPlanning.incrementalDecision` (`mode`, `reverifyStages`, `carryStages`, `reason`); the renderer emits the `### 0.1 Incremental Re-Verification Scope` audit block from it, and the validator fails an `incremental`-mode run whose Section 0 omits that block. In `incremental` mode this run's `planItems` MUST carry every plan-item id from the re-verified stages forward with its updated verdict; if re-verification concludes a plan item should be REMOVED, that is a signal the answer's blast radius is not local — do not drop it here, tell the lead to abandon incremental and re-route to a FULL re-verification, because the carry merge only adds prior items and would resurrect the removed item's stale verdict.
|
|
285
285
|
1. **Cross Verification Results** — 4 categories (Full / Partial / Contested / Worker-Unique) when convergence is enabled, per `convergence`. Prepend the Round History sub-table (columns: `Round | inputQueueSize | resolvedCount | carriedForwardCount | dispatches | skippedWorkers`) plus a `round2SkippedReason: <value>` note, pulled verbatim from `convergence-<task-type>-<seq>.json`. Empty contested list renders as `- 합의 미달 항목 없음.`. Convergence-disabled runs use the legacy Consensus/Differences format and omit the round table.
|
|
286
286
|
2. **Final Verdict** — `Direction` ∈ `continue-investigation` / `begin-implementation` / `approve` / `reject` / `hold`. **Verdict Token** is `not-applicable` for every task-type except `final-verification` — see "Final-verification verdict token contract" below for that case.
|
|
287
287
|
3. **Evidence and Detailed Analysis** — primary evidence rows (file path, line, snippet); secondary evidence / alternate interpretations. If `reference-expectations.md` lists explicit expected values, record match/gap per row.
|
|
@@ -43,6 +43,7 @@
|
|
|
43
43
|
- Cross-verification mode:
|
|
44
44
|
- Phase 5.5 finding convergence runs in **adversarial mode** for this phase (`convergence.adversarial=true`). Verifiers actively try to refute each worker finding (requirement gap / risk / option) by re-inspecting its cited evidence; the burden of proof sits on the claim. See `prompts/lead/convergence.md` §"Adversarial Verification Mode".
|
|
45
45
|
- §5.5.9 plan-body verification runs with an **adversarial posture** (`prompts/lead/plan-body-verification.md` §"Adversarial plan-body posture"): verifiers open and confirm every cited path / command and put the burden of proof on the plan. The gate threshold is majority-based for kinds `b`/`c`/`e`, but a single `DISAGREE` blocks on its own for the concrete, safety-critical kinds `a` (path/symbol mismatch) / `d` (rollback order) — and `f` on `P-Req-*` items. A majority also needs ≥2 participating votes, so a lone dissent whose peer returned a non-result does not block on a majority-gated kind (see that contract's §"Adversarial plan-body posture").
|
|
46
|
+
- **Incremental re-verification scope (clarification re-runs):** when the lead's `okstra incremental-scope` decision is `mode == "incremental"` (procedure in `prompts/launch.template.md` §"Clarification Response Carried In"), workers re-analyze ONLY the stages listed in `reverify_stages` (the downstream closure of the impacted stages). Workers MUST NOT re-open, re-score, or re-judge any stage in `carry_stages` — those stages' prior plan-item verdicts are carried forward verbatim, and a worker never overwrites a carried verdict with its own judgement. When the decision is `mode == "full"` (the default), every stage is re-analyzed as usual.
|
|
46
47
|
{{INCLUDE:_coverage-critic.md}}
|
|
47
48
|
- Non-goals:
|
|
48
49
|
- code-level micro-optimization unless it changes the implementation approach
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"""Merge carried-forward plan-item verdicts from the prior run into the
|
|
2
|
+
current incremental re-run's data.json. Plan items the current run did not
|
|
3
|
+
re-verify keep their prior verdict, tagged so the report shows they are
|
|
4
|
+
carried forward and unchanged.
|
|
5
|
+
"""
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import argparse
|
|
9
|
+
import json
|
|
10
|
+
import sys
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class CarryError(Exception):
|
|
15
|
+
"""Carry merge refused — schema drift or structural mismatch."""
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def merge_carried_forward(prev: dict, cur: dict, prev_seq: str = "previous") -> dict:
|
|
19
|
+
if prev.get("schemaVersion") != cur.get("schemaVersion"):
|
|
20
|
+
raise CarryError(
|
|
21
|
+
f"schemaVersion drift {prev.get('schemaVersion')!r} != {cur.get('schemaVersion')!r}; "
|
|
22
|
+
"cannot carry forward — caller must fall back to full"
|
|
23
|
+
)
|
|
24
|
+
prev_pbv = prev.get("implementationPlanning", {}).get("planBodyVerification", {})
|
|
25
|
+
cur_ip = cur.setdefault("implementationPlanning", {})
|
|
26
|
+
cur_pbv = cur_ip.setdefault("planBodyVerification", {})
|
|
27
|
+
cur_items = cur_pbv.setdefault("planItems", [])
|
|
28
|
+
reverified_ids = {i["id"] for i in cur_items}
|
|
29
|
+
for item in prev_pbv.get("planItems", []):
|
|
30
|
+
if item["id"] in reverified_ids:
|
|
31
|
+
continue
|
|
32
|
+
carried = dict(item)
|
|
33
|
+
carried["carriedForwardFromSeq"] = str(prev_seq)
|
|
34
|
+
cur_items.append(carried)
|
|
35
|
+
cur_items.sort(key=lambda i: i["id"])
|
|
36
|
+
return cur
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def main(argv: list[str]) -> int:
|
|
40
|
+
ap = argparse.ArgumentParser(prog="okstra incremental-carry")
|
|
41
|
+
ap.add_argument("--prev-data", required=True, help="prior run final-report data.json")
|
|
42
|
+
ap.add_argument("--cur-data", required=True, help="current incremental re-run data.json")
|
|
43
|
+
ap.add_argument("--prev-seq", required=True, help="prior run seq, tagged onto carried items")
|
|
44
|
+
ap.add_argument("--out", required=True, help="path to write the merged data.json")
|
|
45
|
+
args = ap.parse_args(argv)
|
|
46
|
+
|
|
47
|
+
prev = json.loads(Path(args.prev_data).read_text(encoding="utf-8"))
|
|
48
|
+
cur = json.loads(Path(args.cur_data).read_text(encoding="utf-8"))
|
|
49
|
+
try:
|
|
50
|
+
merged = merge_carried_forward(prev, cur, prev_seq=args.prev_seq)
|
|
51
|
+
except CarryError as exc:
|
|
52
|
+
print(f"carry refused: {exc}", file=sys.stderr)
|
|
53
|
+
return 1
|
|
54
|
+
|
|
55
|
+
Path(args.out).write_text(
|
|
56
|
+
json.dumps(merged, ensure_ascii=False, indent=2) + "\n", encoding="utf-8"
|
|
57
|
+
)
|
|
58
|
+
carried = sum(
|
|
59
|
+
1
|
|
60
|
+
for i in merged.get("implementationPlanning", {})
|
|
61
|
+
.get("planBodyVerification", {})
|
|
62
|
+
.get("planItems", [])
|
|
63
|
+
if i.get("carriedForwardFromSeq") == str(args.prev_seq)
|
|
64
|
+
)
|
|
65
|
+
print(f"merged {carried} carried-forward plan item(s) from seq {args.prev_seq} -> {args.out}")
|
|
66
|
+
return 0
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
if __name__ == "__main__":
|
|
70
|
+
raise SystemExit(main(sys.argv[1:]))
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
"""Decide whether an implementation-planning clarification re-run can be
|
|
2
|
+
incremental, and if so which stages to re-verify vs carry forward.
|
|
3
|
+
|
|
4
|
+
Deterministic half of the incremental-reverification feature: the lead
|
|
5
|
+
prompt supplies the impacted-stage set (its discretionary C2 judgement) and
|
|
6
|
+
the base SHAs; everything here is pure so the same inputs always yield the
|
|
7
|
+
same decision. See docs/superpowers/specs/2026-07-09-incremental-clarification-reverification-design.md.
|
|
8
|
+
"""
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import argparse
|
|
12
|
+
import json
|
|
13
|
+
import sys
|
|
14
|
+
from dataclasses import asdict, dataclass
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
|
|
17
|
+
from okstra_ctl.stage_targets import downstream_stage_closure
|
|
18
|
+
|
|
19
|
+
CUTOFF_RATIO = 0.5
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@dataclass
|
|
23
|
+
class IncrementalDecision:
|
|
24
|
+
mode: str # "incremental" | "full"
|
|
25
|
+
reverify_stages: list[int]
|
|
26
|
+
carry_stages: list[int]
|
|
27
|
+
reason: str
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _parse_depends_on(cell: str) -> list[int]:
|
|
31
|
+
text = (cell or "").strip()
|
|
32
|
+
if not text or text == "(none)":
|
|
33
|
+
return []
|
|
34
|
+
return [int(tok.strip()) for tok in text.split(",") if tok.strip()]
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def parse_stage_graph(data: dict) -> list[tuple[int, list[int]]]:
|
|
38
|
+
stage_map = data.get("implementationPlanning", {}).get("stageMap", [])
|
|
39
|
+
return [(int(row["stage"]), _parse_depends_on(row.get("dependsOn", ""))) for row in stage_map]
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def decide_scope(
|
|
43
|
+
*,
|
|
44
|
+
stages: list[tuple[int, list[int]]],
|
|
45
|
+
impacted_stages: set[int],
|
|
46
|
+
prev_base_sha: str,
|
|
47
|
+
cur_base_sha: str,
|
|
48
|
+
cutoff_ratio: float = CUTOFF_RATIO,
|
|
49
|
+
) -> IncrementalDecision:
|
|
50
|
+
if not prev_base_sha or prev_base_sha != cur_base_sha:
|
|
51
|
+
return IncrementalDecision(
|
|
52
|
+
"full", [], [],
|
|
53
|
+
f"base-ref changed ({prev_base_sha!r} -> {cur_base_sha!r}); prior analysis invalid",
|
|
54
|
+
)
|
|
55
|
+
if not impacted_stages:
|
|
56
|
+
return IncrementalDecision("full", [], [], "no impacted stages resolved; falling back to full")
|
|
57
|
+
all_stages = {num for num, _ in stages}
|
|
58
|
+
closure = downstream_stage_closure(stages, set(impacted_stages))
|
|
59
|
+
if len(closure) * 2 > len(all_stages):
|
|
60
|
+
return IncrementalDecision(
|
|
61
|
+
"full", [], [],
|
|
62
|
+
f"closure {len(closure)}/{len(all_stages)} exceeds {int(cutoff_ratio * 100)}% cutoff",
|
|
63
|
+
)
|
|
64
|
+
reverify = sorted(closure)
|
|
65
|
+
carry = sorted(all_stages - closure)
|
|
66
|
+
return IncrementalDecision("incremental", reverify, carry, f"closure {reverify} within cutoff")
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def main(argv: list[str]) -> int:
|
|
70
|
+
ap = argparse.ArgumentParser(prog="okstra incremental-scope")
|
|
71
|
+
ap.add_argument("--prev-data", required=True, help="prior run final-report data.json")
|
|
72
|
+
ap.add_argument("--cur-base-sha", required=True)
|
|
73
|
+
ap.add_argument("--prev-base-sha", required=True)
|
|
74
|
+
ap.add_argument("--impacted", default="", help="comma-separated impacted stage numbers")
|
|
75
|
+
args = ap.parse_args(argv)
|
|
76
|
+
|
|
77
|
+
data = json.loads(Path(args.prev_data).read_text(encoding="utf-8"))
|
|
78
|
+
stages = parse_stage_graph(data)
|
|
79
|
+
impacted = {int(t.strip()) for t in args.impacted.split(",") if t.strip()}
|
|
80
|
+
decision = decide_scope(
|
|
81
|
+
stages=stages, impacted_stages=impacted,
|
|
82
|
+
prev_base_sha=args.prev_base_sha, cur_base_sha=args.cur_base_sha,
|
|
83
|
+
)
|
|
84
|
+
print(json.dumps(asdict(decision), ensure_ascii=False))
|
|
85
|
+
return 0
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
if __name__ == "__main__":
|
|
89
|
+
raise SystemExit(main(sys.argv[1:]))
|
|
@@ -306,6 +306,32 @@ def order_stage_closure(
|
|
|
306
306
|
return out
|
|
307
307
|
|
|
308
308
|
|
|
309
|
+
def downstream_stage_closure(
|
|
310
|
+
stages: list[tuple[int, list[int]]],
|
|
311
|
+
seed: set[int],
|
|
312
|
+
) -> set[int]:
|
|
313
|
+
"""Stages that (transitively) depend on any stage in ``seed``, plus ``seed``.
|
|
314
|
+
|
|
315
|
+
``stages`` mirrors ``order_stage_closure``: ``(stage_number, [depends_on_numbers])``.
|
|
316
|
+
``order_stage_closure`` walks dependencies (upstream); this walks dependents
|
|
317
|
+
(downstream) — the reverse edge — so an answered clarification that changes a
|
|
318
|
+
seed stage re-verifies everything built on top of it.
|
|
319
|
+
"""
|
|
320
|
+
dependents: dict[int, list[int]] = {}
|
|
321
|
+
for num, deps in stages:
|
|
322
|
+
for dep in deps:
|
|
323
|
+
dependents.setdefault(dep, []).append(num)
|
|
324
|
+
result = set(seed)
|
|
325
|
+
queue = list(seed)
|
|
326
|
+
while queue:
|
|
327
|
+
cur = queue.pop()
|
|
328
|
+
for child in dependents.get(cur, []):
|
|
329
|
+
if child not in result:
|
|
330
|
+
result.add(child)
|
|
331
|
+
queue.append(child)
|
|
332
|
+
return result
|
|
333
|
+
|
|
334
|
+
|
|
309
335
|
def commit_is_ancestor(project_root: Path, ancestor: str, descendant: str) -> bool:
|
|
310
336
|
"""True iff ``ancestor`` is an ancestor of ``descendant`` in git history."""
|
|
311
337
|
from .worktree import is_ancestor
|
|
@@ -445,6 +445,24 @@
|
|
|
445
445
|
"skippedAdrCandidates": {
|
|
446
446
|
"type": "array",
|
|
447
447
|
"items": { "$ref": "#/$defs/SkippedAdrCandidate" }
|
|
448
|
+
},
|
|
449
|
+
"incrementalDecision": {
|
|
450
|
+
"type": "object",
|
|
451
|
+
"description": "Verbatim `okstra incremental-scope` output for a clarification re-run. Present only when this implementation-planning run scoped itself against a prior run; drives the Section 0 incremental audit block.",
|
|
452
|
+
"required": ["mode", "reverifyStages", "carryStages", "reason"],
|
|
453
|
+
"additionalProperties": false,
|
|
454
|
+
"properties": {
|
|
455
|
+
"mode": { "type": "string", "enum": ["incremental", "full"] },
|
|
456
|
+
"reverifyStages": {
|
|
457
|
+
"type": "array",
|
|
458
|
+
"items": { "type": "integer" }
|
|
459
|
+
},
|
|
460
|
+
"carryStages": {
|
|
461
|
+
"type": "array",
|
|
462
|
+
"items": { "type": "integer" }
|
|
463
|
+
},
|
|
464
|
+
"reason": { "type": "string", "minLength": 1 }
|
|
465
|
+
}
|
|
448
466
|
}
|
|
449
467
|
}
|
|
450
468
|
},
|
|
@@ -1500,6 +1518,7 @@
|
|
|
1500
1518
|
"id": { "type": "string", "minLength": 1 },
|
|
1501
1519
|
"subject": { "type": "string", "minLength": 1 },
|
|
1502
1520
|
"sourceSection": { "type": "string" },
|
|
1521
|
+
"carriedForwardFromSeq": { "type": "string" },
|
|
1503
1522
|
"clarificationId": {
|
|
1504
1523
|
"anyOf": [
|
|
1505
1524
|
{ "type": "null" },
|
|
@@ -43,6 +43,19 @@ implementation-option: {{ frontmatter.implementationOption | yaml_scalar }}
|
|
|
43
43
|
{{ t("sectionIntro.clarificationCarryIn") }}
|
|
44
44
|
|
|
45
45
|
- Source file: `{{ clarificationCarryIn.sourceFile }}`
|
|
46
|
+
{% if implementationPlanning and implementationPlanning.incrementalDecision %}
|
|
47
|
+
{% set incrementalDecision = implementationPlanning.incrementalDecision %}
|
|
48
|
+
### 0.1 Incremental Re-Verification Scope
|
|
49
|
+
|
|
50
|
+
- Decision mode: `{{ incrementalDecision.mode }}`
|
|
51
|
+
- Reason: {{ incrementalDecision.reason }}
|
|
52
|
+
{% if incrementalDecision.mode == 'incremental' %}
|
|
53
|
+
- Re-verified stages: {% for s in incrementalDecision.reverifyStages %}`stage-{{ s }}`{% if not loop.last %}, {% endif %}{% else %}(none){% endfor +%}
|
|
54
|
+
- Carried-forward stages: {% for s in incrementalDecision.carryStages %}`stage-{{ s }}` carried-forward (unchanged){% if not loop.last %}, {% endif %}{% else %}(none){% endfor +%}
|
|
55
|
+
|
|
56
|
+
Carried-forward plan items retain their prior verdicts verbatim; each such item carries a `carriedForwardFromSeq` tag pointing at the run it was verified in.
|
|
57
|
+
{% endif %}
|
|
58
|
+
{% endif %}
|
|
46
59
|
|
|
47
60
|
{% endif %}
|
|
48
61
|
## Summary of the Problem or Verification Target
|
|
@@ -744,6 +744,14 @@ _EMPTY_CARRY_IN_SOURCE_RE = re.compile(
|
|
|
744
744
|
re.MULTILINE,
|
|
745
745
|
)
|
|
746
746
|
|
|
747
|
+
# Section 0 incremental audit sub-block. When the data.json records an
|
|
748
|
+
# `implementationPlanning.incrementalDecision` with `mode == "incremental"`,
|
|
749
|
+
# the rendered report MUST expose the decision so a reader can audit which
|
|
750
|
+
# stages were re-verified vs carried forward unchanged.
|
|
751
|
+
_INCREMENTAL_AUDIT_HEADING_RE = re.compile(
|
|
752
|
+
r"^###[ \t]+0\.1[ \t]+Incremental Re-Verification Scope\b", re.MULTILINE
|
|
753
|
+
)
|
|
754
|
+
|
|
747
755
|
# Deprecated section headings removed by the report-format readability
|
|
748
756
|
# pass. Each entry is (regex, human-readable remedy). The regexes are
|
|
749
757
|
# line-anchored to avoid false positives from inline references in prose
|
|
@@ -971,6 +979,44 @@ def _validate_conformance(report_path: Path, failures: list[str],
|
|
|
971
979
|
)
|
|
972
980
|
|
|
973
981
|
|
|
982
|
+
def _check_incremental_audit_block(
|
|
983
|
+
report_path: Path, content: str, failures: list[str]
|
|
984
|
+
) -> None:
|
|
985
|
+
"""Enforce the Section 0 incremental audit block.
|
|
986
|
+
|
|
987
|
+
When the data.json records an `implementationPlanning.incrementalDecision`
|
|
988
|
+
with `mode == "incremental"`, the rendered markdown MUST carry the
|
|
989
|
+
`### 0.1 Incremental Re-Verification Scope` block naming the re-verified
|
|
990
|
+
and carried-forward stages — otherwise the narrowed re-run is silently
|
|
991
|
+
un-auditable. Non-incremental (`full`) runs and reports without the
|
|
992
|
+
decision are exempt, so this never conflicts with the empty-Section-0
|
|
993
|
+
stub rule (that fires only when NO carry-in was provided at all).
|
|
994
|
+
"""
|
|
995
|
+
data = _load_final_report_data(report_path)
|
|
996
|
+
planning = data.get("implementationPlanning")
|
|
997
|
+
if not isinstance(planning, dict):
|
|
998
|
+
return
|
|
999
|
+
decision = planning.get("incrementalDecision")
|
|
1000
|
+
if not isinstance(decision, dict) or decision.get("mode") != "incremental":
|
|
1001
|
+
return
|
|
1002
|
+
if _INCREMENTAL_AUDIT_HEADING_RE.search(content) is None:
|
|
1003
|
+
failures.append(
|
|
1004
|
+
"final report data.json records an `incremental`-mode "
|
|
1005
|
+
"implementationPlanning.incrementalDecision, but the markdown is "
|
|
1006
|
+
"missing the `### 0.1 Incremental Re-Verification Scope` audit "
|
|
1007
|
+
"block under Section 0. Re-render from the data.json so the "
|
|
1008
|
+
"re-verified / carried-forward stages are visible."
|
|
1009
|
+
)
|
|
1010
|
+
return
|
|
1011
|
+
for needle in ("Re-verified stages", "Carried-forward stages"):
|
|
1012
|
+
if needle not in content:
|
|
1013
|
+
failures.append(
|
|
1014
|
+
f"final report's Section 0 incremental audit block is missing "
|
|
1015
|
+
f"the `{needle}` line — an `incremental`-mode run must list "
|
|
1016
|
+
"both the re-verified and the carried-forward stages."
|
|
1017
|
+
)
|
|
1018
|
+
|
|
1019
|
+
|
|
974
1020
|
def validate_report(
|
|
975
1021
|
report_path: Path,
|
|
976
1022
|
required_agent_status_entries: list[str],
|
|
@@ -1064,6 +1110,10 @@ def validate_report(
|
|
|
1064
1110
|
"and body — do NOT emit a placeholder stub."
|
|
1065
1111
|
)
|
|
1066
1112
|
|
|
1113
|
+
# Incremental audit block — an `incremental`-mode re-run must expose its
|
|
1114
|
+
# scope decision in Section 0 so the narrowed re-verification is auditable.
|
|
1115
|
+
_check_incremental_audit_block(report_path, content, failures)
|
|
1116
|
+
|
|
1067
1117
|
# Deprecated section headings — pre-1.0 hard removal.
|
|
1068
1118
|
for pattern, remedy in _DEPRECATED_FINAL_REPORT_PATTERNS:
|
|
1069
1119
|
if pattern.search(content) is not None:
|
package/src/cli-registry.mjs
CHANGED
|
@@ -305,6 +305,20 @@ export const COMMAND_REGISTRY = [
|
|
|
305
305
|
category: "introspection",
|
|
306
306
|
summary: ["Append run error events to the run error log"],
|
|
307
307
|
},
|
|
308
|
+
{
|
|
309
|
+
name: "incremental-scope",
|
|
310
|
+
module: "./commands/execute/incremental-scope.mjs",
|
|
311
|
+
export: "run",
|
|
312
|
+
category: "introspection",
|
|
313
|
+
summary: ["Decide re-verify vs carry-forward scope for a clarification re-run"],
|
|
314
|
+
},
|
|
315
|
+
{
|
|
316
|
+
name: "incremental-carry",
|
|
317
|
+
module: "./commands/execute/incremental-carry.mjs",
|
|
318
|
+
export: "run",
|
|
319
|
+
category: "introspection",
|
|
320
|
+
summary: ["Merge carried-forward plan-item verdicts into an incremental re-run"],
|
|
321
|
+
},
|
|
308
322
|
{
|
|
309
323
|
name: "memory",
|
|
310
324
|
module: "./commands/memory/memory.mjs",
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { runInstalledScript } from "../../lib/python-helper.mjs";
|
|
2
|
+
|
|
3
|
+
const USAGE = `okstra incremental-carry — merge carried-forward plan-item verdicts into an incremental re-run's data.json
|
|
4
|
+
|
|
5
|
+
Wraps the python helper (\`okstra-incremental-carry.py\`) installed under
|
|
6
|
+
\`~/.okstra/bin/\` so the report-writer worker calls \`okstra incremental-carry\`
|
|
7
|
+
instead of emitting a \`python3 "$HOME/..."\` invocation (which breaks
|
|
8
|
+
\`Bash(okstra:*)\` permission matching and prompts on every call).
|
|
9
|
+
|
|
10
|
+
Usage:
|
|
11
|
+
okstra incremental-carry --prev-data <path> --cur-data <path> \\
|
|
12
|
+
--prev-seq <str> [--carry-stages <csv>] --out <path>
|
|
13
|
+
|
|
14
|
+
Writes the merged data.json to --out. Exits non-zero on schema drift
|
|
15
|
+
(\`CarryError\`) so the caller's shell can fall back to a full re-run.
|
|
16
|
+
`;
|
|
17
|
+
|
|
18
|
+
export async function run(args) {
|
|
19
|
+
return runInstalledScript({ scriptName: "okstra-incremental-carry.py", args, usage: USAGE });
|
|
20
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { runInstalledScript } from "../../lib/python-helper.mjs";
|
|
2
|
+
|
|
3
|
+
const USAGE = `okstra incremental-scope — decide the re-verify vs carry-forward scope for a clarification re-run
|
|
4
|
+
|
|
5
|
+
Wraps the python helper (\`okstra-incremental-scope.py\`) installed under
|
|
6
|
+
\`~/.okstra/bin/\` so the lead prompt calls \`okstra incremental-scope\`
|
|
7
|
+
instead of emitting a \`python3 "$HOME/..."\` invocation (which breaks
|
|
8
|
+
\`Bash(okstra:*)\` permission matching and prompts on every call).
|
|
9
|
+
|
|
10
|
+
Usage:
|
|
11
|
+
okstra incremental-scope --prev-data <path> --cur-base-sha <sha> \\
|
|
12
|
+
--prev-base-sha <sha> [--impacted <csv>] [--option <name>]
|
|
13
|
+
|
|
14
|
+
Prints a JSON decision {mode, reverify_stages, carry_stages, reason} to stdout.
|
|
15
|
+
All arguments are forwarded verbatim to the python helper.
|
|
16
|
+
`;
|
|
17
|
+
|
|
18
|
+
export async function run(args) {
|
|
19
|
+
return runInstalledScript({ scriptName: "okstra-incremental-scope.py", args, usage: USAGE });
|
|
20
|
+
}
|