Skip to content

feat(signals): let scouts link reports on emit and read incoming links #108164

Open

Description

@andrewm4894

Problem

#104402 (phase 2 of #102776) made typed report links act: auto-start skips a report that is a duplicate_of worked-on report, one whose depends_on target has no pull request, and a plan parent that other reports are part_of. Three gaps in how scouts write and read those links stop the gates from holding reliably, and block scouts from adopting links at all.

  1. A scout cannot link a report when it files it. links exists on scout-edit-report only. scout-emit-report has no such field. A scout that files a duplicate, a dependent step or a follow-up has to emit and then edit.
  2. That gap lets auto-start run before the link exists. emit_report persists the report and then calls _maybe_autostart_report, which runs _evaluate_link_gates. The link from the later scout-edit-report call lands after the gate already passed. A scout-filed duplicate_of or depends_on report can open its own pull request, which is the outcome the gate exists to prevent.
  3. The plan-parent gate has the same race from the other side. plan_parent checks for incoming part_of links at the moment the parent is evaluated. A parent filed first and evaluated before its steps link to it passes the gate and can auto-start. Steps link from their own side, so a step is always written after its parent exists.
  4. A scout cannot see incoming links. A link is one row on the source report, with no mirror row. inbox-report-artefacts-list on a report shows the links that report wrote, never the links other reports wrote about it. A scout standing on a resolved report cannot see that a recurrence_of or follow_up_of already points at it, so it cannot tell whether a follow-up is already filed. incoming_links() in typed_report_links.py answers this, but only backend code can call it.

These gaps also block the next step: the harness prompt and the canonical scouts should start writing links, but that is not worth doing while an emitted link can lose the race and a scout cannot read what it would link to.

Proposal

1. links on scout-emit-report

  • Add the same links field scout-edit-report takes (ReportLinkWriteSerializer, at most MAX_REPORT_LINKS_PER_WRITE) to the emit request, and a links_appended count to the response.
  • Write the links before _maybe_autostart_report runs. Either write them inside the persist transaction (preferred: a rejected link then rejects the emit, so the scout never gets a half-linked report), or write them right after persist and before the auto-start hand-off.
  • Same validation as edit: add_log enforces no self-link, a live same-team target, and no cycle within a kind. A link that fails validation fails the emit with the scout tool's normal error shape.
  • Link reason strings go to the safety judge on emit the same way _link_reasons feeds them on edit.
  • Include the links in the emit idempotency key, so a retry with the same links returns the first report and a retry that adds links does not get treated as a new report.
  • A report the judge suppresses keeps its links, as it keeps its charts. The links explain the suppression to whoever reviews it.
  • Update the scout-emit-report tool description and links help text so the use case is obvious: duplicate_of when a sibling report was filed first, follow_up_of for a failed validation of a resolved report, recurrence_of when a resolved problem is back, depends_on / part_of for a split.

2. Close the plan-parent race

Pick one and record why in the PR:

  • (a) Re-check on link write. When a part_of link lands on a parent that has an auto-started run still in flight and no pull request yet, cancel that run and write an autostart_skip (plan_parent) on the parent.
  • (b) Defer the parent. When a report is emitted together with the part_of links of its steps in one run, evaluate auto-start for the parent only after the run has written them.
  • (c) Accept and document it. If the parent is almost always a pipeline report the steps are split from later, record that the race is not reachable today, and add a test that pins the current order.

Whatever the choice, a parent that already has an open pull request when a step links to it keeps that pull request. The roll-up already keeps such a plan open while its own pull request is active.

3. An incoming-links read a scout can call

Expose both directions on the report itself rather than adding a tool. SignalReportSerializer (so inbox-reports-retrieve) gains a read-only links block:

"links": {
  "outgoing": [{"kind": "depends_on", "report_id": "…", "title": "…", "status": "…", "reason": "…"}],
  "incoming": [{"kind": "follow_up_of", "report_id": "…", "title": "…", "status": "…", "reason": "…"}]
}
  • Built from outgoing_links() / incoming_links() so the tolerance rules and graph budgets stay in one module. Deleted source reports stay out of incoming, as incoming_links already does.
  • Retrieve only, not the list serializer, because the incoming read is a team-scoped scan and the list would pay it once per row.
  • help_text on every field, then hogli build:openapi, so the MCP tool and the generated frontend types pick it up. This is also the data the "Linked reports" rail and the Desktop link rendering (feat(desktop): show typed links between inbox reports #103927) need for the incoming side.
  • Update the inbox-reports-retrieve tool description to say links are on the report in both directions.

A linked_to=<report id> filter on inbox-reports-list would also work, but it answers only "what points at this", and costs a second call for the outgoing side.

4. Analytics

signals_report_linked fires from SignalReportArtefact.add_log, so links written on emit are counted with no change. It carries actor_kind / actor_agent but not the tool that wrote the link. Add a write_path property (emit, edit, or the pipeline producer), so scout adoption is measurable per path.

Tests

  • Emit with a duplicate_of link to a report that has an open pull request: no implementation run starts, and the report carries a duplicate_of autostart_skip. This is the regression the race produces today.
  • Emit with depends_on a report that has no pull request: blocked_by_dependency skip, no run.
  • Emit with an invalid link (self, other team, cycle): the emit fails and no report is written (if links go in the persist transaction).
  • Idempotent retry of an emit with links returns the first report and writes no second set of links.
  • Retrieve returns incoming links and drops a deleted source.
  • Whichever option section 2 picks, a test for it.

Extend the existing emit and link tests (test_report_links.py, the scout report tool tests, test_auto_start.py) rather than adding new files where practical.

Out of scope

Pointers

  • Emit path: products/signals/backend/scout_harness/tools/report.py (emit_report, _maybe_autostart_report, _build_links, _link_reasons, _emit_idempotency_key)
  • Edit link write: products/signals/backend/scout_report/persistence.py (append_report_links)
  • Request serializers: products/signals/backend/scout_harness/serializers.py (the links field on the edit request)
  • Link reads: products/signals/backend/typed_report_links.py (outgoing_links, incoming_links)
  • Gates: products/signals/backend/auto_start.py (_evaluate_link_gates, _record_link_gate_skip)
  • Roll-up: products/signals/backend/plan_rollup.py
  • Report serializer: products/signals/backend/serializers.py (SignalReportSerializer)
  • MCP tool config: products/signals/mcp/tools.yaml

Refs #102776, #104402, #103927.

Activity

  1. posthog commented on Sep 29, 2026

    @posthog
    Contributor

    PostHog self-driving picked up this issue and researched it. Somebody may already be working on it, so ask before you start a fix.

    Report (team access only): https://us.posthog.com/project/2/inbox/reports/01a0ec73-a597-7f95-ab7c-3eae585f1589

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions