PostHog / PostHog/posthog

feat(signals): typed report links, and a report per PR for stacked work

Open
#102,776 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

self-driving
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" (ReportPresentationOutput in products/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_branch and _head_branch_instruction in products/signals/backend/auto_start.py). That name is stamped into run state as self_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_url only (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 link action on SignalReportViewSet (POST /signals/reports/:id/link/ with report_id, kind, optional note; DELETE removes both rows), task:write. Validation lives in one module next to artefact_schemas.py so every writer below shares it.
  • MCP: inbox-reports-link and inbox-reports-unlink in products/signals/mcp/tools.yaml, documented in the inbox-exploration skill.
  • Scouts can write and edit links. scout-emit-report and scout-edit-report take a links list, so a scout filing a follow-up names the report it follows in the same call, and scout-link-reports links two existing reports. Gate on the same report-channel opt-in as scout-edit-report, since a duplicate_of link can stop an implementation.
  • Pipeline: grouping keeps writing recurrence_of. Research writes duplicate_of when its in-flight check finds a sibling report's open PR covering the same fix, instead of only setting already_addressed. The failed-check re-surface from #102283 writes follow_up_of with the verdict as the note.
3. Readers
  • Research context. A report with a follow_up_of, depends_on or part_of edge 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 is duplicate_of a report with an open or merged PR. Skip while any depends_on target 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_on gate 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). A part_of parent 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 a depends_on subgraph, 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_on order 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. ReportPresentationOutput gets an optional layers list: title, scope, and the index of the layer it depends on. Research fills it from a ## Stack or ## Phases section 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 through links on scout-emit-report. A person can build one by hand with inbox-reports-link.
  • Children. mark_report_ready_activity creates 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_of the parent and depends_on its predecessor. Children are born ready with 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 kind yields recurrence_of.
  • A depends_on cycle inside a part_of group, a cross-team target and a self-link are rejected at every write surface.
  • Auto-start does not start a duplicate_of report 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 layers creates the children, the part_of and depends_on rows and the parent summary in one transaction; a research output without layers creates 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_of report'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_input with 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: RelatedTo in products/signals/backend/artefact_schemas.py; the symmetric write in SignalReportArtefact.add_log (products/signals/backend/models.py); writers in products/signals/backend/temporal/grouping.py and the recurrence read in products/signals/backend/temporal/agentic/report.py; rendering in products/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 under already_addressed); persistence in products/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 in products/signals/mcp/tools.yaml; the inbox-exploration skill.
  • Run-to-PR binding: find_signal_implementation_run in products/tasks/backend/facade/api.py and products/tasks/backend/webhooks.py.
  • Sandbox stacking: the stack instructions in products/desktop/packages/agent/src/server/agent-server.ts and products/desktop/packages/agent/src/adapters/local-tools/tools/gh-stack.ts; the stacking-prs skill for the gh_stack sandbox 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.

  1. Typed links: schema, validation module, REST and MCP write surfaces, scout links arguments, the detail-rail section, analytics. No pipeline behavior change.
  2. Readers: research context for linked reports, duplicate_of from the in-flight check, the auto-start gate for duplicate_of and depends_on, the PR base from the dependency, the completion roll-up.
  3. Stacks: layers on 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

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.