See CONTRIBUTING.md for build setup, testing, and PR workflow.
The standard development loop in this repo: make changes, run bin/ci, fix
what it catches, repeat until green, then push. Treat bin/ci as your
inner-loop companion, not a final hurdle.
basecamp-cli/
βββ cmd/basecamp/ # Main entrypoint
βββ internal/
β βββ appctx/ # Application context
β βββ auth/ # OAuth authentication
β βββ cli/ # CLI framework
β βββ commands/ # Command implementations
β βββ completion/ # Shell completion
β βββ config/ # Configuration management
β βββ dateparse/ # Date parsing
β βββ hostutil/ # Host utilities
β βββ models/ # Data models
β βββ names/ # Name resolution
β βββ observability/ # Tracing and metrics
β βββ output/ # Output formatting
β βββ presenter/ # Output presentation
β βββ resilience/ # Retry and backoff
β βββ sdk/ # Basecamp SDK wrapper
β βββ tui/ # Terminal UI
β βββ version/ # Version info
βββ e2e/ # BATS integration tests
βββ skills/ # Agent skills
βββ hooks/ # Agent lifecycle hooks (both agents)
βββ .claude-plugin/ # Claude Code integration
βββ .codex-plugin/ # Codex plugin manifest
API documentation: https://github.com/basecamp/bc3-api
Key endpoints used by the CLI:
/projects.json- List projects/buckets/{id}/todolists/{id}/todos.json- Todos in a list/buckets/{id}/todos/{id}/completion.json- Complete todo/people.json- List people/my/profile.json- Current user
Search: Use basecamp search "query" for full-text search across projects. The Recordings API (basecamp recordings) is for browsing by type/status without a search term.
bin/ci is the local CI gate. It runs every check that remote CI runs:
formatting, vetting, linting, unit tests, e2e tests, naming conventions,
CLI surface snapshots, skill drift detection, SDK provenance, and go mod tidy.
Run it early and often β after finishing a feature, after fixing a bug, before
pushing. If you're about to git push and haven't run bin/ci in this
session, stop and run it first.
Skill drift: make check-skill-drift runs over both skills/basecamp/SKILL.md
and skills/basecamp-doctor/SKILL.md, checking that the commands and flags each one
references still exist in the .surface snapshot. It catches stale references, not
missing coverage, so adding a command breaks neither skill.
Removal is only partly caught. resolve_cmd walks up to the nearest existing ancestor,
so dropping a nested subcommand leaves the reference resolving against its parent and the
check still passes β basecamp setup <removed> resolves as basecamp setup. Removing a
top-level command is caught; removing a subcommand is not. Don't lean on CI for this. Update the skill the change actually affects;
basecamp-doctor deliberately covers only doctor, setup and auth remediation.
bin/ci # The single command β run thisWhen iterating on a specific area, use targeted make targets for faster
feedback, then finish with bin/ci before pushing:
make build # Build binary to ./bin/basecamp
make test # Go unit tests
make test-e2e # BATS end-to-end tests
make lint # Linter
make check # All checks (what bin/ci runs)Dead code: make deadcode reports unreachable functions whole-program, rooted
at the shipped binary and again with -tags dev. It is a report, not a gate, and
is deliberately outside make check. The linter's unused cannot do this job β
it runs per-package and counts every exported identifier in a non-main package as
used, which is how 600 lines of exported, zero-caller internal/tui API survived
it. Root it at the binary, not ./...: only main packages are roots, so ./...
reports everything no main reaches β mostly the dev-tagged workspace tree β as
unreachable.
It also analyzes one GOOS/GOARCH at a time, while we release five. A host run says
nothing about the others: code behind another platform's build tag is never loaded,
and a function whose only caller sits behind one looks unreachable. Before deleting
anything platform-adjacent, check the other targets β install the tool for the host
and set GOOS for the analysis (GOOS=windows deadcode ./cmd/basecamp), since
GOOS=windows go run cross-compiles the tool itself and fails.
Read the output before acting on it: a zero-caller exported symbol is a candidate,
not a verdict, and the dev-tagged tree is legitimately partial.
Requirements: Go 1.26+, bats-core for e2e tests.
For local development against BC3:
BASECAMP_BASE_URL=http://3.basecamp.localhost:3001 basecamp auth loginOAuth endpoints are discovered via .well-known/oauth-authorization-server.
make bench # Run all benchmarks
make bench-cpu # Run with CPU profiling
make bench-mem # Run with memory profiling
make bench-save # Save baseline for comparison
make bench-compare # Compare against baselineThe CLI depends on the Basecamp SDK (github.com/basecamp/basecamp-sdk/go). When the
SDK adds new API operations, the CLI must add corresponding commands.
Tracking: internal/version/sdk-provenance.json records the SDK version +
API revision the CLI is built against. API-COVERAGE.md tracks endpoint coverage.
Workflow (see also .claude/skills/sdk-bump.md):
make bump-sdkβ updates go.mod + provenance (never edit go.mod directly)go build ./...β detect breaking changes- Fix any compilation errors in
internal/commands/andinternal/sdk/ - Check for new SDK services/methods β create corresponding CLI commands
make testβ verify all pass including catalog parity- Update
API-COVERAGE.mdwith new endpoint coverage
Completeness bar: every new SDK service method needs:
- Command file in
internal/commands/ - Catalog entry in
commands.go - Registration in
internal/cli/root.go+commands_test.go - API-COVERAGE.md row
Andon cord: if the SDK lacks a Go service wrapper for a generated endpoint, stop and open an issue on basecamp-sdk β never call the raw generated client from CLI code.
See STYLE.md for the Go conventions used here. Read it when writing or reviewing Go;
it is not imported, so it stays out of context for sessions that never touch Go.