okstra 0.225.1 → 0.226.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/dist/cli-registry.mjs +6 -0
- package/dist/cli-registry.mjs.map +1 -1
- package/dist/commands/execute/render-bundle.mjs +5 -0
- package/dist/commands/execute/render-bundle.mjs.map +1 -1
- package/docs/cli.md +17 -1
- package/docs/project-structure-overview.md +2 -0
- package/package.json +1 -1
- package/runtime/BUILD.json +2 -2
- package/runtime/bin/lib/okstra/cli.sh +5 -1
- package/runtime/bin/lib/okstra/globals.sh +1 -0
- package/runtime/bin/lib/okstra/usage.sh +3 -0
- package/runtime/bin/okstra.sh +2 -0
- package/runtime/prompts/duties/technical-verification-worker.json +2 -1
- package/runtime/prompts/lead/convergence.md +4 -1
- package/runtime/prompts/lead/phase-routing.md +7 -3
- package/runtime/prompts/lead/team-contract.md +1 -1
- package/runtime/prompts/wizard/prompts.ko.json +15 -0
- package/runtime/python/okstra_ctl/adapters/providers/codex/adapter.py +5 -0
- package/runtime/python/okstra_ctl/analysis_packet.py +108 -2
- package/runtime/python/okstra_ctl/approved_plan_stage_extract.py +206 -0
- package/runtime/python/okstra_ctl/brief_sources.py +200 -0
- package/runtime/python/okstra_ctl/business_flow/store.py +15 -2
- package/runtime/python/okstra_ctl/cmux.py +18 -3
- package/runtime/python/okstra_ctl/convergence_critic_prompt.py +4 -1
- package/runtime/python/okstra_ctl/design_surfaces.py +19 -7
- package/runtime/python/okstra_ctl/execution_mutation_audit.py +20 -18
- package/runtime/python/okstra_ctl/implementation_direction.py +10 -0
- package/runtime/python/okstra_ctl/initial_prompt_materialization.py +41 -0
- package/runtime/python/okstra_ctl/manager_split.py +185 -27
- package/runtime/python/okstra_ctl/next_phase.py +3 -1
- package/runtime/python/okstra_ctl/phases/final_verification/prior_blockers.py +128 -0
- package/runtime/python/okstra_ctl/phases/final_verification/profile.md +8 -5
- package/runtime/python/okstra_ctl/phases/final_verification/report_assets/final-verification.template.html +1 -1
- package/runtime/python/okstra_ctl/phases/final_verification/validation.py +84 -0
- package/runtime/python/okstra_ctl/phases/implementation/instructions/_implementation-executor.md +1 -1
- package/runtime/python/okstra_ctl/phases/implementation_option_selection/authoring.py +13 -5
- package/runtime/python/okstra_ctl/phases/implementation_option_selection/boundary.json +1 -1
- package/runtime/python/okstra_ctl/phases/implementation_option_selection/entry.py +67 -0
- package/runtime/python/okstra_ctl/phases/implementation_option_selection/profile.md +4 -2
- package/runtime/python/okstra_ctl/phases/implementation_option_selection/report_assets/implementation-option-selection.template.html +2 -1
- package/runtime/python/okstra_ctl/phases/implementation_option_selection/validation.py +127 -18
- package/runtime/python/okstra_ctl/phases/implementation_planning/profile.md +2 -1
- package/runtime/python/okstra_ctl/phases/technical_verification/profile.md +2 -2
- package/runtime/python/okstra_ctl/report_assembly.py +23 -2
- package/runtime/python/okstra_ctl/report_views.py +2 -1
- package/runtime/python/okstra_ctl/run.py +46 -1
- package/runtime/python/okstra_ctl/run_context.py +13 -8
- package/runtime/python/okstra_ctl/wizard/confirmation.py +6 -0
- package/runtime/python/okstra_ctl/wizard/ids.py +6 -0
- package/runtime/python/okstra_ctl/wizard/registry.py +21 -0
- package/runtime/python/okstra_ctl/wizard/render.py +4 -0
- package/runtime/python/okstra_ctl/wizard/sources.py +33 -11
- package/runtime/python/okstra_ctl/wizard/state.py +4 -0
- package/runtime/python/okstra_ctl/wizard/steps_options.py +74 -9
- package/runtime/python/okstra_ctl/worker_prompt_body.py +21 -1
- package/runtime/python/okstra_ctl/worker_prompt_headers.py +11 -4
- package/runtime/python/okstra_ctl/worker_prompt_policy.py +3 -0
- package/runtime/schemas/final-report-v2.0.schema.json +74 -0
- package/runtime/schemas/final-report-v3.0.schema.json +99 -2
- package/runtime/skills/okstra-brief-gen/SKILL.md +26 -0
- package/runtime/skills/okstra-manager/SKILL.md +15 -2
- package/runtime/templates/reports/brief.template.md +10 -1
- package/runtime/templates/reports/html/i18n/en.json +8 -0
- package/runtime/templates/reports/html/i18n/ko.json +8 -0
- package/runtime/templates/worker-prompt-preamble.md +1 -1
- package/runtime/validators/validate-brief.py +39 -1
- package/runtime/validators/validate-run.py +5 -0
|
@@ -20,6 +20,8 @@ def validate_final_verification_content(data: dict, failures: list[str]) -> None
|
|
|
20
20
|
if (data.get("header") or {}).get("taskType") != "final-verification":
|
|
21
21
|
return
|
|
22
22
|
_validate_added_surface_audit(data, failures)
|
|
23
|
+
_validate_defect_class_sweep(data, failures)
|
|
24
|
+
_validate_recurring_class_routing(data, failures)
|
|
23
25
|
_validate_verdict_consistency(data, failures)
|
|
24
26
|
_validate_verification_scope(data, failures)
|
|
25
27
|
|
|
@@ -97,6 +99,88 @@ def _added_surface_row_failures(
|
|
|
97
99
|
return []
|
|
98
100
|
|
|
99
101
|
|
|
102
|
+
def _acceptance_blockers(data: dict) -> list[Mapping]:
|
|
103
|
+
rows = (data.get("finalVerification") or {}).get("acceptanceBlockers") or []
|
|
104
|
+
return [row for row in rows if isinstance(row, Mapping)]
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def _validate_defect_class_sweep(data: dict, failures: list[str]) -> None:
|
|
108
|
+
"""차단 사유마다 그것이 속한 결함 계열을 위치×경우 칸 단위로 훑었는지 본다.
|
|
109
|
+
|
|
110
|
+
위치 하나만 적은 차단 사유는 다음 run 이 같은 원리의 다른 위치나 다른 경우를
|
|
111
|
+
새로 찾게 만들었다. 실측: 한 계열이 final-verification 네 번에 걸쳐 하나씩
|
|
112
|
+
드러났다(dev-11562 stage 1, 001-004).
|
|
113
|
+
"""
|
|
114
|
+
for row in _acceptance_blockers(data):
|
|
115
|
+
row_id = str(row.get("id") or "<id 없음>")
|
|
116
|
+
defect_class = row.get("defectClass")
|
|
117
|
+
if not isinstance(defect_class, Mapping):
|
|
118
|
+
failures.append(
|
|
119
|
+
f"final-verification: acceptance blocker {row_id} has no "
|
|
120
|
+
"`defectClass` — name the mechanism and sweep every location "
|
|
121
|
+
"and case that shares it before reporting the blocker."
|
|
122
|
+
)
|
|
123
|
+
continue
|
|
124
|
+
cells = [
|
|
125
|
+
cell for cell in defect_class.get("cells") or []
|
|
126
|
+
if isinstance(cell, Mapping)
|
|
127
|
+
]
|
|
128
|
+
defective = [cell for cell in cells if cell.get("verdict") == "defective"]
|
|
129
|
+
if not defective:
|
|
130
|
+
failures.append(
|
|
131
|
+
f"final-verification: acceptance blocker {row_id} sweeps no "
|
|
132
|
+
"`defective` cell — the blocker itself is one, so record it."
|
|
133
|
+
)
|
|
134
|
+
untested = [
|
|
135
|
+
f"{cell.get('location')} / {cell.get('case')}"
|
|
136
|
+
for cell in defective
|
|
137
|
+
if not str(cell.get("regressionTest") or "").strip()
|
|
138
|
+
]
|
|
139
|
+
if untested:
|
|
140
|
+
failures.append(
|
|
141
|
+
f"final-verification: acceptance blocker {row_id} has "
|
|
142
|
+
f"`defective` cells with no `regressionTest`: {untested} — the "
|
|
143
|
+
"fix covers the whole class, one regression test per cell."
|
|
144
|
+
)
|
|
145
|
+
if (
|
|
146
|
+
defect_class.get("recurrence") == "recurring"
|
|
147
|
+
and not defect_class.get("priorBlockers")
|
|
148
|
+
):
|
|
149
|
+
failures.append(
|
|
150
|
+
f"final-verification: acceptance blocker {row_id} is "
|
|
151
|
+
"`recurring` but cites no `priorBlockers` "
|
|
152
|
+
"(`final-verification-NNN AB-NNN`)."
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
_CLASS_REPLANNING_TARGETS = (
|
|
157
|
+
"error-analysis",
|
|
158
|
+
"implementation-option-selection",
|
|
159
|
+
"implementation-planning",
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def _validate_recurring_class_routing(data: dict, failures: list[str]) -> None:
|
|
164
|
+
"""이전 run 이 이미 막았던 계열은 한 지점 수정으로 다시 보내지 않고 재계획한다."""
|
|
165
|
+
recurring = [
|
|
166
|
+
str(row.get("id") or "<id 없음>")
|
|
167
|
+
for row in _acceptance_blockers(data)
|
|
168
|
+
if isinstance(row.get("defectClass"), Mapping)
|
|
169
|
+
and row["defectClass"].get("recurrence") == "recurring"
|
|
170
|
+
]
|
|
171
|
+
if not recurring:
|
|
172
|
+
return
|
|
173
|
+
routing = (data.get("finalVerification") or {}).get("routingRecommendation")
|
|
174
|
+
target = str((routing or {}).get("target") or "") if isinstance(routing, Mapping) else ""
|
|
175
|
+
if target not in _CLASS_REPLANNING_TARGETS:
|
|
176
|
+
failures.append(
|
|
177
|
+
f"final-verification: acceptance blockers {recurring} are "
|
|
178
|
+
f"`recurring`, but routing targets `{target}`. A class a single-point "
|
|
179
|
+
"fix already failed to close routes to one of "
|
|
180
|
+
f"{list(_CLASS_REPLANNING_TARGETS)} so the whole class is re-planned."
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
|
|
100
184
|
def _validate_verdict_consistency(data: dict, failures: list[str]) -> None:
|
|
101
185
|
verdict = data.get("finalVerdict") or {}
|
|
102
186
|
token = (verdict.get("verdictToken") or "").strip().lower()
|
package/runtime/python/okstra_ctl/phases/implementation/instructions/_implementation-executor.md
CHANGED
|
@@ -52,7 +52,7 @@ template's check; that template is gone.
|
|
|
52
52
|
- **External-source adapters — structure AND fixture both derive from a captured real sample; a self-authored fixture is NOT reality evidence:** when this run's diff builds or changes an `external-interface` or `transformation-mapping` surface (an HTTP / network client, or a parser / mapper of a third-party payload — HTML / JSON / XML / CSV originating outside this repo), the adapter's structural assumptions (selectors, field paths, expected response shape) AND the static fixture / golden that tests them MUST BOTH derive from a **captured real sample** of that payload — the capture cited in the stage's `external-interface` / `transformation-mapping` design-prep item, or one captured this run and recorded with its `source` + capture time. The captured sample is a static fixture (no live socket), so a parser test against it stays in source like any unit test — the Real-IO isolation rule below governs *live* calls, not the captured bytes. Do NOT hand-invent the shape and then hand-write a fixture that agrees with it: the passing test then only proves the code matches your assumption, never that the assumption matches reality (self-confirming oracle — the observed failure was a parser whose selectors existed in its synthetic fixture and in zero real pages: hundreds of green units over a fiction, and the whole structure built on the wrong shape). When no real sample is reachable (no network this run, or the brief supplied none), do NOT synthesize a stand-in and present its green tests as correctness: mark the adapter's shape `reality-unverified (no captured sample)` in `Validation evidence`, keep any placeholder fixture explicitly labelled an assumption (never validation evidence), and surface an explicit **user-owned** item in the routing recommendation to confirm against real data. Unlike the DB gate above this does NOT itself block acceptance — live external verification stays a user-owned item per `final-verification`'s External QA advisory policy — but a synthetic external fixture presented as reality-verified is exactly the mock-only external evidence the `final-verification` test-correctness pass is meant to reject.
|
|
53
53
|
- **Real-IO test isolation (BLOCKING).** A test that exercises a **real** datastore, HTTP endpoint, external service, message queue, or filesystem — a live DB connection / DSN, a real `fetch` / `axios` / `http` request, an actual S3 / queue client, anything the project's normal CI test suite cannot run because that backend is absent — MUST be written under the task's qa scripts directory `<task_root>/qa/scripts/` (`<TASK_QA_PATH>/scripts`; the `qa/` root itself holds only data sidecars — the Tier 3 conformance manifest and `result-*.json`). It MUST NOT be written into the project source test tree — `src/**`, `test/**`, `tests/**`, `**/__test__/**`, `**/__tests__/**`, `*.spec.*`, `*.test.*`, or anywhere the project's lint/test globs collect. Two reasons: (a) the project's CI / normal suite has no real DB or network, so a real-IO test placed in source silently breaks the pipeline; (b) it is an okstra verification artifact, and the artifact-home rule confines okstra outputs to `.okstra/`. **The dividing line is the IO, not the intent:** a unit test that stubs/spies only *injected collaborators* (mock — no real socket, no real DB handle) is a TDD red-green artifact and stays in source; the moment a test opens a real connection or makes a real network call it belongs in qa. A stage's real-IO requirement check is a Tier 3 conformance script under `<task_root>/qa/scripts/` (declared via the implementation-planning conformance entry) — never smuggle real IO into a `*.spec.*` in source to make it run "as a unit test". The `db-test` real-execution gate above is satisfied by the conformance/db-test path against the replica, NOT by adding a live-DB `*.spec.*` to the project suite. **Author qa specs with the project's own test framework — never hand-roll `describe`/`it`/`expect`.** When the project ships a test runner as a devDependency (jest / vitest / pytest …), the qa spec uses it, invoked with the project config plus a discovery override pointing at the qa scripts dir (jest: `npx jest --config <project jest config> --roots <task_root>/qa/scripts --runInBand <spec-name>`) — the project config keeps module aliases resolving while the default sweep never collects the file; never widen the project's own test config to include qa paths. For TypeScript qa specs also write `<task_root>/qa/scripts/tsconfig.json` (`extends` the project tsconfig, adds the runner's `types` entry, `"include": ["**/*.ts"]`) so editors resolve path aliases and test globals — it is a qa artifact like the rest (untracked). **These qa artifacts stay untracked — never commit them.** `.okstra/**` is gitignored (the artifact-home rule); conformance scripts and their results are *executed* and recorded in the carry sidecar / verifier result, never written into git history. A committed `.okstra/qa` file is a stage-branch defect that leaks okstra internals into the eventual PR (see the `git add` rules below).
|
|
54
54
|
- **Stage conformance script (BLOCKING when the approved plan declared `Conformance tests:`).** Planning only declared the path and `requires`. This run MUST write the script to that path under `<task_root>/qa/scripts/` and add the matching `<task_root>/qa/conformance-manifest.json` entry: `stageKey` (= `<task-id>-stage-<N>`), `script`, `runCommand`, `requirementIds`, `requires` (the set the plan declared, widened by any db/io/http/external surface this stage's diff touches that the plan left out — never narrowed; `validate-run.py` `_declared_conformance_errors` accepts a superset and rejects a subset), `passContract`, `exemption: null`, `waiver: null`. Do not skip this when the plan declared tests. If the plan declared `Conformance exemption:`, do not invent a script — with one exception: when this stage's diff touches a db/io/http/external surface the exemption promised it would not (the verifier's diff-surface cross-check names the surface), write the script and the manifest entry for this stage exactly as for a declared stage, with `requires` covering those surfaces. The approved plan is not rewritten; `validate-run.py` `_declared_conformance_errors` accepts an entry for a stage the plan exempted and still rejects one for a stage the plan does not have. The script's standard interface: a `main` that exits `0`=PASS / non-zero=FAIL, and whose stdout ends with `QA-RESULT: PASS|FAIL` followed by one `REQ <id>: PASS|FAIL: <reason>` line per requirement. The verifier runs `runCommand` from the **worktree cwd**, and that cwd is the tree under test. `runCommand` MUST NOT repoint it: a leading `cd <checkout> &&` sends the script at a tree without this stage's changes. Absolute paths are fine and usually necessary — the script and its `tsconfig` live under `<task_root>/qa/scripts/`, i.e. under `.okstra/`, and a worktree does not carry `.okstra/`. Point at those by absolute path; leave the cwd alone. **Enforced:** `scripts/okstra_ctl/conformance.py` `_check_entry` rejects a `runCommand` whose first word in any `&&` / `;` segment changes directory; `validators/validate-run.py` `_validate_conformance` fails the run if the inherited declaration has no script file.
|
|
55
|
-
- read the
|
|
55
|
+
- read the stage extract at this prompt's `**Approved plan stage extract:**` anchor end-to-end. It is the approved plan projected onto this run's stage: the whole Stage Map (`stageMap`), this stage's entry, the requirement-coverage, validation, rollback and risk rows that name this stage or name no stage, each with the `planItemId` (`P-Val-4`, `P-Rb-2`) directives cite it by, and the clarification items. Its `omitted` object names everything left out and why. The plan at the `**Approved plan:**` anchor stays the source of record: open it only for a targeted lookup of a key the extract omits, never end-to-end. When the prompt has no extract anchor (a Markdown plan, or a prompt published before the extract existed), read the approved plan end-to-end and parse its Stage Map instead. Read this prompt's `**Stage for this implementation run:**` anchor: the single stage number this run owns. The runtime already selected and reserved this stage (one run = one stage) — do NOT recompute the start stage from `consumers.jsonl`. Both anchors are generated headers; when either is missing, stop and report `contract-violated` rather than inferring the value.
|
|
56
56
|
- load every `runs/<plan-key>/carry/stage-<i>.json` for `i ∈ depends-on(this stage)` and inject them into the executor's working context as "runtime carry-in". For a `depends-on (none)` stage, no sidecar load — task-brief only.
|
|
57
57
|
- this stage's `depends-on` are all already `status:done`. Its file list, step order, Stage Validation commands, Stage Exit Contract, and rollback path are the authoritative scope.
|
|
58
58
|
- **Clarification answers carried in (read before the first edit):** when the user answered the approved plan's `## 1. Clarification Items` rows, those answers arrive with this prompt under the heading `# Clarification answers carried in (authoritative)`, or as the `Clarification response` path under `## Inputs`. Treat each answer as an authoritative refinement of the plan's scope for the matching row; an answer that contradicts or expands the approved scope beyond the plan is a re-plan trigger (route to a new `implementation-planning` run), not a silent in-run change.
|
|
@@ -52,12 +52,18 @@ def requirement_instructions(original_requirement_ids: Sequence[str], participat
|
|
|
52
52
|
"A ranked option is valid only when every participating analyser "
|
|
53
53
|
"supplied one feasibility vote, at least "
|
|
54
54
|
f"{required_feasible_votes(participating_analysers)} of them are "
|
|
55
|
-
"`feasible`,
|
|
56
|
-
"
|
|
55
|
+
"`feasible`, `safetyBlockers` is empty, and every "
|
|
56
|
+
"`unresolvedFeasibilityFacts` entry is a `deferred-gate` fact that names "
|
|
57
|
+
"its `gate`. A candidate whose `splitAcceptance.clarificationRef` names "
|
|
58
|
+
"the user's `accept-risk` answer to the split needs one `feasible` vote, "
|
|
59
|
+
"and its remaining facts stay on the row as the accepted risk; never set "
|
|
60
|
+
"`splitAcceptance` before that answer is recorded.",
|
|
57
61
|
("Classify each `unresolvedFeasibilityFacts` entry with `resolutionKind`: "
|
|
58
62
|
"`user-decision` requires nonempty `clarificationRefs` linking that fact "
|
|
59
63
|
"to actual C-NNN records, including answered records; `technical-verification` "
|
|
60
|
-
"requires no user question
|
|
64
|
+
"requires no user question; `deferred-gate` is for a fact only a later gate "
|
|
65
|
+
"can close (a deployed-environment measurement technical-verification may not "
|
|
66
|
+
"run) and requires `gate` naming that gate. Preserve the fact, whyItMatters, and evidence. "
|
|
61
67
|
"Historical unclassified facts remain readable but require classification "
|
|
62
68
|
"before blocked report reassembly. Saving does not lift blocked routing."),
|
|
63
69
|
("After a linked decision is answered, preserve its disposition and selected "
|
|
@@ -73,8 +79,10 @@ def requirement_instructions(original_requirement_ids: Sequence[str], participat
|
|
|
73
79
|
"`requirement-fit`, `correctness-risk`, `architecture-fit`, "
|
|
74
80
|
"`weightedScore`, then id.",
|
|
75
81
|
"In `candidate-comparison`, keep `preselectedDirection` null. In "
|
|
76
|
-
"`preselected-validation`,
|
|
77
|
-
"
|
|
82
|
+
"`preselected-validation`, keep a cited `preselectedDirection`; a "
|
|
83
|
+
"validated direction is the only ranked option with an empty "
|
|
84
|
+
"`candidateAudit`, and a direction that did not validate is the only "
|
|
85
|
+
"`candidateAudit` row with no ranked option.",
|
|
78
86
|
"Record candidate validity and selection mode; the lead chooses the next "
|
|
79
87
|
"phase under `## implementation-option-selection` in "
|
|
80
88
|
"`prompts/lead/phase-routing.md`.",
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"allowed": " - candidate-comparison evidence for up to three options per worker\n - preselected-validation of exactly ONE candidate: every analyser returns its own feasibility verdict on that one direction
|
|
2
|
+
"allowed": " - candidate-comparison evidence for up to three options per worker\n - preselected-validation of exactly ONE candidate: every analyser returns its own feasibility verdict on that one direction and no analyser generates an alternative. A validated direction is the only ranked option with an empty `candidateAudit`; a direction that did not validate is the only `candidateAudit` row and the run routes `blocked` or `technical-verification`. It is not a re-evaluation of the merged set — `okstra_ctl.phases.implementation_option_selection.validation._validate_mode` rejects any other shape\n - a ranked display of at most three options with requirement mappings\n - an audit record for every rejected candidate\n - one endStateCoverage row per brief end-state id (this phase authors no goal of its own)",
|
|
3
3
|
"forbidden": [
|
|
4
4
|
"source or configuration edits, refactors, or fix attempts",
|
|
5
5
|
"tests, builds, migrations, deployments, or any state-mutating command",
|
|
@@ -2,9 +2,15 @@
|
|
|
2
2
|
from __future__ import annotations
|
|
3
3
|
|
|
4
4
|
from pathlib import Path
|
|
5
|
+
from okstra_ctl.final_report_paths import final_report_data_path
|
|
6
|
+
from okstra_ctl.json_boundary import JsonBoundaryError, load_owned_object
|
|
7
|
+
from okstra_ctl.path_resolve import relative_to_project_root
|
|
5
8
|
from okstra_ctl.prepare_error import PrepareError
|
|
6
9
|
from okstra_ctl.scope_provenance import brief_end_state_id_sequence
|
|
7
10
|
|
|
11
|
+
CANDIDATE_COMPARISON = "candidate-comparison"
|
|
12
|
+
PRESELECTED_VALIDATION = "preselected-validation"
|
|
13
|
+
|
|
8
14
|
|
|
9
15
|
def validate_brief_requirements(brief_path: Path) -> None:
|
|
10
16
|
if not brief_end_state_id_sequence(brief_path):
|
|
@@ -12,3 +18,64 @@ def validate_brief_requirements(brief_path: Path) -> None:
|
|
|
12
18
|
"regenerate the brief with stable end-state IDs before starting "
|
|
13
19
|
"implementation-option-selection"
|
|
14
20
|
)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def selection_mode(preselected_direction: str) -> str:
|
|
24
|
+
return PRESELECTED_VALIDATION if preselected_direction.strip() else CANDIDATE_COMPARISON
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def upstream_next_steps(carry_in: Path) -> tuple[str, ...]:
|
|
28
|
+
"""The next-phase instruction a carried-in report gave this phase.
|
|
29
|
+
|
|
30
|
+
The clarification carry-in projects only the report's C-NNN rows, so a
|
|
31
|
+
routing rationale such as "confirm one already-chosen direction" never
|
|
32
|
+
reached the designers.
|
|
33
|
+
"""
|
|
34
|
+
data_path = final_report_data_path(carry_in)
|
|
35
|
+
if not data_path.is_file():
|
|
36
|
+
return ()
|
|
37
|
+
try:
|
|
38
|
+
data = load_owned_object(data_path, artifact="carried-in final report")
|
|
39
|
+
except JsonBoundaryError as exc:
|
|
40
|
+
raise PrepareError(str(exc)) from exc
|
|
41
|
+
card = data.get("verdictCard")
|
|
42
|
+
steps = [card.get("nextStep")] if isinstance(card, dict) else []
|
|
43
|
+
for row in data.get("recommendedNextSteps") or []:
|
|
44
|
+
if isinstance(row, dict):
|
|
45
|
+
steps.append(row.get("text"))
|
|
46
|
+
return tuple(step.strip() for step in steps if isinstance(step, str) and step.strip())
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def selection_mode_packet_block(
|
|
50
|
+
preselected_direction: str, carry_in: Path | None, project_root: Path,
|
|
51
|
+
) -> str:
|
|
52
|
+
mode = selection_mode(preselected_direction)
|
|
53
|
+
lines = ["## Direction Selection Mode", "", f"This run's mode: `{mode}`.", ""]
|
|
54
|
+
if mode == PRESELECTED_VALIDATION:
|
|
55
|
+
lines += [
|
|
56
|
+
"The lead preselected the direction below. Validate only this direction",
|
|
57
|
+
"as `Worker direction-selection procedure` describes for",
|
|
58
|
+
"`preselected-validation`: do not generate, rank or compare alternatives.",
|
|
59
|
+
"Record open choices inside the direction as uncertainties with evidence.",
|
|
60
|
+
"",
|
|
61
|
+
"### Preselected Direction and Citation",
|
|
62
|
+
"",
|
|
63
|
+
preselected_direction.strip(),
|
|
64
|
+
]
|
|
65
|
+
else:
|
|
66
|
+
lines += [
|
|
67
|
+
"No direction was preselected for this run. Propose and compare",
|
|
68
|
+
"candidates as `Worker direction-selection procedure` describes for",
|
|
69
|
+
"`candidate-comparison`.",
|
|
70
|
+
]
|
|
71
|
+
steps = upstream_next_steps(carry_in) if carry_in is not None else ()
|
|
72
|
+
if steps:
|
|
73
|
+
lines += [
|
|
74
|
+
"",
|
|
75
|
+
"### Upstream Next-Phase Instruction",
|
|
76
|
+
"",
|
|
77
|
+
f"From `{relative_to_project_root(final_report_data_path(carry_in), project_root)}`:",
|
|
78
|
+
"",
|
|
79
|
+
*(f"- {step}" for step in steps),
|
|
80
|
+
]
|
|
81
|
+
return "\n".join(lines)
|
|
@@ -8,7 +8,8 @@
|
|
|
8
8
|
- Worker direction-selection procedure:
|
|
9
9
|
- In `candidate-comparison` mode, produce candidate, supporting and contradicting evidence, criterion scores, and requirement mappings. For every candidate, state one feasibility verdict — `feasible`, `not-feasible`, or `uncertain` — with a rationale and the strongest counterevidence, each citing inspected evidence. Write the rationale as long as the argument needs: say why each cited finding supports the verdict, not only which findings do. The report's `feasibilityVotes` row for this worker is built from that statement, so a candidate without one leaves the writer nothing but another worker's words.
|
|
10
10
|
- In `candidate-comparison` mode only, submit at most three candidates. A candidate must be feasible from inspected evidence, not from an assumed future change.
|
|
11
|
-
-
|
|
11
|
+
- Read the run's mode, and in `preselected-validation` the one preselected direction with its citation, from the packet's `## Direction Selection Mode` section (`--preselected-direction`).
|
|
12
|
+
- In `preselected-validation` mode, validate the one preselected direction: its evidence, counterevidence, criterion scores, and requirement mappings, and state the same feasibility verdict with its rationale and counterevidence. The worker must not generate new candidates.
|
|
12
13
|
- Do not produce detailed file lists, stage maps, execution commands, or a plan approval request.
|
|
13
14
|
- Census aspects:
|
|
14
15
|
- requirement mapping: Which candidate or preselected direction satisfies this requirement, and how?
|
|
@@ -26,7 +27,8 @@
|
|
|
26
27
|
- Map every displayed candidate or preselected direction to the stable brief end-state IDs it satisfies, preserves, or leaves unresolved.
|
|
27
28
|
- **Close the vote gaps before you conclude `blocked`.** Round 1 runs the designers in parallel, so each one votes only on the candidates it proposed and the merged set ends up with a different hole per analyser. A candidate that is otherwise sound then fails the every-analyser clause and drops out of the ranking — three such candidates blocked a run whose comparison had in fact converged (2026-09-10, dev-10629-4: IO-001, IO-002 and IO-003 each held two `feasible` votes and each was missing a different designer). Run `okstra option-votes gaps --task-manifest <taskManifestPath> --narrative <report writer narrative>` before assembly. For each analyser it names, dispatch one vote-completion assignment asking for that analyser's own feasibility verdict, rationale, and counterevidence on the named candidate — no new candidate, so the run stays in `candidate-comparison` mode. The command reports no gap when a vote cannot settle the block (safety blockers, unresolved feasibility facts, too few feasible verdicts); that is the honest `blocked`.
|
|
28
29
|
- Clarification request policy (phase-specific addenda — shared policy is in `_common-contract.md`):
|
|
29
|
-
- Classify every `unresolvedFeasibilityFacts` entry with `resolutionKind`: `user-decision` requires nonempty `clarificationRefs` naming the actual C-NNN records for that fact; `technical-verification` requires source review, design evidence, or experiments and does not require a user question. Preserve `fact`, `whyItMatters`, and `evidence`. Reuse answered questions and their recorded dispositions; never reopen one merely to save the report. An unrelated open question does not satisfy a missing reference. Historical unclassified entries remain readable, but blocked report reassembly requires the writer to classify them explicitly. Enforced by `validate_blocked_answer_channel` in `scripts/okstra_ctl/phases/implementation_option_selection/validation.py`, called by report assembly and `validate-run.py`. Saving preserves `routing: blocked` and does not make a candidate valid. In `humanSummary.actions`, `verdictCard.nextStep`, and selection guidance, distinguish pending user decisions from remaining technical verification; after the pilot route is answered, carry the selected route and remove the old answer-waiting instruction.
|
|
30
|
+
- Classify every `unresolvedFeasibilityFacts` entry with `resolutionKind`: `user-decision` requires nonempty `clarificationRefs` naming the actual C-NNN records for that fact; `technical-verification` requires source review, design evidence, or experiments and does not require a user question. `deferred-gate` marks a fact that no technical-verification run can close because closing it needs something that phase forbids, such as a deployed environment (for example, a cold-start measurement of a deployed Lambda); it requires `gate`, naming where the fact closes (a brief `External Gates` entry, or final-verification user-owned external QA), and it does not make the candidate invalid. A fact that a probe, a source read, or a disposable local service can close is `technical-verification`, not `deferred-gate`. The selected direction snapshot carries every remaining fact of the chosen option as `deferredFeasibilityFacts`, so planning schedules its gate. Enforced by `blocking_feasibility_facts` in `scripts/okstra_ctl/phases/implementation_option_selection/validation.py`. Preserve `fact`, `whyItMatters`, and `evidence`. Reuse answered questions and their recorded dispositions; never reopen one merely to save the report. An unrelated open question does not satisfy a missing reference. Historical unclassified entries remain readable, but blocked report reassembly requires the writer to classify them explicitly. Enforced by `validate_blocked_answer_channel` in `scripts/okstra_ctl/phases/implementation_option_selection/validation.py`, called by report assembly and `validate-run.py`. Saving preserves `routing: blocked` and does not make a candidate valid. In `humanSummary.actions`, `verdictCard.nextStep`, and selection guidance, distinguish pending user decisions from remaining technical verification; after the pilot route is answered, carry the selected route and remove the old answer-waiting instruction.
|
|
31
|
+
- **A split vote ends in a user decision, not another run.** When no candidate is valid only because its votes split (at least one `feasible`, the others `uncertain` or `not-feasible`, no `safetyBlockers`), another option-selection or technical-verification run asks the same designers the same question and returns the same split (fontradar dev-11562: three runs, `feasible` against `uncertain` each time). Before you route `blocked` or `technical-verification` on such a candidate, open one approval decision for it with `okstra approval-decision open --ledger <approvalDecisionsPath>` (classification `noncritical-dissent`): the recommended option is `accept-risk` (proceed with this direction and carry the dissent and the remaining facts into planning), the alternative is `request-revision` (keep the run blocked). Ask the user in-session; record the answer with `okstra approval-decision resolve`. On `accept-risk`, the writer sets `splitAcceptance.clarificationRef` on that candidate, which makes it valid with one `feasible` vote; it then follows the ordinary routing (`implementation-planning` for a validated preselected direction, `pending-direction-selection` in `candidate-comparison`). Leave the votes and facts on the row. When the user is not present, publish the report `blocked` with that approval row open; the next run carries the answer. Enforced by `validate_split_acceptance` in `scripts/okstra_ctl/phases/implementation_option_selection/validation.py`, called by report assembly and `validate-run.py`.
|
|
30
32
|
{{INCLUDE:_clarification-recommendation.md}}
|
|
31
33
|
- Cross-verification mode:
|
|
32
34
|
- Phase 5.5 convergence runs in adversarial mode (`convergence.adversarial=true`).
|
|
@@ -33,6 +33,7 @@
|
|
|
33
33
|
<table data-report-field="implementationOptionSelection.rankedOptions.feasibilityVotes"><thead><tr><th>{{ t('tasks.implementation-option-selection.worker') }}</th><th>{{ t('tasks.implementation-option-selection.reason') }}</th><th>{{ t('tasks.implementation-option-selection.counterevidence') }}</th></tr></thead><tbody>{% for vote in row.feasibilityVotes %}<tr>{{ row_key(pairs=[(t('tasks.implementation-option-selection.worker'), vote.worker)], status_name=t('tasks.implementation-option-selection.verdict'), status_raw=vote.verdict, status_text=vote.verdict | enum_label('feasibilityVerdict')) }}<td>{{ vote.rationale | inline_code(extra) }}</td><td>{{ vote.counterevidence | inline_code(extra) }}</td></tr>{% endfor %}</tbody></table>
|
|
34
34
|
{% if row.safetyBlockers %}<p><strong>{{ t('tasks.implementation-option-selection.safety-blockers') }}</strong></p>
|
|
35
35
|
<ul>{% for blocker in row.safetyBlockers %}<li><span class="status status-risk">{{ blocker.category | enum_label('safetyCategory') }}</span> {{ blocker.statement | inline_code(extra) }} <span class="evidence-refs">{{ t('macros.layout.evidence') }} {{ blocker.evidence | evidence_refs(extra) }}</span></li>{% endfor %}</ul>{% endif %}
|
|
36
|
+
{% if row.get('splitAcceptance') %}<p><strong>{{ t('tasks.implementation-option-selection.split-acceptance') }}</strong> {{ row.splitAcceptance.clarificationRef | inline_code(extra) }}</p>{% endif %}
|
|
36
37
|
{{ unresolved_facts(row, extra) }}
|
|
37
38
|
</div>
|
|
38
39
|
<div class="option-detail">
|
|
@@ -49,7 +50,7 @@
|
|
|
49
50
|
|
|
50
51
|
{% macro unresolved_facts(row, extra) -%}
|
|
51
52
|
{% if row.unresolvedFeasibilityFacts %}<p><strong>{{ t('tasks.implementation-option-selection.unresolved-facts') }}</strong></p>
|
|
52
|
-
<ul>{% for fact in row.unresolvedFeasibilityFacts %}<li>{% if fact.get('resolutionKind') == 'technical-verification' %}{{ t('tasks.implementation-option-selection.technical-verification') }}: {% elif fact.get('resolutionKind') == 'user-decision' %}{{ t('tasks.implementation-option-selection.user-decision') }} ({{ fact.get('clarificationRefs', []) | join(', ') | inline_code(extra) }}): {% else %}{{ t('tasks.implementation-option-selection.classification-required') }}: {% endif %}{{ fact.fact | inline_code(extra) }} — {{ t('tasks.implementation-option-selection.why-it-matters') }} {{ fact.whyItMatters | inline_code(extra) }} <span class="evidence-refs">{{ t('macros.layout.evidence') }} {{ fact.evidence | evidence_refs(extra) }}</span></li>{% endfor %}</ul>{% endif %}
|
|
53
|
+
<ul>{% for fact in row.unresolvedFeasibilityFacts %}<li>{% if fact.get('resolutionKind') == 'technical-verification' %}{{ t('tasks.implementation-option-selection.technical-verification') }}: {% elif fact.get('resolutionKind') == 'deferred-gate' %}{{ t('tasks.implementation-option-selection.deferred-gate') }} ({{ fact.get('gate', '') | inline_code(extra) }}): {% elif fact.get('resolutionKind') == 'user-decision' %}{{ t('tasks.implementation-option-selection.user-decision') }} ({{ fact.get('clarificationRefs', []) | join(', ') | inline_code(extra) }}): {% else %}{{ t('tasks.implementation-option-selection.classification-required') }}: {% endif %}{{ fact.fact | inline_code(extra) }} — {{ t('tasks.implementation-option-selection.why-it-matters') }} {{ fact.whyItMatters | inline_code(extra) }} <span class="evidence-refs">{{ t('macros.layout.evidence') }} {{ fact.evidence | evidence_refs(extra) }}</span></li>{% endfor %}</ul>{% endif %}
|
|
53
54
|
{%- endmacro %}
|
|
54
55
|
|
|
55
56
|
{% block human_content %}
|
|
@@ -41,6 +41,11 @@ MAX_RANKED_OPTIONS = 3
|
|
|
41
41
|
MAX_RAW_CANDIDATES_PER_ANALYSER = 3
|
|
42
42
|
CANDIDATE_COMPARISON_ROUTING = "pending-direction-selection"
|
|
43
43
|
NO_VALID_OPTIONS_ROUTING = "blocked"
|
|
44
|
+
# 배포된 환경이 있어야 닫히는 사실(예: 배포된 Lambda 의 실행 시간)은
|
|
45
|
+
# technical-verification 이 배포를 금하므로 그 phase 의 재실행으로 닫히지 않는다.
|
|
46
|
+
# 그런 사실은 닫히는 게이트를 적고 후보를 무효로 만들지 않는다.
|
|
47
|
+
DEFERRED_GATE_KIND = "deferred-gate"
|
|
48
|
+
SPLIT_ACCEPTANCE_DISPOSITION = "accept-risk"
|
|
44
49
|
|
|
45
50
|
_FINGERPRINT_FIELDS = (
|
|
46
51
|
"goal",
|
|
@@ -233,6 +238,37 @@ def required_feasible_votes(participating_analysers: Sequence[str]) -> int:
|
|
|
233
238
|
return min(MIN_FEASIBLE_VOTES, max(1, len(set(participating_analysers))))
|
|
234
239
|
|
|
235
240
|
|
|
241
|
+
def split_acceptance_ref(option: Mapping[str, object]) -> str:
|
|
242
|
+
"""후보가 인용한 사용자 위험 감수 결정의 C-id. 없으면 빈 문자열."""
|
|
243
|
+
acceptance = option.get("splitAcceptance")
|
|
244
|
+
if not isinstance(acceptance, Mapping):
|
|
245
|
+
return ""
|
|
246
|
+
return str(acceptance.get("clarificationRef") or "").strip()
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
def _is_deferred_gate_fact(fact: object) -> bool:
|
|
250
|
+
return (
|
|
251
|
+
isinstance(fact, Mapping)
|
|
252
|
+
and fact.get("resolutionKind") == DEFERRED_GATE_KIND
|
|
253
|
+
and bool(str(fact.get("gate") or "").strip())
|
|
254
|
+
)
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
def blocking_feasibility_facts(option: Mapping[str, object]) -> list[object]:
|
|
258
|
+
"""후보를 무효로 두는 미확정 사실.
|
|
259
|
+
|
|
260
|
+
게이트를 적은 `deferred-gate` 사실은 막지 않는다. 사용자가 위험 감수로
|
|
261
|
+
받아들인 후보(`splitAcceptance`)는 남은 사실을 기록으로만 들고 간다 —
|
|
262
|
+
계획 phase 의 `accept-risk` 와 같은 뜻이다.
|
|
263
|
+
"""
|
|
264
|
+
if split_acceptance_ref(option):
|
|
265
|
+
return []
|
|
266
|
+
return [
|
|
267
|
+
fact for fact in option.get("unresolvedFeasibilityFacts") or ()
|
|
268
|
+
if not _is_deferred_gate_fact(fact)
|
|
269
|
+
]
|
|
270
|
+
|
|
271
|
+
|
|
236
272
|
def _validate_option_feasibility(
|
|
237
273
|
option: Mapping[str, object],
|
|
238
274
|
participating_analysers: Sequence[str],
|
|
@@ -255,13 +291,36 @@ def _validate_option_feasibility(
|
|
|
255
291
|
isinstance(vote, Mapping) and vote.get("verdict") == "feasible"
|
|
256
292
|
for vote in votes
|
|
257
293
|
)
|
|
258
|
-
|
|
294
|
+
# 갈린 표는 같은 분석자들에게 다시 물어도 갈린다. 사용자가 위험 감수로
|
|
295
|
+
# 받아들인 후보는 `feasible` 한 표로 충분하다 — 방향을 낸 근거가 있어야
|
|
296
|
+
# 받아들일 대상이 있다.
|
|
297
|
+
accepted = bool(split_acceptance_ref(option))
|
|
298
|
+
required = 1 if accepted else required_feasible_votes(participating_analysers)
|
|
259
299
|
if require_valid and feasible < required:
|
|
260
|
-
errors.append(
|
|
300
|
+
errors.append(
|
|
301
|
+
f"{option_id} must have at least {required} feasible vote(s); a split "
|
|
302
|
+
"vote is settled by a user accept-risk decision cited in splitAcceptance, "
|
|
303
|
+
"not by another run with the same analysers"
|
|
304
|
+
)
|
|
261
305
|
if require_valid and option.get("safetyBlockers"):
|
|
262
306
|
errors.append(f"{option_id} safetyBlockers must be empty")
|
|
263
|
-
|
|
264
|
-
|
|
307
|
+
blocking_facts = blocking_feasibility_facts(option)
|
|
308
|
+
if require_valid and blocking_facts:
|
|
309
|
+
errors.append(
|
|
310
|
+
f"{option_id} unresolvedFeasibilityFacts must be empty, "
|
|
311
|
+
f"{DEFERRED_GATE_KIND} facts that name their gate, or accepted through "
|
|
312
|
+
"splitAcceptance"
|
|
313
|
+
)
|
|
314
|
+
for index, fact in enumerate(option.get("unresolvedFeasibilityFacts") or ()):
|
|
315
|
+
if (
|
|
316
|
+
isinstance(fact, Mapping)
|
|
317
|
+
and fact.get("resolutionKind") == DEFERRED_GATE_KIND
|
|
318
|
+
and not _is_deferred_gate_fact(fact)
|
|
319
|
+
):
|
|
320
|
+
errors.append(
|
|
321
|
+
f"{option_id} unresolvedFeasibilityFacts[{index}]: a "
|
|
322
|
+
f"{DEFERRED_GATE_KIND} fact must name the gate that closes it"
|
|
323
|
+
)
|
|
265
324
|
copied = _copied_votes(votes)
|
|
266
325
|
for first, second in copied:
|
|
267
326
|
errors.append(
|
|
@@ -272,7 +331,7 @@ def _validate_option_feasibility(
|
|
|
272
331
|
all_participated
|
|
273
332
|
and feasible >= required
|
|
274
333
|
and not option.get("safetyBlockers")
|
|
275
|
-
and not
|
|
334
|
+
and not blocking_facts
|
|
276
335
|
and not copied
|
|
277
336
|
)
|
|
278
337
|
|
|
@@ -415,9 +474,7 @@ def vote_gaps(
|
|
|
415
474
|
for row in (selection.get(key) or ())
|
|
416
475
|
if isinstance(row, Mapping)
|
|
417
476
|
):
|
|
418
|
-
if candidate.get("safetyBlockers") or candidate
|
|
419
|
-
"unresolvedFeasibilityFacts"
|
|
420
|
-
):
|
|
477
|
+
if candidate.get("safetyBlockers") or blocking_feasibility_facts(candidate):
|
|
421
478
|
continue
|
|
422
479
|
votes = candidate.get("feasibilityVotes") or ()
|
|
423
480
|
if _copied_votes(votes):
|
|
@@ -489,12 +546,13 @@ def validate_blocked_answer_channel(
|
|
|
489
546
|
continue
|
|
490
547
|
label += f" ({fact.get('fact')})"
|
|
491
548
|
kind = fact.get("resolutionKind")
|
|
492
|
-
if kind
|
|
549
|
+
if kind in {"technical-verification", DEFERRED_GATE_KIND}:
|
|
493
550
|
continue
|
|
494
551
|
if kind != "user-decision":
|
|
495
552
|
errors.append(f"{label}: classification required — set resolutionKind to "
|
|
496
|
-
"user-decision
|
|
497
|
-
"and evidence, then reassemble; do not
|
|
553
|
+
"user-decision, technical-verification or deferred-gate; "
|
|
554
|
+
"preserve the fact and evidence, then reassemble; do not "
|
|
555
|
+
"infer a user question")
|
|
498
556
|
continue
|
|
499
557
|
refs = fact.get("clarificationRefs")
|
|
500
558
|
if not isinstance(refs, list) or not refs or any(
|
|
@@ -507,6 +565,46 @@ def validate_blocked_answer_channel(
|
|
|
507
565
|
return errors
|
|
508
566
|
|
|
509
567
|
|
|
568
|
+
def validate_split_acceptance(report_data: Mapping[str, object]) -> list[str]:
|
|
569
|
+
"""`splitAcceptance` 가 인용한 C-id 가 사용자의 위험 감수 기록인지 검사한다.
|
|
570
|
+
|
|
571
|
+
서술 검증(`validate_implementation_option_selection`)은 clarification 행을
|
|
572
|
+
보지 못한다. 리드 원장이 합쳐진 뒤에만 이 인용이 실제 `accept-risk` 답인지
|
|
573
|
+
알 수 있으므로 조립과 `validate-run.py` 가 이 함수를 따로 부른다.
|
|
574
|
+
"""
|
|
575
|
+
selection = report_data.get("implementationOptionSelection")
|
|
576
|
+
if not isinstance(selection, Mapping):
|
|
577
|
+
return []
|
|
578
|
+
rows = {
|
|
579
|
+
row.get("id"): row
|
|
580
|
+
for row in (report_data.get("clarificationItems") or ())
|
|
581
|
+
if isinstance(row, Mapping)
|
|
582
|
+
}
|
|
583
|
+
errors: list[str] = []
|
|
584
|
+
for candidate in (
|
|
585
|
+
row for key in ("rankedOptions", "candidateAudit")
|
|
586
|
+
for row in (selection.get(key) or ()) if isinstance(row, Mapping)
|
|
587
|
+
):
|
|
588
|
+
ref = split_acceptance_ref(candidate)
|
|
589
|
+
if not ref and "splitAcceptance" not in candidate:
|
|
590
|
+
continue
|
|
591
|
+
row = rows.get(ref)
|
|
592
|
+
if (
|
|
593
|
+
row is None
|
|
594
|
+
or str(row.get("blocks") or "").strip().lower() != "approval"
|
|
595
|
+
or str(row.get("status") or "").strip().lower() == "obsolete"
|
|
596
|
+
or clarification_disposition(row) != SPLIT_ACCEPTANCE_DISPOSITION
|
|
597
|
+
):
|
|
598
|
+
errors.append(
|
|
599
|
+
f"{candidate.get('id')} splitAcceptance {ref!r} must name a "
|
|
600
|
+
"`blocks: approval` clarification the user answered with "
|
|
601
|
+
f"`{SPLIT_ACCEPTANCE_DISPOSITION}`; open it with `okstra "
|
|
602
|
+
"approval-decision open --ledger <approvalDecisionsPath>`, record "
|
|
603
|
+
"the answer with `okstra approval-decision resolve`, then reassemble"
|
|
604
|
+
)
|
|
605
|
+
return errors
|
|
606
|
+
|
|
607
|
+
|
|
510
608
|
def _validate_candidate_audit(
|
|
511
609
|
data: Mapping[str, object],
|
|
512
610
|
options: Sequence[Mapping[str, object]],
|
|
@@ -630,18 +728,29 @@ def _validate_mode(
|
|
|
630
728
|
if options and data.get("routing") != CANDIDATE_COMPARISON_ROUTING:
|
|
631
729
|
errors.append("candidate-comparison with options must await direction selection")
|
|
632
730
|
return
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
731
|
+
# A direction that failed validation cannot be displayed (displayed options must be
|
|
732
|
+
# valid), so it is the one audit row; `_validate_option_count_and_routing` then
|
|
733
|
+
# requires `blocked` or `technical-verification`.
|
|
734
|
+
validated = bool(options)
|
|
735
|
+
if validated:
|
|
736
|
+
if len(options) != 1:
|
|
737
|
+
errors.append("preselected-validation must contain exactly one validated option")
|
|
738
|
+
if audit:
|
|
739
|
+
errors.append("preselected-validation candidateAudit must be empty")
|
|
740
|
+
elif len(audit) != 1:
|
|
741
|
+
errors.append(
|
|
742
|
+
"preselected-validation must contain exactly one validated option, "
|
|
743
|
+
"or the one unvalidated direction as its only candidateAudit row"
|
|
744
|
+
)
|
|
637
745
|
if not isinstance(direction, Mapping):
|
|
638
746
|
errors.append("preselected-validation requires preselectedDirection")
|
|
639
747
|
return
|
|
640
|
-
|
|
641
|
-
|
|
748
|
+
target = options[0] if validated else (audit[0] if len(audit) == 1 else None)
|
|
749
|
+
if target is not None and direction.get("optionId") != target.get("id"):
|
|
750
|
+
errors.append("preselectedDirection optionId must name the preselected direction")
|
|
642
751
|
if not _CITATION_RE.search(str(direction.get("citation") or "")):
|
|
643
752
|
errors.append("preselectedDirection citation must be citable")
|
|
644
|
-
if data.get("routing") != "implementation-planning":
|
|
753
|
+
if validated and data.get("routing") != "implementation-planning":
|
|
645
754
|
errors.append("validated preselected direction must route to implementation-planning")
|
|
646
755
|
|
|
647
756
|
|
|
@@ -19,6 +19,7 @@ Plan for the actual worktree layout before approval. Shared documentation direct
|
|
|
19
19
|
- **Selected-direction planning procedure** — perform these steps in order and no others:
|
|
20
20
|
1. Read `selected-direction.json` and the original requirements ledger end-to-end. The ledger is not a separate file: it is the brief's `Expected Behavior` / `Preserved Behavior` / `Expected Outcome` sections (`EB-` / `PB-` / `EO-` ids), carried verbatim in the analysis packet's `## Task-Specific Brief Extract`.
|
|
21
21
|
1a. Fill `selectedDirectionRef` from that snapshot: copy `sourceReport`, `sourceDataSha256`, and `optionId` verbatim from the file; write `snapshotPath` as the snapshot's path relative to the **task root** (`instruction-set/selected-direction.json`, never project-relative and never absolute) and `snapshotSha256` as the SHA-256 of its exact bytes. Those last two are the only ref fields the snapshot does not carry.
|
|
22
|
+
1b. When the snapshot carries `deferredFeasibilityFacts`, the direction was validated with those facts still open. A `deferred-gate` fact names the `gate` that closes it; carry it into the plan as that gate (an `External Gates` dependency or final-verification user-owned external QA), not as a stage validation row, because a stage row the implementation run cannot execute blocks the stage. When the snapshot also carries `splitAcceptance`, the user accepted the direction over a split feasibility vote; the remaining facts are that accepted risk, so record each in `dependencyMigrationRisk` or the validation that would expose it, and do not reopen the direction choice.
|
|
22
23
|
2. Concretize the selected direction into files, interfaces, stages, validation, and rollback without changing its mechanism or boundaries.
|
|
23
24
|
3. Link every planned file and stage bidirectionally to the original requirements.
|
|
24
25
|
4. If current evidence requires changing the selected direction, emit `direction-invalidated` and stop planning.
|
|
@@ -147,7 +148,7 @@ Plan for the actual worktree layout before approval. Shared documentation direct
|
|
|
147
148
|
**Pick the cases by distinct outcome, not by line coverage.** When the stage writes or reconciles state, its meaningfully different outcomes are usually more than three — normal success, target already in the desired state (resume), existing data reused rather than created, a conflicting concurrent state, target absent, and mid-way failure with rollback. Enumerate the ones this stage actually implements and route them across the three lines (the `boundary` line is where resume / already-done / reuse belongs; `failure` carries conflict, absence, and rollback), naming each in the cell rather than collapsing them into "edge input". An implemented outcome with no declared case is a coverage gap the executor will not backfill.
|
|
148
149
|
- **Per-stage subsections** (`## 5.5.<i> Stage <i>: <title>` for each `i`), each containing the four required subsections:
|
|
149
150
|
- `### Carry-In` — for `depends-on (none)`: task-brief only. Otherwise: each depended-on stage's static exit contract + runtime sidecar path `runs/<impl-key>/carry/stage-<i>.json` placeholder.
|
|
150
|
-
- `### Stepwise Execution Order` — bite-sized table with `step | action | files | command | outcome | expected`. `outcome` is one word — `PASS` or `FAIL` — and `expected` is the sentence saying what that looks like here; a verdict written inside the sentence is not read as one. The `files` cell lists each touched path in full and `<PROJECT_ROOT>`-relative — never ellipsis-abbreviated (`…` / `...`), which does not resolve and is rejected by plan-body verification as a kind-b path mismatch. **The narrative row additionally carries `plannedPaths`: the paths this step may write as an array, one repository-relative path per entry, with no globs, exclusions, counts or commentary. Read-only checks use `plannedPaths: []`; their working directory belongs in the command, not the write ledger. Never declare the repository or worktree root. Planning assembly and correction preflight enforce this through `write_policy.planned_path_declaration_errors`.** **File size:** the implementation run aims for 300 physical lines per source and test file and enforces 400 — a new file over 400, an existing file taken past 400, and a file already over 400 that grows are blocking findings the executor must split in a correction round; the same conditions at 300 are advisory. When a step would add code or tests to a file near or over 300 lines (`wc -l`), plan a new module or test file for that responsibility in `plannedPaths` instead of appending to the existing one. **Enforced at implementation, not here:** `okstra correction file-size` (`scripts/okstra_ctl/file_size_cap.py`), re-run by `okstra stage-done`. `files` is the sentence a reader sees; `plannedPaths` is the ledger report assembly preserves and the implementer write policy enforces. When a step legitimately covers a set too large to enumerate, split it or name the directory the set lives under. **Effective row count ≤ 8** (excluding header / divider / blank). Each step is one cohesive, self-contained change. **TDD ordering is MUST, not a preference:** the **first** effective step's `action` cell MUST start with the literal `RED:` and describe the failing test(s) that capture this stage's `Acceptance` **and the three declared `Test case (success|boundary|failure)` lines** (`outcome` = `FAIL`); at least one later `action` cell MUST start with the literal `GREEN:` and describe the minimal implementation that makes it pass (`outcome` = `PASS`); an optional refactor step starts with `REFACTOR:`. **Exemption:** doc-only / config-only / pure-rename stages with no observable runtime behaviour may omit RED/GREEN by declaring one line `TDD exemption: <reason>` in the stage section. A stage that is truthfully none of those three declares `TDD exemption: user-bypass — <the user's words>`, which holds only while the user has granted it for that stage with `okstra prepare --tdd-bypass "<stage>:<reason>"` — never file the nearest of the three instead. Validator S10c enforces RED-first + GREEN; the `outcome` cell agreeing with its prefix is a schema conditional. S10e rejects an unsupported exemption reason and a `user-bypass` with no user grant (`validators/validate-implementation-plan-stages.py`).
|
|
151
|
+
- `### Stepwise Execution Order` — bite-sized table with `step | action | files | command | outcome | expected`. `outcome` is one word — `PASS` or `FAIL` — and `expected` is the sentence saying what that looks like here; a verdict written inside the sentence is not read as one. The `files` cell lists each touched path in full and `<PROJECT_ROOT>`-relative — never ellipsis-abbreviated (`…` / `...`), which does not resolve and is rejected by plan-body verification as a kind-b path mismatch. **The narrative row additionally carries `plannedPaths`: the paths this step may write as an array, one repository-relative path per entry, with no globs, exclusions, counts or commentary. Read-only checks use `plannedPaths: []`; their working directory belongs in the command, not the write ledger. Never declare the repository or worktree root. Planning assembly and correction preflight enforce this through `write_policy.planned_path_declaration_errors`.** **File size:** the implementation run aims for 300 physical lines per source and test file and enforces 400 — a new file over 400, an existing file taken past 400, and a file already over 400 that grows are blocking findings the executor must split in a correction round; the same conditions at 300 are advisory. When a step would add code or tests to a file near or over 300 lines (`wc -l`), plan a new module or test file for that responsibility in `plannedPaths` instead of appending to the existing one. **Enforced at implementation, not here:** `okstra correction file-size` (`scripts/okstra_ctl/file_size_cap.py`), re-run by `okstra stage-done`. `files` is the sentence a reader sees; `plannedPaths` is the ledger report assembly preserves and the implementer write policy enforces. The design-surface detector maps every selected-option `fileStructure` path to the stage whose `files` or `plannedPaths` names it; a path neither names fails the draft check that correction preflight and `plan-items prepare` share (`scripts/okstra_ctl/report_assembly.py` `validate_plan_draft`). When a step legitimately covers a set too large to enumerate, split it or name the directory the set lives under. **Effective row count ≤ 8** (excluding header / divider / blank). Each step is one cohesive, self-contained change. **TDD ordering is MUST, not a preference:** the **first** effective step's `action` cell MUST start with the literal `RED:` and describe the failing test(s) that capture this stage's `Acceptance` **and the three declared `Test case (success|boundary|failure)` lines** (`outcome` = `FAIL`); at least one later `action` cell MUST start with the literal `GREEN:` and describe the minimal implementation that makes it pass (`outcome` = `PASS`); an optional refactor step starts with `REFACTOR:`. **Exemption:** doc-only / config-only / pure-rename stages with no observable runtime behaviour may omit RED/GREEN by declaring one line `TDD exemption: <reason>` in the stage section. A stage that is truthfully none of those three declares `TDD exemption: user-bypass — <the user's words>`, which holds only while the user has granted it for that stage with `okstra prepare --tdd-bypass "<stage>:<reason>"` — never file the nearest of the three instead. Validator S10c enforces RED-first + GREEN; the `outcome` cell agreeing with its prefix is a schema conditional. S10e rejects an unsupported exemption reason and a `user-bypass` with no user grant (`validators/validate-implementation-plan-stages.py`).
|
|
151
152
|
- **Real-IO tests stay out of the plan's committed steps (BLOCKING).** A step never creates, edits, or runs a test that opens a real database connection, makes a real network call, or talks to a real external service inside the project source test tree (`src/**`, `test/**`, `tests/**`, `**/__tests__/**`, `*.spec.*`, `*.test.*`, `*.e2e-spec.*`), and no step's `PASS` depends on a reachable database or service. The implementation executor refuses such a step before its first edit (`_implementation-executor.md` "Real-IO test isolation"), so a plan that contains one stops the stage with zero changes (observed 2026-10-18, dev-11374: a live-MySQL `test/font-pairing.e2e-spec.ts` planned in steps 1, 4 and 5). The committed RED/GREEN steps test the adapter with its injected collaborator stubbed — the exact query text, its replacements, and the returned rows — and run without any backend. Real-backend verification is declared only as this stage's `Conformance tests:` line below (`requires=[db|http|external]`): the implementation run writes that script under `<task_root>/qa/scripts/`, runs it against a local or replica backend when one is reachable, never commits it, and records a non-PASS or unreachable backend as advisory. Read a brief line such as `verify: integration test against a test database` as that conformance declaration, not as a committed test. Do not plan a shared real-DB guard, a test DDL fixture, or an edit to an existing real-IO suite in source to support it. **Enforced:** every `P-Step-*` item of the §5.5.9 plan-body round carries this rule to its verifiers (`okstra_ctl.phases.implementation_planning.authoring._REAL_IO_STEP_NOTE`), which vote `DISAGREE(b)` on a step that breaks it; no path pattern tells a mocked test from a live one, so no validator checks it.
|
|
152
153
|
- **The `command` cell runs inside an okstra task worktree, not a bare checkout (BLOCKING).** okstra provisions `.okstra`, the configured sync entries (`.project-docs`, `.claude`, …), and — for `implementation` — a nested `stage-<N>/` worktree into the tree the step executes in. Two consequences bind every command you write:
|
|
153
154
|
- **Clean-tree assertions use `okstra worktree-status --check-clean`.** A bare `git status --porcelain` is never empty there, so an assertion built on one fails on okstra's scaffolding rather than on the stage's work. The okstra command asks the same question over source paths only and exits 1 when dirty, so it stands alone as a step's assertion: `okstra worktree-status --check-clean`. Validator S13 rejects the bare form. Do not add a `git tag stage-<N>-exit` to the step. Stage completion records the commit in the consumer ledger without creating or moving git tags.
|
|
@@ -8,8 +8,8 @@
|
|
|
8
8
|
1. Read the frozen technical input linked in the analysis packet. Preserve the source hash, candidate IDs and fact identities. `resolve_technical_verification_input` enforces same-task source ownership, schema validity, eligible technical facts and unresolved user-decision gates.
|
|
9
9
|
2. Before executing a probe, write its plan under `experiments/<seq>/<worker-id>/`: hypothesis, procedure, commands, confirming and rejecting signals, environment, and inconclusive criteria. Retain the plan with the evidence.
|
|
10
10
|
3. Create a separate source copy in that directory. Record the source commit and any uncommitted input differences. Read the project and task worktree as inputs; perform source edits, installations and builds only in your own copy. Do not share writable dependency directories or use real credentials. Use synthetic local accounts for request-boundary experiments.
|
|
11
|
-
4. Phase 7 removes every `node_modules` and `.next` directory under the run directory, so evidence must not live inside them. Preserve the baseline lockfile. When the hypothesis needs a deliberate dependency change, retain the resulting lockfile and diff, then use a frozen install against that experimental lockfile. Record command, working directory, stdout/stderr and exit code in run-local logs. Distinguish infrastructure failure from a falsified compatibility claim.
|
|
12
|
-
5. Classify each fact as `supported`, `refuted`, `inconclusive` or `not-run` using the declared signals. A zero exit code alone does not establish compatibility. Record limitations and all failed or unavailable probes.
|
|
11
|
+
4. Phase 7 removes every `node_modules` and `.next` directory under the run directory, so evidence must not live inside them. Preserve the baseline lockfile. When the hypothesis needs a deliberate dependency change, retain the resulting lockfile and diff, then use a frozen install against that experimental lockfile. Record command, working directory, stdout/stderr and exit code in run-local logs. End every log with an `exit_code=<n>` line so a command that prints nothing on success, such as `tsc --noEmit`, still leaves a non-empty log; the report validator rejects an empty log. Record as `cwd` the project-relative directory the command ran in: when the command starts with `cd <dir> &&`, that is `<dir>`, not the shell's starting directory. Run every cited command inside `experiments/<seq>/`; a probe run from the project root is not admissible command evidence. Distinguish infrastructure failure from a falsified compatibility claim.
|
|
12
|
+
5. Classify each fact as `supported`, `refuted`, `inconclusive` or `not-run` using the declared signals. A zero exit code alone does not establish compatibility. A `supported` check cites only commands that exited 0. When a command fails for a reason outside the hypothesis, such as a baseline failure that repeats identically on both sides of the comparison, do not cite it under a supported check: cite a command whose exit code expresses the confirming signal, such as `cmp` of the two logs, and name the failing logs in `limitations`. If no such command separates the outcomes, classify the fact `inconclusive`. Record limitations and all failed or unavailable probes.
|
|
13
13
|
6. During convergence, inspect plans and logs; rerun disputed probes in a separate copy when feasible. An independent report is not a substitute for command evidence.
|
|
14
14
|
|
|
15
15
|
Copy boundaries and pre-execution plans are worker instructions reviewed during convergence. The report validator checks fact completeness, run-local log files, experiment working directories and zero exit codes for supported results. It does not sandbox arbitrary shell commands or prove that a declared command produced its log.
|