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
17 changes: 17 additions & 0 deletions .agents/skills/project-management/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,23 @@ A `no-mistakes` project must have an `origin` remote and must complete the initi
A `direct-PR` project needs an `origin` remote but skips no-mistakes initialization.
A `local-only` project may have no remote and skips no-mistakes initialization.

### Fork-contribution projects: origin must be the captain's fork

A fork-contribution project is one where the captain works against a project the captain does not own upstream, and every PR must land in the captain's own `trillium/<repo>` fork, never the upstream project, until the captain explicitly says otherwise.
Clone it so `origin` is the captain's `trillium/<repo>` fork and the original project is a separate `upstream` remote, never the reverse:

```sh
git clone [email protected]:trillium/<repo>.git projects/<repo>
git -C projects/<repo> remote add upstream <original-project-url>
```

If the fork does not exist yet, create it with `gh-axi` before cloning.
This origin/upstream shape is load-bearing, not cosmetic: `git push origin` and, for `no-mistakes` projects, no-mistakes's own PR-open step both act on whatever `origin` is configured to, so an `origin` that still points at the upstream project makes a `no-mistakes` PR land there automatically, with no worker-side override available.
`bin/fm-brief.sh`'s header and `AGENTS.md` section 11 own the resulting brief-scaffold rule and its exact clone-shape detection; do not restate that mechanism here.
Never initialize a fork-contribution project with `no-mistakes init --fork-url` - that flag implements the opposite workflow (push to a named fork, open the PR against `origin`/upstream), documented for external contributors to firstmate itself in `CONTRIBUTING.md`, and is exactly backwards for a fork-contribution project.
A clone still carrying the legacy shape (`origin` = upstream, no `upstream` remote) needs this swap before its next `no-mistakes` run; treat discovering one as a project-management fix, not a task for the ship worker mid-brief to attempt.
Run `bin/fm-fork-origin-check.sh` by hand to advisory-scan every registered clone for a legacy or partially swapped origin/upstream shape; it is read-only and never blocks a spawn or edits a remote.

## Create a project

Creating a GitHub repository is outward-facing.
Expand Down
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -478,7 +478,7 @@ If a ship task touches firstmate's shared tracked material, explicitly require `
If a task will drive Herdr lifecycle behavior, scaffold with `--herdr-lab`; if that need appears after an unguarded scaffold, stop and regenerate rather than adding commands by hand.
The generated Herdr contract must use a named non-`default` isolated lab and its guarded helper for every lifecycle action.
When a task is linked to an external bead (via `--beads <id>` at spawn), set `FM_HOOK_BEADS_ID=<id>` before scaffolding so the brief receives Bead Receipt and Bead Closure sections that guide the worker's interaction with the tracking system.
For push-mode ship briefs (direct-PR and no-mistakes) on projects whose git origin is not under the trillium/ namespace (upstream forks the worker cannot push to), the generated brief receives a fork-first push rule that directs the worker to push to the `trillium/<repo>` fork and open the PR from there; local-only briefs are exempt, as are briefs for Trillium-owned origins, unreadable origins, or absent clones.
For push-mode ship briefs (direct-PR and no-mistakes) on a fork-contribution project, the generated brief receives an explicit rule that the PR must land in the captain's own `trillium/<repo>` fork and never upstream, with mode- and clone-shape-specific instructions, including a stop-and-escalate instruction when a no-mistakes project's clone is not yet set up per `project-management`'s required origin/upstream convention; local-only briefs are exempt, as are briefs for an ordinary captain-owned project, an unreadable origin, or an absent clone.

