← back to Desktop Dotbar
Jev classifies dotbar dot colors ($0 builtin default; paid TypeSafe flip gated off)
cf77e2f8b3f7970d0f37395c7028c9a6c181386f · 2026-09-23 11:56:07 -0700 · Steve Abrams
Re-decide each active terminal's displayed dot color through a Jev-style typed
choice at the dotbar's own getDots() seam. allcolordots.sh is untouched (no fleet
regression); Jev unavailable/capped/errors falls back to the heuristic color.
$0 builtin classifier by default; paid path unreachable unless DOTBAR_JEV_PAID=1
and provably under a $5/day cap (fail-closed). Per-terminal state-hash cache gates
reclassification to real transitions only. /api/jev exposes the counters.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019excZ7L7VE14hbPqKQqH3i
Files touched
A jev-dots.jsM package.jsonM server.jsA test/jev-dots.test.js
Diff
commit cf77e2f8b3f7970d0f37395c7028c9a6c181386f
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Wed Sep 23 11:56:07 2026 -0700
Jev classifies dotbar dot colors ($0 builtin default; paid TypeSafe flip gated off)
Re-decide each active terminal's displayed dot color through a Jev-style typed
choice at the dotbar's own getDots() seam. allcolordots.sh is untouched (no fleet
regression); Jev unavailable/capped/errors falls back to the heuristic color.
$0 builtin classifier by default; paid path unreachable unless DOTBAR_JEV_PAID=1
and provably under a $5/day cap (fail-closed). Per-terminal state-hash cache gates
reclassification to real transitions only. /api/jev exposes the counters.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019excZ7L7VE14hbPqKQqH3i
---
jev-dots.js | 282 ++++++++++++++++++++++++++++++++++++++++++++++++++
package.json | 3 +-
server.js | 11 +-
test/jev-dots.test.js | 117 +++++++++++++++++++++
4 files changed, 411 insertions(+), 2 deletions(-)
diff --git a/jev-dots.js b/jev-dots.js
new file mode 100644
index 0000000..0c67a36
--- /dev/null
+++ b/jev-dots.js
@@ -0,0 +1,282 @@
+'use strict';
+// jev-dots — re-classify each terminal's DOT COLOR through Jev (TypeSafe System One:
+// unstructured per-terminal state in, a typed choice over the dot-color labels out).
+//
+// Scoped to the DOTBAR's OWN read path only: server.js getDots() already runs
+// `allcolordots --json` and gets per-terminal {tty,pid,label,color,variant,parked}.
+// This module re-decides the displayed COLOR at that seam, so the shared fleet
+// allcolordots.sh is never touched. If Jev is unavailable/capped/errors, each row
+// falls back to allcolordots' original heuristic color — never blank, never a throw.
+//
+// $0 BY DEFAULT: the built-in deterministic classifier runs locally with no network
+// and no spend. The PAID TypeSafe backend is gated OFF behind DOTBAR_JEV_PAID=1 AND a
+// hard daily spend cap; flipping it on is a separate Steve-approved step.
+//
+// Load-bearing cost control = the per-terminal STATE-HASH CACHE: a terminal is only
+// (re)classified when its state hash changes, so "every terminal every 2.5s refresh"
+// collapses to a few classifications/day (real transitions only) with no loss of
+// "jev decides every color."
+
+const crypto = require('crypto');
+const fs = require('fs');
+const os = require('os');
+const path = require('path');
+const https = require('https');
+const { execFile } = require('child_process');
+
+// ---- config constants (trivially changeable) --------------------------------
+const LABELS = ['lightblue', 'orange', 'purple', 'yellow', 'green', 'pink', 'none'];
+// Semantic priority (needs-Steve first) — matches allcolordots / server.js ORDER.
+// A higher-priority signal in the label text outweighs a lower-priority one.
+const PRIORITY = { lightblue: 6, orange: 5, purple: 4, yellow: 3, green: 2, pink: 1, none: 0 };
+
+const CONFIG = {
+ // PAID PATH DEFAULT-OFF. Only DOTBAR_JEV_PAID=1 arms it; anything else = $0 builtin.
+ paidEnabled: process.env.DOTBAR_JEV_PAID === '1',
+ // Hard daily spend cap (USD). Steve may set DOTBAR_JEV_CAP_USD=2; default $5.
+ capUsd: Number(process.env.DOTBAR_JEV_CAP_USD || '5'),
+ provider: process.env.DOTBAR_JEV_PROVIDER || 'typesafe',
+ timeoutMs: Number(process.env.DOTBAR_JEV_TIMEOUT_MS || '800'),
+ ledgerPath: path.join(os.homedir(), '.claude', 'cost-ledger.jsonl'),
+ costLogJs: path.join(os.homedir(), '.claude', 'skills', 'cost-tracker', 'scripts', 'log.js'),
+ costApiKey: 'typesafe_jev',
+ costApp: 'desktop-dotbar',
+};
+
+// ---- observability counters (verification reads these) ----------------------
+const stats = {
+ classifications: 0, // rows that actually ran the classifier (cache miss)
+ cacheHits: 0, // rows served from the state-hash cache (no reclassify)
+ cacheMisses: 0,
+ builtinCalls: 0, // classifier decided via the $0 local path
+ paidCalls: 0, // classifier decided via the paid TypeSafe API
+ paidFellBack: 0, // paid attempted but errored -> builtin
+ capBlocked: 0, // paid armed but daily cap reached -> builtin
+ paidEnabled: CONFIG.paidEnabled,
+ capUsd: CONFIG.capUsd,
+ lastSpendUsd: 0,
+ updated: 0,
+};
+
+// tty -> { hash, color, source, confidence }
+const cache = new Map();
+
+// ---- state extraction: what we feed Jev per terminal ------------------------
+function cleanLabel(label) {
+ if (!label) return '';
+ return String(label).replace(/^[\u{1F300}-\u{1FAFF}\u{2600}-\u{27BF}️\s]+/u, '').trim();
+}
+// The meaningful per-terminal state (comparable to what the heuristic sees, plus
+// the label text the heuristic ignores): the heuristic's own color as a prior,
+// the cleaned dot title, and whether the session is stopped.
+function extractState(row) {
+ return {
+ heuristicColor: LABELS.includes(row.color) ? row.color : 'none',
+ label: cleanLabel(row.label),
+ variant: row.variant || '',
+ stopped: row.variant === 'stopped',
+ };
+}
+function stateHash(s) {
+ return crypto.createHash('sha1')
+ .update(`${s.heuristicColor}\u0000${s.label}\u0000${s.variant}`)
+ .digest('hex');
+}
+
+// ---- $0 built-in classifier -------------------------------------------------
+// Deterministic typed choice over the dot-color labels. Designed as a strict
+// REFINEMENT of the heuristic: the heuristic's own color is a strong prior, and
+// label-text keywords can only pull the decision toward a DIFFERENT, correctly
+// prioritized label when the text genuinely says so. With no keyword signal it
+// keeps the heuristic color, so it is at least as good as the heuristic by
+// construction and cannot randomly regress.
+const KEYWORDS = [
+ // color, weight, regex (tested against lowercased label)
+ ['lightblue', 4.0, /needs?\s+steve|waiting on steve|blocked on steve|need (?:you|steve)|your input|awaiting steve/],
+ ['orange', 3.6, /\bpaste|!\s*(?:ssh|bash|sudo|psql|open)|run this|in your console|console step|\bpastes?\b/],
+ ['purple', 3.6, /gated|pending[- ]approval|awaiting approval|memo drafted|approve\/reject|approval queue/],
+ ['yellow', 3.4, /needs? direction|clarify|which approach|decision fork|askuser|1 question|\bquestions?\b/],
+ ['pink', 3.0, /\bparked\b|nothing left|handed off|\bdone\b|complete(?:d)?\b|finished\b/],
+ ['green', 3.0, /working|monitoring|running|building|in progress|started \d|next \d|deploy|scanning|sweeping/],
+];
+function classifyBuiltin(state) {
+ const scores = Object.fromEntries(LABELS.map((c) => [c, 0]));
+ // Prior: trust the heuristic color. 'none' is a weak prior (the session painted
+ // nothing), so label text is allowed to name a real color over it.
+ scores[state.heuristicColor] += state.heuristicColor === 'none' ? 0.6 : 3.0;
+ const text = state.label.toLowerCase();
+ for (const [color, weight, re] of KEYWORDS) {
+ if (re.test(text)) scores[color] += weight;
+ }
+ // A stopped session with no needs-Steve / working signal reads as parked, not working.
+ if (state.stopped) scores.pink += 1.0;
+ // Pick the argmax; break ties toward the higher semantic priority (needs-Steve first).
+ let best = 'none', bestScore = -1;
+ for (const c of LABELS) {
+ const sc = scores[c];
+ if (sc > bestScore || (sc === bestScore && PRIORITY[c] > PRIORITY[best])) {
+ best = c; bestScore = sc;
+ }
+ }
+ const sum = LABELS.reduce((a, c) => a + scores[c], 0) || 1;
+ const distribution = Object.fromEntries(LABELS.map((c) => [c, +(scores[c] / sum).toFixed(3)]));
+ return { color: best, confidence: +(bestScore / sum).toFixed(3), source: 'builtin', distribution };
+}
+
+// ---- daily spend cap (fail-closed) ------------------------------------------
+// Sum today's desktop-dotbar rows in the cost ledger. On ANY read failure this
+// returns Infinity so the cap is treated as reached -> paid path is skipped -> no spend.
+function spendTodayUsd(readLedger) {
+ try {
+ const raw = readLedger();
+ if (!raw) return 0;
+ const today = new Date().toISOString().slice(0, 10);
+ let sum = 0;
+ for (const line of raw.split('\n')) {
+ if (!line.trim()) continue;
+ let e; try { e = JSON.parse(line); } catch { continue; }
+ if (e && e.app === CONFIG.costApp && typeof e.cost_usd === 'number'
+ && String(e.ts || '').slice(0, 10) === today) {
+ sum += e.cost_usd;
+ }
+ }
+ return sum;
+ } catch {
+ return Infinity; // cannot confirm under cap -> never spend
+ }
+}
+
+function logPaidCall(logSpend) {
+ // Log every paid call to the cost ledger via cost-tracker's log.js. Best-effort:
+ // a logging failure must never crash a refresh (but is counted).
+ try { logSpend(); } catch { /* counted by caller */ }
+}
+function defaultLogSpend() {
+ execFile(process.execPath, [CONFIG.costLogJs, '--api', CONFIG.costApiKey,
+ '--units', '1:call', '--app', CONFIG.costApp, '--note', 'dotbar color classification'],
+ { timeout: 4000 }, () => {});
+}
+
+// ---- paid TypeSafe backend (UNREACHABLE unless DOTBAR_JEV_PAID=1) ------------
+// Reuses jev-model-router's typed-choice wire shape (choice question + per-answer
+// confidence), adding only the new state->dot-color schema. Reads the key lazily
+// and ONLY when armed, so with the flag off nothing is ever read or sent.
+function readTypesafeKey() {
+ if (process.env.TYPESAFE_API_KEY) return process.env.TYPESAFE_API_KEY;
+ try {
+ const env = fs.readFileSync(path.join(os.homedir(), 'Projects', 'secrets-manager', '.env'), 'utf8');
+ const m = /^TYPESAFE_API_KEY=(.+)$/m.exec(env);
+ return m ? m[1].trim() : '';
+ } catch { return ''; }
+}
+const DOT_CRITERIA = {
+ lightblue: 'The session has stopped and needs Steve: any stop that requires his input.',
+ orange: 'A paste is waiting: a shell command / console step Steve must run himself.',
+ purple: 'Gated: a memo is drafted to pending-approval awaiting Steve\'s approve/reject.',
+ yellow: 'Needs direction: a question or decision fork is waiting on Steve\'s answer.',
+ green: 'Working: a process, loop, or agent is actively executing or monitoring.',
+ pink: 'Parked: work handed off or complete, nothing left to progress.',
+ none: 'No dot / no discernible state.',
+};
+function typeSafeClassify(state, deps) {
+ const key = (deps && deps.key) || readTypesafeKey();
+ if (!key) return Promise.reject(new Error('no TYPESAFE_API_KEY'));
+ const body = JSON.stringify({
+ model: 'jev-latest',
+ state: { heuristicColor: state.heuristicColor, label: state.label, stopped: state.stopped },
+ questions: {
+ dot: {
+ type: 'choice',
+ instructions: 'Which dot color best classifies this terminal\'s current state?',
+ criteria: DOT_CRITERIA,
+ },
+ },
+ });
+ return new Promise((resolve, reject) => {
+ const req = https.request('https://api.typesafe.ai/v1/systemone', {
+ method: 'POST',
+ headers: { 'content-type': 'application/json', authorization: `Bearer ${key}`,
+ 'content-length': Buffer.byteLength(body) },
+ timeout: CONFIG.timeoutMs,
+ }, (res) => {
+ let out = '';
+ res.on('data', (c) => (out += c));
+ res.on('end', () => {
+ if (res.statusCode < 200 || res.statusCode >= 300) return reject(new Error(`http ${res.statusCode}`));
+ try {
+ const ans = JSON.parse(out).answers && JSON.parse(out).answers.dot;
+ if (ans && LABELS.includes(ans.choice)) {
+ resolve({ color: ans.choice, confidence: typeof ans.confidence === 'number' ? ans.confidence : null, source: 'typesafe' });
+ } else reject(new Error('bad answer shape'));
+ } catch (e) { reject(e); }
+ });
+ });
+ req.on('error', reject);
+ req.on('timeout', () => { req.destroy(new Error('timeout')); });
+ req.end(body);
+ });
+}
+
+// ---- the seam server.js calls ----------------------------------------------
+// rows: allcolordots --json rows (already filtered to active/non-parked by getDots).
+// opts: dependency-injection seam for tests (paidEnabled, capUsd, readLedger,
+// paidTransport, logSpend). Production uses the real env/files.
+// Returns Map<tty, {color, source, confidence}>. Never throws.
+async function classifyDots(rows, opts = {}) {
+ const paidEnabled = opts.paidEnabled !== undefined ? opts.paidEnabled : CONFIG.paidEnabled;
+ const capUsd = opts.capUsd !== undefined ? opts.capUsd : CONFIG.capUsd;
+ const readLedger = opts.readLedger || (() => { try { return fs.readFileSync(CONFIG.ledgerPath, 'utf8'); } catch { return ''; } });
+ const paidTransport = opts.paidTransport || typeSafeClassify;
+ const logSpend = opts.logSpend || defaultLogSpend;
+
+ const seen = new Set();
+ const misses = [];
+ for (const row of rows || []) {
+ if (!row || !row.tty) continue;
+ seen.add(row.tty);
+ const state = extractState(row);
+ const hash = stateHash(state);
+ const cached = cache.get(row.tty);
+ if (cached && cached.hash === hash) { stats.cacheHits++; continue; }
+ stats.cacheMisses++;
+ misses.push({ tty: row.tty, state, hash });
+ }
+
+ // Compute today's spend ONCE per refresh so a burst of misses can't each blow past the cap.
+ let spend = 0;
+ if (paidEnabled) { spend = spendTodayUsd(readLedger); stats.lastSpendUsd = Number.isFinite(spend) ? spend : stats.lastSpendUsd; }
+
+ for (const m of misses) {
+ stats.classifications++;
+ let result = null;
+ // Paid path is structurally UNREACHABLE unless armed AND provably under the cap.
+ if (paidEnabled && spend < capUsd) {
+ try {
+ result = await paidTransport(m.state, {});
+ stats.paidCalls++;
+ logPaidCall(logSpend);
+ spend += 0.001; // reserve the just-spent call so the in-loop cap holds before the ledger flushes
+ } catch {
+ stats.paidFellBack++;
+ result = classifyBuiltin(m.state); stats.builtinCalls++;
+ }
+ } else {
+ if (paidEnabled) stats.capBlocked++; // armed but cap reached -> fall back, no spend
+ result = classifyBuiltin(m.state); stats.builtinCalls++;
+ }
+ cache.set(m.tty, { hash: m.hash, color: result.color, source: result.source, confidence: result.confidence });
+ }
+
+ // Prune cache entries for ttys that vanished (keep it bounded).
+ for (const tty of cache.keys()) if (!seen.has(tty)) cache.delete(tty);
+
+ stats.updated = Date.now();
+ const out = new Map();
+ for (const tty of seen) { const c = cache.get(tty); if (c) out.set(tty, { color: c.color, source: c.source, confidence: c.confidence }); }
+ return out;
+}
+
+function getStats() { return { ...stats, cacheSize: cache.size }; }
+function _resetForTest() { cache.clear(); for (const k of Object.keys(stats)) if (typeof stats[k] === 'number') stats[k] = 0; }
+
+module.exports = { classifyDots, classifyBuiltin, extractState, stateHash, spendTodayUsd, getStats, LABELS, _resetForTest };
diff --git a/package.json b/package.json
index e4273c9..d4dfd64 100644
--- a/package.json
+++ b/package.json
@@ -4,7 +4,8 @@
"private": true,
"main": "electron-main.js",
"scripts": {
- "start": "electron ."
+ "start": "electron .",
+ "test": "node --test test/*.test.js"
},
"dependencies": {
"electron": "^44.4.0"
diff --git a/server.js b/server.js
index 7faf854..a7f15af 100755
--- a/server.js
+++ b/server.js
@@ -8,6 +8,7 @@ const http = require('http');
const { execFile, spawn } = require('child_process');
const fs = require('fs');
const path = require('path');
+const jevDots = require('./jev-dots'); // re-classifies each dot COLOR via Jev ($0 builtin; paid flip gated)
const ALLCOLORDOTS = `${process.env.HOME}/.claude/skills/allcolordots/allcolordots.sh`;
const ROUTER = `${process.env.HOME}/.claude/skills/dot-screen-router/router.sh`;
@@ -59,10 +60,17 @@ async function getDots() {
// A durably-parked live tab peels OUT of the active colour groups into the PARKED
// section, so the active dots stay uncluttered (allcolordots --json carries `parked`).
rows = rows.filter(r => r && r.live && !r.parked);
+ // Re-decide each ACTIVE dot's COLOR through Jev (System One typed choice). Scoped to
+ // the dotbar's own read path — allcolordots.sh is untouched. Jev unavailable/capped/
+ // errors -> we keep allcolordots' original heuristic color (never blank, never throw).
+ let jevMap = new Map();
+ try { jevMap = await jevDots.classifyDots(rows); } catch { jevMap = new Map(); }
const groups = {};
for (const key of ORDER) groups[key] = [];
for (const r of rows) {
- const color = META[r.color] ? r.color : 'none';
+ const jv = jevMap.get(r.tty);
+ const jevColor = jv && META[jv.color] ? jv.color : null;
+ const color = jevColor || (META[r.color] ? r.color : 'none');
groups[color].push({
tty: r.tty,
pid: r.pid,
@@ -265,6 +273,7 @@ const server = http.createServer(async (req, res) => {
const url = new URL(req.url, 'http://x');
if (url.pathname === '/health') return send(res, 200, { ok: true, port: server.address() && server.address().port });
if (url.pathname === '/api/dots') { if (!snapshot.updated) await refresh(); refresh(); return send(res, 200, snapshot); }
+ if (url.pathname === '/api/jev') { return send(res, 200, jevDots.getStats()); }
if (url.pathname === '/api/arrange' && req.method === 'POST') {
return send(res, 200, await arrange());
}
diff --git a/test/jev-dots.test.js b/test/jev-dots.test.js
new file mode 100644
index 0000000..5a26b62
--- /dev/null
+++ b/test/jev-dots.test.js
@@ -0,0 +1,117 @@
+'use strict';
+// Verification for jev-dots: classification (builtin $0), cache-gate, spend cap
+// (fail-closed), fallback, and a negative fault-injection test. Zero deps.
+const test = require('node:test');
+const assert = require('node:assert');
+const jev = require('../jev-dots');
+
+const row = (tty, color, label, variant) => ({ tty, color, label, variant: variant || '', live: true, parked: false });
+const todayLine = (usd) => JSON.stringify({ app: 'desktop-dotbar', cost_usd: usd, ts: new Date().toISOString() });
+
+test('builtin classifies synthetic states to sane colors', () => {
+ const cases = [
+ [row('t1', 'green', 'TK-1 · 2 pastes waiting'), 'orange'], // label overrides heuristic
+ [row('t2', 'none', 'parked — handed off', 'stopped'), 'pink'],
+ [row('t3', 'green', 'monitoring · next 14:00'), 'green'],
+ [row('t4', 'purple', 'gated · 1 memo in pending-approval'), 'purple'],
+ [row('t5', 'none', 'needs Steve to approve the login'), 'lightblue'],
+ [row('t6', 'yellow', '1 question — which approach'), 'yellow'],
+ [row('t7', 'green', 'building the importer'), 'green'],
+ ];
+ for (const [r, expect] of cases) {
+ const res = jev.classifyBuiltin(jev.extractState(r));
+ assert.equal(res.color, expect, `${r.label} -> ${res.color}, expected ${expect}`);
+ assert.equal(res.source, 'builtin');
+ assert.ok(res.confidence > 0 && res.confidence <= 1);
+ }
+});
+
+test('cache-gate: unchanged state does NOT reclassify on later refreshes', async () => {
+ jev._resetForTest();
+ const rows = [row('a', 'green', 'working'), row('b', 'purple', 'gated · 1'), row('c', 'none', 'parked', 'stopped')];
+ await jev.classifyDots(rows);
+ assert.equal(jev.getStats().classifications, 3, 'first pass classifies all 3');
+ await jev.classifyDots(rows);
+ await jev.classifyDots(rows);
+ const s = jev.getStats();
+ assert.equal(s.classifications, 3, 'no new classifications when state unchanged');
+ assert.equal(s.cacheHits, 6, 'both later passes were pure cache hits');
+});
+
+test('negative/fault-injection: a REAL state change reclassifies (stale hash cannot hide it)', async () => {
+ jev._resetForTest();
+ const before = [row('a', 'green', 'working')];
+ await jev.classifyDots(before);
+ assert.equal(jev.getStats().classifications, 1);
+ const after = [row('a', 'orange', 'TK-9 · 1 paste')]; // same tty, changed state
+ const map = await jev.classifyDots(after);
+ assert.equal(jev.getStats().classifications, 2, 'changed state forces a reclassify');
+ assert.equal(map.get('a').color, 'orange', 'color follows the new state, not the stale cache');
+});
+
+test('paid OFF by default: no paid calls, everything via $0 builtin', async () => {
+ jev._resetForTest();
+ const map = await jev.classifyDots([row('a', 'green', 'working')]); // no opts, env unset
+ const s = jev.getStats();
+ assert.equal(s.paidCalls, 0);
+ assert.equal(s.builtinCalls, 1);
+ assert.equal(map.get('a').source, 'builtin');
+});
+
+test('cap fail-closed: cap reached -> paid transport is NEVER called, falls back to builtin', async () => {
+ jev._resetForTest();
+ let paidAttempts = 0;
+ const spyTransport = async () => { paidAttempts++; return { color: 'green', source: 'typesafe', confidence: 0.9 }; };
+ const map = await jev.classifyDots([row('a', 'purple', 'gated · 1'), row('b', 'green', 'working')], {
+ paidEnabled: true, capUsd: 5, readLedger: () => todayLine(5.0), paidTransport: spyTransport,
+ });
+ const s = jev.getStats();
+ assert.equal(paidAttempts, 0, 'no paid attempt once cap is reached');
+ assert.ok(s.capBlocked >= 2, 'cap-block counted for each miss');
+ assert.equal(s.paidCalls, 0);
+ assert.ok(map.get('a').color, 'still shows a color (builtin fallback)');
+});
+
+test('ledger-read failure is fail-closed (Infinity spend -> no paid)', async () => {
+ jev._resetForTest();
+ let paidAttempts = 0;
+ await jev.classifyDots([row('a', 'green', 'working')], {
+ paidEnabled: true, capUsd: 5, readLedger: () => { throw new Error('ledger unreadable'); },
+ paidTransport: async () => { paidAttempts++; return { color: 'green', source: 'typesafe' }; },
+ });
+ assert.equal(paidAttempts, 0, 'unreadable ledger must not enable spend');
+ assert.ok(jev.getStats().capBlocked >= 1);
+});
+
+test('paid fallback: transport error -> builtin color, never blank, never throws', async () => {
+ jev._resetForTest();
+ const map = await jev.classifyDots([row('a', 'purple', 'gated · 1 memo')], {
+ paidEnabled: true, capUsd: 5, readLedger: () => '', // $0 spent -> under cap -> paid attempted
+ paidTransport: async () => { throw new Error('boom'); },
+ });
+ const s = jev.getStats();
+ assert.equal(s.paidFellBack, 1);
+ assert.equal(map.get('a').color, 'purple', 'fell back to a real builtin color');
+ assert.equal(map.get('a').source, 'builtin');
+});
+
+test('paid used when armed + under cap + transport ok, and logs the spend', async () => {
+ jev._resetForTest();
+ let logged = 0;
+ const map = await jev.classifyDots([row('a', 'yellow', 'needs direction')], {
+ paidEnabled: true, capUsd: 5, readLedger: () => '',
+ paidTransport: async () => ({ color: 'green', source: 'typesafe', confidence: 0.88 }),
+ logSpend: () => { logged++; },
+ });
+ const s = jev.getStats();
+ assert.equal(s.paidCalls, 1);
+ assert.equal(logged, 1, 'every paid call is logged to the cost ledger');
+ assert.equal(map.get('a').color, 'green');
+ assert.equal(map.get('a').source, 'typesafe');
+});
+
+test('classifyDots never throws on junk rows', async () => {
+ jev._resetForTest();
+ const map = await jev.classifyDots([null, {}, row('a', 'green', 'x'), { tty: '', color: 'green' }]);
+ assert.ok(map.get('a'));
+});
← 26f22db dotbar: ticket states as vertical chips with right-edge labe
·
back to Desktop Dotbar
·
dotbar: mark orphaned sessions (live claude, no iTerm2 tab) d56df0f →