okstra 0.199.3 → 0.199.5
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/docs/cli.md +1 -1
- package/package.json +1 -1
- package/runtime/BUILD.json +2 -2
- package/runtime/prompts/launch.template.md +12 -0
- package/runtime/prompts/lead/okstra-lead-contract.md +2 -0
- package/runtime/prompts/lead/plan-body-verification.md +17 -5
- package/runtime/prompts/profiles/implementation-planning.md +1 -1
- package/runtime/prompts/wizard/prompts.ko.json +1 -0
- package/runtime/python/okstra_ctl/adapters/hosts/codex/relay.md +18 -4
- package/runtime/python/okstra_ctl/agent/prompt_cli/corrections.py +11 -3
- package/runtime/python/okstra_ctl/agent/prompt_cli/materialize.py +23 -1
- package/runtime/python/okstra_ctl/consumers.py +0 -55
- package/runtime/python/okstra_ctl/domain/write_policy.py +7 -1
- package/runtime/python/okstra_ctl/execution_mutation_audit.py +6 -17
- package/runtime/python/okstra_ctl/final_report_schema.py +16 -16
- package/runtime/python/okstra_ctl/plan_items_cli.py +29 -4
- package/runtime/python/okstra_ctl/report_assembly.py +15 -18
- package/runtime/python/okstra_ctl/report_corrections.py +24 -15
- package/runtime/python/okstra_ctl/report_finalize.py +1 -1
- package/runtime/python/okstra_ctl/report_narrative.py +33 -6
- package/runtime/python/okstra_ctl/report_synthesis_packet.py +1 -1
- package/runtime/python/okstra_ctl/wizard/confirmation.py +4 -0
- package/runtime/python/okstra_ctl/wizard/engine.py +4 -16
- package/runtime/python/okstra_ctl/wizard/picker_navigation.py +7 -37
- package/runtime/python/okstra_ctl/worktree_status_cli.py +2 -3
- package/runtime/python/okstra_ctl/write_policy.py +51 -4
- package/runtime/schemas/execution-manifest-v2.schema.json +1 -0
- package/runtime/schemas/final-report-v2.0.schema.json +1 -1
- package/runtime/schemas/final-report-v3.0.schema.json +1 -1
- package/runtime/skills/okstra-run/SKILL.md +9 -1
- package/runtime/validators/validate-run.py +3 -0
package/docs/cli.md
CHANGED
|
@@ -816,7 +816,7 @@ The `okstra` Node CLI (`bin/okstra`) provides both installer/admin commands and
|
|
|
816
816
|
| `okstra plan-items <prepare\|prompt\|validate-prepared> --run-manifest <path> …` | Bind the implementation-planning verification queue to the run manifest. `prepare` extracts the exact queue from `--narrative` and, when `designPreparation.mode` is `no-design-inputs` and the Stage Map has one row, flips `convergence.planBodyVerification.gating` to `false` (stdout `Gating`). `prompt` emits its fixed lossless view, ending with the parser-facing `## Response format` block so the block reaches every verifier with the queue; `validate-prepared` proves the prepared queue still matches the narrative. With `--state <plan-body-verification.json>` the round is a re-verification: `prepare` also carries each queued item's recorded votes and `selfFixNote` into the envelope as `priorRounds`, `prompt` renders them as that item's `**Prior round dissent**` block behind a re-verification preamble, and `validate-prepared` re-derives the carry and rejects an envelope that dropped it. Python resolves the convergence-owned state path, so model callers never choose it. |
|
|
817
817
|
| `okstra plan-items seed --narrative <report-narrative.md> --state <plan-body-verification.json> [--prior-state <previous plan-body-verification.json>]` | Create the convergence-owned `planBodyVerification.planItems[]` rows every verdict lands in, from the same deterministic extraction `extract` uses. The historical v2 form is `--data <data.json>`. Idempotent by id: an existing row keeps its verdicts and carried fields. Reports `seeded` / `existing` counts. `--prior-state` carries the previous **run**'s verdicts into this one: a newly seeded item whose `contentHash` equals that run's `verifiedContentHash` for the same id inherits its `verdicts[]` and is tagged `carriedForwardFromSeq` with the seq read off the prior filename, so round 1 does not re-judge text nobody changed. A matching id alone never carries — `P-*` ids are positional and shift. It requires `--state`, refuses a prior state whose task root differs from the one `--state` lives under (the state file carries no task identity, so its path is the only identity there is), and when it carries anything it rewrites the sibling `plan-items-*.json` `dispatchQueue` the way `incremental-carry` does. Adds `carried` / `carriedForwardFromSeq` to the reported counts. |
|
|
818
818
|
| `okstra plan-items apply-verdicts --state <plan-body-verification.json> --result <worker-id>=<result.md>… --round <N>` | Read each worker's Markdown verdict directly, validate every current `P-*` id, reject duplicate worker submissions, and overwrite that round's stored verdicts. `--items <plan-items artifact>` narrows and persists the assignment shared by verdict validation and round completion to what a partial round actually dispatched — pass the `--tie-vote` artifact on a critic tie round, or every item the critic was never given is refused as unanswered. The unanswered check still applies inside the narrowed set. Earlier verdicts and completed-round history outside that set remain unchanged. The historical `--verdicts <file>` form remains automation compatibility only. Without `--append` every recorded verdict row of the queued items is replaced; when a row belongs to a round `complete-round` never closed, the command refuses before writing and names the `complete-round --round <M>` to run first. `--discard-open-rounds` replaces anyway — the recovery path when those rounds are being re-applied from their result files in order (the discarded rows are printed); it also restores `dispatchQueue` to the items those result files answer, since the persisted queue belongs to the latest round and an earlier round's verdicts would otherwise be refused as outside it. |
|
|
819
|
-
| `okstra plan-items complete-round --state <plan-body-verification.json> --run-manifest <current-run-manifest.json> --round <N> [--items <plan-items artifact>] [--self-fix-note <item-id>=<markdown-file>]… [--self-fix-group <cause-file>=<item-id>[,<item-id>...]]… [--self-fix-stop-reason <all-resolved\|no-progress\|max-rounds-reached>]` | After `plan-verify` succeeds, atomically derive and record the round's per-item votes, gate result, participant counts from the actual assigned roster, immutable completion time, convergence history, and optional self-fix notes/groups read from Markdown files. `--self-fix-group` requires `--self-fix-stop-reason` — there is no default. `--self-fix-stop-reason` alone records a stop for a round that rewrote nothing and leaves `selfFixGroups` / `selfFixRoundsApplied` untouched.
|
|
819
|
+
| `okstra plan-items complete-round --state <plan-body-verification.json> --run-manifest <current-run-manifest.json> --round <N> [--items <plan-items artifact>] [--self-fix-note <item-id>=<markdown-file>]… [--self-fix-group <cause-file>=<item-id>[,<item-id>...]]… [--self-fix-stop-reason <all-resolved\|no-progress\|max-rounds-reached>]` | After `plan-verify` succeeds, atomically derive and record the round's per-item votes, gate result, participant counts from the actual assigned roster, immutable completion time, convergence history, and optional self-fix notes/groups read from Markdown files. `--self-fix-group` requires `--self-fix-stop-reason` — there is no default. `--self-fix-stop-reason` alone records a stop for a round that rewrote nothing and leaves `selfFixGroups` / `selfFixRoundsApplied` untouched. When `--items` is omitted, completion automatically reads this run's canonical prepared queue and restores it when it includes all votes recorded for the requested round. If that file belongs to a narrower later batch, the persisted queue stays authoritative. This recovers saved critic results after an upgrade without requiring the lead to add a recovery flag. An explicit `--items` still selects a prepared artifact. Only the assigned items need verdicts for this round; earlier verdicts elsewhere remain unchanged. Missing-verdict errors name the round and exact dispatched item ids. Models do not write the state JSON. Stdout also carries `nextDispatch`. With `gating=false`, critic corrections are exempt from the analyser round limit and do not require an even split. A later `--append` updates the critic verdict while retaining analyser votes and completed round history. A second automatic rewrite is rejected; the one rewrite may contain several cause groups. `selfFixRoundsApplied` remains its verification round number, not a rewrite count. |
|
|
820
820
|
| `okstra plan-items next-dispatch --state <plan-body-verification.json> [--run-manifest <path>]` | After `apply-verdicts`, decide whether this round opens a worker batch. `kind: none` — missing-dependency `UNVERIFIABLE` only, no new batch. `kind: worker-correction` — re-prompt only those workers; peers stay idle. `kind: critic-tie` — unsettled analyser 1-1 on a run that rostered a critic, `critic-worker` on those item ids only. `kind: user-decision` — the same 1-1 on a run with no critic rostered: no in-band vote can break it, so open one `okstra approval-decision open` per item (classification `correctness-critical` for unresolved kind `a` outside `P-Var-*` or kind `f` on `P-Req-*`, otherwise `noncritical-dissent`) plus its `## 1. Clarification Items` row and dispatch no further verification for them. `--run-manifest` is what tells the two apart (`invocationAssignments` `critic/*`); without it the answer stays `critic-tie`. A missing path is never environment-unverifiable. After the single automatic self-fix, remaining items route to `lead-decision` first, then `user-decision`, with no further automatic worker batch. |
|
|
821
821
|
| `okstra plan-items resolve-dissent --state <plan-body-verification.json> --item <P-id> --decision-file <markdown-file>` | Record the lead decision, its authority within agreed scope, and cited evidence after the one self-fix. Accepts only current, noncritical design judgements; facts, user-input requirements and non-results require user confirmation. Keeps original votes and appends the decision to the report dissent log. The decision expires when its content, scope or verdicts change. |
|
|
822
822
|
| `okstra plan-items correction-prompt --state <plan-body-verification.json> --run-manifest <path> --worker <id>` | Emit the planning-time environment-gap paragraph, then the assigned queue. The environment exception is first. Exits 2 unless `next-dispatch` named that worker as a blanket-UNVERIFIABLE correction target. |
|
package/package.json
CHANGED
package/runtime/BUILD.json
CHANGED
|
@@ -41,6 +41,18 @@ Relaying a wizard step does not change the option set — relay every `options[]
|
|
|
41
41
|
|
|
42
42
|
## User closeout (BLOCKING)
|
|
43
43
|
|
|
44
|
+
Progress, remaining work, and recommendation
|
|
45
|
+
|
|
46
|
+
At each phase or implementation-stage boundary, at task completion, and before a controlled session pause or handoff, give a concise update in the resolved Report Language. Identify the task and the phase or stage, then include these three items (translate the labels):
|
|
47
|
+
|
|
48
|
+
- Progress: what this boundary actually completed, the result of checks already run, and links to the relevant persisted evidence. Distinguish a completed phase from a completed task; a dispatched worker is still in progress.
|
|
49
|
+
- Remaining work: unfinished work in the current task, open approval or input blockers with their IDs, and any registered follow-up tasks. Say explicitly when none remains in the verified scope; when state is unavailable, say unknown rather than inventing completion or a backlog.
|
|
50
|
+
- Recommendation: the next concrete action and why it comes next. After finalization, use `nextCommand` and `nextRecommendedPhase.rationale` from the result under the routing rules below, including `nextInGroup` when present. Before finalization, use the active run's recorded stage queue and recovery or resume information; do not reuse an earlier run's pointer or invent a resume command.
|
|
51
|
+
|
|
52
|
+
During an authorized continuous run, put the update beside the existing progress checkpoint and continue to the next queued step without asking for confirmation merely to deliver this update. At a controlled session pause or handoff, distinguish persisted results from in-progress work and include the available resume reference. At task completion, distinguish remaining work in this task from follow-ups or the next task in its group. If there is no recorded next action, say none is recommended. These updates do not replace checkpoints or authorize another task.
|
|
53
|
+
|
|
54
|
+
Prompt delivery is checked by `tests/contract/test_next_phase_authoring_delivery.py::test_lead_closeout_names_a_command_the_user_can_run`; this is guidance for the lead's prose, not runtime validation of the emitted message.
|
|
55
|
+
|
|
44
56
|
After Phase 7 persistence, the last user-facing message of this run is the next command. A status dump is not a close. A prohibition (`do not start implementation`) is not a next action. This applies to every task type.
|
|
45
57
|
|
|
46
58
|
**Read the pointer from the `report-finalize` result — do not re-derive it.** Its top-level `nextRecommendedPhase` object (`phase`, `status`, `rationale`) is the value every row below branches on, and the same three values are repeated on stderr as `next phase status:` / `next phase:` / `next phase rationale:`. You did not write that object and you cannot recompute it: you author your task type's routing field, and Phase 7 projects the pointer from it plus the approval state — for `implementation-planning` there is no routing field at all. If the result carries `nextRecommendedPhaseError`, say in one line that the pointer could not be read, then close on the `validate-run` row below. The same result also carries `nextCommand` — `{command, note}`, the table below already applied to this run. When `command` is non-empty it is the close; when it is empty the `note` says what to do with the `rationale` instead.
|
|
@@ -104,6 +104,8 @@ User-utterance interpretation rule:
|
|
|
104
104
|
|
|
105
105
|
## Progress reporting (BLOCKING)
|
|
106
106
|
|
|
107
|
+
At each phase or implementation-stage boundary, at task completion, and before a controlled session pause or handoff, follow the launch prompt's "Progress, remaining work, and recommendation" guidance. Include the result and evidence, unfinished work and blockers, and the next action with its reason. During an authorized continuous run, deliver this update beside the checkpoint and continue; do not turn the update into an approval gate. The same guidance applies to the final reply after persistence below.
|
|
108
|
+
|
|
107
109
|
A single okstra run frequently spans 30–120 minutes with multi-minute silent windows while workers run; without progress signals the user cannot distinguish "still working" from "hung". Lead MUST emit a single short progress line at each checkpoint below — plain user-facing text in a separate brief message (not buried inside a tool call), one line per checkpoint, format: `PROGRESS: <phase-id> <verb-phrase>`. Emit the line raw — the literal `PROGRESS:` token must begin the line. Do NOT wrap it in inline-code backticks (`` `PROGRESS: ...` ``) or a ```` ``` ```` code fence; markdown wrapping is what the post-hoc conformance validator scrapes around, and raw emit keeps the signal unambiguous.
|
|
108
110
|
|
|
109
111
|
Record each checkpoint with `okstra lead-progress append --project-root <dir> --run-manifest <path> --phase <phase-id>`, then emit the `progressLine` it prints as the user-facing line. The command resolves `leadEventsPath` from the run manifest and writes the checkpoint there — on a host whose adapter declares `sessionAccounting: artifact-only` that ledger is the only place the post-hoc validator can read it, so a conversation line alone leaves no trace and the run is reported as missing the checkpoint. Pass `--worker <role>` on the per-worker checkpoints (it rewrites a phase-specific functional label into the roster role team-state records), `--field NAME=VALUE` for the remaining `key=value` tokens in the order the line carries them, and `--detail <text>` where the line ends in a verb phrase; the wording listed below is the default for the fixed-prose checkpoints.
|
|
@@ -335,10 +335,20 @@ Every plan-body verification and self-fix instruction passes [okstra-lead-contra
|
|
|
335
335
|
|
|
336
336
|
Before each verifier call, write one task-instructions file under the current
|
|
337
337
|
run's `state/` directory and run `okstra agent-prompt materialize` with
|
|
338
|
-
`--audience reverification-worker`,
|
|
339
|
-
`--assignment-ref reverify/<workerId>`, the exact `--worker-id`, and
|
|
338
|
+
`--audience reverification-worker`, the exact `--worker-id`, and
|
|
340
339
|
`--dispatch-kind plan-verify-r<N>`, matching the `<N>` in the result filename of
|
|
341
|
-
step 3.
|
|
340
|
+
step 3. For analyser verifiers, use `--assignment-ref reverify/<workerId>`.
|
|
341
|
+
For `critic-worker`, use `--assignment-ref critic/scope` from this run's
|
|
342
|
+
`invocationAssignments`, including a tie round or a later critic correction.
|
|
343
|
+
The verdict worker name `critic-worker` does not create a `reverify/critic-worker`
|
|
344
|
+
assignment. Keep the selected critic's existing model assignment.
|
|
345
|
+
**Enforced:** `okstra_ctl.agent.prompt_cli.run_identity._validate_run_identity`
|
|
346
|
+
accepts the planning critic's verification audience; materialization resolves
|
|
347
|
+
the declared assignment and reports the correct critic reference when the
|
|
348
|
+
invented `reverify/critic-worker` reference is used.
|
|
349
|
+
`tests/run/test_agent_prompt_cli.py` verifies the declared critic's reservation
|
|
350
|
+
and dispatch identity plus this recovery diagnostic.
|
|
351
|
+
That kind, not `reverify-r<N>`, is what this round takes: the verification
|
|
342
352
|
prompt contract requires the renderer signature of the queue's canonical renderer,
|
|
343
353
|
and for plan items that renderer is `okstra plan-items prompt`, not `okstra
|
|
344
354
|
convergence reverify-prompt`. Pass that command's output verbatim as
|
|
@@ -355,7 +365,9 @@ source `RoleExecution` row from the run manifest's static role state. Use its
|
|
|
355
365
|
`RoleExecution` row's `roleExecutionRef`, not that row's
|
|
356
366
|
`sourceRoleExecutionRef` field. Pass `--source-role-execution-ref
|
|
357
367
|
<sourceRoleExecutionRef>`. Do not derive that reference from `workerId`,
|
|
358
|
-
provider, model, or execution-label text.
|
|
368
|
+
provider, model, or execution-label text. For `critic-worker`, select the static
|
|
369
|
+
`role: critic` row; the runtime reserves its verifier execution under the
|
|
370
|
+
existing critic participant and model. A legacy v1 run omits this flag. Run
|
|
359
371
|
`okstra agent-prompt verify` against the
|
|
360
372
|
returned `metadataPath` before dispatch and use the returned `promptPath`
|
|
361
373
|
without modification. Native-session calls use only `hostModelValue`; before
|
|
@@ -391,7 +403,7 @@ For contract 3.0, `prepare` checks the selected-direction draft with the same se
|
|
|
391
403
|
- `dissent-isolated` — only one worker `DISAGREE`s, others `AGREE`. On a blocking kind (`b` / `c` / `e`, and kind `a` on `P-Var-*`) this is scored `majority-disagree` and **blocks approval**. Advisory-only `DISAGREE(d)` and `P-Rb-*` stay recorded dissent and do not block. (Distinct from finding-convergence `worker-unique`, which means the *opposite*: only one worker AGREEs.)
|
|
392
404
|
- `majority-disagree` — a *majority* of analysers `DISAGREE` (majority needs ≥2 participating non-error votes; rollback-ordering `DISAGREE(d)` votes are advisory and excluded from the tally), OR any blocking-kind dissent with ≥2 participating votes (a minority `DISAGREE` is not outvoted), OR an unresolved single-vote-blocking kind fires: one reproduced `DISAGREE(a)` on any item other than a `P-Var-*` one, or one reproduced `DISAGREE(f)` on a `P-Req-*` item (see §"Single-vote-blocking kinds"). This classification **blocks approval**. A valid critic correction is scored before these blocking rules, whether or not the analysers split evenly.
|
|
393
405
|
- `needs-reverify` — one of two shapes the round could not settle.
|
|
394
|
-
- **An even split on a blocking kind.** A panel splitting evenly (1-AGREE / 1-DISAGREE, 2-2, …) needs a critic decision. An unresolved single-vote-blocking kind remains `majority-disagree`; other unresolved splits are `needs-reverify`. Do **not** re-run the original two. Dispatch `critic-worker` immediately on those items only (`okstra plan-items prepare --tie-vote`, then `okstra plan-items prompt`). The prompt carries the analyser split and no other plan items. Read the answer with `okstra plan-items collect-verdicts --items <the `--tie-vote` plan-items artifact> --result critic-worker=<path> --output <envelope>` — `--items` takes that artifact, whose `dispatchQueue` is the tie items, so pointing the next step at the raw result is refused against this round's full queue. Record the critic vote as `verdicts[].worker = critic-worker` with `okstra plan-items apply-verdicts --append --items <the `--tie-vote` plan-items artifact> --result critic-worker=<path>` — `--items` persists the exact partial `dispatchQueue` used by verdict validation and `complete-round`. Earlier verdicts and completed-round history outside that queue remain unchanged. If a previous version
|
|
406
|
+
- **An even split on a blocking kind.** A panel splitting evenly (1-AGREE / 1-DISAGREE, 2-2, …) needs a critic decision. An unresolved single-vote-blocking kind remains `majority-disagree`; other unresolved splits are `needs-reverify`. Do **not** re-run the original two. Dispatch `critic-worker` immediately on those items only (`okstra plan-items prepare --tie-vote`, then `okstra plan-items prompt`). The prompt carries the analyser split and no other plan items. Read the answer with `okstra plan-items collect-verdicts --items <the `--tie-vote` plan-items artifact> --result critic-worker=<path> --output <envelope>` — `--items` takes that artifact, whose `dispatchQueue` is the tie items, so pointing the next step at the raw result is refused against this round's full queue. Record the critic vote as `verdicts[].worker = critic-worker` with `okstra plan-items apply-verdicts --append --items <the `--tie-vote` plan-items artifact> --result critic-worker=<path>` — `--items` persists the exact partial `dispatchQueue` used by verdict validation and `complete-round`. Earlier verdicts and completed-round history outside that queue remain unchanged. If a previous version saved the critic votes but left the full queue in state, the ordinary `okstra plan-items complete-round --state <state> --run-manifest <manifest> --round <N>` automatically reads this run's canonical prepared queue. It uses that queue when all votes recorded for this round are included, preserving a wider recorded batch when a later prepared queue excludes its votes. An explicit `--items <prepared artifact>` remains available for selecting an artifact. Enforced by `plan_items_cli._round_inputs` and `tests/run/test_plan_items.py` completion coverage. Do not fabricate new-round votes for already agreed items. Without it the result is checked against the whole persisted round queue and refused for every item the critic was never given (measured 2026-09-10: a 7-item tie round refused against 44 items), and the only way through was `--verdicts`, which the CLI's own help calls a historical envelope. Critic `AGREE` / `SUPPLEMENT` settles the split to `has-dissent`, including an earlier `DISAGREE(a)` or `DISAGREE(f)` on `P-Req-*`. This decision corrects the disputed judgement before single-vote blocking is evaluated; it does not delete the original dissent. Critic `DISAGREE` on a blocking kind is `majority-disagree`. **Enforced:** `validators/validate-run.py` `_validate_unresolved_tie_was_reverified` fails an in-scope item that carries an even split on a blocking kind and has neither a `critic-worker` vote nor a `blocks: approval` clarification row, `_classify_plan_item_gate` scores the tie shape and fails a settled classification the votes do not support, and `okstra_ctl.plan_items.next_dispatch` returns kind `critic-tie` for exactly these items, so the tie round is the queue the CLI hands you rather than one you assemble. **With no critic on the roster the split is a user decision, not another round.** `okstra plan-items next-dispatch --run-manifest <current-run-manifest.json>` answers `user-decision` (not `critic-tie`) when the run's `invocationAssignments` carries no `critic/*` entry, and its `itemIds` are the tie items. For each of them do what step 8 does for a surviving `majority-disagree`: `okstra approval-decision open` with `approvalContext.classification` set to `correctness-critical` when `_is_correctness_critical` is true, or `noncritical-dissent` otherwise, plus the matching `## 1. Clarification Items` row at `Blocks=approval`. Dispatch no further verification for those items. The `Blocks=approval` row is what withholds approval until the user disposes, exactly as for any other approval row; the gate retains unresolved single-vote blockers as `majority-disagree` and folds other `needs-reverify` items into `passed-with-dissent`, so the round closes on the gate it actually scored. A tie left with neither a critic vote nor a decision row surfaces as recorded dissent plus an `advisories[]` entry, not a round-blocking failure. **Enforced:** `okstra_ctl.plan_items.critic_is_rostered` reads the roster and `next_dispatch` returns the kind; `validators/validate-run.py` `_validate_unresolved_tie_was_reverified` reads a `blocks: approval` row linked to the item as the settlement and emits its advisory only when neither settlement is recorded.
|
|
395
407
|
- **A lone dissent nobody cross-verified** — a single-vote-blocking kind fired but the item has **fewer than 2 participating non-error votes**, i.e. the lone dissent was never cross-verified because its peer returned `verification-error`. A single-vote-blocking kind means "one *confirmed* DISAGREE is enough"; an unconfirmed one is not, and on a `P-Var-*` item none fires at all — its kind `a` never blocks on one vote and takes a majority like `b` / `e`. This does **not** block approval — blocking on it would make a worker failure produce a stricter gate than a healthy roster, the same paradox the ≥2-vote majority rule already rules out. The item is re-dispatched in the next round (step 7); if it survives the round budget it is promoted per step 8 with a Statement that says verification never completed. **Enforced:** `validators/validate-run.py` `_classify_plan_item_gate` returns `needs-reverify` for this shape and `_recompute_plan_body_gate` folds it into `passed-with-dissent`.
|
|
396
408
|
- `contested` only meaningful when `maxRounds > 1`; at default `maxRounds=1`, fold any unresolved item into `partial-consensus`.
|
|
397
409
|
5. Gate result resolution:
|
|
@@ -175,7 +175,7 @@ roles:
|
|
|
175
175
|
- `### Carry-In` — for `depends-on (none)`: task-brief only. Otherwise: each depended-on stage's static exit contract + runtime sidecar path `runs/<impl-key>/carry/stage-<i>.json` placeholder.
|
|
176
176
|
- `### Stepwise Execution Order` — bite-sized table with `step | action | files | command | outcome | expected`. `outcome` is one word — `PASS` or `FAIL` — and `expected` is the sentence saying what that looks like here; a verdict written inside the sentence is not read as one. The `files` cell lists each touched path in full and `<PROJECT_ROOT>`-relative — never ellipsis-abbreviated (`…` / `...`), which does not resolve and is rejected by plan-body verification as a kind-b path mismatch. **The narrative row additionally carries `plannedPaths`: the same paths as an array, one repository-relative path per entry, with no globs, exclusions, counts or commentary.** `files` is the sentence a reader sees; `plannedPaths` is the ledger report assembly preserves and the implementer write policy enforces. When a step legitimately covers a set too large to enumerate, split it or name the directory the set lives under. **Effective row count ≤ 8** (excluding header / divider / blank). Each step is one cohesive, self-contained change. **TDD ordering is MUST, not a preference:** the **first** effective step's `action` cell MUST start with the literal `RED:` and describe the failing test(s) that capture this stage's `Acceptance` **and the three declared `Test case (success|boundary|failure)` lines** (`outcome` = `FAIL`); at least one later `action` cell MUST start with the literal `GREEN:` and describe the minimal implementation that makes it pass (`outcome` = `PASS`); an optional refactor step starts with `REFACTOR:`. **Exemption:** doc-only / config-only / pure-rename stages with no observable runtime behaviour may omit RED/GREEN by declaring one line `TDD exemption: <reason>` in the stage section. A stage that is truthfully none of those three declares `TDD exemption: user-bypass — <the user's words>`, which holds only while the user has granted it for that stage with `okstra prepare --tdd-bypass "<stage>:<reason>"` — never file the nearest of the three instead. Validator S10c enforces RED-first + GREEN; the `outcome` cell agreeing with its prefix is a schema conditional. S10e rejects an unsupported exemption reason and a `user-bypass` with no user grant (`validators/validate-implementation-plan-stages.py`).
|
|
177
177
|
- **The `command` cell runs inside an okstra task worktree, not a bare checkout (BLOCKING).** okstra provisions `.okstra`, the configured sync entries (`.project-docs`, `.claude`, …), and — for `implementation` — a nested `stage-<N>/` worktree into the tree the step executes in. Two consequences bind every command you write:
|
|
178
|
-
- **Clean-tree assertions use `okstra worktree-status --check-clean`.** A bare `git status --porcelain` is never empty there, so an assertion built on one fails on okstra's scaffolding rather than on the stage's work. The okstra command asks the same question over source paths only and exits 1 when dirty, so it stands alone as a step's assertion: `okstra worktree-status --check-clean`. Validator S13 rejects the bare form. Do not add a `git tag stage-<N>-exit` to the step
|
|
178
|
+
- **Clean-tree assertions use `okstra worktree-status --check-clean`.** A bare `git status --porcelain` is never empty there, so an assertion built on one fails on okstra's scaffolding rather than on the stage's work. The okstra command asks the same question over source paths only and exits 1 when dirty, so it stands alone as a step's assertion: `okstra worktree-status --check-clean`. Validator S13 rejects the bare form. Do not add a `git tag stage-<N>-exit` to the step. Stage completion records the commit in the consumer ledger without creating or moving git tags.
|
|
179
179
|
- **Never read an `.okstra/` artifact back out of a git object.** `.okstra/**` is gitignored and never committed — the executor aborts a commit that stages an ignored path and the verifier reports a committed `.okstra` path as a branch defect — so `git cat-file -e <tag>:.okstra/…`, `git show <tag>:.okstra/…`, and every variant of that read can never resolve, at any tag, in any stage. A later stage that needs a QA artifact reads it from the working tree or receives it through the carry sidecar / verifier result; do not design a stage contract around one being reachable from a tag. Validator S12 rejects the read.
|
|
180
180
|
- **Per-stage conformance declaration (mandatory one line, in the stage section — same placement freedom as `TDD exemption:`):** the stage MUST carry exactly one of:
|
|
181
181
|
- `Conformance tests: stage-<N> — <task_root>/qa/scripts/stage-<N>.<ext> (requires=[db|io|http|external,...])` — declare that a Tier3 verification script will prove this stage's upstream requirements (brief / requirements-discovery / error-analysis / improvement-discovery → this stage's `Acceptance`) hold against **real** DB rows, real endpoints, or the real external API — NOT mocks. This phase emits the line and the `requires` set only. Do NOT write `<task_root>/qa/scripts/stage-<N>.*` and do NOT add a `runCommand` or `conformance-manifest.json` entry here — the matching `implementation` stage run creates the script file and the manifest `runCommand`. A plan that declares tests with no script file on disk is valid at this gate. The data.json `conformanceTests` value carries only the remainder after the `Conformance tests: stage-<N> — ` prefix — never the `stage-<N> — ` label itself (report assembly strips a leftover label at publication, and the implementation entry gate rejects one).
|
|
@@ -646,6 +646,7 @@
|
|
|
646
646
|
},
|
|
647
647
|
"confirmation": {
|
|
648
648
|
"header": "선택 확인:",
|
|
649
|
+
"provider_data_scope": "\n전달 대상: 위 역할·모델 목록의 제공자(현재 세션 및 선택한 외부 모델 제공자).\n전달 자료: `{project_root}`의 이 작업에 대한 작업 개요, 작업 수행에 필요한 저장소 소스·문서, 선택한 근거 자료 및 실행 중 생성되는 관련 분석·검증 결과.\n진행을 선택하면 위 대상에 해당 자료를 전달하여 선택한 작업을 실행하는 것을 승인합니다. 선택에 없는 제공자나 작업과 무관한 자료는 승인 범위에 포함되지 않습니다. 실행 환경의 권한 검토는 별도로 적용됩니다.",
|
|
649
650
|
"static_role": " static-role : {role}#{ordinal} / {model}",
|
|
650
651
|
"dynamic_role": " dynamic-role : {role} / reuse selected participant model",
|
|
651
652
|
"workers_implementation_default": " workers : (프로필 기본 — executor + verifier 2 + report-writer)",
|
|
@@ -136,11 +136,11 @@ The `confirm` prompt's `label` is the selection summary (one line per resolved i
|
|
|
136
136
|
|
|
137
137
|
### Runtime-generated selectable screens
|
|
138
138
|
|
|
139
|
-
`wizard/engine.py` adapts choice screens before returning `next` when the session declares `native_single_select`.
|
|
139
|
+
`wizard/engine.py` adapts choice screens before returning `next` when the session declares `native_single_select`. Lists use same-screen question tabs only when the host supports the complete grouped selection. Otherwise, choices beyond `nativeLimits` and unsupported multi-selections use a complete numbered list. Codex has no native multi-select, so a single choice with more than three options uses `numbered-single`; multi-selection uses `numbered-multi`. Oversized or unsupported groups are presented one member at a time, with every option for that member visible. These paths are exercised by `tests/domain/wizard/test_picker_navigation.py` and `test_role_model_selection.py`.
|
|
140
140
|
|
|
141
|
-
Render
|
|
141
|
+
Render the returned screen using its `interaction.kind`. For numbered interactions, show every option label and description in original order in one message, then accept a number, label, or value; multiple choices accept comma-separated input. Submit that reply unchanged through `okstra wizard step`. Do not add a next-page choice, truncate the list, or require the user to remember a model identifier.
|
|
142
142
|
|
|
143
|
-
|
|
143
|
+
The complete numbered list is the intended fallback when a choice cannot fit the native control or same-screen tabs, including when the user generally prefers a selector. Text steps remain text steps; selecting the direct-input option opens the wizard's custom-value step.
|
|
144
144
|
|
|
145
145
|
### Plan decisions and execution permissions
|
|
146
146
|
|
|
@@ -168,7 +168,7 @@ Display the question only through the selected tool. Do not print it or its opti
|
|
|
168
168
|
|---|---|
|
|
169
169
|
| `read_artifacts` | Read the manifest-provided paths through the current host's file interface. |
|
|
170
170
|
| `write_artifact` | Write only core-authorized `.okstra/` artifacts and preserve their schemas. |
|
|
171
|
-
| `prompt_user` | Use the client-appropriate, mode-available question tool selected above for clarifications that fit `nativeLimits`. Show the question once through that tool and wait for the actual answer. Emit the question as the last thing in that turn: assistant text emitted after the call renders below the question and separates it from the user's answer. Follow the host's separate restrictions for permission requests.
|
|
171
|
+
| `prompt_user` | Use the client-appropriate, mode-available question tool selected above for clarifications that fit `nativeLimits`. Show the question once through that tool and wait for the actual answer. Emit the question as the last thing in that turn: assistant text emitted after the call renders below the question and separates it from the user's answer. Follow the host's separate restrictions for permission requests. For lists that cannot fit the native control or same-screen tabs, show the complete numbered list and accept the next message as the answer. |
|
|
172
172
|
| `dispatch_worker` | Verify each materialized invocation first. Dispatch `runner=native-session` with the current Codex host's primitive, the returned `promptPath`, and `hostModelValue`. Pass `runner=cli-wrapper` assignments to `okstra worker-dispatch --project-root <root> --run-manifest <path> --workers <ids>`; use `--dry-run` first when required. **Not in a cmux run:** when `terminalBackend` is `cmux-pane`, the cmux adapter overrides this row. |
|
|
173
173
|
| `await_workers` | Await native host workers through the host primitive and CLI workers through synchronous dispatch, then verify team-state terminal records and Result Paths for both. |
|
|
174
174
|
| `redispatch_worker` | Materialize and verify a fresh invocation, then start a fresh native worker or `okstra worker-dispatch` attempt according to the persisted runner. |
|
|
@@ -176,12 +176,26 @@ Display the question only through the selected tool. Do not print it or its opti
|
|
|
176
176
|
| `record_lead_event` | Append progress and activity records to the manifest-provided `leadEventsPath`. Use `okstra lead-progress append --phase <phase-id>` for a checkpoint and `okstra agent-activity append --kind <kind>` for an activity record; both resolve the ledger path from the run manifest. Emit the matching `PROGRESS:` line — the command prints it as `progressLine` — and, when an activity record is required, the immediately following `ACTIVITY:` line from the same structured fields. |
|
|
177
177
|
| `collect_usage` | Collect artifact/rollout-backed usage through the existing Okstra token-usage path; never read Claude session JSONL as a substitute. |
|
|
178
178
|
|
|
179
|
+
## Codex execution permissions
|
|
180
|
+
|
|
181
|
+
### Permission before bundle preparation
|
|
182
|
+
|
|
183
|
+
Before the first `okstra render-bundle` invocation, check the current host's declared filesystem restrictions and approval policy. Bundle preparation writes run artifacts and can create a task or stage worktree with `git worktree add -b`, which writes to the source repository's Git metadata. `--render-only` does not make this command read-only, and writable access to the project directory or `~/.okstra` does not imply writable access to protected `.git` metadata.
|
|
184
|
+
|
|
185
|
+
When those writes are restricted, use the host's supported execution permission on the original `render-bundle` command (`sandbox_permissions: "require_escalated"` for `exec_command` when available and permitted), preserving every `outcome.renderArgv` token. Explain that the command prepares the selected run and may create its branch and worktree. Do not first run it under restrictions already known to block these writes. Apply the same check to subsequent phase or stage bundle preparations. A wizard confirmation does not grant host privileges; follow the host's approval decision. If escalation is prohibited, report the constraint and retain the invocation instead of changing filesystem permissions or choosing an alternate launcher.
|
|
186
|
+
|
|
187
|
+
If preparation has already failed with a Git lock creation error or `Operation not permitted`, preserve the exact error and retry the same command only after the host permits the required writes. This is host-call guidance, not a permission grant or a runtime-enforced check.
|
|
188
|
+
|
|
179
189
|
## Codex dispatch details
|
|
180
190
|
|
|
181
191
|
### Permission at the dispatch boundary
|
|
182
192
|
|
|
183
193
|
Before a live CLI worker dispatch, check the current host's declared sandbox and approval policy. In a restricted Codex session, request the host's supported execution permission on the dispatch command itself (`sandbox_permissions: "require_escalated"` for `exec_command` when that mechanism is available and permitted). Scope the request to the prepared run and explain that it starts the selected worker processes. Follow the host's approval decision; a wizard confirmation or a successful preflight does not grant this permission.
|
|
184
194
|
|
|
195
|
+
Carry the user's existing authorization into that request: identify the task, the selected providers/models from the prepared assignments, and the task brief, relevant repository source/documents, selected evidence, and related run results those workers will process. Cite the actual `Proceed` response and the data-transfer scope displayed in that confirmation; `outcome.confirmationText` is the runtime's summary reference, not proof by itself that the user saw or accepted it. Explain this scope in the execution tool's justification rather than describing only process startup. Data-transfer authorization and host execution privileges are separate: preserve the former while requesting the latter.
|
|
196
|
+
|
|
197
|
+
Do not infer that an older confirmation included the new disclosure, widen the approved recipients or material, or treat model selection alone as blanket data-transfer consent. When the conversation already authorizes the same recipients and scope, use that evidence without asking again. If approval review rejects a dispatch, retain and report its reason; supply omitted existing evidence only through the host's permitted review mechanism. If the stated gap is not covered by the user's actual authorization, ask one focused question naming that gap and wait for the answer. Never bypass the rejection or repeat an unchanged request.
|
|
198
|
+
|
|
185
199
|
Apply this guidance to both `okstra worker-dispatch` and the cmux override's `okstra team dispatch`, including initial workers, reverify, critic, report-writer, and retries. Read-only previews do not start workers. A session already authorized to execute outside the sandbox does not need another request. When the host prohibits escalation, preserve the pending invocation and report the execution constraint instead of trying an alternate launcher or weakening host controls.
|
|
186
200
|
|
|
187
201
|
The worker's `--sandbox danger-full-access` flag only selects the child Codex policy; it does not remove restrictions inherited from the parent process. If startup reports `Operation not permitted`, retain the exact error and distinguish initialization failure from a worker verdict. Retry only through the permitted host mechanism after the execution conditions change, within the existing retry limit; do not repeat the same restricted invocation. This is host-call guidance, not a permission grant or a runtime-enforced check.
|
|
@@ -25,6 +25,7 @@ from ..activity import (
|
|
|
25
25
|
from ...final_report_schema import load_schema_version
|
|
26
26
|
from ...implementation_options import validate_implementation_option_selection
|
|
27
27
|
from ...report_contract import TASK_TYPE_DATA_PROPERTY
|
|
28
|
+
from ...report_assembly import validate_plan_draft
|
|
28
29
|
from ...report_corrections import (
|
|
29
30
|
CorrectionsCheck,
|
|
30
31
|
check_corrections,
|
|
@@ -100,9 +101,16 @@ def run_corrections_check(
|
|
|
100
101
|
|
|
101
102
|
def semantic_validator(data: dict[str, Any]) -> list[str]:
|
|
102
103
|
block = data.get(block_key)
|
|
103
|
-
return
|
|
104
|
-
|
|
105
|
-
|
|
104
|
+
return [
|
|
105
|
+
f"{block_key}: {error}"
|
|
106
|
+
for error in validate_implementation_option_selection(
|
|
107
|
+
block if isinstance(block, Mapping) else {}, original_ids, analysers,
|
|
108
|
+
)
|
|
109
|
+
]
|
|
110
|
+
elif task_type == "implementation-planning":
|
|
111
|
+
|
|
112
|
+
def semantic_validator(data: dict[str, Any]) -> list[str]:
|
|
113
|
+
return validate_plan_draft(data, project_root, manifest)
|
|
106
114
|
|
|
107
115
|
return check_corrections(
|
|
108
116
|
ledger=ledger,
|
|
@@ -34,7 +34,11 @@ from ...assignment_resolver import AssignmentContext, resolve_dispatch_assignmen
|
|
|
34
34
|
from ...path_hints import hydrate_active_run_context
|
|
35
35
|
from ...worker_prompt_headers import worker_prompt_headers
|
|
36
36
|
from ...worker_prompt_contract import complete_reverify_instruction, validate_reverify_prompt
|
|
37
|
-
from ...worker_prompt_policy import
|
|
37
|
+
from ...worker_prompt_policy import (
|
|
38
|
+
critic_assignment_ref,
|
|
39
|
+
is_plan_critic_verification,
|
|
40
|
+
is_verification_dispatch_kind,
|
|
41
|
+
)
|
|
38
42
|
from ...paths import okstra_home
|
|
39
43
|
from ...final_report_paths import final_report_data_path
|
|
40
44
|
from ...final_report_schema import load_schema_version
|
|
@@ -251,10 +255,28 @@ def _materialize_run(
|
|
|
251
255
|
if args.assignment_ref not in assignments:
|
|
252
256
|
# 유효값은 이 매니페스트 안에만 있고 리드는 그것을 추측할 수 없다.
|
|
253
257
|
# 이름만 거절하면 읽는 쪽이 매니페스트를 직접 열어 키를 세게 된다.
|
|
258
|
+
critic_ref = critic_assignment_ref(str(manifest.get("taskType", "")))
|
|
259
|
+
recovery = ""
|
|
260
|
+
if (
|
|
261
|
+
args.assignment_ref == "reverify/critic-worker"
|
|
262
|
+
and critic_ref in assignments
|
|
263
|
+
and is_plan_critic_verification(
|
|
264
|
+
task_type=str(manifest.get("taskType", "")),
|
|
265
|
+
assignment_ref=critic_ref, dispatch_kind=args.dispatch_kind,
|
|
266
|
+
)
|
|
267
|
+
):
|
|
268
|
+
recovery = (
|
|
269
|
+
f"; for the rostered plan critic, use --assignment-ref {critic_ref} "
|
|
270
|
+
"with --worker-id critic-worker and --audience reverification-worker. "
|
|
271
|
+
"Keep the plan-verify-r<N> dispatch kind and, on v2 runs, the selected "
|
|
272
|
+
"critic's --source-role-execution-ref. This is a reference correction; "
|
|
273
|
+
"the existing critic assignment does not need to be added or changed"
|
|
274
|
+
)
|
|
254
275
|
raise AgentPromptCliError(
|
|
255
276
|
f"assignment reference is missing from run manifest: "
|
|
256
277
|
f"{args.assignment_ref}; this run declares "
|
|
257
278
|
+ (", ".join(sorted(assignments)) or "no invocation assignment")
|
|
279
|
+
+ recovery
|
|
258
280
|
)
|
|
259
281
|
assignment_payload = assignments[args.assignment_ref]
|
|
260
282
|
assignment = agent_model_assignment_from_payload(assignment_payload)
|
|
@@ -10,8 +10,6 @@ from __future__ import annotations
|
|
|
10
10
|
|
|
11
11
|
import json
|
|
12
12
|
import re
|
|
13
|
-
import subprocess
|
|
14
|
-
import sys
|
|
15
13
|
from dataclasses import dataclass
|
|
16
14
|
from pathlib import Path
|
|
17
15
|
from typing import Any, Dict, List, Optional
|
|
@@ -149,8 +147,6 @@ def append_consumer(plan_run_root: Path, *, impl_task_key: str, stage: int,
|
|
|
149
147
|
**fields,
|
|
150
148
|
}
|
|
151
149
|
_append_row(plan_run_root, record)
|
|
152
|
-
if status == "done":
|
|
153
|
-
_tag_stage_exit(plan_run_root, stage, fields.get("head_commit"))
|
|
154
150
|
# 종결 status 는 점유 해제 이벤트이기도 하다 — 중복 append(no-op)에서도 풀어야
|
|
155
151
|
# release 없이 done 만 기록된 과거 run 의 잔존 점유가 다음 호출에서 치유된다.
|
|
156
152
|
if status == "done":
|
|
@@ -159,57 +155,6 @@ def append_consumer(plan_run_root: Path, *, impl_task_key: str, stage: int,
|
|
|
159
155
|
_release_stage_occupancy_keeping_branch(impl_task_key, stage)
|
|
160
156
|
|
|
161
157
|
|
|
162
|
-
STAGE_EXIT_TAG = "stage-{stage}-exit"
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
def _repo_root(start: Path) -> Optional[Path]:
|
|
166
|
-
for candidate in [start, *start.parents]:
|
|
167
|
-
if (candidate / ".git").exists():
|
|
168
|
-
return candidate
|
|
169
|
-
return None
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
def _tag_stage_exit(plan_run_root: Path, stage: int, head_commit: Any) -> None:
|
|
173
|
-
"""Move `stage-<N>-exit` to the commit this ledger row records.
|
|
174
|
-
|
|
175
|
-
The tag used to be a plan step, so it was written while the stage was still
|
|
176
|
-
running — before the carry evidence the ledger reads. A stage then had three
|
|
177
|
-
exit points that could disagree: the tag, the recorded `head_commit`, and
|
|
178
|
-
the branch tip. okstra writes the tag at the moment it settles the stage, so
|
|
179
|
-
the tag and the ledger cannot drift apart.
|
|
180
|
-
|
|
181
|
-
Best effort by design: the ledger is the record and a tag is a convenience
|
|
182
|
-
for the reader. A tree that is not a git repository, or a commit git cannot
|
|
183
|
-
resolve, leaves the row written and says so on stderr.
|
|
184
|
-
"""
|
|
185
|
-
if not isinstance(head_commit, str) or not head_commit.strip():
|
|
186
|
-
return
|
|
187
|
-
root = _repo_root(Path(plan_run_root).resolve())
|
|
188
|
-
if root is None:
|
|
189
|
-
return
|
|
190
|
-
commit = head_commit.strip()
|
|
191
|
-
tag = STAGE_EXIT_TAG.format(stage=stage)
|
|
192
|
-
try:
|
|
193
|
-
exists = subprocess.run(
|
|
194
|
-
["git", "-C", str(root), "cat-file", "-e", f"{commit}^{{commit}}"],
|
|
195
|
-
capture_output=True,
|
|
196
|
-
)
|
|
197
|
-
if exists.returncode != 0:
|
|
198
|
-
print(
|
|
199
|
-
f"okstra: stage {stage} done recorded, but {commit[:12]} is not "
|
|
200
|
-
f"a commit in {root} — `{tag}` not moved",
|
|
201
|
-
file=sys.stderr,
|
|
202
|
-
)
|
|
203
|
-
return
|
|
204
|
-
subprocess.run(
|
|
205
|
-
["git", "-C", str(root), "tag", "-f", tag, commit],
|
|
206
|
-
capture_output=True,
|
|
207
|
-
check=True,
|
|
208
|
-
)
|
|
209
|
-
except (OSError, subprocess.CalledProcessError) as exc:
|
|
210
|
-
print(f"okstra: could not move `{tag}` to {commit[:12]}: {exc}", file=sys.stderr)
|
|
211
|
-
|
|
212
|
-
|
|
213
158
|
def _equivalent_row_exists(plan_run_root: Path, impl_task_key: str, stage: int,
|
|
214
159
|
status: str, force_reappend: bool,
|
|
215
160
|
head_commit: Any) -> bool:
|
|
@@ -131,8 +131,14 @@ def validate_write_policy_payload(payload: Mapping[str, Any]) -> None:
|
|
|
131
131
|
_relative_paths(artifact.get("allowedPaths", ()))
|
|
132
132
|
if source.get("mode") not in {"source-readonly", "project-mutation"}:
|
|
133
133
|
raise WritePolicyError("sourcePolicy mode is invalid")
|
|
134
|
-
if set(source)
|
|
134
|
+
if set(source) - {"plannedPathsDeclared"} != {
|
|
135
|
+
"mode", "allowedRoot", "plannedPaths", "protectedPaths"
|
|
136
|
+
}:
|
|
135
137
|
raise WritePolicyError("sourcePolicy is incomplete")
|
|
138
|
+
if "plannedPathsDeclared" in source and not isinstance(
|
|
139
|
+
source["plannedPathsDeclared"], bool
|
|
140
|
+
):
|
|
141
|
+
raise WritePolicyError("sourcePolicy.plannedPathsDeclared must be a boolean")
|
|
136
142
|
_absolute_text(source.get("allowedRoot"), "sourcePolicy.allowedRoot")
|
|
137
143
|
_relative_paths(source.get("plannedPaths", ()))
|
|
138
144
|
_relative_paths(source.get("protectedPaths", ()))
|
|
@@ -793,28 +793,17 @@ def _stable_git_projection(snapshot: MutationSnapshot) -> dict[str, Any]:
|
|
|
793
793
|
|
|
794
794
|
|
|
795
795
|
def _path_ledger_is_unenforceable(policy: WritePolicy) -> bool:
|
|
796
|
-
"""
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
없어 장부가 빈 채로 온다 — 종전에는 산문에서 유도한 문장 조각(`28 rows)`,
|
|
802
|
-
`captured in Stage 1)`)을 실었고, 그래서 계획이 지시한 파일 전부가 미허가
|
|
803
|
-
변경으로 읽혔다.
|
|
804
|
-
|
|
805
|
-
`project-mutation` 정책에서만 빈 장부가 "물을 수 없음" 을 뜻한다.
|
|
806
|
-
`source-readonly` 워커는 장부가 원래 비어 있고 그것이 "아무것도 바꾸지
|
|
807
|
-
말라" 는 뜻이므로, 그쪽 집행은 건드리지 않는다.
|
|
808
|
-
|
|
809
|
-
이 판정은 감사의 두 절반이 같은 함수를 읽는다. 종전에는 git 쪽만
|
|
810
|
-
`plannedPathsDeclared` 를 봤는데 그 키는 `build_write_policy` 가 만드는
|
|
811
|
-
sourcePolicy 에 아예 실리지 않아(4개 키 고정, `validate_write_policy_payload`
|
|
812
|
-
가 그 집합을 강제) 어느 쪽에서도 참이 된 적이 없다.
|
|
796
|
+
"""옛 계획의 미선언 목록만 면제하고, 산출물 전용 계획의 빈 소스 목록은 집행한다.
|
|
797
|
+
|
|
798
|
+
선언 여부가 저장되기 전 정책은 기존의 빈 목록 해석을 유지한다.
|
|
799
|
+
새 정책은 경로 분류 후에도 선언 여부를 보존하므로 산출물만 남은 계획이
|
|
800
|
+
임의의 소스 변경이나 커밋을 허용하지 않는다.
|
|
813
801
|
"""
|
|
814
802
|
source = policy.source_policy
|
|
815
803
|
return (
|
|
816
804
|
source.get("mode") == "project-mutation"
|
|
817
805
|
and not source.get("plannedPaths")
|
|
806
|
+
and not source.get("plannedPathsDeclared", False)
|
|
818
807
|
)
|
|
819
808
|
|
|
820
809
|
|
|
@@ -211,26 +211,17 @@ class _Validator:
|
|
|
211
211
|
def _misplaced_property_hint(
|
|
212
212
|
self, name: str, path: tuple[str | int, ...],
|
|
213
213
|
) -> str:
|
|
214
|
-
"""
|
|
215
|
-
|
|
216
|
-
`additional property 'X' is not allowed` 만 보면 "스키마에 그런 칸이
|
|
217
|
-
없다" 로 읽힌다. 실제로는 X 가 다른 객체의 필드이고 작성자가 자리를
|
|
218
|
-
틀린 경우가 있고, 그때 지우면 다음 라운드에서 `required property 'X'
|
|
219
|
-
is missing` 이 나온다 — 라운드 하나를 왕복에 쓴다(2026-09-09 실측,
|
|
220
|
-
dev-10642 implementation-planning: 최상위 필수 `summary` 를
|
|
221
|
-
`rationale` 안에 써서 지웠다가 되살렸다).
|
|
222
|
-
"""
|
|
214
|
+
"""동명 필수 필드의 위치만 알린다. 이름 일치는 값의 의미를 증명하지 않는다."""
|
|
223
215
|
found = self._required_property_owners(name) - {_format_path(path)}
|
|
224
216
|
if not found:
|
|
225
217
|
return ""
|
|
226
|
-
# 최상위를 앞에 둔다. 잘못 놓인 키는 대개 최상위 필드가 한 단계 안으로
|
|
227
|
-
# 들어간 것이고, 나머지 소유자는 같은 이름을 쓰는 다른 행 타입이다.
|
|
228
218
|
owners = (
|
|
229
219
|
["<root>"] if "<root>" in found else []
|
|
230
220
|
) + sorted(found - {"<root>"})
|
|
231
221
|
return (
|
|
232
|
-
f" — '{name}' is
|
|
233
|
-
|
|
222
|
+
f" — '{name}' is also required at {', '.join(owners[:3])}; "
|
|
223
|
+
"a shared name does not establish where this value belongs. "
|
|
224
|
+
"Check the target field's schema and meaning before moving any value"
|
|
234
225
|
)
|
|
235
226
|
|
|
236
227
|
def _required_property_owners(self, name: str) -> set[str]:
|
|
@@ -495,14 +486,14 @@ def verdict_token_rule(schema: Mapping[str, Any], task_type: str) -> tuple[str,
|
|
|
495
486
|
|
|
496
487
|
|
|
497
488
|
def task_block_rules(schema: Mapping[str, Any], block_key: str) -> tuple[str, ...]:
|
|
498
|
-
"""
|
|
489
|
+
"""최상위 필드(`properties[block_key]`)가 요구하는 모양을 한
|
|
499
490
|
객체당 한 줄의 저작 계약 문장으로 편다.
|
|
500
491
|
|
|
501
492
|
리드는 report-writer 지시문을 쓸 때 블록 안쪽의 필수 필드·식별자 패턴·
|
|
502
493
|
정수 필드·고정 길이 배열을 알 길이 프롬프트 경로에 없어 스키마 JSON 을
|
|
503
494
|
손으로 파싱했고, 그래도 놓친 만큼 같은 리포트를 다시 썼다(2026-09-03 실측,
|
|
504
495
|
dev-10626 implementation-option-selection: 네 회차). 여기서 뽑아 합성 묶음의
|
|
505
|
-
저작 계약에 싣는다.
|
|
496
|
+
저작 계약에 싣는다. 필드가 없으면 빈 튜플이다.
|
|
506
497
|
|
|
507
498
|
경로는 스키마 키다(`rankedOptions[]` 는 `- Item N` 목록의 각 항목). 같은
|
|
508
499
|
정의(`$ref`)가 두 경로에 나오면 두 번째는 첫 경로를 가리킨다 — 랭킹 옵션과
|
|
@@ -514,7 +505,16 @@ def task_block_rules(schema: Mapping[str, Any], block_key: str) -> tuple[str, ..
|
|
|
514
505
|
return ()
|
|
515
506
|
lines: list[str] = []
|
|
516
507
|
described: dict[str, str] = {}
|
|
517
|
-
|
|
508
|
+
resolved, _ = _resolved(root, block)
|
|
509
|
+
if resolved.get("type") == "object" or "properties" in resolved:
|
|
510
|
+
_describe_object(root, block, block_key, lines, described)
|
|
511
|
+
else:
|
|
512
|
+
nested: list[tuple[str, Any]] = []
|
|
513
|
+
constraint = _describe_property(root, block, block_key, nested)
|
|
514
|
+
if constraint:
|
|
515
|
+
lines.append(f"`{block_key}`: {constraint}")
|
|
516
|
+
for child_path, child in nested:
|
|
517
|
+
_describe_object(root, child, child_path, lines, described)
|
|
518
518
|
return tuple(lines)
|
|
519
519
|
|
|
520
520
|
|
|
@@ -292,7 +292,8 @@ def _parser() -> argparse.ArgumentParser:
|
|
|
292
292
|
complete.add_argument(
|
|
293
293
|
"--items", type=Path,
|
|
294
294
|
help="restore this round's dispatched queue from its prepared items artifact; "
|
|
295
|
-
"
|
|
295
|
+
"defaults to this run's canonical prepared queue when it covers the round's "
|
|
296
|
+
"recorded votes; earlier verdicts outside that queue remain unchanged",
|
|
296
297
|
)
|
|
297
298
|
complete.add_argument("--self-fix-note", action="append", default=[], metavar="<item-id>=<markdown-file>")
|
|
298
299
|
complete.add_argument(
|
|
@@ -419,7 +420,7 @@ def _prepare(args: argparse.Namespace) -> dict[str, Any]:
|
|
|
419
420
|
failures = validate_plan_draft(source, authority.project_root, authority.payload)
|
|
420
421
|
if failures:
|
|
421
422
|
raise PlanItemContractError(
|
|
422
|
-
"owner=report-writer
|
|
423
|
+
"owner=report-writer: "
|
|
423
424
|
+ "; ".join(failures)
|
|
424
425
|
+ "; correct the narrative in this run, then retry plan-items prepare"
|
|
425
426
|
)
|
|
@@ -1726,6 +1727,17 @@ def _round_inputs(args: argparse.Namespace) -> tuple[dict[str, Any], list[dict[s
|
|
|
1726
1727
|
if not isinstance(audit, list) or not isinstance(history, list):
|
|
1727
1728
|
raise PlanItemContractError("state planItems and roundHistory must be arrays")
|
|
1728
1729
|
current = _state_plan_body_items(data, args.state)
|
|
1730
|
+
if args.command == "complete-round" and args.items is None:
|
|
1731
|
+
manifest = _load_json_object(args.run_manifest)
|
|
1732
|
+
if manifest.get("planBodyVerificationPath"):
|
|
1733
|
+
prepared = _prepared_items_path(args.run_manifest)
|
|
1734
|
+
if prepared.is_file():
|
|
1735
|
+
voted = {item["id"] for item in current
|
|
1736
|
+
if _round_snapshot(item, args.round_number)["votes"]}
|
|
1737
|
+
# 이후 회차용 준비 파일이 이미 저장된 현재 회차의 표를 제외하면 쓰지 않는다.
|
|
1738
|
+
if voted <= set(_assigned_item_ids(prepared)):
|
|
1739
|
+
args = copy.copy(args)
|
|
1740
|
+
args.items = prepared
|
|
1729
1741
|
_narrow_dispatch_queue(
|
|
1730
1742
|
args, data["planBodyVerification"], {item.get("id") for item in current},
|
|
1731
1743
|
)
|
|
@@ -1742,8 +1754,15 @@ def _round_snapshots(current: list[dict[str, Any]], verification: Mapping[str, A
|
|
|
1742
1754
|
if isinstance(item, Mapping) and item.get("id") in allowed
|
|
1743
1755
|
]
|
|
1744
1756
|
snapshots = {str(item.get("id")): _round_snapshot(item, round_number) for item in scoped}
|
|
1745
|
-
if not snapshots
|
|
1746
|
-
raise PlanItemContractError("
|
|
1757
|
+
if not snapshots:
|
|
1758
|
+
raise PlanItemContractError(f"round {round_number} dispatch queue has no current plan items")
|
|
1759
|
+
missing = [item_id for item_id, row in snapshots.items() if not row["votes"]]
|
|
1760
|
+
if missing:
|
|
1761
|
+
raise PlanItemContractError(
|
|
1762
|
+
f"round {round_number} has no verdict for dispatched plan items: {', '.join(missing)}. "
|
|
1763
|
+
"Earlier-round verdicts remain recorded; check this round's prepared queue "
|
|
1764
|
+
"before requesting new verdicts."
|
|
1765
|
+
)
|
|
1747
1766
|
summary = _plan_gate_summary(verification)
|
|
1748
1767
|
classes = {row["id"]: row["stateClassification"] for row in summary["items"]}
|
|
1749
1768
|
for item_id, snapshot in snapshots.items():
|
|
@@ -1916,6 +1935,12 @@ def _state_next_dispatch(args: argparse.Namespace) -> NextDispatch:
|
|
|
1916
1935
|
data, current, _audit, _history = _round_inputs(args)
|
|
1917
1936
|
run_manifest = getattr(args, "run_manifest", None)
|
|
1918
1937
|
payloads = _prepared_payloads(run_manifest)
|
|
1938
|
+
# 실제 목록에서 제외할 항목을 다음 비평 검증으로 요구하면 빈 큐에서 멈춘다.
|
|
1939
|
+
allowed = set(dispatch_item_ids(
|
|
1940
|
+
[payloads.get(item["id"], item) for item in current],
|
|
1941
|
+
data["planBodyVerification"].get("stageLedger"),
|
|
1942
|
+
))
|
|
1943
|
+
current = [item for item in current if item["id"] in allowed]
|
|
1919
1944
|
return next_dispatch(
|
|
1920
1945
|
current, payloads, critic_rostered=_rostered_critic(run_manifest),
|
|
1921
1946
|
decision_items=(
|
|
@@ -18,7 +18,7 @@ from .clarification_items import (
|
|
|
18
18
|
from .final_report_schema import load_schema_version, validate
|
|
19
19
|
from .report_inputs import ReportInputPath, report_input_paths, uses_report_contract_v3
|
|
20
20
|
from .json_boundary import JsonBoundaryError, load_owned_object, serialize_owned_object
|
|
21
|
-
from .report_narrative import parse_narrative
|
|
21
|
+
from .report_narrative import parse_narrative, task_narrative_errors
|
|
22
22
|
from .report_synthesis_packet import (
|
|
23
23
|
report_synthesis_packet_paths,
|
|
24
24
|
verify_report_synthesis_packet_sources,
|
|
@@ -112,23 +112,12 @@ def _input_preflight_issues(
|
|
|
112
112
|
try:
|
|
113
113
|
if row.key == "narrative":
|
|
114
114
|
narrative = parse_narrative(row.path.read_text(encoding="utf-8"), schema)
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
if verdict is None:
|
|
119
|
-
continue
|
|
120
|
-
errors = validate(
|
|
121
|
-
{**narrative, "header": {"taskType": task_type}},
|
|
122
|
-
{
|
|
123
|
-
"$defs": schema.get("$defs", {}),
|
|
124
|
-
"if": branch["if"],
|
|
125
|
-
"then": {"properties": {"finalVerdict": verdict}},
|
|
126
|
-
},
|
|
127
|
-
)
|
|
128
|
-
issues.extend(
|
|
129
|
-
AssemblyIssue(row.owner, str(row.path), "finalVerdict", error)
|
|
130
|
-
for error in errors
|
|
115
|
+
issues.extend(
|
|
116
|
+
AssemblyIssue(
|
|
117
|
+
row.owner, str(row.path), error.split(":", 1)[0], error,
|
|
131
118
|
)
|
|
119
|
+
for error in task_narrative_errors(narrative, schema, task_type)
|
|
120
|
+
)
|
|
132
121
|
elif row.key == "agent-activity":
|
|
133
122
|
row.path.read_text(encoding="utf-8")
|
|
134
123
|
else:
|
|
@@ -601,7 +590,15 @@ def validate_plan_draft(
|
|
|
601
590
|
draft = copy.deepcopy(dict(data))
|
|
602
591
|
_apply_selected_direction_snapshot(draft, project_root, manifest)
|
|
603
592
|
_attach_metadata(draft, manifest)
|
|
604
|
-
return
|
|
593
|
+
return [
|
|
594
|
+
*task_narrative_errors(
|
|
595
|
+
draft, load_schema_version("3.0"), str(manifest.get("taskType", "")),
|
|
596
|
+
),
|
|
597
|
+
*[
|
|
598
|
+
f"implementationPlanning: {error}"
|
|
599
|
+
for error in selected_direction_plan_errors(draft, project_root, manifest)
|
|
600
|
+
],
|
|
601
|
+
]
|
|
605
602
|
|
|
606
603
|
|
|
607
604
|
def _identity(manifest: Mapping[str, Any]) -> tuple[str, str, str]:
|
|
@@ -20,7 +20,7 @@ from dataclasses import dataclass
|
|
|
20
20
|
from pathlib import Path
|
|
21
21
|
from typing import Any, Callable, Mapping, Sequence
|
|
22
22
|
|
|
23
|
-
from .final_report_schema import validate
|
|
23
|
+
from .final_report_schema import task_block_rules, validate
|
|
24
24
|
from .json_boundary import JsonBoundaryError, load_owned_object
|
|
25
25
|
from .report_markdown import humanise
|
|
26
26
|
from .report_narrative import (
|
|
@@ -29,6 +29,7 @@ from .report_narrative import (
|
|
|
29
29
|
render_narrative,
|
|
30
30
|
validate_writer_owned,
|
|
31
31
|
writer_owned_path_defect,
|
|
32
|
+
writer_owned_schema,
|
|
32
33
|
)
|
|
33
34
|
|
|
34
35
|
|
|
@@ -204,7 +205,7 @@ def check_corrections(
|
|
|
204
205
|
3. 경로 해소와 `current` 일치. `rewrite` 는 배열의 `[len]` 추가 위치를 허용한다.
|
|
205
206
|
4. 작성자 소유 경로인가.
|
|
206
207
|
5. replace·remove 를 적용한 사본을 작성자 소유 값 스키마와 task 의미 검증기로
|
|
207
|
-
대조한다. rewrite 경로
|
|
208
|
+
대조한다. rewrite 경로 아래로 특정된 결함은 뺀다(작성자가 다시 쓸 자리다).
|
|
208
209
|
6. rewrite 마다 그 경로의 스키마 제약 문장을 붙인다.
|
|
209
210
|
"""
|
|
210
211
|
ledger_dict = dict(ledger)
|
|
@@ -279,7 +280,7 @@ def check_corrections(
|
|
|
279
280
|
continue
|
|
280
281
|
if kind == "rewrite":
|
|
281
282
|
rewrite_paths.append(path)
|
|
282
|
-
constraints[correction_id] = _constraints_for(path, block_rules)
|
|
283
|
+
constraints[correction_id] = _constraints_for(path, block_rules, schema)
|
|
283
284
|
|
|
284
285
|
replacements = [c for c in corrections if c.get("kind") == "replace" and str(c.get("id")) in parsed]
|
|
285
286
|
removals = [c for c in corrections if c.get("kind") == "remove" and str(c.get("id")) in parsed]
|
|
@@ -305,15 +306,16 @@ def check_corrections(
|
|
|
305
306
|
# 회차에 전건을 보게 하려는 것이 이 모듈의 이유다. 결함인 교정은 적용하지
|
|
306
307
|
# 않았으므로 그 자리는 기준 서사의 값 그대로다.
|
|
307
308
|
if scratch is not None:
|
|
308
|
-
|
|
309
|
-
|
|
309
|
+
errors = validate_writer_owned(scratch, schema)
|
|
310
|
+
if semantic_validator is not None:
|
|
311
|
+
errors.extend(semantic_validator(scratch))
|
|
312
|
+
for error in errors:
|
|
313
|
+
location, separator, _ = error.partition(": ")
|
|
314
|
+
location = location if separator else "-"
|
|
310
315
|
if any(location == rp or location.startswith(f"{rp}.") or location.startswith(f"{rp}[")
|
|
311
316
|
for rp in rewrite_paths):
|
|
312
317
|
continue
|
|
313
318
|
defects.append(_defect("applied", location, error))
|
|
314
|
-
if semantic_validator is not None:
|
|
315
|
-
for error in semantic_validator(scratch):
|
|
316
|
-
defects.append(_defect("applied", "-", str(error)))
|
|
317
319
|
|
|
318
320
|
mechanical = not defects and not rewrite_paths
|
|
319
321
|
return CorrectionsCheck(
|
|
@@ -321,16 +323,22 @@ def check_corrections(
|
|
|
321
323
|
)
|
|
322
324
|
|
|
323
325
|
|
|
324
|
-
def _constraints_for(
|
|
325
|
-
|
|
326
|
+
def _constraints_for(
|
|
327
|
+
path: str, block_rules: Sequence[str], schema: Mapping[str, Any],
|
|
328
|
+
) -> tuple[str, ...]:
|
|
329
|
+
"""가장 가까운 필드의 제약. 공통 필드도 검증에 쓰는 스키마에서 추출한다."""
|
|
326
330
|
normalised = normalise_field_path(path)
|
|
331
|
+
root_key = str(parse_field_path(path)[0])
|
|
332
|
+
rules = block_rules
|
|
333
|
+
if not any(rule.startswith(f"`{root_key}`: ") for rule in rules):
|
|
334
|
+
rules = task_block_rules(writer_owned_schema(schema), root_key)
|
|
327
335
|
candidates = [normalised]
|
|
328
336
|
while "." in candidates[-1]:
|
|
329
337
|
candidates.append(candidates[-1].rsplit(".", 1)[0])
|
|
330
338
|
for candidate in candidates:
|
|
331
339
|
lines = tuple(
|
|
332
|
-
rule for rule in
|
|
333
|
-
if rule.startswith(f"`{candidate}`: ")
|
|
340
|
+
rule for rule in rules
|
|
341
|
+
if rule.startswith(f"`{candidate}`: ") or rule.startswith(f"`{candidate}[]")
|
|
334
342
|
)
|
|
335
343
|
if lines:
|
|
336
344
|
return lines
|
|
@@ -423,9 +431,10 @@ def render_corrections_section(
|
|
|
423
431
|
lines.extend(["", "### Context", "", context.strip()])
|
|
424
432
|
lines.extend([
|
|
425
433
|
"",
|
|
426
|
-
"
|
|
427
|
-
"verdict
|
|
428
|
-
"
|
|
434
|
+
"Outside the correction paths, preserve every sentence, score, evidence path, "
|
|
435
|
+
"and verdict. Follow the schema constraints when a correction describes an "
|
|
436
|
+
"incompatible shape. Preserve the blockquote indentation: each value line "
|
|
437
|
+
"sits two spaces deeper than its own `- **Label**` line.",
|
|
429
438
|
])
|
|
430
439
|
return lines
|
|
431
440
|
|
|
@@ -1088,7 +1088,7 @@ def _parser() -> argparse.ArgumentParser:
|
|
|
1088
1088
|
"--only",
|
|
1089
1089
|
action="append",
|
|
1090
1090
|
default=[],
|
|
1091
|
-
choices=list(STEP_ORDER),
|
|
1091
|
+
choices=list(dict.fromkeys((*STEP_ORDER, *V3_STEP_ORDER))),
|
|
1092
1092
|
help=(
|
|
1093
1093
|
"run only these steps (repeatable, contractual order preserved). "
|
|
1094
1094
|
"Use `--only validate-run` to retry the step that usually fails "
|
|
@@ -118,8 +118,8 @@ _MACHINE_OWNED_NESTED = frozenset(
|
|
|
118
118
|
)
|
|
119
119
|
|
|
120
120
|
|
|
121
|
-
def
|
|
122
|
-
"""
|
|
121
|
+
def _without_machine_owned_fields(node: Any, *, negated: bool = False) -> Any:
|
|
122
|
+
"""기계 소유 필드의 정의와 필수 조건을 뺀 작성자용 스키마 사본.
|
|
123
123
|
|
|
124
124
|
`not` 아래는 건드리지 않는다. 거기서 `required: [designPreparation]` 은
|
|
125
125
|
"이 필드가 있으면 안 된다" 는 뜻이고 서사에는 원래 없으니 이미 만족한다.
|
|
@@ -128,7 +128,7 @@ def _without_machine_owned_required(node: Any, *, negated: bool = False) -> Any:
|
|
|
128
128
|
뒤집는다. 실제로 `direction-invalidated` 계획 분기가 그렇게 죽었다.
|
|
129
129
|
"""
|
|
130
130
|
if isinstance(node, list):
|
|
131
|
-
return [
|
|
131
|
+
return [_without_machine_owned_fields(item, negated=negated) for item in node]
|
|
132
132
|
if not isinstance(node, dict):
|
|
133
133
|
return node
|
|
134
134
|
result: dict[str, Any] = {}
|
|
@@ -137,8 +137,13 @@ def _without_machine_owned_required(node: Any, *, negated: bool = False) -> Any:
|
|
|
137
137
|
result[key] = [
|
|
138
138
|
name for name in value if name not in _MACHINE_OWNED_NESTED
|
|
139
139
|
]
|
|
140
|
+
elif key == "properties" and isinstance(value, dict) and not negated:
|
|
141
|
+
result[key] = {
|
|
142
|
+
name: _without_machine_owned_fields(field)
|
|
143
|
+
for name, field in value.items() if name not in _MACHINE_OWNED_NESTED
|
|
144
|
+
}
|
|
140
145
|
else:
|
|
141
|
-
result[key] =
|
|
146
|
+
result[key] = _without_machine_owned_fields(
|
|
142
147
|
value, negated=negated or key == "not"
|
|
143
148
|
)
|
|
144
149
|
return result
|
|
@@ -169,8 +174,8 @@ def writer_owned_schema(schema: Mapping[str, Any]) -> dict[str, Any]:
|
|
|
169
174
|
return {
|
|
170
175
|
"type": "object",
|
|
171
176
|
"additionalProperties": False,
|
|
172
|
-
"properties":
|
|
173
|
-
"$defs":
|
|
177
|
+
"properties": _without_machine_owned_fields(properties),
|
|
178
|
+
"$defs": _without_machine_owned_fields(schema.get("$defs", {})),
|
|
174
179
|
}
|
|
175
180
|
|
|
176
181
|
|
|
@@ -179,6 +184,28 @@ def validate_writer_owned(data: Mapping[str, Any], schema: Mapping[str, Any]) ->
|
|
|
179
184
|
return validate(dict(data), writer_owned_schema(schema))
|
|
180
185
|
|
|
181
186
|
|
|
187
|
+
def task_narrative_errors(
|
|
188
|
+
data: Mapping[str, Any], schema: Mapping[str, Any], task_type: str,
|
|
189
|
+
) -> list[str]:
|
|
190
|
+
"""실행 유형에 따른 판정·후속 작업 규칙을 정본 스키마에서 검사한다."""
|
|
191
|
+
context = {**data, "header": {"taskType": task_type}}
|
|
192
|
+
errors: list[str] = []
|
|
193
|
+
for branch in schema.get("allOf", []):
|
|
194
|
+
fields = {
|
|
195
|
+
key: value
|
|
196
|
+
for key, value in branch.get("then", {}).get("properties", {}).items()
|
|
197
|
+
if key in {"finalVerdict", "followUpTasks"}
|
|
198
|
+
}
|
|
199
|
+
if not fields:
|
|
200
|
+
continue
|
|
201
|
+
errors.extend(validate(context, {
|
|
202
|
+
"$defs": schema.get("$defs", {}),
|
|
203
|
+
"if": branch["if"],
|
|
204
|
+
"then": {"properties": fields},
|
|
205
|
+
}))
|
|
206
|
+
return list(dict.fromkeys(errors))
|
|
207
|
+
|
|
208
|
+
|
|
182
209
|
def writer_owned_path_defect(path: str) -> str | None:
|
|
183
210
|
"""이 필드 경로에 작성자가 쓸 수 없는 이유, 쓸 수 있으면 None.
|
|
184
211
|
|
|
@@ -811,7 +811,7 @@ def _schema_authoring_rules(
|
|
|
811
811
|
if isinstance(key, str) and key in allowed
|
|
812
812
|
)
|
|
813
813
|
block_key = TASK_TYPE_DATA_PROPERTY.get(task_type, "")
|
|
814
|
-
block_rules = task_block_rules(schema, block_key) if block_key else ()
|
|
814
|
+
block_rules = task_block_rules(writer_owned_schema(schema), block_key) if block_key else ()
|
|
815
815
|
return (
|
|
816
816
|
required_top_level,
|
|
817
817
|
verdict_token_rule(schema, task_type),
|
|
@@ -340,4 +340,8 @@ def confirmation_block(state: WizardState) -> str:
|
|
|
340
340
|
lines.append(f" pr-template : {state.pr_template_path} ({state.pr_template_scope or 'once'})")
|
|
341
341
|
if state.fix_cycle:
|
|
342
342
|
lines.append(f" fix-cycle : {state.fix_cycle}")
|
|
343
|
+
lines.append(_msg(
|
|
344
|
+
state.workspace_root, "confirmation", "provider_data_scope",
|
|
345
|
+
project_root=state.project_root,
|
|
346
|
+
))
|
|
343
347
|
return "\n".join(lines)
|
|
@@ -35,7 +35,6 @@ from .ids import (
|
|
|
35
35
|
from .state import Prompt, WizardError, WizardState, _is_role_selection_step
|
|
36
36
|
from .prompts import _domain_prompt
|
|
37
37
|
from .picker_navigation import (
|
|
38
|
-
accept_picker_answer,
|
|
39
38
|
is_split_picker,
|
|
40
39
|
merge_split_picker_answer,
|
|
41
40
|
present_picker,
|
|
@@ -150,11 +149,8 @@ def _native_picker_screen(state: WizardState, prompt: Prompt) -> Prompt:
|
|
|
150
149
|
때마다 턴이 하나씩 들었다(실측 2026-09-09, 후보 13개인 critic 화면).
|
|
151
150
|
|
|
152
151
|
묶음에 못 실리면(옵션 16개 초과, 라벨 중복, 질문 묶음이 없는 세션)
|
|
153
|
-
단일 선택은
|
|
154
|
-
|
|
155
|
-
한 번에 보인다. 체크박스는 쪽으로 나누지 않는다 — 쪽 사본이 추천 표시를
|
|
156
|
-
단 채 단일 선택이 돼 `Prompt` 의 추천 불변식(단일 선택은 추천 정확히
|
|
157
|
-
하나)에 걸려 화면이 열리지 않았다(실측 2026-09-09, verifier 전체 후보 화면).
|
|
152
|
+
`CapabilityInteractionPort.plan` 이 단일 선택은 `numbered-single`,
|
|
153
|
+
체크박스는 `numbered-multi` 로 내려 전체 목록을 한 번에 보인다.
|
|
158
154
|
"""
|
|
159
155
|
if "native_single_select" not in state.available_functions:
|
|
160
156
|
return prompt
|
|
@@ -172,7 +168,7 @@ def _native_picker_screen(state: WizardState, prompt: Prompt) -> Prompt:
|
|
|
172
168
|
return split
|
|
173
169
|
if prompt.multi:
|
|
174
170
|
return prompt
|
|
175
|
-
return present_picker(
|
|
171
|
+
return present_picker(prompt)
|
|
176
172
|
|
|
177
173
|
|
|
178
174
|
def _next_prompt_screen(state: WizardState) -> Prompt:
|
|
@@ -404,13 +400,6 @@ def submit(state: WizardState, value: str) -> dict[str, Any]:
|
|
|
404
400
|
return {"echo": "", "next": prompt_payload(state, prompt)}
|
|
405
401
|
plan = _interaction_plan(state, prompt)
|
|
406
402
|
value = _normalize_interaction_answer(state, prompt, plan, value)
|
|
407
|
-
if plan.kind == "native-single":
|
|
408
|
-
original = _next_prompt_screen(state)
|
|
409
|
-
if original.kind == "pick_group":
|
|
410
|
-
original = next(q for q in original.questions if q.step == prompt.step)
|
|
411
|
-
value = accept_picker_answer(state, original, value)
|
|
412
|
-
if value is None:
|
|
413
|
-
return {"echo": "", "next": prompt_payload(state, next_prompt(state))}
|
|
414
403
|
if is_split_picker(prompt):
|
|
415
404
|
# 질문 묶음으로 잘린 픽 — 탭별 CSV 를 한 줄로 합쳐 원본 step 의 제출
|
|
416
405
|
# 경로로 보낸다. 원본의 선택지로 검증한다.
|
|
@@ -419,8 +408,7 @@ def submit(state: WizardState, value: str) -> dict[str, Any]:
|
|
|
419
408
|
elif prompt.kind == "pick_group":
|
|
420
409
|
return _submit_group(state, prompt, value)
|
|
421
410
|
if _is_role_selection_step(prompt.step):
|
|
422
|
-
#
|
|
423
|
-
# 있다(`present_picker`). 답은 잘리지 않은 원본의 선택지로 검증한다.
|
|
411
|
+
# 화면의 중복 라벨을 구분했어도 답은 원본 선택지의 값으로 검증한다.
|
|
424
412
|
original = next_role_prompt(state)
|
|
425
413
|
if original is not None and original.step == prompt.step:
|
|
426
414
|
prompt = original
|
|
@@ -6,20 +6,16 @@
|
|
|
6
6
|
원래 step 의 제출 경로로 간다. 단일 선택(예: critic 처럼 최대 1개인 역할)도
|
|
7
7
|
같은 방식으로 자르되 통틀어 하나만 고른 답이어야 한다.
|
|
8
8
|
|
|
9
|
-
그 크기도 넘거나 질문 묶음이 없는
|
|
10
|
-
|
|
11
|
-
(`engine._native_picker_screen`). 체크박스를 쪽으로 나누던 경로는 쪽 사본이
|
|
12
|
-
추천 표시를 단 채 단일 선택이 돼 `Prompt` 의 추천 불변식에 걸리므로
|
|
13
|
-
2026-09-09 에 뺐다.
|
|
9
|
+
그 크기도 넘거나 질문 묶음이 없는 세션이면 전체 번호 목록을 한 번에 보인다.
|
|
10
|
+
`present_picker` 는 중복 라벨만 구분하고 선택지 순서와 값을 보존한다.
|
|
14
11
|
"""
|
|
15
12
|
import json
|
|
16
13
|
import math
|
|
17
14
|
from dataclasses import replace
|
|
18
15
|
|
|
19
16
|
from .ids import PICK_TYPE_CUSTOM
|
|
20
|
-
from .state import
|
|
17
|
+
from .state import Prompt, WizardError
|
|
21
18
|
|
|
22
|
-
_PAGE_PREFIX = "__okstra_picker_page__:"
|
|
23
19
|
_SPLIT_SEPARATOR = "#"
|
|
24
20
|
|
|
25
21
|
|
|
@@ -113,7 +109,7 @@ def merge_split_picker_answer(prompt: Prompt, value: str) -> str:
|
|
|
113
109
|
return ",".join(chosen)
|
|
114
110
|
|
|
115
111
|
|
|
116
|
-
def present_picker(
|
|
112
|
+
def present_picker(prompt: Prompt) -> Prompt:
|
|
117
113
|
if prompt.kind != "pick" or not prompt.options:
|
|
118
114
|
return prompt
|
|
119
115
|
labels = [option.label for option in prompt.options]
|
|
@@ -121,33 +117,7 @@ def present_picker(state: WizardState, prompt: Prompt, *, limit: int) -> Prompt:
|
|
|
121
117
|
option,
|
|
122
118
|
label=option.label + (f" [{option.value}]" if labels.count(option.label) > 1 else ""),
|
|
123
119
|
) for option in prompt.options]
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
offset = state.picker_offsets.get(prompt.step, 0)
|
|
127
|
-
offset = offset if 0 <= offset < len(options) else 0
|
|
128
|
-
shown = options[offset:offset + size]
|
|
129
|
-
label = prompt.label
|
|
130
|
-
if paged:
|
|
131
|
-
next_offset = offset + size if offset + size < len(options) else 0
|
|
132
|
-
shown.append(Option(
|
|
133
|
-
f"{_PAGE_PREFIX}{next_offset}",
|
|
134
|
-
"다음 선택지" if next_offset else "처음 선택지로",
|
|
135
|
-
))
|
|
136
|
-
label += f" ({offset + 1}–{min(offset + size, len(options))}/{len(options)})"
|
|
137
|
-
if len(shown) == 1:
|
|
138
|
-
shown.append(Option(f"{_PAGE_PREFIX}0", "다시 보기"))
|
|
139
|
-
shown = [o for o in shown if o.value != PICK_TYPE_CUSTOM] + [
|
|
140
|
-
o for o in shown if o.value == PICK_TYPE_CUSTOM
|
|
120
|
+
options = [o for o in options if o.value != PICK_TYPE_CUSTOM] + [
|
|
121
|
+
o for o in options if o.value == PICK_TYPE_CUSTOM
|
|
141
122
|
]
|
|
142
|
-
return replace(prompt,
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
def accept_picker_answer(state: WizardState, prompt: Prompt, value: str) -> str | None:
|
|
146
|
-
if value.startswith(_PAGE_PREFIX):
|
|
147
|
-
raw_offset = value.removeprefix(_PAGE_PREFIX)
|
|
148
|
-
if not raw_offset.isdecimal() or not 0 <= int(raw_offset) < len(prompt.options):
|
|
149
|
-
raise WizardError("invalid picker page; select a returned navigation option")
|
|
150
|
-
state.picker_offsets[prompt.step] = int(raw_offset)
|
|
151
|
-
return None
|
|
152
|
-
state.picker_offsets.pop(prompt.step, None)
|
|
153
|
-
return value
|
|
123
|
+
return replace(prompt, options=options)
|
|
@@ -19,9 +19,8 @@ and stage-integration steps use.
|
|
|
19
19
|
Use it as the assertion in a plan step:
|
|
20
20
|
okstra worktree-status --check-clean
|
|
21
21
|
|
|
22
|
-
Do not chain a `git tag stage-<N>-exit` onto it.
|
|
23
|
-
|
|
24
|
-
while the stage is still running puts it on an earlier commit.
|
|
22
|
+
Do not chain a `git tag stage-<N>-exit` onto it. Stage completion records the
|
|
23
|
+
commit in the consumer ledger without creating or moving git tags.
|
|
25
24
|
|
|
26
25
|
Output: JSON { ok, path, clean, entries, excluded }. `entries` holds the
|
|
27
26
|
`git status --short` rows that made it dirty; `excluded` lists the paths left
|
|
@@ -51,6 +51,8 @@ def build_write_policy(
|
|
|
51
51
|
_relative_paths(invocation.get("protectedPaths", (".okstra", ".git")))
|
|
52
52
|
),
|
|
53
53
|
}
|
|
54
|
+
if "plannedPathsDeclared" in invocation:
|
|
55
|
+
source_policy["plannedPathsDeclared"] = invocation["plannedPathsDeclared"]
|
|
54
56
|
git_policy = _git_policy(invocation, source_root, source_mode)
|
|
55
57
|
external_roots = _validated_auxiliary_roots(invocation, assignment)
|
|
56
58
|
auxiliary_policy = {
|
|
@@ -144,18 +146,21 @@ def build_invocation_write_contract(
|
|
|
144
146
|
)
|
|
145
147
|
)
|
|
146
148
|
)
|
|
149
|
+
source_root = worktree.resolve() if worktree is not None else root
|
|
150
|
+
source_paths = _planned_source_paths(
|
|
151
|
+
planned_paths, root, source_root, relative_artifacts
|
|
152
|
+
)
|
|
147
153
|
invocation: dict[str, Any] = {
|
|
148
154
|
"role": role,
|
|
149
155
|
"projectRoot": str(root),
|
|
150
156
|
"artifactPaths": list(relative_artifacts),
|
|
151
|
-
"plannedPaths": list(
|
|
157
|
+
"plannedPaths": list(source_paths),
|
|
152
158
|
"plannedPathsDeclared": planned_paths_declared,
|
|
153
159
|
"protectedPaths": [".okstra", ".git"],
|
|
154
160
|
"generatedPaths": list(generated_paths),
|
|
155
161
|
"scratchRoots": [str(path) for path in scratch_roots],
|
|
156
162
|
"auxiliaryRoots": [str(path) for path in auxiliary_roots],
|
|
157
163
|
}
|
|
158
|
-
source_root = worktree.resolve() if worktree is not None else root
|
|
159
164
|
if role == "implementer":
|
|
160
165
|
invocation.update({
|
|
161
166
|
"expectedBaseCommit": _git_output(source_root, "rev-parse", "HEAD"),
|
|
@@ -173,6 +178,47 @@ def build_invocation_write_contract(
|
|
|
173
178
|
return policy, derive_write_enforcement(policy, maximum_precision, role=role)
|
|
174
179
|
|
|
175
180
|
|
|
181
|
+
def _planned_source_paths(
|
|
182
|
+
paths: Sequence[str],
|
|
183
|
+
project_root: Path,
|
|
184
|
+
source_root: Path,
|
|
185
|
+
artifact_paths: Sequence[str],
|
|
186
|
+
) -> tuple[str, ...]:
|
|
187
|
+
"""산출물 권한은 프로젝트에, 소스 변경 목록은 배정 작업 디렉터리에 결속한다."""
|
|
188
|
+
sources: set[str] = set()
|
|
189
|
+
for value in paths:
|
|
190
|
+
candidate = Path(value)
|
|
191
|
+
if candidate.is_absolute():
|
|
192
|
+
root = (
|
|
193
|
+
source_root if candidate.is_relative_to(source_root) else project_root
|
|
194
|
+
)
|
|
195
|
+
relative = _relative_to_root(candidate, root, "planned")
|
|
196
|
+
else:
|
|
197
|
+
relative = _relative_paths((value,))[0]
|
|
198
|
+
root = (
|
|
199
|
+
project_root
|
|
200
|
+
if PurePosixPath(relative).parts[0] == ".okstra"
|
|
201
|
+
else source_root
|
|
202
|
+
)
|
|
203
|
+
_relative_to_root(root / relative, root, "planned")
|
|
204
|
+
relative = _relative_paths((relative,))[0]
|
|
205
|
+
path = PurePosixPath(relative)
|
|
206
|
+
if path.parts[0] == ".okstra":
|
|
207
|
+
if root != project_root or not any(
|
|
208
|
+
path.is_relative_to(allowed) for allowed in artifact_paths
|
|
209
|
+
):
|
|
210
|
+
raise WritePolicyError(
|
|
211
|
+
f"planned artifact path exceeds this worker's artifact permissions: {value}"
|
|
212
|
+
)
|
|
213
|
+
else:
|
|
214
|
+
if path.parts[0] == ".git":
|
|
215
|
+
raise WritePolicyError(f"planned source path is protected: {value}")
|
|
216
|
+
# 원래 체크아웃의 절대 소스 경로도 실제 쓰기는 배정 작업 디렉터리에 한정된다.
|
|
217
|
+
_relative_to_root(source_root / relative, source_root, "planned source")
|
|
218
|
+
sources.add(relative)
|
|
219
|
+
return tuple(sorted(sources))
|
|
220
|
+
|
|
221
|
+
|
|
176
222
|
def task_root_from_run_manifest(
|
|
177
223
|
project_root: Path, manifest: Mapping[str, Any]
|
|
178
224
|
) -> Path | None:
|
|
@@ -191,7 +237,7 @@ def task_root_from_run_manifest(
|
|
|
191
237
|
|
|
192
238
|
def planned_paths_from_run_manifest(
|
|
193
239
|
project_root: Path, manifest: Mapping[str, Any]
|
|
194
|
-
) -> tuple[str, ...]:
|
|
240
|
+
) -> tuple[tuple[str, ...], bool]:
|
|
195
241
|
active_value = manifest.get("activeRunContextPath")
|
|
196
242
|
if not isinstance(active_value, str) or not active_value:
|
|
197
243
|
raise WritePolicyError("implementer write policy has no active run context")
|
|
@@ -421,7 +467,8 @@ def _planned_paths_from_report(
|
|
|
421
467
|
if isinstance(path, str) and path.strip()
|
|
422
468
|
}
|
|
423
469
|
if paths:
|
|
424
|
-
|
|
470
|
+
# 경로 분류는 산출물 권한과 배정 작업 디렉터리를 함께 아는 정책 생성기가 맡는다.
|
|
471
|
+
return tuple(sorted(paths)), True
|
|
425
472
|
# A plan approved before `plannedPaths` existed carries its paths only in
|
|
426
473
|
# the prose `files` cell. That prose does not survive being split into
|
|
427
474
|
# paths — it mis-splits globs and parentheticals — so the ledger stays
|
|
@@ -148,6 +148,7 @@
|
|
|
148
148
|
"mode": {"enum": ["source-readonly", "project-mutation"]},
|
|
149
149
|
"allowedRoot": {"type": "string", "pattern": "^/"},
|
|
150
150
|
"plannedPaths": {"type": "array", "items": {"type": "string", "pattern": "\\S"}, "uniqueItems": true},
|
|
151
|
+
"plannedPathsDeclared": {"type": "boolean"},
|
|
151
152
|
"protectedPaths": {"type": "array", "items": {"type": "string", "pattern": "\\S"}, "uniqueItems": true}
|
|
152
153
|
},
|
|
153
154
|
"additionalProperties": false
|
|
@@ -8651,7 +8651,7 @@
|
|
|
8651
8651
|
"minLength": 1
|
|
8652
8652
|
},
|
|
8653
8653
|
"plannedPaths": {
|
|
8654
|
-
"description": "
|
|
8654
|
+
"description": "Paths this step may write, one per entry. Source paths are relative to the assigned worktree; absolute paths under that worktree or the project checkout identify the same repository-relative source. `.okstra` artifact paths are anchored at the original project root and may be absolute. The write-policy builder checks artifact paths against the worker's existing artifact permissions and keeps them out of the source ledger. Keep absolute artifact paths in executable commands. `files` is human-readable prose, not a path ledger.",
|
|
8655
8655
|
"type": "array",
|
|
8656
8656
|
"minItems": 1,
|
|
8657
8657
|
"items": {
|
|
@@ -8796,7 +8796,7 @@
|
|
|
8796
8796
|
"minLength": 1
|
|
8797
8797
|
},
|
|
8798
8798
|
"plannedPaths": {
|
|
8799
|
-
"description": "
|
|
8799
|
+
"description": "Paths this step may write, one per entry. Source paths are relative to the assigned worktree; absolute paths under that worktree or the project checkout identify the same repository-relative source. `.okstra` artifact paths are anchored at the original project root and may be absolute. The write-policy builder checks artifact paths against the worker's existing artifact permissions and keeps them out of the source ledger. Keep absolute artifact paths in executable commands. `files` is human-readable prose, not a path ledger.",
|
|
8800
8800
|
"type": "array",
|
|
8801
8801
|
"minItems": 1,
|
|
8802
8802
|
"items": {
|
|
@@ -53,7 +53,7 @@ The wizard tells you which relay operation to use via `next.interaction.kind`. S
|
|
|
53
53
|
- `kind: "done"` → input collection finished; move to Step 5.
|
|
54
54
|
- `kind: "aborted"` → the user picked abort; the wizard is terminally cancelled. Tell the user on one short line that the run setup was aborted, delete the state file (`rm` with the literal path), and stop this skill — do NOT call `render-args` or `render-bundle` (the wizard rejects `render-args` on an aborted state).
|
|
55
55
|
|
|
56
|
-
When native
|
|
56
|
+
When native selection is available, the runtime keeps choices in that control or in same-screen question tabs when the complete selection fits. Otherwise, it returns a complete numbered list: show all labels and descriptions in one message and accept a number, label, or value (comma-separated for multiple choices). This applies to plan confirmation, stages, role counts, and provider/model lists. Do not paginate choices or add a next-page option. Follow the returned `interaction.kind`, including numbered interactions when the user generally prefers a selector. If the declared selector itself fails, follow the relay's `recovery` object. Genuine text steps still collect text.
|
|
57
57
|
|
|
58
58
|
Submit the answer shape required by `interaction.answerProtocol`; do not add normalization beyond the registered relay's explicit mapping. Invalid, out-of-range, or ambiguous answers return `ok: false` and must re-render the same complete interaction.
|
|
59
59
|
|
|
@@ -228,6 +228,8 @@ When `next.step == "confirm"`, the prompt's `label` already carries the full sel
|
|
|
228
228
|
|
|
229
229
|
`okstra wizard confirmation --state-file <path>` returns the same block as `{ok: true, text: ...}` for a text-only host or when the user asks to see the summary again; it is not a substitute for the label.
|
|
230
230
|
|
|
231
|
+
Keep the provider data-transfer disclosure in that label alongside the selected roles/models and task inputs. The user's actual `Proceed` response authorizes the displayed recipients and task-material scope; it does not grant host execution privileges. When dispatching, follow the host relay's guidance to carry that response and scope into the execution-permission request. A freshly generated `confirmationText` does not retroactively add disclosure to an older user response.
|
|
232
|
+
|
|
231
233
|
## Step 5: Render the task bundle
|
|
232
234
|
|
|
233
235
|
When `next.kind == "done"`, fetch the public wizard outcome:
|
|
@@ -261,6 +263,8 @@ Before you ask the user for any approval, clarification, or decision after worke
|
|
|
261
263
|
|
|
262
264
|
Build the `okstra render-bundle` invocation from `outcome.renderArgv`, passing every token verbatim and in order (including empty strings — they are intentional `use phase default` markers).
|
|
263
265
|
|
|
266
|
+
Before invoking it, follow the active host relay's execution-permission guidance. For Codex, read "Permission before bundle preparation" in the relay already selected at preflight: bundle preparation can write protected Git metadata while creating a branch and worktree, even with `--render-only`. Apply this guidance to the first invocation and each subsequent phase or stage preparation.
|
|
267
|
+
|
|
264
268
|
Analysis sidetracks therefore forward wizard-owned tokens such as `--analysis-target "<value>"` and `--evidence-inputs "<value>"` when they are present. These are examples of the verbatim token rule, not a separate hard-coded argument list.
|
|
265
269
|
|
|
266
270
|
Step 3's empty-answer and escaping rules apply verbatim: every flag in `renderArgv` whose following value is the empty string MUST still be passed explicitly (e.g. `--workers ""`, `--directive ""`) — `render-bundle` distinguishes "flag absent" from "flag present with empty value", and the wizard's intent is always the latter.
|
|
@@ -460,6 +464,10 @@ Do not read the wizard state file directly. `okstra wizard outcome` exposes any
|
|
|
460
464
|
- Each wizard run owns its own state file (one per `okstra wizard new-state-file`); two parallel skill invocations do not collide.
|
|
461
465
|
- The skill must NOT call `okstra.sh` (or any other bash entrypoint) that would re-implement the orchestration. The wizard + `render-bundle` is the single authority.
|
|
462
466
|
|
|
467
|
+
## Boundary updates
|
|
468
|
+
|
|
469
|
+
Follow the rendered launch prompt's "Progress, remaining work, and recommendation" guidance at each phase or implementation-stage boundary, at task completion, and before a controlled session pause or handoff. Include results, remaining work, and the recommended action with its reason. During an authorized continuous run, report the boundary and continue the queue; when the lead returns, relay its closeout without repeating an already delivered update.
|
|
470
|
+
|
|
463
471
|
## Failure Modes
|
|
464
472
|
|
|
465
473
|
| Symptom | Cause | Fix |
|
|
@@ -9420,6 +9420,9 @@ def run_preflight(report_path: Path, run_manifest_path: Path) -> int:
|
|
|
9420
9420
|
failures.extend(selected_direction_plan_errors(
|
|
9421
9421
|
data, Path(str(manifest.get("projectRoot") or run_manifest_path.parent)), manifest
|
|
9422
9422
|
))
|
|
9423
|
+
_append_stage_data_failures(
|
|
9424
|
+
data, failures, _task_root_from_run_dir(report_path.parent.parent),
|
|
9425
|
+
)
|
|
9423
9426
|
_validate_planning_conformance_declared(report_path, failures)
|
|
9424
9427
|
print(
|
|
9425
9428
|
json.dumps(
|