From 9340cdec1704c7fc8bd800b6a5f64fe052152c44 Mon Sep 17 00:00:00 2001 From: Howard Enos Date: Tue, 7 Jul 2026 20:12:18 -0700 Subject: [PATCH] ask-forum: promote to a full skill + wire into skill-first routing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- .claude/CLAUDE.md | 4 +- .claude/SKILL_ROUTING.md | 1 + .claude/commands/ask-forum.md | 3 +- .claude/memory/MEMORY.md | 2 + .../feedback_background_task_no_ampersand.md | 26 ++++++ .claude/skills/ask-forum/SKILL.md | 92 +++++++++++++++++++ 6 files changed, 126 insertions(+), 2 deletions(-) create mode 100644 .claude/memory/feedback_background_task_no_ampersand.md create mode 100644 .claude/skills/ask-forum/SKILL.md diff --git a/.claude/CLAUDE.md b/.claude/CLAUDE.md index 230df219..91ea0908 100644 --- a/.claude/CLAUDE.md +++ b/.claude/CLAUDE.md @@ -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 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 - 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 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`). diff --git a/.claude/SKILL_ROUTING.md b/.claude/SKILL_ROUTING.md index 8c36d367..6a46be6f 100644 --- a/.claude/SKILL_ROUTING.md +++ b/.claude/SKILL_ROUTING.md @@ -34,6 +34,7 @@ | UniFi WiFi tuning / RF / channel analysis | `unifi-wifi` | | 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` | +| 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` | | Git / Gitea / submodules / sync health | `gitea` (or `/sync`, `/scc`, `/save` for session ops) | | Build/verify GuruRMM agent/server/dashboard, pre-merge check | `gururmm-build` | diff --git a/.claude/commands/ask-forum.md b/.claude/commands/ask-forum.md index a9ee8429..19f8f417 100644 --- a/.claude/commands/ask-forum.md +++ b/.claude/commands/ask-forum.md @@ -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 -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 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. diff --git a/.claude/memory/MEMORY.md b/.claude/memory/MEMORY.md index 3b86d402..c8c4f5bc 100644 --- a/.claude/memory/MEMORY.md +++ b/.claude/memory/MEMORY.md @@ -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. - [M365 app: SharePoint needs CERT](reference_m365_app_sharepoint_rest_vs_graph.md) — SP app-only works via the CERT (get-token.sh 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 +- [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 diff --git a/.claude/memory/feedback_background_task_no_ampersand.md b/.claude/memory/feedback_background_task_no_ampersand.md new file mode 100644 index 00000000..35c440d1 --- /dev/null +++ b/.claude/memory/feedback_background_task_no_ampersand.md @@ -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 --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]]. diff --git a/.claude/skills/ask-forum/SKILL.md b/.claude/skills/ask-forum/SKILL.md new file mode 100644 index 00000000..b03b4a4f --- /dev/null +++ b/.claude/skills/ask-forum/SKILL.md @@ -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 , 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 + +# Resume blocking on an already-posted thread (e.g. after a timeout): +bash .claude/scripts/ask-forum.sh --wait --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 @` +(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//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/` returns 403. + +## Exit codes +`0` answered · `1` usage · `2` no token · `3` Discord API error · `4` timeout (thread stays +open — resume with `--wait `). + +## 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).