Skip to content
4 changes: 2 additions & 2 deletions .agents/skills/secondmate-provisioning/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ That no-fetch path is a purely local fast-forward of tracked files, never an ori
A remote launch and the deferred bootstrap sweep ask the configured host to fast-forward its persistent home to that host's code-root commit under the same clean and ancestry guards.
`/updatefirstmate` first updates the remote code root from its own origin, then runs that guarded home sync.
SSH exit 255 preserves the route and reports unknown completion; it never triggers local respawn or failover.
The same placement-specific launch and deferred bootstrap sweep also propagate the primary's declared inherited local material: `config/crew-dispatch.json`, `config/crew-harness`, `config/backlog-backend`, `config/backend`, `config/herdr-presentation-spaces`, `config/startup-memory-budget`, and the one shared captain-preference file `data/captain-shared.md`.
The same placement-specific launch and deferred bootstrap sweep also propagate the primary's declared inherited local material: `config/crew-dispatch.json`, `config/crew-harness`, `config/backlog-backend`, `config/backend`, `config/herdr-project-spaces`, `config/herdr-presentation-spaces`, `config/startup-memory-budget`, and the one shared captain-preference file `data/captain-shared.md`.
Because these paths are gitignored, that propagation is a separate, primary-authoritative copy independent of the tracked-files fast-forward: it re-converges every live home whether or not its tracked files advanced, and it touches only the declared items.
Propagation failures warn without blocking secondmate launch or session-start continuation, and the destination keeps whatever safely validated state the helper left behind.
Inheritance copies the literal `config/crew-harness` file, so a secondmate's own crewmates use the primary's crewmate harness only when it names a concrete adapter such as `codex`; an unset or `default` value has nothing concrete to inherit, and the secondmate's own crewmates fall back to the secondmate's own or detected harness instead.
Expand All @@ -125,7 +125,7 @@ Keep every `data/learnings.md` fully local by captain decision; route fleet-gene
No AGENTS.md reread nudge is needed at spawn or respawn because the agent reads instructions fresh on launch; only the bootstrap sweep's running-home instruction-surface advance needs that AGENTS.md re-read.
Bootstrap reports successful AGENTS.md re-read sends as `BOOTSTRAP_INFO:` and only emits `NUDGE_SECONDMATES:` when that send fails and needs retry.
A separate, literal-content config reread is required whenever inherited `config/*` material changes under an already-running secondmate.
For a local home, after each successful allowlisted config write, both the locked bootstrap convergence path and mid-session `bin/fm-config-push.sh` use the shared propagation report to build one per-home generation-specific private instruction file from the validated destination post-write bytes for only the allowlisted config items that actually changed for that home (`config/crew-dispatch.json`, `config/crew-harness`, `config/backlog-backend`, `config/backend`, `config/herdr-presentation-spaces`, `config/startup-memory-budget`), in deterministic allowlist order.
For a local home, after each successful allowlisted config write, both the locked bootstrap convergence path and mid-session `bin/fm-config-push.sh` use the shared propagation report to build one per-home generation-specific private instruction file from the validated destination post-write bytes for only the allowlisted config items that actually changed for that home (`config/crew-dispatch.json`, `config/crew-harness`, `config/backlog-backend`, `config/backend`, `config/herdr-project-spaces`, `config/herdr-presentation-spaces`, `config/startup-memory-budget`), in deterministic allowlist order.
Each changed path is printed with clear begin/end delimiters and the destination file's full exact new bytes unparsed, or the explicit token `ABSENT` when propagation removed the destination copy.
The instruction uses only minimal framing that these are defaults/rules and do not remove judgment; it never includes SHA values, selected profiles, parsed summaries, or any other generated interpretation.
`data/captain-shared.md` is not a config file and is never inlined into this instruction file or message.
Expand Down
2 changes: 2 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ config/backlog-backend backlog backend override; LOCAL, gitignored; absent or "
config/backend runtime session-provider backend override for new tasks; LOCAL, gitignored; absent = falls through to runtime auto-detection (the runtime firstmate itself is executing inside), then tmux; tmux is the verified reference backend (docs/tmux-backend.md), while herdr, zellij, orca, and cmux are experimental spawn backends (docs/herdr-backend.md, docs/zellij-backend.md, docs/orca-backend.md, docs/cmux-backend.md) - herdr and cmux can also be selected by runtime auto-detection, zellij and orca never are (always explicit), and codex-app is not accepted; see docs/codex-app-backend.md; inherited by secondmate homes under the primary-authoritative contract in secondmate-provisioning
config/calm Pi Calm presentation preference; LOCAL, gitignored, and not inherited; see docs/configuration.md "Pi Calm preference"
config/startup-memory-budget primary-authoritative per-home startup-memory budget; LOCAL, gitignored, materialized as 7,500 estimated tokens by locked primary bootstrap and inherited into secondmate homes; see docs/configuration.md "Startup memory budget"
config/herdr-project-spaces optional empty or "on" opt-in to exact-id-bound per-project Herdr workspaces; LOCAL, gitignored; inherited by secondmate homes; see docs/herdr-backend.md "Project workspaces"
config/herdr-presentation-spaces optional "off" opt-out from, or "on" opt-in to, Herdr's default-on disposable single-task visual projection, which is unconfigured-default-on only at or above a Herdr version floor; LOCAL, gitignored; inherited by secondmate homes; see docs/herdr-backend.md "Presentation spaces"
config/trace-context optional presence flag enabling default-off native W3C trace-context propagation to spawned agents; LOCAL, gitignored; inherited by secondmate homes; see docs/configuration.md "Trace context propagation" and docs/trace-context.md
config/cmux-socket-password optional cmux control-socket password; LOCAL, gitignored; read fresh on every cmux CLI call and passed through without ever overriding an operator's own ambient CMUX_SOCKET_PASSWORD when absent (docs/cmux-backend.md "Setup")
Expand All @@ -95,6 +96,7 @@ state/ runtime records and signals; gitignored
<id>.cursor-session cursor busy-source binding (projects root, task worktree, prior conversations) written by fm-spawn; removed by teardown
<id>.meta task metadata; each producer script's header owns its exact fields and mutation contract, with docs/configuration.md routing operator-facing backend and trace-context details
<id>.herdr-presentation quarantinable attempt and restart-binding journal for Herdr's optional visual projection; never task or endpoint authority; see docs/herdr-backend.md "Presentation spaces"
.herdr-project-space-<project>-<dirhash> durable exact-id binding for the opt-in per-project Herdr workspace layout; placement authority only, never task or endpoint authority; see docs/herdr-backend.md "Project workspaces"
<id>.check.sh authenticated slow poll; the watcher dispatches validated PR data and the byte-identified Relay shim through trusted repository scripts, runs registered custom checks from hash-validated private snapshots, and rejects every other state check without execution
<id>.check-trust private content binding created by fm-check-register.sh for an intentional custom check
<id>.pr-poll private validated data sidecar for the byte-static PR merge poll
Expand Down
200 changes: 195 additions & 5 deletions bin/backends/herdr.sh
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,12 @@
#
# Default container shape (D4, decided empirically - see
# herdr-verification-p2.md "Task container shape", refined by
# docs/herdr-backend.md "Default task container shape"): ONE herdr workspace PER
# docs/herdr-backend.md "Watching and task containers"): ONE herdr workspace PER
# FIRSTMATE HOME (the primary, and each secondmate, gets its own), ONE herdr TAB
# per task inside its home's workspace. The default-on presentation projection
# creates a disposable workspace for a clean fresh task instead unless the home
# opts out. That
# per task inside its home's workspace. An opt-in project-space layout instead
# groups each project's task tabs in one exact-id-bound workspace. The
# default-on presentation projection creates a disposable workspace for a clean
# fresh task unless project grouping wins or the home opts out. That
# workspace is a non-authoritative visual projection containing only the normal
# task pane. Its random token and mutable label never authorize lookup,
# adoption, reuse, closure, deletion, task ownership, or endpoint selection.
Expand Down Expand Up @@ -145,6 +146,28 @@ FM_BACKEND_HERDR_PRESENTATION_JOURNAL_SUFFIX=".herdr-presentation"
# The config item a home writes to opt out of, or explicitly in to, the
# projection.
FM_BACKEND_HERDR_PRESENTATION_CONFIG="herdr-presentation-spaces"
FM_BACKEND_HERDR_PROJECT_SPACES_CONFIG="herdr-project-spaces"
FM_BACKEND_HERDR_PROJECT_BINDING_PREFIX=".herdr-project-space-"

