Skip to content
Open
Show file tree
Hide file tree
Changes from 3 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
50 changes: 48 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 (without it the scaffold warns and falls back to the git branch step,
# so the token never emits a mandatory command the worker cannot run); 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,47 @@ 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 project marked jj-managed before that tooling is installed falls back to
# the git branch step with a loud warning - never a mandatory command the
# worker cannot run, and never a silent misconfiguration.
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 ] && command -v jj >/dev/null 2>&1 && command -v jjhouse >/dev/null 2>&1; then
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
if [ "$JJ_MANAGED" = 1 ]; then
echo "warn: $REPO is registered jj-managed but jj/jjhouse is not on PATH; falling back to the git branch step (provision jjhouse to activate the jj path)" >&2
fi
BRANCH_STEP="1. First action: create your branch: \`git checkout -b fm/$ID\`"
Comment thread
greptile-apps[bot] marked this conversation as resolved.
Outdated
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 +471,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
8 changes: 8 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,13 @@
# 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 warns and falls back to the git
# branch step.
#
# --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: without the tooling the brief falls back to the git step with a warning), 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: when the
# token is present but the jj/jjhouse tooling is absent from PATH, the scaffold
# falls back to the git branch step and warns loudly instead of scaffolding a
# first action that would fail on launch.
test_jj_token_without_tooling_falls_back_to_git() {
local home data brief out
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)
brief="$data/brief-nool-d1/brief.md"
assert_present "$brief" "brief should still scaffold without jj tooling"
assert_grep 'git checkout -b fm/brief-nool-d1' "$brief" \
"jj token without jj tooling must fall back to the git branch step"
assert_no_grep 'jj bookmark create fm/brief-nool-d1' "$brief" \
"jj token without jj tooling still scaffolded the mandatory jj command"
assert_contains "$out" "warn: jj-proj is registered jj-managed but jj/jjhouse is not on PATH" \
"jj token without jj tooling must warn loudly instead of silently degrading"
pass "fm-brief.sh: jj token without jj/jjhouse tooling falls back to git with a warning"
}

# 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_falls_back_to_git
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