diff --git a/.codex-plugin/plugin.json b/.codex-plugin/plugin.json index 8e2f304..158f822 100644 --- a/.codex-plugin/plugin.json +++ b/.codex-plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "codex-quota-optimizer", - "version": "0.1.0", - "description": "Reduce avoidable Codex usage with task-aware model routing, focused context, minimal changes, and layered verification.", + "version": "0.2.0", + "description": "A zero-friction Codex usage governor with task-aware routing, soft budgets, focused context, local audits, and layered verification.", "author": { "name": "Daniel Chen", "url": "https://github.com/ctdaniel" @@ -20,7 +20,7 @@ "interface": { "displayName": "Codex Quota Optimizer", "shortDescription": "Spend less Codex allowance without sacrificing engineering quality.", - "longDescription": "A lightweight Codex usage governor that routes tasks to the lowest adequate model and reasoning level, limits unnecessary context, keeps patches focused, and verifies in layers.", + "longDescription": "A zero-friction Codex usage governor that routes tasks economically, applies soft budgets without blocking execution, keeps context and patches focused, and offers optional local task audits.", "developerName": "Daniel Chen", "category": "Developer Tools", "capabilities": [ diff --git a/.github/workflows/python-cli-tests.yml b/.github/workflows/python-cli-tests.yml new file mode 100644 index 0000000..5e7e17b --- /dev/null +++ b/.github/workflows/python-cli-tests.yml @@ -0,0 +1,29 @@ +name: Python CLI Tests + +on: + push: + branches: [main] + pull_request: + branches: [main] + +permissions: + contents: read + +jobs: + test: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + + - name: Run unit tests + run: python3 -m unittest discover -s tests -v + + - name: CLI smoke test + env: + CQO_HOME: ${{ runner.temp }}/cqo-home + run: | + python3 skills/codex-quota-optimizer/scripts/cqo.py start Fix checkout bug --mode economy + python3 skills/codex-quota-optimizer/scripts/cqo.py status + python3 skills/codex-quota-optimizer/scripts/cqo.py audit --note ci-smoke + python3 skills/codex-quota-optimizer/scripts/cqo.py history --limit 1 diff --git a/.gitignore b/.gitignore index ef328a8..7940931 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,4 @@ node_modules/ dist/ build/ coverage/ +.cqo/ diff --git a/CHANGELOG.md b/CHANGELOG.md index 9124263..909c179 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,22 @@ All notable changes to this project will be documented here. +## [0.2.0] - 2026-09-20 + +### Added +- Zero-friction runtime policy: no extra model call, no network dependency, no blocking budget gate, and no automatic subagents from CQO itself. +- Optional local `cqo` CLI with `start`, `status`, `audit`, and `history`. +- Local heuristic Task Classifier with English and Chinese risk/complexity signals. +- Soft Session Budgets for discovery, implementation paths, reasoning, verification, and subagent use. +- Local-only task journal under `~/.cqo` (or `CQO_HOME`) with no telemetry or private account scraping. +- Task-level Usage Audit that reports local change surface and CQO policy guardrails without inventing token-savings percentages. +- Python unit tests and CLI smoke-test CI. + +### Changed +- Execution budgets are explicitly advisory and may expand automatically when correctness requires more context or verification. +- The optional CLI is an inspection layer, not a runtime dependency. +- Direct `install.sh` installs a convenient `cqo` command under `~/.local/bin`. + ## [0.1.0] - 2026-09-18 ### Added diff --git a/README.md b/README.md index 2a86016..27dec0d 100644 --- a/README.md +++ b/README.md @@ -46,6 +46,24 @@ It does **not** bypass limits, scrape private quota data, or weaken verification --- +## v0.2 — Zero-friction usage governor + +> **The optimizer should not become the overhead.** + +v0.2 turns the original Skill policy into a lightweight usage governor while keeping normal Codex execution unobstructed. + +| v0.2 capability | How it behaves | +|---|---| +| **Task Classifier** | Classifies XS → XL inside the existing reasoning turn; the optional CLI can also classify locally with heuristics | +| **Soft Session Budget** | Suggests discovery, reasoning, verification and subagent scope without blocking execution | +| **Local Usage Journal** | Stores task-level metadata locally under `~/.cqo`; no telemetry and no private account scraping | +| **Usage Audit** | Records local change surface and CQO policy guardrails without inventing token-savings percentages | +| **`cqo` CLI** | Optional `start / status / audit / history` inspection layer; Codex does not depend on it | + +CQO itself adds **no automatic model call, no network request, no blocking budget gate, and no automatic subagent**. If correctness requires more context or verification than the suggested budget, Codex should simply continue. + +--- + ## Install ### Option A — One-line Skill install · recommended @@ -58,6 +76,16 @@ npx skills add ctdaniel/codex-quota-optimizer --skill codex-quota-optimizer This is the fastest path for Codex users and also makes the Skill discoverable through the wider Skills ecosystem. +The Skill works immediately; the local `cqo` CLI is optional. If you also want the short `cqo` command after a Skills CLI install: + +```bash +mkdir -p ~/.local/bin +chmod +x ~/.agents/skills/codex-quota-optimizer/scripts/cqo.py +ln -sfn ~/.agents/skills/codex-quota-optimizer/scripts/cqo.py ~/.local/bin/cqo +``` + +If `~/.local/bin` is not in your shell `PATH`, you can still run the script directly with Python. + ### Option B — Direct global install Use the repository installer across all of your Codex projects: @@ -68,12 +96,18 @@ cd codex-quota-optimizer ./install.sh ``` -It installs to: +It installs the Skill to: ```text ~/.agents/skills/codex-quota-optimizer ``` +and creates the optional CLI shortcut at: + +```text +~/.local/bin/cqo +``` + Codex should detect the Skill automatically. Restart Codex if it does not appear immediately. ### Option C — Repository-local @@ -230,9 +264,9 @@ A local change should not automatically pay the cost of Level 4. --- -## Two small helper tools +## Local tools -The Skill works without these scripts, but they can reduce repository discovery overhead. +The Skill works without any helper script. These tools are optional and local-only. ### Compact repository snapshot @@ -250,7 +284,26 @@ python skills/codex-quota-optimizer/scripts/change_scope.py Summarizes the current Git change surface and suggests a sensible verification level. -Both scripts are local-only and dependency-light. +### Optional `cqo` usage governor CLI + +The CLI never sits in the Codex runtime path. Use it only when you want local task budgeting/history: + +```bash +cqo start "Fix the checkout bug" --mode economy +cqo status +cqo audit +cqo history +``` + +It uses the Python standard library only, performs no network requests, and writes task-level state to `~/.cqo` (or `CQO_HOME`). + +If the `cqo` shortcut is not installed, run: + +```bash +python ~/.agents/skills/codex-quota-optimizer/scripts/cqo.py status +``` + +The repository snapshot and change-scope scripts are also local-only and dependency-light. --- @@ -292,9 +345,13 @@ codex-quota-optimizer/ │ ├── agents/openai.yaml │ ├── references/ │ └── scripts/ +│ ├── cqo.py # optional local usage governor CLI +│ ├── change_scope.py +│ └── repo_snapshot.py +├── tests/ # standard-library CLI tests ├── assets/ # Plugin icon + README visuals ├── examples/ -├── install.sh # installs Skill to ~/.agents/skills +├── install.sh # installs Skill + optional cqo shortcut └── README.zh-CN.md ``` @@ -302,10 +359,10 @@ codex-quota-optimizer/ ## Roadmap -- [ ] Local **Usage Journal** for task-level observations — no private account scraping -- [ ] **Task Classifier** output: task size, recommended model role, reasoning, verification scope -- [ ] **Session Budget** for discovery / coding / verification work -- [ ] End-of-task **Usage Audit** showing avoidable work that was skipped +- [x] Local **Usage Journal** for task-level observations — no private account scraping +- [x] **Task Classifier** output: task size, recommended model role, reasoning, verification scope +- [x] **Soft Session Budget** for discovery / coding / verification work +- [x] End-of-task **Usage Audit** with honest task-level local observations - [ ] Framework-aware focused-test discovery - [x] Plugin packaging for dual Skill / Plugin distribution - [ ] HOL Codex Plugin Catalog listing diff --git a/README.zh-CN.md b/README.zh-CN.md index c22e639..db879ef 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -46,6 +46,24 @@ Codex 的额度并不只花在“写代码”上。很多消耗其实来自:** --- +## v0.2|零阻塞 Usage Governor + +> **优化器本身不能成为额外负担。** + +v0.2 在原有 Skill 策略上增加了一个轻量的 Usage Governor,但不会挡在 Codex 的正常执行链路前。 + +| v0.2 能力 | 工作方式 | +|---|---| +| **Task Classifier** | 在原本的推理过程中完成 XS → XL 判断;可选 CLI 也可以用本地启发式规则分类 | +| **Soft Session Budget** | 给探索、推理、验证、Subagent 提供建议范围,但绝不阻塞任务 | +| **Local Usage Journal** | 只在本地 `~/.cqo` 保存任务级信息;无 Telemetry、不抓私人账户数据 | +| **Usage Audit** | 记录本地改动面和 CQO 的策略约束,不虚构 Token 节省比例 | +| **`cqo` CLI** | 可选的 `start / status / audit / history` 查看层;Codex 不依赖它运行 | + +CQO 自身不会额外发起模型调用、不会访问网络、不会设置阻塞式 Budget Gate,也不会自动拉起 Subagent。只要正确性需要更多上下文或验证,Codex 应直接继续完成任务。 + +--- + ## 安装 ### 方式 A|一行命令安装 Skill · 推荐 @@ -58,6 +76,16 @@ npx skills add ctdaniel/codex-quota-optimizer --skill codex-quota-optimizer 这是 Codex 用户最快的安装方式,也能让这个 Skill 进入更广泛的 Skills 生态发现路径。 +Skill 安装后即可使用;本地 `cqo` CLI 完全可选。如果你也希望通过短命令 `cqo` 使用本地任务预算与历史: + +```bash +mkdir -p ~/.local/bin +chmod +x ~/.agents/skills/codex-quota-optimizer/scripts/cqo.py +ln -sfn ~/.agents/skills/codex-quota-optimizer/scripts/cqo.py ~/.local/bin/cqo +``` + +如果 `~/.local/bin` 不在你的 `PATH` 中,也可以直接通过 Python 运行脚本。 + ### 方式 B|直接全局安装 如果你希望继续使用仓库自带安装脚本: @@ -74,6 +102,12 @@ Skill 会被安装到: ~/.agents/skills/codex-quota-optimizer ``` +同时会创建可选 CLI 快捷命令: + +```text +~/.local/bin/cqo +``` + Codex 通常会自动检测新 Skill;如果没有出现,重启 Codex 即可。 ### 方式 C|项目级安装 @@ -232,9 +266,9 @@ Level 4 全量测试 / 发布前 Gate --- -## 两个辅助脚本 +## 本地辅助工具 -Skill 不依赖它们也能工作,但在较大的项目中它们可以进一步减少探索开销。 +Skill 完全不依赖任何辅助脚本;下面这些工具都只是可选、本地运行。 ### Compact Repository Snapshot @@ -252,7 +286,26 @@ python skills/codex-quota-optimizer/scripts/change_scope.py 总结当前 Git 改动范围,并给出合理的验证层级建议。 -两个工具都只在本地工作,不上传项目数据。 +### 可选 `cqo` Usage Governor CLI + +CLI 不会插入 Codex 的执行链路。只有你希望查看本地任务预算和历史时才需要运行: + +```bash +cqo start "修复结算页 Bug" --mode economy +cqo status +cqo audit +cqo history +``` + +它只使用 Python 标准库,不访问网络,任务级状态保存在 `~/.cqo`(或 `CQO_HOME`)。 + +如果没有安装 `cqo` 快捷命令,也可以直接运行: + +```bash +python ~/.agents/skills/codex-quota-optimizer/scripts/cqo.py status +``` + +Repository Snapshot 与 Change Scope 两个脚本同样只在本地运行,不上传项目数据。 --- @@ -294,9 +347,13 @@ codex-quota-optimizer/ │ ├── agents/openai.yaml │ ├── references/ │ └── scripts/ +│ ├── cqo.py # 可选本地 Usage Governor CLI +│ ├── change_scope.py +│ └── repo_snapshot.py +├── tests/ # Python 标准库 CLI 测试 ├── assets/ # Plugin 图标 + README 视觉资源 ├── examples/ -├── install.sh # 安装到 ~/.agents/skills +├── install.sh # 安装 Skill + 可选 cqo 快捷命令 └── README.zh-CN.md ``` @@ -304,10 +361,10 @@ codex-quota-optimizer/ ## Roadmap -- [ ] 本地 **Usage Journal**:记录任务级使用行为,不抓取私人账户数据 -- [ ] **Task Classifier**:输出任务规模、模型角色、推理档和测试建议 -- [ ] **Session Budget**:给探索 / 编码 / 验证分配任务级工作预算 -- [ ] **Usage Audit**:任务结束展示本次避免了哪些无效工作 +- [x] 本地 **Usage Journal**:记录任务级使用行为,不抓取私人账户数据 +- [x] **Task Classifier**:输出任务规模、模型角色、推理档和测试建议 +- [x] **Soft Session Budget**:给探索 / 编码 / 验证提供非阻塞式预算建议 +- [x] **Usage Audit**:任务结束输出诚实的本地任务级观察 - [ ] 自动识别不同框架最合适的定向测试 - [x] Plugin 打包:同时支持 Skill 直装与 Plugin 分发 - [ ] HOL Codex Plugin Catalog 收录 diff --git a/install.sh b/install.sh index 04f4e7f..6b0defc 100755 --- a/install.sh +++ b/install.sh @@ -4,6 +4,9 @@ set -euo pipefail ROOT="$(cd "$(dirname "$0")" && pwd)" SRC="${ROOT}/skills/codex-quota-optimizer" DEST="${HOME}/.agents/skills/codex-quota-optimizer" +BIN_DIR="${HOME}/.local/bin" +CLI_SRC="${DEST}/scripts/cqo.py" +CLI_DEST="${BIN_DIR}/cqo" if [[ ! -d "$SRC" ]]; then echo "Skill source not found: $SRC" >&2 @@ -14,5 +17,13 @@ mkdir -p "$(dirname "$DEST")" rm -rf "$DEST" cp -R "$SRC" "$DEST" +mkdir -p "$BIN_DIR" +chmod +x "$CLI_SRC" +ln -sfn "$CLI_SRC" "$CLI_DEST" + echo "Installed codex-quota-optimizer to $DEST" +echo "Installed optional cqo CLI to $CLI_DEST" +if [[ ":${PATH}:" != *":${BIN_DIR}:"* ]]; then + echo "Note: add $BIN_DIR to PATH to run 'cqo' directly." +fi echo "Restart Codex if the skill does not appear immediately." diff --git a/plugin.json b/plugin.json index d1239bc..4720e81 100644 --- a/plugin.json +++ b/plugin.json @@ -1,8 +1,8 @@ { "$schema": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json", "name": "codex-quota-optimizer", - "version": "0.1.0", - "description": "Reduce avoidable Codex usage with task-aware model routing, focused context, minimal changes, and layered verification.", + "version": "0.2.0", + "description": "A zero-friction Codex usage governor with task-aware routing, soft budgets, focused context, local audits, and layered verification.", "author": { "name": "Daniel Chen", "url": "https://github.com/ctdaniel" diff --git a/skills/codex-quota-optimizer/SKILL.md b/skills/codex-quota-optimizer/SKILL.md index 373b94e..4b053e2 100644 --- a/skills/codex-quota-optimizer/SKILL.md +++ b/skills/codex-quota-optimizer/SKILL.md @@ -13,6 +13,19 @@ Use the cheapest execution path that can still meet the acceptance criteria. Esc Never claim an exact remaining quota unless the current Codex client/account exposes it. If quota state matters, tell the user to check `/status` or the usage dashboard, then continue with the best available strategy. +### Zero-friction invariant + +The optimizer must not become the overhead. + +- Do not make an additional model call just to classify or budget a task. +- Do not add network requests for CQO classification, journaling, or auditing. +- Do not put a blocking budget gate in front of normal Codex execution. +- Do not ask the user for confirmation merely because a soft budget was exceeded. +- Do not spawn subagents on CQO's behalf. +- The Skill must remain fully useful when the optional `cqo` CLI is never installed or run. + +See `references/zero-friction.md`. + ## 1. Read only what you need Before broad exploration: @@ -27,7 +40,7 @@ See `references/context-policy.md` for detailed rules. ## 2. Classify the task before execution -Assign one class internally: +Assign one class internally as part of the existing reasoning turn. Do not call another model solely to perform classification. - **XS** — one-file, mechanical, obvious acceptance criteria. - **S** — narrow feature/fix, 1–3 files, known pattern. @@ -56,13 +69,13 @@ For every non-trivial task, maintain this internal budget: - **Edit budget:** minimum files required to satisfy acceptance criteria. - **Verification budget:** targeted checks first; broad checks only at the final gate or when evidence demands them. -Default limits are behavioral, not hard numeric quotas: +These are **soft budgets**, never execution gates: - Stop discovery once the relevant dependency path is understood. - Stop editing once acceptance criteria are met; avoid opportunistic refactors. - Stop verification after relevant tests/type/lint checks pass unless the change is high risk. - -If scope expands, explicitly reclassify the task rather than silently consuming more context. +- If another file, test, or reasoning step is required for correctness, continue without interrupting the user. +- If scope expands, reclassify internally and keep moving; do not request permission just because the initial budget was too small. ## 4. Plan economically @@ -173,9 +186,23 @@ When useful, end with a compact **Usage choices** note containing only actionabl Do not clutter every answer with quota commentary if the skill is operating successfully in the background. -## 12. Optional helper scripts +## 12. Optional local observability + +The `cqo` CLI is optional. It provides task-level classification, soft budgets, a local journal, and a local audit: + +- `cqo start ` +- `cqo status` +- `cqo audit` +- `cqo history` + +The journal is local-only under `~/.cqo` (or `CQO_HOME`). It does not inspect private account pages or estimate hidden quota. + +**Never run `cqo` automatically just to collect analytics.** The CLI is an inspection layer, not a runtime dependency. If the user did not opt into a CQO session, normal Skill behavior continues with zero CLI overhead. + +## 13. Optional helper scripts - `scripts/repo_snapshot.py --compact` — compact project map without reading the whole repo. - `scripts/change_scope.py` — summarize current Git change surface and suggest verification scope. +- `scripts/cqo.py` — optional zero-network local CLI for task-level budgeting and auditing. -Use scripts only when they save model context/tool calls; do not run them ritualistically. +Use scripts only when they save model context/tool calls or the user explicitly wants observability; do not run them ritualistically. diff --git a/skills/codex-quota-optimizer/agents/openai.yaml b/skills/codex-quota-optimizer/agents/openai.yaml index fc1bed6..6eb8a8b 100644 --- a/skills/codex-quota-optimizer/agents/openai.yaml +++ b/skills/codex-quota-optimizer/agents/openai.yaml @@ -1,9 +1,9 @@ interface: display_name: "Codex Quota Optimizer" - short_description: "Save Codex Plus/Pro usage with smarter model routing, focused context, minimal edits, and layered verification." + short_description: "Zero-friction Codex usage governor with smarter routing, focused context, soft budgets, and layered verification." icon_small: "../assets/icon.svg" icon_large: "../assets/icon.svg" brand_color: "#4FD4A4" - default_prompt: "Optimize this Codex task for the lowest practical usage while preserving correctness and acceptance criteria." + default_prompt: "Optimize this Codex task with zero-friction usage discipline: keep context focused, use soft budgets, preserve correctness, and never add blocking overhead just to save quota." policy: allow_implicit_invocation: true diff --git a/skills/codex-quota-optimizer/references/zero-friction.md b/skills/codex-quota-optimizer/references/zero-friction.md new file mode 100644 index 0000000..60ec66f --- /dev/null +++ b/skills/codex-quota-optimizer/references/zero-friction.md @@ -0,0 +1,84 @@ +# Zero-friction policy + +Codex Quota Optimizer exists to improve the Codex experience, so the optimizer itself must not become the overhead. + +## Invariants + +CQO should add: + +- **0 additional model calls** just to classify or budget a task. +- **0 network requests** for task classification, journaling, or auditing. +- **0 blocking budget gates** in the Codex execution path. +- **0 forced user confirmations** when a soft budget is exceeded. +- **0 automatic subagents** created by CQO itself. + +The Skill should remain useful even if the optional CLI is never installed or run. + +## Soft budgets, not hard limits + +Task budgets are guidance. If evidence shows that another file, test, or reasoning step is required for correctness, continue without interrupting the user. + +Do not pause execution just because: + +- a suggested discovery range was exceeded, +- another dependency must be inspected, +- a broader verification step becomes necessary, +- the task was initially classified too small. + +Reclassify internally when needed and keep moving. + +## Classifier behavior + +Task classification should happen inside the existing reasoning turn or through the optional local heuristic CLI. + +Do not make a second LLM call solely to classify a task. + +The local classifier may use: + +- task text, +- current Git change-surface size, +- risk terms, +- complexity/debugging terms. + +Its output is advisory, not authoritative. + +## Optional local journal + +The CLI stores task-level state under: + +```text +~/.cqo/ +``` + +or the directory provided by `CQO_HOME`. + +It stores only local task metadata and does not contact OpenAI, inspect private account pages, or estimate hidden account quota. + +The journal should prefer task-level observations over tool-level tracing. Do not hook every Codex tool call merely to collect analytics. + +## Audit honesty + +CQO must not invent token savings, quota savings, or precise avoided-cost percentages. + +An audit may report: + +- the task classification, +- the selected soft budget, +- the current local Git change surface, +- CQO policy decisions it did not force (for example, no automatic full-suite run), +- explicit notes supplied by the user. + +Do not report actions as "avoided" unless there is direct evidence they would otherwise have occurred. + +## CLI placement + +The `cqo` CLI is an inspection and journaling layer, not a runtime dependency. + +Normal Codex execution must continue to work when: + +- the CLI is absent, +- no CQO session was started, +- the journal directory was deleted, +- the user never asks for an audit. + +The default experience should feel like Codex with better discipline, not Codex behind another approval system. diff --git a/skills/codex-quota-optimizer/scripts/cqo.py b/skills/codex-quota-optimizer/scripts/cqo.py new file mode 100644 index 0000000..4a10479 --- /dev/null +++ b/skills/codex-quota-optimizer/scripts/cqo.py @@ -0,0 +1,490 @@ +#!/usr/bin/env python3 +"""Codex Quota Optimizer local CLI. + +Zero-network, zero-dependency helper for optional task-level budgeting and auditing. +It never intercepts Codex execution and never requires an additional model call. +""" + +from __future__ import annotations + +import argparse +import json +import os +import subprocess +import sys +import uuid +from datetime import datetime, timezone +from pathlib import Path +from typing import Any + +VERSION = "0.2.0" + +HIGH_RISK_TERMS = { + "security", "auth", "authentication", "authorization", "permission", + "payment", "billing", "migration", "migrate", "database schema", + "encryption", "secret", "credential", "production", "data loss", + "rollback", "compliance", + "安全", "鉴权", "认证", "权限", "支付", "账单", "迁移", "数据库", + "加密", "密钥", "凭证", "生产", "数据丢失", "回滚", "合规", +} +COMPLEX_TERMS = { + "architecture", "redesign", "cross-cutting", "distributed", "concurrency", + "race condition", "performance", "refactor", "multi-service", "multi system", + "multi-system", "end-to-end", "e2e", "framework", "protocol", + "架构", "重设计", "跨模块", "分布式", "并发", "竞态", "性能", "重构", + "多服务", "多系统", "端到端", "框架", "协议", +} +DEBUG_TERMS = { + "debug", "flaky", "intermittent", "root cause", "reproduce", "regression", + "memory leak", "deadlock", + "调试", "偶发", "根因", "复现", "回归", "内存泄漏", "死锁", +} +MECHANICAL_TERMS = { + "rename", "typo", "copy change", "text change", "comment", "formatting", + "format only", "documentation", "docs only", "readme", + "重命名", "错别字", "文案修改", "文本修改", "注释", "格式化", "文档", +} + +SIZE_ORDER = ("XS", "S", "M", "L", "XL") + + +def now_iso() -> str: + return datetime.now(timezone.utc).isoformat(timespec="seconds") + + +def cqo_home() -> Path: + return Path(os.environ.get("CQO_HOME", "~/.cqo")).expanduser() + + +def current_path() -> Path: + return cqo_home() / "current.json" + + +def history_path() -> Path: + return cqo_home() / "history.jsonl" + + +def ensure_home() -> None: + cqo_home().mkdir(parents=True, exist_ok=True) + + +def contains_any(text: str, terms: set[str]) -> bool: + low = text.lower() + return any(term in low for term in terms) + + +def git_info(cwd: str | None = None) -> dict[str, Any]: + """Return a tiny local git snapshot. Fail closed and never block for long.""" + result: dict[str, Any] = { + "root": None, + "branch": None, + "changed_files": [], + } + + def run(args: list[str]) -> str | None: + try: + completed = subprocess.run( + ["git", *args], + cwd=cwd, + capture_output=True, + text=True, + timeout=1.0, + check=False, + ) + except (OSError, subprocess.SubprocessError): + return None + if completed.returncode != 0: + return None + return completed.stdout.strip() + + root = run(["rev-parse", "--show-toplevel"]) + if not root: + return result + + result["root"] = root + result["branch"] = run(["branch", "--show-current"]) or None + changed = run(["status", "--porcelain=v1", "-uno"]) + if changed: + paths: list[str] = [] + for line in changed.splitlines(): + payload = line[3:] if len(line) > 3 else "" + if " -> " in payload: + payload = payload.split(" -> ", 1)[1] + if payload: + paths.append(payload.strip('"')) + result["changed_files"] = sorted(set(paths)) + return result + + +def classify_task(task: str, repo: dict[str, Any] | None = None) -> dict[str, str]: + """Classify locally with simple heuristics; no model call.""" + text = (task or "").strip() + words = len(text.split()) + changed_count = len((repo or {}).get("changed_files") or []) + + score = 1 if text else 0 + if changed_count: + if changed_count == 1: + score += 1 + elif changed_count <= 3: + score += 2 + elif changed_count <= 7: + score += 4 + elif changed_count <= 12: + score += 6 + else: + score += 8 + + high_risk = contains_any(text, HIGH_RISK_TERMS) + complex_task = contains_any(text, COMPLEX_TERMS) + debug_task = contains_any(text, DEBUG_TERMS) + mechanical = contains_any(text, MECHANICAL_TERMS) + + if high_risk: + score += 5 + if complex_task: + score += 3 + if debug_task: + score += 2 + if words > 80: + score += 1 + if words > 220: + score += 2 + if mechanical and not (high_risk or complex_task or debug_task): + score -= 1 + + if score <= 0: + size = "XS" + elif score <= 2: + size = "S" + elif score <= 5: + size = "M" + elif score <= 8: + size = "L" + else: + size = "XL" + + if high_risk: + risk = "high" + elif complex_task or debug_task or size in {"L", "XL"}: + risk = "medium" + else: + risk = "low" + + return {"size": size, "risk": risk} + + +def make_budget(size: str, mode: str) -> dict[str, str | int]: + """Return soft guidance only; these are not execution gates.""" + mode = mode.lower() + if mode not in {"economy", "balanced", "emergency"}: + raise ValueError(f"Unsupported mode: {mode}") + + size_index = SIZE_ORDER.index(size) + + discovery_by_mode = { + "economy": [ + "1-2 targeted files", + "2-4 targeted files", + "4-7 targeted files", + "6-10 targeted files", + "targeted only; expand on evidence", + ], + "balanced": [ + "1-3 targeted files", + "3-5 targeted files", + "5-8 targeted files", + "8-12 targeted files", + "targeted first; expand on evidence", + ], + "emergency": ["minimum targeted context"] * 5, + } + verification_by_size = [ + "Level 1", + "Level 2", + "Level 2-3", + "Level 3", + "Level 3-4", + ] + reasoning_by_size = ["low", "low", "low/medium", "medium", "medium/high"] + model_role_by_size = ["economy", "economy", "balanced", "balanced/deep", "deep"] + + if mode == "emergency": + verification = "focused only unless safety-critical" + reasoning = "lowest adequate" + model_role = "economy first" + subagents = "no" + else: + verification = verification_by_size[size_index] + reasoning = reasoning_by_size[size_index] + model_role = model_role_by_size[size_index] + subagents = "no" if size_index <= 2 else "only for independent parallel work" + + return { + "discovery": discovery_by_mode[mode][size_index], + "implementation_paths": 1, + "verification": verification, + "reasoning": reasoning, + "model_role": model_role, + "subagents": subagents, + "budget_type": "soft", + } + + +def read_current() -> dict[str, Any] | None: + try: + return json.loads(current_path().read_text(encoding="utf-8")) + except (FileNotFoundError, json.JSONDecodeError, OSError): + return None + + +def write_current(data: dict[str, Any]) -> None: + ensure_home() + tmp = current_path().with_suffix(".tmp") + tmp.write_text( + json.dumps(data, ensure_ascii=False, indent=2) + "\n", + encoding="utf-8", + ) + tmp.replace(current_path()) + + +def append_history(data: dict[str, Any]) -> None: + ensure_home() + with history_path().open("a", encoding="utf-8") as f: + f.write(json.dumps(data, ensure_ascii=False, separators=(",", ":")) + "\n") + + +def archive_superseded(session: dict[str, Any]) -> None: + item = dict(session) + item["status"] = "superseded" + item["finished_at"] = now_iso() + append_history(item) + + +def start_session(task: str, mode: str, cwd: str | None = None) -> dict[str, Any]: + previous = read_current() + if previous: + archive_superseded(previous) + + repo = git_info(cwd) + classification = classify_task(task, repo) + budget = make_budget(classification["size"], mode) + + session = { + "id": uuid.uuid4().hex[:8], + "version": VERSION, + "status": "active", + "started_at": now_iso(), + "task": task.strip() or "(unspecified task)", + "mode": mode, + "size": classification["size"], + "risk": classification["risk"], + "budget": budget, + "repo": { + "root": repo.get("root"), + "branch": repo.get("branch"), + "changed_files_at_start": repo.get("changed_files") or [], + }, + "cqo_overhead_policy": { + "network_requests": 0, + "blocking_gates": 0, + "automatic_model_calls": 0, + "automatic_subagents": 0, + }, + } + write_current(session) + return session + + +def audit_session( + note: str | None = None, + cwd: str | None = None, +) -> dict[str, Any] | None: + session = read_current() + if not session: + return None + + repo = git_info(cwd or session.get("repo", {}).get("root")) + audit = dict(session) + audit["status"] = "completed" + audit["finished_at"] = now_iso() + audit["repo_at_audit"] = { + "root": repo.get("root"), + "branch": repo.get("branch"), + "changed_files": repo.get("changed_files") or [], + "changed_file_count": len(repo.get("changed_files") or []), + } + audit["policy_guardrails"] = [ + "CQO did not force a repository-wide scan.", + "CQO did not force a full test suite.", + "CQO did not spawn subagents.", + "CQO did not request a model escalation.", + ] + if note: + audit["note"] = note + + append_history(audit) + try: + current_path().unlink() + except FileNotFoundError: + pass + return audit + + +def load_history(limit: int = 10) -> list[dict[str, Any]]: + try: + lines = history_path().read_text(encoding="utf-8").splitlines() + except OSError: + return [] + + items: list[dict[str, Any]] = [] + for line in lines[-max(limit, 1):]: + try: + items.append(json.loads(line)) + except json.JSONDecodeError: + continue + return items + + +def print_json(data: Any) -> None: + print(json.dumps(data, ensure_ascii=False, indent=2)) + + +def print_start(session: dict[str, Any]) -> None: + budget = session["budget"] + print(f"CQO session {session['id']}") + print(f"Task: {session['size']} · Risk: {session['risk']} · Mode: {session['mode']}") + print( + "Soft budget: " + f"discovery {budget['discovery']}; " + f"paths {budget['implementation_paths']}; " + f"reasoning {budget['reasoning']}; " + f"verification {budget['verification']}; " + f"subagents {budget['subagents']}" + ) + print("Zero-friction policy: no network, no blocking gate, no automatic model call.") + + +def print_status(session: dict[str, Any] | None) -> None: + if not session: + print("No active CQO session.") + return + budget = session["budget"] + print(f"CQO session {session['id']} · active") + print(f"{session['size']} · {session['risk']} risk · {session['mode']}") + print(f"Task: {session['task']}") + print(f"Discovery: {budget['discovery']}") + print(f"Verification: {budget['verification']}") + print(f"Model role: {budget['model_role']} · Reasoning: {budget['reasoning']}") + print(f"Subagents: {budget['subagents']}") + + +def print_audit(audit: dict[str, Any] | None) -> None: + if not audit: + print("No active CQO session. Run cqo start first.") + return + repo = audit["repo_at_audit"] + print(f"CQO audit {audit['id']} · completed") + print(f"Task: {audit['size']} · Risk: {audit['risk']} · Mode: {audit['mode']}") + print(f"Current repository change surface: {repo['changed_file_count']} file(s)") + print("CQO overhead: 0 network requests · 0 blocking gates · 0 automatic model calls") + print(f"Recorded locally in {history_path()}") + + +def print_history(items: list[dict[str, Any]]) -> None: + if not items: + print("No CQO history yet.") + return + for item in reversed(items): + status = item.get("status", "?") + print( + f"{item.get('finished_at', item.get('started_at', '?'))} " + f"{item.get('id', '?')} " + f"{item.get('size', '?')}/{item.get('mode', '?')} " + f"{status} " + f"{item.get('task', '')}" + ) + + +def build_parser() -> argparse.ArgumentParser: + parser = argparse.ArgumentParser( + prog="cqo", + description="Zero-friction local usage governor for Codex.", + ) + parser.add_argument("--version", action="version", version=f"%(prog)s {VERSION}") + sub = parser.add_subparsers(dest="command", required=True) + + start = sub.add_parser( + "start", + help="Classify a task and create a soft local budget.", + ) + start.add_argument("task", nargs="*", help="Task text. Quotes are optional.") + start.add_argument( + "--mode", + choices=("economy", "balanced", "emergency"), + default=os.environ.get("CQO_MODE", "balanced"), + ) + start.add_argument("--cwd", default=None, help="Repository path to inspect locally.") + start.add_argument("--json", action="store_true") + + status = sub.add_parser( + "status", + help="Show the active local CQO session.", + ) + status.add_argument("--json", action="store_true") + + audit = sub.add_parser( + "audit", + help="Finalize the active session and write a local audit.", + ) + audit.add_argument("--note", default=None, help="Optional local note.") + audit.add_argument("--cwd", default=None, help="Repository path to inspect locally.") + audit.add_argument("--json", action="store_true") + + history = sub.add_parser( + "history", + help="Show recent local CQO sessions.", + ) + history.add_argument("--limit", type=int, default=10) + history.add_argument("--json", action="store_true") + + return parser + + +def main(argv: list[str] | None = None) -> int: + args = build_parser().parse_args(argv) + + if args.command == "start": + session = start_session(" ".join(args.task), args.mode, args.cwd) + print_json(session) if args.json else print_start(session) + return 0 + + if args.command == "status": + session = read_current() + if args.json: + print_json(session or {}) + else: + print_status(session) + return 0 + + if args.command == "audit": + audit = audit_session(args.note, args.cwd) + if args.json: + print_json(audit or {}) + else: + print_audit(audit) + return 0 + + if args.command == "history": + items = load_history(args.limit) + if args.json: + print_json(items) + else: + print_history(items) + return 0 + + return 2 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/tests/test_cqo.py b/tests/test_cqo.py new file mode 100644 index 0000000..1b69611 --- /dev/null +++ b/tests/test_cqo.py @@ -0,0 +1,68 @@ +import importlib.util +import os +import tempfile +import unittest +from pathlib import Path +from unittest.mock import patch + +SCRIPT = ( + Path(__file__).resolve().parents[1] + / "skills" + / "codex-quota-optimizer" + / "scripts" + / "cqo.py" +) +spec = importlib.util.spec_from_file_location("cqo", SCRIPT) +cqo = importlib.util.module_from_spec(spec) +assert spec.loader is not None +spec.loader.exec_module(cqo) + + +class CqoTests(unittest.TestCase): + def test_simple_bug_defaults_to_small(self): + self.assertEqual(cqo.classify_task("Fix checkout bug")["size"], "S") + + def test_mechanical_change_can_stay_xs(self): + self.assertEqual(cqo.classify_task("Fix README typo")["size"], "XS") + + def test_high_risk_task_escalates_risk(self): + result = cqo.classify_task( + "Migrate production authentication database schema" + ) + self.assertEqual(result["risk"], "high") + self.assertIn(result["size"], {"L", "XL"}) + + def test_chinese_high_risk_task_is_recognized(self): + result = cqo.classify_task("迁移生产环境的支付数据库权限") + self.assertEqual(result["risk"], "high") + self.assertIn(result["size"], {"L", "XL"}) + + def test_emergency_budget_is_soft_and_no_subagents(self): + budget = cqo.make_budget("M", "emergency") + self.assertEqual(budget["budget_type"], "soft") + self.assertEqual(budget["subagents"], "no") + self.assertEqual(budget["implementation_paths"], 1) + + def test_local_session_lifecycle(self): + with tempfile.TemporaryDirectory() as tmp: + with patch.dict(os.environ, {"CQO_HOME": tmp}, clear=False): + session = cqo.start_session( + "Fix checkout bug", + "economy", + cwd=tmp, + ) + self.assertEqual(session["status"], "active") + self.assertTrue(cqo.current_path().exists()) + + audit = cqo.audit_session("unit-test", cwd=tmp) + self.assertIsNotNone(audit) + self.assertEqual(audit["status"], "completed") + self.assertFalse(cqo.current_path().exists()) + + history = cqo.load_history(10) + self.assertEqual(len(history), 1) + self.assertEqual(history[0]["note"], "unit-test") + + +if __name__ == "__main__": + unittest.main() diff --git a/uninstall.sh b/uninstall.sh index 2d2b3bd..98e4a64 100755 --- a/uninstall.sh +++ b/uninstall.sh @@ -1,5 +1,17 @@ #!/usr/bin/env bash set -euo pipefail + DEST="${HOME}/.agents/skills/codex-quota-optimizer" +CLI_DEST="${HOME}/.local/bin/cqo" + +if [[ -L "$CLI_DEST" ]]; then + TARGET="$(readlink "$CLI_DEST" || true)" + if [[ "$TARGET" == *"/.agents/skills/codex-quota-optimizer/scripts/cqo.py" ]]; then + rm -f "$CLI_DEST" + echo "Removed $CLI_DEST" + fi +fi + rm -rf "$DEST" echo "Removed $DEST" +echo "Local CQO journal under ~/.cqo was preserved."