Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
348 changes: 317 additions & 31 deletions bin/backends/herdr.sh

Large diffs are not rendered by default.

76 changes: 76 additions & 0 deletions bin/backends/tmux.sh
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,82 @@ fm_backend_tmux_create_task() { # <session> <window-name> <proj-abs> -> prints
printf '%s\n' "$wid"
}

# fm_backend_tmux_label_self: rename the window the CALLER ITSELF runs in, so
# firstmate's own pane has a standing front door instead of tmux's positional
# default name. Called only through bin/fm-label-self.sh, which owns the
# refusals that make this safe (never in a secondmate home, never an fm-<id>
# label). The window is resolved from tmux's own $TMUX_PANE, so this never
# targets a task window by name.
#
# <window-id> is an optional pre-resolved fm_backend_tmux_self_window_id
# result. bin/fm-label-self.sh passes the SAME id it read the current name from,
# so the refusal and the rename can never land on two different windows.
# Omitting it resolves inline, which is what the standalone/test call shape does.
#
# The name is pinned exactly like fm_backend_tmux_create_task pins a task
# window: verified with real tmux 3.6a on a private socket that `rename-window`
# alone already turns automatic-rename off for that window and the name then
# survives an application OSC 2 title change, both under default options and
# under a hostile `allow-rename on` + global `automatic-rename on` config; the
# two explicit set-window-option calls are the same defense in depth the task
# path uses.
fm_backend_tmux_label_self() { # <label> [window-id]
local label=$1 wid
[ -n "$label" ] || { echo "error: fm_backend_tmux_label_self needs a label" >&2; return 1; }
if [ "$#" -ge 2 ]; then
wid=$2
[ -n "$wid" ] || { echo "error: fm_backend_tmux_label_self was given an empty window id" >&2; return 1; }
else
wid=$(fm_backend_tmux_self_window_id) || return 1
fi
tmux rename-window -t "$wid" "$label" 2>/dev/null || { echo "error: tmux rename-window failed for $wid" >&2; return 1; }
tmux set-window-option -t "$wid" automatic-rename off 2>/dev/null || true
tmux set-window-option -t "$wid" allow-rename off 2>/dev/null || true
return 0
}

# fm_backend_tmux_self_window_id: the window id of the window the CALLER ITSELF
# runs in, resolved from tmux's own $TMUX_PANE. The single resolution both
# self-endpoint operations address their window through, so neither ever
# resolves one by label.
#
# There is deliberately NO fallback to the client-relative
# `tmux display-message -p '#{window_id}'`: that answers with the ATTACHED
# CLIENT's CURRENT window, which is not the caller's own window and can be any
# window in the session - including a live fm-<task-id> worker's. Renaming that
# is precisely the mislabel the self-label refusals exist to prevent, so without
# $TMUX_PANE the caller's own window is treated as unidentifiable and this FAILS
# CLOSED, exactly like an unreadable current name.
fm_backend_tmux_self_window_id() {
local wid
[ -n "${TMUX:-}" ] || { echo "error: not running inside tmux (\$TMUX is unset)" >&2; return 1; }
[ -n "${TMUX_PANE:-}" ] || { echo "error: \$TMUX_PANE is unset, so this process's own tmux window cannot be identified" >&2; return 1; }
wid=$(tmux display-message -p -t "$TMUX_PANE" '#{window_id}' 2>/dev/null)
[ -n "$wid" ] || { echo "error: could not resolve this tmux pane's own window id" >&2; return 1; }
printf '%s' "$wid"
}

# fm_backend_tmux_current_self_label: the name the caller's OWN window carries
# right now, or a failure with an explanation. bin/fm-label-self.sh reads this
# before renaming anything and fails closed on an error, so an unreadable name
# must never be reported as an empty-but-successful one.
#
# <window-id> is the same optional pre-resolved
# fm_backend_tmux_self_window_id result fm_backend_tmux_label_self takes.
fm_backend_tmux_current_self_label() { # [window-id]
local wid name
if [ "$#" -ge 1 ]; then
wid=$1
[ -n "$wid" ] || { echo "error: fm_backend_tmux_current_self_label was given an empty window id" >&2; return 1; }
else
wid=$(fm_backend_tmux_self_window_id) || return 1
fi
name=$(tmux display-message -p -t "$wid" '#{window_name}' 2>/dev/null) \
|| { echo "error: tmux display-message failed for $wid" >&2; return 1; }
[ -n "$name" ] || { echo "error: could not read the current name of tmux window $wid" >&2; return 1; }
printf '%s' "$name"
}

