ship-loop-master — autonomous shipping orchestrator
Activation
CEO invokes /loop ship-loop-master (default) or /loop ship-loop-master --enable-auto-merge (auto-merge mode).
The literal phrase --enable-auto-merge in the activation prompt is the only way to set auto_merge: true. Without it the loop forces auto_merge: false regardless of any state file content. This is the audit trail.
Pre-flight (runs once at first tick)
- Verify
git status is on a feature branch (refuse to run on main)
- Verify
gh auth status shows authenticated session
- Verify a Linear ticket reference exists in env
SHIP_LOOP_TICKET=SYN-XXX OR in .claude/scratchpad/ship-loop-ticket.txt (refuse to start without one — CLAUDE.md hard rule "all work traces to Linear")
- Initialise
.claude/scratchpad/ship-loop-state.json if absent:{
"started_at": "<iso>",
"deadline_at": "<iso + 36h>",
"ticket": "SYN-XXX",
"branch": "<current branch>",
"auto_merge": false,
"completion": "in_progress",
"layers": {
"build": { "state": "unknown", "last_run": null, "retries": 0 },
"test": { "state": "unknown", "last_run": null, "retries": 0 },
"sanity": { "state": "unknown", "last_run": null, "retries": 0 },
"watch": { "state": "unknown", "last_run": null, "retries": 0 },
"smoke": { "state": "unknown", "last_run": null, "retries": 0 },
"pr": { "state": "not_opened", "url": null, "ci_state": null },
"merge": { "state": "not_attempted" }
}
}
- Initialise
ship-loop-events.jsonl (touch) and ship-loop-escalations.md (touch with header)
Tick loop (every dynamic-paced wakeup)
Read ship-loop-state.json. Then:
Hard stops (highest priority, check first)
- Deadline reached:
now() >= deadline_at → set completion = 'timeout', write summary to escalations.md, exit
- Completion state:
completion === 'merged' → exit cleanly with summary
- Unresolved P0 escalation: parse
escalations.md for ^## P0 headings; if any remain unchecked, refuse to invoke any child loop, write current status, sleep 1800s and re-check
Eligibility decisions (in priority order)
The master picks ONE child to invoke per tick (not parallel — sequential discipline keeps debugging tractable):
| If state |
Then invoke |
layers.build.state !== 'green' OR last_run > 30 min ago |
ship-loop-build |
layers.build.state === 'green' AND (layers.test.state !== 'green' OR last_run > 30 min ago) |
ship-loop-test |
build+test green AND layers.sanity.last_run > 60 min ago OR sanity.state !== 'green' |
ship-loop-sanity |
any layer green AND layers.watch.last_run > 15 min ago |
ship-loop-watch |
build+test+sanity green AND layers.smoke.last_run > 60 min ago OR smoke.state !== 'green' |
ship-loop-smoke |
all 5 inner layers green AND git status --short shows staged/unstaged changes AND pr.state === 'not_opened' |
ship-loop-pr |
pr.state === 'opened' AND pr.ci_state === 'green' AND merge.state === 'not_attempted' |
ship-loop-merge |
| nothing eligible |
sleep (longer cache-miss interval) |
After invoking the child
- Read child's exit state from
ship-loop-state.json (children update their layer's section atomically)
- Append a tick entry to
ship-loop-events.jsonl:{"ts":"<iso>","tick":N,"invoked":"ship-loop-build","result":"green","duration_ms":12345}
- Decide next sleep duration via
ScheduleWakeup:
- Active work (something just changed state, more eligible loops queued): 60–270s (in-cache)
- Idle (everything green, just monitoring): 1200–1800s (cache miss but acceptable)
- Approaching deadline: shorter intervals to maximise final-hour work
Hard rules compliance
- Linear ticket required — refuses to start without
SHIP_LOOP_TICKET
- Branch protection — refuses to run on
main
- Auto-merge gating — defaults
false; only flips on if literal --enable-auto-merge was in activation prompt; recorded in state for audit
- No env modifications — never reads/writes
.env*
- Max 1 retry per child loop per tick — child loops handle their own recovery; master does not retry beyond what child reports
- Phase 8 stays human-gated — even with auto-merge enabled, master never invokes Vercel CLI or pushes to
main directly
Exit conditions
| Condition |
Action |
completion === 'merged' |
Write summary to escalations.md (positive), exit clean |
| Deadline reached |
Mark completion = 'timeout', summary, exit |
| P0 escalation written by any child |
Halt; require human to clear escalation before resume |
Activation prompt missing --enable-auto-merge AND merge layer reaches ready_for_human |
Halt with "ready for human review" message; do not exit (loop sleeps so CEO can resume by clearing the escalation) |
Files this skill writes
.claude/scratchpad/ship-loop-state.json (read + write atomically)
.claude/scratchpad/ship-loop-events.jsonl (append-only)
.claude/scratchpad/ship-loop-escalations.md (append-only)
Verification
/loop ship-loop-master on a branch with one trivially failing test should:
- Tick 1: invoke
ship-loop-build (state was unknown)
- Tick 2: invoke
ship-loop-test (build went green)
- Tick 3: child reports
red with one fail; master logs and re-queues
- Eventually:
escalations.md has the test failure escalated after retry exhaustion
Out of scope
- Production deploy invocation (Vercel auto-deploys on merge to main; loop does not call Vercel CLI)
- Cross-repo coordination
- Slack/email notifications (escalations.md is the surface)
1---2name: ship-loop-master3description: 24/7 ~1.5-day autonomous shipping orchestrator for Synthex. Drives 7 child loops (build, test, sanity, watch, smoke, pr, merge) on differentiated cadences. Reads state from .claude/scratchpad/ship-loop-state.json on every tick, dispatches the next eligible child loop, and exits cleanly after 36 hours OR when state.completion === 'merged'. Use when the CEO says "/loop ship-loop-master" to start an autonomous shipping run.4---56# ship-loop-master — autonomous shipping orchestrator78## Activation910CEO invokes `/loop ship-loop-master` (default) or `/loop ship-loop-master --enable-auto-merge` (auto-merge mode).1112The literal phrase `--enable-auto-merge` in the activation prompt is the **only** way to set `auto_merge: true`. Without it the loop forces `auto_merge: false` regardless of any state file content. This is the audit trail.1314## Pre-flight (runs once at first tick)15161. Verify `git status` is on a feature branch (refuse to run on `main`)172. Verify `gh auth status` shows authenticated session183. Verify a Linear ticket reference exists in env `SHIP_LOOP_TICKET=SYN-XXX` OR in `.claude/scratchpad/ship-loop-ticket.txt` (refuse to start without one — CLAUDE.md hard rule "all work traces to Linear")194. Initialise `.claude/scratchpad/ship-loop-state.json` if absent:20 ```json21 {22 "started_at": "<iso>",23 "deadline_at": "<iso + 36h>",24 "ticket": "SYN-XXX",25 "branch": "<current branch>",26 "auto_merge": false,27 "completion": "in_progress",28 "layers": {29 "build": { "state": "unknown", "last_run": null, "retries": 0 },30 "test": { "state": "unknown", "last_run": null, "retries": 0 },31 "sanity": { "state": "unknown", "last_run": null, "retries": 0 },32 "watch": { "state": "unknown", "last_run": null, "retries": 0 },33 "smoke": { "state": "unknown", "last_run": null, "retries": 0 },34 "pr": { "state": "not_opened", "url": null, "ci_state": null },35 "merge": { "state": "not_attempted" }36 }37 }38 ```395. Initialise `ship-loop-events.jsonl` (touch) and `ship-loop-escalations.md` (touch with header)4041## Tick loop (every dynamic-paced wakeup)4243Read `ship-loop-state.json`. Then:4445### Hard stops (highest priority, check first)4647- **Deadline reached**: `now() >= deadline_at` → set `completion = 'timeout'`, write summary to `escalations.md`, exit48- **Completion state**: `completion === 'merged'` → exit cleanly with summary49- **Unresolved P0 escalation**: parse `escalations.md` for `^## P0` headings; if any remain unchecked, refuse to invoke any child loop, write current status, sleep 1800s and re-check5051### Eligibility decisions (in priority order)5253The master picks ONE child to invoke per tick (not parallel — sequential discipline keeps debugging tractable):5455| If state | Then invoke |56| --------------------------------------------------------------------------------------------------------------- | ---------------------------------- |57| `layers.build.state !== 'green' OR last_run > 30 min ago` | `ship-loop-build` |58| `layers.build.state === 'green' AND (layers.test.state !== 'green' OR last_run > 30 min ago)` | `ship-loop-test` |59| build+test green AND `layers.sanity.last_run > 60 min ago OR sanity.state !== 'green'` | `ship-loop-sanity` |60| any layer green AND `layers.watch.last_run > 15 min ago` | `ship-loop-watch` |61| build+test+sanity green AND `layers.smoke.last_run > 60 min ago OR smoke.state !== 'green'` | `ship-loop-smoke` |62| all 5 inner layers green AND `git status --short` shows staged/unstaged changes AND `pr.state === 'not_opened'` | `ship-loop-pr` |63| `pr.state === 'opened' AND pr.ci_state === 'green' AND merge.state === 'not_attempted'` | `ship-loop-merge` |64| nothing eligible | sleep (longer cache-miss interval) |6566### After invoking the child6768- Read child's exit state from `ship-loop-state.json` (children update their layer's section atomically)69- Append a tick entry to `ship-loop-events.jsonl`:70 ```json71 {"ts":"<iso>","tick":N,"invoked":"ship-loop-build","result":"green","duration_ms":12345}72 ```73- Decide next sleep duration via `ScheduleWakeup`:74 - Active work (something just changed state, more eligible loops queued): 60–270s (in-cache)75 - Idle (everything green, just monitoring): 1200–1800s (cache miss but acceptable)76 - Approaching deadline: shorter intervals to maximise final-hour work7778## Hard rules compliance7980- **Linear ticket required** — refuses to start without `SHIP_LOOP_TICKET`81- **Branch protection** — refuses to run on `main`82- **Auto-merge gating** — defaults `false`; only flips on if literal `--enable-auto-merge` was in activation prompt; recorded in state for audit83- **No env modifications** — never reads/writes `.env*`84- **Max 1 retry per child loop per tick** — child loops handle their own recovery; master does not retry beyond what child reports85- **Phase 8 stays human-gated** — even with auto-merge enabled, master never invokes Vercel CLI or pushes to `main` directly8687## Exit conditions8889| Condition | Action |90| ----------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------ |91| `completion === 'merged'` | Write summary to escalations.md (positive), exit clean |92| Deadline reached | Mark `completion = 'timeout'`, summary, exit |93| P0 escalation written by any child | Halt; require human to clear escalation before resume |94| Activation prompt missing `--enable-auto-merge` AND merge layer reaches `ready_for_human` | Halt with "ready for human review" message; do not exit (loop sleeps so CEO can resume by clearing the escalation) |9596## Files this skill writes9798- `.claude/scratchpad/ship-loop-state.json` (read + write atomically)99- `.claude/scratchpad/ship-loop-events.jsonl` (append-only)100- `.claude/scratchpad/ship-loop-escalations.md` (append-only)101102## Verification103104`/loop ship-loop-master` on a branch with one trivially failing test should:1051061. Tick 1: invoke `ship-loop-build` (state was `unknown`)1072. Tick 2: invoke `ship-loop-test` (build went `green`)1083. Tick 3: child reports `red` with one fail; master logs and re-queues1094. Eventually: `escalations.md` has the test failure escalated after retry exhaustion110111## Out of scope112113- Production deploy invocation (Vercel auto-deploys on merge to main; loop does not call Vercel CLI)114- Cross-repo coordination115- Slack/email notifications (escalations.md is the surface)