cyberine
UsageRun recipes

Project-level setup — cyberine init

cyberine init is the per-repo counterpart to cyberine setup. It detects which providers/CLIs are usable on the current machine, suggests a default fleet and mode, and writes <cwd>/.cyberine/config.json (project layer, safe to commit):

cyberine init                 # detect -> suggest fleet/mode -> write config -> pin a fleet.json from the `detected` preset
cyberine init --update        # force update even when config already exists (skip the "update?" prompt)
cyberine init --yes, -y       # non-interactive: accept all suggestions without prompting
cyberine init --fleet detected  # the default preset, named explicitly
cyberine init --seats-only    # pin fleet.json to one claude-<seat> entry per declared seat
cyberine init --fleet full    # pin fleet.json to every built-in provider (availability-gated)
cyberine init --no-fleet      # skip writing fleet.json
cyberine init --rules         # write .claude/rules/cyberine-*.md and record rules.applied in config.local.json (machine-local)
cyberine init --rules --track # the team-shared shape: rules.applied in config.json, guard in settings.json, nothing ignored
cyberine init --no-rules      # do not apply the Cyberine rules; print the hint instead
cyberine init --rules --harness claude,agents   # choose the instruction files (default: claude, plus agents when AGENTS.md exists)

Rules per harness (kbqfx). The rule text has one source, templates/rules/, rendered per harness the repo uses:

