Commit graph

13 commits

Author SHA1 Message Date
Aditya Vikram Singh
33dc05bb90 fix(sync): trust only proven outbound project metadata 2026-08-24 16:05:35 +05:30
Andrew Lee
50c8251719 fix(sync): close credential-leak paths; session retraction; span/key/CLI hardening
Review rounds 2-3 + self-review on --attribution:

Credential egress (round 2):
- normalizeRemoteUrl: scp userinfo expressed as an optional regex group
  let backtracking re-parse a credential prefix as host:path
  (x-access-token:ghp_...@host/repo -> token in git.repo). Userinfo is
  now split off at the first @ BEFORE any host matching.
- Positive validation (allow-list) as the final gate on EVERY branch:
  host must be hostname-shaped, every path segment repo-shaped, total
  identity <= 200 chars. Kills transport-helper remotes (ext:: leaks
  local SSH key paths, codecommit:: leaks AWS profile names), residual
  @, spaces/colons, and unbounded strings.
- sanitizePrLinks: links are rebuilt from origin + pathname — userinfo,
  query strings, and fragments are dropped instead of passed through;
  collapsed duplicates dedupe.

Attribution correctness (round 3 + self-review):
- Double-count fix with precise retraction semantics: when a commit
  migrates to a later-parsed tighter-window session, the loser re-emits
  git.commit_count=0. Empty records are emitted ONLY on a true loss in
  THIS computation (lostCandidacy) — a commit that merely aged out of
  the --since range was lost to nobody, and retracting it would
  permanently zero a still-correct server-side count. The sync layer
  additionally requires a prior ledgered state for the session.
- Session dedup key includes project + both window timestamps, so
  ongoing sessions re-emit with corrected span times.
- Span end times clamped like the usage builder (never 0, never
  earlier than start + 1ms).
- CLI mirrors the usage path on attribution push failures instead of
  claiming success.
- Identity normalization: case-insensitive .git strip, doubled path
  slashes collapse.

AI-Origin: human
2026-08-02 12:56:59 +00:00
Andrew Lee
ccee28ae82 fix(sync): address attribution review — cwd-fallback egress, Windows paths, PR-link validation
Review findings on the --attribution PR:

- Privacy: sessions whose project path no longer resolves inherited the
  cwd-fallback repo identity, egressing whatever (possibly confidential)
  repo the user pushes from and falsely attributing its commits.
  buildRepoGroups now tracks per-session identity provenance; the
  attribution path excludes fallback sessions from commit attribution
  entirely (no repo, no commits, PR links only) — they also can no
  longer steal a commit from a genuine session's window.
- Privacy: Windows drive-letter paths (C:/..., C:\..., drive-relative)
  parsed as scp-like remotes, emitting local filesystem paths as repo
  identities. normalizeRemoteUrl rejects drive letters and
  single-character hosts (dotless intranet hosts still accepted).
- Hardening: PR links are shape-checked before sending (https,
  /org/repo/pull/N path, <=256 chars, max 20 per session) — upstream
  parsers only truthiness-check them.
- Safety valve: MAX_ATTRIBUTION_PER_PUSH (10k) caps a first
  --since all --attribution push; dry-run reports the cap.
- Tests: adversarial normalize corpus, cwd-fallback egress repro,
  commit-stealing prevention, PR-link sanitization, and CLI-level tests
  (mock IdP + collector): dry-run sends nothing to the traces endpoint,
  flag-off emits no attribution span names on the wire.
- Docs: reconciled the 'never sent' wording with reality (PR links ride
  even when repo is null; device_id/methodology/timestamps disclosed).
  CHANGELOG Unreleased entry added.

AI-Origin: human
2026-08-02 12:29:18 +00:00
Andrew Lee
1bf7206842 feat(sync): push git attribution spans with --attribution
Expose the yield session-to-commit correlation through codeburn sync so
backends can join AI usage to git activity without local git hooks.

- yield: export normalizeRemoteUrl (host/org/repo; credentials, ports,
  and .git stripped) and computeAttributionRecords, which reuses the
  exact repo-grouping + tightest-window attribution from computeYield
  (extracted into a shared buildRepoGroups) and joins in the normalized
  origin remote and session prLinks.
- otlp: two new span types sharing the session traceId —
  codeburn.session.attribution (git.repo, git.pr_links, git.commit_count)
  and codeburn.commit (git.sha, git.in_main, git.was_reverted). Resource
  attribute codeburn.attribution_methodology=timestamp-window marks the
  attribution as inferred.
