---
phase: 02-claude-code-beachhead
plan: 03
subsystem: activity-timeline-and-docs
tags: [activity, my-agent, day-grouping, narrative, docs, readme, guides, homepage-draft, screencast]
type: execute
wave: 1
depends_on: []
files_modified:
  - __tests__/unit/activity-day-grouping.test.js
  - __tests__/unit/my-agent-page.test.jsx
  - app/activity/page.js
  - app/my-agent/page.js
  - app/guides/claude-code/page.js
  - README.md
  - docs/homepage-draft-claude-code.md
  - scripts/check-readme-lead.mjs
autonomous: false
requirements: [CCI-04, CCI-05]

must_haves:
  truths:
    - "A developer opening /activity sees events grouped by calendar day with a one-line English header per day (e.g. 'Wed Apr 22 — 12 approvals, 3 denials, 47 silent allows, 0 errors')"
    - "The /activity day-grouping respects the active useAgentFilter — counts reflect the filter"
    - "A developer opening /my-agent with prior activity sees a narrative hero sentence stating today's command count, approvals-required count, and denied count"
    - "A developer opening /my-agent with zero prior activity sees a 3-step install-prompt hero linking to /guides/claude-code"
    - "/my-agent defaults to Today scope with a one-click toggle to Week (trailing 7 days)"
    - "Denied actions on /my-agent render FIRST in the list (pinned) regardless of chronological position, with the blocking policy name + reason inline"
    - "/my-agent updates live via the existing useRealtime hook without manual refresh"
    - "Both /activity and /my-agent reuse existing data sources (/api/actions, /api/guard, /api/activity) — zero schema change, zero new API routes"
    - "/guides/claude-code contains the Discord Developer Portal walkthrough section + the screencast link + tested install snippets"
    - "README.md first 50 lines lead with 'Govern Claude Code in 5 minutes', include a link to /guides/claude-code, keep the existing npx dashclaw-demo + Vercel Deploy button + SDK badges below the fold"
    - "docs/homepage-draft-claude-code.md exists, contains the Phase 3 homepage handoff copy (≥200 words)"
    - "A ≤3-minute screencast is published (Loom or YouTube Unlisted) and linked from both README and /guides/claude-code"
  artifacts:
    - path: "app/activity/page.js"
      provides: "GlobalActivityFeed with client-side day grouping via useMemo"
      contains: "groupEventsByDay"
    - path: "app/my-agent/page.js"
      provides: "New narrative agent activity page — client component with narrative hero, today/week toggle, pinned denials, realtime updates, install-prompt empty state"
      contains: "buildNarrative"
    - path: "app/guides/claude-code/page.js"
      provides: "Rewritten Claude Code guide with Discord setup section + screencast embed"
      contains: "Discord"
    - path: "README.md"
      provides: "Claude Code-first top-of-file lead ('Govern Claude Code in 5 minutes' + 3-step install + screencast link)"
      contains: "Govern Claude Code"
    - path: "docs/homepage-draft-claude-code.md"
      provides: "Phase 3 homepage draft copy + section outline (≥200 words)"
    - path: "__tests__/unit/activity-day-grouping.test.js"
      provides: "Pure-function tests for groupEventsByDay + summarizeDay covering empty, single-event, multi-day, all-zero-counts scenarios"
    - path: "__tests__/unit/my-agent-page.test.jsx"
      provides: "Render tests covering 0-event empty state, 1-event state, 50+-event state, agent-filter propagation, denial pinning"
    - path: "scripts/check-readme-lead.mjs"
      provides: "CI gate — asserts README first 50 lines contain 'Claude Code' + link to /guides/claude-code"
  key_links:
    - from: "app/activity/page.js"
      to: "useMemo-scoped groupEventsByDay"
      via: "transforms existing events state — zero new data fetches"
      pattern: "groupEventsByDay"
    - from: "app/my-agent/page.js"
      to: "/api/actions?limit=200 + /api/guard?limit=200"
      via: "fetch + useRealtime re-fetch on action.created / action.updated / guard.decision.created events"
      pattern: "useRealtime"
    - from: "app/my-agent/page.js"
      to: "useAgentFilter context"
      via: "agentId passed through to fetch querystring"
      pattern: "useAgentFilter"
    - from: "README.md (first 50 lines)"
      to: "/guides/claude-code"
      via: "markdown link to dashclaw.io/guides/claude-code"
      pattern: "guides/claude-code"
    - from: "app/guides/claude-code/page.js"
      to: "screencast URL"
      via: "video embed or link"
      pattern: "loom\\.com|youtube\\.com"
---

<objective>
Ship CCI-04 + CCI-05: the human-readable agent activity surface (`/activity` day-grouping + new `/my-agent` narrative page) and the first-class Claude Code docs bundle (guides rewrite + README lead + screencast + Phase 3 homepage draft).

Purpose: Without CCI-04, the audit trail is raw JSON unreadable at a glance. Without CCI-05, developers can't self-serve from GitHub to a working integration in 5 minutes (CCI-01 hard-depends on the guides being accurate and the README lead being Claude-Code-first). Together these surfaces turn the shipped Phase 2 plumbing into something humans actually understand.

