Skip to content

Latest commit

 

History

History
345 lines (278 loc) · 18.9 KB

CONTRIBUTING.md

File metadata and controls

345 lines (278 loc) · 18.9 KB

Contributing

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.

Prerequisites

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:

  • shellcheck and yamllint on your PATH. The pre-push check scripts/checks/lint.sh runs each of them; a linter that isn't installed can't lint, so without both your push is unchecked locally and the CI lint workflow 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 from pip install yamllint.
  • python3 regardless of whether you enable the ledger — the prompt-logger scripts under flight/scripts/prompt-logger/ are Python, and their unit tests are part of scripts/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.

Repo layout

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.

Dev setup / dogfooding

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.

Running the checks and tests

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 parallel

Unit 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 .githooks

The 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 100755

Adapter development

All 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).

Branch / PR conventions

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 configured merge: pr, feature → develop included (develop and qa also carry gate: post-merge-qa), so promoting a feature branch pushes it, opens a pull request, watches CI, and merges on green with the default merge strategy — not a direct --no-ff merge.

About that PR hop:

  • Bring a branch level with its target by merging, not rebasing. Merge develop into 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.sh rejects any unpushed commit whose signature isn't good (%G? of G/U). Merge commits occasionally sign badly (B) — re-sign with git commit --amend --no-edit -S before pushing.
  • Agent instructions — repo-wide rules for coding agents live in AGENTS.md (Codex reads it directly; CLAUDE.md imports it via @AGENTS.md and adds only Claude Code-specific notes). Edit AGENTS.md for 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.

Filing issues and opening PRs on GitHub

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:

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.

Cutting a release

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.0

It 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 plan

It 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 preview

It 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.

Writing skills

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.