- push: generic send core reused by usage and attribution batches. Dedup
  keys encode mutable state (inMain/wasReverted), so a state transition
  re-sends the updated fact while identical states dedupe via the
  existing sent-ledger.
- cli: opt-in --attribution flag on sync push (dry-run aware); commits
  in repos with no network remote are never sent.

AI-Origin: human
2026-08-02 12:28:13 +00:00
ozymandiashh
2c9871b2f2 fix(yield): case-insensitive canonical repo grouping via realpathSync.native
Follow-up to #719, closing the two deltas flagged in the #714 discussion:

- fs.realpathSync preserves the caller's path casing on case-insensitive
  filesystems (macOS APFS, Windows), so the same repo reached through
  differently cased paths produced distinct canonical keys and split into
  separate groups. fs.realpathSync.native returns the on-disk casing, making
  the canonical key truly canonical. Regression test creates a real repo,
  references it through altered casing (guarded to case-insensitive
  filesystems), and asserts one group.

- Documents the accepted residual: a main repo moved on disk after
  'git worktree add' leaves git's linked-worktree metadata stale, so its
  worktrees may not unify; that is git-state staleness, not a grouping bug.
2026-07-19 03:12:03 +03:00
iamtoruk
b95cb95ce4 fix(yield): group commit attribution by canonical repo, not launch path
Sessions launched from monorepo subdirectories or separate worktrees
of one repository formed independent attribution groups, each awarding
the same commit (double-counted productive sessions; the duplicate
owner was not even flagged ambiguous). Groups now key on the realpath
of git-common-dir, so all subdirs and worktrees of a repo collapse
into one group while distinct repos stay separate; ambiguity semantics
unchanged, now correctly spanning the whole repo. rev-parse results
cached per directory.

