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
1 change: 1 addition & 0 deletions .agents/skills/harness-adapters/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@ This preserves launch success instead of passing a known-bad value.

Send the validation skill using the target harness's skill invocation form.
Natural language is acceptable if uncertain.
`fm-send` refuses a bare installed-skill token whose prefix conflicts with the resolved harness, and names the correct form instead of silently rewriting a caller bug.

- claude: `/<skill>`, for example `/no-mistakes`.
- codex: `$<skill>`, for example `$no-mistakes`; `/<skill>` is claude-only and codex rejects it as "Unrecognized command".
Expand Down
39 changes: 21 additions & 18 deletions bin/fm-account-continuation.sh
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,26 @@ cd "$WORKTREE_REAL" \
exec 9< . \
|| { echo "error: continuation worktree cannot be pinned for $ID" >&2; exit 1; }
cd "$ORIGINAL_CWD" || exit 1

if [ "$KIND" = secondmate ]; then
BRIEF_SNAPSHOT_TMP=$(mktemp "$STATE/.continuation-brief-$ID.XXXXXX") \
|| { echo "error: cannot stage original brief or charter for continuation of $ID" >&2; exit 1; }
if python3 "$SCRIPT_DIR/fm-contained-read.py" cat-optional-fd data/charter.md "$MAX_PACKET_BYTES" 3<&9 \
> "$BRIEF_SNAPSHOT_TMP" 2>/dev/null; then
[ -s "$BRIEF_SNAPSHOT_TMP" ] \
|| { echo "error: secondmate charter is empty for continuation of $ID" >&2; exit 1; }
else
charter_status=$?
if [ "$charter_status" -eq 3 ]; then
rm -f "$BRIEF_SNAPSHOT_TMP"
BRIEF_SNAPSHOT_TMP=
else
echo "error: secondmate charter is present but unsafe for continuation of $ID" >&2
exit 1
fi
fi
fi

git_pinned() {
python3 "$SCRIPT_DIR/fm-contained-read.py" git-fd "$@" 3<&9
}
Expand Down Expand Up @@ -293,24 +313,7 @@ if ! node "$CONTAINED_READ" snapshot "$TASK_DIR" "$TASK_SNAPSHOT_DIR" 1048576 "$
exit 1
fi

if [ "$KIND" = secondmate ]; then
BRIEF_SNAPSHOT_TMP=$(mktemp "$STATE/.continuation-brief-$ID.XXXXXX") \
|| { echo "error: cannot stage original brief or charter for continuation of $ID" >&2; exit 1; }
if python3 "$SCRIPT_DIR/fm-contained-read.py" cat-optional-fd data/charter.md "$MAX_PACKET_BYTES" 3<&9 \
> "$BRIEF_SNAPSHOT_TMP" 2>/dev/null; then
[ -s "$BRIEF_SNAPSHOT_TMP" ] \
|| { echo "error: secondmate charter is empty for continuation of $ID" >&2; exit 1; }
else
charter_status=$?
if [ "$charter_status" -eq 3 ]; then
rm -f "$BRIEF_SNAPSHOT_TMP"
BRIEF_SNAPSHOT_TMP="$TASK_SNAPSHOT_DIR/0.snapshot"
else
echo "error: secondmate charter is present but unsafe for continuation of $ID" >&2
exit 1
fi
fi
else
if [ "$KIND" != secondmate ] || [ -z "$BRIEF_SNAPSHOT_TMP" ]; then
BRIEF_SNAPSHOT_TMP="$TASK_SNAPSHOT_DIR/0.snapshot"
fi
[ -s "$BRIEF_SNAPSHOT_TMP" ] \
Expand Down
127 changes: 124 additions & 3 deletions bin/fm-account-directory.sh
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
# Select and prepare direct Claude or Codex account-directory launches.
# Usage:
# fm-account-directory.sh select <claude|codex>
# fm-account-directory.sh check-credential <claude|codex> <account-home>
# fm-account-directory.sh install-herdr-hook <claude|codex> <account-home>
# fm-account-directory.sh prepare <claude|codex>
#
Expand All @@ -21,8 +22,15 @@
# selects the first real account directory in stable bytewise sort order.
# Selection prints only the chosen absolute account home on stdout and logs
# health, fallback, and choice diagnostics on stderr.
# prepare selects the account and idempotently runs Herdr's own integration
# installer with CODEX_HOME or CLAUDE_CONFIG_DIR set to the chosen home.
# prepare selects the account, verifies a usable credential before endpoint
# creation can begin, and idempotently runs Herdr's own integration installer
# with CODEX_HOME or CLAUDE_CONFIG_DIR set to the chosen home.
# Codex uses a cheap read-only auth.json credential-material check.
# Claude has no sufficient on-disk credential marker on macOS, so it uses the
# CLI's local `auth status --json` check, bounded to two seconds and never making
# a model call.
# A failed check names the selected account home and prints the exact scoped
# provider login command a human can run.
# It verifies the installed per-profile hook before printing the chosen home.
#
# Credential state is read-only.
Expand All @@ -34,7 +42,7 @@ set -u
TEST_LAB_TOKEN=firstmate-account-directory-test-lab-v1

