Rule: Any launcher / dispatch / orchestration script that produces external side-effects (ntfy ping / email / Slack message / webhook POST / GitHub PR comment / git push to non-personal-fork / external API call) MUST:

  1. Have a --dry-run flag that prints what it WOULD do but does NOT execute the side-effect.
  2. Default-validate scripts in --dry-run before running the real path during testing.
  3. Route ALL side-effects through a single helper function (e.g. ntfy_send, slack_post, email_send) that respects the dry-run flag — never inline curl ntfy.sh/... in multiple places.

Why (Rich-trigger 2026-05-03T21:30 BST): I added an A2-dispatch precheck to ~/testatetech/docs-strategy/scripts/dispatch-zeta-3-spike.sh that fires BLOCKED [zeta-3 A2]: A1 closure-bundle missing; A2 not dispatched to ntfy.sh/davieshq2026 when A2 is dispatched without A1’s closure-bundle. I then ran the script live to test the precheck. Rich’s phone pinged unexpectedly — he hadn’t asked for a spike to run, and the message looked like a real failure rather than a smoke-test artefact. He flagged it: “i received a ntfy zeta-3 A2 A1 closure-bundle missing. A2 not dispatched”.

Important nuance — Rich likes ntfy and wants more of it (Rich-clarification 2026-05-04):

This is NOT a “minimise ntfy” rule. Rich’s stated position:

“i like using ntfy when we are using claude, and would like to use it more”

ntfy is a deliberate Claude→Rich phone-alert channel for the moments where Rich is genuinely waiting to hear something. Use it more, not less. Specifically:

  • Heartbeats during long autonomous runs (~every 15 min with [N/M] progress markers per global CLAUDE.md §7).
  • Completions as the LAST action after a verified push (per autopilot prompt techniques).
  • Decisions made during unattended sessions — DECISION-MADE [N/7]: <what> — <rationale> rather than asking and blocking.
  • Real blockers the operator genuinely needs to action — but only when the launcher is in real mode, not smoke-testing.

The rule below is narrower than “avoid ntfy”: it says smoke-tests must not fire ntfy through the real topic. The fix is --dry-run-by-default + a single suppression-aware helper, not “send fewer messages.”

Topic: default to ntfy.sh/davieshq2026 unless project CLAUDE.md overrides. Subscribe via the ntfy.sh app or https://ntfy.sh/davieshq2026.

How to apply:

  • Before authoring any launcher script with side-effects, plan the --dry-run flag from the start.
  • Route side-effects through <verb>_send helpers — single point of suppression.
  • Smoke-test new dispatch logic with --dry-run first; only exercise the real side-effect path when (a) Rich is expecting it OR (b) the side-effect target is a test/sandbox topic (e.g. ntfy.sh/test-davies-noise), never the real one.
  • For ntfy specifically: the canonical TT topic davieshq2026 is the user’s personal phone alert. A test topic lives in stale instructions but isn’t currently configured — if smoke-testing requires real ntfy, ask Rich to confirm the test topic before running.
  • This applies retroactively too: any existing launcher script without --dry-run should have one added when next touched.

Anti-pattern caught 2026-05-03: curl -fsS -d "$msg" ntfy.sh/$NTFY_TOPIC inlined directly inside the main flow + smoke-tested live without —dry-run = real phone ping during validation pass.

Cross-reference:

  • Fixed at commit pending in docs-strategy main; ntfy_send() helper introduced + --dry-run flag added.
  • Related discipline: feedback_unattended_mode_when_scope_locked (don’t ping when not asked); feedback_executing_actions_with_care (low-blast-radius before high-blast-radius).