{"version":1,"source":"https://docs.interlock.sh/skills.json","description":"Skills an Interlock host project should install. Each entry is complete: write body verbatim to the target path for your tool.","targets":{"claude":".claude/skills/<name>/SKILL.md","codex":"~/.codex/skills/interlock-<name>/SKILL.md","cursor":".cursor/rules/<name>.mdc"},"note":"Prefix names with \"interlock-\" for user-global tools (Codex) so they cannot collide with skills from another repo. Workspace-scoped tools (Claude, Cursor) can use the bare name.","skills":[{"name":"interlock-basics","trigger":"Use whenever you create or edit a unit of Interlock code.","source":"https://docs.interlock.sh/skills/interlock-basics.md","available":true,"body":"<!-- GENERATED MIRROR of docs/skills/basics.md — edit that file, then re-run\n     ./setup-dev-computer.sh and 'interlock-local sync code'. -->\n\n---\nname: interlock-basics\ndescription: Start here for Interlock code work. Covers handlers, req and il, responses, stores, imports, id-based URLs, naming, and the one-flat-tree environment model.\n---\n\n# interlock-basics\n\nInterlock runs real code — Java and JavaScript — on your own infrastructure, compiled and executed in the moment, with no redeploy. A **unit of code** is one file (`.java`, `.js`, `.jsx`, `.html`, `.css`) that the runtime compiles and runs. Its return value **is** the response. This skill is the floor.\n\n## Identity and environments\n\nEvery unit has an **id** (`hello`, `gift/finder`, `examples/HelloJava`) and lives in exactly one place: `code/<id>.<ext>`. The id is the path under `code/` minus the extension — folders included.\n\nThere are no environment directories. A unit is published to `dev`, `staging` or `prod` by naming that environment when you sync (`interlock sync code prod`), and publishing at one environment also publishes at the ones below it. So the tree you are editing is the whole truth about your code; which environments currently carry it is a separate question, answered by `interlock status code <env>`.\n\n- **Java files are recommended CamelCase in a folder**: `examples/HelloJava.java` → id `examples/HelloJava`.\n- Ids resolve **case-insensitively** via a lowercase-kebab slug: `examples/HelloJava` is reachable at `/run/examples/hello-java` too. Author Java as CamelCase; address it however reads best.\n\n## The handler — the return value is the response\n\n**JavaScript** (`.js`) — the default export is the handler:\n\n```js\nexport default (req, il) => {\n  il.log('running');\n  return { ok: true, name: req.str('name', 'world') };   // object/array → JSON\n};\n```\n\n**Java** (`.java`) — the class implementing `InterlockHandler`:\n\n```java\nimport sh.interlock.sdk.*;\nimport java.util.Map;\n\npublic class Handler implements InterlockHandler<Object> {\n  public Object handle(Request req, Interlock il) {\n    return Map.of(\"ok\", true, \"name\", req.str(\"name\", \"world\"));\n  }\n}\n```\n\n**Experience** (`.jsx`) — the default export is a React component; the runtime serves a mounted page. Return data → JSON; return markup via `html(...)` (JS) or `Html.of(...)` (Java) → an HTML page.\n\n## `req` — the request\n\n`req.str(name, def)`, `req.integer(name, def)`, `req.number(name, def)`, `req.bool(name, def)`, `req.list(name)`, `req.has(name)`, `req.header(name)`, `req.cookie(name)`, `req.path()`, `req.principal()`, `req.mark(label)`.\n\n## `il` — capabilities\n\nThe one gateway to anything external:\n\n```\nil.store()                     // this unit's persistent store  (il.store('other-id') for another)\nil.secret('some-key')          // a host-provided secret, or null\nil.call('gift/rank', { ... })  // invoke another unit, get its result\nil.url('gift/rank', { ... })   // build a stable URL by id\nil.log('ranked', n)            // streams back to Interlock while the unit runs\nil.ai()                        // generative AI (see interlock-ai)\n```\n\n## Name a phase in traces (Java)\n\nGive calls a short optional customer tag so a waterfall can show the purpose of the work:\n\n```java\nObject ranked = il.withTag(\"Rank candidates\").call(\"example/rank\", params);\n// Equivalent one-call form:\nObject rankedAgain = il.call(\"example/rank\", params, \"Rank candidates\");\n```\n\n`withTag` returns an independent capability; it does not mutate `il`. Child local and remote\ntraced calls inherit the tag, and a nested `withTag` overrides it for that branch. Parallel\nbranches can therefore use different tags. Tags are trace metadata, never added to prompts or\nunit parameters. Control characters are removed, whitespace is trimmed, and tags are limited\nto 120 characters; an empty tag is untagged.\n\nA tag can appear on several layers of the same operation (unit, agent, inference, model call).\nThose rows describe the operation's execution layers, not additional calls created by the tag.\nExisting callers need no changes; historical traces without a tag remain untagged.\n\n## Per-unit store\n\nValues come back directly — no wrappers. See **interlock-storage** for the full API.\n\n```js\nconst store = il.store();\nstore.put('count', (store.get('count') || 0) + 1);\n```\n\n## Import by id\n\nOnly `.jsx` (and `.java`) units resolve imports by id. A `.jsx` unit is transpiled with an id\nresolver, so a sibling import is a real import:\n\n```jsx\nimport { fmt } from 'lib/money';           // .jsx only: another unit, resolved by id\n```\n\nA plain `.js` unit is evaluated as a standalone ES module with NO resolver, so the same line fails\nat runtime with `Operation is not allowed for: lib/money`. In a `.js` unit, data crosses units and\nbehaviour does not:\n\n```js\nconst money = il.call('lib/money', { cents: 1234 });   // ask the other unit for the value\n```\n\nThat is the pattern in general. `il.call` moves data across a unit boundary; to share behaviour,\nuse a `.jsx` unit (imports by id) or a `.java` unit (imports by package, see **interlock-java**).\n\n## Run and manage\n\n- Run: `https://<your-interlock>/run/{id}` (or `interlock run {id}`).\n- Editor (**Interlock Studio**): `https://<your-interlock>/studio`.\n- CLI: `interlock push`, `interlock run`, `interlock status`, `interlock rollback` — see **interlock-cli**.\n\nClean, id-addressed URLs — no query id, no prefix, no file extension.\n"},{"name":"interlock-java","trigger":"Use when creating or editing any .java unit.","source":"https://docs.interlock.sh/skills/interlock-java.md","available":true,"body":"<!-- GENERATED MIRROR of docs/skills/java.md — edit that file, then re-run\n     ./setup-dev-computer.sh and 'interlock-local sync code'. -->\n\n---\nname: interlock-java\ndescription: Write Java Interlock units using InterlockHandler, Request, Interlock, Json, camelCase unit names, and shared libraries imported by id.\nrequires: basics\n---\n\n# interlock-java\n\nReach for Java when the work is heavy — ranking, math, aggregation, anything CPU-bound. A Java unit compiles once and runs at native speed on GraalVM.\n\n## The handler\n\nThe entry is the single `public class` implementing `InterlockHandler<R>`. The returned value is the response.\n\nHow the runtime actually finds it, in two steps: a regex (`JavaRunner.findClassName`) reads the FIRST `public class` name out of your source to build the binary name it compiles under, then after compiling it walks only the classes attributed to your own source file (`MemoryFileManager.classOrigins`) and takes the first of those that implements `InterlockHandler`. Source attribution first, interface second. Two consequences worth knowing: the entry must be the first `public class` in the file, and a handler that arrives from an imported library unit is never mistaken for this unit's entry. The class name does not have to match the file name, though keeping them the same is the convention everywhere in this doc.\n\n```java\nimport sh.interlock.sdk.Interlock;\nimport sh.interlock.sdk.InterlockHandler;\nimport sh.interlock.sdk.Request;\n\nimport java.util.Map;\n\npublic class GiftRank implements InterlockHandler<Object> {\n  public Object handle(Request req, Interlock il) {\n    int budget = req.integer(\"budget\", 5000);\n    il.log(\"ranking under\", budget);\n    return Map.of(\"budgetCents\", budget, \"ok\", true);\n  }\n}\n```\n\n## Sharing code between Java units\n\nDirectory is package. A unit that declares the package its directory implies —\n`world/FloorCanvas.java` starts with `package world;` — can be imported by any other Java unit\n(`import world.FloorCanvas;`), and units in the same directory reference each other with no import\nat all. The runtime resolves siblings through the project tree and recompiles importers when a\ndependency changes. Rules worth knowing before you lean on it:\n\n- **A unit with no `InterlockHandler` is a library**: importable, not runnable — the Java\n  equivalent of a `constants.jsx` that is not a page.\n- **Types crossing units stay in the host.** Each importer compiles its OWN copy of a shared unit,\n  so a shared class's identity (and its statics) is per-importer. The runtime warns on both\n  hazards; design them out by keeping `il.call` payloads host-typed.\n- **Generated units are not importable** — they stay behind `il.call`, where reduced trust holds.\n- Package-less units keep working exactly as before; they just cannot import or be imported.\n\n**File naming:** author CamelCase in a folder — `examples/GiftRank.java` → id `examples/GiftRank`, reachable at `/run/examples/gift-rank`. One `public class` implementing `InterlockHandler` per unit; helpers can be package-private classes in the same file.\n\n## `Request`\n\n```java\nString s = req.str(\"key\", \"default\");\nint n    = req.integer(\"budget\", 5000);\ndouble d = req.number(\"maxKg\", 0.0);\nList<String> ids = req.list(\"skus\");\nboolean b = req.bool(\"giftWrap\", false);\n```\n\n## `Interlock il` — capabilities\n\n```java\nvar store = il.store();                       // il.store(\"other-id\") for another namespace\nString key = il.secret(\"some-key\");\nvar reply  = il.ai().chat(\"claude:sonnet\", \"one line: why this pick?\");\nObject other = il.call(\"catalog/serve\", Map.of(\"category\", \"jackets\"));\nil.log(\"done\");\n```\n\nStore detail is in **interlock-storage**; AI in **interlock-ai**.\n\n## JSON output\n\nReturn any `Map`, `List`, POJO, or primitive — serialized with `sh.interlock.sdk.Json`. To return server-built markup, return an `Html`:\n\n```java\nreturn Html.of(\"<main><h1>Your kit</h1></main>\");\n```\n\nFor an interactive experience, prefer a `.jsx` unit (see **interlock-frontend**).\n\n## Shared libraries — imported by id\n\nReusable Java lives as ordinary units you import by id. The runtime builds the compile graph from your imports and caches each library; a broken library breaks only its dependents. A library declares the `package` its DIRECTORY implies: the id minus the file name, with no prefix of any kind. `JavaRunner.packageForDir` derives it, and anything else is refused at compile with `unit 'lib/money' declares package 'X' but its directory requires package 'lib'`.\n\n```java\n// id lib/money → directory `lib` → package lib;\npackage lib;\npublic class Money { public static String fmt(long c) { return String.format(\"$%.2f\", c / 100.0); } }\n```\n\n```java\nimport lib.Money;\n```\n\nA root-level id (no directory) lands in the default package, which cannot be imported: put anything shared in a folder.\n\n## Deploy\n\n```bash\ninterlock push code/examples/GiftRank.java         # to dev\ninterlock run examples/GiftRank                    # validate\ninterlock push code/examples/GiftRank.java prod    # to prod (and staging + dev with it)\n```\n"},{"name":"interlock-frontend","trigger":"Use when creating or editing any .jsx or .html unit.","source":"https://docs.interlock.sh/skills/interlock-frontend.md","available":true,"body":"<!-- GENERATED MIRROR of docs/skills/frontend.md — edit that file, then re-run\n     ./setup-dev-computer.sh and 'interlock-local sync code'. -->\n\n---\nname: interlock-frontend\ndescription: Build React or HTML Interlock units with server-side JSX, id-based imports, sibling CSS, and backend calls—without a build step or CDN.\nrequires: basics\n---\n\n# interlock-frontend\n\nAn experience is a unit the runtime serves as a page. Write modern React — `import`/`export`, hooks, JSX — and the runtime transpiles and serves it. **No bundler, no CDN, no loader.**\n\n## The entry: the default export is the root component\n\n```jsx\n// id: gift/finder\nimport React, { useState, useEffect } from 'react';\nimport { Results } from 'gift/results';   // a sibling unit, imported by id\nimport 'gift/styles.css';                  // CSS by id\n\nexport default function App() {\n  const [picks, setPicks] = useState(null);\n  useEffect(() => { il.call('gift/rank', { budget: 5000 }).then(setPicks); }, []);\n  if (!picks) return <p>Assembling…</p>;\n  return <Results picks={picks} />;\n}\n```\n\n### Rules\n\n1. **React is a real import** — provided by the runtime, not a CDN global.\n2. **Import siblings by id** — `import { Results } from 'gift/results'`. No relative paths.\n3. **Import CSS by id** — `import 'gift/styles.css'`.\n4. **Call backends by id** — `await il.call('gift/rank', params)` (mirrors the server-side `il.call`). Java thinks; React renders.\n\n## Plain HTML units\n\nA `.html` unit is served as-is (the Interlock home page and Studio are `.html` units). Use it for static pages or a self-contained app that fetches `/run/{id}` and `/api/code` over HTTP.\n\n## Deploy\n\n```bash\ninterlock push code/gift/finder.jsx            # push the root + its imports (to dev)\ninterlock run gift/finder --open               # open the mounted experience\ninterlock push code/gift/finder.jsx prod\n```\n"},{"name":"interlock-storage","trigger":"Use when a unit needs to read or write persistent data.","source":"https://docs.interlock.sh/skills/interlock-storage.md","available":true,"body":"<!-- GENERATED MIRROR of docs/skills/storage.md — edit that file, then re-run\n     ./setup-dev-computer.sh and 'interlock-local sync code'. -->\n\n---\nname: interlock-storage\ndescription: Use Interlock's per-unit persistent store from Java or JavaScript, including typed reads and explicit cross-namespace access.\nrequires: basics\n---\n\n# interlock-storage\n\nEvery unit gets its own persistent key-value namespace via `il.store()`. **Reads return the value directly** — the storage implementation never leaks. Persistence is provided by the host running the code (the Interlock service, or your own infrastructure when you embed the SDK) — the code just calls the API.\n\n## The Store API\n\n```\nstore.get(key)              → the value, or null\nstore.get(key, Type.class)  → typed value (Java)\nstore.getString/getMap/getList(key)   (Java convenience)\nstore.all()                 → the whole namespace as a map\nstore.put(key, value)       → set/overwrite one key\nstore.putAll(map)           → merge\nstore.remove(key)           → delete one key\nstore.clear()               → wipe\n```\n\n## Java\n\n```java\nvar store = il.store();\nstore.put(\"cart:1\", Map.of(\"sku\", \"JKT-01\", \"qty\", 1));\nMap<String, Object> line = store.getMap(\"cart:1\");   // value, directly\nlong total = store.get(\"cart.total\", Long.class);\nMap<String, Object> all = new HashMap<>(store.all());\nstore.putAll(all);\n```\n\n## JavaScript\n\n```js\nconst store = il.store();\nstore.putAll({ variant: 'B', at: Date.now() });\nconst variant = store.get('variant');   // 'B'\n```\n\n## Cross-namespace\n\nEach unit owns its namespace by id; read another explicitly (coordinate — don't scribble where you don't own):\n\n```js\nconst featured = il.store('catalog-index').get('featured');\n```\n\n## Where it lives\n\nThe store is host-provided through the SDK's `StoreFactory` SPI. On the Interlock service it's the service database; when a company embeds the SDK, the store is **their** infrastructure. Either way the code is identical.\n"},{"name":"interlock-cli","trigger":"Use for all Interlock code work from the shell.","source":"https://docs.interlock.sh/skills/interlock-cli.md","available":true,"body":"<!-- GENERATED MIRROR of docs/skills/cli.md — edit that file, then re-run\n     ./setup-dev-computer.sh and 'interlock-local sync code'. -->\n\n---\nname: interlock-cli\ndescription: Use the Interlock CLI to initialize, sync, inspect, run, fetch, roll back, and compare the one flat code tree across environments.\n---\n\n# interlock-cli\n\nA small Node 18+ CLI that talks to the Interlock service over HTTP (global `fetch`, zero deps).\n\n**Git is the master.** Your repo's `code/` tree is the source of truth for code units; the service only ever receives it via `sync`. Don't treat the server as where code lives — edit files, diff with git, commit, then sync.\n\n## Commands target the Interlock service\n\n`interlock` talks to the Interlock service your project is signed into. The API base resolves from `--api`, `INTERLOCK_URL`, the home profile, then `http://localhost:8090`.\n\n**A code tree owns its credential.** `sync`, `yolo`, `status`, `pull`, and file-based `push` require `.interlock-key`\nin the exact directory containing `code/`. They never\ninherit a key from a parent directory or fall back to the home login. This is a hard pre-network\nfailure: a monorepo root cannot mirror a child API's project, and sibling APIs carry separate keys.\nBind one deliberately with `interlock connect --dir . --project <handle>` from the directory that\nowns a deployable `code/`. Never put a catch-all key at the monorepo root. A key in the exact owner\nfolder is the whole opt-in.\n\nNew keys are versioned JSON in the existing `.interlock-key` filename:\n`{\"version\":1,\"project\":\"…\",\"api\":\"https://interlock.sh\",\"key\":\"…\"}`. The whole file is a\nsecret, mode 0600, and must match `**/.interlock-key` in `.gitignore`. Legacy one-line key files\nremain readable.\n\n> **Only when developing the Interlock platform itself** (inside the `interlock` repo, which self-hosts a local server via `./run-dev-servers.sh`): use **`interlock-local`** — same CLI, separate config (`~/.interlock/local.json`), pointed at `localhost`. In any other project you never need it.\n\n## `sync` — three-way sync between the git tree and the service\n\n```bash\ninterlock sync code              # one-shot three-way sync (see rules below)\ninterlock sync code --dry        # preview exactly what sync WOULD do — changes nothing\ninterlock sync code --prune      # delete never-synced server units instead of pulling them\ninterlock sync code --git-wins --yes     # make the target match Git, including deletions\ninterlock sync code --server-wins --yes  # import target; refuses unless authored files are Git-clean\ninterlock yolo code              # LIVE two-way mirror while it runs: saves upload\n                                 # instantly; server/Studio edits arrive in the tree\n                                 # within ~150ms (SSE push, poll as safety net);\n                                 # deletes go both ways; conflicts surface, never\n                                 # auto-clobber; one yolo per tree (lock).\n```\n\n`yolo` first runs the complete sync plan read-only. If the sides differ in a terminal, it lists the\naffected units and offers `[G]` make the target match Git, `[S]` import the target into the tree,\n`[D]` show a syntax-colored target → local Git diff, `[R]` review the full plan, or `[C]` cancel.\nThe target is the red `---` baseline and local Git is the green `+++` result. A direction requires a typed second confirmation; the\nnormal three-way/CAS sync engine reconciles it, YOLO rechecks byte-for-byte equality, then attaches.\nServer-wins refuses unless authored files are clean in Git. Non-interactive callers mutate nothing\nunless they explicitly pass `--git-wins --yes` or `--server-wins --yes`. Byte-identical stale sync\nrecords self-heal automatically because they do not require an ownership decision.\n\nLayout: **one flat tree**, `code/<id>.<ext>`. There are no environment directories — a unit lives in exactly one place.\n\n**The environment is where you PUBLISH, not where a unit lives**, so it is an argument. Omit it and the CLI asks:\n\n```\ninterlock: which environment?\n  [X] dev\n  [ ] staging  (+dev)\n  [ ] prod  (+dev +staging)   ← the public site\n  (↑/↓ then Enter · or pass --env dev|staging|prod to skip this — scripts and CI should)\n```\n\nPublishing at an environment publishes at every environment **below** it, so what is live in production is never simultaneously stale in dev. The environment you name is the **primary** and is two-way (its Studio edits come down, its conflicts surface); the ones beneath it are push-only publish targets.\n\nIn a script or CI always pass `--env` — with no terminal to ask, the CLI uses `dev` and says so. A `prod` environment you did *not* pick from the menu asks you to type `prod`; `--yes` skips that.\n\n**The sync index** — `code/.interlock-index/<server>/<env>/<id>.<ext>.interlock` (per server: the same tree keeps separate bases for your local server and prod) (**committed to git** for shared servers, like a lockfile — commit it with your code changes; indexes for `localhost-*` servers are gitignored, since a per-developer dev server gets wiped and its versions restart, making those bases churn with no value). Each entry records `{\"version\": N, \"hash\": \"sha256:…\"}` — the server version and content hash as of the last sync. That makes every decision three-way and conflict-safe:\n\n| local vs index | server vs index | sync does |\n|---|---|---|\n| unchanged | unchanged | nothing |\n| changed | unchanged | push (compare-and-swap on the version — a 409 becomes a conflict, never a lost update) |\n| unchanged | changed | pull into the tree |\n| changed | changed | **CONFLICT** — nothing is overwritten; the server copy is written to `<file>.conflict-server` (gitignored) |\n\nDeletes follow the same table. Resolve conflicts explicitly:\n\n```bash\ninterlock sync code --ours ping      # keep the local file, push it over the server\ninterlock sync code --theirs ping    # take the server copy into the tree\n```\n\nNever resolve a conflict on the human's behalf — show them and let them pick.\n\n## `interlock init` — set a project up\n\nRun it from any project directory (e.g. `~/workspace/askcart`):\n\n```bash\ninterlock init\n```\n\nIt: (1) signs you in / creates your account and writes this directory's project-bound\n`.interlock-key` with mode 0600; (2) installs the Interlock skills into every AI coding tool in this\nproject (Claude Code, Codex, Cursor), each prefixed `interlock-` so a re-run wipes and recopies\ncleanly. This is how a host project gets the Interlock skills — it does **not** vendor them into its\nown repo.\n\n## File / id mapping\n\n- `code/{id}.{ext}` — one copy of every unit. `{id}` may include folders. Java files are recommended CamelCase (`examples/HelloJava.java`); ids resolve case-insensitively via a kebab slug.\n- `push`/`sync` infer the id from the path relative to `code/`. The environment never comes from the path — it is the argument you pass.\n\n## Commands\n\n```bash\ninterlock sync code [env] [--dry] [--prune]     # git tree → service (see above)\ninterlock yolo code [env]                       # live two-way mirror while it runs\ninterlock status code [env]                     # what differs from that env, BY HASH\ninterlock push code/gift/finder.jsx [env]       # upsert ONE unit (quick iteration)\ninterlock run  gift/finder [env]                # execute, print output\ninterlock run  examples/hello-java              # kebab id resolves the CamelCase unit\ninterlock get  gift/rank [env] [--save ./r.java]  # fetch source\ninterlock pull [env] [--dir <d>]                # service → code/ (then `git diff`, commit)\ninterlock list [env]                            # list units\ninterlock rollback gift/finder [env] [--to <v>] # restore an earlier version as a NEW version\ninterlock login [--api <url>] [--token <t>]     # store the API base + token\ninterlock logs [--limit 100]                    # recent durable JSONL audit records\ninterlock logs --path                           # print the audit-log directory\n```\n\nInside the Interlock monorepo, `./start-interlock-code-yolo-dev.sh` starts the attached dev listener\nfor the tracked `interlock-java-api/code` tree in the foreground, so YOLO can ask how to reconcile\nwhen its read-only preflight finds drift. It deliberately\nexcludes `interlock-node-api`: that project has no tracked product code tree and its server still\nholds legacy crawler units pending retirement. `--check` validates eligibility without starting.\n\n## Durable CLI audit log\n\nEvery invocation writes an append-only JSONL audit stream under `~/.interlock/logs/`. Sync and yolo\nrecords include the chosen target, tree, safe HTTP metadata, every per-unit reconciliation decision\nand reason, transfers, index changes, conflicts and resolutions, watcher/SSE activity, lock ownership,\nsignals, shutdowns, and crashes. Records retain identifiers, versions, byte counts, status codes, and\ntimings, but never source contents, request/response bodies, tokens, authorization headers, cookies,\npasswords, auth codes, or secret query values.\n\nLogs rotate at 10 MB per segment and are retained for 30 days (up to 500 files). Logging is\nbest-effort: an unwritable log directory cannot break a command. Set `INTERLOCK_LOG_DIR` only when a\ntest or operator needs a different location.\n\nProject management: `projects [new <id>]`, `keys <list|new|roll|revoke> --project <id>` (keys are hashed at rest — the secret is shown once; `roll` rotates), `members <list|add|remove> --project <id>`.\n\n## Typical loop\n\n1. Edit `code/<id>.{js,java,jsx,html,css}` in the repo (or leave `interlock yolo code` running and just save files).\n2. `interlock sync code --env dev` (or `push` the one file).\n3. `interlock check` — author-time syntax gate for `.js` units (`.jsx`/`.java` are\n   guarded server-side: a version that fails to build never displaces the serving one).\n4. `interlock run <id>` / check the browser.\n4. `git diff` → commit → push to GitHub.\n5. Ship: `interlock status code prod` to see what would change, then `interlock sync code prod`.\n   There is no `promote` — you do not move a unit between environments, you publish the tree at the\n   one you mean. If a deploy goes wrong: `interlock rollback <id> prod`.\n\nIf someone edited in Studio instead: `interlock pull`, review with `git diff`, commit what you keep, `sync --prune` to make the service match git again.\n"},{"name":"interlock-ai","trigger":"Use when a unit needs to generate text or media.","source":"https://docs.interlock.sh/skills/interlock-ai.md","available":true,"body":"<!-- GENERATED MIRROR of docs/skills/ai.md — edit that file, then re-run\n     ./setup-dev-computer.sh and 'interlock-local sync code'. -->\n\n---\nname: interlock-ai\ndescription: How to call generative AI from an Interlock code unit via il.ai(). Provider-agnostic; the host wires the provider. Use when a unit needs to generate text (and, where the host supports it, media).\nrequires: basics\n---\n\n# interlock-ai\n\nGenerative AI is a capability on `il`: `il.ai()`. The provider is host-provided through the SDK's `Ai` SPI, so a unit stays provider-agnostic — name a model as `\"provider:model\"`.\n\n```java\nString line = il.ai().chat(\"claude:sonnet\", \"One line: pitch this jacket for Iceland in October.\");\nboolean live = il.ai().live();   // false when the host has no provider configured (offline stub)\n```\n\n```js\nconst line = il.ai().chat('claude:sonnet', 'One line: why this pick?');\n```\n\n## Name the AI phase (Java)\n\n```java\nString explanation = il.ai().withTag(\"Explain choices\").chat(Models.MEDIUM, prompt);\n```\n\nThe optional tag names the phase in traces without changing the prompt, model, or response.\n`withTag` returns an independent AI capability, so concurrent calls can carry different tags.\nChat and conversation calls inherit a tag from `il.withTag(...)`; a nested AI `withTag(...)`\noverrides it. The same tag may appear on the agent, inference, and model-call rows for one\nlogical operation.\n\nTags are trimmed, control characters are removed, and the length is limited to 120 characters.\nEmpty tags and historical traces without tags remain untagged. This scope applies to chat and\nconversation calls only; durable `submit` and media calls do not propagate it.\n\n## Naming a model\n\nName a model with the SDK's `Models` constants (Java: `sh.interlock.sdk.Models`; the strings are the same on every SDK). A price tier lets Interlock choose the current best fit for that spend, a specific name pins one model:\n\n- Tiers: `Models.CHEAPEST` · `CHEAP` · `MEDIUM` · `EXPENSIVE` · `MOST_EXPENSIVE` (`\"cheapest\"` … `\"most-expensive\"`).\n- Managed models by name, e.g. `Models.GEMINI_3_1_PRO`, `Models.GROK_4_5`.\n- **Self-hosted:** `Models.QWEN_3_6_35B_A3B` (`\"qwen3-6-35b-a3b\"`) — Qwen3.6-35B-A3B Q8_0, run by a worker **your project owns** (`interlock-task-client` beside a `llama-server`, e.g. on a 64 GB Mac Studio). Naming it routes the call to that worker; nothing else is ever substituted for it. With no online worker the call is refused (`NO_ELIGIBLE_INFERENCE_WORKER`, HTTP 409) and nothing is billed. It is metered per token like the managed models. A bare `\"qwen\"` is refused with the exact name to use — there is one deployment, and the price names it.\n\n```java\nString s = il.ai().chat(Models.QWEN_3_6_35B_A3B, \"Summarise this in one line: \" + text);\n```\n\n`GET /api/models` lists the catalog with `servedHere` answered for YOUR project (a self-hosted model is served when one of your workers is online).\n\n## Reuse a successful result\n\nCompleted-result caching is explicit. Give the job a namespace/version and a TTL; Interlock folds\nthe complete effective request into a private project-scoped fingerprint, so the string is never\nthe whole identity and another project can never hit it.\n\n```java\nAi.ChatOptions options = new Ai.ChatOptions(Models.CHEAP, Ai.RoutePolicy.MANAGED_ONLY)\n        .cache(\"product-summary:v2\", 3_600);\nString summary = il.ai().chatWithOptions(options, prompt);\n```\n\nOnly validated terminal text/structured answers without tools or images are stored. The first\nrequest invokes and bills the model; an exact hit returns the prior result without a model call or\nmodel charge. `/ai` shows requests, actual model invocations, result hits/misses, provider\nprompt-cache tokens, latency, billed spend, and avoided work separately.\n\n## Host-provided\n\nThe Interlock service ships a minimal offline stub by default so units run without credentials. A host that embeds the SDK wires its own `Ai` implementation (its own keys, its own provider), and the same unit code calls it unchanged. Keys never live in unit source — resolve them host-side via `il.secret(...)` or the host's `Ai`.\n"}]}