Skip to content

feat: add campaign task dispatch#266

Open
mielyemitchell wants to merge 9 commits into
kunchenguid:mainfrom
mielyemitchell:fm/fm-factory-intake
Open

feat: add campaign task dispatch#266
mielyemitchell wants to merge 9 commits into
kunchenguid:mainfrom
mielyemitchell:fm/fm-factory-intake

Conversation

@mielyemitchell

Copy link
Copy Markdown

Intent

Add campaign task support to firstmate: a third dispatch shape alongside ship and scout for one long-lived crewmate driving a committed roadmap/spec. Include --campaign brief scaffolding, kind=campaign spawn metadata, ship-like teardown protection, campaign-aware validation state, docs, and tests without changing existing ship/scout behavior. Campaign crewmates must never merge: PR-based campaigns stop at PR ready/checks green so firstmate can merge through bin/fm-pr-merge.sh, including yolo repos; local-only campaigns are supported and use the fixed fm/ task branch, stopping at ready in branch for firstmate's local merge path. Existing no-mistakes review/test fixes through 3c92fe1 are intentional and should be validated; the prior run reached document before a captain-approved daemon restart cancelled it, then a retry hit a Claude session limit.

What Changed

  • Introduces campaign as a third dispatch shape alongside ship and scout: bin/fm-brief.sh --campaign scaffolds a long-lived roadmap-driven crewmate brief, and bin/fm-spawn.sh --campaign records kind=campaign metadata, including model/effort/harness dispatch-profile support consistent with ship/scout launches.
  • Gives campaigns ship-like teardown protection in bin/fm-teardown.sh: every fm/<id>-prefixed local branch (the fixed branch for local-only projects, per-batch branches for PR-based projects) must be verified as landed and is pruned at teardown, with each campaign batch branch checked against its own PR rather than the task's last-recorded PR.
  • Makes campaign crewmates never merge on their own: PR-based campaigns stop at PR-ready/checks-green for firstmate to merge via bin/fm-pr-merge.sh (including yolo repos), and local-only campaigns stop at "ready in branch" on the fixed fm/<id> branch for firstmate's local merge path; bin/fm-crew-state.sh and bin/fm-send.sh gain campaign-aware state handling.
  • Updates AGENTS.md, README.md, CONTRIBUTING.md, and docs/ (architecture, configuration, scripts, herdr/orca/zellij backends, turnend-guard) to document the campaign lifecycle end to end, alongside harness-adapters and secondmate-provisioning skill updates.
  • Adds/updates tests covering campaign brief scaffolding, spawn dispatch profiles, teardown branch protection, and crew-state handling, and fixes a treehouse worktree-discovery race plus test env isolation (hermetic FM_HOME in the turnend-guard test, isolated gotmp env) uncovered while validating this branch.

Risk Assessment

✅ Low: The campaign feature is additive (new --campaign kind alongside ship/scout), the multi-branch teardown protection and per-branch PR verification logic is subtle but thoroughly covered by new targeted tests (unpushed refusal, unlanded batch branch refusal even with HEAD parked elsewhere, all-batches-landed pruning, per-branch PR lookup, local-only detached-HEAD refusal), and existing ship/scout/secondmate code paths are left behavior-unchanged (kind defaults and gating conditions were extended, not altered). No security, data-loss, or breaking-change risks found.

Testing

The configured baseline test command passed, and I independently reran the full 44-file suite (0 failures) plus the four campaign-specific suites in isolation; I additionally exercised bin/fm-brief.sh --campaign directly to produce a real generated brief showing the roadmap contract, fm/<id>/<batch> branch naming, "never merge" rule, and mode-specific stop conditions (PR ready/checks green vs. ready in branch) — end-to-end evidence that campaign scaffolding, spawn metadata, and teardown protection work as intended without regressing ship/scout behavior. Working tree is clean; no stray artifacts left behind.

