Castra is an agentic software development workspace focused on human-in-the-loop coordination, session management, and autopilot supervision.
  • TypeScript 97.7%
  • Shell 1.3%
  • Dockerfile 0.6%
  • JavaScript 0.3%
  • Python 0.1%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
Christoph Görn b229c0a350
docs: 📝 publish releases via the Forgejo API, not gh
`gh release create` is GitHub-only and does not work against this project's
Forgejo host — the hint printed after a release build was unusable, and
cutting v0.3.0 needed the API calls worked out by hand.

- release-build.sh prints the actual publish sequence (signed tag, create
  release, upload each asset, verify the published install path)
- README says Forgejo-only in the cut-a-release section

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-31 11:28:09 +02:00
.agents/skills chore: 🔧 settle terminology on allow-list / deny-list 2026-07-31 11:02:27 +02:00
.beads chore: 🔧 chain pre-commit framework into the beads pre-commit hook 2026-06-30 10:41:58 +02:00
.castra chore: 🔧 settle terminology on allow-list / deny-list 2026-07-31 11:02:27 +02:00
.claude chore: 🔧 add caveman, intake, and showboat skills 2026-07-01 07:48:57 +02:00
.codex/skills chore: 🔧 install OpenSpec workflow skills across agent runtimes 2026-06-18 12:16:24 +02:00
.pi chore: 🔧 add caveman, intake, and showboat skills 2026-07-01 07:48:57 +02:00
.work-issue chore: 🔧 track labels.json edits, personas, prompts, run-state 2026-06-18 15:29:32 +02:00
attic feat: reimplement castra as a TS mirror of the lage.bonn bash norm 2026-06-28 22:28:33 +02:00
docs docs(#3): add showboat demo for castra version 2026-07-01 07:46:01 +00:00
hack chore: 🔧 settle terminology on allow-list / deny-list 2026-07-31 11:02:27 +02:00
manifests fix: 🐛 resolve persona skills from CASTRA_HOME, not the clone (castra-hw4) 2026-07-31 10:57:00 +02:00
openspec docs: 📝 add decouple-forge-provider OpenSpec change 2026-06-18 12:16:39 +02:00
scripts docs: 📝 publish releases via the Forgejo API, not gh 2026-07-31 11:28:09 +02:00
specifications docs: 📝 rewrite README and declare GPLv3+ license 2026-06-18 13:28:31 +02:00
src feat: ship castra as a standalone binary, bump to 0.3.0 2026-07-31 11:12:09 +02:00
test fix: 🐛 resolve persona skills from CASTRA_HOME, not the clone (castra-hw4) 2026-07-31 10:57:00 +02:00
vendor/pi-ca-leash feat: vendor pi-ca-leash 1.3.0 model-catalog for pinned refresh 2026-06-18 17:42:16 +02:00
.containerignore refactor: ♻️ move persona tree under .castra/ to match the norm 2026-06-28 22:54:02 +02:00
.gitignore feat: persist stdout to OTEL via --persist-log / CASTRA_PERSIST_LOG (castra-a8j) 2026-07-01 09:04:33 +02:00
.pre-commit-config.yaml chore: 🔧 settle terminology on allow-list / deny-list 2026-07-31 11:02:27 +02:00
.sops.yaml feat: provision castra-agent-env SealedSecret (model + forge creds) 2026-06-29 13:10:22 +02:00
AGENTS.md chore: 🔧 settle terminology on allow-list / deny-list 2026-07-31 11:02:27 +02:00
biome.json chore: 🔧 migrate biome config off deprecated recommended field 2026-06-30 10:42:10 +02:00
bun.lock chore: 🔧 add biome + tsgo quality gate via pre-commit 2026-06-30 10:20:47 +02:00
bunli.config.ts feat: ship castra as a standalone binary, bump to 0.3.0 2026-07-31 11:12:09 +02:00
CLAUDE.md docs: 📝 collapse AGENTS.md into a pointer at CLAUDE.md 2026-06-30 22:22:22 +02:00
Containerfile fix: 🐛 resolve persona skills from CASTRA_HOME, not the clone (castra-hw4) 2026-07-31 10:57:00 +02:00
install.sh feat: ship castra as a standalone binary, bump to 0.3.0 2026-07-31 11:12:09 +02:00
LICENSE docs: 📝 rewrite README and declare GPLv3+ license 2026-06-18 13:28:31 +02:00
model-catalog.json feat: restore model catalog via bundled snapshot + dump-catalog 2026-06-18 15:44:58 +02:00
package.json feat: ship castra as a standalone binary, bump to 0.3.0 2026-07-31 11:12:09 +02:00
README.md docs: 📝 publish releases via the Forgejo API, not gh 2026-07-31 11:28:09 +02:00
skills-lock.json chore: 🔧 add caveman, intake, and showboat skills 2026-07-01 07:48:57 +02:00
tsconfig.json feat: reimplement castra as a TS mirror of the lage.bonn bash norm 2026-06-28 22:28:33 +02:00

castra

Minimal forge-agnostic CLI for the castra personas — fetch issues, run a persona once, and push its result as a PR, against any supported forge (Forgejo/Codeberg or GitHub).

A clean-room TypeScript reimplementation of the canonical bash castra (lage/bin/castra, "the norm"): the same forge-agnostic command surface, backed by a native TS ForgeBackend layer.

Built on Bunli and runs on Bun.

⚠️ Pre-1.0 (0.2.0). Commands and flags may still change. The previous spellkave orchestrator lives in attic/ and is no longer built.

Install

castra ships as a standalone binary — no bun, node or npm needed at runtime.

curl -fsSL https://git.b4mad.industries/agentic-forges/castra/raw/branch/main/install.sh | sh

Installs the latest release into ~/.local/bin. Override with CASTRA_VERSION (a release tag), CASTRA_PREFIX (install dir) or CASTRA_REPO. Released targets: linux-x64, linux-arm64, darwin-arm64.

From a checkout, to build and install the binary for this machine:

bun install
bun run install:local     # → ~/.local/bin/castra

Cutting a release

bun run release:build     # → dist/release/castra-<target>.gz + SHA256SUMS

Upload every file in dist/release/ to the release tagged v<version>; install.sh fetches from there and verifies the checksum. This project is Forgejo-only — publish via the Forgejo API, not gh; the script prints the exact calls. Cross-compiling needs the native optional deps for all targets (bun install --os '*' --cpu '*', which the script does for you) — bunli pulls in OpenTUI, which ships a per-platform binary.

Development

bun dev -- <command>     # run from source (bun run src/index.ts)
bun run build            # bundle with bunli → ./dist
bun test                 # run the test suite
bun run typecheck        # tsgo --noEmit

Usage

castra issue view <URL>                       # fetch + project an issue as JSON
castra prepare-run --persona code --issue 42  # isolated git worktree for a run
castra persona-run --persona code --input … --output-dir /tmp/out
castra persona-validate --persona intake --file out/agent-result.json  # pre-mutation verdict gate
castra post-run --persona code <issue-url>    # push branch + open/update PR
castra status start <issue-url> --persona code  # upsert a pipeline-status comment
castra label list --repo owner/repo           # dump a repo's label vocabulary
castra label mutex acquire <issue-url>        # per-issue in-progress mutex (castra-hp1)
castra selftest                               # verify forge connectivity

Commands

Command What it does
issue view <URL> Fetch an issue and project the requested fields (--json number,title,body,state,labels,comments).
prepare-run Create an isolated git worktree (castra/<persona>/<issue>) under .worktrees/.
persona-run Render a persona prompt, spawn its driver, persist the output per outputMode.
post-run Inspect git state, push the feature branch, open/update its PR, run the review-label handoff.
persona-validate Validate a persona result file against the shared verdict predicate (verdictIssues) — the pre-mutation gate a post-script calls instead of hand-rolled jq checks.
init-labels Apply the declarative label set (labels.json) to a repo. Idempotent; JSONL events.
label list Dump a repo's live label vocabulary as JSON — forge-agnostic replacement for gh label list.
label mutex acquire|release Per-issue castra:bot-active in-progress mutex (castra-hp1) — Tekton pipelines acquire it as their first task and release it in finally, so at most one lane runs per issue at a time. Advisory — never fails the pipeline.
selftest Hit a known issue and assert the normalized castra shape.
status Upsert a per-run pipeline-status comment on the triggering issue/PR (start/finish). Advisory — never fails the pipeline.
version Print castra v<base>+<short-sha>.

Persisting output to OTEL (--persist-log)

A global opt-in (any command): tee the command's raw stdout and stderr (interleaved in write order) to the OpenTelemetry collector as an OTLP log record, so a run's full output is queryable alongside its pipeline traces. Capturing stderr matters — commands like persona-run write their progress there. Off by default.

castra --persist-log persona-run --persona code …   # global flag
CASTRA_PERSIST_LOG=1 castra version                  # env equivalent

Enabled by the --persist-log flag or CASTRA_PERSIST_LOG=1 (truthy: 1/true/yes/on); the flag wins, so --persist-log=false overrides a truthy env. When on, castra fails fast if the OTLP endpoint is not configured. Config (standard OTLP env vars):

Env var Purpose
OTEL_EXPORTER_OTLP_ENDPOINT Base collector URL; /v1/logs is appended. Required when persist-log is on. Backends that namespace by tenant need the org in the path — e.g. OpenObserve wants …:5080/api/default so the POST lands at /api/default/v1/logs.
OTEL_EXPORTER_OTLP_LOGS_ENDPOINT Logs-specific URL, used verbatim (overrides the base).
OTEL_EXPORTER_OTLP_HEADERS key=value,key=value export headers (e.g. auth).
OTEL_SERVICE_NAME service.name attribute (default castra).
CASTRA_PERSIST_LOG_FILE PVC mirror path (default out/raw-stdout.txt; off/empty disables).

The export and file mirror are best-effort (a collector hiccup warns to stderr, never fails the command); only the missing-endpoint check is fatal. Run/issue correlation attributes (castra.run_id, castra.issue) come from CASTRA_RUN_ID/ISSUE_NUMBER when present. On-cluster this is wired through the castra-agent-env secret — set CASTRA_PERSIST_LOG: "1" there to enable it for all persona lanes (see manifests/castra/).

Forge backend

Operations are served by a native ForgeBackend (src/forge/), selected from the environment:

  • Auth: FORGE_ACCESS_TOKEN (required). Instance: FORGE_URL (default https://codeberg.org).
  • Driver: FORGE_DRIVER (forgejo-mcp | github), else auto-detected from FORGE_URL (github.comgithub, otherwise forgejo-mcp).
  • forgejo-mcp spawns the version-pinned forgejo-mcp --cli; github talks the REST API over fetch.

Failure auto-filing

When persona-run (stdout-json mode) gets agent output that fails the shared verdict predicate (verdictIssues: not a complete JSON object, or — for personas with a registered schema, e.g. intake — a shape error), it still fails non-zero — but, if CASTRA_FAILURE_REPO (owner/repo) is set, it first opens a forge issue there with the run context and a transcript snippet so the failure is debuggable. Repeat failures dedup onto the same issue as comments rather than spawning duplicates. Left unset, the behaviour is skipped (logged, not fatal). CASTRA_RUN_ID (e.g. the PipelineRun name) is recorded in the issue when present. The failure repo is deliberately separate from the processed repo (REPO_FULL_NAME) — a castra/agent bug is not the customer repo's problem.

Deployment

The CLI ships as a container image and is driven by issue-label-triggered Tekton pipelines on OpenShift, delivered via ArgoCD:

Documentation

Issue tracking uses bd (beads) — run bd prime for workflow context.

License

GPL-3.0-or-later