diff --git a/handoffs/002-logging-hardening-and-fleet-biome.md b/handoffs/002-logging-hardening-and-fleet-biome.md new file mode 100644 index 0000000..6f5c58d --- /dev/null +++ b/handoffs/002-logging-hardening-and-fleet-biome.md @@ -0,0 +1,91 @@ +# Handoff: Logging hardening (console→logger) + fleet biome standardization + +**Date**: 2026-06-04 +**Handoff Number**: 002 + +--- + +## Overview / Summary + +Migrated the **entire codebase off ad-hoc `console.*` onto the structured logger** (`lib/log.ts`, shipped in #800/#815), then **locked it in** so it can't regress, then **standardized the fleet's linter on biome 2.x** and gave the fleet a mechanism to **track one biome version together**. + +Concretely: ~500 `console.*` sites across ~115 files converted in 7 reviewable slices; a `noConsole` biome rule (biome 2.4.16) now fails CI on any new `console.*` outside a small documented allowlist; release-tools migrated ESLint→biome; and a new `biome-linter` workspace-config rule flags any JS/TS fleet repo that drifts off the pinned biome version. **14 PRs merged** (12 in protoWorkstacean, 2 in release-tools), **2 releases cut** (protoWorkstacean `v0.8.2`, release-tools `v2.2.0` — published to npm). Along the way, fixed a Quinn duplicate-approval race and an image build that had been silently red for ~13h. + +This matters because logging is now leveled + component-tagged + JSON-in-prod (filterable/queryable instead of grep), the migration is enforced not just done, and the fleet won't drift into per-repo biome versions. + +## Background / Context + +- **The app is a switchboard** (trigger→router→dispatcher→executor over an in-memory bus). Single-node by design. +- **Deploy model**: code ships as `ghcr.io/protolabsai/workstacean:main` (watchtower auto-pulls every merge to `main`); workspace config is a host bind-mount via `git pull`. Releases are a *decoupled* manual ritual (`prepare-release.yml` bumps `package.json` on main → `auto-release.yml` tags + GitHub Release → `release.yml` posts Discord notes via the `release-tools` action). +- **Shared working tree**: `/home/josh/dev/protoWorkstacean` is actively touched by background automaker agents — it currently sits on `feat/roxy-board-pulse-ceremony` (PR #817, **not ours** — don't touch). All work in this session was done in `/tmp` worktrees off `origin/main`; the main checkout was never committed to. +- This session followed **handoff 001** (the production-readiness epic #790). Its follow-up #2 was "bulk `console.*` → `lib/log.ts`" — that's what this session executed end-to-end. + +## Current State + +**protoWorkstacean — all merged to `main`, released as `v0.8.2`:** + +Logging migration (7 slices): +- [x] **#818** dispatch spine (dispatcher, index, executors, ceremony, skill-broker, agent-runtime, router) — the reference conversion +- [x] **#823** lib/plugins integration core (github, linear, scheduler, signal, a2a-delivery, feature/bridge plugins) +- [x] **#824** discord subsystem · **#825** google subsystem +- [x] **#826** src data layer (knowledge / services / telemetry / storage / stores) +- [x] **#827** remaining src/ (plugins / api / webhooks / mcp / loaders) + 2 console-spy-coupled files +- [x] **#828** the missed `lib/` cluster (identity/checkout-cache/channels/auth/dm/linear/fleet/conversation/github-issues/app-alert) **+ the `noConsoleLog` lint lock-in** + +Lock-in upgrade + incidents: +- [x] **#829** biome 1.9.4 → 2.4.16; `noConsoleLog` → `noConsole` (now catches `console.warn/error` too) +- [x] **#819** Quinn duplicate approve-on-green race fix (the approval spam) +- [x] **#822** hotfix: format-coupled tests failed only under the Docker build's `NODE_ENV=production` — image build had been red since #815 (~13h, deploy frozen) +- [x] **#820** release-tools action pin v1.1.0 → v2.1.1 · **#821** v0.8.1 bump · **#830** v0.8.2 bump + +**release-tools — all merged to `main`, released as `v2.2.0` (npm + GitHub):** +- [x] **#42** ESLint → biome 2.4.16 +- [x] **#43** `biome-linter` workspace-config rule + `FLEET_BIOME_VERSION` +- [x] **#44** `chore: release v2.2.0` + +**Releases cut this session:** ws `v0.8.1` (mid-session) and `v0.8.2` (final); release-tools `v2.2.0` (npm `@protolabsai/release-tools@2.2.0` published via OIDC). + +## Technical Approach + +- **Sliced, not big-bang.** ~500 sites across ~115 files would be unreviewable as one PR, so it went out as 7 cohesive slices (spine → integration plugins → discord → google → src data → remaining src → missed lib). Each slice fanned out to parallel subagents working disjoint files, then verified centrally. +- **Translation convention** (established on `skill-dispatcher-plugin.ts` as the worked example): the existing `[tag]` prefix becomes the logger **component** (`logger("tag")`, prefix dropped from the message); `console.log/warn/error/debug` → `log.info/warn/error/debug` with failure-path `console.log` promoted; trailing comma-separated values (errors, ids) become **structured fields** (`{ err }`); human-sentence interpolations stay inline; dynamic tag suffixes (`[discord:${label}]`) collapse to a static component + a field. +- **The load-bearing gotcha** (cost ~13h of frozen deploy before it was caught): the Docker image runs `bun test` under **`NODE_ENV=production`** as a release gate — the *only* place the suite runs in prod env. The logger emits JSON there, so tests asserting the human-readable `[component]` log line fail **only inside the image build**, invisible to PR CI. Rule adopted for every subsequent slice: **run `NODE_ENV=production bun test` before merging anything that touches logging.** Format-coupled tests were fixed to own their format env (clear/restore `NODE_ENV`/`LOG_FORMAT`); the one tag-literal assertion (`a2a-server.test`) was rewritten to assert the message text, not the tag. +- **Intentional `console.*` that stays** (the allowlist): `lib/log.ts` (the logger's own sinks), `lib/plugins/{cli,onboarding,event-viewer,echo}` (terminal UX), `lib/plugins/debug` (DebugPlugin *intercepts* console to republish on the bus), and `lib/conversation/conversation-tracer` + `src/integrations/discord/CeremonyNotifier` (no-backend `[…:fallback]` stdout delivery sinks). +- **Lock-in**: `noConsole: "error"` (no `allow`) in `biome.json` forbids every `console.*` outside the per-file allowlist + tests. Proven to fire on an injected stray call. +- **Fleet biome tracking**: `FLEET_BIOME_VERSION` in `release-tools/lib/workspace-config.mjs` is the single source of truth; the `biome-linter` rule (warn, N/A for non-node repos) flags any repo whose `@biomejs/biome` dep ≠ the pin or lacking a `biome.json`. Bump the constant → the fleet audit flags everyone behind. +- **Quinn race fix**: the deterministic approve-on-green branch `continue`d past `_handleAutoReview`'s dedup, so co-arriving CI webhooks (check_suite + workflow_run + check_run) each read `COMMENTED` before any APPROVE landed and all posted. Fixed with a synchronous in-flight key in the existing `recentDispatches` map. + +## Key Files and Documentation + +| File | Purpose | +|------|---------| +| `lib/log.ts` | the structured logger — `logger(component)`, leveled, JSON-in-prod (`useJson()` keys on `LOG_FORMAT==="json" \|\| NODE_ENV==="production"`) | +| `biome.json` | `noConsole: "error"` + the intentional-keep allowlist override (the regression gate); biome 2.4.16 | +| `src/executor/skill-dispatcher-plugin.ts` | the reference conversion (canonical translation style) | +| `~/dev/release-tools/lib/workspace-config.mjs` | `FLEET_BIOME_VERSION` + the `biome-linter` rule | +| `~/dev/release-tools/docs/reference/workspace-config-standard.md` | the rule reference (updated) | +| `.github/workflows/{prepare-release,auto-release,release}.yml` | the decoupled release ritual | + +## Acceptance Criteria + +- [x] All operational `console.*` migrated to `lib/log.ts`; only documented sinks/terminal-UX remain +- [x] CI fails on any new `console.*` outside the allowlist (`noConsole`, biome 2.x) +- [x] Full suite green under **both** dev and `NODE_ENV=production` (the build gate) — 1080 pass +- [x] release-tools on biome 2.x; `biome-linter` rule live in the fleet audit +- [x] protoWorkstacean `v0.8.2` + release-tools `v2.2.0` released +- [ ] (follow-up) `biome-linter` / `workflow-security-lint` tightened `warn` → `error` once fleet adoption is confirmed +- [ ] (follow-up) other fleet node repos brought onto biome 2.4.16 + +## Open Questions / Considerations — the new-work surface + +1. **Run `fleet-config-audit` to see who's off the biome pin.** The `biome-linter` rule is live (consumed via `github:protoLabsAI/release-tools@main`). Running the audit surfaces which node repos (protoMaker, protoCLI, etc.) are still on ESLint or an older biome — that's the to-do list for *those* repos (owned by their teams). +2. **Tighten the two `warn` rules to `error`** (`biome-linter`, `workflow-security-lint`) once the fleet has adopted — same rollout posture both used. +3. **`console.warn/error` coverage is new as of #829** — anything written against the old `noConsoleLog`-only assumption (only `console.log` blocked) is now stricter. +4. **Handoff 001's other follow-ups remain untouched** (not part of this arc): Prometheus `/metrics` scrape (blocked on your network/security call), `console.*`→logger was #2 (now done), agent-card brand genericization, and the staged PRDs in `.proto/`. + +## Next Steps + +1. **Run the fleet biome audit** and triage which repos `biome-linter` flags. +2. **Migrate the flagged node repos** to biome 2.4.16 (the `biome-linter` `fix` line + this repo's `biome.json` are the template). +3. **When adoption is broad**, flip `biome-linter` (and `workflow-security-lint`) to `error`. +4. **Pick up handoff 001's Prometheus `/metrics` decision** — the one genuinely blocked-on-you item.