Load `secondmate-provisioning` before creating or using a charter brief and preserve its idle-by-default and marked-return-channel contracts.
Status appends are sparse supervisor-actionable events, not routine progress; `bin/fm-classify-lib.sh` owns keyed open and resolved semantics.
Expand Down
145 changes: 108 additions & 37 deletions bin/fm-brief.sh
Original file line number Diff line number Diff line change
Expand Up @@ -45,11 +45,29 @@
# direct-PR implement -> push + open PR via gh-axi (no pipeline) -> captain merge
# local-only implement on branch, stop and report "ready in branch" (no push/PR);
# captain approves, firstmate merges to local main
# Push-mode ship briefs (direct-PR, no-mistakes) whose project clone has a
# non-Trillium git origin add a fork-first push rule: push the branch to the
# trillium/<repo> fork and open the PR from there, since the upstream origin
# refuses the push. Detection reads the clone's real origin remote; a Trillium,
# unreadable, or absent origin (and every local-only brief) adds no such rule.
# Push-mode ship briefs (direct-PR, no-mistakes) on a fork-contribution
# project - one whose PRs must land in the captain's own trillium/<repo> fork
# and never reach the project's upstream - add an explicit anti-upstream
# PR-target rule. Detection reads the clone's real git remotes, never
# data/projects.md prose, and fires in two shapes: a legacy clone whose
# `origin` is still the upstream repo (the not-yet-swapped state the
# project-management skill's convention corrects), or the correct swapped
# setup where `origin` is already the trillium fork and a separate
# `upstream` remote proves the relationship. A Trillium origin with no
# `upstream` remote (an ordinary captain-owned project), an unreadable or
# absent origin, and every local-only brief add no such rule.
# Pushing to the fork does not by itself keep a PR off upstream: `gh pr
# create` and no-mistakes both default an opened PR's base to the upstream
# parent unless told otherwise, and no-mistakes always opens against whatever
# `origin` is configured to. On a legacy (unswapped) clone, no-mistakes mode
# cannot be driven safely at all - the brief tells the worker to stop and
# escalate rather than run it - because there is no flag that redirects the
# pipeline's PR target: `no-mistakes init --fork-url` pushes to the named
# fork while still opening the PR against `origin` (upstream), which is the
# CONTRIBUTING.md-documented "contribute upstream" flow, exactly backwards
# for a fork-contribution project. direct-PR mode instead gives the worker
# the explicit `gh pr create --repo trillium/<repo>` form, which works on
# either clone shape.
# Ship briefs begin with a worktree-isolation assertion before the branch step.
# Scout tasks ignore mode - their deliverable is a report, not a merge.
# Every scaffold's status protocol distinguishes the configured
Expand Down Expand Up @@ -143,19 +161,32 @@ shell_quote() {
printf "'"
}

# Print the bare repository name to fork under trillium/ when a ship task's
# project clone has a non-Trillium git origin (an upstream repo the worker
# cannot push to); print nothing (and succeed) when the origin is Trillium-owned,
# unreadable, or the clone is absent, so the generated brief stays unchanged in
# every case that is not a known upstream fork. Detection reads the clone's real
# `git remote get-url origin`, never data/projects.md prose.
fork_repo_for_origin() {
local repo=$1 dir origin name rest owner
# Resolve a project's clone directory exactly as the origin/upstream lookups
# below need it: an absolute path is used as-is, "projects/<name>" is
# relative to FM_HOME, and a bare name resolves under $FM_HOME/projects (or
# FM_PROJECTS_OVERRIDE).
project_clone_dir() {
local repo=$1
case "$repo" in
/*) dir=$repo ;;
projects/*) dir="${FM_PROJECTS_OVERRIDE:-$FM_HOME/projects}/${repo#projects/}" ;;
*) dir="${FM_PROJECTS_OVERRIDE:-$FM_HOME/projects}/$repo" ;;
/*) printf '%s\n' "$repo" ;;
projects/*) printf '%s\n' "${FM_PROJECTS_OVERRIDE:-$FM_HOME/projects}/${repo#projects/}" ;;
*) printf '%s\n' "${FM_PROJECTS_OVERRIDE:-$FM_HOME/projects}/$repo" ;;
esac
}

# Print "<trillium-repo-name> <state>" for a ship task's fork-contribution
# project, where <state> is "legacy" when `origin` is still the upstream repo
# the worker cannot push to, or "swapped" when `origin` is already the
# trillium fork and a separate `upstream` remote proves the fork-contribution
# relationship (the project-management skill's required setup). Prints
# nothing (and succeeds) for an ordinary captain-owned project - a Trillium
# origin with no `upstream` remote - or an unreadable or absent clone, so the
# generated brief stays unchanged in every case that is not a known fork
# relationship. Detection reads the clone's real git remotes, never
# data/projects.md prose.
fork_repo_for_origin() {
local repo=$1 dir origin name rest owner upstream
dir=$(project_clone_dir "$repo")
origin=$(git -C "$dir" remote get-url origin 2>/dev/null) || return 0
[ -n "$origin" ] || return 0
origin=${origin%.git}
Expand All @@ -164,11 +195,17 @@ fork_repo_for_origin() {
rest=${origin%/*}
owner=${rest##*/} # https://host/owner/repo -> owner
owner=${owner##*:} # git@host:owner/repo -> owner
case "$(printf '%s' "$owner" | tr '[:upper:]' '[:lower:]')" in
trillium) return 0 ;;
esac
[ -n "$name" ] || return 0
printf '%s\n' "$name"
if [ "$(printf '%s' "$owner" | tr '[:upper:]' '[:lower:]')" = trillium ]; then
# origin already is the trillium fork; still a fork-contribution project
# (and the anti-upstream PR-target rule still applies) only when a
# separate `upstream` remote proves the relationship.
upstream=$(git -C "$dir" remote get-url upstream 2>/dev/null) || return 0
[ -n "$upstream" ] || return 0
printf '%s swapped\n' "$name"
return 0
fi
printf '%s legacy\n' "$name"
}

