For everyone who wants to change code in prosa. Before you start, read
../INTENT.md end-to-end. Almost every "should I…?"
question is answered there.
prosa pins its toolchain via ../devbox.json. Inside
devbox shell you get exact versions of:
- Go 1.26
- buf 1.70
- protobuf 34.1
- golangci-lint 2.12
- gofumpt 0.10
- sqlite 3.51
- gitleaks 8.28
- lychee 0.23
- markdownlint-cli2 0.21
- ripgrep 15.1
- just 1.51
- node 24
- pnpm 10.20
You don't need to install any of those by hand. The shell exports GOBIN
into the repo's ./bin/, installs the Node tooling with pnpm, and installs
the Husky git hooks.
git clone https://github.com/c3-oss/prosa.git
cd prosa
devbox shell
just tools # installs protoc plugins into ./bin
just build # builds the three binariesIf you don't use devbox, you'll need the same tool versions installed
somewhere on PATH. The CI workflows use devbox; mirroring that is the
safe path.
The full recipe set is in ../.justfile. Highlights:
| Command | Notes |
|---|---|
just build |
go build -o ./bin/ ./cmd/... |
just run -- <args> |
builds then runs ./bin/prosa <args> |
just test |
go test ./... |
just test-race |
go test -race -count=1 ./... |
just cover |
coverage profile + per-function summary |
just vet |
go vet ./... |
just lint |
golangci-lint run ./... |
just quality |
Markdown, link, agent-config, and secret checks |
just lint-md |
markdownlint over tracked Markdown |
just lint-links |
lychee over tracked Markdown links |
just lint-agents |
agnix over agent-facing config |
just lint-secrets |
gitleaks over the current tree |
just gen |
buf lint + buf generate + gofumpt gen/ |
just gen-check |
regeneration must be no-op |
just tidy-check |
go.mod/go.sum must be tidy |
just ci |
full local pipeline + git diff --exit-code |
just snapshot |
local GoReleaser dry-run into dist/ |
just docker-build |
local Docker image prosa:local |
just clean |
nukes ./bin, ./dist, coverage artifacts |
The repo has no Makefile. just is the canonical task runner — CI workflows
call it directly.
devbox shell installs Husky hooks through pnpm:
commit-msgruns commitlint againsttype(scope): subject.pre-commitruns lint-staged Markdown checks, agnix, and staged gitleaks.pre-pushrunsjust hooks-pre-push, currently the same non-Go gate asjust quality.
CI also runs commitlint over pull-request commits and just quality.
These mirror INTENT § How I think when I code. They are not aspirational; they are enforced by reviewers (human or agent).
- Standard library first. Reach for a dependency only when stdlib costs measurably more code than the dependency would save.
- Error wrapping:
fmt.Errorf("doing X: %w", err). Nopkg/errors-style ladders. - Logging:
log/slogwith the default text handler in CLI commands. Importers and the pusher write through a caller-supplied*slog.Loggerso the interactive sync path can scope them;slog.SetDefaultis never called at runtime. - Comments are sparse, local, and surgical. Reach for one only to document a behaviour that is not immediately obvious; otherwise let the code speak for itself. Docstrings are allowed but stay short and avoid narrating internals. Decisions and rationale belong in Markdown docs, not inline in code.
- Filesystem paths via
internal/paths— never hardcode~/...or XDG layouts elsewhere. - Tests: stdlib
testingplusgithub.com/stretchr/testify/require. No mocking frameworks; if a layer is hard to test without mocks, its shape is wrong. - No premature abstraction. Three call sites or it's not a helper. One call site stays inline.
- No feature flags where a delete would do.
- Generated code under
gen/is committed; CI fails if regeneration produces a diff.
The project follows type(scope): subject in the imperative. Commitlint
enforces the shape locally and in CI.
Examples:
feat(release): ESM shim with async spawn and signal forwarding
chore(tooling): standardize on Node.js 24 across release pipeline
docs(release): install paths (brew, curl, npm), setup mocks, intent update
build(deps): add nodejs@24
Scopes are defined in ../commitlint.config.cjs.
Use the smallest accepted scope that helps a future reader.
For large refactors, prefer several smaller commits grouped by category of change over one mega-commit. Each commit should be reviewable on its own.
Importers live in internal/importers/<agent>/. They implement the
interface in pkg/importer/importer.go:
type Importer interface {
Name() string
DefaultRoots() []string
Walk(ctx context.Context, root string) ([]string, error)
Import(ctx context.Context, jsonlPath string, sink Sink) (ImportResult, error)
}Steps:
- Read the canonical contract —
architecture/canonical-session.md.
Every importer maps onto the same
session.Session,session.Turn,session.ToolUsageshape. - Document the source format — add
docs/sources/<agent>.mdthat describes how the agent's JSONL is laid out on disk, where session IDs come from, how to detect first-prompt, model, project context. - Implement the importer in
internal/importers/<agent>/. Look atinternal/importers/claudecode/andinternal/importers/codex/for shape. Keep the package small: one file per concern (walk.go,parse.go,importer.go, plus tests). - Preserve the raw — copy the source
.jsonlbyte-for-byte into the prosa data dir atRawPath(viainternal/paths.RawRoot(agent)). Never alter source bytes; the hash must match what was on disk. - Be idempotent — short-circuit on hash match with the store's
LastHash. Don't try to merge or diff turns. - Write parser tests under
internal/importers/<agent>/parse_test.gocovering: representative records, missing fields, malformed JSON, truncated files, sessions with no turns. - Register it in the sync command's importer list
(
internal/cli/sync.go).
Before opening a PR, run the importer reviewer agent locally — see agents.md — or at minimum:
just test ./internal/importers/<agent>/... -race
just test ./internal/store/... -race
just test ./internal/cli/... -raceThe proto source of truth is proto/prosa/v1/*.proto. Generated Go lives in
gen/go/prosa/v1/ and is committed.
just gen # regenerates after a proto edit
just gen-check # CI uses this; must be a no-opSchema versions are pathway-versioned (v1, v2, …). Breaking changes get
a new version directory, not an in-place edit.
- If you change a public surface (CLI command/flag, proto, schema), update
the matching
docs/page in the same PR. - Don't write documentation that the code doesn't yet support. If a feature
is planned, put it in
../ROADMAP.mdinstead. - Docs describe the present and state what the code is, not what it was or isn't. After a refactor from X to Y, the doc says "it does Y" — not "instead of X, it does Y". Delete the old description; don't narrate the change.
- Don't duplicate INTENT in
docs/. Link to it. - Markdown rendered by GitHub. No site generator. Keep diagrams as ASCII or inline SVG.
Releases are tag-driven (v* tags trigger GoReleaser + npm + Docker). The
maintainer runbook is in distribution/release.md.
If you're not the maintainer cutting the release, you don't need to read that.
Project-specific subagents and skills live in .codex/ and .claude/.
Their orientation is in agents.md. If you spawn an agent that
should follow project rules, point it at INTENT first.
| Lane | Read first | Code |
|---|---|---|
| CLI commands | architecture/cli.md | internal/cli/ |
| CLI rendering | cli/rendering-contract.md | internal/cli/render/ |
| Importers | architecture/importers.md, sources/.md | internal/importers/<agent>/ |
| Local store | architecture/store.md | internal/store/, migrations/local/ |
| Server | architecture/server.md | internal/server/, migrations/server/ |
| Panel | architecture/panel.md, panel/ | internal/panel/, cmd/prosa-panel/ |
| Sync | concepts.md#push-only-sync | internal/cli/sync*.go |
| Proto / API | architecture/server.md | proto/prosa/v1/, gen/go/prosa/v1/ |
| Paths / XDG | (read the package) | internal/paths/ |
| Distribution | distribution/ | .goreleaser.yaml, install.sh, npm/, Dockerfile, .github/workflows/ |