Prompt file imported from maingocanh1702/bestmintonclub-payment-check-bot (
.claude/commands/p4-notify-tg-primitives-split.md). Fill in{{AUTOPILOT_CODEX_BIN}}before use. Copyright stays with the author.
/p4-split — notify-tg-primitives-split (Payment · lane P · Wave 4 SLICE 0 · P2)
Module-boundary split that kills the Wave-4 cycle at its root. src/notify.ts is two things fused: the Telegram transport (grammY bot + sendMarkdownSafe/editMarkdownSafe/sendHtmlSafe/notifyOwner/postToGroup/escHtml/mentionHtml/isOwner) and the transport-agnostic dispatcher (notifyRosterChange + the 5 dispatch* fns). Transports import notify.ts statically for the TG primitives while the dispatcher lazily require()s the transports — that asymmetry is the cycle. Move the primitives to src/tg/send.ts; notify.ts becomes dispatcher-only. Pure move, zero behavior change. Manual-only, 2 consecutive FULL Codex clean rounds, STOP_AT_READY.
Artifacts:
- Prompt:
docs/autopilot/prompts/notify-tg-primitives-split.md - Manifest:
docs/autopilot-manifests/notify-tg-primitives-split.json - Backlog:
notify-tg-primitives-split - Plan:
docs/planning/global/repo-wide-refactor-plan-2026-07-15.md§9 (Wave 4)
Why this is slice 0 (inserted before the plan's 5): the plan put cycle-removal LAST, but it's cheap and de-risks everything before it. .architecture-boundaries-baseline.json already baselines 5 no-circular + 3 core-must-not-depend-on-transport entries tagged "Wave 4". Doing the split first gives every later slice a clean graph and removes the partial-initialization hazard that already cost a debugging round (see the require-vs-import rationale at notify.ts:261-275).
Verified static transport→core edges: zalo/notify-players.ts:20 (escHtml, mentionHtml, notifyOwner, sendHtmlSafe) · zalo/reminder-players.ts:21 (sendHtmlSafe, notifyOwner) · tg/reminder-players-tg.ts:14 (escHtml, mentionHtml). messenger/notify-players.ts does NOT import notify.ts.
⚠️ Main hazard: notify.ts registers a grammY middleware at module scope (~L37-45, wraps every TG update in runWithLang) and has a test-token API short-circuit (~L29-31). Moving bot moves both, and their timing becomes import-order dependent. STEP 0 must PROVE the middleware still registers exactly once and the test-token guard still fires — otherwise take the descope path (move only the provably-pure escHtml/mentionHtml into src/tg-html.ts, which alone breaks cycle chain D at zero risk) and re-scope.
Prerequisite: Wave 3 CLOSED (roster-read-models merged — origin/main a87c330). INFLIGHT empty → registers clean.
Codex pin: ~/.zshrc exports AUTOPILOT_CODEX_BIN = the payment codex-review-pin.sh (gpt-5.6-sol via npm 0.144.5). Fresh terminal picks it up; export below is a fallback.
Owner-terminal setup (fresh shell), after artifacts committed+pushed:
cd "/Users/maingocanh/Projects/Bestminton Club Payment check bot"
git fetch origin
git show origin/main:docs/autopilot/prompts/notify-tg-primitives-split.md >/dev/null
export AUTOPILOT_CODEX_BIN="{{AUTOPILOT_CODEX_BIN}}"
scripts/ap-run.sh notify-tg-primitives-split
ap-run.sh: worktree from origin/main → scope-gate register → codex smoke → auto-submit prompt → STOP_AT_READY. After READY: scripts/ap-land.sh notify-tg-primitives-split (add --squash if main advanced).
Wave-4 order after this (8 slices total, owner-approved option A — see plan §9): 1 notification-intent-model (types + recipientsFor + equivalence test — not types-only) → 2 notification-port-registry → 3 paid-confirm-notification-port (must resolve the paid_amount divergence explicitly) → 4 waitlist-notification-port (retires exit-criterion 3) → 5 roster-owner-notification-port → 6 reminder-notification-port (reminder.ts + member-notify.ts) → 7 notification-cycle-removal.
Wave-4 Definition of Done (owner-approved): npm run check:arch shows zero remaining "Wave 4"-tagged entries in .architecture-boundaries-baseline.json (16 at the time of writing: 5 no-circular + 13 core-must-not-depend-on-transport). Wave 4 is not "done" until the baseline is clean — exit criteria and baseline must not disagree.