# fm_backend_herdr_project_spaces_enabled <config-dir>: presence-compatible
# opt-in parsing for project workspaces. An empty file or "on" enables the
# layout, "off" and absence disable it, and an unrecognized value warns and
# disables it so a visual preference never fails dispatch.
fm_backend_herdr_project_spaces_enabled() { # <config-dir>
local config_dir=${1:-} file value
[ -n "$config_dir" ] || return 1
file="$config_dir/$FM_BACKEND_HERDR_PROJECT_SPACES_CONFIG"
[ -f "$file" ] || return 1
value=$(tr -d '[:space:]' < "$file" 2>/dev/null | tr '[:upper:]' '[:lower:]') || value=""
case "$value" in
''|on) return 0 ;;
off) return 1 ;;
*)
echo "warning: $file: unrecognized value \"$value\"; herdr project spaces stay disabled (write \"on\" or leave the file empty to opt in)" >&2
return 1
;;
esac
}

# fm_backend_herdr_presentation_preference <config-dir>: the single owner of
# config/herdr-presentation-spaces parsing. Echoes exactly one of "off", "on"
Expand Down Expand Up @@ -338,7 +361,7 @@ fm_backend_herdr_presentation_enabled() { # <config-dir> [<state-dir>]
}

# fm_backend_herdr_workspace_label: the per-firstmate-HOME herdr workspace
# label (docs/herdr-backend.md "Default task container shape"). The PRIMARY home (no
# label (docs/herdr-backend.md "Watching and task containers"). The PRIMARY home (no
# secondmate marker) resolves to the constant "firstmate", byte-identical to
# every pre-existing task's recorded label - no forced migration. A SECONDMATE
# home resolves to "2ndmate-<secondmate-id>", so its tasks land in their own
Expand Down Expand Up @@ -1623,6 +1646,173 @@ fm_backend_herdr_launcher_identity() { # <session>
return 0
}

