okstra 0.198.0 → 0.198.2

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.
Files changed (27) hide show
  1. package/dist/commands/execute/wizard.mjs +6 -0
  2. package/dist/commands/execute/wizard.mjs.map +1 -1
  3. package/package.json +1 -1
  4. package/runtime/BUILD.json +2 -2
  5. package/runtime/bin/okstra-report-translate.py +28 -8
  6. package/runtime/prompts/lead/convergence.md +17 -0
  7. package/runtime/prompts/lead/okstra-lead-contract.md +31 -5
  8. package/runtime/prompts/lead/plan-body-verification.md +29 -10
  9. package/runtime/python/okstra_ctl/adapters/hosts/grok/relay.md +5 -3
  10. package/runtime/python/okstra_ctl/analysis_packet.py +49 -12
  11. package/runtime/python/okstra_ctl/cmux.py +20 -5
  12. package/runtime/python/okstra_ctl/context_cost.py +16 -6
  13. package/runtime/python/okstra_ctl/group_context.py +9 -2
  14. package/runtime/python/okstra_ctl/render.py +4 -11
  15. package/runtime/python/okstra_ctl/report_assembly.py +20 -2
  16. package/runtime/python/okstra_ctl/report_translation.py +19 -0
  17. package/runtime/python/okstra_ctl/user_response.py +1 -0
  18. package/runtime/python/okstra_ctl/wizard/cli.py +2 -0
  19. package/runtime/python/okstra_ctl/wizard/engine.py +2 -0
  20. package/runtime/python/okstra_ctl/wizard/state.py +1 -0
  21. package/runtime/python/okstra_ctl/wizard/steps_identity.py +14 -3
  22. package/runtime/python/okstra_ctl/worker_prompt_headers.py +4 -1
  23. package/runtime/skills/okstra-run/SKILL.md +7 -1
  24. package/runtime/templates/reports/html/base.template.html +3 -2
  25. package/runtime/templates/reports/html/i18n/en.json +1 -0
  26. package/runtime/templates/reports/html/i18n/ko.json +1 -0
  27. package/runtime/validators/validate-run.py +7 -10
@@ -27,6 +27,8 @@ Usage:
27
27
 
28
28
  'init' auto-fills --workspace-root from 'okstra paths --field workspace',
29
29
  so callers do not pass it.
30
+ Pass --preferred-task-key <project:group:task> to prioritize the task selected
31
+ in this conversation over project-wide recommendations without skipping the picker.
30
32
 
31
33
  All subcommands emit a single JSON object on stdout. On validation failure
32
34
  'step' returns {ok:false, error, current} so the skill can re-prompt.
@@ -102,6 +104,10 @@ export function buildPythonArgs(sub, flags, workspaceRoot = "") {
102
104
  if (hostRuntime) {
103
105
  pyArgs.push("--host-runtime", hostRuntime);
104
106
  }
107
+ const preferredTaskKey = stringFlag(flags, "preferred-task-key");
108
+ if (preferredTaskKey) {
109
+ pyArgs.push("--preferred-task-key", preferredTaskKey);
110
+ }
105
111
  for (const value of stringListFlag(flags, "available-function")) {
106
112
  pyArgs.push("--available-function", value);
107
113
  }
@@ -1 +1 @@
1
- {"version":3,"file":"wizard.mjs","sourceRoot":"","sources":["../../../src/commands/execute/wizard.mts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"wizard.mjs","sourceRoot":"","sources":["../../../src/commands/execute/wizard.mts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "okstra",
3
- "version": "0.198.0",
3
+ "version": "0.198.2",
4
4
  "description": "Host-aware multi-provider cross-verification orchestrator runtime and agent skills.",
5
5
  "license": "MIT",
6
6
  "author": "devonshin",
@@ -1,5 +1,5 @@
1
1
  {
2
- "package": "0.198.0",
3
- "builtAt": "2026-09-10T21:29:19.454Z",
2
+ "package": "0.198.2",
3
+ "builtAt": "2026-09-11T13:42:28.980Z",
4
4
  "repoRoot": "/home/runner/work/okstra/okstra"
5
5
  }
@@ -32,7 +32,11 @@ from okstra_bootstrap import prefer_colocated_modules # noqa: E402
32
32
 
33
33
  prefer_colocated_modules(__file__, "okstra_ctl/report_translation.py")
34
34
 
35
- from okstra_ctl.report_translation import extract, overlay # noqa: E402
35
+ from okstra_ctl.report_translation import ( # noqa: E402
36
+ extract,
37
+ group_translation_pointers,
38
+ overlay,
39
+ )
36
40
  from okstra_ctl.final_report_paths import ( # noqa: E402
37
41
  translation_sidecar_path,
38
42
  translation_source_path,
@@ -82,6 +86,7 @@ def _report_snapshot(path: Path):
82
86
  def _source_payload(
83
87
  authority: RunArtifactAuthority, data_path: Path, raw_bytes: bytes, data: dict
84
88
  ) -> dict:
89
+ strings = extract(data)
85
90
  return {
86
91
  "taskKey": authority.task_key,
87
92
  "runManifestPath": authority.manifest_ref,
@@ -89,7 +94,8 @@ def _source_payload(
89
94
  "sourceDataPath": data_path.relative_to(authority.project_root).as_posix(),
90
95
  "sourceDataSha256": hashlib.sha256(raw_bytes).hexdigest(),
91
96
  "lang": str((data.get("meta") or {}).get("reportLanguage") or ""),
92
- "strings": extract(data),
97
+ "strings": strings,
98
+ "translationGroups": group_translation_pointers(data, strings),
93
99
  }
94
100
 
95
101
 
@@ -108,6 +114,10 @@ def _validate_source_payload(
108
114
  raise SystemExit(
109
115
  "error: report changed after translation source publication"
110
116
  )
117
+ if "translationGroups" in source and (
118
+ source["translationGroups"] != expected["translationGroups"]
119
+ ):
120
+ raise SystemExit("error: translation groups do not match report source")
111
121
 
112
122
 
113
123
  def cmd_extract(args: argparse.Namespace) -> int:
@@ -154,9 +164,13 @@ def cmd_source(args: argparse.Namespace) -> int:
154
164
  print(line("Report", payload["sourceDataPath"]), end="")
155
165
  print(line("Run manifest", authority.manifest_ref), end="")
156
166
  print(line("Source digest", payload["sourceDataSha256"]), end="")
157
- print(line("String count", len(strings)), end="")
158
- for index, source in enumerate(strings.values(), 1):
159
- print(f"\n## T-{index:03d}\n{block(source)}")
167
+ groups = payload["translationGroups"]
168
+ print(line("Field count", len(strings)), end="")
169
+ print(line("String count", len(groups)), end="")
170
+ print(line("Source characters", sum(len(value) for value in strings.values())), end="")
171
+ print(line("Translation characters", sum(len(strings[row[0]]) for row in groups)), end="")
172
+ for index, pointers in enumerate(groups, 1):
173
+ print(f"\n## T-{index:03d}\n{block(strings[pointers[0]])}")
160
174
  return 0
161
175
 
162
176
 
@@ -212,12 +226,14 @@ def cmd_write(args: argparse.Namespace) -> int:
212
226
  source = _load(source_path)
213
227
  _validate_source_payload(source, expected, args.source_digest)
214
228
  sources = expected["strings"]
229
+ # 발행된 구형 작업 목록은 필드당 한 블록이므로 진행 중 번역의 번호를 보존한다.
230
+ groups = source.get("translationGroups", [[pointer] for pointer in sources])
215
231
  translated = _translation_blocks(Path(args.translations))
216
- if len(translated) != len(sources):
232
+ if len(translated) != len(groups):
217
233
  raise SystemExit(
218
234
  f"error: translation blocks must match every T-NNN item — "
219
235
  f"{len(translated)} blocks in {Path(args.translations).name}, "
220
- f"{len(sources)} items in the translation source"
236
+ f"{len(groups)} items in the translation source"
221
237
  )
222
238
  empty = [f"T-{index + 1:03d}" for index, value in enumerate(translated) if not value]
223
239
  if empty:
@@ -229,7 +245,11 @@ def cmd_write(args: argparse.Namespace) -> int:
229
245
  sidecar = translation_sidecar_path(data_path, lang)
230
246
  if sidecar.is_symlink():
231
247
  raise SystemExit("error: translation sidecar path is a symlink")
232
- strings = dict(zip(sources, translated))
248
+ strings = {
249
+ pointer: value
250
+ for pointers, value in zip(groups, translated)
251
+ for pointer in pointers
252
+ }
233
253
  _, report = overlay(data, strings)
234
254
  if report.unresolved or report.applied != len(strings):
235
255
  raise SystemExit("error: translations do not validate against report source")
@@ -334,6 +334,23 @@ Lightweight reverify does not require the original `analysis-packet.md`, `analys
334
334
 
335
335
  This is the single largest avoidable cost in `requirements-discovery`, `error-analysis`, `implementation-option-selection`, and `implementation-planning` runs. Treat as mandatory.
336
336
 
337
+ ## Conditional reference reading
338
+
339
+ The common read retains finding classification, queue pruning, dispatch gates,
340
+ state ownership, and output rules. Use the generated `okstra convergence
341
+ reverify-prompt` body for every verifier. The following reads are guidance for
342
+ avoiding unused examples; queue and dispatch validators still enforce execution.
343
+
344
+ | Current operation | Additional section to read |
345
+ |---|---|
346
+ | Diagnosing a rejected verifier prompt | The reference prompt matching the selected verification mode |
347
+ | `config.critic.enabled` is true | Coverage critic pass, including its shared dispatch procedure |
348
+ | An enabled critic uses acceptance mode for `final-verification` | Acceptance critic pass as well as the shared Coverage critic dispatch procedure |
349
+
350
+ Read the matching sections before their dispatch. A disabled critic needs neither
351
+ critic section. Generated prompts retain the selected mode's instructions even
352
+ when the lead does not read the example text.
353
+
337
354
  ### Lightweight Re-verification Prompt
338
355
 
339
356
  Rendered by `okstra convergence reverify-prompt` when `config.adversarial` is false; the block below is the reference shape, and the rendered body additionally carries each finding's `**Origin item**` and `**Origin audit sidecar**` lines.
@@ -22,8 +22,8 @@ This document is the operating contract and phase index. Detailed procedures liv
22
22
  |-------|-------|
23
23
  | [context-loader](./context-loader.md) | Phase 1 task-bundle discovery, manifest fields, run-directory layout |
24
24
  | [team-contract](./team-contract.md) | Phase 2–5 worker roster, model assignment rules, prompt composition (anchor headers, `[Required reading]`, `[Error reporting]`), worker output contract, terminal statuses, usage tracking |
25
- | [convergence](./convergence.md) | Phase 5.5 finding convergence loop, finding categories, reverify dispatch (anchor headers, required-reading suppression), convergence state schema |
26
- | [plan-body-verification](./plan-body-verification.md) | Phase 6 plan-body verification sub-step (implementation-planning only) — plan-item extraction, verdict semantics, gate resolution, state schema. Read only at that sub-step |
25
+ | [convergence](./convergence.md) | Phase 5.5 finding convergence loop, finding categories, reverify dispatch, convergence state schema. Use the bounded common read under Doctrine lazy reads |
26
+ | [plan-body-verification](./plan-body-verification.md) | Phase 6 plan-body verification sub-step (implementation-planning only) — plan-item extraction, verdict semantics, gate resolution, state schema. Read only the common procedure at that sub-step, using the bounded read below |
27
27
  | [report-writer](./report-writer.md) | Phase 6 final-report authorship, dispatch template, resume-safe dispatch, shared-graph integrity check, Phase 7 token-usage collector |
28
28
 
29
29
  Read-side inspection (`/okstra-inspect`) and scheduling (`/okstra-schedule-gen`) are user-invoked skills, not lead support contracts — the lead does not consult them during a run.
@@ -262,11 +262,23 @@ After context-loader completes, read **only the compact intake files below** in
262
262
 
263
263
  **Doctrine lazy reads (BLOCKING — read at the round, not at Phase 1):**
264
264
 
265
- - [convergence](./convergence.md) — read before the first `PROGRESS: phase-5.5-convergence` line of any run that runs a convergence round.
266
- - [plan-body-verification](./plan-body-verification.md) — read before the first `PROGRESS: phase-5.5.9-plan-verify` line.
265
+ - [convergence](./convergence.md) — read the common procedure with the bounded command below before the first `PROGRESS: phase-5.5-convergence` line of any run that runs a convergence round.
266
+ - [plan-body-verification](./plan-body-verification.md) — read the common procedure with the bounded command in the Phase 6 sub-step before the first `PROGRESS: phase-5.5.9-plan-verify` line.
267
267
 
268
268
  Both stay out of the Phase 1 baseline for the token reason above, and neither is optional at its round: together they carry more than half of this contract family's MUST clauses, so a round dispatched without the read is a round run from memory. **Enforced:** `validators/validate_session_conformance.py` `_ENTRY_GUARD_READS` requires each read — a `Read` call or a shell command naming the file — inside this run's window and before that checkpoint. The requirement is conditioned on the checkpoint actually appearing, so a run that holds no such round is never asked for it.
269
269
 
270
+ Read the convergence common procedure from the resolved resource path. Replace
271
+ `<convergence-contract-path>` with its absolute path. This skips reference prompt
272
+ examples and critic-specific procedures; the conditional reading table in the
273
+ output identifies when those sections are needed:
274
+
275
+ ```sh
276
+ awk '/^### Lightweight Re-verification Prompt$/ {skip=1} /^## Convergence State Artifact$/ {skip=0} /^## Coverage critic pass$/ {skip=1} /^## Output$/ {skip=0} !skip {print}' '<convergence-contract-path>'
277
+ ```
278
+
279
+ `tests/contract/test_contract_examples_execute.py::test_convergence_common_read_keeps_protocol`
280
+ checks the emitted common procedure. The same entry-guard read trace applies.
281
+
270
282
  **Lazy reading discipline (do NOT read at Phase 1):**
271
283
 
272
284
  - `task-index.md` — only when the user explicitly asks for a human summary or when history disambiguation is required.
@@ -429,7 +441,21 @@ After the Report writer worker narrative is reviewed, **if** `task_type == "impl
429
441
 
430
442
  This is a Phase 6 sub-step — it does NOT introduce a new top-level lifecycle phase; the lead operating-phase model (Phase 1 Intake → Phase 7 Persist, labels in the "Quick Reference" table above as the single source of truth) is preserved. The round's outcome is read from the final report's `### 5.5.9 Plan Body Verification` section and `implementationPlanning.planBodyVerification` in its data.json — it is not a separate lifecycle phase identifier.
431
443
 
432
- **REQUIRED RESOURCE:** Read [plan-body-verification](./plan-body-verification.md) for the round protocol, plan-item ID scheme (`P-Dir-1` for selected-direction; `P-Opt-*` for legacy candidate comparison; then `P-Step-*` / `P-Dep-*` / `P-Val-*` / `P-Rb-*` / `P-Req-*` / `P-Prep-*`), verdict semantics (`AGREE` / `DISAGREE(a-f)` / `SUPPLEMENT`), classification rules, gate-result resolution, and the state-file schema at `runs/<task-type>/state/plan-body-verification.json`. For `P-Dir-1`, compare `directionRealization` with `selectedDirectionRef` and its snapshot: verify the core mechanism, architecture boundaries, planning invariants, and any hidden direction change.
444
+ **REQUIRED RESOURCE:** Read the common procedure of [plan-body-verification](./plan-body-verification.md) for the round protocol, plan-item ID scheme (`P-Dir-1` for selected-direction; `P-Opt-*` for legacy candidate comparison; then `P-Step-*` / `P-Dep-*` / `P-Val-*` / `P-Rb-*` / `P-Req-*` / `P-Prep-*`), verdict semantics (`AGREE` / `DISAGREE(a-f)` / `SUPPLEMENT`), classification rules, gate-result resolution, and state-path authority. Read the state-file schema only when diagnosing state or projection validation. For `P-Dir-1`, compare `directionRealization` with `selectedDirectionRef` and its snapshot: verify the core mechanism, architecture boundaries, planning invariants, and any hidden direction change.
445
+
446
+ Read from the resolved runtime resource path, replacing `<plan-body-contract-path>`
447
+ with that resource's absolute path. This prints the complete common procedure and
448
+ its conditional reading table, stopping before reference examples:
449
+
450
+ ```sh
451
+ awk '/^## Reference material$/ {exit} {print}' '<plan-body-contract-path>'
452
+ ```
453
+
454
+ Follow the conditional reading table for later rounds and validation failures.
455
+ The command keeps the filename in the read trace used by
456
+ `validators/validate_session_conformance.py` `_ENTRY_GUARD_READS`.
457
+ The bounded output is checked by
458
+ `tests/contract/test_contract_examples_execute.py::test_plan_body_common_read_keeps_gate_rules`.
433
459
 
434
460
  Distinct from Phase 5.5 finding convergence:
435
461
 
@@ -376,7 +376,7 @@ round before any host or provider process starts.
376
376
  **On a re-run, add `--prior-state <the previous run's plan-body-verification-<task-type>-<seq>.json>`.** A newly seeded item whose `contentHash` equals that run's `verifiedContentHash` for the same id inherits its verdicts, is tagged `carriedForwardFromSeq`, and drops out of round 1's `dispatchQueue` — in the state and in the sibling `plan-items-*.json` the next `okstra plan-items prompt` reads. Without it a re-run re-judges every line the previous run already settled, and the same dissent re-opens under a new `P-*` number. A matching id alone never carries: `P-*` ids are positional, so a plan that gained one line hands the old id to a different sentence. The flag requires `--state` and refuses a prior state file that sits under another task's root — a plan-body state carries no task identity of its own, so its path is the only identity there is. **Enforced:** `okstra_ctl.plan_items_cli._carry_prior_run_verdicts`.
377
377
 
378
378
  **`--run-manifest` is what scopes the gate to the stage you are starting.** Seed uses it to overlay disk `done` / `active` onto `planBodyVerification.stageLedger`. The current plan's depends-on fills `ready` / `blocked` when no prior plan exists, so a first run does not treat every stage as in-scope. **Enforced:** `okstra_ctl.plan_items.planning_stage_ledger`.
379
- 2. For each analyser worker in the roster (`claude`, `codex`, and `antigravity` if opted in), lead constructs a reverify prompt using the template in §"Plan-body reverify prompt" below.
379
+ 2. For each analyser worker in the roster, use `okstra plan-items prompt` output verbatim as the instruction body in the materialization sequence above. Read §"Re-verification rounds (round 2+)" before preparing a later round; read §"Plan-body reverify prompt" only when diagnosing a prompt-contract failure.
380
380
  3. Dispatch uses the same wrapper infrastructure as finding convergence, so the `--role-slug` is the same canonical `<role>-worker` that convergence uses — not a round-specific slug. Result file path: `runs/<task-type>/worker-results/<role>-worker-plan-verify-r<N>-implementation-planning-<seq>.md` (e.g. `codex-worker-plan-verify-r1-implementation-planning-003.md`). **`<seq>` is the report's sequence** — the one in this run's `final-report-<task-type>-<seq>` filename, NOT the `workerResults` sequence the initial analysis results carry. The two are equal in most runs and diverge in some (`reports: 004` alongside `workerResults: 005` is a real case). Provenance no longer globs by either seq: it resolves the expected filenames from this run's team-state `workerDispatches[]` rows — the paths the dispatches actually recorded — and falls back to the seq glob only when no team-state is readable, saying so in its finding. The `-worker-` token is load-bearing twice over: §"Plan-body reverify prompt" requires the same anchor headers as convergence, whose `**Audit sidecar path:**` is derived by `okstra_ctl.worker_artifact_paths.audit_sidecar_rel()` inserting `-audit-` after that token — a slug without it makes the header underivable and the helper raises. Record each `planItems[].verdicts[].worker` as the same `<role>-worker` string, because provenance compares it to this filename's prefix. **Enforced:** `tests/contract/test_reverify_dispatch_anchors.py` derives the sidecar from the documented name and re-extracts the prefix the provenance resolver uses.
381
381
  **Verdict provenance.** Every verdict recorded in `planItems[].verdicts[]` MUST trace back to a dispatch that actually returned a result file. The whole gate — classification, self-fix eligibility, promotion, `gateBlockedBy` — is computed from these votes, so an unbacked vote lets the round be skipped while the gate still reads `passed`. **Enforced (advisory):** `validators/validate-run.py` `_validate_plan_body_verdict_provenance` reports any `verdicts[].worker` with no recorded reverify dispatch whose result file exists; the finding is not in the blocking allowlist, so it surfaces as an advisory rather than failing the run — record it, never dismiss it. Recording a `verification-error` for a dispatch that produced no result is the correct way to represent a failed worker — inventing an `AGREE` is a contract violation, and renaming a result file to make provenance match destroys the link the check reads.
382
382
 
@@ -474,7 +474,13 @@ round before any host or provider process starts.
474
474
  - A terminal row preserves its original dissent classification only from the convergence-owned state history. Every `user-decision-required` / `user-decision-evaluated` activity cites the row's `C-NNN` in `clarificationRefs` and affected plan items in `planItemIds`. A resolved decision names only existing `A-NNN` checks. Report assembly validates those links and derives the report backtraces; it does not accept copied IDs from the approval ledger. When an independent coverage-only blocker is corrected, keep the `C-NNN` in the non-blocking Requirement Coverage row's `decisionRefs`. `obsolete` is valid only after current evidence shows that the question or blocker disappeared.
475
475
  9. Approval lives in the report record `frontmatter.approved` field — there is no in-body marker line. The user may set it to `true` (via `--approve` or the in-session wizard) when remaining `Blocks=approval` rows are user-proceeded (`accept-risk` / `select` / `answer`) even if the recorded `gateResult` is still `blocked-by-disagreement`. `aborted-non-result` still withholds approval. **Enforced:** run-prep (`scripts/okstra_ctl/run.py` `_validate_approved_plan` / `_blocking_gate_survives_user_decision`) and `validators/validate-run.py` `_validate_plan_body_gate_recompute`.
476
476
 
477
- ## `plan-body-verification-<task-type>-<seq>.json` schema
477
+ ## Worker non-result handling in plan-body round (BLOCKING)
478
+
479
+ Mirrors finding convergence ([convergence](./convergence.md) §"Worker failure handling in reverify"). Concretely:
480
+
481
+ - A dispatch that returns terminal non-result MUST NOT be aggregated as `DISAGREE`.
482
+ - If at least one dispatch was issued AND **all** plan-body dispatches return non-result, the Gate result is `aborted-non-result`. Record one `contract-violation` event per non-result dispatch.
483
+ - When the gate is `aborted-non-result`, report-writer MUST keep the frontmatter `approved: false` (publishing `approved: true` under this gate result is a validator failure). A single row is added to `## 1. Clarification Items` with `Statement="plan-body verification could not run — all workers returned non-result"`, `Kind=decision`, `Blocks=approval`, allowing the user to either retry the phase or override by running `--approve` on the resume command (or confirming in the in-session wizard). The row MUST name which dispatches returned no result and what re-running them requires. **Enforced:** `validators/validate-run.py` `_validate_aborted_gate_has_clarification` — `_validate_plan_body_clarification_matching` cannot cover this case because it walks `majority-disagree` items and an aborted round produces none, which is exactly how an aborted run used to reach the user with no stated blocker and stall.
478
484
 
479
485
  **Take the path from the launch prompt, never from this filename (BLOCKING).**
480
486
  The run's `## Run Paths` block renders `Plan-body verification state:` with the
@@ -488,6 +494,27 @@ team-state name, so the round count reads as 0 and
488
494
  `verification-round-completed count must match automatic plan-body rounds=0`
489
495
  fails a run whose rounds all ran.
490
496
 
497
+ ## Conditional reference reading
498
+
499
+ The common procedure ends here. Keep the sections above in context for every
500
+ plan-body round. Read the reference sections below only for the matching operation:
501
+
502
+ | Operation | Additional section to read |
503
+ |---|---|
504
+ | Inspecting a state-shape or projection validation failure | `plan-body-verification-<task-type>-<seq>.json` schema |
505
+ | Investigating a generated prompt rejected by the dispatch gate | Plan-body reverify prompt |
506
+ | Preparing round 2 or later after a rewrite | Re-verification rounds (round 2+) — carry the dissent forward |
507
+
508
+ Use `okstra plan-items prompt` output verbatim for every round. The reference
509
+ prompt is explanatory; it does not replace the generated queue or authorize
510
+ hand-written items. Read a referenced section when an unresolved error requires
511
+ it; do not load every reference pre-emptively. This reading policy is guidance;
512
+ queue validation and dispatch checks continue to enforce the generated input.
513
+
514
+ ## Reference material
515
+
516
+ ## `plan-body-verification-<task-type>-<seq>.json` schema
517
+
491
518
  **Which file is authoritative for what.** Contract v3 keeps both views in one convergence-owned state file before publication:
492
519
 
493
520
  | | records | what a self-fix round does to it |
@@ -805,11 +832,3 @@ An item with no recorded vote carrying a round number gets no block, and an enve
805
832
  **Enforced:** `okstra plan-items validate-prepared --state <same state>` re-derives the carry and exits 2 when the prepared envelope's `priorRounds` does not match, alongside the `items` / `dispatchQueue` comparison it already made. A prepared queue that dropped the dissent cannot pass the step-1 validation the dispatch is gated on.
806
833
 
807
834
  The two spellings are different anchors for different artifacts: `**Prior round dissent**` is the block `prompt` puts in the prompt, `**Prior dissent**` is the line the worker puts in its result. `scripts/okstra_ctl/verdict_blocks.py` parses the result line into the verdict block when it is present and leaves it empty when it is not, so an omitted answer line is still silent — the prompt is what is now guaranteed, not the response.
808
-
809
- ## Worker non-result handling in plan-body round (BLOCKING)
810
-
811
- Mirrors finding convergence ([convergence](./convergence.md) §"Worker failure handling in reverify"). Concretely:
812
-
813
- - A dispatch that returns terminal non-result MUST NOT be aggregated as `DISAGREE`.
814
- - If at least one dispatch was issued AND **all** plan-body dispatches return non-result, the Gate result is `aborted-non-result`. Record one `contract-violation` event per non-result dispatch.
815
- - When the gate is `aborted-non-result`, report-writer MUST keep the frontmatter `approved: false` (publishing `approved: true` under this gate result is a validator failure). A single row is added to `## 1. Clarification Items` with `Statement="plan-body verification could not run — all workers returned non-result"`, `Kind=decision`, `Blocks=approval`, allowing the user to either retry the phase or override by running `--approve` on the resume command (or confirming in the in-session wizard). The row MUST name which dispatches returned no result and what re-running them requires. **Enforced:** `validators/validate-run.py` `_validate_aborted_gate_has_clarification` — `_validate_plan_body_clarification_matching` cannot cover this case because it walks `majority-disagree` items and an aborted round produces none, which is exactly how an aborted run used to reach the user with no stated blocker and stall.
@@ -20,6 +20,8 @@ This adapter maps neutral Okstra lead operations to the Grok CLI. Read it only w
20
20
 
21
21
  ## Wizard interaction relay
22
22
 
23
+ Set `multi_select` explicitly on each object in `questions[]`: `true` for a multi-select question and `false` for a single-select question. The field belongs inside the question object, not at the call's top level. Grok uses `multi_select`; `multiSelect` is the Claude Code field. Before calling `ask_user_question`, check that every question carries the value specified by this relay. The relay field names and values are covered by `tests/contract/test_host_orchestration_rules.py`; the live host call still depends on the lead passing them.
24
+
23
25
  Read this contract when `okstra preflight` returns this file as `runtimeReadiness.relayContract`. The JSON is the complete interaction mapping for this host. `semanticFunctions` is an allowlist: the skill may declare a function only when both this list and the live harness expose it.
24
26
 
25
27
  ```json
@@ -45,7 +47,7 @@ Read this contract when `okstra preflight` returns this file as `runtimeReadines
45
47
  "questions": "one",
46
48
  "question": "label-with-progress",
47
49
  "options": "all-in-original-order-as-label-description",
48
- "multiSelect": false
50
+ "multi_select": false
49
51
  },
50
52
  "response": {
51
53
  "envelope": "answers",
@@ -61,7 +63,7 @@ Read this contract when `okstra preflight` returns this file as `runtimeReadines
61
63
  "questions": "one",
62
64
  "question": "label-with-progress",
63
65
  "options": "all-in-original-order-as-label-description",
64
- "multiSelect": true
66
+ "multi_select": true
65
67
  },
66
68
  "response": {
67
69
  "envelope": "answers",
@@ -77,7 +79,7 @@ Read this contract when `okstra preflight` returns this file as `runtimeReadines
77
79
  "questions": "all-in-original-order",
78
80
  "question": "label-with-progress",
79
81
  "options": "all-in-original-order-as-label-description",
80
- "multiSelect": "questions[].multi"
82
+ "multi_select": "questions[].multi"
81
83
  },
82
84
  "response": {
83
85
  "envelope": "answers",
@@ -146,7 +146,11 @@ def build_analysis_packet(
146
146
  )
147
147
  parts.extend(_group_context_block(group_human))
148
148
  parts.extend(_brief_block(brief_text))
149
- parts.extend(_group_memory_block(group_memory, _own_task_segment(task_key)))
149
+ parts.extend(_group_memory_block(
150
+ group_memory, _own_task_segment(task_key),
151
+ "\n".join((brief_text, group_human, clarification_text, directive,
152
+ fix_history_text, prior_planning_summary, stage_ledger_json)),
153
+ ))
150
154
  parts.extend(_profile_block(task_type, profile_text))
151
155
  parts.extend(_reference_block(reference_text))
152
156
  parts.extend(_fix_history_block(fix_history_text))
@@ -310,7 +314,13 @@ GROUP_MEMORY_PREFACE = (
310
314
  "`record`. This section is not this task's requirement ledger, and a sibling's "
311
315
  "decision does not bind this run: where a sibling's conclusion conflicts with "
312
316
  "the `## Task-Specific Brief Extract`, raise a `Clarification Items` row rather "
313
- "than re-deriving what the sibling already settled or silently overriding it."
317
+ "than re-deriving what the sibling already settled or silently overriding it. "
318
+ "Every sibling has a headline and record pointer below. Full details are "
319
+ "included for explicit task references and their referenced siblings, and "
320
+ "for entries with watch-outs or no record pointer. For an index-only entry, "
321
+ "read its section in the Task-group context source or its record when its "
322
+ "headline may affect this task or the relationship is unclear. Omitted "
323
+ "details do not mean the sibling has no decisions or follow-ups."
314
324
  )
315
325
 
316
326
 
@@ -332,22 +342,49 @@ def _own_task_segment(task_key: str) -> str:
332
342
  return group_context.slugify_task_segment(re.split(r"[:/]", task_key)[-1])
333
343
 
334
344
 
335
- def _group_memory_block(region: str, own_task_segment: str) -> list[str]:
336
- """형제 task 의 항목만 싣는다. 자기 항목은 timeline·prior-planning 이 이미 나른다."""
345
+ _TASK_REFERENCE_RE = re.compile(r"[a-z0-9](?:[a-z0-9_.-]*[a-z0-9])?", re.IGNORECASE)
346
+
347
+
348
+ def _group_memory_block(
349
+ region: str, own_task_segment: str, reference_text: str,
350
+ ) -> list[str]:
351
+ """형제 전체의 색인을 유지하며 참조 연결과 주의사항이 있는 항목을 펼친다."""
337
352
  entries = [
338
353
  entry for entry in group_context.parse_memory_entries(region)
339
354
  if entry.task_id != own_task_segment
340
355
  ]
341
356
  if not entries:
342
357
  return []
343
- return [
344
- "",
345
- "## Task-Group Memory",
346
- "",
347
- GROUP_MEMORY_PREFACE,
348
- "",
349
- group_context.render_memory_entries(entries).strip(),
350
- ]
358
+ tokens = set(_TASK_REFERENCE_RE.findall(reference_text.casefold()))
359
+ own_ids = {own_task_segment, group_context.ticket_id_from_brief_id(own_task_segment)}
360
+ rendered = [group_context.render_memory_entries([entry]).strip() for entry in entries]
361
+ entry_tokens = [set(_TASK_REFERENCE_RE.findall(text.casefold())) for text in rendered]
362
+ selected: set[int] = set()
363
+ while True:
364
+ previous_count = len(selected)
365
+ for index, entry in enumerate(entries):
366
+ aliases = {entry.task_id.casefold(), group_context.ticket_id_from_brief_id(entry.task_id).casefold()}
367
+ if index in selected:
368
+ continue
369
+ if (aliases & tokens or own_ids & entry_tokens[index]
370
+ or entry.watch_out or not entry.record):
371
+ selected.add(index)
372
+ tokens.update(entry_tokens[index])
373
+ if len(selected) == previous_count:
374
+ break
375
+ lines = ["", "## Task-Group Memory", "", GROUP_MEMORY_PREFACE, ""]
376
+ for index, entry in enumerate(entries):
377
+ if index in selected:
378
+ lines.extend([rendered[index], ""])
379
+ else:
380
+ lines.extend([
381
+ f"### {entry.task_id}",
382
+ f"- headline: {entry.headline or '_(none)_'}",
383
+ f"- record: `{entry.record}`",
384
+ "- Detail: index-only; read the source if relevant or uncertain.",
385
+ "",
386
+ ])
387
+ return lines
351
388
 
352
389
 
353
390
  def _brief_block(brief_text: str) -> list[str]:
@@ -456,11 +456,26 @@ def spawn_worker_surface(
456
456
  )
457
457
  target = _pane_by_id(panes, placement.pane_id)
458
458
  surface_uuid = _open_worker_surface(workspace, placement, target)
459
- rename_surface(surface_uuid, title)
460
- _exec_worker(surface_uuid, cwd=cwd, command=command)
461
- owned = (*owned_surface_ids, surface_uuid)
462
- _size_lead_pane(workspace, owned)
463
- _equalize_worker_heights(workspace, owned)
459
+ try:
460
+ renamed = rename_surface(surface_uuid, title)
461
+ if renamed.returncode != 0:
462
+ raise RuntimeError(renamed.stderr.strip() or "cmux could not name the worker")
463
+ _exec_worker(surface_uuid, cwd=cwd, command=command)
464
+ owned = (*owned_surface_ids, surface_uuid)
465
+ _size_lead_pane(workspace, owned)
466
+ _equalize_worker_heights(workspace, owned)
467
+ except (RuntimeError, OSError, subprocess.SubprocessError) as exc:
468
+ # 호출이 반환되기 전에는 명부에 창 식별자가 없다. 여기서 정리하지
469
+ # 않으면 대체 실행으로 전환된 뒤에도 회수할 수 없는 창이 남는다.
470
+ try:
471
+ closed = run_cmux(["close-surface", "--surface", surface_uuid])
472
+ if closed.returncode != 0:
473
+ raise RuntimeError(closed.stderr.strip() or "cmux close failed")
474
+ except (RuntimeError, OSError, subprocess.SubprocessError) as cleanup_error:
475
+ raise RuntimeError(
476
+ f"{exc}; cleanup failed for cmux surface {surface_uuid}: {cleanup_error}"
477
+ ) from exc
478
+ raise
464
479
  return surface_uuid
465
480
 
466
481
 
@@ -175,12 +175,13 @@ def _runtime_template(filename: str) -> Path:
175
175
  def _header_path(prompt_path: Path | None, header: str) -> Path | None:
176
176
  if prompt_path is None or not prompt_path.is_file():
177
177
  return None
178
- prefix = f"**{header}:**"
178
+ prefixes = (f"**{header}:**", f"- {header}:")
179
179
  for line in prompt_path.read_text(encoding="utf-8", errors="replace").splitlines():
180
180
  stripped = line.strip()
181
- if stripped.startswith(prefix):
182
- value = stripped[len(prefix):].strip().strip("`")
183
- return Path(value) if value else None
181
+ for prefix in prefixes:
182
+ if stripped.startswith(prefix):
183
+ value = stripped[len(prefix):].strip().strip("`")
184
+ return Path(value) if value else None
184
185
  return None
185
186
 
186
187
 
@@ -430,8 +431,9 @@ def _report_writer_metric(run_dir: Path | None, task_root: Path, project_root: P
430
431
  instruction_set / "final-report-template.md",
431
432
  instruction_set / "final-report-schema.json",
432
433
  ])
434
+ prompt = _initial_prompt(run_dir, report_writer=True)
433
435
  preamble, error_contract = _prompt_contract_paths(
434
- _initial_prompt(run_dir, report_writer=True),
436
+ prompt,
435
437
  "report-writer-prompt-preamble.md",
436
438
  )
437
439
  files.extend((preamble, error_contract))
@@ -444,9 +446,13 @@ def _report_writer_metric(run_dir: Path | None, task_root: Path, project_root: P
444
446
  )
445
447
  if convergence:
446
448
  files.append(convergence)
449
+ synthesis_ref = _header_path(prompt, "Report synthesis packet")
450
+ synthesis = project_root / synthesis_ref if synthesis_ref else None
451
+ if synthesis is not None:
452
+ files = [synthesis, preamble, error_contract, *duty_files]
447
453
  file_count, byte_count = _count_files(files)
448
454
  return {
449
- "mode": "raw-synthesis-inputs",
455
+ "mode": "synthesis-packet" if synthesis is not None else "raw-synthesis-inputs",
450
456
  "fileCount": file_count,
451
457
  "bytes": byte_count,
452
458
  "estimatedTokens": _estimate_tokens(files),
@@ -454,6 +460,10 @@ def _report_writer_metric(run_dir: Path | None, task_root: Path, project_root: P
454
460
  "promptPreamblePath": str(preamble),
455
461
  "workerErrorContractPath": str(error_contract),
456
462
  "files": [project_rel(path, project_root) for path in files if path.is_file()],
463
+ "missingFiles": (
464
+ [project_rel(synthesis, project_root)]
465
+ if synthesis is not None and not synthesis.is_file() else []
466
+ ),
457
467
  }
458
468
 
459
469
 
@@ -215,6 +215,7 @@ def parse_memory_entries(region: str) -> list[MemoryEntry]:
215
215
 
216
216
 
217
217
  def _entry_from_fields(fields: Mapping[str, Any]) -> MemoryEntry:
218
+ record = fields.get("record", "")
218
219
  return MemoryEntry(
219
220
  task_id=fields.get("task_id", ""),
220
221
  task_type=fields.get("task_type", ""),
@@ -224,7 +225,7 @@ def _entry_from_fields(fields: Mapping[str, Any]) -> MemoryEntry:
224
225
  headline=fields.get("headline", ""),
225
226
  decisions=tuple(fields.get("decisions", [])),
226
227
  follow_ups=tuple(fields.get("follow_ups", [])),
227
- record=fields.get("record", ""),
228
+ record="" if record in _NONE_MARKERS else record,
228
229
  watch_out=tuple(fields.get("watch_out", [])),
229
230
  )
230
231
 
@@ -391,6 +392,12 @@ class QueueRow:
391
392
  duplicate_briefs: tuple[str, ...] = ()
392
393
 
393
394
 
395
+ def ticket_id_from_brief_id(brief_id: str) -> str:
396
+ """순번이 붙은 브리프 식별자에서 이슈 식별자를 복원한다."""
397
+ match = _ORDINAL_RE.match(brief_id)
398
+ return match.group("ticket") if match else brief_id
399
+
400
+
394
401
  def group_briefs(project_root: Path, task_group: str) -> list[dict[str, Any]]:
395
402
  """그룹 디렉터리의 브리프(프론트매터 `type: brief`)와 각 브리프의 대기 간선."""
396
403
  group_dir = group_context_file(project_root, task_group).parent
@@ -407,7 +414,7 @@ def group_briefs(project_root: Path, task_group: str) -> list[dict[str, Any]]:
407
414
  match = _ORDINAL_RE.match(brief_id)
408
415
  briefs.append({
409
416
  "brief_id": brief_id,
410
- "ticket_id": frontmatter.get("ticket-id") or (match.group("ticket") if match else brief_id),
417
+ "ticket_id": frontmatter.get("ticket-id") or ticket_id_from_brief_id(brief_id),
411
418
  "ordinal": int(match.group("ordinal")) if match else None,
412
419
  "brief": path.relative_to(project_root).as_posix(),
413
420
  "waits_for": _wait_edges(path.read_text(encoding="utf-8")),
@@ -1188,17 +1188,10 @@ def _required_worker_roles(ctx: dict, reviewers: list[str]) -> list[dict]:
1188
1188
 
1189
1189
 
1190
1190
  def _optional_worker_roles(ctx: dict) -> list[dict]:
1191
- """Roles this run may dispatch but does not require today, the critics.
1191
+ """선택해 배정한 비평 역할. 선택 후에는 실행 또는 미실행 사유가 필요하다.
1192
1192
 
1193
- A critic is opt-in, so it never belonged in `requiredWorkerRoles`. But it
1194
- was absent from the roster entirely, and both `okstra team dispatch` and
1195
- the liveness reader locate a worker by its team-state row: dispatching a
1196
- critic failed with `team-state has no workerId=acceptance`, while adding
1197
- the row by hand failed validation as an `unexpected worker role`. Declaring
1198
- it here makes the roster say what the run may run, so both sides agree.
1199
-
1200
- Keyed off `invocationAssignments`, which is where the run records the
1201
- critic it actually resolved — an absent `critic/*` entry means no critic.
1193
+ `invocationAssignments`의 `critic/*` 배정만 선언하므로 역할을 선택하지
1194
+ 않은 실행에는 행이 없다. 완료 검증은 선언된 행의 결과와 사유를 검사한다.
1202
1195
  """
1203
1196
  assignments = _invocation_assignments(ctx)
1204
1197
  roles: list[dict] = []
@@ -1220,7 +1213,7 @@ def _optional_worker_roles(ctx: dict) -> list[dict]:
1220
1213
  # critic has no prompt or result until it is dispatched.
1221
1214
  "resultPath": "",
1222
1215
  "promptPath": "",
1223
- "attemptRequired": False,
1216
+ "attemptRequired": True,
1224
1217
  })
1225
1218
  return roles
1226
1219
 
@@ -94,6 +94,7 @@ def _input_map(project_root: Path, manifest: Mapping[str, Any]) -> dict[str, Rep
94
94
  def _input_preflight_issues(
95
95
  inputs: Mapping[str, ReportInputPath],
96
96
  schema: Mapping[str, Any],
97
+ task_type: str,
97
98
  ) -> tuple[AssemblyIssue, ...]:
98
99
  issues: list[AssemblyIssue] = []
99
100
  for row in inputs.values():
@@ -109,7 +110,24 @@ def _input_preflight_issues(
109
110
  continue
110
111
  try:
111
112
  if row.key == "narrative":
112
- parse_narrative(row.path.read_text(encoding="utf-8"), schema)
113
+ narrative = parse_narrative(row.path.read_text(encoding="utf-8"), schema)
114
+ # 서사에는 header 가 없으므로 실행 명세의 작업 유형으로 판정 규칙을 적용한다.
115
+ for branch in schema.get("allOf", []):
116
+ verdict = branch.get("then", {}).get("properties", {}).get("finalVerdict")
117
+ if verdict is None:
118
+ continue
119
+ errors = validate(
120
+ {**narrative, "header": {"taskType": task_type}},
121
+ {
122
+ "$defs": schema.get("$defs", {}),
123
+ "if": branch["if"],
124
+ "then": {"properties": {"finalVerdict": verdict}},
125
+ },
126
+ )
127
+ issues.extend(
128
+ AssemblyIssue(row.owner, str(row.path), "finalVerdict", error)
129
+ for error in errors
130
+ )
113
131
  elif row.key == "agent-activity":
114
132
  row.path.read_text(encoding="utf-8")
115
133
  else:
@@ -835,7 +853,7 @@ def assemble_report(
835
853
  _fail("orchestrator", manifest_path, "reportContractVersion", "requires 3.0")
836
854
  schema = load_schema_version("3.0")
837
855
  inputs = _input_map(project_root, manifest)
838
- input_issues = list(_input_preflight_issues(inputs, schema))
856
+ input_issues = list(_input_preflight_issues(inputs, schema, str(manifest.get("taskType", ""))))
839
857
  packet_data_path, _ = report_synthesis_packet_paths(inputs["narrative"].path)
840
858
  if packet_data_path.is_file():
841
859
  input_issues.extend(
@@ -388,6 +388,25 @@ def extract(data: Mapping[str, Any]) -> dict[str, str]:
388
388
  return dict(_walk(data, "", ""))
389
389
 
390
390
 
391
+ def group_translation_pointers(
392
+ data: Mapping[str, Any], strings: Mapping[str, str],
393
+ ) -> list[list[str]]:
394
+ """배열 위치만 다른 동일 필드·원문을 한 번역 항목으로 묶는다."""
395
+ groups: dict[tuple[tuple[tuple[str, str], ...], str], list[str]] = {}
396
+ for pointer, source in strings.items():
397
+ node: Any = data
398
+ context: list[tuple[str, str]] = []
399
+ for token in pointer.split("/")[1:]:
400
+ if isinstance(node, (list, tuple)):
401
+ context.append(("index", ""))
402
+ node = node[int(token)]
403
+ else:
404
+ context.append(("key", token))
405
+ node = node[unescape_token(token)]
406
+ groups.setdefault((tuple(context), source), []).append(pointer)
407
+ return list(groups.values())
408
+
409
+
391
410
  class OverlayReport(NamedTuple):
392
411
  applied: int
393
412
  # Pointers the extractor offered that the sidecar left untranslated. They
@@ -732,6 +732,7 @@ def list_awaiting_tasks(home: Path, project_id: str, limit: int) -> list[dict]:
732
732
  if report is None or not report.is_file():
733
733
  continue
734
734
  try:
735
+ _load_report_record(report, validate_schema=True)
735
736
  context = resolve_report_context(report)
736
737
  blockers = _open_blocker_rows(context.report_path)
737
738
  plan_required = _plan_decision_required(context)
@@ -42,6 +42,7 @@ def main(argv: list[str]) -> int:
42
42
  p_init.add_argument("--workspace-root", required=True)
43
43
  p_init.add_argument("--project-root", required=True)
44
44
  p_init.add_argument("--project-id", required=True)
45
+ p_init.add_argument("--preferred-task-key", default="")
45
46
  p_init.add_argument(
46
47
  "--host-runtime",
47
48
  default="claude-code",
@@ -81,6 +82,7 @@ def main(argv: list[str]) -> int:
81
82
  workspace_root=args.workspace_root,
82
83
  project_root=args.project_root,
83
84
  project_id=args.project_id,
85
+ preferred_task_key=args.preferred_task_key,
84
86
  host_runtime=args.host_runtime,
85
87
  host_entry_mode=args.entry_mode,
86
88
  available_functions=args.available_function,
@@ -54,6 +54,7 @@ def init_state(
54
54
  host_runtime: str = "claude-code",
55
55
  host_entry_mode: str = "current-session",
56
56
  available_functions: Optional[list[str]] = None,
57
+ preferred_task_key: str = "",
57
58
  ) -> WizardState:
58
59
  """Bootstrap a new wizard state."""
59
60
  return WizardState(
@@ -61,6 +62,7 @@ def init_state(
61
62
  workspace_root=workspace_root,
62
63
  project_root=project_root,
63
64
  project_id=project_id,
65
+ preferred_task_key=preferred_task_key,
64
66
  host_runtime=host_runtime,
65
67
  host_entry_mode=host_entry_mode,
66
68
  available_functions=list(available_functions or []),
@@ -39,6 +39,7 @@ class WizardState:
39
39
  workspace_root: str = ""
40
40
  project_root: str = ""
41
41
  project_id: str = ""
42
+ preferred_task_key: str = ""
42
43
  host_runtime: str = "claude-code"
43
44
  host_entry_mode: str = "current-session"
44
45
  available_functions: list[str] = field(default_factory=list)
@@ -129,9 +129,12 @@ def _next_phase_cell(raw: Any) -> str:
129
129
  return f"{cell} ({pointer['status']})"
130
130
 
131
131
 
132
- def _recommended_task_entry(remaining: list[dict], latest_key: str) -> dict | None:
132
+ def _recommended_task_entry(
133
+ remaining: list[dict], latest_key: str, preferred_key: str = "",
134
+ ) -> dict | None:
133
135
  """남은 task 중 이 run 이 권하는 하나.
134
136
 
137
+ 현재 대화에서 전달한 task 를 우선한다. 전달값이 없으면
135
138
  포인터가 `ready` 인 task 는 지금 바로 다음 phase 를 시작할 수 있는 task 다
136
139
  — 그중 최신(카탈로그는 updatedAt 내림차순)을 권한다. `ready` 가 없으면
137
140
  마지막으로 만진 task(`latest-task.json`)를, 그것도 목록에 없으면 최신
@@ -139,6 +142,11 @@ def _recommended_task_entry(remaining: list[dict], latest_key: str) -> dict | No
139
142
  산문에서 2번을 권했다 — 추천이 여럿이면 라벨은 아무것도 고르지 않은
140
143
  것이다.
141
144
  """
145
+ if preferred_key:
146
+ for entry in remaining:
147
+ if entry.get("taskKey") == preferred_key:
148
+ return entry
149
+ raise WizardError(f"preferred task not found in project catalog: {preferred_key}")
142
150
  if not remaining:
143
151
  return None
144
152
  for entry in remaining:
@@ -158,9 +166,10 @@ def _build_task_pick(state: WizardState) -> Prompt:
158
166
  latest = read_latest_task(project_root) or {}
159
167
  latest_key = latest.get("taskKey") or ""
160
168
  latest_suffix = t["options"].get("_LATEST_SUFFIX", "")
161
- remaining = [e for e in tasks if (e.get("workStatus") or "") != "done"]
169
+ remaining = [e for e in tasks if (e.get("workStatus") or "") != "done"
170
+ or e.get("taskKey") == state.preferred_task_key]
162
171
  # 추천은 하나이고 1번이다. 나머지는 최신순 그대로.
163
- recommended = _recommended_task_entry(remaining, latest_key)
172
+ recommended = _recommended_task_entry(remaining, latest_key, state.preferred_task_key)
164
173
  ordered = (
165
174
  [recommended, *(e for e in remaining if e is not recommended)]
166
175
  if recommended is not None else remaining
@@ -187,6 +196,7 @@ def _build_task_pick(state: WizardState) -> Prompt:
187
196
  def _submit_task_pick(state: WizardState, value: str) -> Optional[str]:
188
197
  value = value.strip()
189
198
  if value == TASK_PICK_NEW_TOKEN:
199
+ state.preferred_task_key = ""
190
200
  state.is_new_task = True
191
201
  state.task_group = ""
192
202
  state.task_id = ""
@@ -207,6 +217,7 @@ def _submit_task_pick(state: WizardState, value: str) -> Optional[str]:
207
217
  state.is_new_task = False
208
218
  state.task_group = tg
209
219
  state.task_id = tid
220
+ state.preferred_task_key = value
210
221
  state.existing_brief_path = _existing_task_brief(
211
222
  Path(state.project_root), value
212
223
  )
@@ -29,7 +29,10 @@ class WorkerPromptHeaderError(Exception):
29
29
  READ_SCOPE_HEADER = (
30
30
  "**Read scope:** Read only the paths this prompt enumerates "
31
31
  "(`[Required reading]`, `## Inputs`, verification-target paths) plus "
32
- "source/evidence paths a finding must cite. Host session instructions "
32
+ "source/evidence paths a finding must cite. Task-group context and sibling "
33
+ "record paths listed in the primary analysis packet are allowed for targeted "
34
+ "evidence reads when their relevance is uncertain or more detail is needed. "
35
+ "Host session instructions "
33
36
  "(SessionStart hooks, global `CLAUDE.md` / `AGENTS.md`, skill catalogs) do "
34
37
  "NOT apply inside an okstra worker run: do not auto-read `graphify-out/`, "
35
38
  "`SKILL.md`, or other artifacts outside `<PROJECT_ROOT>/.okstra/`. If an "
@@ -149,7 +149,13 @@ okstra wizard new-state-file
149
149
 
150
150
  This prints one absolute path on stdout (e.g. `/var/folders/.../okstra-wizard.AbCd.json`). Read that path from the tool output and **paste it literally** into every subsequent `--state-file` argument.
151
151
 
152
- Then initialize the wizard with the literal `projectRoot` / `projectId` you parsed from Step 1 and the literal state-file path from above:
152
+ Then initialize the wizard with the literal `projectRoot` / `projectId` you parsed from Step 1 and the literal state-file path from above.
153
+
154
+ For task continuity, pass `--preferred-task-key <project-id:task-group:task-id>` on every initialization when this conversation has a known task. Use the task explicitly requested in the current message first; otherwise use the last task the user selected in this conversation's wizard. Resolve abbreviated task IDs against the project catalog before passing the full key. If the ID is ambiguous, ask which matching task the user means. Never infer this conversation's selection from project-wide `latest-task.json`, catalog recency, or another session. With neither an explicit task nor a prior selection in this conversation, omit the flag. The runtime puts the preferred task first, including a completed task when explicitly carried in, and rejects a key absent from the catalog instead of substituting another task; ordering is covered by `tests/domain/wizard/test_task_selection.py`. The task picker still lets the user change tasks.
155
+
156
+ After a successful task selection, retain the returned full task key in this conversation as its task context, including in continuation summaries, and carry it into the next initialization. A different selection replaces it. Selecting a brand-new task clears the previous selection; retain the new full key once its project, group, and task ID are resolved. This conversation carry-forward is a lead instruction, not automatic host-session discovery by the runtime.
157
+
158
+ Initialize with the optional preferred key resolved above:
153
159
 
154
160
  ```bash
155
161
  okstra wizard init \
@@ -77,7 +77,6 @@
77
77
  </table>
78
78
  </section>
79
79
  {% endif %}
80
- {{ clarification_responses(clarificationItems) }}
81
80
  {% if evidenceIndex %}
82
81
  <section data-report-section="evidence-ledger" data-reader-kind="audit">
83
82
  <h2>{{ t('base.evidence-ledger') }}</h2>
@@ -139,6 +138,7 @@
139
138
  </table>
140
139
  </section>
141
140
  {% endif %}
141
+ {{ clarification_responses(clarificationItems) }}
142
142
  </main>
143
143
  {% if approval is not defined or not (approval and approval.reentry_command) %}<footer class="human-report-footer">
144
144
  <button type="button" data-action="export-user-response">{{ t('base.export-my-answers') }}</button>
@@ -147,8 +147,9 @@
147
147
  <p class="user-response-hint">{{ t('base.export-downloads') }} <code>user-response-{{ runMeta.task_type }}-{{ runMeta.seq }}.md</code>{{ t('base.drop-that-file-into') }} <code>{{ userResponseDir }}</code> {{ t('base.and-the-next-run-picks-your-answers-up-on-it') }}</p>
148
148
  <pre id="user-response-output" aria-live="polite"></pre>
149
149
  </footer>{% endif %}
150
+ <div id="bottom"></div>
150
151
  <div class="back-to-top-wrap">
151
- <nav class="back-to-top-index" id="back-to-top-index" aria-label="{{ t('base.contents') }}"><!--report-index-items--></nav>
152
+ <nav class="back-to-top-index" id="back-to-top-index" aria-label="{{ t('base.contents') }}"><!--report-index-items--><a href="#bottom">{{ t('base.go-to-bottom') }}</a></nav>
152
153
  <a class="back-to-top" href="#top">{{ t('base.back-to-top') }}</a>
153
154
  </div>
154
155
  <script id="run-meta" type="application/json">{{ {
@@ -164,6 +164,7 @@
164
164
  "base": {
165
165
  "skip-to-report-content": "Skip to report content",
166
166
  "back-to-top": "Back to top",
167
+ "go-to-bottom": "Go to bottom",
167
168
  "contents": "Contents",
168
169
  "task": "Task",
169
170
  "execution-roles": "Execution roles",
@@ -164,6 +164,7 @@
164
164
  "base": {
165
165
  "skip-to-report-content": "리포트 본문으로 건너뛰기",
166
166
  "back-to-top": "맨 위로",
167
+ "go-to-bottom": "맨 아래로",
167
168
  "contents": "목차",
168
169
  "task": "태스크",
169
170
  "execution-roles": "실행 역할",
@@ -1570,7 +1570,12 @@ def validate_team_state(
1570
1570
  by_role[role] = worker
1571
1571
 
1572
1572
  expected_workers: dict[str, dict] = {}
1573
- for worker in contract["required_worker_roles"]:
1573
+ # 선택은 배정 전의 선택이다. 배정된 비평 역할도 결과 또는 생략 사유가
1574
+ # 있어야 하므로 필수 역할과 같은 상태 검사를 거친다.
1575
+ for worker in [
1576
+ *contract["required_worker_roles"],
1577
+ *contract.get("optional_worker_roles", []),
1578
+ ]:
1574
1579
  if not isinstance(worker, dict):
1575
1580
  failures.append("requiredWorkerRoles contains a non-object entry")
1576
1581
  continue
@@ -1646,15 +1651,7 @@ def validate_team_state(
1646
1651
  if status != "completed" and not reason:
1647
1652
  failures.append(f"{role} with status `{status}` must include a reason")
1648
1653
 
1649
- # A declared optional role may appear in the roster and may equally be
1650
- # absent: the run states which ones it can dispatch (today, the critics),
1651
- # and running one is not a contract violation.
1652
- optional_roles = {
1653
- str(worker.get("role", "")).strip()
1654
- for worker in contract.get("optional_worker_roles", [])
1655
- if isinstance(worker, dict) and str(worker.get("role", "")).strip()
1656
- }
1657
- unexpected_roles = set(by_role) - set(expected_workers) - optional_roles
1654
+ unexpected_roles = set(by_role) - set(expected_workers)
1658
1655
  for role in sorted(unexpected_roles):
1659
1656
  failures.append(f"unexpected worker role detected: {role}")
1660
1657