okstra 0.198.1 → 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/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/analysis_packet.py +49 -12
- 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/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/worker_prompt_headers.py +4 -1
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.
|
|
@@ -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]:
|
|
@@ -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")),
|
|
@@ -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)
|
|
@@ -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 "
|