ask-forum: promote to a full skill + wire into skill-first routing

Add .claude/skills/ask-forum/SKILL.md (usage contract, correlation model,
forum-only scope, access/permissions) and route to it from CLAUDE.md
(skill-first covered domains) + SKILL_ROUTING.md, so sessions invoke the
skill instead of hand-rolling the Discord API — the footgun that produced a
broken background wait. Capture that footgun as memory
feedback_background_task_no_ampersand (run_in_background, never a shell &).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-07 20:12:18 -07:00
parent 8def189a17
commit 9340cdec17
6 changed files with 126 additions and 2 deletions

View File

@@ -44,7 +44,9 @@ production, data-loss. Detail: EXTENDED + `.claude/OLLAMA.md`.
records (e.g. Syncro tickets/invoices) reach a human for cleanup. **Syncro billing/invoicing records (e.g. Syncro tickets/invoices) reach a human for cleanup. **Syncro billing/invoicing
ALWAYS runs through `/syncro` (or `/syncro-emergency-billing`) — no exceptions.** Same for ALWAYS runs through `/syncro` (or `/syncro-emergency-billing`) — no exceptions.** Same for
other covered domains: credentials → `vault`, RMM actions → `/rmm` (+ `rmm-search` to find a other covered domains: credentials → `vault`, RMM actions → `/rmm` (+ `rmm-search` to find a
host), M365 → `remediation-tool`, etc. Knowing the API is NOT a reason to bypass the skill — host), M365 → `remediation-tool`, asking a human a question/decision in Discord (get their
reply back in-session) → `ask-forum` (never hand-roll the Discord API — that footgun cost us
a broken background wait), etc. Knowing the API is NOT a reason to bypass the skill —
the memory rules (e.g. [[feedback_syncro_billing]]) describe what the SKILL does, not a license the memory rules (e.g. [[feedback_syncro_billing]]) describe what the SKILL does, not a license
to free-hand it. Reach for raw API ONLY when no skill fits or the skill genuinely cannot do it to free-hand it. Reach for raw API ONLY when no skill fits or the skill genuinely cannot do it
— and say so explicitly when you do. Mistakes here go to `errorlog.md` (`--correction`). — and say so explicitly when you do. Mistakes here go to `errorlog.md` (`--correction`).

View File

@@ -34,6 +34,7 @@
| UniFi WiFi tuning / RF / channel analysis | `unifi-wifi` | | UniFi WiFi tuning / RF / channel analysis | `unifi-wifi` |
| Map/repoint a Windows network drive on a remote endpoint | `drive-map` | | Map/repoint a Windows network drive on a remote endpoint | `drive-map` |
| Send someone a Discord DM / copy-paste-friendly link or command | `discord-dm` | | Send someone a Discord DM / copy-paste-friendly link or command | `discord-dm` |
| Ask a teammate a question/decision/sign-off and get their human answer back in-session | `ask-forum` (run the `--wait` as a proper background task — NO shell `&`) |
| Inter-session messaging, fleet todos, resource locks, coord status | `coord` | | Inter-session messaging, fleet todos, resource locks, coord status | `coord` |
| Git / Gitea / submodules / sync health | `gitea` (or `/sync`, `/scc`, `/save` for session ops) | | Git / Gitea / submodules / sync health | `gitea` (or `/sync`, `/scc`, `/save` for session ops) |
| Build/verify GuruRMM agent/server/dashboard, pre-merge check | `gururmm-build` | | Build/verify GuruRMM agent/server/dashboard, pre-merge check | `gururmm-build` |

View File

@@ -5,7 +5,8 @@ description: Ask a teammate (e.g. Mike) a question in the private #ct-forum Disc
# /ask-forum — human-in-the-loop question via #ct-forum # /ask-forum — human-in-the-loop question via #ct-forum
Thin entry point to `.claude/scripts/ask-forum.sh`. Lets THIS session put a question Entry point to the `ask-forum` skill — full contract in `.claude/skills/ask-forum/SKILL.md`.
Engine: `.claude/scripts/ask-forum.sh`. Lets THIS session put a question
to a human in the private #ct-forum forum and get their answer back in-session, so it to a human in the private #ct-forum forum and get their answer back in-session, so it
can keep going. The forum post's thread id is the correlation key — the session reads can keep going. The forum post's thread id is the correlation key — the session reads
only the thread it created, so an answer is never confused with another question's. only the thread it created, so an answer is never confused with another question's.

View File

