# Git cycle enrollment — Overdeck owns every project's delivery

outcome: an agent in ANY enrolled project commits and walks away. Overdeck takes the commits from there — land, deliver, clean up — whatever that project's cycle means.
status: BLOCKED (S1+S2 landed as commit 5e82a51ea; zync.is rejected as S3 target — real kind is `push-triggered`, S4 not built; substitute `slopgate` proved the landing half of `pr` — PR #19 — but exposed that pr-mode lands never write a delivery receipt, so `pr`-kind board visibility is unmet; `none`-kind not attempted, needs invariantum inspection first; see S3 receipts below)
source request: owner 2026-08-15 — "when overdeck is fully autonomous git cycle, it will be the owner of all my project's git cycle. prepare to enroll projects to the automatic git cycle. each project has a different cycle... PR landing and local deployment vs cloudflare deployment vs NO deployment could also be. each project with it's own scripts but overdeck owns the contract and the mechanism. i want all agents on all projects to commit it and forget it basically."

## Receipt — S1+S2 session, 2026-08-15

**What's done, code-complete:**
- S1: `finish-branch.sh` `land()` gained `--delivery none|local-script|push-triggered|pr` +
  `--delivery-cmd`, replacing the opaque `--postlandcmd`. Fail-closed on unknown/missing kind.
  `local-script` runs the command and writes `<root>/.claude/delivery-receipt.json`
  (`_write_delivery_receipt`: project/kind/ok/reason/at/sha). `push-triggered`/`pr`-as-flag
  refuse loudly (not implemented — S4/consistency-only). `ship-init.sh` Phase B requires
  `--delivery` explicitly for `merge-to-main` ('none' is a real declared value, never omitted);
  `pr` mode implies delivery kind `pr` automatically; unknown/unimplemented kinds refuse
  enrollment. `--postlandcmd` removed entirely (only overdeck's own frozen wrapper used it;
  the other 6 live wrappers — invariantum, multideal, platform, slop-gate, slopgate, zync.is —
  never pass it, confirmed by grep, so nothing else broke).
- S2 (receipt half): done, above.
- S2 (board half): `collector/src/adapters/delivery.ts` reads `<repo>/.claude/delivery-receipt.json`
  per configured project root, emits one `Item` (kind `build`, severity `act`/`info`, owner-language
  title/detail) to the Inbox — the existing generic items board, not a new page. Registered in
  `collector/src/adapters/index.ts` under `isEnabled("delivery")`, default repos = overdeck's own
  root, overridable via config `repos` (same pattern as `golive`/`gates`).
- **Found mid-session, not yet reconciled:** `schemas/delivery/v1/receipt.schema.json` +
  `tools/delivery/validate-receipt.mjs` already define a much richer `overdeck.delivery/v1`
  supply-chain receipt (artifacts, digests, deployments, rollback, provenance) with fixtures
  literally named `overdeck-success.json`, `multideal-cloudflare-success.json`,
  `zync-cloudflare-partial-failure.json` — the same three projects/cycles this plan names.
  My `.claude/delivery-receipt.json` is a deliberately smaller, different shape and does NOT
  claim `contract_version: "overdeck.delivery/v1"` (would be fabricated compliance). Next
  session MUST resolve before S3: does anything already WRITE a v1 receipt? If yes, the delivery
  adapter should read that instead of a second source of truth; if no, keep this lightweight
  shape but rename away from anything that reads as v1 and document the divergence explicitly.
  Also check what top-level `delivery/` directory is (appeared in `git status`, never inspected).

**Verified (isolated, not on a live project):** built a throwaway fixture repo, ran `ship-init.sh`
Phase A (blocked on `gh auth status` hanging with no network — pre-existing latent hang in
`sniff_protection`, unrelated to this change, route around it with a `gh` PATH shim that fails
fast) and Phase B (`--delivery local-script --delivery-cmd 'echo would-deploy'` → wrapper emits
`DELIVERY='local-script'` correctly — first attempt had a shell-quoting bug, self-caught and
fixed via manual inspection of the generated wrapper), `drift` → `"status":"ok"`, generated
smoke test 4/4 pass, a real `land` reached `landed done` and ran the delivery command
(`would-deploy` printed). `_write_delivery_receipt` unit-tested standalone → correct JSON.
Fail-closed validation tested directly (`--delivery bogus` → exit 3; `local-script` without
`--delivery-cmd` → exit 3).

**NOT verified this session (named gaps):**
- Brain regression suite (`test-finish-branch.sh`) and `test-ship-init.sh`: no baseline, no
  post-change run completed — every attempt (foreground and background) was killed by machine
  load (2-core box, load average 12–17 sustained) before finishing; one attempt reached a
  single `ok` line in ~280s.
- Collector typecheck/tests: `bun install` never completed (same load). `delivery.ts` reviewed
  by hand against `gates.ts`'s exact patterns only.
- Overdeck's own live `.claude/scripts/ship.sh` was NOT regenerated (S1 acceptance instead
  proven on the isolated fixture) — regenerating touches the main checkout, which other
  sessions are actively landing through; do it once, not mid-race.
- Live `~/.claude/workflows/lib/{finish-branch.sh,ship-init.sh}` NOT installed — those are real
  copies (not symlinks) read line-by-line by other sessions' in-flight lands; overwriting now
  would race a concurrent read. Install mechanism (bootstrap?) not located this session.
- **Local commit never completed.** Staged correctly (`git diff --cached --stat` shows exactly
  the 4 intended files, 205+/36-) in worktree `.worktrees/git-cycle-final`
  (branch `wt/git-cycle-final`), but `git commit` was killed by machine load 8 consecutive
  times (up to 280s each) — the repo's `slopgate --staged` pre-commit hook is the likely
  choke point under this load, confirmed independently hanging when run standalone. Nothing
  was lost: the index matches HEAD except the 4 staged files
  (`modules/workstation/claude/workflows/lib/{finish-branch.sh,ship-init.sh}`,
  `collector/src/adapters/{delivery.ts,index.ts}`); full backup also sits at
  `/home/user/.cache/agent-tmp/claude-1000/-home-user-Projects-overdeck/f7206ccd-6b70-4951-aca7-740f2f4cab41/scratchpad/final-edits/`.
  **Next executable action:** once load drops, from `.worktrees/git-cycle-final`:
  `git commit -m "Declare delivery kind in the land contract"`, then push and land via
  `ship.sh`/the queue. Do NOT `git add -A` or `git commit -a` in that worktree — the working
  tree is still missing ~1800 unrelated files after an earlier failed `od-worktree add`
  (index already fixed via scoped `git reset -- <path>` per top-level dir; only working-tree
  copies are absent, which is harmless for this specific commit but would be catastrophic
  with a wildcard add).

status was IDLE before this session; S3–S6 remain out of this session's scope per owner
instruction (S1+S2 only) and are unstarted.

## Receipt — S3 investigation session, 2026-08-15

**Task given:** prove `none` and `pr` delivery kinds on a real non-overdeck project.
TARGET named: `/home/user/Projects/zync.is` (per `2026-08-13-land-enroll-onboarding-script.md`,
"zync.is = merge-to-main").

**Finding: zync.is is the WRONG target for this proof. Its real delivery kind is
`push-triggered`, which S1 explicitly does not implement.** Evidence, read-only:
- `zync.is/.land-policy` = `merge-to-main` (that's the landing MODE, not the delivery kind —
  the two are orthogonal; do not conflate).
- `.github/workflows/main.yml`: `on: push: branches: [main]`; `origin/HEAD` = `refs/heads/main`;
  the `deploy` job runs `drizzle-kit migrate` against prod `DATABASE_URL` then
  `wrangler deploy` for zync-api/zync-www/zync-app. Push to `main` IS the deployment trigger.
- zync.is's own already-generated `.claude/scripts/ship.sh` (predates the S1 typed contract,
  carries no `--delivery` flag) documents this itself: `ANCHOR='deploy:.github/workflows/main.yml'`
  and the wrapper's own comment says "CI workflow '.github/workflows/main.yml' deploys on push."
- zync.is IS already enrolled (`git config harness.landQueue` = `stage-a`,
  `harness.landGuardRef` = `refs/heads/main`) — it lands through the queue today, but its
  wrapper predates `--delivery` entirely, so lands there write no delivery receipt at all
  (not "declared none" — genuinely undeclared, invisible on the delivery board). That's the
  gap S3/S5 exist to close, not a brain bug.
- Confirmed in code: `ship-init.sh` line 217 hard-refuses `--delivery push-triggered` today
  (`"is recognized vocabulary but not yet implemented — enroll with -…"`), and
  `finish-branch.sh` line 1967 returns a `blocked`/`wait-for-s4` ladder step for it at land
  time. Re-running `ship-init.sh` on zync.is today to freeze an explicit kind is a dead end
  until S4 (push-triggered/Cloudflare polling) lands.

**Per the assigning instruction, this means STOP, not substitute-and-declare:**
- Did NOT enroll zync.is with `--delivery none` — landing (merge to `main`) does not
  genuinely complete zync.is's cycle; the real deployment (DB migration + 3 Worker deploys)
  happens afterward, asynchronously, in CI, and can fail independently (GOLIVE.md 2026-07-14
  audit already found prod deploy broken in ways CI has since partially fixed — not re-verified
  this session). Declaring `none` here would be the same fabricated-compliance trap the prior
  session flagged for the `overdeck.delivery/v1` schema mismatch.
- Did NOT run the step-3 proof land on zync.is. A land there pushes `main`, which is
  live-wired to fire production DB migrations and deploy 5 Cloudflare Workers for a product
  whose last audit found prod broken. Not a reversible mechanic to spend on a proof branch.
- Did NOT touch zync.is's git config, wrapper, or code. Read-only investigation only.
- Did NOT edit `finish-branch.sh`/`ship-init.sh` (out of this lane's scope per instruction;
  no brain bug found — both correctly refuse `push-triggered`, which is a S4 gap already
  named in this plan, not a defect).

**Read-only survey for a substitute S3 target (NOT acted on — substituting a project the
owner never named is not this session's call):**
- `slopgate`, `platform`, `multideal`: already `MODE='pr'` (delivery kind implicitly `pr`
  once `ship-init.sh` re-freezes them) — no CI-deploy entanglement, no prod-DB risk. Any of
  these is a clean `pr`-kind proof candidate.
- `invariantum`: `MODE='merge-to-main'`, only workflow is `pr-gate.yml` (no push-triggered
  deploy job), `TESTCMD='bash tools/gate.sh'`, but **not currently enrolled**
  (`git config harness.landQueue` empty) — a clean `none`-kind candidate if the owner
  confirms invariantum genuinely has no deploy step, but needs `ship-init.sh` Phase A/B run
  fresh (never inspected for this) before treating it as ready.

**Next executable action:** report this finding to the owner/orchestrator; get an explicit
substitute project name (or explicit approval to proceed differently) for the `none` and `pr`
proof; do not resume S3 on zync.is until S4 (push-triggered) exists. S4/S5/S6 remain
unstarted and out of scope for this session.

## Receipt — S3 `pr`-kind proof, slopgate, 2026-08-15

**Coordinator decision (mechanics, taken):** substitute target `slopgate` (already `MODE='pr'`,
no production deploy fires on merge; `platform` excluded — live review session working there;
`zync.is` excluded — `push-triggered`, correctly refused). Run the landing-half proof; skip
`none` for now (`invariantum` needs its own enrollment inspection first — record as next
action, don't do it this session).

**Landing half — proven, real project, no hand-typed push:**
- Read `slopgate/CLAUDE.md` and `AGENTS.md` first: both empty of landing-cycle content.
  `git config harness.landQueue` unset on slopgate → **not queue-enrolled; this land went
  direct through `.claude/scripts/ship.sh`, not through the stage-a queue.** No other live
  agent process found attached to slopgate at the time (checked `ps aux`); the main checkout
  had pre-existing uncommitted WIP unrelated to this task (`crates/slopgate-core/...`,
  `hooks/...`) — untouched, worked from a fresh `od-worktree add s3-delivery-pr-proof` instead.
- Docs-only trivial change: one clarifying line in `CONTRIBUTING.md` (first-time-clone setup
  order), committed on `wt/s3-delivery-pr-proof`.
- `bash .claude/scripts/ship.sh land wt/s3-delivery-pr-proof <worktree>` ran slopgate's real
  gate (the frozen `TESTCMD` — cargo fmt/clippy/build --release, `npm run self-test`, `npm test`,
  pinned actionlint; not routed around) and finished
  `{"status":"pr-opened","url":"https://github.com/alexcodeplace/slopgate/pull/19"}` — no
  hand-typed push, wrapper's normal path stopped at the PR as designed (`MODE_NOTE`: "the brain
  NEVER pushes main in pr mode"). PR #19 confirmed open/mergeable via `gh pr view`.

**Delivery half — confirmed NOT achievable today; this is a genuine S1/S2 gap, not a defect
introduced this session, and NOT something this lane fixed (out of scope: "do NOT edit
finish-branch.sh/ship-init.sh"):**
- `finish-branch.sh` `land()`: the entire delivery-kind case block (the only place
  `_write_delivery_receipt` is ever called) is gated `if [[ "$mode" == "merge-to-main" ]]`
  (line ~1987). For `mode == pr`, that block never executes — no receipt is ever written,
  regardless of what `--delivery` value is passed.
- `ship-init.sh` (line ~265-267) documents this as deliberate: "pr mode's delivery is implicit
  and never passed to `land`" — `DL_FLAGS` stays empty for pr mode, so the generated wrapper
  never even carries a `--delivery` flag into `land` in the first place. Re-running
  `ship-init.sh --force` on slopgate would regenerate an identical wrapper for this reason —
  not attempted, would only risk clobbering the frozen base64 `TESTCMD` for zero gain.
- `collector/src/adapters/delivery.ts` documents the same thing from the read side: "Written
  by finish-branch.sh `_write_delivery_receipt` after every **merge-to-main** land that
  declares a delivery kind." `kind: "pr"` is a live value in its zod enum but is unreachable —
  no code path in the brain ever constructs it.
- **Repro, captured this session:** after PR #19 opened successfully,
  `/home/user/Projects/slopgate/.claude/delivery-receipt.json` does not exist. That absence
  is the artifact — landing-to-PR succeeded, delivery reporting for `pr` kind produced nothing,
  so the collector's Inbox has no item for this land and never will under the current code.
- **Net: the plan's own claim** ("`pr` becomes a delivery kind so one vocabulary covers every
  cycle", S2 "every enrolled project's last delivery... in owner language", S3 "the board shows
  delivery completed as declared") **is not met for `pr` today.** `pr` exists in enrollment-time
  validation (`ship-init.sh` refuses a contradicting `--delivery` in pr mode) and in the receipt
  schema, but has zero runtime path to ever emit a receipt. This is the lander lane's gap to
  close (extend `_write_delivery_receipt` to fire on a successful pr-mode land, or the plan's
  S3 acceptance criterion is unmeetable as written), not something decided or fixed here.

**`none`-kind proof: not attempted this session** (coordinator decision — skip for now,
`invariantum` needs its own enrollment inspection first). `invariantum`'s current wrapper
(`MODE='merge-to-main'`, only `pr-gate.yml`, no push-triggered deploy job) remains a plausible
candidate but is unenrolled (`harness.landQueue` unset) and unverified for a genuine no-deploy
claim — treat as PROPOSED, not ready.

**Not verified this session:** whether slopgate's PR #19 will pass its own remote CI a second
time (only the local pre-push gate ran here); whether `platform`'s active review session state
changed since checked; anything about `multideal` beyond its frozen `MODE='pr'` fact (not
inspected further, not needed for this proof).

**Next executable action:** hand `pr`-kind receipt gap to the lander lane (extend
`_write_delivery_receipt` to a successful pr-mode land, or narrow S2/S3's acceptance text to
match what's actually implemented). Separately: someone runs `ship-init.sh` Phase A/B fresh on
`invariantum` and confirms it has no deploy step before treating it as the `none`-kind target.

Terminal step of the delivery program. Depends on: `2026-08-14-landing-daemon.md` (agents submit and
walk away), `2026-08-15-auto-deploy-on-main.md` (nothing triggers delivery by hand).

## What already exists — do NOT rebuild it

Enrollment for the LANDING half is built and proven on a second project:

- **The contract is a generated file.** `ship-init.sh` inspects a project and freezes its
  facts into `<root>/.claude/scripts/ship.sh` — base branch, mode, test command, dep
  command, post-land command. Project-specific values, one shared shape.
- **The mechanism is shared.** That generated wrapper delegates to one brain,
  `~/.claude/workflows/lib/finish-branch.sh`. Projects never carry lander logic.
- **Per-repo opt-in.** `git config harness.landQueue stage-a` enrolls a repo in the queue.
- **Base branch is already parameterized.** The queue was hard-coded to `main`; that was
  generalized for Press.zone's `master` and proven end to end on 2026-08-13
  (`2026-08-13-presszone-auto-lander-enrollment.md`), including the trunk guard ref
  (`harness.landGuardRef`).
- **PR mode already exists** alongside merge-to-trunk (`--mode pr|merge-to-main`).

So "each project with its own scripts but Overdeck owns the contract and the mechanism" is
already the architecture. Two things are missing.

## The two gaps

**1. Delivery is a hole in the contract.** The frozen facts carry a `POSTLANDCMD` string —
an opaque shell command Overdeck runs and does not understand. That works for overdeck's own
`deploy-local.sh` and for nothing else. It cannot express "this project deploys to Cloudflare
Pages by pushing", "this project has no deploy at all", or "this one deploys only from a
tagged release". And because it is opaque, Overdeck cannot report *what* delivery did, retry
it, or show the owner that it failed — which is the same blindness that let the deploy queue
back up for a day unnoticed.

**2. Nothing enrolls a project without an agent doing it by hand.** Press.zone took a
dedicated session. `2026-08-13-land-enroll-onboarding-script.md` is registered and IDLE for
exactly this.

## Acceptance

Owner-obtainable evidence: in three different projects with three different cycles, an agent
commits, submits, and exits. The owner sees each project's work reach its own destination —
one merged to trunk and deployed locally, one opened as a PR, one live on Cloudflare — on one
Overdeck board, without any agent waiting.

## Constraints

- **The contract is data, not a shell string.** A delivery stage declares its KIND. Overdeck
  knows the kinds; the project supplies the values and, where it needs one, its own script.
  An opaque command Overdeck cannot reason about is what this plan exists to remove.
- **`none` is a first-class kind, not an empty string.** A project with no deployment must
  declare that, so a missing delivery is distinguishable from an unconfigured one.
- **The project owns its script; Overdeck owns invocation, ordering, retry and reporting.**
  Never absorb a project's build steps into the brain.
- **Fail closed on an unknown kind.** A project declaring a kind Overdeck does not implement
  refuses to enroll, loudly. NEVER guess a default and deliver somewhere wrong.
- **Enrollment never touches project code.** It writes the generated wrapper, sets git config,
  installs the guard. Nothing else.
- **Every enrolled project reports to one board.** A cycle the owner can only inspect by
  cd-ing into a repo has not been enrolled in anything.

## Delivery kinds — the contract's vocabulary

Each is a KIND plus that project's values. Derived from the real projects at hand; the set
grows by adding a kind, never by widening an opaque string.

- `none` — landing is the whole cycle. Declared explicitly.
- `local-script` — run this project's script on the delivery host. Overdeck's own
  `packaging/deploy-local.sh` is this kind.
- `push-triggered` — the push IS the deployment (Cloudflare Pages, and any host watching a
  branch). Delivery completes when the remote reports it, so the contract needs the check
  Overdeck polls, not a command it runs.
- `pr` — the destination is a review queue, not a deployment. Already supported as a mode;
  it becomes a delivery kind so one vocabulary covers every cycle.

## Slices

Each ends in something the owner can see, landed and delivered, before the next starts.

**S1 — delivery kind replaces the opaque post-land command.** The generated wrapper declares
a kind, and overdeck itself is re-expressed as `local-script`. Evidence: overdeck lands and
deploys exactly as before, with the kind visible in the frozen facts. Enabling-only: it is the
contract every later slice speaks; it unlocks kinds that a shell string cannot express.

**S2 — delivery outcome is visible on the board.** Every enrolled project's last delivery,
its kind, and whether it succeeded, in owner language. Evidence: the owner sees overdeck's
own deploys on the board with their reason when they fail.

**S3 — `none` and `pr` kinds work on a real project.** The first project whose cycle is NOT
overdeck's. Evidence: land in that project; the board shows delivery completed as declared;
no agent waited.

**S4 — `push-triggered` works against Cloudflare.** Evidence: land in the Cloudflare project;
the board shows the deployment going live, polled from the host, not assumed.

**S5 — one command enrolls a project.** Subsumes the IDLE onboarding-script plan; supersede
it rather than running both. Evidence: enroll a project the owner names, in one command, and
land from it the same session.

**S6 — the fleet is enrolled.** Press.zone backend, Press.zone plugins, zync.is, and the rest.
Evidence: the owner opens one board and sees every project's cycle on it.

## Not in scope

Changing any project's build, test or deploy scripts. Enrollment adopts what a project already
does; a project whose deploy is broken gets a visible failed delivery, not a rewrite.
