File an Insight defect as a GitHub issue in constructorfabric/insight — triage against existing issues, gather evidence, collect what the reproduction produced, draft a report that reads in under a minute, confirm, create, and verify the metadata landed. Prefer this over the general `file-bug` skill for anything in the Insight product — dashboards, metrics, connectors, dbt, ClickHouse, identity, the Helm install — since it carries the medallion evidence walk, the reproduction-data discipline and the live board IDs; the general skill is the router, and it owns a Constructor *platform* defect that belongs in YouTrack. It reports OBSERVATIONS ONLY — no investigation, no root-cause analysis, no naming the file or layer to fix; that is the assignee's job. Use whenever the user asks to file/report/raise/log a bug, ticket, defect or issue, and trigger PROACTIVELY once an investigation has converged on 'this is broken and should be recorded' — don't wait for the words 'file a bug'. Also fires on 'log this', 'report it', 'this is broken, make a ticket', 'turn this into an issue', 'we should file two bugs for X and Y'. The repo is PUBLIC, so the default flow is draft → confirm → create and the body must be scrubbed of internal detail.
Turn an observed defect into an issue someone else can act on in under a minute, carrying the data the reproduction produced.
Report what you saw, not why it happens. Reproduce, collect, attach. Diagnosis — which file, which layer, which expression — belongs to whoever picks the issue up: they have the context to be right, and a confident wrong cause in the body sends them down it before they start. An observation you can defend beats an explanation you cannot.
The tracker is always constructorfabric/insight on GitHub — inside this repo there is no routing decision to make. (A Constructor platform bug — APS, Learn, Proctor, a platform stand's auth or navigation — goes to YouTrack instead; that routing lives in the general file-bug skill, not here.)
The repo is public. PRs close issues with Closes #N, so an issue here is outward-facing. Two consequences: draft → confirm → create is the default flow, and the body gets scrubbed before it goes anywhere.
A finding may arrive from probe-merged-change already carrying a verdict, an existing_issue and a layer. Those fields are self-describing, and a defect you found yourself has to clear the same four gates anyway.
verdict must be CONFIRMED. An UNVERIFIED finding is a hypothesis, and filing one spends a reader's attention on a maybe. Reproduce it yourself first.existing_issue must have been searched. If it names an issue, comment on that issue instead of filing. If the match is closed, say so in the comment — a regression is more urgent than a new bug.layer: stand is not a product bug. A join_use_nulls view mismatch, a stale schema_status cache, an unseeded connector, a tenant mismatch — these are environment faults, and the reproduction shows it: the same check passes on a correctly populated instance. Record that and stop.layer is not a blocker. You do not have to know where the fix lands in order to file. Attach what you observed at each layer you could reach and leave the conclusion open.Each of these owns a slice of the work.
| Skill | Owns | Reach for it when |
|---|---|---|
playwright-cli | the browser command surface — snapshots, refs, clicks, screenshots, console, network | exploring a stand or reproducing any UI defect |
drive-ui | getting an authenticated browser on any stand — the Keycloak realm login and the DEV_USER_EMAIL seed locally, a passkey attach on a remote one — plus the routes and the evidence set | any UI defect, local or remote |
insight-stand | the compose test stand, and whether a failure is the stand or the product | settling "product bug, or empty instance?" |
probe-merged-change | the exploratory pass over a change that just merged | findings arrive here from that pass, already reproduced |
One collection step belongs here rather than in drive-ui: whenever a value on screen looks wrong, capture the browser console and the API response behind it (playwright-cli console, then requests and request <n>) and attach both. Whether the wrong number arrived from the API or was rendered wrong is the single most useful fact in the report — and it is an observation, not a diagnosis, as long as you paste what the response actually contained.
Three checks that routinely change the plan.
Search first. Never file a blind duplicate:
gh issue list --repo constructorfabric/insight --state open --search "<key phrase>" --limit 50
gh issue list --repo constructorfabric/insight --state closed --search "<key phrase>" --limit 50Search the two states separately. In a combined --state all search, closed issues rank after every open one, so they fall off the end of the window: "threshold" returns 30 open and 0 closed at --limit 30, and raising the limit only postpones the problem — enough open matches still crowd them out. Two queries guarantee you see both. A closed match is the more urgent finding, since it means a regression.
Search more than once with different vocabulary — the metric key, the field name, the group title, the error code, the user-visible label. Same defect → add your evidence to the existing issue. A genuinely different symptom → file new and cross-link with a one-line related to #N (a bare link, not a "how this differs" writeup — that reads as noise).
Product bug, or environment artifact? A metric that is empty because nothing was seeded or synced is not a product defect. File only what would still be wrong on a correctly populated instance. The cheapest check is the bottom of the medallion: no bronze rows for that connector and window means a seed or sync gap, so stop.
One bug or several? One issue per distinct reproduction. Two symptoms that need different steps to trigger are two issues; the same symptom reached by two paths is one issue with both paths in Steps. Where you cannot tell, file one and say what else you saw — merging beats splitting a single defect across two threads.
Collect first, write second. The evidence must let someone else reproduce this.
Two facts belong in every report, whatever the bug's kind. The version you reproduced on, and how many attempts out of how many reproduced it.
On a Kubernetes stand the release is the umbrella insight chart, so helm list -n <namespace> prints both the chart version and the appVersion — record both. A chart version can pin an appVersion older than the code you believe you are testing, and that gap is how a fix gets declared verified against a build that never contained it. On the compose stack the version is the image tag the containers are running. Then run the reproduction again and count: 10/10 is deterministic, 2/10 is intermittent, and the two are triaged differently and proven fixed differently. Report the count you actually ran — one successful attempt is 1/1, never 10/10.
Artifacts do not go in this repo. Nothing in this tree is gitignored for scratch output — scratch/, tmp/, artifacts/ are merely untracked, so a screenshot or a body file left behind surfaces in someone's git status and rides along on the next git add -A. Write evidence and the issue body to the session scratchpad directory your environment names, or to a fresh mktemp -d; that is what the --body-file path below assumes. (../insight-workspace/scratch/ also works when that checkout sits alongside this one.)
CH=(docker exec insight-clickhouse clickhouse-client -u insight --password "${CLICKHOUSE_PASSWORD:-insight-local}")
"${CH[@]}" -q "SELECT … FROM insight.<gold> WHERE …" # gold — served
"${CH[@]}" -q "SELECT … FROM silver.class_<domain>_<entity> WHERE …" # silver — dedup / identity
"${CH[@]}" -q "SELECT … FROM bronze_<connector>.<table> WHERE …" # bronze — raw ingestinsight, so an unqualified class_* resolves to the wrong database. The password is required — compose sets CLICKHOUSE_PASSWORD with insight-local as the default.
For a remote stand: ../insight-workspace/scripts/ch.sh query --target <target> "<sql>" (ch.sh lists its targets). Those three queries are the three-layer walk.drive-ui owns the stand and the browser; playwright-cli owns the commands), then lead with a tight annotated shot of the broken widget plus a contrast shot of something that renders correctly. The stand URL belongs in your commands, never in the issue.src/ingestion/ and the definition registry in src/backend/services/analytics/. Read the intent before calling behaviour wrong.Volume and destructive proof belongs on a stand you can throw away. A cap, a flood, a rate limit, a migration against a warm database — each needs writes nobody else is reading. Stand one up rather than reaching for a shared instance, and expect a mass write to a shared stand to be refused outright.
Split deliberately when a finding has two halves. Prove the volume behaviour where you can write freely, and confirm the half that needs no volume — a response carrying no paging field, a parameter being ignored — where the change is actually deployed. Say in the report which half came from where, naming the state and never the environment.
Bug — the native type (--type "Bug"), never a bug label.priority:*. Options: Blocker (blocks the next installable release), High (meaningful demo features), Medium (default). Suggest a level and confirm it.## Summary
<ONE sentence: what is broken in product terms, and its consequence. Nothing else — no repro
detail, no history, no scope. A reader triaging a list often reads only this line.>
**Version:** <what the instance reports, e.g. `0.5.289` (appVersion `0.5.261`) — or `unknown` + what you checked>
**Reproduction rate:** <n/m, e.g. `10/10`>
**State:** <the state a reader must recreate, e.g. "a freshly migrated database". Never a hostname.>
## Steps to Reproduce
1. <UI path, or the fastest isolated check — one query or command>
2. <what to observe>
3. <what the call or query returned — the status, the visible result. The verbatim error text goes
under Additional information, so this stays a list of actions and outcomes.>
**Expected:** <one line>
**Actual:** <one line — the failure at that step, NOT a restatement of Summary>
<A runnable proof, or a matched comparison with one variable changed. If a field being *absent*
(bug) versus *present-but-null* (no data) is the distinguishing signal, say so — that one line
stops a reviewer waving off a real defect as missing data.>
## Additional information
<The data the reproduction produced, and nothing you inferred from it. Whatever you ran and what
it returned: counts at each layer, the API status and response body, the log or dbt error, the
same check on a state where it works, whether this used to work. Label each one with what
produced it. If the rate is below n/n, what differed between the attempts that reproduced and the
ones that did not. If a value looks wrong, give the value you saw and the value the spec or the UI
led you to expect — not a theory about where it went wrong.>
## Notes ← optional, one line (e.g. `related to #N`)No ## Root Cause heading. It used to be in this template, and removing it is the point of the observations-only rule: a cause written by someone who reproduced the bug but did not write the code reads as authoritative, and the assignee spends their first hour ruling it out. What you observed goes under Additional information; what it means is theirs to decide.
No ## Impact heading. It restates the Summary in longer words. A knock-on effect is one line in Notes. Wanting the heading back means the Summary sentence is not carrying its weight.
No ## Environment heading either — the three labelled lines under Summary are the whole of what the report says about where it ran. They are facts a reader acts on: which build to check out, whether to expect a race, what state to recreate. A prose section around them turns those three lines back into paragraphs that restate the Summary.
Additive when it sharpens the report: an Examples table (observed → expected) for a rule, threshold, sign or mapping bug. No fix checklist and no code links — a list of sites to change is a diagnosis, and naming one wrong is worse than naming none.
One idea per sentence. Short declarative lines a tired on-call reader parses on the first pass. If a sentence has more than one comma-joined clause plus a dash-aside, split it. State what happens, then why.
The second version is also the shape this skill asks for: four things observed, no claim about which side owns the fix.
Use words the reader already has. The issue is read by whoever is on triage, not only by someone who has just been in that code. Two habits lose that reader:
Keep the product's own vocabulary where you observed it — table, view and column names inside an error, the API route you called, the metric key on the tile, the medallion layer names. That is what makes the report greppable. The rule is about phrases you invented, not terms the system printed.
POST /api/v1/applications/token returns 404."The check that catches this: read the Summary as if you had not spent the last hour in the system. If any phrase you invented would send that reader to ask "what is that?", replace it — terms the system printed stay.
Say each fact once. Every fact lives in exactly one section. Repetition teaches the reader to skim, and skimming is how the one load-bearing line gets missed.
Title = the plain, user-visible symptom. No metric IDs, table or column symbols, or migration names — those live in the body. Don't append the diagnosis as a trailing clause, don't reach for filler adverbs, and don't use a qualifier the reader can't resolve from the title alone ("after a database migration" — which one?).
outright; "fails" already says it.No prescribed fix and no acceptance criteria — that is the assignee's call. Describe an expected result in plain language and, where a prototype exists, point at it as the source of truth; don't specify exact colours or pixel values.
A real filed bug, condensed, with the fact block filled in to show the shape. Read it for calibration on how little text a complete report needs.
Adding a threshold to a metric makes its threshold list fail permanently
Summary
Once a metric has its first threshold, every read of that metric's thresholds fails, so thresholds can no longer be viewed, edited or removed.
Version: 0.5.289 (appVersion 0.5.261) Reproduction rate: 5/5 State: a freshly migrated database with no other data.
Steps to Reproduce
- Create a metric, then
POST /v1/metrics/{id}/thresholdswith any valid body.- Read them back:
GET /v1/metrics/{id}/thresholds.- Both calls return 500
application/problem+json(server log below).Expected: 201 with the created threshold, then 200 with the list. Actual: 500 on the create and on every later read of that metric's thresholds.
Additional information
- Every later read of that metric's thresholds returns the same 500: the create's read-back, the list, an update and a delete.
- The row is in the table —
SELECT field_name, operator, value FROM thresholdsreturns it withvalue = 1.000000.- The admin threshold endpoints (
metric_threshold) accept and return the same shape on the same instance, with no error.- Server log, verbatim, at the moment of the failed read:
failed to list thresholds error=Query Error: error occurred while decoding column "value": mismatched types; Rust type `core::option::Option<f64>` (as SQL type `DOUBLE`) is not compatible with SQL type `DECIMAL`Notes
Found by the endpoint contract suite; the affected tests are currently skipped against this issue.
Three things that example gets right, and they are the ones reports usually miss. The title is a symptom a user could have reported. The "row is in the table" line is load-bearing — without it a triager reads a 500 as a flaky write and moves on. And every line under Additional information is something that was run and observed: the decode error is pasted, not paraphrased, and the working admin endpoint is offered as a contrast the assignee can use — not as a theory about what differs.
Keep out: internal hostnames of any kind, the phrase "dev stand", cluster and kube context names, workspace paths (wiki/…, scratch/…), JWTs, tokens, credentials, and exact data values tied to a real person (genericize 14,753 → "~14.7k"; use jane.doe@corp.com).
Keep in: the repo's own code references — file paths, view, table and column names, API routes. Those are the product and are what make the bug actionable.
Show the title, the type, the priority you propose and the rendered body, then wait — unless the user said "just create it". Creation is not a draft: the repo is public and watchers are notified the moment the issue exists, so a wrong title or an unscrubbed line is already out. This is also where the priority gets settled, since it is your suggestion until the user picks one.
Write the scrubbed body to a file outside this repo — never inline a multi-line body.
BODY="$(mktemp -d)/bug-body.md" # or a path under the session scratchpad dir
# 1. Create — native Type=Bug, NO labels (grooming applies those), NO bug label
gh issue create --repo constructorfabric/insight \
--type "Bug" --title "<title>" \
--body-file "$BODY"
# 2. Add to the Insight board — idempotent; auto-add is unreliable
gh project item-add 40 --owner constructorfabric --url <issue-url>
# 3. Set the Priority FIELD. Parse with jq, NOT python — issue bodies carry control chars
ITEM=$(gh project item-list 40 --owner constructorfabric --limit 800 --format json \
| jq -r --argjson n <ISSUE_NUMBER> '.items[] | select((.content.number // -1)==$n) | .id')
gh project item-edit --project-id PVT_kwDOERGOus4Ba9e9 --id "$ITEM" \
--field-id PVTSSF_lADOERGOus4Ba9e9zhVxXAs \
--single-select-option-id <Blocker=79628723 | High=0a877460 | Medium=da944a9c>Verify those IDs with gh project field-list 40 --owner constructorfabric if an edit fails.
Images. There is no documented API, but the web UI's upload endpoint accepts a plain gh auth token (verified 2026-08). Upload before creating the issue and embed the returned URL in the body, so the asset attaches with the initial render:
# Upload one PNG; prints the asset URL to embed. The token rides stdin (-H @-),
# never curl's argv, so it can't show up in a process listing.
REPO=constructorfabric/insight # the repo the issue will live in — swap when reusing elsewhere
REPO_ID=$(gh api "repos/$REPO" --jq .id)
ASSET=$(gh auth token | sed 's/^/Authorization: Bearer /' \
| curl -sf --connect-timeout 5 --max-time 120 -X POST -H @- -H "Accept: application/json" \
--data-binary "@<shot>.png" \
"https://uploads.github.com/user-attachments/assets?name=<shot>.png&content_type=image/png&repository_id=$REPO_ID" \
| jq -er '.url // empty')
[ -n "$ASSET" ] || echo "upload failed — use the manual fallback" >&2
# → https://github.com/user-attachments/assets/<uuid>; write 
# into $BODY where the evidence belongs — the embed doesn't happen by itselfThree caveats. The endpoint is undocumented — if the POST fails (empty $ASSET), fall back to the old flow: create the issue, then tell the user to drag the PNGs into the description box (don't imply they attached automatically). Always pass the target repo's repository_id — the POST 404s without it, and asset visibility is scoped to that repo. And the screenshot is public the moment the issue is: scrub it like the body (no internal hostnames in the URL bar, no tokens in a visible console). Unattached uploads 404 anonymously until the issue references them — that's normal, not a failure. For a data or pipeline bug the inline query proof is usually the evidence and no screenshot is needed.
gh issue view <n> --repo constructorfabric/insight --json title,labels,body,url
gh api repos/constructorfabric/insight/issues/<n> --jq '.type.name' # → "Bug"Confirm: Type is Bug; no bug or priority: label; the body renders and is grep-clean of internal detail. The item-add and item-edit calls above already report whether the board and Priority field took, so don't re-read them. Report the URL with a one-line summary.
Don't self-assign, and don't post a status comment unless asked. On this board, moving an issue is a separate decision — To Verify means development is done and awaiting validation, Done means QA verified it, and publishing that claim is the user's call.
c31d302
If you maintain this skill, you can claim it as your own. Once claimed, you can manage eval scenarios, bundle related skills, attach documentation or rules, and ensure cross-agent compatibility.