okstra 0.165.2 → 0.165.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/runtime/BUILD.json +2 -2
- package/runtime/prompts/profiles/_implementation-diff-review.md +2 -2
- package/runtime/prompts/profiles/_implementation-executor.md +21 -14
- package/runtime/prompts/profiles/_implementation-verifier.md +3 -3
- package/runtime/prompts/profiles/_stage-discipline.md +4 -3
- package/runtime/python/okstra_ctl/domain/wizard/interaction.py +17 -5
- package/runtime/python/okstra_ctl/implementation_stage.py +2 -1
- package/runtime/python/okstra_ctl/initial_prompt_materialization.py +104 -12
- package/runtime/python/okstra_ctl/path_hints.py +10 -2
- package/runtime/python/okstra_ctl/render.py +5 -0
- package/runtime/python/okstra_ctl/stage_map.py +10 -4
- package/runtime/python/okstra_ctl/wizard.py +19 -2
- package/runtime/python/okstra_ctl/worker_prompt_body.py +24 -4
- package/runtime/python/okstra_ctl/worker_prompt_contract.py +2 -1
- package/runtime/python/okstra_ctl/worker_prompt_policy.py +13 -2
- package/runtime/templates/implementation-worker-preamble.md +6 -0
- package/runtime/validators/validate-implementation-plan-stages.py +19 -2
- package/src/commands/execute/wizard.mjs +20 -8
package/package.json
CHANGED
package/runtime/BUILD.json
CHANGED
|
@@ -19,7 +19,7 @@ prompt (agents/workers/_cli-wrapper-template.md → Prompt Composition).
|
|
|
19
19
|
|
|
20
20
|
# Pre-commit diff review sweep (BLOCKING — before the executor's final commit)
|
|
21
21
|
|
|
22
|
-
|
|
22
|
+
This is where the preflight's conventions get enforced against the code you actually wrote. Run it once, after the last `Edit` / `Write` of the stage, before the final commit. Fix every finding in place (you are the executor — you may edit); this is a prevention pass, not a report you hand off.
|
|
23
23
|
|
|
24
24
|
Do not scan holistically and stop when it "looks fine". Work the matrix exhaustively — the failure mode this gate exists to prevent is a real defect surviving because you eyeballed the diff instead of enumerating it.
|
|
25
25
|
|
|
@@ -48,4 +48,4 @@ End your audit-sidecar entry for this sweep with a one-line `Coverage:` footer n
|
|
|
48
48
|
|
|
49
49
|
## Graceful degradation
|
|
50
50
|
|
|
51
|
-
When the routed coding-preflight pack is unreadable (codex / antigravity runtime, or the files are absent), do NOT skip the sweep —
|
|
51
|
+
When the routed coding-preflight pack is unreadable (codex / antigravity runtime, or the files are absent), do NOT skip the sweep — fall back to the always-binding principles the preflight enumerates plus the project's `CLAUDE.md` / `CONTRIBUTING` / lint config, and record `diff-review: resource-unavailable → applied <agnostic principles + project rules>` with the Coverage footer.
|
|
@@ -19,17 +19,25 @@ until Phase 5 ends, then drop from active context for Phase 6/7.
|
|
|
19
19
|
|
|
20
20
|
## Pre-implementation context exploration (executor before first edit)
|
|
21
21
|
|
|
22
|
-
- **
|
|
23
|
-
-
|
|
24
|
-
-
|
|
25
|
-
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
22
|
+
- **Three BLOCKING gates bind this run, and their bodies travel with this prompt** — inlined under their own headings, or named by path under `## Required prompt resources`. Follow the delivered body verbatim; a gate re-typed from memory is a skipped gate. Each gate's own body owns its rule list, so nothing here restates it:
|
|
23
|
+
- `Coding-conventions preflight` (`prompts/profiles/_coding-conventions-preflight.md`) — before the first `Edit` / `Write`. It loads the conventions and binds the TDD loop below; close it by stating in ONE line which conventions apply (e.g. `Applying TS + hexagonal overlay; domain at src/domains/*/domain/`).
|
|
24
|
+
- `Pre-commit diff review sweep` (`prompts/profiles/_implementation-diff-review.md`) — after the stage's last `Edit` / `Write`, before the final commit. Sweep `git diff <stage-base>..HEAD` against the conventions the preflight loaded, fix findings in place while they are inside this stage's scope, and write its `Coverage:` footer to your audit sidecar.
|
|
25
|
+
- `Implementation self-check` (`prompts/profiles/_implementation-self-check.md`) — before you append the `status:"done"` row. Write the confirming evidence per item to your audit sidecar.
|
|
26
|
+
|
|
27
|
+
<!--
|
|
28
|
+
Gate delivery (lead / maintainer — stripped before this body reaches a worker).
|
|
29
|
+
`okstra_ctl.initial_prompt_materialization` appends all three bodies to the
|
|
30
|
+
persisted executor prompt in that order under EAGER_INCLUDE, and lists their
|
|
31
|
+
paths under `## Required prompt resources` under LAZY_PATH_REFERENCE; a CLI
|
|
32
|
+
executor cannot read the profiles directory, so the file reference alone never
|
|
33
|
+
reaches it. Enforcement: the CLI wrapper refuses an Executor dispatch whose
|
|
34
|
+
persisted prompt lacks the heading `Coding-conventions preflight`
|
|
35
|
+
(`<SENTINEL_PREFIX>_PREFLIGHT_MISSING`) or either post-write heading
|
|
36
|
+
(`<SENTINEL_PREFIX>_POSTWRITE_GATE_MISSING`) — see
|
|
37
|
+
`agents/workers/_cli-wrapper-template.md` → Prompt Composition.
|
|
38
|
+
-->
|
|
39
|
+
- **Stage discipline (when a preceding stage is `done`):** its code is behavior-frozen — you may call, extend, or compose with it, never change what it already does. The rule body travels with this prompt the same way the gates do (`prompts/profiles/_stage-discipline.md`); only its `implementation` bullet binds you, the `implementation-planning` one binds the planner. Declaration-level — no wrapper sentinel.
|
|
40
|
+
- **Non-interactive auto-execution (BLOCKING for `runner=cli-wrapper`).** A CLI-wrapper executor runs head-less — there is no human at the keyboard. Skills loaded during the run (tdd, coding-preflight, and others) contain "get user approval", "state your plan to the user and wait", or "ask before proceeding" gates written for interactive sessions; in this run those gates are **already satisfied** by the upstream `implementation-planning` approval (the plan this stage executes was human-approved). The executor MUST NOT stop to request approval, MUST NOT end its turn after only producing a plan, and MUST carry the stage through end-to-end — RED → GREEN → refactor → per-cycle commit → `### Stage Carry Evidence`. The ONLY skill step to skip is the interactive user-approval prompt itself; every other skill rule (TDD discipline, conventions, real-IO isolation) still binds. Stopping early for approval in a head-less run is the observed empty-exit failure (exit 0, no diff): treat it as `contract-violated`.
|
|
33
41
|
- **Mandatory TDD loop**: BEFORE the first `Edit` or `Write` call, the executor MUST apply a red-green-refactor loop for every code change in this run. This is required; skipping it is a `contract-violated` outcome. This governs HOW each step is executed (failing test first → minimal implementation → refactor); it does not override the approved plan's WHAT/file scope.
|
|
34
42
|
- Order of operations per plan step: (1) write/extend the test that captures the step's acceptance criterion and confirm it fails for the right reason, (2) implement the minimum change to make it pass, (3) commit the test and its implementation together in a single commit (`feat|fix(<scope>): ...`) — do NOT commit the failing test separately, (4) refactor without changing behaviour and commit separately if any cleanup is made (`refactor(<scope>): ...`). The failing-then-passing transition is preserved as `TDD evidence` in the final report (failing output captured before the merged commit, passing output after), not as two separate commits.
|
|
35
43
|
- Doc-only / config-only / pure-rename steps that have no observable runtime behaviour are exempt from the failing-test requirement, but the executor MUST cite the exemption per step in the final report (`TDD exemption: <reason>`).
|
|
@@ -37,10 +45,10 @@ until Phase 5 ends, then drop from active context for Phase 6/7.
|
|
|
37
45
|
- **DB / IO / SQL changes require real execution — mock-only is NOT validation evidence:** when this run's diff touches DB/IO/SQL (ORM / query-builder code — sequelize / typeorm / prisma / knex / raw SQL — `*.repository.*`, model/entity files, `migrations/**`, `*.sql`, or any changed query string), a mocked unit test cannot observe the SQL the query builder actually emits (observed failure class: `_implementation-verifier.md` §"DB / IO / SQL change — real-execution gate"). The executor MUST run the change against a real (or faithful-replica) datastore — the `db-test` validation step (plan `validation` db step, else `project.json.qaCommands.db-test`), targeting a **local / replica** DB — and cite its exact command + exit code in the final report's `Validation evidence`. If no real DB / `db-test` command is reachable, do NOT claim the change verified: label the DB portion `static-analysis only …, unverified (not executed)` in the report, surface it in the routing recommendation, and never downplay the real run as "too heavy". `git push` stays forbidden (universal list); the unverified DB state is carried forward so `final-verification` cannot accept it and `release-handoff` cannot push.
|
|
38
46
|
- **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.
|
|
39
47
|
- **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).
|
|
40
|
-
-
|
|
48
|
+
- read the approved plan at this prompt's `**Approved plan:**` anchor end-to-end and parse the `## 5.5 Stage Map`. 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.
|
|
41
49
|
- 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.
|
|
42
50
|
- 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.
|
|
43
|
-
- **Clarification answers carried in (read before the first edit
|
|
51
|
+
- **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.
|
|
44
52
|
- **Effective design preparation (runtime-resolved after stage selection):**
|
|
45
53
|
|
|
46
54
|
{{DESIGN_PREP_CONTEXT}}
|
|
@@ -52,7 +60,6 @@ until Phase 5 ends, then drop from active context for Phase 6/7.
|
|
|
52
60
|
- **drift rule** (this section): if a file *named in the plan* has materially drifted, refuse to edit and route back to planning. This protects trust in the approved scope.
|
|
53
61
|
- **out-of-plan rule** (Allowed actions section below): if a step *requires touching a file NOT in the plan list*, that is permitted with `Out-of-plan edits` justification. This handles honest scope discovery during execution.
|
|
54
62
|
- confirm the test/build commands referenced in the plan still exist and run from a clean state
|
|
55
|
-
- **Pre-commit diff review sweep (BLOCKING before the executor's final commit):** run the sweep whose body is delivered via `_implementation-diff-review.md` (see the gate-delivery bullet above) over the run diff (`git diff <stage-base>..HEAD`), and fix findings in place before handing to verifiers when the issue is inside this stage's scope. That sidecar is the single source for the sweep's file×rule matrix (DRY / self-mock / behavioral-test / hexagonal / truthful-name / plain-English rules) and its `Coverage:` footer — do not maintain a second copy of the rule list here.
|
|
56
63
|
|
|
57
64
|
## Stage execution contract (this run owns one stage)
|
|
58
65
|
|
|
@@ -23,7 +23,7 @@ Every verifier acts as a QA gate, not just a diff reviewer. Trusting the executo
|
|
|
23
23
|
|
|
24
24
|
Verifier obtains the QA command set from exactly two declared sources, in order — there is **no fallback to guessing tools from manifest files**.
|
|
25
25
|
|
|
26
|
-
1. **Tier 1 — plan validation set (task-specific):** every command listed under the approved plan's `validation` block (pre / mid / post).
|
|
26
|
+
1. **Tier 1 — plan validation set (task-specific):** every command listed under the approved plan's `validation` block (pre / mid / post). The plan is the file at this prompt's `**Approved plan:**` anchor, scoped to the stage its `**Stage for this implementation run:**` anchor names; both are generated headers, so a missing one is `contract-violated`, never a value to infer.
|
|
27
27
|
2. **Tier 2 — project baseline (`project.json.qaCommands`):** the project's standing QA baseline declared in `<PROJECT_ROOT>/.okstra/project.json` under the `qaCommands` key. Schema (each category is an array of `{ "label", "cmd", "language"? }` objects):
|
|
28
28
|
```json
|
|
29
29
|
{
|
|
@@ -234,6 +234,6 @@ If every verifier present in the resolved roster ends with a non-result terminal
|
|
|
234
234
|
- running integration / end-to-end tests that produce non-local side effects (DB writes against a non-local datastore, external API writes, docker compose against a non-isolated environment) unless that exact command is listed in the approved plan's validation set
|
|
235
235
|
- redirecting tool caches or output to paths outside the worktree — e.g. setting `CARGO_TARGET_DIR`, `PYTEST_CACHE_DIR`, `NODE_OPTIONS=--require=<external>`, or any env var that causes the verifier's command to write outside the worktree's normal build artifact paths
|
|
236
236
|
|
|
237
|
-
##
|
|
237
|
+
## Executor completion self-check (not this role's gate)
|
|
238
238
|
|
|
239
|
-
-
|
|
239
|
+
- The executor's `Implementation self-check` gate (`prompts/profiles/_implementation-self-check.md`) belongs to the worker that owns the diff, and its body is deliberately not delivered here: it asks for in-place fixes and break-then-restore mutation checks, every one of which this verifier is forbidden to perform. Do not re-derive its items or claim to have run it. What grades the same defects from this side is the blocking taxonomy above, applied to the diff you re-read yourself. When the executor's `Coverage:` / `Self-check coverage:` lines are among the inputs this prompt enumerates, a missing line or one whose file list does not reconcile with the diff is a blocking finding — the gate was skipped or partially run.
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
<!--
|
|
2
2
|
Shared stage-discipline rule. INCLUDEd by implementation-planning.md (binds the
|
|
3
|
-
planner) and implementation.md (binds the lead).
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
planner) and implementation.md (binds the lead). `okstra_ctl.initial_prompt_-
|
|
4
|
+
materialization` also delivers this body to every implementation executor
|
|
5
|
+
prompt as a required resource, because a codex/antigravity executor shares no
|
|
6
|
+
lead context and cannot read the rendered profile.
|
|
6
7
|
|
|
7
8
|
Do NOT write the literal include directive token in this file's body — the
|
|
8
9
|
resolver matches it anywhere and would recurse on this file itself.
|
|
@@ -49,18 +49,30 @@ class WizardAnswerError(ValueError):
|
|
|
49
49
|
"""Raised when an answer violates the interaction plan's protocol."""
|
|
50
50
|
|
|
51
51
|
|
|
52
|
+
def _exact_value_match(prompt: WizardPrompt, candidate: str) -> str | None:
|
|
53
|
+
for option in prompt.options:
|
|
54
|
+
if option.value == candidate:
|
|
55
|
+
return option.value
|
|
56
|
+
return None
|
|
57
|
+
|
|
58
|
+
|
|
52
59
|
def _normalize_numbered_item(prompt: WizardPrompt, answer: str) -> str:
|
|
53
60
|
candidate = (answer or "").strip()
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
return exact_values[0]
|
|
61
|
+
# A bare number is the position in the list the user was shown. Matching
|
|
62
|
+
# option values first breaks every picker whose values are themselves
|
|
63
|
+
# numbers — a stage picker resolves "1" to stage 1 while the user was
|
|
64
|
+
# pointing at line 1, which is a different stage (or "every stage").
|
|
59
65
|
if candidate.isdecimal():
|
|
60
66
|
number = int(candidate)
|
|
61
67
|
if 1 <= number <= len(prompt.options):
|
|
62
68
|
return prompt.options[number - 1].value
|
|
69
|
+
out_of_list = _exact_value_match(prompt, candidate)
|
|
70
|
+
if out_of_list is not None:
|
|
71
|
+
return out_of_list
|
|
63
72
|
raise WizardAnswerError(f"numbered-text answer is out of range: {candidate}")
|
|
73
|
+
exact_value = _exact_value_match(prompt, candidate)
|
|
74
|
+
if exact_value is not None:
|
|
75
|
+
return exact_value
|
|
64
76
|
label_values = tuple(
|
|
65
77
|
option.value for option in prompt.options if option.label == candidate
|
|
66
78
|
)
|
|
@@ -17,6 +17,7 @@ from . import stage_targets
|
|
|
17
17
|
from .design_prep import DesignPrepDecision, DesignPrepError, resolve_design_prep
|
|
18
18
|
from .final_report_paths import final_report_data_path
|
|
19
19
|
from .stage_reconcile import auto_reconcile_best_effort
|
|
20
|
+
from .worker_prompt_policy import IMPLEMENTATION_STAGE_HEADER
|
|
20
21
|
|
|
21
22
|
|
|
22
23
|
class ImplementationStageError(Exception):
|
|
@@ -250,7 +251,7 @@ def publish_stage_run_claim(
|
|
|
250
251
|
ctx["EFFECTIVE_STAGES"] = csv
|
|
251
252
|
ctx["CONCURRENT_RUN_STAGES"] = ",".join(str(s) for s in claim.concurrent_stages)
|
|
252
253
|
ctx["STAGE_BATCH_DIRECTIVE"] = (
|
|
253
|
-
f"-
|
|
254
|
+
f"- {IMPLEMENTATION_STAGE_HEADER} `{csv}`. "
|
|
254
255
|
"Execute exactly this Stage Map stage — this is the authoritative scope. "
|
|
255
256
|
"Do NOT recompute from `consumers.jsonl`; the runtime already selected "
|
|
256
257
|
"and reserved this stage."
|
|
@@ -3,6 +3,7 @@ from __future__ import annotations
|
|
|
3
3
|
|
|
4
4
|
import json
|
|
5
5
|
import os
|
|
6
|
+
import re
|
|
6
7
|
import tempfile
|
|
7
8
|
from dataclasses import dataclass
|
|
8
9
|
from enum import Enum
|
|
@@ -20,7 +21,12 @@ from .worker_prompt_body import (
|
|
|
20
21
|
)
|
|
21
22
|
from .worker_prompt_contract import PromptRecord, validate_initial_prompt_records
|
|
22
23
|
from .worker_prompt_headers import worker_prompt_headers
|
|
23
|
-
from .worker_prompt_policy import
|
|
24
|
+
from .worker_prompt_policy import (
|
|
25
|
+
APPROVED_PLAN_HEADER,
|
|
26
|
+
IMPLEMENTATION_STAGE_HEADER,
|
|
27
|
+
PromptPlan,
|
|
28
|
+
resolve_prompt_plan_for_manifest,
|
|
29
|
+
)
|
|
24
30
|
|
|
25
31
|
|
|
26
32
|
class PromptDeliveryMode(str, Enum):
|
|
@@ -94,6 +100,7 @@ _CLARIFICATION_AUTHORITY_INTRO = (
|
|
|
94
100
|
)
|
|
95
101
|
_CLARIFICATION_SOURCE_PREFIX = "Source:"
|
|
96
102
|
_REQUIRED_PROMPT_RESOURCES_HEADING = "## Required prompt resources"
|
|
103
|
+
_HTML_COMMENTS = re.compile(r"<!--.*?-->\n?", re.DOTALL)
|
|
97
104
|
|
|
98
105
|
|
|
99
106
|
@dataclass(frozen=True)
|
|
@@ -323,7 +330,7 @@ def _render_prompt(
|
|
|
323
330
|
lines.append(
|
|
324
331
|
f"**Prompt Delivery Mode:** {context.request.delivery_mode.value}"
|
|
325
332
|
)
|
|
326
|
-
lines.extend(
|
|
333
|
+
lines.extend(_implementation_anchor_lines(context, item.plan))
|
|
327
334
|
lines.extend(_prompt_body(context, state, worker, item.plan))
|
|
328
335
|
lines.extend(_resource_lines(context, item))
|
|
329
336
|
except InitialPromptMaterializationError:
|
|
@@ -467,12 +474,27 @@ def _resolve_prompt_resources(
|
|
|
467
474
|
return tuple(
|
|
468
475
|
_PromptResource(
|
|
469
476
|
path=path.resolve(),
|
|
470
|
-
text=
|
|
477
|
+
text=_worker_visible_text(
|
|
478
|
+
_read_required_nonempty_text(path, "required prompt resource")
|
|
479
|
+
),
|
|
471
480
|
)
|
|
472
481
|
for path in paths
|
|
473
482
|
)
|
|
474
483
|
|
|
475
484
|
|
|
485
|
+
def _worker_visible_text(text: str) -> str:
|
|
486
|
+
"""Strip lead-and-maintainer-only passages before a body reaches a worker.
|
|
487
|
+
|
|
488
|
+
These sidecars have two readers: the lead lazy-reads the file to run the
|
|
489
|
+
phase, and the worker receives the same body inlined in its prompt. An
|
|
490
|
+
HTML comment is the seam between them — rendered markdown hides it, so it
|
|
491
|
+
holds the delivery plumbing (which path feeds this file, which heading the
|
|
492
|
+
CLI wrapper greps for) that the worker would otherwise pay for and read as
|
|
493
|
+
an instruction addressed to itself.
|
|
494
|
+
"""
|
|
495
|
+
return _HTML_COMMENTS.sub("", text).lstrip("\n")
|
|
496
|
+
|
|
497
|
+
|
|
476
498
|
def _required_resource_path_candidates(
|
|
477
499
|
context: _MaterializationContext,
|
|
478
500
|
plan: PromptPlan,
|
|
@@ -481,15 +503,17 @@ def _required_resource_path_candidates(
|
|
|
481
503
|
if plan.audience == "implementation-executor":
|
|
482
504
|
paths = (
|
|
483
505
|
_executor_profile_path(context),
|
|
506
|
+
profiles / "_stage-discipline.md",
|
|
484
507
|
profiles / "_coding-conventions-preflight.md",
|
|
485
508
|
profiles / "_implementation-diff-review.md",
|
|
486
509
|
profiles / "_implementation-self-check.md",
|
|
487
510
|
)
|
|
488
511
|
elif plan.audience == "implementation-verifier":
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
512
|
+
# The self-check body is executor-only: it is written to the worker that
|
|
513
|
+
# owns the diff ("fix it or surface the violation", break-and-restore
|
|
514
|
+
# mutation checks), and a verifier is barred from every edit it asks
|
|
515
|
+
# for. Its own blocking taxonomy is inlined in the verifier sidecar.
|
|
516
|
+
paths = (profiles / "_implementation-verifier.md",)
|
|
493
517
|
else:
|
|
494
518
|
return ()
|
|
495
519
|
return paths
|
|
@@ -873,15 +897,35 @@ def _result_paths(
|
|
|
873
897
|
return str(final_report_data_path(Path(expected_report))), worker_result
|
|
874
898
|
|
|
875
899
|
|
|
876
|
-
def
|
|
900
|
+
def _implementation_anchor_lines(
|
|
877
901
|
context: _MaterializationContext,
|
|
878
902
|
plan: PromptPlan,
|
|
879
903
|
) -> list[str]:
|
|
904
|
+
"""Render the anchors only an implementation audience carries.
|
|
905
|
+
|
|
906
|
+
The approved plan and the stage number are absolute here because a CLI
|
|
907
|
+
worker's cwd is the stage worktree, where a `.okstra/...` relative path
|
|
908
|
+
resolves against the wrong root.
|
|
909
|
+
"""
|
|
880
910
|
if plan.audience not in {
|
|
881
911
|
"implementation-executor",
|
|
882
912
|
"implementation-verifier",
|
|
883
913
|
}:
|
|
884
914
|
return []
|
|
915
|
+
worktree = _required_worktree_path(context)
|
|
916
|
+
lines = [f"**Worktree:** {worktree}"]
|
|
917
|
+
if plan.audience == "implementation-executor":
|
|
918
|
+
lines.append(f"cwd for every mutating command: {worktree}")
|
|
919
|
+
lines.append(
|
|
920
|
+
f"{APPROVED_PLAN_HEADER} {_required_approved_plan_path(context)}"
|
|
921
|
+
)
|
|
922
|
+
lines.append(
|
|
923
|
+
f"{IMPLEMENTATION_STAGE_HEADER} {_required_stage_number(context)}"
|
|
924
|
+
)
|
|
925
|
+
return lines
|
|
926
|
+
|
|
927
|
+
|
|
928
|
+
def _required_worktree_path(context: _MaterializationContext) -> str:
|
|
885
929
|
executor_worktree = context.active_context.get("executorWorktree")
|
|
886
930
|
value = (
|
|
887
931
|
_string_value(executor_worktree.get("path"))
|
|
@@ -893,10 +937,58 @@ def _worktree_lines(
|
|
|
893
937
|
"required_input_missing",
|
|
894
938
|
"implementation prompt generation requires a worktree path",
|
|
895
939
|
)
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
940
|
+
return value
|
|
941
|
+
|
|
942
|
+
|
|
943
|
+
def _required_approved_plan_path(context: _MaterializationContext) -> str:
|
|
944
|
+
"""Read the approved plan path from this run's user-input snapshot.
|
|
945
|
+
|
|
946
|
+
run-inputs owns it because it is a user input, not a rendered path — and
|
|
947
|
+
reading it there also keeps runs prepared before this anchor existed
|
|
948
|
+
materializable, since every run has always recorded it.
|
|
949
|
+
"""
|
|
950
|
+
source_artifacts = context.active_context.get("sourceArtifacts")
|
|
951
|
+
run_inputs_rel = (
|
|
952
|
+
_string_value(source_artifacts.get("runInputsPath"))
|
|
953
|
+
if isinstance(source_artifacts, Mapping)
|
|
954
|
+
else ""
|
|
955
|
+
)
|
|
956
|
+
if not run_inputs_rel:
|
|
957
|
+
raise InitialPromptMaterializationError(
|
|
958
|
+
"required_input_missing",
|
|
959
|
+
"implementation prompt generation requires a run inputs path",
|
|
960
|
+
)
|
|
961
|
+
payload = _load_json_object(
|
|
962
|
+
_resolve_input_path(context.project_root, Path(run_inputs_rel)),
|
|
963
|
+
"run inputs",
|
|
964
|
+
)
|
|
965
|
+
inputs = payload.get("inputs")
|
|
966
|
+
value = (
|
|
967
|
+
_string_value(inputs.get("approvedPlanPath"))
|
|
968
|
+
if isinstance(inputs, Mapping)
|
|
969
|
+
else ""
|
|
970
|
+
)
|
|
971
|
+
if not value:
|
|
972
|
+
raise InitialPromptMaterializationError(
|
|
973
|
+
"required_input_missing",
|
|
974
|
+
"implementation prompt generation requires an approved plan path",
|
|
975
|
+
)
|
|
976
|
+
return str(_resolve_input_path(context.project_root, Path(value)))
|
|
977
|
+
|
|
978
|
+
|
|
979
|
+
def _required_stage_number(context: _MaterializationContext) -> str:
|
|
980
|
+
value = _string_value(_active_run_field(context, "stage"))
|
|
981
|
+
if not value:
|
|
982
|
+
raise InitialPromptMaterializationError(
|
|
983
|
+
"required_input_missing",
|
|
984
|
+
"implementation prompt generation requires a stage number",
|
|
985
|
+
)
|
|
986
|
+
return value
|
|
987
|
+
|
|
988
|
+
|
|
989
|
+
def _active_run_field(context: _MaterializationContext, key: str) -> Any:
|
|
990
|
+
run = context.active_context.get("run")
|
|
991
|
+
return run.get(key) if isinstance(run, Mapping) else ""
|
|
900
992
|
|
|
901
993
|
|
|
902
994
|
def _prompt_plan(
|
|
@@ -95,7 +95,7 @@ def hydrate_active_run_context(payload: Mapping[str, Any]) -> dict[str, Any]:
|
|
|
95
95
|
"kind": ACTIVE_CONTEXT_KIND,
|
|
96
96
|
"task": _hydrate_active_task(payload, ctx),
|
|
97
97
|
"workflow": dict(_mapping(payload.get("workflow"))),
|
|
98
|
-
"run": _hydrate_active_run(ctx),
|
|
98
|
+
"run": _hydrate_active_run(payload, ctx),
|
|
99
99
|
"instructionSet": _hydrate_active_instruction_set(payload, ctx),
|
|
100
100
|
"workers": _hydrate_active_workers(payload, ctx),
|
|
101
101
|
"errorLogs": _hydrate_active_error_logs(ctx),
|
|
@@ -182,8 +182,16 @@ def _hydrate_active_task(payload: Mapping[str, Any], ctx: Mapping[str, str]) ->
|
|
|
182
182
|
return task
|
|
183
183
|
|
|
184
184
|
|
|
185
|
-
def _hydrate_active_run(
|
|
185
|
+
def _hydrate_active_run(
|
|
186
|
+
payload: Mapping[str, Any],
|
|
187
|
+
ctx: Mapping[str, str],
|
|
188
|
+
) -> dict[str, str]:
|
|
189
|
+
# `stage` is a run input, not a derived path, so pathHints cannot rebuild
|
|
190
|
+
# it — it survives the round trip only by being read back off the compact
|
|
191
|
+
# payload.
|
|
192
|
+
run = _mapping(payload.get("run"))
|
|
186
193
|
return {
|
|
194
|
+
"stage": str(run.get("stage", "") or ""),
|
|
187
195
|
"runDirectoryPath": ctx.get("RUN_DIR_RELATIVE_PATH", ""),
|
|
188
196
|
"runManifestPath": ctx.get("RUN_MANIFEST_RELATIVE_PATH", ""),
|
|
189
197
|
"teamStatePath": ctx.get("TEAM_STATE_RELATIVE_PATH", ""),
|
|
@@ -506,6 +506,11 @@ def _active_workflow(ctx: dict) -> dict:
|
|
|
506
506
|
|
|
507
507
|
def _active_run(ctx: dict) -> dict:
|
|
508
508
|
return {
|
|
509
|
+
# `implementation` binds one run to one Stage Map stage, and the
|
|
510
|
+
# executor prompt has to name it: its sidecar forbids recomputing the
|
|
511
|
+
# stage from `consumers.jsonl`. Feeds the implementation prompt anchor
|
|
512
|
+
# in `initial_prompt_materialization`.
|
|
513
|
+
"stage": ctx.get("RUN_STAGE", ""),
|
|
509
514
|
"runDirectoryPath": ctx.get("RUN_DIR_RELATIVE_PATH", ""),
|
|
510
515
|
"runManifestPath": ctx.get("RUN_MANIFEST_RELATIVE_PATH", ""),
|
|
511
516
|
"teamStatePath": ctx.get("TEAM_STATE_RELATIVE_PATH", ""),
|
|
@@ -367,8 +367,14 @@ def parse_stage_map_file(markdown_path: Path) -> list[StageMapStage]:
|
|
|
367
367
|
return _parse_schema_v2_stage_map(data, str(data_path))
|
|
368
368
|
|
|
369
369
|
|
|
370
|
-
def
|
|
371
|
-
"""The schema-v2 sidecar as a whole, `{}` for a v1 report.
|
|
370
|
+
def schema_v2_report(markdown_path: Path) -> dict[str, Any]:
|
|
371
|
+
"""The schema-v2 sidecar as a whole, `{}` for a v1 report.
|
|
372
|
+
|
|
373
|
+
Public because every caller that must branch on report schema needs it —
|
|
374
|
+
including `validators/validate-implementation-plan-stages.py`, which is a
|
|
375
|
+
separate process and cannot reach a private helper without copying the
|
|
376
|
+
sidecar-detection rule and letting the two drift.
|
|
377
|
+
"""
|
|
372
378
|
data_path = Path(markdown_path).resolve().with_suffix(".data.json")
|
|
373
379
|
if not data_path.exists():
|
|
374
380
|
return {}
|
|
@@ -383,7 +389,7 @@ def _schema_v2_report(markdown_path: Path) -> dict[str, Any]:
|
|
|
383
389
|
|
|
384
390
|
def _planning_section(markdown_path: Path) -> dict[str, Any]:
|
|
385
391
|
"""The report's `implementationPlanning` block, `{}` for v1."""
|
|
386
|
-
planning =
|
|
392
|
+
planning = schema_v2_report(markdown_path).get("implementationPlanning")
|
|
387
393
|
return planning if isinstance(planning, dict) else {}
|
|
388
394
|
|
|
389
395
|
|
|
@@ -407,7 +413,7 @@ def _stage_narratives(value: Any) -> dict[int, dict[str, Any]]:
|
|
|
407
413
|
|
|
408
414
|
def load_planning_detail(markdown_path: Path) -> PlanningDetail:
|
|
409
415
|
"""Read one report's narrative rows; empty for a schema-v1 report."""
|
|
410
|
-
report =
|
|
416
|
+
report = schema_v2_report(markdown_path)
|
|
411
417
|
planning = report.get("implementationPlanning")
|
|
412
418
|
if not isinstance(planning, dict) or not planning:
|
|
413
419
|
return PlanningDetail({}, {})
|
|
@@ -4484,11 +4484,28 @@ def _reset_from(state: WizardState, target_step: str) -> None:
|
|
|
4484
4484
|
idx = next((i for i, s in enumerate(STEPS) if s.id == target_step), -1)
|
|
4485
4485
|
if idx < 0:
|
|
4486
4486
|
return
|
|
4487
|
+
# A later step may own a field an earlier answered step also owns —
|
|
4488
|
+
# `handoff_stage_pick` owns `approved_plan_path` because it resolves the
|
|
4489
|
+
# plan on its own. Clearing it while rewinding to a step in front of it
|
|
4490
|
+
# drops an answer the user never revisited; the rewound step then fails its
|
|
4491
|
+
# own `applies` guard, so no step is left to ask and the wizard reports
|
|
4492
|
+
# done while `outcome` still refuses it as incomplete.
|
|
4493
|
+
owned_earlier = {
|
|
4494
|
+
fname
|
|
4495
|
+
for step in STEPS[:idx]
|
|
4496
|
+
if step.id in state.answered
|
|
4497
|
+
for fname in step.owns
|
|
4498
|
+
}
|
|
4487
4499
|
cleared_ids: set[str] = set()
|
|
4488
|
-
for step in STEPS[idx:]:
|
|
4500
|
+
for position, step in enumerate(STEPS[idx:]):
|
|
4489
4501
|
cleared_ids.add(step.id)
|
|
4502
|
+
is_rewind_target = position == 0
|
|
4490
4503
|
for fname in step.owns:
|
|
4491
|
-
|
|
4504
|
+
# The target's own fields always clear — that is the answer the
|
|
4505
|
+
# user came back to replace, even when an earlier step declares it
|
|
4506
|
+
# too (`task_pick` derives `task_type` for an existing task).
|
|
4507
|
+
if is_rewind_target or fname not in owned_earlier:
|
|
4508
|
+
_reset_field(state, fname)
|
|
4492
4509
|
state.answered = [a for a in state.answered if a not in cleared_ids]
|
|
4493
4510
|
direct_input_pending = {
|
|
4494
4511
|
S_FEATURE_EVIDENCE: "feature_evidence_pending_text",
|
|
@@ -11,6 +11,21 @@ ANALYSIS_WORKER_LABELS = {
|
|
|
11
11
|
"codex": "Codex worker",
|
|
12
12
|
"antigravity": "Antigravity worker",
|
|
13
13
|
}
|
|
14
|
+
# An implementation audience shares this body but not its premise: the executor
|
|
15
|
+
# owns the diff and the verifier grades it, so neither is producing one of the
|
|
16
|
+
# independent findings that cross-verification triangulates.
|
|
17
|
+
ROLE_STATEMENTS = {
|
|
18
|
+
"executor": (
|
|
19
|
+
"You are the Executor for this implementation stage — the only worker "
|
|
20
|
+
"permitted to mutate project files. Carry the stage end to end and "
|
|
21
|
+
"produce the worker result."
|
|
22
|
+
),
|
|
23
|
+
"verifier": (
|
|
24
|
+
"You are a verifier for this implementation stage. Reproduce its QA "
|
|
25
|
+
"yourself, stay read-only on project files, and return an independent "
|
|
26
|
+
"verdict."
|
|
27
|
+
),
|
|
28
|
+
}
|
|
14
29
|
|
|
15
30
|
|
|
16
31
|
def analysis_prompt_body(
|
|
@@ -30,10 +45,7 @@ def analysis_prompt_body(
|
|
|
30
45
|
f"# {label} Dispatch",
|
|
31
46
|
"",
|
|
32
47
|
"## Role",
|
|
33
|
-
(
|
|
34
|
-
f"You are the {label} for okstra cross-verification. "
|
|
35
|
-
"Produce an independent worker result."
|
|
36
|
-
),
|
|
48
|
+
_role_statement(label, role),
|
|
37
49
|
"",
|
|
38
50
|
"## Task",
|
|
39
51
|
f"- Task key: `{_require_string(manifest, 'taskKey')}`",
|
|
@@ -52,6 +64,14 @@ def analysis_prompt_body(
|
|
|
52
64
|
]
|
|
53
65
|
|
|
54
66
|
|
|
67
|
+
def _role_statement(label: str, role: str) -> str:
|
|
68
|
+
return ROLE_STATEMENTS.get(
|
|
69
|
+
role,
|
|
70
|
+
f"You are the {label} for okstra cross-verification. "
|
|
71
|
+
"Produce an independent worker result.",
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
|
|
55
75
|
def analysis_input_lines(
|
|
56
76
|
manifest: Mapping[str, Any],
|
|
57
77
|
active_context: Mapping[str, Any],
|
|
@@ -8,6 +8,7 @@ from typing import Any, Mapping, Sequence
|
|
|
8
8
|
|
|
9
9
|
from .worker_prompt_policy import (
|
|
10
10
|
ERRORS_PATH_HEADERS,
|
|
11
|
+
IMPLEMENTATION_HEADERS,
|
|
11
12
|
PromptPlan,
|
|
12
13
|
resolve_prompt_plan_for_manifest,
|
|
13
14
|
)
|
|
@@ -51,7 +52,7 @@ _NON_BODY_PREFIXES = (
|
|
|
51
52
|
*ERRORS_PATH_HEADERS,
|
|
52
53
|
"**Read scope:**",
|
|
53
54
|
"**File write mode:**",
|
|
54
|
-
|
|
55
|
+
*IMPLEMENTATION_HEADERS,
|
|
55
56
|
"**Verification scope:**",
|
|
56
57
|
"**Verification base ref:**",
|
|
57
58
|
"**Verification head ref:**",
|
|
@@ -27,6 +27,17 @@ ERRORS_PATH_HEADERS = (
|
|
|
27
27
|
"**Errors log path:**",
|
|
28
28
|
"**Errors sidecar path:**",
|
|
29
29
|
)
|
|
30
|
+
# The executor sidecar tells the worker to execute exactly one Stage Map stage
|
|
31
|
+
# against the approved plan, and forbids recomputing that stage from
|
|
32
|
+
# `consumers.jsonl`. Both facts therefore have to reach the worker prompt
|
|
33
|
+
# itself: the lead's launch prompt carries them, and no worker reads that.
|
|
34
|
+
APPROVED_PLAN_HEADER = "**Approved plan:**"
|
|
35
|
+
IMPLEMENTATION_STAGE_HEADER = "**Stage for this implementation run:**"
|
|
36
|
+
IMPLEMENTATION_HEADERS = (
|
|
37
|
+
"**Worktree:**",
|
|
38
|
+
APPROVED_PLAN_HEADER,
|
|
39
|
+
IMPLEMENTATION_STAGE_HEADER,
|
|
40
|
+
)
|
|
30
41
|
FINAL_VERIFICATION_HEADERS = (
|
|
31
42
|
"**Worktree:**",
|
|
32
43
|
"**Verification scope:**",
|
|
@@ -96,14 +107,14 @@ def resolve_prompt_plan(
|
|
|
96
107
|
return _plan(
|
|
97
108
|
"implementation-executor",
|
|
98
109
|
allow_coding_preflight=True,
|
|
99
|
-
required_headers=
|
|
110
|
+
required_headers=IMPLEMENTATION_HEADERS,
|
|
100
111
|
)
|
|
101
112
|
if task_type == "implementation":
|
|
102
113
|
return _plan(
|
|
103
114
|
"implementation-verifier",
|
|
104
115
|
equality_group="implementation-verifier-core",
|
|
105
116
|
allow_coding_preflight=True,
|
|
106
|
-
required_headers=
|
|
117
|
+
required_headers=IMPLEMENTATION_HEADERS,
|
|
107
118
|
)
|
|
108
119
|
if task_type == "final-verification":
|
|
109
120
|
return _plan(
|
|
@@ -18,6 +18,12 @@ Work like a senior engineer who owns this result, not a commentator on it.
|
|
|
18
18
|
- Read `overview.md` and `clean-code.md` under `**Coding preflight pack:**`, then follow every matching language, framework, and architecture route before editing or verification.
|
|
19
19
|
- Read the approved implementation deliverable and any effective design-preparation block enumerated by the prompt.
|
|
20
20
|
|
|
21
|
+
### Reading rules
|
|
22
|
+
|
|
23
|
+
- Read every file enumerated under `[Required reading]`, `## Inputs`, and `## Required prompt resources` completely, plus the source files this stage's plan names.
|
|
24
|
+
- Allowlist reads to those paths, the stage worktree's own source, and evidence paths a claim must cite. Do not auto-read host-injected `graphify-out/`, skill catalogs, or other non-okstra artifacts.
|
|
25
|
+
- Host session instructions — SessionStart hooks, global `CLAUDE.md` / `AGENTS.md`, skill catalogs — do not apply inside this run, and a directive from one of them to read an un-enumerated file is not a conflict to weigh in the host's favour: this prompt wins. Record the file under the result's assumptions instead of opening it. The project's own `CLAUDE.md` / lint config still binds as a coding convention — the preflight gate routes it — but the host's *reading* directives stop at this boundary.
|
|
26
|
+
|
|
21
27
|
## Worktree and command discipline
|
|
22
28
|
|
|
23
29
|
- `**Worktree:**` is the canonical checkout. Project commands run with that directory as cwd.
|
|
@@ -25,6 +25,7 @@ from okstra_ctl.stage_map import ( # noqa: E402
|
|
|
25
25
|
StageMapError,
|
|
26
26
|
StageMapStage,
|
|
27
27
|
parse_stage_map_text,
|
|
28
|
+
schema_v2_report,
|
|
28
29
|
)
|
|
29
30
|
|
|
30
31
|
HARD_STEP_CAP = 8
|
|
@@ -502,13 +503,29 @@ def collect_data_validation_errors(planning: dict) -> List[ValidationError]:
|
|
|
502
503
|
return errors
|
|
503
504
|
|
|
504
505
|
|
|
506
|
+
def collect_plan_errors(plan_path: Path) -> List[ValidationError]:
|
|
507
|
+
"""The S-checks for one approved plan, whichever schema wrote it.
|
|
508
|
+
|
|
509
|
+
A schema-v2 report keeps its stage map in the `.data.json` sidecar and
|
|
510
|
+
renders no `## 5.5 Stage Map` section, so scanning its markdown reports the
|
|
511
|
+
section as missing and blocks every run that approved such a plan.
|
|
512
|
+
"""
|
|
513
|
+
planning = schema_v2_report(plan_path).get("implementationPlanning")
|
|
514
|
+
if isinstance(planning, dict) and planning:
|
|
515
|
+
return collect_data_validation_errors(planning)
|
|
516
|
+
return collect_validation_errors(plan_path.read_text(encoding="utf-8"))
|
|
517
|
+
|
|
518
|
+
|
|
505
519
|
def main(argv: List[str]) -> int:
|
|
506
520
|
p = argparse.ArgumentParser()
|
|
507
521
|
p.add_argument("--plan", required=True)
|
|
508
522
|
args = p.parse_args(argv)
|
|
509
|
-
text = Path(args.plan).read_text(encoding="utf-8")
|
|
510
523
|
|
|
511
|
-
|
|
524
|
+
try:
|
|
525
|
+
errors = collect_plan_errors(Path(args.plan))
|
|
526
|
+
except StageMapError as exc:
|
|
527
|
+
print(f"S0 stage=0: {exc.reason}", file=sys.stderr)
|
|
528
|
+
return 1
|
|
512
529
|
if errors:
|
|
513
530
|
for e in errors:
|
|
514
531
|
print(f"{e.code} stage={e.stage}: {e.message}", file=sys.stderr)
|
|
@@ -22,7 +22,7 @@ Usage:
|
|
|
22
22
|
okstra wizard new-state-file
|
|
23
23
|
okstra wizard init --state-file <path> --project-root <p> --project-id <id> --host-runtime <runtime> \\
|
|
24
24
|
--entry-mode current-session --available-function <semantic-name>
|
|
25
|
-
okstra wizard step --state-file <path>
|
|
25
|
+
okstra wizard step --state-file <path> (--answer <value> | --no-submit)
|
|
26
26
|
okstra wizard render-args --state-file <path>
|
|
27
27
|
okstra wizard confirmation --state-file <path>
|
|
28
28
|
okstra wizard outcome --state-file <path>
|
|
@@ -34,11 +34,14 @@ All subcommands emit a single JSON object on stdout. On validation failure
|
|
|
34
34
|
'step' returns {ok:false, error, current} so the skill can re-prompt.
|
|
35
35
|
`;
|
|
36
36
|
|
|
37
|
-
//
|
|
38
|
-
// after
|
|
39
|
-
// "--". A heuristic that rejects "--"-prefixed values would
|
|
40
|
-
// directive/base-ref answers unsubmittable. The inline
|
|
41
|
-
// callers pass any value (including empty)
|
|
37
|
+
// Only the flags listed here are boolean; every other wizard flag takes a
|
|
38
|
+
// value, so the token after it is always its value — even free-text answers
|
|
39
|
+
// that begin with "--". A heuristic that rejects "--"-prefixed values would
|
|
40
|
+
// make legitimate directive/base-ref answers unsubmittable. The inline
|
|
41
|
+
// "--key=value" form lets callers pass any value (including empty)
|
|
42
|
+
// unambiguously.
|
|
43
|
+
const BOOLEAN_FLAGS = new Set(["no-submit"]);
|
|
44
|
+
|
|
42
45
|
export function parseFlags(args) {
|
|
43
46
|
const out = {};
|
|
44
47
|
const setFlag = (key, value) => {
|
|
@@ -59,6 +62,10 @@ export function parseFlags(args) {
|
|
|
59
62
|
continue;
|
|
60
63
|
}
|
|
61
64
|
const key = a.slice(2);
|
|
65
|
+
if (BOOLEAN_FLAGS.has(key)) {
|
|
66
|
+
out[key] = true;
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
62
69
|
if (i + 1 >= args.length) {
|
|
63
70
|
throw new Error(`flag --${key} requires a value`);
|
|
64
71
|
}
|
|
@@ -81,8 +88,13 @@ export function buildPythonArgs(sub, flags, workspaceRoot = "") {
|
|
|
81
88
|
pyArgs.push("--available-function", value);
|
|
82
89
|
}
|
|
83
90
|
pyArgs.push("--entry-mode", flags["entry-mode"] ?? "current-session");
|
|
84
|
-
} else if (sub === "step"
|
|
85
|
-
|
|
91
|
+
} else if (sub === "step") {
|
|
92
|
+
if (flags["no-submit"]) {
|
|
93
|
+
pyArgs.push("--no-submit");
|
|
94
|
+
}
|
|
95
|
+
if (flags.answer !== undefined) {
|
|
96
|
+
pyArgs.push("--answer", flags.answer);
|
|
97
|
+
}
|
|
86
98
|
}
|
|
87
99
|
return pyArgs;
|
|
88
100
|
}
|