Evidence: Campaign-specific test suite transcript (fm-brief, fm-spawn-dispatch-profile, fm-teardown, fm-crew-state)
== tests/fm-brief.test.sh ==
ok - fm-brief.sh: bash -n succeeds
ok - fm-brief.sh: no-mistakes/direct-PR/local-only briefs generate cleanly
ok - fm-brief.sh: no-mistakes DOD wording avoids the apostrophe regression
ok - fm-brief.sh: --campaign generates the roadmap-mode campaign contract
ok - fm-brief.sh: local-only campaigns stop ready in branch
== tests/fm-spawn-dispatch-profile.test.sh ==
ok - no --model/--effort records defaults and keeps the claude launch byte-identical
ok - active crew-dispatch profile requires an explicit harness for ship spawns
ok - active crew-dispatch profile requires an explicit harness for scout spawns
ok - active crew-dispatch profile requires an explicit harness for campaign spawns
ok - campaign spawn records kind=campaign while preserving normal crewmate metadata
ok - active crew-dispatch profile allows an explicit resolved harness
ok - active crew-dispatch profile allows the legacy positional harness form
ok - active crew-dispatch profile allows the raw launch-command escape hatch
ok - claude receives --model and --effort profile flags
ok - codex receives --model and model_reasoning_effort profile flags
ok - codex omits unsupported max effort instead of passing a bad config value
ok - grok receives --model and --reasoning-effort profile flags
ok - grok omits unsupported max reasoning effort
ok - opencode receives --model and omits the unsupported effort axis
ok - pi threads model and omits unsupported max effort
ok - batch dispatch forwards shared --harness, --model, and --effort to every pair
ok - active crew-dispatch profile does not block secondmate launches
# all fm-spawn-dispatch-profile tests passed
== tests/fm-teardown.test.sh ==
ok - local-only worktree with HEAD on a fork remote is torn down (fix holds)
ok - teardown prompts tasks-axi backlog refresh when compatible
ok - teardown honors config/backlog-backend=manual even when tasks-axi is compatible
ok - local-only worktree with truly unpushed work is refused (safety preserved)
ok - local-only worktree with work merged into local main is torn down (no regression)
ok - no-mistakes worktree with HEAD on origin is torn down (no regression)
ok - no-mistakes worktree with genuinely unlanded work is refused (safety preserved)
ok - campaign worktree with genuinely unlanded work is refused like ship work
ok - campaign teardown refuses an unlanded batch branch even with HEAD parked elsewhere
ok - campaign teardown with all batches landed proceeds and prunes every fm/<id>/* ref
ok - campaign teardown verifies each batch branch against its own merged PR (per-branch lookup)
ok - local-only campaign teardown refuses the unmerged fixed branch with HEAD detached
ok - local-only worktree with unpushed work is torn down under --force (escape hatch)
ok - squash-merged + deleted-branch worktree (PR merged) is torn down (the fix)
ok - squash-merged PR accepts a local HEAD that is an ancestor of the final PR head
ok - teardown discovers a merged PR by branch name and tears down when no pr= was ever recorded
ok - squash-merged PR accepts replayed unpushed local patches contained in the PR head
ok - merged PR does not allow teardown after a later local commit
ok - fm-pr-check does not refresh PR head after HEAD moves
ok - fm-pr-check records the remote PR head when the local worktree lags
ok - worktree whose content already landed in the default branch is torn down (content fallback)
ok - content fallback refreshes origin default before comparing trees
ok - dirty worktree is refused even when its committed work has landed (dirty always wins)
ok - gh lookup error with content not in default refuses (fail-safe)
== tests/fm-crew-state.test.sh ==
ok - active run-step is authoritative
ok - stale needs-decision over active run is superseded
ok - stale blocked over active run is superseded
ok - genuine parked run is not flagged superseded
ok - scalar gate parked run is not flagged superseded
ok - gate block parked run is not flagged superseded
ok - ci-ready status log beats monitoring run
ok - terminal passed run is authoritative
ok - terminal failed run is authoritative
ok - cross-branch run is attributed via the real runs list
ok - cross-branch attribution picks the branch's most recent row
ok - another branch's run is ignored, falls back
ok - no run + busy pane reads working from the pane
ok - herdr unknown native state falls back to backend capture busy regex
ok - herdr idle agent_status is corroborated by the pane text, not trusted outright
ok - herdr idle agent_status with a genuinely idle pane stays not-busy (no regression for a human-blocked agent)
ok - no run + idle pane uses the status-log verb
ok - dead window ignores stale status log
ok - closed pane still reports a terminal run-step
ok - closed pane still reports an active run-step
ok - no timeout command uses perl bound
ok - scout skips the run lookup
ok - campaign uses the ship-like no-mistakes run lookup
ok - torn-down worktree is handled gracefully
ok - missing meta is handled gracefully
ok - crew_is_provably_working absorbs a validating crew found only via the runs-list fallback
ok - crew_is_provably_working still surfaces a genuinely stopped crew (safety property preserved)
ok - usage error exits 2
all fm-crew-state tests passed
Evidence: Real generated campaign crewmate brief (bin/fm-brief.sh --campaign output)
You are a campaign crewmate: an autonomous long-lived worker agent managed by firstmate. Work on your own; do not wait for a human.

# Task
{TASK}

# Campaign contract
You work in ONE persistent worktree for the whole roadmap. Do not ask for teardown, respawn, or a fresh worktree between slices or batch PRs.
Teardown happens only after the roadmap is closed and firstmate confirms the final campaign work has landed.

The execution artifact is already finished before you start: either a committed roadmap at `docs/plans/<feature>.md` with `- [ ]` slices and optional `[gate]` / `[risk:high]` markers, or a single upstream-produced spec.
Planning is out of scope. If the artifact is missing, unfinished, or asks you to plan the feature, append `blocked: missing finished campaign artifact` to the status file and stop.

Project delivery mode: ``
Project yolo flag: ``

# Setup
You are in a persistent, isolated git worktree of demo-repo, at a detached HEAD on a clean default-branch commit. It stays yours for the whole roadmap; firstmate tears it down only after the campaign closes.

**Verify isolation before anything else.** Run `pwd -P` and `git rev-parse --show-toplevel`; both must resolve to the isolated task worktree you were launched in, such as a treehouse pool path or an Orca-managed worktree, not the primary checkout firstmate operates from.
The path check is authoritative: `git rev-parse --git-dir` and `git rev-parse --git-common-dir` can help inspect the repo, but they do not prove you are outside the primary checkout.
If the top-level path is the primary checkout or not the worktree you were launched in, STOP - do not branch or commit here - append `blocked: launched in primary checkout, not an isolated worktree` to the status file and stop.

1. Record the launch commit: `git rev-parse HEAD`. Treat this commit as the roadmap base.
2. Do not attempt to check out the default branch. The pooled clone holds it, so this worktree starts detached by design. Create the first slice or batch branch directly from the launch commit with `git switch -c fm/demo-campaign-x1/<batch-slug>`; when the execution skill preflight expects a named default branch, the current detached base commit stands in for it. Name EVERY campaign branch under the `fm/demo-campaign-x1/` prefix - firstmate's teardown protection and ref cleanup track that prefix, and a branch outside it can strand work.

# Execution loop
Drive the captain-provided execution skill in roadmap mode. Invoke `/autopilot`; if the harness needs plain language instead, ask it to run the captain-provided execution skill against the committed roadmap/spec in roadmap mode.

This campaign contract overrides any normal landing behavior inside the execution skill. The execution skill may help open or update the batch PR, but if it would run `land-pr`, call `bin/fm-pr-merge.sh`, call `gh-axi pr merge`, merge directly, or treat `yolo=` as permission for you to merge, do not do that step. Stop before landing at `done: PR {url} checks green` so firstmate can run `bin/fm-pr-merge.sh` after the captain's word or firstmate's yolo decision.

Use this loop:
1. Pick the next unchecked slice from the roadmap/spec.
2. Implement only that slice.
3. Verify the slice.
4. Review the slice.
5. Commit it with a message containing `[S<N>]`, where `<N>` is the slice number.
6. Open or update the batch PR when the batch is ready.
7. Run roadmap-tick after the batch PR state changes as required by the execution artifact.
8. After firstmate reports a batch PR merged, start the next batch branch from the updated default branch, not the launch commit: `git fetch origin` then `git switch -c fm/demo-campaign-x1/<next-batch-slug> origin/<default-branch>`, so each batch PR contains only its own batch's diff.

For `no-mistakes` projects, keep the execution skill's normal inner verify/review loop per slice, then run the no-mistakes pipeline as the final batch-PR gate before reporting the PR ready. The no-mistakes evidence trail is part of the fleet contract.

# Escalation mapping
Every stop raised by the execution skill maps to the status file:

- `[gate]` slice: append `needs-decision: [gate] <exact stop reason + options>`, then wait.
- `[risk:high]` slice: append `needs-decision: [risk:high] <exact stop reason + options>`, then wait.
- Off-spec or off-blueprint UI stop: append `needs-decision: off-spec UI <exact stop reason + options>`, then wait.
- Structured clarification question: append `needs-decision: clarification <exact question + options>`, then wait.
- Visual approval: append `needs-decision: visual approval <artifact/link + options>`, then wait.

Never push past a stop. Firstmate relays the decision to the captain and replies with the answer.

# Rules
1. Never push to the default branch. Never merge a PR.
2. Stay inside this worktree; modify nothing outside it.
3. Use gh-axi for GitHub operations and chrome-devtools-axi for browser operations.
4. Report status by appending one line:
   `echo "{state}: {one short line}" >> '/var/folders/l1/t2yfcls14bs7vfn5yjyc66wr0000gn/T/tmp.qWtOsCbsYN/state/demo-campaign-x1.status'`
   States: working, needs-decision, blocked, done, failed.
   Each append wakes firstmate, so report sparingly: setup complete, each stop that needs a decision, batch ready, roadmap close, blocked, or failed.
5. If you hit the same obstacle twice, append `blocked: {why}` and stop; firstmate will help.
6. If a decision belongs to a human, append `needs-decision: {exact stop reason + options}` and stop. Firstmate will reply with the decision.

# Merge authority
Stop at "PR ready, checks green" for each batch PR and append `done: PR {url} checks green`. Wait for firstmate to relay the captain's merge word or, when `yolo=` is `on`, firstmate's yolo merge decision and continuation instruction. Firstmate performs every PR merge through `bin/fm-pr-merge.sh` so task metadata records the landed PR. Never merge a red PR.

# Project memory
If `AGENTS.md` or `CLAUDE.md` already exists, or if this campaign produced durable project-intrinsic knowledge, run `/var/folders/l1/t2yfcls14bs7vfn5yjyc66wr0000gn/T/tmp.qWtOsCbsYN/bin/fm-ensure-agents-md.sh .` in the worktree.
If this campaign produced durable project-intrinsic knowledge, record it in `AGENTS.md` as part of your change.

# Definition of done
The campaign is complete only when the roadmap/spec is fully closed, every batch has passed its required gates, and the final work has landed or is waiting at the mode-specific merge stop above.
On final roadmap close, append `done: campaign complete {PR url, branch, or summary}` to the status file and stop. Firstmate tears down this worktree only after it confirms the campaign work has landed.

Pipeline

Updates from git push no-mistakes

✅ **intent** - passed

✅ No issues found.

✅ **Rebase** - passed

✅ No issues found.

⚠️ **Review** - 1 info
  • ℹ️ bin/fm-spawn.sh:829 - The worktree-discovery poll now latches a non-isolated cwd as final after only 2 consecutive identical polls (~2s), rather than the full 60s budget, to detect a genuinely stuck/tangled spawn quickly. If treehouse's intermediate staging cwd (e.g. ~/.treehouse) happens to stay put for 2+ seconds before moving to the final leaf worktree on a slower machine, this could produce a premature 'did not yield an isolated worktree' refusal instead of waiting it out. This is a judgment call the author already made deliberately (per the inline comment) to fix a real race; flagging only as a possible flakiness source under system load, not a functional bug.
✅ **Test** - passed

✅ No issues found.

  • command -v tmux >/dev/null || { echo "tmux is required for e2e tests" >&2; exit 1; }; tmux -V; rc=0; for t in tests/*.test.sh; do echo "== $t =="; bash "$t" || rc=1; done; exit "$rc"
  • command -v tmux && tmux -V (baseline env check)
  • bash tests/fm-brief.test.sh
  • bash tests/fm-spawn-dispatch-profile.test.sh
  • bash tests/fm-teardown.test.sh
  • bash tests/fm-crew-state.test.sh
  • Full suite: for t in tests/*.test.sh; do bash "$t"; done (44 files, all pass, FULL_SUITE_RC=0)
  • Manual: bin/fm-brief.sh demo-campaign-x1 demo-repo --campaign in an isolated temp FM_HOME to render the actual generated campaign brief contract
✅ **Document** - passed

✅ No issues found.

✅ **Lint** - passed

✅ No issues found.

✅ **Push** - passed

✅ No issues found.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant