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.
- package/README.md +4 -2
- package/dist/cli-registry.mjs +6 -0
- package/dist/cli-registry.mjs.map +1 -1
- package/docs/cli.md +24 -4
- package/package.json +1 -1
- package/runtime/BUILD.json +2 -2
- package/runtime/agents/workers/report-writer-worker.md +7 -3
- package/runtime/bin/okstra-spawn-followups.py +2 -2
- package/runtime/prompts/duties/technical-verification-worker.md +44 -0
- package/runtime/prompts/launch.template.md +7 -1
- package/runtime/prompts/lead/okstra-lead-contract.md +7 -2
- package/runtime/prompts/lead/plan-body-verification.md +3 -1
- package/runtime/prompts/lead/report-writer.md +8 -2
- package/runtime/prompts/lead/team-contract.md +6 -0
- package/runtime/prompts/profiles/_implementation-verifier.md +7 -1
- package/runtime/prompts/profiles/final-verification.md +5 -0
- package/runtime/prompts/profiles/forbidden-actions.json +6 -0
- package/runtime/prompts/profiles/implementation-option-selection.md +7 -1
- package/runtime/prompts/profiles/implementation-planning.md +1 -0
- package/runtime/prompts/profiles/technical-verification.md +53 -0
- package/runtime/prompts/wizard/prompts.ko.json +2 -1
- package/runtime/python/okstra_ctl/adapters/hosts/claude-code/relay.md +4 -4
- package/runtime/python/okstra_ctl/adapters/hosts/codex/relay.md +2 -0
- package/runtime/python/okstra_ctl/adapters/providers/zai/adapter.py +42 -9
- package/runtime/python/okstra_ctl/agent/invocation.py +14 -6
- package/runtime/python/okstra_ctl/agent/prompt_cli/cli.py +4 -3
- package/runtime/python/okstra_ctl/agent/prompt_cli/corrections.py +83 -22
- package/runtime/python/okstra_ctl/agent/prompt_cli/materialize.py +44 -2
- package/runtime/python/okstra_ctl/conformance.py +2 -20
- package/runtime/python/okstra_ctl/dispatch_core.py +25 -5
- package/runtime/python/okstra_ctl/dispatch_state.py +2 -0
- package/runtime/python/okstra_ctl/domain/provider.py +0 -1
- package/runtime/python/okstra_ctl/domain/role.py +1 -0
- package/runtime/python/okstra_ctl/execution_mutation_audit.py +6 -1
- package/runtime/python/okstra_ctl/implementation_direction.py +64 -7
- package/runtime/python/okstra_ctl/implementation_options.py +58 -45
- package/runtime/python/okstra_ctl/model_pool.py +2 -5
- package/runtime/python/okstra_ctl/next_phase.py +3 -0
- package/runtime/python/okstra_ctl/plan_items.py +15 -0
- package/runtime/python/okstra_ctl/plan_items_cli.py +9 -3
- package/runtime/python/okstra_ctl/qa_commands.py +30 -0
- package/runtime/python/okstra_ctl/registry/provider_registry.py +11 -8
- package/runtime/python/okstra_ctl/render.py +3 -0
- package/runtime/python/okstra_ctl/render_final_report.py +1 -0
- package/runtime/python/okstra_ctl/report_assembly.py +8 -2
- package/runtime/python/okstra_ctl/report_contract.py +3 -0
- package/runtime/python/okstra_ctl/report_corrections.py +209 -93
- package/runtime/python/okstra_ctl/report_finalize.py +25 -8
- package/runtime/python/okstra_ctl/report_html/router.py +2 -0
- package/runtime/python/okstra_ctl/report_html/view_models/technical_verification.py +21 -0
- package/runtime/python/okstra_ctl/report_projections.py +4 -3
- package/runtime/python/okstra_ctl/report_synthesis_packet.py +178 -26
- package/runtime/python/okstra_ctl/run.py +82 -0
- package/runtime/python/okstra_ctl/team.py +4 -1
- package/runtime/python/okstra_ctl/technical_verification.py +195 -0
- package/runtime/python/okstra_ctl/usage_identity.py +54 -0
- package/runtime/python/okstra_ctl/usage_report.py +22 -8
- package/runtime/python/okstra_ctl/verification_target.py +74 -0
- package/runtime/python/okstra_ctl/wizard/__init__.py +1 -1
- package/runtime/python/okstra_ctl/wizard/cli.py +2 -1
- package/runtime/python/okstra_ctl/wizard/confirmation.py +38 -2
- package/runtime/python/okstra_ctl/wizard/engine.py +3 -0
- package/runtime/python/okstra_ctl/wizard/ids.py +1 -0
- package/runtime/python/okstra_ctl/wizard/outcome.py +63 -0
- package/runtime/python/okstra_ctl/wizard/picker_navigation.py +2 -2
- package/runtime/python/okstra_ctl/wizard/registry.py +1 -1
- package/runtime/python/okstra_ctl/wizard/render.py +8 -55
- package/runtime/python/okstra_ctl/wizard/roles.py +11 -7
- package/runtime/python/okstra_ctl/wizard/sources.py +28 -2
- package/runtime/python/okstra_ctl/wizard/state.py +13 -6
- package/runtime/python/okstra_ctl/wizard/steps_plan.py +8 -0
- package/runtime/python/okstra_ctl/worker_liveness.py +52 -39
- package/runtime/python/okstra_ctl/worker_prompt_policy.py +2 -0
- package/runtime/python/okstra_ctl/workflow.py +8 -0
- package/runtime/python/okstra_ctl/write_policy.py +23 -0
- package/runtime/python/okstra_token_usage/blocks.py +50 -1
- package/runtime/python/okstra_token_usage/claude.py +42 -21
- package/runtime/python/okstra_token_usage/codex.py +17 -0
- package/runtime/python/okstra_token_usage/collect.py +307 -164
- package/runtime/python/okstra_token_usage/cursor.py +2 -3
- package/runtime/python/okstra_token_usage/pricing.py +1 -0
- package/runtime/python/okstra_token_usage/report.py +35 -30
- package/runtime/python/okstra_token_usage/task_totals.py +3 -12
- package/runtime/schemas/final-report-v2.0.schema.json +298 -7
- package/runtime/schemas/final-report-v3.0.schema.json +298 -7
- package/runtime/schemas/report-narrative-v3.0.schema.json +1 -0
- package/runtime/schemas/report-writer-corrections-v1.0.schema.json +30 -3
- package/runtime/skills/okstra-run/SKILL.md +10 -2
- package/runtime/skills/okstra-setup/SKILL.md +42 -7
- package/runtime/templates/report-writer-prompt-preamble.md +7 -3
- package/runtime/templates/reports/html/i18n/en.json +11 -0
- package/runtime/templates/reports/html/i18n/ko.json +11 -0
- package/runtime/templates/reports/html/tasks/implementation-option-selection.template.html +7 -3
- package/runtime/templates/reports/html/tasks/technical-verification.template.html +35 -0
- package/runtime/templates/reports/md/tasks/technical-verification.template.md +5 -0
- 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
|
-
-
|
|
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}).
|
|
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.
|
|
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
|
-
-
|
|
203
|
-
-
|
|
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
|
|
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
|
|
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
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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;
|
|
143
|
-
"
|
|
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
|
-
|
|
11
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
218
|
-
|
|
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)}
|
|
282
|
+
f"Applied {len(applied_ids)} correction(s) "
|
|
225
283
|
f"({', '.join(applied_ids)}) from {corrections_rel} to the report "
|
|
226
|
-
"narrative
|
|
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
|
|