main is protected. Direct pushes log a "Bypassed rule violations" warning
on GitHub and are discouraged — even when you have admin override, you
should land changes through PRs so CI runs, the diff gets reviewed (by
you on a re-read, or by anyone else), and the merge commit captures
intent.
main ────────────────────────────────────────────────────► (protected)
▲
feat/short-slug ──────────► PR ───┘
fix/short-slug
chore/short-slug
# start work
git switch -c feat/short-slug
# code, commit (semantic prefix — feat / fix / refactor / test / docs / chore)
git add ...
git commit -m "feat(area): one-line subject
Optional body explaining why."
# push branch + open PR
git push -u origin feat/short-slug
gh pr create --base main --fill
# CI runs against the PR. When green, squash-or-merge from the GitHub UI.feat/— new behavior, new commands, new capabilitiesfix/— bug fixesrefactor/— restructuring without behavior changetest/— adding tests for existing behaviordocs/— README / CONTRIBUTING / inline docstring changeschore/— dependency bumps, CI, tooling
Keep slugs short and verb-noun (feat/peer-label, fix/migrate-backup,
refactor/use-cases-deps).
security/— security audits and their fixes (e.g.security/audit-and-fixes)experiment/— benchmarks and throwaway spikes; carries thebench/,eval/, and.bench-data/working setshandoff/— ephemeral session handoffs; safe to delete after landingbackup/— protected safety refs (e.g.backup/main-pre-cleanup); never rebased, never deleted without a successor
main carries only what ships or builds the package: src/, tests/,
bin/, config/, .claude-plugin/, .claude/{hooks,skills,helpers}, the
benchmark runners (bench/, eval/), and repo meta (README, LICENSE,
package.json, .github/). Its history was rewritten on 2026-06-24 to purge
process/marketing artifacts — .planning/, docs/, site/, demo/,
assets/, and folklore-rs/ — which shrank the clone from ~320 MB to ~3 MB.
The pre-rewrite history is preserved at origin/backup/main-pre-cleanup.
Those purged directories live only on dev branches going forward:
| Directory | Lives on | Why off main |
|---|---|---|
.planning/ |
feat/*, experiment/* |
GSD planning state, not shipped |
docs/ |
docs/* |
research notes, not shipped |
site/ |
a site / docs/* branch |
marketing site (Cloudflare Pages) |
demo/ |
dev branches | demo scripts / recordings |
assets/ |
dev branches | images / media |
folklore-rs/ |
a feat/folklore-rs branch |
Rust client, separate build |
Rule of thumb: if npm ci && npm run build && npm test doesn't need it, it
does not belong on main. Open a dev branch for it.
The marketing-site deploy (
deploy-site.yml) was removed frommainalong withsite/. It now lives on the branch that ownssite/; deploy from there.
push feat/fix branch
│
▼
open PR → main ───────────────► .github/workflows/ci.yml
│ node {22,24}:
│ npm ci → lint → build → bootstrap → test
│ (this is the required "test" check)
▼
green CI + 1 review + conversations resolved
│
▼
squash-merge to main ──────────► (protected; no force-push)
│
▼
git tag vX.Y.Z && git push --tags
│
▼
.github/workflows/release.yml
├─ publish-npm → npm publish --provenance (@usefolklore/folklore)
└─ publish-docker → ghcr.io/usefolklore/folklore:{tag,latest}
Workflows on main:
ci.yml— PR + push gate. Job nametestsatisfies branch protection.release.yml— fires onv*tags; npm (OIDC provenance) + GHCR docker.
Site deploy is intentionally not on main — see the layout note above.
type(scope-or-phase): one-line subject (lowercase, no period)
Optional multi-line body. Explain the *why* — the diff explains the what.
Reference phase / requirement IDs when relevant (ROOMS-DEL-04, NET-02).
Out of scope for this commit (deferred):
- bullet
- bullet
Examples from this project:
feat(26): stamp github_user at indexNode + migrate --stamp-github back-fill
fix(25): migrate v5 idempotent across rollback → re-migrate round-trip
refactor(ci): port Phase 39 to in-process bus, delete Phase 18 NET-04
Don't add Co-Authored-By trailers for AI assistants or third-party tools
unless explicitly requested. Solo-author by default.
Every PR runs .github/workflows/ci.yml against the Node version matrix
(currently 22, 24). The merge button stays grey until both pass.
If you need to bypass for an emergency, the enforce_admins setting on
main is currently false — admin push will land but log a warning. The
preferred path is a hotfix branch + immediate PR + self-merge.
npm ci
npm run build
bash scripts/bootstrap.sh # one-time: pulls submodules + python venv
npm test # full suite (≈ 18 s on a modern Mac)Targeted single-file run:
node --import tsx --test tests/phase26.e2e-share-sync.test.tsThe full suite ships green at 0 fail / 0 cancelled / 7 skipped (the 7
skipped are documented opt-outs — see each test file's header).
- Unit + structural — pure / mock-driven, fast (< 5 s total). Every push.
- Integration in-process — fake-libp2p / Y.Doc-ferry / inline fakes. Still runs on every push because there's no network in the loop.
- Integration real-network — none currently in the tree. The ex-flaky multi-libp2p tests were either ported to in-process (Phase 39) or deleted (Phase 18 NET-04 — libp2p's own contract, not ours).
If you need to add a real-network smoke, gate it behind an opt-in env
var (e.g. FOLKLORE_REAL_NET=1) and exclude it from the default CI
workflow.
Once you're comfortable with the PR-only flow, run:
gh api -X PUT repos/usefolklore/folklore/branches/main/protection \
--input - <<EOF
{ "enforce_admins": true,
"required_pull_request_reviews": { "required_approving_review_count": 1 },
"required_status_checks": null,
"restrictions": null }
EOFThat makes the PR + review requirement bind admins too, so a stray
git push origin main from a tired Friday night gets rejected
instead of silently bypassing the rule.