plugins/dev/skills/catalyst-events-query/SKILL.md
Reference for the natural-language query subcommand of `catalyst-events`. Translates English queries through Groq into a structured DSL, compiles to a jq predicate, and runs against the canonical event log. Also documents the `:` / `?` keys in `catalyst-hud` (TUI) that drive the same compiler. Use when an agent needs to triage events without composing jq predicates by hand, or when documenting how to surface event subsets in dashboards.
npx skillsauth add coalesce-labs/catalyst catalyst-events-queryInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
catalyst-events query "errors in the last hour"
catalyst-events query "show all gh events for ADV-292 and ADV-293 that are PR or CI" --explain
catalyst-events query "failed CI on main branch" --since 1d --limit 50
In the TUI (catalyst-hud):
| Key | Action |
|-----|--------|
| : | Open natural-language query input |
| Enter (in input) | Send to Groq, apply DSL filter to current view |
| ? | Toggle the generated DSL overlay (only shown after a query is set) |
| Esc | Cancel input or drop the active DSL filter |
catalyst-events wait-for --filter '<jq>' directly. The query subcommand pays a Groq round-trip; waiters need to be deterministic.catalyst-events tail --filter '...' is faster.The compiler accepts a strict JSON DSL. You don't normally write this by hand — Groq emits it — but --explain shows it to you, and --dsl lets you bypass Groq for tests / scripts.
type Dsl = {
filter: Node;
sort?: { field: string; order?: "asc" | "desc" } | null;
limit?: number | null;
} | { error: string };
type Node = And | Or | Not | Leaf | {};
type And = { and: Node[] };
type Or = { or: Node[] };
type Not = { not: Node };
type Leaf = { field: string } & (
{ eq: any } | { ne: any }
| { gt: any } | { gte: any } | { lt: any } | { lte: any }
| { in: any[] }
| { startsWith: string } | { endsWith: string } | { contains: string }
| { exists: boolean }
);
field MUST be a path from the canonical event schema — see [[event-schema]] for the
authoritative list. Top-level fields (ts, severityText, severityNumber, traceId,
spanId, resource."service.name", body.message) and all attributes."<key>" paths are
accepted. The validator rejects anything else with a "did you mean" suggestion.
| English | Generated DSL (abbreviated) |
|---|---|
| show all gh events for ADV-292 and ADV-293 that are PR or CI | {filter: {and: [{field: 'attributes."catalyst.worker.ticket"', in: ["ADV-292","ADV-293"]}, {or: [{field:'attributes."event.name"', startsWith: "github.pr."}, {field:'attributes."event.name"', startsWith: "github.check_"}, {field:'attributes."event.name"', startsWith: "github.workflow_run."}]}]}, sort: {field:"ts", order:"desc"}, limit: 200} |
| errors in the last hour | {filter: {and: [{field:"severityText", eq:"ERROR"}, {field:"ts", gte:"{NOW-1h}"}]}, sort: {field:"ts", order:"desc"}, limit: 200} |
| all events for orch-adv-852-2026-05-07 | {filter: {field:'attributes."catalyst.orchestrator.id"', eq:"orch-adv-852-2026-05-07"}, sort: {field:"ts", order:"asc"}, limit: 500} |
| failed CI on main branch | {filter: {and: [{field:'attributes."event.name"', startsWith:"github.check_"}, {field:'attributes."cicd.pipeline.run.conclusion"', eq:"failure"}, {field:'attributes."vcs.ref.name"', eq:"refs/heads/main"}]}, sort:{field:"ts",order:"desc"}, limit:100} |
| delete all heartbeat events | {error: "refused: query is read-only"} (refused — query is read-only by design) |
The {NOW-1h} / {TODAY} placeholders are rewritten to ISO timestamps by the caller (CLI or TUI) before evaluation, so the model can stay deterministic.
| Flag | Purpose |
|---|---|
| --explain | Print the parsed DSL + compiled jq predicate, exit without running. Useful for debugging or auditing what Groq returned. |
| --limit N | Override the limit Groq chose. |
| --since DURATION | Pre-trim by ts >= now - DURATION. Accepts Ns, Nm, Nh, Nd, or today. Relative only in v1. |
| --dsl '<json>' | Skip Groq entirely; pass a hand-written DSL. The validator and compiler still run. Used by tests and power users who don't want the round-trip. |
| Code | Meaning |
|---|---|
| 0 | Success (zero matches is success — no output, exit 0) |
| 2 | Usage error (unknown flag, missing required arg) |
| 3 | Groq HTTP error, malformed JSON, or refusal ({"error": "refused: …"}) |
| 4 | DSL validation error (unknown field, bad operator) |
The compiler error always names the bad field on stderr:
$ catalyst-events query --dsl '{"filter":{"field":"bogus.field","eq":1}}'
{"error":"unknown field: bogus.field","code":"unknown_field","field":"bogus.field"}
| Env var | Purpose |
|---|---|
| GROQ_API_KEY | Groq API key. Falls back to ~/.config/catalyst/config.json::groq.apiKey. |
| FILTER_GROQ_MODEL | Override the model. Default llama-3.1-8b-instant (sub-second, ~$0.05/M input). |
plugins/dev/scripts/catalyst-events::cmd_query. It shells to plugins/dev/scripts/lib/dsl-cli.mjs (Node), which calls Groq, parses, validates, and emits compiled artifacts on stdout. The bash wrapper then splices the jq predicate into the existing apply_filter pipeline.compile() and groqTranslate() from plugins/dev/scripts/lib/dsl-compile.mjs directly — no subprocess. The DSL produces a JS predicate function evaluated in-memory against the TUI's event backbuffer.plugins/dev/scripts/lib/dsl-prompt.mjs and the canonical field whitelist in plugins/dev/scripts/lib/dsl-fields.mjs. Both are read by tests, so prompt/validator drift is caught.refused for now; future ticket will add a {lookup, then} DSL.--follow mode — query is one-shot; use catalyst-events tail --filter '<jq>' for live tail.body.payload — the DSL is structured-attribute only.development
Migrate a single-harness repo to the dual-harness layout so both Claude Code and Codex load the same instructions and skills — AGENTS.md as the portable canonical doc, a thin CLAUDE.md `@AGENTS.md` bridge, and a `.agents/skills` dir with a `.claude/skills` symlink onto it. Use when asked to migrate to dual-harness, make this repo work in both Claude and Codex, or for agent metadata cleanup.
tools
Goal-driven senior-engineer pipeline-unstick sweep (CTL-1176 rung 3). Given the stuck/failed/needs-human set (or ONE ticket handed by the recovery router), its GOAL is to get the pipeline MOVING again — not to fix one ticket's review findings (that is phase-remediate). It runs AFTER the eyes (diagnostician evidence) and the hands (deterministic unstuck-sweep seams) have already tried, and it CONSUMES their output from a recovery-pass.json brief rather than re-diagnosing or redoing their narrow work. It acts like a senior engineer with full tool access — it resolves merge conflicts, rebases, force-pushes, merges green PRs, and re-dispatches stalled phases AUTONOMOUSLY — and escalates to the operator ONLY for a genuine value judgment / something that degrades other functionality / a real cost-benefit trade-off / a serious architecture change / an ADR conflict. On escalation it AUTHORS the operator inbox row + the push notification (executive-voiced). Dispatched as a `claude --bg` job by phase-agent-dispatch via slash command, AND invocable bare by the operator as a sweep — hence `user-invocable: true`. Ships behind CATALYST_RECOVERY_PASS (off by default — no live behavior change until shadow/enforce).
tools
Diagnose and fix Catalyst setup issues. Validates tools, database, config, OTel, direnv, and thoughts. Automatically fixes what it can — creates directories, initializes the database, sets WAL mode, runs migrations. Use for new installs, upgrades, or when something isn't working.
tools
--- name: phase-triage description: Phase agent that triages a Linear ticket — expands acronyms, classifies (feature/bug/docs/refactor/chore), identifies genuine blockers (a semantic second-pass over the backlog — NOT a prose scrape; CTL-838), estimates scope, writes triage.json, and posts a triage analysis comment to Linear. Triage completion is signaled by that comment plus the local triage.json — there is no `triaged` label. Emits phase.triage.complete.<TICKET> on success and phase.triage.fai