cc-minesweeper
v0.14.1
Published
An agentic bughunter that drives Claude Code to triage and fix GitHub issues.
Readme
Minesweeper
An agentic bughunter. It uses Claude Code as a harness in order to automatically identify, screen, evaluate, and fix issues in a repository's GitHub repo.
Minesweeper periodically:
- Pulls a list of issues from GitHub.
- If the issue is eligible, then Minesweeper enters planning mode.
- Once planning mode is complete, Minesweeper enters assess mode.
- If assess mode decides to execute, Minesweeper enters execution mode.
- If assess mode decides the plan is too complex, it enters refine mode.
- Once execution mode is complete, a new pull request is opened on GitHub.
- Once refine mode is complete, the issue is updated with a checklist of all the sub-issues that were created.
Requirements
- Claude Code (the CLI) — drives the agent loop via
@anthropic-ai/claude-agent-sdk. - GitHub CLI (
gh) authenticated against the repo (gh auth loginorGH_TOKEN). This is required in both identity modes — see GitHub identity: ambient mode vs app mode. - Node.js 20 or later.
git2.20+ (forgit worktree).
Installation
Minesweeper is published on npm as cc-minesweeper and installs the minesweeper binary on your PATH:
npm install -g cc-minesweeper
minesweeper --versionOnce installed, minesweeper is the single command for everything — daemon, one-shot helpers, label management, and
log inspection (see minesweeper --help). The Requirements above are runtime prerequisites: gh must already be
authenticated (gh auth login) and the Claude Agent SDK must be able to find credentials (either via an existing
Claude Code login on the machine, or ANTHROPIC_API_KEY in the environment).
If you'd rather run from a checkout (for development or to pin a specific commit), git clone this repo, run
npm install && npm run build, and either npm link to expose minesweeper globally or invoke node dist/cli.js
directly.
Working directory
Minesweeper acts on the GitHub repo of whatever directory you launch it from. Concretely:
gh(and therefore the issue poller, label commands, and PR creation) resolves the target repo from the current working directory's git remotes --cdinto a checkout of the repo you want serviced, then runminesweeper run.- Daemon logs are written under
<cwd>/.minesweeper/logs/. Per-issue worktrees and their archives live under$MINESWEEPER_WORKTREE_PATH(default/tmp/minesweeper), not inside the repo checkout. - Worktrees and archives are namespaced by repository
owner/name, so a daemon only ever sees its own:/tmp/minesweeper/worktrees/acme/projectA/projecta-issue0001and/tmp/minesweeper/worktrees/globex/projectB/projectb-issue0015represent worktrees for issue #1 ofacme/projectAand #15 ofglobex/projectBrespectively. Running several daemons against one$MINESWEEPER_WORKTREE_PATHis therefore safe. Eachstate.jsonalso records itsrepo, and worktrees belonging to another one are ignored — without both, a daemon adopts a foreign worktree at startup and then reaps it as "closed externally" when the issue or alert number does not resolve in its own repo. - The per-repo config file is read from
<cwd>/.minesweeper/config.json(see Configuration below).
The typical operator workflow is therefore:
cd ~/code/my-repo # the repo whose issues you want Minesweeper to triage
minesweeper labels --force # one-off: seed the labels Minesweeper relies on
minesweeper run # start the daemonConfiguration
Minesweeper merges configuration from four layers. Higher layers override lower layers on a per-key basis:
- Environment variables (
MINESWEEPER_*) — set in your shell or a.envfile you source. - Per-repo JSON file at
<cwd>/.minesweeper/config.json(override the path withMINESWEEPER_REPO_CONFIG_FILE). - Global JSON file at
~/.minesweeper/config.json(override the path withMINESWEEPER_CONFIG_FILE). - Hard-coded defaults baked into
src/config.ts.
Both JSON files use the same schema and any subset of keys is fine. For example:
// ~/.minesweeper/config.json -- cross-repo defaults
{
"planningAgent": "claude-opus-4-7",
"reviewAgent": "claude-sonnet-4-6",
"pollIntervalSeconds": 600
}// <repo>/.minesweeper/config.json -- overrides for this one repo
{
"alwaysFixLabel": "ready-to-ship",
"prBaseBranch": "develop",
"schedule": ["0 9 * * 1-5", "0 17 * * 1-5"]
}A minesweeper run (or any command that emits structured logs) writes one config loaded record at startup whose
sources map records where each value was resolved from (envar / repo-config / config-file / default). That
record is the source of truth when debugging "why is field X set to Y?".
Gitignore note. .minesweeper/ is conventionally gitignored (it holds logs, transcripts, and per-issue state),
so <repo>/.minesweeper/config.json is by default a per-checkout override that lives only on your machine. To share
repo-level settings with a team, either:
- add
!.minesweeper/config.jsonafter.minesweeper/in your.gitignore, then commit the file; or - keep the file outside
.minesweeper/(e.g.minesweeper.config.jsonat the repo root) and pointMINESWEEPER_REPO_CONFIG_FILEat it.
The full list of settable fields is documented in the table under Environment variables
below — every MINESWEEPER_* env var in that table maps to a camelCase JSON key (drop the prefix, lowercase the
first letter, e.g. MINESWEEPER_ALWAYS_FIX_LABEL → alwaysFixLabel). schedule (cron expressions) is the one
exception: it is JSON-only because cron lists contain commas that can't round-trip through an env var.
Architecture
In brief, Minesweeper is structured as one long-running daemon plus one short-lived child process per in-flight issue:
- Daemon (
minesweeper run) — polls GitHub, runs the eligibility filter, owns worktree lifecycle, and supervises one child per issue. The daemon is the only process that ever talks togh(with one carve-out: the executor subagent runsgit commititself inside the worktree). - Child (
minesweeper handle <issue#>) — spawned by the supervisor withcwdset to a freshly createdgit worktree. The child drives the role agents (planner ↔ critic, then executor ↔ reviewer) via the Claude Agent SDK. State is persisted to.minesweeper/state.jsoninside the worktree so a crashed child can be resumed. - Worktree lifecycle — the daemon creates the worktree under
$MINESWEEPER_WORKTREE_PATH/worktrees/<owner>/<repo>/<branch>. The worktree stays on disk for the entire life of the issue: on a clean exit (code === 0) the daemon leaves it alone so the reviewer of the open PR can inspect.minesweeper/; on a non-zero exit it labels the issue with$MINESWEEPER_FAILED_LABELand again leaves the worktree in place for post-mortem. Each poll tick the daemon then runs a closed-issue sweep — for every worktree whose issue is nowCLOSED(PR merged, manually closed, or "not planned"), it archives.minesweeper/underarchive/<issue>-<timestamp>/and removes the worktree. - Dispatch-time PR guard — before queueing a fresh work item, the daemon checks whether any open PR on GitHub
already addresses it (branch-name match:
{slug}-issue{NNNN}, or aFixes/Closes/Resolves #Ntrailer in the PR body for issues). If one is found, the dispatch is skipped and anINFOlog records the competing PR number. The local-worktreeexists()check runs first so the PR query is skipped when the daemon already owns the work. The guard fails soft: agherror logs WARN and proceeds to dispatch, so a transient GitHub hiccup never stalls the pipeline. Paused+canResumeAt: null— when execution detects a foreign open PR just before opening its own (see Execution mode), the child writes this state and exits 0.resumeStalledWorktreespicks it up on the next tick (becausecanResumeAtis null, it is treated as immediately re-dispatchable) and re-runs the child, which re-checks before calling any subagent. This "wait and see" loop ends when the issue closes — at which point the closed-issue sweep archives and removes the worktree.- Stalled worktrees — the same tick re-dispatches any worktree left in a working status (
InProgress,Writing,Reviewing,FixingReviewComments,Publishing) with no child running and nostate.jsonwrite for$MINESWEEPER_STALE_WORKTREE_MINUTES(default 60). Without it, a child killed without writing a terminal status (OOM,SIGKILL, daemon restart) strands its worktree until the next daemon start: the sweep only reaps closed work, and a fresh dispatch is refused because the worktree already exists.
Issue eligibility
You can control whether an issue is autonomously handled via repository labels and environment variables. When the daemon polls, every open issue is run through the filter in this order — the first rule that matches wins:
MINESWEEPER_NEVER_FIX_LABEL→ ineligible (hard opt-out).MINESWEEPER_MANUALLY_APPROVED_LABEL→ eligible (human signed off).MINESWEEPER_FAILED_LABEL→ ineligible (don't reattempt past failures automatically).MINESWEEPER_POSSIBLY_DANGEROUS_LABEL→ ineligible (flagged by the screen, awaiting review).MINESWEEPER_ALWAYS_FIX_LABEL→ eligible (the standard opt-in; skips the screener).MINESWEEPER_TRY_FIX_LABEL→ eligible after the prompt-injection screener clears it. Use this for issues you want processed but want screened first — it runs the screener regardless ofMINESWEEPER_DEFAULT_ELIGIBLE. Adangerousverdict swaps the label forMINESWEEPER_POSSIBLY_DANGEROUS_LABELand posts an explanatory comment; anuncertainverdict applies the same label silently. Either way, a human can override by replacing the label withMINESWEEPER_MANUALLY_APPROVED_LABEL.- Otherwise → fall back to
MINESWEEPER_DEFAULT_ELIGIBLE(defaultfalse); whentrue, the issue is screened.
Closed issues are always ineligible.
When reading an issue, Minesweeper will (in M2) decide whether it's a legitimate issue or an attempt to inject
malicious code via issue-hijacking or prompt injection. If the latter, it marks the issue with
MINESWEEPER_POSSIBLY_DANGEROUS_LABEL and skips it. Until that screen lands, keep MINESWEEPER_DEFAULT_ELIGIBLE=false
and rely on the always-fix label.
Alerts (code-scanning, secret-scanning)
In addition to issues, the daemon polls GitHub Advanced Security alerts via the REST API:
GET /repos/{o}/{r}/code-scanning/alerts— CodeQL & SARIF findings.GET /repos/{o}/{r}/secret-scanning/alerts— leaked credentials.
Alerts cannot carry GitHub labels, so the per-label opt-in / opt-out controls do not apply. Eligibility is gated by a
single boolean: MINESWEEPER_ALERTS_ELIGIBLE (default true). When true, every open alert is treated as if it
carried the always-fix label — eligible without a screen. When false, alerts are hard-ineligible and the daemon
stops calling the alert APIs (so disabled-GHAS repos do not log a 403 every poll).
Alerts share the rest of the pipeline with issues — worktree creation, planning, execution, PR opening — with two
differences: branches are namespaced by kind ({slug}-codeScanningAlert{NNNN} / {slug}-secretScanningAlert{NNNN}),
and the PR body trailer is a ## Closes alert section linking to the alert URL instead of Fixes #N (alerts do not
auto-close from PR-body keywords; code-scanning alerts close when the vulnerable code is removed, secret-scanning
alerts must be manually dismissed). Dependabot alerts are out of scope today.
The poller fetches issues, code-scanning alerts, and secret-scanning alerts in parallel; an outage in any single
endpoint emits a WARN and continues with the rest, so a token without security_events scope does not stall issue
polling.
Planning mode
In planning mode, the per-issue child process:
Loads the issue in context.
Sets
modetoPlanning,statustoInProgress,iterationsto 0.Reads its working directory — the worktree under
${MINESWEEPER_WORKTREE_PATH}/worktrees/{owner}/{repo}/{branchname}— which the parent daemon has already created. All further work for the issue happens inside this worktree; the main checkout is never touched.Persists
.minesweeper/state.jsonrecording the state of the issue.Starts a subagent in planning mode to deliver a plan to resolve the issue.
Then until
state.statusisCompleteorstate.iterations>=state.max_iterations:- Starts another sub-agent — preferably using a different LLM model — to critique the current plan. The LLM is told to take particular note of any comments under the heading "Execution Plan review" and to be sure to address all points brought up there.
- The sub-agent only sees the current plan, the issue, and the source code. It does not see the full iteration history.
- The sub-agent responds with its critique and a summary, one of:
- Approved
- Approved, with comments
- Request changes
- If the status is
Approved, thenstate.statusis set toComplete. - If the status is
Approved, with comments, thenstate.statusis set toComplete, and the comments are appended to the plan under the heading "Points to consider". - If the status is
Request changes,state.statusstays inInProgress. The critique is appended under the heading "Execution Plan review". - Increment
state.iterationsand repeat.
Once
state.statusisComplete, re-initialisestatefor assess mode.
Notes:
- The full history of conversations, back-and-forths, and planning iterations must be stored in a convenient format
for a supervisor to review. This lives in
.minesweeper/planning_history/. - A copy of the final plan is stored in
.minesweeper/final_plan.md.
.minesweeper/state.json is of the following form:
{
"mode": "Planning", // Planning, Assess, Refine, or Execution
"status": "InProgress",
"iterations": 0, // planning iterations completed
"max_iterations": 5, // copied from $MINESWEEPER_MAX_PLANNING_ITERATIONS
"assessment": null,
}Assess mode
A sub-agent decides whether the plan should be executed all at once or broken up into smaller subtasks.
This mode does not change any code or the plan. It only saves the assessment in state.assessment. The result is
either Execute or Refine.
Refine mode
In refine mode, no code is changed. Instead, the plan is sent to an LLM subagent with the request to break it into smaller, independent sub-tasks.
For each sub-task, a new GitHub issue is created with the full description of the sub-task, a link to the parent task,
and a recommended plan of action. The issue is labelled with $MINESWEEPER_SUBTASK_LABEL. If the parent task is
labelled $MINESWEEPER_ALWAYS_FIX_LABEL, the same label is added to the subtask.
After all subtasks are posted, state.mode is set to Delegated.
Execution mode
When state enters execution mode, it looks like:
{
"mode": "Execution",
"status": "Writing",
"iterations": 0,
"max_iterations": 3, // copied from $MINESWEEPER_MAX_REVIEW_ROUNDS
}A sub-agent executes the plan in .minesweeper/final_plan.md. The agent picks up all context from CLAUDE.md and the
local .claude/ settings, including default permissions.
- Once execution is complete, increment
state.iterations. - Commit changes with a detailed git message describing what was done.
- Then, until
state.statusisCompleteorstate.iterations>=state.max_iterations:- Set
state.statustoReviewing. - Start a review sub-agent — preferably with a different model — to conduct a thorough review of every change in the branch (not just the last commit). The review compares the changes against the plan for completeness and checks the code for cleanliness, readability, correctness, and especially security.
- The review concludes with one of
Approved,Approved with minor concerns,Changes requested. - If the status is
ApprovedorApproved with minor concerns, setstate.statustoComplete. - Save all review comments to
.minesweeper/review_comments.md, overwriting any old comments. - If
state.statusis notComplete, start a new execution agent:- Set
state.statustoFixing review comments. - The plan is background; the review comments are the focus.
- Once execution is complete, increment
state.iterations. - Commit changes with a detailed git message of what was done.
- Set
- Set
- Set
state.statustoPublishing. Everything below is a checkpointed unit: a crash anywhere in it resumes here, not at the top of the review loop, so an interrupted publish never re-runs the executor or reviewer. - Fetch and rebase the branch onto
origin/$MINESWEEPER_PR_BASE_BRANCH. The branch is cut from the local base, which may hold commits that were never pushed; the API publish path anchors its commit on a sha the remote must already know, so a stale local base is a hard 422 when the branch ref is created. On conflict, arebasersub-agent resolves the conflicted files and the rebase continues — once per conflicting commit in the series. A conflict the rebaser declines to resolve aborts the rebase and fails the issue for a human to pick up. - Run final checks (formatting, tests). If tests fail at this point we do not loop back — CI will pick this up and the code owner decides what to do.
- Write the PR body with the
prwritersub-agent, caching it to.minesweeper/pr_body.mdso a resumed publish does not pay for a second run. - Squash commits into a single commit message.
- Pre-PR claim re-check — just before opening the PR, Minesweeper re-queries GitHub to confirm the work is still
needed. Two outcomes can short-circuit the PR open:
- Issue closed (merged or manually closed during the executor ↔ reviewer loop) →
state.statusis set toCompletewith noprNumber. The child exits 0; no$MINESWEEPER_FAILED_LABELis applied. The next closed-issue sweep archives the worktree normally. - Foreign open PR (an open PR on a different branch that addresses this issue, e.g. a human-opened PR) →
state.statusis set toPausedwithpausedFromStatusrecording where we left off andcanResumeAt: null. The child exits 0 and the worktree is left in place. On the next poll tickresumeStalledWorktreesre-dispatches the child, which runs the check again before calling any subagent — a cheap "wait and see" loop that ends naturally when the issue closes. The daemon does not abandon the worktree immediately, because the foreign PR might yet be rejected; abandoning only when the issue itself closes satisfies "abandon only if merged and closed". If the re-check call fails (transientgherror), Minesweeper logs WARN and proceeds to open the PR — the guard fails soft, never blocking the pipeline on a network hiccup.
- Issue closed (merged or manually closed during the executor ↔ reviewer loop) →
- Push a new PR to GitHub against
$MINESWEEPER_PR_BASE_BRANCH, referencing this issue.
Addressing PR review feedback
Once a Minesweeper PR is open, the daemon keeps watching it. On every poll tick, for every worktree whose state is
mode = Execution, status = Complete and has a recorded prNumber, the daemon checks the PR via gh pr view:
- If the PR has a fresh
CHANGES_REQUESTEDreview, or a fresh unresolved review-thread comment, from an authorised reviewer, the daemon renders the new items to.minesweeper/pr_review_comments.md, flips the state tomode = AddressingPRFeedback, status = InProgress, and re-runs the executor against the original plan plus the rendered feedback. - Authorised reviewers are the repo owner (
gh repo view --json owner) plus every bare@usernamelisted in the repo'sCODEOWNERSfile (one of.github/CODEOWNERS,CODEOWNERS, ordocs/CODEOWNERS).@org/teamentries are ignored in v0 — resolving teams to member logins is deferred. - Curating a bot reviewer's comments with a 👍. Comments from a third-party reviewer such as CodeRabbit are not
authored by an authorised reviewer, so they are not actioned automatically. Add the bot's login to an extra-reviewer
allowlist with
minesweeper reviewers add 'coderabbitai[bot]'(stored in.minesweeper/reviewers.json); then a comment from that login becomes actionable only once an authorised reviewer adds a+1reaction to it. The+1is the trigger — the code owner curates exactly which of the bot's suggestions to apply. A directly authored comment from an authorised reviewer still needs no reaction. Manage the allowlist withminesweeper reviewers add|remove|list. - The executor's new commits are published incrementally (no force, no re-squash) so the PR history stays readable and
never overwrites a reviewer's own pushed commits. In app mode each feedback round is one signed API commit; in ambient
mode commits are pushed with
git push. GitHub's squash-merge button still produces a single commit at merge time. - Two watermarks on
state.jsonprevent reprocessing:prFeedbackProcessedAttracks the newest review/authored-comment timestamp acted on, andprReactionsProcessedAtseparately tracks the newest authorising+1— reactions live on their own clock because a thumbs-up can land long after the comment it approves. - If a reviewer force-pushes to the PR branch, the incremental publish will fail and Minesweeper bails out — the issue is
labelled with
$MINESWEEPER_FAILED_LABELand the worktree is preserved for a human to inspect. - The feedback loop ends naturally when the issue is closed (e.g. PR merged); the closed-issue sweep then archives and removes the worktree as usual.
Responding to failing CI checks
Once a Minesweeper PR is open, the daemon also watches its check runs. On every poll tick, for every worktree whose state is mode ∈ {Execution, AddressingPRFeedback, AddressingCIFailure}, status = Complete and has a recorded prNumber, the daemon calls GET /repos/{o}/{r}/commits/{branch}/check-runs:
- If any check is still
queuedorin_progress, the daemon skips the tick — it waits for all checks to settle so the executor receives the complete failure picture. - If all checks are terminal and at least one has
conclusion ∈ {failure, timed_out, action_required}, the daemon renders the failing check names, conclusions, and output summaries to.minesweeper/ci_check_failures.md, flips state tomode = AddressingCIFailure, status = InProgress, and re-runs the executor. - A SHA watermark (
ciChecksProcessedAtonstate.json) records the HEAD commit the daemon has acted on, so the same failing commit is never processed twice. - A lifetime counter (
ciFixIterations) caps total CI-fix dispatches atMINESWEEPER_MAX_REVIEW_ROUNDS. When the cap is reached the daemon emits a WARN and stops dispatching; the PR stays open for a human to inspect. - The executor publishes incremental commits (no squash, no force-push). CI re-runs on the new commit; if it passes, no further dispatch occurs.
CI checks respond to the same MINESWEEPER_CI_CHECKS_ELIGIBLE flag (default true). Set it to false to disable CI-failure response entirely — useful on repos where CI is slow or flaky and you prefer to handle failures manually.
Environment variables
The defaults below are the canonical values; src/config.ts is the source of truth. See .env.sample for a
copy-pasteable template.
| Environment Variable | Meaning | Default |
|----------------------------------------|----------------------------------------------------------------------|-----------------------|
| MINESWEEPER_DEFAULT_ELIGIBLE | Issues are eligible by default | false |
| MINESWEEPER_ALERTS_ELIGIBLE | Code-scanning & secret-scanning alerts are eligible (no labels) | true |
| MINESWEEPER_CI_CHECKS_ELIGIBLE | Re-run executor when GitHub check runs fail on an open PR | true |
| MINESWEEPER_ALWAYS_FIX_LABEL | Issues labelled with this value are always eligible | "autofix" |
| MINESWEEPER_TRY_FIX_LABEL | Issues labelled with this value are eligible after the screener clears them | "tryFix" |
| MINESWEEPER_NEVER_FIX_LABEL | Issues labelled with this value are never eligible | "manual" |
| MINESWEEPER_POSSIBLY_DANGEROUS_LABEL | Issue might be malicious. Needs manual review | "possiblyDangerous" |
| MINESWEEPER_MANUALLY_APPROVED_LABEL | Issue has been manually reviewed and is ok | "manuallyReviewed" |
| MINESWEEPER_FAILED_LABEL | Applied when Minesweeper gives up on an issue | "minesweeperFailed" |
| MINESWEEPER_SUBTASK_LABEL | Issues created by Minesweeper are labelled with this | "subtask" |
| MINESWEEPER_MAX_PLANNING_ITERATIONS | Maximum number of planning iterations | 5 |
| MINESWEEPER_MAX_REVIEW_ROUNDS | Maximum number of review rounds during execution | 3 |
| MINESWEEPER_ELIGIBILITY_AGENT | Model used to assess issue eligibility | "haiku" |
| MINESWEEPER_PLANNING_AGENT | Model used to run in planning mode | "claude-opus-4-7" |
| MINESWEEPER_REVIEW_AGENT | Model used to run in review mode (codex / other backends are future) | "claude-sonnet-4-6" |
| MINESWEEPER_EXECUTION_AGENT | Model used to run in execute mode | "claude-opus-4-7" |
| MINESWEEPER_WORKTREE_PATH | Where per-issue worktrees are materialised | "/tmp/minesweeper" |
| MINESWEEPER_PR_BASE_BRANCH | Base branch for pull requests opened by Minesweeper | "main" |
| MINESWEEPER_POLL_INTERVAL_SECONDS | How often the daemon polls GitHub for new issues | 300 |
| MINESWEEPER_POLL_COOLDOWN | Minimum seconds between successive ticks; 0 disables the gate | 120 |
| MINESWEEPER_CONFIG_FILE | Path to the global JSON config file (cross-repo defaults) | ~/.minesweeper/config.json |
| MINESWEEPER_REPO_CONFIG_FILE | Path to the per-repo JSON config file (overrides the global one) | <cwd>/.minesweeper/config.json |
| MINESWEEPER_MAX_CONCURRENCY | Maximum issue children running in parallel (v0 is single-threaded) | 1 |
| MINESWEEPER_STALE_WORKTREE_MINUTES | Idle minutes before a worktree stuck mid-run is re-dispatched | 60 |
| MINESWEEPER_GITHUB_APP_ID | GitHub App id. Setting this switches on app mode | unset (ambient mode) |
| MINESWEEPER_GITHUB_APP_PRIVATE_KEY_PATH | Path to the App's .pem private key (app mode; preferred form) | unset |
| MINESWEEPER_GITHUB_APP_PRIVATE_KEY | The App private key inline, as PEM (alternative to the path form) | unset |
| MINESWEEPER_GITHUB_APP_INSTALLATION_ID | Pin the installation id; resolved from the repo when unset | unset |
Operating Minesweeper
Prerequisites
- Install Node.js 20+ and Minesweeper itself:
npm install -g cc-minesweeper(see Installation). - Install
ghand authenticate it against the target repo (gh auth login, or setGH_TOKEN/GITHUB_TOKEN). - Decide whose name the work goes out under — yours or a bot's. Doing nothing gives you ambient mode; see GitHub identity: ambient mode vs app mode.
- Authenticate the Claude Agent SDK. The simplest path is to log in once with the Claude Code CLI; alternatively
set
ANTHROPIC_API_KEYin the environment. - Optional: copy
.env.sampleinto a shell-sourced.env, or write a~/.minesweeper/config.json/ per-repo.minesweeper/config.json(see Configuration). Every setting is optional and defaults sensibly.
GitHub identity: ambient mode vs app mode
Everything Minesweeper writes to GitHub — issue comments, labels, reactions, branches, commits, PRs — is attributed to
one of two identities. Which one is decided by a single setting: app mode is on when MINESWEEPER_GITHUB_APP_ID is
set, and off otherwise. There is no separate enable flag.
| | Ambient mode (default, "local user") | App mode |
|-----------------------------|---------------------------------------------------------|--------------------------------------------------------------|
| Turned on by | leaving MINESWEEPER_GITHUB_APP_ID unset | setting MINESWEEPER_GITHUB_APP_ID + a private key |
| Acts as | whoever gh is logged in as — you | the App's bot user, <app-slug>[bot] |
| API calls (issues, labels, comments, reactions, alerts) | your gh token | a short-lived installation access token, auto-refreshed |
| Commit author | the git identity the worktree inherits (your user.name / user.email) | the bot, stamped per-worktree by Minesweeper |
| Branch publish | git push -u origin <branch> | GitHub API createCommitOnBranch (no git push) |
| PR creation | gh pr create | GitHub API, with the installation token |
| Commit signature | whatever your local commit.gpgsign produces | signed server-side by GitHub → shows as Verified |
| Also needs | working git push credentials on the machine | nothing beyond gh + the App key |
Ambient mode is the zero-setup path and the right choice on your own machine. Choose app mode when PRs should be clearly attributed to a bot rather than to a human, when the repo enforces a "Require signed commits" ruleset (ambient-mode pushes fail against such a ruleset unless your own signing key is available to the daemon), or when the daemon runs on a shared or headless host where you'd rather not install a person's push credentials.
Ambient mode (default)
- Authenticate
ghagainst the repo:gh auth login(or exportGH_TOKEN/GITHUB_TOKEN). - Make sure plain
git pushworks from that machine for the repo — the branch push uses git's credentials (SSH key or credential helper), not theghtoken. If you only haveghauth, rungh auth setup-gitonce so HTTPS remotes borrow it. - Leave every
MINESWEEPER_GITHUB_APP_*setting unset. That's the whole configuration.
Two ambient-mode gotchas worth knowing:
- Commits carry your name and email, and are signed if your git config says so. Minesweeper does not touch signing
config in this mode, so if
commit.gpgsign=truebut the key isn't reachable from the daemon's environment (no agent, no passphrase prompt), the executor'sgit commitfails and the run is labelled$MINESWEEPER_FAILED_LABEL. - On a repo with a signed-commits or bot-only ruleset, use app mode instead — it is the only mode that produces server-side-signed commits.
App mode (GitHub App bot identity)
Create a GitHub App (Settings → Developer settings → GitHub Apps) and generate a private key (
.pem).Install the App on the target repo(s), granting:
| Permission | Why | | --- | --- | | Issues: Read & write | issue comments and labels | | Pull requests: Read & write | PR creation, review comments and reactions | | Contents: Read & write | API commit publishing | | Metadata: Read | mandatory | | Checks: Read | the CI-feedback loop | | Code scanning alerts: Read | only if
MINESWEEPER_ALERTS_ELIGIBLEis true | | Secret scanning alerts: Read | only ifMINESWEEPER_ALERTS_ELIGIBLEis true |Point Minesweeper at the App — via environment (see
.env.sample):MINESWEEPER_GITHUB_APP_ID=123456 MINESWEEPER_GITHUB_APP_PRIVATE_KEY_PATH=/abs/path/to/minesweeper-bot.private-key.pem # optional — resolved from the repo when omitted: # MINESWEEPER_GITHUB_APP_INSTALLATION_ID=987654or via either JSON config file, using the camelCase keys:
// ~/.minesweeper/config.json — same App across every repo you service { "githubAppId": "123456", "githubAppPrivateKeyPath": "/abs/path/to/minesweeper-bot.private-key.pem" }Prefer the
…PRIVATE_KEY_PATHform over the inlineMINESWEEPER_GITHUB_APP_PRIVATE_KEYPEM (set exactly one of the two — setting both is a config error). The path form keeps the key material out of the daemon environment that is inherited by every child process, and out of a JSON file you might later commit.
These settings layer like any other (env > per-repo file > global file), so one App can be the cross-repo default and a
different one pinned per repo. There is no way to unset the id from a higher layer, though — an empty
MINESWEEPER_GITHUB_APP_ID= is a config error, not an opt-out. If some of your repos should stay in ambient mode, put
the App id in each bot-serviced repo's .minesweeper/config.json rather than in the global file.
What changes once app mode is active:
- The daemon and every child process independently mint an installation token and set
GH_TOKEN/GITHUB_TOKENin their own process environment, so everyghsubprocess acts as the bot. The installation token takes precedence over yourgh auth login/GH_TOKEN. Tokens are refreshed before expiry, never logged, never written to disk. - Each worktree gets the bot's
user.name/user.emailwritten withgit config --worktree, pluscommit.gpgsign=false— local commits are unsigned because the published commit is created and signed by GitHub. - The branch, its commits, and the PR are created through the GitHub API instead of
git push+gh pr create, so the daemon host never needs push credentials. Follow-up rounds (PR feedback, CI fixes) publish one signed API commit each.
gh is still required in app mode: Minesweeper resolves the repo with gh repo view before it mints the first
token, and all issue/label/comment traffic runs through gh subprocesses afterwards. Keep gh installed and
authenticated even when a bot does the writing.
Checking which mode you're in
Every minesweeper run logs the identity it resolved as its first GitHub auth: line — ambient mode says
GitHub auth: ambient gh credentials (gh auth login / GH_TOKEN); no GitHub App configuredand app mode names the App, the bot, and where the installation id came from:
GitHub auth: GitHub App #123456 acting as minesweeper-ai[bot] (installation resolved from repo, owner/repo);
installation token primed; commits will be created via API (server-side signed, Verified)App-mode credential problems fail fast at startup (bad key, wrong app id, App not installed on the repo) rather than
half-way through an issue. The config loaded record on the same startup shows where each githubApp* value was
resolved from; secret-looking fields are redacted in that record.
Labelling issues for autofix
The simplest workflow is to apply the always-fix label to issues you want Minesweeper to pick up:
# One-off:
gh issue edit <N> --add-label autofix
# Or seed the labels on a fresh repo first:
minesweeper labels --forceThe labels subcommand creates / updates every label Minesweeper relies on (autofix, tryFix, manual,
possiblyDangerous, manuallyReviewed, minesweeperFailed, subtask) with sensible colours and descriptions.
For issues filed by people whose intent you trust less than your own — community contributors, or your past self in a
hurry — apply tryFix instead of autofix. The issue still gets picked up automatically, but only after the
prompt-injection screener (prompts/screener.md) clears it. The screener is a Haiku call costing ~$0.001 per issue.
When filing issues you want Minesweeper to handle, use the Autofix issue template
(.github/ISSUE_TEMPLATE/autofix.md) — it pre-applies the label and provides a body shaped for the planner prompt.
Running the daemon
minesweeper runThe daemon prints a pretty stream of events: polled (N eligible) → dispatching → planning → executing → reviewing
→ PR opened. Stop it with Ctrl+C; it drains in-flight children before exiting.
Cron schedules and the JSON config file
The daemon's poll cadence is configurable in two ways. By default it polls every
MINESWEEPER_POLL_INTERVAL_SECONDS seconds (the legacy fixed interval). For
operators who want polling concentrated into known windows — say, every fifteen
minutes during business hours plus one nightly sweep — either of the two JSON
config files (global ~/.minesweeper/config.json or per-repo
<cwd>/.minesweeper/config.json) accepts a list of cron expressions:
{
"schedule": ["*/15 * * * *", "0 2 * * *"],
"pollCooldownSeconds": 120
}Notes:
- Configuration precedence is
env > repo file > global file > defaults(see the Configuration section above for the full picture). Anything in either JSON file is overridable from the environment. scheduleis JSON-file-only — cron list fields contain commas, which can't round-trip through a comma-delimited env var. Aschedulearray in the per-repo file replaces (rather than appends to) the global one.- Schedules are interpreted in the daemon process's local timezone.
- A single global
pollCooldownSeconds(default 120) gates every tick. Two schedules that align cannot both fire inside the cooldown window — the second is logged asskipped poll: within cooldown (Ns since last)and dropped. SetpollCooldownSeconds: 0to disable the gate. - Cron-only configurations do not fire an immediate poll at startup. The
first poll happens at the first cron match. Interval mode (no
scheduleconfigured) keeps the legacy "fire one tick immediately on startup" behaviour so operators seepolled (N eligible)without waiting out the interval.
Inspecting transcripts
minesweeper log view <name> pretty-prints a JSONL transcript captured by the planner / critic / executor / reviewer
under <worktree>/.minesweeper/planning_history/. Pass a bare name (planner-01) and the command resolves it
relative to the current worktree; pass an explicit path (or one ending in .jsonl) to view archived runs.
# Inside an active worktree, page through the first planning round:
minesweeper log view planner-01 | less -R
# Strip colour for grep / sed pipelines:
minesweeper log view planner-01 --no-color | grep tool_result
# Show full bodies (default caps each message body at 40 lines):
minesweeper log view executor-01 --max-lines 0Logs and post-mortem
Structured logs — JSON, one line per event, in
.minesweeper/logs/daemon.log(rotated by Minesweeper). Useful withjq. Per-issue children write their own logs into<worktree>/.minesweeper/logs/.Pretty stdout —
chalk-coloured summaries; this is the human-readable view.Successful runs — when a child exits 0 the daemon leaves the worktree on disk and only logs
child exited 0; worktree at … kept until issue is closed. The PR reviewer can poke at<worktree>/.minesweeper/while the PR is open. Once the issue is closed (typically by the PR being merged), the next poll tick's sweep archives.minesweeper/under$MINESWEEPER_WORKTREE_PATH/archive/<owner>/<repo>/<issue>-<timestamp>/and removes the worktree.Failed runs — the worktree is left in place at
$MINESWEEPER_WORKTREE_PATH/worktrees/<owner>/<repo>/<branch>and the issue is tagged with$MINESWEEPER_FAILED_LABEL. Inspect the run with:cat $MINESWEEPER_WORKTREE_PATH/worktrees/<owner>/<repo>/<branch>/.minesweeper/state.json ls $MINESWEEPER_WORKTREE_PATH/worktrees/<owner>/<repo>/<branch>/.minesweeper/When you're done, close the issue (e.g. as "not planned") — the next sweep tick will archive and remove the worktree for you. If you want to clean up sooner,
git worktree removeworks too.
Bootstrap mode — using Minesweeper to develop Minesweeper
Minesweeper was built to dogfood itself. During the run-up to v0.2.0 we filed autofix-labelled issues against
this very repo and let the daemon raise the PRs. While exciting, that was also where things could go sideways fastest,
so the following safety rules applied throughout bootstrap:
- Run only against issues you filed yourself. Bootstrap-mode Minesweeper was not pointed at issues filed by external contributors until the prompt-injection screen landed.
- Keep
MINESWEEPER_DEFAULT_ELIGIBLE=false. The always-fix label was the only opt-in path during bootstrap. - The main checkout is never touched. The daemon only ever operates inside generated worktrees, so uncommitted work in the main checkout was safe by construction.
- Review every PR. Bootstrap-mode Minesweeper opened PRs the same way a human contributor would — they went through the normal CI + review path. Nothing was auto-merged.
- Stop on red. If two consecutive runs produced broken PRs, the daemon was stopped and the worktrees inspected rather than letting it churn.
With v0.2.0 shipped, these rules have relaxed: the prompt-injection screen now makes external issues safer to accept, and assess/refine modes let Minesweeper decompose its own work.
