Skip to content

Launchpads

Build a Launchpad when one product spans several repositories and no single repository can tell a newcomer where to start. A Launchpad is a small, tracked repository that holds the map, the vocabulary, the boundaries, and the tooling for a group of related repositories. It holds no product source.

This playbook describes the pattern and the build order. ETK Launchpad is the reference implementation.

A Launchpad answers five questions for one product family:

  • Which repositories build this product, and what does each one own?
  • Which repositories accept a change, and which are read-only?
  • How does a developer get a working local stack?
  • How does a developer prove that a change works?
  • How do several developers work on the same product at the same time?

Build a Launchpad when three or more of these conditions are true:

  • Two or more repositories must run together to exercise one feature.
  • The repositories live in different organizations, hosts, or teams.
  • New developers need more than a day to get a local stack.
  • A symptom in one repository often has its root cause in another.
  • Another team owns some repositories, and those must never receive a write.

Don’t build one for a single repository, or for a monorepo. Improve that repository’s own guidance instead.

PartPurpose
The manifestOne machine-readable file that declares every repository
The schemaA JSON Schema that constrains the manifest
The routing fileThe first document an agent or a developer reads
The topology documentThe data flow between repositories, and symptom routing
The glossaryA map from a user’s word to the code that owns it
The boundaries documentWhat each repository accepts, and what it refuses
The clone scriptOne command that creates or updates every checkout
The run scriptOne command that starts the local stack
The instance toolingIsolated workspaces for concurrent development
The check orchestratorOne command that invokes each repository’s own checks
The foundation checkA check of the Launchpad itself, with no product build

Add the optional parts only when the product needs them:

  • A coordination skill, for an agent that reports fleet status.
  • A delivery document, for branch, pull request, and merge rules.
  • A verification document, for the evidence that a change works.
  • An automation layer, for a work-item queue that drives the instance lifecycle. ETK Launchpad keeps this configuration under .ostinato/.

Create an empty repository named PRODUCT-launchpad. Add launchpad.json with one entry per repository. Each entry declares the name, the class, the path, the remote, the host, the default branch, and the role.

Replace the following:

  • PRODUCT: the short name of the product family.

Classify every repository as exactly one of these:

  • writable: your team changes it.
  • upstream: another team owns it. It is read-only evidence.

Declare the checkout paths under two folders: repos/ for writable, and upstream/ for read-only. Add both to .gitignore. Product source must never land in Launchpad history.

Declare a capability map next to the repository list. Each capability names an executable entrypoint. Start with four:

  • inventory: print the manifest.
  • context: print the current checkout context.
  • status: print the fleet status envelope.
  • check: validate the Launchpad itself.

Add schemas/launchpad.schema.json. Then add a foundation check that validates the manifest against it, verifies that every declared entrypoint is a checked-in executable, and parses every shell script. The foundation check must pass with no product checkout present. This keeps the Launchpad testable in continuous integration on a clean runner.

Write AGENTS.md as a task-to-document table. One row per task, one owning document per row. Symlink CLAUDE.md to it so that every agent reads the same file.

State the hard boundaries in this file:

  • Never write to an upstream repository.
  • Never run a forced clean, because it destroys the ignored checkouts.
  • Always name a full path, because several checkouts of one repository exist.

Keep the routing file short. It routes. It doesn’t explain. Each owning document holds the detail.

Write one script that reads the manifest and clones every repository to its declared path. Make it idempotent: a second run updates an existing checkout instead of failing. Accept a repository name to clone one, and --all to clone every one.

Write one script that starts the whole local stack and returns only after every listener answers. Give it four modes:

  • --check: report prerequisites and ports, and start nothing.
  • --start: start the stack and wait for verified readiness.
  • --status: report the recorded processes, ports, and logs.
  • --stop: stop only the processes that this stack recorded.

Write a runtime receipt to an ignored state folder. Record the process identities, the URLs, the timestamps, and the log paths. The receipt makes --status and --stop exact instead of approximate. Never add a --force flag. An occupied port must fail safely.

Add instance tooling when two developers, or one developer and one agent, must run the product at the same time. The pattern uses linked Git worktrees, not extra clones. One outer worktree holds the Launchpad. Each participating repository gets one linked child worktree beneath it.

