phpbotscout: interface design¶
- Authors
- Matt Cockayne, Claude Opus 5 (AI drafting assistant)
- Date
- 2026-07-26
- Status
- APPROVED — 2026-07-26
- Implements
- Interface surfaces for phpbotscout: draft requirements.
Requirement identifiers (
R-*) refer to that document.
1. Purpose and scope¶
The requirements say what the bot must do. This document explores how it is touched — every surface through which a person, an agent, or a script interacts with it.
It is deliberately a paper exercise. Nothing here is a commitment; the value is in discovering which surfaces exist, which are load-bearing, and which design questions need answering before implementation specs are written. Where a surface raises a question the requirements did not anticipate, that is recorded rather than resolved.
Two framings worth stating up front, because they shape everything below.
The bot has three distinct audiences with almost nothing in common. An asker in a public channel wants an answer and does not care that a bot produced it. A moderator wants a decision queue with evidence attached. The owner wants operational control and a view of what the thing is actually doing. Designing one surface to serve all three produces something that serves none of them.
Several interfaces are not screens. The GitLab issue the bot files is an interface — it is read by maintainers who were never in the Discord thread. The configuration file is an interface, and the one the owner touches most often. Metrics are an interface. All are designed artefacts, and all are easy to leave to accident.
2. Interaction principles¶
These govern every surface. They are derived from the requirements rather than invented here.
P1 — Intent must be unambiguous where the action is privileged. Filing an issue publishes a user's words; a moderation action affects someone's standing. Neither may hinge on parsing "yeah go on then" out of free text. Privileged actions use explicit interactive components (R-ANS-16).
P2 — Show the consequence before asking for consent. "Shall I raise an issue?" is a weaker gate than showing the issue that will be raised. Consent to an unseen artefact is not informed consent.
P3 — The bot is legible about being a bot. It identifies as one, cites its sources, states uncertainty plainly, and never implies authority it does not have (R-GOV-1).
P4 — Public by default, private when it concerns a person. Answers are public because others benefit. Moderation reports and enforcement notices are private, because publishing them amplifies the content and humiliates the subject (R-MOD-9, R-MOD-17).
P5 — Every surface degrades. Discord components can fail, a modal can be dismissed, the LLM can be unreachable. Each surface names its degraded form rather than erroring at the user (R-ARCH-5).
P6 — The operator can always see and stop. Whatever the bot is doing, the owner can observe it and halt it without a redeploy (R-OPS-4).
3. Surface inventory¶
| # | Surface | Audience | Primary purpose | Depth |
|---|---|---|---|---|
| 1 | Discord — asker | Public members | Ask, receive, consent, feed back | Deep |
| 2 | Discord — moderator | Mod roles | Triage, act, review appeals | Deep |
| 3 | Discord — subject | Actioned member | Be told what happened, contest it | Deep |
| 4 | Bot identity | Everyone | Presence, policy, expectation-setting | Sketch |
| 5 | CLI | Owner, scripts, CI | Operate, inspect, develop | Deep |
| 6 | TUI | Owner | Triage and explore interactively | Deep |
| 7 | MCP server | Other agents | Query the corpus programmatically | Deep |
| 8 | Operator dashboard | Owner (private network) | See trends at a glance | Sketch |
| 9 | GitLab issue | Maintainers | Receive escalated work with context | Deep |
| 10 | Configuration | Owner | Change behaviour | Deep |
| 11 | Observability | Owner, future alerting | Know what happened | Sketch |
| 12 | Go package API | Future consumers | Reuse components | Sketch |
4. Discord — the asker¶
The highest-traffic surface, and the one where a poor design is most visible.
4.1 Triggers¶
| Trigger | Behaviour | Notes |
|---|---|---|
| Direct mention | Answer in a thread on the message | The primary path |
| Reply to a bot message | Continue in the existing thread | Follow-up questions keep context |
/ask <question> |
Answer in a thread | Explicit, discoverable, autocompletes nothing |
/docs <query> |
Retrieval only, no synthesis | Cheaper and faster; often what was actually wanted |
| Passive detection | Off by default | R-ANS-2 — an unbidden wrong answer is worse than silence |
Visibility — resolved 2026-07-26 (Q-I1). /docs responds ephemerally; /ask responds
publicly, as do mentions and replies. The split follows what each command is: a lookup is
private and nobody else needs your search results, whereas a question is a conversation whose answer
compounds — it stays in the channel, becomes searchable, and a visibly repeated question is itself a
signal for the gap analysis.
One consequence to design for: an ephemeral /docs result the asker wants to keep should be
promotable. A [ Post to channel ] affordance on the ephemeral response costs little and prevents
the copy-paste-into-a-new-message pattern that loses the citations.
4.2 Anatomy of an answer¶
Posted as the first message in a thread on the triggering message (R-ANS-10).
┌─────────────────────────────────────────────────────────────┐
│ phpbotscout · BOT │
├─────────────────────────────────────────────────────────────┤
│ Yes — `config.Store` snapshots on read, so a value you │
│ read stays stable for the lifetime of the View even if │
│ another goroutine writes. Use `store.View()` to pin, and │
│ `Apply` for transactional writes. │
│ │
│ **Sources** │
│ • config — Snapshot reads · config.go.phpboyscout.uk │
│ • config/store.go:118 · gitlab.com/phpboyscout/go/config │
│ │
│ ───────────────────────────────────────────────────────── │
│ Indexed 4 hours ago · answered in 2.1s │
└─────────────────────────────────────────────────────────────┘
👍 👎 [ Still stuck? ]
Design decisions embedded here:
- Citations are named, not bare URLs. "config — Snapshot reads" tells you whether it is worth
clicking;
https://config.go.phpboyscout.uk/explanation/...does not. - Index freshness is always shown, satisfying R-KB-6 without a separate command. A stale answer is defensible if the staleness is visible.
- Reactions carry the feedback signal (R-ANS-13) — low-friction, familiar, and unobtrusive when ignored.
- "Still stuck?" is the escalation entry point even on a successful answer. The bot's confidence and the asker's satisfaction are different things, and only one of them matters.
Confidence — resolved 2026-07-26 (Q-I2). Low confidence changes the wording, not a badge: "I found this, though it may not cover your exact case." No number is shown. A percentage invites arguing with the number and readers calibrate to it badly — 71% means very little — whereas hedged prose reads like a person who is unsure, which is the honest signal.
The score is still recorded in telemetry. Not showing it is a presentation decision, not a reason to discard it: the volunteer threshold (R-ANS-2) and the agentic-fallback threshold (R-ANS-6) can only be tuned by correlating confidence against the feedback signal (R-ANS-13). Without the score in telemetry those thresholds stay at whatever they were first guessed at.
4.3 The escalation flow¶
The most consequential interaction in the product: it ends with a member's words published to a public tracker.
Step 1 — the bot fails honestly.
┌─────────────────────────────────────────────────────────────┐
│ phpbotscout · BOT │
├─────────────────────────────────────────────────────────────┤
│ I couldn't find this documented. I searched the config and │
│ controls docs and the closed issues on `go/config`, and │
│ found nothing covering hot-reload with a custom backend. │
│ │
│ This looks like a real gap. Want me to raise it so a │
│ maintainer sees it? │
└─────────────────────────────────────────────────────────────┘
[ Raise an issue ] [ No thanks ]
Stating what was searched matters — it turns "I don't know" into evidence that the gap is real, and it lets the asker correct a bad search ("no, it's in the controls docs").
Step 2 — the modal (P2). Clicking Raise an issue opens a pre-filled, editable modal:
┌─── Raise an issue on phpboyscout/go/config ────────────────┐
│ │
│ Title │
│ ┌───────────────────────────────────────────────────────┐ │
│ │ Document hot-reload behaviour with a custom backend │ │
│ └───────────────────────────────────────────────────────┘ │
│ │
│ What you need │
│ ┌───────────────────────────────────────────────────────┐ │
│ │ Does Store.Watch fire for a custom backend, or only │ │
│ │ for file-based ones? Docs don't say either way. │ │
│ └───────────────────────────────────────────────────────┘ │
│ │
│ This will be posted publicly, credited to you as │
│ `someuser`, with a link back to this thread. │
│ │
│ [ Cancel ] [ Post issue ] │
└─────────────────────────────────────────────────────────────┘
This is the strongest form of P2 available. The asker sees the exact artefact, can edit it, is told plainly that it will be public and attributed, and can still cancel. It also improves issue quality — a hand-corrected title beats a generated one.
Step 3 — the confirmation, posted back into the thread (R-ESC-7), and later the resolution (R-ESC-8).
Duplicates — resolved 2026-07-26 (Q-I3). When deduplication (R-ESC-3) matches an existing issue, the bot links it and offers to subscribe the asker: "Someone already raised this — here it is. Want me to let you know in this thread when it's resolved?" A Discord member may well have no GitLab account, so "subscribe on GitLab yourself" is not a real option for them.
This has a data-model consequence. R-ESC-8b currently describes a set of issue-to-thread links; subscription makes it issue-to-many-subscribers, each with their own thread. Small change, but one that is far cheaper to model correctly now than to retrofit.
The subscription count is also the most honest demand signal the bot produces — it says which gaps are hurting repeatedly, which is exactly what should rank the knowledge-gap report.
4.4 Slash command catalogue¶
| Command | Who | Purpose |
|---|---|---|
/ask <question> |
Anyone | Explicit question |
/docs <query> |
Anyone | Retrieval without synthesis |
/appeal |
Actioned member | Open an appeal (R-MOD-18) |
/botscout status |
Anyone | Index freshness, uptime, degraded subsystems |
/botscout mute / unmute |
Mod roles | Kill switch (R-OPS-4) |
/botscout shadow on\|off |
Mod roles | Shadow mode (R-OPS-7) |
Role-gated commands MUST fail visibly and privately — an ephemeral "you don't have permission", never silence (R-MOD-15 requires the attempt be logged).
5. Discord — the moderator¶
A decision queue, not a notification feed. Every element earns its place by supporting a decision.
5.1 The report card¶
┌─────────────────────────────────────────────────────────────┐
│ ⚠ Harassment · confidence 0.81 · #general │
├─────────────────────────────────────────────────────────────┤
│ Author someuser (joined 2 days ago · 4 messages) │
│ Rule harassment/targeted · LLM-adjudicated │
│ Message "…" (excerpt, jump to context ↗) │
│ │
│ Preceding 3 messages available · thread has 2 other │
│ participants │
│ │
│ Suggested: timeout 1h — repeat pattern, new account │
├─────────────────────────────────────────────────────────────┤
│ audit 7f3a91c · detected 14:22:07 · SHADOW MODE │
└─────────────────────────────────────────────────────────────┘
[ Dismiss ] [ False positive ] [ Delete ] [ Timeout ▾ ] [ Ban ]
- Account age and message count are on the card, because "new account, fourth message" changes the read of an ambiguous message more than the confidence score does.
- Surrounding context is offered, not inlined — moderators need it sometimes, and pasting it always would re-publish the content into another channel.
False positiveis a distinct button fromDismiss. Dismiss means "not worth acting on"; false positive means "the detection was wrong". Collapsing them destroys the R-MOD-12 signal, which is the only measure of whether the ruleset is any good.- The audit ID is visible, so a card can be tied to its record months later.
- Shadow mode is stamped on the card itself, not just in config. A moderator glancing at a queue must never be unsure whether the bot is live.
5.2 Agency: the bot executes moderator decisions¶
Resolved 2026-07-26 (Q-I4). When a moderator with an authorised role clicks an action, the bot performs it. The decision stays human; only the execution is delegated. This keeps the audit trail accurate by construction — the bot logs the action it actually took, rather than recording what a moderator later says they did. Self-reported audit trails decay, and R-MOD-11 commits to answering "why was this user actioned" for twelve months.
The cost is that the bot now holds destructive permissions on the server. Three consequences follow, and they are not optional:
The moderation action path must be unreachable from message content. This is the single most important consequence and it sharpens R-ANS-16 considerably. Before this decision, a successful prompt injection could at worst produce a bad answer. Now the process holds timeout and delete permissions, so the action path must be reachable only from a verified component interaction by an authorised role — never from anything an LLM emitted, and never from a code path that message content can influence. The two paths should not merely be guarded separately; they should not meet.
Permissions are scoped to what is actually used. The bot is granted the narrowest Discord permission set that supports the buttons on the card — message management and timeout. Anything rarer or heavier (ban, kick, role changes) is granted only if a button demands it, and the default posture is to prefer the reversible action.
Irreversible actions confirm. Delete and Timeout execute on click. Ban opens a
confirmation with the reason, because the undo is a conversation rather than a button.
5.3 Appeal review¶
An appeal arrives as a card in the same queue, carrying the original audit record inline
(R-MOD-18) — the original message, the matched rule, the
score, who actioned it, and what the member says in response. Outcome buttons are
[ Uphold ] / [ Overturn ], and an overturn writes back as a false positive
(R-MOD-19).
6. Discord — the subject of an action¶
The surface nobody designs, experienced by people having their worst interaction with the community.
A DM, never a channel post (P4):
Your message in #general was removed by a moderator.
Reason: targeted harassment
When: 26 July, 14:22
If you believe this was a mistake you can appeal, and a
different moderator will review it. Reply here with /appeal,
or contact the mod team at <policy link>.
Reference: 7f3a91c
Plain, factual, no lecture, and a route out. The reference ID lets any moderator retrieve the full record. This template is excluded entirely for the self-harm category (R-MOD-20) — that path leads to support resources, not to a rule citation.
Undeliverable notices — resolved 2026-07-26 (Q-I5). Many members block DMs from server members, and some are actioned after leaving. The bot attempts delivery, records the outcome in the audit entry, and surfaces the failure on the report card so a moderator can decide whether to reach the person another way.
What it must not do is fall back to a public post. That would guarantee delivery at the cost of publishing an enforcement action against a named member — amplifying the incident and humiliating the subject, which is exactly what P4 and R-MOD-9 exist to prevent. Delivery is best-effort; knowing whether it succeeded is not.
7. CLI — the operator surface¶
The owner's primary interface, and the only one that exists before Discord is wired up.
7.1 Command tree¶
phpbotscout
├── serve Run the daemon
├── ask <question> Answer from the terminal (R-CLI-1)
├── index
│ ├── build Build from scratch
│ ├── refresh Incremental refresh
│ ├── status Freshness, doc counts, per-source health
│ └── sources List discovered sources and why each qualified
├── moderate
│ ├── test <file> Replay a corpus against the ruleset
│ └── explain <message> Why would this be flagged?
├── gaps Ranked knowledge-gap report
├── issue
│ ├── triage Review pending escalations
│ └── links Tracked issue-to-thread links
├── audit <id|query> Retrieve moderation audit records
├── spend Token spend by task and provider
└── (gtb built-ins) init · config · doctor · docs · mcp · update · version
Two additions the requirements did not name, both earning their place:
index sources— prints each discovered source and the reason it qualified or was excluded. Given that corpus scope is a predicate with a load-bearing exclusion list (R-KB-2), being able to ask "why is this indexed?" is the difference between trusting the rule and hoping.moderate explain— dry-runs one message through the ruleset and prints which rules matched and why. The tool you want when someone asks "why did the bot flag that?".
7.2 Output contract¶
| Aspect | Contract |
|---|---|
--output text |
Human-first, colourised when a TTY, plain when piped |
--output json |
Stable, documented schema — the scripting interface |
| Exit codes | 0 success · 1 operational failure · 2 usage error · 3 degraded (e.g. answered from a stale index) |
| Secrets | Never printed, in any mode (R-ANS-17) |
| Long operations | Progress to stderr, results to stdout, so piping stays clean |
Exit code 3 is deliberate: a script refreshing an index needs to distinguish "worked", "broke",
and "worked but you should look at this".
8. TUI — interactive triage¶
Some tasks are inherently interactive: reviewing a queue, exploring gaps, comparing candidates. GTB already ships Bubble Tea components, so these are cheaper than they look.
Escalation triage (issue triage) — the queue of pending escalations, with the thread context
on one side and the draft issue on the other; approve, edit, reject, or merge into an existing
issue.
Gap explorer (gaps --interactive) — clustered unanswered questions ranked by frequency, drill
into the actual questions, and mark a cluster as addressed. This is the surface where the bot's
failures become a documentation backlog, and a flat text list undersells it.
Timing — resolved 2026-07-26 (Q-I6). The two screens are split:
- Gap explorer ships early, alongside Phase 4 escalation. The knowledge-gap loop is the highest-value thing the bot produces, and it is the one output that is genuinely hard to read as a flat list — clusters want drilling into. Building it while escalation is fresh also means the loop is visible from the first week rather than theoretical until Phase 6.
- Escalation triage waits for Phase 6, when there is actually a queue. Until then
issue triageis plain text, which is adequate for a handful of items and tells you what the screen should show before you build it.
9. MCP server¶
The scaffold already enables the MCP feature, so the marginal cost here is low — and the value is high, because it makes the corpus available to Claude Code and agy directly rather than only through Discord.
| Tool | Purpose |
|---|---|
search_docs(query, limit) |
BM25 search over the corpus, with provenance |
get_document(id) |
Full text of an indexed document |
answer(question) |
The full retrieve-and-synthesise path — same code as Discord |
list_gaps(since) |
Unanswered-question clusters — the docs backlog, queryable |
recent_escalations() |
Issues filed from Discord, with their threads |
Why this matters more than it appears. list_gaps turns the knowledge-gap stream into something
an agent can act on: an agent working on go/config can ask what people have been failing to find
about config, and write the missing documentation. That is the FAQ-synthesis idea from
§11 of the requirements arriving almost for free.
Two constraints carry over unchanged. The corpus exposed through MCP is the same public-only corpus (R-KB-2a) — MCP must not become a side door to content the Discord path would refuse. And MCP over stdio is a local transport, so it adds no network listener and does not disturb R-OPS-8.
10. Operator dashboard (private network)¶
Reachable over the private overlay network, never from the public internet. R-OPS-8 was clarified on 26 July: the prohibition is on public exposure, not on serving HTTP at all. A listener bound to a Tailscale interface is reachable from the owner's laptop and phone wherever they are, and from nowhere else — which is the actual requirement, and considerably more useful than an SSH tunnel.
Three properties follow, and they are requirements rather than preferences (R-OPS-8a–8d):
- Bind an explicit interface, never
0.0.0.0. Binding everything and trusting a firewall is how private services become public.tsnet— Tailscale's embeddable Go node — is the strongest form of this: the process joins the tailnet as its own node and is never on the LAN or a public interface at all, so the property holds by construction rather than by configuration discipline. It also travels with the container if the bot later moves to a cloud host. - The overlay is a boundary, not authentication. Anything on the tailnet reaches anything bound to it. Fine for read-only; not sufficient the moment the dashboard can change state — see Q-I10.
- Never the only path. If the VPN is down, everything but the owner's convenience keeps working. A VPN outage must not become a bot outage.
Content, read-only: index freshness per source · questions and answer rate over time · feedback ratio · escalations offered versus accepted · moderation detections by category with the false-positive rate · token spend by task and provider · knowledge-gap leaderboard.
Scope — resolved 2026-07-26 (Q-I7, Q-I10). Read-only, Phase 6, and provisional.
Read-only keeps the tailnet boundary sufficient on its own (R-OPS-8d) — no authentication layer to build, and no control plane duplicating what Discord and the CLI already do.
It is provisional because every panel is a query the CLI can already answer as JSON, so it may simply not be worth building. The honest case for it is that trends are easier to notice than to query: a rising false-positive rate is obvious on a chart and invisible if noticing it depends on you deciding to go and look. That case is real but unproven, so the decision is revisited at Phase 6 with the JSON output in hand — build it only if piping that into something existing has actually failed to surface anything.
If it ever gains write actions, R-OPS-8d's authentication requirement arrives with them. Tailscale Serve's identity headers make that cheap, but it is a different surface with a different security posture, not an increment.
11. GitLab — the issue as an interface¶
An interface with a genuinely different audience: a maintainer who was never in the thread, reading weeks later, deciding whether this is real and actionable.
## What was asked
Does `Store.Watch` fire for a custom backend, or only for file-based ones?
The docs don't say either way.
## Context
Asked in Discord `#general` by `someuser` on 26 July 2026.
[View the thread](https://discord.com/channels/.../...)
## What the bot searched
- `go/config` documentation site — 4 pages matched, none covering custom backends
- `go/config` repository — `store.go`, `watch.go`
- Closed issues on `go/config` — no match
Confidence in the answer it could construct: below threshold, so it escalated.
## Why this is likely a real gap
The hot-reload documentation covers file-based backends throughout and never
states whether the behaviour generalises.
---
*Filed by phpbotscout with the asker's consent. Replies here are relayed to
the Discord thread.*
Design points:
- "What the bot searched" is the highest-value section. It tells a maintainer instantly whether this is a documentation gap or a search failure — a distinction that changes who should act.
- The asker's handle is inert — backticked, no
@— per R-ESC-9. - The footer sets expectations both ways: it explains provenance, and tells the maintainer their reply reaches a human who is waiting.
Relay — resolved 2026-07-26 (Q-I8). Two things reach the Discord thread: issue closure, and comments a maintainer explicitly marks for relay. Everything else — triage discussion, working theories, cross-references — stays on GitLab. Relaying every comment would push internal conversation into a public Discord channel, and maintainers who notice that start self-censoring in their own issue tracker.
The marking convention must be trivial and mention-safe. A leading marker line on the comment
([reply] or similar) is enough; it needs no GitLab feature, works from any client, and carries no
@ token that could ping a stranger
(R-ESC-9). Whatever is chosen, the issue footer
documents it — which makes the footer do double duty: provenance for the reader, and instructions
for the maintainer who wants to answer the person waiting.
Closure relay must carry the reason. "Closed" alone is close to useless to the asker — fixed, won't-fix and duplicate mean entirely different things to someone waiting. The relay includes the closing comment where one exists, and names the resolution where GitLab records it. The promise and the behaviour must match, whichever is chosen.
12. Configuration as an interface¶
The interface the owner touches most, and the one most often left to accrete.
discord:
channels:
watch: ["general", "support", "go-toolkit"]
mod: "mod-queue"
ignore:
users: []
roles: ["bots"]
moderator_roles: ["moderator", "admin"] # R-MOD-14
corpus:
group: "phpboyscout" # R-KB-1
exclude: ["infra", "iac", "sandbox"] # R-KB-2 — load-bearing
refresh: "6h"
blog: { enabled: true, markdown_only: true }
answering:
enabled: true
shadow: false # R-OPS-7
provider: gemini # R-OPS-9 — per task
thresholds: { answer: 0.7, volunteer: 0.9 }
moderation:
enabled: true
shadow: true # safe default
provider: haiku
rules_file: "rules.yaml"
escalation:
enabled: true
default_project: "phpboyscout/go-tool-base"
labels: ["support", "from-discord"]
budget:
monthly_cap: null # R-OPS-3a — set from measurement
on_exhausted: "index-only"
Design intent, not just structure. Every subsystem carries its own enabled and shadow flags,
so R-ARCH-4 and
R-OPS-7 are satisfied by shape rather than by discipline.
moderation.shadow defaults to true and answering.enabled defaults to false, so a
freshly-installed bot does nothing until someone deliberately turns it on. budget.monthly_cap is
explicitly null rather than absent, because a missing key reads as an oversight while null reads
as a decision.
Discord-changeable settings — resolved 2026-07-26 (Q-I9). Operational toggles only —
mute, unmute, and shadow on|off. Everything else is file-only.
The distinction that makes this safe is that those three are runtime state, not configuration. They are not settings that belong in a version-controlled file; they are answers to "is the bot currently allowed to speak", which is exactly the sort of thing you need to change at 02:00 without shell access (R-OPS-4). Because they are state rather than config, there is no drift problem to solve — the file never claimed to own them.
Two properties follow:
- Runtime toggles persist to the state store, not the config file, and survive restart. A bot muted for a reason that silently un-mutes itself on the next deploy is a worse failure than not having the toggle.
- Current toggle state is visible in
/botscout statusand on the dashboard. A quiet bot should never leave anyone guessing whether it is muted, in shadow mode, or simply broken.
13. Observability as an interface¶
Consumed by the owner today and by alerting later. Named here because the metric set in R-OPS-2 is a designed vocabulary, not an accident.
The three signals worth watching, and what each is really telling you:
- Feedback ratio — the only honest measure of answer quality. Everything else is a proxy.
- False-positive rate — the only thing standing between a moderation ruleset and quiet unfairness.
- Escalations offered versus accepted — a low acceptance rate means the bot is offering to file issues nobody wants, which is a threshold problem, not a demand signal.
Health endpoints distinguish "process alive" from "Discord connected, index loaded, provider reachable" (R-OPS-1), bound to loopback or the private network only.
14. Go package API¶
The requirements already push the reusable contracts upstream — chat-platform and forge. What
remains in this repository is the orchestration, plus two components that may earn extraction later:
the moderation rule engine, and the retrieve-and-cite pipeline behind
R-ANS-5b's Retriever.
Neither should be designed for reuse now. Both should be given package boundaries that would not embarrass a later extraction — which mostly means not reaching into the daemon's state from either.
15. Design questions¶
All ten questions raised by this exploration were resolved on 26 July 2026.
Resolved¶
| # | Question | Outcome |
|---|---|---|
| Q-I4 | Does the bot execute moderator decisions? | Yes — role-gated and recorded (§5.2). Sharpens R-ANS-16: the action path must be unreachable from message content |
| Q-I5 | Undeliverable enforcement DM? | Attempt, record the outcome, flag to moderators (§6). Never fall back to a public post |
| Q-I1 | Ephemeral or public? | /docs ephemeral, /ask and mentions public (§4.1) |
| Q-I2 | Surfacing low confidence? | Hedge the prose, show no number, record the score in telemetry for threshold tuning (§4.2) |
| Q-I3 | Second asker on a duplicate? | Link it and offer subscription — makes R-ESC-8b a subscriber set, not a single thread (§4.3) |
| Q-I8 | What relays back from GitLab? | Closure (with reason) and explicitly-marked comments only (§11) |
| Q-I6 | TUI timing? | Gap explorer early with Phase 4; triage TUI at Phase 6 (§8) |
| Q-I7 / Q-I10 | Dashboard scope? | Read-only, Phase 6, revisited against --output json first (§10) |
| Q-I9 | Changeable from Discord? | Operational toggles only — runtime state, persisted, not config (§12) |
16. Requirements this exploration suggests amending¶
All applied 2026-07-26 to the requirements, which now carries 140 requirements. Retained here as the record of what this exercise changed and why.
R-OPS-8 should permit an operator-only private-network interface.✅ Amended 2026-07-26 — R-OPS-8a–8d now permit private-overlay listeners, require an explicit bind interface, and separate network boundary from authentication.- ✅ R-MOD must state that "no autonomous action" permits delegated action: the bot executes a decision an authorised moderator made (Q-I4). It must also require the narrowest viable Discord permission grant, a confirmation step on irreversible actions, and — most importantly — that the moderation action path is unreachable from any code path message content can influence.
- ✅ R-MOD-17 must specify undeliverable-DM behaviour: attempt, record the outcome in the audit entry, surface the failure to moderators, never fall back to a public notice (Q-I5).
- ✅ R-ESC-3 must cover the second asker: link the existing issue and offer subscription (Q-I3).
- ✅ R-ESC-8b must model issue-to-subscribers, not issue-to-thread — subscription means several people across several threads await one issue.
- ✅ R-ESC-8 must scope the relay to closure plus explicitly-marked comments, and require the closure relay to carry the resolution reason (Q-I8).
- ✅ R-ANS-13 must require the confidence score to be recorded alongside the feedback signal. Q-I2 hides the score from users, which makes it easy to assume it need not be stored — but the R-ANS-2 and R-ANS-6 thresholds can only be tuned by correlating the two.
- ✅ R-OPS-4 must state that the kill switch and shadow toggle are runtime state persisted across restart, not configuration. A bot muted deliberately that un-mutes itself on the next deploy is a worse failure than having no toggle (Q-I9).
- ✅ R-ANS-16 must be strengthened now that the bot holds destructive Discord permissions: the moderation action path must be reachable only from a verified component interaction by an authorised role, and must share no code path with anything an LLM emitted or message content can influence.
Applying them also exposed two live contradictions the exercise had not predicted: the
non-goals section still said the bot "does not delete, timeout, or ban", and R-ARCH-11 still assumed
the moderation capability was unused in v1. Both were true before Q-I4 and false after it. Deciding
that the bot executes moderator decisions moves real work upstream into
chat-platform-discord on the Phase 5 timeline — which is exactly the kind of consequence that
stays invisible until the decision is written down and reconciled.