## Output Format — SkillObservation

You MUST respond with ONLY a JSON array of observations. No preamble, no markdown fences, no explanation outside the JSON. Single-line JSON preferred; pretty-printed JSON tolerated.

Each observation has this shape:

```
{
  "concern": "correctness" | "architecture" | "performance" | "security" | "legal" | "compliance" | "risk",
  "description": "Terse headline — what you observed",
  "suggestion": "What should be done about it (one actionable line)",
  "blocking": true/false,
  "severity": "high" | "medium" | "low" | "info",
  "confidence": 0-100,
  "rationale": "Why you believe this (short, optional)",
  "details": "Long-form prose body — only rendered in Markdown / SARIF properties (optional)",
  "effort": "small" | "medium" | "large" (optional),
  "file": "src/path/Foo.cs (optional, for source-evident findings)",
  "start_line": 42 (optional),
  "end_line": 48 (optional),
  "api_path": "GET /api/users (optional, for endpoint-level findings)",
  "schema_name": "OktaProcessInfoResponse (optional, for schema-level findings)",
  "evidence_mode": "potential" | "confirmed" | "analyzed_from_source" (optional, defaults to potential),
  "category": "secrets" | "injection" | "auth" | "headers" | "dependencies" | ... (optional, free-form domain tag),
  "review_status": "not_reviewed" | "confirmed" | "false_positive" (optional, defaults to not_reviewed)
}
```

**Field length limits — enforced at parse time:**

| Field | Max chars | Truncation |
|---|---|---|
| `description` | 500 | Yes — truncated with `…[truncated, original was N chars]` marker |
| `suggestion` | 300 | Yes |
| `rationale` | 500 | Yes |
| `details` | 4000 | Yes |

Over-cap fields are truncated, not rejected — the observation always survives, but skill authors will see a `Skill X: 'description' truncated from N to 500 chars` warning. Fix at source: use Description for the headline, Details for long-form context.

**Channel-aware output:**

| Channel | What it renders |
|---|---|
| Console (terse triage line) | `description` only (first 80 chars) |
| Summary (severity-grouped list) | `description` only |
| Markdown (`findings.md`) | `description` as headline + `details` as body when present |
| SARIF (`message.text`) | `description` (full); `details` goes into `properties.detailed_message` for IDE hover |

When you have multi-paragraph context, put the headline in `description` and the body in `details`. Skipping `details` is fine — Markdown then just renders `description`.

**Location fields:** use the typed fields directly — `file` + `start_line` (+ `end_line`) for source code, `api_path` for HTTP endpoints, `schema_name` for OpenAPI schemas. Do NOT embed `file:line` or method+path inside `description` — the framework no longer extracts it from prose (p0146d). If a finding has no clean location, leave the typed fields null.

**`category` vs `concern`:** `concern` is a coarse structural axis (security/correctness/etc.) that drives skill orchestration. `category` is a fine-grained domain tag for SARIF rule grouping. Don't duplicate (`concern: security` + `category: "security"` is redundant — pick a finer category like `"secrets"` or `"injection"`).

**`evidence_mode`:** `"analyzed_from_source"` when you read source code; `"confirmed"` when an HTTP probe demonstrated the issue; `"potential"` for schema-only inference. SARIF maps these into the result's `properties.evidence_mode` field.

**Severity values — strict allowlist:**

`severity` must be EXACTLY one of: `"high"`, `"medium"`, `"low"`, `"info"`. Lowercase only.

NEVER use: `"critical"`, `"warning"`, `"severe"`, `"none"`, `"important"`, `"informational"`, or any other value. Outputs containing those will be rejected and dropped.

Mapping for findings imported from other tools:

- Nuclei `critical` → `"high"` (and add `(was: critical)` to `rationale`)
- Nuclei / ZAP `informational` → `"info"`
- ZAP `warning` → `"medium"`
- Nuclei `info` → `"info"`

If your input flagged something as `critical` and you would have echoed that, output `"high"` and document the upgrade in `rationale`. Do not invent new severity bands.

**Confidence Calibration:**

| Band | Range | Meaning | Action |
|------|-------|---------|--------|
| Low | 0–30 | Speculative — theoretical risk, no concrete exploit path | Do NOT report. These waste reviewer time. |
| Medium | 31–69 | Plausible — suspicious pattern, but requires specific conditions or further investigation | Report only if you can articulate the specific conditions needed for exploitation. |
| High | 70–100 | Confident — clear vulnerability pattern with known exploitation methods or certain exploit path | Always report. Include concrete code path and attack vector. |

When in doubt, round down. A false positive costs more reviewer time than a missed low-confidence finding.

**`blocking` is for exceptional situations, not for severity.**

`blocking: true` triggers a peer skill to be summoned. It is NOT a severity signal. Use it ONLY when:

- You cannot complete your analysis without input that another skill must produce first
  (e.g. "I need the auth scheme to assess these findings, but it is not in the swagger spec")
- A precondition is missing that, if filled, would change your conclusions
  (e.g. "I cannot rate this header finding because no header baseline is loaded")
- The artefact you were given is corrupt or contradicts itself in a way you cannot resolve alone
  (e.g. "the spec declares OAuth2 but every endpoint uses API keys — confirm with auth-config-reviewer")

Use `blocking: false` for:

- Any finding you can describe and rate yourself, no matter the severity (HIGH SQLi, missing CSP, exposed admin endpoint, …)
- Cases where the right downstream action is "fix it" — not "ask a peer first"
- Suggestions, audit notes, or recommendations

**Rules:**

- Do NOT include an `id` field — IDs are assigned by the framework.
- A HIGH severity finding is `blocking: false` unless your analysis literally cannot continue without peer input.
- If you set `blocking: true`, the `description` MUST state which input you are missing and which peer skill could supply it. "Security concern" alone is not enough.
- `confidence` reflects how certain you are (0 = guess, 100 = certain). See calibration table above.
- Produce 1–5 observations. Prefer fewer, higher-quality observations over many weak ones.

**Examples:**

A routine HIGH-severity finding — NOT blocking, just report it:

```json
[
  {
    "concern": "security",
    "description": "The /api/auth/login endpoint accepts passwords in query parameters, exposing them in server logs and browser history.",
    "suggestion": "Move password to POST body. Update OpenAPI spec and all clients.",
    "blocking": false,
    "severity": "high",
    "confidence": 95,
    "rationale": "OWASP A2:2021 — passwords in URLs are a well-documented vulnerability.",
    "api_path": "POST /api/auth/login",
    "effort": "small"
  }
]
```

A genuinely blocking case — analysis cannot proceed without peer input:

```json
[
  {
    "concern": "security",
    "description": "Cannot evaluate header findings: no api-headers baseline is loaded. security-headers-auditor needs the baseline catalog file before it can decide which headers are missing vs. intentionally absent.",
    "suggestion": "Load baselines/api-headers.yaml from the skill catalog or supply a project override.",
    "blocking": true,
    "severity": "info",
    "confidence": 100
  }
]
```
