Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions .claude/skills/product-surface-scan/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,19 @@ This report is read by the **product + engineering teams**. A false surface / ti

Non-negotiable. When in doubt, fetch again or drop the claim. The orchestrator's link-review pass re-checks these against the live pages before publish.

### A WebFetch extraction is not a quote — grep the raw payload before claiming a gate

**Learned 2026-08-21, the hard way.** This scan reported that `docs.copilotkit.ai/premium/headless-ui` had gained an explicit premium gate, quoting a sentence about needing Cloud or a self-hosted license. It went into the report and into this file. The link-review pass then grepped the page's full 264KB Next.js payload — markdown extraction, raw-HTML text, and the script/RSC blocks — and found `license` **zero times**. Every hit for "Enterprise Intelligence Platform", "self-host" and "premium" was sidebar navigation, sidebar-tree JSON, or `<meta>`/OG tags. The "quote" was assembled from chrome, not from body copy. Both the report and this file had to be corrected.

So, for any claim that a feature **is gated, is Premium, is Enterprise-only, or changed tier**:

- **WebFetch markdown extraction alone is not sufficient evidence.** These are JS-heavy marketing and docs pages; the extractor flattens nav, sidebar JSON, and meta tags into the same text stream as the prose, and a gating sentence can be synthesized from fragments that are not next to each other on the rendered page.
- **Fetch the raw payload and grep it**, then confirm the words sit in **body copy** — not in a nav list, a sidebar tree, a `<meta>` tag, or an OG description. Say which, in your return.
- **A URL path is not a gate.** A page living under `/premium/` tells you how the docs are organized. It does not tell you the feature is paid. Report the path as a path.
- **Absence claims need the same treatment, and they hold up better** — the same pass confirmed "Coming Soon" was genuinely gone from `/copilotkit-intelligence` by grepping the full page twice. A negative you have grepped is stronger evidence than a positive you have only extracted.

When the two methods disagree, the raw grep wins and the claim is dropped.

## What it does

