An independent cold build of v0.7.0 — fresh clone, documentation only, no contact with me — found three things. The first is the one that matters and it is the worst class this repository deals in. FOCUS AND DRIVE ANSWERED WITHOUT BEING ADVERTISED. Both verbs worked, both were documented, and neither namespace appeared in /api/contract's capabilities. But contract-meta.md tells every consumer that each namespace it uses must appear there and to CHECK BEFORE USE — so an agent doing exactly what the contract instructs concluded the features were absent. A verb that answers without advertising is worse than a missing verb: the documented path leads to the wrong answer, and the consumer has no reason to doubt it. This was mine, and it was pattern-matching rather than reasoning. When drive shipped I checked whether focus had declared a capability, found none, and made drive match it for consistency. Consistency with an omission. Read and write are separate entries because they are separately true, which is what files.read has always meant, and it lets a runtime offer read-only focus honestly instead of claiming a namespace it half-implements. TWO GUARDS HAD TO BE LOOSENED, AND THAT IS THE INTERESTING PART. Both asserted the capability set was EXACTLY equal to a baseline. True when written, wrong as a rule: capabilities is the feature-detection list, so it must grow when a namespace ships or it can never advertise anything. Equality there would have blocked this very fix — a guard that reads as strictness and is actually a rule that no feature may ever be declared. Both now assert the additive property that was meant: capabilities MAY GAIN, NEVER LOSE, since a consumer gates on them. Verified by planting a removal and watching both fail. One of those loops was written backwards first — iterating the current runtime and requiring the baseline to contain each, which reads identically and asserts the opposite. Caught because the suite failed, not because it looked wrong. A guard now compares the documented capability list against the served one, and a second asserts every namespace with a served route is advertised. Neither existed: measured first, the parity suite passed at 4/4 with a capability deleted from the documented example, because the comparator descends objects and capabilities is an array. Capabilities are shape, not data — the rest of the arrays in that response are counts and errors and differ by design — so they get a named check rather than a general array descent. Also: POST /api/focus's response shape is now documented rather than inferred from a readback, and the generated project's run snippet no longer omits --surfaces, without which the new surface never reaches the rail and looks like a broken row. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
@openrig/create-studio
The create-app on-ramp: scaffold a working openrig studio surface, wired to contract v0.1, in one command.
Status of the public command
The intended front door is:
npm create @openrig/studio my-surface
npm resolves that to the package @openrig/create-studio and runs its bin.
That command does not work yet. This package is unpublished and its final
name is a deferred decision, so there is nothing on the registry to resolve.
Publishing is a metadata step — flip private, settle the name, publish — and
changes nothing about the code below.
Running it today
Directly, from the repository root:
node create-studio/index.mjs my-surface
Or through the real npm resolution path, against a local pack — same bin, same entrypoint, no registry:
npm pack ./create-studio
npm exec --package=./openrig-create-studio-0.1.0.tgz -- create-studio my-surface
Options
create-studio <name> [--dir <parent>] [--glyph <char>] [--hint <text>]
| Option | Meaning |
|---|---|
<name> |
Project directory and surface id. Lowercase letters, digits and dashes. |
--dir |
Where to create the project (default: current directory). |
--glyph |
One-character rail glyph (default ◆). |
--hint |
One-line rail tooltip. |
What it emits
my-surface/
surface.html the surface — one self-contained page, no build step
surfaces.row.json the schema-valid manifest row that registers it
README.md how to install it, and where the contract docs are
The emitted surface uses only stable-classified contract items: the
/api/contract boot check, /api/factory/state, the /api/events change
signal (refresh-only, with the documented degraded/polling fallback), and the
open-agent shell message. Nothing marked provisional or runtime-internal
is generated.
How it refuses
Every check runs before anything is written, so a rejected run leaves the filesystem untouched:
- names must be a single path segment — no traversal, no absolute paths, no
~ - no leading dots, no reserved names, 64 characters max
- lowercase letters, digits and dashes only
- an existing non-empty destination is refused
Generation stages into a sibling temp directory and lands with one atomic rename, so an interrupted run cannot leave a half-written project that looks usable.
OPENRIG_STUDIO_CREATE_FAIL_AFTER=<n> forces a failure after n files have
been staged. It exists so the no-partial-output guarantee can be demonstrated
rather than asserted; it is inert when unset.
Option values are escaped per output context
--hint takes arbitrary text and --glyph takes any single character, so both
can carry quotes, backslashes, newlines or markup. Each output context handles
them differently and the generator treats them separately:
- JSON — the manifest row is built as an object and serialized. It is never
string-substituted into a JSON literal, because that is what previously let a
quoted hint emit a file
JSON.parserejects while the CLI reported success. - HTML — every interpolated value is escaped, so a hint containing markup renders as text rather than becoming an element.
- JS — the one value crossing into a
<script>is JSON-encoded, not HTML-escaped; escaping there would put a literal"into code. - Markdown — deliberately not escaped, because escaping inside a code span is wrong. Safety rests on the project-name rules, and the generator asserts that invariant rather than assuming it.
Tests
npm test # from the repo root, or from create-studio/
Zero dependencies — node:test only. The suite covers generation, every
admission boundary, no-partial-output on forced failure, schema shape and
stability class of the emitted row, the hostile-value matrix for every
user-controlled field, and the packed-tarball bin path.