From f96eb6ab4bb96762ef5a7e42809f33be2dd645f3 Mon Sep 17 00:00:00 2001 From: SebastianBoehler <27767932+SebastianBoehler@users.noreply.github.com> Date: Sun, 24 May 2026 12:21:38 +0200 Subject: [PATCH 1/3] feat(automations): add group channel digest --- README.md | 1 + .../automation.toml | 85 +++++++++++++++++++ .../studyos-group-channel-digest/memory.md | 14 +++ docs/studyos-collaboration-workflow.md | 4 + tests/test_automation_seed_files.py | 7 ++ 5 files changed, 111 insertions(+) create mode 100644 codex/automations/studyos-group-channel-digest/automation.toml create mode 100644 codex/automations/studyos-group-channel-digest/memory.md 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..4931ee3 --- /dev/null +++ b/codex/automations/studyos-group-channel-digest/automation.toml @@ -0,0 +1,85 @@ +version = 1 +id = "studyos-group-channel-digest" +kind = "cron" +name = "StudyOS group channel digest" +prompt = """ +Summarize active StudyOS Discord group channels into one concise shared channel +digest. Do not require humans to configure channel IDs. + +Configuration lives in this automation TOML under [config]. Discover channels +from Discord on every run by listing accessible guilds and channels, then infer +which channels are group project channels from names, categories, and the +patterns below. Channel IDs may be stored only as runtime state in +last_processed_message_ids after a successful digest. + +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 list accessible guilds. + - GET /guilds/{guild_id}/channels for each accessible guild. +- Prefer guilds whose names match guild_name_hints. If no hint matches, inspect + all accessible guilds and continue only when the StudyOS course/server can be + identified confidently from guild, category, or channel names. +- Build a channel inventory with guild name, category name, channel name, type, + and ID. Consider only text channels that the bot can read. +- Identify source group channels by matching group_channel_name_patterns against + channel names or category names, then remove channels matching + excluded_channel_name_patterns. +- Identify the destination channel by exact or 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 the + likely candidate channels in the automation output. +- 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_processed_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. + +Digest 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, do not post a digest. +- Never create issues, branches, pull requests, or merge pull requests. + +Post one concise digest only after exactly one destination channel and at least +one source group channel are discovered. After a successful post, update +last_processed_message_ids in this TOML to the newest fetched message ID per +source channel. Preserve all other fields. If posting fails, report the failure +in the automation output and do not advance last_processed_message_ids. +""" +status = "PAUSED" +rrule = "RRULE:FREQ=DAILY;BYHOUR=17;BYMINUTE=0" +model = "gpt-5.5" +reasoning_effort = "medium" +execution_environment = "local" +cwds = ["/workspaces"] + +[config] +guild_name_hints = ["StudyOS", "ML-4510", "Practical Machine Learning"] +destination_channel_name_candidates = [ + "updates", +] +group_channel_name_patterns = [ + "group", + "team", + "project", +] +excluded_channel_name_patterns = [ + "admin", + "bot", + "log", + "archive", + "announcement", + "digest", +] +last_processed_message_ids = {} 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..fb073a4 --- /dev/null +++ b/codex/automations/studyos-group-channel-digest/memory.md @@ -0,0 +1,14 @@ +# StudyOS group channel digest + +This automation is intended for Codex app automation runners, not plain CLI +startup hooks. + +Summarize active group-project Discord channels into the shared StudyOS updates +channel when there is meaningful new activity. + +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..dd79d56 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,16 @@ 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" + ) + ) 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["config"]["destination_channel_name_candidates"] == ["updates"] def test_codex_config_seed_sets_medium_reasoning() -> None: From a57ab1e67458e5ab465ac92df54511ce54bc0bb3 Mon Sep 17 00:00:00 2001 From: SebastianBoehler <27767932+SebastianBoehler@users.noreply.github.com> Date: Sun, 24 May 2026 10:31:41 +0000 Subject: [PATCH 2/3] fix(automations): require group digest approval --- .../automation.toml | 98 ++++++++++++------- .../studyos-group-channel-digest/memory.md | 10 +- tests/test_automation_seed_files.py | 4 + 3 files changed, 74 insertions(+), 38 deletions(-) diff --git a/codex/automations/studyos-group-channel-digest/automation.toml b/codex/automations/studyos-group-channel-digest/automation.toml index 4931ee3..ece805a 100644 --- a/codex/automations/studyos-group-channel-digest/automation.toml +++ b/codex/automations/studyos-group-channel-digest/automation.toml @@ -3,41 +3,56 @@ id = "studyos-group-channel-digest" kind = "cron" name = "StudyOS group channel digest" prompt = """ -Summarize active StudyOS Discord group channels into one concise shared channel -digest. Do not require humans to configure channel IDs. +Run a daily human-in-the-loop StudyOS Discord group summary review. -Configuration lives in this automation TOML under [config]. Discover channels -from Discord on every run by listing accessible guilds and channels, then infer -which channels are group project channels from names, categories, and the -patterns below. Channel IDs may be stored only as runtime state in -last_processed_message_ids after a successful digest. +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 list accessible guilds. - - GET /guilds/{guild_id}/channels for each accessible guild. -- Prefer guilds whose names match guild_name_hints. If no hint matches, inspect - all accessible guilds and continue only when the StudyOS course/server can be - identified confidently from guild, category, or channel names. -- Build a channel inventory with guild name, category name, channel name, type, - and ID. Consider only text channels that the bot can read. -- Identify source group channels by matching group_channel_name_patterns against - channel names or category names, then remove channels matching - excluded_channel_name_patterns. -- Identify the destination channel by exact or 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 the - likely candidate channels in the automation output. + - 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_processed_message_ids has a value for that channel. + 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. -Digest shape: +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, 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. @@ -48,14 +63,19 @@ 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, do not post a digest. +- 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. -Post one concise digest only after exactly one destination channel and at least -one source group channel are discovered. After a successful post, update -last_processed_message_ids in this TOML to the newest fetched message ID per -source channel. Preserve all other fields. If posting fails, report the failure -in the automation output and do not advance last_processed_message_ids. +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" @@ -65,15 +85,12 @@ execution_environment = "local" cwds = ["/workspaces"] [config] -guild_name_hints = ["StudyOS", "ML-4510", "Practical Machine Learning"] +guild_id = "1501971751247024228" +destination_channel_id = "1501974151165116637" destination_channel_name_candidates = [ "updates", ] -group_channel_name_patterns = [ - "group", - "team", - "project", -] +group_channel_name_globs = ["group-*"] excluded_channel_name_patterns = [ "admin", "bot", @@ -82,4 +99,13 @@ excluded_channel_name_patterns = [ "announcement", "digest", ] -last_processed_message_ids = {} +approval_keywords = [ + "approve", + "approved", + "share", + "ship it", + "post to updates", + "yes, share", +] +last_scanned_message_ids = {} +pending_update_summaries = {} diff --git a/codex/automations/studyos-group-channel-digest/memory.md b/codex/automations/studyos-group-channel-digest/memory.md index fb073a4..1f555e7 100644 --- a/codex/automations/studyos-group-channel-digest/memory.md +++ b/codex/automations/studyos-group-channel-digest/memory.md @@ -3,8 +3,14 @@ This automation is intended for Codex app automation runners, not plain CLI startup hooks. -Summarize active group-project Discord channels into the shared StudyOS updates -channel when there is meaningful new activity. +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. Human policy: - Students approve and merge pull requests. diff --git a/tests/test_automation_seed_files.py b/tests/test_automation_seed_files.py index dd79d56..383fd03 100644 --- a/tests/test_automation_seed_files.py +++ b/tests/test_automation_seed_files.py @@ -61,7 +61,11 @@ def test_automations_encode_human_gate_and_digest_schedule() -> None: 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 "Post to #updates only after" in group_digest["prompt"] def test_codex_config_seed_sets_medium_reasoning() -> None: From 70b6d563389d56f0f542541f13a80e5d59231ca4 Mon Sep 17 00:00:00 2001 From: SebastianBoehler <27767932+SebastianBoehler@users.noreply.github.com> Date: Sun, 24 May 2026 10:42:33 +0000 Subject: [PATCH 3/3] fix(automations): defer summaries during active discussion --- .../automation.toml | 19 +++++++++++++++++-- .../studyos-group-channel-digest/memory.md | 2 ++ tests/test_automation_seed_files.py | 6 ++++++ 3 files changed, 25 insertions(+), 2 deletions(-) diff --git a/codex/automations/studyos-group-channel-digest/automation.toml b/codex/automations/studyos-group-channel-digest/automation.toml index ece805a..36bf13d 100644 --- a/codex/automations/studyos-group-channel-digest/automation.toml +++ b/codex/automations/studyos-group-channel-digest/automation.toml @@ -38,12 +38,24 @@ Workflow: 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, generate a short proposed update summary and post it into that same - group channel first. + 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. @@ -107,5 +119,8 @@ approval_keywords = [ "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 index 1f555e7..0b9530f 100644 --- a/codex/automations/studyos-group-channel-digest/memory.md +++ b/codex/automations/studyos-group-channel-digest/memory.md @@ -11,6 +11,8 @@ Behavior: - 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. diff --git a/tests/test_automation_seed_files.py b/tests/test_automation_seed_files.py index 383fd03..73ce6c0 100644 --- a/tests/test_automation_seed_files.py +++ b/tests/test_automation_seed_files.py @@ -57,6 +57,7 @@ def test_automations_encode_human_gate_and_digest_schedule() -> None: 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"] @@ -65,7 +66,12 @@ def test_automations_encode_human_gate_and_digest_schedule() -> None: 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: