Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 24 additions & 8 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,16 +72,16 @@ bin/ helper scripts, committed; read each script's header before
config/crew-harness crewmate harness override; LOCAL, gitignored; absent or "default" = same as firstmate
data/ personal fleet records; LOCAL, gitignored as a whole
backlog.md task queue, dependencies, history
captain.md captain's curated personal preferences and working style; LOCAL, gitignored, and canonical even if harness memory mirrors it
projects.md thin fleet navigation registry; firstmate-private, parsed by fm-project-mode.sh (section 6)
secondmates.md secondmate routing table; firstmate-private, maintained by fm-home-seed.sh (section 6)
captain.md captain's curated personal preferences and working style - approval posture, communication style, release habits; LOCAL, gitignored; compact rewrite-and-prune counterpart to shared AGENTS.md; canonical harness-portable home, even if harness memory mirrors it as a recall cache
projects.md thin fleet navigation registry: one line per project under projects/ with name, delivery mode, optional "+yolo", optional "+nm-gate", and a one-line description. It is firstmate-private, not a project knowledge dump; fm-project-mode.sh parses it (section 6)
secondmates.md secondmate routing table: one line per persistent domain supervisor, with a natural-language scope, non-exclusive project clone list, and home path; fm-home-seed.sh maintains it and validates unique ids, unique homes, and non-overlapping home paths (section 6)
<id>/brief.md per-task crewmate brief, or per-secondmate charter brief when kind=secondmate
<id>/report.md scout task deliverable, written by the crewmate; survives teardown
projects/ cloned repos; gitignored; READ-ONLY for you
state/ volatile runtime signals; gitignored
<id>.status appended by crewmates: "<state>: <note>" lines
<id>.turn-ended touched by turn-end hooks
<id>.meta written by fm-spawn: window=, worktree=, project=, harness=, kind=, mode=, yolo=; kind=secondmate also records home= and projects= (fm-pr-check appends pr=)
<id>.meta written by fm-spawn: window=, worktree=, project=, harness=, kind=, mode=, yolo=, nm_gate=, nm_status=; kind=secondmate also records home= and projects= (fm-pr-check upserts pr= and pr_source=)
<id>.check.sh optional slow poll you write per task (e.g. merged-PR check)
.wake-queue durable queued wakes: epoch<TAB>seq<TAB>kind<TAB>key<TAB>payload
.afk durable away-mode flag; present = sub-supervisor may inject escalations (set by /afk, cleared on user return)
Expand Down Expand Up @@ -180,7 +180,7 @@ Every project in the fleet has one line:
- <name> [<mode>] - <one-line description> (added <date>)
```

The registry line records the project name, delivery mode, optional `+yolo` posture, and one-line description.
The registry line records the project name, delivery mode, optional `+yolo` posture, optional `+nm-gate` delivery gate, and one-line description.
Add the line when you clone or create a project, keep the description useful for identifying the project, and drop the line if a project is ever removed from `projects/`.
Do not turn the registry into a knowledge dump.
Durable descriptive detail belongs in the project's own `AGENTS.md`.
Expand Down Expand Up @@ -236,10 +236,19 @@ Do not eagerly backfill every project.

- `no-mistakes` (default; `[...]` may be omitted) - full pipeline -> PR -> captain merge. Highest assurance.
- `direct-PR` - push + open a PR via `gh-axi`, no pipeline -> captain merge.
- `direct-PR +nm-gate` - direct-PR remains the base mode, but Firstmate may run no-mistakes as a post-scope ship gate after a worker stops.
- `local-only` - local branch, no remote, no PR; firstmate reviews the diff, the captain approves, firstmate merges to local `main` (section 7).

Orthogonal to mode is an optional `+yolo` flag (`[direct-PR +yolo]`), default off and **not recommended**: with `yolo` on, firstmate makes the approval decisions itself instead of asking the captain (section 7). When the captain adds a project without saying, default to `no-mistakes` with yolo off; only set a faster mode or `+yolo` on the captain's explicit say-so.

Also orthogonal is optional `+nm-gate`, currently intended for `direct-PR` projects that have no-mistakes available but should not use it as the base mode.
It never applies to scout/report tasks.
Workers must not self-run no-mistakes for `+nm-gate`.
They stop at `done: ready for Firstmate PR scope review`.
Firstmate reviews git status, intended PR scope, and generated operational data before the captain decides whether to run `git push no-mistakes <branch>`, `no-mistakes axi run`, or normal direct PR flow.
Generated operational data requires explicit captain approval before no-mistakes or PR scope.
Passing no-mistakes does not auto-chain into merge, teardown, service restart, or another task.

**Clone existing:** `git clone <url> projects/<name>`, add its registry line with the chosen mode, then initialize only if the mode is `no-mistakes`.

**Create new:** for `no-mistakes` and `direct-PR` modes a new project needs a GitHub repo first (they push to an `origin` remote); a `local-only` project needs no remote at all - a purely local git repo is fine.
Expand Down Expand Up @@ -289,7 +298,7 @@ When you create a new secondmate, hand its in-scope queued items off from the ma

Then classify the shape:

- **Ship** (the default): the deliverable is a change to the project. It ships through the project's delivery mode: `no-mistakes`, `direct-PR`, or `local-only`.
- **Ship** (the default): the deliverable is a change to the project. It ships through the project's delivery mode: `no-mistakes`, `direct-PR`, optional `direct-PR +nm-gate`, or `local-only`.
- **Scout:** the deliverable is knowledge - an investigation, a plan, a bug reproduction, an audit. It ends in a report at `data/<id>/report.md`, never a PR. When the captain asks "what's wrong", "how would we", or "find out why" about a project, that is a scout task; dispatch it instead of doing the digging yourself.

Then classify readiness:
Expand Down Expand Up @@ -318,7 +327,7 @@ bin/fm-spawn.sh <id1>=projects/<repo1> <id2>=projects/<repo2> [--scout] # batc
Dispatch several tasks in one call by passing `id=repo` pairs instead of a single `<id> <project>`; each pair is spawned through the same single-task path, a shared `--scout` applies to all, and the looping happens inside the script so you never hand-write a multi-task shell loop.
If one pair fails, the rest still run and the batch exits non-zero.

The script resolves the harness (`fm-harness.sh crew`), owns the verified launch templates, resolves the project's delivery mode (`fm-project-mode.sh`) for ship/scout tasks, and records `harness=`, `kind=`, `mode=`, and `yolo=` in the task's meta; a non-flag third argument containing whitespace is treated as a raw launch command (only for verifying new adapters).
The script resolves the harness (`fm-harness.sh crew`), owns the verified launch templates, resolves the project's delivery mode (`fm-project-mode.sh`) for ship/scout tasks, and records `harness=`, `kind=`, `mode=`, `yolo=`, `nm_gate=`, and `nm_status=` in the task's meta; a non-flag third argument containing whitespace is treated as a raw launch command (only for verifying new adapters).
For `kind=secondmate`, the same script launches in the registered or explicit firstmate home instead of running `treehouse get` for a project, records `home=` and `projects=`, and uses the charter brief as the launch prompt.

For ship and scout tasks, the script creates the window (in your current tmux session, or a dedicated `firstmate` session when you are outside tmux), runs `treehouse get`, waits for the worktree subshell, asserts the resolved worktree is a genuine isolated worktree distinct from the primary checkout (aborting the spawn otherwise, to prevent the worktree tangle of section 8), installs the turn-end hook, records `state/<id>.meta`, and launches the agent with the brief.
Expand All @@ -340,6 +349,7 @@ A ship task's path from `done` to landed on `main` is set by the project's `mode

