Skip to content

feat(brief): inject fork-first push rule for non-Trillium origins - #12

Merged
trillium merged 4 commits into
mainfrom
fm/fork-first-brief-08
Aug 1, 2026
Merged

feat(brief): inject fork-first push rule for non-Trillium origins#12
trillium merged 4 commits into
mainfrom
fm/fork-first-brief-08

Conversation

@trillium

@trillium trillium commented Aug 1, 2026

Copy link
Copy Markdown
Owner

Intent

Inject a fork-first push rule into the ship briefs that bin/fm-brief.sh generates. Several registered firstmate projects are forks: the clone's git origin points at an upstream repo (e.g. kunchenguid/gnhf, david-tejada/rango) that the worker cannot push to, while Trillium maintains a fork at trillium/. Today a worker on such a project hits a refused upstream push and stalls. The change detects, per ship task, whether the project clone's real 'git remote get-url origin' is under the trillium/ namespace; when it is NOT, the generated push-mode brief (direct-PR and no-mistakes) gains a concise fork-first instruction: push the fm/ branch to the trillium/ fork, create the fork with gh-axi if absent, open the PR from the fork, expect the refused origin push, never ask fork-vs-local, never push upstream. Deliberate decisions: (1) detection uses the real git origin remote of the resolved clone, NOT data/projects.md prose, because the coding guidelines call registry free-text an unreliable machine signal; the fork repo name is derived from the origin URL basename and owner-segment parsing handles both https and git@ SSH forms. (2) The rule text lives exactly once as a single shared FORK_FIRST string (one-owner rule) injected once into the shared ship heredoc, gated by mode and origin. (3) local-only is exempt because it never pushes or opens a PR. (4) Trillium-origin, unreadable-origin, and absent-clone cases add NO rule and stay byte-identical to pre-change output (verified: diffing the original HEAD script vs modified for a no-clone project showed only the task-id substitution differing). Built with the IFS= read -r -d '' heredoc pattern to avoid the forbidden heredoc-in-command-substitution class that breaks Bash 3.2 parsing. Tests extend tests/fm-brief.test.sh (not a new runner) covering rule-present for https and ssh origins, Trillium rule-absent, and local-only exemption. bin/fm-lint.sh passes. Out of scope: fm-spawn.sh, fm-teardown.sh, push/merge scripts, and data/projects.md were not touched.

What Changed

  • Fork-first push rule injection: Ship briefs for projects with non-Trillium git origins now include a fork-first rule instructing workers to push to trillium/<repo> and open the PR from the fork, preventing refused upstream pushes.
  • Mode-agnostic rule with safety guard: The fork-first rule applies to both direct-PR and no-mistakes modes with guidance to handle pipeline-worker coordination; local-only tasks and Trillium-owned origins are exempt and remain byte-identical to pre-change output.
  • Test coverage and documentation: Extended test suite verifies fork-first rule behavior for HTTPS and SSH origins, Trillium exemption, and local-only exemption; documented the rule in AGENTS.md section 11.

Risk Assessment

✅ Low: The change is well-bounded and correct: fork detection uses git origin reading (not unreliable prose), the rule injects only for non-Trillium push modes, scout and local-only are properly excluded, and the second commit's reword correctly makes guidance mode-agnostic while adding safety guardrails.

Testing

Comprehensive validation of fork-first push rule injection: all 18 automated tests pass including fork-first-specific test, 5 manual test scenarios confirm correct rule injection for push modes on upstream origins and correct rule absence for Trillium/local-only/nonexistent cases, URL parsing validated for both HTTPS and SSH formats, Bash 3.2 parse safety confirmed with safe heredoc pattern, ShellCheck linting passes cleanly.

Evidence: Fork-First Rule Test Evidence
# Fork-First Push Rule Evidence

## Test Case 1: HTTPS Upstream Origin (no-mistakes mode)
- **Project**: kunchenguid/gnhf (HTTPS URL)
- **Mode**: no-mistakes (push mode)
- **Expected**: Fork-first rule present, fork name correctly extracted as `trillium/gnhf`
- **Result**: ✓ PASS

