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.
Core model
Section titled “Core model”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?
When to use it
Section titled “When to use it”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.
| Part | Purpose |
|---|---|
| The manifest | One machine-readable file that declares every repository |
| The schema | A JSON Schema that constrains the manifest |
| The routing file | The first document an agent or a developer reads |
| The topology document | The data flow between repositories, and symptom routing |
| The glossary | A map from a user’s word to the code that owns it |
| The boundaries document | What each repository accepts, and what it refuses |
| The clone script | One command that creates or updates every checkout |
| The run script | One command that starts the local stack |
| The instance tooling | Isolated workspaces for concurrent development |
| The check orchestrator | One command that invokes each repository’s own checks |
| The foundation check | A 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/.
Build order
Section titled “Build order”1. Create the repository and the manifest
Section titled “1. Create the repository and the manifest”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.
2. Constrain the manifest with a schema
Section titled “2. Constrain the manifest with a schema”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.
3. Write the routing file
Section titled “3. Write the routing file”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.
4. Write the clone script
Section titled “4. Write the clone script”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.
5. Write the run script
Section titled “5. Write the run script”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.
6. Add isolated instances
Section titled “6. Add isolated instances”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.
| Component | Control | Instance A | Instance B |
|---|---|---|---|
| Database | 1433 | 1533 | 1633 |
| API | 5082 | 5182 | 5282 |
| Web | 5173 | 5273 | 5373 |
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.
7. Start every child detached
Section titled “7. Start every child detached”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.
8. Delegate the checks
Section titled “8. Delegate the checks”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.
9. Make cleanup refuse to guess
Section titled “9. Make cleanup refuse to guess”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.
10. Add the coordination skill
Section titled “10. Add the coordination skill”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.
Minimum viable version
Section titled “Minimum viable version”Ship these five items first. Add the rest when a real problem demands them.
launchpad.jsonand its schema.AGENTS.md, with the routing table and the hard boundaries.scripts/clone.sh.run.sh, with--checkand--start.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.
Design rules
Section titled “Design rules”- 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-patterns
Section titled “Anti-patterns”| Anti-pattern | Failure mode | Accepted pattern |
|---|---|---|
| A Launchpad that contains product source | It becomes a monorepo with extra steps | Keep product checkouts in the ignored repos/ and upstream/ folders |
| Submodules instead of worktrees | A submodule pins a commit in tracked history and fights concurrent work | Use linked Git worktrees |
A --force flag on start or stop | It converts a safe failure into a corrupted state | Fail safely on an occupied port |
| A Launchpad that duplicates a child’s build instructions | The copy goes stale, and the developer trusts the wrong one | Invoke the child’s own check entrypoint |
| Broad process termination during cleanup | It kills a colleague’s work | Match a recorded process identity before each signal |
Verification
Section titled “Verification”First release
Section titled “First release”-
scripts/check-foundation.shpasses 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 statusin the Launchpad shows no file fromrepos/orupstream/. -
run.sh --checkreports prerequisites and ports, and starts nothing. -
run.sh --startreturns only after every listener answers. -
run.sh --startfails safely when a port is occupied.
Second release
Section titled “Second release”-
run.sh --stopstops 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.