Skip to content

feat(config): support safe custom PR base branches - #2

Merged
pablog12 merged 39 commits into
mainfrom
fm/no-mistakes-pr-base-b6
Aug 6, 2026
Merged

pablog12 merged 39 commits into
mainfrom
fm/no-mistakes-pr-base-b6

Conversation

@pablog12

@pablog12 pablog12 commented Aug 4, 2026 •

Copy link
Copy Markdown

Intent

Add a backward-compatible trusted repository setting such as pr.base_branch so feature PRs and all base-dependent pipeline operations can target an integration branch like quality-assurance while unset repositories continue using the GitHub default branch. Keep the actual default branch as the security trust root, strictly validate configured targets and shared history, protect both default and configured base branches from direct runs, preserve fork delivery, and use immutable per-run target snapshots where appropriate. Ensure reruns and recovery preserve existing-PR continuity across crashes, legacy records, configuration changes, forge outages, and open/closed/reopened PR states without retargeting or duplicate PR creation. Cover every affected fresh-run, rerun, recovery, Intent, Rebase, CI, PR, identity, cleanup, and failure path with focused tests, and update configuration, setup, troubleshooting, CLI, and release documentation. Keep the change narrowly scoped and validate the exact current pipeline head without weakening checks; additional security scanning can be pursued separately after this capability is working.

What Changed

  • Add trusted pr.base_branch configuration so PRs and base-dependent pipeline steps can target integration branches while preserving the default branch as the trust root.
  • Protect configured and default branches, persist immutable base snapshots, and validate ancestry before delivery, CI approval, and repair pushes.
  • Preserve existing PR targets across reruns and recovery, harden forge response parsing, and document the new configuration and continuity semantics.

Risk Assessment

⚠️ Medium: The change spans security-sensitive PR routing, recovery, and delivery lifecycles, but the full source review found no remaining substantiated defect.

Testing

After baseline repository inspection and provisioning a transient worktree-local Go 1.25 toolchain because Go was absent, I exercised trusted configuration, protected branches, immutable base snapshots, fork/provider routing, delivery revalidation, CI monitoring, persisted PR continuity, reopened/legacy recovery, and approval boundaries; all focused checks passed and the transient toolchain was removed. No screenshot was applicable because this is non-UI pipeline behavior; the reviewer-visible artifact is the simulated provider CLI transcript.

Evidence: Configured integration-branch PR routing transcript
=== RUN   TestResolveBranchBaseSHA_UsesConfiguredPRBase
=== RUN   TestResolveBranchBaseSHA_UsesConfiguredPRBase/pr_summary_and_routing
    pr_base_test.go:187: provider CLI transcript:
        auth status --hostname github.com
        pr list --head feature --base quality-assurance --repo test/repo --state open --json number,url
        pr create --head feature --base quality-assurance --repo test/repo --title feat: target qa --body-file -
        stdin --body ## What Changed
        
        - add feature
--- PASS: TestResolveBranchBaseSHA_UsesConfiguredPRBase (0.64s)
    --- PASS: TestResolveBranchBaseSHA_UsesConfiguredPRBase/pr_summary_and_routing (0.32s)
PASS
ok  	github.com/kunchenguid/no-mistakes/internal/pipeline/steps	1.657s

Pipeline

Updates from git push no-mistakes

✅ **intent** - passed

✅ No issues found.

✅ **Rebase** - passed

✅ No issues found.

🔧 **Review** - 2 issues found → auto-fixed (15) ✅
  • 🚨 internal/daemon/manager.go:958 - Fresh replacement runs preserve continuity from only the newest prior run on the branch. If an older QA-targeted PR is closed, a newer staging-based run is recorded, and then the QA PR is reopened, the next replacement examines only the staging record and can create/update a staging PR while the reopened QA PR remains open. Enumerate distinct persisted base candidates for the branch, verify them authoritatively, preserve the sole open PR, and fail closed if multiple bases are open.
  • 🚨 internal/daemon/manager.go:881 - The continuity check treats (nil, nil) from FindPR as authoritative absence, but GitHub and GitLab currently return (nil, nil) when their successful CLI output is malformed or contains unparseable chatter. After a crash before PR URL persistence, that path drops the saved base and can create a duplicate against the new configured base. Make provider FindPR parsers return errors for malformed/non-empty responses or invalid PR shapes so continuity fails closed on unresolved forge output.

🔧 Fix: Preserve PR continuity and reject malformed forge responses
2 errors still open:

  • 🚨 internal/daemon/manager.go:884 - A persisted merged state skips the authoritative head/base lookup. If an older closed PR for the same head/base is reopened after a newer PR was merged, base deduplication selects the newer run, skips FindPR, and can create a PR against the newly configured base while the reopened PR remains open. Always query each distinct persisted head/base tuple; only use merged state after that lookup proves no open PR exists.
  • 🚨 internal/daemon/manager.go:841 - Legacy runs with a null PR-base snapshot are assigned the repository's current default branch. After an open legacy PR targeting main exists and init refreshes the default branch to trunk, continuity treats that PR as targeting trunk; because this equals the current base, verification is skipped and a duplicate trunk PR can be created. Resolve legacy open PRs by head without a synthesized base, obtain their authoritative target branch through the provider boundary, and persist it before continuity selection.

🔧 Fix: Harden reopened and legacy PR continuity
1 error still open:

  • 🚨 internal/daemon/manager.go:998 - Verified-base deduplication keeps the first candidate's explicit flag. If a user selects an older legacy null-base run, its base-less lookup can discover the same open staging PR recorded by a newer explicit run; the newer candidate is then discarded, so the replacement persists explicit=false and skips immutable snapshot/shared-history validation for staging after a config change. When verified candidates converge on one base, preserve explicit=true if any candidate carries it.

🔧 Fix: Preserve explicit semantics across converged PR bases
2 errors still open:

  • 🚨 internal/daemon/manager.go:860 - Branch-level candidate deduplication discards the duplicate candidate's explicit flag. If an older run targeted main implicitly, pr.base_branch: main was later configured explicitly, and a rerun selects the older run, the implicit preferred candidate wins and the replacement bypasses immutable snapshot/shared-history validation. Merge duplicate metadata so explicit remains true when any persisted candidate for that branch is explicit.
  • 🚨 internal/daemon/manager.go:1054 - PR continuity evidence is snapshotted before cancelActiveRuns waits for the existing executor. That executor can start or create its old-base PR after this query; following a configuration change, the replacement never verifies that base and can create a second PR against the new base. Cancel and await the prior run before loading runs and started-PR evidence.

🔧 Fix: Preserve cancellation-time PR continuity and explicit base semantics
1 error still open:

  • 🚨 internal/daemon/manager.go:1227 - An open historical PR whose base was implicit can disable the current explicit configuration when both resolve to the same branch. The continuity candidate's explicit=false overwrites the current true; ensureConfiguredPRBaseBranch then clears ResolvedBaseSHA, so this and future replacements use a mutable base without explicit-target shared-history validation. When continuity selects the currently configured branch, preserve explicit=true from the current config before applying the candidate metadata.

🔧 Fix: Preserve explicit PR-base semantics across implicit continuity
1 error still open:

  • 🚨 internal/daemon/manager.go:817 - When pr.base_branch is unset, the guard returns before checking the repository default branch. A direct git push gate main bypasses CLI preflight, starts a default-branch pipeline, and lets PushStep publish pipeline changes to the security trust root. Always reject branch == defaultBranch before conditionally checking an explicit configured PR base.

🔧 Fix: Reject default-branch runs without explicit PR configuration
4 errors still open:

  • 🚨 internal/daemon/manager.go:891 - Every started PR step is treated as continuity evidence, including steps that completed as skipped before any Find/Create call (for example, an unavailable CLI or unsupported routing). The next replacement then requires the same unavailable host and fails before running, although the completed skip proves no PR was created. Exclude completed skipped PR steps while retaining running/failed ambiguity and persisted PR evidence.
  • 🚨 internal/daemon/manager.go:239 - Recovery validates the newly configured PR base against the old run's HEAD before consulting its persisted immutable base snapshot. If a parked QA-targeted run is recovered after trusted config changes to a valid staging branch with unrelated history, recovery fails even though the preserved QA snapshot remains usable. Guard the current branch name, but resolve the persisted snapshot first; validate the current target only when it supplies a missing same-base snapshot.
  • 🚨 internal/scm/azuredevops/azuredevops.go:141 - Azure FindPR still treats JSON null as authoritative absence and accepts invalid entries such as [{}] when a base is supplied. During crash recovery before PR URL persistence, this can discard the saved base and permit a duplicate PR against the new configuration. Reject null arrays and PR entries without a valid ID/browsable URL.
  • 🚨 internal/bitbucket/client.go:136 - Bitbucket FindPR treats malformed successful responses such as {} or null as an empty values list, and a base-filtered [{}] becomes a PR with no usable identity. Continuity then interprets the unresolved response as no open PR and can create a duplicate against a changed base. Require the expected response shape and validate the returned PR identity before reporting absence or a match.

🔧 Fix: Harden PR recovery evidence and provider parsing
1 error still open:

  • 🚨 internal/daemon/manager.go:242 - Startup recovery still assigns today’s repository default to every legacy null-base run. If a legacy parked CI gate has an open PR targeting main and the default is renamed to trunk, recovery resumes with trunk as the pipeline base, so a subsequent CI repair can rebase against trunk while the PR remains based on main. For null-base runs with PR evidence, resolve the open PR by head without a base and persist its provider-reported base before falling back to the current default; fail closed on unavailable or ambiguous results.

🔧 Fix: Resolve legacy recovery PR bases authoritatively
1 error still open:

  • 🚨 internal/daemon/manager.go:962 - PR continuity executes provider commands from the bare gate repository. GitLab's FindPR/GetPRState commands do not specify --repo and depend on repository discovery from their working directory, which fails when safe.bareRepository=explicit is enabled. Consequently, GitLab reruns and legacy recovery with PR evidence fail before continuity can be verified. Pass the available run worktree into this boundary or explicitly scope GitLab commands to the project.

🔧 Fix: Scope GitLab PR continuity to explicit repositories
2 errors still open:

  • 🚨 internal/daemon/manager.go:1243 - A replacement run validates the newly configured PR target before checking persisted open-PR continuity. If an open QA-targeted PR exists and trusted config changes to a missing or history-incompatible staging target, rerun fails here instead of preserving QA, despite staging never becoming the effective target. Guard protected branch names first, verify continuity, then resolve only the selected effective base.
  • 🚨 internal/daemon/manager.go:1272 - When continuity preserves a formerly implicit default branch that differs from today’s default/configured target, explicit=false makes this resolution a no-op. The Intent step runs before Rebase, and a supported --skip=rebase run leaves every later diff owner using a stale origin/<old-base> ref; after that target is rewritten, commits included in the live PR can be excluded from review. Freshly fetch and resolve an inherited implicit base at the continuity boundary; apply the same rule to the early return in recovery at line 259.

