Skip to content
Closed
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
91 changes: 91 additions & 0 deletions handoffs/002-logging-hardening-and-fleet-biome.md
Original file line number Diff line number Diff line change
@@ -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.
Loading