okstra 0.200.1 → 0.201.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (96) hide show
  1. package/README.md +4 -2
  2. package/dist/cli-registry.mjs +6 -0
  3. package/dist/cli-registry.mjs.map +1 -1
  4. package/docs/cli.md +24 -4
  5. package/package.json +1 -1
  6. package/runtime/BUILD.json +2 -2
  7. package/runtime/agents/workers/report-writer-worker.md +7 -3
  8. package/runtime/bin/okstra-spawn-followups.py +2 -2
  9. package/runtime/prompts/duties/technical-verification-worker.md +44 -0
  10. package/runtime/prompts/launch.template.md +7 -1
  11. package/runtime/prompts/lead/okstra-lead-contract.md +7 -2
  12. package/runtime/prompts/lead/plan-body-verification.md +3 -1
  13. package/runtime/prompts/lead/report-writer.md +8 -2
  14. package/runtime/prompts/lead/team-contract.md +6 -0
  15. package/runtime/prompts/profiles/_implementation-verifier.md +7 -1
  16. package/runtime/prompts/profiles/final-verification.md +5 -0
  17. package/runtime/prompts/profiles/forbidden-actions.json +6 -0
  18. package/runtime/prompts/profiles/implementation-option-selection.md +7 -1
  19. package/runtime/prompts/profiles/implementation-planning.md +1 -0
  20. package/runtime/prompts/profiles/technical-verification.md +53 -0
  21. package/runtime/prompts/wizard/prompts.ko.json +2 -1
  22. package/runtime/python/okstra_ctl/adapters/hosts/claude-code/relay.md +4 -4
  23. package/runtime/python/okstra_ctl/adapters/hosts/codex/relay.md +2 -0
  24. package/runtime/python/okstra_ctl/adapters/providers/zai/adapter.py +42 -9
  25. package/runtime/python/okstra_ctl/agent/invocation.py +14 -6
  26. package/runtime/python/okstra_ctl/agent/prompt_cli/cli.py +4 -3
  27. package/runtime/python/okstra_ctl/agent/prompt_cli/corrections.py +83 -22
  28. package/runtime/python/okstra_ctl/agent/prompt_cli/materialize.py +44 -2
  29. package/runtime/python/okstra_ctl/conformance.py +2 -20
  30. package/runtime/python/okstra_ctl/dispatch_core.py +25 -5
  31. package/runtime/python/okstra_ctl/dispatch_state.py +2 -0
  32. package/runtime/python/okstra_ctl/domain/provider.py +0 -1
  33. package/runtime/python/okstra_ctl/domain/role.py +1 -0
  34. package/runtime/python/okstra_ctl/execution_mutation_audit.py +6 -1
  35. package/runtime/python/okstra_ctl/implementation_direction.py +64 -7
  36. package/runtime/python/okstra_ctl/implementation_options.py +58 -45
  37. package/runtime/python/okstra_ctl/model_pool.py +2 -5
  38. package/runtime/python/okstra_ctl/next_phase.py +3 -0
  39. package/runtime/python/okstra_ctl/plan_items.py +15 -0
  40. package/runtime/python/okstra_ctl/plan_items_cli.py +9 -3
  41. package/runtime/python/okstra_ctl/qa_commands.py +30 -0
  42. package/runtime/python/okstra_ctl/registry/provider_registry.py +11 -8
  43. package/runtime/python/okstra_ctl/render.py +3 -0
  44. package/runtime/python/okstra_ctl/render_final_report.py +1 -0
  45. package/runtime/python/okstra_ctl/report_assembly.py +8 -2
  46. package/runtime/python/okstra_ctl/report_contract.py +3 -0
  47. package/runtime/python/okstra_ctl/report_corrections.py +209 -93
  48. package/runtime/python/okstra_ctl/report_finalize.py +25 -8
  49. package/runtime/python/okstra_ctl/report_html/router.py +2 -0
  50. package/runtime/python/okstra_ctl/report_html/view_models/technical_verification.py +21 -0
  51. package/runtime/python/okstra_ctl/report_projections.py +4 -3
  52. package/runtime/python/okstra_ctl/report_synthesis_packet.py +178 -26
  53. package/runtime/python/okstra_ctl/run.py +82 -0
  54. package/runtime/python/okstra_ctl/team.py +4 -1
  55. package/runtime/python/okstra_ctl/technical_verification.py +195 -0
  56. package/runtime/python/okstra_ctl/usage_identity.py +54 -0
  57. package/runtime/python/okstra_ctl/usage_report.py +22 -8
  58. package/runtime/python/okstra_ctl/verification_target.py +74 -0
  59. package/runtime/python/okstra_ctl/wizard/__init__.py +1 -1
  60. package/runtime/python/okstra_ctl/wizard/cli.py +2 -1
  61. package/runtime/python/okstra_ctl/wizard/confirmation.py +38 -2
  62. package/runtime/python/okstra_ctl/wizard/engine.py +3 -0
  63. package/runtime/python/okstra_ctl/wizard/ids.py +1 -0
  64. package/runtime/python/okstra_ctl/wizard/outcome.py +63 -0
  65. package/runtime/python/okstra_ctl/wizard/picker_navigation.py +2 -2
  66. package/runtime/python/okstra_ctl/wizard/registry.py +1 -1
  67. package/runtime/python/okstra_ctl/wizard/render.py +8 -55
  68. package/runtime/python/okstra_ctl/wizard/roles.py +11 -7
  69. package/runtime/python/okstra_ctl/wizard/sources.py +28 -2
  70. package/runtime/python/okstra_ctl/wizard/state.py +13 -6
  71. package/runtime/python/okstra_ctl/wizard/steps_plan.py +8 -0
  72. package/runtime/python/okstra_ctl/worker_liveness.py +52 -39
  73. package/runtime/python/okstra_ctl/worker_prompt_policy.py +2 -0
  74. package/runtime/python/okstra_ctl/workflow.py +8 -0
  75. package/runtime/python/okstra_ctl/write_policy.py +23 -0
  76. package/runtime/python/okstra_token_usage/blocks.py +50 -1
  77. package/runtime/python/okstra_token_usage/claude.py +42 -21
  78. package/runtime/python/okstra_token_usage/codex.py +17 -0
  79. package/runtime/python/okstra_token_usage/collect.py +307 -164
  80. package/runtime/python/okstra_token_usage/cursor.py +2 -3
  81. package/runtime/python/okstra_token_usage/pricing.py +1 -0
  82. package/runtime/python/okstra_token_usage/report.py +35 -30
  83. package/runtime/python/okstra_token_usage/task_totals.py +3 -12
  84. package/runtime/schemas/final-report-v2.0.schema.json +298 -7
  85. package/runtime/schemas/final-report-v3.0.schema.json +298 -7
  86. package/runtime/schemas/report-narrative-v3.0.schema.json +1 -0
  87. package/runtime/schemas/report-writer-corrections-v1.0.schema.json +30 -3
  88. package/runtime/skills/okstra-run/SKILL.md +10 -2
  89. package/runtime/skills/okstra-setup/SKILL.md +42 -7
  90. package/runtime/templates/report-writer-prompt-preamble.md +7 -3
  91. package/runtime/templates/reports/html/i18n/en.json +11 -0
  92. package/runtime/templates/reports/html/i18n/ko.json +11 -0
  93. package/runtime/templates/reports/html/tasks/implementation-option-selection.template.html +7 -3
  94. package/runtime/templates/reports/html/tasks/technical-verification.template.html +35 -0
  95. package/runtime/templates/reports/md/tasks/technical-verification.template.md +5 -0
  96. package/runtime/validators/validate-run.py +9 -4