🔧 Fix: Preserve continuity before resolving inherited PR bases
3 errors still open:

  • 🚨 internal/daemon/manager.go:265 - Recovery skips refreshing a persisted implicit base when its name matches today’s explicit pr.base_branch. Only the default branch was fetched earlier, so a parked run targeting QA can resume against a stale origin/QA, bypassing the current explicit snapshot/shared-history guarantees. Preserve current explicit semantics and resolve the target before returning, as the replacement path does.
  • 🚨 internal/daemon/manager.go:818 - The inherited-base resolver proves only that the fetched ref is a commit, not that it shares history with HEAD. If an old implicit PR base is rewritten to unrelated history and Rebase is skipped, branch-delta helpers fall back to Run.BaseSHA and can exclude changes still represented by the PR. Require a usable merge base at this continuity boundary so the run fails closed.
  • 🚨 internal/bitbucket/client.go:134 - The hardened Bitbucket parser still accepts malformed bodies with trailing JSON or garbage because doJSON decodes only the first value. A response such as {"values":[]} {"values":[...]} is treated as authoritative absence, allowing continuity to create a duplicate PR. Make the shared JSON decoder require EOF after one top-level value.

🔧 Fix: Harden recovered PR bases and Bitbucket parsing
3 errors still open:

  • 🚨 internal/pipeline/steps/ci.go:233 - Moving-base validation is skipped entirely when ci_timeout is unlimited. If the configured base snapshot is A and the base is later reset to an ancestor P, the PR widens to include P..A, yet the monitor can report checks passed without refreshing or validating the target. Validate explicit bases on every poll; keep only timeout re-arming conditional on a finite timeout.
  • 🚨 internal/pipeline/steps/common_git.go:121 - Finite-timeout refreshes require only some shared history between HEAD and the moving base. Resetting the base from snapshot A to an ancestor or sibling of A still passes, while the live PR includes commits excluded from the A..HEAD review. Require the immutable run snapshot to be an ancestor of the refreshed base before monitoring or conflict repair proceeds.
  • 🚨 internal/daemon/manager.go:931 - Same-base candidate deduplication discards every duplicate run's persisted PR URL/state. If an older URL-less run is selected, a newer same-base run records a PR, and that PR is manually retargeted while remaining open, the base-filtered lookup returns nil and the retained candidate has no URL to verify directly; continuity can then create another PR while the retargeted one remains open. Preserve and verify all distinct persisted PR identities when consolidating a head/base tuple.

🔧 Fix: Harden moving-base validation and persisted PR continuity
3 errors still open:

  • 🚨 internal/pipeline/steps/pr.go:87 - The immutable-base ancestry check runs only during CI polling. If the configured base snapshot is A, review completes, and the target is reset to ancestor P before delivery, Push publishes H and PR discovery/creation targets the mutable branch without validation; with CI skipped, the run completes while the live PR includes unreviewed P..A commits. Validate the explicit base immediately before Push, and again before PR delivery when Push is skipped.
  • 🚨 internal/pipeline/steps/ci.go:112 - Parked CI reconciliation accepts an open PR without revalidating its explicit base. A target can pass polling, reset behind the immutable snapshot while the timeout gate is parked, and then be approved with the widened PR unverified. Refresh and enforce snapshot ancestry for open PRs before allowing the parked approval to proceed.
  • 🚨 internal/pipeline/steps/ci.go:245 - Moving-base validation occurs before terminal PR-state observation. If a PR is merged or closed and its integration branch is then deleted or reset behind the snapshot before the next poll, CI returns the base error without observing the terminal lifecycle state and can fail an otherwise terminal run. Check PR state first and validate the base only while the PR remains open.

🔧 Fix: Revalidate PR bases at delivery and approval boundaries
3 issues (2 errors, 1 warning) still open:

  • 🚨 internal/pipeline/steps/pr.go:78 - PR-base validation occurs before agent-generated PR content. If snapshot A validates, content generation runs, and the target resets behind A before Find/Create, the step can create a widened PR containing unreviewed commits. Revalidate after content generation at the provider-delivery boundary, including immediately before CreatePR.
  • 🚨 internal/pipeline/steps/ci_fix.go:28 - CI repair validates the base before fetching logs and running the fix agent, but pushUpdatedHeadSHA performs no final validation. A target reset behind the snapshot during that work therefore allows the repair push to update the live PR with unreviewed commits. Revalidate in pushUpdatedHeadSHA so every CI repair push shares the pre-push invariant.
  • ⚠️ internal/pipeline/executor.go:1145 - Any approval-validation error, including a transient forge or Git outage, consumes the approval and permanently fails the run. Preserve or retry the parked gate for non-fatal validation errors, while continuing to fail on ErrFatalGateReconciliation invariant violations.

🔧 Fix: Close PR delivery, CI repair, and approval races
2 errors still open:

  • 🚨 internal/pipeline/steps/push.go:97 - Explicit-base validation occurs before resolveForcePushDecision, which may perform remote reads and fetch/patch-id work. If an existing QA-targeted PR is open and QA resets behind the immutable snapshot during that work, PushCommit still publishes the head and widens the live PR before the later PR step can reject it. Revalidate after the decision, immediately before each mutating PushCommit, as the CI-repair push path does.
  • 🚨 internal/pipeline/steps/ci.go:116 - Every explicit-base validation error is wrapped as ErrFatalGateReconciliation, including transient fetch or Git transport outages. A routine parked-gate reconciliation during such an outage therefore permanently fails the run instead of preserving the gate. Distinguish snapshot/shared-history invariant violations from transient resolution errors and mark only the former fatal.

🔧 Fix: Revalidate pushes and preserve transient CI approvals
✅ Re-checked - no issues remain.

✅ **Test** - passed

✅ No issues found.

  • git status --short, commit inspection, and targeted diff review against 6ed880de1b4ac9d28d706ae20e767e5729024891
  • go test ./internal/config -run '^(TestLoadRepoConfig_PRBaseBranch|TestLoadRepoConfig_PRBaseBranchUnsetPreservesDefaultBehavior|TestLoadRepoConfig_PRBaseBranchRejectsMalformedExplicitValues|TestLoadRepoConfig_PRBaseBranchRejectsUnsafeNames|TestEffectiveRepoConfig_PRBaseBranchTrustedOnly)$' -count=1
  • go test ./internal/cli -run '<configured-base preflight, trusted-config, wizard branch-protection selectors>' -count=1
  • go test ./internal/pipeline/steps -run '<configured PR base, Rebase, Push, PR, Intent, and CI selectors>' -count=1
  • go test ./internal/daemon -run '<PR-base protection, continuity, rerun, replacement, and recovery selectors>' -count=1
  • go test ./internal/scm/github ./internal/scm/gitlab ./internal/scm/azuredevops ./internal/bitbucket -run '<base-aware PR discovery and identity selectors>' -count=1
  • go test ./internal/db ./internal/pipeline ./internal/gate -run '<PR persistence, approval reconciliation, and remote identity selectors>' -count=1
  • go test -v ./internal/pipeline/steps -run &#39;^TestResolveBranchBaseSHA_UsesConfiguredPRBase$/^pr_summary_and_routing$&#39; -count=1 with the provider transcript captured as evidence
  • Focused retries of TestRerunContinuityRecognizesReopenedPR and TestLoadRecoveredConfig_ResolvesLegacyOpenPRBaseAfterDefaultRename after fixes
✅ **Document** - passed

✅ No issues found.

⚠️ **Lint** - 1 warning
  • ⚠️ linter found issues (exit code 2)
✅ **Push** - passed

✅ No issues found.

Summary by CodeRabbit

  • New Features

    • Added support for configuring a pull request’s target base branch.
    • Rebase, push, pull request creation, CI monitoring, and conflict recovery now consistently use the configured base branch.
    • Added safeguards to validate base-branch changes and prevent unsafe delivery actions.
    • Improved pull request detection across supported providers, including clearer handling of ambiguous or malformed responses.
  • Bug Fixes

    • Improved recovery, timeout handling, and cleanup for runs involving changing or invalid base branches.
    • Improved repository-scoped operations and resilience when upstream services are temporarily unavailable.
  • Documentation

    • Updated configuration, setup, pipeline, CLI, troubleshooting, and CI guidance for configurable pull request bases.

pablog12 added 30 commits August 3, 2026 13:47
@coderabbitai

coderabbitai Bot commented Aug 4, 2026 •

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Changes

Configured PR base routing

Layer / File(s) Summary
Configuration and branch guards
AGENTS.md, docs/src/content/docs/..., internal/config/..., internal/cli/..., internal/skill/..., internal/wizard/..., skills/no-mistakes/SKILL.md
Adds trusted pr.base_branch configuration, branch-name validation, base-branch-aware CLI and wizard checks, and updated guidance.
Persistence and SCM contracts
internal/db/..., internal/git/..., internal/gate/..., internal/scm/...
Stores PR-base metadata, manages private base refs, normalizes remote identity, and validates provider PR responses including destination branches.
Daemon base continuity and cleanup
internal/daemon/...
Preserves configured and inherited bases across new runs, reruns, recovery, and PR verification. Terminal cleanup removes private base refs.
Pipeline base resolution and delivery
internal/pipeline/...
Routes rebase, intent, review, test, lint, document, push, PR, CI, and approval-gate operations through the intended PR base with snapshot validation and retry handling.

Estimated code review effort: 5 (Critical) | ~120 minutes

Suggested reviewers: kunchenguid

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 15.05% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: adding support for safe custom PR base branches through configuration. It directly reflects the primary feature addition evident throughout the changeset.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch

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

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 5d76657be3

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

// `--state opened`, but glab v1.5x removed it (it now exposes
// -c/--closed, -M/--merged, -A/--all); passing the unknown flag fails the
// whole command. Rely on the open-by-default behavior.
args = append(args, h.repoArgs()...)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Scope GitLab MR mutations to the refreshed repository

When the working clone's upstream URL has changed since gate initialization, BuildHost constructs this host from the refreshed Repo.UpstreamURL, but the gate worktree's origin intentionally remains stale. This line scopes only mr list; CreatePR, UpdatePR, GetChecks, and the fallback/log-fetch mr view commands still infer the repository from that stale origin. A run can therefore push and search in the refreshed repository but create/update or monitor an MR in the former repository (or an unrelated MR with the same IID). Scope all GitLab MR/CI operations to the same resolved project.

AGENTS.md reference: AGENTS.md:L24-L24

Useful? React with 👍 / 👎.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 12

🧹 Nitpick comments (9)
AGENTS.md (1)

129-136: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Keep AGENTS.md at the policy level.

These additions restate implementation details and enumerate regression tests. Keep the durable rules here, then point to the authoritative source or verification command. This reduces guidance drift.

As per coding guidelines, "AGENTS.md must not repeat what the codebase already shows; point to the authoritative file or command instead."

Also applies to: 214-217

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

In `@AGENTS.md` around lines 129 - 136, Condense the added AGENTS.md guidance to
durable policy-level rules, removing implementation details, symbol/path
references, and enumerated regression tests. Point readers to the authoritative
configuration/documentation source and the relevant verification command
instead, while preserving the essential trust, path-matching, and CI-timeout
policies.

Source: Coding guidelines

internal/daemon/manager_trust_test.go (1)

629-704: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add coverage for the new cancelActiveRuns timeout path.

cancelActiveRuns in internal/daemon/manager.go now returns an error when a cancelled run does not finish within 30 seconds, and startRunWithIntentSource aborts the new run with stage cancel_active_runs. This test exercises the success path, where the cancelled run does finish.

The timeout path is a new failure mode that blocks every subsequent run on the branch. Consider a test that makes the first step ignore context cancellation, with the 30-second wait made injectable, so the abort and its telemetry stage are pinned.

Based on learnings: "Always use test-driven development for bug fixes and feature development".

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

In `@internal/daemon/manager_trust_test.go` around lines 629 - 704, Add a test
alongside TestReplacementWaitsForCancelledRunPRContinuityEvidence for the
cancelActiveRuns timeout path, using an injectable short wait duration instead
of the fixed 30-second delay. Make the first pipeline step ignore context
cancellation so the cancelled run remains active, then assert
startRunWithIntentSource aborts the replacement with stage cancel_active_runs
and verifies the corresponding failure telemetry; keep the test deterministic
without waiting 30 seconds.

