Briefing: split "to-dos" (act) from "topics" (be-aware)
Context
The daily briefing is the user's morning surface. A reference AI-inbox product
opens with two distinct buckets — a short "suggested to-dos" list (items that need
the user to act) above a longer "topics to catch up on" list (awareness-only,
grouped by area). SkyTwin's briefing produces prose organized by Meetings / Tasks /
Signals importance and never makes the act-vs-aware distinction explicit. That
distinction is the decision engine's core job: a signal that generated an
actionable CandidateAction is a to-do; a signal that did not is FYI. Surfacing it
costs almost nothing because the data already exists upstream.
Current State
Verified 2026-06-06.
packages/policy-prompts/prompts/briefing-prose/v1.md:47-51 — the output
constraints are: open with a one-sentence summary; ## headers for Meetings,
Tasks, Signals; bold the single most important item; return valid JSON. There is
no rule that separates action-required from awareness-only, and no notion of "this
signal produced a candidate action."
packages/policy-prompts/prompts/briefing-prose/v1.md:40-45 — output schema is
{ "briefing": string (Markdown, ≤600 words), "highlight_count": number }.
packages/policy-prompts/prompts/briefing-prose/v1.md:11 — deterministic fallback
is pass-through (no LLM → raw passthrough of inputs).
packages/db/src/repositories/briefing-repository.ts — persists daily/weekly
briefings; packages/db/src/migrations/042-briefing-domain.sql adds per-Lifebook
domain scoping. Storage is prose-blob shaped (the briefing string), so a
structured to-do/FYI split needs either new columns or a structured payload field.
- The decision pipeline already distinguishes actionable from non-actionable:
packages/decision-engine/src/decision-maker.ts produces CandidateAction[] and a
DecisionOutcome with autoExecute / requiresApproval.
- Gap found during review (the main work of this spec): the briefing generator
(apps/worker/src/jobs/briefing-generator.ts) does NOT currently read decision
outcomes at all. Its inputs are appSuggestionRepository.getPendingForUser() and a
tier-promotionResult query — there is no join to DecisionOutcome /
CandidateAction. So "tag each item with actionRequired" is not a free derivation
today; it requires a NEW query/data-flow from the decisions store into the briefing
generator. This is a real architectural step, not a relabel. The implementation must
define that query (see Implementation Details step 0).
Proposed Change
Make the briefing emit two explicit, ordered sections:
- To-dos — signals whose decision outcome is "action required from the user"
(escalated-to-approval candidate actions, or signals the engine flagged
actionable but could not auto-resolve). Each line: one-sentence imperative +
source reference + (if present) deadline.
- Topics to catch up on — everything else, grouped by domain, awareness-only.
The classifier is upstream data, not new LLM judgment: a briefing input item is a
to-do iff it carries an associated CandidateAction that did NOT auto-execute
(i.e. requires user action). This is a deterministic mapping from existing pipeline
output.
Implementation Details
-
Source the decision outcomes (do this first — it's the blocker). Add a query
that fetches, for the briefing window, each signal's DecisionOutcome
(autoExecute / requiresApproval) from the decisions store, and join it to the
briefing inputs in briefing-generator.ts. actionRequired = requiresApproval === true (escalated to the user). Decide the join key (signal id / decision id) and
whether this is a new repository method on the decisions repo. Without this step
the rest of the spec has no actionRequired to render.
-
New prompt version packages/policy-prompts/prompts/briefing-prose/v2.md
(do not mutate v1 — prompts are versioned per the package contract). Changes vs v1:
- Input gains a per-item
actionRequired: boolean and optional deadline and
sourceRef fields.
- Output schema becomes:
{
"briefing": "string (Markdown, ≤600 words)",
"todos": [{ "text": "string", "sourceRef": "string", "deadline": "string|null" }],
"topics": [{ "domain": "string", "items": [{ "text": "string", "sourceRef": "string" }] }],
"highlight_count": "number"
}
- Constraints: To-dos section first, max 7 items, each a single imperative
sentence. Topics second, grouped by domain. Never put an actionRequired:true
item in topics. Deterministic fallback: render to-dos as a bullet list, topics
grouped by domain, no prose.
-
Caller wiring — the briefing generator (the code that fills {{events}} /
{{pending_tasks}}) tags each input item with actionRequired derived from the
item's decision outcome. Source: whether the signal produced an escalated
CandidateAction. Pure boolean derivation, no model call.
-
Storage — add a nullable structured_payload JSONB column to the briefing
table (new migration) holding { todos, topics }. Keep the briefing prose
column for back-compat and plain-text/email rendering.
-
Render — web/mobile briefing views read structured_payload when present,
render the two-section layout; fall back to the prose blob when null (old rows).
Acceptance Criteria
- A briefing generated from a fixture containing 3 actionable signals and 7
awareness signals renders exactly 3 items under "To-dos" and 7 under "Topics",
with zero actionable items appearing in Topics.
- No item appears in both sections (dedup by sourceRef).
- To-dos section is rendered above Topics in both web and mobile.
- When the LLM is unavailable, the deterministic fallback still produces the
two-section split (it is a data partition, not a generative step).
- Old briefing rows with
structured_payload = NULL still render via the prose
blob (no regression for historical briefings).
- v1 prompt is untouched and still selectable.
- Tests written and passing.
- No degradation of existing functionality.
Testing Plan
| Layer |
What |
Count |
| Unit |
actionRequired derivation from decision outcome; partition logic; dedup by sourceRef |
+6 |
| Unit |
Deterministic fallback produces both sections |
+2 |
| Integration |
Briefing generator → v2 prompt → structured_payload persisted |
+2 |
| Integration |
Render path: structured payload present vs. NULL (back-compat) |
+2 |
Rollback Plan
The change is additive: v2 prompt is a new file, structured_payload is nullable,
renderers fall back to the prose blob. Roll back by pointing the briefing generator
at briefing-prose v1 and ignoring structured_payload. No data migration to
reverse; the new column can stay unused.
Effort Estimate
- v2 prompt + schema: ~2h
- Caller wiring (
actionRequired derivation): ~2h
- Migration + repository field: ~1h
- Web + mobile render: ~3h
- Tests: ~2h
Total: ~1 day.
Files Reference
| File |
Change |
packages/policy-prompts/prompts/briefing-prose/v2.md |
New: two-section prompt + schema |
packages/policy-prompts/prompts/briefing-prose/v1.md |
Unchanged (versioned, keep) |
packages/db/src/migrations/0NN-briefing-structured-payload.sql |
New: nullable structured_payload JSONB |
packages/db/src/repositories/briefing-repository.ts |
Read/write structured_payload |
briefing generator (caller filling {{events}}/{{pending_tasks}}) |
Tag items with actionRequired |
| web + mobile briefing views |
Two-section render with NULL fallback |
Out of Scope
- Generating new candidate actions for awareness items (that's the act layer).
- Topic grouping quality inside the Topics section — clustering is spec 04.
- Deadline extraction to populate the
deadline field — spec 03.
Related
- 02 (commitment extraction) and 03 (deadline extraction) enrich the To-dos bucket.
- 04 (topic clustering) reshapes the Topics bucket.
Briefing: split "to-dos" (act) from "topics" (be-aware)
Context
The daily briefing is the user's morning surface. A reference AI-inbox product
opens with two distinct buckets — a short "suggested to-dos" list (items that need
the user to act) above a longer "topics to catch up on" list (awareness-only,
grouped by area). SkyTwin's briefing produces prose organized by Meetings / Tasks /
Signals importance and never makes the act-vs-aware distinction explicit. That
distinction is the decision engine's core job: a signal that generated an
actionable
CandidateActionis a to-do; a signal that did not is FYI. Surfacing itcosts almost nothing because the data already exists upstream.
Current State
Verified 2026-06-06.
packages/policy-prompts/prompts/briefing-prose/v1.md:47-51— the outputconstraints are: open with a one-sentence summary;
##headers for Meetings,Tasks, Signals; bold the single most important item; return valid JSON. There is
no rule that separates action-required from awareness-only, and no notion of "this
signal produced a candidate action."
packages/policy-prompts/prompts/briefing-prose/v1.md:40-45— output schema is{ "briefing": string (Markdown, ≤600 words), "highlight_count": number }.packages/policy-prompts/prompts/briefing-prose/v1.md:11— deterministic fallbackis
pass-through(no LLM → raw passthrough of inputs).packages/db/src/repositories/briefing-repository.ts— persists daily/weeklybriefings;
packages/db/src/migrations/042-briefing-domain.sqladds per-Lifebookdomain scoping. Storage is prose-blob shaped (the
briefingstring), so astructured to-do/FYI split needs either new columns or a structured payload field.
packages/decision-engine/src/decision-maker.tsproducesCandidateAction[]and aDecisionOutcomewithautoExecute/requiresApproval.(
apps/worker/src/jobs/briefing-generator.ts) does NOT currently read decisionoutcomes at all. Its inputs are
appSuggestionRepository.getPendingForUser()and atier-
promotionResultquery — there is no join toDecisionOutcome/CandidateAction. So "tag each item withactionRequired" is not a free derivationtoday; it requires a NEW query/data-flow from the decisions store into the briefing
generator. This is a real architectural step, not a relabel. The implementation must
define that query (see Implementation Details step 0).
Proposed Change
Make the briefing emit two explicit, ordered sections:
(escalated-to-approval candidate actions, or signals the engine flagged
actionable but could not auto-resolve). Each line: one-sentence imperative +
source reference + (if present) deadline.
The classifier is upstream data, not new LLM judgment: a briefing input item is a
to-do iff it carries an associated
CandidateActionthat did NOT auto-execute(i.e. requires user action). This is a deterministic mapping from existing pipeline
output.
Implementation Details
Source the decision outcomes (do this first — it's the blocker). Add a query
that fetches, for the briefing window, each signal's
DecisionOutcome(
autoExecute/requiresApproval) from the decisions store, and join it to thebriefing inputs in
briefing-generator.ts.actionRequired = requiresApproval === true(escalated to the user). Decide the join key (signal id / decision id) andwhether this is a new repository method on the decisions repo. Without this step
the rest of the spec has no
actionRequiredto render.New prompt version
packages/policy-prompts/prompts/briefing-prose/v2.md(do not mutate v1 — prompts are versioned per the package contract). Changes vs v1:
actionRequired: booleanand optionaldeadlineandsourceReffields.{ "briefing": "string (Markdown, ≤600 words)", "todos": [{ "text": "string", "sourceRef": "string", "deadline": "string|null" }], "topics": [{ "domain": "string", "items": [{ "text": "string", "sourceRef": "string" }] }], "highlight_count": "number" }sentence. Topics second, grouped by domain. Never put an
actionRequired:trueitem in topics. Deterministic fallback: render to-dos as a bullet list, topics
grouped by domain, no prose.
Caller wiring — the briefing generator (the code that fills
{{events}}/{{pending_tasks}}) tags each input item withactionRequiredderived from theitem's decision outcome. Source: whether the signal produced an escalated
CandidateAction. Pure boolean derivation, no model call.Storage — add a nullable
structured_payload JSONBcolumn to the briefingtable (new migration) holding
{ todos, topics }. Keep thebriefingprosecolumn for back-compat and plain-text/email rendering.
Render — web/mobile briefing views read
structured_payloadwhen present,render the two-section layout; fall back to the prose blob when null (old rows).
Acceptance Criteria
awareness signals renders exactly 3 items under "To-dos" and 7 under "Topics",
with zero actionable items appearing in Topics.
two-section split (it is a data partition, not a generative step).
structured_payload = NULLstill render via the proseblob (no regression for historical briefings).
Testing Plan
actionRequiredderivation from decision outcome; partition logic; dedup by sourceRefRollback Plan
The change is additive: v2 prompt is a new file,
structured_payloadis nullable,renderers fall back to the prose blob. Roll back by pointing the briefing generator
at
briefing-prosev1 and ignoringstructured_payload. No data migration toreverse; the new column can stay unused.
Effort Estimate
actionRequiredderivation): ~2hTotal: ~1 day.
Files Reference
packages/policy-prompts/prompts/briefing-prose/v2.mdpackages/policy-prompts/prompts/briefing-prose/v1.mdpackages/db/src/migrations/0NN-briefing-structured-payload.sqlstructured_payload JSONBpackages/db/src/repositories/briefing-repository.tsstructured_payload{{events}}/{{pending_tasks}})actionRequiredOut of Scope
deadlinefield — spec 03.Related