Summary
Implement M4: Slack Edit and Regenerate flows. M3 stubs these with ephemeral placeholders in src/status/slack/handlers.py (lines 107–123). After M4, users can correct drafts without leaving Slack; edits create new ledger revisions with source = 'drafted_edited'.
Design reference: docs/DESIGN.md §5.3 (Slackbot)
Current stubs (replace these)
# src/status/slack/handlers.py
@app.action(ACTION_EDIT)
def on_edit(...):
ack()
client.chat_postEphemeral(..., text="Edit flow is coming in the next milestone.")
@app.action(ACTION_REGENERATE)
def on_regenerate(...):
ack()
client.chat_postEphemeral(..., text="Regenerate flow is coming in the next milestone.")
Action IDs (already wired in blocks): status_edit, status_regenerate — see src/status/slack/blocks.py.
Edit flow requirements
UX (from design doc)
- User clicks Edit on draft DM
- Within 3 seconds:
ack() then views.open with modal (must open before slow DB reads — trigger_id expires in 3s)
- Modal fields:
- One editable text field per epic (pre-filled with
outcome)
- Checkbox per epic to drop entry from draft
- Free-text: unticketed work (from
unticketed_prompt flag if present)
- Free-text: leadership asks (aggregate or per-entry
ask fields)
- On submit:
- For changed outcomes: supersede current row, insert new row with
source = 'drafted_edited', increment revision, set supersedes_entry_id
- For dropped epics: mark current row
is_current = false (or equivalent — preserve audit chain)
- Update Slack message via
chat.update (reuse _update_message)
- Participation stays
sent (not confirmed) until user clicks Looks right
Modal constraints
- Slack modal block limit ~100 blocks (~20 epics max)
- If more epics: cap modal, note remainder in modal description
- Validate non-blank outcomes (
CheckConstraint outcome_not_blank on status_entry)
DB work needed
New module or functions in src/status/db/ (suggested: src/status/db/edit.py):
def apply_edited_entries(
session,
person_id: str,
week_ending: date,
edits: list[EditSpec], # epic_key, new_outcome, drop: bool
*,
unticketed: str | None,
leadership_ask: str | None,
) -> list[StatusEntry]:
...
Reuse patterns from src/status/db/draft.py:
_status_entry_from_draft / revision increment
supersede_unconfirmed_drafts logic for per-epic grain
EntrySource.DRAFTED_EDITED enum already exists in models.py
Preserve: original drafted rows remain in DB (append-only); only is_current flips.
Regenerate flow requirements
UX
- User clicks Regenerate
ack() → open modal with reason select (e.g. "Wrong epic grouping", "Missing work", "Tone/ wording", "Other" + optional text)
- On submit (background task after ack):
- Re-run collector + drafter for
(person_id, week_ending) — reuse run_collect + draft_and_persist from src/status/skills/drafter.py
persist_draft_output already supersedes unconfirmed drafts idempotently
- Update
participation.regenerated = true, regenerate_reason = <reason>
- Replace Slack message with new draft blocks via
chat.update
Slack timing
- Ack interactivity immediately
- Heavy work (collect + skill call) in background thread/async task
- On failure: ephemeral error to user; do not leave message in broken state
Files to create/modify
| File |
Changes |
src/status/slack/handlers.py |
Replace stubs; add modal open/submit handlers |
src/status/slack/blocks.py |
Optional: modal view builders, regenerate reason blocks |
src/status/db/edit.py |
New: revision logic for edited entries |
src/status/db/confirm.py |
Optional: helper to reset participation on regenerate |
tests/test_slack_edit.py |
New: unit tests for edit revision logic |
tests/test_slack_regenerate.py |
New: mock drafter re-run + supersede |
Tests
Follow existing patterns in tests/test_slack_confirm.py and tests/test_draft_persist.py:
- Edit one epic outcome → new row,
source='drafted_edited', old row is_current=false
- Drop epic → no current row for that epic grain
- Regenerate → superseded_count > 0, new drafts, participation.regenerated=true
- Golden tests on JSON/DB shape, not prose wording
Run: pytest tests/test_slack_edit.py tests/test_slack_regenerate.py tests/test_draft_persist.py
Acceptance criteria
Branch / PR
- Branch:
feat/m4-edit-regenerate
- Base:
main (after M3.5 or M0–M3 merge — can parallel with M3.5)
- PR title:
feat(slack): edit and regenerate draft flows (M4)
Depends on
- M0–M3 merged to
main (Slack confirm + draft persistence)
Out of scope
- OpenShift changes (bot Deployment from M3.5 works unchanged)
- Synthesizer / report generation (M5)
Summary
Implement M4: Slack Edit and Regenerate flows. M3 stubs these with ephemeral placeholders in
src/status/slack/handlers.py(lines 107–123). After M4, users can correct drafts without leaving Slack; edits create new ledger revisions withsource = 'drafted_edited'.Design reference:
docs/DESIGN.md§5.3 (Slackbot)Current stubs (replace these)
Action IDs (already wired in blocks):
status_edit,status_regenerate— seesrc/status/slack/blocks.py.Edit flow requirements
UX (from design doc)
ack()thenviews.openwith modal (must open before slow DB reads —trigger_idexpires in 3s)outcome)unticketed_promptflag if present)askfields)source = 'drafted_edited', incrementrevision, setsupersedes_entry_idis_current = false(or equivalent — preserve audit chain)chat.update(reuse_update_message)sent(not confirmed) until user clicks Looks rightModal constraints
CheckConstraint outcome_not_blankonstatus_entry)DB work needed
New module or functions in
src/status/db/(suggested:src/status/db/edit.py):Reuse patterns from
src/status/db/draft.py:_status_entry_from_draft/ revision incrementsupersede_unconfirmed_draftslogic for per-epic grainEntrySource.DRAFTED_EDITEDenum already exists inmodels.pyPreserve: original drafted rows remain in DB (append-only); only
is_currentflips.Regenerate flow requirements
UX
ack()→ open modal with reason select (e.g. "Wrong epic grouping", "Missing work", "Tone/ wording", "Other" + optional text)(person_id, week_ending)— reuserun_collect+draft_and_persistfromsrc/status/skills/drafter.pypersist_draft_outputalready supersedes unconfirmed drafts idempotentlyparticipation.regenerated = true,regenerate_reason = <reason>chat.updateSlack timing
Files to create/modify
src/status/slack/handlers.pysrc/status/slack/blocks.pysrc/status/db/edit.pysrc/status/db/confirm.pytests/test_slack_edit.pytests/test_slack_regenerate.pyTests
Follow existing patterns in
tests/test_slack_confirm.pyandtests/test_draft_persist.py:source='drafted_edited', old rowis_current=falseRun:
pytest tests/test_slack_edit.py tests/test_slack_regenerate.py tests/test_draft_persist.pyAcceptance criteria
test_slack_confirm.pyBranch / PR
feat/m4-edit-regeneratemain(after M3.5 or M0–M3 merge — can parallel with M3.5)feat(slack): edit and regenerate draft flows (M4)Depends on
main(Slack confirm + draft persistence)Out of scope