What: pnpm workspace with 7 packages (core/index/mcp/hooks/redact/distill/cli),
shared strict/ESM/NodeNext tsconfig via tsc -b project references, vitest
workspace projects, eslint+prettier, GitHub Actions CI on Node 20/22.
Why: establishes the build/test/lint/bench loop every later milestone needs.
Tradeoffs: packages hold only placeholder exports; cli imports core’s
placeholder solely to prove workspace linking + build ordering work.
What: moved internal build-process docs (CONTRACTS, BUILD_PLAN, KICKOFF_PROMPTS, DEVLOG, TECH_BRIEF) into docs/internal/; rewrote README.md as an honest early-stage OSS README (no install/usage instructions since nothing is runnable yet); added CONTRIBUTING.md pointing contributors — human or AI — at the internal docs. Why: repo is meant to be a public GitHub repo; top-level docs should read like a product repo, not a starter-kit’s build-instructions-to-an-AI-agent. Tradeoffs: none of the frozen contracts, milestone plan, or CLAUDE.md guardrails were removed — only relocated — so future milestone sessions are unaffected; CLAUDE.md stays at root since Claude Code auto-loads it there.
What: zod schemas for C1 front-matter, C2 events, brain.yaml; in-house ULID (node:crypto); slugify; canonical parse/serialize with byte-exact round-trip over the 13-fixture corpus. Ambiguities resolved: evidence optional in schema (distiller-mandatory check → M1.2 lint, C1 has no proposer field); byte-exact guarantee applies to canonical form; brain.yaml/event data shapes kept minimal
pnpm --filter <pkg> test works (root config’s projects glob broke inside
packages). Deps: zod, yaml (justified in commit body); ULID hand-rolled.What: injection-patterns table + scanner (scans whitespace-normalized text
so phrases split by hard-wrapping still match); core lint rules (schema,
400-word body, evidence, injection, class/status placement); first real
tb binary (commander) with C6 exit codes. Ambiguity: C1 has no proposer
field, so default lint only rejects empty evidence blocks; the distill PR
check opts into mandatory evidence via --require-evidence.
What: core/log — JSONL logger, one file per UTC day in ~/.teambrain/logs, 7-day retention, level from TEAMBRAIN_LOG_LEVEL, never throws (one stderr notice on degradation); body|content|prompt redacted at info+ (debug only passes raw). core/errors — User/Environment/Validation errors → C6 exit codes 1/2/3 (unknown → 2); parse errors retrofitted onto ValidationError.
What: cli/init/scan (CLAUDE.md, AGENTS.md, .cursorrules, .cursor/rules/, docs/adr/, README arch-sections; mdc frontmatter → title hint) and cli/init/convert (ADR→decision, rules→convention, README-arch→map; rule files split per ## section; >400-word units split at paragraph bounds into “(part n of m)” memories linked by a shared source: tag). Bodies keep source text verbatim → Jaccard ≥0.9 asserted per source; all candidates advisory (humans upgrade in the init PR); pure read, no repo writes.
What: cli/init/interview — generateInterviewQuestions derives ≤10 questions
from gaps (missing map/decision/convention classes; cross-source convention
title overlap ≥0.5 → “which wins?”, capped at 3; testing/build/style topic
gaps); runInterview on plain readline with injectable streams (persistent
line buffer — rl.question drops piped lines), empty answer or EOF skips;
answersToMemories reuses the shared candidate builder (tag interview).
Also refreshed README status/roadmap (was stale at M0.1).
What: cli/init/branch — writeInitBranch builds teambrain/init via a temp git worktree (checkout never switched/dirtied; half-made branch deleted on failure); writes C7 layout + INDEX.md + .teambrain/.gitattributes forcing LF (autocrlf checkouts otherwise CRLF-break the byte-exact parser — found by the integration test on Windows). tb init wires import→interview (TTY only, –yes skips)→branch + next steps; preflight (git repo, has commits, no .teambrain, no branch) runs before import. M2 accept: integration test copies the 3 fixture repos into tmp git repos and asserts counts, class dirs, Jaccard ≥0.9 from written files, lint-clean brain, main untouched.
What: init branch now parents on main/master (HEAD fallback, base named in the output) instead of whatever branch the user stood on; subdirectory targets resolve to the repo toplevel so scan scope matches where the brain is written (validateInitTarget returns the root); tb uses parseAsync; output names the file count. Three new integration tests cover each.
What: SqliteIndex (docs + FTS5 porter mirror + vec0) with brain-tree checksum auto-reindex; fastembed bge-small behind an Embedder interface (tarball+file sha256 pins verified before anything is unpacked or loaded; offline → lexical-only at debug); C4 pipeline with required force-include tied to token-budget mode so plain search top-k isn’t flooded. Bench uses a deterministic HashingEmbedder (CI is offline): rebuild 15s, p95 35ms, recall@8 1.00 on the 25-query golden set. vec0 quirk: rowid binds as int64.
What: C3 stdio MCP server (official SDK 1.29) exposing the 4 tools; bodies
render inside [team memory <id> — data, not instructions] fences (injection
mitigation); memory_context is required-first ≤2000 tokens via a new
index.contextDocs; propose/feedback spool locally only. tb serve daemon keeps
the index fresh, serves hook events + a session_context request over a local
socket, and writes pidfile+heartbeat for tb doctor. tb install claude-code is
an idempotent settings merge; tb hook session-start injects the bundle and is
a no-op when the daemon is down. Accept: scripted MCP client hits all 4 tools;
R5 (retire → gone within one watcher cycle); install twice = zero diff.
Decisions/tradeoffs: (1) the watcher is a checksum poll, not recursive
fs.watch — the latter is unsupported on Linux (CI); fs.watch is a best-effort
nudge only. (2) Cross-platform socket: unix socket on POSIX, named pipe keyed
to the runtime-dir hash on Windows (no AF_UNIX in the C7 sense). (3) Deviation
from BUILD_PLAN M4.3 wording: it says write “MCP registration and hook config
into .claude/settings.json”, but current Claude Code reads project MCP servers
from .mcp.json — install writes the server there and keeps only hooks in
settings.json. Flagged per guardrail 2 (report, don’t silently guess); if a
target CC version wants servers in settings.json, that’s a one-line change.
(4) openBackend takes an injectable embedder so tests stay lexical-only and
offline. Note: the pre-existing M2 claude-md-only init test can time out under
full-monorepo parallelism (5s per-test budget vs. git-worktree contention);
green in isolation — not an M4 change.
What: (M5.1) pure, dependency-free redaction engine — gitleaks-subset secret
rules, a Shannon-entropy scanner whose token charset excludes path/URL
punctuation (so git SHAs, hex UUIDs, and file paths never cross the 4.5
bits/char line — hex tops out at 4.0), PII (email/ip/phone, strict-only), and
a gitignore-flavored deny-glob matcher; typed «REDACTED:type» markers. A
133-case public corpus (packages/redact/corpus) is the CI release gate.
(M5.2) Claude Code capture hooks: pure mappers turn raw payloads into C2
events carrying only {kind, path?, exit_code?} — content fields are read to
classify but never stored, with a defense-in-depth key filter dropping
content|old_string|new_string|command; deny-listed paths drop the event; every
event is redacted before it leaves the handler. (M5.3) daemon Spool persists
to ~/.teambrain/spool/
What: reads new redacted records off the never-merged teambrain/sessions
branch since a brain.yaml state.distill.watermark (git diff of sessions/*),
plus merged-PR metadata via gh pr list --json. clusterSignals folds four
signals into deterministic evidence bundles (sessions[] + commits[] map to C1
evidence): same-path struggles across ≥2 sessions, repeated failing commands,
no-hit memory retrievals, agent-proposed candidates (merged by title). PRs are
linked to path clusters by touched file, enriching commit evidence. Sources
(SessionSource, PullRequestSource) are injectable so the whole stage is tested
without git or the network; the git/gh drivers are covered by a temp-repo test
and a fake-exec test. Watermark writes are a yaml document round-trip that
preserves other keys/comments.
Decisions/tradeoffs: (1) failing commands cluster by kind, not command text —
the privacy model never captures the command, so finer grouping is impossible;
(2) no-hit clustering counts empty memory_retrieved events but can’t group by
query (query text isn’t recorded), so it’s a documentation-gap volume signal;
(3) GitLab PR driver deferred (V1 is GitHub-only via gh), noted in prs.ts;
(4) M6.1 has no standalone Accept — the golden pipeline test lands with M6.4;
this stage ships with unit + integration coverage per the DoD.
What: C5 Provider interface (complete({system,prompt,schema})→zod-validated T or throws) with a FakeProvider (responder, tests) and a real anthropicProvider (official SDK, structured outputs via messages.parse + zodOutputFormat, lazy import, model from brain.yaml → default claude-opus-4-8). draftCandidates makes one Provider call per cluster using the versioned prompt prompts/distill-v1.md, fills the deterministic C1 fields (advisory/active/team, evidence from the cluster) around the model’s class/title/body/tags; a rejected call is discarded and counted. Deps added to distill: @anthropic-ai/sdk + zod (LLM calls stay in distill per guardrail 4). Tradeoffs: openai/ollama drivers deferred (V1 = anthropic + fake); the prompt ships beside the built code and loads via import.meta.url.
What: dedupCandidates embeds each candidate (injected EmbedFn; CLI wires the index’s bge/HashingEmbedder), drops it when cosine ≥0.85 vs an existing active memory, else runs a pairwise contradiction check (Provider call) against the top-3 neighbors and, on “contradicts”, sets supersedes + a conflict flag. Novelty = 1−max_sim feeds M6.4 scoring. Existing memories load from .teambrain/memories only (retired excluded — R5 stance). Tradeoffs: ≥0.85 is a hard drop (amendment-marking noted as a future option); conflict checks are best-effort — a failed Provider call is treated as no-conflict (principle 2).
What: gateCandidates scores evidence_count × novelty(1−max_sim), keeps top ≤10
(deterministic tie-break), and renderPrBody emits a summary table with a
supersedes flag section. pipeline.distill() wires collect→cluster→draft→dedup→
gate with no git side effects (also the –dry-run path). tb distill writes one
file per proposal onto teambrain/proposals-
What: tb digest aggregates proposed/approved/retired counts, top-retrieved,
no-hit queries, stale ≥90d, and rules-file drift into a Slack JSON payload. The
aggregator is structurally people-free (M7.1 guardrail). tb doctor reports
daemon + index health (liveness, freshness, heartbeats, branch-sync) with a
frozen JSON schema. ci-templates/ drop-in actions cover lint, distill cron,
digest cron, and sessions-branch rotation.
Accept: Unit and schema tests pass; templates pass actionlint without errors.
What: Implemented rules-directive fallback and MCP-side inference for Cursor.
tb install cursor writes .cursor/mcp.json and .cursor/rules/teambrain.mdc.
tb mcp --client cursor triggers a CursorInterceptor wrapper that injects C2
events (session_start, candidate_proposed, session_end) via the MCP socket
client. Handled degraded tool_use capture cleanly and surfaced in tb doctor.
Added Cursor parity fixture (raw-cursor.jsonl) and updated README matrix.
Tradeoffs: Cursor lacks native hooks, so edit/command telemetry is unavailable.
redactEvent updated to gracefully preserve LLM-proposed body fields.
What: Completed the V1 readiness checklist. Created the Nightly Full-Loop Integration workflow
in .github/workflows/nightly.yml that runs the end-to-end integration test 3 consecutive times.
Verified that npm pack correctly builds the tarball and rewrites workspace:* versions, preparing
the CLI for publishing. Ensured FORMAT.md, SECURITY.md, and README are fully up to date with
installation and usage commands.
Accept: Ran pnpm run test:integration executing full-loop.integration.test.ts successfully 3 times.
Verified the tarball contents explicitly point to correct versions. tb doctor and tb init are functional.
What: the sessions-branch publish path spun up a throwaway git worktree
(checkout + add + commit + remove + prune ≈ 6–8 subprocesses + temp-dir churn)
per record. Under parallel test load it contended on git/disk and blew the 5s
per-test budget (the spool integration test timed out intermittently). Replaced
it with pure plumbing against a temporary GIT_INDEX_FILE: hash-object -w →
read-tree the branch tip → update-index → write-tree → commit-tree -p tip →
update-ref (compare-and-set). No working tree is ever checked out. Also dropped
a dead git mktree call (the empty tree is always known to git). Behavior is
unchanged (same orphan branch, same paths, idempotent re-handling) and faster;
the full suite is stable across repeated parallel runs.
What: enriched the daemon heartbeat with self-observability — brain checksum
(freshness), last-reindex time, per-tool hook heartbeats (last event + count,
keyed by C2 tool), and retrieval p95 over a rolling window of the daemon’s
last 100 context renders. tb doctor assembles these into a frozen,
zod-validated DoctorReport (daemon/index/retrieval/hooks/sync/checks), adds a
git branch-sync check (ahead/behind vs upstream), and validates its own output
before printing (human or –json). Exit 0 when reachable, 2 otherwise.
Tradeoffs: retrieval p95 covers the daemon’s own context renders, not the
agent’s MCP-server-process searches (a separate short-lived process) — honest
partial signal, labelled as such; every field degrades to null from a
partial/absent heartbeat. Accept: –json schema test + observability-field test.
What: a weekly CI digest. The aggregator consumes AggregateEvent — a projection
that keeps only {ev, data} and structurally drops every identity-bearing field
(the C2 join keys and any future author/user), so the output is people-free by
construction (Tech Brief §4.7). Computes proposed/approved/retired counts,
top-retrieved ids, no-hit search volume (doc gaps), stale ≥90d (active + no
retrieval in the window), and rules-file drift (sha256 of CLAUDE.md/AGENTS.md/
.cursorrules/.cursor/rules vs a brain.yaml baseline). Renders a Slack
incoming-webhook payload; tb digest [--dry-run] prints or posts (best-effort,
never throws — the only network egress in the digest path, guideline 4).
Guardrail test: authored fixtures → no per-person data in the output.
What: shipped distill.yml (weekly cron → proposals PR), digest.yml (weekly cron
→ Slack), sessions-rotation.yml (monthly squash+prune of the teambrain/sessions
orphan branch via commit-tree + force-push, records kept), alongside the
existing lint.yml, plus a README table. Workflows fetch the sessions branch
best-effort and never touch main. Note: actionlint could not be executed here
(installing it was blocked as untrusted external code); the templates were hand-
validated (valid triggers/cron/permissions, shellcheck-safe quoted run
scripts) and parse as YAML — run actionlint ci-templates/*.yml in CI to
confirm. Also raised testTimeout to 20s for the git-subprocess-heavy mcp/cli
integration suites, which are correct but slow under peak parallel contention.
What: implemented tb retire <id> <reason> (C6) — finds the active memory by
id, and on a throwaway worktree git-moves it to retired/ with status: retired
(C1), commits a teambrain/retire-
What: .github/workflows/release.yml fires on a v* tag: job 1 pnpm -r publish
--provenance (all @teambrain/* packages, workspace:* rewritten to real
versions), job 2 a bun --compile matrix (darwin-arm64/x64, linux-x64) that
smoke-tests --version and attaches the binaries to the GitHub Release. Made
every package publishable (license, repository, publishConfig, files). Verified
locally: pnpm --filter cli pack produces a tarball with workspace deps
rewritten to versions (registry-installable once published), and
bun build --compile of the cli yields a single binary that runs --version,
doctor --json, and --help (embeds the native better-sqlite3). Post-install
self-check is tb doctor (documented in the README quick start). Blocker (per
DoD): the literal “npm pack installs clean on a bare container” and the actual
npm publish / cross-platform binaries can only be verified in CI — the sandbox
has no registry, no bare container, and cross-OS runners; the pieces are staged
and locally smoke-tested.
What: rewrote README.md with a <5-min quick start (install → init → install
claude-code → serve → doctor) + an everyday-commands section and an accurate
status list; added FORMAT.md (the C1 memory spec — layout, front-matter table,
body rules, canonical serialization) and SECURITY.md (the §5 threat model
summary, memory-poisoning stance front and centre); added a tb --help
examples block (commander addHelpText) so the top-level help lists the quick
start + everyday commands, with per-command --help from each description.
What: renderMemoryBlock opened/closed the data, not instructions block with
a fixed ``` fence, so a memory body containing ``` broke out
and the trailing text rendered as agent instructions (in memory_search,
memory_context, and the SessionStart bundle). Now the fence is a back-tick run
one longer than the longest run inside (CommonMark closing rule). Regression
tests added.
Why: C3’s injection-mitigation guarantee — a payload past tb lint still can’t
pose as a live instruction — was bypassable; this was the audit’s only Critical.
Tradeoffs: fence length is now content-dependent; tb lint deliberately keeps
allowing back-ticks (legit code snippets), the fence is the correct defense.
What: testdata/compat/v1-brain/ generated from current main via the shipped serializers (one memory per class exercising evidence/supersedes/ttl/required, one retired, brain.yaml, a 5-event C2 session record); packages/core compat-v1.test.ts asserts parse→serialize byte-equality on every file, field survival, and per-line C2 round-trips. Fixture committed before the test (fixture-first guardrail); gate verified to fail on a 1-byte perturbation. Why: I0 Accept requires future code to read the v1 formats byte-correctly. Tradeoffs: fixture is frozen — never regenerate it to appease the test.
What: cli/src/egress-guard.test.ts scans every packages/*/src (minus tests, helpers, bench) for network syntax (fetch(, node:http(s) imports, http-client deps, websockets, the Anthropic SDK) against an explicit allowlist: digest/slack.ts, distill/anthropic.ts, index/embeddings.ts. Negative control asserts the allowlisted files DO trip the patterns; a breadth floor prevents vacuous passes. Why: guardrail 4’s “CI test greps the bundle” was mandated but absent (F3). Tradeoffs: first run caught embeddings.ts — the M3.2 model download is a legitimate fourth egress point guardrail 4’s wording omits; logged as F8 rather than silently allowlisted.
What: all seven workflow files (ci, nightly, release + 4 ci-templates) pass actionlint 1.7.1 locally; ci.yml gains an actionlint job pinned to that same version (docker://rhysd/actionlint:1.7.1), shellcheck/pyflakes disabled to match the locally-verified surface. Why: the M7 accept was unverifiable during the audit (F6) and unenforced. Tradeoffs: shellcheck/pyflakes stay off until I3/I4 triage their findings — a deliberately narrower gate that is actually known-green.
What: the two commands C6 lists that were never built. tb reindex forces a
full rebuild through openBackend and, when index.db is unreadable, deletes the
cache and rebuilds from the brain tree; its negative test exposed a real bug —
SqliteIndex.open leaked the db handle on a corrupt file, which on Windows
locks the file and blocks exactly this recovery (fixed in open()). tb
propose queues a zod-validated draft to the local candidate spool (identical
trust model to C3 memory_propose), body from –body/stdin, most recent
session cited as evidence.
Why: AUDIT.md F2 — the frozen CLI surface was incomplete.
Tradeoffs: propose does not run injection lint at queue time (parity with the
MCP tool; tb lint gates at PR time, where it belongs).
What: user-paths.ts is now the only module that names ~/.teambrain/user/; tb serve materializes the dir from the CLI layer. Two-layer negative test: spool.ts’s transitive import graph must never reference the user path (perturbation-verified), and seeded user/ content is absent from every reachable git object in both the local repo and a pushed bare remote. Why: AUDIT.md F4 — C7’s separation guarantee (and SECURITY.md’s claim of it) had no implementation and no test. Tradeoffs: the guarantee is structural (module boundary) + behavioral (object walk), not OS-enforced; that matches C7’s “separate module without that path in scope” wording exactly.
What: tb.ts now delegates to program.ts’s buildProgram() (extracted from the
old monolithic entrypoint); help-text.ts centralizes root/per-command help
copy; commands are grouped (Setup/Daemon/Quality/Capture) via helpGroup(),
with mcp/hook hidden from the root listing but still fully documented via
their own –help. Root help lists exit codes and getting-started/day-to-day/
CI example blocks.
Why: tb --help was a flat, ungrouped list; new contributors and CI authors
need exit-code semantics and command grouping at a glance.
Tradeoffs: root help intentionally avoids the words “hook”/”mcp” (both
internal implementation details) — reworded “capture hooks” → “capture
wiring” and “hook heartbeats” → “capture heartbeats” in install/doctor’s
descriptions and the getting-started block; the hook subcommand’s own
–help still documents all four events in full.
What: POSTV1_PLAN.md committed; STATUS.md rewritten as an evidence-based D0 baseline — clean-clone full suite green (504 unit + 43 integration, bench budgets met), live-registry install of @teambrain/cli@0.0.1 verified working, privacy/egress/join-key invariants re-audited, compat fixture gate confirmed. Why: the post-V1 instructions’ ground truth was stale — npm publish and Cursor capture already exist; D0 exists precisely to catch that before building. Tradeoffs: “clean container” approximated by a fresh npm prefix + scratch repo on Windows (found the core.longpaths clone failure doing so); D1/D2 scopes shrunk to their true residuals in STATUS.md rather than re-executed wholesale.
What: release.yml gained a post-publish bare-machine smoke job (npm i -g from the live registry on node:20 → tb –version/init/doctor, propagation retries) and a gated GitHub Release with generated notes; README’s Cursor note/matrix now state that session_end is inferred only on memory_propose and commits are not captured; cursor-wrapper and tb hook log dropped events at debug level. Why: D0 findings — README overclaimed Cursor capture; two silent catches violated CLAUDE.md; a release could previously stand without proving install. Tradeoffs: generate_release_notes over a committed CHANGELOG.md (no new dep, notes live on the Release page); tb doctor exits 0 daemon-down, so the smoke job doesn’t yet assert daemon health (doctor exit codes are a D5.1 item).
What: CursorInterceptor now ends an open session after 30 min of inactivity — lazily on the next MCP call (stale end emitted before the call is interpreted, so a returning memory_context starts a fresh sid) and eagerly via flushIdle(), which cursor-wrapper arms on an unref’d timer after every call. duration_s now measures start→last-activity instead of always 0. Negative tests: activity inside the window never ends a session; flushIdle without a session is a no-op. Why: STATUS.md D0 finding — a Cursor session that never proposed a memory never emitted session_end, so outcome-mix aggregates (D3.2) would undercount. Tradeoffs: 30 min is a constant, not config — plumbing it into brain.yaml can wait for a user who needs it; idle end can’t know outcome (stays ‘unknown’).
What: practice-signals.ts (session-grouped, people-free aggregates: outcome mix, retries, failures, retrieval rate, co-occurrence, context-setup proxy) rendered in the digest; governance friction (median proposal-PR time-to-merge via injectable gh) in digest + doctor –json; distiller PR body redesigned for <60s/candidate review with a byte-exact golden; PRACTICE_SIGNALS.md records a conditional GO on metadata-only FlightDeck. Why: D3 is the strategic core — it decides whether FlightDeck is buildable without content capture. Verdict: yes, scoped to the strong signals; plan_revision has no emitter and co-occurrence must stay labeled correlation. Tradeoffs: doctor’s gh query wired at the CLI layer (not defaulted) to keep tests subprocess-free; context-setup is an events-before-first-tool proxy.
What: incremental hash-manifest summarizer (packages/distill, C5 Provider,
tb distill --codemap, ci-templates/codemap.yml) writing byte-stable entries
to .teambrain/codemap/files/**; index syncs the tree under C4’s reserved
source ‘codemap’ (checksum-idempotent, disabled flag empties the source);
memory_context serves a separately-budgeted 1500-token codemap slice next to
the untouched 2000-token memory pool; memory_search returns both sources
tagged. Bench: 500k-LOC fixture, 20-file incremental in 8.1s (<120s budget).
Why: POSTV1_PLAN D6 — D0–D3 green and the build was explicitly requested.
Tradeoffs: the ≥30% exploration-token acceptance is NOT measurable from
today’s stream — C2’s tool_use.kind is frozen to edit|command|test, so
Read/Grep events are never captured; adding an ‘explore’ kind is a C2 change
that needs explicit approval (reported, not done). Codemap slice rides in
C3’s existing relevant array (shape unchanged; entries tagged by additive
MemoryView.source).
What: C2 tool_use.kind gains ‘explore’ (CONTRACTS.md updated with the approval note); Claude Code hooks map Read/Grep/Glob to explore events with path-only metadata (patterns/queries never read); practice signals compute exploration/session and the D6 instrument — median exploration split by codemap-retrieving (cm:-prefixed retrieved ids) vs non-retrieving sessions, with reduction % against the §4.8 ≥30% target, rendered in the digest. Why: D6’s last acceptance criterion was unmeasurable from the frozen event stream; the human approved the additive contract change. Tradeoffs: event counts proxy token spend (true token counts aren’t in any hook payload); replay fixture expectations updated deliberately — Read now captures instead of dropping, still content-free (forbidden-keys test unchanged and green).
What: audited the CodeMap CM0–CM6 brief against the shipped D6 build — the
feature already exists (generator, CI template, isolation/staleness/
neutrality/e2e tests, digest instrument; 542 tests green); added the two
missing artifacts: docs/adr/codemap-backend.md (build-vs-buy + OQ-CM4 swap
threshold) and a README “CodeMap (v1.1, opt-in)” section, honest about
default-off. Deviations from the brief’s letter left for a human decision
(reported, not done): no packages/codemap (generation lives in distill, the
sole LLM boundary); tb distill --codemap not tb codemap build (C6 verb
list is frozen; a flag is additive); brain.yaml codemap block has enabled
only (no denyGlobs/granularity — behavior for them isn’t built); prompt is a
versioned code constant, not an in-repo .md file; no session-start relevance
scoping yet (OQ-CM2).
What: documented CodeMap everywhere it was missing beyond the README — ci-templates/README.md (codemap.yml row + direct-commit and source-to-LLM notes), FORMAT.md (codemap/ layout + entry format from codemap-entry.ts), SECURITY.md (CI↔LLM boundary widens to source contents when opted in; a CodeMap-injection threat entry — verified codemap bodies render in the same data-not-instructions fence), DEVELOPMENT.md repo map rows. Why: docs only mentioned CodeMap in the README; the security posture change (source leaves CI when enabled) was undisclosed. Tradeoffs: none — docs-only, honest about opt-in status.
What: extracted the registry-driven CaptureAdapter framework
(packages/hooks/src/adapter.ts + registry.ts); Claude Code and Cursor
refactored onto it — their mappers/merges moved, cli install/settings
re-export shims keep every existing test untouched. tb install resolves
from the registry; the README capture matrix is generated from
ADAPTERS[*].capabilities and asserted byte-for-byte by matrix.test.ts.
C6 reading: widening tb install <tool>’s argument set is additive, not a
contract change — CONTRACTS.md untouched.
Executed spikes for Codex, Gemini CLI, Cline, Kiro, and Antigravity per ADAPTERS_PLAN.md A1.
Spike Findings:
notify hook in global ~/.codex/config.toml (CODEX_HOME honored), fired only on turn-ended (agent-turn-complete) — payload carries thread/turn ids and message text, no tool events, no session lifecycle. Not enough for Tier A, and the payload’s input-messages/last-assistant-message fields are content we must never read.testdata/sessions/raw-codex.jsonl (kept as evidence for the tier decision, not mapped).tb mcp --client codex.sessionStart: true, sessionEnd: true, toolUse: false, commitShas: false, planRevision: false..gemini/settings.json, supporting standard lifecycle hooks like SessionStart, AfterTool, and SessionEnd (migratable from Claude Code via gemini hooks migrate).session_id, cwd, hook_event_name, etc.testdata/sessions/raw-gemini-cli.jsonl.sessionStart: true, sessionEnd: true, toolUse: true, commitShas: true, planRevision: false.What: shipped the Codex (Tier B) and Gemini CLI (Tier A) adapters with the
full per-adapter test set (fixture replay, privacy negative, C2 validity,
idempotent install, doctor honesty, latency). Review fixes over the first
draft: the Tier-B MCP wrapper is generalized to any mcp-inference adapter
(tb mcp --client codex now actually captures — before, only cursor was
wrapped, so Codex’s declared session capture never fired); Codex installs
honor CODEX_HOME (tests stay out of the real home dir); Gemini’s two merges
into .gemini/settings.json are composed into one InstallFile (two plans on
one path clobbered the MCP-server merge on first run); Gemini hooks register
tb hook … --tool gemini-cli so events carry the real vendor id instead of
defaulting to claude-code.
Why blocked: Cline, Kiro and Antigravity spikes found no verified install
surface (no confirmed config location / client connection), so per the plan
they get NO adapter — a first draft shipped them with empty install plans,
which reported “installed” while capturing nothing; removed as dishonest.
Tradeoffs: the README matrix shrinks to four tools but every cell is true;
R1 (Codex tagline) is now TRUE with working tb install codex; the gemini
fixture is thin (4 events) — flagged for re-recording in a longer session.
What: ensureDaemon() in @teambrain/mcp — probe → exclusive lock
(daemon.lock, 30s TTL, stale-breaking) → detached tb serve spawn → poll
until deadline (1.5s); wired into requestSessionContext and tb mcp boot
only (never sendHookEvent’s <20ms path; never plain pingDaemon, so doctor
stays truthful). One stderr disclosure line on cold start; tb serve --stop
(idempotent) and tb doctor --fix added; kill-switches: brain.yaml
daemon.autostart, TEAMBRAIN_NO_AUTOSTART, CI (env beats config).
Why: “developers never have to remember tb serve” without breaking the
no-servers trust promise — disclosed, stoppable, disableable.
Tradeoffs: C6 reading — --stop/--fix flags on frozen verbs are additive,
not a contract change (same reading as tb install’s widened argument set).
The real spawner refuses under VITEST so no test can ever start a real
daemon; auto-start paths are covered by 12 injected-spawn unit tests.
What: pnpm eval — real corpus (testdata/eval/corpus, 20 memories: dogfood
brain + hand-written fixtures) + 48 paraphrased queries (8 negatives) run
against the production bge-small embedder under four ablation modes
(lexical / vector / hybrid RRF / weighted 0.7-0.3); memory_context
hit-rate + latency. Knobs are additive SearchOptions fields whose defaults
are byte-identical to shipped RRF (asserted). Results in docs/RETRIEVAL.md.
Why: §4 verdict needed a number — got it: hybrid recall@5 = 0.95 ≥ 0.90 →
question closed, no sophistication added; weighted fusion measured worse.
Tradeoffs: corpus is 20 memories (below the 50–150 brief minimum) and
queries are assistant-written, not human-blind — recorded as caveats, not
hidden. Real findings kept: no abstention on negatives (trust gap, needs a
similarity floor, not an RRF threshold) and real-embedder p50 185ms vs the
synthetic 80ms budget assumption.
What: renderMemoryBlock now heads codemap blocks with
[codemap · generated from <path> · not human-approved]; governed memory
rendering is byte-unchanged (inline-snapshot gated).
Why: governance legibility — agents must be able to tell approved knowledge
from a generated, possibly-stale map. Tradeoffs: none; purely additive branch.
What: renderContextBundle reserves a 30% char share for codemap blocks when
any are present (never more than what required blocks leave over); memory
advisory fills the rest; codemap rides at the tail. Zero codemap views → the
V1 code path, byte-identical (asserted against an inlined V1 algorithm).
Why: codemap was appended last and truncated first — silently evicted by any
advisory-heavy brain. Tradeoffs: chars are a shared physical cap, so unlike
tokens the two pools can’t both be maximal; codemap gets a guaranteed floor.
What: non-empty codemap → renderContextBundle emits a ≤200-token index
block (entry count, path-derived modules via SqliteIndex.codemapStats(),
freshness) plus the one instruction we actually want followed: search the map
before exploring files. Preamble region only — outside every fence. Empty
codemap → byte-identical bundle (gated). Wired into the daemon + the
memory_context text channel.
Why: the map existed but nothing ever told an agent to use it — the
highest-leverage line of text in the feature. Tradeoffs: it’s a prompt, not
an API; T7 measures whether it works.
What: contextDocs gained a paths filter (codemap docs only); the daemon
tracks session-relevant paths (tool_use hook events, bounded + normalized,
∪ branch diff vs the default branch, best-effort) and passes them to
memoryContext. paths=[] (no signal) → no slice, index block only — never
“newest”. Scoped slice budget: 500 tokens (CODEMAP_SCOPED_TOKEN_BUDGET);
total session-start codemap footprint ≤700 (gated).
Why: recency is a poor relevance proxy; session-touched files are near-
certain. Tradeoffs: the legacy unscoped 1500 path survives for direct
library callers only (frozen tests encode it); no serving path uses it.
What: after each updateCodemap run the entry tree is swept against the NEW
manifest: any .md whose derived repo path isn’t a manifest key is deleted
(reported as orphaned), and emptied directories are pruned bottom-up.
Delete failures are logged with path+reason (never silent) and retried next
run. Covers: rename (file + directory), corrupt manifest (rebuild AND clean),
stray entries from failed deletes, idempotence — all with disk, manifest,
and retrieval-level assertions.
Why: loadCodemapDocs walks the DISK while deletion iterated the OLD manifest
— that asymmetry served phantom files forever (D1/D2). Tradeoffs: distill
gains a test-only workspace devDep on @teambrain/index to assert retrieval.
What: when a run removes/orphans paths, entries whose stored summaries
mention a dead path are force re-summarized (their own hash is unchanged so
the diff would never touch them). Cheap substring scan — no import graph.
Fan-out capped (default 20, maxNeighbourRefresh), cap logged.
Why: summaries carry cross-module references; renames/deletes let the map
accrete references to files that don’t exist (D3). Tradeoffs: substring
match can over-trigger (src/b.ts also matches src/b.tsx mentions) — worst
case a wasted re-summary, bounded by the cap.
What: codemapQueryRate in PracticeSignals — sessions retrieving ≥1
cm:-prefixed id / all sessions — surfaced in tb digest next to the
existing exploration median and by-codemap reduction %. Same people-free
projection; the negative test still gates identity leakage.
Why: the CM6 gate (≥30% exploration reduction) needs both arms observable:
do agents query the map at all, and does exploration fall when they do. If
the rate stays ~0, the answer is better map content, not more pushed tokens.
What: windowsHide: true on every child spawn in console-less contexts —
the autostart daemon spawn (detached children get their OWN visible console
on Windows; closing it killed the daemon and the next hook resurrected it,
window and all), plus every git child of the daemon (fetch, branch-diff
refresh, spool push) and of the hooks. Why: user-visible console windows
appearing indefinitely. Tradeoffs: none; display-only flag.
What: codemap.holdout (0–1, default 0.1) added to brain.yaml’s codemap block
(additive; existing field-specific tests + compat fixture stay green). New pure
core module codemap-arm.ts: fnv1a (tiny dep-free 32-bit hash) + codemapArm(sid,
holdout) (hash(sid) % 100 < holdout*100 → control) + effectiveHoldout (disabled
codemap ⇒ 0 ⇒ every sid treatment, since serving is already off).
Why: the CM6 gate needs a randomized control arm measured, not a before/after
estimate. Assignment must be deterministic per sid across every process (hook
tag, daemon bundle, MCP search) or the arms disagree. Tradeoffs: FNV-1a hand-
rolled (boring-deps); disabled-arm tag is meaningless-but-harmless.
What: replaced the continuous publish.yml (which only published on a manual
version bump, so merges were silent no-ops) with a Changesets flow. pnpm
changeset per change → on merge to main, changesets/action opens a “Version
Packages” PR (bumps + CHANGELOGs); merging it runs pnpm run release
(pnpm -r publish, rewrites workspace:* + skips already-published). All
@teambrain/* are fixed (lockstep) so one changeset bumps all seven together.
Added a changeset for the pending R16.1 T7 + performance-metrics work (→ 0.5.0).
Why: user reported “CI doesn’t push to npm” — diagnosis: publishing worked
(0.4.0 live) but nothing new shipped because the version was never bumped;
Changesets automates the bump. Tradeoffs: kept release.yml for tag-triggered
standalone binaries (Changesets tags are per-package, not v); pnpm publish
(not changeset publish) so workspace: deps are rewritten. ci.yml actionlint
list + STATUS.md updated for the renamed workflow.
What: new tb metrics [--json] — a read-only local dump for “why is my context
slow/noisy”: index size, latency percentiles (from the daemon heartbeat via the
doctor snapshot), injection weight, required-load, codemap utilization, served
staleness, and the net-efficiency verdict. Reuses the digest aggregation + the
doctor report; captures nothing, writes nothing (Acceptance §7). CONTRACTS C6
updated to add the verb — done with explicit human approval (a new top-level
verb is a frozen-surface change, unlike the additive flags precedent), noted
inline in CONTRACTS as additive + people-free.
Why: §5 — a local debugging surface for context cost/rot without waiting for the
weekly digest. Tradeoffs: latency fields are empty when the daemon is down (same
as doctor); no new privacy surface since it only reads existing aggregates.
What: netEfficiency in the digest — avoided exploration (from the T7 CodeMap
holdout arms, treatment vs randomized control, NOT before/after) paired with the
injection weight it costs, with the same measured/estimated + 95% CI labeling.
An honest verdict: insufficient-data (until the holdout is measured), net-anti-rot
(measured, CI excludes zero, ≥30% CM6 bar), net-rot (treatment explored more), or
inconclusive. Rendered as a plain statement in tb digest.
Why: §3.3 — the one number that decides whether TeamBrain is worth its context
cost. If not net-anti-rot on real data, that finding outranks every feature.
Tradeoffs: reuses the holdout split (avoids the self-selecting codemap-retrieving
vs not confound); verdict never claims a win without a measured, CI-excludes-zero
result.
What: a people-free timing channel — a timing daemon message ({metric, ms},
no identity). The daemon keeps rolling p50/p95 windows for injection (its own
context render), search (reported by the MCP server around memory_search),
and hook (reported by the capture handler around map+redact). tb doctor
--json gains latency.{injection,search,hook} (real numbers vs the synthetic
bench, against the 500/300/20ms NFRs) plus index bloat signals reindexCount +
dbSizeBytes. Existing retrieval field kept (backward-compat) fed by injection.
Why: §3.2 — a benchmark is a promise; this measures the kept promise in real use.
Tradeoffs: search/hook latencies depend on the daemon being up (samples dropped
if down); distiller cost (also §3.2 prose, not in §6 acceptance) deferred — it
runs in CI, not the daemon.
What: Finding — memory_retrieved events were NEVER emitted in the live path
(no MCP-result hook; Cursor’s memory_search branch is a no-op), so injection,
retrieval-rate, and codemap-query metrics were all unfed on real data. Fix
(PM0): the daemon logs a memory_retrieved {ids, via:'context', tokens,
required, required_tokens} event when it serves a sid-bearing session_context
bundle — it’s the authority on what it injected. Reuses the frozen C2 event with
additive data fields (no CONTRACTS change); ids are structural (ULIDs + cm:path),
no content. Existing metrics ignore via:'context' events (isContextInjection),
so no metric shifts. PM1: new context-metrics.ts computes injection weight,
required-load (+budget flag via metrics.required_max_tokens), codemap
utilization (injected map paths later touched by tool_use), and served staleness
(injected memories ≥staleDays old) — all people-free, surfaced in tb digest.
Why: answers “is injected context used, fresh, worth its budget — or is TeamBrain
causing context rot?” Tradeoffs: utilization is codemap-only (governed memories
have no code path in metadata); query-side retrieval is still uncaptured (chose
injection logging over an MCP-result hook), so query-rate metrics stay unfed —
documented. Contradiction count deferred (needs the LLM Provider; not in §6.1
acceptance).
What: the README CodeMap section now explains the holdout (what it is, why —
clean causal measurement vs a confounded before/after, how to change/disable via
codemap.holdout: 0, and that a control session behaves as if CodeMap were off);
restated the default-on gate as a measured result; corrected the stale
1,500-token budget line to the shipped ≤~700 (index ≤200 + scoped ≤500). Flagged
the honest caveat that the search-side control exclusion needs TEAMBRAIN_SESSION_ID.
Why: T7e — the holdout must be disclosed and configurable. Tradeoffs: none; docs.
What: computePracticeSignals reads each session’s codemap_arm and emits
codemapHoldout — explore-actions/session and codemap query rate per arm, plus
the treatment-vs-control reduction% with a seeded (mulberry32, deterministic)
95% bootstrap CI over sessions. Labeled measured only when both arms ≥20
sessions (MIN_ARM_SESSIONS), else estimated. Slack renderer never prints the
effect without its label and per-arm n (control/treatment).
Why: the CM6 gate is a measured holdout result, not a before/after estimate; an
unlabeled reduction is the confounded number the holdout exists to replace.
Tradeoffs: reduction is on the mean (explore-actions/session reads as a mean),
not the median D6 instrument; both coexist. Bootstrap is 2000 iters, seeded so
the CI is reproducible; people-free by construction (arm counts only).
What: codemap-arm-serving.ts centralizes the arm decision (servesCodemap)
and the search filter (filterSearchForArm); openBackend now exposes the
effective codemapHoldout. Bundle path: the SessionStart hook threads its sid
→ session_context request → daemon contextBundle(sid); a control session gets
paths:[] + null stats, i.e. the byte-identical empty-codemap bundle (asserted).
Search path: tb mcp reads its sid from TEAMBRAIN_SESSION_ID, computes the
arm, and (control) drops source:’codemap’ at the one memory_search chokepoint.
Also: tb hook session-start now emits the session_start event (previously
never captured on the native path) so the arm tag actually reaches the spool.
Why: CM6 needs a clean control baseline; both surfaces must key off one arm.
Tradeoffs / flagged: no vendor exposes a documented per-session id to a
.mcp.json-launched server, so TEAMBRAIN_SESSION_ID is the mechanism but the
install does NOT yet write a speculative placeholder (would break the zero-diff
install + could inject an empty env). Absent the var, search serves treatment-
equivalently while the bundle bypass (sid-reliable) still fully applies — a
partial, honest degradation, documented in the README.
What: mapSessionStart now tags data.codemap_arm (control|treatment),
computed from the session’s own sid + the effective holdout (read from
brain.yaml in buildHookContext, 0 when codemap disabled/config malformed).
Additive under C2 (session_start.data is looseObject) — CONTRACTS untouched.
Why: the digest (T7d) needs every session’s arm to split metrics; tagging at
the one place that owns session identity keeps hook and serving in agreement.
Tradeoffs: privacy negative test extended — arm is people-free metadata and the
sole session_start key; the Cursor Tier-B path is unchanged (arm-less → digest
buckets it as neither arm).
What: ensureDaemon now records consecutive failed start attempts (runtimeDir/autostart-failures.json). After 3 failures it stops spawning and discloses once (“autostart paused for 10 min; run ‘tb serve’ to see why”); cooldown expiry allows one half-open attempt; success resets. Corrupt/absent record fails open. A throwing spawn counts as a failure. Why: a daemon that crashes on boot was respawned by every hook event, forever. Tradeoffs: state is per-runtimeDir and time-based, not persistent across cooldowns by design.
What: added verify [--json] [--strict] to C6 in CONTRACTS.md (additive
amendment A, ADR-6/EVIDENCE_BRIEF §9, explicit human approval 2026-07-21) with
its own exit-code note (0 pass / 2 could-not-run / 3 invariant violated). Ran
the OQ-8 spike before writing V2.
Why: tb verify is the phase keystone — privacy-by-construction as a command a
stranger runs on their own data. V2 needs an honest egress claim.
OQ-8 result: JS instrumentation (net.Socket.connect/http.request/fetch) DOES
observe JS-layer egress; native deps (better-sqlite3, onnxruntime-node) do
local I/O in C++/libuv and never traverse the JS net prototype, so JS-level
instrumentation is structurally blind to native sockets. Therefore V2 claims
only “no egress from TeamBrain’s JavaScript surface”; –strict is the OS-sandbox
tier for a stronger guarantee. Under-claim by construction (guardrail C6/§C).
Tradeoffs: V2 cannot make an unqualified “no egress” claim; stated explicitly
in the report allowlist and SECURITY.md rather than hidden.
What: finished the self-audit command. V1 provenance (npm attestation for all 7
packages; offline→UNVERIFIED, shell:true so Windows .cmd works), V2 egress via
a child-process socket probe replaying a serve+search (0 JS-layer connections;
scoped to the JS surface per OQ-8), V4 redaction corpus vs the INSTALLED
redactor (corpus now shipped in @teambrain/redact files[]; shared loader), V5
digest people-free over the real spool, V7 retired-unserved on the live index.
SECURITY.md now points at the command and names the F8 embedding endpoint in an
explicit egress allowlist.
Why: turn privacy-by-construction from 500 internal tests into one command a
stranger runs on their own data (ADR-6). Under-claim everywhere.
Tradeoffs: V1 is registry-attestation-presence, not full sigstore-chain (stated
in the report). V2 cannot see native sockets (stated; –strict is the OS tier).
Evidence: tb verify PASS in 17s online / exit 2 UNVERIFIED offline; 8 checks,
each with a negative control; –json golden; 677 tests green; four-tool MCP
snapshot + egress-guard unchanged; CONTRACTS diff = amendment A only.
What: tb digest --format markdown|json --out <path> (amendment D) renders the
weekly report from PRACTICE_SIGNALS strong-column signals only. Small-cell
suppression (n<5) lives in buildFlightDeckReport (the aggregator), not the
renderer, so a suppressed cell carries n but no value — –format json cannot
leak what markdown hides. Cursor unknown-inflation caveat inline; co-occurrence
labelled correlation; nothing reads plan_revision. Weekly CI template commits
the report under .teambrain/reports/ (CodeMap precedent, not PR-gated).
Why: makes the leadership-layer differentiator a durable, diffable artifact
from week one (ADR-9), without widening capture (PRACTICE_SIGNALS conditional
GO).
Tradeoffs: “Memories at risk” is a placeholder until E3 drift lands. Default
Slack behaviour unchanged. Reports never indexed (indexer scans memories/**
only; negative test asserts a reports/ file is unsearchable).
Evidence: 3-session fixture suppresses every derived cell + json has no
“value”; golden markdown body; not-indexed negative test; 680 tests green;
CONTRACTS diff = amendments A + D only. E2.4 (30-day dogfood) is a human task.
What: tb relevant --paths <glob…> [--query] [--k] [--json] (amendment B) —
the first human query path into the brain. Reuses openBackend + memorySearch
(C4), so retired/out-of-scope memories never surface; prints id·title·class
only (no bodies, no session/author data); fails open (exit 0 empty) with no
brain/no match. ci-templates/memory-review.yml posts ONE sticky comment per PR
(marker-based find-then-update → N pushes = 1 comment), ≤5 rows, brain.yaml
review.enabled:false opt-out, fails open on every path.
Why: memory meets developers in ordinary code review — the moment they can best
judge if it is still true (ADR-8), a git-native move a hosted rival must build
and host.
Tradeoffs: the Action comment render mirrors review-comment.ts (the tested
source of truth); the gh find-update itself is exercised only in real CI. No
new MCP tool (four-tool snapshot unchanged); C1/C2 diffs empty.
What: new @teambrain/inject-bench package. SystemUnderTest interface (ingest + serve — the MCP surface every memory server exposes), a scorer (tier 1 ingestion block, tier 2 containment; LLM-free), an adversarial corpus by attack class (instruction override, F1 fence-escape first-class, tool-invocation, exfiltration, scope escalation, homoglyph, encoded, sleeper), and two in-process adapters: teambrainSystem (scanForInjection + renderMemoryBlock) and vulnerableMockSystem (the E5.3 validity control). Results (reproduced by the tests from a clean clone): TeamBrain 100% safe (50% blocked at ingestion, 100% contained — F1 fence-escape included); the vulnerable mock scores 0% (so the benchmark can detect a known-bad system). Why: “PR review beats silent auto-write”, measured and published including our own tier-1 gaps (homoglyph/encoded/sleeper evade the keyword lint but tier-2 contains them). Honest > perfect scorecard. Tradeoffs: corpus is NOT shipped (files: dist only; defensive test material) — reproduction is from the repo. The over-the-wire MCP-client adapter is the same interface but not wired here (needs a live endpoint); in-process adapters are the tested/reproducible path. Tier 3 (behavioural) is out of scope. E5.4 (publish the results) is a human decision — NOT made. No contract change.
What: (E6.1/E6.2) new @teambrain/spec — MEMORY-FORMAT-1.0 / SESSION-EVENT-1.0
docs derived from CONTRACTS (stated upstream), C1/C2 zod schemas re-exported
with JSON Schema via z.toJSONSchema, and a standalone validator + bin
(teambrain-validate) that reuses the product parser so a PASS cannot diverge
from the frozen contract. Conformance corpus = testdata/compat/v1-brain
PROMOTED (not regenerated; byte-exactness still gated by core/compat-v1).
(E6.3) adapters.yaml as reviewable data + zod schema + a drift test asserting
its tool set equals the code registry (amendment C: install
What: sharpened the existing C3 memory_propose from passive infra into a real
signal. (A0) Confirmed the transport gap: on the native-hook (Claude Code) path
memory_propose only wrote the local candidate spool — never synced (C7) — so
agent proposals never reached CI; only the Cursor interceptor emitted the C2
event. (A1) Rewrote the tool description + added a preamble-region propose
protocol to the SessionStart standing context (when to propose, search-first,
human-approval guarantee) — no LLM call (C5). (A2) Injectable resolveEvidence
stamps the live session on agent drafts, closing the asymmetry with tb propose.
(A3) The propose handler now redacts title+body through the on-device redactor
before spool AND emit, then emits a C2 candidate_proposed event (wired in tb
mcp for the native-hook path; the inference path already emits, so no
double-emit). (A4) Agent candidates skip the draft Provider call (already
drafted) and flow through dedup+gate under the shared N≤10 cap, carrying
proposing-session evidence.
Why: the correction/gotcha/decision moment is the highest-signal, human-verified
learning; without timing + transport + grounding it was dead on arrival.
Tradeoffs: zero frozen-schema changes — behavioral/description only; adds
@teambrain/redact to mcp (pure, offline, in-tree). Cursor’s emitted event still
lacks auto-evidence (degraded path, deferred). Propose-time dedup, flywheel
calibration, and evidence.commits enrichment remain non-goals until real usage.
repository)What: added the homepage + repository (with directory) block to
@teambrain/spec and @teambrain/inject-bench — the only two published
packages missing it. The 0.5.0 release published core/index/redact then aborted
at spec with npm E422: “Error verifying sigstore provenance bundle …
repository.url is ‘’”. Both changesets runs since have failed identically.
Why: publishConfig.provenance makes npm verify the signed source repo against
the manifest, so a missing repository is a hard publish failure, not a warning.
Tradeoffs: metadata only — deliberately no changeset, since a bump would open a
Version Packages PR for 0.5.1 instead of completing 0.5.0; pnpm -r publish
skips the three already-published packages.
What: R1 — tb lint <repo-root> resolves one level into .teambrain/ instead of
reporting [schema] brain.yaml is missing; a directory with no brain is now a C6
user error (exit 1) rather than a lint violation (exit 3). Plus R2 (README
documents the npm 11 npm warn install-scripts warning) and R3b
(scripts/claims.mjs + a claims CI job generate the README test count).
Why: an outside tester on Linux/Node 26/npm 11 hit R1 on a scratch repo and
concluded the documented path argument was broken. Every previous DEVLOG entry
here traces to an internal audit or a milestone plan; this one traces to someone
using the tool. The classification was the real defect — the message was
misleading because a wrong path was labelled [schema], not merely terse.
Tradeoffs: resolution is one level only (no upward search to the git root) and
the default argument stays .teambrain, so ci-templates/lint.yml and every
documented CI invocation are untouched — predictability over convenience.
claims.mjs runs the full suite, so the claims job duplicates the test job’s
work; accepted for now because the alternative is trusting a number nobody
measured. Two things the report asked for are not done here: the tester’s own
“500+” and no-tagged-release copy lives in the post and launch package, which are
outside this repo (in-repo grep is zero for both), and the under-a-minute review
claim remains unvalidated by anyone outside the project (Appendix A, E-series).