Source: Learnings

internal/daemon/manager.go (2)

232-334: 📐 Maintainability & Code Quality | 🔵 Trivial | 🏗️ Heavy lift

Extract the recovered PR-base resolution into a helper.

loadRecoveredConfig now mixes three concerns: trusted-config loading, the protected-branch guard, and a six-branch PR-base continuity resolution. The continuity part spans lines 239-334 and contains four separate fetch-and-resolve paths that differ only in the ref they target and whether they fetch.

Move lines 239-334 into a method such as resolveRecoveredPRBase(ctx, run, repo, workDir, cfg) error. loadRecoveredConfig then reads as: load config, guard the branch, resolve the base. The branch table becomes reviewable in isolation, and the existing tests keep covering it through loadRecoveredConfig.

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

In `@internal/daemon/manager.go` around lines 232 - 334, Extract the recovered
PR-base continuity logic from loadRecoveredConfig into a helper such as
resolveRecoveredPRBase(ctx, run, repo, workDir, cfg) error, preserving all
existing branches, fetch timeouts, ref handling, persistence, and error
propagation. Leave loadRecoveredConfig responsible for config loading and
configuredPRBaseBranchGuard, then invoke the helper and return its error before
returning cfg.

786-810: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Collapse the two upstream fetch helpers into one.

fetchRunUpstreamBranch and fetchRunUpstreamBranchToPrivateRef share the whole routing decision and differ only in the destination ref and the git call. A future change to the routing rule must be applied twice.

Consider one helper that takes the destination ref, with the tracking-ref path passing "refs/remotes/origin/"+branch.

♻️ Suggested consolidation
func fetchRunUpstreamBranchToRef(ctx context.Context, workDir string, repo *db.Repo, branch, localRef string, private bool) error {
	originURL, err := git.GetRemoteURL(ctx, workDir, "origin")
	remote := repo.UpstreamURL
	if !repo.URLsVerified || strings.TrimSpace(repo.UpstreamURL) == "" || (err == nil && gate.SameRemoteRepository(originURL, repo.UpstreamURL)) {
		remote = "origin"
	}
	if private {
		return git.FetchRemoteBranchToPrivateRef(ctx, workDir, remote, branch, localRef)
	}
	return git.FetchRemoteBranchToRef(ctx, workDir, remote, branch, localRef)
}

Note that internal/pipeline/steps/common_git.go lines 284-300 hold a second, structurally identical pair. Both layers now carry the same routing rule.

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

In `@internal/daemon/manager.go` around lines 786 - 810, Collapse
fetchRunUpstreamBranch and fetchRunUpstreamBranchToPrivateRef into one helper
that computes the remote once and accepts the destination ref plus the fetch
mode; use the tracking-ref path with "refs/remotes/origin/"+branch and preserve
the existing FetchRemoteBranchToRef versus FetchRemoteBranchToPrivateRef
behavior. Also consolidate the structurally identical helper pair in
common_git.go so the upstream routing rule is defined only once per layer.
internal/pipeline/steps/intent.go (1)

259-265: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Record when the configured base merge-base cannot be resolved.

For an explicit base, a failed mergeBaseWithTarget silently returns git.EmptyTreeSHA. The intent diff then covers the whole tree instead of the feature branch delta, and nothing reports why the match quality dropped. Intent is a hint, not a delivery gate, so failing closed is not required. Add a warning so this fallback is visible in logs.

