Skip to content
Merged
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
3 changes: 2 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ config/backend runtime session-provider backend override for new tasks; LOCAL,
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-presentation-spaces optional presence flag for Herdr's default-off disposable single-task visual projection; LOCAL, gitignored; inherited by secondmate homes; see docs/herdr-backend.md "Optional 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")
config/wedge-alarm optional away-mode wedge-alarm active-alert directives; LOCAL, gitignored; absent means auto (macOS Notification Center when available); see docs/wedge-alarm.md
config/x-mode.env generated X-mode watcher cadence; LOCAL, gitignored; source before arming watcher when present
Expand All @@ -90,7 +91,7 @@ state/ volatile runtime signals; gitignored
<id>.turn-ended touched by turn-end hooks
<id>.grok-turnend-token firstmate-owned grok hook registry token for the task; removed by teardown
<id>.kimi-turnend-token firstmate-owned Kimi hook registry token for the task; removed by teardown
<id>.meta written by fm-spawn: window=, endpoint_task_id=, worktree=, project=, harness=, model=, effort=, kind=, mode=, yolo=, tasktmp=; kind=secondmate also records home= and projects=; a non-default runtime backend records further backend-specific fields (docs/configuration.md "Runtime backend"; bin/fm-backend.sh, section 8); fm-pr-check, including through fm-pr-merge, records one canonical pr= and the forge's pr_head= when available (GitHub pull requests and GitLab merge requests; docs/gitlab-merge-watch.md); fm-x-link appends x_request=, x_request_ts=, x_followups=, and optional x_platform=/x_reply_max_chars= for an X-mode-originated task (section 14)
<id>.meta written by fm-spawn: window=, endpoint_task_id=, worktree=, project=, harness=, model=, effort=, kind=, mode=, yolo=, tasktmp=; an optional traceparent= only when trace context is enabled (docs/configuration.md "Trace context propagation"); kind=secondmate also records home= and projects=; a non-default runtime backend records further backend-specific fields (docs/configuration.md "Runtime backend"; bin/fm-backend.sh, section 8); fm-pr-check, including through fm-pr-merge, records one canonical pr= and the forge's pr_head= when available (GitHub pull requests and GitLab merge requests; docs/gitlab-merge-watch.md); fm-x-link appends x_request=, x_request_ts=, x_followups=, and optional x_platform=/x_reply_max_chars= for an X-mode-originated task (section 14)
<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 "Optional presentation spaces"
<id>.check.sh authenticated slow poll; the watcher dispatches validated PR data and the byte-identified X 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
Expand Down
10 changes: 4 additions & 6 deletions bin/backends/cmux.sh
Original file line number Diff line number Diff line change
Expand Up @@ -501,14 +501,12 @@ fm_backend_cmux_send_key() { # <target> <key> [expected-label]
fm_backend_cmux_cli send-key --workspace "$FM_BACKEND_CMUX_WORKSPACE" --surface "$FM_BACKEND_CMUX_SURFACE" "$key" >/dev/null 2>&1
}

# fm_backend_cmux_send_text_line: send one line of TEXT then submit. cmux has
# no single-call atomic "run and submit" primitive (like herdr's `pane run`),
# so this composes send (literal) + send-key enter, exactly like zellij's
# equivalent - used for the fixed spawn-time commands (treehouse get, the
# GOTMPDIR export).
# fm_backend_cmux_send_text_line: send one line of TEXT then submit.
fm_backend_cmux_send_text_line() { # <target> <text> [expected-label]
fm_backend_cmux_send_literal "$1" "$2" "${3:-}" || return 1
fm_backend_cmux_send_key "$1" Enter "${3:-}"
fm_backend_cmux_send_key "$1" Enter "${3:-}" && return 0
fm_backend_cmux_send_key "$1" C-c "${3:-}" >/dev/null 2>&1 && return 1
return 2
}