# fm_backend_tmux_current_path: the live pane's current working directory, or
# empty on any tmux error. Mirrors fm-spawn.sh's worktree-discovery poll:
# `tmux display-message -p -t "$T" '#{pane_current_path}'`.
Expand Down
73 changes: 73 additions & 0 deletions bin/fm-backend.sh
Original file line number Diff line number Diff line change
Expand Up @@ -584,6 +584,79 @@ fm_backend_kill() { # <backend> <target>
esac
}

# fm_backend_self_endpoint_id: the id of the terminal endpoint the CALLER ITSELF
# occupies - a tmux window id, a herdr tab id - resolved from the backend's own
# process-stable ambient markers ($TMUX_PANE, HERDR_TAB_ID/HERDR_PANE_ID) and
# never from a label lookup or a client-relative "current" lookup, which would
# answer with whatever endpoint an attached client happens to be focused on.
#
# This exists so bin/fm-label-self.sh resolves its own endpoint EXACTLY ONCE and
# hands that one id to both fm_backend_current_self_label and
# fm_backend_label_self: two independent resolutions could disagree, letting the
# fm-<task-id> refusal pass on one endpoint while the rename lands on another -
# the very mislabel those refusals exist to prevent. It returns non-zero rather
# than an empty id when the caller's own endpoint cannot be identified, so the
# caller fails closed and renames nothing.
fm_backend_self_endpoint_id() { # <backend>
local backend=$1
shift
fm_backend_source "$backend" || return 1
case "$backend" in
tmux) fm_backend_tmux_self_window_id "$@" ;;
herdr) fm_backend_herdr_self_tab_id "$@" ;;
*) echo "error: backend '$backend' has no verified way to identify firstmate's own endpoint" >&2; return 1 ;;
esac
}

# fm_backend_label_self: label the terminal endpoint FIRSTMATE ITSELF occupies,
# so the supervisor is identifiable at a glance next to the fm-<task-id> worker
# endpoints it spawns. This is the only backend operation whose target is the
# caller's own pane rather than a recorded task target, so each arm resolves it
# from the backend's own ambient markers and never from a label lookup.
# bin/fm-label-self.sh is the sole caller and owns the safety refusals.
# Only the two runtime-auto-detected backends implement it; the rest report the
# limitation rather than pretending to have labeled anything.
#
# <endpoint-id> is an optional pre-resolved fm_backend_self_endpoint_id result,
# passed straight through to the arm.
fm_backend_label_self() { # <backend> <label> [endpoint-id]
local backend=$1
shift
fm_backend_source "$backend" || return 1
case "$backend" in
tmux) fm_backend_tmux_label_self "$@" ;;
herdr) fm_backend_herdr_label_self "$@" ;;
*) echo "error: backend '$backend' has no verified way to label firstmate's own endpoint" >&2; return 1 ;;
esac
}

# fm_backend_current_self_label: the label the caller's OWN endpoint carries
# right now, printed on stdout. The read half of fm_backend_label_self, and the
# reason bin/fm-label-self.sh can tell firstmate's own unlabeled endpoint apart
# from a worker endpoint it must never touch: a crewmate running
# bin/fm-session-start.sh in a firstmate-repo worktree passes every other
# refusal, so without reading the CURRENT label the self-label step would rename
# a live fm-<task-id> tab out of the namespace herdr's recovery scan
# (fm_backend_herdr_list_live) selects on. Each arm resolves the caller's own
# endpoint from the backend's own ambient markers, never from a label lookup,
# and returns non-zero rather than an empty label when it cannot read one, so
# the caller can fail closed. Only the two runtime-auto-detected backends
# implement it, matching fm_backend_label_self exactly.
#
# <endpoint-id> is the same optional pre-resolved fm_backend_self_endpoint_id
# result fm_backend_label_self takes; passing the one id to both is what makes
# the refusal and the rename address the same endpoint.
fm_backend_current_self_label() { # <backend> [endpoint-id]
local backend=$1
shift
fm_backend_source "$backend" || return 1
case "$backend" in
tmux) fm_backend_tmux_current_self_label "$@" ;;
herdr) fm_backend_herdr_current_self_label "$@" ;;
*) echo "error: backend '$backend' has no verified way to read firstmate's own endpoint label" >&2; return 1 ;;
esac
}