usage() {
sed -n '2,32p' "$0" | sed 's/^# \{0,1\}//' >&2
sed -n '2,40p' "$0" | sed 's/^# \{0,1\}//' >&2
}

log() {
Expand All @@ -45,6 +53,12 @@ test_lab_enabled() {
[ "${FM_ACCOUNT_DIRECTORY_TEST_LAB:-}" = "$TEST_LAB_TOKEN" ]
}

shell_quote() {
printf "'"
printf '%s' "$1" | sed "s/'/'\\\\''/g"
printf "'"
}

system_perl() {
if test_lab_enabled && [ -n "${FM_ACCOUNT_DIRECTORY_PERL_BIN:-}" ]; then
printf '%s\n' "$FM_ACCOUNT_DIRECTORY_PERL_BIN"
Expand Down Expand Up @@ -203,6 +217,108 @@ valid_account_home() { # <vendor-dir> <candidate>
esac
}

provider_binary() { # <vendor> <account-home>
local vendor=$1 account_home=$2 override manifest binary install_path home perl_bin
if test_lab_enabled; then
case "$vendor" in
claude) override=${FM_ACCOUNT_DIRECTORY_CLAUDE_BIN:-} ;;
codex) override=${FM_ACCOUNT_DIRECTORY_CODEX_BIN:-} ;;
*) return 1 ;;
esac
if [ -n "$override" ]; then
[ -f "$override" ] && [ ! -L "$override" ] && [ -x "$override" ] || {
echo "error: test $vendor provider binary is not a real executable: $override" >&2
return 1
}
printf '%s\n' "$override"
return 0
fi
fi
manifest=$account_home/.agent-fleet-provider-binary.json
if [ -f "$manifest" ] && [ ! -L "$manifest" ]; then
binary=$(jq -er '.binary.resolved_path | select(type == "string" and startswith("/"))' "$manifest" 2>/dev/null) || binary=
if [ -n "$binary" ] && [ -f "$binary" ] && [ ! -L "$binary" ] && [ -x "$binary" ]; then
printf '%s\n' "$binary"
return 0
fi
fi
home=$(passwd_home) || return 1
install_path=$home/.local/bin/$vendor
perl_bin=$(system_perl) || return 1
# shellcheck disable=SC2016 # Perl source is intentionally single-quoted.
binary=$("$perl_bin" -MCwd=realpath -e '
my $path = realpath($ARGV[0]);
exit 1 unless defined $path && $path =~ m{^/} && $path !~ /[\x00-\x1f\x7f]/;
print $path;
' "$install_path" 2>/dev/null) || binary=
if [ -n "$binary" ] && [ -f "$binary" ] && [ ! -L "$binary" ] && [ -x "$binary" ]; then
printf '%s\n' "$binary"
return 0
fi
echo "error: cannot resolve the $vendor provider binary for account credential verification at $account_home" >&2
return 1
}

credential_login_command() { # <vendor> <account-home> [provider-binary]
local vendor=$1 account_home=$2 provider_bin=${3:-$1}
case "$vendor" in
codex)
printf 'CODEX_HOME=%s %s login' "$(shell_quote "$account_home")" "$(shell_quote "$provider_bin")"
;;
claude)
printf 'CLAUDE_CONFIG_DIR=%s %s auth login' "$(shell_quote "$account_home")" "$(shell_quote "$provider_bin")"
;;
esac
}

