@caryhu/codemine-forge
v1.0.0
Published
Browser-side runtime layer for Codemine demos.
Readme
Codemine Forge
Codemine Forge is the browser-side runtime layer for Codemine demos.
It is not a full Node.js emulator and it is not a backend runner. Its job is to make browser-hosted projects feel like a small StackBlitz-style environment by owning the virtual workspace, simulated terminal commands, dependency resolution, browser bundling, and preview runtime.
Scope
Forge owns these capabilities:
- Virtual file system for project files, snapshots, package cache, and build cache.
- Fake terminal with a controlled command registry.
- Browser bundling through
esbuild-wasm. - TS, JSX, TSX, CSS, JSON, and static asset bundling.
package.jsondependency parsing.- Import map generation or bundle alias generation.
- Simulated
forge start(install → build → dev) experience, with the individualforge run install,forge run build, andforge run devcommands still available. - Vite-like dev server compatibility for preview workflows.
Forge does not own:
- User accounts, billing, permissions, or product navigation.
- Server-side execution, containers, SSH, or unrestricted shell access.
- Full npm CLI compatibility.
- Native Node modules,
node-gyp, install scripts, or arbitrary process spawning. - Generated frontend output in the main Codemine repository.
Runtime Shape
Codemine host app
Editor / file tree / demo page
|
v
Codemine Forge
Virtual FS
Fake terminal
Package resolver
Build worker
Dev server facade
|
v
Preview iframe
Runs compiled browser modulesForge should expose a small API to the host application. The host application can render its own editor, file tree, tabs, and buttons, while Forge provides the runtime behavior behind those controls.
Document Map
- Architecture
- Virtual File System
- Package Resolution
- Build Pipeline
- Caching
- Terminal Commands
- Vite-like Dev Server
- Demo Debug Bridge
- Behavior Verification
- Sample Failure Triage
- Sample Defect Handoff List
- Angular Preview Compatibility
- Release Validation
- Pi Agent Workflows
- Roadmap
Demo
Start the interactive demo with Vite:
pnpm run demoThe demo defaults to http://127.0.0.1:4175/ and accepts PORT and SAMPLE
environment variables.
AI-assisted debugging
The demo relays its browser-side state (samples, dev sessions, build summaries, diagnostics, and console output) to the Vite dev server. A terminal agent can read it — and, through a whitelisted control channel, drive it — without touching the page:
pnpm run debug:forge state
pnpm run debug:forge console --level error --limit 50
pnpm run debug:forge events --type sample:error
pnpm run debug:forge run "forge start"An MCP client can use the bundled stdio server instead:
pnpm run debug:mcpSee Demo Debug Bridge for the full endpoint,
control-channel, MCP tool, and window.__forgeDebug surface. The project
.pi/mcp.json, the codemode setup, and concurrent catalog audits are described
in Pi Agent Workflows.
Bounded AI repair loop
The repair loop composes the behavior runner with an external coding agent. It
requires an already running demo, writes each iteration's report and
repair-context.json under .tmp/forge-repair-loop/, and stops after a bounded
number of iterations or when a failure needs human review. The agent command is
run from the repository root with FORGE_REPAIR_CONTEXT pointing at the JSON
context file.
# Validate the selection without connecting to a demo
pnpm run debug:repair-loop -- --dry-run --sample pure-framework/react-tsx
# The command may be a wrapper around OpenHands, SWE-agent, or another coding agent
$env:FORGE_REPAIR_AGENT_COMMAND = 'node scripts/my-coding-agent.mjs'
pnpm run debug:repair-loop -- `
--base-url http://127.0.0.1:4175 `
--sample pure-framework/react-tsx `
--scenario counter.increment-twice `
--max-iterations 3Omit --scenario and choose autonomous exploration in the Demo panel, or use
the CLI directly:
pnpm run debug:repair-loop -- `
--base-url http://127.0.0.1:4175 `
--mode explore `
--sample pure-framework/react-tsxExploration reads the rendered preview, identifies common controls, performs a
small bounded set of real browser actions, and records before/after text,
screenshots, a video of the run, console errors, and action results. Only
visible, enabled controls are offered; editable text fields are filled, while
non-editable inputs (Wijmo/InputMan combo boxes, pickers) are clicked to open
their list, and each interaction is capped at 5s. When a modal is open only its
own controls are offered (a covered background control cannot be clicked), and if
an unknown overlay still blocks an action, Escape is pressed and the action is
retried once before it is recorded as failed. Canvas-rendered surfaces are not
skipped either: a SpreadJS workbook's cells are exposed as targets through the
public GC.Spread.Sheets API (click the cell, type, Enter), and a bare canvas
falls back to a center click. The video is written as
exploration-video.webm next to exploration-report.json (both
fields: video relative to evidenceDir, videoPath absolute); set
FORGE_EXPLORE_VIDEO=0 to skip the encoding cost. It is a discovery signal and
can produce a repair hypothesis; a stable scenario is still the stronger
regression contract for a feature that has been confirmed.
The loop does not start or restart the demo, does not approve arbitrary shell
commands, and treats infrastructure and unsupported-probe failures as
needs-human-review instead of asking the agent to guess.
Jev AI judge and exploration policy (optional)
The autonomous explorer can call a self-hosted Jev decisions model for two advisory capabilities:
- A - judge. After each sample it asks Jev to classify the run
(
healthy/real_defect/env_noise/harness_flake/render_incomplete), score severity, and pick the next step. The verdict is written toexploration-report.jsonasai. The repair loop uses it only to narrow the automated scope: environment/tool problems or an explicitneeds_humandecision are never handed to a coding agent. - B - action policy. With
FORGE_JEV_ACTION=1Jev plans the preview interactions instead of the explorer clicking the first N controls in DOM order. Because Jev answers a set of named questions per request, one call returns an ordered group ofaction_1 … action_nchoices; the explorer executes that group, re-lists the controls, and plans the next group until the step budget runs out. When a goal, description or source is available the model first picks which behavior to verify, so the plan is targeted at the sample's intent rather than at whatever control is first. Supply that grounding with--goal,--description-file,--docs-dir(a README / description checkout such as the onboardingSitesarchive, resolved per sample by keyword) or--docs-map(a curated{ sampleId: docPath }JSON, which wins over the keyword lookup and is the reliable option when the archive's folder taxonomy has drifted).FORGE_JEV_PLAN_ACTIONScaps the actions per call (default 4). If the model call fails, the remaining controls are still exercised deterministically, so a model outage cannot silently reduce coverage. The own actions are executed in the explorer's isolated browser (that is where screenshots and console evidence come from) and mirrored onto the connected demo tab through the debug bridge, so the person watching the demo sees each AI action happen in the real UI. SetFORGE_EXPLORE_MIRROR=0to keep exploration headless-only.
Both are off by default and only read the key from the environment; a key is never written to disk, embedded in an artifact, or printed in an error.
Every call is streamed as a jev:* event (jev:judge:started,
jev:judge:finished, jev:goal:decided, jev:plan:decided,
jev:action:failed, …). The repair
monitor at /repair.html draws the batch as a flow chart: one lane per sample
with a node per stage (排队 → 探索 → AI 动作 → AI 判定 → 结果). Hovering a lane opens
a status card with the verdict, reason code and recent event timeline, so the
model's decisions are visible instead of being buried in
exploration-report.json. Samples resumed from a previous run are skipped, so
their card replays that run's AI verdict and the controls the AI touched last
time. A run that passes the automated checks but that Jev still classifies as a
real_defect (or an incomplete render) is shown as AI 标记 instead of
passed, and is re-run next batch so a defect the checks missed cannot hide
behind a green result. A run whose documented goal action could not be
performed at all (for example a control that is not visible) is shown as
动作失败 and re-run too: an incomplete verification of a claimed behavior is
not a pass, matching the repo's "untested does not count as success" rule. An
exploratory click that is not bound to a documented action and could not be
executed is recorded on the report but does not flag the run, because the AI
verdict already decides whether the unexercised extra interaction hides a
defect. When a batch resumes from
progress.json, those samples are marked amber as soon as the previous verdicts
load, so they stand out from the already-passed gray lanes instead of waiting
for the sequential run to reach them. The header shows how many samples Jev
judged and the verdict breakdown. The raw JSON event log stays available behind
the footer toggle.
Exploration report page
Each sample gets its own self-contained index.html beside its
exploration-report.json (samples/<id>/index.html), regenerated whenever the
sample runs or is resumed. It is grounded in the sample's own forge.goal.json:
a Sample 目标 panel shows the description, the documented behaviors and their
expectations, the source documentation and the frameworks, and each executed
step is tagged with the goal it served, its position in the AI's plan and (when
the control matches a documented action) the outcome that action was meant to
prove. When the exploration recorded its own decisions (goal choice and action
plan groups), an AI 决策轨迹 list replays them above the steps. The Jev
verdict with severity / next step sits beside the recorded
exploration-video.webm, and the page links to that sample's
exploration-report.json, before.png and after.png. The debug bridge serves
the audit tree at /__forge/repair/report/..., so clicking a lane in the monitor
opens the page;
the file also opens directly from disk with no server.
$env:FORGE_JEV = '1' # enable the judge (A)
$env:FORGE_JEV_ACTION = '1' # also let Jev pick actions (B)
$env:JEV_BASE_URL = 'http://10.32.0.160:8080/v1/systemone'
$env:JEV_MODEL = 'jev-1.13.0'
$env:JEV_API_KEY = '<key>'
$env:JEV_MAX_RETRIES = '5' # optional; retries per request (default 5)
$env:JEV_RETRY_DELAY_MS = '3000' # optional; delay between retries (default 3000)Or put the same keys in a gitignored .env (repository root or demo/); the
demo's Vite config loads it for the server process, FORGE_JEV=1 included:
FORGE_JEV=1
FORGE_JEV_ACTION=1
JEV_BASE_URL=http://10.32.0.160:8080/v1/systemone
JEV_MODEL=jev-1.13.0
JEV_API_KEY=...
JEV_MAX_RETRIES=5
JEV_RETRY_DELAY_MS=3000A real environment variable always wins over .env, and the repo-root .env
wins over demo/.env. The demo prints [demo] Jev judge enabled/disabled at
startup, and the repair monitor's first line is a [jev] {"type":"jev:config"…}
record.
Jev is not an OpenAI chat model: it answers typed questions (noul, choice,
score) about an arbitrary JSON state. The client lives in
scripts/agent/jev-client.mjs and the Forge question sets in
scripts/agent/jev-questions.mjs. Transport errors, request timeouts and
retryable HTTP statuses (408/425/429/5xx) are retried 5 times with a 3s gap
before the request is reported as failed (JEV_MAX_RETRIES,
JEV_RETRY_DELAY_MS).
Per-demo goal files (forge.goal.json)
A demo is the directory that holds its per-framework samples (angular/,
react/, vue/, purejs/) and one shared README. That directory carries a
single forge.goal.json describing what the demo is meant to demonstrate
(intent), what to check (goals[].expect), and how (goals[].actions) plus
provenance (docs, source, frameworks[]). The explorer auto-loads it when
no explicit --goal / --description-file / --docs-dir grounding is given,
looking in the framework sample directory first and then one level up at the
demo directory, so the AI plans targeted actions instead of walking the DOM.
Generation is one-time and then incremental:
# 1. scaffold: intent + goals; syncs each demo's README/des.md into the demo dir
pnpm run goals:generate -- \
--docs-dir "wijmo=D:/Products/Wijmo/source-code/wijmo/monorepo_migration/wijmo-sample;D:/Archive/Onboarding Related/Onboarding Site/Sites"
# once a demo is synced, --docs-dir is optional (the synced bundle is the grounding)
pnpm run goals:generate -- --only <sampleId>
# 2. enrich: an AI agent fills goals[].actions from the README + sample source
pnpm run goals:generate -- --docs-dir <archive> --agent pi --limit 20
# or a custom agent (FORGE_GOAL_CONTEXT in, FORGE_GOAL_OUTPUT out):
pnpm run goals:generate -- --docs-dir <archive> --agent-command "node scripts/my-goal-agent.mjs"scripts/agent/generate-sample-goals.mjs walks the hands-on catalog, groups the
framework samples by demo, grounds each demo in its sources and the matching
README/description, and writes one scaffold (reviewed: false). The matched
documentation is synced into the demo directory in its original bundle form
— readme.md plus the description.md / des.md beside it, whichever the
sample uses (one set per demo, shared by every framework) — so the docs live
with the samples and the goal does not depend on an absolute archive path.
Provenance keeps the original location in docs.sourcePath.
Grounding is then read from the synced bundle rather than from the archive, so
--docs-dir is only needed to bootstrap or refresh a demo's docs; once a
demo has been synced, later runs (and --only runs) need no archive at all and
produce the same docsText and fingerprint either way. Passing --docs-dir
re-syncs the bundle, rewriting a file only when its content differs; --force is
separate and forces the goal itself to be rewritten even when the fingerprint is
unchanged.
The archive match itself is exact-first: a demo id's <category>/<leaf> tail is
looked up as a path suffix under the product's site, and only when that fails
does the keyword scoring run. The scorer cannot resolve stopword leaves
(overview, basic, style) at all, so the path pass is what grounds them. A
flat id (activereports-js/parameters) has no category to disambiguate with, so
its unique leaf is trusted when every candidate carries the same text, and
locale-qualified docs (readme{ja}.md) are indexed with the plain file
preferred.
--docs-dir takes a ;-separated list of roots. An entry may be labeled
<product>=<dir> to restrict that root to one product — Wijmo's documentation
lives in its own single-product sample repository
(D:/Products/Wijmo/source-code/wijmo/monorepo_migration/wijmo-sample), whose
feature-samples/samples/<category>/<leaf> tree the exact-path pass resolves
directly, while the keyword fallback would happily let a Wijmo page answer an
InputMan sample. Unlabeled roots are still scoped per product site, and a
product's canonical site may itself be a nested path: SpreadJS is served from
js-sheets-demo/public/en/tutorial/source/demos/en/feature-samples/samples/features,
which mirrors the catalog ids but sits under a public/ directory that the
indexer skips when walking into a root.
The actions are written by an AI agent, not by regex heuristics: for each
demo the generator writes a context (README + every framework's source + the
draft goals) and invokes the agent, which returns the goal with
goals[].actions filled and reviewed: true. --agent pi uses the bundled
scripts/agent/goal-agent-pi.mjs adapter (pi in non-interactive mode), which is
locked to --tools read,write: enrichment only needs to read the context and
write the goal, and dropping bash makes an OS-level find / structurally
impossible. The adapter also runs with cwd set to the demo directory, hands
the agent absolute input paths, writes a per-demo agent.log and session
directory, and loads scripts/pi-extensions/guard-workspace-search.ts, which
blocks any search that points outside the working directory. Each nested agent
runs inside a Windows Job Object so a timeout tears down the whole process tree
instead of leaking it. Action shape:
{ "kind": "click", "target": { "hint": "JSON", "selector": "#toJSON" }, "expect": "a download.ssjson snapshot is downloaded" }Two goals are run-level checks, not behaviors: every draft writes a render
goal and a standing console goal ("check": "console-clean"). A check goal
never carries actions — it is filtered out of the action policy's candidate list
so the AI cannot answer it with clicks — and the explorer evaluates it
deterministically from the runtime records
(scripts/agent/console-check.mjs): only internal records count (preview
transport faults and external CDNs are environment noise), an unexplained
uncaught exception fails with runtime_error, and an unexplained internal
console.error / failed request fails with console_error (repairable by the
repair loop). Known benign noise is declared per demo, using the same shape as
the behavior runner's diagnostic policy:
"diagnosticPolicy": {
"allowedNoise": [
{ "source": "console", "textContains": "NG0304", "reason": "Angular linker probe (upstream)" }
]
}ensureConsoleGoal re-asserts the check after every agent run, so a goal file
cannot lose its console check or turn it into planned actions.
Each goal stores a fingerprint of the demo's framework sources, its README
and the draft version, so a re-run skips unchanged demos and only rewrites
the ones whose sources moved; a stale per-framework goal left by an earlier
layout is removed. When an agent is configured, a demo that has no actions yet
(or whose sources changed) is re-enriched, while a reviewed: true goal with
actions is left alone unless --force-agent is passed. The file is the durable
product of the AI's judgment, not a per-run artifact.
Because the fingerprint also covers the draft version, a draft-logic upgrade
invalidates every goal at once. When that happens, run the generator without
--agent first: the merge is purely local (it appends the new standing goals
and refreshes provenance) and rewrites the fingerprint, so a later --agent pi
run only enriches demos that still lack actions instead of re-querying the model
for all of them.
Syncing the sample repositories
The regenerated goal files and the synced documentation live in the
codemine-forge-demo-* submodules, so they are committed there first and the
superproject then points at the result. scripts/repo/ owns that order:
pnpm run repo:status # branch, upstream, ahead/behind and pending changes per repo
pnpm run repo:sync # commit + push the submodules, then the pointer commit + push
pnpm run repo:sync -- --dry-run # report only, change nothing
pnpm run repo:sync -- --only codemine-forge-demo-spread-jsA submodule commit is pushed before the superproject pointer commit, so a
pointer never references a commit nobody else can fetch. A submodule that
carries a path outside the generated set (forge.goal.json, readme.md,
description.md, des.md) is refused unless --allow-any is passed, and
working-tree changes outside the submodule paths are left uncommitted for the
operator to handle.
Forge sample catalog
The demo ships no bundled samples: every sample is loaded from external static
directories, by default the codemine-forge-demo-* git submodules under
samples/. This keeps the initial browser bundle small: only catalog.json
is loaded at startup, and the selected sample's files are fetched when it is
opened.
When FORGE_SAMPLES_DIR is unset, pnpm run demo resolves the roots in this
order: an in-repo samples/ directory (check the product repositories out
there, for example as git submodules), C:/Users/cary/Desktop/forge-demos, and
the historical single-root
C:/Users/cary/Desktop/sample-migration/forge-samples. Point it somewhere else
with an explicit value, or set it to an empty string to start without the
external catalog:
$env:FORGE_SAMPLES_DIR = 'D:\path\to\forge-demos'
pnpm run demoTwo layouts are supported (see demo/sample-roots.mjs):
- Product repositories (the recommended layout): one git repository per
product, named
codemine-forge-demo-<product>and holding<category>/<sample>/<framework>/. A directory that contains them is expanded automatically, and each repository is mounted at/forge-samples/<product>. A repository name may be mapped to a different product id (for examplecodemine-forge-demo-wijmo-js->wijmo). - A monolithic root: one directory holding
<product>/...directly, optionally with its owncatalog.json(the historicalforge-samplesmigration output). The root is mounted at the/forge-samplesURL root.
FORGE_SAMPLES_DIR accepts a ;/,-separated list, so several roots can be
served together. The Vite host serves the roots at /forge-samples and
synthesizes /forge-samples/catalog.json when a root does not ship one: it
reads any catalog.json beside the samples and otherwise scans the root for
package.json + vite.config.* sample directories. In production, publish the
same sample directories beside the demo assets, or set
VITE_FORGE_SAMPLE_CATALOG_URL to the deployed catalog URL. Forge receives
only the selected sample files through the existing virtual file system; no
migration logic runs in Forge.
Sample submodules
The product repositories are vendored as git submodules under samples/, so a
fresh checkout resolves them before any machine-local path:
git submodule update --init --recursive
pnpm run demopnpm run demo serves samples/codemine-forge-demo-<product> at
/forge-samples/<product> and needs no FORGE_SAMPLES_DIR. Update the samples
with git submodule update --remote samples/... and commit the new gitlinks.
The machine-local defaults (~/Desktop/forge-demos, then the historical
single-root forge-samples) remain as fallbacks for an external checkout.
Publishing
Before publishing, run the complete release gate:
pnpm run validateThis includes type checking, automated tests, the static browser demo smoke, and npm package-content verification. See Release Validation for focused commands and the Playwright browser prerequisite.
Publish prerelease builds to npm with:
pnpm run publish:npmThe script updates the package version, validates the package, checks that the
target version is not already published, runs a package dry run, and publishes
with the alpha dist-tag by default. In auto mode, it keeps the current
version when it has not been published yet and bumps to the next available
prerelease version when it has. If npm login is required, it starts the npm web
login flow in the browser and waits for authentication to complete.
Useful options:
pnpm run publish:npm -- --dry-run
pnpm run publish:npm -- --bump prerelease
pnpm run publish:npm -- --tag next
pnpm run publish:npm -- --login-only
pnpm run publish:npm -- --otp 123456
pnpm run publish:npm -- --version 0.0.2-alpha.0--dry-run still validates and packs the selected target version, but restores
package.json before exiting so a rehearsal does not leave a version bump in
the working tree.
Design Principles
- Prefer browser-native execution over hidden backend work.
- Keep commands deterministic and explicitly supported.
- Treat
forge startandforge runcommands as product affordances, not a real shell. - Make unsupported Node behavior fail early with clear diagnostics.
- Cache aggressively, but keep cache invalidation visible and debuggable.
- Keep the preview iframe isolated from Codemine application privileges.
First Milestone
The first useful milestone is a browser-only runner that can:
- Load a template workspace from memory.
- Persist edits in IndexedDB.
- Parse
package.json. - Resolve public browser-compatible dependencies.
- Bundle a React or Vue entry with
esbuild-wasm. - Simulate
forge start(install → build → dev) and the individualforge runcommands. - Render the result in an iframe.
- Rebuild and refresh the iframe after file changes.
Preview URL resources
The preview API supports resourceMode: 'inline' | 'url'. URL mode serves session-scoped HTML, JavaScript, classic assets and source maps through serve(), while inline mode preserves srcdoc compatibility. Hosts may set previewBaseUrl for a mounted application path. The demo keeps inline mode as its compatibility default; the Service Worker transport is available for explicit URL-mode hosts.
