OmniNode-ai

/onex:self_healing_dispatch — Self-Healing Orchestration Wrapper

Self-healing orchestration wrapper — accepts ticket list or epic ID, groups by repo, dispatches via TeamCreate (enforced), monitors workers via agent_healthcheck, auto-recovers stalls with bounded retry (max 2 redispatches per task), logs all events to structured NDJSON.

OmniNode-ai 4 3 Updated 4mo ago
GitHub

Install

npx skillscat add omninode-ai/omniclaude/plugins-onex-skills-self-healing-dispatch

Install via the SkillsCat registry.

SKILL.md

/onex:self_healing_dispatch — Self-Healing Orchestration Wrapper

Skill ID: onex:self_healing_dispatch
Workstream: B Phase 3 — built on earlier stall-detection and dispatch-enforcement foundations

Purpose

Single orchestration entrypoint that combines:

  • Phase 1 signals — stall detection and bounded redispatch from agent_healthcheck
  • Phase 2 enforcement — TeamCreate dispatch (not advisory) from the dispatch-mode guardrail

Handles workloads of ≤10 tickets across ≤4 repos without human intervention.

Invocation

/onex:self_healing_dispatch --tickets PROJ-1234,PROJ-5678,PROJ-9012
/onex:self_healing_dispatch --epic-id PROJ-7253
/onex:self_healing_dispatch --epic-id PROJ-7253 --dry-run

Announce at start:

[self_healing_dispatch] Starting orchestration run <run_id> | tickets: N | mode: <live|dry-run>

Execution Protocol

Phase 0 — Input Resolution

  1. If --tickets given: parse and validate each as OMN-\d+. Fail fast on invalid IDs.
  2. If --epic-id given: call decompose_epic to get child ticket list. Fail fast if decompose returns empty.
  3. Exactly one of --tickets / --epic-id must be provided. Emit usage error and stop if neither or both.
ERROR: Provide exactly one of --tickets or --epic-id.
Usage:
  /onex:self_healing_dispatch --tickets PROJ-1234,PROJ-5678
  /onex:self_healing_dispatch --epic-id PROJ-7253

Phase 1 — Repo Grouping

Call the Python orchestrator:

from omniclaude.hooks.self_healing_orchestrator import orchestrate, group_by_repo

result = orchestrate(
    ticket_ids=["PROJ-1234", "PROJ-5678"],
    epic_id="PROJ-7253",   # or None
    repo_hints={"PROJ-1234": "omniclaude"},  # or None
    run_id="orch-20260405T120000Z",
)

The orchestrator emits an orchestration_planned NDJSON event and returns a OrchestratorResult.

Grouping rules:

  • Tickets with repo hints land in that repo's group.
  • Tickets without hints: resolve from Linear ticket metadata (labels field or team field).
  • Tickets that cannot be resolved land in the unassigned group — surface a warning and stop.
WARNING: Cannot resolve repo for PROJ-9999. Provide --repo-hints or set the ticket's repo label in Linear before orchestrating.

Phase 2 — TeamCreate Dispatch (enforced)

For each dispatch group, spawn a named worker via TeamCreate. Never use a bare Agent() call — the dispatch-mode guardrail will fire an advisory, and this skill enforces TeamCreate unconditionally.

# Pseudo-code — executed by the skill entrypoint, not the Python module
team_name = f"orch-{run_id}"

for group in result.groups:
    prompt = build_team_dispatch_prompt(group, epic_id=result.epic_id)
    Agent(
        name=f"worker-{group.repo}-{run_id}",
        team_name=team_name,
        prompt=prompt,
    )

Log a worker_dispatched event per group:

{"timestamp_utc":"...","event":"worker_dispatched","run_id":"...","repo":"omniclaude","tickets":["PROJ-1234"]}

Phase 3 — Monitoring and Stall Recovery

After dispatching all workers, enter a polling loop. Use onex:agent_healthcheck as a composable sub-skill to check each active worker:

Skill(skill="onex:agent_healthcheck", args="--ticket-id PROJ-1234 --agent-id worker-omniclaude-orch-... --timeout-minutes 2 --max-redispatches 2")

On stall detected (status == stalled or failed):

from omniclaude.hooks.self_healing_orchestrator import record_stall_recovery, build_stall_recovery_prompt

should_redispatch, attempt = record_stall_recovery(
    group=group,
    ticket_id=stalled_ticket_id,
    run_id=run_id,
    completed_tickets=completed_so_far,
)

if should_redispatch:
    prompt = build_stall_recovery_prompt(group, stalled_ticket_id, attempt, completed_so_far)
    Agent(
        name=f"recovery-{stalled_ticket_id}-attempt-{attempt}",
        team_name=team_name,
        prompt=prompt,
    )
else:
    # Exceeded MAX_REDISPATCHES (2) — escalate
    linear.save_issue(identifier=stalled_ticket_id, status="Blocked",
                      comment=f"Auto-blocked: stalled {attempt} times. See dispatch log.")

Recovery prompt rules:

  • Include only remaining (uncompleted) tickets — never re-scope to the full original list.
  • Include the checkpoint path if written by agent_healthcheck.
  • Cap recovery at MAX_REDISPATCHES = 2. Third stall → escalate to Blocked.

Phase 4 — Completion

Exit when all workers have returned completed or escalated status.

Emit final orchestration_complete event:

{
  "timestamp_utc": "...",
  "event": "orchestration_complete",
  "run_id": "...",
  "total_tickets": 5,
  "stalls_recovered": 1,
  "escalated": [],
  "elapsed_seconds": 430
}

Print final summary:

[self_healing_dispatch] Run <run_id> complete.
  Tickets: 5 | Recovered stalls: 1 | Escalated: 0
  Log: $ONEX_STATE_DIR/dispatch-log/2026-04-05.ndjson

Constraints

Constraint Value
Max tickets 10
Max repos 4
Max redispatches per ticket 2
Stall threshold 2 minutes (delegated to agent_healthcheck)
Dispatch mode TeamCreate only — bare Agent() forbidden

Event Log Schema

All events append to $ONEX_STATE_DIR/dispatch-log/{YYYY-MM-DD}.ndjson. One JSON object per line, never pretty-printed.

Event Key Fields
orchestration_planned run_id, epic_id, ticket_count, group_count, repos
worker_dispatched run_id, repo, tickets
stall_recovery_dispatched run_id, ticket_id, redispatch_attempt, max_redispatches
escalated_to_blocked run_id, ticket_id, attempt_count
orchestration_complete run_id, total_tickets, stalls_recovered, escalated, elapsed_seconds

Dry-Run Mode

When --dry-run is set:

  • Run Phases 0 and 1 (resolve + group).
  • Print the planned dispatch groups.
  • Emit orchestration_planned event.
  • Do NOT launch any workers (skip Phases 2–4).
[self_healing_dispatch] DRY RUN — no workers launched.
Planned groups:
  omniclaude: PROJ-1234, PROJ-5678
  omnibase_core: PROJ-9012
Log: $ONEX_STATE_DIR/dispatch-log/2026-04-05.ndjson

Backing Module

All grouping, prompt generation, stall accounting, and log emission logic lives in:

src/omniclaude/hooks/self_healing_orchestrator.py

Do not duplicate this logic inline. Import and call the module functions.

See Also

  • onex:agent_healthcheck — stall detection sub-skill
  • onex:dispatch_watchdog — epic-level watchdog for larger wave runs
  • onex:epic_team — full epic orchestration (use for >10 tickets or >4 repos)