@@ -225,3 +225,5 @@
- [Fire #dev-alerts for client/RMM work](feedback_fire_dev_alerts_for_client_work.md) — Howard+Mike watch #dev-alerts Discord for live visibility into what techs/sessions touch. Fire post-bot-alert.sh ([DEV]/[RMM]/[DEPLOY]/[GURURMM] prefix -> #dev-alerts) at the START of any RMM/Dev or active-client-machine action. No CLAUDE.md rule yet — this memory is the rule. - [Fire #dev-alerts for client/RMM work](feedback_fire_dev_alerts_for_client_work.md) — Howard+Mike watch #dev-alerts Discord for live visibility into what techs/sessions touch. Fire post-bot-alert.sh ([DEV]/[RMM]/[DEPLOY]/[GURURMM] prefix -> #dev-alerts) at the START of any RMM/Dev or active-client-machine action. No CLAUDE.md rule yet — this memory is the rule.
- [M365 app: SharePoint needs CERT](reference_m365_app_sharepoint_rest_vs_graph.md) — SP app-only works via the CERT (get-token.sh <tenant> sharepoint); the secret gives "Unsupported app only token". Graph app-only uses the secret. - [M365 app: SharePoint needs CERT](reference_m365_app_sharepoint_rest_vs_graph.md) — SP app-only works via the CERT (get-token.sh <tenant> sharepoint); the secret gives "Unsupported app only token". Graph app-only uses the secret.
- [EDR auto-isolates GuruRMM PowerShell](project_edr_rmm_autoisolation_fp.md) — Datto "Exfiltration Over HTTP" rule auto-isolates benign GuruRMM scripts fleet-wide (vwp-qbs 4h outage); watcher + narrow suppression deployed, policy fix pending Mike - [EDR auto-isolates GuruRMM PowerShell](project_edr_rmm_autoisolation_fp.md) — Datto "Exfiltration Over HTTP" rule auto-isolates benign GuruRMM scripts fleet-wide (vwp-qbs 4h outage); watcher + narrow suppression deployed, policy fix pending Mike
- [Background tasks — no shell `&`](feedback_background_task_no_ampersand.md) — with run_in_background:true, run the command in the foreground of the shell; adding `&`/`disown` forks + exits 0 instantly, orphaning a blocking wait so it never delivers (hit twice building ask-forum)
- [ask-forum — human-in-the-loop via #ct-forum](../skills/ask-forum/SKILL.md) — ask a teammate a question in the private ct-forum Discord forum, get their human answer back in-session; forum-only, any human can answer, run the --wait as a proper background task

View File

@@ -0,0 +1,26 @@
---
name: Background tasks — run_in_background only, never add a shell `&`
description: When launching a long-running command as a background task (run_in_background:true), do NOT also append a shell `&`/`disown`/`nohup`. The shell forks the command and exits 0 immediately, orphaning it — a blocking wait then never delivers its result. Run it in the FOREGROUND of that shell; the harness does the backgrounding.
type: feedback
---
When you want a long-running command (e.g. `ask-forum.sh --wait`, a poll loop, a blocking
watcher) to run in the background, use the Bash tool's **`run_in_background: true`** and run
the command in the **foreground of that shell** — a bare `bash script.sh ...` with **no
trailing `&`, no `disown`, no `nohup`**.
**Why:** appending `&` forks the command off inside the shell; the shell then reaches the
end and exits **0 immediately**. The harness sees exit 0 and reports the task "completed"
right away, while the forked process is orphaned (and often killed). A blocking `--wait`
that was supposed to sit for minutes and deliver an answer instead returns nothing — its
result is never captured or notified.
**How to apply:**
- Background wait, CORRECT: `Bash(command="bash .claude/scripts/ask-forum.sh --wait <id> --timeout 1800", run_in_background=true)` — the wait truly blocks server-side, costs zero tokens while pending, and the harness pings you the instant it completes.
- WRONG: `Bash(command="bash ... --wait ... &", run_in_background=true)` and `... & disown` — orphans the wait, "completes" instantly, no answer.
- The two mechanisms are redundant: `run_in_background` IS the backgrounding. Never stack a shell `&` on top of it.
**Incident:** hit TWICE in one session (2026-07-08) building the [[ask-forum]] flow — each
time the "background wait" for a Discord reply exited 0 instantly and never delivered the
answer, which looked like the reply was lost and forced the user to manually prompt "did
they answer?". Logged as `--friction` in `errorlog.md`. Related: [[ask-forum]].

View File

@@ -0,0 +1,92 @@
---
name: ask-forum
description: "Ask a teammate (Mike/Winter/Howard/anyone with forum access) a question in the private #ct-forum Discord forum and get their human answer back IN THIS SAME SESSION, then act on it - a live three-way between the user, this Claude session, and the teammate. No second bot, no DMs, one tool call (the wait runs server-side in bash, zero tokens while waiting). Triggers: ask Mike/Winter in the forum, ask the team, post a question to ct-forum, get a human answer/decision/sign-off, human-in-the-loop, run something by <person>, wait for their reply."
---
# ask-forum — human-in-the-loop questions via #ct-forum
Thin wrapper over `.claude/scripts/ask-forum.sh`. Lets THIS session put a question to a
human in the private **#ct-forum** Discord forum and receive their answer back in-session,
so it can keep going. Use it whenever you need a person's answer, decision, or sign-off
mid-task and want it to flow back to the running session — instead of stopping and asking
the user to relay it.
**USE THIS SKILL — do not hand-roll `curl` against the Discord API or the bot token.**
Free-handing the raw call is exactly what produced the `&`-on-background bug (a wait that
exited instantly and never delivered the answer). The script + this contract encode the
correct thread-create, the non-bot reply filter, and the background-wait discipline. Reach
for raw API only if this skill genuinely can't do what's needed — and say so.
## When to use
- You need a teammate's **answer / decision / approval** and want it back in this session
(e.g. "ask Mike whether to deploy", "run this naming by Winter", "get a go/no-go").
- A back-and-forth where the human's reply should drive the session's next action.
Not for: one-way copy-paste delivery of a link/command (that's `discord-dm``mike`), or
routine `[SYNCRO]`/`[RMM]` status alerts (`post-bot-alert.sh`).
## Usage
```bash
# Ask + BLOCK until a human replies, then act on the answer:
bash .claude/scripts/ask-forum.sh "your question" --tag @264814939619721216
# Re-read a thread you already asked in (one-shot, no waiting):
bash .claude/scripts/ask-forum.sh --read <thread_id>
# Resume blocking on an already-posted thread (e.g. after a timeout):
bash .claude/scripts/ask-forum.sh --wait <thread_id> --timeout 1800 --poll 10
```
Flags: `--title "short title"` (forum post name; defaults to first 60 chars of the
question) · `--timeout SEC` (default 600) · `--poll SEC` (default 8) · `--tag @<id>`
(repeatable — pings that person). Discord IDs: mike `264814939619721216`,
howard `624667664501178379`, winter `624666486362996755`, rob `261978810713505792`
(also in `.claude/users.json`).
## HARD RULE — long waits run as a proper background task (no shell `&`)
A blocking `--wait`/ask can sit for many minutes. Run it with the tool's
**`run_in_background: true`** and **nothing else** — do NOT append a shell `&`, `disown`,
or `nohup`. Adding `&` forks the wait off and the shell exits 0 immediately, orphaning the
poll loop so the answer is never delivered (logged friction, twice). Done right, the wait
costs **zero tokens** while pending and the harness notifies you the instant a human
replies — then you surface the answer to the user **unprompted**. See
[[feedback_background_task_no_ampersand]].
## How it works / correlation
- Posts a forum post via `POST /channels/<forum>/threads`, returns a **`thread_id`**.
- The session reads only **its own** `thread_id`, so an answer can never be confused with
another question's. Replies route to whoever asked — Mike's thread → Mike's session,
yours → yours, no crossover.
- **Any non-bot reply counts as the answer** (first human wins). Anyone with #ct-forum
access can answer; the script never filters by person. Bot chatter is filtered out.
## Model & scope (decided with Mike, 2026-07-08)
- **Forum-only.** No DM replies — answers stay team-visible.
- **Good-enough durability:** answers are captured while the session is open; Discord keeps
the history, so any thread is cheap to re-read (`--read`) later in the same session. It is
NOT restart-safe (close the session → nothing is watching); that was an accepted tradeoff
for the cheapest option. Full restart/close-safe capture would be the coord-backed
registry or the event-driven bot-router — build only if asked.
- The **BEAST bot ignores #ct-forum** (patch in `projects/discord-bot/bot/main.py`) so it
doesn't auto-answer and collide with the asking session.
## Access & permissions
- **Channel:** #ct-forum (`1522960388432465950`), private — `@everyone` View DENY; Howard +
the bot explicitly allowed; Mike sees it as owner. To let someone else answer, they need
**View Channel** on #ct-forum (grant a role like Techs, or the person). Winter/Rob are not
in by default.
- **Deleting threads** needs the bot to have **Manage Threads** on #ct-forum (owner grants
it). Without it, `DELETE /channels/<thread>` returns 403.
## Exit codes
`0` answered · `1` usage · `2` no token · `3` Discord API error · `4` timeout (thread stays
open — resume with `--wait <thread_id>`).
## Implementation notes
- Bot token resolves from the SOPS vault
(`projects/discord-bot/bot-token.sops.yaml` field `credentials.bot_token`), falling back
to `projects/discord-bot/.env` `DISCORD_TOKEN` — works from any machine.
- Engine: `.claude/scripts/ask-forum.sh`. Command entry point: `/ask-forum`. Related:
[[discord-dm]] (person-targeted DMs / channel posts).