Summary
Implement M5: end-to-end management report synthesis from confirmed ledger entries — skill invocation, audit chain persistence, and Slack channel delivery.
Design reference: docs/DESIGN.md §5.4 (Synthesizer), §7.3–7.4 (contracts), §8 (Skills integration)
Current state (partial implementation)
| Component |
Status |
skills/weekly-status-synthesizer/SKILL.md |
Exists — rules ported from weekly_status-main/.claude/commands/format-status.md |
src/status/skills/synthesizer.py |
Stub — build_synthesis_input() + run_synthesizer() with dry-run fallback |
src/status/cli.py status report |
Exists — --dry-run defaults True; writes markdown to stdout or -o file |
report_run / report_entry tables |
Schema exists in models.py + Alembic — not written to yet |
| Slack report delivery |
Not implemented |
| Skill publish |
status skills publish --skill synthesizer works if SYNTHESIZER_SKILL_ID unset (creates new) |
Known bugs to fix in M5
In src/status/skills/synthesizer.py:
display_name=entry.person_id — should use Person.display_name from DB join
flags=[] hardcoded — should include unacknowledged flag rows for the week
- No persistence to
report_run / report_entry after skill returns
Tasks
1. Publish synthesizer skill
status skills publish --skill synthesizer
# Set SYNTHESIZER_SKILL_ID and pin SYNTHESIZER_SKILL_VERSION in .env / OpenShift Secret
Validate against fixtures built from confirmed ledger rows (create fixtures/synthesis-input-*.json from real confirmed data — scrub PII, gitignore if needed).
2. Complete build_synthesis_input()
- Join
Person for display_name
- Load unacknowledged flags:
Flag where week_ending matches and acknowledged = false
- Include all
participation rows for the week (confirmed, expired, sent, send_failed, on_leave)
- Only include
status_entry where is_current AND confirmed_at IS NOT NULL
Repo helpers exist in src/status/db/repo.py:
get_confirmed_entries_for_week
get_participation_for_week
3. Persist audit chain
After successful skill invocation, write:
report_run:
week_ending, prompt_version, model, generated_at
output_uri — optional path or object store URI if saving markdown file
delivered_at — set when posted to Slack
report_entry:
- One row per cited entry:
(run_id, entry_id, section) where section comes from synthesizer metadata if available
New module suggested: src/status/db/report.py
def persist_report_run(session, week_ending, output: SynthesisOutput, *, model, prompt_version) -> ReportRun:
...
4. Wire status report CLI
- Change default
--dry-run to False when SYNTHESIZER_SKILL_ID is set (keep --dry-run flag for testing)
- Add
--deliver flag to post markdown to REPORT_CHANNEL_ID via Slack Web API
- Add
--persist (default true) to write report_run rows
5. Slack delivery
Post synthesized markdown to private channel (REPORT_CHANNEL_ID):
- Use
slack_sdk.WebClient.chat_postMessage with mrkdwn text
- Long reports may need splitting (Slack 40k char limit) — split on section headers if needed
- Set
report_run.delivered_at on success
Optional: attach as snippet or upload file if formatting breaks in chat.
6. Synthesis validation
SynthesisOutput schema in src/status/skills/schemas.py — ensure Pydantic validates:
markdown non-empty
week_ending matches input
- Metadata fields:
sections_used, entries_cited, non_responders, asks
On validation failure: do not persist partial report; surface error to CLI.
7. Tests
tests/test_synthesizer.py — build input from fixture DB session (SQLite or mocked)
- Test dry-run path unchanged
- Test
report_run + report_entry rows created on persist
- Golden test: synthesis input JSON shape, not markdown prose
Synthesis rules (skill must enforce)
From design doc / SKILL.md:
- Sections: Partner Enablement, Certification / CI, Mindshare
- Format:
* **Name** - description with links on noun phrases
- No raw Jira IDs in visible text
- Alphabetical within sections
- Non-responders explicit
- Every
ask from ledger appears in report
- Organize by epic/project, not by person
Acceptance criteria
Branch / PR
- Branch:
feat/m5-synthesizer
- Base:
main
- PR title:
feat(synthesizer): management report generation and delivery (M5)
Depends on
- M0–M3 on
main (ledger + confirm flow)
- At least one week of confirmed entries in Postgres for integration testing
- M4 not strictly required but improves data quality before first real report
Out of scope (M6 / Phase 2)
- Monday 09:00 auto-expire unconfirmed (
participation.status = 'expired') — M6
- CronJob
lock-and-report
- DOCX export / email (
weekly_status-main commands)
Summary
Implement M5: end-to-end management report synthesis from confirmed ledger entries — skill invocation, audit chain persistence, and Slack channel delivery.
Design reference:
docs/DESIGN.md§5.4 (Synthesizer), §7.3–7.4 (contracts), §8 (Skills integration)Current state (partial implementation)
skills/weekly-status-synthesizer/SKILL.mdweekly_status-main/.claude/commands/format-status.mdsrc/status/skills/synthesizer.pybuild_synthesis_input()+run_synthesizer()with dry-run fallbacksrc/status/cli.pystatus report--dry-rundefaults True; writes markdown to stdout or-ofilereport_run/report_entrytablesmodels.py+ Alembic — not written to yetstatus skills publish --skill synthesizerworks ifSYNTHESIZER_SKILL_IDunset (creates new)Known bugs to fix in M5
In
src/status/skills/synthesizer.py:display_name=entry.person_id— should usePerson.display_namefrom DB joinflags=[]hardcoded — should include unacknowledgedflagrows for the weekreport_run/report_entryafter skill returnsTasks
1. Publish synthesizer skill
status skills publish --skill synthesizer # Set SYNTHESIZER_SKILL_ID and pin SYNTHESIZER_SKILL_VERSION in .env / OpenShift SecretValidate against fixtures built from confirmed ledger rows (create
fixtures/synthesis-input-*.jsonfrom real confirmed data — scrub PII, gitignore if needed).2. Complete
build_synthesis_input()Personfordisplay_nameFlagwhereweek_endingmatches andacknowledged = falseparticipationrows for the week (confirmed, expired, sent, send_failed, on_leave)status_entrywhereis_current AND confirmed_at IS NOT NULLRepo helpers exist in
src/status/db/repo.py:get_confirmed_entries_for_weekget_participation_for_week3. Persist audit chain
After successful skill invocation, write:
report_run:week_ending,prompt_version,model,generated_atoutput_uri— optional path or object store URI if saving markdown filedelivered_at— set when posted to Slackreport_entry:(run_id, entry_id, section)wheresectioncomes from synthesizer metadata if availableNew module suggested:
src/status/db/report.py4. Wire
status reportCLI--dry-runto False whenSYNTHESIZER_SKILL_IDis set (keep--dry-runflag for testing)--deliverflag to post markdown toREPORT_CHANNEL_IDvia Slack Web API--persist(default true) to writereport_runrows5. Slack delivery
Post synthesized markdown to private channel (
REPORT_CHANNEL_ID):slack_sdk.WebClient.chat_postMessagewithmrkdwntextreport_run.delivered_aton successOptional: attach as snippet or upload file if formatting breaks in chat.
6. Synthesis validation
SynthesisOutputschema insrc/status/skills/schemas.py— ensure Pydantic validates:markdownnon-emptyweek_endingmatches inputsections_used,entries_cited,non_responders,asksOn validation failure: do not persist partial report; surface error to CLI.
7. Tests
tests/test_synthesizer.py— build input from fixture DB session (SQLite or mocked)report_run+report_entryrows created on persistSynthesis rules (skill must enforce)
From design doc /
SKILL.md:* **Name** - descriptionwith links on noun phrasesaskfrom ledger appears in reportAcceptance criteria
status report --week 2026-08-14invokes skill when configured (not dry-run)report_runandreport_entrypopulated with audit chain--deliverposts toREPORT_CHANNEL_IDpytest tests/test_synthesizer.pyBranch / PR
feat/m5-synthesizermainfeat(synthesizer): management report generation and delivery (M5)Depends on
main(ledger + confirm flow)Out of scope (M6 / Phase 2)
participation.status = 'expired') — M6lock-and-reportweekly_status-maincommands)