Four commits, three tasks and one issue. Two threads: helping a caller pick the right record surface, and making the badge layer legible and singly-owned.
Scribe's record kinds get reached for interchangeably, and the moment of choice is the only moment a correction is cheap. create_rule and start_planning already carried real disambiguators; create_note — far and away the highest-volume surface — carried none. The guidance sat in the rarest tool and was missing from the most common one.
Each surface now opens with one deciding question in its own terms: what else could hold this / is anything actually owed / is the shape the point, or the advice / followed, or read.
The guard asserts structure, never wording — each surface must name at least two siblings — plus a second half asserting the Args: block survives, since the first check is satisfiable by turning a docstring into an essay about the other tools. It caught two gaps on its first run, one of them its own: "design system" is hard-wrapped across a line break, so matching the raw docstring reported it absent.
task_kind was visible only inside the editor's Kind select, so every list surface rendered work, issue and spike identically. One KindBadge across five surfaces; work renders nothing, since badging the default says nothing.
Two things the work turned up. The recorded canon for a status chip (#2960 StatusBadge) had no live consumer. And the dashboard's _task_row omitted task_kind entirely — the badge would have rendered nothing there while working everywhere else, reading as "this list has no issues" rather than as a missing field.
Contrast. Every status and priority pair painted its raw hue as TEXT on a 12% tint of that same hue. Measured on the dark palette, all six failed the kit's own AA floor — todo 1.60:1, in-progress 1.97:1, done 2.06:1, low 2.02:1, high 2.92:1, medium 2.97:1, against 4.5.
The cause is structural: a 12% tint sits near the surface, so the hue as text on it has nowhere to go. A stronger tint was measured and rejected — on a dark palette a heavier tint moves the chip toward the light text and makes it worse.
Seven -fg tokens, recorded in the design system record and regenerated, not hand-written into theme.css (the file says not to, and a hand-edit is silently reverted next regeneration). Each is the hue mixed toward --fs-text-primary until it clears 4.5:1 worst-case over raised and hover in both modes — and because that token inverts, one declaration covers both. The ladder keeps its shape: high holds 52% saturation, medium 31%, while low/todo/done wash toward neutral, which is what their own rationales ask for.
One owner per shape. The issue claimed "three scoped re-spellings"; reading them showed only one was a duplicate. The others were a clickable cycler and a different value domain — sharing a class name and nothing else, which is precisely what would make a future consolidation merge three unrelated things. A fourth site the issue never found (ProjectListView vs ProjectView) was the genuine duplicate.
TaskCard deleted — dead since 2026-04-08, when TasksListView was removed for the Knowledge view. Its removal is what lets StatusBadge become real by adoption rather than remain a museum piece the ledger points at.
Verification
CI green on ce1376e, all six jobs. Typecheck covers eight touched frontend files; the token and dangling-style checks were compared against prior runs, not just read as green.
The contrast guard had a hole and was widened. It only knew background: var(--fs-X-bg); the project pills used an inline color-mix and sat at 1.61–2.39:1 in two views. Widened it finds 48 app-wide, 26 of them --fs-accent — filed as #3141. The check now GATES the token form, which is clean, and REPORTS the inline form with a count: a gate nobody can satisfy on the day it lands gets switched off, and then it guards nothing.
Both guards were verified by reintroducing the defect rather than by passing on already-fixed code.
After merging
Nothing needs deploying for correctness. The docstrings reach sessions only once deployed, since they are what the MCP client reads.
Four commits, three tasks and one issue. Two threads: helping a caller pick the right record surface, and making the badge layer legible and singly-owned.
## Tool disambiguators (#3123)
Scribe's record kinds get reached for interchangeably, and the moment of choice is the only moment a correction is cheap. `create_rule` and `start_planning` already carried real disambiguators; `create_note` — far and away the highest-volume surface — carried **none**. The guidance sat in the rarest tool and was missing from the most common one.
Each surface now opens with one deciding question in its own terms: *what else could hold this* / *is anything actually owed* / *is the shape the point, or the advice* / *followed, or read*.
The guard asserts structure, never wording — each surface must name at least two siblings — plus a second half asserting the `Args:` block survives, since the first check is satisfiable by turning a docstring into an essay about the other tools. It caught two gaps on its first run, one of them **its own**: `"design system"` is hard-wrapped across a line break, so matching the raw docstring reported it absent.
## Kind badges (#3124)
`task_kind` was visible only inside the editor's Kind select, so every list surface rendered work, issue and spike identically. One `KindBadge` across five surfaces; `work` renders nothing, since badging the default says nothing.
Two things the work turned up. The recorded canon for a status chip (**#2960 StatusBadge**) had **no live consumer**. And the dashboard's `_task_row` omitted `task_kind` entirely — the badge would have rendered nothing there while working everywhere else, reading as "this list has no issues" rather than as a missing field.
## The badge layer (#3132)
**Contrast.** Every status and priority pair painted its raw hue as TEXT on a 12% tint of that same hue. Measured on the dark palette, **all six failed** the kit's own AA floor — todo 1.60:1, in-progress 1.97:1, done 2.06:1, low 2.02:1, high 2.92:1, medium 2.97:1, against 4.5.
The cause is structural: a 12% tint sits near the surface, so the hue as text on it has nowhere to go. A stronger tint was measured and **rejected** — on a dark palette a heavier tint moves the chip toward the light text and makes it worse.
Seven `-fg` tokens, recorded in the **design system record and regenerated**, not hand-written into theme.css (the file says not to, and a hand-edit is silently reverted next regeneration). Each is the hue mixed toward `--fs-text-primary` until it clears 4.5:1 worst-case over raised and hover in both modes — and because that token inverts, one declaration covers both. The ladder keeps its shape: high holds 52% saturation, medium 31%, while low/todo/done wash toward neutral, which is what their own rationales ask for.
**One owner per shape.** The issue claimed "three scoped re-spellings"; reading them showed only one was a duplicate. The others were a clickable cycler and a different value domain — sharing a class name and nothing else, which is precisely what would make a future consolidation merge three unrelated things. A **fourth** site the issue never found (ProjectListView vs ProjectView) was the genuine duplicate.
TaskCard deleted — dead since 2026-04-08, when `TasksListView` was removed for the Knowledge view. Its removal is what lets StatusBadge become real by adoption rather than remain a museum piece the ledger points at.
## Verification
CI green on `ce1376e`, all six jobs. Typecheck covers eight touched frontend files; the token and dangling-style checks were compared against prior runs, not just read as green.
**The contrast guard had a hole and was widened.** It only knew `background: var(--fs-X-bg)`; the project pills used an inline `color-mix` and sat at 1.61–2.39:1 in two views. Widened it finds **48 app-wide, 26 of them `--fs-accent`** — filed as **#3141**. The check now GATES the token form, which is clean, and REPORTS the inline form with a count: a gate nobody can satisfy on the day it lands gets switched off, and then it guards nothing.
Both guards were verified by reintroducing the defect rather than by passing on already-fixed code.
## After merging
Nothing needs deploying for correctness. The docstrings reach sessions only once deployed, since they are what the MCP client reads.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Scribe's record kinds get reached for interchangeably, and the moment of
choice is the only moment a correction is cheap. Rule 119 puts product
guidance in the instruction surfaces, so the docstring is where this
belongs — but a docstring that only documents parameters answers "how do I
call this" and leaves "should I be calling this at all" unasked.
The gap was lopsided. create_rule and start_planning already carried real
disambiguators; create_note — far and away the highest-volume surface —
carried none at all. The guidance sat in the rarest tool and was missing
from the most common one.
Each surface now opens with ONE deciding question in its own terms rather
than a pasted block:
create_note WHAT ELSE COULD HOLD THIS? note is right when nothing
is owed and nothing enforces
create_task IS ANYTHING ACTUALLY OWED? nothing owed -> note;
an arc -> start_planning
create_snippet SHAPE, OR ADVICE? a snippet is code with a
LOCATION
create_process FOLLOWED, OR READ? applies uninvoked -> rule
create_project_rule now points at the entity check too; it had only ever
covered rule-vs-rule scope.
The guard asserts STRUCTURE, never wording: each surface must name at least
two siblings. Pinning phrasing would make every improvement a test failure,
and a test that punishes editing is a test that gets deleted. Its second
half asserts the Args: block survives — the first check is satisfiable by
turning a docstring into an essay about the other tools, which would be a
worse contract than the one being fixed.
The guard caught two gaps on its first run, one of them its own: "design
system" is hard-wrapped across a line break in create_rule, so matching the
raw docstring reported it absent. _doc() now flattens whitespace. It also
caught start_planning naming only one alternative, which was true and is
now fixed.
Deliberately NOT built: an intent-router tool. It has a bootstrapping
problem — it is itself a tool that must be reached for — and MCP clients
already list every tool's description. Recorded in #3123; build it only if
wrong-surface reaches survive this.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
task_kind was only visible inside the task editor's Kind select, so every
list surface rendered work, issue and spike identically and a list of tasks
hid the fact that three different things were in it.
ONE component, not a fifth spelling. The badge layer had already drifted —
StatusBadge.vue is the recorded canon (#2960) but WorkspaceTaskPanel,
ProjectView and KnowledgeView each carry their own scoped `.status-badge`.
KindBadge is modelled on PriorityBadge, its closest sibling, which already
does the thing that matters here: the DEFAULT value renders nothing. `work`
is most tasks, so badging it would put a chip on nearly every row and say
nothing — the same reason RuleListPane marks only `conditional`.
COLOUR BY TEMPERATURE, measured rather than eyeballed. Issue and spike are
opposite in character — corrective vs exploratory — so they split warm
(warning) against cool (info), which survives being small and stays
distinguishable without reading the word. Neither uses the accent; kind is
not one of the places it is allowed.
The raw semantic colour FAILS the contrast floor on the dark palette:
warning on its own 12% tint measures 2.97:1 against AA's 4.5. So the text is
the hue mixed toward --fs-text-primary, which passes and, because that token
inverts by mode, follows light/dark for free. Measured both ways — issue
5.23:1 dark / 6.68:1 light, spike 5.33:1 / 9.26:1.
`plan` renders hue-free and italic: retired since 0066, so a legacy row
should read as archival rather than as a fourth kind competing for
attention. In KnowledgeView it is passed as null instead, because the type
badge beside it already says "Plan" and two chips reading the same word
would look like two facts.
Weight is 500, not the 600 the two older badges use — the house style allows
400 and 500 only, and copying 600 would spread it.
SERVER FIX, without which this was decorative: dashboard's `_task_row`
omitted task_kind entirely. The badge would have rendered nothing there
while working everywhere else, which reads as "this list has no issues"
rather than as a missing field. The guard is on the payload, where the
omission was.
Surfaces: ProjectView's three status columns, WorkspaceTaskPanel's two task
lists, DashboardView's milestone and no-milestone rows, KnowledgeView's
result rows.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Every status and priority badge used its raw hue as TEXT on a 12% tint of
that same hue. Measured on the dark palette, all six pairs failed the kit's
own AA floor: todo 1.60:1, in-progress 1.97:1, done 2.06:1, low 2.02:1,
high 2.92:1, medium 2.97:1, against 4.5. Four also failed in light mode.
The cause is structural, not a bad colour pick. A 12% tint sits near the
surface it composites over, so the hue as text on it has almost nowhere to
go. Strengthening the tint was measured and REJECTED: on a dark palette a
heavier tint moves the chip toward the light text and makes it worse. 12%
was already optimal.
So each pair gains a `-fg` sibling: the hue mixed toward --fs-text-primary
until it clears 4.5:1 worst-case over surface-raised AND surface-hover in
BOTH modes. Mixing toward that token rather than a literal is what makes one
declaration cover both — it inverts by mode, so the text follows.
Recorded in the DESIGN SYSTEM, not hand-written into theme.css: seven tokens
on design system 2, each carrying its measurement and its reasoning, then the
sheet regenerated. theme.css says not to hand-edit the --fs-* block and it is
right — a hand-edit would be silently reverted by the next regeneration.
The ladder keeps its shape. High priority still holds 52% saturation and
medium 31% — the rungs that need to shout still shout. Low, todo and done
wash toward neutral, which is what their own rationales ask for: status-todo
is derived from the border colour precisely so not-yet-started recedes.
Receding and illegible are different things and the old value was the second.
--fs-status-cancelled-fg was found by measuring, not by reasoning. Cancelled
derives from --fs-text-tertiary, which looks like the obviously-correct
"quiet" choice and is a HINT colour tuned for plain surfaces — 2.63:1 on a
badge tint in light mode.
StatusBadge additionally dropped a `color-mix(..., #000 15%)` that darkened
the hue: a light-mode instinct that made these worse on a near-black surface,
and a literal besides.
THE GUARD IS THE POINT. check_design_tokens.py now FAILS on any rule that
paints text with a token on a tint of that same token, and names the -fg
sibling as the fix. Verified by reintroducing the defect: exit 1 with it,
exit 0 without. Unlike a raw literal there is nothing to weigh up, so it
gates rather than reports.
Two `border-top-color` uses keep the raw hue, correctly — a border is a
non-text graphic and needs 3:1, which is what the hue is for.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
ITEM 1 — the dead canon. StatusBadge is recorded canon (#2960) and its only
consumer, TaskCard, has been unreachable since 2026-04-08, when
TasksListView was deleted in favour of the Knowledge view. Four and a half
months of a canon that rendered nowhere, which is worse than no canon: a
session pulls #2960, builds from it, and matches a component nobody has
seen. TaskCard is deleted (rule 22), and the canon is made real by adoption
rather than by being left as a museum piece.
ITEM 2 — MY OWN ISSUE OVERSTATED THIS, and the correction is the finding.
"Three scoped re-spellings" assumed one shape spelled thrice. Reading them:
KnowledgeView a task-status chip, just smaller -> a real duplicate
WorkspaceTaskPanel a CLICKABLE cycler: pointer,
outlined, transparent background -> a control, not a chip
ProjectView PROJECT lifecycle (active/paused/
completed/archived) -> a different vocabulary
Only the first was ever a duplicate. The others shared a class NAME and
nothing else — which is exactly what would make a future consolidation merge
three unrelated things. So: KnowledgeView adopts StatusBadge/PriorityBadge
via the `compact` variant the canon already anticipated ("interactive/compact
re-spellings are variants of it"); the cycler becomes `.status-cycler`; and
project status becomes its own vocabulary.
And there was a FOURTH, in ProjectListView — the genuine duplicate of
ProjectView's project pill, differing by the amounts two hands differ by:
0.68rem vs 0.7rem, a 14% tint vs 15%, one bordered and one not. Both now use
one ProjectStatusBadge. `statusLabel` went with its only caller.
ITEM 3 — weight. StatusBadge and PriorityBadge used font-weight 600; the
house style allows 400 and 500 only. Also "In Progress" -> "In progress",
which was invisible under `text-transform: uppercase` and becomes visible the
moment the compact variant turns that off.
THE GUARD MISSED FOUR LIVE SITES, which is the part worth keeping. The
project pills painted a hue on an inline `color-mix` tint of itself —
measured 1.61-2.39:1 — and the checker only knew the `--fs-X-bg` token form.
Widened, it finds 48 across the app, 26 of them --fs-accent.
That backlog is not this task, so the check now splits: it GATES the token
form, which is clean, and REPORTS the inline form with a count and its worst
offenders. A gate nobody can satisfy today gets switched off, and then it
guards nothing. Gate re-verified by reintroducing a defect — exit 1 with it,
exit 0 without.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Four commits, three tasks and one issue. Two threads: helping a caller pick the right record surface, and making the badge layer legible and singly-owned.
Tool disambiguators (#3123)
Scribe's record kinds get reached for interchangeably, and the moment of choice is the only moment a correction is cheap.
create_ruleandstart_planningalready carried real disambiguators;create_note— far and away the highest-volume surface — carried none. The guidance sat in the rarest tool and was missing from the most common one.Each surface now opens with one deciding question in its own terms: what else could hold this / is anything actually owed / is the shape the point, or the advice / followed, or read.
The guard asserts structure, never wording — each surface must name at least two siblings — plus a second half asserting the
Args:block survives, since the first check is satisfiable by turning a docstring into an essay about the other tools. It caught two gaps on its first run, one of them its own:"design system"is hard-wrapped across a line break, so matching the raw docstring reported it absent.Kind badges (#3124)
task_kindwas visible only inside the editor's Kind select, so every list surface rendered work, issue and spike identically. OneKindBadgeacross five surfaces;workrenders nothing, since badging the default says nothing.Two things the work turned up. The recorded canon for a status chip (#2960 StatusBadge) had no live consumer. And the dashboard's
_task_rowomittedtask_kindentirely — the badge would have rendered nothing there while working everywhere else, reading as "this list has no issues" rather than as a missing field.The badge layer (#3132)
Contrast. Every status and priority pair painted its raw hue as TEXT on a 12% tint of that same hue. Measured on the dark palette, all six failed the kit's own AA floor — todo 1.60:1, in-progress 1.97:1, done 2.06:1, low 2.02:1, high 2.92:1, medium 2.97:1, against 4.5.
The cause is structural: a 12% tint sits near the surface, so the hue as text on it has nowhere to go. A stronger tint was measured and rejected — on a dark palette a heavier tint moves the chip toward the light text and makes it worse.
Seven
-fgtokens, recorded in the design system record and regenerated, not hand-written into theme.css (the file says not to, and a hand-edit is silently reverted next regeneration). Each is the hue mixed toward--fs-text-primaryuntil it clears 4.5:1 worst-case over raised and hover in both modes — and because that token inverts, one declaration covers both. The ladder keeps its shape: high holds 52% saturation, medium 31%, while low/todo/done wash toward neutral, which is what their own rationales ask for.One owner per shape. The issue claimed "three scoped re-spellings"; reading them showed only one was a duplicate. The others were a clickable cycler and a different value domain — sharing a class name and nothing else, which is precisely what would make a future consolidation merge three unrelated things. A fourth site the issue never found (ProjectListView vs ProjectView) was the genuine duplicate.
TaskCard deleted — dead since 2026-04-08, when
TasksListViewwas removed for the Knowledge view. Its removal is what lets StatusBadge become real by adoption rather than remain a museum piece the ledger points at.Verification
CI green on
ce1376e, all six jobs. Typecheck covers eight touched frontend files; the token and dangling-style checks were compared against prior runs, not just read as green.The contrast guard had a hole and was widened. It only knew
background: var(--fs-X-bg); the project pills used an inlinecolor-mixand sat at 1.61–2.39:1 in two views. Widened it finds 48 app-wide, 26 of them--fs-accent— filed as #3141. The check now GATES the token form, which is clean, and REPORTS the inline form with a count: a gate nobody can satisfy on the day it lands gets switched off, and then it guards nothing.Both guards were verified by reintroducing the defect rather than by passing on already-fixed code.
After merging
Nothing needs deploying for correctness. The docstrings reach sessions only once deployed, since they are what the MCP client reads.
🤖 Generated with Claude Code
Scribe's record kinds get reached for interchangeably, and the moment of choice is the only moment a correction is cheap. Rule 119 puts product guidance in the instruction surfaces, so the docstring is where this belongs — but a docstring that only documents parameters answers "how do I call this" and leaves "should I be calling this at all" unasked. The gap was lopsided. create_rule and start_planning already carried real disambiguators; create_note — far and away the highest-volume surface — carried none at all. The guidance sat in the rarest tool and was missing from the most common one. Each surface now opens with ONE deciding question in its own terms rather than a pasted block: create_note WHAT ELSE COULD HOLD THIS? note is right when nothing is owed and nothing enforces create_task IS ANYTHING ACTUALLY OWED? nothing owed -> note; an arc -> start_planning create_snippet SHAPE, OR ADVICE? a snippet is code with a LOCATION create_process FOLLOWED, OR READ? applies uninvoked -> rule create_project_rule now points at the entity check too; it had only ever covered rule-vs-rule scope. The guard asserts STRUCTURE, never wording: each surface must name at least two siblings. Pinning phrasing would make every improvement a test failure, and a test that punishes editing is a test that gets deleted. Its second half asserts the Args: block survives — the first check is satisfiable by turning a docstring into an essay about the other tools, which would be a worse contract than the one being fixed. The guard caught two gaps on its first run, one of them its own: "design system" is hard-wrapped across a line break in create_rule, so matching the raw docstring reported it absent. _doc() now flattens whitespace. It also caught start_planning naming only one alternative, which was true and is now fixed. Deliberately NOT built: an intent-router tool. It has a bootstrapping problem — it is itself a tool that must be reached for — and MCP clients already list every tool's description. Recorded in #3123; build it only if wrong-surface reaches survive this. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>