♻️ Proposed change
 func resolveIntentBaseSHA(ctx context.Context, workDir, baseSHA, baseTarget string, explicitBase bool) string {
 	if explicitBase {
 		if mb := mergeBaseWithTarget(ctx, workDir, baseTarget); mb != "" {
 			return mb
 		}
+		slog.Warn("intent: could not resolve merge-base with configured pr.base_branch; diffing against the empty tree",
+			"base_target", baseTarget)
 		return git.EmptyTreeSHA
 	}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/pipeline/steps/intent.go` around lines 259 - 265, In the
resolveIntentBaseSHA function, when handling an explicit base and
mergeBaseWithTarget returns an empty string, add a warning log before returning
git.EmptyTreeSHA. The warning should indicate that the configured base
merge-base could not be resolved so this fallback behavior is visible in logs,
helping with debugging when intent diff coverage changes unexpectedly.
internal/pipeline/steps/ci_fix.go (1)

26-34: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Hoist the repeated explicit-base condition into one local.

Lines 29 and 32 repeat sctx.Config != nil && sctx.Config.PR.HasExplicitBaseBranch(). One local variable makes the fail-closed policy easier to read and keeps future gates consistent.

♻️ Proposed refactor
 	baseBranch := pipelineBaseBranch(sctx)
 	baseSHA := resolvePipelineBranchBaseSHA(ctx, sctx)
 	rebaseBaseSHA, baseResolved, baseErr := refreshRunBaseBranchTip(ctx, sctx, sctx.Run.BaseSHA, baseBranch)
-	if sctx.Config != nil && sctx.Config.PR.HasExplicitBaseBranch() && baseErr != nil {
+	explicitBase := sctx.Config != nil && sctx.Config.PR.HasExplicitBaseBranch()
+	if explicitBase && baseErr != nil {
 		return false, baseErr
 	}
-	if mergeConflict && sctx.Config != nil && sctx.Config.PR.HasExplicitBaseBranch() && !baseResolved {
+	if mergeConflict && explicitBase && !baseResolved {
 		return false, fmt.Errorf("resolve configured pr.base_branch %q for merge-conflict repair", baseBranch)
 	}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/pipeline/steps/ci_fix.go` around lines 26 - 34, Extract the repeated
condition sctx.Config != nil && sctx.Config.PR.HasExplicitBaseBranch() that
appears in both the line-29 and line-32 if statements into a single local
boolean variable. Replace both occurrences of this condition with the variable
reference to reduce duplication and improve readability.
internal/pipeline/steps/pr_base_test.go (1)

199-218: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Assert that the push case delivered nothing to the remote.

The pr case checks the provider transcript and proves pr create never ran. The push case checks only the returned error. If a regression pushes first and reports the snapshot error afterward, this test still passes. Capture the remote tip for the run branch before and after PushStep.Execute and assert it did not move.

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

In `@internal/pipeline/steps/pr_base_test.go` around lines 199 - 218, In the
"push" case of the switch statement, add validation that the remote branch tip
does not change after executing PushStep.Execute. Capture the remote tip for the
run branch before the PushStep.Execute call, then capture it again after
execution and assert the two values are identical to prove nothing was delivered
to the remote. Keep the existing error validation for the snapshot immutability
check unchanged.
internal/pipeline/steps/push_test.go (1)

194-196: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Assert the feature ref keeps its prior value instead of being absent.

setupConfiguredBaseRewrite(t, "") never pushes feature to upstream. The rev-parse therefore fails because the ref never existed, and it also fails if upstream becomes unreadable for any other reason. The assertion cannot distinguish "push was blocked" from "ref was never there".

TestCIStep_RevalidatesConfiguredBaseBeforeRepairPush in internal/pipeline/steps/ci_test.go uses the stronger form: it pushes feature first, then compares the remote SHA. Please match that shape here.

♻️ Proposed change
 	dir, upstream, baseSHA, headSHA, snapshotSHA := setupConfiguredBaseRewrite(t, "")
+	gitCmd(t, dir, "push", "origin", "feature:refs/heads/feature")
 	realGit, err := exec.LookPath("git")
-	if _, err := exec.Command(realGit, "--git-dir="+upstream, "rev-parse", "--verify", "refs/heads/feature").CombinedOutput(); err == nil {
-		t.Fatal("push mutated the feature branch after configured-base reset")
+	if got := gitCmd(t, upstream, "rev-parse", "refs/heads/feature"); got != headSHA {
+		t.Fatalf("push mutated the feature branch to %s after configured-base reset, want %s", got, headSHA)
 	}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/pipeline/steps/push_test.go` around lines 194 - 196, Update the test
around setupConfiguredBaseRewrite and the rev-parse assertion to first push the
feature branch to upstream and capture its original remote SHA. After the push
attempt, resolve the remote feature ref again and assert it still exists with
the same SHA, matching the stronger pattern used by
TestCIStep_RevalidatesConfiguredBaseBeforeRepairPush.
internal/pipeline/steps/ci_test.go (1)

93-104: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Fail the fixture on an unknown replacement value.

The switch handles "sibling" and "backward". Every other value, including a typo, leaves the configured base at snapshotSHA and performs no rewrite. A test that expects a rewritten base would then assert against an unmodified fixture and could pass for the wrong reason. Callers pass "" intentionally, so keep that case explicit.

♻️ Proposed guard
 	case "backward":
 		gitCmd(t, dir, "push", "--force", "origin", baseSHA+":refs/heads/quality-assurance")
+	case "":
+		// Leave the configured base at the pushed snapshot.
+	default:
+		t.Fatalf("unknown replacement fixture %q", replacement)
 	}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/pipeline/steps/ci_test.go` around lines 93 - 104, Update the
replacement setup switch in the CI fixture test to explicitly allow the
intentional empty replacement value, while adding a default branch that fails
the test for any unknown value. Preserve the existing "sibling" and "backward"
behavior and use the test's existing failure mechanism.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@internal/cli/axi_guidance_test.go`:
- Line 25: Add the exact phrase “PR base branch” to AGENTS.md, ensuring it
appears on the guidance surface checked by the synchronization test.

In `@internal/pipeline/executor_reconcile_test.go`:
- Around line 214-244: Use a separate fresh deadline for the retry loop around
exec.Respond in the approval validation test, rather than reusing the deadline
from the initial validation wait. Keep the existing retry behavior and timeout
error intact while ensuring the second loop receives its full timeout budget.

In `@internal/pipeline/steps/ci.go`:
- Around line 205-213: Update Execute’s base-branch error handling to return a
hard error only when explicitPRBaseInvariantFailed(baseBranchTipErr) is true.
Keep fetch, resolution, and resolveWindow deadline errors retryable so the next
poll can retry them, matching ReconcileApprovalGate behavior.

In `@internal/pipeline/steps/common_git.go`:
- Around line 219-227: Update mergeBaseWithTarget to stop using len(target) !=
40 as the raw-commit test; add and use isHexCommitSHA to classify only exactly
40-character hexadecimal values as commit SHAs, while preserving origin/<target>
resolution for 40-character branch names containing non-hex characters.

In `@internal/pipeline/steps/host.go`:
- Around line 40-49: Update the recovery fallback in the PRURL handling block to
replace host and repository as an atomic pair only when PRURL resolves to a
complete host and repository identity. Do not assign repo from prRepo when
either value alone is missing; validate that the resolved host and slug are both
present, then update both host and repo together, preserving the existing values
otherwise.

In `@internal/pipeline/steps/pr.go`:
- Around line 92-120: The PR step must preserve an existing pull request when
persisted run identity differs from the current configured base branch. At the
start of the flow containing `FindPR`, inspect `sctx.Run.PRURL` and
`sctx.Run.PRBaseBranch` before querying; when either provides a prior PR or the
persisted base differs from `baseBranch`, reuse the persisted PR or adjust the
lookup to that persisted base instead of allowing `CreatePR` to run for the same
head branch. Keep the current `FindPR` behavior for runs without persisted PR
continuity.

In `@internal/pipeline/steps/rebase.go`:
- Around line 539-545: Remove the unused baseBranch variable and its "main"
fallback from the diff-check setup, leaving resolvePipelineBranchBaseSHA(ctx,
sctx) as the next operation.
- Around line 95-108: Update the remoteBaseBranchAdvanced call in the force-push
approval check to pass baseTarget instead of baseBranch as the parameter. This
ensures the approval guard validates against the correct base target reference
(either RunPRBaseMonitorRef for explicit branches or ResolvedBaseSHA for
snapshots) instead of always checking origin/<baseBranch>, preventing silent
approval skips or inspection of unintended live refs.
- Around line 52-63: The explicit-base refresh path in the rebase step must not
use an unverified base tip. Update the refreshRunBaseBranchTip handling to
retain and validate its resolved boolean, returning an error when it is false
even if no error was returned; only assign the refreshed SHA to baseTarget after
validation. Also revise the mergeBaseWithTarget error in this block so it
describes baseTarget accurately rather than calling it a resolved commit.

In `@internal/pipeline/steps/steps_test.go`:
- Around line 122-129: Replace the bare `git push --force` command in the
resetSource branch with a safer `--force-with-lease` operation that validates
the expected SHA. Capture the current SHA of the branch being reset before the
push, then use `--force-with-lease=refs/heads/<branch>:<expectedSHA>` to anchor
the operation. Instead of calling exec.Command directly, route the command
through the context-aware shellenv helper. Finally, add a regression test case
that advances the destination ref before attempting reset, verifying that the
operation fails and the newer ref is preserved when lease validation prevents
the unsafe push.

In `@internal/scm/github/github.go`:
- Around line 191-247: Update FindPR to always request headRefName and
headRepositoryOwner, including when h.forkOwner is empty. Validate every
candidate’s head branch and source repository against the expected source before
allowing reuse, and make matchesHead enforce that repository identity rather
than accepting all candidates for same-repository searches. Preserve the
existing invalid-response errors and unique-match behavior.

In `@internal/scm/gitlab/gitlab.go`:
- Around line 206-223: Require candidate.IID > 0 in the merge-request validation
loop in internal/scm/gitlab/gitlab.go (lines 206-223), and compare the extracted
URL number against the IID unconditionally. In
internal/scm/gitlab/gitlab_test.go (lines 314-341), add a missing-IID fixture
with a numeric web_url and assert that parsing returns an error and nil PR.

---

Nitpick comments:
In `@AGENTS.md`:
- Around line 129-136: Condense the added AGENTS.md guidance to durable
policy-level rules, removing implementation details, symbol/path references, and
enumerated regression tests. Point readers to the authoritative
configuration/documentation source and the relevant verification command
instead, while preserving the essential trust, path-matching, and CI-timeout
policies.

In `@internal/daemon/manager_trust_test.go`:
- Around line 629-704: Add a test alongside
TestReplacementWaitsForCancelledRunPRContinuityEvidence for the cancelActiveRuns
timeout path, using an injectable short wait duration instead of the fixed
30-second delay. Make the first pipeline step ignore context cancellation so the
cancelled run remains active, then assert startRunWithIntentSource aborts the
replacement with stage cancel_active_runs and verifies the corresponding failure
telemetry; keep the test deterministic without waiting 30 seconds.

In `@internal/daemon/manager.go`:
- Around line 232-334: Extract the recovered PR-base continuity logic from
loadRecoveredConfig into a helper such as resolveRecoveredPRBase(ctx, run, repo,
workDir, cfg) error, preserving all existing branches, fetch timeouts, ref
handling, persistence, and error propagation. Leave loadRecoveredConfig
responsible for config loading and configuredPRBaseBranchGuard, then invoke the
helper and return its error before returning cfg.
- Around line 786-810: Collapse fetchRunUpstreamBranch and
fetchRunUpstreamBranchToPrivateRef into one helper that computes the remote once
and accepts the destination ref plus the fetch mode; use the tracking-ref path
with "refs/remotes/origin/"+branch and preserve the existing
FetchRemoteBranchToRef versus FetchRemoteBranchToPrivateRef behavior. Also
consolidate the structurally identical helper pair in common_git.go so the
upstream routing rule is defined only once per layer.

In `@internal/pipeline/steps/ci_fix.go`:
- Around line 26-34: Extract the repeated condition sctx.Config != nil &&
sctx.Config.PR.HasExplicitBaseBranch() that appears in both the line-29 and
line-32 if statements into a single local boolean variable. Replace both
occurrences of this condition with the variable reference to reduce duplication
and improve readability.

In `@internal/pipeline/steps/ci_test.go`:
- Around line 93-104: Update the replacement setup switch in the CI fixture test
to explicitly allow the intentional empty replacement value, while adding a
default branch that fails the test for any unknown value. Preserve the existing
"sibling" and "backward" behavior and use the test's existing failure mechanism.

In `@internal/pipeline/steps/intent.go`:
- Around line 259-265: In the resolveIntentBaseSHA function, when handling an
explicit base and mergeBaseWithTarget returns an empty string, add a warning log
before returning git.EmptyTreeSHA. The warning should indicate that the
configured base merge-base could not be resolved so this fallback behavior is
visible in logs, helping with debugging when intent diff coverage changes
unexpectedly.

In `@internal/pipeline/steps/pr_base_test.go`:
- Around line 199-218: In the "push" case of the switch statement, add
validation that the remote branch tip does not change after executing
PushStep.Execute. Capture the remote tip for the run branch before the
PushStep.Execute call, then capture it again after execution and assert the two
values are identical to prove nothing was delivered to the remote. Keep the
existing error validation for the snapshot immutability check unchanged.

In `@internal/pipeline/steps/push_test.go`:
- Around line 194-196: Update the test around setupConfiguredBaseRewrite and the
rev-parse assertion to first push the feature branch to upstream and capture its
original remote SHA. After the push attempt, resolve the remote feature ref
again and assert it still exists with the same SHA, matching the stronger
pattern used by TestCIStep_RevalidatesConfiguredBaseBeforeRepairPush.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

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

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: f5b99f2b-7b91-41fc-ae41-179fbdf6f301

📥 Commits

Reviewing files that changed from the base of the PR and between 6ed880d and 5d76657.

📒 Files selected for processing (76)
  • AGENTS.md
  • docs/src/content/docs/concepts/gate-model.md
  • docs/src/content/docs/concepts/pipeline.md
  • docs/src/content/docs/guides/agents.md
  • docs/src/content/docs/guides/configuration.md
  • docs/src/content/docs/guides/setup-wizard.md
  • docs/src/content/docs/guides/troubleshooting.md
  • docs/src/content/docs/guides/tui.md
  • docs/src/content/docs/reference/cli.md
  • docs/src/content/docs/reference/global-config.md
  • docs/src/content/docs/reference/pipeline-steps.md
  • docs/src/content/docs/reference/repo-config.md
  • docs/src/content/docs/start-here/quick-start.md
  • internal/bitbucket/client.go
  • internal/bitbucket/client_test.go
  • internal/bitbucket/host.go
  • internal/cli/attach.go
  • internal/cli/axi_drive.go
  • internal/cli/axi_guidance.go
  • internal/cli/axi_guidance_test.go
  • internal/cli/axi_test.go
  • internal/cli/wizard.go
  • internal/cli/wizard_test.go
  • internal/config/config.go
  • internal/config/config_pr_test.go
  • internal/daemon/daemon.go
  • internal/daemon/helpers_test.go
  • internal/daemon/manager.go
  • internal/daemon/manager_repo_refresh_test.go
  • internal/daemon/manager_test.go
  • internal/daemon/manager_trust_test.go
  • internal/daemon/startup_recovery_test.go
  • internal/daemon/subscribe_recover_test.go
  • internal/db/run.go
  • internal/db/run_test.go
  • internal/db/schema.go
  • internal/gate/refresh.go
  • internal/gate/refresh_test.go
  • internal/git/git.go
  • internal/pipeline/executor.go
  • internal/pipeline/executor_reconcile_test.go
  • internal/pipeline/pipeline.go
  • internal/pipeline/steps/ci.go
  • internal/pipeline/steps/ci_fix.go
  • internal/pipeline/steps/ci_merge_test.go
  • internal/pipeline/steps/ci_test.go
  • internal/pipeline/steps/common_git.go
  • internal/pipeline/steps/demo.go
  • internal/pipeline/steps/document.go
  • internal/pipeline/steps/helpers_test.go
  • internal/pipeline/steps/host.go
  • internal/pipeline/steps/intent.go
  • internal/pipeline/steps/intent_integration_test.go
  • internal/pipeline/steps/lint.go
  • internal/pipeline/steps/pr.go
  • internal/pipeline/steps/pr_base_test.go
  • internal/pipeline/steps/pr_test.go
  • internal/pipeline/steps/push.go
  • internal/pipeline/steps/push_test.go
  • internal/pipeline/steps/rebase.go
  • internal/pipeline/steps/rebase_local_default_test.go
  • internal/pipeline/steps/rebase_test.go
  • internal/pipeline/steps/review.go
  • internal/pipeline/steps/steps_test.go
  • internal/pipeline/steps/test.go
  • internal/pipeline/steps/upstream_test.go
  • internal/scm/azuredevops/azuredevops.go
  • internal/scm/azuredevops/azuredevops_test.go
  • internal/scm/github/github.go
  • internal/scm/github/github_test.go
  • internal/scm/gitlab/gitlab.go
  • internal/scm/gitlab/gitlab_test.go
  • internal/scm/host.go
  • internal/skill/skill.go
  • internal/wizard/wizard.go
  • skills/no-mistakes/SKILL.md

// auto-rebases and re-pushes such a PR, so the agent runs no command and never
// hand-rebases, and `no-mistakes rerun` is only the dead-monitor recovery.
var canonicalStaleMonitorPhrases = []string{
"PR base branch",

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

for file in AGENTS.md internal/skill/skill.go internal/cli/axi_guidance.go; do
  rg -F -q 'PR base branch' "$file" || {
    printf 'missing exact phrase in %s\n' "$file" >&2
    exit 1
  }
done

Repository: Thaloz/thz-no-mistakes

Length of output: 194


🏁 Script executed:

set -euo pipefail
printf '%s\n' '--- test ---'
sed -n '1,90p' internal/cli/axi_guidance_test.go
printf '%s\n' '--- exact phrase occurrences ---'
rg -n -F 'PR base branch' AGENTS.md internal/skill/skill.go internal/cli/axi_guidance.go || true
printf '%s\n' '--- related guidance terms in AGENTS.md ---'
rg -n -i 'pr[ -]?base|base branch|base_branch' AGENTS.md || true

Repository: Thaloz/thz-no-mistakes

Length of output: 8359


Add PR base branch to AGENTS.md.

The synchronization test requires this phrase on every guidance surface, and AGENTS.md is missing it.

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

In `@internal/cli/axi_guidance_test.go` at line 25, Add the exact phrase “PR base
branch” to AGENTS.md, ensuring it appears on the guidance surface checked by the
synchronization test.

Comment on lines +214 to +244
deadline := time.Now().Add(3 * time.Second)
for step.validationCalls.Load() != 1 && time.Now().Before(deadline) {
time.Sleep(5 * time.Millisecond)
}
if step.validationCalls.Load() != 1 {
t.Fatal("approval validation did not run")
}
select {
case err := <-done:
t.Fatalf("transient approval validation ended the run: %v", err)
case <-time.After(50 * time.Millisecond):
}
got, err := database.GetRun(run.ID)
if err != nil {
t.Fatal(err)
}
if got.Status != types.RunRunning || got.AwaitingAgentSince == nil {
t.Fatalf("transient validation changed parked run: status %s awaiting %v", got.Status, got.AwaitingAgentSince)
}

step.validationErr.Store(nil)
for {
err = exec.Respond(types.StepCI, types.ActionApprove, nil)
if err == nil {
break
}
if time.Now().After(deadline) {
t.Fatalf("parked gate was not restored after transient validation: %v", err)
}
time.Sleep(5 * time.Millisecond)
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Reuse of deadline can make the retry loop fail early.

Line 214 computes deadline before the first wait. Line 240 reuses the same deadline for the retry loop. If the first wait consumes the full budget, the retry loop gets no time and the test fails with a misleading message. Compute a fresh deadline for the second loop.

♻️ Proposed fix
 	step.validationErr.Store(nil)
+	retryDeadline := time.Now().Add(3 * time.Second)
 	for {
 		err = exec.Respond(types.StepCI, types.ActionApprove, nil)
 		if err == nil {
 			break
 		}
-		if time.Now().After(deadline) {
+		if time.Now().After(retryDeadline) {
 			t.Fatalf("parked gate was not restored after transient validation: %v", err)
 		}
 		time.Sleep(5 * time.Millisecond)
 	}
📝 Committable suggestion

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

Suggested change
deadline := time.Now().Add(3 * time.Second)
for step.validationCalls.Load() != 1 && time.Now().Before(deadline) {
time.Sleep(5 * time.Millisecond)
}
if step.validationCalls.Load() != 1 {
t.Fatal("approval validation did not run")
}
select {
case err := <-done:
t.Fatalf("transient approval validation ended the run: %v", err)
case <-time.After(50 * time.Millisecond):
}
got, err := database.GetRun(run.ID)
if err != nil {
t.Fatal(err)
}
if got.Status != types.RunRunning || got.AwaitingAgentSince == nil {
t.Fatalf("transient validation changed parked run: status %s awaiting %v", got.Status, got.AwaitingAgentSince)
}
step.validationErr.Store(nil)
for {
err = exec.Respond(types.StepCI, types.ActionApprove, nil)
if err == nil {
break
}
if time.Now().After(deadline) {
t.Fatalf("parked gate was not restored after transient validation: %v", err)
}
time.Sleep(5 * time.Millisecond)
}
deadline := time.Now().Add(3 * time.Second)
for step.validationCalls.Load() != 1 && time.Now().Before(deadline) {
time.Sleep(5 * time.Millisecond)
}
if step.validationCalls.Load() != 1 {
t.Fatal("approval validation did not run")
}
select {
case err := <-done:
t.Fatalf("transient approval validation ended the run: %v", err)
case <-time.After(50 * time.Millisecond):
}
got, err := database.GetRun(run.ID)
if err != nil {
t.Fatal(err)
}
if got.Status != types.RunRunning || got.AwaitingAgentSince == nil {
t.Fatalf("transient validation changed parked run: status %s awaiting %v", got.Status, got.AwaitingAgentSince)
}
step.validationErr.Store(nil)
retryDeadline := time.Now().Add(3 * time.Second)
for {
err = exec.Respond(types.StepCI, types.ActionApprove, nil)
if err == nil {
break
}
if time.Now().After(retryDeadline) {
t.Fatalf("parked gate was not restored after transient validation: %v", err)
}
time.Sleep(5 * time.Millisecond)
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/pipeline/executor_reconcile_test.go` around lines 214 - 244, Use a
separate fresh deadline for the retry loop around exec.Respond in the approval
validation test, rather than reusing the deadline from the initial validation
wait. Keep the existing retry behavior and timeout error intact while ensuring
the second loop receives its full timeout budget.

Comment on lines +205 to 213
var baseBranchTipErr error
if baseBranchTip == nil {
baseBranchTip = func(ctx context.Context) (string, bool) {
return resolveRunDefaultBranchTip(ctx, sctx, sctx.Run.BaseSHA, sctx.Repo.DefaultBranch)
tip, resolved, err := refreshRunBaseBranchTip(ctx, sctx, sctx.Run.BaseSHA, pipelineBaseBranch(sctx))
if sctx.Config != nil && sctx.Config.PR.HasExplicitBaseBranch() {
baseBranchTipErr = err
}
return tip, resolved
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Inspect refreshRunBaseBranchTip and the explicit-base error classifiers.
set -euo pipefail
rg -n 'func refreshRunBaseBranchTip|func validateExplicitPRBase|func explicitPRBaseInvariantFailed|func pipelineBaseTarget|func pipelineBaseBranch|immutable run snapshot|could not be fetched' internal/pipeline/steps -C 6

Repository: Thaloz/thz-no-mistakes

Length of output: 17144


🏁 Script executed:

#!/bin/bash
set -euo pipefail
printf '%s\n' '--- ci.go ---'
sed -n '160,325p' internal/pipeline/steps/ci.go
printf '%s\n' '--- related symbols ---'
rg -n 'baseBranchTipErr|resolveWindow|defaultBaseBranchTipResolveWindow|ReconcileApprovalGate|func \(.*CIStep.*Execute|for .*poll|ticker|time\\.New' internal/pipeline/steps/ci.go internal/pipeline/steps/ci_test.go -C 5
printf '%s\n' '--- test bodies ---'
sed -n '1,225p' internal/pipeline/steps/ci_test.go

Repository: Thaloz/thz-no-mistakes

Length of output: 25536


🏁 Script executed:

#!/bin/bash
set -euo pipefail
printf '%s\n' '--- ci.go ---'
sed -n '160,325p' internal/pipeline/steps/ci.go
printf '%s\n' '--- related symbols ---'
rg -n 'baseBranchTipErr|resolveWindow|defaultBaseBranchTipResolveWindow|ReconcileApprovalGate|func \(.*CIStep.*Execute|for .*poll|ticker|time\.New' internal/pipeline/steps/ci.go internal/pipeline/steps/ci_test.go -C 5
printf '%s\n' '--- test bodies ---'
sed -n '1,225p' internal/pipeline/steps/ci_test.go

Repository: Thaloz/thz-no-mistakes

Length of output: 25536


🏁 Script executed:

#!/bin/bash
set -euo pipefail
printf '%s\n' '--- ReconcileApprovalGate ---'
sed -n '56,136p' internal/pipeline/steps/ci.go
printf '%s\n' '--- refreshRunBaseBranchTip ---'
sed -n '91,170p' internal/pipeline/steps/common_git.go
printf '%s\n' '--- base-branch timeout and fetch helpers ---'
rg -n 'fetchRunUpstreamBranch(ToPrivateRef)?|defaultBaseBranchTipResolveWindow|context\\.DeadlineExceeded|explicitPRBaseInvariantFailed' internal/pipeline/steps -C 8

Repository: Thaloz/thz-no-mistakes

Length of output: 16638


🏁 Script executed:

#!/bin/bash
set -euo pipefail
printf '%s\n' '--- ReconcileApprovalGate ---'
sed -n '56,136p' internal/pipeline/steps/ci.go
printf '%s\n' '--- refreshRunBaseBranchTip ---'
sed -n '91,170p' internal/pipeline/steps/common_git.go
printf '%s\n' '--- base-branch timeout and fetch helpers ---'
rg -n 'fetchRunUpstreamBranch(ToPrivateRef)?|defaultBaseBranchTipResolveWindow|context\.DeadlineExceeded|explicitPRBaseInvariantFailed' internal/pipeline/steps -C 8

Repository: Thaloz/thz-no-mistakes

Length of output: 16638


Keep transient base-branch refresh errors retryable.

In Execute, return a hard error only when explicitPRBaseInvariantFailed(baseBranchTipErr) is true. Retry fetch, resolution, and resolveWindow deadline errors on the next poll, consistent with ReconcileApprovalGate.

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

In `@internal/pipeline/steps/ci.go` around lines 205 - 213, Update Execute’s
base-branch error handling to return a hard error only when
explicitPRBaseInvariantFailed(baseBranchTipErr) is true. Keep fetch, resolution,
and resolveWindow deadline errors retryable so the next poll can retry them,
matching ReconcileApprovalGate behavior.

Comment on lines +219 to +227
func mergeBaseWithTarget(ctx context.Context, workDir, target string) string {
target = strings.TrimSpace(target)
if target == "" {
return ""
}
for _, ref := range []string{"origin/" + defaultBranch, defaultBranch} {
refs := []string{target}
if !strings.HasPrefix(target, "refs/") && len(target) != 40 {
refs = []string{"origin/" + target, target}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Do not use length 40 as the test for a commit SHA.

Line 225 treats any target of exactly 40 characters as a raw commit and drops the origin/<target> candidate. A branch name may legally be 40 characters long. For such a base branch that exists only as a remote-tracking ref, merge-base HEAD <name> fails, mergeBaseWithTarget returns "", and the caller silently falls back to run.BaseSHA or the empty tree. The branch-delta scope then changes with no error reported.

Test for hex content, not length.

🐛 Proposed fix
 func mergeBaseWithTarget(ctx context.Context, workDir, target string) string {
 	target = strings.TrimSpace(target)
 	if target == "" {
 		return ""
 	}
 	refs := []string{target}
-	if !strings.HasPrefix(target, "refs/") && len(target) != 40 {
+	if !strings.HasPrefix(target, "refs/") && !isHexCommitSHA(target) {
 		refs = []string{"origin/" + target, target}
 	}

Add the helper alongside it:

func isHexCommitSHA(s string) bool {
	if len(s) != 40 {
		return false
	}
	for _, r := range s {
		if !((r >= '0' && r <= '9') || (r >= 'a' && r <= 'f') || (r >= 'A' && r <= 'F')) {
			return false
		}
	}
	return true
}
📝 Committable suggestion

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

Suggested change
func mergeBaseWithTarget(ctx context.Context, workDir, target string) string {
target = strings.TrimSpace(target)
if target == "" {
return ""
}
for _, ref := range []string{"origin/" + defaultBranch, defaultBranch} {
refs := []string{target}
if !strings.HasPrefix(target, "refs/") && len(target) != 40 {
refs = []string{"origin/" + target, target}
}
func mergeBaseWithTarget(ctx context.Context, workDir, target string) string {
target = strings.TrimSpace(target)
if target == "" {
return ""
}
refs := []string{target}
if !strings.HasPrefix(target, "refs/") && !isHexCommitSHA(target) {
refs = []string{"origin/" + target, target}
}
func isHexCommitSHA(s string) bool {
if len(s) != 40 {
return false
}
for _, r := range s {
if !((r >= '0' && r <= '9') || (r >= 'a' && r <= 'f') || (r >= 'A' && r <= 'F')) {
return false
}
}
return true
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/pipeline/steps/common_git.go` around lines 219 - 227, Update
mergeBaseWithTarget to stop using len(target) != 40 as the raw-commit test; add
and use isHexCommitSHA to classify only exactly 40-character hexadecimal values
as commit SHAs, while preserving origin/<target> resolution for 40-character
branch names containing non-hex characters.

Comment on lines +40 to +49
if (repo == "" || host == "") && sctx.Run.PRURL != nil {
prHost := scm.ResolveHost(sctx.Ctx, *sctx.Run.PRURL)
repo = github.HostPrefixedSlugForHost(*sctx.Run.PRURL, prHost)
prRepo := github.HostPrefixedSlugForHost(*sctx.Run.PRURL, prHost)
// A local bare-gate path can look like owner/repo to RepoSlug even
// though it has no forge host. In recovery, use the persisted PR
// identity for both fields rather than sending an unscoped auth
// check and a filesystem-derived --repo value.
if repo == "" || host == "" {
repo = prRepo
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Keep the fallback host and repository identity atomic.

At Line 47, repo is replaced when either value is missing, but host is replaced only when it is empty. If UpstreamURL resolves to a host without a repository slug and PRURL belongs to another GitHub host, the adapter checks authentication for one host and sends --repo for another. This breaks PR continuity recovery. Only replace both values when PRURL resolves to a complete host and repository pair.

Proposed fix
-			if repo == "" || host == "" {
-				repo = prRepo
-			}
-			if host == "" {
-				host = prHost
+			if prHost != "" && prRepo != "" {
+				host, repo = prHost, prRepo
 			}
📝 Committable suggestion

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

Suggested change
if (repo == "" || host == "") && sctx.Run.PRURL != nil {
prHost := scm.ResolveHost(sctx.Ctx, *sctx.Run.PRURL)
repo = github.HostPrefixedSlugForHost(*sctx.Run.PRURL, prHost)
prRepo := github.HostPrefixedSlugForHost(*sctx.Run.PRURL, prHost)
// A local bare-gate path can look like owner/repo to RepoSlug even
// though it has no forge host. In recovery, use the persisted PR
// identity for both fields rather than sending an unscoped auth
// check and a filesystem-derived --repo value.
if repo == "" || host == "" {
repo = prRepo
}
if (repo == "" || host == "") && sctx.Run.PRURL != nil {
prHost := scm.ResolveHost(sctx.Ctx, *sctx.Run.PRURL)
prRepo := github.HostPrefixedSlugForHost(*sctx.Run.PRURL, prHost)
// A local bare-gate path can look like owner/repo to RepoSlug even
// though it has no forge host. In recovery, use the persisted PR
// identity for both fields rather than sending an unscoped auth
// check and a filesystem-derived --repo value.
if prHost != "" && prRepo != "" {
host, repo = prHost, prRepo
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/pipeline/steps/host.go` around lines 40 - 49, Update the recovery
fallback in the PRURL handling block to replace host and repository as an atomic
pair only when PRURL resolves to a complete host and repository identity. Do not
assign repo from prRepo when either value alone is missing; validate that the
resolved host and slug are both present, then update both host and repo
together, preserving the existing values otherwise.

Comment on lines +95 to 108
if forcePush && branch == baseBranch && remoteBaseBranchAdvanced(ctx, sctx.WorkDir, baseBranch, sctx.Run.BaseSHA) {
findingsJSON, _ := json.Marshal(Findings{
Items: []Finding{{
Severity: "warning",
File: filepath.Join("internal", "pipeline", "steps", "rebase.go"),
Description: fmt.Sprintf("origin/%s advanced after the force push; manual review required before updating the default branch", defaultBranch),
Description: fmt.Sprintf("origin/%s advanced after the force push; manual review required before updating the PR base branch", baseBranch),
}},
Summary: fmt.Sprintf("remote %s advanced during force push", defaultBranch),
Summary: fmt.Sprintf("remote %s advanced during force push", baseBranch),
})
return &pipeline.StepOutcome{
NeedsApproval: true,
Findings: string(findingsJSON),
}, nil
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check whether the explicit-base path populates refs/remotes/origin/<base> or only a private ref.
set -euo pipefail
rg -n 'refreshRunBaseBranchTip|RunPRBaseMonitorRef|refs/remotes/origin/' -C 4 internal/pipeline/steps internal/git

Repository: Thaloz/thz-no-mistakes

Length of output: 15227


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- rebase.go ---'
cat -n internal/pipeline/steps/rebase.go | sed -n '1,330p'

printf '%s\n' '--- common_git.go ---'
cat -n internal/pipeline/steps/common_git.go | sed -n '110,330p'

printf '%s\n' '--- remoteBaseBranchAdvanced definitions and call sites ---'
rg -n 'remoteBaseBranchAdvanced|baseTarget|RunPRBaseMonitorRef' internal/pipeline/steps -C 6

printf '%s\n' '--- focused tests ---'
rg -n 'forcePush|NeedsApproval|base.*advanced|immutable run snapshot|private ref|ResolvedBaseSHA' internal/pipeline/steps/*test.go -C 5

Repository: Thaloz/thz-no-mistakes

Length of output: 50379


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
from pathlib import Path
import re

rebase = Path("internal/pipeline/steps/rebase.go").read_text()
common = Path("internal/pipeline/steps/common_git.go").read_text()

call = re.search(
    r'if forcePush && branch == baseBranch && '
    r'remoteBaseBranchAdvanced\(([^)]*)\)',
    rebase,
)
body = re.search(
    r'func remoteBaseBranchAdvanced\([^)]*\) bool \{(.*?)\n\}',
    rebase,
    re.S,
)

assert call, "force-push approval call not found"
assert body, "remoteBaseBranchAdvanced definition not found"

args = [x.strip() for x in call.group(1).split(",")]
function_body = body.group(1)

print("call arguments:", args)
print("uses baseTarget at call:", "baseTarget" in args)
print("hard-coded origin lookup:", '"origin/"+baseBranch' in function_body)
print("explicit-base refresh target:", "ref = git.RunPRBaseMonitorRef(sctx.Run.ID)" in common)
print("explicit-base fetch target:", "fetchRunUpstreamBranchToPrivateRef" in common)

assert "baseTarget" not in args
assert '"origin/"+baseBranch' in function_body
assert "ref = git.RunPRBaseMonitorRef(sctx.Run.ID)" in common
assert "fetchRunUpstreamBranchToPrivateRef" in common
PY

printf '%s\n' '--- pipelineBaseTarget definition ---'
rg -n 'func pipelineBaseTarget|ResolvedBaseSHA|RunPRBaseRef' internal/pipeline/steps -C 5

Repository: Thaloz/thz-no-mistakes

Length of output: 20605


Use baseTarget in the force-push approval check.

For an explicit pr.base_branch, the refresh writes to RunPRBaseMonitorRef, while a configured snapshot uses ResolvedBaseSHA; neither requires origin/<baseBranch>. Update remoteBaseBranchAdvanced to resolve baseTarget so the guard cannot silently skip approval or inspect a different live ref.

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

In `@internal/pipeline/steps/rebase.go` around lines 95 - 108, Update the
remoteBaseBranchAdvanced call in the force-push approval check to pass
baseTarget instead of baseBranch as the parameter. This ensures the approval
guard validates against the correct base target reference (either
RunPRBaseMonitorRef for explicit branches or ResolvedBaseSHA for snapshots)
instead of always checking origin/<baseBranch>, preventing silent approval skips
or inspection of unintended live refs.

Comment on lines +539 to +545
// Check if the branch has any diff against the intended PR base.
// If the diff is empty (e.g. branch was already merged), skip remaining steps.
defaultBranch := strings.TrimSpace(sctx.Repo.DefaultBranch)
if defaultBranch == "" {
defaultBranch = "main"
baseBranch := pipelineBaseBranch(sctx)
if baseBranch == "" {
baseBranch = "main"
}
baseSHA := resolveBranchBaseSHA(ctx, sctx.WorkDir, sctx.Run.BaseSHA, defaultBranch)
baseSHA := resolvePipelineBranchBaseSHA(ctx, sctx)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Remove the unused baseBranch variable.

resolvePipelineBranchBaseSHA(ctx, sctx) takes no branch argument, so baseBranch and its "main" fallback are never read. golangci-lint reports ineffectual assignment to baseBranch at Line 543, and make lint runs the linter, so this fails local verification.

🐛 Proposed fix
 	// Check if the branch has any diff against the intended PR base.
 	// If the diff is empty (e.g. branch was already merged), skip remaining steps.
-	baseBranch := pipelineBaseBranch(sctx)
-	if baseBranch == "" {
-		baseBranch = "main"
-	}
 	baseSHA := resolvePipelineBranchBaseSHA(ctx, sctx)
📝 Committable suggestion

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

Suggested change
// Check if the branch has any diff against the intended PR base.
// If the diff is empty (e.g. branch was already merged), skip remaining steps.
defaultBranch := strings.TrimSpace(sctx.Repo.DefaultBranch)
if defaultBranch == "" {
defaultBranch = "main"
baseBranch := pipelineBaseBranch(sctx)
if baseBranch == "" {
baseBranch = "main"
}
baseSHA := resolveBranchBaseSHA(ctx, sctx.WorkDir, sctx.Run.BaseSHA, defaultBranch)
baseSHA := resolvePipelineBranchBaseSHA(ctx, sctx)
// Check if the branch has any diff against the intended PR base.
// If the diff is empty (e.g. branch was already merged), skip remaining steps.
baseSHA := resolvePipelineBranchBaseSHA(ctx, sctx)
🧰 Tools
🪛 golangci-lint (2.12.2)

[error] 543-543: ineffectual assignment to baseBranch

(ineffassign)

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

In `@internal/pipeline/steps/rebase.go` around lines 539 - 545, Remove the unused
baseBranch variable and its "main" fallback from the diff-check setup, leaving
resolvePipelineBranchBaseSHA(ctx, sctx) as the next operation.

Source: Linters/SAST tools

Comment on lines +122 to +129
if resetSource := os.Getenv("FAKE_CLI_PR_LIST_RESET_SOURCE"); resetSource != "" {
resetBranch := os.Getenv("FAKE_CLI_PR_LIST_RESET_BRANCH")
cmd := exec.Command("git", "push", "--force", "origin", resetSource+":refs/heads/"+resetBranch)
if out, err := cmd.CombinedOutput(); err != nil {
fmt.Fprintf(os.Stderr, "reset PR base: %v: %s", err, out)
os.Exit(1)
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -eu
printf '%s\n' '--- target files ---'
git ls-files 'internal/pipeline/steps/steps_test.go' 'internal/pipeline/steps/*.go' 'internal/shellenv/*.go' 'internal/winproc/*.go' 'internal/safeurl/*.go'
printf '%s\n' '--- target file outline ---'
ast-grep outline internal/pipeline/steps/steps_test.go
printf '%s\n' '--- target sections ---'
sed -n '1,230p' internal/pipeline/steps/steps_test.go
printf '%s\n' '--- shellenv symbols ---'
rg -n 'func (ConfigureShellCommand|RunShellCommand|OutputShellCommand|CombinedOutputShellCommand|StartShellCommand|TerminateShellCommandGroup)|package shellenv' internal
printf '%s\n' '--- related test helpers and force pushes ---'
rg -n -C 4 -- '--force|FAKE_CLI_PR_LIST_(RESET_SOURCE|RESET_BRANCH)|git push' internal/pipeline/steps

Repository: Thaloz/thz-no-mistakes

Length of output: 29023


🏁 Script executed:

#!/bin/bash
set -eu
printf '%s\n' '--- repository status and relevant imports ---'
git status --short
sed -n '1,70p' internal/pipeline/steps/steps_test.go
printf '%s\n' '--- all shellenv package files ---'
fd -t f . | rg '(^|/)(shellenv|winproc)(/|$)|shellenv|winproc'
printf '%s\n' '--- helper definitions and call sites ---'
rg -n -C 6 'ConfigureShellCommand|CombinedOutputShellCommand|OutputShellCommand|RunShellCommand|StartShellCommand|TerminateShellCommandGroup' .

Repository: Thaloz/thz-no-mistakes

Length of output: 50378


🏁 Script executed:

#!/bin/bash
set -eu
printf '%s\n' '--- exact reset helpers ---'
sed -n '100,220p' internal/pipeline/steps/steps_test.go
printf '%s\n' '--- configured-base tests and fixture setup ---'
sed -n '1,290p' internal/pipeline/steps/pr_base_test.go
printf '%s\n' '--- fixture and fake CLI helpers ---'
rg -n -C 8 'func (fakeGH|fakeGit|setupConfiguredBaseRewrite|fakeGH|fakeGit)|FAKE_CLI_PR_LIST|FAKE_CLI_RESET_BASE|origin' internal/pipeline/steps/pr_base_test.go internal/pipeline/steps/ci_test.go internal/pipeline/steps/helpers_test.go
printf '%s\n' '--- relevant imports and git helpers ---'
sed -n '1,90p' internal/pipeline/steps/pr_base_test.go
sed -n '1,180p' internal/pipeline/steps/helpers_test.go

Repository: Thaloz/thz-no-mistakes

Length of output: 50378


🏁 Script executed:

#!/bin/bash
set -eu
printf '%s\n' '--- subprocess policy in test package ---'
rg -n 'exec\.Command(Context)?\(' internal/pipeline/steps --glob '*_test.go'
printf '%s\n' '--- git command helper implementation ---'
rg -n -C 10 '^func gitCmd|func .*Git.*Cmd|exec\.Command' internal/pipeline/steps/*_test.go
printf '%s\n' '--- shellenv API implementation ---'
cat -n internal/shellenv/shell_command.go | sed -n '1,75p'

Repository: Thaloz/thz-no-mistakes

Length of output: 40225


🏁 Script executed:

#!/bin/bash
set -eu
printf '%s\n' '--- fake CLI environment and process working directory ---'
sed -n '230,275p' internal/pipeline/steps/steps_test.go
sed -n '234,285p' internal/pipeline/steps/helpers_test.go
printf '%s\n' '--- provider and git execution call sites used by the two resets ---'
rg -n -C 12 'gh.*pr.*list|pr list|ls-remote|Run\(.*gh|CommandContext.*gh|CommandContext.*git|func .*Run' internal/pipeline/steps internal/scm internal/git
printf '%s\n' '--- configured-base reset test call graph ---'
rg -n -C 12 'FAKE_CLI_MODE.*git-reset-base|git-reset-base-on-ls-remote|RESET_BASE_SOURCE|RESET_BASE_BRANCH' internal/pipeline/steps

Repository: Thaloz/thz-no-mistakes

Length of output: 50379


🏁 Script executed:

#!/bin/bash
set -eu
python3 - <<'PY'
from pathlib import Path
p = Path("internal/pipeline/steps/steps_test.go")
lines = p.read_text().splitlines()
targets = [124, 197]
for n in targets:
    line = lines[n - 1]
    print(f"{p}:{n}: {line.strip()}")
    print("  bare_force:", '"--force"' in line and "--force-with-lease" not in line)
    print("  explicit_lease:", "--force-with-lease=" in line)
    print("  conditional_update_ref:", "update-ref" in line)
print("reset destinations:")
for n in [123, 192]:
    print(f"{p}:{n}: {lines[n - 1].strip()}")
print("expected-old-sha inputs:")
for i, line in enumerate(lines, 1):
    if 116 <= i <= 205 and ("RESET" in line or "expected" in line.lower()):
        print(f"{p}:{i}: {line.strip()}")
PY

Repository: Thaloz/thz-no-mistakes

Length of output: 1231


Anchor the test-only forced reset.

Both reset helpers use a bare git push --force against inherited origin, with the destination branch taken from environment input. Use one reset helper with an expected old SHA and an explicit --force-with-lease=refs/heads/<branch>:<expectedSHA>. Validate the temporary remote and route the command through the context-aware shellenv helper. Add a regression case that advances the destination ref before reset; reset must fail and preserve the newer ref.

🧰 Tools
🪛 ast-grep (0.45.0)

[error] 123-123: An argument passed to exec.Command/exec.CommandContext is built by concatenating a string literal with dynamic input. If that input is attacker-controlled (and especially when the command is a shell such as sh -c/bash -c), this enables OS command injection. Pass untrusted data as separate, fixed arguments instead of interpolating it into a command string, avoid invoking a shell, and validate/escape the input where a shell is unavoidable.
Context: exec.Command("git", "push", "--force", "origin", resetSource+":refs/heads/"+resetBranch)
Note: [CWE-78] Improper Neutralization of Special Elements used in an OS Command ('OS Command Injection').

(command-injection-exec-concat-arg-go)

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

In `@internal/pipeline/steps/steps_test.go` around lines 122 - 129, Replace the
bare `git push --force` command in the resetSource branch with a safer
`--force-with-lease` operation that validates the expected SHA. Capture the
current SHA of the branch being reset before the push, then use
`--force-with-lease=refs/heads/<branch>:<expectedSHA>` to anchor the operation.
Instead of calling exec.Command directly, route the command through the
context-aware shellenv helper. Finally, add a regression test case that advances
the destination ref before attempting reset, verifying that the operation fails
and the newer ref is preserved when lease validation prevents the unsafe push.

Source: Coding guidelines

Comment on lines +191 to +247
jsonFields := []string{"number", "url"}
if strings.TrimSpace(base) == "" {
jsonFields = append(jsonFields, "baseRefName")
}
if h.forkOwner != "" {
jsonFields = "number,url,headRefName,headRepositoryOwner"
jsonFields = append(jsonFields, "headRefName", "headRepositoryOwner")
}
args = append(args, "--state", "open", "--json", jsonFields)
args = append(args, "--state", "open", "--json", strings.Join(jsonFields, ","))
cmd := h.cmd(ctx, "gh", args...)
out, err := cmd.CombinedOutput()
if err != nil {
return nil, fmt.Errorf("gh pr list: %s: %w", strings.TrimSpace(string(out)), err)
}
if strings.TrimSpace(string(out)) == "" {
return nil, nil
}
var prs []struct {
Number int `json:"number"`
URL string `json:"url"`
BaseRefName string `json:"baseRefName"`
HeadRefName string `json:"headRefName"`
HeadRepositoryOwner *struct {
Login string `json:"login"`
} `json:"headRepositoryOwner"`
}
if err := json.Unmarshal(out, &prs); err != nil || len(prs) == 0 {
return nil, nil
if err := json.Unmarshal(out, &prs); err != nil {
return nil, fmt.Errorf("parse gh pr list response: %w", err)
}
if prs == nil {
return nil, errors.New("parse gh pr list response: expected a JSON array")
}
var matched *scm.PR
for _, candidate := range prs {
pr := &scm.PR{URL: strings.TrimSpace(candidate.URL), BaseBranch: strings.TrimSpace(candidate.BaseRefName)}
number, numberErr := scm.ExtractPRNumber(pr.URL)
if pr.URL == "" || numberErr != nil || candidate.Number <= 0 || number != fmt.Sprintf("%d", candidate.Number) {
return nil, errors.New("parse gh pr list response: invalid pull request")
}
pr.Number = number
if strings.TrimSpace(base) == "" && pr.BaseBranch == "" {
return nil, errors.New("parse gh pr list response: missing pull request base")
}
if pr.BaseBranch == "" {
pr.BaseBranch = strings.TrimSpace(base)
}
if h.forkOwner != "" && (strings.TrimSpace(candidate.HeadRefName) == "" || candidate.HeadRepositoryOwner == nil || strings.TrimSpace(candidate.HeadRepositoryOwner.Login) == "") {
return nil, errors.New("parse gh pr list response: invalid pull request head")
}
if !h.matchesHead(candidate.HeadRefName, candidate.HeadRepositoryOwner, branch) {
continue
}
pr := &scm.PR{URL: strings.TrimSpace(candidate.URL)}
if candidate.Number > 0 {
pr.Number = fmt.Sprintf("%d", candidate.Number)
} else if num, nerr := scm.ExtractPRNumber(pr.URL); nerr == nil {
pr.Number = num
}
if pr.URL == "" {
return nil, nil
if matched != nil {
return nil, errors.New("multiple open pull requests found for source branch")
}
return pr, nil
matched = pr
}
return nil, nil
return matched, nil

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

For the GitHub CLI version supported by this repository, verify whether gh pr list --head performs an exact head match and excludes same-name or prefix-matching fork heads. Use official GitHub CLI documentation, source, and release notes.

💡 Result:

The GitHub CLI command gh pr list --head <branch> does not perform a guaranteed "exact match" in the way that excludes all other branches; it relies on GitHub's server-side search/query mechanisms, which can exhibit fuzzy or prefix-matching behavior [1]. Based on GitHub CLI documentation and repository discussions: 1. Server-Side Matching: The --head flag in gh pr list passes the branch name to the underlying GitHub API (either the repository.pullRequests GraphQL field or the search API, depending on other flags used) [1][2]. These server-side mechanisms are known to perform prefix matching for head branch names in certain contexts, rather than strict exact matching [1]. Users have reported that --head foo may return PRs where the head branch starts with "foo" [1]. 2. Lack of Explicit Exact Matching: There is no native flag or configuration in gh pr list to force an "exact match" that strictly excludes branches with the same name prefix [1]. Because this matching happens server-side, gh itself cannot override the API's behavior to enforce an exact constraint [1]. 3. Workarounds: To reliably filter for an exact match, users are advised to perform client-side filtering [1]. You can use the --json flag to output relevant fields (such as headRefName) and pipe the result to jq to perform a precise string comparison [1]. For example: gh pr list --head --json headRefName --jq 'map(select(.headRefName == ""))' 4. Syntax Limitations: The --head flag does not support the owner:branch qualified ref syntax (e.g., gh pr list --head user:branch) for cross-repository fork branch filtering [3][2]. Documentation explicitly notes that this syntax is not supported [3][4]. In summary, gh pr list --head does not perform an exact match, and it does not inherently exclude same-name prefix matches; client-side filtering with jq is the recommended approach to achieve exact matching [1].

Citations:


🏁 Script executed:

#!/bin/bash
set -eu
printf '%s\n' '--- github.go symbols and surrounding code ---'
rg -n -A35 -B20 'func \(h \*Host\) FindPR|func \(h \*Host\) matchesHead|forkOwner|FindPR\(' internal/scm/github/github.go internal/scm/github/github_test.go
printf '%s\n' '--- PR reuse/update call sites ---'
rg -n -A25 -B15 'FindPR\(|UpdatePR\(' internal --glob '*.go'
printf '%s\n' '--- GitHub CLI version/config references ---'
rg -n -i 'gh version|github-cli|cli/cli|gh pr list' . --glob '!vendor/**' --glob '!node_modules/**'

Repository: Thaloz/thz-no-mistakes

Length of output: 50379


🏁 Script executed:

#!/bin/bash
set -eu
printf '%s\n' '--- all FindPR callers ---'
rg -n 'FindPR\(' internal --glob '*.go'
printf '%s\n' '--- BuildHost and GitHub host construction ---'
rg -n -A35 -B15 'func BuildHost|NewWithFork|github\.New|fork_url|ForkURL|Fork' internal/pipeline internal/steps internal/daemon internal/scm --glob '*.go'
printf '%s\n' '--- FindPR implementation and nearby tests ---'
sed -n '185,265p' internal/scm/github/github.go
sed -n '580,725p' internal/scm/github/github_test.go

Repository: Thaloz/thz-no-mistakes

Length of output: 50379


🏁 Script executed:

#!/bin/bash
set -eu
printf '%s\n' '--- PR step ---'
sed -n '1,180p' internal/pipeline/steps/pr.go
printf '%s\n' '--- relevant manager continuity section ---'
sed -n '1035,1115p' internal/daemon/manager.go
printf '%s\n' '--- host construction tests and fork routing comments ---'
rg -n -A18 -B8 'BuildHost|NewWithFork|ForkURL' internal/pipeline/steps/*_test.go internal/pipeline/steps/*.go

Repository: Thaloz/thz-no-mistakes

Length of output: 50378


IDOR (CWE-639): Authorization Bypass Through User-Controlled Key (IDOR)

Reachability: External

Reachability path
● Entry
  internal/scm/host.go:176
  Host: Available returns nil when the host is ready to use, or a descriptive
│
▼
● Sink
  internal/scm/github/github.go

Validate the PR source before reuse.

When h.forkOwner == "", FindPR requests no source identity and matchesHead accepts every candidate. Because gh pr list --head <branch> is not an exact source selector, a colliding fork PR can be returned. PRStep can then edit that PR and persist its URL. Request and validate headRefName and the source repository against the expected source before reuse.

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

In `@internal/scm/github/github.go` around lines 191 - 247, Update FindPR to
always request headRefName and headRepositoryOwner, including when h.forkOwner
is empty. Validate every candidate’s head branch and source repository against
the expected source before allowing reuse, and make matchesHead enforce that
repository identity rather than accepting all candidates for same-repository
searches. Preserve the existing invalid-response errors and unique-match
behavior.

Source: MCP tools

Comment on lines +206 to +223
for _, candidate := range mrs {
pr := candidate.toPR()
number, numberErr := scm.ExtractPRNumber(pr.URL)
if pr.URL == "" || numberErr != nil || (candidate.IID > 0 && number != fmt.Sprintf("%d", candidate.IID)) {
return nil, errors.New("parse glab mr list response: invalid merge request")
}
if strings.TrimSpace(base) == "" && pr.BaseBranch == "" {
return nil, errors.New("parse glab mr list response: missing merge request target branch")
}
if pr.BaseBranch == "" {
pr.BaseBranch = strings.TrimSpace(base)
}
if matched != nil {
return nil, errors.New("multiple open merge requests found for source branch")
}
matched = pr
}
return pr, nil
return matched, nil

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Reject merge requests without an IID.

A response with web_url ending in a number but no iid passes the current condition. toPR then returns a pull request with an empty Number, so the response is accepted without a valid merge-request identity.

  • internal/scm/gitlab/gitlab.go#L206-L223: Require candidate.IID > 0 and compare the URL number against the IID unconditionally.
  • internal/scm/gitlab/gitlab_test.go#L314-L341: Add a missing IID fixture with a valid numeric web_url and assert an error with a nil PR.
Proposed fix
-		if pr.URL == "" || numberErr != nil || (candidate.IID > 0 && number != fmt.Sprintf("%d", candidate.IID)) {
+		if candidate.IID <= 0 || pr.URL == "" || numberErr != nil || number != fmt.Sprintf("%d", candidate.IID) {
📝 Committable suggestion

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

Suggested change
for _, candidate := range mrs {
pr := candidate.toPR()
number, numberErr := scm.ExtractPRNumber(pr.URL)
if pr.URL == "" || numberErr != nil || (candidate.IID > 0 && number != fmt.Sprintf("%d", candidate.IID)) {
return nil, errors.New("parse glab mr list response: invalid merge request")
}
if strings.TrimSpace(base) == "" && pr.BaseBranch == "" {
return nil, errors.New("parse glab mr list response: missing merge request target branch")
}
if pr.BaseBranch == "" {
pr.BaseBranch = strings.TrimSpace(base)
}
if matched != nil {
return nil, errors.New("multiple open merge requests found for source branch")
}
matched = pr
}
return pr, nil
return matched, nil
for _, candidate := range mrs {
pr := candidate.toPR()
number, numberErr := scm.ExtractPRNumber(pr.URL)
if candidate.IID <= 0 || pr.URL == "" || numberErr != nil || number != fmt.Sprintf("%d", candidate.IID) {
return nil, errors.New("parse glab mr list response: invalid merge request")
}
if strings.TrimSpace(base) == "" && pr.BaseBranch == "" {
return nil, errors.New("parse glab mr list response: missing merge request target branch")
}
if pr.BaseBranch == "" {
pr.BaseBranch = strings.TrimSpace(base)
}
if matched != nil {
return nil, errors.New("multiple open merge requests found for source branch")
}
matched = pr
}
return matched, nil
📍 Affects 2 files
  • internal/scm/gitlab/gitlab.go#L206-L223 (this comment)
  • internal/scm/gitlab/gitlab_test.go#L314-L341
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/scm/gitlab/gitlab.go` around lines 206 - 223, Require candidate.IID
> 0 in the merge-request validation loop in internal/scm/gitlab/gitlab.go (lines
206-223), and compare the extracted URL number against the IID unconditionally.
In internal/scm/gitlab/gitlab_test.go (lines 314-341), add a missing-IID fixture
with a numeric web_url and assert that parsing returns an error and nil PR.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

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

Inline comments:
In `@internal/scm/gitlab/gitlab.go`:
- Around line 67-78: Update NewForRemote to reject the "http" scheme before
assigning h.host or h.repository, while continuing to accept HTTPS and leaving
HTTP unchanged rather than rewriting it. Adjust
TestCustomRemoteAuthorityScopesAllOperations to assert that HTTP remotes are
rejected before authenticated glab operations can use them.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

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

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 943b55e7-3e24-4cef-8052-5b2b528b1374

📥 Commits

Reviewing files that changed from the base of the PR and between 5d76657 and 34d56b6.

📒 Files selected for processing (14)
  • AGENTS.md
  • docs/src/content/docs/reference/pipeline-steps.md
  • docs/src/content/docs/reference/repo-config.md
  • internal/cli/attach.go
  • internal/cli/root_test.go
  • internal/cli/wizard.go
  • internal/pipeline/steps/ci.go
  • internal/pipeline/steps/ci_fix.go
  • internal/pipeline/steps/ci_test.go
  • internal/pipeline/steps/common_git.go
  • internal/pipeline/steps/host.go
  • internal/pipeline/steps/steps_test.go
  • internal/scm/gitlab/gitlab.go
  • internal/scm/gitlab/gitlab_test.go
🚧 Files skipped from review as they are similar to previous changes (7)
  • internal/pipeline/steps/host.go
  • internal/pipeline/steps/ci.go
  • internal/cli/wizard.go
  • internal/pipeline/steps/common_git.go
  • AGENTS.md
  • docs/src/content/docs/reference/pipeline-steps.md
  • docs/src/content/docs/reference/repo-config.md

Comment on lines +67 to +78
scheme := strings.ToLower(strings.TrimSpace(parsed.Scheme))
if scheme != "http" && scheme != "https" {
return h
}
authority := strings.TrimSpace(parsed.Host)
if authority == "" {
return h
}
h.host = authority
if h.projectPath != "" {
h.repository = scheme + "://" + authority + "/" + h.projectPath
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -eu
printf '%s\n' '--- internal/scm/gitlab/gitlab.go ---'
sed -n '1,125p' internal/scm/gitlab/gitlab.go
printf '%s\n' '--- relevant tests ---'
sed -n '390,435p' internal/scm/gitlab/gitlab_test.go
printf '%s\n' '--- command construction ---'
rg -n -C 3 'h\.repository|h\.host|repoArgs|glab api|glab auth|NewForRemote' internal/scm/gitlab/gitlab.go internal/scm/gitlab/gitlab_test.go
printf '%s\n' '--- glab references and version metadata ---'
rg -n -i 'glab|gitlab cli|GITLAB_TOKEN' --glob '!internal/scm/gitlab/gitlab.go' --glob '!internal/scm/gitlab/gitlab_test.go' . | head -120

Repository: Thaloz/thz-no-mistakes

Length of output: 40343


🏁 Script executed:

#!/bin/bash
set -eu
sed -n '1,125p' internal/scm/gitlab/gitlab.go
sed -n '390,435p' internal/scm/gitlab/gitlab_test.go
rg -n -C 3 'h\.repository|h\.host|repoArgs|glab api|glab auth|NewForRemote' internal/scm/gitlab/gitlab.go internal/scm/gitlab/gitlab_test.go

Repository: Thaloz/thz-no-mistakes

Length of output: 25348


🌐 Web query:

GitLab glab CLI HTTP API URL GITLAB_TOKEN does glab send credentials over http

💡 Result:

Yes, the glab CLI sends your credentials—including those provided via the GITLAB_TOKEN environment variable—over HTTP when communicating with the GitLab API [1][2]. When you use glab, it makes authenticated HTTP requests to the GitLab API [1]. If you set the GITLAB_TOKEN environment variable, glab uses this token as the authentication credential for these requests [3][2]. Specifically, the token is included in the HTTP request headers (typically as a Private-Token or Authorization header) to authenticate the session [2]. Important security considerations: 1. Transport Security: Because these credentials are sent over the network, it is critical that you connect to GitLab instances over HTTPS to ensure the communication is encrypted. 2. Debugging Exposure: If you enable HTTP debugging by setting the GLAB_DEBUG_HTTP environment variable to true, the CLI will print request and response information to your terminal [3][2]. While this is useful for troubleshooting, it may expose your authentication token in plain text in your terminal logs or console output [2]. To avoid storing tokens in environment variables (which can be logged or exposed to other processes), the recommended best practice is to use glab auth login [4][5]. This command securely stores your credentials in your operating system's native keyring (such as the macOS Keychain, Windows Credential Manager, or Linux Secret Service) [4].

Citations:


🏁 Script executed:

#!/bin/bash
set -eu
python3 - <<'PY'
from pathlib import Path

src = Path("internal/scm/gitlab/gitlab.go").read_text()
test = Path("internal/scm/gitlab/gitlab_test.go").read_text()

assert 'if scheme != "http" && scheme != "https"' in src
assert 'h.repository = scheme + "://" + authority + "/" + h.projectPath' in src
assert 'return []string{"--repo", h.repository}' in src
assert 'const repo = "http://gitlab.example:8443/group/project"' in test
assert '"glab mr list --source-branch feature --target-branch main --repo " + repo' in test
assert '"glab mr view 42 --repo " + repo' in test

print("HTTP is accepted, preserved in h.repository, and passed to authenticated glab porcelain commands via --repo.")
PY

Repository: Thaloz/thz-no-mistakes

Length of output: 269


Sensitive Data Exposure (CWE-319): Cleartext Transmission of Sensitive Information

Reachability: External · Exploitability: Moderate

Reachability path
● Entry
  internal/cli/root_test.go:21
  TestRootReattachesActiveRunWhenUpstreamIsUnavailable
│
▼
● Sink
  internal/scm/gitlab/gitlab.go

Reject HTTP remotes before invoking authenticated glab commands.

NewForRemote accepts http:// remotes and passes them to authenticated MR/CI commands through --repo. glab sends credentials over that connection, so an on-path attacker can read or modify requests. Reject HTTP before setting h.repository or h.host, and update TestCustomRemoteAuthorityScopesAllOperations to assert rejection. Do not rewrite HTTP to HTTPS.

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

In `@internal/scm/gitlab/gitlab.go` around lines 67 - 78, Update NewForRemote to
reject the "http" scheme before assigning h.host or h.repository, while
continuing to accept HTTPS and leaving HTTP unchanged rather than rewriting it.
Adjust TestCustomRemoteAuthorityScopesAllOperations to assert that HTTP remotes
are rejected before authenticated glab operations can use them.

@pablog12
pablog12 merged commit 5465963 into main Aug 6, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant