Imported from spinyfin/mono (
AGENTS.md). Install upstream withnpx skills add spinyfin/mono. Copyright stays with the author.
- always use minimal bazel visibility, never default to public. Maintain bazel visibility health.
- Documentation-only changes (markdown files, design docs, plans, READMEs) should be pushed directly to
maininstead of opening a PR. - HARD RULE: always use Bazel for local builds and tests. Never invoke
cargo build,cargo test,cargo fmt, or any barecargocommand directly. Directcargois slow, uncached, and does not match what CI builds. Bazel is the canonical, cached path — it is what CI runs, and it is what you must run locally. For the engine:bazel test //tools/boss/engine/....cargo fmtis not a harmless exception to this: it shells out tocargo metadata, which creates atarget/directory (withCACHEDIR.TAGand.rustc_info.json) even though it compiles nothing — it is still the forbidden bare-cargoshape. Usecheckleft fixto reformat Rust files, or invoke therustfmtbinary directly. - HARD RULE: run tests only via
bazel test. Never execute a built test binary directly (bazel-bin/...), and never usebazel runon a test target as a substitute. Both bypasstools/test-sandbox/hermetic_test_wrapper.sh, whichbazel testapplies viarun_under. The wrapper redirectsHOMEinto the test's private tmpdir and, on macOS, applies a seatbelt profile that denies writes under/Usersand deniesprocess-execoftmux,claude,codex, andgh. Skip it and a test runs with your realHOMEand no sandbox — it can write into live Boss state under~/Library/Application Support/Bossand reach the real tmux server. A test result obtained outside the sandbox is not evidence: a test that passes unsandboxed can fail underbazel testand vice versa. If a test fails only outside the sandbox, that is not flakiness — do not dismiss it as environmental.--test_filterdoes work forrust_testhere (mono patchesTESTBRIDGE_TEST_ONLYsupport intorules_rust— seethird_party/patches/rules_rust-0.70.0-test-wrapper.patch), and a filter that matches nothing fails loudly rather than reporting an empty green run. It is still usually unnecessary — Bazel's own change detection already scopes a run to what's affected — but narrowing with--test_filteris never a reason to run a binary out ofbazel-bin/. checkleft(the linter) lives in this repo, attools/checkleft/— it is not a standalone external repo. Don't go hunting elsewhere for it; it's published to crates.io and released as prebuilt binaries from here (seetools/checkleft/docs/buildkite-release-setup.md). A private manual playground that consumes those prebuilts (Rust + Bazel, rules_multitool) lives atbrianduff/checkleft-sandbox— seetools/checkleft/docs/checkleft-sandbox.md.- Run
checkleft runwith no flags. Its change detection scopes the run to what you actually touched, which is what makes it fast in a monorepo — no SHA or base-ref plumbing needed. Do not runcheckleft --all. It is reserved for CI's dedicated integrity pipeline, for work that modifies checkleft itself, or for a case with a strong stated justification.--allis not a stricter superset of the default: checks withchanged_lines_onlybecome a no-op under it, so it reports every pre-existing violation in the repo and buries the findings that belong to your change.
No Boss work-item ids in PRs, commits, or source
spinyfin/mono runs boss-ism/pr-text-leakage (changeset scope) and
boss-ism/file-text-leakage (changed-lines file scope) in root
CHECKS.yaml. Both forbid internal Boss work-item id shapes
(T<n> / P<n>) in worker-authored text:
- PR titles and bodies, and commit messages — hard error via
boss-ism/pr-text-leakage(\b[TP]\d+\b). A PR cannot cite the work-item id that spawned it, and the check also trips on incidental text such as a quoted commit title that embeds a short id like(T+ digits +). - Source and docs —
boss-ism/file-text-leakageflags the same shape on changed lines (floored at three digits to avoid ISO-8601 / percentile false positives). Confirmed when a recovered patch whose comments cited a work-item id failed localcheckleft run.
Cite the public PR instead (e.g. mono#2303), never the work-item
id. Do not reference a work-item id anywhere a worker writes —
commit messages, code comments, design docs, or PR body.
If you hit this check, fix the text at the root. Do not add a bypass, exclusion, or allowlist entry for the check. That is the same root-cause rule as the section below.
Operational docs workers should know
- Bazel / Xcode LaunchServices pin on macOS hosts:
tools/boss/docs/mac-toolchain-xcode-pinning.md - Boss forensic surfaces (
engine-audit.log, per-task cost / transcripts):tools/boss/docs/forensic-surfaces.md - Worker liveness contract (what the agent indicator derives from; how the
engine converges when it has lost track of a live worker):
tools/boss/docs/worker-liveness-contract.md - Failure-signal lifecycle (what raises an attention item / the card's
"Failed to start" banner, and what specifically lowers it again):
tools/boss/docs/attention-lifecycle.md - Post-crash orphan recovery:
tools/boss/docs/post-crash-recovery.md - Crash watchdog (why an abort always kills the app, and
BOSS_CRASH_WATCHDOG_SECONDS):tools/boss/docs/crash-watchdog.md - Coordinator session handoff (what the outgoing coordinator session
writes, how the incoming one is briefed, and the three states it can
report):
tools/boss/docs/coordinator-session-handoff.md - Operator runbooks:
tools/boss/docs/runbooks/
Prefer crates over modules for distinct units of functionality (Rust)
We generally keep distinct units in their own crates rather than as modules inside a larger crate: bazel incrementality is per-crate, so smaller crates mean smaller rebuild and retest scopes. When adding or extracting such a unit, it is OK to do light dependency/interface refactoring to support the split — e.g. introduce a small trait or plain context type at the boundary, or move shared types down into a lower-level crate. Keep each crate's dependency list minimal and the edges one-directional: a transport/utility/pipeline crate must never import from the higher-level crate that consumes it; if a cycle threatens, the shared types belong in a lower crate, not in the consumer. "Generally" means use judgment: a tiny glue module doesn't need a crate; a unit with its own vocabulary, tests, and multiple consumers does.
Precedent: the claude_client extraction (PR #1702) pulled the Claude API transport out of engine/core into tools/boss/claude_client, with a one-way engine → claude_client edge.
Hard constraint: fix failing checks at the root cause; never bypass them
When a CI check or repository check (checkleft, file-size, lint, test) is failing, fix the underlying problem. The following are forbidden bypasses — do NOT do any of them:
- Adding a file to a check exclusion or allowlist (
CHECKS.yamlexclude_files, checkleft excludes, lint-disable comments, etc.) to suppress the failure. - Setting
allow_bypass, using an override flag, or invoking any bypass/override mechanism on a check. - Passing
--no-verify/ skipping git hooks; adding broad#[allow(...)]/// swiftlint:disable/# noqaannotations solely to suppress a warning or error. - Deleting,
#[ignore]-ing,xfail-ing, skipping, or weakening assertions in a failing test to make it pass. - Raising a threshold or limit (e.g.
max_linesin a file-size check) solely to accommodate the offending file without reducing its size.
Required behavior: fix the real problem — split the oversized file, fix the lint/compile error, fix the test failure, resolve the root cause. If a check genuinely SHOULD be relaxed (a legitimately needed exclusion or threshold change), that is a human decision — STOP and surface it for operator approval with full justification. Do not decide this autonomously.
Builder pattern convention
Structs with more than 5 fields in boss-protocol (and in boss-engine's internal types) use #[derive(bon::Builder)] with #[builder(on(String, into))]. This prevents additive-change PRs from touching every construction site across the repo.
Rules:
Option<T>fields are automatically optional in the builder (bon defaults them toNone).- Non-optional fields that have a sensible runtime default (e.g.
autostart = true,priority = "medium",last_status_actor = "human") carry#[builder(default = ...)]; use the existingdefault_*()helpers fromtypes.rs. - Fields with no sensible default remain required in the builder — omitting them is a compile error.
- When adding a new optional field to a builder-equipped struct: add
#[builder(default)](or#[builder(default = expr)]) alongside any#[serde(default)]. Existing construction sites need no changes. - When adding a new required field: that is an explicit breaking change — call it out in the PR description. All construction sites must be updated.
- The production DB mapper functions (
map_task,map_product, etc. inwork.rs) continue to use struct literals — they must explicitly set every field from named columns, and a compile error when a new column isn't mapped is desirable. Do not convert DB mappers to builder calls. - When calling
Option<String>setter methods on a builder withon(String, into): pass the inner string value directly (e.g..started_at("2026-01-01")), not wrapped inSome(...). To pass a dynamicOption<&str>orOption<String>, use themaybe_field_name()variant (e.g..maybe_repo_remote_url(repo)).
Structs currently on the builder pattern: Task, WorkExecution, Product, Project (all in boss-protocol/src/types.rs).