okstra 0.188.0 → 0.189.0
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/docs/cli.md +2 -2
- package/docs/project-structure-overview.md +2 -1
- package/package.json +1 -1
- package/runtime/BUILD.json +2 -2
- package/runtime/agents/workers/report-writer-worker.md +2 -0
- package/runtime/prompts/lead/okstra-lead-contract.md +1 -1
- package/runtime/prompts/lead/plan-body-verification.md +1 -1
- package/runtime/prompts/lead/report-writer.md +11 -8
- package/runtime/python/okstra_ctl/agent/activity.py +3 -0
- package/runtime/python/okstra_ctl/agent/prompt_cli/cli.py +51 -1
- package/runtime/python/okstra_ctl/agent/prompt_cli/corrections.py +280 -0
- package/runtime/python/okstra_ctl/agent/prompt_cli/materialize.py +191 -0
- package/runtime/python/okstra_ctl/report_corrections.py +443 -0
- package/runtime/python/okstra_ctl/report_finalize.py +40 -1
- package/runtime/python/okstra_ctl/report_narrative.py +44 -3
- package/runtime/python/okstra_ctl/report_projections.py +3 -1
- package/runtime/python/okstra_token_usage/collect.py +34 -2
- package/runtime/python/okstra_token_usage/report.py +1 -1
- package/runtime/schemas/final-report-v3.0.schema.json +1 -0
- package/runtime/schemas/report-writer-corrections-v1.0.schema.json +52 -0
- package/runtime/templates/report-writer-prompt-preamble.md +6 -0
package/docs/cli.md
CHANGED
|
@@ -834,7 +834,7 @@ The `okstra` Node CLI (`bin/okstra`) provides both installer/admin commands and
|
|
|
834
834
|
| `okstra codex-run <args…>` | Codex lead-adapter dry-run entry point. Accepts the same arguments as `render-bundle` but owns `--render-only --lead-runtime codex`. It prepares the task bundle and prints the prompt for the Codex lead without dispatching workers |
|
|
835
835
|
| `okstra worker-dispatch --project-root <dir> --run-manifest <path> [--workers <csv>] [--dry-run]` | Provider-neutral deterministic dispatcher for `runner=cli-wrapper` assignments. It verifies each adjacent invocation specification against the immutable run manifest immediately before process creation and records `core-pre-dispatch`; native-session rows stay with the host. The default selects CLI analysis assignments only. Phase 6 uses explicit `--workers report-writer`, and a mixed analysis/report batch is rejected. `--dry-run` performs the same verification and resolution without starting a provider process. |
|
|
836
836
|
| `okstra codex-dispatch --project-root <dir> --run-manifest <path> [--workers <csv>] [--dry-run]` | Compatibility alias for `okstra worker-dispatch`; it no longer selects a Codex-only transport-agent path. |
|
|
837
|
-
| `okstra agent-prompt materialize\|verify\|record-dispatch\|link-result\|reject-result\|abandon-attempt\|materialize-result\|complete\|verify-completion` | Internal invocation-contract CLI. `materialize` composes model assignment, functional duty, and task instructions; `verify` rejects identity, path, snapshot, assignment, source, or digest drift. Run-backed calls resolve `assignmentRef` from the manifest, enforce `authorizedPaths`, and reject real-path or symbolic-link escape. `record-dispatch` records a verified host-native specification before dispatch and `link-result` binds the accepted result; one result path belongs to one dispatch, so a corrective round retires the first attempt with `reject-result --dispatch-id <first> --superseded-by <corrective> --reason <text>` before the new link is accepted — the rejected row stays in `agentResultLinks` carrying `supersededBy` and `rejectionReason` rather than being deleted. `abandon-attempt --invocation-ref <ref> --reason <text>` closes a started attempt whose worker died without producing a result — the one case neither `link-result` (which needs the result file) nor the dispatch-failure path covers — so a retry can follow it instead of the run having to be re-rendered. It refuses any attempt whose `writePolicy.sourcePolicy.mode` is not `source-readonly`: closing an attempt records `failed-no-mutation`, which is true by policy for a read-only worker and a guess for a mutating one. Standalone calls are identified by `(purpose, invocationId)` under `.okstra/agent-invocations/<purpose>/`; they publish a canonical result envelope and publish the completion marker last. Consumers use only the `returnedBody` from `verify-completion`. Metadata contains exactly `catalogDigest`, `assignmentDigest`, `dutyDigest`, `instructionDigest`, and `promptDigest`; JSON inputs use UTF-8, sorted keys, compact separators, and no non-finite values, while duty files use versioned sorted-name/byte framing. Instruction sources use `{kind: project\|runtime, path: <relative POSIX path>}` and never persist an installed absolute runtime path. A published prompt is immutable, so re-running `materialize` with an edited instruction file fails as `existing_invocation_conflict`; `--replace-undispatched` is the one exit, for a call that failed a pre-dispatch gate and therefore ran nowhere — it covers a differing prompt and a differing metadata alike, since the two are published together and describe one call. It republishes prompt and metadata together, and it is verified rather than trusted — a row in `agentDispatches` or `workerDispatches` naming this `invocationId` refuses the replacement and names the dispatch that used it. |
|
|
837
|
+
| `okstra agent-prompt materialize\|check-corrections\|apply-corrections\|verify\|record-dispatch\|link-result\|reject-result\|abandon-attempt\|materialize-result\|complete\|verify-completion` | Internal invocation-contract CLI. `materialize` composes model assignment, functional duty, and task instructions; `verify` rejects identity, path, snapshot, assignment, source, or digest drift. Every run-branch report-writer prompt gets its `## Output` section (narrative, pointer record, reading audit) rendered by okstra, and an instruction body that writes a `## Output` or `## Corrections` heading is refused. A corrective report-writer round — the narrative at `reportNarrativePath` already exists and its structure parses, value defects included — must pass `--corrections <ledger>` (`schemas/report-writer-corrections-v1.0.schema.json`: `replace` / `remove` / `rewrite` entries keyed by the validator's field-path grammar, `baseNarrativePath` naming a preserved copy of the attempt): the ledger is applied to that base and checked against the writer-owned schema and the task's semantic validator before dispatch, every defect is reported at once, and okstra renders the prompt's `## Corrections` section from it; a report-writer materialization without a ledger over such a narrative is refused before any prompt is written, while a narrative whose structure does not parse (line grammar, unknown top-level field) is re-authored without one. `check-corrections --run-manifest <path> --corrections <ledger> [--json]` runs the same check without materializing (exit 1 lists the defects; `mechanical: true` means every entry is a `replace` or `remove` that passed). `apply-corrections` with the same arguments applies such a mechanical ledger without a writer round: it writes the corrected narrative to `reportNarrativePath` and records a `lead-correction-applied` activity row (`evidenceRefs` = ledger path + correction ids) through the run's activity contract; it refuses a ledger with `rewrite` entries or any defect, a base that is the live narrative, a run without `activityContractVersion` 1, and a ledger already applied. Run-backed calls resolve `assignmentRef` from the manifest, enforce `authorizedPaths`, and reject real-path or symbolic-link escape. `record-dispatch` records a verified host-native specification before dispatch and `link-result` binds the accepted result; one result path belongs to one dispatch, so a corrective round retires the first attempt with `reject-result --dispatch-id <first> --superseded-by <corrective> --reason <text>` before the new link is accepted — the rejected row stays in `agentResultLinks` carrying `supersededBy` and `rejectionReason` rather than being deleted. `abandon-attempt --invocation-ref <ref> --reason <text>` closes a started attempt whose worker died without producing a result — the one case neither `link-result` (which needs the result file) nor the dispatch-failure path covers — so a retry can follow it instead of the run having to be re-rendered. It refuses any attempt whose `writePolicy.sourcePolicy.mode` is not `source-readonly`: closing an attempt records `failed-no-mutation`, which is true by policy for a read-only worker and a guess for a mutating one. Standalone calls are identified by `(purpose, invocationId)` under `.okstra/agent-invocations/<purpose>/`; they publish a canonical result envelope and publish the completion marker last. Consumers use only the `returnedBody` from `verify-completion`. Metadata contains exactly `catalogDigest`, `assignmentDigest`, `dutyDigest`, `instructionDigest`, and `promptDigest`; JSON inputs use UTF-8, sorted keys, compact separators, and no non-finite values, while duty files use versioned sorted-name/byte framing. Instruction sources use `{kind: project\|runtime, path: <relative POSIX path>}` and never persist an installed absolute runtime path. A published prompt is immutable, so re-running `materialize` with an edited instruction file fails as `existing_invocation_conflict`; `--replace-undispatched` is the one exit, for a call that failed a pre-dispatch gate and therefore ran nowhere — it covers a differing prompt and a differing metadata alike, since the two are published together and describe one call. It republishes prompt and metadata together, and it is verified rather than trusted — a row in `agentDispatches` or `workerDispatches` naming this `invocationId` refuses the replacement and names the dispatch that used it. |
|
|
838
838
|
| `okstra team dispatch --project-root <dir> --run-manifest <path> [--workers <csv>] [--jobs-file <path>] [--dry-run]` / `okstra team await --project-root <dir> --run-manifest <path> [--json]` / `okstra team teardown --project-root <dir> --run-manifest <path> [--dry-run] [--json]` | Read a `leadRuntime=external` run manifest and dispatch, await, or tear down pane-backed workers. Default dispatch excludes report writer; Phase 6 selects it explicitly, and mixed analysis/report jobs are rejected. If a pane cannot be opened, gracefully degrade to the CLI wrapper and record the fallback in `workerDispatches[].degradedFrom` |
|
|
839
839
|
| `okstra agent-activity append --project-root <dir> --run-manifest <path> --kind <kind> --agent <assigned-id> (--summary <text>\|--summary-file <markdown>) --outcome <outcome> [--plan-item-id <current-id>]… [--command <text> --command-cwd <dir> --command-exit-code <n> --command-output-file <markdown>] [--request-ref <returned-ref>]` | Append one structured activity after checking the agent against this run's role assignments and every plan item against its current convergence state. Python returns an `activityRequestRef`; supply only that returned value with `--request-ref` to retry idempotently. A new call without it remains a distinct activity even with identical contents. Legacy JSON command records remain automation compatibility only. |
|
|
840
840
|
| `okstra agent-activity project --project-root <dir> --run-manifest <path> --data <data.json>` | Project this run's canonical activity events into `agentActivity[]`. The command preserves event order, rejects duplicate or decreasing activity IDs, and replaces no other report field. A historical manifest without `activityContractVersion: 1` returns an empty projection and leaves data.json unchanged. Normal Phase 7 execution reaches this behavior through `report-finalize`; use the standalone command only for diagnostics. |
|
|
@@ -847,7 +847,7 @@ The `okstra` Node CLI (`bin/okstra`) provides both installer/admin commands and
|
|
|
847
847
|
| `okstra wizard <init\|step\|render-args\|confirmation\|outcome> --state-file <path>` | Interactive input state machine for okstra-run, implemented by `okstra_ctl.wizard`. Seed a state file with `init`, then repeatedly call `step --answer <val>` to receive the next `Prompt` JSON. `--answer` is **required**; use `--no-submit` to peek at the next prompt without submitting a response. A `pick` with more choices than the host picker can display keeps `kind: "pick"` but adds `presentation: "numbered-text"`; render every option as a numbered Markdown list and submit the user's 1-based number, exact value, or exact label. Invalid, out-of-range, and ambiguous answers re-prompt without dropping choices. `render-args` returns the final `render-bundle` argument map, and `confirmation` returns the user echo block. On a completed wizard, `outcome` returns `renderArgs`, `persistActions`, and `confirmationText` together; project/global release-handoff PR-template persistence appears as `persistActions[].command == "config.set"`. For an `implementation` task type, `stage_pick` follows `approved_plan_pick` and selects the stage before `executor_pick`. The brief step appears only for entry task types—requirements-discovery, error-analysis, improvement-discovery, project-analysis, feature-analysis, and change-impact-analysis. Analysis inputs use `feature_evidence_pick` / `feature_evidence`, `project_evidence_pick` / `project_evidence`, and `analysis_target_pick` / `analysis_target`; a revision-requested report prioritizes its same-task, same-type rerun. Downstream lifecycle phases automatically carry the manifest brief, with a three-option `brief_carry` fallback when none is registered; `release-handoff` has no brief and enters multi-select `handoff_stage_pick` for eligible stage groups or the whole task |
|
|
848
848
|
| `okstra token-usage ...` | Wrap the installed `okstra-token-usage.py` to collect and substitute run token usage. Session JSONL is incrementally scanned by default through a byte-cursor cache at `$OKSTRA_HOME/cache/token-usage/`; `--no-cache` bypasses the cache and forces a full rescan as an accuracy fallback |
|
|
849
849
|
|
|
850
|
-
In prose, `okstra agent-prompt materialize|verify|record-dispatch|link-result|materialize-result|complete|verify-completion` denotes that internal command family; the vertical bars separate subcommands and are not literal shell arguments.
|
|
850
|
+
In prose, `okstra agent-prompt materialize|verify|record-dispatch|link-result|materialize-result|complete|verify-completion` denotes that internal command family; the vertical bars separate subcommands and are not literal shell arguments. `check-corrections` and `apply-corrections` belong to the same family and are described in the table row above.
|
|
851
851
|
|
|
852
852
|
The convergence state lifecycle is `groups v1.0 → work v1.0 → final v1.3`; round-plan, round-results, and optional critic-results v1.0 artifacts provide the auditable transitions between those endpoints.
|
|
853
853
|
|
|
@@ -279,6 +279,7 @@ Important modules:
|
|
|
279
279
|
| `report_assembly.py` | Validates the role-owned report inputs and publishes the contract 3.0 record once (assembled in a temp file, then atomically promoted); a bad input aborts with owner / artifact path / field path / reason and preserves the existing `data.json` |
|
|
280
280
|
| `report_projections.py` | Pure projections turning role-owned execution inputs (agent activity, execution status, convergence, design, token usage) into canonical final-report fragments |
|
|
281
281
|
| `report_narrative.py` | Lossless read/write contract for the report-writer-owned narrative Markdown (`report-narrative-<task-type>-<seq>.md`) — the only artifact the report-writer authors under contract 3.0 |
|
|
282
|
+
| `report_corrections.py` | The lead's corrective request to the report writer as a machine-checked ledger (`report-writer-corrections-<task-type>-<seq>-a<N>.json`, schema `schemas/report-writer-corrections-v1.0.schema.json`): applies `replace` / `remove` entries to the base narrative, validates the result against the writer-owned schema and the task's semantic validator, attaches the schema constraint to each `rewrite`, reports every defect at once, renders the prompt's `## Corrections` / `## Output` sections, and renders the corrected narrative for a mechanical (replace/remove only) ledger. Consumed by `agent/prompt_cli/corrections.py` |
|
|
282
283
|
| `report_synthesis_packet.py` | Builds the frozen, read-only synthesis input packet handed to the report-writer — collects each source's label / owner / path / content digest into `report-writer-synthesis-packet-<task-type>-<seq>.{json,md}`, materialized at dispatch by `initial_prompt_materialization.py` so the writer synthesizes from a byte-stable snapshot. `report_assembly.py` calls `verify_report_synthesis_packet_sources` at publish time so a drifted source aborts the record; schema is `schemas/report-synthesis-packet-v1.0.schema.json` |
|
|
283
284
|
| `approval_decisions.py` | Lead-owned approval-decision input ledger — `disposition` (`select`/`accept-risk`/`request-revision`/`reject`), reach, scope effect, and classification invariants (`correctness-critical`/`noncritical-dissent`/`user-decision`) |
|
|
284
285
|
| `design_snapshot.py` | Builds the design-surface-detector-owned snapshot from the report narrative — reproducible design surfaces plus conservative `PREP-NNN` preparation items (delegates surface detection to `design_surfaces.py`) |
|
|
@@ -311,7 +312,7 @@ Important modules:
|
|
|
311
312
|
| `manager_sync.py` | One-way child project `.okstra` snapshot reader; corrupt child state becomes row-level `error` so other children continue |
|
|
312
313
|
| `manager_launch.py` | Child launch packet and manager child context renderer; records `prepared` launch metadata/events without changing project-local task state |
|
|
313
314
|
| `agent/invocation.py` | Deep invocation-contract module — composes model assignment, common/functional duty, and task instructions; publishes immutable prompt/metadata pairs; verifies five digests; owns standalone result/completion envelopes |
|
|
314
|
-
| `agent/prompt_cli/` | CLI boundary for run-backed and standalone materialization/verification plus host-native dispatch and result-link records. `inputs` resolves the path arguments this model-facing surface cannot trust and `emit` writes the result; above them `run_identity` refuses a role the run never issued and `dynamic_verifier` reserves a re-verification slot only after that role qualifies; `materialize` authors the specification, `results` links what came back, and `cli` is the argparse surface with the command's canonical USAGE epilog |
|
|
315
|
+
| `agent/prompt_cli/` | CLI boundary for run-backed and standalone materialization/verification plus host-native dispatch and result-link records. `inputs` resolves the path arguments this model-facing surface cannot trust and `emit` writes the result; above them `run_identity` refuses a role the run never issued and `dynamic_verifier` reserves a re-verification slot only after that role qualifies; `materialize` authors the specification (every report-writer prompt gets the okstra-rendered `## Output`; with `--corrections` it runs the ledger check in `corrections` and prepends `## Corrections`; without one it refuses a corrective dispatch over a parsing narrative), `results` links what came back, and `cli` is the argparse surface with the command's canonical USAGE epilog (`check-corrections` runs the ledger check without materializing; `apply-corrections` writes a mechanical ledger to the narrative and records the `lead-correction-applied` activity row via `corrections.run_corrections_apply`) |
|
|
315
316
|
| `dispatch_state.py` | Provider-neutral `WorkerJob`, invocation metadata validation, immutable host-native dispatch/result-link recording, and shared team-state mutation helpers |
|
|
316
317
|
| `dispatch_core.py` | Backend-neutral worker dispatch core — verifies invocation metadata immediately before worker execution, then records and collects code-owned process/pane attempts shared by every lead runtime |
|
|
317
318
|
| `worker_dispatch.py` | Provider-neutral deterministic dispatcher for every `runner=cli-wrapper` assignment; it never composes or rewrites a prompt |
|
package/package.json
CHANGED
package/runtime/BUILD.json
CHANGED
|
@@ -28,4 +28,6 @@ The narrative is not free-form Markdown. After the line `# OKSTRA Report Narrati
|
|
|
28
28
|
|
|
29
29
|
Only these top-level names are allowed: `Analysis Common`, `Change Impact Analysis`, `End State Coverage`, `Error Analysis`, `Feature Analysis`, `Final Verdict`, `Final Verification`, `Follow Up Tasks`, `Human Summary`, `Implementation`, `Implementation Option Selection`, `Implementation Planning`, `Improvement Discovery`, `Project Analysis`, `Rationale`, `Recommended Next Steps`, `Release Handoff`, `Requirements Discovery`, `Summary`, `Ticket Coverage`, `Verdict Card`. A section title from a lead procedure document is not a field name. On a refusal, read the allowed names the parser lists for that position instead of guessing again.
|
|
30
30
|
|
|
31
|
+
A `## Corrections` section in the prompt is okstra-validated: apply it verbatim and change nothing else. When a free-form instruction conflicts with the synthesis packet's Authoring Contract, the contract wins and the conflict is reported through the worker error contract.
|
|
32
|
+
|
|
31
33
|
Report assembly validates every owner input and publishes the final record once. An assembly error naming another owner must be returned to that owner, not repaired in the narrative.
|
|
@@ -66,7 +66,7 @@ This gate applies before every initial dispatch, retry, re-verification, critic,
|
|
|
66
66
|
5. If any lead-owned value remains undetermined, stop before dispatch and resolve it from the authoritative source or ask the user when it is user-owned. Do not delegate an accidental ambiguity as a worker choice. A worker may compare alternatives only when alternative evaluation is the explicit assignment.
|
|
67
67
|
6. Preserve independent judgement. Exact instructions define the task and its constraints; they do not prescribe the finding, vote, verdict, or recommendation an analysis or verification worker must reach.
|
|
68
68
|
|
|
69
|
-
For a corrective dispatch, name every known defect in one instruction with its current value and exact replacement or required outcome. Do not spend one worker round per defect that was already visible before dispatch.
|
|
69
|
+
For a corrective dispatch, name every known defect in one instruction with its current value and exact replacement or required outcome. Do not spend one worker round per defect that was already visible before dispatch. For the report writer that instruction is a corrections ledger checked by `okstra agent-prompt check-corrections` before dispatch and applied without a writer round when it holds only `replace` / `remove` entries — see [report-writer](./report-writer.md) "Corrective report-writer dispatch (ledger required)".
|
|
70
70
|
|
|
71
71
|
**Enforcement:** `tests/contract/test_agent_spawn_surface_census.py` requires this gate to remain in the lead contract and requires every worker-dispatch support contract to point back to it. Semantic correctness remains a lead-side pre-dispatch check because no generic validator can decide whether free-form task instructions accurately represent the cited project evidence.
|
|
72
72
|
|
|
@@ -418,7 +418,7 @@ round before any host or provider process starts.
|
|
|
418
418
|
Then run `okstra plan-items complete-round --state <plan-body-verification.json> --run-manifest <current-run-manifest.json> --round <N>`. Python appends one immutable round history entry, records each verified item's votes, derives the current projection from the actual assigned roster, and stamps `completedAt` after the preceding verification command succeeds. The file accumulates across rounds; it is never truncated to the latest one. Report assembly later projects the completed nested `planBodyVerification` into the final record.
|
|
419
419
|
7. **Self-fix loop (one rewrite, targeting planner-fixable defects).** After round 1, lead may run one report-writer rewrite when at least one `majority-disagree` item has a majority of its `DISAGREE` verdicts at `fixability == planner-fixable`. The targeted re-verification after that rewrite is round 2. After round 2, stop automatic self-fix regardless of outcome. Classify every remaining item as `user-decision`, `noncritical-dissent`, or `correctness-critical`. A second automatic self-fix is a contract violation. The fixed order is initial verification → one planner self-fix → targeted re-verification → user gate.
|
|
420
420
|
- **Group the targets by cause before instructing (BLOCKING).** Blocked items are usually several derivatives of one defect. Lead partitions this round's targets into cause groups and instructs each group as **"remove this cause"**, naming the derivatives it accounts for. The convergence command owns the persisted group and correction fields; the lead does not edit JSON state.
|
|
421
|
-
- lead instructs report-writer to rewrite the items in each cause group (NOT a full draft regeneration; procedure in [report-writer](./report-writer.md) §"
|
|
421
|
+
- lead instructs report-writer to rewrite the items in each cause group (NOT a full draft regeneration): one `rewrite` entry per item in a corrections ledger, whose `rule` states the cause group's required outcome; procedure in [report-writer](./report-writer.md) §"Corrective report-writer dispatch (ledger required)".
|
|
422
422
|
- missing or weak `P-Prep-*` contracts are repaired by adding kind-specific inline detail or an AI-prepared PREP item with a concrete proposal. Facts that require user or external authority remain `blocked` and keep their request material; never invent those facts during self-fix.
|
|
423
423
|
- **Drop plan items whose element the round deleted.** A self-fix rewrite may remove a plan element (a validation check, a rollback row). `P-*` ids are positional, so a deletion shifts every later row and silently re-points surviving verdicts at their neighbours — and a verdict recorded against a removed element keeps blocking a gate while being unfindable in the plan, so reading the plan never reveals the cause. After each round, re-extract plan items with `okstra plan-items extract` and re-verify any item whose `subject` no longer matches; never carry the old vote forward across a shift. **Enforced:** `validators/validate-run.py` `_validate_verdicts_match_current_subjects` (re-pointing) and `_validate_plan_item_extraction_completeness` (dangling ids).
|
|
424
424
|
- **Classify each cause group before instructing it (BLOCKING).** A group is either an *authoring* defect — the plan says something wrong, incomplete, or self-contradictory, which self-fix owns — or a *citation* defect, where the plan points at an analysis artifact incorrectly. Only the first is self-fix work. For the second the finding already exists and already went through convergence, so the fix is to re-cite the converged artifact; instructing report-writer to re-derive the fact means the author reads the source material and produces a **finding that never went through convergence**, which the plan then carries as if it had. That is the role boundary the lead contract draws ("keep analysis, execution, verification, and report authoring responsibilities distinct; return defects to the role that owns them"), and report-writer is authoring-only by its own contract. `P-Req-*` items with breakage kind `f` are where this goes wrong most often: the question is usually whether a coverage row points correctly at something already measured, not whether the measurement is right. State the classification in the group's instruction so the author knows which of the two it is being asked to do.
|
|
@@ -68,18 +68,21 @@ Register the dispatch with `okstra agent-prompt record-dispatch`. When `terminal
|
|
|
68
68
|
|
|
69
69
|
The pointer record contains the narrative and audit paths. Completion never depends on the final record because assembly runs after writer completion.
|
|
70
70
|
|
|
71
|
-
**
|
|
71
|
+
**okstra renders the `## Output` section of every run-branch report-writer prompt** — the narrative at `**Result Path:**`, the pointer record at `**Worker Result Path:**`, and the reading audit at `**Audit sidecar path:**` — for the first dispatch and for every follow-up alike, so the instruction body must not contain a `## Output` or `## Corrections` heading; the materializer refuses a body that does. The three are enumerated by code rather than by hand because `dispatch_state.dispatch_completion_paths` treats the narrative and the pointer as two separate completion artifacts: a hand-written `## Output` that named only the narrative ended the dispatch with `required worker artifact was not produced` even though the writer did everything the body asked. **Enforced:** `_with_report_writer_sections` in `scripts/okstra_ctl/agent/prompt_cli/materialize.py`.
|
|
72
72
|
|
|
73
|
-
### Corrective report-writer dispatch
|
|
73
|
+
### Corrective report-writer dispatch (ledger required)
|
|
74
74
|
|
|
75
|
-
This section adds report-specific checks to [okstra-lead-contract](./okstra-lead-contract.md) "Worker instruction quality gate"; it does not limit that common gate to report-writer calls.
|
|
75
|
+
This section adds report-specific checks to [okstra-lead-contract](./okstra-lead-contract.md) "Worker instruction quality gate"; it does not limit that common gate to report-writer calls. A follow-up dispatch that revises a narrative which already exists and whose structure parses is a corrective dispatch, and its instruction is a corrections ledger, not prose:
|
|
76
76
|
|
|
77
|
-
1.
|
|
78
|
-
2.
|
|
79
|
-
3.
|
|
80
|
-
4.
|
|
77
|
+
1. Preserve the attempt: copy the current narrative to `worker-results/report-writer-narrative-a<N>-<task-type>-<seq>.md`. The ledger's `baseNarrativePath` names that copy, never the live `reportNarrativePath` — the correction overwrites the live file, and the copy is what the writer (or `apply-corrections`) reads.
|
|
78
|
+
2. The ledger lives at `runs/<task-type>/state/report-writer-corrections-<task-type>-<seq>-a<N>.json` (schema `schemas/report-writer-corrections-v1.0.schema.json`) and holds one entry per defect: `replace` with the exact replacement value (add `current` when you want it checked), `remove` for an item or optional field, `rewrite` with a `rule` when the writer has to re-author prose. Paths use the validator's grammar (`implementationOptionSelection.rankedOptions[1].coverageSummary.coveragePercent`), so a report-assembly refusal can be copied into the ledger verbatim. Never write an indirect instruction such as `use the schema value`, `use the valid status`, or `fix the enum`: a `replacement` is the literal, and a `rule` names the required outcome. You do not copy allowed enum literals by hand — okstra attaches each `rewrite`'s schema constraint from the frozen schema.
|
|
79
|
+
3. Run `okstra agent-prompt check-corrections --project-root <root> --run-manifest <path> --corrections <ledger>` until it reports no defect. It applies the ledger to a scratch copy of the base narrative and validates the complete proposed narrative against the writer-owned value schema and the task's semantic validator, listing every defect at once. Validating only the edited field is insufficient because one replacement can select a different schema branch, which is why the check covers the whole narrative.
|
|
80
|
+
4. When the check reports `mechanical: true` (every entry is a `replace` or `remove`), run `okstra agent-prompt apply-corrections` with the same arguments: okstra writes the corrected narrative to `reportNarrativePath` and records a `lead-correction-applied` activity row naming the ledger and its correction ids. No writer dispatch, `record-dispatch`, or `link-result` follows; the roster row's result already exists.
|
|
81
|
+
5. Otherwise materialize the writer prompt with the same `--corrections <ledger>` under a new invocation id and prompt path (retire the first attempt's link with `reject-result` as [plan-body-verification](./plan-body-verification.md) describes). okstra renders `## Corrections` (each entry with its label path, current value, replacement or rule, schema constraint, and reason) and `## Output`; the instruction body carries only context.
|
|
81
82
|
|
|
82
|
-
|
|
83
|
+
A report-writer materialization without `--corrections` whose narrative already exists and parses is refused before any prompt is written — free-form corrections cannot be checked before the writer runs, and four of six re-runs in the 2026-09-03 measurement were lead instructions that contradicted the authoring contract. Only a narrative whose structure does not parse (line grammar, an unknown top-level field) is re-authored, not corrected: that dispatch needs no ledger, and its body quotes the parser's message. Value defects — an id outside its pattern, a value outside its enum, a missing required field — leave the structure readable and are exactly what the ledger fixes; the a3 attempt of the 2026-09-03 run carried twenty `SC-` ids that assembly refused and was still a corrective base.
|
|
84
|
+
|
|
85
|
+
**Enforced:** `_with_report_writer_sections` / `_refuse_free_form_correction` in `scripts/okstra_ctl/agent/prompt_cli/materialize.py`, `report_corrections.check_corrections`, `agent/prompt_cli/corrections.run_corrections_apply`; `tests/run/test_agent_prompt_corrections.py` and `tests/contract/test_report_writer_v3_contract.py` keep this procedure in the lead contract.
|
|
83
86
|
|
|
84
87
|
## Implementation-planning sequence
|
|
85
88
|
|
|
@@ -32,6 +32,9 @@ ACTIVITY_KINDS = frozenset({
|
|
|
32
32
|
"worker-completed",
|
|
33
33
|
"verification-round-completed",
|
|
34
34
|
"self-fix-applied",
|
|
35
|
+
# 리드의 교정 원장(replace·remove 만)을 okstra 가 작성자 라운드 없이 서사에
|
|
36
|
+
# 적용한 행. `okstra agent-prompt apply-corrections` 만 쓴다.
|
|
37
|
+
"lead-correction-applied",
|
|
35
38
|
"user-decision-required",
|
|
36
39
|
"user-decision-evaluated",
|
|
37
40
|
})
|
|
@@ -15,7 +15,7 @@ from ...worker_prompt_headers import WorkerPromptHeaderError
|
|
|
15
15
|
from ...dispatch_state import BACKEND_CLI_WRAPPER, BACKEND_CMUX_PANE, DispatchError
|
|
16
16
|
from .emit import _emit, _prepared_payload
|
|
17
17
|
from .inputs import AgentPromptCliError
|
|
18
|
-
from .materialize import _materialize
|
|
18
|
+
from .materialize import _apply_corrections, _check_corrections, _materialize
|
|
19
19
|
from .results import (
|
|
20
20
|
_complete,
|
|
21
21
|
_link_result,
|
|
@@ -30,6 +30,8 @@ from .results import (
|
|
|
30
30
|
|
|
31
31
|
_CLI_EPILOG = r"""Usage:
|
|
32
32
|
okstra agent-prompt materialize [options]
|
|
33
|
+
okstra agent-prompt check-corrections --project-root <dir> --run-manifest <path> --corrections <file>
|
|
34
|
+
okstra agent-prompt apply-corrections --project-root <dir> --run-manifest <path> --corrections <file>
|
|
33
35
|
okstra agent-prompt verify [options]
|
|
34
36
|
okstra agent-prompt materialize-result [options]
|
|
35
37
|
okstra agent-prompt complete [options]
|
|
@@ -76,6 +78,15 @@ def _add_materialize_parser(commands: argparse._SubParsersAction) -> None:
|
|
|
76
78
|
materialize.add_argument("--assignment-ref", help=run_only)
|
|
77
79
|
materialize.add_argument("--source-role-execution-ref", help=run_only)
|
|
78
80
|
materialize.add_argument("--result", help=run_only)
|
|
81
|
+
materialize.add_argument(
|
|
82
|
+
"--corrections",
|
|
83
|
+
help=run_only + "; report-writer only: a corrections ledger "
|
|
84
|
+
"(report-writer-corrections-v1.0) checked against the base "
|
|
85
|
+
"narrative before dispatch; okstra renders the prompt's "
|
|
86
|
+
"## Corrections section from it. Required once a parsing "
|
|
87
|
+
"narrative exists (a corrective dispatch); ## Output is rendered "
|
|
88
|
+
"on every report-writer prompt",
|
|
89
|
+
)
|
|
79
90
|
materialize.add_argument(
|
|
80
91
|
"--audit-source",
|
|
81
92
|
help=run_only + "; accepted only for report-writer and translator, whose "
|
|
@@ -104,6 +115,28 @@ def _add_materialize_parser(commands: argparse._SubParsersAction) -> None:
|
|
|
104
115
|
)
|
|
105
116
|
materialize.add_argument("--json", action="store_true")
|
|
106
117
|
|
|
118
|
+
check = commands.add_parser(
|
|
119
|
+
"check-corrections",
|
|
120
|
+
help="check a report-writer corrections ledger against the base "
|
|
121
|
+
"narrative without materializing a prompt; exit 1 lists every defect",
|
|
122
|
+
)
|
|
123
|
+
_common_paths(check)
|
|
124
|
+
check.add_argument("--run-manifest", required=True)
|
|
125
|
+
check.add_argument("--corrections", required=True)
|
|
126
|
+
check.add_argument("--json", action="store_true")
|
|
127
|
+
|
|
128
|
+
apply = commands.add_parser(
|
|
129
|
+
"apply-corrections",
|
|
130
|
+
help="apply a report-writer corrections ledger of replace/remove "
|
|
131
|
+
"entries to the narrative without a writer round and record a "
|
|
132
|
+
"lead-correction-applied activity row; a ledger with rewrite "
|
|
133
|
+
"entries or any defect is refused",
|
|
134
|
+
)
|
|
135
|
+
_common_paths(apply)
|
|
136
|
+
apply.add_argument("--run-manifest", required=True)
|
|
137
|
+
apply.add_argument("--corrections", required=True)
|
|
138
|
+
apply.add_argument("--json", action="store_true")
|
|
139
|
+
|
|
107
140
|
verify = commands.add_parser("verify")
|
|
108
141
|
_common_paths(verify)
|
|
109
142
|
verify.add_argument("--run-manifest")
|
|
@@ -188,6 +221,23 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
188
221
|
prepared = _materialize(args)
|
|
189
222
|
_emit(_prepared_payload(prepared, args.audience), args.json)
|
|
190
223
|
return 0
|
|
224
|
+
if args.command == "check-corrections":
|
|
225
|
+
payload = _check_corrections(args)
|
|
226
|
+
if args.json:
|
|
227
|
+
_emit(payload, True)
|
|
228
|
+
else:
|
|
229
|
+
for defect in payload["defects"]:
|
|
230
|
+
print(defect, file=sys.stderr)
|
|
231
|
+
print("ok" if payload["ok"] else "defects: " + str(len(payload["defects"])))
|
|
232
|
+
return 0 if payload["ok"] else 1
|
|
233
|
+
if args.command == "apply-corrections":
|
|
234
|
+
payload = _apply_corrections(args)
|
|
235
|
+
if args.json:
|
|
236
|
+
_emit(payload, True)
|
|
237
|
+
else:
|
|
238
|
+
print(payload["activityLine"])
|
|
239
|
+
print(payload["narrativePath"])
|
|
240
|
+
return 0
|
|
191
241
|
if args.command == "verify":
|
|
192
242
|
_verify(args)
|
|
193
243
|
_emit(
|
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
"""run 갈래의 교정 원장 대조 — 매니페스트가 정한 스키마·소스로 `check_corrections` 를 돌린다.
|
|
2
|
+
|
|
3
|
+
`materialize --corrections`, `check-corrections`, `apply-corrections` 가 같은 대조
|
|
4
|
+
함수를 부른다. 스키마는 조립(`report_assembly.assemble_report`)이 쓰는 것과 같은
|
|
5
|
+
계약 3.0 완성 리포트 스키마이고, task 의미 검증기의 입력(요구 id 순서·참여
|
|
6
|
+
analyser)은 작성자가 받는 합성 묶음에서 가져온다 — 두 소비자가 다른 규칙을 보지
|
|
7
|
+
않게 하기 위해서다.
|
|
8
|
+
|
|
9
|
+
`apply-corrections` 는 replace·remove 만 있는 원장을 작성자 라운드 없이 서사에
|
|
10
|
+
쓰고 활동 원장에 `lead-correction-applied` 행을 남긴다. 선례는 implementation-
|
|
11
|
+
planning 의 planner self-fix 기록(`plan_items_cli._record_self_fixes`)이다.
|
|
12
|
+
"""
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import os
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
import tempfile
|
|
18
|
+
from typing import Any, Mapping
|
|
19
|
+
|
|
20
|
+
from ..activity import (
|
|
21
|
+
ActivityProjectionError,
|
|
22
|
+
agent_activity_rows,
|
|
23
|
+
record_activity,
|
|
24
|
+
)
|
|
25
|
+
from ...final_report_schema import load_schema_version
|
|
26
|
+
from ...implementation_options import validate_implementation_option_selection
|
|
27
|
+
from ...report_contract import TASK_TYPE_DATA_PROPERTY
|
|
28
|
+
from ...report_corrections import (
|
|
29
|
+
CorrectionsCheck,
|
|
30
|
+
check_corrections,
|
|
31
|
+
load_corrections,
|
|
32
|
+
render_applied_narrative,
|
|
33
|
+
)
|
|
34
|
+
from ...report_inputs import report_narrative_path, uses_report_contract_v3
|
|
35
|
+
from ...report_synthesis_packet import (
|
|
36
|
+
ReportSynthesisPacketError,
|
|
37
|
+
build_report_synthesis_packet,
|
|
38
|
+
)
|
|
39
|
+
from .inputs import AgentPromptCliError, _authorized_path, _relative
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
LEAD_CORRECTION_ACTIVITY_KIND = "lead-correction-applied"
|
|
43
|
+
# 활동 원장의 `agent` 허용값 중 리드를 뜻하는 고정 이름(`activity._validate_activity_ownership`).
|
|
44
|
+
_LEAD_AGENT = "okstra-lead"
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def run_corrections_check(
|
|
48
|
+
*,
|
|
49
|
+
project_root: Path,
|
|
50
|
+
manifest: Mapping[str, Any],
|
|
51
|
+
active_context: Mapping[str, Any],
|
|
52
|
+
team_state: Mapping[str, Any],
|
|
53
|
+
corrections_path: Path,
|
|
54
|
+
narrative_path: Path,
|
|
55
|
+
) -> CorrectionsCheck:
|
|
56
|
+
"""원장을 이 run 의 기준 서사에 대조한다. 결함은 `CorrectionsCheck.defects` 에 모인다."""
|
|
57
|
+
ledger, load_defects = load_corrections(corrections_path)
|
|
58
|
+
if load_defects:
|
|
59
|
+
return CorrectionsCheck(ledger, (), None, False, {}, tuple(load_defects))
|
|
60
|
+
if not uses_report_contract_v3(manifest):
|
|
61
|
+
return CorrectionsCheck(ledger, (), None, False, {}, (
|
|
62
|
+
"owner=lead correction=ledger path=- reason=corrections require report "
|
|
63
|
+
"contract 3.0 (a Markdown narrative); this run uses an older contract",
|
|
64
|
+
))
|
|
65
|
+
contract = manifest.get("agentContract")
|
|
66
|
+
authorized = contract.get("authorizedPaths") if isinstance(contract, Mapping) else None
|
|
67
|
+
authorized = authorized if isinstance(authorized, Mapping) else {}
|
|
68
|
+
try:
|
|
69
|
+
base_path = _authorized_path(
|
|
70
|
+
project_root,
|
|
71
|
+
str(ledger.get("baseNarrativePath") or ""),
|
|
72
|
+
authorized.get("resultRoots"),
|
|
73
|
+
"base narrative",
|
|
74
|
+
must_exist=True,
|
|
75
|
+
)
|
|
76
|
+
except AgentPromptCliError as exc:
|
|
77
|
+
return CorrectionsCheck(ledger, (), None, False, {}, (
|
|
78
|
+
f"owner=lead correction=ledger path={ledger.get('baseNarrativePath')} reason={exc}",
|
|
79
|
+
))
|
|
80
|
+
try:
|
|
81
|
+
packet = build_report_synthesis_packet(
|
|
82
|
+
project_root=project_root,
|
|
83
|
+
manifest=manifest,
|
|
84
|
+
active_context=active_context,
|
|
85
|
+
team_state=team_state,
|
|
86
|
+
narrative_path=narrative_path,
|
|
87
|
+
)
|
|
88
|
+
except ReportSynthesisPacketError as exc:
|
|
89
|
+
return CorrectionsCheck(ledger, (), None, False, {}, tuple(
|
|
90
|
+
f"owner={issue.owner} correction=ledger path={issue.path} "
|
|
91
|
+
f"reason=synthesis packet source defect: {issue.reason}"
|
|
92
|
+
for issue in exc.issues
|
|
93
|
+
))
|
|
94
|
+
task_type = str(manifest.get("taskType") or "")
|
|
95
|
+
semantic_validator = None
|
|
96
|
+
if task_type == "implementation-option-selection":
|
|
97
|
+
block_key = TASK_TYPE_DATA_PROPERTY[task_type]
|
|
98
|
+
original_ids = packet.original_requirement_ids
|
|
99
|
+
analysers = packet.participating_analysers
|
|
100
|
+
|
|
101
|
+
def semantic_validator(data: dict[str, Any]) -> list[str]:
|
|
102
|
+
block = data.get(block_key)
|
|
103
|
+
return validate_implementation_option_selection(
|
|
104
|
+
block if isinstance(block, Mapping) else {}, original_ids, analysers,
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
return check_corrections(
|
|
108
|
+
ledger=ledger,
|
|
109
|
+
base_narrative=base_path.read_text(encoding="utf-8"),
|
|
110
|
+
schema=load_schema_version("3.0"),
|
|
111
|
+
block_rules=packet.block_rules,
|
|
112
|
+
semantic_validator=semantic_validator,
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def corrections_payload(
|
|
117
|
+
check: CorrectionsCheck, *, project_root: Path, corrections_path: Path,
|
|
118
|
+
) -> dict[str, Any]:
|
|
119
|
+
"""`check-corrections --json` 의 출력."""
|
|
120
|
+
return {
|
|
121
|
+
"ok": check.ok,
|
|
122
|
+
"correctionsPath": _relative(project_root, corrections_path),
|
|
123
|
+
"mechanical": check.mechanical,
|
|
124
|
+
"corrections": [
|
|
125
|
+
{
|
|
126
|
+
"id": item.get("id"),
|
|
127
|
+
"kind": item.get("kind"),
|
|
128
|
+
"path": item.get("path"),
|
|
129
|
+
"constraints": list(check.constraints.get(str(item.get("id")), ())),
|
|
130
|
+
}
|
|
131
|
+
for item in check.corrections
|
|
132
|
+
],
|
|
133
|
+
"defects": list(check.defects),
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def run_corrections_apply(
|
|
138
|
+
*,
|
|
139
|
+
project_root: Path,
|
|
140
|
+
manifest: Mapping[str, Any],
|
|
141
|
+
manifest_path: Path,
|
|
142
|
+
active_context: Mapping[str, Any],
|
|
143
|
+
team_state: Mapping[str, Any],
|
|
144
|
+
corrections_path: Path,
|
|
145
|
+
) -> dict[str, Any]:
|
|
146
|
+
"""기계적 원장을 서사에 쓰고 활동 행을 남긴다 — `apply-corrections` 의 본체.
|
|
147
|
+
|
|
148
|
+
순서: 대조(결함이면 거절) → rewrite 없음 확인 → 활동 계약·중복 적용 확인 →
|
|
149
|
+
서사 원자적 쓰기 → 활동 행. 활동 계약이 없는 run 은 서사를 쓰기 전에
|
|
150
|
+
거절한다: 행 없는 기계 수정은 감사에서 작성자의 것으로 읽힌다.
|
|
151
|
+
"""
|
|
152
|
+
if not uses_report_contract_v3(manifest):
|
|
153
|
+
raise AgentPromptCliError(
|
|
154
|
+
"apply-corrections requires report contract 3.0 (a Markdown "
|
|
155
|
+
"narrative); this run uses an older contract"
|
|
156
|
+
)
|
|
157
|
+
narrative_path = report_narrative_path(project_root, manifest)
|
|
158
|
+
events_value = manifest.get("leadEventsPath")
|
|
159
|
+
if manifest.get("activityContractVersion") != 1 or not (
|
|
160
|
+
isinstance(events_value, str) and events_value.strip()
|
|
161
|
+
):
|
|
162
|
+
raise AgentPromptCliError(
|
|
163
|
+
"apply-corrections records a `lead-correction-applied` activity row, "
|
|
164
|
+
"and this run manifest declares no activity contract "
|
|
165
|
+
"(activityContractVersion 1 with leadEventsPath); dispatch the "
|
|
166
|
+
"writer with --corrections instead"
|
|
167
|
+
)
|
|
168
|
+
corrections_rel = _relative(project_root, corrections_path)
|
|
169
|
+
check = run_corrections_check(
|
|
170
|
+
project_root=project_root,
|
|
171
|
+
manifest=manifest,
|
|
172
|
+
active_context=active_context,
|
|
173
|
+
team_state=team_state,
|
|
174
|
+
corrections_path=corrections_path,
|
|
175
|
+
narrative_path=narrative_path,
|
|
176
|
+
)
|
|
177
|
+
if check.defects:
|
|
178
|
+
raise AgentPromptCliError(
|
|
179
|
+
"report-writer corrections defects: " + "; ".join(check.defects)
|
|
180
|
+
)
|
|
181
|
+
if not check.mechanical:
|
|
182
|
+
pending = ", ".join(
|
|
183
|
+
str(item.get("id")) for item in check.corrections
|
|
184
|
+
if item.get("kind") == "rewrite"
|
|
185
|
+
)
|
|
186
|
+
raise AgentPromptCliError(
|
|
187
|
+
f"corrections ledger has rewrite entries ({pending}): a rewrite "
|
|
188
|
+
"needs a writer round — materialize the report-writer prompt with "
|
|
189
|
+
"--corrections instead of applying"
|
|
190
|
+
)
|
|
191
|
+
base_path = project_root / str(check.ledger.get("baseNarrativePath") or "")
|
|
192
|
+
if os.path.normpath(base_path) == os.path.normpath(narrative_path):
|
|
193
|
+
raise AgentPromptCliError(
|
|
194
|
+
"baseNarrativePath is the live narrative "
|
|
195
|
+
f"({_relative(project_root, narrative_path)}); applying would "
|
|
196
|
+
"overwrite the only copy of the attempt being corrected. Copy it "
|
|
197
|
+
"to worker-results/report-writer-narrative-a<N>-<task-type>-<seq>.md "
|
|
198
|
+
"first and point the ledger at that copy"
|
|
199
|
+
)
|
|
200
|
+
previous = _already_applied(project_root, manifest_path, corrections_rel)
|
|
201
|
+
if previous is not None:
|
|
202
|
+
raise AgentPromptCliError(
|
|
203
|
+
f"corrections ledger {corrections_rel} was already applied as "
|
|
204
|
+
f"activity {previous}; a further correction needs a new ledger "
|
|
205
|
+
"whose baseNarrativePath is the narrative that apply wrote"
|
|
206
|
+
)
|
|
207
|
+
applied_ids = [str(item.get("id")) for item in check.corrections]
|
|
208
|
+
narrative_rel = _relative(project_root, narrative_path)
|
|
209
|
+
_write_text_atomic(
|
|
210
|
+
narrative_path, render_applied_narrative(check, load_schema_version("3.0")),
|
|
211
|
+
)
|
|
212
|
+
details = {
|
|
213
|
+
"kind": LEAD_CORRECTION_ACTIVITY_KIND,
|
|
214
|
+
"agent": _LEAD_AGENT,
|
|
215
|
+
"summary": (
|
|
216
|
+
f"Applied {len(applied_ids)} mechanical correction(s) "
|
|
217
|
+
f"({', '.join(applied_ids)}) from {corrections_rel} to the report "
|
|
218
|
+
"narrative without a writer round"
|
|
219
|
+
),
|
|
220
|
+
"planItemIds": [],
|
|
221
|
+
"resultPath": narrative_rel,
|
|
222
|
+
"commands": [],
|
|
223
|
+
"evidenceRefs": [corrections_rel, *applied_ids],
|
|
224
|
+
"outcome": "completed",
|
|
225
|
+
}
|
|
226
|
+
try:
|
|
227
|
+
event = record_activity(project_root, manifest_path, details)
|
|
228
|
+
except ActivityProjectionError as exc:
|
|
229
|
+
raise AgentPromptCliError(
|
|
230
|
+
f"narrative written to {narrative_rel} but the activity row was "
|
|
231
|
+
f"refused: {exc}; re-run apply-corrections once the cause is fixed "
|
|
232
|
+
"(the same ledger applies again to the same base)"
|
|
233
|
+
) from exc
|
|
234
|
+
return {
|
|
235
|
+
"ok": True,
|
|
236
|
+
"correctionsPath": corrections_rel,
|
|
237
|
+
"narrativePath": narrative_rel,
|
|
238
|
+
"appliedCorrectionIds": applied_ids,
|
|
239
|
+
"activityId": event.details.get("activityId"),
|
|
240
|
+
"activityLine": (
|
|
241
|
+
f"ACTIVITY: id={event.details.get('activityId')} agent={_LEAD_AGENT} "
|
|
242
|
+
f"kind={LEAD_CORRECTION_ACTIVITY_KIND} corrections={corrections_rel} "
|
|
243
|
+
f"result={narrative_rel}"
|
|
244
|
+
),
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
def _already_applied(
|
|
249
|
+
project_root: Path, manifest_path: Path, corrections_rel: str,
|
|
250
|
+
) -> str | None:
|
|
251
|
+
"""이 원장을 이미 적용한 활동 행의 id. 없으면 None."""
|
|
252
|
+
try:
|
|
253
|
+
rows = agent_activity_rows(project_root, manifest_path)
|
|
254
|
+
except ActivityProjectionError as exc:
|
|
255
|
+
raise AgentPromptCliError(f"activity ledger is unreadable: {exc}") from exc
|
|
256
|
+
for row in rows:
|
|
257
|
+
refs = row.get("evidenceRefs")
|
|
258
|
+
if (
|
|
259
|
+
row.get("kind") == LEAD_CORRECTION_ACTIVITY_KIND
|
|
260
|
+
and isinstance(refs, list)
|
|
261
|
+
and corrections_rel in refs
|
|
262
|
+
):
|
|
263
|
+
return str(row.get("activityId"))
|
|
264
|
+
return None
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
def _write_text_atomic(path: Path, text: str) -> None:
|
|
268
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
269
|
+
descriptor, temporary = tempfile.mkstemp(
|
|
270
|
+
prefix=f".{path.name}.", suffix=".tmp", dir=path.parent,
|
|
271
|
+
)
|
|
272
|
+
try:
|
|
273
|
+
with os.fdopen(descriptor, "w", encoding="utf-8") as handle:
|
|
274
|
+
handle.write(text)
|
|
275
|
+
handle.flush()
|
|
276
|
+
os.fsync(handle.fileno())
|
|
277
|
+
os.replace(temporary, path)
|
|
278
|
+
finally:
|
|
279
|
+
if os.path.exists(temporary):
|
|
280
|
+
os.unlink(temporary)
|