check_codex_credential() { # <account-home>
local account_home=$1 credential provider_bin login
credential=$account_home/auth.json
provider_bin=$(provider_binary codex "$account_home" 2>/dev/null || printf 'codex')
login=$(credential_login_command codex "$account_home" "$provider_bin")
if [ ! -f "$credential" ] || [ -L "$credential" ] || ! jq -e '
((.tokens.access_token? | type) == "string" and (.tokens.access_token | length) > 0
and (.tokens.refresh_token? | type) == "string" and (.tokens.refresh_token | length) > 0)
or ((.OPENAI_API_KEY? | type) == "string" and (.OPENAI_API_KEY | length) > 0)
' "$credential" >/dev/null 2>&1; then
echo "error: selected codex account directory '$account_home' has no usable on-disk credential; run: $login" >&2
return 1
fi
}

check_claude_credential() { # <account-home>
local account_home=$1 provider_bin status_json login
provider_bin=$(provider_binary claude "$account_home") || return 1
login=$(credential_login_command claude "$account_home" "$provider_bin")
status_json=$(run_bounded 2 /usr/bin/env CLAUDE_CONFIG_DIR="$account_home" \
"$provider_bin" auth status --json 2>/dev/null) || status_json=
if ! jq -e '.loggedIn == true' >/dev/null 2>&1 <<EOF
$status_json
EOF
then
echo "error: selected claude account directory '$account_home' has no usable credential; run: $login" >&2
return 1
fi
}

check_credential() { # <vendor> <account-home>
local vendor=$1 account_home=$2 root vendor_dir
root=$(account_root) || return 1
vendor_dir=$root/$vendor
valid_account_home "$vendor_dir" "$account_home" || {
echo "error: unsafe $vendor account home for credential verification: $account_home" >&2
return 1
}
case "$vendor" in
codex) check_codex_credential "$account_home" ;;
claude) check_claude_credential "$account_home" ;;
*)
echo "error: account credential verification supports only claude or codex, not '$vendor'" >&2
return 1
;;
esac
}

first_account_home() { # <vendor>
local vendor=$1 root vendor_dir candidate
root=$(account_root) || return 1
Expand Down Expand Up @@ -381,13 +497,18 @@ case "${1:-}" in
[ "$#" -eq 2 ] || { usage; exit 2; }
select_account "$2"
;;
check-credential)
[ "$#" -eq 3 ] || { usage; exit 2; }
check_credential "$2" "$3"
;;
install-herdr-hook)
[ "$#" -eq 3 ] || { usage; exit 2; }
install_herdr_hook "$2" "$3"
;;
prepare)
[ "$#" -eq 2 ] || { usage; exit 2; }
selected_home=$(select_account "$2") || exit 1
check_credential "$2" "$selected_home" || exit 1
install_herdr_hook "$2" "$selected_home" || exit 1
printf '%s\n' "$selected_home"
;;
Expand Down
17 changes: 16 additions & 1 deletion bin/fm-brief.sh
Original file line number Diff line number Diff line change
Expand Up @@ -223,7 +223,18 @@ EOF
)
fi

BROWSER_SECTION=$(cat <<'EOF'
# Browser automation safety
Never set `CHROME_DEVTOOLS_AXI_AUTO_CONNECT=1` or `CHROME_DEVTOOLS_AXI_HEADED=1`.
Always set your own `CHROME_DEVTOOLS_AXI_SESSION`.
When an authenticated dashboard is needed, set `CHROME_DEVTOOLS_AXI_USER_DATA_DIR` to a per-crewmate profile directory; the persistent profile keeps its login cookie across headless runs.
Never use `open -a "Google Chrome"` or AppleScript that focuses an app.
If a check genuinely cannot run headlessly, report the exact step that fails instead of falling back to a visible or auto-connected browser.
EOF
)

if [ "$KIND" = scout ]; then
REPORT_HEADINGS=$(fm_completion_report_required_headings)
cat > "$BRIEF" <<EOF
You are a crewmate: an autonomous worker agent managed by firstmate. Work on your own; do not wait for a human.

Expand All @@ -232,6 +243,8 @@ You are a crewmate: an autonomous worker agent managed by firstmate. Work on you

$HERDR_SECTION

$BROWSER_SECTION

# Setup
You are in a disposable git worktree of $REPO, at a detached HEAD on a clean default branch.
This is a SCOUT task: the deliverable is a written report, not a PR.
Expand Down Expand Up @@ -262,7 +275,7 @@ The report is the only thing that survives, so anything worth keeping must be in

