Skip to content

Latest commit

 

History

History
160 lines (107 loc) · 38 KB

File metadata and controls

160 lines (107 loc) · 38 KB

CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

What this is

The Paigasus AI SDK (codename Helikon, after Mt Helicon where Pegasus's hoof struck the Hippocrene spring). A Rust SDK for building AI agents. All crates live under the paigasus-helikon-* namespace.

The full architectural reference lives in Notion: "Crate Reference". Linear project: Paigasus Helikon (issues prefixed SMA-).

Common commands

cargo build --workspace                              # all 14 crates
cargo build --workspace --all-features               # facade with every optional crate
cargo run -p paigasus-helikon-cli --bin helikon
cargo run -p paigasus-helikon-cli --bin paigasus-helikon

To reproduce every CI gate locally (matches .github/workflows/ci.yml job-for-job):

cargo fmt --all -- --check
cargo clippy --workspace --all-features --all-targets -- -D warnings
cargo test --workspace --all-features
RUSTDOCFLAGS="-D warnings" cargo doc --workspace --all-features --no-deps
DOC_COVERAGE_THRESHOLD=80 NIGHTLY_CHANNEL=nightly-2026-05-01 \
  bash scripts/check-doc-coverage.sh                 # requires: rustup toolchain install nightly-2026-05-01

The full list lives in CONTRIBUTING.md (single source of truth for contributor policies).

Workspace layout

15 crates under crates/. The facade paigasus-helikon re-exports paigasus-helikon-core unconditionally and the other 12 sibling crates behind Cargo features.

Implementation status (as of 2026-07-06): every crate in the workspace carries a real implementation and publishes to crates.io — the original ten (paigasus-helikon-core, paigasus-helikon, paigasus-helikon-macros, paigasus-helikon-providers-openai, paigasus-helikon-providers-anthropic, paigasus-helikon-providers-bedrock (SMA-329), paigasus-helikon-sessions-sqlite, paigasus-helikon-runtime-tokio (ascended from stub in SMA-346), paigasus-helikon-mcp (SMA-327), and paigasus-helikon-tools (SMA-328)) plus paigasus-helikon-providers-gemini (SMA-449), paigasus-helikon-sessions-postgres/-sessions-redis (SMA-330), paigasus-helikon-runtime-axum (SMA-331), and the last four ascends — paigasus-helikon-runtime-temporal, paigasus-helikon-runtime-agentcore (both SMA-332), paigasus-helikon-evals, and paigasus-helikon-cli (both SMA-333). The workspace was first published to crates.io in SMA-385; zero name-claim stubs remain. paigasus-helikon-cli publishes as a binary crate (0.1.0) — its lib target is internal (missing_docs opted out) and carries no stability guarantee, publishing only so cargo install paigasus-helikon-cli resolves. paigasus-helikon-sessions-testkit is the sole non-published crate: an internal Session conformance test harness kept at 0.0.0 with publish = false by design, not a stub awaiting an ascend.

Workspace inheritance is mandatory: per-crate Cargo.tomls only set name, description, and any crate-specific bits. Everything else (edition, rust-version, authors, license, repository, homepage, keywords, categories) inherits from [workspace.package] in the root Cargo.toml. Don't hardcode these per-crate.

Per-crate version is the one exception, with a two-state lifecycle:

  1. Stub state — version = "0.0.0" + publish = false in Cargo.toml + release = false block in release-plz.toml. Every stub was pre-published once to crates.io at 0.0.0 during SMA-385 to claim the name and satisfy the facade's optional-dep resolver. After that pre-publish, cargo refuses to republish (the per-crate publish = false); release-plz ignores them entirely (the release = false).

  2. Released state — bumped to a real version (≥ 0.1.0) after the first real public-API ticket lands. The 4-step ascend recipe:

    • Bump version = "0.0.0""0.1.0" in the crate's Cargo.toml.
    • Remove publish = false from that Cargo.toml.
    • Remove the crate's [[package]] … release = false block from release-plz.toml.
    • Land as one chore(release): SMA-### lift stage-1 gates for <crate> commit on the feature branch alongside the implementation. release-plz handles the first crates.io publish on CI.

    The 4-step recipe applies to stubs ascending from 0.0.0. The ten already-released crates (-core, facade, -macros, -providers-openai, -providers-anthropic, -providers-bedrock, -sessions-sqlite, -runtime-tokio, -mcp, -tools) ship through release-plz's normal flow — no manual ritual needed for their future bumps. The historical chain of chore(release): … escape release-plz 0.0.0 trap … commits in the git log (SMA-317/347/350/372/382) is pre-Stage-1 archaeology and won't recur.

    Caveat — when the ascending crate uses paigasus-helikon-core API added in the same PR, bump core too (a 5th step). cargo publish --verify builds the ascending crate's tarball against the registry core (the path is stripped at publish), so if crates.io core lacks the new API the publish fails with failed to verify package tarball, and release-plz's combined job (SMA-351) aborts before its release-PR step — a deadlock, since core never gets its auto bump (a squashed feat(<ascending-crate>) commit attributes nothing to core). Fix: in the same PR, also bump paigasus-helikon-core (patch for additive/non-breaking-behind-#[non_exhaustive], e.g. 0.2.00.2.1) and its [workspace.dependencies] pin + CHANGELOG. release-plz then publishes core first, then the ascending crate verifies against the fresh core (dependency-ordered publish). Diagnosed in SMA-321: PR #45's release failed against the stale core 0.2.0; PR #46 (chore(release) bumping core to 0.2.1) cleared it.

    Second-order caveat — the manual core bump silently defeats dependencies_update, so the facade drifts. release-plz.toml sets dependencies_update = true, which is supposed to cascade: when a sibling's version changes, release-plz bumps the facade's [workspace.dependencies] pin and gives the facade a patch bump. But that cascade only runs when release-plz itself performs the sibling bump. The same-PR manual bump above means the sibling version is already at target when the PR merges, so release-plz just publishes it and never runs the dependent-bump step — the facade (paigasus-helikon) is left untouched and stops tracking. Consequence: the facade stays at its old version with stale published dep reqs (e.g. after SMA-346, facade 0.2.0 still requested paigasus-helikon-runtime-tokio = ^0.0.0, so the new runner-boundary surface was unreachable through the facade's runtime-tokio feature). Fix: in any PR that uses the same-PR manual bump, ALSO bump the facade (patch: version in crates/paigasus-helikon/Cargo.toml + its [workspace.dependencies] self-pin + CHANGELOG) so it republishes with current sibling reqs. Diagnosed after SMA-346: PRs #48/#49 shipped core 0.2.3 + runtime-tokio 0.1.1 but left facade 0.2.0; PR #50 (chore(release) bumping facade to 0.2.1) cleared it. NB: feature branches must match the branch-names ruleset (feature/** or hotfix/**); a chore/** branch is rejected at push with GH013 … creations being restricted.

Released vs stub crates (exact versions move every release — read each crate's Cargo.toml for current numbers, don't trust hardcoded values here): every crate publishes normally now. The last stubs ascended via the 4-step recipe below: -runtime-axum (SMA-331), -runtime-temporal and -runtime-agentcore (SMA-332), and -evals alongside paigasus-helikon-cli (SMA-333). paigasus-helikon-cli publishes as a binary crate — its lib target is internal and carries no stability guarantee, existing only so cargo install resolves the version; it is not "never published as a library." The sole exception left in the workspace is paigasus-helikon-sessions-testkit, which stays at 0.0.0 with publish = false + a release = false block in release-plz.toml — an intentional internal test harness, not a stub awaiting an ascend.

Third-party version pins live in [workspace.dependencies] (root). Members reference them via dep.workspace = true. Internal crate paths are also in [workspace.dependencies] so the facade can use workspace = true consistently.

Non-obvious patterns to preserve

  • Feature naming: kebab-case in [features] (runtime-tokio), snake-case in pub use aliases (runtime_tokio). They must stay paired across the facade's Cargo.toml and src/lib.rs.
  • paigasus-helikon-cli uses autobins = false because the paigasus-helikon (hyphen) binary maps to src/bin/paigasus_helikon.rs (underscore — hyphens are illegal in Rust filenames). Removing autobins = false reintroduces a phantom paigasus_helikon binary that conflicts with the explicit [[bin]] entry.
  • paigasus-helikon-macros is a proc-macro crate from day one ([lib] proc-macro = true). Don't convert it to a regular lib even though it currently has no macros.
  • The paigasus-helikon facade lib shares its name with the paigasus-helikon CLI binary by design (Notion ref's "fully-qualified shim alias"). This produces a non-fatal cargo doc filename-collision warning. Don't "fix" it by renaming either — both names are user-facing API. The accepted future fix is doc = false on the CLI binary entry.
  • License is dual Apache-2.0 OR MIT (decided 2026-05-20, reversing the 2026-05-16 MIT-only call). Both LICENSE-APACHE and LICENSE-MIT live at the repo root; the workspace metadata is license = "Apache-2.0 OR MIT". Per Rust ecosystem convention — no Apache-only or MIT-only crates in the workspace. Contributions are accepted under the same dual license by default (the standard inbound-equals-outbound clause is restated in README.md).
  • MSRV is 1.94 (workspace-package level; raised from 1.85 in SMA-329 because sqlx 0.9.0 declares rust-version = "1.94.0" — the pre-existing highest floor in the workspace). If a dep raises MSRV, bump rust-version to what cargo demands rather than downgrading the dep.
  • Workspace-wide missing_docs enforcement lives in root Cargo.toml ([workspace.lints.rust] missing_docs = "warn"). Each non-CLI crate opts in with [lints] workspace = true. The CLI overrides locally with [lints.rust] missing_docs = "allow" and does not include workspace = true — cargo treats [lints] workspace = true and an inline [lints.<tool>] table as mutually exclusive. When adding a new crate, copy the opt-in block. When adding a new pub use re-export to the facade, give it a /// doc comment or -D warnings will fail the docs job.
  • cargo msrv has no --workspace flag. The msrv workflow verifies one representative inheriting crate: cargo msrv --path crates/paigasus-helikon-core verify. Because every member uses rust-version.workspace = true, success on one is success on all. Don't "fix" the workflow by adding --workspace; that's what the first SMA-305 CI run died on.
  • Nightly is date-pinned (NIGHTLY_TOOLCHAIN: nightly-2026-05-01 at the workflow env: level in ci.yml, threaded into the doc-coverage script as NIGHTLY_CHANNEL). The rustdoc JSON coverage format is -Z unstable-options and can shift between nightlies; floating nightly would be a CI footgun. Bumping is a one-line follow-up chore, not an emergency.
  • Bootstrap commits on release infrastructure must use chore(...) or docs(...) types, never feat/fix. release-plz parses every commit since the last per-crate tag — a feat(workspace): ... commit that touches every Cargo.toml would attribute a bump to every crate. The SMA-307 bootstrap PR followed this rule; the same rule applies to any future release-plz.toml or release-plz.yml edits.

Workflow conventions

  • Branch per Linear issue: feature/<sma-####>-<kebab-title>. The branch name is pre-computed in each Linear ticket's gitBranchName field.
  • Design artifacts per ticket (docs/superpowers/specs/YYYY-MM-DD-<topic>-design.md, docs/superpowers/plans/YYYY-MM-DD-<topic>.md) land on the feature branch alongside the implementation — not pre-merged to main.
  • Keep the public mdBook (docs/book/) current — update it in the same PR as any user-facing change. Before opening a PR, check whether the work changes public API, the quickstart/example flow, the crate roster (incl. a stub crate ascending to published), or a documented concept; if so, bring the affected docs/book/src/*.md page(s) into line on the same branch. The book is published from main and drifts silently otherwise — it sat as the untouched SMA-311 scaffold (13/17 pages still > **Stub.**) through all of Stage 1 before anyone noticed; SMA-423 is the one-time catch-up, and this rule keeps the backlog from rebuilding. A pure-internal change (refactor, CI, deps, release plumbing) needs no book edit — but make that a conscious call, not a silent skip. mdbook build docs/book must stay clean ([output.linkcheck] warning-policy = "error").
  • Keep crate README.md files current — update the affected crate's README in the same PR as any change to its public surface. Each of the ten published crates' README.md is its crates.io (and docs.rs landing-sidebar) page — no crate sets an explicit readme, so Cargo uses the default README.md. Before opening a PR, for every crate the work touches check whether the change affects that crate's public API / usage example, its install or feature story, or its published status (a stub ascending to published, a renamed/added feature flag); if so, bring its crates/<crate>/README.md into line on the same branch — and also the facade crates/paigasus-helikon/README.md and the root README.md whenever the crate roster or the feature → module map changes. README install snippets deliberately use drift-free cargo add (no hardcoded versions), so a routine version bump alone needs no README edit. Like the mdBook, the READMEs drift silently otherwise — they sat as untouched 3-line SMA-304 stubs (Stub — see SMA-304) through all of Stage 1 while ten crates shipped real implementations; SMA-424 is the one-time catch-up, and this rule keeps the backlog from rebuilding. A pure-internal change (refactor, CI, deps, release plumbing) needs no README edit — but make that a conscious call, not a silent skip.
  • Commit prefix: <type>(<scope>): SMA-### <message> (e.g. feat(facade): SMA-304 ...).
  • PR titles must satisfy two independent rules from pr-title.yml (amannn/action-semantic-pull-request):
    1. Full Conventional Commits format. The action enforces a valid type(scope): prefix from the action's configured types list — independent of the subject regex. SMA-317 add anthropic provider (no prefix) fails; feat(providers-anthropic): SMA-317 add anthropic provider passes.
    2. Subject must start lowercase after the SMA-### prefix. The subjectPattern: ^([A-Z]{2,4}-\d+ )?[^A-Z].+$ rejects feat(core): SMA-314 LlmAgent + ... because L is uppercase; lead the subject with a lowercase verb (add, wire, pin, promote, implement, fix). Per-commit Conventional Commit titles on the feature branch don't trip either rule — only the PR title (which becomes the squashed main commit) is gated.
  • Linear auto-closes the linked SMA-* issue when its PR merges; no manual status move needed.
  • Always implement GitHub Actions against the latest stable major. Before adding or updating any uses: line in .github/workflows/, resolve the latest release of the action and pin to its commit SHA (never a moving @vN tag). Use:
    gh api repos/<owner>/<repo>/releases/latest | jq -r '.tag_name'
    gh api repos/<owner>/<repo>/git/ref/tags/<tag> | jq -r '.object.sha'
    # if .object.type == "tag" (annotated), dereference:
    # gh api repos/<owner>/<repo>/git/tags/<sha> | jq -r '.object.sha'
    Do not use a plan-time version pin if a newer major has shipped between plan-writing and implementation — bump immediately, then let Dependabot's github-actions group track patch/minor updates from there. The above-the-fold human-readable version stays as a # action vX.Y.Z comment so the SHA is auditable. Note the corollary: because that group is configured for patch + minor only, a major bump never arrives on its own and must be swept by hand — which is how actions/checkout sat on v6.0.2 until SMA-486.
  • dtolnay/rust-toolchain needs its bump direction checked, not assumed. It does not ship conventional releases — it publishes rolling branches (stable, nightly, master, 1.01.14) and a v1 tag that is re-pointed at intervals. As of 2026-08-18 releases/latest, the v1 tag, master's head, and this repo's pin are all 6c977a6ca4077a0ceb28ffbe03f59d46e9ac8772, so a Dependabot bump here is legitimate and was accepted in #201. That has not always been true: through SMA-486 v1 sat abandoned on a 2025-08-23 commit 11 commits behind the pin, and running the "bump to the latest release" recipe would have proposed an ~11-month downgrade. So do not treat this action as blanket-exempt or as blanket-safe — before accepting any bump, confirm the direction:
    gh api repos/dtolnay/rust-toolchain/compare/<current-pin>...<proposed-sha> \
      --jq '{status, ahead_by, behind_by}'   # must be ahead_by > 0, behind_by == 0
    Independently of all that, one rule is permanent: every site pins a SHA and must pair it with an explicit with: toolchain: …. The ref name is the toolchain selector, so a SHA ref carries no toolchain — the input is mandatory, not stylistic. The workflow comments point here.
  • After a PR merges to main, release-plz opens/updates a chore: release PR (authored by the paigasusbot App) carrying the version bumps + CHANGELOG; in the normal flow merging that PR is what publishes to crates.io (the merged feature PR left versions matching the registry, so its own main push publishes nothing). The exception is a PR that bumps its own version — the stub-ascend ritual — which publishes on its own merge with no separate release PR. Check the release PR after every merge and watch its CI — its release-PR cargo update can pull a fresh advisory that reddens audit/deny on the bot PR only (independent of main); fix with a chore(deps) pin and release-plz regenerates the PR clean.

CI

.github/workflows/ci.yml runs eight jobs on every PR (the commits job is PR-only; the other seven also run on push to main): fmt, clippy, test (matrix: {ubuntu, macos, windows} × {stable, 1.94}, fail-fast: false), build-no-default-features (SMA-452: cargo build --no-default-features for both paigasus-helikon-runtime-axum and paigasus-helikon-runtime-actix, catching openapi-feature-gating regressions, plus a cargo tree assertion that axum does not leak into the runtime-actix feature graph), docs (with RUSTDOCFLAGS=-D warnings), doc-coverage (nightly rustdoc --show-coverage, aggregated by scripts/check-doc-coverage.sh, gated at DOC_COVERAGE_THRESHOLD — default 80%), commits (SMA-335: convco check against the PR's commit range, gated by if: github.event_name == 'pull_request'), and sessions-it (SMA-330: Postgres/Redis session integration suite, path-filtered, on both PR and push to main). The paigasus-helikon-cli crate is excluded from both the missing_docs lint and the coverage aggregator until its public API stabilizes.

.github/workflows/integration.yml (SMA-457) runs two signal-only jobs — temporal-it and agentcore-image — on PR, push to main, a nightly cron at 05:00 UTC, and workflow_dispatch. Deliberately not in ci.yml: temporal-it is expected to flake while it earns promotion (its crash-resume test aborts a real worker against wall-clock activity timeouts), and a failing job makes its whole workflow run conclude failure whether or not it is required — so keeping these out of ci.yml is what stops "ci is red on main" from becoming meaningless. Neither job appears in main-protection-checks.json; "signal-only" means not listed as required, never continue-on-error — that would report green unconditionally and remove the signal rather than weaken it.

Both use step-level if: guards, not a job-level one, for the same reason sessions-it does: a skipped job reports no status at all, which blocks every PR the moment the context is promoted to required. dorny/paths-filter needs a diff base that schedule and workflow_dispatch do not provide, so the filter step is itself event-guarded and a decide step collapses "filter matched" and "manually or nightly triggered" into one output. agentcore-image maps schedule to false. Measured cold on the first CI run, both images build and all four gates run in ~4 minutes, so this is not a cost decision — re-measuring two numbers that sit far under their budgets every night simply adds no information. workflow_dispatch still reaches the job.

temporal-it installs a checksum-pinned temporalio/cli tarball (version and SHA-256 as literals in the workflow — a checksum fetched from the same host as the artifact would prove only that the download was not corrupted, never its identity; hand-bumped, Dependabot does not track it), runs temporal server start-dev --headless, and probes readiness with temporal operator namespace describe default rather than operator cluster health — the cluster reports healthy before the default namespace the suite connects to finishes registering, and a namespace-not-found in the first test would look like a real regression. It sets HELIKON_REQUIRE_TEMPORAL=1, which turns gate() in temporal_live.rs from a loud skip into a panic. That is load-bearing, not belt-and-braces: a skipped test passes, and cargo test captures a passing test's output, so without it a job that never reached a server is indistinguishable from a green one (the same reasoning as HELIKON_REQUIRE_SANDBOX). It deliberately does not retry — unlike sessions-it, which is required and retries three times so a flake cannot block a merge. The whole point of the signal-only phase is to measure the flake rate, and a retry loop erases exactly that evidence; the per-run record goes to the job summary. Promotion bar: ≥ 20 executed runs with ≤ 1 flake, or 30 consecutive green nightlies — at which point temporal-it is added to both main-protection-checks.json and CONTRIBUTING.md's required-contexts table, and the retry decision is revisited.

agentcore-image runs on ubuntu-24.04-arm (free for public repos) because the Dockerfile hardcodes --platform linux/arm64 — AgentCore's runtime targets are arm64 microVMs, and qemu-emulating a musl build of aws-lc-rs would take an hour-plus per image. It runs scripts/agentcore-image-check.sh with AGENTCORE_COLD_START_LIMIT_MS=250, because the 50 ms AC was measured on a quiet developer machine and a shared runner is a different measuring instrument; the script prints a loud NOTE: … this is NOT the AC value whenever the effective gate differs from the default. The 30 MB size gate is deliberately not overridable — it carries the STOP RULE, and an env knob on it would be precisely the quiet relaxation that rule exists to prevent. The Dockerfile's builder RUN uses BuildKit cache mounts so the second image reuses the first's compiled dependencies (~40–50% off the second build; no help to the first, and no persistence across runs — every job gets a fresh runner). The cp out of target/ must stay inside that same RUN: a cache mount is not part of the image filesystem, so splitting it would silently produce an image with no binary in it.

protoc comes from .github/actions/setup-protoc, a repo-local composite action, not from a third-party one (SMA-458). It installs protoc 35.1, pinned exactly and verified against a per-platform SHA-256 before extraction, at all nine sites that compile the workspace (ci.yml ×6, msrv.yml, release-plz.yml, integration.yml). It replaced arduino/setup-protoc, whose version input defaults to 23.x, not to latest — the action's README claims otherwise and is wrong, and CI had therefore been running 23.4 since SMA-332. SMA-458 was consequently a deliberate 12-major upgrade as well as a pin, not the no-op its one-line ticket framing implied. install.sh does download → verify → extract → export; that order is load-bearing, so an unverified archive never reaches an executable location. It exports PROTOC and PROTOC_INCLUDE via $GITHUB_ENV as well as prepending to $GITHUB_PATH, because prost-build resolves PROTOC before falling back to a PATH lookup — that makes the install authoritative regardless of PATH ordering, and moots the well-known-type include/ tree having to sit beside the binary. verify.sh must stay its own step: $GITHUB_PATH/$GITHUB_ENV writes do not affect the step that makes them, so an assertion folded back into install.sh would validate a local export PATH= rather than the mechanism cargo sees, and would be structurally blind to the propagation failure it exists to catch. Only Linux-X64, macOS-ARM64 and Windows-X64 are supported; anything else exits non-zero naming the file to edit. linux-aarch_64 is deliberately absent even though agentcore-image runs on ubuntu-24.04-arm (it has no protoc step) — an unexercised digest is an unexercised code path, and a wrong one reads as tampering rather than as a typo.

Nothing tracks the protoc pin — bumping it is a human act with no prompt. Dependabot follows action SHAs, and after SMA-458 there is no third-party action here for it to follow at all. It sits alongside the repo's other hand-bumped pins: TEMPORAL_CLI_VERSION/TEMPORAL_CLI_SHA256 in integration.yml and NIGHTLY_TOOLCHAIN in ci.yml. Bump runbook: edit PROTOC_VERSION and all three digests in .github/actions/setup-protoc/install.sh and EXPECTED_VERSION in verify.sh, then run bash .github/actions/setup-protoc/selftest.sh — it re-downloads every published asset and fails if any pinned digest disagrees, and also exercises the tampered-digest and unsupported-platform paths. actionlint lints .github/workflows/*.yml and not action.yml, so shellcheck on the three scripts is the only lint coverage the install logic has. A checksum mismatch is not a signal to update the digest — the causes are, in order, a truncated download, an upstream re-tag, and tampering; verify upstream independently first. The accepted cost of pinning is that it does not self-heal: if protobuf removes or replaces the v35.1 assets, every required job and release-plz go red until someone bumps the pin.

.github/workflows/pr-title.yml (SMA-335) runs amannn/action-semantic-pull-request on pull_request_target to gate the PR title — the squashed commit on main. Permissions are minimal (pull-requests: read, statuses: write); no actions/checkout step under pull_request_target keeps PR-controlled code out of the runner. Concurrency keys on github.event.pull_request.number because pull_request_target sets github.ref to the base ref and keying on it would cross-cancel different PRs. Dependabot PRs are exempt from the title check via ignoreLabels: [area:deps] — their auto-generated Bump … titles capitalize the subject and can't be reconfigured, so they'd otherwise block every dependency PR; the ignore label makes the check pass for them (not skip-and-block, which would leave the required context unreported and still block). Don't remove it.

.github/workflows/msrv.yml runs cargo msrv --path crates/paigasus-helikon-core verify as a non-required signal that the declared MSRV is truthful.

The required-status-check contexts gated on main are (bare job names, as posted by the GitHub Actions app): fmt, clippy, test (ubuntu-latest, stable), test (macos-latest, stable), docs, doc-coverage, book-build, commits, pr-title, audit, deny, sessions-it, build-no-default-features. The macOS job is required because it is the only gate that compiles and runs the Seatbelt backend; sessions-it because it is the only gate that runs the live Postgres/Redis session backends; build-no-default-features because it is the only gate that compiles runtime-axum and runtime-actix with default features off and asserts axum does not leak into the runtime-actix feature graph. The canonical declaration is .github/rulesets/main-protection-checks.json (see CONTRIBUTING.md → "Repo configuration"). Other matrix variants (test (windows-latest, …), test (…, 1.94)) run as signals only. Concurrency cancels in-flight PR runs but lets main pushes complete. ci.yml declares contents: read plus pull-requests: read — the latter for sessions-it's dorny/paths-filter, which calls pulls.listFiles on PR events (SMA-487); the workflow comment carries the rationale and the reason it must not be minimised away.

Supply-chain workflows (.github/workflows/audit.yml, deny.yml, sbom.yml) are separate from ci.yml because they have independent triggers and failure semantics. Required status checks added in SMA-306: audit, deny (declared in .github/rulesets/main-protection-checks.json alongside the CI gates). Both audit.yml and deny.yml run on push to main, PRs, a daily cron, and workflow_dispatch — the cron and the manual trigger were aligned in SMA-479 so that main is re-evaluated daily at exactly PR severity.

The two jobs in audit.yml have deliberately different roles, and only one of them is a verdict. The audit job runs cargo audit --deny warnings on every event — it is the same job definition that gates PRs, un-gated in SMA-479 precisely so the daily and PR severities cannot drift apart. Do not re-add an event filter to it, and do not copy its command into a second step somewhere: one command, in one place, is the whole point. The scheduled-audit job runs rustsec/audit-check for its auto-issue-filing behaviour (the only place in the repo where a wrapper action is preferred over direct tool invocation) — and its green status means nothing at any severity. The action routes schedule events to a reportIssues() code path that files issues and returns without ever failing, including for critical vulnerabilities; it also files nothing at all for yanked crates, and never re-files an advisory whose issue has been closed. Read the run conclusion, never scheduled-audit's job status. Correspondingly, do not widen its if: to include workflow_dispatch — a non-schedule event routes the action to reportCheck(), which needs checks: write that the job does not grant, and 403s.

Both workflows key their concurrency group on github.event_name as well as github.ref. This is load-bearing, not decoration: schedule, workflow_dispatch, and push to main all resolve github.ref to refs/heads/main, so a shared group with cancel-in-progress: false lets a queued cron run sit pending until the next merge cancels it — silently discarding the day's only strict evaluation of main. Do not simplify the key back.

Reading the daily signal: green means the strict audit job passed; red means some job in the workflow failed and needs triage; absent or cancelled means unverified. Red is not a synonym for "advisory present" — the run also goes red when the advisory-DB or crates.io fetch hits a network failure, and when scheduled-audit itself fails (e.g. the GitHub API rejecting an issue write) even though audit passed. So check which job failed before concluding anything, then reproduce with cargo audit --deny warnings on a clean checkout. Scheduled runs are also best-effort — GitHub can delay or drop them under load and disables them entirely after 60 days of repository inactivity — so a missing row is not a passing row, and no upper bound on staleness can actually be guaranteed.

To read the verdict against a commit, use the Checks API, not the legacy commit-status API: gh api repos/SMK1085/paigasus-helikon/commits/main/check-runs --jq '.check_runs[] | select(.name=="audit" or .name=="deny") | {name, status, conclusion}'. GitHub Actions publishes check runs; /commits/{ref}/status returns only legacy statuses and on this repo reports just CodeRabbit — so it renders a confident state: success that contains no audit verdict whatsoever. Reading it would reproduce exactly the bug this section exists to prevent. The deny job additionally runs scripts/check-advisory-ignore-sync.sh, which asserts that the [advisories].ignore lists in .cargo/audit.toml and deny.toml have not drifted apart — they are policy-mirrored, and both are now evaluated daily against the same database.

The SBOM workflow invokes cargo cyclonedx --manifest-path crates/paigasus-helikon/Cargo.toml --format json --spec-version 1.5 --all-features. cargo-cyclonedx 0.5.x has no -p flag (must target via --manifest-path) and defaults to --spec-version 1.3, so 1.5 is pinned explicitly. With --all-features the facade's dep graph equals the workspace's dep graph, so one SBOM covers everything. The workflow's find crates/paigasus-helikon -maxdepth 1 -name '*.cdx.json' picks the facade's SBOM specifically — cargo-cyclonedx 0.5 walks the workspace and emits one SBOM under each member directory regardless of which member you point at, so scoping the find pattern matters.

deny.toml declares version = 2 under both [advisories] and [licenses] — v1 fields (vulnerability, unmaintained, unsound, copyleft, etc.) are removed in modern cargo-deny and adding them will fail with a schema error. The license allowlist includes Unicode-3.0 in addition to the ticket-prescribed Unicode-DFS-2016 because unicode-ident ≥ 1.0.13 (pulled transitively by serde_derive) relicensed in 2024. cargo-deny's advisory DB lives at ~/.cargo/advisory-dbs (plural) per deny.toml's db-path; cargo-audit's DB is at ~/.cargo/advisory-db (singular) — each tool caches its own, and the CI cache directories are scoped per-workflow.

Dependabot is configured for cargo + github-actions ecosystems, weekly Monday 06:00 UTC (aligned with the daily audit cron), with patch + minor updates grouped into one PR per ecosystem.

The microvm/forkd live-KVM path is not validated locally or in GitHub CI — the dev host is arm64 macOS (no /dev/kvm) and GitHub runners have none. Validate it on a GCP nested-virtualization VM (Ubuntu 24.04 — the forkd binaries need glibc ≥ 2.38; Intel n2) per docs/runbooks/forkd-live-validation.md. The tests/forkd_live.rs tests are env-gated (FORKD_URL / FORKD_TOKEN / FORKD_SNAPSHOT) and loud-skip when no controller is configured, so cargo test stays green without one.

Local hooks

Hooks are managed via cargo-husky (user-hooks mode) and live in .cargo-husky/hooks/. They're installed into .git/hooks/ on the next dev-dep realization of paigasus-helikon (e.g. cargo test -p paigasus-helikon --no-run). To force re-install after editing a hook: rm -rf target/debug/build/cargo-husky-* && cargo test -p paigasus-helikon --no-run.

That re-install silently does nothing inside a git worktree. cargo-husky walks up from its OUT_DIR looking for a .git directory; in a worktree .git is a file pointing at …/.git/worktrees/<name>, so the search fails and the build script exits having installed nothing. The only trace is a Warning: .git directory was not found in … line in target/debug/build/cargo-husky-*/stderr, which cargo does not surface — the build looks clean and the old hook keeps running. Worktrees share the main checkout's .git/hooks/ (git rev-parse --git-common-dir), so after editing a hook from a worktree, run the re-install from the main checkout. Verify with grep against the installed copy rather than assuming — a stale pre-push survived an entire branch's worth of pushes in SMA-547 this way.

Do not hand-copy a hook onto .git/hooks/<name>. On this machine the Entire CLI owns .git/hooks/pre-push: its wrapper pushes session logs, then chains to the previous hook at .git/hooks/pre-push.pre-entire, which is where the cargo-husky hook actually lives. Overwriting pre-push silently destroys the Entire integration while still appearing to work, because the cargo-husky body runs either way. Check for a .pre-entire sibling first and install into that slot, preserving cargo-husky's three-line # This hook was set by cargo-husky banner. Any hook manager that chains this way (Entire, lefthook, pre-commit) has the same shape.

  • commit-msg — runs convco check --from-stdin (enforces the .versionrc allowlist).
  • pre-commit — intentional no-op (exit 0). The file exists to claim the slot so future cargo-husky upgrades don't fill it in with surprise behavior.
  • pre-push — runs cargo fmt --all -- --check, cargo clippy --workspace --all-features --all-targets -- -D warnings, and convco check <merge-base>..HEAD. Catches the three fastest CI gates pre-push; deliberately omits cargo test and cargo doc (too slow for every push). Bypass for WIP branches: git push --no-verify. The convco baseline must be a merge-base, not a branch tip (fixed in SMA-547). convco check A..B only walks the commits git would list when A is an ancestor of B; given a diverged A it silently falls back to the entire history instead — here 220 commits back to Initial commit, three of which predate the .versionrc scope allowlist and can never pass. The hook previously fed origin/main's tip, which is diverged for any branch whose first push happens after main moved ahead of its branch point, so it rejected correct branches with failures the author did not write. git merge-base is an ancestor by construction; don't "simplify" it back to the tip.

Fixture line endings

.gitattributes pins crates/paigasus-helikon-providers-anthropic/tests/fixtures/*.txt to text eol=lf. The streaming tests include_str! the SSE fixtures and split them on literal \n delimiters; without this, Windows checkouts produce CRLF bytes and the literal-string splits return one part instead of two. When adding wire-format fixtures elsewhere that the test code parses byte-level, extend the rule.

Cargo.lock

Committed (workspace contains a binary).