diff --git a/.claude/research/harness-upgrade-plan.md b/.claude/research/harness-upgrade-plan.md index bba977398..62d88d6bd 100644 --- a/.claude/research/harness-upgrade-plan.md +++ b/.claude/research/harness-upgrade-plan.md @@ -63,17 +63,18 @@ The generic, one-mechanism fix for "why do you violate instructions" — targets - [x] Added `effort:` to the subagents: `parity-checker`/`i18n-syncer` → `low` (already `model: haiku`); `contract-aligner`/`security-reviewer`/`design-reviewer` → `medium`. **design-reviewer** also got `model: sonnet` — it carried no `model` and would have inherited the Opus driver, which defeats the routing for an auto-firing reviewer; pinning it to sonnet matches its two effort-tier siblings (a reasoned extension of the literal checkbox, flagged). **Lockstepped** the same `effort: medium` onto orbit-api's own (body-drifted) copies of `contract-aligner`/`security-reviewer` so Stage 3 did not widen the drift Stage 6 will dedupe. Frontmatter order follows the Stage-2 `Explore` convention (`model:` then `effort:`). - [x] Converted `/audit-security`, `/audit-tests`, `/audit-performance`, `/audit-code-quality`, and `/prod-readiness` to **dynamic workflows**. New `.claude/workflows/audit.mjs` — ONE generic engine keyed by `kind` (per rule 10 DRY; ≥4 uses justify the abstraction): a **Haiku finder per surface** (reads that audit's rubric/checklist) → a **Haiku default-refuted skeptic** per serious finding (Tier 1/2 or Critical/High) → a **completeness-critic loop-until-dry** (cap 2 dry rounds, 4-round hard cap) → returns verified structured findings + coverage + deferred. New `.claude/workflows/prod-readiness.mjs` — runs the four audit workflows in parallel via one-level `workflow()` nesting (scriptPath form, name-resolution-independent) + the ops-layer fan-out (observability/multi-instance/background-durability/staging; backups always Deferred) + ops skeptic verify. The five skills became **thin non-fork drivers**: resolve scope → invoke `Workflow` (the skill's own instructions are the sanctioned opt-in per the Workflow gate) → **Opus synthesizes** the report from the compact return. **This is the real quota win**: the entire multi-phase fan-out/verify/loop *reasoning* the Opus `context: fork` used to do is now free deterministic JS; Opus spends only on synthesis, every leaf agent is Haiku. Dropped `context: fork` (the driver must be main-loop to call `Workflow`); each skill keeps a **headless/CI fallback** (inline `Explore` fan-out) because the Workflow tool is absent from CI tool-allowlists (verified: only `/pr-review` runs in CI, via `Task`, and the audits run interactively only). Both scripts full-syntax-validated (the `node --check` "illegal return" is a false positive — the Workflow runtime wraps scripts so top-level `return` is legal). Live end-to-end fires on the next real `/audit-*` invocation; the core `agent({model:'haiku', schema})` + file-tool mechanics were smoke-tested via a 1-agent probe workflow. - [x] Confirmed **no `opusplan`** and **no `"model"` pin** in any `.claude` config (grepped) — Opus stays the session driver. `ultrathink`-per-hard-turn is a usage habit, nothing to configure. Model routing lives entirely at the subagent/workflow tier, per the locked decision. +- [x] **Verified live (2026-07-09, Stage-4 session): `/audit-security api` end-to-end.** Found + fixed four issues the conversion carried: (1) **named resolution is unavailable in this build** — `Workflow({name:'audit'})` → "Available: (none)"; repointed all five skills (4 audits + prod-readiness) to the **`scriptPath`** form (prod-readiness.mjs already nested via scriptPath). (2) **Windows `autocrlf=true` gives the working-tree `.mjs` CRLF**, which the Workflow approval dialog rejects as "control characters" — added a `.gitattributes` gate (`.claude/workflows/*.mjs text eol=lf`) + renormalized; durable across re-checkout. (3) **The top-level Workflow tool delivers `args` as a JSON string** (not a parsed object) → `args?.kind` was undefined; both scripts now normalize at the boundary (`typeof args === 'string' ? JSON.parse(args) : args`). (4) **`scope:'api'` under-covered security** — `isApiSurface` inferred repo from a substring match on each surface's `where`, so injection/secrets/rate-limit/error-leakage (all real API surfaces) were dropped and the completeness critic expensively regenerated them (dragging in Tier-3 noise: GDPR/CVE-scan/monitoring). Fixed with explicit `repos: 'api'|'ui'|'both'` tags on the security surfaces + a tier-calibrated, ≤6-gap critic prompt. (5) **The adversarial skeptic pass was silently bypassed for ~all findings** — the Haiku finders emit `CRITICAL/HIGH/MEDIUM` severities, but `isSerious` gated verification on the security ladder's `Tier 1/Tier 2` strings via exact set-membership, so the default-refuted verify never fired on the 21 "CRITICAL"/26 "HIGH" findings. Fixed `isSerious` to be `rank()`-based (vocabulary-agnostic — `rank()` already normalizes both `CRITICAL`↔`Tier 1`; validated by construction, no costly re-run) and deleted the now-dead `KIND.*.verify` config. The re-run itself completed (43 agents, 0 errors, 4 critic rounds, 184 raw findings) and validated the full plumbing; its findings are UNVERIFIED (produced pre-fix), so `.claude/audits/security-api.md` is written as an explicit **smoke-test validation artifact**, not an authoritative report — a clean re-run is the source of truth. The other three kinds classify correctly via their `api-` label prefixes (unaffected). ## Stage 4 — Proactive standards + second opinion + investigate (one session, may split) -- [ ] `~/.claude/rules/` — unscoped rule(s) for proactive tooling defaults (the rule-tier sibling of the Stage-1 guard). This is the mechanism for "always use tool X" without repeating it. -- [ ] **`/investigate` skill:** Sentry issue → Render deploy/logs → Postgres rows → localize to code (needs `csharp-lsp` from Stage 2) → root cause + minimal fix, gated. A runbook skill (Anthropic keeps these). Consider running it on Fable. -- [ ] **`/second-opinion` skill (deferred until opencode is back):** `opencode run --model /glm-5.2 --format json ""`. Wire it **inside `/pr-review`** so it auto-fires on each Critical finding that survives the skeptic; if GLM disagrees, mark `CONTESTED` and surface both verdicts (do not force a merge). Confirm the exact GLM provider slug from opencode's catalog at build time. +- [x] `~/.claude/rules/` — proactive tooling defaults. Confirmed (claude-code-guide, cited docs) that `.claude/rules/*.md` is a **native auto-load** — files load at session start, survive compaction, and re-inject post-compaction; unscoped (no `paths` frontmatter) = loads unconditionally. **No loader hook needed.** Wrote `~/.claude/rules/tooling-defaults.md` — the "always reach for tool X" defaults for the live servers: Sentry MCP (error triage), Render MCP (deploys/logs/metrics, workspace-select-first guardrail), `query_render_postgres` (read-only data), csharp-lsp (orbit-api C# nav), `gh` CLI (issues/PRs/CI). Pairs with, does not duplicate, the Stage-1 proactivity reminder (disposition) — this names the concrete servers. +- [x] **`/investigate` skill** — `.claude/skills/investigate/SKILL.md`. Gated runbook: Sentry issue → Render deploy/logs → Postgres rows (read-only) → localize via csharp-lsp → root cause → **minimal fix behind a human gate** (read-only until the gate). Uses the real MCP servers (verified live this session: Sentry org `thomasluizon` ✓, Render MCP ✓ — needs a workspace select, encoded in the runbook). **No `model:` pin — inherits the session model (Opus 4.8 at `xhigh`).** (Skills *can* pin a model via frontmatter; deliberately not here.) Rationale: incident root-cause is low-frequency, high-stakes judgment — model routing pays off on high-*volume* cheap work (the Haiku audit leaves, 40+/run), not a once-per-incident runbook, and a wrong diagnosis costs time even behind the human gate. Dropping the pin also permanently removes the model-churn fragility that surfaced when the first `claude-fable-5` pin met Fable's **2026-07-12 disablement** (a pinned-to-dead-model skill errors or silently falls back). The runbook's value is the gated Sentry→Render→Postgres→csharp-lsp structure + the human gate, not a model tier. (Briefly repointed Fable→Sonnet-5 before landing on drop-the-pin.) +- [ ] **`/second-opinion` skill — UNBLOCKED (opencode is back).** opencode is now installed (`v1.14.48`, verified this session) — the "deferred until opencode is back" condition is void. Held for a follow-up session per this session's scope (rules + investigate). When built: `opencode run --model /glm-5.2 --format json ""`, wired **inside `/pr-review`** to auto-fire on each Critical finding that survives the skeptic; GLM disagreement → mark `CONTESTED`, surface both verdicts (never force a merge). Confirm the exact GLM provider slug from opencode's catalog at build time. ## Stage 5 — Workflow docs (one session) -- [ ] `WORKFLOW.md`: add the **slice-vs-campaign router** (campaign = looped / multi-session / externally-tracked, e.g. #243 → workflows + fresh sessions; slice = `/execute`) and the **plan→implement fresh-session split** rule for cross-repo/risky work (persist plan file → `/clear` → fresh `/prime` + `/implement `). -- [ ] Document when to use `/execute` vs the manual split; both stay. +- [x] `WORKFLOW.md`: added the **slice-vs-campaign router** as the top-level Step 1 (litmus: converging-metric finish line → campaign; known diff → slice), a **Campaign pattern** section (assess via workflow → fix a batch via `/implement` → PR → `/clear` → re-run, fresh sessions; the #243 worked example), and the **plan→implement fresh-session split** rule for cross-repo/risky slices. Kept the four slice paths intact. Landed in ui#438 (bundled into the Stage-4 PR per the minimum-PRs preference, not a separate PR). +- [x] Documented `/execute` vs the split: `/execute` stays the **default for slices**; the split is for cross-repo/risky slices; the campaign loop is for converging-metric work. Both `/execute` and the manual path remain. (Triggered by the user's "I was used to running `/execute` for everything" — the doc now teaches when NOT to.) ## Stage 6 — Drift hygiene (one session) diff --git a/.claude/skills/audit-code-quality/SKILL.md b/.claude/skills/audit-code-quality/SKILL.md index 02a494324..47a82741d 100644 --- a/.claude/skills/audit-code-quality/SKILL.md +++ b/.claude/skills/audit-code-quality/SKILL.md @@ -69,10 +69,10 @@ executes §1/§2/§3; you emit the Verify summary + Deferred ledger (§4/§5). Invoke the `Workflow` tool (this skill's instructions are the opt-in): ``` -Workflow({ name: 'audit', args: { kind: 'code-quality', scope: '' } }) +Workflow({ scriptPath: '.claude/workflows/audit.mjs', args: { kind: 'code-quality', scope: '' } }) ``` -(If `name` does not resolve, use `scriptPath: '.claude/workflows/audit.mjs'`.) +(`scriptPath` is canonical — named workflow resolution is not available in this Claude Code build.) It fans out **one Haiku finder per area** — `apps/web` · `apps/mobile` · `packages/shared` · `orbit-api/src/Orbit.Application` · `orbit-api/src/{Orbit.Domain,Orbit.Infrastructure, diff --git a/.claude/skills/audit-performance/SKILL.md b/.claude/skills/audit-performance/SKILL.md index 2828e085e..7a1a444da 100644 --- a/.claude/skills/audit-performance/SKILL.md +++ b/.claude/skills/audit-performance/SKILL.md @@ -64,10 +64,10 @@ emit the Verify summary + Deferred ledger (§4/§5). Invoke the `Workflow` tool (this skill's instructions are the opt-in): ``` -Workflow({ name: 'audit', args: { kind: 'performance', scope: '' } }) +Workflow({ scriptPath: '.claude/workflows/audit.mjs', args: { kind: 'performance', scope: '' } }) ``` -(If `name` does not resolve, use `scriptPath: '.claude/workflows/audit.mjs'`.) +(`scriptPath` is canonical — named workflow resolution is not available in this Claude Code build.) It fans out **one Haiku finder per slice** — `api-queries` (N+1, index coverage vs the migrations) · `api-requestpath` (sync slow work, blocking async, over-fetch, AsNoTracking) · diff --git a/.claude/skills/audit-security/SKILL.md b/.claude/skills/audit-security/SKILL.md index ee21683db..9d1f533a6 100644 --- a/.claude/skills/audit-security/SKILL.md +++ b/.claude/skills/audit-security/SKILL.md @@ -79,10 +79,10 @@ return (§4/§5). Invoke the `Workflow` tool (this skill's instructions are the opt-in): ``` -Workflow({ name: 'audit', args: { kind: 'security', scope: '' } }) +Workflow({ scriptPath: '.claude/workflows/audit.mjs', args: { kind: 'security', scope: '' } }) ``` -(If `name` does not resolve, use `scriptPath: '.claude/workflows/audit.mjs'`.) +(`scriptPath` is canonical — named workflow resolution is not available in this Claude Code build.) It fans out **one Haiku finder per attack surface** — authz & data-isolation · AI/MCP tool scoping · injection · secrets & config · rate-limit & AI-abuse · error-leakage & web/mobile diff --git a/.claude/skills/audit-tests/SKILL.md b/.claude/skills/audit-tests/SKILL.md index b6098faf8..b0e11d49a 100644 --- a/.claude/skills/audit-tests/SKILL.md +++ b/.claude/skills/audit-tests/SKILL.md @@ -83,10 +83,10 @@ the workflow tags it **Critical**. Invoke the `Workflow` tool (this skill's instructions are the opt-in): ``` -Workflow({ name: 'audit', args: { kind: 'tests', scope: '' } }) +Workflow({ scriptPath: '.claude/workflows/audit.mjs', args: { kind: 'tests', scope: '' } }) ``` -(If `name` does not resolve, use `scriptPath: '.claude/workflows/audit.mjs'`.) +(`scriptPath` is canonical — named workflow resolution is not available in this Claude Code build.) It fans out **one Haiku finder per suite area** — `web` · `mobile` · `shared` · `api-application` · `api-domain` — each scoring tests against `rubric.md` (Behavior / Edge / diff --git a/.claude/skills/investigate/SKILL.md b/.claude/skills/investigate/SKILL.md new file mode 100644 index 000000000..d7fc29120 --- /dev/null +++ b/.claude/skills/investigate/SKILL.md @@ -0,0 +1,84 @@ +--- +name: investigate +description: Root-cause a production incident end to end — pull the Sentry issue, correlate it to a Render deploy and its runtime logs, inspect the Postgres rows it implicates, localize the fault to code via csharp-lsp, then propose the minimal fix behind a human gate. Use when a production error, crash, Sentry alert, or "why is X broken in prod" needs diagnosing. Not for a local dev bug you already have a reproducible stack trace for — just read the code. +argument-hint: +--- + +# Investigate: Production Incident Root-Cause + +**Input**: $ARGUMENTS + +Diagnose a live incident against the real systems — Sentry, Render, Postgres, and the code — and hand back a root cause plus the **minimal** fix. Every claim is pinned to evidence a tool returned; nothing is guessed. + +## Operating rules + +- **Read-only until the gate.** Phases 0–5 only *inspect* (Sentry, Render logs, a Postgres `SELECT`, code navigation). No edits, no data mutation. +- **Human gate before Phase 6.** Present the root cause + proposed fix and **STOP**. Do not touch a file until I approve. (Autonomy within the investigation; a gate before the change.) +- **Root cause, not symptom** (CLAUDE.md rule 1). Name the upstream cause — a missing validator, a nullable that should not be, contract drift, a migration gap. No workaround that masks it. +- **Verify, don't guess.** Each finding traces to a Sentry event, a Render log line, a DB row, or a code line. If a step's tool is unreachable, say so and continue — don't invent its output. + +## Phase 0 — Frame the incident + +Parse `$ARGUMENTS`: +- A Sentry issue id / short-id / URL → go straight to it. +- A description ("signup 500s", "widget crash on ColorOS") → search Sentry for the matching issue. + +Confirm the Sentry org is `thomasluizon` and identify the project (api / web / mobile). + +## Phase 1 — Sentry: what broke + +- `mcp__sentry__search_issues` (or `mcp__sentry__get_sentry_resource` with the id) → exception type, culprit, level, first/last seen, event count, affected releases + environments. +- Pull a representative event → full stack trace, breadcrumbs, request context, tags, affected-user data. +- Optionally `mcp__sentry__analyze_issue_with_seer` for an AI root-cause hypothesis — treat it as a lead to verify against evidence, never as fact. + +**Capture:** the exact exception + message, the top **in-app** stack frame (file:line), the release it started in, and how often / who it hits. + +## Phase 2 — Render: what changed and what the runtime saw + +- Ensure a Render workspace is selected: `mcp__render__get_selected_workspace`; if none, `list_workspaces` → **confirm the choice with me** → `select_workspace`. Never auto-pick. +- `list_services` → the orbit-api service. `list_deploys` / `get_deploy` → find the deploy whose window brackets the issue's first-seen. Did the error start right after a deploy? Which commit? +- `list_logs` across the incident window for that service → the runtime lines around the Sentry event timestamps (log context Sentry may lack). + +**Capture:** the suspect deploy + commit if the error is deploy-correlated (or "not deploy-correlated"), and any runtime log detail the Sentry event is missing. + +## Phase 3 — Postgres: what the data says (only if the fault implicates data) + +- `mcp__render__query_render_postgres` — **read-only**. Inspect the rows the stack trace implicates: the offending record, an unexpected null, a violated constraint, a duplicate, the affected user's state. +- Never `UPDATE` / `DELETE` / `INSERT`. Confirm or kill the data hypothesis with a `SELECT`. + +**Capture:** the row-level evidence, or "data not implicated." + +## Phase 4 — Localize to code + +- **orbit-api (C#) → csharp-lsp.** From the top in-app frame: `mcp__csharp-lsp__find_symbol` → the method; `find_callers` / `find_references` → how it is reached with the bad input; `get_diagnostics` on the file; `get_type_hierarchy` / `find_implementations` when the frame is an interface / virtual dispatch. +- **web / mobile (TS) →** Grep/Read the implicated `apps/web` or `apps/mobile` module and its shared types in `packages/shared`. + +**Capture:** the exact repo/path:line of the fault and the code path that triggers it. + +## Phase 5 — Root cause + +One tight paragraph: the exact line + the condition that triggers it + why the deploy / data / input produced it *now*. Point at the upstream cause, not the thrown symptom. + +## Phase 6 — Propose the minimal fix — THEN STOP (human gate) + +Present, and wait for my approval before any edit: + +- **Root cause** — one paragraph. +- **Minimal fix** — file:line → the smallest correct change that removes the cause (not a defensive branch around it). Flag: cross-platform parity if it is a UI/shared change; backend-source-of-truth if it is validation; the append-only contract if it is a DTO. +- **Regression test** — the test that would have caught this, to add with the fix. +- **Blast radius / verification** — what to run after (`/validate`, a targeted test), and what else the change touches. + +On approval: implement with parity + the regression test, then hand to `/validate` (or `/pr-review`). Do not edit before approval. + +## Output — the incident dossier (rendered before the gate) + +``` +## Incident: {exception} ({sentry-short-id}) + +- **Seen**: first {…} · last {…} · {N} events · {environment} · {who} +- **Deploy correlation**: {commit / deploy} — or "not deploy-correlated" +- **Data**: {row evidence} — or "not implicated" +- **Fault**: {repo}/{path}:{line} — {the trigger path} +- **Root cause**: {one paragraph} +- **Proposed fix (awaiting approval)**: {file:line → change} + {regression test} + {verification} +``` diff --git a/.claude/skills/prod-readiness/SKILL.md b/.claude/skills/prod-readiness/SKILL.md index 6185fce4e..8e4cb416c 100644 --- a/.claude/skills/prod-readiness/SKILL.md +++ b/.claude/skills/prod-readiness/SKILL.md @@ -86,10 +86,10 @@ Parse `$ARGUMENTS` into one `{scope}` token, forwarded to every child audit: Invoke the `Workflow` tool (this skill's instructions are the opt-in): ``` -Workflow({ name: 'prod-readiness', args: { scope: '' } }) +Workflow({ scriptPath: '.claude/workflows/prod-readiness.mjs', args: { scope: '' } }) ``` -(If `name` does not resolve, use `scriptPath: '.claude/workflows/prod-readiness.mjs'`.) +(`scriptPath` is canonical — named workflow resolution is not available in this Claude Code build.) It runs the four `audit` workflows in parallel (each self-caps its own fan-out + verify + loop), then fans out the **ops-layer** checks — observability · multi-instance · background diff --git a/.claude/workflows/audit.mjs b/.claude/workflows/audit.mjs index 64572a690..f87117359 100644 --- a/.claude/workflows/audit.mjs +++ b/.claude/workflows/audit.mjs @@ -73,22 +73,20 @@ const CRITIC_SCHEMA = { const KIND = { security: { ladder: 'Tier 1 (must fix — exploitable now, real blast radius) / Tier 2 (should fix before launch) / Tier 3 (enterprise — OUT of scope, mention once)', - verify: ['tier 1', 'tier 2'], rationale: 'threat — who reaches it (other user / anon / forged webhook / crafted prompt) and what they get', checklist: `${UI}\\.claude\\skills\\audit-security\\checklist.md`, extra: 'For cross-user access, PROVE the query is NOT scoped to the caller userId by citing the exact line. Payment/webhook handlers: verify the signature check exists. Secrets: cite the committed line. AI/MCP tools must derive userId from the session and accept no target-user parameter.', surfaces: [ - { label: 'authz-isolation', where: 'orbit-api controllers + every CQRS query/command handler in src/Orbit.Application — each must scope its query by the authenticated userId (from the JWT, never a client-controlled field)', sections: 'A' }, - { label: 'ai-mcp-scoping', where: 'the agent/MCP tool handlers in orbit-api (execute_agent_operation_v2, bulk_delete_habits, bulk_log_habits, delete_goal, manage_account, and the per-entity mutators) — each must resolve the caller userId and cannot touch another user rows', sections: 'A, F' }, - { label: 'injection', where: 'raw or interpolated SQL/EF, dangerouslySetInnerHTML (web), Process.Start, path building from user input', sections: 'B' }, - { label: 'secrets-config', where: 'hardcoded keys / JWT secrets / connection strings, .env-shaped values in source, debug flags, security headers + CORS in Program.cs and the Extensions', sections: 'C, D' }, - { label: 'ratelimit-ai-abuse', where: 'rate-limit coverage on auth (send-code / verify-code), password-reset, and the AI/chat endpoints; request-size limits; prompt-injection and unbounded-cost paths in the AI flow', sections: 'E, F' }, - { label: 'error-web-auth', where: 'stack traces / DB schema leaked in API responses; web auth cookie flags (httpOnly + sameSite strict + secure); mobile token storage (SecureStore, never AsyncStorage)', sections: 'G, H' }, + { label: 'authz-isolation', repos: 'both', where: 'orbit-api controllers + every CQRS query/command handler in src/Orbit.Application — each must scope its query by the authenticated userId (from the JWT, never a client-controlled field)', sections: 'A' }, + { label: 'ai-mcp-scoping', repos: 'api', where: 'the agent/MCP tool handlers in orbit-api (execute_agent_operation_v2, bulk_delete_habits, bulk_log_habits, delete_goal, manage_account, and the per-entity mutators) — each must resolve the caller userId and cannot touch another user rows', sections: 'A, F' }, + { label: 'injection', repos: 'both', where: 'raw or interpolated SQL/EF, dangerouslySetInnerHTML (web), Process.Start, path building from user input', sections: 'B' }, + { label: 'secrets-config', repos: 'both', where: 'hardcoded keys / JWT secrets / connection strings, .env-shaped values in source, debug flags, security headers + CORS in Program.cs and the Extensions', sections: 'C, D' }, + { label: 'ratelimit-ai-abuse', repos: 'both', where: 'rate-limit coverage on auth (send-code / verify-code), password-reset, and the AI/chat endpoints; request-size limits; prompt-injection and unbounded-cost paths in the AI flow', sections: 'E, F' }, + { label: 'error-web-auth', repos: 'both', where: 'stack traces / DB schema leaked in API responses; web auth cookie flags (httpOnly + sameSite strict + secure); mobile token storage (SecureStore, never AsyncStorage)', sections: 'G, H' }, ], }, tests: { ladder: 'Critical (untested critical path) / High (happy-path-only or rubber-stamp on a critical path) / Medium (missing edge/failure off the critical path)', - verify: ['critical', 'high'], rationale: 'what a real behavior break this test would NOT catch', checklist: `${UI}\\.claude\\skills\\audit-tests\\rubric.md`, extra: 'Judge tests by what they would FAIL on, never by count/coverage. For each test decide if it covers a critical path (auth, billing/subscription, AI/MCP tools, data-isolation, timezone/dates, validation) and score Behavior/Edge/Failure. Flag happy-path-only, rubber-stamp (asserts a mock was called / tautological / assertion-free), over-mocked (the unit never runs), implementation-coupled (asserts private state or call order). For each gap write the CONCRETE missing test — name + arrange/act/assert + the real factory from packages/shared/src/__tests__/factories.ts. Unit-tests-only policy: flag any integration/E2E/real-DB harness as out-of-policy, do not reward it.', @@ -102,7 +100,6 @@ const KIND = { }, performance: { ladder: 'High (degrades with scale — fix before it bites) / Medium (measurable but bounded) / Low or Info (micro, or only-at-enterprise-scale — noted, not prioritized)', - verify: ['critical', 'high'], rationale: 'impact — how it scales, concrete (e.g. "50-habit user → 50 round-trips")', checklist: `${UI}\\.claude\\skills\\audit-performance\\SKILL.md`, extra: 'Flag ONLY patterns that degrade quadratically/linearly with data or traffic: N+1 queries (missing .Include / projecting after materializing), missing index on a hot Where/OrderBy/FK, unbounded list rendered in full, sync slow work (HTTP/AI/email/push) inline in a request path, blocking async (.Result/.Wait), IQueryable materialized too early, missing AsNoTracking on hot reads; frontend render thrash, bundle bloat, over-eager or stale caching, waterfalls. CONFIRM every index claim against the EF migrations (read them, cite the migration). Do NOT micro-optimize, do NOT over-prescribe memoization/virtualization, do NOT list enterprise-only tuning (note once).', @@ -115,7 +112,6 @@ const KIND = { }, 'code-quality': { ladder: 'Critical / High / Medium / Low / Info (a deep audit KEEPS Low/Info — the sanctioned rubric exception — but bucket them separately)', - verify: ['critical', 'high'], rationale: 'the rubric dimension it breaks and why it is real debt', checklist: `${UI}\\.claude\\skills\\pr-review\\rubric.md`, extra: 'Hunt dead/stale code and PROVE each with a zero-reference grep (cite the command and its empty result — never guess). Flag SOLID/clean-arch (functions over the ~50-line soft cap / ~100 hard cap, nesting past ~3), premature abstraction, DRY-at-the-wrong-level, comment-policy breaks (fix is rename-the-symbol or extract, never reword), naming (data/info/temp/helper/util as final names, abbreviations), and DESIGN.md drift on apps/* UI files only. Rank by blast-radius × churn — a smell in a hot handler outranks the same in a stable leaf. Do NOT re-derive security/contract findings (owned by /audit-security and /pr-review).', @@ -132,7 +128,7 @@ const KIND = { const EXCLUDE = 'Exclude generated/vendored dirs (node_modules, .next, dist, build, bin, obj, coverage, .turbo, Migrations/ except when reading them to confirm an index, design/handoff/).' const isApiSurface = (s) => s.label.startsWith('api-') || /orbit-api/.test(s.where) -const isUiSurface = (s) => !isApiSurface(s) +const surfaceRepos = (s) => s.repos || (isApiSurface(s) ? 'api' : 'ui') function scopeLabelFor(scope) { if (!scope || scope === 'both') return 'both repos' @@ -144,8 +140,8 @@ function scopeLabelFor(scope) { function resolveSurfaces(kind, scope) { const all = KIND[kind].surfaces if (!scope || scope === 'both') return all - if (['api', 'backend'].includes(scope)) return all.filter(isApiSurface) - if (['ui', 'web', 'mobile', 'frontend'].includes(scope)) return all.filter(isUiSurface) + if (['api', 'backend'].includes(scope)) return all.filter((s) => ['api', 'both'].includes(surfaceRepos(s))) + if (['ui', 'web', 'mobile', 'frontend'].includes(scope)) return all.filter((s) => ['ui', 'both'].includes(surfaceRepos(s))) return all.map((s) => ({ ...s, where: `${s.where} — but ONLY within the path "${scope}"` })) } @@ -177,6 +173,7 @@ function criticPrompt(kind, scope, sweptLabels, count) { `Completeness critic for the ${kind} audit of ${scopeLabelFor(scope)}.`, `Surfaces swept so far: ${sweptLabels.join(', ')} — producing ${count} findings.`, `What did this audit NOT examine — a surface never swept, a file/handler/route skipped, or a claim left unverified (a dead-code grep not run, a userId scope unchecked, an index-in-migration unconfirmed, a critical-path test unmapped)?`, + `Stay strictly within this audit's calibration — ${KIND[kind].ladder}. Do NOT propose gaps outside the in-scope tiers (for security, enterprise/Tier-3 controls — GDPR/SOC2, dependency-CVE scanning, SIEM/attack-monitoring — are deliberately out of scope; for tests, coverage-percentage). Propose at most 6 gaps, highest-value first.`, `Return gaps as {label, prompt}, where prompt is a ready-to-run finder objective for that gap (same finding shape as the finders). Return an EMPTY gaps array if coverage is genuinely complete — do not invent gaps.`, ].join('\n') } @@ -198,12 +195,12 @@ const countBy = (findings) => { return out } -const kind = args?.kind -const scope = args?.scope || 'both' +const parsedArgs = typeof args === 'string' ? JSON.parse(args) : args || {} +const kind = parsedArgs.kind +const scope = parsedArgs.scope || 'both' if (!KIND[kind]) throw new Error(`audit workflow: unknown kind "${kind}" (expected security | tests | performance | code-quality)`) const cfg = KIND[kind] -const verifySet = new Set(cfg.verify) -const isSerious = (f) => verifySet.has((f.severity || '').toLowerCase().trim()) +const isSerious = (f) => rank(f.severity) <= 1 const seen = new Set() const dedupeFresh = (findings) => { @@ -259,7 +256,7 @@ log(`verified: ${kept.length} kept · ${capped.length} deferred (cap)`) phase('Complete') let round = 0 let dry = 0 -const maxDry = args?.loop?.maxDryRounds ?? 2 +const maxDry = parsedArgs.loop?.maxDryRounds ?? 2 while (dry < maxDry && round < HARD_ROUNDS) { round += 1 const critic = await agent(criticPrompt(kind, scope, sweptLabels, kept.length), { label: `critic:round-${round}`, phase: 'Complete', model: 'haiku', effort: 'low', schema: CRITIC_SCHEMA }) diff --git a/.claude/workflows/prod-readiness.mjs b/.claude/workflows/prod-readiness.mjs index 2b3dacbcd..dd01745fe 100644 --- a/.claude/workflows/prod-readiness.mjs +++ b/.claude/workflows/prod-readiness.mjs @@ -95,7 +95,8 @@ const rank = (s) => { } const isSeriousOps = (f) => /blocker|high/i.test(f.severity || '') -const scope = args?.scope || 'both' +const parsedArgs = typeof args === 'string' ? JSON.parse(args) : args || {} +const scope = parsedArgs.scope || 'both' const AUDIT_KINDS = ['security', 'tests', 'performance', 'code-quality'] phase('Audits') diff --git a/.gitattributes b/.gitattributes index 2c120e090..57ddc4544 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1 +1,2 @@ packages/shared/src/types/__generated__/*.generated.ts text eol=lf +.claude/workflows/*.mjs text eol=lf diff --git a/.gitignore b/.gitignore index 7722380fe..73a088245 100644 --- a/.gitignore +++ b/.gitignore @@ -21,6 +21,7 @@ dist/ .claude/plans/ .claude/reviews/ .claude/reports/ +.claude/audits/ .mcp.json *.swp *.swo diff --git a/WORKFLOW.md b/WORKFLOW.md index a3f3a44d9..8ab617c84 100644 --- a/WORKFLOW.md +++ b/WORKFLOW.md @@ -1,16 +1,26 @@ # Workflow — Picking a Path -When you have a feature idea or bug fix, pick the lightest path that fits. The PIV loop scales down. +Pick the lightest path that fits. Two questions, in order: **(1) slice or campaign?** then **(2) which slice tier?** Most work is a slice — `/execute` is still the default. Campaigns are the shape for big, looped work. -## Paths +## Step 1 — Slice or campaign? + +| | **Slice** | **Campaign** | +|---|---|---| +| **What** | A bounded change you can land in one focused pass — a bug, a feature, even a multi-story feature. You can name the finish line up front. | Big, **looped**, multi-session work: run → assess → fix → re-run until a bar is met. You *can't* name the finish line — it's "until the audit is clean" / "until coverage hits 100%." | +| **Examples** | fix a timezone bug; add streak-freeze; a 3-story feature | #243 (final gate: loop `/prod-readiness` + Sonar-to-zero); a repo-wide audit-and-remediate; a large migration | +| **How** | `/execute` + the slice ladder below | the **Campaign pattern** below — workflows to assess, `/implement` to fix, across **fresh sessions**. Never one `/execute`. | + +**Litmus test:** if the finish line is a *converging metric* ("0 findings", "100% coverage") rather than a *known diff*, it's a campaign. + +## Step 2 (slices) — the path ladder ### Tiny bug (1 file, you know the fix) -Just edit + `/validate`. No PRD, no plan, no issue. +Just edit + `/validate`. No PRD, plan, or issue. ### Real bug or small feature (one vertical slice, clear scope) -Driven shortcut: `/execute ` chains `/prime` (context load only) → `grill-me` → `/plan` → [confirm plan] → `/implement`, with a hard, blocking gate at every stage boundary. The manual steps below remain available if you'd rather drive each one yourself. +`/execute ` chains `/prime` (context load) → `grill-me` → `/plan` → [confirm plan] → `/implement`, with a hard blocking gate at every boundary. Or drive the steps yourself: 1. `/prime` — load context (add `` if you opened one) 2. `/plan "short description"` — get a plan file @@ -19,28 +29,52 @@ Driven shortcut: `/execute ` chains `/prime` (context load only) → `gr ### Medium / large feature (multiple stories, spans repos) -Driven shortcut: `/feature ""` chains grill → `/create-prd` (or `/prd-interactive` with `--cold`) → [confirm PRD] → `/create-stories`, gated so nothing is created without approval. The manual steps below remain available if you'd rather drive each one yourself. +`/feature ""` chains grill → `/create-prd` (or `/prd-interactive --cold`) → [confirm PRD] → `/create-stories`, gated. Then per issue: `/prime <#>` → `/plan <#>` → `/implement ` → `/pr-review`. + +### Multi-issue path (parallelize 2+ issues) + +`/prime ` → `/plan ` → `/implement ` — the harness makes paired worktrees (mobile + orbit-api) under `.claude/worktrees/` and runs the loop per issue, **3 concurrent max**; excess queues. Two issues touching the same files → conflicts surface at PR time. + +## Campaign pattern (looped, multi-session) + +For work that converges over many sessions. Three moves, repeated: + +1. **Assess with a workflow — don't hand-audit.** `/audit-security|tests|performance|code-quality` and `/prod-readiness` are now **dynamic workflows**: they fan out cheap Haiku finders per surface, run an adversarial skeptic pass, loop until dry, and hand back one tier-tagged report. You just invoke the skill — the fan-out *and* the model routing happen inside it (Opus stays your driver; Haiku does the volume; you don't manage tiers). This is the "workflows / ultracode" part, and it is already built. +2. **Fix a batch with `/implement`** (parity + tests). The workflow *assesses*; it never edits. Remediation is normal slice work — one coherent batch per session. +3. **PR → `/clear` → fresh session → re-run the assessment.** Repeat until the report is clean / the bar holds. + +**Why fresh sessions, not one long run:** an assess→fix→re-assess loop is context-heavy; a single session degrades as it fills — the exact anti-pattern this harness fights. Externalize state to the issue + plan/report files; `/clear` between iterations keeps each session sharp. `/execute` is single-pass — it structurally cannot loop to convergence. + +**Worked example — #243 (final pre-launch gate):** + +1. Sonar surface config — **one `/execute` per repo** (config-only, no app code; the single slice-shaped phase). +2. Loop `/prod-readiness` → fix a batch via `/implement` → PR → `/clear` → re-run, until no unresolved Critical/High/Medium across the four audits + ops. +3. Sonar burn-down **last**, against frozen code — same loop shape (smells / duplication / coverage → 0 at source). +4. Make both Sonar checks required once green. + +Model note: Opus stays the driver throughout (judgment); the cheap leaves are Haiku, wired inside the workflows. There is no Fable/flagship-driver step — model routing lives in the workflow leaves, not in how you invoke the skill. + +## Plan → implement across fresh sessions (any cross-repo or risky slice) -1. `/prd-interactive` (cold start) **or** `/create-prd` (after you've already chatted through the idea) — pick one, not both -2. `/create-stories .claude/PRDs/.prd.md` — writes the local stories file and creates labeled GitHub issues in `orbit-ui-mobile` -3. For each issue: `/prime ` → `/plan ` → `/implement ` -4. `/pr-review` on the resulting PRs +Even a single slice, if it is cross-repo or high-risk, is safer split across two sessions than run as one `/execute`: -### Multi-issue path (parallelize across 2+ issues at once) +1. **Session A:** `/prime ` → `/plan ` → the plan file persists in `.claude/plans/`. +2. `/clear`. +3. **Session B:** fresh `/prime ` → `/implement .claude/plans/.plan.md`. -Pass multiple issue numbers; the harness creates paired worktrees (mobile + orbit-api) and runs the loop concurrently per issue, up to 3 at a time. +The plan file is the durable handoff; Session B implements against clean context. `/execute` (plan→implement in one shot) stays right for bounded slices — reach for the split when the plan is large or the blast radius is high. -1. `/prime ` — creates one worktree per issue under `.claude/worktrees/`, primes each in a background subagent -2. `/plan ` — writes one plan file per worktree in parallel -3. `/implement ` — implements + tests + opens PRs per worktree in parallel; failures are reported at the end without halting siblings +## Session hygiene (applies everywhere) -Cap: 3 concurrent subagents (avoid grinding `dotnet build` and `turbo` simultaneously). Excess issues queue. Two issues touching the same files → conflicts surface at PR time and are resolved manually. +- **Small sessions.** `/clear` between tasks and between campaign iterations. +- **Externalize state** to issues + plan/report files, not the conversation. +- **Delegate heavy reading** to `Explore` subagents and the audit workflows — don't read the repo into your main context. -## Two Recurring Details +## Two recurring details -- Stories get a `repo:frontend` / `repo:backend` / `repo:both` label. `/implement` reads it and creates branches + PRs in the right repo(s), cross-linked. -- Anything that touches a web hook/component needs the mobile counterpart unless the story explicitly says otherwise — `/implement`'s parity-check phase enforces this via the `parity-checker` subagent. +- Stories carry a `repo:frontend` / `repo:backend` / `repo:both` label; `/implement` reads it and opens branches + PRs in the right repo(s), cross-linked. +- Anything touching a web hook/component needs its mobile counterpart unless the story says otherwise — `/implement`'s parity phase enforces this via the `parity-checker` subagent. -## Rule of Thumb +## Rule of thumb -If it's clearly a single PR's worth of work, skip the PRD and go straight to `/prime` → `/plan`. PRDs and stories start to pay off once you have **3 or more** connected pieces of work. +Single PR's worth of work → skip the PRD, go `/prime` → `/plan`. PRDs and stories pay off at **3+** connected pieces. A converging-metric finish line → it's a **campaign**: loop workflows across fresh sessions, don't force it into one `/execute`.