# Definition of done
Write your findings to \`$DATA/$ID/report.md\`.
Use these level-two sections: Summary, What changed, Verification, Visual evidence, Artifacts, and Follow-ups.
Use these exact level-two headings in this order: $REPORT_HEADINGS.
The report must stand alone: what you did, what you found, the evidence (commands run, output, file:line references), and what you recommend.
When the report is complete, append \`done: {one-line conclusion}\` to the status file and stop.
If your findings reveal work that should ship (e.g. you reproduced a bug and the fix is clear), say so in the report; firstmate may promote this task in place, and you would then receive mode-specific ship instructions as a follow-up message.
Expand Down Expand Up @@ -365,6 +378,8 @@ You are a crewmate: an autonomous worker agent managed by firstmate. Work on you

$HERDR_SECTION

$BROWSER_SECTION

# Setup
You are in a disposable git worktree of $REPO, at a detached HEAD on a clean default branch.

Expand Down
5 changes: 4 additions & 1 deletion bin/fm-promote.sh
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ DATA="${FM_DATA_OVERRIDE:-$FM_HOME/data}"
fm_refuse_if_gate_agent
# shellcheck source=bin/fm-account-routing-lib.sh
. "$SCRIPT_DIR/fm-account-routing-lib.sh"
# shellcheck source=bin/fm-report-contract-lib.sh
. "$SCRIPT_DIR/fm-report-contract-lib.sh"
"$FM_ROOT/bin/fm-guard.sh" || true
ID=$1
META="$STATE/$ID.meta"
Expand Down Expand Up @@ -48,7 +50,8 @@ fm_account_lifecycle_lock_release "$LIFECYCLE_LOCK" || exit 1
LIFECYCLE_LOCK=
trap - EXIT

MESSAGE="<ship instructions: review scratch state with git status and git log; reset to a clean default-branch base; carry over only intended fix changes; create branch fm/$ID; implement; write $DATA/$ID/completion.md with sections Summary, What changed, Verification, Visual evidence, Artifacts, and Follow-ups; report done>"
REPORT_HEADINGS=$(fm_completion_report_required_headings)
MESSAGE="<ship instructions: review scratch state with git status and git log; reset to a clean default-branch base; carry over only intended fix changes; create branch fm/$ID; implement; write $DATA/$ID/completion.md with exact headings $REPORT_HEADINGS; report done>"
MESSAGE_Q=$(printf '%s' "$MESSAGE" | sed "s/'/'\\\\''/g")
echo "promoted $ID to ship (teardown protection restored)"
printf "next: FM_HOME=%q bin/fm-send.sh fm-%s '%s'\n" "$FM_HOME" "$ID" "$MESSAGE_Q"
12 changes: 9 additions & 3 deletions bin/fm-report-contract-lib.sh
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,18 @@
# managed continuation prompts.
# The durable publication and storage contract lives in docs/report-stack.md.

fm_completion_report_required_headings() {
# shellcheck disable=SC2016 # Markdown backticks must remain literal output.
printf '%s' '`## Summary`, `## What changed`, `## Verification`, `## Visual evidence`, `## Artifacts`, `## Follow-ups`'
}

fm_completion_report_contract() { # <data-dir> <task-id>
local data=$1 task=$2
local data=$1 task=$2 headings
headings=$(fm_completion_report_required_headings)
printf '%s\n' \
'# Completion report' \
"Before the final \`done:\` status, write \`$data/$task/completion.md\` with these six sections, each as a LEVEL-TWO markdown heading spelled exactly: \`## Summary\`, \`## What changed\`, \`## Verification\`, \`## Visual evidence\`, \`## Artifacts\`, \`## Follow-ups\`." \
"Publication rejects the report if any of those headings is missing, spelled differently, or written at another level - a level-one \`# Summary\` fails. Each section needs substantive prose directly under it; if you use sub-headings inside a section, make them \`###\` so they nest rather than ending the section." \
"Before the final \`done:\` status, write \`$data/$task/completion.md\` with these six exact level-two headings in this order: $headings." \
"Keep all six at level two; publication also accepts a report that consistently uses the level-one equivalents as its section structure, but do not mix heading levels. Each section needs substantive prose directly under it; if you use sub-headings inside a section, make them \`###\` so they nest rather than ending the section." \
'When a section genuinely does not apply, say so in a sentence under the heading rather than omitting the heading.' \
'Make it stand alone for the captain: explain the outcome, name important files or links, record the validation performed, and call out remaining risk or decisions.' \
"Put screenshots, diagrams, or other visual artifacts under \`$data/$task/visuals/\` and reference them from the report when they materially help review." \
Expand Down
Loading
Loading