feat(coach): scope filter + conversation provenance on the plans list

Groundwork for the plan confirm/manage UI. The CoachPlan pipeline has been
complete on the server since v1.21.3/v1.22 — extractor writes proposals
(status "proposed", per-user cap, de-dup, encrypted prose), the PATCH
confirms proposed -> active, buildCoachPlansBlock injects the top-6 active
plans into the snapshot, and the review sweep drives the reviewDate
lifecycle — but no surface ever listed or confirmed a proposal, so the
extraction spend had no user-visible outcome.

Two additive contract changes the UI needs:
- GET /api/coach/plans accepts ?scope=open|past|all (open = proposed +
  active + review_due, past = met + abandoned + reviewed, all = no status
  clause) so the management page reads its ledger in one round-trip.
  Mutually exclusive with ?status=; both omitted keeps today's
  non-terminal default unchanged.
- Plan items now carry sourceConversationId so the chat thread can show
  only the proposals born in the open conversation.

OpenAPI regenerated; route tests cover the scope mappings, the
status+scope 422, and the provenance passthrough.
This commit is contained in:
Marc-André Bombeck committed 2026-07-05 19:45:20 +02:00
1 parent 4d0f16d5a9
commit acd2f29da2
6 files changed
+152 -16

No files matched your search

+25 -5
View File
@@ -5205,10 +5205,11 @@ paths:
description: v1.21.3 (B1) — returns the durable plans the Coach has proposed for the caller, newest first, each
decrypted on the fly. A plan is an "if-then" implementation intention tied to one metric, with an optional
target. The Coach extractor writes a plan as `proposed`; only `PATCH /api/coach/plans/{id}` activates it. Pass
`?status=` to filter (proposed | active | met | abandoned); omitted returns the non-terminal set (proposed +
active). Coach-gated (`requireModuleEnabled("coach")`); a disabled surface 403s. Auth via cookie or Bearer; the
owner is always narrowed from the session, never the body. Undecryptable rows are omitted rather than failing
the read.
`?status=` to filter to one lifecycle status, or `?scope=` for a named group (open = proposed + active +
review_due, past = met + abandoned + reviewed, all = every non-deleted plan) — mutually exclusive. Both omitted
returns the non-terminal set (proposed + active). Coach-gated (`requireModuleEnabled("coach")`); a disabled
surface 403s. Auth via cookie or Bearer; the owner is always narrowed from the session, never the body.
Undecryptable rows are omitted rather than failing the read.
parameters:
- name: status
in: query
@@ -5222,7 +5223,19 @@ paths:
- abandoned
- review_due
- reviewed
description: Filter to a single lifecycle status. Omit for the non-terminal set (proposed + active).
description: Filter to a single lifecycle status. Mutually exclusive with `scope`. Omit both for the non-terminal set
(proposed + active).
- name: scope
in: query
required: false
schema:
type: string
enum:
- open
- past
- all
description: "Named status group: open (proposed + active + review_due), past (met + abandoned + reviewed), or all.
Mutually exclusive with `status`."
responses:
"200":
description: The caller's plans.
@@ -20461,6 +20474,12 @@ components:
pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$
- type: "null"
description: Optional check-in checkpoint; null when none.
sourceConversationId:
anyOf:
- type: string
- type: "null"
description: Id of the conversation the proposal was extracted from; null for a plan without conversation provenance.
Lets a chat surface show only the proposals born in the open thread.
createdAt:
type: string
format: date-time
@@ -20477,6 +20496,7 @@ components:
- target
- status
- reviewDate
- sourceConversationId
- createdAt
- updatedAt
additionalProperties: false