okstra 0.199.5 → 0.200.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md 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.5",
3
+ "version": "0.200.1",
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.5",
3
- "builtAt": "2026-09-12T23:19:40.021Z",
2
+ "package": "0.200.1",
3
+ "builtAt": "2026-09-13T00:15:52.944Z",
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 "$@"
@@ -357,7 +357,7 @@ After each worker terminates, BEFORE classifying its terminal status, verify the
357
357
 
358
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:
359
359
 
360
- - `--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`
361
361
  - `--agent-role` — `lead` | `worker` | `report-writer`
362
362
  - `--error-type` — `cli-failure` | `contract-violation` | `tool-failure`
363
363
 
@@ -44,15 +44,15 @@ packet. The packet contains the task brief, analysis packet, report template,
44
44
  report schema, convergence state, every successful settled worker result recorded
45
45
  by the run manifest, accumulated `user-responses/` sidecars, and the current
46
46
  session/token/cost accounting snapshot. Each file-backed source carries its owner,
47
- project-relative path, SHA-256 digest, and value. The report writer receives the
47
+ project-relative path and value. The report writer receives the
48
48
  packet's Markdown reading projection as the only task input instead of an
49
49
  independently assembled list of raw paths.
50
50
 
51
51
  The packet's authoring contract names the result path, narrative format, writing
52
52
  instructions, runtime-owned content, and validation rules. Missing configured
53
53
  paths and missing files are collected across the full source set before dispatch
54
- and reported together with their owners. Report assembly compares every frozen
55
- source digest again and reports every changed or missing source in one result.
54
+ and reported together with their owners. Report assembly checks source availability
55
+ and readability without rejecting changed content or comparing hashes.
56
56
  This is enforced by
57
57
  `initial_prompt_materialization._materialize_report_writer_packet()`,
58
58
  `report_synthesis_packet.build_report_synthesis_packet()`, and
@@ -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
+ }
@@ -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)
@@ -21,6 +21,7 @@ _AGENT_LABELS = {
21
21
  "antigravity": "Antigravity",
22
22
  "grok": "Grok",
23
23
  "kimi": "Kimi",
24
+ "zai": "Z.ai GLM",
24
25
  }
25
26
  _STATUS = {
26
27
  "done": "completed",
@@ -2,7 +2,6 @@
2
2
  from __future__ import annotations
3
3
 
4
4
  import copy
5
- import hashlib
6
5
  import json
7
6
  import os
8
7
  import re
@@ -54,7 +53,6 @@ class ReportSynthesisSource:
54
53
  label: str
55
54
  owner: str
56
55
  path: str
57
- digest: str
58
56
  content: str
59
57
 
60
58
  def to_dict(self) -> dict[str, str]:
@@ -62,7 +60,6 @@ class ReportSynthesisSource:
62
60
  "label": self.label,
63
61
  "owner": self.owner,
64
62
  "path": self.path,
65
- "digest": self.digest,
66
63
  "content": self.content,
67
64
  }
68
65
 
@@ -282,7 +279,6 @@ class ReportSynthesisPacket:
282
279
  "user response carry-in",
283
280
  ],
284
281
  "validationRules": [
285
- "source-digest-match",
286
282
  "writer-owned-fields-only",
287
283
  "all-defects-collected",
288
284
  *self._carry_validation_rules(),
@@ -307,7 +303,7 @@ class ReportSynthesisPacket:
307
303
  "- Runtime-owned values: session identifiers, token usage, estimated cost, "
308
304
  "user response carry-in",
309
305
  "- Accounting details stay in the JSON snapshot; assembly supplies them.",
310
- "- Validation: source digest match, writer-owned fields, all defects collected",
306
+ "- Validation: writer-owned fields, all defects collected",
311
307
  ]
312
308
  # 작성자가 실제로 읽는 것은 마크다운이다. JSON 정본에만 실으면 지시가
313
309
  # 도달하지 않는다.
