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
8 changes: 5 additions & 3 deletions .agents/skills/refit/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,14 @@ The pass never updates tooling, pushes, restarts daemons, or merges on its own.

### 1. CURRENCY

Run `bin/fm-upstream-check.sh` for the upstream gap.
Run `bin/fm-upstream-check.sh` for the upstream content verdict and its commit-identity context.
Treat the content verdict as the sync decision, never the raw ancestry count.
A nonzero ancestry gap with zero incoming content is squash residue from a prior refit, so report the fork as content-current and do not sync.
Run `bin/fm-external-tooling-check.sh` for version drift and its `safe-anytime` versus `needs-quiet-fleet` coordination.
Preserve those helpers' existing behavior exactly.
Also report currency for the globally installed agent skills under `~/.agents/skills/`, which every harness and every worker discovers and which no other leg covers.
Read their installed state only; `skills update -g` is the update action and stays subject to this pass's never-update rule, so name it as a recommendation and run it only on the captain's word.
Report what new capabilities landed upstream, how far behind this fork is, and any external-tooling or installed-skill drift with its coordination tag.
Report notable upstream advances by commit identity, what content would arrive, the ancestry context, and any external-tooling or installed-skill drift with its coordination tag.

### 2. FIT

Expand Down Expand Up @@ -82,7 +84,7 @@ In FIT, compare newly available capabilities with the existing fleet flow and re
In MEMORY, invoke `/stow` and relay its completion receipt.
In INTEGRITY, perform both-direction memory-index checks, dangling-`data/`-pointer checks, and agent-skills repository checks.

Report the upstream gap, notable upstream capabilities, external-tooling and installed-skill drift with coordination tags, the fit verdict, stow's receipt, agent-policy coverage and remote-baseline drift, and every other integrity finding in plain outcomes language.
Report the upstream content verdict, incoming content, commit-identity context, external-tooling and installed-skill drift with coordination tags, the fit verdict, stow's receipt, agent-policy coverage and remote-baseline drift, and every other integrity finding in plain outcomes language.
Stop after the report.
The captain decides whether to proceed to full-sync mode.

Expand Down
228 changes: 177 additions & 51 deletions bin/fm-upstream-check.sh
Original file line number Diff line number Diff line change
@@ -1,11 +1,28 @@
#!/usr/bin/env bash
# Check the gap between this fork and the canonical upstream firstmate repo.
#
# Fetches upstream/main, then prints:
# - ahead/behind commit counts between main and upstream/main
# - a grouped summary of notable new upstream commits since the merge-base
# Fetches upstream/main, then reports:
# - the ahead/behind commit-identity counts as ancestry context
# - the non-conflicted content that a three-way merge of main and upstream/main would add
# - conflicted paths in a separate category, excluded from the content measurement
# - a grouped summary of upstream commit identities since the merge-base
#
# The content verdict is the sync decision. Commit-identity counts are not a
# missing-content measure because a squash refit can inflate the behind count.
# Conflict-marker lines from merge-tree's result are excluded by excluding the
# conflicted paths themselves, so they cannot inflate incoming-content counts.
#
# READ-ONLY: never writes tracked files, the working tree, the index, HEAD, or
# local branch refs, and never pushes. Fetch refreshes upstream's remote-tracking
# ref as required to inspect upstream/main. git merge-tree --write-tree writes
# only loose merge-result objects to the object database; it does not write the
# working tree or index.
#
# Exit status is 0 after a successful fetch and report, whether content is
# current or incoming. Missing or unreachable remotes/refs and a failed content
# simulation exit non-zero. Git older than 2.38 uses an ancestry-only fallback
# with a loud caveat and still exits 0 to preserve the existing report contract.
#
# READ-ONLY: never writes to tracked files, never pushes.
# Used by /refit check mode and the weekly heartbeat job.
#
# Upstream remote expected: kunchenguid/firstmate at remote name "upstream".
Expand Down Expand Up @@ -59,65 +76,174 @@ BEHIND="$(printf '%s' "$COUNTS" | awk '{print $2}')"

MERGE_BASE="$(git merge-base "$LOCAL_REF" "$UPSTREAM_REF")"