1. **Fetch the canonical pages** (below) with `WebFetch`. If a page 404s or is unreachable, record that (don't guess) — a page going live/dead is itself a signal (e.g. `copilotkit.ai/enterprise` is currently a 404; if it goes live, flag it).
Expand Down
21 changes: 18 additions & 3 deletions .claude/skills/weekly-report/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ Covering the **most recent complete Friday→Friday week** (Friday end-date incl
- **Auth.** Need a Composio API key with the `Tools` resource = **Write**, set as `COMPOSIO_API_KEY` in repo-root `.env`. All calls use header `x-api-key: $COMPOSIO_API_KEY`.
- **Get the connected account.** `GET https://backend.composio.dev/api/v3/connected_accounts?toolkit_slugs=reddit` → pick the `ACTIVE` account's `id` (`ca_…`; it changes whenever the auth config is recreated). If none is ACTIVE → render "source not configured" and move on. (Prior blockers, now avoided: the MCP entity mismatch + a default read-only key.)
- **Window:** rolling **last 90 days**. Compute cutoff = now − 90d (epoch seconds); filter posts by `created_utc >= cutoff` client-side (Reddit search has no native date filter).
- **Dedup ledger:** load `docs/community-signal/reddit-pulse-seen.json`. Skip any post `id` already listed. After the run, append ALL surfaced + dropped-as-noise ids under a new dated entry (so noise can't resurface).
- **Dedup ledger:** load `docs/community-signal/reddit-pulse-seen.json`. Skip any post `id` already listed. **Entries come in two shapes — bare `"1abc234"` strings (runs up to 2026-08-14) and `{"id": "1abc234", "created": "YYYY-MM-DD"}` objects (2026-08-21 onward) — so normalise before comparing:** flatten with `ids = entry.seen_ids.map(x => typeof x === 'string' ? x : x.id)`. A check shaped like `entry.seen_ids.includes(post.id)` or `new Set(entry.seen_ids)` matches **nothing** against the object shape, so every post recorded by a recent run would be re-reported next week — in the one file whose entire job is dedup. After the run, append ALL surfaced + dropped-as-noise ids under a new dated entry (so noise can't resurface).
**Record each id WITH its post date — `{"id": "<base36>", "created": "YYYY-MM-DD"}` — so the >90d prune is actually possible.** Older entries store bare id strings and therefore **cannot be pruned**: a run window (e.g. `2026-03-21..2026-06-19`) spans both sides of any later cutoff, so you can't tell from the entry which of its ids have aged out, and dropping one that is still in-window would let that post re-report. Until every entry carries dates, **skip the prune and say so** rather than guessing — the file is a few hundred ids, so carrying extras costs nothing while a wrong prune costs a duplicate report. Readers of both shapes must tolerate bare strings and `{id, created}` objects.
- **Execute via REST:** `POST https://backend.composio.dev/api/v3/tools/execute/<TOOL_SLUG>` with body `{"connected_account_id":"ca_…","arguments":{…}}`. Response posts nest under `.data.search_results.data.children[].data` (parse defensively — a `posts[]` array may also appear). Tools:
- `REDDIT_SEARCH_ACROSS_SUBREDDITS` — one call per `REDDIT_BRAND_TERMS` entry (default `CopilotKit`, `AG-UI`, `ag-ui`), `restrict_sr=false`, `sort` new + relevance.
Expand Down Expand Up @@ -115,6 +115,14 @@ Covering the **most recent complete Friday→Friday week** (Friday end-date incl
gh pr list --repo <repo> --state all --search "fixes #<num> OR closes #<num>" --json number,title,state,url,isDraft,mergedAt
```
Markers: `🛠️ Fix PR [#NNNN](url) OPEN` · `🛠️ Fix PR [#NNNN](url) MERGED <date>` · `🛠️ No fix PR yet.`
**The `fixes/closes` search is NOT sufficient on its own — always also read the issue's own `closedByPullRequestsReferences`.** The search depends on GitHub's indexing and on *when* you ran it; the field is authoritative and cheap:
```
gh issue view <n> --repo <repo> --json closedByPullRequestsReferences
gh api repos/<repo>/issues/<n>/timeline --paginate --jq '.[] | select(.event=="cross-referenced")'
```
**Re-run this on every ranked item during the final pre-publish refresh, not only during the first pull.** A fix PR can appear hours after the deep-read pass — and the busiest hour for it is right after a maintainer posts a root cause, which is exactly when a contributor picks the issue up.
*(Precedent 2026-08-21: Top issue #1 `#3510` published as "no PR yet". PR #6648 declared `closes #3510` and was opened **56 seconds after** the contributor's claim comment — a comment the correction pass did record, without re-checking for the PR that followed it. Detection had run before the PR existed; `closedByPullRequestsReferences` would have caught it either way. Rank was unaffected because the PR was open, but the card was wrong on the most-read line of the report.)*
**A claim comment is not a fix PR, and a fix PR is not a merge.** Record all three states distinctly: someone volunteered · a PR is open awaiting review · a PR is merged (→ ✅ Resolved).
**The MERGED/OPEN marker must come from the live PR state (`mergedAt` non-null), never inferred from the thread — always read the actual PR.** A **MERGED** closing PR (or a CLOSED issue) means the item is **resolved** → it goes to ✅ Resolved (step 9), not into Pain/Top issues with a "fix PR merged" note. "Fix PR MERGED" on a *still-open* issue is only valid when the merge genuinely didn't resolve it (e.g. partial fix) — say why. Procedurally-closed PRs (branch-name violation etc.) don't count as competing fixes — read the closing comment.

11. **Score & rank the Top issues** (see the ranking rubric in `front-door-triage`). First **record the naive order** — what you'd get ranking the candidates by loudness alone (engagement: 👍 + comments, recency, reporter count) — so the comparison page can show the delta. Then **score each candidate on the five axes** (surface tier · blast radius · severity · exposure · signal), using measurable inputs — `gh issue view --json reactionGroups,comments,labels`, fix-PR status from step 10, Discord distinct-reporter counts, and the enrichment. Sum, sort descending; the top 3–5 are the Top issues, ranked. Keep BOTH the scored table and the naive order — they get published in the ranking + comparison child pages (step 12). Community (CK vs AG-UI) is never an axis.
Expand All @@ -138,6 +146,7 @@ Covering the **most recent complete Friday→Friday week** (Friday end-date incl

14. **Spawn Subagent F — link review (after the pages are built).** A dedicated review pass over both published pages. Two jobs:
- **Claim support — open every Source link and ask whether it evidences the sentence beside it.** This is a SEPARATE check from coverage and correctness, and it is the one that has actually failed: a link can resolve, carry the right issue number, and match its quoted title, while still pointing at something that proves a *different* thing than the card asserts. Flag any card whose source, read cold by someone who knows nothing about the run, would lead them to the opposite conclusion. See "Source links are mandatory" for the 2026-08-14 precedent that both other checks passed.
- **Fix-plan PR links — the PR leads the line, every named PR is clickable, and every "no fix PR yet" is re-checked.** Walk every card's **Fix plan** / **Status** / **Fix** line and confirm it opens with the linked live PR (`[PR #NNNN](url) · …`) or with `No PR yet · …`. Any PR referenced anywhere in the line must be a markdown link — flag bare `#NNNN`. Any card claiming no fix PR must be re-verified against the issue's `closedByPullRequestsReferences` at review time, because a PR can land after the report is drafted. Flag stale "not started" lines.
- **Coverage — every item has a source link.** Scan every Top-issue card, Demand/Pain bullet, Docs bullet, Resolved row, Reddit Pulse thread, Enterprise reporter, and Patterns entity. **Any item with no source link is flagged.** For each flagged item, hand it to a search retrieval pass (gh search for the issue/PR, Discord `list_forum_threads`/search for the thread, Composio for the Reddit permalink) to find the canonical link. If a link is found → add it. If none can be found → **the item does not stay on the page** (remove it). No bare claims survive. (See "Source links are mandatory".)
- **Correctness.** For links that exist: every Discord thread URL's thread ID came from this run's pull (never memory/prior report) and the anchor matches the thread's title; every issue/PR number matches the title quoted next to it; every Reddit permalink is the one returned by Composio this run; external links (YouTube/Loom repro, docs) appear verbatim in the source — never reconstructed; anchor text names what the reader lands on.
- **🎯 Prospect LinkedIn links are sales-critical — VERIFY each against the person's own GitHub `social_accounts`.** For every prospect, run `gh api users/<login>/social_accounts`. If the person self-linked a `linkedin` URL there, the report's LinkedIn link **MUST equal it exactly** — a differing link is a wrong-person guess and must be corrected (or set to "LinkedIn not confirmed"). A self-linked account is authoritative; never publish a name-searched LinkedIn when the profile provides its own. (Precedent: the report linked `in/nchatlapalli` for Ashling Partners' Naveen when his GitHub self-linked `in/navaifanatic` — a different person.) **The published name must match the linked profile — verify it.** LinkedIn itself usually can't be fetched (returns HTTP 999), so confirm the person's FULL name (first + last) against a self-owned source that IS fetchable — their self-linked blog/personal site or a LinkedIn article they authored (byline). GitHub's `name` field is often just a first name or a handle — never publish that alone for a sales list. Don't attach a surname the sources don't support. (Precedent: published "Naveen" then a wrong-person link; his self-linked blog gave the full "Naveen Chatlapalli" and his GitHub self-linked the correct profile.)
Expand Down Expand Up @@ -271,7 +280,7 @@ Every reported item — in 🔝 Top issues, 🔥 Demand, 💢 Pain, and 📚 Doc
| 4 | **Description:** | The longer, very-readable explanation — 1–3 human sentences, no wall of text, no jargon dump. This is where detail lives (not the one-liner). |
| 5 | **CPK version:** | Just the version number — `v1.61.0`, `@copilotkitnext/core 1.54.0`, `unknown`, or `n/a — AG-UI`. **Number only** — the deprecated/experimental note goes in *What it is* / *Description* / *Fix plan*, not here. |
| 6 | **Impact:** | Human-readable — who it hits and how bad, in plain terms. (Demand: this is "why it matters".) |
| 7 | **Fix plan:** | Human-readable — shipped / in progress / in testing / not started + the PR or release. (Demand → **Status:**; Docs → **Fix:**.) |
| 7 | **Fix plan:** | **The linked PR comes FIRST, before the prose** — same shape as the *Source* line, so the reader can open the thread without hunting for the link. Format: `**Fix plan:** [PR #NNNN](url) · <status> — <prose>`. Write the anchor as `PR #NNNN` (not bare `#NNNN`, which reads as an issue), separated from the status by ` · `. When there is no PR: `**Fix plan:** No PR yet · <status> — <prose>`. When several PRs are in play, lead with the **live** one and mention the closed or competing ones in the prose, linked. Status vocabulary: shipped / in progress / in testing / not started.<br/>**The link is mandatory whenever a PR exists.** Never write a bare `#NNNN` or the words "a PR" with nothing to click — the reader's next action after a Fix plan is to open the thread, and an unlinked PR silently blocks it. A "No PR yet" claim must be backed by the issue's `closedByPullRequestsReferences` per step 10, not by a search. (Demand → **Status:**; Docs → **Fix:** — same leading-link rule.) |
| 8 | **Owner + Priority** | `**Owner:** _<blank — Nathan fills>_ · **Priority:** 🔴 High / 🟡 Medium / 🟢 Low`. **Mandatory on 🔝 Top issues, 🏢 Enterprise, 💢 Pain, and 📚 Docs cards** (optional only on 🔥 Demand). Owner always blank (never auto-named); Priority derived from the rank/severity (see `front-door-triage`). |

- **Docs cards:** prefix *What it is* with the type — `Drift` / `Gap` / `Links-bot`.
Expand Down Expand Up @@ -301,7 +310,7 @@ What goes in it (per leadership):
- **What:** the concrete failure.
- **CopilotKit version:** the version the reporter is on (from repro / body / comments), or `unknown` — see "Section item cards".
- **Impact:** who hit it and how bad.
- **Fix plan:** shipped / in-progress / not-started + the PR or release. Call out **"fixed same day"** when true.
- **Fix plan:** `[PR #NNNN](url) · <status> — <prose>` — **the linked PR leads the line**, before the prose, mirroring the *Source* line (see the card-format table). `No PR yet · …` when there is none. Call out **"fixed same day"** when true.
- **Owner + Priority** (meta line): `**Owner:** _<blank>_ · **Priority:** 🔴 High / 🟡 Medium / 🟢 Low` — owner left blank for Nathan to assign; priority derived from the rank (see `front-door-triage` "Priority from rank").
- **Tag each `[CK]` / `[AG-UI]` / `[CK + AG-UI]`** and link the canonical issue.
- **Front-page items get the CI-gap takeaway.** If something big shipped broken, ask "how did this ship?" — usually a missing smoke test.
Expand Down Expand Up @@ -484,6 +493,12 @@ The action checklist. **Draw the items from the `report-sources` evidence pass**
- Text channel: link to channel + include date.
- GitHub reporters: **link the handle to its GitHub profile** — `[``login``](https://github.com/login)` — and **link the `🏢 Company` badge to the company site** when enterprise (`🏢 [Amazon](https://www.amazon.com)`). Both URLs come from `enrich-reporter` (profile_url + company_url). The issue number itself is linked on the Source line. No plain-text handles or company names on any card.
- Append `🏢 <Company>` badge inline next to enterprise users' handles. Indie / solo get no badge.
- **`Reported by:` is the issue AUTHOR, taken from the author field — never the most recent commenter.** Get it from `gh issue view <n> --json author`, every time, for every card. Do not infer it from whoever appears in the pull's summary, whoever is loudest in the thread, or whoever opened the fix PR.
**This is load-bearing, not cosmetic — it feeds two mechanisms.** The final Top-issue tie-break is *community-confirmed over staff-filed*, and the enterprise reporter counts and 🎯 prospect roster are built from the same field. A wrong author silently changes a rank and can put a real person's name on work they never filed.
When the filer and the person doing the work differ, name both and say which is which: `**Reported by:** [author] , with [implementer] now implementing it` — the implementer belongs in **Fix plan**, never alone in *Reported by*.
**Mark staff-filed cards as staff-filed.** Our own team files issues too, and they are not community signal. Staff are invisible on their GitHub profile (no org membership, empty `company`) — **only the commit email reveals them**: `gh search commits --author <login>` and look for an `@copilotkit.ai` address. Do this for every reporter before publishing a community count.
*(Precedent 2026-08-21: five cards named the in-window commenter instead of the filer, and one — `#6408` — hid that it was staff-filed, which put it above a community-filed item at an equal score. The rank had to be corrected post-publish. One card named a handle that appears nowhere on its issue.)*
- **Never publish a bare "community authors: N" figure without its definition.** Distinct authors, distinct authors excluding staff, and distinct unaffiliated authors excluding contracted partner maintainers are three different numbers, and the gap between them is large. State which one you mean in the sentence that carries it.
- Identity collisions: merge same person across handles silently in the count; note inline if useful.
- Same-author duplicate-filing: one reporter, one signal.

Expand Down
13 changes: 13 additions & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,16 @@ packages/outpost/db/prisma/migrations/
# apps/web/src/app/**/templates/, exempting them from both the CI check and
# `pnpm format` with no way to bring them back into compliance.
/templates/

# Skill definitions under .claude/skills/ are hand-maintained spec prose, and
# several carry wide reference tables. Prettier pads every cell out to the
# widest one, so a one-word edit rewrites the whole table and the real change
# disappears into the diff. Anchored so it matches the directory at the repo
# root rather than any `skills` directory at any depth.
/.claude/skills/

# Operational dedup state for Reddit Pulse — appended by the weekly-report
# routine rather than written by hand, and 2-space by construction. Formatting
# it to the repo's 4-space width reindents all 412 lines and would do so again
# after every run.
/docs/community-signal/reddit-pulse-seen.json
Loading
Loading