Commit graph

1192 commits

Author SHA1 Message Date
41fred
dd0039ab88 Add progress indicator and event-loop yielding to cold-cache Claude scan
A cold-cache scan over a large ~/.claude/projects tree (hundreds of
project dirs, e.g. a git-worktree-per-task workflow) ran long enough to
look hung and CPU-heavy enough on a single thread to visibly compete
with anything else running interactively. scanProjectDirs now emits a
`\r`-updated progress line (TTY-gated, so piped/captured output is
untouched) and yields to the event loop every 25 items during both the
directory-discovery and file-parsing passes.

Verified against a real ~934-dir / ~18k-session cold cache: 51s wall at
60% CPU with visible progress, vs. a previously silent single-threaded
block. Warm-cache totals unchanged; full test suite and tsc still pass.
2026-07-10 00:50:35 +02:00
Resham Joshi
c419d5fb6d
Merge pull request #650 from getagentseal/fix/isolate-provider-discovery
Isolate a throwing provider so one bad file can't blank every scan
2026-07-10 00:05:56 +02:00
AgentSeal
5f14a1f5ee fix(providers): isolate a throwing provider so one bad file can't blank every scan
Both provider-discovery loops (discoverAllSessions, and the menubar
provider enumeration in usage-aggregator) called discoverSessions with no
try/catch, so any provider that threw — a crafted or corrupt file reaching
a string op — took down usage discovery for all 32 providers at once,
blanking the whole dashboard, CLI, and menubar.

Add safeDiscoverSessions: wrap discovery in try/catch, warn once per
provider per run to stderr (matching the per-file parse-failure isolation
already in parser.ts and the whole-data-loss notices in fs-utils), and
skip the provider instead of aborting. Both loop sites use it.
discoverAllSessions takes an injectable provider list so the isolation
loop itself is tested; mutation-verified that reverting either call site
fails the new loop-level test.

