Community Signal spec fixes, and template overrides wired through to storage - #238
Conversation
|
Review notes on the templates commit ( The good news first, because most of this holds up: the sandboxed-iframe replacement for Two worth settling before this leaves draft. A saved override does not reach a real invite email yet. The Smaller things, all optional:
Happy to be argued out of any of these, particularly the |
jerelvelarde
left a comment
There was a problem hiding this comment.
Reviewed both halves. The skill-spec commit (08cd06a) is accurate and matches its description. The code commit (5427278) is where the problems are, and I verified the two biggest ones against the tree myself rather than taking the review at face value.
The sandboxed-iframe work is correct and genuinely closes the stored-XSS sink: sandbox="" with neither allow-scripts nor allow-same-origin, srcDoc, asserted structurally with an exact-value test, and no other dangerouslySetInnerHTML or rehype-raw sink left in apps/web. The tests are real — the loader/renderer/markdown pipeline actually execute; only session and Prisma are mocked.
Blocker — a saved override never reaches a real email, and the UI says it did
apps/web/src/app/api/team/invite/route.ts:72 and .../invite/resend/route.ts:52.
The write path works: PUT upserts templateOverride, the row lands, and the page does await fetchTemplates(); setSuccess('Template saved successfully'). Every read surface then confirms it — the list flips the row from Default to Custom, GET returns the override, the preview renders it.
The one surface that matters does not. sendEmail only consults an override when handed a dbLookup (packages/outpost/shared/src/email/sender.ts:166, :171), and neither invite call site passes one. The invitee gets the on-disk copy.
So an admin edits a template, is told it saved, sees a Custom badge, sends an invite, and the edit is silently ignored — with no trace anywhere. That is exactly the silent-success failure this commit's own test header says it is fixing. You flagged it yourself in the PR body as "worth settling before this leaves draft" — the PR is now out of draft with it still open, which is why I am gating on it rather than filing it.
Either export findOverride (or move it to a shared lib) and pass it as dbLookup at both call sites, with a test that a stored override changes the subject/body handed to the transport — or, if it is genuinely deferred, the editor has to stop claiming otherwise: no Custom badge and no unqualified success banner until the override is actually wired.
Also blocking, and undisclosed — the feature is dead in the production image
I checked this because it seemed too big to be true. It holds:
templates/is seven.mdfiles at the repo root..dockerignore:7excludes*.md, so they never enter the build context — the pruner'sCOPY . .cannot pick them up.apps/web/Dockerfile's runner stage copies.next/standalone,.next/static,public, the Prisma bits andstart.sh. Nothing copiestemplates/.
So in production findTemplatesDir() finds nothing, loadFromFilesystem returns null for every slug, listTemplateSlugs() returns [], and every new fail-closed gate 404s. The 35 tests pass only because they run from apps/web, where the repo-root templates/ exists. Worth confirming against a built image before this merges — if I have the packaging wrong I would rather be corrected than have you take my word for it.
The slug still reaches the filesystem unvalidated
loader.ts:73-75 does join(dir, ${slug}.md) with no validation, and Next decodes percent-encoding in a dynamic segment. ..%2F..%2F..%2Fsomething becomes a traversal. It is admin-gated and bounded to .md files, so the blast radius is small — but it is the second item your own PR body listed as unsettled, and a /^[a-z0-9-]+$/ test at the route boundary closes it in one line.
Two things to fix in the skill commit
-
reddit-pulse-seen.json:389changes shape mid-file. The new run'sseen_idsare{id, created}objects; all twelve prior runs are bare strings.weekly-report/SKILL.md:75says to "skip any postidalready listed", and any check shaped likeseen.includes(post.id)ornew Set(entry.seen_ids)matches nothing against the new shape — so this run's posts will be re-reported next week. This is the one file in the repo whose entire job is dedup state. -
weekly-report/SKILL.md:150— the Coverage bullet lost its three-space indent, so it escapes the sub-list under step 14 and renders as a top-level item. Every sibling above and below keeps the indent.
Process, not code
This PR carries two unrelated changes. That is already awkward for review, and it is how both of the items you flagged as unsettled ended up shipping out of draft — they are buried under a skill-spec description. Worth splitting the code commit out; I will re-review it on its own immediately.
The fix-PR detection change (reading closedByPullRequestsReferences and re-running at the final refresh) is a genuinely good catch, and the four cards it would have corrected make the case on their own.
…ed on Addresses the two skill-commit findings on #238. The ledger's newest run records `{id, created}` objects while the twelve before it store bare id strings. The commit already noted that readers must tolerate both, but that sentence sat at the end of a dense paragraph about pruning — 200 words after the "Skip any post `id` already listed" line a reader actually acts on. So the instruction that matters could be followed correctly and still produce `entry.seen_ids.includes(post.id)`, which matches nothing against the object shape and would re-report every post the recent runs recorded. In the one file whose entire job is dedup. The normalisation now sits in the dedup instruction itself, with the flatten expression and the failure it prevents. Kept the richer `{id, created}` shape rather than flattening the new run back to bare strings: the dates are what makes the >90d prune possible at all, and the skill explicitly says the prune stays skipped until every entry carries them. Dropping them to restore uniformity would trade a real capability for tidiness. Also restores the three-space indent on the Coverage bullet under step 14, so it stays in the sub-list with its siblings instead of rendering as a top-level item. Unrelated web/template work that was previously on this branch is split out to fix/226-template-preview-xss-and-overrides, so this PR is Community Signal only.
3f8fe43 to
724c5ac
Compare
|
Thanks Jerel — the split you asked for is done, and the two skill findings are fixed in The web half is #248, commit The dedup shape — you were right, with one wrinkle worth recordingThe commit did already carry a line saying readers must tolerate both shapes. The problem is where it sat: about 200 words below the The normalisation now lives in the dedup instruction itself, with the flatten expression and the consequence spelled out:
One judgement call, flagging it in case you'd rather go the other way: I kept the Verified the file still parses: 10 runs, both shapes present by design. The Coverage bulletIndent restored, so it sits with On the fix-PR detection changeGlad that one reads well — the |
…ed on Addresses the two skill-commit findings on #238. The ledger's newest run records `{id, created}` objects while the twelve before it store bare id strings. The commit already noted that readers must tolerate both, but that sentence sat at the end of a dense paragraph about pruning — 200 words after the "Skip any post `id` already listed" line a reader actually acts on. So the instruction that matters could be followed correctly and still produce `entry.seen_ids.includes(post.id)`, which matches nothing against the object shape and would re-report every post the recent runs recorded. In the one file whose entire job is dedup. The normalisation now sits in the dedup instruction itself, with the flatten expression and the failure it prevents. Kept the richer `{id, created}` shape rather than flattening the new run back to bare strings: the dates are what makes the >90d prune possible at all, and the skill explicitly says the prune stays skipped until every entry carries them. Dropping them to restore uniformity would trade a real capability for tidiness. Also restores the three-space indent on the Coverage bullet under step 14, so it stays in the sub-list with its siblings instead of rendering as a top-level item. Unrelated web/template work that was previously on this branch is split out to fix/226-template-preview-xss-and-overrides, so this PR is Community Signal only.
724c5ac to
bd8be35
Compare
…d surface-claim verification Three rule fixes from the Aug 14-21 run, each caught by a verification pass after the report had already been published. Reporter attribution is load-bearing, not cosmetic `Reported by:` must come from the issue author field, never from the most recent commenter. Five cards named the commenter instead of the filer, and one named a handle that appears nowhere on its issue. The final Top-issue tie-break is community-filed over staff-filed, and the enterprise counts and prospect roster read the same field -- so a wrong author silently changes a rank. It did: #6408's card named the volunteer implementer, hiding that it was staff-filed, which placed it above a community-filed item at an equal score. Also adds staff detection (staff are invisible on their GitHub profile; only the commit email reveals them) and forbids publishing a bare "community authors: N" figure without stating which definition it uses -- the three plausible readings differed by a factor of two this week. Fix plan leads with the linked PR, and detection reads the authoritative field Card format is now `**Fix plan:** [PR #NNNN](url) - <status> - <prose>`, mirroring the Source line, or `No PR yet - ...` when there is none. The reader's next action after a Fix plan is to open the thread. More importantly, the `fixes/closes` PR search is no longer sufficient on its own -- it depends on GitHub indexing and on when it ran. Detection must also read the issue's own closedByPullRequestsReferences and timeline, and re-run at the final pre-publish refresh. Four cards shipped claiming no PR when an open one existed, including one where a Microsoft engineer filed the bug and its fix the same day, and one where two contributors are building the same integration in different languages because neither got a reply. A WebFetch extraction is not a quote For any gating / Premium / tier claim, grep the raw payload and confirm the words sit in body copy. A claimed premium gate was assembled from sidebar nav and meta tags; the page's full 264KB payload contains no licence wording at all. A URL under /premium/ is a path, not a gate. When extraction and grep disagree, the grep wins. Absence claims verified the same way hold up better, and one did. Also records this run's Reddit dedup entry. Prune still skipped: earlier entries store bare id strings with no dates, so they cannot be aged out safely. Report content itself lives only in Notion, per the standing rule.
…ed on Addresses the two skill-commit findings on #238. The ledger's newest run records `{id, created}` objects while the twelve before it store bare id strings. The commit already noted that readers must tolerate both, but that sentence sat at the end of a dense paragraph about pruning — 200 words after the "Skip any post `id` already listed" line a reader actually acts on. So the instruction that matters could be followed correctly and still produce `entry.seen_ids.includes(post.id)`, which matches nothing against the object shape and would re-report every post the recent runs recorded. In the one file whose entire job is dedup. The normalisation now sits in the dedup instruction itself, with the flatten expression and the failure it prevents. Kept the richer `{id, created}` shape rather than flattening the new run back to bare strings: the dates are what makes the >90d prune possible at all, and the skill explicitly says the prune stays skipped until every entry carries them. Dropping them to restore uniformity would trade a real capability for tidiness. Also restores the three-space indent on the Coverage bullet under step 14, so it stays in the sub-list with its siblings instead of rendering as a top-level item. Unrelated web/template work that was previously on this branch is split out to fix/226-template-preview-xss-and-overrides, so this PR is Community Signal only.
bd8be35 to
b87572e
Compare
The format check added by #250 runs against the files a PR touches, which is the right shape -- but two kinds of file in this repo get worse when prettier formats them, and both are now failing PRs that have nothing wrong with them. .claude/skills/*.md are hand-maintained spec prose carrying 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. On #238 that is 305 changed lines across two SKILL.md files. docs/community-signal/reddit-pulse-seen.json is operational dedup state, appended by the weekly-report routine rather than written by hand, and 2-space by construction. The repo's tabWidth is 4, so formatting it reindents all 412 lines -- and the next run rewrites it 2-space again, so the check would break again every week. Both patterns are anchored with a leading slash, for the reason the /templates/ entry above them records: unanchored, a gitignore-style pattern matches a directory of that name at any depth. Verified against the CI step's own command over #238's three changed files: fails before, passes after. Repo-wide `prettier --check .` is unchanged at 334 pre-existing warnings, and .claude/settings.json stays covered -- the only tracked files this exempts are the 13 markdown skill definitions.
jerelvelarde
left a comment
There was a problem hiding this comment.
Approving the content. Both findings from my last pass are fixed, and the split is clean — #248 carries 5427278 with my three code findings recorded against the code they concern. That was the right call.
Verified rather than taken on trust:
- Dedup normalisation now sits in the instruction a reader acts on, with the flatten expression and the consequence. That was the actual defect — the old line was correct but 200 words from the point of use.
- Coverage bullet indent — all five step-14 siblings (
Claim support,Fix-plan PR links,Coverage,Correctness,Prospect LinkedIn) are at matching indent. - File still parses — 10 runs, both id shapes present.
On the {id, created} judgement call: keep it. Restoring uniformity would trade the only thing that makes the prune possible for a tidier file, and the skill already tells readers to tolerate both shapes at the point they compare. Right trade.
Blocking merge, but not your bug
Format check (changed files) is red on all three files. I checked out origin/main's versions of the same three files and ran prettier against them — they fail identically. The check arrived with #250 and this branch's merge of main pulled it in; it gates any PR that touches a file that was never formatted, regardless of what the PR changed.
Please don't fix it here. Prettier wants ~1,200 lines: it reindents the SKILL.md list nesting, repads every table, and 4-space-reindents the ledger. On these files that is a content change — this PR's own review turned up an indentation bug where the Coverage bullet escaped step 14, which is exactly the class of damage a wholesale reindent causes silently.
.prettierignore already carries the argument, for /templates/:
Prettier reflows markdown — wrapping lines, renumbering lists, normalising emphasis — and every one of those changes what lands in a customer's inbox.
Skill specs are read by agents as instructions, so the same reasoning holds with the same force. Suggest a separate one-line PR adding .claude/skills/ (and docs/community-signal/*.json) to .prettierignore — that unblocks this and every future skill-spec PR. Happy to open it if you'd rather not context-switch.
Worth fixing before merge (cheap)
-
Title and body are stale. The title still ends "and template overrides wired through to storage", and the body still opens with the two-commit note describing
5427278and its two unsettled items — none of which are in this PR any more. It also says "Opening as a draft" while the PR is not a draft. This becomes the permanent merge-commit record, so it's worth the edit. -
_commentin the ledger contradicts the new rule. It still instructs "prune ids older than 90 days", while the run note and the SKILL.md now say the prune is skipped and explain why bare-string entries make it unsafe. A reader following_commentdoes the thing the spec forbids. One line, in a file this PR already edits. -
Minor: the two pre-existing
_comment/_notelines got their em dashes rewritten to—escapes. Valid JSON and harmless, but it is unexplained churn in a hand-read file and suggests something other than a plain editor wrote it. Worth knowing which tool did it, in case it touches more next run.
Open questions from the body
- Leading-link on Resolved rows — leave as-is. The rows are scanned, not acted on; the format earns its cost on cards where the next action is opening the thread.
- Severity tier for a safety control silently disabling itself — worth adding. Two items needing a priority override in one week is the rubric mispricing the axis, not the overrides being wrong. Separate PR, since it changes every future run.
Approving so this isn't waiting on me — merge once CI is green.
Three rule changes to the Community Signal skill suite, from the Aug 14-21 run. Opening as a draft since more commits are likely.
Nathan spotted both of the things that drove the biggest change here: that the week's top issue had a fix PR the card never mentioned, and that the Fix plan line should lead with the PR link the same way
Source:does. Both are in this commit, and the second turned out to be the more useful of the two, because it made the first kind of gap visible at a glance.1. The Fix plan line leads with the linked PR
New shape, mirroring the
Source:line:Anchor text is
PR #NNNNrather than a bare#NNNN, so it does not read as an issue number. When more than one PR is in play the live one leads and the closed or competing ones are linked in the prose. Same rule for Demand'sStatus:and Docs'Fix:.The reason to put it first: the reader's next action after a Fix plan is to open the thread. A link sitting mid-sentence, or missing, stops that.
There is a matching check in the link-review pass so the format is verified on every run rather than remembered.
2. Fix-PR detection reads the authoritative field, and re-runs late
gh pr list --search "fixes #N OR closes #N"depends on GitHub's search indexing and on when in the run it executes, so a PR can exist and not appear. Detection now also reads the issue's ownclosedByPullRequestsReferencesand its timeline, and re-runs on every ranked item at the final pre-publish refresh.The timing matters more than it looks. On
CopilotKit#3510, PR #6648 was opened 56 seconds after the contributor's comment claiming the issue. Any pass that reads the comment and stops there will miss the PR that follows it.Four cards on this week's report were in that position, and the field would have caught all four:
ag-ui#2447— PR #2448, open since 2026-08-18. The reporter, a Microsoft engineer, filed the bug and the fix on the same day.ag-ui#167— PR #2210, an open Python implementation since 2026-07-19, while a second contributor builds the TypeScript version. Neither has had an answer, so two people are currently building the same integration.CopilotKit#6507— PR #6628 implemented the request in full and closed unmerged the same day, unreviewed.CopilotKit#6526— two PRs open, one per competing root cause, with no call made between them.Also worth stating plainly, because the three states get collapsed: a claim comment is not a PR, and a PR is not a merge. All three are recorded distinctly now.
The good news is what this surfaced. Once those four were corrected, seventeen fixes are written and waiting on review across both repos, several of them one-line changes on the worst open bugs. That is a healthier problem to have than a shortage of contributions, and it is now the report's lead finding.
3. Reporter attribution comes from the author field
Reported by:is taken fromgh issue view --json author, not from whoever commented most recently.This one is load-bearing rather than cosmetic. The last Top-issue tie-break is community-filed over staff-filed, and the enterprise counts and prospect roster read the same field, so the author determines a rank. Where the filer and the person doing the work differ, both are named and the implementer belongs on the Fix plan line.
Two supporting rules:
companyon their GitHub profile, sogh search commits --author <login>and the@copilotkit.aiaddress is what identifies them.4. Gating and tier claims are checked against the raw payload
For any claim that a feature is Premium, Enterprise-only or gated, the check is now a grep of the raw page payload, with confirmation that the words sit in body copy rather than in navigation, a sidebar tree or a
<meta>tag.docs.copilotkit.ai/premium/headless-uiis the worked example: 264KB of payload, and no licence or Cloud requirement anywhere in it. A URL under/premium/describes how the docs are organised, not what the feature costs.Where a text extraction and a raw grep disagree, the grep decides. Absence claims verified the same way hold up well, and one did this week: "Coming Soon" is genuinely gone from
/copilotkit-intelligence, confirmed by grepping the full page twice.Also here
This run's Reddit dedup entry. The prune is still skipped, because earlier entries store bare id strings with no dates and a run window can span the 90-day cutoff, so ageing one out risks re-reporting a post. Carrying extra ids costs nothing by comparison.
Report content is not in this commit. That lives only in Notion, per the standing rule.
Two open questions
Happy to go either way on both, and neither is changed here:
✅ Resolvedtable rows? Right now it applies to cards only, and the rows carry their PR in prose.