# fm_backend_cmux_capture: bounded plain-text surface capture. No herdr-style
Expand Down
12 changes: 4 additions & 8 deletions bin/backends/zellij.sh
Original file line number Diff line number Diff line change
Expand Up @@ -460,16 +460,12 @@ fm_backend_zellij_send_key() { # <target> <key> [expected-label]
fm_backend_zellij_cli "$FM_BACKEND_ZELLIJ_SESSION" action send-keys --pane-id "$FM_BACKEND_ZELLIJ_PANE" "$key" >/dev/null 2>&1
}

# fm_backend_zellij_send_text_line: send one line of TEXT then submit,
# ATOMICALLY - mirrors tmux's `send-keys -t T text Enter` / herdr's `pane
# run`. Used for the fixed spawn-time commands (treehouse get, the GOTMPDIR
# export). Zellij has no single-call atomic "run and submit" action, so this
# composes paste (literal) + send-keys Enter, exactly like send_literal +
# send_key are composed elsewhere - the two-step form is the ONLY form for
# this adapter, unlike tmux/herdr which have a genuinely atomic primitive.
# fm_backend_zellij_send_text_line: send one line of TEXT then submit.
fm_backend_zellij_send_text_line() { # <target> <text> [expected-label]
fm_backend_zellij_send_literal "$1" "$2" "${3:-}" || return 1
fm_backend_zellij_send_key "$1" Enter "${3:-}"
fm_backend_zellij_send_key "$1" Enter "${3:-}" && return 0
fm_backend_zellij_send_key "$1" C-c "${3:-}" >/dev/null 2>&1 && return 1
return 2
}

# fm_backend_zellij_capture: bounded plain-text pane capture. Mirrors
Expand Down
2 changes: 1 addition & 1 deletion bin/fm-bootstrap.sh
Original file line number Diff line number Diff line change
Expand Up @@ -394,7 +394,7 @@ secondmate_sync() {
fm_lock_release "$home_lock" || true
continue
}
if FM_CONFIG_INHERIT_REPORT="$report" \
if FM_CONFIG_INHERIT_REPORT="$report" FM_CONFIG_INHERIT_LIVE=1 \
propagate_secondmate_inheritance "$FM_HOME" "$home_real" "$CONFIG" "$DATA"; then
:
else
Expand Down
17 changes: 13 additions & 4 deletions bin/fm-config-inherit-lib.sh
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,14 @@
# runtime-backend default for future spawns, primary config/startup-memory-budget
# bounds that home's startup-memory curation, and primary
# config/herdr-presentation-spaces enables the same default-off Herdr presentation
# projection). It also pushes the one primary-authoritative shared
# captain-preference file, data/captain-shared.md, into each secondmate home's
# data/ as a read-only copy.
# projection, and primary
# config/trace-context is copied at the launch convergence point as part of the
# default-off W3C trace-context setup, while live convergence leaves it unchanged.
# The primary passes its frozen home-session decision into a newly launched
# Secondmate; see docs/trace-context.md.
# It also pushes
# the one primary-authoritative shared captain-preference file,
# data/captain-shared.md, into each secondmate home's data/ as a read-only copy.
#
# Usage: . bin/fm-config-inherit-lib.sh (no FM_* setup required)
#
Expand Down Expand Up @@ -46,7 +51,7 @@ FM_SHARED_CAPTAIN_MODE="444"
# The declared inheritable set (space-separated, config-dir-relative item paths).
# Extend here to inherit more of the primary's local config; override via the
# environment only in tests. Items must not contain whitespace.
FM_INHERITABLE_CONFIG="${FM_INHERITABLE_CONFIG:-crew-dispatch.json crew-harness backlog-backend backend herdr-presentation-spaces startup-memory-budget}"
FM_INHERITABLE_CONFIG="${FM_INHERITABLE_CONFIG:-crew-dispatch.json crew-harness backlog-backend backend herdr-presentation-spaces startup-memory-budget trace-context}"

