Written at the end of a long session. Read this first, then bd list --status=open.
- Branch
chore/dependency-upgrade-mcp-v2, commitd711c2a, working tree clean. npm run build→ 0 errors. Suite → 2977 passing / 0 failing / 20 skipped (119 files).- Live MCP probe: stdout pure JSON-RPC, protocol
2025-06-18, version1.1.0, 17 tools / 0 empty schemas / 17 annotated,tools/callround-trips.
d711c2a contains the whole upgrade: Biome, tsx, isolatedModules, secrets/auth
hardening, gh + GitHub App auth, AI provider plumbing, MCP v2 migration, the agent
budget gate, server-side metering, and ~1400 test/lint fixes. Its commit message is the
detailed record — read it before changing any of those areas.
-
The e2e suite is intermittently flaky. Three consecutive full runs gave 2977 pass, 8 fail, 2977 pass. Failures are confined to the four spawn-based e2e files (
stdio-transport,stdout-purity,mcp-protocol-compliance, and anything viaMCPToolTestUtils). Root cause partially fixed — all four spawned servers used to bindWEBHOOK_PORT3001 and collide under Vitest's parallel file execution; they now setSSE_ENABLED: 'false'so no port is bound. Residual flakiness is server startup timing under parallel load, not a port clash.--no-file-parallelismis reliably green. Do not treat a single red run as a regression — re-run before investigating. -
GitNexus index is stale (last indexed
7c7d188, HEAD is nowd711c2a). Runnode .gitnexus/run.cjs analyzebefore relying onimpact/context, or the blast radius numbers will describe pre-upgrade code. -
Live-API E2E has never run (
hb8.25). Everything is verified against mocks.E2E_REAL_API=true npm run test:e2e:tools:realconsumes GitHub rate limit. Given Octokit 22, the GraphQL node-id resolution change inGitHubProjectRepository.create, and the MCP v2 migration, this is the only thing that proves the real API surface — andhb8.18is exactly the class of bug mocks hide.
A /grilling + /domain-modeling session settled 19 decisions. All of this is agreed
and confirmed — implement it, don't re-litigate it.
AgentBudget splits its counter by provenance:
meteredTokens— measured by us at the provider boundary (ai-SDK middleware).reportedTokens— asserted by the agent viarecord_usage. Unverifiable.usedTokens— derived sum, kept for compatibility.estimatedTokens— the third provenance (words×1.3 heuristic). Never touches budget.
Enforcement stays on the derived sum against one totalTokens. Splitting the counters
also makes the record_usage double-count structurally impossible (it currently routes
through the metering dispatcher and only escapes by accident) — pin that with a test.
Migration: reset all counters to zero. Verified safe — the agent registry holds one
agent, zero budgets, zero non-zero usedTokens. No lazy-read branch needed.
Currently: hardStop is validated then silently dropped (de-facto constant true);
resetBudgetIfDue has zero callers and holds the ONLY budget_exhausted → idle recovery,
so an exhausted agent can never recover; canAfford has zero callers;
isWarning/usagePercent are display-only.
- Make
hardStopgenuinely settable throughsetBudget. - Call
resetBudgetIfDuelazily on every budget read (not from the scheduler — that would put GitHub writes on a timer). canAffordbecomes a public advisory query. Enforcement stays at task checkout — not per-AI-call, which would mean a GitHub round trip per call on an unlocked read-modify-write store.- Crossing
warningFractionappends a warning to the tool result (the model reads tool results; it is the only actor that can slow down).
AgentReclaimScheduler:sweepBudgetMs→sweepTimeoutMs(it's milliseconds).TokenCounter:fitsInBudget/truncateToFit→ prompt-limit language.ContextQualityMetrics.tokenUsage→estimatedTokens.warningThreshold→warningFraction(budget, 0–1) andwarningScore(ConfidenceConfig, 0–100). Both renamed to encode units.- Delete
src/domain/value-objects/AgentMetrics.ts— imported by nothing, and it carries a third parallel budget-exhaustion implementation. get_agent_activitymust callgetBudgetStatusinstead of hand-rolling the arithmetic; it currently skips parent resolution, so reporting disagrees with enforcement for subagents.
CONTEXT.md— glossary only, no implementation detail. The three token provenances; agent vs subagent (a subagent has no budget of its own and cannot check out a task); the now-disambiguated senses of "budget".- Four ADRs under
docs/adr/: split counters by provenance; beads as working tracker with GitHub Issues as external reporting; MCP SDK v2 migration; Biome replacing ESLint. (The last two were judged below the skill's bar on one criterion each but were explicitly requested — lean them on the trade-off rather than the decision.)
The 8 bugs found this session, hb8.15–hb8.22. Four are P1 silent-breakage:
hb8.15forceSyncis a no-op across all four sync methods.hb8.16The issue body never reaches the AI triage model (prompt uses title only).hb8.17Caller-supplied confidence/enrichment config dropped at 3 sites.hb8.18Sprint issue assignments never written to GitHub —update()returns a fabricated object.
None are regressions from this session; all pre-date it and were found because dead-code removal exposed them.
One commit for all of the above, on this branch. No PR.
hb8.24— 421noExplicitAny+ 161noNonNullAssertion. A typing project, not lint.noExplicitAnyiswarnon purpose for MCP boundary adapters. Mechanically swappingany→unknownprovably does not compile here.hb8.12— duplicated Beads block in AGENTS.md. Fix viabd setup codex --remove; it is the user's deliberate integration, so it is their call.hb8.23— Biome cannot parse tsyringe@inject(...) private x: Tparameter properties and false-positives unused params. Upstream tool limitation.
biome lint --writeis a code change, not an inspection. ItsuseArrowFunctionautofix (classified "safe") rewrote constructiblefunctionmocks to arrows and broke ~90 tests. The rule is now OFF with rationale inbiome.json. Apply lint autofixes one rule at a time with a full test run after each.- Arrow functions are not constructible. Test mocks invoked with
newmust stayfunction. This bit twice, in two different shapes. beforeEach(() => x.mockReset())with an implicit return hands the mock back to Vitest, which treats a returned function as a teardown callback and calls it. Use a block body.ai@7 reports usage asinputTokens: {total,...}objects, not numbers. Reading them as numbers yieldsNaNand silently poisons the budget.