feat(signals): forward resolve notes to scouts, research and implementation runs
Nobody has claimed this yet.
- Dominant language
- Python
- Stars
- 39.9k
- Forks
- 3.4k
- Avg merge
- 6h 51m
- Merged PRs (30d)
- 232
Description
Is your feature request related to a problem?
When someone resolves an inbox report with a note, that note is stored but never forwarded. A dismissal note reaches the authoring scout as a report_dismissal steering note at cold start, and reaches the research run through load_research_steering. A resolve note does neither.
The resolve flow posts to the same POST /signals/reports/<id>/state/ endpoint as dismiss, with state="resolved", a resolve reason (fixed_outside_posthog, pr_merged, already_fixed, other), and an optional dismissal_note. The backend writes a dismissal artefact and then calls forward_dismissal_note, but _FORWARDED_STATUS_VERBS in products/signals/backend/dismissal_notes.py has no entry for RESOLVED, so _describe drops the report before a note is written. The scout prompt (_SCOUT_NOTES in scout_harness/prompt.py) states this explicitly: "A resolved report never reaches you this way."
The reasoning was that resolving means the report did its job, so there is nothing to steer on. In practice, people type useful context into a resolve note: what the fix was, where it landed, why it was fixed outside PostHog, what to watch for, or a caveat the next run should know. Today the only path for that text is dismissal_note on the reports API, which an agent sees only if it happens to retrieve that report. The research run on a recurrence gets the resolved report's title and summary as "Previously resolved report" context, but not the note. The implementation run never sees it.
Describe the solution you'd like
Forward resolve notes the same way dismissal notes are forwarded, so scouts, research runs, and implementation runs can weigh the text, decide whether to act on it, and record anything durable in the scratchpad.
Backend: forward on resolve
- Add
SignalReport.Status.RESOLVEDto the forwarded set indismissal_notes.pywith the verbresolved. Only forward when there is note text (a resolve reason code alone carries nothing to steer on;wrong_repostays the one no-prose exception). - Add a new origin
SignalScoutNote.Origin.REPORT_RESOLUTION(report_resolution) rather than reusingreport_dismissal. A resolve is not a verdict that filing the report was wrong, the prompts framereport_dismissalas exactly that, and the inbox labels it "From a dismissal". A separate origin lets prompts, the notes UI, and telemetry treat it correctly. Adding aTextChoicesvalue on aCharFieldwithchoicesneeds a smallAlterFieldmigration. - Keep the refund path unchanged.
refundinviews.pycalls the same forwarding helper and today relies on the forwarding path dropping a report that a refund leftRESOLVED(merged-PR case), pinned bytest_no_note_when_a_refund_leaves_a_merged_pr_report_resolved. Make that explicit instead of implicit: for example aninclude_resolvedflag that the two state endpoints pass and refund does not, or have the helper key on the caller's intent rather than only on the resulting status. - Bulk resolve already funnels through
_forward_dismissal_note(reports=transitioned), so one note per scout for a bulk resolve comes for free onceRESOLVEDis forwarded. - Note content (
_build_note_content): keep the shape (subject, reason code, quoted note, closing guidance) but write resolve-specific guidance. It should say the report was resolved, that the finding was real and a fix shipped, and ask the reader to record that a fix shipped and when so a recurrence is distinguishable from the original, fold anything durable into analready_addressed:orpattern:entry, and keep watching the topic rather than stop filing. - Same
DERIVED_NOTE_TTL, same_may_steer_scoutsauthorization. The dismissal reasoning applies unchanged: the text already reaches run context throughdismissal_noteon the reports API, so the extra API key scopes are not required on top of the RBAC and team-scope legs.
Scouts
- Update
_SCOUT_NOTESinscout_harness/prompt.py: remove the "A resolved report never reaches you this way" paragraph and add areport_resolutionbullet next toreport_dismissal,report_discussion. It should say a resolve means the finding was real and done, that the note often carries where and how the fix landed, and that the scout should record a fix shipped (with the date) and keep watching for a recurrence rather than treat it as a reason to stop filing. - Update the
_DISMISSAL_CONTEXTguidance if it should mention thatdismissal_reasonanddismissal_noteon a resolved report describe the fix rather than a rejection.
Research run
load_research_steeringalready reads every origin, so areport_resolutionnote reaches the research prompt as soon as it exists. Addreport_resolutionto_DERIVED_ORIGINSinreport_steering.pyso the origin is classified consistently, and add aresolution_notes_attachedcounter toReportSteeringalongsidedismissal_notes_attached, reported on the steering telemetry event intemporal/agentic/report.py.- Update
_RESEARCH_NOTES_HEADcopy, which currently says the derived notes carry what a person said when they "dismissed, discussed, or rated" an earlier report. - On a recurrence, the grouping activity creates a fresh report linked to the resolved one via a
related_toartefact, and research receives the resolved report's title and summary through_render_resolved_report_contextinreport_generation/research.py. Also pass the resolved report's latest resolve note (reason and text from its newestdismissalartefact) into that block, since this is the single most relevant place for it: the run is deciding whether this is a regression of that fix.
Implementation run
load_report_steeringexcludes all derived origins because they quote report content (title and reviewer text built from raw product data) into a run that can push a PR. Includereport_resolutionin the implementation run's read set while keeping the other derived origins excluded.- To keep the injection surface the same as today, render the resolution note body without the report title: report id, reason code, and the human-typed note only. The note text is authored by someone who passed the steer-scouts gate, which is the same trust level as a
humannote. Update thereport_steering.pymodule docstring and_IMPLEMENTATION_NOTES_HEADto describe this. - This is the one design point worth a reviewer's eye before implementation: whether including a human-authored resolve note, minus the title, in a PR-pushing run is acceptable. If not, the fallback is to forward to scouts and research only and leave implementation on
humannotes.
Other consumers of the origin enum
scout_harness/views.pynotes list: addreport_resolutionto the origins withheld from callers without report read access.scout_harness/tools/notes.pymodule docstring andleave_notecomments that enumerate the derived origins.products/signals/frontend/inbox/logics/scoutNotesLogic.ts: add a label, for example "From a resolution".- Regenerate OpenAPI types with
hogli build:openapiso theoriginenum inproducts/signals/frontend/generated/api.schemas.tsandservices/mcp/src/api/generated.tspicks up the new value. products/signals/backend/models.py: theSignalScoutNotedocstring lists the derived writers and says "not resolve". Update it.products/signals/ARCHITECTURE.md: mention the resolve forwarding path where the resolve transitions are described.
Tests
test_dismissal_scout_notes.py::test_only_transitions_that_judge_the_report_are_forwarded: theresolvecase changes from 0 notes to 1. Add cases for a resolve with no note (0 notes) and for a bulk resolve writing one note per scout.- A test that the resolve note body carries the
resolvedverb, the reason code, the quoted note, and no report title. test_no_note_when_a_refund_leaves_a_merged_pr_report_resolvedmust keep passing.test_report_steering.py: research steering carries areport_resolutionnote and counts it; implementation steering carriesreport_resolutionbut still excludesreport_dismissal,report_discussion, andreport_feedback.- Research prompt test: the "Previously resolved report" block includes the resolve reason and note when present.
Describe alternatives you've considered
- Reuse the
report_dismissalorigin with aresolvedverb. Least plumbing, but every prompt and the inbox UI would present a resolve as a dismissal, and the scout prompt tells scouts a dismissal is the one thing that should make them stop filing. That is the opposite of what a resolve note means. - Rely on
dismissal_noteon the reports API. This is the status quo. It only reaches an agent that happens to retrieve that report, and never reaches the implementation run. - Forward to scouts and research only. The safe fallback if the implementation-run question above lands on no.
Additional context
Relevant code:
products/signals/backend/dismissal_notes.py(_FORWARDED_STATUS_VERBS,_describe,_build_note_content)products/signals/backend/views.py(state,bulk_state,refund,_forward_dismissal_note,_transition_report_state)products/signals/backend/report_steering.py(_DERIVED_ORIGINS,load_report_steering,load_research_steering)products/signals/backend/scout_harness/prompt.py(_SCOUT_NOTES,_DISMISSAL_CONTEXT)products/signals/backend/report_generation/research.py(_render_resolved_report_context)products/signals/backend/temporal/grouping.py(recurrence of a resolved report)products/signals/backend/models.py(SignalScoutNote.Origin)
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 products/signals/backend/dismissal_notes.py, views.py, report_steering.py, and models.py, then read the named prompt, research, and frontend notes files. Run the listed dismissal and report steering tests first. Done means resolve notes are forwarded with a distinct origin to the intended scout, research, and implementation consumers, with migrations, generated types, prompt/UI updates, and all specified tests passing.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- javascript, python, typescript
- Domain
- api, backend, frontend, testing
- Issue type
- Feature
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Active
- Clarity
- Clearly specified
- Newbie friendliness
- 38/100