Follow-up (separate): surface a structured 'incomplete discovery' signal
in the JSON/MCP payload so resident consumers can distinguish a real $0
from a dropped provider, and scope the warn-dedup per request rather than
per process lifetime.
2026-07-10 00:00:22 +02:00
Resham Joshi
23f118879c
Merge pull request #649 from getagentseal/merge/lingtai-tui-hardened
Add LingTai TUI provider (hardened) — supersedes #636
2026-07-09 23:51:02 +02:00
AgentSeal
308bec0ce6 fix(lingtai): harden manifest parsing against wrong-typed fields
A planted or corrupt .agent.json that is valid JSON but has a non-string
field (e.g. agent_name as an object) reached sanitizeProject().trim() and
threw. discoverAllSessions loops providers with no try/catch, so that one
file broke usage discovery for every provider, not just LingTai. Normalize
the manifest to string-or-undefined at read time; add a wrong-typed-field
regression test.
2026-07-09 23:48:46 +02:00
ZacharyHu0
a485087b5b Add LingTai TUI provider support 2026-07-09 23:47:59 +02:00
Resham Joshi
ab27a4cd82
Merge pull request #648 from getagentseal/fix/menubar-energy
menubar: cut idle energy (video-call-class drain, fixes #647)
2026-07-09 22:51:46 +02:00
AgentSeal
3befd8554c fix(menubar): Usage Refresh picker label now reflects the selection
The computed Binding wrote UserDefaults but nothing invalidated the
view, so the picker visually stayed on Auto while the value silently
landed. Back it with AppStorage so selection updates render.

Refs #647
2026-07-09 22:48:07 +02:00
AgentSeal
121181e13b fix(menubar): move Usage Refresh setting to the General tab
It controls the app-wide payload refresh loop, not anything
Claude-specific; Quota Refresh stays in the Claude tab where the
Anthropic rate-limit note applies.

Refs #647
2026-07-09 22:48:07 +02:00
AgentSeal
fd3ec8707c fix(menubar): manual cadence never respawns from stuck-refresh cleanup
Stale-force or stale-loading recovery could set shouldForceRefresh even
with a nil interval, so Manual mode (and its wake path) still spawned
after a stuck fetch. Cleanup now clears state without respawning unless
the cadence allows auto-spawns; the next user interaction fetches.

Refs #647
2026-07-09 22:48:07 +02:00
AgentSeal
b4c04e7beb feat(menubar): user-selectable usage refresh cadence (Auto / 1m / 5m / 15m / Manual)
Settings gains a Usage Refresh picker mirroring the quota cadence
pattern. Auto keeps the adaptive default (30s active, battery and Low
Power backoff); fixed cadences ignore power state but an open popover
always gets the active 30s; Manual never auto-spawns, refreshing only
on popover open, Refresh Now, and first launch (wake included: manual
wakes run stuck-state cleanup without spawning). Stored in UserDefaults
as CodeBurnMenubarRefreshSeconds (-1 auto, 0 manual, else seconds) and
documented in the README with the defaults-write escape hatch.

Refs #647
2026-07-09 22:48:07 +02:00
AgentSeal
6a08a8f31c fix(menubar): cut idle energy by gating spawns on popover, power, and display state
Every 30s tick spawned 2-3 concurrent Node CLI processes at 100%+ CPU,
and an app-lifetime activity assertion disabled App Nap forever, putting
the menubar's energy use in the video-call class (#647).

- Closed popover now fetches only what the status item renders (menubar
  period plus today); the popover's selected key refreshes on open via
  the existing recovery path, which now also catches up the status
  figure and clears a latched display-sleep flag.
- Spawn cadence stretches to 150s on battery and 300s in Low Power Mode
  while the popover is closed; the 30s timer itself keeps running.
- The app-lifetime beginActivity is gone; each in-flight refresh
  (including manual Refresh Now) holds its own scoped activity, and an
  NSBackgroundActivityScheduler backstop tick every ~3min bounds status
  staleness once App Nap starts coalescing the timer.
- Display sleep skips ticks entirely; wake and popover open clear it.
- The status-payload fallback honors the same spawn interval so the 30s
  cache TTL cannot defeat the backoff.

Child process QoS stays .userInitiated on purpose: lowering it is
documented to slow parses 5-10x and starve the cadence; the win here is
spawn frequency, not priority. Measured on an idle machine: two
concurrent 100%+ CPU spawns per 30s window before, one spawn in 72s
after, with App Nap coalescing the rest.

Fixes #647
2026-07-09 22:47:30 +02:00
Resham Joshi
270ee81b45
Merge pull request #646 from getagentseal/fix/unpriced-model-visibility
Surface unpriced models instead of silently counting them at $0
2026-07-09 21:39:44 +02:00
AgentSeal
4018a0ba99 fix(tui): keep the model-alias hint visible when the unpriced footer truncates
At 80 columns the By Model footer truncated mid-command. The flagged
models are already visible as $0 rows in the table above, so put the
fix command before the names and let the names truncate instead.

Refs #638
2026-07-09 21:27:33 +02:00
AgentSeal
bf6eee00f3 fix(report): only exact zero-rate overrides suppress the unpriced flag
getModelCosts resolves exact overrides before any table hit, so an exact
zero-rate override provably priced the model and means free. Prefix and
case-insensitive overrides resolve after table hits: a zero-rate stub
shadowed by one still reports $0 from the stub, so it stays flagged.

Refs #638
2026-07-09 21:22:23 +02:00
AgentSeal
e6162f6b50 fix(report): address unpriced-detector review findings
Adversarial review found two false-negative paths and two polish items:

- Drop the reverse-display-name suppression. Droid prices the lowercased
  display name and gets $0, so a $0 row keyed "Sonnet 4.6" is real
  uncounted spend; suppressing it because claude-sonnet-4-6 prices today
  hid exactly the failure this feature exists to expose.
- Treat zero-rate pricing hits as unpriced. LiteLLM ships [0,0] stubs
  for models it lists without a price; a stub hit means unknown price,
  not free. Explicit zero-rate user overrides still count as free.
- Make MenubarPayload.current.unpricedModels optional for source
  compatibility with payload producers that predate the field.
- Replace localeCompare with a byte comparison so tied rows sort
  identically on every host locale.

Refs #638
2026-07-09 21:17:47 +02:00
AgentSeal
231e7cc0d4 feat(report): surface unpriced models across overview, JSON, dashboard, and MCP
Models missing from the pricing tables silently contributed $0 to every
total with no indication anywhere (the warning was gated behind
CODEBURN_VERBOSE). Add render-time detection that flags aggregated model
rows with usage but $0 cost whose pricing lookup fails right now, and
surface it in overview, report/today/month JSON (unpricedModels), the
TUI By Model panel, and the MCP get_usage summary.

Render-time detection covers cached sessions (cost is computed at parse
time) and heals as soon as pricing data, an alias, or a price override
arrives. Rows with real cost are never flagged: aggregation keys rows by
display name, which the pricing lookup misses; $0 display-name rows are
reverse-resolved to their raw id before flagging so sessions cached
before their model's pricing landed don't get a misleading alias hint.
Local models and model-savings mappings stay excluded: $0 is correct
for them.

Fixes #638
2026-07-09 21:04:56 +02:00
Resham Joshi
1a75484970
Fix model pricing variant resolution
Some checks are pending
CI / semgrep (push) Waiting to run
2026-07-08 23:02:34 +02:00
AgentSeal
f2fa575a52 Fix model pricing variant resolution 2026-07-08 22:55:44 +02:00
Resham Joshi
74b1f1e14d
Fix menubar install release lookup 2026-07-08 21:43:24 +02:00
AgentSeal
75b6925749 Fix menubar install release lookup 2026-07-08 21:36:20 +02:00
Leonardo V M Claudio
cf07b6515a feat(kiro): add CLI tool-name mappings and MCP passthrough
- Map code → Read, subagent → Agent, web_fetch → WebFetch in toolNameMap
- Pass through mcp__* prefixed tools unchanged in toolDisplayName
- Add tests for CLI tool normalization and MCP passthrough
2026-07-06 10:14:44 -03:00
ozymandiashh
f8543ac8fc fix(currency): resolve RON symbol to "lei" across all surfaces
RON was added to the native macOS/GNOME pickers but not to the shared
SYMBOL_OVERRIDES map, so the CLI, CSV/JSON export, and web dashboard
resolved it through Intl to "RON" while native UI showed "lei" — a
cross-surface split for GNOME/CLI-set currency. Mirror the CNY precedent
(906c533): symbol override + fraction-digits test + README example.
2026-07-06 03:43:03 +03:00
morpheus9393
cc6d84a6ca
fix(opencode): merge legacy file sessions and SQLite DB instead of short-circuiting (#622)
Some checks failed
CI / semgrep (push) Has been cancelled
2026-07-06 02:37:45 +03:00
Resham Joshi
d41ca116d0
Merge pull request #608 from NihalJain/feat/jetbrains-copilot-tracking
Some checks are pending
CI / semgrep (push) Waiting to run
feat(copilot): track GitHub Copilot JetBrains IDE usage
2026-07-05 15:38:14 +02:00
AgentSeal
d2edf8c9da fix(copilot): content-derived JetBrains dedup keys; isolate the new env var in tests
Maintainer follow-up:

- Derive JetBrains dedup keys from the reply content (sha256 prefix plus a
  per-hash occurrence counter) instead of the blob's scan position. Copilot
  is a durable provider: cached turns are never deleted and a re-parse
  appends any unseen key, while MVStore compaction can rewrite the store
  with blobs in a different byte order. With positional keys, a rewrite
  that moves a new blob ahead of an old one hands the new turn the old
  key (skipped as seen) and re-emits the old turn under a fresh index,
  double-billing it. Covered by a regression test that fails on the
  positional scheme.
- Add CODEBURN_COPILOT_JETBRAINS_DIR to the env-isolation cleared list so
  a developer's real JetBrains store never bleeds into fixture tests.
2026-07-05 15:36:49 +02:00
Resham Joshi
58152ab9f2
Merge pull request #609 from NihalJain/fix/overview-locale-number-grouping
fix(overview): pin number grouping to en-US so output is locale-indep…
2026-07-05 15:32:22 +02:00
Resham Joshi
6b722c4278
Merge pull request #618 from Enclavet/fix-kiro-ide-parsing
fix(kiro): Fix Kiro IDE session parsing — context.messages, .kiro-server path, workspace-sessions
2026-07-05 15:11:07 +02:00
AgentSeal
926dcdb7fc fix(kiro): harden ws-session parsing and cover the stale-cache path (#619)
Maintainer follow-up on top of the parsing fix:

- Replace the placeholder cache tests with a regression test that runs the
  full parseAllSessions() pipeline against a seeded session-cache.json: a
  zero-turn entry at the current fingerprint is honored (control), and a
  pre-fix fingerprint forces the re-parse that recovers the missed calls.
- Treat history items carrying an executionId as execution-backed regardless
  of their text, so a stub-text change can never reintroduce double-counting;
  the 'On it.' check remains for stubs whose ref rides a separate item.
- Resolve the workspace-session timestamp before consuming the dedup key,
  and drop the call when stat fails instead of fabricating a current time.
- Read selectedModel through stringField instead of an unchecked cast.
- Import stat statically.
2026-07-05 15:02:25 +02:00
Andrew Lee
9c2ac6e22a fix(kiro): parse context.messages, add .kiro-server path, extract usageSummary tools, workspace-sessions
Four fixes for the Kiro IDE provider:

1. Add 'entries' to extractText() key list — Kiro IDE stores message
   content in context.messages[].entries (not .content), causing the
   parser to extract 0 chars from every execution file.

2. Check data.context[key] for conversation arrays in parseModernExecution
   — current Kiro builds store messages at data.context.messages, not at
   the top-level data.messages path the parser was checking.

3. Scan both ~/.kiro-server/data/... AND ~/.config/Kiro/... on Linux —
   remote dev boxes use .kiro-server while local installs use .config/Kiro.
   Both can have data simultaneously; the old code short-circuited on the
   first path found.

4. Discover and parse workspace-sessions/<base64>/*.json files — newer
   Kiro builds write session state here with history[].message format.
   Skips stub entries (executionId refs + 'On it.' only) to avoid
   double-counting with execution files parsed separately.

5. Add kiro: 'ide-parsing-v1' to PROVIDER_PARSE_VERSIONS for automatic
   cache invalidation — users upgrading from the broken parser will get
   a fresh re-parse without manually clearing session-cache.json.

Bonus: Extract tool names from usageSummary[].usedTools, add chatSessionId
to session ID resolution, add Kiro-specific tool name mappings.

AI-Origin: human
2026-07-05 01:28:33 +00:00
ozymandiashh
1678cbd883
Merge pull request #599 from Enclavet/fix-cursor-projects
Some checks failed
CI / semgrep (push) Has been cancelled
Fix cursor project parsing
2026-07-03 19:44:48 +03:00
Nihal Jain
cd07707363 fix(copilot): parse JetBrains agent sessions from old plugin format (≤1.5.x)
JetBrains Copilot plugin ≤1.5.x (e.g. 1.5.59-243) stores all session turns
inside ONE large binary-framed outer Nitrite document, rather than the
per-turn {"__first__":{"type":"Subgraph",...}} blobs introduced in later
plugins (≥1.12.x, e.g. 1.12.1-251).

In the old format each assistant turn is a UUID-keyed Value entry whose
value field contains a JSON-string-escaped AgentRound record:

  {"<uuid>":{"type":"Value","value":"{\"type\":\"AgentRound\",
    \"data\":\"{...reply...}\"}"}, ...}

The extractResponseText depth-unescape loop already handles this one extra
level of escaping; the only gap was that extractJetBrainsDbTurns never fed
it the outer document — it only scanned for __first__/Subgraph blobs, which
the old plugin never writes.

Add a fallback that activates when the Subgraph scan produces zero turns but
'AgentRound' text is present in the raw file (old-format signal). It locates
the binary-framed outer document (UUID-keyed Value entry, hex matched
case-insensitively so an uppercase UUID does not fall through to $0), extracts
it with matchJsonObject, and passes it to extractResponseText. Because the outer
document holds every turn in one blob, this emits ONE session-level call per
document (all rounds' replies joined): cost/tokens are correct, only the
per-turn call-count granularity is coarser — an accepted tradeoff for legacy
data. MVStore keeps two identical collection copies; seenReplies dedupes them.

The fallback is guarded by turns.length === 0 so new-format sessions (whose
Subgraph scan succeeds) are completely unaffected and never double-counted.

Tests: old-format doc with multiple AgentRound rounds → 1 call whose token
count equals the two non-empty replies joined (the empty tool-call round is
excluded); an uppercase-UUID variant (fails without the case-insensitive
match); and a guard that new-format Subgraph turns are not double-counted.
docs/providers/copilot.md documents the old format and the one-call-per-session
limitation.
2026-07-03 18:21:08 +05:30
Resham Joshi
4b8e74447e
Merge pull request #610 from getagentseal/feat/act-core
Some checks are pending
CI / semgrep (push) Waiting to run
Acting layer: optimize --apply, session guard, realized savings (opt-in)
2026-07-03 14:29:00 +02:00
AgentSeal
fea66a80d3 fix(act): report scale, corrupt-journal safety, and under-claim gaps (#606)
Bug-hunt follow-ups on the realized-savings report:

- Scale the displayed estimate to the measured window so the Estimated and
  Realized columns are comparable: mcp/archive use the per-session baseline
  times the post-window session count, read-edit uses deficitThen times the
  same edits denominator as realized (so realized never exceeds it). The
  at-apply estimate stays in --json as estimatedAtApply next to
  estimatedForWindow; the footer states the scaling and that mcp/archive
  realized figures are derived from session counts, not independently
  measured.
- Never crash on a corrupt journal: records without a string status or a
  parseable string `at` are skipped and surfaced ("N malformed records
  skipped"); the optimize header computation is additionally wrapped so any
  error just drops the header.
- Zero post-window sessions now reads "not measurable: no sessions in the
  window yet" instead of measured-zero.
- The optimize header sums only normal-confidence measured rows (under-claim);
  low-confidence rows stay visible in act report only.
- Tests: floor discipline on non-integer mcp and read-edit products (a ceil
  mutation fails), the project-scope keeper subtraction, the archive
  estimate==realized tautology, malformed-journal robustness, and the
  low-confidence header exclusion.
2026-07-03 13:36:36 +02:00
Nihal Jain
2916cd988e fix(copilot): read JetBrains agent-mode replies from AgentRound records
JetBrains Copilot has two turn shapes in the Nitrite .db:

- ask mode — the reply is a `Markdown` record's `text`;
- agent / plan mode (e.g. PyCharm agent sessions, `/plan …`) — the reply is the
  `reply` field of an `AgentRound` record, and the `Markdown` record instead
  holds the USER's prompt.

extractResponseText only read Markdown, so agent-mode turns yielded no reply
text: they were discovered (session/turn counts showed up) but priced at $0
because output tokens came out zero. On this machine that silently
under-counted a PyCharm session ($0 → $0.35) and several IntelliJ agent turns.

Determine the mode by the PRESENCE of an `AgentRound` record and read only that
record's `reply` (collecting every non-empty round in a multi-round blob).
Crucially, an agent blob whose reply is empty — a failed turn or a pure
tool-call round — does NOT fall back to the Markdown record, so a user prompt
is never mistaken for the assistant's output; such turns bill $0 as before.
Ask-mode blobs (no AgentRound) keep reading Markdown. Plan mode's sidecar
records — Thinking, PendingChanges (proposed diff, under `content`), AskQuestion,
Notification, SubTurn, and file-read `text` results — are never read as output.
Verified across all local stores: the two reply shapes never coexist in one
blob, so the split is unambiguous.

Tests: agent-mode reply extraction (ignoring the prompt Markdown), pure
tool-call rounds → $0, multi-round collection, and a failed agent turn → $0.
docs/providers/copilot.md documents both turn shapes and the ignored sidecar
records.
2026-07-03 17:06:14 +05:30
AgentSeal
ad471f3d8c feat(act): realized savings measurement (codeburn act report) (#606)
Capture a trailing-14-day before-baseline when a fix is applied and
re-measure it against the post-apply window so optimize can show realized
numbers next to estimates.

- ActionBaseline (windowDays, capturedAt, estimatedTokens, sessions, metrics)
  persisted by runAction; captured in the optimize --apply flow and at guard
  install time.
- codeburn act report [--json]: applied, not-undone actions older than 3 days,
  re-running the detectors over apply-date-to-now (capped 30 days). Per-kind
  realized deltas: MCP/archive tokens-per-session times saved sessions with
  reverted-by-user detection; read-edit deficit reduction; guard yield split
  labeled correlation. Bash cap is marked not measurable (result sizes are not
  retained). Low confidence under 20 post-window sessions or past a 2x volume
  shift. Realized numbers rounded down, estimate kept visible.
- optimize gains one header line only when a measured action exists, and
  appends "(previously applied <date>, re-flagged)" to re-triggered findings.
  No change for users with no applied actions.

Reuses scanAndDetect helpers over a date-bounded range; exports the token
constants and read/edit tool sets rather than duplicating the math.
2026-07-03 13:04:37 +02:00
Nihal Jain
4e6e02d08d fix(overview): pin number grouping to en-US so output is locale-independent
`codeburn overview` formatted token/call/session counts with a bare
`toLocaleString()`, which groups digits according to the host's locale. On an
Indian-locale machine (en-IN) a value like 2,002,000,000 renders as
2,00,20,00,000, so the rendered summary — and its snapshot test — differed by
machine. (formatCost already avoided this via an explicit thousands regex.)

Pin `formatTokens` to 'en-US' and route the remaining count columns through a
new `formatCount` helper that does the same, so the overview output is
byte-identical everywhere.

Verify: `npm test -- overview` (previously failed on non-US locales; the
"thousands separators" case now passes regardless of $LANG / ICU default).
2026-07-03 16:32:59 +05:30
Nihal Jain
ccc9deb2bd docs(readme): note Copilot OTel + JetBrains sources in the support matrix
The README "Data location" support matrix listed GitHub Copilot as only the
legacy CLI and VS Code transcript sources. Update the row to reflect all
sources the provider actually reads — the OpenTelemetry `agent-traces.db`
(preferred when present) and the JetBrains IDE Nitrite `.db` — and how the
project is resolved. Links to docs/providers/copilot.md for the full detail.
2026-07-03 16:23:54 +05:30
Nihal Jain
bae016c050 feat(copilot): track GitHub Copilot JetBrains IDE usage
## What & why

The JetBrains Copilot plugin (IntelliJ, PyCharm, RubyMine, …) stores its
chat/agent sessions under `~/.config/github-copilot/<ide>/<kind>/<storeId>/` —
a location none of the existing Copilot sources (CLI JSONL, VS Code chat
sessions/transcripts, OTel SQLite) read. As a result all JetBrains Copilot
usage was silently uncounted in every CodeBurn report. This adds a reader for
that store so those sessions are discovered, priced, and attributed to the
right project.

## How it works

- **Reader.** The store's session content is a Nitrite `.db` — an H2 MVStore of
  Java-serialized documents. It is scanned as `latin1` for byte-offset
  stability: no Java deserializer, no new dependency, and it is not SQLite so
  `node:sqlite` is not involved.
- **Reply text.** Assistant replies live in nested-escaped
  `{"__first__":{"type":"Subgraph"…}}` blobs. The text is recovered by
  unescaping one level at a time and, at the depth where the Markdown record's
  `data` field is a well-formed one-level-escaped JSON document, reading it
  structurally — so a reply containing its own quotes is never truncated or
  duplicated (which would otherwise inflate the estimate).
- **Tokens/cost.** The store records no token counts, so output tokens are
  estimated from the reply text (`CHARS_PER_TOKEN = 4`, re-decoded
  latin1→utf8 so multibyte replies count by codepoint) and every call is marked
  `costIsEstimated`. Failed generations (error status, no reply) are billed $0.
- **Sessions.** One `.db` holds many chat tabs; turns are grouped back to their
  conversation GUID so the UI shows one session per tab, deduped by reply
  content per conversation.
- **Project attribution**, most authoritative first:
  1. the plugin-recorded `projectName` field (JetBrains Copilot 1.12+), joined
     across kind dirs by store id — the billable turns live in
     `chat-agent-sessions`, but the label is usually written into the sibling
     `chat-sessions`/`chat-edit-sessions` store. Read length-delimited and
     re-decoded latin1→utf8 so non-ASCII repo names round-trip.
  2. the `.git` repo root of a referenced `file://` path.
  3. a generic `copilot-jetbrains` bucket when neither signal exists.
  The conversation title is a chat-thread name, not a project, so it is kept
  out of the project field and surfaced as the session label instead.

Override the JetBrains github-copilot root with
`CODEBURN_COPILOT_JETBRAINS_DIR`.

## Docs

- `docs/providers/copilot.md` — full JetBrains section (store layout, latin1
  scan, reply extraction, projectName precedence + cross-kind join).
- `docs/providers/README.md` — Copilot storage updated to note the Nitrite .db.

## How to verify

- `npm test -- copilot` and `npx tsc --noEmit` (fixtures reproduce the real
  nested-escaped .db framing, including quote- and multibyte-bearing replies).
- End to end against a real install:
  `CODEBURN_CACHE_DIR=$(mktemp -d) node dist/cli.js status --provider copilot \
     --period all --format menubar-json`
  — JetBrains sessions appear By-Project under their real repo names.
- Set `CODEBURN_COPILOT_JETBRAINS_DIR` to a fixture root to parse a controlled
  store without touching the real config dir.
2026-07-03 16:11:27 +05:30
AgentSeal
be2470d0e5 fix(guard): dedupe streaming message copies in the incremental cost fold
Claude Code rewrites each assistant message several times as it streams,
every copy carrying the full final usage; the shipped parser dedupes these
last-wins (dedupeStreamingMessageIds) but the guard fold summed every line,
measuring real sessions at 2.5-2.8x their true cost and false-blocking the
hard cap at roughly 40% of the configured spend.

- The session cache now maps message id -> that id's cost contribution and
  each id-carrying line replaces its previous contribution; id-less lines
  keep plain adds. Validated against two real transcripts (90MB and 116MB):
  guard totals now equal the shipped deduped totals exactly.
- Replace semantics also self-heal the trailing-line case: a complete final
  line without its newline is folded but byteOffset stops before it, so the
  next invocation re-reads it as a replace, not a double add.
- editCount becomes a set-once sawEdit boolean so duplicate copies of an
  edit tool_use cannot inflate it; cache schema bumped to v2 (old caches
  cold-reparse once).
- Per-session state moves to guard/sessions/ so a session id can never
  collide with the shared flags.json, dropping the doAllow special case.
- The git-commit detector now requires commit as the git subcommand at a
  command boundary (start of string or line, or ; & |), with intra-command
  gaps that never cross newlines: 'git log --grep commit' and
  'git diff && echo commit' no longer match, while newline-separated
  'git add ...\ngit commit' in multi-line Bash calls now does (verified as
  a real false negative on a live transcript).
- Corrected the statusline protocol note: each stdout line renders as its
  own row; we emit exactly one.
- New tests: streaming-duplicate fixtures (3x identical, growing last-wins,
  incremental replace) asserted equal to a cold shipped-parser computation,
  the trailing-partial-line scenario, the commit-detector matrix, and a
  stale-plan test proving guard-install plans carry expectedHash (a
  concurrent settings edit aborts the apply and survives). The act list CLI
  spawn test now anchors to the repo root from the test file location.
2026-07-03 12:32:46 +02:00
AgentSeal
dc182dc275 feat(guard): opt-in session-time hook pack for Claude Code (#605)
codeburn guard install|uninstall|status|refresh|allow plus the internal
hook/statusline handlers. Off by default, fully local, cleanly removable.

- Settings edits go through the action journal (guard-install / guard-uninstall)
  with expectedHash, appending our entries and removing exactly ours by command
  prefix; a byte-identical uninstall is asserted by test.
- PreToolUse budget cap (soft warn once, hard block with a per-session allow
  override), Stop yield checkpoint (expensive with no edits and no commit, once),
  and a flagged-project SessionStart opener built from the optimize detectors.
- Incremental per-session cache keyed by session id: resumes the transcript
  parse from the last complete-line byte offset via readSessionLines, folding
  only the tail into running totals. Warm invocation ~0.28s against a 90MB
  transcript, dominated by CLI startup; the tail parse itself is negligible.
- All handlers fail open: any error, malformed stdin, or missing transcript
  exits 0 with no output so a broken guard can never block a session.
- Hook protocol verified against the live docs (dated block at the top of
  hooks.ts); zero new dependencies.
2026-07-03 11:54:04 +02:00
AgentSeal
ab949f35a4 refactor(parser,optimize): export parseApiCall and lift session openers to constants
Guard (#605) reuses both as a single source of truth: parseApiCall is the
narrowest per-entry cost+tools extractor for the incremental transcript parse,
and the two session-opener strings become exported constants so the optimize
findings and the guard SessionStart hook can never drift.
2026-07-03 11:53:56 +02:00
AgentSeal
dcd47e65f5 fix(act): refuse to apply a plan whose target changed since it was built
Plans serialize the full post-edit file content at build time, so a
target edited between the preview and the interactive confirm would be
silently overwritten with stale content (recoverable via backup, but a
silent violation of the dry-run-shows-exactly-what-changes principle).

- PlannedChange edit/create variants gain optional expectedHash: sha256
  of the raw on-disk bytes the plan was built from (hashed before the
  BOM strip), null when the plan expects the file to be absent,
  undefined to skip validation (framework back-compat).
- plans.ts sets it everywhere a target is read: ConfigDocs hashes the
  raw buffer it parses, and the marker builders hash the file behind a
  shared markerChange helper.
- runAction validates all expected hashes after snapshotting and before
  the first mutation; a mismatch throws "<path> changed since the plan
  was built; re-run codeburn optimize --apply", removes the backup dir,
  and journals nothing. No rollback is involved since nothing mutated.

Tests: stale edit target rejected with no journal record and no backup
dir left, expects-absent plan rejected when the file appeared, matching
hash still applies, and a hash-less change still applies unchecked.
2026-07-03 11:20:59 +02:00
AgentSeal
92d970196e fix(act): scope project-scope removals, --yes safety, --only validation (#604)
Review fixes plus one coordinator amendment on top of the initial
optimize --apply implementation.

- mcp-project-scope no longer strips a server from every projects[*]
  container in ~/.claude.json: only the top-level entry and the
  finding's cold projects lose it, and the cwd's own config files count
  as cold only when the cwd is in the cold list. The plan preview
  annotates ~/.claude.json with the project entries that lose the
  server.
- unused-mcp findings are now appliable (remove-everywhere mcp-remove
  plans, same as low-coverage).
- Notes are rendered under manual findings too, so an all-unparseable
  config surfaces its parse error instead of a bare "manual".
- ConfigDocs strips a leading UTF-8 BOM before JSON.parse.
- claude-md plans are excluded from --apply --yes (they write to
  cwd/CLAUDE.md); they apply via the interactive picker or an explicit
  --only selection, and --yes prints them as skipped with the reason.
- --only with an unknown or not-appliable id errors to stderr with the
  run's valid appliable ids and exit code 2.
- EOF at the interactive prompt prints "Nothing applied." and exits 0;
  an answer that arrives together with EOF is still honored.

Adds end-to-end tests driving runOptimizeApply over injected stdio and
a fixture home: --yes output with journal ids and undo hints, picker
parsing, --only filtering and validation, EOF, claude-md skip, the
projects[*] over-deletion regression, manual-note rendering, and BOM
configs.
2026-07-03 11:14:06 +02:00
AgentSeal
0dd54366b7 feat(act): optimize --apply for config-class fixes (#604)
Add stable kebab-case finding ids to every optimize detector and to the
JSON report, then route the config-class findings through the action
journal so they can be applied and undone.

- optimize.ts: id on every WasteFinding and OptimizeJsonReport entry;
  appliable findings also carry a machine-readable apply payload (mcp
  server list, project-scope keepers, archive names).
- act/plans.ts: planFor(finding) builds concrete, journaled file
  mutations for mcp-remove, mcp-project-scope, skill/agent/command
  archives, CLAUDE.md rule blocks, and the bash output cap. JSON edits
  preserve the rest of the document (2-space indent, trailing newline);
  unparseable config files are reported and skipped, not fatal.
- act/optimize-apply.ts: codeburn optimize --apply with a plain-readline
  confirm, plus --yes, --dry-run, and --only; prints each journal id and
  the undo hint. --apply with --json exits 2.

Tests cover mcp remove/undo, project-scope global-to-project move,
unparseable-file skip, archive collision suffixing, CLAUDE.md marker
idempotency, a byte-identical dry-run tree hash, and a finding-id guard.
2026-07-03 10:41:29 +02:00
AgentSeal
2eec2fdc31 fix(act): support directory moves for archive actions
The archive actions in the acting epic move whole skill/agent directories,
which the framework could not handle: snapshotFile used copyFile (EISDIR on
a directory) and the afterHash pass used readFile. Snapshots now branch on
lstat, copying directory trees with fs.cp recursive. Directories get an
empty afterHash ('' means no content hash) and drift detection skips the
hash comparison for them; the occupied-original-path check still applies and
a missing movedTo still falls back to the backup. Backup restore likewise
branches: a directory snapshot replaces the target (rm then cp recursive).

Apply-side rename cannot replace a directory destination (ENOTEMPTY), so a
move retries once after clearing the already-snapshotted destination,
rethrowing other codes before any destination damage.

Tests: archive a directory tree and restore it byte-identical, move a
directory onto an existing destination directory (destBackup taken, both
trees restored), and dir-move undo with an occupied original path refusing
without --force and overwriting with it.
2026-07-03 10:14:48 +02:00
AgentSeal
d756eacae5 fix(act): harden undo, apply, and locking against review findings
Undo no longer clobbers files it did not create: an occupied original path
counts as drift for moves (--force removes then renames back), a move
destination that already exists is snapshotted (destBackup) and restored
after the file moves back, and a missing moved file falls back to the source
snapshot so forced undo cannot die mid-loop. Non-move reverts now key on
backup presence instead of the op label, which also restores files that a
create overwrote.

Apply snapshots once per unique path, hashes after all mutations so
overlapping changes carry the final state, and journals inside the rollback
region so a failed append reverts the mutations. The lock is taken with a
single wx write and goes stale by mtime only, so a fresh lock can never be
stolen while empty. Drift reads treat any unreadable target as drift with
its error code, ambiguous id prefixes report the match count, undo --last
skips already-undone records ("Nothing to undo."), and readRecords only
swallows ENOENT.

Tests cover each new behavior plus two mutation probes (forward-order revert
and removed locking both fail the suite), and a CLI-level check of
`act list --json` output shape and ordering.
2026-07-03 10:07:22 +02:00
AgentSeal
883580e6b9 feat(act): action journal, backups, and undo core
Add src/act, a dependency-free framework for journaling and reverting any
file CodeBurn modifies. runAction is the single mutation path: it snapshots
every target, applies the changes, then appends a JSONL record, rolling back
completed steps and journaling nothing if a mutation throws midway. Undo
checks each file against its post-apply sha256 and refuses on drift unless
forced, restoring edits, deletes (created files), and moves. A pid plus
timestamp lockfile (stale after 60s) guards apply and undo, and the journal
reader tolerates corrupt lines with last-line-wins status updates.

Wire up `codeburn act list` (table or --json) and
`codeburn act undo <id|--last> [--force]`. Storage lives under the existing
config home via the config.ts resolver. Tests cover apply with backups and
afterHash, byte-identical undo per op type, drift refusal and --force,
mid-apply rollback, and corrupt-journal tolerance.
2026-07-03 09:39:33 +02:00
ozymandiashh
c913fc5cf6 Add Romanian Leu (RON) currency support to menubar and GNOME extension
The CLI already supports any ISO 4217 code dynamically via Intl.NumberFormat,
but the macOS menubar app (SupportedCurrency enum) and GNOME extension
(hardcoded CURRENCIES array) had fixed lists. RON was missing from both.

- Add RON case to SupportedCurrency enum + displayName in AppStore.swift
- Add RON symbol override ('lei') in CurrencyState.swift
- Add RON entry to CURRENCIES array in gnome/indicator.js
2026-07-03 01:24:46 +03:00
Resham Joshi
8e55b5ff55
Merge pull request #598 from getagentseal/fix/menubar-plan-quota
Some checks are pending
CI / semgrep (push) Waiting to run
menubar: fix Codex plan tab copy, show model-scoped weekly limits (Fable)
2026-07-02 20:02:08 +02:00