okstra 0.199.4 → 0.200.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.
Files changed (42) hide show
  1. package/README.md +4 -1
  2. package/dist/lib/runtime-payload.mjs +1 -0
  3. package/dist/lib/runtime-payload.mjs.map +1 -1
  4. package/docs/cli.md +10 -0
  5. package/package.json +1 -1
  6. package/runtime/BUILD.json +2 -2
  7. package/runtime/bin/okstra-provider-exec.py +1 -1
  8. package/runtime/bin/okstra-zai-exec.sh +5 -0
  9. package/runtime/prompts/launch.template.md +12 -0
  10. package/runtime/prompts/lead/okstra-lead-contract.md +3 -1
  11. package/runtime/prompts/lead/plan-body-verification.md +16 -4
  12. package/runtime/prompts/profiles/implementation-planning.md +1 -1
  13. package/runtime/prompts/wizard/prompts.ko.json +1 -0
  14. package/runtime/python/okstra_ctl/adapters/hosts/codex/relay.md +18 -4
  15. package/runtime/python/okstra_ctl/adapters/providers/zai/adapter.py +96 -0
  16. package/runtime/python/okstra_ctl/adapters/providers/zai/manifest.json +5 -0
  17. package/runtime/python/okstra_ctl/agent/prompt_cli/corrections.py +6 -3
  18. package/runtime/python/okstra_ctl/agent/prompt_cli/materialize.py +23 -1
  19. package/runtime/python/okstra_ctl/consumers.py +0 -55
  20. package/runtime/python/okstra_ctl/domain/worker_exec.py +5 -2
  21. package/runtime/python/okstra_ctl/domain/write_policy.py +7 -1
  22. package/runtime/python/okstra_ctl/execution_mutation_audit.py +6 -17
  23. package/runtime/python/okstra_ctl/final_report_schema.py +16 -16
  24. package/runtime/python/okstra_ctl/plan_items_cli.py +6 -0
  25. package/runtime/python/okstra_ctl/report_assembly.py +4 -1
  26. package/runtime/python/okstra_ctl/report_corrections.py +24 -15
  27. package/runtime/python/okstra_ctl/report_finalize.py +1 -1
  28. package/runtime/python/okstra_ctl/report_narrative.py +11 -6
  29. package/runtime/python/okstra_ctl/report_projections.py +1 -0
  30. package/runtime/python/okstra_ctl/report_synthesis_packet.py +1 -1
  31. package/runtime/python/okstra_ctl/wizard/confirmation.py +4 -0
  32. package/runtime/python/okstra_ctl/wizard/engine.py +4 -16
  33. package/runtime/python/okstra_ctl/wizard/picker_navigation.py +7 -37
  34. package/runtime/python/okstra_ctl/worker_runner.py +7 -2
  35. package/runtime/python/okstra_ctl/worktree_status_cli.py +2 -3
  36. package/runtime/python/okstra_ctl/write_policy.py +51 -4
  37. package/runtime/python/okstra_token_usage/claude.py +29 -0
  38. package/runtime/python/okstra_token_usage/collect.py +11 -1
  39. package/runtime/schemas/execution-manifest-v2.schema.json +1 -0
  40. package/runtime/schemas/final-report-v2.0.schema.json +3 -2
  41. package/runtime/schemas/final-report-v3.0.schema.json +3 -2
  42. package/runtime/skills/okstra-run/SKILL.md +9 -1
package/README.md CHANGED
@@ -37,6 +37,7 @@ Role assignments are persisted separately from the host runtime, but the lead pr
37
37
  | Antigravity | Antigravity CLI | `okstra-antigravity-exec.sh` | leader, analyser, critic, implementer, verifier |
38
38
  | Grok | Grok CLI | `okstra-grok-exec.sh` | leader, read-only analyser, and critic |
39
39
  | Kimi | Kimi CLI | `okstra-kimi-exec.sh` | leader, read-only analyser, and critic |
40
+ | Z.ai GLM | — | `okstra-zai-exec.sh` (Claude Code) | analyser, critic, designer, planner, implementer, verifier, report-writer, translator |
40
41
 
41
42
  Canonical role names are `leader`, `analyser`, `critic`, `designer`, `planner`, `implementer`, `verifier`, `report-writer`, and `translator`. `lead` is a compatibility alias for `leader`. `executor` is a compatibility alias for `implementer`. New records write only the canonical names.
42
43
 
@@ -46,7 +47,9 @@ Grok and Kimi can now lead through their registered host adapters. Their worker
46
47
 
47
48
  `okstra run <host-id-or-alias>` resolves the requested adapter from the host registry and checks its `spawn-process` readiness before starting it. The installed `okstra-run` skill instead uses `current-session`: it declares only the semantic functions the live harness can actually perform (`plain_text_input`, plus any available native single-select, multi-select, or grouped-question function) and reuses the current lead session. User-installed adapters are discovered only from `~/.okstra/adapters/hosts/<id>/` and `~/.okstra/adapters/providers/<id>/`; project-local executable adapter code is ignored.
48
49
 
