flower
/
All briefs
complete draft mcp flower
epic · Redesign /decisions feed to deliver decision context...

[#216 PR-3b] /decisions "Brief me" AI button — on-demand RAG-over-our-recall highlight (deepseek)

canonical · plan

Spec

markdown

hand-off · dispatch

Dispatch

Auto-dispatch

when it reaches planned

Design-loop

design pass before build

This brief is complete — dispatch is closed.

#107 done fresh flower · flower/223-decisions-brief-me
agent: claude
You are being dispatched from flower Brief #223: [#216 PR-3b] /decisions "Brief me" AI button — on-demand RAG-over-our-recall highlight (deepseek)

Recall pointer:
- Use recall_brief with id 223 for the full folder if you need provenance.

Target:
- project: flower (/Users/mikeferrara/Documents/code/flower)
- branch: flower/223-decisions-brief-me
- worktree: not specified
- kind: fresh

Current brief spec:
(no spec yet)

This is a direct request, not a fully-specced plan. If it's clear, resolve it. If you hit a blocking ambiguity, call brief_ask (or brief_append) with your questions and flip the brief to `refining` before proceeding — don't guess.

Recent/key trace events:
[1] participant_joined flower-216-worker: (no body)
[2] note_added flower-216-worker: Child of Brief #216. Design doc: `docs/design/216-decisions-feed-redesign.md` §"The Brief me AI button" + §"Sequenced delivery" PR-3b. Depends on PR-3a (#222) — builds on its `DecisionBriefContext` assembler. Honors operator Q52: AI is an OPTIONAL on-demand button, RAG-fed by flower's OWN recall (highlights, does not fetch/decide).

## Scope (≈ ≤300 lines)
1. **Migration** — nullable columns on `decisions`: `ai_brief` (json), `ai_briefed_at`, `ai_brief_cost`, `ai_brief_model`.
2. **Completion helper** — extract the private `sendJsonCompletion($base,$body)` core (the `Http::withToken → /chat/completions` call + provider pin + graceful-degrade-on-`response_format`-reject retry + untrusted-data hardening) from `OpenRouterBriefRefinementClient` into a small shared helper. Do NOT call `refine()` (it's DTO/schema-bound). Equivalent alt: add a sibling `briefDecision(DecisionBriefRequest): array` on the client. Model selection follows the brief chain → default `deepseek/deepseek-v4-flash`.
3. **`BriefDecision` queued job** — match `RefineBrief` pattern (`ShouldBeUnique`, `uniqueId='decision-brief:'.$id`, `tries=2`, `timeout=180`) on the fast queue. Consumes PR-3a's `DecisionBriefContext` (7 buckets), builds a `{model, messages, max_tokens, temperature, response_format: json_schema(tldr/bullets/considerations)}` body wrapping all assembled context as delimited UNTRUSTED data. System prompt: "brief the operator on ONE pending decision using ONLY the context below; highlight what changed / why now / what each option implies; do not invent/fetch/assume; if context is thin, say so." Store result+cost+model+timestamp on the decision, then broadcast `DecisionBriefed` on the EXISTING `decisions` channel.
4. **Realtime wiring (one line)** — add `'echo-private:decisions,.'.DecisionBriefed::class => 'onDecisionsChanged'` to `Index::getListeners()` (`app/Livewire/Decisions/Index.php:43`) — it enumerates by FQCN, not wildcard, so a new event is NOT picked up automatically. `wire:poll.30s` is the fallback.
5. **`x-decisions.ai-brief` panel** — light fence (`mt-3 border-t border-border/70 pt-3` + `mono-label` "ai briefing" eyebrow, NOT a nested `x-ui.card`). States: idle/loading/cached/error. `:surface` prop (`'glance'|'detail'`) suffixes DOM id → `ai-brief-glance-{id}` / `ai-brief-detail-{id}` (the flyout renders in-DOM alongside the card, so a bare id collides). `aria-busy` + `aria-live="polite"` + `aria-controls` to the surface-suffixed id. Cached structure: TL;DR / What changed–why now / Considerations (tied to actual options; recommended option echoes the affordance's amber ring, never color-only).
6. **`Concerns/BriefsDecisionsWithAi` host concern** — new SIBLING of `AnswersDecisions` (never an edit to it). Method `briefMe($decisionId)` dispatches the job + sets a per-decision boolean for panel open/closed.

## Critical constraints
- **Scope every loading/disabled directive** to `wire:target="briefMe({id})"` — the board is a single Livewire component; a bare `wire:loading` + `wire:poll.30s` would flip EVERY card's button to "Briefing…" every 30s. Panel open/closed driven by the `briefMe()` boolean, NOT `wire:loading`.
- **Toggle vs re-brief:** fresh → "Brief me" opens loading; cached → same button is a plain show/hide (spends nothing); re-running the model ONLY via the foot "re-brief" link.
- **Button styling:** text-link tier (`mono-label text-fg-faint hover:text-accent` + `bolt` glyph), matching row peers "View more"/"Open in new tab ↗" — NOT a bordered pill (Q52: AI not the centerpiece).
- **Cost/latency signal** visible before (button title ~$0.002), during (foot "deepseek-v4-flash · ~$0.002 · usually 5–15s"), after (foot stamp with actual cost).
- Model highlights, never auto-answers; the affordance is the only thing that records a call. Answer path frozen (review gate: `affordance.blade.php` + `AnswersDecisions.php` unchanged).

## Acceptance
Clicking Brief me on one card briefs only that card (no cross-card flip on poll/answer); result caches on the decision + shows cost/latency; error degrades with retry; re-brief re-dispatches; standalone/no-context → disabled with a reason; `php artisan test` green; `pint` clean.
[3] parent_set flower-216-worker: Grouped under epic #216.
[4] dependency_added flower-216-worker: Now depends on #222 ([#216 PR-3a] /decisions Depth (structured, no AI) — decisionSummary brief-web + grouped linked-context).
[5] participant_joined flower-222-worker: (no body)
[6] unblocked flower-222-worker: Unblocked — #222 reached complete.
[7] participant_joined flower-orchestrator: (no body)
[8] status_change flower-orchestrator: (no body)

Recommended linked context:
{
    "todos": [],
    "scratchpads": []
}

Execution notes:
- Treat the brief as the source of truth.
- Keep work scoped to this dispatch request.
- Use brief_append / brief_update_status when reporting material progress; as your final dispatched-worker step, call brief_dispatch_complete with dispatch_request_id (or brief_id) and actor_ref.
- Codex workers should verify mutating Flower tools with tool_search query `brief_append brief_dispatch_complete flower_feedback` (limit 20) when tool availability is in doubt; report raw SEE/LOAD vs NOT visible instead of silently using local fallbacks.
- Add a git commit trailer `Brief: #223` to every commit for this brief so flower can exact-link commits back to the brief.

provenance · append-only

Trace

live
or paste a screenshot uploading…
  1. link added 1d ago
    agent · system:commit-trailer
  2. participant joined 1d ago
    system · system:commit-trailer
  3. note added 1d ago

    Provenance cleanup (fb #110 / brief #227): removed the 6 spurious `result` commit-links that system:brief-autolink wrote at 12:00:02–03 (brief_links ids 467/469/470/473/476/478 → commits d3d3700 #109, 9671c9a #156, d1f12b4 #219, 0afe7d3 #222, 576c17e #221, f54c7ac #220 — all OTHER briefs' merges, none #223's work). Deleted directly on MAIN MySQL (targeted by id, verified 6 rows removed). #223's real provenance is worker commit 1ad0be0 (carries the `Brief: #223` trailer → re-links via commit-ingest) + merge ce569c3. The autolink code fix that prevents this recurring is dispatched as brief #227 (worker flower-227-worker building).

    agent · flower-orchestrator
  4. merged 1d ago

    Merged flower/223-decisions-brief-me into master on MAIN — merge commit **ce569c3** (worker commit 1ad0be0). Linear descendant of d3d3700 (branched off current master, no divergence) → clean merge. Migration `2026_07_04_170000_add_ai_brief_to_decisions_table` ran on real MySQL (78ms, nullable ai_brief cols). Full suite green on MAIN (1117 passed, 2 skips, 6920 assertions); pint clean; npm build done (ai-brief panel + card/detail views). **Horizon gracefully reloaded** (`flower:horizon-reload` → SIGTERM master 1309, in-flight jobs finish, Solo auto-restarts on fresh code) so the new `BriefDecision` queued job is live. Worker proc 1129 closed. Worker correctly deferred the shared-MySQL migration to the orchestrator (classifier-blocked in worktree — good boundary respect). NOTE: the 6 auto-linked "result" commits on this brief from system:brief-autolink (12:00Z) are SPURIOUS (bug #110 — fired before any commit existed); the real result is worker commit 1ad0be0 (Brief: #223 trailer) + merge ce569c3. 🎉 #216 /decisions redesign chain COMPLETE end-to-end: #220 (display seam) → #221 (capture/enrich) → #222 (structured depth) → #223 (Brief me AI). Backfill already enriched real decisions (#221). The whole redesign is shipped + verified on MAIN.

    agent · flower-orchestrator
  5. status change 1d ago
    agent · flower-223-worker
  6. dispatched 1d ago

    Dispatch request #107 marked done.

    agent · flower-223-worker
  7. note added 1d ago

    PR-3b shipped on `flower/223-decisions-brief-me` (commit 1ad0be0). Full suite green (1117 passed, 2 pre-existing skips), pint clean. Answer path FROZEN — affordance.blade.php + AnswersDecisions.php verified unchanged in the diff. NOT merged (orchestrator merges from MAIN). ## Files Backend (new): `app/Services/OpenRouter/SendsOpenRouterJsonCompletions.php` (shared JSON core), `app/Services/Decisions/{DecisionBriefRequest,DecisionBriefClient,OpenRouterDecisionBriefClient,DecisionBriefer}.php`, `app/Jobs/BriefDecision.php`, `app/Events/DecisionBriefed.php`, `app/Livewire/Decisions/Concerns/BriefsDecisionsWithAi.php`, migration `..._add_ai_brief_to_decisions_table.php`. Backend (edited): `OpenRouterBriefRefinementClient` (now uses the trait — identical behavior), `Decision` (3 casts), `Index` (+listener +concern), `Show` (+concern), `FlowerServiceProvider` (bind DecisionBriefClient), `config/flower.php` (decisions.brief_max_tokens). UI (new): `components/decisions/{ai-brief,brief-me-button}.blade.php`. UI (edited): `card`/`detail`/`cluster` blades + `index`/`show` livewire views (thread the per-decision `briefing` map). Tests (new): `DecisionBriefTest` (service/job/Livewire, mocked client) + `OpenRouterDecisionBriefClientTest` (Http::fake transport — schema body, untrusted envelope, degrade-on-reject). ## Approach / how the 6 scope items map (1) migration — nullable ai_brief(json)/ai_briefed_at/ai_brief_cost(decimal 12,6)/ai_brief_model; portable (sqlite suite proves it; `after()` is a MySQL-only no-op elsewhere). (2) extracted the transport core into a TRAIT `SendsOpenRouterJsonCompletions` (provider pin + degrade-on-response_format-reject retry + untrusted-data envelope + tolerant decode) rather than adding a sibling method — both clients `use` it, keeping each client's prompt/schema its own; refine() untouched behaviorally (BriefRefinerTest + OpenRouterProviderPinTest still green). (3) BriefDecision job = RefineBrief pattern (ShouldBeUnique `decision-brief:{id}`, tries=2, timeout=180, default→fast queue). (4) one `getListeners()` entry (DecisionBriefed→onDecisionsChanged). (5) x-decisions.ai-brief light fence, surface-suffixed ids, aria-busy/live/controls; every loading directive `wire:target="briefMe({id})"`-scoped. (6) BriefsDecisionsWithAi sibling concern; panel open = per-decision boolean, never wire:loading. ## Judgment calls - **Cost source (open Q):** request sends `usage:{include:true}` and stores OpenRouter's real `usage.cost`; falls back to token×rate via UsagePricer::match() (deepseek isn't in the pricing table, so the estimate path would read $0 — hence preferring the real charge). - **Broadcast channels:** DecisionBriefed uses `DecisionChannels::for()` (decisions + project + briefs) for parity with the posted/answered/released siblings; the board only listens on `decisions`, the extra lanes are inert no-ops. - **Detail/deep-link mirror:** wired BriefsDecisionsWithAi into `Show` too so the flyout + `/decisions/{id}` Brief me actually work (not just render); in the depth view a generated briefing reveals on load. - **Deferred (not in the 6 scope items):** the design's faint "N links added since — re-brief?" staleness note. Timestamps are enough to add it later (DecisionLink.ts vs ai_briefed_at) without a new column; left out to hold the line budget. Considerations render as plain bullets — the recommended-option "amber ring" already lives on the affordance above, so the panel highlights in prose (per the model's output) rather than adding a second color signal. - **Error UX:** terminal job failure (both tries) writes `ai_brief={error:…}` + broadcasts, so the panel flips from skeleton→danger line + "Try again" (=rebrief) instead of a stuck skeleton.

    agent · flower-223-worker
  8. participant joined 1d ago
    system · flower-223-worker
  9. link added 1d ago
    agent · system:brief-autolink
  10. link added 1d ago
    agent · system:brief-autolink
  11. link added 1d ago
    agent · system:brief-autolink
  12. link added 1d ago
    agent · system:brief-autolink
  13. link added 1d ago
    agent · system:brief-autolink
  14. link added 1d ago
    agent · system:brief-autolink
  15. comment 1d ago

    Target branch flower/223-decisions-brief-me is merged to the default branch; suggest marking the brief complete.

    system · system:brief-autolink
  16. participant joined 1d ago
    system · system:brief-autolink
  17. dispatched 1d ago

    Dispatch request #107 queued for flower.

    agent · flower-orchestrator
  18. status change 1d ago
    agent · flower-orchestrator
  19. status change 1d ago
    agent · flower-orchestrator
  20. participant joined 1d ago
    system · flower-orchestrator
  21. unblocked 1d ago

    Unblocked — #222 reached complete.

    system · flower-222-worker
  22. participant joined 1d ago
    system · flower-222-worker
  23. dependency added 1d ago

    Now depends on #222 ([#216 PR-3a] /decisions Depth (structured, no AI) — decisionSummary brief-web + grouped linked-context).

    agent · flower-216-worker
  24. parent set 1d ago

    Grouped under epic #216.

    agent · flower-216-worker
  25. note added 1d ago

    Child of Brief #216. Design doc: `docs/design/216-decisions-feed-redesign.md` §"The Brief me AI button" + §"Sequenced delivery" PR-3b. Depends on PR-3a (#222) — builds on its `DecisionBriefContext` assembler. Honors operator Q52: AI is an OPTIONAL on-demand button, RAG-fed by flower's OWN recall (highlights, does not fetch/decide). ## Scope (≈ ≤300 lines) 1. **Migration** — nullable columns on `decisions`: `ai_brief` (json), `ai_briefed_at`, `ai_brief_cost`, `ai_brief_model`. 2. **Completion helper** — extract the private `sendJsonCompletion($base,$body)` core (the `Http::withToken → /chat/completions` call + provider pin + graceful-degrade-on-`response_format`-reject retry + untrusted-data hardening) from `OpenRouterBriefRefinementClient` into a small shared helper. Do NOT call `refine()` (it's DTO/schema-bound). Equivalent alt: add a sibling `briefDecision(DecisionBriefRequest): array` on the client. Model selection follows the brief chain → default `deepseek/deepseek-v4-flash`. 3. **`BriefDecision` queued job** — match `RefineBrief` pattern (`ShouldBeUnique`, `uniqueId='decision-brief:'.$id`, `tries=2`, `timeout=180`) on the fast queue. Consumes PR-3a's `DecisionBriefContext` (7 buckets), builds a `{model, messages, max_tokens, temperature, response_format: json_schema(tldr/bullets/considerations)}` body wrapping all assembled context as delimited UNTRUSTED data. System prompt: "brief the operator on ONE pending decision using ONLY the context below; highlight what changed / why now / what each option implies; do not invent/fetch/assume; if context is thin, say so." Store result+cost+model+timestamp on the decision, then broadcast `DecisionBriefed` on the EXISTING `decisions` channel. 4. **Realtime wiring (one line)** — add `'echo-private:decisions,.'.DecisionBriefed::class => 'onDecisionsChanged'` to `Index::getListeners()` (`app/Livewire/Decisions/Index.php:43`) — it enumerates by FQCN, not wildcard, so a new event is NOT picked up automatically. `wire:poll.30s` is the fallback. 5. **`x-decisions.ai-brief` panel** — light fence (`mt-3 border-t border-border/70 pt-3` + `mono-label` "ai briefing" eyebrow, NOT a nested `x-ui.card`). States: idle/loading/cached/error. `:surface` prop (`'glance'|'detail'`) suffixes DOM id → `ai-brief-glance-{id}` / `ai-brief-detail-{id}` (the flyout renders in-DOM alongside the card, so a bare id collides). `aria-busy` + `aria-live="polite"` + `aria-controls` to the surface-suffixed id. Cached structure: TL;DR / What changed–why now / Considerations (tied to actual options; recommended option echoes the affordance's amber ring, never color-only). 6. **`Concerns/BriefsDecisionsWithAi` host concern** — new SIBLING of `AnswersDecisions` (never an edit to it). Method `briefMe($decisionId)` dispatches the job + sets a per-decision boolean for panel open/closed. ## Critical constraints - **Scope every loading/disabled directive** to `wire:target="briefMe({id})"` — the board is a single Livewire component; a bare `wire:loading` + `wire:poll.30s` would flip EVERY card's button to "Briefing…" every 30s. Panel open/closed driven by the `briefMe()` boolean, NOT `wire:loading`. - **Toggle vs re-brief:** fresh → "Brief me" opens loading; cached → same button is a plain show/hide (spends nothing); re-running the model ONLY via the foot "re-brief" link. - **Button styling:** text-link tier (`mono-label text-fg-faint hover:text-accent` + `bolt` glyph), matching row peers "View more"/"Open in new tab ↗" — NOT a bordered pill (Q52: AI not the centerpiece). - **Cost/latency signal** visible before (button title ~$0.002), during (foot "deepseek-v4-flash · ~$0.002 · usually 5–15s"), after (foot stamp with actual cost). - Model highlights, never auto-answers; the affordance is the only thing that records a call. Answer path frozen (review gate: `affordance.blade.php` + `AnswersDecisions.php` unchanged). ## Acceptance Clicking Brief me on one card briefs only that card (no cross-card flip on poll/answer); result caches on the decision + shows cost/latency; error degrades with retry; re-brief re-dispatches; standalone/no-context → disabled with a reason; `php artisan test` green; `pint` clean.

    agent · flower-216-worker
  26. participant joined 1d ago
    system · flower-216-worker

epic · dependencies

Relationships

depends on

agents · waves

Participants

  • flower-216-worker participant · active
  • flower-222-worker participant · active
  • flower-orchestrator participant · active
  • system:brief-autolink participant · active
  • flower-223-worker participant · active
  • system:commit-trailer participant · active

trace · graph

Links

  • Commit #4018 execution

scope

Projects

  • flower · primary

dogfood · read-only

Agent’s-eye view

The literal recall_brief payload an agent gets — same service path as the MCP tool.