STATUS_FILE=$(shell_quote "$STATE/$ID.status")
Expand Down Expand Up @@ -423,28 +460,62 @@ esac
# briefs stay byte-identical to the historical Bash 5 output.
DOD=${DOD%$'\n'}

# Fork-first push rule: a project whose git origin is the upstream repository
# (not under trillium/) cannot be pushed to directly, so a worker on the push
# modes must push its branch to the trillium/<repo> fork and open the PR from
# there. Only direct-PR and no-mistakes push; local-only never does, so it is
# exempt. The rule text lives here exactly once and is empty (no rule) for
# local-only and for every Trillium-origin, unreadable, or absent-clone case,
# Fork-contribution PR-target rule: a project whose PRs must land in the
# captain's own trillium/<repo> fork, never the upstream project it was
# forked from. Only direct-PR and no-mistakes push or open a PR; local-only
# never does, so it is exempt. Rule text lives here exactly once and is empty
# (no rule) for local-only and for every ordinary captain-owned project,
# keeping those briefs byte-identical to the pre-rule output.
#
# Pushing to the fork does not by itself keep the PR off upstream: `gh pr
# create` and no-mistakes both default an opened PR's base to the upstream
# parent unless told otherwise. direct-PR mode gets an explicit `--repo`
# override that works on either clone shape. no-mistakes mode has no such
# override - it always opens against whatever `origin` is configured to - so
# a legacy (unswapped) clone cannot be driven safely at all; the worker is
# told to stop and escalate instead.
FORK_FIRST=""
if [ "$MODE" != local-only ]; then
FORK_REPO=$(fork_repo_for_origin "$REPO")
FORK_REPO=""
FORK_STATE=""
read -r FORK_REPO FORK_STATE <<EOF
$(fork_repo_for_origin "$REPO")
EOF
if [ -n "$FORK_REPO" ]; then
IFS= read -r -d '' FORK_FIRST <<EOF || true