@@ -351,8 +347,7 @@ def _source_markdown(
351
347
  if content != source.content.rstrip("\n"):
352
348
  view = [
353
349
  "- View: repeated extracts refer to frozen sources below; "
354
- "the original line index is omitted. The digest identifies "
355
- "the frozen original.",
350
+ "the original line index is omitted.",
356
351
  ]
357
352
  if source.label == "Final report schema":
358
353
  try:
@@ -361,7 +356,7 @@ def _source_markdown(
361
356
  schema = {}
362
357
  view = ["- View: schema is not JSON; the original text is retained."]
363
358
  if isinstance(schema, dict) and schema.get("properties"):
364
- # 원본과 해시는 JSON 보관본에 남긴다. 저작용 읽기에는 검증기와
359
+ # 원본은 JSON 보관본에 남긴다. 저작용 읽기에는 검증기와
365
360
  # 같은 소유권 투영을 쓰고, 도달하지 않는 정의만 기존 발췌기로 뺀다.
366
361
  owned = writer_owned_schema(schema)
367
362
  # 부분 서사 검증에서 빠지는 루트 조건도 작성 지침에는 남긴다.
@@ -374,8 +369,8 @@ def _source_markdown(
374
369
  excerpt = build_schema_excerpt(owned, task_type)
375
370
  content = json.dumps(excerpt, ensure_ascii=False, indent=2)
376
371
  view = [
377
- "- View: writer-owned schema excerpt; the digest identifies the "
378
- "frozen original. Task-specific requirements are listed above.",
372
+ "- View: writer-owned schema excerpt. "
373
+ "Task-specific requirements are listed above.",
379
374
  ]
380
375
  longest = max((len(run) for run in re.findall(r"`+", content)), default=0)
381
376
  fence = "`" * max(3, longest + 1)
@@ -385,7 +380,6 @@ def _source_markdown(
385
380
  "",
386
381
  f"- Owner: `{source.owner}`",
387
382
  f"- Path: `{source.path}`",
388
- f"- Digest: `{source.digest}`",
389
383
  *view,
390
384
  "",
391
385
  f"{fence}text",
@@ -700,13 +694,11 @@ def _read_sources(
700
694
  except (OSError, UnicodeError) as exc:
701
695
  issues.append(ReportSynthesisPacketIssue(owner, label, path, str(exc)))
702
696
  continue
703
- digest = "sha256:" + hashlib.sha256(content.encode("utf-8")).hexdigest()
704
697
  sources.append(
705
698
  ReportSynthesisSource(
706
699
  label=label,
707
700
  owner=owner,
708
701
  path=_relative(project_root, path),
709
- digest=digest,
710
702
  content=content,
711
703
  )
712
704
  )
@@ -929,18 +921,8 @@ def verify_report_synthesis_packet_sources(
929
921
  )
930
922
  continue
931
923
  try:
932
- content = path.read_text(encoding="utf-8")
924
+ path.read_text(encoding="utf-8")
933
925
  except (OSError, UnicodeError) as exc:
934
926
  issues.append(ReportSynthesisPacketIssue(owner, label, path, str(exc)))
935
927
  continue
936
- digest = "sha256:" + hashlib.sha256(content.encode("utf-8")).hexdigest()
937
- if digest != source.get("digest"):
938
- issues.append(
939
- ReportSynthesisPacketIssue(
940
- owner,
941
- label,
942
- path,
943
- "source value changed after packet materialization",
944
- )
945
- )
946
928
  return tuple(issues)
@@ -202,7 +202,7 @@ def _launch(
202
202
  stdout=subprocess.PIPE,
203
203
  stderr=_stderr_target(strategy),
204
204
  start_new_session=True,
205
- env=_child_env(),
205
+ env=_child_env(command.environment),
206
206
  )
207
207
  on_spawn(process)
208
208
  exit_code, timed_out, idle_seconds = _pump(
@@ -604,7 +604,7 @@ def _terminate(process: subprocess.Popen[bytes]) -> None:
604
604
  process.wait()
605
605
 
606
606
 
607
- def _child_env() -> dict[str, str]:
607
+ def _child_env(overrides: Mapping[str, str | None]) -> dict[str, str]:
608
608
  """The worker's environment with git's fsmonitor disabled.
609
609
 
610
610
  The main worktree's fsmonitor daemon leaks its IPC socket into task
@@ -614,6 +614,11 @@ def _child_env() -> dict[str, str]:
614
614
  GIT_CONFIG_* the caller already set.
615
615
  """
616
616
  env = dict(os.environ)
617
+ for key, value in overrides.items():
618
+ if value is None:
619
+ env.pop(key, None)
620
+ else:
621
+ env[key] = value
617
622
  index = int(env.get("GIT_CONFIG_COUNT", "0") or "0")
618
623
  env[f"GIT_CONFIG_KEY_{index}"] = "core.fsmonitor"
619
624
  env[f"GIT_CONFIG_VALUE_{index}"] = "false"
@@ -9,6 +9,7 @@ from pathlib import Path
9
9
 
10
10
  from .cursor import MAX_NEEDLES, fresh_cache, load_cache, save_cache
11
11
  from .paths import claude_project_dir, ts_in_window
12
+ from okstra_ctl.wrapper_status import read_wrapper_status
12
13
 
13
14
  # lead 의 단계 체크포인트 라인(prompts/lead/okstra-lead-contract.md "Progress reporting") — phase id 가
14
15
  # `phase-<digit>` 로 시작하는 라인만 마커로 인정해 일반 대화의 오탐을 줄인다.
@@ -16,6 +17,34 @@ _PROGRESS_RE = re.compile(r"^PROGRESS:\s+(phase-\d\S*(?:[ \t].*)?)$", re.MULTILI
16
17
  _PROGRESS_MARKER_MAX_CHARS = 160
17
18
 
18
19
 
20
+ def claude_code_status_total(status_path: Path) -> dict:
21
+ """Claude Code의 최종 누적 사용량을 제공자 가격과 분리해서 읽는다."""
22
+ status = read_wrapper_status(status_path)
23
+ usage = status.raw.get("usage") if status is not None else None
24
+ if not isinstance(usage, dict) or not usage:
25
+ return {"available": False, "totalTokens": 0}
26
+ attestation = status.raw.get("servedModelAttestation")
27
+ model = attestation.get("observedModel") if isinstance(attestation, dict) else None
28
+ input_tokens = usage.get("input_tokens")
29
+ output_tokens = usage.get("output_tokens")
30
+ cache_creation = usage.get("cache_creation_input_tokens", 0)
31
+ cache_read = usage.get("cache_read_input_tokens", 0)
32
+ if any(
33
+ not isinstance(value, int) or isinstance(value, bool) or value < 0
34
+ for value in (input_tokens, output_tokens, cache_creation, cache_read)
35
+ ):
36
+ return {"available": False, "totalTokens": 0}
37
+ return {
38
+ "available": True,
39
+ "totalTokens": input_tokens + output_tokens + cache_creation,
40
+ "inputTokens": input_tokens,
41
+ "outputTokens": output_tokens,
42
+ "cacheCreationTokens": cache_creation,
43
+ "cacheReadTokens": cache_read,
44
+ "model": model,
45
+ }
46
+
47
+
19
48
  def _event_from_record(rec: dict) -> dict | None:
20
49
  """jsonl 레코드 1개 → 압축 이벤트. 집계에 기여하지 않으면 None.
21
50
 
@@ -11,6 +11,7 @@ from okstra_project.dirs import OKSTRA_RELATIVE
11
11
 
12
12
  from .blocks import na_block, usage_block
13
13
  from .claude import (
14
+ claude_code_status_total,
14
15
  claude_session_totals,
15
16
  find_claude_agent_sessions,
16
17
  find_claude_team_sessions,
@@ -662,6 +663,8 @@ def _cli_session_totals(
662
663
  )
663
664
  elif provider == "grok":
664
665
  total = grok_session_total(session_path)
666
+ elif provider == "zai":
667
+ total = claude_code_status_total(session_path)
665
668
  else:
666
669
  continue
667
670
  if total.get("available"):
@@ -705,12 +708,15 @@ def _cli_usage_block(provider: str, totals: dict, session_paths: list[Path]) ->
705
708
  billable = provider_billable_equivalent(provider, *rates)
706
709
  if billable is not None:
707
710
  block["billableEquivalentTokens"] = billable
708
- else:
711
+ elif provider == "antigravity":
709
712
  cost = antigravity_cost_usd(
710
713
  totals.get("model"),
711
714
  totals.get("inputTokens", 0) or 0,
712
715
  totals.get("outputTokens", 0) or 0,
713
716
  )
717
+ else:
718
+ cost = None
719
+ block["cliNote"] = "Token usage recorded; provider billing cost is unavailable."
714
720
  if cost is not None:
715
721
  block["cliEstimatedCostUsd"] = cost
716
722
  return block
@@ -919,6 +925,10 @@ def _worker_cli_usage_block(
919
925
  if path not in session_paths:
920
926
  session_paths.append(path)
921
927
  status_path = status_paths[-1] if status_paths else None
928
+ if provider == "zai":
929
+ session_paths = list(dict.fromkeys(
930
+ path for path in status_paths if path is not None and path.is_file()
931
+ ))
922
932
  if provider == "antigravity":
923
933
  for raw, source_status in zip(prompt_paths, status_paths):
924
934
  for source in _antigravity_usage_sources(
@@ -7025,7 +7025,8 @@
7025
7025
  "Codex",
7026
7026
  "Antigravity",
7027
7027
  "Grok",
7028
- "Kimi"
7028
+ "Kimi",
7029
+ "Z.ai GLM"
7029
7030
  ]
7030
7031
  },
7031
7032
  "role": {
@@ -7122,7 +7122,8 @@
7122
7122
  "Codex",
7123
7123
  "Antigravity",
7124
7124
  "Grok",
7125
- "Kimi"
7125
+ "Kimi",
7126
+ "Z.ai GLM"
7126
7127
  ]
7127
7128
  },
7128
7129
  "role": {
@@ -111,7 +111,7 @@
111
111
  "minItems": 1,
112
112
  "items": {
113
113
  "type": "object",
114
- "required": ["label", "owner", "path", "digest", "content"],
114
+ "required": ["label", "owner", "path", "content"],
115
115
  "additionalProperties": false,
116
116
  "properties": {
117
117
  "label": {