Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
63 commits
Select commit Hold shift + click to select a range
aa43462
fix(session-lock): identify a harness session on Windows/Git Bash
lmktechnology Sep 2, 2026
a975ea4
fix(session-lock): keep the nudge's own ancestry question intact
lmktechnology Sep 2, 2026
36dbddc
fix(session-lock): accept a tagged identity in every gate that reads …
lmktechnology Sep 2, 2026
a24f8d1
feat(native-owner): consolidate experimental ownership core and Codex…
cr101 Sep 15, 2026
351345e
test(native-owner): bind evidence to its exact build across rebuilds
cr101 Sep 15, 2026
54aa673
feat(native-owner): persist receipt lifecycle and preserve interrupte…
cr101 Sep 15, 2026
fd1f7f7
feat(native-owner): recover completed receipts and bound shutdown
cr101 Sep 15, 2026
d4c848c
feat(native-owner): integrate experimental Codex launcher
cr101 Sep 16, 2026
9b7efa2
no-mistakes(review): Fix native ownership and app isolation regressions
cr101 Sep 16, 2026
1be4151
no-mistakes(review): Harden native launcher isolation and regression …
cr101 Sep 16, 2026
99bb453
no-mistakes(review): Harden app policy pagination and response valida…
cr101 Sep 16, 2026
5807ed2
no-mistakes(review): Harden native isolation and remove fixture-only …
cr101 Sep 16, 2026
7d9ef2b
no-mistakes(review): Fix native startup, shutdown, and lock compatibi…
cr101 Sep 16, 2026
89b0aba
no-mistakes(review): Fix native notification recovery and isolation s…
cr101 Sep 16, 2026
7adf5e8
no-mistakes(review): Preserve unoffered notifications and reject popu…
cr101 Sep 16, 2026
b47d18f
no-mistakes(review): Consolidate native admission, recovery, and host…
cr101 Sep 16, 2026
c969131
no-mistakes(review): Harden native admission and acknowledgement reco…
cr101 Sep 16, 2026
2df27f1
no-mistakes(review): Reject residual work and document fixture usage
cr101 Sep 16, 2026
788f4f5
no-mistakes(review): Reject owner-managed residual work and preserve …
cr101 Sep 16, 2026
30d765d
no-mistakes(review): Harden native admission and acknowledgement evid…
cr101 Sep 16, 2026
087163d
no-mistakes(review): Clarify host authority and consolidate owner con…
cr101 Sep 16, 2026
9b9cedf
no-mistakes(review): Harden native admission and restore supervision …
cr101 Sep 16, 2026
ce60983
no-mistakes(review): Restore native restart reconciliation paths
cr101 Sep 16, 2026
a339214
no-mistakes(review): Harden native launch admission and recovery owne…
cr101 Sep 16, 2026
de8ee11
no-mistakes(review): Fix native ownership; Git Bash verification perm…
cr101 Sep 16, 2026
7fb52a1
no-mistakes(review): Fix receipt target reuse and launcher argument b…
cr101 Sep 17, 2026
0623188
no-mistakes(review): Fix receipt inspection and MCP protocol validation
cr101 Sep 17, 2026
2ca48a4
no-mistakes(review): Fix receipt counters and disabled app classifica…
cr101 Sep 17, 2026
7fa3d28
no-mistakes(test): Canonicalize launcher fixture state path
cr101 Sep 17, 2026
b4ed646
no-mistakes(test): Canonicalize launcher fixture code-root path
cr101 Sep 17, 2026
9fbdb97
no-mistakes(test): Prevent acknowledgement failure on closed native o…
cr101 Sep 17, 2026
71a7f97
no-mistakes(test): Fix Windows custom-check mode setup; host retest p…
cr101 Sep 17, 2026
eff7d0a
no-mistakes(test): Remove unsupported Windows custom-check registrati…
cr101 Sep 17, 2026
1ea5f38
no-mistakes(test): Fix non-temporary launcher fixture path selection
cr101 Sep 17, 2026
1530747
no-mistakes(test): Retry native admission during notification publica…
cr101 Sep 17, 2026
47be0f6
no-mistakes(test): Fix launcher notification fixture synchronization
cr101 Sep 17, 2026
688941c
Scope publication rendezvous to its launcher scenario
cr101 Sep 17, 2026
07437da
Synchronize inbox publication with native admission
cr101 Sep 17, 2026
d51c412
Canonicalize the paused inbox fixture home
cr101 Sep 18, 2026
53b50a8
Tolerate brief Windows reader contention when publishing host progress
cr101 Sep 18, 2026
eb17006
Merge main into the native Windows replacement, preserving both test …
cr101 Sep 18, 2026
69bef3b
no-mistakes(review): Captain, harden ownership/validators; live proof…
cr101 Sep 18, 2026
a47060a
no-mistakes(review): Preserve malformed Windows owners during ordinar…
cr101 Sep 18, 2026
4dfb766
no-mistakes(review): Distinguish malformed owners; Claude proof remai…
cr101 Sep 18, 2026
9760ce1
no-mistakes(review): Reject unresolved native process-event results
cr101 Sep 18, 2026
578ba5f
no-mistakes(review): Reject hidden process-event results and repair f…
cr101 Sep 18, 2026
2c8ba91
no-mistakes(review): Fix native admission, selection, and owner-state…
cr101 Sep 18, 2026
39591e4
no-mistakes(review): Support fallback receipts and repair source-awar…
cr101 Sep 18, 2026
0e432cd
no-mistakes(review): Preserve native owner uncertainty and admission …
cr101 Sep 18, 2026
b276786
no-mistakes(review): Preserve notification offers and atomically publ…
cr101 Sep 18, 2026
544fe68
no-mistakes(review): Bind acknowledgements to current-turn notificati…
cr101 Sep 18, 2026
657f658
no-mistakes(review): Reject unsafe projects paths and document termin…
cr101 Sep 18, 2026
21f3600
no-mistakes(document): Clarify native launcher admission documentation
cr101 Sep 18, 2026
b106d69
no-mistakes(lint): Resolve source-aware ShellCheck diagnostics
cr101 Sep 18, 2026
6f74dd7
no-mistakes(lint): Preserve successful evidence loads after cleanup f…
cr101 Sep 18, 2026
2181d67
no-mistakes(lint): Annotate indirect ShellCheck consumers
cr101 Sep 19, 2026
919b756
no-mistakes(lint): Persist startup fixture overrides before sourcing
cr101 Sep 19, 2026
09d19aa
no-mistakes(lint): Localize startup fixture environment overrides
cr101 Sep 19, 2026
ead9b36
docs: describe native-owner detection precedence
cr101 Sep 19, 2026
203a69a
test: isolate native host fixture appdata on Linux
cr101 Sep 19, 2026
12b2061
Merge upstream main preserving native Windows candidate history
cr101 Sep 19, 2026
755daaf
no-mistakes(document): Document Windows owner identities and verifica…
cr101 Sep 19, 2026
ee9be1c
no-mistakes(ci): Captain, fixed the pre-existing cleanup race in test…
cr101 Sep 19, 2026
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
5 changes: 3 additions & 2 deletions .agents/skills/harness-adapters/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,9 @@ Muse, Gemini, and AGY are verified only for crewmate and scout work, never a sec

## Detection

`../../../bin/fm-harness.sh` prints firstmate's own harness from verified environment markers and process ancestry, and owns how they combine.
A marker names its harness, but a structural ancestor of a different harness outranks it, because a marker is ordinary environment state a child or a multiplexer can retain while ancestry is what proves who owns the process tree.
`../../../bin/fm-harness.sh` prints firstmate's own harness and owns how native-owner verification, verified environment markers, and process ancestry combine.
For the [experimental native Windows Codex launcher](../../../docs/native-windows-codex.md), durable home records select native-owner verification before marker and ancestry detection; an unverified or unsupported native-owner result remains `unknown`, without falling back to those other signals.
Outside that native path, a marker names its harness, but a structural ancestor of a different harness outranks it, because a marker is ordinary environment state a child or a multiplexer can retain while ancestry is what proves who owns the process tree.
Only `FM_PI_HARNESS=pi-signed` at the launch boundary together with `PI_CODING_AGENT=true` selects Pi-signed; shared unmarked launcher ancestry remains Pi.
omp publishes no marker of its own; `FM_OMP_HARNESS=omp` is Firstmate's launch marker and the anchored process name `omp` is its ancestry evidence, as `references/harness/omp.md` records.
`../../../bin/fm-spawn.sh` owns worker marker establishment, while the README launch command owns the signed-primary boundary.
Expand Down
3 changes: 3 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -1,2 +1,5 @@
# Bash parses shell scripts with LF line endings on every supported platform.
*.sh text eol=lf
# Extensionless Bash helpers must survive Windows clones.
tests/fixtures/native-owner/jq text eol=lf
bin/native-owner/tools/jq text eol=lf
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,6 @@ __pycache__/
config/