- **no-mistakes** - the stages below as written: no-mistakes validation pipeline -> PR -> captain merge.
- **direct-PR** - no pipeline. The crewmate pushes and opens the PR itself (its brief says so) and reports `done: PR <url>`. Skip the Validate step and go straight to PR ready (run `fm-pr-check`, relay the PR). Teardown uses the normal pushed-branch check.
- **direct-PR +nm-gate** - no worker-run pipeline. The crewmate prepares a clean intended diff, runs targeted checks, reports recommended PR scope, and stops at `done: ready for Firstmate PR scope review`. Firstmate reviews status and scope, asks the captain before including generated operational data, then the captain decides whether to run the no-mistakes gate or use normal direct PR flow.
- **local-only** - no remote, no PR. The crewmate stops at `done: ready in branch fm/<id>`. Review the diff with `bin/fm-review-diff.sh <id>`, relay a one-paragraph summary to the captain, and on approval run `bin/fm-merge-local.sh <id>` to fast-forward local `main` (it refuses anything but a clean fast-forward - if it does, have the crewmate rebase). No `fm-pr-check`. Then teardown, whose safety check requires the branch already merged into local `main`, OR the work pushed to any remote (a fork counts - relevant for upstream-contribution PRs on a local-only-registered project).

When reviewing any crewmate branch diff, use `bin/fm-review-diff.sh <id>` rather than `git diff <default>...branch` directly.
Expand All @@ -357,10 +367,16 @@ The no-mistakes pipeline fixes auto-fix findings on its own (inside its own work
When it reports `needs-decision` (ask-user findings), relay the findings to the captain unless `yolo=on` permits routine approval on your judgment, then send the decision back as a short instruction (the crewmate responds via `no-mistakes axi respond`).
Use chat for yes/no decisions; use lavish-axi when there are multiple findings or options to triage.

For `direct-PR +nm-gate`, do not send `/no-mistakes` or `$no-mistakes` to the worker.
Firstmate owns the gate decision after scope review.
Before the first real gate run in a worktree, inspect current no-mistakes help/status there.
Do not run the gate while the branch contains accidental broad generated churn.

### PR ready

For PR-based ship tasks, the ready signal depends on mode: `no-mistakes` reports `done: PR <url> checks green` after CI is green, while `direct-PR` reports `done: PR <url>` after opening the PR.
Run `bin/fm-pr-check.sh <id> <PR url>` - it records `pr=` in the task's meta and arms the watcher's merge poll.
Run `bin/fm-pr-check.sh <id> <PR url> [direct|no-mistakes] [nm-status]` - it records `pr=` and `pr_source=` in the task's meta and arms the watcher's merge poll.
Use `nm-status=passed` only when the no-mistakes gate actually passed or produced the PR; otherwise use the conservative default `pr_recorded`.
Tell the captain: the PR's full URL (always the complete `https://...` link, never a bare `#number` - the captain's terminal makes a full URL clickable), a one-paragraph summary, and, for `no-mistakes`, the risk level it emitted.
(The check contract, for any custom `state/<id>.check.sh` you write yourself: print one line only when firstmate should wake, print nothing otherwise, and finish before `FM_CHECK_TIMEOUT`.)

Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ This is.. a directory that turns any agent into your firstmate, and you the capt
- **A visible crew** - every crewmate works in its own tmux window you can watch or type into; the first mate reconciles.
- **Disposable worktrees** - each task runs in a clean [treehouse](https://github.com/kunchenguid/treehouse) git worktree, so parallel work on one repo never collides.
- **Two task shapes** - ship tasks deliver a change; scout tasks investigate, plan, reproduce, or audit and leave a report.
- **Explicit project modes** - each project ships via `no-mistakes`, `direct-PR`, or `local-only`, with an optional `+yolo` autonomy flag.
- **Explicit project modes** - each project ships via `no-mistakes`, `direct-PR`, or `local-only`, with optional `+yolo` and `+nm-gate` flags.
- **Optional secondmates** - opt in to persistent domain supervisors that run from isolated firstmate homes with their own `FM_HOME`, state, projects, and session lock.
- **Event-driven, zero-token supervision** - a bash watcher sleeps on the fleet and wakes the first mate only when something needs you.
- **Guarded by construction** - the first mate is read-only over your projects outside clean default-branch refreshes, safe branch pruning, and approved `local-only` fast-forward merges; crewmates make every project change behind your merge approval.
Expand Down
24 changes: 20 additions & 4 deletions bin/fm-brief.sh
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@
# (data/projects.md via fm-project-mode.sh; see AGENTS.md project management
# and task lifecycle):
# no-mistakes implement -> /no-mistakes pipeline -> PR -> captain merge (default)
# direct-PR implement -> push + open PR via gh-axi (no pipeline) -> captain merge
# direct-PR implement -> push + open PR via gh-axi (no pipeline) -> captain merge,
# unless +nm-gate makes Firstmate own post-scope validation
# local-only implement on branch, stop and report "ready in branch" (no push/PR);
# firstmate reviews, captain approves, firstmate merges to local main
# Ship briefs begin with a worktree-isolation assertion before the branch step.
Expand Down Expand Up @@ -154,22 +155,37 @@ fi

# Ship task: shape Setup / Rule 1 / Definition of done by the project's delivery mode.
# yolo does not affect the brief (it governs firstmate's approval behaviour), so discard it.
read -r MODE _ <<EOF
read -r MODE _ NM_GATE <<EOF
$("$FM_ROOT/bin/fm-project-mode.sh" "$REPO")
EOF
: "${NM_GATE:=off}"

case "$MODE" in
direct-PR)
SETUP2=""
RULE1='1. Never push to the default branch (push only your `fm/'"$ID"'` branch). Never merge a PR.'
DOD=$(cat <<EOF
if [ "$NM_GATE" = on ]; then
RULE1='1. Never push to any remote, never open a PR, never run no-mistakes, and never merge. Work only on your `fm/'"$ID"'` branch until Firstmate reviews scope.'
DOD=$(cat <<EOF
# Definition of done
This project ships **direct-PR +nm-gate**: no-mistakes is available only as a Firstmate-controlled delivery gate after PR scope review.
The task is complete when the intended diff is ready for Firstmate PR scope review. Commit only if the task explicitly authorizes commits.
Before stopping, keep the diff narrow, run targeted checks, and report the recommended PR scope.
When the intended diff is ready, append \`done: ready for Firstmate PR scope review\` to the status file and stop.
Do NOT run /no-mistakes. Do NOT push. Do NOT open a PR.
Firstmate and the captain decide whether to run \`git push no-mistakes <branch>\`, \`no-mistakes axi run\`, or normal direct PR flow.
EOF
)
else
RULE1='1. Never push to the default branch (push only your `fm/'"$ID"'` branch). Never merge a PR.'
DOD=$(cat <<EOF
# Definition of done
This project ships **direct-PR**: you raise the PR yourself, without the no-mistakes pipeline.
The task is complete only when committed on your branch.
When it is implemented and committed, push your branch and open a PR with \`gh-axi\`, then append \`done: PR {url}\` to the status file and stop.
Do NOT run /no-mistakes. The captain reviews and merges the PR; firstmate relays it.
EOF
)
fi
;;
local-only)
SETUP2=""
Expand Down
6 changes: 4 additions & 2 deletions bin/fm-fleet-sync.sh
Original file line number Diff line number Diff line change
Expand Up @@ -100,8 +100,10 @@ sync_project() {
echo "$label: skipped: not a git repo"
return 0
fi
mode_line=$("$FM_ROOT/bin/fm-project-mode.sh" "$label" 2>/dev/null || echo "no-mistakes off")
mode=${mode_line%% *}
mode_line=$("$FM_ROOT/bin/fm-project-mode.sh" "$label" 2>/dev/null || echo "no-mistakes off off")
read -r mode _ _nm_gate <<EOF
$mode_line
EOF
if [ "$mode" = "local-only" ]; then
echo "$label: skipped: local-only project"
return 0
Expand Down
6 changes: 3 additions & 3 deletions bin/fm-home-seed.sh
Original file line number Diff line number Diff line change
Expand Up @@ -534,7 +534,7 @@ clone_project() {
dst=$(validate_project_destination "$home" "$project") || return 1
[ -d "$src" ] || { echo "error: project $project not found at $src" >&2; return 1; }
git -C "$src" rev-parse --is-inside-work-tree >/dev/null 2>&1 || { echo "error: project $project is not a git repo" >&2; return 1; }
read -r mode _ <<EOF
read -r mode _ _nm_gate <<EOF
$(FM_HOME="$FM_HOME" FM_DATA_OVERRIDE="$DATA" "$FM_ROOT/bin/fm-project-mode.sh" "$project")
EOF
if [ "$mode" = local-only ]; then
Expand All @@ -561,7 +561,7 @@ validate_seed_project() {
src="$PROJECTS/$project"
[ -d "$src" ] || { echo "error: project $project not found at $src" >&2; return 1; }
git -C "$src" rev-parse --is-inside-work-tree >/dev/null 2>&1 || { echo "error: project $project is not a git repo" >&2; return 1; }
read -r mode _ <<EOF
read -r mode _ _nm_gate <<EOF
$(FM_HOME="$FM_HOME" FM_DATA_OVERRIDE="$DATA" "$FM_ROOT/bin/fm-project-mode.sh" "$project")
EOF
if [ "$mode" = local-only ]; then
Expand Down Expand Up @@ -728,7 +728,7 @@ registry_line_for_project() {

project_mode_in_home() {
local home=$1 project=$2 mode
read -r mode _ <<EOF
read -r mode _ _nm_gate <<EOF
$(FM_ROOT_OVERRIDE='' FM_STATE_OVERRIDE='' FM_DATA_OVERRIDE='' FM_PROJECTS_OVERRIDE='' FM_CONFIG_OVERRIDE='' FM_HOME="$home" "$FM_ROOT/bin/fm-project-mode.sh" "$project")
EOF
printf '%s\n' "$mode"
Expand Down
53 changes: 49 additions & 4 deletions bin/fm-pr-check.sh
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
#!/usr/bin/env bash
# Record a PR-ready task: appends pr=<url> to state/<id>.meta and arms the
# Record a PR-ready task: upserts pr=<url> to state/<id>.meta and arms the
# watcher's merge poll by writing state/<id>.check.sh, which prints one line iff
# the PR is merged (the watcher's check contract: output = wake firstmate,
# silence = keep sleeping).
# Usage: fm-pr-check.sh <task-id> <pr-url>
# Usage: fm-pr-check.sh <task-id> <pr-url> [direct|no-mistakes] [nm-status]
# nm-status is optional and may be pr_recorded|passed|failed|skipped.
# For no-mistakes PRs, the default is pr_recorded; pass "passed" only when the
# gate actually passed or produced the PR.
set -eu

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
Expand All @@ -13,12 +16,54 @@ STATE="${FM_STATE_OVERRIDE:-$FM_HOME/state}"
"$FM_ROOT/bin/fm-guard.sh" || true
ID=$1
URL=$2
SOURCE=${3:-direct}
NM_STATUS=${4:-}
case "$SOURCE" in
direct|no-mistakes) ;;
*) echo "error: pr source must be direct or no-mistakes" >&2; exit 1 ;;
esac
case "$NM_STATUS" in
''|pr_recorded|passed|failed|skipped) ;;
*) echo "error: nm-status must be pr_recorded, passed, failed, or skipped" >&2; exit 1 ;;
esac

META="$STATE/$ID.meta"
if [ -f "$META" ] && ! grep -qxF "pr=$URL" "$META"; then
echo "pr=$URL" >> "$META"
meta_upsert() {
local key=$1 value=$2 tmp
[ -f "$META" ] || return 0
tmp=$(mktemp "$META.tmp.XXXXXX")
awk -v key="$key" -v value="$value" -F= '
$1 == key {
if (!seen) {
print key "=" value
seen = 1
}
next
}
{ print }
END {
if (!seen) print key "=" value
}
' "$META" > "$tmp"
mv "$tmp" "$META"
}

meta_has() {
local key=$1 value=$2
[ -f "$META" ] || return 1
grep -qxF "$key=$value" "$META"
}

if [ "$SOURCE" = no-mistakes ] && [ -z "$NM_STATUS" ]; then
NM_STATUS=pr_recorded
elif [ "$SOURCE" = direct ] && [ -z "$NM_STATUS" ] && meta_has nm_gate on; then
NM_STATUS=skipped
fi

meta_upsert pr "$URL"
meta_upsert pr_source "$SOURCE"
[ -n "$NM_STATUS" ] && meta_upsert nm_status "$NM_STATUS"

cat > "$STATE/$ID.check.sh" <<EOF
state=\$(gh pr view "$URL" --json state -q .state 2>/dev/null)
[ "\$state" = "MERGED" ] && echo "merged"
Expand Down
Loading