@@ -49,7 +49,7 @@ roles:
49
49
  - Map every displayed candidate or preselected direction to the stable brief end-state IDs it satisfies, preserves, or leaves unresolved.
50
50
  - **Close the vote gaps before you conclude `blocked`.** Round 1 runs the designers in parallel, so each one votes only on the candidates it proposed and the merged set ends up with a different hole per analyser. A candidate that is otherwise sound then fails the every-analyser clause and drops out of the ranking — three such candidates blocked a run whose comparison had in fact converged (2026-09-10, dev-10629-4: IO-001, IO-002 and IO-003 each held two `feasible` votes and each was missing a different designer). Run `okstra option-votes gaps --task-manifest <taskManifestPath> --narrative <report writer narrative>` before assembly. For each analyser it names, dispatch one vote-completion assignment asking for that analyser's own feasibility verdict, rationale, and counterevidence on the named candidate — no new candidate, so the run stays in `candidate-comparison` mode. The command reports no gap when a vote cannot settle the block (safety blockers, unresolved feasibility facts, too few feasible verdicts); that is the honest `blocked`.
51
51
  - Clarification request policy (phase-specific addenda — shared policy is in `_common-contract.md`):
52
- - **A blocked run must leave an answer channel.** `routing: blocked` is this phase's only end state with no destination, and its usual cause is `unresolvedFeasibilityFacts` an undecided value, a missing payload contract, an unconfirmed reporter intent. Those are the user's to settle, and the only channel that reaches them is `clarificationItems[]`. Before assembly, open one `Kind=decision, Blocks=next-phase` row per answerable fact. A fact left in `humanSummary.blockers`, `selectionGuidance`, or a follow-up task reaches no answer channel: `okstra user-response` shows the user zero questions on a run that is waiting for their answer. **Enforced:** `scripts/okstra_ctl/implementation_options.py` `validate_blocked_answer_channel` fails a `blocked` report whose candidates carry unresolved facts while no clarification row is open. A block with no unresolved facts every analyser's result missing, for instance has nothing for the user to answer and is exempt.
52
+ - Classify every `unresolvedFeasibilityFacts` entry with `resolutionKind`: `user-decision` requires nonempty `clarificationRefs` naming the actual C-NNN records for that fact; `technical-verification` requires source review, design evidence, or experiments and does not require a user question. Preserve `fact`, `whyItMatters`, and `evidence`. Reuse answered questions and their recorded dispositions; never reopen one merely to save the report. An unrelated open question does not satisfy a missing reference. Historical unclassified entries remain readable, but blocked report reassembly requires the writer to classify them explicitly. Enforced by `validate_blocked_answer_channel` in `scripts/okstra_ctl/implementation_options.py`, called by report assembly and `validate-run.py`. Saving preserves `routing: blocked` and does not make a candidate valid. In `humanSummary.actions`, `verdictCard.nextStep`, and selection guidance, distinguish pending user decisions from remaining technical verification; after the pilot route is answered, carry the selected route and remove the old answer-waiting instruction.
53
53
  {{INCLUDE:_clarification-recommendation.md}}
54
54
  - Cross-verification mode:
55
55
  - Phase 5.5 convergence runs in adversarial mode (`convergence.adversarial=true`).
@@ -57,3 +57,9 @@ roles:
57
57
  - source or configuration edits, tests, builds, migrations, deployments, or other state-mutating commands
58
58
  - detailed implementation planning, file-change specifications, stage maps, execution commands, or user approval
59
59
  - starting `implementation-planning` or any other lifecycle phase inside this run
60
+
61
+ ## Technical evidence loop
62
+
63
+ When no candidate is valid and explicit eligible technical facts remain, route to `technical-verification` to collect experimental evidence. Keep `rankedOptions` empty and `recommendedOptionId` null. `validate_blocked_answer_channel` rejects this route while user decisions remain unresolved or no safe, explicitly classified technical fact is available. Historical blocked reports can be supplied explicitly without rewriting their verdict.
64
+
65
+ When `--clarification-response` carries a technical-verification report, read its source comparison, experiment plans, results and logs. Check that the tested environment matches current code, then author fresh feasibility votes. Do not directly convert a supported result into a feasible candidate, remove unrelated uncertainties or treat a failed or unrun probe as compatibility proof. Preserve the user's pilot scope and any deferred production rollout.
@@ -220,6 +220,7 @@ roles:
220
220
  - **Why enumeration is the scale gate.** The number of stages a plan carries is not bounded by any threshold — a genuinely large requirement may need many, and okstra does not guess a ratio. What IS bounded is how cheaply a plan can *claim* coverage of them: one `Stages 1-64` cell used to satisfy the reverse check for the whole map while the planner confirmed nothing, so scale grew for free. Enumeration prices it — every stage you claim costs you the act of naming it and asking whether this requirement is really satisfied there. A plan that cannot bring itself to type the numbers is telling you the stages are not all needed. The typing is the confirmation, so do not batch it mechanically: a row listing `Stages 1, 2, 3, ..., 12` you did not check one by one is the same rubber stamp with more characters.
221
221
  - Because that reader only sees prose, it still cannot tell a citation from a mention: `Stage 1 (superseded by Stage 2)` counts Stage 1 as cited. Cite the stages a requirement is actually satisfied by, not stages merely mentioned.
222
222
  - **Requirement Coverage (mandatory, §5.5.8):** selected-direction plans preserve the original requirement IDs and link each row to `stageRefs`, `stepRefs`, `validationRefs`, and `fileRefs`; exact forward and reverse coverage is enforced by `validate_selected_direction_plan`. Legacy candidate-comparison plans retain one `R-NNN` row per concrete requirement and the existing Option Candidate plus Stage/Step `coveredBy` semantics. The exact `P-Req-*` queue comes from `scripts/okstra_ctl/plan_items.py` in both branches.
223
+ - Selected-direction requirements assigned to another project use `status: externally-tracked` with `crossProjectDependencyRefs` pointing to complete, unique `XP-*` rows. Their local stage/step/validation/file references may be empty; mixed requirements retain their actual local references. This counts planned responsibility, never completed work: preserve unresolved execution evidence in `endStateCoverage`. Missing, duplicate or incomplete dependencies fail `implementation_direction._coverage_reference_errors`; schema validation requires external references and keeps all local references mandatory for local-only coverage.
223
224
  - **Every coverage and validation row states the stages it belongs to.** Put the stage numbers in `stageRefs` on each `### Requirement Coverage` row and each `### Validation Checklist` row — the same integers the row's `Covered by` prose or its check already names. `coveredBy` stays as it is; this is the machine-readable form of the same fact.
224
225
 
225
226
  Prose is not a substitute. Reading a stage out of `Covered by` needs a regular expression over a sentence, and a gate cannot be scored on that. Without the field, every requirement and validation row counts against whatever stage is being started, including rows that belong only to stages already finished or not yet begun — measured on one run, that is 9 of 13 blockers, 6 of them on frozen stages no amount of planning can close.
@@ -0,0 +1,53 @@
1
+ # Technical Verification Profile
2
+
3
+ ```yaml
4
+ roles:
5
+ - role: analyser
6
+ min: 2
7
+ recommended: 2
8
+ max: 5
9
+ duty: technical-verification-worker
10
+ - role: critic
11
+ min: 0
12
+ recommended: 1
13
+ max: 1
14
+ duty: scope-critic
15
+ - role: report-writer
16
+ min: 1
17
+ recommended: 1
18
+ max: 1
19
+ duty: report-writer
20
+ - role: verifier
21
+ min: 0
22
+ recommended: 0
23
+ max: 0
24
+ duty: reverification-worker
25
+ dynamic: true
26
+ ```
27
+
28
+ - Purpose: collect experimental evidence for unresolved technical facts before a new implementation comparison.
29
+ - Required workers:
30
+ - claude
31
+ - codex
32
+ - report-writer
33
+ {{INCLUDE:_common-contract.md}}
34
+
35
+ ## Input and experiment procedure
36
+
37
+ 1. Read the frozen technical input linked in the analysis packet. Preserve the source hash, candidate IDs and fact identities. `resolve_technical_verification_input` enforces same-task source ownership, schema validity, eligible technical facts and unresolved user-decision gates.
38
+ 2. Before executing a probe, write its plan under `experiments/<seq>/<worker-id>/`: hypothesis, procedure, commands, confirming and rejecting signals, environment, and inconclusive criteria. Retain the plan with the evidence.
39
+ 3. Create a separate source copy in that directory. Record the source commit and any uncommitted input differences. Read the project and task worktree as inputs; perform source edits, installations and builds only in your own copy. Do not share writable dependency directories or use real credentials. Use synthetic local accounts for request-boundary experiments.
40
+ 4. Preserve the baseline lockfile. When the hypothesis needs a deliberate dependency change, retain the resulting lockfile and diff, then use a frozen install against that experimental lockfile. Record command, working directory, stdout/stderr and exit code in run-local logs. Distinguish infrastructure failure from a falsified compatibility claim.
41
+ 5. Classify each fact as `supported`, `refuted`, `inconclusive` or `not-run` using the declared signals. A zero exit code alone does not establish compatibility. Record limitations and all failed or unavailable probes.
42
+ 6. During convergence, inspect plans and logs; rerun disputed probes in a separate copy when feasible. An independent report is not a substitute for command evidence.
43
+
44
+ Copy boundaries and pre-execution plans are worker instructions reviewed during convergence. The report validator checks fact completeness, run-local log files, experiment working directories and zero exit codes for supported results. It does not sandbox arbitrary shell commands or prove that a declared command produced its log.
45
+
46
+ ## Deliverable and handoff
47
+
48
+ Copy `sourceReport`, `sourceDataSha256` and `scope` from the frozen input into `technicalVerification`. Emit exactly one check per fact, preserving its `id`, `candidateId`, `factIndex` and `fact`. Each check records its hypothesis, procedure, signals, command evidence, observation, status and limitations. Use project-relative evidence paths. Mark contested results inconclusive.
49
+
50
+ Route only to `implementation-option-selection`, with one matching phase-continuation follow-up. The next comparison consumes this report through `--clarification-response` and authors fresh feasibility votes. Do not edit the source comparison, clear unrelated uncertainties, rank candidates, approve implementation, merge code or expand the user's pilot scope.
51
+
52
+ `validate_technical_verification_report` checks frozen-input identity and evidence before publication and during run validation. The report schema restricts the routing target and requires `frontmatter.approved=false`.
53
+ {{INCLUDE:_coverage-critic.md}}
@@ -620,7 +620,7 @@
620
620
  "echo_template": "edit-target: {value}"
621
621
  },
