| name | sar-triage |
| description | Visually review SAR drone-image tiles for signs of a subject of interest. Use when the user asks to "triage", "search", "scan", "analyze", or "review" prepared SAR tiles — e.g. "start the search on incident013", "keep triaging", "scan the tiles". Loads the incident profile (subject, terrain, region, cues), renders a mission-specific triage prompt, batches ~6 keep-tiles at a time, shows each tile to you for profile-aware visual analysis, and appends one JSON record per tile to findings.jsonl. Resumable — safe to interrupt and re-invoke; previously-reviewed tiles are skipped automatically. |
sar-triage
Perform the Stage-2 multimodal review of SAR drone tiles for a given incident,
using the incident-specific visual-cue profile.
Inputs
- Incident id — e.g.
incident013. Used to locate the manifest and the incident profile.
Derived paths (project-relative; <project_root> resolves from $CLAUDE_PROJECT_DIR when set, otherwise the project root of this checkout):
- Manifest:
<project_root>/runs/<incident_id>/manifest.json
- Profile:
<project_root>/assets/<incident_id>/incident.json (override with --profile PATH on the render step)
If the manifest doesn't exist, run sar-ingest first. If the profile is missing, the skill falls back to a generic profile — but you should author one (see the _shared/incident_profile.py schema) so the triage prompt is tuned to the mission.
Context management — read this before starting
Each batch pulls ~6 tile images into the reviewing agent's context. A 500-tile incident is ~90 batches; looping that inline in a single session will eventually fill context and force a hard stop mid-batch.
Before Step 0, check remaining work:
python3 .claude/skills/sar-triage/scripts/next_batch.py \
--manifest runs/<INCIDENT_ID>/manifest.json --batch-size 6 \
| jq '.remaining_total // 0'
Pick the loop pattern based on the result:
- ≤ 15 batches (~90 tiles) remaining: run the loop inline in this session (Pattern B below).
- > 15 batches remaining: delegate chunks to fresh sub-agents (Pattern A below). Each sub-agent has its own context; their tile images don't accumulate in yours.
findings.jsonl is the hand-off medium.
Either way, findings.jsonl is the source of truth — if any loop iteration dies or is interrupted, re-firing this skill resumes exactly where it stopped, because next_batch.py dedups against it.
Pattern A — sub-agent chunking (preferred for large incidents)
You (the top-level agent) delegate, never view tiles yourself:
-
Spawn a sub-agent with a self-contained prompt along the lines of:
"Run sar-triage on incident <INCIDENT_ID> for AT MOST 10 batches. Read .claude/skills/sar-triage/SKILL.md for the loop (steps 1–4 under "What to do"). Use Pattern B. Stop either after 10 batches OR when next_batch.py returns batch_id: null. Report back with: batches completed, total lines in findings.jsonl, any high_confidence hits by frame/GPS."
-
While sub-agents run, you can call sar-evaluate to monitor progress without spending your own context on tiles — it's read-only and safe during active triage.
-
When the sub-agent returns, check remaining_total again. Spawn the next sub-agent if > 0. Stop when the queue drains.
-
Sub-agents can run in parallel if you want to speed things up — append_findings.py flocks findings.jsonl, and next_batch.py gives each sub-agent a distinct slice because the already-appended findings from a concurrent sub-agent are reflected on the next next_batch.py call.
Pattern B — inline loop (small incidents, or you ARE the sub-agent)
Run the steps below directly in this session. Stop at a batch boundary when either:
next_batch.py returns batch_id: null (fully done), OR
- you've hit your allotted batch count (if delegated), OR
- context pressure is obvious (responses getting truncated, reminders about context usage).
Never leave a batch half-written. If /tmp/sar_verdicts.json exists but append_findings.py hasn't run, either finish that append or discard the verdicts before reporting "paused" — findings.jsonl must remain consistent.
What to do
Paths below are project-relative; run from the project root (or prefix with ${CLAUDE_PROJECT_DIR}/).
Step 0 — Render the incident-specific prompt
Once per session, render the triage prompt from the profile:
python3 .claude/skills/sar-triage/scripts/render_prompt.py \
--incident-id <INCIDENT_ID> \
> /tmp/sar_triage_prompt.md
Read /tmp/sar_triage_prompt.md — these are the criteria you will apply to every tile. The cue vocabulary, subject description, terrain context, and calibration notes all come from the incident profile.
Loop (repeat until no tiles remain)
-
Fetch the next batch of unreviewed keep-tiles:
python3 .claude/skills/sar-triage/scripts/next_batch.py \
--manifest runs/<INCIDENT_ID>/manifest.json \
--batch-size 6 \
> /tmp/sar_batch.json
Inspect /tmp/sar_batch.json. If batch_id is null, triage is complete — go to step 5.
-
View each tile. For every tile entry in the batch, use the Read tool on its tile_path. You see the tiles as images. Apply the criteria from the rendered prompt.
-
Emit verdicts. Produce a JSON array with one object per tile, in the order the batch listed them, using the schema at the bottom of the rendered prompt. Use the exact tile_name string from the batch. Write the array to /tmp/sar_verdicts.json.
-
Persist findings. Atomically append to findings.jsonl:
cat /tmp/sar_verdicts.json | \
python3 .claude/skills/sar-triage/scripts/append_findings.py \
--manifest runs/<INCIDENT_ID>/manifest.json \
--batch-file /tmp/sar_batch.json \
--model claude-opus-4-7
Check: appended should equal the batch size; unmatched_tile_names should be []. If a tile_name is unmatched, your JSON used a different string than the batch provided — fix and retry the batch.
-
Done or paused. Report back — same format whether the queue drained or you're handing off mid-incident:
- Batches completed this session.
- Total lines in
findings.jsonl (cumulative across all sessions — this is progress).
remaining_total from the last next_batch.py call (0 = fully done).
- Count by verdict tier (
clear, possible, probable, high_confidence).
- Any
high_confidence or decisive-cued hits surfaced immediately, by frame + GPS.
- Next step:
- If
remaining_total == 0: invoke sar-report to generate the ground-team report.
- Otherwise: tell the user to re-fire (or, if you're the parent coordinating sub-agents, spawn the next one). Saying is enough — the skill auto-resumes from .
Calibration reminders
- Recall > precision. The prompt emphasizes this; apply it.
- Follow the profile's cue weighting.
decisive cues (typically blaze orange or similar) warrant high confidence on their own; strong cues need one clear instance; medium/weak cues compound.
- Profile-declared modality warnings (e.g. ghillie suit defeats RGB) appear in the rendered prompt — keep them in mind when calibrating.
Resumability (reference)
findings.jsonl is the source of truth. Safe to interrupt at any batch boundary; on re-invocation, next_batch.py skips any (frame_id, tile_index) already present and returns only unprocessed tiles.
- Tiles previously marked
stage1.verdict == "skip" by sar-ingest are never sent to review.
append_findings.py uses flock, so parallel sub-agents (Pattern A) can append concurrently without corrupting the log.
- See "Context management" above for the primary use: handing off between sessions or sub-agents on large incidents.
Output schema (one record per tile in findings.jsonl)
frame_id e.g. "DJI_0401"
tile_index 0..11
tile_path absolute path
gps {lat, lon, alt_m, timestamp_utc}
crop_box {x, y, w, h} in parent-frame pixels
verdict "clear" | "possible" | "probable" | "high_confidence"
confidence float [0, 1]
cues controlled-vocabulary strings (per the profile's visual_cues)
pixel_location_tile {x, y, w, h} in tile-local coords (or null)
pixel_location_frame {x, y, w, h} in parent-frame coords (auto-computed)
description model's one-sentence observation
alternative_explanation non-human explanation considered
raw_response verbatim model output object
reviewed_utc, model, batch_id