Repository navigation
Description
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.
- A scout cannot link a report when it files it.
linksexists onscout-edit-reportonly.scout-emit-reporthas no such field. A scout that files a duplicate, a dependent step or a follow-up has to emit and then edit. - That gap lets auto-start run before the link exists.
emit_reportpersists the report and then calls_maybe_autostart_report, which runs_evaluate_link_gates. The link from the laterscout-edit-reportcall lands after the gate already passed. A scout-filedduplicate_ofordepends_onreport can open its own pull request, which is the outcome the gate exists to prevent. - The plan-parent gate has the same race from the other side.
plan_parentchecks for incomingpart_oflinks 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. - A scout cannot see incoming links. A link is one row on the source report, with no mirror row.
inbox-report-artefacts-liston 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 arecurrence_oforfollow_up_ofalready points at it, so it cannot tell whether a follow-up is already filed.incoming_links()intyped_report_links.pyanswers 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
linksfieldscout-edit-reporttakes (ReportLinkWriteSerializer, at mostMAX_REPORT_LINKS_PER_WRITE) to the emit request, and alinks_appendedcount to the response. - Write the links before
_maybe_autostart_reportruns. 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_logenforces 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
reasonstrings go to the safety judge on emit the same way_link_reasonsfeeds 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-reporttool description andlinkshelp text so the use case is obvious:duplicate_ofwhen a sibling report was filed first,follow_up_offor a failed validation of a resolved report,recurrence_ofwhen a resolved problem is back,depends_on/part_offor 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_oflink lands on a parent that has an auto-started run still in flight and no pull request yet, cancel that run and write anautostart_skip(plan_parent) on the parent. - (b) Defer the parent. When a report is emitted together with the
part_oflinks 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 ofincoming, asincoming_linksalready 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_texton every field, thenhogli 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-retrievetool 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_oflink to a report that has an open pull request: no implementation run starts, and the report carries aduplicate_ofautostart_skip. This is the regression the race produces today. - Emit with
depends_ona report that has no pull request:blocked_by_dependencyskip, 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
- Harness prompt and canonical scout skill changes that tell scouts when to write links. That is the follow-on once this lands.
- Link removal (tracked on feat(signals): typed report links, and a report per PR for stacked work #102776).
- Research writing
duplicate_ofon its own, basing a child pull request on its dependency's head branch, and the "Linked reports" rail UI. Phase 3 of feat(signals): typed report links, and a report per PR for stacked work #102776 or their own issues. - Migrating legacy
related_torows torecurrence_of.
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(thelinksfield 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
Activity
fix(signals): write scout report links at emit, before autostart
posthog commentedon Sep 29, 2026
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