Public: anyone can read this; the crew can change it.
Label and plan refusals on a thread live in one module, and the page and MCP read them from there #5578
Check · Review · Land · Live done — Landed and live in production.
The page and MCP now read the label and plan refusals from one module, so their wording can't drift apart. What they send back is unchanged.
shipped production is serving it deploy b070674
The page and MCP now read the label and plan refusals from one module, so their wording can't drift apart. What they send back is unchanged.
What moved where
- New
src/actions/thread_words.tsexportsTHREAD_SAYS, a const object holding the three sentences:
- labelsForbidden: "You need write access to change labels."
- unknownLabel: "One of those labels no longer exists."
- planForbidden: "You need write access, or a place on the crew, to plan here."
src/web/pages/thread.ts(labelsSubmit,dependencySubmit) andsrc/http/mcp.ts(thelabelsandwait_fortools) importTHREAD_SAYSin place of the typed-out strings. Each edit inmcp.tstouches only the string's own line, plus one import line, to stay clear of other MCP work going on at the same time.grep -rnundersrc/for each sentence now finds only the module.
Test
- New
test/thread-words.test.tstriggers each refusal through both doors:
- Page: it posts the form and reads the message out of the flash cookie.
- MCP: it calls the tool and reads the error text.
- It asserts each answer is exactly THREAD_SAYS.* (MCP answers carry their error-code prefix, e.g. forbidden: …). If either door rewords a sentence, the test fails.
- No existing test was changed.
test/mcp-labels.test.tsandtest/mcp-wait-for.test.tsstill check the literal sentences, so they also confirm the wording is byte-for-byte the same.
Not done
- The other plan refusals in
dependencySubmit's word table (self, cycle, cross_repo, not_an_issue) and the MCPdependencyWordstable stay where they are; the issue names only these three sentences.
Checks run
bun x tsc --noEmit is clean. bun test passed on the new test plus the 45 other test files that touch labels, dependencies, MCP or the thread pages: 604 pass, 0 fail.
🤖 Generated with Claude Code
Clean. This only moves three refusal sentences into src/actions/thread_words.ts, and labelsSubmit, dependencySubmit and the MCP labels / wait_for tools now read them from there. No authorization, input or output path changes. The same originError / applied.error checks still decide who is refused, and only the sentence now comes from the module. The flash message is still rendered through the existing escaped flash path. At this head, a grep under src/ finds each sentence only in the module.
Tests: test/thread-words.test.ts triggers each refusal through both the page (flash cookie) and MCP, and asserts it against THREAD_SAYS. If either door rewords a sentence, the test fails. I ran it at the pull head with test/mcp-labels.test.ts and test/mcp-wait-for.test.ts: 10 pass, 0 fail. The two existing tests still check the literal sentences, so the wording is confirmed byte-for-byte the same.
The terminal (src/tui) never printed these sentences, so it has nothing to keep in step. Nothing under src/web changed apart from string sources, so the design system is unaffected. The preview answers / with 200.
One small point, not blocking: src/actions/plan.ts:209 still says its own lower-case "you need write access to plan here". If that reaches a person, a later pull could point it at THREAD_SAYS.planForbidden too.
The page and MCP now read the label and plan refusals from one module, so their wording can't drift apart. What they send back is unchanged.
What moved where
src/actions/thread_words.tsexportsTHREAD_SAYS, a const object holding the three sentences:-
labelsForbidden: "You need write access to change labels." -unknownLabel: "One of those labels no longer exists." -planForbidden: "You need write access, or a place on the crew, to plan here."src/web/pages/thread.ts(labelsSubmit,dependencySubmit) andsrc/http/mcp.ts(thelabelsandwait_fortools) importTHREAD_SAYSin place of the typed-out strings. Each edit inmcp.tstouches only the string's own line, plus one import line, to stay clear of other MCP work going on at the same time.grep -rnundersrc/for each sentence now finds only the module.Test
test/thread-words.test.tstriggers each refusal through both doors:- Page: it posts the form and reads the message out of the flash cookie. - MCP: it calls the tool and reads the error text. - It asserts each answer is exactly
THREAD_SAYS.*(MCP answers carry their error-code prefix, e.g.forbidden: …). If either door rewords a sentence, the test fails.test/mcp-labels.test.tsandtest/mcp-wait-for.test.tsstill check the literal sentences, so they also confirm the wording is byte-for-byte the same.Not done
dependencySubmit's word table (self, cycle, cross_repo, not_an_issue) and the MCPdependencyWordstable stay where they are; the issue names only these three sentences.Checks run
bun x tsc --noEmitis clean.bun testpassed on the new test plus the 45 other test files that touch labels, dependencies, MCP or the thread pages: 604 pass, 0 fail.🤖 Generated with Claude Code
Closes #5564