# 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

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

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.

## Parts

| 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

### 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

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

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

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

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

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

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

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

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

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

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.

## 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

| 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

### First release

- [ ] `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.

### Second release

- [ ] `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.