printf '\n=== upstream gap ===\n'
printf 'fork ahead of upstream/main: %s commits\n' "$AHEAD"
printf 'fork behind upstream/main: %s commits\n' "$BEHIND"
printf 'merge-base: %s\n' "$MERGE_BASE"
print_commit_identity_context() {
printf '\n=== commit-identity context (not the content verdict) ===\n'
printf 'fork ahead of upstream/main: %s commit identities\n' "$AHEAD"
printf 'fork behind upstream/main: %s commit identities\n' "$BEHIND"
printf 'merge-base: %s\n' "$MERGE_BASE"
printf 'These ancestry counts can be inflated by squash refits and do not identify missing content.\n'
}

if [ "$BEHIND" = "0" ]; then
printf '\nfork is current with upstream/main - no sync needed.\n'
exit 0
fi
print_group() {
local commits=$1 header=$2 pattern=$3 lines
lines="$(printf '%s\n' "$commits" | grep -iE "$pattern" || true)"
if [ -n "$lines" ]; then
printf '\n%s:\n' "$header"
printf '%s\n' "$lines" | sed 's/^/ /'
fi
}

# --- summarize notable new upstream commits --------------------------------
print_upstream_summary() {
local commits matched other

printf '\n=== notable upstream advances (new since merge-base) ===\n'
[ "$BEHIND" != "0" ] || return 0
commits="$(git log --oneline "${MERGE_BASE}..${UPSTREAM_REF}")"

# Collect commits newer than the merge-base on upstream/main.
# Group loosely by conventional-commit scope keyword; print unmatched last.
printf '\n=== notable upstream advances (commit identities, not missing content) ===\n'
if [ -z "$commits" ]; then
printf '(none)\n'
return 0
fi

COMMITS="$(git log --oneline "${MERGE_BASE}..${UPSTREAM_REF}")"
print_group "$commits" "backends / runtime" "backend|herdr|orca|cmux|zellij|tmux"
print_group "$commits" "watcher / supervision" "watch|wake|beacon|heartbeat|supervisi"
print_group "$commits" "daemon / afk" "daemon|afk|away"
print_group "$commits" "session-start" "session.start|session-start|bootstrap"
print_group "$commits" "spawn / teardown" "spawn|teardown|brief|lifecycle"
print_group "$commits" "sync / fleet" "fleet.sync|fleet-sync|sync|update"
print_group "$commits" "features / feat" "^[a-f0-9]+ feat"
print_group "$commits" "fixes" "^[a-f0-9]+ fix"
print_group "$commits" "docs" "^[a-f0-9]+ docs?"

matched="$(printf '%s\n' "$commits" | grep -iE \
"backend|herdr|orca|cmux|zellij|tmux|watch|wake|beacon|heartbeat|supervisi|daemon|afk|away|session.start|session-start|bootstrap|spawn|teardown|brief|lifecycle|fleet.sync|fleet-sync|sync|update|^[a-f0-9]+ feat|^[a-f0-9]+ fix|^[a-f0-9]+ docs?" \
|| true)"
other="$(comm -23 \
<(printf '%s\n' "$commits" | sort) \
<(printf '%s\n' "$matched" | sort) \
|| true)"
if [ -n "$other" ]; then
printf '\nother:\n'
printf '%s\n' "$other" | sed 's/^/ /'
fi
}

if [ -z "$COMMITS" ]; then
printf '(none)\n'
# --- content simulation ----------------------------------------------------

GIT_VERSION_LINE="$(git --version 2>/dev/null || true)"
git_supports_merge_tree_write_tree() {
local version major minor rest
version=${1#git version }
major=${version%%.*}
[ "$version" != "$major" ] || return 1
rest=${version#*.}
minor=${rest%%.*}
case "$major" in
'' | *[!0-9]*) return 1 ;;
esac
case "$minor" in
'' | *[!0-9]*) return 1 ;;
esac
[ "$major" -gt 2 ] || { [ "$major" -eq 2 ] && [ "$minor" -ge 38 ]; }
}

