diff --git a/.agent/exec-plans/completed/execplan-native-grok-agent.md b/.agent/exec-plans/completed/execplan-native-grok-agent.md new file mode 100644 index 000000000..0e1979f6c --- /dev/null +++ b/.agent/exec-plans/completed/execplan-native-grok-agent.md @@ -0,0 +1,239 @@ + +--- +execplanTemplateVersion: 4 +deliveryShape: standalone +--- + +# ExecPlan — Native Grok pipeline agent + +## Purpose / Big Picture + +Make Grok Build a first-class No Mistakes pipeline backend so repositories can +configure `agent: [codex, grok]`. The Grok adapter must use Grok's current +default model unless an operator explicitly supplies a model override, support +native structured output, preserve subprocess and retry safety, and honestly +report whether it can neutralize target-repository instructions when trusted +policy sets `disable_project_settings: true`. If complete isolation cannot be +proved, No Mistakes must keep Grok available for ordinary repositories while +failing closed under that trusted opt-out. + +## Authority and Read Order + +1. `AGENTS.md`, especially **Repo Config Trust Boundary (security)**, + **Context, Concurrency, and Processes**, and **When Making Changes**, owns + the repository implementation and verification rules. +2. `internal/agent/agent.go` owns the adapter interface and native construction. +3. `internal/config/config.go` owns agent selection, probing, binary paths, and + argument override policy. +4. `internal/types/types.go` owns supported agent names. +5. `docs/src/content/docs/reference/global-config.md` and + `docs/src/content/docs/reference/repo-config.md` own public configuration + documentation; `docs/src/content/docs/guides/agents.md` owns agent guidance. +6. Base checkpoint: + `origin/main@6859d1e827f5ab2592a4703d3bab8734a38c9aa5`. + +acceptedCheckpoint: `6859d1e827f5ab2592a4703d3bab8734a38c9aa5` + +## Reference Index + +| Owner | Kind | Authority ref | +|---|---|---| +| Repository rules | git | `AGENTS.md` | +| Agent contract | code | `internal/agent/agent.go` | +| Verified instruction neutralization | code/tests | `internal/agent/gateneutralize_test.go` | +| Adapter resolution | code | `internal/config/config.go` | +| Agent names | code | `internal/types/types.go` | +| User configuration | docs | `docs/src/content/docs/reference/global-config.md` | +| Repo configuration | docs | `docs/src/content/docs/reference/repo-config.md` | +| Agent behavior | docs | `docs/src/content/docs/guides/agents.md` | +| Verification procedure | git | `AGENTS.md` verification sequence | +| Plan authority | installed contract | `/Users/boriza/.codex/.agent/PLANS.md` | + +## Program Integration Line Strategy + +Implement on `feat/native-grok-agent` in +`/Users/boriza/Documents/dev/tmp/no-mistakes`, based on the immutable checkpoint +above. Commit only the Grok adapter, its tests, configuration/docs projections, +and this plan. Push to a PR branch rather than upstream `main`. Agent Platform +consumes the feature only after a tested binary is available locally and on the +remote host; its independent activation plan owns its repo policy change. + +## Milestones + +| Id | Outcome | Status | +|---|---|---| +| M0 | Baseline, security boundary, and Grok CLI wire format measured | complete | +| M1 | Failing tests pin native selection, arguments, parsing, and neutralization | complete | +| M2 | Native Grok adapter and configuration support implemented | complete | +| M3 | Documentation and fake-agent/e2e coverage updated | complete | +| M4 | Repository verification suite passes; provider-backed smoke is truthfully dispositioned | complete | +| M5 | Branch published; Agent Platform activation is either proven safe or explicitly held fail-closed | complete | + +## Progress + +- **2026-08-19:** Confirmed No Mistakes v1.40.3 source at the accepted + checkpoint supports dynamic ACP targets but has no `grok` native agent name, + binary mapping, probe order, adapter, or verified project-instruction + suppression. Confirmed local Grok Build 1.0.5 is authenticated and exposes + headless JSON/JSON-Schema operation plus `grok agent stdio`. +- **2026-08-19:** Added the native adapter, first-class config/type/probe + resolution, managed-argument validation, structured event parsing, role-safe + session resume, fake-agent support, full Grok e2e journey coverage, and + public docs. The first focused test run passed. The first Grok e2e run then + exposed a missing `doctor` row; added a targeted regression and corrected the + supported-agent projection. +- **2026-08-19:** `go test ./cmd/fakeagent ./internal/agent ./internal/config + ./internal/daemon ./internal/types` passed. `go test ./internal/cli -run + TestDoctorAgentChecksIncludesGrok -count=1` passed. `go test -tags=e2e + ./internal/e2e -run 'TestUserJourney/grok' -count=1` passed after the doctor + correction and exercised every agent-driven pipeline step. +- **2026-08-19:** A standalone Grok 1.0.5 inspection probe in + `/tmp/no-mistakes-grok-isolation.FZ9kYU` showed that Grok still discovers the + repository's native `Agents.md` even with `--system-prompt-override` and all + exposed Claude/Cursor compatibility discovery disabled. Changed + `NeutralizesGateInstructions()` to return false and added negative gate + regressions so `disable_project_settings: true` refuses Grok. +- **2026-08-19:** Rebased the implementation onto + `origin/main@6859d1e827f5ab2592a4703d3bab8734a38c9aa5`, preserving upstream's + review-session rule (fresh reviewers; reusable fixer only) and verified Pi + neutralization. Subsequent entries record the completed post-rebase proof. +- **2026-08-19:** The first post-rebase full `make e2e` run timed out at the + historical 300-second package limit while the fourth serial backend + (OpenCode) was 26 seconds into an existing 60-second wait. Grok's focused + journey had already passed. The full rerun passed under a 420-second ceiling + (`internal/e2e` 391.176s; pipeline-step e2e 63.820s). Set the final wrapper + budget to 480 seconds so normal local variance does not consume the entire + margin while individual wait deadlines still surface stuck journeys. +- **2026-08-19:** Post-rebase verification passed: focused affected-package + tests, the focused Grok e2e journey, `make lint`, `go test -race ./...`, the + full `make e2e` rerun, and `go build -o ./bin/no-mistakes + ./cmd/no-mistakes`. The only non-passing proof is the explicitly blocked + provider-backed smoke (HTTP 402); no success is claimed for it. +- **2026-08-19:** Initialized the repository gate with + `kunchenguid/no-mistakes` as upstream and + `p3ngu1nx/no-mistakes` as the fork. AXI run + `01M0CVF0Y466C7CRWCC3HV0ATD` reached `checks-passed` and opened + `https://github.com/kunchenguid/no-mistakes/pull/776`. The pipeline fixed the + missed invocation-environment propagation in commit `c60ea0d5`, added its + process-level regression, clarified Grok's pipeline-only skill role in + `dff83ad6`, and then passed targeted Test, documentation, lint, push, PR, and + hosted CI. Guarded AXI sync integrated `dff83ad6` into this worktree. +- **2026-08-19:** Verified Agent Platform remains fail-closed on both machines: + the local activation worktree uses `[codex, claude]` and the remote clone uses + `[claude, codex]`, both with `disable_project_settings: true` and no Grok + fallback. The separate Agent Platform activation plan owns reconciling the + remote preference order and publishing that repository's workflow changes. +- **Next:** a maintainer reviews and merges PR #776. After a release contains + this adapter, rerun the adversarial project-discovery probe against the then + current Grok CLI before considering Agent Platform activation; absent new + positive isolation proof, keep the trusted opt-out and safe fallback. + +## Findings + +- Grok can run through generic ACP, but generic ACP is intentionally rejected + when `disable_project_settings: true`; Agent Platform retains that boundary. +- Leaving `-m`/`--model` absent is the required model policy: Grok selects its + installed current default, avoiding a stale repository pin. +- Local Grok currently resolves its unpinned default to `grok-4.6`. +- Grok 1.0.5 still discovers native repository instruction surfaces despite + full system-prompt replacement and compatibility-discovery opt-outs. Native + Grok support is therefore valid for ordinary repositories but deliberately + ineligible when `disable_project_settings: true`. +- A live isolated model call reached Grok but failed with HTTP 402 because the + account's Grok Build usage balance is exhausted. Deterministic unit and e2e + validation can proceed. The non-model inspection probe is already sufficient + negative evidence to reject the isolation capability; replenishing usage + cannot turn the current adapter's claim on without a new positive adversarial + probe. + +## Integrated Proof Obligations and Results + +| Obligation | Evidence | Expected observation | Result | +|---|---|---|---| +| Native selection | config/types/constructor tests | `grok` parses, probes `grok`, and constructs a native adapter | passed | +| Default-model policy | argv tests | no managed `-m`/`--model` flag is emitted | passed | +| Structured output | parser + subprocess fixture tests | valid schema result becomes `Result.Output`; malformed/missing output fails closed | passed | +| Instruction isolation | standalone Grok inspect probe + neutralization/gate tests | either every project surface is inert or Grok is refused under the trusted opt-out | negative provider capability observed; fail-closed refusal implemented and focused tests previously passed | +| Process safety | cancellation/reaping tests and existing shell helper contract | invocation uses configured process groups and leaves no child behind | passed in full race suite | +| Focused verification | `go test ./cmd/fakeagent ./internal/agent ./internal/config ./internal/daemon ./internal/types`; targeted doctor and Grok e2e journey | exit 0 | passed | +| Full verification | `make lint`; `go test -race ./...`; `make e2e`; `go build -o ./bin/no-mistakes ./cmd/no-mistakes` | all exit 0 | passed | +| Live smoke | installed branch binary invokes Grok without a model override | successful structured response reports Grok backend | blocked: provider returned HTTP 402 usage balance exhausted; no success claimed | +| Consumer proof | Agent Platform trusted config remains valid locally and remotely | unsafe `[codex, grok]` is rejected while `disable_project_settings: true`; existing safe fallback remains configured | passed: Grok absent and opt-out enabled on both; activation intentionally held | +| Publication proof | AXI run `01M0CVF0Y466C7CRWCC3HV0ATD`; PR #776 | fork push, upstream PR, and hosted checks pass without direct upstream-main write | passed; `checks-passed`, PR open and mergeable | + +## Surprises & Discoveries + +- The installed Grok CLI advertises `--system-prompt-override`, `--verbatim`, + `--json-schema`, and headless JSON output, but no Claude-style + `--setting-sources` switch. Effective instruction isolation therefore needs + empirical proof before the adapter can claim the neutralization capability. +- The first complete Grok e2e journey passed the pipeline but failed its doctor + assertion because the native resolver and doctor table had separate agent + projections. The dedicated doctor regression now pins Grok in that table. +- Live isolation proof is externally blocked by an exhausted Grok Build usage + balance (HTTP 402); this is not a code/test failure. +- Unlike the compatibility surfaces disabled by environment variables, Grok's + native `Agents.md` discovery remains active under + `--system-prompt-override`. A replacement system prompt is defense in depth, + not verified project-setting isolation. +- Adding Grok as the fourth serial user-journey backend exceeded the e2e + wrapper's historical five-minute package timeout before an existing OpenCode + wait received its own full deadline. The suite budget must grow with the + intentionally serial environment-owning matrix. +- Independent review found that Grok's subprocess environment omitted + `RunOpts.Env`. Routing it through the existing `gitSafeEnv(opts.CWD, + opts.Env)` primitive fixed the defect and a subprocess regression now proves + the invocation value reaches Grok. + +## Decision Log + +| Date | Decision | Rationale | +|---|---|---| +| 2026-08-19 | Implement a native adapter rather than relaxing Agent Platform's opt-out. | The project-instruction boundary is an intentional security feature; generic ACP cannot currently prove it. | +| 2026-08-19 | Do not set a managed Grok model. | The operator explicitly wants Grok's changing default model rather than a repository pin. | +| 2026-08-19 | Use TDD and require a real isolated instruction-loading probe. | Repository rules mandate TDD, and the security capability cannot be inferred from flag names alone. | +| 2026-08-19 | Keep the live isolation proof explicitly partial rather than weakening `disable_project_settings` or claiming fixture evidence as live proof. | The provider balance blocker is external; the trusted security boundary remains fail-closed. | +| 2026-08-19 | Report Grok as unverified and reject it whenever `disable_project_settings: true`. | The standalone inspect probe discovered native project instructions; claiming neutralization would violate the trusted repo-config boundary. | +| 2026-08-19 | Do not activate Grok in Agent Platform yet. | Agent Platform intentionally enables the trusted opt-out, so `[codex, grok]` must fail until Grok exposes complete and empirically verified isolation. | +| 2026-08-19 | Accept the AXI environment fix and documentation clarification. | Both changes reuse existing owners: invocation environment belongs to `gitSafeEnv`, and public guidance must distinguish pipeline-backend support from user-level skill installation. Fresh rereview, targeted Test, and hosted CI passed. | + +## Durable Next Action / Recovery + +The implementation is published at PR #776 with checks passed. A maintainer can +review and merge it without further local delivery work. Do not change Agent +Platform to `grok` while its trusted config keeps +`disable_project_settings: true`: the current adapter must be rejected there. +The provider-backed structured-response smoke remains blocked until Grok usage +is replenished; do not represent it as passed. The isolation decision is not +blocked on billing because a standalone probe already supplied negative proof. +If PR monitoring later reports a conflict, let the active AXI CI monitor own +the rebase; do not hand-rebase the branch. + +## Outcomes & Retrospective + +Native Grok support is implemented, independently reviewed, locally verified, +published from a fork, and green in hosted CI. The adapter leaves Grok's model +unmanaged by default, supports native structured output and fixer-session +resume, preserves invocation environment, and fails closed under the trusted +project-settings opt-out. AXI corrected one environment-propagation miss and +clarified documentation before publication. + +Agent Platform activation is deliberately held, not silently incomplete: +current Grok 1.0.5 demonstrably discovers native project instructions, while +both Agent Platform machines retain `disable_project_settings: true` and a +verified non-Grok fallback. HTTP 402 still blocks a successful live response +smoke, but it does not weaken the negative isolation evidence or the shipped +fail-closed behavior. Remaining human action is review/merge of PR #776. + +Revision note (2026-08-19): recorded the implemented adapter/config/docs/e2e +scope, focused proof, doctor omission and correction, and the external HTTP 402 +live-provider blocker so execution can resume without chat history. + +Revision note (2026-08-19): rebound the plan to the post-rebase checkpoint, +recorded the standalone negative isolation proof and fail-closed design, +replaced unsafe Agent Platform activation with an explicit held outcome, and +recorded the complete post-rebase local verification ladder. + +Revision note (2026-08-19): recorded AXI fixes, fork/PR/CI publication, +guarded local synchronization, local/remote held-consumer proof, and final +outcomes before moving the plan to completed. diff --git a/.agent/exec-plans/tech-debt-tracker.md b/.agent/exec-plans/tech-debt-tracker.md new file mode 100644 index 000000000..cc4200246 --- /dev/null +++ b/.agent/exec-plans/tech-debt-tracker.md @@ -0,0 +1,6 @@ +# Tech Debt Tracker + +Accumulated tech debt from exec-plans. Review before starting new plans. + +| Date | Source Plan | Description | Priority | Status | +|---|---|---|---|---| diff --git a/.circleci/config.yml b/.circleci/config.yml deleted file mode 100644 index 15878badf..000000000 --- a/.circleci/config.yml +++ /dev/null @@ -1,248 +0,0 @@ -# ============================================================================= -# CircleCI — Parallel CI for multi-language projects -# ============================================================================= -# Used alongside GitHub Actions for: -# - Parallel execution on free tier (3000 credits/mo) -# - Faster builds with machine executors -# - Additional build matrix coverage -# ============================================================================= - -version: 2.1 - -# Orbs — reusable packages -orbs: - rust: circleci/rust@1.6.1 - python: circleci/python@2.1.1 - node: circleci/node@6.3.0 - go: circleci/go@1.11.2 - trunk: trunk-io/trunk@1.0.0 - -# Executors -executors: - small: - docker: - - image: cimg/base:current - resource_class: small - - medium: - docker: - - image: cimg/base:current - resource_class: medium - - large: - docker: - - image: cimg/base:current - resource_class: large - - rust-executor: - docker: - - image: cimg/rust:1.80 - resource_class: medium - - python-executor: - docker: - - image: cimg/python:3.12 - resource_class: medium - - go-executor: - docker: - - image: cimg/go:1.22 - resource_class: medium - - node-executor: - docker: - - image: cimg/node:22.0 - resource_class: medium - -# Commands (reusable steps) -commands: - install-rust-tools: - steps: - - run: - name: Install Rust tools - command: | - cargo install cargo-deny cargo-audit || true - - install-python-tools: - steps: - - run: - name: Install Python tools - command: | - pip install uv || curl -LsSf https://astral.sh/uv/install.sh | sh - - install-go-tools: - steps: - - run: - name: Install Go tools - command: | - go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest - - save-deps-cache: - parameters: - key: - type: string - steps: - - save_cache: - key: deps-{{ .Branch }}-<< parameters.key >>-{{ checksum "package-lock.json" }}{{ checksum "Cargo.lock" }}{{ checksum "go.sum" }}{{ checksum "uv.lock" }} - paths: - - ~/.cargo - - ~/.cache/pip - - node_modules - - vendor - - restore-deps-cache: - parameters: - key: - type: string - steps: - - restore_cache: - keys: - - deps-{{ .Branch }}-<< parameters.key >>-{{ checksum "package-lock.json" }}{{ checksum "Cargo.lock" }}{{ checksum "go.sum" }}{{ checksum "uv.lock" }} - - deps-{{ .Branch }}-<< parameters.key >>- - - deps-main-<< parameters.key >>- - -# Jobs -jobs: - # --- TRUNK CHECK (unified) --- - trunk-lint: - executor: small - steps: - - checkout - - trunk-check: - trunk-args: [--ci=fix] - - # --- RUST --- - rust-check: - executor: rust-executor - steps: - - checkout - - restore-deps-cache: - key: rust - - run: - name: Check formatting - command: cargo fmt --all -- --check - - run: - name: Clippy lints - command: cargo clippy --all-targets --all-features -- -D warnings - - run: - name: Run tests - command: cargo test --all --workspace - - save-deps-cache: - key: rust - - rust-build: - executor: rust-executor - steps: - - checkout - - restore-deps-cache: - key: rust - - run: - name: Build release - command: cargo build --release - - save-deps-cache: - key: rust - - persist_to_workspace: - root: target - paths: - - release - - # --- PYTHON --- - python-check: - executor: python-executor - steps: - - checkout - - install-python-tools - - restore-deps-cache: - key: python - - run: - name: Install dependencies - command: uv sync --all-extras - - run: - name: Ruff lint - command: uv run ruff check . - - run: - name: Ruff format check - command: uv run ruff format --check . - - run: - name: Type check - command: uv run mypy . || true - - run: - name: Run tests - command: uv run pytest --cov -v - - save-deps-cache: - key: python - - store_test_results: - path: test-results - - # --- GO --- - go-check: - executor: go-executor - steps: - - checkout - - restore-deps-cache: - key: go - - run: - name: Vet - command: go vet ./... - - run: - name: Lint - command: | - go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest - golangci-lint run - - run: - name: Test - command: go test -v -race -coverprofile=coverage.out ./... - - store_artifacts: - path: coverage.out - - save-deps-cache: - key: go - - # --- TYPESCRIPT --- - node-check: - executor: node-executor - steps: - - checkout - - restore-deps-cache: - key: node - - run: - name: Install deps - command: npm ci || true - - run: - name: Lint - command: npx biome lint . || npx eslint . || true - - run: - name: Format check - command: npx biome format --check . || npx prettier --check . || true - - run: - name: Test - command: npm test || true - - save-deps-cache: - key: node - -# Workflows -workflows: - version: 2 - ci: - jobs: - - trunk-lint - - rust-check: - filters: - branches: - only: /.*/ - tags: - only: /^v.*/ - - rust-build: - requires: - - rust-check - - python-check: - filters: - branches: - only: /.*/ - - go-check: - filters: - branches: - only: /.*/ - - node-check: - filters: - branches: - only: /.*/ diff --git a/.github/actions/require-no-mistakes/README.md b/.github/actions/require-no-mistakes/README.md new file mode 100644 index 000000000..f39c4a793 --- /dev/null +++ b/.github/actions/require-no-mistakes/README.md @@ -0,0 +1,135 @@ +# `require-no-mistakes` + +Composite action that checks whether a pull request body declares a completed, +head-bound no-mistakes pipeline run. It is the reusable shared implementation +of the check named **`PR must be raised via no-mistakes`**; enforcing +repositories can call it instead of copying the shell into their own workflow. + +It verifies, in order: + +1. the PR body carries the no-mistakes signature line; +2. the body carries a parseable `` + comment; +3. the attestation's `head_sha` equals the PR head SHA, so a later push cannot + pass on an older attestation; +4. `review`, `test`, and `document` each recorded `status == "completed"`. + Quota skips and agent skips are not compliant. + +Missing or unparseable attestation reports the no-mistakes `>= 1.46.0` floor; +a missing signature reports the not-raised-via-no-mistakes guidance. + +## Usage + +Consumers pin a release tag or a commit SHA. Never `@main`: `main` is editable +by the very PR the gate is judging. + +```yaml +name: Require no-mistakes +on: + pull_request: + types: [opened, edited, reopened] + branches: [main] + +permissions: + contents: read + +jobs: + check: + name: PR must be raised via no-mistakes + runs-on: ubuntu-latest + steps: + - uses: kunchenguid/no-mistakes/.github/actions/require-no-mistakes@ + with: + exempt-authors: | + github-actions[bot] + dependabot[bot] +``` + +Replace `` with a no-mistakes release tag or commit SHA +that contains this action. + +The job name must stay exactly `PR must be raised via no-mistakes` so branch +rulesets keep matching the same check across the fleet. + +An ordinary `pull_request`-triggered caller forwards no PR facts: the action +reads the body, head SHA, head branch, author, and number from the workflow +event payload. Pass the `pr-*` inputs only when driving it from another event. + +## Inputs + +| Input | Default | Purpose | +| --- | --- | --- | +| `exempt-authors` | `""` | Newline- or comma-separated author logins that bypass the gate (automation accounts that cannot be routed through the pipeline). | +| `exempt-bot-authors` | `false` | When true, every `*[bot]` author bypasses the gate. | +| `exempt-head-branches` | `""` | Glob patterns; a matching head branch bypasses the gate, for structural automation branches such as `release-please--*`. | +| `pr-body`, `pr-head-sha`, `pr-head-ref`, `pr-author`, `pr-number` | `""` | Override the corresponding event-payload fact. | + +Which steps are required is deliberately **not** an input. A caller configures +who is exempt, never what the gate certifies, so no repository can weaken the +check while still reporting the same name. + +## Outputs + +| Output | Meaning | +| --- | --- | +| `compliant` | `true` only when the PR satisfied the pipeline gate. It remains `false` for an exemption because bypass is not validation. | +| `exempt` | `true` when a configured exemption bypassed the gate. | +| `exempt-reason` | Why the PR was exempt; empty when it was judged. | + +## Boundary + +The action never checks out or executes repository code, so it is safe on +`pull_request` runs from forks. Callers should keep `permissions: contents: read` +and stay on `pull_request` rather than `pull_request_target`. + +An exemption is trusted outer-repository policy supplied by the caller's pinned +workflow. It does not claim that no-mistakes ran: exempt PRs report +`compliant=false` and `exempt=true`. This is separate from the invariant that no +standing configuration may skip a step inside a no-mistakes run. + +### Non-goal: a contributor guardrail, not a forgery-proof boundary + +This gate is a **contributor guardrail**. It is explicitly **not** a +forgery-proof security boundary, and it is not trying to become one. + +The attestation is a deterministic, commit-bound declaration published in the +PR body, not a cryptographic signature. A pull request author can edit their own +body and reproduce the documented format by hand, and such a PR passes this +check. That is a **known and accepted limitation**, and a **pre-existing** one: +it is inherited verbatim from the inline gate this action extracts, so +consolidating the fleet onto one implementation neither introduces nor widens +it. The action emits a warning on every structural pass to keep the boundary +visible in the required check's logs. + +What it does reliably catch is the case it exists for: a contributor who +bypassed the pipeline by accident, a malformed or incomplete declaration, and an +attestation left stale by a later push. It authorizes nothing against an author +who forges the format on purpose. + +Authenticated (signed) attestations are the robust fix. They are tracked +separately as backlog item `nm-signed-attestations-r1` and are deliberately out +of scope for this action. + +## Rollout + +This repository's own gate (`.github/workflows/no-mistakes-required.yml`) is a +thin caller of this action, pinned to the commit that first published it. GitHub +downloads `uses:` actions at job setup, so the pin must always name a ref that +already carries the action; a caller pinned to a tag that predates it fails +closed on every pull request. + +Pinning the gate to an already-published commit is the self-certification guard. +A pull request that edits this action is fully **tested** on its own head - the +repository's Go tests execute `verify.py` from the working tree - while the +required check judging that pull request keeps running the published pinned copy. The +gate is therefore never rewritten by the change it is judging. Bumping the pin +is a deliberate, separate pull request. + +Migrating the other enforcing repositories follows the same rule: pin a released +tag or a commit SHA, never `@main`. + +## Behavior is pinned by tests + +`require_no_mistakes_action_test.go` in the repository root executes +`verify.py` the way a runner does and covers every verdict, the exemption +surface, and the event-payload fallback. diff --git a/.github/actions/require-no-mistakes/action.yml b/.github/actions/require-no-mistakes/action.yml new file mode 100644 index 000000000..3e73de233 --- /dev/null +++ b/.github/actions/require-no-mistakes/action.yml @@ -0,0 +1,96 @@ +name: Require no-mistakes +description: >- + Verify that a pull request body declares a no-mistakes pipeline run: the body + carries the signature line and a v1 pipeline-step attestation bound to the + current PR head, with review, test, and document all completed. + +inputs: + pr-body: + description: >- + Pull request body to judge. Defaults to the body in the workflow event + payload, so an ordinary pull_request-triggered caller passes nothing. + required: false + default: "" + pr-head-sha: + description: >- + Commit the forge currently has as the PR head. The attestation must bind + to it. Defaults to the event payload's pull_request.head.sha. + required: false + default: "" + pr-head-ref: + description: >- + PR head branch name, matched against exempt-head-branches. Defaults to the + event payload's pull_request.head.ref. + required: false + default: "" + pr-author: + description: >- + PR author login, matched against exempt-authors. Defaults to the event + payload's pull_request.user.login. + required: false + default: "" + pr-number: + description: >- + PR number, used only in log output. Defaults to the event payload's + pull_request.number. + required: false + default: "" + exempt-authors: + description: >- + Newline- or comma-separated author logins that bypass the gate, for the + automation accounts a repository cannot route through the pipeline (for + example github-actions[bot] raising the release-please PR). Empty by + default: exemptions are opt-in per repository. + required: false + default: "" + exempt-bot-authors: + description: >- + When true, every author login ending in '[bot]' bypasses the gate. Broader + than exempt-authors and off by default. + required: false + default: "false" + exempt-head-branches: + description: >- + Newline- or comma-separated glob patterns; a PR whose head branch matches + one bypasses the gate. Intended for structural automation branches such as + 'release-please--*'. + required: false + default: "" + +outputs: + compliant: + description: "true only when the PR body structurally satisfies the pipeline gate; false for exemptions." + value: ${{ steps.verify.outputs.compliant }} + exempt: + description: "true when the PR bypassed the gate through a configured exemption." + value: ${{ steps.verify.outputs.exempt }} + exempt-reason: + description: "Human-readable reason the PR was exempt; empty when it was judged." + value: ${{ steps.verify.outputs.exempt-reason }} + +runs: + using: composite + steps: + - name: Verify no-mistakes signature and pipeline attestation + id: verify + shell: bash + env: + PR_BODY: ${{ inputs.pr-body }} + PR_HEAD_SHA: ${{ inputs.pr-head-sha }} + PR_HEAD_REF: ${{ inputs.pr-head-ref }} + PR_AUTHOR: ${{ inputs.pr-author }} + PR_NUMBER: ${{ inputs.pr-number }} + NM_EXEMPT_AUTHORS: ${{ inputs.exempt-authors }} + NM_EXEMPT_BOT_AUTHORS: ${{ inputs.exempt-bot-authors }} + NM_EXEMPT_HEAD_BRANCHES: ${{ inputs.exempt-head-branches }} + run: | + set -eu + if command -v python3 >/dev/null 2>&1; then + PY=python3 + elif command -v python >/dev/null 2>&1; then + PY=python + else + echo "::error::python interpreter not found; cannot parse pipeline attestation." >&2 + exit 1 + fi + "$PY" "${GITHUB_ACTION_PATH}/verify.py" diff --git a/.github/actions/require-no-mistakes/verify.py b/.github/actions/require-no-mistakes/verify.py new file mode 100644 index 000000000..f01d74ebc --- /dev/null +++ b/.github/actions/require-no-mistakes/verify.py @@ -0,0 +1,282 @@ +#!/usr/bin/env python3 +"""Enforce that a pull request was raised through the no-mistakes pipeline. + +This is the single shared implementation of the `PR must be raised via +no-mistakes` gate. Enforcing repositories can call the `require-no-mistakes` +composite action instead of copying this logic into their own workflows; the +inline copies drifted (several fleet copies never gained the head_sha bind), +which is exactly what this file exists to prevent. + +The verdict is a pure function of the pull request body plus the PR head SHA: + + 1. the body carries the no-mistakes signature line; + 2. the body carries a parseable v1 pipeline-step attestation comment; + 3. the attestation's head_sha equals the current PR head SHA, so a later push + cannot pass on an older attestation; + 4. review, test, and document each recorded status == "completed". Skips + (quota or agent) and failures are not compliant. + +Nothing here reads the repository contents, so a fork's code is never executed. + +NON-GOAL: this gate is a CONTRIBUTOR GUARDRAIL, not a forgery-proof security +boundary. The signature line and the attestation are author-editable assertions +published in the PR body, so a hand-written body that reproduces the documented +format passes this check and exits 0. That is a known and accepted limitation, +and a pre-existing one: it is inherited verbatim from the inline gate this file +consolidates, not introduced by consolidating it. What the gate does reliably +catch is the case it exists for - a contributor who bypassed the pipeline by +accident, a malformed or incomplete declaration, and an attestation left stale +by a later push. It authorizes nothing against an author who forges the format +on purpose. Authenticated (signed) attestations are the robust fix and are +tracked separately as backlog item nm-signed-attestations-r1; do not build them +into this file. +""" + +from __future__ import annotations + +import fnmatch +import json +import os +import sys + +SIGNATURE_MARKER = ( + "Updates from [git push no-mistakes](https://github.com/kunchenguid/no-mistakes)" +) +ATTESTATION_PREFIX = "" + +# Fixed on purpose: these are the steps whose completion the gate certifies. A +# caller configures WHO is exempt, never WHICH steps are required, so a repo +# cannot quietly weaken the gate while still reporting the same check name. +REQUIRED_STEPS = ("review", "test", "document") + +VERSION_FLOOR = "1.46.0" +VERSION_FLOOR_PR = "https://github.com/kunchenguid/no-mistakes/pull/670" + + +def env(name: str) -> str: + return (os.environ.get(name) or "").strip() + + +def event_payload() -> dict: + """Read the workflow event payload, so a caller need not forward PR facts.""" + path = os.environ.get("GITHUB_EVENT_PATH") or "" + if not path or not os.path.exists(path): + return {} + try: + with open(path, "r", encoding="utf-8") as handle: + payload = json.load(handle) + except (OSError, json.JSONDecodeError): + return {} + if not isinstance(payload, dict): + return {} + pull_request = payload.get("pull_request") + return pull_request if isinstance(pull_request, dict) else {} + + +def parse_list(raw: str) -> list[str]: + """Split a newline- or comma-separated input into trimmed, non-empty items.""" + items: list[str] = [] + for line in raw.replace(",", "\n").splitlines(): + value = line.strip() + if value: + items.append(value) + return items + + +def parse_bool(raw: str) -> bool: + return raw.strip().lower() in ("true", "1", "yes", "on") + + +def emit_output(name: str, value: str) -> None: + path = os.environ.get("GITHUB_OUTPUT") or "" + if not path: + return + try: + with open(path, "a", encoding="utf-8") as handle: + handle.write(f"{name}={value}\n") + except OSError: + pass + + +def fail(message: str) -> "NoReturn": # type: ignore[name-defined] + sys.stderr.write(message) + emit_output("compliant", "false") + emit_output("exempt", "false") + raise SystemExit(1) + + +class Facts: + def __init__(self) -> None: + payload = event_payload() + head = payload.get("head") if isinstance(payload.get("head"), dict) else {} + user = payload.get("user") if isinstance(payload.get("user"), dict) else {} + + self.body = os.environ.get("PR_BODY") or _payload_str(payload, "body") + self.head_sha = env("PR_HEAD_SHA") or _payload_str(head, "sha").strip() + self.head_ref = env("PR_HEAD_REF") or _payload_str(head, "ref").strip() + self.author = env("PR_AUTHOR") or _payload_str(user, "login").strip() + number = env("PR_NUMBER") + if not number: + raw_number = payload.get("number") + number = str(raw_number) if isinstance(raw_number, int) else "" + self.number = number + + +def _payload_str(payload: dict, key: str) -> str: + value = payload.get(key) + return value if isinstance(value, str) else "" + + +def exemption_reason(facts: Facts) -> str: + """Return why this PR is exempt from the gate, or "" when it is not.""" + authors = parse_list(os.environ.get("NM_EXEMPT_AUTHORS") or "") + if facts.author and facts.author in authors: + return f"author {facts.author} is a configured exempt author" + + if parse_bool(os.environ.get("NM_EXEMPT_BOT_AUTHORS") or "") and facts.author.endswith("[bot]"): + return f"author {facts.author} is a bot and bot authors are exempt" + + for pattern in parse_list(os.environ.get("NM_EXEMPT_HEAD_BRANCHES") or ""): + if facts.head_ref and fnmatch.fnmatchcase(facts.head_ref, pattern): + return f"head branch {facts.head_ref} matches exempt pattern {pattern}" + + return "" + + +def check_signature(facts: Facts) -> None: + if SIGNATURE_MARKER in facts.body: + return + fail( + "::error::This PR was not raised through no-mistakes.\n\n" + "Contributions to this repository must be submitted via 'git push no-mistakes'.\n" + "That pipeline runs the required review/test/lint/CI steps and writes a\n" + "deterministic '## Pipeline' section into the PR body containing:\n\n" + f" {SIGNATURE_MARKER}\n\n" + "See CONTRIBUTING.md for setup and the full workflow.\n\n" + f"PR author: {facts.author}\n" + ) + + +def fail_missing_attestation(facts: Facts) -> "NoReturn": # type: ignore[name-defined] + fail( + "::error::This PR is missing structured pipeline step attestation.\n\n" + f"This repository requires no-mistakes >= {VERSION_FLOOR} " + f"({VERSION_FLOOR_PR}). " + "Older no-mistakes that only writes the signature line is not enough.\n\n" + "The PR body must include a comment of the form:\n" + ' \n\n' + "Contributions to this repository must be submitted via 'git push no-mistakes'.\n" + "See CONTRIBUTING.md for setup and the full workflow.\n\n" + f"PR author: {facts.author}\n" + ) + + +def parse_attestation(facts: Facts) -> dict: + start = facts.body.find(ATTESTATION_PREFIX) + if start < 0: + fail_missing_attestation(facts) + start += len(ATTESTATION_PREFIX) + end = facts.body.find(ATTESTATION_CLOSING, start) + if end < 0: + fail_missing_attestation(facts) + try: + payload = json.loads(facts.body[start:end]) + except json.JSONDecodeError: + fail_missing_attestation(facts) + if not isinstance(payload, dict): + fail_missing_attestation(facts) + if not isinstance(payload.get("head_sha"), str) or not isinstance(payload.get("steps"), list): + fail_missing_attestation(facts) + return payload + + +def check_head_bind(facts: Facts, attested_head: str) -> None: + """Bind the attestation to the commit the forge currently has for this PR. + + Without this the gate certifies a body, not a commit: a compliant PR can be + pushed to afterwards and the stale attestation would still pass. This is the + piece the drifted fleet copies were missing. + """ + if attested_head and facts.head_sha and attested_head == facts.head_sha: + return + fail( + "::error::Pipeline attestation head_sha does not match the current PR head.\n\n" + f"attestation.head_sha: {attested_head or '(missing)'}\n" + f"PR head: {facts.head_sha or '(missing)'}\n\n" + "A later push must not pass on an older attestation. " + "Re-run 'git push no-mistakes' so the PR body attestation binds to the current head.\n\n" + "See CONTRIBUTING.md for setup and the full workflow.\n\n" + f"PR author: {facts.author}\n" + ) + + +def check_required_steps(facts: Facts, steps: list) -> None: + status_by_step: dict[str, str] = {} + for item in steps: + if not isinstance(item, dict): + fail_missing_attestation(facts) + name = item.get("step") + status = item.get("status") + if not isinstance(name, str) or name == "" or not isinstance(status, str): + fail_missing_attestation(facts) + status_by_step[name] = status + + incomplete = [] + for name in REQUIRED_STEPS: + status = status_by_step.get(name) + if status == "completed": + continue + if status is None: + incomplete.append(f"{name} (missing)") + else: + incomplete.append(f"{name} (status={status})") + + if not incomplete: + return + listed = ", ".join(incomplete) + fail( + f"::error::Required no-mistakes pipeline steps are not completed: {listed}.\n\n" + "This repository requires " + f"{', '.join(REQUIRED_STEPS)} to have status=completed. " + "Quota skips and agent skips are not compliant.\n\n" + "Contributions to this repository must be submitted via 'git push no-mistakes'.\n" + "See CONTRIBUTING.md for setup and the full workflow.\n\n" + f"PR author: {facts.author}\n" + ) + + +def main() -> int: + facts = Facts() + + reason = exemption_reason(facts) + if reason: + print(f"Skipping no-mistakes enforcement: {reason}.") + emit_output("exempt", "true") + emit_output("exempt-reason", reason) + # Exemption is an explicit caller policy, not evidence that the PR ran + # and satisfied the pipeline. Keep the successful bypass distinct from + # a validated compliant verdict for downstream consumers. + emit_output("compliant", "false") + return 0 + emit_output("exempt", "false") + + check_signature(facts) + label = f"PR #{facts.number}" if facts.number else "PR" + print(f"Found no-mistakes signature in {label} body.") + + payload = parse_attestation(facts) + check_head_bind(facts, payload["head_sha"]) + check_required_steps(facts, payload["steps"]) + + print("Found structurally compliant pipeline step attestation.") + print( + "::warning::PR-body attestation is author-editable and is not cryptographic proof " + "that no-mistakes produced it." + ) + emit_output("compliant", "true") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 54bd956d6..44186e754 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,81 +1,38 @@ name: CI on: - push: - branches: [main, master, develop] pull_request: - branches: [main, master, develop] - -permissions: - contents: read - -concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true + branches: + - main + # Never create a run for a release-please PR. GITHUB_TOKEN-opened PRs land in + # action_required and never start; excluding the exact release-output set means + # no run is created at all. push/tag/release triggers are unaffected. + paths-ignore: + - .release-please-manifest.json + - CHANGELOG.md jobs: - detect: - name: Detect Languages + check: + name: ci / lint runs-on: ubuntu-latest - outputs: - has_rust: ${{ steps.detect.outputs.has_rust }} - has_deny: ${{ steps.detect.outputs.has_deny }} - has_python: ${{ steps.detect.outputs.has_python }} - has_go: ${{ steps.detect.outputs.has_go }} - has_typescript: ${{ steps.detect.outputs.has_typescript }} - has_security: ${{ steps.detect.outputs.has_security }} - has_trunk: ${{ steps.detect.outputs.has_trunk }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 + + - uses: actions/setup-go@v6 with: - fetch-depth: 1 + go-version-file: go.mod - - id: detect + - name: Format check run: | - set +e - # Initialize all outputs to false (critical: every output must be set) - echo "has_rust=false" >> $GITHUB_OUTPUT - echo "has_deny=false" >> $GITHUB_OUTPUT - echo "has_python=false" >> $GITHUB_OUTPUT - echo "has_go=false" >> $GITHUB_OUTPUT - echo "has_typescript=false" >> $GITHUB_OUTPUT - echo "has_security=false" >> $GITHUB_OUTPUT - echo "has_trunk=false" >> $GITHUB_OUTPUT - - # Rust: needs Cargo.toml AND deny.toml - if [ -f "Cargo.toml" ] || [ -f "**/Cargo.toml" ]; then - echo "has_rust=true" >> $GITHUB_OUTPUT - if [ -f "deny.toml" ]; then - echo "has_deny=true" >> $GITHUB_OUTPUT - fi - fi - - # Python: needs pyproject.toml, setup.py, requirements.txt, or *.py - if [ -f "pyproject.toml" ] || [ -f "setup.py" ] || [ -f "requirements.txt" ] || find . -maxdepth 3 -name "*.py" -type f 2>/dev/null | head -1 | grep -q .; then - echo "has_python=true" >> $GITHUB_OUTPUT - fi - - # Go: needs go.mod - if [ -f "go.mod" ]; then - echo "has_go=true" >> $GITHUB_OUTPUT - fi - - # TypeScript / JS: needs package.json (with type=module or tsconfig or js files) - if [ -f "package.json" ]; then - echo "has_typescript=true" >> $GITHUB_OUTPUT - fi - - # Security: always run (bandit/gitleaks/dependency-review) - echo "has_security=true" >> $GITHUB_OUTPUT - - # Trunk: only if trunk.yaml exists - if [ -f "trunk.yaml" ]; then - echo "has_trunk=true" >> $GITHUB_OUTPUT + output=$(gofmt -l .) + if [ -n "$output" ]; then + echo "Files not formatted:" + echo "$output" + exit 1 fi - echo "" - echo "=== Detected ===" - echo "rust=${{ steps.detect.outputs.has_rust }} deny=${{ steps.detect.outputs.has_deny }} python=${{ steps.detect.outputs.has_python }} go=${{ steps.detect.outputs.has_go }} typescript=${{ steps.detect.outputs.has_typescript }} security=${{ steps.detect.outputs.has_security }} trunk=${{ steps.detect.outputs.has_trunk }}" + - name: Vet + run: go vet ./... test: name: ${{ matrix.name }} @@ -105,32 +62,11 @@ jobs: # it, and it still keeps a wedged runner well short of the six-hour default. timeout-minutes: 40 steps: - - uses: actions/checkout@v4 - - uses: dtolnay/rust-toolchain@stable - - uses: Swatinem/rust-cache@v2 - - name: cargo fmt --check - run: | - cargo fmt --all -- --check 2>&1 || echo "::warning::cargo fmt issues (advisory)" - - name: cargo clippy - run: | - cargo clippy --all-targets -- -D warnings 2>&1 || echo "::warning::clippy issues (advisory)" - - name: cargo test - run: | - cargo test --workspace --no-fail-fast 2>&1 || echo "::warning::test failures (advisory)" + - uses: actions/checkout@v6 - cargo-deny: - name: Cargo Deny (Advisories + Licenses) - needs: detect - if: needs.detect.outputs.has_deny == 'true' - runs-on: ubuntu-latest - continue-on-error: true - steps: - - uses: actions/checkout@v4 - - uses: dtolnay/rust-toolchain@stable - - name: cargo-deny check - uses: EmbarkStudios/cargo-deny-action@v1 + - uses: actions/setup-go@v6 with: - arguments: --all-features + go-version-file: go.mod # The Windows suite is process-spawn bound, not compute bound: the # git-backed packages run thousands of git.exe invocations, and Defender @@ -165,29 +101,13 @@ jobs: if: runner.os != 'Windows' run: go test -race ./... - python: - name: Python - needs: detect - if: needs.detect.outputs.has_python == 'true' - runs-on: ubuntu-latest - continue-on-error: true - steps: - - uses: actions/checkout@v4 - - uses: actions/setup-python@v5 - with: - python-version: '3.11' - - name: Install ruff - run: pip install ruff - - name: ruff check - run: ruff check . 2>&1 || echo "::warning::ruff issues (advisory)" - - name: ruff format --check - run: ruff format --check . 2>&1 || echo "::warning::format issues (advisory)" # Git-heavy packages are the Windows wall floor. Keep this list in lockstep # with NM_CI_WINDOWS_GIT_EXCLUDE on the core shard; the workflow tests # resolve both through `go list` and fail if they drift. - name: Test on Windows (git-heavy) if: runner.os == 'Windows' && matrix.shard == 'git' run: go test -v -timeout=15m ./internal/git ./internal/branchsync ./internal/gate ./internal/evidence ./internal/pipeline/steps ./internal/daemon ./internal/eval + - name: Test on Windows (core) if: runner.os == 'Windows' && matrix.shard == 'core' shell: pwsh @@ -197,116 +117,34 @@ jobs: $pkgs = go list ./... | Where-Object { $_ -notmatch $env:NM_CI_WINDOWS_GIT_EXCLUDE } go test -v -timeout=15m @pkgs - go: - name: Go - needs: detect - if: needs.detect.outputs.has_go == 'true' - runs-on: ubuntu-latest - continue-on-error: true - steps: - - uses: actions/checkout@v4 - - uses: actions/setup-go@v5 - with: - go-version: '1.22' - - name: gofmt check - run: gofmt -l . 2>&1 || echo "::warning::gofmt issues (advisory)" - - name: go vet - run: go vet ./... 2>&1 || echo "::warning::go vet issues (advisory)" - - name: go test - run: go test ./... 2>&1 || echo "::warning::test failures (advisory)" - - typescript: - name: TS/JS - needs: detect - if: needs.detect.outputs.has_typescript == 'true' - runs-on: ubuntu-latest - continue-on-error: true - steps: - - uses: actions/checkout@v4 - - uses: actions/setup-node@v4 - with: - node-version: '20' - cache: 'npm' - - run: npm ci --no-audit --no-fund || npm install --no-audit --no-fund - - name: lint - run: | - if [ -f package.json ] && grep -q '"lint"' package.json; then - npm run lint 2>&1 || echo "::warning::lint issues (advisory)" - fi - - name: test - run: | - if [ -f package.json ] && grep -q '"test"' package.json; then - npm test 2>&1 || echo "::warning::test failures (advisory)" - fi + - name: Build + run: go build ./cmd/no-mistakes - security: - name: Security Scan - needs: detect - if: needs.detect.outputs.has_security == 'true' + e2e: runs-on: ubuntu-latest - continue-on-error: true steps: - - uses: actions/checkout@v4 - - name: gitleaks - uses: gitleaks/gitleaks-action@v2 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + - uses: actions/checkout@v6 - dep-review: - name: Dependency Review - if: github.event_name == 'pull_request' - runs-on: ubuntu-latest - continue-on-error: true - steps: - - uses: actions/checkout@v4 - - uses: actions/dependency-review-action@v4 + - uses: actions/setup-go@v6 with: - fail-on-severity: low - - lint: - name: ci / lint - if: always() - needs: [detect, rust, cargo-deny, python, go, typescript, security, dep-review] - runs-on: ubuntu-latest - steps: - - name: Aggregate lint gate - run: | - failed=0 - for pair in \ - "rust:${{ needs.rust.result }}" \ - "cargo-deny:${{ needs.cargo-deny.result }}" \ - "python:${{ needs.python.result }}" \ - "go:${{ needs.go.result }}" \ - "typescript:${{ needs.typescript.result }}" \ - "security:${{ needs.security.result }}" \ - "dep-review:${{ needs.dependency-review.result }}" \ - ; do - name="${pair%%:*}" - result="${pair#*:}" - if [ "$result" = "failure" ] || [ "$result" = "cancelled" ]; then - echo " ✗ $name: $result" - failed=$((failed + 1)) - elif [ "$result" = "success" ] || [ "$result" = "skipped" ]; then - echo " ✓ $name: $result" - else - echo " ? $name: $result" - fi - done - if [ "$failed" -gt 0 ]; then - echo "" - echo "❌ $failed lint check(s) failed" - exit 1 - fi - echo "" - echo "✅ All lint checks passed (or were skipped)" - - test: + go-version-file: go.mod + + # The e2e suite drives the real no-mistakes binary against a fake + # agent through `git push -> daemon -> pipeline -> push to upstream` + # for claude, codex, grok, and opencode. It builds the binary itself, so + # no separate build step is needed. Linux-only for now: opencode's + # ephemeral HTTP server picks up unused ports via :0, which is + # fine on Linux runners but flakes on Windows agent harnesses. + - name: End-to-end suite + run: make e2e + + test-gate: name: ci / test - if: always() - needs: [lint] + if: ${{ always() }} + needs: [test, e2e] runs-on: ubuntu-latest steps: - - name: Test gate + - name: Require every test job to pass run: | - # Test gate is currently combined with lint - echo "✅ All test stages passed (gated via ci / lint)" + test "${{ needs.test.result }}" = "success" + test "${{ needs.e2e.result }}" = "success" diff --git a/.github/workflows/infisical.yml b/.github/workflows/infisical.yml deleted file mode 100644 index 2f0d31785..000000000 --- a/.github/workflows/infisical.yml +++ /dev/null @@ -1,59 +0,0 @@ -# Pull secrets from Infisical into CI -# Requires: INFISICAL_TOKEN (machine identity service token) stored in repo secrets -# Docs: https://infisical.com/docs/cli/usage - -name: Infisical Sync - -on: - workflow_call: - workflow_dispatch: - push: - branches: [main, master] - paths: - - '.github/workflows/infisical.yml' - pull_request: - -jobs: - sync-secrets: - name: Pull secrets from Infisical - runs-on: blacksmith-2vcpu-ubuntu-2204 - timeout-minutes: 5 - steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: Install Infisical CLI - run: | - curl -1sLf "https://dl.cloudsmith.io/public/infisical/infisical-cli/setup.deb.sh" | sudo -E bash - sudo apt-get install -y infisical - - - name: Pull secrets to .env - env: - INFISICAL_TOKEN: ${{ secrets.INFISICAL_TOKEN }} - INFISICAL_PROJECT_ID: ${{ vars.INFISICAL_PROJECT_ID || secrets.INFISICAL_PROJECT_ID || '8efe392e-56a6-4c3c-89f9-8141183dd7e8' }} - INFISICAL_ENV: ${{ vars.INFISICAL_ENV || github.ref == 'refs/heads/main' && 'prod' || github.ref == 'refs/heads/staging' && 'staging' || 'dev' }} - run: | - # Validate required secrets are present - if [ -z "$INFISICAL_TOKEN" ]; then - echo "::error::INFISICAL_TOKEN secret not configured in repo settings" - exit 1 - fi - if [ -z "$INFISICAL_PROJECT_ID" ]; then - echo "::error::INFISICAL_PROJECT_ID var or secret not set" - exit 1 - fi - - # Pull all secrets from the configured env into .env - infisical run \ - --projectId "$INFISICAL_PROJECT_ID" \ - --env "$INFISICAL_ENV" \ - --token "$INFISICAL_TOKEN" \ - -- bash -c 'echo "Secrets loaded into env successfully" && env | grep -E "^[A-Z_]+=" | grep -v -E "^(PATH|HOME|PWD|SHELL|HOSTNAME|TERM|GITHUB_|RUNNER_|CARGO_|DEPLOY_|INPUT_|IMAGE_|ACTIONS_|STEP_|BOOT_|GRUB_|LS_COLORS|INVOCATION_ID|JOURNAL_)" | head -20' - - - name: Upload secrets as artifact (debug only, never published) - if: failure() - uses: actions/upload-artifact@v4 - with: - name: infisical-debug - path: .env - retention-days: 1 \ No newline at end of file diff --git a/.github/workflows/no-mistakes-required.yml b/.github/workflows/no-mistakes-required.yml index a2bdb8fa2..2556923a1 100644 --- a/.github/workflows/no-mistakes-required.yml +++ b/.github/workflows/no-mistakes-required.yml @@ -3,7 +3,17 @@ run-name: "PR #${{ github.event.pull_request.number }} body compliance - ${{ git on: pull_request: - types: [opened, edited, synchronize, reopened] + # Body-bearing events only. The verdict is a pure function of + # pull_request.body, so a push carries no new body to judge - but it does + # move the head SHA. The pipeline pushes (Push step) before it writes the + # deterministic "## Pipeline" section (PR step), so a `synchronize` trigger + # pinned a FAILURE check run to the new head for a body the same run was + # about to fix. GitHub keeps that failure next to the later `edited` SUCCESS + # rather than replacing it, and `gh pr checks` collapses same-named check + # runs by startedAt alone, so the pipeline's own CI monitor could read the + # stale failure and park the run red forever (PR #773). No ruleset requires + # this status check, so no head SHA needs a run of its own. + types: [opened, edited, reopened] branches: - main # Never create a run for a release-please PR. The job-level author exemption @@ -19,7 +29,7 @@ permissions: # GitHub concurrency groups retain at most one pending run, replacing older # pending runs even when cancel-in-progress is false. Give body-bearing events # an immutable per-event group so first-time-fork approvals can never collapse -# opened/edited checks. Keep synchronize/reopened coalescing as before. +# opened/edited checks. Keep reopened coalescing as before. concurrency: group: no-mistakes-required-${{ github.event.pull_request.number }}-${{ (github.event.action == 'opened' || github.event.action == 'edited') && github.run_id || 'head-change' }} cancel-in-progress: true @@ -32,34 +42,21 @@ jobs: # - github-actions[bot] opens the release-please PR via GITHUB_TOKEN # - dependabot[bot] opens dependency update PRs # Other authors (human or bot) must raise PRs through `git push no-mistakes`. + # + # These stay job-level rather than moving to the action's `exempt-authors` + # input on purpose: an in-job exemption still requires the run to start, and + # a GITHUB_TOKEN PR's run is created in action_required and never starts. A + # skipped job is also what every ruleset in the fleet already sees for these + # authors, so keeping the condition here preserves the exact verdict shape. if: >- github.event.pull_request.user.login != 'github-actions[bot]' && github.event.pull_request.user.login != 'dependabot[bot]' && github.event.pull_request.user.login != 'release-please[bot]' steps: - - name: Verify no-mistakes signature in PR body - env: - PR_BODY: ${{ github.event.pull_request.body }} - PR_AUTHOR: ${{ github.event.pull_request.user.login }} - PR_NUMBER: ${{ github.event.pull_request.number }} - run: | - set -eu - marker='Updates from [git push no-mistakes](https://github.com/kunchenguid/no-mistakes)' - if printf '%s' "${PR_BODY:-}" | grep -qF -- "$marker"; then - echo "Found no-mistakes signature in PR #${PR_NUMBER} body." - exit 0 - fi - { - echo "::error::This PR was not raised through no-mistakes." - echo - echo "Contributions to this repository must be submitted via 'git push no-mistakes'." - echo "That pipeline runs the required review/test/lint/CI steps and writes a" - echo "deterministic '## Pipeline' section into the PR body containing:" - echo - echo " $marker" - echo - echo "See CONTRIBUTING.md for setup and the full workflow." - echo - echo "PR author: ${PR_AUTHOR}" - } >&2 - exit 1 + # Pinned to the immutable commit that published the action, never @main: + # main is editable by the very pull request this gate is judging. A pull + # request that changes the action is still TESTED here (the repository's + # own Go tests execute verify.py from the PR head), but it cannot certify + # itself through the required check. + - name: Verify no-mistakes signature and pipeline attestation + uses: kunchenguid/no-mistakes/.github/actions/require-no-mistakes@32d396ac0f29135daf7fcb9964aba9d5f4e796d6 # post-v1.57.1, untagged (action added in #819) diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml deleted file mode 100644 index 345805e34..000000000 --- a/.github/workflows/scorecard.yml +++ /dev/null @@ -1,49 +0,0 @@ -# ============================================================================= -# OpenSSF Scorecard — Automated security best practices check -# ============================================================================= -# Runs weekly + on main to track supply chain security posture -# Free for public repos, adds security badge -# ============================================================================= - -name: OpenSSF Scorecard - -on: - branch_protection_rule: - schedule: - - cron: '25 4 * * 1' # Weekly Monday 4:25 UTC - push: - branches: [main] - -permissions: read-all - -jobs: - analysis: - name: Scorecard analysis - runs-on: ubuntu-latest - security: - permissions: read-all - - steps: - - name: Checkout - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - with: - persist-credentials: false - - - name: Run Scorecard - uses: ossf/scorecard-action@62b2cac7ed8198b15735ed49ab1e5cf35480ba46 # v2.4.0 - with: - results_file: results.sarif - results_format: sarif - publish_results: true - - - name: Upload SARIF - uses: github/codeql-action/upload-sarif@fca7ace96b7d713c7035881819e25a804e825323 # v3.28.18 - with: - sarif_file: results.sarif - - - name: Upload artifact - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 - with: - name: SARIF file - path: results.sarif - retention-days: 5 diff --git a/.github/workflows/trunk-check.yml b/.github/workflows/trunk-check.yml deleted file mode 100644 index cb6198004..000000000 --- a/.github/workflows/trunk-check.yml +++ /dev/null @@ -1,114 +0,0 @@ -# ============================================================================= -# Trunk Check — Unified linting/formatting in GitHub Actions -# ============================================================================= -# Historically ran trunk-io/trunk-action to coordinate prettier, actionlint, -# ruff, mypy, clippy, golangci-lint, shellcheck, eslint, etc. However: -# -# 1. trunk-io/trunk-action@1.3.1 has a known bug: -# post-init: trunk install -> downloads trunk CLI to a temp location -# that is NOT on PATH for the next shell step, producing -# `trunk: command not found` (exit 127) repo-wide. -# 2. The upstream `trunk-io/trunk` GitHub repo no longer hosts CLI releases -# (the project migrated to a managed distribution model), so a direct -# binary download is no longer possible. -# 3. The repo's `ci / lint` + `ci / test` jobs (ci.yml) already run ruff, -# mypy, clippy, golangci-lint, cargo fmt, etc. independently — so dropping -# the trunk layer does not lose coverage. -# 4. actionlint over ALL workflow files is deliberately NOT run here: it would -# flag unrelated pre-existing issues (e.g. self-hosted runner labels) in -# other workflows across the repo and turn this check red for reasons -# unrelated to the change under review. Choose a single fleet-wide actionlint -# pass separately if desired. -# -# This workflow runs prettier on the files changed by the PR (or the whole repo -# on schedule), using a pinned, deterministic install. No trunk dependency. -# ============================================================================= - -name: Trunk Check - -on: - pull_request: - merge_group: - push: - branches: [main, develop] - schedule: - - cron: "0 3 * * 1" # Weekly Monday 3am UTC - -concurrency: - group: trunk-${{ github.ref }} - cancel-in-progress: true - -permissions: - contents: read - -jobs: - trunk-check: - name: Lint & Format - runs-on: ubuntu-latest - timeout-minutes: 10 - - steps: - - name: Checkout - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - - - name: Install Prettier - shell: bash - run: | - set -euo pipefail - npm install --global prettier@3.6.2 - prettier --version - - - name: prettier check - shell: bash - run: | - set -euo pipefail - - if [[ "${{ github.event_name }}" == "schedule" ]]; then - # Full-repo pass on schedule: prefer compiled ignore via .prettierignore - # over git diff, which has no meaningful base on a scheduled run. - if [ -f .prettierignore ]; then - raw=$(prettier --check 2>&1 || true) - else - raw=$(prettier --check '**/*.{md,yml,yaml,json,jsonc,mdx}' 2>&1 || true) - fi - if grep -qE "\[warn\]|\[error\]" <<<"$raw"; then - printf '%s\n' "$raw" | grep -E "\[warn\]|\[error\]" | head -80 - echo "::error::prettier --check failed (full-repo schedule pass); run prettier --write" - exit 1 - fi - echo "prettier OK (schedule full-repo pass)" - exit 0 - fi - - if [[ "${{ github.event_name }}" == "pull_request" ]]; then - base_ref="${{ github.event.pull_request.base.sha }}" - elif [[ "${{ github.event_name }}" == "merge_group" ]]; then - base_ref="${{ github.event.merge_group.base_sha }}" - else - base_ref="${{ github.event.before }}" - fi - - git fetch --no-tags --depth=1 origin "$base_ref" - - files_arg="$(cd "$GITHUB_WORKSPACE" && git diff --diff-filter=ACMR --name-only FETCH_HEAD HEAD -- '*.md' '*.yml' '*.yaml' '*.json' '*.jsonc' '*.mdx')" - - if [ -z "$files_arg" ]; then - echo "No changed Prettier-supported files to check." - exit 0 - fi - - mapfile -t files <<<"$files_arg" - - ignore_args=() - if [ -f .prettierignore ]; then - ignore_args+=(--ignore-path .prettierignore) - fi - if [ -f .gitignore ] && [ ! -f .prettierignore ]; then - ignore_args+=(--ignore-path .gitignore) - fi - - if ! prettier --check "${files[@]}" "${ignore_args[@]}"; then - echo "::error::prettier --check failed on changed files; run prettier --write" - exit 1 - fi - echo "prettier OK (scoped to changed files)" diff --git a/.gitignore b/.gitignore index dbaf7d903..1259c1861 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ # Build output bin/ tmp/ +.tmp/ # Go *.exe @@ -32,3 +33,10 @@ demo_raw.gif .env .env.local .no-mistakes/evidence/ + +# Agent scratch HOME inside a run worktree +# The CI-repair agent reproduces a failing check locally by pointing HOME at a +# directory in the worktree, which fills it with Go module and OS caches. The +# repair commit stages with `git add -A`, so without this entry those caches are +# committed and pushed onto the pull request branch. +.ci-local-home/ diff --git a/.mergify.yml b/.mergify.yml deleted file mode 100644 index 7de2e29cb..000000000 --- a/.mergify.yml +++ /dev/null @@ -1,138 +0,0 @@ -# Mergify Configuration — Optimized for multi-language monorepos -# Docs: https://docs.mergify.com/ - -pull_request_rules: - # Auto-merge when all CI checks pass and PR is approved - - name: Auto-merge when approved + CI green - conditions: - - "#review-requested=0" - - "#approved-reviews-by>=1" - - check-success=ci - - check-success=lint - - check-success=typecheck - - check-success=test - - -conflict - - -closed - actions: - merge: - method: squash - commit_message_template: | - {{ title }} (#{{ number }}) - - Co-authored-by: {{ author }} - post_merge: - action: close - - # Auto-merge dependabot/Renovate PRs when CI passes - - name: Auto-merge dependency updates - conditions: - - author=dependabot[bot] | renovate[bot] - - check-success=ci - - -conflict - - -closed - actions: - merge: - method: squash - commit_message_template: | - {{ title }} (#{{ number }}) - - Co-authored-by: {{ author }} - post_merge: - action: close - - # Auto-merge bot PRs (CI configs, formatting) when CI passes - - name: Auto-merge bot housekeeping PRs - conditions: - - author=trunk-io[bot] | mergify[bot] | github-actions[bot] - - check-success=ci - - check-success=lint - - -conflict - - -closed - actions: - merge: - method: squash - - # Add reviewers based on changed paths - - name: Request review from team - conditions: - - -closed - - -draft - actions: - request_reviews: - teams: - - phenotype/core - github_accounts: - - KooshaPari - - # Label PRs based on changed files - - name: Label Python changes - conditions: - - files~=\.py$ - actions: - label: - add: - - python - - - name: Label Rust changes - conditions: - - files~=\.rs$|Cargo\. - actions: - label: - add: - - rust - - - name: Label Go changes - conditions: - - files~=\.go$|go\. - actions: - label: - add: - - go - - - name: Label TypeScript changes - conditions: - - files~=\.ts$|\.tsx$|package\.json - actions: - label: - add: - - typescript - - # Close stale PRs after 30 days - - name: Close stale PRs - conditions: - - -closed - - -draft - - age>=30d - - "#review-requested=0" - actions: - comment: - message: > - This PR has been automatically closed after 30 days of inactivity. - Feel free to reopen if still relevant. - close: {} - - # Warn on large PRs - - name: Warn on large PRs - conditions: - - -closed - - -draft - - "#files>20" - actions: - comment: - message: > - **Large PR Alert**: This PR touches {{ number }} files. - Consider splitting into smaller PRs for easier review. - - # Add ready-to-merge label when all checks pass - - name: Add ready-to-merge label - conditions: - - -closed - - -draft - - check-success=ci - - check-success=lint - - check-success=test - - "#approved-reviews-by>=1" - actions: - label: - add: - - ready-to-merge diff --git a/AGENTS.md b/AGENTS.md index abcd13305..105b04ee9 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -18,22 +18,68 @@ Safest local verification sequence after non-trivial changes: - `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 ` expects `origin` to point at the GitHub parent repository and `` at the contributor fork; plain `no-mistakes init` preserves an existing fork URL on idempotent refresh. -- Push and CI auto-fix push code must resolve the push URL via `resolvePushURL` (`internal/pipeline/steps/common_git.go`) so configured forks still receive branch updates; 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. +- 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 :` when `fork_url` is set; existing-PR lookup must list by the bare branch and filter head-owner fields, never pass `:` to `gh pr list --head`. -- GitLab and Bitbucket fork MR/PR routing is intentionally out of scope until implemented end to end; if a legacy row has `fork_url` for those hosts, PR creation must skip instead of opening a self PR. +- 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. +**Repository Forge Identity (`internal/forgecontext`)** + +- Optional global `forge_profiles` map raw remote host tokens/SSH aliases to one isolated `gh` or `glab` config directory, plus an optional `expected_login` pin. The resolver owns profile selection, validation, parent/fork ambiguity, provider-specific fail-closed activation, and the immutable run environment; do not add ambient account switching or per-step routing. Profile identity for the parent/fork same-profile check is the config directory AND the pin, so conflicting pins fail as ambiguous instead of silently picking one account (`sameProfile`/`expectLogin` own the rationale). +- A resolved context must reach built-in provider commands, configured shell commands, native agents, managed agent servers, and recovered approval reconciliation. Never mutate the daemon environment or persist credentials/profile selection in the DB; recovery re-resolves from current global config. +- No configured profiles means exact legacy ambient behavior. Online auth failures keep provider steps' existing skip behavior; deterministic config/routing errors fail before the pipeline. The public contract lives in `docs/src/content/docs/reference/global-config.md`. + **Credential Redaction in Stored URLs and Errors (security)** -- `gate.InitWithFork` runs the upstream URL through `safeurl.Redact` before every DB persist (`UpdateRepoMetadata*`, `InsertRepoWithIDAndFork`) and the "gate initialized" log line; the bare gate's `origin` remote still carries the full credentialled URL (via `provisionGate`) so carved worktrees authenticate. Because the DB copy is redacted, push and CI auto-fix push must recover the credential from the worktree's `origin` remote at run time (`resolvePushURL`/`resolveUpstreamURL`), never from `Repo.UpstreamURL`/`Repo.PushURL()`. +- `gate.InitWithFork` runs the upstream URL through `safeurl.Redact` before every DB persist (`UpdateRepoMetadata*`, `InsertRepoWithIDAndFork`) and the "gate initialized" log line; the bare gate's `origin` remote still carries the full credentialled URL (via `provisionGate`) so carved worktrees authenticate. Because the DB copy is redacted, push and branch-sync code must recover the credential from the worktree's `origin` remote at run time (`resolvePushURL`/`resolveUpstreamURL`), never from `Repo.UpstreamURL`/`Repo.PushURL()`. - Step-failure errors (`executor.go` `FailStep`/log/IPC emit) and the Bitbucket resolve-repo error are redacted via `safeurl.RedactText`/`safeurl.Redact` so a credentialled URL wrapped into an error can never reach a step log or `runs.error`. Reuse `internal/safeurl` for new redaction sites rather than adding a git-local helper; it is already wired into `git.Run`/step git-run error formatting. - Regressions: `TestInitRedactsCredentialURL`, `TestResolveUpstreamURL_PreservesCredential`, `TestResolveUpstreamURL_FallsBackToRecordedURL`, `TestResolvePushURL_ForkWinsOverCredential`. +**Home-Path Redaction in Published PR Content (security)** + +- `internal/safepath` is the one owner of home-directory redaction, the path analogue of `internal/safeurl`. `RedactText` rewrites the process's own home plus `/home/`, `/Users/`, and `C:\Users\` to `~`, unconditionally and for every occurrence. Add new shapes there rather than scrubbing paths at a call site. Candidate resolution must stay free of `filepath.IsAbs`/`VolumeName` and of any reliance on `filepath.Clean`'s separator normalisation: those answer for the build platform, and on Windows `IsAbs` discards the POSIX-rooted `HOME` that Git Bash, MSYS2, and Cygwin set - silently disabling redaction instead of failing. Regression: `TestUsableHomeCandidate_AcceptsBothPlatformSpellings`, `TestHomeCandidates_AreSeparatorSpellingIndependent`. +- `PRStep.buildPRContent` is the single render boundary: it drafts through `draftPRContent` and returns `redactPRContent(content)`, and `Execute` publishes exactly that. Every source that can reach a PR body - agent prose, extracted intent, findings, fix summaries, step errors, artifact `path`, artifact captions, and captured output embedded from evidence files - is covered there, so a new rendering path cannot reintroduce the leak. Redaction runs after every length cap, which is only safe because the placeholder is never longer than the path it replaces. +- The `artifacts[].path` description in `testFindingsSchema` (`common.go`) must not solicit absolute paths, and must not forbid them either. The renderer's allowlist is the worktree or the run's evidence directory and a path under neither is dropped, while the evidence directory defaults under the operator's home - so soliciting more just re-supplies what the boundary has to strip, and a blanket "never report a home directory path" clause makes an obedient agent drop its own evidence. Publication safety is the `pr.go` boundary's job; the schema only stops soliciting paths from elsewhere on the machine. Regressions: `TestTestFindingsSchema_DoesNotSolicitAbsolutePaths`, `TestTestFindingsSchema_KeepsEvidenceDirectoryPathsReportable`. +- Two other public surfaces deliberately do NOT share this rendering and are not covered: agent-authored commit subjects (`commitAgentFixes` -> `Commit.RenderFixMessage`), which reach the remote through Push, and the opt-in evidence branch (`test.evidence.store_in_repo`), which copies artifact files verbatim. Keep the `internal/safepath` package doc honest about that scope. +- The PR body must contain exactly ONE live pipeline-attestation marker, the run's own. `require-no-mistakes` (`.github/actions/require-no-mistakes/verify.py`) binds the FIRST marker in the RAW body to the PR head, so a foreign copy placed earlier fails a PR the pipeline did produce - and a code fence is no defense, because that scan is raw text. Step agents embed foreign markers routinely, by capturing a generated PR body as evidence. +- Neutralize at the assembly choke point (`appendGeneratedSectionsToCleanBodyWithinLimit` plus the two intent paths), never per render path. `pipelineMD` alone carries the real marker and is left intact; `BuildPipelineSummaryFor` neutralizes its own step-detail blocks, which quote agent text. A first attempt put this in `escapePipelineFoldMarkers` - per-render-path - and shipped three live foreign markers to #831 anyway. Regressions: `TestPRStep_ForeignAttestationsInEveryComponentDoNotShadowTheRealOne` (all components at once), plus the per-component guards in `pr_test.go`. +- Regressions: `internal/safepath/redact_test.go`, `internal/pipeline/steps/pr_homepath_test.go`. + **GitLab Backend (`internal/scm/gitlab`)** -- The backend is pinned against `glab v1.5x`, whose flag surface drifts between versions: the auth check must be host-scoped (`--hostname `, falling back to unscoped only when the host is unknown), `glab mr list` no longer accepts `--state opened`, and the daemon's detached-HEAD worktree breaks `glab ci get`, so pipeline jobs are read via the branch-independent `glab api .../pipelines//jobs` REST endpoint. +- The backend is pinned against `glab v1.5x`, whose flag surface drifts between versions: the auth check must be host-scoped (`--hostname `, falling back to unscoped only when the host is unknown), `glab mr list` no longer accepts `--state opened`, `glab mr update` has no `-y`/`--yes` flag at all (unlike `mr create`, which does, so `UpdatePR` must not pass it), and the daemon's detached-HEAD worktree breaks `glab ci get`, so pipeline jobs are read via the branch-independent `glab api .../pipelines//jobs` REST endpoint. - The comments in `internal/scm/gitlab/gitlab.go` own the full rationale for each trap; extend them there when you hit new glab version drift. +**Gitea Backend (`internal/scm/gitea`)** + +- Verified empirically against a real `tea 0.15.1` CLI and a real Gitea 1.27.2 + Actions instance (Docker/Podman `gitea/gitea` + `gitea/act_runner`), not guessed from docs. `tea whoami` has no `--login` flag (unlike every other tea entity subcommand), so `Host.Available` scopes its check through `tea api --login /user` instead - the same host-scoping purpose as glab's `--hostname`. +- `tea actions runs view --jobs --output json` does NOT emit clean structured JSON: `--output json` renders the run header as plain text and only the trailing jobs array is real JSON, and that array carries `status` (queued/in_progress/completed) but no `conclusion` (success/failure/...) at the job level. Job-level pass/fail is read from the REST endpoint (`GET /repos/{owner}/{repo}/actions/runs/{run}/jobs`, which does carry `status` + `conclusion`) reached through `tea api`, which reuses tea's own stored login/token - no separate HTTP client or credential needed. +- `tea pulls list --output json` renders every field (including `index` and `mergeable`) as a JSON *string*, while `tea pulls --output json` (single-PR view) renders the same fields with native JSON types (`index` an int, `mergeable`/`hasMerged` bools). The two response shapes are genuinely different structs in `gitea.go`; do not unify them. +- `tea pulls create` has no `--output json` flag and echoes the PR body into its human-readable stdout, so a body containing an `http(s)://` URL can defeat a naive "first URL line" scrape. `CreatePR` re-lists the PR by head branch via `tea pulls list` for a structured result instead of parsing create's own output; scraping stdout is only a last-resort fallback. +- tea infers "which Gitea instance" from the current directory's git remote, which the daemon's detached bare-gate repo never has, so every invocation carries `--login ` explicitly. The login name is resolved from tea's own `config.yml` by host (`scm.ResolveGiteaLogin`), mirroring `glabKnowsHost`/`ghKnowsHost` for detection. +- `Capabilities().MergeableState` is declined (matching Bitbucket): Gitea's PR `mergeable` field has a documented upstream bug (go-gitea/gitea#25849) that can stick `false` after a conflict is actually resolved. +- `tea actions runs list`'s array order is not documented as newest-first, and a branch can have more than one run sharing the same head SHA (e.g. a manual UI re-run), so `GetChecks`/`FetchFailedCheckLogs` select the run via `mostRecentRun` (highest numeric run ID) rather than trusting list order or index `[0]`. +- The comments in `internal/scm/gitea/gitea.go` own the full rationale for each trap. + +**Shared PR-Enforcement Action (`.github/actions/require-no-mistakes`)** + +- The shared implementation of the `PR must be raised via no-mistakes` gate is a composite action that lets enforcing repositories replace copied, drift-prone scripts. It verifies the signature line, parses the v1 pipeline-step attestation, binds `head_sha` to the PR head, and requires `review`, `test`, and `document` to be `completed`. Callers pin a release tag or commit SHA, never `@main`, which the judged PR can edit. Per-repo configuration is exemptions only (`exempt-authors`, `exempt-bot-authors`, `exempt-head-branches`); which steps are required is deliberately not an input, so no caller can weaken the gate while still reporting the same check name. The action README owns usage; `CONTRIBUTING.md` owns the contributor-facing contract. +- This repository's own gate (`.github/workflows/no-mistakes-required.yml`) is a thin caller of the action, pinned at an already-published commit SHA. GitHub downloads `uses:` at job setup, so the pin must always name a ref that already carries the action. That pin IS the self-certification guard: a PR editing the action is fully tested on its own head (the Go tests execute the working-tree `verify.py`) while the required check judging it runs the published pinned copy, so the change cannot rewrite its own judge. Bumping the pin is a separate deliberate PR. +- This repo's automation exemptions stay in the job-level `if:`, not in `exempt-authors`. An in-job exemption still needs the run to start, and a GITHUB_TOKEN PR's run is created in `action_required` and never starts; the `paths-ignore` entries exist for the same reason. Repos without that constraint should prefer the action's inputs. +- Duplicate step records are LAST-WINS by design (`check_required_steps` in `verify.py`), and a skip-shaped sibling field on a `completed` record is deliberately not inspected. Some pre-migration inline gates were stricter (requiring every record of a name to be `completed`); that strictness is explicitly NOT the standard, and relaxing to last-wins on migration is the intended outcome, not a regression. Do not "harden" this without an owner decision. +- A caller that gains `head_sha` binding must also drop `synchronize` from `on.pull_request.types`, matching this repository's own post-#773 trigger set. Binding plus `synchronize` pins a stale FAILURE check run to a head the pipeline is about to fix, and `gh pr checks` collapses same-named runs by `startedAt`, so the CI monitor can park the run red forever. That change is only safe where no ruleset or branch protection REQUIRES the check - otherwise a pushed head gets no run and the requirement blocks the merge forever. Verify per repo with `gh api repos///rulesets` plus `.../branches//protection`; at the time of the fleet migration `treehouse`, `sshhip`, and `wheelhouse` required it and therefore keep `synchronize`. +- Migrating a repository is rarely a one-file swap. Repos whose tests extract and execute the inline `run:` block (an `extractGateScript()` helper and its gate test) break at import once the block is gone, and repo-level `AGENTS.md` notes that tell agents to hand-copy the gate from a sibling repository must be rewritten - that copying is the drift the shared action exists to remove. +- Regressions: `require_no_mistakes_action_test.go` executes `verify.py` the way a runner does (verdicts, exemption surface, event-payload binding); `workflow_no_mistakes_required_test.go` owns the CALLER - immutable-SHA pin, single delegating step, exemptions, triggers, concurrency identity, fork boundary - and drives the real action through the event payload. + +**OpenCode Adapter Failure Reporting (`internal/agent/opencode*.go`)** + +- opencode reports a failed turn on `info.error` with an HTTP 200 and no parts, and serializes every named error as `{"name": ..., "data": {...}}` - the payload fields are nested under `data`, never at the top level. Decoding only the flat shape silently blanks the message, and ignoring non-`StructuredOutputError` variants drops the cause entirely so the run reports the undiagnosable `opencode returned no text output`. `opencodeMessageError` owns the wire shape and `opencodeMessageFailure` owns the surfaced error. +- Retry is opencode's call, not ours: `classifyOpencodeTransient` trusts `data.isRetryable` (status class only as a fallback) and deliberately does not fall through to the shared substring matching, so a 400 quoting a provider's rate-limit prose is never retried. Regressions: `TestOpencodeAgent_FailedTurnSurfacesProviderErrorInsteadOfEmptyOutput`, `TestOpencodeAgent_StructuredOutputErrorReadsNestedErrorData`, `TestOpencodeAgent_RetriesRetryableProviderErrorThenSucceeds`, `TestClassifyOpencodeTransient`. +- A retryable verdict is necessary but not sufficient: `runOnce` creates a FRESH session per attempt, so retrying a turn that already invoked a tool replays its side effects with no memory of them. `classifyOpencodeTransient` withdraws the retry when `opencodeTurnRanTools` saw a `tool` part on the SSE stream (`opencodeStreamState.toolInvoked`, durable - not the cosmetic `pendingStepSeparator` that `step-finish` sets and every emit clears) or in the message response body. Resuming the session instead was rejected: opencode's message endpoint only appends another user message, so whether the model re-runs the tool stays its judgement rather than a guarantee. `step-finish` alone is never tool activity - every turn emits one - and the blip this retry exists for kills the turn before any tool runs, so it is unaffected. Regressions: `TestOpencodeAgent_RetryableFailureAfterToolActivityIsNotRetried`, `TestOpencodeAgent_RetryableFailureWithoutToolActivityStillRetries`, `TestOpencodeAgent_ToolPartInTheMessageResponseAlsoBlocksRetry`. +- The general `info.error` branch in `runOnceWithFormat` must stay AFTER the `isThinkingToolChoiceConflict` check: that conflict is the trigger for the prompt-only structured-output fallback, and a general failure claiming it first silently disables the fallback. +- That fallback is a second attempt in a fresh session too, and a `session.error` can arrive at any point in a turn, so it carries the same gate: `thinkingConflict` annotates the trigger with `errOpencodeToolsAlreadyRan` when the turn ran a tool and `runOnce` then reports the conflict instead of falling back. Regression: `TestOpencodeAgent_ThinkingConflictAfterToolActivityDoesNotFallBack`. +- `opencodeMessageError.rawData` keeps the verbatim `data` payload beside the typed `Data`, because thinking/tool_choice detection scans the payload as text (`providerText`) and a decode into the typed struct drops every provider-specific extra it has no field for. + **Documentation** - Keep `README.md` concise and high-level; the bar needs to be extremely high for what shows up there. @@ -48,12 +94,20 @@ Safest local verification sequence after non-trivial changes: - The shared default test-quality rule lives in `internal/testguidance`; render it only into the task-first skill and pipeline roles that can author, repair, or review tests. Its fake-agent prompt tests are the intentional generated-interface contract, not source-text checks. - Review auto-fix is disabled by default (`auto_fix.review: 0` in `config.go` `autoFixDefaults`), so blocking and ask-user review findings park for an agent decision; keep the skill, the live `axi` gate `note`, and docs qualified if you touch review auto-fix. +**Unified Agent Tuning (`internal/agentcfg`)** + +- `agentcfg` is the single owner of the harness-neutral model/effort surface and of the mapping down to each harness's native mechanism (claude/copilot `--effort`, codex `-m` + `-c model_reasoning_effort`, grok `--reasoning-effort`, pi `--thinking`, opencode's session-message `model`/`variant`, acpx `--model` for `cursor`/`acp:`). Add a harness there, not in an adapter or in eval. `rovodev` and `antigravity` are deliberately declared unmappable, so a request for them is a config error rather than a flag that is silently ignored. +- `agent.NewWithOptions` is the one funnel: it validates `Options.Profile` and splices the mapped args after the operator's raw `agent_args_override` args, so both the pipeline (`cfg.AgentProfileFor`) and eval replay (`Candidate.Profile()`) reach every harness by the same path. Never re-derive a model or effort flag at a call site. +- Precedence is fixed: a raw `agent_args_override` flag that already pins a knob natively wins and the mapped value is not emitted, which is what keeps every pre-`agent_config` configuration byte-identical and stops a harness receiving one knob twice. `agent_config` is global-only for the same reason as `agent_args_override`. +- Eval candidates are `agent,model=[,effort=]` (the previous `agent+model` spelling is refused with a migration message), effort is part of the persisted candidate identity, and `agentNeutralGlobalConfig` strips `agent`, `agent_args_override`, and `agent_config` so a replay never inherits the capturing machine's pins. +- Regressions: `internal/agentcfg`, `internal/agent/profile_test.go`, `internal/config/config_agent_config_test.go`, `internal/daemon/pipeline_agent_profile_test.go`, `TestParseCandidate*`, `TestReplayPinsCandidateModelAndEffortOnTheHarness`, `TestCaptureStripsEveryHarnessPinFromThePinnedConfig`. + **Context, Concurrency, and Processes** - Thread `context.Context` through long-running, subprocess, and networked work; prefer `exec.CommandContext`; use derived contexts and timeouts for cleanup and HTTP calls. - Route every long-lived subprocess spawned for a cancellable step or agent invocation through `shellenv.ConfigureShellCommand(cmd)`: it creates a process-tree boundary and installs `cmd.Cancel` to kill the whole tree, so grandchildren (test workers, build watchers) cannot outlive cancellation and hold the next run's worktree locked. - `cmd.Cancel` covers only cancellation; on clean exit or error the group is not reaped, and leaked grandchildren accumulate until the OS OOM-kills the daemon (surfacing as `daemon crashed during execution` with no stack trace). Use `shellenv.RunShellCommand` / `OutputShellCommand` / `CombinedOutputShellCommand` for one-shot commands, or `StartShellCommand` plus `TerminateShellCommandGroup` when handling pipes manually; the helper doc comments in `internal/shellenv` own the details. `ConfigureShellCommand` also installs a 5s `cmd.WaitDelay` backstop so a grandchild holding an inherited pipe cannot wedge `cmd.Wait` forever. Regressions: `TestCodexAgent_Run_ReapsLeakedGrandchildOnCleanExit`, `TestRunShellCommandWithEnv_ReapsGrandchildOnCleanExit`, `TestTerminateShellCommandGroup_*`. -- A process group is a lineage container, not a sandbox: a descendant that calls `setsid(2)`/`setpgid(2)` (agent CLIs sandboxing their tool runners, any daemonizing worker script) leaves the group, and after its parent exits nothing lineage-based can name it again - it burns CPU and holds a deleted worktree's cwd forever. `internal/procreap` is the identity-based backstop: it matches a process by the run worktree its **cwd** resolves under (deliberately never argv, which a legitimate `git worktree remove` also carries), never touches pid<=1/itself/its ancestors, spares worktrees whose run is still pending or running, and escalates SIGTERM to SIGKILL only after a grace period. Two call sites, both best effort: `recoverOnStartup` sweeps every inactive worktree older than `orphanProcessMinAge`, and `RunManager.sweepRunWorktreeProcesses` sweeps one finished run's worktree with no age floor, before the directory is removed. Windows needs none of this - job objects contain the whole tree - so the platform layer reports an empty table. Regressions: `internal/procreap`, `TestSweepOrphanRunProcessesReapsFinishedRunAndSparesActiveOne`, `TestSweepRunWorktreeProcessesReapsLeakedChildAtRunCleanup`, `TestTerminateShellCommandGroup_AsksBeforeKilling`, `TestTerminateShellCommandGroup_EscalatesWhenSIGTERMIsIgnored`. +- A process group is a lineage container, not a sandbox: a descendant that calls `setsid(2)`/`setpgid(2)` (agent CLIs sandboxing their tool runners, any daemonizing worker script) leaves the group, and after its parent exits nothing lineage-based can name it again - it burns CPU and holds a deleted worktree's cwd forever. `internal/procreap` is the identity-based backstop: it matches a process by the run worktree its **cwd** resolves under (deliberately never argv, which a legitimate `git worktree remove` also carries), never touches pid<=1/itself/its ancestors, spares worktrees whose run is still pending or running, and escalates SIGTERM to SIGKILL only after a grace period. Reach is `/worktrees` by path shape plus exactly the run worktrees a caller names from run records (`Options.Worktrees`), never a configured worktree root by shape - an operator's own directory is unmatchable unless a run row names it. Every site that removes a run worktree sweeps it first through `procreap.SweepRunWorktree(s)` (run cleanup and setup failure via `RunManager.removeRunWorktree`, startup cleanup, eject), scoped and therefore without age floor or run-active check; the unscoped startup sweep in `recoverOnStartup` keeps the `orphanProcessMinAge` floor. All best effort. Windows needs none of this - job objects contain the whole tree - so the platform layer reports an empty table. Regressions: `internal/procreap`, `TestSweepOrphanRunProcessesReapsFinishedRunAndSparesActiveOne`, `TestSweepRunWorktreeProcessesReapsLeakedChildAtRunCleanup`, `TestTerminateShellCommandGroup_AsksBeforeKilling`, `TestTerminateShellCommandGroup_EscalatesWhenSIGTERMIsIgnored`. - On Windows the daemon runs console-less, so route every console child through `winproc.Harden(cmd)` (no-op elsewhere, idempotent, preserves existing creation flags) or a console window flashes per child (#287). `shellenv.ConfigureShellCommand` already calls it; one-shot commands built directly must call it themselves. Regressions: `TestHarden*` in `internal/winproc`. - Protect shared mutable state with the standard sync/atomic tools, and be explicit about ownership and cleanup of goroutines, worktrees, temp dirs, and channels. @@ -65,7 +119,9 @@ Safest local verification sequence after non-trivial changes: **Filesystem and Paths** - Use `filepath.Join`; respect `NM_HOME` for app state; directories are `0o755` and files `0o644` by convention. -- On macOS, path comparisons may need symlink resolution (`/var` vs `/private/var`). +- On macOS, path comparisons may need symlink resolution (`/var` vs `/private/var`); use `worktrees.Canonical`/`worktrees.Contains` wherever run worktree paths are compared, so one spelling matches everywhere. +- Run worktree placement (`worktree_roots`) is owned by `internal/worktrees`. Configuration decides it exactly once, at run creation (`Layout.Dir` in `RunManager.startRunWithIntentSource`), and the result is persisted in `runs.worktree_dir`; every later consumer - resume, step diff, startup cleanup, `procreap`, eject, gatecontext attribution - must read it back through `worktrees.RecordedDir` and never re-derive it from config, so a mid-flight edit can neither strand a parked run nor point a removal at a directory the run never used. An empty column means the default `/worktrees//`. +- `worktrees.CheckPlacement` is the single policy for an unusable root (inside `NM_HOME`, inside any registered checkout); `config.ValidateWorktreeRoots` owns what the config can judge alone. The daemon refuses to start on an unusable placement, so `init --worktree-root` must refuse exactly the same set or it prints a paste that takes the operator's CLI down, and EVERY `init` refuses to register a checkout that contains a configured root - the same state reached from the other direction. User-facing semantics live in `docs/src/content/docs/reference/global-config.md`. Regressions: `internal/worktrees`, `internal/config/config_worktree_roots_test.go`, `internal/daemon/worktree_roots_test.go`, `internal/gate/eject_sweep_test.go`, `internal/cli/init_test.go`. **Git on Bare Gate Repos (`safe.bareRepository`)** @@ -90,7 +146,7 @@ Safest local verification sequence after non-trivial changes: - A successful stop means the daemon process is gone, not merely that IPC health has disappeared, because only process exit releases the singleton lock. Capture the daemon instance before requesting shutdown, and close the shutdown client before waiting because the daemon drains in-flight handlers during exit. See `waitForDaemonStop` and `stopDetachedDaemon`; regressions: e2e `TestDaemonStopLeavesNoDaemonProcessOwningTheRoot`, `TestDaemonRestartReplacesTheDaemonWithExactlyOneOwner`. - Independent layers: `internal/ipc` `listen()` dials the socket before unlinking it and refuses to steal a live one; client probes bound the dial with `daemon_connect_timeout` and fail fast on a dead or wedged socket instead of starting a replacement daemon (`EnsureDaemon` surfaces the error with a `daemon start` recovery hint; the health RPC itself is bounded separately by `ipc.DefaultDialTimeout`). - Daemon execution is explicit-only (`no-mistakes daemon run --root`); never let inherited environment reinterpret probes like `--version` or `status` as daemon workers. -- Startup worktree cleanup is DB-aware: never remove a worktree whose run row is `pending` or `running`; `startRun` inserts the run row before creating the worktree, so a no-row directory is safe to remove immediately. +- Startup worktree cleanup is DB-aware: never remove a worktree whose run row is `pending` or `running`; `startRun` inserts the run row before creating the worktree, so a no-row directory is safe to remove immediately. That no-row rule holds only inside `/worktrees`, which is discovered by walking because no-mistakes owns it; a configured worktree root is the operator's directory, so cleanup and eject there act on exactly the recorded run worktrees and never enumerate anything else. - The user-facing model lives in `docs/src/content/docs/concepts/daemon.md`; the lock rationale lives in the `internal/daemon/lock.go` and `daemon.go` comments. Regressions: `TestAcquireSingletonLock_*`, `TestRunWithResources_SecondDaemonForSameRootFailsWithoutStealingSocket`, `TestRunWithOptions_RequiresSingletonLockBeforeRecovery`, `TestRecoverOnStartup_DoesNotDeleteActiveRunWorktree`, `TestServe_SecondListenerForLiveSocketDoesNotStealIt`, `TestDialConnectTimeoutFailsFastAndNamesSocket`, `TestIsRunningFailsFastWhenSocketAcceptsButDoesNotRespond`, `TestIsRunningSurfacesExistingDeadSocket`, `TestDaemonRunRootFromArgs_EnvDoesNotForceDaemonModeForProbes`, `TestValidateDaemonPIDFallback_RefusesToKillOwnProcess`. **Bounded Daemon Logging and Event-Driven AXI Runs** @@ -130,24 +186,38 @@ Safest local verification sequence after non-trivial changes: - The daemon runs `commands.*` from `.no-mistakes.yaml` verbatim via `sh -c`, and `agent` selects which process launches with the maintainer's credentials. The code-executing selection fields (`commands.{test,lint,format}` and `agent`) are therefore loaded from the trusted default branch at a **pinned SHA** resolved by a fresh fetch, never from the pushed SHA. The run aborts when the trusted commit or its present config cannot be read and parsed; a readable tree with no config is valid. See `internal/daemon/manager.go` `startRun`, `loadTrustedRepoConfig`, and `assertGateTrustedConfigReadable`. - `document.instructions` (the repo's documentation placement policy), `review.path_instructions` (path-scoped review guidance appended to the review prompt), `disable_project_settings` (the gate-agent project-instruction opt-out), `no_ci` (positive declaration that the repository intentionally has no CI), and `ci.rerun_transient` (how many times a transiently failed check may be re-run) are also trusted-only, regardless of `allow_repo_commands`: a pushed branch must not weaken any of those boundaries, self-declare no-CI to bypass checks, or steer its own review; enabling the commands opt-in must not drop the maintainer's own trusted values; and every re-run `ci.rerun_transient` authorizes bills another provider-side workflow run to the repository, so a contributor must not be able to raise it (the operator's own global `ci.rerun_transient` is a separate, non-contributor surface that the trusted repo value still overrides). When the opt-out is enabled, only adapters with verified effective suppression may launch. Other non-executing fields (`ignore_patterns`, `auto_fix`, `commit`, `intent`, `test`) are still read from the pushed branch. - Selecting which trusted config applies to a run must never depend on a pushed-branch field. `review.path_instructions` is matched against the COMPLETE changed-file set, never the `ignore_patterns`-filtered subset, because filtering there lets a contributor suppress a maintainer's rule from their own review by ignoring its glob. `reviewablePaths` (`internal/pipeline/steps/common_diff.go`) answers only "does this run have anything to work on". +- `pr.base_branch` (the PR, rebase, and CI-merge-conflict-auto-fix integration branch, falling back to `Repo.DefaultBranch` when unset) is trusted-default-branch-only, but unlike the fields in the bullet above it is the deliberate exception that also honors the `allow_repo_commands: true` opt-in, since it controls where an already-maintainer-authorized PR lands rather than what executes. Once a PR already exists, its actual forge base branch (read live via `scm.PRBaseBranchReader`) is authoritative for CI merge-conflict repair and base-branch tip monitoring over a since-changed `pr.base_branch`, and PR lookup matches the existing PR by branch alone, never filtered by base, so a later config change updates that PR instead of opening a duplicate against the new base. Full semantics are owned by `docs/src/content/docs/reference/repo-config.md` (`pr.base_branch`). Regressions: `TestEffectiveRepoConfig_PRBaseBranchTrustedOnly`, `TestEffectiveRepoConfig_PRBaseBranchOptInUsesPushedValue`, `TestEffectiveRepoConfig_PRBaseBranchOptInWithNoTrustedCopyUsesPushedValue`, `TestLoadRepoConfig_PRBaseBranchRejectsInvalidBranchName`, `TestLoadRepoConfig_PRBaseBranchEmptyIsValid`, `TestPRStep_UsesConfiguredBaseBranch`, `TestRebaseStep_UsesConfiguredPRBaseBranch`, `TestCIStep_AutoFixUsesExistingPRBaseAfterConfigChanges`, `TestPRStep_ExistingPRAgainstDifferentBaseIsUpdatedNotDuplicated`. - `allow_repo_commands` is per-repo, read only from the trusted default-branch copy, and defaults `false`; a contributor cannot self-enable it from a pushed branch. The e2e harness models a trusted single-developer environment and commits `allow_repo_commands: true` via `SetupOpts.AllowRepoCommands`; security tests pass `false`. - Regressions: `TestLoadTrustedRepoConfig_FailClosedOnFetchFailure`, `TestLoadTrustedRepoConfig_PinnedSHAReadsFreshDefaultBranch`, `TestEffectiveRepoConfig_DocumentPolicyTrustedOnly`, `TestEffectiveRepoConfig_ReviewPathInstructionsTrustedOnly`, `TestMatchPathInstructions_PushedIgnorePatternsCannotSuppressTrustedRule`, `TestReviewStep_PushedIgnorePatternsCannotSuppressPathInstructions`, `TestEffectiveRepoConfig_DisableProjectSettingsTrustedOnly`, `TestEffectiveRepoConfig_CIRerunTransientTrustedOnly`, `TestAssertGateTrustedConfigReadable_*`, `TestNewPipelineAgent_OptOut_*`, `TestLoadRecoveredConfig_BoundsFetchAndFailsClosed`, e2e `TestRepoConfigCommandsFromDefaultBranch` (incl. `pushed_branch_cannot_self_enable`), e2e `TestReviewPathInstructionsJourney`. **CI Monitor Lifecycle** - `ci_timeout` is an idle timeout, not an absolute deadline: only `timeoutAnchor` re-arms when the upstream default-branch tip advances, `started` stays fixed for poll pacing, and re-arm only ever extends the deadline (fail-safe on transient base-tip failures). Value semantics (`0` unset, negative unlimited sentinel, keyword parsing) live in `config.go`; keep `config.DefaultCITimeout` and `defaultConfigYAML` in sync (`TestDefaultConfigYAML_MatchesGoDefaults`). User-facing semantics are owned by `docs/src/content/docs/reference/global-config.md`. +- GitHub readiness is the union of the exact current PR head commit's check rollup and every Actions workflow run returned by the Actions API for that same SHA. A workflow rejected before creating jobs/check-runs is absent from the commit rollup but still present in that API; run discovery errors and unknown run states fail closed instead of certifying a green rollup. Regressions: `TestGetChecksIncludesFailedWorkflowRunMissingFromPRRollup`, `TestGetChecksBindsRollupAcrossABAHeadMovement`, `TestCIStep_FailedHeadWorkflowRunPreventsChecksPassed`. +- GitHub's raw commit `statusCheckRollup` returns every check run a commit ever had, including a same-named run a later run at that same head has already superseded (e.g. a required-check job re-triggered by `synchronize` after a pipeline auto-fix push). `Host.GetChecks` (`internal/scm/github/github.go`) collapses same-name reruns of one workflow to the newest `startedAt` (using Actions run identity to break timestamp ties, comparing against every retained duplicate, and preserving unordered records, including ambiguous pending replacements, before a `completedAt` fallback) while preserving independent workflows, unidentified external checks, and same-named commit status contexts, restoring the semantics `gh pr checks` already applies and this repo's own gate `verify.py` already assumes (last-wins). Collapse must run AFTER `appendUnrepresentedWorkflowRuns`, never before: that call dedupes the Actions-run union against checks by run ID over the FULL uncollapsed rollup, so collapsing first drops a superseded run's ID out of the "represented" set and the union re-adds the same stale run under its own workflow run name. Without this collapse, a monitor that keeps `synchronize` in its gate trigger set can loop forever: each auto-fix push legitimately fails the gate at the old head, a later same-head rebind turns it green, but the stale FAILURE stays visible and re-triggers another auto-fix round. Regressions: `TestGetChecksCollapsesSupersededSameNameCheckToLatestAtOneHead`, `TestGetChecksCollapseOrderingDoesNotLetWorkflowRunUnionResurrectSupersededCheck`, `TestGetChecksPreservesIndependentSameNameWorkflows`, `TestGetChecksPreservesIndependentSameNameExternalCheckRuns`, `TestGetChecksCollapseComparesNewestRunWithEverySameNameCandidate`, `TestGetChecksKeepsQueuedReplacementWithEqualStartTime`, `TestGetChecksPreservesUnorderedExternalPendingReplacement`, `TestGetChecksPreservesSameNameStatusContextAndCheckRun`. - CI readiness never treats an unproven empty forge check list as green. Ready requires observed all-green checks, or trusted default-branch `no_ci: true` with zero registered checks (`internal/pipeline/steps/ci.go` decides whether the declaration applies; `internal/cimonitor` owns the agent-facing log vocabulary and Ready/DeclaredNoCI parse). Delayed registration, pending checks, failures, errors, unknowns, and stale-head evidence stay not-ready; registered checks on a declared no-CI repo are still honored. Regressions: `TestChecksPassed_PR607RealLogSequence`, `TestCIStep_EmptyChecksWithoutNoCIStaysNotReadyPastOldGracePeriod`, `TestCIStep_EmptyChecksWithTrustedNoCIBecomesReady`, `TestCIStep_DelayedCheckRegistrationStaysNotReadyUntilGreen`, `TestCIStep_DeclaredNoCIWithUnexpectedChecksHonorsThem`, `TestEffectiveRepoConfig_NoCITrustedOnly`. +- Persistent provider check-read failures are not an invisible spin: 6 consecutive `GetChecks` errors while the PR is still open park the CI step at an ask-user gate whose provider-neutral finding names the provider CLI/credentials support and includes the underlying error (with a GitHub-specific clause requiring `gh` >= 2.50 for the `gh pr checks --json` call), and the streak resets on any successful read. Regression: `TestCIStep_PersistentCheckReadFailureParksAtAskUser`. - Reap an orphaned monitor from outside its worktree with `no-mistakes axi abort --run `; it needs only `NM_HOME` and never starts a stopped daemon. A known run succeeds only with durable terminal truth, a recorded nonterminal run fails unconfirmed, and only an unknown id is an idempotent no-op. Bare `axi abort` stays worktree/branch-scoped. - A merged or closed PR observation transactionally completes an active run and its CI step; PR lifecycle state is monotonic, so duplicate or delayed observations cannot reactivate or regress a terminal run. Startup reconciles legacy `pending` or `running` rows that already hold terminal PR state before parked-run planning and generic crash recovery. Regressions: `TestUpdateRunPRStateFinalizesActiveTerminalOutcomes`, `TestUpdateRunPRStateIgnoresDuplicateAndDelayedRegressions`, `TestReconcileTerminalPRRunsFinalizesLegacyActiveRows`, `TestRecoverOnStartup_FinalizesLegacyTerminalPRRun`, e2e `TestTerminalPRRunDisappearsFromActiveListing`. -- A provider-reported `cancelled` check is never a job verdict, so the deterministic rerun runs strictly before any CI fix round: it is the only outcome that earns a rerun, a check cancelled again after its budget parks as `ask-user` instead of entering the `auto_fix.ci` loop, and any genuine or unrecognized failure or merge conflict in the same poll suppresses reruns so real failures still escalate on their first observation. The budget is per check name per run and spent on request rather than on success, and a rerun is never issued once the published branch head no longer equals `runs.head_sha`, because it would certify a commit this run never delivered. Each outstanding rerun records its verified pipeline head and the same-name provider links visible when it was requested; it retires durably when the run head advances or a new conclusive non-cancel link appears. Retirement keeps the spent budget and never changes check buckets. A delayed same-named green sibling can satisfy the link trigger on the same head, matching the default branch's existing name-keyed masking; removing that limitation requires provider truth outside this policy. Classification, the deliberate `TIMED_OUT`/`STALE` exclusions, rollup-lag grace, and retirement live in `internal/pipeline/steps/ci_transient.go`; provider support is the optional `scm.CheckRerunner` (GitHub only) and user-facing semantics are owned by `docs/src/content/docs/reference/repo-config.md`. Regressions: `TestCIStep_CancelledCheckIsRerunBeforeEscalating`, `TestCIStep_CancelledCheckStaysUnresolvedAfterItsBudget`, `TestCIStep_LaggingRerunRollupKeepsWaitingForTheRepublishedCheck`, `TestCIStep_SameHeadGreenRerunEmitsChecksPassed`, `TestCIStep_DelayedSameNameCheckRetainsLegacyNameBehavior`, `TestCIStep_ResolvedRerunDoesNotParkALaterGreenHead`, `TestRetireResolvedReruns`, `TestRetireResolvedRerunsRetriesAfterPersistenceFailure`, `TestCIStep_MovedPublishedHeadTerminatesInsteadOfRerunning`, `TestCIStep_MovedPublishedHeadClearsCIReadiness`, `TestClassifyCheckFailure`. +- A provider-reported `cancelled` check is never a job verdict, so the deterministic rerun runs strictly before any CI fix round: cancellation and a GitHub pre-run infrastructure failure (a job that failed in setup/action resolution before any repository step ran, flagged `Check.PreRunFailure` by the opt-in `scm.PreRunFailureDetector` and re-bucketed to cancel so it never masks a real test/lint failure, which cleared setup and failed a later step) are the outcomes that earn a rerun, either outcome after its budget parks as `ask-user` instead of entering the `auto_fix.ci` loop, and any genuine or unrecognized failure or merge conflict in the same poll suppresses reruns so real failures still escalate on their first observation. The budget is per check name per run and spent on request rather than on success, and a rerun is never issued once the published branch head no longer equals `runs.head_sha`, because it would certify a commit this run never delivered. Each outstanding rerun records its verified pipeline head and the same-name provider links visible when it was requested; it retires durably when the run head advances or a new conclusive non-cancel link appears. Retirement keeps the spent budget and never changes check buckets. A delayed same-named green sibling can satisfy the link trigger on the same head, matching the default branch's existing name-keyed masking; removing that limitation requires provider truth outside this policy. Classification, the deliberate `TIMED_OUT`/`STALE` exclusions, rollup-lag grace, and retirement live in `internal/pipeline/steps/ci_transient.go`; provider support is the optional `scm.CheckRerunner` (GitHub only), pre-run infrastructure detection is the optional `scm.PreRunFailureDetector` (GitHub only, folded in by `markPreRunInfraFailures`), and user-facing semantics are owned by `docs/src/content/docs/reference/repo-config.md`. Regressions: `TestCIStep_CancelledCheckIsRerunBeforeEscalating`, `TestCIStep_CancelledCheckStaysUnresolvedAfterItsBudget`, `TestCIStep_LaggingRerunRollupKeepsWaitingForTheRepublishedCheck`, `TestCIStep_SameHeadGreenRerunEmitsChecksPassed`, `TestCIStep_DelayedSameNameCheckRetainsLegacyNameBehavior`, `TestCIStep_ResolvedRerunDoesNotParkALaterGreenHead`, `TestRetireResolvedReruns`, `TestRetireResolvedRerunsRetriesAfterPersistenceFailure`, `TestCIStep_MovedPublishedHeadTerminatesInsteadOfRerunning`, `TestCIStep_MovedPublishedHeadClearsCIReadiness`, `TestClassifyCheckFailure`, `TestMarkPreRunInfraFailures_RetriesInfraButNotGenuine`, `TestMarkPreRunInfraFailures_OptInGated`, `TestPreRunFailures_FlagsSetupFailureNotGenuine`, `TestPreRunFailures_FailsClosedOnUnreadableRun`. - **Terminal is not pending.** Readiness must reject every non pass/fail/skip bucket (`hasUnresolvedChecks`), but only checks that can still finish on their own (`hasPendingChecks`) may keep the monitor polling. A `cancel` bucket - GitHub `CANCELLED`, GitLab `canceled`, Bitbucket `STOPPED`, and how GitHub reports a job killed by its own `timeout-minutes` - is a published conclusion that nothing will replace, so with no rerun outstanding it parks at `ciUnresolvedCancelledOutcome` (`cancelledWithoutRerun`) instead of waiting. Conflating the two is the #628 regression that hung real runs for their whole `ci_timeout`; an unrecognized bucket is deliberately still treated as waiting, because unknown is not evidence of terminal. Regressions: `TestCIStep_CancelledCheckAmongPassingChecksEscalatesInsteadOfPollingForever`, `TestCIStep_ZeroRerunBudgetEscalatesCancelledCheckWithoutMakingItReady`, `TestCIStep_BitbucketStoppedCheckParksForADecision`. -- CI readiness is read from the provider's live PR check rollup on every poll, so it always describes the head the forge currently has for that PR; no recorded SHA gates it, and a run whose row still names a pre-advance commit must still recognize green at the head the pipeline last pushed. Regression: `TestCIStep_GreenChecksAtAdvancedHeadAreRecognizedWhileRunTracksOlderHead`. +- CI readiness is read from the provider's live PR head check rollup on every poll, so it always describes the head the forge currently has for that PR; no recorded SHA gates it, and a run whose row still names a pre-advance commit must still recognize green at the head the pipeline last pushed. Regression: `TestCIStep_GreenChecksAtAdvancedHeadAreRecognizedWhileRunTracksOlderHead`. **Parked / Awaiting-Agent Signal** - `runs.awaiting_agent_since` is non-nil **iff** a step is actually parked at an `awaiting_approval`/`fix_review` gate: the executor sets it on gate entry, clears it when `waitForApproval` returns, and `RecoverStaleRuns` clears it on crash recovery. It is observability only (rendered as `awaiting_agent: parked ` in `axi status`) and never changes gate resolution, auto-resume, or the `--yes` default. - Tests: `internal/db/run_test.go`, `internal/pipeline/executor_approval_test.go`, `internal/cli/axi_test.go`, e2e `TestAxiParkedAwaitingAgentSignal`. +**AXI Run Resolution Is Branch-Scoped (`resolveRun`)** + +- `axi status` and `axi logs` resolve an implicit run from the caller's current branch only: its active run, else its most recent one, else nothing. +There is deliberately no repo-wide "most recent run" fallback - one clone routinely has several worktrees on different branches, and that fallback reported one worktree's terminal run to every other worktree under the same `run:` key a run of the caller's own gets, which reads as "your work failed" while the real pipeline is mid-flight. +A detached HEAD owns no branch, so it resolves nothing either. +- Deliberate cross-branch inspection is `--run `. When the caller's branch is known and differs, `axi status` renders the selected run under `other_branch_run:` plus a top-level `current_branch:` - reusing the home view's `other_branch_active_run` vocabulary - so a consumer keyed on `run:` cannot pick up a run proven to be on another branch. An explicit run selected while the caller's branch is unknown stays under `run:` because no branch relationship is proven. +- Labels and commands use separate evidence rules: branch-scoped `axi respond` help is safe only for implicit current-branch resolution. Every explicit `--run ` selection is observation-only, even when its branch matches the caller's, because a newer active run on that branch could receive the bare response command instead; its gate stays visible and every log command retains `--run `. +The rationale lives in the `resolveRun` doc comment and the status-rendering comments (`internal/cli/axi_query.go`). +- Regressions: `internal/cli/axi_status_branch_test.go` covers branch-scoped resolution, positive-evidence labels, observation-only gate help, branch lookup failures, detached-HEAD help, snapshot consistency, explicit-ID errors, and run-scoped log continuations; `TestAxiStatusUnknownBranchRunRelationshipGuidance_InInstalledSkill` pins the published relationship wording. + **Review-Loop Agent Sessions (`internal/pipeline/sessions.go`)** - Per run, the review loop keeps ONE durable fixer session across review-fix turns, and EVERY review turn (initial review and every full rereview) runs session-free. A rereview certifies fixes implementing the previous review turn's findings, so resuming any review session seats the prescriber as certifier - the mechanism that let one fix round ship wrong code plus the test blessing it with zero findings. Cross-round review context travels only in the explicit sanitized round history; the fixer session is never lent to review turns, no other step uses sessions, and sessions are keyed strictly by run. The rereview prompt reframes fix-round changes as pipeline-authored code under the author-grade adversarial standard (`fixRoundProvenanceClause`); the same clause is emitted on a later run's initial review when a persisted uncertified range is bound. Prior findings, fix summaries, and same-round tests are claims, not evidence. @@ -155,6 +225,14 @@ Safest local verification sequence after non-trivial changes: - `codex exec resume` has a narrower flag surface than `codex exec`, so an unsupported override fails the resume and falls back; the e2e fakeagent must keep parsing both codex argv shapes (`extractCodexPrompt`). - Regressions: `internal/pipeline/sessions_test.go`, `internal/pipeline/steps/review_session_test.go` (incl. `TestReviewLoop_RereviewNeverResumesTheSessionThatPrescribedItsFixes`), `TestReviewStep_RereviewTreatsFixRoundsAsPipelineAuthoredCode`, `internal/agent/session_test.go`. +**Recorded Human Decisions on Findings** + +- Approve, skip, and abort each record `selected_finding_ids = "[]"` plus `selection_source = user_declined` on a gated round with findings (`executor.go` `recordDeclinedRound`, `db.SetStepRoundDeclined`); a round with no findings records no decision. The conditional write must never erase an existing selection. User-facing semantics are owned by `docs/src/content/docs/reference/pipeline-steps.md`. +- A decline is stored as the COMPLEMENT of the selection, never as its own list; `declinedFindingLines` derives it and deliberately excludes `auto_fix` selections, whose complement is findings still awaiting a decision (rendered under `auto_fix_left_unselected`, which carries no do-not-re-report instruction). +- `roundHistoryPromptSection` (`internal/pipeline/steps/round_history.go`) now carries three parts: this step's rounds, this run's OTHER steps' decisions, and earlier runs' decisions on this branch (bound per step by `pipeline.BindBranchDecisions`, unlike review-only `BindUncertifiedPipelineRange`). Nothing clears branch decisions - a completed review deletes the uncertified range, which is why that channel could not carry a decision forward, but approving a gate IS the decision. The prompt states that a recorded decision SUPERSEDES the user-intent wording. +- Deliberately ADVISORY and fail-open: no step is blocked and no commit is gated, so an agent may still re-raise a declined finding when the code genuinely changed. There is no reversion detector; `assertPipelineHeadContinuity` and `assertReviewApprovedPushHead` remain lineage-only. `ci_fix.go` and `rebase.go` build prompts without `roundHistoryPromptSection`, so they do not receive decisions. +- Regressions: `TestExecutor_GateResolutionsWithoutASelectionRecordTheDecline`, `TestExecutor_GateResolutionWithNoFindingsRecordsNoDecision`, `TestExecutor_FixResolutionStillRecordsAUserSelection`, `internal/db/round_decisions_test.go`, `TestDeclinedFindingReachesALaterStepInTheSameRun`, `TestDeclinedFindingReachesALaterRunOnTheSameBranch`, `TestCompletedReviewDoesNotClearBranchDecisions`, `TestAutoFixComplementIsNeverPresentedAsAUserDecision`. + **Uncertified Review Provenance (`internal/pipeline/uncertified.go`)** - When a review-step fixer round commits and its re-review does not complete, persist the per-branch uncertified range (`from_sha`, `to_sha`). Persist on review-step fixer commits only, not lint or document. On the next run's initial review, bind that range and emit `fixRoundProvenanceClause` even when `Fixing==false`, so the replacement reviewer is not cold. Rerun proceeds; there is no refusal or `--ack-uncertified-review` gate. @@ -175,7 +253,9 @@ Safest local verification sequence after non-trivial changes: `commands.test` is the same contract when set: targeted baseline, not CI-parity complete-suite configuration; docs owner is `docs/src/content/docs/reference/repo-config.md` (`commands.test`), step behavior owner is `docs/src/content/docs/reference/pipeline-steps.md` (Test). This repository dogfoods an empty `commands.test` so the agent-driven targeted path is the default; do not reintroduce `go test -race ./...` as a local Test override. Process-group reaping on clean/error exit (#357) and Unix WaitDelay remain the lifecycle safety net when agents spawn test workers - restoring the agent-driven path must not revive the daemon OOM leak. - Regressions: `TestTestStep_InitialAgent_TargetedValidationContract`, `TestTestStep_FixMode_TargetedVerificationContract`, `TestTestStep_FixMode_DriverFullSuiteInstructionDoesNotOverrideContract`, `TestTestStep_InitialAgent_NoTargetedEvidenceRequiresHonestFinding`, `TestDogfoodConfig_NoBroadLocalTestCommand`, `TestCIWorkflow_RetainsFullRaceSuiteAsBroadRegressionOwner`, plus the existing #357 reap/WaitDelay tests. + Those agent turns are bounded by `test_agent_timeout` (default 30m, global-only): a stalled evidence or repair agent is cancelled and the run fails instead of waiting forever. Native adapters already honor that deadline through `CommandContext`; the missing piece was the Test step never setting one. Docs owner is `docs/src/content/docs/reference/global-config.md`. + Every other pipeline agent invocation is bounded by `agent_timeout` (default 30m, global-only) at `pipeline.RunAgent` / the executor `timeoutAgent` seam, so a new agent-spawning step cannot hang a run by forgetting a deadline. Review keeps `review_agent_timeout` as a per-round budget; an existing sooner deadline is honored rather than capped. The invocation context is scoped only to `Agent.Run`; a late successful return after the deadline is rejected. Docs owner is `docs/src/content/docs/reference/global-config.md`. + Regressions: `TestTestStep_InitialAgent_TargetedValidationContract`, `TestTestStep_FixMode_TargetedVerificationContract`, `TestTestStep_FixMode_DriverFullSuiteInstructionDoesNotOverrideContract`, `TestTestStep_InitialAgent_NoTargetedEvidenceRequiresHonestFinding`, `TestTestStep_HangingEvidenceAgentFailsRunAfterTimeout`, `TestCodexAgent_RunCancelsSilentHang`, `TestDogfoodConfig_NoBroadLocalTestCommand`, `TestCIWorkflow_RetainsFullRaceSuiteAsBroadRegressionOwner`, plus the existing #357 reap/WaitDelay tests, `TestRunAgent_*`, `TestExecutor_DirectAgentRunIsDeadlineBounded`, `TestDocumentStep_HangingAgentFailsRunAfterTimeout`, `TestLintStep_HangingAgentFailsRunAfterTimeout`, `TestCIStep_HangingFixAgentFailsAfterTimeout`, `TestRebaseStep_HangingConflictAgentFailsAfterTimeout`. **Intent Provenance & Conformance (`internal/pipeline/steps/intent_prompt.go`)** @@ -212,10 +292,11 @@ Safest local verification sequence after non-trivial changes: - Collection is automatic and default-on through `eval.capture_provenance` / `eval.auto_capture` / `eval.max_cases` / `eval.diversified_size` in `config.yaml`, never an environment variable: the daemon's launchd/systemd unit is re-rendered on install and update and preserves only proxy variables (`internal/daemon/service.go` `proxyEnvKeys`), so an env-gated corpus silently stops collecting after an update. The keys are global-only - `Merge` copies them straight from `GlobalConfig`, and an `eval` block in a repo's `.no-mistakes.yaml` is ignored. - Provenance is unrecoverable: `executor.go` writes it with the review round or never. A round recorded with `capture_provenance` off can never be captured, so the rejection names the setting rather than the round's age. - The trigger is `RunManager.autoCaptureEvalCase`, called last in the run goroutine after the outcome is already reported: it recovers its own panic (the enclosing recover would otherwise mark a finished run failed), bounds itself with `evalAutoCaptureTimeout` off the run context, serializes runs on `evalCaptureMu` (shared pool + registry), and logs rather than propagates. `ErrNoCapturableReview` separates "nothing to freeze" (DEBUG) from a real fault (WARN). Automatic and manual capture call the same `eval.Capture`. A merged PR also best-effort relabels already-captured cases via `RunManager.relabelEvalRun` (same mutex/timeout); `eval relabel` is the CLI path. -- The unit of truth is finding-level gold, not park/pass, and it is keyed on the round's **recorded fix-vs-skip decision** plus merge state, never on whether a later round still raises the finding (a fix and a ship both make it disappear): a user-selected Fix is true-positive gold (no merge required); an auto-fix selection on a merged run is true-positive gold even if a later round re-raised or rewrote it; a raised `auto-fix`/`ask-user` finding the human did NOT select, on a merged run, is false-positive gold - deliberately reversing the older "never auto-FP from a skip" stance, because in this operator's corpus an approved-and-shipped finding IS a false positive; a human-added finding is false-negative gold; skip/approve without a merge and any round with no recorded decision (unknown/aborted) stay unlabeled / pending; `no-op` findings are never labeled; unmatched candidate findings stay queued - never inferred as false positives - and a confirmed post-PR miss ingested via `eval miss ingest` is also false-negative gold (`recorded-post-pr-miss`). Owner: `internal/eval` (`goldFromRound`, `hasRecordedDecision`, `IngestPostPRMiss`, `ScoreCandidate`); user-facing language is `docs/src/content/docs/reference/eval.md`. +- The unit of truth is finding-level gold, not park/pass, and it is keyed on the round's **recorded fix-vs-skip decision** plus merge state, never on whether a later round still raises the finding (a fix and a ship both make it disappear): a user-selected Fix is true-positive gold (no merge required); an auto-fix selection on a merged run is true-positive gold even if a later round re-raised or rewrote it; a raised `auto-fix`/`ask-user` finding the human did NOT select, on a merged run, is false-positive gold - deliberately reversing the older "never auto-FP from a skip" stance, because in this operator's corpus an approved-and-shipped finding IS a false positive; a human-added finding is false-negative gold; skip/approve/abort without a merge and any round with no recorded decision stay unlabeled / pending; `no-op` findings are never labeled; unmatched candidate findings stay queued - never inferred as false positives - and a confirmed post-PR miss ingested via `eval miss ingest` is also false-negative gold (`recorded-post-pr-miss`). Owner: `internal/eval` (`goldFromRound`, `hasRecordedDecision`, `IngestPostPRMiss`, `ScoreCandidate`); user-facing language is `docs/src/content/docs/reference/eval.md`. - `diversified` is gold-only and pinned (empty gold -> empty set + `eval sets` warning, never unlabeled fill). Those pins are the held-out official set; leftover labeled cases are `tune`. ListCases trims pins to the live `eval.diversified_size` cap (at most one per stratum when reconciling to 0 or a lower cap); `RefreshDiversified` is only for an explicit rebuild. Never fit matcher thresholds or review prompts on `diversified`. Report F1 as the headline metric only when false-positive gold exists; otherwise recall + precision bounds. RelabelRun recomputes derived merge labels and drops the obsolete ones. Matcher assignment is ONE globally optimal bipartite matching over all gold and candidate findings, weighted so an exact match outweighs any number of fuzzy ones; per-strength-tier greedy assignment understated recall and must not come back. Regressions: `TestListCasesDiversified_*`, `TestGoldFromRoundLabelsByRecordedDecision`, `TestCaptureWritesAutoFixMergedAsTruePositive`, `TestCaptureWritesShippedUnfixedAsFalsePositive`, `TestCaptureWritesShippedUnfixedEvenWhenTheFinalRoundNoLongerRaisesIt`, `TestCaptureLabelsSelectedAutoFixAsTruePositiveEvenWhenLaterRoundReRaisesIt`, `TestRelabelReplacesShippedUnfixedWhenTheRoundLaterRecordsAFixDecision`, `TestMergeGoldClearsStoredShippedUnfixedWhenRecomputedUnlabeled`, `TestRelabelClearsStoredShippedUnfixedFPWhenRecomputedUnlabeled`, `TestScoreCandidateDoesNotLetFuzzyEarlierGoldStealExactLaterMatch`, `TestScoreCandidateRecoversMatchTheTieredMatcherLost`, `TestMaxWeightAssignmentMatchesBruteForceOptimum`, `TestEvaluationSummaryWithholdsHeadlineF1WithoutFalsePositiveGold`, `TestCaptureDoesNotLabelSkipOrApproveAsPass`, `TestCaptureWritesFalseNegativeGoldForUserAddedFinding`, `TestCaptureSkipsIncompleteReviewRoundAndKeepsCompletedSibling`, `TestIngestPostPRMissWritesFalseNegativeGoldOnGreenReview`, `TestCaptureAndReport*`, CLI `TestEvalCaptureAndSetsSpeakInFindingGoldTerms`, `TestEvalMissIngestLabelsFalseNegativeGold`. - A case stores no Git bundle. Bundles were a full history copy per review pass (~8 MB each here) and cannot be trimmed, because a bundle built with negative refs records prerequisites an empty restore gate lacks. Cases of one repository instead share `/eval/pools/.git`, pinned by `refs/no-mistakes/eval//{head,source-head,base,trusted-config}`; the marginal case costs ~8 KB. `Store.Prune` applies `max_cases` oldest-first but protects active replay reservations and cases with recorded evaluations, so the cap is a retention target rather than a hard bound. - Capture stays read-only against the gate, so objects reach the pool through a throwaway bare clone plus a refspec fetch - never a bare-object-id fetch, whose want policy is off by default and version-dependent. +- Every eval subcommand is idempotent and tested so (`internal/eval/idempotency_test.go`, CLI `TestEvalCaptureSetsReportAndRelabelAreIdempotentAtTheCLI`): capture/relabel converge in place, sets reads self-stabilize their pins, and replay is additive-by-cohort but never rewrites case labels or manifests - queued unmatched-finding counts derive from the evaluations table (`Store.pendingFindingCounts`), never from a stored counter. The `eval sets` and `eval run` dashboards render in `internal/cli/eval_render.go`, sharing the stats box idioms (`renderTitledBox`); the diversified headline's instant self-score is `SelfScoreRecordedReviews` scoring each case's recorded review against its own gold. - Regressions: `TestCaptureDoesNotCopyRepositoryHistoryPerCase`, `TestPruneBoundsTheCorpusOldestFirstAndKeepsEvaluatedCases`, `TestDropCaseObjectsReleasesOnlyItsOwnPins`, `TestAutoCaptureEvalCase*` (`internal/daemon`), `TestEvalDefaultsCollectWithoutSetup`, `TestRepoConfigCannotChangeEvalCollection`, e2e `TestEvalAutoCaptureJourney`. **Telemetry Shape** @@ -227,13 +308,14 @@ Safest local verification sequence after non-trivial changes: **Guarded Local Branch Synchronization (`internal/branchsync`)** - `sync`, `axi sync`, and the TUI `u` action share one service whose only ordinary worktree mutation is a clean guarded move to an exact freshly verified pipeline push binding: strict fast-forward for behind branches, or an anchored reset to an equivalent diverged pipeline head when local unique work is already represented there. Under `--recover`, the worktree can only strict-fast-forward to the gate-preserved head, or adopt a diverged preserved head that `preservedContainsLocalWork` proves carries every local change. Passive status never fetches, and blocked states never reset, stash, merge, rebase, force, switch, delete, or update an external remote. +- Give each network remote operation its own bounded child context derived from the caller: `Refresh` must not share one deadline across sequential `git.LsRemote` and `git.FetchRemoteBranchToPrivateRef` calls, and `Apply` uses the same per-operation budget for its final live check. The per-operation budget is `Service.RemoteTimeout`, sourced only from the operator's global `branch_sync_remote_timeout` setting (default `config.DefaultBranchSyncRemoteTimeout`, 60s); `RepoConfig` deliberately has no matching field. `Recover`'s local-gate fetch is outside this network deadline contract. Regressions: `TestRefreshSlowSuccessfulLsRemoteDoesNotStealFetchBudget`, `TestRefreshSlowButSuccessfulLsRemoteAloneExceedsItsOwnBudgetReportsOffline`, `TestRefreshRaisedRemoteTimeoutAcceptsTheSameLegitimateSlowLsRemote`, `TestRefreshParentCancellationStopsFetchAfterLsRemoteSucceeds`, `TestServiceRemoteTimeoutDefaultsToConfigDefault`, `TestLoadGlobal_InvalidBranchSyncRemoteTimeout`, `TestLoadRepo_BranchSyncRemoteTimeoutIsNotARepoSetting`. - Successful pipeline pushes persist the exact SHA, credential-free target fingerprint/ref, and generation; legacy rows remain nullable and must never infer provenance from mutable `head_sha`. Structured PR lifecycle retires merged/closed branches. The service rechecks the invoking worktree, target, live remote equality, ancestry or equivalent-divergence proof, generation, and all mutable assumptions immediately before apply. -- A TERMINAL run with unpublished pipeline commits (moved head) is recoverable, never a dead end: inspection reports `blocked_pipeline_owned_recoverable` + `next_action recover_custody` with the exact submitted/current-head and relation facts (active runs keep the plain block), and `sync --recover` anchors the preserved head at `refs/no-mistakes/recover/` before stamping `runs.custody_returned_at`. +- A TERMINAL run with unpublished pipeline commits (moved head) is recoverable only from verified, non-conflicting evidence: inspection and `Recover` share one eligibility model. Equal/ahead local ancestry can create the local anchor without requiring gate access, but available gate evidence must agree; importing a missing preserved head requires exact or safely anchorable gate evidence, a clean worktree, and either ancestry or the content-preservation proof below. Only then does inspection report `blocked_pipeline_owned_recoverable` + `next_action recover_custody` with the exact submitted/current-head and relation facts (active runs keep the plain block). Missing, non-commit, symbolic, or conflicting evidence, and import cases that are dirty or genuinely divergent, fail closed with manual reconciliation instead. `sync --recover` anchors the preserved head at `refs/no-mistakes/recover/` before stamping `runs.custody_returned_at`. Cancellation RELEASES a terminal run that never changed the submitted head (`head_sha == submitted_head_sha`, no push, no custody stamp): selection keeps it visible so it never misreports as `blocked_wrong_branch`, and it classifies `user_owned` - no `next_action`, non-blocking exit, never represented as recoverable custody, `--recover` there is an idempotent no-op that mutates nothing, and a fresh `axi run` or separately authorized direct push is never blocked. - Equal/ahead worktrees anchor locally without gate access; behind/diverged worktrees verify and fetch the preserved head from the gate branch, fast-forwarding only a clean behind worktree. + Equal/ahead worktrees anchor locally without requiring gate access, but an available gate's existing recovery ref must agree with the recorded head; behind/diverged worktrees verify and fetch the preserved head from the run-specific recovery ref, fast-forwarding only a clean behind worktree. A cancelled validation routinely leaves a preserved head that is a REBASE of the local branch, which equality and ancestry read as plain divergence, so a clean diverged worktree is adopted when `preservedContainsLocalWork` proves containment. That proof is an executable `merge-tree` three-way merge whose result must equal the preserved head's tree, anchored on the merge-base - never `runs.base_sha`, the previous gate head. It deliberately does NOT use patch identity: patch IDs discard hunk locations and whitespace, so they cannot tell a genuine replay from a same-shaped edit to another identical block, and a containment claim built on them is not a proof. Everything undecidable escalates, including a rebase whose fix rounds also rewrote operator lines, where nothing separates a deliberate fix from a dropped change. Adoption anchors the pre-recovery local head at `refs/no-mistakes/recover-local/`, then moves the branch with Git operations that fail closed on their own rather than after an observation - an atomic `update-ref` CAS plus `read-tree -m -u`, never check-then-act followed by `reset --hard`, which destroys anything landing in the gap. `recoverAdoptPreserved` owns the reasoning. - Reach limit: a rebase-only cancelled run advances `runs.head_sha` but not the gate branch ref (the run worktree is detached), so recovery still refuses it at the pre-existing `gateHead == preserved` check (upstream #551). Adoption is reachable once any pipeline commit advances that ref. + Terminalization pins every verified unpublished head at `refs/no-mistakes/recover/` before the managed worktree can be removed. Recovery reads that run-specific ref rather than requiring the gate branch to match, so aborts, rebases, and pre-push failures remain recoverable while an independently moved gate branch is preserved. Legacy recorded heads that still exist as dangling gate objects are anchored on recovery; a truly missing recorded head reports a distinct manual-reconciliation action instead of advertising an impossible `recover_custody` command. When the operator keeps a behind or diverged local head instead of taking the preserved head, `--keep-local` never touches the worktree and CAS-moves the gate branch to the kept head, staging objects via gate-side fetch - never a push, which would fire the receive hook and start a run. The full relation matrix and fail-safe rules live in the `Recover` doc comment in `internal/branchsync/sync.go`. - Public guidance is owned by `internal/skill/skill.go` plus live AXI strings, then regenerated with `make skill`. Core regressions live in `internal/branchsync` (incl. `recover_test.go`), `internal/cli/sync_test.go`, `internal/tui/branch_sync_test.go`, and e2e `TestAxiBranchSyncJourney` / `TestAxiCustodyRecoveryJourney` / `TestAxiCustodyRecoveryAfterRebaseJourney` / `TestAxiPrePushAbortUnmovedHeadCustodyJourney`. @@ -248,8 +330,8 @@ Safest local verification sequence after non-trivial changes: - The whole job of this tool is to not lose people's code; favor refusing the push and surfacing a finding over any clever recovery. The comments in `internal/pipeline/steps/forcepush.go` own the full reasoning; the invariants are the next three bullets. - Rebase bases come from the freshly fetched authoritative remote refs, never local or stale state; and a branch built on unpushed local-default-branch commits parks with `NeedsApproval` + `AutoFixable=false` instead of silently widening the PR (`detectBundledLocalDefaultCommits`, #283). - Every force-push routes through `resolveForcePushDecision`, which re-reads the live remote head and allows the push only for a new branch, an already-equal remote, an unchanged `lastSeenSHA`, or remote commits already incorporated by patch-id (excluding `^baseSHA` history the run knowingly rewrites). Anything else refuses, and a failed ls-remote/fetch fails closed; never degrade to a bare `--force`/`--force-with-lease` without an explicit anchor. -- `lastSeenSHA` must stay the head the run last **observed**, never the live remote tip: the rebase step refreshes `origin/` only on a normal push, NOT on a force push, and the CI step passes `Run.HeadSHA`. Anchoring the lease to a SHA read immediately before pushing is the original #281 bug (it always passes and protects nothing); always-fetching the branch on force push recreates it. Never reintroduce either. -- Regressions: `TestCIStep_CommitAndPush_RefusesToClobberUnseenUpstreamCommit` (#281), `TestPushStep_RefusesToClobberAdvancedUpstreamBranch` (#305), `TestForcePushRun_RefusesToClobberOutOfBandBranchCommit`, `TestRebaseStep_DetectsUnpushedLocalDefaultBranchCommits` (#283), `TestResolveForcePushDecision_*`. +- `lastSeenSHA` must stay the head the run last **observed** (from run/prior-run push provenance or the remote-tracking ref), never the live remote tip: the rebase step refreshes `origin/` only on a normal push, NOT on a force push. CI repairs commit locally and restart validation at Review; the later Push step owns their remote update and force-push safety. Anchoring a lease to a SHA read immediately before pushing is the original #281 bug (it always passes and protects nothing); always-fetching the branch on force push recreates it. Never reintroduce either. +- Regressions: `TestPushStep_RefusesToClobberAdvancedUpstreamBranch` (#305), `TestForcePushRun_RefusesToClobberOutOfBandBranchCommit`, `TestRebaseStep_DetectsUnpushedLocalDefaultBranchCommits` (#283), `TestResolveForcePushDecision_*`, `TestExecutor_CIRestartRevalidatesBeforePush`, `TestPushStep_AllowsForcePushAfterMidRunRebaseOverPriorPushedGeneration` (#837), `TestPushStep_AllowsForcePushOnRerunOverPriorRunPushedGeneration` (#837). **macOS Release Signing (permanent identity)** diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index f274543d4..4b6f76ae4 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -5,13 +5,15 @@ Thanks for wanting to contribute. One rule up front: **All pull requests to this repository must be raised through `no-mistakes`.** This repo _is_ no-mistakes. Contributions should be done using the tool itself, which reduces the maintainer's burden of reviewing and merging contributions. -The `Require no-mistakes` GitHub Actions workflow runs on every PR and fails if the body is missing the deterministic signature that no-mistakes writes. PRs without it will not be reviewed or merged. +The `Require no-mistakes` GitHub Actions workflow runs on every PR and fails if the body is missing the deterministic signature and structured pipeline step attestation that no-mistakes writes. PRs without them will not be reviewed or merged. +If you revise the PR description after no-mistakes creates it, preserve the generated `## Pipeline` section. Replacing the whole body removes the signature or attestation and makes the required check fail until no-mistakes writes the section again. Every `opened` or `edited` event gets an independent run, including first-time-fork runs that become actionable through GitHub's normal approval process. The integration contract for consumers such as Wheelhouse is: - The stable check name is `PR must be raised via no-mistakes`. +- The reusable check implementation is the shared composite action in [`.github/actions/require-no-mistakes`](.github/actions/require-no-mistakes/README.md). Consumers pin it to a release tag or commit SHA rather than copying the enforcement shell. This repository uses the same action through a thin workflow caller. - The workflow run's `display_title` identifies the PR number, event action, `run_number`, and immutable `run_id`. For a PR, increasing `run_number` orders distinct events; a re-run retains that event identity and increments `run_attempt`. -- The run's `head_sha` binds the evidence to the reviewed commit. After the latest `opened` or `edited` run reaches `status: completed`, `conclusion: success` means that event's body contained the signature and `conclusion: failure` means it did not. `action_required` or `cancelled` is not compliance evidence and must be handled conservatively. +- The run's `head_sha` binds the evidence to the reviewed commit. After the latest `opened` or `edited` run reaches `status: completed`, `conclusion: success` means that event's body contained the signature and a parseable v1 pipeline attestation whose `head_sha` matches `github.event.pull_request.head.sha` and whose `review`, `test`, and `document` steps are `completed`. `conclusion: failure` means it did not. `action_required` or `cancelled` is not compliance evidence and must be handled conservatively. - Fork runs stay on the `pull_request` boundary with read-only contents permission, no repository secrets, and no checkout or execution of fork code. Approval permits only this body check; it does not grant write authority. ## Workflow @@ -36,7 +38,7 @@ See the [quick start](https://kunchenguid.github.io/no-mistakes/start-here/quick - Go 1.25+, standard toolchain. See `AGENTS.md` for agent instructions. - Run `make fmt`, `make lint`, and `make test` before pushing. Run `make e2e` too when you touch agent integrations, the e2e harness, or recorded fixtures. The pipeline will run them again, but a fast local pass saves rounds. - Run `make skill` when you change the canonical agent skill content under `internal/skill`; `make lint` fails if any committed no-mistakes skill file has drifted. -- Use `make e2e-record` only when an upstream agent wire format changes or you are adding a new fixture flavor. It overwrites `internal/e2e/fixtures/`, spends real API quota, and the diff should be reviewed before committing. +- Use `make e2e-record` only when an upstream agent wire format changes or you are adding a new agent or fixture flavor. It overwrites `internal/e2e/fixtures/`, spends real API quota, and the diff should be reviewed before committing. - Keep `README.md` high-level. Deep reference material belongs in `docs/`. - Do not hand-edit `CHANGELOG.md` or `.release-please-manifest.json`. They are regenerated by release-please from your conventional commit messages, and a separate `Generated files must not be hand-edited` check will fail the PR if either is touched. diff --git a/Makefile b/Makefile index ca6e62c65..03d018d1a 100644 --- a/Makefile +++ b/Makefile @@ -65,14 +65,15 @@ test: e2e: @bash scripts/e2e.sh -# Re-record fixtures from the real claude/codex/opencode CLIs and overwrite +# Re-record fixtures from the real claude/codex/opencode/antigravity CLIs and overwrite # internal/e2e/fixtures/. Spends real API quota — run only when the upstream -# wire format changes or when adding a new flavour. Personal paths are +# wire format changes or when adding a new agent or flavour. Personal paths are # scrubbed automatically; review the diff before committing. e2e-record: go run ./cmd/recordfixture claude --out internal/e2e/fixtures/claude go run ./cmd/recordfixture codex --out internal/e2e/fixtures/codex go run ./cmd/recordfixture opencode --out internal/e2e/fixtures/opencode + go run ./cmd/recordfixture antigravity --out internal/e2e/fixtures/antigravity # Regenerate the committed agent skill (skills/no-mistakes/SKILL.md) from the # internal/skill source of truth. diff --git a/README.md b/README.md index 915656cb2..e1e069233 100644 --- a/README.md +++ b/README.md @@ -38,7 +38,7 @@ Push to `no-mistakes` instead of `origin`, and it spins up a disposable worktree, runs an AI-driven validation pipeline, forwards the branch to the configured push target only after every check passes, and opens a clean PR automatically. - **Non-blocking** - the pipeline runs in an isolated worktree without disrupting your work. -- **Agent-agnostic** - `claude`, `codex`, `rovodev`, `opencode`, `pi`, `copilot`, or `cursor` / `acp:` via `acpx`, with ordered fallbacks; every gate requires a runnable configured pipeline agent. +- **Agent-agnostic** - `claude`, `codex`, `grok`, `rovodev`, `opencode`, `pi`, `copilot`, `antigravity`, or `cursor` / `acp:` via `acpx`, with ordered fallbacks; every gate requires a runnable configured pipeline agent. - **Agent-native** - `/no-mistakes` lets your coding agent do a task and gate it, or gate existing committed work: it runs the pipeline, has the pipeline apply safe fixes, and escalates the rest to you. - **Human stays in charge** - auto-fix or review findings, your call. - **Clean PRs by default** - push, open PR, watch CI, and auto-fix failures in one shot. @@ -134,7 +134,7 @@ make docs # Build the Astro docs site in docs/dist See `Makefile` for the full target list. -`make e2e-record` overwrites `internal/e2e/fixtures/` from the real `claude`, `codex`, and `opencode` CLIs, spends real API quota, and should be reviewed before committing. +`make e2e-record` overwrites `internal/e2e/fixtures/` from the real `claude`, `codex`, `opencode`, and `antigravity` CLIs, spends real API quota, and should be reviewed before committing. ## Star History diff --git a/README.zh-CN.md b/README.zh-CN.md index af2093257..2128da1d8 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -38,7 +38,7 @@ 把分支推给 `no-mistakes` 而不是 `origin`,它会拉起一个用完即弃的 worktree,跑一条 AI 驱动的校验流水线,**只有每一项检查都通过后**才把分支转发到配置的推送目标,并自动开出一个干净的 PR。 - **不阻塞** —— 流水线在隔离的 worktree 里跑,不打断你手头的工作。 -- **不挑 agent** —— 支持 `claude`、`codex`、`rovodev`、`opencode`、`pi`、`copilot`,也可通过 `acpx` 使用 `cursor` / `acp:`,并支持有序 fallback。 +- **不挑 agent** —— 支持 `claude`、`codex`、`grok`、`rovodev`、`opencode`、`pi`、`copilot`、`antigravity`,也可通过 `acpx` 使用 `cursor` / `acp:`,并支持有序 fallback。 - **agent 原生** —— `/no-mistakes` 既能让编码 agent 完成一个任务再过网关,也能直接为已提交的工作过网关:它跑完流水线、让流水线应用安全的修复,剩下的升级给你。 - **人始终说了算** —— 自动修复,还是逐条审查 findings,你决定。 - **默认就是干净 PR** —— 推送、开 PR、盯 CI、自动修复失败,一气呵成。 @@ -134,7 +134,7 @@ make docs # 在 docs/dist 构建 Astro 文档站 完整 target 列表见 `Makefile`。 -`make e2e-record` 会用真实的 `claude`、`codex`、`opencode` CLI 覆盖 `internal/e2e/fixtures/`,会消耗真实 API 额度,提交前应当审查。 +`make e2e-record` 会用真实的 `claude`、`codex`、`opencode` 和 `antigravity` CLI 覆盖 `internal/e2e/fixtures/`,会消耗真实 API 额度,提交前应当审查。 ## Star 历史 diff --git a/VISION.md b/VISION.md index 9a323ae3f..d2008fbe7 100644 --- a/VISION.md +++ b/VISION.md @@ -46,7 +46,7 @@ The pushed branch is untrusted input: nothing on it may choose what executes wit Every verdict must be traceable to something inspectable: findings, executed tests, gathered evidence, and the history of what was fixed and how many attempts it took. A verdict is attributable: every run records the exact tool build and configuration that produced it, so a surprising outcome can always be traced to the software that made it. Evidence stays attached to the change without contaminating it: artifacts are durable and PR-visible, but the shipped branch's history is the author's change and nothing else. -The gate does not define compliance: external systems do, and the gate's duty is to publish facts sufficient for any of them to derive its own verdict. +The gate does not define compliance: external systems do, and the gate's duty is to publish facts sufficient for any of them to derive its own verdict. An external repository policy may explicitly exempt a class of pull requests it does not route through no-mistakes, but that policy bypass must stay distinguishable from evidence that the gate passed and must never skip a step inside a no-mistakes run. The PR a run raises is written for a reviewer who was not there: what changed, what was checked, what the risks are, and what the pipeline had to fix. Run state must honestly distinguish working, parked waiting on a human, and dead; a stall that looks alive is a lie. Failure is a first-class outcome: loud, attributed, explained, and followed by a next action. diff --git a/cmd/fakeagent/antigravity.go b/cmd/fakeagent/antigravity.go new file mode 100644 index 000000000..0533d16f0 --- /dev/null +++ b/cmd/fakeagent/antigravity.go @@ -0,0 +1,176 @@ +package main + +import ( + "bytes" + "encoding/json" + "fmt" + "os" + "strings" +) + +func runAgy(args []string, scenario *Scenario) int { + prompt, err := extractAgyPrompt(args) + if err != nil { + fmt.Fprintf(os.Stderr, "fakeagent: agy prompt: %v\n", err) + return 1 + } + logInvocation("antigravity", prompt, args) + + action := scenario.Match(prompt) + if err := applyAction(action); err != nil { + return 1 + } + + // Fixture mode: replay the real agy wire envelope captured from a + // live headless run, splicing scenario-driven content into the fields + // no-mistakes parses (step_update text deltas, result response and + // structured_output). The event ordering and field shapes stay exactly + // what agy emits, so wire-format drift surfaces in e2e. + flavour := "plain" + if hasAgySchema(args) { + flavour = "structured" + } + if data, err := readFixtureFile(fixtureDir("antigravity"), flavour, ".jsonl"); err != nil { + fmt.Fprintf(os.Stderr, "fakeagent: antigravity fixture: %v\n", err) + return 1 + } else if data != nil { + patched, err := patchAgyFixture(data, action) + if err != nil { + fmt.Fprintf(os.Stderr, "fakeagent: agy patch: %v\n", err) + return 1 + } + os.Stdout.Write(patched) + return 0 + } + + // Synthetic fallback when no recorded fixture is available. + enc := json.NewEncoder(os.Stdout) + _ = enc.Encode(map[string]any{ + "event": "init", + "conversation_id": "fake-agy-session", + }) + _ = enc.Encode(map[string]any{ + "event": "step_update", + "step_update": map[string]any{ + "conversation_id": "fake-agy-session", + "step_index": 0, + "state": "DONE", + "step_type": "agent_response", + "text_delta": action.textOrDefault(), + "usage": map[string]any{ + "input_tokens": 100, + "output_tokens": 50, + "thinking_tokens": 8, + "cache_read_tokens": 0, + "total_tokens": 150, + }, + }, + }) + result := map[string]any{ + "event": "result", + "result": map[string]any{ + "conversation_id": "fake-agy-session", + "status": "SUCCESS", + "response": action.textOrDefault(), + "num_turns": 1, + "usage": map[string]any{ + "input_tokens": 100, + "output_tokens": 50, + "thinking_tokens": 8, + "cache_read_tokens": 0, + "total_tokens": 150, + }, + }, + } + if action.hasStructuredOutput() { + result["result"].(map[string]any)["structured_output"] = json.RawMessage(action.structuredJSON()) + } + _ = enc.Encode(result) + return 0 +} + +// patchAgyFixture rewrites the agent_response text delta and the terminal +// result's response/structured_output to match the scenario action, leaving +// every other event byte-for-byte so the envelope stays a real agy trace. +func patchAgyFixture(raw []byte, action Action) ([]byte, error) { + text := action.textOrDefault() + structuredJSON := action.structuredJSON() + var out bytes.Buffer + for _, line := range bytes.Split(raw, []byte("\n")) { + if len(line) == 0 { + out.WriteByte('\n') + continue + } + var probe struct { + Event string `json:"event"` + } + if err := json.Unmarshal(line, &probe); err != nil { + out.Write(line) + out.WriteByte('\n') + continue + } + switch probe.Event { + case "step_update": + var event map[string]any + if err := json.Unmarshal(line, &event); err != nil { + return nil, fmt.Errorf("parse step_update event: %w", err) + } + step, _ := event["step_update"].(map[string]any) + if step != nil && step["step_type"] == "agent_response" { + step["text_delta"] = text + event["step_update"] = step + patched, err := json.Marshal(event) + if err != nil { + return nil, fmt.Errorf("marshal patched step_update: %w", err) + } + line = patched + } + case "result": + var event map[string]any + if err := json.Unmarshal(line, &event); err != nil { + return nil, fmt.Errorf("parse result event: %w", err) + } + result, _ := event["result"].(map[string]any) + if result != nil { + result["response"] = text + if action.hasStructuredOutput() { + result["structured_output"] = json.RawMessage(structuredJSON) + } else { + delete(result, "structured_output") + } + event["result"] = result + patched, err := json.Marshal(event) + if err != nil { + return nil, fmt.Errorf("marshal patched result: %w", err) + } + line = patched + } + } + out.Write(line) + out.WriteByte('\n') + } + return out.Bytes(), nil +} + +func hasAgySchema(args []string) bool { + for i, arg := range args { + if arg == "--json-schema" && i < len(args)-1 { + return true + } + if strings.HasPrefix(arg, "--json-schema=") { + return true + } + } + return false +} + +// extractAgyPrompt mirrors agy's print-mode contract: --print (or -p / +// --prompt) is followed by the prompt text as the next argv element. +func extractAgyPrompt(args []string) (string, error) { + for _, flag := range []string{"--print", "-p", "--prompt"} { + if prompt := argAfter(args, flag); prompt != "" { + return prompt, nil + } + } + return "", fmt.Errorf("missing --print") +} diff --git a/cmd/fakeagent/antigravity_test.go b/cmd/fakeagent/antigravity_test.go new file mode 100644 index 000000000..10c600dc8 --- /dev/null +++ b/cmd/fakeagent/antigravity_test.go @@ -0,0 +1,173 @@ +package main + +import ( + "bytes" + "os" + "path/filepath" + "strings" + "testing" +) + +func TestExtractAgyPromptReadsPrintArgument(t *testing.T) { + got, err := extractAgyPrompt([]string{"--dangerously-skip-permissions", "--print", "review this branch", "--output-format", "stream-json"}) + if err != nil { + t.Fatalf("extractAgyPrompt() error = %v", err) + } + if got != "review this branch" { + t.Fatalf("prompt = %q", got) + } +} + +func TestExtractAgyPromptAcceptsShortAlias(t *testing.T) { + got, err := extractAgyPrompt([]string{"-p", "short alias"}) + if err != nil { + t.Fatalf("extractAgyPrompt() error = %v", err) + } + if got != "short alias" { + t.Fatalf("prompt = %q", got) + } +} + +func TestRunAgyEmitsStreamJSONResult(t *testing.T) { + oldStdout := os.Stdout + r, w, err := os.Pipe() + if err != nil { + t.Fatal(err) + } + os.Stdout = w + t.Cleanup(func() { os.Stdout = oldStdout }) + + status := runAgy([]string{"--print", "do work", "--output-format", "stream-json"}, defaultScenario()) + _ = w.Close() + var out bytes.Buffer + _, _ = out.ReadFrom(r) + if status != 0 { + t.Fatalf("runAgy() status = %d", status) + } + for _, want := range []string{`"event":"init"`, `"event":"step_update"`, `"event":"result"`, `"status":"SUCCESS"`, `"structured_output"`} { + if !strings.Contains(out.String(), want) { + t.Fatalf("output missing %s:\n%s", want, out.String()) + } + } +} + +func TestRunAgyReturnsErrorWhenConfiguredFixtureIsMissing(t *testing.T) { + t.Setenv("FAKEAGENT_FIXTURE", t.TempDir()) + + if status := runAgy([]string{"--print", "do work", "--output-format", "stream-json"}, defaultScenario()); status == 0 { + t.Fatal("runAgy() status = 0, want configured fixture error") + } +} + +func TestPatchAgyFixtureRewritesContentAndKeepsEnvelope(t *testing.T) { + raw := []byte(strings.Join([]string{ + `{"event":"init","conversation_id":"fix","init":{"cwd":"/tmp","tools":["run_command"]}}`, + `{"event":"step_update","step_update":{"step_index":2,"state":"DONE","step_type":"agent_response","text_delta":"recorded"}}`, + `{"event":"result","result":{"conversation_id":"fix","status":"SUCCESS","response":"recorded","structured_output":{"ok":true},"usage":{"input_tokens":1}}}`, + "", + }, "\n")) + + patched, err := patchAgyFixture(raw, Action{ + Text: "scenario text", + Structured: map[string]any{ + "findings": []any{}, + "summary": "no issues found", + }, + }) + if err != nil { + t.Fatalf("patchAgyFixture() error = %v", err) + } + + joined := string(patched) + if !strings.Contains(joined, `"text_delta":"scenario text"`) { + t.Fatalf("agent_response delta not patched:\n%s", joined) + } + if !strings.Contains(joined, `"response":"scenario text"`) { + t.Fatalf("result response not patched:\n%s", joined) + } + if !strings.Contains(joined, `"summary":"no issues found"`) { + t.Fatalf("structured_output not patched:\n%s", joined) + } + if strings.Contains(joined, `"cwd":"/tmp"`) == false || !strings.Contains(joined, `"usage":{"input_tokens":1}`) { + t.Fatalf("envelope fields should stay untouched:\n%s", joined) + } +} + +func TestPatchAgyFixtureDropsStructuredOutputForPlainActions(t *testing.T) { + raw := []byte(`{"event":"result","result":{"status":"SUCCESS","response":"x","structured_output":{"ok":true}}}` + "\n") + + patched, err := patchAgyFixture(raw, Action{Text: "plain prose"}) + if err != nil { + t.Fatalf("patchAgyFixture() error = %v", err) + } + if strings.Contains(string(patched), "structured_output") { + t.Fatalf("plain action should drop structured_output:\n%s", patched) + } +} + +func TestRunAgyReplaysRecordedFixture(t *testing.T) { + dir := t.TempDir() + fixture := filepath.Join(dir, "antigravity") + if err := os.MkdirAll(fixture, 0o755); err != nil { + t.Fatal(err) + } + fixtureContents := map[string]string{ + "plain.jsonl": `{"event":"result","result":{"conversation_id":"plain-recorded","status":"SUCCESS","response":"recorded"}}` + "\n", + "structured.jsonl": `{"event":"result","result":{"conversation_id":"structured-recorded","status":"SUCCESS","response":"recorded","structured_output":{"ok":true}}}` + "\n", + } + for name, content := range fixtureContents { + if err := os.WriteFile(filepath.Join(fixture, name), []byte(content), 0o644); err != nil { + t.Fatal(err) + } + } + t.Setenv("FAKEAGENT_FIXTURE", dir) + + t.Run("structured", func(t *testing.T) { + oldStdout := os.Stdout + r, w, err := os.Pipe() + if err != nil { + t.Fatal(err) + } + os.Stdout = w + status := runAgy([]string{"--print", "work", "--json-schema", "{}", "--output-format", "stream-json"}, defaultScenario()) + _ = w.Close() + os.Stdout = oldStdout + var out bytes.Buffer + _, _ = out.ReadFrom(r) + if status != 0 { + t.Fatalf("runAgy() status = %d", status) + } + if !strings.Contains(out.String(), `"conversation_id":"structured-recorded"`) { + t.Fatalf("fixture replay should carry the recorded envelope:\n%s", out.String()) + } + if !strings.Contains(out.String(), `"summary"`) { + t.Fatalf("fixture replay should splice scenario structured output:\n%s", out.String()) + } + }) + + t.Run("plain", func(t *testing.T) { + oldStdout := os.Stdout + r, w, err := os.Pipe() + if err != nil { + t.Fatal(err) + } + os.Stdout = w + status := runAgy([]string{"--print", "work", "--output-format", "stream-json"}, defaultScenario()) + _ = w.Close() + os.Stdout = oldStdout + var out bytes.Buffer + _, _ = out.ReadFrom(r) + if status != 0 { + t.Fatalf("runAgy() status = %d", status) + } + if !strings.Contains(out.String(), `"conversation_id":"plain-recorded"`) { + t.Fatalf("plain fixture replay should carry the recorded envelope:\n%s", out.String()) + } + if strings.Contains(out.String(), `"conversation_id":"structured-recorded"`) { + t.Fatalf("plain call replayed the structured fixture:\n%s", out.String()) + } + if !strings.Contains(out.String(), `"summary"`) { + t.Fatalf("plain fixture replay should splice scenario structured output:\n%s", out.String()) + } + }) +} diff --git a/cmd/fakeagent/codex.go b/cmd/fakeagent/codex.go index f4cd5c701..c43e4a371 100644 --- a/cmd/fakeagent/codex.go +++ b/cmd/fakeagent/codex.go @@ -4,12 +4,17 @@ import ( "bytes" "encoding/json" "fmt" + "io" "os" "strings" ) -func runCodex(args []string, scenario *Scenario) int { - prompt := extractCodexPrompt(args) +func runCodex(args []string, promptReader io.Reader, scenario *Scenario) int { + prompt, err := extractCodexPrompt(args, promptReader) + if err != nil { + fmt.Fprintf(os.Stderr, "fakeagent: codex prompt: %v\n", err) + return 1 + } logInvocation("codex", prompt, args) action := scenario.Match(prompt) @@ -172,19 +177,19 @@ func filterStructuredToSchema(structured map[string]any, schemaPath string) (map return filtered, nil } -// extractCodexPrompt finds the prompt positional. Real codex argv is -// `codex exec [user-flags...] --json [...]` for a fresh session and -// `codex exec resume [user-flags...] --json [...]` for -// a session-resume turn, so on resume the prompt is the positional after the -// session id. -func extractCodexPrompt(args []string) string { +// extractCodexPrompt mirrors Codex's stdin prompt contract. Real codex argv is +// `codex exec [user-flags...] - --json [...]` for a fresh session and +// `codex exec resume [user-flags...] - --json [...]` for a +// resumed session; in both cases the complete prompt is read to EOF from stdin. +func extractCodexPrompt(args []string, promptReader io.Reader) (string, error) { flagsWithValues := map[string]bool{ "-m": true, "--model": true, "--sandbox": true, "--ask-for-approval": true, "--config": true, "--profile": true, "--output-schema": true, "--reasoning-effort": true, "--reasoning-summary": true, - "-c": true, "--cd": true, + "--color": true, + "-c": true, "--cd": true, } start := 0 for i, a := range args { @@ -200,19 +205,24 @@ func extractCodexPrompt(args []string) string { i++ continue } - if len(a) > 0 && a[0] == '-' { + if len(a) > 1 && a[0] == '-' { continue } positionals = append(positionals, a) } if len(positionals) == 0 { - return "" + return "", fmt.Errorf("missing stdin prompt marker") } if positionals[0] == "resume" { - if len(positionals) >= 3 { - return positionals[2] // resume + if len(positionals) != 3 || positionals[2] != "-" { + return "", fmt.Errorf("resume missing stdin prompt marker") } - return "" // resume without id+prompt is not a shape no-mistakes emits + } else if len(positionals) != 1 || positionals[0] != "-" { + return "", fmt.Errorf("missing stdin prompt marker") + } + prompt, err := io.ReadAll(promptReader) + if err != nil { + return "", fmt.Errorf("read stdin: %w", err) } - return positionals[0] + return string(prompt), nil } diff --git a/cmd/fakeagent/codex_test.go b/cmd/fakeagent/codex_test.go index ef03fa59d..881436da2 100644 --- a/cmd/fakeagent/codex_test.go +++ b/cmd/fakeagent/codex_test.go @@ -6,6 +6,7 @@ import ( "os" "path/filepath" "reflect" + "strings" "testing" ) @@ -181,18 +182,37 @@ func TestExtractCodexOutputSchemaPath(t *testing.T) { } } -func TestExtractCodexPromptSkipsOutputSchemaValue(t *testing.T) { - t.Helper() - - args := []string{ - "exec", - "--output-schema", "/tmp/schema.json", - "--model", "gpt-5.4", - "review this diff", - "--json", +func TestExtractCodexPromptReadsStdin(t *testing.T) { + tests := []struct { + name string + args []string + }{ + { + name: "fresh", + args: []string{"exec", "--output-schema", "/tmp/schema.json", "--model", "gpt-5.4", "-", "--json", "--color", "never"}, + }, + { + name: "resume", + args: []string{"exec", "resume", "--model", "gpt-5.4", "thread-123", "-", "--json"}, + }, } + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + const prompt = "review this diff" + got, err := extractCodexPrompt(tc.args, strings.NewReader(prompt)) + if err != nil { + t.Fatalf("extractCodexPrompt: %v", err) + } + if got != prompt { + t.Fatalf("prompt = %q, want %q", got, prompt) + } + }) + } +} - if got := extractCodexPrompt(args); got != "review this diff" { - t.Fatalf("prompt = %q, want %q", got, "review this diff") +func TestExtractCodexPromptRejectsArgvPrompt(t *testing.T) { + _, err := extractCodexPrompt([]string{"exec", "prompt in argv", "--json"}, strings.NewReader("")) + if err == nil { + t.Fatal("expected argv prompt to be rejected") } } diff --git a/cmd/fakeagent/grok.go b/cmd/fakeagent/grok.go new file mode 100644 index 000000000..bfebfdf06 --- /dev/null +++ b/cmd/fakeagent/grok.go @@ -0,0 +1,79 @@ +package main + +import ( + "encoding/json" + "fmt" + "os" + "strings" +) + +func runGrok(args []string, scenario *Scenario) int { + prompt, err := extractGrokPrompt(args) + if err != nil { + fmt.Fprintf(os.Stderr, "fakeagent: grok prompt: %v\n", err) + return 1 + } + logInvocation("grok", prompt, args) + + action := scenario.Match(prompt) + if err := applyAction(action); err != nil { + return 1 + } + + sessionID := argAfter(args, "--resume") + if sessionID == "" { + sessionID = "fake-grok-session" + } + enc := json.NewEncoder(os.Stdout) + _ = enc.Encode(map[string]any{ + "type": "system", + "subtype": "init", + "session_id": sessionID, + "model": "grok-default", + }) + _ = enc.Encode(map[string]any{ + "type": "assistant", + "session_id": sessionID, + "message": map[string]any{ + "model": "grok-default", + "content": []any{ + map[string]any{"type": "text", "text": action.textOrDefault()}, + }, + }, + }) + _ = enc.Encode(map[string]any{ + "type": "result", + "subtype": "success", + "is_error": false, + "result": action.textOrDefault(), + "structured_output": json.RawMessage(action.structuredJSON()), + "session_id": sessionID, + "usage": map[string]int{ + "input_tokens": 100, + "output_tokens": 50, + "cache_read_input_tokens": 0, + "cache_creation_input_tokens": 0, + }, + }) + return 0 +} + +func extractGrokPrompt(args []string) (string, error) { + path := argAfter(args, "--prompt-file") + if path == "" { + for _, arg := range args { + if strings.HasPrefix(arg, "--prompt-file=") { + path = strings.TrimPrefix(arg, "--prompt-file=") + break + } + } + } + if path == "" { + return "", fmt.Errorf("missing --prompt-file") + } + data, err := os.ReadFile(path) + if err != nil { + return "", fmt.Errorf("read %s: %w", path, err) + } + return string(data), nil +} diff --git a/cmd/fakeagent/grok_test.go b/cmd/fakeagent/grok_test.go new file mode 100644 index 000000000..ec8bcb2ff --- /dev/null +++ b/cmd/fakeagent/grok_test.go @@ -0,0 +1,50 @@ +package main + +import ( + "bytes" + "os" + "path/filepath" + "strings" + "testing" +) + +func TestExtractGrokPromptReadsManagedPromptFile(t *testing.T) { + path := filepath.Join(t.TempDir(), "prompt.txt") + if err := os.WriteFile(path, []byte("review this branch"), 0o600); err != nil { + t.Fatal(err) + } + got, err := extractGrokPrompt([]string{"--prompt-file", path, "--output-format", "streaming-messages-json"}) + if err != nil { + t.Fatalf("extractGrokPrompt() error = %v", err) + } + if got != "review this branch" { + t.Fatalf("prompt = %q", got) + } +} + +func TestRunGrokEmitsStreamingMessagesStructuredResult(t *testing.T) { + path := filepath.Join(t.TempDir(), "prompt.txt") + if err := os.WriteFile(path, []byte("review"), 0o600); err != nil { + t.Fatal(err) + } + oldStdout := os.Stdout + r, w, err := os.Pipe() + if err != nil { + t.Fatal(err) + } + os.Stdout = w + t.Cleanup(func() { os.Stdout = oldStdout }) + + status := runGrok([]string{"--prompt-file", path}, defaultScenario()) + _ = w.Close() + var out bytes.Buffer + _, _ = out.ReadFrom(r) + if status != 0 { + t.Fatalf("runGrok() status = %d", status) + } + for _, want := range []string{`"type":"system"`, `"type":"assistant"`, `"type":"result"`, `"structured_output"`, `"model":"grok-default"`} { + if !strings.Contains(out.String(), want) { + t.Fatalf("output missing %s:\n%s", want, out.String()) + } + } +} diff --git a/cmd/fakeagent/main.go b/cmd/fakeagent/main.go index 061653aa6..9be53043c 100644 --- a/cmd/fakeagent/main.go +++ b/cmd/fakeagent/main.go @@ -1,7 +1,8 @@ -// fakeagent is a deterministic stand-in for the real Claude, Codex, and -// OpenCode CLIs used by no-mistakes' e2e tests. One binary is compiled and -// then symlinked under each agent name; argv[0]'s basename selects which -// wire protocol to speak. +// fakeagent is a deterministic stand-in for the real Claude, Codex, Grok, +// OpenCode, and Antigravity CLIs used by no-mistakes' e2e tests. One binary is +// compiled and then symlinked under each agent's dispatch name; Antigravity +// is linked as both `antigravity` and its probed binary name `agy`. +// argv[0]'s basename selects which wire protocol to speak. // // All invocations are appended to $FAKEAGENT_LOG (one JSON object per line) // so tests can assert on exactly which prompts the pipeline issued. @@ -17,6 +18,7 @@ import ( "io" "os" "path/filepath" + "strconv" "strings" "time" ) @@ -39,11 +41,17 @@ func run(argv []string) int { case "claude": return runClaude(args, os.Stdin, scenario) case "codex": - return runCodex(args, scenario) + return runCodex(args, os.Stdin, scenario) + case "grok": + return runGrok(args, scenario) + case "antigravity", "agy": + return runAgy(args, scenario) case "opencode": return runOpencode(args, scenario) case "gh": return runGhStub(args) + case "tea": + return runTeaStub(args) default: fmt.Fprintf(os.Stderr, "fakeagent: invoked under unknown name %q (argv[0]=%q)\n", name, argv[0]) return 2 @@ -145,6 +153,88 @@ func recordGhStubInvocation(args []string) { _ = json.NewEncoder(f).Encode(inv) } +// runTeaStub shadows any system-installed tea during the Gitea provider e2e +// journey. It is a stateless canned-response stub, mirroring +// runGhForkPRStub: `pulls list` always reports no existing PR (so the PR +// step exercises CreatePR), `pulls create` fabricates a plausible PR URL +// from its own human-readable output (real tea has no --output json on +// create; the pipeline's CreatePR falls back to scanning that output because +// this stub's stateless `pulls list` can never re-find the PR it just +// created), and `pulls ` (view) reports the PR as already merged so the +// CI step's GetPRState short-circuits on the first poll without needing to +// model Gitea Actions runs at all. +func runTeaStub(args []string) int { + recordTeaStubInvocation(args) + + if len(args) >= 1 && args[0] == "api" && args[len(args)-1] == "/user" { + fmt.Println(`{"login":"e2e-tea-user"}`) + return 0 + } + if len(args) >= 2 && args[0] == "pulls" && args[1] == "list" { + fmt.Println("[]") + return 0 + } + if len(args) >= 2 && args[0] == "pulls" && args[1] == "create" { + repo := argAfter(args, "--repo") + if repo == "" { + repo = "owner/repo" + } + host := os.Getenv("FAKEAGENT_TEA_HOST") + if host == "" { + host = "gitea.example.com" + } + fmt.Printf(" # #99 stub PR (open)\n\n http://%s/%s/pulls/99\n", host, strings.TrimSuffix(repo, ".git")) + return 0 + } + if len(args) >= 2 && args[0] == "pulls" && args[1] == "edit" { + fmt.Println("updated") + return 0 + } + if len(args) >= 2 && args[0] == "pulls" { + // `tea pulls --output json` (view a single PR by index). Merged + // on the first poll so the CI step's GetPRState exits without needing + // to model Gitea Actions runs. + if _, err := strconv.Atoi(args[1]); err == nil { + fmt.Println(`{"index":99,"state":"closed","hasMerged":true,"head":"","base":"main"}`) + return 0 + } + } + + fmt.Fprintf(os.Stderr, "fakeagent tea: subcommand not implemented in e2e stub: %v\n", args) + return 1 +} + +type teaStubInvocation struct { + Time string `json:"time"` + Args []string `json:"args"` + Repo string `json:"repo,omitempty"` + Login string `json:"login,omitempty"` + Head string `json:"head,omitempty"` + Base string `json:"base,omitempty"` +} + +func recordTeaStubInvocation(args []string) { + logPath := os.Getenv("FAKEAGENT_TEA_LOG") + if logPath == "" { + return + } + f, err := os.OpenFile(logPath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0o644) + if err != nil { + return + } + defer f.Close() + + inv := teaStubInvocation{ + Time: time.Now().Format(time.RFC3339Nano), + Args: append([]string(nil), args...), + Repo: argAfter(args, "--repo"), + Login: argAfter(args, "--login"), + Head: argAfter(args, "--head"), + Base: argAfter(args, "--base"), + } + _ = json.NewEncoder(f).Encode(inv) +} + func argAfter(args []string, flag string) string { for i := 0; i < len(args)-1; i++ { if args[i] == flag { diff --git a/cmd/fakeagent/scenario.go b/cmd/fakeagent/scenario.go index 0e4ee9fef..32cfdb8f0 100644 --- a/cmd/fakeagent/scenario.go +++ b/cmd/fakeagent/scenario.go @@ -30,7 +30,7 @@ type Action struct { Match string `yaml:"match"` // Structured is the JSON body returned in the structured-output slot - // (claude.result.structured_output, opencode.info.structured, or the + // (claude/grok result.structured_output, opencode.info.structured, or the // agent_message.text payload for codex). Encoded back to JSON when // emitted, so YAML authors can write it inline without escaping. Structured map[string]any `yaml:"structured,omitempty"` diff --git a/cmd/nm-test/main.go b/cmd/nm-test/main.go deleted file mode 100644 index 93983061f..000000000 --- a/cmd/nm-test/main.go +++ /dev/null @@ -1,32 +0,0 @@ -package main - -import ( - "context" - "fmt" - "os" - - steps "github.com/kunchenguid/no-mistakes/internal/pipeline/steps" - "github.com/kunchenguid/no-mistakes/internal/pipeline" -) - -func main() { - dir := os.Args[1] - ctx := context.Background() - sctx := &pipeline.StepContext{Ctx: ctx, WorkDir: dir} - - fmt.Println("=== worktree-gc ===") - r, err := (&steps.WorktreeGcStep{}).Execute(sctx) - if err != nil { - fmt.Println("err:", err) - return - } - fmt.Printf("findings: %s\n", r.Findings) - - fmt.Println("=== branch-lint ===") - r, _ = (&steps.BranchLintStep{}).Execute(sctx) - fmt.Printf("findings: %s\n", r.Findings) - - fmt.Println("=== health-check ===") - r, _ = (&steps.HealthCheckStep{}).Execute(sctx) - fmt.Printf("findings: %s\n", r.Findings) -} diff --git a/cmd/no-mistakes/main.go b/cmd/no-mistakes/main.go index 2e1293c6c..99cbb787a 100644 --- a/cmd/no-mistakes/main.go +++ b/cmd/no-mistakes/main.go @@ -17,11 +17,16 @@ import ( "github.com/kunchenguid/no-mistakes/internal/update" ) +var cleanupOldExecutable = update.CleanupOldExecutable +var maybeHandleBackgroundCheck = update.MaybeHandleBackgroundCheck + func main() { os.Exit(run()) } func run() int { + _ = cleanupOldExecutable() + if root, ok, err := daemonLogSinkRootFromArgs(os.Args[1:]); err != nil { fmt.Fprintln(os.Stderr, err) return 1 @@ -53,7 +58,7 @@ func run() int { return 0 } - if handled, err := update.MaybeHandleBackgroundCheck(os.Args[1:]); handled { + if handled, err := maybeHandleBackgroundCheck(os.Args[1:]); handled { if err != nil { fmt.Fprintln(os.Stderr, err) return 1 diff --git a/cmd/no-mistakes/main_test.go b/cmd/no-mistakes/main_test.go index 216d023dc..c733e2ecf 100644 --- a/cmd/no-mistakes/main_test.go +++ b/cmd/no-mistakes/main_test.go @@ -35,6 +35,66 @@ func TestMain(m *testing.M) { os.Exit(code) } +func TestRunAttemptsOldExecutableCleanupBeforeEarlyRoutes(t *testing.T) { + originalArgs := os.Args + originalCleanup := cleanupOldExecutable + originalBackground := maybeHandleBackgroundCheck + t.Cleanup(func() { + os.Args = originalArgs + cleanupOldExecutable = originalCleanup + maybeHandleBackgroundCheck = originalBackground + }) + + t.Run("daemon", func(t *testing.T) { + called := false + cleanupOldExecutable = func() error { + called = true + return nil + } + os.Args = []string{"no-mistakes", "daemon", "log-sink", "--root", ""} + if code := run(); code != 1 { + t.Fatalf("run code = %d, want 1", code) + } + if !called { + t.Fatal("cleanup was not attempted before daemon routing") + } + }) + + t.Run("background update", func(t *testing.T) { + cleanupFinished := false + cleanupOldExecutable = func() error { + cleanupFinished = true + return fmt.Errorf("executable is still locked") + } + maybeHandleBackgroundCheck = func([]string) (bool, error) { + if !cleanupFinished { + t.Fatal("background routing ran before cleanup") + } + return true, nil + } + os.Args = []string{"no-mistakes", "--update-check", "v1.2.3"} + if code := run(); code != 0 { + t.Fatalf("run code = %d, want 0", code) + } + }) + + t.Run("interactive", func(t *testing.T) { + called := false + cleanupOldExecutable = func() error { + called = true + return nil + } + maybeHandleBackgroundCheck = originalBackground + os.Args = []string{"no-mistakes", "--version"} + if code := run(); code != 0 { + t.Fatalf("run code = %d, want 0", code) + } + if !called { + t.Fatal("cleanup was not attempted before interactive routing") + } + }) +} + func TestCLILogWriterReturnsDiscardWhenLogsDirMissing(t *testing.T) { nmHome := t.TempDir() t.Setenv("NM_HOME", nmHome) diff --git a/cmd/recordfixture/antigravity.go b/cmd/recordfixture/antigravity.go new file mode 100644 index 000000000..16c1b0f48 --- /dev/null +++ b/cmd/recordfixture/antigravity.go @@ -0,0 +1,132 @@ +package main + +import ( + "context" + "encoding/json" + "fmt" + "io" + "os" + "os/exec" + "path/filepath" + + "github.com/kunchenguid/no-mistakes/internal/shellenv" +) + +// recordAntigravity captures agy CLI's NDJSON stream-json events. The +// fakeagent replays these envelopes while patching only the agent_response +// text delta and the result's response/structured_output, so the recorded +// content just needs to exercise both flavours: one run with --json-schema +// (structured) and one plain text run. +func recordAntigravity(ctx context.Context, out string, args []string) int { + bin, forward := splitBinArgs(args, "agy") + + if err := captureAgy(ctx, bin, forward, + "Return JSON with field ok set to true", + filepath.Join(out, "structured.jsonl"), + []string{"--json-schema", `{"type":"object","properties":{"ok":{"type":"boolean"}},"required":["ok"],"additionalProperties":false}`}, + ); err != nil { + fmt.Fprintln(os.Stderr, err) + return 1 + } + + if err := captureAgy(ctx, bin, forward, + "Reply with the literal word OK and nothing else.", + filepath.Join(out, "plain.jsonl"), + nil, + ); err != nil { + fmt.Fprintln(os.Stderr, err) + return 1 + } + + fmt.Fprintf(os.Stderr, "antigravity fixtures written to %s\n", out) + return 0 +} + +func captureAgy(ctx context.Context, bin string, forward []string, prompt, outPath string, extraArgs []string) error { + cmdArgs := make([]string, 0, len(forward)+len(extraArgs)+6) + cmdArgs = append(cmdArgs, forward...) + cmdArgs = append(cmdArgs, "--dangerously-skip-permissions", "--print", prompt) + cmdArgs = append(cmdArgs, extraArgs...) + cmdArgs = append(cmdArgs, "--output-format", "stream-json") + cmd := exec.CommandContext(ctx, bin, cmdArgs...) + tmp, err := os.MkdirTemp("", "recordagy-*") + if err != nil { + return fmt.Errorf("tempdir: %w", err) + } + defer os.RemoveAll(tmp) + cmd.Dir = tmp + + // Capture into a temporary sibling so a nonzero exit or cancellation + // cannot leave an existing fixture truncated or half-written; the + // destination is replaced only after the run and scrub both succeed. + staging := outPath + ".recording" + f, err := os.Create(staging) + if err != nil { + return fmt.Errorf("create %s: %w", staging, err) + } + defer func() { + f.Close() + os.Remove(staging) + }() + + cmd.Stdout = f + cmd.Stderr = os.Stderr + shellenv.ConfigureShellCommand(cmd) + fmt.Fprintf(os.Stderr, "recording agy → %s\n", outPath) + if err := shellenv.RunShellCommand(cmd); err != nil { + return fmt.Errorf("run agy: %w", err) + } + if err := f.Close(); err != nil { + return fmt.Errorf("close %s: %w", staging, err) + } + if err := validateAgyCapture(staging); err != nil { + return fmt.Errorf("validate %s: %w", staging, err) + } + if err := scrubFile(staging); err != nil { + return fmt.Errorf("scrub %s: %w", staging, err) + } + if err := os.Rename(staging, outPath); err != nil { + return fmt.Errorf("publish %s: %w", outPath, err) + } + return nil +} + +func validateAgyCapture(path string) error { + f, err := os.Open(path) + if err != nil { + return err + } + defer f.Close() + + var resultCount int + decoder := json.NewDecoder(f) + for { + var envelope struct { + Event string `json:"event"` + Result *struct { + Status string `json:"status"` + } `json:"result"` + } + err := decoder.Decode(&envelope) + if err == io.EOF { + break + } + if err != nil { + return fmt.Errorf("decode event: %w", err) + } + if envelope.Event != "result" { + continue + } + if envelope.Result == nil { + return fmt.Errorf("result event is missing result envelope") + } + if status := envelope.Result.Status; status != "SUCCESS" { + return fmt.Errorf("result status %q is not SUCCESS", status) + } + resultCount++ + } + if resultCount == 0 { + return fmt.Errorf("capture has no result event") + } + return nil +} diff --git a/cmd/recordfixture/main.go b/cmd/recordfixture/main.go index 2b7fbb784..4742b5e86 100644 --- a/cmd/recordfixture/main.go +++ b/cmd/recordfixture/main.go @@ -7,15 +7,17 @@ // go run ./cmd/recordfixture claude --out internal/e2e/fixtures/claude // go run ./cmd/recordfixture codex --out internal/e2e/fixtures/codex // go run ./cmd/recordfixture opencode --out internal/e2e/fixtures/opencode +// go run ./cmd/recordfixture antigravity --out internal/e2e/fixtures/antigravity // // Each agent gets a small set of fixture files (one per pipeline-step // flavour: review with structured output, plain text, etc). The fake -// agent in cmd/fakeagent replays these byte-for-byte at runtime. +// agent in cmd/fakeagent replays the recorded wire envelopes and patches +// scenario-dependent response fields where needed. // -// The recorder keeps no schema knowledge of its own — it just shells out -// to the real CLI and tees stdout/stderr/SSE/HTTP responses to disk. If -// the real wire format drifts upstream, re-recording produces the new -// fixture and the fake's replay automatically reflects it. +// The recorder keeps no schema knowledge of its own — it shells out to each +// real CLI and captures that CLI's wire output to disk. If the real wire +// format drifts upstream, re-recording produces the new fixture and the +// fake's replay automatically reflects it. package main import ( @@ -59,15 +61,17 @@ func run() int { return recordCodex(ctx, out, args) case "opencode": return recordOpencode(ctx, out, args) + case "antigravity": + return recordAntigravity(ctx, out, args) default: - fmt.Fprintf(os.Stderr, "unknown agent %q (want claude|codex|opencode)\n", agent) + fmt.Fprintf(os.Stderr, "unknown agent %q (want claude|codex|opencode|antigravity)\n", agent) usage() return 2 } } func usage() { - fmt.Fprintln(os.Stderr, "usage: recordfixture --out [--bin ]") + fmt.Fprintln(os.Stderr, "usage: recordfixture --out [--bin ]") fmt.Fprintln(os.Stderr, "captures real agent output as e2e fixture files. burns real API quota.") } diff --git a/cmd/recordfixture/main_test.go b/cmd/recordfixture/main_test.go index 8bbbbbb54..83912aa9f 100644 --- a/cmd/recordfixture/main_test.go +++ b/cmd/recordfixture/main_test.go @@ -165,3 +165,154 @@ func TestCaptureCodexPlacesForwardedFlagsBeforePrompt(t *testing.T) { t.Fatalf("expected output file: %v", err) } } + +func TestCaptureAgyPlacesForwardedFlagsBeforePromptAndSchemaLast(t *testing.T) { + tmp := t.TempDir() + outPath := filepath.Join(tmp, "out.jsonl") + argsPath := filepath.Join(tmp, "args.txt") + binName := "agy" + script := strings.Join([]string{ + "#!/bin/sh", + "printf '%s\n' \"$@\" > \"$ARGS_FILE\"", + "printf '{\"event\":\"result\",\"result\":{\"status\":\"SUCCESS\"}}\n'", + }, "\n") + if runtime.GOOS == "windows" { + binName = "agy.cmd" + script = strings.Join([]string{ + "@echo off", + "setlocal", + "if exist \"%ARGS_FILE%\" del \"%ARGS_FILE%\"", + ":loop", + "if \"%~1\"==\"\" goto done", + ">> \"%ARGS_FILE%\" echo(%~1", + "shift", + "goto loop", + ":done", + "echo {\"event\":\"result\",\"result\":{\"status\":\"SUCCESS\"}}", + }, "\r\n") + } + binPath := filepath.Join(tmp, binName) + if err := os.WriteFile(binPath, []byte(script), 0o755); err != nil { + t.Fatalf("write fake agy: %v", err) + } + + t.Setenv("ARGS_FILE", argsPath) + err := captureAgy(t.Context(), binPath, + []string{"--model", "gemini-flash"}, + "prompt text", + outPath, + []string{"--json-schema", `{"type":"object"}`}, + ) + if err != nil { + t.Fatalf("captureAgy: %v", err) + } + + argvRaw, err := os.ReadFile(argsPath) + if err != nil { + t.Fatalf("read argv: %v", err) + } + argv := strings.Split(strings.TrimSpace(string(argvRaw)), "\n") + for i := range argv { + argv[i] = strings.TrimSuffix(argv[i], "\r") + } + schemaArg := `{"type":"object"}` + if runtime.GOOS == "windows" { + // The fake agent is a .cmd batch script: Go escapes the embedded + // quotes as \" per the Windows command-line convention, and cmd.exe + // %~1 expansion hands those backslashes through verbatim instead of + // undoing them. Real agy parses argv with CommandLineToArgvW and sees + // the unescaped JSON; only this batch harness records the escaped + // form. Flag order is identical on every platform. + schemaArg = `{\"type\":\"object\"}` + } + want := []string{ + "--model", "gemini-flash", "--dangerously-skip-permissions", "--print", + "prompt text", "--json-schema", schemaArg, "--output-format", "stream-json", + } + if !reflect.DeepEqual(argv, want) { + t.Fatalf("argv = %#v, want %#v", argv, want) + } + + data, err := os.ReadFile(outPath) + if err != nil { + t.Fatalf("read captured output: %v", err) + } + if !strings.Contains(string(data), `"event":"result"`) { + t.Fatalf("captured output missing stdout: %q", data) + } +} + +func TestCaptureAgyRejectsErrorThenSuccessResult(t *testing.T) { + tmp := t.TempDir() + outPath := filepath.Join(tmp, "out.jsonl") + if err := os.WriteFile(outPath, []byte("existing fixture\n"), 0o644); err != nil { + t.Fatal(err) + } + + binName := "agy" + script := strings.Join([]string{ + "#!/bin/sh", + "printf '{\"event\":\"result\",\"result\":{\"status\":\"ERROR\"}}\n'", + "printf '{\"event\":\"result\",\"result\":{\"status\":\"SUCCESS\"}}\n'", + }, "\n") + if runtime.GOOS == "windows" { + binName = "agy.cmd" + script = strings.Join([]string{ + "@echo off", + "echo {\"event\":\"result\",\"result\":{\"status\":\"ERROR\"}}", + "echo {\"event\":\"result\",\"result\":{\"status\":\"SUCCESS\"}}", + }, "\r\n") + } + binPath := filepath.Join(tmp, binName) + if err := os.WriteFile(binPath, []byte(script), 0o755); err != nil { + t.Fatalf("write fake agy: %v", err) + } + + err := captureAgy(t.Context(), binPath, nil, "prompt text", outPath, nil) + if err == nil { + t.Fatal("captureAgy() error = nil, want trailing-success capture with an ERROR result rejected") + } + data, err := os.ReadFile(outPath) + if err != nil { + t.Fatal(err) + } + if string(data) != "existing fixture\n" { + t.Fatalf("destination = %q, want existing fixture preserved", data) + } +} + +func TestCaptureAgyRejectsErrorResultWithoutReplacingFixture(t *testing.T) { + tmp := t.TempDir() + outPath := filepath.Join(tmp, "out.jsonl") + if err := os.WriteFile(outPath, []byte("existing fixture\n"), 0o644); err != nil { + t.Fatal(err) + } + + binName := "agy" + script := strings.Join([]string{ + "#!/bin/sh", + "printf '{\"event\":\"result\",\"result\":{\"status\":\"ERROR\"}}\n'", + }, "\n") + if runtime.GOOS == "windows" { + binName = "agy.cmd" + script = "@echo off\r\necho {\"event\":\"result\",\"result\":{\"status\":\"ERROR\"}}\r\n" + } + binPath := filepath.Join(tmp, binName) + if err := os.WriteFile(binPath, []byte(script), 0o755); err != nil { + t.Fatalf("write fake agy: %v", err) + } + + if err := captureAgy(t.Context(), binPath, nil, "prompt text", outPath, nil); err == nil { + t.Fatal("captureAgy() error = nil, want ERROR result rejection") + } + data, err := os.ReadFile(outPath) + if err != nil { + t.Fatal(err) + } + if string(data) != "existing fixture\n" { + t.Fatalf("destination = %q, want existing fixture preserved", data) + } + if _, err := os.Stat(outPath + ".recording"); !os.IsNotExist(err) { + t.Fatalf("staging file error = %v, want missing staging file", err) + } +} diff --git a/cmd/recordfixture/scrub.go b/cmd/recordfixture/scrub.go index 1fb2503f2..801081416 100644 --- a/cmd/recordfixture/scrub.go +++ b/cmd/recordfixture/scrub.go @@ -34,9 +34,22 @@ func scrubBytes(data []byte) []byte { out = scrubTempDir(out) out = scrubHomeDir(out) out = scrubClaudeHookEvents(out) + out = scrubAgyConversationIDs(out) return out } +// scrubAgyConversationIDs replaces agy's per-run conversation UUIDs with a +// stable placeholder. Nothing downstream reads the value - the fakeagent +// patcher rewrites content and the no-mistakes parser only needs the field +// present - but a recorded UUID identifies the recording session. +var agyConversationPattern = regexp.MustCompile(`("conversation_id"\s*:\s*")[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}(")`) + +const agyConversationPlaceholder = `${1}00000000-0000-4000-8000-000000000000${2}` + +func scrubAgyConversationIDs(data []byte) []byte { + return agyConversationPattern.ReplaceAll(data, []byte(agyConversationPlaceholder)) +} + func scrubHomeDir(data []byte) []byte { u, err := user.Current() if err != nil || u.HomeDir == "" { diff --git a/cmd/recordfixture/scrub_test.go b/cmd/recordfixture/scrub_test.go index 0cc8ca2a5..839a55da1 100644 --- a/cmd/recordfixture/scrub_test.go +++ b/cmd/recordfixture/scrub_test.go @@ -2,6 +2,7 @@ package main import ( "bytes" + "strings" "testing" ) @@ -32,3 +33,27 @@ func TestReplacePathForms_ReplacesEscapedWindowsPaths(t *testing.T) { t.Fatalf("expected escaped temp path preserved under placeholder, got %q", scrubbed) } } + +func TestScrubAgyConversationIDsReplacesUUIDs(t *testing.T) { + line := `{"event":"init","conversation_id":"5f63304d-2c5a-48a3-aa40-6f077ce51686","init":{"cwd":"/tmp/x"}}` + "\n" + + `{"event":"result","result":{"conversation_id":"0198a7b6-c5d4-4e3f-9a2b-123456789abc","status":"SUCCESS"}}` + "\n" + + scrubbed := string(scrubAgyConversationIDs([]byte(line))) + + if strings.Contains(scrubbed, "5f63304d") || strings.Contains(scrubbed, "0198a7b6") { + t.Fatalf("conversation UUIDs survived scrubbing:\n%s", scrubbed) + } + want := `{"event":"init","conversation_id":"00000000-0000-4000-8000-000000000000","init":{"cwd":"/tmp/x"}}` + "\n" + + `{"event":"result","result":{"conversation_id":"00000000-0000-4000-8000-000000000000","status":"SUCCESS"}}` + "\n" + if scrubbed != want { + t.Fatalf("scrubbed =\n%s\nwant\n%s", scrubbed, want) + } +} + +func TestScrubBytesAppliesAgyConversationIDs(t *testing.T) { + data := []byte(`{"result":{"conversation_id":"11111111-2222-4333-8444-555555555555"}}`) + got := string(scrubBytes(data)) + if strings.Contains(got, "11111111") { + t.Fatalf("scrubBytes did not apply conversation-id scrubbing: %s", got) + } +} diff --git a/docs/src/content/docs/concepts/auto-fix.md b/docs/src/content/docs/concepts/auto-fix.md index 0f7ec5722..e98a0cdee 100644 --- a/docs/src/content/docs/concepts/auto-fix.md +++ b/docs/src/content/docs/concepts/auto-fix.md @@ -39,45 +39,18 @@ Unresolved documentation findings and unresolved blocking lint findings pause fo The CI step has one cheaper option than a fix round, and it tries it first. -<<<<<<< HEAD -<<<<<<< HEAD -A check the provider reports as `cancelled` is the provider telling you about itself, not about your commit. Handing that to the fix agent spends an agent round reading a run that never tested anything, and the fix it invents edits code that was never broken. So when every terminally failed check on the pull request is cancelled and the configured budget authorizes a rerun, the CI step asks the provider to run those checks again for the same commit and keeps polling. -======= -A check the provider reports as `cancelled` is the provider telling you about itself, not about your commit. Handing that to the fix agent spends an agent round reading a run that never tested anything, and the fix it invents edits code that was never broken. So when every terminally failed check on the pull request is cancelled, the CI step asks the provider to run those checks again for the same commit and keeps polling. ->>>>>>> 1753783 (feat(pipeline): re-run provider-cancelled CI checks before escalating (#595)) -======= -A check the provider reports as `cancelled` is the provider telling you about itself, not about your commit. Handing that to the fix agent spends an agent round reading a run that never tested anything, and the fix it invents edits code that was never broken. So when every terminally failed check on the pull request is cancelled and the configured budget authorizes a rerun, the CI step asks the provider to run those checks again for the same commit and keeps polling. ->>>>>>> eb826b4 (fix(pipeline): stop polling terminal cancelled CI checks (#637)) +A check whose failure the provider attributes to itself rather than to your commit, such as a cancellation, is the provider telling you about itself. On GitHub, a positive transient rerun budget also enables structural detection of jobs that failed before any repository step ran. Handing a detected provider failure to the fix agent spends an agent round reading a run that never tested anything, and the fix it invents edits code that was never broken. So when every terminally failed check on the pull request is one of those and the configured budget authorizes a rerun, the CI step asks the provider to run those checks again for the same commit and keeps polling. That deterministic rerun sits strictly before the agent rounds described above: 1. Every check finishes and at least one has failed. -<<<<<<< HEAD -<<<<<<< HEAD -2. If all of those failures are cancelled checks, the pull request has no merge conflict, and the configured budget authorizes it, each one is re-run and the monitor keeps polling. No `auto_fix.ci` attempt is consumed. -3. When cancellation is the only remaining issue, a check with no authorized or outstanding rerun pauses for a decision without consuming an `auto_fix.ci` attempt. +2. If all of those failures are provider-attributed checks, the pull request has no merge conflict, and the configured budget authorizes it, each one is re-run and the monitor keeps polling. No `auto_fix.ci` attempt is consumed. +3. When such a detected provider-attributed outcome is the only remaining issue, a check with no authorized or outstanding rerun pauses for a decision without consuming an `auto_fix.ci` attempt. 4. Every other failure escalates into the `auto_fix.ci` loop on its first observation. [`ci.rerun_transient`](/no-mistakes/reference/repo-config/#cirerun_transient) owns the budget, the exact classification, and every case that skips the rerun. -Nothing that survives a rerun falls into the agent loop either. A check the provider cancels again is still not a verdict on the code, so it pauses for a decision instead of spending a fix round on a run that never tested anything. A cancellation no rerun is going to replace - the default budget is `0` - reaches that same decision directly: the provider has published its conclusion and will not replace it, so waiting on it would never end. A rerun costs another CI run of that job, so the budget is deliberately small and is spent when the rerun is requested, which bounds the loop by construction. Each rerun is announced in the step log, so a run that is waiting on one says so instead of looking stalled. Reruns never cross a head change: if the published branch head no longer matches the commit the run delivered, the step pauses with the expected and observed commits rather than re-running checks against a revision it never produced. -======= -2. If all of those failures are cancelled checks and the pull request has no merge conflict, each one is re-run and the monitor keeps polling. No `auto_fix.ci` attempt is consumed. -3. Otherwise the failure escalates into the `auto_fix.ci` loop exactly as it always has, on its first observation. - -[`ci.rerun_transient`](/no-mistakes/reference/repo-config/#cirerun_transient) owns the budget, the exact classification, and every case that skips the rerun. - -Nothing that survives a rerun falls into the agent loop either. A check the provider cancels again is still not a verdict on the code, so it pauses for a decision instead of spending a fix round on a run that never tested anything. A rerun costs another CI run of that job, so the budget is deliberately small and is spent when the rerun is requested, which bounds the loop by construction. Each rerun is announced in the step log, so a run that is waiting on one says so instead of looking stalled. Reruns never cross a head change: if the published branch head no longer matches the commit the run delivered, the step pauses with the expected and observed commits rather than re-running checks against a revision it never produced. ->>>>>>> 1753783 (feat(pipeline): re-run provider-cancelled CI checks before escalating (#595)) -======= -2. If all of those failures are cancelled checks, the pull request has no merge conflict, and the configured budget authorizes it, each one is re-run and the monitor keeps polling. No `auto_fix.ci` attempt is consumed. -3. When cancellation is the only remaining issue, a check with no authorized or outstanding rerun pauses for a decision without consuming an `auto_fix.ci` attempt. -4. Every other failure escalates into the `auto_fix.ci` loop on its first observation. - -[`ci.rerun_transient`](/no-mistakes/reference/repo-config/#cirerun_transient) owns the budget, the exact classification, and every case that skips the rerun. - -Nothing that survives a rerun falls into the agent loop either. A check the provider cancels again is still not a verdict on the code, so it pauses for a decision instead of spending a fix round on a run that never tested anything. A cancellation no rerun is going to replace - the default budget is `0` - reaches that same decision directly: the provider has published its conclusion and will not replace it, so waiting on it would never end. A rerun costs another CI run of that job, so the budget is deliberately small and is spent when the rerun is requested, which bounds the loop by construction. Each rerun is announced in the step log, so a run that is waiting on one says so instead of looking stalled. Reruns never cross a head change: if the published branch head no longer matches the commit the run delivered, the step pauses with the expected and observed commits rather than re-running checks against a revision it never produced. ->>>>>>> eb826b4 (fix(pipeline): stop polling terminal cancelled CI checks (#637)) +Nothing that survives a rerun falls into the agent loop either. A check the provider cancels again, or a detected GitHub setup failure that persists after its budget, is still not a verdict on the code, so it pauses for a decision instead of spending a fix round on a run that never tested anything. A cancellation no rerun is going to replace, including at the default budget of `0`, reaches that same decision directly: the provider has published its conclusion and will not replace it, so waiting on it would never end. A rerun costs another provider-side workflow run, so the budget is deliberately small and is spent when the rerun is requested, which bounds the loop by construction. Each rerun is announced in the step log, so a run that is waiting on one says so instead of looking stalled. Reruns never cross a head change: if the published branch head no longer matches the commit the run delivered, the step pauses with the expected and observed commits rather than re-running checks against a revision it never produced. ## Configuration @@ -114,16 +87,15 @@ When the pipeline pauses for approval, you can manually trigger a fix from the T 3. Optionally press `e` to attach a note to the current finding, or `+` to add your own finding to the fix request 4. Press `f` to fix the selected findings -The agent receives the merged fix payload for that round: the selected agent findings, any per-finding user notes, any selected user-authored findings added from the TUI or AXI interface, and a sanitized history of previous rounds for that step. -That history includes which finding IDs were selected for a prior fix attempt, which findings were left unselected by the user, and any one-line summaries from earlier fix commits. -On follow-up review passes, that history tells the agent not to re-report user-ignored findings unless the code now presents a materially different issue. +The agent receives the merged fix payload for that round: the selected agent findings, any per-finding user notes, any selected user-authored findings added from the TUI or AXI interface, and the shared [finding decision history](/no-mistakes/reference/pipeline-steps/#finding-decision-history). +The current step's part of that history also includes one-line summaries from earlier fix commits. After a user-triggered fix, the step re-runs and pauses again to show you the results (`fix_review` status). You can then approve, fix again, skip, or abort. Yolo and AXI `--yes` approve that fix review automatically after their one fix round, so a finding that remains after the fix does not trigger an unbounded fix loop. ## Fix commits -When the Review, Test, Document, or Lint step commits auto-fix changes, its subject comes from `commit.fix_message`. +When the Review, Test, Document, Lint, or CI step commits auto-fix changes, its subject comes from `commit.fix_message`. The [global config reference](/no-mistakes/reference/global-config/#commitfix_message) owns the template syntax, default, validation rules, size limits, and supported placeholders; the [repo config reference](/no-mistakes/reference/repo-config/#commitfix_message) owns the repository override and trust behavior. The pipeline validates the template, agent summary, predicted output size, and final rendered subject before `git add -A`, so a rejected value does not leave changes staged. The combined document-and-lint housekeeping pass runs in the Document step, so its documentation and safe lint fixes use the Document value for `{{.Step}}`; configured-command lint fixes use the Lint value. @@ -131,8 +103,8 @@ The combined document-and-lint housekeeping pass runs in the Document step, so i Before a step-specific fix commit, the pipeline verifies that the live worktree HEAD still descends from the head recorded after its previous commit. It allows a legitimate forward commit made by an agent, but aborts the run if an out-of-band backward or divergent reset would drop the reviewed history. -The template does not control commits created by the Rebase, CI, or Push steps. -The CI step uses `no-mistakes: apply CI fixes`, and the Push step uses `no-mistakes: apply agent fixes` for remaining uncommitted changes. +The template does not control commits created by the Rebase or Push steps. +The Push step uses `no-mistakes: apply agent fixes` for remaining uncommitted changes. ## Step rounds diff --git a/docs/src/content/docs/concepts/daemon.md b/docs/src/content/docs/concepts/daemon.md index 6259b966d..f67512a5e 100644 --- a/docs/src/content/docs/concepts/daemon.md +++ b/docs/src/content/docs/concepts/daemon.md @@ -82,7 +82,7 @@ As an independent safety layer, the daemon also refuses to bind the Unix socket When a push arrives via the post-receive hook: -1. Creates a detached worktree at `~/.no-mistakes/worktrees///` +1. Creates a detached worktree at `~/.no-mistakes/worktrees///`, or at `/` when [`worktree_roots`](/no-mistakes/reference/global-config/#worktree_roots) names a directory for that repository. The placement is resolved once, at run creation, and recorded on the run, so editing the setting never retargets a run that already exists 2. Starts the pipeline executor in that worktree 3. Streams events to any connected TUI clients and serves request/response state to AXI clients 4. Cleans up the worktree when the run finishes (success or failure) @@ -95,7 +95,7 @@ While executing steps, the daemon also owns child-process cleanup. Configured commands and one-shot agent subprocesses are terminated as a process tree on completion, failure, or cancellation so leaked test workers, build watchers, or dev servers cannot accumulate across runs. Each process is asked to exit first and only forcibly killed if it is still running a few seconds later. A process can still escape that tree by detaching itself into its own session, so when a run finishes the daemon also terminates anything still standing in that run's worktree before removing the directory. -That sweep is scoped by working directory: it never touches a worktree whose run is still active, and it can never reach a process working outside `~/.no-mistakes/worktrees/`. +That sweep is scoped by working directory: it never touches a worktree whose run is still active, and it can never reach a process working outside `~/.no-mistakes/worktrees/` or outside a run worktree a run record names in a configured worktree root. ## Concurrent push handling @@ -116,11 +116,14 @@ On startup, the daemon checks for runs that were left in `pending` or `running` - Completes legacy active rows whose persisted PR state is already `merged` or `closed`, including their CI step, before active-run recovery and parked-run planning - Resumes only fully recorded parked approval gates whose worktree and step history can be validated; incomplete or ambiguous active runs fail closed +- Re-resolves and validates any configured repository forge profile before rebuilding the recovered run, so resumed provider checks and agents use the same repository-scoped identity model rather than persisted credentials or ambient active accounts - Before resuming a parked CI gate, re-checks its persisted PR URL through the configured provider; a currently merged or closed PR completes the stale gate, while an open, unknown, or unreachable PR remains parked +- Preserves a run that was actively monitoring CI for an already-created PR as `ci_monitor_interrupted` rather than failing it: the PR is still open, so a restart mid-monitor is not a pipeline failure. That run is terminal and never resumed +- Before failing any other stale active run, verifies its managed worktree head and pins an unpublished descendant under the run-specific recovery ref so later rerun or guarded custody recovery does not fall back to a stale gate branch - Marks every other stale active run as `failed` with the message "daemon crashed during execution" - Reaps orphaned managed agent servers left behind by a crashed daemon or setup wizard - Terminates processes a crashed daemon left running in worktrees no run owns any more, using the same working-directory scoping as run cleanup plus a ten-minute age floor so a run starting concurrently with startup is never mistaken for a leak -- Removes orphaned worktree directories via `git worktree remove --force` - but never one whose run is still `pending` or `running`; only leftovers from terminal runs or directories with no matching run record are removed +- Removes orphaned worktree directories via `git worktree remove --force` - but never one whose run is still `pending` or `running`; under `~/.no-mistakes/worktrees/` that means leftovers from terminal runs plus directories with no matching run record, while in a [configured worktree root](/no-mistakes/reference/global-config/#worktree_roots) only the directories run records name are ever swept or removed. A `ci_monitor_interrupted` worktree is also kept when its checked-out commit differs from the run's last pushed commit, since it may still hold an unpushed CI auto-fix commit - Migrates gates named by authoritative repository records, plus legacy directories with the strict `.git` shape. Before changing an unstamped candidate, it validates that the directory is a bare repository without relying on the current directory or ancestor Git discovery; unrelated and malformed directories are rejected without hook or Git mutation - For a validated legacy gate, installs or refreshes the no-mistakes-managed pre-receive admission and post-receive notification hooks, preserving an existing custom pre-receive hook behind the admission wrapper, then enables push-option support and reapplies per-worktree hook-path isolation - Records a content-versioned gate configuration stamp only after the whole migration succeeds. Normal restarts check current stamped gates from the filesystem without rerunning the mutating Git commands diff --git a/docs/src/content/docs/concepts/gate-model.md b/docs/src/content/docs/concepts/gate-model.md index ff67d3fac..695c06d39 100644 --- a/docs/src/content/docs/concepts/gate-model.md +++ b/docs/src/content/docs/concepts/gate-model.md @@ -71,7 +71,7 @@ That is a core design choice, not an implementation detail. - **Named remote** - `origin` is never hijacked. You push to `no-mistakes` on purpose, so regular `git push` still works normally. - **Recursive-run containment** - managed gate identity and authenticated daemon peer ancestry prevent active validation steps from starting or controlling another pipeline. `NO_MISTAKES_GATE` is diagnostic evidence only, not authorization. -- **Disposable worktrees** - each run happens in its own detached worktree under `~/.no-mistakes/worktrees/`. The daemon can safely modify files, run tests, and commit fixes without touching your working directory. +- **Disposable worktrees** - each run happens in its own detached worktree, under `~/.no-mistakes/worktrees/` by default or under the directory [`worktree_roots`](/no-mistakes/reference/global-config/#worktree_roots) names for that repository. The daemon can safely modify files, run tests, and commit fixes without touching your working directory. - **Fixed pipeline** - the step order is opinionated and not configurable: `intent → rebase → review → test → document → lint → push → pr → ci`. What you _can_ configure is the commands each step runs, how many auto-fix attempts are allowed, and whether transcript-based intent extraction is used when intent is not supplied directly. - **Remote data-loss guard** - force-pushes are checked against the live push target and refused when they would discard commits the run did not incorporate. @@ -207,7 +207,7 @@ Everything lives under `~/.no-mistakes/` by default. Set `NM_HOME` to relocate i | `servers/` | PID-tracking records for managed agent servers | | `repos/.git` | Bare gate repos | | `repos/.git/notify-push.log` | Persistent hook notification failure log | -| `worktrees///` | Disposable worktrees (cleaned up after each run) | +| `worktrees///` | Disposable worktrees (cleaned up after each run); [`worktree_roots`](/no-mistakes/reference/global-config/#worktree_roots) can place a repository's elsewhere | | `logs//.log` | Per-step log files | | `logs/daemon.log` | Bounded daemon lifecycle log | | `logs/daemon-bootstrap.log` | Bounded pre-logger bootstrap and direct crash output | diff --git a/docs/src/content/docs/concepts/pipeline.md b/docs/src/content/docs/concepts/pipeline.md index edaa1df24..e6ea1527f 100644 --- a/docs/src/content/docs/concepts/pipeline.md +++ b/docs/src/content/docs/concepts/pipeline.md @@ -56,7 +56,7 @@ The pipeline is opinionated so that "passed the gate" has a stable meaning: - **Document after test** so docs are updated against code that's known to work. - **Lint last among local checks** so it doesn't churn over code that may still change. - **Push → PR → CI** happens after all local checks pass. - The push and CI auto-fix paths refuse to overwrite commits that reached the configured push target out of band. + A CI repair restarts the pipeline at Review, so the repaired commit passes the local checks before the Push step publishes it through the same overwrite protection. CI is the only step that talks to the outside world for validation. ## What each step can do diff --git a/docs/src/content/docs/guides/agents.md b/docs/src/content/docs/guides/agents.md index 0cb54851b..4942af8e6 100644 --- a/docs/src/content/docs/guides/agents.md +++ b/docs/src/content/docs/guides/agents.md @@ -18,8 +18,8 @@ code review, evidence-oriented test validation, test or lint detection when you have not configured explicit commands, auto-fixing, and setup-wizard suggestions when you leave prompts blank. -Pipeline agent prompts also include a workspace-boundary preamble. -It tells agents to keep intentional source, project, user-data, and system file writes inside the disposable worktree, avoid mutating system state such as Homebrew packages, `/Applications`, or global tool config, and treat that boundary as prompt steering rather than true enforcement. +Pipeline agent prompts also include a workspace-boundary preamble and an execution-context section with the exact worktree directory and path contract. +It tells agents to keep intentional source, project, user-data, and system file writes inside the disposable worktree, use that exact path prefix when tools require absolute paths without guessing or re-resolving paths, avoid mutating system state such as Homebrew packages, `/Applications`, or global tool config, and treat that boundary as prompt steering rather than true enforcement. The only intentional out-of-worktree write it allows is test evidence under the run's managed evidence directory when a testing prompt asks for it. Incidental temp or cache writes from normal development tools are still allowed. Testing prompts also ask agents to remove transient working-tree artifacts they created, such as downloaded models, caches, build outputs, large binaries, or generated data directories, before reporting completion. @@ -43,6 +43,8 @@ That directory is always outside the worktree and is reaped by no-mistakes on a | --- | --- | --- | | Claude | `claude` | Subprocess per invocation, JSONL streaming | | Codex | `codex` | Subprocess per invocation, JSONL events | +| Grok Build | `grok` | Subprocess per invocation, Messages-compatible JSONL streaming | +| Antigravity | `agy` | Subprocess per invocation, NDJSON `stream-json` events | | Rovo Dev | `acli` | Persistent HTTP server, SSE streaming | | OpenCode | `opencode` | Persistent HTTP server, SSE streaming | | Pi | `pi` | Subprocess per invocation, JSONL events | @@ -111,7 +113,7 @@ Repo config takes precedence over global config. ```yaml # ~/.no-mistakes/config.yaml or .no-mistakes.yaml -agent: [codex, claude] +agent: [codex, grok] ``` ### Optional ACP target @@ -140,8 +142,7 @@ Use bare `/no-mistakes` to validate existing committed work. Use `/no-mistakes ` to have the agent first do the task, commit only that task's changes on a feature branch, then run the pipeline with the task text as `--intent`. In both modes, it resolves low-risk findings on its own and stops to relay anything that needs your decision. -`no-mistakes init` installs that skill at user level: `~/.claude/skills/no-mistakes/SKILL.md` for Claude Code and `~/.agents/skills/no-mistakes/SKILL.md` for Codex, OpenCode, Rovo Dev, and Pi. -One install makes the skill available to every supported agent in every repo, without committing tool-generated files to any repo. +Grok Build is a pipeline runner, not a driving-skill target. `no-mistakes init` installs the `/no-mistakes` skill for Claude Code and agents that use the vendor-neutral `.agents` convention; the [`init` reference](/no-mistakes/reference/cli/#no-mistakes-init) owns its locations and supported consumers. If your home directory consolidates `.claude` and `.agents` with symlinks, `init` follows the links and keeps the skill reachable from both logical paths. Re-run `no-mistakes init` after an upgrade to refresh that skill, including overwriting stale `SKILL.md` content from an older binary. Older versions vendored the skill into each initialized repo's `.claude/skills` and `.agents/skills`; those copies are no longer needed, and `init` prints a notice when it finds one so you can remove it. @@ -152,7 +153,7 @@ Successful outcomes also instruct the agent to summarize the run for the user. When the pipeline applied fixes, successful outcomes include a `fixes` table listing each fix so the agent can acknowledge what it missed and the user can review them. If that PR later falls behind the default branch or hits a merge conflict - commonly because another PR merged first - the agent runs no command and must never hand-rebase. -The CI monitor stays live in the background after checks pass, and when it sees an actual conflict it rebases onto the base, resolves it, and re-pushes the branch itself, so no agent or user action is needed. +The CI monitor stays live in the background after checks pass, and when it sees an actual conflict it rebases onto the base, resolves it, restarts validation at Review, and re-pushes the branch through Push, so no agent or user action is needed. A PR that is merely behind but still clean needs nothing either, since the platform merges it. The one exception is when that monitor is no longer running - the PR was closed, the run was aborted or superseded, it idle-timed-out, or its auto-fix attempts were exhausted - in which case the agent recovers with `no-mistakes rerun`, which cancels the stale monitor and re-runs the full pipeline including a deterministic rebase step. The agent must not use `no-mistakes axi run` to refresh a still-active PR: after `checks-passed` it reattaches to the running monitor with HEAD unchanged and returns the monitor output without rebasing. @@ -191,17 +192,18 @@ The [CLI reference](/no-mistakes/reference/cli/) documents each `axi` command an When the daemon is running through a managed service, its `PATH` comes from your login shell environment on macOS and Linux plus common user, Homebrew, and system binary directories; on Windows it reuses the current process environment. If native agent discovery does not resolve the binary you expect, check `~/.no-mistakes/logs/daemon.log` and set an explicit override; [Environment the daemon sees](/no-mistakes/reference/environment/#environment-the-daemon-sees) owns the full resolution story. -Five global config fields tune resolution and invocation, and the [Global Config Reference](/no-mistakes/reference/global-config/) owns each one: +Six global config fields tune resolution and invocation, and the [Global Config Reference](/no-mistakes/reference/global-config/) owns each one: - [`agent_path_override`](/no-mistakes/reference/global-config/#agent_path_override) - custom binary paths per native agent, plus the default native binary-name table. -- [`agent_args_override`](/no-mistakes/reference/global-config/#agent_args_override) - extra CLI flags per native agent for model selection, service tier, reasoning depth, or permission mode, including the reserved-flag rules and smart defaults. Keep it global-only; it reflects your local agent setup rather than repo policy. +- [`agent_config`](/no-mistakes/reference/global-config/#agent_config) - model and reasoning effort per agent in one common spelling, mapped down to each harness's own mechanism, with the full per-harness mapping table and the precedence rule against raw flags. +- [`agent_args_override`](/no-mistakes/reference/global-config/#agent_args_override) - extra CLI flags per native agent for anything `agent_config` does not cover, such as service tier or permission mode, including the reserved-flag rules and smart defaults. Keep both global-only; they reflect your local agent setup rather than repo policy. - [`acpx_path`](/no-mistakes/reference/global-config/#acpx_path) - the bridge binary path for explicit ACP targets and first-class ACP aliases. - [`acp_registry_overrides`](/no-mistakes/reference/global-config/#acp_registry_overrides) - raw ACP target commands, including replacements for alias defaults such as `cursor-agent acp`, plus their availability-probing rules. - [`agent`](/no-mistakes/reference/global-config/#agent) - the `auto` resolution order and ordered fallback-list semantics. ## Review session reuse -With the default `session_reuse: true`, Claude and Codex keep one durable review-fixer session per run, and resume failures fall back to a fresh fixer session instead of skipping the fix turn. +With the default `session_reuse: true`, Claude, Codex, Grok, Pi, and Antigravity keep one durable review-fixer session per run, and resume failures fall back to a fresh fixer session instead of skipping the fix turn. Pi stores its native fixer transcript in Pi's session directory; no-mistakes persists only the minimum session identity needed to resume it. Review turns always run in fresh, session-free invocations: a rereview certifies fixes that implement the previous review turn's findings, so it must never resume the session that prescribed them. The [`session_reuse` field reference](/no-mistakes/reference/global-config/#session_reuse) owns the exact reuse, fallback, privacy, and restart-recovery semantics. @@ -209,7 +211,7 @@ The [`session_reuse` field reference](/no-mistakes/reference/global-config/#sess All agents implement the same interface. Each invocation receives: -- **Prompt** - the task description (review this diff, fix these findings, etc.), prefixed during pipeline runs with the workspace-boundary steering described above +- **Prompt** - the task description (review this diff, fix these findings, etc.), prefixed during pipeline runs with the workspace-boundary steering and exact execution-context contract described above - **CWD** - the worktree directory - **Environment** - the daemon environment plus non-interactive Git overrides (`GIT_EDITOR=true`, `GIT_SEQUENCE_EDITOR=true`, and `GIT_TERMINAL_PROMPT=0`) so agent-invoked Git commands do not hang on editors or credential prompts - **JSONSchema** - optional structured output schema for typed responses @@ -226,12 +228,14 @@ Each invocation returns: - **SessionID** and **Resumed** - the adapter-native session identity and whether this invocation resumed it, when supported - **Model** and **Provider** - adapter-reported serving metadata when available -One-shot subprocess agents (Claude, Codex, Pi, Copilot CLI, and acpx) are invocation-scoped. +When structured output comes from final text, no-mistakes validates JSON fences and concluding bare JSON objects extracted from prose against the requested schema. It accepts inline or unclosed JSON fence forms, but rejects multiple valid candidates and fails closed when fenced and bare candidates differ; semantically identical fenced and bare candidates are accepted. A bare object followed by substantive prose is not treated as a verdict. + +One-shot subprocess agents (Claude, Codex, Grok, Pi, Copilot CLI, Antigravity, and acpx) are invocation-scoped. After no-mistakes starts one, it terminates any remaining child processes when the invocation exits, fails, or is cancelled, so agent-spawned test workers, build watchers, and dev servers do not survive the step. Step logs record their process lifecycle, including start and exit lines with the PID, and AXI status exposes that PID while the subprocess is still active. Persistent server agents (Rovo Dev and OpenCode) use their managed server lifecycle instead. -Transient API and network failures are retried up to three times with exponential backoff. Retry messages are recorded as lifecycle activity for native subprocess agents, falling back to the streaming text path for direct callers that do not supply `OnLifecycle`. +Transient API and network failures, stochastic prose turn endings, and transient tool-call or permission validation errors are retried up to three times with exponential backoff. Provider quota and free-usage-limit errors are terminal, even when a backend marks them retryable. Retry messages are recorded as lifecycle activity for native subprocess agents, falling back to the streaming text path for direct callers that do not supply `OnLifecycle`. ## Intent extraction @@ -244,7 +248,7 @@ Transcript readers collect user and assistant text messages but exclude tool cal They read Claude Code transcripts from `~/.claude/projects`, Codex metadata from `~/.codex/state_*.sqlite` plus referenced rollout files, OpenCode messages from `$XDG_DATA_HOME/opencode/opencode.db` or `~/.local/share/opencode/opencode.db`, Rovo Dev sessions from `~/.rovodev/sessions`, Pi transcripts from `~/.pi/agent/sessions`, and GitHub Copilot CLI sessions from `~/.copilot/session-state`. Sessions are eligible when they come from the same working directory or an equivalent Git checkout with the same common Git directory or normalized remote URL. ACP transcripts are not currently read for intent extraction. -When deterministic matching leaves multiple plausible sessions, no-mistakes may ask the configured pipeline agent to choose among them using the matching file paths and sanitized transcript packet files. +When deterministic matching leaves multiple plausible sessions, no-mistakes may ask the configured pipeline agent to choose among them using the matching file paths and sanitized transcript packet files. That disambiguation prompt receives the same exact execution-worktree path contract as other pipeline prompts; transcript packets remain sanitized data, not instructions. The selected transcript text is then sent to the configured pipeline agent for summarization during the `intent` step, so intent extraction may incur additional agent or API invocations. Before disambiguation or summarization, no-mistakes excludes tool output, redacts likely secrets, strips common prompt-control markers, and clamps long transcripts while preserving the beginning and end. no-mistakes stores derived intent summaries and matching metadata in `~/.no-mistakes/state.sqlite`, including the source, session ID, and match score on each run plus cached summaries for matching transcript sessions. @@ -260,33 +264,46 @@ For review-fixer reuse, Claude starts a stream-json session and resumes it with ## Codex -Spawns a `codex` subprocess for each invocation with `exec --json`. When structured output is requested, no-mistakes also writes a normalized schema file and passes it with `--output-schema`. By default it also adds `--dangerously-bypass-approvals-and-sandbox`, unless you already set your own Codex approval or sandbox flag through `agent_args_override`. Reads JSONL events. Structured output is returned from the final `agent_message` text, with fallback parsing that accepts JSON fences, inline fence markers, or a final bare JSON object after prose, then validates the result against the normalized schema. -Codex model and config overrides, such as `-m gpt-5.4`, `-c service_tier="priority"`, or `-c model_reasoning_effort="low"`, belong in global `agent_args_override.codex`. +Spawns a `codex` subprocess for each invocation with `exec --json`. When structured output is requested, no-mistakes also writes a normalized schema file and passes it with `--output-schema`. By default it also adds `--dangerously-bypass-approvals-and-sandbox`, unless you already set your own Codex approval or sandbox flag through `agent_args_override`. Reads JSONL events. Structured output is returned from the final `agent_message` text and uses the common text fallback described above when needed. +Codex model and reasoning effort belong in global [`agent_config.codex`](/no-mistakes/reference/global-config/#agent_config), which renders them as `-m` and `-c model_reasoning_effort`. Other config overrides, such as `-c service_tier="priority"`, belong in `agent_args_override.codex`. For review-fixer reuse, Codex resumes the reported thread with `codex exec resume `. That resume command has a narrower flag surface than `codex exec`, so a resume that rejects an override falls back to a fresh fixer session rather than skipping the fix turn. +## Grok Build + +Spawns a `grok` subprocess for each invocation using a permission-restricted prompt file and `--output-format streaming-messages-json`. Native structured output is requested with `--json-schema`; the terminal `structured_output`, session identity, model, and usage fields are read from the Messages-compatible result event. Review-loop reuse resumes the reported Grok session with `--resume`. +Without an explicit pin, Grok uses its current configured default model. See [`agent_config`](/no-mistakes/reference/global-config/#agent_config) for model and effort configuration and native mapping. +Grok is not available to a repository with `disable_project_settings: true`, because Grok 1.0.5 still discovers native project instructions and `.grok` project surfaces; the gate fails closed before launch. See [`disable_project_settings`](/no-mistakes/reference/repo-config/#disable_project_settings) for the security boundary. System-prompt, alternate-agent, working-directory, worktree, and restore flags are reserved so global overrides cannot redirect the managed invocation. + +## Antigravity + +Spawns an `agy` subprocess for each invocation with `--print --output-format stream-json`, plus `--dangerously-skip-permissions` (always present; `agent_args_override` cannot suppress it). Reads NDJSON events from stdout, streaming `step_update` text deltas to the TUI. Structured output is requested with a temporary `--json-schema` file and read from the terminal result's `structured_output`. Result precedence is terminal-authoritative: `structured_output` outranks the terminal result's `response`, which outranks the streamed deltas. +For review-fixer reuse, Antigravity resumes the reported conversation with `--conversation `. A pruned or unknown conversation id starts a fresh conversation instead of failing the turn. +Usage accounting includes agy's reported `thinking_tokens` as reasoning tokens. +`--conversation`, `-c`/`--continue`, the print/output flags, and the permission flag are reserved so global overrides cannot redirect the managed invocation. + ## Rovo Dev -Starts a persistent HTTP server (`acli rovodev serve`) on first use and reuses it across invocations. If a reused server refuses a connection, no-mistakes discards it and retries with a fresh server. Any `agent_args_override.rovodev` flags are inserted before no-mistakes' managed serve flags. Communicates via REST API and SSE streaming. Each invocation creates a session, sends the prompt, streams results, then deletes the session. Structured output is handled by injecting schema instructions into a system prompt, then parsing the final text with fallback parsing that accepts JSON fences, inline fence markers, or a final bare JSON object after prose, and validates the result against the requested schema while allowing `null` for optional fields. +Starts a persistent HTTP server (`acli rovodev serve`) on first use and reuses it across invocations. If a reused server refuses a connection, no-mistakes discards it and retries with a fresh server. Any `agent_args_override.rovodev` flags are inserted before no-mistakes' managed serve flags. Neither the serve command nor the REST session API takes a model or reasoning parameter, so no-mistakes cannot map those knobs for Rovo Dev; the [`agent_config`](/no-mistakes/reference/global-config/#agent_config) reference owns the refusal and escape-hatch semantics. Communicates via REST API and SSE streaming. Each invocation creates a session, sends the prompt, streams results, then deletes the session. Structured output is handled by injecting schema instructions into a system prompt, then parsing the final text with the common fallback described above while allowing `null` for optional fields. ## OpenCode -Starts a persistent HTTP server (`opencode serve`) on first use and reuses it across invocations. If a reused server refuses a connection, no-mistakes discards it and retries with a fresh server. Any `agent_args_override.opencode` flags are inserted before no-mistakes' managed serve flags. Similar session lifecycle to Rovo Dev: create session, send message, stream SSE events until idle, delete session. Supports `json_schema` format in the message request for structured output, with `retryCount: 2` so the model gets a second chance to emit a structured response. When opencode reports `info.error.name = "StructuredOutputError"` (the model did not call the StructuredOutput tool after those retries), no-mistakes surfaces a clean error including the retry count rather than falling through to text-parsing the streamed reasoning prose. When native structured output is genuinely absent, it falls back to parsing the final text with the same JSON fence and bare-object fallback, validating that fallback result against the requested schema while allowing `null` for optional fields. +Starts a persistent HTTP server (`opencode serve`) on first use and reuses it across invocations. If a reused server refuses a connection, no-mistakes discards it and retries with a fresh server. Any `agent_args_override.opencode` flags are inserted before no-mistakes' managed serve flags; `opencode serve` exits with usage on an unknown flag, so a model flag does not belong there. Model and reasoning effort come from [`agent_config.opencode`](/no-mistakes/reference/global-config/#agent_config) and travel in the session message as `model` (from the `provider/model` form) and `variant`. Similar session lifecycle to Rovo Dev: create session, send message, stream SSE events until idle, delete session. Supports `json_schema` format in the message request for structured output, with `retryCount: 2` so the model gets a second chance to emit a structured response. When a provider explicitly rejects the required or forced tool choice used by that format because thinking or reasoning is enabled, no-mistakes retries once without the native format; the schema remains in the prompt, and the returned text must parse and validate against the original schema. Other provider errors do not trigger this retry, and neither does a conflict reported after the turn already invoked a tool, since that retry runs in a fresh session and would replay the tool. A turn that fails reports its cause on `info.error` with an HTTP 200, so no-mistakes fails the invocation on any `info.error` and surfaces the provider's own name, status, and message rather than falling through to the streamed reasoning prose. `StructuredOutputError` (the model did not call the StructuredOutput tool after those retries) keeps its own wording including the retry count. A failed turn is retried only when opencode marks it retryable and the turn ran no tools: a provider blip that kills the turn before the model acts costs a retry, while a request the provider rejected as invalid fails immediately with an actionable message, and a turn that already invoked a tool is never repeated because each attempt runs in a fresh session and would replay that tool's side effects. When native structured output is genuinely absent, it falls back to the common text fallback described above while allowing `null` for optional fields. ## Pi -Spawns a `pi` subprocess for each invocation with `--mode json --no-session`. -See [`agent_args_override`](/no-mistakes/reference/global-config/#agent_args_override) for Pi override precedence. +Spawns a `pi` subprocess for each invocation with `--mode json`. Cold invocations add `--no-session`; with `session_reuse: true`, review-fixer turns instead create and resume one Pi session per run via `--session `. +Model and reasoning effort come from [`agent_config.pi`](/no-mistakes/reference/global-config/#agent_config), rendered as `--model` and `--thinking`. See [`agent_args_override`](/no-mistakes/reference/global-config/#agent_args_override) for Pi override precedence. Reads JSONL events from stdout and streams incremental text deltas to the TUI. -When structured output is requested, no-mistakes injects the JSON schema into the prompt and validates the final text response. +When structured output is requested, no-mistakes injects the JSON schema into the prompt and validates the final text response with the common text fallback described above. ## Copilot CLI Spawns a `copilot` subprocess for each invocation with `-p --output-format json`. It also adds `--no-color` and `--no-ask-user` so the run is non-interactive, plus `--allow-all-tools` (required for non-interactive mode) unless you already set your own Copilot permission flag through `agent_args_override`. -Any `agent_args_override.copilot` flags are inserted before no-mistakes' managed flags, so user choices such as `--model` or `--effort` take effect. +Any `agent_args_override.copilot` flags are inserted before no-mistakes' managed flags, so user choices take effect. Prefer [`agent_config.copilot`](/no-mistakes/reference/global-config/#agent_config) for model and reasoning effort; it renders the same `--model` and `--effort` flags, and a raw flag here still wins over it. Reads JSONL events from stdout, streaming incremental `assistant.message_delta` text to the TUI and capturing the final `assistant.message` content. -The Copilot CLI has no output-schema flag, so when structured output is requested no-mistakes injects the JSON schema into the prompt and validates the final text response with the same JSON fence and bare-object fallback used by Pi and Rovo Dev. +The Copilot CLI has no output-schema flag, so when structured output is requested no-mistakes injects the JSON schema into the prompt and validates the final text response with the common text fallback described above. ## ACP aliases @@ -306,7 +323,8 @@ When the target matches a first-class alias such as `acp:cursor`, no-mistakes su Configure custom target commands in the [Global Config Reference](/no-mistakes/reference/global-config/#acp_registry_overrides). no-mistakes invokes acpx with JSON output, approve-all permissions, denied non-interactive permission prompts, and the repo worktree as `--cwd`. -Structured output is handled by appending the requested JSON schema to the prompt and validating the final assistant text. +Structured output is handled by appending the requested JSON schema to the prompt and validating the final assistant text with the common text fallback described above. +A `model` set under [`agent_config`](/no-mistakes/reference/global-config/#agent_config) for an alias or `acp:` is passed as acpx's own `--model`, so ACP targets can be pinned to an explicit model. acpx exposes no reasoning-effort surface, so `effort` is refused for ACP names rather than silently ignored. ## Checking agent availability @@ -321,10 +339,12 @@ $ no-mistakes doctor ✓ daemon running ✓ claude – codex (not found) + – grok (not found) – rovodev (not found) – opencode (not found) – pi (not found) – copilot (not found) + – antigravity (not found) – acpx (not found) – cursor (not found (cursor-agent, acpx)) ✓ gate validation claude is runnable diff --git a/docs/src/content/docs/guides/configuration.md b/docs/src/content/docs/guides/configuration.md index dcc5d3d3e..0545b345f 100644 --- a/docs/src/content/docs/guides/configuration.md +++ b/docs/src/content/docs/guides/configuration.md @@ -55,7 +55,7 @@ The rest of this page covers only the cross-cutting rules that involve both file ## Precedence - Repo config overrides global config field by field: repo `agent` replaces the global `agent` (including a full ordered fallback list), while `auto_fix`, `ci`, `commit`, `intent`, and the repository-scoped `test.evidence` fields overlay individual fields and fall through to the global default for anything unset (`intent.disabled_readers` adds to the globally disabled readers instead of replacing them). Local evidence location and retention are machine-wide and remain global-only; the [Global Config Reference](/no-mistakes/reference/global-config/#testevidence) owns the exact boundary. -- `agent_path_override`, `agent_args_override`, `acpx_path`, `acp_registry_overrides`, `ci_timeout`, `daemon_connect_timeout`, `step_quiet_warning`, `log_level`, and `session_reuse` are global-only fields. +- `agent_path_override`, `agent_config`, `agent_args_override`, `acpx_path`, `acp_registry_overrides`, `ci_timeout`, `daemon_connect_timeout`, `branch_sync_remote_timeout`, `step_quiet_warning`, `agent_timeout`, `review_agent_timeout`, `test_agent_timeout`, `log_level`, and `session_reuse` are global-only fields. - `commands`, `ignore_patterns`, `document.instructions`, `review.path_instructions`, `allow_repo_commands`, and `disable_project_settings` are repo-only fields. By default, `commands` and `agent` are read from the trusted default branch; a trusted `allow_repo_commands: true` opt-in instead honors their pushed-branch values. The other gate-control fields, including `review.path_instructions` and the repo `ci` overlay, always come from the trusted default branch. See the [Repo Config Reference](/no-mistakes/reference/repo-config/) security note. - no-mistakes reloads global config while setting up each run, so edits made before starting a run apply to it. For repeatable profiles (for example fast versus deep Codex settings), use separately initialized `NM_HOME` roots; `NM_HOME` moves all no-mistakes state, not just config. diff --git a/docs/src/content/docs/guides/provider-integration.md b/docs/src/content/docs/guides/provider-integration.md index de928fe43..47fec2c9c 100644 --- a/docs/src/content/docs/guides/provider-integration.md +++ b/docs/src/content/docs/guides/provider-integration.md @@ -1,12 +1,13 @@ --- title: Provider Integration -description: Set up GitHub, GitLab, Bitbucket Cloud, or Azure DevOps for PR creation and CI monitoring. +description: Set up GitHub, GitLab, Forgejo, Bitbucket Cloud, Azure DevOps, or Gitea for PR creation and CI monitoring. --- -The PR and CI steps need to talk to your git host. Four hosts are supported: -GitHub, GitLab, Bitbucket Cloud (`bitbucket.org`), and Azure DevOps -(`dev.azure.com` and legacy `*.visualstudio.com`). Everything else -short-circuits the PR and CI steps with `skipped`. +The PR and CI steps need to talk to your git host. Six hosts are supported: +GitHub, GitLab, Forgejo, Bitbucket Cloud (`bitbucket.org`), Azure DevOps +(`dev.azure.com` and legacy `*.visualstudio.com`), and Gitea (almost always +self-hosted). Everything else short-circuits the PR and CI steps with +`skipped`. Provider integration is optional for the local gate. You only need it for the steps that happen after validation: opening or updating the PR, watching hosted @@ -25,14 +26,14 @@ What you do not get is PR automation and CI monitoring. ## What each step needs -| Step | GitHub | GitLab | Bitbucket Cloud | Azure DevOps | -|---|---|---|---|---| -| **PR** (create/update) | `gh` CLI, authenticated | `glab` CLI, authenticated | `NO_MISTAKES_BITBUCKET_EMAIL` + `NO_MISTAKES_BITBUCKET_API_TOKEN` | `az` CLI + `azure-devops` extension, authenticated | -| **CI** (polling, auto-fix) | `gh` CLI | `glab` CLI | same env vars | `az` CLI | -| **Merge conflict auto-fix** | `gh` CLI | `glab` CLI | not supported | `az` CLI | -| **Mergeability polling** | `gh` CLI | `glab` CLI | not supported | `az` CLI | -| **Failed check log fetching** | `gh` CLI | `glab` CLI | supported | not yet | -| **[Cancelled-check rerun](/no-mistakes/reference/repo-config/#cirerun_transient)** | `gh` CLI | not supported | not supported | not supported | +| Step | GitHub | GitLab | Forgejo | Bitbucket Cloud | Azure DevOps | Gitea | +| --- | --- | --- | --- | --- | --- | --- | +| **PR** (create/update) | `gh` CLI, authenticated | `glab` CLI, authenticated | `forgejo-axi`, authenticated | `NO_MISTAKES_BITBUCKET_EMAIL` + `NO_MISTAKES_BITBUCKET_API_TOKEN` | `az` CLI + `azure-devops` extension, authenticated | `tea` CLI, authenticated | +| **CI** (polling, auto-fix) | `gh` CLI | `glab` CLI | `forgejo-axi` | same env vars | `az` CLI | `tea` CLI | +| **Merge conflict auto-fix** | `gh` CLI | `glab` CLI | `forgejo-axi` | not supported | `az` CLI | not supported | +| **Mergeability polling** | `gh` CLI | `glab` CLI | `forgejo-axi` | not supported | `az` CLI | not supported | +| **Failed check log fetching** | `gh` CLI | `glab` CLI | `forgejo-axi` when runtime routes are available | supported | not yet | supported | +| **[Transient-check rerun](/no-mistakes/reference/repo-config/#cirerun_transient)** (cancellations and pre-run infra failures) | `gh` CLI | not supported | not supported | not supported | not supported | not supported | ## What changes when provider wiring is present @@ -41,8 +42,8 @@ pushes to the configured target: - create or update the PR automatically - keep polling hosted CI until the PR is merged, closed, declined, or the configured `ci_timeout` idle window elapses -- fetch failing job logs for the CI auto-fix loop -- on GitHub, GitLab, and Azure DevOps, watch mergeability and fix merge conflicts when possible +- fetch failing job logs for the CI auto-fix loop when the provider exposes them +- on GitHub, GitLab, Forgejo, and Azure DevOps, watch mergeability and fix merge conflicts when possible ## GitHub @@ -67,6 +68,10 @@ gh auth status `no-mistakes doctor` also checks for `gh` availability. For PR and workflow-run commands, no-mistakes passes the repository slug from the recorded upstream remote or PR URL to `gh`, so daemon-run commands do not depend on the daemon's current working directory. +### Multiple GitHub or GitLab identities + +If one daemon serves repositories that require non-overlapping accounts, give each account an isolated CLI config directory and use account-specific host aliases in repository remotes, for example `git@github-personal:you/project.git`. Then map those raw host tokens with global [`forge_profiles`](/no-mistakes/reference/global-config/#forge_profiles). The reference owns the configuration, validation, compatibility, and fail-closed routing contract. + **What you get:** - PR creation and update on pushes @@ -84,12 +89,12 @@ git remote set-url origin git@github.com:parent-owner/repo.git no-mistakes init --fork-url git@github.com:your-user/repo.git ``` -With this setup, the push and CI auto-fix push steps update the fork, while the PR and CI steps stay scoped to the parent repository. +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`. 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. -GitLab and Bitbucket fork MR/PR routing are not implemented yet; if a legacy or manually edited repo record has `fork_url` set for those providers, PR creation skips instead of opening an unsafe self PR. +GitLab, Forgejo, Bitbucket, and Azure DevOps fork MR/PR routing are not implemented yet; if a legacy or manually edited repo record has `fork_url` set for those providers, PR creation skips instead of opening an unsafe self PR. ## GitLab @@ -112,6 +117,24 @@ glab auth login - Failed job trace fetching (`glab ci trace`) for the CI auto-fix step - Merge-conflict polling and auto-fix, same as GitHub +## Forgejo + +Install [`forgejo-axi`](https://github.com/escidmore/forgejo-axi) and make it available on `PATH`. It currently installs from source with Node.js 20 or newer; set [`forgejo_axi_path`](/no-mistakes/reference/global-config/#forgejo_axi_path) when the executable lives elsewhere. + +Give the daemon a Forgejo token through either the generic `FORGEJO_TOKEN` variable or forgejo-axi's host-scoped token variable. Configure `FORGEJO_BASE_URL` for SSH origins and unrecognized self-hosted HTTPS hostnames. The [environment reference](/no-mistakes/reference/environment/#forgejo_base_url) owns the exact base-URL and host-key rules. + +Verify without mutating a deployed Forgejo instance: + +```sh +FORGEJO_BASE_URL=https://forgejo.example forgejo-axi status --json +``` + +`no-mistakes` delegates PR identity, lifecycle, commit-status and required-context evaluation, mergeability, and merged proof to forgejo-axi's stable JSON commands. Capabilities are runtime-probed from the server instead of guessed from a major version. + +Failed-log retrieval uses forgejo-axi's stable `run view --log-failed --json` command only when the runtime probe advertises commit statuses, Actions runs, run jobs, and job logs. `no-mistakes` accepts only a failing status's canonical native Actions target, verifies the exact run and freshly polled PR head, validates every returned job's run identity, and returns non-empty failed-job logs in deterministic order under the aggregate 1 MiB ceiling. Live testing against Forgejo 16.0.1 proves identity-matched retrieval through those routes. On Forgejo 15.0.5, commit-status gating, mergeability checks, and merged-state proof remain available when job and log routes are unavailable. + +Fork PR routing is not implemented for Forgejo; a configured `fork_url` makes the PR step skip rather than opening a self PR. + ## Bitbucket Cloud Bitbucket Cloud uses the REST API directly rather than a provider CLI. Set two environment variables (and optionally a third): @@ -137,7 +160,7 @@ Get an API token from [Bitbucket account settings](https://bitbucket.org/account - PR mergeability polling - Merge-conflict auto-fix -These are GitHub, GitLab, and Azure DevOps only right now. +These are GitHub, GitLab, Forgejo, and Azure DevOps only right now. ## Azure DevOps @@ -183,7 +206,47 @@ well as their SSH forms (`git@ssh.dev.azure.com:v3/...`). - Failed check log fetching for the CI auto-fix step (the `az` CLI has no first-class build-log command) -- Fork PR routing (same as GitLab and Bitbucket) +- Fork PR routing (same as GitLab, Forgejo, and Bitbucket) + +## Gitea + +Gitea uses `tea`, its official CLI. Install it and log in with a token: + +```sh +# see https://gitea.com/gitea/tea for install options (Homebrew, packages, or a release binary) + +tea logins add --url https://your-gitea.example.com --token your-token --name your-instance +``` + +Create a token from **Settings → Applications → Manage Access Tokens** on your Gitea instance, with read/write `repository` and `issue` scopes. + +Verify: + +```sh +tea logins list +``` + +**What you get:** + +- PR creation and update (`tea pulls create`/`edit`) +- CI status polling through Gitea Actions until the PR is merged or closed, or the configured `ci_timeout` idle window elapses. Job-level pass/fail comes from Gitea's Actions REST API (`GET .../actions/runs/{run}/jobs`), reached through `tea api` (which reuses the same stored login/token, so no separate HTTP client or credential is needed) - `tea`'s own `--output json` on `actions runs view --jobs` reports each job's run/queued/completed status but not its pass/fail conclusion. +- Failed job log fetching (`tea actions runs logs`) for the CI auto-fix step + +**What you don't get (yet):** + +- PR mergeability polling and merge-conflict auto-fix. Gitea's PR `mergeable` field has a documented upstream reliability bug ([go-gitea/gitea#25849](https://github.com/go-gitea/gitea/issues/25849)) that can stick `false` after a conflict is actually resolved, so no-mistakes declines the capability rather than trust it - the same posture as Bitbucket Cloud. +- Fork PR routing (same as GitLab, Bitbucket Cloud, and Azure DevOps) +- [Transient-check rerun](/no-mistakes/reference/repo-config/#cirerun_transient) + +Gitea Actions shipped in Gitea 1.19 (2023); older instances have no Actions API to poll. As with any repository with no CI, declare `no_ci: true` on the trusted default branch so the CI step does not wait for checks that will never appear - see the [CI step reference](/no-mistakes/reference/pipeline-steps/#ci). + +### Self-hosted Gitea + +Nearly every real Gitea instance is self-hosted at an arbitrary hostname with no `gitea` marker in it at all, so detection cannot use a substring match the way `gitlab.com`/`github.com` do. Instead, `no-mistakes` consults `tea`'s own login config (`config.yml`, under `$XDG_CONFIG_HOME/tea` or `~/.config/tea`) and treats the upstream as Gitea if its host matches a configured login's `url` or `ssh_host`. + +Running `tea logins add --url https://your-gitea.example.com --token --name ` is enough to make detection succeed; if `tea` has no login for the host, detection fails closed and the upstream is treated as unsupported. + +Because `tea` infers "which instance" from the current directory's git remote - context the daemon's detached worktree does not have - every `tea` invocation `no-mistakes` makes carries `--login ` explicitly, resolved from the matched login's name at request time. ## Self-hosted GitHub/GitLab @@ -209,13 +272,13 @@ The GitLab backend is pinned against `glab v1.5x`. Self-hosted detection and the ## SSH host aliases -SSH remotes that use a host alias from your SSH configuration (for example `git@github-personal:owner/repo` or `git@gitlab-work:group/repo`, where `github-personal`/`gitlab-work` map to a real `HostName` via `~/.ssh/config`) are supported. `no-mistakes` resolves the alias through `ssh -G` to its real host name and uses that host only for provider detection and for scoping the provider CLI (`gh`/`glab`) to the right instance. The original Git remote URL is left untouched, so authentication and pushes continue to use the alias exactly as your SSH configuration expects. +SSH remotes that use a host alias from your SSH configuration (for example `git@github-personal:owner/repo` or `git@gitlab-work:group/repo`, where `github-personal`/`gitlab-work` map to a real `HostName` via `~/.ssh/config`) are supported. `no-mistakes` resolves the alias through `ssh -G` to its real host name and uses that host only for provider detection and identity checks, including scoping `gh` or `glab` to the right instance and matching an SSH Forgejo remote to `FORGEJO_BASE_URL`. The original Git remote URL is left untouched, so authentication and pushes continue to use the alias exactly as your SSH configuration expects. If `ssh -G` is unavailable or the alias does not resolve, detection falls back to the literal host in the remote URL rather than failing the run. ## Unsupported hosts -If your upstream isn't GitHub, GitLab, Bitbucket Cloud, or Azure DevOps: +If your upstream isn't GitHub, GitLab, Forgejo, Bitbucket Cloud, Azure DevOps, or Gitea: - The **push** step still runs - `no-mistakes` pushes through git to the configured target like any other remote. - The **PR** step marks itself as `skipped`. @@ -229,8 +292,8 @@ Everything before push (rebase, review, test, document, lint) still works regard no-mistakes doctor ``` -`doctor` checks `gh` and `az` availability. For GitLab, confirm `glab` is installed and authenticated. For Bitbucket Cloud, confirm the two env vars are set in the environment the daemon runs under. For Azure DevOps, confirm the `azure-devops` extension is installed (`az extension show --name azure-devops`) and a PAT is available. +`doctor` checks `gh` and `az` availability. It also validates every configured forge profile, including its provider config, target host, and online authentication. Without profiles, confirm `glab` is installed and authenticated for GitLab. For Forgejo, run `FORGEJO_BASE_URL= forgejo-axi status --json` from the daemon's environment. For Bitbucket Cloud, confirm the two env vars are set in that environment. For Azure DevOps, confirm the `azure-devops` extension is installed (`az extension show --name azure-devops`) and a PAT is available. For Gitea, confirm `tea` is installed and has a login configured for your instance (`tea logins list`). :::note -When the daemon runs through a managed service (launchd, systemd, Task Scheduler), it reloads environment from your login shell on macOS and Linux so `gh` auth and `NO_MISTAKES_BITBUCKET_*` vars are picked up, and it augments `PATH` with common binary directories. If credentials or PATH-derived tools are missing, check `~/.no-mistakes/logs/daemon.log` for a login-shell environment resolution warning. On Windows it reuses the current process environment. +When the daemon runs through a managed service (launchd, systemd, Task Scheduler), it reloads environment from your login shell on macOS and Linux so CLI auth and provider token variables are picked up, and it augments `PATH` with common binary directories. If credentials or PATH-derived tools are missing, check `~/.no-mistakes/logs/daemon.log` for a login-shell environment resolution warning. On Windows it reuses the current process environment. ::: diff --git a/docs/src/content/docs/guides/troubleshooting.md b/docs/src/content/docs/guides/troubleshooting.md index 17ede7869..16a55fefc 100644 --- a/docs/src/content/docs/guides/troubleshooting.md +++ b/docs/src/content/docs/guides/troubleshooting.md @@ -69,6 +69,11 @@ no-mistakes daemon start If the socket file exists but nothing answers at all (a dead socket left behind by an unclean exit, e.g. a crash or `SIGKILL`), commands that ensure the daemon is running (`no-mistakes`, `init`, `attach`, `rerun`, `axi run`, `axi respond`) now fail fast with a `connect to daemon socket` error instead of silently starting a replacement daemon. The error message itself includes a `(run 'no-mistakes daemon start' to recover)` hint - run `no-mistakes daemon start` directly to recover, since it self-heals past a dead socket and starts a fresh daemon. +### "configured worktree placement is unusable" + +The daemon refuses to start while any [`worktree_roots`](/no-mistakes/reference/global-config/#worktree_roots) entry names a directory it cannot create run worktrees in, and `~/.no-mistakes/logs/daemon.log` names the offending entry. +Because every command starts the daemon, that takes the whole CLI down until the entry is fixed: point it at a directory outside `NM_HOME` and outside every gated checkout, or remove it, then run `no-mistakes daemon start`. + ### Managed service logs - **macOS (launchd):** `launchctl list | grep no-mistakes` and check `~/Library/LaunchAgents/com.kunchenguid.no-mistakes.daemon.*.plist` @@ -222,27 +227,30 @@ Symptom: pipeline completes but the PR step shows `skipped`. Check the [Provider Integration](/no-mistakes/guides/provider-integration/) requirements. Most common causes: -- `gh` or `glab` not installed -- `gh auth status` shows not authenticated +- `gh`, `glab`, `forgejo-axi`, or `tea` not installed +- The provider CLI reports that it is not authenticated - Bitbucket env vars not set in the daemon's environment -- Upstream is on a host that isn't supported (GitHub, GitLab, `bitbucket.org`, or Azure DevOps) +- Upstream is not one of the hosts listed in Provider Integration - Self-hosted GitHub Enterprise on a hostname that is not `github.com` isn't detected because `gh` isn't configured for the host; run `gh auth login --hostname your-ghe.example.com` so detection finds it. Once detection succeeds, the availability check is host-scoped (`gh auth status --hostname your-ghe.example.com`), so a stale token on `github.com` or any other configured gh host can no longer falsely mark the GHE repo as unauthenticated. - Self-hosted GitLab on a hostname with no `gitlab` marker isn't detected because `glab` isn't configured for the host; run `glab auth login --hostname your-gitlab.example.com` so detection finds it. Once detection succeeds, the availability check is host-scoped (`glab auth status --hostname your-gitlab.example.com`), so a stale token on `gitlab.com` or any other configured glab host can no longer falsely mark the self-hosted repo as unauthenticated. -- A GitLab, Bitbucket, or Azure DevOps repo record has a fork URL set; fork MR/PR routing is currently GitHub-only -- You pushed the default branch (PR step always skips on the default branch) +- Self-hosted Gitea isn't detected because `tea` has no login configured for the host; run `tea logins add --url https://your-gitea.example.com --token --name ` so detection finds it. See [Self-hosted Gitea](/no-mistakes/guides/provider-integration/#self-hosted-gitea). +- A non-GitHub repo record has a fork URL set; fork MR/PR routing is currently GitHub-only +- You pushed the PR base branch (PR step always skips there; this is the repository's default branch, or the configured [`pr.base_branch`](/no-mistakes/reference/repo-config/#prbase_branch) when set) ## CI step stuck or timed out Symptom: CI step keeps monitoring an open PR longer than expected, or pauses after the idle timeout. Monitoring while the PR remains open - even after checks are currently healthy - is intended behavior, because a later default-branch update can make the PR conflict or rerun CI. -Once the CI monitor reports readiness and the PR is mergeable, the CI panel shows `✓ Checks passed` and the terminal title switches to `Checks passed`, so you can tell when to go merge the PR; the signal clears automatically if checks start re-running or a new failure appears. A trusted [`no_ci: true` declaration](/no-mistakes/reference/repo-config/#no_ci) can establish readiness for a zero-check repository; an empty forge response without that declaration is not ready. +Once the CI monitor reports readiness and the PR is mergeable, the CI panel shows `✓ Checks passed` and the terminal title switches to `Checks passed`, so you can tell when to go merge the PR; the signal clears automatically if checks start re-running or a new failure appears. A trusted [`no_ci: true` declaration](/no-mistakes/reference/repo-config/#no_ci) can establish readiness for a zero-check repository; an empty forge response without that declaration is not ready. The [CI step reference](/no-mistakes/reference/pipeline-steps/#ci) owns the exact readiness and signal-clearing rules, including GitHub Actions runs that do not appear in the PR check rollup. How long the monitor runs is controlled by `ci_timeout` in `~/.no-mistakes/config.yaml`, an idle timeout that re-arms whenever the upstream default branch advances; the [`ci_timeout` field reference](/no-mistakes/reference/global-config/#ci_timeout) owns the default, the `unlimited` keyword and its aliases, and the exact re-arm semantics. Older config files may still contain an explicit `ci_timeout: "4h"` value; update it if you want the newer default behavior. If the PR is still open at the timeout, the step pauses for approval with findings for the open monitoring state or any known unresolved failures. You can approve, fix, or skip from the TUI or `no-mistakes axi respond`. + +A park that happens **before** the timeout, with a finding that CI checks could not be read from the provider, means the check read itself is failing (after 6 consecutive failed polls, the step stops waiting instead of spinning to `ci_timeout`). The finding is provider-neutral and the step log shows the underlying provider error; for GitHub, a `gh` older than 2.50 rejects the `gh pr checks --json` call and needs upgrading. The same park on GitLab, Bitbucket Cloud, or Azure DevOps points at that provider's CLI or credentials instead. Use `no-mistakes axi abort` only when you mean to cancel the whole active run. ## Step looks quiet or wedged @@ -253,6 +261,10 @@ Symptom: `no-mistakes axi status` shows an active step with `last_activity` pref It is only a liveness signal. It does not cancel the step, fail the run, or mean the pipeline is safe to bypass. +A quiet Review step still ends on its own: its agent turns are bounded by [`review_agent_timeout`](/no-mistakes/reference/global-config/#review_agent_timeout), after which the run fails with a timeout diagnostic in the step log. +A quiet Test step is bounded the same way by [`test_agent_timeout`](/no-mistakes/reference/global-config/#test_agent_timeout), covering the post-test evidence-gathering agent and a Test-repair turn. +Every other agent-spawning step (Document, Lint, Rebase conflict repair, PR drafting, CI auto-fix) is bounded by [`agent_timeout`](/no-mistakes/reference/global-config/#agent_timeout), so a stall reaches the step's normal agent-error handling instead of remaining active until you abort. Those errors fail agent-driven mutation steps; PR drafting instead continues with its deterministic fallback content. + Start by reading the active run and the step log: ```sh @@ -268,7 +280,7 @@ Start a new run only after abort confirms the terminal state; see the [abort com ## Worktree won't clean up -Symptom: `~/.no-mistakes/worktrees///` sticks around after a run ends. +Symptom: `~/.no-mistakes/worktrees///` - or `/` when the repository has a [configured worktree root](/no-mistakes/reference/global-config/#worktree_roots) - sticks around after a run ends. The daemon removes worktrees at run completion, and also on daemon startup (crash recovery). If one is still there: @@ -295,6 +307,8 @@ rm -rf ~/.no-mistakes/worktrees ~/.no-mistakes/servers ~/.no-mistakes/socket ~/. no-mistakes daemon start ``` +If [`worktree_roots`](/no-mistakes/reference/global-config/#worktree_roots) places a repository's runs outside `NM_HOME`, delete the leftover `/` directories there as well - only those; the root is your own directory and holds nothing else of no-mistakes'. + This keeps your gate repos, database, and config but clears transient state. For a full wipe, see the [Uninstall section](/no-mistakes/start-here/installation/#uninstall). Wedged state often means a run is stuck `pending` or `running`, so `daemon stop` refuses without `--force`; only force through once you've confirmed it's fine for the listed runs to fail. diff --git a/docs/src/content/docs/reference/cli.md b/docs/src/content/docs/reference/cli.md index 0c2508ee9..f7621dc17 100644 --- a/docs/src/content/docs/reference/cli.md +++ b/docs/src/content/docs/reference/cli.md @@ -30,22 +30,34 @@ Initialize or refresh the gate for the current repository. ```sh no-mistakes init no-mistakes init --fork-url git@github.com:you/my-repo.git +no-mistakes init --worktree-root ~/work/my-repo-runs ``` -| Flag | Type | Default | Description | -| ------------ | -------- | ------- | ----------------------------------------------------------------------------- | -| `--fork-url` | `string` | (none) | GitHub fork remote URL to push branches to while opening PRs against `origin` | +| Flag | Type | Default | Description | +| ----------------- | -------- | ------- | ------------------------------------------------------------------------------------------------ | +| `--fork-url` | `string` | (none) | GitHub fork remote URL to push branches to while opening PRs against `origin` | +| `--worktree-root` | `string` | (none) | Directory to create this repository's run worktrees in; prints the `worktree_roots` entry to add | Creates or refreshes a local bare repo, installs the managed pre-receive admission and post-receive notification hooks, best-effort isolates the gate repo's hook path from shared git config changes when Git supports `config --worktree`, adds or repairs the `no-mistakes` git remote, detects the default branch, records or updates the repo in SQLite, installs the `/no-mistakes` agent skill at user level into `~/.claude/skills/no-mistakes/SKILL.md` and `~/.agents/skills/no-mistakes/SKILL.md`, and ensures the daemon is running, installing the managed service when available and falling back to a detached daemon otherwise. -`init` writes no skill files into the repo; the user-level copies cover every supported agent (`~/.claude/skills` for Claude Code, `~/.agents/skills` for Codex, OpenCode, Rovo Dev, and Pi) across all repos. +`init` writes no skill files into the repo; the user-level copies serve Claude Code (`~/.claude/skills`) and agents that use the vendor-neutral `~/.agents/skills` convention (Codex, OpenCode, Rovo Dev, and Pi) across all repos. Grok Build is a pipeline runner and does not consume this installed skill. If the home `.claude` links to `.agents`, `.claude/skills` links to `.agents/skills`, or the reverse, `init` follows that layout and still makes the skill readable from both logical paths. If the repo still contains a vendored skill copy written by an older no-mistakes version, `init` leaves it untouched and prints a notice that it is no longer needed and can be removed. The gate advertises Git push-option support, so you can skip steps for one push with `git push -o no-mistakes.skip=test,lint no-mistakes `. For GitHub fork contributions, keep `origin` pointed at the parent repository and pass `--fork-url` with your fork remote URL. -The push, rebase branch-sync, and CI auto-fix pushes use the fork, while GitHub PR and CI commands stay scoped to the parent repository and create PRs with `--head :`. +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 routing currently requires both `origin` and `--fork-url` to be GitHub remotes with owner/repo paths. +`--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. +When the file already has a `worktree_roots:` block, `init` prints just the entry line to add under it - a second `worktree_roots:` key would make the config unparseable and stop the daemon. +Runs are created at `/` once the entry is in place; no-mistakes only ever touches the directories its own run records name, and everything else in that directory is left alone. +`init` rejects the directories the daemon would refuse to start on, so the entry it prints is always one you can paste: a directory inside `NM_HOME`, inside the repository being initialized or any other gated checkout, already used by another checkout (it names that checkout), or that exists as a non-directory. + +Two refusals apply to every `init`, with or without the flag. +It refuses to register a checkout that contains a directory an existing [`worktree_roots`](/no-mistakes/reference/global-config/#worktree_roots) entry points at, naming that entry, because registering it is what would make the placement unusable and stop the daemon; place the checkout elsewhere or repoint the entry first. +It also refuses to register anything while `~/.no-mistakes/config.yaml` does not load, naming the fault, because the daemon refuses to start on that same config. + Re-running `init` on an already-initialized repo succeeds and reports `Gate already initialized (refreshed)`. It refreshes managed gate wiring, origin/default-branch metadata, hook-path isolation, and the installed agent skill, overwriting any stale `SKILL.md` content from an older binary. When a fork URL is already recorded, re-running `init` without `--fork-url` preserves it. @@ -116,7 +128,7 @@ Backgrounding a call is fine for an agent harness, but the run never advances pa When the CI step is still monitoring an open PR and checks are green - or the trusted default-branch config declares [`no_ci: true`](/no-mistakes/reference/repo-config/#no_ci) with no registered checks - `axi run` exits successfully with `outcome: checks-passed` instead of waiting for a human merge. A generic empty check list without that declaration is not ready. Treat that as the agent stopping point: ask the user to review and merge the PR from the `help` line. If that PR later falls behind the default branch or hits a merge conflict, do not run `axi run`, `rerun`, or a manual rebase while the CI monitor is still running. -The monitor auto-rebases onto the base, resolves actual conflicts, and re-pushes the branch; a PR that is merely behind but clean needs no command. +The monitor auto-rebases onto the base, resolves actual conflicts, restarts validation at Review, and re-pushes the branch through Push; a PR that is merely behind but clean needs no command. Use `no-mistakes rerun` only after that monitor is no longer running, such as a closed PR, aborted or superseded run, idle timeout, or exhausted CI auto-fix attempts. Successful outcomes (`checks-passed` and `passed`) also carry `help` instructions telling the agent to summarize the run. When the pipeline applied fixes, they include a `fixes` table and a `help` instruction to acknowledge the misses and list those fixes for the user's review. @@ -149,24 +161,32 @@ The same successful-output reporting instructions apply to `axi respond` results ## no-mistakes axi status -Show a run, preferring the current branch's active or most recent run before falling back to repo-wide active or recent runs. +When `--run` is omitted, show this branch's run: its active run, else its most recent one. +Resolution is scoped to the current branch and never falls back to another branch's run, because one clone commonly has several worktrees on different branches. +On a successful status response, when the current branch has no run of its own - including a detached `HEAD`, which owns no branch and so reports `current_branch: unknown` - the output carries no run object at all. +It reports `current_branch`, `runs_on_current_branch: 0` where a branch is known, and the recent-runs listing, so an unrelated run can never be read as this worktree's. +If the implicit current-branch lookup itself fails, status returns that error instead of presenting the failure as a detached or no-run result. +Detached-`HEAD` help offers deliberate `--run ` inspection or checking out a branch; it does not offer `axi run`, which requires a branch. +With `--run `, inspect exactly that run regardless of branch; when its branch differs from a known current branch, it is rendered under `other_branch_run:` instead of `run:`, alongside a top-level `current_branch`, so a parser keyed on `run:` never picks up a run proven to be on another branch. +An explicit `--run ` rendered under `run:` while the current branch is unknown (detached `HEAD` or a branch-lookup failure) encodes no branch relationship. ```sh no-mistakes axi status no-mistakes axi status --run ``` -| Flag | Type | Default | Description | -| ------- | -------- | ------------ | ------------------------- | -| `--run` | `string` | resolved run | Inspect a specific run ID | +| Flag | Type | Default | Description | +| ------- | -------- | ------------------ | ------------------------- | +| `--run` | `string` | current-branch run | Inspect a specific run ID | -When the resolved run is parked at an `awaiting_approval` or `fix_review` gate, its top-level `run:` object includes `awaiting_agent: parked ` immediately after `status`. -The field disappears after `axi respond`, on cancel, and on terminal outcomes; use it to distinguish a run waiting for the driving agent from one actively running, fixing, or watching CI. +When the resolved run is parked at an `awaiting_approval` or `fix_review` gate, its top-level `run:` or `other_branch_run:` object includes `awaiting_agent: parked ` immediately after `status`. +The field disappears after that run's gate is answered, on cancel, and on terminal outcomes; use it to distinguish a run waiting for the driving agent from one actively running, fixing, or watching CI. +Status offers branch-scoped `axi respond` commands only for the current branch's implicitly resolved run. An explicitly selected gate stays inspection-only even when its branch matches, because a newer active run on that branch could receive the bare response command instead; the gate remains visible and its log commands retain `--run `. When the resolved run has a `running` or `fixing` step, the run object includes `active_steps`. Each row reports how long the step has been active, the latest meaningful log or native-agent lifecycle activity, the native agent PID if one is currently running, and the current round such as `round 1`, `auto-fix 1/3`, or `fix 2`. If no activity arrives for longer than `step_quiet_warning`, `last_activity` is prefixed with `quiet`; this is only a liveness signal and does not cancel the step. For older active runs with no recorded activity timestamp, AXI falls back to the step log file modification time. -Gate summaries and finding descriptions are bounded in this default status view; truncated values disclose their original length, and the gate help points to `no-mistakes axi logs --step --full` for the complete step log. +Gate summaries and finding descriptions are bounded in this default status view; truncated values disclose their original length, and the gate help points to `no-mistakes axi logs --step --full` for an implicitly resolved run or `no-mistakes axi logs --run --step --full` for an explicitly selected run. Relevant current-branch states also include a cached `branch_sync` object with full SHAs, the run's status, the persisted pipeline push binding, target kind and ref, relation, safety result, PR lifecycle, and a structured next action. Cached home and status rendering performs no network read and labels the remote observation `pipeline_push`; only explicit sync check or apply reports `live` freshness. @@ -201,22 +221,22 @@ Run `axi sync` only when structured output offers `next_action.code: sync`; proc ### Custody recovery -A run that goes terminal (cancelled, failed, or completed without a push stage) after moving the pipeline head leaves the branch `pipeline_owned` with `safety: blocked_pipeline_owned_recoverable`, the run's terminal `pipeline.status`, the exact `submitted_head`/`current_head`/`relation` ownership facts, and `next_action.code: recover_custody`. +A run that goes terminal (cancelled, failed, or completed without a push stage) after moving the pipeline head leaves the branch `pipeline_owned`. Status offers `next_action.code: recover_custody` only when recovery can establish the same eligibility it will enforce: an equal or ahead local head proves the source locally and can create the local anchor when the gate is unavailable, but any existing gate recovery ref must still match the recorded head; importing a missing preserved head requires an exact run-specific gate anchor (or legacy commit evidence that can be anchored), a clean worktree, and either local ancestry or the content-preservation proof described below. The eligible state reports `safety: blocked_pipeline_owned_recoverable`, the run's terminal `pipeline.status`, and the exact `submitted_head`/`current_head`/`relation` ownership facts. A run whose terminalization verifies that the managed worktree head never changed from the submitted head releases the branch instead: the terminal outcome, including cancellation, ends ownership; status reports `state: user_owned` with the same exact ownership facts and no `next_action`; the branch and head are immediately usable for any separately authorized delivery; and nothing blocks a direct push or PR. -Without that positive terminal head evidence, custody stays recoverable rather than being guessed away. +Without positive evidence that the submitted head stayed unchanged, custody is not guessed away. Missing or conflicting evidence, and import cases with a dirty worktree or genuinely divergent history, require manual reconciliation instead of advertising a recovery that will refuse. While a run is still active, it reports `state: pipeline_owned`, the exact submitted/current heads and their relation, and `next_action.code: continue_active_run` with `no-mistakes axi status`, even when its head has not moved yet. `--recover` verifies the run is terminal, anchors the preserved head under `refs/no-mistakes/recover/` in the invoking repository, and stamps custody returned so a fresh run can start. -For equal or ahead worktrees where the preserved head is already locally reachable, recovery writes that anchor locally without gate access. -For behind or diverged worktrees, recovery verifies the preserved head at the local gate branch and fetches it into the anchor before moving or refusing. +For equal or ahead worktrees where the preserved head is already locally reachable, recovery writes that anchor locally without requiring gate access. If the gate is available, an existing symbolic, non-commit, or mismatched recovery ref is conflicting evidence and recovery refuses without overwriting it. +For behind or diverged worktrees, recovery verifies the preserved head at the run-specific recovery ref in the local gate and fetches it into the anchor before moving or refusing. Legacy recorded heads that remain available as unreferenced gate objects are anchored before recovery continues. A clean behind worktree fast-forwards. A diverged worktree is adopted only when the preserved head provably carries every local change, proven by an executable three-way merge whose result is exactly the preserved head's tree. -This covers a pipeline rebase onto a newer base once a later pipeline commit has also advanced the gate branch to the preserved head. -A rebase-only cancelled run can still refuse recovery because its detached worktree advances the recorded run head without advancing that gate branch; use `no-mistakes rerun` in that case. +This covers a pipeline rebase onto a newer base without requiring the gate branch to advance to the preserved head. +Terminalization pins a verified unpublished pipeline head under a run-specific recovery ref, so recovery does not require the gate branch itself to have advanced. If the recorded head is genuinely missing, status reports manual reconciliation instead of advertising `recover_custody`. That adoption anchors the pre-recovery local head under `refs/no-mistakes/recover-local/`, then moves the branch with Git operations that refuse on their own rather than after a preceding check: an atomic compare-and-swap on the branch ref, and a working-tree update that aborts instead of overwriting a modified or untracked file. The proof is deliberately narrow and never uses patch identity, which discards hunk locations and whitespace and so cannot tell a genuine replay from a same-shaped edit elsewhere. Anything it cannot decide - unlanded local commits, or a rebase whose fix rounds also rewrote your own lines - still refuses with the anchor named, because only escalation can tell a deliberate pipeline fix apart from a dropped change. A dirty worktree refuses with explicit choices. -When you explicitly keep a behind or diverged local head instead of taking the preserved head, `--keep-local` returns custody at the current head without touching the worktree and atomically points the gate branch at it, so a concurrent gate push wins and the recovery refuses instead. +When you explicitly keep a behind or diverged local head instead of taking the preserved head, `--keep-local` returns custody at the current head without touching the worktree and atomically points the gate branch at it. If the gate branch moved independently, recovery first preserves that head under `refs/no-mistakes/recover-gate/`; a conflicting pre-existing anchor makes recovery refuse, and a concurrent gate push wins the compare-and-swap and also makes recovery refuse. `no-mistakes rerun` is the alternative exit that resumes validating the preserved head instead of taking the branch back. A recovered never-pushed run reports `state: custody_returned`; a recovered pushed run reports its ordinary classification against the last push binding, typically `local_ahead`. On a `user_owned` branch, `--recover` is an idempotent no-op success: nothing pipeline-created exists to recover, and no file, ref, or database row changes. @@ -231,13 +251,16 @@ no-mistakes axi logs --step review --full no-mistakes axi logs --step review --run ``` -| Flag | Type | Default | Description | -| -------- | -------- | ------------ | --------------------------------------- | -| `--step` | `string` | (none) | Step name; required | -| `--run` | `string` | resolved run | Run ID to inspect | -| `--full` | `bool` | `false` | Show the entire log instead of the tail | +| Flag | Type | Default | Description | +| -------- | -------- | ------------------ | --------------------------------------- | +| `--step` | `string` | (none) | Step name; required | +| `--run` | `string` | current-branch run | Run ID to inspect | +| `--full` | `bool` | `false` | Show the entire log instead of the tail | -Without `--full`, long logs show the last 40 lines and a help hint for the full log. +When `--run` is omitted, the run is resolved the same way as [`axi status`](#no-mistakes-axi-status): this branch's run, never another branch's. +With `--run `, logs are read from exactly that run regardless of branch. +An unknown explicit run ID exits nonzero with `error: run "" not found` instead of reporting that the current branch has no run. +Without `--full`, long logs show the last 40 lines and a help hint for the full log; when `--run ` selected the log, that hint retains the same run ID. Step logs include native subprocess agent lifecycle lines such as `codex started pid=4242`, `codex exited pid=4242 status=success`, and transient retry messages when the selected agent supports lifecycle events. They also include fix-loop markers such as `auto-fix round 1/3 starting after round 1` and `user-fix round starting after round 2`. @@ -303,7 +326,13 @@ no-mistakes rerun no-mistakes rerun --intent "the revised user goal" ``` -Starts a new pipeline run using the last-known head SHA on the current branch. +Starts a new pipeline run from the current gate branch, except when the latest +terminal run has a verified unpublished head whose custody has not been +returned: rerun then uses that preserved terminal head even if the gate branch +is stale. The command refuses instead of falling back to the gate branch when +the run-specific recovery ref is conflicting, invalid, or the recorded head is +unavailable. Use `no-mistakes axi status` and reconcile custody first in that +case. If the selected prior run has explicit intent, rerun inherits it exactly by default; otherwise it performs fresh intent inference. `--intent` supplies a new canonical explicit intent in either case. Inherited intent keeps distinct rerun provenance; @@ -386,7 +415,7 @@ no-mistakes stats Displays total changes, rescued changes, rescue rate, reported and fixed mistakes, fixes by pipeline step, and the top repos by rescue activity. Use `--agents` for local, per-purpose agent performance aggregates: duration and the subprocess-vs-model time split, session mode, errors, the token totals (input, output, cache-read, cache-creation, fresh input, reasoning), and the model round-trip and tool-category activity histogram, with a `METRICS` coverage count that tells a real zero apart from missing instrumentation. -Use `--run ` to inspect the individual agent invocations for one run - including each invocation's per-round token deltas next to the raw (cumulative for resumed sessions) counters, tool-category breakdown, workload size, finding count, and fallback reason - plus the total time parked at approval gates; it implies `--agents`. +Use `--run ` to inspect the individual agent invocations for one run - including each invocation's per-round token deltas next to the raw counters (cumulative across a resumed session for codex; per-invocation for pi), tool-category breakdown, workload size, finding count, and fallback reason - plus the total time parked at approval gates; it implies `--agents`. Nullable fields an adapter did not report render as `-` (unknown), which is distinct from a recorded `0`; the legacy raw input, output, and cache-read counters remain numeric. ```sh @@ -413,9 +442,10 @@ Checks: - Data directory (`~/.no-mistakes/`) - SQLite database - Daemon status -- Agent runners: native binaries `claude`, `codex`, `acli`, `opencode`, `pi`, and `copilot`, plus the optional ACP bridge `acpx` +- Agent runners: native binaries `claude`, `codex`, `grok`, `acli`, `opencode`, `pi`, `copilot`, and `agy` (Antigravity), plus the optional ACP bridge `acpx` - ACP alias default binaries: `cursor-agent` plus `acpx` for `cursor` - Effective global agent configuration, reported as `gate validation`; an unavailable configured runner is a failed check because the gate cannot validate without it +- Every configured [`forge_profiles`](/no-mistakes/reference/global-config/#forge_profiles) entry, reported as `forge `: the profile resolves and validates, its provider CLI is installed, and that CLI is authenticated for the profile's host Uses indicators: `✓` (available), `–` (not found, optional), `✗` (problem detected). @@ -423,7 +453,9 @@ The standalone runner rows inspect default binary names; the `cursor` row report The [Global Config Reference](/no-mistakes/reference/global-config/) owns ACP gate-validation availability and probing semantics. Each validation run performs the authoritative agent resolution again after applying any trusted repository-level override. -`doctor` checks `gh` and `az` availability. For GitLab PR and CI steps, install and authenticate `glab`. For Bitbucket Cloud PR and CI steps, set `NO_MISTAKES_BITBUCKET_EMAIL` and `NO_MISTAKES_BITBUCKET_API_TOKEN`. For Azure DevOps PR and CI steps, install the `azure-devops` extension and provide a PAT. +`doctor` checks `gh` and `az` availability. [Provider Integration](/no-mistakes/guides/provider-integration/) owns the separate setup checks for GitLab, Forgejo, Bitbucket Cloud, Gitea, and the Azure DevOps extension and PAT. + +`tea` stays docs-only like `glab`, `forgejo-axi`, and Bitbucket's env vars, rather than an active `doctor` check like `gh`/`az`: Gitea is almost always self-hosted, so a bare "`tea` not found" row would be a near-universal, low-value warning for the vast majority of users who have no Gitea instance at all. ## no-mistakes update diff --git a/docs/src/content/docs/reference/environment.md b/docs/src/content/docs/reference/environment.md index 1569d80eb..2963e0075 100644 --- a/docs/src/content/docs/reference/environment.md +++ b/docs/src/content/docs/reference/environment.md @@ -16,7 +16,7 @@ When set, everything else moves under this root: - Global config: `$NM_HOME/config.yaml` - Gate repos: `$NM_HOME/repos/.git` -- Worktrees: `$NM_HOME/worktrees///` +- Worktrees: `$NM_HOME/worktrees///`, unless [`worktree_roots`](/no-mistakes/reference/global-config/#worktree_roots) places a repository's run worktrees elsewhere - Logs: `$NM_HOME/logs/` - Database: `$NM_HOME/state.sqlite` - Socket / PID / singleton lock: `$NM_HOME/socket`, `$NM_HOME/daemon.pid`, and `$NM_HOME/daemon.lock` @@ -35,6 +35,61 @@ Override how long a CLI client waits for an existing daemon socket to accept a c Takes precedence over `daemon_connect_timeout` in `config.yaml`. An empty, unparsable, or non-positive value is ignored and the config value (or its default) is used instead. +## `FORGEJO_BASE_URL` + +Canonical Forgejo web base URL used for provider discovery and forgejo-axi commands. + +| | | +| ------- | -------- | +| Type | `URL` | +| Default | (none) | + +Set this for every SSH origin, because an SSH remote does not identify the canonical Forgejo web scheme, port, or path prefix. Also set it when an HTTPS origin uses a self-hosted hostname other than `codeberg.org` or one containing `forgejo`. Recognized HTTPS origins are detected automatically, including non-default ports and path prefixes inferred from the origin. When set, the host and prefix must match the repository's upstream origin; credentials, query strings, and fragments are rejected. + +## `FORGEJO_TOKEN_` + +Preferred host-scoped token used by forgejo-axi for Forgejo PR and CI operations. + +| | | +| ------- | -------- | +| Type | `string` | +| Default | (none) | + +`HOST_KEY` is the uppercase URL host, including a non-default port, with each non-alphanumeric ASCII character replaced by its uppercase hexadecimal code point between underscores. For example, `forgejo.example:8443` uses `FORGEJO_TOKEN_FORGEJO_2E_EXAMPLE_3A_8443`. Host-scoped names prevent credentials from being reused for a look-alike host. + +## `FORGEJO_TOKEN` + +Generic Forgejo token fallback. When present, no-mistakes passes its name explicitly to forgejo-axi; prefer a host-scoped token when the daemon serves multiple Forgejo instances. + +| | | +| ------- | -------- | +| Type | `string` | +| Default | (none) | + +Tokens remain in the subprocess environment: no-mistakes never puts a token value in command arguments and redacts Forgejo token values from surfaced provider errors. + +## `FORGEJO_TIMEOUT_MS` + +Per-request forgejo-axi timeout in positive integer milliseconds. + +| | | +| ------- | -------- | +| Type | `int` | +| Default | `15000` | + +The pipeline context still bounds and cancels the complete subprocess independently. + +## `FORGEJO_CA_FILE` + +Path to a replacement CA trust bundle used by forgejo-axi for HTTPS requests. + +| | | +| ------- | -------- | +| Type | `string` | +| Default | (none) | + +This replaces rather than appends to the platform trust store. See [Provider Integration](/no-mistakes/guides/provider-integration/#forgejo) for provider setup. + ## `NO_MISTAKES_BITBUCKET_EMAIL` Bitbucket Cloud account email used for PR creation and CI monitoring. @@ -134,7 +189,7 @@ Directory holding glab's `config.yml`, consulted when detecting self-hosted GitL | Type | `string` | | Default | (none) | -When the upstream hostname carries no `gitlab` marker, no-mistakes reads glab's configured hosts from `$GLAB_CONFIG_DIR/config.yml` to decide whether the host is a GitLab instance. It takes precedence over `XDG_CONFIG_HOME`. See [Provider Integration](/no-mistakes/guides/provider-integration/#self-hosted-githubgitlab). +When the upstream hostname carries no `gitlab` marker, no-mistakes reads glab's configured hosts from `$GLAB_CONFIG_DIR/config.yml` to decide whether the host is a GitLab instance. It takes precedence over `XDG_CONFIG_HOME`. A selected [`forge_profiles`](/no-mistakes/reference/global-config/#forge_profiles) entry overrides this variable for that run and removes `GITLAB_TOKEN`, `GITLAB_ACCESS_TOKEN`, `OAUTH_TOKEN`, `CI_JOB_TOKEN`, `GLAB_ENABLE_CI_AUTOLOGIN`, `GITLAB_HOST`, `GL_HOST`, `GITLAB_URI`, `GITLAB_API_HOST`, `GITLAB_REPO`, `GITLAB_GROUP`, `REMOTE_ALIAS`, and `GIT_REMOTE_URL_VAR` from all child processes. See [Provider Integration](/no-mistakes/guides/provider-integration/#self-hosted-githubgitlab). ## `GH_CONFIG_DIR` @@ -145,11 +200,11 @@ Directory holding gh's `hosts.yml`, consulted when detecting self-hosted GitHub | Type | `string` | | Default | (none) | -When the upstream hostname is not `github.com`, no-mistakes reads gh's configured hosts from `$GH_CONFIG_DIR/hosts.yml` to decide whether the host is a GitHub Enterprise instance. It takes precedence over `XDG_CONFIG_HOME`. See [Provider Integration](/no-mistakes/guides/provider-integration/#self-hosted-githubgitlab). +When the upstream hostname is not `github.com`, no-mistakes reads gh's configured hosts from `$GH_CONFIG_DIR/hosts.yml` to decide whether the host is a GitHub Enterprise instance. It takes precedence over `XDG_CONFIG_HOME`. A selected [`forge_profiles`](/no-mistakes/reference/global-config/#forge_profiles) entry overrides this variable for that run and removes `GH_TOKEN`, `GITHUB_TOKEN`, `GH_ENTERPRISE_TOKEN`, `GITHUB_ENTERPRISE_TOKEN`, `GH_HOST`, and `GH_REPO` from all child processes. See [Provider Integration](/no-mistakes/guides/provider-integration/#self-hosted-githubgitlab). ## `XDG_CONFIG_HOME` -Config directory used to locate glab's `config.yml` for self-hosted GitLab detection and gh's `hosts.yml` for self-hosted GitHub Enterprise detection. +Config directory used to locate glab's `config.yml` for self-hosted GitLab detection, gh's `hosts.yml` for self-hosted GitHub Enterprise detection, and tea's `config.yml` for Gitea detection. | | | | ------- | ----------- | @@ -158,6 +213,7 @@ Config directory used to locate glab's `config.yml` for self-hosted GitLab detec When `GLAB_CONFIG_DIR` is unset, no-mistakes looks for glab's configured hosts at `$XDG_CONFIG_HOME/glab-cli/config.yml`, falling back to `~/.config/glab-cli/config.yml` when `XDG_CONFIG_HOME` is unset. When `GH_CONFIG_DIR` is unset, no-mistakes looks for gh's configured hosts at `$XDG_CONFIG_HOME/gh/hosts.yml`, falling back to `~/.config/gh/hosts.yml` when `XDG_CONFIG_HOME` is unset. +tea has no CLI-specific override env var (unlike `GLAB_CONFIG_DIR`/`GH_CONFIG_DIR`); no-mistakes always looks for its configured logins at `$XDG_CONFIG_HOME/tea/config.yml`, falling back to `~/.config/tea/config.yml` when `XDG_CONFIG_HOME` is unset. See [Provider Integration](/no-mistakes/guides/provider-integration/#self-hosted-gitea). ## `NO_MISTAKES_UMAMI_HOST` @@ -203,7 +259,7 @@ It never stores prompts, model outputs, diffs, raw command arguments, secret val The additive session-fidelity fields are nullable and read back as unknown (rendered `-`) rather than a fabricated zero when the adapter did not report them, so rows written before a field existed, and adapters that do not surface a datum, stay honest. The legacy raw input, output, and cache-read token counters render numerically; use the nullable per-round and derived fields to determine whether the adapter reported comparable usage: -- Token detail: `input_tokens`/`output_tokens`/`cache_read_tokens` (raw, cumulative across a resumed session for codex), `fresh_input_tokens` (input minus cache reads), `cache_creation_tokens` (unknown when the provider does not surface it), `reasoning_tokens`, and `delta_input_tokens`/`delta_output_tokens`/`delta_cache_read_tokens` (the correct per-round amounts, so a resumed session's cumulative counter is never mistaken for one round's usage). +- Token detail: `input_tokens`/`output_tokens`/`cache_read_tokens` (raw, cumulative across a resumed session for codex; per-invocation for pi), `fresh_input_tokens` (input minus cache reads), `cache_creation_tokens` (unknown when the provider does not surface it), `reasoning_tokens`, and `delta_input_tokens`/`delta_output_tokens`/`delta_cache_read_tokens` (the correct per-round amounts, so a resumed session's cumulative counter is never mistaken for one round's usage). - Activity: `model_roundtrips` (a proxy for productive model turns), `tool_calls`, and a bounded tool-category histogram (`tool_wait_calls`, `tool_test_lint_calls`, `tool_edit_calls`, `tool_read_calls`, `tool_git_calls`, `tool_other_calls`); a compound command counts once per sub-command, so the histogram can sum higher than `tool_calls`. - Timing split: `subprocess_wait_ms` is the wall-clock spent inside tool subprocesses; model/reasoning time is the invocation duration minus it, clamped at zero. - Context: `workload_files`/`workload_lines` (bounded change size), `finding_count` (findings in the structured output), and `fallback_reason` (why a failed resume forced a fresh session, one of transient/parse/exit/spawn/unsupported/other). diff --git a/docs/src/content/docs/reference/eval.md b/docs/src/content/docs/reference/eval.md index a7ca4d132..895214e0b 100644 --- a/docs/src/content/docs/reference/eval.md +++ b/docs/src/content/docs/reference/eval.md @@ -10,6 +10,8 @@ The `eval` commands do not start or use the shared daemon, alter a gate, emit re Replay does invoke the selected agent normally, so that agent may send the restored code and review context to its configured model provider. The local-only guarantee concerns eval storage and transport added by no-mistakes, not the selected agent's ordinary provider traffic. +When `eval sets`, `eval report`, or `eval run` resolves repository fingerprints into display names, it consults `state.sqlite` only if that pipeline database already exists and opens it read-only. The display lookup never creates or migrates pipeline state; without a readable database, the dashboards fall back to fingerprints. + ## How cases are collected Cases arrive on their own. When an eligible run finishes, its decided Review passes are frozen into the local corpus - one case per pass. Collection happens after the pipeline has already reported its outcome, so it can never change or fail the run; a problem is logged and nothing else. @@ -34,6 +36,8 @@ no-mistakes eval miss ingest \ `--finding` is repeatable. The command captures the run if needed (recapture is a no-op, so existing labels survive), then writes false-negative gold onto the last completed non-blocking review pass. Duplicate finding IDs are no-ops. A parked or blocking review is refused: that class found something, so it is not a post-PR miss. +`id` and `description` are required. `severity` defaults to `error` and must be one of `error`, `warning`, or `info` - it becomes gold and then a composition stratum, so an unrecognized value is refused rather than shown as an invented finding type. `action`, if given, must be one of `auto-fix`, `ask-user`, or `no-op`; gold carries no action, so a valid one is accepted and dropped rather than silently changing what is stored. + The ingest payload is the source of truth. Eval does not scrape GitHub review comments and does not read an external markdown ledger. The curator (a human, or an automation that already vetted the miss) supplies the structured finding. Automatic collection and `eval capture` do the same freeze, so a case is equally trustworthy either way. Capturing a run that was already collected relabels gold from later merge evidence and otherwise leaves the frozen case in place. `eval miss ingest` can still attach confirmed post-PR-miss gold afterwards. @@ -46,7 +50,7 @@ A case includes: - agent-neutral global configuration and the effective repository configuration frozen at capture - the original run, step, review-round, decision, and local invocation-metric records - a manifest with commit pins, changed-file counts, build identity, and a hash of the redacted remote URL -- a local `labels.json` file that stores finding-level gold and queued unmatched candidate findings +- a local `labels.json` file that stores finding-level gold; queued unmatched candidate findings are counted from the recorded replays themselves, so replays never rewrite a case's labels The manifest never stores a remote URL. Capture is read-only against the existing local database and gate. It does not fetch from the network. @@ -61,10 +65,10 @@ Capture writes gold from the **recorded gate decision** for a review round - wha - A finding the pipeline selected for auto-fix on a run whose PR **merged** is **true-positive** gold (`recorded-auto-fix-merged`): the decision to fix it is the evidence, so a fix a later round re-raised or rewrote is still labeled. Closed-not-merged and still-open runs stay unlabeled until the merge is observed. - A finding that was raised (`auto-fix` or `ask-user`, including a missing action that defaults to `ask-user`), **not selected for fix**, and then **shipped in a merged PR** is **false-positive** gold (`recorded-shipped-unfixed`). This is a deliberate operator judgement: a finding you approve and ship without fixing is a false positive in your own corpus. It needs both halves - a recorded gate decision for the round and the merge - and informational `no-op` findings are never labeled this way. - A confirmed post-PR miss ingested with `eval miss ingest` is also **false-negative** gold (`recorded-post-pr-miss`): review passed green, and a later vetted finding showed a real defect. -- Skip and approve-with-findings **without a merge** stay **unlabeled / pending** until later adjudication, and so does any round whose gate decision was never recorded (an unknown or aborted resolution), merged or not. Absence of a decision is never read as a judgement. +- Skip, approve-with-findings, and abort **without a merge** stay **unlabeled / pending** until later adjudication, and so does any legacy or unresolved round whose gate decision was never recorded, merged or not. Absence of a decision is never read as a judgement. - A later replay that raises a new issue absent from the gold set is queued as an unmatched candidate finding. It is never auto-scored as a false positive. -If a PR merges after the first capture, already-captured cases are relabeled. The daemon does this best-effort when it observes the merge; `eval relabel [run-id]` or recapture is the CLI path. Relabel adds merge-derived labels onto previously unlabeled findings and drops obsolete derived merge labels that the current recorded decisions no longer support. Adjudicated, user-fix, and ingested post-PR-miss labels are never overwritten. +If a PR merges after the first capture, already-captured cases are relabeled. The daemon does this best-effort when it observes the merge; `eval relabel [run-id]` or recapture is the CLI path. Relabel adds merge-derived labels onto previously unlabeled findings and drops obsolete derived merge labels that the current recorded decisions no longer support. Adjudicated, user-fix, and ingested post-PR-miss labels are never overwritten. Relabel and recapture converge in place: repeating either with unchanged source evidence produces the same labels, including for gold findings that lack IDs. A case with no finding-level gold is unlabeled / pending, never a pass. True-negative also stays unlabeled because the current capture evidence cannot establish that a finding is invalid without the shipped-unfixed or adjudication paths above. @@ -84,7 +88,11 @@ Finding-level gold uses `labels.json` schema version 2. There is no migration fr no-mistakes eval sets ``` -The command shows counts, finding-level gold coverage, unlabeled / pending cases, queued candidate findings, and composition by repository fingerprint, dominant language, change-size bucket, source severity, and finding type. +The command renders a dashboard headlined by the **diversified holdout** - the official gold-only set - showing its size, pin and cap state, finding-level gold as a confusion-matrix table (raised / missed against real issue / not an issue; true negatives are never counted, because a correctly silent review leaves no gold), and stratum composition (repository, dominant language, change-size bucket, source severity, finding type). A case stores only the fingerprint of its upstream URL, so the repository column resolves each locally registered repository to its upstream namespace/name, then its working-directory name or repository ID; an unresolved case falls back to its short fingerprint. The other sets appear as a compact footnote with their counts, gold coverage, unlabeled / pending cases, and queued candidate findings. + +The headline includes an instant **self-score**: the recorded source reviews of the diversified set scored against their own gold with the same matcher a replayed candidate faces. It is computed from the already-captured case files - no replay, agent invocation, or network - and is the baseline a candidate has to beat. Recall, precision bounds, and F1 follow the report's semantics, including withholding F1 when no false-positive gold exists. + +`eval sets` is safe to re-run: inspecting the sets materializes the diversified pins, and a second read returns the same summaries without repinning anything. Four logical sets are available to replay: @@ -102,11 +110,15 @@ Do not fit matcher thresholds or review product prompts against `diversified`. T ```sh no-mistakes eval run \ --cases diversified \ - --candidate codex+gpt-5.4 \ + --candidate codex,model=gpt-5.4,effort=low \ --repeats 3 ``` -A candidate is always explicit: `agent+model`. The replay restores each case into a fresh temporary bare gate and worktree, then invokes only the existing Review step. Push, PR, CI, test, lint, document, and fix loops are outside this subject under test. +A candidate is `agent,model=[,effort=]`. The fields are the same harness-neutral knobs [`agent_config`](/no-mistakes/reference/global-config/#agent_config) exposes to the pipeline, and they resolve through the same per-harness mapping, so a candidate can express exactly what a real run can. `model` is mandatory - a comparison that inherited whatever default the harness happened to resolve would not be reproducible - while `effort` is optional and one of `minimal`, `low`, `medium`, `high`, `xhigh`, `max`. + +Effort is part of the candidate identity, so `codex,model=gpt-5.4,effort=low` and `codex,model=gpt-5.4,effort=high` are reported as two candidates rather than collapsing into one. + +The replay restores each case into a fresh temporary bare gate and worktree, then invokes only the existing Review step. Push, PR, CI, test, lint, document, and fix loops are outside this subject under test. Replay scores each candidate finding against that gold: @@ -119,7 +131,13 @@ Matching is a documented cascade of strengths: the same finding ID, the same fil The report prints recall, precision bounds (adjudicated vs pending-as-FP), and F1 as the headline metric **only when false-positive gold exists** so precision is real. Otherwise F1 is withheld rather than reported as recall-in-disguise. -`--repeats` defaults to `3` and must be at least `1`. Candidates must use an agent that can enforce an explicit model; ACP targets such as `cursor` and `acp:` are rejected. Replays are intentionally isolated from the production `NM_HOME`; they do not contact the shared no-mistakes daemon. The selected agent still communicates with its configured model provider in the normal way. +`--repeats` defaults to `3` and must be at least `1`. Candidates must use an agent whose model no-mistakes can actually pin. ACP targets such as `cursor` and `acp:` are pinned through `acpx --model`, but they cannot take `effort`; `rovodev` and `antigravity` expose no mechanism at all and are rejected outright. `opencode` needs the `provider/model` form. The per-harness mapping table lives in [`agent_config`](/no-mistakes/reference/global-config/#agent_config). + +The replay never inherits this machine's own harness pins: capture strips `agent`, `agent_args_override`, and `agent_config` from the configuration it freezes, so the candidate is the only thing that decides what the harness runs as. + +The earlier `agent+model` candidate spelling was replaced by the key=value form and is no longer accepted; evaluations recorded under it keep their old candidate string and are reported as their own group. Replays are intentionally isolated from the production `NM_HOME`; they do not contact the shared no-mistakes daemon. The selected agent still communicates with its configured model provider in the normal way. + +The command streams one scored progress line per replay as it completes, then renders the session's score summary in the same dashboard style as `eval sets` and `stats`, followed by the session identifier. Re-running the same `eval run` is additive by design - each invocation records a fresh measurement session - but it is safe: identical inputs land in the same cohort so the report aggregates the samples instead of fragmenting into a new comparison group, while captured labels and manifests remain unchanged. ## Report results @@ -139,7 +157,7 @@ The report groups local replays by candidate and cohort. A cohort pins the selec - a finite-sample case-level recall range, with repeats averaged inside each case - whether a candidate lies on the observed recall-versus-token-cost frontier -The report is deliberately cautious. It never treats an unadjudicated candidate finding as a false positive, excludes candidates with failed replays from the frontier, and distinguishes missing token instrumentation from a real zero. +The report is deliberately cautious. It never treats an unadjudicated candidate finding as a false positive, excludes candidates with failed replays from the frontier, and distinguishes missing token instrumentation from a real zero. It is a pure read: repeated reports over unchanged recorded evaluations produce identical text output. ## Current boundary diff --git a/docs/src/content/docs/reference/global-config.md b/docs/src/content/docs/reference/global-config.md index 65530ad63..0e687c9b3 100644 --- a/docs/src/content/docs/reference/global-config.md +++ b/docs/src/content/docs/reference/global-config.md @@ -12,36 +12,59 @@ agent: auto acpx_path: acpx +forgejo_axi_path: forgejo-axi + acp_registry_overrides: local-gemini: node /opt/mock-acp-agent.mjs agent_path_override: claude: /Users/you/bin/claude codex: /opt/homebrew/bin/codex + grok: /Users/you/.grok/bin/grok rovodev: /usr/local/bin/acli opencode: /usr/local/bin/opencode pi: /usr/local/bin/pi copilot: /usr/local/bin/copilot +agent_config: + codex: + model: gpt-5.4 + effort: low + agent_args_override: codex: - - -m - - gpt-5.4 - -c - service_tier="priority" - - -c - - model_reasoning_effort="low" ci_timeout: "168h" step_quiet_warning: "10m" +agent_timeout: "30m" + +review_agent_timeout: "30m" + +test_agent_timeout: "30m" + daemon_connect_timeout: "3s" +branch_sync_remote_timeout: "60s" + log_level: info session_reuse: true +worktree_roots: + /Users/you/src/my-repo: /Users/you/work/my-repo-runs + +forge_profiles: + github-personal: + gh_config_dir: ~/.config/gh-personal + github-work: + gh_config_dir: ~/.config/gh-work + gitlab-work: + glab_config_dir: ~/.config/glab-work + auto_fix: rebase: 3 review: 0 @@ -80,10 +103,10 @@ Default agent for all repos and setup-wizard suggestions. Can be overridden per- | | | | ------- | ------------------------------------------------------------------------------------------- | | Type | `string` or `string[]` | -| Values | `auto`, `claude`, `codex`, `rovodev`, `opencode`, `pi`, `copilot`, `cursor`, `acp:` | +| Values | `auto`, `claude`, `codex`, `grok`, `rovodev`, `opencode`, `pi`, `copilot`, `antigravity`, `cursor`, `acp:` | | Default | `auto` | -`auto` resolves to the first supported native agent or ACP alias in this order: `claude`, `codex`, `opencode`, `acli` with `rovodev` support, `pi`, `copilot`, then `cursor`. +`auto` resolves to the first supported native agent or ACP alias in this order: `claude`, `codex`, `grok`, `opencode`, `acli` with `rovodev` support, `pi`, `copilot`, `antigravity`, then `cursor`. `cursor` is an ACP alias for the `cursor` target with default command `cursor-agent acp`. With default paths, `auto` only selects it when both `cursor-agent` and `acpx` resolve; `acp_registry_overrides.cursor` and `acpx_path` replace those respective defaults during availability checks. `acp:` uses the user-installed `acpx` binary to run an ACP target, for example `acp:gemini`; `acp:cursor` uses the same default command as `cursor`. @@ -95,7 +118,7 @@ If an explicit agent is unavailable, `auto` finds no native agent or ACP alias, You can also set an ordered fallback list: ```yaml -agent: [codex, claude] +agent: [codex, grok] ``` The list is filtered to entries available to the daemon at run startup, and the first available entry becomes the primary agent. @@ -113,6 +136,17 @@ Path to the user-installed `acpx` binary used for `agent: acp:` and ACP | Type | `string` | | Default | `acpx` | +### forgejo_axi_path + +Executable used for Forgejo PR and CI operations. + +| | | +| ------- | ------------- | +| Type | `string` | +| Default | `forgejo-axi` | + +A bare name is resolved from the daemon's effective `PATH`; an explicit path is executed directly. See [Provider Integration](/no-mistakes/guides/provider-integration/#forgejo) for setup and the [environment reference](/no-mistakes/reference/environment/#forgejo_base_url) for host and token configuration. + ### acp_registry_overrides Map an ACP target name to a raw ACP agent command. @@ -151,20 +185,81 @@ Default native binary names when no override is set: | ---------- | ---------- | | `claude` | `claude` | | `codex` | `codex` | +| `grok` | `grok` | | `rovodev` | `acli` | | `opencode` | `opencode` | | `pi` | `pi` | | `copilot` | `copilot` | +| `antigravity` | `agy` | + +### agent_config + +Model and reasoning effort per agent, in one common spelling. no-mistakes maps each field down to whatever mechanism that harness actually uses, so you no longer have to know each CLI's own flag. + +| | | +| ------- | ----------------------------------------------------------------------------------- | +| Type | `map[string]{model, effort}` | +| Keys | `claude`, `codex`, `grok`, `rovodev`, `opencode`, `pi`, `copilot`, `antigravity`, `cursor`, `acp:` | +| Default | Empty (every harness keeps its own defaults) | + +```yaml +agent_config: + codex: + model: gpt-5.4 + effort: low + claude: + model: sonnet + effort: high + opencode: + model: openai/gpt-5 + cursor: + model: gpt-5 +``` + +`effort` is one of `minimal`, `low`, `medium`, `high`, `xhigh`, `max`. The value is passed to the harness as written, so a level that harness does not implement is rejected by the harness itself rather than silently downgraded. + +How each field maps: + +| Agent | `model` | `effort` | Accepted effort levels | +| ----------------- | --------------------------------------------- | --------------------------------- | --------------------------------------------------- | +| `claude` | `--model` | `--effort` | `low`, `medium`, `high`, `xhigh`, `max` | +| `codex` | `-m` | `-c model_reasoning_effort="…"` | `minimal`, `low`, `medium`, `high` | +| `grok` | `--model` | `--reasoning-effort` | whatever the selected reasoning model accepts | +| `copilot` | `--model` | `--effort` | `minimal`, `low`, `medium`, `high`, `xhigh`, `max` | +| `pi` | `--model` | `--thinking` | `minimal`, `low`, `medium`, `high`, `xhigh`, `max` | +| `opencode` | session-message `model` (needs `provider/model`) | session-message `variant` | provider-specific | +| `cursor`, `acp:*` | `acpx --model` | not expressible | - | +| `rovodev` | not expressible | not expressible | - | +| `antigravity` | not expressible | not expressible | - | + +`opencode` needs the `provider/model` form (for example `openai/gpt-5`) because its session API takes the provider and the model as separate fields; a bare model name is refused at config load rather than dropped. Both of its knobs travel in the session message, not in the launch command, because `opencode serve` exits with usage on an unknown flag. + +`rovodev` and `antigravity` have no mechanism no-mistakes can set - `acli rovodev serve` plus its REST session API take no model parameter, and the `agy` CLI parses flags strictly - so `agent_config` for them is a config error rather than a request that quietly does nothing. Reach for [`agent_args_override`](#agent_args_override) there if your build of the CLI accepts a flag. Reasoning effort is likewise unavailable for ACP targets: no-mistakes drives them through `acpx`, which exposes `--model` but no effort surface. + +`agent_config` is global-only. Like `agent_args_override`, it decides which model runs with your credentials, so an `agent_config` block in a repository's `.no-mistakes.yaml` is ignored. + +**Precedence.** `agent_args_override` always wins. If a raw flag already pins a knob natively - for example, `-m`, `--model`, or a `-c`/`--config` assignment whose exact key is `model` or `model_reasoning_effort` for Codex, plus the other harnesses' `--effort`, `--reasoning-effort`, or `--thinking` forms - then `agent_config` does not emit its value for that knob. Text such as `model=` nested inside an unrelated option's value is not a pin. Any knob the raw flags leave alone still comes from `agent_config`, so adding `agent_config` to an existing configuration never changes the arguments that configuration already supplied: + +```yaml +agent_config: + codex: + model: gpt-5.4 # ignored: the raw -m below pins it + effort: low # applied: nothing raw pins reasoning depth +agent_args_override: + codex: + - -m + - o3 +``` ### agent_args_override Extra CLI flags to pass to each native agent. -Use this to set model selection, service tier, reasoning effort, permission mode, or any other flag the underlying agent supports. +Use this for anything [`agent_config`](#agent_config) does not cover - service tier, permission mode, profiles, or any other flag the underlying agent supports - and as the escape hatch for a harness whose model or effort flag no-mistakes cannot map. For model and reasoning effort on a mapped harness, prefer `agent_config`: one spelling instead of seven. | | | | ------- | --------------------------------------------------------- | | Type | `map[string][]string` | -| Keys | `claude`, `codex`, `rovodev`, `opencode`, `pi`, `copilot` | +| Keys | `claude`, `codex`, `grok`, `rovodev`, `opencode`, `pi`, `copilot`, `antigravity` | | Default | Empty (no extra flags) | User-supplied flags are normally inserted ahead of no-mistakes' managed flags, so your choices usually take precedence. Security suppression selected by trusted [`disable_project_settings`](/no-mistakes/reference/repo-config/#disable_project_settings) may be placed first while preserving a compatible operator pin. A few flags are reserved because no-mistakes depends on them to communicate with the agent - setting any of these returns a config error on load: @@ -173,18 +268,21 @@ User-supplied flags are normally inserted ahead of no-mistakes' managed flags, s | ---------- | ----------------------------------------------------------------------------------------------------------- | | `claude` | `-p`, `--print`, `--verbose`, `--output-format`, `--json-schema`, `-r`, `--resume`, `--session-id`, `-c`, `--continue`, `--fork-session` | | `codex` | `exec`, `resume`, `--resume`, `--session`, `--session-id`, `--thread`, `--thread-id`, `--last`, `--json`, `--color` | +| `grok` | `-p`, `--single`, `--prompt-file`, `--prompt-json`, `--output-format`, `--json-schema`, `-r`, `--resume`, `-c`, `--continue`, `--fork-session`, `--session-id`, `--system-prompt-override`, `--system-prompt`, `--rules`, `--append-system-prompt`, `--agent`, `--agents`, `--verbatim`, `--no-subagents`, `--no-auto-update`, `--cwd`, `--restore-code`, `--worktree`, `--worktree-ref` | | `rovodev` | `rovodev`, `serve`, `--disable-session-token` | | `opencode` | `serve`, `--hostname`, `--port`, `--print-logs` | -| `pi` | `--mode`, `--no-session` | +| `pi` | `--mode`, `--no-session`, `-c`, `--continue`, `-r`, `--resume`, `--session`, `--session-id`, `--fork` | | `copilot` | `-p`, `--prompt`, `--output-format`, `--no-color` | +| `antigravity` | `--dangerously-skip-permissions`, `--print`, `--json-schema`, `--output-format`, `--conversation`, `-c`, `--continue` | For structured `codex` runs, no-mistakes also appends its own `--output-schema ` after your overrides. Treat that flag as managed even though config validation does not currently reject it. -The Claude and Codex session-control forms are reserved so no-mistakes can keep review-loop conversations deterministic: review turns stay session-free while the fixer keeps its own isolated durable session. +The Claude, Codex, Grok, Pi, and Antigravity session-control forms are reserved so no-mistakes can keep review-loop conversations deterministic: review turns stay session-free while the fixer keeps its own isolated durable session. Smart defaults: - For `claude`, supplying `--permission-mode` (or `--dangerously-skip-permissions`) suppresses the default `--dangerously-skip-permissions`. - For `codex`, supplying `--ask-for-approval`, `--sandbox`, or `--dangerously-bypass-approvals-and-sandbox` suppresses the default `--dangerously-bypass-approvals-and-sandbox`. +- For `grok`, supplying `--permission-mode` or `--always-approve` suppresses the default `--permission-mode bypassPermissions`. No model flag is added: Grok uses its current configured default unless you explicitly set `-m` or `--model`. Permission and sandbox flags affect the underlying agent, but they do not disable no-mistakes' pipeline prompt steering. Pipeline agents are still told to keep intentional writes inside the worktree and avoid mutating system state outside it. @@ -205,22 +303,56 @@ agent_args_override: - service_tier="priority" - -c - model_reasoning_effort="low" + grok: + - --reasoning-effort + - high rovodev: - --profile - work - opencode: - - --model - - gpt-5 pi: - --provider - google ``` -For Codex, `service_tier` and `model_reasoning_effort` tune different things: `service_tier` selects the speed or priority lane, while `model_reasoning_effort` selects reasoning depth. no-mistakes reloads global config while setting up each run, so edits made before `no-mistakes axi run` apply to that run. For repeatable profiles, use separately initialized `NM_HOME` directories; each has its own `config.yaml` and no-mistakes state. +Do not put a model flag under `opencode` here: these flags go to `opencode serve`, which exits with usage on an unknown option. Use `agent_config.opencode.model` instead. + +For Codex, `service_tier` and reasoning effort tune different things: `service_tier` selects the speed or priority lane, while reasoning depth is what [`agent_config`](#agent_config)'s `effort` sets (as `-c model_reasoning_effort`). no-mistakes reloads global config while setting up each run, so edits made before `no-mistakes axi run` apply to that run. For repeatable profiles, use separately initialized `NM_HOME` directories; each has its own `config.yaml` and no-mistakes state. + +### forge_profiles + +Optional machine-local routing for repositories that use different GitHub or GitLab identities. Keys are the raw host tokens recorded in the repository remote, including SSH aliases such as `github-personal`. Each entry must set exactly one provider config directory: + +```yaml +forge_profiles: + github-personal: + gh_config_dir: ~/.config/gh-personal + github-work: + gh_config_dir: /Users/you/.config/gh-work + gitlab-work: + glab_config_dir: ~/.config/glab-work + expected_login: team-bot +``` + +Paths must be absolute or begin with `~/`; environment variables and other shell expansion are not supported. Host keys are case-insensitive. + +When a repository matches a profile, no-mistakes validates it before starting the pipeline and applies an immutable environment to every subprocess in that run: built-in provider commands, custom shell commands, agents, managed agent servers, and the run's Git commands together with any hooks or credential helpers they spawn. A GitHub profile sets `GH_CONFIG_DIR` and removes higher-precedence GitHub token, host, and repository variables; a GitLab profile does the equivalent for `GLAB_CONFIG_DIR` and GitLab variables. The daemon process environment is never changed. + +Each selected GitHub config must contain the target host and exactly one account for it, with that account active. A selected GitLab config must contain the target host. An optional `expected_login` pins the account name the profile must be signed in as; resolution fails closed when the config's active login differs or is missing, so a swapped or re-authenticated config directory can never route a run through the wrong account. It carries an account name only, never credentials. `no-mistakes doctor` validates every configured profile and its online authentication. + +Profile activation is provider-specific and fail-closed: after at least one GitHub profile is configured, a GitHub repository must match a GitHub profile; GitLab remains ambient unless a GitLab profile is also configured, and vice versa. With no `forge_profiles`, provider detection and ambient CLI authentication behave exactly as before. + +For a GitHub fork, no-mistakes considers both the parent and fork host tokens. A match on either side is sufficient. If both match, they must select the same account: the same effective provider config directory *and* the same `expected_login` pin. Otherwise startup fails as ambiguous, so two host tokens sharing a config directory while pinning different logins can never silently resolve to one of them. Fork PR topology itself is unchanged. + +Deliberate scope boundaries, so profiles never duplicate what other layers own: + +- **Commit identity stays with Git.** Author and committer for pipeline fix commits come from the effective Git configuration (for example remote-keyed `includeIf` sections), which resolves naturally inside run worktrees. Profiles carry no name/email fields. +- **Two accounts on the same host are distinguished by remote host tokens.** Give each account its own SSH alias (`github-personal`, `github-work`) and key a profile per alias; a profile cannot disambiguate two accounts behind one identical remote URL. +- **Executable selection stays with the machine.** Which `gh`, `glab`, or `git` runs is owned by `PATH` and the existing command resolution, not by profile configuration. +- **Credential-helper context stays with Git configuration.** Profiles point at provider CLI config directories and never model or store credential material; credentials remain in the CLI's own store. ### ci_timeout -How long the CI step monitors an open PR, including provider CI status and on GitHub, GitLab, or Azure DevOps PR mergeability, before giving up. +How long the CI step monitors an open PR, including provider CI status and on GitHub, GitLab, Forgejo, or Azure DevOps PR mergeability, before giving up. | | | | ------- | ----------------------------------------------- | @@ -231,7 +363,7 @@ Accepts any Go `time.ParseDuration` string: `30m`, `2h`, `4h30m`, etc. This is an idle timeout, not an absolute deadline: every time the base branch advances, the monitor re-arms it. So an actively-updated green PR keeps its monitor no matter how long it stays open. -If it later develops an actual GitHub, GitLab, or Azure DevOps merge conflict, the CI auto-fix path rebases and re-pushes it, while a clean behind PR needs no command. +If it later develops an actual GitHub, GitLab, Forgejo, or Azure DevOps merge conflict, the CI auto-fix path rebases it, restarts validation at Review, and publishes it through Push, while a clean behind PR needs no command. A genuinely idle/abandoned PR still parks at an approval gate after the timeout elapses. While that CI gate is parked, the daemon continues bounded read-only PR-state checks. If the PR is merged or closed externally, the stale gate completes automatically; an open, unknown, or temporarily unreachable PR remains parked for a user decision. @@ -257,6 +389,54 @@ It does not cancel the step, change auto-fix behavior, or mark the run failed. AXI renders the quiet signal in the `active_steps` table as part of `last_activity`, for example `quiet 12m3s ago: codex started pid=4242`. For older active runs that do not yet have activity rows, AXI falls back to the step log file's modification time. +### agent_timeout + +Maximum wall-clock time for one pipeline agent invocation that does not already have a more specific deadline. +This is the default-by-construction budget: Document, Lint, Rebase conflict repair, PR drafting, CI auto-fix, and any future agent-spawning step are bounded even if they forget to install their own timer. +Review still uses [`review_agent_timeout`](#review_agent_timeout) as a per-round budget, Test still uses [`test_agent_timeout`](#test_agent_timeout) per invocation, and Intent keeps its five-minute extraction cap; any existing deadline is honored rather than capped. +When this deadline expires, the agent is cancelled and the invocation returns a timeout diagnostic instead of remaining active indefinitely. Agent-driven mutation steps fail the run, while PR drafting follows its existing agent-error fallback and continues with deterministic content. +A late successful return after the deadline is rejected, so post-agent commits and PR content cannot use work from a timed-out turn. + +| | | +| ------- | ---------------------- | +| Type | `string` (Go duration) | +| Default | `30m` | + +Accepts any positive Go `time.ParseDuration` string: `5m`, `30m`, `1h`, etc. +Non-positive values are rejected when loading the global config. +Raise it for repositories whose document, lint, rebase, PR, or CI-fix agent turns legitimately run long. +It is global-only: repository config and environment variables cannot override it. + +### review_agent_timeout + +Maximum wall-clock time for the Review step's agent turns in one review round. +The budget starts at that round's first agent turn and covers its optional review-fix turn plus the rereview turn together; every later auto-fix round starts a fresh budget. +When the deadline expires, the review agent is cancelled and the run fails with a diagnostic naming the timeout instead of remaining active indefinitely. + +| | | +| ------- | ---------------------- | +| Type | `string` (Go duration) | +| Default | `30m` | + +Accepts any positive Go `time.ParseDuration` string: `5m`, `30m`, `1h`, etc. +Non-positive values are rejected when loading the global config. +Raise it for repositories whose reviews legitimately run long; it bounds only the Review step, and no other step or environment variable overrides it. + +### test_agent_timeout + +Maximum wall-clock time for one Test-step agent invocation. +The budget covers the post-test evidence-gathering turn, and a Test-repair turn gets its own budget of the same length. +When the deadline expires, the test agent is cancelled and the run fails with a diagnostic naming the timeout instead of remaining active indefinitely. + +| | | +| ------- | ---------------------- | +| Type | `string` (Go duration) | +| Default | `30m` | + +Accepts any positive Go `time.ParseDuration` string: `5m`, `30m`, `1h`, etc. +Non-positive values are rejected when loading the global config. +Raise it for repositories whose targeted tests or evidence gathering legitimately run long; it bounds only the Test step, and no other step or environment variable overrides it. + ### daemon_connect_timeout Maximum time a CLI client waits for an existing daemon socket to accept a connection before failing instead of hanging. Guards against a daemon process that is alive but stuck or unresponsive. @@ -268,6 +448,19 @@ Maximum time a CLI client waits for an existing daemon socket to accept a connec Accepts any positive Go `time.ParseDuration` string. Overridable per-invocation with the `NM_DAEMON_CONNECT_TIMEOUT` environment variable; see [Environment Variables](/no-mistakes/reference/environment/#nm_daemon_connect_timeout). +### branch_sync_remote_timeout + +Maximum time guarded branch synchronization (`sync`, `axi sync`, and the TUI's sync action) waits for each remote Git operation - `ls-remote` or `fetch` - before remote verification fails closed and synchronization is refused. + +| | | +| ------- | ---------------------- | +| Type | `string` (Go duration) | +| Default | `60s` | + +Accepts any positive Go `time.ParseDuration` string. + +Raise this if your environment's Git credential helper (for example `gh auth git-credential`, invoked by Git as a child process against a private remote) legitimately takes longer than the default - this is a real, non-outage latency characteristic that has been observed taking 19-22s in some environments, not a hang. It is a machine/environment setting, not a per-repository one: it is read only from global config and has no matching field in a repository's `.no-mistakes.yaml`, so a pushed branch cannot widen or narrow how long the local service waits before failing closed. It never changes the fail-closed guarantee itself - a timeout or unknown remote state still always refuses synchronization without changing files or refs, whatever this value is set to. + ### log_level Daemon log verbosity. @@ -287,14 +480,45 @@ Per-run agent session reuse for the review loop's fixer role. | Type | `bool` | | Default | `true` | -When enabled and the pipeline agent supports native session resume (claude via `--resume`, codex via `exec resume`), each run keeps one durable fixer session across its review-fix turns. +When enabled and the pipeline agent supports native session resume (Claude or Grok via `--resume`, Codex via `exec resume`, Pi via `--session `, Antigravity via `--conversation `), each run keeps one durable fixer session across its review-fix turns. Review turns - the initial full review and every full rereview - always run as fresh, session-free invocations regardless of this setting: a rereview certifies fixes that implement the previous review turn's findings, so it must never resume the session that prescribed them; cross-round review context travels only in the explicit sanitized round history. The fixer session is never lent to review turns, other pipeline steps stay session-isolated in their own cold invocations, and different runs never reuse identities. -When resume is unavailable or fails, the fix turn falls back to a cold run or a fresh fixer session and the fallback is recorded in the local `agent_invocations` performance record. -Session identities are persisted only as minimum local resume metadata, never as prompts or transcripts. +When resume is unavailable or fails, the fix turn falls back to a cold run or a fresh fixer session and the fallback is recorded in the local `agent_invocations` performance record. Pi emits per-invocation usage after a resume, unlike Codex's cumulative session counters. +Session identities are persisted only as minimum local resume metadata, never as prompts or transcripts; Pi's own session directory retains its native transcript. Keep Pi's session directory private, and keep any `--session-dir` or `PI_CODING_AGENT_SESSION_DIR` setting stable while a run is active so a daemon restart can find the fixer session. The [daemon crash-recovery reference](/no-mistakes/concepts/daemon/#crash-recovery) owns which parked gates can resume or reconcile after a restart. Set `false` to force every agent invocation cold. +### worktree_roots + +Where a repository's pipeline run worktrees are created. + +| | | +| ------- | ----------------------------------------------- | +| Type | `map[string]string` | +| Keys | Absolute registered checkout paths (what you ran `no-mistakes init` in) | +| Values | Absolute directory paths | +| Default | Empty (`/worktrees//`) | + +By default a run worktree is created under `NM_HOME`, outside every checkout, so directory-scoped toolchain configuration (mise, direnv) never reaches it: those tools resolve their settings by path ancestry. +Point a checkout at a directory of your own and its runs are created at `/` instead, inheriting whatever that directory configures. +A relative value is rejected at load time, because the daemon that reads it has an unrelated working directory. + +The directory stays yours. no-mistakes never enumerates it: the only directories it touches there are the exact ones its own run records name, which is what startup cleanup, orphan-process reaping, and `no-mistakes eject` all go by. Anything else in it - your files, your scratch checkouts, and a directory that merely looks like a run worktree but no run created - is never read, never swept, never signalled, and never removed. + +Each checkout needs its own root: two entries pointing at the same directory, two spellings of one checkout, or a root equal to its checkout are rejected at load time, and `init --worktree-root` refuses a directory another checkout already claims. + +Two more values are refused at daemon startup, because they cannot work: + +- **Inside `NM_HOME`.** It collides with no-mistakes' own state - under `worktrees` a run worktree is indistinguishable from the per-repository directories the default placement owns, and under `logs` a run's worktree *is* its log directory, so removing the worktree at run end would take the run's logs with it. +- **Inside any checkout.** The run worktree is then an untracked directory in that checkout while the run executes, so the checkout is dirty and [branch synchronization](/no-mistakes/reference/cli/#no-mistakes-sync) refuses to move it until the run finishes. That holds whether the victim is the checkout whose own runs land there or an unrelated gated one, so the daemon refuses a root inside any repository it has registered. Registering a repository *around* an already configured root is refused by `no-mistakes init` itself, so you can still place that checkout elsewhere or repoint the entry; anything that reaches the configuration another way is caught at the next daemon start. + +Changing an entry affects new runs only. +Each run records the directory it was created in, so editing, adding, or removing an entry never retargets a run that already exists - resuming it after a restart, reading its diff, cleaning it up, reaping processes left standing in it, and ejecting its repository all keep using the directory that run actually has, including after you point the checkout somewhere else. + +The key is matched against the checkout path recorded at `init`. After moving a checkout, re-run `no-mistakes init` from the new path and update the key; a key that matches no registered repository is reported in the daemon log at startup and otherwise does nothing. + +`no-mistakes init --worktree-root ` prints the exact entry to add for the checkout you are initializing. The global config is hand-maintained, so init never rewrites it for you. + ### auto_fix Maximum follow-up auto-fix attempts per step. Set a step to `0` to disable the follow-up auto-fix loop, so findings require manual approval. @@ -312,7 +536,7 @@ For empty `commands.lint`, the document step's combined housekeeping pass also a | `auto_fix.test` | `int` | `3` | Test failure auto-fix attempts | | `auto_fix.document` | `int` | `3` | Not used by the automatic document pass | | `auto_fix.lint` | `int` | `3` | Lint issue auto-fix attempts | -| `auto_fix.ci` | `int` | `3` | CI auto-fix attempts for CI failures, plus GitHub, GitLab, and Azure DevOps merge conflicts | +| `auto_fix.ci` | `int` | `3` | CI auto-fix attempts for CI failures, plus GitHub, GitLab, Forgejo, and Azure DevOps merge conflicts | Legacy alias: `auto_fix.babysit`. @@ -320,7 +544,8 @@ These are global defaults. Per-repo config can override individual steps. ### ci.rerun_transient -How many times the CI step may re-run a single check the provider reported as cancelled before that check reaches an approval gate. +How many times the CI step may re-run a single provider-attributed check before that check reaches an approval gate. +This covers cancellations on supported providers and, when the value is positive, opts GitHub into detecting jobs that failed before any repository step ran. | | | |---|---| @@ -340,7 +565,7 @@ The per-repo [`ci.rerun_transient`](/no-mistakes/reference/repo-config/#cirerun_ ### commit.fix_message -Template for the subject of commits created by the shared Review, Test, Document, and Lint fix path. +Template for the subject of commits created by the Review, Test, Document, Lint, and CI repair paths. | | | | --- | --- | @@ -351,7 +576,7 @@ The template supports literal text and two Go-style placeholders: | Variable | Value | | --- | --- | -| `{{.Step}}` | Pipeline step name, such as `review`, `test`, `document`, or `lint` | +| `{{.Step}}` | Pipeline step name, such as `review`, `test`, `document`, `lint`, or `ci` | | `{{.Summary}}` | Sanitized one-line summary returned by the fix agent, or the step's deterministic fallback summary | The value must be a valid UTF-8 template that renders to a non-empty, single-line commit subject. @@ -362,7 +587,7 @@ Template functions, control actions, named templates, unknown placeholders, malf The blocked format set includes every Unicode `Bidi_Control` code point plus `U+00AD`, `U+180E`, `U+200B`, `U+2060` through `U+2064`, the deprecated bidi controls `U+206A` through `U+206F`, `U+FEFF`, `U+FFF9` through `U+FFFB`, and Unicode tag characters in `U+E0000` through `U+E007F`. Legitimate `U+200C` zero-width non-joiner and `U+200D` zero-width joiner text shaping remains allowed. The final rendered subject is validated again, so unsafe characters in an agent-provided summary are also rejected. -The setting does not change commit subjects created by the Rebase, CI, or Push steps. +The setting does not change commit subjects created by the Rebase or Push steps. A per-repo [`commit.fix_message`](/no-mistakes/reference/repo-config/#commitfix_message) value overrides this global setting. ### intent @@ -463,4 +688,4 @@ These are operator settings for this machine's local disk, so they are global-on ## Environment variables -See [Environment Variables](/no-mistakes/reference/environment/) for `NM_HOME`, `NM_DAEMON_CONNECT_TIMEOUT`, Bitbucket Cloud credentials, and update-check suppression. +See [Environment Variables](/no-mistakes/reference/environment/) for `NM_HOME`, `NM_DAEMON_CONNECT_TIMEOUT`, Forgejo host and token settings, Bitbucket Cloud credentials, and update-check suppression. diff --git a/docs/src/content/docs/reference/pipeline-steps.md b/docs/src/content/docs/reference/pipeline-steps.md index 7ca95d361..e4864bdbc 100644 --- a/docs/src/content/docs/reference/pipeline-steps.md +++ b/docs/src/content/docs/reference/pipeline-steps.md @@ -5,7 +5,7 @@ description: Reference for each step in the validation pipeline. This is the per-step reference. For the overview and rationale, see [Pipeline](/no-mistakes/concepts/pipeline/). For the fix loop, see [Auto-Fix Loop](/no-mistakes/concepts/auto-fix/). -``` +```text intent → rebase → review → test → document → lint → push → pr → ci ``` @@ -16,18 +16,29 @@ This is a soft boundary, not OS-level sandbox enforcement. The steering still allows requested test evidence under the run's managed evidence directory, plus incidental temp or cache writes from normal development tools. Configured shell commands and one-shot agent subprocesses are scoped to their step: when the invocation exits, fails, or is cancelled, no-mistakes terminates remaining child processes it spawned so background workers do not outlive the run. When configured Test or Lint command output exceeds 64 KiB, the complete output remains in the authoritative step log while findings, IPC responses, and repair prompts receive a valid-UTF-8 head-and-tail projection capped at 64 KiB. The truncation marker reports the exact original and omitted byte counts and points to `no-mistakes axi logs --step --full` for the complete output. -Commits created by the shared Review, Test, Document, and Lint fix path use the configurable [`commit.fix_message`](/no-mistakes/reference/global-config/#commitfix_message) template. +Commits created by the shared Review, Test, Document, and Lint fix path, plus CI repair commits, use the configurable [`commit.fix_message`](/no-mistakes/reference/global-config/#commitfix_message) template. +The shared correction commits, and the Push step's commit of leftover changes from a pipeline agent or formatter, are machine-authored records of pipeline output. Each is created with the complete local commit-hook family suppressed by combining `--no-verify` with an empty temporary `core.hooksPath` for that invocation, so `pre-commit`, `prepare-commit-msg`, `commit-msg`, and `post-commit` do not run. This lets a disposable run worktree commit a correction even when a tracked hook depends on generated untracked runtime files that do not exist there - the canonical case is `core.hooksPath=.husky` with a tracked hook that sources the absent `.husky/_/husky.sh`. +The suppression is limited to those correction-commit invocations. It does not change the repository, Git configuration, or daemon environment; CI repair commits and all other commit paths keep normal hook behavior. The Review, Test, Document, Lint, Push, PR, and CI gates remain the authoritative checks on what these commits contain. Agent roles that can write, repair, or review tests reject tests whose only evidence is matching implementation source text, tokens, syntax, or incidental snapshots. They instead require an executable interface or a typed or normalized semantic model that proves observable behavior. Reading a file remains valid when that file is itself an owned output or data contract, and deterministic tests may inspect the final emitted agent prompt as a generated interface; model interpretation is reserved for development-only evaluation. Review flags every newly added violation and requires same-pattern tests encountered directly in the accepted change's scope to be removed or made semantic, without expanding the change into a repository-wide test cleanup. +## Finding decision history + +When a human resolves a findings gate with Approve, Skip, or Abort without selecting a fix, no-mistakes records that the round's findings were declined. A gate with no findings records no decision. When the human selects only some findings to fix, the unselected complement is recorded as declined; findings merely left out by automatic filtering remain undecided. + +Review, Test, Document, and Lint agent prompts receive a sanitized history containing the current step's earlier rounds, decisions from other steps in the same run, and a bounded window of decisions from earlier runs on the same branch. A recorded decision takes precedence over conflicting user-intent wording, and later decisions about the same concern supersede earlier ones. Completing Review does not clear branch decisions. + +This context is advisory and fails open. It tells agents not to implement or re-report a declined finding unless the current code introduces a materially different problem, but it does not block a step or commit and is not a reversion detector. Rebase and CI fix prompts do not receive this decision history. + ## Intent Uses explicit intent when a run provides it, including exact explicit intent inherited by a rerun, otherwise infers the author's intent from recent local Claude Code, Codex, OpenCode, Rovo Dev, Pi, or GitHub Copilot CLI transcripts. This is best-effort context, and when available it is included in rebase fixes, review checks and fixes, test detection, evidence validation, and fixes, documentation checks and fixes, lint detection and fixes, CI auto-fixes, and PR drafting. **Behavior:** + - Treats newly supplied explicit intent (`agent`) and exact inherited rerun intent (`rerun`) as authoritative acceptance criteria, while preserving their distinct sources, and skips transcript-based inference even when `intent.enabled` is false - Runs transcript-based inference only when `intent.enabled` is true - Matches local agent transcripts against non-deleted changed files when present, falling back to all changed files for all-deletion diffs, may use the configured pipeline agent to disambiguate plausible matches, and summarizes the likely author intent with that agent @@ -43,19 +54,22 @@ It can fail the run only if cleanup fails after the disambiguation agent leaves Fetches the latest authoritative remote state, fetches the configured pushed-branch target, and rebases your branch onto those refs. +The integration branch used below is the [PR base branch](/no-mistakes/reference/repo-config/#prbase_branch): the repository's forge default branch, or the trusted [`pr.base_branch`](/no-mistakes/reference/repo-config/#prbase_branch) when configured. + **Behavior:** -- Fetches `origin/` from the remote into the worktree, and also fetches the pushed branch for non-default branches unless the push rewrote branch history +- Fetches `origin/` from the remote into the worktree, and also fetches the pushed branch for non-base branches unless the push rewrote branch history - Without fork routing, the pushed-branch target is `origin/` - With GitHub fork routing, the pushed-branch target is the fork branch fetched into `refs/remotes/no-mistakes-push/` -- If the branch is not the default branch, tries rebasing onto the pushed-branch target first, then `origin/` +- If the branch is not the PR base branch, tries rebasing onto the pushed-branch target first, then `origin/` - If the push rewrote branch history, skips the pushed-branch rebase target so prior remote autofix commits do not get reintroduced -- If the push rewrote the default branch and `origin/` advanced after that rewrite, pauses for manual approval before updating the branch -- If the branch carries commits from the contributor's local default branch that are not on `origin/`, pauses with an `ask-user` finding instead of silently bundling that local work into the PR -- The local-default check is best-effort and only fires when the local default tip is ahead of `origin/` and is an ancestor of the branch `HEAD` +- If the push rewrote the PR base branch and `origin/` advanced after that rewrite, pauses for manual approval before updating the branch +- If the branch carries commits from the contributor's local default branch that are not on `origin/`, pauses with an `ask-user` finding instead of silently bundling that local work into the PR +- The local-default check is best-effort and only fires when the local default tip is ahead of `origin/` and is an ancestor of the branch `HEAD` - Skips targets that don't exist or are already ancestors - If a fast-forward is possible, does a hard-reset instead of a rebase -- If the diff against the default branch is empty after rebase, completes rebase and skips all remaining pipeline steps +- If the diff against the PR base branch is empty after rebase, completes rebase and skips all remaining pipeline steps - On conflict: records conflicting files, aborts the rebase, and reports findings +- Bounds the conflict-repair agent with [`agent_timeout`](/no-mistakes/reference/global-config/#agent_timeout): an expired budget cancels the agent and fails the step with a timeout diagnostic rather than leaving the run active indefinitely **Auto-fix:** when enabled, the agent resolves conflict markers, stages files, and runs `git rebase --continue` in a non-interactive Git environment so Git accepts the existing commit message instead of opening an editor. The prompt includes user intent when available. Manual fix rounds also include any per-conflict user notes, any selected user-authored findings from the TUI or AXI interface, and sanitized prior-round history in the prompt. The Rebase step does not synthesize a fix commit subject; `git rebase --continue` preserves the rebased commits' subjects. @@ -66,6 +80,7 @@ Fetches the latest authoritative remote state, fetches the configured pushed-bra AI code review of your diff. **Behavior:** + - Diffs the base commit against head - Filters out files matching `ignore_patterns` from the repo config - Sends the filtered diff to the agent with structured review instructions and a structured output schema @@ -84,12 +99,13 @@ AI code review of your diff. - Also returns a `risk_level` (`low`, `medium`, `high`) and `risk_rationale` - Runs every review turn - the initial review and every full rereview - as a fresh, session-free invocation, so the rereview that certifies a fix round never resumes the session whose findings prescribed those fixes; the rereview prompt additionally reframes fix-round changes as pipeline-authored code to review under the same adversarial standard as the author's changes, with prior findings, fix summaries, and same-round tests treated as claims rather than evidence - When a review-step fixer round commits and its re-review does not complete, persists that branch's uncertified commit range (lint and document fixer commits do not); the next run's initial review of that range receives the same pipeline-authored provenance framing so the replacement reviewer is not cold. A later rebase remaps the persisted SHAs onto the rewritten head. The range is cleared only after a completed review whose approved head equals or descends from the range tip; parked, failed, skipped, and aborted reviews leave it in place -- With the default `session_reuse: true`, Claude and Codex reuse one durable fixer session across review-fix turns; a resume failure retries the same fix turn in a fresh fixer session, and unsupported agents run cold +- With the default `session_reuse: true`, Claude, Codex, Grok, Pi, and Antigravity reuse one durable fixer session across review-fix turns; a resume failure retries the same fix turn in a fresh fixer session, and unsupported agents run cold +- Bounds its agent turns with [`review_agent_timeout`](/no-mistakes/reference/global-config/#review_agent_timeout): a round's optional fix turn and its rereview turn share one budget, each later auto-fix round starts a fresh one, and an expired budget cancels the agent and fails the step with a timeout diagnostic rather than leaving the run active indefinitely - Atomically records the exact commit examined when a full review completes successfully; a parked review retains its candidate only for recovery, while failed, skipped, superseded, and legacy reviews grant no inferred approval authority **Approval:** required if any finding has severity `error` or `warning`. Findings with `action: ask-user` pause for approval instead of entering the normal auto-fix loop. This is for findings that challenge the author's intent, not routine correctness, reliability, or security fixes that may need to re-add a small amount of deleted logic. With the default `auto_fix.review: 0`, blocking review findings park for approval even when their action is `auto-fix`; setting repo or global `auto_fix.review` above `0` re-enables the automatic review fix loop for eligible `auto-fix` findings. Findings with `action: no-op` are informational only. The shared [finding-action model](/no-mistakes/concepts/auto-fix/#finding-actions) owns the behavior for a missing `action`. -**Auto-fix:** the agent receives the selected previous findings plus any per-finding user notes, any selected user-authored findings from the TUI or AXI interface, and a sanitized history of prior rounds for that step, including earlier fix summaries and which findings the user left unselected. +**Auto-fix:** the agent receives the selected previous findings plus any per-finding user notes, any selected user-authored findings from the TUI or AXI interface, and the shared [finding decision history](#finding-decision-history), including earlier fix summaries for this step. The fixer applies all selected fixes before running one focused verification limited to the changed area, and it is instructed not to run the complete repository test or lint suite during the fix round. The dedicated Test and Lint steps after review remain the authoritative gates, although their coverage may be focused when commands are unconfigured. Follow-up review passes use the history to avoid re-reporting user-ignored findings unless the code now has a materially different problem. @@ -107,18 +123,20 @@ Local Test is never a repository-wide regression-suite substitute; broad regress [`commands.test`](/no-mistakes/reference/repo-config/#commandstest) owns the configuration contract for any explicit baseline command. **Behavior:** + - If `commands.test` is set in repo config: runs it first as a baseline via the platform shell (`sh -c` on POSIX, `cmd.exe /c` on Windows) and captures output. Non-zero exit produces `error` findings. Configure a **targeted** command here (see repo-config); do not treat this field as CI-parity complete-suite configuration. - If `commands.test` is empty, or user intent is available after the baseline command passes: the agent validates the change with the **smallest relevant** evidence-oriented tests or manual checks, returning structured findings with severity, description, and `action` (`no-op`, `auto-fix`, `ask-user`). Both the normal evidence agent and the Test-repair agent are instructed not to run the complete repository test suite; a generic driver instruction asking for broad or full-suite confirmation does not override that product boundary. For UI, HTML, CSS, browser, visual layout, or copy-placement changes, the agent attempts reviewer-visible visual evidence and explains in `testing_summary` when screenshots, images, videos, GIFs, or rendered HTML artifacts are not captured. +- Bounds those agent turns with [`test_agent_timeout`](/no-mistakes/reference/global-config/#test_agent_timeout): each evidence-gathering or Test-repair invocation gets its own budget, and an expired budget cancels the agent and fails the step with a timeout diagnostic rather than leaving the run active indefinitely - "Do not run everything" is not "run nothing": when no targeted check can establish the intent, the agent must write or improve a focused test, perform manual verification with evidence, or report a warning finding that sufficient targeted evidence is not possible. - The step records the exact tests and checks it exercised in a `tested` array, may include a short natural-language `testing_summary`, and includes an `artifacts` array for reviewer-visible evidence; `path` artifacts may be repository-relative paths or absolute paths under the run's evidence directory, `url` artifacts must be externally visible, and `content` artifacts should be short logs or command output shown directly in the PR. - Evidence is always collected under the run's evidence directory (`/evidence/` by default, see [`test.evidence`](/no-mistakes/reference/global-config/#testevidence)), outside the worktree, so artifacts never enter the branch being validated. On GitHub, [`test.evidence.store_in_repo: true`](/no-mistakes/reference/global-config/#testevidence) makes the PR step publish that directory to the push-target repository's orphan evidence branch under `/` and link the artifacts from the PR body. The config reference owns provider support and fail-closed behavior. - Before finishing, test agents are instructed to remove transient working-tree artifacts they created, such as downloaded models, caches, build outputs, large binaries, or generated data directories, while preserving intentional source or test-file changes and evidence files under the dedicated evidence directory. -- Missing evidence for user intent can be reported as a warning with `action: ask-user`. +- Missing evidence for user intent can be reported as a warning with `action: ask-user`. When a host capability or OS permission is unavailable to the agent process, the agent is instructed to name the specific capability or permission and explain how to grant it before the test is rerun. - If the agent creates new test files (detected via `git status --porcelain`), they are recorded as informational `no-op` findings and do not require approval when tests pass. **Approval:** test findings with `action: ask-user` pause for approval, including missing-evidence warnings for user intent. `action: auto-fix` findings stay eligible for the fix loop. `action: no-op` findings are informational only. -**Auto-fix:** the agent receives the previous test findings plus any per-finding user notes, any selected user-authored findings from the TUI or AXI interface, and a sanitized history of prior rounds for that step, including earlier fix summaries and any findings the user left unselected in prior approval cycles. Repair mode reproduces the specific failure, applies a root-cause fix, and re-runs only focused verification - not a complete-suite confirmation - then the step's configured baseline (if any) and evidence path run again. +**Auto-fix:** the agent receives the previous test findings plus any per-finding user notes, any selected user-authored findings from the TUI or AXI interface, and the shared [finding decision history](#finding-decision-history), including earlier fix summaries for this step. Repair mode reproduces the specific failure, applies a root-cause fix, and re-runs only focused verification - not a complete-suite confirmation - then the step's configured baseline (if any) and evidence path run again. **Default auto-fix limit:** `3`. @@ -127,6 +145,7 @@ Local Test is never a repository-wide regression-suite substitute; broad regress Updates matching documentation for code changes and reports only unresolved gaps. **Behavior:** + - Diffs the base commit against head and skips the step if there are no non-ignored changed files to document - Asks the agent to find every documentation gap, update docs or doc comments for all gaps it can resolve, verify its edits, and commit any documentation changes under the placement policy - The placement policy gives each fact one authoritative owner, prefers removing stale duplicates or replacing them with pointers, avoids new documentation surfaces for perceived gaps, and keeps durable incident lessons near their owner instead of in `AGENTS.md` @@ -135,8 +154,9 @@ Updates matching documentation for code changes and reports only unresolved gaps - Includes user intent when available - Returns findings only for unresolved documentation gaps or human judgment calls - Requires approval whenever any unresolved documentation finding is returned, including `info` findings +- Bounds the documentation (and combined housekeeping) agent with [`agent_timeout`](/no-mistakes/reference/global-config/#agent_timeout): an expired budget cancels the agent and fails the step with a timeout diagnostic rather than leaving the run active indefinitely -**Auto-fix:** documentation fixes happen during the initial document pass. Unresolved findings pause for approval instead of starting another automatic document/fix loop. If you manually trigger a fix from the TUI or AXI interface, the agent receives the selected previous findings plus any per-finding user notes, any selected user-authored findings, and sanitized prior-round history. +**Auto-fix:** documentation fixes happen during the initial document pass. Unresolved findings pause for approval instead of starting another automatic document/fix loop. If you manually trigger a fix from the TUI or AXI interface, the agent receives the selected previous findings plus any per-finding user notes, any selected user-authored findings, and the shared [finding decision history](#finding-decision-history). **Default auto-fix limit:** not used for automatic document follow-up loops. @@ -145,15 +165,17 @@ Updates matching documentation for code changes and reports only unresolved gaps Runs linters and static analysis. **Behavior:** + - If `commands.lint` is set: runs it via the platform shell (`sh -c` on POSIX, `cmd.exe /c` on Windows). Non-zero exit produces `warning` findings. - If `commands.lint` is empty: consumes lint-category findings from the document step's combined housekeeping pass, avoiding a second cold agent invocation. If no usable combined result exists, the lint step detects appropriate linters/formatters, applies safe fixes, reruns the relevant checks, commits any agent changes, and returns structured findings only for unresolved issues. +- Bounds those agent turns, including a configured-lint repair turn, with [`agent_timeout`](/no-mistakes/reference/global-config/#agent_timeout): an expired budget cancels the agent and fails the step with a timeout diagnostic rather than leaving the run active indefinitely **Approval:** lint findings with `action: ask-user` pause for approval. `action: auto-fix` findings stay eligible for the fix loop when `commands.lint` is configured. `action: no-op` findings are informational only. Combined-pass lint findings use the same gate: `error` and `warning` findings pause for a decision, while `info` findings do not. -**Auto-fix:** when `commands.lint` is configured, the lint step follows the same pattern as test - the agent fixes `action: auto-fix` issues using the previous findings plus any per-finding user notes, any selected user-authored findings from the TUI or AXI interface, and a sanitized history of prior rounds for that step, including earlier fix summaries and any findings the user left unselected in prior approval cycles, then lint re-runs. +**Auto-fix:** when `commands.lint` is configured, the lint step follows the same pattern as test - the agent fixes `action: auto-fix` issues using the previous findings plus any per-finding user notes, any selected user-authored findings from the TUI or AXI interface, and the shared [finding decision history](#finding-decision-history), including earlier fix summaries for this step, then lint re-runs. When `commands.lint` is empty, unresolved findings from the combined pass pause for approval instead of starting another automatic lint/fix loop, because the agent already attempted safe fixes during housekeeping. **Default auto-fix limit:** `3`. @@ -163,8 +185,9 @@ When `commands.lint` is empty, unresolved findings from the combined pass pause Pushes the validated branch to the configured push target. **Behavior:** + - If `commands.format` is set, runs it first -- Commits any uncommitted agent changes with message `no-mistakes: apply agent fixes` +- Commits any uncommitted changes left by pipeline agents or the formatter with message `no-mistakes: apply agent fixes` - Without fork routing, successful run-start validation selects the upstream URL from the working clone; when it matches the gate worktree's `origin`, the worktree URL is used so embedded credentials retained outside the database can authenticate. If validation fails, the run continues with its prior routing. - With GitHub fork routing, the push target is `repos.fork_url` - Immediately before remote mutation, reloads the durable review-approved commit and refuses to push when that binding is missing, malformed, or unreachable @@ -189,28 +212,31 @@ This step never requires approval - it runs automatically after review, test, do Creates or updates a pull request. **Skipped when:** -- The branch is the default branch -- The upstream host is not GitHub, GitLab, Bitbucket Cloud (`bitbucket.org`), or Azure DevOps (`dev.azure.com` / `*.visualstudio.com`) -- The provider CLI (`gh` or `glab`) is not installed for GitHub or GitLab -- The provider CLI is not authenticated for GitHub or GitLab +- The branch is the [PR base branch](/no-mistakes/reference/repo-config/#prbase_branch) (the repository's forge default branch, or the trusted `pr.base_branch` when configured) +- The upstream host is not GitHub, GitLab, Forgejo, Bitbucket Cloud (`bitbucket.org`), Azure DevOps (`dev.azure.com` / `*.visualstudio.com`), or Gitea +- The provider CLI (`gh`, `glab`, `forgejo-axi`, or `tea`) is not installed for GitHub, GitLab, Forgejo, or Gitea +- The provider CLI is not authenticated for GitHub, GitLab, Forgejo, or Gitea - Bitbucket Cloud credentials are missing (`NO_MISTAKES_BITBUCKET_EMAIL` or `NO_MISTAKES_BITBUCKET_API_TOKEN`) - The `az` CLI with the `azure-devops` extension is not installed or not authenticated for Azure DevOps -- A legacy or manually edited GitLab, Bitbucket, or Azure DevOps repo record has `fork_url` set, because fork MR/PR routing is currently GitHub-only +- A legacy or manually edited non-GitHub repo record has `fork_url` set, because fork MR/PR routing is currently GitHub-only **Behavior:** -- Checks for an existing PR on the branch -- If one exists, updates it. If not, creates a new one. -- Uses the provider CLI for GitHub/GitLab, the `az` CLI for Azure DevOps, and the Bitbucket API for Bitbucket Cloud +- Checks for an existing PR on the branch, matching by branch alone rather than filtering by base, so a still-open PR against a since-changed [`pr.base_branch`](/no-mistakes/reference/repo-config/#prbase_branch) is found and updated instead of orphaned behind a duplicate +- 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 :` - 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. -- `## Pipeline` keeps the existing human-readable signature and includes the stable structured step attestation documented below. +- `## Pipeline` keeps the existing human-readable signature and includes the stable structured step attestation documented below. Bitbucket Cloud PR descriptions omit HTML-only features (`
`, ``, `