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
53 changes: 51 additions & 2 deletions bin/fm-brief.sh
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,12 @@
# without it carry a loud declaration so an omitted contract cannot be silent.
# For ship tasks, --mode is REQUIRED and shapes the definition of done. Firstmate
# resolves it per task at intake (AGENTS.md section 7); data/projects.md holds the
# captain's standing posture as context, and this script never reads it:
# captain's standing posture as context plus the optional `jj` opt-in token that
# selects jj bookmarks for jj-managed projects when the jj/jjhouse tooling is
# installed (a jj-managed project without that tooling refuses to scaffold: its
# AGENTS.md forbids raw git writes, so no branch command this host could emit
# would be runnable and the token must never mandate one); the delivery mode
# itself is never read from it here - the explicit --mode is authoritative:
# no-mistakes implement -> /no-mistakes pipeline -> PR -> configured merge authority
# direct-PR implement -> push + open PR via gh-axi (no pipeline) -> configured merge authority
# local-only implement on branch, stop and report "ready in branch" (no push/PR);
Expand Down Expand Up @@ -410,6 +415,50 @@ esac
# briefs stay byte-identical to the historical Bash 5 output.
DOD=${DOD%$'\n'}

# jj-managed projects (registry token `jj` after the mode bracket) get a jj
# branch step instead of `git checkout -b`: their AGENTS.md forbids raw git
# write commands and jj is their version control. The token is part of the
# registry line format documented in bin/fm-project-mode.sh, so firstmate
# records it once per project instead of hand-patching every brief.
#
# The token takes effect only when the tooling that makes the step executable
# is present on this host (the same host the crew worktrees live on): `jj`
# runs the bookmark command and `jjhouse` provisions the jj workspace pool.
# A jj-managed project without that tooling cannot be worked on from this host
# at all: `jj bookmark create` would fail and the git fallback would violate
# the project's raw-git-write ban, so the scaffold refuses instead of emitting
# a mandatory first action the worker cannot run.
JJ_MANAGED=0
if [ -f "$DATA/projects.md" ]; then
# The token is read only from the documented registry line format: the mode
# bracket must open at field 3 right after the project name. Legacy rows
# without a bracket are never scanned, so bracket-like description text
# cannot be mistaken for an opt-in.
JJ_TOKEN=$(awk -v n="$REPO" '
$1=="-" && $2==n {
if ($3 ~ /^\[/) {
for (i=3; i<=NF; i++)
if ($i ~ /\]$/) {
if ($(i+1)=="jj") print "jj"
break
}
}
exit
}
' "$DATA/projects.md")
[ "$JJ_TOKEN" = jj ] && JJ_MANAGED=1
fi
if [ "$JJ_MANAGED" = 1 ]; then
if command -v jj >/dev/null 2>&1 && command -v jjhouse >/dev/null 2>&1; then

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Gate checks the wrong environment

If firstmate's PATH contains jj and jjhouse but the long-lived worker daemon's PATH lacks jj, this check emits jj bookmark create even though the worker pane cannot execute it, causing the mandatory first action to fail before task work begins.

BRANCH_STEP="1. First action: create your branch. This repo is jj-managed (its AGENTS.md forbids raw git write commands): run \`jj bookmark create fm/$ID\`."
else
echo "error: $REPO is registered jj-managed in data/projects.md but jj/jjhouse is not on PATH; a jj-managed project forbids raw git writes, so there is no branch step this host can run - provision jjhouse here or remove the jj token, then re-scaffold" >&2
exit 1
fi
else
BRANCH_STEP="1. First action: create your branch: \`git checkout -b fm/$ID\`"
fi

cat > "$BRIEF" <<EOF
You are a crewmate: an autonomous worker agent managed by firstmate. Work on your own; do not wait for a human.

Expand All @@ -425,7 +474,7 @@ You are in a disposable git worktree of $REPO, at a detached HEAD on a clean def
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. First action: create your branch: \`git checkout -b fm/$ID\`$SETUP2
$BRANCH_STEP$SETUP2

# Rules
$RULE1
Expand Down
9 changes: 9 additions & 0 deletions bin/fm-project-mode.sh
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
# - <name> - <desc> (added <date>) -> no-mistakes off (legacy default)
# - <name> [<mode>] - <desc> (added <date>) -> <mode> off
# - <name> [<mode> +yolo] - <desc> (added <date>) -> <mode> on
# - <name> [<mode>] jj - <desc> (added <date>) -> <mode> off (jj-managed)
#
# Registered modes:
# no-mistakes full pipeline -> PR -> configured merge authority (default)
Expand All @@ -28,6 +29,14 @@
# project as the remote-backed pipeline project it is.
# yolo (orthogonal) = merge authority only: when on, firstmate merges green,
# in-scope work itself (AGENTS.md section 7).
# jj (orthogonal) = the project's crew worktrees are jj-managed: bin/fm-brief.sh
# scaffolds `jj bookmark create` instead of `git checkout -b` because the
# project's AGENTS.md forbids raw git write commands. The token implies
# captain-side jjhouse provisioning and is ignored by this parser, so the
# emitted mode/yolo are unaffected. fm-brief.sh honours the token only when
# the jj/jjhouse tooling is on PATH, else it refuses the scaffold: a jj-managed
# project without that tooling has no runnable branch step (its AGENTS.md
# forbids the raw-git fallback), so no brief is emitted.
#
# --raw prints the registered annotation unmapped, so a caller that must tell a
# conditional policy apart from a flat mode sees "no-mistakes-prod-only" itself.
Expand Down
2 changes: 1 addition & 1 deletion docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -248,7 +248,7 @@ The `data/secondmates.md` line contract is owned by the [`secondmate-provisionin
Each task's mode and `yolo` merge posture are firstmate's decision at intake.
The mode is passed explicitly to `bin/fm-brief.sh`, and both values are passed explicitly to `bin/fm-spawn.sh` and `bin/fm-promote.sh`; each command refuses to guess the values it consumes.
A ship brief records its mode as a fixed machine-readable line and the spawn refuses to launch on a different one, so the worker's instructions and the recorded task delivery cannot diverge.
`data/projects.md` records each project's standing posture and optional `+yolo` merge flag as the captain's default and as context for that decision, including the conditional `no-mistakes-prod-only` policy; a ship spawn that drops below the registered rigor prints a deviation notice and continues.
`data/projects.md` records each project's standing posture and optional `+yolo` merge flag as the captain's default and as context for that decision, including the conditional `no-mistakes-prod-only` policy; the optional `jj` token after the mode bracket opts a jj-managed project into a `jj bookmark create` scaffold step instead of `git checkout -b` when the jj/jjhouse tooling is present on the host (the token alone never emits a mandatory command the worker cannot run: a jj-managed project without that tooling refuses to scaffold, since its AGENTS.md forbids the raw-git fallback), implying captain-side jjhouse provisioning (registry line format is owned by `bin/fm-project-mode.sh`). A ship spawn that drops below the registered rigor prints a deviation notice and continues.
`bin/fm-project-mode.sh` remains the one registry parser for the mechanical consumers that have no task in hand: fleet sync's `local-only` skip and home seeding's refusal and no-mistakes initialization.
When a selected delivery path calls for a diff, `bin/fm-review-diff.sh` refreshes the authoritative base and, when task meta records `pr=`, always fetches and compares against `refs/pull/<n>/head` by default (recorded `pr_head=` is only an offline fallback) before falling back to the local branch with a warning.
Where a no-mistakes pipeline stores evidence in the repo, it publishes that PR-viewable validation evidence to an orphan evidence branch that shares no history with code branches, so it never enters the crew branch or the default branch.
Expand Down
144 changes: 139 additions & 5 deletions tests/fm-brief.test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -177,9 +177,11 @@ test_help_includes_entire_header() {
pass "fm-brief.sh: --help renders the complete header"
}

# Registry with one project per delivery mode. fm-brief.sh no longer reads it -
# the ship mode arrives as an explicit flag - so this fixture exists to prove the
# scaffold ignores the registered posture (test_ship_mode_is_explicit_not_registry).
# Registry with one project per delivery mode. The ship mode arrives as an
# explicit flag, so this fixture exists to prove the scaffold follows the
# explicit --mode regardless of the registered posture
# (test_ship_mode_is_explicit_not_registry). The registry is consulted only for
# the optional `jj` token that selects the jj branch step, never for the mode.
write_registry() {
local home=$1
mkdir -p "$home/data"
Expand Down Expand Up @@ -246,7 +248,8 @@ ROWS

# The registry is the captain's standing posture, not this task's answer: the
# scaffold must follow the explicit flag even when the project is registered
# with a different mode, and must not consult the registry at all.
# with a different mode; the registry is consulted only for the optional `jj`
# token that switches the branch step, never for the delivery mode.
test_ship_mode_is_explicit_not_registry() {
local home brief
home="$TMP_ROOT/explicit-over-registry-home"
Expand All @@ -259,14 +262,142 @@ test_ship_mode_is_explicit_not_registry() {
assert_grep "Firstmate will then instruct you to run /no-mistakes" "$brief" \
"explicit no-mistakes brief did not render the pipeline definition of done"

# An unregistered project is not a blocker either, because nothing is looked up.
# An unregistered project is not a blocker either: a missing row simply
# means the default git branch step.
FM_HOME="$home" "$ROOT/bin/fm-brief.sh" brief-explicit-a6 never-registered --mode local-only >/dev/null 2>&1 \
|| fail "unregistered project should still scaffold from the explicit mode"
grep -qx "Delivery contract: mode=local-only" "$home/data/brief-explicit-a6/brief.md" \
|| fail "unregistered project did not honour the explicit --mode"
pass "fm-brief.sh: the explicit ship mode wins over the registered posture"
}

# A `jj` token after the mode bracket opts the scaffold into the jj bookmark
# branch step; its absence keeps the git branch step. The token is honoured
# through the FM_DATA_OVERRIDE seam like the rest of the script, and only when
# the jj/jjhouse tooling is present. Tooling presence is simulated with a
# minimal PATH so the tests are deterministic whether or not the host (or CI
# runner) has jujutsu installed.
make_tooling_bin() {
local dir=$1; shift
mkdir -p "$dir"
local tool real
for tool in env bash awk cat mkdir dirname basename pwd sed grep tail tr; do
real=$(command -v "$tool") || continue
ln -sf "$real" "$dir/$tool"
done
local fake
for fake in "$@"; do
printf '%s\n' '#!/bin/sh' 'exit 0' > "$dir/$fake"
chmod +x "$dir/$fake"
done
}

test_jj_token_selects_jj_branch_step() {
local home data brief toolbin
home="$TMP_ROOT/jj-token-home"
data="$TMP_ROOT/jj-token-data"
toolbin="$TMP_ROOT/jj-toolbin"
make_tooling_bin "$toolbin" jj jjhouse
mkdir -p "$data"
cat > "$data/projects.md" <<'EOF'
- jj-proj [no-mistakes] jj - fixture for jj-managed (added 2026-07-01)
- git-proj [no-mistakes] - fixture for git-managed (added 2026-07-01)
EOF

FM_HOME="$home" FM_DATA_OVERRIDE="$data" PATH="$toolbin" \
"$ROOT/bin/fm-brief.sh" brief-jj-a9 jj-proj --mode no-mistakes >/dev/null 2>&1 \
|| fail "jj-token brief should scaffold"
brief="$data/brief-jj-a9/brief.md"
assert_grep 'jj bookmark create fm/brief-jj-a9' "$brief" \
"jj-token project brief did not scaffold the jj bookmark step"
assert_no_grep 'git checkout -b fm/brief-jj-a9' "$brief" \
"jj-token project brief still scaffolds the git branch step"

FM_HOME="$home" FM_DATA_OVERRIDE="$data" PATH="$toolbin" \
"$ROOT/bin/fm-brief.sh" brief-git-b9 git-proj --mode no-mistakes >/dev/null 2>&1 \
|| fail "git-managed brief should scaffold"
brief="$data/brief-git-b9/brief.md"
assert_grep 'git checkout -b fm/brief-git-b9' "$brief" \
"project without the jj token did not keep the git branch step"
assert_no_grep 'jj bookmark create fm/brief-git-b9' "$brief" \
"project without the jj token got the jj bookmark step"

# A relocated data dir (FM_DATA_OVERRIDE) is where the token is read from;
# a home whose default-location registry has no jj token must not flip it.
mkdir -p "$home/data"
cat > "$home/data/projects.md" <<'EOF'
- jj-proj [no-mistakes] - no jj token in the default-location registry (added 2026-07-01)
EOF
FM_HOME="$home" FM_DATA_OVERRIDE="$data" PATH="$toolbin" \
"$ROOT/bin/fm-brief.sh" brief-jj-c9 jj-proj --mode no-mistakes >/dev/null 2>&1 \
|| fail "jj-token brief with relocated data should scaffold"
brief="$data/brief-jj-c9/brief.md"
assert_grep 'jj bookmark create fm/brief-jj-c9' "$brief" \
"FM_DATA_OVERRIDE relocated registry token was ignored"
pass "fm-brief.sh: jj registry token selects the jj branch step through FM_DATA_OVERRIDE"
}

# The jj token never emits a mandatory command the worker cannot run: a
# jj-managed project's AGENTS.md forbids raw git writes, so when the token is
# present but the jj/jjhouse tooling is absent from PATH there is no branch
# step this host can run and the scaffold refuses outright (exit 1, no brief)
# instead of degrading to a forbidden git command.
test_jj_token_without_tooling_refuses_scaffold() {
local home data out rc
home="$TMP_ROOT/jj-no-tooling-home"
data="$TMP_ROOT/jj-no-tooling-data"
make_tooling_bin "$TMP_ROOT/jj-no-toolbin"
mkdir -p "$data"
cat > "$data/projects.md" <<'EOF'
- jj-proj [no-mistakes] jj - fixture for jj-managed (added 2026-07-01)
EOF
out=$(FM_HOME="$home" FM_DATA_OVERRIDE="$data" PATH="$TMP_ROOT/jj-no-toolbin" \
"$ROOT/bin/fm-brief.sh" brief-nool-d1 jj-proj --mode no-mistakes 2>&1); rc=$?
expect_code 1 "$rc" "jj token without jj tooling must refuse the scaffold (got $rc)"
assert_contains "$out" "jj-proj is registered jj-managed" \
"jj token without jj tooling must name the misconfigured project"
assert_contains "$out" "jjhouse" \
"jj token without jj tooling must point at the missing tooling"
assert_absent "$data/brief-nool-d1/brief.md" \
"jj token without jj tooling must not scaffold a brief with an unrunnable first action"
pass "fm-brief.sh: jj token without jj/jjhouse tooling refuses the scaffold instead of a git fallback"
}

# The jj scan reads only the documented registry line format: the mode bracket
# must open at field 3 right after the project name. Legacy rows without a
# bracket, and bracket-like description text after the mode bracket, must never
# be misread as an opt-in even when the jj tooling is installed.
test_jj_scan_ignores_legacy_description_brackets() {
local home data brief toolbin
home="$TMP_ROOT/jj-legacy-home"
data="$TMP_ROOT/jj-legacy-data"
toolbin="$TMP_ROOT/jj-legacy-toolbin"
make_tooling_bin "$toolbin" jj jjhouse
mkdir -p "$data"
cat > "$data/projects.md" <<'EOF'
- legacy-proj - uses [custom] jj workflows in the description (added 2026-07-01)
- bracketed-proj [no-mistakes] - fixture with [note] jj text (added 2026-07-01)
EOF
FM_HOME="$home" FM_DATA_OVERRIDE="$data" PATH="$toolbin" \
"$ROOT/bin/fm-brief.sh" brief-legacy-e1 legacy-proj --mode no-mistakes >/dev/null 2>&1 \
|| fail "legacy-row brief should scaffold"
brief="$data/brief-legacy-e1/brief.md"
assert_grep 'git checkout -b fm/brief-legacy-e1' "$brief" \
"legacy row without a mode bracket was misread as jj-managed"
assert_no_grep 'jj bookmark create fm/brief-legacy-e1' "$brief" \
"legacy description bracket text flipped the branch step to jj"

FM_HOME="$home" FM_DATA_OVERRIDE="$data" PATH="$toolbin" \
"$ROOT/bin/fm-brief.sh" brief-bracket-e2 bracketed-proj --mode no-mistakes >/dev/null 2>&1 \
|| fail "bracketed-description brief should scaffold"
brief="$data/brief-bracket-e2/brief.md"
assert_grep 'git checkout -b fm/brief-bracket-e2' "$brief" \
"description text after the mode bracket flipped the branch step to jj"
assert_no_grep 'jj bookmark create fm/brief-bracket-e2' "$brief" \
"description bracket text after the mode bracket was misread as a jj token"
pass "fm-brief.sh: jj scan reads only the documented registry line format"
}

# yolo is firstmate's merge authority and never reaches the worker, and a scout
# or charter carries no delivery contract. Each must refuse rather than accept and
# discard the flag, which would look recorded but change nothing.
Expand Down Expand Up @@ -718,6 +849,9 @@ test_help_includes_entire_header
test_ship_modes_generate_clean_briefs
test_ship_mode_is_required_and_closed_set
test_ship_mode_is_explicit_not_registry
test_jj_token_selects_jj_branch_step
test_jj_token_without_tooling_refuses_scaffold
test_jj_scan_ignores_legacy_description_brackets
test_delivery_flags_are_refused_where_they_do_not_apply
test_faster_paths_use_configured_authority_without_stacked_review
test_no_mistakes_dod_wording
Expand Down