Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
126 changes: 126 additions & 0 deletions codex/automations/studyos-group-channel-digest/automation.toml
Original file line number Diff line number Diff line change
@@ -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 = {}
22 changes: 22 additions & 0 deletions codex/automations/studyos-group-channel-digest/memory.md
Original file line number Diff line number Diff line change
@@ -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.
4 changes: 4 additions & 0 deletions docs/studyos-collaboration-workflow.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
17 changes: 17 additions & 0 deletions tests/test_automation_seed_files.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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:
Expand Down
Loading