622
622
  "role_models": {
623
- "label": "{role} 역할의 모델을 고르세요 — 고른 만큼 인스턴스를 띄웁니다 ({range}). 앞줄 추천은 프로젝트 modelDefaults(없으면 카탈로그 기본값) 순서입니다",
623
+ "label": "{role} 역할의 모델을 고르세요 — 고른 만큼 인스턴스를 띄웁니다 ({range}). 제공자별로 표시하며 추천은 프로젝트 modelDefaults(없으면 카탈로그 기본값) 따릅니다",
624
624
  "echo_template": "role-models: {value}",
625
625
  "options": {
626
626
  "model": "{model_ref} — {display}",
@@ -646,6 +646,7 @@
646
646
  },
647
647
  "confirmation": {
648
648
  "header": "선택 확인:",
649
+ "translation_scope": " 번역 작업자: {model} — 보고서 작성자와 같은 제공자·모델을 사용합니다. 선택 언어의 보고서가 필요하면 보고서 본문과 관련 분석·검증 결과를 전달해 번역하고 최종 보고서를 생성합니다. 같은 작업 범위의 재검증·수정·재시도에도 위에서 선택한 제공자·모델을 사용합니다.",
649
650
  "provider_data_scope": "\n전달 대상: 위 역할·모델 목록의 제공자(현재 세션 및 선택한 외부 모델 제공자).\n전달 자료: `{project_root}`의 이 작업에 대한 작업 개요, 작업 수행에 필요한 저장소 소스·문서, 선택한 근거 자료 및 실행 중 생성되는 관련 분석·검증 결과.\n진행을 선택하면 위 대상에 해당 자료를 전달하여 선택한 작업을 실행하는 것을 승인합니다. 선택에 없는 제공자나 작업과 무관한 자료는 승인 범위에 포함되지 않습니다. 실행 환경의 권한 검토는 별도로 적용됩니다.",
650
651
  "static_role": " static-role : {role}#{ordinal} / {model}",
651
652
  "dynamic_role": " dynamic-role : {role} / reuse selected participant model",
@@ -196,17 +196,17 @@ The `confirm` prompt's `label` is the selection summary (one line per resolved i
196
196
 
197
197
  ## Completion, cleanup, and resume
198
198
 
199
- - Follow the core Result Path + terminal-status completion contract. The Claude adapter's wake mechanism is one `Bash(run_in_background: true)` poll covering every pending Result Path, not foreground sleep or an idle-notification dependency. A spawn acknowledgement is never completion.
199
+ - Follow the core Result Path + terminal-status completion contract. Start one `okstra worker-liveness --wait` background command for the pending native worker selectors; CLI batches are awaited by their dispatcher or `okstra team await`. Await the background handle through the current host's completion notification or blocking wait when available. A spawn acknowledgement is never completion.
200
200
  - The background poll uses a per-worker deadline of twice the expected duration: 20 minutes for `requirements-discovery`, 30 for `error-analysis`, 40 for `implementation-planning`, 40 for `implementation`, and 20 for `final-verification`. On timeout, record terminal status and apply the core's single shared retry budget.
201
201
  - Each in-process worker heartbeat audit sidecar must update at least every five minutes while its result is pending. A missing or stale heartbeat consumes the same one-retry budget; after the second silent hang, record `timeout`. The result file remains the authoritative completion signal.
202
- - **The background poll checks liveness, not only Result Paths.** Result Paths change once, at the very end, so polling them alone pays the full deadline for a worker that died at minute three. Each poll iteration MUST also run, in the same background shell, one `okstra worker-liveness` call covering every pending worker — one paired `--team-state <path> --worker <id>` per worker, in-process and CLI-wrapper alike. The probe reads that worker row's `livenessMode` to pick the artifact and its `startedAt` as the grace anchor; never pass an artifact path yourself and never infer the transport from provider or filename. It exits non-zero when a worker is `stalled` (heartbeat older than the cadence budget) or `did-not-launch`; either verdict ends the wait for that worker immediately and spends the core's one-retry budget, rather than waiting out the deadline. The command reports only — it never kills or re-dispatches. It shares its heartbeat budget with the Phase 7 audit (`okstra_ctl.worker_heartbeat`), so a worker the live probe passes cannot fail the post-hoc one for cadence. A `stalled` verdict is confirmed before it is returned — the probe waits half the stage's budget and re-reads, so a worker that is merely slow gets to prove it by appending its next heartbeat. Budget that confirmation window into the poll iteration; it is the price of not spending the one-retry budget on a live worker.
203
- - The Claude Code harness blocks long foreground sleeps and shorter-sleep circumvention loops. Keep the result poll in a single background shell and let wrapper agents use their documented `BashOutput` loop.
202
+ - The single `worker-liveness --wait` command checks live worker evidence as well as Result Paths. Supply paired `--team-state <path> --dispatch-id <id>` selectors (`--worker <id>` only for an unambiguous legacy record). Its existing grace, cadence, stall confirmation, and timeout checks remain active inside that process. It reports the terminal verdict; the lead applies the shared retry budget.
203
+ - Keep the wait in one background shell. When the host yields without a state change, resume the same handle through its supported wait interface; do not start another poll or reread logs.
204
204
  - On approved cleanup, reconcile the current live session roster before sending shutdown requests. Never target the lead session.
205
205
  - Collect usage before teardown. Resume through the recorded Claude session id and keep all run artifacts authoritative.
206
206
 
207
207
  ### CLI process polling
208
208
 
209
- - Start `okstra worker-dispatch` with `Bash(run_in_background: true)` and poll `BashOutput(bash_id)` back-to-back until terminal completion. The deterministic dispatcher starts the registered provider script after metadata verification. Never add a foreground sleep.
209
+ - Start `okstra worker-dispatch` with `Bash(run_in_background: true)` and retain the handle until terminal completion. Prefer the host's completion notification or blocking wait; if it yields early, resume the same handle with the supported wait interval. The deterministic dispatcher starts the registered provider script after metadata verification.
210
210
  - Return accumulated stdout on success. On a non-zero `exit_code`, record the real code and observed duration.
211
211
  - At the 1800-second cap, inspect the live log mtime once. Recent output grants one extension to 2100 seconds; otherwise call `KillShell(shell_id)`, record exit code 124, and return the wrapper timeout sentinel.
212
212
  - Keep the background process handle until the provider process reaches terminal state.
@@ -190,6 +190,8 @@ If preparation has already failed with a Git lock creation error or `Operation n
190
190
 
191
191
  ### Permission at the dispatch boundary
192
192
 
193
+ Read `runManifest.userAuthorization` before a live dispatch, including `report-finalize`, which can start the translator internally. It preserves the confirmation text and the user response relayed through the wizard; quote its relevant recipient/material scope and response in the execution justification alongside the prepared assignment. The translator reuses the report writer's provider/model, as disclosed at confirmation. Do not wait until translation to inspect that authorization. A missing record is not consent, and the record does not override a host rejection. If review reports missing evidence already present in the record, submit that existing evidence through the permitted review mechanism; ask the user only when the actual approved scope does not cover the dispatch.
194
+
193
195
  Before a live CLI worker dispatch, check the current host's declared sandbox and approval policy. In a restricted Codex session, request the host's supported execution permission on the dispatch command itself (`sandbox_permissions: "require_escalated"` for `exec_command` when that mechanism is available and permitted). Scope the request to the prepared run and explain that it starts the selected worker processes. Follow the host's approval decision; a wizard confirmation or a successful preflight does not grant this permission.
194
196
 
195
197
  Carry the user's existing authorization into that request: identify the task, the selected providers/models from the prepared assignments, and the task brief, relevant repository source/documents, selected evidence, and related run results those workers will process. Cite the actual `Proceed` response and the data-transfer scope displayed in that confirmation; `outcome.confirmationText` is the runtime's summary reference, not proof by itself that the user saw or accepted it. Explain this scope in the execution tool's justification rather than describing only process startup. Data-transfer authorization and host execution privileges are separate: preserve the former while requesting the latter.
@@ -1,7 +1,11 @@
1
1
  """Claude Code 실행기를 사용하는 Z.ai GLM 작업자."""
2
2
  import os
3
+ import re
4
+ import shlex
5
+ from collections.abc import Mapping
3
6
  from dataclasses import replace
4
- from typing import Any, Mapping
7
+ from pathlib import Path
8
+ from typing import Any
5
9
 
6
10
  from okstra_ctl.adapters.providers.claude.adapter import ClaudeExecution
7
11
  from okstra_ctl.domain.provider import ModelSpec, ProviderSpec, ServedModelAttestation
@@ -10,12 +14,13 @@ from okstra_ctl.domain.worker_exec import ExecCommand, PolicySupport, WorkerExec
10
14
  from okstra_ctl.domain.worker_presentation import JsonEvents
11
15
  from okstra_ctl.domain.worker_stream import content_block_events
12
16
 
13
-
14
17
  ZAI_MODELS = {
15
- name: ModelSpec(name, label, name)
16
- for name, label in (
17
- ("glm-5.3", "GLM-5.3"),
18
- ("glm-5.3-flash", "GLM-5.3 Flash"),
18
+ # 입력·캐시 입력·출력 USD/100만 토큰, 2026-09-14 공식 단가 확인.
19
+ # https://docs.z.ai/guides/overview/pricing
20
+ name: ModelSpec(name, label, name, pricing=pricing)
21
+ for name, label, pricing in (
22
+ ("glm-5.3", "GLM-5.3", (1.40, 0.26, 4.40)),
23
+ ("glm-5.3-flash", "GLM-5.3 Flash", (0.15, 0.03, 0.50)),
19
24
  )
20
25
  }
21
26
 
@@ -41,11 +46,39 @@ def observe_usage(event: Mapping[str, Any]) -> Mapping[str, Any] | None:
41
46
  return usage if isinstance(usage, Mapping) and usage else None
42
47
 
43
48
 
49
+ def _read_zai_api_key() -> str:
50
+ """별도 창에서도 동일한 홈 파일을 읽으며 다른 항목은 환경에 넣지 않는다."""
51
+ api_key = os.environ.get("ZAI_API_KEY", "").strip()
52
+ if api_key:
53
+ return api_key
54
+ try:
55
+ lines = (Path.home() / ".env").read_text(encoding="utf-8-sig").splitlines()
56
+ except FileNotFoundError:
57
+ # expected-miss: 홈 파일 없이 환경 변수만 사용하는 설치도 지원한다.
58
+ lines = []
59
+ except (OSError, UnicodeError):
60
+ raise OSError("Cannot read ZAI_API_KEY from ~/.env") from None
61
+ for line in lines:
62
+ match = re.fullmatch(r"\s*(?:export\s+)?ZAI_API_KEY\s*=\s*(.*)", line)
63
+ if match is None:
64
+ continue
65
+ try:
66
+ values = shlex.split(match.group(1), comments=True, posix=True)
67
+ except ValueError:
68
+ raise OSError(
69
+ "Invalid ZAI_API_KEY in ~/.env: use a single-line value"
70
+ ) from None
71
+ if len(values) > 1:
72
+ raise OSError("Invalid ZAI_API_KEY in ~/.env: use a single-line value")
73
+ api_key = values[0].strip() if values else ""
74
+ if not api_key:
75
+ raise OSError("ZAI_API_KEY is required in the process environment or ~/.env")
76
+ return api_key
77
+
78
+
44
79
  class ZaiExecution:
45
80
  def build_command(self, request: WorkerExecRequest) -> ExecCommand:
46
- api_key = os.environ.get("ZAI_API_KEY", "").strip()
47
- if not api_key:
48
- raise OSError("ZAI_API_KEY is required for Z.ai GLM workers")
81
+ api_key = _read_zai_api_key()
49
82
  command = ClaudeExecution().build_command(request)
50
83
  return replace(
51
84
  command,
@@ -28,6 +28,7 @@ AgentAudience = Literal[
28
28
  "analysis-worker",
29
29
  "discovery-worker",
30
30
  "diagnosis-worker",
31
+ "technical-verification-worker",
31
32
  "planning-worker",
32
33
  "direction-selection-worker",
33
34
  "implementation-executor",
@@ -533,6 +534,16 @@ def load_duty_catalog(duty_root: Path) -> dict[AgentAudience, DutyContract]:
533
534
  return catalog
534
535
 
535
536
 
537
+ def load_role_duty_contract(duty_root: Path, audience: AgentAudience) -> DutyContract:
538
+ """현재 호출의 지침만 읽어 이후 추가된 역할을 기존 실행에 요구하지 않는다."""
539
+ if audience not in _SUPPORTED_AUDIENCES:
540
+ raise AgentInvocationError(f"unknown duty audience: {audience}")
541
+ duty = _load_role_duty(duty_root / f"{audience}.md")
542
+ if duty.applies_to != audience:
543
+ raise AgentInvocationError(f"duty audience does not match requested audience: {audience}")
544
+ return duty
545
+
546
+
536
547
  def digest_duty_catalog(duty_root: Path) -> str:
537
548
  """Return the canonical digest of every duty file in a snapshot."""
538
549
  names = [path.relative_to(duty_root).as_posix() for path in duty_root.glob("*.md")]
@@ -722,8 +733,7 @@ def compose_unbound_run_prompt(request: AgentInvocationRequest) -> bytes:
722
733
  _validate_common_request(request)
723
734
  _validate_unbound_v2_run_request(request)
724
735
  common = load_common_duty_contract(request.duty_root)
725
- catalog = load_duty_catalog(request.duty_root)
726
- duty = catalog[request.audience]
736
+ duty = load_role_duty_contract(request.duty_root, request.audience)
727
737
  return _render_prompt(request, common, duty).encode("utf-8")
728
738
 
729
739
 
@@ -1037,8 +1047,7 @@ def _utc_seconds(value: datetime) -> str:
1037
1047
 
1038
1048
  def _materialize(request: AgentInvocationRequest) -> _MaterializedInvocation:
1039
1049
  common = load_common_duty_contract(request.duty_root)
1040
- catalog = load_duty_catalog(request.duty_root)
1041
- duty = catalog[request.audience]
1050
+ duty = load_role_duty_contract(request.duty_root, request.audience)
1042
1051
  if _request_identity_version(request) == 2 and request.duty_id != duty.id:
1043
1052
  raise AgentInvocationError("v2 execution identity duty does not match audience")
1044
1053
  metadata = _metadata_payload(request, duty, {})
@@ -2109,8 +2118,7 @@ def _verify_duty_snapshot(
2109
2118
  try:
2110
2119
  root = _project_path(project_root, source["dutyRootPath"], must_exist=True)
2111
2120
  common = load_common_duty_contract(root)
2112
- catalog = load_duty_catalog(root)
2113
- duty = catalog[duty_contract["id"]]
2121
+ duty = load_role_duty_contract(root, duty_contract["id"])
2114
2122
  except (AgentInvocationError, KeyError):
2115
2123
  return ["duty snapshot is invalid"]
2116
2124
  errors = _verify_duty_metadata(duty_contract, duty)
@@ -137,14 +137,15 @@ def _add_materialize_parser(commands: argparse._SubParsersAction) -> None:
137
137
 
138
138
  apply = commands.add_parser(
139
139
  "apply-corrections",
140
- help="apply a report-writer corrections ledger of replace/remove "
140
+ help="apply a report-writer corrections ledger of replace/remove/add/move "
141
141
  "entries to the narrative without a writer round and record a "
142
- "lead-correction-applied activity row; a ledger with rewrite "
143
- "entries or any defect is refused",
142
+ "lead-correction-applied activity row; rewrite entries require "
143
+ "--rewrite-results; any remaining defect is refused",
144
144
  )
145
145
  _common_paths(apply)
146
146
  apply.add_argument("--run-manifest", required=True)
147
147
  apply.add_argument("--corrections", required=True)
148
+ apply.add_argument("--rewrite-results", help="JSON replacement values for every rewrite id, with the base narrative SHA-256")
148
149
  apply.add_argument("--json", action="store_true")
149
150
 
150
151
  verify = commands.add_parser("verify")
@@ -6,16 +6,17 @@
6
6
  analyser)은 작성자가 받는 합성 묶음에서 가져온다 — 두 소비자가 다른 규칙을 보지
7
7
  않게 하기 위해서다.
8
8
 
9
- `apply-corrections` 는 replace·remove 있는 원장을 작성자 라운드 없이 서사에
10
- 쓰고 활동 원장에 `lead-correction-applied` 행을 남긴다. 선례는 implementation-
11
- planning planner self-fix 기록(`plan_items_cli._record_self_fixes`)이다.
9
+ `apply-corrections` 는 replace·remove·add·move 계산된 단계 수를 직접 적용한다.
10
+ rewrite 작성자가 제출한 교체 값을 대조한 뒤 적용하고, 활동 원장에
11
+ `lead-correction-applied` 행과 교체 파일 근거를 남긴다.
12
12
  """
13
13
  from __future__ import annotations
14
14
 
15
15
  import os
16
16
  from pathlib import Path
17
+ import runpy
17
18
  import tempfile
18
- from typing import Any, Mapping
19
+ from typing import Any, Callable, Mapping
19
20
 
20
21
  from ..activity import (
21
22
  ActivityProjectionError,
@@ -23,6 +24,7 @@ from ..activity import (
23
24
  record_activity,
24
25
  )
25
26
  from ...final_report_schema import load_schema_version
27
+ from ...json_boundary import load_owned_object
26
28
  from ...implementation_options import validate_implementation_option_selection
27
29
  from ...report_contract import TASK_TYPE_DATA_PROPERTY
28
30
  from ...report_assembly import validate_plan_draft
@@ -33,8 +35,11 @@ from ...report_corrections import (
33
35
  render_applied_narrative,
34
36
  )
35
37
  from ...report_inputs import report_narrative_path, uses_report_contract_v3
38
+ from ...paths import STAGE_VALIDATOR_RELATIVE, find_asset_root
39
+ from ...report_narrative import task_narrative_errors
36
40
  from ...report_synthesis_packet import (
37
41
  ReportSynthesisPacketError,
42
+ ReportSynthesisPacket,
38
43
  build_report_synthesis_packet,
39
44
  )
40
45
  from .inputs import AgentPromptCliError, _authorized_path, _relative
@@ -53,6 +58,7 @@ def run_corrections_check(
53
58
  team_state: Mapping[str, Any],
54
59
  corrections_path: Path,
55
60
  narrative_path: Path,
61
+ rewrite_results: Mapping[str, Any] | None = None,
56
62
  ) -> CorrectionsCheck:
57
63
  """원장을 이 run 의 기준 서사에 대조한다. 결함은 `CorrectionsCheck.defects` 에 모인다."""
58
64
  ledger, load_defects = load_corrections(corrections_path)
@@ -92,8 +98,26 @@ def run_corrections_check(
92
98
  f"reason=synthesis packet source defect: {issue.reason}"
93
99
  for issue in exc.issues
94
100
  ))
101
+ return check_corrections(
102
+ ledger=ledger,
103
+ base_narrative=base_path.read_text(encoding="utf-8"),
104
+ schema=load_schema_version("3.0"),
105
+ block_rules=packet.block_rules,
106
+ semantic_validator=_correction_semantic_validator(
107
+ project_root, manifest, narrative_path, packet,
108
+ rewrite_results is None and any(item.get("kind") == "rewrite" for item in ledger["corrections"]),
109
+ ),
110
+ rewrite_results=rewrite_results,
111
+ )
112
+
113
+
114
+ def _correction_semantic_validator(
115
+ project_root: Path, manifest: Mapping[str, Any], narrative_path: Path,
116
+ packet: ReportSynthesisPacket, pending_rewrites: bool,
117
+ ) -> Callable[[dict[str, Any]], list[str]] | None:
95
118
  task_type = str(manifest.get("taskType") or "")
96
- semantic_validator = None
119
+ schema = load_schema_version("3.0")
120
+ semantic_validator = lambda data: task_narrative_errors(data, schema, task_type)
97
121
  if task_type == "implementation-option-selection":
98
122
  block_key = TASK_TYPE_DATA_PROPERTY[task_type]
99
123
  original_ids = packet.original_requirement_ids
@@ -101,24 +125,31 @@ def run_corrections_check(
101
125
 
102
126
  def semantic_validator(data: dict[str, Any]) -> list[str]:
103
127
  block = data.get(block_key)
104
- return [
128
+ return task_narrative_errors(data, schema, task_type) + [
105
129
  f"{block_key}: {error}"
106
130
  for error in validate_implementation_option_selection(
107
131
  block if isinstance(block, Mapping) else {}, original_ids, analysers,
108
132
  )
109
133
  ]
110
134
  elif task_type == "implementation-planning":
135
+ validator_root = find_asset_root(STAGE_VALIDATOR_RELATIVE)
136
+ if validator_root is None:
137
+ raise AgentPromptCliError("cannot locate implementation planning validator")
138
+ stage_validator = runpy.run_path(str(validator_root.joinpath(*STAGE_VALIDATOR_RELATIVE)))
139
+ granted = stage_validator["user_bypassed_stages_for_plan"](narrative_path)
111
140
 
112
141
  def semantic_validator(data: dict[str, Any]) -> list[str]:
113
- return validate_plan_draft(data, project_root, manifest)
142
+ planning = data.get("implementationPlanning")
143
+ stage_errors = [] if pending_rewrites else stage_validator["collect_data_validation_errors"](
144
+ planning if isinstance(planning, dict) else {}, granted,
145
+ )
146
+ return [
147
+ *task_narrative_errors(data, schema, task_type),
148
+ *validate_plan_draft(data, project_root, manifest),
149
+ *[f"implementationPlanning: {error}" for error in stage_errors],
150
+ ]
114
151
 
115
- return check_corrections(
116
- ledger=ledger,
117
- base_narrative=base_path.read_text(encoding="utf-8"),
118
- schema=load_schema_version("3.0"),
119
- block_rules=packet.block_rules,
120
- semantic_validator=semantic_validator,
121
- )
152
+ return semantic_validator
122
153
 
123
154
 
124
155
  def corrections_payload(
@@ -129,11 +160,10 @@ def corrections_payload(
129
160
  "ok": check.ok,
130
161
  "correctionsPath": _relative(project_root, corrections_path),
131
162
  "mechanical": check.mechanical,
163
+ "baseNarrativeSha256": check.ledger.get("baseNarrativeSha256"),
132
164
  "corrections": [
133
165
  {
134
- "id": item.get("id"),
135
- "kind": item.get("kind"),
136
- "path": item.get("path"),
166
+ **item,
137
167
  "constraints": list(check.constraints.get(str(item.get("id")), ())),
138
168
  }
139
169
  for item in check.corrections
@@ -150,6 +180,7 @@ def run_corrections_apply(
150
180
  active_context: Mapping[str, Any],
151
181
  team_state: Mapping[str, Any],
152
182
  corrections_path: Path,
183
+ rewrite_results_path: Path | None = None,
153
184
  ) -> dict[str, Any]:
154
185
  """기계적 원장을 서사에 쓰고 활동 행을 남긴다 — `apply-corrections` 의 본체.
155
186
 
@@ -174,6 +205,10 @@ def run_corrections_apply(
174
205
  "writer with --corrections instead"
175
206
  )
176
207
  corrections_rel = _relative(project_root, corrections_path)
208
+ rewrite_results = (
209
+ load_owned_object(rewrite_results_path, artifact="report writer rewrite results")
210
+ if rewrite_results_path is not None else None
211
+ )
177
212
  check = run_corrections_check(
178
213
  project_root=project_root,
179
214
  manifest=manifest,
@@ -181,7 +216,18 @@ def run_corrections_apply(
181
216
  team_state=team_state,
182
217
  corrections_path=corrections_path,
183
218
  narrative_path=narrative_path,
219
+ rewrite_results=rewrite_results,
184
220
  )
221
+ return _apply_checked_corrections(
222
+ project_root, manifest_path, narrative_path, corrections_path, check, rewrite_results_path,
223
+ )
224
+
225
+
226
+ def _apply_checked_corrections(
227
+ project_root: Path, manifest_path: Path, narrative_path: Path,
228
+ corrections_path: Path, check: CorrectionsCheck, rewrite_results_path: Path | None,
229
+ ) -> dict[str, Any]:
230
+ corrections_rel = _relative(project_root, corrections_path)
185
231
  if check.defects:
186
232
  raise AgentPromptCliError(
187
233
  "report-writer corrections defects: " + "; ".join(check.defects)
@@ -214,21 +260,36 @@ def run_corrections_apply(
214
260
  )
215
261
  applied_ids = [str(item.get("id")) for item in check.corrections]
216
262
  narrative_rel = _relative(project_root, narrative_path)
217
- _write_text_atomic(
218
- narrative_path, render_applied_narrative(check, load_schema_version("3.0")),
263
+ rendered = render_applied_narrative(check, load_schema_version("3.0"))
264
+ if narrative_path.is_file() and narrative_path.read_text(encoding="utf-8") not in (
265
+ base_path.read_text(encoding="utf-8"), rendered,
266
+ ):
267
+ raise AgentPromptCliError("live narrative changed after the correction base was preserved; create a new ledger")
268
+ _write_text_atomic(narrative_path, rendered)
269
+ return _record_correction_application(
270
+ project_root, manifest_path, narrative_rel, corrections_rel, applied_ids, rewrite_results_path,
219
271
  )
272
+
273
+
274
+ def _record_correction_application(
275
+ project_root: Path, manifest_path: Path, narrative_rel: str,
276
+ corrections_rel: str, applied_ids: list[str], rewrite_results_path: Path | None,
277
+ ) -> dict[str, Any]:
220
278
  details = {
221
279
  "kind": LEAD_CORRECTION_ACTIVITY_KIND,
222
280
  "agent": _LEAD_AGENT,
223
281
  "summary": (
224
- f"Applied {len(applied_ids)} mechanical correction(s) "
282
+ f"Applied {len(applied_ids)} correction(s) "
225
283
  f"({', '.join(applied_ids)}) from {corrections_rel} to the report "
226
- "narrative without a writer round"
284
+ + ("narrative using supplied writer replacements" if rewrite_results_path
285
+ else "narrative without a writer round")
227
286
  ),
228
287
  "planItemIds": [],
229
288
  "resultPath": narrative_rel,
230
289
  "commands": [],
231
- "evidenceRefs": [corrections_rel, *applied_ids],
290
+ "evidenceRefs": [corrections_rel, *applied_ids] + (
291
+ [_relative(project_root, rewrite_results_path)] if rewrite_results_path else []
292
+ ),
232
293
  "outcome": "completed",
233
294
  }
234
295
  try:
@@ -11,6 +11,7 @@ from __future__ import annotations
11
11
  import argparse
12
12
  import os
13
13
  from pathlib import Path
14
+ import shlex
14
15
  import shutil
15
16
  import tempfile
16
17
  from typing import Any, Mapping, get_args
@@ -51,6 +52,7 @@ from ...report_synthesis_packet import (
51
52
  from ...worker_prompt_body import report_writer_input_lines
52
53
  from ...dispatch_state import detect_terminal_backend
53
54
  from ...report_corrections import (
55
+ CorrectionsCheck,
54
56
  body_owned_section_conflicts,
55
57
  render_corrections_section,
56
58
  render_output_section,
@@ -325,7 +327,7 @@ def _materialize_run(
325
327
  is_reverify = is_verification_dispatch_kind(args.dispatch_kind)
326
328
  if is_reverify:
327
329
  body = _complete_run_reverify_body(args, manifest, active_context, assignment, body, instruction_path)
328
- if args.audience == "report-writer":
330
+ if args.audience == "report-writer" and not getattr(args, "corrections", None):
329
331
  body = _with_inputs_section(
330
332
  body,
331
333
  _report_writer_input_lines(
@@ -494,6 +496,8 @@ def _with_report_writer_sections(
494
496
  base_narrative_rel=str(check.ledger.get("baseNarrativePath") or ""),
495
497
  corrections_rel=_relative(project_root, corrections_path),
496
498
  ))
499
+ body = "## Inputs\n\nCorrection-only task: use the checked field values and evidence below."
500
+ sections.extend(_render_correction_application(args, project_root, narrative_path, corrections_path, check))
497
501
  sections.append("")
498
502
  else:
499
503
  _refuse_free_form_correction(project_root, narrative_path)
@@ -507,6 +511,35 @@ def _with_report_writer_sections(
507
511
  return body.rstrip("\n") + "\n\n" + "\n".join(sections) + "\n"
508
512
 
509
513
 
514
+ def _render_correction_application(
515
+ args: argparse.Namespace, project_root: Path, narrative_path: Path,
516
+ corrections_path: Path, check: CorrectionsCheck,
517
+ ) -> list[str]:
518
+ result_path = narrative_path.with_name(f"{narrative_path.stem}.{args.invocation_id}.rewrites.json")
519
+ command = [
520
+ "okstra", "agent-prompt", "apply-corrections", "--project-root", str(project_root),
521
+ "--run-manifest", str(args.run_manifest), "--corrections", str(corrections_path),
522
+ ]
523
+ sections: list[str] = []
524
+ if not check.mechanical:
525
+ command.extend(["--rewrite-results", str(result_path)])
526
+ sections.extend([
527
+ "", f"Write only rewrite replacement values to `{_relative(project_root, result_path)}`:",
528
+ '{"baseNarrativeSha256":"' + check.ledger["baseNarrativeSha256"]
529
+ + '","replacements":[{"id":"RC-...","replacement":"the complete value for that field"}]}',
530
+ "Include every rewrite id exactly once. Do not repeat unchanged narrative fields. "
531
+ "Use the JSON value type required by each target's schema constraint.",
532
+ ])
533
+ sections.extend([
534
+ "", "Run this command to validate all replacements and generate the complete narrative:",
535
+ "```sh", shlex.join(command), "```",
536
+ "The command reports every remaining schema and semantic defect together. "
537
+ "Repair the replacement values and retry only when it reports a defect. "
538
+ "Do not write the complete narrative yourself. Then write the pointer record and reading audit.",
539
+ ])
540
+ return sections
541
+
542
+
510
543
  def _preserve_reauthored_narrative(
511
544
  project_root: Path, narrative_path: Path, *, invocation_id: str,
512
545
  ) -> str | None:
@@ -567,7 +600,7 @@ def _refuse_free_form_correction(project_root: Path, narrative_path: Path) -> No
567
600
  "names a preserved copy of that attempt), run `okstra agent-prompt "
568
601
  "check-corrections --project-root <root> --run-manifest <manifest> "
569
602
  "--corrections <ledger>` until it reports no defect, then pass the same "
570
- "--corrections here. A ledger of only replace/remove entries needs no "
603
+ "--corrections here. A ledger of only replace/remove/add/move entries needs no "
571
604
  "writer round: `okstra agent-prompt apply-corrections` writes the "
572
605
  "narrative and records the activity row. Only a narrative whose "
573
606
  "structure does not parse (line grammar, unknown top-level field) is "
@@ -631,6 +664,14 @@ def _apply_corrections(args: argparse.Namespace) -> dict[str, Any]:
631
664
  project_root, manifest_path, manifest, active_context, corrections_path = (
632
665
  _corrections_context(args)
633
666
  )
667
+ results_path = None
668
+ if getattr(args, "rewrite_results", None):
669
+ contract = _mapping(manifest.get("agentContract"), "agent contract")
670
+ authorized = _mapping(contract.get("authorizedPaths"), "authorized paths")
671
+ results_path = _authorized_path(
672
+ project_root, args.rewrite_results, authorized.get("resultRoots"),
673
+ "rewrite results", must_exist=True,
674
+ )
634
675
  return run_corrections_apply(
635
676
  project_root=project_root,
636
677
  manifest=manifest,
@@ -638,6 +679,7 @@ def _apply_corrections(args: argparse.Namespace) -> dict[str, Any]:
638
679
  active_context=active_context,
639
680
  team_state=_load_team_state(project_root, manifest),
640
681
  corrections_path=corrections_path,
682
+ rewrite_results_path=results_path,
641
683
  )
642
684
 
643
685