Who this is for: any agent (or human) picking up this work cold. Every item is broken into TDD units. A unit is not done until its cycle is: 🔴 red (failing test committed to working tree) → 🟢 green (minimal code passes) → 📦 commit (one atomic commit per unit,
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>). Never batch units into one commit. Never write code before its failing test.Ground rules (from CLAUDE.md + project memory):
node --testmust be green before every commit (full suite, <5 s, zero npm deps).- Deprecated code moves to
ARCHIVE/, never@deprecatedmarkers.experience/must NOT import fromhooks/— verified boundary, keep it.- Update this file's checkboxes as part of each unit's commit.
Plan origin: approved 2026-07-19 (plan file elegant-splashing-token.md).
Baseline at start: master fast-forwarded to 44a22cc, 1500 tests green.
- Fast-forward
mastertokaaro/feat/session-clusterstip (44a22cc), push -
.github/workflows/test.yml—node --test, matrix Node 18/20/22, push+PR (63a0980) - Verify: Action run 29698488417 → success on all three Node versions
Notes: remote moved to https://github.com/karx/kaaroSessions.git (old URL redirects).
node:sqlite (copilot index) degrades gracefully pre-22.5 — test self-skips, matrix safe.
- 🔴
test/adapters/pi.test.mjs:thinking_level_change→mode_shiftNR (mode: "thinking:<level>") - 🟢
hooks/adapters/pi.mjs: handlethinking_level_change - Docs: TODO.md #1 rewritten as resolved/data-absent; CLAUDE.md coverage gap + docs/harnesses.md Pi row updated
- 📦 One commit, suite 1501 green
Grounding (2026-07-19 survey of all local Pi transcripts): raw format has ONLY
session / model_change / thinking_level_change / message record types.
context_resets, ai_title, subagent_count, branches do not exist in Pi data —
registry false capability flags are honest. Do not reopen without new Pi format evidence.
Design (locked during exploration):
hooks/enrich-session.mjsis the ONLY place token arithmetic lives:- export
tokensWork(t)=(t?.output || 0) + (t?.cache_create || 0) enrichSession(sess)additionally setssess.tokens_work = tokensWork(sess.tokens)- new export
enrichProject(proj)setsproj.tokens_work+proj.tokens_total
- export
surface/analyze-orchestrator.mjsbuildSessionsOutput()callsenrichProjecton every merged project (single choke point — per-harness analyzers already callenrichSession).experience/graph-pipeline.mjsbecomes strict passthrough: session nodes usesess.tokens_work || 0; project nodes useproj.tokens_work/proj.tokens_total; timeline usess.tokens_work || s.tool_calls || 0;MAX_WORKfroms.tokens_work. Theexperience/ → hooks/import boundary stays clean (no imports — passthrough only). Test fixtures intest/graph-pipeline.test.mjsgaintokens_work(they model post-enrich data). sizeNorm tool_calls fallback for tokenless harnesses is sizing policy, not arithmetic — it stays in the pipeline.hooks/sessions-schema.mjs: documenttokens_workinOPTIONAL_SESSION_FIELDS(derived by enrich, consumed as passthrough).
Units:
- 🔴→🟢→📦 Unit 3a:
test/enrich-session.test.mjs—tokensWorkhelper +enrichSessionsetstokens_work+enrichProjectsets project fields; implement inhooks/enrich-session.mjs; wireenrichProjectintosurface/analyze-orchestrator.mjs(+ orchestrator test asserts projects carrytokens_work) — suite 1505 - 🔴→🟢→📦 Unit 3b:
test/graph-pipeline.test.mjs— passthrough proof (fixturetokens_work: 999≠ formula → node carries 999) + missing passthrough coverage fortools_top,context_resets,ai_title,subagent_count,branches(closes CLAUDE.md gap); make pipeline strict-passthrough; update fixtures to enriched shape — suite 1513 - 📦 Unit 3c: schema doc for
tokens_work+ CLAUDE.md "Known coverage gaps" update (rode with 3b commit) - Verify:
node analyze.mjs && node build.mjs— 30 real sessions, nodes carry enriched tokens_work (spot-checked graph-data.json)
Signals only — auditor, never gatekeeper. Predicates needing W-OBS-02 attribution
(tools.contains) are OUT of scope; evaluator skips unknown predicates with an
INFO diagnostic (visible, not silent).
- 🔴→🟢→📦 Unit 4a:
hooks/policy.mjsloader/merger (aa4c078) + BOM-tolerance follow-up unit (PowerShell writes UTF-8 BOM; loader strips it) - 🔴→🟢→📦 Unit 4b:
hooks/signal-evaluator.mjs— 7 predicates, first match wins, diagnostic signals,buildSignalsDataW-REP-02 payload (0491e85) - 🔴→🟢→📦 Unit 4c:
analyze.mjswriteOutput →signals-data.jsonevery run (empty payload included), gitignored (fb03dc5) - 🔴→🟢→📦 Unit 4d:
GET /api/signalsroute + serve.mjs paths.signals; tests on ephemeral ports (present-file + absent-file cases) - Verify end-to-end: sample
.agents/policy.json→ analyze emitted 31 signals (1 rule match + 30 visible diagnostics for the deliberately-unsupportedtools.contains); liveserve --port=3399→/api/signalsreturned the payload - W-REP-01 graph overlay: explicitly OUT of scope (next batch)
Refinement noted for next batch: diagnostics are per-session (30× for one bad rule) —
consider deduping diagnostics per rule in buildSignalsData.
- 🔴→🟢
orderClientModules()inbuild.mjs: filenames must beNN-name.js(two-digit prefix) or the build throws; tested intest/build.test.mjsincluding a sweep over the realexperience/client/dir - Split comment at the definition site (
hooks/helpers/analyze-helpers.mjs) + schema field contract (hooks/sessions-schema.mjs) forskills[]vsbuiltin_commands[] - TODO.md refresh: header count 1436 → 1544+, CI + policy noted, items #2/#4/#6 struck as resolved with pointers
- Verify: full suite green + real
node build.mjsexits 0 with the assertion live - 📦 Single commit for the batch
User choices: the two highest-value policy items (attribution + severity-ring overlay),
then start the OTLP emission path (CONTEXT.md 2026-06 locked decision,
RFC-observability-surface.md — fully spec'd, zero code as of batch start).
Grounding (verified 2026-07-20):
skill_invokeNRs exist withts(claude-code.mjs:127) but only from<command-name>scanning; the CC adapter emits genericattachmentNRs without extractinginvoked_skillsskill names (W-OBS-01's reliable source).- The
tools.containspredicate currently yields per-session INFO diagnostics (verified live: 30 for one rule) — attribution makes it evaluate for real. build.mjsside-file pattern to copy:loadClusterOverrides. Node annotation is centralized inexperience/graph-pipeline.mjs.- RFC locks: pure-Node OTLP/HTTP JSON (no protobuf, no deps, global fetch),
Stream first (tool_call→Span, tokens→Metric, words→LogRecord), Snapshot as
normalized LogRecords after rebuild, one resource per deployment
(
kaaro.deployment.harnesses),harness+session.id+kaaro.snapshot.generationon every signal.
- 🔴→🟢→📦 Unit 6a: CC adapter —
attachment.type === 'invoked_skills'→ oneskill_invokeNR per skill (ts kept);<command-name>scan remains as fallback source; test intest/adapters/claude-code.test.mjs - 🔴→🟢→📦 Unit 6b: reducer —
skill_timeline: [{skill, ts}](chronological) +skill_attribution: {<skill>: {tool_calls, tools{}, errors}}; window opens at eachskill_invoke, closes at nextskill_invoke— and does NOT survive acontext_reset(test this); BUILTIN_COMMANDS excluded;{}/[]defaults; add both toOPTIONAL_SESSION_FIELDS+ schema doc block - 🔴→🟢→📦 Unit 6c: evaluator —
tools.containspredicate (rule form{ skill: X, 'tools.contains': Y }→ attribution[X].tools[Y]); keep INFO diagnostic when attribution data absent; rider: dedupe diagnostics per rule (not per session) inbuildSignalsData - Verify: sample policy with
tools.containsrule → real evaluations, no per-session diagnostic spam
- 🔴→🟢→📦 Unit 7a:
buildGraph(data, { signals })annotates session + project nodes{ signals, max_signal, signal_ids[] }; absent → zero defaults; unknown session_ids ignored; tests intest/graph-pipeline.test.mjs - 🔴→🟢→📦 Unit 7b:
loadSignalsData()inbuild.mjs(mirrorloadClusterOverrides: absent/malformed → null, build never fails) + pass tobuildGraph; test intest/build.test.mjs - 📦 Unit 7c (browser, no Node tests): severity ring in
experience/client/04-rendering.js(INFO dim → ALERT bright; load thekaaro-designskill before any CSS/colors; design-lint forbids blue chrome); counts + rule ids in tooltip/panel (05-interaction.js); CSS inexperience/pages/template.html - Verify: sample policy → analyze → build →
/graphshows rings, panel lists rules; remove sample policy afterward
- 🔴→🟢→📦 new
surface/otlp-encoder.mjs+test/otlp-encoder.test.mjs:buildResource(harnesses, opts);pulseToOtlp(pulse, ctx)→{ kind: span|metric|log, payload }(tool_call→Spantool.{name}, tokens→Sumkaaro.tokens.*, words→LogRecord, others→LogRecord withevent.type);snapshotToLog(sessionsData, generation); common attrs on every signal; OTLP/HTTP JSON envelopes (resourceSpans/resourceMetrics/resourceLogs). Pure — no I/O, no Date.now (caller passes generation/ts)
- 🔴→🟢→📦 new
surface/otlp-emitter.mjs+test/otlp-emitter.test.mjs:createOtlpEmitter({ endpoint, headers, fetchFn })— batches pulses (coalesce like pulse-emitter), POSTs to<endpoint>/v1/{traces,metrics,logs}via injected fetchFn; network errors warn + swallow (never breaks serve); no-op when endpoint absent;flush() - 📦 Wire in
serve.mjs:--otlp-endpoint=/OTLP_ENDPOINTenv (+OTLP_HEADERSJSON env); emit beside SSE notify. Off by default — local mirror unchanged
- 🔴→🟢→📦 Unit 10a:
emitSnapshot(sessionsData)— oneresourceLogsPOST, full normalized body, monotonickaaro.snapshot.generation; subsequent stream signals carry the same generation (correlation contract) - 📦 Unit 10b: hook into rebuild-success path (serve.mjs /
surface/rebuild-orchestrator.mjscallback) when OTLP configured - 📦 Unit 10c:
docs/OTLP.md(config, example Collector config, signal reference table); update CLAUDE.md + CONTEXT.md status (OTLP Emission: implemented for Stream + Snapshot) - Verify end-to-end: throwaway local HTTP sink (scratchpad script printing
request paths/bodies) +
node serve.mjs --otlp-endpoint=http://127.0.0.1:4318 --no-open→ touch a session file → span/metric/log POSTs arrive + one snapshot log after rebuild
Sequencing: 6→10; items 6–7 independent of 8–10; 9 depends on 8; 10 on 9.
Risk watch: attribution across context_reset (decided: window dies at reset —
test it); snapshot log payload size (30+ sessions ≈ MBs — acceptable per RFC "full
shape preferred"; gzip noted as future work).
| When (UTC) | Event |
|---|---|
| 2026-07-19 | Plan approved; Item 1 done (CI green run 29698488417); Item 2 done afe9867, suite 1501 |
| 2026-07-19 | Item 3 design locked (strict passthrough + enrichProject in orchestrator); units 3a–3c pending |
| 2026-07-19 | Item 3 done (96cfc1a, 6f397e6); Item 4 done (aa4c078, 0491e85, fb03dc5, e381eba) incl. BOM fix + live /api/signals verification |
| 2026-07-20 | Item 5 done; suite 1549 green; all 5 items complete — next batch candidates: W-REP-01 graph signal overlay, diagnostic dedupe, OTLP emission (deferred by choice), W-COG-07 content-aware pitching |
| 2026-07-20 | Batch 2 planned (items 6–10): user chose half policy (attribution W-OBS-01/02 + severity-ring overlay W-REP-01) / half OTLP (encoder, stream emitter, snapshot emission per RFC). Not started. |
| 2026-07-20 | Unit 6a done: CC adapter extracts invoked_skills → skill_invoke NRs (attachment NR kept; command-name fallback retained); suite 1552 |
| 2026-07-20 | Unit 6b done: reducer skill_timeline + skill_attribution (window dies at context_reset; BUILTIN excluded); suite 1558 |
| 2026-07-20 | Unit 6c done: tools.contains predicate + diagnostic dedupe per rule in buildSignalsData; suite 1565 |