# fm_backend_herdr_project_dir_hash <project-dir>: a stable short digest of the
# canonical project directory, used only to key the binding filename.
fm_backend_herdr_project_dir_hash() { # <project-dir>
local dir=$1 digest
if command -v shasum >/dev/null 2>&1; then
digest=$(printf '%s' "$dir" | shasum -a 256 2>/dev/null | awk '{print substr($1, 1, 16)}')
elif command -v sha256sum >/dev/null 2>&1; then
digest=$(printf '%s' "$dir" | sha256sum 2>/dev/null | awk '{print substr($1, 1, 16)}')
else
digest=$(printf '%s' "$dir" | cksum 2>/dev/null | awk '{printf "%08x%08x", $1, $2}')
fi
case "$digest" in
*[!0-9a-f]*|'') return 1 ;;
esac
printf '%s' "$digest"
}

# fm_backend_herdr_project_binding_path <state-dir> <project-name> <project-dir>:
# resolve the home-local exact-id binding record for one project. The filename
# is keyed by the registered name PLUS a stable digest of the canonical project
# directory, so two same-named projects in different directories keep
# independent durable bindings; the name itself stays a cosmetic label only.
# Project names become filenames, so only the registry's safe identifier shape
# is accepted; an unsafe name makes project placement fall back flat.
fm_backend_herdr_project_binding_path() { # <state-dir> <project-name> <project-dir>
local state=$1 project_name=$2 project_dir=$3 dir_hash
case "$project_name" in
''|.|..|*[!A-Za-z0-9._-]*) return 1 ;;
esac
[ -n "$project_dir" ] || return 1
dir_hash=$(fm_backend_herdr_project_dir_hash "$project_dir") || return 1
printf '%s/%s%s-%s' "$state" "$FM_BACKEND_HERDR_PROJECT_BINDING_PREFIX" "$project_name" "$dir_hash"
}

