Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
5dac5e8
fix(pipeline): guard GitHub fork PR destinations
tiago-peixoto Aug 28, 2026
83f4165
no-mistakes(review): Expose GitHub fork routing through AXI
tiago-peixoto Aug 28, 2026
cfb317a
no-mistakes(review): Use status topology for fork destination guidance
tiago-peixoto Aug 28, 2026
ecff61d
no-mistakes(review): Harden fork PR destination authorization
tiago-peixoto Aug 28, 2026
6f0902b
no-mistakes(review): Require leading PR destination declarations
tiago-peixoto Aug 28, 2026
0884855
no-mistakes(review): Lead E2E intent with PR destination
tiago-peixoto Aug 28, 2026
34ff59c
no-mistakes(review): Document explicit intent for fork PRs
tiago-peixoto Aug 28, 2026
fe5aa18
no-mistakes(document): Consolidate fork PR destination guidance
tiago-peixoto Aug 28, 2026
42b4224
no-mistakes(review): Preserve intent indentation through PR authoriza…
tiago-peixoto Aug 29, 2026
9db4524
no-mistakes(review): Reject legacy normalized PR destination intents
tiago-peixoto Aug 29, 2026
8e44a58
no-mistakes(document): Document fork destination structure and migrat…
tiago-peixoto Aug 29, 2026
61feedd
no-mistakes(review): Bind fork PR authorization to AXI provenance
tiago-peixoto Aug 29, 2026
6239002
no-mistakes(review): Restore authoritative explicit-intent PR authori…
tiago-peixoto Aug 29, 2026
9cdccc5
no-mistakes(review): Preserve exact destinations and superseding PR i…
tiago-peixoto Aug 29, 2026
17a3999
no-mistakes(review): Order superseding PR decisions chronologically
tiago-peixoto Aug 29, 2026
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
2 changes: 1 addition & 1 deletion .agents/skills/repository-routing-security/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ metadata:
- `repos.upstream_url` is the parent repository used for PR base routing; `repos.fork_url` is an optional GitHub fork push target.
- `no-mistakes init --fork-url <url>` expects `origin` to point at the GitHub parent repository and `<url>` at the contributor fork; plain `no-mistakes init` preserves an existing fork URL on idempotent refresh.
- Push code must resolve the push URL via `resolvePushURL` (`internal/pipeline/steps/common_git.go`) so configured forks still receive branch updates, including after a CI repair restarts validation; the non-fork path recovers the credentialled upstream from the worktree's `origin` remote at run time because the DB `upstream_url` is stored redacted (see Credential Redaction below). `Repo.PushURL()` remains correct only for fork-only callers (e.g. `rebase.go`), since fork URLs carry no embedded credentials.
- GitHub PR code must keep `--repo` pointed at the parent and use `--head <fork_owner>:<branch>` when `fork_url` is set; existing-PR lookup must list by the bare branch and filter head-owner fields, never pass `<owner>:<branch>` to `gh pr list --head`.
- GitHub PR code must keep `--repo` pointed at the parent and use `--head <fork_owner>:<branch>` when `fork_url` is set; existing-PR lookup must list by the bare branch and filter head-owner fields, never pass `<owner>:<branch>` to `gh pr list --head`. After provider availability succeeds and before any PR operation, `PRStep` requires an authoritative intent that starts with an unindented `PR destination: owner/repo` line matching that parent and whose persistence proves its leading structure was preserved; incidental repository mentions never authorize publication, and missing, malformed, conflicting, fork, or migrated unproven destinations fail the step.
- Non-GitHub fork MR/PR routing is intentionally out of scope until implemented end to end; if a legacy row has `fork_url` for another provider, PR creation must skip instead of opening a self PR.
- Every new run best-effort refreshes registered upstream/fork URLs from the working clone through `gate.RefreshRepoURLs`: origin is the upstream authority, an existing fork requires one uniquely matching clone remote, both DB fields replace atomically, and every discovery/validation/write failure logs only a bounded reason and continues with the exact old registration. The refresh never rewrites clone or gate remotes; `Repo.URLsVerified` is run-scoped evidence that trusted fetch/push may use the refreshed DB URL instead of an inherited stale gate origin.

Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ $ no-mistakes
# opens the TUI for the active run
```

For GitHub fork contributions, keep `origin` pointed at the parent repository and initialize with `no-mistakes init --fork-url <your-fork-url>`.
For GitHub fork contributions, keep `origin` pointed at the parent repository and initialize with `no-mistakes init --fork-url <your-fork-url>`. PR-producing fork runs follow the [explicit-destination workflow](https://kunchenguid.github.io/no-mistakes/guides/provider-integration/#github-fork-contributions).

From the TUI you act on each **finding**: **auto-fix** ones are applied for you (or approve to let them), **ask-user** ones are a judgement call you approve, fix, or skip.
Once every check is green, the gate forwards your branch to the configured push target and opens the PR for you, so there is no manual `git push origin` and no hand-written PR body.
Expand Down
2 changes: 1 addition & 1 deletion README.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ $ no-mistakes
# 打开当前运行的 TUI
```