HarnessReadsWhat init --rules writes thereBoard-write guard
Claude Code.claude/rules/*.mdcyberine-autonomy.md, cyberine-board.md (unchanged behaviour)yes, the cyberine tasks guard PreToolUse hook
Codex, Muse, Cyberine loop workersAGENTS.md (loop workers prefer it over CLAUDE.md)one marker-delimited block per rule (<!-- cyberine:begin <rule> --> ... <!-- cyberine:end <rule> -->), headings demoted one level, appended after the file's own contentnone: those CLIs have no hook, so the rule text is all they get

agents is on by default only when AGENTS.md already exists; --harness overrides detection (claude, agents, or both). AGENTS.md is normally tracked, so init ASKS before writing each rule block, one rule at a time, and only when the write would change the file; without a terminal it writes nothing there and says so. A re-run on a later day is byte-identical (the block keeps the date it was first written on); editing a template and re-running replaces only that block, never the rest of the file. cyberine doctor's Repo readiness section reports each harness: the Claude Code rule files and whether the guard is installed, and whether AGENTS.md carries the blocks.

Known limitations: GEMINI.md and .cursor/rules/ are not rendered yet (their instruction-file conventions are not confirmed from their docs); there is no guard for any harness except Claude Code; --track does not change the AGENTS.md path, since a block in a tracked file is shared the moment it is committed.

Re-running when a config already exists shows the current values and asks whether to update (never clobbers silently). The config written is the build.fleet + build.route + mode keys in the project layer.

After config.json, cyberine init also pins <cwd>/.cyberine/fleet.json (unless it already exists, in which case it is left untouched). Since kvywt (2026-09-18) the default preset is detected, and it is the default for every shape of the command — TTY, --yes, piped alike. It enables exactly the workers detection found usable on this machine plus one claude-<seat> lane per seat declared in the global $CYBERINE_HOME/fleet.json, and writes every other built-in provider enabled: false. There is no longer a seats-only-versus-full prompt: init wires what it detects, and --seats-only / --fleet full remain as flags for the two cases detection deliberately cannot decide — a repo that wants subscription lanes only even where other credentials exist, and a machine whose credentials arrive later (CI, a shared image) where every provider should be written out ready to be flipped on by hand.

Since k5jzv (2026-09-18) detection by credential presence alone never overrides an operator's explicit disable: for every provider the detected preset would otherwise enable, it also checks that provider's entry in the GLOBAL $CYBERINE_HOME/fleet.json, matched by workerId (and, for a seat lane, by workerId + seat) rather than by the provider's key name, so a renamed global entry still matches. When that global entry exists and is enabled: false, or still carries a k2g29 autoDisabled block (set when a worker was auto-turned-off for an out-of-credits or auth-failure refusal), the project fleet.json keeps it enabled: false too — deepseek being funded on this machine does not re-enable it in a fresh project when the operator turned it off globally because the account is out of credit. A provider absent from the global file entirely still follows plain detection, unaffected. --fleet full is unchanged by this: it still writes every built-in provider availability-gated only, with no global-disable check.

The run prints what it enabled and why, one line per worker, then says plainly what enabling means, followed by one line per provider held off this way, naming the reason:

wrote /path/to/repo/.cyberine/fleet.json (preset: detected)

Enabled 4 worker(s) -- this is exactly what was detected:
  commandcode       env var                     @ env CMD_API_KEY
  commandcode-free  env var                     @ env CMD_API_KEY
  codex             file OAuth (ChatGPT login)  @ ~/.codex/auth.json
  claude-kt         seat token file             @ ~/.cyberine/seats/kt.txt mode 600

Enabled means Cyberine MAY USE these workers for every role -- to plan, to judge and to implement.
Turn one off with cyberine fleet disable <provider>; see them with cyberine fleet list.

Held off 2 detected worker(s) because the GLOBAL fleet.json disables them:
  deepseek      disabled in the global fleet.json
  xai           auto-disabled (out-of-credits): 402 payment required
A global OFF is a hard OFF: neither detection nor cyberine fleet enable <provider> in this project can re-enable one. Opt back in where it was turned off, with cyberine fleet enable <provider> run from a directory that has no .cyberine/fleet.json.

The "Held off" block only appears when at least one provider detection would have enabled is kept off by the global file, and is silent otherwise. The left column is the fleet.json provider name, which is exactly what cyberine fleet disable / cyberine fleet enable <provider> take. There is no billing acknowledgement prompt — the billing class per worker lives in cyberine fleet list and cyberine setup --status, not behind a gate on the way in.

A global OFF wins at dispatch, not only at init — fleet.json layer precedence (ktc2v)

A project .cyberine/fleet.json REPLACES the global $CYBERINE_HOME/fleet.json wholesale (only seats and routing.seats are carried down, because a project file may not declare either). The k5jzv check above therefore only covered the moment cyberine init wrote the project file: a cyberine fleet disable <provider> run globally AFTERWARDS never reached a repo initialised earlier, and that repo kept dispatching the provider from its own stale snapshot. Since ktc2v (2026-09-18) precedence is enforced at load time instead, so init and every dispatch agree:

  • Global OFF is a hard OFF. A global entry that is enabled: false, or that still carries an autoDisabled block, turns the matching project entry off no matter what the project file says. Nothing in a project fleet.json can re-enable it — cyberine fleet enable <provider> run inside the project writes the flag and then prints that the global OFF still wins.
  • A project file can still narrow. enabled: false in the project file disables a provider the global file enables, exactly as before.
  • The global file deciding nothing leaves the project flag alone. A provider with no entry in the global file at all follows the project flag only.
  • Matching is by workerId (plus seat for a seat lane), not by provider key name, the same rule cyberine init's snapshot uses, so a renamed global entry still matches.
  • With no fleet.json anywhere, behaviour is byte-for-byte unchanged — every keyed provider is still built, and no gate note is emitted.

The *-direct compare adapters follow the provider switch too. loop-deepseek-direct, loop-mimo-direct, loop-gateway-a and the rest of the exploration/compare loop adapters are built by buildDefaultAdapters from a present API key, and each one is gated on the fleet entry binding its build worker (loop-deepseek-direct on loop-deepseek-build, and so on — the map is ALWAYS_ON_ADAPTER_WORKER_IDS in src/core/fleetGate.ts). Turning a provider off, globally or in the project, removes BOTH its build worker and its direct adapter from the pool, with one note saying which file decided it and how to undo it. Before, the operator's one switch could be defeated by a stale project snapshot and the direct adapter would still spend the account.

Run recipe — turn a provider off everywhere, then check it took:

# from a directory with NO .cyberine/fleet.json (so the global file is the one in effect)
cd ~ && cyberine fleet disable deepseek

# now from any repo, including one initialised before the disable
cd /path/to/repo
cyberine fleet list      # deepseek row reads "global-off"; a block underneath names the reason + the global path
cyberine route "fix a bug"   # deepseek is absent from the "available:" line
cyberine fleet doctor    # one WARN naming the global path, the global provider name and the reason

cyberine fleet list renders a globally-off provider with global-off in the ENABLED column and prints an off in the GLOBAL fleet.json (this project file cannot re-enable them): block listing, per provider, the global path, the global entry's own name, the reason (disabled in the global fleet.json or auto-disabled (<kind>): <evidence>) and the exact command that undoes it. cyberine route drops it from the candidate pool, and a dispatch prints the skip note on the run.

Known Limitations and Deferred Work — fleet layer precedence

  • An explicit pin still wins over both layers, by design. CYBERINE_BUILD_FLEET=<id> does not consult fleet.json at all, and a routing.steps pin in the in-effect fleet.json is dispatched verbatim with workersStrict — neither is filtered by a global OFF. cyberine fleet doctor reports a step pin that a global OFF covers as a not-enabled step issue, but nothing refuses the dispatch. Naming a worker explicitly is treated as the operator overriding their own switch.
  • Precedence is evaluated only in the project-over-global direction. A global file that is itself the one in effect (no project file) keeps today's semantics exactly: enabled: false is off, and an enabled: true entry still carrying a stale autoDisabled block stays ON, with the existing validator warning telling you to clear it with cyberine fleet enable <provider>.
  • The overlay is runtime-only and never written to disk. The project file keeps whatever enabled value the operator set; the global decision is recomputed on every load. So a later global re-enable restores the project entry with no second edit — but a project file copied to a machine with a different global file will behave differently there, on purpose.
  • Nothing reconciles the two files for you. There is no cyberine fleet sync; the surfaces above report the disagreement and the fix, and the operator decides which layer to change.

seats-only writes one claude-<seat> entry per declared global seat and refuses (naming the global path to declare one) when no seat exists yet; full writes every built-in provider, availability-gated. Every preset appends .cyberine/fleet.json to .gitignore (idempotent — it checks for the line first) and finishes by running cyberine fleet doctor in place, so a first cyberine init ends with the same validation you'd get from running the doctor by hand. A seats-only pin also rewrites build.fleet in the config.json it just wrote to claude (k5rhx, 2026-09-12): the provider-detection suggestion (claude,muse,codex) named workers the seats-only fleet.json had just set to enabled: false, so the two files contradicted each other on the first run. claude is the bare seat base worker; at dispatch time it expands to the enabled claude-<seat> lanes through routing.seats (see "build.fleet from .cyberine/config.json is a preference"). The rewrite prints one rewrote ... build.fleet claude,muse,codex -> claude line and is skipped when build.fleet already reads claude. cyberine fleet init still exists standalone for pinning a fleet outside of init.

cyberine init also seeds a task manifest (TASKS-INTEROP-SMOKE), regardless of provider detection: if the resolved root (see docs/tasks-manifest.md section 2) carries none of .claude/TASKS.md, .codex/TASKS.md or .local/tasks/ACTIVE.md yet, it creates .local/tasks/ACTIVE.md with the spec's header comment and zero tasks and prints the path it created; it never creates a tracked .claude/TASKS.md on its own. Seeding also appends .local/ to the repo's .gitignore (k1bcx, 2026-09-12; idempotent, and .local, /.local, /.local/ or .local/** already present count as done) so the manifest and every later .local/runs/ entry stay untracked — before that only .cyberine/fleet.json was ignored and the seeded board was trackable. When a manifest already exists at any candidate, it prints which one and leaves it untouched — re-running cyberine init is always safe. The seeded file parses to zero tasks with zero warnings under both cyberine tasks doctor and the nf-tasks skill's own status/doctor.

cyberine init --rules (kfctv, 2026-09-14, superseding the previous day's --vibe-rule) writes .claude/rules/cyberine-autonomy.md and .claude/rules/cyberine-board.md at the resolved git toplevel from the bundled templates/rules/*.md, substituting {{DATE}} with today's UTC date, and merges "rules": { "applied": true, "appliedAt": "<YYYY-MM-DD>", "files": [...] } into .cyberine/config.local.json (preserving every other key; --track writes .cyberine/config.json instead, kzfa2). cyberine autopilot refuses to start unless rules.applied === true in either file (config.local.json is read first, so a repo applied before kzfa2 keeps working) — the flag, or the interactive prompt below, is how a repo opts into unattended commits. Consent is always ASKED, never assumed: on a real TTY with neither --rules nor --no-rules nor --yes, plain cyberine init asks once (Apply the Cyberine rules to this repo? ... [y/N], default no); --yes alone, --no-rules, or a non-TTY leaves the rules unapplied and prints one hint line naming cyberine init --rules. Each template file is refused per-file when already present (prints <path> already present -- leaving it. and still counts it as applied — a repo that already wrote one file by hand is not blocked from recording the other or the config field) rather than the whole operation failing when only one exists. config.json's rules.applied is authoritative for the autopilot precondition regardless of whether the rule files themselves are present — the CLI only ever writes both together, but a repo can also record the field by hand-editing .cyberine/config.local.json when a different file already plays the autonomy-grant role (this repo does exactly that in its untracked .cyberine/config.local.json, since .claude/rules/user-vibe-coding.md already covers it here, so --rules was never run against this repo). Known limitation: the bundled templates are a generic pre-authorization (commit/push/merge for a Cyberine-driven session, idiomatic-default autonomy, offer-once verification, a fixed hard floor) and generic board discipline (cyberine tasks only, evidenced done flips, owner-only backlog promotion) — a repo with its own commit/branch/CI conventions should edit the written files afterward; the flag does not read or merge any existing policy.

The Cyberine ignore block (kzfa2, 2026-09-26). Every cyberine init appends the missing lines of one canonical block to the repo's .gitignore, under a # Cyberine: ... header, and never twice (the last three lines only with --rules, and not with --track):

.local/
.cyberine/*.local.json
.cyberine/fleet.json
.cyberine/plans/
.cyberine/test-output.txt
.claude/rules/cyberine-autonomy.md
.claude/rules/cyberine-board.md
.claude/settings.local.json

The autonomy rule grants unattended commit/push/merge, so by default it is a per-machine opt-in: the two rule files, rules.applied (in config.local.json) and the board guard (in .claude/settings.local.json) all stay out of git, and a fresh clone keeps the ask-first default. cyberine init --rules --track is the team-shared shape: rules.applied in .cyberine/config.json, the guard in .claude/settings.json, and the rule files not ignored, so every clone inherits them. Everything else under .cyberine/ — config.json, policy.json, hooks.json, commands.json, rules/, workflows/, skills/ — is shared config and stays tracked. The convention going forward: a new personal file under .cyberine/ is named *.local.json (already covered) or is added to CYBERINE_GITIGNORE_LINES in src/commands/init.ts. Known limitation: the block is append-only — init never removes a line, never moves an existing tracked rules.applied from config.json to config.local.json, and never untracks a file already committed (that is git rm --cached, the operator's call).

The board-write guard (ke1am). The same consent also installs a Claude Code PreToolUse hook into the repo's .claude/settings.local.json (.claude/settings.json with --track; a guard already in either file counts), because cyberine-board.md's "every board write goes through cyberine tasks" was prose and nothing enforced it — an empty [] bracket only ever comes from a hand-edit. The hook is one entry, matcher Edit|Write|NotebookEdit|MultiEdit|Bash, running cyberine tasks guard: it reads the tool payload on stdin and exits 2 (block, reason handed back to the model) when the call would WRITE a board file — the manifest, TASKS_ARCHIVE.md, status.log, anything under status.d/ or journal/. Reads are always allowed, and so is a redirect whose destination is not a board path, so cat .local/tasks/ACTIVE.md > /tmp/copy.md and cp .local/tasks/ACTIVE.md /tmp/ both pass while sed -i, a > onto the board, mv/cp ONTO it, tee, and rm are refused with the rule quoted. An existing settings.json is MERGED, never rewritten: other hooks, permissions and env survive, re-running is a no-op, and a settings.json that is not readable JSON is reported and left untouched rather than replaced.

cyberine init --rules      # writes the rules AND the guard hook
echo '{"tool_name":"Write","tool_input":{"file_path":".local/tasks/ACTIVE.md"}}' | cyberine tasks guard; echo $?   # 2

Known Limitations and Deferred Work. The Bash arm matches write INTENT plus a board path rather than parsing the shell, so an exotic route (a python one-liner opening the file, an unrecognised verb) still gets through, and a command it refuses may occasionally be innocent — rephrase it rather than fighting it. The guard is per-repo: it protects the repo whose settings.json holds it, and only inside Claude Code. The hook calls the cyberine on PATH, so a repo initialised by one install follows that install's version of the rule.

The board banner at session start (kzyxj). A session learns the board exists from one banner line at start: the status --summary counts, a Resume: line naming the first [doing] (or unblocked [todo]) task, and a recurring-due line when one is due. It stays silent in a repo with no board or a board with no task blocks. With the plugin installed it comes from the plugin's own SessionStart hook (see "Plugin hooks" above). Without the plugin, add it by hand to the one repo that wants it, either in the tracked .claude/settings.json (the whole team gets it) or in the gitignored .claude/settings.local.json (only this machine), and drop it again once the plugin is installed:

{
  "hooks": {
    "SessionStart": [
      { "matcher": "startup|resume|clear",
        "hooks": [{ "type": "command", "command": "cyberine-tasks-banner", "timeout": 5 }] }
    ]
  }
}

cyberine-tasks-banner (bin/cyberine-tasks-banner.mjs, a package bin) is a plain-Node launcher: it caches the rendered banner under $CYBERINE_HOME/cache/tasks-banner/, keyed on the session cwd, the local date, the package version and the size and mtime of every board input (all three manifest candidates, status.log, every status.d/ entry, TASKS_ARCHIVE.md), and only boots the CLI (cyberine tasks banner --cwd <dir> --json) on a miss. Measured on this repo: a miss 1.85 s, a hit 0.12 s, both inside the 5 s hook budget. From a checkout that is not npm-linked, point the command at the file instead: node "$CLAUDE_PROJECT_DIR/bin/cyberine-tasks-banner.mjs".

cyberine tasks banner                          # print the banner for this repo (no cache)
cyberine tasks banner --cwd ../other --json    # {"banner": "...", "inputs": [...]}, what the launcher caches
echo '{"cwd":"'"$PWD"'"}' | cyberine-tasks-banner   # exactly what the hook runs
CYBERINE_TASKS_BANNER_NO_CACHE=1 cyberine-tasks-banner   # force a re-render

Coexistence with nf-tasks (the author's own board tool, which prints its own banner from a global hook): nf-tasks' tasks-inject.sh stays silent in a repo whose .claude/settings.json or .claude/settings.local.json names cyberine-tasks-banner, so a repo that opted into Cyberine's banner shows exactly one.

Known Limitations and Deferred Work. cyberine init does not offer to write the hook yet; add it by hand. The cache key does not cover the journal (the banner does not read it) or the claim-lease clock inside a day, so a lease that expires mid-day shows on the next board write or the next day. The banner shows the same counts as cyberine tasks status --summary, which also counts FORBIDDEN tombstones where nf-tasks' banner does not.

Known Limitations and Deferred Work — detection and the detected preset

Detection is evidence-based and read-only: it looks at env vars, config/credential FILES, the macOS Keychain (claude only, and only when the platform really is darwin), PATH, and the seat token files declared in the global fleet.json. It never makes a network call, never opens a session and never reads a credential VALUE into anything it prints. That draws a hard boundary, and these setups fall outside it — each one is a case where cyberine init will UNDER-enable and you finish by hand with cyberine fleet enable <provider>:

  • A credential that only exists inside a login the CLI keeps to itself. Muse stores its real token in the OS keychain and keeps only a login-STATE record in auth.json; delete that record while the keychain token is still live and muse keeps working while detection reports not-logged-in. Cursor Agent is detected from the mere presence of ~/.cursor/, which is coarse in the other direction. Neither is closable without reading a credential store this code deliberately does not touch.
  • A credential that is only valid, not only present. Every check is presence, never validity: an expired, revoked, typo'd or unfunded key detects as usable and fails at dispatch instead. The one exception is the GitHub Copilot store, which carries its own expiresAt. A live check would mean spending a call per worker on every init, fleet list and setup --status.
  • A credential injected at RUN time only. A key exported by a shell wrapper, a CI secret, a direnv block, a systemd unit or a secret manager that is not in the environment when init runs is invisible to it. config.ts folds shell env, the config.json layers, $CYBERINE_HOME/.env and ./.env into the environment first, so anything in those four is seen; nothing else is.
  • A credential on a remote machine. Detection describes THIS host. A fleet.json committed from a laptop and used on a runner describes the laptop.
  • A provider behind a gateway or proxy that authenticates for you. loop-claude-build / loop-gpt-build are enabled by any explicit GATEWAY_* / LOOP_MODEL_* var, which is an opt-in signal and not proof the gateway is reachable; a gateway that needs no env var at all is not detected.
  • Bedrock and Vertex ambient credentials. Both accept an instance profile, ECS task role or gcloud ADC that no env var names. Detection requires the explicit opt-in (BEDROCK_PROFILE / AWS_PROFILE / an explicit key pair; VERTEX_PROJECT_ID), so an ambient-credential host under-enables.
  • opencode's per-provider credentials are fail-open, not detected. An unreadable auth.json or an unrecognized provider prefix reports usable rather than blocking a working custom route (hl-t693), so an opencode worker can be enabled and still fail to authenticate at dispatch.
  • Several accounts for the SAME worker are only modelled for claude. Seats give one claude-<seat> lane per declared seat; no other worker has a seat concept, so two ChatGPT accounts or two DeepSeek keys collapse into one row. k1fpd tracks the related case where a seat and the keychain login are the same account and nothing warns.
  • A seat token's MODE is reported, not enforced. A 0644 seat token is listed with mode 644 and still enabled; nothing refuses it.
  • A retired worker is never detected-usable, whatever credential is present — RETIRED_WORKERS wins. Un-retire it in src/core/workerProfiles.ts if the account is funded again.

Reproduce any of these against the real code with a throwaway script driving detectWorker / detectFleet from src/core/workerDetection.ts: every input (env, platform, home, PATH, $CYBERINE_HOME, cwd) is an injectable field on DetectionContext, so a Linux-without-a-keychain or env-credentials-only machine can be described without a container and without touching a real credential file.