okstra 0.119.0 → 0.121.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -0
- package/docs/architecture.md +5 -4
- package/docs/cli.md +5 -4
- package/docs/for-ai/README.md +3 -1
- package/docs/for-ai/skills/okstra-usage.md +28 -0
- package/docs/project-structure-overview.md +7 -3
- package/package.json +1 -1
- package/runtime/BUILD.json +2 -2
- package/runtime/bin/lib/okstra/globals.sh +0 -5
- package/runtime/bin/lib/okstra/usage.sh +2 -2
- package/runtime/prompts/lead/convergence.md +1 -1
- package/runtime/prompts/lead/okstra-lead-contract.md +1 -1
- package/runtime/prompts/profiles/_common-contract.md +1 -1
- package/runtime/prompts/profiles/_implementation-verifier.md +1 -1
- package/runtime/python/okstra_ctl/models.py +75 -47
- package/runtime/python/okstra_ctl/rollup.py +2 -2
- package/runtime/python/okstra_ctl/run.py +5 -5
- package/runtime/python/okstra_ctl/time_report.py +10 -6
- package/runtime/python/okstra_ctl/usage_report.py +269 -0
- package/runtime/python/okstra_ctl/wizard.py +4 -3
- package/runtime/python/okstra_token_usage/pricing.py +19 -15
- package/runtime/skills/okstra-usage/SKILL.md +64 -0
- package/src/cli-registry.mjs +7 -0
- package/src/commands/inspect/usage-report.mjs +25 -0
- package/src/lib/skill-catalog.mjs +1 -0
package/README.md
CHANGED
|
@@ -169,6 +169,7 @@ Use these slash commands inside a Claude Code session:
|
|
|
169
169
|
| `/okstra-memory` | Store, search, and archive global conversation memory in `~/.okstra/memory-book` |
|
|
170
170
|
| `/okstra-inspect` | Unified read side. Subcommands: `status` (phase/state and workStatus updates), `history` (past tasks, reruns, resumes), `report` (find/read final reports), `time` (elapsed-time breakdown), `logs` (wrapper log sidecar inventory and cleanup suggestions), `cost` (task bundle context/read cost), `errors` (aggregate run error logs into a report), `error-zip` (collect cross-project error logs into an anonymized zip and summarize clusters), and `recap` (run-to-run before/after summary plus free-form Q&A over a task's `.okstra` artifacts) |
|
|
171
171
|
| `/okstra-rollup` | Aggregate every task run in a task group or project, including per-task run counts, duration, errors, group totals, and a cross-task report digest |
|
|
172
|
+
| `/okstra-usage` | Show the current project's recent run coverage, raw and billable-equivalent tokens, known USD cost, CPU time, and wall-clock time grouped by task type (default: last 30 days) |
|
|
172
173
|
| `/okstra-schedule` | Generate a work schedule for an entire task group |
|
|
173
174
|
| `/okstra-container-build` | Deploy a verified task's code as a local Docker Compose group and monitor per-container logs (subcommands: `up` / `status` / `logs` / `stop-watcher` / `down`) |
|
|
174
175
|
| `/okstra-graphify` | Build and query a knowledge graph over the project's own `.okstra/` memory—final reports, `decisions/*.md`, and `glossary.md`—with scope restricted to `.okstra/` and output under `.okstra/graph/` (subcommands: `build` / `query` / `path` / `explain` / `mcp` / `wiki`) |
|
package/docs/architecture.md
CHANGED
|
@@ -147,9 +147,9 @@ Runtime entry points are consolidated in Python packages. Bash and skills only c
|
|
|
147
147
|
- [`prompts/lead/okstra-lead-contract.md`](../prompts/lead/okstra-lead-contract.md) — main okstra lead contract. It is a runtime resource (`~/.okstra/prompts/lead/`), not an agent skill.
|
|
148
148
|
- [`skills/okstra-setup/SKILL.md`](../skills/okstra-setup/SKILL.md) — **first-run bootstrap**. Runs `okstra install` and creates `project.json`.
|
|
149
149
|
- [`skills/okstra-run/SKILL.md`](../skills/okstra-run/SKILL.md) — in-session entry point that **starts an okstra task in the current Claude session**. Calls `prepare_task_bundle` directly.
|
|
150
|
-
-
|
|
150
|
+
- Thirteen skills are user-invocable: `skills/okstra-setup/SKILL.md`, `skills/okstra-brief-gen/SKILL.md`, `skills/okstra-run/SKILL.md`, `skills/okstra-manager/SKILL.md`, `skills/okstra-memory/SKILL.md`, `skills/okstra-inspect/SKILL.md`, `skills/okstra-rollup/SKILL.md`, `skills/okstra-usage/SKILL.md`, `skills/okstra-schedule/SKILL.md`, `skills/okstra-container-build/SKILL.md`, `skills/okstra-graphify/SKILL.md`, `skills/okstra-pr-gen/SKILL.md`, and `skills/okstra-user-response/SKILL.md`. Only these are copied into the agent skill home. They cover brief authoring, phase execution, cross-project manager task coordination, global Memory Book storage/search, read-side status/history/report/time/logs/cost/errors/recap, task-group-level aggregation of run results (rollup), project-wide historical resource usage, schedule support, local container deployment, `.okstra` knowledge graphs, PR description generation, and clarification-response submission. `okstra-manager` uses `okstra manager` CLI JSON/launch packets as the source of truth, and stores manager-owned plans, assignments, directives, snapshots, and events under `~/.okstra/managers/<manager-id>/`. `okstra-rollup` is a read-side layer that fans the single-task aggregators from `okstra-inspect` (time/errors/recap) out to a task group or the whole project catalog. The `okstra rollup` CLI owns deterministic aggregation, while the skill (LLM) writes only the synthesized report summary. `okstra-usage` is a separate read-only resource snapshot grouped by lifecycle task type; it does not replace single-task `okstra-inspect` detail or the status/report digest from `okstra-rollup`. The canonical definition of `okstra-inspect` read-side facets is the subcommand table in `skills/okstra-inspect/SKILL.md`. `okstra-inspect logs` provides a read-only inventory and cleanup guidance for the live-log sidecars that the Codex/Antigravity wrappers write on every dispatch at the resolved `<run-dir>/prompts/<worker>-prompt-<phase>-<seq>.log`; for stage executions, the stage-qualified `run_dir` includes `stage-<N>/`. `okstra-inspect cost` summarizes `okstra context-cost`; `okstra-inspect errors` collects a task's okstra-run error logs into a timestamped Markdown error report and prints a summary; and `okstra-inspect recap` answers free-form questions about `.okstra` artifacts in addition to summarizing phases before and after each task run.
|
|
151
151
|
- Internal operating contracts—`context-loader` / `team-contract` / `convergence` / `report-writer` and the lead contract—have moved to `prompts/lead/*.md`. Language-specific coding preflight for implementation/verification workers has moved to `prompts/coding-preflight/*` (overview router + clean-code + three-stage language/framework/architecture selection). All are runtime resources installed under `~/.okstra/prompts/` and are not discoverable as skills. The generated launch prompt provides the lead with absolute paths, and reinstalling prunes the legacy exact-name skill directories `okstra-context-loader` / `okstra-team-contract` / `okstra-convergence` / `okstra-report-writer` / `okstra-coding-preflight` / `okstra`.
|
|
152
|
-
- Plugin manifest: [`../../.claude-plugin/plugin.json`](../.claude-plugin/plugin.json) — referenced by the supplementary `npx skills@latest add Devonshin/okstra` channel. Use `npx okstra@latest install` for normal setup. The plugin manifest exposes only the
|
|
152
|
+
- Plugin manifest: [`../../.claude-plugin/plugin.json`](../.claude-plugin/plugin.json) — referenced by the supplementary `npx skills@latest add Devonshin/okstra` channel. Use `npx okstra@latest install` for normal setup. The plugin manifest exposes only the thirteen user entry points (`okstra-setup`, `okstra-brief-gen`, `okstra-run`, `okstra-manager`, `okstra-memory`, `okstra-inspect`, `okstra-rollup`, `okstra-usage`, `okstra-schedule`, `okstra-container-build`, `okstra-graphify`, `okstra-pr-gen`, `okstra-user-response`).
|
|
153
153
|
- Installation location: `~/.claude/skills/<name>/SKILL.md` or `~/.agents/skills/<name>/SKILL.md`.
|
|
154
154
|
- Release procedure: [`../../RELEASING.md`](../RELEASING.md) — npm publish flow and release-please / manual fallback.
|
|
155
155
|
|
|
@@ -254,7 +254,7 @@ The standard `okstra` workflow applies the following team contract consistently
|
|
|
254
254
|
- The main Claude is always the `Claude lead` and operates in synthesis-only mode.
|
|
255
255
|
- The default required worker roles are `Claude worker`, `Codex worker`, and `Report writer worker`. `Antigravity worker` is optional and is included as required only when explicitly named by `--workers` or the profile's `- Workers:` section.
|
|
256
256
|
- `Report writer worker` focuses on report structure and evidence organization, but `Claude lead` remains the final synthesis owner.
|
|
257
|
-
- The default model contract is computed from central defaults. Fallbacks are `Claude lead`=`opus`, `Claude worker`=`opus`, `Codex worker`=`gpt-5.
|
|
257
|
+
- The default model contract is computed from central defaults. Fallbacks are `Claude lead`=`opus`, `Claude worker`=`opus`, `Codex worker`=`gpt-5.6`, and `Antigravity worker`=`auto` (when opted in). Without a separate override, `Report writer worker` follows the `Claude lead` model (therefore `opus` by default).
|
|
258
258
|
- Because `Antigravity worker` is optional, it is attempted only in runs where it is explicitly included.
|
|
259
259
|
- Before the final judgment, each required role in the current run's worker roster must have either a result or an explicit terminal status (`completed`, `timeout`, `error`, `not-run`).
|
|
260
260
|
- Every attempted worker (`completed`, `timeout`, `error`) must have an assigned worker prompt history file under the current run's `prompts/` directory.
|
|
@@ -711,6 +711,7 @@ Tokens used in each run are collected from lead/worker session transcripts and w
|
|
|
711
711
|
- Antigravity CLI: per-message `tokens.total` in `~/.antigravity/tmp/*/chats/session-*.json`
|
|
712
712
|
- Records billable-equivalent token math and USD cost estimates. It applies Anthropic billing ratios (`cache_creation_5m=1.25x`, `cache_creation_1h=2.0x`, `cache_read=0.1x`, `output=5x`). When the transcript provides separate `usage.cache_creation.ephemeral_5m_input_tokens` / `ephemeral_1h_input_tokens` values, they are counted separately.
|
|
713
713
|
- Pricing is centrally managed in `scripts/okstra_token_usage/pricing.py`. Update it when model prices change. Model IDs that fail price matching are exposed to the user in `usageSummary.unmatchedModels`, preventing silent-zero incidents.
|
|
714
|
+
- Project-wide historical usage is exposed through the read-only `okstra usage-report` command (`src/commands/inspect/usage-report.mjs` → `scripts/okstra_ctl/usage_report.py`) and the `okstra-usage` skill. It defaults to the whole current project's last 30 days and returns run coverage, raw and billable-equivalent tokens, known USD cost, CPU-sum milliseconds, and wall-clock milliseconds grouped by task type. Runs without usable Phase 7 usage are excluded from resource totals and reported through unavailable reason counts rather than treated as zero usage; unmatched model names remain visible when their tokens and time are included but their cost is not. Use `okstra-inspect` for one task's elapsed/context detail and `okstra-rollup` for task-group or project status/report digests.
|
|
714
715
|
- **Incremental scan cache (P6)**: To avoid rescanning session jsonl files, a per-file byte cursor and the extracted usage events before windowing are stored in `$OKSTRA_HOME/cache/token-usage/<transcript-dir>/<sessionId>.json` (`scripts/okstra_token_usage/cursor.py`). The run window (since/until) is reevaluated over events on every invocation, so even if a rerun narrows the window, the total matches a full scan. The cache is derived data; identifier mismatch, truncation, or corruption automatically falls back to a full rescan, and `okstra-token-usage.py --no-cache` forces a bypass.
|
|
715
716
|
- **Phase timeline (P0 instrumentation)**: The collector extracts `PROGRESS: phase-*` checkpoint lines (see "Progress reporting" in prompts/lead/okstra-lead-contract.md) from the lead session jsonl scoped to the run window, and records them in team-state as a `phaseTimeline` block (`{source, phases: [{phase, firstAt, lastAt, markerCount, wallMsToNext}]}`) (`scripts/okstra_token_usage/collect.py :: phase_timeline`). This provides measurement points for per-phase wall-clock time within the run and is consumed by the "Per-run phase breakdown" in the `okstra-inspect` time facet. Runs without markers explicitly report that measurement is unavailable with `phases: []`.
|
|
716
717
|
|
|
@@ -741,7 +742,7 @@ Each validator blocks the phase with a `contract-violated` exit code when a cont
|
|
|
741
742
|
- The run directory is organized into typed subdirectories such as `manifests/`, `state/`, `prompts/`, `reports/`, `status/`, `sessions/`, and `worker-results/`; prompt snapshots are prepared under `prompts/` first.
|
|
742
743
|
- Claude creates workers and collects results.
|
|
743
744
|
- The standard workflow uses a `Claude lead` with default workers `Claude worker`, `Codex worker`, and `Report writer worker`; `Antigravity worker` is optional and included only when explicitly requested.
|
|
744
|
-
- Worker models can be overridden with `--lead-model`, `--claude-model`, `--codex-model`, `--antigravity-model`, and `--report-writer-model`; defaults are centrally managed through `OKSTRA_DEFAULT_*` environment variables. Fallback defaults are `Claude lead`/`Report writer worker`=`opus`, `Claude worker`=`opus`, `Codex worker`=`gpt-5.
|
|
745
|
+
- Worker models can be overridden with `--lead-model`, `--claude-model`, `--codex-model`, `--antigravity-model`, and `--report-writer-model`; defaults are centrally managed through `OKSTRA_DEFAULT_*` environment variables. Fallback defaults are `Claude lead`/`Report writer worker`=`opus`, `Claude worker`=`opus`, `Codex worker`=`gpt-5.6`, and `Antigravity worker`=`auto`.
|
|
745
746
|
- For `--task-type implementation`, select the provider that takes the Executor role with `--executor <claude|codex|antigravity>` (or `OKSTRA_DEFAULT_EXECUTOR`, fallback `claude`). Only the Executor may mutate project files. The other two providers and the Executor's own provider are each dispatched as verifiers in separate CLI sessions (session isolation preserves the self-review safeguard). The Executor's model reuses the selected provider's worker-model flag (`--claude-model` / `--codex-model` / `--antigravity-model`). Provider / displayName / workerAgent / model are recorded in the run-manifest `teamContract.executor` block.
|
|
746
747
|
- Worktree cwd injection by Executor: Codex / Antigravity executors pin cwd to the worktree at the CLI layer through wrappers (`okstra-codex-exec.sh -C` / `okstra-antigravity-exec.sh --include-directories`). Because the Bash tool has no per-call cwd argument, the Claude executor prefixes cwd-sensitive toolchain invocations (`cargo`/`npm`/`pnpm`/`bun`/`pytest`/`make`/`go`) with `cd {{EXECUTOR_WORKTREE_PATH}} && <cmd>` in the same Bash invocation. Wrapping in `bash -lc`/`bash -c` is prohibited because it hides the leading `cd` token and defeats permission auto-allow. Prefer working-directory flags such as `git -C` or `cargo --manifest-path` when available. See the *Executor Worktree* block in `prompts/profiles/implementation.md` and the Executor exception in `agents/workers/claude-worker.md` for details.
|
|
747
748
|
- The project-level current-task convenience pointer is `.okstra/discovery/latest-task.json`.
|
package/docs/cli.md
CHANGED
|
@@ -342,7 +342,7 @@ The Codex worker (`--workers codex`, `--codex-model`) and Codex lead runtime are
|
|
|
342
342
|
|
|
343
343
|
> Every `--*-model` flag accepts only aliases registered in the provider mappings in `scripts/okstra_ctl/models.py`. An unregistered value is immediately rejected with `UnknownModelError`, preventing a contract violation where the manifest's `modelExecutionValue` differs from the actual execution value. Allowed values:
|
|
344
344
|
> - Claude (`--lead-model` / `--claude-model` / `--report-writer-model`): `fable`, `fable-5`, `claude-fable-5`, `opus`, `opus-4-8`, `claude-opus-4-8`, `opus-4-7`, `claude-opus-4-7`, `opus-4-6`, `claude-opus-4-6`, `sonnet`, `sonnet-4-6`, `claude-sonnet-4-6`, `haiku`, `haiku-4-5`, `claude-haiku-4-5`, `claude-haiku-4-5-20251001`
|
|
345
|
-
> - Codex (`--codex-model`): `gpt-5.5`, `gpt-5.4`, `gpt-5.4-mini`, `gpt-5.3-codex`, `gpt-5.2`, `codex-auto-review`
|
|
345
|
+
> - Codex (`--codex-model`): `gpt-5.6`, `gpt-5.5`, `gpt-5.4`, `gpt-5.4-mini`, `gpt-5.3-codex`, `gpt-5.2`, `codex-auto-review`
|
|
346
346
|
> - Antigravity (`--antigravity-model`): `gemini-3.1-pro` (default), `gemini-3.5-flash`, and the space-separated aliases `gemini 3.1 pro` / `gemini 3.5 flash`. The antigravity worker uses the `agy` CLI to run Gemini-family models, so model IDs retain the `gemini-*` form.
|
|
347
347
|
|
|
348
348
|
### `--claude-model`
|
|
@@ -358,7 +358,7 @@ When omitted, it uses the central default `OKSTRA_DEFAULT_LEAD_MODEL`, falling b
|
|
|
358
358
|
### `--codex-model`
|
|
359
359
|
|
|
360
360
|
Selects the model used by the `Codex worker`.
|
|
361
|
-
When omitted, it uses the central default `OKSTRA_DEFAULT_CODEX_MODEL`, falling back to `gpt-5.
|
|
361
|
+
When omitted, it uses the central default `OKSTRA_DEFAULT_CODEX_MODEL`, falling back to `gpt-5.6`.
|
|
362
362
|
|
|
363
363
|
### `--antigravity-model`
|
|
364
364
|
|
|
@@ -384,7 +384,7 @@ Fallback defaults are:
|
|
|
384
384
|
- `Claude lead`: `opus`
|
|
385
385
|
- `Report writer worker`: `opus`
|
|
386
386
|
- `Claude worker`: `opus`
|
|
387
|
-
- `Codex worker`: `gpt-5.
|
|
387
|
+
- `Codex worker`: `gpt-5.6`
|
|
388
388
|
- `Antigravity worker`: `auto`
|
|
389
389
|
- Implementation executor: `claude`, so the default is `Claude executor`.
|
|
390
390
|
|
|
@@ -394,7 +394,7 @@ Selects the provider that performs the Executor role for `--task-type implementa
|
|
|
394
394
|
|
|
395
395
|
- Default: `OKSTRA_DEFAULT_EXECUTOR` → fallback `claude`.
|
|
396
396
|
- The Executor is the **only worker allowed to mutate project files** in this run. The other two providers are dispatched as strict read-only verifiers in the same run.
|
|
397
|
-
- The Executor reuses the provider's worker model flag. With `--executor codex`, its model comes from `--codex-model`, default `gpt-5.
|
|
397
|
+
- The Executor reuses the provider's worker model flag. With `--executor codex`, its model comes from `--codex-model`, default `gpt-5.6`; with `--executor antigravity`, it comes from `--antigravity-model`, default `auto`.
|
|
398
398
|
- All three Claude, Codex, and Antigravity verifiers are always dispatched regardless of the Executor provider. Even the verifier using the same provider runs in a separate CLI session with isolated context, preserving the self-review safeguard.
|
|
399
399
|
- Codex and Antigravity mutate files through each CLI's auto-edit mode, for example `codex exec --sandbox workspace-write`, without passing through Claude-side Edit/Write tools. Mutations occur in the task worktree described below. Both wrappers—`scripts/okstra-codex-exec.sh` and `scripts/okstra-antigravity-exec.sh`—receive the worktree path as their fourth positional argument and forward it through `--add-dir` for Codex or `--include-directories` for Antigravity. Without it, the Codex `workspace-write` sandbox rejects worktree writes with EPERM.
|
|
400
400
|
- **Claude Executor cwd handling**: Claude's Bash tool has no per-call cwd argument and inherits the lead session cwd. To run cwd-sensitive toolchains such as `cargo`, `npm`, `pnpm`, `bun`, `pytest`, `make`, or `go` inside the worktree, prefix the invocation with `cd {{EXECUTOR_WORKTREE_PATH}} && <cmd>`. Keep `cd` as the leading token in a single Bash call so Claude Code permission auto-allow works; do not wrap it in `bash -lc "..."` or `bash -c "..."`, which hides `cd` and causes a permission prompt on every call. Prefer a tool's working-directory option—such as `git -C <path>`, `cargo --manifest-path`, or `pytest --rootdir`—over a `cd && ` chain. Edit/Write/Read tools already use absolute paths and need no cwd handling. This rule applies only to the Claude Executor; the Codex and Antigravity wrappers inject cwd.
|
|
@@ -637,6 +637,7 @@ The `okstra` Node CLI (`bin/okstra`) provides both installer/admin commands and
|
|
|
637
637
|
| `okstra memory <add\|list\|search\|show\|archive>` | Manage global conversation memory in `~/.okstra/memory-book`, a user-home store separate from project `.okstra/` and the CLI basis of the `save this in okstra` natural-language skill |
|
|
638
638
|
| `okstra manager <init\|discover-projects\|new\|task>` | Public CLI for grouping cross-project okstra tasks into manager-owned context. `new project`, `new task-group`, and `new task` create manager plans; `task assign`, `task note`, `task sync`, `task status`, and `task run` manage per-project assignments and snapshots. `new project --project-root` accepts only existing directories and performs setup-equivalent registration only if `.okstra/project.json` is absent. Public documentation uses the full `project-id:task-group:task-id` child task key; when child task IDs differ within the same manager task, select the exact child with `--child-task-id`. `task run` does not execute the child lead directly; it returns `prepared` launch metadata/event and a child launch-context packet as JSON |
|
|
639
639
|
| `okstra rollup [--task-group <group>] [--project-root <dir>] [--cwd <dir>]` | Read-only backend for the okstra-rollup skill. For every catalog task, or one task group, it emits JSON with per-task run counts, raw duration in ms, error counts, latest report paths, group totals, and status/category/phase distributions. Omitting `--task-group` targets the whole project catalog. The caller skill formats raw ms as HH:MM:SS and synthesizes report prose. Use the `okstra inspect` family for a single-task drill-down |
|
|
640
|
+
| `okstra usage-report [--days <positive-int>] [--project-root <dir>] [--cwd <dir>] [--json]` | Read-only backend for the okstra-usage skill. Defaults to the whole current project's last 30 days and emits task-type run coverage, raw/billable tokens, known USD cost, CPU-sum milliseconds, wall-clock milliseconds, unavailable reason counts, and unmatched pricing models |
|
|
640
641
|
| `okstra migrate [--apply] [--cwd <dir>] [--quiet]` | One-time migration of the project artifact root from `.project-docs/okstra/` to `.okstra/`. It is a dry run by default; `--apply` performs the move with `git mv` in a Git worktree, removes an empty `.project-docs/`, and synchronizes the `<PROJECT>/CLAUDE.md` import line, `.gitignore`, the project's rows in `~/.okstra/{recent,active}.jsonl`, and `~/.okstra/worktrees/registry.json`. It exits 1 if `.okstra/` already exists or the legacy directory is absent. Scheduled for removal by the end of v0.x |
|
|
641
642
|
| `okstra task-list [--project-root <path>]` | Combine `list_project_tasks` and `read_latest_task` into JSON containing the task catalog and latest task |
|
|
642
643
|
| `okstra task-show <task-key> [--project-root <path>]` | Summarize workflow, phase, status, and artifacts from the Task Read-Side Snapshot |
|
package/docs/for-ai/README.md
CHANGED
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
| 여러 프로젝트에 걸친 okstra task 묶음, 할당, sync snapshot, child launch packet 관리 | `okstra-manager` | [`skills/okstra-manager.md`](skills/okstra-manager.md) |
|
|
23
23
|
| 상태, history, report, time, logs, cost, errors, error-zip, recap 확인 | `okstra-inspect` | [`skills/okstra-inspect.md`](skills/okstra-inspect.md) |
|
|
24
24
|
| task-group(또는 프로젝트 전체)의 여러 task run 결과를 모아 집계·종합 요약 | `okstra-rollup` | [`skills/okstra-rollup.md`](skills/okstra-rollup.md) |
|
|
25
|
+
| Project-wide recent run coverage, tokens, known cost, CPU, and wall-clock usage by task type | `okstra-usage` | [`skills/okstra-usage.md`](skills/okstra-usage.md) |
|
|
25
26
|
| task-group 전체의 클라이언트용 작업 일정 생성 | `okstra-schedule` | [`skills/okstra-schedule.md`](skills/okstra-schedule.md) |
|
|
26
27
|
| 대화/결정/선호/요구사항을 전역 Memory Book에 저장 또는 검색 | `okstra-memory` | [`skills/okstra-memory.md`](skills/okstra-memory.md) |
|
|
27
28
|
| implementation task worktree 기반 docker compose 사용자 테스트 환경 관리 | `okstra-container-build` | [`skills/okstra-container-build.md`](skills/okstra-container-build.md) |
|
|
@@ -45,7 +46,7 @@
|
|
|
45
46
|
|
|
46
47
|
## 공개 스킬 목록
|
|
47
48
|
|
|
48
|
-
|
|
49
|
+
이 AI 매뉴얼에 나열된 공개 스킬은 다음 10개다.
|
|
49
50
|
|
|
50
51
|
- `okstra-setup`
|
|
51
52
|
- `okstra-brief-gen`
|
|
@@ -54,5 +55,6 @@
|
|
|
54
55
|
- `okstra-memory`
|
|
55
56
|
- `okstra-inspect`
|
|
56
57
|
- `okstra-rollup`
|
|
58
|
+
- `okstra-usage`
|
|
57
59
|
- `okstra-schedule`
|
|
58
60
|
- `okstra-container-build`
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# okstra-usage AI Manual
|
|
2
|
+
|
|
3
|
+
## Purpose
|
|
4
|
+
|
|
5
|
+
Use `okstra-usage` for a project-wide historical resource snapshot. It is not a
|
|
6
|
+
single-task inspection (`okstra-inspect`) and not a cross-task status/report
|
|
7
|
+
digest (`okstra-rollup`).
|
|
8
|
+
|
|
9
|
+
## Call
|
|
10
|
+
|
|
11
|
+
Run preflight, resolve a positive day count (default 30), then call exactly once:
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
okstra usage-report --days 30 --project-root <projectRoot> --json
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Render contract
|
|
18
|
+
|
|
19
|
+
Render returned task-type rows and totals without recomputing them. Show Runs,
|
|
20
|
+
Coverage, Raw tokens, Billable, Cost, CPU, and Wall. Surface unavailable reason
|
|
21
|
+
counts and unmatched model names. Missing usage is excluded data, not zero usage.
|
|
22
|
+
|
|
23
|
+
## Boundaries
|
|
24
|
+
|
|
25
|
+
- One task's elapsed/context detail: `okstra-inspect`.
|
|
26
|
+
- Task-group/project status and report digest: `okstra-rollup`.
|
|
27
|
+
- HTML dashboards, budgets, forecasts, and task/model/worker drill-downs are not
|
|
28
|
+
part of this MVP.
|
|
@@ -130,7 +130,7 @@ Runtime/install asset changes follow this checklist:
|
|
|
130
130
|
- `runtime/python/*` → `~/.okstra/lib/python/`
|
|
131
131
|
- `runtime/bin/*` → `~/.okstra/bin/`
|
|
132
132
|
- `runtime/templates/*` → `~/.okstra/templates/`
|
|
133
|
-
- `runtime/skills/<name>` (the
|
|
133
|
+
- `runtime/skills/<name>` (the thirteen user-facing skills only) → `~/.agents/skills` always, plus `~/.claude/skills` when `~/.claude` exists
|
|
134
134
|
- `runtime/prompts/*` → `~/.okstra/prompts/` (lead contracts under `prompts/lead/`, coding-preflight pack under `prompts/coding-preflight/`)
|
|
135
135
|
- `runtime/agents/workers/*.md` → `~/.claude/agents/*-worker.md` when `~/.claude` exists
|
|
136
136
|
- install manifests → `~/.okstra/installed-skills.json` (target-aware), `~/.okstra/installed-agents.json`
|
|
@@ -184,6 +184,7 @@ Runtime/install asset changes follow this checklist:
|
|
|
184
184
|
| `pr` | `src/commands/pr/pr.mjs` | `okstra pr <template\|branches\|gen>` — PR body template store under `~/.okstra/template/pr/` (bundled fallback `src/commands/pr/default.md`), base-branch recommendation, and the `gen` JSON bundle (template + `<base>..HEAD` commits + `<base>...HEAD` diffstat) backing the okstra-pr-gen skill. Git-only; no project registration required |
|
|
185
185
|
| `recap` | `src/commands/inspect/recap.mjs` | `okstra recap <assemble\|record>` Node wrapper backing the okstra-inspect `recap` facet |
|
|
186
186
|
| `rollup` | `src/commands/inspect/rollup.mjs` | `okstra rollup` thin shim into `scripts/okstra_ctl/rollup.py` — read-only cross-task roll-up backing the okstra-rollup skill |
|
|
187
|
+
| `usage-report` | `src/commands/inspect/usage-report.mjs` | `okstra usage-report` thin shim into `scripts/okstra_ctl/usage_report.py` — read-only project usage snapshot backing the okstra-usage skill |
|
|
187
188
|
| `container` | `src/commands/inspect/container.mjs` | `bin okstra container` thin shim into `scripts/okstra_ctl/container.py` for the okstra-container-build skill |
|
|
188
189
|
| `manager` | `src/commands/manager.mjs` | Thin shim into `scripts/okstra_ctl/manager_cli.py` for cross-project manager state and child launch packets |
|
|
189
190
|
|
|
@@ -228,7 +229,7 @@ Important modules:
|
|
|
228
229
|
| `recap.py` | okstra-inspect `recap` facet 의 결정론적 백엔드 — `assemble` 은 timeline 에서 run 간 phase 전이를 조립, `record` 는 `<task-root>/recap/recap-log.jsonl` 에 요약·Q&A append-only 기록 (다른 task 산출물 불변) |
|
|
229
230
|
| `render.py` | task manifest, run manifest, timeline, task index, discovery, team-state, prompt/template render |
|
|
230
231
|
| `workflow.py` | Phase sequence, allowed outputs, forbidden actions, next phase |
|
|
231
|
-
| `workers.py`, `models.py` | Worker roster
|
|
232
|
+
| `workers.py`, `models.py` | Worker roster; `models.py` is the model catalog SSOT (`ModelSpec` per alias + `ROLE_DEFAULTS`) — add-a-model single reference point from which picker options, codex pricing, and role defaults all derive |
|
|
232
233
|
| `worktree.py`, `worktree_registry.py` | One worktree per task-key, branch registry, sync dirs/files/snapshots |
|
|
233
234
|
| `project_meta.py`, `resolver.py`, `path_resolve.py` | Project/task/run resolution |
|
|
234
235
|
| `clarification_items.py` | Unified §5 clarification table parser and approval blockers |
|
|
@@ -244,6 +245,7 @@ Important modules:
|
|
|
244
245
|
| `error_report.py`, `error_log_core.py`, `error_zip.py` | okstra-inspect errors/error-zip facet 백엔드 — `error_log_core` 는 `errors-*.jsonl` 글롭·파싱·집계 read-only 코어, `error_report` 는 errors facet 렌더, `error_zip` 은 cross-project run 디렉터리 수집·allowlist 익명화·클러스터 집계·zip 생성 |
|
|
245
246
|
| `log_report.py`, `time_report.py` | okstra-inspect logs/time facet 의 read-side 백엔드 (`okstra log-report` 는 wrapper sidecar 로그 인벤토리, `okstra time-report` 는 task 단위 시간 집계) |
|
|
246
247
|
| `rollup.py` | okstra-rollup 스킬의 read-side 백엔드 — catalog 를 task-group(또는 프로젝트 전체) 단위로 fan-out 해 task 별 run 수·소요 시간(raw ms)·에러 수·최신 report 경로와 group 차원 합계/상태·카테고리·phase 분포를 deterministic 하게 집계. `time_report`/`error_log_core` 함수 재사용, report 본문 종합은 skill 에 위임 |
|
|
248
|
+
| `usage_report.py` | Read-only okstra-usage backend — scans the whole current project's recent run timelines, defaults to 30 days, and returns task-type coverage, raw/billable tokens, known USD cost, CPU-sum and wall-clock milliseconds, unavailable reason counts, and unmatched pricing models |
|
|
247
249
|
| `json_registry.py` | 작은 okstra 레지스트리 공용 flock + atomic JSON persistence (`registry_lock`/`load_registry_json`/`save_registry_json`) — `container_registry`·`worktree_registry` 가 공유 |
|
|
248
250
|
| `stage_integrate.py` | whole-task stage 통합(머지)+worktree teardown 코어 (`integrate_stages`) — whole-task final-verification 진입·`okstra integrate-stages`·container up 이 공유 |
|
|
249
251
|
| `resolve_task_key.py` | bare task-id → `task-catalog.json` 후보 entry 해석 skill 공용 helper (`okstra resolve-task-key`) |
|
|
@@ -320,7 +322,7 @@ Optional (v1.0 하위호환) 최상위 키:
|
|
|
320
322
|
|
|
321
323
|
### 4.10 `skills/`
|
|
322
324
|
|
|
323
|
-
|
|
325
|
+
13 user-facing skills (the only skills `okstra install` copies). The list SSOT is `USER_SKILL_NAMES` in `src/lib/skill-catalog.mjs`; the Claude plugin manifest and the installer both derive from it:
|
|
324
326
|
|
|
325
327
|
| Skill | User-invocable | Role |
|
|
326
328
|
|---|---:|---|
|
|
@@ -329,12 +331,14 @@ Optional (v1.0 하위호환) 최상위 키:
|
|
|
329
331
|
| `okstra-memory` | yes | Store/search/archive global conversation memory under `~/.okstra/memory-book` |
|
|
330
332
|
| `okstra-inspect` | yes | Unified read-side — sub-commands `status` (lifecycle + workStatus), `history` (past runs / re-run / resume), `report` (find final-report), `time` (elapsed-time breakdown), `logs` (wrapper log inventory + cleanup), `cost` (task bundle context/read cost) |
|
|
331
333
|
| `okstra-rollup` | yes | Cross-task roll-up — aggregate runs/time/errors across a task-group (or whole project) and synthesize a digest from the report files |
|
|
334
|
+
| `okstra-usage` | yes | Read-only project usage snapshot — aggregate recent run coverage, tokens, known cost, CPU, and wall-clock time by task type (default: 30 days) |
|
|
332
335
|
| `okstra-schedule` | yes | Generate task-group schedule |
|
|
333
336
|
| `okstra-container-build` | yes | Non-linear deploy tool — deploy a verified task's code as a docker compose group and watch each container (sub-commands `up` / `status` / `logs` / `stop-watcher` / `down`) |
|
|
334
337
|
| `okstra-graphify` | yes | Knowledge graph over the project's own `.okstra/` memory — final reports, `decisions/*.md`, `glossary.md`; scope restricted to `.okstra/`, output under `.okstra/graph/` (sub-commands `build` / `query` / `path` / `explain` / `mcp` / `wiki`) |
|
|
335
338
|
| `okstra-pr-gen` | yes | Register PR body templates under `~/.okstra/template/pr/` and generate a PR description from a branch diff (drives `okstra pr template` / `branches` / `gen`). **Global skill** — needs a Git repo, not `<PROJECT_ROOT>/.okstra/project.json` |
|
|
336
339
|
| `okstra-manager` | yes | Coordinate cross-project okstra tasks through manager-owned plans, assignments, sync snapshots, status, and child launch context packets |
|
|
337
340
|
| `okstra-setup` | yes | Install/check runtime and register project |
|
|
341
|
+
| `okstra-user-response` | yes | Submit answers to a run's open clarification items through the installed response helper without hand-editing artifacts |
|
|
338
342
|
|
|
339
343
|
> The former internal skills — `context-loader`, `team-contract`, `convergence`, `report-writer`, `coding-preflight` — and the lead contract are no longer skills. They ship as runtime resources under `prompts/lead/*` and `prompts/coding-preflight/*` (installed to `~/.okstra/prompts/`); see §4.11.
|
|
340
344
|
|
package/package.json
CHANGED
package/runtime/BUILD.json
CHANGED
|
@@ -159,10 +159,5 @@ ANTIGRAVITY_WORKER_MODEL_EXECUTION_VALUE=""
|
|
|
159
159
|
REPORT_WRITER_MODEL=""
|
|
160
160
|
REPORT_WRITER_MODEL_EXECUTION_VALUE=""
|
|
161
161
|
DEFAULT_WORKERS="claude,codex,report-writer"
|
|
162
|
-
DEFAULT_LEAD_MODEL_NAME="${OKSTRA_DEFAULT_LEAD_MODEL:-opus}"
|
|
163
|
-
DEFAULT_CLAUDE_WORKER_MODEL_NAME="${OKSTRA_DEFAULT_CLAUDE_MODEL:-opus}"
|
|
164
|
-
DEFAULT_CODEX_WORKER_MODEL_NAME="${OKSTRA_DEFAULT_CODEX_MODEL:-gpt-5.5}"
|
|
165
|
-
DEFAULT_ANTIGRAVITY_WORKER_MODEL_NAME="${OKSTRA_DEFAULT_ANTIGRAVITY_MODEL:-gemini-3.1-pro}"
|
|
166
|
-
DEFAULT_REPORT_WRITER_MODEL_NAME="${OKSTRA_DEFAULT_REPORT_WRITER_MODEL:-$DEFAULT_LEAD_MODEL_NAME}"
|
|
167
162
|
DISPLAY_COMMAND_NAME="${OKSTRA_COMMAND_NAME:-$(basename "$0")}"
|
|
168
163
|
DISPLAY_TOOL_NAME="${OKSTRA_TOOL_NAME:-okstra}"
|
|
@@ -90,7 +90,7 @@ options:
|
|
|
90
90
|
(Antigravity worker is optional; add \`antigravity\` explicitly, e.g. --workers claude,codex,antigravity,report-writer)
|
|
91
91
|
--lead-model Model for Claude lead. Default: OKSTRA_DEFAULT_LEAD_MODEL or opus
|
|
92
92
|
--claude-model Model for Claude worker. Default: OKSTRA_DEFAULT_CLAUDE_MODEL or opus
|
|
93
|
-
--codex-model Model for Codex worker. Default: OKSTRA_DEFAULT_CODEX_MODEL or gpt-5.
|
|
93
|
+
--codex-model Model for Codex worker. Default: OKSTRA_DEFAULT_CODEX_MODEL or gpt-5.6
|
|
94
94
|
--antigravity-model Model for Antigravity worker. Default: OKSTRA_DEFAULT_ANTIGRAVITY_MODEL or auto
|
|
95
95
|
--report-writer-model
|
|
96
96
|
Model for report writer worker. Default: OKSTRA_DEFAULT_REPORT_WRITER_MODEL or lead model default
|
|
@@ -124,7 +124,7 @@ model defaults:
|
|
|
124
124
|
Claude lead: OKSTRA_DEFAULT_LEAD_MODEL or opus
|
|
125
125
|
Report writer worker: OKSTRA_DEFAULT_REPORT_WRITER_MODEL or Claude lead default
|
|
126
126
|
Claude worker: OKSTRA_DEFAULT_CLAUDE_MODEL or opus
|
|
127
|
-
Codex worker: OKSTRA_DEFAULT_CODEX_MODEL or gpt-5.
|
|
127
|
+
Codex worker: OKSTRA_DEFAULT_CODEX_MODEL or gpt-5.6
|
|
128
128
|
Antigravity worker: OKSTRA_DEFAULT_ANTIGRAVITY_MODEL or auto
|
|
129
129
|
Implementation executor: OKSTRA_DEFAULT_EXECUTOR or claude (one of: claude | codex | antigravity)
|
|
130
130
|
|
|
@@ -292,7 +292,7 @@ Assigned worker prompt history path: <Project Root>/<Prompt History Path>
|
|
|
292
292
|
2. `team-state-<task-type>-<seq>.json` → `workers[].usage.cliModel` for that role (initial run's actual execution value)
|
|
293
293
|
3. The `**Model:**` line of the initial Phase 4 prompt for that role (read from its persisted prompt-history file)
|
|
294
294
|
|
|
295
|
-
If none of the three is available, **abort the reverify dispatch for that role** and record a `contract-violation` event via `okstra error-log append-observed`. Do NOT guess, do NOT fall back to training-data defaults — for codex this silently produces `o4-mini` instead of the assigned `gpt-5.
|
|
295
|
+
If none of the three is available, **abort the reverify dispatch for that role** and record a `contract-violation` event via `okstra error-log append-observed`. Do NOT guess, do NOT fall back to training-data defaults — for codex this silently produces `o4-mini` instead of the assigned `gpt-5.6`-class model.
|
|
296
296
|
|
|
297
297
|
For Codex/Antigravity wrapper subagents, the `**Model:** <role>, <modelExecutionValue>` line is what their wrapper extracts to pass into the underlying CLI's `--model` flag. Omitting it forces the wrapper to fall back to its own training-data knowledge of the CLI's historical default.
|
|
298
298
|
|
|
@@ -126,7 +126,7 @@ The table below documents those prep-time seed values **for reference only** —
|
|
|
126
126
|
| Claude lead | opus | -- | orchestration + convergence supervision + final-report review/approval |
|
|
127
127
|
| Report writer worker | opus | report-writer-worker | `agents/workers/report-writer-worker.md` |
|
|
128
128
|
| Claude worker | opus | claude-worker | `agents/workers/claude-worker.md` |
|
|
129
|
-
| Codex worker | gpt-5.
|
|
129
|
+
| Codex worker | gpt-5.6 | codex-worker | generated from `agents/workers/_cli-wrapper-template.md` + `codex-worker.params.json` |
|
|
130
130
|
| Antigravity worker | auto | antigravity-worker | generated from `agents/workers/_cli-wrapper-template.md` + `antigravity-worker.params.json` |
|
|
131
131
|
|
|
132
132
|
All three analysis workers use dedicated agent definitions; Codex/Antigravity wrappers handle external CLI invocation internally; Claude worker runs as an in-process subagent with explicitly registered MCP tools so it does not fall back to `claude --mcp-cli` Bash invocations.
|
|
@@ -8,7 +8,7 @@ profile document.
|
|
|
8
8
|
- Team contract (shared):
|
|
9
9
|
- `Claude lead` is synthesis-only and stays distinct from `Claude worker` (or, in `implementation`, the `Executor` and verifiers).
|
|
10
10
|
- `Report writer worker` is the **author** of the final-report file; `Claude lead` reviews and approves the produced draft and does NOT write the file itself (see `team-contract` and `report-writer` for the authoritative contract).
|
|
11
|
-
- default model assignments are resolved from centralised defaults; the fallback values are `Claude lead`/`Report writer worker`=`opus`, `Claude worker`=`opus`, `Codex worker`=`gpt-5.
|
|
11
|
+
- default model assignments are resolved from centralised defaults; the fallback values are `Claude lead`/`Report writer worker`=`opus`, `Claude worker`=`opus`, `Codex worker`=`gpt-5.6`, `Antigravity worker`=`auto`. Phase-specific overrides (e.g. `implementation`'s executor binding) live in the per-profile document.
|
|
12
12
|
- every required worker listed in the per-profile `Required workers:` block must be attempted; the final verdict waits until each has either a result or an explicit terminal status (`timeout`, `error`, `not-run`).
|
|
13
13
|
- unnamed generic parallel workers must not replace the required role roster, and no additional sub-agent dispatch is allowed beyond this roster.
|
|
14
14
|
- Worker interaction model (shared — read before inferring behaviour from the roster):
|
|
@@ -12,7 +12,7 @@ at Phase 5, BEFORE constructing the verifier worker dispatch prompts.
|
|
|
12
12
|
- **Verifier dispatch labelling.** Agent `name` = `<provider>-verifier` (here, and identically in `final-verification`); for a `codex` / `antigravity` verifier, Lead additionally injects `**Pane role:** verifier` into the dispatched prompt body. Naming/mapping rules and the token-attribution guarantee are owned by `prompts/lead/okstra-lead-contract.md` Phase 4 "Agent `name` on dispatch".
|
|
13
13
|
- The verifier slots are `Claude verifier` and `Codex verifier`, plus `Antigravity verifier` **only when `antigravity` is in the resolved `--workers` roster**. Every verifier in the resolved roster is dispatched regardless of which provider holds the executor role; the executor's own provider is run *separately* as a verifier (a fresh CLI session with no shared context) so that no verdict is produced from the same session that wrote the diff. Verifiers MUST NOT call Edit, Write, or any Bash command that mutates files outside the run's artifact directories. If a verifier wants a fix, it records the recommendation in its worker result; it does not apply the fix itself.
|
|
14
14
|
- Session isolation — not model-variant divergence — is the primary self-review safeguard: each verifier is a separate CLI invocation with its own context window, so reusing the same model variant for executor and same-provider verifier is acceptable. Different model variants (e.g. executor=opus / Claude verifier=sonnet) remain recommended when available.
|
|
15
|
-
- Phase-specific model defaults override the shared defaults: `Claude verifier`=`opus`, `Codex verifier`=`gpt-5.
|
|
15
|
+
- Phase-specific model defaults override the shared defaults: `Claude verifier`=`opus`, `Codex verifier`=`gpt-5.6`, `Antigravity verifier`=`auto` (only when present in the roster). The `Executor`'s model is taken from the provider-specific worker model corresponding to `--executor`: claude→`--claude-model` (default `opus`), codex→`--codex-model` (default `gpt-5.6`), antigravity→`--antigravity-model` (default `auto`).
|
|
16
16
|
- Verifiers read from the SAME working tree path the Executor used so they observe the exact diff the Executor produced. Verifiers remain strictly read-only there.
|
|
17
17
|
|
|
18
18
|
## Verifier QA duties (independent re-run mandate)
|
|
@@ -1,49 +1,66 @@
|
|
|
1
|
-
"""Model display / execution
|
|
1
|
+
"""Model catalog: display / execution / pricing / picker for okstra workers.
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
Single reference point. Adding a model = one ModelSpec entry here. Changing a
|
|
4
|
+
role default = one ROLE_DEFAULTS line here.
|
|
5
5
|
"""
|
|
6
6
|
from __future__ import annotations
|
|
7
7
|
|
|
8
8
|
from dataclasses import dataclass
|
|
9
|
+
from typing import Optional
|
|
9
10
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
"
|
|
21
|
-
"
|
|
22
|
-
"
|
|
23
|
-
"
|
|
24
|
-
"
|
|
25
|
-
"
|
|
26
|
-
"
|
|
27
|
-
"claude-
|
|
11
|
+
|
|
12
|
+
@dataclass(frozen=True)
|
|
13
|
+
class ModelSpec:
|
|
14
|
+
display: str
|
|
15
|
+
execution: str
|
|
16
|
+
pricing: Optional[tuple] = None # codex only: (input, cached_input, output) USD/1M
|
|
17
|
+
in_picker: bool = False # exposed in the wizard 3-option picker
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
CLAUDE = {
|
|
21
|
+
"fable": ModelSpec("fable", "fable", in_picker=True),
|
|
22
|
+
"fable-5": ModelSpec("fable-5", "claude-fable-5"),
|
|
23
|
+
"claude-fable-5": ModelSpec("fable-5", "claude-fable-5"),
|
|
24
|
+
"opus": ModelSpec("opus", "opus", in_picker=True),
|
|
25
|
+
"opus-4-8": ModelSpec("opus-4-8", "claude-opus-4-8"),
|
|
26
|
+
"claude-opus-4-8": ModelSpec("opus-4-8", "claude-opus-4-8"),
|
|
27
|
+
"opus-4-7": ModelSpec("opus-4-7", "claude-opus-4-7"),
|
|
28
|
+
"claude-opus-4-7": ModelSpec("opus-4-7", "claude-opus-4-7"),
|
|
29
|
+
"opus-4-6": ModelSpec("opus-4-6", "claude-opus-4-6"),
|
|
30
|
+
"claude-opus-4-6": ModelSpec("opus-4-6", "claude-opus-4-6"),
|
|
31
|
+
"sonnet": ModelSpec("sonnet", "sonnet", in_picker=True),
|
|
32
|
+
"sonnet-4-6": ModelSpec("sonnet-4-6", "claude-sonnet-4-6"),
|
|
33
|
+
"claude-sonnet-4-6": ModelSpec("sonnet-4-6", "claude-sonnet-4-6"),
|
|
34
|
+
"haiku": ModelSpec("haiku", "haiku", in_picker=True),
|
|
35
|
+
"haiku-4-5": ModelSpec("haiku-4-5", "claude-haiku-4-5"),
|
|
36
|
+
"claude-haiku-4-5": ModelSpec("haiku-4-5", "claude-haiku-4-5"),
|
|
37
|
+
"claude-haiku-4-5-20251001": ModelSpec("haiku-4-5", "claude-haiku-4-5-20251001"),
|
|
38
|
+
}
|
|
39
|
+
# Execution values stay `gemini-3.x`: the `agy` CLI runs Gemini-family models and
|
|
40
|
+
# accepts those slugs. Only the okstra provider/worker identity is "antigravity".
|
|
41
|
+
ANTIGRAVITY = {
|
|
42
|
+
"gemini-3.1-pro": ModelSpec("Gemini 3.1 Pro", "gemini-3.1-pro", in_picker=True),
|
|
43
|
+
"gemini 3.1 pro": ModelSpec("Gemini 3.1 Pro", "gemini-3.1-pro"),
|
|
44
|
+
"gemini-3.5-flash": ModelSpec("Gemini 3.5 Flash", "gemini-3.5-flash", in_picker=True),
|
|
45
|
+
"gemini 3.5 flash": ModelSpec("Gemini 3.5 Flash", "gemini-3.5-flash"),
|
|
28
46
|
}
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
"
|
|
34
|
-
"
|
|
35
|
-
"
|
|
36
|
-
"
|
|
47
|
+
CODEX = {
|
|
48
|
+
"gpt-5.6": ModelSpec("gpt-5.6", "gpt-5.6", (5.00, 0.50, 30.0), in_picker=True),
|
|
49
|
+
"gpt-5.5": ModelSpec("gpt-5.5", "gpt-5.5", (5.00, 0.50, 30.0), in_picker=True),
|
|
50
|
+
"gpt-5.4": ModelSpec("gpt-5.4", "gpt-5.4", (2.50, 0.25, 15.0), in_picker=True),
|
|
51
|
+
"gpt-5.4-mini": ModelSpec("gpt-5.4-mini", "gpt-5.4-mini", (0.75, 0.075, 4.50), in_picker=True),
|
|
52
|
+
"gpt-5.3-codex": ModelSpec("gpt-5.4", "gpt-5.4"),
|
|
53
|
+
"gpt-5.2": ModelSpec("gpt-5.2", "gpt-5.2", (1.75, 0.175, 14.0)),
|
|
54
|
+
"codex-auto-review": ModelSpec("codex-auto-review", "codex-auto-review"),
|
|
37
55
|
}
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
"
|
|
44
|
-
"
|
|
56
|
+
PROVIDER_MAPPINGS = {"claude": CLAUDE, "antigravity": ANTIGRAVITY, "codex": CODEX}
|
|
57
|
+
|
|
58
|
+
# ROLE-keyed (not provider): 1:1 with run.py recommended_role_models. "lead" uses
|
|
59
|
+
# the claude provider; "report-writer" is role-only (None → caller uses lead's).
|
|
60
|
+
ROLE_DEFAULTS = {
|
|
61
|
+
"lead": "opus", "claude": "opus", "codex": "gpt-5.6",
|
|
62
|
+
"antigravity": "auto", "report-writer": None,
|
|
45
63
|
}
|
|
46
|
-
PROVIDER_MAPPINGS = {"claude": CLAUDE_MAPPING, "antigravity": ANTIGRAVITY_MAPPING, "codex": CODEX_MAPPING}
|
|
47
64
|
|
|
48
65
|
|
|
49
66
|
@dataclass
|
|
@@ -59,15 +76,8 @@ class UnknownModelError(ValueError):
|
|
|
59
76
|
def resolve_model_metadata(
|
|
60
77
|
*, provider: str, raw_value: str, default_display: str, default_execution: str,
|
|
61
78
|
) -> ModelAssignment:
|
|
62
|
-
"""alias → (display,
|
|
63
|
-
|
|
64
|
-
provider: "claude" | "codex" | "antigravity" (그 외는 mapping 미적용)
|
|
65
|
-
|
|
66
|
-
Mapping 이 정의된 provider 에 대해 사용자가 빈 값이 아닌 raw_value 를 줬는데
|
|
67
|
-
mapping 에 없는 경우 `UnknownModelError` 를 발생시킨다. 이는 manifest 에
|
|
68
|
-
Codex 가 지원하지 않는 `gpt-5.5-high` 같은 유령 모델이 기록되어 실제
|
|
69
|
-
실행값과 contract 가 불일치하는 사고를 막기 위함이다.
|
|
70
|
-
"""
|
|
79
|
+
"""alias → (display, execution). Unchanged contract; mapping values are now
|
|
80
|
+
ModelSpec instead of tuples."""
|
|
71
81
|
raw_value = (raw_value or "").strip()
|
|
72
82
|
mapping = PROVIDER_MAPPINGS.get(provider)
|
|
73
83
|
if raw_value and mapping is not None:
|
|
@@ -83,5 +93,23 @@ def resolve_model_metadata(
|
|
|
83
93
|
execution = raw_value or default_execution
|
|
84
94
|
normalized = value.strip().lower()
|
|
85
95
|
if mapping and normalized in mapping:
|
|
86
|
-
|
|
96
|
+
spec = mapping[normalized]
|
|
97
|
+
display, execution = spec.display, spec.execution
|
|
87
98
|
return ModelAssignment(display=display, execution=execution)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def picker_options(provider: str) -> list[str]:
|
|
102
|
+
"""Picker-exposed aliases for a provider, in catalog insertion order."""
|
|
103
|
+
mapping = PROVIDER_MAPPINGS.get(provider, {})
|
|
104
|
+
return [alias for alias, spec in mapping.items() if spec.in_picker]
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def default_model(role: str) -> Optional[str]:
|
|
108
|
+
"""Role → default model alias (env override is the caller's concern)."""
|
|
109
|
+
return ROLE_DEFAULTS.get(role)
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def catalog_pricing(provider: str) -> dict:
|
|
113
|
+
"""Selectable-model pricing for a provider (codex only carries pricing)."""
|
|
114
|
+
mapping = PROVIDER_MAPPINGS.get(provider, {})
|
|
115
|
+
return {alias: spec.pricing for alias, spec in mapping.items() if spec.pricing is not None}
|
|
@@ -15,7 +15,7 @@ import sys
|
|
|
15
15
|
from pathlib import Path
|
|
16
16
|
|
|
17
17
|
from okstra_ctl.error_log_core import glob_error_logs, parse_records
|
|
18
|
-
from okstra_ctl.time_report import
|
|
18
|
+
from okstra_ctl.time_report import aggregate_time, load_runs
|
|
19
19
|
from okstra_project import (
|
|
20
20
|
ResolverError,
|
|
21
21
|
list_project_tasks,
|
|
@@ -30,7 +30,7 @@ def _collect_task(entry: dict, project_root: Path) -> dict:
|
|
|
30
30
|
cpuSumMs/wallClockMs 는 durationMs 가 있는 run 만 반영하므로 0 일 수 있다.
|
|
31
31
|
"""
|
|
32
32
|
task_root = Path(entry["_resolvedTaskRoot"])
|
|
33
|
-
runs =
|
|
33
|
+
runs, _ = load_runs(task_root)
|
|
34
34
|
time_agg = aggregate_time(runs, project_root)
|
|
35
35
|
records, _ = parse_records(glob_error_logs(task_root))
|
|
36
36
|
return {
|
|
@@ -46,7 +46,7 @@ from .final_report_schema import load_schema
|
|
|
46
46
|
from .final_report_paths import final_report_data_path as _final_report_data_path
|
|
47
47
|
from .lead_events import LeadEvent, append_lead_event
|
|
48
48
|
from .lead_runtime import ALLOWED_LEAD_RUNTIMES
|
|
49
|
-
from .models import ModelAssignment, resolve_model_metadata
|
|
49
|
+
from .models import ModelAssignment, default_model, resolve_model_metadata
|
|
50
50
|
from .schema_excerpt import build_schema_excerpt
|
|
51
51
|
from .path_resolve import relative_to_project_root, resolve_user_file
|
|
52
52
|
from .render import (
|
|
@@ -1339,12 +1339,12 @@ class _ModelBindings:
|
|
|
1339
1339
|
def recommended_role_models() -> dict[str, str]:
|
|
1340
1340
|
"""역할 → 추천 모델 display 값 (env override 반영). prepare 의 모델 해소와
|
|
1341
1341
|
wizard 의 안내 표기가 공유하는 단일 기준점."""
|
|
1342
|
-
lead_default = _default("OKSTRA_DEFAULT_LEAD_MODEL", "
|
|
1342
|
+
lead_default = _default("OKSTRA_DEFAULT_LEAD_MODEL", default_model("lead"))
|
|
1343
1343
|
return {
|
|
1344
1344
|
"lead": lead_default,
|
|
1345
|
-
"claude": _default("OKSTRA_DEFAULT_CLAUDE_MODEL", "
|
|
1346
|
-
"codex": _default("OKSTRA_DEFAULT_CODEX_MODEL", "
|
|
1347
|
-
"antigravity": _default("OKSTRA_DEFAULT_ANTIGRAVITY_MODEL", "
|
|
1345
|
+
"claude": _default("OKSTRA_DEFAULT_CLAUDE_MODEL", default_model("claude")),
|
|
1346
|
+
"codex": _default("OKSTRA_DEFAULT_CODEX_MODEL", default_model("codex")),
|
|
1347
|
+
"antigravity": _default("OKSTRA_DEFAULT_ANTIGRAVITY_MODEL", default_model("antigravity")),
|
|
1348
1348
|
"report-writer": _default("OKSTRA_DEFAULT_REPORT_WRITER_MODEL", lead_default),
|
|
1349
1349
|
}
|
|
1350
1350
|
|
|
@@ -18,13 +18,16 @@ from okstra_ctl.paths import resolve_under_root
|
|
|
18
18
|
from okstra_ctl.task_target import resolve_task_root
|
|
19
19
|
|
|
20
20
|
|
|
21
|
-
def
|
|
21
|
+
def load_runs(task_root: Path) -> tuple[list[dict], bool]:
|
|
22
|
+
path = task_root / "history" / "timeline.json"
|
|
22
23
|
try:
|
|
23
|
-
data = json.loads(
|
|
24
|
+
data = json.loads(path.read_text(encoding="utf-8"))
|
|
24
25
|
except (OSError, ValueError):
|
|
25
|
-
return []
|
|
26
|
-
runs = data.get("runs", [])
|
|
27
|
-
|
|
26
|
+
return [], True
|
|
27
|
+
runs = data.get("runs", []) if isinstance(data, dict) else []
|
|
28
|
+
if not isinstance(runs, list):
|
|
29
|
+
return [], True
|
|
30
|
+
return runs, False
|
|
28
31
|
|
|
29
32
|
|
|
30
33
|
def _task_key(task_root: Path) -> str:
|
|
@@ -190,8 +193,9 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
190
193
|
args = parser.parse_args(argv)
|
|
191
194
|
|
|
192
195
|
task_root, project_root = resolve_task_root(args.target, args.project_root, args.cwd)
|
|
196
|
+
runs, _ = load_runs(task_root)
|
|
193
197
|
result = {"ok": True, "taskKey": _task_key(task_root)}
|
|
194
|
-
result.update(aggregate_time(
|
|
198
|
+
result.update(aggregate_time(runs, project_root))
|
|
195
199
|
print(json.dumps(result, ensure_ascii=False, indent=2))
|
|
196
200
|
return 0
|
|
197
201
|
|
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import datetime as dt
|
|
5
|
+
import json
|
|
6
|
+
import math
|
|
7
|
+
import sys
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
from okstra_ctl.time_report import (
|
|
12
|
+
_duration_ms,
|
|
13
|
+
_read_state,
|
|
14
|
+
_wall_clock_ms,
|
|
15
|
+
load_runs,
|
|
16
|
+
)
|
|
17
|
+
from okstra_project import ResolverError, list_project_tasks, resolve_project_root
|
|
18
|
+
|
|
19
|
+
UTC = dt.timezone.utc
|
|
20
|
+
UNAVAILABLE_KEYS = (
|
|
21
|
+
"invalidRunTimestamp",
|
|
22
|
+
"teamStateMissing",
|
|
23
|
+
"usageSummaryMissing",
|
|
24
|
+
"durationMissing",
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _iso_z(value: dt.datetime) -> str:
|
|
29
|
+
return value.astimezone(UTC).isoformat().replace("+00:00", "Z")
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _parse_timestamp(value: Any) -> dt.datetime | None:
|
|
33
|
+
if not isinstance(value, str) or not value:
|
|
34
|
+
return None
|
|
35
|
+
try:
|
|
36
|
+
parsed = dt.datetime.fromisoformat(value.replace("Z", "+00:00"))
|
|
37
|
+
if parsed.tzinfo is None:
|
|
38
|
+
parsed = parsed.replace(tzinfo=UTC)
|
|
39
|
+
return parsed.astimezone(UTC)
|
|
40
|
+
except (OverflowError, ValueError):
|
|
41
|
+
return None
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _project_id(project_root: Path) -> str:
|
|
45
|
+
try:
|
|
46
|
+
data = json.loads(
|
|
47
|
+
(project_root / ".okstra" / "project.json").read_text(encoding="utf-8")
|
|
48
|
+
)
|
|
49
|
+
except (OSError, ValueError):
|
|
50
|
+
return ""
|
|
51
|
+
return str(data.get("projectId") or "") if isinstance(data, dict) else ""
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _int_value(value: Any) -> int:
|
|
55
|
+
try:
|
|
56
|
+
return int(value or 0)
|
|
57
|
+
except (OverflowError, TypeError, ValueError):
|
|
58
|
+
return 0
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _float_value(value: Any) -> float:
|
|
62
|
+
try:
|
|
63
|
+
result = float(value or 0)
|
|
64
|
+
except (OverflowError, TypeError, ValueError):
|
|
65
|
+
return 0.0
|
|
66
|
+
return result if math.isfinite(result) else 0.0
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _usage_block(value: Any) -> dict:
|
|
70
|
+
if not isinstance(value, dict):
|
|
71
|
+
return {}
|
|
72
|
+
block = {"source": value["source"]} if "source" in value else {}
|
|
73
|
+
if "durationMs" in value:
|
|
74
|
+
block["durationMs"] = _int_value(value["durationMs"])
|
|
75
|
+
for key in ("startedAt", "endedAt"):
|
|
76
|
+
if isinstance(value.get(key), str):
|
|
77
|
+
block[key] = value[key]
|
|
78
|
+
return block
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _usage_blocks(state: dict) -> tuple[dict, list[dict]]:
|
|
82
|
+
workers = state.get("workers")
|
|
83
|
+
worker_entries = workers if isinstance(workers, list) else []
|
|
84
|
+
return _usage_block(state.get("leadUsage")), [
|
|
85
|
+
_usage_block(worker.get("usage"))
|
|
86
|
+
for worker in worker_entries
|
|
87
|
+
if isinstance(worker, dict)
|
|
88
|
+
]
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def _wall_clock_from_blocks(lead: dict, workers: list[dict]) -> int:
|
|
92
|
+
state = {
|
|
93
|
+
"leadUsage": lead,
|
|
94
|
+
"workers": [{"usage": usage} for usage in workers],
|
|
95
|
+
}
|
|
96
|
+
try:
|
|
97
|
+
return _wall_clock_ms(state)
|
|
98
|
+
except TypeError:
|
|
99
|
+
return 0
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def _run_metrics(run: dict, project_root: Path) -> tuple[dict | None, str | None]:
|
|
103
|
+
state = _read_state(project_root, str(run.get("teamStatePath") or ""))
|
|
104
|
+
if state is None:
|
|
105
|
+
return None, "teamStateMissing"
|
|
106
|
+
summary = state.get("usageSummary")
|
|
107
|
+
if not isinstance(summary, dict) or _int_value(summary.get("grandTotalTokens")) <= 0:
|
|
108
|
+
return None, "usageSummaryMissing"
|
|
109
|
+
lead, workers = _usage_blocks(state)
|
|
110
|
+
durations = [_duration_ms(lead), *(_duration_ms(worker) for worker in workers)]
|
|
111
|
+
positive_durations = [duration for duration in durations if duration > 0]
|
|
112
|
+
if not positive_durations:
|
|
113
|
+
return None, "durationMissing"
|
|
114
|
+
costs = summary.get("estimatedCostUsd")
|
|
115
|
+
cost = costs.get("grandTotal", 0) if isinstance(costs, dict) else 0
|
|
116
|
+
unmatched = summary.get("unmatchedModels")
|
|
117
|
+
unmatched_models = unmatched if isinstance(unmatched, list) else []
|
|
118
|
+
return {
|
|
119
|
+
"rawTokens": _int_value(summary.get("grandTotalTokens")),
|
|
120
|
+
"billableTokens": _int_value(summary.get("grandBillableEquivalentTokens")),
|
|
121
|
+
"costUsd": _float_value(cost),
|
|
122
|
+
"cpuSumMs": sum(positive_durations),
|
|
123
|
+
"wallClockMs": _wall_clock_from_blocks(lead, workers),
|
|
124
|
+
"unmatchedModels": [
|
|
125
|
+
model for model in unmatched_models if isinstance(model, str) and model
|
|
126
|
+
],
|
|
127
|
+
}, None
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def _new_bucket(task_type: str, first_run_at: dt.datetime) -> dict:
|
|
131
|
+
return {
|
|
132
|
+
"taskType": task_type,
|
|
133
|
+
"firstRunAt": _iso_z(first_run_at),
|
|
134
|
+
"runs": 0,
|
|
135
|
+
"collectedRuns": 0,
|
|
136
|
+
"rawTokens": 0,
|
|
137
|
+
"billableTokens": 0,
|
|
138
|
+
"costUsd": 0.0,
|
|
139
|
+
"cpuSumMs": 0,
|
|
140
|
+
"wallClockMs": 0,
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def _collect_usage(
|
|
145
|
+
project_root: Path,
|
|
146
|
+
start_at: dt.datetime,
|
|
147
|
+
end_at: dt.datetime,
|
|
148
|
+
unavailable: dict[str, int],
|
|
149
|
+
warnings: dict[str, int],
|
|
150
|
+
) -> tuple[dict[str, dict], set[str], int, int]:
|
|
151
|
+
buckets: dict[str, dict] = {}
|
|
152
|
+
unmatched_models: set[str] = set()
|
|
153
|
+
total_runs = 0
|
|
154
|
+
collected_runs = 0
|
|
155
|
+
for entry in list_project_tasks(project_root):
|
|
156
|
+
runs, timeline_warning = load_runs(Path(entry["_resolvedTaskRoot"]))
|
|
157
|
+
warnings["missingOrInvalidTimelines"] += int(timeline_warning)
|
|
158
|
+
for run in runs:
|
|
159
|
+
if not isinstance(run, dict):
|
|
160
|
+
unavailable["invalidRunTimestamp"] += 1
|
|
161
|
+
continue
|
|
162
|
+
run_at = _parse_timestamp(run.get("runTimestamp"))
|
|
163
|
+
if run_at is None:
|
|
164
|
+
unavailable["invalidRunTimestamp"] += 1
|
|
165
|
+
continue
|
|
166
|
+
if run_at < start_at or run_at > end_at:
|
|
167
|
+
continue
|
|
168
|
+
task_type = str(run.get("taskType") or "unknown")
|
|
169
|
+
bucket = buckets.setdefault(task_type, _new_bucket(task_type, run_at))
|
|
170
|
+
run_at_text = _iso_z(run_at)
|
|
171
|
+
if run_at_text < bucket["firstRunAt"]:
|
|
172
|
+
bucket["firstRunAt"] = run_at_text
|
|
173
|
+
total_runs += 1
|
|
174
|
+
bucket["runs"] += 1
|
|
175
|
+
metrics, reason = _run_metrics(run, project_root)
|
|
176
|
+
if reason is not None:
|
|
177
|
+
unavailable[reason] += 1
|
|
178
|
+
continue
|
|
179
|
+
collected_runs += 1
|
|
180
|
+
bucket["collectedRuns"] += 1
|
|
181
|
+
for key in ("rawTokens", "billableTokens", "cpuSumMs", "wallClockMs"):
|
|
182
|
+
bucket[key] += metrics[key]
|
|
183
|
+
bucket["costUsd"] += metrics["costUsd"]
|
|
184
|
+
unmatched_models.update(metrics["unmatchedModels"])
|
|
185
|
+
return buckets, unmatched_models, total_runs, collected_runs
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def build_usage_report(project_root: Path, days: int, now: dt.datetime) -> dict:
|
|
189
|
+
if days <= 0:
|
|
190
|
+
raise ValueError("days must be a positive integer")
|
|
191
|
+
project_root = Path(project_root).resolve()
|
|
192
|
+
end_at = now.astimezone(UTC)
|
|
193
|
+
try:
|
|
194
|
+
start_at = end_at - dt.timedelta(days=days)
|
|
195
|
+
except OverflowError:
|
|
196
|
+
start_at = dt.datetime.min.replace(tzinfo=UTC)
|
|
197
|
+
unavailable = {key: 0 for key in UNAVAILABLE_KEYS}
|
|
198
|
+
warnings = {"missingOrInvalidTimelines": 0}
|
|
199
|
+
buckets, unmatched_models, total_runs, collected_runs = _collect_usage(
|
|
200
|
+
project_root, start_at, end_at, unavailable, warnings
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
rows = sorted(buckets.values(), key=lambda row: row["firstRunAt"])
|
|
204
|
+
for row in rows:
|
|
205
|
+
row["costUsd"] = round(_float_value(row["costUsd"]), 4)
|
|
206
|
+
totals = {
|
|
207
|
+
"runs": total_runs,
|
|
208
|
+
"collectedRuns": collected_runs,
|
|
209
|
+
"rawTokens": sum(row["rawTokens"] for row in rows),
|
|
210
|
+
"billableTokens": sum(row["billableTokens"] for row in rows),
|
|
211
|
+
"costUsd": round(_float_value(sum(row["costUsd"] for row in rows)), 4),
|
|
212
|
+
"cpuSumMs": sum(row["cpuSumMs"] for row in rows),
|
|
213
|
+
"wallClockMs": sum(row["wallClockMs"] for row in rows),
|
|
214
|
+
}
|
|
215
|
+
return {
|
|
216
|
+
"ok": True,
|
|
217
|
+
"projectId": _project_id(project_root),
|
|
218
|
+
"range": {"days": days, "startAt": _iso_z(start_at), "endAt": _iso_z(end_at)},
|
|
219
|
+
"runs": {
|
|
220
|
+
"total": total_runs,
|
|
221
|
+
"collected": collected_runs,
|
|
222
|
+
"collectionRate": collected_runs / total_runs if total_runs else 0.0,
|
|
223
|
+
},
|
|
224
|
+
"byTaskType": rows,
|
|
225
|
+
"totals": totals,
|
|
226
|
+
"unavailable": unavailable,
|
|
227
|
+
"warnings": warnings,
|
|
228
|
+
"unmatchedModels": sorted(unmatched_models),
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def utc_now() -> dt.datetime:
|
|
233
|
+
return dt.datetime.now(UTC)
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def _positive_days(value: str) -> int:
|
|
237
|
+
try:
|
|
238
|
+
days = int(value)
|
|
239
|
+
except ValueError as exc:
|
|
240
|
+
raise argparse.ArgumentTypeError("--days must be a positive integer") from exc
|
|
241
|
+
if days <= 0:
|
|
242
|
+
raise argparse.ArgumentTypeError("--days must be a positive integer")
|
|
243
|
+
return days
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def main(argv: list[str] | None = None) -> int:
|
|
247
|
+
parser = argparse.ArgumentParser(
|
|
248
|
+
prog="okstra usage-report",
|
|
249
|
+
description="Aggregate recent project usage by task type.",
|
|
250
|
+
)
|
|
251
|
+
parser.add_argument("--days", type=_positive_days, default=30)
|
|
252
|
+
parser.add_argument("--project-root", default="")
|
|
253
|
+
parser.add_argument("--cwd", default=".")
|
|
254
|
+
parser.add_argument("--json", action="store_true", help="emit JSON (always on)")
|
|
255
|
+
args = parser.parse_args(argv)
|
|
256
|
+
try:
|
|
257
|
+
project_root = Path(
|
|
258
|
+
resolve_project_root(explicit_root=args.project_root, cwd=args.cwd)
|
|
259
|
+
).resolve()
|
|
260
|
+
except ResolverError as exc:
|
|
261
|
+
print(json.dumps({"ok": False, "stage": "resolve", "reason": str(exc)}))
|
|
262
|
+
return 2
|
|
263
|
+
result = build_usage_report(project_root, args.days, utc_now())
|
|
264
|
+
print(json.dumps(result, ensure_ascii=False, indent=2))
|
|
265
|
+
return 0
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
if __name__ == "__main__":
|
|
269
|
+
raise SystemExit(main(sys.argv[1:]))
|
|
@@ -33,6 +33,7 @@ from okstra_ctl.ids import slugify_task_segment
|
|
|
33
33
|
from okstra_ctl.models import (
|
|
34
34
|
PROVIDER_MAPPINGS,
|
|
35
35
|
UnknownModelError,
|
|
36
|
+
picker_options,
|
|
36
37
|
resolve_model_metadata,
|
|
37
38
|
)
|
|
38
39
|
from okstra_ctl.clarification_items import scan_approval_gate, user_response_sidecars
|
|
@@ -128,9 +129,9 @@ _BRIEF_ENTRY_TASK_TYPES = ("requirements-discovery", "improvement-discovery",
|
|
|
128
129
|
CANONICAL_BASE_REFS = ["main", "dev", "staging", "preprod", "prod"]
|
|
129
130
|
BASE_REF_FREE_INPUT_TOKEN = "__free_input__"
|
|
130
131
|
|
|
131
|
-
CLAUDE_MODEL_OPTIONS = ["default", "
|
|
132
|
-
CODEX_MODEL_OPTIONS = ["default", "
|
|
133
|
-
ANTIGRAVITY_MODEL_OPTIONS = ["default", "
|
|
132
|
+
CLAUDE_MODEL_OPTIONS = ["default", *picker_options("claude")]
|
|
133
|
+
CODEX_MODEL_OPTIONS = ["default", *picker_options("codex")]
|
|
134
|
+
ANTIGRAVITY_MODEL_OPTIONS = ["default", *picker_options("antigravity")]
|
|
134
135
|
|
|
135
136
|
# special pick value: start a brand-new task
|
|
136
137
|
TASK_PICK_NEW_TOKEN = "__new__"
|
|
@@ -11,8 +11,9 @@ provider:
|
|
|
11
11
|
* Google / Gemini — `gemini-3.5-flash*`, `gemini-3.1-pro*`, `gemini-3-flash*`,
|
|
12
12
|
`gemini-2.5-pro*`, `gemini-2.5-flash*`, `gemini-2.0-flash*`.
|
|
13
13
|
|
|
14
|
-
|
|
15
|
-
|
|
14
|
+
Matching prefers the longest key that is a substring of the model id (most
|
|
15
|
+
specific wins), so table ordering does not affect the result. Update when
|
|
16
|
+
providers change list pricing.
|
|
16
17
|
|
|
17
18
|
Sources (last verified 2026-05-17, public list prices, USD per 1M tokens):
|
|
18
19
|
* Anthropic: https://www.anthropic.com/pricing
|
|
@@ -21,6 +22,8 @@ Sources (last verified 2026-05-17, public list prices, USD per 1M tokens):
|
|
|
21
22
|
"""
|
|
22
23
|
from __future__ import annotations
|
|
23
24
|
|
|
25
|
+
from okstra_ctl.models import catalog_pricing
|
|
26
|
+
|
|
24
27
|
|
|
25
28
|
# Anthropic billing ratios relative to base input: cache_creation (5m) = 1.25x,
|
|
26
29
|
# cache_creation (1h) = 2x, cache_read = 0.1x, output = 5x. The CLAUDE_PRICING
|
|
@@ -29,8 +32,8 @@ from __future__ import annotations
|
|
|
29
32
|
CLAUDE_PRICING = {
|
|
30
33
|
# model substring -> (input, cache_creation_5m, cache_read, output) USD/1M.
|
|
31
34
|
#
|
|
32
|
-
#
|
|
33
|
-
#
|
|
35
|
+
# _match_pricing picks the longest matching key (most specific), so ordering
|
|
36
|
+
# here is for readability, not correctness.
|
|
34
37
|
#
|
|
35
38
|
# For the newer 4.x point releases (Opus 4.7, Sonnet 4.6, Haiku 4.5),
|
|
36
39
|
# Anthropic's public price page only lists input/output. Cache-write and
|
|
@@ -65,20 +68,17 @@ CLAUDE_PRICING = {
|
|
|
65
68
|
# Anthropic 1h ephemeral cache_creation multiplier on the base input rate.
|
|
66
69
|
CLAUDE_CACHE_CREATE_1H_MULT = 2.0
|
|
67
70
|
|
|
68
|
-
|
|
71
|
+
# Legacy / non-selectable codex prices. Selectable-model prices (the aliases the
|
|
72
|
+
# picker offers) live in the model catalog and are merged in below via
|
|
73
|
+
# `catalog_pricing("codex")`. Substring match order is now longest-key-first
|
|
74
|
+
# (see `_match_pricing`), so this dict's ordering no longer affects matching.
|
|
75
|
+
# For models with no published cached-input rate (o1-pro, o3-pro), cached is set
|
|
76
|
+
# equal to input as a conservative no-discount default.
|
|
77
|
+
_LEGACY_CODEX_PRICING = {
|
|
69
78
|
# model substring -> (input USD/1M, cached_input USD/1M, output USD/1M).
|
|
70
|
-
# IMPORTANT: substring match order is insertion order. List the most
|
|
71
|
-
# specific keys first (e.g. `gpt-5-mini` before `gpt-5`, `o3-mini` before
|
|
72
|
-
# `o3`, `gpt-4o-mini` before `gpt-4o`, `gpt-4o` before the legacy `gpt-4`).
|
|
73
|
-
# For models with no published cached-input rate (o1-pro, o3-pro), cached
|
|
74
|
-
# is set equal to input as a conservative no-discount default.
|
|
75
79
|
|
|
76
80
|
# GPT-5 series.
|
|
77
|
-
"gpt-5.5": (5.00, 0.50, 30.0),
|
|
78
|
-
"gpt-5.4-mini": (0.75, 0.075, 4.50),
|
|
79
|
-
"gpt-5.4": (2.50, 0.25, 15.0),
|
|
80
81
|
"gpt-5.2-pro": (21.0, 2.10, 168.0),
|
|
81
|
-
"gpt-5.2": (1.75, 0.175, 14.0),
|
|
82
82
|
"gpt-5.1": (1.25, 0.125, 10.0),
|
|
83
83
|
"gpt-5-mini": (0.25, 0.025, 2.00),
|
|
84
84
|
"gpt-5-nano": (0.05, 0.005, 0.40),
|
|
@@ -101,6 +101,10 @@ CODEX_PRICING = {
|
|
|
101
101
|
"gpt-4": (2.50, 0.625, 10.0), # legacy gpt-4 fallback
|
|
102
102
|
}
|
|
103
103
|
|
|
104
|
+
# catalog + legacy: catalog owns selectable prices, legacy covers the rest. No
|
|
105
|
+
# key overlaps today; if one ever collided, the legacy (right) side would win.
|
|
106
|
+
CODEX_PRICING = {**catalog_pricing("codex"), **_LEGACY_CODEX_PRICING}
|
|
107
|
+
|
|
104
108
|
GEMINI_PRICING = {
|
|
105
109
|
# model substring -> (input USD/1M, output USD/1M).
|
|
106
110
|
#
|
|
@@ -141,7 +145,7 @@ def _match_pricing(model: str | None, table: dict) -> tuple | None:
|
|
|
141
145
|
if not model:
|
|
142
146
|
return None
|
|
143
147
|
m = model.lower()
|
|
144
|
-
for key, val in table.items():
|
|
148
|
+
for key, val in sorted(table.items(), key=lambda kv: len(kv[0]), reverse=True):
|
|
145
149
|
if key in m:
|
|
146
150
|
return val
|
|
147
151
|
return None
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: okstra-usage
|
|
3
|
+
description: >-
|
|
4
|
+
Show project-wide okstra resource usage over a recent day window. Use when the
|
|
5
|
+
user asks for okstra usage, token usage, estimated spend, CPU/wall time, usage
|
|
6
|
+
coverage, or which lifecycle task types consumed resources. Defaults to the
|
|
7
|
+
whole current project for the last 30 days. For one task's elapsed/context
|
|
8
|
+
detail use okstra-inspect; for status/report digests use okstra-rollup.
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
# OKSTRA Usage
|
|
12
|
+
|
|
13
|
+
Read-only project usage snapshot. The CLI owns every sum; never re-add rows.
|
|
14
|
+
|
|
15
|
+
## Step 0: Preflight
|
|
16
|
+
|
|
17
|
+
Run one literal Bash call:
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
okstra preflight --runtime claude-code --json
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
On `ok:true`, carry `projectRoot`. On `ok:false`, tell the user to run `/okstra-setup` first
|
|
24
|
+
and stop. On `unknown command: preflight`, tell the user to run
|
|
25
|
+
`npm i -g okstra@latest` and stop.
|
|
26
|
+
|
|
27
|
+
## Step 1: Resolve the day window
|
|
28
|
+
|
|
29
|
+
- No period stated: `30`.
|
|
30
|
+
- “last N days”: use N only when it is a positive integer.
|
|
31
|
+
- A calendar range, task/group/model/worker breakdown, forecast, or budget is
|
|
32
|
+
outside this MVP; state that limitation instead of inventing flags.
|
|
33
|
+
|
|
34
|
+
## Step 2: Fetch once
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
okstra usage-report --days 30 --project-root <projectRoot> --json
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Replace `30` only with the resolved positive integer. Do not wrap the command in
|
|
41
|
+
`if`, `eval`, command substitution, variable assignment, `&&`, `||`, or `npx`.
|
|
42
|
+
|
|
43
|
+
## Step 3: Render
|
|
44
|
+
|
|
45
|
+
If `ok:false`, report `stage` and `reason`, then stop. If `runs.total == 0`, say
|
|
46
|
+
`No okstra usage found in the last N days.`, omit the table, and continue to Step 4.
|
|
47
|
+
|
|
48
|
+
Otherwise, render `byTaskType` in returned order:
|
|
49
|
+
|
|
50
|
+
| Task type | Runs | Coverage | Raw tokens | Billable | Cost | CPU | Wall |
|
|
51
|
+
|---|---:|---:|---:|---:|---:|---:|---:|
|
|
52
|
+
|
|
53
|
+
- `Coverage` = returned `collectedRuns/runs`; never calculate a new aggregate.
|
|
54
|
+
- Format tokens with K/M suffixes, milliseconds as zero-padded `HH:MM:SS`, and
|
|
55
|
+
USD to two decimals. For tokens, use one decimal at `>= 1,000` (`K`) and
|
|
56
|
+
`>= 1,000,000` (`M`), trimming a trailing `.0`; below 1,000 render the integer.
|
|
57
|
+
- Render the `totals` object as the final bold row.
|
|
58
|
+
- Never treat unavailable usage as collected zero usage.
|
|
59
|
+
|
|
60
|
+
## Step 4: Diagnostics
|
|
61
|
+
|
|
62
|
+
- Render non-zero `unavailable` and `warnings` counts after the Step 3 output.
|
|
63
|
+
- If `unmatchedModels` is non-empty, mark Cost with `*` when the table exists and
|
|
64
|
+
always say known-price cost excludes those exact model names.
|
package/src/cli-registry.mjs
CHANGED
|
@@ -138,6 +138,13 @@ export const COMMAND_REGISTRY = [
|
|
|
138
138
|
category: "introspection",
|
|
139
139
|
summary: ["Aggregate elapsed work time across a task's runs (raw ms)"],
|
|
140
140
|
},
|
|
141
|
+
{
|
|
142
|
+
name: "usage-report",
|
|
143
|
+
module: "./commands/inspect/usage-report.mjs",
|
|
144
|
+
export: "run",
|
|
145
|
+
category: "introspection",
|
|
146
|
+
summary: ["Aggregate recent token, cost, and time usage by task type"],
|
|
147
|
+
},
|
|
141
148
|
{
|
|
142
149
|
name: "log-report",
|
|
143
150
|
module: "./commands/inspect/log-report.mjs",
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { runPythonModule } from "../../lib/python-helper.mjs";
|
|
2
|
+
|
|
3
|
+
const USAGE = `okstra usage-report — aggregate recent project usage by task type
|
|
4
|
+
|
|
5
|
+
Usage:
|
|
6
|
+
okstra usage-report [--days <positive-int>] [--project-root <dir>] [--cwd <dir>] [--json]
|
|
7
|
+
|
|
8
|
+
Read-only. Prints JSON containing run coverage, raw and billable-equivalent
|
|
9
|
+
tokens, known USD cost, CPU-sum time, and wall-clock time grouped by task type.
|
|
10
|
+
The default window is the current project's last 30 days.
|
|
11
|
+
`;
|
|
12
|
+
|
|
13
|
+
export async function run(args) {
|
|
14
|
+
if (args.includes("--help") || args.includes("-h")) {
|
|
15
|
+
process.stdout.write(USAGE);
|
|
16
|
+
return 0;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const result = await runPythonModule({
|
|
20
|
+
module: "okstra_ctl.usage_report",
|
|
21
|
+
args,
|
|
22
|
+
stdio: "inherit-stdout",
|
|
23
|
+
});
|
|
24
|
+
return result.code;
|
|
25
|
+
}
|