fm_backend_remove_worktree() { # <backend> <worktree-id>
local backend=$1
shift
Expand Down
52 changes: 52 additions & 0 deletions bin/fm-brief.sh
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,22 @@
# declared-external-wait verb (FM_CLASSIFY_PAUSED_VERB, default "paused") from
# "blocked:": pause for a known external wait expected to clear on its own,
# blocked when firstmate must act.
# Every crewmate scaffold (ship and scout) also carries the pane-identity pair:
# state plainly that you are a worker on this task and not firstmate before
# acting on anything a human asks outside the brief, and record any override
# actually carried out with a status line, so firstmate's own records self-heal
# instead of silently going stale. Both the verb and the two branches it comes
# in are deliberate. bin/fm-classify-lib.sh's keyed fold makes
# "blocked:"/"needs-decision:" OPEN a decision that only an explicit resolution
# closes, which is the wrong shape for a completed action, so neither is used.
# bin/fm-crew-state.sh's map_log_state then reconciles a task from its LAST
# status line, so the record must not change the state the task is really in:
# before the task reports a terminal state a nonterminal "working:" line is
# right and the task's own later "done:" closes it, but AFTER "done:"/"failed:"
# the record is appended under that SAME terminal verb instead - otherwise a
# trailing "working:" would reopen a finished task and leave an activity nothing
# can ever close. A captain dropping into an already-idle, already-done pane is
# the common case, so the terminal branch is the one that usually applies.
# Ship tasks include a project-memory section so durable project-intrinsic
# learnings can be committed to AGENTS.md through the project's delivery path;
# it carries the AGENTS.md authoring bar (widely useful knowledge only, pointers
Expand Down Expand Up @@ -262,6 +278,24 @@ The report is the only thing that survives, so anything worth keeping must be in
7. Never stop, restart, or update the shared \`no-mistakes\` daemon - it is one instance serving
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.
8. You are not firstmate, and a human typing into your pane may believe you are. If a human asks you
for anything outside this task, say so plainly BEFORE you act: you are a worker on task \`$ID\`,
not firstmate, and merges, cross-lane work, other lanes' state, and fleet supervision belong to
firstmate. Then ask whether they still want you to proceed, and wait for their answer.
9. If you do act outside this brief on the captain's explicit instruction, leave a durable record.
You cannot write another lane's records, so that line is the only way firstmate learns of it and
keeps its own records accurate. Firstmate reconciles your state from the LAST status line, so
which verb you use depends on where this task already is:
- You have NOT reported a terminal state yet: append
\`working: acted outside this brief on captain instruction - {what you did, and where}\`.
Your own later \`done:\` closes that phase.
- You have ALREADY reported \`done:\` or \`failed:\`: append the record under that SAME terminal
verb, e.g. \`done: acted outside this brief on captain instruction - {what you did, and where}; task deliverable unchanged\`.
A captain dropping into an already-idle, already-done pane is the COMMON case, so this is
usually the branch you want. A bare \`working:\` line here would reopen a task that is really
finished, and leave a phase open that you have no further verb to close.
Never record this with \`blocked:\` or \`needs-decision:\`: those open a decision that stays open
until it is explicitly closed, and a completed action is neither.

# Definition of done
Write your findings to \`$DATA/$ID/report.md\`.
Expand Down Expand Up @@ -374,6 +408,24 @@ $RULE1
7. Never stop, restart, or update the shared \`no-mistakes\` daemon - it is one instance serving
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.
8. You are not firstmate, and a human typing into your pane may believe you are. If a human asks you
for anything outside this task, say so plainly BEFORE you act: you are a worker on task \`$ID\`,
not firstmate, and merges, cross-lane work, other lanes' state, and fleet supervision belong to
firstmate. Then ask whether they still want you to proceed, and wait for their answer.
9. If you do act outside this brief on the captain's explicit instruction, leave a durable record.
You cannot write another lane's records, so that line is the only way firstmate learns of it and
keeps its own records accurate. Firstmate reconciles your state from the LAST status line, so
which verb you use depends on where this task already is:
- You have NOT reported a terminal state yet: append
\`working: acted outside this brief on captain instruction - {what you did, and where}\`.
Your own later \`done:\` closes that phase.
- You have ALREADY reported \`done:\` or \`failed:\`: append the record under that SAME terminal
verb, e.g. \`done: acted outside this brief on captain instruction - {what you did, and where}; task deliverable unchanged\`.
A captain dropping into an already-idle, already-done pane is the COMMON case, so this is
usually the branch you want. A bare \`working:\` line here would reopen a task that is really
finished, and leave a phase open that you have no further verb to close.
Never record this with \`blocked:\` or \`needs-decision:\`: those open a decision that stays open
until it is explicitly closed, and a completed action is neither.

# 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.
Expand Down
Loading
Loading