49
- The next direct-CLI candidates are Mistral Vibe and Qwen Code. DeepSeek V4, GLM-5.2, and MiniMax M2.7 remain API-adapter candidates because their current official coding-agent execution surfaces do not fit Okstra's wrapper contract as directly. Newly published provider models are exposed only after the corresponding local CLI reports them; this keeps model discovery separate from marketing availability.
50
+ Z.ai GLM workers reuse the installed Claude Code executable with process-local Z.ai connection settings. Set `ZAI_API_KEY` in the environment that launches Okstra, then select `zai/glm-5.3` or `zai/glm-5.3-flash` for a worker role. Claude configuration files remain unchanged. See [GLM worker setup](docs/cli.md#zai-glm-workers) for isolation and accounting details.
51
+
52
+ The next direct-CLI candidates are Mistral Vibe and Qwen Code. DeepSeek V4 and MiniMax M2.7 remain API-adapter candidates because their current official coding-agent execution surfaces do not fit Okstra's wrapper contract as directly. Model catalog entries and account availability are distinct; GLM authorization is checked when its CLI process calls Z.ai.
50
53
 
51
54
  okstra is **not** a one-shot code review tool. It is for work that **spans multiple phases, needs input from multiple agents, and feeds each phase's output into the next phase**.
52
55
 
@@ -15,6 +15,7 @@ export const RUNTIME_BIN_FILES = Object.freeze([
15
15
  { name: "okstra-antigravity-exec.sh" },
16
16
  { name: "okstra-grok-exec.sh" },
17
17
  { name: "okstra-kimi-exec.sh" },
18
+ { name: "okstra-zai-exec.sh" },
18
19
  { name: "okstra-provider-exec.py" },
19
20
  // 진입 스크립트들이 import 하는 경로 부트스트랩. 이들과 같은
20
21
  // 디렉터리에 있어야 `sys.path` 를 고치기 전에 import 된다.
@@ -1 +1 @@
1
- {"version":3,"file":"runtime-payload.mjs","sourceRoot":"","sources":["../../src/lib/runtime-payload.mts"],"names":[],"mappings":"AAAA,uBAAuB;AACvB,EAAE;AACF,oCAAoC;AACpC,0DAA0D;AAC1D,iEAAiE;AACjE,gEAAgE;AAChE,kDAAkD;AAClD,EAAE;AACF,kCAAkC;AAalC,yDAAyD;AACzD,MAAM,CAAC,MAAM,iBAAiB,GAA8B,MAAM,CAAC,MAAM,CAAC;IACxE,EAAE,IAAI,EAAE,WAAW,EAAE;IACrB,EAAE,IAAI,EAAE,sBAAsB,EAAE;IAChC,EAAE,IAAI,EAAE,uBAAuB,EAAE;IACjC,EAAE,IAAI,EAAE,4BAA4B,EAAE;IACtC,EAAE,IAAI,EAAE,qBAAqB,EAAE;IAC/B,EAAE,IAAI,EAAE,qBAAqB,EAAE;IAC/B,EAAE,IAAI,EAAE,yBAAyB,EAAE;IACnC,uCAAuC;IACvC,2CAA2C;IAC3C,EAAE,IAAI,EAAE,qBAAqB,EAAE;IAC/B,EAAE,IAAI,EAAE,wBAAwB,EAAE;IAClC,EAAE,IAAI,EAAE,4BAA4B,EAAE;IACtC,EAAE,IAAI,EAAE,0BAA0B,EAAE;IACpC,EAAE,IAAI,EAAE,qBAAqB,EAAE,MAAM,EAAE,IAAI,EAAE;IAC7C,EAAE,IAAI,EAAE,+BAA+B,EAAE,MAAM,EAAE,IAAI,EAAE;IACvD,EAAE,IAAI,EAAE,+BAA+B,EAAE,MAAM,EAAE,IAAI,EAAE;IACvD,EAAE,IAAI,EAAE,+BAA+B,EAAE,MAAM,EAAE,IAAI,EAAE;IACvD,EAAE,IAAI,EAAE,4BAA4B,EAAE,MAAM,EAAE,IAAI,EAAE;IACpD,EAAE,IAAI,EAAE,2BAA2B,EAAE,MAAM,EAAE,IAAI,EAAE;IACnD,EAAE,IAAI,EAAE,uBAAuB,EAAE,MAAM,EAAE,IAAI,EAAE;CAChD,CAAC,CAAC;AAEH,0DAA0D;AAC1D,MAAM,CAAC,MAAM,uBAAuB,GAAsB,MAAM,CAAC,MAAM,CAAC;IACtE,gBAAgB;IAChB,YAAY;IACZ,oBAAoB;IACpB,eAAe;CAChB,CAAC,CAAC;AAEH;;;GAGG;AACH,MAAM,CAAC,MAAM,iBAAiB,GAA2C,MAAM,CAAC,MAAM,CAAC;IACrF,CAAC,oBAAoB,EAAE,gBAAgB,CAAC;IACxC,CAAC,QAAQ,EAAE,QAAQ,CAAC;IACpB,CAAC,SAAS,EAAE,SAAS,CAAC;IACtB,CAAC,SAAS,EAAE,SAAS,CAAC;IACtB,CAAC,WAAW,EAAE,WAAW,CAAC;IAC1B,CAAC,YAAY,EAAE,YAAY,CAAC;IAC5B,CAAC,QAAQ,EAAE,QAAQ,CAAC;CACZ,CAAC,CAAC;AAEZ,uDAAuD;AACvD,MAAM,CAAC,MAAM,oBAAoB,GAAsB,MAAM,CAAC,MAAM,CAClE,iBAAiB,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,IAAI,CAAC,CAC7C,CAAC;AAEF,uDAAuD;AACvD,MAAM,CAAC,MAAM,mBAAmB,GAAsB,MAAM,CAAC,MAAM,CACjE,iBAAiB,CAAC,MAAM,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,IAAI,CAAC,CAC7E,CAAC"}
1
+ {"version":3,"file":"runtime-payload.mjs","sourceRoot":"","sources":["../../src/lib/runtime-payload.mts"],"names":[],"mappings":"AAAA,uBAAuB;AACvB,EAAE;AACF,oCAAoC;AACpC,0DAA0D;AAC1D,iEAAiE;AACjE,gEAAgE;AAChE,kDAAkD;AAClD,EAAE;AACF,kCAAkC;AAalC,yDAAyD;AACzD,MAAM,CAAC,MAAM,iBAAiB,GAA8B,MAAM,CAAC,MAAM,CAAC;IACxE,EAAE,IAAI,EAAE,WAAW,EAAE;IACrB,EAAE,IAAI,EAAE,sBAAsB,EAAE;IAChC,EAAE,IAAI,EAAE,uBAAuB,EAAE;IACjC,EAAE,IAAI,EAAE,4BAA4B,EAAE;IACtC,EAAE,IAAI,EAAE,qBAAqB,EAAE;IAC/B,EAAE,IAAI,EAAE,qBAAqB,EAAE;IAC/B,EAAE,IAAI,EAAE,oBAAoB,EAAE;IAC9B,EAAE,IAAI,EAAE,yBAAyB,EAAE;IACnC,uCAAuC;IACvC,2CAA2C;IAC3C,EAAE,IAAI,EAAE,qBAAqB,EAAE;IAC/B,EAAE,IAAI,EAAE,wBAAwB,EAAE;IAClC,EAAE,IAAI,EAAE,4BAA4B,EAAE;IACtC,EAAE,IAAI,EAAE,0BAA0B,EAAE;IACpC,EAAE,IAAI,EAAE,qBAAqB,EAAE,MAAM,EAAE,IAAI,EAAE;IAC7C,EAAE,IAAI,EAAE,+BAA+B,EAAE,MAAM,EAAE,IAAI,EAAE;IACvD,EAAE,IAAI,EAAE,+BAA+B,EAAE,MAAM,EAAE,IAAI,EAAE;IACvD,EAAE,IAAI,EAAE,+BAA+B,EAAE,MAAM,EAAE,IAAI,EAAE;IACvD,EAAE,IAAI,EAAE,4BAA4B,EAAE,MAAM,EAAE,IAAI,EAAE;IACpD,EAAE,IAAI,EAAE,2BAA2B,EAAE,MAAM,EAAE,IAAI,EAAE;IACnD,EAAE,IAAI,EAAE,uBAAuB,EAAE,MAAM,EAAE,IAAI,EAAE;CAChD,CAAC,CAAC;AAEH,0DAA0D;AAC1D,MAAM,CAAC,MAAM,uBAAuB,GAAsB,MAAM,CAAC,MAAM,CAAC;IACtE,gBAAgB;IAChB,YAAY;IACZ,oBAAoB;IACpB,eAAe;CAChB,CAAC,CAAC;AAEH;;;GAGG;AACH,MAAM,CAAC,MAAM,iBAAiB,GAA2C,MAAM,CAAC,MAAM,CAAC;IACrF,CAAC,oBAAoB,EAAE,gBAAgB,CAAC;IACxC,CAAC,QAAQ,EAAE,QAAQ,CAAC;IACpB,CAAC,SAAS,EAAE,SAAS,CAAC;IACtB,CAAC,SAAS,EAAE,SAAS,CAAC;IACtB,CAAC,WAAW,EAAE,WAAW,CAAC;IAC1B,CAAC,YAAY,EAAE,YAAY,CAAC;IAC5B,CAAC,QAAQ,EAAE,QAAQ,CAAC;CACZ,CAAC,CAAC;AAEZ,uDAAuD;AACvD,MAAM,CAAC,MAAM,oBAAoB,GAAsB,MAAM,CAAC,MAAM,CAClE,iBAAiB,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,IAAI,CAAC,CAC7C,CAAC;AAEF,uDAAuD;AACvD,MAAM,CAAC,MAAM,mBAAmB,GAAsB,MAAM,CAAC,MAAM,CACjE,iBAAiB,CAAC,MAAM,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,IAAI,CAAC,CAC7E,CAAC"}
package/docs/cli.md CHANGED
@@ -781,6 +781,16 @@ It then displays `Continue? [y/yes]:`. Any response other than `y` or `yes` exit
781
781
 
782
782
  ---
783
783
 
784
+ ## Z.ai GLM workers
785
+
786
+ Z.ai is a worker provider (`zai`) that uses the existing `claude` executable. Install Claude Code, provide `ZAI_API_KEY` in the environment inherited by Okstra, and select `zai/glm-5.3` or `zai/glm-5.3-flash` in a role-model step. For explicit launch arguments, use `--role-model analyser=zai/glm-5.3`. These models support worker roles; this integration does not provide a Z.ai lead host.
787
+
788
+ Each GLM child process uses `https://api.z.ai/api/anthropic` and its own model/authentication environment. Okstra does not edit `~/.claude/settings.json`. GLM runs with `--setting-sources ""` so user, project, and local settings cannot override those connection values; settings-based hooks and integrations are consequently not inherited for GLM workers. The regular Claude provider keeps its existing settings behavior.
789
+
790
+ The runtime rejects a missing `ZAI_API_KEY` before starting the worker. Catalog visibility does not verify the account's model entitlement or remaining quota. The installed Claude Code version and Z.ai account must support the selected model. See [Z.ai's Claude Code setup](https://docs.z.ai/devpack/tool/claude) and [Coding Plan usage policy](https://docs.z.ai/devpack/usage-policy) for provider requirements.
791
+
792
+ GLM response model identities are recorded as `zai/<model>`; a startup model label alone is not treated as proof of the served model. The final Claude Code usage event supplies input, output, cache-creation, and cache-read counters. Repeated dispatches are summed by their status files. Missing usage stays unavailable, and billing-cost estimates are omitted rather than using Anthropic prices or treating subscription usage as metered API spend.
793
+
784
794
  ## `okstra` Node CLI — introspection subcommands
785
795
 
786
796
  The `okstra` Node CLI (`bin/okstra`) provides both installer/admin commands and introspection commands used by skills and agents. It goes through the Node wrapper instead of invoking the Python runtime directly, so `src/lib/python-helper.mts` wires `PYTHONPATH`.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "okstra",
3
- "version": "0.199.4",
3
+ "version": "0.200.0",
4
4
  "description": "Host-aware multi-provider cross-verification orchestrator runtime and agent skills.",
5
5
  "license": "MIT",
6
6
  "author": "devonshin",
@@ -1,5 +1,5 @@
1
1
  {
2
- "package": "0.199.4",
3
- "builtAt": "2026-09-12T08:39:25.893Z",
2
+ "package": "0.200.0",
3
+ "builtAt": "2026-09-12T23:30:51.348Z",
4
4
  "repoRoot": "/home/runner/work/okstra/okstra"
5
5
  }
@@ -405,7 +405,7 @@ def _check_command(strategy: ExecutionStrategy, request: WorkerExecRequest) -> N
405
405
  """Refuse a missing CLI before the run leaves any artifact behind.
406
406
 
407
407
  Building the command is the only truthful way to learn which binary this
408
- provider runs, and it is a pure call. The check stays out of the runner so a
408
+ provider runs and validates its configuration. The check stays out of the runner so a
409
409
  refused dispatch leaves no `started` status sidecar for the liveness probe to
410
410
  read as a worker that launched.
411
411
  """
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)"
5
+ exec python3 "$script_dir/okstra-provider-exec.py" zai "$@"
@@ -41,6 +41,18 @@ Relaying a wizard step does not change the option set — relay every `options[]
41
41
 
42
42
  ## User closeout (BLOCKING)
43
43
 
44
+ Progress, remaining work, and recommendation
45
+
46
+ At each phase or implementation-stage boundary, at task completion, and before a controlled session pause or handoff, give a concise update in the resolved Report Language. Identify the task and the phase or stage, then include these three items (translate the labels):
47
+
48
+ - Progress: what this boundary actually completed, the result of checks already run, and links to the relevant persisted evidence. Distinguish a completed phase from a completed task; a dispatched worker is still in progress.
49
+ - Remaining work: unfinished work in the current task, open approval or input blockers with their IDs, and any registered follow-up tasks. Say explicitly when none remains in the verified scope; when state is unavailable, say unknown rather than inventing completion or a backlog.
50
+ - Recommendation: the next concrete action and why it comes next. After finalization, use `nextCommand` and `nextRecommendedPhase.rationale` from the result under the routing rules below, including `nextInGroup` when present. Before finalization, use the active run's recorded stage queue and recovery or resume information; do not reuse an earlier run's pointer or invent a resume command.
51
+
52
+ During an authorized continuous run, put the update beside the existing progress checkpoint and continue to the next queued step without asking for confirmation merely to deliver this update. At a controlled session pause or handoff, distinguish persisted results from in-progress work and include the available resume reference. At task completion, distinguish remaining work in this task from follow-ups or the next task in its group. If there is no recorded next action, say none is recommended. These updates do not replace checkpoints or authorize another task.
53
+
54
+ Prompt delivery is checked by `tests/contract/test_next_phase_authoring_delivery.py::test_lead_closeout_names_a_command_the_user_can_run`; this is guidance for the lead's prose, not runtime validation of the emitted message.
55
+
44
56
  After Phase 7 persistence, the last user-facing message of this run is the next command. A status dump is not a close. A prohibition (`do not start implementation`) is not a next action. This applies to every task type.
45
57
 
46
58
  **Read the pointer from the `report-finalize` result — do not re-derive it.** Its top-level `nextRecommendedPhase` object (`phase`, `status`, `rationale`) is the value every row below branches on, and the same three values are repeated on stderr as `next phase status:` / `next phase:` / `next phase rationale:`. You did not write that object and you cannot recompute it: you author your task type's routing field, and Phase 7 projects the pointer from it plus the approval state — for `implementation-planning` there is no routing field at all. If the result carries `nextRecommendedPhaseError`, say in one line that the pointer could not be read, then close on the `validate-run` row below. The same result also carries `nextCommand` — `{command, note}`, the table below already applied to this run. When `command` is non-empty it is the close; when it is empty the `note` says what to do with the `rationale` instead.
@@ -104,6 +104,8 @@ User-utterance interpretation rule:
104
104
 
105
105
  ## Progress reporting (BLOCKING)
106
106
 
107
+ At each phase or implementation-stage boundary, at task completion, and before a controlled session pause or handoff, follow the launch prompt's "Progress, remaining work, and recommendation" guidance. Include the result and evidence, unfinished work and blockers, and the next action with its reason. During an authorized continuous run, deliver this update beside the checkpoint and continue; do not turn the update into an approval gate. The same guidance applies to the final reply after persistence below.
108
+
107
109
  A single okstra run frequently spans 30–120 minutes with multi-minute silent windows while workers run; without progress signals the user cannot distinguish "still working" from "hung". Lead MUST emit a single short progress line at each checkpoint below — plain user-facing text in a separate brief message (not buried inside a tool call), one line per checkpoint, format: `PROGRESS: <phase-id> <verb-phrase>`. Emit the line raw — the literal `PROGRESS:` token must begin the line. Do NOT wrap it in inline-code backticks (`` `PROGRESS: ...` ``) or a ```` ``` ```` code fence; markdown wrapping is what the post-hoc conformance validator scrapes around, and raw emit keeps the signal unambiguous.
108
110
 
109
111
  Record each checkpoint with `okstra lead-progress append --project-root <dir> --run-manifest <path> --phase <phase-id>`, then emit the `progressLine` it prints as the user-facing line. The command resolves `leadEventsPath` from the run manifest and writes the checkpoint there — on a host whose adapter declares `sessionAccounting: artifact-only` that ledger is the only place the post-hoc validator can read it, so a conversation line alone leaves no trace and the run is reported as missing the checkpoint. Pass `--worker <role>` on the per-worker checkpoints (it rewrites a phase-specific functional label into the roster role team-state records), `--field NAME=VALUE` for the remaining `key=value` tokens in the order the line carries them, and `--detail <text>` where the line ends in a verb phrase; the wording listed below is the default for the fixed-prose checkpoints.
@@ -355,7 +357,7 @@ After each worker terminates, BEFORE classifying its terminal status, verify the
355
357
 
356
358
  `--agent`, `--agent-role`, and `--error-type` are **closed enums**, not free-form labels — the role names used elsewhere in these contracts (`Codex worker`, `Claude worker`) are rejected. Use exactly:
357
359
 
358
- - `--agent` — `claude-worker` | `codex-worker` | `antigravity-worker` | `grok-worker` | `kimi-worker` | `report-writer`
360
+ - `--agent` — `claude-worker` | `codex-worker` | `antigravity-worker` | `grok-worker` | `kimi-worker` | `zai-worker` | `report-writer`
359
361
  - `--agent-role` — `lead` | `worker` | `report-writer`
360
362
  - `--error-type` — `cli-failure` | `contract-violation` | `tool-failure`
361
363
 
@@ -335,10 +335,20 @@ Every plan-body verification and self-fix instruction passes [okstra-lead-contra
335
335
 
336
336
  Before each verifier call, write one task-instructions file under the current
337
337
  run's `state/` directory and run `okstra agent-prompt materialize` with
338
- `--audience reverification-worker`,
339
- `--assignment-ref reverify/<workerId>`, the exact `--worker-id`, and
338
+ `--audience reverification-worker`, the exact `--worker-id`, and
340
339
  `--dispatch-kind plan-verify-r<N>`, matching the `<N>` in the result filename of
341
- step 3. That kind, not `reverify-r<N>`, is what this round takes: the verification
340
+ step 3. For analyser verifiers, use `--assignment-ref reverify/<workerId>`.
341
+ For `critic-worker`, use `--assignment-ref critic/scope` from this run's
342
+ `invocationAssignments`, including a tie round or a later critic correction.
343
+ The verdict worker name `critic-worker` does not create a `reverify/critic-worker`
344
+ assignment. Keep the selected critic's existing model assignment.
345
+ **Enforced:** `okstra_ctl.agent.prompt_cli.run_identity._validate_run_identity`
346
+ accepts the planning critic's verification audience; materialization resolves
347
+ the declared assignment and reports the correct critic reference when the
348
+ invented `reverify/critic-worker` reference is used.
349
+ `tests/run/test_agent_prompt_cli.py` verifies the declared critic's reservation
350
+ and dispatch identity plus this recovery diagnostic.
351
+ That kind, not `reverify-r<N>`, is what this round takes: the verification
342
352
  prompt contract requires the renderer signature of the queue's canonical renderer,
343
353
  and for plan items that renderer is `okstra plan-items prompt`, not `okstra
344
354
  convergence reverify-prompt`. Pass that command's output verbatim as
@@ -355,7 +365,9 @@ source `RoleExecution` row from the run manifest's static role state. Use its
355
365
  `RoleExecution` row's `roleExecutionRef`, not that row's
356
366
  `sourceRoleExecutionRef` field. Pass `--source-role-execution-ref
357
367
  <sourceRoleExecutionRef>`. Do not derive that reference from `workerId`,
358
- provider, model, or execution-label text. A legacy v1 run omits this flag. Run
368
+ provider, model, or execution-label text. For `critic-worker`, select the static
369
+ `role: critic` row; the runtime reserves its verifier execution under the
370
+ existing critic participant and model. A legacy v1 run omits this flag. Run
359
371
  `okstra agent-prompt verify` against the
360
372
  returned `metadataPath` before dispatch and use the returned `promptPath`
361
373
  without modification. Native-session calls use only `hostModelValue`; before
@@ -175,7 +175,7 @@ roles:
175
175
  - `### Carry-In` — for `depends-on (none)`: task-brief only. Otherwise: each depended-on stage's static exit contract + runtime sidecar path `runs/<impl-key>/carry/stage-<i>.json` placeholder.
176
176
  - `### Stepwise Execution Order` — bite-sized table with `step | action | files | command | outcome | expected`. `outcome` is one word — `PASS` or `FAIL` — and `expected` is the sentence saying what that looks like here; a verdict written inside the sentence is not read as one. The `files` cell lists each touched path in full and `<PROJECT_ROOT>`-relative — never ellipsis-abbreviated (`…` / `...`), which does not resolve and is rejected by plan-body verification as a kind-b path mismatch. **The narrative row additionally carries `plannedPaths`: the same paths as an array, one repository-relative path per entry, with no globs, exclusions, counts or commentary.** `files` is the sentence a reader sees; `plannedPaths` is the ledger report assembly preserves and the implementer write policy enforces. When a step legitimately covers a set too large to enumerate, split it or name the directory the set lives under. **Effective row count ≤ 8** (excluding header / divider / blank). Each step is one cohesive, self-contained change. **TDD ordering is MUST, not a preference:** the **first** effective step's `action` cell MUST start with the literal `RED:` and describe the failing test(s) that capture this stage's `Acceptance` **and the three declared `Test case (success|boundary|failure)` lines** (`outcome` = `FAIL`); at least one later `action` cell MUST start with the literal `GREEN:` and describe the minimal implementation that makes it pass (`outcome` = `PASS`); an optional refactor step starts with `REFACTOR:`. **Exemption:** doc-only / config-only / pure-rename stages with no observable runtime behaviour may omit RED/GREEN by declaring one line `TDD exemption: <reason>` in the stage section. A stage that is truthfully none of those three declares `TDD exemption: user-bypass — <the user's words>`, which holds only while the user has granted it for that stage with `okstra prepare --tdd-bypass "<stage>:<reason>"` — never file the nearest of the three instead. Validator S10c enforces RED-first + GREEN; the `outcome` cell agreeing with its prefix is a schema conditional. S10e rejects an unsupported exemption reason and a `user-bypass` with no user grant (`validators/validate-implementation-plan-stages.py`).
177
177
  - **The `command` cell runs inside an okstra task worktree, not a bare checkout (BLOCKING).** okstra provisions `.okstra`, the configured sync entries (`.project-docs`, `.claude`, …), and — for `implementation` — a nested `stage-<N>/` worktree into the tree the step executes in. Two consequences bind every command you write:
178
- - **Clean-tree assertions use `okstra worktree-status --check-clean`.** A bare `git status --porcelain` is never empty there, so an assertion built on one fails on okstra's scaffolding rather than on the stage's work. The okstra command asks the same question over source paths only and exits 1 when dirty, so it stands alone as a step's assertion: `okstra worktree-status --check-clean`. Validator S13 rejects the bare form. Do not add a `git tag stage-<N>-exit` to the step okstra writes that tag itself when it settles the stage, at the commit the carry evidence records, and a step that tags mid-stage puts it on an earlier commit.
178
+ - **Clean-tree assertions use `okstra worktree-status --check-clean`.** A bare `git status --porcelain` is never empty there, so an assertion built on one fails on okstra's scaffolding rather than on the stage's work. The okstra command asks the same question over source paths only and exits 1 when dirty, so it stands alone as a step's assertion: `okstra worktree-status --check-clean`. Validator S13 rejects the bare form. Do not add a `git tag stage-<N>-exit` to the step. Stage completion records the commit in the consumer ledger without creating or moving git tags.
179
179
  - **Never read an `.okstra/` artifact back out of a git object.** `.okstra/**` is gitignored and never committed — the executor aborts a commit that stages an ignored path and the verifier reports a committed `.okstra` path as a branch defect — so `git cat-file -e <tag>:.okstra/…`, `git show <tag>:.okstra/…`, and every variant of that read can never resolve, at any tag, in any stage. A later stage that needs a QA artifact reads it from the working tree or receives it through the carry sidecar / verifier result; do not design a stage contract around one being reachable from a tag. Validator S12 rejects the read.
180
180
  - **Per-stage conformance declaration (mandatory one line, in the stage section — same placement freedom as `TDD exemption:`):** the stage MUST carry exactly one of:
181
181
  - `Conformance tests: stage-<N> — <task_root>/qa/scripts/stage-<N>.<ext> (requires=[db|io|http|external,...])` — declare that a Tier3 verification script will prove this stage's upstream requirements (brief / requirements-discovery / error-analysis / improvement-discovery → this stage's `Acceptance`) hold against **real** DB rows, real endpoints, or the real external API — NOT mocks. This phase emits the line and the `requires` set only. Do NOT write `<task_root>/qa/scripts/stage-<N>.*` and do NOT add a `runCommand` or `conformance-manifest.json` entry here — the matching `implementation` stage run creates the script file and the manifest `runCommand`. A plan that declares tests with no script file on disk is valid at this gate. The data.json `conformanceTests` value carries only the remainder after the `Conformance tests: stage-<N> — ` prefix — never the `stage-<N> — ` label itself (report assembly strips a leftover label at publication, and the implementation entry gate rejects one).
@@ -646,6 +646,7 @@
646
646
  },
647
647
  "confirmation": {
648
648
  "header": "선택 확인:",
649
+ "provider_data_scope": "\n전달 대상: 위 역할·모델 목록의 제공자(현재 세션 및 선택한 외부 모델 제공자).\n전달 자료: `{project_root}`의 이 작업에 대한 작업 개요, 작업 수행에 필요한 저장소 소스·문서, 선택한 근거 자료 및 실행 중 생성되는 관련 분석·검증 결과.\n진행을 선택하면 위 대상에 해당 자료를 전달하여 선택한 작업을 실행하는 것을 승인합니다. 선택에 없는 제공자나 작업과 무관한 자료는 승인 범위에 포함되지 않습니다. 실행 환경의 권한 검토는 별도로 적용됩니다.",
649
650
  "static_role": " static-role : {role}#{ordinal} / {model}",
650
651
  "dynamic_role": " dynamic-role : {role} / reuse selected participant model",
651
652
  "workers_implementation_default": " workers : (프로필 기본 — executor + verifier 2 + report-writer)",
@@ -136,11 +136,11 @@ The `confirm` prompt's `label` is the selection summary (one line per resolved i
136
136
 
137
137
  ### Runtime-generated selectable screens
138
138
 
139
- `wizard/engine.py` adapts choice screens before returning `next` when the session declares `native_single_select`. `wizard/picker_navigation.py` preserves all original choices while paging long lists, collecting multi-selection through toggle/complete choices, and disambiguating duplicate labels. Oversized or unsupported groups are presented one member at a time. These paths are exercised by `tests/domain/wizard/test_picker_navigation.py` and `test_role_model_selection.py`.
139
+ `wizard/engine.py` adapts choice screens before returning `next` when the session declares `native_single_select`. Lists use same-screen question tabs only when the host supports the complete grouped selection. Otherwise, choices beyond `nativeLimits` and unsupported multi-selections use a complete numbered list. Codex has no native multi-select, so a single choice with more than three options uses `numbered-single`; multi-selection uses `numbered-multi`. Oversized or unsupported groups are presented one member at a time, with every option for that member visible. These paths are exercised by `tests/domain/wizard/test_picker_navigation.py` and `test_role_model_selection.py`.
140
140
 
141
- Render exactly the returned current screen using its `interaction.kind`, including navigation and completion options. Submit their original values through `okstra wizard step` like other options. The runtime keeps navigation and partial selections in the state file and does not submit the underlying workflow decision until selection is complete. Do not reconstruct the full model list, perform pagination yourself, or replace model choices with a request to type `provider/model`. A repeated step ID after navigation or a toggle is the next screen, not a duplicated question; render that newly returned screen once.
141
+ Render the returned screen using its `interaction.kind`. For numbered interactions, show every option label and description in original order in one message, then accept a number, label, or value; multiple choices accept comma-separated input. Submit that reply unchanged through `okstra wizard step`. Do not add a next-page choice, truncate the list, or require the user to remember a model identifier.
142
142
 
143
- If the runtime still returns a numbered interaction when the user requires a selector, preserve the state and check installation and live capabilities. Do not invent a text-input exception. Text steps remain text steps; a direct-input choice is selected through the picker before the runtime asks for the custom value.
143
+ The complete numbered list is the intended fallback when a choice cannot fit the native control or same-screen tabs, including when the user generally prefers a selector. Text steps remain text steps; selecting the direct-input option opens the wizard's custom-value step.
144
144
 
145
145
  ### Plan decisions and execution permissions
146
146
 
@@ -168,7 +168,7 @@ Display the question only through the selected tool. Do not print it or its opti
168
168
  |---|---|
169
169
  | `read_artifacts` | Read the manifest-provided paths through the current host's file interface. |
170
170
  | `write_artifact` | Write only core-authorized `.okstra/` artifacts and preserve their schemas. |
171
- | `prompt_user` | Use the client-appropriate, mode-available question tool selected above for clarifications that fit `nativeLimits`. Show the question once through that tool and wait for the actual answer. Emit the question as the last thing in that turn: assistant text emitted after the call renders below the question and separates it from the user's answer. Follow the host's separate restrictions for permission requests. Use host text for unsupported interactions only when the user has not required a selectable interface; otherwise preserve the pending step and follow the recovery above. |
171
+ | `prompt_user` | Use the client-appropriate, mode-available question tool selected above for clarifications that fit `nativeLimits`. Show the question once through that tool and wait for the actual answer. Emit the question as the last thing in that turn: assistant text emitted after the call renders below the question and separates it from the user's answer. Follow the host's separate restrictions for permission requests. For lists that cannot fit the native control or same-screen tabs, show the complete numbered list and accept the next message as the answer. |
172
172
  | `dispatch_worker` | Verify each materialized invocation first. Dispatch `runner=native-session` with the current Codex host's primitive, the returned `promptPath`, and `hostModelValue`. Pass `runner=cli-wrapper` assignments to `okstra worker-dispatch --project-root <root> --run-manifest <path> --workers <ids>`; use `--dry-run` first when required. **Not in a cmux run:** when `terminalBackend` is `cmux-pane`, the cmux adapter overrides this row. |
173
173
  | `await_workers` | Await native host workers through the host primitive and CLI workers through synchronous dispatch, then verify team-state terminal records and Result Paths for both. |
174
174
  | `redispatch_worker` | Materialize and verify a fresh invocation, then start a fresh native worker or `okstra worker-dispatch` attempt according to the persisted runner. |
@@ -176,12 +176,26 @@ Display the question only through the selected tool. Do not print it or its opti
176
176
  | `record_lead_event` | Append progress and activity records to the manifest-provided `leadEventsPath`. Use `okstra lead-progress append --phase <phase-id>` for a checkpoint and `okstra agent-activity append --kind <kind>` for an activity record; both resolve the ledger path from the run manifest. Emit the matching `PROGRESS:` line — the command prints it as `progressLine` — and, when an activity record is required, the immediately following `ACTIVITY:` line from the same structured fields. |
177
177
  | `collect_usage` | Collect artifact/rollout-backed usage through the existing Okstra token-usage path; never read Claude session JSONL as a substitute. |
178
178
 
179
+ ## Codex execution permissions
180
+
181
+ ### Permission before bundle preparation
182
+
183
+ Before the first `okstra render-bundle` invocation, check the current host's declared filesystem restrictions and approval policy. Bundle preparation writes run artifacts and can create a task or stage worktree with `git worktree add -b`, which writes to the source repository's Git metadata. `--render-only` does not make this command read-only, and writable access to the project directory or `~/.okstra` does not imply writable access to protected `.git` metadata.
184
+
185
+ When those writes are restricted, use the host's supported execution permission on the original `render-bundle` command (`sandbox_permissions: "require_escalated"` for `exec_command` when available and permitted), preserving every `outcome.renderArgv` token. Explain that the command prepares the selected run and may create its branch and worktree. Do not first run it under restrictions already known to block these writes. Apply the same check to subsequent phase or stage bundle preparations. A wizard confirmation does not grant host privileges; follow the host's approval decision. If escalation is prohibited, report the constraint and retain the invocation instead of changing filesystem permissions or choosing an alternate launcher.
186
+
187
+ If preparation has already failed with a Git lock creation error or `Operation not permitted`, preserve the exact error and retry the same command only after the host permits the required writes. This is host-call guidance, not a permission grant or a runtime-enforced check.
188
+
179
189
  ## Codex dispatch details
180
190
 
181
191
  ### Permission at the dispatch boundary
182
192
 
183
193
  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.
184
194
 
195
+ 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.
196
+
197
+ Do not infer that an older confirmation included the new disclosure, widen the approved recipients or material, or treat model selection alone as blanket data-transfer consent. When the conversation already authorizes the same recipients and scope, use that evidence without asking again. If approval review rejects a dispatch, retain and report its reason; supply omitted existing evidence only through the host's permitted review mechanism. If the stated gap is not covered by the user's actual authorization, ask one focused question naming that gap and wait for the answer. Never bypass the rejection or repeat an unchanged request.
198
+
185
199
  Apply this guidance to both `okstra worker-dispatch` and the cmux override's `okstra team dispatch`, including initial workers, reverify, critic, report-writer, and retries. Read-only previews do not start workers. A session already authorized to execute outside the sandbox does not need another request. When the host prohibits escalation, preserve the pending invocation and report the execution constraint instead of trying an alternate launcher or weakening host controls.
186
200
 
187
201
  The worker's `--sandbox danger-full-access` flag only selects the child Codex policy; it does not remove restrictions inherited from the parent process. If startup reports `Operation not permitted`, retain the exact error and distinguish initialization failure from a worker verdict. Retry only through the permitted host mechanism after the execution conditions change, within the existing retry limit; do not repeat the same restricted invocation. This is host-call guidance, not a permission grant or a runtime-enforced check.
@@ -0,0 +1,96 @@
1
+ """Claude Code 실행기를 사용하는 Z.ai GLM 작업자."""
2
+ import os
3
+ from dataclasses import replace
4
+ from typing import Any, Mapping
5
+
6
+ from okstra_ctl.adapters.providers.claude.adapter import ClaudeExecution
7
+ from okstra_ctl.domain.provider import ModelSpec, ProviderSpec, ServedModelAttestation
8
+ from okstra_ctl.domain.role import ROLE_DEFINITIONS
9
+ from okstra_ctl.domain.worker_exec import ExecCommand, PolicySupport, WorkerExecRequest
10
+ from okstra_ctl.domain.worker_presentation import JsonEvents
11
+ from okstra_ctl.domain.worker_stream import content_block_events
12
+
13
+
14
+ ZAI_MODELS = {
15
+ name: ModelSpec(name, label, name)
16
+ for name, label in (
17
+ ("glm-5.3", "GLM-5.3"),
18
+ ("glm-5.3-flash", "GLM-5.3 Flash"),
19
+ )
20
+ }
21
+
22
+
23
+ def normalise_served_model(raw_model: str | None) -> ServedModelAttestation:
24
+ if not raw_model or not raw_model.strip():
25
+ return ServedModelAttestation.unknown()
26
+ return ServedModelAttestation(
27
+ raw_model, f"zai/{raw_model.strip().lower()}", "exact", "provider-output"
28
+ )
29
+
30
+
31
+ def observe_served_model(event: Mapping[str, Any]) -> str | None:
32
+ # init.model 은 실행기가 요청한 값이다. 응답 message.model 만 관측으로 인정한다.
33
+ message = event.get("message")
34
+ model = message.get("model") if isinstance(message, Mapping) else None
35
+ return model if isinstance(model, str) and model.strip() else None
36
+
37
+
38
+ def observe_usage(event: Mapping[str, Any]) -> Mapping[str, Any] | None:
39
+ # assistant.usage 는 턴별 값이므로 최종 누적 result.usage 와 섞어 더하지 않는다.
40
+ usage = event.get("usage") if event.get("type") == "result" else None
41
+ return usage if isinstance(usage, Mapping) and usage else None
42
+
43
+
44
+ class ZaiExecution:
45
+ def build_command(self, request: WorkerExecRequest) -> ExecCommand:
46
+ api_key = os.environ.get("ZAI_API_KEY", "").strip()
47
+ if not api_key:
48
+ raise OSError("ZAI_API_KEY is required for Z.ai GLM workers")
49
+ command = ClaudeExecution().build_command(request)
50
+ return replace(
51
+ command,
52
+ # 설정 파일의 env 가 자식 환경을 다시 덮어쓰지 않게 한다.
53
+ argv=(*command.argv, "--setting-sources", ""),
54
+ environment={
55
+ "ANTHROPIC_BASE_URL": "https://api.z.ai/api/anthropic",
56
+ "ANTHROPIC_AUTH_TOKEN": api_key,
57
+ "ANTHROPIC_API_KEY": None,
58
+ "CLAUDE_CODE_OAUTH_TOKEN": None,
59
+ "CLAUDE_CODE_USE_BEDROCK": None,
60
+ "CLAUDE_CODE_USE_VERTEX": None,
61
+ "CLAUDE_CODE_USE_FOUNDRY": None,
62
+ "CLAUDECODE": None,
63
+ "ANTHROPIC_MODEL": request.model,
64
+ "ANTHROPIC_DEFAULT_OPUS_MODEL": request.model,
65
+ "ANTHROPIC_DEFAULT_SONNET_MODEL": request.model,
66
+ "ANTHROPIC_DEFAULT_HAIKU_MODEL": request.model,
67
+ "CLAUDE_CODE_SUBAGENT_MODEL": request.model,
68
+ "CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC": "1",
69
+ },
70
+ presentation=JsonEvents(
71
+ normalise=content_block_events,
72
+ observe=observe_served_model,
73
+ observe_usage=observe_usage,
74
+ ),
75
+ )
76
+
77
+ def policy_support(self) -> PolicySupport:
78
+ return ClaudeExecution().policy_support()
79
+
80
+
81
+ def create_provider() -> ProviderSpec:
82
+ roles = frozenset(role.id for role in ROLE_DEFINITIONS if role.id != "leader")
83
+ return ProviderSpec(
84
+ provider="zai",
85
+ display_label="Z.ai GLM",
86
+ models=ZAI_MODELS,
87
+ default_models={role: "glm-5.3" for role in roles},
88
+ wrapper="okstra-zai-exec.sh",
89
+ supported_roles=roles,
90
+ execution_capabilities=frozenset({
91
+ "worker-artifact-io", "source-readonly",
92
+ "extended-artifact-authoring", "project-mutation",
93
+ }),
94
+ exec_strategy=ZaiExecution(),
95
+ served_model_normalizer=normalise_served_model,
96
+ )
@@ -0,0 +1,5 @@
1
+ {
2
+ "schemaVersion": 1,
3
+ "id": "zai",
4
+ "factory": "adapter.py:create_provider"
5
+ }
@@ -101,9 +101,12 @@ def run_corrections_check(
101
101
 
102
102
  def semantic_validator(data: dict[str, Any]) -> list[str]:
103
103
  block = data.get(block_key)
104
- return validate_implementation_option_selection(
105
- block if isinstance(block, Mapping) else {}, original_ids, analysers,
106
- )
104
+ return [
105
+ f"{block_key}: {error}"
106
+ for error in validate_implementation_option_selection(
107
+ block if isinstance(block, Mapping) else {}, original_ids, analysers,
108
+ )
109
+ ]
107
110
  elif task_type == "implementation-planning":
108
111
 
109
112
  def semantic_validator(data: dict[str, Any]) -> list[str]:
@@ -34,7 +34,11 @@ from ...assignment_resolver import AssignmentContext, resolve_dispatch_assignmen
34
34
  from ...path_hints import hydrate_active_run_context
35
35
  from ...worker_prompt_headers import worker_prompt_headers
36
36
  from ...worker_prompt_contract import complete_reverify_instruction, validate_reverify_prompt
37
- from ...worker_prompt_policy import is_verification_dispatch_kind
37
+ from ...worker_prompt_policy import (
38
+ critic_assignment_ref,
39
+ is_plan_critic_verification,
40
+ is_verification_dispatch_kind,
41
+ )
38
42
  from ...paths import okstra_home
39
43
  from ...final_report_paths import final_report_data_path
40
44
  from ...final_report_schema import load_schema_version
@@ -251,10 +255,28 @@ def _materialize_run(
251
255
  if args.assignment_ref not in assignments:
252
256
  # 유효값은 이 매니페스트 안에만 있고 리드는 그것을 추측할 수 없다.
253
257
  # 이름만 거절하면 읽는 쪽이 매니페스트를 직접 열어 키를 세게 된다.
258
+ critic_ref = critic_assignment_ref(str(manifest.get("taskType", "")))
259
+ recovery = ""
260
+ if (
261
+ args.assignment_ref == "reverify/critic-worker"
262
+ and critic_ref in assignments
263
+ and is_plan_critic_verification(
264
+ task_type=str(manifest.get("taskType", "")),
265
+ assignment_ref=critic_ref, dispatch_kind=args.dispatch_kind,
266
+ )
267
+ ):
268
+ recovery = (
269
+ f"; for the rostered plan critic, use --assignment-ref {critic_ref} "
270
+ "with --worker-id critic-worker and --audience reverification-worker. "
271
+ "Keep the plan-verify-r<N> dispatch kind and, on v2 runs, the selected "
272
+ "critic's --source-role-execution-ref. This is a reference correction; "
273
+ "the existing critic assignment does not need to be added or changed"
274
+ )
254
275
  raise AgentPromptCliError(
255
276
  f"assignment reference is missing from run manifest: "
256
277
  f"{args.assignment_ref}; this run declares "
257
278
  + (", ".join(sorted(assignments)) or "no invocation assignment")
279
+ + recovery
258
280
  )
259
281
  assignment_payload = assignments[args.assignment_ref]
260
282
  assignment = agent_model_assignment_from_payload(assignment_payload)
@@ -10,8 +10,6 @@ from __future__ import annotations
10
10
 
11
11
  import json
12
12
  import re
13
- import subprocess
14
- import sys
15
13
  from dataclasses import dataclass
16
14
  from pathlib import Path
17
15
  from typing import Any, Dict, List, Optional
@@ -149,8 +147,6 @@ def append_consumer(plan_run_root: Path, *, impl_task_key: str, stage: int,
149
147
  **fields,
150
148
  }
151
149
  _append_row(plan_run_root, record)
152
- if status == "done":
153
- _tag_stage_exit(plan_run_root, stage, fields.get("head_commit"))
154
150
  # 종결 status 는 점유 해제 이벤트이기도 하다 — 중복 append(no-op)에서도 풀어야
155
151
  # release 없이 done 만 기록된 과거 run 의 잔존 점유가 다음 호출에서 치유된다.
156
152
  if status == "done":
@@ -159,57 +155,6 @@ def append_consumer(plan_run_root: Path, *, impl_task_key: str, stage: int,
159
155
  _release_stage_occupancy_keeping_branch(impl_task_key, stage)
160
156
 
161
157
 
162
- STAGE_EXIT_TAG = "stage-{stage}-exit"
163
-
164
-
165
- def _repo_root(start: Path) -> Optional[Path]:
166
- for candidate in [start, *start.parents]:
167
- if (candidate / ".git").exists():
168
- return candidate
169
- return None
170
-
171
-
172
- def _tag_stage_exit(plan_run_root: Path, stage: int, head_commit: Any) -> None:
173
- """Move `stage-<N>-exit` to the commit this ledger row records.
174
-
175
- The tag used to be a plan step, so it was written while the stage was still
176
- running — before the carry evidence the ledger reads. A stage then had three
177
- exit points that could disagree: the tag, the recorded `head_commit`, and
178
- the branch tip. okstra writes the tag at the moment it settles the stage, so
179
- the tag and the ledger cannot drift apart.
180
-
181
- Best effort by design: the ledger is the record and a tag is a convenience
182
- for the reader. A tree that is not a git repository, or a commit git cannot
183
- resolve, leaves the row written and says so on stderr.
184
- """
185
- if not isinstance(head_commit, str) or not head_commit.strip():
186
- return
187
- root = _repo_root(Path(plan_run_root).resolve())
188
- if root is None:
189
- return
190
- commit = head_commit.strip()
191
- tag = STAGE_EXIT_TAG.format(stage=stage)
192
- try:
193
- exists = subprocess.run(
194
- ["git", "-C", str(root), "cat-file", "-e", f"{commit}^{{commit}}"],
195
- capture_output=True,
196
- )
197
- if exists.returncode != 0:
198
- print(
199
- f"okstra: stage {stage} done recorded, but {commit[:12]} is not "
200
- f"a commit in {root} — `{tag}` not moved",
201
- file=sys.stderr,
202
- )
203
- return
204
- subprocess.run(
205
- ["git", "-C", str(root), "tag", "-f", tag, commit],
206
- capture_output=True,
207
- check=True,
208
- )
209
- except (OSError, subprocess.CalledProcessError) as exc:
210
- print(f"okstra: could not move `{tag}` to {commit[:12]}: {exc}", file=sys.stderr)
211
-
212
-
213
158
  def _equivalent_row_exists(plan_run_root: Path, impl_task_key: str, stage: int,
214
159
  status: str, force_reappend: bool,
215
160
  head_commit: Any) -> bool:
@@ -7,9 +7,9 @@ regardless of provider — see ``ExecutionPolicy``.
7
7
  """
8
8
  from __future__ import annotations
9
9
 
10
- from dataclasses import dataclass
10
+ from dataclasses import dataclass, field
11
11
  from pathlib import Path
12
- from typing import Literal, Protocol, runtime_checkable
12
+ from typing import Literal, Mapping, Protocol, runtime_checkable
13
13
 
14
14
  from .worker_presentation import Presentation
15
15
  from .write_policy import WriteEnforcement, WritePolicy
@@ -84,6 +84,9 @@ class ExecCommand:
84
84
  stdin_text: str | None
85
85
  cwd: Path
86
86
  presentation: Presentation
87
+ # 인증 값은 프로세스 경계에서만 전달하고 명령 표현·실행 기록에서 제외한다.
88
+ # None 은 부모에게서 상속된 설정을 지운다.
89
+ environment: Mapping[str, str | None] = field(default_factory=dict, repr=False)
87
90
 
88
91
 
89
92
  @dataclass(frozen=True)
@@ -131,8 +131,14 @@ def validate_write_policy_payload(payload: Mapping[str, Any]) -> None:
131
131
  _relative_paths(artifact.get("allowedPaths", ()))
132
132
  if source.get("mode") not in {"source-readonly", "project-mutation"}:
133
133
  raise WritePolicyError("sourcePolicy mode is invalid")
134
- if set(source) != {"mode", "allowedRoot", "plannedPaths", "protectedPaths"}:
134
+ if set(source) - {"plannedPathsDeclared"} != {
135
+ "mode", "allowedRoot", "plannedPaths", "protectedPaths"
136
+ }:
135
137
  raise WritePolicyError("sourcePolicy is incomplete")
138
+ if "plannedPathsDeclared" in source and not isinstance(
139
+ source["plannedPathsDeclared"], bool
140
+ ):
141
+ raise WritePolicyError("sourcePolicy.plannedPathsDeclared must be a boolean")
136
142
  _absolute_text(source.get("allowedRoot"), "sourcePolicy.allowedRoot")
137
143
  _relative_paths(source.get("plannedPaths", ()))
138
144
  _relative_paths(source.get("protectedPaths", ()))
@@ -793,28 +793,17 @@ def _stable_git_projection(snapshot: MutationSnapshot) -> dict[str, Any]:
793
793
 
794
794
 
795
795
  def _path_ledger_is_unenforceable(policy: WritePolicy) -> bool:
796
- """ 정책의 경로 장부를 근거로 변경을 거절할 있는가.
797
-
798
- 승인된 계획서에 `plannedPaths` 컬럼이 있으면 실행기는 목록에 묶이고,
799
- 목록은 반드시 비어 있지 않다(`write_policy._planned_paths_from_report`
800
- 선언된 경우에만 항목을 싣는다). 컬럼이 없는 옛 계획서에서는 실을 값이
801
- 없어 장부가 빈 채로 온다 — 종전에는 산문에서 유도한 문장 조각(`28 rows)`,
802
- `captured in Stage 1)`)을 실었고, 그래서 계획이 지시한 파일 전부가 미허가
803
- 변경으로 읽혔다.
804
-
805
- `project-mutation` 정책에서만 빈 장부가 "물을 수 없음" 을 뜻한다.
806
- `source-readonly` 워커는 장부가 원래 비어 있고 그것이 "아무것도 바꾸지
807
- 말라" 는 뜻이므로, 그쪽 집행은 건드리지 않는다.
808
-
809
- 이 판정은 감사의 두 절반이 같은 함수를 읽는다. 종전에는 git 쪽만
810
- `plannedPathsDeclared` 를 봤는데 그 키는 `build_write_policy` 가 만드는
811
- sourcePolicy 에 아예 실리지 않아(4개 키 고정, `validate_write_policy_payload`
812
- 가 그 집합을 강제) 어느 쪽에서도 참이 된 적이 없다.
796
+ """ 계획의 미선언 목록만 면제하고, 산출물 전용 계획의 빈 소스 목록은 집행한다.
797
+
798
+ 선언 여부가 저장되기 정책은 기존의 목록 해석을 유지한다.
799
+ 정책은 경로 분류 후에도 선언 여부를 보존하므로 산출물만 남은 계획이
800
+ 임의의 소스 변경이나 커밋을 허용하지 않는다.
813
801
  """
814
802
  source = policy.source_policy
815
803
  return (
816
804
  source.get("mode") == "project-mutation"
817
805
  and not source.get("plannedPaths")
806
+ and not source.get("plannedPathsDeclared", False)
818
807
  )
819
808
 
820
809