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
5 changes: 4 additions & 1 deletion .agents/skills/bootstrap-diagnostics/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
name: bootstrap-diagnostics
description: >-
Agent-only handling playbook for session-start bootstrap diagnostics.
Use whenever the session-start digest's bootstrap section prints any diagnostic or capability line - MISSING, MISSING_MANUAL, BACKEND_INVALID, NEEDS_GH_AUTH, TANGLE, CREW_HARNESS_OVERRIDE, CREW_DISPATCH, FLEET_SYNC, SECONDMATE_SYNC, SECONDMATE_LIVENESS, TASKS_AXI, NUDGE_SECONDMATES, or FMX - or when a standalone bin/fm-bootstrap.sh run prints one.
Use whenever the session-start digest's bootstrap section prints any diagnostic or capability line - MISSING, MISSING_MANUAL, BACKEND_INVALID, NEEDS_GH_AUTH, TANGLE, GATE_FORK, CREW_HARNESS_OVERRIDE, CREW_DISPATCH, FLEET_SYNC, SECONDMATE_SYNC, SECONDMATE_LIVENESS, TASKS_AXI, NUDGE_SECONDMATES, or FMX - or when a standalone bin/fm-bootstrap.sh run prints one.
A silent bootstrap section means all good and needs no skill load.
user-invocable: false
metadata:
Expand All @@ -26,6 +26,9 @@ The inline rules in `AGENTS.md` section 3 still bind: detect, then consent, then
- `TANGLE: <remediation>` - the primary checkout is stranded on a feature branch instead of its default branch; `AGENTS.md` section 8 explains why this guard exists and what it protects.
The work is safe on that branch ref; restore the primary to its default branch with the printed `git -C <root> checkout <default>`, then re-validate that branch in a proper worktree.
This is the only sanctioned firstmate-initiated git write to the primary, and it is a non-destructive branch switch that strands nothing.
- `GATE_FORK: <working_path>: ... Fix: (cd <working_path> && no-mistakes init --fork-url git@github.com:<login>/<repo>.git)` - that gate-initialized repo has no push-writable fork target while the current gh user cannot push to its upstream, so the next no-mistakes ship there would run the whole review+test+doc+lint pipeline and only then 403 at the push step (the `project-management` skill's Initialize section explains the fork-contributed-upstream init requirement).
This is a captain-owned local fix, not a firstmate git write: relay the printed `no-mistakes init --fork-url ...` for the captain to run (`! (cd ... && no-mistakes init --fork-url ...)`), or run it yourself only with the captain's say-so, then rerun session start to confirm the line clears before dispatching a ship task to that repo.
It never blocks a scout task or work on an already-fork-routed repo.
- `CREW_HARNESS_OVERRIDE: <name>` - record and use the override silently; surface a harness fact only if it actually blocks work or the captain asks.
- `CREW_DISPATCH: invalid config/crew-dispatch.json - <reason>` - the optional dispatch profile file exists but failed low-cost bootstrap validation; continue with the normal fallback chain, resolve and pass the chosen fallback harness explicitly while the file remains present, fix the malformed schema, unverified harness name, unknown selector, or invalid harness/effort pair when convenient, and do not select a bad profile.
- `CREW_DISPATCH: active config/crew-dispatch.json` - bootstrap validated the optional dispatch profile file and printed its active rules and `default:` when present.
Expand Down
4 changes: 4 additions & 0 deletions .agents/skills/project-management/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,10 @@ Run no-mistakes initialization only for `no-mistakes` projects:
cd projects/<name> && no-mistakes init && no-mistakes doctor
```

When the current GitHub user cannot push to the project's upstream, as with a fork-contributed upstream such as the firstmate repo itself, initialize with `no-mistakes init --fork-url git@github.com:<you>/<repo>.git` instead, so the gate pushes branches to the writable fork and opens the PR cross-repo.
Plain `no-mistakes init` is only for an upstream you can push to directly.
`CONTRIBUTING.md` owns the fork onboarding detail, and `bin/fm-gate-fork-assert.sh`, surfaced as the session-start `GATE_FORK:` line, catches a missing or non-writable fork target before a ship wastes the whole pipeline and 403s at push.

Initialization configures the local gate and does not vendor a no-mistakes skill into the project.
Do not create a commit merely because initialization ran.
If doctor reports an environment, authentication, or daemon problem, resolve that blocker before dispatching work and never restart the shared daemon from a project operation.
Expand Down
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -353,7 +353,7 @@ It performs guarded fast-forward updates of firstmate and registered secondmate

These skills are not captain-invocable; load them only at their precise triggers.

- `bootstrap-diagnostics` - load whenever the session-start digest's bootstrap section prints any diagnostic or capability line (`MISSING:`, `MISSING_MANUAL:`, `BACKEND_INVALID:`, `NEEDS_GH_AUTH`, `TANGLE:`, `CREW_HARNESS_OVERRIDE:`, `CREW_DISPATCH:`, `FLEET_SYNC:`, `SECONDMATE_SYNC:`, `SECONDMATE_LIVENESS:`, `TASKS_AXI:`, `NUDGE_SECONDMATES:`, or `FMX:`); silence needs no load.
- `bootstrap-diagnostics` - load whenever the session-start digest's bootstrap section prints any diagnostic or capability line at all (the skill's own trigger list enumerates them, and `bin/fm-bootstrap.sh`'s header owns which lines exist); silence needs no load.
- `diagnostic-reasoning` - load before scoping a reported bug and before acting on a diagnostic report.
- `harness-adapters` - load before spawning or recovering a crewmate or secondmate, handling a trust dialog, sending a harness-specific skill invocation, interrupting or exiting an agent, resuming an exited agent, or verifying a new harness adapter.
- `firstmate-orca` - load before switching to Orca, spawning or supervising Orca-backed work, smoke-testing Orca backend behavior, debugging Orca task state, or reconciling Orca-backed task metadata.
Expand Down
8 changes: 6 additions & 2 deletions bin/backends/herdr.sh
Original file line number Diff line number Diff line change
Expand Up @@ -739,7 +739,11 @@ FM_BACKEND_HERDR_IDLE_RE=${FM_BACKEND_HERDR_IDLE_RE:-'^Type a message\.\.\.$'}
# Known bare (unbordered) prompt glyphs a composer row may start with: ❯
# (claude) and › (codex) only. Generic shell-style glyphs > $ % # are still
# recognized after a bordered composer row has already been structurally found.
FM_BACKEND_HERDR_BARE_PROMPT_RE=${FM_BACKEND_HERDR_BARE_PROMPT_RE:-'^[❯›]'}
# Alternation, not a [❯›] bracket class: under a byte-oriented locale (LC_CTYPE=C)
# grep walks a bracket class byte-wise, so [❯›] matches any row whose first byte is
# 0xE2 - including the box-drawing rows ╭ ╰ │ - and misreads a composer border as a
# bare prompt. The (❯|›) form matches each full glyph sequence, correct in any locale.
FM_BACKEND_HERDR_BARE_PROMPT_RE=${FM_BACKEND_HERDR_BARE_PROMPT_RE:-'^(❯|›)'}
# Pi allows a multi-line composer between its horizontal separators. Bound the
# structural candidate so two unrelated transcript rules with an arbitrarily
# large region between them can never be promoted into a composer.
Expand Down Expand Up @@ -903,7 +907,7 @@ EOF
fi
# Delegate the empty/pending/unknown decision to the shared owner. The bare
# shape only ever starts with an AGENT glyph (FM_BACKEND_HERDR_BARE_PROMPT_RE
# is '^[❯›]'), so a bare shell prompt never reaches here - it stays 'unknown'
# is '^(❯|›)'), so a bare shell prompt never reaches here - it stays 'unknown'
# via the no-composer-row path above, exactly as before.
fm_composer_classify_content "$bordered" "$stripped" "$FM_BACKEND_HERDR_IDLE_RE"
}
Expand Down
10 changes: 10 additions & 0 deletions bin/fm-bootstrap.sh
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
# "CREW_DISPATCH: active config/crew-dispatch.json" plus indented rules,
# "FLEET_SYNC: <repo>: skipped|recovered|STUCK: <detail>",
# "TASKS_AXI: available", "TANGLE: <remediation>",
# "GATE_FORK: <working_path>: ... Fix: no-mistakes init --fork-url ...",
# "SECONDMATE_SYNC: secondmate <id>: skipped: <reason>",
# "NUDGE_SECONDMATES: fm-<id>...",
# "SECONDMATE_LIVENESS: secondmate <id>: already-live|respawned|skipped: <reason>|respawn failed: <reason>",
Expand Down Expand Up @@ -41,6 +42,10 @@
# A TANGLE line means the firstmate primary checkout (FM_ROOT) is stranded
# on a feature branch instead of its default branch - a crewmate's work
# landed in the primary instead of its own worktree; restore it per the line.
# A GATE_FORK line means a gate-initialized repo whose upstream the
# current gh user cannot push to has a missing or non-writable fork_url,
# so the next no-mistakes ship would 403 at the push step (7 of 9) after
# the whole pipeline; fm-gate-fork-assert.sh owns the check and message.
# treehouse is also MISSING when its installed version lacks
# "treehouse get --lease" support.
# no-mistakes is also MISSING when its installed version is older than
Expand Down Expand Up @@ -624,6 +629,11 @@ if [ -n "$tangle_branch" ]; then
echo "TANGLE: primary checkout on feature branch '$tangle_branch' (expected '$tangle_default'); the work is safe on that ref - restore the primary with: git -C $FM_ROOT checkout $tangle_default, then re-validate the branch in a proper worktree"
fi
fi
# Fork-push pre-flight: a gate-initialized repo whose upstream the current gh
# user cannot push to must have a push-writable fork_url, or the next ship 403s
# at the push step after the whole pipeline. fm-gate-fork-assert.sh owns the
# check and the GATE_FORK: line; read-only, best-effort, never fatal here.
"$SCRIPT_DIR/fm-gate-fork-assert.sh" 2>/dev/null || true
crew=
[ -f "$CONFIG/crew-harness" ] && crew=$(tr -d '[:space:]' < "$CONFIG/crew-harness" || true)
[ -n "$crew" ] && [ "$crew" != "default" ] && echo "CREW_HARNESS_OVERRIDE: $crew"
Expand Down
132 changes: 132 additions & 0 deletions bin/fm-gate-fork-assert.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
#!/usr/bin/env bash
# fm-gate-fork-assert.sh - pre-flight the no-mistakes fork-push target.
#
# The no-mistakes gate pushes a validated branch and opens its PR at step 7 of 9
# (review -> test -> document -> lint -> push -> pr -> ci). When the current gh
# user has no write access to a gate-initialized repo's UPSTREAM, that push must
# go to a FORK instead, routed by the repo's fork_url in the local, gitignored
# gate DB (~/.no-mistakes/state.sqlite). That routing is a one-time hand-applied
# `no-mistakes init --fork-url ...` step: it is absent from a fresh gate DB, a
# reinstall, or a new firstmate home, and nothing else verifies it. When it is
# missing, the whole multi-hour review+test+doc+lint pipeline runs first and only
# THEN 403s at push, throwing the validated work away (5 of 20 historical
# firstmate runs died exactly this way). See data/fm-pr-stuck-w4/report.md.
#
# This check converts that multi-hour silent waste into a one-line session-start
# warning. For each gate-initialized repo whose UPSTREAM the current gh user
# cannot push to, it asserts the repo has a push-writable fork_url, and prints
# one GATE_FORK: line per repo that would 403 at push:
#
# GATE_FORK: <working_path>: no push access to <upstream_slug> and <no fork
# target|fork <fork_slug> not push-writable>; the next ship 403s at push after
# the full pipeline. Fix: (cd <working_path> && no-mistakes init --fork-url
# git@github.com:<gh_login>/<repo>.git)
#
# It exits non-zero when any repo is flagged, 0 when every repo is healthy. It is
# read-only (only `gh api` reads and a sqlite SELECT), detect-only, and
# best-effort: it stays silent and exits 0 when it cannot decide (no gate DB, no
# sqlite3, gh absent or unauthenticated - NEEDS_GH_AUTH owns that case, and a
# non-github upstream it cannot verify).
#
# bin/fm-bootstrap.sh runs it as a detect line, like the TANGLE guard, and
# bootstrap-diagnostics owns the response.
#
# Env:
# FM_GATE_STATE_DB override the gate DB path (default ~/.no-mistakes/state.sqlite)
set -u

DB=${FM_GATE_STATE_DB:-$HOME/.no-mistakes/state.sqlite}

# Silent, healthy no-op when we cannot decide: nothing to assert, or NEEDS_GH_AUTH
# (owned by bootstrap) already covers the missing/unauthenticated gh case.
[ -f "$DB" ] || exit 0
command -v sqlite3 >/dev/null 2>&1 || exit 0
command -v gh >/dev/null 2>&1 || exit 0
gh auth status >/dev/null 2>&1 || exit 0
GH_LOGIN=$(gh api user --jq .login 2>/dev/null || true)
[ -n "$GH_LOGIN" ] || exit 0

# Memoize push-access verdicts so repos sharing an upstream cost one gh call.
CACHE=$(mktemp "${TMPDIR:-/tmp}/fm-gate-fork.XXXXXX") || exit 0
trap 'rm -f "$CACHE"' EXIT

# Echo "owner/repo" for a github URL (https, ssh, or scp-style), or return 1 for
# any non-github or malformed URL (which we cannot verify and must not flag).
gate_github_slug() {
local url=$1 rest
case $url in
https://github.com/*) rest=${url#https://github.com/} ;;
http://github.com/*) rest=${url#http://github.com/} ;;
ssh://git@github.com/*) rest=${url#ssh://git@github.com/} ;;
git@github.com:*) rest=${url#git@github.com:} ;;
*) return 1 ;;
esac
rest=${rest%.git}
rest=${rest%/}
case $rest in
*/*/*) return 1 ;; # more than owner/repo
*/*)
[ -n "${rest%/*}" ] && [ -n "${rest#*/}" ] || return 1
printf '%s\n' "$rest"
return 0 ;;
esac
return 1
}

# Echo the current gh user's push-access verdict for github slug $1:
# push - gh confirmed .permissions.push is true
# nopush - gh confirmed .permissions.push is false
# unknown - the gh call failed or returned unparseable output (cannot decide)
# Verdicts (including unknown) are memoized so repos sharing a slug cost one call.
gate_push_verdict() {
local slug=$1 verdict perm rc
verdict=$(grep -F " $slug " "$CACHE" 2>/dev/null | cut -f1 || true)
if [ -z "$verdict" ]; then
perm=$(gh api "repos/$slug" --jq '.permissions.push // false' 2>/dev/null)
rc=$?
if [ "$rc" -ne 0 ]; then
verdict=unknown
else
case $perm in
true) verdict=push ;;
false) verdict=nopush ;;
*) verdict=unknown ;;
esac
fi
printf '%s\t%s\t\n' "$verdict" "$slug" >> "$CACHE"
fi
printf '%s\n' "$verdict"
}

problem=0
while IFS='|' read -r working_path upstream_url fork_url; do
[ -n "$upstream_url" ] || continue
upstream_slug=$(gate_github_slug "$upstream_url") || continue
# Only flag when we AFFIRMATIVELY know the upstream is unwritable; a writable
# upstream (no fork needed) or an undecidable probe both stay silent.
[ "$(gate_push_verdict "$upstream_slug")" = nopush ] || continue

fork_state=
if [ -z "$fork_url" ]; then
fork_state="no fork target"
else
fork_slug=$(gate_github_slug "$fork_url" || true)
if [ -z "$fork_slug" ]; then
fork_state="fork $fork_url not push-writable"
else
case "$(gate_push_verdict "$fork_slug")" in
push) ;;
nopush) fork_state="fork $fork_slug not push-writable" ;;
*) continue ;;
esac
fi
fi
[ -n "$fork_state" ] || continue

repo_name=${upstream_slug#*/}
printf 'GATE_FORK: %s: no push access to %s and %s; the next ship 403s at push after the full pipeline. Fix: (cd %s && no-mistakes init --fork-url git@github.com:%s/%s.git)\n' \
"$working_path" "$upstream_slug" "$fork_state" "$working_path" "$GH_LOGIN" "$repo_name"
problem=1
done < <(sqlite3 -separator '|' "$DB" "SELECT working_path, upstream_url, IFNULL(fork_url,'') FROM repos;" 2>/dev/null)

[ "$problem" -eq 0 ]
2 changes: 1 addition & 1 deletion docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -341,7 +341,7 @@ FM_BACKEND= # optional runtime backend override for new spawns; tmux
HERDR_SESSION=default # herdr-only: named session for normal backend ops; not enough for destructive cleanup (docs/herdr-backend.md)
FM_BACKEND_HERDR_COMPOSER_LINES=20 # herdr-only: tail lines scanned by composer-state guard/fallback paths; idle-baseline submit confirmation uses agent-state
FM_BACKEND_HERDR_IDLE_RE='^Type a message\.\.\.$' # herdr-only: empty-composer placeholder regex after shared ghost extraction plus border and prompt stripping
FM_BACKEND_HERDR_BARE_PROMPT_RE='^[❯›]' # herdr-only: verified agent glyphs recognized as an UNBORDERED (bare) composer row, e.g. claude's ❯ or codex's ›; shell glyphs remain unknown rather than empty, and de-emphasised ghost/placeholder text (dim or dark-truecolor) after an agent prompt reads empty via the shared fm_composer_strip_ghost (docs/herdr-backend.md "Incident (2026-07-08)", "Incident (2026-07-10)")
FM_BACKEND_HERDR_BARE_PROMPT_RE='^(❯|›)' # herdr-only: verified agent glyphs recognized as an UNBORDERED (bare) composer row, e.g. claude's ❯ or codex's ›; shell glyphs remain unknown rather than empty, and de-emphasised ghost/placeholder text (dim or dark-truecolor) after an agent prompt reads empty via the shared fm_composer_strip_ghost (docs/herdr-backend.md "Incident (2026-07-08)", "Incident (2026-07-10)"); spell an override as an alternation, never a [❯›] bracket class, which a byte-oriented locale matches byte-wise and misreads composer borders through (see the default's comment in bin/backends/herdr.sh)
FM_BACKEND_HERDR_PI_COMPOSER_MAX_LINES=8 # herdr-only: maximum rows admitted between Pi's native-identity-corroborated separator pair; taller or ambiguous candidates stay unknown (docs/herdr-backend.md "Incident (2026-07-14)")
FM_BACKEND_HERDR_SUBMIT_POLLS=6 # herdr-only: agent-state samples spread across each Enter attempt's budget when confirming a submit (docs/herdr-backend.md "Native agent-state submit confirmation")
FM_BACKEND_HERDR_SUBMIT_MIN_SLEEP=0.6 # herdr-only: minimum per-Enter confirmation budget before polling agent-state after an idle baseline
Expand Down
1 change: 1 addition & 0 deletions docs/scripts.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ The shared no-mistakes gate refusal used by `fm-spawn.sh`, `fm-send.sh`, and `fm
| ------------------------ | ------------------------------------------------------------------------------------ |
| `fm-session-start.sh` | Compose lock, bootstrap, and wake drain into the single ordered session-start digest |
| `fm-bootstrap.sh` | Detect toolchain and fleet problems, run the locked session-start sweeps, and install approved tools |
| `fm-gate-fork-assert.sh` | Assert a push-writable fork target for each gate-initialized repo whose upstream this gh user cannot push to, so a ship cannot 403 at push after the whole pipeline |
| `fm-fleet-sync.sh` | Refresh project clones with safe fast-forwards, self-heals, `STUCK:` reports, branch pruning, and bounded recovery from an orphaned `.git/packed-refs.lock` |
| `fm-fleet-snapshot.sh` | Print the read-only structured fleet snapshot JSON (schema `fm-fleet-snapshot.v1`) |
| `fm-fleet-view.sh` | Render the fleet snapshot as a human Markdown view |
Expand Down
Loading
Loading