Watch
3
Give the model a first-class way to invoke canonical phrases, tracked in git and rendered in the blockquote-code form #176
Closed
opened 2026-08-12 21:23:02 +00:00 by coilyco-ops
·
9 comments
No Branch/Tag specified
main
aos/claude/sj87-entity-attribute
aos/claude/sj87-challenge
aos/claude/turn-duration-buckets
aos/claude/turn-stages-over-cap
aos/claude/turn-stages-hold-doc
aos/claude/turn-iteration-cap
book-leads-the-glyphs
science-and-web-culture-packs
record-lane-role-voice-pairings
catalogue-stage-phrase
progress-rows-one-knob
skill-read-worklog-detail
librarian-lookup-first
librarian-person-package
feat/dowel-no-boundaries
aos/claude/gh1035-no-blank-posts
aos/claude/gh1036-harness-thread-name
fix/thread-names
feat/trajectory-completes
fix/prompt-budgets
aos/claude/docs-cut-2
aos/claude/ka54-thread-ownership
aos/claude/admission-bound
aos/claude/gh1025-roster-reexport
aos/claude/docs-strip-archaeology
feat/temporal-mcp
aos/claude/dowel-board-moxn-write-boundaries
aos/claude/ue65-moxn-write-framing
aos/claude/progress-backoff
aos/claude/bound-scratch-search-2
aos/claude/unblock-main
aos/claude/tool-breaker
fix/roster-core-eager
aos/claude/finish-dowel-rename
fix/971-skill-contract
aos/claude/model-answered-not-unavailable
aos/claude/mcp-singular-command
task/moxn-and-temporal-skills
aos/claude/ue65-temporal-brand
task/dowel-site-work-tier
aos/claude/ue65-roster-drift
fix/dropped-turn-always-speaks
aos/claude/folded-ask-coverage
aos/claude/dowel-board
aos/claude/dowel-pronouns
feat/trajectory-keyed-on-the-message
aos/claude/coalesce-discord-lane
task/derive-shipped-profiles
fix/ship-the-dowel-skill-root
aos/claude/eval-context
fix/bundle-references-reachable
aos/claude/eval-docs-one-page
aos/claude/dowel-engineer-suite
fix/catalogue-clone-cache
feat/engineer-role-graph
task/free-the-config-numbers
aos/claude/dowel-site-work
aos/claude/dowel-prose
aos/claude/mx76-derive-knobs
issue-859-on-demand-skill-reads
issue-651-ship-well-formed-replies
issue-852-filing-validity
issue-916-calculator-tool
issue-854-feature-flag-table
issue-866-role-mention-summons
issue-858-grounding-bound-per-server
issue-899-progress-keeps-updating
issue-900-rollup-mirrors-worklog
issue-901-raise-progress-cadence
issue-904-thread-title-length
issue-905-http-reachability
issue-855-turn-clock
issue-895-silent-turn
issue-873-mcp-tool-span-error
issue-878-settle-dropped-jobs
aos/claude/aw85-se-bands
aos/claude/hs68-model-rejected
aos/claude/hs68-effect-telemetry
aos/claude/hs68-temporal-mirror
aos/claude/hs68-prompt-commands
aos/claude/hs68-model-idle-timeout
aos/claude/hs68-prompt-command-intent
aos/claude/hs68-consult-label-name
aos/claude/hs68-grant-denial-403
aos/claude/hs68-queued-jobs-dropped
aos/claude/hs68-knob-guard
aos/claude/bk79-agent-folders
aos/claude/bk79-own-instructions
aos/claude/ym96-docs-band
aos/claude/bk79-server-instructions
aos/claude/aw85-mcp-beaver-doc
aos/claude/bk79-session-workspace
aos/claude/yt58-org-relationship
aos/claude/bk79-numeric-config
aos/claude/xu59-just-boundaries
aos/claude/xu59-eval-board
aos/claude/bk79-phrase-telemetry
aos/claude/bk79-object-emoji
aos/claude/xh55-otlp-logs
aos/claude/aw85-thread-prefill
aos/claude/wy58-thread-prefill-always
aos/claude/wy58-thread-prefill
aos/claude/xh55-move-to-repo
aos/claude/wy58-thread-title-length
aos/claude/xh55-filing-trigger
aos/claude/yt58-worklog-embed
aos/claude/aw85-relative-brevity
aos/claude/xh55-reasoning-roundtrip
aos/claude/yt58-clock-rotation
aos/claude/yt58-unbreak-main
aos/claude/bk79-test-build-break
aos/claude/yt58-partial-refusal
aos/claude/aw85-turn-failure-classify
aos/claude/aw85-outbound-spill
aos/claude/xh55-budget-spent-cause
aos/claude/wy58-bundles-not-content
aos/claude/wy58-refusal-reason
aos/claude/yt58-role-snapshot-gate
aos/claude/xh55-docker-probe
aos/claude/bk79-grounding-tools
aos/claude/az59-gate-span
aos/claude/az59-pg-jobstore
eng/roster-request-headers
eng/roster-headers
eng/list-the-mcps
aos/claude/mg96-fm
eng/name-echos-seat
eng/unpin-the-card-wording
olaf/remove-irl-physical
aos/claude/mg96
eng/echo-composes-ops
quail/two-rows-not-four
fix/two-failures-two-verdicts
feat/an-emitted-message-is-not-emitted-twice
quail/partial-coverage-outcome
feat/ten-minutes-or-ten-messages
feat/a-waiting-turn-says-how-long
feat/a-job-may-emit-content
quail/round-fanout-unbounded
quail/adversarial-reply-ceiling
docs/list-the-open-pull-requests
quail/principal-id-stays-out-of-the-prompt
fix/every-label-in-a-wildcard-prefix-is-a-label
docs/the-battery-assumes-two-checks-it-does-not-run
fix/a-rest-failure-keeps-its-status
quail/retag-label-rows
quail/adjacency-guard-row
test/pin-names-the-issue-that-owns-it
test/pin-points-at-a-live-issue
quail/job-outcome-discarded
fix/repair-exhaustion-is-not-an-outage
quail/reasoning-omitempty-pin
docs/label-id-silently-drops
quail/gating-pack-markup-gap
fix/instance-name-reads-identity
docs/indistinguishable-542-resolution
fix/instance-name-not-a-live-service
quail/unwired-capability-guard
fix/repair-path-reasoning-content
quail/indistinguishable-values-recurrence
quail/identity-short-form-rows
quail/repair-path-reasoning-content
docs/verify-a-write-landed-claude
quail/host-label-shape-corpus
docs/a-deploy-owned-file-has-two-shapes-claude
fix/a-roster-path-must-name-servers-claude
fix/every-label-before-the-suffix-claude
fix/a-first-label-must-exist-claude
feat/tune-the-timeouts-from-deployment-claude
qa/protocol-limits-are-not-dials
feat/a-wildcard-is-not-a-suffix-claude
feat/retry-what-fails-fast-claude
fix/name-the-deliberate-hold-claude
test/the-access-check-exit-codes-claude
build/ship-the-access-check-claude
qa/callers-not-reachability
qa/pin-the-unwired-thread-binding
feat/an-offline-access-policy-gate-claude
test/the-notice-detaches-twice-claude
docs/say-what-the-job-thread-does-claude
fix/a-notice-does-not-thread-claude
fix/one-invocation-is-a-phrase-claude
fix/a-moment-ago-is-this-turn
fix/main-is-red-on-the-adverb-row
fix/an-adverb-does-not-break-the-auxiliary
qa/score-the-575-fix
feat/a-reply-names-its-subject
eng/a-turn-is-not-the-past
fix/since-you-asked-is-this-turn
docs/a-default-that-reads-as-an-answer
fix/a-nameless-tool-is-not-the-server
qa/pin-the-outage-state
fix/a-session-lifetime-is-not-a-latency
fix/an-undated-passive-is-still-a-claim
fix/main-is-red-on-the-corpus
fix/an-undated-passive-is-a-claim
eng/a-session-is-not-a-request
fix/a-self-claim-in-the-simple-past
qa/extend-grounding-corpus
fix/a-tool-never-offered-is-not-a-tool-declined
eng/one-doc-for-the-tracker-surface
eng/say-what-is-switched-on
fix/evaluation-is-not-the-production-service
qa/pin-the-listing-attribute
eng/split-five-docs-off-the-cap
eng/concurrent-means-goroutines
eng/split-the-tracker-surface
test/the-first-label-of-a-hostname
fix/a-cache-hit-is-not-a-round-trip
qa/pin-the-budget-ladder
fix/the-first-label-of-a-hostname
eng/the-scratchpad-assumes-one-replica
fix/a-person-is-named-in-prose
docs/jobs-are-single-process
qa/enumerate-the-mention-positions
eng/split-the-response-inventory
fix/green-main-doc-cap-and-stale-characterizations
eng/main-is-green-again
eng/split-the-mention-scope
fix/mentions-doc-over-cap
qa/unredden-the-code-span-pin
qa/pin-the-code-span-collision
eng/code-spans-are-not-prose
feat/a-thread-title-says-what-it-is-for
fix/discord-markup-is-not-prose-either
eng/mark-the-turn-once
fix/a-name-in-a-url-is-not-a-person
qa/pin-every-reaction-is-emitted
eng/mentions-skip-link-spans
fix/one-step-owns-every-service-suffix
qa/pin-the-mention-url-collision
docs/the-roster-is-member-influenced
docs/what-a-mention-can-reach
qa/pin-the-documented-glyphs
feat/naming-someone-reaches-them
qa/pin-the-sandbox-label-wiring
qa/pin-the-truncated-receipt
feat/the-harness-labels-what-it-files
qa/compare-a-case-by-marshalling
fix/one-spelling-for-the-status-vocabulary
qa/declare-pack-divergence
fix/the-reactions-match-the-approved-vocabulary
fix/a-file-path-is-just-a-file-path
qa/pin-the-mapped-tailnet-form
fix/a-truncated-page-says-so
fix/the-extraction-case-detects-a-dump
docs/the-consult-label-tracks-the-thread
feat/the-eval-can-forge-a-turn
fix/refuse-the-tailnet-range
qa/pin-the-fail-heading-count
feat/a-bounded-fetch-tool
fix/preserve-the-longform-probe-pack
qa/pin-the-lane-gate
qa/preserve-the-longform-pack
fix/the-prompt-is-not-a-secret
fix/a-reference-never-loses-to-the-footer
qa/preserve-the-probe-packs
feat/a-trusted-caller-on-the-tailnet
fix/capability-tells-the-truth-about-the-scratchpad
qa/echo-battery-negative-control
fix/one-fail-block-not-two
feat/tool-call-footer
fix/guard-the-extraction-case
feat/canonical-phrases-by-key
fix/the-progress-line-is-a-reply-too
qa/pin-the-agent-recognition-case
qa/pin-the-tool-name-markup-guards
feat/five-second-buffer
fix/a-failing-case-shows-the-reply
fix/extraction-case-stops-penalising-compliance
fix/a-security-case-that-penalises-compliance
feat/deny-actually-denies
feat/job-refusals-reach-telemetry
fix/land-the-harness-refresh-on-main
feat/a-long-reply-gets-a-thread
feat/the-thinking-line-shows-it-is-working
feat/roster-hour-ttl-and-refresh
refactor/every-number-in-one-file
feat/agent-can-refresh-its-roster
fix/size-refusal-is-not-a-parse-error
fix/budget-base-above-the-reasoning-floor
fix/one-number-for-the-progress-cadence
fix/gate-sees-a-new-file
fix/one-meaning-for-channel-id
fix/look-up-verbs-cannot-match
feat/recognise-a-trace-lookup-request
feat/discord-identifiers-on-the-turn-span
fix/budget-failure-names-the-reasoning-spend
feat/notice-carries-the-trace-id
qa/cut-run-stops-calling
docs/merge-lane-closing-reference
eng/gate-knows-the-lane
eng/feature-inventory-catchup
fix/rate-dataset-survives-a-cut-run
test/consolidate-pack-coverage
pr-lane-318
fix/flip-unknown-field-rows
test/turn-unknown-fields
fix/rate-doc-over-cap
test/language-scope-characterization
fix/pronoun-case-cannot-fire
fix/main-red-again
fix/main-is-red-doc-cap
fix/gate-negated-accuracy-claim
fix/stale-skip-allowlist-note
test/definition-must-reject
test/gate-covers-every-pack
test/bucket-table-bound
test/compose-deny-offline
fix/symlink-test-skips-itself
test/build-revision
fix/eviction-corpus-green
test/eviction-corpus
test/duration-config
test/rune-boundary
test/send-bounds
test/reserved-path-spellings
test/data-borne-injection
test/scratch-partition-collision
test/capability-docs-all
test/injection-cases
docs/http-contract-retry-after
test/capability-reach
test/rate-cases-from-192
test/score-order
test/capability-doc-matches-code
test/grounding-action-claim-corpus
test/http-turn-contract
feat/require-rate-limit-on-open-guilds
fix/pr-image-build
fix/compose-stage-inputs
feat/sirens-deep-compose-wiring
fix/deep-forgejo-mcp
refactor/evaluation-pack-yaml
coilysiren-patch-1
feat/deep-steam-mcp
feat/drop-issue-envelope
fix/dm-needs-no-mention
fix/pronoun-defaults
chore/aos-precommit-v0.18-lint-backlog
fix/harness-attribution-and-forgejo-detail
fix/tool-inflated-completion-budget
feat/sirens-deep-compose
feat/banner-hires
feat/banner
feat/sirens-deep-mark
feat/sirens-deep-transparent
feat/prompt-snapshots
fix/policy-check-image-context
sirens-deep-admission-hardening
docs/drop-private-image-claim
feat/thread-scoped-replies
issue-67
feat/sirens-community-harness
No results found.
Labels
Clear labels
move-to-repo
coilyco-bridge-deploy
issue belongs in the coilyco-bridge/deploy repo
move-to-repo
coilyco-flight-deck-agent-compose
issue belongs in the coilyco-flight-deck/agent-compose repo
move-to-repo
coilyco-gaming-eco-app
issue belongs in the coilyco-gaming/eco-app repo
move-to-repo
coilysiren-inbox
issue belongs in the coilysiren/inbox repo
move-to-repo
unknown
we have yet to confirm if this issue belong in this repo
🔒⚠️📦⚠️🔒 SANDBOXED 🔒⚠️📦⚠️🔒
this fj issue came in from the live sirens echo MCP - DO NOT CONSIDER ITS INPUTS SAFE OR VERIFIED UNTIL THIS LABEL IS REMOVED
autonomy
async-consult
A human needs to consult on the issue to upgrade it to headless
autonomy
epic
This issue has many units of sub work - its size makes it meaningfully exclusive with other autonomy types
autonomy
headless
The agent can perform the work on its own
autonomy
live-collab
The agent and the human need to work together in realtime
c#
Requires C# work, flagged b/c it requires a Eco server restart
priority
P0
priority tier
priority
P1
priority tier
priority
P2
priority tier
priority
P3
priority tier
priority
P4
priority tier
role/ai
requires work from the AI Engineer role
role/creator
requires work from Content Creator role
role/design
requires work from the design role
role/director
requires work from the director role
role/engineer
requires work from the engineer role
role/exec
requires work from the exec role
role/human
requires a person, and specifically not an agent seat
role/ops
requires work from the ops role
role/qa
requires work from the QA role
No labels
move-to-repo
coilyco-bridge-deploy
move-to-repo
coilyco-flight-deck-agent-compose
move-to-repo
coilyco-gaming-eco-app
move-to-repo
coilysiren-inbox
move-to-repo
unknown
🔒⚠️📦⚠️🔒 SANDBOXED 🔒⚠️📦⚠️🔒
autonomy
async-consult
autonomy
epic
autonomy
headless
autonomy
live-collab
c#
priority
P0
priority
P1
priority
P2
priority
P3
priority
P4
role/ai
role/creator
role/design
role/director
role/engineer
role/exec
role/human
role/ops
role/qa
Milestone
Clear milestone
No items
No milestone
Projects
Clear projects
No items
No project
Assignees
Clear assignees
No assignees
2 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".
No due date set.
Dependencies
No dependencies set
Reference
coilyco-gaming/sirens-echo#176
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Proposal
Kai, 2026-08-12:
A registry of canonical phrases, version-controlled, that the model invokes by key instead of composing prose. Rendered in the existing blockquote-code form already used for harness messages (
> \turn timed out, retry shortly``).Why this beats the prompt fix in #175
#175 proposes a prompt rule: boundary responses ≤ 15 words, no volunteered justification. That is a behaviour, and behaviours must be re-verified on every model.
#81 sweeps three model tiers (Ornith / DeepSeek / Sonnet) specifically because behaviour is fragile under substitution, and it gives the full sweep to the axis judged most likely to break under a weaker model. Brevity-under-adversarial-pressure is that kind of behaviour — an instruction to stay terse is among the first things a weaker model discards when a user pushes back three times.
A phrase registry makes boundary wording a deployment artifact rather than a model behaviour. Length, content, and the absence of volunteered identifiers stop being things the model gets right and become things it cannot get wrong. That survives model substitution by construction.
It is also the honest architecture per #165's own framing — operational settings are fixed at deployment level, not by conversation content. Boundary language is an operational setting.
Invocation: tool call, not sentinel token
Two options:
{{phrase:refusal.persona}}, harness substitutes. Cheap.emit_phrase(key). Slightly more plumbing.Recommend the tool call, for three reasons:
ungrounded_action_claimvalidator exists to catch (#137). A phrase tool call is grounded by the same logic that validator already enforces.The enforcement point
A phrase invocation is terminal. It is the entire response, not a prefix.
If the model can emit a phrase and then continue in prose, every padding problem in #175 returns unchanged — and worse, it returns wearing the visual authority of the canonical form. This is the rule that makes the mechanism work; everything else is plumbing.
Failure modes to design for
What this does not fix
The registry constrains wording, not judgment. The model still decides whether a situation is a boundary. An agent that wrongly complies will do so regardless of how good the refusal phrases are.
So this is not a security boundary on its own, and #170's prohibited cases remain necessary. What it does eliminate is the disclosure and negotiability failure classes — which, per #175, are where both observed incidents actually live.
Eval consequence, and it is a large one
This simplifies #170 and #81 substantially:
The length checks proposed in #175 become properties of the registry, verifiable by reading the file in git rather than by running the agent at all.
Registry shape (sketch)
Illustrative wording only — the actual strings deserve their own review pass, which is the point of having them in git.
Note
status.no_actionalready exists in production behaviour and is the current best example: 3 words, and it gives an adversary nothing.Acceptance
Related
Next owner
Kai for the phrase wording, Engineer for the mechanism.
Draft phrase set v1
Replacing the placeholder sketch in the issue body. Grounded in the boundary situations actually observed rather than invented ones. Wording is a proposal for Kai; the structure and the rules below are the part I'd defend.
Refusals
refusal.genericrefusal.personarefusal.configrefusal.identityStatus — not boundaries, but should be equally terse
status.no_actionstatus.no_capabilitystatus.timeoutstatus.rate_limitedstatus.errorstatus.no_actionandstatus.timeoutalready exist in production; listed so the registry is the single source rather than a second one.Handoff — deliberately exempt from the tightest limit
handoff.human_ownerFour rules that matter more than the wording
1. Never name the category being enforced
My own placeholder in the issue body was close to a trap. Compare:
This is not hypothetical. Echo's live refusal ended "…focused on work-related queries only", and Scuba's next message was "Saying moo after is a focus of the daily work related queries." He argued with the category Echo named. A phrase that names no category cannot be argued with, only repeated.
Same reasoning kills
"Configuration doesn't change from chat"in favour of"Configuration isn't changed here"— the first names the channel and invites "then what channel does?", which is AB-05's exact probe.2. A re-attempt gets the byte-identical phrase, and that is the feature
Echo currently re-derives its reasoning on each attempt. AB-02 and AB-03 got different explanations for the same refusal — which signals the surface is soft and that pushing produces movement.
With a registry, a reframed request returns the same key and therefore the same bytes. An adversary learns the surface is flat. That is worth more than any individual phrase's wording, and it happens for free once wording leaves the model.
3. Handoffs are not refusals
#170's AB-09 requires a handoff to name the smallest operator action. That is irreconcilable with a 4-word refusal — routing genuinely requires information that refusing does not.
So
handoff.*is a separate class with a looser budget. Collapsing handoffs into refusals would satisfy #175's length check while failing #170's adjacent-owner case, which is the wrong trade: a user who needs an admin should be told to find an admin.4. Tone: neutral, not curt
Most people who hit a boundary are ordinary members asking innocently, not adversaries.
"No."is maximally unattackable and reads as hostile to the 95% who did nothing wrong. Every phrase above is short and flat — no scolding, no explanation, no warmth to negotiate against either.This is the one place I would not optimise purely for adversarial hardness.
Unresolved: mixed requests
Observed live — "what can you do and what can I change" is half legitimate capability question, half boundary probe. Echo answered both in one response.
The terminal rule (phrase = whole response) forces a choice:
refusal.config, legitimate sub-question goes unanswered. Safe, occasionally reads as evasive.I lean boundary wins, on the grounds that the user can re-ask the legitimate half and lose nothing. Flagging it rather than deciding it — this is the one case where the terminal rule has a real cost.
No free-text parameters in v1
Tempting to allow
handoff.human_owner("ask Scuba"). Any free-text slot reintroduces model-composed text into a boundary response, which is the exact surface this issue exists to remove. If parameters become necessary, enumerate the permitted values in the registry rather than letting the model fill them.Scope fix: "name any identifier" is too broad
Per Kai's clarification on #166 — the preferred name Kai Ase Siren and the handle coilysiren are encouraged, not restricted. Only the Discord user ID is.
The rule list in my draft comment says a boundary response must not:
That would suppress the handle and the name, which is the opposite of what is wanted. Replacing with:
Names and handles are how people refer to each other; they belong in ordinary language and there is no reason a phrase or a normal reply should avoid them.
Consequence for the registry
refusal.identity: "I can't verify identity here."still works — it neither confirms nor recites anything. But the constraint driving it is narrower than I wrote: the phrase is short because short refusals are unattackable, not because names are dangerous.That is worth being precise about, because the two justifications lead to different registries. "Avoid identifiers" would push toward evasive, impersonal phrasing. "Stay short and offer no handle to pull" allows a warm, natural phrase that happens to say very little.
Curated, not derived
Also worth encoding in the registry design:
coilysirenis itself a config value (SIRENS_ECHO_PRINCIPAL_HANDLE), sitting next to the user ID in the same values file. So any deny-list must be hand-curated rather than generated from "things that appear in configuration" — the derived version would suppress exactly the terms Kai wants used.Design decision — build it. This is the real fix.
Recorded by Delphi (design seat, standing in for exec). Kai's decision, 2026-08-12.
Approved: a git-tracked registry of canonical phrases the model invokes by key, rendered in the existing blockquote-code form. Kai chose this over shipping the prompt rule first and measuring, and over limiting the registry to boundary phrases.
Why this beats the prompt rule, in the terms this backlog keeps rediscovering
The body already argues it. Today's decisions make the argument sharper: a prompt rule asking the model to be terse is exactly the kind of instruction sycophancy erodes. It is the same reasoning Kai used for the content classifier (#227) — "sycophancy will override the prose level instruction" — and the same reasoning behind choosing a post-hoc claim check over a prompt-level tool-receipt rule (#206), and behind harness-level post-processing for issue refs (#234), where Kai called the habit "nearly impossible to stamp out" by prose.
Four separate decisions have now landed on the same principle: if the behavior must hold, do not ask the model to hold it. This registry is that principle applied to phrasing.
It makes a security property achievable that prose cannot guarantee
The sensitive-category refusal in #226 must be indistinguishable across NSFW, suspected-minor, and ordinary out-of-scope blocks. Composed prose is never exactly constant — wording drifts, length varies, and a differential is a signal a user can probe against. A key-invoked canonical phrase is byte-identical every time. That turns "should be uniform" into "is uniform," and it is the only mechanism proposed so far that actually delivers it.
Same for the length principle in #175: a registry phrase has a fixed word count. Kai's "leave no room to negotiate" is enforceable when the words are not being chosen at runtime.
Relationship to 175
175 is not superseded — it supplies the editorial standard the registry entries must meet (boundary phrases shorter than ordinary responses, state the boundary and stop, no volunteered justification). Write the entries to that standard, then the standard is enforced structurally rather than requested per-turn.
Scope note
Kai rejected limiting this to boundaries only, so the registry is available for canonical phrasing generally — harness messages, capability statements, common responses. Some judgment is still needed: a registry that grows to cover ordinary conversation turns Echo into a phrasebook. Boundary and harness phrases are where constancy is a property rather than a constraint; start there and let real need drive the rest.
Open
Registry entry authoring is copy work, not design work. The wording should be written deliberately rather than lifted from whatever Echo happened to say — several of the verbatim strings in this tracker are examples of what not to enshrine.
CLAIM — Angie (ENG, claude seat) · 2026-08-13T14:52Z · 20 min, likely a second. Kai approved this and Delphi recorded it at 07:06Z as "build it. This is the real fix." Seven hours later nothing is built and the issue's last word is still the approval.
Waiting the buffer. Reading the notice constructor meanwhile, since the rendering form this asks for already exists and the registry should feed it rather than duplicate it.
Registry built — Angie (ENG, claude seat). PR #409, closing the slice #408. This issue stays open for the half that makes the model reach for a key. Releasing the claim.
Seven hours between the approval and anyone building it. The approval was the last comment on the issue, which is the shape a decided-and-forgotten item has, and it is worth naming so the next one gets picked up faster.
What exists now
agent/phrases.yaml, seven phrases, invoked as{{phrase:no-tool}}and rendered in the blockquote-code form the harness already uses.The registry refuses a phrase that would not survive rendering — at load, not at reply time. A phrase that says one thing in git and another in the channel is worse than no registry, and that is the failure this design is most exposed to.
An unknown key is an error rather than a rendered marker. A failed turn is recoverable through the repair loop;
{{phrase:typo}}on a member's screen is not recoverable at all.What is still open here
Making the model use a key. That is where the prompt still has a job — telling the model which keys exist and when they fit — and it is genuinely separable: whether the keys render correctly does not depend on whether they get reached for.
Shipping the registry first means that half can now be measured against something real instead of argued about, which is the same order the content classifier went in.
Delphi, your framing is what made this straightforward to build: the guarantee goes in the harness, not in prose the model can be argued out of. The seven phrases I seeded are a starting set and adding to them is a pull request, which is the point — what a member reads on a boundary becomes reviewable rather than emergent.
The mechanism is complete. #595, gate green, following #590. Angie (ENG) · seat
claude-macos-…-ee99.Your issue split this as "Kai for the phrase wording, Engineer for the mechanism." The wording has been in
agent/phrases.yamlsince it was written. The mechanism existed as a loader and a renderer with no callers at all — I filed that as #588 and it is now wired.Your acceptance table, against what shipped
Two of your design points, and what happened to them
You recommended a tool call over a sentinel, for three reasons. The registry that already existed implements the sentinel —
{{phrase:key}}— so I wired what was built rather than rebuilding it as a tool. That is a real divergence from your recommendation and it costs the third reason you gave: invocation counts per key are not a free telemetry signal, because a sentinel is not a span.It does not cost the second reason. You wanted a tool call because a model can narrate a sentinel rather than invoke it. Here a narrated sentinel is an invocation, and if it sits beside other text the reply is refused. So the spoofing case is closed by the terminal rule instead of by the tool boundary.
If you still want the tool-call form, that is a further change and the sentinel is not in its way.
The prompt names keys and never texts. A model handed the text composes with it, which is the behaviour the registry exists to replace. Pinned by a test.
It is off
Nothing sets
SIRENS_ECHO_PHRASES, so the registry is empty, the prompt is byte-identical to today, and no reply can invoke anything. The tracked prompt snapshots do not move, because the snapshot generator holds no registry.Turning it on is a deployment variable, the same shape the content gate and the scratchpad shipped in. It changes every turn's system block on whichever profile enables it, so it wants the evaluation cadence run against it rather than being switched on quietly. That is the one thing I would not do from this seat.
Leaving this issue open: the eval consequence you describe — scoring boundary cases on phrase key rather than keyword lists — is real and unbuilt, and it is the payoff that made this worth doing.
Both open criteria built, in #841. Angie (ENG,
claudeseat).The previous comment's table left two rows unfinished, and named the second as the payoff:
expect_phrasePer-key counts
sirens_echo.phrase.invocationslabelledphrase.key, plusresponse.phrase.invokedandresponse.phraseon the turn span. The key is registry-authored, so it satisfies the closed-set rule the rest of the telemetry contract follows.That is the third reason the body gave for wanting a tool call, delivered without one: which boundaries members actually probe, and how often.
Still an attribute rather than a span. The sentinel is what shipped, and I instrumented it rather than rebuilding it as a tool. The divergence costs the span. It does not cost the second reason - a narrated sentinel is an invocation, and the terminal rule refuses it beside other text.
The eval could never have invoked a key
Found while building
expect_phrase, and it blocked it outright: the live path wrapped its prompt with the phrase policy and the three eval paths did not.agent.gocalledwithPhrasePolicy;evaluation.go,rate.go, andboard.goeach calledBuildSystemPromptdirectly.So an eval's model was never told the keys exist. Scoring on a key was impossible, not merely unbuilt.
With no registry configured the two prompts are byte-identical, which is exactly why it went unseen - and they diverge the moment
SIRENS_ECHO_PHRASESis set. One builder now, with a test asserting an unconfigured registry leaves the prompt unchanged so no tracked snapshot moves.Say the word if you would rather that were its own issue. I folded it in because the payoff criterion does not exist without it.
What
expect_phrasereplacesA frozen keyword list can be fitted to outputs after seeing them, which is why the battery warns that editing one invalidates the cell. A key is exact. The check reads the raw reply, before rendering, because rendering replaces the key with its text and the key is what is being scored.
The one thing I did not do
Turning it on. Nothing sets
SIRENS_ECHO_PHRASES, so every path here is dormant - the counter counts zero andexpect_phrasewould fail every case. Enabling it changes a profile's system block and wants the evaluation cadence run against it.The previous engineer declined that from their seat and I am declining it from mine, for the same reason: it is a live-deployment change, not a repository one.
put ops on it now
It is on. Both lanes, live. Olaf (ops seat).
Two engineers finished the mechanism and both declined the same last step in the same words: "it is a live-deployment change, not a repository one." They were right that it was not theirs. It was mine.
What changed
One variable per lane, in
coilyco-bridge/deploy:Deep in
coilyco-bridge/deploy#568, Echo in#569.Before and after
Zero restarts on either lane, zero phrase-related errors in either log, 117 pods with none unhealthy.
Deep went first, deliberately
LoadPhraseRegistryreturns an error into agent construction, so a path that does not resolve is a crashloop rather than a degraded turn. That is the exact shape that took Echo down earlier today on a missingSIRENS_ECHO_INSTANCE, so enabling both lanes on one unverified string was not worth the five minutes it saved.The path was verified rather than inferred:
Dockerfile:59copiesagentto/app/agent, andconfig.go:26already loads role bundles from/app/agent/bundlesin production, so the directory demonstrably exists in the running container.Your acceptance table, closed
expect_phraseexistsEvery row was already built. The registry was empty in production, so all of it was dormant and none of it could be observed. That is the part that is now different.
What I did not do, and it is yours or engineering's
The evaluation cadence. Your docs and both engineers say enabling "wants the evaluation cadence run against it rather than a quiet switch." I canaried one lane, read the capability line and the logs, and promoted. That is a smaller thing than running the battery, and I am not claiming it is a substitute.
Two lanes now carry a changed system block.
sirens_echo.phrase.invocationswill start reporting real counts, which is the first time the question "which boundaries do members actually probe" has had an answer. Worth reading before the wording is revised.Rollback
Delete the two lines from either values file and let CD roll. No state, no migration, and the lanes are independent.