# Fork-based project: all pushes target the fork
This project's \`origin\` is the upstream repository that refuses pushes, so a refused push to \`origin\` is expected, not a blocker.
The \`fm/$ID\` branch and its PR must target the \`trillium/$FORK_REPO\` fork, not upstream.
If the fork does not exist yet, create it with \`gh-axi\`.
Anything that pushes this branch or opens its PR must target the fork, not upstream.
In no-mistakes mode, ensure the pipeline is configured to push to the fork, not upstream.
Never push to the upstream \`origin\`, and never stop to ask fork-vs-local: always use the fork.
**CRITICAL:** a push to the upstream origin must NEVER happen automatically. If pushing to the fork is not possible, stop and get direct captain confirmation before any upstream push attempt.
FORK_HEADLINE="# Fork-based project: PRs stay in the fork, never upstream"
FORK_TARGET_RULE="This project's PRs must land in the captain's own \`trillium/$FORK_REPO\` fork, NEVER upstream; the PR only goes upstream on the captain's explicit word."
if [ "$MODE" = direct-PR ]; then
IFS= read -r -d '' FORK_FIRST <<EOF || true

$FORK_HEADLINE
$FORK_TARGET_RULE
If the fork does not exist yet, create it with \`gh-axi\` before pushing.
Push your branch to the fork - \`git push [email protected]:trillium/$FORK_REPO.git fm/$ID:fm/$ID\` if \`origin\` here is still upstream, or plain \`git push origin fm/$ID\` once \`origin\` is the fork.
Open the PR with an explicit repo override so it can never default to upstream: \`gh pr create --repo trillium/$FORK_REPO --base <fork-default-branch> --head fm/$ID\`.
Never omit that \`--repo trillium/$FORK_REPO\` override, and never stop to ask fork-vs-local: always target the fork.
**CRITICAL:** a PR opened against the upstream repo must NEVER happen automatically. If targeting the fork is not possible, stop and get direct captain confirmation before any upstream PR attempt.
EOF
elif [ "$FORK_STATE" = swapped ]; then
IFS= read -r -d '' FORK_FIRST <<EOF || true

$FORK_HEADLINE
$FORK_TARGET_RULE
\`origin\` here is already the \`trillium/$FORK_REPO\` fork, with \`upstream\` as a separate remote, so no-mistakes's normal PR-open behavior already targets the fork - do not change that.
Never run \`no-mistakes init --fork-url\` or any similar remote reconfiguration on this project: that flag pushes to the named fork while still opening the PR against \`origin\`, which is the exact wrong direction here.
Never stop to ask fork-vs-local: always target the fork.
**CRITICAL:** if no-mistakes ever proposes or opens a PR against anything other than \`trillium/$FORK_REPO\`, stop and escalate immediately rather than letting it proceed.
EOF
else
IFS= read -r -d '' FORK_FIRST <<EOF || true

# Fork-based project: origin is not yet the fork - STOP before running no-mistakes
$FORK_TARGET_RULE
\`origin\` here is still the upstream repository, not the \`trillium/$FORK_REPO\` fork. no-mistakes opens its PR against whatever \`origin\` is configured to, so running it now would open the PR against upstream.
This clone's remotes need the captain-approved origin swap (origin -> the \`trillium/$FORK_REPO\` fork, upstream -> the current origin) before no-mistakes can run safely here; that change is outside this task's worktree.
Do NOT run \`no-mistakes init --fork-url\` as a workaround: it pushes to the named fork while still opening the PR against \`origin\` (upstream), reproducing the same failure.
Append \`blocked: project clone's origin is not the trillium fork yet, no-mistakes would target upstream\` and stop; do not attempt any workaround that could open a PR against upstream.
EOF
fi
fi
fi

Expand Down
89 changes: 89 additions & 0 deletions bin/fm-fork-origin-check.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
#!/usr/bin/env bash
# Advisory scan for registered project clones whose remotes look like an
# unswapped or partially swapped fork-contribution setup: `origin` still
# points at the upstream repo the captain does not own, rather than the
# captain's own trillium/<repo> fork. See the project-management skill's
# "Fork-contribution projects" section for the required origin/upstream
# convention, and bin/fm-brief.sh for how a generated brief reacts to each
# clone shape at dispatch time.
#
# This is a read-only report, never a gate: it never blocks a spawn, edits a
# remote, or fails the build. Run it by hand when triaging suspected
# misconfigured clones (e.g. after an incident like the rango PR opened
# against upstream instead of the captain's fork).
#
# For each project in data/projects.md with a local clone under projects/:
# - origin owned by trillium, no upstream remote -> ordinary project, silent.
# - origin owned by trillium, upstream remote set -> correctly swapped
# fork-contribution clone, silent (this is the target state).
# - origin NOT owned by trillium, upstream remote set -> MISCONFIGURED:
# the swap was started (upstream added) but never finished (origin still
# points upstream). Always reported; no network call needed.
# - origin NOT owned by trillium, no upstream remote -> possibly an
# unswapped fork-contribution clone, or a genuinely non-Trillium project
# with no fork relationship at all. Best-effort: report it only when
# `trillium/<repo>` exists on GitHub and is itself a fork (via `gh`), so
# an ordinary non-Trillium project with no fork counterpart stays silent.
# A missing `gh` or a failed lookup is not an error - that project is
# skipped rather than misreported.
#
# Usage: fm-fork-origin-check.sh
# Exit status is always 0; findings go to stdout, one per line.
set -eu

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}}"
DATA="${FM_DATA_OVERRIDE:-$FM_HOME/data}"
PROJECTS="${FM_PROJECTS_OVERRIDE:-$FM_HOME/projects}"
REG="$DATA/projects.md"

case "${1:-}" in
-h|--help) awk 'NR==1{next} /^#/{sub(/^# ?/,""); print; next} {exit}' "$0"; exit 0 ;;
esac

if [ ! -f "$REG" ]; then
echo "no registry at $REG; nothing to check"
exit 0
fi

owner_of() {
local url=$1 rest owner
url=${url%.git}
url=${url%/}
rest=${url%/*}
owner=${rest##*/}
owner=${owner##*:}
printf '%s\n' "$owner" | tr '[:upper:]' '[:lower:]'
}

have_gh=0
command -v gh >/dev/null 2>&1 && have_gh=1

found=0
while read -r name; do
[ -n "$name" ] || continue
dir="$PROJECTS/$name"
[ -d "$dir" ] || continue
origin=$(git -C "$dir" remote get-url origin 2>/dev/null) || continue
[ -n "$origin" ] || continue
upstream=$(git -C "$dir" remote get-url upstream 2>/dev/null) || upstream=""
owner=$(owner_of "$origin")
[ "$owner" = trillium ] && continue
if [ -n "$upstream" ]; then
echo "MISCONFIGURED: $name - origin ($origin) is not trillium but an upstream remote is set; finish the swap (project-management skill)"
found=1
continue
fi
[ "$have_gh" -eq 1 ] || continue
is_fork=$(gh repo view "trillium/$name" --json isFork -q .isFork 2>/dev/null) || continue
if [ "$is_fork" = "true" ]; then
echo "CANDIDATE: $name - origin ($origin) is not trillium, and trillium/$name exists as a fork; likely needs the origin/upstream swap (project-management skill)"
found=1
fi
done <<EOF
$(awk '$1=="-"{print $2}' "$REG")
EOF
Comment on lines +84 to +86

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail
file=$(fd -t f '^fm-project-mode\.sh$' bin)
echo "== bin/fm-project-mode.sh header (parser contract) =="
sed -n '1,60p' "$file"
echo "== fm-fork-origin-check.sh registry read =="
rg -n 'awk|REG' bin/fm-fork-origin-check.sh

Repository: trillium/firstmate

Length of output: 2827


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== fm-fork-origin-check.sh relevant sections =="
sed -n '1,130p' bin/fm-fork-origin-check.sh

echo "== data/projects.md sample/line count =="
for f in data/projects.md **/projects.md; do
  [ -n "${f:-}" ] && [ -f "$f" ] || continue
  echo "--- $f ($(wc -l < "$f") lines) ---"
  sed -n '1,120p' "$f"
done

echo "== project-management skill registry guidance =="
skill=$(fd -t f '^SKILL\.md$' .agents/skills/project-management)
for s in "${skill[@]:-}"; do
  [ -n "$s" ] || continue
  echo "--- $s ---"
  sed -n '1,220p' "$s"
done

echo "== all fm-fork-origin-check.sh awk invocations =="
awk 'BEGIN{p=0} /awk \'{print NR": "$0} /awk '\''\$1=="-"\{print \$2'\'' "$REG"\n/ /EOF/' bin/fm-fork-origin-check.sh

Repository: trillium/firstmate

Length of output: 11766


Use bin/fm-project-mode.sh’s registry parser contract here.

bin/fm-fork-origin-check.sh still extracts project names with awk '$1=="-"{print $2}', but the project-management skill requires data/projects.md readers to use the registry format and parser contract owned by bin/fm-project-mode.sh. This simplified extraction does not follow that contract and may miss or misread registry line shapes intended by bin/fm-project-mode.sh.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@bin/fm-fork-origin-check.sh` around lines 84 - 86, Update the project-name
extraction in the here-document within the fork-origin check flow to reuse the
registry parser contract provided by bin/fm-project-mode.sh instead of the
inline awk expression. Preserve the existing iteration behavior while ensuring
data/projects.md entries are interpreted through that parser’s supported
registry format.


[ "$found" -eq 1 ] || echo "no misconfigured fork-contribution clones found"
exit 0
1 change: 1 addition & 0 deletions docs/scripts.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ The shared no-mistakes gate refusal for fleet lifecycle entrypoints is summarize
| `fm-remote-launch.sh` | EXPERIMENTAL, explicitly transitional: launch, reclaim, reconcile, and inspect a crewmate on a named mini over the herdr-web bridge, self-contained and outside `fm-backend.sh`'s runtime-backend contract |
| `fm-config-push.sh` | Push declared inherited local material to live secondmates mid-session and send a pointer to the literal-content config reread when config changed |
| `fm-project-mode.sh` | Resolve a project's delivery mode and `+yolo` flag from `data/projects.md` |
| `fm-fork-origin-check.sh` | Advisory, read-only scan for registered project clones with an unswapped or partially swapped fork-contribution origin/upstream shape |
| `fm-merge-local.sh` | Fast-forward a `local-only` project's local default branch after approval |
| `fm-review-diff.sh` | Review a crewmate branch or resolved PR head against the authoritative base |
| `fm-marker-lib.sh` | Compatibility entry point for the from-firstmate carrier owned by `fm-operational-input.sh` |
Expand Down
Loading
Loading