.tools/
# Local native candidate build; never distributed as a tracked executable.
/bin/fm-native-owner.exe
/bin/fm-native-owner.build
7 changes: 5 additions & 2 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,8 +63,11 @@ Coordinate any workflow rollback with its required-check names so a retired chec
A local `config/backend` file explicitly overrides runtime auto-detection for new task endpoints and stays gitignored; spawn-supported values are `tmux`, `herdr` (which has its own required CI lane), and `zellij`, `orca`, and `cmux`, which remain experimental with no dedicated real-backend CI lane, while `codex-app` is documented only in `docs/codex-app-backend.md`.
It does not make `data/` tracked.
- Helper scripts in `bin/` are plain bash.
Each starts with a usage header comment; keep it accurate when you change behavior.
Test scripts and helpers in `tests/` are plain bash too.
The restricted experimental native Windows Codex launcher is the sole scoped exception: `bin/fm-native-codex.ps1` and the C# and JavaScript implementation under `bin/native-owner/` may use PowerShell, C#, and JavaScript.
Matching fixtures under `tests/fixtures/native-owner/` may use those languages too.
This exception does not extend to other helpers, other platforms, or future implementations.
Each Bash helper starts with a usage header comment; keep it accurate when you change behavior.
Test scripts and helpers in `tests/` are plain bash too, except for the matching native Windows fixtures above.
`bin/fm-lint.sh` must pass: it is the single owner of the lint definition (the shellcheck file set, config, pinned shellcheck version, pinned actionlint workflow lint, and the backend-purity check rejecting direct Beads CLI calls in core `bin/` scripts).
CI uses its full canonical partitions; the no-mistakes pre-push gate uses its context-selected default.
`docs/fm-test-portable-shards.md` owns partition verification and performance evidence.
Expand Down
2 changes: 1 addition & 1 deletion bin/backends/herdr.sh
Original file line number Diff line number Diff line change
Expand Up @@ -3083,7 +3083,7 @@ fm_backend_herdr_composer_state() { # <target> -> empty|pending|pending-unprove
verdict=$(fm_composer_classify_screen "$caps" "$cap")
if [ "$verdict" = need-identity ]; then
if ! identity=$(fm_backend_herdr_composer_identity "$target" 2>/dev/null) || [ -z "$identity" ]; then
identity=probe-absent
identity='probe-absent'
fi
verdict=$(fm_composer_classify_screen "$caps" "$cap" '' "$identity")
[ "$verdict" != need-identity ] || verdict=unknown
Expand Down
104 changes: 104 additions & 0 deletions bin/fm-backlog-transition-lib.sh
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,18 @@
# closes a row that reads as an open captain call. An answer that closes the row
# first applies any supported retained artifact from the validated record, then
# replay simply retires the record.
#
# EMPTY-FLEET ADMISSION. fm_backlog_markdown_empty <backlog-file> accepts a
# readable, non-symlink markdown file only when its nonblank LF or CRLF lines are
# either `# Backlog` alone or the optional `# Backlog` title followed by the
# `## In flight`, `## Queued`, and `## Done` empty section skeleton.
# fm_backlog_empty_fleet_preflight <state-dir> <data-dir> additionally rejects
# task, close-recovery, inbox, and handoff work records, requires the configured
# backlog owner to resolve to markdown, and applies that semantic empty-file
# check when the backlog exists. Both functions write nothing, return 0 only for
# recognized empty state, and otherwise return non-zero with the refusal in
# FM_BACKLOG_EMPTY_ERROR. Callers must source fm-tasks-axi-lib.sh first so backend
# selection and configuration errors retain their owning contract.

# Set by fm_backlog_transition_applies for a return-1 exemption.
# shellcheck disable=SC2034 # Output global, read by the sourcing caller.
Expand All @@ -72,6 +84,9 @@ FM_BACKLOG_ROW_HOLD_KIND=
# retained_incomplete | answered | stale | noop.
# shellcheck disable=SC2034 # Output global, read by the sourcing caller.
FM_BACKLOG_CLOSE_REPLAY_RESULT=
# Set by the empty-markdown and empty-fleet admission helpers when they refuse.
# shellcheck disable=SC2034 # Output global, read by the sourcing caller.
FM_BACKLOG_EMPTY_ERROR=

# Bounded execution is fm-timeout-lib.sh's alone; source it rather than
# re-deriving a deadline here. It is stateless, so the memoisation reason this
Expand Down Expand Up @@ -149,6 +164,95 @@ fm_backlog_file() { # <data-dir>
fi
}

fm_backlog_markdown_empty() { # <backlog-file>
local file=$1
FM_BACKLOG_EMPTY_ERROR=
if [ ! -r "$file" ] || [ ! -f "$file" ] || [ -L "$file" ]; then
FM_BACKLOG_EMPTY_ERROR="backlog is not a readable regular file at $file"
return 1
fi
if ! LC_ALL=C awk '
{
sub(/\r$/, "")
if (length($0) != 0) lines[++count]=$0
}
END {
start=1
if (lines[1] == "# Backlog") start=2
if (count == 1 && start == 2) exit 0
if (count - start + 1 != 3) exit 1
if (lines[start] != "## In flight") exit 1
if (lines[start + 1] != "## Queued") exit 1
if (lines[start + 2] != "## Done") exit 1
}
' "$file"; then
FM_BACKLOG_EMPTY_ERROR="backlog contains work or an unrecognized empty skeleton at $file"
return 1
fi
}

fm_backlog_empty_fleet_preflight() { # <state-dir> <data-dir>
local state=$1 data=$2 record root backend file
FM_BACKLOG_EMPTY_ERROR=
if [ -e "$state" ] || [ -L "$state" ]; then
if ! fm_backlog_directory_present "$state" "state directory"; then
FM_BACKLOG_EMPTY_ERROR=$FM_BACKLOG_TRANSITION_ERROR
return 1
fi
for record in "$state"/*.meta "$state"/*.status "$state"/*.backlog-close \
"$state"/*.inbox "$state"/.backlog-handoff-*.wake-pending \
"$state"/handoff/*.outbox.md; do
if [ -e "$record" ] || [ -L "$record" ]; then
FM_BACKLOG_EMPTY_ERROR="work-bearing task record is present at $record"
return 1
fi
done
fi
if [ ! -e "$data" ] && [ ! -L "$data" ]; then
root=${data%/*}
[ -n "$root" ] || root=/
if [ -e "$root/.tasks.toml" ] || [ -L "$root/.tasks.toml" ]; then
FM_BACKLOG_EMPTY_ERROR="the experimental empty-fleet preflight cannot validate a configured backlog without its data directory"
return 1
fi
return 0
fi
if ! fm_backlog_directory_present "$data" "data directory"; then
FM_BACKLOG_EMPTY_ERROR=$FM_BACKLOG_TRANSITION_ERROR
return 1
fi
for record in "$data"/handoff/*.outbox.md; do
if [ -e "$record" ] || [ -L "$record" ]; then
FM_BACKLOG_EMPTY_ERROR="work-bearing handoff record is present at $record"
return 1
fi
done
root=$(fm_backlog_root "$data") || {
FM_BACKLOG_EMPTY_ERROR=${FM_BACKLOG_TRANSITION_ERROR:-"data directory cannot be resolved: $data"}
return 1
}
if ! fm_backlog_config_present "$root" "$(fm_backlog_authorized_root "$data")"; then
FM_BACKLOG_EMPTY_ERROR=$FM_BACKLOG_TRANSITION_ERROR
return 1
fi
backend=$(fm_tasks_axi_backend "$root" 2>&1) || {
FM_BACKLOG_EMPTY_ERROR=$backend
return 1
}
if [ "$backend" != markdown ]; then
FM_BACKLOG_EMPTY_ERROR="the experimental empty-fleet preflight does not accept a non-markdown backlog backend"
return 1
fi
file=$(fm_backlog_file "$data") || {
FM_BACKLOG_EMPTY_ERROR=${FM_BACKLOG_TRANSITION_ERROR:-"backlog path cannot be resolved"}
return 1
}
if [ ! -e "$file" ] && [ ! -L "$file" ]; then
return 0
fi
fm_backlog_markdown_empty "$file"
}

# The directory a backlog's own `.tasks.toml` is resolved from.
fm_backlog_root() { # <data-dir>
local data parent
Expand Down
6 changes: 5 additions & 1 deletion bin/fm-bootstrap.sh
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,10 @@ DATA="${FM_DATA_OVERRIDE:-$FM_HOME/data}"
# deferred network stage sets, so an ordinary bootstrap run records nothing.
# shellcheck source=bin/fm-timing-lib.sh disable=SC1091
. "$SCRIPT_DIR/fm-timing-lib.sh"
# Sourced only for fm_session_pid_valid: the lock identity this script compares
# is not always a local pid.
# shellcheck source=bin/fm-session-lock-lib.sh disable=SC1091
. "$SCRIPT_DIR/fm-session-lock-lib.sh"

# Network-phase selection (see the header). An unrecognized value resolves to
# `all` so a malformed override runs every step rather than silently dropping a
Expand All @@ -213,7 +217,7 @@ network_phase() { [ "$FM_BOOTSTRAP_NETWORK_PHASE" != skip ]; }
network_mutation_authorized() {
local expected=${FM_BOOTSTRAP_NETWORK_LOCK_PID:-} current
[ -n "$expected" ] || return 0
case "$expected" in *[!0-9]*) return 1 ;; esac
fm_session_pid_valid "$expected" || return 1
[ -f "$STATE/.lock" ] && [ ! -L "$STATE/.lock" ] || return 1
current=$(cat "$STATE/.lock" 2>/dev/null) || return 1
[ "$current" = "$expected" ]
Expand Down
31 changes: 16 additions & 15 deletions bin/fm-claude-stop-autoarm.sh
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,15 @@
# secondmate home) with AGENTS.md, bin/, and the effective state dir - the
# exact fm-turnend-guard.sh scope. Child crew/scout worktrees stay inert.
# - Identity: only when THIS session holds state/.lock, as
# bin/fm-session-lock-lib.sh decides it: the recorded pid is a harness
# ancestor, or a live lock was recorded under this same trusted Claude
# session id (which is what keeps a background session arming after its
# transient helper chain is recycled).
# When an existing numeric owner fails the shared harness-liveness predicate,
# the hook delegates guarded recovery to bin/fm-lock.sh and then re-verifies
# ownership. A live owner, missing lock, malformed lock, or unresolved
# ancestry remains inert, so a competing session never arms or rewakes.
# bin/fm-session-lock-lib.sh decides it: the recorded owner belongs to this
# session's verified identity set, or a live lock was recorded under this
# same trusted Claude session id (which is what keeps a background session
# arming after its transient helper chain is recycled).
# When an existing recognized owner is proven dead by the shared liveness
# predicate, the hook delegates guarded recovery to bin/fm-lock.sh and then
# re-verifies ownership. A live or unknown owner, missing lock, malformed
# lock, or unresolved identity remains inert, so a competing session never
# arms or rewakes.
# - AFK: while state/.afk exists the away daemon owns the watcher and triage;
# this hook exits 0 and NEVER rewakes the primary (checked again at
# translation time so a mid-cycle AFK transition is honored).
Expand Down Expand Up @@ -59,8 +60,8 @@
# until the synchronous guard has consumed its attended fail-open.
#
# The epoch ledger state/.claude-autoarm-epoch records the latest claim
# generation and outcome, and binds rewake outcomes to the session-lock pid and
# watcher recovery generation, so the synchronous Stop guard
# generation and outcome, and binds rewake outcomes to the session-lock owner
# identity and watcher recovery generation, so the synchronous Stop guard
# (bin/fm-turnend-guard.sh --claude) can allow a stop whose recovery this hook
# already owns, instead of forcing a duplicate continuation for the same event
# epoch. The failure marker
Expand Down Expand Up @@ -124,17 +125,17 @@ fm_hook_payload_is_foreign_host "$PAYLOAD" && exit 0
fm_primary_scope_matches "$FM_ROOT" "$STATE" || exit 0

# --- identity: only the lock-owning session's hooks may arm ------------------
# A prior session may have died after leaving its numeric harness pid in .lock.
# Use the shared liveness predicate to recognize only that stale-owner case.
# A prior session may have died after leaving its harness identity in .lock.
# Use the shared liveness predicate to recognize only that stale-owner case; it
# resolves a tagged identity too, so a dead Windows session is still recoverable
# rather than being read as a malformed lock nobody may ever clear.
# Defer the mutating claim until after the unchanged AFK and need gates, so an
# idle or away home remains byte-for-byte inert. Missing or malformed locks are
# uncertainty rather than stale-owner evidence and remain inert.
RECOVER_SESSION_LOCK=0
if ! fm_session_lock_owned_by_self "$STATE"; then
LOCK_PID=$(cat "$STATE/.lock" 2>/dev/null || true)
case "$LOCK_PID" in
''|*[!0-9]*) exit 0 ;;
esac
fm_session_pid_valid "$LOCK_PID" || exit 0
fm_harness_pid_alive "$LOCK_PID" && exit 0
RECOVER_SESSION_LOCK=1
fi
Expand Down
22 changes: 17 additions & 5 deletions bin/fm-harness.sh
Original file line number Diff line number Diff line change
Expand Up @@ -52,18 +52,22 @@
# Ancestry - the nearest harness process in this process's parent chain. This
# is the structural fact about who actually owns the process tree,
# so it is what settles a disagreement.
# detect_own is the single owner of how the two combine; harness_marker and
# harness_ancestry only report evidence. Record each newly verified env marker
# in harness_marker, and each newly verified command name in harness_ancestry.
# Native owner - a launch-bound identity published by the experimental Windows
# launcher and authenticated through its native owner endpoint.
# When selected by its home record, it precedes marker and ancestry.
# detect_own is the single owner of how the three combine; harness_marker and
# harness_ancestry only report their evidence. Record each newly verified env
# marker in harness_marker, and each newly verified command name in
# harness_ancestry.
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}}"
CONFIG="${FM_CONFIG_OVERRIDE:-$FM_HOME/config}"

# shellcheck source=bin/fm-cursor-lib.sh
. "$SCRIPT_DIR/fm-cursor-lib.sh"
# shellcheck source=bin/fm-session-lock-lib.sh
. "$SCRIPT_DIR/fm-session-lock-lib.sh"
# shellcheck source=bin/fm-gemini-lib.sh
. "$SCRIPT_DIR/fm-gemini-lib.sh"

Expand Down Expand Up @@ -395,6 +399,14 @@ harness_family() {
# a harness-shaped path in some node process's arguments is weaker evidence
# than a harness publishing its own identity.
detect_own() {
local native_harness
case "$(uname -s)" in MINGW*|MSYS*|CYGWIN*)
if fm_native_owner_selected; then
native_harness=$(fm_native_owner_call harness 2>/dev/null) || { echo unknown; return; }
case "$native_harness" in codex) echo codex ;; *) echo unknown ;; esac
return
fi ;;
esac
local marker ancestry strength harness
marker=$(harness_marker)
ancestry=$(harness_ancestry)
Expand Down
Loading
Loading