Skip to content

feat(web): pull the four new design-system primitives into the app (HT-93) - #103

Merged
zaridan merged 2 commits into
mainfrom
feat/ht-93-ds-new-primitives
Jul 20, 2026
Merged

feat(web): pull the four new design-system primitives into the app (HT-93)#103
zaridan merged 2 commits into
mainfrom
feat/ht-93-ds-new-primitives

Conversation

@zaridan

@zaridan zaridan commented Jul 20, 2026

Copy link
Copy Markdown
Contributor

Closes HT-93.

Why

The Claude Design "Helpthread" project gained four net-new primitives on 2026-07-19, authored in response to a request from app work. They were never pulled down. They were sitting in templates/new-primitives/Primitives.jsx, unused.

Primitive Maps to
SplitButton Send + "Send and close" / "Send and snooze"
CommandMenu Saved-replies inserter (saved_replies table)
SnoozePicker Presets + calendar (snooze-wake cron)
CredentialRow / PasskeyList Passkey management (webauthn_credentials, HT-75)

The conversion

Mechanical, with every style value preserved. The design source is browser-rendered, so it used React.createElement, module.exports, and local duplicate copies of MenuItem/Button. Those become JSX, ESM exports, and real imports from ./MenuItem and ./Button.

Dropped: the Showcase, its layout scaffolding (Section/Panel/StateLabel/Stack/twoUp), and the specimen fixtures. Design-project demo code, not app code.

Shared helpers (icon glyphs, the RING token, date formatters) go in primitives-support.jsx — the source carried one copy in a single file, and splitting that file must not turn one definition into four.

One intentional behavioral difference

The design source froze a reference clock (NOW = 2026-07-19 14:30) so specimen times wouldn't drift between renders. Correct for a showcase, wrong for the appSnoozePicker would compute "tomorrow" from a date in the past. Replaced with now(), commented at the site.

Five lint findings suppressed, not fixed

Biome marks most of these FIXABLE, but each "fix" would be silent drift from the approved design:

  • noAutofocus ×2 — both fields appear only in response to an explicit user action (opening the command menu; clicking Rename). Removing autofocus would strand keyboard users.
  • noArrayIndexKey ×2 — weekday initials repeat (two T, two S) in a fixed-length row that never reorders; month-padding cells have no identity of their own.
  • useExhaustiveDependencies ×1q is the effect's trigger, not a read. Dropping it would stop the highlight resetting when the query changes.

Each carries a biome-ignore with the reason inline.

Verification

Check Result
biome check web/src/components/ds/core/ exit 0
tsc -p tsconfig.json exit 0
next build exit 0

None of the existing 16 ds/ components were modifiedgit status shows only the 10 new files. The formatter pass was run with --linter-enabled=false specifically so it couldn't touch them or apply behavior-changing lint fixes.

Not in this PR

  • Wiring these into features — separate tickets. They compile and are ready; nothing imports them yet.
  • Promoting them to components/core/ upstream so the design project's own library matches. Right now they live in templates/ remotely, so they're staged on both sides.
  • The Biome-formatting problem. web/src/components/ds/ is Biome-formatted, so it no longer byte-matches the design project despite CLAUDE.md requiring verbatim copies. Button.jsx was confirmed semantically identical — the entire diff is quotes, semicolons, and wrapping. This makes byte comparison useless as a drift detector and wants a deliberate call: exclude ds/ from Biome, or adopt a normalizing comparison. Filed in HT-93's description.

Reviewer attention

The claim worth checking is pixel fidelity — that no style value drifted in the createElement → JSX conversion. Worth a spot-check against templates/new-primitives/Primitives.jsx in the design project, particularly SplitButton's seam/border handling and SnoozePicker's MiniCalendar cell states, which had the densest conditional styling.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features
    • Added searchable, keyboard-navigable saved-reply menus.
    • Added passkey displays with rename, revoke confirmation, and add-passkey actions.
    • Added snooze controls with presets or custom date and time selection.
    • Added split buttons with loading, disabled, and dropdown action states.
    • Added shared icons, focus handling, and date/time formatting support.

…T-93)

The Claude Design "Helpthread" project gained four net-new primitives on
2026-07-19, authored in response to a request from app work. They were
never pulled down. This adds them to web/src/components/ds/core/:

- SplitButton — primary action with an attached caret menu
- CommandMenu — searchable saved-replies inserter
- SnoozePicker — presets plus a custom calendar and time
- CredentialRow / PasskeyList — passkey management, rename and two-step revoke

Shared icon glyphs, the focus-ring token, and the date formatters live in
primitives-support.jsx rather than being duplicated per component — the
design source carried one copy in a single file, and splitting that file
must not turn one definition into four.

Conversion from the design source was mechanical and every style value is
preserved: the source is browser-rendered, so it used React.createElement,
module.exports, and local duplicate copies of MenuItem/Button. Those become
JSX, ESM exports, and real imports from ./MenuItem and ./Button. The
Showcase, its layout scaffolding, and the specimen fixtures are dropped —
they are design-project demo code, not app code.

One intentional behavioral difference, commented at the site: the design
source froze a reference clock (NOW = 2026-07-19 14:30) so specimen times
would not drift between renders. The app uses real time via now(); a frozen
clock would make SnoozePicker compute "tomorrow" from a past date.