如果是 GitHub fork 贡献,让 `origin` 指向父仓库,并用 `no-mistakes init --fork-url <your-fork-url>` 初始化。
如果是 GitHub fork 贡献,让 `origin` 指向父仓库,并用 `no-mistakes init --fork-url <your-fork-url>` 初始化。需要创建 PR 的 fork 运行请遵循[显式目标工作流](https://kunchenguid.github.io/no-mistakes/guides/provider-integration/#github-fork-contributions)。

在 TUI 里你逐条处理 **finding**:**auto-fix** 类自动替你应用(或由你 approve 放行),**ask-user** 类需要你判断,由你 approve、fix 或 skip。
每项检查变绿后,网关会把你的分支转发到配置的推送目标并替你开好 PR,不用手动 `git push origin`,也不用手写 PR 正文。
Expand Down
1 change: 1 addition & 0 deletions docs/src/content/docs/guides/provider-integration.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ no-mistakes init --fork-url git@github.com:your-user/repo.git

With this setup, the Push step updates the fork, including after a CI repair restarts validation, while the PR and CI steps stay scoped to the parent repository.
The GitHub PR step opens PRs with a fork-qualified head such as `your-user:feature-branch`.
Configuring a fork does not authorize publication to its parent. PR-producing fork runs need authoritative explicit intent whose first line is `PR destination: owner/repo`; [`axi run --intent`](/no-mistakes/reference/cli/#no-mistakes-axi-run) is the normal agent workflow, while `rerun --intent` and the existing notify-push intent option carry the same authorization. A bare direct push has no destination, so it can finish validation and fork push before refusing PR publication; attaching later does not modify that run's intent.
Re-running `no-mistakes init` later preserves the stored fork URL unless you pass a new `--fork-url`.

Fork routing currently requires both `origin` and `--fork-url` to be GitHub remotes with owner/repo paths.
Expand Down
7 changes: 7 additions & 0 deletions docs/src/content/docs/reference/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ The gate advertises Git push-option support, so you can skip steps for one push
For GitHub fork contributions, keep `origin` pointed at the parent repository and pass `--fork-url` with your fork remote URL.
The Push step and rebase branch-sync use the fork, including when CI repair restarts validation and reaches Push again, while GitHub PR and CI commands stay scoped to the parent repository and create PRs with `--head <fork-owner>:<branch>`.
Fork routing currently requires both `origin` and `--fork-url` to be GitHub remotes with owner/repo paths.
For the authoritative explicit destination required by a fork run that should create a PR, see [`axi run`](#no-mistakes-axi-run) and [`rerun`](#no-mistakes-rerun).

`--worktree-root` is for directory-scoped toolchain configuration (mise, direnv), which resolves by path ancestry and so never reaches a run worktree under `NM_HOME`.
The flag resolves the directory, then prints the [`worktree_roots`](/no-mistakes/reference/global-config/#worktree_roots) entry to add to `~/.no-mistakes/config.yaml`; the global config is hand-maintained, so `init` never rewrites it for you.
Expand Down Expand Up @@ -111,6 +112,8 @@ no-mistakes axi run --intent "the user's goal" --yes
`--intent` is not a description of the diff.
It is the user's goal or request, and no-mistakes uses it verbatim instead of transcript inference.
Err on the side of completeness: include the goal, important decisions and tradeoffs, constraints or approaches ruled in or out, and explicit requests that might otherwise look surprising in the diff.
For a GitHub repository initialized with `--fork-url`, a run that should create a PR needs authoritative explicit intent beginning with an unindented, dedicated `PR destination: owner/repo` line naming the repository allowed to receive the pull request. `axi run --intent` is the normal agent workflow for supplying it. Repository mentions elsewhere in the intent, including issue links, do not authorize publication.
A bare `git push no-mistakes` creates the run without an authoritative destination; attaching later cannot retrofit one, so the run may complete validation and push to the fork but the PR step refuses creation.
When starting a new run, `axi run` refuses the default branch and uncommitted working trees with actionable errors instead of auto-branching or auto-committing.
Reattaching to an in-flight run does not require `--intent`.
Reattachment accepts either the run's immutable submitted head or its current pipeline head, so pipeline-created fix commits do not detach an unchanged submitting worktree.
Expand Down Expand Up @@ -341,6 +344,10 @@ records the transcript source. If another run is active on that branch, rerun
cancels it before starting over. Treat rerun as a between-runs action after a
failed or cancelled outcome, or after you have committed a separate fix outside
an active run; do not use it to bypass a gate.
For a GitHub fork registration, `rerun --intent` may supply the same authoritative
first-line `PR destination: owner/repo` declaration as `axi run --intent`.
A structure-preserved explicit intent inherited from the selected run remains
authoritative; inferred or legacy normalized intent does not authorize publication.

| Flag | Type | Default | Description |
| ---- | ---- | ------- | ----------- |
Expand Down
5 changes: 3 additions & 2 deletions docs/src/content/docs/reference/pipeline-steps.md
Original file line number Diff line number Diff line change
Expand Up @@ -226,11 +226,12 @@ Creates or updates a pull request.
- If one exists, updates it. If not, creates a new one against the configured base branch.
- If existing-PR discovery fails or its provider response cannot be decoded and validated as a PR listing for the configured repository, stops instead of treating the result as no PR and creating a duplicate.
- Uses `gh` for GitHub, `glab` for GitLab, `forgejo-axi` for Forgejo, `tea` for Gitea, the Bitbucket API for Bitbucket Cloud, and `az` for Azure DevOps
- For GitHub fork routing, keeps `gh --repo` pointed at the parent repository from `origin`, checks existing PRs with the bare branch name, filters matching PRs by head owner, and creates PRs with `--head <fork-owner>:<branch>`
- For GitHub fork routing, after provider availability succeeds and before any PR lookup, update, or creation, requires authoritative explicit intent to start with an unindented, dedicated `PR destination: owner/repo` line and verifies it against the selected parent PR repository. The existing explicit-intent transports, including `axi run --intent`, `rerun --intent`, and the notify-push intent option, carry the same authorization. A bare direct push has none and can fail here after validation and fork push. A missing, conflicting, malformed, or fork destination fails the step instead of publishing; an explicit parent destination preserves fork-head-to-parent contributions. An intent persisted by an older version has no evidence that its leading structure survived normalization, so it also fails closed and requires a fresh explicit intent.
- After that verification, keeps `gh --repo` pointed at the parent repository, checks existing PRs with the bare branch name, filters matching PRs by head owner, and creates PRs with `--head <fork-owner>:<branch>`
- PR title: agent-generated from the final branch delta with user intent when available, in conventional commit format (`type(scope): description` or `type: description`); user-facing product impact should use `feat` or `fix` so release automation can pick it up; when a scope is used, it should be the primary affected real module/package from the changed paths and kept broad rather than file-level. If drafting fails, the fallback uses the neutral title `chore: update pull request` rather than inferring scope from earlier commits.
- Bounds the PR-drafting agent with [`agent_timeout`](/no-mistakes/reference/global-config/#agent_timeout): an expired budget cancels the agent and uses that same fallback rather than leaving the run active indefinitely; a late successful title after the deadline is not used
- The PR stage exclusively owns the complete branch-scope description. It drafts `## What Changed` from the actual final diff after local mutating stages finish, and its fallback lists the final changed paths and statuses.
- PR body includes a `## Intent` section when user intent is available, the final-diff `## What Changed`, and regenerated `## Risk Assessment`, `## Testing`, and `## Pipeline` sections from recorded step results and rounds. Only `## What Changed` describes the complete final branch scope; the deterministic sections remain evidence for the commit each step inspected. Auto-fix results in `## Pipeline` render as an issue -> fix -> verification narrative using captured fix summaries, re-check success text, and any still-open findings; Test details also list the recorded commands.
- PR body includes a `## Intent` section when user intent is available, the final-diff `## What Changed`, and regenerated `## Risk Assessment`, `## Testing`, and `## Pipeline` sections from recorded step results and rounds. User instructions attached to selected findings appear under `### Superseding Decisions`, ordered globally by decision creation time and stable round ID. Repeated decisions remain visible so the latest entry takes precedence over conflicting original intent and earlier entries. Only `## What Changed` describes the complete final branch scope; the deterministic sections remain evidence for the commit each step inspected. Auto-fix results in `## Pipeline` render as an issue -> fix -> verification narrative using captured fix summaries, re-check success text, and any still-open findings; Test details also list the recorded commands.
- `## Pipeline` keeps the existing human-readable signature and includes the stable structured step attestation documented below. Bitbucket Cloud PR descriptions omit HTML-only features (`<details>`, `<code>`, `<video>`, and the attestation comment) because Cloud renders Python-Markdown and escapes raw HTML.
- Generated PR bodies are capped at 63,488 bytes, leaving a 2 KB safety buffer below GitHub's 65,536-character body limit.
- When a body would exceed that cap, the PR step first omits older `## Pipeline` update rounds at clean update boundaries, keeps the newest rounds when possible, and points reviewers to the run log for the full pipeline history.
Expand Down
2 changes: 2 additions & 0 deletions docs/src/content/docs/start-here/introduction.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ touched, so you can keep coding while the pipeline runs.
through the full gate.
- For GitHub fork contributions, the configured push target can be your fork
while `origin` stays pointed at the parent repository used for the PR base.
Parent PR publication is fail-closed and requires authoritative explicit intent
with a first-line `PR destination: owner/repo` declaration.

That design matters for trust. The tool is not trying to hide Git from you. It
is trying to make one deliberate path mean something consistent.
Expand Down
3 changes: 2 additions & 1 deletion docs/src/content/docs/start-here/quick-start.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,8 @@ For GitHub fork contributions, keep `origin` pointed at the parent repository an
no-mistakes init --fork-url git@github.com:you/my-repo.git
```

The gate will push validated branches to the fork while opening PRs against the parent.
The gate pushes validated branches to the fork and can open PRs against the parent.
The parent PR path is fail-closed: supply authoritative explicit intent with an unindented first-line `PR destination: owner/repo` declaration when this run should open a PR. [`axi run --intent`](/no-mistakes/reference/cli/#no-mistakes-axi-run) is the normal agent workflow. A bare direct push stops before PR publication, after validation and fork push, and attaching later cannot modify that run's intent.

```
$ no-mistakes init
Expand Down
17 changes: 12 additions & 5 deletions internal/daemon/manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
"sync"
"sync/atomic"
"time"
"unicode"

"github.com/kunchenguid/no-mistakes/internal/agent"
"github.com/kunchenguid/no-mistakes/internal/config"
Expand Down Expand Up @@ -776,6 +777,7 @@ func (m *RunManager) HandleRerun(ctx context.Context, repoID, branch, previousRu
}

intentSource := db.RunIntentSourceAgent
intentLeadingStructurePreserved := strings.TrimSpace(intent) != ""
if strings.TrimSpace(intent) == "" {
intentSource = ""
if selectedRun.Intent != nil && selectedRun.IntentSource != nil &&
Expand All @@ -785,10 +787,11 @@ func (m *RunManager) HandleRerun(ctx context.Context, repoID, branch, previousRu
// replacement run.
intent = *selectedRun.Intent
intentSource = db.RunIntentSourceRerun
intentLeadingStructurePreserved = selectedRun.IntentLeadingStructurePreserved
}
}

return m.startRunWithIntentSource(ctx, repo, branch, headSHA, baseSHA, "rerun", skipSteps, intent, intentSource)
return m.startRunWithIntentSource(ctx, repo, branch, headSHA, baseSHA, "rerun", skipSteps, intent, intentSource, intentLeadingStructurePreserved)
}

func resolveRerunHead(ctx context.Context, gateDir, branch string, latest *db.Run) (string, error) {
Expand Down Expand Up @@ -850,13 +853,13 @@ func fetchRunDefaultBranch(ctx context.Context, workDir string, repo *db.Repo) e
// A non-empty intent is stamped onto the run as agent-supplied, so the intent
// step uses it instead of inferring from transcripts.
func (m *RunManager) startRun(ctx context.Context, repo *db.Repo, branch, headSHA, baseSHA, trigger string, skipSteps []types.StepName, intent string) (string, error) {
return m.startRunWithIntentSource(ctx, repo, branch, headSHA, baseSHA, trigger, skipSteps, intent, db.RunIntentSourceAgent)
return m.startRunWithIntentSource(ctx, repo, branch, headSHA, baseSHA, trigger, skipSteps, intent, db.RunIntentSourceAgent, true)
}

// startRunWithIntentSource is the common run-creation path. source is empty
// when no intent is supplied, RunIntentSourceAgent for a new explicit
// override, and RunIntentSourceRerun for inherited explicit intent.
func (m *RunManager) startRunWithIntentSource(ctx context.Context, repo *db.Repo, branch, headSHA, baseSHA, trigger string, skipSteps []types.StepName, intent, source string) (string, error) {
func (m *RunManager) startRunWithIntentSource(ctx context.Context, repo *db.Repo, branch, headSHA, baseSHA, trigger string, skipSteps []types.StepName, intent, source string, intentLeadingStructurePreserved bool) (string, error) {
branchRole := telemetryBranchRole(branch, repo.DefaultBranch)
trackStartFailure := func(stage string) {
telemetry.Track("run", telemetry.Fields{
Expand Down Expand Up @@ -894,16 +897,20 @@ func (m *RunManager) startRunWithIntentSource(ctx context.Context, repo *db.Repo
// Cancel any active run for this repo+branch.
m.cancelActiveRuns(repo.ID, branch)

// Preserve leading whitespace exactly as supplied. PR destination
// authorization treats indentation as context, so trimming it could turn an
// example into a top-level declaration. Legacy inherited intents retain
// their false preservation marker and therefore continue to fail closed.
storedIntent := intent
if source != db.RunIntentSourceRerun {
storedIntent = strings.TrimSpace(storedIntent)
storedIntent = strings.TrimRightFunc(storedIntent, unicode.IsSpace)
}
var runIntent *db.RunIntent
if strings.TrimSpace(storedIntent) != "" {
if source == "" {
source = db.RunIntentSourceAgent
}
runIntent = &db.RunIntent{Summary: storedIntent, Source: source, Score: 1}
runIntent = &db.RunIntent{Summary: storedIntent, Source: source, Score: 1, LeadingStructurePreserved: intentLeadingStructurePreserved}
}

run, err := m.db.InsertRunWithIntent(repo.ID, branch, headSHA, baseSHA, runIntent)
Expand Down
Loading
Loading