if ! git_supports_merge_tree_write_tree "$GIT_VERSION_LINE"; then
printf '\n=== upstream currency verdict ===\n'
if [ -n "$GIT_VERSION_LINE" ]; then
printf 'content verdict unavailable: %s does not support git merge-tree --write-tree (Git 2.38 or newer is required).\n' "$GIT_VERSION_LINE"
else
printf 'content verdict unavailable: the installed Git version could not be determined for git merge-tree --write-tree (Git 2.38 or newer is required).\n'
fi
printf 'This is an ancestry-only fallback. The count may be squash-inflated and must not be used as a sync trigger.\n'
print_commit_identity_context
print_upstream_summary
exit 0
fi

# Print grouped by area keyword; each group is filtered by grep -i.
# We print a header only when the group is non-empty.
TREE_OUTPUT=
TREE_RC=0
TREE_OUTPUT="$(git merge-tree --write-tree "$LOCAL_REF" "$UPSTREAM_REF" 2>&1)" || TREE_RC=$?
TREE="$(printf '%s\n' "$TREE_OUTPUT" | sed -n '1p')"
TREE_OBJECT=
if [ -n "$TREE" ] && [ "$(git cat-file -t "$TREE" 2>/dev/null || true)" = tree ]; then
TREE_OBJECT="$(git rev-parse --verify "$TREE^{tree}" 2>/dev/null || true)"
fi
if [ -z "$TREE_OBJECT" ]; then
printf 'error: upstream content simulation failed; merge-tree produced no valid tree\n' >&2
if [ -n "$TREE_OUTPUT" ]; then
printf '%s\n' "$TREE_OUTPUT" >&2
fi
exit 1
fi

print_group() {
local header="$1"; shift
local pattern="$1"; shift
local lines
lines="$(printf '%s\n' "$COMMITS" | grep -iE "$pattern" || true)"
if [ -n "$lines" ]; then
printf '\n%s:\n' "$header"
printf '%s\n' "$lines" | sed 's/^/ /'
# A conflicted merge tree contains stage 1/2/3 records in its diagnostic output.
# Exclude those paths from the content diff so conflict-marker lines are never
# counted as incoming content or mistaken for files absent from the fork.
CONFLICT_PATHS="$(printf '%s\n' "$TREE_OUTPUT" | awk '
{
tab = index($0, "\t")
prefix = tab ? substr($0, 1, tab - 1) : ""
if (prefix ~ /^[0-7][0-7][0-7][0-7][0-7][0-7] [0-9a-f]+ [123]$/) {
print substr($0, tab + 1)
}
}
' | sort -u)"
CONFLICT_EXCLUDES=()
if [ -n "$CONFLICT_PATHS" ]; then
while IFS= read -r CONFLICT_PATH; do
[ -n "$CONFLICT_PATH" ] || continue
CONFLICT_EXCLUDES+=(":(exclude,top,literal)$CONFLICT_PATH")
done <<< "$CONFLICT_PATHS"
fi
CONFLICT_COUNT=0
if [ -n "$CONFLICT_PATHS" ]; then
CONFLICT_COUNT="$(printf '%s\n' "$CONFLICT_PATHS" | awk 'NF { count++ } END { print count + 0 }')"
fi

INCOMING_STAT="$(git diff --stat "$LOCAL_REF" "$TREE_OBJECT" -- . "${CONFLICT_EXCLUDES[@]}")"
INCOMING_SHORTSTAT="$(git diff --shortstat "$LOCAL_REF" "$TREE_OBJECT" -- . "${CONFLICT_EXCLUDES[@]}" | sed 's/^ *//')"
INCOMING_FILES="$(git diff --diff-filter=A --name-only "$LOCAL_REF" "$TREE_OBJECT" -- . "${CONFLICT_EXCLUDES[@]}")"

printf '\n=== upstream currency verdict ===\n'
if [ -z "$INCOMING_STAT" ] && [ "$CONFLICT_COUNT" -eq 0 ]; then
printf 'fork is content-current with upstream/main - no incoming content and no sync needed.\n'
if [ "$BEHIND" != "0" ]; then
printf 'The %s-commit ancestry gap is squash residue from a prior refit, not missing work.\n' "$BEHIND"
fi
}
printf 'incoming content summary: 0 files changed, 0 insertions(+), 0 deletions(-)\n'
else
printf 'upstream content would arrive; review the incoming diff before syncing.\n'
if [ -n "$INCOMING_SHORTSTAT" ]; then
printf 'incoming content summary: %s\n' "$INCOMING_SHORTSTAT"
else
printf 'incoming content summary: 0 files changed, 0 insertions(+), 0 deletions(-)\n'
fi
if [ "$CONFLICT_COUNT" -gt 0 ]; then
printf '\nincoming content diffstat (conflicted paths excluded):\n'
else
printf '\nincoming content diffstat:\n'
fi
if [ -n "$INCOMING_STAT" ]; then
printf '%s\n' "$INCOMING_STAT"
else
printf ' (none)\n'
fi
printf '\nfiles genuinely absent from fork main:\n'
if [ -n "$INCOMING_FILES" ]; then
printf '%s\n' "$INCOMING_FILES" | sed 's/^/ /'
else
printf ' (none)\n'
fi
if [ "$CONFLICT_COUNT" -gt 0 ]; then
printf '\nconflicted paths (excluded from incoming-content measurement): %s\n' "$CONFLICT_COUNT"
printf '%s\n' "$CONFLICT_PATHS" | sed 's/^/ /'
fi
fi

print_group "backends / runtime" "backend|herdr|orca|cmux|zellij|tmux"
print_group "watcher / supervision" "watch|wake|beacon|heartbeat|supervisi"
print_group "daemon / afk" "daemon|afk|away"
print_group "session-start" "session.start|session-start|bootstrap"
print_group "spawn / teardown" "spawn|teardown|brief|lifecycle"
print_group "sync / fleet" "fleet.sync|fleet-sync|sync|update"
print_group "features / feat" "^[a-f0-9]+ feat"
print_group "fixes" "^[a-f0-9]+ fix"
print_group "docs" "^[a-f0-9]+ docs?"

# Remaining commits that matched none of the above
MATCHED="$(printf '%s\n' "$COMMITS" | grep -iE \
"backend|herdr|orca|cmux|zellij|tmux|watch|wake|beacon|heartbeat|supervisi|daemon|afk|away|session.start|session-start|bootstrap|spawn|teardown|brief|lifecycle|fleet.sync|fleet-sync|sync|update|^[a-f0-9]+ feat|^[a-f0-9]+ fix|^[a-f0-9]+ docs?" \
|| true)"
OTHER="$(comm -23 \
<(printf '%s\n' "$COMMITS" | sort) \
<(printf '%s\n' "$MATCHED" | sort) \
|| true)"
if [ -n "$OTHER" ]; then
printf '\nother:\n'
printf '%s\n' "$OTHER" | sed 's/^/ /'
if [ "$TREE_RC" -ne 0 ]; then
printf '\nmerge simulation reported conflicts but produced a valid tree; review conflicts before syncing.\n'
fi

print_commit_identity_context
print_upstream_summary
printf '\n'
1 change: 1 addition & 0 deletions docs/scripts.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ The shared no-mistakes gate refusal for fleet lifecycle entrypoints is summarize
| `fm-bearings-board.sh` | Build and arm the stable interactive `/bearings lavish` fleet board |
| `fm-secondmate-reconcile.sh` | Ask each secondmate to reconcile an inventory mismatch through its durable inbox, limited by a per-home cooldown |
| `fm-update.sh` | Fast-forward-only self-update of firstmate and local or remote secondmate homes |
| `fm-upstream-check.sh` | Fetch upstream/main and report non-conflicted incoming content by three-way merge simulation, with conflicts and ancestry counts as labeled context |
| `fm-on.sh` | Execute one tracked Firstmate command in a configured remote secondmate home, using its job worker except for the doctor bootstrap |
| `fm-remote-job-lib.sh` | Shared bounded remote job queue, worker readiness, LaunchAgent contract, and filesystem-composed PATH |
| `fm-remote-job-worker.sh` | Long-lived remote queue worker for tracked `fm-*.sh` commands in the account runtime |
Expand Down
Loading
Loading