Not every writable repository belongs in an instance. A shared library that no local service loads adds cost and no isolation. Mark it in the manifest, and keep it out of the snapshot and the child worktrees. ETK Launchpad uses instanceRequired: false for that case, so one parent carries three children out of four writable repositories.

Allocate a port offset block per instance. Reserve a round number, for example 100 ports. Then derive every port from the base port plus the offset.

ComponentControlInstance AInstance B
Database143315331633
API508251825282
Web517352735373

Decide which dependencies are instance-local and which are shared. State that decision in the documentation. Data stores are usually instance-local. Local emulators for cloud storage and mail are usually shared.

Write a generated identity record to the ignored state folder. It binds the instance name, the port offset, the exact commit of each repository, and the provenance of the composition. Name the bootstrap script as its only writer. Never hand-edit it.

Serialize the allocation with a lock that is keyed by the shared Git common directory. Two agents can request an instance at the same moment.

Check out every child worktree at a detached commit. Then require one explicit command to promote exactly one child to a named branch. This makes the editable surface of a task visible and small. A second editable repository needs a second task.

Each writable repository owns its own check entrypoint, for example ./check.sh. The Launchpad only orchestrates them. Bind each result to the exact commit and the worktree state, then write a receipt. Missing, failing, stale, or dirty evidence must block delivery.

The Launchpad isn’t the merge authority. Server pipelines remain the authority. The local checks exist to fail fast.

Write teardown as a preflight followed by a delete. The preflight inspects every worktree, link, and generated output before it deletes anything. Dirty source blocks removal. Unknown tracked, untracked, or ignored files block removal. Report the blocker and stop.

Terminate only the processes that this stack started and still owns. Match a recorded process identity before each signal. Never select a target by executable name, by port, or by path alone.

Add a repository-local agent skill that reports fleet status on demand. It correlates the task roster, the worktrees, the identity records, the runtime receipts, and the check receipts. It coordinates. It never implements a product change. Keep one persistent instance of it in the canonical checkout.

Derive status. Don’t store it. A second task database becomes wrong within a day.

Ship these five items first. Add the rest when a real problem demands them.

  1. launchpad.json and its schema.
  2. AGENTS.md, with the routing table and the hard boundaries.
  3. scripts/clone.sh.
  4. run.sh, with --check and --start.
  5. scripts/check-foundation.sh, and a pipeline that runs it.

Concurrent instances, receipts, and the coordination skill are the second release. A single developer with a working stack needs none of them.

  • One document owns each subject. Correct the smallest owning document. Duplicated prose drifts.
  • Code and pipelines beat prose. Encode a rule in a script when you can. Write prose only for what a script can’t check.
  • Nearest instructions win. The child repository owns its own mechanics. The Launchpad owns only the cross-repository floor and its own lifecycle.
  • Write files timelessly. Describe how a thing works, not how it changed.
  • Machine-local files stay ignored. Credentials, personal paths, toolchain overlays, and port files must never enter Git history.
  • Every observation reports its own completeness. A status envelope must declare what it couldn’t observe. Missing permission isn’t proof of absence.
Anti-patternFailure modeAccepted pattern
A Launchpad that contains product sourceIt becomes a monorepo with extra stepsKeep product checkouts in the ignored repos/ and upstream/ folders
Submodules instead of worktreesA submodule pins a commit in tracked history and fights concurrent workUse linked Git worktrees
A --force flag on start or stopIt converts a safe failure into a corrupted stateFail safely on an occupied port
A Launchpad that duplicates a child’s build instructionsThe copy goes stale, and the developer trusts the wrong oneInvoke the child’s own check entrypoint
Broad process termination during cleanupIt kills a colleague’s workMatch a recorded process identity before each signal
  • scripts/check-foundation.sh passes on a clean runner with no product checkout.
  • The manifest validates against schemas/launchpad.schema.json.
  • A second run of the clone script updates each checkout and doesn’t fail.
  • git status in the Launchpad shows no file from repos/ or upstream/.
  • run.sh --check reports prerequisites and ports, and starts nothing.
  • run.sh --start returns only after every listener answers.
  • run.sh --start fails safely when a port is occupied.
  • run.sh --stop stops only the processes in the runtime receipt.
  • Two instances run at the same time, each on its own port offset.
  • Missing, failing, stale, or dirty check evidence blocks delivery.
  • Teardown stops and reports the blocker when a worktree has dirty source.