The worklog element, with #370's notice lines as its fallback #820

Merged
coilysiren merged 1 commit from aos/claude/yt58-worklog-embed into main 2026-08-15 17:07:37 +00:00
Member

Closes #111. Direction A, both lanes, plus Kai's 16:23 amendment making the notice lines the fallback.

What renders

Working on it
> ✅ `eco.get_market`
> 📭 `eco.get_stores`
> ❌ `forgejo.list_issue`
> 🔨 `eco.find_trade`
4 tools, 14 seconds elapsed

Resolved rows read as working; a lone spinner reads as possibly hung. That distinction is the whole point, since #137 and #190 were both a live service looking dead.

The two surfaces

EMBED_LINKS decides. Granted gives the embed, absent gives #370's stacked notice lines unchanged, and #370's surface stays maintained rather than superseded.

Read, not assumed. A channel that reads as unknown counts as granted, because a direct message always reads that way. A wrong yes costs one refused call and then degrades; a wrong no would strand the richer surface for a whole turn with nothing to correct it.

A refusal degrades and never fails. Discord answering 50013 routes the same turn to the notice lines and latches, so a channel without the grant pays one rejected call rather than one per beat. Both branches are tested, and per the acceptance note the absent-permission branch is the one that will never be exercised in the Sirens guild, so it has the test that matters.

Why the notice alphabet gained the underscore

The decision is that the code span applies inside the embed, and Kai's own example of it was > \calling forgejo list_issue`. noticeAllowedwas[a-z0-9 ,./-], so list_issuesanitized tolist issue` — a name nobody can look up, in a surface whose entire payload is tool names.

So the widening is implementing the decision rather than relaxing it. It is also narrow: the alphabet's job is that a phrase cannot close the code span early, inject markdown that renders, or span two lines. An underscore does none of those from inside a code span, and the backtick is still stripped. TestHarnessNoticeSanitizesThePhrase gains a row pinning eco.get_market intact, and its markdown row now expects _failed_ to survive, which is the honest statement of the new rule.

Glyphs and an honest gap

🔨 📭 , the reaction vocabulary, via toolOutcomeGlyph rather than a second copy. tooldisclosure.go already said the vocabulary was "shared with the reaction set and the progress element", so this is the third rendering that comment anticipated.

An unresolved row on a stopped element keeps 🔨. That is deliberate: 🔨 means tool call invoked in the approved set, which is exactly what is known. Resolving it to would claim a failure the harness cannot support, since the call may have succeeded and the turn died after.

Terminal states

Delivered answer deletes the element. The reply's disclosure footer already names the tools, and two lists of them is what #385 avoided. This satisfies "progress and the footer never coexist".

Every other ending resolves to Did not finish and stays. An element that merely vanishes mid-narration is the #137 silence in a costume.

The block rule is structural, not a wording convention. Every stop gets the same title, and nothing in progressView takes a category or a reason, so there is no field through which a classifier could leak. A block is not tellable from a timeout.

Two contract rows I did not implement, and why

