From 6f7c050fd3f704e2698a45d8bf10ee15beb50512 Mon Sep 17 00:00:00 2001 From: TJ Baker <1617679+zaridan@users.noreply.github.com> Date: Mon, 20 Jul 2026 11:33:35 -0700 Subject: [PATCH 1/9] =?UTF-8?q?docs(spec):=20module=20design=20pack=20?= =?UTF-8?q?=E2=80=94=20permissive=20components=20+=20tokens=20so=20modules?= =?UTF-8?q?=20match=20the=20desk=20(HT-95)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- specs/modules/design-pack-v1.md | 134 ++++++++++++++++++++++++++++++++ 1 file changed, 134 insertions(+) create mode 100644 specs/modules/design-pack-v1.md diff --git a/specs/modules/design-pack-v1.md b/specs/modules/design-pack-v1.md new file mode 100644 index 0000000..470a55a --- /dev/null +++ b/specs/modules/design-pack-v1.md @@ -0,0 +1,134 @@ +# Module Design Pack v1 — components and tokens so modules match the desk + +Status: **draft for TJ review** (HT-95). Governed by CHARTER.md §3/§4 (module boundary, +out-of-process preference, zero privileged first-party access), `specs/modules/catalog.md` +(HT-66) §1's born-proprietary discipline, and CLAUDE.md's UI-fidelity rule. Depends on +HT-93 (PR #103) and HT-94 landing first — this spec describes a package *generated from* +`web/src/components/ds/`, so `ds/` must be reconciled before it can be a source. + +## 1. Purpose + +Modules are out-of-process and render their own UI (`specs/modules/substrate-v1.md` §1 +non-goals: "UI injection points"). That is the right boundary and this spec does not +change it. But it leaves a gap: **nothing makes a module look like the desk it is +installed into**, and nothing can, because there is no shared package a module author +can consume. + +The goal, stated as the operator experiences it: a module's screens should look like +Helpthread — and when an operator has white-labeled their desk, like *their* desk. + +**Non-goals for v1**: UI injection into core screens (still deferred; see §7), a +module manifest, runtime theme negotiation beyond token values, any change to `ds/` +itself. + +## 2. Why a separate package, and why permissive + +`web/src/components/ds/` (20 components after HT-93) and `web/src/theme/tokens/` +(`colors.css`, `shape.css`, `typography.css`) are the right raw material. They are also +**AGPL-3.0**, because they live in this tree. + +A paid module is born-proprietary (`catalog.md` §1). A proprietary module that imports +AGPL components links them **in-process** — which is exactly the combination the §7 +Module API Exception was drafted to permit, and that exception is +`legal/module-api-exception.md` **Status: DRAFT… Not yet adopted**. So today there is no +clean way for a paid module to use these components. + +Three ways out; this spec picks the third. + +| Option | Verdict | +| --- | --- | +| Wait for §7 adoption, import from the AGPL tree | Couples the design pack to a legal gate that exists for a different purpose (build-time core linkage). Also drags third-party authors into exception-compliance analysis to use a button. | +| Ship tokens only, no components | Preserves licensing purity, delivers little. Colors without components is not a design system. | +| **Publish the pack under a permissive license (MIT or Apache-2.0)** | **Needs no §7 exception at all.** Any module — first-party paid, third-party free, or a competitor's — can consume it with zero copyleft entanglement. | + +The strategic read: components are not the moat. The mail engine, the modules, and the +hosted engine are. AGPL on the design pack actively fights the goal in §1; a permissive +license serves it. And per `catalog.md` §1, **paid → free stays possible** — moving +these components to a permissive license is a move in the permitted direction. + +**Open decision 1 (TJ): MIT or Apache-2.0.** Apache-2.0 adds an express patent grant and +is the safer default for anything a competitor may adopt; MIT is shorter and more +familiar to the module-author audience. Recommendation: **Apache-2.0**. + +**Open decision 2 (TJ): this repo or its own.** Recommendation: **its own repo**, because +a permissively-licensed directory inside an AGPL tree is a recurring source of +misreading, and the pack has its own release cadence. + +## 3. Scope of the pack + +Generated from the reconciled `ds/`, in three layers: + +1. **Tokens** — the `theme/tokens/` custom properties, published as CSS and as a typed + export. This is the layer that makes §4 work. +2. **Core components** — the 16 primitives in `ds/core/` (Button, Avatar, DropdownMenu, + StatusPill, TagChip, Toast, TextInput, MenuItem, IconButton, EmptyState, Skeleton, + Kbd, plus HT-93's SplitButton, CommandMenu, SnoozePicker, CredentialRow/PasskeyList). +3. **Inbox components** — `ds/inbox/` (ConversationRow, MessageBand, ToolbarBand, + FolderItem). Included because a module rendering conversation-shaped data should + render it the same way the desk does. + +## 4. Theming resolves against the installed desk + +The pack must not hardcode Helpthread's palette. **White-labeling is a paid catalog +item** (`catalog.md` §3.3) — operators will re-skin, and a module that ships Helpthread +blue into a re-skinned desk looks broken, which is the failure this spec exists to +prevent. + +Tokens are already CSS custom properties, so the mechanism is simple: the module +consumes token *values* from the desk it is installed into rather than bundling its +own. The transport for those values is the one genuinely new engine-side surface this +spec implies, and per **zero privileged first-party access** it ships public — available +to every module author, not just first-party ones. + +**Open decision 3 (TJ):** whether that transport is a public read endpoint on the +engine, or whether modules that render in an embedded context inherit the desk's CSS +scope directly. The second is cheaper and covers the embedded case; the first also +covers a module rendering on its own origin. Recommendation: **defer until the first +module needs it** — consistent with substrate-v1's "each waits for a real module to +need it." + +## 5. Generated, never forked + +CLAUDE.md: `ds/` files are **verbatim copies** of the Claude Design project, and +improvements go upstream. HT-94 exists because Biome silently broke that byte-equality. + +The pack inherits the same discipline, one hop further out: **the pack is generated from +`ds/`, and `ds/` is generated from the design project.** A hand-maintained second copy +would drift the desk and its modules apart — the precise failure §1 is trying to +prevent, reintroduced one layer down. + +Practical consequence: the pack needs a generation step and a drift check in CI, not a +one-time copy. Byte-comparison is the check; HT-93's biome override is what makes byte +comparison meaningful again. + +## 6. Conformance is contractual, not technical + +An out-of-process module renders its own UI on its own origin. **Nothing in the engine +can force it to use the pack** — no runtime check exists, and per the marketplace's +distribution-credential-only rule, none ever will. + +So conformance is a **marketplace listing requirement**, checked at review: + +- a listed module SHOULD consume the pack for any surface an operator sees +- a listed module MUST NOT ship a look that impersonates core Helpthread UI while + behaving differently +- deviations need the same sign-off any UI deviation needs (CLAUDE.md: TJ's explicit + sign-off) + +Marketplace is first-party-only today — "Every Module row is first-party; no seller +onboarding" (`marketplace-v1.md`) — so this costs nothing to adopt now. **The rule needs +to exist before the first third-party module, not after**, because retrofitting a +conformance requirement onto published modules is a breaking change to someone else's +product. + +## 7. What this does not solve + +The pack makes a module's own screens look native. It does **not** put module UI inside +core screens — no QA panel in the conversation view, no module column in the inbox list. +That still requires a public UI extension point, still deferred by +`substrate-v1.md` §1, and still subject to zero-privileged-first-party-access if it is +ever built. + +That deferral is correct until a real module proves the need. The first candidate is the +QA module (HT-96): ship it notes-only, and let the dogfood answer whether notes are +sufficient or whether the extension point is worth building. From 324fda4aa0aceede2abd523f5c4738f2d5ed62de Mon Sep 17 00:00:00 2001 From: TJ Baker <1617679+zaridan@users.noreply.github.com> Date: Mon, 20 Jul 2026 11:40:03 -0700 Subject: [PATCH 2/9] =?UTF-8?q?docs(spec):=20settle=20the=20three=20open?= =?UTF-8?q?=20decisions=20=E2=80=94=20MIT,=20own=20repo,=20no=20token=20en?= =?UTF-8?q?dpoint=20(HT-95)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- specs/modules/design-pack-v1.md | 34 ++++++++++++++++++++------------- 1 file changed, 21 insertions(+), 13 deletions(-) diff --git a/specs/modules/design-pack-v1.md b/specs/modules/design-pack-v1.md index 470a55a..2fa378e 100644 --- a/specs/modules/design-pack-v1.md +++ b/specs/modules/design-pack-v1.md @@ -46,13 +46,17 @@ hosted engine are. AGPL on the design pack actively fights the goal in §1; a pe license serves it. And per `catalog.md` §1, **paid → free stays possible** — moving these components to a permissive license is a move in the permitted direction. -**Open decision 1 (TJ): MIT or Apache-2.0.** Apache-2.0 adds an express patent grant and -is the safer default for anything a competitor may adopt; MIT is shorter and more -familiar to the module-author audience. Recommendation: **Apache-2.0**. - -**Open decision 2 (TJ): this repo or its own.** Recommendation: **its own repo**, because -a permissively-licensed directory inside an AGPL tree is a recurring source of -misreading, and the pack has its own release cadence. +**Decision: MIT.** Apache-2.0 was considered for its express patent grant and rejected — +there is no patentable invention in a component library, so the grant covers a threat +that does not exist here, while its NOTICE-preservation requirements are real overhead. +MIT is the ecosystem norm for React component libraries (React, Radix, Tailwind, shadcn), +and a third-party module author can read it without legal review. Adoption friction is +the live constraint; patent exposure is not. + +**Decision: its own repo** — `helpthread-design-pack`, published as +`@helpthread/design-pack`. A permissively-licensed directory inside an AGPL tree is a +recurring source of misreading by exactly the audience that needs to trust it, and the +pack has its own release cadence. ## 3. Scope of the pack @@ -80,12 +84,16 @@ own. The transport for those values is the one genuinely new engine-side surface spec implies, and per **zero privileged first-party access** it ships public — available to every module author, not just first-party ones. -**Open decision 3 (TJ):** whether that transport is a public read endpoint on the -engine, or whether modules that render in an embedded context inherit the desk's CSS -scope directly. The second is cheaper and covers the embedded case; the first also -covers a module rendering on its own origin. Recommendation: **defer until the first -module needs it** — consistent with substrate-v1's "each waits for a real module to -need it." +**Decision: ship the custom properties with the pack and let the desk's values win. No +endpoint.** A module rendering in an embedded context inherits the desk's token scope for +free — no engine change, no new surface. A module on its own origin gets the pack's +Helpthread defaults, which is correct until an operator has both re-skinned their desk +*and* installed a cross-origin module. + +At that point the fix is a small public read endpoint serving token values, and it ships +public like everything else. Building it before that pair of conditions is met is +speculative, and substrate-v1's rule applies: each surface waits for a real module to +need it. ## 5. Generated, never forked From 7649171befc4ddb6c801bd1c5649fcca274ce01c Mon Sep 17 00:00:00 2001 From: TJ Baker <1617679+zaridan@users.noreply.github.com> Date: Mon, 20 Jul 2026 11:46:04 -0700 Subject: [PATCH 3/9] docs(spec): pack sources from the design project as a sibling of ds/, not downstream (HT-95) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- specs/modules/design-pack-v1.md | 63 ++++++++++++++++++++++++--------- 1 file changed, 47 insertions(+), 16 deletions(-) diff --git a/specs/modules/design-pack-v1.md b/specs/modules/design-pack-v1.md index 2fa378e..bedbae2 100644 --- a/specs/modules/design-pack-v1.md +++ b/specs/modules/design-pack-v1.md @@ -2,9 +2,12 @@ Status: **draft for TJ review** (HT-95). Governed by CHARTER.md §3/§4 (module boundary, out-of-process preference, zero privileged first-party access), `specs/modules/catalog.md` -(HT-66) §1's born-proprietary discipline, and CLAUDE.md's UI-fidelity rule. Depends on -HT-93 (PR #103) and HT-94 landing first — this spec describes a package *generated from* -`web/src/components/ds/`, so `ds/` must be reconciled before it can be a source. +(HT-66) §1's born-proprietary discipline, and CLAUDE.md's UI-fidelity rule. + +**Not blocked on HT-93/HT-94.** The pack sources from the Claude Design project directly, +as a sibling of `web/src/components/ds/` rather than downstream of it (§5). 16 of the 20 +components are already promoted there; the four new primitives arrive in the pack whenever +HT-94 part B promotes them upstream. ## 1. Purpose @@ -60,14 +63,15 @@ pack has its own release cadence. ## 3. Scope of the pack -Generated from the reconciled `ds/`, in three layers: +Sourced from the design project (§5), in three layers: -1. **Tokens** — the `theme/tokens/` custom properties, published as CSS and as a typed - export. This is the layer that makes §4 work. -2. **Core components** — the 16 primitives in `ds/core/` (Button, Avatar, DropdownMenu, +1. **Tokens** — `tokens/{colors,shape,typography}.css` plus `theme/helpthread.css` and + `fonts/fonts.css`, published as CSS and as a typed export. This is the layer that + makes §4 work. +2. **Core components** — the 16 primitives in `components/core/` (Button, Avatar, DropdownMenu, StatusPill, TagChip, Toast, TextInput, MenuItem, IconButton, EmptyState, Skeleton, Kbd, plus HT-93's SplitButton, CommandMenu, SnoozePicker, CredentialRow/PasskeyList). -3. **Inbox components** — `ds/inbox/` (ConversationRow, MessageBand, ToolbarBand, +3. **Inbox components** — `components/inbox/` (ConversationRow, MessageBand, ToolbarBand, FolderItem). Included because a module rendering conversation-shaped data should render it the same way the desk does. @@ -95,19 +99,46 @@ public like everything else. Building it before that pair of conditions is met i speculative, and substrate-v1's rule applies: each surface waits for a real module to need it. -## 5. Generated, never forked +## 5. Sourced from the design project, as a sibling of `ds/` CLAUDE.md: `ds/` files are **verbatim copies** of the Claude Design project, and improvements go upstream. HT-94 exists because Biome silently broke that byte-equality. -The pack inherits the same discipline, one hop further out: **the pack is generated from -`ds/`, and `ds/` is generated from the design project.** A hand-maintained second copy -would drift the desk and its modules apart — the precise failure §1 is trying to -prevent, reintroduced one layer down. +The pack inherits that discipline, but **not by chaining off `ds/`**. Both are +independent verbatim consumers of the same upstream: + +``` +Claude Design project ("Helpthread", 40b953cc) + ├── web/src/components/ds/ (the desk) + └── @helpthread/design-pack (modules) +``` + +A chain (`pack ← ds/ ← design project`) would propagate any `ds/` drift into every +module — reintroducing one layer down the exact failure §1 exists to prevent. As +siblings, the desk and its modules cannot drift *from each other* without both drifting +from a single source that byte-comparison catches. + +The upstream already carries everything the pack needs: `components/core/` (12), +`components/inbox/` (4), `tokens/{colors,shape,typography}.css`, `theme/helpthread.css`, +and `fonts/fonts.css`. The four new primitives are still staged at +`templates/new-primitives/Primitives.jsx` and enter the pack when HT-94 part B promotes +them to `components/core/`. + +### Sync is a process, not a pipe + +There is **no unattended sync**, and this spec does not assume one. DesignSync +authenticates through the operator's claude.ai login and its write path requires an +interactive plan approval, so it cannot hold a service credential or run in CI. + +What is real: + +- **The `/design-sync` skill** — an agent session that pulls changed components + incrementally, the same mechanism `ds/` already uses. Repeatable, human-initiated. +- **Optionally, a scheduled agent** that runs that sync on a cadence and opens a PR + against the pack repo. Automation of the *cadence*, not a live connection. -Practical consequence: the pack needs a generation step and a drift check in CI, not a -one-time copy. Byte-comparison is the check; HT-93's biome override is what makes byte -comparison meaningful again. +Either way the merge is a reviewed PR, which is what keeps §6's conformance claim +honest. ## 6. Conformance is contractual, not technical From 09246e419af4d8a458c62d336e2ddeb466212e87 Mon Sep 17 00:00:00 2001 From: TJ Baker <1617679+zaridan@users.noreply.github.com> Date: Mon, 20 Jul 2026 13:26:14 -0700 Subject: [PATCH 4/9] =?UTF-8?q?docs(spec):=20address=20CodeRabbit=20?= =?UTF-8?q?=E2=80=94=20provenance=20gate,=20content-hash=20drift=20gate,?= =?UTF-8?q?=20MUST-level=20conformance=20(HT-95)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- specs/modules/design-pack-v1.md | 58 +++++++++++++++++++++++++++++++-- 1 file changed, 55 insertions(+), 3 deletions(-) diff --git a/specs/modules/design-pack-v1.md b/specs/modules/design-pack-v1.md index bedbae2..15569bf 100644 --- a/specs/modules/design-pack-v1.md +++ b/specs/modules/design-pack-v1.md @@ -61,6 +61,27 @@ the live constraint; patent exposure is not. recurring source of misreading by exactly the audience that needs to trust it, and the pack has its own release cadence. +### 2.1 Provenance gate before first publish + +Relicensing requires owning the rights. Generation from the design project establishes +where the files came from, not what may be granted. Before the first publish: + +- **Fonts — cleared.** `fonts/fonts.css` `@import`s Source Serif 4 and Source Code Pro + from Google Fonts, both OFL, and **no font binaries are bundled**; the UI sans is the + native system stack. Nothing is redistributed, so no font license travels with the + pack. (Verified 2026-07-20; re-check if the pack ever vendors a binary.) +- **Components — audit required.** The `ds/` components originate in the Claude Design + project as AI-assisted work product. Confirm the rights chain against the repo's own + provenance/AI policy (`legal/`, drafted for counsel under HT-5) — specifically that the + human review CLAUDE.md requires was real enough to support authorship, and that nothing + was adapted from a source whose license forbids relicensing (CHARTER provenance rules). + +**The asymmetry that lowers the stakes:** MIT grants whatever rights exist. If some of +the pack turns out to be uncopyrightable, the license still functions — the exposure is +"cannot stop others from using it," not "infringing." For a pack whose entire purpose is +for others to use it, that failure mode is close to harmless. The audit is due diligence, +not a blocker to design around. + ## 3. Scope of the pack Sourced from the design project (§5), in three layers: @@ -140,6 +161,29 @@ What is real: Either way the merge is a reviewed PR, which is what keeps §6's conformance claim honest. +### The drift gate needs a content hash, not a revision pin + +"Regenerate and byte-compare" is only meaningful against a recorded baseline — otherwise +a stale pack and a moved upstream are indistinguishable, and both look like "the files +differ." + +The obvious answer, pinning an upstream revision, **is not available**: the design +project is not a git repo and DesignSync exposes no commit or version identifier +(`list_files`/`get_file` return paths and content, nothing more). So the baseline is a +**content-hash manifest** committed to the pack repo — per-file SHA-256 of every sourced +file, recorded at generation time. + +CI then re-fetches, re-hashes, and compares against the manifest, which distinguishes the +two cases the byte-compare alone conflates: + +| Manifest vs. fetched | Manifest vs. published pack | Means | +| --- | --- | --- | +| differs | matches | **upstream moved** — regenerate, review, release | +| matches | differs | **pack was hand-edited** — reject, this is the fork §5 forbids | + +HT-93's Biome override is what makes the hashes stable; without it, formatting-on-arrival +would churn them on every sync. + ## 6. Conformance is contractual, not technical An out-of-process module renders its own UI on its own origin. **Nothing in the engine @@ -148,11 +192,19 @@ distribution-credential-only rule, none ever will. So conformance is a **marketplace listing requirement**, checked at review: -- a listed module SHOULD consume the pack for any surface an operator sees -- a listed module MUST NOT ship a look that impersonates core Helpthread UI while +- a listed module **MUST** match the desk's design on every operator-visible surface. + The pack is the supported way to satisfy this and the only one that stays correct as + the design moves; an independent implementation is permitted but carries the whole + burden of proving parity, including after upstream changes. +- a listed module **MUST NOT** ship a look that impersonates core Helpthread UI while behaving differently - deviations need the same sign-off any UI deviation needs (CLAUDE.md: TJ's explicit - sign-off) + sign-off), recorded on the listing + +The requirement is on the *outcome* (matches the desk), not the *mechanism* (imports the +pack) — otherwise a module with no operator-visible UI at all, like a notifications +relay, would be non-conformant for having nothing to style. Such a module is trivially +conformant. Marketplace is first-party-only today — "Every Module row is first-party; no seller onboarding" (`marketplace-v1.md`) — so this costs nothing to adopt now. **The rule needs From 531198178b54c693b2c49486a5d85ecbda082082 Mon Sep 17 00:00:00 2001 From: TJ Baker <1617679+zaridan@users.noreply.github.com> Date: Mon, 20 Jul 2026 13:58:25 -0700 Subject: [PATCH 5/9] =?UTF-8?q?docs(spec):=20address=20CodeRabbit=20round?= =?UTF-8?q?=202=20=E2=80=94=207=20findings=20(HT-95)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- specs/modules/design-pack-v1.md | 97 +++++++++++++++++++++++++-------- 1 file changed, 73 insertions(+), 24 deletions(-) diff --git a/specs/modules/design-pack-v1.md b/specs/modules/design-pack-v1.md index 15569bf..f512c17 100644 --- a/specs/modules/design-pack-v1.md +++ b/specs/modules/design-pack-v1.md @@ -86,16 +86,36 @@ not a blocker to design around. Sourced from the design project (§5), in three layers: -1. **Tokens** — `tokens/{colors,shape,typography}.css` plus `theme/helpthread.css` and - `fonts/fonts.css`, published as CSS and as a typed export. This is the layer that - makes §4 work. -2. **Core components** — the 16 primitives in `components/core/` (Button, Avatar, DropdownMenu, +1. **Tokens** — `tokens/{colors,shape,typography}.css` plus `theme/helpthread.css`, + published as CSS and as a typed export. This is the layer that makes §4 work. + `fonts/fonts.css` is **excluded pending HT-99** — see §3.1. +2. **Core components** — `components/core/`. **12 at v1**: Button, Avatar, DropdownMenu, StatusPill, TagChip, Toast, TextInput, MenuItem, IconButton, EmptyState, Skeleton, - Kbd, plus HT-93's SplitButton, CommandMenu, SnoozePicker, CredentialRow/PasskeyList). + Kbd. **16 after HT-94 part B** promotes SplitButton, CommandMenu, SnoozePicker, and + CredentialRow/PasskeyList out of `templates/new-primitives/`. 3. **Inbox components** — `components/inbox/` (ConversationRow, MessageBand, ToolbarBand, FolderItem). Included because a module rendering conversation-shaped data should render it the same way the desk does. +**Totals, stated once to stop the drift:** v1 ships **16** (12 core + 4 inbox); after +HT-94 part B it is **20** (16 core + 4 inbox). Every count elsewhere in this spec refers +to the post-HT-94 figure unless it says "at v1". + +### 3.1 Fonts are excluded from v1 — HT-99 + +`fonts/fonts.css` is a single `@import` of Source Serif 4 and Source Code Pro from +`fonts.googleapis.com`. The fonts are OFL and no binaries are vendored, so §2.1's +*licensing* question is settled — but shipping that line in the pack would make every +module page issue a runtime request to Google, carrying the visitor's IP with it. + +That is a poor fit for a product that ships **open-tracking privacy default OFF** as a +free-core feature, and it raises CSP and offline-availability problems for self-hosters +besides. The same `@import` is already live in the desk (`web/src/theme/fonts/fonts.css`), +so this is not a pack-only question — filed as **HT-99** against core. The pack takes +whatever core decides (vendor the OFL files, or document the dependency and its fallback); +until then it ships tokens without the font layer, falling back to the native stack the +UI sans already uses. + ## 4. Theming resolves against the installed desk The pack must not hardcode Helpthread's palette. **White-labeling is a paid catalog @@ -120,6 +140,15 @@ public like everything else. Building it before that pair of conditions is met i speculative, and substrate-v1's rule applies: each surface waits for a real module to need it. +**Consequence: cross-origin white-label parity is explicitly OUT of §6's MUST for v1.** +Deciding to ship no token transport means a cross-origin module *cannot* match a +re-skinned desk — so requiring it would be requiring the impossible. The v1 requirement +therefore binds as: match the desk's design, resolving tokens from the desk wherever the +render context allows it (embedded), and from the pack's Helpthread defaults where it +does not (cross-origin). When the token endpoint ships, this exemption is removed and the +MUST applies everywhere. Named here rather than left implicit, because a conformance rule +nobody can satisfy is worse than no rule. + ## 5. Sourced from the design project, as a sibling of `ds/` CLAUDE.md: `ds/` files are **verbatim copies** of the Claude Design project, and @@ -128,7 +157,7 @@ improvements go upstream. HT-94 exists because Biome silently broke that byte-eq The pack inherits that discipline, but **not by chaining off `ds/`**. Both are independent verbatim consumers of the same upstream: -``` +```text Claude Design project ("Helpthread", 40b953cc) ├── web/src/components/ds/ (the desk) └── @helpthread/design-pack (modules) @@ -139,28 +168,32 @@ module — reintroducing one layer down the exact failure §1 exists to prevent. siblings, the desk and its modules cannot drift *from each other* without both drifting from a single source that byte-comparison catches. -The upstream already carries everything the pack needs: `components/core/` (12), -`components/inbox/` (4), `tokens/{colors,shape,typography}.css`, `theme/helpthread.css`, -and `fonts/fonts.css`. The four new primitives are still staged at -`templates/new-primitives/Primitives.jsx` and enter the pack when HT-94 part B promotes -them to `components/core/`. +The upstream already carries everything the pack needs at v1: `components/core/` (12), +`components/inbox/` (4), `tokens/{colors,shape,typography}.css`, and +`theme/helpthread.css`. (`fonts/fonts.css` is excluded — §3.1.) The four new primitives +are still staged at `templates/new-primitives/Primitives.jsx` and enter the pack when +HT-94 part B promotes them to `components/core/`, taking core to 16. -### Sync is a process, not a pipe +### Sync is never unattended past the PR boundary -There is **no unattended sync**, and this spec does not assume one. DesignSync -authenticates through the operator's claude.ai login and its write path requires an -interactive plan approval, so it cannot hold a service credential or run in CI. +DesignSync authenticates through the operator's claude.ai login and its write path +requires an interactive plan approval, so it cannot hold a service credential or run +unattended in CI. The *fetch* side can be scheduled; nothing past the PR can. What is real: -- **The `/design-sync` skill** — an agent session that pulls changed components +- **The `/design-sync` skill** — an **Assistant** session that pulls changed components incrementally, the same mechanism `ds/` already uses. Repeatable, human-initiated. -- **Optionally, a scheduled agent** that runs that sync on a cadence and opens a PR - against the pack repo. Automation of the *cadence*, not a live connection. +- **Optionally, a scheduled Assistant** that runs the fetch on a cadence and **opens a + PR** against the pack repo. It may prepare and open; it may never merge or release. + Automation of the *cadence*, not of the decision. Either way the merge is a reviewed PR, which is what keeps §6's conformance claim honest. +(Vocabulary, per CLAUDE.md: **Agents** are human support staff, **Assistants** are AI +actors. The automation here is an Assistant.) + ### The drift gate needs a content hash, not a revision pin "Regenerate and byte-compare" is only meaningful against a recorded baseline — otherwise @@ -170,16 +203,30 @@ differ." The obvious answer, pinning an upstream revision, **is not available**: the design project is not a git repo and DesignSync exposes no commit or version identifier (`list_files`/`get_file` return paths and content, nothing more). So the baseline is a -**content-hash manifest** committed to the pack repo — per-file SHA-256 of every sourced -file, recorded at generation time. +**content-hash manifest** committed to the pack repo. + +It must cover **two path sets, not one** — hashing only the sourced files cannot tell +expected generation from a hand edit, because the pack also contains derived output the +manifest never saw: + +- **`sources`** — per-file SHA-256 of every file fetched from the design project, plus + its exact path set. Detects upstream movement. +- **`generated`** — per-file SHA-256 of every artifact the build emits (typed token + exports, entry points, type declarations), plus its exact path set. Detects hand edits + *and* files that appear or vanish, which a hash-only check would miss. + +Both sets are exhaustive and closed: a path present in the package but absent from the +manifest fails the gate, same as a mismatched hash. -CI then re-fetches, re-hashes, and compares against the manifest, which distinguishes the -two cases the byte-compare alone conflates: +CI then re-fetches, rebuilds, re-hashes, and compares, which distinguishes the two cases +the byte-compare alone conflates: -| Manifest vs. fetched | Manifest vs. published pack | Means | +| `sources` vs. re-fetched | `generated` vs. rebuilt | Means | | --- | --- | --- | | differs | matches | **upstream moved** — regenerate, review, release | | matches | differs | **pack was hand-edited** — reject, this is the fork §5 forbids | +| differs | differs | upstream moved *and* someone edited — reject, resolve separately | +| matches | matches | clean | HT-93's Biome override is what makes the hashes stable; without it, formatting-on-arrival would churn them on every sync. @@ -192,7 +239,9 @@ distribution-credential-only rule, none ever will. So conformance is a **marketplace listing requirement**, checked at review: -- a listed module **MUST** match the desk's design on every operator-visible surface. +- a listed module **MUST** match the desk's design on every operator-visible surface, + **subject to §4's cross-origin exemption** — a module that cannot inherit the desk's + token scope conforms by matching the pack's defaults, until the token endpoint ships. The pack is the supported way to satisfy this and the only one that stays correct as the design moves; an independent implementation is permitted but carries the whole burden of proving parity, including after upstream changes. From f15ed5e53ab6343c4e7b76c852add6f1039c9c83 Mon Sep 17 00:00:00 2001 From: TJ Baker <1617679+zaridan@users.noreply.github.com> Date: Mon, 20 Jul 2026 16:29:23 -0700 Subject: [PATCH 6/9] docs(spec): MIT ratified by TJ; exclude brand assets from the pack (HT-95) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- specs/modules/design-pack-v1.md | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/specs/modules/design-pack-v1.md b/specs/modules/design-pack-v1.md index f512c17..960e1dc 100644 --- a/specs/modules/design-pack-v1.md +++ b/specs/modules/design-pack-v1.md @@ -49,7 +49,16 @@ hosted engine are. AGPL on the design pack actively fights the goal in §1; a pe license serves it. And per `catalog.md` §1, **paid → free stays possible** — moving these components to a permissive license is a move in the permitted direction. -**Decision: MIT.** Apache-2.0 was considered for its express patent grant and rejected — +**Decision: MIT — ratified by TJ, 2026-07-20** ("yes MIT"), after confirming the scope is +the design pack alone: the core stays AGPL-3.0, `LICENSE` is untouched, and +`web/src/components/ds/` remains part of the AGPL core. This is not a relicensing of core +code — Resonant IQ holds the copyright outright (`ds/` has a single author), so the same +components are simply published under a second licence in a separate package while the +copies inside the core stay AGPL. On the cost, TJ: *"i really don't care if someone does +anything with design components."* The brand-asset carve-out in §3 is the one boundary +that stands. + +Apache-2.0 was considered for its express patent grant and rejected — there is no patentable invention in a component library, so the grant covers a threat that does not exist here, while its NOTICE-preservation requirements are real overhead. MIT is the ecosystem norm for React component libraries (React, Radix, Tailwind, shadcn), @@ -84,6 +93,16 @@ not a blocker to design around. ## 3. Scope of the pack +**Brand assets are excluded, permanently.** The wordmark, logo, and any +Helpthread-identifying mark stay out of the pack and out of its repo — including +`guidelines/type-wordmark.html` and any brand-specific value in `theme/helpthread.css`. + +MIT grants copyright, **not trademark**, so a permissive licence would not let anyone +call their product Helpthread regardless. But shipping the wordmark inside an MIT package +invites exactly the confusion `legal/trademark-policy.md` exists to prevent, and a +licence file is a poor place to litigate it. The pack ships the *system* — components and +tokens — never the *identity*. + Sourced from the design project (§5), in three layers: 1. **Tokens** — `tokens/{colors,shape,typography}.css` plus `theme/helpthread.css`, From a0a643725b4715d5a8772d16cc16a707e12da215 Mon Sep 17 00:00:00 2001 From: TJ Baker <1617679+zaridan@users.noreply.github.com> Date: Sun, 2 Aug 2026 10:04:14 -0700 Subject: [PATCH 7/9] =?UTF-8?q?docs(spec):=20address=20CodeRabbit=20round?= =?UTF-8?q?=203=20=E2=80=94=20inventory,=20provenance=20gate,=20hash=20cov?= =?UTF-8?q?erage=20(HT-95)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- specs/modules/design-pack-v1.md | 76 +++++++++++++++++++++++++-------- 1 file changed, 59 insertions(+), 17 deletions(-) diff --git a/specs/modules/design-pack-v1.md b/specs/modules/design-pack-v1.md index 960e1dc..1f539e5 100644 --- a/specs/modules/design-pack-v1.md +++ b/specs/modules/design-pack-v1.md @@ -6,8 +6,8 @@ out-of-process preference, zero privileged first-party access), `specs/modules/c **Not blocked on HT-93/HT-94.** The pack sources from the Claude Design project directly, as a sibling of `web/src/components/ds/` rather than downstream of it (§5). 16 of the 20 -components are already promoted there; the four new primitives arrive in the pack whenever -HT-94 part B promotes them upstream. +files are already promoted there; the four remaining files arrive in the pack whenever +HT-94 part B promotes them upstream (see §3 for the file-vs-component counts). ## 1. Purpose @@ -26,7 +26,7 @@ itself. ## 2. Why a separate package, and why permissive -`web/src/components/ds/` (20 components after HT-93) and `web/src/theme/tokens/` +`web/src/components/ds/` (20 files / 21 components after HT-93, §3) and `web/src/theme/tokens/` (`colors.css`, `shape.css`, `typography.css`) are the right raw material. They are also **AGPL-3.0**, because they live in this tree. @@ -85,11 +85,30 @@ where the files came from, not what may be granted. Before the first publish: human review CLAUDE.md requires was real enough to support authorship, and that nothing was adapted from a source whose license forbids relicensing (CHARTER provenance rules). -**The asymmetry that lowers the stakes:** MIT grants whatever rights exist. If some of -the pack turns out to be uncopyrightable, the license still functions — the exposure is -"cannot stop others from using it," not "infringing." For a pack whose entire purpose is -for others to use it, that failure mode is close to harmless. The audit is due diligence, -not a blocker to design around. +**This audit is a blocking release gate, not due diligence.** An earlier draft called it +"not a blocker to design around" on the reasoning that MIT grants only whatever rights +exist, so a wrongly-published component leaves you unable to enforce rather than +infringing. That reasoning holds for the *downside*, and it is still true — but it is the +wrong test for a **one-way door**. Once published under MIT, copies already taken cannot +be recalled, so the check has to pass *before* the release, not eventually. + +Concretely, before the first MIT publish: + +1. **Every published path** is cleared, not just components — tokens, `theme/`, typed + exports, and any file the package ships. A rights gap in a token file is the same + problem as one in a button. +2. **Sole authorship is verified and dated**, not assumed. `git log` on `ds/` showing a + single author is the current evidence; it stops being sufficient the moment a DCO + contribution lands, because charter §3's inbound-equals-outbound rule makes those lines + AGPL-3.0 and unrelicensable by anyone but their author. +3. **AI-assisted generation is reconciled** against `legal/provenance-policy.md` — single + authorship in `git log` records who committed, which is not by itself a determination + of copyright subsistence in machine-generated output. +4. **The clearance is written down** in the pack repo and dated, so a later contributor + can see what was checked and when rather than re-deriving it. + +If any item is unresolved, the release does not ship. Publishing first and auditing after +is the one sequence this gate exists to prevent. ## 3. Scope of the pack @@ -108,17 +127,29 @@ Sourced from the design project (§5), in three layers: 1. **Tokens** — `tokens/{colors,shape,typography}.css` plus `theme/helpthread.css`, published as CSS and as a typed export. This is the layer that makes §4 work. `fonts/fonts.css` is **excluded pending HT-99** — see §3.1. -2. **Core components** — `components/core/`. **12 at v1**: Button, Avatar, DropdownMenu, - StatusPill, TagChip, Toast, TextInput, MenuItem, IconButton, EmptyState, Skeleton, - Kbd. **16 after HT-94 part B** promotes SplitButton, CommandMenu, SnoozePicker, and - CredentialRow/PasskeyList out of `templates/new-primitives/`. +2. **Core components** — `components/core/`. **12 at v1**, one per file: Button, Avatar, + DropdownMenu, StatusPill, TagChip, Toast, TextInput, MenuItem, IconButton, EmptyState, + Skeleton, Kbd. HT-94 part B promotes **four more files** out of + `templates/new-primitives/`: SplitButton, CommandMenu, SnoozePicker, and + `CredentialRow.jsx` — which exports **two** components, `CredentialRow` and + `PasskeyList` (verified 2026-07-20; there is no separate `PasskeyList.jsx`). 3. **Inbox components** — `components/inbox/` (ConversationRow, MessageBand, ToolbarBand, FolderItem). Included because a module rendering conversation-shaped data should render it the same way the desk does. -**Totals, stated once to stop the drift:** v1 ships **16** (12 core + 4 inbox); after -HT-94 part B it is **20** (16 core + 4 inbox). Every count elsewhere in this spec refers -to the post-HT-94 figure unless it says "at v1". +**Totals, stated once to stop the drift.** Count *files* and *components* separately — +conflating them is what made earlier drafts of this section disagree with themselves: + +| | Files | Components | +| --- | --- | --- | +| Core at v1 | 12 | 12 | +| Inbox | 4 | 4 | +| **v1 total** | **16** | **16** | +| + HT-94 part B | 4 | 5 (`CredentialRow.jsx` exports two) | +| **After HT-94 part B** | **20** | **21** | + +Every count elsewhere in this spec means **files** unless it says otherwise, and refers to +the post-HT-94 figure unless it says "at v1". ### 3.1 Fonts are excluded from v1 — HT-99 @@ -234,8 +265,19 @@ manifest never saw: exports, entry points, type declarations), plus its exact path set. Detects hand edits *and* files that appear or vanish, which a hash-only check would miss. -Both sets are exhaustive and closed: a path present in the package but absent from the -manifest fails the gate, same as a mismatched hash. +There is a third category the first two miss — **files that are neither fetched nor +generated**: `package.json`, `README.md`, `LICENSE`, CI config, anything hand-authored in +the pack repo. A hand-edit there changes what customers receive while passing both checks +above. + +- **`static`** — per-file SHA-256 of every published path that is neither sourced nor + generated. Changes here are legitimate and routine; the manifest just makes them + *visible in the diff* rather than silent. + +**The manifest is exhaustive over the published package, not merely over its inputs.** +Every path the package ships appears in exactly one of the three sets. A path present in +the package but absent from the manifest fails the gate, same as a mismatched hash — that +rule is what makes "exhaustive" mean something rather than being an aspiration. CI then re-fetches, rebuilds, re-hashes, and compares, which distinguishes the two cases the byte-compare alone conflates: From 022d9dfa90e61a377d8208041c7da659c3fdc06a Mon Sep 17 00:00:00 2001 From: TJ Baker <1617679+zaridan@users.noreply.github.com> Date: Sun, 2 Aug 2026 11:59:54 -0700 Subject: [PATCH 8/9] docs(spec): fix 6 defects found by adversarial review (HT-95) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- specs/modules/design-pack-v1.md | 139 ++++++++++++++++++++++---------- 1 file changed, 97 insertions(+), 42 deletions(-) diff --git a/specs/modules/design-pack-v1.md b/specs/modules/design-pack-v1.md index 1f539e5..f29c53e 100644 --- a/specs/modules/design-pack-v1.md +++ b/specs/modules/design-pack-v1.md @@ -4,10 +4,10 @@ Status: **draft for TJ review** (HT-95). Governed by CHARTER.md §3/§4 (module out-of-process preference, zero privileged first-party access), `specs/modules/catalog.md` (HT-66) §1's born-proprietary discipline, and CLAUDE.md's UI-fidelity rule. -**Not blocked on HT-93/HT-94.** The pack sources from the Claude Design project directly, -as a sibling of `web/src/components/ds/` rather than downstream of it (§5). 16 of the 20 -files are already promoted there; the four remaining files arrive in the pack whenever -HT-94 part B promotes them upstream (see §3 for the file-vs-component counts). +**HT-93 and HT-94 have merged.** The pack sources from the Claude Design project +directly, as a sibling of `web/src/components/ds/` rather than downstream of it (§5). +Everything it needs is promoted upstream today; see §3 for exact counts, which are stated +in **published files**, not components. ## 1. Purpose @@ -46,17 +46,37 @@ Three ways out; this spec picks the third. The strategic read: components are not the moat. The mail engine, the modules, and the hosted engine are. AGPL on the design pack actively fights the goal in §1; a permissive -license serves it. And per `catalog.md` §1, **paid → free stays possible** — moving -these components to a permissive license is a move in the permitted direction. +license serves it. (An earlier draft cited `catalog.md` §1's "paid → free stays possible" as authority +here. That is a **monetization** axis — whether a paid module may later join the free +core — and says nothing about copyleft→permissive relicensing. The citation was a +category error and is withdrawn; the argument above stands without it.) **Decision: MIT — ratified by TJ, 2026-07-20** ("yes MIT"), after confirming the scope is the design pack alone: the core stays AGPL-3.0, `LICENSE` is untouched, and -`web/src/components/ds/` remains part of the AGPL core. This is not a relicensing of core -code — Resonant IQ holds the copyright outright (`ds/` has a single author), so the same -components are simply published under a second licence in a separate package while the -copies inside the core stay AGPL. On the cost, TJ: *"i really don't care if someone does -anything with design components."* The brand-asset carve-out in §3 is the one boundary -that stands. +`web/src/components/ds/` remains part of the AGPL core. On the cost, TJ: *"i really don't +care if someone does anything with design components."* The brand-asset carve-out in §3 +is the one boundary that stands. + +**Who may grant MIT is an open question, not a settled one.** An earlier draft asserted +that "Resonant IQ holds the copyright outright (`ds/` has a single author)." That does not +survive checking, and the error mattered because everything downstream rested on it: + +- `git log --format='%an' -- web/src/components/ds/` returns exactly one name — **TJ + Baker, an individual.** Not Resonant IQ, Inc. Single authorship in `git log` records who + *committed*; it is not a determination of who *owns*. +- **CHARTER.md §3 says the opposite of what that draft assumed:** contributions come in + under **DCO**, no CLA, no copyright assignment, and "Contributors keep the copyright on + their work." The project holds an inbound AGPL-3.0 licence, not title. That is the + structural feature making the core's licence permanent — and it cuts against a casual + claim of corporate ownership. +- If a written assignment to Resonant IQ exists (e.g. an IP-assignment instrument + executed as a founder), **that instrument is the operative authority and must be cited + by name here.** Absent it, the grantor is TJ Baker personally. + +Publishing under MIT still works either way — a sole author may licence their own work +however they like. What must not ship is a public claim about *which entity* holds title +that the repository's own constitution contradicts. Resolving this is item 5 of the §2.1 +gate. Apache-2.0 was considered for its express patent grant and rejected — there is no patentable invention in a component library, so the grant covers a threat @@ -107,14 +127,41 @@ Concretely, before the first MIT publish: 4. **The clearance is written down** in the pack repo and dated, so a later contributor can see what was checked and when rather than re-deriving it. +5. **The grantor is named.** Either the assignment instrument transferring `ds/` to + Resonant IQ is cited, or the licence is granted by TJ Baker personally. The public + `LICENSE` file must name whoever actually holds title (see the copyright discussion + in §2). + +**How this gate is enforced, since prose gates drift.** An earlier draft called this +blocking while providing no mechanism — the same shape as the instructions that failed in +the audit this repo has already run. Concretely: + +- The clearance lives at **`CLEARANCE.md` in the pack repo**, dated, listing each item + above and what was checked. +- The pack's **publish workflow asserts that file exists and is newer than the last + `sources` manifest change**, and fails otherwise. A missing or stale clearance blocks + `npm publish` mechanically, not by convention. +- **Owner: TJ**, as the person who must sign the licensing call. No one else can clear it. +- **The pack repo is not created until item 5 is answered** — that keeps the question in + front of a human rather than behind a checkbox nobody reads. + If any item is unresolved, the release does not ship. Publishing first and auditing after is the one sequence this gate exists to prevent. ## 3. Scope of the pack -**Brand assets are excluded, permanently.** The wordmark, logo, and any -Helpthread-identifying mark stay out of the pack and out of its repo — including -`guidelines/type-wordmark.html` and any brand-specific value in `theme/helpthread.css`. +**Brand assets are excluded, permanently** — the wordmark and logo artwork, and anything +that functions as a Helpthread mark: `guidelines/type-wordmark.html` and any logo file. + +**What is NOT excluded, stated plainly because an earlier draft implied otherwise.** That +draft claimed the pack "ships the *system* — never the *identity*", and pointed at +`theme/helpthread.css` as carrying brand values. Checked: that file is a comment plus +three `@import`s and contains no brand value at all, so the carve-out as written excluded +nothing. Meanwhile the token files the pack *does* ship are brand-bearing — +`colors.css` opens with a block literally commented `/* identity */` above +`--ht-accent: #4a55a2`, and `typography.css` sets `--ht-serif: "Source Serif 4"`, the +wordmark face. **The palette and type scale ship, and are meant to be overridden** (§4); +only the marks themselves are withheld. MIT grants copyright, **not trademark**, so a permissive licence would not let anyone call their product Helpthread regardless. But shipping the wordmark inside an MIT package @@ -127,29 +174,31 @@ Sourced from the design project (§5), in three layers: 1. **Tokens** — `tokens/{colors,shape,typography}.css` plus `theme/helpthread.css`, published as CSS and as a typed export. This is the layer that makes §4 work. `fonts/fonts.css` is **excluded pending HT-99** — see §3.1. -2. **Core components** — `components/core/`. **12 at v1**, one per file: Button, Avatar, - DropdownMenu, StatusPill, TagChip, Toast, TextInput, MenuItem, IconButton, EmptyState, - Skeleton, Kbd. HT-94 part B promotes **four more files** out of - `templates/new-primitives/`: SplitButton, CommandMenu, SnoozePicker, and - `CredentialRow.jsx` — which exports **two** components, `CredentialRow` and - `PasskeyList` (verified 2026-07-20; there is no separate `PasskeyList.jsx`). -3. **Inbox components** — `components/inbox/` (ConversationRow, MessageBand, ToolbarBand, - FolderItem). Included because a module rendering conversation-shaped data should - render it the same way the desk does. - -**Totals, stated once to stop the drift.** Count *files* and *components* separately — -conflating them is what made earlier drafts of this section disagree with themselves: - -| | Files | Components | -| --- | --- | --- | -| Core at v1 | 12 | 12 | -| Inbox | 4 | 4 | -| **v1 total** | **16** | **16** | -| + HT-94 part B | 4 | 5 (`CredentialRow.jsx` exports two) | -| **After HT-94 part B** | **20** | **21** | - -Every count elsewhere in this spec means **files** unless it says otherwise, and refers to -the post-HT-94 figure unless it says "at v1". +2. **Core** — `components/core/`, **34 files**: 17 `.jsx` and their 17 `.d.ts` + siblings. Button, Avatar, DropdownMenu, StatusPill, TagChip, Toast, TextInput, + MenuItem, IconButton, EmptyState, Skeleton, Kbd, SplitButton, CommandMenu, + SnoozePicker, CredentialRow — plus `primitives-support.jsx`, a **shared helper the + four newest primitives import** (`chevron`, `RING`, `useFocusRing`, `fmtDate`, icons). + It is not a component and ships anyway; omitting it would break the package. +3. **Inbox** — `components/inbox/`, **8 files**: ConversationRow, MessageBand, + ToolbarBand, FolderItem and their `.d.ts` siblings. Included because a module + rendering conversation-shaped data should render it the same way the desk does. + +**Totals, verified against the tree on 2026-08-02.** Counted in **published files** — +every path the package ships, which is the unit §2.1's clearance gate and §5's manifest +both operate on. Earlier drafts counted only `.jsx` and silently omitted 21 `.d.ts` files +plus the shared helper: + +| | `.jsx` | `.d.ts` | Files | +| --- | --- | --- | --- | +| `components/core/` | 17 | 17 | **34** | +| `components/inbox/` | 4 | 4 | **8** | +| **Total** | 21 | 21 | **42** | + +Components are a different count and deliberately not the unit used elsewhere: 20 +components across 21 `.jsx` files, because `CredentialRow.jsx` exports both +`CredentialRow` and `PasskeyList`, and `primitives-support.jsx` exports no component at +all. Where this spec says a number without qualification, it means **files**. ### 3.1 Fonts are excluded from v1 — HT-99 @@ -241,8 +290,10 @@ What is real: Either way the merge is a reviewed PR, which is what keeps §6's conformance claim honest. -(Vocabulary, per CLAUDE.md: **Agents** are human support staff, **Assistants** are AI -actors. The automation here is an Assistant.) +(The sync automation is an AI actor, not a person. This spec deliberately does not +restate the Agent/Assistant vocabulary rule — that definition was revised on 2026-07-31 +and a copy here would be one more place to drift out of date. `CLAUDE.md` is the single +source.) ### The drift gate needs a content hash, not a revision pin @@ -262,8 +313,12 @@ manifest never saw: - **`sources`** — per-file SHA-256 of every file fetched from the design project, plus its exact path set. Detects upstream movement. - **`generated`** — per-file SHA-256 of every artifact the build emits (typed token - exports, entry points, type declarations), plus its exact path set. Detects hand edits - *and* files that appear or vanish, which a hash-only check would miss. + exports, entry points), plus its exact path set. Detects hand edits *and* files that + appear or vanish, which a hash-only check would miss. + **The `.d.ts` files are NOT in this set.** They are fetched verbatim from the design + project alongside their `.jsx` siblings (HT-97 re-pulled 21 of them), so they belong in + `sources`. Classifying them as generated would mean upstream `.d.ts` churn could never + trip the "upstream moved" row — it would read as clean forever. There is a third category the first two miss — **files that are neither fetched nor generated**: `package.json`, `README.md`, `LICENSE`, CI config, anything hand-authored in From f8a3101635f03fdedf0eef76cea47504eeb9e504 Mon Sep 17 00:00:00 2001 From: TJ Baker <1617679+zaridan@users.noreply.github.com> Date: Sun, 2 Aug 2026 12:01:14 -0700 Subject: [PATCH 9/9] docs(spec): correct one stale count missed in the previous pass (HT-95) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit §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 --- specs/modules/design-pack-v1.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/specs/modules/design-pack-v1.md b/specs/modules/design-pack-v1.md index f29c53e..ef980cd 100644 --- a/specs/modules/design-pack-v1.md +++ b/specs/modules/design-pack-v1.md @@ -26,7 +26,7 @@ itself. ## 2. Why a separate package, and why permissive -`web/src/components/ds/` (20 files / 21 components after HT-93, §3) and `web/src/theme/tokens/` +`web/src/components/ds/` (42 published files, §3) and `web/src/theme/tokens/` (`colors.css`, `shape.css`, `typography.css`) are the right raw material. They are also **AGPL-3.0**, because they live in this tree.