diff --git a/.agents/skills/captain-hold-lifecycle/SKILL.md b/.agents/skills/captain-hold-lifecycle/SKILL.md index eaea0c27411..13f2faf1082 100644 --- a/.agents/skills/captain-hold-lifecycle/SKILL.md +++ b/.agents/skills/captain-hold-lifecycle/SKILL.md @@ -17,7 +17,7 @@ The agent performs the semantic inventory because scripts must not infer captain ## Policy Every unresolved question that belongs to the captain and is discovered while producing, reading, presenting, or ending an investigation or visual review must be carried by a captain-held task in the authoritative backlog of the home that owns the originating work before that work or review may be treated as complete. -For a Lavish board-backed handoff, pass the reply through `bin/fm-procevent-lavish.sh arm --agent-reply-file` before appending the status; the adapter owns version-specific acceptance ordering. +For a Lavish board-backed or Nextrade app-backed handoff, pass the reply through the corresponding `bin/fm-procevent-lavish.sh` or `bin/fm-procevent-nextrade.sh` `arm --agent-reply-file` before appending the status; the adapter owns acceptance ordering. Prefer holding the work item the question gates over minting a new row; create a new task only when no work item exists to hold. Put the question and its options in the hold reason, and keep one held task per genuine gate: a multi-question review is one held task pointing at its report, not a row per question. Represent that task with exactly one board card that consolidates its questions and options; never fan one task id into duplicate same-key cards. Register or re-hold through `bin/fm-captain-hold.sh hold`, which is idempotent per task id. @@ -35,6 +35,8 @@ When the answer changes what a task must build, follow `AGENTS.md` section 7's m When the captain says "later", that is an answer too: re-hold with `bin/fm-captain-hold.sh hold --reason "" --until ` so the item leaves the live Captain's Call and resurfaces on its date, instead of leaving a live-looking card or fabricating a closure. "A keyed answer resolves its matching captain-held task" is one capability with one owner, `bin/fm-captain-hold.sh answers`, and every channel that carries a captain answer feeds it the same task id and answer; a channel never maps keys to tasks, records a decision, or resolves anything itself. Chat already feeds it through `bin/fm-send.sh --resolve-key`, and a captured-answer source feeds it once bound with `bin/fm-captain-hold.sh bind `; bind before arming the source, and key each structured question by the held task's id. +For an app-backed review, create the Nextrade experiment decision with that task id as its `key` and the intended `close_mode`, then bind the source identified by `bin/fm-procevent-nextrade.sh source-id --for ` and arm it for the hosting task. +The Nextrade adapter's header owns task-target resolution and CLI syntax; it reports already-answered app decisions and never answers one itself. An unbound source and a key that names no captain-held task both simply feed nothing: the answer is still captured and firstmate is still woken, and closing falls back to the direct command above. One answer value is reserved and closes nothing: `reconcile` means "go re-check reality", never "the captain answered", so the shared intake refuses it from every channel and creates nothing. A bound captured source uses a separate seam: its adapter omits reconcile from keyed answers and emits the selected task id through `reconciles`, the generic runner feeds that into `reconcile-requests`, and the intake verifies the source binding and the local captain-held task before filing the durable board request. diff --git a/.agents/skills/process-event-sources/SKILL.md b/.agents/skills/process-event-sources/SKILL.md index 999834abbde..4820970d28b 100644 --- a/.agents/skills/process-event-sources/SKILL.md +++ b/.agents/skills/process-event-sources/SKILL.md @@ -34,13 +34,14 @@ bin/fm-procevent-lavish.sh arm ``` A worker-owned board uses `bin/fm-procevent-lavish.sh arm --for ` and re-arms with its reply after each nonterminal round; the existing handled marker is the acknowledgement. +A Nextrade app-backed review uses `bin/fm-procevent-nextrade.sh`; its header owns experiment arming and task-target resolution, and [the operating contract](../../../docs/configuration.md#nextrade-app-backed-reviews) records its shared runner boundary. Arm it once, then re-arm only when a round is actually waiting: arming again with nothing to acknowledge is refused. A terminal round is never re-armed: the board stays yours until you acknowledge it with `bin/fm-procevent.sh handled `, which retires it, and until then `retire` refuses the board too. Never arm a board that a live task hosts; follow the [crew-hosted Lavish board contract](../../../docs/configuration.md#crew-hosted-lavish-review-boards) for reply acceptance and older-version limits. Registering a source is not the same fact as listening to it. -Lavish `arm` waits until this registration's listener is confirmed running and does not report ready without that evidence; other adapters still record the source for the watcher's next reconcile. -When an earlier registration's listener still holds the board as the confirm window ends, Lavish `arm` prints `still-listening` instead of `armed`; that listener keeps serving the board, and the new registration takes effect only after you retire the source and arm it again. +Lavish and Nextrade `arm` wait until this registration's listener is confirmed running and do not report ready without that evidence; other adapters still record the source for the watcher's next reconcile. +When an earlier registration's listener still holds the source as the confirm window ends, those adapters print `still-listening` instead of `armed`; that listener keeps serving the source, and the new registration takes effect only after you retire the source and arm it again. After arming by hand, confirm `bin/fm-procevent.sh list` reports that source as `live`, and run `bin/fm-procevent.sh reconcile` when it does not. Reconcile reports every launch that did not prove it took its claim within the confirm window as `failed=` and exits non-zero, so a source that cannot be started says so instead of looking armed, and it wakes you once per failure episode about it because the watcher discards that count; `start` does not fix that - if the source stays unowned, run `start` attached to read the runner's refusal, then check the source command and adapter binary the registration names, and if a later reconcile finds the source owned the episode closes on its own. A source `list` reports as `orphaned` is one reconcile will not relaunch, because something may still be polling it; reconcile wakes you once about it, and that wake's payload says which of two recoveries applies. @@ -82,7 +83,7 @@ Eligibility is a firstmate judgment made BEFORE arming, because the scripts cann Never bind an action that is destructive, irreversible, or security-sensitive, an action needing captain approval or any gate decision, or an action whose right form depends on what the condition finds - those keep the existing check-fires-then-firstmate-decides flow, for which a plain custom check or another adapter stays correct. When in doubt, arm only the condition half as an ordinary check and keep the action as a wake-time decision. -`bin/fm-procevent.sh --help`, `bin/fm-procevent-lavish.sh --help`, `bin/fm-procevent-when.sh --help`, `bin/fm-procevent-quota.sh --help`, and `bin/fm-procevent-remote-reply.sh --help` own the exact commands and flags. +`bin/fm-procevent.sh --help`, `bin/fm-procevent-lavish.sh --help`, `bin/fm-procevent-nextrade.sh --help`, `bin/fm-procevent-when.sh --help`, `bin/fm-procevent-quota.sh --help`, and `bin/fm-procevent-remote-reply.sh --help` own the exact commands and flags. An explicitly enabled external adapter registers through `bin/fm-procevent.sh register-extension`, never through a package-discovered script or package-supplied argv. [`docs/configuration.md`](../../../docs/configuration.md#trusted-external-process-event-adapters-configextensionsd) owns setup and [`docs/extension-bindings.md`](../../../docs/extension-bindings.md) owns the narrow trusted-code and untrusted-evidence boundary. @@ -114,6 +115,7 @@ Two rules the commands cannot enforce for you: : Ask the adapter what the result means rather than parsing it yourself. `bin/fm-procevent.sh classify ` routes through the immutable built-in or extension identity captured with that result; for Lavish, its existing direct command returns `feedback`, `ended`, `waiting`, `disconnected`, `missing`, or `unknown`. Consume a Lavish capture with `bin/fm-procevent-lavish.sh read ` rather than grepping the raw file: that command reports declared and presented item counts plus a completeness verdict, enumerates every captured queued item while retaining supplied element identity, and surfaces a `tag=message` freeform message as its own field, labeling it as session-ending only when the session ended. + Consume a Nextrade capture with `bin/fm-procevent-nextrade.sh read ` to present every delivered message and decision before concluding or re-arming the app review. `answers` remains the keyed-choice extractor and never treats freeform prose as a decision key. A `feedback` result can still be the last one a review ever produces, so never assume another wake is coming just because the state is not `ended`. The crew-hosted recovery ordering and arm-and-acknowledge rule are owned by the [crew-hosted Lavish board contract](../../../docs/configuration.md#crew-hosted-lavish-review-boards); `bin/fm-brief.sh` emits its instruction at the point of use. diff --git a/bin/fm-procevent-nextrade.sh b/bin/fm-procevent-nextrade.sh new file mode 100755 index 00000000000..cd86be392b6 --- /dev/null +++ b/bin/fm-procevent-nextrade.sh @@ -0,0 +1,235 @@ +#!/usr/bin/env bash +# Nextrade experiment adapter for the generic process-to-event runner. +# +# Usage: +# fm-procevent-nextrade.sh arm --for [--agent-reply-file ] +# fm-procevent-nextrade.sh classify|terminal|silent|answers|reconciles|read +# fm-procevent-nextrade.sh source-id|retire [--for ] +# fm-procevent-nextrade.sh poll [--worktree --target ] [--agent-reply-file ] +# fm-procevent-nextrade.sh deliver-reply poll --worktree --target --agent-reply-file +# +# arm resolves the task's worktree metadata and its nearest .nextrade-axi/target. +# The listener retains that worktree and explicit target, never the supervisor's +# ambient selection. source-id and retire use the same task context with --for, +# or the current directory without it. Identity includes the target name and +# experiment UUID; dev also includes the physical worktree because each dev +# stack is independent. Named remote targets retain nextrade-axi's host config. +# +# poll is the registered blocking command, not a conversational-turn command. +# It runs nextrade-axi experiment poll --owner firstmate --json. The CLI +# owns the server wait and listener lease; this adapter keeps no cursor, takes +# over no lease, and adds no retry loop. deliver-reply posts a staged reply with +# experiment reply --file synchronously under the generic registration lock. +# This is the only write command: the adapter never answers or consumes a decision. +# +# classify prints feedback, ended, waiting, missing, or unknown. terminal +# succeeds for end=true (including a final answer batch) or HTTP_404. silent +# succeeds only for a valid empty batch. Unknown or malformed output is announced. +# read presents all messages and decisions with prefixed untrusted body lines. +# answers emits keyselectionlabelclose_mode from captain-answered +# decisions only; a free-text-only answer uses answer_text, and a selection's +# answer_text is retained as a note. label is the selected option's visible label. +# reconcile selections use reconciles, never answers. Neither command checks or +# mutates held tasks: binding and keyed-answer intake belong to fm-captain-hold.sh. +# Ownership, capture, wake delivery, re-arm and terminal acknowledgement belong +# to fm-procevent.sh. Server delivery marks can advance before local capture; +# this adapter provides no source-side no-loss guarantee. +set -u + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +FM_ROOT="${FM_ROOT_OVERRIDE:-$(cd "$SCRIPT_DIR/.." && pwd)}" +FM_HOME="${FM_HOME:-${FM_ROOT_OVERRIDE:-$FM_ROOT}}" +STATE="${FM_STATE_OVERRIDE:-$FM_HOME/state}" + +# shellcheck source=bin/fm-pr-lib.sh +. "$SCRIPT_DIR/fm-pr-lib.sh" + +die() { printf 'error: %s\n' "$1" >&2; exit 1; } +usage() { sed -n '2,/^set -u$/p' "${BASH_SOURCE[0]}" | sed '$d; s/^# \{0,1\}//'; exit "${1:-2}"; } + +context() { # [task-id]; sets worktree and target + local task=${1-} dir + worktree=$(pwd -P) + if [ -n "$task" ]; then + fm_pr_task_id_valid "$task" || die "invalid task id: $task" + [ -f "$STATE/$task.meta" ] && [ ! -L "$STATE/$task.meta" ] || die "missing task metadata: $task" + worktree=$(awk '/^worktree=/ { sub(/^worktree=/, ""); print; exit }' "$STATE/$task.meta") + [ -n "$worktree" ] || die "task has no worktree: $task" + worktree=$(cd "$worktree" && pwd -P) || die "task worktree is unavailable: $task" + fi + dir=$worktree + target='' + while :; do + if [ -f "$dir/.nextrade-axi/target" ]; then + target=$(cat "$dir/.nextrade-axi/target") + break + fi + [ "$dir" != / ] || break + dir=$(dirname "$dir") + done + case "$target" in ''|*[!A-Za-z0-9._-]*) die "no valid Nextrade target selected in task context" ;; esac +} + +source_id() { # ; uses resolved context + local experiment=$1 scope=$target + [[ "$experiment" =~ ^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$ ]] \ + || die "experiment id must be a UUID" + [ "$target" != dev ] || scope="$scope:$worktree" + perl -MDigest::SHA=sha256_hex -e 'print "nextrade-", substr(sha256_hex(lc($ARGV[0])."\n".$ARGV[1]),0,32), "\n"' "$experiment" "$scope" +} + +cmd_arm() { + local experiment=${1-} task='' reply='' id listening=0 owner + local worktree target + [ -n "$experiment" ] || usage + shift + while [ "$#" -gt 0 ]; do + [ "$#" -ge 2 ] || usage + case "$1" in + --for) [ -z "$task" ] || usage; task=$2 ;; + --agent-reply-file) [ -z "$reply" ] || usage; reply=$2 ;; + *) usage ;; + esac + shift 2 + done + [ -n "$task" ] || usage + context "$task" + id=$(source_id "$experiment") || exit 1 + command -v nextrade-axi >/dev/null 2>&1 || die "nextrade-axi is not installed" + local -a listener=("$SCRIPT_DIR/fm-procevent-nextrade.sh" poll "$experiment" --worktree "$worktree" --target "$target") + [ -z "$reply" ] || listener+=(--agent-reply-file "$reply") + "$SCRIPT_DIR/fm-procevent.sh" register-task nextrade "$id" "$task" -- "${listener[@]}" || exit 1 + "$SCRIPT_DIR/fm-procevent.sh" ensure-listening "$id" || listening=$? + if [ "$listening" -eq 3 ]; then + printf 'still-listening: %s\n' "$id" + elif [ "$listening" -eq 0 ]; then + printf 'armed: %s\n' "$id" + else + owner=$("$SCRIPT_DIR/fm-procevent.sh" list 2>/dev/null | awk -v id="$id" '$1 == id { print $3; exit }') + case "$owner" in + live|orphaned|task:*/listening|task:*/round-open) ;; + *) "$SCRIPT_DIR/fm-procevent.sh" retire "$id" >/dev/null 2>&1 || true ;; + esac + return 1 + fi + printf 'experiment: %s\nowner-task: %s\n' "$experiment" "$task" +} + +cmd_poll() { # [listener options] + local action=$1 experiment=${2-} reply='' worktree='' target='' + [ -n "$experiment" ] || usage + shift 2 + while [ "$#" -gt 0 ]; do + [ "$#" -ge 2 ] || usage + case "$1" in + --worktree) [ -z "$worktree" ] || usage; worktree=$2 ;; + --target) [ -z "$target" ] || usage; target=$2 ;; + --agent-reply-file) [ -z "$reply" ] || usage; reply=$2 ;; + *) usage ;; + esac + shift 2 + done + if [ -z "$worktree" ] && [ -z "$target" ]; then context; fi + [ -n "$worktree" ] && [ -n "$target" ] || usage + source_id "$experiment" >/dev/null || exit 1 + if [ "$action" = reply ]; then + [ -n "$reply" ] || usage + local output + if ! output=$(cd "$worktree" && nextrade-axi experiment reply "$experiment" --target "$target" --file "$reply" 2>&1); then + die "Nextrade did not accept the staged reply: $output" + fi + else + local -a argv=(experiment poll "$experiment" --target "$target" --owner firstmate --json) + [ -z "$reply" ] || argv+=(--agent-reply-file "$reply") + (cd "$worktree" && exec nextrade-axi "${argv[@]}") + fi +} + +cmd_result() { # + [ "$#" -eq 2 ] || usage + [ -f "$2" ] && [ ! -L "$2" ] || die "result file does not exist: $2" + perl -MJSON::PP -e ' + use strict; use warnings; + binmode STDOUT, ":encoding(UTF-8)"; + my ($op, $file) = @ARGV; + open my $fh, "<", $file or die $!; local $/; my $raw = <$fh>; + my $data = eval { decode_json($raw) }; + my $valid = ref($data) eq "HASH" && ref($data->{messages}) eq "ARRAY" + && ref($data->{decisions_answered}) eq "ARRAY" && JSON::PP::is_bool($data->{end}); + if (!$valid) { + my $missing = $raw =~ /\Aerror:[^\n]*\ncode: HTTP_404\s*(?:\n|\z)/; + if ($op eq "classify") { print $missing ? "missing\n" : "unknown\n"; exit 0 } + exit($missing ? 0 : 1) if $op eq "terminal"; + exit 1 if $op eq "silent"; + die "invalid Nextrade inbox result\n"; + } + my $content = @{$data->{messages}} || @{$data->{decisions_answered}}; + if ($op eq "classify") { print $content ? "feedback\n" : $data->{end} ? "ended\n" : "waiting\n"; exit } + exit($data->{end} ? 0 : 1) if $op eq "terminal"; + exit($content ? 1 : 0) if $op eq "silent"; + sub scalar_text { defined($_[0]) && !ref($_[0]) } + sub field { my $s = scalar_text($_[0]) ? $_[0] : ""; $s =~ s/[\x00-\x1f\x7f]/ /g; return $s } + if ($op eq "answers" || $op eq "reconciles") { + my (@rows, %seen); + for my $d (@{$data->{decisions_answered}}) { + next unless ref($d) eq "HASH" && field($d->{status}) eq "answered" && field($d->{answered_by}) eq "captain"; + my ($key, $selection, $note, $mode) = map { $d->{$_} } qw(key selection answer_text close_mode); + next unless scalar_text($key) && $key =~ /\A[A-Za-z0-9._-]{1,64}\z/; + next unless scalar_text($mode) && ($mode eq "done" || $mode eq "release"); + next if defined($selection) && !scalar_text($selection) || defined($note) && !scalar_text($note); + my $label = ""; + if (defined($selection) && length($selection)) { + next unless ref($d->{options}) eq "ARRAY"; + my @options = grep { ref($_) eq "HASH" && scalar_text($_->{value}) && $_->{value} eq $selection } @{$d->{options}}; + next unless @options == 1 && scalar_text($options[0]->{label}); + $label = $options[0]->{label}; + } + my $answer = field($selection); + $answer .= (length($answer) ? " - " : "") . field($note) if defined($note) && length($note); + next unless length($answer); + $rows[$seen{$key}] = undef if exists $seen{$key}; + $seen{$key} = scalar @rows; + push @rows, { key => $key, selection => $selection // "", answer => $answer, label => field($label), mode => $mode }; + } + for my $r (grep { defined } @rows) { + if ($op eq "reconciles") { print "$r->{key}\n" if $r->{selection} eq "reconcile" } + elsif ($r->{selection} ne "reconcile" && $r->{answer} ne "reconcile") { print join("\t", @$r{qw(key answer label mode)}), "\n" } + } + exit; + } + sub body { my $s = scalar_text($_[0]) ? $_[0] : ""; $s =~ s/[\x00-\x08\x0b-\x1f\x7f]/ /g; print " | $_\n" for split /\n/, $s, -1 } + my $json = JSON::PP->new; + print "end: ", $data->{end} ? "true\n" : "false\n"; + print "messages: ", scalar @{$data->{messages}}, "\n"; + for my $m (@{$data->{messages}}) { + ref($m) eq "HASH" or die "invalid message\n"; + print "message: ", join(" ", map { field($m->{$_}) } qw(id kind author)), "\n"; + body($m->{body}); + print "refs: ", $json->encode($m->{refs}), "\n"; + } + print "decisions_answered: ", scalar @{$data->{decisions_answered}}, "\n"; + for my $d (@{$data->{decisions_answered}}) { + ref($d) eq "HASH" or die "invalid decision\n"; + print "decision: ", join(" ", map { field($d->{$_}) } qw(id key status close_mode)), "\n"; + for my $name (qw(title explanation example selection answer_text)) { print "$name:\n"; body($d->{$name}) } + print "options: ", $json->encode($d->{options}), "\n"; + } + ' "$@" +} + +command=${1-} +for arg in "$@"; do [ "$arg" != --help ] || usage 0; done +[ "$#" -gt 0 ] && shift +case "$command" in + arm) cmd_arm "$@" ;; + poll) cmd_poll poll "$@" ;; + deliver-reply) [ "${1-}" = poll ] || usage; shift; cmd_poll reply "$@" ;; + classify|terminal|silent|answers|reconciles|read) cmd_result "$command" "$@" ;; + source-id|retire) + [ "$#" -eq 1 ] || { [ "$#" -eq 3 ] && [ "$2" = --for ]; } || usage + context "${3-}" + id=$(source_id "$1") || exit 1 + if [ "$command" = source-id ]; then printf '%s\n' "$id"; else "$SCRIPT_DIR/fm-procevent.sh" retire "$id"; fi + ;; + *) usage ;; +esac diff --git a/bin/fm-procevent.sh b/bin/fm-procevent.sh index 3b6bfc3affd..c35f3f96abe 100755 --- a/bin/fm-procevent.sh +++ b/bin/fm-procevent.sh @@ -557,7 +557,7 @@ cmd_register_task() { local pending_rounds=0 delivered local -a argv=() kept=() shift 4 2>/dev/null || usage - [ "$adapter" = lavish ] || die "register-task is reserved for the Lavish adapter" + case "$adapter" in lavish|nextrade) ;; *) die "register-task is reserved for the Lavish and Nextrade adapters" ;; esac fm_procevent_adapter_valid "$adapter" || die "adapter name must be lowercase alphanumeric or dash: $adapter" fm_procevent_source_id_valid "$id" || die "source id must be path-safe and at most 64 characters: $id" fm_pr_task_id_valid "$task" || die "task id is invalid: $task" @@ -809,19 +809,21 @@ cmd_register_extension() { # and drains until `fm_procevent_mark_handled` records it. publish_result() { # local result=$1 id seq adapter line status=1 owner_task='' message='' record='' + local review_label=Lavish review_surface=board local ring_backend ring_target ring_meta inbox_dir handled_dir pre_existing existing new_record id=$(fm_procevent_result_source_id "$result") seq=$(fm_procevent_result_sequence "$result") fm_procevent_source_id_valid "$id" || return 1 adapter=$(fm_procevent_result_adapter "$result" 2>/dev/null || true) [ -n "$adapter" ] || return 1 + if [ "$adapter" = nextrade ]; then review_label=Nextrade; review_surface=review; fi line=$(fm_procevent_event_line "$adapter" "$id" "$seq") || return 1 owner_task=$(fm_procevent_result_owner_task "$result" 2>/dev/null || true) fm_procevent_source_lock_acquire "$id" || return 1 if ! fm_procevent_is_handled "$STATE" "$id" "$seq"; then if [ -n "$owner_task" ]; then if adapter_result_is_terminal "$adapter" "$result"; then - message="Lavish review result $id sequence $seq is terminal at $result. Read it with bin/fm-procevent-lavish.sh read $result, stop and conclude the review, and do not re-arm the board. The board stays yours until you acknowledge this round with bin/fm-procevent.sh handled $id $seq, which retires it." + message="$review_label review result $id sequence $seq is terminal at $result. Read it with bin/fm-procevent-$adapter.sh read $result, stop and conclude the review, and do not re-arm the $review_surface. The $review_surface stays yours until you acknowledge this round with bin/fm-procevent.sh handled $id $seq, which retires it." else export FM_PROCEVENT_CAPTURE_SOURCE_LOCK_HELD=1 if adapter_result_is_silent "$adapter" "$result"; then @@ -835,7 +837,7 @@ publish_result() { # esac fi unset FM_PROCEVENT_CAPTURE_SOURCE_LOCK_HELD - message="Lavish review feedback is captured for task $owner_task at $result. Read it with bin/fm-procevent-lavish.sh read $result, apply the round, and re-arm the board with the reply." + message="$review_label review feedback is captured for task $owner_task at $result. Read it with bin/fm-procevent-$adapter.sh read $result, apply the round, and re-arm the $review_surface with the reply." fi # Snapshot the records that already exist (active and handled) before # the idempotent write, so a dedup match - including one already diff --git a/bin/fm-test-run.sh b/bin/fm-test-run.sh index dfff544fbdf..3ccdba99984 100755 --- a/bin/fm-test-run.sh +++ b/bin/fm-test-run.sh @@ -423,7 +423,7 @@ family_for_basename() { fm-extension-binding.test.sh|fm-gitignore-config.test.sh|\ fm-no-mistakes-required.test.sh|fm-peek-remote.test.sh|\ fm-pending-reply.test.sh|fm-pi-branch-extension.test.sh|\ - fm-procevent-quota.test.sh|fm-procevent-when.test.sh|fm-procevent.test.sh|\ + fm-procevent-quota.test.sh|fm-procevent-nextrade.test.sh|fm-procevent-when.test.sh|fm-procevent.test.sh|\ fm-live-gate.test.sh|\ fm-project-origin.test.sh|fm-public-followup.test.sh|fm-quota-choose.test.sh|\ fm-remote-entrypoint.test.sh|fm-remote-secondmate-parent-binding.test.sh|\ @@ -1583,11 +1583,15 @@ families_for_changed_path() { printf '%s\n' __script__:fm-extension-binding.test.sh ;; bin/fm-procevent.sh|bin/fm-procevent-lib.sh|bin/fm-procevent-extension-capture.pl) + printf '%s\n' __script__:fm-procevent-nextrade.test.sh printf '%s\n' __script__:fm-extension-binding.test.sh printf '%s\n' __script__:fm-procevent.test.sh printf '%s\n' __script__:fm-procevent-when.test.sh printf '%s\n' __script__:fm-remote-reply.test.sh ;; + bin/fm-procevent-nextrade.sh) + printf '%s\n' __script__:fm-procevent-nextrade.test.sh + ;; bin/fm-timeout-lib.sh) # The shared hard bound: session start's runtime bound, the fleet/bearings # snapshots, the vendor auth probe, the stow cascade's per-home step, and diff --git a/docs/captain-hold-lifecycle.md b/docs/captain-hold-lifecycle.md index 7b0c3ddfe86..ef335c14505 100644 --- a/docs/captain-hold-lifecycle.md +++ b/docs/captain-hold-lifecycle.md @@ -182,6 +182,7 @@ Only `answer` with the captain's words or evidence-backed `reconcile close` reso `answers` is its channel-agnostic entry point. It reads `\t\t