Field feature · illuminate rung
When the intent itself moves: who sees it, who signs it.
The cheapest way to satisfy a stubborn test is to soften the sentence it answers for. No check that reads only the code can catch that: the durable ID holds, the named test still passes, nothing looks broken. It is also exactly the shortcut an agent under pressure to go green will find. So the report watches the words themselves. A restatement shows up as a diff on a known row, and Intent Changed asks the one question that matters: did the code and tests move with it? Then two small gates decide who signs, one stock GitHub and one SpecAssay's: Product decides whether we want the new promise, the build side confirms the code still keeps it. Most restatements are healthy; requirements tighten, the field pushes back. The report treats them all the same way: visibly, and it never blocks. The signatures are the gates, and they are human. An agent can reword a spec; it cannot merge one.
The healthy case, and the everyday one. Mid-build, the team measured reconcile on real devices: changes land in well under two seconds, every run. The 5s budget in AC-SYNC-01 was out of date. So PR #5 updates the wording and the test together, in one diff:
| examples/example-app/PRD.md | AC-SYNC-01 restated: within 5s → within 2s | intent moved |
| tests/test_sync.py | the proof moves with it: assert elapsed < 2.0 | on the thread |
5s → 2s changed, but not found verbatim in the code or tests — re-confirm by reading.Nothing stale to point at, and the report says so usefully. The old 5s is gone from the statement and gone from the test, because both moved in the same PR. The (updated here) mark confirms the proof came along. A reviewer reads the wording change and the code change as one story. That's what the durable ID buys: AC-SYNC-01 itself never changes, so its wording and its proof can move together without breaking the thread.
The case to watch. PR #4 makes the same one-line change, 5s → 2s, and nothing else. Maybe Product tightened the requirement. Maybe an agent "cleaned up" the spec and changed its meaning without anyone deciding that. Either way the Gate stays green: the ID didn't change, and the named test still passes. But that test was written for 5s.
Here the report earns its keep. Same rewording, but the code and tests sat still, and the report can prove it: test_sync.py line 37 still asserts elapsed < 5.0. A passing test, a green thread, and it's testing the old requirement. The report points at the exact line and stops. It won't rewrite your test; it shows the reviewer where to look. Put the two reports side by side and the difference is one glance.
Product and Dev share the repo, but the wording can still belong to Product. GitHub has a built-in feature for exactly this: a file called CODEOWNERS. Each line in it names a file and its owner; one line here reads PRD.md @product-team. From then on, any PR that changes the PRD automatically gets Product added as a reviewer, and with one repo setting, it can't merge until Product approves. The example app ships this file →
So when Dev's field discovery rewrites a requirement, the PR itself is the request to Product: new wording, updated test, report attached. Product reviews it on the diff. No meeting. And an agent that rewords a spec can't merge anything; a person has to sign first.
The reverse direction needs a second tool. When Product rewrites a requirement, none of Dev's files change, so CODEOWNERS never asks Dev anything. But the tests now need a check. That's the intent_ack checkbox: someone from the build side ticks that the code and tests still match the new wording, before merge. Two gates, one each:
| Product's gate | Dev's gate | |
| Instrument | CODEOWNERS review | intent_ack tick |
| Fires when | the wording is touched | an intent is restated |
| Question | "Do we want this promise?" | "Does the code still keep it?" |
Can't tick it honestly because the test really is out of date? Two normal fixes: add the test update to this same PR (the report then shows (updated here)), or file the follow-up task and the row shows tracked-debt until it's done. Nothing merges hidden.
How hard the report leans depends on what it can prove. Three tiers, sharpest first:
5sThe old value (a number, a quoted string, a constant) still sits in the code or a test, and the report points at the exact line. PR #4's case, and the sharpest hint it can give: here is the line that still says what you just changed.
5s → 2s changed, but not found verbatim in the code or tests — re-confirm by reading.The value changed, but the old one isn't in the code anymore; on a discovery PR that's because it was just updated (PR #5's case). Nothing to point at, so the report says so and lists what to re-read.
The wording changed in meaning, not in any searchable value: "ordered by relevance" becomes "ordered by recency." The report doesn't pretend otherwise: it lists the code and the test and asks a human to read them against the new sentence. The most common case.
Rewording an intent is normal work: requirements tighten, reality pushes back, product learns. Whether the existing code still satisfies a new sentence is a human judgement, so the report never blocks on it:
The Intent Changed section above: what was reworded, and which code and tests to check. No verdict.
Set intent_ack: required and the report adds a checkbox a human must tick before merge. It matters most when the wording moved alone; on a discovery PR the answer is already in the diff.
The Gate blocks only what it can prove is a defect: a silent gap, an invented ID, registry drift. A rewording is none of those, so it never earns a refusal.
A durable ID means the wording can change without the thread snapping. Intent Changed keeps that honest, in both directions, and never silently.
And it stays a comment. SpecAssay posts beside your PR; it never edits the wording, the code, or the test. The diff is the record; the Thread Report is the reading glass.
Intent Changed is one section of the Thread Report. The reference doc carries the technical depth: how restatements are detected, the token heuristic behind the tiers, and the intent_ack config. The upward direction has its own essay: Bidirectional Traceability.