-
Notifications
You must be signed in to change notification settings - Fork 0
feat(brief): inject fork-first push rule for non-Trillium origins #12
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
d700927
6dbb285
ca4b673
99e1cf3
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -39,6 +39,11 @@ | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # 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. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # 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 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -132,6 +137,34 @@ 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 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| case "$repo" in | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /*) dir=$repo ;; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| projects/*) dir="${FM_PROJECTS_OVERRIDE:-$FM_HOME/projects}/${repo#projects/}" ;; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| *) dir="${FM_PROJECTS_OVERRIDE:-$FM_HOME/projects}/$repo" ;; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| esac | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| origin=$(git -C "$dir" remote get-url origin 2>/dev/null) || return 0 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| [ -n "$origin" ] || return 0 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| origin=${origin%.git} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| origin=${origin%/} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| name=${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" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+153
to
+165
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win Reject malformed origin URLs before deriving the fork name.
Validate the host, owner, and repository components before printing Proposed fix- origin=${origin%.git}
origin=${origin%/}
+ origin=${origin%.git}
+ if [[ ! "$origin" =~ ^(https?|ssh)://[^/]+/[^/]+/[^/]+$ ]] &&
+ [[ ! "$origin" =~ ^[^`@/`:]+@[^/:]+:[^/]+/[^/]+$ ]]; then
+ return 0
+ fi
name=${origin##*/}
rest=${origin%/*}
owner=${rest##*/} # https://host/owner/repo -> owner
owner=${owner##*:} # git@host:owner/repo -> owner
+ [ -n "$owner" ] || return 0📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| STATUS_FILE=$(shell_quote "$STATE/$ID.status") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if [ "$KIND" = secondmate ]; then | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -362,6 +395,31 @@ 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, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # keeping those briefs byte-identical to the pre-rule output. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| FORK_FIRST="" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if [ "$MODE" != local-only ]; then | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| FORK_REPO=$(fork_repo_for_origin "$REPO") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 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. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| EOF | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| fi | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| fi | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| cat > "$BRIEF" <<EOF | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| You are a crewmate: an autonomous worker agent managed by firstmate. Work on your own; do not wait for a human. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -403,7 +461,7 @@ $RULE1 | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 7. Never stop, restart, or update the shared \`no-mistakes\` daemon - it is one instance serving | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| every lane/home, so restarting it kills other lanes' in-flight pipeline runs. On ANY no-mistakes | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| daemon error, append \`blocked: {the daemon error}\` and stop; only firstmate manages the daemon. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| $FORK_FIRST | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # Project memory | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| If \`AGENTS.md\` or \`CLAUDE.md\` already exists, or if this task produced durable project-intrinsic knowledge, run \`$FM_ROOT/bin/fm-ensure-agents-md.sh .\` in the worktree. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Record only project knowledge useful to almost every future session. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -598,6 +598,68 @@ test_scout_and_secondmate_load_decision_hold_policy() { | |
| pass "fm-brief.sh: investigation and visual-review completions load the shared decision policy" | ||
| } | ||
|
|
||
| # Fork-first push rule: a ship task on a project whose git origin is NOT under | ||
| # trillium/ must be told to push its branch to the trillium/<repo> fork; a | ||
| # Trillium-origin project gets no such rule (byte-identical to pre-rule output), | ||
| # and local-only never pushes so it stays exempt even on an upstream origin. | ||
| make_clone() { | ||
| local dir=$1 origin=$2 | ||
| mkdir -p "$dir" | ||
| git -C "$dir" init -q | ||
| git -C "$dir" remote add origin "$origin" | ||
| } | ||
|
|
||
| test_fork_first_push_rule() { | ||
| local home brief | ||
| home="$TMP_ROOT/fork-first-home" | ||
| mkdir -p "$home/data" "$home/projects" | ||
| # local-only fixture project (for the exemption case) needs the registry mode. | ||
| cat > "$home/data/projects.md" <<'EOF' | ||
| - upstream-local [local-only] - upstream fork on a local-only project (added 2026-07-01) | ||
| EOF | ||
| make_clone "$home/projects/upstream-proj" "https://github.com/kunchenguid/gnhf.git" | ||
| make_clone "$home/projects/upstream-ssh" "[email protected]:david-tejada/rango.git" | ||
| make_clone "$home/projects/trillium-proj" "[email protected]:trillium/firstmate.git" | ||
| make_clone "$home/projects/upstream-local" "https://github.com/gastownhall/gascity.git" | ||
|
|
||
| # no-mistakes on a non-Trillium origin: rule present, correct fork named. | ||
| FM_HOME="$home" "$ROOT/bin/fm-brief.sh" fork-nm upstream-proj >/dev/null 2>&1 | ||
| brief="$home/data/fork-nm/brief.md" | ||
| assert_grep "# Fork-based project: all pushes target the fork" "$brief" \ | ||
| "no-mistakes brief on an upstream origin lost the fork-first rule" | ||
| # shellcheck disable=SC2016 # Literal backticks must stay unexpanded. | ||
| assert_grep 'the `trillium/gnhf` fork' "$brief" \ | ||
| "no-mistakes fork-first rule named the wrong fork" | ||
| assert_grep "never stop to ask fork-vs-local" "$brief" \ | ||
| "fork-first rule dropped the never-ask-fork-vs-local instruction" | ||
| # shellcheck disable=SC2016 # Literal backticks must stay unexpanded. | ||
| assert_grep 'Never push to the upstream `origin`' "$brief" \ | ||
| "fork-first rule dropped the never-push-upstream instruction" | ||
|
|
||
| # direct-PR pushes too; SSH origin still resolves the fork name. | ||
| cat >> "$home/data/projects.md" <<'EOF' | ||
| - upstream-ssh [direct-PR] - upstream fork reached over SSH (added 2026-07-01) | ||
| EOF | ||
| FM_HOME="$home" "$ROOT/bin/fm-brief.sh" fork-dp upstream-ssh >/dev/null 2>&1 | ||
| brief="$home/data/fork-dp/brief.md" | ||
| # shellcheck disable=SC2016 # Literal backticks must stay unexpanded. | ||
| assert_grep 'the `trillium/rango` fork' "$brief" \ | ||
| "direct-PR fork-first rule did not resolve the SSH-origin fork name" | ||
|
|
||
| # Trillium-owned origin: no fork-first rule at all. | ||
| FM_HOME="$home" "$ROOT/bin/fm-brief.sh" fork-tr trillium-proj >/dev/null 2>&1 | ||
| brief="$home/data/fork-tr/brief.md" | ||
| assert_no_grep "# Fork-based project: all pushes target the fork" "$brief" \ | ||
| "Trillium-origin brief wrongly carried the fork-first rule" | ||
|
|
||
| # local-only never pushes: exempt even though the origin is upstream. | ||
| FM_HOME="$home" "$ROOT/bin/fm-brief.sh" fork-lo upstream-local >/dev/null 2>&1 | ||
| brief="$home/data/fork-lo/brief.md" | ||
| assert_no_grep "# Fork-based project: all pushes target the fork" "$brief" \ | ||
| "local-only brief wrongly carried the fork-first push rule" | ||
| pass "fm-brief.sh: fork-first push rule appears only for push modes on non-Trillium origins" | ||
| } | ||
|
|
||
| # Scout and secondmate paths still scaffold well-formed briefs. | ||
| test_scout_and_secondmate_scaffold() { | ||
| local brief | ||
|
|
@@ -634,4 +696,5 @@ test_secondmate_marked_request_reporting_contract | |
| test_secondmate_directory_paths_are_absolute_and_output_is_stable | ||
| test_pause_verb_override_renders_all_brief_scaffolds | ||
| test_scout_and_secondmate_load_decision_hold_policy | ||
| test_fork_first_push_rule | ||
| test_scout_and_secondmate_scaffold | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win
Do not edit
AGENTS.mddirectly.Apply this addition through the selected delivery path with
bin/fm-ensure-agents-md.sh. Do not commit this direct file edit.As per coding guidelines, Firstmate must not write
AGENTS.mddirectly; contributors must update it lazily throughbin/fm-ensure-agents-md.sh.🤖 Prompt for AI Agents
Source: Coding guidelines