Repository navigation
Add simlock instructions: the rules an agent must follow, printable and served over MCP #147
Description
Part of #157.
Scope
An agent or operator runs simlock instructions and gets the current rules for using Simlock as one Markdown text, a section per surface, agent-device first. An MCP agent gets the short version of the rules when it connects, and can read the full text as the resource simlock://instructions. The website's "Connect your agent" page points at the command instead of carrying its own copy.
Start from branch origin/feature/157 (commits 525fd53ca, 54e5dde76, 0b29c03a6): src/instructions/index.ts and its tests apply cleanly. The CLI part has to be re-ported to src/cli/tree.ts and src/cli/commands/ (F2), and the text rewritten as below.
In short
simlock instructions prints the rules: what every agent must do, then how to use agent-device, how to build for iOS, the simlock CLI, and MCP. It never contacts a daemon. An MCP client receives the first and last sections as the server instructions on connect, at most 4096 characters long. If it goes wrong, an agent follows a rule Simlock no longer has, or never hears about agent-device.
flowchart LR
A[Agent or operator] -->|simlock instructions| T[rules text]:::changed
M[MCP client] -->|connect| S[server instructions]:::changed
M -->|read simlock://instructions| T
classDef changed stroke-width:3px
Before and after:
$ simlock instructions
{"error":{"code":"USAGE","message":"Unknown command: instructions (run `simlock --help` for usage)"}} # exit 2
$ simlock instructions # after
# Using Simlock to share iOS simulators and Android emulators
... # exit 0
$ simlock instructions --json # after
{"instructions":"# Using Simlock ..."}
| Input | Today | After |
|---|---|---|
simlock instructions |
USAGE, exit 2 | text, exit 0 |
simlock instructions --json |
USAGE, exit 2 | one JSON object |
simlock instructions --help |
USAGE, exit 2 | Usage: simlock instructions [--json], exit 0 |
simlock instructions extra |
USAGE, exit 2 | USAGE, exit 2 |
simlock instructions --bogus |
USAGE, exit 2 | USAGE, exit 2 |
simlock --gateway instructions, no gateway here |
USAGE, exit 2 | text, exit 0 |
What the text says
One Markdown text: a # title, a one-paragraph intro, then these ## sections in this order (the feature's Shared values). Each section says, in plain words, what its "Says" cell lists, and only commands that exist today. The text ends with the line More: https://callstackincubator.github.io/simlock/docs/.
Each section must also contain every string in its "Required strings" cell, word for word (backticks included): the tests check these and nothing else about the wording.
| Section | Says | Required strings |
|---|---|---|
## Rules for every agent |
Never run the platform tools yourself (F7). Drive only the device you leased. Under Claude Code and Codex your identity is automatic: do not set SIMLOCK_AGENT_ID (F10). Under any other tool, use a SIMLOCK_AGENT_ID of your own for the whole session, never one shared with another agent. The operator decides how many leases you may hold; REQUESTER_ALREADY_LEASED means use the lease you hold or release it first (F8, F17). Never pass --allow-download unless a person told you to. Never run simlock release --all or simlock nuke. Check that Simlock is set up for you with simlock whoami (F5). |
xcrun simctl, adb, avdmanager, emulator, Claude Code, Codex, SIMLOCK_AGENT_ID, REQUESTER_ALREADY_LEASED, --allow-download, simlock release --all, simlock nuke, simlock whoami |
## agent-device |
Get a token and the URL from the operator, set AGENT_DEVICE_DAEMON_AUTH_TOKEN, then run connect proxy once (F12). Pass your own --session <name> on every command, or set AGENT_DEVICE_SESSION, when other agents run on the same machine (F12). open --platform with --device leases a device and may wait in a queue (F13: 0.21.28 rejects --os, so the text does not name it). install uploads the app from your machine (F14). close gives the device back. The lease-ended message means: run agent-device disconnect, then open; the old device and its data are gone. Through a gateway, the URL is the gateway's. |
AGENT_DEVICE_DAEMON_AUTH_TOKEN, agent-device connect proxy --daemon-base-url <url>/v1/agent-device, --session <name>, AGENT_DEVICE_SESSION, agent-device open --platform <ios|android> --device <model>, agent-device install <app> <path>, agent-device close, agent-device disconnect, and LEASE_ENDED_MESSAGE (F11) |
## iOS builds |
Simlock's simulators are in a device set of their own, not the default set Xcode and xcrun simctl use (F15), so tools that pick a simulator from the default set cannot find them. Build for any simulator, then install the .app with agent-device install, or with simlock simctl install when the path is on the machine that runs Simlock. |
xcodebuild -destination id=<udid>, react-native run-ios, expo run:ios, Detox, Maestro, -destination 'generic/platform=iOS Simulator', agent-device install, simlock simctl install <device.address> <path-to-.app> |
## simlock CLI |
Run simlock catalog first. Lease in the background: one JSON line on stdout, progress on stderr, renewed while it runs and released when it exits; --bind-pid ties the lease to another process. Without a background process: --detach --ttl, then renew before ttlDeadline. --physical leases an enrolled phone (F16). Release with simlock release. Reach the device through simlock simctl and simlock adb, or the grant's environment; on another machine, with --lease. The refusals below are on purpose: do not work around them. The exit codes below and what to do on each. Every failure writes one JSON error line on stderr: branch on code. If this machine runs a gateway and the operator says to use it, put --gateway right after simlock (F6). |
simlock catalog, simlock lease --platform <ios|android> --device <model>, --bind-pid <pid>, --detach, --ttl <duration>, simlock lease renew <lease-id>, --physical, simlock release <lease-id>, lease.id, lease.ttlDeadline, device.address, environment, --export-env, simlock simctl --lease <lease-id>, simlock adb -s <device.address>, simlock adb --lease <lease-id>, {"error":{"code":"...","message":"..."}}, --gateway; each refusal of the refusals list below; each of 10, 11, 13, 14 as `<code>` |
## MCP |
The four tools and what each is for (F3). Call lease_status after a context compaction. The tools lease and release only: install and launch through simlock simctl / simlock adb with device.address. Under Codex, the MCP server and the CLI are two identities: use only one of them per session (F19). |
each tool name as `<name>`, context compaction |
The refusals list, each named in the ## simlock CLI section as written here (F7):
- iOS: each verb of
REFUSED_SIMCTL_VERBSas`simlock simctl <verb>`;`simlock simctl shutdown all`;`simlock simctl runtime delete`; the sentencesimlock simctlrefuses any option before the subcommand,--setand--profilesincluded. - Android:
`simlock adb kill-server`; each sequence ofREFUSED_ADB_SEQUENCESas`simlock adb <sequence>`; the sentencesimlock adbrefuses any option before the subcommand other than-s,-t,-dor-e; and that on another machinesimlock adb shellneeds a command to run.
The MCP server instructions are the # title line, ## Rules for every agent, ## MCP, and the line Run `simlock instructions` for the rest: agent-device, iOS builds and the CLI. Both texts are built from the same section strings, so nothing is written twice. Simlock keeps the server instructions at most 4096 characters, so that a client that cuts long server instructions still gets all of it.
Technical spec
Facts
Commit cac23e033 unless noted.
| # | Fact | Source |
|---|---|---|
| F1 | simlock instructions is an unknown command today: Unknown command: instructions, exit 2 |
src/cli/index.ts:215 throw new UsageError(withHelpHint(\Unknown command: ...`)); node dist/cli/main.js instructionsprinted{"error":{"code":"USAGE","message":"Unknown command: instructions (run `simlock --help` for usage)"}}`, exit 2 |
| F2 | Commands are nodes in COMMANDS, each loading commands/<name>.js; mcp is the model of a command that never contacts a daemon; origin/feature/157 still edits the old runCli switch |
src/cli/tree.ts:59, :131 { name: "mcp", load: () => import("./commands/mcp.js") }; src/cli/commands/mcp.ts:20; git merge-tree of f76b7e56e, main and origin/feature/157: 4 conflict hunks in src/cli/index.ts |
| F3 | The MCP server declares only logging, sets no instructions, registers no resource; tools lease_simulator, list_devices, release_simulator, lease_status |
src/mcp/server.ts:119 new McpServer(SERVER_INFO, { capabilities: { logging: {} } }); :121, :154, :173, :192 server.registerTool( |
| F4 | SDK 1.29.0: ServerOptions.instructions?: string, McpServer.registerResource, client getInstructions() |
package.json:74 "@modelcontextprotocol/sdk": "1.29.0"; in that package dist/esm/server/index.d.ts:15 instructions?: string;, dist/esm/server/mcp.d.ts registerResource, dist/esm/client/index.d.ts:167 getInstructions(): string | undefined; |
| F5 | simlock whoami exists and asks the daemon for the caller's identity |
src/cli/commands/whoami.ts:21-22 connectDaemonClient(environment, token) then client.identity() |
| F6 | With --gateway and no gateway here, every command not in GATEWAY_EXEMPT_PATHS fails GATEWAY_NOT_PRESENT; the list is daemon start/install/uninstall, config set, mcp; the website says "Five commands are exempt" |
src/cli/index.ts:160-166; website/src/content/docs/docs/reference/cli.mdx:60 |
| F7 | Refusals: iOS create, erase, delete (unexported constant), shutdown all, the inline runtime delete, and any option before the subcommand (--set/--profiles named in the message); Android kill-server, emu kill, emu avd stop, emu avd snapshot delete (unexported constants), any option before the subcommand except -s/-t/-d/-e, and a bare shell where there is no terminal (a device on another machine) |
src/drivers/ios/index.ts:134 REFUSED_SIMCTL_VERBS, :136 CALLER_SUPPLIED_SCOPE_FLAGS, :138 SHUTDOWN_ALL_TARGET, :1390 verb === "runtime" ... "delete", :1415-1422 "does not recognize any caller-supplied argument ahead of the subcommand"; src/drivers/android/index.ts:289 REFUSED_ADB_VERB, :396-408 REFUSED_ADB_SEQUENCES, :705-711 hasTerminal === false && isBareShell, :728-733 "only allows -s/-t/-d/-e" |
| F8 | Exit codes: QUEUE_TIMEOUT 10, NO_CAPACITY 11, REQUESTER_ALREADY_LEASED and LEASE_ID_TAKEN 13; 14 is the CLI's lease-lost exit, an unexported constant a test outside src/cli cannot import (past-index applies to tests) |
src/contract/errors.ts:262, :261, :267, :283; src/cli/commands/lease.ts:17 const LEASE_LOST_EXIT_CODE = 14;; .dependency-cruiser.mjs:36-45 |
| F9 | The website ships a hand-written rules block for AGENTS.md; the CLI reference has a commands table and one ## simlock <name> section per command |
website/src/content/docs/docs/getting-started/connect-your-agent.mdx:43-63; website/src/content/docs/docs/reference/cli.mdx:12-22, :153-158 |
| F10 | Requester id: SIMLOCK_AGENT_ID, else claude-code:<CLAUDE_CODE_SESSION_ID>, else codex:<CODEX_SESSION_ID>, else the fallback |
src/agent-identity/index.ts:20-35 |
| F11 | The agent-device lease-ended reply is lease ended by simlock; run disconnect, then open, an unexported constant |
src/agent-device/shared/replies.ts:12 const LEASE_ENDED_MESSAGE |
| F12 | agent-device 0.21.28: connect proxy --daemon-base-url, token from --daemon-auth-token or AGENT_DEVICE_DAEMON_AUTH_TOKEN; session is --session, else AGENT_DEVICE_SESSION, else default; open accepts --platform and --device |
package.json:71 "@agent-device/proxy": "0.21.28"; installed agent-device 0.21.28 dist/src/cli-help.js:817, dist/src/cli.js:1 process.env.AGENT_DEVICE_SESSION??\default`` |
| F13 | agent-device 0.21.28 open has no --physical flag and rejects --os |
agent-device open --help: the flag list has no --physical; agent-device open --platform ios --os 18 → Error (INVALID_ARGS): Unknown flag: --os (spec check, 2026-10-23) |
| F14 | A worker-side install path is refused: refused: send the app from your machine as an upload |
src/agent-device/front/request-rules.ts:29 |
| F15 | The iOS driver passes its own device set to simctl; simctl uses the default set unless --set is given |
src/drivers/ios/index.ts:339 deviceSet: deviceRoot; xcrun simctl help line 1 usage: simctl [--set <path>] [--profiles <path>] <subcommand> ... |
| F16 | simlock lease accepts --physical, --bind-pid <pid>, --detach, --ttl, --export-env; lease renew <lease-id>; catalog; release <lease-id> |
src/cli/commands/lease.ts:66-71, :245-253; src/cli/commands/catalog.ts:19; src/cli/commands/release.ts:16 |
| F17 | lease.maxPerRequester defaults to 1 |
src/core/config.ts:61 DEFAULT_LEASE_MAX_PER_REQUESTER = 1 |
| F18 | Fast e2e with a daemon home that never starts: withDaemon({ mode: "auto" }), env.cli, env.socketPath; MCP e2e via env.mcpClient |
origin/feature/157:e2e/agent-instructions.test.ts:9-19; e2e/mcp-session.test.ts:17; e2e/helpers/mcp.ts:50 |
| F19 | The MCP server's fallback identity is mcp:<pid>; Codex does not pass its session id to MCP servers |
src/mcp/main.ts:85; connect-your-agent.mdx:24 (docs; the code half is F10: no Codex variable reaches the server) |
Modules touched
Searches: GATEWAY_EXEMPT_PATHS, COMMANDS, USAGE in src/cli; new McpServer in src; REFUSED_, SHUTDOWN_ALL_TARGET, CALLER_SUPPLIED_SCOPE_FLAGS, "runtime delete" in src/drivers; LEASE_ENDED_MESSAGE; instructions in website/src/content/docs.
src/instructions/index.ts(new): the section strings,AGENT_INSTRUCTIONS(all sections),MCP_SERVER_INSTRUCTIONS(title, Rules for every agent, MCP, pointer line),renderInstructions(format: "text" | "json"). The only copy of the text.src/instructions/AGENTS.md(new manual) and its row indocs/internal/ARCHITECTURE.md.src/cli/tree.ts: aninstructionsnode;src/cli/commands/instructions.ts(new):[--json],--help, any other argumentUSAGE. Never connects to a daemon.src/cli/index.ts:instructionsin theUSAGEbanner and inGATEWAY_EXEMPT_PATHS.src/contract/errors.ts(exported throughsrc/contract/index.ts):LEASE_LOST_EXIT_CODE = 14moves here fromsrc/cli/commands/lease.ts, which imports it; behaviour unchanged (F8).src/mcp/server.ts:instructions: MCP_SERVER_INSTRUCTIONSin the server options;registerResourceofsimlock://instructions(text/markdown,AGENT_INSTRUCTIONS). No new tool.src/drivers/ios/index.ts,src/drivers/android/index.ts: export the refusal constants (and aruntime deleteconstant in place of the inline literal) for the test; behaviour unchanged.src/agent-device/shared/replies.ts: exportLEASE_ENDED_MESSAGE(through the module's index) for the test.src/cli/AGENTS.md,src/mcp/AGENTS.md:Usesgainsinstructions; the MCP manual names the resource and server instructions; the CLI manual's exempt list gainsinstructions.- Tests:
src/instructions/index.test.ts,e2e/agent-instructions.test.ts(new, from the branch),e2e/mcp-session.test.ts,src/cli/index.test.ts. - Website:
reference/cli.mdx(commands table row,## simlock instructionssection,instructionsadded to the exempt list at line 60, worded without a count),getting-started/connect-your-agent.mdx(the hand-written block at lines 43-63 becomes "runsimlock instructionsand paste its output", the MCP part says the server sends the rules on connect).
flowchart LR
CLI[cli/commands/instructions]:::changed --> I[instructions]:::changed
MCP[mcp/server]:::changed --> I
T[instructions tests] -.-> D[drivers constants]:::changed
T -.-> AD[agent-device replies]:::changed
classDef changed stroke-width:3px
Surface and extension points
src/instructions/index.ts exports constants and one pure function; no factory, no handle. No second implementation.
Contract and event changes
None after a search of src/contract and docs/internal/EVENTS.md: no daemon operation, no event, no config key, no protocol bump. Moving LEASE_LOST_EXIT_CODE into src/contract/errors.ts changes no wire shape, so its commit carries the Protocol-bump: none (<reason>) trailer toolchain.md asks for. The MCP initialize result gains instructions and the resources capability; no tool changes.
Other code on the same state
None: the task adds static text and reads no state. Searched instructions and registerResource in src (0 hits on main).
Failure modes
None: no call crosses a process or another tool, and the task writes no state. The MCP resource read is answered in process.
Rules in play
- Documentation rule 3, "Nothing the running tool prints or returns ever names a file path in this repository": the text points only at the website URL. A test proves it.
- Documentation rule 9, "A change to the public surface updates the website in the same change": the two pages above.
- Architecture rule 8, "The daemon owns all state; the CLI is a thin client": the text is static and lives in the frontends; the daemon never learns it.
src/cli/AGENTS.md, router files "import no component for a value":instructionsis reached only fromcommands/instructions.ts.- Testing rule 10: a claim of "every" names its cases; the refused verbs and exit codes are read from their definitions, not copied.
Tests
Seams: e2e/agent-instructions.test.ts (fast lane, model origin/feature/157:e2e/agent-instructions.test.ts), src/instructions/index.test.ts, src/cli/index.test.ts, e2e/mcp-session.test.ts.
- A user runs
simlock instructionsand gets the rules text, no daemon: plain →AGENT_INSTRUCTIONSon stdout, stderr empty, exit 0, no socket under the home;--json→ one line,{ instructions }equal to the plain output;--bogusand a positional →USAGE, exit 2, stdout empty;--help→Usage: simlock instructions [--json], exit 0. - A user passes
--gatewayor--localtosimlock instructionson a machine with no gateway and still gets the text:--gateway;--local; andsimlock --helplistsinstructions(unit,src/cli/index.test.ts). - A reader of the rules text finds every section and required item: the five headings in order; each "Required strings" entry inside its own section; each refusal of the refusals list, the iOS verbs and Android sequences built from the exported driver constants;
10,11,13fromERROR_TABLEand14fromLEASE_LOST_EXIT_CODE;LEASE_ENDED_MESSAGE; the last line is the website URL. - The rules text names no path in this repository: with every
https://URL removed first,AGENT_INSTRUCTIONSandMCP_SERVER_INSTRUCTIONShold no top-level directory ofgit ls-filesfollowed by/, and no*.mdname other thanAGENTS.mdandCLAUDE.md(adapt the branch's test, which also matched the URL). - An MCP client connects and gets the server instructions, and reads the resource:
getInstructions()equalsMCP_SERVER_INSTRUCTIONS, starts with the title, holds## Rules for every agentand## MCP, and is at most 4096 characters;listResourcesholdssimlock://instructionswithtext/markdown;readResourceequalssimlock instructionsoutput; every name fromlistToolsappears in the text.
Done when
simlock instructionsandsimlock instructions --jsonbehave as above, verified bypnpm test:e2e e2e/agent-instructions.test.ts.simlock --helplistsinstructions.- An MCP client gets the server instructions and can read
simlock://instructions, verified bypnpm test:e2e e2e/mcp-session.test.ts. reference/cli.mdxdocuments the command and lists it among the exempt commands;connect-your-agent.mdxhas no hand-written rules block and points atsimlock instructions.pnpm checkis green.
Out of scope
- Generating the text from the live catalog or config; it is static.
- Describing simlock run holds a lease around one command, and a detached lease follows the agent process #944, simlock simctl and simlock adb on a worker run only against the caller's lease, and fill the device in #948, MCP: a run_on_device tool runs simctl, adb or devicectl against the session's lease #949, agent-device on the same Mac works with no operator step: loopback session identity and on-by-default #950: each updates its section when it ships.
agent-device open --physical(F13): the text teaches phones throughsimlock lease --physical.- An MCP prompt primitive or a new MCP tool.
simlock setup: One command installs the agent rules where agent tools read them #620.
Depends on
Written by an agent.
Activity
github-actions commentedon Oct 2, 2026
Claim released: no activity for 3 days. The label is unchanged; the next agent starts from the branch and the last handoff.
github-actions commentedon Oct 5, 2026
Claim released: no activity for 3 days. The label is unchanged; the next agent starts from the branch and the last handoff.
github-actions commentedon Oct 9, 2026
Claim released: no activity for 3 days. The label is unchanged; the next agent starts from the branch and the last handoff.
V3RON commentedon Oct 10, 2026
Spec updated: Depends on gains #442. This task edits files the physical-device tasks of #428 also change (#436–#442), and the maintainer chose to land those first.
Written by an agent.
github-actions commentedon Oct 10, 2026
Back to
task:draft: waiting on #442.github-actions commentedon Oct 12, 2026
Now
task:ready: approved, technical spec present, #442 closed.V3RON commentedon Oct 12, 2026
Spec updated: Depends on gains #471. Feature #466 (agent-device through simlock) edits the same files or bumps the protocol, and the maintainer chose to deliver it first.
Written by an agent.
14 remaining items
V3RON commentedon Oct 23, 2026
Heads-up from #952 (being specified): once #956 and #957 land, a local agent on a standalone worker connects to agent-device with
agent-device connect --remote-config ~/.simlock/agent-device-profile.json --session <its own id>and passes--sessionon every command. No token orconfig setis needed. The instructions written here may want that line instead of the token flow. ADR 0046 (PR #954) has the details.Written by an agent.
test: a user passes --gateway or --local to simlock instructions and …
refactor(contract): export the refusal and exit-code constants the ru…
test: a reader of the rules text finds every section and required item (
feat(instructions): the rules text, a section per surface (#147)
test: the rules text names no path in this repository (#147)
test: an MCP client gets the server instructions and reads simlock://…
feat(mcp): the server sends the rules on connect and serves simlock:/…
docs: simlock instructions on the website, the manuals and the archit…
docs(instructions): the manual gets its CLAUDE.md and names real paths (
fix(instructions): iOS builds say the udid destinations do not work (#…
feat(cli): simlock instructions prints the rules an agent must follow (…