Five lint findings are suppressed with reasons rather than auto-fixed,
because Biome's fixes would each be silent behavior drift from the design:
two noAutofocus (both fields appear only in response to an explicit user
action), two noArrayIndexKey (repeating weekday initials in a fixed row,
and month-padding cells that have no identity), and one
useExhaustiveDependencies (q is the effect's trigger, not a read).

Verified: biome check exit 0, tsc exit 0, next build exit 0. None of the
existing 16 ds components were modified.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jul 20, 2026

Copy link
Copy Markdown

Review Change Stack

Important

Review skipped

Auto incremental reviews are disabled on this repository.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 7bd8570e-2203-4113-a9a8-ca69cc74333f

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds shared design-system primitives and declarations alongside four interactive React components: a searchable command menu, passkey credential controls, a snooze picker, and a split action button.

Changes

Design system components

Layer / File(s) Summary
Shared icons, focus, and date utilities
web/src/components/ds/core/primitives-support.*
Adds reusable SVG icons, focus-ring state handling, date constants, timestamp formatting, and TypeScript declarations.
Searchable command menu
web/src/components/ds/core/CommandMenu.*
Adds query filtering, keyboard navigation, highlighted-item scrolling, empty states, and reply selection.
Snooze date and time picker
web/src/components/ds/core/SnoozePicker.*
Adds preset snooze times, custom calendar/time selection, wake-time resolution, and snooze submission.
Credential and split-action controls
web/src/components/ds/core/CredentialRow.*, web/src/components/ds/core/SplitButton.*
Adds passkey rename/revoke/list interactions and a loading-aware split button with selectable menu actions.

Estimated code review effort: 4 (Complex) | ~60 minutes

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 54.55% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: adding four design-system primitives, though "into the app" is slightly broad.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/ht-93-ds-new-primitives

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🧹 Nitpick comments (3)
web/src/components/ds/core/primitives-support.d.ts (1)

2-9: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Prefer React.JSX.Element over the deprecated global JSX.Element namespace.

React 19 removed the global JSX namespace in favor of React.JSX. While @types/react@19 still ships a backwards-compatible global for now, it is deprecated and will be removed in a future release. Since this file doesn't import React, add a type-only import and update the return types.

♻️ Proposed refactor
+import type { JSX } from 'react'
+
 export declare const RING: string
-export declare function chevron(dir?: 'down' | 'up' | 'left' | 'right', sz?: number): JSX.Element
-export declare function IconKey(sz?: number): JSX.Element
-export declare function IconSearch(sz?: number): JSX.Element
-export declare function IconReply(sz?: number): JSX.Element
-export declare function IconClock(sz?: number): JSX.Element
-export declare function IconPlus(sz?: number): JSX.Element
-export declare function IconPencil(sz?: number): JSX.Element
-export declare function IconTrash(sz?: number): JSX.Element
+export declare function chevron(dir?: 'down' | 'up' | 'left' | 'right', sz?: number): JSX.Element
+export declare function IconKey(sz?: number): JSX.Element
+export declare function IconSearch(sz?: number): JSX.Element
+export declare function IconReply(sz?: number): JSX.Element
+export declare function IconClock(sz?: number): JSX.Element
+export declare function IconPlus(sz?: number): JSX.Element
+export declare function IconPencil(sz?: number): JSX.Element
+export declare function IconTrash(sz?: number): JSX.Element

With import type { JSX } from 'react', the JSX.Element references resolve to React.JSX.Element without needing the global namespace.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web/src/components/ds/core/primitives-support.d.ts` around lines 2 - 9, Add a
type-only React import in the declarations file and update the return types of
chevron, IconKey, IconSearch, IconReply, IconClock, IconPlus, IconPencil, and
IconTrash to use the imported React JSX element type instead of the deprecated
global JSX.Element.
web/src/components/ds/core/SplitButton.jsx (1)

99-122: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Caret toggle lacks aria-expanded/aria-haspopup.

The caret button controls the dropdown (open state) but doesn't expose aria-haspopup="menu"/aria-expanded={open} for assistive tech. Since ds/core files must remain verbatim copies of the design source, please confirm whether the prototype already includes these attributes; if not, this should be added upstream first rather than patched locally.

As per coding guidelines, "Design-system files under web/src/components/ds/ must remain verbatim copies of the Claude Design prototype/design-system source; improvements must be made upstream in the design project first."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web/src/components/ds/core/SplitButton.jsx` around lines 99 - 122, Verify
whether the upstream design-system prototype’s caret button includes
aria-haspopup="menu" and aria-expanded={open}; if present, synchronize this
SplitButton caret button with the prototype. If absent, make no local change and
instead flag the upstream design source for adding these attributes before
updating the verbatim copy.

Source: Coding guidelines

web/src/components/ds/core/CredentialRow.jsx (1)

290-290: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

key={c.name} assumes credential names are unique.

Credential has no id field, and renaming is user-controlled, so two credentials could end up sharing a name, causing React key collisions/incorrect reconciliation. Consider a stable id on Credential for the list key.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web/src/components/ds/core/CredentialRow.jsx` at line 290, Update the
credential list rendering around CredentialRow to use a stable, unique
Credential identifier for the React key instead of c.name. Add or propagate an
id field on Credential as needed, and pass that identifier to key while
preserving the existing row props and ordering.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@web/src/components/ds/core/CommandMenu.d.ts`:
- Line 15: Update the CommandMenu declaration to import the JSX type namespace
from React and retain JSX.Element as the return type of CommandMenu, removing
its reliance on the ambient global JSX namespace.

In `@web/src/components/ds/core/CommandMenu.jsx`:
- Around line 47-49: Update the ArrowDown handling in the CommandMenu highlight
state update to clamp the highlighted index at zero when filtered is empty,
while preserving the existing upper bound for non-empty results. Ensure hi
remains non-negative so later-arriving items can be highlighted and selected.

In `@web/src/components/ds/core/CredentialRow.jsx`:
- Around line 230-291: Update PasskeyList to accept onRename and onRevoke and
forward both callbacks to every rendered CredentialRow, preserving the
credential and new name arguments used by CredentialRow. Add the corresponding
optional callback declarations to PasskeyListProps in CredentialRow.d.ts.

In `@web/src/components/ds/core/SnoozePicker.jsx`:
- Around line 169-174: Update the resolved useMemo in SnoozePicker to validate
that time is non-empty and contains valid hour/minute values before calling
setHours; return a safe non-submittable state when invalid. Ensure the
confirmation display does not use an invalid Date, and disable or guard the
submission path so onSnooze cannot receive an invalid resolved value.
- Around line 5-8: Update laterToday() so it never returns a past timestamp:
after 5 PM, either hide the Later today preset or roll its timestamp forward to
the next valid slot, while preserving the existing 5 PM behavior before that
cutoff.

---

Nitpick comments:
In `@web/src/components/ds/core/CredentialRow.jsx`:
- Line 290: Update the credential list rendering around CredentialRow to use a
stable, unique Credential identifier for the React key instead of c.name. Add or
propagate an id field on Credential as needed, and pass that identifier to key
while preserving the existing row props and ordering.

In `@web/src/components/ds/core/primitives-support.d.ts`:
- Around line 2-9: Add a type-only React import in the declarations file and
update the return types of chevron, IconKey, IconSearch, IconReply, IconClock,
IconPlus, IconPencil, and IconTrash to use the imported React JSX element type
instead of the deprecated global JSX.Element.

In `@web/src/components/ds/core/SplitButton.jsx`:
- Around line 99-122: Verify whether the upstream design-system prototype’s
caret button includes aria-haspopup="menu" and aria-expanded={open}; if present,
synchronize this SplitButton caret button with the prototype. If absent, make no
local change and instead flag the upstream design source for adding these
attributes before updating the verbatim copy.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: b30c6b5f-14f6-40b1-a7be-a6a7afa78542

📥 Commits

Reviewing files that changed from the base of the PR and between aca83cb and b5afa9a.

📒 Files selected for processing (10)
  • web/src/components/ds/core/CommandMenu.d.ts
  • web/src/components/ds/core/CommandMenu.jsx
  • web/src/components/ds/core/CredentialRow.d.ts
  • web/src/components/ds/core/CredentialRow.jsx
  • web/src/components/ds/core/SnoozePicker.d.ts
  • web/src/components/ds/core/SnoozePicker.jsx
  • web/src/components/ds/core/SplitButton.d.ts
  • web/src/components/ds/core/SplitButton.jsx
  • web/src/components/ds/core/primitives-support.d.ts
  • web/src/components/ds/core/primitives-support.jsx

width?: number
initialQuery?: string
}
export declare function CommandMenu(props: CommandMenuProps): JSX.Element

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

