T-2QGZ-ops-sdlc-notify-send-test-grows-phase-2-s-minimal-notifier
State: closed/done · Impact: medium · Complexity: medium
sdlc notify send/sdlc notify test ops, growing Phase 2’s minimal desktop/ntfy notifier into the
full notify: surface.
Approach
Section titled “Approach”The channel-routing notify() rewrite and lib/services/notify/webhook.ts
that this task’s ops and call-site rewiring build on landed under
T-IIZZ instead of here (built early so this task had a real runtime to
build on from the start — see that task’s Approach item 5 and Areas table).
This task’s own work:
-
sdlc notify send --channel <name> [--message <text>]: dispatch straight to one named channel fromnotify.channels, bypassing event routing — the manual “does this channel work” primitive. Non-zero exit on that channel’s failure. -
sdlc notify test: iterate every channel innotify.channels, send each a canned probe message, print one pass/fail line per channel, exit non-zero if any failed. -
Rewire every Phase-2 call site to fire one of the 9 canonical event names (dropping the retired
orchestrator.notify-shaped config read):File Old event New event Note orchestrator/ticks/work.ts(hook-park-consumed path)'task.parked''task.parked'unchanged — already canonical orchestrator/ticks/work.ts(step-failure path)'work-park''task.parked'one canonical name for “this task’s dispatch stood down,” whichever path discovered it orchestrator/ticks/issues.ts'stage.ceiling_reached''stage.ceiling_reached'unchanged — already canonical services/hooks/run.ts(on_fail: block)'hook.blocked''hook.blocked'unchanged — already canonical services/hooks/run.ts(on_fail: park)'hook.parked'(dropped) superseded by the worktick’s own later'task.parked'; not one of the 9 configurable names anywayorchestrator/ticks/_pr_action.ts(max_roundsexhausted)'max-rounds''pr.ci_failed'or'pr.needs_response'a reasonthreaded in from the caller picks whichorchestrator/ticks/prs.ts/merges.ts— — pass that reason('ci-failed'for a CI-FAILED verdict,'needs-response'for NEEDS-RESPONSE or a CONFLICTS-drivenpr-respond) into_pr_action.ts -
Wire the two additional events that already have a live detector on this branch, beyond the Phase-2 call sites:
File Event Fires when services/lease/ops/reconcile.ts'lease.expired'once per expired-task-leasesanomaly the run observes (report mode already reads the world; this adds a best-effort notify, no new write)services/session/ops/launch.ts'session.ended'a headless session’s record is finalized (the second writeSessionRecordcall, after the child exits) — interactive sessions have no record and stay unwired, matching the module’s own “a person IS the audit trail” contract -
Regenerate
lib/_generated/op-manifest.ts(bun run solutions/ontological/scripts/gen-op-manifest.ts) so the two new ops are in the compiled binary’s static import graph.
| Area | Note |
|---|---|
solutions/ontological/lib/services/notify/ops/send.ts | new op: sdlc notify send |
solutions/ontological/lib/services/notify/ops/test.ts | new op: sdlc notify test |
solutions/ontological/lib/services/orchestrator/ticks/{work,issues,prs,merges,_pr_action}.ts | rewired call sites |
solutions/ontological/lib/services/hooks/run.ts | rewired call site |
solutions/ontological/lib/services/lease/ops/reconcile.ts | new lease.expired wiring |
solutions/ontological/lib/services/session/ops/launch.ts | new session.ended wiring |
Acceptance criteria
Section titled “Acceptance criteria”- AC-1:
sdlc notify testreports one pass/fail line per entry innotify.channels, exit 0 iff every channel’s send succeeded; exercised in tests with a faked transport per channel type (no real network call, no real desktop notification). - AC-2:
sdlc notify send --channel <name>dispatches to exactly that channel and exits non-zero when it fails. - AC-3: Every event
notify()fires is one of the 9 canonical names; no code or test references'work-park','hook.parked','max-rounds', ororchestrator.notifyafter this task. - AC-4: An event absent from
notify.events, or mapped to[], is a silent no-op whennotify()fires it — no channel dispatched, no error. - AC-5:
lease.expiredfires fromsdlc lease reconcile’sexpired-task-leasesdetector andsession.endedfires when a headlesssdlc session launch --headlessrun’s session record is finalized — both exercised with faked channel transports.
Out of scope
Section titled “Out of scope”Both are valid config/event names (T-IIZZ’s schema accepts routing
them), but no code on this branch detects either condition. Inventing
either detector is a materially larger, separately-scoped task, not a
Phase-8 notifier-surface change. Declared as a parked follow-up in the
phase hand-back.
| Event | Why no detector exists today |
|---|---|
phase.ready_for_review | Presently a Tier-2 phase-orchestrator judgment call (moving a PR out of draft once its gate passes), not a deterministic tick. |
orchestrator.dead_stop | Explicitly named in orchestrator/ops/watch.ts’s own doc comment as a known blind spot with no detector today. |
Dependencies
Section titled “Dependencies”T-IIZZ(thenotify:schema, and the channel-routingnotify()+webhook.tsruntime it grew, that this task’s ops and call-site rewiring build on).
Depends on
Section titled “Depends on”T-IIZZ