diff --git a/README.md b/README.md index 414fff7..cecb815 100644 --- a/README.md +++ b/README.md @@ -255,6 +255,7 @@ Codex automations are seeded under `$CODEX_HOME/automations/`: - `studyos-issue-refinement`: find vague or duplicate issues every 6 hours. - `studyos-implementation-candidates`: identify ready work daily, but do not implement unattended. - `studyos-coordinator-thread`: heartbeat automation for a long-lived coordinator thread; replace `REPLACE_WITH_CODEX_THREAD_ID` before enabling. +- `studyos-group-channel-digest`: summarize meaningful group-channel activity into `#updates` daily. - `studyos-weekly-digest`: Thursday 16:00 course progress digest. The image contains tools, not credentials. Do not bake GitHub auth, Codex auth, Claude auth, SSH keys, or Discord tokens into the image. diff --git a/codex/automations/studyos-group-channel-digest/automation.toml b/codex/automations/studyos-group-channel-digest/automation.toml new file mode 100644 index 0000000..36bf13d --- /dev/null +++ b/codex/automations/studyos-group-channel-digest/automation.toml @@ -0,0 +1,126 @@ +version = 1 +id = "studyos-group-channel-digest" +kind = "cron" +name = "StudyOS group channel digest" +prompt = """ +Run a daily human-in-the-loop StudyOS Discord group summary review. + +Configuration lives in this automation TOML under [config]. Use the configured +StudyOS guild_id as the only Discord server. Discover channels inside that +guild on every run, but only treat text channels whose normalized channel names +match group_channel_name_globs as source channels. Do not infer group channels +from categories, vague names, or broad project/team keywords. + +Workflow: +- Read this TOML from $CODEX_HOME/automations/studyos-group-channel-digest/automation.toml. +- Require DISCORD_TOKEN. Never print or store the token. +- Call the Discord REST API directly: + - GET /users/@me/guilds to confirm the bot can access guild_id. + - GET /guilds/{guild_id}/channels to list StudyOS channels. +- Build a channel inventory with category name, channel name, type, and ID. + Consider only text channels that the bot can read and send messages in. +- Identify source group channels only by matching channel names against + group_channel_name_globs. For now this must mean channels named group-*. +- Identify the destination channel as destination_channel_id when present, or + by exact normalized name match against destination_channel_name_candidates. + The intended destination is the StudyOS #updates channel. If no single + destination is found, do not post; report likely candidate channels in the + automation output. +- First check each group channel for replies to pending proposals recorded in + pending_update_summaries. If a non-bot channel member explicitly approves + sharing with wording like "approve", "approved", "share", "ship it", + "post to updates", or "yes, share", post the approved summary to #updates. + If members reject or ask for edits, do not post to #updates; leave the + proposal pending unless the group clearly rejects it. +- For each discovered source channel, fetch messages with + GET /channels/{channel_id}/messages?limit=100 when no last message ID exists, + or GET /channels/{channel_id}/messages?after={last_message_id}&limit=100 when + last_scanned_message_ids has a value for that channel. +- Ignore bot messages unless they contain necessary context for a human request. +- Summarize only meaningful human discussion. +- Before proposing a new summary, check whether the channel still has ongoing + engagement. Treat a channel as actively engaged when recent non-bot messages + inside ongoing_engagement_window_minutes show an unresolved back-and-forth, + unanswered question, active planning, or any discussion that would be + disrupted by a summary proposal. If there is ongoing engagement, do not post + a proposal in that channel during this run. +- When holding off because of ongoing engagement, schedule exactly one deferred + summary-proposal check for that channel at a later time between the hold-off + time and the next regular cron fire. Prefer the midpoint, bounded by + deferred_proposal_min_delay_minutes after the hold-off time. Record or update + that deferred check in deferred_summary_proposals so repeated cron runs do + not stack duplicate reminders for the same channel and activity window. + +Group proposal workflow: +- Do not publish new group summaries directly to #updates. +- For each group channel with meaningful new activity and no unresolved pending + proposal and no ongoing engagement, generate a short proposed update summary + and post it into that same group channel first. +- Ask clearly whether the group would like to share the summary to #updates + now. Tell members to reply with an explicit approval phrase if they accept, + or edits/rejection if they do not. +- Record the posted proposal in pending_update_summaries with source channel + ID, proposal message ID, newest fetched message ID, proposed summary text, + and status. After posting a proposal, update last_scanned_message_ids for + that source channel so the same activity is not proposed again. + +Summary shape: +- Group by source channel name. +- Include decisions or emerging project direction. +- Include blockers or open questions. +- Include requests that would benefit from an issue, PR, or agent follow-up. +- Include cross-group context useful to the wider StudyOS course. + +Safety and noise rules: +- Do not include secrets, credentials, private personal data, or long quotes. +- Do not mention individual students unless the source discussion clearly needs + a named owner or reviewer. +- If there is no meaningful new group activity and no approval/rejection on + pending proposals, do not post. +- Do not send more than one new proposal per group channel per run. +- Never create issues, branches, pull requests, or merge pull requests. + +Posting to #updates: +- Post to #updates only after a group channel member explicitly approves a + pending proposal in that group channel. +- When posting to #updates, include the source channel name and the approved + summary. Keep it concise. +- After a successful #updates post, mark the pending proposal as shared and + preserve all other fields. If posting fails, report the failure in the + automation output and leave the proposal pending. +""" +status = "PAUSED" +rrule = "RRULE:FREQ=DAILY;BYHOUR=17;BYMINUTE=0" +model = "gpt-5.5" +reasoning_effort = "medium" +execution_environment = "local" +cwds = ["/workspaces"] + +[config] +guild_id = "1501971751247024228" +destination_channel_id = "1501974151165116637" +destination_channel_name_candidates = [ + "updates", +] +group_channel_name_globs = ["group-*"] +excluded_channel_name_patterns = [ + "admin", + "bot", + "log", + "archive", + "announcement", + "digest", +] +approval_keywords = [ + "approve", + "approved", + "share", + "ship it", + "post to updates", + "yes, share", +] +ongoing_engagement_window_minutes = 90 +deferred_proposal_min_delay_minutes = 60 +last_scanned_message_ids = {} +pending_update_summaries = {} +deferred_summary_proposals = {} diff --git a/codex/automations/studyos-group-channel-digest/memory.md b/codex/automations/studyos-group-channel-digest/memory.md new file mode 100644 index 0000000..0b9530f --- /dev/null +++ b/codex/automations/studyos-group-channel-digest/memory.md @@ -0,0 +1,22 @@ +# StudyOS group channel digest + +This automation is intended for Codex app automation runners, not plain CLI +startup hooks. + +Run a daily human-in-the-loop summary review for StudyOS Discord channels named +`group-*`. + +Behavior: +- Generate proposed summaries from `group-*` channels only. +- Post proposals back into the source group channel first. +- Share to `#updates` only after a non-bot group channel member explicitly + approves the pending proposal. +- Do not interrupt ongoing channel engagement with a new proposal; schedule one + deferred proposal check between the hold-off time and the next cron fire. + +Human policy: +- Students approve and merge pull requests. +- Implementation starts only after a human explicitly asks for it in Discord or + a GitHub issue comment. +- Do not close issues or PRs unless explicitly asked. +- Prefer issue refinement and PR review summaries before implementation. diff --git a/docs/studyos-collaboration-workflow.md b/docs/studyos-collaboration-workflow.md index 6ef3261..05a4ea8 100644 --- a/docs/studyos-collaboration-workflow.md +++ b/docs/studyos-collaboration-workflow.md @@ -97,6 +97,10 @@ the automation itself. `target_thread_id` so the same Codex thread retains coordination context. This needs a real thread ID before enabling. +`studyos-group-channel-digest`: daily at 17:00. Summarizes meaningful group +channel activity into the shared updates channel when there is something useful +for the wider course. + `studyos-weekly-digest`: weekly Thursday at 16:00. Summarizes merged work, open review load, blockers, stale issues, and next milestones. diff --git a/tests/test_automation_seed_files.py b/tests/test_automation_seed_files.py index 9c783a3..73ce6c0 100644 --- a/tests/test_automation_seed_files.py +++ b/tests/test_automation_seed_files.py @@ -8,6 +8,7 @@ EXPECTED_TEMPLATE_IDS = { "studyos-coordinator-thread", "studyos-github-triage", + "studyos-group-channel-digest", "studyos-implementation-candidates", "studyos-issue-refinement", "studyos-pr-review-nudge", @@ -51,10 +52,26 @@ def test_automations_encode_human_gate_and_digest_schedule() -> None: weekly = tomllib.loads( (SEED_ROOT / "studyos-weekly-digest" / "automation.toml").read_text(encoding="utf-8") ) + group_digest = tomllib.loads( + (SEED_ROOT / "studyos-group-channel-digest" / "automation.toml").read_text( + encoding="utf-8" + ) + ) + group_digest_prompt = " ".join(group_digest["prompt"].split()) assert "Do not start implementation" in triage["prompt"] assert "human-gated" in triage["prompt"] assert weekly["rrule"] == "RRULE:FREQ=WEEKLY;BYDAY=TH;BYHOUR=16;BYMINUTE=0" + assert group_digest["rrule"] == "RRULE:FREQ=DAILY;BYHOUR=17;BYMINUTE=0" + assert group_digest["config"]["guild_id"] == "1501971751247024228" + assert group_digest["config"]["destination_channel_name_candidates"] == ["updates"] + assert group_digest["config"]["group_channel_name_globs"] == ["group-*"] + assert group_digest["config"]["ongoing_engagement_window_minutes"] == 90 + assert group_digest["config"]["deferred_proposal_min_delay_minutes"] == 60 + assert group_digest["config"]["deferred_summary_proposals"] == {} + assert "Post to #updates only after" in group_digest["prompt"] + assert "ongoing engagement" in group_digest["prompt"] + assert "between the hold-off time and the next regular cron fire" in group_digest_prompt def test_codex_config_seed_sets_medium_reasoning() -> None: