monitor.issue_context.v1
Status: Implemented (E2.5/E2.7/E2.8). Produced by
internal/explain. Buildand consumed bymonitor issue <id|short-prefix|latest>(human,--json,--md) and MCP'smonitor_issue, whose DEFAULT response now is this bounded schema (its own smallissuesummary included) — see "Compatibility" below for the opt-in legacy{issue, occurrences, occurrences_truncated}shape and how it interacts with this one. Studio and cortex are still later work. The rules this schema encodes (fingerprint/culprit,ObservedAt,--scan) are specified by the internal naming ADR (see the repo's AGENTS.md). A few shape details were settled during implementation and are called out inline below, since the draft explicitly left them open.
Why this exists
Before E2.5, monitor issues show <id> returned whatever the issues store held verbatim (see Local Issues), and internal/cli/ issues.go registered issue only as a cobra alias of issues, so monitor issue list|show|resolve|reopen|ignore ... behaved exactly like monitor issues list|show|resolve|reopen|ignore ..., but a bare monitor issue <id> (a real id, not one of those subcommand names) had no meaning of its own — cobra had nothing to dispatch "an id" to, so it fell through to usage/help rather than the enriched page below. E2.5 gave monitor issue <id> its own command, taking over that alias (see the naming ADR's issue/ issues collision row); monitor issue list|show|resolve|reopen|ignore ... keeps working during the deprecation by delegating to the matching issues subcommand (with a deprecation note on stderr) rather than being removed outright. Once exceptions carry frames, causes, and a culprit (§6 of the naming ADR), the issue page needs one producer that reads the store once, enriches it with codemap/git/ vecgrep, and degrades honestly when any of those are unavailable — instead of every renderer (CLI, --md, MCP) re-implementing that enrichment. explain.Build is that producer. It always opens the store read-only (issues.OpenReadOnly), so it never blocks on — or is blocked by — a writer such as watch --stash.
Budgets
explain.Build(id, budget) takes a budget so a caller can ask for less than the full page. The budget does not gate which top-level sections show up — every section explain.Build knows how to fill is present at every budget, because a "why did it fail" answer that silently drops impact or last_touched to hit a size target is worse than a shorter version of the same answer (see the roadmap's own monitor_issue {id:"latest"} brief example, which carries culprit.snippet, causes, impact, last_touched, degraded[], and next[] in under 4 KB). The budget instead gates how much detail each section carries:
| Budget | Target size | Every section is present; budget controls detail |
|---|---|---|
brief | ≤ 4 KB | frames capped to 5 in-app entries (the rest counted in truncated.frames); causes capped to the 2 closest to the innermost cause (the rest counted in truncated.causes); last_touched.author_email always omitted. A pathologically long culprit/snippet still over budget after those fixed caps is shrunk further (snippet lines first, then frames, then causes) until it fits — see truncated below. |
standard | ≤ 16 KB | The same fixed caps do not apply — every in-app frame (already bounded to 12 at ingest) and every cause (bounded to 3) — but the same shrink-until-it-fits backstop still runs if 16 KB is somehow exceeded. |
full | unbounded | Everything standard has; the backstop never runs. |
Both budgets currently read the same ±4-line snippet window around the culprit — the draft's "a wider snippet window" distinction for standard was not implemented; truncated is what actually shows whether anything was cut. MCP's monitor_issue defaults to brief so a "what's the latest crash" question costs one small, cheap call and still gets a real culprit, causes, impact, and last-touched answer, rather than a stub that needs a second call to be useful. The CLI's monitor issue <id> defaults to standard.
Shape
{
"schema": "monitor.issue_context.v1",
"budget": "brief", // "brief" | "standard" | "full"
"generated_at": "2026-09-22T18:04:11Z",
// present only when the caller asked for id:"latest" (or a filter) rather
// than a specific short_id
"resolved_from": {
"id": "latest",
"project": "polyglot",
"service": "workload",
"kind": "exception"
},
"issue": {
"id": "ISS-A07E1234567890AB",
// uppercase first 4 hex chars of the id's hex portion (store.go builds
// `id` as "ISS-" + strings.ToUpper(fingerprint[:16])). This is a new,
// purely-display field: today's `issues.Store.Get` only matches the
// full `id` exactly, so E2.5 also has to teach issue lookup to accept
// an unambiguous short_id/id prefix, not just the value itself.
"short_id": "A07E",
"status": "open", // open | resolved | ignored
// Issue.Kind is an open string; producers write "exception" (this
// schema, new), "investigation" (internal/cli/investigate.go), or
// "monitor.alert.<rule>" (internal/cli/watch.go) — never "alert" bare.
"kind": "exception",
"title": "TypeError: Cannot read properties of undefined (reading 'id')",
"exception_type": "TypeError",
"handled": false,
"project": "polyglot",
"service": "workload"
},
"timeline": {
"first_seen": "2026-09-20T09:11:00Z",
"last_seen": "2026-09-22T18:03:50Z",
"occurrences": 37,
"reopened": 1,
"first_git_sha": "a1b2c3d",
"runs": ["CI-4821", "CI-4830"], // MONITOR_LAUNCH_ID / CHALUPA_CI_RUN_ID seen
// "line" | "mtime" | "live" | "unknown" — see naming ADR §4. Neither
// issues.Issue nor issues.Occurrence PERSISTS which of the three
// produced an occurrence's ObservedAt yet (that provenance belongs to
// the E2.1/E2.4 producers, `stacktrace parse --record` and `monitor
// run --`, on a parallel branch not merged here); until a producer-side
// field exists, this is a documented placeholder that always reads
// "unknown" — NOT "live": every issue on this branch is seeded through
// internal/explain/testdata/seed / issues.RecordException directly,
// never through a real log-replay producer, so claiming "live"
// unconditionally would be fabricated provenance, not an honest
// placeholder.
"time_source": "unknown"
},
"culprit": {
"function": "loadUser",
// fqn is set only when codemap resolved the symbol (ProbeCodemap
// HealthOK and CodemapSymbolAt found a real range) — omitted, not
// null, otherwise, like every other omitempty field below.
"fqn": "workload/src/users.ts:loadUser",
"file": "src/users.ts",
"line": 42,
"source": "stack", // "stack" | "message_search" — see naming ADR §7
"confidence": "high", // "high" | "low"; always "low" when source is "message_search"
// "via" and "mapping" are BOTH omitted entirely when source is "stack"
// (a real frame needs neither); a "message_search" culprit (E2.8: no
// in_app frame anywhere in the chain, resolved by searching the
// normalized message's longest literal fragment) instead carries both:
// "via": "vecgrep", // "vecgrep" | "git_grep" — which tool found it
// "mapping": "inferred", // ALWAYS "inferred" for message_search —
// // naming ADR §7's "siempre marcado mapping:
// // inferred"; reuses stacktrace.Frame's
// // existing source-map confidence enum as the
// // vocabulary for "not read off a real frame"
// range.source is "codemap" when CodemapSymbolAt resolved it, or
// "frame" for a synthetic +-4-line window around the culprit line when
// codemap is unavailable/unhealthy or found nothing there.
"range": { "start": 38, "end": 51, "source": "codemap" },
"snippet": {
"start": 38,
"lines": ["function loadUser(id) {", " return db.users.find(id).name;", "}"],
"highlight": 42,
"sha256": "…", // sha256 of the snippet SLICE (the joined lines above), not the whole file
// true when git blame's last-touching commit for this line differs
// from Issue.FirstGitSHA (the file changed since this issue was
// first recorded) -- always false when either SHA is unknown, which
// is the common case for an ordinary local dev session with no
// MONITOR_GIT_SHA/GIT_SHA/GITHUB_SHA set (see Issue.FirstGitSHA).
"stale": false
}
},
// culprit is entirely absent (omitted, not null) when the exception chain
// has no in_app frame anywhere AND the message-search fallback also found
// nothing to blame -- see "degraded" for why in that case.
"causes": [
{ "type": "TypeError", "culprit": { "function": "loadUser", "file": "src/users.ts", "line": 42 } }
],
"frames": [
// in_app frames only, closest to the crash first; how many more were
// cut (brief's 5-frame cap, or the ingest-time 12-frame cap) is in
// truncated.frames below, never inlined per-entry here
{ "function": "loadUser", "file": "src/users.ts", "line": 42, "in_app": true }
],
"impact": {
"status": "ok", // "ok" | "skipped"
"callers": 3,
"blast_radius": 12,
"tests": 1, // count of tests exercising the culprit line, from a single codemap `impact --at` call
"test_files": ["src/users.test.ts"], // best-effort, additive; may be shorter than `tests` if codemap's own per-entry shape couldn't be parsed
"untested": false,
"call_graph": "confirmed" // codemap's confidence enum
},
"last_touched": {
"status": "ok",
"sha": "a1b2c3d",
"subject": "fix: guard against missing user record",
"author_time": "2026-09-18T14:02:00Z"
// author_email omitted entirely at brief budget; present at standard/full
},
"related_notes": {
"status": "skipped",
"items": [] // <= 3 items, later epic (N12); never the note body in `brief`
},
"degraded": [
{
"component": "codemap",
"state": "schema_skew",
"detail": "graph db schema v9 is newer than this codemap (supports v7)",
"recovery": "upgrade the codemap binary; do not run codemap index --reindex"
}
],
// sorted by component name; deduplicated -- codemap being unhealthy is
// reported once even though both culprit.fqn/range AND impact depend on it
"next": [
{ "cli": "monitor issue a07e --md", "why": "paste-ready fix context for your agent" }
],
"truncated": {}, // e.g. {"frames": 7} when brief's cap (or the size backstop) dropped 7 frames; {"causes": N} likewise; omitted keys mean nothing was cut
// scrubbed: a count of values a defense-in-depth scrub pass redacted from
// this READ (title/snippet/commit subject) -- separate from, and on top
// of, whatever internal/scrub already redacted before this text was ever
// persisted (E2.2)
"privacy": { "scrubbed": 0, "text_is_untrusted": true }
}Degradation
Every section that depends on an external tool — impact (codemap), last_touched (git), culprit.via (vecgrep or git grep) — reports its own status. A missing or unhealthy dependency produces status: "skipped" with a detail and a recovery string in degraded[]; it never produces an empty-but-present field that looks like "there was no impact" or "nobody touched this line". This mirrors the existing {status, limitation, recovery} habit already used by monitor investigate's pipeline steps.
codemap's "index is a newer schema than this binary supports" case reports schema_skew (or, once codemap ships the fix from the roadmap's E1.3b, its own schema_newer code) with the recovery "upgrade the binary, don't reindex" — never codemap index --reindex, which would downgrade the shared global index.
degraded[] can carry a vecgrep entry even when culprit.source is message_search and a culprit WAS found via via: "git_grep": vecgrep not being ready for the project's current branch is worth surfacing on its own (with its own recovery) regardless of whether the git grep fallback happened to succeed anyway — the roadmap's own wording is "en una rama sin índice, degraded incluye la recuperación".
Compatibility
fullbudget frames andrelated_notesbodies are additive to the existingissues.Issue/issues.OccurrenceJSON — v1.15 consumers that decode today's fields keep working unchanged.- MCP's
monitor_issue(E2.7) response, BY DEFAULT (nooccurrence_limit, oroccurrence_limit: 0), IS this schema — keyed exactly as above (schema,budget,generated_at,resolved_from,issue,timeline,culprit,causes,frames,impact,last_touched,related_notes,degraded,next,truncated,privacy) at thebriefbudget, INCLUDING this schema's own smallissuesummary ({id, short_id, status, kind, title, exception_type, handled, project, service}). The pre-E2.7{issue, occurrences, occurrences_truncated}shape is NOT merged in by default any more: it is an explicit opt-in viaoccurrence_limit > 0(max 200), and when a caller does opt in, its ownissuekey — the full, richerissues.Issue, unchanged since before E2.7 — OVERWRITES this schema's small summary above, soshort_idis then visible only through the CLI/--json's dedicatedmonitor issuepage, not throughmonitor_issue's response. This is a deliberate size-budget decision, not the schema's original "always additive" design: the legacyissue/occurrencesfields have no size bound of their own (a long exception message, or a large occurrence count, took the ALWAYS-merged response well past AC-6's "brief ≤ 4 KB, ~1.5k tokens" promise even after this schema's OWN budget enforcement was fixed), so the response a caller gets by default had to become the bounded schema alone, with the richer legacy shape available only at explicit extra cost. id:"latest"(E2.7) is a NEW accepted value formonitor_issue'sidfield and the CLI'smonitor issuepositional argument — additive to the existing literal-id/prefix behavior, never a required change for an existing caller that always passes a real id.- A "latest" (or filtered) query matching nothing returns
{not_found: true, recovery: "..."}— never anerrorfield or a protocol-level failure; an ordinary, expected outcome (a healthy project between crashes), not a broken tool. suspectsandsimilar(later epics, L1/L3) will be added the same way: additive fields withstatus: "skipped"until they exist, never a breaking rename.- This schema does not change
monitor profile --json; that stays exactly as documented inline-heatmap-v1, and onlyinvestigate --json/ MCP omitprofile.textby default.