Skip to content

Commit 1af2807

Browse files
committed
feat(admin): /dispatch setup -- first-run wizard, detection, and a once-ever nudge (issue #92)
- bare /dispatch now detects where the deployment is: valid pointer or explicit env -> panel exactly as before; cwd scaffold or a reachable queue -> panel plus one hint; nothing anywhere AND queue unreachable -> the offer. An exported VALKEY_URL is probed, never trusted -- a dead URL still reaches the offer, and an ops outage on a configured deployment keeps the banner, never the wizard - /dispatch setup: dialogs-first, overlay-per-handoff (the tui suspend handle exists only inside a ctx.ui.custom factory; dialogs cannot run under a capturing overlay; stdin belongs to the attached child) -- pick a deploy dir, consented npm install of @edgehero/pi-dispatch@RUNTIME_VERSION under import-pi's spawn doctrine (no path in argv, --ignore-scripts, post-install version assertion that stops the wizard loudly), hand the terminal to pi-dispatch up (its own y/N gates ARE the host-mutation consents; --yes is forwarded to nothing), optional user-level service install, pointer written with the JSON shown verbatim then reapplied in-process, provider key printed never written, optional setup github hand-off, optional first trigger for the repo the session sits in (folder pre-filled, flow picked from its .pi/skills via the shared SKILL_NAME_RE, the in-place-edit warning printed, and the ai-trigger: allow line PRINTED for the operator to commit -- the wizard never writes repo files), then the panel - session_start nudge: reason startup only, hasUI only, sync-only checks, a marker file makes it once-ever, notify-only - every step individually declinable; declines continue converge-style; headless refuses; USED_API unchanged (spawns via node:child_process) Specs: new DES-FIRST-RUN-SETUP-WIZARD (incl. the recorded rejections); REQ-ADMIN-VIA-PI-EXTENSION amended (setup, the detection tree, the nudge, declined-offer acceptance); REQ-DEPLOYMENT-BOOTSTRAP scope carve-in (the wizard is a driver, not a power); Revision History rows in design.md + requirements.md; DES-TRIGGER-OUTSIDE-PI, DES-CLI-SURFACE, CONST-BUDGET-BEFORE-TOKENS unchanged, checked. operate-pi-dispatch SKILL.md gains the setup chapter (operator-typed only; the model asks the operator to type it). README first-run story; cli/init next-steps mention the wizard. Signed-off-by: Rob Boerman <robboerman@live.nl>
1 parent 3681ee2 commit 1af2807

11 files changed

Lines changed: 1527 additions & 7 deletions

File tree

README.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -279,6 +279,16 @@ then open the panel:
279279
pi install npm:@edgehero/pi-dispatch-admin # then, in pi: /dispatch
280280
```
281281

282+
**No deployment yet? The console builds one.** When `/dispatch` finds nothing — no pointer, no env, no
283+
config in the folder, queue unreachable — it offers **`/dispatch setup`**: pick a deployment folder
284+
(default `~/pi-dispatch`), consent to an `npm install` of the pinned runtime, watch `pi-dispatch up` run
285+
its own per-action prompts in your terminal, optionally install the worker as a user-level service, and
286+
land in the panel — with an optional first trigger for the repo you're sitting in, its flow picked from
287+
that repo's own `.pi/skills/`. Every step shows what it will do and asks first; every step can be
288+
declined; nothing is ever written into your repo (the `ai-trigger: allow` opt-in line is printed for
289+
*you* to commit), and no credential ever passes through a dialog. It's the same consented CLI underneath
290+
— the wizard just types it for you.
291+
282292
Two other ways to load it: from a clone, the in-repo `.pi/extensions` shim auto-loads once you've trusted
283293
the project; or point pi at the source with `pi -e admin/src/index.ts` (add that path to the `"extensions"`
284294
array in `~/.pi/agent/settings.json` to make it permanent). Pointing the panel at a **live** deployment is

admin/skills/operate-pi-dispatch/SKILL.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,19 @@ Use them like this:
5757

5858
The confirm is the approval step. Treat a decline as a final, legitimate answer.
5959

60+
## Setting up a deployment — `/dispatch setup`, and why you cannot run it
61+
62+
When the tools report no reachable deployment (queue unreachable, no configured paths), the fix is the
63+
first-run wizard — and it is **operator-typed only**: there is no model-callable setup tool, on purpose.
64+
Tell the operator to type `/dispatch setup`. It will, with a consent step per action: create a deployment
65+
folder, npm-install the pinned runtime into it, hand the terminal to `pi-dispatch up` (whose own y/N
66+
prompts gate the docker actions), optionally install the worker as a user-level service, write the
67+
deployment pointer so the panel finds everything afterwards, and offer a first trigger for the repo the
68+
session is in. What it will NOT do, ever: write into the operator's repo (the `ai-trigger: allow` line is
69+
printed for them to commit), accept a credential through a dialog, or run anything with `--yes`.
70+
Do not try to reproduce the wizard's steps through other tools or shell access — the sequencing exists
71+
so each mutation carries its own human gate.
72+
6073
## Staged packages — `run.packages`, and why you cannot set it
6174

6275
When a trigger fires, the job loads the third-party **pi packages the operator staged** into their global

admin/src/index.ts

Lines changed: 48 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,11 @@
3333
* ships an `operate-pi-dispatch` skill, advertised via `resources_discover`, that tells
3434
* the model how to use those human-in-the-loop write gates.
3535
*
36+
* `/dispatch setup` (issue #92) runs the guided deployment wizard (setup-wizard.ts); a bare
37+
* `/dispatch` on a host with no deployment at all offers it, and a one-time session_start
38+
* nudge names it on a fresh host. The wizard sequences the worker CLI's own consented
39+
* commands and writes the deployment pointer this factory applies above.
40+
*
3641
* Supported pi version: 0.80.7. The factory registers nothing unless every API
3742
* member it consumes is present; on a miss it names the member and the
3843
* supported version on stderr and returns.
@@ -76,6 +81,10 @@ import { buildSandboxRunArgs, launchSandbox as spawnSandbox, resolveSandbox, san
7681
import { readManifest } from "@edgehero/pi-dispatch/sandbox-store";
7782
import { renderStatus, renderRuns, renderBudget, renderTriggers, renderSettingsView, renderCosts, renderWhatIf } from "./render.mjs";
7883
import { makeDashboard, createDashboardDeps } from "./dashboard.ts";
84+
// Only the nudge is loaded eagerly (it must register its session_start handler at factory time); the
85+
// wizard itself stays behind the dispatch handler's lazy import. The setup-wizard module imports
86+
// buildTriggerEntry back from here -- a cycle on paper, but both sides only call across it at runtime.
87+
import { registerNudge } from "./setup-wizard.ts";
7988
import { matchesKey } from "./keys.mjs";
8089

8190
// The single source of truth for the ExtensionAPI surface this extension
@@ -90,7 +99,7 @@ const REBUILT_NOTICE = (reason: string) =>
9099
`replaced invalid settings file (${reason}) — other keys were lost`;
91100

92101
const USAGE =
93-
"usage: /dispatch <status|pause|resume|run|runs|logs|budget|costs|triggers|settings|set|unset>";
102+
"usage: /dispatch <status|pause|resume|run|runs|logs|budget|costs|triggers|settings|set|unset|setup>";
94103

95104
const KNOWN_SUBCOMMANDS = [
96105
"status",
@@ -105,6 +114,7 @@ const KNOWN_SUBCOMMANDS = [
105114
"settings",
106115
"set",
107116
"unset",
117+
"setup",
108118
] as const;
109119

110120
export default function admin(pi: ExtensionAPI): void {
@@ -135,13 +145,14 @@ export default function admin(pi: ExtensionAPI): void {
135145

136146
pi.registerCommand("dispatch", {
137147
description:
138-
"pi-dispatch admin: status|pause|resume|run|runs|logs|budget|costs|triggers|settings|set|unset",
148+
"pi-dispatch admin: status|pause|resume|run|runs|logs|budget|costs|triggers|settings|set|unset|setup",
139149
getArgumentCompletions: (prefix) => completeArguments(prefix),
140150
handler: async (args, ctx) => dispatch(pi, args, ctx),
141151
});
142152

143153
registerTools(pi);
144154
registerSkill(pi);
155+
registerNudge(pi);
145156
}
146157

147158
/**
@@ -655,7 +666,7 @@ const PR_ACTION_VOCAB: Record<string, { hint: string; dflt: string }> = {
655666
azure: { hint: "created updated", dflt: "updated" },
656667
};
657668

658-
function buildTriggerEntry(kind: string, f: any): any {
669+
export function buildTriggerEntry(kind: string, f: any): any {
659670
if (kind === "cron") {
660671
// Optional per-entry provider/model/maxTurns pass through to job.data (highest precedence); omitted when
661672
// blank so the value still resolves against the settings overlay/env at job start (triggers.mjs:127-131).
@@ -744,6 +755,32 @@ async function dispatch(pi: ExtensionAPI, args: string, ctx: any): Promise<void>
744755
if (pnote) notify?.(pnote, "warning");
745756

746757
if (sub === "") {
758+
// Detection decides what a bare /dispatch means (issue #92). Lazy import: the wizard module loads
759+
// only on the bare command and `setup`, never for the read subcommands or the LLM tools.
760+
const { detectDeployment, runSetupWizard } = await import("./setup-wizard.ts");
761+
const det = await detectDeployment({ env: process.env, cwd: ctx?.cwd });
762+
if (det.state === "none") {
763+
// Nothing anywhere: offer the wizard instead of opening a panel onto an empty deployment. The
764+
// confirm is the offer -- a decline (or a build without dialogs) degrades to the usage line.
765+
if (typeof ctx?.ui?.confirm === "function") {
766+
const ok = await ctx.ui.confirm(
767+
"pi-dispatch setup",
768+
"No deployment found (no pointer, no env, no config here, queue unreachable). Set one up now?",
769+
);
770+
if (ok) {
771+
await runSetupWizard(paths, ctx, notify, { openDashboardFn: openDashboard });
772+
return;
773+
}
774+
}
775+
notify?.(`${USAGE} — run /dispatch setup when you are ready`, "info");
776+
return;
777+
}
778+
if (det.state === "cwd" || det.state === "reachable") {
779+
// A deployment that works only from this directory (cwd scaffold) or was merely probed
780+
// (reachable queue, no config wired): open the panel as always, plus ONE hint that a pointer
781+
// would make it work from anywhere. "pointer"/"env" open with no hint -- exactly as today.
782+
notify?.(`using ${det.detail} — /dispatch setup can write a deployment pointer so this works from anywhere`, "info");
783+
}
747784
await openDashboard(paths, ctx, notify);
748785
return;
749786
}
@@ -832,6 +869,14 @@ async function dispatch(pi: ExtensionAPI, args: string, ctx: any): Promise<void>
832869
applyUnset(paths.settingsFile, tokens, notify);
833870
return;
834871
}
872+
case "setup": {
873+
// Same lazy import as the bare branch; openDashboard rides in as a dep so the wizard's final
874+
// step (and its "Open the panel anyway" escape) reuse this module's opener without a cycle at
875+
// evaluation time.
876+
const { runSetupWizard } = await import("./setup-wizard.ts");
877+
await runSetupWizard(paths, ctx, notify, { openDashboardFn: openDashboard });
878+
return;
879+
}
835880
default:
836881
notify?.(`dispatch: unknown subcommand '${sub}'. ${USAGE}`, "warning");
837882
return;

0 commit comments

Comments
 (0)