okstra 0.199.4 → 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/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 +16 -4
- 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 +6 -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 +6 -0
- package/runtime/python/okstra_ctl/report_assembly.py +4 -1
- 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 +11 -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/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
|
|
@@ -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.
|
|
@@ -101,9 +101,12 @@ def run_corrections_check(
|
|
|
101
101
|
|
|
102
102
|
def semantic_validator(data: dict[str, Any]) -> list[str]:
|
|
103
103
|
block = data.get(block_key)
|
|
104
|
-
return
|
|
105
|
-
|
|
106
|
-
|
|
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
|
+
]
|
|
107
110
|
elif task_type == "implementation-planning":
|
|
108
111
|
|
|
109
112
|
def semantic_validator(data: dict[str, Any]) -> list[str]:
|
|
@@ -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
|
|
|
@@ -1935,6 +1935,12 @@ def _state_next_dispatch(args: argparse.Namespace) -> NextDispatch:
|
|
|
1935
1935
|
data, current, _audit, _history = _round_inputs(args)
|
|
1936
1936
|
run_manifest = getattr(args, "run_manifest", None)
|
|
1937
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]
|
|
1938
1944
|
return next_dispatch(
|
|
1939
1945
|
current, payloads, critic_rostered=_rostered_critic(run_manifest),
|
|
1940
1946
|
decision_items=(
|
|
@@ -594,7 +594,10 @@ def validate_plan_draft(
|
|
|
594
594
|
*task_narrative_errors(
|
|
595
595
|
draft, load_schema_version("3.0"), str(manifest.get("taskType", "")),
|
|
596
596
|
),
|
|
597
|
-
*
|
|
597
|
+
*[
|
|
598
|
+
f"implementationPlanning: {error}"
|
|
599
|
+
for error in selected_direction_plan_errors(draft, project_root, manifest)
|
|
600
|
+
],
|
|
598
601
|
]
|
|
599
602
|
|
|
600
603
|
|
|
@@ -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
|
|
|
@@ -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 |
|