okstra 0.201.0 → 0.201.2

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 CHANGED
@@ -799,7 +799,16 @@ Each GLM child process uses `https://api.z.ai/api/anthropic` and its own model/a
799
799
 
800
800
  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.
801
801
 
802
- 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.
802
+ 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. Costs use Z.ai's public API token prices regardless of whether the account uses a subscription or metered API billing. Missing usage or an unknown model price stays unavailable.
803
+
804
+ The [official API prices](https://docs.z.ai/guides/overview/pricing), checked on 2026-09-14, are USD per million tokens:
805
+
806
+ | Model | Input | Cached input | Output |
807
+ |---|---:|---:|---:|
808
+ | `glm-5.3` | $1.40 | $0.26 | $4.40 |
809
+ | `glm-5.3-flash` | $0.15 | $0.03 | $0.50 |
810
+
811
+ Cache-creation tokens use the input rate; cache-read tokens use the cached-input rate. Z.ai currently lists cached-input storage as free for a limited time. The reported cost is the API-equivalent token cost, not the subscription invoice. Regression coverage is in `tests/adapters/accounting/test_cli_provider_transcripts.py` and `tests/adapters/providers/test_provider_registry.py`.
803
812
 
804
813
  ## `okstra` Node CLI — introspection subcommands
805
814
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "okstra",
3
- "version": "0.201.0",
3
+ "version": "0.201.2",
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.201.0",
3
- "builtAt": "2026-09-13T22:07:32.212Z",
2
+ "package": "0.201.2",
3
+ "builtAt": "2026-09-13T23:51:12.747Z",
4
4
  "repoRoot": "/home/runner/work/okstra/okstra"
5
5
  }
@@ -173,7 +173,7 @@ roles:
173
173
  **Pick the cases by distinct outcome, not by line coverage.** When the stage writes or reconciles state, its meaningfully different outcomes are usually more than three — normal success, target already in the desired state (resume), existing data reused rather than created, a conflicting concurrent state, target absent, and mid-way failure with rollback. Enumerate the ones this stage actually implements and route them across the three lines (the `boundary` line is where resume / already-done / reuse belongs; `failure` carries conflict, absence, and rollback), naming each in the cell rather than collapsing them into "edge input". An implemented outcome with no declared case is a coverage gap the executor will not backfill.
174
174
  - **Per-stage subsections** (`## 5.5.<i> Stage <i>: <title>` for each `i`), each containing the four required subsections:
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
- - `### 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`).
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 paths this step may write as an array, one repository-relative path per entry, with no globs, exclusions, counts or commentary. Read-only checks use `plannedPaths: []`; their working directory belongs in the command, not the write ledger. Never declare the repository or worktree root. Planning assembly and correction preflight enforce this through `write_policy.planned_path_declaration_errors`.** `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
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.
@@ -15,10 +15,12 @@ from okstra_ctl.domain.worker_presentation import JsonEvents
15
15
  from okstra_ctl.domain.worker_stream import content_block_events
16
16
 
17
17
  ZAI_MODELS = {
18
- name: ModelSpec(name, label, name)
19
- for name, label in (
20
- ("glm-5.3", "GLM-5.3"),
21
- ("glm-5.3-flash", "GLM-5.3 Flash"),
18
+ # 입력·캐시 입력·출력 USD/100만 토큰, 2026-09-14 공식 단가 확인.
19
+ # https://docs.z.ai/guides/overview/pricing
20
+ name: ModelSpec(name, label, name, pricing=pricing)
21
+ for name, label, pricing in (
22
+ ("glm-5.3", "GLM-5.3", (1.40, 0.26, 4.40)),
23
+ ("glm-5.3-flash", "GLM-5.3 Flash", (0.15, 0.03, 0.50)),
22
24
  )
23
25
  }
24
26
 
@@ -41,6 +41,7 @@ from .report_projections import (
41
41
  )
42
42
  from .scope_provenance import parse_source
43
43
  from .verification_target import read_verification_target
44
+ from .write_policy import planned_path_declaration_errors
44
45
 
45
46
 
46
47
  @dataclass(frozen=True)
@@ -594,6 +595,7 @@ def validate_plan_draft(
594
595
  *task_narrative_errors(
595
596
  draft, load_schema_version("3.0"), str(manifest.get("taskType", "")),
596
597
  ),
598
+ *planned_path_declaration_errors(draft),
597
599
  *[
598
600
  f"implementationPlanning: {error}"
599
601
  for error in selected_direction_plan_errors(draft, project_root, manifest)
@@ -880,6 +882,12 @@ def assemble_report(
880
882
  raise ReportAssemblyError(tuple(input_issues))
881
883
  data = _compose(project_root, manifest_path, manifest, inputs, schema)
882
884
  errors = validate(data, schema)
885
+ path_errors = planned_path_declaration_errors(data)
886
+ if path_errors:
887
+ raise ReportAssemblyError(tuple(
888
+ AssemblyIssue("report-writer", str(inputs["narrative"].path), error.split(":", 1)[0], error)
889
+ for error in path_errors
890
+ ))
883
891
  direction_errors = selected_direction_plan_errors(
884
892
  data, project_root, manifest
885
893
  )
@@ -447,7 +447,7 @@ def _relative_to_root(path: Path, root: Path, label: str) -> str:
447
447
  except ValueError as exc:
448
448
  raise WritePolicyError(f"{label} path is outside project root") from exc
449
449
  if not relative or relative == ".":
450
- raise WritePolicyError(f"{label} path must name a file")
450
+ raise WritePolicyError(f"{label} path must name a file or scoped subdirectory, not root: {path}")
451
451
  current = root
452
452
  for part in PurePosixPath(relative).parts:
453
453
  current /= part
@@ -466,6 +466,9 @@ def _planned_paths_from_report(
466
466
  # 그 파일은 없으므로 implementer 디스패치가 전부 막혔다. 정본 헬퍼는 이미
467
467
  # 레코드인 경로를 그대로 돌려주고 `.md` 만 짝으로 바꾼다.
468
468
  payload = _read_json(final_report_data_path(report_path), "approved plan data")
469
+ errors = planned_path_declaration_errors(payload)
470
+ if errors:
471
+ raise WritePolicyError(f"{report_path}: {'; '.join(errors)}")
469
472
  planning = payload.get("implementationPlanning")
470
473
  stages = planning.get("stages") if isinstance(planning, Mapping) else None
471
474
  selected = next(
@@ -489,7 +492,10 @@ def _planned_paths_from_report(
489
492
  for path in (step.get("plannedPaths") or [])
490
493
  if isinstance(path, str) and path.strip()
491
494
  }
492
- if paths:
495
+ if paths or (rows and all(
496
+ isinstance(step, Mapping) and isinstance(step.get("plannedPaths"), list)
497
+ for step in rows
498
+ )):
493
499
  # 경로 분류는 산출물 권한과 배정 작업 디렉터리를 함께 아는 정책 생성기가 맡는다.
494
500
  return tuple(sorted(paths)), True
495
501
  # A plan approved before `plannedPaths` existed carries its paths only in
@@ -516,6 +522,32 @@ def _planned_paths_from_report(
516
522
  return (), False
517
523
 
518
524
 
525
+ def planned_path_declaration_errors(data: Mapping[str, Any]) -> list[str]:
526
+ """검사 단계의 빈 선언은 보존하고 저장소 전체를 쓰기 대상으로 승인하지 않는다."""
527
+ planning = data.get("implementationPlanning")
528
+ stages = planning.get("stages") if isinstance(planning, Mapping) else None
529
+ errors: list[str] = []
530
+ for si, stage in enumerate(stages if isinstance(stages, list) else []):
531
+ steps = stage.get("stepwiseExecution") if isinstance(stage, Mapping) else None
532
+ for ti, step in enumerate(steps if isinstance(steps, list) else []):
533
+ paths = step.get("plannedPaths") if isinstance(step, Mapping) else None
534
+ for pi, value in enumerate(paths if isinstance(paths, list) else []):
535
+ if not isinstance(value, str):
536
+ continue
537
+ path = Path(value.strip().strip("`"))
538
+ # 절대 경로는 디스패치와 같은 실제 작업트리를 가리킨다. 새 파일은 없어도 된다.
539
+ root = path == Path(".") or path == Path(path.anchor or ".")
540
+ if path.is_absolute():
541
+ root = root or (path / ".git").exists()
542
+ if root:
543
+ field = f"implementationPlanning.stages[{si}].stepwiseExecution[{ti}].plannedPaths[{pi}]"
544
+ errors.append(
545
+ f"{field}: planned path must name a file or a scoped subdirectory, "
546
+ f"not a repository/worktree root: {value!r}; use [] for a read-only step"
547
+ )
548
+ return errors
549
+
550
+
519
551
  def _rooted(project_root: Path, value: str) -> Path:
520
552
  path = Path(value)
521
553
  return path if path.is_absolute() else project_root / path
@@ -702,10 +702,16 @@ def _cli_usage_block(provider: str, totals: dict, session_paths: list[Path]) ->
702
702
  if totals.get("model"):
703
703
  block["model"] = totals["model"]
704
704
  block["cliModel"] = totals["model"]