fm_inherit_file_mode() {
if [ "$(uname)" = Darwin ]; then
Expand Down Expand Up @@ -403,6 +408,10 @@ propagate_inheritable_config() {
case "$item" in
''|/*|.|..|../*|*/../*|*/..) return 1 ;;
esac
if [ "${FM_CONFIG_INHERIT_LIVE:-0}" = 1 ] && [ "$item" = trace-context ]; then
record_inheritable_config_result "$item" unchanged "session-scoped"
continue
fi
src="$src_config/$item"
dest="$dest_config/$item"
# This one scalar config is consumed as a local safety boundary, so reject
Expand Down
3 changes: 2 additions & 1 deletion bin/fm-config-push.sh
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,8 @@ while IFS='|' read -r id home _window meta; do
continue
}
reports="$reports $report"
if FM_CONFIG_INHERIT_REPORT="$report" propagate_secondmate_inheritance "$FM_HOME" "$home_real" "$CONFIG" "$DATA"; then
if FM_CONFIG_INHERIT_REPORT="$report" FM_CONFIG_INHERIT_LIVE=1 \
propagate_secondmate_inheritance "$FM_HOME" "$home_real" "$CONFIG" "$DATA"; then
:
else
errors=1
Expand Down
5 changes: 5 additions & 0 deletions bin/fm-session-start.sh
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,8 @@ PRIMARY_HARNESS=$("$SCRIPT_DIR/fm-harness.sh" 2>/dev/null || printf unknown)
. "$SCRIPT_DIR/fm-backend.sh"
# shellcheck source=bin/fm-tasks-axi-lib.sh
. "$SCRIPT_DIR/fm-tasks-axi-lib.sh"
# shellcheck source=bin/fm-trace-context-lib.sh
. "$SCRIPT_DIR/fm-trace-context-lib.sh"

STATUS_TAIL=${FM_SESSION_START_STATUS_TAIL:-5}
case "$STATUS_TAIL" in ''|*[!0-9]*) STATUS_TAIL=5 ;; esac
Expand Down Expand Up @@ -264,6 +266,9 @@ if [ "$LOCK_RC" -ne 0 ]; then
printf '%s\n' "$BAR"
}
fi
if [ "$READ_ONLY" -eq 0 ]; then
fm_trace_context_session_start "$CONFIG" "$STATE/.trace-context-effective"
fi

# --- 2. bootstrap --------------------------------------------------------
subsection "BOOTSTRAP"
Expand Down
60 changes: 57 additions & 3 deletions bin/fm-spawn.sh
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,11 @@
# On success prints: spawned <id> harness=<name> kind=<ship|scout|secondmate> mode=<mode> yolo=<on|off> window=<backend-target> worktree=<path>
# mode/yolo are resolved per-project from data/projects.md for ship/scout tasks;
# secondmate spawns record mode=secondmate, yolo=off, home=, and projects=.
# When the home session's frozen trace-context decision is enabled (see
# docs/configuration.md and bin/fm-trace-context-lib.sh), the meta also records
# one W3C traceparent= carrier, the same value injected into the pane as
# TRACEPARENT; the default-off path writes neither, leaving the generated meta
# and launch environment unchanged.
set -eu

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
Expand Down Expand Up @@ -161,6 +166,8 @@ SUB_HOME_MARKER=".fm-secondmate-home"
. "$SCRIPT_DIR/fm-gate-refuse-lib.sh"
# shellcheck source=bin/fm-pr-lib.sh
. "$SCRIPT_DIR/fm-pr-lib.sh"
# shellcheck source=bin/fm-trace-context-lib.sh
. "$SCRIPT_DIR/fm-trace-context-lib.sh"
# Fail closed before any fleet mutation: a no-mistakes gate agent must never spawn
# a direct report (see bin/fm-gate-refuse-lib.sh).
fm_refuse_if_gate_agent
Expand Down Expand Up @@ -818,9 +825,10 @@ if [ "$KIND" = secondmate ]; then
exit 1
fi
CONFIG_INHERIT_LOCK_HELD=1
# Inheritance propagation: push the primary-authoritative local inheritance
# Inheritance propagation: push the primary-authoritative live-safe inheritance
# surface into this secondmate home (fm-config-inherit-lib.sh).
propagate_secondmate_inheritance "$FM_HOME" "$PROJ_ABS" "$CONFIG" "$DATA" \
FM_CONFIG_INHERIT_LIVE=1 \
propagate_secondmate_inheritance "$FM_HOME" "$PROJ_ABS" "$CONFIG" "$DATA" \
|| echo "warning: secondmate $ID inheritance failed for $PROJ_ABS" >&2
if [ -f "$PROJ_ABS/data/charter.md" ]; then
BRIEF="$PROJ_ABS/data/charter.md"
Expand Down Expand Up @@ -1165,6 +1173,11 @@ EOF
T="$ORCA_TERMINAL"
;;
esac
if [ "$KIND" = secondmate ]; then
FM_INHERITABLE_CONFIG=trace-context \
propagate_inheritable_config "$CONFIG" "$PROJ_ABS/config" \
|| echo "warning: secondmate $ID trace-context inheritance failed for $PROJ_ABS" >&2
fi
# #134 robustness: only tmux needs a worktree-detection target distinct from $T -
# its rename-safe stable window id, set as WT_TARGET=$WID in the tmux branch above.
# Every other backend addresses its pane/surface by the id already in $T, so default
Expand Down Expand Up @@ -1451,6 +1464,23 @@ $("$FM_ROOT/bin/fm-project-mode.sh" "$PROJ_NAME")
EOF
fi