Does @types/react19.2.4 expose a global JSX namespace, or should library declaration files import JSX fromreactand useJSX.Element?

💡 Result:

No, @types/react 19.2.4 does not expose a global JSX namespace [1][2]. The global JSX namespace was removed in @types/react version 19 to prevent type pollution and conflicts with other UI libraries that also utilize JSX [3][4][1]. For library declaration files and general TypeScript code, you should import the JSX namespace directly from React and use React.JSX.Element instead of the previously global JSX.Element [1][5]. You can access it by importing: import { JSX } from 'react'; // Use as const myElement: JSX.Element =

; If you need to perform module augmentation to add intrinsic elements, you can no longer use declare global [6][5]. Instead, you must target the specific React module based on your compiler's JSX runtime setting [3][4][6]: declare module 'react' { namespace JSX { interface IntrinsicElements { 'my-element': { myElementProps: string }; } } } The exact module name to augment depends on your tsconfig.json configuration [3][4]: - For "jsx": "react-jsx", use 'react/jsx-runtime'. - For "jsx": "react-jsxdev", use 'react/jsx-dev-runtime'. - For "jsx": "react" or "preserve", use 'react'.

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

file='web/src/components/ds/core/CommandMenu.d.ts'

echo '--- file outline ---'
wc -l "$file"
echo
cat -n "$file"

echo
echo '--- search for JSX imports/usages in nearby ds core declarations ---'
rg -n --hidden --glob 'web/src/components/ds/core/*.{d.ts,ts,tsx,js,jsx}' 'from '\''react'\''|namespace JSX|JSX\.Element|import type \{ JSX \}' .

Repository: Helpthread/helpthread

Length of output: 4929


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- package.json ---'
if [ -f package.json ]; then
  cat -n package.json | sed -n '1,240p'
fi

echo
echo '--- tsconfig files mentioning jsx or types ---'
for f in $(git ls-files '*tsconfig*.json' 'package.json' 'pnpm-lock.yaml' 'yarn.lock' 'package-lock.json' 2>/dev/null); do
  case "$f" in
    *tsconfig*.json|package.json)
      echo "### $f"
      cat -n "$f" | sed -n '1,240p'
      echo
      ;;
  esac
done

echo '--- any JSX namespace declarations in repo ---'
rg -n --hidden --glob '!**/node_modules/**' 'declare\s+namespace\s+JSX|namespace\s+JSX|interface\s+IntrinsicElements' .

Repository: Helpthread/helpthread

Length of output: 4990


Reference React’s scoped JSX namespace.
web/src/components/ds/core/CommandMenu.d.ts:15 should not depend on the ambient global JSX namespace. Import type { JSX } from 'react' and keep the return type as JSX.Element so this public declaration stays compatible with React 19 consumers.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web/src/components/ds/core/CommandMenu.d.ts` at line 15, Update the
CommandMenu declaration to import the JSX type namespace from React and retain
JSX.Element as the return type of CommandMenu, removing its reliance on the
ambient global JSX namespace.

Comment on lines +47 to +49
if (e.key === 'ArrowDown') {
e.preventDefault()
setHi((i) => Math.min(i + 1, filtered.length - 1))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Keep the highlighted index non-negative for empty results.

Line 49 sets hi to -1 when filtered is empty. If items later arrive without changing q, the menu has no highlighted row and Enter cannot select one.

Proposed fix
-      setHi((i) => Math.min(i + 1, filtered.length - 1))
+      setHi((i) => Math.max(0, Math.min(i + 1, filtered.length - 1)))
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (e.key === 'ArrowDown') {
e.preventDefault()
setHi((i) => Math.min(i + 1, filtered.length - 1))
if (e.key === 'ArrowDown') {
e.preventDefault()
setHi((i) => Math.max(0, Math.min(i + 1, filtered.length - 1)))
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web/src/components/ds/core/CommandMenu.jsx` around lines 47 - 49, Update the
ArrowDown handling in the CommandMenu highlight state update to clamp the
highlighted index at zero when filtered is empty, while preserving the existing
upper bound for non-empty results. Ensure hi remains non-negative so
later-arriving items can be highlighted and selected.

