Notes for people working on the tools in this repo. If you just want to use
flight in your own repo, that's flight/GUIDE.md —
nothing here (bumping versions, adapters, the test rigs) is something a plugin
user ever does.
Rule of thumb for where a doc belongs: "would someone who only installed the
plugin ever do this?" If no, it goes here; if yes, it goes in GUIDE.md.
Everything a user needs — curl, jq, git, and python3 (standard library only) if
the cost ledger is on — is listed under
What you need in the guide. Working on the repo adds
two more:
shellcheckandyamllinton yourPATH. The pre-push checkscripts/checks/lint.shruns each of them; a linter that isn't installed can't lint, so without both your push is unchecked locally and the CIlintworkflow is the first thing to see the problem. Install them from your distro's package manager or Homebrew; on Windows, winget, scoop and choco all carry shellcheck, and yamllint comes frompip install yamllint.python3regardless of whether you enable the ledger — the prompt-logger scripts underflight/scripts/prompt-logger/are Python, and their unit tests are part ofscripts/run-tests.sh.
Platform-wise, contributing works anywhere the plugin does: Linux, macOS (stock bash 3.2 and BSD tools; nothing to install) and Windows via Git Bash / MSYS. All three run in CI on every change, so a contribution that is green only on Linux is not finished — see Branch / PR conventions.
This is a monorepo of Claude Code + Codex tools. Each tool owns its own directory, its own docs/changelog and a manifest for each harness; shared tooling lives at the root.
.
├── flight/ # the flight plugin
│ ├── .claude-plugin/plugin.json # Claude Code manifest (version source of truth)
│ ├── .codex-plugin/plugin.json # Codex manifest (bump-version.sh keeps it in lockstep)
│ ├── GUIDE.md # user-facing guide
│ ├── README.md
│ ├── CHANGELOG.md # this plugin's changelog
│ ├── bin/ # entrypoints: flight, batch-manifest, deprecated lightspeed shim
│ ├── skills/ # the skills (filing-issues, working-an-issue, …)
│ ├── scripts/
│ │ ├── flight # the dispatcher (single entrypoint)
│ │ └── adapters/<backend>/ # per-backend adapters (forgejo, github, gitlab, jira)
│ └── references/ # adapter-contract.md, flight-setup.md, default-labels.md
├── .claude-plugin/marketplace.json # published marketplace manifest (all plugins, both harnesses)
├── scripts/ # repo-wide tooling (checks, tests, release)
├── test-rig/ # per-backend integration rigs
├── docs/ # repo docs + docs/adr/ (architecture decision records)
├── AGENTS.md # repo-wide agent instructions (CLAUDE.md imports it)
├── CHANGELOG.md # root index → each tool's changelog
├── CONTRIBUTING.md # this file
└── README.md
A second tool would slot in as a sibling of flight/ (its own dir with both a
.claude-plugin/plugin.json and a .codex-plugin/plugin.json), gain an entry in
.claude-plugin/marketplace.json (which Codex reads too), its own CHANGELOG.md, and a
line in the root CHANGELOG.md index.
This repo dogfoods flight on itself via a two-marketplace split (a published git-source marketplace and a local directory-source one symlinked to the live tree). The full setup, the refresh cycle after you change the plugin, and the gotchas are in docs/plugin-marketplace-dogfooding.md. Read that before your first change — the CLI has no version pinning, so knowing how the cache refreshes matters.
There are three distinct layers — keep them straight:
| What | Where | Runs | Purpose |
|---|---|---|---|
| Pre-push checks | scripts/checks/*.sh via scripts/run-checks.sh |
.githooks/pre-push (local), on demand |
Working-tree cleanliness: lint.sh (yamllint + shellcheck), verify-git-logs.sh (commit signatures) |
| Script unit tests | scripts/tests/*.test.sh via scripts/run-tests.sh |
CI (.github/workflows/tests.yml), on demand |
Unit tests for the repo's own scripts (e.g. bump-version.test.sh) |
| Integration rigs | test-rig/<backend>/ |
on demand | Per-backend adapter smoke tests (see below) |
scripts/run-checks.sh # lint + signature checks (what the pre-push hook runs)
scripts/run-tests.sh # all scripts/tests/*.test.sh, in parallelUnit tests run in parallel, since each test file builds its own mktemp -d sandbox.
Each file's output is buffered and flushed whole when it finishes, so blocks stay
contiguous, but they arrive in completion order, not alphabetical. Two knobs:
TEST_JOBS=1 scripts/run-tests.sh # serial; reach for this first when a failure
# looks like it depends on what else was running
TEST_JOBS=4 scripts/run-tests.sh # a specific width (default: cores, capped at 8)ci-watch.test.sh is split into one job per backend (CI_WATCH_BACKENDS), because
its timeout cases wait on a real clock and it would otherwise set the floor for the
whole run. Running that file directly still covers all three backends.
lint.sh can only run the linters you have: install yamllint and shellcheck first
(see Prerequisites), or the local lint pass covers less than CI's does.
Enable the pre-push hook once per clone:
git config core.hooksPath .githooksThe hook reads the ref list git passes on stdin and verifies signatures on exactly
the commits each ref will push (<remote>..<local>, or everything not yet on the
remote for a new branch), then runs the remaining checks with the signature pass
skipped (RUNCHECKS_SKIP). A push that carries no commits (a git push --delete <branch>, or nothing to push) runs nothing at all. verify-git-logs.sh can also be
run by hand with a count or any git rev-list selection, e.g.
scripts/checks/verify-git-logs.sh origin/develop..HEAD.
CI runs two workflows on push to develop and on PRs: lint (yamllint +
shellcheck) and tests (run-tests.sh).
The docs-only cheap path. On a PR whose every changed path is on the allowlist in
scripts/ci/docs-only.sh — files no test reads, such as docs/**
and this file — each tests leg still runs and reports success, but skips its install
and test steps. To see which path a leg took, open its Classify the change step (or the
job summary): it says CHEAP path or FULL run, and on a full run names the first file
that forced it. A push to develop always runs in full, and anything the classifier cannot
decide (no base ref, a git error, an unknown path) runs in full too. Jobs are never skipped,
only steps: a skipped job reads as "CI verified nothing" to flight ci watch, and Forgejo
posts a green status for it. lint has no cheap path: it takes seconds and reads .yml and
scripts a docs PR can touch.
Most of this repo's markdown is product, and the suite reads it — skills, references,
flight/CHANGELOG.md, flight/GUIDE.md, the root README.md — so the allowlist is a list
of paths, never a *.md match. To extend it, first confirm nothing under scripts/tests/
reads the path (or runs a script that does), then add it to ALLOWLIST.
scripts/tests/docs-only.test.sh fails if any test file names an allowlisted path, so a test
that later starts reading one forces it back off the list.
Adding a check or test. A pre-push check is any *.sh in scripts/checks/
(run-checks.sh runs each that is executable). A unit test is any *.test.sh in
scripts/tests/ (run-tests.sh runs each). Keep unit tests out of scripts/checks/
— the pre-push path is for cleanliness, not for testing individual scripts.
A new unit test must be self-contained: build a mktemp -d sandbox, touch nothing
under $HOME or the repo, and never git config --global. The runner runs files
concurrently, so a test that reaches outside its sandbox will fail intermittently and
blame whichever test it collided with. run-tests.test.sh covers the runner itself.
Markdown may be CRLF. .gitattributes gives *.md native line endings on purpose, so on
a Windows checkout every line of a SKILL.md, reference or changelog ends in \r. A test or
script that reads markdown must strip it before matching: tr -d '\r' on a stream, or
sub(/\r$/, "") first thing in an awk program. The usual casualties are exact-line
comparisons ($0 == marker) and end-anchored patterns (^name: foo$, /^```$/). They
pass on Linux and macOS and fail only on the Windows leg, looking like plain false asserts.
That leg runs only on pull requests into qa and main (not the main → qa sync-down)
and on a manual dispatch of the tests workflow, never on feature PRs into develop
(FJ-305), so a Windows-only failure first shows up when develop is promoted. To catch it
before then, run the suite on a scratch copy of your committed HEAD with every .md
converted to CRLF:
D="$(mktemp -d)" && git archive HEAD | tar -x -C "$D" && cd "$D" && git init -q \
&& find . -name '*.md' -exec sed -i 's/\r\?$/\r/' {} + && ./scripts/run-tests.sh(sed -i without a suffix is GNU sed. On macOS, use sed -i ''.)
Executable bit — important. This repo has core.fileMode = false, so a plain
chmod +x is not recorded by git; a fresh clone would get a 644 file and
directly-run scripts (and the pre-push hook) would break. Commit the bit explicitly:
git update-index --chmod=+x scripts/your-new-script.sh
git ls-files -s scripts/your-new-script.sh # verify it shows 100755All backend access goes through the dispatcher (flight/scripts/flight)
— never raw API calls or MCP. It routes <group> <verb> to a per-backend adapter and
resolves coordinates/tokens from .flightdirector/config.json.
flight/scripts/adapters/<backend>/
_common.sh # shared helpers for the backend
issues # the four groups, one executable each
labels
pr
ci
The contract every adapter implements — the groups, verbs, arguments, and output/exit
conventions — is
flight/references/adapter-contract.md.
Adding a backend means adding adapters/<backend>/{issues,labels,pr,ci} (an
issues-axis-only backend implements just issues + labels); the dispatcher picks it
up by name with no dispatcher changes.
Prove parity with a rig under test-rig/<backend>/: up.sh provisions a throwaway
target (a disposable container for self-hostable backends, an ephemeral repo/token for
SaaS ones) leaving a gitignored .work/; smoke.sh runs the adapter verbs against it;
down.sh tears it down. See test-rig/README.md. Open backend
work: GitLab (#12) and Jira (#13).
flight develops itself through its own pipeline: feature → develop → qa → main
(code.stages in .flightdirector/config.json). Use the skills:
working-an-issue— one branch + worktree per issue under.worktrees/, status labels that track the board, an explicit human merge gate.promoting-a-branch— advances a branch one hop, applying that hop's merge strategy and gate. Every hop in this repo is configuredmerge: pr,feature → developincluded (developandqaalso carrygate: post-merge-qa), so promoting a feature branch pushes it, opens a pull request, watches CI, and merges on green with the defaultmergestrategy — not a direct--no-ffmerge.
About that PR hop:
- Bring a branch level with its target by merging, not rebasing. Merge
developinto the feature branch; a rebase rewrites the commits and their signatures with them, and signed commits are required (below). - CI runs four blocking legs on a PR, all in
.github/workflows/tests.yml: bash 5, bash 3.2 (the interpreter macOS ships), macOS (BSD userland) and Windows (MSYS). A test that is green locally on Linux can still go red on bash 3.2 or on BSD/MSYS tools, so expect to read all four..github/workflows/lint.yml(yamllint + shellcheck) runs alongside them.
Other conventions:
- Signed commits are required. The pre-push
verify-git-logs.shrejects any unpushed commit whose signature isn't good (%G?ofG/U). Merge commits occasionally sign badly (B) — re-sign withgit commit --amend --no-edit -Sbefore pushing. - Agent instructions — repo-wide rules for coding agents live in AGENTS.md
(Codex reads it directly; CLAUDE.md imports it via
@AGENTS.mdand adds only Claude Code-specific notes). EditAGENTS.mdfor anything both harnesses should know. - Commit trailers — the required
Co-Authored-By/ session trailers are added by each harness; keep them on agent-authored commits. - Code style — tabs (width 4); trailing whitespace trimmed on save (except
.md); leave one final newline. See AGENTS.md.
Public issues live on GitHub (AICrafting/FlightDirector) — the tracker this repo's
flight config points at is private, so GitHub is where outside reports land and get triaged.
Blank issues are disabled; pick one of the forms:
.github/ISSUE_TEMPLATE/bug_report.yml— harness, plugin version, backend, the skill or dispatcher command, expected vs actual, and a redacted.flightdirector/config.json. Never attachsecrets.jsonor a token..github/ISSUE_TEMPLATE/feature_request.yml— the problem first, then the proposed behaviour and what it touches.- Security problems go to SECURITY.md by email, not to an issue.
Pull requests are pre-filled from
.github/PULL_REQUEST_TEMPLATE.md, which mirrors what the
promoting-a-branch skill produces: a summary, a ## Test plans block (numbered steps ending
in Expected:), the Ready #N / Closes #N lines, and the signed-commit checklist —
review is routed to the owners listed in .github/CODEOWNERS.
When you bump a plugin's version, three things must move together and its changelog
needs rolling. scripts/bump-version.sh does the in-repo mechanical part in one pass —
it takes the plugin name, so it works for any plugin in this repo:
scripts/bump-version.sh flight 0.5.0It resolves the plugin's directory from its source in .claude-plugin/marketplace.json,
then:
- updates the version in
<plugin>/.claude-plugin/plugin.json, in<plugin>/.codex-plugin/plugin.json(when present, so both harness manifests stay in lockstep), and in that plugin's entry in the published.claude-plugin/marketplace.json(only that entry — other plugins are left alone); - rolls
<plugin>/CHANGELOG.md: the top## [Unreleased]becomes## [0.5.0] - <today>, with a fresh empty## [Unreleased]seeded above it. It warns (but doesn't stop) if[Unreleased]was empty when you rolled it.
The dev-marketplace cache refresh stays a manual step — it lives outside the repo; see docs/plugin-marketplace-dogfooding.md.
Tagging. The bump happens on a release/<plugin>-<version> branch merged into develop;
the tag happens once that version has been promoted all the way to main. Then run:
scripts/tag-release.sh flight # add --dry-run first to see the planIt reads the version from flight/.claude-plugin/plugin.json at origin/main, refuses if the
tag flight-<version> already exists anywhere (tags are immutable — bump and ship the next
version instead), takes that version's flight/CHANGELOG.md section as the notes (and refuses if
it is missing), creates a signed annotated tag on the main commit, pushes it to origin
(--push-to github as well if you want the mirror tagged), and creates the matching Forgejo
Release with the same notes using the coordinates in .flightdirector/config.json and the token
in .flightdirector/secrets.json (--no-release to skip). scripts/release-notes.sh is the
changelog-section extractor it uses; both are repo tooling, not part of the plugin.
Run the live test rigs. They are the only tests that talk to a real backend, and they are
manual on purpose — each waits minutes on real CI and writes to a shared throwaway repo — so a
release is the moment to run them. In the Forgejo web UI: Actions → rigs → Run workflow, with
the input all (or one of forgejo / github / gitlab). They can also be run locally:
( cd test-rig/github && ./up.sh && ./smoke.sh && ./down.sh ) # same under forgejo/ and gitlab/The workflow sets RIG_STRICT=1, so a rig that cannot reach a CI verdict fails rather than
warning. scripts/tag-release.sh prints this reminder too. See
test-rig/README.md for what each rig needs.
Publishing to the GitHub mirror. origin (Forgejo) is the source of truth; GitHub is a
mirror that is pushed by hand. After tagging, run:
scripts/push-to-mirror.sh # = --remote github --branch main; add --dry-run to previewIt pushes origin/main to the mirror fast-forward only (it refuses, and never forces, if the
mirror's main has moved out of band) and then pushes every tag pointing at that head, so the
release branch and its flight-<version> tag always arrive together. --branch is repeatable if
other branches should be mirrored too.
The skills under flight/skills/ follow the superpowers writing-skills
conventions (a skill is a directory with a SKILL.md plus any references/ or
templates/). Invoke that skill when creating or editing a skill, and mirror the voice
and structure of the existing skills (red-flags section, numbered lifecycle,
common-mistakes table).
The shell inside a skill's fenced blocks is what an agent actually runs, so it follows the
rules in flight/references/skill-shell-blocks.md.
Examples: flatten a branch name before using it in a filename, end every STOP branch with
something that actually stops (exit, return, an else), and put each push or pr open
behind the preflight check. scripts/tests/skill-shell-blocks.test.sh checks each rule over
every skill, so run-tests.sh catches a block that breaks one. To add a rule, follow that
reference's "Adding a rule" section: state the rule there and add its check to the test.