diff options
| author | Craig Jennings <c@cjennings.net> | 2026-06-09 17:16:08 -0500 |
|---|---|---|
| committer | Craig Jennings <c@cjennings.net> | 2026-06-09 17:16:08 -0500 |
| commit | c91bd0b1e8183814f248b0751d88a8e422a905e8 (patch) | |
| tree | f70abe516c47162f8e6538c2d9fa639aa9349d13 | |
| parent | 1f0900281b8262539137bc1aff3f01cc05745139 (diff) | |
| download | rulesets-c91bd0b1e8183814f248b0751d88a8e422a905e8.tar.gz rulesets-c91bd0b1e8183814f248b0751d88a8e422a905e8.zip | |
feat(workflows): generalize broadcast into announcement + situational modes
cross-project-broadcast handled tooling and rule announcements but had no shape for the situational case: a life or work event I want every project's agent to know, said once so none is missing context when I next talk to them. I renamed it to broadcast (helper and test alongside) and split it into two modes over the same fan-out plumbing. Announcement keeps the rigid capability template. Situational carries a general-not-comprehensive summary plus a fixed receiving-agent contract: record it in notes.org, hold it time-boxed or standing, apply on the project's own judgment, ask follow-ups at startup. The broadcasting agent does no per-project relevance analysis. Each receiving agent decides what the event means for its own work.
| -rwxr-xr-x | .ai/scripts/broadcast.py (renamed from .ai/scripts/cross-project-broadcast.py) | 8 | ||||
| -rw-r--r-- | .ai/scripts/tests/test_broadcast.py (renamed from .ai/scripts/tests/test_cross_project_broadcast.py) | 6 | ||||
| -rw-r--r-- | .ai/workflows/INDEX.org | 5 | ||||
| -rw-r--r-- | .ai/workflows/broadcast.org | 184 | ||||
| -rw-r--r-- | .ai/workflows/cross-project-broadcast.org | 139 | ||||
| -rwxr-xr-x | claude-templates/.ai/scripts/broadcast.py (renamed from claude-templates/.ai/scripts/cross-project-broadcast.py) | 8 | ||||
| -rw-r--r-- | claude-templates/.ai/scripts/tests/test_broadcast.py (renamed from claude-templates/.ai/scripts/tests/test_cross_project_broadcast.py) | 6 | ||||
| -rw-r--r-- | claude-templates/.ai/workflows/INDEX.org | 5 | ||||
| -rw-r--r-- | claude-templates/.ai/workflows/broadcast.org | 184 | ||||
| -rw-r--r-- | claude-templates/.ai/workflows/cross-project-broadcast.org | 139 |
10 files changed, 388 insertions, 296 deletions
diff --git a/.ai/scripts/cross-project-broadcast.py b/.ai/scripts/broadcast.py index 2c4c690..ba5c786 100755 --- a/.ai/scripts/cross-project-broadcast.py +++ b/.ai/scripts/broadcast.py @@ -6,8 +6,8 @@ whose .ai/protocols.org exists. Uses the existing inbox-send.py helper to deliver per-target. Usage: - cross-project-broadcast.py --list - cross-project-broadcast.py --file <path> [--exclude <name> ...] [--dry-run] + broadcast.py --list + broadcast.py --file <path> [--exclude <name> ...] [--dry-run] """ from __future__ import annotations @@ -67,7 +67,7 @@ def inbox_send_path() -> Path: candidate = ancestor / ".ai" / "scripts" / "inbox-send.py" if candidate.is_file(): return candidate - raise SystemExit("cross-project-broadcast: inbox-send.py not found in current project") + raise SystemExit("broadcast: inbox-send.py not found in current project") def main() -> int: @@ -115,7 +115,7 @@ def main() -> int: msg_path = Path(args.file).resolve() if not msg_path.is_file(): - print(f"cross-project-broadcast: file not found: {msg_path}", file=sys.stderr) + print(f"broadcast: file not found: {msg_path}", file=sys.stderr) return 2 inbox_send = inbox_send_path() diff --git a/.ai/scripts/tests/test_cross_project_broadcast.py b/.ai/scripts/tests/test_broadcast.py index 5919fbf..a0decf5 100644 --- a/.ai/scripts/tests/test_cross_project_broadcast.py +++ b/.ai/scripts/tests/test_broadcast.py @@ -1,4 +1,4 @@ -"""Tests for cross-project-broadcast.py: project fingerprinting + discovery. +"""Tests for broadcast.py: project fingerprinting + discovery. Plain python3 script. The pure-ish helpers are driven against tmp project trees; discovery is exercised with SEARCH_ROOTS monkeypatched to the tree, and @@ -11,12 +11,12 @@ from pathlib import Path import pytest -SCRIPT = Path(__file__).resolve().parents[1] / "cross-project-broadcast.py" +SCRIPT = Path(__file__).resolve().parents[1] / "broadcast.py" @pytest.fixture(scope="module") def bcast(): - spec = importlib.util.spec_from_file_location("cross_project_broadcast", SCRIPT) + spec = importlib.util.spec_from_file_location("broadcast", SCRIPT) assert spec and spec.loader module = importlib.util.module_from_spec(spec) spec.loader.exec_module(module) diff --git a/.ai/workflows/INDEX.org b/.ai/workflows/INDEX.org index 9b957d2..1d520e2 100644 --- a/.ai/workflows/INDEX.org +++ b/.ai/workflows/INDEX.org @@ -86,8 +86,9 @@ This index must list every =.org= file in =.ai/workflows/= except this one and e - Triggers: "process the transcript", "process the recording". Auto: new files in =~/sync/recordings/=. - =page-signal.org= — send Craig a Signal message via =page-signal= when desktop notifications won't reach him. Defaults to note-to-self. Outbound to other contacts requires explicit =--to <+number>= every call. Auto: long-running task completion, cross-device deliverables, operator-attention events. NOT for routine completions or periodic status pings. - Triggers: "page me on signal", "signal me when X is done", "send a signal note about X" -- =cross-project-broadcast.org= — fan out a single message to every AI project's inbox via the discovery helper =cross-project-broadcast.py= + the existing =inbox-send.py=. Use sparingly for capability announcements and shared rule changes; not for project-specific handoffs. - - Triggers: "broadcast this to every project", "notify every project about X", "fan out this announcement", "let every project know X is available" +- =broadcast.org= — fan out a single message to every AI project's inbox via the discovery helper =broadcast.py= + the existing =inbox-send.py=. Two modes: *announcement* (a tooling / capability / rule change every agent should know) and *situational* (a life or work event — travel, a visitor, quitting a job — said once so every project's agent holds the context and applies it on its own judgment). Use sparingly; not for project-specific handoffs. + - Announcement triggers: "broadcast this to every project", "notify every project about X", "fan out this announcement", "let every project know X is available" + - Situational triggers: "broadcast the <event> to all projects", "broadcast that <situation>", "let every project know I'll be away ..." - =flashcard-review.org= — review an org-drill flashcard file, restructure cards to question-form headings (no answer hints), audit content accuracy against project source-of-truth via subagent, rewrite source preserving SRS state, regenerate the Anki =.apkg= to =~/sync/phone/anki/=. Person cards use "Who is X? Tell me about their Y."; talking-points cards stay as-is. Script behavior: =flashcard-to-anki.py= strips =:PROPERTIES:= drawers + =SCHEDULED:= / =DEADLINE:= planning lines from Anki output. - Triggers: "review the flashcards", "update the flashcards", "review the drill deck", "update the drill deck", "refresh the Anki cards", "let's run the flashcard-review workflow" - =page-me.org= — set a timed notification. diff --git a/.ai/workflows/broadcast.org b/.ai/workflows/broadcast.org new file mode 100644 index 0000000..0d3d1c0 --- /dev/null +++ b/.ai/workflows/broadcast.org @@ -0,0 +1,184 @@ +#+TITLE: Broadcast Workflow +#+AUTHOR: Craig Jennings & Claude +#+DATE: 2026-05-29 + +* Overview + +Fan out a single message to every AI project's inbox in one operation. Discovers projects by fingerprint (any directory with =.ai/protocols.org=) and delivers via the existing =inbox-send.py= per-target. Say a thing once instead of hand-walking 20+ projects. + +Two modes share the same fan-out plumbing and differ only in what the message carries and what the receiver does with it: + +- *Announcement* — a tooling, capability, or rule change every project's agent should know about. Maintainer-facing. +- *Situational* — a life or work event in Craig's world (travel, a visitor, a birth or death, quitting a job, a move) that should reach every project's agent so none is missing context. Craig says it once; each agent folds it into its own work on its own judgment. + +The mode picks the message template (Compose) and the receiving contract. Everything else — discovery, scope confirmation, fan-out, reporting — is shared. + +* When to Use This Workflow + +*Announcement triggers:* +- "broadcast this to every project" +- "notify every project about <thing>" +- "fan out this announcement" +- "let every project know X is available" + +Automatic announcement triggers: +- *New machine-global capability landed.* A new script in =~/.local/bin/=, a new MCP server, a new tool (e.g. =signal-cli=, =page-signal=). Projects need to know it's available. +- *Shared rule or protocol change* in =claude-rules/= or =claude-templates/.ai/= that materially changes how every project's agent should behave. +- *Deprecation notice* — a script, workflow, or rule going away; give every project a chance to migrate. + +*Situational triggers:* +- "broadcast the <event> to all projects" (e.g. "broadcast the travel to all projects") +- "broadcast that <situation>" +- "let every project know I'll be <away / out / unreachable> ..." + +Situational mode is user-driven only — never automatic. Craig decides an event is worth every project knowing. + +* When NOT to Use This Workflow + +- *Project-specific work.* A handoff for one project goes through =inbox-send= directly, not broadcast. +- *Routine status updates.* The session log and todo.org cover routine work. +- *Bulk noise.* Every broadcast adds N inbox files across the fleet. Use sparingly. Ask whether projects actually need to know. +- *Every commit.* Most commits are project-internal hygiene the other projects don't care about. Cross-project workflow updates already rsync into every project's =.ai/= at next startup, so the capability lands without a broadcast. Broadcast only when projects need to *act* on the change or *hold* the context. The startup rsync carries the bits; the broadcast carries the *attention*, and attention is the costly resource. + +* Cadence Guideline + +Broadcasts are event-level, not commit-level. A reasonable cadence is one to four per month. Each broadcast costs N inbox processings across the fleet (currently ~23 targets), so daily broadcasts mean daily noise and train projects to ignore the inbox — then a real handoff gets missed. If a session ships several broadcastable changes, bundle them into one broadcast at session end rather than firing one per commit. + +Situational broadcasts are naturally rare (a real life or work event), so the same restraint applies without much effort — but don't withhold one that genuinely bears on how the agents should act. + +* The Fan-Out (shared by both modes) + +** Phase A — Discover targets + +#+begin_src bash +python3 .ai/scripts/broadcast.py --list +#+end_src + +The helper scans =~/code/=, =~/projects/=, =~/.emacs.d= for any directory containing =.ai/protocols.org=. Prints basename and full path of each, sender-excluded (the current project never receives its own broadcast). + +** Phase B — Compose the message + +Pick the mode, write the body to =/tmp/broadcast-<topic>.org= using that mode's template (below), then continue to Phase C. The mode templates are the only place the two modes diverge. + +** Phase C — Confirm scope with Craig + +Surface the discovered project list and the message inline. Both modes confirm before fan-out — it reaches many inboxes and a situational message goes out under Craig's situation, so the content gets one approval first. + +#+begin_example +Broadcast scope (<mode>): +- Target projects: <N> (list) +- Message: <2-line summary> +- <Action required / Receiving contract>: <one line> + +1. Send to all <N> targets (recommended) +2. Exclude specific projects (name them) +3. Cancel — message stays at /tmp/broadcast-<topic>.org +#+end_example + +** Phase D — Fan out + +#+begin_src bash +python3 .ai/scripts/broadcast.py \ + --file /tmp/broadcast-<topic>.org \ + [--exclude project1 --exclude project2 ...] +#+end_src + +The helper iterates targets, runs =inbox-send.py <target> --file <broadcast>= per target, and captures success/failure per project. The =from-<sender>= prefix in each resulting filename traces provenance. + +** Phase E — Report + +Summarize the fan-out: total targets discovered, sent successfully (count), failed (list with reason), excluded (list with reason). Surface any failures — a partial broadcast is the failure mode you'll never notice otherwise. + +** Phase F — Cleanup + +Delete =/tmp/broadcast-<topic>.org=. The content lives in each target's inbox now. + +* Mode A — Announcement + +A tooling, capability, or rule change. Write the body to =/tmp/broadcast-<topic>.org= with this structure — rigid on purpose, so every project's next session scans 20+ broadcasts in seconds: + +#+begin_example +,#+TITLE: <one-line summary> +,#+DATE: YYYY-MM-DD +,#+SOURCE: <sender project name> + +,* What's new + +<two to five sentences: the capability, rule, or change.> + +,* How to use it + +<two to five lines: the concrete invocation, command, or path. Code in =#+begin_src= blocks. No prose walls.> + +,* Why this matters / when to use + +<two to four sentences: the rationale and the discrimination rule for when the new thing applies vs. when existing alternatives suffice.> + +,* Action required + +<one of:> +- =FYI=, no action required (most broadcasts) +- =Update workflow X= to reference the new capability +- =Deprecate workflow Y= by date Z +#+end_example + +* Mode B — Situational + +A life or work event Craig wants every project to know. The point is to beat the silo problem: Craig forgets to mention something during a conversation with one project, but it actually bears on how that project works and on the agent's decisions or ideas. Broadcast once, and every agent already knows by the time he's chatting with them. + +** Composing principle + +- *No per-project tailoring.* The broadcasting agent does NOT figure out how the event is relevant to each project. One general message goes to all. Each receiving agent decides relevance to its own current tasks. +- *General, not comprehensive.* Communicate a good amount of detail, but don't try to be exhaustive for a complex situation. Enough that an agent unfamiliar with it gets the picture and knows the shape of what changed. The receiving agent can ask Craig follow-up questions after startup. +- *Gather, then ask for gaps.* Pull the salient facts from the current session and project. Ask Craig only for the few you don't have (the concrete ones an agent would need — dates, who, where, what's changing). + +Write the body to =/tmp/broadcast-<topic>.org= with this structure: + +#+begin_example +,#+TITLE: Broadcast: <one-line situation> +,#+DATE: YYYY-MM-DD +,#+SOURCE: <sender project name> + +,* What's happening + +<A general but substantive summary: what the event is, who/what/where, any +dates or duration, what's changing. General, not comprehensive.> + +,* For the receiving agent + +This is situational context Craig wanted every project to have — said once and +broadcast, not a task and not tailored to this project. + +- Decide for yourself whether and how it bears on this project's work and your + suggestions. He is not prescribing actions; you judge relevance. +- Record it so it persists: add it to this project's =notes.org= Active + Reminders. Time-box it with an end date if the situation has one (a return + date, a departure date); otherwise hold it as standing context until it is no + longer relevant. +- Surface it at the next startup and raise it if it touches current work. +- Ask Craig any follow-up questions then — this message is deliberately general. +#+end_example + +The "For the receiving agent" block is fixed text — it travels with every situational broadcast so the message is self-describing. A receiving project's =process-inbox= reads it and acts on those instructions without needing any special-casing; the value gate accepts it as situational awareness that improves how the project works. + +** Receiving behavior (what a project does with an incoming situational broadcast) + +When =process-inbox= encounters a =Broadcast:= item, the disposition is *record-and-hold*, not file-as-task: + +1. Add a dated entry to =notes.org= Active Reminders capturing the situation and its end date (if any). +2. If the event bears on an open task, note the connection in that task's body. +3. Surface it at the next startup; retire it once its end date passes or it stops being relevant. + +* Common Mistakes + +1. *Broadcasting project-specific work.* Each broadcast costs N inbox files across the fleet. Routine handoffs go through =inbox-send= directly. +2. *Per-project tailoring in situational mode.* The broadcasting agent does not analyze relevance per project — that's the receiving agent's job. One general message, full stop. +3. *Over-engineering the situational summary.* General, not comprehensive. The receiver asks follow-ups; don't stall trying to anticipate every project's needs. +4. *Skipping the announcement structure.* Free-form announcements force every recipient to parse them differently. Use the rigid headings. +5. *Sender-includes-itself.* The discovery helper excludes the sender automatically. Don't override it — broadcasting to your own inbox creates a self-reply loop. +6. *Forgetting Phase E.* A broadcast that partially succeeded is the failure mode you'll never notice. Always check the per-target results. +7. *Announcement without an "Action required" line, or situational without the "For the receiving agent" block.* Each mode's recipients need to know what the message is for. + +* Living Document + +If the discovery roots change (a new top-level directory for AI projects), update =broadcast.py='s =SEARCH_ROOTS=. If the per-broadcast structure proves too rigid or too loose, tune the mode templates. If recipient projects complain about broadcast noise, the rule is "broadcast less," not "structure broadcasts harder." If situational broadcasts start needing per-project routing, that's a sign the silo is better solved by the shared org-roam KB than by fan-out — revisit before adding routing logic here. diff --git a/.ai/workflows/cross-project-broadcast.org b/.ai/workflows/cross-project-broadcast.org deleted file mode 100644 index 63af84f..0000000 --- a/.ai/workflows/cross-project-broadcast.org +++ /dev/null @@ -1,139 +0,0 @@ -#+TITLE: Cross-Project Broadcast Workflow -#+AUTHOR: Craig Jennings & Claude -#+DATE: 2026-05-29 - -* Overview - -Fan out a single message to every AI project's inbox in one operation. Discovers projects by fingerprint (any directory with =.ai/protocols.org=) and delivers via the existing =inbox-send.py= per-target. The point is announcing a new capability or shared change once, instead of hand-walking 20+ projects. - -* When to Use This Workflow - -User triggers: - -- "broadcast this to every project" -- "notify every project about <thing>" -- "fan out this announcement" -- "let every project know X is available" - -Automatic invocation: - -- *New machine-global capability landed.* A new script in =~/.local/bin/=, a new MCP server registered, a new tool installed (e.g. =signal-cli=, =page-signal=). Projects need to know it's available so their agents can use it. -- *Shared rule or protocol change.* When a change to =claude-rules/= or =claude-templates/.ai/= materially affects how every project's agent should behave, broadcast a heads-up so the next session of each project picks it up explicitly rather than discovering it on rsync. -- *Deprecation notice.* When a script, workflow, or rule is going away, give every project a chance to migrate before the removal lands. - -* When NOT to Use This Workflow - -- *Project-specific work.* A handoff intended for one project goes through =inbox-send= directly, not broadcast. -- *Routine status updates.* The session log and todo.org cover routine work. Broadcast is for capability- or rule-level changes. -- *Bulk noise.* Every broadcast adds N inbox files. Use sparingly. Ask whether projects actually need to know. -- *Every commit.* Most commits are project-internal hygiene (audit, intake, TODO updates) that the other projects don't care about. Even cross-project workflow updates rsync into every project's =.ai/= at next startup, so the capability lands without a broadcast. Broadcast only when projects need to *act* on the change (use a new tool, migrate before a deprecation, change their behavior per a new rule). Resist the urge to fan out a "here's what shipped today" digest. The startup rsync already carries the bits; the broadcast carries the *attention*, and attention is the costly resource. - -* Cadence Guideline - -Broadcasts are capability-and-rule-level events, not commit-level events. A reasonable cadence is one to four broadcasts per month, depending on what landed. Five reasons for the restraint: - -1. Each broadcast costs N inbox processings across the fleet (currently ~23 targets). Daily broadcasts mean daily noise. -2. Most commits are project-internal. Other projects do not need to read about another project's TODO sweeps, audit passes, or hygiene work. -3. The startup rsync already does the capability work. Broadcasts add awareness, not availability. -4. Aggregation wins. One weekly "here's what shipped" digest beats ten per-commit pings on every dimension. Fewer inbox files, easier to skim, easier to defer reading. -5. Per-commit broadcasts train projects to ignore inbox. Then a real handoff gets missed. - -If a session ships several broadcastable changes, bundle them into one broadcast at session end rather than firing one per commit. - -* The Workflow - -** Phase A — Discover targets - -Run the discovery helper: - -#+begin_src bash -python3 .ai/scripts/cross-project-broadcast.py --list -#+end_src - -The helper scans =~/code/=, =~/projects/=, =~/.emacs.d= for any directory containing =.ai/protocols.org=. Prints the basename and full path of each, in sender-excluded order (the current project never receives its own broadcast). - -** Phase B — Compose the message - -Write the broadcast body to =/tmp/broadcast-<topic>.org=. Required structure: - -#+begin_example -,#+TITLE: <one-line summary> -,#+DATE: YYYY-MM-DD -,#+SOURCE: <sender project name> - -,* What's new - -<two to five sentences: the capability, rule, or change being announced.> - -,* How to use it - -<two to five lines: the concrete invocation, command, or path. Code examples in =#+begin_src= blocks. No prose walls.> - -,* Why this matters / when to use - -<two to four sentences: the user-facing rationale and the discrimination rule for when the new thing applies vs. when existing alternatives suffice.> - -,* Action required - -<one of:> -- =FYI=, no action required (most broadcasts) -- =Update workflow X= to reference the new capability -- =Deprecate workflow Y= by date Z -#+end_example - -The structure is rigid on purpose. Every project's next session has to read 20+ broadcasts as efficiently as possible. The standard headings let the agent scan in seconds. - -** Phase C — Confirm scope with the user - -Surface the discovered project list and the message inline. Ask: - -#+begin_example -Broadcast scope: -- Target projects: <N> (list) -- Message: <2-line summary> -- Action required: <FYI / update workflow / deprecation> - -1. Send to all <N> targets (recommended) -2. Exclude specific projects (name them) -3. Cancel — message stays at /tmp/broadcast-<topic>.org -#+end_example - -** Phase D — Fan out - -Run the broadcast helper with the composed message: - -#+begin_src bash -python3 .ai/scripts/cross-project-broadcast.py \ - --file /tmp/broadcast-<topic>.org \ - [--exclude project1 --exclude project2 ...] -#+end_src - -The helper iterates over targets, runs =inbox-send.py <target> --file <broadcast>= per target, captures success/failure per project. The =from-<sender>= prefix in the resulting filename traces provenance. - -** Phase E — Report - -Summarize the fan-out: - -- Total targets discovered -- Sent successfully (count) -- Failed (list with reason) -- Excluded (list with reason) - -If any failures, surface them — silent failure on a broadcast means some projects never learn about the change. - -** Phase F — Cleanup - -Delete =/tmp/broadcast-<topic>.org=. The content lives in each target's inbox now. - -* Common Mistakes - -1. *Broadcasting project-specific work.* Each broadcast costs N inbox files across the fleet. Routine handoffs go through =inbox-send= directly. -2. *Skipping Phase B's structure.* Free-form broadcasts force every recipient to parse them differently. Use the rigid headings. -3. *Sender-includes-itself.* The discovery helper excludes the sender automatically. Don't override it — broadcasting to your own inbox creates a self-reply loop. -4. *Forgetting Phase E.* A broadcast that partially succeeded is the failure mode you'll never notice. Always check the per-target results. -5. *Broadcasting without an "Action required" line.* Recipients need to know whether this is FYI or whether their project has to do something. The line is non-optional. -6. *Using broadcast as a substitute for documentation.* Capability announcements should be paired with a rule or workflow update so the next-next session can rediscover the capability from canonical docs, not from a stale inbox file. - -* Living Document - -If the discovery roots change (a new top-level directory for AI projects), update =cross-project-broadcast.py='s =SEARCH_ROOTS=. If the per-broadcast structure proves too rigid or too loose, tune Phase B. If the recipient projects start complaining about broadcast noise, the rule is "broadcast less," not "structure broadcasts harder." diff --git a/claude-templates/.ai/scripts/cross-project-broadcast.py b/claude-templates/.ai/scripts/broadcast.py index 2c4c690..ba5c786 100755 --- a/claude-templates/.ai/scripts/cross-project-broadcast.py +++ b/claude-templates/.ai/scripts/broadcast.py @@ -6,8 +6,8 @@ whose .ai/protocols.org exists. Uses the existing inbox-send.py helper to deliver per-target. Usage: - cross-project-broadcast.py --list - cross-project-broadcast.py --file <path> [--exclude <name> ...] [--dry-run] + broadcast.py --list + broadcast.py --file <path> [--exclude <name> ...] [--dry-run] """ from __future__ import annotations @@ -67,7 +67,7 @@ def inbox_send_path() -> Path: candidate = ancestor / ".ai" / "scripts" / "inbox-send.py" if candidate.is_file(): return candidate - raise SystemExit("cross-project-broadcast: inbox-send.py not found in current project") + raise SystemExit("broadcast: inbox-send.py not found in current project") def main() -> int: @@ -115,7 +115,7 @@ def main() -> int: msg_path = Path(args.file).resolve() if not msg_path.is_file(): - print(f"cross-project-broadcast: file not found: {msg_path}", file=sys.stderr) + print(f"broadcast: file not found: {msg_path}", file=sys.stderr) return 2 inbox_send = inbox_send_path() diff --git a/claude-templates/.ai/scripts/tests/test_cross_project_broadcast.py b/claude-templates/.ai/scripts/tests/test_broadcast.py index 5919fbf..a0decf5 100644 --- a/claude-templates/.ai/scripts/tests/test_cross_project_broadcast.py +++ b/claude-templates/.ai/scripts/tests/test_broadcast.py @@ -1,4 +1,4 @@ -"""Tests for cross-project-broadcast.py: project fingerprinting + discovery. +"""Tests for broadcast.py: project fingerprinting + discovery. Plain python3 script. The pure-ish helpers are driven against tmp project trees; discovery is exercised with SEARCH_ROOTS monkeypatched to the tree, and @@ -11,12 +11,12 @@ from pathlib import Path import pytest -SCRIPT = Path(__file__).resolve().parents[1] / "cross-project-broadcast.py" +SCRIPT = Path(__file__).resolve().parents[1] / "broadcast.py" @pytest.fixture(scope="module") def bcast(): - spec = importlib.util.spec_from_file_location("cross_project_broadcast", SCRIPT) + spec = importlib.util.spec_from_file_location("broadcast", SCRIPT) assert spec and spec.loader module = importlib.util.module_from_spec(spec) spec.loader.exec_module(module) diff --git a/claude-templates/.ai/workflows/INDEX.org b/claude-templates/.ai/workflows/INDEX.org index 9b957d2..1d520e2 100644 --- a/claude-templates/.ai/workflows/INDEX.org +++ b/claude-templates/.ai/workflows/INDEX.org @@ -86,8 +86,9 @@ This index must list every =.org= file in =.ai/workflows/= except this one and e - Triggers: "process the transcript", "process the recording". Auto: new files in =~/sync/recordings/=. - =page-signal.org= — send Craig a Signal message via =page-signal= when desktop notifications won't reach him. Defaults to note-to-self. Outbound to other contacts requires explicit =--to <+number>= every call. Auto: long-running task completion, cross-device deliverables, operator-attention events. NOT for routine completions or periodic status pings. - Triggers: "page me on signal", "signal me when X is done", "send a signal note about X" -- =cross-project-broadcast.org= — fan out a single message to every AI project's inbox via the discovery helper =cross-project-broadcast.py= + the existing =inbox-send.py=. Use sparingly for capability announcements and shared rule changes; not for project-specific handoffs. - - Triggers: "broadcast this to every project", "notify every project about X", "fan out this announcement", "let every project know X is available" +- =broadcast.org= — fan out a single message to every AI project's inbox via the discovery helper =broadcast.py= + the existing =inbox-send.py=. Two modes: *announcement* (a tooling / capability / rule change every agent should know) and *situational* (a life or work event — travel, a visitor, quitting a job — said once so every project's agent holds the context and applies it on its own judgment). Use sparingly; not for project-specific handoffs. + - Announcement triggers: "broadcast this to every project", "notify every project about X", "fan out this announcement", "let every project know X is available" + - Situational triggers: "broadcast the <event> to all projects", "broadcast that <situation>", "let every project know I'll be away ..." - =flashcard-review.org= — review an org-drill flashcard file, restructure cards to question-form headings (no answer hints), audit content accuracy against project source-of-truth via subagent, rewrite source preserving SRS state, regenerate the Anki =.apkg= to =~/sync/phone/anki/=. Person cards use "Who is X? Tell me about their Y."; talking-points cards stay as-is. Script behavior: =flashcard-to-anki.py= strips =:PROPERTIES:= drawers + =SCHEDULED:= / =DEADLINE:= planning lines from Anki output. - Triggers: "review the flashcards", "update the flashcards", "review the drill deck", "update the drill deck", "refresh the Anki cards", "let's run the flashcard-review workflow" - =page-me.org= — set a timed notification. diff --git a/claude-templates/.ai/workflows/broadcast.org b/claude-templates/.ai/workflows/broadcast.org new file mode 100644 index 0000000..0d3d1c0 --- /dev/null +++ b/claude-templates/.ai/workflows/broadcast.org @@ -0,0 +1,184 @@ +#+TITLE: Broadcast Workflow +#+AUTHOR: Craig Jennings & Claude +#+DATE: 2026-05-29 + +* Overview + +Fan out a single message to every AI project's inbox in one operation. Discovers projects by fingerprint (any directory with =.ai/protocols.org=) and delivers via the existing =inbox-send.py= per-target. Say a thing once instead of hand-walking 20+ projects. + +Two modes share the same fan-out plumbing and differ only in what the message carries and what the receiver does with it: + +- *Announcement* — a tooling, capability, or rule change every project's agent should know about. Maintainer-facing. +- *Situational* — a life or work event in Craig's world (travel, a visitor, a birth or death, quitting a job, a move) that should reach every project's agent so none is missing context. Craig says it once; each agent folds it into its own work on its own judgment. + +The mode picks the message template (Compose) and the receiving contract. Everything else — discovery, scope confirmation, fan-out, reporting — is shared. + +* When to Use This Workflow + +*Announcement triggers:* +- "broadcast this to every project" +- "notify every project about <thing>" +- "fan out this announcement" +- "let every project know X is available" + +Automatic announcement triggers: +- *New machine-global capability landed.* A new script in =~/.local/bin/=, a new MCP server, a new tool (e.g. =signal-cli=, =page-signal=). Projects need to know it's available. +- *Shared rule or protocol change* in =claude-rules/= or =claude-templates/.ai/= that materially changes how every project's agent should behave. +- *Deprecation notice* — a script, workflow, or rule going away; give every project a chance to migrate. + +*Situational triggers:* +- "broadcast the <event> to all projects" (e.g. "broadcast the travel to all projects") +- "broadcast that <situation>" +- "let every project know I'll be <away / out / unreachable> ..." + +Situational mode is user-driven only — never automatic. Craig decides an event is worth every project knowing. + +* When NOT to Use This Workflow + +- *Project-specific work.* A handoff for one project goes through =inbox-send= directly, not broadcast. +- *Routine status updates.* The session log and todo.org cover routine work. +- *Bulk noise.* Every broadcast adds N inbox files across the fleet. Use sparingly. Ask whether projects actually need to know. +- *Every commit.* Most commits are project-internal hygiene the other projects don't care about. Cross-project workflow updates already rsync into every project's =.ai/= at next startup, so the capability lands without a broadcast. Broadcast only when projects need to *act* on the change or *hold* the context. The startup rsync carries the bits; the broadcast carries the *attention*, and attention is the costly resource. + +* Cadence Guideline + +Broadcasts are event-level, not commit-level. A reasonable cadence is one to four per month. Each broadcast costs N inbox processings across the fleet (currently ~23 targets), so daily broadcasts mean daily noise and train projects to ignore the inbox — then a real handoff gets missed. If a session ships several broadcastable changes, bundle them into one broadcast at session end rather than firing one per commit. + +Situational broadcasts are naturally rare (a real life or work event), so the same restraint applies without much effort — but don't withhold one that genuinely bears on how the agents should act. + +* The Fan-Out (shared by both modes) + +** Phase A — Discover targets + +#+begin_src bash +python3 .ai/scripts/broadcast.py --list +#+end_src + +The helper scans =~/code/=, =~/projects/=, =~/.emacs.d= for any directory containing =.ai/protocols.org=. Prints basename and full path of each, sender-excluded (the current project never receives its own broadcast). + +** Phase B — Compose the message + +Pick the mode, write the body to =/tmp/broadcast-<topic>.org= using that mode's template (below), then continue to Phase C. The mode templates are the only place the two modes diverge. + +** Phase C — Confirm scope with Craig + +Surface the discovered project list and the message inline. Both modes confirm before fan-out — it reaches many inboxes and a situational message goes out under Craig's situation, so the content gets one approval first. + +#+begin_example +Broadcast scope (<mode>): +- Target projects: <N> (list) +- Message: <2-line summary> +- <Action required / Receiving contract>: <one line> + +1. Send to all <N> targets (recommended) +2. Exclude specific projects (name them) +3. Cancel — message stays at /tmp/broadcast-<topic>.org +#+end_example + +** Phase D — Fan out + +#+begin_src bash +python3 .ai/scripts/broadcast.py \ + --file /tmp/broadcast-<topic>.org \ + [--exclude project1 --exclude project2 ...] +#+end_src + +The helper iterates targets, runs =inbox-send.py <target> --file <broadcast>= per target, and captures success/failure per project. The =from-<sender>= prefix in each resulting filename traces provenance. + +** Phase E — Report + +Summarize the fan-out: total targets discovered, sent successfully (count), failed (list with reason), excluded (list with reason). Surface any failures — a partial broadcast is the failure mode you'll never notice otherwise. + +** Phase F — Cleanup + +Delete =/tmp/broadcast-<topic>.org=. The content lives in each target's inbox now. + +* Mode A — Announcement + +A tooling, capability, or rule change. Write the body to =/tmp/broadcast-<topic>.org= with this structure — rigid on purpose, so every project's next session scans 20+ broadcasts in seconds: + +#+begin_example +,#+TITLE: <one-line summary> +,#+DATE: YYYY-MM-DD +,#+SOURCE: <sender project name> + +,* What's new + +<two to five sentences: the capability, rule, or change.> + +,* How to use it + +<two to five lines: the concrete invocation, command, or path. Code in =#+begin_src= blocks. No prose walls.> + +,* Why this matters / when to use + +<two to four sentences: the rationale and the discrimination rule for when the new thing applies vs. when existing alternatives suffice.> + +,* Action required + +<one of:> +- =FYI=, no action required (most broadcasts) +- =Update workflow X= to reference the new capability +- =Deprecate workflow Y= by date Z +#+end_example + +* Mode B — Situational + +A life or work event Craig wants every project to know. The point is to beat the silo problem: Craig forgets to mention something during a conversation with one project, but it actually bears on how that project works and on the agent's decisions or ideas. Broadcast once, and every agent already knows by the time he's chatting with them. + +** Composing principle + +- *No per-project tailoring.* The broadcasting agent does NOT figure out how the event is relevant to each project. One general message goes to all. Each receiving agent decides relevance to its own current tasks. +- *General, not comprehensive.* Communicate a good amount of detail, but don't try to be exhaustive for a complex situation. Enough that an agent unfamiliar with it gets the picture and knows the shape of what changed. The receiving agent can ask Craig follow-up questions after startup. +- *Gather, then ask for gaps.* Pull the salient facts from the current session and project. Ask Craig only for the few you don't have (the concrete ones an agent would need — dates, who, where, what's changing). + +Write the body to =/tmp/broadcast-<topic>.org= with this structure: + +#+begin_example +,#+TITLE: Broadcast: <one-line situation> +,#+DATE: YYYY-MM-DD +,#+SOURCE: <sender project name> + +,* What's happening + +<A general but substantive summary: what the event is, who/what/where, any +dates or duration, what's changing. General, not comprehensive.> + +,* For the receiving agent + +This is situational context Craig wanted every project to have — said once and +broadcast, not a task and not tailored to this project. + +- Decide for yourself whether and how it bears on this project's work and your + suggestions. He is not prescribing actions; you judge relevance. +- Record it so it persists: add it to this project's =notes.org= Active + Reminders. Time-box it with an end date if the situation has one (a return + date, a departure date); otherwise hold it as standing context until it is no + longer relevant. +- Surface it at the next startup and raise it if it touches current work. +- Ask Craig any follow-up questions then — this message is deliberately general. +#+end_example + +The "For the receiving agent" block is fixed text — it travels with every situational broadcast so the message is self-describing. A receiving project's =process-inbox= reads it and acts on those instructions without needing any special-casing; the value gate accepts it as situational awareness that improves how the project works. + +** Receiving behavior (what a project does with an incoming situational broadcast) + +When =process-inbox= encounters a =Broadcast:= item, the disposition is *record-and-hold*, not file-as-task: + +1. Add a dated entry to =notes.org= Active Reminders capturing the situation and its end date (if any). +2. If the event bears on an open task, note the connection in that task's body. +3. Surface it at the next startup; retire it once its end date passes or it stops being relevant. + +* Common Mistakes + +1. *Broadcasting project-specific work.* Each broadcast costs N inbox files across the fleet. Routine handoffs go through =inbox-send= directly. +2. *Per-project tailoring in situational mode.* The broadcasting agent does not analyze relevance per project — that's the receiving agent's job. One general message, full stop. +3. *Over-engineering the situational summary.* General, not comprehensive. The receiver asks follow-ups; don't stall trying to anticipate every project's needs. +4. *Skipping the announcement structure.* Free-form announcements force every recipient to parse them differently. Use the rigid headings. +5. *Sender-includes-itself.* The discovery helper excludes the sender automatically. Don't override it — broadcasting to your own inbox creates a self-reply loop. +6. *Forgetting Phase E.* A broadcast that partially succeeded is the failure mode you'll never notice. Always check the per-target results. +7. *Announcement without an "Action required" line, or situational without the "For the receiving agent" block.* Each mode's recipients need to know what the message is for. + +* Living Document + +If the discovery roots change (a new top-level directory for AI projects), update =broadcast.py='s =SEARCH_ROOTS=. If the per-broadcast structure proves too rigid or too loose, tune the mode templates. If recipient projects complain about broadcast noise, the rule is "broadcast less," not "structure broadcasts harder." If situational broadcasts start needing per-project routing, that's a sign the silo is better solved by the shared org-roam KB than by fan-out — revisit before adding routing logic here. diff --git a/claude-templates/.ai/workflows/cross-project-broadcast.org b/claude-templates/.ai/workflows/cross-project-broadcast.org deleted file mode 100644 index 63af84f..0000000 --- a/claude-templates/.ai/workflows/cross-project-broadcast.org +++ /dev/null @@ -1,139 +0,0 @@ -#+TITLE: Cross-Project Broadcast Workflow -#+AUTHOR: Craig Jennings & Claude -#+DATE: 2026-05-29 - -* Overview - -Fan out a single message to every AI project's inbox in one operation. Discovers projects by fingerprint (any directory with =.ai/protocols.org=) and delivers via the existing =inbox-send.py= per-target. The point is announcing a new capability or shared change once, instead of hand-walking 20+ projects. - -* When to Use This Workflow - -User triggers: - -- "broadcast this to every project" -- "notify every project about <thing>" -- "fan out this announcement" -- "let every project know X is available" - -Automatic invocation: - -- *New machine-global capability landed.* A new script in =~/.local/bin/=, a new MCP server registered, a new tool installed (e.g. =signal-cli=, =page-signal=). Projects need to know it's available so their agents can use it. -- *Shared rule or protocol change.* When a change to =claude-rules/= or =claude-templates/.ai/= materially affects how every project's agent should behave, broadcast a heads-up so the next session of each project picks it up explicitly rather than discovering it on rsync. -- *Deprecation notice.* When a script, workflow, or rule is going away, give every project a chance to migrate before the removal lands. - -* When NOT to Use This Workflow - -- *Project-specific work.* A handoff intended for one project goes through =inbox-send= directly, not broadcast. -- *Routine status updates.* The session log and todo.org cover routine work. Broadcast is for capability- or rule-level changes. -- *Bulk noise.* Every broadcast adds N inbox files. Use sparingly. Ask whether projects actually need to know. -- *Every commit.* Most commits are project-internal hygiene (audit, intake, TODO updates) that the other projects don't care about. Even cross-project workflow updates rsync into every project's =.ai/= at next startup, so the capability lands without a broadcast. Broadcast only when projects need to *act* on the change (use a new tool, migrate before a deprecation, change their behavior per a new rule). Resist the urge to fan out a "here's what shipped today" digest. The startup rsync already carries the bits; the broadcast carries the *attention*, and attention is the costly resource. - -* Cadence Guideline - -Broadcasts are capability-and-rule-level events, not commit-level events. A reasonable cadence is one to four broadcasts per month, depending on what landed. Five reasons for the restraint: - -1. Each broadcast costs N inbox processings across the fleet (currently ~23 targets). Daily broadcasts mean daily noise. -2. Most commits are project-internal. Other projects do not need to read about another project's TODO sweeps, audit passes, or hygiene work. -3. The startup rsync already does the capability work. Broadcasts add awareness, not availability. -4. Aggregation wins. One weekly "here's what shipped" digest beats ten per-commit pings on every dimension. Fewer inbox files, easier to skim, easier to defer reading. -5. Per-commit broadcasts train projects to ignore inbox. Then a real handoff gets missed. - -If a session ships several broadcastable changes, bundle them into one broadcast at session end rather than firing one per commit. - -* The Workflow - -** Phase A — Discover targets - -Run the discovery helper: - -#+begin_src bash -python3 .ai/scripts/cross-project-broadcast.py --list -#+end_src - -The helper scans =~/code/=, =~/projects/=, =~/.emacs.d= for any directory containing =.ai/protocols.org=. Prints the basename and full path of each, in sender-excluded order (the current project never receives its own broadcast). - -** Phase B — Compose the message - -Write the broadcast body to =/tmp/broadcast-<topic>.org=. Required structure: - -#+begin_example -,#+TITLE: <one-line summary> -,#+DATE: YYYY-MM-DD -,#+SOURCE: <sender project name> - -,* What's new - -<two to five sentences: the capability, rule, or change being announced.> - -,* How to use it - -<two to five lines: the concrete invocation, command, or path. Code examples in =#+begin_src= blocks. No prose walls.> - -,* Why this matters / when to use - -<two to four sentences: the user-facing rationale and the discrimination rule for when the new thing applies vs. when existing alternatives suffice.> - -,* Action required - -<one of:> -- =FYI=, no action required (most broadcasts) -- =Update workflow X= to reference the new capability -- =Deprecate workflow Y= by date Z -#+end_example - -The structure is rigid on purpose. Every project's next session has to read 20+ broadcasts as efficiently as possible. The standard headings let the agent scan in seconds. - -** Phase C — Confirm scope with the user - -Surface the discovered project list and the message inline. Ask: - -#+begin_example -Broadcast scope: -- Target projects: <N> (list) -- Message: <2-line summary> -- Action required: <FYI / update workflow / deprecation> - -1. Send to all <N> targets (recommended) -2. Exclude specific projects (name them) -3. Cancel — message stays at /tmp/broadcast-<topic>.org -#+end_example - -** Phase D — Fan out - -Run the broadcast helper with the composed message: - -#+begin_src bash -python3 .ai/scripts/cross-project-broadcast.py \ - --file /tmp/broadcast-<topic>.org \ - [--exclude project1 --exclude project2 ...] -#+end_src - -The helper iterates over targets, runs =inbox-send.py <target> --file <broadcast>= per target, captures success/failure per project. The =from-<sender>= prefix in the resulting filename traces provenance. - -** Phase E — Report - -Summarize the fan-out: - -- Total targets discovered -- Sent successfully (count) -- Failed (list with reason) -- Excluded (list with reason) - -If any failures, surface them — silent failure on a broadcast means some projects never learn about the change. - -** Phase F — Cleanup - -Delete =/tmp/broadcast-<topic>.org=. The content lives in each target's inbox now. - -* Common Mistakes - -1. *Broadcasting project-specific work.* Each broadcast costs N inbox files across the fleet. Routine handoffs go through =inbox-send= directly. -2. *Skipping Phase B's structure.* Free-form broadcasts force every recipient to parse them differently. Use the rigid headings. -3. *Sender-includes-itself.* The discovery helper excludes the sender automatically. Don't override it — broadcasting to your own inbox creates a self-reply loop. -4. *Forgetting Phase E.* A broadcast that partially succeeded is the failure mode you'll never notice. Always check the per-target results. -5. *Broadcasting without an "Action required" line.* Recipients need to know whether this is FYI or whether their project has to do something. The line is non-optional. -6. *Using broadcast as a substitute for documentation.* Capability announcements should be paired with a rule or workflow update so the next-next session can rediscover the capability from canonical docs, not from a stale inbox file. - -* Living Document - -If the discovery roots change (a new top-level directory for AI projects), update =cross-project-broadcast.py='s =SEARCH_ROOTS=. If the per-broadcast structure proves too rigid or too loose, tune Phase B. If the recipient projects start complaining about broadcast noise, the rule is "broadcast less," not "structure broadcasts harder." |
