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.
- package/dist/commands/execute/wizard.mjs +6 -0
- package/dist/commands/execute/wizard.mjs.map +1 -1
- package/package.json +1 -1
- package/runtime/BUILD.json +2 -2
- package/runtime/bin/okstra-report-translate.py +28 -8
- package/runtime/prompts/lead/convergence.md +17 -0
- package/runtime/prompts/lead/okstra-lead-contract.md +31 -5
- package/runtime/prompts/lead/plan-body-verification.md +29 -10
- package/runtime/python/okstra_ctl/adapters/hosts/grok/relay.md +5 -3
- package/runtime/python/okstra_ctl/analysis_packet.py +49 -12
- package/runtime/python/okstra_ctl/cmux.py +20 -5
- package/runtime/python/okstra_ctl/context_cost.py +16 -6
- package/runtime/python/okstra_ctl/group_context.py +9 -2
- package/runtime/python/okstra_ctl/render.py +4 -11
- package/runtime/python/okstra_ctl/report_assembly.py +20 -2
- package/runtime/python/okstra_ctl/report_translation.py +19 -0
- package/runtime/python/okstra_ctl/user_response.py +1 -0
- package/runtime/python/okstra_ctl/wizard/cli.py +2 -0
- package/runtime/python/okstra_ctl/wizard/engine.py +2 -0
- package/runtime/python/okstra_ctl/wizard/state.py +1 -0
- package/runtime/python/okstra_ctl/wizard/steps_identity.py +14 -3
- package/runtime/python/okstra_ctl/worker_prompt_headers.py +4 -1
- package/runtime/skills/okstra-run/SKILL.md +7 -1
- package/runtime/templates/reports/html/base.template.html +3 -2
- package/runtime/templates/reports/html/i18n/en.json +1 -0
- package/runtime/templates/reports/html/i18n/ko.json +1 -0
- 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":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,SAAS,CAAC;AACtC,OAAO,EAAE,MAAM,EAAE,MAAM,SAAS,CAAC;AACjC,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAEjC,OAAO,EAAE,eAAe,EAAE,MAAM,6BAA6B,CAAC;AAC9D,OAAO,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AAGnD,MAAM,KAAK,GAAG
|
|
1
|
+
{"version":3,"file":"wizard.mjs","sourceRoot":"","sources":["../../../src/commands/execute/wizard.mts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
package/runtime/BUILD.json
CHANGED
|
@@ -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
|
|
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":
|
|
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
|
-
|
|
158
|
-
|
|
159
|
-
|
|
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(
|
|
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(
|
|
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 =
|
|
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
|
|
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
|
|
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
|
|
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
|
-
##
|
|
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
|
-
"
|
|
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
|
-
"
|
|
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
|
-
"
|
|
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(
|
|
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
|
-
|
|
336
|
-
|
|
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
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
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
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
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
|
-
|
|
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
|
-
|
|
182
|
-
|
|
183
|
-
|
|
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
|
-
|
|
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=
|
|
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 (
|
|
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
|
-
"""
|
|
1191
|
+
"""선택해 배정한 비평 역할. 선택 후에는 실행 또는 미실행 사유가 필요하다.
|
|
1192
1192
|
|
|
1193
|
-
|
|
1194
|
-
|
|
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":
|
|
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(
|
|
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.
|
|
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
|
|
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">{{ {
|
|
@@ -1570,7 +1570,12 @@ def validate_team_state(
|
|
|
1570
1570
|
by_role[role] = worker
|
|
1571
1571
|
|
|
1572
1572
|
expected_workers: dict[str, dict] = {}
|
|
1573
|
-
|
|
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
|
-
|
|
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
|
|