705
- if provider in _THREE_RATE_PROVIDERS:
705
+ if provider in _THREE_RATE_PROVIDERS or provider == "zai":
706
+ input_tokens = totals.get("inputTokens", 0) or 0
707
+ if provider == "zai":
708
+ # GLM 상태는 캐시 토큰을 입력과 별도로 보고한다. 공통 단가는
709
+ # 캐시 포함 입력을 받으며, 캐시 생성은 일반 입력 단가로 계산한다.
710
+ cached_input = totals.get("cacheReadTokens", 0) or 0
711
+ input_tokens += (totals.get("cacheCreationTokens", 0) or 0) + cached_input
706
712
  rates = (
707
713
  totals.get("model"),
708
- totals.get("inputTokens", 0) or 0,
714
+ input_tokens,
709
715
  cached_input,
710
716
  totals.get("outputTokens", 0) or 0,
711
717
  )
@@ -243,6 +243,7 @@ _THREE_RATE_TABLES = {
243
243
  "codex": CODEX_PRICING,
244
244
  "grok": GROK_PRICING,
245
245
  "kimi": KIMI_PRICING,
246
+ "zai": catalog_pricing("zai"),
246
247
  }
247
248
 
248
249
 
@@ -8943,9 +8943,9 @@
8943
8943
  "minLength": 1
8944
8944
  },
8945
8945
  "plannedPaths": {
8946
- "description": "Paths this step may write, one per entry. Source paths are relative to the assigned worktree; absolute paths under that worktree or the project checkout identify the same repository-relative source. `.okstra` artifact paths are anchored at the original project root and may be absolute. The write-policy builder checks artifact paths against the worker's existing artifact permissions and keeps them out of the source ledger. Keep absolute artifact paths in executable commands. `files` is human-readable prose, not a path ledger.",
8946
+ "description": "Paths this step may write, one per entry. Use an empty array for read-only steps. Never name the repository or worktree root; scoped subdirectories remain allowed. Source paths are relative to the assigned worktree; absolute paths under that worktree or the project checkout identify the same repository-relative source. `.okstra` artifact paths are anchored at the original project root and may be absolute. The write-policy builder checks artifact paths against the worker's existing artifact permissions and keeps them out of the source ledger. Keep absolute artifact paths in executable commands. `files` is human-readable prose, not a path ledger.",
8947
8947
  "type": "array",
8948
- "minItems": 1,
8948
+ "minItems": 0,
8949
8949
  "items": {
8950
8950
  "type": "string",
8951
8951
  "minLength": 1
@@ -9088,9 +9088,9 @@
9088
9088
  "minLength": 1
9089
9089
  },
9090
9090
  "plannedPaths": {
9091
- "description": "Paths this step may write, one per entry. Source paths are relative to the assigned worktree; absolute paths under that worktree or the project checkout identify the same repository-relative source. `.okstra` artifact paths are anchored at the original project root and may be absolute. The write-policy builder checks artifact paths against the worker's existing artifact permissions and keeps them out of the source ledger. Keep absolute artifact paths in executable commands. `files` is human-readable prose, not a path ledger.",
9091
+ "description": "Paths this step may write, one per entry. Use an empty array for read-only steps. Never name the repository or worktree root; scoped subdirectories remain allowed. Source paths are relative to the assigned worktree; absolute paths under that worktree or the project checkout identify the same repository-relative source. `.okstra` artifact paths are anchored at the original project root and may be absolute. The write-policy builder checks artifact paths against the worker's existing artifact permissions and keeps them out of the source ledger. Keep absolute artifact paths in executable commands. `files` is human-readable prose, not a path ledger.",
9092
9092
  "type": "array",
9093
- "minItems": 1,
9093
+ "minItems": 0,
9094
9094
  "items": {
9095
9095
  "type": "string",
9096
9096
  "minLength": 1
@@ -21,6 +21,7 @@ for _ssot_dir in (_VALIDATORS_DIR.parent / "scripts", _VALIDATORS_DIR.parent / "
21
21
  sys.path.insert(0, str(_ssot_dir))
22
22
 
23
23
  from okstra_ctl.md_table import split_pipe_row # noqa: E402
24
+ from okstra_ctl.write_policy import planned_path_declaration_errors # noqa: E402
24
25
  from okstra_ctl.tdd_bypass import ( # noqa: E402
25
26
  REASON_TOKEN as TDD_USER_BYPASS_TOKEN,
26
27
  bypass_file,
@@ -688,6 +689,10 @@ def collect_data_validation_errors(
688
689
 
689
690
  raw_stage_map = planning.get("stageMap") or []
690
691
  stage_map, errors = _data_stage_metas(raw_stage_map)
692
+ errors.extend(
693
+ ValidationError("S10", 0, error)
694
+ for error in planned_path_declaration_errors({"implementationPlanning": planning})
695
+ )
691
696
  stages = [s for s in (planning.get("stages") or []) if isinstance(s, dict)]
692
697
  if not stage_map and not stages:
693
698
  return errors