Generated brief section:
`` `
# 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/fork-nm` branch and its PR must target the `trillium/gnhf` 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.
`` `

## Test Case 2: SSH Upstream Origin (direct-PR mode)
- **Project**: david-tejada/rango (SSH URL)
- **Mode**: direct-PR (push mode)
- **Expected**: Fork-first rule present, fork name correctly extracted as `trillium/rango`
- **Result**: ✓ PASS

Generated brief section:
`` `
# 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/fork-dp` branch and its PR must target the `trillium/rango` 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.
`` `

## Test Case 3: Trillium-Owned Origin (no-mistakes mode)
- **Project**: trillium/firstmate (Trillium-owned)
- **Mode**: no-mistakes (push mode)
- **Expected**: Fork-first rule NOT present (no rule for Trillium-owned projects)
- **Result**: ✓ PASS (rule absent)

## Test Case 4: Local-Only Mode Exemption
- **Project**: gastownhall/gascity (upstream)
- **Mode**: local-only (non-push mode)
- **Expected**: Fork-first rule NOT present (local-only never pushes)
- **Result**: ✓ PASS (rule absent)

## Test Case 5: Nonexistent Project Clone
- **Project**: nonexistent-proj (clone does not exist)
- **Mode**: no-mistakes
- **Expected**: Fork-first rule NOT present (no origin to detect)
- **Result**: ✓ PASS (rule absent, brief still well-formed)

## URL Parsing Verification
- HTTPS URLs: ✓ Correctly extracts owner/repo
- SSH URLs: ✓ Correctly extracts owner/repo
- Trillium detection: ✓ Case-insensitive, correctly filters
- .git suffix handling: ✓ Strips correctly from both formats

## Code Quality Verification
- Bash 3.2 parse safety: ✓ PASS (safe heredoc-in-read pattern used)
- ShellCheck linting: ✓ PASS (no warnings)
- One-owner principle: ✓ PASS (FORK_FIRST variable used exactly once)
- Test coverage: ✓ PASS (all test cases pass)

Pipeline

Updates from git push no-mistakes

✅ **intent** - passed

✅ No issues found.

✅ **Rebase** - passed

✅ No issues found.

🔧 **Review** - 1 issue found → auto-fixed ✅
  • ⚠️ bin/fm-brief.sh:413 - The fork-first rule is injected into both push modes, but no-mistakes mode ships the PR via the pipeline, not the worker. The no-mistakes DOD (fm-brief.sh:373-388) has the worker run /no-mistakes and let the pipeline push and open the PR, and forbids the worker from acting directly during a run. The injected rule (fm-brief.sh:413) instead tells the worker to manually push its fm/&lt;id&gt; branch to the trillium fork and open the PR from there. If the no-mistakes pipeline pushes to origin (the unpushable upstream), the brief text does not resolve the stall for the default mode; and a worker following the rule literally may open a fork PR while the pipeline separately attempts an origin push, yielding a refused push or conflicting/duplicate PRs. Confirm no-mistakes honors a fork remote (or scope the rule to direct-PR / add pipeline-specific guidance).

🔧 Fix: Reword fork-first rule to be mode-agnostic with upstream-safety guard
✅ Re-checked - no issues remain.

✅ **Test** - passed

✅ No issues found.

  • bash tests/fm-brief.test.sh test_fork_first_push_rule (all 18 tests pass)
  • bash tests/fm-brief.test.sh | grep fork-first (fork-first-specific test passes)
  • bin/fm-lint.sh (ShellCheck 0.11.0 - no warnings)
  • Manual verification: 5 test cases - HTTPS upstream (rule present, correct fork), SSH upstream (rule present, correct fork), Trillium-owned (rule absent), local-only exemption (rule absent), nonexistent clone (rule absent)
  • URL parsing verification: HTTPS with/without .git, SSH with/without .git, Trillium detection (case-insensitive), all correct
  • Bash 3.2 parse safety: safe heredoc-in-read pattern verified
✅ **Document** - passed

✅ No issues found.

✅ **Lint** - passed

✅ No issues found.

✅ **Push** - passed

✅ No issues found.

Summary by CodeRabbit

  • New Features

    • Push-mode shipping briefs now provide fork-first instructions for projects hosted outside Trillium, including fork creation, push, pull request targeting, and upstream safety guidance.
    • Repository origins are detected automatically to tailor instructions appropriately.
  • Bug Fixes

    • Local-only projects and Trillium-hosted repositories no longer receive unnecessary fork guidance.
  • Tests

    • Added coverage for HTTPS, SSH, Trillium, and local repository scenarios.

trillium added 3 commits July 31, 2026 14:51
Push-mode ship briefs (direct-PR, no-mistakes) whose project clone has a
git origin outside the trillium/ namespace now tell the worker to push
its branch to the trillium/<repo> fork and open the PR from there,
instead of stalling on a refused upstream push. Detection reads the
clone's real origin remote, not registry prose. Trillium-origin,
unreadable, or absent origins and every local-only brief are unchanged.

Extends tests/fm-brief.test.sh with the rule-present (HTTPS and SSH
origins), Trillium rule-absent, and local-only exemption cases.
@coderabbitai

coderabbitai Bot commented Aug 1, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@trillium, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 35 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 4b10704e-9603-4117-b3c7-deccd0669e1b

📥 Commits

Reviewing files that changed from the base of the PR and between ca4b673 and 99e1cf3.

📒 Files selected for processing (1)
  • .github/workflows/ci.yml
📝 Walkthrough

Walkthrough

Push-mode ship briefs now detect non-Trillium Git origins and direct workers to push to trillium/<repo> before opening a PR. Tests cover HTTPS, SSH, Trillium-owned, and local-only origins.

Changes

Fork-first push guidance

Layer / File(s) Summary
Origin detection and brief generation
bin/fm-brief.sh, AGENTS.md
The script resolves Git origins, identifies eligible upstream repositories, and adds fork creation, push, PR-targeting, and upstream-push safety instructions to applicable briefs.
Fork-rule regression coverage
tests/fm-brief.test.sh
Tests cover HTTPS and SSH upstream origins, Trillium-owned origins, local-only projects, and test registration.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Sequence Diagram(s)

sequenceDiagram
  participant fm-brief.sh
  participant Git
  participant ProjectClone
  fm-brief.sh->>ProjectClone: Resolve clone path
  fm-brief.sh->>Git: Read origin URL
  Git-->>fm-brief.sh: Return origin
  fm-brief.sh->>fm-brief.sh: Build trillium/<repo> fork rule
  fm-brief.sh-->>ProjectClone: Add rule to ship brief
Loading

Possibly related PRs

  • trillium/firstmate#5: Documents related fork-first push guidance for non-writable or non-Trillium repositories.

Suggested reviewers: kunchenguid, karotkriss

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main change: adding fork-first push guidance for non-Trillium origins.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fm/fork-first-brief-08

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Prompt for all review comments with 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.

Inline comments:
In `@AGENTS.md`:
- Line 469: Do not edit AGENTS.md directly; apply this generated brief behavior
through the selected delivery path using bin/fm-ensure-agents-md.sh. Ensure the
script handles the fork-first rule for eligible push-mode briefs while
preserving exemptions for local-only briefs, Trillium-owned origins, unreadable
origins, and absent clones.

In `@bin/fm-brief.sh`:
- Around line 153-165: Update the origin parsing logic in the repository-name
function to validate the host, owner, and non-empty repository components before
deriving or printing name; reject malformed origins such as
https://github.com/trillium without adding a rule, while preserving the existing
trillium-owner exclusion. Add a regression test covering an origin with a
missing repository segment.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 5439ed92-a185-47e9-b47c-cfc6628561d5

📥 Commits

Reviewing files that changed from the base of the PR and between 426fda0 and ca4b673.

📒 Files selected for processing (3)
  • AGENTS.md
  • bin/fm-brief.sh
  • tests/fm-brief.test.sh

Comment thread AGENTS.md
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.

Copy link
Copy Markdown

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.md directly.

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.md directly; contributors must update it lazily through bin/fm-ensure-agents-md.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 `@AGENTS.md` at line 469, Do not edit AGENTS.md directly; apply this generated
brief behavior through the selected delivery path using
bin/fm-ensure-agents-md.sh. Ensure the script handles the fork-first rule for
eligible push-mode briefs while preserving exemptions for local-only briefs,
Trillium-owned origins, unreadable origins, and absent clones.

Source: Coding guidelines

Comment thread bin/fm-brief.sh
Comment on lines +153 to +165
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"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The 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.

https://github.com/trillium has no repository segment. This code returns trillium and generates guidance for trillium/trillium. That conflicts with the stated behavior that invalid origins add no rule.

Validate the host, owner, and repository components before printing $name. Add a regression test for an origin with a missing repository component.

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

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
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"
origin=$(git -C "$dir" remote get-url origin 2>/dev/null) || return 0
[ -n "$origin" ] || return 0
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
case "$(printf '%s' "$owner" | tr '[:upper:]' '[:lower:]')" in
trillium) return 0 ;;
esac
[ -n "$name" ] || return 0
printf '%s\n' "$name"
🤖 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-brief.sh` around lines 153 - 165, Update the origin parsing logic in
the repository-name function to validate the host, owner, and non-empty
repository components before deriving or printing name; reject malformed origins
such as https://github.com/trillium without adding a rule, while preserving the
existing trillium-owner exclusion. Add a regression test covering an origin with
a missing repository segment.

@trillium
trillium merged commit 575dfa1 into main Aug 1, 2026
11 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant