Development Guide

July 17, 2026 · View on GitHub

Development setup and workflows for get-tbd (the tbd CLI).

Prerequisites

  • Node.js >= 20
  • pnpm (will be installed automatically via corepack)

Setup

# Enable corepack (includes pnpm)
corepack enable

# Install dependencies
pnpm install

# Install git hooks
pnpm prepare

Development Workflow

Running the CLI from source

During development, run the CLI directly from TypeScript source (no build needed):

pnpm tbd --help
pnpm tbd list
pnpm tbd create "My issue" -t bug

Running the built CLI

To test the production build:

pnpm build
pnpm tbd:bin --help

Testing new shortcuts, guidelines, or templates

Important: When adding new documentation files (shortcuts, guidelines, templates) to packages/tbd/docs/, you must test with the local build, not the globally installed tbd.

The globally installed tbd has its own bundled docs from the published npm package. Running tbd setup --auto with the global installation won’t include your new files.

# 1. Add your new file to packages/tbd/docs/shortcuts/standard/my-shortcut.md

# 2. Build to bundle the new file into dist/docs/
pnpm build

# 3. Run setup with the LOCAL build (from repo root)
node packages/tbd/dist/bin.mjs setup --auto

# 4. Verify the file was copied to .tbd/docs/
ls .tbd/docs/shortcuts/standard/my-shortcut.md

# 5. Test the shortcut with the local build
node packages/tbd/dist/bin.mjs shortcut my-shortcut

Never manually create files in .tbd/docs/—always add them to packages/tbd/docs/ and let setup --auto copy them. This ensures the setup process works correctly for users.

Testing the packaged installation

To test the CLI exactly as users would install it from npm:

# Build, pack, and install globally (like npm install -g get-tbd)
pnpm test:install

# Test the installed binary
tbd --help

# Uninstall when done
pnpm test:uninstall

This creates an npm tarball and installs from it, validating the full package structure.

Building

# Build all packages
pnpm build

# Watch mode for development
pnpm --filter get-tbd dev

Testing

# Run tests
pnpm test

# Run tests with coverage
pnpm test:coverage

# Watch mode
pnpm --filter get-tbd test:watch

Formatting and Linting

# Format code (auto-fix)
pnpm format

# Check formatting (CI)
pnpm format:check

# Lint with auto-fix
pnpm lint

# Lint check only (CI)
pnpm lint:check

# Type check
pnpm typecheck

Validating Package

# Validate package.json exports
pnpm publint

Git Hooks

Git hooks are managed by lefthook and run automatically:

  • pre-commit: Format, lint, and typecheck staged files
  • pre-push: Build (if needed), run tests, and—when a package.json is staged—enforce the 14-day package-age rule via pnpm check:package-age.

To skip hooks (emergency only):

git commit --no-verify
git push --no-verify

Dependency Hygiene: The 14-Day Package-Age Rule

This repo enforces the 14-day package-age rule documented in packages/tbd/docs/guidelines/pnpm-monorepo-patterns.md: do not install or upgrade to any package version less than 14 days old.

  • pnpm upgrade:check, pnpm upgrade, and pnpm upgrade:major are wired to ncu --cooldown 14; they will refuse to bump to versions inside the window.
  • pnpm check:package-age (also wired into pre-push) scans every package.json in the repo, queries the npm registry for each pinned version’s publish time, and exits non-zero on any pin under 14 days. Add --warn to report without failing.
  • Exceptions (CVE patches inside the window) must be documented in the commit message or PR description with CVE ID, upstream link, and a Reviewed-by: line.

The check requires registry access (https://registry.npmjs.org); skip it with SKIP=package-age git push only if you’re pushing infrastructure changes that do not touch dependencies.

Commit Conventions

We use Conventional Commits for commit messages.

Format

<type>: <description>

[optional body]

[optional footer(s)]

Types

  • feat: New feature
  • fix: Bug fix
  • docs: Documentation for the product or main codebase (not process docs)
  • process: Documentation or tooling changes to agent or human development processes
  • style: Code style (formatting, no logic change)
  • refactor: Code change that neither fixes a bug nor adds a feature
  • test: Adding or updating tests
  • chore: Maintenance tasks (deps, config, etc.)

Examples

feat: Add support for custom labels

fix: Handle empty issue list gracefully

docs: Update CLI usage examples

process: Add TDD guidelines for agent workflows

test: Add golden tests for sync command

chore: Update dependencies

Notes

  • No scope by default: Don’t include a scope like fix(tbd): for the main codebase. Only use a scope when it provides key disambiguation or clarification (e.g., fix(parser): vs fix(cli): when the distinction matters).
  • Keep the first line under 72 characters
  • Use imperative mood ("Add feature" not “Added feature”)

Creating Releases

Releases are tag-triggered and assembled from clean conventional commits—we do not use Changesets (no .changeset/ files, no “Version Packages” PR). get-tbd is a single published package, so the per-PR changeset ceremony isn’t worth it; release notes are composed from the commits since the last tag at release time.

.github/workflows/release.yml runs on a v* tag push: it builds, runs publint, publishes get-tbd to npm, and creates a GitHub Release whose body is the matching ## X.Y.Z section of packages/tbd/CHANGELOG.md.

Release process

  1. From clean main, review git log <last-tag>..HEAD and choose the version by the substance of the user-facing change, not the commit-type label: a new CLI capability → minor; fixes, docs, and guidance-content changes → patch (even when a commit is labeled feat); breaking → major. Note for 0.x a semver minor is 0.MINOR.0. See publishing.md §Step 2 for details.
  2. On a claude/release-vX.Y.Z branch: bump version in packages/tbd/package.json and prepend a ## X.Y.Z section to packages/tbd/CHANGELOG.md with notes written per release-notes-guidelines.
  3. pnpm release:verify (build and publint) and pnpm test; open the release PR; merge once CI is green.
  4. Gate before tagging: wait until main CI has reached conclusion=success on the merge commit itself (filter the run by that SHA—right after a merge an unfiltered query can return the previous run). Only then tag vX.Y.Z on that exact commit and push it—the Release workflow publishes to npm and creates the GitHub Release. See publishing.md §Step 6 for the exact gate commands.

For the full step-by-step (including the version-bump heuristic, supply-chain review, and verification), see publishing.md.

CI and GitHub Actions

Keep logic out of workflow YAML. Do not put non-trivial shell—multi-line awk, sed, jq pipelines, regex parsing, conditional logic—inline in a GitHub Actions run: step. Inline CI shell cannot be tested or debugged in isolation; the only way to exercise it is to push a tag or branch and wait for the runner, which is slow and error-prone. A real bug shipped this way: the release workflow’s inline awk changelog extractor silently produced an empty body on every release (v0.1.30 and v0.2.0 both went out with the fallback Release vX.Y.Z string) because it exited 0 either way.

Instead, write a clean, unit-tested script and invoke it by reference:

  • Put the pure logic in a src/ module with an exported function and a thin scripts/*.ts CLI wrapper (run via tsx). See src/utils/changelog.ts, its wrapper scripts/extract-changelog.ts, and the test tests/extract-changelog.test.ts. Import source from tests as ../src/...js (avoid importing .mjs from a test—it resolves inconsistently under vitest on Windows).
  • Cover it with a normal vitest test so the behavior is locked in and debuggable locally.
  • In the workflow, the run: step should only call the script (pnpm exec tsx packages/tbd/scripts/extract-changelog.ts …) and wire its output; keep any remaining shell to trivial plumbing (e.g. the GITHUB_OUTPUT heredoc).

If you find yourself reaching for awk/sed in a workflow, that is the signal to move it into a script.

Project Structure

tbd/
├── packages/
│   └── tbd/               # Main CLI package
│       ├── src/
│       │   ├── index.ts   # Library entry (node-free)
│       │   ├── cli/       # CLI-specific code
│       │   │   ├── bin.ts
│       │   │   ├── cli.ts
│       │   │   ├── commands/
│       │   │   └── lib/
│       │   ├── lib/       # Core library (schemas, types)
│       │   ├── file/      # File layer
│       │   └── git/       # Git layer
│       └── tests/
├── scripts/               # Development scripts
├── docs/                  # Documentation
└── .github/workflows/     # CI/CD (release.yml publishes on v* tags)

Architecture

See tbd-design.md for the full design document.

Key concepts:

  • File Layer: Markdown and YAML front matter format
  • Git Layer: Sync via dedicated tbd-sync branch
  • CLI Layer: Commander.js with Beads-compatible commands

CLI Patterns

The CLI follows patterns from research-modern-typescript-cli-patterns.md:

  • Base Command pattern for shared functionality
  • Dual output mode (text and JSON)
  • OutputManager for consistent output handling
  • Proper stdout/stderr separation

Worktree Architecture

tbd uses a hidden git worktree to store issue data on the tbd-sync branch while keeping the user’s working directory clean. The sync worktree is anchored under Git’s common directory so the main checkout and any linked worktrees created by tools like Codex all share the same local issue state. See tbd-design.md §2.3 for the full specification.

Why Worktree?

  • Fast search: ripgrep can search issues without git plumbing commands
  • Direct file access: Read/write issues as regular files, no git show/git cat-file
  • Isolated from main: Issues don’t pollute working directory or affect main branch
  • Conflict-free across linked worktrees: One shared worktree owns tbd-sync, and a repo-scoped lock serializes mutations

Path Conventions

.tbd/                               # Config directory (on main branch)

│ Committed to the repo:
├── config.yml                      # Project configuration
├── .gitignore                      # Controls what's gitignored below
├── doc-forks/                      # Fork manifest + base snapshots (f05; tbd-design.md §2.9)
├── workspaces/                     # Persistent state (outbox, named workspaces)
│   └── outbox/                     # Sync failure recovery data

│ Gitignored (local only):
├── state.yml                       # Local state
├── docs/                           # Installed documentation (regenerated on setup)
└── backups/                        # Legacy local backups

$GIT_COMMON_DIR/tbd/                # Shared by all linked worktrees of this repo
├── layout.yml                      # Common-dir layout metadata (mirrors config's tbd_format)
├── locks/
│   └── data-sync.lock/             # mkdir-based repo-scoped lock
├── backups/                        # Shared migration/repair backups
└── data-sync-worktree/             # Hidden worktree
    └── .tbd/data-sync/             # Actual issue storage (on tbd-sync branch)
        ├── issues/
        ├── mappings/
        ├── attic/
        └── meta.yml

.tbd/doc-forks/ is committed and holds only fork tracking state: the forks.yml manifest plus base/ snapshots that tbd docs update three-way merges against. The doc fork dir itself lives deliberately outside .tbd/ — default docs/tbd/, tracked in git like any other docs.

CRITICAL: Issues must be written to the worktree path ($GIT_COMMON_DIR/tbd/data-sync-worktree/.tbd/data-sync/issues/), NOT the direct path (.tbd/data-sync/issues/). The direct path is gitignored and exists only as a legacy diagnostic/migration location.

Format Upgrades and Rollback

A tbd_format bump writes exactly two stamps: the tracked .tbd/config.yml and the machine-local $GIT_COMMON_DIR/tbd/layout.yml (plus, only when tbd setup --auto is run, the tracked agent-surface markers). It never touches issue data, so any upgrade can be aborted: restore the tracked files from git and delete layout.yml (it regenerates from the config). The full state inventory and abort recipe are user-facing in tbd-docs.md §Troubleshooting → “Aborting a Format Upgrade”; the migrate → revert → repeat loop and both interrupted-upgrade partial states are pinned by tests in tests/common-dir-layout-doctor.test.ts (“f04 → f05 upgrade”).

Key Source Files

  • packages/tbd/src/lib/paths.ts - Path constants and resolveDataSyncDir()
  • packages/tbd/src/file/git.ts - Worktree init/health/repair functions
  • packages/tbd/src/cli/commands/sync.ts - Sync command with worktree checks
  • packages/tbd/src/cli/commands/doctor.ts - Health checks and repair

Worktree Health States

StateDescriptionFix
validWorktree exists and has correct branchNone needed
missingWorktree directory doesn’t existtbd doctor --fix
prunableDirectory deleted but git tracks ittbd sync --fix
corruptedMissing .git file or wrong branchtbd doctor --fix

Common Failure Modes

  1. Worktree deleted manually: User or tool deletes $GIT_COMMON_DIR/tbd/data-sync-worktree/. Git may still track it (prunable state). Fix: tbd sync --fix or tbd doctor --fix.

  2. Data in wrong location: Bug or old code writes to .tbd/data-sync/ instead of worktree. Fix: tbd doctor --fix migrates data to worktree.

  3. Fresh clone: Repo cloned but worktree not created. tbd setup --auto or first sync creates it.

  4. Git version mismatch: Orphan worktree requires Git 2.42+. Check: git --version, update if needed.

Debugging Tips

# Check worktree health
tbd doctor

# Verbose sync for debugging
tbd sync --debug

# List git worktrees
git worktree list

# Check what git thinks about the worktree
git worktree list --porcelain

# Manually prune stale worktree entries
git worktree prune

# Enable debug logging for path resolution
DEBUG=1 tbd sync
# or
TBD_DEBUG=1 tbd sync

Testing Worktree Code

Run the worktree health tests:

npx vitest run tests/worktree-health.test.ts

Run the e2e worktree scenarios:

npx tryscript run tests/cli-sync-worktree-scenarios.tryscript.md

Testing Forkable Docs

Forkable-docs behavior (fork/unfork/update/diff/status) is covered by tests/cli-docs-fork.tryscript.md, tests/cli-docs-update.tryscript.md, and tests/fork-cross-platform-e2e.test.ts (run from packages/tbd/).