Comment on lines +230 to +291
export function PasskeyList({ creds = [], empty, onAdd }) {
const addBtn = (
<Button variant="outline" onClick={() => onAdd?.()}>
<span style={{ display: 'inline-flex', marginRight: -2 }}>{IconPlus(14)}</span>
Add a passkey
</Button>
)
return (
<div
style={{
border: '1px solid var(--ht-divider)',
borderRadius: 'var(--ht-radius-md)',
background: 'var(--ht-surface)',
overflow: 'hidden',
}}
>
{empty || creds.length === 0 ? (
<div style={{ padding: '40px 24px 34px', textAlign: 'center' }}>
<div
style={{
width: 44,
height: 44,
margin: '0 auto 14px',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
borderRadius: 'var(--ht-radius-md)',
background: 'var(--ht-surface-2)',
color: 'var(--ht-ink-dim)',
}}
>
{IconKey(24)}
</div>
<div
style={{
fontFamily: 'var(--ht-display)',
fontSize: 18,
fontWeight: 600,
color: 'var(--ht-ink)',
}}
>
No passkeys yet
</div>
<div
style={{
margin: '7px auto 18px',
maxWidth: 320,
fontSize: 13.5,
lineHeight: 1.6,
color: 'var(--ht-ink-muted)',
}}
>
Add a passkey to sign in with your fingerprint, face, or security key — no password to
remember.
</div>
{addBtn}
</div>
) : (
<>
{creds.map((c, i) => (
<CredentialRow key={c.name} cred={c} first={i === 0} />
))}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

PasskeyList never forwards onRename/onRevoke to CredentialRow.

PasskeyList only destructures creds, empty, onAdd. Each rendered CredentialRow therefore always has onRename/onRevoke as undefined, so the rename/revoke UI is fully interactive but has no observable effect — onRename?.(cred, name) and onRevoke?.(cred) are silent no-ops for anyone using the composed PasskeyList (rather than CredentialRow directly). This defeats the primary purpose of the list.

🐛 Proposed fix
-export function PasskeyList({ creds = [], empty, onAdd }) {
+export function PasskeyList({ creds = [], empty, onAdd, onRename, onRevoke }) {
   ...
   {creds.map((c, i) => (
-    <CredentialRow key={c.name} cred={c} first={i === 0} />
+    <CredentialRow key={c.name} cred={c} first={i === 0} onRename={onRename} onRevoke={onRevoke} />
   ))}

(Also add onRename?: (cred: Credential, name: string) => void and onRevoke?: (cred: Credential) => void to PasskeyListProps in CredentialRow.d.ts.)

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export function PasskeyList({ creds = [], empty, onAdd }) {
const addBtn = (
<Button variant="outline" onClick={() => onAdd?.()}>
<span style={{ display: 'inline-flex', marginRight: -2 }}>{IconPlus(14)}</span>
Add a passkey
</Button>
)
return (
<div
style={{
border: '1px solid var(--ht-divider)',
borderRadius: 'var(--ht-radius-md)',
background: 'var(--ht-surface)',
overflow: 'hidden',
}}
>
{empty || creds.length === 0 ? (
<div style={{ padding: '40px 24px 34px', textAlign: 'center' }}>
<div
style={{
width: 44,
height: 44,
margin: '0 auto 14px',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
borderRadius: 'var(--ht-radius-md)',
background: 'var(--ht-surface-2)',
color: 'var(--ht-ink-dim)',
}}
>
{IconKey(24)}
</div>
<div
style={{
fontFamily: 'var(--ht-display)',
fontSize: 18,
fontWeight: 600,
color: 'var(--ht-ink)',
}}
>
No passkeys yet
</div>
<div
style={{
margin: '7px auto 18px',
maxWidth: 320,
fontSize: 13.5,
lineHeight: 1.6,
color: 'var(--ht-ink-muted)',
}}
>
Add a passkey to sign in with your fingerprint, face, or security key no password to
remember.
</div>
{addBtn}
</div>
) : (
<>
{creds.map((c, i) => (
<CredentialRow key={c.name} cred={c} first={i === 0} />
))}
export function PasskeyList({ creds = [], empty, onAdd, onRename, onRevoke }) {
const addBtn = (
<Button variant="outline" onClick={() => onAdd?.()}>
<span style={{ display: 'inline-flex', marginRight: -2 }}>{IconPlus(14)}</span>
Add a passkey
</Button>
)
return (
<div
style={{
border: '1px solid var(--ht-divider)',
borderRadius: 'var(--ht-radius-md)',
background: 'var(--ht-surface)',
overflow: 'hidden',
}}
>
{empty || creds.length === 0 ? (
<div style={{ padding: '40px 24px 34px', textAlign: 'center' }}>
<div
style={{
width: 44,
height: 44,
margin: '0 auto 14px',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
borderRadius: 'var(--ht-radius-md)',
background: 'var(--ht-surface-2)',
color: 'var(--ht-ink-dim)',
}}
>
{IconKey(24)}
</div>
<div
style={{
fontFamily: 'var(--ht-display)',
fontSize: 18,
fontWeight: 600,
color: 'var(--ht-ink)',
}}
>
No passkeys yet
</div>
<div
style={{
margin: '7px auto 18px',
maxWidth: 320,
fontSize: 13.5,
lineHeight: 1.6,
color: 'var(--ht-ink-muted)',
}}
>
Add a passkey to sign in with your fingerprint, face, or security key no password to
remember.
</div>
{addBtn}
</div>
) : (
<>
{creds.map((c, i) => (
<CredentialRow key={c.name} cred={c} first={i === 0} onRename={onRename} onRevoke={onRevoke} />
))}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web/src/components/ds/core/CredentialRow.jsx` around lines 230 - 291, Update
PasskeyList to accept onRename and onRevoke and forward both callbacks to every
rendered CredentialRow, preserving the credential and new name arguments used by
CredentialRow. Add the corresponding optional callback declarations to
PasskeyListProps in CredentialRow.d.ts.

Comment on lines +5 to +8
function laterToday() {
const d = now()
d.setHours(17, 0, 0, 0)
return d

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== file listing ==\n'
git ls-files 'web/src/components/ds/core/SnoozePicker.jsx'

printf '\n== line count ==\n'
wc -l web/src/components/ds/core/SnoozePicker.jsx

printf '\n== relevant excerpt ==\n'
cat -n web/src/components/ds/core/SnoozePicker.jsx | sed -n '1,220p'

printf '\n== search for now() / laterToday / time handling in nearby files ==\n'
rg -n "laterToday|now\(\)|type=\"time\"|setHours\(|SnoozePicker" web/src/components/ds/core web/src/components/ds -g '!**/node_modules/**'

Repository: Helpthread/helpthread

Length of output: 12578


🏁 Script executed:

#!/bin/bash
set -euo pipefail

sed -n '1,140p' web/src/components/ds/core/SnoozePicker.jsx | nl -ba

Repository: Helpthread/helpthread

Length of output: 199


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== SnoozePicker time section ==\n'
sed -n '240,300p' web/src/components/ds/core/SnoozePicker.jsx | cat -n

printf '\n== primitives-support clock commentary ==\n'
sed -n '80,120p' web/src/components/ds/core/primitives-support.jsx | cat -n

printf '\n== search for empty-time handling ==\n'
rg -n "time.*''|setTime\(|onChange=.*time|type=\"time\"" web/src/components/ds/core/SnoozePicker.jsx

Repository: Helpthread/helpthread

Length of output: 4323


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== SnoozePicker around time input ==\n'
sed -n '248,292p' web/src/components/ds/core/SnoozePicker.jsx | cat -n

printf '\n== primitives-support around now() commentary ==\n'
sed -n '96,118p' web/src/components/ds/core/primitives-support.jsx | cat -n

Repository: Helpthread/helpthread

Length of output: 2941


Make Later today future-safe (web/src/components/ds/core/SnoozePicker.jsx:5-8). After 5 PM, laterToday() resolves to a past timestamp; hide the preset once it expires or roll it forward to the next valid slot.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web/src/components/ds/core/SnoozePicker.jsx` around lines 5 - 8, Update
laterToday() so it never returns a past timestamp: after 5 PM, either hide the
Later today preset or roll its timestamp forward to the next valid slot, while
preserving the existing 5 PM behavior before that cutoff.

Source: Coding guidelines

Comment on lines +169 to +174
const resolved = React.useMemo(() => {
const [h, mm] = time.split(':').map(Number)
const d = new Date(sel)
d.setHours(h, mm, 0, 0)
return d
}, [sel, time])

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Prevent an invalid Date when the time field is cleared.

A native time input can emit ''; destructuring then leaves mm undefined, so setHours() produces an invalid Date. The confirmation text becomes invalid and onSnooze can receive that invalid value. Validate the time before deriving resolved and disable or guard submission until it is valid.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web/src/components/ds/core/SnoozePicker.jsx` around lines 169 - 174, Update
the resolved useMemo in SnoozePicker to validate that time is non-empty and
contains valid hour/minute values before calling setHours; return a safe
non-submittable state when invalid. Ensure the confirmation display does not use
an invalid Date, and disable or guard the submission path so onSnooze cannot
receive an invalid resolved value.

CLAUDE.md requires web/src/components/ds/ to be verbatim copies of the
Claude Design project's components. They aren't: Biome reformats them on
arrival — the repo's javascript.formatter settings (single quotes,
semicolons asNeeded) rewrite the design source's double quotes and
semicolons, and organizeImports reorders their imports.

The code behaves identically; Button.jsx was confirmed semantically
identical to the design project's copy, with the entire diff being quotes,
semicolons and line wrapping. But that noise makes byte comparison useless
as a drift detector — a real design change would be indistinguishable from
formatter churn.

An override for ds/** already existed, disabling several lint rules that
don't suit design-source code. It left the formatter and the import assist
enabled, which is what does the rewriting. This turns both off for that
path.

Verified by construction: a file written in design-source style (double
quotes, semicolons) passes `biome check` with exit 0 inside ds/, and fails
with a format error outside it. Whole-repo `biome check` stays exit 0.

Note this prevents FUTURE mangling; it does not retroactively restore the
16 existing components, which are already reformatted. Making byte
comparison actually work requires re-pulling those from the design project
— tracked separately.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
zaridan added a commit that referenced this pull request Jul 20, 2026
The four new primitives (plus their shared helpers) were promoted into the
design project's components/core/ in this ticket. This re-pulls them back
down verbatim, so ds/ no longer holds five files that were a conversion
rather than a copy — every file under ds/ is now a byte-for-byte copy of its
design-project counterpart, which is what CLAUDE.md claims.

Round-trip proven byte-exact: the ten re-pulled files were `cmp`'d against
the exact bytes uploaded, all identical.

Two defects were caught by the normalized-diff pass and fixed upstream first,
then re-pulled:

- CommandMenu flattened the curly quotes in `Nothing matches “…”` to straight
  quotes. That is rendered output, so it is a fidelity break, not formatting.
- CredentialRow.d.ts narrowed `lastUsed?: Date | null` to `Date`, losing the
  null the component actually branches on.

Three .d.ts signatures deliberately differ from PR #103's: IconKey, IconReply
and IconClock take a required `sz`, because unlike IconSearch and IconPlus
they carry no default and render wrong without it.

The biome.json override gains noAutofocus, useExhaustiveDependencies and
noArrayIndexKey. PR #103 suppressed these with inline biome-ignore comments;
those are app-lint artifacts and do not belong in verbatim design source, so
the suppression moves to the override that already exists for exactly this
purpose. The design rationale comments stay in the components. No rule is
relaxed outside web/src/components/ds/**.

Gates (each on its own exit code): biome check . = 0, npm run typecheck = 0,
npm run build = 0.
@zaridan
zaridan merged commit b6a1845 into main Jul 20, 2026
5 checks passed
zaridan added a commit that referenced this pull request Jul 20, 2026
PR #103 stopped Biome from reformatting web/src/components/ds/ on arrival,
but it could not un-mangle the 16 components already there. This does that:
each file re-fetched from the Claude Design "Helpthread" project via
DesignSync and written byte-for-byte — double quotes, semicolons, original
import order and line wrapping restored.

Equivalence was proven before overwriting, not assumed. Both the pre-change
and post-change trees were formatted through one canonical Biome config and
diffed; the diff is empty across all 32 files. So no style value, prop, or
branch of logic changed — the whole historical drift really was formatting,
and nothing had been hand-edited in the app or moved in the design project.

Byte fidelity spot-checked against fresh get_file responses; all 32 files
end with a newline and none carry CRLF.

Gates (each on its own exit code): biome check . = 0, npm run typecheck = 0,
npm run build = 0.

Stacked on feat/ht-93-ds-new-primitives — the biome.json override there is a
precondition for the biome gate to pass on verbatim files.
zaridan added a commit that referenced this pull request Jul 20, 2026
The four new primitives (plus their shared helpers) were promoted into the
design project's components/core/ in this ticket. This re-pulls them back
down verbatim, so ds/ no longer holds five files that were a conversion
rather than a copy — every file under ds/ is now a byte-for-byte copy of its
design-project counterpart, which is what CLAUDE.md claims.

Round-trip proven byte-exact: the ten re-pulled files were `cmp`'d against
the exact bytes uploaded, all identical.

Two defects were caught by the normalized-diff pass and fixed upstream first,
then re-pulled:

- CommandMenu flattened the curly quotes in `Nothing matches “…”` to straight
  quotes. That is rendered output, so it is a fidelity break, not formatting.
- CredentialRow.d.ts narrowed `lastUsed?: Date | null` to `Date`, losing the
  null the component actually branches on.

Three .d.ts signatures deliberately differ from PR #103's: IconKey, IconReply
and IconClock take a required `sz`, because unlike IconSearch and IconPlus
they carry no default and render wrong without it.

The biome.json override gains noAutofocus, useExhaustiveDependencies and
noArrayIndexKey. PR #103 suppressed these with inline biome-ignore comments;
those are app-lint artifacts and do not belong in verbatim design source, so
the suppression moves to the override that already exists for exactly this
purpose. The design rationale comments stay in the components. No rule is
relaxed outside web/src/components/ds/**.

Gates (each on its own exit code): biome check . = 0, npm run typecheck = 0,
npm run build = 0.
zaridan added a commit that referenced this pull request Jul 20, 2026
* chore(web): re-pull the 16 design-system components verbatim (HT-94)

PR #103 stopped Biome from reformatting web/src/components/ds/ on arrival,
but it could not un-mangle the 16 components already there. This does that:
each file re-fetched from the Claude Design "Helpthread" project via
DesignSync and written byte-for-byte — double quotes, semicolons, original
import order and line wrapping restored.

Equivalence was proven before overwriting, not assumed. Both the pre-change
and post-change trees were formatted through one canonical Biome config and
diffed; the diff is empty across all 32 files. So no style value, prop, or
branch of logic changed — the whole historical drift really was formatting,
and nothing had been hand-edited in the app or moved in the design project.

Byte fidelity spot-checked against fresh get_file responses; all 32 files
end with a newline and none carry CRLF.

Gates (each on its own exit code): biome check . = 0, npm run typecheck = 0,
npm run build = 0.

Stacked on feat/ht-93-ds-new-primitives — the biome.json override there is a
precondition for the biome gate to pass on verbatim files.

* docs: UI fidelity is bidirectional, not design-first-only (HT-94)

The section said improvements go upstream in the design project first. That
has not been the working policy: HT-54's screens were built app-first and
approved in the app, and TJ's call (2026-07-20) is that approval in the app
is approval — the work flows back up.

Documents both directions, why ds/ is excluded from Biome (a formatter pass
breaks byte comparison as a drift detector), and that a semantic difference
found during a re-pull is a finding to escalate rather than something to
quietly resolve.

* chore(web): close the loop on the five promoted primitives (HT-94)

The four new primitives (plus their shared helpers) were promoted into the
design project's components/core/ in this ticket. This re-pulls them back
down verbatim, so ds/ no longer holds five files that were a conversion
rather than a copy — every file under ds/ is now a byte-for-byte copy of its
design-project counterpart, which is what CLAUDE.md claims.

Round-trip proven byte-exact: the ten re-pulled files were `cmp`'d against
the exact bytes uploaded, all identical.

Two defects were caught by the normalized-diff pass and fixed upstream first,
then re-pulled:

- CommandMenu flattened the curly quotes in `Nothing matches “…”` to straight
  quotes. That is rendered output, so it is a fidelity break, not formatting.
- CredentialRow.d.ts narrowed `lastUsed?: Date | null` to `Date`, losing the
  null the component actually branches on.

Three .d.ts signatures deliberately differ from PR #103's: IconKey, IconReply
and IconClock take a required `sz`, because unlike IconSearch and IconPlus
they carry no default and render wrong without it.

The biome.json override gains noAutofocus, useExhaustiveDependencies and
noArrayIndexKey. PR #103 suppressed these with inline biome-ignore comments;
those are app-lint artifacts and do not belong in verbatim design source, so
the suppression moves to the override that already exists for exactly this
purpose. The design rationale comments stay in the components. No rule is
relaxed outside web/src/components/ds/**.

Gates (each on its own exit code): biome check . = 0, npm run typecheck = 0,
npm run build = 0.

* docs: record the design project's three-folder component taxonomy (HT-94)

TJ approved components/app/ as the home for app-level surface upstream, so
the rule it implies gets written down rather than living in one PR thread.

core/ is primitives, inbox/ is inbox-specific composition, app/ is whole
screens plus the chrome framing them; the test for app/ is that the thing
owns a route or wraps all of them. Also records the one place the two sides
are deliberately NOT byte-identical: the app/ screens are .tsx here and are
converted to presentational .jsx on the way up, unlike ds/ which is a copy.

The same taxonomy note is now in the design project's own readme.md.
zaridan added a commit that referenced this pull request Aug 2, 2026
…dules match the desk (HT-95)

Modules are out-of-process and render their own UI, so nothing today makes a
module look like the desk it installs into. web/src/components/ds/ and
web/src/theme/tokens/ are the right raw material but are AGPL, and a
born-proprietary paid module importing them links AGPL code in-process — the
case the §7 Module API Exception covers, which is still DRAFT.

Proposes publishing the pack under a permissive license as its own package:
needs no §7 exception at all, and paid → free is the permitted direction under
catalog.md §1. Components are not the moat.

Also: theming resolves against the installed desk (white-labeling is a paid
item), the pack is generated from ds/ rather than forked (same discipline as
CLAUDE.md's UI-fidelity rule, one hop out), and conformance is a marketplace
listing requirement since no runtime check can ever enforce it.

Docs only. ds/ is owned by HT-93 (PR #103) and HT-94 — untouched here.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
zaridan added a commit that referenced this pull request Aug 8, 2026
…dules match the desk (HT-95)

Modules are out-of-process and render their own UI, so nothing today makes a
module look like the desk it installs into. web/src/components/ds/ and
web/src/theme/tokens/ are the right raw material but are AGPL, and a
born-proprietary paid module importing them links AGPL code in-process — the
case the §7 Module API Exception covers, which is still DRAFT.

Proposes publishing the pack under a permissive license as its own package:
needs no §7 exception at all, and paid → free is the permitted direction under
catalog.md §1. Components are not the moat.

Also: theming resolves against the installed desk (white-labeling is a paid
item), the pack is generated from ds/ rather than forked (same discipline as
CLAUDE.md's UI-fidelity rule, one hop out), and conformance is a marketplace
listing requirement since no runtime check can ever enforce it.

Docs only. ds/ is owned by HT-93 (PR #103) and HT-94 — untouched here.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
zaridan added a commit that referenced this pull request Aug 8, 2026
…dules match the desk (HT-95) (#104)

* docs(spec): module design pack — permissive components + tokens so modules match the desk (HT-95)

Modules are out-of-process and render their own UI, so nothing today makes a
module look like the desk it installs into. web/src/components/ds/ and
web/src/theme/tokens/ are the right raw material but are AGPL, and a
born-proprietary paid module importing them links AGPL code in-process — the
case the §7 Module API Exception covers, which is still DRAFT.

Proposes publishing the pack under a permissive license as its own package:
needs no §7 exception at all, and paid → free is the permitted direction under
catalog.md §1. Components are not the moat.

Also: theming resolves against the installed desk (white-labeling is a paid
item), the pack is generated from ds/ rather than forked (same discipline as
CLAUDE.md's UI-fidelity rule, one hop out), and conformance is a marketplace
listing requirement since no runtime check can ever enforce it.

Docs only. ds/ is owned by HT-93 (PR #103) and HT-94 — untouched here.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* docs(spec): settle the three open decisions — MIT, own repo, no token endpoint (HT-95)

Apache-2.0 was considered and rejected: no patentable invention in a component
library, so its patent grant covers a threat that does not exist, while NOTICE
preservation is real overhead. MIT is the React component-library norm and
readable without legal review. Adoption friction is the live constraint.

Pack ships as its own repo (helpthread-design-pack, @helpthread/design-pack) —
a permissive directory inside an AGPL tree gets misread by the audience that
needs to trust it.

Token transport: custom properties ship with the pack, desk values win. Embedded
modules inherit the desk scope for free; a public endpoint waits until an
operator has both re-skinned and installed a cross-origin module.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* docs(spec): pack sources from the design project as a sibling of ds/, not downstream (HT-95)

Chaining pack <- ds/ <- design project would propagate any ds/ drift into every
module — the same failure the spec exists to prevent, one layer down. As
siblings, the desk and its modules cannot drift from each other without both
drifting from a single source that byte-comparison catches.

Also unblocks: 16 of 20 components are already promoted upstream, so the pack no
longer waits on HT-93/HT-94. The four new primitives arrive when HT-94 part B
promotes them out of templates/new-primitives/.

Verified against the design project's file list rather than assumed — it already
carries components/core/, components/inbox/, tokens/, theme/, and fonts/.

Sync is documented as a process, not a pipe: DesignSync authenticates through a
claude.ai login and its writes need interactive plan approval, so it cannot run
unattended in CI. Options are the /design-sync skill or a scheduled agent that
opens a PR — cadence automation, not a live connection.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* docs(spec): address CodeRabbit — provenance gate, content-hash drift gate, MUST-level conformance (HT-95)

Three Major findings, all real.

Provenance (§2.1): relicensing requires owning the rights, and generation
establishes origin, not grantability. Fonts verified clear — fonts.css @imports
Source Serif 4 and Source Code Pro from Google Fonts, both OFL, no binaries
bundled, so nothing is redistributed. Components still need an audit against the
repo's provenance/AI policy. Noted the asymmetry that lowers the stakes: MIT
grants whatever rights exist, so uncopyrightability means 'cannot enforce', not
'infringing' — near-harmless for a pack meant to be used.

Drift gate (§5): a revision pin is not available — the design project is not a
git repo and DesignSync exposes no version identifier. Baseline is a
content-hash manifest instead, which separates 'upstream moved' from 'pack was
hand-edited'; byte-compare alone conflates them.

Conformance (§6): SHOULD-consume contradicted calling it a listing requirement.
Now MUST, but on the outcome (matches the desk) rather than the mechanism
(imports the pack) — otherwise a module with no operator-visible UI would be
non-conformant for having nothing to style.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* docs(spec): address CodeRabbit round 2 — 7 findings (HT-95)

Vocabulary (CLAUDE.md violation, mine): 'agent session' and 'scheduled agent'
described AI automation. Agents are human support staff; Assistants are AI
actors. Corrected, with the rule cited inline so the next editor does not
repeat it.

Inventory: §3 said 16 core, §5 said 12 core with 4 staged — both true at
different times, neither said when. Now stated as 12 core at v1, 16 after HT-94
part B, with totals given once (16 -> 20) and a rule for reading every other
count in the doc.

Cross-origin conformance: §6's MUST required matching a white-labeled desk while
§4 shipped no token transport, so a cross-origin module could not conform. Made
the exemption explicit and time-bound — a conformance rule nobody can satisfy is
worse than no rule.

Drift gate: hashing only sourced files cannot distinguish expected generation
from hand edits, since the pack also contains derived output. Now two closed
path sets, sources and generated, with appearing/vanishing files failing the
gate. Truth table extended to all four states.

Fonts: excluded from the pack pending HT-99. Licensing was settled (OFL, no
binaries vendored) but the @import means every module page would hit
fonts.googleapis.com with the visitor's IP — inconsistent with shipping
open-tracking-off as a free-core position, and a CSP/offline problem for
self-hosters. Already live in the desk, so filed against core.

Sync wording: 'no unattended sync' contradicted permitting scheduled sync.
Reworded to bound it at the PR — an Assistant may prepare and open, never merge
or release.

Markdown: fenced the source-tree diagram as text (MD040).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* docs(spec): MIT ratified by TJ; exclude brand assets from the pack (HT-95)

TJ ratified MIT explicitly ('yes MIT') after confirming the scope: the design
pack only. Core stays AGPL-3.0, LICENSE untouched, ds/ stays part of the AGPL
core. Recorded as his decision with his words, per the verdict protocol — the
earlier PR body framed it as settled on the strength of 'MIT, shit, i don't
care', which is not ratification of a one-way licensing door.

Noted the nuance that makes it low-risk: this is not relicensing core code.
Resonant IQ holds the copyright outright — ds/ has a single author — so the same
components are published under a second licence in a separate package while the
copies inside the core remain AGPL. Dual-licensing your own work costs the core
nothing.

Added the one boundary worth holding: brand assets are excluded permanently. The
wordmark, logo, and any Helpthread-identifying mark stay out of the pack and its
repo. MIT grants copyright and not trademark, so nobody could call their product
Helpthread either way — but shipping the wordmark inside an MIT package invites
the confusion legal/trademark-policy.md exists to prevent. The pack ships the
system, never the identity.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* docs(spec): address CodeRabbit round 3 — inventory, provenance gate, hash coverage (HT-95)

Three findings, all real.

Inventory: the spec counted 'CredentialRow/PasskeyList' as one item, leaving the
post-HT-94 total ambiguous. Verified against the file — CredentialRow.jsx exports
BOTH CredentialRow and PasskeyList, and there is no separate PasskeyList.jsx. So
it is one file, two components. Counts are now given as an explicit files-vs-
components table (20 files / 21 components after HT-94 part B) with a stated
default so every other figure in the doc reads unambiguously.

Provenance: the audit was described as 'due diligence, not a blocker to design
around', on the reasoning that MIT grants only whatever rights exist so the
downside is unenforceability rather than infringement. That reasoning is sound
about the downside and wrong about the sequencing — MIT publication is a one-way
door, and copies already taken cannot be recalled. It is now a blocking release
gate with four named items: every published path cleared (tokens and theme, not
just components), sole authorship verified and dated, AI-assisted generation
reconciled against legal/provenance-policy.md, and the clearance written down in
the pack repo.

Hash coverage: the manifest covered sourced and generated files but not
release-only paths — package.json, README, LICENSE, CI config. A hand-edit there
would pass both existing checks. Added a third 'static' set and stated the
invariant plainly: the manifest is exhaustive over the published package, not
merely over its inputs, and any published path missing from the manifest fails
the gate.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* docs(spec): fix 6 defects found by adversarial review (HT-95)

CodeRabbit was rate limited, so an independent adversarial pass ran in its place.
Six findings, five blocking. All were mine.

1. THE COPYRIGHT CLAIM CONTRADICTED THE CHARTER. The spec asserted 'Resonant IQ
   holds the copyright outright (ds/ has a single author)'. Checked: git log
   returns one name, TJ Baker — an individual, not the company — and CHARTER.md §3
   says contributions arrive under DCO with no assignment and 'Contributors keep
   the copyright on their work'. The project holds an inbound AGPL licence, not
   title. Single authorship records who committed, not who owns. Publishing under
   MIT still works — a sole author may license their own work — but a public claim
   about which entity holds title, contradicted by the repo's own constitution,
   must not ship. Naming the grantor is now item 5 of the §2.1 gate.

2. WRONG AUTHORITY CITED. catalog.md's 'paid → free stays possible' is a
   monetization axis — whether a paid module may later join the free core. It says
   nothing about copyleft→permissive. Citing it made relicensing look pre-approved
   by an accepted spec. Withdrawn; the argument stands without it.

3. THE BLOCKING GATE COULD NOT BLOCK. It governed a repo that does not exist yet,
   with no owner, no artifact, and no mechanism — prose wearing the word
   'blocking', the same shape as the instructions that failed in this repo's own
   audit. Now: a dated CLEARANCE.md in the pack repo, asserted by the publish
   workflow and failing npm publish if missing or stale, owned by TJ, with the pack
   repo not created until the grantor question is answered.

4. THE ANTI-DRIFT TABLE WAS ITSELF WRONG. It counted only .jsx and omitted 21 .d.ts
   files and primitives-support.jsx — a shared helper the four newest primitives
   import, which ships and would break the package if left out. Real counts,
   verified against the tree: 34 core + 8 inbox = 42 published files; 20 components
   across 21 .jsx. Files is now the stated default unit, which is also what the
   clearance gate and the manifest operate on.

5. .d.ts WERE CLASSIFIED AS GENERATED. They are fetched verbatim from the design
   project (HT-97 re-pulled 21), so they belong in 'sources'. As written, upstream
   .d.ts churn could never trip the 'upstream moved' row.

6. THE BRAND CARVE-OUT EXCLUDED ALMOST NOTHING. It pointed at theme/helpthread.css,
   which is a comment plus three @imports and holds no brand value. Meanwhile the
   token files that DO ship are brand-bearing: colors.css opens with a block
   commented '/* identity */' above --ht-accent, and typography.css sets --ht-serif
   to the wordmark face. Now states plainly that the palette and type scale ship and
   are meant to be overridden; only the marks are withheld.

Also dropped a restatement of the Agent/Assistant vocabulary rule — that definition
was revised 2026-07-31 and a copy here is one more place to drift.

Rebased on current main; HT-93 and HT-94 have merged, so the counts reflect the
tree as it stands rather than a pending promotion.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* docs(spec): correct one stale count missed in the previous pass (HT-95)

§2 still read '20 files / 21 components after HT-93' — a figure from before the
counts were verified against the tree. It is 42 published files (§3). Caught on
re-check after committing; the earlier replacement had matched an intermediate
version of the line rather than the final one.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* docs(spec): name Resonant IQ, Inc. as MIT grantor; close clearance-gate item 5 (HT-95)

Replaces the unsupported chain-of-title discussion with a plain licensing
statement, and tags the three remaining provenance decisions (repo split,
brand carve-out, marketplace conformance MUST) as maintainer-decided.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* docs(spec): item 5 states a future requirement, not a present fact (HT-95)

Adversarial review caught: the pack's LICENSE file can't "state" the
grantor yet — the pack repo doesn't exist. Made it a MUST.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant