---
name: strike-trio-en-v1-3-0
description: "Strike Trio — Set up and run a Strike Trio project with PM + 2 AI Dev agents as a trio. Use in two situations. (1) PROJECT START — to begin a new product project the Strike Trio way: 'start a new project', 'set up a project', 'let's go Strike Trio', or opening an empty folder to begin development. Runs Phase 1: collects people/project info, creates the document scaffold, then hands off to Discovery & Framing. (2) ENVIRONMENT SETUP — after D&F, when the user says 'start setup', 'set up the environment', 'set up the dev environment', or 'infrastructure setup'. Runs Phase 2: builds infrastructure (TB project, repo, bot, hosting) and local wiring via a step-by-step environment-setup checklist, then guides Pre-IPM → IPM. Use this skill both to start a Strike Trio project and to set up its dev environment — even if the user just says 'let's start' in an empty folder."
---

# Strike Trio `v1.3` (EN)

This skill sets up a Strike Trio project from an empty folder and brings it to a runnable state. The goal: an environment where PM + 2 AI Dev agents (Track A / Track B) work as a trio.

Setup is split into **two phases**, with Discovery & Framing (D&F) running in the PM session between them.

```
Phase 1 (project start)  →  D&F (PM session)  →  Phase 2 (environment setup)  →  start dev
```

- **Phase 1** — Collect people/project info + create the document scaffold. Hands off to D&F when done.
- **Phase 2** — Entered via the `start setup` trigger once D&F is done. Builds this project's infrastructure (TB project, repo, bot, hosting) and local wiring via the `environment-setup.md` checklist, **one step at a time**, then guides Pre-IPM → IPM.

> **Why two phases**: Environment setup (repo, tracker, commands, Dev names) only matters once D&F is done and you know what you're building. If you ask about infrastructure from the start and stamp out every file at once, the D&F you actually need gets pushed back, and info like Dev names is collected before there's any use for it. Gather information when it's needed.

## Entry Decision — Phase 1 vs Phase 2

When the skill is triggered, first assess the current state.

- If `documents/product/product-overview.md` is **missing or empty** → **Phase 1** (new project).
- If the document scaffold **already exists** and the user entered via `start setup` / `set up the environment`, etc. → **Phase 2**.
- If ambiguous, ask the user which one once.

---

# Phase 1 — Project Start

## Structure to Create (Phase 1)

```
[project-folder]/
  CLAUDE.md                         ← Dev/infra values are (confirmed during setup) placeholders
  .gitignore                        ← includes documents/tracks/
  documents/
    aabt-workflow.md
    delivery-playbook.md
    glossary.md
    product/
      product-overview.md
    coding-standards.md             ← Kent Beck TDD/Tidy First principles + stack-specific style guide
  commands/.gitkeep                 ← placeholder only. Actual command files are created in Phase 2
```

> Track files and command files require Dev names, so they're created in Phase 2.

## Step 0 — Pre-flight Notice (Install the Persona Builder)

**Before** collecting any information, advise the user to install the persona builder skill first. It's used in the D&F persona stage, and if it's missing when you get there the workshop stalls — so have it installed up front.

```
ℹ️ Before we start — the D&F persona stage of this project uses a separate 'persona builder' skill.
   Please install it now: Settings → Customize → Skills → ＋ → Create skill → Upload.
   If you wait until the persona stage to install it, the workshop will stall midway.
```

## Step 1 — Collect Information (2 Rounds)

Call AskUserQuestion twice in order. Each round proceeds after the previous one is complete. **Dev names and infrastructure (Tracker/GitHub/Netlify) are not asked here — they're collected in Phase 2.**

### Round 1 — People
1. **Name** — What should Claude call you? (e.g., `Heewon`)
2. **Company / Team name** — e.g., `Bekind Labs`, `Acme Corp`

### Round 2 — Project
1. **Project (app) name** — e.g., `Mitte!Beat`, `Mitte!Guests v2`
2. **One-line description** — What this product is, in one sentence
3. **Tech stack** — e.g., `React + Supabase + Netlify`, `Next.js + Vercel`

## Step 2 — Generate the Document Scaffold

> **Always announce this before you start generating**:
> "I'll now generate the project document scaffold. There are several files, so this may take anywhere from tens of seconds to a few minutes. I'll let you know when it's done."

Using the collected information, create the following files in order.

### 2-1. CLAUDE.md

Read this file and use it as the basis:
`assets/CLAUDE.md.template`

Replace the `{{USER_NAME}}`, `{{COMPANY_NAME}}`, `{{PROJECT_NAME}}`, `{{PROJECT_DESCRIPTION}}`, `{{TECH_STACK}}` placeholders with the collected values.

**Values you don't know yet** (`{{TRACKER_BOOT_ID}}`, `{{GITHUB_REPO_URL}}`, `{{DEPLOY_PROVIDER}}`, `{{PROD_URL}}`, `{{PREVIEW_URL_A}}`, `{{PREVIEW_URL_B}}`) are filled in as `(confirmed during setup)`. They're replaced with real values in Phase 2 based on the hosting choice.

**Handle the Dev-dependent sections so no broken trigger keywords are created.** At Phase 1 there are no Dev names yet, so leave the parts below as placeholder notes (replaced with the full template in Phase 2, Step 3-3):
- "Dev Team" table → replace with a single `(confirmed during setup)` line.
- The `dev-{{DEV_A_NAME}} start` / `dev-{{DEV_B_NAME}} start` rows in the session trigger table → remove and replace with a single `dev-* start` (added after setup) line. **Do not create broken keywords like `dev-(confirmed during setup) start`.**
- Delivery cycle / preview/Accept formats / Dev track report format sections → replace with a `> (activated after environment setup)` note.

> All D&F needs is Project / Me / the `PM start` and `create a persona` triggers / Preferences. The Dev and delivery parts are completed in Phase 2.

### 2-2. documents/aabt-workflow.md

Copy this file as-is:
`assets/aabt-workflow.md`

Do not modify. The team operations playbook is standard.

### 2-3. documents/delivery-playbook.md

Read this file and use it as the basis:
`assets/delivery-playbook.md`

Replace `{{PROJECT_NAME}}`. Leave `{{DEV_A_NAME}}`, `{{DEV_B_NAME}}` as `(confirmed during setup)` and replace them in Phase 2.

### 2-4. documents/glossary.md

Read and copy this file:
`assets/glossary.md.template`

### 2-5. documents/product/product-overview.md

Read this file and use it as the basis:
`assets/product-overview.md.template`

Replace the `{{PROJECT_NAME}}`, `{{PROJECT_DESCRIPTION}}` placeholders.

### 2-6. documents/coding-standards.md

Copy this file as-is:
`assets/coding-standards.md`

Do not modify. The Kent Beck original + stack-specific style guide is standard.

### 2-7. .gitignore

Create `.gitignore` or append the following to an existing one.

```
# Strike Trio — track files managed via symlink, excluded from git
documents/tracks/

# macOS
.DS_Store
```

> **Why**: Dev worktrees live in a separate path and reference the main project's track files via symlink. Excluding track files from git prevents conflicts caused by git mistaking symlinks for real files. `.DS_Store` is a macOS junk file, excluded so it never gets staged into commits.

### 2-8. commands/.gitkeep

Create an empty file as a placeholder for command files. Actual command files are created in Phase 2.

## Step 3 — Completion Report + D&F Handoff

> Persona builder installation was already advised in Step 0. Here, only the completion report and D&F guidance.

```
✅ [PROJECT_NAME] document scaffold created (Phase 1)

Files created:
- CLAUDE.md (with session triggers / Dev and infra values confirmed during setup)
- .gitignore (documents/tracks/ excluded)
- documents/aabt-workflow.md (team operations playbook)
- documents/delivery-playbook.md (delivery playbook)
- documents/glossary.md
- documents/product/product-overview.md
- documents/coding-standards.md
- commands/.gitkeep

Next step — Discovery & Framing:
→ In a PM session, load context with "PM start" or "playbook".
→ D&F order: Vision → Personas → Problem Exploration → Problem Priority → Value Propositions
   → Feature Exploration → Feature Priority → Technical Feasibility → Wireframes → Story Mapping
   (see documents/aabt-workflow.md section 2 for the full sequence)
→ The persona stage uses the persona builder skill.

When D&F is done, say "start setup". I'll set up the development environment (repo, tracker, commands).
```

---

# Phase 2 — Environment Setup

**Trigger**: `start setup`.

Entered once D&F is done and you know what you're building. Create this project's infrastructure (TB project, repo, bot, hosting) and local wiring **one step at a time**.

> **Onboarding ≠ Phase 2.** Installing the Claude desktop app, accounts, tools, and the skill is **once per person·machine (onboarding)** — it should already be done via `docs/strike-trio-onboarding-prompt.md`. On entry, if `gh auth status` (GitHub CLI) or the Tracker Boot MCP connection is missing, tell them "go through the onboarding prompt first" and stop. Phase 2 uses those accounts/tools to create **this project's resources** (accounts/tools are once; project resources are per project).
>
> On entry, lightly check that `documents/product/product-overview.md` has D&F results. If empty, ask: "It looks like D&F isn't finished. Proceed anyway?"

## Principle — one step at a time, via the checklist

- **`documents/environment-setup.md` is the single source of truth for state.** Create it from `assets/environment-setup.md.template` if missing; if present, read it and continue **from the next ⬜ step**. As each step finishes, update the table to ✅ and record the confirmed infra value.
- **Never dump everything at once.** Don't start the next step until the previous is ✅. (Prevents overwhelming beginners.)
- **Infrastructure is NOT a Tracker Boot Chore.** (Older versions registered repo/deploy as Chores — dropped; don't mix product stories with environment chores.)
- **Claude does what it can directly** (`gh`, files, bash). Only human account actions (web login, clicks, token copy) get hand-held. It's "human + Claude," not "human alone."
- **Never put `#` comments in command blocks the user pastes into a terminal** (runbook ④). zsh has `interactive_comments` off by default, so a pasted `#`·`)` dies with `parse error`. Keep explanations in prose outside the block.

## Step 0 — Precondition gate + team gate + create the checklist

**Check what's needed up front — don't leave it to the last step (runbook ①).** If anything is missing, fill it right here, then proceed. (Using a secret only at the final step means its absence surfaces late and forces a backtrack.)

1. **Local context** — run environment setup in a **local Claude Code session**. The Cowork sandbox has no gh or global env, so gh/bot/secret commands won't run there (runbook ⑦).
2. **Check preconditions all at once** — run this and read the result:
   ```
   gh auth status
   set -a; . ~/.strike-trio/.env 2>/dev/null; set +a
   echo "devbot=${#DEVBOT_TOKEN} tbkey=${#TRACKER_BOOT_API_KEY}"
   ```
   - gh not authenticated → "finish `gh auth login` in onboarding first" and stop.
   - TB MCP not connected (`tb_get_projects` fails) → "connect Tracker Boot in onboarding first" and stop.
   - If `devbot`/`tbkey` length is **0, fill it right here**: `touch ~/.strike-trio/.env && open -e ~/.strike-trio/.env`, and guide the user to paste each key on **its own line** (bot token = 1Password one-time share, TB key = trackerboot.com/settings/api). Save, then **re-verify** with the `echo` above — both must be non-zero to pass.
3. If `documents/environment-setup.md` doesn't exist, create it from `assets/environment-setup.md.template` (replace `{{PROJECT_NAME}}`, `{{DATE}}`); if it exists, read it and brief the progress.
4. **AskUserQuestion: "Are you on the Bekind Labs team?"** — **Yes** = internal shared bot `strike-trio-devbot` / **No** = your own team bot. Record the answer in the environment-setup.md team gate.
5. Start from the next ⬜ step.

> **Safe env-wiring rule (runbook ②):** a `>>` append with no trailing newline in the file **merges into the previous line and silently destroys the token** (a real incident where the TB key attached to `DEVBOT_TOKEN` and broke both). So make **`open -e` editor editing the default**; if you must append, guarantee the newline with `printf '\n%s=%s\n' KEY "$VAL" >> file`. **Always verify length right after wiring** with `echo "devbot=${#DEVBOT_TOKEN} tbkey=${#TRACKER_BOOT_API_KEY}"` — stop immediately if 0. Wiring is "up front, all at once, with verification."

## Step 1 — Tracker Boot project

The MCP connection was done at onboarding (precondition). Here you only create **the TB project for this app**.

- Guide (human account action): "Log in to trackerboot.com → top-left **＋ New project** → name it and create. Once created, just tell me the name."
- Claude finds the just-created project via `tb_get_projects` and **grabs the Project ID automatically** (no copy/paste).
- environment-setup.md: record the Project ID, mark step 1 ✅. This value is `{{TRACKER_BOOT_ID}}`.

## Step 2 — GitHub repo (Claude does it via gh)

- Check auth with `gh auth status`. **If authenticated, Claude creates it directly** — don't ask for the account (`gh` already knows it):
  ```bash
  git init && git add -A && git commit -m "chore: project scaffold + D&F documents"
  gh repo create <account>/<repo> --private --source=. --remote=origin --push
  git branch production main && git push -u origin production
  ```
  Suggest the repo name as the project name in kebab-case. Run `git init` first — `--source=.` assumes an existing git repo, so an empty folder fails without it (runbook ③). Harmless if already initialized. Pre-create the `production` branch (deploy-topology precondition).
- If `gh` isn't authenticated: tell them "finish the GitHub CLI step (`gh auth login`) in onboarding first" and stop. (Web creation is a fallback only when gh isn't usable.)
- Use the clone URL as `{{GITHUB_REPO_URL}}`. environment-setup.md: record `owner/repo`, mark step 2 ✅.

## Step 3 — Bot access (per team gate + auto-accept)

The PR author must be a **bot** separate from the PM for the merge approve gate to work (author = approver → GitHub blocks self-approve → deadlock). Track ownership is anchored by the branch name `dev-*` (source of truth), so a shared bot doesn't get tangled.

- **Yes (internal)** — Claude ① invites the bot to this repo (PM's own `gh` permission) and ② **auto-accepts immediately with the bot token** (zero human wait; works on personal or org repos). No `#` comments in the block below (runbook ④):
  ```bash
  gh api repos/<owner>/<repo>/collaborators/strike-trio-devbot -X PUT -f permission=push
  [ -z "$DEVBOT_TOKEN" ] && echo "No bot token — do the Step 0 preconditions first" && exit 1
  GH_TOKEN="$DEVBOT_TOKEN" gh api /user/repository_invitations \
    --jq '.[] | select(.repository.full_name=="<owner>/<repo>") | .id' \
    | xargs -I {} env GH_TOKEN="$DEVBOT_TOKEN" gh api --method PATCH /user/repository_invitations/{}
  ```
  `DEVBOT_TOKEN` is already loaded into the session from the global `~/.strike-trio/.env` in Step 0 (confirmed/filled at the precondition gate — runbook ①⑤). The guard stops loudly on an empty token — no silent 404.
- **No (external)** — same with your own team bot. If there's no bot, create the bot account + a classic PAT (`repo` scope) once, then proceed.
- environment-setup.md: mark step 3 ✅.

> **Invite/accept need no bot token** — the invite uses the PM's `gh`, the accept uses the bot token. The bot token is actually used only later, when `pr.sh` opens a PR under the bot's name. **Never expose the token in chat, docs, or the repo — if exposed, revoke and reissue immediately.**

## Step 4 — Deploy hosting

Pick the hosting with AskUserQuestion — this choice determines the preview/acceptance/production URLs and the CLAUDE.md deploy values.

**Options**: `Netlify` / `Cloudflare Pages` / `Vercel` / `Other (set up yourself)`

| Hosting | `{{DEPLOY_PROVIDER}}` | `{{PROD_URL}}` | `{{PREVIEW_URL_*}}` (dev-{name} branch) | Also collect |
|---------|---------------------|--------------|----------------------------------------|--------------|
| Netlify | `Netlify` | `https://{site}.netlify.app` | `https://dev-{name}--{site}.netlify.app` | site name |
| Cloudflare Pages | `Cloudflare Pages` | `https://{project}.pages.dev` | `https://dev-{name}.{project}.pages.dev` | project name |
| Vercel | `Vercel` | `https://{project}.vercel.app` | `https://{project}-git-dev-{name}-{scope}.vercel.app` | project, scope (team/account slug) |
| Other | (user's answer) | type in directly, or `(add later)` | `(fill in yourself)` | — |

- **Netlify/Cloudflare/Vercel**: Complete `{{PROD_URL}}`, `{{PREVIEW_URL_A}}` (dev-{{DEV_A_NAME}}), and `{{PREVIEW_URL_B}}` (dev-{{DEV_B_NAME}}) using the formats above.
- **Other**: Don't invent deploy automation or URLs. Set `{{DEPLOY_PROVIDER}}` to the hosting name the user gave, `{{PROD_URL}}` to a direct entry or "add later", and the preview URLs to `(fill in yourself)`. Delegate it: "Connect this hosting yourself — tell me the production and preview URLs and I'll fill them in" (when you don't know, don't block — delegate; blocker-not-breaker).

#### Acceptance gate (standard — the last stop before production)

A merge doesn't go straight to production; it lands on **acceptance** (= the `main` branch deploy) for checking first. Build it as a **single site + Netlify-native deploy contexts** (do NOT split into two sites):

- **`production` branch → production context** — user-facing. `{{PROD_URL}}`. After Accept, **GitHub Actions (`promote.yml`)** fast-forwards the `production` branch and it builds automatically (within ~5 min).
- **`main` branch → branch deploy (acceptance mirror)** — the PM's final Accept check. Same build and env as prod. `{{ACCEPTANCE_URL}}`.
- **`dev-*` branch → branch deploy (dev preview)** — `{{PREVIEW_URL_*}}` (a branch deploy on the same site).
- **PR → deploy preview** — for code review.

Because `promote` is a fast-forward of the GitHub `production` branch, it's **hosting-agnostic** — only the hosting setup below:

- **Netlify (single site)**: connect the repo to **one** site and set the production branch to `production`. Then `production` = production context (`{{PROD_URL}}` = `https://{site}.netlify.app`), `main` = branch deploy (`{{ACCEPTANCE_URL}}` = `https://main--{site}.netlify.app`), and `dev-*`·PR all appear on the **same site**. ⚠️ **Do NOT create a separate site for acceptance** — splitting into two sites scatters env across site×context, so the **PR preview gets no env and renders blank** (a trap verified on mitte!beat).
  - **Connect click-path (current Netlify UI)**: ① **Add new project** → **Import a Git repository** → GitHub → pick the repo ② set the **Project name** (= your site name; no separate "Change site name" step) ③ change **Branch to deploy** from `main` to **`production`** ④ hit **Deploy**. *(Build command·publish use the stack defaults — if there's no app yet, the first build failing is fine; the app appears in the first story. Netlify may change UI labels, so match the nearest item if the screen differs.)*
  - **Pin env in `netlify.toml` as the single source** (no per-site dashboard scoping — that's the drift source):
    ```toml
    [build.environment]
      VITE_SUPABASE_URL = "https://<project>.supabase.co"
      VITE_SUPABASE_ANON_KEY = "<anon-key>"
    ```
    `[build.environment]` applies to every context, so production·branch·preview all get the same env. Pin only `VITE_` public keys — the anon key is RLS-protected, so committing it to git is fine. Real secrets (service_role, tokens) live only in the local `.env` and Edge Function secrets.
- **Cloudflare Pages / Vercel**: set the production branch to `production` (not the default main); `main`·`dev-*`·PR are previews. (These are single-project models to begin with, so there's no two-site trap.)
- **Other**: configure the connection from the `production` branch to your production environment yourself (`promote.yml` works as-is — it's a GitHub branch fast-forward).

**Accept → auto-publish to production (GitHub Actions)** — `promote.yml` (generated in the local-wiring step) polls the tracker every 5 min via cron and FFs Accepted stories to `production`. It doesn't depend on the PM session's memory, and self-heals on the next tick even if one is missed. Two settings:
- **Register a repo secret**: GitHub repo → Settings → Secrets and variables → Actions → **New repository secret** → name `TRACKER_BOOT_API_KEY`, value identical to the one in your global `.env` (promote uses it to query Accepted status).
- Production advances via the Actions-native `GITHUB_TOKEN` (`permissions: contents: write`) — **no bot token or extra secret needed** (the bot token is solely for separating the PR author).

environment-setup.md: record the site name/URLs, mark step 4 ✅.

## Step 5 — Local wiring

Now collect the Dev names and create the track files, docs, commands, and wiring.

### 5-1. Collect Dev names
Via AskUserQuestion:
1. **Dev display names** — What should Dev A and Dev B be called? (e.g., `Alice / Bob`) — free input.
2. **Dev system names** — English names for branches/paths. **Letters and hyphens only** (no numbers, spaces, or special characters). Claude adds the `dev-` prefix. (e.g., `alice / bob`)

### 5-2. Track files
Read `assets/track.md.template` and create `documents/tracks/dev-{{DEV_A_NAME}}.md` and `dev-{{DEV_B_NAME}}.md`. Replace `{{DEV_NAME}}`, `{{PROJECT_NAME}}`, `{{USER_NAME}}`.

### 5-3. Complete CLAUDE.md / delivery-playbook.md
**Regenerate CLAUDE.md from the full `assets/CLAUDE.md.template`.** Restore all the Dev-dependent sections left as placeholder notes in Phase 1 (Dev Team table, the `dev-{{DEV_A_NAME}} start`/`dev-{{DEV_B_NAME}} start` trigger rows, delivery cycle, preview/Accept formats, Dev track report format), and fill in the placeholders with real values:
`{{TRACKER_BOOT_ID}}` (Step 1), `{{GITHUB_REPO_URL}}` (Step 2), `{{DEV_A_NAME}}`·`{{DEV_B_NAME}}`·`{{DEV_A_DISPLAY}}`·`{{DEV_B_DISPLAY}}` (5-1), and the `{{DEPLOY_PROVIDER}}`, `{{PROD_URL}}`, `{{ACCEPTANCE_URL}}`, `{{PREVIEW_URL_A}}`, `{{PREVIEW_URL_B}}` **confirmed in Step 4**. If hosting is "Other" or URLs are undecided, leave `{{PROD_URL}}`/`{{ACCEPTANCE_URL}}` as `(add later)` and the preview URLs as `(fill in yourself)`.
Also replace `{{DEV_A_NAME}}`·`{{DEV_B_NAME}}` in delivery-playbook.md.

### 5-4. iteration-plan.md
Read `assets/iteration-plan.md.template`, replace `{{PROJECT_NAME}}`, and create the file. It's a **decision log** — the rationale for "why this order / what was decided or rejected." It does not hold state (next story, progress, points) — the tracker is the source of truth.

### 5-5. Command files + promote.yml
Created in "Create Command Files" below (`ready`, `pr.sh`, `approve`, `decline`, `merge`, `push-docs` + `.github/workflows/promote.yml`). The bot token uses the global `.env` fallback, so you don't re-enter it per project.

environment-setup.md: mark step 5 ✅.

## Step 5-5 detail — Create command files (+ `.github/workflows/promote.yml`)

> ⚠️ Proceed only after the local repo path is confirmed. Ask first if the path is unknown.

Create the following files — double-click commands and scripts go in the `commands/` folder, and the promote workflow (`promote.yml`) goes in `.github/workflows/`. `{{DEV_A_NAME}}`/`{{DEV_B_NAME}}` are the system names collected in Step 1. `{{GITHUB_REPO_URL}}` is the GitHub clone URL collected in Step 2.

**ready-{{DEV_A_NAME}}.command** (ready-{{DEV_B_NAME}}.command has the same structure, name replaced):
```bash
#!/bin/bash
REPO_URL="{{GITHUB_REPO_URL}}"
REPO_DIR="$(cd "$(dirname "$0")/.." && pwd)"
WORKTREE="$HOME/Documents/Claude/Worktrees/$(basename "$REPO_DIR")-{{DEV_A_NAME}}"

# Prune stale worktree registrations
git -C "$REPO_DIR" worktree prune 2>/dev/null

if [ ! -d "$WORKTREE" ]; then
  # First-time worktree creation — branch from origin/main
  mkdir -p "$(dirname "$WORKTREE")"
  git -C "$REPO_DIR" fetch origin
  git -C "$REPO_DIR" worktree add "$WORKTREE" -B dev-{{DEV_A_NAME}} origin/main 2>/dev/null || \
  git -C "$REPO_DIR" worktree add "$WORKTREE" dev-{{DEV_A_NAME}} 2>/dev/null
else
  # Existing worktree: reset to a clean origin/main state before starting a new story
  git -C "$REPO_DIR" fetch origin
  git -C "$WORKTREE" reset --hard origin/main
  git -C "$WORKTREE" clean -fd 2>/dev/null
fi

# Symlink track file from main project — recreated on every run (auto-recovers if lost)
mkdir -p "$WORKTREE/documents/tracks"
ln -sf "$REPO_DIR/documents/tracks/dev-{{DEV_A_NAME}}.md" "$WORKTREE/documents/tracks/dev-{{DEV_A_NAME}}.md"

# Current story number — extracted inline here in the ready body so the finished guard is self-sufficient (it does NOT depend on
# the beat helper, so the guard survives even if the helper is missing or broken). First #NNN in the "## Current Story" section
# only — section-scoped so it never grabs a number from "## Completed Stories" above (guard accuracy).
STORY=$(awk '/^## Current Story/{f=1;next} f&&match($0,/#[0-9]+/){print substr($0,RSTART+1,RLENGTH-1);exit}' "$REPO_DIR/documents/tracks/dev-{{DEV_A_NAME}}.md")

# Finished-story guard (#290): refuse ready if the current story is Finished/Delivered/Accepted —
# blocks regressions that re-Start a closed story or pointless re-launches. blocker-not-breaker:
# any uncertainty (tracker query failure, missing key, etc.) passes (exit 0); only an explicit finished verdict BLOCKS (exit 3).
node "$(dirname "$0")/ready-guard.mjs" "$REPO_DIR/.env" "$STORY"
if [ "$?" -eq 3 ]; then
  echo "⚠️ Finished story — aborting ready. Assign a new story, then run again." >&2
  exit 1
fi

# ── Beat wiring (only meaningful if a team-activity tool is parked; a harmless no-op otherwise) ──
# Beat is an add-on — the finished guard above is already done, independent of this block. If the helper is absent we skip the
# source (existence check) and emit silently skips (function-absent). The guard's fate is never tied to beat.
BEAT_HELPER="$(dirname "$0")/_emit-pm-beat.sh"
[ -f "$BEAT_HELPER" ] && source "$BEAT_HELPER"

# Worktree .env — unlike the track file this is a COPY, not a symlink: copy main .env but swap MITTE_BEAT_TRACK to this lane
# and inject the current story as MITTE_BEAT_STORY. When a team-activity beat tool (e.g. mitte-beat) is parked, the reporter
# running in the worktree reads these via vite loadEnv and attributes making cubes to the right track/story. If main .env is
# absent (project not parked / fresh clone) skip silently. Re-created each run. (A symlink would point both worktrees at the
# same .env, pinning the track to one value.)
if [ -f "$REPO_DIR/.env" ]; then
  if grep -q '^MITTE_BEAT_TRACK=' "$REPO_DIR/.env"; then
    sed 's/^MITTE_BEAT_TRACK=.*/MITTE_BEAT_TRACK={{DEV_A_NAME}}/' "$REPO_DIR/.env" > "$WORKTREE/.env"
  else
    { cat "$REPO_DIR/.env"; echo "MITTE_BEAT_TRACK={{DEV_A_NAME}}"; } > "$WORKTREE/.env"
  fi
  if [ -n "$STORY" ]; then
    grep -v '^MITTE_BEAT_STORY=' "$WORKTREE/.env" > "$WORKTREE/.env.tmp" && mv "$WORKTREE/.env.tmp" "$WORKTREE/.env"
    echo "MITTE_BEAT_STORY=$STORY" >> "$WORKTREE/.env"
  fi
fi

# Assignment = PM intervention → assign white cube. Emitted here, before claude (blocking), to open the beat bundle at dev start.
# Emits only if the helper loaded and the project is parked — otherwise skips silently (no effect on guard/ready).
command -v emit_pm_beat >/dev/null 2>&1 && emit_pm_beat assign {{DEV_A_NAME}} "$REPO_DIR/.env" "$STORY"

cd "$WORKTREE"
claude "dev-{{DEV_A_NAME}} start"
```

> **Why `~/Documents/Claude/Worktrees`**: Placing the worktree inside the project folder causes PM session file edits to affect the worktree too, triggering "local changes would be overwritten" errors. A permanent path keeps it separate from the PM workspace while preserving the working copy across restarts. `reset --hard` + `clean -fd` guarantees a clean state at every session start.

> **Symlink approach**: `documents/tracks/` is excluded from git via `.gitignore`, and on each `ready` run, the main project's track files are symlinked into the worktree. When the PM edits a track file in the PM session, it is immediately visible to the Dev session — no git push required.

> When `claude "dev-{{DEV_A_NAME}} start"` runs: Claude Code receives the trigger from CLAUDE.md and first executes `git fetch origin && git reset --hard origin/main`. Since `documents/tracks/` is excluded via `.gitignore`, symlinks are preserved. Then the track file is read.

**_emit-pm-beat.sh** (a shared helper ready sources when present — for beat tools):
> In a parked project, ready emits the one act with no other trace — `assign` — as a white cube at hand-off (details in the code header below). **In an unparked project there are no creds, so it's a harmless no-op** — the scaffold works as a generic Strike Trio setup.

```bash
#!/bin/bash
# Source: mitte-beat commands/_emit-pm-beat.sh (canonical). If the beat tool changes, fix the canonical first, then match here.
# Sync: the Strike Trio skill lays this file down verbatim — do not change a single character (drift guard).
# Shared helper — emit the one PM beat that has no other trace: `assign`, the
# ledger that a story was handed to a track. POSTs a kind:'pm' white cube to the
# SAME ingest path the reporter uses, from the `ready` command at hand-off time.
#
#   emit_pm_beat <action> <track> <env_file> [story]
#     action: assign  (white cube, kind:'pm') — the only retained action
#     track:  this lane's track id
#     story:  Tracker Boot story id — groups the delivery bundle (required)
emit_pm_beat() {
  local action="$1" track="$2" env_file="$3" story="$4"
  local kind state
  case "$action" in
    assign) kind="pm"; state="assign" ;;
    *) return 0 ;;
  esac

  # storyless assign = no story to attribute → skip with warning, never a phantom cube.
  if [ "$action" = "assign" ] && [ -z "$story" ]; then
    echo "mitte-beat: skipping storyless assign for ${track} — no current story to attribute" >&2
    return 0
  fi

  [ -f "$env_file" ] || return 0

  local url secret
  url=$(grep -E '^MITTE_BEAT_INGEST_URL=' "$env_file" | head -1 | cut -d= -f2-)
  secret=$(grep -E '^MITTE_BEAT_INGEST_SECRET=' "$env_file" | head -1 | cut -d= -f2-)
  [ -n "$url" ] && [ -n "$secret" ] || return 0

  local ts id
  ts=$(date -u +%Y-%m-%dT%H:%M:%S.000Z)
  id="${kind}:${track}:${state}:${ts}"

  local story_field=""
  [ -n "$story" ] && story_field=",\"story\":\"${story}\""

  # Never block the PM command — but never fail SILENTLY (#200026849): retry then warn loudly.
  local attempt http_code curl_rc
  for attempt in 1 2 3; do
    http_code=$(curl -s -m 5 -o /dev/null -w '%{http_code}' -X POST "$url" \
      -H 'content-type: application/json' \
      -H "x-ingest-secret: ${secret}" \
      -d "{\"rows\":[{\"id\":\"${id}\",\"track\":\"${track}\",\"kind\":\"${kind}\",\"state\":\"${state}\",\"ts\":\"${ts}\"${story_field}}]}")
    curl_rc=$?
    if [ "$curl_rc" -eq 0 ] && [ "${http_code:-0}" -ge 200 ] && [ "${http_code:-0}" -lt 300 ]; then
      return 0
    fi
    [ "$attempt" -lt 3 ] && sleep 1
  done

  echo "⚠️  mitte-beat: failed to emit the assign white cube — ${track} #${story:-?} (curl rc=${curl_rc}, http=${http_code:-none}, 3 attempts)." >&2
  echo "   The beat may not have been ingested, so the delivery bundle may not have opened. Check the network and re-fire manually." >&2
  echo "   (The command continues — emit is not a blocker.)" >&2
  return 0
}
```
> Current-story extraction (first `#NNN` in `## Current Story`) is shared with the finished guard, so it lives **inline in the ready body** — to keep the guard from depending on the beat helper's presence. That awk is the shell-side canonical, and the e2e `storyIdFromTrack` (JS) below mirrors it.

**pr.sh** (Dev-independent — a single file taking the track as an argument. `pr-*.command` retired):
> Separating the PR author into a **bot account** is the key. If one PM account both writes and approves, GitHub blocks self-approve (422) and the merge gate (refuse without an approval) deadlocks forever. When the bot opens the PR, the PM can approve with their own account so the gate works, and the HITL structure ("AI writes, human approves") stays honest in the history.
> Creating a PR is **the Dev's (author's) act** — the PM does not stand in for it with a double-click. When the PM says "PR it" in the Dev session, the Dev runs `bash commands/pr.sh <track>`.

```bash
#!/bin/bash
# Create a PR under the bot's identity. Run in the Dev session on PM's "PR it": bash commands/pr.sh <track>
TRACK="$1"
if [ -z "$TRACK" ]; then echo "usage: pr.sh <track>"; exit 1; fi
REPO_DIR="$(cd "$(dirname "$0")/.." && pwd)"
cd "$REPO_DIR"
git worktree prune 2>/dev/null

# Load the bot token (.env is .gitignore'd). DEVBOT_TOKEN = the shared bot token (common to all projects)
[ -f .env ] && set -a && . ./.env && set +a
# Fall back to the global ~/.strike-trio/.env if the project .env has no token (internal: wired once at onboarding → no per-project re-wiring)
[ -z "$DEVBOT_TOKEN" ] && [ -f "$HOME/.strike-trio/.env" ] && set -a && . "$HOME/.strike-trio/.env" && set +a
BOT_TOKEN="${DEVBOT_TOKEN}"
if [ -z "$BOT_TOKEN" ]; then
  echo "❌ Bot token (DEVBOT_TOKEN) not found in the project .env or global ~/.strike-trio/.env. Not opening an un-botted PR —"
  echo "   if author == approver, the merge approve gate dies. Notify the PM."
  exit 1
fi

# Title = the last commit subject of dev-<track> verbatim (the commit convention ends with (#story number), so the number rides along — the anchor for batch ownership)
TITLE=$(node "$REPO_DIR/commands/pr-title.mjs" "$(git -C "$REPO_DIR" log -1 --format=%s "dev-$TRACK")")

GH_TOKEN="$BOT_TOKEN" gh pr create --base main --head "dev-$TRACK" --title "$TITLE" --body ""
```

**pr-title.mjs** (pure function — shared by tests and the shell, "does not invent a number"):
```js
// PR title = the last commit subject verbatim. Doesn't generate a number — if the commit has (#number) it's preserved, if not, so be it.
export function prTitle(commitSubject) {
  return (commitSubject || "").trim();
}
// CLI: node pr-title.mjs "<commit subject>"
if (process.argv[2] !== undefined) process.stdout.write(prTitle(process.argv[2]));
```

> **`DEVBOT_TOKEN`** is the shared bot token used by every project. Never expose the token in chat, docs, or the repo — **if exposed, revoke and reissue immediately.**

**approve-{{DEV_A_NAME}}.command** (approve-{{DEV_B_NAME}}.command has the same structure):
> Code-review **pass** gate. Since the bot is the PR author, an approval from a human account opens the merge gate. A pass has no body, so this is a **review-free double-click** (no confirmation prompt, unlike decline). Looks up exactly one PR — if zero or multiple, it stops instead of guessing.
```bash
#!/bin/bash
REPO_DIR="$(cd "$(dirname "$0")/.." && pwd)"
cd "$REPO_DIR"
PR_NUMBERS=$(gh pr list --head dev-{{DEV_A_NAME}} --state open --json number --jq '.[].number')
COUNT=$(printf '%s' "$PR_NUMBERS" | grep -c .)
if [ "$COUNT" -ne 1 ]; then
  echo "❌ dev-{{DEV_A_NAME}} does not have exactly one open PR (count=$COUNT). Stopping instead of guessing — PM, please check."
  read -n 1; exit 1
fi
gh pr review "$PR_NUMBERS" --approve
echo "✅ Approved (PR #$PR_NUMBERS) — merge with merge-{{DEV_A_NAME}}.command."
echo "Press any key to close."
read -n 1
osascript -e 'tell application "Terminal" to close front window' &
```

**decline-{{DEV_A_NAME}}.command** (decline-{{DEV_B_NAME}}.command has the same structure):
> Code-review **request-changes** gate. Claude (PM) writes the body into `.review-body.md`, and **the human makes the submit decision (HITL)** — it shows the body, then submits only after a "submit as-is? (y/n)" confirmation. (Why named `decline-*`: a code-review note is a **PR review** channel — distinct from preview feedback = story comments.)
```bash
#!/bin/bash
REPO_DIR="$(cd "$(dirname "$0")/.." && pwd)"
cd "$REPO_DIR"
BODY_FILE="$REPO_DIR/.review-body.md"
if [ ! -f "$BODY_FILE" ]; then echo "❌ .review-body.md not found. Write the review body first."; read -n 1; exit 1; fi
PR_NUMBERS=$(gh pr list --head dev-{{DEV_A_NAME}} --state open --json number --jq '.[].number')
COUNT=$(printf '%s' "$PR_NUMBERS" | grep -c .)
if [ "$COUNT" -ne 1 ]; then
  echo "❌ dev-{{DEV_A_NAME}} does not have exactly one open PR (count=$COUNT). Stopping instead of guessing — PM, please check."
  read -n 1; exit 1
fi
echo "── Review body to submit (PR #$PR_NUMBERS) ──"; cat "$BODY_FILE"; echo "────────────────────"
read -p "Submit as-is? (y/n) " CONFIRM
if [ "$CONFIRM" != "y" ]; then echo "Cancelled — nothing submitted."; read -n 1; exit 0; fi
gh pr review "$PR_NUMBERS" --request-changes --body-file "$BODY_FILE"
echo "✅ request-changes submitted (PR #$PR_NUMBERS)"
echo "Press any key to close."
read -n 1
osascript -e 'tell application "Terminal" to close front window' &
```

**ready-guard.mjs** (a single shared file — called by the ready commands. Pure core + CLI, like pr-title.mjs):
> Refuses `ready` on a finished (Finished/Delivered/Accepted) story (#290). **blocker-not-breaker**: refuses (exit 3) only when the tracker **explicitly says "finished."** Missing key, network error, any other uncertainty passes (exit 0) — a tracker outage won't block ready. Requires `TRACKER_BOOT_API_KEY` (same value as the poller) in `.env` to work (without it, the guard is silently disabled).
```js
import { realpathSync, existsSync, readFileSync } from 'node:fs'
import { fileURLToPath } from 'node:url'
import { homedir } from 'node:os'

export const FINISHED_STATUSES = ['Finished', 'Delivered', 'Accepted']
export const BLOCK_CODE = 3  // not 1, so 1/127 (crash / missing interpreter) is read as "pass"

export function shouldBlockReady(status) { return FINISHED_STATUSES.includes(status) }
export function buildStatusQuery(storyId) { return `query { story(storyId: "${storyId}") { id status } }` }
export function parseStatus(json) {
  const story = json?.data?.story
  if (!story || typeof story !== 'object') return null
  return typeof story.status === 'string' ? story.status : null
}

const GRAPHQL_URL = process.env.TRACKER_BOOT_GRAPHQL_URL || 'https://trackerboot.com/analytics/graphql'
function readEnvKey(envFile, key) {
  if (!envFile || !existsSync(envFile)) return ''
  const line = readFileSync(envFile, 'utf8').split('\n').find((l) => l.startsWith(`${key}=`))
  return line ? line.slice(key.length + 1).trim() : ''
}
async function fetchStatus(storyId, apiKey) {
  const controller = new AbortController()
  const timer = setTimeout(() => controller.abort(), 5000)
  try {
    const res = await fetch(GRAPHQL_URL, {
      method: 'POST',
      headers: { 'content-type': 'application/json', 'x-api-key': apiKey },
      body: JSON.stringify({ query: buildStatusQuery(storyId) }),
      signal: controller.signal,
    })
    if (!res.ok) return null
    return parseStatus(await res.json())
  } finally { clearTimeout(timer) }
}
const isMain = process.argv[1] && realpathSync(fileURLToPath(import.meta.url)) === realpathSync(process.argv[1])
if (isMain) {
  const [, , envFile = '', storyId = ''] = process.argv
  // Fall back to the global ~/.strike-trio/.env if the project .env has no key (supports one-time global token wiring)
  const apiKey = readEnvKey(envFile, 'TRACKER_BOOT_API_KEY')
    || readEnvKey(`${homedir()}/.strike-trio/.env`, 'TRACKER_BOOT_API_KEY')
  if (!storyId || !apiKey) process.exit(0)        // nothing to check, or no key → pass
  let status = null
  try { status = await fetchStatus(storyId, apiKey) } catch { process.exit(0) }  // network/parse failure → pass
  if (shouldBlockReady(status)) {
    process.stderr.write(`story #${storyId} is ${status} — a finished story cannot be restarted.\n`)
    process.exit(BLOCK_CODE)
  }
  process.exit(0)
}
```

**merge-{{DEV_A_NAME}}.command** (merge-{{DEV_B_NAME}}.command has the same structure):
```bash
#!/bin/bash
cd "$(dirname "$0")/.."
PR_NUMBER=$(gh pr list --head dev-{{DEV_A_NAME}} --state open --json number --jq '.[0].number')
if [ -z "$PR_NUMBER" ]; then
  echo "❌ No open PR found."
  read -n 1
  exit 1
fi

# Approve gate — refuse the merge unless the review is APPROVED. A missing review is caught immediately in the workflow.
DECISION=$(gh pr view "$PR_NUMBER" --json reviewDecision --jq '.reviewDecision')
if [ "$DECISION" != "APPROVED" ]; then
  echo "❌ PR #$PR_NUMBER review is not APPROVED (current: ${DECISION:-none})."
  echo "   After the PM passes code review, approve with approve-{{DEV_A_NAME}}.command and try again."
  read -n 1
  exit 1
fi

gh pr merge "$PR_NUMBER" --squash --delete-branch
echo ""
echo "⏳ Syncing local main... (merge goes to acceptance=main — production is promoted after Accept)"
git fetch origin

# On a successful merge, force-sync the remote dev branch to origin/main.
# The dev branch is checked out in a resident worktree, so `--delete-branch` fails every time;
# without cleanup, pre-squash old commits and rejected commits pile up stale on the remote dev branch,
# and the next ready's `reset --hard` collides with commits that "look unmerged" (but were already squash-merged).
PR_STATE=$(gh pr view "$PR_NUMBER" --json state --jq '.state')
if [ "$PR_STATE" = "MERGED" ]; then
  git push origin --force origin/main:refs/heads/dev-{{DEV_A_NAME}}
fi

git reset --hard origin/main
echo ""
echo "✅ Merge complete (PR #$PR_NUMBER) → deployed to acceptance. Production auto-publishes after Accept (GitHub Actions, within ~5 min)."
echo "Press any key to close."
read -n 1
osascript -e 'tell application "Terminal" to close front window' &
```

**promote-target.mjs** (a single shared file — called by `promote.yml` (Actions). Pure core + CLI):
> Computes the target sha that advances production only up to the **tip of the accepted leading run**. A direct port of mitte!beat's `productionTargetSha` (a verified pure function) — instead of cubes it reads merged stories from `git log`. **This isn't a standing daemon — it delegates the promote trigger from a human's manual double-click to the GitHub scheduler (cron)**; every run is idempotent, so if production is already at the tip it's a no-op. **An unaccepted story stops the run (no skipping)**, and the production push is FF-only so it never goes backward. Requires `TRACKER_BOOT_API_KEY` (shared with ready-guard) in the local `.env` **or a repo secret** to work.
```js
import { execFileSync } from 'node:child_process'
import { existsSync, readFileSync, realpathSync } from 'node:fs'
import { fileURLToPath } from 'node:url'

// production fast-forwards only up to the tip of the accepted leading run in merge order (ts). The first unaccepted stops the run —
// even a reordered Accept can't leak unaccepted code to prod. (Verbatim from mitte!beat's verified version.)
export function productionTargetSha(mergeCommits, acceptedStoryIds) {
  const accepted = new Set([...(acceptedStoryIds ?? [])].map(String))
  const ordered = (mergeCommits ?? [])
    .filter((m) => m && m.sha && m.story != null)
    .sort((a, b) => String(a.ts).localeCompare(String(b.ts)))
  let tip = null
  for (const m of ordered) {
    if (!accepted.has(String(m.story))) break // the first unaccepted story stops the run
    tip = m.sha
  }
  return tip
}
// (#number) in the squash commit subject → storyId. A commit with no number (chore/docs) is null = excluded from the run (transparent).
export function storyFromSubject(subject) { const m = (subject || '').match(/#(\d+)/); return m ? m[1] : null }
export function buildStoryQuery(ids) {
  return `query { ${ids.map((id, i) => `s${i}: story(storyId: "${id}") { id status }`).join(' ')} }`
}
export function acceptedFromResponse(json) {
  const data = json?.data
  if (!data || typeof data !== 'object') return new Set()
  return new Set(Object.values(data).filter((s) => s?.status === 'Accepted').map((s) => String(s.id)))
}
function readEnvKey(envFile, key) {
  if (!envFile || !existsSync(envFile)) return ''
  const line = readFileSync(envFile, 'utf8').split('\n').find((l) => l.startsWith(`${key}=`))
  return line ? line.slice(key.length + 1).trim() : ''
}
const GRAPHQL_URL = process.env.TRACKER_BOOT_GRAPHQL_URL || 'https://trackerboot.com/analytics/graphql'
const isMain = process.argv[1] && realpathSync(fileURLToPath(import.meta.url)) === realpathSync(process.argv[1])
if (isMain) {
  const [, , repoDir = '.', envFile = ''] = process.argv
  let log = ''
  try {
    log = execFileSync('git', ['-C', repoDir, 'log', 'origin/production..origin/main', '--pretty=format:%H|%cI|%s'], { encoding: 'utf8' })
  } catch { process.exit(0) } // no production branch, etc. → promote nothing
  const merges = log.split('\n').filter(Boolean).map((line) => {
    const [sha, ts, ...rest] = line.split('|')
    return { sha, ts, story: storyFromSubject(rest.join('|')) }
  })
  const storyIds = [...new Set(merges.map((m) => m.story).filter(Boolean))]
  if (storyIds.length === 0) process.exit(0)
  const apiKey = readEnvKey(envFile, 'TRACKER_BOOT_API_KEY') || process.env.TRACKER_BOOT_API_KEY
  if (!apiKey) process.exit(0) // no key, can't compute — blocker-not-breaker (just don't promote). local uses .env, Actions uses process.env (repo secret)
  let accepted = new Set()
  try {
    const res = await fetch(GRAPHQL_URL, {
      method: 'POST',
      headers: { 'content-type': 'application/json', 'x-api-key': apiKey },
      body: JSON.stringify({ query: buildStoryQuery(storyIds) }),
    })
    if (res.ok) accepted = acceptedFromResponse(await res.json())
    else process.exit(0)
  } catch { process.exit(0) }
  const tip = productionTargetSha(merges, accepted)
  if (tip) process.stdout.write(tip)
}
```

**`.github/workflows/promote.yml`** (Accept → auto-publish to production — created in `.github/workflows/`, NOT `commands/`):
> Accept is an event that happens in the tracker, so GitHub can't know about it — which is why **periodic polling is essential**. Every 5 min, cron runs `promote-target.mjs`, computes the accepted leading run tip from the stories merged to acceptance (main) — **only the Accepted ones, in merge order** — and advances the `production` branch **FF-only**; an unaccepted story stops the run (**serial gate built in**). Production advances via the Actions-native `GITHUB_TOKEN`, so **no bot token is needed** (the bot token is solely for separating the PR author). With `force:false`, production never goes backward. **The PM session doesn't know promote exists — just Accept, and it's reflected automatically (within ~5 min).** In a hurry, publish immediately with GitHub's `Run workflow` (workflow_dispatch) button.
```yaml
name: promote
on:
  schedule:
    - cron: '*/5 * * * *'   # Accept is a tracker event → periodic polling is essential
  push:
    branches: [main]         # secondary trigger, right after a merge
  workflow_dispatch:         # manual immediate publish (GitHub UI button when you don't want to wait)
permissions:
  contents: write            # needed to advance the production ref (GITHUB_TOKEN)
concurrency:
  group: promote
  cancel-in-progress: false
jobs:
  promote:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with: { fetch-depth: 0 }
      - uses: actions/setup-node@v4
        with: { node-version: 20 }
      - name: fetch production·main
        run: git fetch origin main production 2>/dev/null || git fetch origin main
      - name: compute accepted-run tip → FF production
        env:
          TRACKER_BOOT_API_KEY: ${{ secrets.TRACKER_BOOT_API_KEY }}
          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        run: |
          TIP=$(node commands/promote-target.mjs .)
          if [ -z "$TIP" ]; then echo "no accepted story to promote (production unchanged)"; exit 0; fi
          CUR=$(gh api "repos/$GITHUB_REPOSITORY/git/refs/heads/production" --jq .object.sha 2>/dev/null || true)
          if [ "$CUR" = "$TIP" ]; then echo "production already at $TIP — no-op"; exit 0; fi
          gh api "repos/$GITHUB_REPOSITORY/git/refs/heads/production" -X PATCH -f sha="$TIP" -F force=false \
            && echo "✅ production → $TIP advanced. The production build will start." \
            || echo "❌ promote skipped (non-FF rejection, etc.). production unchanged."
```

**push-docs.command** (Dev-independent — create just one):
```bash
#!/bin/bash
# Persist PM documents (documents/ + CLAUDE.md) to origin/main immediately.
# Dev merge/ready runs `git reset --hard origin/main`, which wipes uncommitted changes,
# so the PM double-clicks this right after editing docs to persist them to origin.
REPO_DIR="$(cd "$(dirname "$0")/.." && pwd)"
cd "$REPO_DIR"
git add -A documents/ CLAUDE.md   # documents/tracks/ and .DS_Store auto-excluded via .gitignore
if git diff --staged --quiet; then
  echo "📄 No document changes to persist."
else
  git commit -m "docs: persist PM documents ($(date '+%Y-%m-%d %H:%M'))"
  if git push origin main; then
    echo "✅ Documents persisted to origin/main. They will survive the next reset."
  else
    echo "❌ Push failed — on conflict, run 'git pull --rebase origin main' and double-click again."
  fi
fi
echo "Press any key to close."
read -n 1
osascript -e 'tell application "Terminal" to close front window' &
```

After creation, grant execute permissions:
```bash
chmod +x commands/*.command commands/*.sh
```

After creation, request confirmation with the following format:

```
✅ Command files have been created in commands/

- ready-{{DEV_A_NAME}}.command  ← double-click before starting Dev A session (ready-guard may refuse if the story is finished)
- ready-{{DEV_B_NAME}}.command  ← double-click before starting Dev B session
- pr.sh                         ← run by the Dev on "PR it" (bot-identity PR). Not a double-click
- pr-title.mjs                  ← pure title-extraction function used by pr.sh
- approve-{{DEV_A_NAME}}.command ← PM double-click when code review passes (opens the merge gate)
- approve-{{DEV_B_NAME}}.command
- decline-{{DEV_A_NAME}}.command ← PM double-click for code-review notes (shows .review-body.md, submits after y/n confirmation)
- decline-{{DEV_B_NAME}}.command
- _emit-pm-beat.sh              ← beat helper sourced by ready (no-op unless a beat tool is parked)
- ready-guard.mjs               ← finished-story guard called by ready (a single shared file)
- merge-{{DEV_A_NAME}}.command  ← includes the approve gate (merge goes to acceptance=main)
- merge-{{DEV_B_NAME}}.command
- promote-target.mjs            ← computes the accepted-run tip used by promote.yml (Actions) (pure function + CLI)
- push-docs.command  ← double-click after editing PM docs to persist them to origin

Plus `.github/workflows/promote.yml` is created — a GitHub Action that auto-publishes to production after Accept (not a double-click command).

Please confirm the files are visible in Finder, then say "done".
```

## Step 6 — Verify

Double-click `ready-<dev>.command` and confirm the Dev session opens. `documents/environment-setup.md`: mark step 6 ✅ → environment setup complete.

> **Persist right after completion (runbook #44):** commit `environment-setup.md`·`CLAUDE.md`·`delivery-playbook.md`·`iteration-plan.md` to origin (double-click `push-docs.command`). **Otherwise the uncommitted state is lost on the next session or a Dev's `git reset --hard`**, and the next `PM start` misjudges setup as "not done." The state source (`environment-setup.md`) must be **committed** to carry across sessions — you have to connect both the writer (setup) and the reader (PM start).

## After setup — Pre-IPM → IPM → Start Dev

Once environment setup is done, guide the user through Pre-IPM and IPM in order before entering development.

```
✅ [PROJECT_NAME] environment setup complete (Phase 2)

Created:
- environment-setup.md (progress ✅ + infra values)
- documents/tracks/dev-{{DEV_A_NAME}}.md, dev-{{DEV_B_NAME}}.md
- commands/ — the full set of command files + .github/workflows/promote.yml
- CLAUDE.md, delivery-playbook.md, iteration-plan.md infrastructure values filled in

Next step — Pre-IPM → IPM:
1. Pre-IPM: Move the story drafts derived from D&F into Tracker Boot's Pre-IPM and
   get Tobi's review. (Story writing follows the /story procedure — aabt-workflow.md §4)
2. Bring Tobi's review into the PM track for discussion → finalize changes → hand back to Tobi.
3. IPM: Once Pre-IPM is done, estimate each story one by one and move it to the backlog.

Once IPM is done, double-click the ready command to start development.
```

---

## (Optional) Beat Onboarding — Park a Team-Activity Visualization Tool

Strike Trio can **park** onto a tool that shows the team's development beat in real time (e.g. mitte-beat). The dormant wiring laid down in Step 5 (`_emit-pm-beat.sh` · ready's worktree `.env` injection · the `assign` emit) is a complete no-op without creds — this chapter lights it up with a single token.

> **Parking is activation, not assembly.** Assembly (the helper and ready wiring) is already done by setup. Parking just switches it on with one line; leave it off and the wiring stays harmless dead code, working fine as a generic Strike Trio scaffold. We do not ship a separate "not-connected" variant — a variant is the seed of drift, and dormancy is the separation.

### Opt-in — "Park onto a beat tool now?"

- **No → skip.** Stays dormant. Re-run this chapter any time later to drop in a token and light it up — zero re-scaffold.
- **Yes → the flow below.**

### Parking flow (mitte-beat as the example)

1. **Issue a tenant** — connect this project on the beat tool's Connect screen and obtain an `ingest_token`. (The issuance mechanism differs per tool — mitte-beat uses a Connect screen.)
2. **Light making (one line)** — from the project root:
   ```sh
   npm i -D @bekindlabs/mitte-beat-reporter && \
   npx mitte-beat-init --url <ingestUrl> --secret <ingest_token> && \
   npm test
   ```
   `mitte-beat-init` wires the reporter into the Vitest config as an instance, writes `MITTE_BEAT_INGEST_URL`·`MITTE_BEAT_INGEST_SECRET` to `.env`, and adds `.env` to `.gitignore`. Re-running is idempotent. The first red→green of `npm test` is the first making beat. The track is auto-derived from the `dev-<track>` worktree branch and the story is injected into the worktree `.env` by ready — **so there is no per-session config.**
3. **Light e2e — any runner.** The e2e broadcaster now lives inside the package (`@bekindlabs/mitte-beat-reporter/e2e`, **≥ 0.3.5**): one call, `broadcastE2e({ storyId, state, track, ingestUrl, ingestSecret, runId })`. It's **framework-agnostic** — whether `{{E2E_TOOL}}` is Playwright, Cucumber, or a bash wrapper, call this at the end of a run and a pass/fail border lights up at the bottom of the bundle. The broadcaster is **pulled from the package, not copied** (no drift); you only lay down a thin adapter for your runner. If you don't park, skip this whole step.

   > **Why it comes from the package (drift ended, #200027789)**: the contract (`broadcastE2e`/`e2eRunToRow`) lives in one place — the package — and only the per-runner adapter lives in the project, so there's no copy to drift and any runner can light a border. (The package's internal `gitTrack` helper isn't in `exports`, so don't import it — the adapter resolves track from env `MITTE_BEAT_TRACK`, which ready writes.)

   **Common — env / story-attribution glue `reporter/e2eEnv.js`** (runner-agnostic. e2e runs outside Vite, so it loads the worktree `.env` directly and reads the current story from the track file):
   ```js
   // Read the current story id from the track's "## Current Story" section — a JS mirror of ready's inline (guard) extraction.
   export function storyIdFromTrack(content) {
     if (!content) return ''
     let inSection = false
     for (const line of content.split('\n')) {
       if (/^## Current Story/.test(line)) { inSection = true; continue }
       if (inSection) { const m = line.match(/#(\d+)/); if (m) return m[1] }
     }
     return ''
   }

   export function resolveE2eEnv({ envStory, envRunId, githubRunId, trackContent, now } = {}) {
     return {
       storyId: envStory || storyIdFromTrack(trackContent),
       runId: envRunId || githubRunId || `local-${now}`,
     }
   }

   // e2e runs outside Vite, so load the worktree .env explicitly. A missing .env (fresh clone) is a no-op.
   export function loadEnvFile(path, { loader = process.loadEnvFile } = {}) {
     try { loader(path); return true } catch { return false }
   }
   ```

   **Adapter (A) Playwright — `reporter/e2eReporter.js`** (the thin layer that maps Playwright's terminal status to a state and hands the rest to the broadcaster):
   ```js
   import { broadcastE2e } from '@bekindlabs/mitte-beat-reporter/e2e'

   // Only a clean run is a pass; failure, timeout, interruption all mean the integration did not hold.
   const STATE_BY_STATUS = { passed: 'pass' }
   export function statusToState(status) { return STATE_BY_STATUS[status] ?? 'fail' }

   export default class E2eReporter {
     constructor({ ingestUrl, ingestSecret, track, storyId, runId, fetchImpl, env = process.env } = {}) {
       this.ingestUrl = ingestUrl
       this.ingestSecret = ingestSecret
       // track: option → MITTE_BEAT_TRACK (written by ready). Never fabricate a default (the old 'ponyo' default is
       // removed): an unset track is harmless because the row is keyed by cycle=storyId, never by track.
       this.track = track ?? env.MITTE_BEAT_TRACK
       this.storyId = storyId
       this.runId = runId
       this.fetch = fetchImpl ?? globalThis.fetch
       this.enabled = Boolean(ingestUrl && ingestSecret && storyId && runId)
       if (!this.enabled) {
         console.warn(
           '[mitte-beat] e2e reporter: MITTE_BEAT_INGEST_URL / MITTE_BEAT_INGEST_SECRET / MITTE_BEAT_STORY / run id missing — E2E result will not be broadcast.',
         )
       }
     }

     // Playwright terminal hook. result.status = 'passed'|'failed'|'timedout'|'interrupted'.
     async onEnd(result) {
       if (!this.enabled) return
       await broadcastE2e({
         state: statusToState(result?.status),
         track: this.track, storyId: this.storyId,
         ingestUrl: this.ingestUrl, ingestSecret: this.ingestSecret,
         runId: this.runId, fetchImpl: this.fetch,
       })
     }
   }
   ```

   **`playwright.config.js` wiring** — add the .env load / story attribution at the top, and add the adapter to the reporter array:
   ```js
   import { existsSync, readFileSync } from 'node:fs'
   import { dirname, join } from 'node:path'
   import { fileURLToPath } from 'node:url'
   import { loadEnvFile, resolveE2eEnv } from './reporter/e2eEnv.js'

   const root = dirname(fileURLToPath(import.meta.url))
   loadEnvFile(join(root, '.env'))                               // outside Vite — load worktree .env directly
   const track = process.env.MITTE_BEAT_TRACK   // ready writes this per-track; no invented default (same policy as the reporter — if unset, track is unlabelled and the row is keyed by cycle=storyId, harmless)
   const trackFile = join(root, 'documents', 'tracks', `dev-${track}.md`)
   const trackContent = existsSync(trackFile) ? readFileSync(trackFile, 'utf8') : ''
   const { storyId, runId } = resolveE2eEnv({
     envStory: process.env.MITTE_BEAT_STORY,
     envRunId: process.env.MITTE_BEAT_RUN_ID,
     githubRunId: process.env.GITHUB_RUN_ID,
     trackContent,
     now: Date.now(),
   })

   // inside defineConfig's reporter array:
   //   reporter: [
   //     ['list'],
   //     ['./reporter/e2eReporter.js', {
   //       ingestUrl: process.env.MITTE_BEAT_INGEST_URL,
   //       ingestSecret: process.env.MITTE_BEAT_INGEST_SECRET,
   //       track, storyId, runId,
   //     }],
   //   ],
   ```

   **Adapter (B) Cucumber — `tests/e2e/support/hooks.js`** (a border lights up even without Playwright — the very point where tool choice stops being punished):
   ```js
   import { After, AfterAll } from '@cucumber/cucumber'
   import { existsSync, readFileSync } from 'node:fs'
   import { dirname, join } from 'node:path'
   import { fileURLToPath } from 'node:url'
   import { broadcastE2e } from '@bekindlabs/mitte-beat-reporter/e2e'
   import { loadEnvFile, resolveE2eEnv } from '../../../reporter/e2eEnv.js'

   // Only a clean run is a pass — if any scenario is not PASSED, the whole run is a fail.
   let anyFailed = false
   After(function (scenario) { if (scenario.result?.status !== 'PASSED') anyFailed = true })

   AfterAll(async function () {
     const root = join(dirname(fileURLToPath(import.meta.url)), '..', '..', '..')
     loadEnvFile(join(root, '.env'))
     const track = process.env.MITTE_BEAT_TRACK   // ready writes this per-track; no invented default (same policy as the reporter — if unset, track is unlabelled and the row is keyed by cycle=storyId, harmless)
     const trackFile = join(root, 'documents', 'tracks', `dev-${track}.md`)
     const trackContent = existsSync(trackFile) ? readFileSync(trackFile, 'utf8') : ''
     const { storyId, runId } = resolveE2eEnv({
       envStory: process.env.MITTE_BEAT_STORY,
       githubRunId: process.env.GITHUB_RUN_ID,
       trackContent,
       now: Date.now(),
     })
     await broadcastE2e({
       state: anyFailed ? 'fail' : 'pass',
       track, storyId, runId,
       ingestUrl: process.env.MITTE_BEAT_INGEST_URL,
       ingestSecret: process.env.MITTE_BEAT_INGEST_SECRET,
     })
   })
   ```

   **(C) Any other runner** — whatever the runner, resolve `{ storyId, state, track, runId, creds }` the same way in its end-of-run hook and call `broadcastE2e`. There is one contract, and the tool is the tenant's choice.

### Verification = anti-drift contract + demonstration (show, don't tell)

Right after parking, double-click ready and confirm with your **eyes** — a demonstration, not a lecture (the beat-tool version of "you learn by writing"):

1. Double-click ready → on screen, the **assign white cube appears → the bundle opens** (= "assignment opens the bundle").
2. Run the tests once in the Dev session → a **making cube lives inside that bundle** (= "development is the beat").
3. (If e2e is on) `npm run test:e2e` → a pass/fail border at the bottom of the bundle.

If a cube doesn't appear, stop there and check the token, `ingestUrl`, and `.env` — never silent. This verification is itself the anti-drift contract.

---

## Notes

- If a file already exists, ask for confirmation before overwriting.
- Follow the folder structure above exactly. Do not add or remove files arbitrarily.
- Do not modify the trigger keywords in CLAUDE.md. They are standard.
- Copy aabt-workflow.md without changes.
- Tracker Boot Project ID is required in Phase 2. If missing, guide the user to create a project first.
- Don't ask about infrastructure or Dev info in Phase 1. Collect it in Phase 2.
- **Keep the project folder out of cloud sync (iCloud, Dropbox, etc.).** A repo inside a synced folder can get conflict copies in `.git/refs/...` (broken refs like `dev-ponyo 2`) that block fetch. Mention this when guiding the folder location at the start of Phase 1.

---

## Release Notes

### v1.3 — 2026-07-02 · Environment setup, redesigned for beginners & non-developers

Environment setup (Phase 2) is much smoother. Once accounts and tools are in place, Claude walks you through this project's infrastructure **one step at a time** — so non-developers aren't overwhelmed.

- **A progress checklist** — `environment-setup.md` is the single source of truth for state. Even if a session is interrupted, it resumes with "here's where we are, here's what's next" (mirror of the D&F status briefing).
- **Only human account actions are hand-held** — Claude creates the GitHub repo for you via `gh`, and the bot invite/accept is automatic. You only do your own account actions like web login and clicks. It's "human + Claude," not "human alone."
- **The bot is one question: "Bekind team?"** — shared-bot invite → auto-accept (zero human wait) + a one-time global token (no per-project re-wiring). `pr.sh`/`ready-guard` fall back to the global `~/.strike-trio/.env` when the project `.env` has none.
- **Infrastructure split out of Tracker Boot Chores** — product stories and environment chores are no longer mixed.
- **Cleanly separated from onboarding** — installing accounts, tools, and the skill is onboarding (once per person·machine); environment setup is per project. One onboarding prompt takes even a non-developer, with their own Claude, all the way to their first project.

### v1.2 — 2026-07-01 · Park a team-activity visualization tool (optional) + e2e tool freedom

You can **park** a project onto a tool that shows your development beat in real time (e.g. mitte-beat). Setup lays the wiring down ahead of time (a harmless no-op until you park), and a single token lights it up — the `assign` white cube opens the beat bundle at the start of dev, and making and e2e beats accumulate inside it.

- **One-line parking** — get a token on the Connect screen and light making with a single `npx mitte-beat-init`. Safe to re-run.
- **e2e with any tool** — the e2e border is no longer tied to Playwright. The broadcaster now lives in the package (no copy, no drift), so Playwright, Cucumber, or any other runner lights the border with a single call at the end of a run. Whether to use Gherkin glue (Cucumber) is demoted to a **tenant trade-off** the methodology does not prescribe — tool choice is never punished with a blank screen.
- **Show, don't tell** — right after parking, hit ready and watch the cubes appear; that demonstration is the verification.
- A project that doesn't park keeps working as a generic Strike Trio scaffold, and the finished-story guard runs independently of the beat wiring.

### v1.1 — 2026-06-25 · Auto-publish to production

Accept now publishes to production automatically. Previously you had to double-click `promote.command` after Accept; now GitHub Actions (`promote.yml`) polls the tracker and reflects Accepted stories onto production by itself (within ~5 min). Nothing to double-click, nothing for the PM session to remember — **Accept is the publish**. The source of truth is the tracker's Accepted state, and it self-heals on the next poll even if one is missed.

- Production advances via the Actions-native token — **no bot token needed**. Just register one repo secret, `TRACKER_BOOT_API_KEY`.
- In a hurry, publish immediately with GitHub's `Run workflow` button.

### v1.0 — 2026-06-25 · *First public release*

The first public release of Strike Trio. One PM and two AI Dev sessions work as a team, taking an empty folder from D&F (Discovery & Framing) through development, code review, and deployment in one flow.

Highlights:
- **Full D&F → dev → deploy workflow** (TDD · code review · PM Accept gates)
- **Bot-authored PRs + approve/decline gate** — "AI writes, human approves" stays in history. One shared team bot, reused across all projects.
- **Single-site Netlify topology** — main = acceptance, `production` branch = production, published via `promote`.
- **Onboarding** — persona builder installed up front + a start guide.

> Pre-release internal history (through build v2.8) is preserved in git history and the feedback log. **Public 1.0 = internal build v2.8.**
