Skip to content

feat(local): Stunt Double Studio as a standalone local app - #47

Merged
mrprkr merged 5 commits into
mainfrom
feat/studio-standalone
Nov 3, 2026
Merged

mrprkr merged 5 commits into
mainfrom
feat/studio-standalone

Conversation

@mrprkr

@mrprkr mrprkr commented Nov 3, 2026 •

Copy link
Copy Markdown
Contributor

Summary

npx stuntdouble studio (or npx stuntdouble in a terminal) starts Studio on 127.0.0.1 and opens the browser. Studio is a local copy of the core capabilities: create and edit actors and checklists, run them with an AI actor in a local Chrome, watch and stop a run, review results.

  • Local runs use the person's own connection: an Anthropic API key, or an OpenAI-compatible endpoint (OpenAI, OpenRouter, Ollama, LM Studio). Keys live only in ~/.stuntdouble/credentials.json (0600) or the environment. The page sees only a status.
  • Cloud runs are available after stuntdouble login (OAuth 2.1, PKCE, loopback redirect), for workspaces that allow them. The plan gate reads a plan field when present, and otherwise defers to the server's allowance check.
  • Same results as a Claude Code run: the same LocalSession, brief, citation check, scoring and file formats. run.json gains optional runner.model and runner.usage.
  • Policy line: the MCP path makes no model calls and never spawns claude. test/no-model.test.ts enforces that the MCP path's import graph never reaches the model, credentials, cloud or Studio modules, and that no model SDK is imported. A Claude subscription is never used.

Verification

  • pnpm -r type-check, pnpm lint, pnpm format:check, pnpm build
  • pnpm --filter stuntdouble test: 67 pass, including the Studio end-to-end test (headless Chrome, a scripted OpenAI-compatible server) and the mocked cloud sign-in and push
  • Real run on local Ollama (qwen3.5:9b): correct verdicts on all three checks
  • Viewed the app in light, dark and at 390 px

Risk and rollout

  • Minor version change, behind a new command. The MCP path and existing files are unchanged.
  • Follow-ups for the monorepo: list_workspaces should return each workspace's plan, and docs/architecture/local-suite.md needs a Studio section.
  • No way to cancel a cloud run yet, so Studio hides Stop for cloud runs.
  • Merge note: this and feat/run-feedback both touch src/run.ts and src/ui.html.

A model on the person's own connection (an Anthropic API key or an
OpenAI-compatible endpoint) drives the same LocalSession as the MCP server,
through the same browser tools, evaluate_check and finish_run, so the files
and verdicts match. Step and token limits, usage and cost, Stop.

run.json's runner gains optional model and usage. no-model.test.ts now proves
the MCP server's import graph never reaches the model client, the keys or a
process, and that only the runner imports model.ts.

Signed-off-by: Michael Parker <michael@stuntdouble.io>
OAuth 2.1 with PKCE and dynamic registration against the hosted MCP, back to a
loopback address, with the token in ~/.stuntdouble/credentials.json. A cloud
run pushes the project, actor and checklist through the hosted tools, writes
the ids back, polls every 30 seconds and pulls the run back. A plan field
gates when present; otherwise the server's refusal is relayed with the
billing link.

Signed-off-by: Michael Parker <michael@stuntdouble.io>
stuntdouble studio (the default in a terminal) opens the viewer grown into an
app: onboarding, actor and checklist editors validated by the run's schemas,
Run locally or in the cloud, the live run with its usage and Stop, and
settings for the connection and the Stunt Double sign-in. The viewer's
security model holds, with this server's Origin and a JSON body required on
every write. Adds run, login and logout to the CLI.

Signed-off-by: Michael Parker <michael@stuntdouble.io>
Signed-off-by: Michael Parker <michael@stuntdouble.io>
Comment thread packages/local/test/studio.test.ts Fixed
…ck test

CodeQL flagged /<script>/ as a filter that misses <SCRIPT>.

Signed-off-by: Michael Parker <michael@stuntdouble.io>
@mrprkr
mrprkr merged commit 9b3e69b into main Nov 3, 2026
5 checks passed
@mrprkr
mrprkr deleted the feat/studio-standalone branch November 3, 2026 20:18
@github-actions github-actions Bot mentioned this pull request Nov 3, 2026

@graphify-labs graphify-labs Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Graphify reviewed this change.

Worth a look — the grounded gate found no coupling regressions or blocking issues, but 5 advisory finding(s) below merit a look before merge.

Not checked: tests were not run; no formal proof of the changed code.


Graphify review — findings

