From 025505b1f82161826f16312493f7c815a9863be9 Mon Sep 17 00:00:00 2001 From: Varun Suchanti Date: Fri, 10 Jul 2026 13:47:17 +0530 Subject: [PATCH 01/12] fix(brief): add crewmate stop caps --- bin/fm-brief.sh | 43 ++++++++++++++++++++++++++++++---- tests/fm-brief.test.sh | 53 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 92 insertions(+), 4 deletions(-) diff --git a/bin/fm-brief.sh b/bin/fm-brief.sh index 56f3008d3..2d15d373c 100755 --- a/bin/fm-brief.sh +++ b/bin/fm-brief.sh @@ -6,7 +6,7 @@ # description, acceptance criteria, and context, and may adjust other sections # when the task genuinely deviates (e.g. working an existing external PR instead # of shipping a new one). -# Usage: fm-brief.sh [--scout] [--herdr-lab] +# Usage: fm-brief.sh [--scout] [--herdr-lab] [--budget ''] # fm-brief.sh --secondmate {...|--no-projects} # --scout writes the scout contract instead: the deliverable is a report at # data//report.md (no branch, no push, no PR) and the worktree is scratch. @@ -26,6 +26,8 @@ # The flag must be explicit because {TASK} is filled after scaffolding and the # caller-supplied repo string cannot reliably identify this repo. Briefs made # without it carry a loud declaration so an omitted contract cannot be silent. +# --budget '' adds a task-specific hard stop to a crewmate ship or scout +# brief's standard stop conditions. # For ship tasks, the definition of done is shaped by the project's delivery mode # (data/projects.md via fm-project-mode.sh; see the project-management skill # and AGENTS.md task lifecycle): @@ -73,15 +75,25 @@ STATE="${FM_STATE_OVERRIDE:-$FM_HOME/state}" KIND=ship HERDR_LAB=0 NO_PROJECTS=0 +BUDGET="" POS=() -for a in "$@"; do - case "$a" in +while [ "$#" -gt 0 ]; do + case "$1" in --scout) KIND=scout ;; --secondmate) KIND=secondmate ;; --herdr-lab) HERDR_LAB=1 ;; --no-projects) NO_PROJECTS=1 ;; - *) POS+=("$a") ;; + --budget) + if [ "$#" -lt 2 ] || [ -z "$2" ]; then + echo "error: --budget requires non-empty text" >&2 + exit 1 + fi + BUDGET=$2 + shift + ;; + *) POS+=("$1") ;; esac + shift done ID=${POS[0]} @@ -90,6 +102,11 @@ if [ "$KIND" = secondmate ] && [ "$HERDR_LAB" -eq 1 ]; then exit 1 fi +if [ "$KIND" = secondmate ] && [ -n "$BUDGET" ]; then + echo "error: --budget applies only to crewmate ship or scout briefs" >&2 + exit 1 +fi + if [ "$NO_PROJECTS" -eq 1 ] && [ "$KIND" != secondmate ]; then echo "error: --no-projects applies only to --secondmate charters" >&2 exit 1 @@ -189,6 +206,20 @@ fi REPO=${POS[1]} +STOP_CONDITIONS=$(cat <<'EOF' +## Stop conditions +Keep working while the task makes meaningful progress. +Expected waiting on a validation run or long test suite does not count as a no-phase-change stretch. +If roughly two hours or 50 significant actions pass without a supervisor-actionable phase change, append `blocked: budget - {one-line progress + what remains}` and stop. +If the work is clearly larger than this brief, append `blocked: scope - {one-line progress + newly discovered work}` and stop before silently expanding scope. +EOF +) +if [ -n "$BUDGET" ]; then + STOP_CONDITIONS="${STOP_CONDITIONS} +Task-specific budget: $BUDGET +Treat this as a hard cap, and when it is reached append \`blocked: budget - {one-line progress + what remains}\` and stop." +fi + if [ "$HERDR_LAB" -eq 1 ]; then HERDR_LAB_HELPER=$(shell_quote "$FM_ROOT/bin/fm-herdr-lab.sh") # shellcheck disable=SC2016 # single quotes are deliberate: these lines are literal brief text whose backtick-wrapped $(...) and "$HERDR_LAB_SESSION" snippets must reach the reading agent verbatim, not expand at scaffold time; only the '"$VAR"' break-outs interpolate. @@ -258,6 +289,8 @@ The report is the only thing that survives, so anything worth keeping must be in every lane/home, so restarting it kills other lanes' in-flight pipeline runs. On ANY no-mistakes daemon error, append \`blocked: {the daemon error}\` and stop; only firstmate manages the daemon. +$STOP_CONDITIONS + # Definition of done Write your findings to \`$DATA/$ID/report.md\`. The report must stand alone: what you did, what you found, the evidence (commands run, output, file:line references), and what you recommend. @@ -366,6 +399,8 @@ $RULE1 every lane/home, so restarting it kills other lanes' in-flight pipeline runs. On ANY no-mistakes daemon error, append \`blocked: {the daemon error}\` and stop; only firstmate manages the daemon. +$STOP_CONDITIONS + # Project memory If \`AGENTS.md\` or \`CLAUDE.md\` already exists, or if this task produced durable project-intrinsic knowledge, run \`$FM_ROOT/bin/fm-ensure-agents-md.sh .\` in the worktree. Record only project knowledge useful to almost every future session. diff --git a/tests/fm-brief.test.sh b/tests/fm-brief.test.sh index 645e451c2..302fe40f6 100755 --- a/tests/fm-brief.test.sh +++ b/tests/fm-brief.test.sh @@ -134,6 +134,57 @@ test_ship_project_memory_wording() { pass "fm-brief.sh: ship project-memory wording carries the AGENTS.md authoring bar" } +test_stop_conditions_default_for_ship_and_scout() { + local home id brief + home="$TMP_ROOT/stop-conditions-default-home" + mkdir -p "$home/data" + + for kind in ship scout; do + id="brief-stop-default-$kind" + if [ "$kind" = scout ]; then + FM_HOME="$home" "$ROOT/bin/fm-brief.sh" "$id" firstmate --scout >/dev/null 2>&1 + else + FM_HOME="$home" "$ROOT/bin/fm-brief.sh" "$id" firstmate >/dev/null 2>&1 + fi + brief="$home/data/$id/brief.md" + assert_grep "# Rules" "$brief" "$kind brief lost the Rules section" + assert_grep "## Stop conditions" "$brief" "$kind brief missing the stop-conditions block" + assert_grep "Expected waiting on a validation run or long test suite does not count as a no-phase-change stretch." "$brief" \ + "$kind brief treats healthy validation waiting as budget exhaustion" + assert_grep "If roughly two hours or 50 significant actions pass without a supervisor-actionable phase change" "$brief" \ + "$kind brief missing the default turn and time cap" + assert_grep "blocked: budget - {one-line progress + what remains}" "$brief" \ + "$kind brief missing the budget exhaustion status protocol" + assert_grep "blocked: scope - {one-line progress + newly discovered work}" "$brief" \ + "$kind brief missing the scope-expansion stop condition" + assert_grep "# Definition of done" "$brief" "$kind brief lost the Definition of done section" + assert_no_grep "Task-specific budget:" "$brief" "$kind brief rendered a budget override without --budget" + done + pass "fm-brief.sh: ship and scout briefs include safe default stop conditions" +} + +test_stop_conditions_budget_override_for_ship_and_scout() { + local home id brief budget + home="$TMP_ROOT/stop-conditions-budget-home" + budget="hard stop at 30 minutes" + mkdir -p "$home/data" + + for kind in ship scout; do + id="brief-stop-budget-$kind" + if [ "$kind" = scout ]; then + FM_HOME="$home" "$ROOT/bin/fm-brief.sh" "$id" firstmate --scout --budget "$budget" >/dev/null 2>&1 + else + FM_HOME="$home" "$ROOT/bin/fm-brief.sh" "$id" firstmate --budget "$budget" >/dev/null 2>&1 + fi + brief="$home/data/$id/brief.md" + assert_grep "Task-specific budget: $budget" "$brief" "$kind brief omitted its --budget override" + assert_grep "Treat this as a hard cap" "$brief" "$kind brief did not make --budget a stop condition" + assert_grep "blocked: budget - {one-line progress + what remains}" "$brief" \ + "$kind brief lost the budget exhaustion status protocol" + done + pass "fm-brief.sh: --budget renders in ship and scout stop conditions" +} + test_herdr_lab_contract_is_explicit_and_complete() { local home id brief home="$TMP_ROOT/herdr-lab-home" @@ -345,6 +396,8 @@ test_ship_modes_generate_clean_briefs test_faster_paths_use_configured_authority_without_stacked_review test_no_mistakes_dod_wording test_ship_project_memory_wording +test_stop_conditions_default_for_ship_and_scout +test_stop_conditions_budget_override_for_ship_and_scout test_herdr_lab_contract_is_explicit_and_complete test_herdr_lab_contract_quotes_foreign_firstmate_path test_herdr_lab_omission_is_loud_for_ship_and_scout From b5699f5db198fc75bcd525d1758af12b16b48b27 Mon Sep 17 00:00:00 2001 From: Varun Suchanti Date: Fri, 10 Jul 2026 17:25:05 +0530 Subject: [PATCH 02/12] no-mistakes(review): Captain, fix brief help and document task budgets --- AGENTS.md | 1 + docs/scripts.md | 2 +- tests/fm-brief.test.sh | 11 ++++++++--- 3 files changed, 10 insertions(+), 4 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 4f37db5cd..ce8d4caa9 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -426,6 +426,7 @@ Preserve durable structured identifiers, dependencies, and completion artifact l ## 11. Crewmate briefs `bin/fm-brief.sh` and its help own scaffold syntax, generated variants, status protocol, delivery-mode definitions of done, and exact safety mechanics. +At intake, pass `--budget ''` for ship or scout work whose unattended, long-running, or overnight shape needs a task-specific hard stop beyond the standard stop conditions; omit it for ordinary work. Use its scaffold as the contract, then replace every `{TASK}` placeholder with a clear task description, acceptance criteria, constraints, and necessary context before dispatch or seeding. Keep additions task-specific rather than repeating lifecycle instructions, and alter generated sections only when the task genuinely differs from the standard shape. diff --git a/docs/scripts.md b/docs/scripts.md index 5a4cc0f0e..4fa72ea4e 100644 --- a/docs/scripts.md +++ b/docs/scripts.md @@ -17,7 +17,7 @@ The shared no-mistakes gate refusal for fleet lifecycle entrypoints is summarize | `fm-update.sh` | Fast-forward-only self-update of firstmate and secondmate homes from origin | | `fm-backlog-handoff.sh` | Validate and delegate queued backlog-item moves into a secondmate home | | `fm-decision-hold.sh` | Create, verify, complete, and resolve durable captain-held decisions | -| `fm-brief.sh` | Scaffold ship, scout, secondmate-charter, and Herdr-lab briefs | +| `fm-brief.sh` | Scaffold ship, scout, secondmate-charter, and Herdr-lab briefs with optional task-specific stop budgets | | `fm-herdr-lab.sh` | Provision and guardedly operate an isolated, never-default Herdr lab session | | `fm-ensure-agents-md.sh` | Ensure a project's real `AGENTS.md`, its `CLAUDE.md` symlink, and the canonical self-governance section | | `fm-guard.sh` | Warn on primary-checkout tangles, pending queued wakes, and stale watcher liveness | diff --git a/tests/fm-brief.test.sh b/tests/fm-brief.test.sh index 302fe40f6..8eb48737a 100755 --- a/tests/fm-brief.test.sh +++ b/tests/fm-brief.test.sh @@ -30,10 +30,15 @@ test_script_parses() { } test_help_includes_entire_header() { - local help - help=$("$ROOT/bin/fm-brief.sh" --help) + local help status=0 + help=$("$ROOT/bin/fm-brief.sh" --help 2>&1) || status=$? + expect_code 0 "$status" "fm-brief.sh --help" + assert_contains "$help" "[--budget '']" \ + "fm-brief.sh --help omitted --budget from usage" + assert_contains "$help" "--budget '' adds a task-specific hard stop" \ + "fm-brief.sh --help omitted --budget documentation" assert_contains "$help" "Refuses to overwrite an existing brief." "fm-brief.sh --help omitted its header terminator" - pass "fm-brief.sh: --help renders the complete header" + pass "fm-brief.sh: --help includes the complete header" } # Registry with one project per delivery mode, so each ship-mode DOD branch is From 37035c01f84977f1445e28b260931e1e473d2ffa Mon Sep 17 00:00:00 2001 From: Varun Suchanti Date: Fri, 10 Jul 2026 17:34:42 +0530 Subject: [PATCH 03/12] no-mistakes(review): Captain, reject option flags as missing budget values --- bin/fm-brief.sh | 9 ++++++++- tests/fm-brief.test.sh | 19 +++++++++++++++++++ 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/bin/fm-brief.sh b/bin/fm-brief.sh index 2d15d373c..f3e8a23ce 100755 --- a/bin/fm-brief.sh +++ b/bin/fm-brief.sh @@ -59,6 +59,13 @@ usage() { ' "$0" } +is_option_token() { + case "${1:-}" in + -h|--help|--scout|--secondmate|--herdr-lab|--no-projects|--budget) return 0 ;; + *) return 1 ;; + esac +} + case "${1:-}" in -h|--help) usage; exit 0 ;; esac @@ -84,7 +91,7 @@ while [ "$#" -gt 0 ]; do --herdr-lab) HERDR_LAB=1 ;; --no-projects) NO_PROJECTS=1 ;; --budget) - if [ "$#" -lt 2 ] || [ -z "$2" ]; then + if [ "$#" -lt 2 ] || [ -z "$2" ] || is_option_token "$2"; then echo "error: --budget requires non-empty text" >&2 exit 1 fi diff --git a/tests/fm-brief.test.sh b/tests/fm-brief.test.sh index 8eb48737a..5a9c6883e 100755 --- a/tests/fm-brief.test.sh +++ b/tests/fm-brief.test.sh @@ -190,6 +190,24 @@ test_stop_conditions_budget_override_for_ship_and_scout() { pass "fm-brief.sh: --budget renders in ship and scout stop conditions" } +test_budget_rejects_recognized_option_as_value() { + local home id brief option status output + home="$TMP_ROOT/budget-option-home" + mkdir -p "$home/data" + + for option in -h --help --scout --secondmate --herdr-lab --no-projects --budget; do + id="brief-budget-option-${option//-/}" + status=0 + output=$(FM_HOME="$home" "$ROOT/bin/fm-brief.sh" "$id" firstmate --budget "$option" 2>&1) || status=$? + expect_code 1 "$status" "--budget must reject $option as a missing value" + assert_contains "$output" "error: --budget requires non-empty text" \ + "--budget $option returned the wrong error" + brief="$home/data/$id/brief.md" + assert_absent "$brief" "--budget $option still scaffolded a brief" + done + pass "fm-brief.sh: --budget rejects recognized options as its value" +} + test_herdr_lab_contract_is_explicit_and_complete() { local home id brief home="$TMP_ROOT/herdr-lab-home" @@ -403,6 +421,7 @@ test_no_mistakes_dod_wording test_ship_project_memory_wording test_stop_conditions_default_for_ship_and_scout test_stop_conditions_budget_override_for_ship_and_scout +test_budget_rejects_recognized_option_as_value test_herdr_lab_contract_is_explicit_and_complete test_herdr_lab_contract_quotes_foreign_firstmate_path test_herdr_lab_omission_is_loud_for_ship_and_scout From 3105db4fdae0d6037d77d2b283f9837f927fafba Mon Sep 17 00:00:00 2001 From: Varun Suchanti Date: Fri, 10 Jul 2026 19:37:28 +0530 Subject: [PATCH 04/12] no-mistakes(test): Captain, skip handoff tests when tasks-axi lacks atomic moves --- tests/fm-backlog-handoff.test.sh | 7 +++++-- tests/fm-secondmate-lifecycle-e2e.test.sh | 11 +++++++---- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/tests/fm-backlog-handoff.test.sh b/tests/fm-backlog-handoff.test.sh index b0b931d71..4ac3f83d3 100755 --- a/tests/fm-backlog-handoff.test.sh +++ b/tests/fm-backlog-handoff.test.sh @@ -10,8 +10,11 @@ set -u . "$(dirname "${BASH_SOURCE[0]}")/secondmate-helpers.sh" # The move is delegated to `tasks-axi mv`, so this suite exercises the real -# binary. Skip cleanly when it is absent (matching the backend smoke suites). -command -v tasks-axi >/dev/null 2>&1 || { echo "skip: tasks-axi not found (required by the delegated handoff path)"; exit 0; } +# binary. Skip cleanly when it is absent or lacks the atomic multi-ID contract +# required by the delegated handoff path (matching the backend smoke suites). +# shellcheck source=bin/fm-tasks-axi-lib.sh disable=SC1091 +. "$ROOT/bin/fm-tasks-axi-lib.sh" +fm_tasks_axi_compatible || { echo "skip: compatible tasks-axi not found (required by the delegated handoff path)"; exit 0; } TMP_ROOT=$(fm_test_tmproot fm-backlog-handoff) diff --git a/tests/fm-secondmate-lifecycle-e2e.test.sh b/tests/fm-secondmate-lifecycle-e2e.test.sh index fd71fdb35..451662fc6 100755 --- a/tests/fm-secondmate-lifecycle-e2e.test.sh +++ b/tests/fm-secondmate-lifecycle-e2e.test.sh @@ -151,10 +151,13 @@ phase_send() { } phase_handoff() { - # The move is delegated to `tasks-axi mv`; skip cleanly when it is absent (the - # downstream recovery and teardown phases do not depend on this phase). - if ! command -v tasks-axi >/dev/null 2>&1; then - echo "skip: tasks-axi not found (backlog handoff delegates to it)" + # The move is delegated to `tasks-axi mv`; skip cleanly when it is absent or + # incompatible (the downstream recovery and teardown phases do not depend on + # this phase). + # shellcheck source=bin/fm-tasks-axi-lib.sh disable=SC1091 + . "$ROOT/bin/fm-tasks-axi-lib.sh" + if ! fm_tasks_axi_compatible; then + echo "skip: compatible tasks-axi not found (backlog handoff delegates to it)" return 0 fi cat > "$HOME_DIR/data/backlog.md" <<'EOF' From b1ef874c512f6e74f33a46efcb54958341857fee Mon Sep 17 00:00:00 2001 From: Varun Suchanti Date: Fri, 10 Jul 2026 20:27:02 +0530 Subject: [PATCH 05/12] no-mistakes(document): Captain, document bounded brief stop conditions --- AGENTS.md | 2 +- docs/scripts.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index ce8d4caa9..5664e79aa 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -425,7 +425,7 @@ Preserve durable structured identifiers, dependencies, and completion artifact l ## 11. Crewmate briefs -`bin/fm-brief.sh` and its help own scaffold syntax, generated variants, status protocol, delivery-mode definitions of done, and exact safety mechanics. +`bin/fm-brief.sh` and its help own scaffold syntax, generated variants, bounded stop conditions, status protocol, delivery-mode definitions of done, and exact safety mechanics. At intake, pass `--budget ''` for ship or scout work whose unattended, long-running, or overnight shape needs a task-specific hard stop beyond the standard stop conditions; omit it for ordinary work. Use its scaffold as the contract, then replace every `{TASK}` placeholder with a clear task description, acceptance criteria, constraints, and necessary context before dispatch or seeding. Keep additions task-specific rather than repeating lifecycle instructions, and alter generated sections only when the task genuinely differs from the standard shape. diff --git a/docs/scripts.md b/docs/scripts.md index 4fa72ea4e..3845d2ed7 100644 --- a/docs/scripts.md +++ b/docs/scripts.md @@ -17,7 +17,7 @@ The shared no-mistakes gate refusal for fleet lifecycle entrypoints is summarize | `fm-update.sh` | Fast-forward-only self-update of firstmate and secondmate homes from origin | | `fm-backlog-handoff.sh` | Validate and delegate queued backlog-item moves into a secondmate home | | `fm-decision-hold.sh` | Create, verify, complete, and resolve durable captain-held decisions | -| `fm-brief.sh` | Scaffold ship, scout, secondmate-charter, and Herdr-lab briefs with optional task-specific stop budgets | +| `fm-brief.sh` | Scaffold ship and scout briefs with bounded stop conditions and optional task-specific budgets, plus secondmate-charter and Herdr-lab briefs | | `fm-herdr-lab.sh` | Provision and guardedly operate an isolated, never-default Herdr lab session | | `fm-ensure-agents-md.sh` | Ensure a project's real `AGENTS.md`, its `CLAUDE.md` symlink, and the canonical self-governance section | | `fm-guard.sh` | Warn on primary-checkout tangles, pending queued wakes, and stale watcher liveness | From 223dba39d7a9ef20cea0d1b39e51ad08b8cea55b Mon Sep 17 00:00:00 2001 From: Varun Suchanti Date: Fri, 10 Jul 2026 21:10:25 +0530 Subject: [PATCH 06/12] no-mistakes(test): Captain: fix transient Herdr worktree readiness race --- bin/fm-spawn.sh | 7 +++++-- tests/fm-backend.test.sh | 23 +++++++++++++++++++++++ 2 files changed, 28 insertions(+), 2 deletions(-) diff --git a/bin/fm-spawn.sh b/bin/fm-spawn.sh index fc177354b..ab8564f36 100755 --- a/bin/fm-spawn.sh +++ b/bin/fm-spawn.sh @@ -846,8 +846,11 @@ if [ "$KIND" != secondmate ] && [ "$BACKEND" != orca ]; then for _ in $(seq 1 60); do p=$(spawn_current_path "$WT_TARGET" || true) if [ -n "$p" ] && [ "$(real_path_or_raw "$p")" != "$PROJ_ABS_REAL" ]; then - WT="$p" - break + p_top=$(git -C "$p" rev-parse --show-toplevel 2>/dev/null || true) + if [ -n "$p_top" ] && [ "$(real_path_or_raw "$p")" = "$(real_path_or_raw "$p_top")" ]; then + WT="$p" + break + fi fi sleep 1 done diff --git a/tests/fm-backend.test.sh b/tests/fm-backend.test.sh index c25bf8a22..4e979d81a 100755 --- a/tests/fm-backend.test.sh +++ b/tests/fm-backend.test.sh @@ -871,6 +871,28 @@ test_spawn_symlinked_project_prefix_avoids_false_refusal() { pass "fm-spawn.sh: a project reached through a symlinked prefix (e.g. macOS /tmp -> /private/tmp) does not trip the isolation guard's false refusal" } +test_spawn_ignores_transient_non_worktree_cwd() { + local proj wt data id state config fb log out rc + proj="$TMP_ROOT/transient-cwd-project"; wt="$TMP_ROOT/transient-cwd-wt"; data="$TMP_ROOT/transient-cwd-data" + id="spawntransient1" + fm_git_worktree "$proj" "$wt" "fm/$id" + fb=$(make_spawn_symlink_fakebin "$TMP_ROOT/transient-cwd-fake" / "$wt") + mkdir -p "$data/$id" + printf 'test brief content\n' > "$data/$id/brief.md" + state="$TMP_ROOT/transient-cwd-state"; config="$TMP_ROOT/transient-cwd-config" + mkdir -p "$state" "$config" + log="$TMP_ROOT/transient-cwd-spawn.log" + + out=$(run_spawn_case "$ROOT" "$fb" "$log" "$state" "$data" "$config" "$proj" -- "$id" "$proj" claude 2>&1) + rc=$? + expect_code 0 "$rc" "fm-spawn.sh should ignore a transient non-worktree cwd while treehouse get is settling"$'\n'"$out" + assert_contains "$out" "worktree=$wt" \ + "fm-spawn.sh did not continue polling through a transient non-worktree cwd" + + rm -rf "/tmp/fm-$id" + pass "fm-spawn.sh: transient non-worktree cwd is ignored until treehouse reports the leased worktree" +} + # --- old vs new: fm-teardown.sh ---------------------------------------------- make_teardown_fakebin() { # -> echoes fakebin dir; logs tmux+treehouse calls @@ -1084,6 +1106,7 @@ test_backend_of_selector_matches_explicit_target_meta test_send_conformance_old_vs_new test_peek_conformance_old_vs_new test_spawn_symlinked_project_prefix_avoids_false_refusal +test_spawn_ignores_transient_non_worktree_cwd test_teardown_conformance_old_vs_new test_spawn_refuses_unknown_backend_flag test_spawn_refuses_codex_app_backend_flag From 4077139269801204b08cc8d9c358b0775af3f148 Mon Sep 17 00:00:00 2001 From: Varun Suchanti Date: Fri, 10 Jul 2026 21:25:50 +0530 Subject: [PATCH 07/12] no-mistakes(test): Captain: validate repeated invalid spawn cwd without timing out --- bin/fm-spawn.sh | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/bin/fm-spawn.sh b/bin/fm-spawn.sh index ab8564f36..722cd5aae 100755 --- a/bin/fm-spawn.sh +++ b/bin/fm-spawn.sh @@ -843,6 +843,8 @@ if [ "$KIND" != secondmate ] && [ "$BACKEND" != orca ]; then # Compare against PROJ_ABS_REAL (physical), not PROJ_ABS: a symlinked project # prefix would otherwise make the pane's OS-level cwd read differ from # PROJ_ABS on the very first poll, before the pane has actually moved. + invalid_candidate= + invalid_candidate_polls=0 for _ in $(seq 1 60); do p=$(spawn_current_path "$WT_TARGET" || true) if [ -n "$p" ] && [ "$(real_path_or_raw "$p")" != "$PROJ_ABS_REAL" ]; then @@ -851,6 +853,22 @@ if [ "$KIND" != secondmate ] && [ "$BACKEND" != orca ]; then WT="$p" break fi + # A backend can briefly report an intermediate non-worktree cwd while + # treehouse is settling. Ignore one sample, but hand a stable invalid + # path to the isolation validator instead of misreporting a timeout. + if [ "$(real_path_or_raw "$p")" = "$invalid_candidate" ]; then + invalid_candidate_polls=$((invalid_candidate_polls + 1)) + else + invalid_candidate=$(real_path_or_raw "$p") + invalid_candidate_polls=1 + fi + if [ "$invalid_candidate_polls" -ge 2 ]; then + WT="$p" + break + fi + else + invalid_candidate= + invalid_candidate_polls=0 fi sleep 1 done From fbe9954b810ab536c95daa4b55e0099cb8db8e3a Mon Sep 17 00:00:00 2001 From: Varun Suchanti Date: Fri, 10 Jul 2026 20:07:20 +0530 Subject: [PATCH 08/12] fix: stabilize locale and timeout tests --- bin/backends/herdr.sh | 4 ++-- bin/fm-composer-lib.sh | 14 ++++++++++++-- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/bin/backends/herdr.sh b/bin/backends/herdr.sh index 260d2b666..29de3ddb5 100644 --- a/bin/backends/herdr.sh +++ b/bin/backends/herdr.sh @@ -739,7 +739,7 @@ FM_BACKEND_HERDR_IDLE_RE=${FM_BACKEND_HERDR_IDLE_RE:-'^Type a message\.\.\.$'} # Known bare (unbordered) prompt glyphs a composer row may start with: ❯ # (claude) and › (codex) only. Generic shell-style glyphs > $ % # are still # recognized after a bordered composer row has already been structurally found. -FM_BACKEND_HERDR_BARE_PROMPT_RE=${FM_BACKEND_HERDR_BARE_PROMPT_RE:-'^[❯›]'} +FM_BACKEND_HERDR_BARE_PROMPT_RE=${FM_BACKEND_HERDR_BARE_PROMPT_RE:-'^(❯|›)'} # Pi allows a multi-line composer between its horizontal separators. Bound the # structural candidate so two unrelated transcript rules with an arbitrarily # large region between them can never be promoted into a composer. @@ -903,7 +903,7 @@ EOF fi # Delegate the empty/pending/unknown decision to the shared owner. The bare # shape only ever starts with an AGENT glyph (FM_BACKEND_HERDR_BARE_PROMPT_RE - # is '^[❯›]'), so a bare shell prompt never reaches here - it stays 'unknown' + # is '^(❯|›)'), so a bare shell prompt never reaches here - it stays 'unknown' # via the no-composer-row path above, exactly as before. fm_composer_classify_content "$bordered" "$stripped" "$FM_BACKEND_HERDR_IDLE_RE" } diff --git a/bin/fm-composer-lib.sh b/bin/fm-composer-lib.sh index 437b8c689..e94b39a26 100644 --- a/bin/fm-composer-lib.sh +++ b/bin/fm-composer-lib.sh @@ -207,8 +207,18 @@ fm_composer_classify_content() { # [idle_re] [idle_case] [ fi # Strip a leading prompt glyph, then re-judge the remainder. case "$content" in - '❯ '*|'› '*|'> '*|'$ '*|'% '*|'# '*) content=${content#??} ;; - '❯'*|'›'*|'>'*|'$'*|'%'*|'#'*) content=${content#?} ;; + '❯ '*) content=${content#'❯ '} ;; + '› '*) content=${content#'› '} ;; + '> '*) content=${content#'> '} ;; + '$ '*) content=${content#'$ '} ;; + '% '*) content=${content#'% '} ;; + '# '*) content=${content#'# '} ;; + '❯'*) content=${content#'❯'} ;; + '›'*) content=${content#'›'} ;; + '>'*) content=${content#'>'} ;; + '$'*) content=${content#'$'} ;; + '%'*) content=${content#'%'} ;; + '#'*) content=${content#'#'} ;; esac content="${content#"${content%%[![:space:]]*}"}" content="${content%"${content##*[![:space:]]}"}" From e084ae0868a71e269841264ed496a9f88cf7bd1d Mon Sep 17 00:00:00 2001 From: Varun Suchanti Date: Sat, 11 Jul 2026 01:22:54 +0530 Subject: [PATCH 09/12] no-mistakes(document): Document bounded brief stops in architecture --- docs/architecture.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/architecture.md b/docs/architecture.md index 23ff9de20..9929fd80c 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -123,6 +123,7 @@ The helper's header owns the exact signal detection, relocated-home limitation, ## Two task shapes Ship tasks change projects and ship by project mode (`no-mistakes`, `direct-PR`, or `local-only`); scout tasks investigate, plan, reproduce bugs, or audit, then leave a report at `data//report.md` and never push. +Both task shapes receive the bounded stop conditions owned by `fm-brief.sh`, and an optional `--budget ''` adds a task-specific hard cap for unattended or long-running work. ## Dispatch profiles From 116e70055d72c7b0cf19d26eedb1aaaa1fb93dd5 Mon Sep 17 00:00:00 2001 From: Varun Suchanti Date: Sat, 11 Jul 2026 01:52:38 +0530 Subject: [PATCH 10/12] no-mistakes(review): Captain, validate brief arguments and document locale-safe Herdr regex --- bin/fm-brief.sh | 6 ++++++ docs/configuration.md | 2 +- tests/fm-brief.test.sh | 23 +++++++++++++++++++++++ 3 files changed, 30 insertions(+), 1 deletion(-) diff --git a/bin/fm-brief.sh b/bin/fm-brief.sh index f3e8a23ce..a4f4668c7 100755 --- a/bin/fm-brief.sh +++ b/bin/fm-brief.sh @@ -102,6 +102,12 @@ while [ "$#" -gt 0 ]; do esac shift done + +if [ "$KIND" != secondmate ] && [ "${#POS[@]}" -gt 2 ]; then + echo "error: ship and scout briefs accept only and as positional arguments" >&2 + exit 1 +fi + ID=${POS[0]} if [ "$KIND" = secondmate ] && [ "$HERDR_LAB" -eq 1 ]; then diff --git a/docs/configuration.md b/docs/configuration.md index bbe5ca4f3..fd489faed 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -352,7 +352,7 @@ FM_BACKEND= # optional runtime backend override for new spawns; tmux HERDR_SESSION=default # herdr-only: named session for normal backend ops; not enough for destructive cleanup (docs/herdr-backend.md) FM_BACKEND_HERDR_COMPOSER_LINES=20 # herdr-only: tail lines scanned by composer-state guard/fallback paths; idle-baseline submit confirmation uses agent-state FM_BACKEND_HERDR_IDLE_RE='^Type a message\.\.\.$' # herdr-only: empty-composer placeholder regex after shared ghost extraction plus border and prompt stripping -FM_BACKEND_HERDR_BARE_PROMPT_RE='^[❯›]' # herdr-only: verified agent glyphs recognized as an UNBORDERED (bare) composer row, e.g. claude's ❯ or codex's ›; shell glyphs remain unknown rather than empty, and de-emphasised ghost/placeholder text (dim or dark-truecolor) after an agent prompt reads empty via the shared fm_composer_strip_ghost (docs/herdr-backend.md "Incident (2026-07-08)", "Incident (2026-07-10)") +FM_BACKEND_HERDR_BARE_PROMPT_RE='^(❯|›)' # herdr-only: verified agent glyphs recognized as an UNBORDERED (bare) composer row, e.g. claude's ❯ or codex's ›; alternation keeps the glyph match locale-safe under byte-oriented locales, shell glyphs remain unknown rather than empty, and de-emphasised ghost/placeholder text (dim or dark-truecolor) after an agent prompt reads empty via the shared fm_composer_strip_ghost (docs/herdr-backend.md "Incident (2026-07-08)", "Incident (2026-07-10)") FM_BACKEND_HERDR_PI_COMPOSER_MAX_LINES=8 # herdr-only: maximum rows admitted between Pi's native-identity-corroborated separator pair; taller or ambiguous candidates stay unknown (docs/herdr-backend.md "Incident (2026-07-14)") FM_BACKEND_HERDR_SUBMIT_POLLS=6 # herdr-only: agent-state samples spread across each Enter attempt's budget when confirming a submit (docs/herdr-backend.md "Native agent-state submit confirmation") FM_BACKEND_HERDR_SUBMIT_MIN_SLEEP=0.6 # herdr-only: minimum per-Enter confirmation budget before polling agent-state after an idle baseline diff --git a/tests/fm-brief.test.sh b/tests/fm-brief.test.sh index 5a9c6883e..9b26227b7 100755 --- a/tests/fm-brief.test.sh +++ b/tests/fm-brief.test.sh @@ -208,6 +208,28 @@ test_budget_rejects_recognized_option_as_value() { pass "fm-brief.sh: --budget rejects recognized options as its value" } +test_ship_and_scout_reject_excess_positionals() { + local home id brief kind status output + home="$TMP_ROOT/excess-positionals-home" + mkdir -p "$home/data" + + for kind in ship scout; do + id="brief-excess-positionals-$kind" + status=0 + if [ "$kind" = scout ]; then + output=$(FM_HOME="$home" "$ROOT/bin/fm-brief.sh" "$id" firstmate --scout --budget hard stop at 30 minutes 2>&1) || status=$? + else + output=$(FM_HOME="$home" "$ROOT/bin/fm-brief.sh" "$id" firstmate --budget hard stop at 30 minutes 2>&1) || status=$? + fi + expect_code 1 "$status" "$kind brief must reject excess positional arguments" + assert_contains "$output" "error: ship and scout briefs accept only and as positional arguments" \ + "$kind brief returned the wrong excess-positionals error" + brief="$home/data/$id/brief.md" + assert_absent "$brief" "$kind brief silently scaffolded with a truncated unquoted budget" + done + pass "fm-brief.sh: ship and scout briefs reject excess positional arguments" +} + test_herdr_lab_contract_is_explicit_and_complete() { local home id brief home="$TMP_ROOT/herdr-lab-home" @@ -422,6 +444,7 @@ test_ship_project_memory_wording test_stop_conditions_default_for_ship_and_scout test_stop_conditions_budget_override_for_ship_and_scout test_budget_rejects_recognized_option_as_value +test_ship_and_scout_reject_excess_positionals test_herdr_lab_contract_is_explicit_and_complete test_herdr_lab_contract_quotes_foreign_firstmate_path test_herdr_lab_omission_is_loud_for_ship_and_scout From 4e54eb06fc692adc284cfd93902472541f079248 Mon Sep 17 00:00:00 2001 From: Varun Suchanti Date: Sun, 19 Jul 2026 15:29:03 +0530 Subject: [PATCH 11/12] fix(brief): use guideline terminology --- AGENTS.md | 4 +- bin/fm-brief.sh | 38 +++++++++--------- docs/architecture.md | 2 +- docs/scripts.md | 2 +- tests/fm-brief.test.sh | 87 +++++++++++++++++++++--------------------- 5 files changed, 67 insertions(+), 66 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 5664e79aa..269b75e6c 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -425,8 +425,8 @@ Preserve durable structured identifiers, dependencies, and completion artifact l ## 11. Crewmate briefs -`bin/fm-brief.sh` and its help own scaffold syntax, generated variants, bounded stop conditions, status protocol, delivery-mode definitions of done, and exact safety mechanics. -At intake, pass `--budget ''` for ship or scout work whose unattended, long-running, or overnight shape needs a task-specific hard stop beyond the standard stop conditions; omit it for ordinary work. +`bin/fm-brief.sh` and its help own scaffold syntax, generated variants, guidelines, status protocol, delivery-mode definitions of done, and exact safety mechanics. +At intake, pass `--guideline ''` for ship or scout work whose unattended, long-running, or overnight shape needs additional task-specific guidance beyond the standard guidelines; omit it for ordinary work. Use its scaffold as the contract, then replace every `{TASK}` placeholder with a clear task description, acceptance criteria, constraints, and necessary context before dispatch or seeding. Keep additions task-specific rather than repeating lifecycle instructions, and alter generated sections only when the task genuinely differs from the standard shape. diff --git a/bin/fm-brief.sh b/bin/fm-brief.sh index a4f4668c7..e1450d143 100755 --- a/bin/fm-brief.sh +++ b/bin/fm-brief.sh @@ -6,7 +6,7 @@ # description, acceptance criteria, and context, and may adjust other sections # when the task genuinely deviates (e.g. working an existing external PR instead # of shipping a new one). -# Usage: fm-brief.sh [--scout] [--herdr-lab] [--budget ''] +# Usage: fm-brief.sh [--scout] [--herdr-lab] [--guideline ''] # fm-brief.sh --secondmate {...|--no-projects} # --scout writes the scout contract instead: the deliverable is a report at # data//report.md (no branch, no push, no PR) and the worktree is scratch. @@ -26,8 +26,8 @@ # The flag must be explicit because {TASK} is filled after scaffolding and the # caller-supplied repo string cannot reliably identify this repo. Briefs made # without it carry a loud declaration so an omitted contract cannot be silent. -# --budget '' adds a task-specific hard stop to a crewmate ship or scout -# brief's standard stop conditions. +# --guideline '' adds task-specific advisory guidance to a crewmate ship +# or scout brief's standard guidelines. # For ship tasks, the definition of done is shaped by the project's delivery mode # (data/projects.md via fm-project-mode.sh; see the project-management skill # and AGENTS.md task lifecycle): @@ -61,7 +61,7 @@ usage() { is_option_token() { case "${1:-}" in - -h|--help|--scout|--secondmate|--herdr-lab|--no-projects|--budget) return 0 ;; + -h|--help|--scout|--secondmate|--herdr-lab|--no-projects|--guideline) return 0 ;; *) return 1 ;; esac } @@ -82,7 +82,7 @@ STATE="${FM_STATE_OVERRIDE:-$FM_HOME/state}" KIND=ship HERDR_LAB=0 NO_PROJECTS=0 -BUDGET="" +GUIDELINE="" POS=() while [ "$#" -gt 0 ]; do case "$1" in @@ -90,12 +90,12 @@ while [ "$#" -gt 0 ]; do --secondmate) KIND=secondmate ;; --herdr-lab) HERDR_LAB=1 ;; --no-projects) NO_PROJECTS=1 ;; - --budget) + --guideline) if [ "$#" -lt 2 ] || [ -z "$2" ] || is_option_token "$2"; then - echo "error: --budget requires non-empty text" >&2 + echo "error: --guideline requires non-empty text" >&2 exit 1 fi - BUDGET=$2 + GUIDELINE=$2 shift ;; *) POS+=("$1") ;; @@ -115,8 +115,8 @@ if [ "$KIND" = secondmate ] && [ "$HERDR_LAB" -eq 1 ]; then exit 1 fi -if [ "$KIND" = secondmate ] && [ -n "$BUDGET" ]; then - echo "error: --budget applies only to crewmate ship or scout briefs" >&2 +if [ "$KIND" = secondmate ] && [ -n "$GUIDELINE" ]; then + echo "error: --guideline applies only to crewmate ship or scout briefs" >&2 exit 1 fi @@ -219,18 +219,18 @@ fi REPO=${POS[1]} -STOP_CONDITIONS=$(cat <<'EOF' -## Stop conditions +GUIDELINES=$(cat <<'EOF' +## Guidelines Keep working while the task makes meaningful progress. Expected waiting on a validation run or long test suite does not count as a no-phase-change stretch. -If roughly two hours or 50 significant actions pass without a supervisor-actionable phase change, append `blocked: budget - {one-line progress + what remains}` and stop. +If roughly two hours or 50 significant actions pass without a supervisor-actionable phase change, follow this advisory guideline: append `blocked: guideline - {one-line progress + what remains}` and stop. If the work is clearly larger than this brief, append `blocked: scope - {one-line progress + newly discovered work}` and stop before silently expanding scope. EOF ) -if [ -n "$BUDGET" ]; then - STOP_CONDITIONS="${STOP_CONDITIONS} -Task-specific budget: $BUDGET -Treat this as a hard cap, and when it is reached append \`blocked: budget - {one-line progress + what remains}\` and stop." +if [ -n "$GUIDELINE" ]; then + GUIDELINES="${GUIDELINES} +Task-specific guideline: $GUIDELINE +Treat this as advisory guidance alongside the standard guidelines." fi if [ "$HERDR_LAB" -eq 1 ]; then @@ -302,7 +302,7 @@ The report is the only thing that survives, so anything worth keeping must be in every lane/home, so restarting it kills other lanes' in-flight pipeline runs. On ANY no-mistakes daemon error, append \`blocked: {the daemon error}\` and stop; only firstmate manages the daemon. -$STOP_CONDITIONS +$GUIDELINES # Definition of done Write your findings to \`$DATA/$ID/report.md\`. @@ -412,7 +412,7 @@ $RULE1 every lane/home, so restarting it kills other lanes' in-flight pipeline runs. On ANY no-mistakes daemon error, append \`blocked: {the daemon error}\` and stop; only firstmate manages the daemon. -$STOP_CONDITIONS +$GUIDELINES # Project memory If \`AGENTS.md\` or \`CLAUDE.md\` already exists, or if this task produced durable project-intrinsic knowledge, run \`$FM_ROOT/bin/fm-ensure-agents-md.sh .\` in the worktree. diff --git a/docs/architecture.md b/docs/architecture.md index 9929fd80c..4afd0daa6 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -123,7 +123,7 @@ The helper's header owns the exact signal detection, relocated-home limitation, ## Two task shapes Ship tasks change projects and ship by project mode (`no-mistakes`, `direct-PR`, or `local-only`); scout tasks investigate, plan, reproduce bugs, or audit, then leave a report at `data//report.md` and never push. -Both task shapes receive the bounded stop conditions owned by `fm-brief.sh`, and an optional `--budget ''` adds a task-specific hard cap for unattended or long-running work. +Both task shapes receive the advisory guidelines owned by `fm-brief.sh`, and an optional `--guideline ''` adds task-specific guidance for unattended or long-running work. ## Dispatch profiles diff --git a/docs/scripts.md b/docs/scripts.md index 3845d2ed7..bbf87b87f 100644 --- a/docs/scripts.md +++ b/docs/scripts.md @@ -17,7 +17,7 @@ The shared no-mistakes gate refusal for fleet lifecycle entrypoints is summarize | `fm-update.sh` | Fast-forward-only self-update of firstmate and secondmate homes from origin | | `fm-backlog-handoff.sh` | Validate and delegate queued backlog-item moves into a secondmate home | | `fm-decision-hold.sh` | Create, verify, complete, and resolve durable captain-held decisions | -| `fm-brief.sh` | Scaffold ship and scout briefs with bounded stop conditions and optional task-specific budgets, plus secondmate-charter and Herdr-lab briefs | +| `fm-brief.sh` | Scaffold ship and scout briefs with standard guidelines and optional task-specific guidance, plus secondmate-charter and Herdr-lab briefs | | `fm-herdr-lab.sh` | Provision and guardedly operate an isolated, never-default Herdr lab session | | `fm-ensure-agents-md.sh` | Ensure a project's real `AGENTS.md`, its `CLAUDE.md` symlink, and the canonical self-governance section | | `fm-guard.sh` | Warn on primary-checkout tangles, pending queued wakes, and stale watcher liveness | diff --git a/tests/fm-brief.test.sh b/tests/fm-brief.test.sh index 9b26227b7..557bd4e32 100755 --- a/tests/fm-brief.test.sh +++ b/tests/fm-brief.test.sh @@ -33,10 +33,10 @@ test_help_includes_entire_header() { local help status=0 help=$("$ROOT/bin/fm-brief.sh" --help 2>&1) || status=$? expect_code 0 "$status" "fm-brief.sh --help" - assert_contains "$help" "[--budget '']" \ - "fm-brief.sh --help omitted --budget from usage" - assert_contains "$help" "--budget '' adds a task-specific hard stop" \ - "fm-brief.sh --help omitted --budget documentation" + assert_contains "$help" "[--guideline '']" \ + "fm-brief.sh --help omitted --guideline from usage" + assert_contains "$help" "--guideline '' adds task-specific advisory guidance" \ + "fm-brief.sh --help omitted --guideline documentation" assert_contains "$help" "Refuses to overwrite an existing brief." "fm-brief.sh --help omitted its header terminator" pass "fm-brief.sh: --help includes the complete header" } @@ -139,13 +139,13 @@ test_ship_project_memory_wording() { pass "fm-brief.sh: ship project-memory wording carries the AGENTS.md authoring bar" } -test_stop_conditions_default_for_ship_and_scout() { +test_guidelines_default_for_ship_and_scout() { local home id brief - home="$TMP_ROOT/stop-conditions-default-home" + home="$TMP_ROOT/guidelines-default-home" mkdir -p "$home/data" for kind in ship scout; do - id="brief-stop-default-$kind" + id="brief-guideline-default-$kind" if [ "$kind" = scout ]; then FM_HOME="$home" "$ROOT/bin/fm-brief.sh" "$id" firstmate --scout >/dev/null 2>&1 else @@ -153,59 +153,60 @@ test_stop_conditions_default_for_ship_and_scout() { fi brief="$home/data/$id/brief.md" assert_grep "# Rules" "$brief" "$kind brief lost the Rules section" - assert_grep "## Stop conditions" "$brief" "$kind brief missing the stop-conditions block" + assert_grep "## Guidelines" "$brief" "$kind brief missing the guidelines block" assert_grep "Expected waiting on a validation run or long test suite does not count as a no-phase-change stretch." "$brief" \ - "$kind brief treats healthy validation waiting as budget exhaustion" + "$kind brief treats healthy validation waiting as reaching the guideline" assert_grep "If roughly two hours or 50 significant actions pass without a supervisor-actionable phase change" "$brief" \ - "$kind brief missing the default turn and time cap" - assert_grep "blocked: budget - {one-line progress + what remains}" "$brief" \ - "$kind brief missing the budget exhaustion status protocol" + "$kind brief missing the default action and time guideline" + assert_grep "blocked: guideline - {one-line progress + what remains}" "$brief" \ + "$kind brief missing the guideline status protocol" assert_grep "blocked: scope - {one-line progress + newly discovered work}" "$brief" \ - "$kind brief missing the scope-expansion stop condition" + "$kind brief missing the scope-expansion guideline" assert_grep "# Definition of done" "$brief" "$kind brief lost the Definition of done section" - assert_no_grep "Task-specific budget:" "$brief" "$kind brief rendered a budget override without --budget" + assert_no_grep "Task-specific guideline:" "$brief" "$kind brief rendered task-specific guidance without --guideline" done - pass "fm-brief.sh: ship and scout briefs include safe default stop conditions" + pass "fm-brief.sh: ship and scout briefs include the standard guidelines" } -test_stop_conditions_budget_override_for_ship_and_scout() { - local home id brief budget - home="$TMP_ROOT/stop-conditions-budget-home" - budget="hard stop at 30 minutes" +test_task_specific_guideline_for_ship_and_scout() { + local home id brief guideline + home="$TMP_ROOT/task-specific-guideline-home" + guideline="check in after 30 minutes" mkdir -p "$home/data" for kind in ship scout; do - id="brief-stop-budget-$kind" + id="brief-task-guideline-$kind" if [ "$kind" = scout ]; then - FM_HOME="$home" "$ROOT/bin/fm-brief.sh" "$id" firstmate --scout --budget "$budget" >/dev/null 2>&1 + FM_HOME="$home" "$ROOT/bin/fm-brief.sh" "$id" firstmate --scout --guideline "$guideline" >/dev/null 2>&1 else - FM_HOME="$home" "$ROOT/bin/fm-brief.sh" "$id" firstmate --budget "$budget" >/dev/null 2>&1 + FM_HOME="$home" "$ROOT/bin/fm-brief.sh" "$id" firstmate --guideline "$guideline" >/dev/null 2>&1 fi brief="$home/data/$id/brief.md" - assert_grep "Task-specific budget: $budget" "$brief" "$kind brief omitted its --budget override" - assert_grep "Treat this as a hard cap" "$brief" "$kind brief did not make --budget a stop condition" - assert_grep "blocked: budget - {one-line progress + what remains}" "$brief" \ - "$kind brief lost the budget exhaustion status protocol" + assert_grep "Task-specific guideline: $guideline" "$brief" "$kind brief omitted its --guideline text" + assert_grep "Treat this as advisory guidance alongside the standard guidelines." "$brief" \ + "$kind brief did not describe --guideline as advisory" + assert_grep "blocked: guideline - {one-line progress + what remains}" "$brief" \ + "$kind brief lost the guideline status protocol" done - pass "fm-brief.sh: --budget renders in ship and scout stop conditions" + pass "fm-brief.sh: --guideline renders as advisory guidance in ship and scout briefs" } -test_budget_rejects_recognized_option_as_value() { +test_guideline_rejects_recognized_option_as_value() { local home id brief option status output - home="$TMP_ROOT/budget-option-home" + home="$TMP_ROOT/guideline-option-home" mkdir -p "$home/data" - for option in -h --help --scout --secondmate --herdr-lab --no-projects --budget; do - id="brief-budget-option-${option//-/}" + for option in -h --help --scout --secondmate --herdr-lab --no-projects --guideline; do + id="brief-guideline-option-${option//-/}" status=0 - output=$(FM_HOME="$home" "$ROOT/bin/fm-brief.sh" "$id" firstmate --budget "$option" 2>&1) || status=$? - expect_code 1 "$status" "--budget must reject $option as a missing value" - assert_contains "$output" "error: --budget requires non-empty text" \ - "--budget $option returned the wrong error" + output=$(FM_HOME="$home" "$ROOT/bin/fm-brief.sh" "$id" firstmate --guideline "$option" 2>&1) || status=$? + expect_code 1 "$status" "--guideline must reject $option as a missing value" + assert_contains "$output" "error: --guideline requires non-empty text" \ + "--guideline $option returned the wrong error" brief="$home/data/$id/brief.md" - assert_absent "$brief" "--budget $option still scaffolded a brief" + assert_absent "$brief" "--guideline $option still scaffolded a brief" done - pass "fm-brief.sh: --budget rejects recognized options as its value" + pass "fm-brief.sh: --guideline rejects recognized options as its value" } test_ship_and_scout_reject_excess_positionals() { @@ -217,15 +218,15 @@ test_ship_and_scout_reject_excess_positionals() { id="brief-excess-positionals-$kind" status=0 if [ "$kind" = scout ]; then - output=$(FM_HOME="$home" "$ROOT/bin/fm-brief.sh" "$id" firstmate --scout --budget hard stop at 30 minutes 2>&1) || status=$? + output=$(FM_HOME="$home" "$ROOT/bin/fm-brief.sh" "$id" firstmate --scout --guideline check in after 30 minutes 2>&1) || status=$? else - output=$(FM_HOME="$home" "$ROOT/bin/fm-brief.sh" "$id" firstmate --budget hard stop at 30 minutes 2>&1) || status=$? + output=$(FM_HOME="$home" "$ROOT/bin/fm-brief.sh" "$id" firstmate --guideline check in after 30 minutes 2>&1) || status=$? fi expect_code 1 "$status" "$kind brief must reject excess positional arguments" assert_contains "$output" "error: ship and scout briefs accept only and as positional arguments" \ "$kind brief returned the wrong excess-positionals error" brief="$home/data/$id/brief.md" - assert_absent "$brief" "$kind brief silently scaffolded with a truncated unquoted budget" + assert_absent "$brief" "$kind brief silently scaffolded with truncated unquoted guidance" done pass "fm-brief.sh: ship and scout briefs reject excess positional arguments" } @@ -441,9 +442,9 @@ test_ship_modes_generate_clean_briefs test_faster_paths_use_configured_authority_without_stacked_review test_no_mistakes_dod_wording test_ship_project_memory_wording -test_stop_conditions_default_for_ship_and_scout -test_stop_conditions_budget_override_for_ship_and_scout -test_budget_rejects_recognized_option_as_value +test_guidelines_default_for_ship_and_scout +test_task_specific_guideline_for_ship_and_scout +test_guideline_rejects_recognized_option_as_value test_ship_and_scout_reject_excess_positionals test_herdr_lab_contract_is_explicit_and_complete test_herdr_lab_contract_quotes_foreign_firstmate_path From 0b0e8136fd4963fc08b7c8802589cab857eb8399 Mon Sep 17 00:00:00 2001 From: Varun Suchanti Date: Mon, 20 Jul 2026 12:56:54 +0530 Subject: [PATCH 12/12] fix(brief): use stop-condition terminology --- AGENTS.md | 4 +- bin/fm-brief.sh | 39 ++++++++++--------- docs/architecture.md | 2 +- docs/scripts.md | 2 +- tests/fm-brief.test.sh | 88 +++++++++++++++++++++--------------------- 5 files changed, 69 insertions(+), 66 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 269b75e6c..c6093d99c 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -425,8 +425,8 @@ Preserve durable structured identifiers, dependencies, and completion artifact l ## 11. Crewmate briefs -`bin/fm-brief.sh` and its help own scaffold syntax, generated variants, guidelines, status protocol, delivery-mode definitions of done, and exact safety mechanics. -At intake, pass `--guideline ''` for ship or scout work whose unattended, long-running, or overnight shape needs additional task-specific guidance beyond the standard guidelines; omit it for ordinary work. +`bin/fm-brief.sh` and its help own scaffold syntax, generated variants, stop conditions, status protocol, delivery-mode definitions of done, and exact safety mechanics. +At intake, pass `--stop-condition ''` for ship or scout work whose unattended, long-running, or overnight shape needs an additional task-specific stop condition beyond the standard stop conditions; omit it for ordinary work. Use its scaffold as the contract, then replace every `{TASK}` placeholder with a clear task description, acceptance criteria, constraints, and necessary context before dispatch or seeding. Keep additions task-specific rather than repeating lifecycle instructions, and alter generated sections only when the task genuinely differs from the standard shape. diff --git a/bin/fm-brief.sh b/bin/fm-brief.sh index e1450d143..82e53e4ae 100755 --- a/bin/fm-brief.sh +++ b/bin/fm-brief.sh @@ -6,7 +6,7 @@ # description, acceptance criteria, and context, and may adjust other sections # when the task genuinely deviates (e.g. working an existing external PR instead # of shipping a new one). -# Usage: fm-brief.sh [--scout] [--herdr-lab] [--guideline ''] +# Usage: fm-brief.sh [--scout] [--herdr-lab] [--stop-condition ''] # fm-brief.sh --secondmate {...|--no-projects} # --scout writes the scout contract instead: the deliverable is a report at # data//report.md (no branch, no push, no PR) and the worktree is scratch. @@ -26,8 +26,8 @@ # The flag must be explicit because {TASK} is filled after scaffolding and the # caller-supplied repo string cannot reliably identify this repo. Briefs made # without it carry a loud declaration so an omitted contract cannot be silent. -# --guideline '' adds task-specific advisory guidance to a crewmate ship -# or scout brief's standard guidelines. +# --stop-condition '' adds a task-specific advisory stop condition to a +# crewmate ship or scout brief's standard stop conditions. # For ship tasks, the definition of done is shaped by the project's delivery mode # (data/projects.md via fm-project-mode.sh; see the project-management skill # and AGENTS.md task lifecycle): @@ -61,7 +61,7 @@ usage() { is_option_token() { case "${1:-}" in - -h|--help|--scout|--secondmate|--herdr-lab|--no-projects|--guideline) return 0 ;; + -h|--help|--scout|--secondmate|--herdr-lab|--no-projects|--stop-condition) return 0 ;; *) return 1 ;; esac } @@ -82,7 +82,7 @@ STATE="${FM_STATE_OVERRIDE:-$FM_HOME/state}" KIND=ship HERDR_LAB=0 NO_PROJECTS=0 -GUIDELINE="" +STOP_CONDITION="" POS=() while [ "$#" -gt 0 ]; do case "$1" in @@ -90,12 +90,12 @@ while [ "$#" -gt 0 ]; do --secondmate) KIND=secondmate ;; --herdr-lab) HERDR_LAB=1 ;; --no-projects) NO_PROJECTS=1 ;; - --guideline) + --stop-condition) if [ "$#" -lt 2 ] || [ -z "$2" ] || is_option_token "$2"; then - echo "error: --guideline requires non-empty text" >&2 + echo "error: --stop-condition requires non-empty text" >&2 exit 1 fi - GUIDELINE=$2 + STOP_CONDITION=$2 shift ;; *) POS+=("$1") ;; @@ -115,8 +115,8 @@ if [ "$KIND" = secondmate ] && [ "$HERDR_LAB" -eq 1 ]; then exit 1 fi -if [ "$KIND" = secondmate ] && [ -n "$GUIDELINE" ]; then - echo "error: --guideline applies only to crewmate ship or scout briefs" >&2 +if [ "$KIND" = secondmate ] && [ -n "$STOP_CONDITION" ]; then + echo "error: --stop-condition applies only to crewmate ship or scout briefs" >&2 exit 1 fi @@ -219,18 +219,19 @@ fi REPO=${POS[1]} -GUIDELINES=$(cat <<'EOF' -## Guidelines +STOP_CONDITIONS=$(cat <<'EOF' +## Stop conditions +These stop conditions are advisory guidance evaluated by the crewmate, not deterministically enforced limits. Keep working while the task makes meaningful progress. Expected waiting on a validation run or long test suite does not count as a no-phase-change stretch. -If roughly two hours or 50 significant actions pass without a supervisor-actionable phase change, follow this advisory guideline: append `blocked: guideline - {one-line progress + what remains}` and stop. +If roughly two hours or 50 significant actions pass without a supervisor-actionable phase change, append `blocked: stop condition - {one-line progress + what remains}` and stop. If the work is clearly larger than this brief, append `blocked: scope - {one-line progress + newly discovered work}` and stop before silently expanding scope. EOF ) -if [ -n "$GUIDELINE" ]; then - GUIDELINES="${GUIDELINES} -Task-specific guideline: $GUIDELINE -Treat this as advisory guidance alongside the standard guidelines." +if [ -n "$STOP_CONDITION" ]; then + STOP_CONDITIONS="${STOP_CONDITIONS} +Task-specific stop condition: $STOP_CONDITION +Treat this as advisory guidance alongside the standard stop conditions." fi if [ "$HERDR_LAB" -eq 1 ]; then @@ -302,7 +303,7 @@ The report is the only thing that survives, so anything worth keeping must be in every lane/home, so restarting it kills other lanes' in-flight pipeline runs. On ANY no-mistakes daemon error, append \`blocked: {the daemon error}\` and stop; only firstmate manages the daemon. -$GUIDELINES +$STOP_CONDITIONS # Definition of done Write your findings to \`$DATA/$ID/report.md\`. @@ -412,7 +413,7 @@ $RULE1 every lane/home, so restarting it kills other lanes' in-flight pipeline runs. On ANY no-mistakes daemon error, append \`blocked: {the daemon error}\` and stop; only firstmate manages the daemon. -$GUIDELINES +$STOP_CONDITIONS # Project memory If \`AGENTS.md\` or \`CLAUDE.md\` already exists, or if this task produced durable project-intrinsic knowledge, run \`$FM_ROOT/bin/fm-ensure-agents-md.sh .\` in the worktree. diff --git a/docs/architecture.md b/docs/architecture.md index 4afd0daa6..c42c58e5a 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -123,7 +123,7 @@ The helper's header owns the exact signal detection, relocated-home limitation, ## Two task shapes Ship tasks change projects and ship by project mode (`no-mistakes`, `direct-PR`, or `local-only`); scout tasks investigate, plan, reproduce bugs, or audit, then leave a report at `data//report.md` and never push. -Both task shapes receive the advisory guidelines owned by `fm-brief.sh`, and an optional `--guideline ''` adds task-specific guidance for unattended or long-running work. +Both task shapes receive the advisory stop conditions owned by `fm-brief.sh`, and an optional `--stop-condition ''` adds a task-specific stop condition for unattended or long-running work. ## Dispatch profiles diff --git a/docs/scripts.md b/docs/scripts.md index bbf87b87f..30aa884ee 100644 --- a/docs/scripts.md +++ b/docs/scripts.md @@ -17,7 +17,7 @@ The shared no-mistakes gate refusal for fleet lifecycle entrypoints is summarize | `fm-update.sh` | Fast-forward-only self-update of firstmate and secondmate homes from origin | | `fm-backlog-handoff.sh` | Validate and delegate queued backlog-item moves into a secondmate home | | `fm-decision-hold.sh` | Create, verify, complete, and resolve durable captain-held decisions | -| `fm-brief.sh` | Scaffold ship and scout briefs with standard guidelines and optional task-specific guidance, plus secondmate-charter and Herdr-lab briefs | +| `fm-brief.sh` | Scaffold ship and scout briefs with standard and task-specific advisory stop conditions, plus secondmate-charter and Herdr-lab briefs | | `fm-herdr-lab.sh` | Provision and guardedly operate an isolated, never-default Herdr lab session | | `fm-ensure-agents-md.sh` | Ensure a project's real `AGENTS.md`, its `CLAUDE.md` symlink, and the canonical self-governance section | | `fm-guard.sh` | Warn on primary-checkout tangles, pending queued wakes, and stale watcher liveness | diff --git a/tests/fm-brief.test.sh b/tests/fm-brief.test.sh index 557bd4e32..ab3be69ba 100755 --- a/tests/fm-brief.test.sh +++ b/tests/fm-brief.test.sh @@ -33,10 +33,10 @@ test_help_includes_entire_header() { local help status=0 help=$("$ROOT/bin/fm-brief.sh" --help 2>&1) || status=$? expect_code 0 "$status" "fm-brief.sh --help" - assert_contains "$help" "[--guideline '']" \ - "fm-brief.sh --help omitted --guideline from usage" - assert_contains "$help" "--guideline '' adds task-specific advisory guidance" \ - "fm-brief.sh --help omitted --guideline documentation" + assert_contains "$help" "[--stop-condition '']" \ + "fm-brief.sh --help omitted --stop-condition from usage" + assert_contains "$help" "--stop-condition '' adds a task-specific advisory stop condition" \ + "fm-brief.sh --help omitted --stop-condition documentation" assert_contains "$help" "Refuses to overwrite an existing brief." "fm-brief.sh --help omitted its header terminator" pass "fm-brief.sh: --help includes the complete header" } @@ -139,13 +139,13 @@ test_ship_project_memory_wording() { pass "fm-brief.sh: ship project-memory wording carries the AGENTS.md authoring bar" } -test_guidelines_default_for_ship_and_scout() { +test_stop_conditions_default_for_ship_and_scout() { local home id brief - home="$TMP_ROOT/guidelines-default-home" + home="$TMP_ROOT/stop-conditions-default-home" mkdir -p "$home/data" for kind in ship scout; do - id="brief-guideline-default-$kind" + id="brief-stop-condition-default-$kind" if [ "$kind" = scout ]; then FM_HOME="$home" "$ROOT/bin/fm-brief.sh" "$id" firstmate --scout >/dev/null 2>&1 else @@ -153,60 +153,62 @@ test_guidelines_default_for_ship_and_scout() { fi brief="$home/data/$id/brief.md" assert_grep "# Rules" "$brief" "$kind brief lost the Rules section" - assert_grep "## Guidelines" "$brief" "$kind brief missing the guidelines block" + assert_grep "## Stop conditions" "$brief" "$kind brief missing the stop conditions block" + assert_grep "These stop conditions are advisory guidance evaluated by the crewmate, not deterministically enforced limits." "$brief" \ + "$kind brief did not identify the stop conditions as advisory" assert_grep "Expected waiting on a validation run or long test suite does not count as a no-phase-change stretch." "$brief" \ - "$kind brief treats healthy validation waiting as reaching the guideline" + "$kind brief treats healthy validation waiting as reaching a stop condition" assert_grep "If roughly two hours or 50 significant actions pass without a supervisor-actionable phase change" "$brief" \ - "$kind brief missing the default action and time guideline" - assert_grep "blocked: guideline - {one-line progress + what remains}" "$brief" \ - "$kind brief missing the guideline status protocol" + "$kind brief missing the default action and time stop condition" + assert_grep "blocked: stop condition - {one-line progress + what remains}" "$brief" \ + "$kind brief missing the stop-condition status protocol" assert_grep "blocked: scope - {one-line progress + newly discovered work}" "$brief" \ - "$kind brief missing the scope-expansion guideline" + "$kind brief missing the scope-expansion stop condition" assert_grep "# Definition of done" "$brief" "$kind brief lost the Definition of done section" - assert_no_grep "Task-specific guideline:" "$brief" "$kind brief rendered task-specific guidance without --guideline" + assert_no_grep "Task-specific stop condition:" "$brief" "$kind brief rendered a task-specific stop condition without --stop-condition" done - pass "fm-brief.sh: ship and scout briefs include the standard guidelines" + pass "fm-brief.sh: ship and scout briefs include the standard stop conditions" } -test_task_specific_guideline_for_ship_and_scout() { - local home id brief guideline - home="$TMP_ROOT/task-specific-guideline-home" - guideline="check in after 30 minutes" +test_task_specific_stop_condition_for_ship_and_scout() { + local home id brief stop_condition + home="$TMP_ROOT/task-specific-stop-condition-home" + stop_condition="check in after 30 minutes" mkdir -p "$home/data" for kind in ship scout; do - id="brief-task-guideline-$kind" + id="brief-task-stop-condition-$kind" if [ "$kind" = scout ]; then - FM_HOME="$home" "$ROOT/bin/fm-brief.sh" "$id" firstmate --scout --guideline "$guideline" >/dev/null 2>&1 + FM_HOME="$home" "$ROOT/bin/fm-brief.sh" "$id" firstmate --scout --stop-condition "$stop_condition" >/dev/null 2>&1 else - FM_HOME="$home" "$ROOT/bin/fm-brief.sh" "$id" firstmate --guideline "$guideline" >/dev/null 2>&1 + FM_HOME="$home" "$ROOT/bin/fm-brief.sh" "$id" firstmate --stop-condition "$stop_condition" >/dev/null 2>&1 fi brief="$home/data/$id/brief.md" - assert_grep "Task-specific guideline: $guideline" "$brief" "$kind brief omitted its --guideline text" - assert_grep "Treat this as advisory guidance alongside the standard guidelines." "$brief" \ - "$kind brief did not describe --guideline as advisory" - assert_grep "blocked: guideline - {one-line progress + what remains}" "$brief" \ - "$kind brief lost the guideline status protocol" + assert_grep "Task-specific stop condition: $stop_condition" "$brief" "$kind brief omitted its --stop-condition text" + assert_grep "Treat this as advisory guidance alongside the standard stop conditions." "$brief" \ + "$kind brief did not describe --stop-condition as advisory" + assert_grep "blocked: stop condition - {one-line progress + what remains}" "$brief" \ + "$kind brief lost the stop-condition status protocol" done - pass "fm-brief.sh: --guideline renders as advisory guidance in ship and scout briefs" + pass "fm-brief.sh: --stop-condition renders as advisory guidance in ship and scout briefs" } -test_guideline_rejects_recognized_option_as_value() { +test_stop_condition_rejects_recognized_option_as_value() { local home id brief option status output - home="$TMP_ROOT/guideline-option-home" + home="$TMP_ROOT/stop-condition-option-home" mkdir -p "$home/data" - for option in -h --help --scout --secondmate --herdr-lab --no-projects --guideline; do - id="brief-guideline-option-${option//-/}" + for option in -h --help --scout --secondmate --herdr-lab --no-projects --stop-condition; do + id="brief-stop-condition-option-${option//-/}" status=0 - output=$(FM_HOME="$home" "$ROOT/bin/fm-brief.sh" "$id" firstmate --guideline "$option" 2>&1) || status=$? - expect_code 1 "$status" "--guideline must reject $option as a missing value" - assert_contains "$output" "error: --guideline requires non-empty text" \ - "--guideline $option returned the wrong error" + output=$(FM_HOME="$home" "$ROOT/bin/fm-brief.sh" "$id" firstmate --stop-condition "$option" 2>&1) || status=$? + expect_code 1 "$status" "--stop-condition must reject $option as a missing value" + assert_contains "$output" "error: --stop-condition requires non-empty text" \ + "--stop-condition $option returned the wrong error" brief="$home/data/$id/brief.md" - assert_absent "$brief" "--guideline $option still scaffolded a brief" + assert_absent "$brief" "--stop-condition $option still scaffolded a brief" done - pass "fm-brief.sh: --guideline rejects recognized options as its value" + pass "fm-brief.sh: --stop-condition rejects recognized options as its value" } test_ship_and_scout_reject_excess_positionals() { @@ -218,9 +220,9 @@ test_ship_and_scout_reject_excess_positionals() { id="brief-excess-positionals-$kind" status=0 if [ "$kind" = scout ]; then - output=$(FM_HOME="$home" "$ROOT/bin/fm-brief.sh" "$id" firstmate --scout --guideline check in after 30 minutes 2>&1) || status=$? + output=$(FM_HOME="$home" "$ROOT/bin/fm-brief.sh" "$id" firstmate --scout --stop-condition check in after 30 minutes 2>&1) || status=$? else - output=$(FM_HOME="$home" "$ROOT/bin/fm-brief.sh" "$id" firstmate --guideline check in after 30 minutes 2>&1) || status=$? + output=$(FM_HOME="$home" "$ROOT/bin/fm-brief.sh" "$id" firstmate --stop-condition check in after 30 minutes 2>&1) || status=$? fi expect_code 1 "$status" "$kind brief must reject excess positional arguments" assert_contains "$output" "error: ship and scout briefs accept only and as positional arguments" \ @@ -442,9 +444,9 @@ test_ship_modes_generate_clean_briefs test_faster_paths_use_configured_authority_without_stacked_review test_no_mistakes_dod_wording test_ship_project_memory_wording -test_guidelines_default_for_ship_and_scout -test_task_specific_guideline_for_ship_and_scout -test_guideline_rejects_recognized_option_as_value +test_stop_conditions_default_for_ship_and_scout +test_task_specific_stop_condition_for_ship_and_scout +test_stop_condition_rejects_recognized_option_as_value test_ship_and_scout_reject_excess_positionals test_herdr_lab_contract_is_explicit_and_complete test_herdr_lab_contract_quotes_foreign_firstmate_path