"Same message: embed cleared, content set." Not done. The element is deleted on success rather than becoming the answer. Making it the answer means routing delivery through the progress message, which bypasses the overflow-attachment path (#791, merged three hours ago) and the thread routing. That is its own change with its own risk, and deleting still satisfies the rule that motivated it. Say the word and it is a follow-up.

"Appearance threshold ~2.5s." Left at 5s. turnProgressAfter is an operator knob (SIRENS_ECHO_PROGRESS_AFTER) with turnProgressEvery and turnLongReplyAfter derived from it, so halving it also halves the edit beat and the thread-creation threshold. The contract asked for the appearance threshold, not those two. It is one line whenever you want it, but it is a tuning decision with side effects the design did not weigh.

Verified

Eleven new tests in worklog_test.go: rows resolve in place, each outcome has its own glyph and no two share one, the cap renders a count for what it dropped, every line matches noticeShape including a capped list, a tool name survives the alphabet, a channel without the permission gets notice lines and not silence, a refused embed falls back, a refusal latches for the turn, a stopped turn resolves the element, a delivered answer deletes it, and nothing in the stopped rendering narrates the stop.

ward exec gate green. The full existing suite passes unchanged apart from the one notice row above.

Not verified, and it cannot be from here

The embed against real Discord. Nothing in this repository has ever constructed one, so the payload shape is asserted against discordgo types rather than against Discord. The first long turn in the Sirens guild is the real check, and the fallback is what makes a wrong guess a degradation rather than silence.

Closes #111. Direction A, both lanes, plus Kai's 16:23 amendment making the notice lines the fallback. ## What renders ``` Working on it > ✅ `eco.get_market` > 📭 `eco.get_stores` > ❌ `forgejo.list_issue` > 🔨 `eco.find_trade` 4 tools, 14 seconds elapsed ``` Resolved rows read as working; a lone spinner reads as possibly hung. That distinction is the whole point, since #137 and #190 were both a live service looking dead. ## The two surfaces `EMBED_LINKS` decides. Granted gives the embed, absent gives #370's stacked notice lines unchanged, and #370's surface stays maintained rather than superseded. **Read, not assumed.** A channel that reads as *unknown* counts as granted, because a direct message always reads that way. A wrong yes costs one refused call and then degrades; a wrong no would strand the richer surface for a whole turn with nothing to correct it. **A refusal degrades and never fails.** Discord answering `50013` routes the same turn to the notice lines and latches, so a channel without the grant pays one rejected call rather than one per beat. Both branches are tested, and per the acceptance note the absent-permission branch is the one that will never be exercised in the Sirens guild, so it has the test that matters. ## Why the notice alphabet gained the underscore The decision is that the code span applies *inside* the embed, and Kai's own example of it was `> \`calling forgejo list_issue\``. `noticeAllowed` was `[a-z0-9 ,./-]`, so `list_issue` sanitized to `list issue` — a name nobody can look up, in a surface whose entire payload is tool names. So the widening is implementing the decision rather than relaxing it. It is also narrow: the alphabet's job is that a phrase cannot close the code span early, inject markdown that renders, or span two lines. An underscore does none of those from inside a code span, and the backtick is still stripped. `TestHarnessNoticeSanitizesThePhrase` gains a row pinning `eco.get_market` intact, and its markdown row now expects `_failed_` to survive, which is the honest statement of the new rule. ## Glyphs and an honest gap 🔨 ✅ 📭 ❌, the reaction vocabulary, via `toolOutcomeGlyph` rather than a second copy. `tooldisclosure.go` already said the vocabulary was "shared with the reaction set and the progress element", so this is the third rendering that comment anticipated. An unresolved row on a stopped element keeps 🔨. That is deliberate: 🔨 means *tool call invoked* in the approved set, which is exactly what is known. Resolving it to ❌ would claim a failure the harness cannot support, since the call may have succeeded and the turn died after. ## Terminal states **Delivered answer** deletes the element. The reply's disclosure footer already names the tools, and two lists of them is what #385 avoided. This satisfies "progress and the footer never coexist". **Every other ending** resolves to `Did not finish` and stays. An element that merely vanishes mid-narration is the #137 silence in a costume. **The block rule is structural, not a wording convention.** Every stop gets the same title, and nothing in `progressView` takes a category or a reason, so there is no field through which a classifier could leak. A block is not tellable from a timeout. ## Two contract rows I did not implement, and why **"Same message: embed cleared, content set."** Not done. The element is deleted on success rather than becoming the answer. Making it the answer means routing delivery through the progress message, which bypasses the overflow-attachment path (#791, merged three hours ago) and the thread routing. That is its own change with its own risk, and deleting still satisfies the rule that motivated it. Say the word and it is a follow-up. **"Appearance threshold ~2.5s."** Left at 5s. `turnProgressAfter` is an operator knob (`SIRENS_ECHO_PROGRESS_AFTER`) with `turnProgressEvery` and `turnLongReplyAfter` *derived* from it, so halving it also halves the edit beat and the thread-creation threshold. The contract asked for the appearance threshold, not those two. It is one line whenever you want it, but it is a tuning decision with side effects the design did not weigh. ## Verified Eleven new tests in `worklog_test.go`: rows resolve in place, each outcome has its own glyph and no two share one, the cap renders a count for what it dropped, every line matches `noticeShape` including a capped list, a tool name survives the alphabet, a channel without the permission gets notice lines and not silence, a refused embed falls back, a refusal latches for the turn, a stopped turn resolves the element, a delivered answer deletes it, and nothing in the stopped rendering narrates the stop. `ward exec gate` green. The full existing suite passes unchanged apart from the one notice row above. ## Not verified, and it cannot be from here The embed against real Discord. Nothing in this repository has ever constructed one, so the payload shape is asserted against `discordgo` types rather than against Discord. The first long turn in the Sirens guild is the real check, and the fallback is what makes a wrong guess a degradation rather than silence.
feat(discord): the worklog element, with the notice lines as its fallback
All checks were successful
ci / image-build (pull_request) Successful in 25s
ci / test (pull_request) Successful in 41s
ci / publish-echo-image (pull_request) Has been skipped
ci / publish-observed (pull_request) Has been skipped
b512ae9c66
Direction A from the #111 design. A long turn renders an embed with one
row per tool call, resolving in place, so a member can see progress
rather than motion. Three resolved rows read as working where a lone
spinner reads as possibly hung, which is the distinction #137 and #190
both turned on.

Two surfaces, because an embed needs EMBED_LINKS and that bit was
missing from every install link this estate published. The permission is
read rather than assumed, and a channel that reads as unknown counts as
granted, because a direct message always reads that way. A wrong yes
costs one refused call and then degrades. A wrong no would strand the
richer surface for a whole turn.

A refusal degrades and never fails. Discord answering 50013 routes the
turn to #370's notice lines and latches, so a channel without the grant
pays one rejected call rather than one per beat. Posting nothing is the
silence this element exists to remove.

Rows are notice-shaped, because Kai's decision is that the embed is the
container and the code span is the text contract. That is why the
alphabet gained the underscore: a tool name is the payload, and
list_issue sanitized to `list issue` is a name nobody can look up.
Markdown cannot act on an underscore inside a code span and the backtick
is still stripped.

The glyphs are the reaction vocabulary rather than a new one. An
unresolved row on a stopped element keeps the hammer, which carries its
approved meaning exactly: invoked, outcome never learned.

Terminal states resolve. A delivered answer deletes the element, since
the disclosure footer under the reply already names the tools. Every
other ending resolves it to one wording, so a content block is not
tellable from a timeout, which is #226 as a structural property rather
than a convention.

Co-authored-by: Kai Siren <coilysiren@gmail.com>
Co-authored-by: Claude <noreply@anthropic.com>
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
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!820
No description provided.