Adds Stunt Double Studio, a local app opened with npx stuntdouble studio for creating and editing actors and checklists, running one with an AI actor on a user-configured Anthropic or OpenAI-compatible connection, and reviewing the verdicts. A Studio run reuses the MCP run end to end, so it writes the same files, stops at 80 calls or 3M tokens, honours Stop, and adds optional model and usage to run.json's runner; mcp, view and doctor are unchanged and make no model calls. Adds login, logout and run <checklist> --cloud, which sign in via OAuth with PKCE, push the project, poll the hosted run and pull the result back, so the session and credentials handling in cloud.ts and the cli.ts command dispatch carry the main risk.

Worth a look

  • Check IDs mapped to keys by position, not by identity — packages/local/src/cloud.ts · Escalate · medium
    • NOT verified (no proof, no reproducing execution) — needs human review
  • In-flight token refresh can re-create credentials after logout — packages/local/src/cloud.ts:99 · Escalate · medium
    • NOT verified (no proof, no reproducing execution) — needs human review
  • Starting a sign-in wipes the existing session before the new one succeeds — packages/local/src/cloud.ts:140 · Escalate · medium
    • NOT verified (no proof, no reproducing execution) — needs human review
  • --help opens Studio in an interactive terminal instead of printing usage — packages/local/src/cli.ts:180 · Escalate · medium
    • NOT verified (no proof, no reproducing execution) — needs human review
  • Local run failure leaves the session open, so the CLI can hang — packages/local/src/cli.ts:150 · Escalate · medium
    • NOT verified (no proof, no reproducing execution) — needs human review

Review partial — this diff was larger than one review pass covers, so later files were not reviewed; some findings may be missing.

Analysis details — impact, health, verification

Impact & health

Graphify review

Impact — 387 functions depend on the 268 functions this change touches.

Health — this change adds coupling hotspots:

  • new: startCloudRun() — 2 callers, 11 callees
  • worse: createServer() — 2 callers, 8 callees
  • worse: resolveScoreBands() — 5 callers, 3 callees
  • worse: isLive() — 4 callers, 3 callees
  • new: loop() — 1 callers, 10 callees
  • new: run() — 1 callers, 8 callees
  • new: renderActorEditor() — 1 callers, 8 callees
  • new: renderChecklistEditor() — 1 callers, 8 callees
  • …and 2 more — each is listed as a finding

Verification — 387 functions in the blast radius were not formally verified this run (proofs are advisory here).

Gate & verification

graphify gate

PASS — objectively clean (no health regressions, tests not run — proofs not run this pass (advisory)). Grounded, not self-assessed.

Advisory (not blocking):

  • verification_scope: 377 function(s) in the blast radius were not formally verified this run

Risky patterns (advisory)

  • medium no-unbounded-fetch in packages/local/src/studio.js:555: unbounded read (no limit/bound): for (const row of rows) row.item = row.read();

Docs that may be stale (advisory)

…and 10 more.

· 7 grounded finding(s) anchored inline below; 3 more finding(s) on lines outside this diff (see the check run).

Comment thread packages/local/src/cli.ts
);
}

async function run(

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Health regression — run()

fans out to 8 callees (efferent coupling).

Grounded coupling-delta finding (deterministic), not an LLM guess.

* cloud ids already in the files, start a run and pull it back as
* `runs/<stamp>-<checklist>-cloud/run.json` beside the local runs.
*/
export async function startCloudRun(options: {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Health regression — startCloudRun()

fans out to 11 callees (efferent coupling).

Grounded coupling-delta finding (deterministic), not an LLM guess.

return { runId, done };
}

async function loop(input: {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Health regression — loop()

fans out to 10 callees (efferent coupling).

Grounded coupling-delta finding (deterministic), not an LLM guess.

}

// ── Onboarding ────────────────────────────────────────────────────────────
function renderWelcome() {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Health regression — renderWelcome()

fans out to 7 callees (efferent coupling).

Grounded coupling-delta finding (deterministic), not an LLM guess.

const tidy = (object) =>
Object.fromEntries(Object.entries(object).filter(([, v]) => v !== undefined && v !== ''));

async function renderChecklistEditor(slug) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Health regression — renderChecklistEditor()

fans out to 8 callees (efferent coupling).

Grounded coupling-delta finding (deterministic), not an LLM guess.

];
}

async function renderActorEditor(slug) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Health regression — renderActorEditor()

fans out to 8 callees (efferent coupling).

Grounded coupling-delta finding (deterministic), not an LLM guess.

['LM Studio', 'http://localhost:1234/v1', ''],
];

async function renderSettings() {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Health regression — renderSettings()

fans out to 7 callees (efferent coupling).

Grounded coupling-delta finding (deterministic), not an LLM guess.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants