|
| 1 | +# F1 — Plan Compliance Audit |
| 2 | + |
| 3 | +``` |
| 4 | +Must Have [16/17] | Must NOT Have [13/16] | Tasks [48/48] | Evidence [62/48] | VERDICT: REJECT |
| 5 | +``` |
| 6 | + |
| 7 | +**Date**: 2026-05-06 |
| 8 | +**Auditor**: F1 (oracle plan-compliance) |
| 9 | +**Plan**: `.sisyphus/plans/openlinear-80-percent.md` |
| 10 | + |
| 11 | +--- |
| 12 | + |
| 13 | +## Headline |
| 14 | + |
| 15 | +Implementation is materially complete (all 48 tasks committed, all named deliverables on disk, API + sidecar typecheck clean, schema models migrated, security routes hardened, brand assets shipped, landing copy purged). However, **3 explicit "Must NOT Have" guardrails are violated** and **1 typecheck target fails**, which the plan's Must-Have list requires to pass implicitly via the build/run criterion. Verdict is **REJECT** pending small fixes — none touch architecture, all fixable in <30 min. |
| 16 | + |
| 17 | +--- |
| 18 | + |
| 19 | +## 1. Must Have — Evidence |
| 20 | + |
| 21 | +| # | Item | Evidence | Status | |
| 22 | +|---|------|----------|--------| |
| 23 | +| 1 | Auth + team-scope on POST/PATCH/DELETE `/api/tasks` | `apps/api/src/services/ownership.ts` exists; `apps/api/src/routes/tasks.ts` uses `requireAuth` + `assertTaskOwned`; commit `66ee3c6`; evidence `task-7-unauth-rejected.txt`, `task-7-cross-tenant-rejected.txt`, `task-7-archived-scoped.txt` | ✅ | |
| 24 | +| 2 | Shell injection eliminated in git.ts/worktree.ts | `grep -rEn 'execAsync\(\`\|exec\(\`' apps/sidecar/src/services/execution/git.ts apps/sidecar/src/services/worktree.ts` → 0 matches; `apps/sidecar/src/services/execution/exec.ts` exports `execFileAsync`; commit `867c895`; evidence `task-6-injection-blocked.txt`, `task-6-no-token-leak.txt`, `task-6-happy-path.txt` | ✅ | |
| 25 | +| 3 | Single `apiFetch()` wrapper used everywhere | `apps/desktop-ui/lib/api/fetch.ts` exists; commit `72c38b4`; evidence `task-3-no-module-capture.txt` | ⚠️ See REJECT-1 (2 stragglers) | |
| 26 | +| 4 | 401 clears token + redirects to /login | `apiFetch` dispatches `auth:expired`; `hooks/use-auth.tsx` listens; evidence `task-3-qa-scenarios.txt` | ✅ | |
| 27 | +| 5 | Comment + Notification + AgentRun + ActivityLog models | `grep -E "^model (Comment\|AgentRun\|Notification\|ActivityLog) " packages/db/prisma/schema.prisma` → 4 matches; commit `88dbd92`; evidence `task-1-migration-success.txt` | ✅ | |
| 28 | +| 6 | `assigneeId` + `creatorId` on Task; my-issues filters | schema.prisma:114-119 + indexes 130-131; commit `6af55e0`; evidence `task-32-assignee.txt` | ✅ | |
| 29 | +| 7 | Cmd+K command palette with global search | `apps/desktop-ui/components/command-palette.tsx`; commit `bd3b498`; evidence `task-25-palette.txt` | ✅ | |
| 30 | +| 8 | Markdown rendering on tasks + comments | commit `ecd48dc`; evidence `task-26-markdown.txt` | ✅ | |
| 31 | +| 9 | Theme switcher functional (dark/system) | commit `f8d412d` (theme batch); evidence `task-29-theme.txt` | ✅ | |
| 32 | +| 10 | `prefers-reduced-motion` respected | commit `8dacbb6` + `22da6de`; evidence `task-21-css-diff.txt`, `task-24-animations.txt` | ✅ | |
| 33 | +| 11 | `focus-visible` 2px accent ring | commit `22da6de`; evidence `task-21-css-diff.txt` | ✅ | |
| 34 | +| 12 | SVG logomark + wordmark + favicon + Tauri icon set | `apps/desktop-ui/public/brand/{logomark,wordmark}.svg` exist; commits `f374471`, `117d517`; evidence `task-5-tauri-icons.txt`, `task-5-logomark-sizes.png`, `task-34-logo-svg.txt`, `task-35-tauri-icons.txt` | ✅ | |
| 35 | +| 13 | Domain unified to `openlinear.tech` | `grep -rn 'openlinear\.dev' apps/landing/` → 0 matches; commit `2310987` | ✅ | |
| 36 | +| 14 | `kaizen403` handle replaced everywhere user-visible | `grep -rin "kaizen403"` → only in `.sisyphus/plans/` and `.sisyphus/notepads/` (plan history, not user-visible); zero user-visible refs; commit `5b6f39b`; evidence `task-41-handle.txt` | ✅ | |
| 37 | +| 15 | Landing copy: zero Zep/Mem0, zero fake stats, footer links resolve | grep for `zep\|mem0\|google drive\|onedrive` in `apps/landing/` → 0 matches; commits `810710a`, `2310987`; evidence `task-39-copy.txt`, `task-40-footer.txt` | ✅ | |
| 38 | +| 16 | App Router error boundaries (loading.tsx/error.tsx/not-found.tsx) | All 3 files exist at `apps/desktop-ui/app/`; commit `4495952`; evidence `task-43-boundaries.txt` | ✅ | |
| 39 | +| 17 | Final container builds/runs smoke test | API typecheck PASS; sidecar typecheck PASS; **desktop-ui typecheck FAIL** (see REJECT-3) | ❌ | |
| 40 | + |
| 41 | +**Score: 16/17** |
| 42 | + |
| 43 | +--- |
| 44 | + |
| 45 | +## 2. Must NOT Have — Violations |
| 46 | + |
| 47 | +| # | Guardrail | Result | Status | |
| 48 | +|---|-----------|--------|--------| |
| 49 | +| 1 | No `as any` / `@ts-ignore` introduced | **1 violation** in `apps/sidecar/src/routes/opencode.ts:196` | ❌ REJECT-2 | |
| 50 | +| 2 | No `console.log` in production code | **20+ violations** in sidecar (see REJECT-4) | ❌ | |
| 51 | +| 3 | No `window.confirm` / `window.alert` / `window.prompt` | grep clean | ✅ | |
| 52 | +| 4 | No raw `localStorage.getItem('token')` outside `apiFetch()` | **2 violations** (sidebar.tsx:198, inbox/page.tsx:141) | ❌ REJECT-1 | |
| 53 | +| 5 | No new direct `fetch()` in pages — must go through `apiFetch()` | Spot-checked OK | ✅ | |
| 54 | +| 6 | No new raw hex color literals | grep for `bg-\[#\|text-\[#\|border-\[#` in components/app → 0 matches | ✅ | |
| 55 | +| 7 | No `exec()` with shell interpolation in git.ts/worktree.ts | grep clean | ✅ | |
| 56 | +| 8 | No new endpoints without auth + ownership | Spot-checked tasks/comments/notifications/activity → all use `requireAuth` + ownership helpers | ✅ | |
| 57 | +| 9 | No new module-level `getApiUrl()` captures | Per `task-3-no-module-capture.txt` — clean at audit time | ✅ | |
| 58 | +| 10 | No deferring of security tasks (T6, T7, W2.1, W2.2) | All committed (`867c895`, `66ee3c6`, `b08e37f`, `9289d9d`) | ✅ | |
| 59 | +| 11 | No light theme | Dark/system only — confirmed | ✅ | |
| 60 | +| 12 | No file attachments | Not added | ✅ | |
| 61 | +| 13 | No cycles/roadmap/templates | Not added | ✅ | |
| 62 | +| 14 | No Tauri Rust changes | Not touched | ✅ | |
| 63 | +| 15 | No OpenCode binary bundling | Not added | ✅ | |
| 64 | +| 16 | No "fix everything in one mega-task" | All 48 tasks bounded with separate commits | ✅ | |
| 65 | + |
| 66 | +**Score: 13/16** |
| 67 | + |
| 68 | +--- |
| 69 | + |
| 70 | +## 3. Tasks (48/48) |
| 71 | + |
| 72 | +48 implementation commits since baseline (T1=`88dbd92` through T48=`a81b72d`). All 48 plan checkboxes marked `[x]`. The 11 unchecked boxes in the plan are the Definition-of-Done meta-items + the F1-F4 review tasks themselves (this audit being one), not implementation tasks. |
| 73 | + |
| 74 | +## 4. Evidence (62/48) |
| 75 | + |
| 76 | +`ls .sisyphus/evidence/ | wc -l` → 62. All `task-N-*` slots filled for N=1..48 (some tasks have multiple files, e.g. `task-1-migration-success.txt` + `task-1-email-unique-error.txt`). |
| 77 | + |
| 78 | +## 5. Concrete Deliverables — file existence audit |
| 79 | + |
| 80 | +| Plan deliverable | File | Status | |
| 81 | +|------------------|------|--------| |
| 82 | +| `apiFetch()` wrapper | `apps/desktop-ui/lib/api/fetch.ts` | ✅ | |
| 83 | +| Design tokens module | `apps/desktop-ui/lib/design-tokens.ts` | ✅ | |
| 84 | +| Brand SVG logomark | `apps/desktop-ui/public/brand/logomark.svg` | ✅ | |
| 85 | +| Brand SVG wordmark | `apps/desktop-ui/public/brand/wordmark.svg` | ✅ | |
| 86 | +| `execFileAsync` helper | `apps/sidecar/src/services/execution/exec.ts` | ✅ | |
| 87 | +| Ownership helper | `apps/api/src/services/ownership.ts` | ✅ | |
| 88 | +| Comments API | `apps/api/src/routes/comments.ts` | ✅ | |
| 89 | +| Search API | `apps/api/src/routes/search.ts` | ✅ | |
| 90 | +| Notifications API | `apps/api/src/routes/notifications.ts` | ✅ | |
| 91 | +| Activity Log API | `apps/api/src/routes/activity-log.ts` (plan said `activity.ts` — same surface, different filename) | ✅ | |
| 92 | +| Cost analytics page | `apps/desktop-ui/app/usage/page.tsx` | ✅ | |
| 93 | +| Command palette | `apps/desktop-ui/components/command-palette.tsx` | ✅ | |
| 94 | +| Error boundaries | `apps/desktop-ui/app/{loading,error,not-found}.tsx` | ✅ | |
| 95 | +| Schema migrated | `packages/db/prisma/schema.prisma` (Comment/AgentRun/Notification/ActivityLog + assigneeId/creatorId/parentId + indexes) | ✅ | |
| 96 | + |
| 97 | +--- |
| 98 | + |
| 99 | +## REJECT items (with file:line + remediation) |
| 100 | + |
| 101 | +### REJECT-1 — `localStorage.getItem('token')` outside `apiFetch()` (Must Have #3 + Must NOT #4) |
| 102 | + |
| 103 | +**Files**: |
| 104 | +- `apps/desktop-ui/components/layout/sidebar.tsx:198` — reads token to build EventSource URL for `/api/events` (SSE notification stream) |
| 105 | +- `apps/desktop-ui/app/inbox/page.tsx:141` — `readToken()` helper for SSE EventSource |
| 106 | + |
| 107 | +**Why it slipped**: Both are EventSource consumers. `apiFetch` does HTTP only; native `EventSource` cannot attach Authorization headers, so the codepath chose query-string token. The plan does not exempt SSE. |
| 108 | + |
| 109 | +**Remediation** (Quick, ~15 min): Either |
| 110 | +1. Add `getAuthToken()` exported from `lib/api/fetch.ts` and call it from these two sites (centralised access still goes through one module — satisfies the spirit), OR |
| 111 | +2. Extract `createAuthedEventSource(path)` into `lib/api/sse.ts` and have both sites use it. |
| 112 | + |
| 113 | +Option 2 is cleaner; bundles the lazy-URL + token-attach pattern that `apiFetch` enforces. |
| 114 | + |
| 115 | +### REJECT-2 — `as any` introduced in sidecar (Must NOT #1) |
| 116 | + |
| 117 | +**File**: `apps/sidecar/src/routes/opencode.ts:196` |
| 118 | + |
| 119 | +```ts |
| 120 | +allProviders.push({ |
| 121 | + id: 'opencode', |
| 122 | + name: 'OpenCode', |
| 123 | + models: {}, |
| 124 | +} as any); |
| 125 | +``` |
| 126 | + |
| 127 | +**Remediation** (Quick, ~5 min): Type the pushed object against the `OpenCode SDK Provider` type or define a local `type ProviderListEntry = typeof providerList.data.all[number]` and cast to that, or use `satisfies ProviderListEntry`. Since `models` is `{}` it likely needs `Record<string, never>` or the actual `ModelMap` type — check the SDK type. |
| 128 | + |
| 129 | +### REJECT-3 — desktop-ui typecheck FAILS (Must Have #17, smoke-build criterion) |
| 130 | + |
| 131 | +**File**: `apps/desktop-ui/components/ui/command.tsx:33` |
| 132 | + |
| 133 | +``` |
| 134 | +error TS2322: Type 'ReactNode' is not assignable to type 'React.ReactNode'. |
| 135 | + Property 'children' is missing in type 'ReactElement<...>' but required in type 'ReactPortal'. |
| 136 | +``` |
| 137 | + |
| 138 | +**Cause**: React 19 type duplication — `cmdk`/`@radix-ui` packages bundle a different `@types/react` version, producing two incompatible `ReactNode` types in the same graph. |
| 139 | + |
| 140 | +**Remediation** (Short, ~30 min): |
| 141 | +1. Pin a single `@types/react` version via `pnpm.overrides` in root `package.json`: |
| 142 | + ```json |
| 143 | + "pnpm": { "overrides": { "@types/react": "19.2.7" } } |
| 144 | + ``` |
| 145 | +2. Run `pnpm install` to dedupe. |
| 146 | +3. If issue persists, wrap the offending portal child in `<>{child}</>` or add an explicit `ReactNode` import alias from one canonical location inside `command.tsx`. |
| 147 | + |
| 148 | +### REJECT-4 — `console.log` in sidecar production code (Must NOT #2) |
| 149 | + |
| 150 | +**Files (sample, 20+ total)**: |
| 151 | +- `apps/sidecar/src/routes/execution.ts:33,37,180,183,191,196` |
| 152 | +- `apps/sidecar/src/services/worktree.ts:34,40,44,47,53,77,80,97,118,122,133,163,172,179` (and more) |
| 153 | + |
| 154 | +**Why it slipped**: T4 added `pino` to API + sidecar `app.ts`, but execution/worktree services were authored pre-pino and never migrated. They use a `[Worktree]`/`[Tasks]` prefix convention as a poor-man's structured log. |
| 155 | + |
| 156 | +**Remediation** (Short, ~1h): |
| 157 | +1. Export a `logger` from `apps/sidecar/src/lib/logger.ts` (pino instance). |
| 158 | +2. Replace `console.log(\`[Worktree] X\`)` → `logger.info({ component: 'worktree' }, 'X')` via codemod or hand-edit. |
| 159 | +3. ast-grep replace pattern: |
| 160 | + ``` |
| 161 | + console.log(`[$NAME] $MSG`) → logger.info({ component: '$NAME' }, `$MSG`) |
| 162 | + ``` |
| 163 | + (Note: needs `.toLowerCase()` on $NAME or post-fix.) |
| 164 | + |
| 165 | +--- |
| 166 | + |
| 167 | +## Recommendation |
| 168 | + |
| 169 | +**Do not approve until REJECT-1, REJECT-2, REJECT-3 land.** Total effort: **~1 hour**. |
| 170 | + |
| 171 | +REJECT-4 (console.log) is technically a guardrail violation but is **low risk in a Tauri-bundled sidecar** (logs go to stderr regardless). Recommend either: |
| 172 | +- (a) Fix in this audit cycle for clean compliance (~1h), or |
| 173 | +- (b) File as a P2 follow-up ticket with explicit waiver — pino infrastructure already exists, so the migration is mechanical and doesn't change behaviour. |
| 174 | + |
| 175 | +Everything else lines up with the plan. Schema migrated, security holes closed, brand identity shipped, landing truthful, 48/48 task commits visible in `git log`, evidence trail complete (62 files for 48 tasks). |
| 176 | + |
| 177 | +After REJECT-1/2/3 fixes, re-run: |
| 178 | +```bash |
| 179 | +grep -rn "localStorage.getItem('token')" apps/desktop-ui/components/ apps/desktop-ui/app/ # expect 0 |
| 180 | +grep -rn "as any\|@ts-ignore" apps/api/src apps/sidecar/src apps/desktop-ui/{lib,components,hooks,providers,app} # expect 0 |
| 181 | +pnpm --filter @openlinear/desktop-ui typecheck # expect exit 0 |
| 182 | +``` |
| 183 | + |
| 184 | +Then this verdict flips to **APPROVE**. |
0 commit comments