diff --git a/.claude/agents/product-manager.md b/.claude/agents/product-manager.md index 7a780d7328..18aa0ac246 100644 --- a/.claude/agents/product-manager.md +++ b/.claude/agents/product-manager.md @@ -1,6 +1,6 @@ --- name: product-manager -description: Interrogates a feature idea or bug report and returns executable ticket material for /ticket, including how many tickets it should become. Knows the parity contract, the append-only DTO rule, deploy-API-first, and the 6.2 ticket template. Read-only; it shapes work, it never implements. +description: Interrogates a feature idea or bug report and returns executable ticket material for /ticket, including how many tickets it should become. Runs the eight-category edge-case pass on every request. Knows the parity contract, the append-only DTO rule, deploy-API-first, and the 6.2 ticket template. Read-only; it shapes work, it never implements. tools: Glob, Grep, Read model: inherit effort: high @@ -18,7 +18,17 @@ Return, as structured text: 2. **Affected surface inventory**: routes (web + mobile pair), endpoints, DTOs, i18n namespaces. Name the parity consequence explicitly: web + mobile in one ticket (parity:yes), or the adapter-only exemption with its justification. -3. **The ticket split**: each ticket one repo and one coherent, independently mergeable PR. +3. **The edge-case pass**: walk the proposed behaviour through eight categories, always + in this order — user types, contexts of use, unexpected inputs and system failures, + user error, feature interactions, load, security and privacy, accessibility. The fixed + order exists because it reaches the categories every author forgets. For each category, + name the scenarios where the request is silent and a headless worker would invent the + behaviour; a spec gap no longer becomes a reviewer's question, it ships as confident + wrong code. Every surviving scenario states the expected behaviour in one line and + lands in the owning ticket's Expected behaviour or Test scenarios. A scenario with no + defensible default becomes an open question (section 6). State an empty category as + empty; never skip one silently. +4. **The ticket split**: each ticket one repo and one coherent, independently mergeable PR. Prefer small tickets when behavior or deployment boundaries are genuinely separable; file and line estimates are advisory and never justify splitting an atomic behavior or its required generated artifacts. Cross-repo work is an api ticket BLOCKING a ui ticket @@ -26,11 +36,11 @@ Return, as structured text: fields, never rename/remove/retype what shipped mobile clients still read. Never propose: a tests-only ticket, a foundation ticket of unused code, or a migration split from its feature. -4. **Per ticket, the 6.2 material**: problem, scope, out-of-scope, expected behaviour, +5. **Per ticket, the 6.2 material**: problem, scope, out-of-scope, expected behaviour, technical details with file paths you VERIFIED exist, acceptance criteria (each one checkable by a machine or a screenshot), test scenarios, rollout/kill-switch where risk exists, events/metrics where success needs measuring. -5. **Open questions**: ONLY genuine forks a codebase read cannot answer, each with your +6. **Open questions**: ONLY genuine forks a codebase read cannot answer, each with your recommended answer first. An empty list is a good list. Hard rules: never invent a file path (verify with Read/Glob before citing); never diff --git a/.claude/skills/audit-security/checklist.md b/.claude/skills/audit-security/checklist.md index cef19ce094..b774b70374 100644 --- a/.claude/skills/audit-security/checklist.md +++ b/.claude/skills/audit-security/checklist.md @@ -120,6 +120,15 @@ Every finding cites a file:line and a **threat** (who reaches it, what they get) tool beyond the user's own data or trigger an unbounded loop. The cost/iteration ceiling is server-enforced, not prompt-enforced. - [ ] **Model output isn't reflected as trusted HTML/markup** without escaping. +- [ ] **A self-harm or crisis disclosure gets a crisis response**: the Astra system + prompt or a server-side check routes self-harm text to a supportive reply that names + crisis resources for the user's locale (BR: CVV 188, US: 988), never to a productivity + nudge. This is Orbit's own product-safety bar for its audience, and no handling + anywhere in `orbit-api` is **Tier 1** by that policy. Companion-chatbot statutes + (California SB 243, in force 2026) are conditional context only: whether Astra is a + covered companion chatbot rather than a productivity-only bot is a legal call, and + the statute's duty is a published protocol with crisis referral, not this exact + implementation — cite it as context, never as the severity basis. ## G. Error handling & data exposure @@ -163,13 +172,16 @@ Every finding cites a file:line and a **threat** (who reaches it, what they get) ## J. Legal & data-handling posture -> Collecting user data puts Orbit under GDPR/CCPA at any scale. These four are -> repo-checkable; the policy's legal wording is not, and is not a finding. +> Collecting user data puts Orbit under GDPR/LGPD/CCPA at any scale, and selling a +> subscription adds consumer-protection exposure (FTC and state equivalents). Every item +> here is repo- or live-checkable; the policy's legal wording is not, and is not a finding. - [ ] **A privacy policy is reachable and linked** from the app and the landing page, and the processors it names match the ones the code actually calls (Supabase/Render region, - Stripe, PostHog US Cloud, Sentry, Resend, Firebase). A policy that contradicts the real - processor list is **Tier 2**. + Stripe, PostHog US Cloud, Sentry, Resend, Firebase, OpenAI). The AI flow is named in + plain terms — user text reaches OpenAI and the policy says so; state AI-disclosure laws + make silence its own violation. A policy that contradicts the real processor list is + **Tier 2**. - [ ] **Account deletion and data export are user-reachable**, not a manual DB action (GDPR erasure + portability). Missing a user-facing delete path is **Tier 2**. - [ ] **Third-party data flows are intentional**: analytics, crash, and log payloads carry @@ -178,6 +190,37 @@ Every finding cites a file:line and a **threat** (who reaches it, what they get) - [ ] **No copyleft contamination in shipped dependencies**: a GPL/AGPL package under `apps/*` or a NuGet reference in a shipped project forces source disclosure. Cite the package and its license (**Tier 2**). +- [ ] **Object storage is private and deletion is complete**: every Supabase storage + bucket (`SupabaseObjectStorageService.cs`) rejects anonymous reads — prove it from + outside with the publishable key, like the RLS probe, never by inference. A public + bucket over user content is **Tier 1**. Account deletion removes the user's storage + objects along with the rows; content the policy promises to delete but that survives in + a bucket is **Tier 2**. +- [ ] **Cancellation is easy online and unobstructed**: a subscriber reaches a working + cancel path from the app without contacting support — in-app, or the Play + Subscription Center link the app already opens (`apps/mobile/app/upgrade.tsx`, + `apps/web/components/upgrade/play-billing-dashboard.tsx`), which is compliant. The + finding is obstruction: a dead or hidden path, a forced support email, or friction + added to delay the exit (ROSCA demands a simple mechanism; counting steps against + signup is not the test). An obstructed or missing path is **Tier 2**. +- [ ] **Trial-to-paid conversion notice matches the duty that applies**: derive the + requirement from the trial length, the user's jurisdiction, and the billing channel + before flagging — California's pre-expiry reminder binds free periods over 31 days + (Orbit's trial is 7), and Play's duty is upfront conversion disclosure. Where a + reminder is due, provider-side delivery counts, with proof the customer was actually + notified: an enabled and sent Stripe reminder email, or an Orbit-sent equivalent. + `customer.subscription.trial_will_end` is the webhook TRIGGER for such an email; + receiving or handling the event alone never satisfies the check. Today the trial + holds no card; re-derive when ORB-138 applies the price. A charging trial missing a + notice its duty demands is **Tier 2**. +- [ ] **No invented social proof**: no testimonial, review count, star rating, or + `aggregateRating` structured data inside the supplied audit roots that does not trace + to a real user statement. An invented review is **Tier 1** (the FTC fake-review rule + fines per violation). The landing repository and the live Play store listing sit + OUTSIDE this audit's roots (`audit.mjs` passes only orbit-ui-mobile and orbit-api): + put both in the Deferred ledger every run, never let an empty local grep stand for a + clean result on them. Landing JSON-LD verified clean by hand 2026-08-13; that line is + a snapshot, not coverage. ## Tier 3 — out of scope for this audit (acknowledge, don't itemize as findings) diff --git a/.claude/skills/ticket/SKILL.md b/.claude/skills/ticket/SKILL.md index 00e550c173..daa674ffde 100644 --- a/.claude/skills/ticket/SKILL.md +++ b/.claude/skills/ticket/SKILL.md @@ -39,7 +39,9 @@ Test scenarios prove the new or changed behaviour, and no fabricated red case be ## B. product-manager: how many tickets 1. Spawn `product-manager` with the raw request plus `architecture.json`. It returns the sharpened - problem, the affected surfaces and endpoints, the per-ticket 6.2 material, and a first-cut split. + problem, the affected surfaces and endpoints, the eight-category edge-case scenarios, the + per-ticket 6.2 material, and a first-cut split. A return with no edge-case pass is incomplete; + send it back rather than filling the gap yourself. 2. Spawn `design-specialist` in parallel whenever any surface is user-visible. For a pure chore or harness item with no UI, state that it was skipped and why. 3. **Splitting one message that lists several problems.** Decide which of these three shapes each