Fixes #713. Regression tests fail on the unfixed code.
2026-07-16 18:58:49 -07:00
Aditya Vikram Singh
f2864ff964
yield: single-owner commit attribution, ambiguous bucket, heuristic caveat (#692)
Some checks are pending
CI / semgrep (push) Waiting to run
* yield: single-owner commit attribution, ambiguous bucket, heuristic caveat (#641)

* yield: repo-level single-owner attribution, sharper ambiguous rule, determinism pins

---------

Co-authored-by: Aditya Vikram Singh <247195684+avs-io@users.noreply.github.com>
2026-07-16 13:09:26 -07:00
iamtoruk
a916981107 feat(cli): yield --provider filter
yield always analyzed all providers, so provider-filtered GUI panels
(cost per outcome, reverts, abandoned) showed all-provider numbers
under a filtered header.
2026-07-16 01:36:59 -07:00
barry
a2947a48be Add JSON output for optimize and yield
Dashboard integrations need machine-readable optimize findings and yield ratios without parsing terminal output. This threads the existing analysis results into conservative JSON serializers while preserving text output as the default.

Constraint: Issue #419 asks for dashboard-friendly JSON for optimize and yield

Rejected: Build a separate analysis path | would risk drift from terminal output

Confidence: high

Scope-risk: narrow

Tested: npm test

Tested: npm run build

Tested: npm run dev -- optimize -p today --format json

Tested: npm run dev -- yield -p today --format json

Not-tested: Real downstream dashboard integration
2026-06-18 12:13:17 +02:00
Resham Joshi
afd0ee7011
Validator hardenings on the bug-hunt batch (#254)
* Five correctness fixes from multi-agent bug hunt

A multi-agent audit of the codeburn correctness surface found five
real bugs each producing visibly wrong numbers or risking data loss.
All five fixes were validated by parallel review agents and exercised
end-to-end against real session data on this machine.

- src/cli.ts: --refresh <seconds> was using bare parseInt as the
  commander callback. Commander invokes the callback as
  parseInt(value, previous), so previous becomes the radix:
  --refresh 30 was being parsed as parseInt('30', 30) = 90, and
  --refresh 60 became NaN. Replaced with parseInteger (already
  defined at line 48 with radix locked to 10) at all three sites.

- src/providers/cursor.ts: parseAgentKv was timestamping every
  agentKv call as new Date().toISOString() because the Cursor
  SQLite schema has no per-message timestamp. Result: every
  Cursor agent call regardless of when it happened landed in
  today's date bucket. Now uses statSync(dbPath).mtimeMs as a
  bounded ceiling so calls land at the actual last-write time of
  the Cursor database, not today. Verified locally: a 1904-call
  Cursor history with March 22 mtime now correctly bucket into
  all-time only and shows 0 calls for today/week/30days.

- src/providers/codex.ts: prev token counters were only updated
  inside the cumulative-fallback branch, so a session emitting N
  events with last_token_usage followed by one cumulative-only
  event computed the next delta against prev=0 and double-counted
  the entire cumulative window. Cost could be inflated 10-100x
  for any mixed-format Codex session. Now prev advances to the
  current cumulative state regardless of which branch ran.

- src/providers/gemini.ts: totalOutput accumulated output+thoughts
  while totalThoughts was tracked separately. The result was
  outputTokens = output+thoughts AND reasoningTokens = thoughts;
  any consumer summing the two double-counted thoughts. Now
  totalOutput holds just output, reasoningTokens holds thoughts,
  and the cost calc folds thoughts into the output count to keep
  pricing correct (Google bills thoughts at the output rate;
  calculateCost has no reasoning parameter).

- src/export.ts: exportJson had no safety check before writeFile,
  so codeburn export -f json -o ~/important.json would silently
  clobber the user's file. CSV path had a marker-file guard; JSON
  did not. Now refuses to overwrite a file unless its first 4KB
  contain the codeburn schema marker. Uses a streaming partial
  read so a large existing file does not OOM Node's ~512MB
  string limit. Refuses directories outright.

Skipped intentionally: cursor-auto/copilot-auto/cline-auto/
qwen-auto are aliased to claude-sonnet-4-5. The audit flagged
this as wrong pricing for non-Anthropic auto-routed turns, but
Cursor's "auto" mode does not expose the actual model and any
alternative estimate is equally arbitrary. README already
documents this as a Sonnet-based estimate.

vitest run: 38 files, 529 tests pass.

* Five more correctness fixes from the bug-hunt round

This commit closes out the remaining critical-tier findings from the
multi-agent audit, with one item documented as a known limitation.

- src/providers/cursor.ts: bubble dedup key included mutable
  inputTokens/outputTokens. Cursor mutates token counts on the row in
  place when streaming completes, so re-parsing the same DB produced
  a fresh dedup key per bubble and silently double-counted. Switched
  to the SQLite row key (`bubbleId:<unique>`) which is stable per
  bubble. Adjusted BubbleRow type and BUBBLE_QUERY_BASE to expose
  `key as bubble_key`.

- src/providers/pi.ts: usage fields were destructured non-optionally,
  but real Pi/OMP session files sometimes omit individual fields.
  `calculateCost(model, undefined, ...)` returned NaN, and that NaN
  propagated into every aggregate cost total. Coerce each field to
  0 with `?? 0`.

- src/models.ts: getShortModelName and the getModelCosts startsWith
  fallback both walked the dictionary in insertion order. A model id
  like `gpt-5-mini` could resolve to the entry for `gpt-5` (matched
  by startsWith first) and silently get GPT-5's display name and
  pricing tier. Iterate longest keys first so more-specific prefixes
  win. Tightened the cost fallback's match condition from
  `startsWith(key) || startsWith(key + '-')` to require either an
  exact match or a `key + '-'` continuation, removing accidental
  matches like `gpt-50` against `gpt-5`.

- src/models.ts: calculateCost returned 0 silently for any model
  missing from the pricing snapshot. New Anthropic / OpenAI models
  shipped between snapshot refreshes look free until the user
  notices. Now warns once per unknown model name per process to
  stderr. Skips the warning for the `<synthetic>` placeholder so
  the noise floor stays low.

- src/yield.ts: revert detection was broken on the canonical case.
  Two problems: (1) `subject.toLowerCase().includes('revert')`
  matched any commit whose subject mentioned the word ("Add revert
  button" was misclassified). (2) The window logic only counted
  reverts within the original session's 1-hour boundary, but real
  `git revert` commits land in later sessions, so original sessions
  always looked productive. Now: getRevertedShas runs once with
  `--grep=^This reverts commit` and parses bodies to build a Set of
  SHAs that were the target of a revert anywhere in history.
  CommitInfo.wasReverted is set when this commit's SHA appears in
  that set. categorizeSession then flags a session as reverted when
  its in-main commits were later reverted, regardless of when the
  revert itself happened.

- src/providers/droid.ts: SKIPPED with comment. Droid records token
  usage only at session level. The current behavior splits evenly
  across emitted assistant calls and prices all of them at
  settings.model (the latest model). For sessions where the user
  switched models mid-stream, costs are approximate. Added an
  inline comment documenting this; a real fix requires per-message
  model data that isn't in the Droid JSONL schema.

Verified end-to-end on this machine:
- vitest run: 38 files, 529 tests pass
- `codeburn report --format json` produces valid JSON
- `codeburn yield -p week` runs without crashing, finds 0 reverts
  in the user's recent git history (plausible — fix changed the
  detection from "subject contains revert" to "this commit's SHA
  appears in a later 'This reverts commit ...' body")
- Stderr now warns for unknown model ids: `openai/gpt-5.3`,
  `qwen3.6:35b-a3b-bf16`, `big-pickle`. These previously priced
  silently at $0.

* Four high-severity fixes from the bug-hunt round

- src/currency.ts: getExchangeRate wrapped fetchRate and cacheRate in
  one try/catch. If fetchRate succeeded but cacheRate threw (disk
  full, ENOSPC, no permissions on the cache dir), the catch block
  swallowed the error and returned 1. Every cost rendered after that
  point became USD-equivalent silently. Now the fetch and the cache
  write live in separate paths: a successful fetch returns the rate
  even if the persist fails, and the cache-write error is dropped to
  a fire-and-forget so transient disk problems do not corrupt the
  user's currency display.

- src/cursor-cache.ts: writeFile was non-atomic. Two concurrent
  codeburn invocations writing to cursor-results.json could
  interleave bytes mid-write, leaving a truncated file that
  parsed-error on next read and forced a full SQLite re-scan every
  run. Switched to the temp-file + rename pattern with a randomized
  temp name so each writer gets its own staging file and the rename
  is atomic on POSIX. Crash mid-write also leaves only a leftover
  temp file, which gets unlinked in the catch path; the destination
  is never half-written.

- mac/.../CodeBurnApp.swift refresh loop on sleep: the loop's
  Task.sleep keeps a wakeup pending across system sleep, so on wake
  the natural tick fires the same instant the wake observers do.
  Combined with didWakeNotification, screensDidWakeNotification, and
  the launchd com.codeburn.refresh distributed notification, that
  produced 2-3 concurrent CLI spawns within ms of every wake. Now:
  willSleepNotification cancels the loop task; didWakeNotification
  restarts it. The loop also reads lastRefreshTime and skips its
  natural tick if a wake/manual/distributed-notification refresh ran
  within the last 5 seconds, coalescing the two sources of refresh
  into one CLI spawn per wake event.

- mac/.../CodeBurnApp.swift observeStore: the read closure had an
  implicit strong self capture (it accessed store.* without a
  capture annotation), pinning self for the lifetime of any
  unfired observation. Added [weak self] and a guard to make the
  capture explicit. withObservationTracking is one-shot per call,
  so there is at most one active subscription at a time; the
  earlier audit's claim of an unbounded leak overstated the issue,
  but tightening the capture pattern is still cleaner.

Verified:
- vitest run: 38 files, 529 tests pass
- swift build -c release --arch arm64 --arch x86_64: clean, no
  diagnostics, no MainActor warnings
- mac/Scripts/package-app.sh dev produces a valid universal bundle
- Menubar launches and runs without crash

* Eleven medium-severity fixes from the bug-hunt round

- src/format.ts formatTokens: guard against Infinity, NaN, and
  negative input. Previously a corrupt aggregate could leak into
  the UI as the literal strings "NaN" or "Infinity". Negatives now
  render as "0" rather than "-500" with no scaling.

- src/cli-date.ts parseDateRangeFlags: the missing-from default
  was new Date(0), which opened a 55-year scan from 1970 epoch
  whenever the user passed only --to. Default now anchors at 6
  months back from now, matching the dashboard's all-time period.
  Test updated to assert the new bounded window.

- src/cli-date.ts toPeriod: previously fell back silently to "week"
  for any unknown input, so a typo like `-p mounth` produced a
  quiet 7-day report while the user thought they were viewing the
  month. Now exits with a clear stderr error and exit code 1.
  Test updated to assert the loud-failure behavior.

- src/optimize.ts urgencyScore: rebalanced weights so a high-impact
  finding with zero observed tokens cannot outrank a medium-impact
  finding with millions of tokens. Old 0.7/0.3 split made high+0
  (0.70) beat medium+1B (0.65). New 0.5/0.5 split makes medium+1B
  (0.75) beat high+0 (0.50). Token normalization lifted to 5M so
  the ramp covers a realistic spend range.

- src/models.ts calculateCost: clamp negative or non-finite token
  inputs to 0 before pricing. A corrupt JSONL emitting a negative
  count would otherwise produce a negative cost that silently
  subtracted from real spend in aggregates.

- src/currency.ts convertCost: stop rounding during aggregation.
  For zero-fraction currencies (JPY, KRW, CLP) this clamped every
  per-session cost to a whole unit before sum, so a project of
  1000 sessions averaging ¥0.4 each aggregated to ¥0 instead of
  ¥400. formatCost still rounds at the display boundary.

- src/config.ts saveConfig: the temp file path was a fixed
  `${configPath}.tmp` suffix. Two simultaneous saveConfig calls
  (overlapping menubar and CLI runs) raced on the same staging
  file and could leave one writer reading partial bytes from the
  other. Randomized the temp suffix per call.

- src/providers/antigravity.ts flushCache: the early return on
  `!cacheDirty` short-circuited eviction when liveCascadeIds was
  supplied but no cascade had been added or updated this run. As
  a result, deleted .pb files persisted in the cache forever once
  the user stopped writing to it. Eviction now runs whenever
  liveCascadeIds is provided, marks the cache dirty if anything
  was removed, and only then short-circuits if there is nothing
  to write.

- src/daily-cache.ts addNewDays: cap retention at 2 years. The
  days array previously merged forever, growing the cache file by
  hundreds of bytes per day until JSON parse on every CLI
  invocation became measurable. The 6-month UI period plus the
  365-day BACKFILL_DAYS bootstrap both fit comfortably inside the
  cap, with headroom for a future longer window.

- src/dashboard.tsx useInput: period number keys (1-5) and arrow
  keys triggered a reload while the compare view was mounted. The
  parent's data state changed underneath the user with no visual
  affordance back to the dashboard. Now those keys are gated on
  view !== 'compare', and `b` / Esc inside compare returns to the
  dashboard.

- mac/.../HeatmapSection.swift formatters: prettyDate, buildTrend
  Bars, computeTrendStats, computeForecast, and computeAllStats
  each allocated a fresh DateFormatter (and Calendar) on every
  call. SwiftUI re-evaluates these views many times per second
  during hover scrubbing on the trend chart, so the allocations
  were a measurable hot spot. Lifted the yyyy-MM-dd / "EEE MMM d"
  / "MMM d" formatters and the gregorian Calendar to fileprivate
  cached singletons.

Two findings from the same bucket were not addressed here:
- UpdateChecker SHA-256 / codesign verification is already
  performed by src/menubar-installer.ts (verifyChecksum at line
  85). The Swift side just kicks off `codeburn menubar --force`
  which runs that path. The audit's claim of missing verification
  was a misread.
- NSDistributedNotificationCenter sender validation: the
  `com.codeburn.refresh` listener accepts from any sender, but
  forceRefresh has a 5-second rate-limit gate so the abuse
  ceiling is one CLI spawn per 5 seconds. Mitigations (Mach IPC,
  per-launch shared secret) are disproportionate to the impact.

vitest run: 38 files, 529 tests pass.
swift build -c release: clean, no warnings.

* Validator hardenings on the bug-hunt batch

Hoist the per-call sort in getModelCosts and getShortModelName to module
scope so model lookups on the hot path stop reallocating sorted key arrays.

Sanitize the unknown-model stderr warning by stripping C0/C1 controls
and capping length, so a hostile or corrupt JSONL cannot inject terminal
escape sequences via the model field.

Skip the daily-cache prune when newestDate fails to parse. The previous
code produced a NaN cutoff and silently dropped every cached day on the
next merge.

Adds tests locking down the stable resolution of common model names
(gpt-5-mini vs gpt-5, claude-haiku-4-5 vs claude-3-5-haiku, etc.) and
the prune NaN guard.
2026-05-06 19:50:40 -07:00
iamtoruk
6fb05c6813 Fix command injection in yield via execFileSync
Replace execSync with execFileSync and argument arrays so shell
metacharacters in git branch names cannot be interpreted as commands.
Add SAFE_REF_PATTERN validation as defense in depth for branch names
from git symbolic-ref.

Addresses #214.
2026-05-04 10:13:40 -07:00
AgentSeal
6eb9ba0a3b chore: clean up yield.ts - remove redundant comments and dead code 2026-04-25 14:04:16 +02:00
iamtoruk
1c5ca45bd8 Add experimental yield command to track productive vs wasted spend
New command: codeburn yield --period <period>

Correlates AI sessions with git commits to categorize spend:
- Productive: sessions with nearby commits that made it to main
- Reverted: sessions with commits that were reverted
- Abandoned: sessions with no commits or not in main

Uses timestamp proximity heuristic (session time + 1 hour window).
Works across branches, squash merges, and rebases by checking
if commits are in main branch ancestry.

Closes #152
2026-04-25 13:47:07 +02:00