Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,23 @@ All notable changes to nexum are documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]
### Added
- **Caveman prompts — telegraphic plans + dispatch prompts (`caveman_prompts_enabled`, default true).** `/nx-plan` now writes the plan's prose fields (task summary, step `title`/`objective`/`contract`/`scope`) in clipped, telegraphic English — articles, copulas, and filler dropped — and `/nx-build` builds its executor dispatch prompts the same way. The plan is re-read by every executor and the shared dispatch prefix ships on every step, so trimming function words from them is a recurring token saving. Strict carve-outs stay verbatim and unambiguous: file paths, identifiers, signatures, config keys, code, and the runnable `acceptance` command — terseness never costs precision. Set `false` for normal prose.
- **Grep narrowing — a *working* PreToolUse context-savings lever (`scripts/scan_guard.py`).** An unscoped/broad search now has its **output capped** instead of being hard-denied: the `Grep` tool gets a `head_limit` injected and an unscoped recursive Bash `grep`/`rg` gets `| head -n N` appended (via PreToolUse `updatedInput`, which current Claude Code honors — unlike PostToolUse output shrink). The model still gets a bounded answer with no retry round-trip. Searches into a `scan_deny_paths` directory, and Glob (no `head_limit`), still deny; a Bash grep that already pipes falls back to deny; an explicit caller `head_limit` is never overridden. Config: `grep_narrow_enabled` (default true), `grep_head_limit` (default 80).
- **Confidence-aware, bidirectional routing calibration (`store.calibration_advice`, `store.py calib-advice` CLI).** Replaces the raw first-try pass ratio with a **Wilson score lower bound**, so a short lucky/unlucky streak no longer flips a route. Advice is now **bidirectional**: nudge a route *up* a tier when low-confidence (lower bound < `route_calib_min_success_ratio`) and *down* a tier when a cheaper tier reliably suffices (lower bound ≥ `route_calib_downgrade_ratio`, default 0.9). Falls back to a cross-repo `_global` prior when a repo lacks `route_calib_min_samples` of its own history. `/nx-plan` consumes the new `calib-advice` JSON (action/reason/samples/lower/source). Config: `route_calib_downgrade_ratio` (default 0.9; 1.0 disables downgrades).
- **Honest savings split in `/nx-report` (`scripts/report.py`, `store.savings_by_source`).** The report now separates savings into three buckets so the headline never overclaims the inert PostToolUse lever: **Realized** (PreToolUse, measured — `predup`'s exact denied-repeat tokens), **Bounded interventions** (`read_guard`/`grep_narrow` — output capped, exact saving unknowable, counted only), and **Theoretical** (`dedup`/`truncate` — PostToolUse shrink that is inert because Claude Code ignores `updatedToolOutput` for built-in tools, tracking #65403). read-guard and grep-narrow now record a 0-token intervention row so the bounded count is visible.
- **`PreCompact` hook (`scripts/precompact.py`).** Fires at the exact compaction boundary to (a) invalidate this session's `tool_calls` rows — so predup can never deny a re-read of output the compaction just evicted — and (b) write a deterministic handoff skeleton. Never blocks compaction. Closes the predup-after-compaction correctness gap that the `predup_max_age_seconds` time guard could only approximate.
- **`SessionStart` reset/prune hook (`scripts/session_reset.py`).** On `source` `clear`/`compact`, clears `tool_calls` for the same reason as PreCompact (filtered in-script, not via matcher). Also runs the throttled retention prune.
- **`SubagentStop` hook (`scripts/subagent_usage.py`) → real per-tier usage.** Maps a nexum executor agent (`nexum-impl-{haiku,sonnet,opus}`) to its tier and records a `usage` row with token totals parsed best-effort from the subagent transcript (`store.transcript_usage_totals`), so the cost-report breakdown reflects measured spend rather than the plan-preview estimate. (The SubagentStop payload carries no usage fields, so attribution is transcript-based and best-effort — documented as such.)
- **Retention/pruning (`store.prune`, `store.maybe_prune`, `store.py prune` CLI).** Rows older than `retention_days` (default 14) are pruned from the ephemeral tables (`tool_calls`, `savings`, `outputs`, `usage`, `memo`, `file_activity`), throttled to at most once/day on session start, keeping the SQLite file and predup lookups bounded. `0` disables. (`route_calibration`/`step_ledger` are persistent and intentionally not age-pruned.)
- **Fable pricing.** `store.PRICING` gains `fable` ($10/$50 per MTok) and `cost_report._model_key` recognises it (and orders matching most-specific-first), so Fable-class usage is no longer mispriced as Sonnet. Bedrock/Vertex `anthropic.`-prefixed IDs still map correctly.
- **Wasted-context analytics + `/nx-report` (inspired by the open-source `claude-context-optimizer`).** A PostToolUse tracker (extends `dedup.py`, now also matching `Edit|Write|MultiEdit`) records per-file read/edit counts and injected-token estimates in a new `file_activity` table. The new `/nx-report` command (`scripts/report.py`) prints a deterministic, no-LLM session digest: the cost summary plus a wasted-context analysis — total tokens read, tokens spent on files read but never edited, a waste ratio, an S–F efficiency grade, a per-file useful/WASTED table, and concrete "drop X → save ~N tokens" suggestions. New `store` helpers: `record_file_read`, `record_file_edit`, `file_activity_rows`, `wasted_files`. Config `file_activity_enabled` (default true).
- **Tiered budget alerts (C).** `context_watch` now compares the session's real metered cost (the status-line `session_cost` snapshot) to `budget_usd`, and cumulative input+output tokens to `budget_tokens`, emitting an escalating, once-per-tier non-blocking `systemMessage` at `budget_alert_tiers` (default 50/70/80/90%). At ≥70% it names the biggest never-edited files to drop (from the wasted-context tracker); at ≥90% it urges `/compact`. Both budgets default to `0` (disabled).

### Changed
- **predup recall — canonicalised signatures.** `store.tool_call_sig` now realpath-canonicalises `file_path`/`path` before hashing, so `./foo.py`, `foo.py`, and the absolute path collapse to one signature and predup catches the repeat. Distinct read ranges (`offset`/`limit`) and patterns stay distinct. New `store.clear_tool_calls`.

## [0.4.0] - 2026-06-18
### Added
- **Estimated per-tier usage recording in `/nx-build`** so the cost report breakdown is populated with token estimates per execution tier, giving visibility into where the session's cost is incurred.
Expand Down
42 changes: 36 additions & 6 deletions SPEC.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,8 +52,14 @@ reliably reduce context today are:
via `updatedInput` for files above `read_guard_min_bytes` (default 262144 bytes)
that do not already carry an explicit limit. Configurable via `config.json`:
`read_guard_enabled`, `read_guard_min_bytes`, `read_guard_inject_lines`.
- **Scan-guard blocking** — unscoped recursive greps, broad globs, and reads into
deny paths are blocked via `permissionDecision: deny` before the tool executes.
- **Scan-guard blocking** — reads into deny paths, recursive searches over deny
paths, and unscoped `find`/`ls -R` are blocked via `permissionDecision: deny`
before the tool executes.
- **Grep narrowing** (`grep_narrow_enabled`, default true) — instead of denying a
broad/unscoped *search*, nexum caps its output via `updatedInput`: `head_limit`
on the `Grep` tool, `| head -n grep_head_limit` on an unscoped recursive Bash
`grep`/`rg`. A bounded answer is returned without a retry. Deny-path searches,
Glob, already-piped greps, and caller-set `head_limit` fall back to deny.

---

Expand Down Expand Up @@ -111,6 +117,25 @@ nexum/
> Net rule: dedup is the authority on the final `updatedToolOutput`; truncate is a
> fallback for tools dedup doesn't handle. Keep both wired; dedup wins when it acts.

**Lifecycle hooks (beyond the wiring block above).** The plugin also wires:
`SessionStart` → `resume_nudge.py`, `audit_nudge.py`, `session_reset.py`
(the last clears `tool_calls` on a `clear`/`compact` source and runs the
throttled retention prune); `PreCompact` → `precompact.py` (clears `tool_calls`
at the compaction boundary so predup can't deny a re-read of evicted output, and
writes a handoff skeleton — never blocks compaction); `SubagentStop` →
`subagent_usage.py` (records a real per-tier `usage` row for `nexum-impl-*`
executors, token totals parsed best-effort from the subagent transcript since
the payload carries none). All fail-open: print `{}` / exit 0 on any error.

**Wasted-context analytics + budget alerts.** The PostToolUse `dedup.py` (matcher
extended to `…|Edit|Write|MultiEdit`) also records per-file read/edit counts and
injected tokens into a `file_activity` table. `/nx-report` (`report.py`) renders
a deterministic digest: cost summary + a wasted-context view (tokens spent on
files read but never edited, a waste ratio, an S–F grade, and "drop X → save ~N
tokens" picks). `context_watch` adds tiered budget alerts keyed to the real
metered `session_cost` (`budget_usd`) and cumulative tokens (`budget_tokens`),
naming the biggest never-edited files at ≥70% and urging `/compact` at ≥90%.

---

## 2. `scripts/store.py` — foundation (build FIRST; everything imports it) · TIER: standard (Sonnet)
Expand Down Expand Up @@ -355,16 +380,21 @@ Prompt-driven (uses the Task/subagent mechanism), referencing the agents in 4.2.
- **Grep/Glob:** `path` missing or repo-root AND `glob`/`pattern` very broad
(`**/*` or `*`); OR path under a `scan_deny_paths` entry.
- **Read:** `file_path` under a `scan_deny_paths` entry.
- Action: emit
- Action: by default emit
`{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"deny","permissionDecisionReason":"[nexum] <why> — scope the search to a directory or add -maxdepth/path."}}`.
For Bash where a safe narrowing exists, prefer `updatedInput` to inject a path/
limit instead of denying (only when unambiguous; else deny).
When `grep_narrow_enabled` (default true) and the problem is *result volume* (an
unscoped/broad search, not a deny-path or traversal problem), prefer `updatedInput`
to cap the output instead: inject `head_limit` on the `Grep` tool, or append
`| head -n grep_head_limit` to an unscoped recursive Bash grep that does not
already pipe. Deny-path searches, Glob, already-piped greps, and a caller-set
`head_limit` still deny.
- Respect `scan_guard_enabled`; fail-open on any error.
- **EDGE CASES:** legitimately scoped commands must pass untouched; a command that
already has `-maxdepth` or an explicit non-root path → allow; never deny a plain
`Read` of a normal source file. False-positive risk is high — be conservative;
when unsure, ALLOW.
- **ACCEPTANCE:** `grep -r foo` → deny; `grep -r foo src/` → allow; `Read node_modules/x`
- **ACCEPTANCE:** `grep -r foo` → narrow to `grep -r foo | head -n 80` (or deny when
`grep_narrow_enabled` is false); `grep -r foo src/` → allow; `Read node_modules/x`
→ deny; `Read src/app.py` → allow; disabled flag → always allow.

### 5.2 `scripts/context_watch.py` (UserPromptSubmit) · TIER: standard
Expand Down
Loading
Loading