Files
openrig-studio/create-studio
mvschwarzandClaude Opus 5 9b322f6ae2 Advertise focus and drive, so feature detection stops lying
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]>
2026-08-04 20:47:26 -07:00
..

@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.parse rejects 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 &quot; 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.