diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json new file mode 100644 index 0000000..6d80fb4 --- /dev/null +++ b/.claude-plugin/plugin.json @@ -0,0 +1,22 @@ +{ + "$schema": "https://json.schemastore.org/claude-code-plugin.json", + "name": "wgo", + "displayName": "Whats.Going.On.", + "version": "0.2.0+claude.20260804025205", + "description": "Evidence-led startup and SMB audits with decision-grade reports, detailed control artifacts, and optional draft operator aids.", + "author": { + "name": "Patrick Drolet", + "email": "patrick@drolet.name", + "url": "https://github.com/Patrick-Loves-Espresso" + }, + "homepage": "https://wgo-audit.com", + "repository": "https://github.com/wgo-audit/code", + "license": "MIT", + "keywords": [ + "audit", + "startup", + "smb", + "due-diligence", + "repo-analysis" + ] +} diff --git a/.codex-plugin/plugin.json b/.codex-plugin/plugin.json index b6cfc0b..c761b11 100644 --- a/.codex-plugin/plugin.json +++ b/.codex-plugin/plugin.json @@ -5,10 +5,10 @@ "author": { "name": "Patrick Drolet", "email": "patrick@drolet.name", - "url": "https://github.com/PatD42" + "url": "https://github.com/Patrick-Loves-Espresso" }, - "homepage": "https://github.com/PatD42/whats-going-on", - "repository": "https://github.com/PatD42/whats-going-on", + "homepage": "https://wgo-audit.com", + "repository": "https://github.com/wgo-audit/code", "license": "MIT", "keywords": [ "audit", @@ -28,7 +28,7 @@ "Interactive", "Write" ], - "websiteURL": "https://github.com/PatD42/whats-going-on", + "websiteURL": "https://wgo-audit.com", "defaultPrompt": [ "Run wgo:onboard.", "Run wgo:audit product-value.", diff --git a/README.md b/README.md index 1b8809f..4a83ca7 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ When key developers leave, a vendor handoff goes sideways, costs stop making sen Whats.Going.On. (WGO) is an open-source, prompt-first audit plugin for startups and SMBs. It guides an auditor from a plain-language concern to evidence-backed findings, owned decisions, and reports written for the people who must act on them. -**WGO runs locally with Codex or Claude. There is no hosted service, database, dashboard, or proprietary scoring model.** +**WGO runs locally with Codex, Claude, or OpenCode. There is no hosted service, database, dashboard, or proprietary scoring model.** > WGO is not another static analyzer. It is a semi-structured audit lead that helps you ask the right questions, inspect the right evidence, preserve uncertainty, and explain the result clearly. @@ -43,7 +43,7 @@ Clone WGO, then install it into the project you want to audit. macOS or Linux: ```bash -git clone https://github.com/PatD42/whats-going-on.git +git clone https://github.com/wgo-audit/code.git whats-going-on cd whats-going-on ./install.sh /path/to/project ``` @@ -51,21 +51,36 @@ cd whats-going-on Windows: ```bat -git clone https://github.com/PatD42/whats-going-on.git +git clone https://github.com/wgo-audit/code.git whats-going-on cd whats-going-on install.bat C:\path\to\project ``` -Open the target project in Codex or Claude and start onboarding: +Open the target project in Codex, Claude, or OpenCode and start onboarding: ```text # Codex wgo:onboard # Claude -/wgo_onboard +/wgo:onboard + +# OpenCode +/wgo-onboard ``` +The installer creates a native Codex plugin at `plugins/wgo` and a native +Claude plugin at `.claude/skills/wgo-claude`. It renders the same canonical +skill for OpenCode at `.opencode/skills/wgo` and adds thin command wrappers at +`.opencode/commands/wgo-*.md`. All three providers are rendered from the same +canonical skill and command files; provider-specific frontmatter is filtered +from installed copies, so the workflow cannot drift between maintained +implementations. +On first use, accept Claude's workspace-trust prompt; if Claude was already +open during installation, run `/reload-plugins` or reopen the project. +Restart OpenCode after installation. Until OpenCode indexes newly installed +skill files, its command wrappers load `.opencode/skills/wgo/SKILL.md` directly. + For a first audit, WGO confirms the company, product, business concern, additional evidence, and reviewers. It creates `_whats-going-on-YYYYMMDD/`, dated when onboarding starts, only after approval. @@ -113,7 +128,7 @@ flowchart TB 1. **Onboard:** establish the product, mandate, evidence boundary, success criteria, and selected reviewers. WGO also discovers project-local reviewer extensions, but never selects one without approval. 2. **Audit:** inspect one reviewer or run all selected reviewers in coordinator-defined dependency waves; register evidence, ask material questions, and update shared controls. 3. **Check status:** see completed work, open proof, access blockers, risks, decisions, and remaining reviewers. -4. **Summarize:** reconcile the whole audit and create decision-grade reports for each audience. +4. **Summarize:** reconcile the whole audit, create decision-grade reports for each audience, and produce a reconciled API-equivalent audit cost estimate from Codex, Claude, or OpenCode's own session records. 5. **Operationalize when justified:** after a completed audit, explicitly request draft operator aids. WGO will not execute them or change a system. Run a named reviewer when you want focused control. With no reviewer argument, @@ -122,13 +137,14 @@ that do not depend on each other. ## Commands -| Purpose | Codex | Claude | -|---|---|---| -| Start, improve, or compare an audit | `wgo:onboard` with `[compare\|blind-compare] [YYYYMMDD]` | `/wgo_onboard` with `[compare\|blind-compare] [YYYYMMDD]` | -| Run one reviewer or all selected reviewers | `wgo:audit [reviewer-id\|all]` | `/wgo_audit [reviewer-id\|all]` | -| Show truthful progress and blockers | `wgo:status` | `/wgo_status` | -| Reconcile findings and generate reports | `wgo:summarize` | `/wgo_summarize` | -| Draft operator aids after approved synthesis | `wgo:operationalize` | `/wgo_operationalize` | +| Purpose | Codex | Claude | OpenCode | +|---|---|---|---| +| Start, improve, or compare an audit | `wgo:onboard` with `[compare\|blind-compare] [YYYYMMDD]` | `/wgo:onboard` with `[compare\|blind-compare] [YYYYMMDD]` | `/wgo-onboard` with `[compare\|blind-compare] [YYYYMMDD]` | +| Run one reviewer or all selected reviewers | `wgo:audit [reviewer-id\|all]` | `/wgo:audit [reviewer-id\|all]` | `/wgo-audit [reviewer-id\|all]` | +| Show truthful progress and blockers | `wgo:status` | `/wgo:status` | `/wgo-status` | +| Reconcile findings and generate reports | `wgo:summarize` | `/wgo:summarize` | `/wgo-summarize` | +| Produce a reconciled API-equivalent audit cost estimate | `wgo:cost` | `/wgo:cost` | `/wgo-cost` | +| Draft operator aids after approved synthesis | `wgo:operationalize` | `/wgo:operationalize` | `/wgo-operationalize` | The audit always concerns the full project in the current folder, including all repository subfolders. @@ -199,6 +215,19 @@ An audit should not silently turn into operational work. `wgo:operationalize` is 2. The auditor explicitly requests operationalization. 3. Creating local audit artifacts is permitted. +When operationalization completes, WGO refreshes `controls/cost-estimate.md` +through the operator-aid phase. It preserves the earlier audit-only cost +manifest and excludes both cost-calculation passes from the estimate. + +Cost evidence is provider-specific rather than translated into a synthetic +common log format. Codex uses rollout JSONL request usage, Claude uses its root and +subagent JSONL transcripts with Anthropic's dated API rate card, and OpenCode +uses validated session exports with their recorded per-message provider cost. +Every platform freezes the included session graph before two independent +calculations. Missing provenance, malformed exports, unknown rates, and +unexplained zero-cost OpenCode models produce `Unreconciled` or `unpriced` +results instead of a guessed `$0.00`. + It produces a focused four-part operating packet. These are complementary operating perspectives, not variants of one generic runbook: @@ -241,17 +270,18 @@ expansion. It does not quietly treat the referenced material as reviewed. ## Improve Or Compare An Audit WGO audit state is local, readable Markdown rather than model-specific memory. -An auditor can begin an audit with Codex and later improve the same dated audit -root with Claude, or the reverse. Plain `wgo:onboard` reopens the newest root -read-write, displays its configuration, and asks whether anything needs -updating. - -`wgo:onboard compare [YYYYMMDD]` creates today's root and performs a targeted, -read-only comparison against the named completed baseline or the latest -completed audit. `wgo:onboard blind-compare [YYYYMMDD]` performs a full audit -without exposing baseline findings, then compares the completed audits. Both -comparison modes report reviewer-version differences and, when versions differ, -require the auditor to accept the installed versions before proceeding. +An auditor can begin an audit with Codex, Claude, or OpenCode and later improve +the same dated audit root with either of the other providers. The provider's +plain onboarding command reopens the newest root read-write, displays its +configuration, and asks whether anything needs updating. + +Adding `compare [YYYYMMDD]` to the provider's onboarding command creates today's +root and performs a targeted, read-only comparison against the named completed +baseline or the latest completed audit. Adding `blind-compare [YYYYMMDD]` +performs a full audit without exposing baseline findings, then compares the +completed audits. Both comparison modes report reviewer-version differences +and, when versions differ, require the auditor to accept the installed versions +before proceeding. The result is cumulative, but not blindly append-only: diff --git a/commands/audit.md b/commands/audit.md index 25ce88f..5a93982 100644 --- a/commands/audit.md +++ b/commands/audit.md @@ -3,15 +3,18 @@ name: audit description: Run one selected Whats.Going.On. reviewer, or all selected reviewers in dependency waves. args: "[reviewer-id|all]" skills: wgo +argument-hint: "[reviewer-id|all]" +disable-model-invocation: true --- # /audit -Codex users may invoke this as `wgo:audit `, `wgo:audit all`, or -`wgo:audit`. +Invoke this as `wgo:audit `/`wgo:audit all` in Codex, +`/wgo:audit `/`/wgo:audit all` in Claude, or +`/wgo-audit `/`/wgo-audit all` in OpenCode. -Resolve the newest dated audit root with -`skills/wgo/references/common/audit-root.md`. Read and write only that root. +Load and use the WGO skill. Resolve the newest dated audit root with its +`references/common/audit-root.md`. Read and write only that root. ## One Reviewer @@ -45,7 +48,8 @@ then: insights when they meet the shared standard, and compact handoff; then update the checklist and material shared open items. -If this completes the last selected reviewer, automatically run `wgo:summarize`. +If this completes the last selected reviewer, pass the delegated-task lifecycle +gate below, then automatically run `wgo:summarize`. ## All Selected Reviewers @@ -83,8 +87,9 @@ checklist. At the end of the wave, surface a qualifying auditor question before starting dependent work: ask one exact question, wait for its answer, record it in the brief and its existing `decision-needed` item, then ask another only if it remains material. Do not let `wgo:audit all` defer such a question merely to -keep the run uninterrupted. Then validate the audit root and start the next -wave. Once all selected reviewers are complete, automatically run +keep the run uninterrupted. Then pass the delegated-task lifecycle gate, +validate the audit root, and start the next wave. Once all selected reviewers +are complete, pass the gate again and automatically run `wgo:summarize`. Under `complete-missing`, if no reviewer work was missing and the existing synthesis already covers the unchanged selected reviewer set, do not rewrite it; report that nothing was missing. Under `rerun-all`, always rerun @@ -96,5 +101,18 @@ to reassess them. They do not search for unrelated new findings. For `blind-compare`, do not give reviewers any baseline content or prior identifiers. +## Delegated-Task Lifecycle Gate + +Before ending a reviewer, advancing a dependency wave, or starting synthesis, +recursively inventory every WGO task spawned during the current audit from the +recorded collaboration and task-lifecycle provenance. Require exactly one +terminal outcome (`completed`, `failed`, `cancelled`, or `interrupted`) for +every recorded start. Do not infer closure from a report, checklist state, +message, or worker disappearance. Wait for a running task; if a start remains +open, has multiple terminal outcomes, or cannot be correlated unambiguously, +stop the transition and identify the exact task/session. A failed, cancelled, +or interrupted task is lifecycle-closed but still follows the existing +dependency and audit-limitation rules. + Do not mark work complete merely because a report exists. A bounded, completed-with-open-verification conclusion is valid when its limits are clear. diff --git a/commands/cost.md b/commands/cost.md new file mode 100644 index 0000000..8f484d3 --- /dev/null +++ b/commands/cost.md @@ -0,0 +1,36 @@ +--- +name: cost +description: Produce a reconciled API-equivalent cost estimate for a completed Whats.Going.On. audit. +skills: wgo +disable-model-invocation: true +--- + +# /cost + +Invoke this as `wgo:cost` in Codex, `/wgo:cost` in Claude, or `/wgo-cost` in +OpenCode. + +Load and use the WGO skill. Resolve the newest dated audit root with its +`references/common/audit-root.md`, then read the audit platform recorded in +`audit-brief.md` and run exactly one matching workflow: + +- Codex: `references/common/cost-estimation.md`; +- Claude: `references/common/cost-estimation-claude.md`; or +- OpenCode: `references/common/cost-estimation-opencode.md`. + +Do not inspect a provider's session store or apply its usage schema to an audit +recorded on another platform. If the platform is missing, ambiguous, or +unsupported, write an `Unreconciled` cost control naming that limitation rather +than guessing. + +This is a read-only analysis of accessible provider-native session records that +writes `/controls/cost-estimate.md` and its frozen calculation +evidence. +Do not install a package, invoke `ccusage`, add a helper program, or use an +OS-specific binary. +The active-platform coordinator discovers the audit descendants from recorded +collaboration and task-lifecycle provenance, freezes the manifest before any +calculation, and runs exactly two independent calculations over that unchanged +manifest using the worker configuration in the selected provider workflow. +Never identify an included session solely by date, CWD, folder, or model; those +may corroborate recorded provenance only. diff --git a/commands/onboard.md b/commands/onboard.md index 016444b..a7c6f8c 100644 --- a/commands/onboard.md +++ b/commands/onboard.md @@ -3,12 +3,15 @@ name: onboard description: Design or resume a Whats.Going.On. audit for the current project folder. args: "[compare|blind-compare] [YYYYMMDD]" skills: wgo +argument-hint: "[compare|blind-compare] [YYYYMMDD]" +disable-model-invocation: true --- # /onboard -Resolve audit roots with `skills/wgo/references/common/audit-root.md`, then run -`skills/wgo/references/common/onboarding.md`. +Load and use the WGO skill. Resolve audit roots with its +`references/common/audit-root.md`, then run its +`references/common/onboarding.md`. The audit covers the full current project folder at detailed depth. Quietly perform read-only discovery and confirm company/product context. When a prior diff --git a/commands/operationalize.md b/commands/operationalize.md index e0f6ca9..a501e45 100644 --- a/commands/operationalize.md +++ b/commands/operationalize.md @@ -2,16 +2,19 @@ name: operationalize description: Turn an approved synthesis into explicitly untested, source-linked operator aids without executing procedures or changing systems. skills: wgo +disable-model-invocation: true --- # /operationalize -Codex users may invoke this as `wgo:operationalize`. +Invoke this as `wgo:operationalize` in Codex, `/wgo:operationalize` in Claude, +or `/wgo-operationalize` in OpenCode. -Resolve the newest dated audit root with -`skills/wgo/references/common/audit-root.md`. +Load and use the WGO skill. Resolve the newest dated audit root with its +`references/common/audit-root.md`. -Run the complete workflow in `skills/wgo/references/common/operationalization.md`. +Run the complete workflow in its +`references/common/operationalization.md`. Hard rules: @@ -25,3 +28,9 @@ Hard rules: - Create an operating-control strategy only when the completed controls need an explicit stabilization and authority model; it is a derived decision aid, not a second backlog or a runbook. - Create the four-part transition packet at `operator-aids/`: `replacement-maintainer`, `recovery`, `observability`, and `iam-and-credential-control`. Keep them separate, cross-linked, and evidence-bounded. - Do not create a fixed-size 72-hour mission board. Generate the execution-priority taxonomy instead. +- After the packet is complete, emit the operationalization-complete cutoff and + rerun the Codex, Claude, or OpenCode cost workflow selected from the platform + recorded in `audit-brief.md`. Update + `controls/cost-estimate.md` through operationalization without modifying the + frozen audit-only cost evidence or charging either cost-calculation phase to + the audit. diff --git a/commands/status.md b/commands/status.md index cc86b42..bdbe218 100644 --- a/commands/status.md +++ b/commands/status.md @@ -2,14 +2,16 @@ name: status description: Show truthful Whats.Going.On. audit progress, blockers, and material open items. skills: wgo +disable-model-invocation: true --- # /status -Codex users may invoke this as `wgo:status`. +Invoke this as `wgo:status` in Codex, `/wgo:status` in Claude, or +`/wgo-status` in OpenCode. -Resolve the newest dated audit root with -`skills/wgo/references/common/audit-root.md`. Read, without modifying: +Load and use the WGO skill. Resolve the newest dated audit root with its +`references/common/audit-root.md`. Read, without modifying: - `/audit-checklist.md` - `/controls/open-items.md` diff --git a/commands/summarize.md b/commands/summarize.md index 62e73b2..6279833 100644 --- a/commands/summarize.md +++ b/commands/summarize.md @@ -2,16 +2,18 @@ name: summarize description: Reconcile completed Whats.Going.On. reviewers and create decision-grade audience reports. skills: wgo +disable-model-invocation: true --- # /summarize -Codex users may invoke this as `wgo:summarize`. +Invoke this as `wgo:summarize` in Codex, `/wgo:summarize` in Claude, or +`/wgo-summarize` in OpenCode. -Resolve the newest dated audit root with -`skills/wgo/references/common/audit-root.md`. +Load and use the WGO skill. Resolve the newest dated audit root with its +`references/common/audit-root.md`. -Run `skills/wgo/references/common/synthesis.md`. Read completed +Run its `references/common/synthesis.md`. Read completed reviewer reports, their handoffs, and linked evidence/artifacts directly. Reconcile before drafting and preserve source-access limits, uncertainty, and evidence cutoff. @@ -21,5 +23,13 @@ and `technical-lead-notes.md` at the audit root. Include an owner-assigned 30–90 day section only where evidence supports it. Do not create a separate reconciliation command, decision queue, or action backlog. +After synthesis validation succeeds, run the complete `wgo:cost` phase before +asking about operationalization. It must discover and freeze the audit-session +manifest itself, select the provider workflow from `audit-brief.md`, use its two +independent verification passes over that manifest, and write +`controls/cost-estimate.md`. Link that control from `index.md` and the three +audience reports. A cost result may be `Unreconciled`; do not suppress it or +replace it with a precise total. + After the summary is complete, ask exactly: `Should I proceed with wgo:operationalize?` Do not draft operator aids until the auditor answers yes. diff --git a/docs/onboarding-expectations.md b/docs/onboarding-expectations.md index 5d27a69..94da903 100644 --- a/docs/onboarding-expectations.md +++ b/docs/onboarding-expectations.md @@ -1,8 +1,9 @@ # What To Expect During WGO Onboarding -`wgo:onboard` turns a broad concern into an audit boundary that is useful to -the people who will act on its results. Every independent audit uses -`_whats-going-on-YYYYMMDD`, dated when onboarding starts. +The WGO onboarding command turns a broad concern into an audit boundary that is +useful to the people who will act on its results. Invoke it as `wgo:onboard` in +Codex, `/wgo:onboard` in Claude, or `/wgo-onboard` in OpenCode. Every +independent audit uses `_whats-going-on-YYYYMMDD`, dated when onboarding starts. WGO audits the complete current folder and any confirmed supporting code repositories at detailed transition-control depth. It does not change the @@ -23,11 +24,11 @@ answers, known unknowns, and success criteria. Use one of three modes: -| Command | Behavior | -|---|---| -| `wgo:onboard` | Improve the newest audit in its existing read-write root. | -| `wgo:onboard compare [YYYYMMDD]` | Create today's audit root and reassess only findings and open items from the specified completed baseline, or the latest completed audit when the date is omitted. | -| `wgo:onboard blind-compare [YYYYMMDD]` | Run a full audit without exposing baseline findings, then compare the two completed audits in detail. | +| Behavior | Codex | Claude | OpenCode | +|---|---|---|---| +| Improve the newest audit in its existing read-write root. | `wgo:onboard` | `/wgo:onboard` | `/wgo-onboard` | +| Create today's audit root and reassess only findings and open items from the specified completed baseline, or the latest completed audit when the date is omitted. | `wgo:onboard compare [YYYYMMDD]` | `/wgo:onboard compare [YYYYMMDD]` | `/wgo-onboard compare [YYYYMMDD]` | +| Run a full audit without exposing baseline findings, then compare the two completed audits in detail. | `wgo:onboard blind-compare [YYYYMMDD]` | `/wgo:onboard blind-compare [YYYYMMDD]` | `/wgo-onboard blind-compare [YYYYMMDD]` | For either comparison mode, WGO compares the baseline reviewer versions with the installed packages. An unavailable selected package blocks the run. WGO @@ -164,6 +165,8 @@ It uses the auditor's active platform and existing session only; WGO never needs another model provider's credentials to prepare the catalog. For a reused configuration, answering no to the update question proceeds -without a second start question. First-ever onboarding asks: -`Should I proceed with wgo:audit all?` Once reviewers finish, WGO automatically -synthesizes the results and asks before drafting any operator aids. +without a second start question. First-ever onboarding asks permission to run +every selected reviewer. The corresponding commands are `wgo:audit all` in +Codex, `/wgo:audit all` in Claude, and `/wgo-audit all` in OpenCode. Once +reviewers finish, WGO automatically synthesizes the results and asks before +drafting any operator aids. diff --git a/install.bat b/install.bat index 03b651c..3d311e2 100644 --- a/install.bat +++ b/install.bat @@ -10,8 +10,11 @@ if "%~1"=="" ( set "PLUGIN_NAME=wgo" set "CODEX_DEST=%TARGET_DIR%\plugins\%PLUGIN_NAME%" -set "CLAUDE_COMMANDS_DEST=%TARGET_DIR%\.claude\commands" -set "CLAUDE_SKILLS_DEST=%TARGET_DIR%\.claude\skills" +set "CLAUDE_PLUGIN_DEST=%TARGET_DIR%\.claude\skills\%PLUGIN_NAME%-claude" +set "LEGACY_CLAUDE_COMMANDS_DEST=%TARGET_DIR%\.claude\commands" +set "LEGACY_CLAUDE_SKILL_DEST=%TARGET_DIR%\.claude\skills\%PLUGIN_NAME%" +set "OPENCODE_COMMANDS_DEST=%TARGET_DIR%\.opencode\commands" +set "OPENCODE_SKILL_DEST=%TARGET_DIR%\.opencode\skills\%PLUGIN_NAME%" set "PYTHON_VERSION=3.13.11" set "PYTHON_MINOR=313" set "PYMUPDF4LLM_PACKAGE=pymupdf4llm" @@ -22,6 +25,11 @@ if not exist "%SCRIPT_DIR%.codex-plugin\plugin.json" ( exit /b 1 ) +if not exist "%SCRIPT_DIR%.claude-plugin\plugin.json" ( + echo Missing source path: %SCRIPT_DIR%.claude-plugin\plugin.json + exit /b 1 +) + if not exist "%SCRIPT_DIR%commands" ( echo Missing source path: %SCRIPT_DIR%commands exit /b 1 @@ -48,36 +56,51 @@ xcopy "%SCRIPT_DIR%commands" "%CODEX_DEST%\commands\" /E /I /Y >nul if errorlevel 1 exit /b 1 xcopy "%SCRIPT_DIR%skills" "%CODEX_DEST%\skills\" /E /I /Y >nul if errorlevel 1 exit /b 1 +call :filter_frontmatter codex "%SCRIPT_DIR%skills\%PLUGIN_NAME%\SKILL.md" "%CODEX_DEST%\skills\%PLUGIN_NAME%\SKILL.md" +if errorlevel 1 exit /b 1 +for %%F in ("%SCRIPT_DIR%commands\*.md") do ( + call :filter_frontmatter codex "%%~fF" "%CODEX_DEST%\commands\%%~nxF" + if errorlevel 1 exit /b 1 +) del /s /q "%CODEX_DEST%\.DS_Store" >nul 2>nul -echo Installing Claude command and skill files... -mkdir "%CLAUDE_COMMANDS_DEST%" >nul 2>nul -mkdir "%CLAUDE_SKILLS_DEST%" >nul 2>nul -del /q "%CLAUDE_COMMANDS_DEST%\wgo_*.md" >nul 2>nul -copy /Y "%SCRIPT_DIR%commands\onboard.md" "%CLAUDE_COMMANDS_DEST%\wgo_onboard.md" >nul -if errorlevel 1 exit /b 1 -copy /Y "%SCRIPT_DIR%commands\audit.md" "%CLAUDE_COMMANDS_DEST%\wgo_audit.md" >nul +echo Installing Claude plugin files... +for %%C in (onboard audit status summarize operationalize) do del /q "%LEGACY_CLAUDE_COMMANDS_DEST%\wgo_%%C.md" >nul 2>nul +if exist "%LEGACY_CLAUDE_SKILL_DEST%" rmdir /s /q "%LEGACY_CLAUDE_SKILL_DEST%" +if exist "%CLAUDE_PLUGIN_DEST%" rmdir /s /q "%CLAUDE_PLUGIN_DEST%" +mkdir "%CLAUDE_PLUGIN_DEST%" >nul 2>nul +xcopy "%SCRIPT_DIR%.claude-plugin" "%CLAUDE_PLUGIN_DEST%\.claude-plugin\" /E /I /Y >nul if errorlevel 1 exit /b 1 -copy /Y "%SCRIPT_DIR%commands\status.md" "%CLAUDE_COMMANDS_DEST%\wgo_status.md" >nul +xcopy "%SCRIPT_DIR%commands" "%CLAUDE_PLUGIN_DEST%\commands\" /E /I /Y >nul if errorlevel 1 exit /b 1 -copy /Y "%SCRIPT_DIR%commands\summarize.md" "%CLAUDE_COMMANDS_DEST%\wgo_summarize.md" >nul +xcopy "%SCRIPT_DIR%skills\%PLUGIN_NAME%\references" "%CLAUDE_PLUGIN_DEST%\references\" /E /I /Y >nul if errorlevel 1 exit /b 1 -copy /Y "%SCRIPT_DIR%commands\operationalize.md" "%CLAUDE_COMMANDS_DEST%\wgo_operationalize.md" >nul +xcopy "%SCRIPT_DIR%skills\%PLUGIN_NAME%\scripts" "%CLAUDE_PLUGIN_DEST%\scripts\" /E /I /Y >nul if errorlevel 1 exit /b 1 -powershell -NoProfile -Command "(Get-Content '%CLAUDE_COMMANDS_DEST%\wgo_onboard.md') -replace '^name: onboard$', 'name: wgo_onboard' | Set-Content '%CLAUDE_COMMANDS_DEST%\wgo_onboard.md'" +call :filter_frontmatter claude "%SCRIPT_DIR%skills\%PLUGIN_NAME%\SKILL.md" "%CLAUDE_PLUGIN_DEST%\SKILL.md" if errorlevel 1 exit /b 1 -powershell -NoProfile -Command "(Get-Content '%CLAUDE_COMMANDS_DEST%\wgo_audit.md') -replace '^name: audit$', 'name: wgo_audit' | Set-Content '%CLAUDE_COMMANDS_DEST%\wgo_audit.md'" -if errorlevel 1 exit /b 1 -powershell -NoProfile -Command "(Get-Content '%CLAUDE_COMMANDS_DEST%\wgo_status.md') -replace '^name: status$', 'name: wgo_status' | Set-Content '%CLAUDE_COMMANDS_DEST%\wgo_status.md'" -if errorlevel 1 exit /b 1 -powershell -NoProfile -Command "(Get-Content '%CLAUDE_COMMANDS_DEST%\wgo_summarize.md') -replace '^name: summarize$', 'name: wgo_summarize' | Set-Content '%CLAUDE_COMMANDS_DEST%\wgo_summarize.md'" +for %%F in ("%SCRIPT_DIR%commands\*.md") do ( + call :filter_frontmatter claude "%%~fF" "%CLAUDE_PLUGIN_DEST%\commands\%%~nxF" + if errorlevel 1 exit /b 1 +) +del /s /q "%CLAUDE_PLUGIN_DEST%\.DS_Store" >nul 2>nul + +echo Installing OpenCode command files... +mkdir "%OPENCODE_COMMANDS_DEST%" >nul 2>nul +for %%C in (onboard audit status summarize operationalize) do ( + del /q "%OPENCODE_COMMANDS_DEST%\wgo-%%C.md" >nul 2>nul + call :filter_frontmatter opencode-command "%SCRIPT_DIR%commands\%%C.md" "%OPENCODE_COMMANDS_DEST%\wgo-%%C.md" + if errorlevel 1 exit /b 1 +) +if exist "%OPENCODE_SKILL_DEST%" rmdir /s /q "%OPENCODE_SKILL_DEST%" +mkdir "%OPENCODE_SKILL_DEST%" >nul 2>nul +xcopy "%SCRIPT_DIR%skills\%PLUGIN_NAME%\references" "%OPENCODE_SKILL_DEST%\references\" /E /I /Y >nul if errorlevel 1 exit /b 1 -powershell -NoProfile -Command "(Get-Content '%CLAUDE_COMMANDS_DEST%\wgo_operationalize.md') -replace '^name: operationalize$', 'name: wgo_operationalize' | Set-Content '%CLAUDE_COMMANDS_DEST%\wgo_operationalize.md'" +xcopy "%SCRIPT_DIR%skills\%PLUGIN_NAME%\scripts" "%OPENCODE_SKILL_DEST%\scripts\" /E /I /Y >nul if errorlevel 1 exit /b 1 -if exist "%CLAUDE_SKILLS_DEST%\%PLUGIN_NAME%" rmdir /s /q "%CLAUDE_SKILLS_DEST%\%PLUGIN_NAME%" -xcopy "%SCRIPT_DIR%skills\%PLUGIN_NAME%" "%CLAUDE_SKILLS_DEST%\%PLUGIN_NAME%\" /E /I /Y >nul +call :filter_frontmatter opencode-skill "%SCRIPT_DIR%skills\%PLUGIN_NAME%\SKILL.md" "%OPENCODE_SKILL_DEST%\SKILL.md" if errorlevel 1 exit /b 1 -del /s /q "%CLAUDE_SKILLS_DEST%\%PLUGIN_NAME%\.DS_Store" >nul 2>nul +del /s /q "%OPENCODE_SKILL_DEST%\.DS_Store" >nul 2>nul echo. echo Whats.Going.On. installed. @@ -86,8 +109,11 @@ echo Codex: echo %CODEX_DEST% echo. echo Claude: -echo %CLAUDE_COMMANDS_DEST%\wgo_*.md -echo %CLAUDE_SKILLS_DEST%\%PLUGIN_NAME% +echo %CLAUDE_PLUGIN_DEST% +echo. +echo OpenCode: +echo %OPENCODE_COMMANDS_DEST%\wgo-*.md +echo %OPENCODE_SKILL_DEST% echo. echo PDF extraction: echo PyMuPDF4LLM is optional. If installed, restart Codex or Claude before using it. @@ -98,12 +124,17 @@ echo installations leave their built-in WGO fallbacks in place. echo. echo Next: echo In Codex, run wgo:onboard to start an audit. -echo In Claude, run /wgo_onboard to start an audit. -echo After a completed synthesis, run wgo:operationalize or /wgo_operationalize only with explicit auditor approval. +echo In Claude, run /wgo:onboard to start an audit. +echo In OpenCode, run /wgo-onboard to start an audit. +echo After a completed synthesis, use the provider's WGO operationalize command only with explicit auditor approval. endlocal exit /b 0 +:filter_frontmatter +powershell -NoProfile -ExecutionPolicy Bypass -File "%SCRIPT_DIR%scripts\filter-frontmatter.ps1" -Provider "%~1" -Source "%~2" -Destination "%~3" +exit /b %errorlevel% + :ask_to_install echo. echo %~1 diff --git a/install.sh b/install.sh index cce4d78..321aca6 100755 --- a/install.sh +++ b/install.sh @@ -7,8 +7,11 @@ TARGET_DIR="$(cd "$TARGET_DIR" && pwd)" PLUGIN_NAME="wgo" CODEX_DEST="$TARGET_DIR/plugins/$PLUGIN_NAME" -CLAUDE_COMMANDS_DEST="$TARGET_DIR/.claude/commands" -CLAUDE_SKILLS_DEST="$TARGET_DIR/.claude/skills" +CLAUDE_PLUGIN_DEST="$TARGET_DIR/.claude/skills/${PLUGIN_NAME}-claude" +LEGACY_CLAUDE_COMMANDS_DEST="$TARGET_DIR/.claude/commands" +LEGACY_CLAUDE_SKILL_DEST="$TARGET_DIR/.claude/skills/$PLUGIN_NAME" +OPENCODE_COMMANDS_DEST="$TARGET_DIR/.opencode/commands" +OPENCODE_SKILL_DEST="$TARGET_DIR/.opencode/skills/$PLUGIN_NAME" PYTHON_VERSION="3.13.11" PYTHON_MINOR="3.13" PYMUPDF4LLM_PACKAGE="pymupdf4llm" @@ -30,6 +33,64 @@ copy_dir() { cp -R "$src" "$dest" } +filter_frontmatter() { + local source="$1" destination="$2" provider="$3" allowed + + case "$provider" in + codex) + allowed="|name|description|args|skills|" + ;; + claude) + allowed="|name|description|when_to_use|argument-hint|arguments|disable-model-invocation|user-invocable|allowed-tools|model|background|hooks|paths|shell|" + ;; + opencode-command) + allowed="|description|agent|model|variant|subtask|" + ;; + opencode-skill) + allowed="|name|description|license|compatibility|metadata|" + ;; + *) + echo "Unknown frontmatter provider: $provider" >&2 + exit 1 + ;; + esac + + awk -v allowed="$allowed" ' + NR == 1 && $0 == "---" { in_frontmatter = 1; keep = 1; print; next } + in_frontmatter && $0 == "---" { in_frontmatter = 0; print; next } + in_frontmatter { + if (match($0, /^[A-Za-z][A-Za-z0-9_-]*:/)) { + key = substr($0, 1, RLENGTH - 1) + keep = index(allowed, "|" key "|") > 0 + } + if (keep) print + next + } + { print } + ' "$source" > "$destination" +} + +render_opencode_command() { + local source="$1" destination="$2" filtered + + filtered="$(mktemp)" + filter_frontmatter "$source" "$filtered" opencode-command + awk ' + $0 == "---" { + delimiter_count++ + print + if (delimiter_count == 2) { + print "" + print "OpenCode command arguments: `$ARGUMENTS`." + print "Load the `wgo` skill with the OpenCode skill tool. If it is not listed yet, read `.opencode/skills/wgo/SKILL.md` directly." + } + next + } + { print } + ' "$filtered" > "$destination" + rm -f "$filtered" +} + find_python() { local candidate for candidate in python3 "python${PYTHON_MINOR}" python; do @@ -171,6 +232,7 @@ install_pymupdf4llm() { } require_source ".codex-plugin/plugin.json" +require_source ".claude-plugin/plugin.json" require_source "commands" require_source "skills/$PLUGIN_NAME/SKILL.md" @@ -186,23 +248,43 @@ mkdir -p "$CODEX_DEST" copy_dir "$SCRIPT_DIR/.codex-plugin" "$CODEX_DEST/.codex-plugin" copy_dir "$SCRIPT_DIR/commands" "$CODEX_DEST/commands" copy_dir "$SCRIPT_DIR/skills" "$CODEX_DEST/skills" +filter_frontmatter "$SCRIPT_DIR/skills/$PLUGIN_NAME/SKILL.md" "$CODEX_DEST/skills/$PLUGIN_NAME/SKILL.md" codex +for command in "$SCRIPT_DIR"/commands/*.md; do + filter_frontmatter "$command" "$CODEX_DEST/commands/$(basename "$command")" codex +done find "$CODEX_DEST" -name ".DS_Store" -type f -delete -echo "Installing Claude command and skill files..." -mkdir -p "$CLAUDE_COMMANDS_DEST" "$CLAUDE_SKILLS_DEST" -rm -f "$CLAUDE_COMMANDS_DEST"/wgo_*.md -cp "$SCRIPT_DIR/commands/onboard.md" "$CLAUDE_COMMANDS_DEST/wgo_onboard.md" -cp "$SCRIPT_DIR/commands/audit.md" "$CLAUDE_COMMANDS_DEST/wgo_audit.md" -cp "$SCRIPT_DIR/commands/status.md" "$CLAUDE_COMMANDS_DEST/wgo_status.md" -cp "$SCRIPT_DIR/commands/summarize.md" "$CLAUDE_COMMANDS_DEST/wgo_summarize.md" -cp "$SCRIPT_DIR/commands/operationalize.md" "$CLAUDE_COMMANDS_DEST/wgo_operationalize.md" -sed -i.bak 's/^name: onboard$/name: wgo_onboard/' "$CLAUDE_COMMANDS_DEST/wgo_onboard.md" && rm -f "$CLAUDE_COMMANDS_DEST/wgo_onboard.md.bak" -sed -i.bak 's/^name: audit$/name: wgo_audit/' "$CLAUDE_COMMANDS_DEST/wgo_audit.md" && rm -f "$CLAUDE_COMMANDS_DEST/wgo_audit.md.bak" -sed -i.bak 's/^name: status$/name: wgo_status/' "$CLAUDE_COMMANDS_DEST/wgo_status.md" && rm -f "$CLAUDE_COMMANDS_DEST/wgo_status.md.bak" -sed -i.bak 's/^name: summarize$/name: wgo_summarize/' "$CLAUDE_COMMANDS_DEST/wgo_summarize.md" && rm -f "$CLAUDE_COMMANDS_DEST/wgo_summarize.md.bak" -sed -i.bak 's/^name: operationalize$/name: wgo_operationalize/' "$CLAUDE_COMMANDS_DEST/wgo_operationalize.md" && rm -f "$CLAUDE_COMMANDS_DEST/wgo_operationalize.md.bak" -copy_dir "$SCRIPT_DIR/skills/$PLUGIN_NAME" "$CLAUDE_SKILLS_DEST/$PLUGIN_NAME" -find "$CLAUDE_SKILLS_DEST/$PLUGIN_NAME" -name ".DS_Store" -type f -delete +echo "Installing Claude plugin files..." +for legacy_command in onboard audit status summarize operationalize; do + rm -f "$LEGACY_CLAUDE_COMMANDS_DEST/wgo_${legacy_command}.md" +done +rm -rf "$LEGACY_CLAUDE_SKILL_DEST" +rm -rf "$CLAUDE_PLUGIN_DEST" +mkdir -p "$CLAUDE_PLUGIN_DEST" +copy_dir "$SCRIPT_DIR/.claude-plugin" "$CLAUDE_PLUGIN_DEST/.claude-plugin" +copy_dir "$SCRIPT_DIR/commands" "$CLAUDE_PLUGIN_DEST/commands" +copy_dir "$SCRIPT_DIR/skills/$PLUGIN_NAME/references" "$CLAUDE_PLUGIN_DEST/references" +copy_dir "$SCRIPT_DIR/skills/$PLUGIN_NAME/scripts" "$CLAUDE_PLUGIN_DEST/scripts" +filter_frontmatter "$SCRIPT_DIR/skills/$PLUGIN_NAME/SKILL.md" "$CLAUDE_PLUGIN_DEST/SKILL.md" claude +for command in "$SCRIPT_DIR"/commands/*.md; do + filter_frontmatter "$command" "$CLAUDE_PLUGIN_DEST/commands/$(basename "$command")" claude +done +find "$CLAUDE_PLUGIN_DEST" -name ".DS_Store" -type f -delete + +echo "Installing OpenCode command files..." +mkdir -p "$OPENCODE_COMMANDS_DEST" +for command_name in onboard audit status summarize operationalize; do + rm -f "$OPENCODE_COMMANDS_DEST/wgo-${command_name}.md" + render_opencode_command \ + "$SCRIPT_DIR/commands/${command_name}.md" \ + "$OPENCODE_COMMANDS_DEST/wgo-${command_name}.md" +done +rm -rf "$OPENCODE_SKILL_DEST" +mkdir -p "$OPENCODE_SKILL_DEST" +copy_dir "$SCRIPT_DIR/skills/$PLUGIN_NAME/references" "$OPENCODE_SKILL_DEST/references" +copy_dir "$SCRIPT_DIR/skills/$PLUGIN_NAME/scripts" "$OPENCODE_SKILL_DEST/scripts" +filter_frontmatter "$SCRIPT_DIR/skills/$PLUGIN_NAME/SKILL.md" "$OPENCODE_SKILL_DEST/SKILL.md" opencode-skill +find "$OPENCODE_SKILL_DEST" -name ".DS_Store" -type f -delete cat <.md controls/ open-items.md + cost-estimate.md reviewer-reports// report.md diff --git a/skills/wgo/references/common/artifact-quality-review.md b/skills/wgo/references/common/artifact-quality-review.md index 55af2dc..fd2be55 100644 --- a/skills/wgo/references/common/artifact-quality-review.md +++ b/skills/wgo/references/common/artifact-quality-review.md @@ -17,7 +17,9 @@ Check whether each artifact: 5. supports zero or more decision insights only where the evidence changes a decision, priority, sequence, claim, or stop condition. 6. identifies evidence-supported strengths that reduce a stated mandate concern, - when any exist, without treating an evidence gap as a strength. + when any exist, without treating an evidence gap as a strength; and +7. supports each finding's severity and effort classification from its evidenced + consequence, correction boundary, and stated limitations. A decision-insight candidate must state the decision, causal relationship or conflict, consequence, and smallest next proof/action. Reject fact restatements, diff --git a/skills/wgo/references/common/cost-estimation-claude.md b/skills/wgo/references/common/cost-estimation-claude.md new file mode 100644 index 0000000..6eae415 --- /dev/null +++ b/skills/wgo/references/common/cost-estimation-claude.md @@ -0,0 +1,480 @@ +# Claude Cost Estimation Workflow + +Use only when the audit platform recorded in `audit-brief.md` is Claude. This +is a portable Anthropic-API-equivalent estimate from Claude Code's own root and +subagent JSONL records. It is not a Claude subscription or provider invoice. + +The active Claude audit lead is the coordinator and final decision-maker. After +freezing the manifest, launch exactly two independent `haiku` Agent workers to +run the bounded calculation below. If `haiku` is unavailable, use the active +Claude model and disclose the substitution. Do not create one worker per +session, use `/usage` or `/cost` as the source of truth, install a package, or +add a persistent helper program. + +## 1. Discover And Freeze The Claude Manifest + +Start with the current audit's exact Claude root session ID from runtime/session +metadata. Resolve that ID to its transcript under Claude's application-data +directory. Never select a session by date, CWD, project-directory encoding, or +model; those may corroborate an ID but cannot establish membership. + +Recursively inspect only that root and its proven descendants: + +1. Find an assistant `Agent` or `Task` `tool_use` and record its tool-use ID, + description/WGO role, and parent transcript. +2. Match it to exactly one `subagents/agent-.meta.json` whose + `toolUseId` matches. Require the child JSONL records to carry that `agentId` + and the audit root `sessionId`. +3. Require exactly one terminal result. A synchronous result has the matching + tool-use ID and terminal `toolUseResult.status`. For an asynchronous launch, + `async_launched` is not terminal: require the later `task-notification` with + matching agent/task ID and `completed`, `failed`, `cancelled`, or + `interrupted` status. +4. Repeat for nested Agent/Task spawns found in an included child transcript. + +A missing transcript, metadata file, parent link, or terminal outcome is a +named reconciliation issue. Exclude other sessions and subagents in the same +project/day explicitly. Claude's documented transcript location is supporting +storage information, not proof that every file there belongs to this audit. + +Use the completion marker emitted immediately before this cost phase: + +- audit-only: `WGO_AUDIT_COMPLETE_COST_PHASE_STARTS`; +- through operationalization: + `WGO_OPERATIONALIZATION_COMPLETE_COST_PHASE_STARTS`. + +For the root, freeze the exact byte prefix through the line containing that +marker, including its terminating newline. For each terminal descendant, freeze +its complete JSONL and its metadata file, plus the exact parent spawn and +terminal line numbers. The parent marker's request belongs to the phase active +before the marker; the marker changes attribution only for later requests. + +Write `/controls/cost-manifest-claude.json`, or +`cost-manifest-claude-operationalized.json` for the refresh, before calculating. +Do not rewrite a manifest to make two calculations agree. Include at least: + +```json +{ + "schema_version": 1, + "provider": "claude-code", + "coverage": "audit", + "root_session_id": "root-session-uuid", + "pricing_basis": { + "rate_card": "references/data/anthropic-api-rate-card-2026-08-07.json", + "default_service_tier_when_not_returned": "standard", + "default_inference_geo_when_not_returned": "global" + }, + "phase_policy": { + "included": ["unattributed", "onboarding", "audit", "summary"], + "excluded": ["cost-estimation"], + "markers": { + "WGO_PHASE_ONBOARDING_START": "onboarding", + "WGO_PHASE_AUDIT_START": "audit", + "WGO_PHASE_SUMMARY_START": "summary", + "WGO_AUDIT_COMPLETE_COST_PHASE_STARTS": "cost-estimation", + "WGO_PHASE_OPERATIONALIZATION_START": "operationalization", + "WGO_OPERATIONALIZATION_COMPLETE_COST_PHASE_STARTS": "cost-estimation" + } + }, + "sessions": [ + { + "record_set_id": "root-session-uuid", + "session_id": "root-session-uuid", + "agent_id": null, + "file_path": "/accessible/claude/root-session-uuid.jsonl", + "prefix_sha256": "...", + "wgo_role_task_name": "Claude audit coordinator", + "phase": "from-markers", + "parent_record_set_id": null, + "root_relationship": "root", + "decision": "included", + "rationale": "Current Claude audit root through the explicit cutoff.", + "cutoff": { + "marker": "WGO_AUDIT_COMPLETE_COST_PHASE_STARTS", + "line_number": 81 + }, + "usage_schema": "claude-code-assistant-message-usage-v1" + }, + { + "record_set_id": "root-session-uuid:agent:abc123", + "session_id": "root-session-uuid", + "agent_id": "abc123", + "file_path": "/accessible/claude/root-session-uuid/subagents/agent-abc123.jsonl", + "prefix_sha256": "...", + "metadata_path": "/accessible/claude/root-session-uuid/subagents/agent-abc123.meta.json", + "metadata_sha256": "...", + "tool_use_id": "toolu_123", + "wgo_role_task_name": "wgo reviewer: security-privacy", + "phase": "audit", + "parent_record_set_id": "root-session-uuid", + "root_relationship": "descendant", + "decision": "included", + "rationale": "Matching spawn, subagent metadata, child agentId, and terminal result.", + "lifecycle": { + "spawn_line": 20, + "terminal_line": 62, + "outcome": "completed" + }, + "cutoff": {"line_number": 40}, + "usage_schema": "claude-code-assistant-message-usage-v1" + } + ], + "exclusions": [ + { + "record_set_id": "unrelated-session", + "file_path": "/accessible/claude/unrelated.jsonl", + "decision": "excluded", + "rationale": "No Agent/Task provenance path from the audit root." + } + ] +} +``` + +For an operationalization refresh, verify and reference the immutable +audit-only manifest, retain its membership, and add only the explicitly invoked +operationalization root/descendants. Extend a reused root only in the new +manifest. Exclude both cost phases by marker. Never rediscover the older audit +from dates or directory proximity. + +## 2. Calculate Request-Level Usage + +Read only included manifest prefixes. A request record is a Claude JSONL +`type: assistant` record with `message.model` and `message.usage`. Use +top-level `requestId`; fall back to `message.id` only when the request ID is +absent. Claude may append progressive copies of one assistant response. For a +stable ID, require the same message ID, model, phase, service tier, geography, +input, cache-creation, and cache-read values, then retain only the last +non-decreasing `output_tokens` value. Identical repeats are echoes. A changed +fixed field, message ID, phase, or decreasing output is disputed; do not choose +one silently. + +Total separately: input, 5-minute cache creation, 1-hour cache creation, cache +read, and output. Claude Code does not expose a separate reasoning-token total +in this schema. Require the nested 5-minute/1-hour cache split to equal +`cache_creation_input_tokens`; a nonzero unsplit value is unpriced. Never add a +tool result's aggregate `usage` or `totalTokens` to assistant-message usage. + +Use `../data/anthropic-api-rate-card-2026-08-07.json`. Price only exact model +and service-tier matches: + +```text +input × input rate ++ 5-minute cache creation × 5-minute write rate ++ 1-hour cache creation × 1-hour write rate ++ cache read × cache-read rate ++ output × output rate +``` + +Apply the US geography multiplier only when `inference_geo` is `us`. When the +field is missing or `not_available`, use the manifest's declared global +API-equivalent basis and disclose that geography was not observed. An unknown +model, tier, geography, cache split, or expired dated rate is `unpriced`, never +zero. + +## Portable One-Off Recipe + +Run this standard-library recipe independently twice. It writes JSON to stdout +and creates no helper file. + +```sh +python3 - /absolute/path/to/cost-manifest-claude.json /absolute/path/to/anthropic-api-rate-card-2026-08-07.json <<'PY' +import hashlib, json, sys +from collections import defaultdict +from decimal import Decimal +from pathlib import Path + +manifest_path = Path(sys.argv[1]).resolve() +manifest = json.loads(manifest_path.read_text(), parse_float=Decimal) +rates = json.loads(Path(sys.argv[2]).read_text(), parse_float=Decimal) +issues, limitations, duplicates, events, invalid = [], [], [], {}, set() +rows = defaultdict(lambda: { + "input_tokens": 0, "cache_write_5m_tokens": 0, + "cache_write_1h_tokens": 0, "cache_read_tokens": 0, + "output_tokens": 0, "reasoning_tokens": None, + "cost_usd": Decimal("0"), "priced": True, "request_ids": [] +}) +policy = manifest.get("phase_policy", {}) +included_phases = set(policy.get("included", ["unattributed"])) +markers = policy.get("markers", {}) +default_tier = manifest["pricing_basis"]["default_service_tier_when_not_returned"] +default_geo = manifest["pricing_basis"]["default_inference_geo_when_not_returned"] + +def integer(value, field, record_set_id, line): + if isinstance(value, bool) or not isinstance(value, (int, float, Decimal)): + issues.append({"kind": "schema", "record_set_id": record_set_id, + "line_number": line, "field": field}) + return None + value = Decimal(str(value)) + if value < 0 or value != value.to_integral_value(): + issues.append({"kind": "schema", "record_set_id": record_set_id, + "line_number": line, "field": field}) + return None + return int(value) + +def message_text(message): + content = message.get("content", []) + if isinstance(content, str): + return content + if not isinstance(content, list): + return "" + return "\n".join(item.get("text", "") for item in content + if isinstance(item, dict) and isinstance(item.get("text"), str)) + +if manifest.get("provider") != "claude-code": + issues.append({"kind": "wrong-provider", "observed": manifest.get("provider")}) +session_specs = { + item.get("record_set_id"): item for item in manifest.get("sessions", []) + if item.get("decision") == "included" +} + +for session in manifest.get("sessions", []): + if session.get("decision") != "included": + continue + record_set_id = session.get("record_set_id") + path = Path(session.get("file_path", "")) + if not path.is_absolute(): + path = manifest_path.parent / path + if not path.is_file(): + issues.append({"kind": "missing-file", "record_set_id": record_set_id, + "file_path": str(path)}) + continue + lines = path.read_bytes().splitlines(keepends=True) + end = session.get("cutoff", {}).get("line_number") + if not isinstance(end, int) or end < 1 or end > len(lines): + issues.append({"kind": "invalid-cutoff", "record_set_id": record_set_id}) + continue + prefix = b"".join(lines[:end]) + digest = hashlib.sha256(prefix).hexdigest() + if digest != session.get("prefix_sha256"): + issues.append({"kind": "changed-prefix", "record_set_id": record_set_id, + "expected": session.get("prefix_sha256"), "actual": digest}) + continue + marker = session.get("cutoff", {}).get("marker") + if marker and marker.encode() not in lines[end - 1]: + issues.append({"kind": "cutoff-marker-mismatch", + "record_set_id": record_set_id, "marker": marker}) + continue + if session.get("root_relationship") == "descendant": + meta_path = Path(session.get("metadata_path", "")) + if not meta_path.is_absolute(): + meta_path = manifest_path.parent / meta_path + if not meta_path.is_file(): + issues.append({"kind": "missing-subagent-metadata", + "record_set_id": record_set_id}) + continue + meta_bytes = meta_path.read_bytes() + if hashlib.sha256(meta_bytes).hexdigest() != session.get("metadata_sha256"): + issues.append({"kind": "changed-subagent-metadata", + "record_set_id": record_set_id}) + continue + try: + meta = json.loads(meta_bytes) + except (UnicodeDecodeError, json.JSONDecodeError): + issues.append({"kind": "invalid-subagent-metadata", + "record_set_id": record_set_id}) + continue + if meta.get("toolUseId") != session.get("tool_use_id"): + issues.append({"kind": "subagent-tool-use-mismatch", + "record_set_id": record_set_id}) + continue + parent = session_specs.get(session.get("parent_record_set_id")) + lifecycle = session.get("lifecycle", {}) + spawn_line, terminal_line = lifecycle.get("spawn_line"), lifecycle.get("terminal_line") + outcome = lifecycle.get("outcome") + if parent is None or outcome not in ("completed", "failed", "cancelled", "interrupted"): + issues.append({"kind": "missing-parent-lifecycle", + "record_set_id": record_set_id}) + continue + parent_path = Path(parent.get("file_path", "")) + if not parent_path.is_absolute(): + parent_path = manifest_path.parent / parent_path + if not parent_path.is_file(): + issues.append({"kind": "missing-parent-file", "record_set_id": record_set_id}) + continue + parent_lines = parent_path.read_bytes().splitlines(keepends=True) + parent_end = parent.get("cutoff", {}).get("line_number") + if (not isinstance(spawn_line, int) or not isinstance(terminal_line, int) or + not isinstance(parent_end, int) or not 1 <= spawn_line <= terminal_line <= parent_end or + parent_end > len(parent_lines)): + issues.append({"kind": "invalid-parent-lifecycle", + "record_set_id": record_set_id}) + continue + try: + spawn = json.loads(parent_lines[spawn_line - 1]) + terminal = json.loads(parent_lines[terminal_line - 1]) + except (UnicodeDecodeError, json.JSONDecodeError): + issues.append({"kind": "invalid-parent-lifecycle-json", + "record_set_id": record_set_id}) + continue + spawn_content = spawn.get("message", {}).get("content", []) + spawn_matches = [item for item in spawn_content if isinstance(item, dict) and + item.get("type") == "tool_use" and + item.get("name") in ("Agent", "Task") and + item.get("id") == session.get("tool_use_id")] + terminal_content = terminal.get("message", {}).get("content") + synchronous = ( + isinstance(terminal_content, list) and + any(isinstance(item, dict) and item.get("type") == "tool_result" and + item.get("tool_use_id") == session.get("tool_use_id") + for item in terminal_content) and + terminal.get("toolUseResult", {}).get("agentId") == session.get("agent_id") and + terminal.get("toolUseResult", {}).get("status") == outcome + ) + asynchronous = ( + isinstance(terminal_content, str) and + f"{session.get('agent_id')}" in terminal_content and + f"{outcome}" in terminal_content + ) + if len(spawn_matches) != 1 or not (synchronous or asynchronous): + issues.append({"kind": "parent-lifecycle-mismatch", + "record_set_id": record_set_id}) + continue + + active_phase = "unattributed" + fixed_phase = session.get("phase") + for line_number, raw in enumerate(lines[:end], 1): + try: + record = json.loads(raw) + except (UnicodeDecodeError, json.JSONDecodeError): + issues.append({"kind": "invalid-json", "record_set_id": record_set_id, + "line_number": line_number}) + continue + if record.get("type") != "assistant" or not isinstance(record.get("message"), dict): + continue + message = record["message"] + usage = message.get("usage") + phase = active_phase if fixed_phase == "from-markers" else fixed_phase + if isinstance(usage, dict): + request_id = record.get("requestId") or message.get("id") + message_id = message.get("id") + model = message.get("model") + observed_session = record.get("sessionId") + observed_agent = record.get("agentId") + if (not all(isinstance(value, str) and value for value in + (request_id, message_id, model)) or + observed_session != session.get("session_id") or + observed_agent != session.get("agent_id")): + issues.append({"kind": "identity", "record_set_id": record_set_id, + "line_number": line_number}) + else: + inp = integer(usage.get("input_tokens"), "input_tokens", record_set_id, line_number) + creation = integer(usage.get("cache_creation_input_tokens", 0), + "cache_creation_input_tokens", record_set_id, line_number) + read = integer(usage.get("cache_read_input_tokens", 0), + "cache_read_input_tokens", record_set_id, line_number) + output = integer(usage.get("output_tokens"), "output_tokens", record_set_id, line_number) + split = usage.get("cache_creation", {}) + five = integer(split.get("ephemeral_5m_input_tokens", 0), + "ephemeral_5m_input_tokens", record_set_id, line_number) if isinstance(split, dict) else None + hour = integer(split.get("ephemeral_1h_input_tokens", 0), + "ephemeral_1h_input_tokens", record_set_id, line_number) if isinstance(split, dict) else None + tier = usage.get("service_tier") or default_tier + observed_geo = usage.get("inference_geo") + geo = default_geo if observed_geo in (None, "", "not_available") else observed_geo + if observed_geo in (None, "", "not_available"): + limitations.append({"kind": "declared-inference-geo", "request_id": request_id, + "record_set_id": record_set_id, "basis": geo}) + if None not in (inp, creation, read, output, five, hour): + if five + hour != creation: + issues.append({"kind": "cache-creation-split", "request_id": request_id, + "record_set_id": record_set_id, "line_number": line_number}) + invalid.add(request_id) + fixed = (message_id, model, phase, tier, geo, inp, five, hour, read) + event = {"request_id": request_id, "message_id": message_id, + "record_set_id": record_set_id, "session_id": session.get("session_id"), + "agent_id": session.get("agent_id"), "line_number": line_number, + "phase": phase, "model": model, "service_tier": tier, + "inference_geo": geo, "input_tokens": inp, + "cache_write_5m_tokens": five, "cache_write_1h_tokens": hour, + "cache_read_tokens": read, "output_tokens": output, + "fixed": fixed} + previous = events.get(request_id) + if previous is None: + events[request_id] = event + elif previous["fixed"] != fixed or output < previous["output_tokens"]: + issues.append({"kind": "disputed-request", "request_id": request_id, + "first": {"record_set_id": previous["record_set_id"], + "line_number": previous["line_number"]}, + "second": {"record_set_id": record_set_id, + "line_number": line_number}}) + invalid.add(request_id) + else: + duplicates.append({"kind": "progressive" if output > previous["output_tokens"] else "echo", + "request_id": request_id, + "earlier": {"record_set_id": previous["record_set_id"], + "line_number": previous["line_number"]}, + "retained": {"record_set_id": record_set_id, + "line_number": line_number}}) + events[request_id] = event + if fixed_phase == "from-markers": + text = message_text(message) + for marker_name, next_phase in markers.items(): + if marker_name in text: + active_phase = next_phase + +for request_id, event in sorted(events.items()): + if request_id in invalid or event["phase"] not in included_phases: + continue + key = (event["phase"], event["record_set_id"], event["model"], + event["service_tier"], event["inference_geo"]) + row = rows[key] + for field in ("input_tokens", "cache_write_5m_tokens", "cache_write_1h_tokens", + "cache_read_tokens", "output_tokens"): + row[field] += event[field] + row["request_ids"].append(request_id) + try: + rate = rates["models"][event["model"]][event["service_tier"]] + multiplier = rates["inference_geo_multipliers"][event["inference_geo"]] + except KeyError: + row["priced"] = False + issues.append({"kind": "unpriced", "request_id": request_id, + "model": event["model"], "service_tier": event["service_tier"], + "inference_geo": event["inference_geo"]}) + continue + row["cost_usd"] += multiplier * ( + Decimal(event["input_tokens"]) * rate["input"] + + Decimal(event["cache_write_5m_tokens"]) * rate["cache_write_5m"] + + Decimal(event["cache_write_1h_tokens"]) * rate["cache_write_1h"] + + Decimal(event["cache_read_tokens"]) * rate["cache_read"] + + Decimal(event["output_tokens"]) * rate["output"] + ) / Decimal(1000000) + +flat = [] +for (phase, record_set_id, model, tier, geo), row in sorted(rows.items()): + row.update({"phase": phase, "record_set_id": record_set_id, "model": model, + "service_tier": tier, "inference_geo": geo}) + row["cost_usd"] = str(row["cost_usd"]) if row["priced"] else None + flat.append(row) +total = None if issues else str(sum(Decimal(row["cost_usd"]) for row in flat)) +print(json.dumps({"rows": flat, "duplicates": duplicates, "issues": issues, + "limitations": limitations, "total_cost_usd": total}, indent=2)) +PY +``` + +## 3. Independent Verification And Output + +Give both workers the same immutable manifest, JSONL prefixes, metadata files, +rate card, and this prompt: + +```text +You are a Claude cost-calculation worker. Use only the frozen manifest. Parse Claude assistant-message usage at request level, collapse progressive copies by stable request/message ID, and return the recipe's machine-checkable table. Do not discover sessions, count Agent tool-result aggregates, or guess missing cache splits, models, tiers, geography, or prices. Flag exact disputed records. +``` + +Preserve results as `cost-claude-pass-a.json` and +`cost-claude-pass-b.json` (or `*-operationalized-*`). Compare them exactly after +normalizing row order. Any mismatch names the record set, request ID, line, and +field and leaves the result `Unreconciled`. + +Read `../templates/cost-estimate-template.md` immediately before writing +`controls/cost-estimate.md`. Link the manifest and both result files. Show token +totals by phase, record set/session, model, tier, and geography; show the two +cache-write TTLs; show exact rate components and a total only when all included +requests reconcile and are priced. Display money as `$X.XX` while preserving +exact decimals in JSON evidence. + +Always state that this is an Anthropic-API-equivalent estimate, not Claude Code +subscription or provider billing. Include transcript retention/access, +plaintext-sensitive-data handling, progressive-record handling, declared +geography, unknown model/tier/rate, non-token tool charges, and rate-card expiry +among the limitations. See `../fixtures/cost-estimation-claude/README.md` for +the dry run. diff --git a/skills/wgo/references/common/cost-estimation-opencode.md b/skills/wgo/references/common/cost-estimation-opencode.md new file mode 100644 index 0000000..7cc42f2 --- /dev/null +++ b/skills/wgo/references/common/cost-estimation-opencode.md @@ -0,0 +1,460 @@ +# OpenCode Cost Estimation Workflow + +Use only when the audit platform recorded in `audit-brief.md` is OpenCode. This +is a portable replay of OpenCode's provider/model token usage and exact +per-message cost estimate. It is not an OpenCode, provider, subscription, or +local-compute invoice. + +The active OpenCode audit lead is the coordinator and final decision-maker. +After freezing the manifest, launch exactly two independent `general` +subagents over that manifest. They use the active audit model because OpenCode +provider/model availability is installation-specific. Do not create one worker +per session, build a universal cross-provider rate card, query the internal +SQLite database as the portable interface, or add a persistent helper program. + +## 1. Export, Discover, And Freeze The OpenCode Manifest + +Start with the current audit's exact OpenCode session ID supplied by the active +runtime. Export it with the installed CLI: + +```text +opencode export --pure +``` + +Save the exact stdout JSON as calculation evidence and validate that it parses. +If export fails or emits malformed JSON, record the file/command result and mark +the audit `Unreconciled`; never repair or silently skip it. `--sanitize` may be +used only after verifying that it preserves all session, parent, message, +provider, model, usage, cost, task-state, and phase-marker fields required here. + +Recursively follow only explicit OpenCode task provenance from each included +export. A parent `tool` part with `tool: task` records the task description, +agent type, `state.metadata.sessionId`, and lifecycle state. Require the child +export's `info.id` to equal that child ID and `info.parentID` to equal the +parent. A `running` task is not terminal; require one terminal completed, +failed/error, cancelled, or interrupted state before synthesis/cost closeout. +Repeat for nested child sessions. Date, CWD, project ID, directory, agent name, +or model may corroborate provenance but never establish audit membership. + +Use the exact completion marker emitted immediately before this cost phase: + +- audit-only: `WGO_AUDIT_COMPLETE_COST_PHASE_STARTS`; +- through operationalization: + `WGO_OPERATIONALIZATION_COMPLETE_COST_PHASE_STARTS`. + +Freeze every validated export's whole-file SHA-256. In the root export, record +the exact assistant message ID containing the completion marker and count only +messages through that ID. The marker-producing request belongs to the phase +active before the marker; the marker changes only later attribution. Give a +descendant its phase from the recorded parent WGO task. Record all same-project +or same-day sessions considered and excluded for lacking the parent/child path. + +Write `/controls/cost-manifest-opencode.json`, or +`cost-manifest-opencode-operationalized.json` for a refresh, before calculating: + +```json +{ + "schema_version": 1, + "provider": "opencode", + "coverage": "audit", + "root_session_id": "ses_root", + "pricing_basis": { + "basis_file": "references/data/opencode-cost-basis-2026-08-07.json", + "opencode_version": "1.x.y", + "zero_cost_authorizations": [] + }, + "phase_policy": { + "included": ["unattributed", "onboarding", "audit", "summary"], + "excluded": ["cost-estimation"], + "markers": { + "WGO_PHASE_ONBOARDING_START": "onboarding", + "WGO_PHASE_AUDIT_START": "audit", + "WGO_PHASE_SUMMARY_START": "summary", + "WGO_AUDIT_COMPLETE_COST_PHASE_STARTS": "cost-estimation", + "WGO_PHASE_OPERATIONALIZATION_START": "operationalization", + "WGO_OPERATIONALIZATION_COMPLETE_COST_PHASE_STARTS": "cost-estimation" + } + }, + "sessions": [ + { + "session_id": "ses_root", + "export_path": "/audit/evidence/opencode/ses_root.json", + "export_sha256": "...", + "wgo_role_task_name": "OpenCode audit coordinator", + "phase": "from-markers", + "parent_session_id": null, + "root_relationship": "root", + "decision": "included", + "rationale": "Current OpenCode audit root through the explicit marker message.", + "cutoff": { + "marker": "WGO_AUDIT_COMPLETE_COST_PHASE_STARTS", + "message_id": "msg_cost_boundary" + }, + "aggregate_reconciliation": "partial-export-not-applicable", + "usage_schema": "opencode-export-assistant-info-v1" + }, + { + "session_id": "ses_child", + "export_path": "/audit/evidence/opencode/ses_child.json", + "export_sha256": "...", + "wgo_role_task_name": "wgo reviewer: architecture", + "phase": "audit", + "parent_session_id": "ses_root", + "root_relationship": "descendant", + "decision": "included", + "rationale": "Completed parent task and matching child parentID.", + "provenance": { + "parent_message_id": "msg_spawn", + "task_part_id": "prt_task", + "call_id": "call_task", + "terminal_state": "completed" + }, + "cutoff": {"message_id": "msg_child_last"}, + "aggregate_reconciliation": "full-export", + "usage_schema": "opencode-export-assistant-info-v1" + } + ], + "exclusions": [ + { + "session_id": "ses_unrelated", + "export_path": "/audit/evidence/opencode/ses_unrelated.json", + "decision": "excluded", + "rationale": "No task/parent path from ses_root." + } + ] +} +``` + +A zero-cost authorization must name the exact `provider_id` and `model_id`, its +rationale, and evidence that zero means no metered API charge (for example, an +explicit local-model configuration). Plan credits or a provider returning +`0` without an independently established basis are `unpriced`, not `$0.00`. + +For an operationalization refresh, reference and hash the immutable audit-only +manifest, retain its session membership, and add only the explicitly invoked +operationalization root/children. Exclude both cost phases by marker. Do not +rediscover the older audit from project, directory, date, or model filters. + +## 2. Calculate Message-Level Usage And Recorded Cost + +Read only included exports through their frozen message cutoffs. Each assistant +message's `info.id` is the stable request/message identity. De-duplicate that ID +globally; identical repeats are echoes, while different provider, model, token, +cost, session, or phase fields are disputed. Never add `export.info.tokens`, +`export.info.cost`, database session totals, or `opencode stats` output to the +message-level totals. A full-export session aggregate may be compared to the +message sum as a separate validation only. + +Total `tokens.input`, `tokens.cache.read`, `tokens.cache.write`, +`tokens.output`, and `tokens.reasoning` separately by phase, session, provider, +and model. OpenCode reports reasoning separately in this schema. It remains +informational because the recorded message `cost` already applies the selected +provider's billing semantics. The export does not expose cache-write TTL, so do +not invent a 5-minute/1-hour split. + +Use the dated `../data/opencode-cost-basis-2026-08-07.json`. Sum exact positive +assistant-message `cost` values. An absent/invalid cost or unexplained zero is +`unpriced`. This deliberately avoids a stale, incomplete rate catalog for all +providers OpenCode can use. Record the OpenCode version and exact provider/model +IDs so the estimate is reproducible as a replay of what that runtime recorded. + +## Portable One-Off Recipe + +Run this standard-library recipe independently twice. It writes JSON to stdout +and creates no helper file. + +```sh +python3 - /absolute/path/to/cost-manifest-opencode.json /absolute/path/to/opencode-cost-basis-2026-08-07.json <<'PY' +import hashlib, json, sys +from collections import defaultdict +from decimal import Decimal +from pathlib import Path + +manifest_path = Path(sys.argv[1]).resolve() +manifest = json.loads(manifest_path.read_text(), parse_float=Decimal) +basis = json.loads(Path(sys.argv[2]).read_text(), parse_float=Decimal) +issues, duplicates, events = [], [], {} +rows = defaultdict(lambda: { + "input_tokens": 0, "cache_read_tokens": 0, "cache_write_tokens": 0, + "output_tokens": 0, "reasoning_tokens": 0, "cost_usd": Decimal("0"), + "priced": True, "message_ids": [] +}) +session_sums = {} +policy = manifest.get("phase_policy", {}) +included_phases = set(policy.get("included", ["unattributed"])) +markers = policy.get("markers", {}) +zero_authorized = { + (item.get("provider_id"), item.get("model_id")) + for item in manifest.get("pricing_basis", {}).get("zero_cost_authorizations", []) + if isinstance(item, dict) and item.get("rationale") +} + +def integer(value, field, session_id, message_id): + if isinstance(value, bool) or not isinstance(value, (int, float, Decimal)): + issues.append({"kind": "schema", "session_id": session_id, + "message_id": message_id, "field": field}) + return None + value = Decimal(str(value)) + if value < 0 or value != value.to_integral_value(): + issues.append({"kind": "schema", "session_id": session_id, + "message_id": message_id, "field": field}) + return None + return int(value) + +def money(value, session_id, message_id): + if isinstance(value, bool) or not isinstance(value, (int, float, Decimal)): + issues.append({"kind": "invalid-cost", "session_id": session_id, + "message_id": message_id}) + return None + value = Decimal(str(value)) + if value < 0: + issues.append({"kind": "invalid-cost", "session_id": session_id, + "message_id": message_id}) + return None + return value + +def text(parts): + if not isinstance(parts, list): + return "" + return "\n".join(part.get("text", "") for part in parts + if isinstance(part, dict) and part.get("type") == "text" and + isinstance(part.get("text"), str)) + +if manifest.get("provider") != "opencode": + issues.append({"kind": "wrong-provider", "observed": manifest.get("provider")}) +if basis.get("schema_version") != 1: + issues.append({"kind": "unsupported-cost-basis"}) +session_specs = { + item.get("session_id"): item for item in manifest.get("sessions", []) + if item.get("decision") == "included" +} + +for session in manifest.get("sessions", []): + if session.get("decision") != "included": + continue + session_id = session.get("session_id") + path = Path(session.get("export_path", "")) + if not path.is_absolute(): + path = manifest_path.parent / path + if not path.is_file(): + issues.append({"kind": "missing-export", "session_id": session_id, + "export_path": str(path)}) + continue + raw = path.read_bytes() + digest = hashlib.sha256(raw).hexdigest() + if digest != session.get("export_sha256"): + issues.append({"kind": "changed-export", "session_id": session_id, + "expected": session.get("export_sha256"), "actual": digest}) + continue + try: + export = json.loads(raw, parse_float=Decimal) + except (UnicodeDecodeError, json.JSONDecodeError): + issues.append({"kind": "malformed-export", "session_id": session_id}) + continue + info, messages = export.get("info"), export.get("messages") + if (not isinstance(info, dict) or not isinstance(messages, list) or + info.get("id") != session_id or + info.get("parentID") != session.get("parent_session_id")): + issues.append({"kind": "session-provenance", "session_id": session_id}) + continue + cutoff_id = session.get("cutoff", {}).get("message_id") + cutoff_indexes = [index for index, message in enumerate(messages) + if isinstance(message, dict) and + isinstance(message.get("info"), dict) and + message["info"].get("id") == cutoff_id] + if len(cutoff_indexes) != 1: + issues.append({"kind": "invalid-cutoff", "session_id": session_id, + "message_id": cutoff_id}) + continue + cutoff_index = cutoff_indexes[0] + marker = session.get("cutoff", {}).get("marker") + if marker and marker not in text(messages[cutoff_index].get("parts")): + issues.append({"kind": "cutoff-marker-mismatch", "session_id": session_id, + "message_id": cutoff_id, "marker": marker}) + continue + if session.get("root_relationship") == "descendant": + parent = session_specs.get(session.get("parent_session_id")) + provenance = session.get("provenance", {}) + terminal_state = provenance.get("terminal_state") + if parent is None or terminal_state not in ("completed", "failed", "error", "cancelled", "interrupted"): + issues.append({"kind": "missing-parent-task", "session_id": session_id}) + continue + parent_path = Path(parent.get("export_path", "")) + if not parent_path.is_absolute(): + parent_path = manifest_path.parent / parent_path + if not parent_path.is_file(): + issues.append({"kind": "missing-parent-export", "session_id": session_id}) + continue + parent_raw = parent_path.read_bytes() + if hashlib.sha256(parent_raw).hexdigest() != parent.get("export_sha256"): + issues.append({"kind": "changed-parent-export", "session_id": session_id}) + continue + try: + parent_export = json.loads(parent_raw, parse_float=Decimal) + except (UnicodeDecodeError, json.JSONDecodeError): + issues.append({"kind": "malformed-parent-export", "session_id": session_id}) + continue + parent_messages = parent_export.get("messages", []) + parent_message_id = provenance.get("parent_message_id") + parent_matches = [ + (index, message) for index, message in enumerate(parent_messages) + if isinstance(message, dict) and isinstance(message.get("info"), dict) and + message["info"].get("id") == parent_message_id + ] + parent_cutoff_id = parent.get("cutoff", {}).get("message_id") + parent_cutoffs = [ + index for index, message in enumerate(parent_messages) + if isinstance(message, dict) and isinstance(message.get("info"), dict) and + message["info"].get("id") == parent_cutoff_id + ] + if len(parent_matches) != 1 or len(parent_cutoffs) != 1 or parent_matches[0][0] > parent_cutoffs[0]: + issues.append({"kind": "parent-task-boundary", "session_id": session_id}) + continue + task_parts = [ + part for part in parent_matches[0][1].get("parts", []) + if isinstance(part, dict) and part.get("type") == "tool" and + part.get("tool") == "task" and part.get("id") == provenance.get("task_part_id") + ] + if len(task_parts) != 1: + issues.append({"kind": "parent-task-part", "session_id": session_id}) + continue + task = task_parts[0] + state = task.get("state", {}) + if (task.get("callID") != provenance.get("call_id") or + state.get("status") != terminal_state or + state.get("metadata", {}).get("sessionId") != session_id): + issues.append({"kind": "parent-task-provenance", "session_id": session_id}) + continue + + active_phase = "unattributed" + fixed_phase = session.get("phase") + aggregate = {"input": 0, "output": 0, "reasoning": 0, + "cache_read": 0, "cache_write": 0, "cost": Decimal("0")} + for message in messages[:cutoff_index + 1]: + message_info = message.get("info", {}) if isinstance(message, dict) else {} + if message_info.get("role") != "assistant": + continue + message_id = message_info.get("id") + provider_id = message_info.get("providerID") + model_id = message_info.get("modelID") + tokens = message_info.get("tokens") + phase = active_phase if fixed_phase == "from-markers" else fixed_phase + if (not all(isinstance(value, str) and value for value in + (message_id, provider_id, model_id)) or not isinstance(tokens, dict)): + issues.append({"kind": "message-schema", "session_id": session_id, + "message_id": message_id}) + else: + cache = tokens.get("cache", {}) + if not isinstance(cache, dict): + cache = {} + values = { + "input_tokens": integer(tokens.get("input"), "tokens.input", session_id, message_id), + "cache_read_tokens": integer(cache.get("read", 0), "tokens.cache.read", session_id, message_id), + "cache_write_tokens": integer(cache.get("write", 0), "tokens.cache.write", session_id, message_id), + "output_tokens": integer(tokens.get("output"), "tokens.output", session_id, message_id), + "reasoning_tokens": integer(tokens.get("reasoning", 0), "tokens.reasoning", session_id, message_id) + } + cost = money(message_info.get("cost"), session_id, message_id) + if all(value is not None for value in values.values()) and cost is not None: + event = {"message_id": message_id, "session_id": session_id, + "phase": phase, "provider_id": provider_id, + "model_id": model_id, "cost_usd": cost, **values} + fingerprint = tuple(event[key] for key in ( + "session_id", "phase", "provider_id", "model_id", "input_tokens", + "cache_read_tokens", "cache_write_tokens", "output_tokens", + "reasoning_tokens", "cost_usd")) + if message_id in events: + duplicates.append({"kind": "stable-message-id", "message_id": message_id, + "first_session_id": events[message_id]["session_id"], + "duplicate_session_id": session_id}) + if events[message_id]["fingerprint"] != fingerprint: + issues.append({"kind": "disputed-message", "message_id": message_id, + "first_session_id": events[message_id]["session_id"], + "second_session_id": session_id}) + else: + event["fingerprint"] = fingerprint + events[message_id] = event + for source, target in (("input_tokens", "input"), + ("cache_read_tokens", "cache_read"), + ("cache_write_tokens", "cache_write"), + ("output_tokens", "output"), + ("reasoning_tokens", "reasoning")): + aggregate[target] += event[source] + aggregate["cost"] += cost + if fixed_phase == "from-markers": + body = text(message.get("parts", [])) + for marker_name, next_phase in markers.items(): + if marker_name in body: + active_phase = next_phase + session_sums[session_id] = aggregate + + if session.get("aggregate_reconciliation") == "full-export": + reported_tokens = info.get("tokens", {}) + reported_cache = reported_tokens.get("cache", {}) if isinstance(reported_tokens, dict) else {} + observed = { + "input": reported_tokens.get("input") if isinstance(reported_tokens, dict) else None, + "output": reported_tokens.get("output") if isinstance(reported_tokens, dict) else None, + "reasoning": reported_tokens.get("reasoning") if isinstance(reported_tokens, dict) else None, + "cache_read": reported_cache.get("read") if isinstance(reported_cache, dict) else None, + "cache_write": reported_cache.get("write") if isinstance(reported_cache, dict) else None, + "cost": Decimal(str(info.get("cost"))) if isinstance(info.get("cost"), (int, float, Decimal)) and not isinstance(info.get("cost"), bool) else None + } + if any(observed[key] != aggregate[key] for key in aggregate): + issues.append({"kind": "session-aggregate-mismatch", "session_id": session_id, + "message_sum": {key: str(value) for key, value in aggregate.items()}, + "reported": {key: str(value) for key, value in observed.items()}}) + +for message_id, event in sorted(events.items()): + if event["phase"] not in included_phases: + continue + key = (event["phase"], event["session_id"], event["provider_id"], event["model_id"]) + row = rows[key] + for field in ("input_tokens", "cache_read_tokens", "cache_write_tokens", + "output_tokens", "reasoning_tokens"): + row[field] += event[field] + row["message_ids"].append(message_id) + if event["cost_usd"] == 0 and (event["provider_id"], event["model_id"]) not in zero_authorized: + row["priced"] = False + issues.append({"kind": "unpriced-zero", "message_id": message_id, + "provider_id": event["provider_id"], "model_id": event["model_id"]}) + else: + row["cost_usd"] += event["cost_usd"] + +flat = [] +for (phase, session_id, provider_id, model_id), row in sorted(rows.items()): + row.update({"phase": phase, "session_id": session_id, + "provider_id": provider_id, "model_id": model_id}) + row["cost_usd"] = str(row["cost_usd"]) if row["priced"] else None + flat.append(row) +total = None if issues else str(sum(Decimal(row["cost_usd"]) for row in flat)) +print(json.dumps({"rows": flat, "duplicates": duplicates, "issues": issues, + "total_cost_usd": total}, indent=2)) +PY +``` + +## 3. Independent Verification And Output + +Give both workers the same immutable manifest, exports, cost-basis file, and +this prompt: + +```text +You are an OpenCode cost-calculation worker. Use only the frozen manifest and validated exports. De-duplicate assistant messages by info.id, total provider/model token fields, and replay exact per-message cost. Do not discover sessions, add session aggregates, invent rates or cache TTLs, or accept an unexplained zero as free. Flag exact disputed messages and malformed evidence. +``` + +Preserve results as `cost-opencode-pass-a.json` and +`cost-opencode-pass-b.json` (or `*-operationalized-*`). Compare them exactly +after normalizing row order. Investigate every mismatch by session, message ID, +provider/model, and field. Any mismatch remains `Unreconciled`. + +Read `../templates/cost-estimate-template.md` immediately before writing +`controls/cost-estimate.md`. Link the manifest, validated exports, cost-basis +file, and both results. Show phase/session/provider/model tokens and the exact +recorded-cost basis. State that reasoning is separate and cache-write TTL is not +reported. Publish a total only when all included messages reconcile and are +priced. Display `$X.XX` while preserving exact decimals in JSON evidence. + +Always state that this replays OpenCode's API-equivalent estimate and is not a +provider invoice. List export availability/validity, OpenCode version, provider +rate drift, custom or subscription pricing, local compute/electricity, +unexplained zero costs, and non-token charges as limitations. See +`../fixtures/cost-estimation-opencode/README.md` for the dry run. diff --git a/skills/wgo/references/common/cost-estimation.md b/skills/wgo/references/common/cost-estimation.md new file mode 100644 index 0000000..24e4af3 --- /dev/null +++ b/skills/wgo/references/common/cost-estimation.md @@ -0,0 +1,618 @@ +# Codex Cost Estimation Workflow + +Use only when the audit platform recorded in `audit-brief.md` is Codex. Use for +`wgo:cost`, at the end of `wgo:summarize`, and after a requested +`wgo:operationalize`. This is a portable, API-equivalent estimate of the +completed work's recorded model requests. It is not a Codex invoice. + +Terra at high reasoning is the coordinator and final decision-maker. Two +independent Terra workers at high reasoning only extract and verify usage from +the coordinator's already frozen manifest. Do not +create a worker per audit session, install a package, invoke `ccusage`, or add +a persistent helper program. + +## 1. Discover And Freeze The Audit Manifest + +For audit-only coverage, before opening a calculator or delegating work, Terra +finds the current audit's root Codex session transcript among the accessible +session JSONL roots. Use the runtime's current root-session metadata and inspect +the JSONL headers/records to find its matching session ID and path. For an +operationalization refresh, use the preserved audit-only manifest plus the +current operationalization root as described below. If either required root +cannot be established from accessible records, write an `Unreconciled` cost +control with that exact limit; do not estimate an assumed session set. + +Scan accessible JSONL files, not a date, CWD, project folder, or model filter. +Those attributes may corroborate a result but never establish membership. From +the root transcript, recursively follow only recorded Codex/WGO collaboration +spawn links and corresponding task-lifecycle records. For every candidate, +resolve its session ID to its own JSONL record and require provenance that ties +the spawned task to its parent and WGO role/task name. A missing child file, +missing lifecycle correlation, ambiguous parent, or unrelated later reuse is a +manifest exclusion with its exact reason and a reconciliation issue. + +Use the exact completion marker emitted immediately before this cost phase: + +- audit-only: `WGO_AUDIT_COMPLETE_COST_PHASE_STARTS`; +- through operationalization: + `WGO_OPERATIONALIZATION_COMPLETE_COST_PHASE_STARTS`. + +Locate the recorded response/message line containing the applicable marker, +record its line number in the manifest, and freeze through that line. For +standalone `wgo:cost`, use the latest applicable existing completion marker; +if none exists, emit the audit-only marker before calculation. The root prefix +may contain an earlier cost phase when operationalization follows synthesis; +phase attribution below excludes its requests. For every descendant, record +both boundaries of its WGO task lifecycle: the `task_started` line and exactly +one terminal `task_complete`, `task_failed`, `task_cancelled`, or +`task_interrupted` line. Parse only that inclusive interval. Do not parse +inherited history before `task_started`; when a session later does unrelated +work, stop at its terminal audit-task event. Record the exact child-specific +`session_meta` line separately and require its session ID to match the +manifest. Never select whichever metadata record happens to appear last. Use +recorded lifecycle markers and line positions (plus stable record IDs when +present), not a timestamp alone. Hash the exact byte prefix through each +cutoff, including its terminating newline when present. Do not hash the whole +JSONL: later reuse may append records, but any mutation before the frozen +cutoff is unreconciled. + +The audit coordinator must already have passed the delegated-task lifecycle +gate before synthesis: every recursively spawned WGO task has exactly one +recorded terminal outcome. An open, multiply terminated, or ambiguously +correlated task blocks synthesis; it is not silently dropped from the cost +manifest. + +Before calculation, freeze `/controls/cost-manifest.json` for the +audit-only closeout or +`/controls/cost-manifest-operationalized.json` for the refresh. +Each is an immutable input for its calculation; never revise the audit-only +file or either manifest to make worker results match. The refreshed manifest +must list delegated earlier cost sessions as explicit exclusions and exclude +same-session cost requests through its phase policy. It must contain, at +minimum: + +```json +{ + "schema_version": 2, + "coverage": "audit", + "root_session_id": "ses_root", + "session_search_roots": ["/accessible/codex/sessions"], + "pricing_basis": { + "rate_card": "references/data/api-rate-card-2026-08-07.json", + "default_service_tier_when_not_returned": "standard", + "note": "Declared API-equivalent basis, not actual Codex backend tier." + }, + "phase_policy": { + "included": ["unattributed", "onboarding", "audit", "summary"], + "excluded": ["cost-estimation"], + "markers": { + "WGO_PHASE_ONBOARDING_START": "onboarding", + "WGO_PHASE_AUDIT_START": "audit", + "WGO_PHASE_SUMMARY_START": "summary", + "WGO_AUDIT_COMPLETE_COST_PHASE_STARTS": "cost-estimation", + "WGO_PHASE_OPERATIONALIZATION_START": "operationalization", + "WGO_OPERATIONALIZATION_COMPLETE_COST_PHASE_STARTS": "cost-estimation" + } + }, + "phase_boundaries": [ + {"phase": "summary", "marker": "WGO_PHASE_SUMMARY_START", "session_id": "ses_root", "line_number": 70}, + {"phase": "cost-estimation", "marker": "WGO_AUDIT_COMPLETE_COST_PHASE_STARTS", "session_id": "ses_root", "line_number": 81} + ], + "root_cutoff": {"marker": "WGO_AUDIT_COMPLETE_COST_PHASE_STARTS", "line_number": 81}, + "sessions": [ + { + "session_id": "ses_root", + "file_path": "/accessible/codex/sessions/root.jsonl", + "prefix_sha256": "...", + "wgo_role_task_name": "Terra audit coordinator", + "phase": "from-markers", + "parent_session_id": null, + "root_relationship": "root", + "decision": "included", + "rationale": "Current audit root through explicit cost-phase cutoff.", + "session_meta": {"line_number": 1, "session_id": "ses_root"}, + "lifecycle": null, + "cutoff": {"marker": "WGO_AUDIT_COMPLETE_COST_PHASE_STARTS", "line_number": 81}, + "provenance": [{"kind": "session_meta"}, {"kind": "sub_agent_activity"}], + "usage_schema": "codex-rollout-token-count-v1" + }, + { + "session_id": "ses_child", + "file_path": "/accessible/codex/sessions/child.jsonl", + "prefix_sha256": "...", + "wgo_role_task_name": "wgo reviewer: code-quality", + "phase": "audit", + "parent_session_id": "ses_root", + "root_relationship": "descendant", + "decision": "included", + "rationale": "Recorded spawn, matching child metadata, and one bounded lifecycle.", + "session_meta": {"line_number": 64, "session_id": "ses_child"}, + "lifecycle": { + "task_id": "task_code_quality", + "start": {"event": "task_started", "line_number": 65}, + "terminal": {"event": "task_complete", "outcome": "completed", "line_number": 92} + }, + "cutoff": {"provenance": "task_complete", "line_number": 92}, + "provenance": [{"kind": "sub_agent_activity"}, {"kind": "thread_spawn"}, {"kind": "task_started"}, {"kind": "task_complete"}], + "usage_schema": "codex-rollout-token-count-v1" + } + ], + "exclusions": [ + { + "session_id": "ses_unrelated", + "file_path": "/accessible/codex/sessions/unrelated.jsonl", + "decision": "excluded", + "rationale": "No recorded spawn and task-lifecycle path from ses_root." + } + ] +} +``` + +Record every included and excluded session, including: session ID and file +path; WGO role/task name; parent/root relationship; inclusion/exclusion +decision and rationale; spawn/lifecycle evidence; exact child-specific +`session_meta` line; descendant lifecycle start and terminal boundaries; exact +cutoff; SHA-256 of the exact cutoff byte prefix; and the inspected usage schema. The +schema makes the result reproducible when Codex JSONL schemas differ. Also +record every observed phase marker and line. Give each descendant its fixed +phase from the recorded parent spawn/task lifecycle; use `from-markers` only +for a session whose own markers are scanned. For operationalized coverage, add +`operationalization` to `phase_policy.included` and explicitly exclude every +earlier cost coordinator/worker session. Do not treat an uninspected field name +as a known schema. + +For an operationalization refresh, use the immutable audit-only manifest as the +frozen audit baseline. Record its path and SHA-256 in `prior_manifest`, verify +that file and every original cutoff prefix, then add only the current +operationalization root/session and its recorded descendants. If +operationalization continues in the audit root session, extend that session's +cutoff only in the new manifest and use phase exclusion for the earlier cost +requests. If it runs in a later Codex session, keep the audit root and add the +current session as `operationalization_root_session_id`; the explicit +`wgo:operationalize` invocation against that resolved audit root plus its phase +and completion markers establish the follow-on relationship. Never use date, +CWD, folder, or model alone, and never rediscover or change the audit-only +membership during the refresh. + +## 2. Calculate Request-Level Usage + +For accessible Codex rollout JSONL, take model and active turn ID from the most +recent `turn_context` record, then take a request-level usage record only from +an `event_msg` whose payload type is `token_count`, at +`payload.info.last_token_usage`. Never add `payload.info.total_token_usage`: +it is cumulative and forked agents can inherit parent context. + +Use root `event_msg` `sub_agent_activity` records (`event_id`, +`agent_thread_id`, `agent_path`, `kind: started`) together with the child +`session_meta.payload.source.subagent.thread_spawn` and child `event_msg` +`task_started` plus terminal records to establish provenance. If a raw +token-count record carries a stable request/event ID, deduplicate it globally +and retain duplicate locations. If legacy `token_count` has no stable ID, +treat `last_token_usage` as request state: within the same session, turn, +model, tier, and context band, count a changed state once and classify +consecutive identical states as unchanged echoes of the first line. Give each +counted state the deterministic identity +`legacy-state:::`. +Disclose that this is a schema-aware fallback, not a provider request ID; it +cannot distinguish two genuinely separate legacy requests with identical +usage state. A missing active turn ID is a schema ambiguity. If the same stable +ID has non-identical usage or model fields, flag those exact records as +disputed rather than choosing one. + +For a session marked `from-markers`, scan assistant `response_item` text in +line order and change its active phase when an exact `phase_policy.markers` +value appears. A marker applies only to later request events. For a descendant, +use its manifest `phase` derived from the parent spawn/task lifecycle. Include +only `phase_policy.included`; record every request in an excluded phase with +its session, line, request ID, phase, and rationale. Never include +`cost-estimation` in the priced audit total. Retain `unattributed` for legacy +or pre-marker requests instead of guessing from a timestamp or command name. + +For each accepted request, total separately: + +- uncached input = `input_tokens - cached_input_tokens`; +- cached input; +- output; and +- reasoning, as informational output subcomponent. + +Reject any negative input, cached input, output, reasoning, or cache-write +field; also reject cached input greater than total input. Reasoning tokens are +already part of output for this estimate. Show them, but never add them again +to billable output. + +Use the checked-in dated rate card +`references/data/api-rate-card-2026-08-07.json`. It records the official +OpenAI API basis in USD per one million tokens, service tier, and context band. +Use an actual returned tier (`default` normalizes to `standard`; `priority` +normalizes to `fast`) when present. Codex rollout JSONL often has no returned +tier, so then use the frozen manifest/rate-card declared `standard` tier. Label +that as an API-equivalent declared basis, not the actual backend tier. + +Use an actual returned context band when present. Otherwise derive `long` only +when that request's recorded `input_tokens` is greater than the rate card's +official 272,000-token threshold; use `short` at or below it. An unknown model +or rate is `unpriced`, never zero. The required formula excludes cache-write +charges: when raw usage reports nonzero cache-write tokens, list their exact +session/event/token count as a limitation rather than silently charging or +ignoring them. Do not infer regional uplift, tool charges, or other surcharges. + +For a priced request, calculate only: + +```text +uncached input × input rate + cached input × cached-input rate + output × output rate +``` + +## Portable One-Off Recipe + +Run this inline Python standard-library recipe once per verification pass from any +machine where the frozen manifest, its referenced JSONL files, and the rate +card are accessible. It writes JSON to stdout and creates no helper file. The +manifest must first record the inspected Codex rollout `token_count` schema. + +```sh +python3 - /absolute/path/to/cost-manifest.json /absolute/path/to/api-rate-card-2026-08-07.json <<'PY' +import hashlib, json, sys +from collections import defaultdict +from decimal import Decimal +from pathlib import Path + +manifest_path = Path(sys.argv[1]).resolve() +manifest = json.loads(manifest_path.read_text(), parse_float=Decimal) +rates = json.loads(Path(sys.argv[2]).read_text(), parse_float=Decimal) +issues, limitations, seen, legacy_states = [], [], {}, {} +duplicates, excluded_requests = [], [] +rows = defaultdict(lambda: { + "uncached_input_tokens": 0, "cached_input_tokens": 0, + "output_tokens": 0, "reasoning_tokens": 0, "cost_usd": Decimal("0"), + "priced": True, "rate_per_million": None, "request_ids": [], "tier_sources": set(), + "context_band_sources": set() +}) +long_threshold = int(rates["context_band_rule"]["long_context_input_tokens_gt"]) +declared_tier = manifest["pricing_basis"]["default_service_tier_when_not_returned"] +phase_policy = manifest.get("phase_policy", {}) +included_phases = set(phase_policy.get("included", ["unattributed"])) +phase_markers = phase_policy.get("markers", {}) + +def number(value, label, session_id, line_number): + if isinstance(value, bool) or not isinstance(value, (int, float, Decimal)): + issues.append({"kind": "schema", "session_id": session_id, + "line_number": line_number, "field": label}) + return None + parsed = Decimal(str(value)) + if parsed != parsed.to_integral_value() or parsed < 0: + issues.append({"kind": "schema", "session_id": session_id, + "line_number": line_number, "field": label}) + return None + return int(parsed) + +def optional_text(*values): + return next((value for value in values if isinstance(value, str) and value), None) + +def assistant_message_text(record): + payload = record.get("payload", {}) + if (record.get("type") != "response_item" or not isinstance(payload, dict) or + payload.get("type") != "message" or payload.get("role") != "assistant"): + return "" + content = payload.get("content", []) + if not isinstance(content, list): + return "" + return "\n".join(item.get("text", "") for item in content + if isinstance(item, dict) and isinstance(item.get("text"), str)) + +def decode_record(raw, session_id, line_number): + try: + return json.loads(raw.decode("utf-8")) + except (UnicodeDecodeError, json.JSONDecodeError): + issues.append({"kind": "invalid-json", "session_id": session_id, + "line_number": line_number}) + return None + +def lifecycle_event(record): + if not isinstance(record, dict) or record.get("type") != "event_msg": + return None + payload = record.get("payload", {}) + return payload.get("type") if isinstance(payload, dict) else None + +prior_manifest = manifest.get("prior_manifest") +if isinstance(prior_manifest, dict): + prior_path = Path(prior_manifest.get("file_path", "")) + if not prior_path.is_absolute(): + prior_path = manifest_path.parent / prior_path + if not prior_path.is_file(): + issues.append({"kind": "missing-prior-manifest", "file_path": str(prior_path)}) + else: + prior_digest = hashlib.sha256(prior_path.read_bytes()).hexdigest() + if prior_digest != prior_manifest.get("sha256"): + issues.append({"kind": "changed-prior-manifest", "file_path": str(prior_path), + "expected_sha256": prior_manifest.get("sha256"), + "actual_sha256": prior_digest}) + +for session in manifest["sessions"]: + if session["decision"] != "included": + continue + path = Path(session["file_path"]) + if not path.is_absolute(): + path = manifest_path.parent / path + session_id, cutoff = session["session_id"], session["cutoff"] + if not path.is_file(): + issues.append({"kind": "missing-file", "session_id": session_id, "file_path": str(path)}) + continue + byte_lines = path.read_bytes().splitlines(keepends=True) + end_line = cutoff.get("line_number") + if not isinstance(end_line, int) or end_line < 1 or end_line > len(byte_lines): + issues.append({"kind": "invalid-cutoff", "session_id": session_id}) + continue + prefix = b"".join(byte_lines[:end_line]) + digest = hashlib.sha256(prefix).hexdigest() + if digest != session["prefix_sha256"]: + issues.append({"kind": "changed-prefix", "session_id": session_id, + "expected_sha256": session["prefix_sha256"], "actual_sha256": digest}) + continue + marker = cutoff.get("marker") + if marker and marker not in byte_lines[end_line - 1].decode("utf-8", errors="replace"): + issues.append({"kind": "cutoff-marker-mismatch", "session_id": session_id, + "line_number": end_line, "marker": marker}) + continue + + meta_spec = session.get("session_meta", {}) + meta_line = meta_spec.get("line_number") if isinstance(meta_spec, dict) else None + if not isinstance(meta_line, int) or meta_line < 1 or meta_line > end_line: + issues.append({"kind": "invalid-session-meta-boundary", "session_id": session_id}) + continue + meta_record = decode_record(byte_lines[meta_line - 1], session_id, meta_line) + meta_payload = meta_record.get("payload", {}) if isinstance(meta_record, dict) else {} + observed_meta_ids = {value for value in (meta_payload.get("id"), meta_payload.get("session_id")) + if isinstance(value, str) and value} + if (not isinstance(meta_record, dict) or meta_record.get("type") != "session_meta" or + not observed_meta_ids or observed_meta_ids != {session_id} or + meta_spec.get("session_id") != session_id): + issues.append({"kind": "session-meta-mismatch", "session_id": session_id, + "line_number": meta_line, "observed_session_ids": sorted(observed_meta_ids)}) + continue + + start_line = 1 + if session.get("root_relationship") == "descendant": + lifecycle = session.get("lifecycle", {}) + start = lifecycle.get("start", {}) if isinstance(lifecycle, dict) else {} + terminal = lifecycle.get("terminal", {}) if isinstance(lifecycle, dict) else {} + start_line, terminal_line = start.get("line_number"), terminal.get("line_number") + if (not isinstance(start_line, int) or not isinstance(terminal_line, int) or + start_line < 1 or start_line > terminal_line or terminal_line != end_line): + issues.append({"kind": "invalid-lifecycle-interval", "session_id": session_id}) + continue + start_record = decode_record(byte_lines[start_line - 1], session_id, start_line) + terminal_record = decode_record(byte_lines[terminal_line - 1], session_id, terminal_line) + start_event, terminal_event = lifecycle_event(start_record), lifecycle_event(terminal_record) + allowed_terminal_events = {"task_complete", "task_failed", "task_cancelled", "task_interrupted"} + terminal_outcomes = {"task_complete": "completed", "task_failed": "failed", + "task_cancelled": "cancelled", "task_interrupted": "interrupted"} + if start_event != "task_started" or start.get("event") != start_event: + issues.append({"kind": "lifecycle-start-mismatch", "session_id": session_id, + "line_number": start_line, "observed_event": start_event}) + continue + if (terminal_event not in allowed_terminal_events or + terminal.get("event") != terminal_event or + terminal.get("outcome") != terminal_outcomes.get(terminal_event)): + issues.append({"kind": "lifecycle-terminal-mismatch", "session_id": session_id, + "line_number": terminal_line, "observed_event": terminal_event}) + continue + task_id = lifecycle.get("task_id") + start_payload = start_record.get("payload", {}) + terminal_payload = terminal_record.get("payload", {}) + if (not isinstance(task_id, str) or not task_id or + start_payload.get("task_id") != task_id or terminal_payload.get("task_id") != task_id): + issues.append({"kind": "lifecycle-task-mismatch", "session_id": session_id, + "task_id": task_id}) + continue + task_events = [] + for candidate_line in range(start_line, terminal_line + 1): + try: + candidate = json.loads(byte_lines[candidate_line - 1].decode("utf-8")) + except (UnicodeDecodeError, json.JSONDecodeError): + continue + candidate_payload = candidate.get("payload", {}) + candidate_event = lifecycle_event(candidate) + if (isinstance(candidate_payload, dict) and candidate_payload.get("task_id") == task_id and + candidate_event in allowed_terminal_events | {"task_started"}): + task_events.append({"event": candidate_event, "line_number": candidate_line}) + if task_events != [{"event": "task_started", "line_number": start_line}, + {"event": terminal_event, "line_number": terminal_line}]: + issues.append({"kind": "lifecycle-event-count", "session_id": session_id, + "task_id": task_id, "events": task_events}) + continue + + active_turn = {} + active_phase = session.get("phase", "unattributed") + if active_phase == "from-markers": + active_phase = "unattributed" + for line_number in range(start_line, end_line + 1): + record = decode_record(byte_lines[line_number - 1], session_id, line_number) + if record is None: + continue + payload = record.get("payload", {}) + message_text = assistant_message_text(record) + if message_text: + for phase_marker, marker_phase in phase_markers.items(): + if phase_marker in message_text: + active_phase = marker_phase + continue + if record.get("type") == "turn_context" and isinstance(payload, dict): + active_turn = { + "turn_id": payload.get("turn_id"), "model": payload.get("model"), + "service_tier": payload.get("service_tier"), + "context_band": payload.get("context_band") + } + continue + if record.get("type") != "event_msg" or not isinstance(payload, dict) or payload.get("type") != "token_count": + continue + info = payload.get("info", {}) + usage = info.get("last_token_usage", {}) if isinstance(info, dict) else {} + if not isinstance(usage, dict): + issues.append({"kind": "schema", "session_id": session_id, "line_number": line_number, + "field": "payload.info.last_token_usage"}) + continue + inp = number(usage.get("input_tokens"), "input_tokens", session_id, line_number) + cached = number(usage.get("cached_input_tokens"), "cached_input_tokens", session_id, line_number) + output = number(usage.get("output_tokens"), "output_tokens", session_id, line_number) + reasoning = number(usage.get("reasoning_output_tokens", 0), "reasoning_output_tokens", session_id, line_number) + cache_write = number(usage.get("cache_write_input_tokens", 0), "cache_write_input_tokens", session_id, line_number) + turn_id, model = active_turn.get("turn_id"), active_turn.get("model") + if None in (inp, cached, output, reasoning, cache_write) or cached > inp or not isinstance(turn_id, str) or not turn_id or not isinstance(model, str) or not model: + issues.append({"kind": "incomplete-usage", "session_id": session_id, "line_number": line_number}) + continue + raw_id = optional_text(usage.get("request_id"), info.get("request_id"), + payload.get("request_id"), usage.get("event_id"), + info.get("event_id"), payload.get("event_id")) + tier = optional_text(usage.get("service_tier"), info.get("service_tier"), active_turn.get("service_tier")) + tier_source = "returned" if tier else "declared-api-equivalent" + tier = {"default": "standard", "priority": "fast"}.get(tier, tier or declared_tier) + band = optional_text(usage.get("context_band"), info.get("context_band"), active_turn.get("context_band")) + band_source = "returned" if band else "derived-input-threshold" + band = band or ("long" if inp > long_threshold else "short") + fingerprint = (model, inp, cached, output, reasoning, cache_write, tier, band) + if raw_id: + request_id = raw_id + legacy_states.pop((session_id, turn_id, model, tier, band), None) + else: + legacy_key = (session_id, turn_id, model, tier, band) + previous_state = legacy_states.get(legacy_key) + if previous_state and previous_state["fingerprint"] == fingerprint: + request_id = previous_state["request_id"] + duplicates.append({"kind": "unchanged-legacy-state", "request_id": request_id, + "first": previous_state["location"], + "duplicate": {"session_id": session_id, + "line_number": line_number}}) + continue + request_id = f"legacy-state:{session_id}:{turn_id}:{line_number}" + legacy_states[legacy_key] = { + "fingerprint": fingerprint, + "request_id": request_id, + "location": {"session_id": session_id, "line_number": line_number} + } + if request_id in seen: + duplicates.append({"kind": "stable-id", "request_id": request_id, + "first": seen[request_id]["location"], + "duplicate": {"session_id": session_id, "line_number": line_number}}) + if seen[request_id]["fingerprint"] != fingerprint: + issues.append({"kind": "disputed-request", "request_id": request_id, + "first": seen[request_id]["location"], + "second": {"session_id": session_id, "line_number": line_number}}) + continue + seen[request_id] = {"fingerprint": fingerprint, + "location": {"session_id": session_id, "line_number": line_number}} + if active_phase not in included_phases: + excluded_requests.append({"request_id": request_id, "session_id": session_id, + "line_number": line_number, "phase": active_phase, + "rationale": "Phase excluded by frozen manifest."}) + continue + if not raw_id: + limitations.append({"kind": "legacy-state-identity", "request_id": request_id, + "session_id": session_id, "line_number": line_number, + "turn_id": turn_id}) + if cache_write: + limitations.append({"kind": "cache-write-input-excluded-by-formula", "request_id": request_id, + "session_id": session_id, "line_number": line_number, + "cache_write_input_tokens": cache_write}) + bucket = rows[(active_phase, session_id, model, tier, band)] + bucket["uncached_input_tokens"] += inp - cached + bucket["cached_input_tokens"] += cached + bucket["output_tokens"] += output + bucket["reasoning_tokens"] += reasoning + bucket["request_ids"].append(request_id) + bucket["tier_sources"].add(tier_source) + bucket["context_band_sources"].add(band_source) + try: + rate = rates["models"][model][tier][band] + except KeyError: + bucket["priced"] = False + issues.append({"kind": "unpriced", "request_id": request_id, "model": model, + "service_tier": tier, "context_band": band}) + continue + bucket["rate_per_million"] = {name: str(value) for name, value in rate.items()} + bucket["cost_usd"] += (Decimal(inp - cached) * rate["input"] + + Decimal(cached) * rate["cached_input"] + + Decimal(output) * rate["output"]) / Decimal(1000000) + +flat_rows = [] +for (phase, session_id, model, tier, band), row in sorted(rows.items()): + row.update({"phase": phase, "session_id": session_id, "model": model, "service_tier": tier, + "context_band": band, "tier_sources": sorted(row["tier_sources"]), + "context_band_sources": sorted(row["context_band_sources"])}) + row["cost_usd"] = str(row["cost_usd"]) if row["priced"] else None + flat_rows.append(row) +total = None if issues else str(sum(Decimal(row["cost_usd"]) for row in flat_rows)) +print(json.dumps({"rows": flat_rows, "excluded_requests": excluded_requests, + "duplicates": duplicates, "issues": issues, + "limitations": limitations, "total_cost_usd": total}, indent=2, default=str)) +PY +``` + +The coordinator supplies the same immutable manifest and rate-card path to each +pass. For audit-only coverage, preserve the raw results as +`cost-terra-pass-a.json` and `cost-terra-pass-b.json`. For operationalized +coverage, use `cost-terra-operationalized-pass-a.json` and +`cost-terra-operationalized-pass-b.json`. Keep them beside their manifest; they +are calculation evidence, not substitutes for the readable control. + +## 3. Independent Terra Verification + +Launch exactly two independent `gpt-5.6-terra` workers at high reasoning after +the manifest is frozen. They +receive the same frozen manifest, referenced read-only JSONL files, rate-card +path, and the following prompt verbatim: + +```text +You are a cost-calculation worker. Use only the frozen manifest supplied by the Terra coordinator. Parse only each manifest lifecycle interval and its exact matching session metadata. At request/event level, de-duplicate stable request IDs and suppress consecutive unchanged legacy last_token_usage echoes. Return a machine-checkable table of tokens by session and model. Do not infer missing records, include sessions or lines outside the manifest boundaries, or sum cumulative session totals. Flag any schema ambiguity or missing pricing input rather than guessing. +``` + +Each worker returns the JSON table from the one-off recipe (or an equivalent +standard-library parser) with request IDs, duplicate locations, totals by +session/model/tier/context band, and flags. They do not discover more sessions, +change the manifest, decide eligibility, or price an absent rate. + +Terra compares both tables exactly after normalizing table order. Investigate +every mismatch by naming the session ID, request/event ID, JSONL line, field, +and worker values. Do not calculate a final total while any mismatch remains. + +## 4. Write The Cost Control + +Read `../templates/cost-estimate-template.md` immediately before writing +`/controls/cost-estimate.md` and follow it exactly. + +Identify the pricing basis, source URL, rate-card date, currency, recorded +service tiers/context bands, coverage (`audit` or +`audit-and-operationalization`), and the exact formula. List the full manifest, +phase boundaries, excluded phases/requests, and session exclusions with their +rationales; link or reference the applicable frozen manifest and two Terra +tables. For refreshed coverage, also link the preserved audit-only manifest and +state that `cost-estimate.md` supersedes only the earlier readable estimate. +The token table must show phase, uncached input, cached input, output, and +informational reasoning by session, model, service tier, and context band, plus +phase subtotals. +The cost table must show model, service tier/context band, rate, cost, +`unpriced` status where relevant, and a total only when every included priced +request reconciles. Identify each declared service tier and input-threshold +context band as API-equivalent rather than observed backend billing. List any +nonzero cache-write tokens as excluded-by-formula limitations. + +Keep exact decimal costs in the manifest-linked machine-readable results. In +`cost-estimate.md`, display every priced rate, row, subtotal, and total as +`$X.XX`, rounded half up from its exact value. Round the exact total directly; +never sum already-rounded display rows. Use `unpriced`, not `$0.00`, for a +missing rate. When a positive exact cost rounds to `$0.00`, link the exact +machine-readable value so the display is not mistaken for a zero calculation. + +Set the control's status to one of: + +- `Final` — manifest, phase attribution/exclusions, duplicate handling, two + Terra tables, pricing inputs, and totals reconcile; or +- `Unreconciled` — list each disputed/missing session or event and do not state + a falsely precise total. + +Always state that the result is an API-equivalent estimate, not a Codex invoice, +and that its own calculation requests are excluded. Its limitations include +unavailable or changed session files, missing phase markers, unattributed +requests, JSONL schema ambiguity, unknown model/tier/context/rate, unpriced +non-token charges, and any provider billing or subscription differences. + +See `references/fixtures/cost-estimation/README.md` for the dry-run fixture. diff --git a/skills/wgo/references/common/documentation-prep.md b/skills/wgo/references/common/documentation-prep.md index 95e71c1..b30d8a3 100644 --- a/skills/wgo/references/common/documentation-prep.md +++ b/skills/wgo/references/common/documentation-prep.md @@ -12,7 +12,7 @@ scope, or select reviewers. Use only the active audit platform and its existing session or credentials. Do not request, configure, or call another provider. The onboarding lead supplies -one catalog model: Codex/OpenAI uses `gpt-5.6-luna` at high reasoning, +one catalog model: Codex/OpenAI uses `gpt-5.6-terra` at high reasoning, Claude/Anthropic uses Sonnet 5 at high reasoning, Antigravity/Gemini uses `gemini-3.5-flash-lite`, and another platform uses its active audit model. If a preferred same-platform model is unavailable, use that active audit model. diff --git a/skills/wgo/references/common/onboarding.md b/skills/wgo/references/common/onboarding.md index ed3af17..c84b5fe 100644 --- a/skills/wgo/references/common/onboarding.md +++ b/skills/wgo/references/common/onboarding.md @@ -4,6 +4,10 @@ Use for `wgo:onboard`. Onboarding agrees a bounded audit before it writes anything. Explain that WGO audits the current folder, reads approved sources, and creates evidence-backed outputs without changing the audited system. +Before doing onboarding work, emit this normal progress message once: +`Starting onboarding. `. The HTML comment is +a persisted cost-attribution marker, not audit evidence. + ## Resolve Mode And Audit Root Parse only these forms: @@ -235,7 +239,7 @@ repository URL/ref/resolved-commit/local-root mappings, converter availability, catalog output path, audit mandate, selected reviewer IDs, cheap source-discovery capability signals, active audit platform, and selected catalog model. Select only from that active platform: Codex/OpenAI uses -`gpt-5.6-luna` at high reasoning, Claude/Anthropic uses Sonnet 5 at high +`gpt-5.6-terra` at high reasoning, Claude/Anthropic uses Sonnet 5 at high reasoning, and Antigravity/Gemini uses `gemini-3.5-flash-lite`; another platform uses its active audit model. If the preferred same-platform model is not selectable, use the active audit model. Do not request, configure, or use another provider's credentials or API. diff --git a/skills/wgo/references/common/operationalization.md b/skills/wgo/references/common/operationalization.md index 6871818..49cf30b 100644 --- a/skills/wgo/references/common/operationalization.md +++ b/skills/wgo/references/common/operationalization.md @@ -4,6 +4,10 @@ Use only after a completed or bounded synthesis and the auditor explicitly asks for it. Read the brief, final reports, selected artifacts, and evidence directly. Do not infer a completed synthesis from a status table. +Before doing operationalization work, emit this normal progress message once: +`Starting operationalization. `. The +HTML comment is a persisted cost-attribution marker, not audit evidence. + Before drafting, tell the auditor that WGO will create the required four-part transition packet: `replacement-maintainer`, `recovery`, `observability`, and `iam-and-credential-control`. Also list the optional additions: @@ -50,3 +54,15 @@ parallel runbook library. Update the relevant checklist entry and add an open item only when a future owner, authority decision, or proof is required. + +After the selected operator packet is complete, emit this normal progress +message once: `Operationalization complete; refreshing cost estimate. `. Then rerun the complete +cost-estimation workflow selected from the platform in `audit-brief.md`: +`cost-estimation.md` for Codex, `cost-estimation-claude.md` for Claude, or +`cost-estimation-opencode.md` for OpenCode, with coverage through +operationalization. Update `controls/cost-estimate.md`, but preserve the frozen +audit-only manifest and verification outputs. Exclude both cost-calculation +phases from the estimate itself. If the refreshed result is unreconciled, +publish that status and its exact limitations rather than retaining the earlier +precise total as though it included operationalization. diff --git a/skills/wgo/references/common/reviewer-audit.md b/skills/wgo/references/common/reviewer-audit.md index 11bce00..3f52b7a 100644 --- a/skills/wgo/references/common/reviewer-audit.md +++ b/skills/wgo/references/common/reviewer-audit.md @@ -3,6 +3,11 @@ Use for `wgo:audit `. A reviewer is a bounded evidence-to-artifact loop, not a parallel state system. +Before doing reviewer or audit-coordination work for this invocation, emit this +normal progress message once: `Starting reviewer execution. `. The HTML comment is a persisted cost-attribution +marker, not audit evidence. + ## Inputs And Output Selection Read `audit-brief.md`, the reviewer card, its relevant predecessor handoffs, @@ -113,6 +118,14 @@ Do not create an insight to fill a count. Combine facts that change the same decision; retain every independent material decision rather than imposing a maximum. +Classify every report finding by consequence-based severity (`High`, `Medium`, +or `Low`) and smallest credible corrective effort (`S`, `M`, or `L`). Use the +confidence/limitation column to bound either classification. Severity is impact, +effort is size, and open-item priority remains urgency within its next-move lane. +`High` can materially block or harm the mandate outcome; `Medium` materially +degrades it; `Low` is bounded and non-blocking. `S` is localized, `M` crosses +components or needs coordinated validation, and `L` spans major surfaces or owners. + For a material open item, use its existing `Type` as the next-move lane: `decision-needed` means decision now, `verification` means evidence needed, and `action` means implementation correction. Use `risk` only when no responsible @@ -142,6 +155,14 @@ and their cited evidence. The reviewer does not read the worker rubric. Revise the selected outputs once from its feedback; the worker writes no artifact, shared state, or gate. +Before marking the reviewer complete, reconcile the quality worker and every +other task recursively spawned by this reviewer against recorded lifecycle +provenance. Each start requires exactly one terminal outcome: completed, +failed, cancelled, or interrupted. Do not infer closure from returned prose or +an artifact. Leave the reviewer incomplete while a task is running, open, +multiply terminated, or ambiguously correlated, and return the exact task and +session to the coordinator. + On a resume in the same audit root, read the prior open-items table and relevant decision inventory/register before writing. For each material prior open item, retain its identifier and mark it `open`, `verified-fixed`, `superseded`, or diff --git a/skills/wgo/references/common/synthesis.md b/skills/wgo/references/common/synthesis.md index 02dbfdd..9a3eeb6 100644 --- a/skills/wgo/references/common/synthesis.md +++ b/skills/wgo/references/common/synthesis.md @@ -3,6 +3,20 @@ Use for `wgo:summarize`. Synthesis is an audit-lead reconciliation step, not a concatenation of reports or a workflow gate. +Before doing synthesis work or emitting the summary phase marker, recursively +reconcile every WGO task spawned from the audit root against recorded +collaboration and task-lifecycle provenance. Every start must have exactly one +terminal outcome: completed, failed, cancelled, or interrupted. Do not infer +closure from reports, checklist states, messages, or worker disappearance. If +any task remains running, open, multiply terminated, or ambiguously correlated, +do not begin synthesis; wait or report the exact blocking task/session. A +failed, cancelled, or interrupted task is lifecycle-closed but does not make +its reviewer successful; retain the existing dependency limitation. + +After that gate passes, emit this normal progress message once: +`Starting synthesis. `. The HTML comment is a +persisted cost-attribution marker, not audit evidence. + Read each completed reviewer report, handoff, and linked evidence/artifacts directly. A reviewer run out of order remains publishable when its report states the specific limitation. @@ -61,8 +75,30 @@ versions, model names, finding counts, timestamps, or source citations. Omit unsupported optional fields; for required unknowns use `null`, `[]`, or a controlled `unknown` value. +## Cost Closeout + +After the four audience reports pass synthesis validation, run the complete +provider workflow selected from `audit-brief.md`: `cost-estimation.md` for +Codex, `cost-estimation-claude.md` for Claude, or +`cost-estimation-opencode.md` for OpenCode. This is the final +end-of-audit phase, before the operationalization question. It creates +`controls/cost-estimate.md` with frozen calculation evidence; link the control +from `index.md`, `executive-summary.md`, +`product-manager-notes.md`, and `technical-lead-notes.md` as an +API-equivalent estimate with its reconciliation status. + +Immediately before starting that cost workflow, emit this normal progress +message once: `Synthesis validated; starting cost estimation. `. The marker is the audit-only cost +cutoff; the cost workflow itself is excluded from the estimate. + +Do not delay or omit the audit cost phase because a rate, JSONL field, session +file, or reconciliation result is unknown. State the limitation, mark the +affected model or aggregate unpriced/unreconciled, and preserve the exact +manifest and disputed records instead. + Do not create an action backlog, decision queue, status table, or a separate reconciliation workflow. Any operator aid remains a later, explicitly -authorized `wgo:operationalize` activity. After the summary is complete, ask -exactly: `Should I proceed with wgo:operationalize?` Do not draft operator aids -until the auditor answers yes. +authorized `wgo:operationalize` activity. After the summary and cost closeout +are complete, ask exactly: `Should I proceed with wgo:operationalize?` Do not +draft operator aids until the auditor answers yes. diff --git a/skills/wgo/references/data/anthropic-api-rate-card-2026-08-07.json b/skills/wgo/references/data/anthropic-api-rate-card-2026-08-07.json new file mode 100644 index 0000000..f1344f7 --- /dev/null +++ b/skills/wgo/references/data/anthropic-api-rate-card-2026-08-07.json @@ -0,0 +1,59 @@ +{ + "schema_version": 1, + "rate_card_date": "2026-08-07", + "currency": "USD", + "unit": "per_1_million_tokens", + "pricing_basis": "Anthropic first-party API list pricing; this is not a Claude subscription, Bedrock, Vertex AI, or Microsoft Foundry invoice.", + "source": "https://platform.claude.com/docs/en/about-claude/pricing", + "source_checked_at": "2026-08-07", + "default_inference_geo_when_not_returned": "global", + "inference_geo_multipliers": { + "global": 1.0, + "us": 1.1 + }, + "models": { + "claude-opus-5": { + "standard": { + "input": 5.0, + "cache_write_5m": 6.25, + "cache_write_1h": 10.0, + "cache_read": 0.5, + "output": 25.0 + } + }, + "claude-sonnet-5": { + "standard": { + "input": 2.0, + "cache_write_5m": 2.5, + "cache_write_1h": 4.0, + "cache_read": 0.2, + "output": 10.0, + "effective_through": "2026-08-31" + } + }, + "claude-sonnet-4-6": { + "standard": { + "input": 3.0, + "cache_write_5m": 3.75, + "cache_write_1h": 6.0, + "cache_read": 0.3, + "output": 15.0 + } + }, + "claude-haiku-4-5": { + "standard": { + "input": 1.0, + "cache_write_5m": 1.25, + "cache_write_1h": 2.0, + "cache_read": 0.1, + "output": 5.0 + } + } + }, + "notes": [ + "Use only an exact model key and observed standard service tier; unknown models or tiers are unpriced.", + "Apply the US inference multiplier only when the transcript reports inference_geo=us. When inference_geo is absent or not_available, use the manifest's declared global API-equivalent basis and disclose that geography was not observed.", + "Cache creation must be split into ephemeral 5-minute and 1-hour token fields. A nonzero unsplit cache-creation total is unpriced rather than assumed to use the 5-minute rate.", + "Claude Sonnet 5 pricing in this dated card is the introductory rate effective through 2026-08-31; later audits require a newer card." + ] +} diff --git a/skills/wgo/references/data/api-rate-card-2026-08-07.json b/skills/wgo/references/data/api-rate-card-2026-08-07.json new file mode 100644 index 0000000..258caf4 --- /dev/null +++ b/skills/wgo/references/data/api-rate-card-2026-08-07.json @@ -0,0 +1,40 @@ +{ + "schema_version": 1, + "rate_card_date": "2026-08-07", + "currency": "USD", + "pricing_basis": "OpenAI API token pricing, USD per 1,000,000 tokens; direct API standard/Batch/Flex/Fast mode, with returned or threshold-derived context band. This is not a Codex subscription invoice.", + "api_equivalent_default_service_tier": "standard", + "context_band_rule": { + "long_context_input_tokens_gt": 272000, + "short": "input_tokens <= 272000", + "long": "input_tokens > 272000", + "source": "OpenAI pricing context threshold" + }, + "source": "https://platform.openai.com/docs/pricing", + "source_checked_at": "2026-08-07", + "models": { + "gpt-5.6-sol": { + "standard": {"short": {"input": 5.00, "cached_input": 0.50, "output": 30.00}, "long": {"input": 10.00, "cached_input": 1.00, "output": 45.00}}, + "batch": {"short": {"input": 2.50, "cached_input": 0.25, "output": 15.00}, "long": {"input": 5.00, "cached_input": 0.50, "output": 22.50}}, + "flex": {"short": {"input": 2.50, "cached_input": 0.25, "output": 15.00}, "long": {"input": 5.00, "cached_input": 0.50, "output": 22.50}}, + "fast": {"short": {"input": 10.00, "cached_input": 1.00, "output": 60.00}, "long": {"input": 20.00, "cached_input": 2.00, "output": 90.00}} + }, + "gpt-5.6-terra": { + "standard": {"short": {"input": 2.00, "cached_input": 0.20, "output": 12.00}, "long": {"input": 4.00, "cached_input": 0.40, "output": 18.00}}, + "batch": {"short": {"input": 1.00, "cached_input": 0.10, "output": 6.00}, "long": {"input": 2.00, "cached_input": 0.20, "output": 9.00}}, + "flex": {"short": {"input": 1.00, "cached_input": 0.10, "output": 6.00}, "long": {"input": 2.00, "cached_input": 0.20, "output": 9.00}}, + "fast": {"short": {"input": 4.00, "cached_input": 0.40, "output": 24.00}, "long": {"input": 8.00, "cached_input": 0.80, "output": 36.00}} + }, + "gpt-5.6-luna": { + "standard": {"short": {"input": 0.20, "cached_input": 0.02, "output": 1.20}, "long": {"input": 0.40, "cached_input": 0.04, "output": 1.80}}, + "batch": {"short": {"input": 0.10, "cached_input": 0.01, "output": 0.60}, "long": {"input": 0.20, "cached_input": 0.02, "output": 0.90}}, + "flex": {"short": {"input": 0.10, "cached_input": 0.01, "output": 0.60}, "long": {"input": 0.20, "cached_input": 0.02, "output": 0.90}}, + "fast": {"short": {"input": 0.40, "cached_input": 0.04, "output": 2.40}, "long": {"input": 0.80, "cached_input": 0.08, "output": 3.60}} + } + }, + "notes": [ + "`priority` is normalized to `fast` only when the raw record identifies that service tier.", + "When Codex rollout JSONL does not return a service tier, use this card's declared standard tier for a reproducible API-equivalent estimate; it does not identify the actual backend service tier.", + "Use a returned context band when present; otherwise derive only from the recorded `input_tokens` threshold above. Do not infer regional-processing uplift, tool charge, cache-write charge, or other surcharge from token totals." + ] +} diff --git a/skills/wgo/references/data/opencode-cost-basis-2026-08-07.json b/skills/wgo/references/data/opencode-cost-basis-2026-08-07.json new file mode 100644 index 0000000..6103bcd --- /dev/null +++ b/skills/wgo/references/data/opencode-cost-basis-2026-08-07.json @@ -0,0 +1,23 @@ +{ + "schema_version": 1, + "basis_date": "2026-08-07", + "currency": "USD", + "pricing_basis": "Exact per-assistant-message cost recorded by the installed OpenCode runtime for its selected provider and model.", + "source": "https://opencode.ai/docs/cli/", + "source_checked_at": "2026-08-07", + "required_message_fields": [ + "id", + "role", + "providerID", + "modelID", + "tokens", + "cost" + ], + "rules": [ + "Never reconstruct a universal rate card across OpenCode providers or silently substitute another provider's price.", + "A positive recorded message cost is replayed exactly and identified as an OpenCode-recorded API-equivalent estimate.", + "A recorded zero is unpriced unless the frozen manifest explicitly documents why that provider/model is legitimately zero-cost, such as a local model with no metered API.", + "Session aggregate tokens and cost are reconciliation evidence only; never add them to message-level totals.", + "Subscription, plan-credit, negotiated, local-compute, and provider-invoice differences remain limitations." + ] +} diff --git a/skills/wgo/references/fixtures/cost-estimation-claude/README.md b/skills/wgo/references/fixtures/cost-estimation-claude/README.md new file mode 100644 index 0000000..da50d6f --- /dev/null +++ b/skills/wgo/references/fixtures/cost-estimation-claude/README.md @@ -0,0 +1,11 @@ +# Claude Cost Estimation Dry Run + +Run the inline recipe from `../../common/cost-estimation-claude.md` with +`frozen-manifest.json` and +`../../data/anthropic-api-rate-card-2026-08-07.json`. + +The fixture proves that progressive copies of the root summary and child +response are each counted once. It includes the child only through matching +Agent metadata/provenance, excludes `unrelated.jsonl` despite directory/day +proximity, and excludes the root request after the explicit cost cutoff. The +reconciled exact total is USD `0.007645`. diff --git a/skills/wgo/references/fixtures/cost-estimation-claude/agent-worker1.jsonl b/skills/wgo/references/fixtures/cost-estimation-claude/agent-worker1.jsonl new file mode 100644 index 0000000..af5c191 --- /dev/null +++ b/skills/wgo/references/fixtures/cost-estimation-claude/agent-worker1.jsonl @@ -0,0 +1,2 @@ +{"type":"assistant","sessionId":"claude-root","agentId":"worker1","requestId":"req-child","message":{"id":"msg-child","model":"claude-opus-5","content":[{"type":"text","text":"Partial child result."}],"usage":{"input_tokens":500,"cache_creation_input_tokens":0,"cache_read_input_tokens":100,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":0},"output_tokens":10,"service_tier":"standard","inference_geo":"not_available"}}} +{"type":"assistant","sessionId":"claude-root","agentId":"worker1","requestId":"req-child","message":{"id":"msg-child","model":"claude-opus-5","content":[{"type":"text","text":"Complete child result."}],"usage":{"input_tokens":500,"cache_creation_input_tokens":0,"cache_read_input_tokens":100,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":0},"output_tokens":30,"service_tier":"standard","inference_geo":"not_available"}}} diff --git a/skills/wgo/references/fixtures/cost-estimation-claude/agent-worker1.meta.json b/skills/wgo/references/fixtures/cost-estimation-claude/agent-worker1.meta.json new file mode 100644 index 0000000..9346d62 --- /dev/null +++ b/skills/wgo/references/fixtures/cost-estimation-claude/agent-worker1.meta.json @@ -0,0 +1 @@ +{"agentType":"general-purpose","description":"wgo reviewer: architecture","model":"claude-opus-5","spawnDepth":1,"toolUseId":"toolu_child"} diff --git a/skills/wgo/references/fixtures/cost-estimation-claude/frozen-manifest.json b/skills/wgo/references/fixtures/cost-estimation-claude/frozen-manifest.json new file mode 100644 index 0000000..9aa632f --- /dev/null +++ b/skills/wgo/references/fixtures/cost-estimation-claude/frozen-manifest.json @@ -0,0 +1,67 @@ +{ + "schema_version": 1, + "provider": "claude-code", + "coverage": "audit", + "root_session_id": "claude-root", + "pricing_basis": { + "rate_card": "../../data/anthropic-api-rate-card-2026-08-07.json", + "default_service_tier_when_not_returned": "standard", + "default_inference_geo_when_not_returned": "global" + }, + "phase_policy": { + "included": ["unattributed", "onboarding", "audit", "summary"], + "excluded": ["cost-estimation"], + "markers": { + "WGO_PHASE_ONBOARDING_START": "onboarding", + "WGO_PHASE_AUDIT_START": "audit", + "WGO_PHASE_SUMMARY_START": "summary", + "WGO_AUDIT_COMPLETE_COST_PHASE_STARTS": "cost-estimation", + "WGO_PHASE_OPERATIONALIZATION_START": "operationalization", + "WGO_OPERATIONALIZATION_COMPLETE_COST_PHASE_STARTS": "cost-estimation" + } + }, + "sessions": [ + { + "record_set_id": "claude-root", + "session_id": "claude-root", + "agent_id": null, + "file_path": "root.jsonl", + "prefix_sha256": "30a2d5c312382727c36ce36b6c4fa426e1cbcbaa5ea6507983785978367132e2", + "wgo_role_task_name": "Claude audit coordinator", + "phase": "from-markers", + "parent_record_set_id": null, + "root_relationship": "root", + "decision": "included", + "rationale": "Fixture root through the explicit cost boundary.", + "cutoff": {"marker": "WGO_AUDIT_COMPLETE_COST_PHASE_STARTS", "line_number": 9}, + "usage_schema": "claude-code-assistant-message-usage-v1" + }, + { + "record_set_id": "claude-root:agent:worker1", + "session_id": "claude-root", + "agent_id": "worker1", + "file_path": "agent-worker1.jsonl", + "prefix_sha256": "3b35711bade0d243f4ca9a6673c1f13ef75061d85a630314610612b7c4449b87", + "metadata_path": "agent-worker1.meta.json", + "metadata_sha256": "28072eb9cb59269f022e54587e1b09be8bd4a2119f7901abac4df22233266c6b", + "tool_use_id": "toolu_child", + "wgo_role_task_name": "wgo reviewer: architecture", + "phase": "audit", + "parent_record_set_id": "claude-root", + "root_relationship": "descendant", + "decision": "included", + "rationale": "Matching Agent tool use, metadata, child agentId, and completed result.", + "lifecycle": {"spawn_line": 4, "terminal_line": 5, "outcome": "completed"}, + "cutoff": {"line_number": 2}, + "usage_schema": "claude-code-assistant-message-usage-v1" + } + ], + "exclusions": [ + { + "record_set_id": "claude-unrelated", + "file_path": "unrelated.jsonl", + "decision": "excluded", + "rationale": "No Agent/Task provenance path from claude-root." + } + ] +} diff --git a/skills/wgo/references/fixtures/cost-estimation-claude/root.jsonl b/skills/wgo/references/fixtures/cost-estimation-claude/root.jsonl new file mode 100644 index 0000000..a559df5 --- /dev/null +++ b/skills/wgo/references/fixtures/cost-estimation-claude/root.jsonl @@ -0,0 +1,10 @@ +{"type":"assistant","sessionId":"claude-root","requestId":"req-start","message":{"id":"msg-start","model":"claude-sonnet-5","content":[{"type":"text","text":"Starting onboarding. "}],"usage":{"input_tokens":100,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":0},"output_tokens":10,"service_tier":"standard","inference_geo":"not_available"}}} +{"type":"assistant","sessionId":"claude-root","requestId":"req-onboard","message":{"id":"msg-onboard","model":"claude-sonnet-5","content":[{"type":"text","text":"Onboarding work."}],"usage":{"input_tokens":200,"cache_creation_input_tokens":50,"cache_read_input_tokens":100,"cache_creation":{"ephemeral_5m_input_tokens":50,"ephemeral_1h_input_tokens":0},"output_tokens":20,"service_tier":"standard","inference_geo":"not_available"}}} +{"type":"assistant","sessionId":"claude-root","requestId":"req-audit-marker","message":{"id":"msg-audit-marker","model":"claude-sonnet-5","content":[{"type":"text","text":"Starting audit. "}],"usage":{"input_tokens":50,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":0},"output_tokens":5,"service_tier":"standard","inference_geo":"not_available"}}} +{"type":"assistant","sessionId":"claude-root","requestId":"req-spawn","message":{"id":"msg-spawn","model":"claude-sonnet-5","content":[{"type":"tool_use","id":"toolu_child","name":"Agent","input":{"description":"wgo reviewer: architecture","subagent_type":"general-purpose","prompt":"fixture"}}],"usage":{"input_tokens":300,"cache_creation_input_tokens":0,"cache_read_input_tokens":200,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":0},"output_tokens":30,"service_tier":"standard","inference_geo":"not_available"}}} +{"type":"user","sessionId":"claude-root","message":{"content":[{"type":"tool_result","tool_use_id":"toolu_child","content":"completed"}]},"toolUseResult":{"agentId":"worker1","status":"completed","resolvedModel":"claude-opus-5"}} +{"type":"assistant","sessionId":"claude-root","requestId":"req-summary-marker","message":{"id":"msg-summary-marker","model":"claude-sonnet-5","content":[{"type":"text","text":"Starting synthesis. "}],"usage":{"input_tokens":100,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":0},"output_tokens":10,"service_tier":"standard","inference_geo":"not_available"}}} +{"type":"assistant","sessionId":"claude-root","requestId":"req-summary","message":{"id":"msg-summary","model":"claude-sonnet-5","content":[{"type":"text","text":"Partial summary."}],"usage":{"input_tokens":400,"cache_creation_input_tokens":100,"cache_read_input_tokens":50,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":100},"output_tokens":5,"service_tier":"standard","inference_geo":"not_available"}}} +{"type":"assistant","sessionId":"claude-root","requestId":"req-summary","message":{"id":"msg-summary","model":"claude-sonnet-5","content":[{"type":"text","text":"Complete summary."}],"usage":{"input_tokens":400,"cache_creation_input_tokens":100,"cache_read_input_tokens":50,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":100},"output_tokens":40,"service_tier":"standard","inference_geo":"not_available"}}} +{"type":"assistant","sessionId":"claude-root","requestId":"req-cost-boundary","message":{"id":"msg-cost-boundary","model":"claude-sonnet-5","content":[{"type":"text","text":"Synthesis validated; starting cost estimation. "}],"usage":{"input_tokens":100,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":0},"output_tokens":10,"service_tier":"standard","inference_geo":"not_available"}}} +{"type":"assistant","sessionId":"claude-root","requestId":"req-cost-work","message":{"id":"msg-cost-work","model":"claude-sonnet-5","content":[{"type":"text","text":"Excluded cost work."}],"usage":{"input_tokens":999,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":0},"output_tokens":99,"service_tier":"standard","inference_geo":"not_available"}}} diff --git a/skills/wgo/references/fixtures/cost-estimation-claude/unrelated.jsonl b/skills/wgo/references/fixtures/cost-estimation-claude/unrelated.jsonl new file mode 100644 index 0000000..17e6acb --- /dev/null +++ b/skills/wgo/references/fixtures/cost-estimation-claude/unrelated.jsonl @@ -0,0 +1 @@ +{"type":"assistant","sessionId":"claude-unrelated","requestId":"req-unrelated","message":{"id":"msg-unrelated","model":"claude-opus-5","content":[{"type":"text","text":"Same day, unrelated."}],"usage":{"input_tokens":9999,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":0},"output_tokens":999,"service_tier":"standard","inference_geo":"not_available"}}} diff --git a/skills/wgo/references/fixtures/cost-estimation-opencode/README.md b/skills/wgo/references/fixtures/cost-estimation-opencode/README.md new file mode 100644 index 0000000..cc13755 --- /dev/null +++ b/skills/wgo/references/fixtures/cost-estimation-opencode/README.md @@ -0,0 +1,11 @@ +# OpenCode Cost Estimation Dry Run + +Run the inline recipe from `../../common/cost-estimation-opencode.md` with +`frozen-manifest.json` and +`../../data/opencode-cost-basis-2026-08-07.json`. + +The fixture proves that the parent and child assistant messages are counted +once at message level. The deliberately inflated root session aggregate is not +added, the child's matching full-export aggregate is only reconciled, the +post-cutoff cost message is excluded, and `unrelated.json` is excluded despite +workspace/day proximity. The reconciled exact total is USD `0.0079`. diff --git a/skills/wgo/references/fixtures/cost-estimation-opencode/child.json b/skills/wgo/references/fixtures/cost-estimation-opencode/child.json new file mode 100644 index 0000000..6f778dc --- /dev/null +++ b/skills/wgo/references/fixtures/cost-estimation-opencode/child.json @@ -0,0 +1,19 @@ +{ + "info": { + "id": "ses-child", + "parentID": "ses-root", + "version": "1.18.11", + "tokens": {"input": 700, "output": 50, "reasoning": 5, "cache": {"read": 100, "write": 0}}, + "cost": 0.0043 + }, + "messages": [ + { + "info": {"id": "msg-child-a", "sessionID": "ses-child", "role": "assistant", "providerID": "anthropic", "modelID": "claude-opus-5", "agent": "general", "tokens": {"input": 500, "output": 30, "reasoning": 5, "cache": {"read": 100, "write": 0}}, "cost": 0.0033}, + "parts": [{"type": "text", "text": "Child analysis."}] + }, + { + "info": {"id": "msg-child-last", "sessionID": "ses-child", "role": "assistant", "providerID": "anthropic", "modelID": "claude-opus-5", "agent": "general", "tokens": {"input": 200, "output": 20, "reasoning": 0, "cache": {"read": 0, "write": 0}}, "cost": 0.001}, + "parts": [{"type": "text", "text": "Child complete."}] + } + ] +} diff --git a/skills/wgo/references/fixtures/cost-estimation-opencode/frozen-manifest.json b/skills/wgo/references/fixtures/cost-estimation-opencode/frozen-manifest.json new file mode 100644 index 0000000..2bd787b --- /dev/null +++ b/skills/wgo/references/fixtures/cost-estimation-opencode/frozen-manifest.json @@ -0,0 +1,62 @@ +{ + "schema_version": 1, + "provider": "opencode", + "coverage": "audit", + "root_session_id": "ses-root", + "pricing_basis": { + "basis_file": "../../data/opencode-cost-basis-2026-08-07.json", + "opencode_version": "1.18.11", + "zero_cost_authorizations": [] + }, + "phase_policy": { + "included": ["unattributed", "onboarding", "audit", "summary"], + "excluded": ["cost-estimation"], + "markers": { + "WGO_PHASE_ONBOARDING_START": "onboarding", + "WGO_PHASE_AUDIT_START": "audit", + "WGO_PHASE_SUMMARY_START": "summary", + "WGO_AUDIT_COMPLETE_COST_PHASE_STARTS": "cost-estimation", + "WGO_PHASE_OPERATIONALIZATION_START": "operationalization", + "WGO_OPERATIONALIZATION_COMPLETE_COST_PHASE_STARTS": "cost-estimation" + } + }, + "sessions": [ + { + "session_id": "ses-root", + "export_path": "root.json", + "export_sha256": "918bd03d14acf1d2e7bce46513e0c36abababb8a7393a81b6e393309d122919a", + "wgo_role_task_name": "OpenCode audit coordinator", + "phase": "from-markers", + "parent_session_id": null, + "root_relationship": "root", + "decision": "included", + "rationale": "Fixture root through the explicit marker message.", + "cutoff": {"marker": "WGO_AUDIT_COMPLETE_COST_PHASE_STARTS", "message_id": "msg-cost-boundary"}, + "aggregate_reconciliation": "partial-export-not-applicable", + "usage_schema": "opencode-export-assistant-info-v1" + }, + { + "session_id": "ses-child", + "export_path": "child.json", + "export_sha256": "dc2b7d63ccdedd9eb9ab5ae987b82617ce8539c79c6cb264fb2681dfed4e91da", + "wgo_role_task_name": "wgo reviewer: architecture", + "phase": "audit", + "parent_session_id": "ses-root", + "root_relationship": "descendant", + "decision": "included", + "rationale": "Completed parent task and matching child parentID.", + "provenance": {"parent_message_id": "msg-spawn", "task_part_id": "prt-task", "call_id": "call-task", "terminal_state": "completed"}, + "cutoff": {"message_id": "msg-child-last"}, + "aggregate_reconciliation": "full-export", + "usage_schema": "opencode-export-assistant-info-v1" + } + ], + "exclusions": [ + { + "session_id": "ses-unrelated", + "export_path": "unrelated.json", + "decision": "excluded", + "rationale": "No task/parent path from ses-root." + } + ] +} diff --git a/skills/wgo/references/fixtures/cost-estimation-opencode/root.json b/skills/wgo/references/fixtures/cost-estimation-opencode/root.json new file mode 100644 index 0000000..4f25f1c --- /dev/null +++ b/skills/wgo/references/fixtures/cost-estimation-opencode/root.json @@ -0,0 +1,39 @@ +{ + "info": { + "id": "ses-root", + "parentID": null, + "version": "1.18.11", + "tokens": {"input": 999999, "output": 99999, "reasoning": 9999, "cache": {"read": 99999, "write": 9999}}, + "cost": 99.99 + }, + "messages": [ + { + "info": {"id": "msg-start", "sessionID": "ses-root", "role": "assistant", "providerID": "anthropic", "modelID": "claude-sonnet-5", "agent": "build", "tokens": {"input": 100, "output": 10, "reasoning": 0, "cache": {"read": 0, "write": 0}}, "cost": 0.0003}, + "parts": [{"type": "text", "text": "Starting onboarding. "}] + }, + { + "info": {"id": "msg-audit-marker", "sessionID": "ses-root", "role": "assistant", "providerID": "anthropic", "modelID": "claude-sonnet-5", "agent": "build", "tokens": {"input": 50, "output": 5, "reasoning": 0, "cache": {"read": 0, "write": 0}}, "cost": 0.00015}, + "parts": [{"type": "text", "text": "Starting audit. "}] + }, + { + "info": {"id": "msg-spawn", "sessionID": "ses-root", "role": "assistant", "providerID": "anthropic", "modelID": "claude-sonnet-5", "agent": "build", "tokens": {"input": 300, "output": 30, "reasoning": 5, "cache": {"read": 200, "write": 0}}, "cost": 0.00094}, + "parts": [{"id": "prt-task", "type": "tool", "callID": "call-task", "tool": "task", "state": {"status": "completed", "input": {"description": "wgo reviewer: architecture", "subagent_type": "general"}, "metadata": {"sessionId": "ses-child"}}}] + }, + { + "info": {"id": "msg-summary-marker", "sessionID": "ses-root", "role": "assistant", "providerID": "anthropic", "modelID": "claude-sonnet-5", "agent": "build", "tokens": {"input": 100, "output": 10, "reasoning": 0, "cache": {"read": 0, "write": 0}}, "cost": 0.0003}, + "parts": [{"type": "text", "text": "Starting synthesis. "}] + }, + { + "info": {"id": "msg-summary", "sessionID": "ses-root", "role": "assistant", "providerID": "anthropic", "modelID": "claude-sonnet-5", "agent": "build", "tokens": {"input": 400, "output": 40, "reasoning": 10, "cache": {"read": 50, "write": 100}}, "cost": 0.00161}, + "parts": [{"type": "text", "text": "Summary."}] + }, + { + "info": {"id": "msg-cost-boundary", "sessionID": "ses-root", "role": "assistant", "providerID": "anthropic", "modelID": "claude-sonnet-5", "agent": "build", "tokens": {"input": 100, "output": 10, "reasoning": 0, "cache": {"read": 0, "write": 0}}, "cost": 0.0003}, + "parts": [{"type": "text", "text": "Synthesis validated; starting cost estimation. "}] + }, + { + "info": {"id": "msg-cost-work", "sessionID": "ses-root", "role": "assistant", "providerID": "anthropic", "modelID": "claude-sonnet-5", "agent": "build", "tokens": {"input": 999, "output": 99, "reasoning": 0, "cache": {"read": 0, "write": 0}}, "cost": 0.00999}, + "parts": [{"type": "text", "text": "Excluded cost work."}] + } + ] +} diff --git a/skills/wgo/references/fixtures/cost-estimation-opencode/unrelated.json b/skills/wgo/references/fixtures/cost-estimation-opencode/unrelated.json new file mode 100644 index 0000000..3bfc126 --- /dev/null +++ b/skills/wgo/references/fixtures/cost-estimation-opencode/unrelated.json @@ -0,0 +1,9 @@ +{ + "info": {"id": "ses-unrelated", "parentID": null, "version": "1.18.11", "tokens": {"input": 9999, "output": 999, "reasoning": 0, "cache": {"read": 0, "write": 0}}, "cost": 9.99}, + "messages": [ + { + "info": {"id": "msg-unrelated", "sessionID": "ses-unrelated", "role": "assistant", "providerID": "anthropic", "modelID": "claude-opus-5", "agent": "build", "tokens": {"input": 9999, "output": 999, "reasoning": 0, "cache": {"read": 0, "write": 0}}, "cost": 9.99}, + "parts": [{"type": "text", "text": "Same project and day, unrelated."}] + } + ] +} diff --git a/skills/wgo/references/fixtures/cost-estimation/README.md b/skills/wgo/references/fixtures/cost-estimation/README.md new file mode 100644 index 0000000..e25e4d2 --- /dev/null +++ b/skills/wgo/references/fixtures/cost-estimation/README.md @@ -0,0 +1,59 @@ +# Cost Estimation Dry Run + +This fixture uses the minimal Codex rollout shape used by the portable recipe: +`session_meta`, `turn_context` with `turn_id` and `model`, and +`event_msg`/`token_count` records with +`payload.info.last_token_usage` plus the cumulative `total_token_usage` object. + +Run the one-off Python recipe in `../../common/cost-estimation.md` with: + +```text +audit-only manifest: frozen-manifest.json +operationalized manifest: operationalized-manifest.json +rate card: ../../data/api-rate-card-2026-08-07.json +``` + +The expected machine-checkable results are `expected-result.json` and +`expected-operationalized-result.json`. + +`root.jsonl` records onboarding, audit, summary, cost-estimation, and +operationalization phase markers plus a `sub_agent_activity` start with its +event ID, child thread ID, and agent path. `child.jsonl` is a full-history fork: +it begins with inherited root metadata and usage, then records child-specific +metadata, `task_started`, one child request echoed twice, a nested spawn, and +`task_complete`. Unrelated reuse is appended after that terminal boundary. +`nested-child.jsonl` repeats this pattern for the nested quality worker. + +The manifests select the child-specific metadata and parse only each inclusive +`task_started`-through-terminal interval. They therefore exclude inherited +parent requests and later reuse. Both dry runs read only `last_token_usage`, so +inherited cumulative usage is not counted again. Because these events have no +raw request IDs, consecutive identical usage states are disclosed as unchanged +echoes and counted once under deterministic `legacy-state` identities. + +The audit-only cutoff is the root's line 12 +`WGO_AUDIT_COMPLETE_COST_PHASE_STARTS` marker. It produces separate onboarding, +audit, and summary rows, including both descendant levels, totaling USD +0.009573. Later records do not alter the frozen prefixes. + +The operationalized cutoff is line 18 +`WGO_OPERATIONALIZATION_COMPLETE_COST_PHASE_STARTS`. It adds the USD 0.00202 +operationalization row while explicitly excluding the earlier line 14 +cost-estimation request, producing USD 0.011593. The later refresh calculation +at lines 19–20 is outside the cutoff. This proves that refreshing +`cost-estimate.md` includes operationalization without charging either cost +calculation to the audit. The refreshed manifest also verifies the SHA-256 of +the preserved audit-only manifest before calculating. + +`unrelated.jsonl` has the same workspace/day signal and a much larger request, +but no recorded collaboration spawn or task-lifecycle path from `ses-root`; it +is excluded. Both results also report 40 root `cache_write_input_tokens` as an +excluded-by-formula limitation. Pricing uses the declared standard tier and +threshold-derived short-context API-equivalent rates. It is not a Codex +invoice. + +Before synthesis, the workflow separately requires the coordinator to prove +that every recursively spawned task has exactly one completed, failed, +cancelled, or interrupted outcome. Removing either fixture terminal record is +therefore a blocking lifecycle defect, not a session that may be omitted from +the manifest. diff --git a/skills/wgo/references/fixtures/cost-estimation/child.jsonl b/skills/wgo/references/fixtures/cost-estimation/child.jsonl new file mode 100644 index 0000000..e08a826 --- /dev/null +++ b/skills/wgo/references/fixtures/cost-estimation/child.jsonl @@ -0,0 +1,12 @@ +{"timestamp":"2026-08-07T09:00:00Z","type":"session_meta","payload":{"id":"ses-root","session_id":"ses-root","cwd":"/same/workspace"}} +{"timestamp":"2026-08-07T09:02:30Z","type":"turn_context","payload":{"turn_id":"turn-root-audit","model":"gpt-5.6-terra","cwd":"/same/workspace"}} +{"timestamp":"2026-08-07T09:03:30Z","type":"event_msg","payload":{"type":"token_count","info":{"last_token_usage":{"input_tokens":600,"cached_input_tokens":100,"output_tokens":100,"reasoning_output_tokens":20,"total_tokens":700},"total_token_usage":{"input_tokens":1000,"cached_input_tokens":200,"output_tokens":200,"reasoning_output_tokens":40,"total_tokens":1200},"model_context_window":272000}}} +{"timestamp":"2026-08-07T09:03:01Z","type":"session_meta","payload":{"id":"ses-child","session_id":"ses-child","cwd":"/same/workspace","source":{"subagent":{"thread_spawn":{"parent_thread_id":"ses-root","agent_path":"reviewer/code-quality"}}}}} +{"timestamp":"2026-08-07T09:03:02Z","type":"event_msg","payload":{"type":"task_started","task_id":"task-code-quality","parent_thread_id":"ses-root","agent_path":"reviewer/code-quality"}} +{"timestamp":"2026-08-07T09:03:03Z","type":"turn_context","payload":{"turn_id":"turn-child-review","model":"gpt-5.6-luna","cwd":"/same/workspace"}} +{"timestamp":"2026-08-07T09:03:04Z","type":"event_msg","payload":{"type":"token_count","info":{"last_token_usage":{"input_tokens":500,"cached_input_tokens":100,"output_tokens":100,"reasoning_output_tokens":20,"total_tokens":600},"total_token_usage":{"input_tokens":1500,"cached_input_tokens":300,"output_tokens":300,"reasoning_output_tokens":60,"total_tokens":1800},"model_context_window":272000}}} +{"timestamp":"2026-08-07T09:03:04Z","type":"event_msg","payload":{"type":"token_count","info":{"last_token_usage":{"input_tokens":500,"cached_input_tokens":100,"output_tokens":100,"reasoning_output_tokens":20,"total_tokens":600},"total_token_usage":{"input_tokens":1500,"cached_input_tokens":300,"output_tokens":300,"reasoning_output_tokens":60,"total_tokens":1800},"model_context_window":272000}}} +{"timestamp":"2026-08-07T09:03:05Z","type":"event_msg","payload":{"type":"sub_agent_activity","event_id":"evt-spawn-quality-check","agent_thread_id":"ses-nested","agent_path":"reviewer/code-quality/quality-check","kind":"started"}} +{"timestamp":"2026-08-07T09:03:06Z","type":"event_msg","payload":{"type":"task_complete","task_id":"task-code-quality","parent_thread_id":"ses-root","agent_path":"reviewer/code-quality"}} +{"timestamp":"2026-08-07T11:00:00Z","type":"turn_context","payload":{"turn_id":"turn-child-unrelated-reuse","model":"gpt-5.6-luna","cwd":"/same/workspace"}} +{"timestamp":"2026-08-07T11:00:01Z","type":"event_msg","payload":{"type":"token_count","info":{"last_token_usage":{"input_tokens":7000,"cached_input_tokens":0,"output_tokens":700,"reasoning_output_tokens":0,"total_tokens":7700},"total_token_usage":{"input_tokens":8500,"cached_input_tokens":300,"output_tokens":1000,"reasoning_output_tokens":60,"total_tokens":9500},"model_context_window":272000}}} diff --git a/skills/wgo/references/fixtures/cost-estimation/expected-operationalized-result.json b/skills/wgo/references/fixtures/cost-estimation/expected-operationalized-result.json new file mode 100644 index 0000000..ce6dfcd --- /dev/null +++ b/skills/wgo/references/fixtures/cost-estimation/expected-operationalized-result.json @@ -0,0 +1,124 @@ +{ + "rows": [ + { + "uncached_input_tokens": 400, + "cached_input_tokens": 100, + "output_tokens": 100, + "reasoning_tokens": 20, + "cost_usd": "0.000202", + "priced": true, + "rate_per_million": {"input": "0.20", "cached_input": "0.02", "output": "1.20"}, + "request_ids": ["legacy-state:ses-child:turn-child-review:7"], + "tier_sources": ["declared-api-equivalent"], + "context_band_sources": ["derived-input-threshold"], + "phase": "audit", + "session_id": "ses-child", + "model": "gpt-5.6-luna", + "service_tier": "standard", + "context_band": "short" + }, + { + "uncached_input_tokens": 150, + "cached_input_tokens": 50, + "output_tokens": 50, + "reasoning_tokens": 10, + "cost_usd": "0.000091", + "priced": true, + "rate_per_million": {"input": "0.20", "cached_input": "0.02", "output": "1.20"}, + "request_ids": ["legacy-state:ses-nested:turn-quality-check:13"], + "tier_sources": ["declared-api-equivalent"], + "context_band_sources": ["derived-input-threshold"], + "phase": "audit", + "session_id": "ses-nested", + "model": "gpt-5.6-luna", + "service_tier": "standard", + "context_band": "short" + }, + { + "uncached_input_tokens": 500, + "cached_input_tokens": 100, + "output_tokens": 100, + "reasoning_tokens": 20, + "cost_usd": "0.00222", + "priced": true, + "rate_per_million": {"input": "2.00", "cached_input": "0.20", "output": "12.00"}, + "request_ids": ["legacy-state:ses-root:turn-root-audit:8"], + "tier_sources": ["declared-api-equivalent"], + "context_band_sources": ["derived-input-threshold"], + "phase": "audit", + "session_id": "ses-root", + "model": "gpt-5.6-terra", + "service_tier": "standard", + "context_band": "short" + }, + { + "uncached_input_tokens": 300, + "cached_input_tokens": 100, + "output_tokens": 100, + "reasoning_tokens": 20, + "cost_usd": "0.00182", + "priced": true, + "rate_per_million": {"input": "2.00", "cached_input": "0.20", "output": "12.00"}, + "request_ids": ["legacy-state:ses-root:turn-root-onboarding:4"], + "tier_sources": ["declared-api-equivalent"], + "context_band_sources": ["derived-input-threshold"], + "phase": "onboarding", + "session_id": "ses-root", + "model": "gpt-5.6-terra", + "service_tier": "standard", + "context_band": "short" + }, + { + "uncached_input_tokens": 400, + "cached_input_tokens": 100, + "output_tokens": 100, + "reasoning_tokens": 20, + "cost_usd": "0.00202", + "priced": true, + "rate_per_million": {"input": "2.00", "cached_input": "0.20", "output": "12.00"}, + "request_ids": ["legacy-state:ses-root:turn-operationalization:17"], + "tier_sources": ["declared-api-equivalent"], + "context_band_sources": ["derived-input-threshold"], + "phase": "operationalization", + "session_id": "ses-root", + "model": "gpt-5.6-terra", + "service_tier": "standard", + "context_band": "short" + }, + { + "uncached_input_tokens": 800, + "cached_input_tokens": 200, + "output_tokens": 300, + "reasoning_tokens": 80, + "cost_usd": "0.00524", + "priced": true, + "rate_per_million": {"input": "2.00", "cached_input": "0.20", "output": "12.00"}, + "request_ids": ["legacy-state:ses-root:turn-root-summary:11"], + "tier_sources": ["declared-api-equivalent"], + "context_band_sources": ["derived-input-threshold"], + "phase": "summary", + "session_id": "ses-root", + "model": "gpt-5.6-terra", + "service_tier": "standard", + "context_band": "short" + } + ], + "excluded_requests": [ + {"request_id": "legacy-state:ses-root:turn-cost-phase:14", "session_id": "ses-root", "line_number": 14, "phase": "cost-estimation", "rationale": "Phase excluded by frozen manifest."} + ], + "duplicates": [ + {"kind": "unchanged-legacy-state", "request_id": "legacy-state:ses-child:turn-child-review:7", "first": {"session_id": "ses-child", "line_number": 7}, "duplicate": {"session_id": "ses-child", "line_number": 8}}, + {"kind": "unchanged-legacy-state", "request_id": "legacy-state:ses-nested:turn-quality-check:13", "first": {"session_id": "ses-nested", "line_number": 13}, "duplicate": {"session_id": "ses-nested", "line_number": 14}} + ], + "issues": [], + "limitations": [ + {"kind": "legacy-state-identity", "request_id": "legacy-state:ses-root:turn-root-onboarding:4", "session_id": "ses-root", "line_number": 4, "turn_id": "turn-root-onboarding"}, + {"kind": "legacy-state-identity", "request_id": "legacy-state:ses-root:turn-root-audit:8", "session_id": "ses-root", "line_number": 8, "turn_id": "turn-root-audit"}, + {"kind": "legacy-state-identity", "request_id": "legacy-state:ses-root:turn-root-summary:11", "session_id": "ses-root", "line_number": 11, "turn_id": "turn-root-summary"}, + {"kind": "cache-write-input-excluded-by-formula", "request_id": "legacy-state:ses-root:turn-root-summary:11", "session_id": "ses-root", "line_number": 11, "cache_write_input_tokens": 40}, + {"kind": "legacy-state-identity", "request_id": "legacy-state:ses-root:turn-operationalization:17", "session_id": "ses-root", "line_number": 17, "turn_id": "turn-operationalization"}, + {"kind": "legacy-state-identity", "request_id": "legacy-state:ses-child:turn-child-review:7", "session_id": "ses-child", "line_number": 7, "turn_id": "turn-child-review"}, + {"kind": "legacy-state-identity", "request_id": "legacy-state:ses-nested:turn-quality-check:13", "session_id": "ses-nested", "line_number": 13, "turn_id": "turn-quality-check"} + ], + "total_cost_usd": "0.011593" +} diff --git a/skills/wgo/references/fixtures/cost-estimation/expected-result.json b/skills/wgo/references/fixtures/cost-estimation/expected-result.json new file mode 100644 index 0000000..57c180e --- /dev/null +++ b/skills/wgo/references/fixtures/cost-estimation/expected-result.json @@ -0,0 +1,104 @@ +{ + "rows": [ + { + "uncached_input_tokens": 400, + "cached_input_tokens": 100, + "output_tokens": 100, + "reasoning_tokens": 20, + "cost_usd": "0.000202", + "priced": true, + "rate_per_million": {"input": "0.20", "cached_input": "0.02", "output": "1.20"}, + "request_ids": ["legacy-state:ses-child:turn-child-review:7"], + "tier_sources": ["declared-api-equivalent"], + "context_band_sources": ["derived-input-threshold"], + "phase": "audit", + "session_id": "ses-child", + "model": "gpt-5.6-luna", + "service_tier": "standard", + "context_band": "short" + }, + { + "uncached_input_tokens": 150, + "cached_input_tokens": 50, + "output_tokens": 50, + "reasoning_tokens": 10, + "cost_usd": "0.000091", + "priced": true, + "rate_per_million": {"input": "0.20", "cached_input": "0.02", "output": "1.20"}, + "request_ids": ["legacy-state:ses-nested:turn-quality-check:13"], + "tier_sources": ["declared-api-equivalent"], + "context_band_sources": ["derived-input-threshold"], + "phase": "audit", + "session_id": "ses-nested", + "model": "gpt-5.6-luna", + "service_tier": "standard", + "context_band": "short" + }, + { + "uncached_input_tokens": 500, + "cached_input_tokens": 100, + "output_tokens": 100, + "reasoning_tokens": 20, + "cost_usd": "0.00222", + "priced": true, + "rate_per_million": {"input": "2.00", "cached_input": "0.20", "output": "12.00"}, + "request_ids": ["legacy-state:ses-root:turn-root-audit:8"], + "tier_sources": ["declared-api-equivalent"], + "context_band_sources": ["derived-input-threshold"], + "phase": "audit", + "session_id": "ses-root", + "model": "gpt-5.6-terra", + "service_tier": "standard", + "context_band": "short" + }, + { + "uncached_input_tokens": 300, + "cached_input_tokens": 100, + "output_tokens": 100, + "reasoning_tokens": 20, + "cost_usd": "0.00182", + "priced": true, + "rate_per_million": {"input": "2.00", "cached_input": "0.20", "output": "12.00"}, + "request_ids": ["legacy-state:ses-root:turn-root-onboarding:4"], + "tier_sources": ["declared-api-equivalent"], + "context_band_sources": ["derived-input-threshold"], + "phase": "onboarding", + "session_id": "ses-root", + "model": "gpt-5.6-terra", + "service_tier": "standard", + "context_band": "short" + }, + { + "uncached_input_tokens": 800, + "cached_input_tokens": 200, + "output_tokens": 300, + "reasoning_tokens": 80, + "cost_usd": "0.00524", + "priced": true, + "rate_per_million": {"input": "2.00", "cached_input": "0.20", "output": "12.00"}, + "request_ids": ["legacy-state:ses-root:turn-root-summary:11"], + "tier_sources": ["declared-api-equivalent"], + "context_band_sources": ["derived-input-threshold"], + "phase": "summary", + "session_id": "ses-root", + "model": "gpt-5.6-terra", + "service_tier": "standard", + "context_band": "short" + } + ], + "excluded_requests": [], + "duplicates": [ + {"kind": "unchanged-legacy-state", "request_id": "legacy-state:ses-child:turn-child-review:7", "first": {"session_id": "ses-child", "line_number": 7}, "duplicate": {"session_id": "ses-child", "line_number": 8}}, + {"kind": "unchanged-legacy-state", "request_id": "legacy-state:ses-nested:turn-quality-check:13", "first": {"session_id": "ses-nested", "line_number": 13}, "duplicate": {"session_id": "ses-nested", "line_number": 14}} + ], + "issues": [], + "limitations": [ + {"kind": "legacy-state-identity", "request_id": "legacy-state:ses-root:turn-root-onboarding:4", "session_id": "ses-root", "line_number": 4, "turn_id": "turn-root-onboarding"}, + {"kind": "legacy-state-identity", "request_id": "legacy-state:ses-root:turn-root-audit:8", "session_id": "ses-root", "line_number": 8, "turn_id": "turn-root-audit"}, + {"kind": "legacy-state-identity", "request_id": "legacy-state:ses-root:turn-root-summary:11", "session_id": "ses-root", "line_number": 11, "turn_id": "turn-root-summary"}, + {"kind": "cache-write-input-excluded-by-formula", "request_id": "legacy-state:ses-root:turn-root-summary:11", "session_id": "ses-root", "line_number": 11, "cache_write_input_tokens": 40}, + {"kind": "legacy-state-identity", "request_id": "legacy-state:ses-child:turn-child-review:7", "session_id": "ses-child", "line_number": 7, "turn_id": "turn-child-review"}, + {"kind": "legacy-state-identity", "request_id": "legacy-state:ses-nested:turn-quality-check:13", "session_id": "ses-nested", "line_number": 13, "turn_id": "turn-quality-check"} + ], + "total_cost_usd": "0.009573" +} diff --git a/skills/wgo/references/fixtures/cost-estimation/frozen-manifest.json b/skills/wgo/references/fixtures/cost-estimation/frozen-manifest.json new file mode 100644 index 0000000..1c94863 --- /dev/null +++ b/skills/wgo/references/fixtures/cost-estimation/frozen-manifest.json @@ -0,0 +1,96 @@ +{ + "schema_version": 2, + "coverage": "audit", + "root_session_id": "ses-root", + "session_search_roots": ["fixture directory"], + "pricing_basis": { + "rate_card": "../../data/api-rate-card-2026-08-07.json", + "default_service_tier_when_not_returned": "standard", + "note": "Declared API-equivalent basis, not actual Codex backend tier." + }, + "phase_policy": { + "included": ["unattributed", "onboarding", "audit", "summary"], + "excluded": ["cost-estimation"], + "markers": { + "WGO_PHASE_ONBOARDING_START": "onboarding", + "WGO_PHASE_AUDIT_START": "audit", + "WGO_PHASE_SUMMARY_START": "summary", + "WGO_AUDIT_COMPLETE_COST_PHASE_STARTS": "cost-estimation", + "WGO_PHASE_OPERATIONALIZATION_START": "operationalization", + "WGO_OPERATIONALIZATION_COMPLETE_COST_PHASE_STARTS": "cost-estimation" + } + }, + "phase_boundaries": [ + {"phase": "onboarding", "marker": "WGO_PHASE_ONBOARDING_START", "session_id": "ses-root", "line_number": 2}, + {"phase": "audit", "marker": "WGO_PHASE_AUDIT_START", "session_id": "ses-root", "line_number": 5}, + {"phase": "summary", "marker": "WGO_PHASE_SUMMARY_START", "session_id": "ses-root", "line_number": 9}, + {"phase": "cost-estimation", "marker": "WGO_AUDIT_COMPLETE_COST_PHASE_STARTS", "session_id": "ses-root", "line_number": 12} + ], + "root_cutoff": {"marker": "WGO_AUDIT_COMPLETE_COST_PHASE_STARTS", "line_number": 12}, + "sessions": [ + { + "session_id": "ses-root", + "file_path": "root.jsonl", + "prefix_sha256": "d6f056039423e06c164e4c90c780590114af1e24405c2d95fefe81d11b9aca44", + "wgo_role_task_name": "Terra audit coordinator", + "phase": "from-markers", + "parent_session_id": null, + "root_relationship": "root", + "decision": "included", + "rationale": "Current audit root through the persisted WGO_AUDIT_COMPLETE_COST_PHASE_STARTS marker.", + "session_meta": {"line_number": 1, "session_id": "ses-root"}, + "lifecycle": null, + "cutoff": {"marker": "WGO_AUDIT_COMPLETE_COST_PHASE_STARTS", "line_number": 12}, + "provenance": [{"kind": "session_meta"}, {"kind": "sub_agent_activity", "event_id": "evt-spawn-code-quality"}], + "usage_schema": "codex-rollout-token-count-v1" + }, + { + "session_id": "ses-child", + "file_path": "child.jsonl", + "prefix_sha256": "d178ce20cf1f3d1c717a1b873c357944698ebe83f81a262a5454b1b8a6cd5933", + "wgo_role_task_name": "wgo reviewer: code-quality", + "phase": "audit", + "parent_session_id": "ses-root", + "root_relationship": "descendant", + "decision": "included", + "rationale": "Root sub_agent_activity and matching child thread_spawn, task_started, and task_complete records name ses-root and code-quality.", + "session_meta": {"line_number": 4, "session_id": "ses-child"}, + "lifecycle": { + "task_id": "task-code-quality", + "start": {"event": "task_started", "line_number": 5}, + "terminal": {"event": "task_complete", "outcome": "completed", "line_number": 10} + }, + "cutoff": {"provenance": "task_complete", "line_number": 10}, + "provenance": [{"kind": "sub_agent_activity"}, {"kind": "thread_spawn"}, {"kind": "task_started"}, {"kind": "task_complete"}], + "usage_schema": "codex-rollout-token-count-v1" + }, + { + "session_id": "ses-nested", + "file_path": "nested-child.jsonl", + "prefix_sha256": "46224cc928440cd85d8049a37d9399f53258c18072ca8a95b2974a2dd2846e1d", + "wgo_role_task_name": "code-quality artifact quality check", + "phase": "audit", + "parent_session_id": "ses-child", + "root_relationship": "descendant", + "decision": "included", + "rationale": "The included code-quality child records the nested spawn; matching nested thread_spawn, task_started, and task_complete records identify its bounded quality-check task.", + "session_meta": {"line_number": 10, "session_id": "ses-nested"}, + "lifecycle": { + "task_id": "task-quality-check", + "start": {"event": "task_started", "line_number": 11}, + "terminal": {"event": "task_complete", "outcome": "completed", "line_number": 15} + }, + "cutoff": {"provenance": "task_complete", "line_number": 15}, + "provenance": [{"kind": "sub_agent_activity"}, {"kind": "thread_spawn"}, {"kind": "task_started"}, {"kind": "task_complete"}], + "usage_schema": "codex-rollout-token-count-v1" + } + ], + "exclusions": [ + { + "session_id": "ses-unrelated", + "file_path": "unrelated.jsonl", + "decision": "excluded", + "rationale": "Same workspace/day but no recorded collaboration spawn and task-lifecycle path from ses-root." + } + ] +} diff --git a/skills/wgo/references/fixtures/cost-estimation/nested-child.jsonl b/skills/wgo/references/fixtures/cost-estimation/nested-child.jsonl new file mode 100644 index 0000000..d03c90d --- /dev/null +++ b/skills/wgo/references/fixtures/cost-estimation/nested-child.jsonl @@ -0,0 +1,17 @@ +{"timestamp":"2026-08-07T09:00:00Z","type":"session_meta","payload":{"id":"ses-root","session_id":"ses-root","cwd":"/same/workspace"}} +{"timestamp":"2026-08-07T09:02:30Z","type":"turn_context","payload":{"turn_id":"turn-root-audit","model":"gpt-5.6-terra","cwd":"/same/workspace"}} +{"timestamp":"2026-08-07T09:03:30Z","type":"event_msg","payload":{"type":"token_count","info":{"last_token_usage":{"input_tokens":600,"cached_input_tokens":100,"output_tokens":100,"reasoning_output_tokens":20,"total_tokens":700},"total_token_usage":{"input_tokens":1000,"cached_input_tokens":200,"output_tokens":200,"reasoning_output_tokens":40,"total_tokens":1200},"model_context_window":272000}}} +{"timestamp":"2026-08-07T09:03:01Z","type":"session_meta","payload":{"id":"ses-child","session_id":"ses-child","cwd":"/same/workspace","source":{"subagent":{"thread_spawn":{"parent_thread_id":"ses-root","agent_path":"reviewer/code-quality"}}}}} +{"timestamp":"2026-08-07T09:03:02Z","type":"event_msg","payload":{"type":"task_started","task_id":"task-code-quality","parent_thread_id":"ses-root","agent_path":"reviewer/code-quality"}} +{"timestamp":"2026-08-07T09:03:03Z","type":"turn_context","payload":{"turn_id":"turn-child-review","model":"gpt-5.6-luna","cwd":"/same/workspace"}} +{"timestamp":"2026-08-07T09:03:04Z","type":"event_msg","payload":{"type":"token_count","info":{"last_token_usage":{"input_tokens":500,"cached_input_tokens":100,"output_tokens":100,"reasoning_output_tokens":20,"total_tokens":600},"total_token_usage":{"input_tokens":1500,"cached_input_tokens":300,"output_tokens":300,"reasoning_output_tokens":60,"total_tokens":1800},"model_context_window":272000}}} +{"timestamp":"2026-08-07T09:03:04Z","type":"event_msg","payload":{"type":"token_count","info":{"last_token_usage":{"input_tokens":500,"cached_input_tokens":100,"output_tokens":100,"reasoning_output_tokens":20,"total_tokens":600},"total_token_usage":{"input_tokens":1500,"cached_input_tokens":300,"output_tokens":300,"reasoning_output_tokens":60,"total_tokens":1800},"model_context_window":272000}}} +{"timestamp":"2026-08-07T09:03:05Z","type":"event_msg","payload":{"type":"sub_agent_activity","event_id":"evt-spawn-quality-check","agent_thread_id":"ses-nested","agent_path":"reviewer/code-quality/quality-check","kind":"started"}} +{"timestamp":"2026-08-07T09:03:05Z","type":"session_meta","payload":{"id":"ses-nested","session_id":"ses-nested","cwd":"/same/workspace","source":{"subagent":{"thread_spawn":{"parent_thread_id":"ses-child","agent_path":"reviewer/code-quality/quality-check"}}}}} +{"timestamp":"2026-08-07T09:03:06Z","type":"event_msg","payload":{"type":"task_started","task_id":"task-quality-check","parent_thread_id":"ses-child","agent_path":"reviewer/code-quality/quality-check"}} +{"timestamp":"2026-08-07T09:03:07Z","type":"turn_context","payload":{"turn_id":"turn-quality-check","model":"gpt-5.6-luna","cwd":"/same/workspace"}} +{"timestamp":"2026-08-07T09:03:08Z","type":"event_msg","payload":{"type":"token_count","info":{"last_token_usage":{"input_tokens":200,"cached_input_tokens":50,"output_tokens":50,"reasoning_output_tokens":10,"total_tokens":250},"total_token_usage":{"input_tokens":1700,"cached_input_tokens":350,"output_tokens":350,"reasoning_output_tokens":70,"total_tokens":2050},"model_context_window":272000}}} +{"timestamp":"2026-08-07T09:03:08Z","type":"event_msg","payload":{"type":"token_count","info":{"last_token_usage":{"input_tokens":200,"cached_input_tokens":50,"output_tokens":50,"reasoning_output_tokens":10,"total_tokens":250},"total_token_usage":{"input_tokens":1700,"cached_input_tokens":350,"output_tokens":350,"reasoning_output_tokens":70,"total_tokens":2050},"model_context_window":272000}}} +{"timestamp":"2026-08-07T09:03:09Z","type":"event_msg","payload":{"type":"task_complete","task_id":"task-quality-check","parent_thread_id":"ses-child","agent_path":"reviewer/code-quality/quality-check"}} +{"timestamp":"2026-08-07T11:30:00Z","type":"turn_context","payload":{"turn_id":"turn-nested-unrelated-reuse","model":"gpt-5.6-luna","cwd":"/same/workspace"}} +{"timestamp":"2026-08-07T11:30:01Z","type":"event_msg","payload":{"type":"token_count","info":{"last_token_usage":{"input_tokens":8000,"cached_input_tokens":0,"output_tokens":800,"reasoning_output_tokens":0,"total_tokens":8800},"total_token_usage":{"input_tokens":9700,"cached_input_tokens":350,"output_tokens":1150,"reasoning_output_tokens":70,"total_tokens":10850},"model_context_window":272000}}} diff --git a/skills/wgo/references/fixtures/cost-estimation/operationalized-manifest.json b/skills/wgo/references/fixtures/cost-estimation/operationalized-manifest.json new file mode 100644 index 0000000..21e1608 --- /dev/null +++ b/skills/wgo/references/fixtures/cost-estimation/operationalized-manifest.json @@ -0,0 +1,102 @@ +{ + "schema_version": 2, + "coverage": "audit-and-operationalization", + "root_session_id": "ses-root", + "prior_manifest": { + "file_path": "frozen-manifest.json", + "sha256": "b3953038f722191b4e50099ca0a5ba9a4f9ee98a00ce369a3cfe66c823fd0b24" + }, + "session_search_roots": ["fixture directory"], + "pricing_basis": { + "rate_card": "../../data/api-rate-card-2026-08-07.json", + "default_service_tier_when_not_returned": "standard", + "note": "Declared API-equivalent basis, not actual Codex backend tier." + }, + "phase_policy": { + "included": ["unattributed", "onboarding", "audit", "summary", "operationalization"], + "excluded": ["cost-estimation"], + "markers": { + "WGO_PHASE_ONBOARDING_START": "onboarding", + "WGO_PHASE_AUDIT_START": "audit", + "WGO_PHASE_SUMMARY_START": "summary", + "WGO_AUDIT_COMPLETE_COST_PHASE_STARTS": "cost-estimation", + "WGO_PHASE_OPERATIONALIZATION_START": "operationalization", + "WGO_OPERATIONALIZATION_COMPLETE_COST_PHASE_STARTS": "cost-estimation" + } + }, + "phase_boundaries": [ + {"phase": "onboarding", "marker": "WGO_PHASE_ONBOARDING_START", "session_id": "ses-root", "line_number": 2}, + {"phase": "audit", "marker": "WGO_PHASE_AUDIT_START", "session_id": "ses-root", "line_number": 5}, + {"phase": "summary", "marker": "WGO_PHASE_SUMMARY_START", "session_id": "ses-root", "line_number": 9}, + {"phase": "cost-estimation", "marker": "WGO_AUDIT_COMPLETE_COST_PHASE_STARTS", "session_id": "ses-root", "line_number": 12}, + {"phase": "operationalization", "marker": "WGO_PHASE_OPERATIONALIZATION_START", "session_id": "ses-root", "line_number": 15}, + {"phase": "cost-estimation", "marker": "WGO_OPERATIONALIZATION_COMPLETE_COST_PHASE_STARTS", "session_id": "ses-root", "line_number": 18} + ], + "root_cutoff": {"marker": "WGO_OPERATIONALIZATION_COMPLETE_COST_PHASE_STARTS", "line_number": 18}, + "sessions": [ + { + "session_id": "ses-root", + "file_path": "root.jsonl", + "prefix_sha256": "a2b14dc414ab86713ae4372bfbe90561e25768a35407ca0db60fbc7a41f64348", + "wgo_role_task_name": "Terra audit coordinator", + "phase": "from-markers", + "parent_session_id": null, + "root_relationship": "root", + "decision": "included", + "rationale": "Current root through operationalization; the earlier cost-estimation phase is excluded by the frozen phase policy.", + "session_meta": {"line_number": 1, "session_id": "ses-root"}, + "lifecycle": null, + "cutoff": {"marker": "WGO_OPERATIONALIZATION_COMPLETE_COST_PHASE_STARTS", "line_number": 18}, + "provenance": [{"kind": "session_meta"}, {"kind": "sub_agent_activity", "event_id": "evt-spawn-code-quality"}], + "usage_schema": "codex-rollout-token-count-v1" + }, + { + "session_id": "ses-child", + "file_path": "child.jsonl", + "prefix_sha256": "d178ce20cf1f3d1c717a1b873c357944698ebe83f81a262a5454b1b8a6cd5933", + "wgo_role_task_name": "wgo reviewer: code-quality", + "phase": "audit", + "parent_session_id": "ses-root", + "root_relationship": "descendant", + "decision": "included", + "rationale": "Root sub_agent_activity and matching child thread_spawn, task_started, and task_complete records name ses-root and code-quality.", + "session_meta": {"line_number": 4, "session_id": "ses-child"}, + "lifecycle": { + "task_id": "task-code-quality", + "start": {"event": "task_started", "line_number": 5}, + "terminal": {"event": "task_complete", "outcome": "completed", "line_number": 10} + }, + "cutoff": {"provenance": "task_complete", "line_number": 10}, + "provenance": [{"kind": "sub_agent_activity"}, {"kind": "thread_spawn"}, {"kind": "task_started"}, {"kind": "task_complete"}], + "usage_schema": "codex-rollout-token-count-v1" + }, + { + "session_id": "ses-nested", + "file_path": "nested-child.jsonl", + "prefix_sha256": "46224cc928440cd85d8049a37d9399f53258c18072ca8a95b2974a2dd2846e1d", + "wgo_role_task_name": "code-quality artifact quality check", + "phase": "audit", + "parent_session_id": "ses-child", + "root_relationship": "descendant", + "decision": "included", + "rationale": "The included code-quality child records the nested spawn; matching nested thread_spawn, task_started, and task_complete records identify its bounded quality-check task.", + "session_meta": {"line_number": 10, "session_id": "ses-nested"}, + "lifecycle": { + "task_id": "task-quality-check", + "start": {"event": "task_started", "line_number": 11}, + "terminal": {"event": "task_complete", "outcome": "completed", "line_number": 15} + }, + "cutoff": {"provenance": "task_complete", "line_number": 15}, + "provenance": [{"kind": "sub_agent_activity"}, {"kind": "thread_spawn"}, {"kind": "task_started"}, {"kind": "task_complete"}], + "usage_schema": "codex-rollout-token-count-v1" + } + ], + "exclusions": [ + { + "session_id": "ses-unrelated", + "file_path": "unrelated.jsonl", + "decision": "excluded", + "rationale": "Same workspace/day but no recorded collaboration spawn and task-lifecycle path from ses-root." + } + ] +} diff --git a/skills/wgo/references/fixtures/cost-estimation/root.jsonl b/skills/wgo/references/fixtures/cost-estimation/root.jsonl new file mode 100644 index 0000000..abee72f --- /dev/null +++ b/skills/wgo/references/fixtures/cost-estimation/root.jsonl @@ -0,0 +1,20 @@ +{"timestamp":"2026-08-07T09:00:00Z","type":"session_meta","payload":{"id":"ses-root","session_id":"ses-root","cwd":"/same/workspace"}} +{"timestamp":"2026-08-07T09:00:30Z","type":"response_item","payload":{"type":"message","role":"assistant","content":[{"type":"output_text","text":"Starting onboarding. "}]}} +{"timestamp":"2026-08-07T09:01:00Z","type":"turn_context","payload":{"turn_id":"turn-root-onboarding","model":"gpt-5.6-terra","cwd":"/same/workspace"}} +{"timestamp":"2026-08-07T09:01:30Z","type":"event_msg","payload":{"type":"token_count","info":{"last_token_usage":{"input_tokens":400,"cached_input_tokens":100,"output_tokens":100,"reasoning_output_tokens":20,"total_tokens":500},"total_token_usage":{"input_tokens":400,"cached_input_tokens":100,"output_tokens":100,"reasoning_output_tokens":20,"total_tokens":500},"model_context_window":272000}}} +{"timestamp":"2026-08-07T09:02:00Z","type":"response_item","payload":{"type":"message","role":"assistant","content":[{"type":"output_text","text":"Starting reviewer execution. "}]}} +{"timestamp":"2026-08-07T09:02:30Z","type":"turn_context","payload":{"turn_id":"turn-root-audit","model":"gpt-5.6-terra","cwd":"/same/workspace"}} +{"timestamp":"2026-08-07T09:03:00Z","type":"event_msg","payload":{"type":"sub_agent_activity","event_id":"evt-spawn-code-quality","agent_thread_id":"ses-child","agent_path":"reviewer/code-quality","kind":"started"}} +{"timestamp":"2026-08-07T09:03:30Z","type":"event_msg","payload":{"type":"token_count","info":{"last_token_usage":{"input_tokens":600,"cached_input_tokens":100,"output_tokens":100,"reasoning_output_tokens":20,"total_tokens":700},"total_token_usage":{"input_tokens":1000,"cached_input_tokens":200,"output_tokens":200,"reasoning_output_tokens":40,"total_tokens":1200},"model_context_window":272000}}} +{"timestamp":"2026-08-07T09:04:00Z","type":"response_item","payload":{"type":"message","role":"assistant","content":[{"type":"output_text","text":"Starting synthesis. "}]}} +{"timestamp":"2026-08-07T09:04:30Z","type":"turn_context","payload":{"turn_id":"turn-root-summary","model":"gpt-5.6-terra","cwd":"/same/workspace"}} +{"timestamp":"2026-08-07T09:05:00Z","type":"event_msg","payload":{"type":"token_count","info":{"last_token_usage":{"input_tokens":1000,"cached_input_tokens":200,"output_tokens":300,"reasoning_output_tokens":80,"cache_write_input_tokens":40,"total_tokens":1300},"total_token_usage":{"input_tokens":2000,"cached_input_tokens":400,"output_tokens":500,"reasoning_output_tokens":120,"total_tokens":2500},"model_context_window":272000}}} +{"timestamp":"2026-08-07T09:05:30Z","type":"response_item","payload":{"type":"message","role":"assistant","content":[{"type":"output_text","text":"Synthesis validated; starting cost estimation. "}]}} +{"timestamp":"2026-08-07T09:06:00Z","type":"turn_context","payload":{"turn_id":"turn-cost-phase","model":"gpt-5.6-terra","cwd":"/same/workspace"}} +{"timestamp":"2026-08-07T09:06:30Z","type":"event_msg","payload":{"type":"token_count","info":{"last_token_usage":{"input_tokens":999,"cached_input_tokens":0,"output_tokens":99,"reasoning_output_tokens":0,"total_tokens":1098},"total_token_usage":{"input_tokens":2999,"cached_input_tokens":400,"output_tokens":599,"reasoning_output_tokens":120,"total_tokens":3598},"model_context_window":272000}}} +{"timestamp":"2026-08-07T09:07:00Z","type":"response_item","payload":{"type":"message","role":"assistant","content":[{"type":"output_text","text":"Starting operationalization. "}]}} +{"timestamp":"2026-08-07T09:07:30Z","type":"turn_context","payload":{"turn_id":"turn-operationalization","model":"gpt-5.6-terra","cwd":"/same/workspace"}} +{"timestamp":"2026-08-07T09:08:00Z","type":"event_msg","payload":{"type":"token_count","info":{"last_token_usage":{"input_tokens":500,"cached_input_tokens":100,"output_tokens":100,"reasoning_output_tokens":20,"total_tokens":600},"total_token_usage":{"input_tokens":3499,"cached_input_tokens":500,"output_tokens":699,"reasoning_output_tokens":140,"total_tokens":4198},"model_context_window":272000}}} +{"timestamp":"2026-08-07T09:08:30Z","type":"response_item","payload":{"type":"message","role":"assistant","content":[{"type":"output_text","text":"Operationalization complete; refreshing cost estimate. "}]}} +{"timestamp":"2026-08-07T09:09:00Z","type":"turn_context","payload":{"turn_id":"turn-cost-refresh","model":"gpt-5.6-terra","cwd":"/same/workspace"}} +{"timestamp":"2026-08-07T09:09:30Z","type":"event_msg","payload":{"type":"token_count","info":{"last_token_usage":{"input_tokens":888,"cached_input_tokens":0,"output_tokens":88,"reasoning_output_tokens":0,"total_tokens":976},"total_token_usage":{"input_tokens":4387,"cached_input_tokens":500,"output_tokens":787,"reasoning_output_tokens":140,"total_tokens":5174},"model_context_window":272000}}} diff --git a/skills/wgo/references/fixtures/cost-estimation/unrelated.jsonl b/skills/wgo/references/fixtures/cost-estimation/unrelated.jsonl new file mode 100644 index 0000000..0d6915c --- /dev/null +++ b/skills/wgo/references/fixtures/cost-estimation/unrelated.jsonl @@ -0,0 +1,3 @@ +{"timestamp":"2026-08-07T12:00:00Z","type":"session_meta","payload":{"id":"ses-unrelated","session_id":"ses-unrelated","cwd":"/same/workspace"}} +{"timestamp":"2026-08-07T12:00:01Z","type":"turn_context","payload":{"turn_id":"turn-unrelated","model":"gpt-5.6-terra","cwd":"/same/workspace"}} +{"timestamp":"2026-08-07T12:00:02Z","type":"event_msg","payload":{"type":"token_count","info":{"last_token_usage":{"input_tokens":9000,"cached_input_tokens":0,"output_tokens":900,"reasoning_output_tokens":0,"total_tokens":9900},"total_token_usage":{"input_tokens":9000,"cached_input_tokens":0,"output_tokens":900,"reasoning_output_tokens":0,"total_tokens":9900},"model_context_window":272000}}} diff --git a/skills/wgo/references/reviewers/architecture/reviewer.md b/skills/wgo/references/reviewers/architecture/reviewer.md index 94f932a..946e902 100644 --- a/skills/wgo/references/reviewers/architecture/reviewer.md +++ b/skills/wgo/references/reviewers/architecture/reviewer.md @@ -2,7 +2,7 @@ id: architecture name: Architecture summary: Is the current technical boundary and its material decisions understood well enough for safe change? -version: 0.1 +version: 0.2 codegraph: required --- diff --git a/skills/wgo/references/reviewers/business-continuity/reviewer.md b/skills/wgo/references/reviewers/business-continuity/reviewer.md index 2112208..bdd0bb9 100644 --- a/skills/wgo/references/reviewers/business-continuity/reviewer.md +++ b/skills/wgo/references/reviewers/business-continuity/reviewer.md @@ -2,7 +2,7 @@ id: business-continuity name: Business Continuity summary: Can the company demo, deploy, operate, recover, and transfer control if a person, vendor, account, or environment disappears? -version: 0.1 +version: 0.2 codegraph: optional depends_on: - architecture diff --git a/skills/wgo/references/reviewers/code-quality/reviewer.md b/skills/wgo/references/reviewers/code-quality/reviewer.md index 5254e71..94b73c5 100644 --- a/skills/wgo/references/reviewers/code-quality/reviewer.md +++ b/skills/wgo/references/reviewers/code-quality/reviewer.md @@ -2,7 +2,7 @@ id: code-quality name: Code Quality summary: Which code-level risks materially affect correctness, delivery, maintainability, security, or product promises? -version: 0.1 +version: 0.2 codegraph: required depends_on: - architecture @@ -57,9 +57,12 @@ collectors do not invoke CodeGraph. ## Completion Criteria State the declared-gate inventory, exact executed and unexecuted check -boundaries, quality evidence, material test-fixture contract drift, and selected -outputs. For any green suite, state the critical paths it did and did not cover. -Separate source inspection from executed behavior. +boundaries, and coverage position as `measured`, brief-authorized `declined`, or +`blocked`. State material fixture provenance as `production-generated`, +`independently-built`, or `unknown`, including duplicated production contracts, +contract drift, quality evidence, and selected outputs. For any green suite, +state the critical paths it did and did not cover. Separate source inspection +from executed behavior. ## Escalation Conditions diff --git a/skills/wgo/references/reviewers/contributor-vendor-value/reviewer.md b/skills/wgo/references/reviewers/contributor-vendor-value/reviewer.md index 554aaef..236d572 100644 --- a/skills/wgo/references/reviewers/contributor-vendor-value/reviewer.md +++ b/skills/wgo/references/reviewers/contributor-vendor-value/reviewer.md @@ -2,7 +2,7 @@ id: contributor-vendor-value name: Contributor and Vendor Value summary: What usable output, ownership, knowledge, handoff, and cost-relative value did people or vendors provide? -version: 0.1 +version: 0.2 codegraph: none depends_on: - product-value diff --git a/skills/wgo/references/reviewers/expense-exposure/reviewer.md b/skills/wgo/references/reviewers/expense-exposure/reviewer.md index d6e43f4..79f8b5a 100644 --- a/skills/wgo/references/reviewers/expense-exposure/reviewer.md +++ b/skills/wgo/references/reviewers/expense-exposure/reviewer.md @@ -2,7 +2,7 @@ id: expense-exposure name: Expense Exposure summary: What actual or potential cash exposure comes from infrastructure, software, staffing, commitments, and failure modes? -version: 0.1 +version: 0.2 codegraph: none depends_on: - architecture diff --git a/skills/wgo/references/reviewers/maintenance-cost/reviewer.md b/skills/wgo/references/reviewers/maintenance-cost/reviewer.md index 7fa7b82..4b37881 100644 --- a/skills/wgo/references/reviewers/maintenance-cost/reviewer.md +++ b/skills/wgo/references/reviewers/maintenance-cost/reviewer.md @@ -2,7 +2,7 @@ id: maintenance-cost name: Maintenance Cost summary: What skill mix, effort, operating burden, and change risk will a small replacement team face? -version: 0.1 +version: 0.2 codegraph: optional depends_on: - architecture diff --git a/skills/wgo/references/reviewers/product-value/reviewer.md b/skills/wgo/references/reviewers/product-value/reviewer.md index 5b645cb..3513fc9 100644 --- a/skills/wgo/references/reviewers/product-value/reviewer.md +++ b/skills/wgo/references/reviewers/product-value/reviewer.md @@ -2,7 +2,7 @@ id: product-value name: Product Value summary: What customer and business value is demonstrably implemented, partial, promised, or awaiting sign-off? -version: 0.1 +version: 0.2 codegraph: required depends_on: - architecture diff --git a/skills/wgo/references/reviewers/project-health/reviewer.md b/skills/wgo/references/reviewers/project-health/reviewer.md index 5fb06db..3471307 100644 --- a/skills/wgo/references/reviewers/project-health/reviewer.md +++ b/skills/wgo/references/reviewers/project-health/reviewer.md @@ -2,7 +2,7 @@ id: project-health name: Project Health summary: Can a small team understand, prioritize, review, accept, release, and learn from the work? -version: 0.1 +version: 0.2 codegraph: none depends_on: - code-quality @@ -44,7 +44,9 @@ and Contributor/Vendor Value. Do not infer a healthy cadence from a few commits. ## Completion Criteria State the evidence-bounded delivery position, material gaps, and selected -outputs. +outputs. When documentation is material, state its audience/task coverage, +currency, and conflicts; route each conflict to the reviewer owning its +consequence. ## Escalation Conditions diff --git a/skills/wgo/references/reviewers/revenue-risk/reviewer.md b/skills/wgo/references/reviewers/revenue-risk/reviewer.md index c9fc5c6..ec587d8 100644 --- a/skills/wgo/references/reviewers/revenue-risk/reviewer.md +++ b/skills/wgo/references/reviewers/revenue-risk/reviewer.md @@ -2,7 +2,7 @@ id: revenue-risk name: Revenue Risk summary: What could interrupt demos, sales, pilots, onboarding, renewals, expansion, trust, or customer delivery? -version: 0.1 +version: 0.2 codegraph: none depends_on: - product-value diff --git a/skills/wgo/references/reviewers/scalability/reviewer.md b/skills/wgo/references/reviewers/scalability/reviewer.md index 63fb3eb..fd41450 100644 --- a/skills/wgo/references/reviewers/scalability/reviewer.md +++ b/skills/wgo/references/reviewers/scalability/reviewer.md @@ -2,7 +2,7 @@ id: scalability name: Scalability summary: Does the product support its realistic business growth envelope across workload, data, operations, third parties, and cost? -version: 0.1 +version: 0.2 codegraph: optional depends_on: - architecture diff --git a/skills/wgo/references/reviewers/security-privacy/workers/identity-secrets-data-boundaries.md b/skills/wgo/references/reviewers/security-privacy/workers/identity-secrets-data-boundaries.md index 646cdae..272bf96 100644 --- a/skills/wgo/references/reviewers/security-privacy/workers/identity-secrets-data-boundaries.md +++ b/skills/wgo/references/reviewers/security-privacy/workers/identity-secrets-data-boundaries.md @@ -3,8 +3,7 @@ Scope: authentication/authorization, service identity, secret consumers/metadata Use the reviewer’s topology packet; do not invoke CodeGraph or shared collectors. Read relevant routes, middleware, IAM/configuration declarations, schemas, data models, and tests directly. Trace each material boundary from identity or data ingress through authorization, storage, consumer, and revocation/deletion path. -For material signatures, checksums, or provenance references, trace the trust material to its consumer validation; record a missing or unclear verifier. -Exclude edge/network/TLS/WAF/public-route configuration. +Exclude edge/network/TLS/WAF/public-route configuration and produced-artifact trust-anchor verification. Verify every result in source and give an exact file and line/symbol locator. Separate source declaration, effective state, observed behavior, approval, and unknown. Never reveal secret values or infer compromise, compliance, live privilege, ownership, or rotation success. diff --git a/skills/wgo/references/templates/cost-estimate-template.md b/skills/wgo/references/templates/cost-estimate-template.md new file mode 100644 index 0000000..b75c052 --- /dev/null +++ b/skills/wgo/references/templates/cost-estimate-template.md @@ -0,0 +1,74 @@ +# Cost Estimate Template + +Replace prompts in the completed control. Preserve exact costs in the frozen +machine-readable evidence. In this Markdown control, display every priced +monetary amount as `$X.XX`, rounded half up from its exact value. Round the +exact total independently; never sum already-rounded display rows. Use +`unpriced`, not `$0.00`, when a rate is unavailable. A priced amount below half +a cent may display as `$0.00`; link its exact machine-readable value. + +```markdown +# API-Equivalent Audit Cost Estimate + +| Field | Value | +|---|---| +| Coverage | audit or audit-and-operationalization | +| Reconciliation status | Final or Unreconciled | +| Rate-card/basis date | | +| Currency | USD | +| Pricing basis | Provider-specific API-equivalent estimate; not a provider invoice | + +## Pricing Basis And Dated Data + +State the provider, dated source, exact formula or recorded-cost rule, and the +service tier/context/geography where applicable. Display each priced component +and calculated cost as `$X.XX`; preserve exact decimals in the linked +calculation evidence. + +## Frozen Manifest And Exclusions + +Link the applicable manifest and both independent verification results. For +operationalized coverage, also link the preserved audit-only manifest. + +### Phase Boundaries + +| Phase | Session | Marker and record boundary | Included or excluded | +|---|---|---|---| + +### Session And Request Exclusions + +| Session/request | Phase | Rationale | +|---|---|---| + +## Token Totals By Session And Model + +| Phase | WGO role/task | Session | Model/provider | Service tier/basis | Uncached/new input | Cache read | Cache write | Cache-write detail | Output | Reasoning (informational) | Cost | +|---|---|---|---|---|---:|---:|---:|---|---:|---:|---:| + +Add one subtotal row per phase. State whether the provider reports reasoning as +part of output or separately. Reasoning is informational and is never added a +second time to billable output or a provider-recorded cost. For Claude, split +cache writes into 5-minute and 1-hour tokens in `Cache-write detail`; for +OpenCode, state that its export does not expose the TTL split. Display priced +costs as `$X.XX` from exact evidence. + +## Model-By-Model Cost + +| Model/provider | Service tier/context | Rate components or recorded-cost basis | Exact-evidence status | Displayed cost | +|---|---|---|---|---:| +| **Total** | | | | **$X.XX** | + +Calculate the displayed total by rounding the exact reconciled total, not by +adding displayed rows. Omit the total when the result is unreconciled. + +## Reconciliation Status + +State `Final` or `Unreconciled`, the two-pass comparison result, and every +disputed session/event when applicable. + +## Limitations + +State that this is an API-equivalent estimate rather than a Codex, Claude, or +OpenCode/provider invoice and that cost-calculation requests are excluded. List +material evidence, schema, pricing, attribution, and rounding limitations. +``` diff --git a/tests/test_installers.py b/tests/test_installers.py index c7cda0b..249a79d 100644 --- a/tests/test_installers.py +++ b/tests/test_installers.py @@ -14,17 +14,53 @@ class InstallerTests(unittest.TestCase): - def test_plugin_identity_and_skill_folder_are_wgo(self) -> None: - manifest = json.loads((ROOT / ".codex-plugin/plugin.json").read_text(encoding="utf-8")) - self.assertEqual("wgo", manifest["name"]) + def test_provider_manifests_share_the_wgo_identity_and_core_version(self) -> None: + codex_manifest = json.loads( + (ROOT / ".codex-plugin/plugin.json").read_text(encoding="utf-8") + ) + claude_manifest = json.loads( + (ROOT / ".claude-plugin/plugin.json").read_text(encoding="utf-8") + ) + self.assertEqual("wgo", codex_manifest["name"]) + self.assertEqual("wgo", claude_manifest["name"]) + self.assertEqual( + codex_manifest["version"].split("+", maxsplit=1)[0], + claude_manifest["version"].split("+", maxsplit=1)[0], + ) self.assertTrue((ROOT / "skills/wgo/SKILL.md").is_file()) - def test_claude_command_names_are_documented_as_slash_commands(self) -> None: + def test_provider_command_names_are_documented(self) -> None: readme = (ROOT / "README.md").read_text(encoding="utf-8") + onboarding_docs = (ROOT / "docs/onboarding-expectations.md").read_text( + encoding="utf-8" + ) installer = (ROOT / "install.sh").read_text(encoding="utf-8") - self.assertIn("`/wgo_onboard`", readme) - self.assertIn("`/wgo_operationalize`", readme) - self.assertIn("In Claude, run /wgo_onboard", installer) + self.assertIn("`/wgo:onboard`", readme) + self.assertIn("`/wgo:operationalize`", readme) + self.assertIn("`/wgo-onboard`", readme) + self.assertIn("`/wgo-operationalize`", readme) + self.assertIn("In Claude, run /wgo:onboard", installer) + self.assertIn("In OpenCode, run /wgo-onboard", installer) + for command in ( + "/wgo:onboard compare [YYYYMMDD]", + "/wgo:onboard blind-compare [YYYYMMDD]", + "/wgo-onboard compare [YYYYMMDD]", + "/wgo-onboard blind-compare [YYYYMMDD]", + "/wgo-audit all", + ): + self.assertIn(command, onboarding_docs) + self.assertNotIn("/wgo_onboard", onboarding_docs) + + def test_canonical_frontmatter_contains_both_provider_contracts(self) -> None: + skill = (ROOT / "skills/wgo/SKILL.md").read_text(encoding="utf-8") + onboard = (ROOT / "commands/onboard.md").read_text(encoding="utf-8") + + self.assertIn("when_to_use:", skill) + self.assertIn("user-invocable: false", skill) + self.assertIn('args: "[compare|blind-compare] [YYYYMMDD]"', onboard) + self.assertIn("skills: wgo", onboard) + self.assertIn('argument-hint: "[compare|blind-compare] [YYYYMMDD]"', onboard) + self.assertIn("disable-model-invocation: true", onboard) def test_shell_installer_copies_wgo_and_installs_pymupdf4llm_after_consent(self) -> None: with tempfile.TemporaryDirectory() as temp: @@ -34,6 +70,12 @@ def test_shell_installer_copies_wgo_and_installs_pymupdf4llm_after_consent(self) python_log = temp_path / "python.log" target.mkdir() bin_dir.mkdir() + legacy_command = target / ".claude/commands/wgo_onboard.md" + legacy_command.parent.mkdir(parents=True) + legacy_command.write_text("legacy command\n", encoding="utf-8") + legacy_skill = target / ".claude/skills/wgo/SKILL.md" + legacy_skill.parent.mkdir(parents=True) + legacy_skill.write_text("legacy skill\n", encoding="utf-8") fake_python = bin_dir / "python3" fake_python.write_text( '#!/usr/bin/env sh\nprintf "%s\\n" "$*" >> "$PYTHON_LOG"\n' @@ -57,6 +99,18 @@ def test_shell_installer_copies_wgo_and_installs_pymupdf4llm_after_consent(self) self.assertEqual(0, result.returncode, result.stderr) self.assertTrue((target / "plugins/wgo/skills/wgo/SKILL.md").is_file()) + codex_skill = (target / "plugins/wgo/skills/wgo/SKILL.md").read_text( + encoding="utf-8" + ) + codex_onboard = (target / "plugins/wgo/commands/onboard.md").read_text( + encoding="utf-8" + ) + self.assertNotIn("when_to_use:", codex_skill) + self.assertNotIn("user-invocable:", codex_skill) + self.assertIn("args:", codex_onboard) + self.assertIn("skills:", codex_onboard) + self.assertNotIn("argument-hint:", codex_onboard) + self.assertNotIn("disable-model-invocation:", codex_onboard) self.assertTrue( ( target @@ -75,6 +129,68 @@ def test_shell_installer_copies_wgo_and_installs_pymupdf4llm_after_consent(self) / "plugins/wgo/skills/wgo/references/common/reviewer-authoring.md" ).is_file() ) + self.assertTrue( + ( + target + / "plugins/wgo/skills/wgo/references/common/cost-estimation-claude.md" + ).is_file() + ) + self.assertTrue( + ( + target + / "plugins/wgo/skills/wgo/references/common/cost-estimation-opencode.md" + ).is_file() + ) + claude_plugin = target / ".claude/skills/wgo-claude" + self.assertTrue((claude_plugin / ".claude-plugin/plugin.json").is_file()) + self.assertTrue((claude_plugin / "SKILL.md").is_file()) + self.assertTrue((claude_plugin / "commands/onboard.md").is_file()) + self.assertTrue( + (claude_plugin / "references/common/reviewer-contract.md").is_file() + ) + self.assertTrue( + (claude_plugin / "references/common/cost-estimation-claude.md").is_file() + ) + claude_skill = (claude_plugin / "SKILL.md").read_text(encoding="utf-8") + claude_onboard = (claude_plugin / "commands/onboard.md").read_text( + encoding="utf-8" + ) + self.assertIn("when_to_use:", claude_skill) + self.assertIn("user-invocable: false", claude_skill) + self.assertNotIn("args:", claude_onboard) + self.assertNotIn("skills:", claude_onboard) + self.assertIn("argument-hint:", claude_onboard) + self.assertIn("disable-model-invocation: true", claude_onboard) + opencode_onboard_path = target / ".opencode/commands/wgo-onboard.md" + self.assertTrue(opencode_onboard_path.is_file()) + opencode_onboard = opencode_onboard_path.read_text(encoding="utf-8") + self.assertIn("description:", opencode_onboard) + self.assertNotIn("\nname:", opencode_onboard) + self.assertNotIn("\nargs:", opencode_onboard) + self.assertNotIn("\nskills:", opencode_onboard) + self.assertNotIn("\nargument-hint:", opencode_onboard) + self.assertNotIn("\ndisable-model-invocation:", opencode_onboard) + self.assertIn("OpenCode command arguments: `$ARGUMENTS`.", opencode_onboard) + self.assertIn("read `.opencode/skills/wgo/SKILL.md` directly", opencode_onboard) + self.assertIn("Load and use the WGO skill.", opencode_onboard) + opencode_skill_path = target / ".opencode/skills/wgo/SKILL.md" + self.assertTrue(opencode_skill_path.is_file()) + opencode_skill = opencode_skill_path.read_text(encoding="utf-8") + self.assertIn("\nname: wgo", opencode_skill) + self.assertIn("\ndescription:", opencode_skill) + self.assertNotIn("\nwhen_to_use:", opencode_skill) + self.assertNotIn("\nuser-invocable:", opencode_skill) + self.assertTrue( + (target / ".opencode/skills/wgo/references/common/reviewer-contract.md").is_file() + ) + self.assertTrue( + ( + target + / ".opencode/skills/wgo/references/common/cost-estimation-opencode.md" + ).is_file() + ) + self.assertFalse(legacy_command.exists()) + self.assertFalse(legacy_skill.exists()) self.assertIn(f"-m pip install --user {PYMUPDF4LLM}", python_log.read_text(encoding="utf-8")) def test_shell_installer_skips_every_available_optional_tool(self) -> None: @@ -123,6 +239,12 @@ def test_windows_installer_has_the_same_optional_tool_contract(self) -> None: self.assertIn("JohnMacFarlane.Pandoc", content) self.assertIn("colbymchenry/codegraph/main/install.ps1", content) self.assertIn(f'-m pip install --user "%PYMUPDF4LLM_PACKAGE%"', content) + self.assertIn("scripts\\filter-frontmatter.ps1", content) + self.assertIn(".claude-plugin\\plugin.json", content) + self.assertIn("/wgo:onboard", content) + self.assertIn(".opencode\\commands", content) + self.assertIn("/wgo-onboard", content) + self.assertIn("opencode", (ROOT / "scripts/filter-frontmatter.ps1").read_text(encoding="utf-8")) self.assertNotIn("astral.sh/uv", content) def test_shell_installer_checks_before_each_optional_install(self) -> None: diff --git a/tests/test_skill_contract.py b/tests/test_skill_contract.py index 923636d..07316ac 100644 --- a/tests/test_skill_contract.py +++ b/tests/test_skill_contract.py @@ -1,5 +1,6 @@ from __future__ import annotations +import json import re import subprocess import sys @@ -17,9 +18,6 @@ "product-value", "project-health", "revenue-risk", "scalability", "security-privacy", } -EXPECTED_REVIEWER_VERSIONS = { - "security-privacy": "0.2", -} def reviewer_card(reviewer_id: str) -> Path: @@ -47,6 +45,47 @@ def frontmatter_dependencies(path: Path) -> list[str]: return dependencies +def run_provider_cost_fixture( + workflow_name: str, + fixture_name: str, + basis_name: str, + manifest_name: str = "frozen-manifest.json", +) -> dict[str, object]: + workflow = (SKILL / f"references/common/{workflow_name}").read_text(encoding="utf-8") + match = re.search( + r"```sh\npython3 - .*? <<'PY'\n(?P.*?)\nPY\n```", + workflow, + re.DOTALL, + ) + if not match: + raise AssertionError("portable cost recipe not found") + fixture = SKILL / f"references/fixtures/{fixture_name}" + result = subprocess.run( + [ + sys.executable, + "-", + str(fixture / manifest_name), + str(SKILL / f"references/data/{basis_name}"), + ], + input=match.group("recipe"), + text=True, + capture_output=True, + check=False, + ) + if result.returncode: + raise AssertionError(result.stderr) + return json.loads(result.stdout) + + +def run_cost_fixture(manifest_name: str) -> dict[str, object]: + return run_provider_cost_fixture( + "cost-estimation.md", + "cost-estimation", + "api-rate-card-2026-08-07.json", + manifest_name, + ) + + class SkillContractTests(unittest.TestCase): def test_skill_routes_a_lean_layout(self) -> None: content = (SKILL / "SKILL.md").read_text(encoding="utf-8") @@ -73,8 +112,7 @@ def test_every_reviewer_has_a_compact_output_menu(self) -> None: for reviewer_id, path in files.items(): content = path.read_text(encoding="utf-8") self.assertRegex(content, rf"(?m)^id: {re.escape(reviewer_id)}$") - expected_version = EXPECTED_REVIEWER_VERSIONS.get(reviewer_id, "0.1") - self.assertRegex(content, rf"(?m)^version: {re.escape(expected_version)}$") + self.assertRegex(content, r"(?m)^version: 0\.2$") self.assertRegex(content, r"(?m)^codegraph: (?:none|optional|required)$") dependencies = set(frontmatter_dependencies(path)) self.assertLessEqual(dependencies, EXPECTED_REVIEWERS) @@ -253,7 +291,8 @@ def test_documentation_catalog_uses_only_the_active_audit_platform(self) -> None skill = (SKILL / "SKILL.md").read_text(encoding="utf-8") for content in (onboarding, preparation): - self.assertIn("gpt-5.6-luna", content) + self.assertIn("gpt-5.6-terra", content) + self.assertIn("high reasoning", content) self.assertIn("Sonnet 5", content) self.assertIn("gemini-3.5-flash-lite", content) self.assertIn("active audit model", content) @@ -268,6 +307,10 @@ def test_documentation_catalog_uses_only_the_active_audit_platform(self) -> None "active audit platform, and chosen same-platform catalog model", " ".join(skill.split()), ) + self.assertIn( + "every delegated WGO worker uses `gpt-5.6-terra` at high reasoning", + " ".join(skill.split()), + ) def test_audit_without_a_reviewer_runs_the_resolved_dependency_graph(self) -> None: command = (ROOT / "commands/audit.md").read_text(encoding="utf-8") @@ -364,6 +407,184 @@ def test_audit_flow_confirms_then_summarizes_then_confirms_operationalization(se self.assertIn("Should I proceed with\nwgo:operationalize?", summary) self.assertIn("Should I proceed with wgo:operationalize?", synthesis) + def test_cost_phases_are_attributed_and_operationalization_refreshes_the_estimate(self) -> None: + onboarding = (SKILL / "references/common/onboarding.md").read_text(encoding="utf-8") + audit = (SKILL / "references/common/reviewer-audit.md").read_text(encoding="utf-8") + synthesis = (SKILL / "references/common/synthesis.md").read_text(encoding="utf-8") + operationalization = ( + SKILL / "references/common/operationalization.md" + ).read_text(encoding="utf-8") + cost = (SKILL / "references/common/cost-estimation.md").read_text(encoding="utf-8") + cost_template = ( + SKILL / "references/templates/cost-estimate-template.md" + ).read_text(encoding="utf-8") + skill = (SKILL / "SKILL.md").read_text(encoding="utf-8") + command = (ROOT / "commands/operationalize.md").read_text(encoding="utf-8") + + self.assertIn("WGO_PHASE_ONBOARDING_START", onboarding) + self.assertIn("WGO_PHASE_AUDIT_START", audit) + self.assertIn("WGO_PHASE_SUMMARY_START", synthesis) + self.assertIn("WGO_AUDIT_COMPLETE_COST_PHASE_STARTS", synthesis) + self.assertIn("WGO_PHASE_OPERATIONALIZATION_START", operationalization) + self.assertIn( + "WGO_OPERATIONALIZATION_COMPLETE_COST_PHASE_STARTS", + operationalization, + ) + self.assertIn("cost-manifest-operationalized.json", cost) + self.assertIn('"excluded": ["cost-estimation"]', cost) + self.assertIn("phase subtotals", cost) + self.assertIn("cost-estimate-template.md", cost) + self.assertIn("cost-estimate-template.md", skill) + self.assertIn("display every priced", cost_template) + self.assertIn("`$X.XX`", cost_template) + self.assertIn("rounded half up", cost_template) + self.assertIn("never sum already-rounded display rows", cost_template) + self.assertIn("Use `unpriced`, not `$0.00`", " ".join(cost_template.split())) + self.assertIn("## Token Totals By Session And Model", cost_template) + self.assertIn("Update\n `controls/cost-estimate.md`", command) + self.assertIn("preserve the frozen\naudit-only manifest", operationalization) + + def test_cost_fixture_bounds_full_history_and_suppresses_unchanged_echoes(self) -> None: + fixture = SKILL / "references/fixtures/cost-estimation" + for manifest_name, expected_name in ( + ("frozen-manifest.json", "expected-result.json"), + ("operationalized-manifest.json", "expected-operationalized-result.json"), + ): + actual = run_cost_fixture(manifest_name) + expected = json.loads((fixture / expected_name).read_text(encoding="utf-8")) + self.assertEqual(expected, actual) + self.assertFalse(actual["issues"]) + self.assertEqual( + {"ses-child", "ses-nested"}, + {item["first"]["session_id"] for item in actual["duplicates"]}, + ) + self.assertTrue( + all(item["kind"] == "unchanged-legacy-state" + for item in actual["duplicates"]) + ) + + manifest = json.loads( + (fixture / "frozen-manifest.json").read_text(encoding="utf-8") + ) + descendants = { + session["session_id"]: session + for session in manifest["sessions"] + if session["root_relationship"] == "descendant" + } + self.assertEqual({"ses-child", "ses-nested"}, set(descendants)) + self.assertEqual(5, descendants["ses-child"]["lifecycle"]["start"]["line_number"]) + self.assertEqual(10, descendants["ses-child"]["lifecycle"]["terminal"]["line_number"]) + self.assertEqual(11, descendants["ses-nested"]["lifecycle"]["start"]["line_number"]) + self.assertEqual(15, descendants["ses-nested"]["lifecycle"]["terminal"]["line_number"]) + child_first = json.loads( + (fixture / "child.jsonl").read_text(encoding="utf-8").splitlines()[0] + ) + self.assertEqual("ses-root", child_first["payload"]["id"]) + self.assertEqual( + "ses-child", descendants["ses-child"]["session_meta"]["session_id"] + ) + self.assertEqual( + {"ses-unrelated"}, + {session["session_id"] for session in manifest["exclusions"]}, + ) + + def test_cost_command_routes_to_distinct_provider_auditors(self) -> None: + command = (ROOT / "commands/cost.md").read_text(encoding="utf-8") + skill = (SKILL / "SKILL.md").read_text(encoding="utf-8") + workflows = { + "Codex": "cost-estimation.md", + "Claude": "cost-estimation-claude.md", + "OpenCode": "cost-estimation-opencode.md", + } + + for provider, workflow_name in workflows.items(): + self.assertIn(f"{provider}: `references/common/{workflow_name}`", command) + self.assertTrue((SKILL / f"references/common/{workflow_name}").is_file()) + self.assertIn(workflow_name, skill) + self.assertIn("missing, ambiguous, or\nunsupported", command) + self.assertIn("`Unreconciled`", command) + + def test_claude_cost_fixture_collapses_progressive_requests(self) -> None: + actual = run_provider_cost_fixture( + "cost-estimation-claude.md", + "cost-estimation-claude", + "anthropic-api-rate-card-2026-08-07.json", + ) + + self.assertFalse(actual["issues"]) + self.assertEqual("0.007645", actual["total_cost_usd"]) + self.assertEqual( + {"req-summary", "req-child"}, + {duplicate["request_id"] for duplicate in actual["duplicates"]}, + ) + self.assertEqual(1750, sum(row["input_tokens"] for row in actual["rows"])) + self.assertEqual( + {"claude-root", "claude-root:agent:worker1"}, + {row["record_set_id"] for row in actual["rows"]}, + ) + fixture = SKILL / "references/fixtures/cost-estimation-claude" + manifest = json.loads((fixture / "frozen-manifest.json").read_text(encoding="utf-8")) + self.assertEqual( + {"claude-unrelated"}, + {item["record_set_id"] for item in manifest["exclusions"]}, + ) + + def test_opencode_cost_fixture_uses_messages_not_session_aggregates(self) -> None: + actual = run_provider_cost_fixture( + "cost-estimation-opencode.md", + "cost-estimation-opencode", + "opencode-cost-basis-2026-08-07.json", + ) + + self.assertFalse(actual["issues"]) + self.assertFalse(actual["duplicates"]) + self.assertEqual("0.00790", actual["total_cost_usd"]) + self.assertEqual(1750, sum(row["input_tokens"] for row in actual["rows"])) + self.assertEqual( + {"ses-root", "ses-child"}, + {row["session_id"] for row in actual["rows"]}, + ) + message_ids = { + message_id for row in actual["rows"] for message_id in row["message_ids"] + } + self.assertNotIn("msg-cost-work", message_ids) + self.assertNotIn("msg-unrelated", message_ids) + fixture = SKILL / "references/fixtures/cost-estimation-opencode" + manifest = json.loads((fixture / "frozen-manifest.json").read_text(encoding="utf-8")) + self.assertEqual( + {"ses-unrelated"}, + {item["session_id"] for item in manifest["exclusions"]}, + ) + + def test_delegated_task_lifecycle_gate_precedes_synthesis(self) -> None: + command = (ROOT / "commands/audit.md").read_text(encoding="utf-8") + reviewer = (SKILL / "references/common/reviewer-audit.md").read_text( + encoding="utf-8" + ) + synthesis = (SKILL / "references/common/synthesis.md").read_text( + encoding="utf-8" + ) + cost = (SKILL / "references/common/cost-estimation.md").read_text( + encoding="utf-8" + ) + + for content in (command, reviewer, synthesis): + normalized = " ".join(content.split()) + self.assertIn("exactly one terminal outcome", normalized) + for outcome in ("completed", "failed", "cancelled", "interrupted"): + self.assertIn(outcome, normalized) + self.assertIn("Do not infer closure", normalized) + self.assertLess( + synthesis.index("exactly one\nterminal outcome"), + synthesis.index("WGO_PHASE_SUMMARY_START"), + ) + self.assertIn("blocks synthesis", cost) + self.assertIn("Parse only that inclusive interval", cost) + self.assertIn( + "Never select whichever metadata record happens to appear last", + " ".join(cost.split()), + ) + def test_architecture_and_product_require_decision_inventory_and_register(self) -> None: architecture = reviewer_card("architecture").read_text(encoding="utf-8") product = reviewer_card("product-value").read_text(encoding="utf-8") @@ -699,7 +920,7 @@ def test_minor_evidence_and_reviewer_improvements_remain_bounded(self) -> None: ).read_text(encoding="utf-8") code_quality = reviewer_card("code-quality").read_text(encoding="utf-8") security = reviewer_card("security-privacy").read_text(encoding="utf-8") - identity = (REVIEWERS / "security-privacy/workers/identity-secrets-data-boundaries.md").read_text(encoding="utf-8") + tooling = (REVIEWERS / "security-privacy/workers/supply-chain-and-tooling.md").read_text(encoding="utf-8") self.assertIn("Documented outside audited scope; not independently\n verified.", evidence) self.assertIn("smallest useful\nscope expansion", workflow) @@ -713,19 +934,58 @@ def test_minor_evidence_and_reviewer_improvements_remain_bounded(self) -> None: self.assertIn("runtime-build-surfaces.md", code_quality) self.assertIn("public security, privacy, and disclosure claims", security) self.assertIn("abuse or\nmisuse controls", security) - self.assertIn("trust material to its consumer validation", identity) + self.assertIn("consuming verifier", tooling) self.assertIn("vulnerability-class checklist", security) self.assertIn("OSPS Baseline tier", security) self.assertIn("trust-anchor consumption", security) self.assertIn("supply-chain-and-tooling.md", security) checklist = (REVIEWERS / "security-privacy/vulnerability-class-checklist.md").read_text(encoding="utf-8") - tooling = (REVIEWERS / "security-privacy/workers/supply-chain-and-tooling.md").read_text(encoding="utf-8") for phrase in ("Canonicalization", "Data minimization", "Product-Class Abuse"): self.assertIn(phrase, checklist) for phrase in ("OpenSSF Scorecard", "OSV-Scanner", "gitleaks", "SBOM", "trust anchor"): self.assertIn(phrase, tooling) + def test_lean_feedback_improvements_are_explicit_and_bounded(self) -> None: + workflow = (SKILL / "references/common/reviewer-audit.md").read_text(encoding="utf-8") + quality = (SKILL / "references/common/artifact-quality-review.md").read_text(encoding="utf-8") + reports = ( + SKILL / "references/templates/reviewer-report-template.md" + ).read_text(encoding="utf-8") + code_quality = reviewer_card("code-quality").read_text(encoding="utf-8") + security = reviewer_card("security-privacy").read_text(encoding="utf-8") + project_health = reviewer_card("project-health").read_text(encoding="utf-8") + security_worker = ( + REVIEWERS / "security-privacy/workers/supply-chain-and-tooling.md" + ).read_text(encoding="utf-8") + + self.assertIn("| Finding | Severity | Effort | Evidence links", reports) + for phrase in ( + "consequence-based severity", + "smallest credible corrective effort", + "open-item priority remains urgency", + ): + self.assertIn(phrase, workflow) + self.assertIn("severity and effort classification", quality) + + for state in ("`measured`", "`declined`", "`blocked`"): + self.assertIn(state, code_quality) + for state in ("`production-generated`", "`independently-built`", "`unknown`"): + self.assertIn(state, code_quality) + + self.assertIn("supply-chain-and-tooling.md", security) + self.assertIn("vulnerability-class checklist", security) + self.assertLessEqual(len(security_worker.splitlines()), 25) + self.assertIn("do not write audit artifacts", security_worker) + self.assertIn("do not invoke CodeGraph", security_worker) + self.assertIn("OpenSSF Scorecard", security_worker) + self.assertIn("Never install project dependencies", security_worker) + self.assertIn("If a tool cannot run", security_worker) + self.assertIn("route each conflict to the reviewer owning its", project_health) + + for reviewer_id in EXPECTED_REVIEWERS: + self.assertIn("version: 0.2", reviewer_card(reviewer_id).read_text(encoding="utf-8")) + def test_structural_validation_is_optional(self) -> None: workflow = (SKILL / "references/common/reviewer-audit.md").read_text(encoding="utf-8") readme = (ROOT / "README.md").read_text(encoding="utf-8") @@ -974,7 +1234,7 @@ def test_public_docs_explain_cross_agent_resume_and_reviewer_extensions(self) -> normalized_guide = " ".join(guide.split()) self.assertIn("## Improve Or Compare An Audit", readme) - self.assertIn("begin an audit with Codex", readme) + self.assertIn("begin an audit with Codex, Claude, or OpenCode", readme) self.assertIn("not blindly append-only", readme) self.assertIn("Comparison modes never modify the\nbaseline", readme) self.assertIn( @@ -985,7 +1245,7 @@ def test_public_docs_explain_cross_agent_resume_and_reviewer_extensions(self) -> self.assertIn("Does anything in this onboarding configuration need to be updated?", onboarding) self.assertIn("## Audit Roots And Comparison", onboarding) self.assertIn("temporary project copy", " ".join(onboarding.split())) - self.assertIn("Plain `wgo:onboard` reopens the newest root", readme) + self.assertIn("provider's\nplain onboarding command reopens the newest root", readme) self.assertIn( "require the auditor to accept the installed versions", " ".join(readme.split()),