okstra 0.140.0 → 0.141.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/README.md +1 -1
- package/docs/architecture.md +1 -1
- package/docs/cli.md +1 -1
- package/package.json +1 -1
- package/runtime/BUILD.json +2 -2
- package/runtime/agents/workers/report-writer-worker.md +4 -2
- package/runtime/prompts/launch.template.md +7 -5
- package/runtime/prompts/lead/convergence.md +22 -0
- package/runtime/prompts/lead/plan-body-verification.md +45 -6
- package/runtime/prompts/lead/report-writer.md +2 -0
- package/runtime/prompts/profiles/error-analysis.md +1 -0
- package/runtime/prompts/profiles/final-verification.md +1 -1
- package/runtime/prompts/profiles/implementation-planning.md +6 -4
- package/runtime/prompts/profiles/improvement-discovery.md +2 -1
- package/runtime/prompts/profiles/requirements-discovery.md +2 -1
- package/runtime/python/okstra_ctl/build_tools.py +95 -0
- package/runtime/python/okstra_ctl/incremental_scope.py +95 -0
- package/runtime/python/okstra_ctl/render.py +26 -11
- package/runtime/python/okstra_ctl/render_final_report.py +24 -1
- package/runtime/python/okstra_ctl/rollup.py +49 -0
- package/runtime/python/okstra_ctl/scope_provenance.py +98 -1
- package/runtime/python/okstra_ctl/stage_citations.py +47 -0
- package/runtime/python/okstra_ctl/workflow.py +7 -4
- package/runtime/schemas/final-report-v1.0.schema.json +60 -1
- package/runtime/skills/okstra-brief-gen/SKILL.md +38 -11
- package/runtime/skills/okstra-rollup/SKILL.md +3 -2
- package/runtime/templates/reports/brief.template.md +42 -11
- package/runtime/templates/reports/fan-out-unit.template.md +9 -4
- package/runtime/templates/reports/final-report.template.md +20 -0
- package/runtime/templates/reports/final-verification-input.template.md +4 -2
- package/runtime/templates/reports/i18n/en.json +11 -4
- package/runtime/templates/reports/i18n/ko.json +11 -4
- package/runtime/templates/reports/improvement-discovery-input.template.md +2 -2
- package/runtime/validators/validate-brief.py +171 -26
- package/runtime/validators/validate-run.py +450 -78
- package/runtime/validators/validate_fanout.py +19 -10
- package/runtime/validators/validate_improvement_report.py +17 -4
package/README.md
CHANGED
|
@@ -216,7 +216,7 @@ Major workflow changes added to `main` after 0.8.0:
|
|
|
216
216
|
- **Brief as translation layer + Step 6.5 reporter batch confirmation** — `okstra-brief-gen` converts external input—an issue ticket, requirements document, or user message—verbatim and marks okstra-added content as labeled augmentation. Step 6.5 asks the user to confirm in one batch whether that conversion changed meaning and records the result in `Reporter Confirmations`. Every analysis profile requires this section before phase analysis begins; `validators/validate-brief.py` enforces the requirement.
|
|
217
217
|
- **Artifact-home rule (`.okstra/`)** — `<project>/.okstra/` is the only project artifact root owned by okstra. Anything outside this root is not okstra memory and may be read only when explicitly cited in Source Material or Reporter Confirmations. Writing outside the root requires the same explicit requested path. Internal equivalents are `glossary.md` for terminology and `decisions/<NNNN>-<slug>.md` for decision records, evaluated during `implementation-planning`.
|
|
218
218
|
- **Self-contained HTML final-report view** — After Phase 7 writes `final-report-<task-type>-<seq>.md`, `okstra render-views` automatically creates a sibling self-contained HTML view in the same `reports/` directory, with inline CSS/JavaScript and no external URLs. Its `Export user response` button serializes `## 1. Clarification Items` responses to `runs/<task-type>/user-responses/user-response-<task-type>-<seq>.md` for the next phase. View generation never changes the source Markdown.
|
|
219
|
-
- **`improvement-discovery` task type (sidetrack entry point)** — Within a codebase scope and priority-lens allowlist, multi-worker consensus produces N improvement candidates, with a default of eight and a hard cap of 12. This is a sidetrack entry point outside `PHASE_SEQUENCE`; the user selects candidates and starts each under a new task ID with `requirements-discovery`, `implementation-planning`, or `error-analysis`. Lens enum SSOT: [`scripts/okstra_ctl/improvement_lenses.py`](scripts/okstra_ctl/improvement_lenses.py). Output section: `## 5.9 Improvement Candidates` (
|
|
219
|
+
- **`improvement-discovery` task type (sidetrack entry point)** — Within a codebase scope and priority-lens allowlist, multi-worker consensus produces N improvement candidates, with a default of eight and a hard cap of 12. This is a sidetrack entry point outside `PHASE_SEQUENCE`; the user selects candidates and starts each under a new task ID with `requirements-discovery`, `implementation-planning`, or `error-analysis`. Lens enum SSOT: [`scripts/okstra_ctl/improvement_lenses.py`](scripts/okstra_ctl/improvement_lenses.py). Output section: `## 5.9 Improvement Candidates` (11-column table). Validator: [`validators/validate_improvement_report.py`](validators/validate_improvement_report.py).
|
|
220
220
|
|
|
221
221
|
<a id="ops-commands"></a>
|
|
222
222
|
### 3.5 Operations commands
|
package/docs/architecture.md
CHANGED
|
@@ -474,7 +474,7 @@ The stage-group interaction order is: **G1 select base → G2 confirm stages (se
|
|
|
474
474
|
[requirements-discovery | implementation-planning | error-analysis] (new task-id per selected candidate)
|
|
475
475
|
````
|
|
476
476
|
|
|
477
|
-
A sidetrack entry point that is not a formal member of `PHASE_SEQUENCE`. It supports codebase-discovery scenarios without breaking the one-way lifecycle. The lens allowlist and candidate cap are consolidated in the single source of truth `scripts/okstra_ctl/improvement_lenses.py`.
|
|
477
|
+
A sidetrack entry point that is not a formal member of `PHASE_SEQUENCE`. It supports codebase-discovery scenarios without breaking the one-way lifecycle. The lens allowlist and candidate cap are consolidated in the single source of truth `scripts/okstra_ctl/improvement_lenses.py`. `validators/validate_improvement_report.py` checks eleven contract items against the final report; one of them is the shape of the `## 5.9 Improvement Candidates` table, whose eleven columns run from `Cand ID` through `Evidence` (the two counts are independent and happen to coincide). Two bidirectional grilling points—an enhanced budget of 8 in `okstra-brief-gen` Step 4 and the lead's Phase 1.5 reflect-back budget of 12—align the user's and AI's understanding.
|
|
478
478
|
|
|
479
479
|
### requirements-discovery fan-out
|
|
480
480
|
|
package/docs/cli.md
CHANGED
|
@@ -124,7 +124,7 @@ For standard values and phase-specific responsibilities, see [Task type](#--task
|
|
|
124
124
|
- `scan-scope`: one or more paths.
|
|
125
125
|
- `out-of-scope`: optional.
|
|
126
126
|
- `candidate-cap`: 1–12; default 8.
|
|
127
|
-
- Output: the `## 5.9 Improvement Candidates` table with
|
|
127
|
+
- Output: the `## 5.9 Improvement Candidates` table with 11 columns: Cand ID / Lens / Title / Scope / Severity / Effort / Consensus / Source workers / Recommended next-phase / Expected behavior after / Evidence.
|
|
128
128
|
- Verdict Token: `candidates-ready` / `no-candidates` / `blocked`.
|
|
129
129
|
- Routing: there is no automatic spin-off. The user selects candidates and starts each under a new task ID with `requirements-discovery`, `implementation-planning`, or `error-analysis`.
|
|
130
130
|
- Workers: claude + codex + antigravity + report-writer are all required.
|
package/package.json
CHANGED
package/runtime/BUILD.json
CHANGED
|
@@ -78,6 +78,7 @@ For the report writer specifically, the `## Inputs` list always includes:
|
|
|
78
78
|
- `templates/reports/i18n/en.json` and `templates/reports/i18n/ko.json`.
|
|
79
79
|
- Every analysis worker's result file under `worker-results/`.
|
|
80
80
|
- `state/convergence-<task-type>-<seq>.json` (if present). When present, reproduce its `roundHistory[]`, `round2SkippedReason`, and `finalClassificationCounts` verbatim into the final report's Section 6 Round History sub-table — do not recompute from worker results.
|
|
81
|
+
- `<instruction-set>/task-brief.md` — the brief this run was prepared from. The lead already lists it under `## Inputs`; what it is FOR is the `## Expected Behavior` / `## Preserved Behavior` / `## Expected Outcome` items, whose `EB-NNN` / `PB-NNN` / `EO-NNN` ids are exactly what `endStateCoverage` maps. Read the instruction-set copy, NOT the project's `.okstra/briefs/...` path from the task manifest — that path is not resolvable here, same as the full schema source. A report that omits an id the brief pinned is rejected by the run validator.
|
|
81
82
|
|
|
82
83
|
For the carry-in `clarification-response.md` (if present), walk every row of `## 1. Clarification Items` including rows whose `User input` cell is blank — a blank cell with `Status=open` is a signal you must surface in the conditional `## 0. Clarification Response Carried In From Previous Run` section (the template's `RENDER_IF` guard activates it when the carry-in path is non-empty). When no carry-in path was provided, OMIT the `## 0.` heading entirely — do NOT write an empty-state stub.
|
|
83
84
|
|
|
@@ -114,10 +115,11 @@ Rules (the schema enforces most of these — they are listed here so you know *w
|
|
|
114
115
|
- If evidence is missing, write `"I don't know"` in the relevant statement field rather than fabricating confidence.
|
|
115
116
|
- Cite file paths and line numbers in every `evidence.primary[].source` / `consensus[].evidence` cell.
|
|
116
117
|
- Preserve every analysis worker's ticket tagging — every row's `ticketId` field carries the ticket key or the task-fallback. For single-ticket runs, set `ticketCoverage` to `{"singleTicket": "<ticket>"}`. For runs that do not require ticket tagging (`release-handoff`, `final-verification`), set `ticketCoverage` to `{"omit": true}`.
|
|
118
|
+
- For `requirements-discovery`, `error-analysis`, and `implementation-planning`, populate the top-level `endStateCoverage` with exactly one row per end-state id the brief declares — no more, no fewer. `disposition` is one of `addressed` / `deferred` / `not-applicable` / `blocked`. `addressed` requires a `coveredBy` anchor in THIS phase's own deliverable (requirements-discovery: the routing decision, the fan-out unit id, or the `C-NNN` clarification; error-analysis: the root-cause candidate or the next diagnostic; implementation-planning: the `R-NNN` row); every other disposition requires a `rationale`. Do not author a goal of your own here and do not restate the brief — this table records only how this phase accounted for what the reporter already pinned. When the brief declares no end-state ids (a brief authored before those sections existed), omit the field entirely. **Enforced:** `validators/validate-run.py` `_validate_end_state_coverage`.
|
|
117
119
|
- For `implementation-planning`, populate `implementationPlanning.requirementCoverage` with one row per concrete requirement from the brief / packet, using IDs `R-001`, `R-002`, ... in source order. A `covered` row's `coveredBy` MUST name the specific Option Candidate plus Stage/Step that satisfies the requirement. Use `status: "covered"` only when the report's plan actually covers it; use `documented-deviation` only when `coveredBy` states the concrete alternative and the row records non-empty unique `decisionRefs` plus `approvalDisposition`. Each `C-NNN` ref must name a clarification in this report; each `D-NNNN` ref must name a `decisionDrafts[].number`. `approvalDisposition: "accepted"` requires a referenced clarification with `status: answered|resolved` and non-empty `userInput`; `approvalDisposition: "blocked C-NNN"` requires that same-report clarification to be `status: open, blocks: approval`. Otherwise use `gap` or `blocked C-NNN` and ensure the corresponding `Clarification Items` row blocks approval. Do not collapse this into `ticketCoverage`; ticket coverage is not requirement coverage. **Enforced:** `schemas/final-report-v1.0.schema.json` `$defs.ImplementationRequirementCoverageRow` and `validators/validate-run.py` `_validate_requirement_deviations`.
|
|
118
|
-
- For `implementation-planning`, each `requirementCoverage` row's `source` is a graded cell, not prose — free text like `"carry-in from requirements-discovery C-001"` is rejected. Write exactly one of: `brief
|
|
120
|
+
- For `implementation-planning`, each `requirementCoverage` row's `source` is a graded cell, not prose — free text like `"carry-in from requirements-discovery C-001"` is rejected. Write exactly one of: `brief:EB-001` / `brief:PB-001` / `brief:EO-001`, an end-state id the brief declares — when the brief pins ids, citing a heading instead is rejected, because every brief carries the same generic headings and a heading cannot say WHICH reporter line the requirement came from (only a brief authored before the end-state sections existed still takes the older `brief:<heading>` form, and there the heading must literally exist in it); `derived:R-NNN — <one-line reason>`, whose chain must terminate at a `brief:` or `contract:` row of the same table without cycling; or `contract:<rule>`, for artifacts okstra's own phase contract mandates, whose allowlist is exactly the two tokens `decision-record-step` (the §5.4 Decision Drafts materialization step) and `glossary-step` (the glossary proposal step) — any other rule name is rejected, so never invent one. (Maintainer SSOT for that allowlist: `scripts/okstra_ctl/scope_provenance.py` in the okstra repo.) A requirement you cannot source this way does not belong in the table: put it in `clarificationItems[]` with `Blocks=approval`. **Enforced:** `validators/validate-run.py` `_validate_requirement_provenance`. In the same table, anchor every stage number in `coveredBy` to a `Stage` / `Stages` word (`Stage 2`, `Stages 1-3`) — `_validate_stage_has_requirement` reads that cell as prose and fails the plan when a Stage Map stage is cited by no row.
|
|
119
121
|
- For `implementation-planning`, also populate `implementationPlanning.decisionDrafts` (one row per decision meeting all three decision-record criteria; `[]` otherwise) and `implementationPlanning.skippedAdrCandidates` (evaluated-but-dropped adr-candidates; `[]` otherwise). The schema excerpt enumerates the row shape; the renderer emits §5.4 `### Decision Drafts`. When `decisionDrafts` is non-empty, the plan's stages MUST carry a stepwise step that creates `.okstra/decisions/<NNNN>-<slug>.md` (validate-run gates this).
|
|
120
|
-
- When the `Task Type` is `improvement-discovery`, populate `## 5.9 Improvement Candidates` with the
|
|
122
|
+
- When the `Task Type` is `improvement-discovery`, populate `## 5.9 Improvement Candidates` with the 11-column schema enforced by `validators/validate_improvement_report.py`. The `Expected behavior after` cell states in one observable sentence what becomes different once the candidate is applied — it seeds the downstream brief's `EB-NNN` / `EO-NNN`, and an empty cell fails the run. Source the row IDs (`I-NNN`), lens whitelist, and Source workers patterns from `scripts/okstra_ctl/improvement_lenses.py` — do NOT introduce new lens names or worker prefixes. `improvement-discovery` is NOT in the data.json schema enum, so author its markdown directly (not via `okstra-render-final-report.py`). Immediately after writing the markdown, run (`Bash`): `okstra inject-report-index <markdown path> --report-language <en|ko>`. That adds the top-of-report Index plus `I-NNN` / `C-NNN` scroll anchors; the run validator fails the report when the Index anchor is absent.
|
|
121
123
|
|
|
122
124
|
Write the data.json (and the audit sidecar `.md`) with your `Write` tool — that is the canonical authoring path, and okstra ships no hook that blocks `.md` writes (its only settings hook is the `SessionEnd` trace-cleanup; the coding-preflight hook emits reminders but never blocks). A Bash heredoc is acceptable ONLY when a specific `Write` call is genuinely rejected by the host environment, and it MUST produce byte-identical content — do not reach for it pre-emptively. Then invoke the renderer (`Bash`): `okstra render-final-report <data.json path>`. Confirm both files exist and respond with a short status line prefixed by your model identity, per the preamble §"Return message to the lead":
|
|
123
125
|
|
|
@@ -91,10 +91,10 @@ Emit one `PROGRESS: <phase-id> <verb-phrase>` line as plain user-facing text at
|
|
|
91
91
|
|
|
92
92
|
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.
|
|
93
93
|
|
|
94
|
-
1. **
|
|
95
|
-
-
|
|
96
|
-
-
|
|
97
|
-
-
|
|
94
|
+
1. **List the answered clarifications (a fact, not a judgement).** Collect the `C-NNN` ids you reconciled as answered in §1, and the IDs of any PREP items changed by the carried response. Pass the ids through `--answered-clarifications`; the CLI resolves which Stage Map stages each one touches by reading the prior run's own links — the `P-*` plan item that carried that `clarificationId`, and the requirement-coverage row whose status is `blocked C-NNN`. Do NOT map answers to stage numbers yourself.
|
|
95
|
+
- An id that traces to no stage in the prior report makes the CLI return `mode == "full"` for the whole re-run. That is the intended outcome: an answer whose blast radius the prior report never recorded must not narrow anything.
|
|
96
|
+
- **Structural change is your call, and you declare it.** When an answer overturns the selected Option, restructures the stages, or changes the recommended approach, pass `--full-reason "<what changes and how>"`. It forces `mode == "full"` regardless of every other input — the back-trace resolves stages, it cannot judge whether the plan's shape survived. Do NOT express this by leaving the CSVs empty: that produces the same `no impacted stages resolved` reason as a re-run that simply failed to map anything, and the two become indistinguishable in the record. `--impacted` remains available for stage numbers you resolved yourself.
|
|
97
|
+
- Pass ONLY stage numbers that appear in that Stage Map when you do use `--impacted`. Never invent or guess a stage number — a number absent from the graph must never enter `--impacted` (it would leak into `reverify_stages`).
|
|
98
98
|
2. **Resolve the two base SHAs (safety condition C1 — code-unchanged, decided by the CLI, not by you).**
|
|
99
99
|
- Current base SHA: `{{EXECUTOR_WORKTREE_BASE_REF}}` (this run's resolved worktree base commit).
|
|
100
100
|
- 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.
|
|
@@ -105,7 +105,9 @@ The **default is full re-verification**. Only narrow this re-run to the impacted
|
|
|
105
105
|
--cur-base-sha {{EXECUTOR_WORKTREE_BASE_REF}} \
|
|
106
106
|
--prev-base-sha <prior baseRef from step 2> \
|
|
107
107
|
--impacted <csv of impacted stage numbers, empty for full> \
|
|
108
|
-
--prep-items <csv of changed PREP item ids, empty when none>
|
|
108
|
+
--prep-items <csv of changed PREP item ids, empty when none> \
|
|
109
|
+
--answered-clarifications <csv of answered C-NNN ids, empty when none> \
|
|
110
|
+
--full-reason "<empty, or what structural change forces full>"
|
|
109
111
|
```
|
|
110
112
|
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.
|
|
111
113
|
4. **`mode == "full"`** → run the existing full re-verification path unchanged; ignore `reverify_stages` / `carry_stages`.
|
|
@@ -246,6 +246,15 @@ The two errors paths carry the same absolute values the lead forwarded in the in
|
|
|
246
246
|
|
|
247
247
|
Relative to the Phase 4 anchor set rendered by `okstra_ctl.worker_prompt_headers.worker_prompt_headers()`, a reverify prompt adds `**Model:**` and drops two anchors whose targets lightweight mode never reads: `**Worker Preamble Path:**` and `**Coding preflight pack:**`.
|
|
248
248
|
|
|
249
|
+
For an `antigravity` assignment, append the exact `PLAIN_FILE_WRITE_HEADER`
|
|
250
|
+
value from `okstra_ctl.worker_prompt_headers` immediately after
|
|
251
|
+
`**Read scope:**` and before the prompt body. Copy it verbatim from that role's
|
|
252
|
+
persisted initial Phase 4 prompt; do not paraphrase or reconstruct it. If the
|
|
253
|
+
persisted initial prompt does not contain that generated header, abort the
|
|
254
|
+
reverify dispatch and record a `contract-violation` event instead of
|
|
255
|
+
dispatching without the plain-file safeguard. This provider-specific header is
|
|
256
|
+
outside the common 9-header count above. Other providers do not receive it.
|
|
257
|
+
|
|
249
258
|
The rationale for both drops is §"Reverify prompt: required-reading suppression" below.
|
|
250
259
|
|
|
251
260
|
`<modelExecutionValue>` MUST be resolved from one of these canonical sources, in priority order:
|
|
@@ -256,6 +265,19 @@ The rationale for both drops is §"Reverify prompt: required-reading suppression
|
|
|
256
265
|
|
|
257
266
|
If none of the three is available, **abort the reverify dispatch for that role** and record a `contract-violation` event via `okstra error-log append-observed`. Do NOT guess or fall back to a runtime default. The selected adapter receives the exact value through the assignment and owns how it reaches the worker runtime. The current Codex catalog default is `gpt-5.6-sol`, but that reference value never replaces the manifest assignment.
|
|
258
267
|
|
|
268
|
+
### Required reverify output contract (BLOCKING)
|
|
269
|
+
|
|
270
|
+
Every lightweight, adversarial, full-reanalysis, and plan-body reverify prompt
|
|
271
|
+
MUST append this block verbatim after its variant-specific response format:
|
|
272
|
+
|
|
273
|
+
```markdown
|
|
274
|
+
## Output Contract
|
|
275
|
+
|
|
276
|
+
- Write the complete response to the exact `**Result Path:**` before returning.
|
|
277
|
+
- Write the reverify-session audit to `**Audit sidecar path:**` before returning; list the evidence paths actually inspected, or state that the prompt required no external evidence reads.
|
|
278
|
+
- Do not return only an inline response. Inline text is diagnostic output, not the persisted worker result.
|
|
279
|
+
```
|
|
280
|
+
|
|
259
281
|
### Reverify prompt: required-reading suppression
|
|
260
282
|
|
|
261
283
|
Reverify prompts MUST NOT inject the Phase 2 `[Required reading]` clause:
|
|
@@ -94,7 +94,7 @@ The verdict tokens `AGREE` / `DISAGREE` / `SUPPLEMENT` are reused, but their mea
|
|
|
94
94
|
- **AGREE**: the item is executable as written *and* internally consistent with other items in the plan.
|
|
95
95
|
- **DISAGREE(<kind>)**: the item is broken. `<kind>` MUST be one of:
|
|
96
96
|
- `a` — a concrete referenced file path / symbol **contradicts** a different concrete path / symbol for the same artifact in another step or option's File Structure list (a genuine mismatch between two spelled-out references). An abbreviated / ellipsis (`…`) / under-specified path is NOT kind `a` — it contradicts nothing, it is merely imprecise notation; classify it as `b`.
|
|
97
|
-
- `b` — a command **or a referenced path** is not executable or is ambiguous — including an abbreviated, ellipsis, or under-specified path that does not resolve as written
|
|
97
|
+
- `b` — a command **or a referenced path** is not executable or is ambiguous — including an abbreviated, ellipsis, or under-specified path that does not resolve as written. A command that fails **only** because the planning-time worktree lacks build/test dependencies is NOT kind `b` — see §"Planning-time environment gap" below.
|
|
98
98
|
- `c` — validation signal is not observable
|
|
99
99
|
- `d` — rollback violates commit / dependency order
|
|
100
100
|
- `e` — item contradicts the trade-off matrix
|
|
@@ -116,6 +116,33 @@ The semantic checks above are the plan-body enforcement layer for the schema-val
|
|
|
116
116
|
|
|
117
117
|
Worker non-result handling (`timeout`, `error`, no result file, wrapper `cli-failure`) is identical to finding convergence: do NOT aggregate as DISAGREE, record `contract-violation`, and apply the round-level abort rule below.
|
|
118
118
|
|
|
119
|
+
## Planning-time environment gap
|
|
120
|
+
|
|
121
|
+
`implementation-planning` runs in the task-key worktree, and that worktree carries **no installed
|
|
122
|
+
build/test dependencies** — `scripts/okstra_ctl/worktree.py` `DEFAULT_WORKTREE_SYNC_DIRS` mirrors
|
|
123
|
+
only okstra-owned directories, and installing dependencies here would violate the profile's
|
|
124
|
+
"no build/migration/deploy execution" boundary. So a plan's `npm test` / `yarn workspace … build` /
|
|
125
|
+
`pytest` command **cannot succeed at plan time by construction**, no matter how correctly the plan
|
|
126
|
+
wrote it.
|
|
127
|
+
|
|
128
|
+
This is a property of the phase, not a defect of the plan. Judge accordingly:
|
|
129
|
+
|
|
130
|
+
| What the verifier observes | Verdict |
|
|
131
|
+
|---|---|
|
|
132
|
+
| The command is not declared anywhere (no such script in `package.json` / `Makefile` / task runner) | `DISAGREE(b)` — the plan cites a command that does not exist |
|
|
133
|
+
| The command is declared, but running it here fails on missing dependencies (`exit 127`, `command not found`, missing module/binary) | `UNVERIFIABLE` — not `DISAGREE(b)` |
|
|
134
|
+
| The command is declared and its arguments contradict the plan's own file structure | `DISAGREE(a)` or `DISAGREE(b)` per the usual rules |
|
|
135
|
+
| A referenced **path** does not exist in the tree | `DISAGREE(b)` unchanged — paths are checkable here; dependencies are not |
|
|
136
|
+
|
|
137
|
+
**What this phase can confirm about a command is that it is defined**, that its arguments name real
|
|
138
|
+
paths, and that its stated success signal is observable in principle. Execution success is the
|
|
139
|
+
`implementation` run's evidence to produce, not this round's.
|
|
140
|
+
|
|
141
|
+
Recording `UNVERIFIABLE` is the honest outcome — it is persisted as `verification-error`, excluded
|
|
142
|
+
from both numerator and denominator, and never converted to `DISAGREE`. Voting `DISAGREE(b)` on a
|
|
143
|
+
dependency-missing command instead manufactures a defect the planner cannot fix by editing the plan,
|
|
144
|
+
which is what sends the item into a self-fix round that can only paper over it.
|
|
145
|
+
|
|
119
146
|
## Mode constraint
|
|
120
147
|
|
|
121
148
|
Plan-body verification only supports **lightweight mode** (defined in [convergence](./convergence.md) §"Verification Mode"). `full-reanalysis` is not meaningful here because the "original source materials" for a plan item are the worker's own analysis plus the lead-mediated synthesis — there is no independent ground truth to re-read. The manifest's top-level `verificationMode` is ignored for this round; lightweight is always used.
|
|
@@ -127,7 +154,7 @@ Exception for `P-Req-*`: verifiers still MUST NOT re-open the original task brie
|
|
|
127
154
|
When `config.adversarial == true` (the default for `implementation-planning`; see [convergence](./convergence.md) §"Configuration"), the plan-body round runs with an **adversarial posture**. The classification rules and gate arithmetic in §"Round protocol" are UNCHANGED — `majority-disagree` (a *majority* of analysers DISAGREE) remains the only classification that blocks approval, and `dissent-isolated` still passes the gate. Adversarial mode changes only *how each verifier evaluates an item*:
|
|
128
155
|
|
|
129
156
|
- The burden of proof sits on the plan: an item earns `AGREE` only if the verifier actively tried to break it and could not.
|
|
130
|
-
- The verifier MUST open the file paths / symbols / commands the item cites and confirm they exist and are
|
|
157
|
+
- The verifier MUST open the file paths / symbols / commands the item cites and confirm they exist and are **defined** as written. This is the one allowed widening of the lightweight "judge from internal consistency and stated commands / paths" rule — confirming the existence of cited paths is not "re-analyzing the original requirements". The widening stops at *definition*: a build/test command's **execution success** is out of scope here, because the planning worktree has no dependencies installed (§"Planning-time environment gap"). Confirm the script is declared; do not treat its failure to run as evidence against the plan.
|
|
131
158
|
- If a cited path / command / validation signal cannot be confirmed, the verifier responds `DISAGREE(<kind>)` with the applicable breakage kind (a–f); uncertainty resolves toward DISAGREE, not AGREE.
|
|
132
159
|
- **Single-vote-blocking kinds.** A single `DISAGREE` is approval-blocking on its own — no majority needed — when the breakage kind is `a` (cited path/symbol mismatch) or `d` (rollback violates commit/dependency order) on *any* plan item, or `f` (requirement-coverage mismatch) on a `P-Req-*` item. These defects are concrete, safety-critical, and adversarially verifiable (the verifier confirmed the cited path / order / requirement), so one correct dissent must not be outvoted. Each creates a `majority-disagree` classification and MUST become a `Blocks=approval` clarification row. Kinds `b` / `c` / `e` still need a majority — `b` especially is prone to planning-vs-implementation environment false positives. Because `a` is reserved for a concrete contradiction between two spelled-out references (see §"Plan-body verdict semantics"), an abbreviated / ellipsis / under-specified path is raised as `b` (majority-gated), never `a` — a lone "this path is abbreviated" dissent must not single-vote-block on notation alone, and it is especially not blocking on a *rejected* option that will never be implemented. **Enforced:** `validators/validate-run.py` `_classify_plan_item_gate` (`_SINGLE_VOTE_BLOCKING_KINDS = {a, d}` + the P-Req `f` rule).
|
|
133
160
|
|
|
@@ -156,7 +183,7 @@ Plan-body verification stays **lightweight** even under this posture — the `ve
|
|
|
156
183
|
**Record the cause, not just the outcome.** The gate value names the outcome; `planBodyVerification.gateBlockedBy` (array) names every input that blocked it — `majority-disagree`, `coverage-gap`, `non-result`. Two independent inputs can block: a `majority-disagree` plan item, and a Requirement Coverage `gap` / `blocked C-NNN` row (`prompts/profiles/implementation-planning.md` §"Requirement Coverage"). A coverage-only block still renders as `blocked-by-disagreement` because that is the only blocking non-abort value, so **without `gateBlockedBy` the report asserts a worker disagreement that never happened** and the reader hunts for a dissent that does not exist. Leave the array empty for a passing gate. **Enforced:** `validators/validate-run.py` `_validate_gate_blocked_by` cross-checks the declared causes against the recorded verdicts and coverage rows, and fails a passing gate that has a blocking coverage row — the coverage rule was prose-only before.
|
|
157
184
|
|
|
158
185
|
**A coverage row citing this run's own `C-NNN` is not an independent blocker.** When a coverage row's `blocked C-NNN` points at a clarification that step 8 below promoted from a `majority-disagree` item in *this same run*, that blocker is already counted once as the plan item. Counting it again as a coverage gap makes the run block on a clarification it just authored, and the row carries into the next run as a fresh blocker — the Requirement Coverage ↔ Clarification cycle. Such rows are excluded from `coverage-gap`. **Enforced:** `validators/validate-run.py` `_independent_coverage_blockers`.
|
|
159
|
-
6. Lead writes `runs/<task-type>/state/plan-body-verification-<task-type>-<seq>.json` (schema below) and populates `### 5.5.9 Plan Body Verification` in the final report's data.json (`implementationPlanning.planBodyVerification`, schema `schemas/final-report-v1.0.schema.json`; template at `templates/reports/final-report.template.md`). The §5.5.9 body is **grouped by plan item**: `planItems[]`, each carrying its `id`, its plain-language `subject` (rendered as the item heading), an optional `sourceSection`, an optional `clarificationId` (the `C-<N>` this item blocks on when `majority-disagree`), and a `verdicts[]` list (`worker / verdict / breakageKind / note`) — one verdict row per worker under that item. The renderer prints three fixed legends (gate values, verdict tokens, breakage kinds a–f) so the reader can decode every cell without opening this spec. The older flat `#### Verdict details` table (`Plan item / Worker / …`, one row per plan-item × worker pair) is superseded by the grouped layout — it hid *what* each vote was about behind a bare `P-*` ID; the subject heading is the fix. The validator's `Plan Body Verification` + `Gate result:` substring checks still gate this section.
|
|
186
|
+
6. Lead records `planBodyVerification.participatingAnalysers` as `{rostered, voting}` — how many analysers the roster carried, and how many actually returned a non-error vote. The gate arithmetic is unchanged, but a shrunken roster loosens it silently: with two participating analysers a 1-AGREE / 1-DISAGREE split is a tie, so it never reaches `majority-disagree` and the dissent passes as `dissent-isolated`. A reader comparing two runs' gate values cannot see that without this pair. **Enforced:** `validators/validate-run.py` `_validate_participating_analysers` recomputes `voting` from the recorded verdicts and fails a declared figure the table denies. Then lead writes `runs/<task-type>/state/plan-body-verification-<task-type>-<seq>.json` (schema below) and populates `### 5.5.9 Plan Body Verification` in the final report's data.json (`implementationPlanning.planBodyVerification`, schema `schemas/final-report-v1.0.schema.json`; template at `templates/reports/final-report.template.md`). The §5.5.9 body is **grouped by plan item**: `planItems[]`, each carrying its `id`, its plain-language `subject` (rendered as the item heading), an optional `sourceSection`, an optional `clarificationId` (the `C-<N>` this item blocks on when `majority-disagree`), and a `verdicts[]` list (`worker / verdict / breakageKind / note`) — one verdict row per worker under that item. The renderer prints three fixed legends (gate values, verdict tokens, breakage kinds a–f) so the reader can decode every cell without opening this spec. The older flat `#### Verdict details` table (`Plan item / Worker / …`, one row per plan-item × worker pair) is superseded by the grouped layout — it hid *what* each vote was about behind a bare `P-*` ID; the subject heading is the fix. The validator's `Plan Body Verification` + `Gate result:` substring checks still gate this section.
|
|
160
187
|
7. **Self-fix loop (up to `selfFixMaxRounds`, targeting planner-fixable defects).** After aggregation, while at least one `majority-disagree` item has a majority of its `DISAGREE` verdicts at `fixability == planner-fixable`, lead runs self-fix rounds **before** promoting anything to the user:
|
|
161
188
|
- **Group the targets by cause before instructing (BLOCKING).** Blocked items are usually several derivatives of one defect — one constant declared twice, one responsibility given two owners — and the coverage rows that cite them fail as a consequence, not independently. Lead MUST partition this round's targets into cause groups and instruct each group as **"remove this cause"**, naming the derivatives it accounts for. **Handing report-writer a bare item list is forbidden**: patched one at a time, each correction leaves the sibling sections still asserting the old value, so the next round re-finds the same family and the budget drains without converging. Record the partition in `planBodyVerification.selfFixGroups[]` (`round`, `causeSummary`, `itemIds`). One group per item is a legitimate outcome only when the items genuinely share no cause — recorded that way, it is a visible diagnosis rather than a skipped one. **Enforced:** `validators/validate-run.py` `_validate_self_fix_grouping` requires the partition, ties `selfFixRoundsApplied` to the highest recorded round, and fails any corrected item that belongs to no group.
|
|
162
189
|
- lead instructs report-writer to rewrite the items in each cause group (NOT a full draft regeneration; procedure in [report-writer](./report-writer.md) §"Self-fix rewrite").
|
|
@@ -169,6 +196,7 @@ Plan-body verification stays **lightweight** even under this posture — the `ve
|
|
|
169
196
|
- `all-resolved` — no planner-fixable `majority-disagree` item remains. Exit.
|
|
170
197
|
- `no-progress` — the round resolved **zero** planner-fixable items relative to the previous round. Exit even with budget left: the same rewrite would repeat. Newly *introduced* defects count against progress, so a rewrite that trades one defect for another stops the loop rather than churning.
|
|
171
198
|
- `max-rounds-reached` — `selfFixRoundsApplied == selfFixMaxRounds`. Exit.
|
|
199
|
+
- `cause-group-recurrence` — this round's `itemIds` are a subset of what the *previous* round left unresolved (its targets minus the ones that earned a `selfFixNote` for that round). Exit even with budget left. `no-progress` counts resolutions, which a round can claim while its own correction plants the next round's defect; repeating the previous round's unresolved remainder is the earlier signal for the same dead end. Narrowing onto genuinely-open items is progress and does NOT fire this. **Enforced (advisory):** `validators/validate-run.py` `_detect_self_fix_recurrence` reports the shape as a warning — it does not fail the run, because the boundary between narrowing and re-digging is not settled.
|
|
172
200
|
- `not-attempted` — the loop never ran because no item qualified.
|
|
173
201
|
The `no-progress` and `max-rounds-reached` exits are what make the loop terminate; `selfFixMaxRounds` alone is the backstop.
|
|
174
202
|
- a `majority-disagree` item with a majority of `needs-user-input` is NOT a self-fix target — it goes straight to the next step's clarification promotion.
|
|
@@ -244,6 +272,9 @@ Plan-body verification stays **lightweight** even under this posture — the `ve
|
|
|
244
272
|
|
|
245
273
|
Required prompt anchor headers are identical to finding convergence (see [convergence](./convergence.md) §"Required reverify-prompt anchor headers"). The prompt body changes from F-* listing to P-* listing:
|
|
246
274
|
|
|
275
|
+
The [convergence](./convergence.md) §"Required reverify output contract"
|
|
276
|
+
applies unchanged: append it verbatim after the response format below.
|
|
277
|
+
|
|
247
278
|
````
|
|
248
279
|
You are <worker-role> performing plan-body verification for <task-key> (round 1).
|
|
249
280
|
|
|
@@ -257,7 +288,7 @@ verdict:
|
|
|
257
288
|
other items in the plan.
|
|
258
289
|
- **DISAGREE(<kind>)**: The item is broken. Cite which kind:
|
|
259
290
|
(a) a concrete referenced file path / symbol contradicts a different concrete path / symbol for the same artifact elsewhere — a genuine mismatch; an abbreviated / ellipsis path is NOT (a), use (b),
|
|
260
|
-
(b) command or referenced path is not executable or is ambiguous — including an abbreviated / ellipsis / under-specified path that does not resolve as written,
|
|
291
|
+
(b) command or referenced path is not executable or is ambiguous — including an abbreviated / ellipsis / under-specified path that does not resolve as written. A command that IS declared but cannot run here because build/test dependencies are not installed is NOT (b) — answer UNVERIFIABLE,
|
|
261
292
|
(c) validation signal is not observable,
|
|
262
293
|
(d) rollback violates commit / dependency order,
|
|
263
294
|
(e) item contradicts the trade-off matrix,
|
|
@@ -266,8 +297,16 @@ verdict:
|
|
|
266
297
|
- **SUPPLEMENT**: The item is sound but a dependency / edge case / precondition
|
|
267
298
|
is missing.
|
|
268
299
|
- **UNVERIFIABLE**: Capability, credential, network, or service state prevents
|
|
269
|
-
verification of this item
|
|
270
|
-
|
|
300
|
+
verification of this item — **including the planning worktree having no
|
|
301
|
+
build/test dependencies installed**, which makes any `npm test` /
|
|
302
|
+
`yarn workspace … build` / `pytest` command fail here by construction. The
|
|
303
|
+
input alias is persisted as `verification-error`; Fixability is not applicable.
|
|
304
|
+
|
|
305
|
+
You are judging a plan, not running it. For a build/test command, confirm it is
|
|
306
|
+
**declared** (a real script/target in `package.json`, `Makefile`, or the task
|
|
307
|
+
runner) and that its arguments name real paths. If it is declared but will not
|
|
308
|
+
run in this tree for lack of dependencies, that is UNVERIFIABLE — not a defect
|
|
309
|
+
of the plan. If it is not declared at all, that IS `DISAGREE(b)`.
|
|
271
310
|
|
|
272
311
|
Do NOT re-analyze the original requirements. Judge solely from plan internal
|
|
273
312
|
consistency and stated commands / paths. Do NOT inspect the original task brief
|
|
@@ -245,6 +245,8 @@ Lead instructs a self-fix round as **cause groups**, not a flat `P-*` list (`pla
|
|
|
245
245
|
|
|
246
246
|
**Enumerate before you edit.** Patching at the positions the lead named is what makes a round trade one defect for another: the correction lands, its siblings keep asserting the old value, and the next round finds a *new* contradiction the fix itself created. So for each cause group, first list every place the plan mentions that decision — grep the constant, the symbol, the path, the requirement ID across the whole plan body including rejected options, per-stage `Test case (…)` lines, `Acceptance`, `exitContract`, `stageValidation`, and the Requirement Coverage row — then reconcile each hit against the new decision and only then write. Record the enumeration in the group's supersession entry so the next round can see what was considered in scope. A patch that leaves its own contradictions standing produces the same defect class in the next round, so the loop spends its budget re-finding what the previous round created. Record each retirement in `implementationPlanning.supersessionLedger[]` exactly as the answer-carry-in rule requires (`_common-contract.md` §"Supersession").
|
|
247
247
|
|
|
248
|
+
**Record the reach.** For each cause group you rewrite, list the data.json paths you actually changed in that group's `rewrittenPaths`, and the subset of those lying outside the sections its `itemIds` point at in `outsideScopePaths` (e.g. `stages[0].stepwiseExecution`, `validationChecklist[3]`). Carrying a correction to its contradictions legitimately reaches past the flagged item, so the second list is a measurement and not a violation — no threshold is applied to either. It exists because "this round was a targeted correction, not a full regeneration" is currently a claim with nothing behind it, and a round that quietly rewrites the whole draft costs the same tokens every time it repeats. **Enforced:** `validators/validate-run.py` `_validate_self_fix_rewrite_scope` requires every `outsideScopePaths` entry to appear in `rewrittenPaths`.
|
|
249
|
+
|
|
248
250
|
After correcting, leave `self-fixed in round <N>: <what was fixed>` in that `planItems[].selfFixNote`. Do NOT set `planBodyVerification.selfFixRoundsApplied` — the lead owns that count and records it at the round's end, so promotion eligibility does not hang on a worker's self-report. `needs-user-input` items are NEVER a correction target — they are promoted to clarification as-is.
|
|
249
251
|
|
|
250
252
|
Lead may instruct several rounds (bounded by `selfFixMaxRounds`, see [plan-body-verification](./plan-body-verification.md) §"Round protocol" step 7). Each round targets only the items still broken, so a round that fixes nothing new ends the loop — correcting an item by breaking a sibling counts as no progress, not progress.
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
- `conversion-block:` rows mean the brief could not map a reporter statement to project vocabulary; never invent the missing mapping in this phase.
|
|
17
17
|
- Worker diagnosis procedure:
|
|
18
18
|
- **Symptom lock:** state the reporter's symptom verbatim, then translate it into one observable failure condition. If no observable condition can be derived from the brief, record that gap as the first blocker instead of guessing.
|
|
19
|
+
- **Pass condition lock:** the brief's `EB-NNN` items state what the system must do once the defect is gone. State, per id, the observation that would show the symptom resolved — this is the upper bound on the fix. A diagnosis that leaves "how far do we fix this" open is what lets the later plan expand. Record each as an `endStateCoverage` row whose `coveredBy` names the root-cause candidate or next diagnostic that accounts for it. **Enforced:** `validators/validate-run.py` `_validate_end_state_coverage`.
|
|
19
20
|
- **Reproduction status:** classify the run as `reproduced`, `not-reproduced`, or `blocked-before-repro`. Cite the command/log/file evidence used. If no command can be run safely in this phase, explain the read-only evidence path and the exact material needed next.
|
|
20
21
|
- **Falsifiable cause candidates:** every root-cause candidate must include supporting evidence, the strongest falsifying evidence checked, confidence, and the next diagnostic action that would disprove it. A candidate that cannot be falsified is too vague for this phase.
|
|
21
22
|
- **Graph-aware scope:** a graph edge can explain ordering or duplication, but it is not proof of cause by itself. Cite code/log evidence before claiming an upstream related task caused the current symptom.
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
{{INCLUDE:_common-contract.md}}
|
|
11
11
|
- Primary focus areas (each maps to a deliverable section below):
|
|
12
12
|
- Acceptance-gating — a failure here pushes the verdict toward `blocked` / `conditional-accept`:
|
|
13
|
-
- requirement & acceptance coverage — every must-pass point in the brief's `##
|
|
13
|
+
- requirement & acceptance coverage — every must-pass point in the brief's `## Expected Behavior` / `## Preserved Behavior` / `## Expected Outcome` (and the approved plan's requirements) is covered with a cited artifact or raised as an Acceptance Blocker; no silent omissions
|
|
14
14
|
- delivered artifacts match recorded expected values in `reference-expectations` (config files, deployment manifests, other recorded expected states); when reference-expectations are absent, record it as missing information rather than assuming a match
|
|
15
15
|
- test & validation suite pass status — independently re-run the read-only two-tier command set (Tier 1 = brief/approved-plan `validation`, Tier 2 = `project.json` `qaCommands`) and confirm each passes on the verified head, citing exact command + exit code
|
|
16
16
|
- test correctness — delivered tests actually assert the intended behaviour: no gutted/weakened assertions, no tautological or always-passing tests, no tests exercising only mocks; new behaviour has matching coverage. For an `external-interface` / `transformation-mapping` surface specifically, treat a test whose only oracle is a self-authored synthetic fixture (no captured-real-sample provenance) as NOT establishing external correctness — it shows only that the parser agrees with its author's assumed shape, never that the shape matches reality; record that surface's external correctness as a user-owned external advisory gap (a Residual Risk carrying the exact "capture a real sample and confirm" next step, per the Coverage check in the self-review pass below), never as covered — the same non-blocking treatment the External QA advisory policy gives a live external check
|
|
@@ -43,6 +43,7 @@
|
|
|
43
43
|
- feasible plan options
|
|
44
44
|
- dependency and risk visibility
|
|
45
45
|
- recommended execution order
|
|
46
|
+
- one `endStateCoverage` row per brief end-state id, whose `coveredBy` names the `R-NNN` row that carries it. The two tables are a chain, not duplicates: `endStateCoverage` proves no reporter requirement was dropped, `requirementCoverage` proves each one reaches a stage.
|
|
46
47
|
- Implementation Design Preparation (`implementation-design-prep-v1`, BLOCKING):
|
|
47
48
|
- **Detector SSOT:** the planner MUST run the V1 detector defined by `scripts/okstra_ctl/design_surfaces.py` (`detect_design_surfaces()` over the detector's `RULES`) and MUST NOT invent or copy a second keyword list into the plan or prompt. **Enforced:** `validators/validate-run.py` `_validate_detector_coverage` reruns that detector and compares every `(stage, kind)` plus its trigger evidence.
|
|
48
49
|
- **Exactly-once coverage:** for every detector-produced `(stage, kind)`, the planner MUST write exactly one `designSurfaceCoverage` row on that stage. **Enforced:** `validators/validate-run.py` `_validate_detector_coverage` rejects missing, duplicate, extra-detector-kind, or evidence-mismatched rows; `schemas/final-report-v1.0.schema.json` `$defs.DesignSurfaceCoverage` enforces the row shape.
|
|
@@ -58,7 +59,7 @@
|
|
|
58
59
|
- 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".
|
|
59
60
|
- §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").
|
|
60
61
|
- **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.
|
|
61
|
-
- **Single incremental-scope decision:** the lead calls `okstra incremental-scope` exactly once for the re-run, passing answered-
|
|
62
|
+
- **Single incremental-scope decision:** the lead calls `okstra incremental-scope` exactly once for the re-run, passing the answered `C-NNN` ids through `--answered-clarifications`, changed design-preparation IDs through `--prep-items`, and any lead-resolved stage numbers through `--impacted`; the CLI unions all three before applying the existing dependency closure and cutoff. The clarification ids are resolved to stages by the CLI from the prior report's own `planItems[].clarificationId` and `blocked C-NNN` coverage links — the lead does not map answers to stage numbers. An answer that changes the selected option, Stage Map, or recommended approach is not a local impact: pass every CSV empty so the same call returns `mode == "full"`. A clarification id that traces to no stage, unknown PREP IDs, or invalid `stageRefs` also return an explicit full decision instead of being guessed.
|
|
62
63
|
- **Stage-aware carry:** for an incremental decision, pass its `carry_stages` and `reverify_stages` CSVs unchanged to `okstra incremental-carry`. The helper carries the prior whole stage rows and their owned PREP / `P-Prep-*` artifacts; overlap, cross-scope ownership, scope leaks, or canonical conflicts return `CarryError`. On that error, discard the partial merge and run full re-verification.
|
|
63
64
|
{{INCLUDE:_coverage-critic.md}}
|
|
64
65
|
- Non-goals:
|
|
@@ -122,6 +123,7 @@
|
|
|
122
123
|
The manifest lives at the **task level** (`<task_root>/qa/`, path token `TASK_QA_PATH`) and is shared across planning → implementation → final-verification. Layout split: executable scripts (conformance + any real-IO test) live under `<task_root>/qa/scripts/`; data sidecars (`conformance-manifest.json`, `result-*.json`) stay at the `qa/` root. This declaration is enforced at four layers: `validators/validate-implementation-plan-stages.py` check **S11** forces every stage to carry one of the two lines; at the planning boundary `validators/validate-run.py` `_validate_planning_conformance_declared` fails when a stage that declared `Conformance tests:` has no matching `-stage-<N>` entry in the shared manifest (a declaration that was never materialized); the manifest JSON structure — including each entry's `script` living under `qa/scripts/` — is enforced by `validate_conformance_manifest` (called from both the run path and validate-run); and the result policy is evaluated by `conformance.py` and `validate-run.py`.
|
|
123
124
|
- `### Stage Exit Contract` — predicted added/modified files, newly exposed identifiers/types/endpoints, downstream-usable resources.
|
|
124
125
|
- `### Stage Validation` — pre / mid / post exact commands or observable outcomes for this stage only.
|
|
126
|
+
- **Dependency precondition (stages that run the project toolchain).** The planning worktree is created without installed dependencies, so a stage whose steps call `npm` / `yarn` / `pytest` / `cargo` / equivalent cannot have those commands succeed at plan time — they exit `127`, not RED/GREEN. Declare the install **once** as a `phase: pre` row in `### Validation Checklist` (e.g. `VC-008 — the implementation run's stage worktree has workspace dependencies installed`) and have every such stage's `Stage Validation` cite that `VC-NNN` in its `pre:` line. Do not repeat the install commands per stage, and do not silently assume the tooling is present: a plan that never states the precondition produces steps whose commands never resolve, which the §5.5.9 round then reports as unverifiable. **Enforced (advisory):** `validators/validate-run.py` `_detect_missing_dependency_precondition` warns when a toolchain-invoking stage cites no `VC-NNN`, or cites one that is not `phase: pre`. Whether the cited row genuinely covers dependencies is a §5.5.9 judgement, not a machine check. Detection uses the token allowlist in `scripts/okstra_ctl/build_tools.py`; a project overrides it with `buildToolTokens` in `.okstra/project.json`.
|
|
125
127
|
- **Vertical-slice-first partition rule (1st-class):** the grouping anchor is a **thin end-to-end vertical slice** — one stage delivers a single user-observable increment, crossing whatever layers are needed (data → service → API → UI) to make that one increment work. File/module proximity is demoted to the **intra-slice grouping rule**: within a slice, keep steps touching the same file/directory/module together so the diff, PR, and rollback unit stay cohesive. **Horizontal layer-splitting is forbidden** — never carve "the DB layer" into one stage and "the service layer" into the next; that produces stages that ship no standalone user value. A stage is split ONLY when (a) a real `depends-on` data/contract dependency exists, (b) effective steps would exceed 8, or (c) it is a distinct vertical slice (a different user-value increment). Maximising the number of parallel stages is NOT a reason to split — parallelism is an emergent property of independent stages, never a partitioning goal.
|
|
126
128
|
- **Project-boundary partition rule (hard boundary):** a *project* boundary is either (a) a different repository / `PROJECT_ROOT`, or (b) a different top-level independently-deployable module within one repo. A stage maps to a single worktree on one repo/branch, so **no stage may contain edits belonging to more than one project** — this is a hard split that overrides the ≤8-step merging allowance; never co-locate two projects' changes in one stage to save a stage. Two cases:
|
|
127
129
|
- **Same repo, different modules** — put each project/module's increment in its own stage. Their file sets are disjoint, so they default to `depends-on (none)` and run in parallel; add a `depends-on` link ONLY when a real cross-module contract / shared-schema / deploy-order dependency exists.
|
|
@@ -138,13 +140,13 @@
|
|
|
138
140
|
- **recommendedNextSteps policy:** keep the substance of cross-project preconditions/carries in `crossProjectDependencies`, and put in `§3 Recommended Next Steps` only a pointer to that section (`§5.4 Cross-Project Dependencies`) — no double recording.
|
|
139
141
|
- validation checklist (pre / mid / post) — each item is an exact command or observable outcome
|
|
140
142
|
- rollback strategy — exact revert path (commits, flags, migrations) and the signal that triggers rollback
|
|
141
|
-
- **Requirement admissibility (scope boundary):** a brief line becomes a Requirement Coverage row only when **a stage can satisfy it by changing files in this repository** — source, tests, config, or deployment *manifest files*. A line whose satisfaction needs a person's approval, a ticket status change, or an action against live infrastructure (applying a manifest, a cutover, creating a dashboard or alert, validating in staging/production) is NOT a requirement for this phase: it belongs to the brief's `## External Gates`, and this plan neither creates a stage for it nor cites it in coverage. Briefs generated by `okstra-brief-gen` pre-split these into `##
|
|
143
|
+
- **Requirement admissibility (scope boundary):** a brief line becomes a Requirement Coverage row only when **a stage can satisfy it by changing files in this repository** — source, tests, config, or deployment *manifest files*. A line whose satisfaction needs a person's approval, a ticket status change, or an action against live infrastructure (applying a manifest, a cutover, creating a dashboard or alert, validating in staging/production) is NOT a requirement for this phase: it belongs to the brief's `## External Gates`, and this plan neither creates a stage for it nor cites it in coverage. Briefs generated by `okstra-brief-gen` pre-split these into the end-state sections `## Expected Behavior` / `## Preserved Behavior` / `## Expected Outcome` (admissible) and `## External Gates` (not); when reading an older brief that carries a raw Definition-of-Done checklist, apply the same test line by line. The boundary is the *action*, not the topic — "add the flag to `values-prod.yaml`" is admissible, "apply that manifest to prod" is not. Planning an operational stage this phase cannot execute (see the run-scope rule above forbidding deployments) produces steps whose commands never resolve, which the §5.5.9 gate then correctly blocks — the plan must not create that deadlock in the first place.
|
|
142
144
|
- **Scope provenance (BLOCKING):** every Requirement Coverage row's `Source` cell must be exactly one of three forms, and every Stage Map stage must be cited by at least one coverage row's `Covered by`. The forms:
|
|
143
|
-
- `brief
|
|
145
|
+
- `brief:EB-001` / `brief:PB-001` / `brief:EO-001` — an end-state id the brief declares. Citing a heading instead is rejected when the brief pins ids: every brief carries the same generic headings, so a heading citation cannot say WHICH reporter line the requirement came from. Briefs authored before the end-state sections existed keep the older `brief:<heading>` form, and there a heading the brief does not contain is a fabricated requirement. **Enforced:** `validators/validate-run.py` `_validate_requirement_provenance` and `validators/validate_fanout.py` `_check_provenance`, both via `scope_provenance.brief_citation_problem`.
|
|
144
146
|
- `derived:R-NNN — <one-line rationale>` — derived from another row in the same table. The chain must terminate at a `brief:` or `contract:` row and must not cycle. Use this for genuine technical consequences: a migration stage is `derived:R-003 — R-003 cannot be satisfied without a schema change`.
|
|
145
147
|
- `contract:<rule>` — an artifact okstra's own phase contract mandates, so it has no brief line to cite. The allowlist is exactly `decision-record-step` (the §5.4 Decision Drafts materialization step) and `glossary-step` (the glossary proposal step); the SSOT is `scripts/okstra_ctl/scope_provenance.py`. Never widen this form to launder work the brief did not ask for.
|
|
146
148
|
An item you can give none of these three sources to is **not a requirement and not a stage**. Its only admissible outlet is a `## 1. Clarification Items` row with `Blocks=approval`, carrying the recommendation format from `_clarification-recommendation.md`. Do not fold it into an option, a stage, or a step "while we are in here" — that is the scope expansion this rule exists to stop. This makes concrete the planning-input rule that any change beyond what `Requirement Summary` explicitly demands is out of scope by default. **Enforced:** `validators/validate-run.py` `_validate_requirement_provenance` (source resolution) and `_validate_stage_has_requirement` (no stage without a requirement).
|
|
147
|
-
- **The reach of this gate — do not over-trust it.** What is mechanically enforced is the *form* of each source, that a `brief:` heading literally exists, that a `derived:` chain terminates without cycling, and that no stage is uncited. What is **not** enforced is whether the cited source genuinely demands the requirement.
|
|
149
|
+
- **The reach of this gate — do not over-trust it.** What is mechanically enforced is the *form* of each source, that a cited `brief:` id is one the brief actually declares (or, on a pre-end-state brief, that the heading literally exists), that a `derived:` chain terminates without cycling, and that no stage is uncited. What is **not** enforced is whether the cited source genuinely demands the requirement. The id form closes the older loophole — a brief no longer offers generic headings any invented work could be hung on — but it leaves one open: attaching a requirement the cited `EB-NNN` does not actually ask for still parses clean, because no machine reads that id's sentence and compares it to your row. The gate's value is that it forces every item to name a specific reporter line and makes fabrication explicit and auditable — judging whether that line actually demands the item remains a reviewer / `DISAGREE(f)` responsibility, and passing this gate is never evidence that the scope is justified.
|
|
148
150
|
- **Stage citation format:** the reverse check reads each `Covered by` cell as prose, so a stage counts as cited only when its number is anchored to a `Stage` / `Stages` word on the same line. These all read: `Stage 2`; `Stage 1, Stage 2, Stage 3`; `Stages 1, 2, 3`; `Stages 1, 2, and 3`; ranges (`Stages 1-3`, `Stages 1 to 3`, `Stages 1 through 3`); and `and` / `&` conjunctions. A bare number with no `stage` word anchoring it is NOT read as a citation, so `covered by the recommended option, step 4` cites nothing. **Enforced:** `validators/validate-run.py` `_validate_stage_has_requirement` fails the plan when any Stage Map stage is cited by no coverage row.
|
|
149
151
|
- Because that reader only sees prose, it cannot tell a citation from a mention: `Stage 1 (superseded by Stage 2)` still counts Stage 1 as cited, and one row citing `Stages 1-64` rubber-stamps every stage in the map. Cite the stages a requirement is actually satisfied by — not stages merely mentioned, and never a blanket range standing in for the work of checking.
|
|
150
152
|
- **Requirement Coverage (mandatory, §5.5.8):** one row per concrete requirement from the task brief / packet. Assign stable IDs `R-001`, `R-002`, ... in source order. Columns: `ID | Source | Requirement | Covered by option / stage / step | Status`. `Source` follows the three-form grammar defined in the **Scope provenance** rule above — a free-form `file:line` is not one of the three forms and is rejected. When this run's brief is a fan-out packet, the task manifest's `taskBriefPath` points at that packet file, so `brief:` cites the packet's own headings (`## Scope`, `## Evidence`, `## Requirement Provenance`) — not the headings of the upstream user brief the packet came from. For `covered`, `Covered by` must name the specific Option Candidate and Stage/Step that satisfies it, not just "recommended option". **Enforced:** `validators/validate-run.py` `_validate_requirement_coverage_covered_by` fails a `covered` row whose `coveredBy` is bare "recommended option", names no Option/Stage/Step anchor, or cites a Stage number absent from the Stage Map (whether the cited step *actually satisfies* the requirement remains a worker `DISAGREE(f)` judgment). `Status` is one of `covered`, `gap`, `blocked C-NNN`, or `documented-deviation`. A deviation records the concrete alternative in `coveredBy`, non-empty unique `decisionRefs` (`C-NNN` clarification IDs and/or `D-NNNN` decision-draft numbers), and `approvalDisposition: accepted|blocked C-NNN`. `accepted` is valid only when a referenced clarification is user-confirmed (`answered|resolved` with non-empty `userInput`); `blocked C-NNN` is valid only when that same-report clarification is `open` and `Blocks=approval`. **Enforced:** schema `$defs.ImplementationRequirementCoverageRow` plus `validators/validate-run.py` `_validate_requirement_deviations`; the exact `P-Req-*` queue still comes from `scripts/okstra_ctl/plan_items.py`. If any row is `gap`, plain `blocked C-NNN`, or a deviation whose approval disposition is blocked, the Plan Body Verification gate MUST NOT be `passed` / `passed-with-dissent`; add a matching `Blocks=approval` row for the blocker and keep `approved: false`, and record `coverage-gap` in `planBodyVerification.gateBlockedBy` so the blocking value does not silently read as a worker disagreement. **Exception — no double counting:** a row whose `blocked C-NNN` cites a clarification that *this same run's* §5.5.9 round promoted from a `majority-disagree` item is not an independent blocker; that blocker is already counted as the plan item, and re-counting it makes the run block on a clarification it just authored and carries the row into the next run as a fresh blocker. **Enforced:** `validators/validate-run.py` `_validate_gate_blocked_by` (fails a passing gate with a blocking coverage row) and `_independent_coverage_blockers` (the same-run exclusion).
|
|
@@ -46,8 +46,9 @@
|
|
|
46
46
|
- evidence-backed candidate or explicit no-candidate result for every resolved lens
|
|
47
47
|
- worker-local candidate IDs that convergence can trace back to their source worker
|
|
48
48
|
- uncertainty and overlap relationships kept explicit for downstream consensus classification
|
|
49
|
+
- every candidate's `Expected behavior after` is checked against the brief's `## Preserved Behavior` items before the row is written. A candidate whose observable change contradicts a `PB-NNN` item is dropped, or raised as a `## 1. Clarification Items` row — never softened into a vaguer cell. The scan brief's PB set is the upper bound on what this phase may propose changing.
|
|
49
50
|
- Report assembly instructions:
|
|
50
|
-
- the `## 5.9 Improvement Candidates` table populated with rows that obey the
|
|
51
|
+
- the `## 5.9 Improvement Candidates` table populated with rows that obey the 11-column schema from `validators/validate_improvement_report.py` (Cand ID `I-NNN`, Lens from whitelist, Title, Scope ⊆ scan-scope, Severity, Effort, Consensus, Source workers `<worker>:<id>` from {claude, codex, antigravity}, Recommended next-phase ∈ {requirements-discovery, implementation-planning, error-analysis}, Expected behavior after, Evidence as path:line list). `Expected behavior after` states, in one observable sentence, what becomes different once the candidate is applied — it is the seed of the downstream brief's `EB-NNN` / `EO-NNN`. A candidate you cannot write this cell for is a preference, not a finding: drop it rather than filling the cell with a restatement of the title.
|
|
51
52
|
- `Consensus` cells in `## 5.9 Improvement Candidates` use the table enum exactly: `full`, `partial`, `contested`, `worker-unique`. Map convergence's `full-consensus` / `partial-consensus` labels to `full` / `partial` before writing the table.
|
|
52
53
|
- `## 7. Final Verdict` Verdict Token ∈ {`candidates-ready`, `no-candidates`, `blocked`}; Direction `routing`; Next Step "ask the user to select K candidates (see the ## 5.9 table)"
|
|
53
54
|
- `## 3. Recommended Next Steps` first entry summarises per-candidate routing and proposes new task-key names of the form `<task-group>/imp-<Cand-ID>`
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
`domain`(work-category 5-enum: bugfix / feature / refactor / ops / improvement),
|
|
36
36
|
`depends-on`(an inline list of unit-ids within the same fan-out `[unit-001]`, or `[]` if none),
|
|
37
37
|
`recommended-next-phase`(error-analysis | implementation-planning) are filled in.
|
|
38
|
-
Each packet MUST carry a non-empty `## Requirement Provenance` section whose every bullet is `brief:<heading
|
|
38
|
+
Each packet MUST carry a non-empty `## Requirement Provenance` section whose every bullet is `brief:EB-001` / `brief:PB-001` / `brief:EO-001` — an end-state id the brief declares — or `contract:<rule>`. When the brief pins ids, citing a heading is rejected: every brief carries the same generic headings, so a heading cannot say WHICH reporter line demanded this unit, and a fan-out unit becomes the brief for a whole downstream task. Only a brief authored before the end-state sections existed still takes the older `brief:<heading>` form, and there the heading must literally exist in it. `derived:` is not admissible in a packet — cross-packet derivation cannot be resolved from a single packet, so each unit anchors directly on the brief. A unit you cannot source that way is not a work item: raise it as a clarification row instead of publishing a packet for it. **Enforced:** `validators/validate_fanout.py` `_check_provenance`.
|
|
39
39
|
- in `runs/requirements-discovery/fan-out/index.md`, list the packets in depends-on topological order
|
|
40
40
|
as a numbered list (`1. unit-001`) (a generated view; explicitly do not hand-edit). The depends-on graph
|
|
41
41
|
must be a DAG — `validate_fanout` rejects a cycle as a validation failure, so if a cycle appears,
|
|
@@ -56,6 +56,7 @@
|
|
|
56
56
|
- uncertainty boundaries and missing inputs
|
|
57
57
|
- next recommended phase and safe resume guidance
|
|
58
58
|
- canonical-term resolution for every `terminology:*` brief item as `<term> = <definition>`, plus whether `<PROJECT_ROOT>/.okstra/glossary.md` should be updated
|
|
59
|
+
- one `endStateCoverage` row per brief end-state id (`EB-NNN` / `PB-NNN` / `EO-NNN`). `coveredBy` names the routing decision, the fan-out unit id, or the `C-NNN` clarification that accounts for it. This phase does NOT author a goal of its own — the brief already pinned the end state, and restating it here is how four phases end up with four different targets. **Enforced:** `validators/validate-run.py` `_validate_end_state_coverage`.
|
|
59
60
|
- Report assembly instructions:
|
|
60
61
|
- write canonical-term resolutions in a new `Domain Alignment` subsection of the final report; actual glossary writes happen via `okstra-brief-gen` Step 4.5 on a subsequent run
|
|
61
62
|
- Clarification request policy (phase-specific addenda — shared policy is in `_common-contract.md`):
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
"""Decide whether a plan's command cell invokes the project's build toolchain.
|
|
2
|
+
|
|
3
|
+
The planning worktree has no dependencies installed, so a stage that calls the
|
|
4
|
+
toolchain needs its precondition declared once and referenced (see the
|
|
5
|
+
plan-body contract's "Planning-time environment gap"). This module owns the
|
|
6
|
+
set of commands that counts as such a call.
|
|
7
|
+
|
|
8
|
+
An allowlist, not a denylist: a tool nobody listed goes undetected, which
|
|
9
|
+
weakens one advisory check, whereas treating every unknown leading token as a
|
|
10
|
+
build call would fire on `grep` and `sed` in every plan and train readers to
|
|
11
|
+
ignore the warning.
|
|
12
|
+
"""
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import json
|
|
16
|
+
import shlex
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
|
|
19
|
+
DEFAULT_BUILD_TOOL_TOKENS: tuple[str, ...] = (
|
|
20
|
+
"npm",
|
|
21
|
+
"yarn",
|
|
22
|
+
"pnpm",
|
|
23
|
+
"npx",
|
|
24
|
+
"bun",
|
|
25
|
+
"pytest",
|
|
26
|
+
"tox",
|
|
27
|
+
"poetry",
|
|
28
|
+
"uv",
|
|
29
|
+
"cargo",
|
|
30
|
+
"go",
|
|
31
|
+
"gradle",
|
|
32
|
+
"mvn",
|
|
33
|
+
"make",
|
|
34
|
+
"bundle",
|
|
35
|
+
"composer",
|
|
36
|
+
"dotnet",
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
# Prefixes that stand in front of the real command without being it.
|
|
40
|
+
_TRANSPARENT_LEADERS = frozenset({"sudo", "env", "time", "nice", "exec", "command"})
|
|
41
|
+
_CLAUSE_SEPARATORS = ("&&", "||", ";", "|", "\n")
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def resolve_build_tool_tokens(project_root: Path) -> tuple[str, ...]:
|
|
45
|
+
"""Project override, else the built-in list.
|
|
46
|
+
|
|
47
|
+
`buildToolTokens` in `.okstra/project.json` REPLACES the defaults and an
|
|
48
|
+
empty array disables detection — the same precedence `worktreeSyncDirs`
|
|
49
|
+
uses, so one rule covers both.
|
|
50
|
+
"""
|
|
51
|
+
try:
|
|
52
|
+
payload = json.loads(
|
|
53
|
+
(Path(project_root) / ".okstra" / "project.json").read_text(encoding="utf-8")
|
|
54
|
+
)
|
|
55
|
+
except (OSError, json.JSONDecodeError):
|
|
56
|
+
return DEFAULT_BUILD_TOOL_TOKENS
|
|
57
|
+
configured = payload.get("buildToolTokens") if isinstance(payload, dict) else None
|
|
58
|
+
if not isinstance(configured, list):
|
|
59
|
+
return DEFAULT_BUILD_TOOL_TOKENS
|
|
60
|
+
return tuple(str(token).strip() for token in configured if str(token).strip())
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _clauses(command: str) -> list[str]:
|
|
64
|
+
parts = [command]
|
|
65
|
+
for separator in _CLAUSE_SEPARATORS:
|
|
66
|
+
parts = [piece for part in parts for piece in part.split(separator)]
|
|
67
|
+
return [part.strip() for part in parts if part.strip()]
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _leading_token(clause: str) -> str:
|
|
71
|
+
try:
|
|
72
|
+
words = shlex.split(clause)
|
|
73
|
+
except ValueError:
|
|
74
|
+
words = clause.split()
|
|
75
|
+
for word in words:
|
|
76
|
+
# `cd <dir>` and `VAR=value` lead a clause without being the command;
|
|
77
|
+
# step past them rather than reading the directory as the tool.
|
|
78
|
+
if word == "cd" or "=" in word.split("/")[0] and not word.startswith("-"):
|
|
79
|
+
continue
|
|
80
|
+
if word in _TRANSPARENT_LEADERS:
|
|
81
|
+
continue
|
|
82
|
+
if word.startswith("-"):
|
|
83
|
+
continue
|
|
84
|
+
return Path(word).name
|
|
85
|
+
return ""
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def command_invokes_build_tool(
|
|
89
|
+
command: str, *, tokens: tuple[str, ...] | None = None
|
|
90
|
+
) -> bool:
|
|
91
|
+
"""True when any clause of *command* starts with an allowlisted tool."""
|
|
92
|
+
allowed = DEFAULT_BUILD_TOOL_TOKENS if tokens is None else tokens
|
|
93
|
+
if not allowed:
|
|
94
|
+
return False
|
|
95
|
+
return any(_leading_token(clause) in allowed for clause in _clauses(command))
|