Launchpad Links Index
Launchpad Links Index (#1243)
Section titled “Launchpad Links Index (#1243)”The machine-readable per-project link map behind the Launchpad. Schema of the generated index, the derivation rules (declared sources only — nothing guessed), and how to run the generator.
Generator
Section titled “Generator”node ~/.claude/scripts/launchpad/links-index.js # pretty JSON to stdoutnode ~/.claude/scripts/launchpad/links-index.js --out FILE # write to FILE insteadnode ~/.claude/scripts/launchpad/links-index.js --compact # single-line JSONSource of truth: BOB_SOURCE/scripts/launchpad/links-index.js (deployed to
~/.claude/scripts/ by deploy.sh).
Like the fleet readiness probe (scripts/fleet/readiness.js, #446), the
generator is static and side-effect-free — it reads declared files only
and never executes dev-up, a server, or a deploy. It is also
deterministic: no timestamps, projects sorted by name — re-running with
unchanged inputs produces byte-identical output, so the emitted index can be
diffed and cached safely.
Index schema (bob-launchpad-links/1)
Section titled “Index schema (bob-launchpad-links/1)”{ "schema": "bob-launchpad-links/1", "projects": [ { "name": "tct", // ledger key (provisions/ports.json) "slot": 76, // ledger slot (absent for pinned entries) "base": 5960, // band base port "pinned": true, // only when the ledger entry is pinned "ports": { // band sub-offsets (port-allocation.md) "app": 5960, // base+0 Vite/app dev "wrangler": 5961, // base+1 Wrangler dev "dashboard": 5962, // base+2 cds dashboard "docs": 5963 // base+3 docs-site dev }, "path": "$HOME/Sites/tct", // declared path (ledger entry, else projects.json), unexpanded "dev_json": "malformed", // only when dev.json exists but fails to parse "stack": ["cloudflare-workers", "node", "vitest", "typescript"], "deploy_path": "cf", // cf | external | none (readiness.js) "github": { "repo": "https://github.com/paulirv/tct", "issues": "https://github.com/paulirv/tct/issues", "actions": "https://github.com/paulirv/tct/actions", // only when CI workflows are declared (#1392) "project": "https://github.com/users/paulirv/projects/7" // only with a cdproj gh_project linkage (#1392) }, "urls": { "dev": "http://localhost:5960", "docs": "http://localhost:5963", // dev.json docs block, else docs-site.json fallback (#1407) "dashboard": "http://localhost:5962", // cds dashboard — ledger band base+2 (#1392) "prod": "https://currentthing.app" // only for cf projects with a declared route }, "startable": ["dev"] // surfaces with a declared start path (#1391): present iff dev.json exists } ]}Fields that cannot be derived are omitted, never null-filled or guessed. A project with nothing but a ledger entry still appears — with its band and a ledger-derived dev URL.
Derivation rules
Section titled “Derivation rules”Every value comes from a declared source; there are no hand-maintained entries anywhere in the pipeline.
| Field | Declared source | Rule |
|---|---|---|
| coverage | BOB_SOURCE/provisions/ports.json |
one entry per ledger project, sorted by name |
ports |
ledger band | base + sub-offsets per the port allocation convention |
path |
ledger entry path, else ~/.claude/projects.json |
the ledger entry’s own declared path wins (#1406 — recorded $HOME-contracted at band allocation by port-project-sync, so every cdi run records or heals it); the legacy registry is the fallback: exact name match, then path-basename match; inactive entries skipped |
urls.dev |
project dev.json |
one resolved port feeds the link, the port badge, and the liveness probe (#1499): server.port, else the ledger app port, is authoritative; access.localhost supplies the URL form (scheme/path) only while its port agrees. A contradicting (stale) access.localhost is overridden and surfaced as access_drift on the entry — never silently trusted. With no port declared anywhere, access.localhost stands alone |
urls.docs |
project dev.json docs block, else docs-site.json |
an explicit dev.json docs block always wins — including enabled: false, which suppresses the pill outright (the opt-out beats any fallback); docs.port → ledger docs port (base+3). With no docs block, a present, non-disabled docs-site.json (the docs-site skill’s declared config, #1407) declares the docs site; it has no port field, so the ledger docs port (base+3) applies — the same band-only derivation as the dashboard pill. Malformed docs-site.json = absent; neither source declared → omitted, never guessed |
urls.dashboard |
ledger band | the cds dashboard sub-offset (base+2, #1392) — present for every banded project (cds auto-assigns that port); omitted when the ledger entry has no base |
urls.prod |
wrangler.toml / .jsonc / .json |
only where deploy path is cf: first declared route/custom-domain host, production env preferred, monorepo apps/<name>/ configs scanned; no declared route → omitted (a workers.dev URL would need the undeclared account subdomain) |
github |
.git/config [remote "origin"] |
parsed statically (no git exec; worktree gitdir: pointers followed); non-GitHub remotes omitted |
github.actions |
.github/workflows/ |
the repo /actions URL, only when at least one .yml/.yaml workflow is declared (#1392) — the same CI detection readiness.js uses |
github.project |
provisions/<name>.json gh_project block |
the Projects v2 board URL (https://github.com/users/<owner>/projects/<number>), only when the cdproj linkage is enabled and initialised (number written back by cdproj init); owner from gh_project.owner, else the repo owner; anything less → omitted (#1392) |
stack, deploy_path |
project files | shared fleet readiness detection (scripts/fleet/readiness.js, #446) |
startable |
project dev.json |
["dev"] when a dev.json exists at the registered path — the declared start path the start action requires; omitted otherwise (no affordance, nothing guessed) |
Path ownership (#1406)
Section titled “Path ownership (#1406)”The port ledger owns name→path. port-project-sync records each
project’s checkout path onto its ledger entry at band-allocation time (every
cdi run), and converges an absent or stale path — but never overwrites a
declared path whose directory still exists, so a sync run from a secondary
checkout (e.g. a cdfork worktree) cannot steal the canonical location. This
closes the drift mode that left 17 ledger projects pathless: the
projects.json registry used to be the only path source and was never
updated for the ~/projects/* generation. The registry remains a read-only
legacy fallback; new drift cannot recur because paths now travel with the
allocation itself.
.claude / BoB aliasing (intended). The ledger’s .claude entry
declares $HOME/.claude — the deployed BOB_HOME runtime dir, whose git
origin is the bigbrain repo. It therefore shows the same repo/issues links
as the bigbrain entry (the source checkout at $HOME/projects/bigbrain).
Both are real, distinct surfaces: one is the deploy target, one the source
of truth.
Graceful degradation
Section titled “Graceful degradation”A missing, partial, or malformed dev.json never drops a project from the
index: the entry keeps whatever is derivable (band ports, ledger-fallback dev
URL, GitHub URLs). A malformed dev.json is additionally flagged with
"dev_json": "malformed" so the Launchpad can surface it.
Liveness probe (bob-launchpad-liveness/1, #1246)
Section titled “Liveness probe (bob-launchpad-liveness/1, #1246)”The one deliberate exception to “static and side-effect-free”: the liveness
probe (scripts/launchpad/liveness.js) answers “is this dev server actually
up right now?” for the page’s badges. Its semantics:
- Verdicts.
up= a TCP connect to the port succeeds (something is listening — thedev-healthconvention, where any live listener counts);down= the connect is refused or times out. Unknown is the absence of a verdict: a project with no probeable URL gets no key, and a page whose/liveness.jsonfetch fails (static file, older server) keeps its hollow badges. Nothing is ever guessed. - Local only. Only
localhost/127.0.0.1URLs from the index are probed — thedev,docs, anddashboardURLs. Remote hosts (urls.prod, the GitHub links) are never touched: the badge reports local dev liveness, not production health. - Cheap and bounded. All connects run in parallel under a short per-probe
timeout (default 400 ms,
--timeout MS), so a full-fleet sweep completes in roughly one timeout regardless of project count and the page never hangs. - Probe-at-render, never a poller. The module holds no timers, cache, or
state; a probe runs only when asked — one
GET /liveness.jsonper page load/refresh (the serve.js route re-probes per request), or the CLI. The badge is painted by a one-shot post-load fetch, so first paint (the instant-load metric) is never delayed, and a verdict only ever colors the dot — links render identically for up, down, and unknown.
// GET http://localhost:7777/liveness.json · node ~/.claude/scripts/launchpad/liveness.js{ "schema": "bob-launchpad-liveness/1", "timeout_ms": 400, "projects": { "tct": { "dev": "up", "docs": "down" }, // key absent = unknown (nothing declared) "bob-web": {} }}Start action (#1391)
Section titled “Start action (#1391)”Where the liveness probe is the read-only exception, the start action is the Launchpad’s one deliberate write exception: clicking the down dot on a startable dev pill may start that project’s dev server. Everything else the Launchpad does remains a derivation.
- Endpoint.
POST /action/startwith{"project": "<name>", "surface": "dev"}. Action routes are POST-only — a GET gets405+Allow: POST, so no link, prefetch, or crawler can ever trigger a side effect. The server binds127.0.0.1, so the action is local-only by construction. - Declared start path only. A project is startable iff its index entry
carries
startable: ["dev"]— i.e. adev.jsonexists at its registered path. The page shows no start affordance otherwise, and the endpoint refuses (422) with a concrete reason (unknown project, unregistered path, nodev.json, undeclared surface). The start always runs through the samedev-upentrypoint the CLI uses — the Launchpad never invents a start sequence. - One in flight per project. A second start request while one runs answers
409(in_flight: true) and never spawns a seconddev-up; the project becomes startable again when the run settles. - Bounded, non-blocking. The child is spawned asynchronously (the server
keeps answering page/liveness routes) and killed at the timeout (default
180 s); results report
timed_outwhen that happens. - Results.
200on success;502on failure with the short combined output tail (last 2 000 chars) so the page can show why — a failed start is never a silently-red dot. - Page flow. down dot → click-to-start control (
.dot.start) → pulsing in-progress state (.dot.starting) → one liveness re-probe paints the final verdict; failures render an inline message with the output tail.
Consumers
Section titled “Consumers”The index feeds the Launchpad page (#1244) and its liveness badges (#1246) — the page renders links; the generator owns all derivation. Regenerate whenever declared state changes (a project gains a port band, a dev.json, or a route); re-running is always safe.
make test-launchpad-links — dependency-free unit suite
(tests/test-launchpad-links.js) covering URL-derivation precedence,
route/remote parsing, degradation, and byte-identical re-runs.
make test-launchpad-liveness covers the probe (local-only guard, up/down
verdicts against real listeners, per-request freshness of /liveness.json);
make test-launchpad-page pins the badge-informs-never-hides page contract
and the startable-only affordance; make test-launchpad-start covers the
start action (start invoked via the declared path, GET rejection, refusal
reasons, in-flight dedupe, failure surfacing, bounded execution).