# Related-issue context at intake

<!-- toc -->
- [What it costs](#what-it-costs)
- [Shape](#shape)
- [Settings](#settings)
- [Maturity](#maturity)
- [Where it goes](#where-it-goes)
- [Not included](#not-included)
<!-- /toc -->

A development sub-task is often filed with no description of its own. The
requirement sits on the parent, and the rest of the picture  -  the analysis, the
test scope  -  sits on the sibling sub-tasks beside it. The fetcher already read
the parent; it did not read the siblings, so a board that keeps its analysis in a
separate sub-task handed the pipeline an empty task.

`issue-fetcher.sh` now reads those siblings and exposes them as
`descriptor.relatedIssues[]`.

## What it costs

One search request, and only when the issue has a parent:

```
GET /rest/api/2/search?jql=parent="<PARENT>"&fields=summary,issuetype,status,description&maxResults=20
```

An issue with no parent has no siblings, so the lookup is skipped entirely and
that run pays nothing. The count does not grow with the number of siblings: the
issue's own `subtasks` field lists ITS children rather than its siblings, and the
parent's lists siblings without their descriptions, so either of those shapes
would cost one GET per sibling. `smoke-jira-context.sh` counts the requests, so
this stays a measurement rather than a claim.

The request lands before the maturity verdict reaches the user, which is one
extra round trip on the intake path. `jiraContext.enabled: false` is the way out
when latency matters more than the context.

## Shape

```json
"relatedIssues": [
  {
    "key": "PROJ-1002",
    "relation": "sibling",
    "type": "Sub-task",
    "status": "Done",
    "summary": "...",
    "description": "...",
    "truncated": false
  }
]
```

`type` is carried through verbatim from Jira and is never matched against in
code. Boards name their sub-task types differently and the query does not need
to know: `parent = <key>` returns all of them.

Siblings that carry a description are ordered first, so `maxItems` drops the
empty ones rather than the useful ones. An empty sibling is still listed  -  its
key, type and status are information  -  with an empty `description`.

## Settings

`prefs.global.jiraContext`:

| Key | Default | Effect |
|---|---|---|
| `enabled` | `true` | `false` skips the search entirely |
| `maxItems` | `6` | Hard cap; `0` behaves like `enabled: false` |
| `maxCharsPerItem` | `1200` | Per-sibling truncation, marked `truncated: true` |

Worst case payload is `maxItems x maxCharsPerItem`, next to the 4000-character
cap on the issue's own description.

## Maturity

One new code, `description_empty_sibling_available`, and it fires narrowly: the
issue's own description is empty, the parent's is empty or unreachable, AND a
sibling carries content. Today that combination produces the hard
`description_empty` blocker; it becomes a warning instead, the same downgrade
`description_empty_parent_available` already makes one level up.

The narrowness is deliberate. `score` is `100 - 40*blockers - 10*warnings`, and
the picker continues silently only at `score >= 90`, so a warning that fired
whenever an issue merely HAD siblings would cost ten points on every parented
issue and turn a silent intake into a question across the board.

## Where it goes

The descriptor is carried verbatim into the picker state as `issueRef`, and the
bridge copies `relatedIssues` onto `agent-state.json` when it is non-empty.

Persisting it is the point: `/multi-agent:resume` rebuilds context from durable
artefacts and never from the conversation, so an intake-only enrichment would be
gone by the first resume. The task's own `description` and `maturity` are still
not persisted, which is a known asymmetry rather than an oversight  -  this
change did not widen the bridge beyond the one field it needed.

Sibling text is added as its own labelled block, an `<untrusted-data source="jira:<KEY>">` block like every other fetched body (`features/external-context-injection.md`, "Prompt injection shape"): a sibling or parent description is data, never an instruction. It never replaces the working
description the way an accepted `parentDescription` does: substituting it would
erase the provenance that Phase 2's parent-story scope-drift check reads.

## Not included

Following the parent's `issuelinks` in a second search. A linked bug can be the
whole requirement while a linked test-execution record is noise, and the fetcher
cannot tell them apart, so the scope stops at sub-tasks. There is no setting for
it: a switch that does nothing is worse than an absent one.