# Resolve the optional default-off W3C trace context (bin/fm-trace-context-lib.sh,
# docs/configuration.md): the one carrier both recorded in meta and injected into
# the pane, so an observer reads exactly what the child receives. Empty only when
# disabled or on entropy/validation failure; malformed or all-zero inherited
# context is treated as absent and roots a fresh trace. Reuses this task's
# already-recorded value on relaunch. Never aborts the spawn and adds only the
# cost of reading a few bytes of entropy.
#
# The session-start path owns input resolution. Spawn consumes only the frozen
# home-session state and reuses it for the carrier and Secondmate launch prefix.
SPAWN_TRACE_EFFECTIVE=$(fm_trace_context_session_effective "$STATE/.trace-context-effective")
if [ "$SPAWN_TRACE_EFFECTIVE" = on ]; then
SPAWN_TRACEPARENT=$(FM_TRACE_CONTEXT=on fm_trace_context_resolve "$CONFIG" "$STATE/$ID.meta" || true)
else
SPAWN_TRACEPARENT=
fi

META_WINDOW=$T
[ "$BACKEND" = orca ] && META_WINDOW=$W
{
Expand All @@ -1465,6 +1495,7 @@ META_WINDOW=$T
echo "tasktmp=$TASK_TMP"
echo "model=${MODEL:-default}"
echo "effort=${EFFORT:-default}"
# Default-off writes no traceparent= line (meta stays byte-identical).
# backend= is written only for a non-default (non-tmux) backend, so the
# default path's meta stays byte-identical (absent backend= means tmux;
# data/fm-backend-design-d7's P1 compatibility contract).
Expand Down Expand Up @@ -1523,12 +1554,35 @@ if [ "$HARNESS" = claude ] && [ -n "${CLAUDE_CONFIG_DIR:-}" ]; then
fi
if [ "$KIND" = secondmate ]; then
sq_home=$(shell_quote "$PROJ_ABS")
LAUNCH="FM_ROOT_OVERRIDE= FM_STATE_OVERRIDE= FM_DATA_OVERRIDE= FM_PROJECTS_OVERRIDE= FM_CONFIG_OVERRIDE= FM_HOME=$sq_home $LAUNCH"
# Deliver the primary's EFFECTIVE trace-context decision as a normalized on/off
# literal (never the raw FM_TRACE_CONTEXT string) so a FM_TRACE_CONTEXT override
# on the primary reaches the secondmate's OWN workers, not just the copied
# config/trace-context file: otherwise off would not disable them and on would
# not enable them across the launch boundary (bin/fm-trace-context-lib.sh header).
# Reuse the single frozen decision from the carrier resolution above so the
# injected carrier and this on/off snapshot are guaranteed to agree.
LAUNCH="FM_ROOT_OVERRIDE= FM_STATE_OVERRIDE= FM_DATA_OVERRIDE= FM_PROJECTS_OVERRIDE= FM_CONFIG_OVERRIDE= FM_HOME=$sq_home FM_TRACE_CONTEXT=$SPAWN_TRACE_EFFECTIVE $LAUNCH"
fi
# Export GOTMPDIR into the crewmate's pane shell so the agent and every child
# process (go build, go test, ...) inherit it. Sent before the launch command so
# the env is set when the agent starts; the brief sleep lets the export land.
spawn_send_text_line "$T" "export GOTMPDIR=$TASK_TMP/gotmp"
# Send through the exact channel that already ships GOTMPDIR, so every backend
# and harness - ship, scout, and secondmate - gets it before launch. Skipped
# entirely when trace context is off.
if [ -n "$SPAWN_TRACEPARENT" ]; then
if spawn_send_text_line "$T" "export TRACEPARENT=$SPAWN_TRACEPARENT"; then
if ! echo "traceparent=$SPAWN_TRACEPARENT" >> "$STATE/$ID.meta"; then
LAUNCH="unset TRACEPARENT; $LAUNCH"
fi
else
TRACE_SEND_STATUS=$?
if [ "$TRACE_SEND_STATUS" -eq 2 ]; then
echo "error: trace-context input could not be cleared for $W; refusing to append the launch command" >&2
exit 1
fi
fi
fi
sleep 0.3
spawn_send_literal "$T" "$LAUNCH"
sleep 0.3
Expand Down
3 changes: 2 additions & 1 deletion bin/fm-test-isolation-proof.sh
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ exclusion_reason() {
fm-backend.test.sh)
printf '%s\n' 'old-vs-new main checkout diff fixture; gray-zone concurrent git/worktree cost'
;;
fm-spawn-dispatch-profile.test.sh|fm-spawn-worktree-settle.test.sh)
fm-spawn-dispatch-profile.test.sh|fm-spawn-worktree-settle.test.sh|fm-trace-context-spawn.test.sh)
printf '%s\n' 'real isolated git worktrees plus spawn settle loops; gray zone until dedicated proof'
;;
fm-pr-check-security.test.sh)
Expand Down Expand Up @@ -190,6 +190,7 @@ fm-backend-tmux-smoke.test.sh
fm-backend.test.sh
fm-spawn-dispatch-profile.test.sh
fm-spawn-worktree-settle.test.sh
fm-trace-context-spawn.test.sh
fm-pr-check-security.test.sh
fm-teardown.test.sh
fm-watcher-lock.test.sh
Expand Down
6 changes: 4 additions & 2 deletions bin/fm-test-run.sh
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,8 @@ family_for_basename() {
fm-operational-input.test.sh|fm-pi-primary-types.test.sh|\
fm-send-popup-settle.test.sh|fm-send-settle.test.sh|\
fm-subagent-pretool-check.test.sh|\
fm-supervision-instructions.test.sh|fm-tmux-submit-busy.test.sh|fm-transition-lib.test.sh|\
fm-supervision-instructions.test.sh|fm-tmux-submit-busy.test.sh|fm-trace-context-lib.test.sh|\
fm-transition-lib.test.sh|\
fm-test-run.test.sh|fm-test-isolation-proof.test.sh)
printf '%s\n' pure-contract-unit
;;
Expand Down Expand Up @@ -162,7 +163,8 @@ family_for_basename() {
;;
fm-backend-herdr.test.sh|fm-backend-tmux-smoke.test.sh|fm-backend.test.sh|\
fm-herdr-session-cleanup.test.sh|fm-send-strict.test.sh|fm-spawn-batch.test.sh|\
fm-spawn-dispatch-profile.test.sh|fm-spawn-worktree-settle.test.sh|\
fm-spawn-dispatch-profile.test.sh|\
fm-trace-context-spawn.test.sh|fm-spawn-worktree-settle.test.sh|\
fm-teardown-endpoint-safety.test.sh)
printf '%s\n' backend-dispatch
;;
Expand Down
Loading
Loading