Run a squadron of AI coding agents in parallel — each one isolated, briefed, and tasked to ship.
A small toolkit that lets you run multiple AI coding agents at the same time — each one in its own Git worktree and Zellij tab, all coordinating through a shared task tracker (Jira, Notion, or GitHub Projects).
You stay in the cockpit. The agents fly the missions:
- PM grooms the backlog and creates tasks
- Planner reads the code and writes the spec into the tracker
- Worker picks up the spec, implements it on its own branch, runs tests, opens a PR
Pick which AI tool you fly with (Claude Code or Kiro) and which tracker you brief them through (Jira, Notion, or GitHub Projects). One combo, or all of them.
Inspired by How I run 4–8 parallel coding agents.
| Combo | AI Agent | Task Tracker | Folder |
|---|---|---|---|
| claude-jira | Claude Code | Jira (Atlassian MCP) | claude-jira/ |
| claude-notion | Claude Code | Notion (Notion MCP) | claude-notion/ |
| claude-gh | Claude Code | GitHub Projects (gh CLI) | claude-gh/ |
| claude-local | Claude Code | Local markdown files (Obsidian-style) | claude-local/ |
| kiro-notion | Kiro CLI | Notion (Notion MCP) | kiro-notion/ |
| kiro-gh | Kiro CLI | GitHub Projects (gh CLI) | kiro-gh/ |
| kiro-local | Kiro CLI | Local markdown files (Obsidian-style) | kiro-local/ |
All seven share the same shape — same roles, same task-work / task-done commands, same Zellij workflow. The only thing that changes is which AI flies the missions and where the briefings (or markdown task files) live.
You always need:
- Zellij (≥ 0.44) — the multiplexer that hosts each agent in its own tab
- gh CLI — opens pull requests
- Git
- Either Claude Code or Kiro CLI
- For Jira / Notion trackers, the matching MCP server; for GitHub Projects, just the
ghCLI authenticated (gh auth status) — see Per-combo setup
git clone https://github.com/martin-conur/agentic-workflow ~/agentic-workflow
cd ~/agentic-workflow
./install.sh./install.sh shows an interactive picker (uses fzf or gum if installed, falls back to a numbered menu). Or be explicit:
./install.sh claude-gh # install one combo
./install.sh all # install all sevenThe installer drops slash commands / agents into your AI tool's config and links task-work, task-done, and task-init into ~/.local/bin.
In any Git repo you want to use this with:
cd ~/my-project
task-init # interactive picker
# or skip the picker:
task-init claude-ghThis writes a workflow config (e.g. .claude/gh-workflow.md or .kiro/steering/notion-workflow.md) so your agents know which board to read from. After that, task-work and task-done auto-detect the right combo from that file — see How the dispatchers work.
task-init is safe to re-run any time you want to pull in new template scaffolding or restore deleted files. Two orthogonal axes of control:
Scope — which categories of files to touch:
| Flag | Files touched |
|---|---|
| (none) | workflow doc + slash commands / agents + tasks/ for local |
--workflow |
workflow doc only (+ CLAUDE.md import for Claude loadouts) |
--commands |
slash commands / agents only |
Overwrite policy — what to do when a target file already exists:
| Flag | Existing file behavior |
|---|---|
| (TTY) | per-file prompt: [k]eep / [o]verwrite / [d]iff (default = keep) |
| (non-TTY) | silently keep (exit 0) — script-safe |
--force |
overwrite everything in scope, no prompt |
--restore |
fill missing files only; never touch anything that exists |
--force and --restore are mutually exclusive.
When the workflow doc is re-rendered, previously-filled {OWNER} / {REPO} / {PROJECT} / {SITE} / {KEY} / {BOARD} values are carried forward automatically. Precedence: this-run flag → existing-file value → {PLACEHOLDER}. So task-init claude-gh --force after you've already filled in your IDs does the right thing (refreshes the template, keeps your values).
Common use cases:
task-init claude-gh --restore # restore a deleted slash command, leave everything else alone
task-init claude-gh --commands --force # refresh slash commands to current templates
task-init claude-gh --workflow # pull in template updates, keep current scope's other filesOpen Zellij, start your AI tool, and brief the PM:
/pm show me the backlog
/pm create task for "add login button"
/planner plan the new task
task-work <task-url> # spawns a fresh tab with a worker on its own branch
When the worker is done it opens a PR. Run task-done in the worker's tab to clean up the worktree and close the tab.
┌─ Zellij Tab 1 (main branch, always open) ──────────────────────┐
│ │
│ /pm → grooms backlog, creates tasks in the tracker │
│ /planner → reads code, writes spec into the task │
│ task-work → spawns a new tab with a worker on a fresh branch │
│ │
└────────────────────────────────────────────────────────────────┘
│
│ task-work creates a Git worktree + new Zellij tab
▼
┌─ Zellij Tabs 2…N (one per task, isolated worktrees) ───────────┐
│ │
│ /worker → reads spec, implements, tests, commits │
│ task-done → opens PR, removes worktree, closes the tab │
│ │
└────────────────────────────────────────────────────────────────┘
Each worker has its own checkout of the repo, so 4–8 of them can fly in parallel without colliding.
task-work, task-done, and task-init are project-aware dispatchers that live at the repo root. After install, they're symlinked into ~/.local/bin and work the same regardless of which combo was installed last.
When you run task-work or task-done inside a project, the dispatcher detects the impl by looking at which workflow doc is present:
| File present | Combo |
|---|---|
.claude/jira-workflow.md |
claude-jira |
.claude/notion-workflow.md |
claude-notion |
.claude/gh-workflow.md |
claude-gh |
.claude/local-workflow.md |
claude-local |
.kiro/steering/notion-workflow.md |
kiro-notion |
.kiro/steering/gh-workflow.md |
kiro-gh |
.kiro/steering/local-workflow.md |
kiro-local |
So you can have different combos in different projects and never have to think about it.
Overrides (in priority order):
--impl <name>flagAW_IMPL=<name>environment variable- Auto-detection from the workflow file
task-done is worktree-aware: when run from a task worktree (which has no workflow doc), it falls back to inspecting the main worktree so detection still works.
Pick the section that matches what you installed.
Need: Claude Code with the Atlassian Remote MCP added (verify with claude mcp list).
./install.sh claude-jira
cd ~/my-project
task-init claude-jira --site https://acme.atlassian.net --key PROJ --board "My Board"This writes .claude/jira-workflow.md and references it from CLAUDE.md so every session loads it. See Re-running task-init for the --force / --restore / --workflow / --commands flags.
| Role | How to invoke |
|---|---|
/pm |
typed in Claude Code |
/planner |
typed in Claude Code |
/worker |
auto-launched by task-work PROJ-123 |
Need: Claude Code with the Notion MCP added:
claude mcp add --transport http notion https://mcp.notion.com/mcp./install.sh claude-notion
cd ~/my-project
task-init claude-notion
# Edit .claude/notion-workflow.md and drop in your Notion database IDsSpawn workers with task-work <notion-url>. Same /pm, /planner, /worker roles as claude-jira.
Need: the gh CLI authenticated (gh auth status; gh auth login if not — needs repo + project scopes).
./install.sh claude-gh
cd ~/my-project
task-init claude-gh # auto-detects owner/repo from your git remotetask-init claude-gh seeds a read-only gh allow-list into .claude/settings.json so the PM / planner / worker can read freely:
gh issue view */gh issue list */gh issue comment *gh project view */gh project item-list */gh project field-list */gh project list *gh search issues *gh pr view */gh pr diff */gh pr list *gh label list */gh repo view */gh auth status
Mutations (gh issue edit, gh pr merge, gh project item-edit, …) are deliberately excluded and stay confirmation-gated.
Spawn workers with task-work <github-issue-url>. The issue number becomes the worktree slug (issue-42).
Optional add-on: if you frequently mutate Projects v2 iteration / number fields, add the GitHub MCP:
claude mcp add --transport stdio github -- npx -y @github/github-mcp-server(requires GITHUB_PERSONAL_ACCESS_TOKEN in your environment)
Need: nothing extra — no MCP, no remote tracker. Tasks live as files committed inside the repo.
./install.sh claude-local
cd ~/my-project
task-init claude-local # creates tasks/, .claude/local-workflow.md, and slash commandstask-init writes .claude/local-workflow.md and references it from CLAUDE.md, plus drops a tasks/ directory and task-board into your ~/.local/bin (alongside the shared dispatchers).
What "local tracking" means — there is no Jira, Notion, or GitHub board. The markdown files in tasks/ are the database, and tasks/_board.md is an auto-generated kanban view. Everything renders cleanly in Obsidian, so you can plan and read tasks from your editor of choice.
Layout under <repo>/tasks/:
- One
NNN-slug.mdper task (e.g.001-add-login-flow.md).NNNis the zero-padded id the PM allocates;slugis a kebab-case short title. - Each file opens with YAML frontmatter (
id,title,status,priority,tags,created,branch,pr) followed by## Problem,## Solution,## Files to Create/Modify,## Verification. tasks/_board.mdis the auto-generated board — never hand-edit. It has three columns: Todo / In Progress / Done.
Lifecycle — todo → in-progress → done. The worker bumps status in the task file's frontmatter on its first commit (in-progress) and again before opening the PR (done). The PM is the only role that creates new task files and allocates ids.
task-board — regenerates tasks/_board.md from tasks/*.md frontmatter, overlaying live worktree state. It runs automatically after task-work and task-done (and whenever the PM mutates a task), but you can also run it manually to refresh the view:
task-board # uses $(git rev-parse --show-toplevel)
task-board --repo ~/other # explicit repoLive state — .git/task-force/state.json is a gitignored, per-clone sidecar that tracks which worktrees are currently active. task-work writes a row; task-done removes it. Frontmatter is the durable, committed state; the sidecar is the live overlay. If a task appears in the sidecar, the board forces it into the In Progress column regardless of frontmatter.
Spawn workers with task-work tasks/NNN-slug.md. The slug (filename minus the NNN- prefix and .md suffix) becomes the worktree name — 001-add-login-flow.md → worktree add-login-flow on branch task/add-login-flow.
| Role | How to invoke |
|---|---|
/pm |
typed in Claude Code |
/planner |
typed in Claude Code |
/worker |
auto-launched by task-work tasks/NNN-slug.md |
Need: Kiro CLI with Notion MCP configured.
./install.sh kiro-notion
cd ~/my-project
task-init kiro-notion
# Edit .kiro/steering/notion-workflow.md with your Notion database IDsAgents are bound to Kiro shortcuts:
| Agent | Shortcut |
|---|---|
pm |
ctrl+shift+p |
planner |
ctrl+shift+l |
worker |
ctrl+shift+w |
Need: the gh CLI authenticated (gh auth status; gh auth login if not — needs repo + project scopes). The bundled Kiro agents shell out to gh via execute_bash.
./install.sh kiro-gh
cd ~/my-project
task-init kiro-ghSame Kiro shortcuts as kiro-notion.
Optional add-on: if you frequently mutate Projects v2 iteration / number fields, add the GitHub MCP to each agent's mcpServers block in .kiro/agents/*.json and set GITHUB_PERSONAL_ACCESS_TOKEN in your environment.
Need: nothing extra — no MCP, no remote tracker. Same model as claude-local, just driven by Kiro instead of Claude Code.
./install.sh kiro-local
cd ~/my-project
task-init kiro-local # creates tasks/, .kiro/steering/local-workflow.md, and agentsTasks live in <repo>/tasks/*.md with the same NNN-slug filenames, frontmatter schema (id, title, status, priority, tags, created, branch, pr), and four-section body (## Problem, ## Solution, ## Files to Create/Modify, ## Verification) as claude-local. tasks/_board.md is auto-generated by task-board.
Lifecycle — todo → in-progress → done, with the worker mutating frontmatter on first commit and again before the PR. The PM allocates ids and creates new task files. The board script triggers automatically from task-work / task-done / PM mutations, and can be run by hand:
task-boardLive state — .git/task-force/state.json is the gitignored, per-clone sidecar that overlays live worktree state on top of the committed frontmatter. Same model as claude-local.
Spawn workers with task-work tasks/NNN-slug.md. Same Kiro shortcuts as kiro-notion.
Once you've installed a Claude loadout, task-init auto-installs radio — a low-latency mailbox CLI under ~/.task-force/radio/ that lets the PM agent and worker agents ping each other directly. No human courier, seconds-level wake-up when the recipient tab is idle, queue-and-defer when busy. The kiro-* loadouts install the equivalent hooks under .kiro/hooks/ instead.
Radio is the canonical coordination channel between the planner, PM, and workers — every role transition in the workflow runs through it. The PM / planner / worker prompts shell out to radio send at every documented handoff point.
These are the five transitions that make up a full PR cycle. Each one is a single radio send call, baked into the corresponding agent's prompt:
| From | When | Command |
|---|---|---|
| Planner | spec written into the issue | radio send --to pm --intent spec-ready --issue <N> |
| Worker | PR opened | radio send --to pm --intent review-requested --pr <N> |
| Worker | new commits pushed after review | radio send --to pm --intent re-review-requested --pr <N> |
| PM | review requested changes | radio send --to <worker-role> --intent changes-requested --pr <N> |
| PM | PR merged | radio send --to <worker-role> --intent approved-and-merged --pr <N> |
PR review content still lives in gh pr comment / gh pr review (or the equivalent on Jira / Notion / local); radio only carries the routing ping.
Role names are addressable strings, not free-form: the PM is pm, and each worker is worker-<reponame>-<slug> (e.g. worker-task-force-issue-42). List live ones with ls ~/.task-force/radio/sessions/.
| Command | What it does |
|---|---|
radio send --to <role> --intent <kind> [--pr N] [--issue N] [--body TEXT] |
Send a message (e.g. --to pm --intent review-requested --pr 42); body can come from stdin |
radio check |
List unread messages addressed to this role |
radio read <id> |
Print one message AND mark it acknowledged (moves inbox/ → processed/) |
radio read --peek <id> |
Print without acknowledging — for inspection / debugging |
radio ack <id> |
Mark it acknowledged (idempotent — no-op if already processed by a prior read) |
radio register / radio unregister |
Add/remove this tab's session file (~/.task-force/radio/sessions/<role>.info) |
radio ready / radio busy |
Toggle this session's STATE field — drives the wake-up vs. queue decision on the sender side |
radio orphans |
List session files whose heartbeat is >1h stale |
radio send reads the recipient's session file. If STATE=idle, it resolves the recipient's tab/pane id via zellij action list-tabs --json / list-panes --json --tab and writes radio check\n straight into that pane with zellij action write-chars --pane-id — no focus switch, so the sender's tab stays put. On the recipient's next turn end, the Stop hook (radio ready && radio check) surfaces the new message. If STATE=busy, the message is queued silently — no failed wake attempt, no interrupting the recipient mid-turn.
task-init claude-* writes these into your project's .claude/settings.json automatically:
| Hook | Command | Why |
|---|---|---|
SessionStart |
radio register |
Claims the role's session file for this tab |
UserPromptSubmit |
radio busy |
Marks the session busy while a turn is running |
Stop |
radio ready && radio check |
Marks idle and surfaces any queued messages |
For the kiro loadouts the same logic lives in .kiro/hooks/ and runs off Kiro's equivalent triggers.
A queued message arriving at an idle worker won't kick it into motion on its own — the worker only sees the message on its next turn (a human keystroke or its own next prompt). This is deliberate: radio is notification + queue, not auto-action. If you want fully autonomous handoffs, dispatch the worker with task-work --auto and bake all the instructions into the issue body.
If a tab dies unexpectedly (or Claude resumes without re-firing SessionStart), the session file's LAST_HEARTBEAT will go stale. Run radio orphans to list any session older than an hour. Safe to rm ~/.task-force/radio/sessions/<role>.info or just leave it — the next legitimate radio register overwrites it.
Here's what an end-to-end PR cycle looks like once everything is wired up. Eight beats, each handed off via radio:
1. Spin up the PM. In any zellij tab:
task-pmRenames the current tab to pm, registers via the SessionStart hook, and starts the PM agent in-place.
2. PM grooms the backlog and dispatches a worker. From the PM tab:
/pm show me the backlog # or: gh issue list --state open
/pm let's do issue 42
PM picks one and spawns a worker:
task-work issue-42 https://github.com/<owner>/<repo>/issues/42 --autoThis creates a worktree, opens a new zellij tab, and launches the worker agent. --auto is the recommended default — it runs the worker under auto-approve since PM-filed specs should be self-contained.
3. Planner (optional). If the issue needs a design pass first, the PM dispatches a planner instead (task-work … --plan). The planner reads the code, writes the spec into the issue body, and ends with:
radio send --to pm --intent spec-ready --issue 42PM's tab gets focused; on the next turn the PM picks it up and dispatches a worker for the same issue.
4. Worker implements. In the worker tab the agent reads the spec, edits files, runs tests, and commits with the issue title as a prefix.
5. Worker opens the PR, bumps Status, pings PM. Once the implementation is in, the worker opens the PR:
gh pr create --base main --head task/issue-42 --fillThen bumps the project Status field to In Review (or keeps it at In Progress if the project has no In Review column — the worker reads the mapping from .claude/gh-workflow.md), and pings the PM:
radio send --to pm --intent review-requested --pr 42PM's zellij tab gets focused; on its next turn end the Stop hook's radio check surfaces the ping.
6. Worker idles. The worker stops here. It does not run task-done yet — cleanup waits for PM's explicit go-ahead in step 8.
7. PM reviews. Round-trip if needed. From the PM tab:
gh pr view 42
gh pr diff 42
gh pr review 42 --comment --body "…" # or: gh pr comment 42 --body "…"If requesting changes (worker roles are worker-<reponame>-<slug>; see ls ~/.task-force/radio/sessions/):
radio send --to worker-task-force-issue-42 --intent changes-requested --pr 42The worker tab is focused, picks up the comments, pushes fixes, and pings back:
radio send --to pm --intent re-review-requested --pr 42Loop until the PR is clean.
8. PM merges and signals cleanup. PM squash-merges, then radios the worker:
gh pr merge 42 --squash --delete-branch
radio send --to worker-task-force-issue-42 --intent approved-and-merged --pr 42On its next turn the worker sees the ping, sets the project Status field to Done, and runs task-done --remove-worktree itself — removing the worktree and closing its own zellij tab. Done.
To shift PR review off the PM's (Opus) tab and onto a cheaper Sonnet model, dispatch a one-shot reviewer worker per PR:
task-reviewer <pr-url-or-number> [<spec-identifier>]task-reviewer spawns a fresh zellij tab + git worktree on the PR's head ref, then runs the /reviewer slash command (or the kiro reviewer agent) inside it on Sonnet (ANTHROPIC_MODEL=claude-sonnet-4-6 by default — pre-set the env var to override). The PM's tab stays focused.
The reviewer:
- Reads the spec (passed as the second arg — issue number / URL for
claude-gh/kiro-gh; Jira key forclaude-jira; Notion page URL forclaude-notion; local task slug or path forclaude-local). Onclaude-gh/kiro-ghonly, the wrapper also auto-detects from the PR body's firstCloses #N/Fixes #N/Resolves #Nline (case-insensitive); non-gh loadouts require the spec identifier explicitly because PR bodies don't carry their tracker's linking convention. - Reads the PR diff + comments.
- Cross-checks the diff against the spec, then runs the
code-reviewskill on top (claude variants — kiro stays prompt-driven). - Posts one thorough PR comment with spec-compliance findings, code-review findings, and a verdict (
clean,clean-with-nits, orchanges-requested). - Radios PM back with
review-complete-cleanorreview-complete-with-findings.
PM still decides whether to merge or request changes — the reviewer never approves, merges, closes, or mutates Status. The tab stays open showing the analysis; clean up the worktree later with task-done --remove-worktree.
task-reviewer 42 # claude-gh / kiro-gh: PR by number, auto-detect issue
task-reviewer https://github.com/owner/repo/pull/42 # PR by URL
task-reviewer 42 38 # claude-gh / kiro-gh: PR + GitHub issue explicit
task-reviewer 42 PROJ-123 # claude-jira: PR + Jira key explicit (no auto-detect)
task-reviewer 42 https://notion.so/page-id # claude-notion: PR + Notion page URL explicit
task-reviewer 42 042-add-login # claude-local: PR + local task slug explicit
task-reviewer 42 --no-auto # opt out of auto-permission (interactive review)--auto is the default — the reviewer's authority list rules out merge / push / approve / close / Status, so auto-permission is safe for the review flow, and matches the "dispatch and walk away" intent of the command. Pass --no-auto to drop into the interactive permission-prompt mode. Kiro reviewers default to --trust-all-tools for the same reason; opt out with --no-trust-all.
Common to every combo:
-b, --base BRANCH— branch the PR will target (default: branch you're on whentask-workruns)-f, --from REF— git ref to fork the new worktree's branch from (default: current HEAD). Accepts any refgitaccepts (branches, tags, SHAs,origin/foo). Use this to stack a PR on an in-flight branch (--from task/issue-46 --base main) or to spike offorigin/mainwithout checking it out first.--no-launch— create the worktree and open the tab at that directory, but don't auto-start the agent (you pick the model/command yourself)--impl <name>— force a specific combo, bypassing auto-detection
kiro-* combos also accept -m/--model MODEL and -a/--trust-all.
Tests live in tests/ and use bats-core.
git submodule update --init --recursive # first time only
./run_tests.sh # run everything
./run_tests.sh task_done # run a single suiteTest suites (click to expand)
| Suite | Covers |
|---|---|
install.bats |
Root install.sh — direct args, fzf/gum TUI, numbered-menu fallback |
task_init_dispatcher.bats |
Root task-init — impl dispatch, passthrough flags, TUI selector |
task_work_dispatcher.bats |
Root bin/task-work — auto-detect impl, --impl/AW_IMPL overrides, passthrough |
task_done_dispatcher.bats |
Root bin/task-done — same detection + worktree-aware fallback |
jira_task_work.bats |
claude-jira/bin/task-work — input parsing, worktree, zellij launch |
jira_task_init.bats |
claude-jira/bin/task-init — placeholder substitution, CLAUDE.md, --force |
claude_notion_task_work.bats |
claude-notion/bin/task-work — URL/slug detection, worktree, zellij launch |
claude_notion_task_init.bats |
claude-notion/bin/task-init — template copy, CLAUDE.md, --force |
claude_gh_task_work.bats |
claude-gh/bin/task-work — GitHub URL → issue-N slug, launch |
claude_gh_task_init.bats |
claude-gh/bin/task-init — owner/repo/project substitution, remote auto-detect |
claude_local_task_work.bats |
claude-local/bin/task-work — tasks/NNN-slug.md → kebab slug (NNN- stripped), frontmatter bump, board regen |
claude_local_task_init.bats |
claude-local/bin/task-init — tasks/ scaffolding, .claude/local-workflow.md, slash commands |
kiro_task_work.bats |
kiro-notion/bin/task-work — URL/slug detection, model/trust-all flags |
kiro_notion_task_init.bats |
kiro-notion/bin/task-init — template copy, --force |
kiro_gh_task_work.bats |
kiro-gh/bin/task-work — same as claude-gh but launching kiro-cli |
kiro_gh_task_init.bats |
kiro-gh/bin/task-init — owner/repo/project substitution |
kiro_local_task_work.bats |
kiro-local/bin/task-work — same as claude-local but launching kiro-cli |
kiro_local_task_init.bats |
kiro-local/bin/task-init — tasks/ scaffolding, .kiro/steering/local-workflow.md, agents |
task_board.bats |
Shared task-board script — frontmatter parsing, sidecar overlay, _board.md regen |
task_done.bats |
task-done across combos — cleanup, PR, guards |
Infrastructure:
tests/helpers/common.bash—setup_repo,setup_stubs,setup_worktree,teardown_all,assert_stub_calledtests/helpers/stubs/— fakes forzellij,gh,kiro-cli,claude; every call lands in$STUB_CALLS_DIR/*.callstests/libs/— bats-core, bats-support, bats-assert as git submodules
- Create
<impl-name>/withinstall.sh,bin/task-work,bin/task-done,bin/task-init, and asteering/*.example.md. - Add
<impl-name>to the picker andcasestatement in the rootinstall.sh. - Add
<impl-name>to the picker andcasestatement in the roottask-init. - Add the new workflow doc filename to
lib/detect-impl.shso dispatchers can route to it. - Add a row to the loadout table and a per-combo section in this README.
- Write tests — one
.batsfile per script.
#!/usr/bin/env bats
bats_load_library bats-support
bats_load_library bats-assert
load helpers/common
setup() { setup_repo; setup_stubs; cd "$MAIN_REPO"; }
teardown() { teardown_all; }
@test "description" {
run "$MY_SCRIPT" arg
assert_success
assert_output --partial "expected text"
assert_stub_called zellij "some-command"
}Add script path variables to tests/helpers/common.bash. The runner picks up every tests/*.bats file automatically.
set -euo pipefailin every script- Quote every variable
- No package managers, no compiled code — pure shell
- 2-space indentation
main is protected via the GitHub API (not a YAML file), so the config lives off-repo. The current rules:
- Required status checks (all four must be green before merge):
ShellCheck,Bats tests (ubuntu-latest),Bats tests (macos-latest),Loadout drift check strict=true— PR branch must be up-to-date withmainenforce_admins=true— the maintainer can't bypass either- Force-pushes and branch deletion blocked
Re-apply (or restore after an accidental clear):
gh api -X PUT repos/martin-conur/task-force/branches/main/protection --input - <<'JSON'
{
"required_status_checks": {
"strict": true,
"contexts": [
"ShellCheck",
"Bats tests (ubuntu-latest)",
"Bats tests (macos-latest)",
"Loadout drift check"
]
},
"enforce_admins": true,
"required_pull_request_reviews": null,
"restrictions": null
}
JSONInspect: gh api repos/martin-conur/task-force/branches/main/protection | jq.
MIT © Martin Conur
