moving
Some checks failed
Installer Smoke / installer-smoke (push) Has been cancelled

This commit is contained in:
Oleg Maslov
2026-09-02 10:10:29 +02:00
commit 0c3e2ead3b
3841 changed files with 970576 additions and 0 deletions

View File

@@ -0,0 +1,80 @@
# Waggle OS — Consolidated "What's Left for GA"
**Date:** 2026-05-30 · **Baseline:** `main @ 6a526a6` (pushed, synced) · **Owner:** Marko (solo)
**Companion docs:** `PRODUCTION-PLAN.md` (5-phase roadmap) · `RECONCILIATION-2026-05-30.md` (verified residuals) · `TRUST-REPORT.md` (verified state) · `PHASE2-FAILURE-INJECTION-2026-05-30.md`
> This supersedes the open-item lists in the companion docs where they conflict. Every "DONE" below is a commit on `origin/main`; every "OPEN" names why it's blocked (platform vs. visual vs. upstream).
---
## 1. One-paragraph headline
The **security cluster is closed** (2 real production bugs found + fixed this session, both surfaced by *running* code, not auditing it), **billing correctness is closed**, **all 5 CI gates give real signal**, **deploy manifests are correctly wired per-target**, and **failure-injection E2E + a11y + light-mode tokens** landed with regression ratchets. What remains for GA is **not codeable headlessly on this Windows host**: macOS signing/notarization + binary runtime smokes (need a Mac / signed build), and the broad light-mode Tailwind-utility migration (needs a build + human visual contrast QA). The CLI is correctly packaged but its npm publish is gated on unpublished workspace deps. **GA is close on the engineering axis; the gating work is environmental, not unknown.**
---
## 2. GA gate checklist (from PRODUCTION-PLAN §1) — current truth
| Gate | Status | Evidence |
|---|---|---|
| 5/5 CI gates real (build-web, build-packages, lint, tauri-tsc, test) | ✅ **DONE** | `ci.yml`: tsc `:29`, lint `:32`, tauri-tsc `:35`, test `:37`, build `:65` |
| Security: AV-1..5 + R2-006 + R6-005 + R1-011 closed, adversarially re-verified | ✅ **DONE** | `9f76344`, `3a4f70a`, `0e4dfa7`; + D1 `d28abed`/`99347da` |
| Billing correctness (R1-002 payment gate, AV-4) | ✅ **DONE** | `e511f04` (webhook payment_status gate); AV-4 already closed `27a98f7` |
| E2E journeys + failure-injection in CI | 🟡 **PARTIAL** | network-drop 3/3 (`25cb5c0`); harness revived (`b732a5f`); broader persona journeys exist; **no full failure-injection matrix** |
| Win signed + mac notarized installers from CI; updater verifies | 🔴 **OPEN (platform)** | R7-008 — needs Mac + signing certs |
| Web deploys to staging; live-mode Stripe round-trips; prod CORS exact-match | 🟡 **PARTIAL** | env wired (`adf607b`), CORS fail-closed (`c00232c`); **live round-trip untested** (needs a real deploy + Stripe keys) |
| drizzle 0.44→0.45; light-mode contrast audit passes; a11y basics | 🟡 **PARTIAL** | drizzle ✅ (`fb33ca8`); a11y modals ✅ (`3300b1e`); light tokens ✅ AA (`dc6090c`); **broad util migration + visual audit OPEN** |
| Commits pushed; tag cut | ✅/🟡 | all pushed; `v0.1.0-ga-hardening-phase1` tagged; **final GA tag not cut** |
---
## 3. DONE this session (all on `origin/main`, 22 commits `0f6bb4c..6a526a6`)
**Security & billing (the high-value finds):**
- 🐛 **R1-002 Stripe webhook bypass** (`e511f04`) — `checkout.session.completed` granted paid tiers on *unpaid* sessions in both handlers. Now gated on `payment_status`. TDD, 30/30 stripe green.
- 🐛 **D1 SPA-asset auth regression** (`99347da`) — D1 gated the SPA shell + static assets behind a bearer token → browsers/webview got `401 MISSING_TOKEN` on load, unbootstrappable; **bricked the hosted web deploy**. Exempted non-API GETs; auth boundary intact (negative-control tests).
- **AV-1/2/3/5, R2-006, R6-005, R1-011** (`9f76344`, `3a4f70a`, `0e4dfa7`) — network/auth boundary, dev-tier-override gate, webhook serialize, rate-limit mkdir.
- **CORS fail-closed in prod** (`c00232c`) — team server throws on missing `CORS_ORIGIN` instead of silent localhost fallback.
**Verification infra (Phase 2):**
- **network-drop failure-injection E2E** (`25cb5c0`) — 3/3 green; SSE abort / mid-stream truncation / re-send recovery, LLM-mode-independent.
- **Harness-wide trust + revived dormant UI suite** (`b732a5f`) — `WAGGLE_TRUST_LOCALHOST` for API-contract specs; §14 UI suite was silently skipping on a dead `:8080` probe, repointed at `:3333`.
- **Repaired broken Playwright npm scripts** (`c3c569b`) — `--project=api/visual` referenced non-existent projects.
**Perf & a11y (P4):**
- **Harvest skip-unchanged** (`da29e77`) — R3-004; `harvestSetHash` wired into the dedup path + 3 callsites. +7 tests.
- **Shared modal focus-trap** (`3300b1e`) — R5-004; reusable `useFocusTrap` on 3 modals. +5 tests.
- **Light-mode token set** (`dc6090c`) — R5-006 tokens; 4 missing status + 5 KG tokens defined, all WCAG-AA verified numerically. +12-test contrast ratchet.
**Deploy & packaging (P3):**
- **Per-manifest env wiring** (`adf607b`) — render.yaml (sidecar) += Stripe; docker-compose (team) += CORS_ORIGIN. Corrected 2 false reconciliation claims.
- **@waggle/cli publish config** (`6a526a6`) — `files:[dist,bin]`, full metadata; pack verified 0 src/test files.
---
## 4. What genuinely remains — by blocker class
### 4a. 🔴 Platform-blocked on Windows (need a Mac and/or signed binary)
- **R7-008 — macOS signing + notarization** + updater signature verification. Developer ID + `notarytool` need macOS.
- **Windows code-signing** — *can* be wired from here once an EV cert is procured (cert procurement is the blocker, not the code).
- **Binary runtime smokes** — orphan-sidecar kill, updater URL, static/Clerk first-run, and the **D1 desktop first-run connect** (the one path still only server-level tested; revert hatch = `WAGGLE_TRUST_LOCALHOST=1`).
### 4b. 🟡 Needs a build + human visual QA (not headless-verifiable)
- **R5-006 broad light-mode migration** — **~462 hardcoded Tailwind color utilities** (`bg-red-500` etc.) + 20 hex in `apps/web/src`. The token layer is done + AA-ratcheted; these utilities need a running build and **independent visual contrast audit** (the plan's anti-self-grading doctrine). Do not blind-rewrite.
- **Live-mode Stripe round-trip** — env is wired; needs a real staging deploy + live Stripe keys to confirm checkout + webhook end-to-end.
### 4c. 🟢 Codeable headless — small remaining
- **Full failure-injection matrix** — only network-drop landed; capability-missing/traversal/unpaid-tier are unit-covered, sidecar-restart is architecturally infeasible (documented). A persona-journey E2E expansion is optional-not-blocking.
- **@waggle/cli end-to-end publish** — packaging done; **gated upstream**: `@waggle/core/agent/weaver` are pinned `"*"` and unpublished (404). Publish those (or bundle) to unblock.
- **§7.5 CI path-drift** — deprecated sync workflows still hard-code old `packages/core/src/mind` paths (inert, but flagged in RECONCILIATION).
---
## 5. Recommended path to GA cut
1. **On a Mac (or mac CI runner):** wire signing/notarization (R7-008), produce signed installers, run the binary runtime smokes incl. D1 desktop first-run. ← the critical-path blocker.
2. **One staging deploy:** apply `render.yaml` with real Stripe keys → confirm a live checkout + webhook round-trip; apply `docker-compose.production` with `CORS_ORIGIN` → confirm team server boots + exact-match CORS.
3. **Light-mode visual pass:** build, open light mode, audit the 462-util surface with a contrast tool; migrate failures to tokens. Independent QA, not self-graded.
4. **Then:** full verification re-run, cut the GA tag, wire Enterprise CTA → kvark.ai.
**Bottom line:** the headless-codeable GA backlog is essentially cleared. Remaining work is gated on a **macOS/binary environment**, a **real staging deploy**, and **human visual QA** — none of which are unknowns, all of which are outside this Windows session's reach.

207
docs/ga/OPERATING-MANUAL.md Normal file
View File

@@ -0,0 +1,207 @@
# Waggle OS → GA: Solo-Dev Operating Manual
**For:** Marko (solo on waggle-os + hive-mind; team reserved for KVARK)
**Date:** 2026-05-29
**Companion docs:** `TRUST-REPORT.md` (empirical state), `PRODUCTION-PLAN.md` (the roadmap)
> This manual is the *how you work* layer. It turns "I'm one person facing 62 findings + GA"
> into "I'm one person orchestrating a fleet that does the 62 findings + GA." The plan tells you
> **what** to ship; this tells you **how to make Claude Code do most of it** safely.
---
## 0. The mental model shift
You are not pair-programming. At GA scale, as a solo founder, **pair-programming is the wrong unit** — it makes you the bottleneck on every line. The unit that matches your situation is **fleet orchestration**: you author a deterministic harness, dozens of agents execute and *check each other*, and you read conclusions, not diffs.
Three primitives do this. You already used all three implicitly this session — the goal is to make them deliberate:
| Primitive | What it is | Your use for GA |
|---|---|---|
| **Workflow** | A JS script that fans out subagents deterministically (loops, pipelines, parallel, adversarial panels). Runs in background, returns structured data. | The engine for every multi-item job: verify N findings, fix N findings, generate N tests, sweep N components. |
| **ultracode** | A *standing opt-in*: when on, author + run a workflow for every substantive task **by default**, and adversarially verify by default. Token cost is not a constraint. | Turn ON for the thorough GA push (you chose 12 months / thorough). Turn OFF for chat + trivial edits. |
| **Opus 4.8 (1M ctx)** | Deepest-reasoning model; 1M context holds whole subsystems at once. `/fast` = same model, faster output. | The *reasoning* tier: architecture, security verdicts, synthesis, "is this fix bypassable?". Route mechanical work down. |
**The doctrine that ties them together — verification-first.** Your audits keep self-grading 10/10 while admitting they never tested failure paths (UI 10/10 with "error recovery not exercised"; ~24 findings "unverified"). A fix written *and graded by the same agent* is the single biggest source of false confidence in this project. **Every claim gets independently, adversarially verified by a different agent before it counts as done.** That is exactly the read-only workflow running right now against your 31 commits.
---
## 1. Model routing (don't pay Opus rates for mechanical work)
In a workflow, `agent()` inherits the session model by default — usually correct. Override with `opts.model` **only** when confident a tier fits:
```
Opus 4.8 → architecture decisions · security verdicts · adversarial verify ·
synthesis · "should we ship?" · anything where being wrong is expensive
Sonnet 4.6 → the default fix tier · most route/component/test edits · code review
Haiku 4.5 → mechanical bulk · rename sweeps · import fixes · "does file X contain Y?" ·
the 324-hardcoded-color token swap (pattern-substitution, not judgment)
```
In practice: keep the workflow on the inherited model, and push *down* to Haiku for the bulk-mechanical clusters via `opts.model: 'haiku'`. Reserve explicit `opts.model: 'opus'` for verify/synthesis stages if you're running the session on Sonnet.
`/fast` toggles faster Opus output for interactive work — use it when you're steering live; it does not downgrade the model.
---
## 2. The four ready-to-run workflows for the remaining GA work
These are copy-pasteable starting points. They assume `REPO = 'D:/Projects/waggle-os'`. The first one already ran this session — it's your template for the rest.
### 2.1 Independent verification (READ-ONLY) — *the template*
**When:** after any fixing session, before you believe it. Re-run after the concurrent session pushes.
**Why it's safe alongside another session:** `code-reviewer` agents (no write tools), no build/test/install, writes nothing.
**Shape:** one verifier per finding-cluster → adversarial skeptics attack every "fixed" security/billing verdict.
→ See the live script at `…/workflows/scripts/waggle-prod-verification-*.js`. Reuse via `{scriptPath}`.
### 2.2 Fix-execution (WORKTREE-ISOLATED) — *the only safe way to mutate while a session is live*
**When:** to close residual/regressed findings the Trust Report surfaces.
**Key safety:** `isolation: 'worktree'` gives each fixing agent its own git worktree, so parallel fixes never collide — with each other *or* with your other session's tree.
```js
export const meta = {
name: 'waggle-fix-residuals',
description: 'Close residual findings on isolated worktrees; each fix self-verifies before returning',
phases: [{ title: 'Fix' }, { title: 'Verify' }],
}
const REPO = 'D:/Projects/waggle-os'
// residuals = the findings TRUST-REPORT.md marks not_fixed / partial / regressed
const residuals = args?.residuals || [] // pass via Workflow({args:{residuals:[...]}})
const FIX = { type:'object', additionalProperties:false, properties:{
id:{type:'string'}, changed:{type:'array',items:{type:'string'}},
summary:{type:'string'}, selfTest:{type:'string'} }, required:['id','summary'] }
const VERDICT = { type:'object', additionalProperties:false, properties:{
id:{type:'string'}, verdict:{type:'string',enum:['fixed','partial','not_fixed']},
evidence:{type:'string'} }, required:['id','verdict','evidence'] }
const results = await pipeline(residuals,
// stage 1: fix on an isolated worktree (Sonnet is fine; push security to Opus)
(f) => agent(
`On ${REPO}: implement the prescribed fix for ${f.id} ("${f.prescribed}"). `+
`Make the SMALLEST correct change. Add/extend a test that fails before and passes after. `+
`Run only the narrowest relevant vitest file. Return what you changed.`,
{ label:'fix:'+f.id, phase:'Fix', schema:FIX, isolation:'worktree',
model: /R1-|R2-|R6-|R9-/.test(f.id) ? 'opus' : undefined }),
// stage 2: a DIFFERENT agent verifies the fix (no worktree; read the diff)
(fix, f) => agent(
`Independently verify fix for ${f.id}. Read the changed files; confirm the prescribed `+
`behavior holds and find any bypass. Be adversarial.`,
{ label:'verify:'+f.id, phase:'Verify', schema:VERDICT, agentType:'code-reviewer' })
.then(v => ({ ...v, fix })))
return results.filter(Boolean)
```
> You review the returned diffs, then cherry-pick/merge the worktree branches yourself. Agents propose; you commit.
### 2.3 E2E + synthetic-failure generation — *kills the 10/10s structurally*
**When:** to convert the audit's biggest admitted gap ("zero browser E2E; error-recovery untested") into real coverage.
**Shape:** discover the critical user journeys + the untested failure paths → generate a Playwright spec per journey and a failure-injection test per path.
```js
export const meta = { name:'waggle-e2e-synth', description:'Generate Playwright journeys + failure-injection tests for untested paths', phases:[{title:'Discover'},{title:'Generate'}] }
const REPO='D:/Projects/waggle-os'
const SURFACES = { type:'object', additionalProperties:false, properties:{
journeys:{type:'array',items:{type:'object',additionalProperties:false,
properties:{name:{type:'string'},steps:{type:'string'},failureModes:{type:'string'}},
required:['name','steps']}} }, required:['journeys'] }
phase('Discover')
const map = await agent(
`Read apps/web routing + the 5 personas in docs/ui-ux-audit-2026-05-27/PERSONAS.md. `+
`List the critical user journeys and, for each, the failure modes NOT covered by tests `+
`(network drop mid-stream, capability-missing hard error, traversal-rejected, unpaid-tier gate).`,
{ schema:SURFACES, agentType:'code-reviewer' })
phase('Generate')
await parallel(map.journeys.map(j => () =>
agent(`Write a Playwright spec for journey "${j.name}" (steps: ${j.steps}) AND a `+
`failure-injection test for: ${j.failureModes}. Match the existing playwright.config.ts `+
`project layout. Place under tests/e2e/. Return the file content only — DO NOT run it.`,
{ label:'spec:'+j.name, isolation:'worktree', schema:{type:'object',additionalProperties:false,
properties:{file:{type:'string'},content:{type:'string'}},required:['file','content']} })))
```
> Generation on worktrees → you review specs → land them → run `npm run test:all` yourself once your session owns the tree.
### 2.4 Release-readiness sweep (READ-ONLY) — *the GA launch-line*
**When:** before the GA cut. Verifies the things the audit *didn't* cover because they're not code-findings.
```js
export const meta = { name:'waggle-release-readiness', description:'Read-only check of the GA launch-line: gates, signing, updater, macOS, npx, deploy', phases:[{title:'Check'}] }
const REPO='D:/Projects/waggle-os'
const checks = [
['gates','Do all 5 gates give real signal now? root eslint.config present? app/tsconfig points at real TS? build runs tsc? CI targets correct branch?'],
['signing-win','Windows code-signing: is there a signing identity/cert wired in tauri.conf + release.yml? Or unsigned (SmartScreen warning)?'],
['signing-mac','macOS: Developer ID + notarization configured, or still ad-hoc (Gatekeeper block)? Is the DMG target defined?'],
['updater','tauri updater: correct repo slug everywhere, pubkey set (not empty), latest.json reachable?'],
['npx','Is the CLI npx-publishable (bin points at built .js, workspace deps resolve)?'],
['web-deploy','apps/www + render.yaml/docker-compose.production: is the WEB SaaS deploy path complete (env, Stripe live keys, Clerk prod, CORS for prod origin)?'],
]
phase('Check')
const out = await parallel(checks.map(([k,q]) => () =>
agent(`READ-ONLY on ${REPO}. ${q} Report status + exact gap + the file to change.`,
{ label:k, schema:{type:'object',additionalProperties:false, properties:{
area:{type:'string'},status:{type:'string',enum:['ready','partial','missing']},
gap:{type:'string'},fileToChange:{type:'string'}},required:['area','status','gap']},
agentType:'code-reviewer' })))
return out.filter(Boolean)
```
---
## 3. ultracode: when to flip it on
ultracode = a standing instruction that says *"author and run a workflow for every substantive task by default; verify adversarially; token cost is not the constraint; quality is."*
- **ON** for the GA push you chose (thorough, 12 months, all four dimensions). It makes the fleet-orchestration default instead of something you have to ask for each time.
- **OFF** for conversational turns, trivial mechanical edits, and when you're exploring/steering and want fast single-threaded answers.
A reminder tells the agent which state it's in. Practically: you flip it on when you sit down to *grind a workstream*, off when you're *thinking with* the agent.
---
## 4. The solo cadence (your day)
```
Morning ── strategic-compact, read overnight workflow results (TRUST-REPORT deltas)
── pick ONE workstream (Verification / Security / Release / UX) for the day
Midday ── author/run the fix-execution workflow for that workstream's residuals (worktrees)
── you review returned diffs, land the good ones, re-run verification on what you landed
Evening ── run a read-only verification or release-readiness sweep on the day's work
── /loop a CI/push monitor if you pushed; note tomorrow's residuals
```
Tools that make this sustainable solo:
- **Worktree-per-stream** (you already do this — `waggle-os-gaia2-wt` exists). One worktree per concurrent session so nothing collides. *This is non-negotiable while two sessions are live.*
- **`/loop`** for recurring waits (poll a CI run, watch a deploy) — self-paced, interrupts you only when state changes.
- **`strategic-compact`** between phases so context stays sharp across a multi-week push.
- **Background workflows** — launch, keep steering elsewhere, get notified on completion. You ran one this session and kept talking; that's the pattern.
---
## 5. The two-session sync protocol (active right now)
Two Claude Code sessions are on `waggle-os` simultaneously. Collision rules:
| Rule | Why |
|---|---|
| **Only ONE session mutates the shared working tree** (`D:/Projects/waggle-os`). | The other (this one) is the fixing lane; mine is read-only verify+plan. Two writers on one tree corrupt each other. |
| **Any *additional* mutation goes on a NEW worktree** (`git worktree add`). | Lets a second session execute fixes without touching the live tree. Your own memory rule. |
| **Verification is always read-only** + no build/test/install on the shared tree. | A verifier that runs `npm test` races the fixer's test artifacts. Static read = zero collision. |
| **One designated session pushes** the 31 unpushed commits. | Avoid divergent pushes on `hardening/prod-readiness`. |
| **Deliverable docs live OUTSIDE the repo** until a session owns the tree. | This plan is in `North star/waggle-ga/`; fold into `waggle-os/docs/` during a quiet window. |
**Recommended handoff:** let the fixing session finish + push → this session's Trust Report identifies residuals → a single integration session works residuals on a worktree (workflow 2.2) → merge → re-verify → GA cut.
---
## 6. Guardrails
- **Budget directives:** prefix a turn with `+500k` (etc.) to set a hard token target; workflows scale fan-out to it and stop at the ceiling. Use for "go as deep as N tokens buys."
- **Read-only by default; worktree to write.** Never let an agent Edit a tree another session owns.
- **Agents propose, you dispose.** For anything security/billing/release, you read the diff and commit. The fleet does volume + verification; you keep the final commit bit.
- **Don't trust a green run that doesn't gate.** Until lint + tauri-tsc give real signal, "tests pass" is two-fifths blind. Gate repair is workstream-0.
---
*Next: `TRUST-REPORT.md` lands when the verification workflow completes, then `PRODUCTION-PLAN.md` sequences the 12 month GA push across all four workstreams for both web + desktop.*

View File

@@ -0,0 +1,161 @@
# Phase 2 — Failure-Injection Coverage & CI Wiring
**Date:** 2026-05-30
**Repo:** `D:/Projects/waggle-os``main` @ `c00232c`
**Scope:** Failure-injection / fault-tolerance coverage for the GA gate, plus the CI wiring fix to ensure the new specs run.
---
## 1. Headline
**What now exists:** One net-new browser-driven failure-injection spec was authored and compiles (validated via `playwright test --list`, 3 tests discovered under the `chromium` project, server not booted):
- `tests/e2e/failure-injection/network-drop.spec.ts` — 3 UI-driven tests covering the **network-drop** path on `POST /api/chat` (mid-stream SSE interruption, partial-token render, and re-send recovery). Injection uses `page.route()` exactly like the existing `spawn-agent-flow.spec.ts`; assertions are verified against `ChatApp.tsx` / `BlockRenderer.tsx` / `useChat.ts` in this repo state.
**What is deliberately deferred (with reasons):**
- **capability-missing** — already covered at unit level (`packages/agent/tests/capability-router.test.ts`, `agent-loop.test.ts`). It is an agent-internal decision over deterministic LLM text, not a browser-triggerable DOM/UI state. An e2e here would only re-exercise client SSE plumbing, not the capability logic. **No e2e added.**
- **traversal-rejected** — already covered by 14 Vitest tests across `phase2-traversal-{tasks,documents,ingest}.test.ts` (R1-004 included) plus generic e2e traversal checks in `competitive-benchmarks.spec.ts`/`waggle-complete.spec.ts`. The task/document/ingest endpoints specifically are covered at the unit layer where out-of-root write absence can be asserted directly. **No e2e added** (a thin e2e is feasible but redundant; flagged below as an optional nicety, not a gate blocker).
- **unpaid-tier-gate** — already covered by `tier-enforcement-matrix.test.ts` (13 gated routes) + e2e `competitive-benchmarks.spec.ts` B5.4 + `polish-verification.spec.ts`. An additional e2e is *feasible* (mutate `config.json` mid-test, assert `403 TIER_INSUFFICIENT`) but not required for the gate since the matrix test already proves enforcement file-backed (no header spoof). **Optional, not added in this pass.**
- **sidecar-restart** — **INFEASIBLE at e2e level.** Playwright `webServer` with `reuseExistingServer:true` boots the single `:3333` process once and never restarts it; `page.route()` stubs HTTP traffic, it cannot cleanly kill/respawn the Node host without race conditions against the 120s health probe. The frontend adapter's `connectWebSocket()` is fire-once with no reconnect/state-recovery loop, so there is nothing to assert post-restart anyway. **Recommend Vitest unit coverage** of the offline-detection + (absent) reconnect path instead.
- **R3-002 (circular-dependency / subagent dup-worker)** — already green via `packages/agent/tests/phase4-subagent-dup-worker.test.ts` (2 tests). **Not re-added** per instruction.
**Net Phase-2 result:** the one genuinely browser-observable, not-yet-covered failure path (network-drop) is now an e2e. Everything else is either already covered (unit and/or e2e) or infeasible at the browser layer and correctly pushed to unit.
---
## 1b. EXECUTION OUTCOME (2026-05-30, post-authoring) — pushed to main @ 25cb5c0
Authoring the network-drop spec and **actually running it** surfaced a real production regression. Three commits landed + pushed:
- **`c3c569b`** `fix(test)` — repaired broken Playwright npm scripts (`test:e2e`/`test:fast` referenced `--project=api`, `test:visual` `--project=visual`; only `chromium` exists → "Project not found", zero tests for any human invoker). Routed through `playwright-e2e.config.ts` + `--project=chromium`.
- **🐛 `99347da`** `fix(server)`**D1 REGRESSION FIX (the session's most important find).** D1 (`d28abed`) gated EVERY route behind a bearer token, including the SPA shell (`GET /`) and static assets. A browser/webview gets `401 MISSING_TOKEN` on initial load and can never run the app code that bootstraps the token → blank 401 page. **Bricks the hosted web deployment** (render.yaml serves the frontend from the server); desktop Tauri less affected (bundles its own UI, only calls `/api/*`). Fix: exempt non-API GETs (inert static reads) from bearer auth; every `/api/*` route + any non-GET stays gated; session-token endpoint stays same-origin gated → auth boundary unchanged. TDD: +4 tests incl. negative controls (POST `/` still 401, `/api/*` GET still 401). 8/8 D1 + 60/60 security-suite green. **This is effectively the D1 desktop-smoke-test the handoff flagged as never run.**
- **`25cb5c0`** `test(e2e)` — the network-drop spec, **3/3 green** against a live `:3333` boot. Recovery assertion made LLM-mode-independent (composer settles + new assistant bubble + no new offline error) after discovering a live LiteLLM proxy preempts `WAGGLE_ECHO_MODE`.
**Triage chain (3 rounds, each a real cause, not a flake):** missing chromium binary (infra) → stale boot/nav helpers copied from `live-chat-flow.spec.ts` (fixed to mirror `waggle-complete.spec.ts`) → **D1 static-asset auth wall** (the production bug) → echo-mode assumption vs live LLM (assertion de-coupled).
**KNOWN RESIDUAL — existing e2e UI suite is effectively dormant.** `waggle-complete.spec.ts` §14 self-skips UI tests when port **8080** returns non-HTML (`:889-898`) — but the webServer is `:3333`, so those UI journeys have been skipping, not running. Separate from this pass; flagged for a follow-up (repoint the skip-guard at `:3333`).
---
## 2. Per-Path Table
| Path | Already covered? | Feasible at e2e? | Action taken | File |
|------|------------------|------------------|--------------|------|
| **network-drop** | No | Yes | **Authored 3 e2e tests** (abort/partial-token/recovery) | `tests/e2e/failure-injection/network-drop.spec.ts` |
| **capability-missing** | Yes (unit) | No | None — keep unit coverage | `packages/agent/tests/capability-router.test.ts`, `agent-loop.test.ts` |
| **traversal-rejected** | Yes (14 unit + generic e2e) | Yes (redundant) | None — covered at unit layer | `packages/server/tests/local/phase2-traversal-{tasks,documents,ingest}.test.ts` |
| **unpaid-tier-gate** | Yes (matrix unit + B5.4 e2e) | Yes (optional) | None this pass — flagged optional | `packages/server/tests/tier-enforcement-matrix.test.ts`, `tests/e2e/competitive-benchmarks.spec.ts` |
| **sidecar-restart** | No | **No (infeasible)** | Deferred to unit; documented residual | _N/A — recommend Vitest_ |
| **R3-002 circular-dep** | Yes (2 unit, green) | n/a | None (do-not-readd) | `packages/agent/tests/phase4-subagent-dup-worker.test.ts` |
---
## 3. CI Wiring Fix
### 3a. Does the new spec already run in CI?
**Yes — recursively.** ci.yml line 71 runs `npx playwright test tests/e2e/`, which is a **path-prefix filter**, not a flat glob. The default `playwright.config.ts` has `testDir: './tests'` + `testMatch: '**/*.spec.ts'`, so any `*.spec.ts` under `tests/e2e/**` — including the new `tests/e2e/failure-injection/network-drop.spec.ts` — is automatically discovered. **No change to line 71 is strictly required for the new spec to run.**
However, two real defects warrant a minimal, intent-revealing fix:
1. **Implicit-discovery fragility.** Relying on the bare path prefix means a future reader cannot tell the failure-injection suite is in scope. Make it explicit so the gate is self-documenting and a deleted/renamed dir fails loudly.
2. **Dead-script project mismatch (the flagged issue).** Root `package.json` scripts reference Playwright projects that **do not exist** in either config:
- `test:e2e``--project=api`**broken** (only `chromium` exists → `Error: Project(s) "api" not found`)
- `test:fast``--project=api`**broken** (same)
- `test:visual``--project=visual`**broken** (only `chromium` exists)
Both `playwright.config.ts` **and** `playwright-e2e.config.ts` define **only** `name: 'chromium'`. CI never calls these scripts (it invokes `npx playwright test` directly), so CI is green — but any human running `npm run test:e2e` gets a zero-test error. This is a latent footgun, not a CI failure.
### 3b. EXACT diff — ci.yml (make scope explicit; behavior-equivalent, lists the new suite)
```diff
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -70,7 +70,7 @@
- name: Run Playwright E2E tests
- run: npx playwright test tests/e2e/
+ run: npx playwright test tests/e2e/ tests/e2e/failure-injection/
env:
WAGGLE_ECHO_MODE: "1"
NODE_ENV: test
```
> Note: `tests/e2e/failure-injection/` is already a subpath of `tests/e2e/`, so this is **functionally identical** — its only purpose is to make the failure-injection suite an explicit, reviewable line item in the gate. If you prefer zero redundancy, leave line 71 unchanged; the spec runs either way. The **load-bearing** fix is the package.json scripts below.
### 3c. EXACT diff — package.json (resolve the project mismatch)
The scripts must reference the real project name (`chromium`) or drop `--project` entirely. `test:e2e`/`test:fast` should additionally point at the e2e config (`-c playwright-e2e.config.ts`) so they exercise the e2e suite, not the visual baselines.
```diff
--- a/package.json
+++ b/package.json
@@ -29,9 +29,9 @@
- "test:e2e": "node node_modules/playwright/cli.js test --project=api --reporter=list",
- "test:visual": "node node_modules/playwright/cli.js test --project=visual --reporter=list",
+ "test:e2e": "node node_modules/playwright/cli.js test tests/e2e --project=chromium --reporter=list",
+ "test:visual": "node node_modules/playwright/cli.js test tests/visual --project=chromium --reporter=list",
"test:all": "node node_modules/playwright/cli.js test --reporter=list",
"test:retry": "node node_modules/playwright/cli.js test --last-failed --reporter=list",
- "test:fast": "node node_modules/playwright/cli.js test --project=api --grep-invert=\"4\\.9|B8\\.5\" --reporter=list"
+ "test:fast": "node node_modules/playwright/cli.js test tests/e2e --project=chromium --grep-invert=\"4\\.9|B8\\.5\" --reporter=list"
```
Rationale:
- `--project=api` / `--project=visual` matched no project in `playwright.config.ts` or `playwright-e2e.config.ts` (both only define `chromium`) → swapped to `--project=chromium`.
- Added explicit `tests/e2e` / `tests/visual` path filters so the previously project-encoded intent (e2e vs visual split) is preserved via testDir path instead of a nonexistent project name.
- `test:all` (no `--project`) was already correct and is unchanged — it runs the full default config.
---
## 4. Serial Verification Runbook (one-boot constraint)
Playwright auto-boots a **single** `:3333` server (`reuseExistingServer:true`, `fullyParallel:false`). Run everything against that one boot. Do **not** start a second server.
**Step 0 — clean port (optional, only if a stale server is bound):**
```powershell
Get-NetTCPConnection -LocalPort 3333 -State Listen -ErrorAction SilentlyContinue |
ForEach-Object { Stop-Process -Id $_.OwningProcess -Force }
```
**Step 1 — compile-only sanity (no server boot):**
```powershell
cd D:/Projects/waggle-os
node node_modules/playwright/cli.js test tests/e2e/failure-injection/network-drop.spec.ts --list
```
Green = exactly **3** tests discovered under project `chromium`. (This is the state already validated by the spec author.)
**Step 2 — run the new spec against a real :3333 (single boot, echo mode):**
```powershell
$env:WAGGLE_ECHO_MODE = "1"; $env:NODE_ENV = "test"
node node_modules/playwright/cli.js test tests/e2e/failure-injection/network-drop.spec.ts --project=chromium --reporter=list
```
Playwright runs `npm run build && npx tsx packages/server/src/local/start.ts --skip-litellm`, waits for `:3333`, then drives the UI. This single boot is reused for all 3 tests.
**Step 3 — full e2e gate (same one boot, mirrors CI line 71):**
```powershell
$env:WAGGLE_ECHO_MODE = "1"; $env:NODE_ENV = "test"
node node_modules/playwright/cli.js test tests/e2e/ --project=chromium --reporter=list
```
**What green looks like:**
- Step 1: `Total: 3 tests in 1 file`, exit 0.
- Step 2: `3 passed`, exit 0. Specifically — Test 1 renders the `.text-destructive` "Backend is offline" block and the composer re-enables (no hang); Test 2 renders the partial `MIDSTREAM_TOKEN_PROBE` token then settles with no crash; Test 3 shows the offline error on the aborted send, then after `page.unroute` the re-send streams the echo-mode "local mode" success token.
- Step 3: all 16 existing specs + the 3 new tests pass; `playwright-report/` written.
**How to triage a failure:**
1. **Boot/timeout (120s webServer):** check `npm run build` succeeds standalone and `:3333` is free. A failed Vite build or stale port is the #1 cause — re-run Step 0.
2. **Test 3 asserts "local mode" but fails:** confirm `WAGGLE_ECHO_MODE=1` is set **and** no live Anthropic proxy key is present in the env/config. With a real key the agent loop runs instead of the echo branch (`chat.ts:697-713`) and the deterministic "local mode" token won't appear — this is an honest failure, not a flake; clear the key.
3. **Selector drift (`.text-destructive`, `textarea[placeholder*="Message"]`, `button[aria-label="Chat"]`):** re-verify against current `ChatApp.tsx` / `BlockRenderer.tsx` — UI refactors invalidate selectors.
4. **Flake on retry:** `retries:1` is set; a pass-on-retry indicates a timing race in route registration. Inspect the trace (`trace: 'on-first-retry'`) in `playwright-report/`.
5. **Auth:** none needed — the app self-bootstraps its D1 bearer token via `/api/auth/session-token` on connect (`adapter.ts:126-136`), same path the existing `live-chat-flow.spec.ts` relies on.
---
## 5. Honest Residuals
- **sidecar-restart — INFEASIBLE at e2e, left to unit.** The single-boot `reuseExistingServer` model cannot kill/respawn the host process without racing the 120s health probe, and the frontend has no reconnect-with-state-recovery to observe (`connectWebSocket()` is fire-once; `useOfflineStatus` only toggles a pill). Recommended follow-up: a Vitest test in `packages/server/tests/local/` asserting `/health` failure detection and documenting the *absence* of client WS reconnect as a known limitation (or implementing reconnect + a unit test for it). **This is the one true coverage gap and it is architectural, not a test-authoring gap.**
- **capability-missing — unit-only by nature.** Browser cannot trigger a tool-not-found state; the fallback is deterministic LLM text. Existing Vitest coverage is the correct layer. No residual risk.
- **traversal (tasks/documents/ingest) e2e — optional nicety.** Unit layer already proves 400 + no out-of-root write. An e2e would be belt-and-suspenders only; deferred without risk.
- **unpaid-tier-gate e2e — optional.** Matrix unit test + B5.4 e2e already prove file-backed enforcement (no header spoof). A dedicated `unpaid-tier-gate.spec.ts` is feasible if the gate wants browser-level evidence, but adds no enforcement guarantee beyond the matrix test.
- **CI script mismatch — fixed by the package.json diff above, not yet committed.** Until applied, `npm run test:e2e`, `test:fast`, and `test:visual` error with "Project not found" for any human invoker. CI itself is unaffected (it bypasses these scripts).

141
docs/ga/PRODUCTION-PLAN.md Normal file
View File

@@ -0,0 +1,141 @@
# Waggle OS → GA Production Plan
**Date:** 2026-05-29 · **Owner:** Marko (solo) · **Surfaces:** web SaaS **+** signed desktop (Win+mac) · **Horizon:** 68 weeks (thorough)
**Companions:** `TRUST-REPORT.md` (verified state) · `OPERATING-MANUAL.md` (how to run it as a fleet)
> This plan starts from *verified* reality, not the audit's claims or the commit log. Every phase closes specific findings (IDs trace to `TRUST-REPORT.md`), names the workflow that does the work, and states an **exit gate** that is *independently re-verified* — never self-graded.
---
## 0. Post-merge status — 2026-05-29 (Phase 0 + Phase 1 LANDED on `main`)
`ga/phase0-gates` (6 commits) was rebased onto `main` after the other session landed `hardening/phase7-closeout` (main `0f6bb4c → ee923d5`: Phase7ae). Fast-forward merged to **`main @ 12c60e8`**. Re-verified on the rebased tree before merge: `tsc --build` (all pkgs) exit 0 · 65 security/D1/billing tests green · `eslint .` 0 errors / 970 warnings (ratchet backlog).
**Landed on main:**
- `9f76344` AV-1/AV-2/AV-5 — network/auth boundary residuals closed
- `3a4f70a` AV-3/R2-006 — dev tier-override gated (`WAGGLE_ALLOW_TIER_OVERRIDE=1` to enable in dev/test), debug-log recon trimmed
- `0e4dfa7` R1-011/R6-005 — Stripe webhook serialized, rate-limit mkdir
- `d28abed` **D1** — bearer token required even on loopback; same-origin `/api/auth/session-token` bootstrap; webview `connect()` fetches it
- `12c60e8` D1 test reconciliation (test env defaults trust ON; D1 tests opt into secure path)
- `811ec78` Phase0 gates — lint + tauri-tsc wired into `ci.yml` (eslint.config.js deferred to main's Phase7c config to avoid a competing gate)
**⚠️ D1 SHIPPED WITHOUT A DESKTOP SMOKE-TEST** (couldn't launch Tauri headlessly). The webview `connect()` bootstrap (HTTP `/api/auth/session-token``authToken``/ws ?token=`) is tested at the server/unit level only.
> **REVERT SWITCH if the desktop app bricks on first-run connect:** set env `WAGGLE_TRUST_LOCALHOST=1` — restores the loopback-trust exemption (reverts D1 behaviorally without reverting the commit). Verify the live desktop build at the next opportunity, then remove the switch.
**Not yet done:** push to `origin` (handoff state was local-only) · cut GA tag · the remaining Phase 25 workstreams below.
---
## 1. Definition of GA (the bar you chose)
GA = **both surfaces shippable to a paying stranger**, not just "tests pass."
**Desktop (Tauri, Win+mac):** signed + notarized installers; auto-updater verifies signatures; no orphaned sidecar; first-run works offline-to-online; local-trust threat model closed.
**Web SaaS:** deployable (render/docker) with live-mode Stripe, Clerk prod, prod-origin CORS, multi-tenant isolation; the `apps/www` landing live.
**Both:** all 5 quality gates give real signal; security cluster closed under adversarial re-verification; real E2E + failure-injection suite green in CI; light mode actually usable.
**Strategic:** the Enterprise CTA → KVARK path is wired (Waggle's job is to qualify KVARK demand — a credible GA *is* the demand engine).
**GA gate checklist** (Phase 5 verifies all):
- [ ] 5/5 gates real (build-web ✅, build-packages ✅, **lint**, **tauri-tsc**, test-no-flake)
- [ ] Security: AV-1..5 + R2-006 + R6-005 + R1-011 closed; verification workflow re-run → all skeptics "holds"
- [ ] E2E journeys (5 personas) + failure-injection suite green in CI
- [ ] Win signed + mac notarized installers from CI; updater verifies a real signed release
- [ ] Web deploys to staging; live-mode Stripe checkout + webhook round-trips; prod CORS exact-match
- [ ] drizzle 0.44→0.45 migrated; light-mode contrast audit passes (independently); a11y basics
- [ ] 31+ commits pushed; tag cut
---
## 2. Two decisions to make first (they gate everything)
**D1 — The localhost-trust threat model (strategic).** Today `security-middleware.ts` exempts *all* loopback callers from bearer auth. AV-3 (PATCH /api/tier free upgrade) proves any local web page/extension/app can drive the authenticated API. For a desktop app coexisting with browsers, "local = trusted" is unsafe.
**Decide:** require the bearer token (or a per-origin capability token) even on loopback, with the Tauri webview holding the only token. This reframes WS-A from "patch sibling routes" to "remove the trust exemption." **Recommended.** Defer only if you accept that any local software can act as the user.
**D2 — Gate repair is Workstream-0, not optional.** Until `lint` + `tauri-tsc` give signal, you're hardening blind on two axes. Do it before any other fix lands so every subsequent phase gets real verification.
---
## 3. Workstreams (mapped to verified residuals)
| WS | Theme | Closes (verified open items) |
|---|---|---|
| **0** | Gate repair + signal | lint-gate, tauri-tsc, CI branch target |
| **A** | Security to GA | D1 decision, AV-1, AV-2, AV-3, AV-5, R2-006, R6-005, R1-011 (mutex), R6-007 (wire Zod) |
| **B** | Real verification | E2E journeys, synthetic-failure injection, sibling-path sweep, R3-002 test, re-verify loop |
| **C** | Build / Release / Distribution | Win+mac signing, updater signatures, macOS notarize, npx publishable, web deploy (Stripe live, Clerk prod, prod CORS), R9-001 drizzle migration, AV-4 |
| **D** | UX to GA | R5-006 (light-mode systemic), R5-002/003, R5-004/005 (a11y), R3-004 (hot-path perf), addictiveness-audit feature triage, first-run |
---
## 4. Phased sequence (solo, ~68 weeks)
Each phase: **Goal · Closes · Workflow (from OPERATING-MANUAL) · Exit gate.** Fixes run on **isolated worktrees**; you keep the commit bit; every phase ends with an **independent re-verify**.
### Phase 0 — Make signal real (Days 13) · WS-0
- **Goal:** all 5 gates give true signal; full launch-line gap list captured; D1 decided.
- **Do:** add root `eslint.config.js` (flat) covering `packages/*`, `app/*`, `apps/*` (or scope `lint` script explicitly + lint each workspace); repoint or retire the `tauri-tsc` gate (point `app/tsconfig.json` at real `app/scripts`+`app/tests`, or delete the dead gate); fix CI branch target if still `master`. Record D1.
- **Workflow:** `2.4 release-readiness` (read-only) → the authoritative gap list for WS-C.
- **Exit:** `npm run lint` lints >0 files and is green or has a triaged error list; `tsc` for the Tauri layer compiles real files; CI runs all 5 gates on the branch.
### Phase 1 — Close security to GA (Weeks 12) · WS-A
- **Goal:** no unauthenticated/over-trusted path to authenticated APIs, billing, or fs.
- **Closes:** D1 (remove/replace loopback-auth exemption) → which structurally kills AV-3; then AV-1 (block empty Host), AV-2 (route waggle-signals SSE + agent-run through `corsOriginAllowed`), AV-5 (recognize `localhost`/`::1`/`::ffff:` as loopback), R2-006 (limit debug-logs columns+rows), R6-005 (confine mkdir + tight rate-limit), R1-011 (promise-queue mutex on webhook writes), R6-007 (wire `@waggle/shared` Zod into the 4 local fs routes).
- **Workflow:** `2.2 fix-execution` (worktrees; `model:'opus'` for the auth/threat-model changes) → then **re-run `2.1 verification`** with the adversarial skeptics.
- **Exit:** verification re-run shows security + billing clusters all `holds`; new tests cover empty-Host, origin-reflection, PATCH /api/tier denial, mkdir confinement, webhook race.
### Phase 2 — Real verification infrastructure (Weeks 23) · WS-B
- **Goal:** "done" means proven. Institutionalize catching the sibling-path gap-class (the thing the self-grading missed).
- **Closes:** zero-browser-E2E gap; untested failure paths (network drop mid-stream, capability-missing hard error, traversal-rejected, unpaid-tier gate, sidecar restart); R3-002 (circular-dep test).
- **Workflow:** `2.3 e2e-synth` → generate Playwright journeys for the 5 personas + a failure-injection test per path; land them; wire `test:all` into CI. Add a recurring `2.1 verification` as a pre-merge step.
- **Exit:** E2E + failure-injection suite green in CI; the 5 audit personas have real browser coverage; error-recovery is *tested*, not asserted.
### Phase 3 — Build / Release / Distribution, both surfaces (Weeks 35) · WS-C
- **Goal:** CI emits shippable artifacts for both surfaces.
- **Desktop:** Windows code-signing cert wired into `tauri.conf` + `release.yml`; macOS build target + Developer ID + notarization; fix updater `signature:""` so signed auto-updates verify (R7-008); npx-publishable CLI (bin→built .js, workspace deps resolve).
- **Web:** complete `render.yaml`/`docker-compose.production` deploy; live-mode Stripe (incl. AV-4: validate `billingPeriod`, fail-closed on missing annual price); Clerk prod; **prod-origin CORS exact-match on the team server** (`packages/server/src/index.ts` currently uses raw `CORS_ORIGIN` env, disconnected from `corsOriginAllowed` — unify it); Docker non-root.
- **Migration:** R9-001 drizzle 0.44→0.45 (+ re-tsc + server tests) — its own sub-task with full build verify.
- **Workflow:** `2.4 release-readiness` to drive the checklist; `2.2 fix-execution` for the migration + signing wiring.
- **Exit:** CI produces a signed Win installer + notarized mac DMG; updater verifies a real signed release; web deploys to staging and a live-mode test checkout + webhook round-trips; drizzle migrated, build+tests green.
### Phase 4 — UX to GA (Weeks 57) · WS-D
- **Goal:** light mode usable, a11y basics, hot-path perf, stickiness triaged.
- **Closes:** R5-006 (the 358 hardcoded colors → semantic tokens), R5-002/003 (add light-theme values for status tokens), R5-004 (modal role/aria/Escape/focus-trap), R5-005 (window controls), R3-004 (add `content_hash` column → kill O(n·500) on harvest), addictiveness-audit `FEATURE-REQUESTS.md` triaged into GA-must vs post-GA.
- **Workflow:** a Haiku-heavy mechanical sweep (palette→token) parallelized by component cluster, then an **independent contrast/verify pass** (don't self-grade the light mode — that's the 10/10 trap); `2.2` for a11y + perf.
- **Exit:** independent light-mode contrast audit passes (WCAG AA on load-bearing text); a11y smoke on the three modals; large-harvest no longer blocks the event loop; feature list triaged.
### Phase 5 — GA cut (Weeks 78)
- **Goal:** ship.
- **Do:** full `2.1 verification` over the *entire* audit + new tests; `2.4 release-readiness` all green; 5/5 gates green; push the (now 40+) commits; tag; cut beta → GA; wire Enterprise CTA → kvark.ai.
- **Exit:** GA gate checklist (§1) 100%.
---
## 5. Immediate next actions (this week)
1. **Concurrent session:** let it finish + **push** the 31 commits to `origin/hardening/prod-readiness` so both lanes share a base. One designated session pushes.
2. **This/integration session:** start **Phase 0 (gate repair)** on a fresh worktree (`git worktree add ../waggle-os-ga hardening/prod-readiness`) — it's read-mostly config work, lowest collision risk, highest signal payoff.
3. **Decide D1** (localhost-trust) — it reshapes Phase 1. I recommend "require token even on loopback."
4. Feed `TRUST-REPORT.md`'s open items as the `residuals` arg into the `2.2 fix-execution` workflow.
---
## 6. Sync protocol (active — two sessions live)
Per `OPERATING-MANUAL.md §5`: one session owns the shared tree; all *additional* mutation on new worktrees; verification stays read-only; one session pushes; deliverable docs live here in `North star/waggle-ga/` until a session owns the tree, then fold into `waggle-os/docs/`. **Recommended handoff:** fixing session pushes → this report identifies residuals → a single integration session works residuals on a worktree → merge → re-verify → GA.
---
## 7. Risks & how the fleet de-risks them
| Risk | Mitigation |
|---|---|
| Self-graded fixes hide sibling-path gaps (the recurring failure) | Phase 2 makes independent adversarial re-verify a *pre-merge gate*, not a one-off. |
| Solo bandwidth across 5 phases | Fan-out per workstream; Haiku for mechanical bulk (light-mode), Opus for security/threat-model; background workflows while you steer. |
| drizzle major migration breaks runtime (already bit once) | Isolated worktree + full re-tsc + the 19 server test files as the exit gate before merge. |
| Light-mode "looks done" but isn't (it's 0.6% now) | Independent contrast audit, not self-assessment — same doctrine as security. |
| Two sessions corrupt the tree | Worktree-per-stream is non-negotiable; only one writer per tree. |
| Scope creep from addictiveness feature-requests | Triaged into GA-must vs post-GA in Phase 4; default post-GA. |
---
*Plan is intentionally verification-anchored: if a later re-run of the verification workflow contradicts a "closed" item here, the re-run wins and the item reopens.*

View File

@@ -0,0 +1,65 @@
# Waggle-OS GA Reconciliation — 2026-05-30
> Baseline: `main @ 5ccfa53` (2026-05-29). This reconciles `PRODUCTION-PLAN.md` Phases 25 against the code after the Phase0/Phase1 security track (AV-1/2/3/5, D1, gates) and the parallel-session Phase7ae + S4 full type-ratchet (`no-explicit-any` 937→0 repo-wide) both landed. The plan was authored **before** that work, so some items are now closed and some plan claims — especially Phase 7d's "light-mode finish" — are corrected against verified evidence below.
## 1. Headline — what genuinely remains for GA
The hard security and supply-chain work is done (drizzle 0.45 migrated and clean; Stripe `billingPeriod` fail-closed validation in place), but **GA is not shippable as-is**: there is a live **CORS suffix-attack vulnerability in the team server** (`packages/server/src/index.ts:46` passes a raw origin array to Fastify CORS instead of the exact-match `corsOriginAllowed()` the local server already uses), a **payment-gate bypass in both Stripe webhook handlers** (checkout.session.completed grants paid tiers without checking `payment_status`), **no real browser/persona E2E or failure-injection coverage**, **deployment manifests missing all Stripe/CORS env vars**, **light mode ~0.6% migrated** (4 status tokens + 5 KG tokens missing for light theme, 60+ files still hardcoded, contrast never visually validated), and **desktop signing + R7-008 updater-signature unfixed** (these are platform-blocked on Windows). The two items the verifiers downgraded from CLOSED→OPEN/PARTIAL (P3-stripe webhook gate, P3-cors) are the most urgent because they are security regressions, not gaps.
## 2. Cluster status matrix
| Cluster | Phase | Final Status | Key Residuals | Evidence |
|---|---|---|---|---|
| P2-e2e | Phase 2 (WS-B) | **PARTIAL** | No Playwright persona journeys; no failure-injection suite; no R3-002 cycle test; CI bypasses npm scripts | `tests/e2e/user-journeys.spec.ts` (generic J1J10 only); `.github/workflows/ci.yml:71` runs `npx playwright test tests/e2e/` directly |
| P3-sign | Phase 3 (WS-C desktop) | **PARTIAL** | Win cert not in CI; mac Developer ID/notarization not in CI; R7-008 empty `signature:""` | `release.yml:56-65,111-121,137/141/145`; `tauri.conf.json`; pilot self-sign scripts exist but manual |
| P3-cli | Phase 3 (WS-C desktop) | **PARTIAL** | `@waggle/cli` missing `files`, `publishConfig`, metadata, `exports` (publishes tests/src) | `packages/cli/package.json:7-9,16-22`; vs fully-configured `hive-mind-cli` |
| P3-deploy | Phase 3 (WS-C web) | **PARTIAL** | Stripe + CORS env vars absent from both deploy manifests; `render.yaml` build path mismatch (`cd app` vs `apps/web`) | `render.yaml:1-63`; `docker-compose.production.yml:16-57`; Dockerfile non-root OK (`:91-115`) |
| P3-stripe | Phase 3 (WS-C web) | **PARTIAL** ⚠️ *(verifier downgraded from CLOSED)* | Webhook handlers grant tiers without `payment_status` check (R1-002 bypass) | `packages/server/src/stripe/webhook.ts:115-126`; `apps/www/app/api/webhooks/stripe/route.ts:98-110` |
| P3-cors | Phase 3 (WS-C web) | **OPEN** ⚠️ *(verifier downgraded from PARTIAL)* | Team server passes raw array to Fastify CORS → suffix attack; deploy `CORS_ORIGIN` unset | `packages/server/src/index.ts:46` vs correct local `index.ts:1912-1920` + `cors-config.ts:57-59` |
| P3-drizzle | Phase 3 (WS-C migration) | **CLOSED** ✓ | None | `packages/server/package.json:32` `drizzle-orm@^0.45.2`; journal v7; commit `fb33ca8`; tsc clean |
| discovered-drift | Discovered §7.5 | **PARTIAL** ⚠️ *(verifier downgraded from CLOSED)* | Stale `packages/core/src/mind/**` paths + hard-coded script paths in deprecated sync workflows | `sync-mind.yml:51,117-118`; `mind-parity-check.yml:35,41,118,170-171` |
| P4-light | Phase 4 (WS-D) | **OPEN** | 4 light status tokens + 5 KG tokens missing; 60+ files hardcoded; contrast never validated | `apps/web/src/index.css:190`; commit `6a3d598` ("Contrast NOT visually validated"); TRUST-REPORT "~0.6% done" |
| P4-a11y-perf | Phase 4 (WS-D) | **PARTIAL** | ComplianceTemplateModal no Escape/focus-trap/labelledby; window controls absent; harvest `contentHash` never computed/passed | `ComplianceTemplateModal.tsx:171-172`; `harvest.ts:409` + `index.ts:1226,1449` call `recordSync` w/ 3 args; `tauri.conf.json:37` |
Legend: ⚠️ = adversarial verifier overrode the original investigator status.
## 3. Per-phase: what's actually left
### Phase 2 (WS-B) — E2E + failure-injection — PARTIAL
The "E2E" that exists is Vitest server-level tests plus generic Playwright UI journeys. None of the plan's persona/failure-recovery intent is met.
- Add Playwright **persona journeys** for researcher, analyst, coder, product-manager, executive-assistant (chat → tool invocation → result), distinct from generic J1J10. The 17-persona coverage that exists is behavioral unit tests in `packages/agent/tests/behaviors/`, not browser journeys.
- Add a **failure-injection suite** (e.g. `tests/e2e/failure-injection.spec.ts`): network-drop mid-stream, capability-missing hard error, traversal-rejected 403, unpaid-tier gate, sidecar-restart.
- Add the **R3-002 circular-dependency test** asserting `@waggle/shared` / `@waggle/server` / `@waggle/agent` / `@waggle/core` form an acyclic import graph (none exists; grep for `R3-002` returns nothing).
- Fix **CI wiring**: `.github/workflows/ci.yml:71` invokes Playwright directly instead of `npm run test:all` / `test:e2e`, so CI and local diverge from the package.json scripts.
- Publish a **persona→journey coverage map** for CI visibility.
### Phase 3 (WS-C) — desktop, web, migration — mixed
**P3-drizzle is genuinely CLOSED** (verifier agreed): 0.44.7→0.45.2 across server/launcher/worker, journal v7, tsc clean, no audit findings — a pure security patch (GHSA-gpj5-g38j-94v9), 42 usage sites verified.
**P3-stripe — verifier OVERRODE CLOSED → PARTIAL.** Commit `27a98f7` correctly fixed `billingPeriod` (R1-003) and atomic writes (R1-011), but the R1-002 payment gate was only added to the `/api/stripe/sync` endpoint, **not the webhook handlers**. Both `webhook.ts:115-126` and `apps/www/.../route.ts:98-110` process `checkout.session.completed` without checking `session.payment_status`, so webhook delivery can grant a paid tier from an unpaid session. **Next action:** add a `payment_status === 'paid'` (or `'no_payment_required'`) guard in both webhook handlers before any tier grant; add a regression test.
**P3-cors — verifier OVERRODE PARTIAL → OPEN (security).** `packages/server/src/index.ts:46` passes `{ origin: config.corsOrigin }` (raw array) to `@fastify/cors`, which does substring/`includes`-style matching — vulnerable to suffix attacks (e.g. `localhost:1420.evil.com`). The local server already does it right at `index.ts:1912-1920` via `corsOriginAllowed()` (`cors-config.ts:57-59`). **Next action:** unify the team server onto the callback-based exact-match `corsOriginAllowed()`; set `CORS_ORIGIN` in both deploy manifests; add a test asserting lookalike-origin rejection. Treat as a release blocker for any team-server deployment.
**P3-deploy — PARTIAL.** `render.yaml` and `docker-compose.production.yml` declare DB/Redis/Anthropic/Clerk but **omit** `STRIPE_SECRET_KEY`, `STRIPE_WEBHOOK_SECRET`, `STRIPE_PRICE_*`, and `CORS_ORIGIN`. Non-root Docker user is correctly set (`Dockerfile:91-115`). **Next action:** add the Stripe + CORS env vars (`sync:false`) to both manifests; resolve the `render.yaml:16` build-path mismatch (`cd app` vs package.json `cd apps/web`) — confirm/alias or fix, since it is a latent deploy failure; document that `apps/www` is a separate Next.js deployment.
**P3-sign — PARTIAL (partly platform-blocked, see §4).** `release.yml` passes no signing secrets for Windows or macOS; the updater manifest hard-codes `signature:""` for all three targets (R7-008); pilot self-sign scripts exist but are manual. **Next action (codeable on Windows):** wire Windows EV cert + `TAURI_PRIVATE_KEY`/`TAURI_KEY_PASSWORD` into `release.yml`, add the GitHub Actions secrets, and populate the updater `signature` field from the signing step. Cert procurement (timeline T+14/2026-05-21 start) appears **overdue** with no in-repo evidence.
**P3-cli — PARTIAL.** `bin → dist/.js` routing works and workspace deps resolve, but `@waggle/cli` lacks `files`, `publishConfig.access`, standard metadata (`repository`/`bugs`/`homepage`/`types`/`author`/`keywords`/`engines`), and `exports` — so it would publish tests/src. **Next action:** mirror `hive-mind-cli`'s package.json publishing config.
**discovered-drift — verifier OVERRODE CLOSED → PARTIAL.** The stale `packages/core/src/mind/**` trigger paths in `sync-mind.yml` and `mind-parity-check.yml` won't fire (files moved to `packages/hive-mind-core` on `3b556c0`), but the shell scripts still hard-code stale paths (`mind-parity-check.yml:118,170-171`) and there's no re-activation runbook — inert by accident, not by design. **Next action:** either delete the deprecated workflows or clean the hard-coded paths and add a one-line runbook note.
### Phase 4 (WS-D) — light mode + a11y/perf — OPEN / PARTIAL
**P4-light — OPEN. The Phase 7d "light-mode finish" claim is materially false.** Verified: only `--status-healthy` has a light value (`index.css:190`); `--status-warning/error/info/ai` and `--kg-person/project/concept/org/default` have **no** light variants; 60+ component files still use hardcoded Tailwind classes (e.g. `UserProfileApp.tsx:232,268,279`); commit `6a3d598` explicitly states "Contrast NOT visually validated (no binary build)"; TRUST-REPORT puts it at "~0.6% done". **Next actions:** add the 4 missing status tokens + 5 KG tokens to the light-theme block; migrate hardcoded classes to semantic tokens; run an independent WCAG-AA contrast audit; review the ~13 flagged mid-tone `text-*-300` accents for regression.
**P4-a11y-perf — PARTIAL.** `ComplianceTemplateModal` has `role="dialog"`/`aria-modal` but no Escape handler, no `aria-labelledby`, no focus trap; `TrialExpiredModal`/`UpgradeModal` have Escape + labelledby but only focus-on-mount (no trap). Window min/max/close controls are absent (`tauri.conf.json:37` `decorations=true`, no custom titlebar). Harvest dedup is **half-wired**: `last_content_hash` exists in DDL and `recordSync()` accepts a 4th `contentHash` arg, but all three callsites (`harvest.ts:409`, `index.ts:1226`, `index.ts:1449`) pass only 3 args, so the O(n·500) rescan persists. **Next actions:** add Escape + `aria-labelledby` + a shared focus-trap to all three modals; build the custom Tauri titlebar; compute SHA-256 `contentHash`, pass it at all three callsites, and add the dedup skip on `last_content_hash`.
### Phase 5 — not present in reconciliation set
No Phase 5 clusters were verified in this pass. The plan's Phase 5 items are **unreconciled** here — treat as untracked and re-scope before GA sign-off (do not assume closed).
## 4. Platform-blocked on Windows (cannot be completed in this env)
These require a macOS host and/or real signed binary runtime and are **not codeable here**:
- **R7-008 / macOS signing + notarization** — Developer ID signing and `notarytool` need a Mac; the updater `signature` field cannot be populated with a real signature without an actual signed-build pipeline. (Windows EV signing *can* be wired from here once the cert is procured; only the mac half and live signature generation are blocked.)
- **Binary runtime smokes** — light-mode contrast validation, modal a11y smoke, and window-controls verification that the Phase 7d commit explicitly deferred ("no binary build") need a built desktop binary to observe; they cannot be visually validated in this environment.
## 5. Recommended next workflow
Fix the two security downgrades first as a TDD security pass — **P3-cors exact-match unification + P3-stripe webhook `payment_status` gate** (write failing tests, fix, verify), then run the Phase 2 E2E/failure-injection build-out and Phase 3 deploy-env wiring; defer all macOS-signing/binary-runtime items to a Mac/CI environment.

100
docs/ga/TRUST-REPORT.md Normal file
View File

@@ -0,0 +1,100 @@
# Waggle OS — Independent Verification (Trust Report)
**Date:** 2026-05-29
**Verifier:** independent read-only workflow (13 agents, ~873K tokens) — a *different* lane from the session that wrote the fixes.
**Target:** `hardening/prod-readiness` @ HEAD `0f6bb4c` (31 unpushed commits, Phases 1→6 of the 2026-05-29 audit).
**Method:** static read of the live tree at run time + adversarial skeptics on every "fixed" security/billing verdict. **Caveat:** static analysis, not dynamic — no builds/tests were run (collision-safe alongside the active fixing session). Items needing live exercise are flagged. Tree was clean at run time; if the other session edited mid-run, a verdict could reflect a transient state.
---
## 1. Bottom line
**The 6-phase execution largely worked.** ~29 findings are *solidly* closed under independent review — including the headline LAN-bind (R1-001 default path), the memory-moat mock-embedder regression (R3-001), all 8 dependency CVEs that could be safely bumped, and all 8 frontend-resilience findings.
**But "committed" ≠ "closed."** The adversarial pass found that **5 of the security/billing fixes verified as "fixed" by the cluster reviewers are actually bypassable or incomplete**, and **2 quality gates remain dead** (lint, tauri-tsc) — meaning the green test run is still two-fifths blind. The single biggest *systemic* finding: the sidecar's **"any loopback caller is trusted, no auth"** model is the root cause behind several bypasses and is the wrong threat model for a desktop app that coexists with browsers, extensions, and other local apps.
**Ship-readiness verdict:** the *architecture* and the *bulk* of the hardening are genuinely sound. **Do not GA** until the localhost-trust cluster + the empty-Host bypass + PATCH /api/tier + the two dead gates are closed. These are surgical, not architectural.
---
## 2. Scorecard
| Cluster | Solid-fixed | Partial / skeptic-downgraded | Not fixed |
|---|---|---|---|
| Security · network/auth | R1-001*, R2-003*, R2-004* (verified fixed, **skeptic-downgraded**) | R2-006, R6-005 | — |
| fs-traversal | R6-001, R6-002, R6-006, R6-008, R1-004, R1-005, R2-005 | — | **R6-007** (Zod unwired) |
| Billing | — | R1-011; **R1-002\***, **R1-003\*** (skeptic-downgraded) | — |
| Quality gates | build-typecheck ✅ | — | **lint-gate** ⛔, **tauri-tsc** ⛔ |
| Memory/agent core | R3-001, R3-003 | R3-002 (code ok, no test) | **R3-004** (O(n·500) hot path) |
| Desktop lifecycle | R7-003, R7-004 | **R7-002** (orphan via tray Quit) | — |
| Deps supply-chain | R9-002/003/004/005/006/007/008/009 | — | **R9-001** (drizzle, deferred) |
| Frontend resilience | R4-001…R4-008, R5-001 | R5-002, R5-003 | **R5-006** (light-mode ~0.6%) |
`*` = cluster verifier said *fixed*, but the adversarial skeptic found a concrete bypass. **Treat as open.**
---
## 3. 🔴 NEW issues the adversarial pass surfaced (highest value — not in the original audit, or understated by it)
These are the findings that *independent* verification bought you. None were caught by the same-agent self-grading.
| # | Severity | Where | Issue | Fix |
|---|---|---|---|---|
| **AV-1** | **HIGH (default path!)** | `security-middleware.ts:289-290` | **Empty/absent `Host` header bypasses the DNS-rebinding allowlist in the DEFAULT 127.0.0.1 config.** `if (hostHeader && !ALLOW.has(hostHeader))` — empty string is falsy, so a `Host:`-less request (curl, HTTP lib) skips the check entirely. No env change needed. | Block on absent host too: `if (!hostHeader \|\| !ALLOW.has(hostHeader)) return 403`. |
| **AV-2** | **HIGH** | `local/routes/waggle-signals.ts:~89` | **`/api/waggle/stream` reflects `request.headers.origin ?? '*'`** as `Access-Control-Allow-Origin` — textbook origin-reflection. A page on `evil.com` can `EventSource` the full signal stream. Does NOT use the fixed `corsOriginAllowed`. | Route the SSE header through `corsOriginAllowed` / `validateOrigin` like chat.ts does. |
| **AV-3** | **HIGH (revenue)** | `local/routes/settings.ts:351-373` | **`PATCH /api/tier` writes any tier to `config.json` with no payment check and no auth** (localhost-trust exemption). Any local web page/extension/app can `PATCH {tier:'TEAMS'}` and unlock paid features for free. The R1-002 fix only guarded the `/stripe/sync` path — this sibling path is wide open. | Gate tier mutation behind verified Stripe state; remove the unauthenticated write path or require a signed token. |
| **AV-4** | MED (correctness) | `stripe/checkout.ts` + `stripe/index.ts:91-99` | **`billingPeriod` has no runtime validation** (TS-only type; Fastify doesn't enforce). And under the legacy single-var env contract, an `annual` request **silently resolves to the monthly price** — the test suite even asserts this as passing. | Validate `billingPeriod` enum at runtime; fail closed (or warn) when an annual request can't find an annual price. |
| **AV-5** | MED | `net-config.ts:23` | `isLoopbackBind()` strict-compares to `'127.0.0.1'` only, so **`WAGGLE_HOST=localhost` *or* `::1`** (both natural loopback choices) disable the entire Host allowlist. | Recognize `localhost`, `::1`, `::ffff:127.0.0.1` as loopback. |
**Systemic root cause (the strategic one):** `security-middleware.ts` exempts *all* loopback connections from bearer auth. For a **desktop** product that runs next to browsers/extensions/other local apps, "local = trusted" means any of them can drive the authenticated API (AV-3 is the proof). This is the threat-model item to decide deliberately, not patch piecemeal — see PRODUCTION-PLAN Workstream A.
---
## 4. ⛔ Ship blockers still open
1. **lint-gate — NOT FIXED.** No root `eslint.config.*`; `npm run lint` (`eslint .`) aborts under ESLint 9 flat-config having linted **zero files**. The commit that closed build-gate issues *documented skipping this*. All of `packages/*`, `app/*` are unlinted in CI.
2. **tauri-tsc — NOT FIXED.** `app/tsconfig.json` still `"include": ["src"]`; `app/src/` doesn't exist → `TS18003`, zero type signal for the Tauri layer. `app/scripts/` + `app/tests/` TS is unchecked.
-**Two of five gates report green by reporting nothing.** This is workstream-0.
3. **AV-1, AV-2, AV-3** above (default-path Host bypass; SSE origin reflection; unauthenticated tier escalation).
---
## 5. 🟡 Partials (fix landed, but incomplete)
- **R2-006** — same-origin gate + vault-key-names dropped ✅, but `/api/debug/logs` still `SELECT *`s 500 `install_audit` rows (capability installs w/ risk_level, approval_class) — recon material. Limit columns + rows.
- **R6-005** — both browse routes gated ✅, but `POST /api/browse/local/mkdir` does `mkdirSync(path.resolve(dirPath))` with **no root confinement** → can create dirs anywhere; no dedicated rate limit.
- **R1-011** — atomic temp+rename write ✅, idempotency list-check ✅, but **no mutex** → concurrent Stripe retry can race the read-check-write and double-process a tier-change event. Add a module-scoped promise queue.
- **R7-002** — `RunEvent::Exit` kill block exists ✅ **but is unreachable via the primary quit path**: tray "Quit Waggle" emits `waggle://quit` to a frontend with **no listener** (confirmed by grep), and window-close is prevented. User clicks Quit → sidecar orphaned on :3333 → next launch fails. Add `app.exit()` in the tray quit arm.
- **R3-002** — circular-dep duplicate-worker-id fixed in code ✅ but **no test** exercises the circular-dep path.
- **R5-002 / R5-003** — token renamed / container made semantic, but `--status-healthy` has **no light-theme value** and 10+ palette badges remain → still fails contrast on cream.
---
## 6. ❌ Not fixed (deliberate or deferred)
- **R6-007** — Zod schemas in `@waggle/shared` are used in *cloud* routes but **still not wired to the four local sidecar fs-write routes** (chat, tasks, ingest, documents) — they hand-roll checks. Traversal itself is guarded by `assertSafeSegment`, so this is **defence-in-depth / body-validation quality**, not an open traversal sink.
- **R3-004** — `frames.ts findDuplicate` still `SELECT *`s 500 rows and SHA-256-hashes each on **every** insert → O(n·500) event-loop block on large harvests. No `content_hash` column added. Hot-path perf/reliability.
- **R9-001** — `drizzle-orm@0.44.7` stays (GHSA HIGH). 0.44→0.45 major broke module resolution (19 test files), **honestly deferred**. Latent, not live: `sql.identifier` sink is absent from the codebase today. Needs a real migration + re-tsc before GA.
- **R5-006** — light mode is **~0.6% done**: 358 hardcoded palette classes across 62 files; only 2 converted. The whole light surface is effectively broken.
- **R5-004, R5-005** — a11y modals / window-control dots: never claimed fixed; **cannot determine** (out of scope of these commits).
---
## 7. ✅ Genuinely closed (high confidence) — keep, don't re-litigate
- **R1-001 default path** (127.0.0.1 bind via `resolveBindHost()`; `/health` no longer returns `wsSessionToken`) — solid for the default config (caveats AV-1/AV-5 are the *Host-allowlist* layer, not the bind/token leak).
- **fs-traversal**: R6-001/002/006/008, R1-004/005, R2-005 — `assertSafeSegment` at every route boundary + 6 dedicated traversal test files asserting 400 + no out-of-root artifact. Strong.
- **R3-001** (mock-embedder poisoning) — now reads `fastify.embeddingProvider`, skips indexing when mock, with a two-branch integration test. The memory-moat regression is genuinely repaired.
- **Deps**: protobufjs 7.6.1 (clears 9.8 RCE), js-cookie 3.0.8, lodash 4.18.1, tmp 0.2.7, fastify 5.8.5, @fastify/static 9.1.3, @clerk/fastify 3.1.32, @xmldom/xmldom 0.8.13 — single resolved installs, no shadow copies. (Recommend a live HTTP smoke for fastify/@fastify/static.)
- **Frontend resilience**: R4-001 (root + per-window error boundaries), R4-002 (real restore picker), R4-003…008 (async handlers wrapped + toasts), R5-001 (context-menu keyboard index) — well-executed, several with unit tests.
- **Desktop**: R7-003 (self-healing watchdog w/ backoff), R7-004 (updater slug correct in conf + release.yml).
> Updater note: pubkey is set but `release.yml` writes `"signature": ""` for all platforms — signed auto-updates still can't verify. That's R7-008 (signing), tracked in the plan's Release workstream.
---
## 8. What this means
The fixing session did **strong, real work** — this is not a case of hollow commits. But it confirms the project's core risk pattern: **fixes are graded by the agent that wrote them, against the one path it was thinking about.** Independent adversarial review found the *sibling paths* (PATCH /api/tier vs /stripe/sync; waggle-signals vs chat SSE; empty-Host vs named-Host). That gap-class is exactly what the PRODUCTION-PLAN's verification workstream institutionalizes so it stops recurring.
→ See `PRODUCTION-PLAN.md` for how these residuals sequence into the 12 month GA push.