Skip to content

feat(reactor-devtools): export a run as one self-contained HTML file (gist view) - #140

Draft
rawwerks wants to merge 1 commit into
mainfrom
prose-contributor/devtools-run-export
Draft

feat(reactor-devtools): export a run as one self-contained HTML file (gist view)#140
rawwerks wants to merge 1 commit into
mainfrom
prose-contributor/devtools-run-export

Conversation

@rawwerks

Copy link
Copy Markdown
Contributor

Closes #139

Summary

  • New reactor-devtools <state-dir> --export <file.html> flag: renders a saved run as one self-contained HTML file — the gist view: copyable .prose.md contract on top, expandable receipt timeline in the middle, output assets below.
  • --source <file-or-dir> embeds the contract block (opt-in, since reactor state-dirs carry no source snapshot today; state-dir root.prose.md/sources/ auto-detect is wired for when they do). --title sets the page title.
  • Output assets: JSON pretty-prints; .html/.svg assets get a live preview in a no-token sandboxed iframe (scripts blocked) plus an open in tab button on the collapsed summary row (a blob: URL from the embedded source, detached via noopener).
  • Exit-code contract mirrors --describe: clean or legitimately-empty chain → 0; detected tamper → 1, with the file still written carrying the tamper verdict. Existing targets refused without --force.
  • Pure formatter over the existing data layer (openStateDirbuildSnapshot / describeStateDir / readNodeWorldModel); re-derives nothing, adds no dependencies, changes no existing public surface.

Use Case / Run Evidence

agent-experience — this came out of an agent session exploring how prose logs runs and asserts output-type contracts. The friction: a run's trust story (contract → chain-verified receipts → outputs) is only tellable to someone with the package installed and the state-dir on disk. The unit of sharing is a claim about the run, never the run itself. Full motivation in the linked issue.

Demonstrated on the shipped masked-relay fixture paired with its skills/open-prose/examples/masked-relay/src/ contracts (12 sources, 77 receipts, 13 node outputs), and on a minimal chain-valid state-dir whose node publishes report.html (the "prose in → HTML out" story).

Design Boundary

This belongs in packages/reactor-devtools (the keyless replay viewer): it is a presentation of what the harness already persists, exactly like the SPA and --describe. It does not touch the SDK, the reactor CLI, or VM/skill semantics. The one adjacent gap it names but does not implement: reactor state-dirs don't snapshot authored sources (the VM run-envelope convention) — that belongs in compile/run, and this PR's --source flag plus auto-detect bridges until then.

Examples

reactor-devtools --example masked-relay --export run.html \
  --source skills/open-prose/examples/masked-relay/src \
  --title "Masked Relay — customer-signal fan-out"
# → reactor-devtools: exported 12 source file(s) + the run artifact + outputs

Without a contract source, the export states the absence honestly and stdout hints at --source; it never fabricates a contract.

Testing

pnpm -C packages/reactor-devtools test (typecheck + node --test, keyless/offline by nature — no model calls in this package): 108/108 pass on this branch in a clean worktree based on current origin/main. 12 new tests in src/export/export.test.ts:

  • gist-view section ordering and content on the shipped masked-relay fixture (CLI-level, spawns dist/cli.js like cli.test.ts)
  • --source embedding with HTML-escaping assertions (hostile <script> payload never appears live)
  • end-to-end .html asset path: builds a chain-valid state-dir from real SDK primitives (commitPublishedcreateReceiptappendReceipt), asserts the sandboxed iframe, the summary-row open button, and the noopener blob handler
  • tamper path: edits a persisted receipt without re-stamping its content_hash → exit 1, file still written carrying the chain ✗ TAMPERED verdict
  • --force overwrite contract, ambiguous-flag refusals (--export + --describe/--copy-to, --source without --export), missing flag values (--export with no value never falls through to the blocking server), missing or empty --source fails loudly with no half-export
  • unit: collectSources (file / dir / src/ child / index.prose.md ordering), self-containment (no external src=/href= URLs)

Also verified in a real browser (Chromium via Playwright, over both http:// and file://): copy button puts the actual prose source on the clipboard, open-in-tab renders the artifact in its own tab without toggling the accordion, zero console errors, zero external requests; a hostile asset script executed nowhere (top window and sandboxed iframe both clean, zero dialogs, zero network).

An adversarial pre-PR audit pass reviewed the diff and exercised the binary; its actionable findings are folded in: missing-value flag refusal, loud empty---source failure, basename-only state-dir in the shareable artifact (no absolute local paths), escaped receipt status, accurate script-inventory wording in docs, and the tamper-path test above.

Residual Risk / Follow-ups

  • Open-in-tab runs the artifact live (isolated blob origin, noopener) — a deliberate, documented trust boundary distinct from the always-sandboxed inline preview; an explicit user click, same model as viewing a raw gist file.
  • Raw receipts are embedded up to 4 MB, then omitted with a named note pointing at the state-dir's receipts.json — no silent truncation.
  • Output assets are not size-capped, and a previewable asset embeds twice (srcdoc + view-source) — fine for receipt-scale world-models; a cap with an honest omission note is a follow-up if large artifacts show up in practice.
  • Follow-up (out of scope): snapshot root.prose.md/sources/ into the state-dir at compile/run so every export carries its contract without --source.
  • Follow-up (out of scope): a hosted/served variant of the same render (run permalinks).

🤖 Generated with Claude Code

…HTML file

The gist view: the copyable .prose.md contract on top, the receipt
timeline (frames, per-node chain-verify, cost by surprise-cause, raw
receipts) expandable in the middle, and each node's published
world-model assets below — JSON pretty-printed, .html/.svg previewed in
a no-token sandboxed iframe with an open-in-tab blob button on the
collapsed summary row.

Pure formatter over the existing data layer; no new dependencies. Exit
codes mirror --describe: tamper exits 1 with the file still written
carrying the verdict. --source embeds the contract until state-dirs
snapshot authored sources (auto-detect already wired).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@rawwerks rawwerks added the enhancement New feature or request label Jun 12, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

A reactor run should be shareable as a single self-contained HTML file (gist view)

1 participant