A collection of PostHog skills for enhancing AI-assisted workflows. Add this repo as a Claude Code plugin marketplace to get access to all PostHog skills: Then install individual plugins: Or browse available plugins: Copy any skill directory to .claude/skills/ in your project: Any directory under skills/ that contains a .claude-plugin/plugin.json is automatically discovered and added to the market
Add this skill
npx mdskills install PostHog/exploring-signals-scouts@PostHog? Sign in with GitHub to claim this listing.Comprehensive observability guide for PostHog's fleet of scheduled agents with clear workflows and tools
1---2name: exploring-signals-scouts3description: >4 How to explore and make sense of PostHog Signals scouts — the scheduled agents that scan a5 project and emit findings into the Signals inbox. Use when a user wants to understand what6 scouts they have, how each one is behaving, and whether the fleet is actually working. Covers7 surveying the fleet and its schedules, reading recent scout runs and drilling into a single8 run's reasoning, inspecting the durable scratchpad memory the fleet has built up, tracing a9 run to the findings it emitted, and assessing a scout's health and performance over time10 (cadence, success rate, emit rate, signal-to-noise). Read-only and exploratory — to write or11 tune a scout, use `authoring-signals-scouts` instead. Trigger on "what are my scouts doing",12 "how is my <x> scout performing", "show me recent scout runs", "why did this scout find/emit13 nothing", "what has the fleet learned", "explore scout run <id>", "is my scout working".14metadata:15 owner_team: signals16---1718# Exploring Signals scouts1920A **scout** is a scheduled agent that wakes on its own interval, looks at one PostHog project,21decides what's genuinely worth surfacing, and either emits it as a **finding** into the Signals22inbox or closes out empty (a real, valid outcome). PostHog ships a fleet of canonical scouts — a23cross-product generalist (`signals-scout-general`) plus per-surface specialists (error tracking,24logs, AI observability, experiments, feature flags, session replay, web analytics, surveys, and25more). A project may also have **custom scouts** beyond the canonical fleet — any26`signals-scout-*` skill a team authored (e.g. `-brand-mentions`, `-mcp-feedback`) shows up here27too, so don't assume a fixed roster: `signals-scout-config-list` is the authoritative roster for28a project. (One caveat: a just-authored scout has no config row until the coordinator's next29tick auto-registers one — or until someone registers it via the write-side30`signals-scout-config-create` — so a brand-new scout may briefly be missing from the list.)3132This skill helps you **understand and explore what a project's scouts are doing and how they're33performing** — entirely through read-only MCP tools. It is the observability counterpart to34the `authoring-signals-scouts` skill (which teaches writing and tuning) and to the35`inbox-exploration` skill (which covers the inbox reports scouts feed into).3637There are five things you can observe about the fleet, each with its own tool:3839| What you want to know | Tool | What it tells you |40| -------------------------------------------- | ---------------------------------------- | ------------------------------------------------------------------------------- |41| Which scouts run, how often, in what posture | `signals-scout-config-list` | One row per scout: schedule, `enabled`, `emit`, `last_run_at`, `description` |42| What the scouts actually did, run by run | `signals-scout-runs-list` / `-retrieve` | Per-run status, timing, end-of-run summary, `emitted_count`, deep-link |43| What the fleet has learned across runs | `signals-scout-scratchpad-search` | Durable per-team memory (baselines, noise, allowlists) |44| What the scouts actually **emitted** | `execute-sql` over `document_embeddings` | The authoritative per-finding record (weight, severity, confidence) — see below |45| What the scouts surfaced to the user | `inbox-reports-list` | Findings that cleared the bar and became inbox reports |4647The orienting sixth is `signals-scout-project-profile-get` — the deterministic snapshot of "what's48true about this project" that every scout cold-starts from. When a scout found nothing, this is49usually why.5051## Output handling: expect to offload to a file5253Two of these tools — `signals-scout-runs-list` and especially54`tasks-runs-session-logs-retrieve` — routinely return payloads that **overflow an MCP client's55token budget and get spilled to a file**. This is the normal path, not an error. Plan for it up56front rather than discovering it after a failed call:5758- **Keep `limit` small** on `signals-scout-runs-list` (~10–15). Each row carries a long prose59 `summary`, and runs come back newest-first across the _whole_ fleet, so even a modest page is60 large.61- **Session logs are large by nature.** A single run's log is hundreds of KB to a few MB. Fetch it62 with **`call --json`** (so the saved file is real JSON, not the pretty text format — `jq`-able)63 and read the saved file with `jq` / a script rather than inline.64- **Don't hand-parse the session log.** The bundled [`scripts/`](#helper-scripts) do the65 reconstruction for you — see below.6667## Start here: is the fleet even set up?6869Don't assume the project has scouts. The fleet only runs on teams enrolled via the `signals-scout`70feature flag, and a project may have no configs, all-disabled scouts, or scouts stuck in dry-run.71Run this first whenever a user asks about their scouts for the first time in a session.7273```json74signals-scout-config-list75```7677Read the result against three cases:7879The config list is unpaginated — it comes back as `{ results: [...] }` (a bare array), with no80`count` field. Read the result against three cases:8182- **Empty (`results: []`)** — no scouts are registered. The project isn't enrolled in the scout83 fleet (or hasn't ticked yet). Say so plainly; don't go fishing for runs. Point the user at the84 Signals scout settings / PostHog Code onboarding rather than inventing activity.85- **Configs exist but all `enabled: false`** — the fleet is registered but paused. Nothing is86 running. Tell the user which scouts exist and that they're all off.87- **At least one `enabled: true`** — the fleet is registered and that scout is allowed to run. For88 each enabled scout note its `run_interval_minutes` (cadence), `emit` (false = **dry-run**, runs89 but writes nothing to the inbox), and `last_run_at`. One caveat before reporting "it's live": runs90 are gated by the `signals-scout` feature flag, not by `enabled`. A project that was enrolled and91 later drained from the flag keeps its `enabled: true` rows, but the coordinator no longer plans92 runs for it — so a stale or `null` `last_run_at` on an enabled scout usually means the project is93 no longer enrolled, not that the scout is idle.9495 **`last_run_at` is a _dispatch_ stamp, not proof a run executed.** The coordinator advances it the96 moment it _enqueues_ a child workflow for a due scout — before any worker picks the run up. Child97 dispatch is fire-and-forget, so if workers are saturated or down the children just queue and no98 run ever materializes, yet `last_run_at` keeps marching forward each tick. So a recent99 `last_run_at` means "dispatched this tick," **not** "a run is genuinely happening." The100 authoritative liveness signal is the newest actual **run row** in `signals-scout-runs-list`, not101 the config stamp. Cross-check them: if `last_run_at` is fresh (minutes ago) but no run row has102 appeared for that scout in well over its `run_interval_minutes`, the fleet is **dispatching but103 not running** — workers backed up / down, or runs stranded — a real reliability problem, not a104 live scout. Don't report "it's running" off `last_run_at` alone.105106A scout that is `enabled: true` but `emit: false` is the most common source of "my scout isn't107doing anything" confusion: it _is_ running and reasoning every tick, it just isn't allowed to post108findings yet. Always surface the `emit` posture when reporting on a scout.109110See [`references/scout-data-model.md`](references/scout-data-model.md) for every field on a config,111run, and scratchpad entry, the run status values, and how the pieces link together.112113## Workflow: survey the fleet114115"What scouts do I have / what are they doing?" — lead with `config-list`, then enrich with the116most recent run per scout so the user sees liveness, not just configuration.1171181. `signals-scout-config-list` — the roster.1192. For each enabled scout, `signals-scout-runs-list` and pick the newest run with a matching120 `skill_name` (runs come back newest-first across the whole fleet, so a single call usually121 covers everyone). Report `status` and how long ago it ran.122123Present it as a table the user can scan — scout, cadence, posture, last run, last outcome — and124call out anything anomalous (never run, last run errored, stuck in dry-run for a long time).125126## Workflow: understand one scout end to end127128"How does my error-tracking scout work / how is it doing?"1291301. **Read its config** — find the row in `config-list` for `signals-scout-error-tracking`:131 schedule, posture, last run.1322. **Read its body** — `posthog:llma-skill-get {"skill_name": "signals-scout-error-tracking"}`133 returns the team's actual instruction set (which may be a canonical default or a diverged,134 hand-edited row). This is what the agent is told to do every run — its signal-vs-noise135 discriminator, explore patterns, and disqualifiers. To understand _why_ a scout behaves the136 way it does, read its body.1373. **Read its recent runs** — `runs-list` with `text` set to the skill's domain, or just scan the138 newest runs and filter to its `skill_name`. The end-of-run `summary` on each run is the scout's139 own account of what it looked at and decided.1404. **Read what it remembered** — `scratchpad-search` (see below). The memory entries a scout wrote141 reveal the baselines and noise it has internalized about this project.142143## Workflow: read recent runs144145`signals-scout-runs-list` returns the most recent runs across the whole fleet, newest first146(capped at 100). Use it to answer "what happened lately?"147148- **Scope to a window** with `date_from` / `date_to` (ISO-8601; inclusive lower, exclusive upper149 on `created_at`). Walk backwards by passing an earlier `date_to`.150- **Search summaries** with `text` — a case-insensitive substring match on each run's end-of-run151 `summary`. This is how the headless scout dedupes, and it's how you find "did any run already152 look at the checkout error spike?"153- **Filter by emit outcome** with `emitted` — `emitted=true` returns only runs that surfaced at154 least one finding, `emitted=false` only the quiet runs. This is the direct way to answer "which155 runs actually emitted something?" without parsing prose.156157Each summary row carries `run_id`, `skill_name`, `skill_version`, `status`, `started_at`,158`completed_at`, `emitted_count` (how many findings the run emitted), `emitted_finding_ids` (their159ids), `task_url` (a deep-link into the Tasks UI for the full transcript), and the `summary` prose.160Lead with the `summary` when narrating to the user — it's the scout's own plain-language close-out —161and always offer the `task_url` for the full reasoning.162163## Workflow: drill into a single run164165When the user wants the full story of one run (or pastes a run id / Tasks URL):166167```json168signals-scout-runs-retrieve169{ "id": "<uuid>" }170```171172Note the field name flip: `runs-list` returns each run's id as `run_id`, but `runs-retrieve`173takes it as `id`. Pass the `run_id` value through as `id`.174175Returns the full run: `status`, `started_at` / `completed_at` (compute duration from these),176`skill_name` / `skill_version` (what ran, at what body version), the end-of-run `summary`,177`emitted_count` / `emitted_finding_ids`, and `task_url`. The transcript — the actual tool calls and178reasoning — lives in the Tasks UI behind `task_url`, not in this payload; hand the user that link179when they want to see every step. A **failed** run returns an empty `summary` and **no error180field** — the payload looks the same as the list row, so to learn _why_ it failed you need the181transcript.182183You don't have to open the UI for that: **`tasks-runs-session-logs-retrieve` returns the run's184session log (every tool call, message, and reasoning step) as data** — handy when you're185diagnosing a failure or want to trace exactly what a run did without leaving the conversation. Pass186the run's `task_run_id` as `id` and its `task_id` (both are on the run row).187188The raw stream is large (hundreds of KB to a few MB) and will overflow inline, so **fetch it with189`call --json` and let it spill to a file**, then run it through190[`scripts/render_run_report.py`](#helper-scripts) rather than parsing it by hand.191192⚠️ **Do not reach for `exclude_types: "tool_call_update,…"` to slim it down.** It is tempting —193the stream is dominated by incremental `tool_call_update` chunks — but each tool's **actual input194lives only in those chunks**: the base `tool_call` event carries an empty `rawInput`, and the195streamed updates build the input (and the final `rawOutput`) token by token. Excluding them leaves196you with tool _names_ but no idea what the scout actually queried. Fetch the **full** log and let197the script reassemble each call (it groups by `toolCallId`, keeps the richest `rawInput`, and198attaches the completion's `rawOutput`/`status`).199200**Whether a run emitted is a first-class field: `emitted_count`.** `emitted_count > 0` means the201run surfaced that many findings; `emitted_count: 0` means it closed out empty. Don't parse the prose202`summary` for this any more — a phrase like "already emitted P1 … did not re-emit" describes a203_prior_ run, so substring-matching the summary for "emitted" is unreliable, whereas `emitted_count`204is the authoritative tally. `emitted_finding_ids` lists the `finding_id`s behind that count, in emit205order; each maps to a `Signal` with `source_id = run:<run_id>:finding:<finding_id>`, giving a206reliable run → finding link. See [`references/scout-data-model.md`](references/scout-data-model.md)207for the run-to-finding link and how an emitted finding rides through grouping into the208`source_product: "signals_scout"` inbox filter.209210A run with `status` complete and an empty-handed summary ("surface at baseline, nothing to211emit") is a **healthy** outcome, not a failure — most runs should close out empty. Treat a stream212of empty close-outs as the fleet doing its job, not as the fleet being broken.213214## Workflow: inspect what the fleet has learned215216The **scratchpad** is the fleet's durable, per-team memory — prose entries scouts write so future217runs get smarter and quieter. Reading it tells you what the fleet believes about this project.218219```json220signals-scout-scratchpad-search221{ "text": "error_tracking" }222```223224Returns entries newest-first (capped at 100); `text` matches `content` and `key`225case-insensitively. Omit `text` to browse everything. Each entry's `key` carries a category226prefix that tells you _what kind_ of learning it is:227228| Prefix | Meaning |229| ------------- | ------------------------------------------------------------------ |230| `pattern:` | A baseline — how this team's data normally shapes |231| `watch:` | A live issue being tracked but still below the emit bar |232| `noise:` | A pattern the fleet has decided to ignore (dev-only, single-user…) |233| `addressed:` | Something the team fixed or moved on from |234| `dedupe:` | A gate on re-emitting a specific issue / fingerprint / finding |235| `allowlist:` | Vetted entities never to re-surface |236| `not-in-use:` | A product/surface this team doesn't use (close-out memo) |237| `mcp-gap:` | A tooling gap a scout noticed worth raising later |238239This is the common vocabulary, not a closed set — scouts coin their own prefixes and `<domain>`240labels as needed (the live fleet uses `watch:` heavily, for example), so treat an unfamiliar241prefix as just another category. Entries cross-reference each other with `[[key]]` wikilinks. Keys242follow `<prefix>:<domain>:<entity>` (e.g. `dedupe:error_tracking:019e8375-…`).243244When a user asks "why isn't my scout flagging X anymore?", search the scratchpad for `noise:`,245`addressed:`, `dedupe:`, and `allowlist:` entries — the fleet may have deliberately learned to246suppress it. The canonical prefix vocabulary and the four-state dedupe classifier the fleet247reasons in terms of are documented in the `authoring-signals-scouts` skill248(`references/dedupe-and-memory.md`).249250## Workflow: list what scouts have actually emitted251252"What has the fleet emitted lately / show me every finding my scouts produced." The run row253carries no emit flag and no finding count, the prose `summary` is heuristic, and the inbox254filter (below) is lossy because grouping merges scout findings into mixed-source clusters. The255**authoritative** per-finding record is the emitted signal itself, in the `document_embeddings`256table — queryable for any team via `execute-sql` (the general path). When a scout emits,257`emit_signal` writes a signal with `source_product="signals_scout"`; the scout's attribution258(`skill_name`, `finding_id`, `severity`, `confidence`) lands in `metadata.extra`, with `weight`259and `source_id` at the top level.260261Fetch with `execute-sql` and format with [`scripts/emitted_signals.py`](#helper-scripts) — the262exact query lives in the script's header. One row per finding, filterable by any set of scouts:263264```bash265# call --json execute-sql { "truncate": false, "query": "<the emitted-signals query>" } -> emitted.txt266python scripts/emitted_signals.py --signals emitted.txt --now <ISO> [--skill mcp-feedback,general]267```268269A row here is **ground truth that a finding persisted** — it cleared every emit gate. The flip270side matters when explaining a gap: a scout can narrate "EMITTED ..." in its `summary` yet have271the emit **silently dropped** by a preflight gate (dry-run at the time, the org hasn't approved272AI processing, or the `signals_scout` source is disabled), or the emit failed. Those never reach273this table, so a claimed-but-absent finding is itself a diagnostic, not a script bug. The emit274contract behind each row (weight vs. confidence rubrics, severity, dedupe) is documented in the275`authoring-signals-scouts` skill (`references/emit-contract.md`); the run → finding link and its276limits are in [`references/scout-data-model.md`](references/scout-data-model.md).277278## Workflow: see what scouts have surfaced279280Scout findings reach the user as inbox reports. Filter the inbox to the scout source:281282```json283inbox-reports-list284{ "source_product": "signals_scout", "limit": 20 }285```286287This is the direct way to find scout-backed reports. Each finding is emitted with288`source_product="signals_scout"`, that tag rides through grouping into the report's signal metadata,289and the inbox filter keeps any report whose contributing signals include `signals_scout` — so the290result is the set of reports the fleet has surfaced.291292An empty result means the fleet hasn't emitted (yet), **not** that the filter is broken. Scouts hold293a high bar — most runs close out without emitting — so on a quiet or newly enrolled project zero294scout-backed reports is the normal, expected state. For the per-run view of what emitted, work from295the runs instead: `signals-scout-runs-list?emitted=true` lists every emitting run, and each run's296`emitted_count` / `emitted_finding_ids` tell you how many and which findings it produced (each297`finding_id` maps to a `Signal` with `source_id = run:<run_id>:finding:<finding_id>`). To browse the298inbox more broadly, use the `inbox-exploration` skill (statuses, suggested reviewers, drilling299into a report's underlying signals). The emit contract behind each finding — weight, confidence,300severity, the description prose — is documented in the `authoring-signals-scouts` skill301(`references/emit-contract.md`).302303## Workflow: assess health and performance304305"Is my scout actually working / earning its cost?" There's no single metric — judge a scout over a306window of runs. Pull the runs (`runs-list` with a `date_from`), then reason across the dimensions307below. The full playbook, including how to read each signal and the common failure modes, is in308[`references/assessing-performance.md`](references/assessing-performance.md).309310- **Cadence adherence** — are runs landing roughly every `run_interval_minutes`? Large gaps mean311 the coordinator is skipping it (disabled, drained from the flag, or capped out on busy ticks) —312 _or_ it's dispatching but the runs aren't materializing. Tell the two apart with `last_run_at`: if313 the config's `last_run_at` is also stale, the coordinator stopped planning it; if `last_run_at` is314 fresh but the newest run row is hours old, it's the dispatch-vs-execution divergence above (workers315 backed up / down, or runs stranded), which `runs-list` alone hides.316- **Success rate** — how many runs reach a clean `status` vs. error out? A run of errors is a317 broken scout, not a quiet one.318- **Emit rate** — what fraction of runs emitted vs. closed out empty. Read it straight off319 `emitted_count` per run (or split the window with `runs-list?emitted=true` / `?emitted=false`).320 Near-zero over a long window on a live surface can mean the discriminator is too strict (or the321 surface really is quiet); near-100% usually means it's too noisy. Most healthy scouts emit rarely.322- **Signal-to-noise** — of what it emitted, how much became actionable inbox reports vs. got323 suppressed? Use each emitting run's `emitted_finding_ids` to tie runs to their `Signal` rows, and324 cross-check against `inbox-reports-list` report states.325- **Memory growth** — a healthy scout accumulates `pattern:` / `noise:` / `dedupe:` entries over326 time. A scout with an empty scratchpad after many runs isn't learning.327328## Helper scripts329330The skill bundles four **pure formatters** under [`scripts/`](scripts/) for the most common asks.331They do **no network I/O** — they are the back half of an "agent fetches, script formats" split.332The pattern is always the same:3333341. Fetch each payload with the MCP using **`call --json`** (raw JSON, not the pretty text format)335 and save it to a file. For the big ones (`runs-list`, `tasks-runs-session-logs-retrieve`) this336 is mandatory anyway — they overflow inline and spill to a file you can point the script at.3372. Run the script over those files.338339All four are stdlib-only Python 3.11+ and print **plain text** to stdout (or `--out`) — designed340to read well in a terminal, so save them as `.txt`.341342### `scripts/render_run_report.py` — drill into one run343344Produces the kind of detailed write-up you'd want when inspecting a single run: header345(status, duration, posture), a **narrated timeline that interleaves the agent's narration with346each tool call _and its real input_**, the end-of-run summary, and any scratchpad memory.347348```bash349# fetch (note --json), saving each to a file:350# call --json signals-scout-runs-retrieve { "id": "<run_id>" } -> run.json351# call --json tasks-runs-session-logs-retrieve { "id": "<task_run_id>", "task_id": "<task_id>", "offset": 0 } -> log.json (FULL — no exclude_types)352# (optional) call --json signals-scout-scratchpad-search { ... } -> mem.json353# (optional) call --json signals-scout-config-list {} -> cfg.json354python scripts/render_run_report.py --run run.json --log log.json \355 --scratchpad mem.json --config cfg.json --out report.txt356```357358Modes (`--mode`, default `detailed`):359360| Mode | Contains | `--log` needed? |361| ---------- | ------------------------------------------------------------------ | --------------- |362| `summary` | header + posture + close-out prose | no |363| `detailed` | + narrated timeline with tool **inputs** + tool tally + scratchpad | yes |364| `full` | + each tool call's (truncated) **output** inline | yes |365366Other flags: `--show-output` (outputs in detailed mode), `--input-width` / `--output-width`367(truncation), `--no-art` (skip the hedgehog banner), `--base-url` (defaults to `us.posthog.com`).368369### `scripts/fleet_survey.py` — survey the whole fleet370371One scannable table — scout, enabled, posture, cadence, last run, last outcome — with a "worth a372look" section that flags never-run, stuck-in-dry-run, and last-run-failed scouts.373374```bash375# call --json signals-scout-config-list {} -> cfg.json376# (optional) call --json signals-scout-runs-list { "limit": 30 } -> runs.json (small limit!)377python scripts/fleet_survey.py --config cfg.json --runs runs.json --now <current-ISO-time>378```379380Pass `--now` (the current time, ISO-8601) to get relative "ago" columns; the emit/quiet column is381a **heuristic** on each run's summary prose — confirm against the summary before trusting it.382383### `scripts/assess_health.py` — health over a window of runs384385Implements the "assess health and performance" workflow above: a per-scout table (runs, success386%, emit %, cadence gap vs interval, adherence, median duration, memory growth) plus a "worth a387look" section flagging all-failed scouts, timeout-shaped failures, cadence stalls, staleness, and388empty scratchpads.389390```bash391# call --json signals-scout-runs-list { "limit": 100, "date_from": "<ISO>" } -> runs.json392# (optional) call --json signals-scout-config-list {} -> cfg.json393# (optional) call --json signals-scout-scratchpad-search {} -> mem.json394python scripts/assess_health.py --runs runs.json --config cfg.json \395 --scratchpad mem.json --now <current-ISO-time> [--skill signals-scout-general]396```397398`--config` is what lets it score cadence adherence (the expected interval) and staleness (the399authoritative `last_run_at`, which the windowed runs can miss when the 100-row cap truncates the400newest runs). Without `--scratchpad` the memory column shows `n/a` and no memory flags fire. The401emit % is the same summary-prose heuristic — cross-check signal-to-noise against402`inbox-reports-list`.403404### `scripts/emitted_signals.py` — every finding the fleet actually emitted405406Implements the "list what scouts have actually emitted" workflow: the authoritative per-finding407table (when, scout, severity, weight, confidence, `finding_id`, one-line hypothesis) plus a408per-scout rollup (emit count, severity mix, weight range, latest emit). Unlike `assess_health`'s409emit **%** — a prose heuristic — this reads the emitted signals directly, so it's exact.410411Its input is **not** a `signals-scout-*` tool; it's an `execute-sql` result over412`document_embeddings` (the general, any-team path). The full query lives in the script's header —413copy it verbatim. `execute-sql` returns a pipe-delimited text table (even under `call --json` it's414that text wrapped in a JSON string), so the script parses that text; the query deliberately selects415only pipe-safe scalar columns (the multi-line `description` is excluded, `hypothesis` is sanitized).416417```bash418# call --json execute-sql { "truncate": false, "query": "<emitted-signals query from the header>" } -> emitted.txt419python scripts/emitted_signals.py --signals emitted.txt --now <current-ISO-time> \420 [--skill mcp-feedback,general] [--severity P0,P1,P2] [--since <ISO>] [--sort weight] [--wide]421```422423`--skill` takes a comma-separated set (the `signals-scout-` prefix is optional). `--wide` adds the424`scout_run_id` so you can chain straight into `render_run_report.py` for the run that emitted a425finding. Remember the coverage caveat: this lists signals that **persisted** — a finding a run426summary claims but that's absent here was gated (dry-run / AI processing not approved / source427disabled) or failed.428429## Tips430431- **Always surface the `emit` posture.** "Running but in dry-run" is the single most common reason432 a user thinks a scout is broken when it isn't.433- **An empty close-out is success.** Most runs should find nothing. Don't report a wall of clean,434 empty runs as a problem.435- **Emit-vs-quiet is a first-class run field.** Filter runs directly with `runs-list?emitted=true`436 (or read `emitted_count` per run) to find what emitted, without parsing the prose `summary`. The437 `source_product: "signals_scout"` inbox filter lists the _reports_ the fleet surfaced; an empty438 result there means it hasn't emitted yet (scouts hold a high bar), not that the filter is broken.439- **A ~30-min run that `failed` is usually a timeout, not a broken scout.** Completed runs finish440 in a couple of minutes. Most often the scout over-investigated and ran the full budget (the fleet441 self-corrects by writing "tight-run recipe" scratchpad entries) — but some are false timeouts442 where the scout actually finished in a few minutes and the run then hung on a dropped close-out.443 The session log (above) tells them apart: real over-investigation shows tool calls right up to the444 wall; a false timeout goes silent long before it. Don't assume over-investigation from duration445 alone.446- **Lead with the run `summary`**, then offer `task_url` for the full transcript — don't dump raw447 run rows at the user.448- **`last_run_at: null`** means a scout has never fired — check it's enabled and the project is449 enrolled before digging further.450- **To explain a quiet scout, read the project profile.** `signals-scout-project-profile-get`451 shows whether the surface it watches is even in use — a logs scout on a project with no logs has452 nothing to do.453- **This skill is read-only.** To change a scout's schedule, posture, or body, hand off to454 the `authoring-signals-scouts` skill — it covers `signals-scout-config-update` and the455 skills-store edit path.456
Full transparency — inspect the skill content before installing.