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:
| Harness | Reads | What init --rules writes there | Board-write guard |
|---|---|---|---|
| Claude Code | .claude/rules/*.md | cyberine-autonomy.md, cyberine-board.md (unchanged behaviour) | yes, the cyberine tasks guard PreToolUse hook |
| Codex, Muse, Cyberine loop workers | AGENTS.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 content | none: 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 anautoDisabledblock, 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: falsein 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(plusseatfor a seat lane), not by provider key name, the same rulecyberine 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 reasoncyberine 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 arouting.stepspin in the in-effect fleet.json is dispatched verbatim withworkersStrict— neither is filtered by a global OFF.cyberine fleet doctorreports a step pin that a global OFF covers as anot-enabledstep 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: falseis off, and anenabled: trueentry still carrying a staleautoDisabledblock stays ON, with the existing validator warning telling you to clear it withcyberine fleet enable <provider>. - The overlay is runtime-only and never written to disk. The project file keeps whatever
enabledvalue 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.jsonThe 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 $? # 2Known 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-renderCoexistence 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 everyinit,fleet listandsetup --status. - A credential injected at RUN time only. A key exported by a shell wrapper, a CI secret, a
direnvblock, a systemd unit or a secret manager that is not in the environment wheninitruns is invisible to it.config.tsfolds shell env, the config.json layers,$CYBERINE_HOME/.envand./.envinto 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-buildare enabled by any explicitGATEWAY_*/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
gcloudADC 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.jsonor 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.k1fpdtracks 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
0644seat token is listed withmode 644and still enabled; nothing refuses it. - A retired worker is never detected-usable, whatever credential is present —
RETIRED_WORKERSwins. Un-retire it insrc/core/workerProfiles.tsif 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.