Output:
- `app/activity/page.js` gains a pure `useMemo` day-grouping layer (D-13, presentational only, zero schema change)
- `app/my-agent/page.js` is new — narrative hero + today/week toggle + pinned denials + install-prompt empty state (D-08..D-12)
- `app/guides/claude-code/page.js` rewritten to include the Discord Developer Portal walkthrough (D-18) and link the screencast
- `README.md` top-of-file rewritten "Govern Claude Code in 5 minutes" with 3-step install + screencast GIF, existing content preserved below the fold (D-15, D-16, D-17)
- `docs/homepage-draft-claude-code.md` created — Phase 3 handoff artifact, ≥200 words copy + outline (D-19)
- A ≤3-minute screencast published to Loom (or YouTube Unlisted — D-20, Claude's discretion) and linked from README + guide
- Two new unit test files covering grouping logic and page render states
- A small CI check script asserting the README lead stays Claude-Code-forward

Non-goals (Phase 3 or deferred):
- Homepage hero publish to `app/page.js` — Phase 3 (DOG-03)
- Show HN / tweet / blog post — Phase 3 (DOG-04)
- Mac/Linux walkthrough — deferred
- `/my-agent` timeline slider — rejected (D-09)
- `MODAL_SUBMIT` denial-reason modal — Claude's discretion, SKIPPED per RESEARCH §Anti-Patterns
</objective>

<execution_context>
@$HOME/.claude/get-shit-done/workflows/execute-plan.md
@$HOME/.claude/get-shit-done/templates/summary.md
</execution_context>

<context>
@.planning/PROJECT.md
@.planning/STATE.md
@.planning/phases/02-claude-code-beachhead/02-SPEC.md
@.planning/phases/02-claude-code-beachhead/02-CONTEXT.md
@.planning/phases/02-claude-code-beachhead/02-RESEARCH.md
@.planning/phases/02-claude-code-beachhead/02-VALIDATION.md
@.impeccable.md
@CLAUDE.md

# Existing surfaces to extend / reference
@app/activity/page.js
@app/approve/page.js
@app/approvals/page.jsx
@app/guides/claude-code/page.js
@README.md

# UI kit + hooks the new page reuses
@app/components/ui/EmptyState.js
@app/components/ui/Card.js
@app/hooks/useRealtime.js
@app/lib/AgentFilterContext.js
@app/lib/colors.js

<interfaces>
<!-- Extracted from codebase; executor uses these directly. -->

From `app/hooks/useRealtime.js` (singleton EventSource):
```javascript
// Subscribe to org-scoped SSE events. Handler fires on each event name.
export function useRealtime(
  handler: (eventName: string, payload: unknown) => void
): void;

// Known event names emitted by existing routes:
//   'action.created', 'action.updated', 'guard.decision.created'
```

From `app/lib/AgentFilterContext.js`:
```javascript
export function useAgentFilter(): { agentId: string | null; setAgentId: (id) => void };
```

From `app/components/ui/EmptyState.js` (4-prop shape — can extend with custom `action` node):
```javascript
export function EmptyState(props: {
  icon?: ReactNode;
  title: string;
  description?: string;
  action?: ReactNode;   // ← 3-step install hero goes here
}): JSX.Element;
```

From `app/components/ui/Card.js`:
```javascript
export function Card(props): JSX.Element;
export function CardContent(props): JSX.Element;
```

From `app/lib/colors.js`:
```javascript
export function getAgentColor(agentId: string): { bg: string; text: string; chipClass: string };
```

Existing data endpoints used — do NOT add new ones per D-13 + RESEARCH §code_context:
- GET `/api/actions?limit=<n>&agent_id=<id>` → `{ actions: Array<{ action_id, agent_id, action_type, declared_goal, risk_score, status, approved_by, timestamp_start, ... }> }`
- GET `/api/guard?limit=<n>&agent_id=<id>` → `{ evaluations: Array<{ action_id, agent_id, decision, reason, matched_policies, created_at, ... }> }`
- GET `/api/activity?limit=<n>` → `{ logs: Array<{ category, actor_id, ... }> }` (no agent_id filter)
</interfaces>
</context>

<tasks>

<task type="auto" tdd="true">
  <name>Task 1: Wave 0 — Scaffold failing tests for day-grouping and /my-agent render</name>
  <files>See files_modified frontmatter for the full list; task-specific subset is documented in the action block.</files>
  <read_first>
    - app/activity/page.js (full — to understand the event shape state currently holds)
    - app/approvals/page.jsx (read-only — for testing/render pattern reference)
    - __tests__/unit/approvals.page.test.jsx (Testing Library snapshot pattern the project uses)
    - .planning/phases/02-claude-code-beachhead/02-RESEARCH.md §Pattern 3 (day-grouping pseudocode, lines ~604-677)
    - .planning/phases/02-claude-code-beachhead/02-RESEARCH.md §Pattern 4 (`/my-agent` skeleton, lines ~679-800)
    - .planning/phases/02-claude-code-beachhead/02-RESEARCH.md §Validation Architecture Wave 0 Gaps (lines ~1148-1157)
  </read_first>
  <behavior>
    RED-state tests before production code. Two new test files.

    File 1: `__tests__/unit/activity-day-grouping.test.js`
    Tests for two pure functions that Task 2 will export from `app/activity/page.js` (or a sibling helper if the executor prefers clean separation — either works):
    - `groupEventsByDay(events)` returns an array of `{ dayKey, label, events, counts }`
    - `summarizeDay(group)` returns a string like "— 12 approvals, 3 denials, 47 silent allows, 0 errors"

    Test cases:
    - Empty array → returns empty array
    - Single event on 2026-04-22 → returns 1 group with label `"Wed Apr 22"`, 1 event, counts based on event.category + event.status
    - Two events, same day → 1 group, 2 events
    - Two events, different days → 2 groups in timestamp order (newest day first — preserves input DESC ordering)
    - Mix of guard events (allow/block/deny) + decision events (completed/failed/error) → counts distributed into correct buckets (allowed, denied, approved, errored)
    - Day with events but all counts zero (e.g., only `pending_approval` that never resolved) → group still renders, summarizeDay returns "0 approvals, 0 denials, 0 silent allows, 0 errors"
    - Singular vs plural grammar: `1 approval` (no s), `2 approvals` (s); same for denial/allow/error
    - Per D-14: if `useAgentFilter` is active upstream, the array fed to `groupEventsByDay` is already filtered — grouping function itself is agent-agnostic. (No separate test needed, but a comment in the file noting this invariant is helpful.)

    File 2: `__tests__/unit/my-agent-page.test.jsx`
    Tests for `MyAgentPage` default export using React Testing Library (same pattern as `approvals.page.test.jsx`). Mock `useRealtime`, `useAgentFilter`, and global `fetch`.

    Test cases:
    - 0-event state: renders install-prompt hero with "Your agent hasn't run anything yet" heading, ordered-list 3-step (install hook, connect Discord, trigger command), link to `/guides/claude-code`
    - 1-event state (action status=completed): narrative hero sentence contains "Today your agent ran 1 command." with singular grammar
    - 50-event state: narrative hero counts correct, pinned-denials section appears above chronological list IF any denials are present
    - Today/Week toggle: clicking "This week" re-filters and re-renders with 7-day cutoff
    - `useAgentFilter` returning a non-null `agentId` → fetch URLs include `&agent_id=<id>` querystring (assert on mock fetch call args)
    - Denial pinning (D-11): given a mix of 5 approvals and 2 denials, the first 2 rendered events are the denials (assert by test-id or section order) regardless of their timestamp position
    - Realtime: invoking the useRealtime subscriber with `action.updated` triggers a re-fetch (assert fetch mock called a second time)

    Tests MUST fail on first run because `app/my-agent/page.js` doesn't exist and `app/activity/page.js` doesn't yet export `groupEventsByDay` / `summarizeDay`.
  </behavior>
  <action>
    Create the two test files. Use `vi.mock('../../app/hooks/useRealtime', ...)` and `vi.mock('../../app/lib/AgentFilterContext', ...)` patterns — mirror the mocking approach in existing `approvals.page.test.jsx`.

    For the day-grouping test, import under test:
    ```javascript
    // Option A: re-exported from the page file
    import { groupEventsByDay, summarizeDay } from '../../app/activity/page.js';
    // Option B: factored into a helper for cleanliness (executor's choice in Task 2)
    // import { groupEventsByDay, summarizeDay } from '../../app/activity/dayGrouping.js';
    ```
    Task 2 will ensure one of these import paths resolves. Comment both options at the top of the test file and commit with Option A active (executor can switch to B if they factor the helper out).

    For the `/my-agent` test, import:
    ```javascript
    import MyAgentPage from '../../app/my-agent/page.js';
    ```

    Mock the minimum surface area:
    ```javascript
    vi.mock('../../app/hooks/useRealtime', () => ({ useRealtime: vi.fn() }));
    vi.mock('../../app/lib/AgentFilterContext', () => ({ useAgentFilter: () => ({ agentId: null }) }));
    vi.stubGlobal('fetch', vi.fn().mockResolvedValue({
      ok: true, status: 200, json: async () => ({ actions: [], evaluations: [] }),
    }));
    ```

    Fixtures: define a small `makeAction({...overrides})` / `makeGuard({...overrides})` helper at the top of the `/my-agent` test file to keep the cases terse.

    Commit the tests.
  </action>
  <verify>
    <automated>npm test -- --run __tests__/unit/activity-day-grouping.test.js __tests__/unit/my-agent-page.test.jsx 2>&amp;1 | grep -E "FAIL|Cannot find module|is not exported" &amp;&amp; echo "RED-STATE-CONFIRMED"</automated>
  </verify>
  <done>
    - Two test files exist under `__tests__/unit/`
    - `npm test -- --run __tests__/unit/activity-day-grouping.test.js __tests__/unit/my-agent-page.test.jsx` exits non-zero (RED state — production surface doesn't exist yet)
    - Test files compile syntactically (import paths defined even if unresolved)
  </done>
</task>

<task type="auto" tdd="true">
  <name>Task 2: Implement /activity day-grouping + new /my-agent page</name>
  <files>See files_modified frontmatter for the full list; task-specific subset is documented in the action block.</files>
  <read_first>
    - app/activity/page.js (full — modify in place)
    - app/approve/page.js (for mobile-first pattern reference — useRealtime + safeVibrate + useEffectiveRole usage)
    - app/approvals/page.jsx (for PageLayout + Card usage pattern)
    - app/components/ui/EmptyState.js (the install-prompt hero composes around or inside this)
    - app/components/ui/Card.js + Skeleton.jsx
    - app/hooks/useRealtime.js (confirm event names)
    - app/lib/AgentFilterContext.js (confirm hook shape)
    - app/lib/colors.js (getAgentColor for denial chips)
    - .impeccable.md (all 7 tiebreaker principles — especially #2 brand orange discipline and #4 token-first)
    - .planning/phases/02-claude-code-beachhead/02-RESEARCH.md §Pattern 3 + §Pattern 4 (copy-ready skeletons)
  </read_first>
  <action>
    **File 2a (MODIFY): `app/activity/page.js`**

    Add two pure functions near the top of the file (above `GlobalActivityFeed`) AND export them at the bottom so the test file can import them:

    ```javascript
    export function groupEventsByDay(events) {
      const groups = new Map();
      for (const evt of events) {
        const d = new Date(evt.timestamp);
        const dayKey = d.toISOString().slice(0, 10);
        if (!groups.has(dayKey)) {
          const label = d.toLocaleDateString('en-US', {
            weekday: 'short', month: 'short', day: 'numeric',
          });
          groups.set(dayKey, {
            dayKey, label, events: [],
            counts: { approved: 0, denied: 0, allowed: 0, errored: 0 },
          });
        }
        const group = groups.get(dayKey);
        group.events.push(evt);
        if (evt.category === 'guard') {
          if (evt.status === 'allow') group.counts.allowed += 1;
          else if (evt.status === 'block' || evt.status === 'deny') group.counts.denied += 1;
        } else if (evt.category === 'decision') {
          if (evt.status === 'completed') group.counts.approved += 1;
          else if (evt.status === 'failed' || evt.status === 'error') group.counts.errored += 1;
        }
      }
      return Array.from(groups.values());
    }

    export function summarizeDay({ counts }) {
      const s = (n, word) => `${n} ${word}${n === 1 ? '' : 's'}`;
      return `— ${s(counts.approved, 'approval')}, ${s(counts.denied, 'denial')}, `
           + `${s(counts.allowed, 'silent allow')}, ${s(counts.errored, 'error')}`;
    }
    ```

    Inside `GlobalActivityFeed`, add (near the render block):
    ```javascript
    const groupedByDay = useMemo(() => groupEventsByDay(events), [events]);
    ```

    Replace the flat events map in the render JSX with:
    ```jsx
    {groupedByDay.map(group => (
      <section key={group.dayKey} className="mb-6">
        <header className="mb-2 flex items-center justify-between border-b border-border/40 pb-1">
          <span className="font-medium text-primary">{group.label}</span>
          <span className="text-xs text-tertiary tabular-nums">{summarizeDay(group)}</span>
        </header>
        {/* existing per-event render — keep the EXACT current row markup */}
        {group.events.map(evt => /* existing row JSX unchanged */)}
      </section>
    ))}
    ```

    IMPORTANT — PRESERVE existing behavior: loading state, empty state, the `useRealtime` subscriber, the `useAgentFilter` filter. The day-grouping layer wraps the existing render; it does not replace any state or fetch logic. Per D-13, zero schema change.

    Use CSS tokens only — no hardcoded hex. Tailwind classes like `text-primary`, `text-tertiary`, `border-border/40` resolve via the theme extension.

    **File 2b (NEW): `app/my-agent/page.js`**

    Follow RESEARCH §Pattern 4 as the structural template. Produce a complete client component ~200 lines that passes the 7 test cases from Task 1.

    Critical design constraints per .impeccable.md:
    - Brand orange ONLY on the CTA link ("Open the full guide →") and the denial-badge chip — NOT ambient (tiebreaker #2)
    - Use `text-brand` / `bg-brand/10` / `border-active/30` Tailwind utilities — NEVER hardcode `#f97316` in JSX (tiebreaker #4)
    - Tabular numerals on all count cells (tabular-nums class)
    - Icons from `lucide-react` only (Activity, Zap, Shield, Terminal, AlertTriangle)
    - Narrative copy voice: direct, declarative, short. "Today your agent ran 47 commands. 3 required approval. 0 were denied." — never emoji, never "welcome to…" fluff.
    - Empty state copy: "Your agent hasn't run anything yet." — then 3 steps, then "Open the full guide →" link.

    Structural slots (top-to-bottom):
    1. `PageLayout` wrapper with title "My Agent" and breadcrumbs `['Command', 'My Agent']`
    2. If `!hasAnyActivity`: render `<InstallPromptHero />` and return early (D-10)
    3. Narrative hero card: large-type sentence built by `buildNarrative(scope, counts)`. If `counts.denied > 0`, apply `text-status-warning` or `text-status-error` visual weight to the denied clause (per .impeccable.md status-color tokens, NOT a hardcoded color)
    4. Today/Week toggle — simple two-button row, active state uses `border-active` (D-09)
    5. Pinned denials section: if any denials in filtered set, render FIRST with the policy name + reason (D-11). Use `getAgentColor(agent_id)` for the agent chip.
    6. Chronological list: remaining events (approvals, silent allows) in time-DESC order

    Data flow:
    - `fetchData()` on mount + on `useAgentFilter` change + on `useRealtime` event (`action.created` | `action.updated` | `guard.decision.created`)
    - Limit=200 per endpoint — enough headroom for "this week" view
    - `useAgentFilter` `agentId` passed to querystring (per D-14 + D-12)
    - No new API routes — uses `/api/actions` + `/api/guard` only (per RESEARCH §code_context)

    Export `buildNarrative` for testing OR keep it module-private; the test imports the default component so exposure isn't strictly required. Prefer module-private unless the test explicitly asserts on it — then unit-export only for tests.

    **XSS discipline (T-02-03-03)**: render action text via React's default interpolation only. Use plain `{action.declared_goal}` — never any raw-HTML injection API. The codebase's DOMPurify dependency is NOT needed on this page because no HTML-as-string is rendered.

    **File 2c (MODIFY): re-run tests, all green**

    After both files are in place:
    ```bash
    npm test -- --run __tests__/unit/activity-day-grouping.test.js __tests__/unit/my-agent-page.test.jsx
    ```
    All tests green. If any test fails, fix the production code — do NOT loosen the test.

    **SECURITY T-02-03-01**: cross-tenant data isolation. `/my-agent` does NOT introduce a new repository query — it uses the existing `/api/actions` and `/api/guard` routes, which are already `req.org_id`-scoped via the middleware's API-key → org_id resolution. Verify by reading `app/api/actions/route.js` GET handler and confirming it uses `req.org_id` (or equivalent) in the query. If this route was modified or a new one added in the future, that's when the T-02-03-01 check matters — for this plan, we rely on the existing boundary.
  </action>
  <verify>
    <automated>npm test -- --run __tests__/unit/activity-day-grouping.test.js __tests__/unit/my-agent-page.test.jsx</automated>
  </verify>
  <done>
    - `app/activity/page.js` exports `groupEventsByDay` and `summarizeDay`
    - `app/activity/page.js` renders events grouped by day with a header per day — verified by running the dev server on a seeded DB is OPTIONAL for this task (unit tests are sufficient; real rendering is verified in plan 02-01 walkthrough)
    - `app/my-agent/page.js` exists and the default export renders the narrative page
    - `grep "'use client'" app/my-agent/page.js` returns 1 match (client component)
    - `grep "useRealtime" app/my-agent/page.js` returns 1 match (D-12)
    - `grep "useAgentFilter" app/my-agent/page.js` returns 1 match (D-14)
    - `grep -E "#f97316|#[0-9a-fA-F]{6}" app/my-agent/page.js` returns ZERO matches (tokens only; .impeccable.md tiebreaker #4)
    - `grep "/guides/claude-code" app/my-agent/page.js` returns at least 1 match (empty-state CTA link, D-10)
    - `grep -E "dangerously.?Set.?Inner.?HTML" app/my-agent/page.js` returns ZERO matches (T-02-03-03 — React interpolation only)
    - 7 test cases in `activity-day-grouping.test.js` GREEN
    - 7+ test cases in `my-agent-page.test.jsx` GREEN
    - No changes to `/api/actions`, `/api/guard`, or database schema (D-13 locked)
  </done>
</task>

<task type="auto" tdd="false">
  <name>Task 3: Rewrite /guides/claude-code + README lead + create homepage draft + CI script</name>
  <files>See files_modified frontmatter for the full list; task-specific subset is documented in the action block.</files>
  <read_first>
    - app/guides/claude-code/page.js (full, 186 lines — structure to extend, NOT rewrite from scratch)
    - README.md (first 60 lines — current layout to preserve below the new lead)
    - .impeccable.md (voice + 4 anti-references: no generic-SaaS, no consumer-AI sparkle, no enterprise-compliance density, no crypto/web3 glow)
    - .planning/phases/02-claude-code-beachhead/02-CONTEXT.md §decisions D-15, D-16, D-17, D-18, D-19 (the copy/structure locks)
    - .planning/phases/02-claude-code-beachhead/02-SPEC.md §CCI-05 Acceptance
    - .planning/phases/02-claude-code-beachhead/02-RESEARCH.md §Summary recommendation on Loom
  </read_first>
  <action>
    Four distinct edits. Order matters because the README lead references the guide + screencast; the guide references the screencast; so drop the screencast link placeholder into both in this task — plan 02-01 will replace the placeholder with the real URL after recording.

    **File 3a (MODIFY): `app/guides/claude-code/page.js`**

    Add a new section between the existing "install hook" content and the "settings.json" block. Structure (reuse existing JSX pattern — Card, code blocks, GuideClient for copy-code):

    ```
    ## 2. Connect Discord (2 minutes)

    (Prose: explain Discord bot = phone approvals, Telegram parity, ENV-only setup)

    ### Create the bot in Discord Developer Portal
    - Open https://discord.com/developers/applications -> New Application
    - Name the app; skip the Installation tab
    - Open the "Bot" tab -> Reset Token -> copy as DISCORD_BOT_TOKEN
    - Open "General Information" -> copy the Public Key as DISCORD_PUBLIC_KEY
    - Under "Privileged Gateway Intents" leave ALL off (button-only bot; no message content needed)

    ### Invite the bot to a mutual server (so DMs work)
    - Open "OAuth2" -> URL Generator -> scopes: "bot" -> permissions: "Send Messages"
    - Paste the URL in a browser, invite the bot to a personal test server
    - In Discord client, enable Developer Mode (Settings -> Advanced)
    - Right-click your own user in the member list -> Copy User ID -> paste as DISCORD_APPROVER_USER_ID

    ### Register the interactions endpoint
    - In "General Information" set:
      Interactions Endpoint URL: https://<your-deployment>/api/discord/interactions
    - Discord will send a PING; DashClaw responds {type:1} and the URL saves.

    ### Add to .env.local (or Vercel env vars)
    ```
    DISCORD_BOT_TOKEN=<token>
    DISCORD_PUBLIC_KEY=<hex>
    DISCORD_APPROVER_USER_ID=<numeric id>
    DISCORD_APPROVER_ORG_ID=<your org id>
    DASHCLAW_ALERTS_DISCORD=true
    ```

    ### Verify
    - Trigger a Claude Code tool call that hits an approval-required policy
    - Your phone's Discord app lights up; tap Approve or Deny
    - The DM edits in place to show APPROVED/DENIED with timestamp
    ```

    Add a screencast section near the top of the guide:
    ```
    ## Watch the 3-minute walkthrough
    [Screencast placeholder — <SCREENCAST_URL>]
    ```

    Use a PLACEHOLDER token `<SCREENCAST_URL>` that plan 02-01 replaces once the recording is live. The CI check in File 3d MUST skip this placeholder (or run in no-op mode) until 02-01 populates it — see Task 4 below for how we handle the gating.

    Update all existing install snippets to reflect the SHIPPED Phase 2 state:
    - `.claude/settings.json` content (already present in the file)
    - `dashclaw.config.json` / env vars
    - Policy pack reference (`claude-code-starter` pack is auto-loaded per Phase 2-01)

    Per SPEC §CCI-05 Acceptance: every code snippet MUST be runnable as-is. The CCI-01 walkthrough (plan 02-01) will verify this by running each snippet verbatim on fresh Windows/WSL. The guide author MUST NOT invent syntax — every command must already work in the current codebase.

    **File 3b (MODIFY): `README.md`** — top-of-file rewrite

    Insert the following block at the TOP of `README.md`, keeping ALL existing content (npx dashclaw-demo, Works-with list, Deploy button, SDK badges, etc.) below the new lead. D-15 locks "Claude Code lead, existing content below."

    ```markdown
    <div align="center">
      <img src="public/images/logo-circular.png" alt="DashClaw" width="240" />
      <h1>DashClaw</h1>
      <p><strong>Govern Claude Code in 5 minutes.</strong></p>
      <p>Install the hook. Connect Discord. Approve risky tool calls from your phone.<br />
      Built so your coding agent can never surprise you with a destructive action.</p>

      <a href="<SCREENCAST_URL>"><img src="public/images/demo-gif2.gif" alt="DashClaw governing Claude Code — click to watch the full 3-minute walkthrough" width="1000" /></a>

      <p><strong>3-step install</strong></p>
      <pre><code>npm install
npm run hooks:install
# Connect Discord (see guide)
      </code></pre>

      <p>
        <a href="https://dashclaw.io/guides/claude-code">Claude Code integration guide &rarr;</a>
        &nbsp;·&nbsp;
        <a href="<SCREENCAST_URL>">Watch the 3-min walkthrough &rarr;</a>
      </p>
    </div>

    <hr />

    <!-- Existing content begins here — preserved verbatim -->

    <div align="center">
      <p><sub>Prefer a 10-second zero-setup taste? Try the Decision Replay demo:</sub></p>
      <pre><code>npx dashclaw-demo</code></pre>
      <p><sub>No setup. Opens Decision Replay automatically.</sub></p>
      ...
    ```

    Preserve (verbatim, do not delete):
    - The `public/images/demo-gif2.gif` image reference
    - Vercel Deploy button + "$0 to deploy" text
    - `npx dashclaw-demo` block (per D-16 — moved below the fold, reframed as secondary)
    - "Works with" framework list
    - All SDK badges (npm, pypi, github stars, license, website, docs)
    - All existing sections below (Deploy, Connect Your Agent, etc.)

    D-17 is honored in full by wrapping the GIF in a markdown HTML link: `<a href="<SCREENCAST_URL>"><img src="public/images/demo-gif2.gif" ... /></a>`. This is the canonical markdown click-through pattern (GitHub, npm, Loom all render it as a clickable image). The `<SCREENCAST_URL>` placeholder is backfilled by plan 02-01 Task 3 once the screencast is published — at which point both the text link ("Watch the 3-min walkthrough →") and the GIF itself resolve to the same public video URL. The earlier claim that "markdown doesn't support GIF click targets trivially" was incorrect; correcting here per checker warning d17-gif-clickthrough-descoped (2026-04-22).

    **File 3c (NEW): `docs/homepage-draft-claude-code.md`**

    Create an artifact file with Phase 3 handoff copy. ≥200 words (SPEC). 300-500 words per D-19. Structure:

    ```markdown
    # Homepage Draft — Claude Code Beachhead

    *Phase 3 handoff artifact. Not published to app/page.js in Phase 2. When Phase 3 lifts
    this into the homepage hero, the below copy + outline are the canonical source.*

    ## Hero

    **Headline:** Govern Claude Code in 5 minutes.

    **Subhead:** Install a hook. Connect Discord. Approve or deny risky tool calls from
    your phone before they run. Built so your coding agent can never surprise you.

    **Hero visual:** Side-by-side split. Left: a terminal showing a real Claude Code
    session hitting a destructive command; right: a phone mockup with the Discord
    DashClaw DM, Approve/Deny buttons visible. Motion: on hover (or auto-play), the
    phone animates the tap → the terminal unblocks. Same animation pattern as demo-gif2.

    ## Three sections below the hero

    ### Section 1: What it prevents
    Three concrete before/after pairs, taken from real blocked actions in the dogfood
    ledger. Example: "Claude Code: `rm -rf ./dist` → DashClaw: blocked by policy
    `block_destructive_shell`. Your build cache lived."

    ### Section 2: The 5-minute path
    The same 3-step install block from the README, rendered visually. Numbered. Each
    step has a tiny code snippet + a "copy" button. Anchor link to the full guide.

    ### Section 3: What you get
    Three product cards: Mission Control (live decision stream), /my-agent (what did
    my agent do today), /activity (ledger + day grouping). Screenshots from the
    real dogfood instance — no mockups, evidence over decoration per .impeccable.md.

    ## Body copy (~200 words)

    DashClaw is a governance runtime for AI coding agents. It intercepts every tool
    call before it executes, enforces policies written in plain YAML, and records an
    audit trail of every decision. When a call needs human judgment, DashClaw routes
    the approval request to Discord — approve from your phone in under 10 seconds.

    The first-class integration is Claude Code. A single `npm run hooks:install`
    registers a `PreToolUse` hook that gates every `Bash`, `Edit`, `Write`, and
    `MultiEdit` through DashClaw. The default policy pack (`claude-code-starter`)
    silently allows git commits and test runs, always blocks `rm -rf` and force-pushes
    to main, and requires approval for ambiguous actions like network calls and
    package installs.

    Every blocked action is visible in the ledger. Every approval is signed. Every
    denial has a reason attached. The Discord DM is a scannable audit surface — the
    full message history is your trail.

    Self-host on Vercel free tier. One command to deploy. No vendor lock-in.

    ## Voice notes

    - Direct, declarative, technical. No "unleash," no "empower," no exclamation marks.
    - Brand orange ONLY on the primary CTA and the "live decision" pulse. Not ambient.
    - Four anti-references guardrail: before ship, verify the hero does not drift
      toward generic-SaaS, consumer-AI, enterprise-compliance, or crypto/web3 (see
      .impeccable.md §4 anti-references).

    ## CTA pair

    - Primary: "Start with Claude Code" → /guides/claude-code
    - Secondary: "Deploy to Vercel" → existing Deploy button

    ## What this draft does NOT cover (Phase 3 decides)

    - Final hex tokens (design will finalize against globals.css)
    - Concrete screenshots (take from dogfood instance at handoff time)
    - Show-HN launch copy — separate artifact (DOG-04)
    - Metrics badges (stars, deployments) — freshness vs. stability tradeoff
    ```

    Confirm ≥200 words: `wc -w docs/homepage-draft-claude-code.md` ≥ 200.

    **File 3d (NEW): `scripts/check-readme-lead.mjs`**

    Small Node script, runnable via `node scripts/check-readme-lead.mjs`. Asserts:
    1. README.md first 50 lines contain the substring "Claude Code" (case-insensitive)
    2. README.md first 50 lines contain "/guides/claude-code" as a link target
    3. Exits 0 on success, 1 on failure with a descriptive message

    ```javascript
    #!/usr/bin/env node
    import { readFileSync } from 'node:fs';

    const text = readFileSync('README.md', 'utf8');
    const first50 = text.split('\n').slice(0, 50).join('\n');

    const hasClaudeCode = /claude code/i.test(first50);
    const hasGuideLink = /\/guides\/claude-code/.test(first50);

    if (!hasClaudeCode) {
      console.error('check-readme-lead: README first 50 lines do not mention "Claude Code"');
      process.exit(1);
    }
    if (!hasGuideLink) {
      console.error('check-readme-lead: README first 50 lines missing link to /guides/claude-code');
      process.exit(1);
    }
    console.log('check-readme-lead: OK');
    ```

    Do NOT wire this into `npm test` or `run-pre-commit-checks.mjs` in this plan — adding it as a gate is optional per RESEARCH §Validation Architecture. Adding the script file itself is enough; future plans (or Phase 3) can promote it to a gate.

    **Screencast recording:** NOT in this task. Plan 02-01's walkthrough checkpoint covers CCI-05 screencast recording + publishing + backfilling `<SCREENCAST_URL>` placeholders in both `README.md` and `app/guides/claude-code/page.js`.
  </action>
  <verify>
    <automated>node scripts/check-readme-lead.mjs &amp;&amp; wc -w docs/homepage-draft-claude-code.md | awk '{ if ($1 &lt; 200) { print "FAIL: homepage draft &lt;200 words"; exit 1 } else { print "OK: " $1 " words" } }' &amp;&amp; grep -q "Discord" app/guides/claude-code/page.js &amp;&amp; echo "guide has Discord section" &amp;&amp; grep -qE '<a href="&lt;SCREENCAST_URL&gt;"&gt;.*&lt;img src="public/images/demo-gif2.gif"' README.md &amp;&amp; echo "D-17 click-through present"</automated>
  </verify>
  <done>
    - `app/guides/claude-code/page.js` contains a Discord setup section with the 4-step Developer Portal flow (create app, copy token + public key, invite to mutual server, register interactions endpoint URL)
    - `grep -E "DISCORD_BOT_TOKEN|DISCORD_PUBLIC_KEY|DISCORD_APPROVER" app/guides/claude-code/page.js` returns 3+ matches
    - `grep "<SCREENCAST_URL>" app/guides/claude-code/page.js` returns 1 match (placeholder, backfilled by plan 02-01)
    - `README.md` first 50 lines contain "Govern Claude Code in 5 minutes" + link to `/guides/claude-code` + link to `<SCREENCAST_URL>` placeholder
    - `head -50 README.md | grep -i "claude code"` returns at least 1 match
    - `head -50 README.md | grep "/guides/claude-code"` returns at least 1 match
    - `grep -E '<a href="<SCREENCAST_URL>">.*<img src="public/images/demo-gif2.gif"' README.md` returns 1 match (D-17 click-through honored; placeholder backfilled by plan 02-01 Task 3)
    - All existing README content preserved below the new lead (`npx dashclaw-demo`, Works-with list, Deploy button, SDK badges)
    - `docs/homepage-draft-claude-code.md` exists, ≥200 words (via `wc -w`)
    - `scripts/check-readme-lead.mjs` exists, executable, exits 0 against the current README
    - No hardcoded hex in any modified file (CSS tokens only, .impeccable.md tiebreaker #4)
    - No emoji in narrative copy / guide copy (tone per .impeccable.md — emoji excluded per the "no AI sparkle" anti-reference)
  </done>
</task>

<task type="auto" tdd="false">
  <name>Task 4: Full-suite regression gate</name>
  <files>See files_modified frontmatter for the full list; task-specific subset is documented in the action block.</files>
  <read_first>
    - .planning/phases/02-claude-code-beachhead/02-VALIDATION.md (full suite mandatory per plan wave)
    - CLAUDE.md §GitNexus Self-Check
  </read_first>
  <action>
    1. Full test suite:
       ```bash
       npm test
       ```
       Must exit 0. Including `claude-code-starter-pack.test.js` (CCI-02 no-regression).

    2. Static guardrails:
       ```bash
       npm run lint
       npm run route-sql:check    # should pass trivially — this plan adds no routes
       npm run openapi:check      # should pass trivially — this plan adds no routes
       npm run api:inventory:check
       npm run docs:check
       node scripts/check-readme-lead.mjs
       ```

    3. GitNexus impact + detect:
       ```
       gitnexus_impact({target: "GlobalActivityFeed", direction: "upstream"})
       gitnexus_detect_changes({scope: "staged"})
       ```
       Expected modifications:
       - `app/activity/page.js` — added groupEventsByDay, summarizeDay exports; render block wrapped in day sections. HIGH-risk expected: ZERO (presentational layer only; no API or state flow change).
       - `app/my-agent/page.js` — new file, new default export
       - `app/guides/claude-code/page.js` — content changes only
       - `README.md` — markdown only
       - New artifact files (test files, homepage-draft, check-readme-lead script)

       If `detect_changes` flags unexpected symbol modifications in code outside the `files_modified` frontmatter, revert and re-commit deliberately.
  </action>
  <verify>
    <automated>npm test &amp;&amp; npm run lint &amp;&amp; npm run openapi:check &amp;&amp; npm run api:inventory:check &amp;&amp; npm run docs:check &amp;&amp; node scripts/check-readme-lead.mjs</automated>
  </verify>
  <done>
    - `npm test` exits 0 (full suite, 1648+ tests)
    - `npm run lint` exits 0
    - `npm run openapi:check`, `npm run api:inventory:check`, `npm run docs:check` exit 0
    - `node scripts/check-readme-lead.mjs` exits 0
    - GitNexus detect_changes output matches `files_modified` — no stray symbol changes
    - `__tests__/unit/claude-code-starter-pack.test.js` passes (CCI-02 untouched)
    - `__tests__/unit/discord-*.test.js` still passes (02-02 work untouched)
  </done>
</task>

</tasks>

<threat_model>
## Trust Boundaries

| Boundary | Description |
|----------|-------------|
| Browser → DashClaw API (`/api/actions`, `/api/guard`) | Authenticated via existing API key / session middleware; `req.org_id` is set by that middleware. Our `/my-agent` client fetches these as-is — no new boundary introduced. |
| `/my-agent` client state → UI render | In-process, trusted. Narrative copy is derived from counts, not from raw action content. Action `declared_goal` text is rendered verbatim; React's default escaping handles untrusted text safely. |
| README / guide / homepage draft → public web | Public-facing docs; no user-supplied input embedded. |

## STRIDE Threat Register

| Threat ID | Category | Component | Disposition | Mitigation Plan |
|-----------|----------|-----------|-------------|-----------------|
| T-02-03-01 | Information Disclosure | `/my-agent` narrative reveals cross-tenant data if the aggregation query isn't org-scoped | mitigate | `/my-agent` does NOT introduce a new repository query. It reads via `/api/actions` and `/api/guard`, both of which are already `req.org_id`-scoped by the middleware's API-key → org_id resolution (verified in middleware.js). No new SQL in this plan. If a future task adds a custom aggregation endpoint, it MUST pin `org_id = ${req.org_id}` and re-verify. |
| T-02-03-02 | Information Disclosure | Docs screencast recording leaks secrets (env vars, API keys, tokens visible on-screen) | mitigate | This plan ships only the `<SCREENCAST_URL>` placeholder in README.md + guide. Recording itself is owned by plan 02-01 (human-gated checkpoint). Plan 02-01's action block includes the post-record review checklist: redact `.env*` files + terminal history + clipboard before recording; use a throwaway workspace with rotatable tokens; review recording frame-by-frame before publishing; if any secret appears, re-record. |
| T-02-03-03 | XSS via action declared_goal | `/my-agent` renders action.declared_goal text | mitigate | `app/my-agent/page.js` uses React text interpolation only (`{action.declared_goal}`) which escapes untrusted content automatically. No raw-HTML string injection API is used. Asserted by grep in Task 2 `<done>` criteria — zero matches for the raw-HTML API name in the new page file. |
| T-02-03-04 | Information Disclosure | Narrative counts reveal action volume to an attacker who has session cookie but wrong org | accept | This is already the risk model for `/api/actions` and `/api/guard`. `/my-agent` introduces no new exposure surface. Mitigated by existing session/org middleware — see T-02-03-01. |
</threat_model>

<verification>

### Task-level
- Task 1 confirms RED state (tests fail before production code exists)
- Task 2 confirms GREEN state after implementation (all day-grouping + /my-agent tests pass)
- Task 3 confirms docs gates pass (check-readme-lead, word count, guide has Discord section)
- Task 4 confirms full-suite + static guardrails + GitNexus impact all green

### Plan-level
```bash
npm test                           # full suite green
npm run lint                        # clean
npm run openapi:check               # no route changes → still clean
npm run api:inventory:check         # no route changes → clean
npm run docs:check                  # clean
node scripts/check-readme-lead.mjs  # README lead verified

# Content spot-checks
grep -c "Discord" app/guides/claude-code/page.js        # expect ≥ 3
grep -c "DISCORD_BOT_TOKEN" app/guides/claude-code/page.js # expect ≥ 1
head -50 README.md | grep -ic "claude code"              # expect ≥ 1
head -50 README.md | grep -c "/guides/claude-code"       # expect ≥ 1
wc -w docs/homepage-draft-claude-code.md | awk '{print $1}' # expect ≥ 200

# D-17 GIF click-through (wraps <img> in <a href="<SCREENCAST_URL>"> per CONTEXT.md D-17)
grep -cE '<a href="<SCREENCAST_URL>">.*<img src="public/images/demo-gif2.gif"' README.md # expect 1

# Presentational-only guard (D-13 lock)
grep -E "/api/new-" app/my-agent/page.js                 # expect NOTHING — no new API routes
grep -rE "#f97316|#[0-9a-fA-F]{6}" app/my-agent/page.js  # expect NOTHING — tokens only
```

### Manual (plan 02-01's walkthrough, not gating THIS plan)
- On a seeded dev instance with real action history, `/activity` renders day headers with the 4-count summary
- `/my-agent` with real Claude Code activity shows the narrative + denials pinned + realtime updates
- Denial chips use the correct agent colors; narrative uses `text-status-warning` tokens (not hex)
- Every code snippet on `/guides/claude-code` copy-pastes and runs on a fresh Windows/WSL clone

</verification>

<success_criteria>

The plan is complete when:

1. **CCI-04 `/activity` day-grouping**: `/activity` renders events grouped by day with a single-line English summary per day (`"Wed Apr 22 — N approvals, N denials, N silent allows, N errors"`). Zero schema change. Zero new API routes. Filter respects `useAgentFilter`.
2. **CCI-04 `/my-agent` page**: new page exists at `app/my-agent/page.js`; renders narrative hero + today/week toggle + pinned denials + chronological list + install-prompt empty state; respects `useAgentFilter`; updates live via `useRealtime`.
3. **CCI-04 coverage**: `activity-day-grouping.test.js` + `my-agent-page.test.jsx` cover 0-event, 1-event, 50+-event, filter-active, denial-pinning, realtime-refresh scenarios.
4. **CCI-05 guide rewrite**: `/guides/claude-code` has a dedicated Discord Developer Portal walkthrough section with all 4 env vars and the interactions endpoint URL format; all code snippets accurately reflect the shipped Phase 2 integration.
5. **CCI-05 README lead**: top of `README.md` leads with "Govern Claude Code in 5 minutes"; first 50 lines mention Claude Code + link to `/guides/claude-code`; existing content preserved below the fold.
6. **CCI-05 homepage draft**: `docs/homepage-draft-claude-code.md` exists, ≥200 words, contains hero copy + 3-section outline + body copy + voice notes.
7. **CCI-05 CI script**: `scripts/check-readme-lead.mjs` runs and exits 0 against the current README.
8. **CCI-05 screencast placeholder + D-17 click-through**: `<SCREENCAST_URL>` placeholder exists in both README.md and the guide. README demo-gif2.gif is wrapped in `<a href="<SCREENCAST_URL>">...</a>` so the GIF is click-through to the full video once plan 02-01 backfills the real URL (D-17 honored in full).
9. **Design compliance**: no hardcoded hex in any modified file; brand orange only on CTAs and denial flags; lucide-react only; CSS tokens only; copy is declarative/technical (no "welcome to…" fluff, no emoji).
10. **Full suite clean**: `npm test`, `npm run lint`, all static gates pass. CCI-02 test (`claude-code-starter-pack.test.js`) untouched.
11. **T-02-03-01..04**: cross-tenant isolation inherited from existing middleware, XSS mitigated by React escaping, docs-secret-leak deferred to plan 02-01's recording checklist. No new attack surface.

</success_criteria>

<output>
After completion, create `.planning/phases/02-claude-code-beachhead/02-03-SUMMARY.md` capturing:
- What shipped (file inventory + line counts)
- Screencast URL status (placeholder only; plan 02-01 owns final backfill)
- .impeccable.md compliance spot-check (tokens used, anti-reference guardrail passed)
- Any deltas vs. plan (e.g., if groupEventsByDay was factored into a separate helper module)
- Handoff notes for plan 02-01: "Backfill `<SCREENCAST_URL>` in README.md line X and guides page line Y after recording"
</output>
