feat(signals): typed report links, and a report per PR for stacked work
Nobody has claimed this yet.
- Dominant language
- Python
- Stars
- 39.9k
- Forks
- 3.4k
- Avg merge
- 6h 51m
- Merged PRs (30d)
- 232
Description
Problem
The inbox pipeline is shaped as one report, one implementation run, one PR. A GitHub issue that specs a stack of dependent PRs comes out the other end as a single PR, or as several unrelated ones.
Where the bias lives:
- Research asks for a title "that one engineer could ship in a single PR" (
ReportPresentationOutputinproducts/signals/backend/report_generation/research.py). The output has no plan or layers field, so phasing in the source issue is discarded. - Auto-start pre-generates exactly one head branch and tells the agent a PR from any other branch will not be recognized (
_generate_self_driving_head_branchand_head_branch_instructioninproducts/signals/backend/auto_start.py). That name is stamped into run state asself_driving_head_branch, and it is the attested end of the run-to-PR join that the tasks webhook and the stamphog review carve-out rely on. - The babysit loop reads
output.pr_urlonly (get_pr_babysit_snapshot.py), so a second PR from the same run gets no CI or review follow-up. - Supersession replaces PRs one at a time.
What already works: a report can hold many PRs. inbox-reports-claim takes a pull_requests list, SignalReportPullRequest rows are one per PR, the task-run receiver links every URL in pr_urls, completion is computed over the set, and the detail view offers a picker when there is more than one. The sandbox harness also already knows how to build a stack (git_signed_commit per layer plus the gh_stack tool, in products/desktop/packages/agent/src/server/agent-server.ts). Nothing on the signals side asks for one or can bind one.
Report-to-report linkage exists as the related_to artefact: untyped, undirected (direction is only recoverable from row order), written symmetrically by SignalReportArtefact.add_log, and produced by one writer, the grouping stage, when a recurrence spawns a fresh report next to a resolved one. The detail rail renders it as "Related report" with no further meaning.
A recent example. #102283 ends with "This can land as one PR or two. The second depends on the first." The pipeline opened two inbox reports for the one issue and auto-started both. #102381 scoped itself to the smaller half and noted the larger half "belongs in its own change", and nothing then created that change. #102406, from the other report, did the whole thing on its own branch. Both are open against master, unlinked, and overlap. The phasing was read correctly by one agent, then lost, and the fan-out (#90190) turned two dependent PRs into two competing ones.
Proposal
Two principles. One report per PR. Every existing rule about claims, PRs, completion, billing, checks and reviewers stays per report, and a stack is several reports. Linkage is a typed graph on reports. Stacks are one consumer of it; a follow-up on a merged fix, a regression, a duplicate and a recurrence are the others.
1. Typed, directed report links
Extend the RelatedTo content schema (artefact_schemas.py) with kind, direction and an optional note. Keep the symmetric write in add_log; the mirror row carries the inverse direction, so either side renders correctly without a join.
| Kind | Meaning | Writers |
|---|---|---|
recurrence_of |
A fresh report for something a resolved report covered. Today's untyped row. | Grouping, as now |
follow_up_of |
This report continues or fixes work done under another: a regression after a merge, a failed check, a next phase. | Research, scouts, checks, people, MCP |
depends_on |
This report's PR must base on the other's branch. The stack edge. | Research plan, scouts, people, MCP |
part_of |
This report is one piece of a parent whose summary is the plan. | Research plan, scouts, people, MCP |
duplicate_of |
Same fix as another report. | Research's in-flight check, scouts, people, MCP |
Rules: both reports on the same team; no self-links; depends_on edges must be acyclic within a part_of group; a duplicate_of target that is itself a duplicate resolves to the root. Rows without kind read as recurrence_of, so no data migration is needed. Attribution follows the existing ArtefactAttribution rules.
2. Write surfaces
- REST: a
linkaction onSignalReportViewSet(POST /signals/reports/:id/link/withreport_id,kind, optionalnote;DELETEremoves both rows),task:write. Validation lives in one module next toartefact_schemas.pyso every writer below shares it. - MCP:
inbox-reports-linkandinbox-reports-unlinkinproducts/signals/mcp/tools.yaml, documented in theinbox-explorationskill. - Scouts can write and edit links.
scout-emit-reportandscout-edit-reporttake alinkslist, so a scout filing a follow-up names the report it follows in the same call, andscout-link-reportslinks two existing reports. Gate on the same report-channel opt-in asscout-edit-report, since aduplicate_oflink can stop an implementation. - Pipeline: grouping keeps writing
recurrence_of. Research writesduplicate_ofwhen its in-flight check finds a sibling report's open PR covering the same fix, instead of only settingalready_addressed. The failed-check re-surface from #102283 writesfollow_up_ofwith the verdict as the note.
3. Readers
- Research context. A report with a
follow_up_of,depends_onorpart_ofedge gets the linked report's title, summary, findings and PR URLs in its research prompt. Today a follow-up re-derives everything and often misses the earlier PR. - Auto-start gate (
_create_implementation_task_if_absent). Skip when the report isduplicate_ofa report with an open or merged PR. Skip while anydepends_ontarget has no open PR. Both skips are recorded as an artefact so the inbox can say why. This closes #90186 without a separate heuristic. - PR base. When the
depends_ongate is met, the task description sets the PR base to the dependency's head branch. The child's own pre-generated head branch stays the attested link, so the webhook binding and the review carve-out are unchanged. - Completion roll-up (
apply_report_completion). Apart_ofparent resolves when every child resolves, is suppressed when every child is suppressed, and otherwise stays visible with progress. Children keep the current rule. - Supersession (
supersession.py) replaces adepends_onsubgraph, not one PR: a replaced lower layer replaces the layers above it. - Inbox. The detail rail gets a "Linked reports" section grouped by kind with state pills. A parent shows its children in
depends_onorder with PR state. Behind a flag, the list collapses children under their parent and duplicates under their root.
4. Stacks: a report per PR
- Plan authoring.
ReportPresentationOutputgets an optionallayerslist: title, scope, and the index of the layer it depends on. Research fills it from a## Stackor## Phasessection in the source issue when one exists, otherwise from its own judgment, and only when the layers are independently reviewable. Scouts can emit the same plan throughlinksonscout-emit-report. A person can build one by hand withinbox-reports-link. - Children.
mark_report_ready_activitycreates one child report per layer in the same transaction as the parent's summary, each with the parent's repo selection, reviewers and priority, a summary scoped to the layer,part_ofthe parent anddepends_onits predecessor. Children are bornreadywith the parent's judgments and are marked plan-authored, so a re-research is available as today but is not required before implementation. - The parent is a real report. It is the report the issue produced, its summary is the plan, and it never gets its own implementation run. No new object and no new list view.
- Sequencing. Layer 1 auto-starts under the existing thresholds. Layer N+1 auto-starts when layer N's PR is open, through the same receiver that syncs run PRs onto reports. After a child's PR opens, link it under the previous layer's PR with GitHub's Stacks API from the server side, best effort. The graph in Postgres is the source of truth; the GitHub stack is a side effect.
- Billing is per child PR run.
5. Analytics
signals_report_linked with kind, the writer identity (pipeline:*, scout, user, MCP client) and whether the link was plan-authored. Auto-start skips gain skip_reason values blocked_by_dependency and duplicate_of.
Tests
- Writing a typed link records the mirror row with the inverse direction, and reading a legacy row without
kindyieldsrecurrence_of. - A
depends_oncycle inside apart_ofgroup, a cross-team target and a self-link are rejected at every write surface. - Auto-start does not start a
duplicate_ofreport whose root has an open PR, does not start a child whose dependency has no open PR, starts it once that PR opens, and the task description names the dependency's head branch as the base. - A parent resolves when the last child resolves, is suppressed when all children are suppressed, and is untouched while any child is open.
- Research output with
layerscreates the children, thepart_ofanddepends_onrows and the parent summary in one transaction; a research output withoutlayerscreates nothing new. - A scout without the report-channel opt-in cannot write links; one with it can, attributed to the run's task.
- Prompt snapshot: a
follow_up_ofreport's research prompt carries the linked report's PR URL.
Out of scope
- Restacking a child after its dependency's PR changes. The babysit loop reports a moved base on the child; a follow-up run to rebase is a later issue.
- A human approval gate before children are created. Can be added as a team config that lands the parent
pending_inputwith the proposed layers. - Syncing GitHub sub-issues into the GitHub source.
- Fixing the one-issue-many-reports fan-out itself (#90189, #90190). Linkage makes the duplicates visible and stops competing runs; it does not stop the extra reports from being created.
Pointers
- Existing link primitive:
RelatedToinproducts/signals/backend/artefact_schemas.py; the symmetric write inSignalReportArtefact.add_log(products/signals/backend/models.py); writers inproducts/signals/backend/temporal/grouping.pyand the recurrence read inproducts/signals/backend/temporal/agentic/report.py; rendering inproducts/signals/frontend/inbox/components/detail/ArtefactLogList.tsx. - Auto-start:
products/signals/backend/auto_start.py(_create_implementation_task_if_absent,_build_autostart_task_description,_generate_self_driving_head_branch). - PR set, completion and supersession:
products/signals/backend/pull_requests.py,products/signals/backend/receivers.py(sync_task_run_pr_to_assignments),products/signals/backend/supersession.py. - Research output:
products/signals/backend/report_generation/research.py(ReportPresentationOutput, the in-flight check underalready_addressed); persistence inproducts/signals/backend/temporal/summary.py(mark_report_ready_activity). - Scout tools:
products/signals/backend/scout_harness/tools/report.py(emit_report,edit_report); tool definitions inproducts/signals/mcp/tools.yaml; theinbox-explorationskill. - Run-to-PR binding:
find_signal_implementation_runinproducts/tasks/backend/facade/api.pyandproducts/tasks/backend/webhooks.py. - Sandbox stacking: the stack instructions in
products/desktop/packages/agent/src/server/agent-server.tsandproducts/desktop/packages/agent/src/adapters/local-tools/tools/gh-stack.ts; thestacking-prsskill for thegh_stacksandbox path. products/signals/ARCHITECTURE.md, "SignalReportArtefact" and "Autonomy & Auto-Start".
Landing plan
Three PRs, each useful on its own. The second depends on the first, the third on both.
- Typed links: schema, validation module, REST and MCP write surfaces, scout
linksarguments, the detail-rail section, analytics. No pipeline behavior change. - Readers: research context for linked reports,
duplicate_offrom the in-flight check, the auto-start gate forduplicate_ofanddepends_on, the PR base from the dependency, the completion roll-up. - Stacks:
layerson the research output, child creation, sequencing on PR open, the server-side Stacks API link, supersession over a subgraph, the collapsed list view.
Refs #102283, #90186, #90190.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start with the three-PR landing plan and the pointers in products/signals/backend/artefact_schemas.py, models.py, auto_start.py, and report_generation/research.py. Read the existing link, auto-start, completion, and report-persistence tests first, then use the listed tests as the definition of done for typed links, dependency sequencing, stacked report creation, and completion roll-up.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- python, typescript
- Domain
- api, backend-api-design, frontend, testing-qa
- Issue type
- Feature
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Active
- Clarity
- Mostly clear
- Newbie friendliness
- 25/100