Skip to content

Add simlock instructions: the rules an agent must follow, printable and served over MCP #147

Closed

Description

@V3RON

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
Loading

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_VERBS as `simlock simctl <verb>`; `simlock simctl shutdown all`; `simlock simctl runtime delete`; the sentence simlock simctl refuses any option before the subcommand, --set and --profiles included.
  • Android: `simlock adb kill-server`; each sequence of REFUSED_ADB_SEQUENCES as `simlock adb <sequence>`; the sentence simlock adb refuses any option before the subcommand other than -s, -t, -d or -e; and that on another machine simlock adb shell needs 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 in docs/internal/ARCHITECTURE.md.
  • src/cli/tree.ts: an instructions node; src/cli/commands/instructions.ts (new): [--json], --help, any other argument USAGE. Never connects to a daemon.
  • src/cli/index.ts: instructions in the USAGE banner and in GATEWAY_EXEMPT_PATHS.
  • src/contract/errors.ts (exported through src/contract/index.ts): LEASE_LOST_EXIT_CODE = 14 moves here from src/cli/commands/lease.ts, which imports it; behaviour unchanged (F8).
  • src/mcp/server.ts: instructions: MCP_SERVER_INSTRUCTIONS in the server options; registerResource of simlock://instructions (text/markdown, AGENT_INSTRUCTIONS). No new tool.
  • src/drivers/ios/index.ts, src/drivers/android/index.ts: export the refusal constants (and a runtime delete constant in place of the inline literal) for the test; behaviour unchanged.
  • src/agent-device/shared/replies.ts: export LEASE_ENDED_MESSAGE (through the module's index) for the test.
  • src/cli/AGENTS.md, src/mcp/AGENTS.md: Uses gains instructions; the MCP manual names the resource and server instructions; the CLI manual's exempt list gains instructions.
  • 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 instructions section, instructions added 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 "run simlock instructions and 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
Loading

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": instructions is reached only from commands/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 instructions and gets the rules text, no daemon: plain → AGENT_INSTRUCTIONS on stdout, stderr empty, exit 0, no socket under the home; --json → one line, { instructions } equal to the plain output; --bogus and a positional → USAGE, exit 2, stdout empty; --help → Usage: simlock instructions [--json], exit 0.
  • A user passes --gateway or --local to simlock instructions on a machine with no gateway and still gets the text: --gateway; --local; and simlock --help lists instructions (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, 13 from ERROR_TABLE and 14 from LEASE_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_INSTRUCTIONS and MCP_SERVER_INSTRUCTIONS hold no top-level directory of git ls-files followed by /, and no *.md name other than AGENTS.md and CLAUDE.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() equals MCP_SERVER_INSTRUCTIONS, starts with the title, holds ## Rules for every agent and ## MCP, and is at most 4096 characters; listResources holds simlock://instructions with text/markdown; readResource equals simlock instructions output; every name from listTools appears in the text.

Done when

  • simlock instructions and simlock instructions --json behave as above, verified by pnpm test:e2e e2e/agent-instructions.test.ts.
  • simlock --help lists instructions.
  • An MCP client gets the server instructions and can read simlock://instructions, verified by pnpm test:e2e e2e/mcp-session.test.ts.
  • reference/cli.mdx documents the command and lists it among the exempt commands; connect-your-agent.mdx has no hand-written rules block and points at simlock instructions.
  • pnpm check is green.

Out of scope

Depends on

Written by an agent.

Activity

  1. added
    task:readySpec checked. An agent may implement it once its Depends on allow.
    on Sep 27, 2026
  2. self-assigned this
    on Sep 27, 2026
  3. github-actions commented on Oct 2, 2026

    @github-actions

    Claim released: no activity for 3 days. The label is unchanged; the next agent starts from the branch and the last handoff.

  4. github-actions commented on Oct 5, 2026

    @github-actions

    Claim released: no activity for 3 days. The label is unchanged; the next agent starts from the branch and the last handoff.

  5. github-actions commented on Oct 9, 2026

    @github-actions

    Claim released: no activity for 3 days. The label is unchanged; the next agent starts from the branch and the last handoff.

  6. V3RON commented on Oct 10, 2026

    @V3RON
    ContributorAuthor

    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.

  7. added
    task:draftScope written; technical spec or its spec check missing.
    and removed
    task:readySpec checked. An agent may implement it once its Depends on allow.
    on Oct 10, 2026
  8. github-actions commented on Oct 10, 2026

    @github-actions

    Back to task:draft: waiting on #442.

  9. added
    task:readySpec checked. An agent may implement it once its Depends on allow.
    and removed
    task:draftScope written; technical spec or its spec check missing.
    on Oct 12, 2026
  10. github-actions commented on Oct 12, 2026

    @github-actions

    Now task:ready: approved, technical spec present, #442 closed.

  11. V3RON commented on Oct 12, 2026

    @V3RON
    ContributorAuthor

    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.

  12. 14 remaining items

  13. added
    task:readySpec checked. An agent may implement it once its Depends on allow.
    on Oct 23, 2026
  14. self-assigned this
    on Oct 23, 2026
  15. V3RON commented on Oct 23, 2026

    @V3RON
    ContributorAuthor

    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 --session on every command. No token or config set is 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.

  16. added a commit that references this issue on Oct 24, 2026
    c61e257
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

task:readySpec checked. An agent may implement it once its Depends on allow.

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions