diff --git a/AGENTS.md b/AGENTS.md index 4f37db5cd..c6093d99c 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -425,7 +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, status protocol, delivery-mode definitions of done, and exact safety mechanics. +`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/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-brief.sh b/bin/fm-brief.sh index 56f3008d3..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] +# 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,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. +# --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): @@ -57,6 +59,13 @@ usage() { ' "$0" } +is_option_token() { + case "${1:-}" in + -h|--help|--scout|--secondmate|--herdr-lab|--no-projects|--stop-condition) return 0 ;; + *) return 1 ;; + esac +} + case "${1:-}" in -h|--help) usage; exit 0 ;; esac @@ -73,16 +82,32 @@ STATE="${FM_STATE_OVERRIDE:-$FM_HOME/state}" KIND=ship HERDR_LAB=0 NO_PROJECTS=0 +STOP_CONDITION="" 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") ;; + --stop-condition) + if [ "$#" -lt 2 ] || [ -z "$2" ] || is_option_token "$2"; then + echo "error: --stop-condition requires non-empty text" >&2 + exit 1 + fi + STOP_CONDITION=$2 + shift + ;; + *) POS+=("$1") ;; 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 @@ -90,6 +115,11 @@ if [ "$KIND" = secondmate ] && [ "$HERDR_LAB" -eq 1 ]; then exit 1 fi +if [ "$KIND" = secondmate ] && [ -n "$STOP_CONDITION" ]; then + echo "error: --stop-condition 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 +219,21 @@ fi REPO=${POS[1]} +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, 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 "$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 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 +303,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 +413,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/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:]]}"}" diff --git a/bin/fm-spawn.sh b/bin/fm-spawn.sh index fc177354b..722cd5aae 100755 --- a/bin/fm-spawn.sh +++ b/bin/fm-spawn.sh @@ -843,11 +843,32 @@ 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 - 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 + # 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 diff --git a/docs/architecture.md b/docs/architecture.md index 23ff9de20..c42c58e5a 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 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/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/docs/scripts.md b/docs/scripts.md index 5a4cc0f0e..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, scout, 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-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 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-brief.test.sh b/tests/fm-brief.test.sh index 645e451c2..ab3be69ba 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" "[--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 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 @@ -134,6 +139,100 @@ 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-condition-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 "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 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 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 stop condition" + assert_grep "# Definition of done" "$brief" "$kind brief lost the Definition of done section" + 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 stop conditions" +} + +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-stop-condition-$kind" + if [ "$kind" = scout ]; then + 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 --stop-condition "$stop_condition" >/dev/null 2>&1 + fi + brief="$home/data/$id/brief.md" + 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: --stop-condition renders as advisory guidance in ship and scout briefs" +} + +test_stop_condition_rejects_recognized_option_as_value() { + local home id brief option status output + home="$TMP_ROOT/stop-condition-option-home" + mkdir -p "$home/data" + + 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 --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" "--stop-condition $option still scaffolded a brief" + done + pass "fm-brief.sh: --stop-condition 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 --stop-condition check in after 30 minutes 2>&1) || status=$? + else + 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" \ + "$kind brief returned the wrong excess-positionals error" + brief="$home/data/$id/brief.md" + assert_absent "$brief" "$kind brief silently scaffolded with truncated unquoted guidance" + 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" @@ -345,6 +444,10 @@ 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_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 test_herdr_lab_omission_is_loud_for_ship_and_scout 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'