From session-orchestrator
Scans all repos in your portfolio, ranks free ones by priority×staleness×readiness, recommends the best, atomically claims it, and routes you there. Use after finishing a session or when asking "what should I work on next".
How this skill is triggered — by the user, by Claude, or both
Slash command
/session-orchestrator:dispatchersonnetThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
> Cross-repo autopilot front-door — enumerate → rank → owner-AUQ → atomic claim → route. Read-only until the operator confirms; the only mutating step is the atomic `session.lock` claim, and it happens BEFORE any launch.
Cross-repo autopilot front-door — enumerate → rank → owner-AUQ → atomic claim → route. Read-only until the operator confirms; the only mutating step is the atomic
session.lockclaim, and it happens BEFORE any launch.
The dispatcher answers one question: "of all my repos, which is the most worthwhile to work on right now, and is it free?" It scans the confinement-root children, resolves each repo's free/busy status from its session.lock v2 lease (same lease semantics as the vault-status board), ranks only the FREE ones by priority × staleness × readiness, and recommends the single best one. You confirm via a picker, it claims the lease atomically (winning the race or excluding-and-re-ranking on a loss), then routes you to the entry command for that repo. Busy repos are listed-as-such, never selected.
/session, /plan, or /discovery directly in that repo./portfolio (gitlab-portfolio), not the dispatcher.gitlab-ops.AskUserQuestion (unavailable in dispatched agents; see .claude/rules/ask-via-tool.md AUQ-004).Run the read path (non-mutating). Either invoke the CLI directly or call runDispatch from the module:
node scripts/lib/dispatcher/cli.mjs --json
The JSON object has keys { candidates, free, ranked, warnings, recommended }:
candidates — every repo found below the confinement root (busy ones LISTED, not dropped).free — the subset with no live lease (free === true).ranked — the free candidates sorted DESC by score; ranked[0] is the recommendation.recommended — ranked[0] or null (no free candidates).warnings — human-readable degradation notes (glab/gh missing, host probe failed). Surface every warning to the operator — they explain why a repo was ranked on partial signals.Ranking combines three signals per repo (implementation: scripts/lib/dispatcher/rank.mjs): backlog priority (critical/high counts), staleness (days since the last completed session — older = more worthwhile, capped at 90d), and readiness (CI status × host resource verdict — only ever dampens). A null priority (glab/gh missing) is ranked on staleness × readiness alone with a warning; the dispatcher NEVER blocks on a missing CLI.
Decides ONE thing for the recommended repo R (
ranked[0]): may the dispatcher launch WITHOUT per-selection confirmation, or must it inform-and-ask? This sits BETWEEN ranking (Phase 1) and the Owner-AUQ (Phase 2). It is a pre-launch decision, NOT a per-iteration kill-switch — the autopilot loop's 10 kill-switches are reused unchanged once a session is running (seeskills/autopilot/SKILL.md § Pre-Loop Verdict Gate).
Compute the suitability verdict for R via the pure four-gate engine computeSuitabilityVerdict(deps) from scripts/lib/autonomy/suitability.mjs. The engine is pure + DI: the dispatcher gathers every signal and passes it in.
Source each verdict input as follows (the dispatcher already has most in hand from Phase 1):
deps field | Source | Notes |
|---|---|---|
autonomy | resolveDispatcherAutonomy({ committed, env, ownerConfig }) from scripts/lib/config/dispatcher-autonomy.mjs | The effective dial. Defaults to 'off' when unset (fail-closed). |
confidenceFloor | the confidence-floor from the parsed dispatcher-autonomy: block (default 0.5) | Same source object as autonomy. |
confidence | mode-selector selectMode(signals).confidence (0..1 float) for the recommended session-type | The same mode-selector the Phase-2 heuristic and autopilot use. |
ci | checkCiStatus({ repoRoot: R }) → { status } | null | CRITICAL (NICE-b): Phase-1 rank.mjs exposes only the BARE status string (readiness.ciStatus). The engine's G3 gate expects an OBJECT { status } — wrap it as { status: ciStatus } when you HAVE a status; on a CI-fetch FAILURE pass ci = null (checkCiStatus already returns null on failure — pass it straight through). Do NOT synthesize { status: undefined } (or {}): that present-but-unusable object hits the engine's MALFORMED branch ('CI signal malformed — treated as absent') instead of the clean ABSENT branch ('CI signal absent'). Both pass G3 + warn, but null is the honest "no signal" — reserve the malformed branch for a genuinely unexpected shape. A bare string ALSO hits the malformed branch — always wrap or null. |
resourceVerdict | the host resource verdict string ('green'|'warn'|'degraded'|'critical') from rank.mjs (readiness.resourceVerdict) or a fresh evaluate(probe(), thresholds).verdict | Host-level — already fetched once in Phase 1. NICE-b: on a genuine probe FAILURE (no signal), prefer resourceVerdict = null over synthesizing 'green'. null = "no signal" ⇒ G4 passes + warns ('resource signal absent') — honest. Synthesizing 'green' fabricates a positive signal the host never reported and can let an autonomous launch proceed against an unknown host state. Pass the real verdict string when you have one; null when you do not. |
recentRuns | readRecentAutopilotRuns({ repoRoot: R }) from scripts/lib/autopilot/recent-runs.mjs | NEW reader. Reads <R>/.orchestrator/metrics/autopilot.jsonl, returns the most-recent records (newest-last), never throws ([] on missing/unreadable). Pass the TRUE count — the engine's G2 gate omits-with-warn below 5 runs and otherwise checks fired/N < 0.2. |
The launch decision (FAIL-CLOSED invariant):
verdict = computeSuitabilityVerdict({ autonomy, confidenceFloor, confidence, ci, resourceVerdict, recentRuns })
IF autonomy === 'autonomous-gated' AND verdict.suitable === true:
# MAY launch WITHOUT per-selection confirmation:
# skip the Phase-2 AUQ and proceed straight to Phase 3 (atomic claim) → Phase 4 (route).
ELSE:
# INFORM the operator of verdict.rationale + verdict.warnings,
# then run the Phase-2 AUQ (ask before launch). NEVER auto-launch.
autonomy === 'autonomous-gated' AND verdict.suitable === true. Keying only on verdict.suitable is a fail-OPEN bug — it would auto-launch even in advisory/off mode. The autonomy field inside deps is ADVISORY-ONLY inside the engine (it pushes a warning but never flips suitable); the CALLER is responsible for the autonomy === 'autonomous-gated' half of the AND. resolveDispatcherAutonomy defaults to 'off' when no dispatcher-autonomy: config is present, so an absent config forces the inform-and-ask ELSE branch.null is "no signal", not a failure (NICE-b). Passing ci = null or resourceVerdict = null on a fetch/probe failure is the CORRECT honest wiring: the engine treats each null as absent ⇒ the corresponding gate (G3 / G4) PASSES and a warning is recorded ('CI signal absent' / 'resource signal absent'). A null signal does NOT by itself block an autonomous launch — it just surfaces a warning the operator sees. Reserve synthesized objects/strings for real signals; never fabricate a positive ('green' / { status: 'green' }) to paper over a missing probe.verdict.suitable === false REGARDLESS of confidence — the four-gate AND fails on G3/G4, and the engine words the rationale FORCED: CI red / FORCED: resource critical / FORCED: CI red + resource critical. For this branch to be reachable, the wiring MUST actually pass the live signals through: ci wrapped as { status: ciStatus } (a bare string or a synthesized-absent object would hit the malformed-PASS branch and mask a real red) and the real resourceVerdict string ('critical' must arrive lowercase-or-normalizable, NOT replaced by a fabricated 'green'). With the signals wired through, a CI-red / resource-critical repo in autonomous-gated mode falls to the ELSE branch: the gate INFORMS the operator (rationale + warnings) and ASKS via the Phase-2 AUQ — it NEVER auto-launches. (Wave 4 covers this with an integration test; this prose is the wiring contract.)resolveDispatcherAutonomy returns 'off' when no config is present, so an unconfigured repo correctly takes the ELSE branch (inform + ask). A CI-red or resource-critical repo also fails the engine's G3/G4 gate (the rationale words it FORCED: CI red / FORCED: resource critical), so even in autonomous-gated mode it falls to inform + ask. The dispatcher never auto-launches against a non-green verdict.autonomous-gated (verdict not suitable), surface verdict.rationale (the one-line gate breakdown) and every entry in verdict.warnings to the operator BEFORE the AUQ, so they understand WHY confirmation is still required.Conditional (#682): SKIP this phase when Phase 1.5 green-lit an autonomous launch (
autonomy === 'autonomous-gated'ANDverdict.suitable === true) — in that branch proceed straight to Phase 3. In EVERY other case (any non-autonomous-gateddial, OR a non-suitable verdict) this phase RUNS: inform the operator of the verdict first, then ask. The dispatcher NEVER auto-launches outside the green-verdict autonomous-gated branch.
Present the decision to the operator via the AskUserQuestion tool — never inline prose (.claude/rules/ask-via-tool.md AUQ-001..005, enforced).
AskUserQuestion is a deferred tool. Call ToolSearch with "select:AskUserQuestion" ONCE per session before the first use to load its schema.(Recommended): the top-ranked free repo paired with a recommended session-type. Options 2–4 are overrides (other high-ranked free repos, or other session-types for the same repo). 2–4 options total, each with a one-line description explaining the trade-off.AskUserQuestion from inside a subagent — if a sub-step needs the decision, bubble it back to the coordinator (AUQ-004).Recommended session-type heuristic for option 1: high critical/high backlog ⇒ /session deep; stale-but-clean (no backlog signal, high staleness) ⇒ /discovery or /session housekeeping; unscoped/new work ⇒ /plan. Offer the alternatives as the other options.
Claim the lease for repo R BEFORE launching anything. This runs in BOTH branches — the operator-confirmed Phase-2 path AND the autonomous-gated green-verdict path that skipped Phase 2 (#682). The claim ALWAYS precedes the Phase-4 route: an autonomous launch does not bypass the atomic claim, it only bypasses the per-selection AUQ.
// via the module (preferred — returns the acquire() result verbatim)
import { claimRepo } from 'scripts/lib/dispatcher/cli.mjs';
const res = claimRepo({ repoRoot: R, sessionId, mode, ttlHours, semanticSessionId });
Or reuse the primitive directly: acquire({ sessionId, mode, ttlHours, repoRoot, semanticSessionId }) from scripts/lib/session-lock.mjs. The claim is a linkSync create-or-fail = atomic.
ok: true → the claim is held. Proceed to Phase 4.ok: false (race lost / busy — reasons: active, stale-pid-alive, stale-pid-dead, fs-error, …) → exclude R, re-rank the remaining free candidates (drop R from free, re-run Phase 1's rank step), and re-present Phase 2. Loop until a claim succeeds or no free candidate remains (then Phase 5).Do NOT reinvent the claim — always go through claimRepo/acquire. The ok:false path is the load-bearing concurrency guard: two parallel dispatchers can both recommend R, but only one wins the linkSync; the loser must re-rank, never force.
With the lease held, the coordinator invokes the chosen entry slash-command for repo R:
/session housekeeping or /session deep — execution modes./plan — read-only planning precursor (produces a wave plan; does not execute)./discovery — read-only investigation precursor (maps scope; does not execute)./plan and /discovery are read-only precursors, NOT execution modes — the menu may route to them, but they only produce artifacts for a later execution session. The full mode taxonomy lives in the mode-selector surface (P2 of this epic); the dispatcher only routes to the entry command the operator picked.
recommended === null / free empty) → report "all repos busy", and offer resume (an in-progress session) or wait via AUQ. Never force a selection of a busy repo.warnings array: rank on staleness × readiness only, surface the warning, continue. A missing CLI is never fatal.--start-dir → CLI exits 1 (user/input error); fix the path and re-run.node scripts/lib/dispatcher/cli.mjs [--json] [--dry-run] [--repo <name>] [--start-dir <path>] [--help] [--version]
| Flag | Description |
|---|---|
--json | Emit { candidates, free, ranked, warnings, recommended } as a single JSON object to stdout. |
--dry-run | Explicit non-mutating rank (the read path is already non-mutating; documents intent). |
--repo <name> | Filter the human-readable table to one repoName (informational; does not change ranking). |
--start-dir <path> | Override the scan root (defaults to the confinement root). |
--help / --version | Print usage / version and exit 0. |
Data → stdout, warnings/errors → stderr (never mixed). Exit codes follow .claude/rules/cli-design.md:
| Code | Meaning |
|---|---|
0 | Success |
1 | User/input error (e.g. bad --start-dir) |
2 | System error (unexpected dispatch failure) |
AskUserQuestion (AUQ-001). A numbered markdown list of repos is a bug.acquire MUST succeed before Phase 4. Launching then claiming re-opens the race the lease exists to close.free === false candidate.claimRepo/acquire; do not hand-roll a lockfile.ok:false — on a lost race you MUST exclude-and-re-rank, not retry the same repo or proceed without the lease.verdict.suitable alone (ignoring autonomy === 'autonomous-gated') auto-launches in advisory/off mode. ALWAYS gate on BOTH.rank.mjs exposes ciStatus as a bare string; computeSuitabilityVerdict wants { status }. Wrap it ({ status: ciStatus }) or pass null — a bare string silently hits the malformed-absent branch. A red CI fed as a bare string would PASS G3 (masked as malformed) instead of forcing the FORCED-fail branch (NICE-c).null (NICE-b) — on a CI-fetch or resource-probe failure, pass ci = null / resourceVerdict = null (honest "no signal" ⇒ gate passes + warns). Do NOT synthesize { status: undefined } (hits the malformed branch) and do NOT fabricate 'green' / { status: 'green' } (invents a positive signal the host never reported and can green-light an autonomous launch against an unknown state).recentRuns below 5 — passing fewer than the true on-disk count when ≥ 5 runs exist falsely triggers the engine's <5-run omission branch and skips the kill-switch gate. Pass the TRUE count; never call readRecentAutopilotRuns with limit < 5 on the launch-gate read (the reader honours a small limit literally and will not clamp it upward).autonomous-gated. Never proceed straight to claim on a non-suitable verdict.runDispatch / cli.mjs without a claim) is NON-MUTATING. The ONLY mutating step is the Phase-3 atomic claim.linkSync create-or-fail via acquire(...). ok:false ⇒ exclude the repo and re-rank — this is the concurrency guard, not an error to swallow.AskUserQuestion with option 1 = recommendation (Recommended); coordinator-only.MAY launch without per-selection confirmation requires autonomy === 'autonomous-gated' AND verdict.suitable === true. Every other case informs the operator (rationale + warnings) and asks via Phase-2 AUQ. The dispatcher NEVER auto-launches outside that single green branch. The 10 autopilot kill-switches are reused unchanged once a session is running — the verdict gate is a pre-launch decision, not a kill-switch.scripts/lib/dispatcher/cli.mjs (orchestration: runDispatch, claimRepo) · scripts/lib/dispatcher/enumerate.mjs (enumeration + free/busy) · scripts/lib/dispatcher/rank.mjs (scoring) · scripts/lib/session-lock.mjs (acquire atomic claim) · scripts/lib/autonomy/suitability.mjs (computeSuitabilityVerdict four-gate engine, #682) · scripts/lib/config/dispatcher-autonomy.mjs (resolveDispatcherAutonomy effective dial, #682) · scripts/lib/autopilot/recent-runs.mjs (readRecentAutopilotRuns kill-switch-history reader, #682).npx claudepluginhub kanevry/session-orchestrator --plugin session-orchestratorOrchestrates complex tasks by decomposing into DAGs and dispatching focused sub-agents with minimal context. Invoke /orchestra run for multi-step, multi-repo coding workflows.
Orchestrates git operations with safety tiers: read-only inline, safe writes via background agent, destructive with preflight confirmation. Manage commits, PRs, branches, worktrees, releases.
Detects repository state and user intent to route to appropriate skills or CLI commands for setup, sync, repair, or launch.