# Read one complete binding and reject malformed, duplicate, cross-project, or
# cross-session fields. The record is placement authority only for the exact
# workspace id it carries; its label is an expected cosmetic property, never a
# discovery key.
fm_backend_herdr_project_binding_snapshot() { # <file> <project-dir> <project-name> <session>
local file=$1 project_dir=$2 project_name=$3 session=$4 parsed
FM_BACKEND_HERDR_PROJECT_BOUND_WORKSPACE_ID=""
[ ! -L "$file" ] || return 1
if [ -e "$file" ] && [ ! -f "$file" ]; then
return 1
fi
[ -f "$file" ] || return 2
parsed=$(awk -F= '
BEGIN { required["version"]; required["project_dir"]; required["project_name"]; required["session"]; required["workspace_id"] }
!($1 in required) || seen[$1]++ { bad=1; next }
{ value[$1]=substr($0, index($0, "=") + 1) }
END {
for (key in required) if (!(key in seen)) bad=1
if (NR != 5 || bad) exit 1
printf "%s\034%s\034%s\034%s\034%s", value["version"], value["project_dir"], value["project_name"], value["session"], value["workspace_id"]
}
' "$file" 2>/dev/null) || return 1
IFS=$'\034' read -r binding_version binding_dir binding_name binding_session binding_workspace <<EOF
$parsed
EOF
[ "$binding_version" = 1 ] || return 1
[ "$binding_dir" = "$project_dir" ] || return 2
[ "$binding_name" = "$project_name" ] || return 2
[ "$binding_session" = "$session" ] || return 2
[ -n "$binding_workspace" ] || return 1
FM_BACKEND_HERDR_PROJECT_BOUND_WORKSPACE_ID=$binding_workspace
return 0
}

# Validate the bound id live in the named session on every use. A missing or
# renamed workspace is stale (2). An unreadable or duplicate response is
# ambiguous (1) and grants neither adoption nor creation authority.
fm_backend_herdr_project_binding_live() { # <session> <workspace-id> <expected-label>
local session=$1 workspace_id=$2 expected_label=$3 out match_count actual_label
out=$(fm_backend_herdr_cli "$session" workspace list 2>/dev/null) || return 1
match_count=$(printf '%s' "$out" | jq -r --arg workspace "$workspace_id" '
select((.result.workspaces | type) == "array")
| [.result.workspaces[] | select(.workspace_id == $workspace)] | length
' 2>/dev/null) || return 1
case "$match_count" in
0) return 2 ;;
1) ;;
*) return 1 ;;
esac
actual_label=$(printf '%s' "$out" | jq -r --arg workspace "$workspace_id" '
.result.workspaces[] | select(.workspace_id == $workspace) | .label // empty
' 2>/dev/null) || return 1
[ "$actual_label" = "$expected_label" ] || return 2
return 0
}

# Atomically publish one exact create-response id. A binding is never derived
# from a label search, and an existing symlink is never replaced.
fm_backend_herdr_project_binding_publish() { # <file> <project-dir> <project-name> <session> <workspace-id>
local file=$1 project_dir=$2 project_name=$3 session=$4 workspace_id=$5 dir tmp
dir=${file%/*}
mkdir -p "$dir" || return 1
[ ! -L "$file" ] || return 1
tmp=$(umask 077; mktemp "$dir/.herdr-project-space.XXXXXX" 2>/dev/null) || return 1
{
printf 'version=1\n'
printf 'project_dir=%s\n' "$project_dir"
printf 'project_name=%s\n' "$project_name"
printf 'session=%s\n' "$session"
printf 'workspace_id=%s\n' "$workspace_id"
} > "$tmp" || { rm -f "$tmp"; return 1; }
mv -f "$tmp" "$file" || { rm -f "$tmp"; return 1; }
}

# Resolve or create the project workspace while the caller holds the named
# session lock. Existing bindings are used only after exact live-id and label
# validation. Stale bindings create a fresh workspace from the create response;
# malformed or unreadable bindings fall back flat without guessing.
fm_backend_herdr_project_workspace_ensure() { # <session> <project-dir> <state-dir> <project-name>
local session=$1 project_dir=$2 state=$3 project_name=$4 binding status out wsid seeded
FM_BACKEND_HERDR_PROJECT_WS_ID=""
FM_BACKEND_HERDR_PROJECT_WS_SEEDED_TAB_ID=""
case "$project_dir$session" in
*$'\n'*|*$'\r'*|*$'\034'*)
echo "warning: herdr project-space placement cannot encode the exact project or session identity; using the ordinary flat layout" >&2
return 1
;;
esac
binding=$(fm_backend_herdr_project_binding_path "$state" "$project_name" "$project_dir") || {
echo "warning: herdr project-space placement cannot represent project name '$project_name'; using the ordinary flat layout" >&2
return 1
}
fm_backend_herdr_project_binding_snapshot "$binding" "$project_dir" "$project_name" "$session" && status=0 || status=$?
if [ "$status" -eq 0 ]; then
fm_backend_herdr_project_binding_live "$session" "$FM_BACKEND_HERDR_PROJECT_BOUND_WORKSPACE_ID" "$project_name" && status=0 || status=$?
case "$status" in
0)
FM_BACKEND_HERDR_PROJECT_WS_ID=$FM_BACKEND_HERDR_PROJECT_BOUND_WORKSPACE_ID
[ -n "$FM_BACKEND_HERDR_PROJECT_WS_ID" ] || return 1
return 0
;;
2) ;;
*)
echo "warning: herdr project-space binding for '$project_name' is ambiguous or unreadable; using the ordinary flat layout" >&2
return 1
;;
esac
elif [ "$status" -eq 1 ]; then
echo "warning: herdr project-space binding for '$project_name' is malformed or unreadable; using the ordinary flat layout" >&2
return 1
fi
out=$(fm_backend_herdr_cli "$session" workspace create --cwd "$project_dir" --label "$project_name" --no-focus 2>/dev/null) || {
echo "warning: herdr could not create a project workspace for '$project_name'; using the ordinary flat layout" >&2
return 1
}
wsid=$(printf '%s' "$out" | jq -r '.result.workspace.workspace_id // empty' 2>/dev/null)
seeded=$(printf '%s' "$out" | jq -r '.result.tab.tab_id // empty' 2>/dev/null)
if [ -z "$wsid" ] || [ -z "$seeded" ] \
|| [[ "$wsid$seeded" == *$'\n'* || "$wsid$seeded" == *$'\r'* || "$wsid$seeded" == *$'\034'* ]]; then
echo "warning: herdr project workspace create for '$project_name' returned incomplete exact ids; using the ordinary flat layout" >&2
return 1
fi
fm_backend_herdr_project_binding_publish "$binding" "$project_dir" "$project_name" "$session" "$wsid" || {
echo "warning: herdr could not publish the exact project-space binding for '$project_name'; using the ordinary flat layout" >&2
return 1
}
FM_BACKEND_HERDR_PROJECT_WS_ID=$wsid
FM_BACKEND_HERDR_PROJECT_WS_SEEDED_TAB_ID=$seeded
[ -n "$FM_BACKEND_HERDR_PROJECT_WS_ID" ] \
&& [ -n "$FM_BACKEND_HERDR_PROJECT_WS_SEEDED_TAB_ID" ] || return 1
return 0
}

# fm_backend_herdr_workspace_prune_seeded_default_tab: close EXACTLY
# <seeded_tab_id>, the auto-created default tab id that THIS SAME
# fm_backend_herdr_workspace_ensure call captured straight from its own
Expand Down
Loading
Loading