Does: the simlock command surface over simlock/admin — parse, render, hold a lease while alive.
Entry: main.ts (the simlock bin) → index.ts runCli(argv).
Uses: admin, bus (event key, type), client (types), agent-identity, lease-policy, history (reader), core
(loadConfig only), contract, ports, module-registry (optionalModules only); instructions
(commands/instructions.ts and commands/setup.ts for values; types.ts for a type; index.ts and
errors.ts load it on demand); mcp, loaded on demand. Never imports: daemon, drivers, gateway, http.
Everything except agent-identity (the requester-id resolver, the one cheap component) is
loaded on demand: the CLI's own shared files (main.ts, index.ts, tree.ts,
errors.ts, usage-error.ts, types.ts, environment.ts, stdio.ts, log-file.ts,
args.ts, connect.ts) import no component for a value and environment.ts none but
agent-identity, so the router answers --help or a usage error without loading core,
contract, admin, ports or any command module. The dependency rule
cli-router-imports-no-component-values rejects a value import of any other component into
those files; lazy-loading.test.ts measures what a command loads when it runs.
Surface: cli.mdx — every command and flag; errors.mdx — exit codes.
| File | Role |
|---|---|
index.ts |
runCli: the global --token, --gateway and --local, the command lookup, the error boundary |
tree.ts |
The command tree — each node names the module that implements it |
environment.ts |
buildCliEnvironment: the ports a command may reach, from the ports it is handed |
types.ts |
CliEnvironment and its ports — types only, so routing loads no code |
errors.ts |
The error → code/exit mapping |
usage-error.ts |
UsageError and withHelpHint, the help pointer's one wording |
connect.ts |
connectDaemonClient: credential order, the admin session, the notice |
args.ts |
commandArgs / requiredPositional / subcommandOrUsage / writeResult |
commands/*.ts |
One module per command, run(argv, context); loaded by the tree on a hit |
format.ts |
The human renderers for status, worker list, daemon start, etc. |
duration.ts |
parseDuration for --ttl-style flags |
termination.ts |
The signals and parent-exit wait a held lease blocks on |
log-file.ts |
readLogFile: daemon.log plus a rotated generation |
stdio.ts |
readPipedStdin: a piped stdin for simctl/adb, read to EOF |
passthrough.ts |
spawnPassthrough: runs a resolved simctl/adb command, stdio inherited |
follow-log.ts |
followLog: daemon logs --follow across rotations |
main.ts |
The bin; sets process.exitCode |
format.ts, duration.ts and termination.ts are reached only from a command module, and
follow-log.ts and passthrough.ts only from a dynamic import inside environment.ts.
- Route —
runClitakes out the global--token,--gatewayand--local(bare, anywhere;--gateway=<x>and--local=<x>are usage errors, and both flags together are one), then walksCOMMANDSto the deepest node the words name and loads that one module (a parent and its children can share it). Nothing runs without a node: an unmatched word is a usage error before any command module is read.--help/-has the first word, or no words, prints the root usage without building an environment at all. Any error is one JSON line on stderr,{ error: { code, message } }. Exit code:ERROR_TABLEcliExitCode, 2 for a usage error, else 1. The flag goes toresolveDaemonHome, which picks the folder:--gateway<root>/gateway,--localthe worker, and with no flag the gateway only when one is present and<root>/instance.jsondoes not exist. The launcher gets--gatewaywhenever the chosen role is the gateway (SIMLOCK_HOMEstill the root). When a worker answersUNSUPPORTED_IN_WORKER_MODE, no--gatewaywas given and a gateway is present, the printed message gets the--gatewayhint appended. With--gatewayand no gateway present every command butdaemon start,daemon install,daemon uninstall,config set,mcp,instructionsandsetup(and any--help, except afteradb/simctl) stops withGATEWAY_NOT_PRESENT, exit 2, before it connects. - Connect (
connectDaemonClient) — alwaysconnectSimlockAdmin; principal =resolveRequesterId(env, <pid>). Credential, first found:--token→SIMLOCK_ADMIN_TOKEN→admin.tokenin the data directory (3 reads, 50 ms apart). None, or a rejected one → anagentsession and one stderrnoticeline. - Auto-launch (
AutoLaunchAdminConnector) — when nothing listens: load the config first (a bad one fails on this stderr), launch the daemon (launcherFor: through the service when it is loaded and whole, else spawned detached with a stderr notice;daemon installanddaemon uninstallwrite the service through theServiceManagerport;daemon statusonly reads its state and prints aService:line, with no line on a refused handshake or an unsupported platform), retry every 50 ms forDAEMON_STARTUP_TIMEOUT_MS; a launched daemon that exits fails the command at once withstartErrorFrom's text, unless another daemon answers a last connect; a start through the service never reports an exit, so a daemon it refuses fails at the deadline with thedaemon.loglines since the start. After a launch,status.getprobes the connection before handing it to a command, within that same deadline. A connection loss during the handshake or this probe becomesDAEMON_STARTUP_FAILED; a silent probe times out with that code and starts closing its client without waiting for the daemon to end its side. An answered probe ends the window, including astartingstatus: subsequent command failures retain their original codes even while a lease queues or boots. The connector retains the launch deadline across a rejected credential and agent fallback.daemon start(untilStarted) then pollsstatus.getevery 50 ms until health leavesstarting:runningends it; anything else (aDAEMON_STOPPINGanswer included) fails with the daemon's own error fromstartErrorFrom; 90 s (STARTUP_READ_LIMIT_MSplus 30 s) ofstartingfails withDAEMON_STARTUP_FAILED("still starting") and leaves the daemon running. A pre-existing daemon is never probed or reclassified; other daemon errors keep their codes. simlock lease—requestLease; progress pushes as JSON lines on stderr; one result line on stdout, with the session'srole(--export-env: shell lines).--agent-idsets the requester. Then, unless--detach:- renew on
startLeaseRenewal(a third of the TTL); - wait for
SIGINT/SIGTERM, or the exit of--bind-pidor the parent (ParentWatch); - stop renewing, release within
RELEASE_TIMEOUT_MS(5 s), close. Lease lost → exit 14. Connection lost →DAEMON_CONNECTION_LOST; the lease runs to its deadline.
- renew on
simlock simctl/adb— readsstatus.getdaemon.mode:- worker:
driver.passthroughreturns a scoped command;spawnPassthroughruns it here. - gateway:
device.execon--lease, or the requester's only lease; the worker's driver fills the lease's device in and refuses another (ADR 0047). A piped stdin is read to EOF first; output chunks go to stdout and stderr.PASSTHROUGH_REFUSEDandUNKNOWN_PASSTHROUGH_TOOLbecome usage errors (exit 2).
- worker:
simlock events --follow— subscribe, then replay, then print buffered pushes not already replayed (byeventKey).--sincewithout--follownever launches a daemon; with none running it readshistory.dbitself (read-only; no file is empty, an unreadable one is an error, exit 1).simlock statusadds aHistory:line for a history that is not written, one for a set-aside file, one after a rebuild of the usage figures and one after a failed rebuild.simlock config setdropseventLog.rotateBytesand says so on stderr. 6a.simlock stats—usageWindow(contract) turns--since,--from,--tointo a window; each refusal is a usage error (exit 2) worded here. Asksusage.getand printsformatUsage(stats.ts) or the answer as--json; computes no figure.--csvasks withincludeLedgerand writesledgerCsv(csv.ts, RFC 4180, CRLF, a leading'before=,+,-,@);--csvwith--jsonis a usage error; aledgerTruncatedanswer adds a stderr notice.simlock daemon logs --follow—followLogpolls every 250 ms from a byte offset. A new identity ondaemon.log.1is a rotation: print its rest, restart at offset 0.simlock config set— validates the merged file withloadConfig, creates the data directory0700when missing, writesconfig.jsonatomically, mode0600.simlock mcp— importssrc/mcp/main.tsand runs it, passing the global flag as{ daemonFlag }.simlock instructions [--json]— prints the static rules text fromsrc/instructions, or one JSON object{ instructions }; never connects to a daemon.simlock setup [--user | --project] [--tool <t>] [--mcp | --no-mcp] [--json]— installs the router skill throughsetupAgentTools, withenvironment.homeDirectory,workingDirectory,codexHome,filesystemandrunTool(the composition root readshomedir(),process.cwd()andCODEX_HOME); never connects to a daemon. Prompts only without--json. A refusal isSETUP_REFUSED(exit 2); a failedmcp addprints the report, thenSETUP_MCP_FAILED(exit 1).
- No decision logic and no state beyond the lease it holds (architecture rule 8).
- The connection holds nothing: a lease ends by release or TTL.
- Renewal stops before the release; no timer outlives a command.
CliEnvironmentcarries every port and process handle; tests build it with fakes.- Nothing printed names a file path in this repo (documentation rules).
simlock simctl/adbcan hang on a piped stdin. Through a gateway,readPipedStdinreads stdin to EOF before sending, with no bound. A pipe nobody closes blocks before the command goes out. Redirect stdin from/dev/null.