Implementation Plan: Mini-App Build Harness
Author: Matti Georgi Status: Draft — for review Last updated: 2026-08-02 PRD: mini-app-build-harness-prd.md Design: mini-app-build-harness-design.md
Written to be executed by an implementing agent (Opus) PR by PR. Each PR names its files, the invariants it must hold, and the tests that prove it. PRs marked ∥ can proceed in parallel once their listed dependency lands.
0. Ground rules
- Every PR passes
npm run checkbefore commit.packages/cli,packages/execution,packages/agents, andpackages/websocketare plain TS packages — nobuild:packagesneeded for their own tests, but run it before any end-to-endnodetool app debugsmoke since the CLI loads decorator packages fromdist/. - Read before writing. The modules this plan touches are documented in-file:
packages/cli/src/app-debug/harness.ts(orchestration),packages/agents/src/evals/surfaces/app.ts(bridge + drift rules),packages/agents/src/evals/tool-loop-eval.ts(loop driver),packages/cli/src/commands/eval.ts(“suites are data”). Honor the invariants their header comments state. - The relocation PR (PR 3) is behavior-preserving and must be provably so:
nodetool app debugoutput on the shipped example bundles is byte-identical before and after (modulo timestamps/paths). BuildSpec/BuildReportshapes change only via the design doc, in the same PR.- No web UI work in this plan. The web surface (if any) consumes
BuildReportafter M4; it does not define it. - Prose in reports and docs follows the repo style rules in
docs/WRITING_STYLE.md.
Phase M1 — close the oracle (independently shippable)
PR 1 — Simulate conditions and format (R6)
All in packages/cli/src/app-debug/ (pre-relocation — keep the diff where the
code lives today; PR 3 moves it wholesale).
runtime.ts: after every fold into app state, evaluate each widget’svisibleWhen/disabledWhenviaparseCondition/evaluateConditionfrom@nodetool-ai/app-runtime(already exported — wire, don’t reimplement). Track per-widget{ visible, disabled }in the runtime’s widget state.harness.ts: an interaction step targeting a hidden or disabled widget fails that step with a message naming the condition source (clicked "Approve" while hidden by \draft == null`).set` steps that address state keys directly (not widgets) are exempt — they model the runtime, not a user.- Display checks and
report.mdpreviews render throughformatTemplatebefore comparison. types.ts: widget final-state record gainsvisible/disabled; the report’snotSimulated(add the field if absent) no longer lists conditions/format.- Verdict: new issue for a run-trigger widget whose
visibleWhennever evaluated true in any executed interaction. - Tests (
packages/cli/tests/next to the existing app-debug tests): a conditional Approve button — hidden click fails, satisfied click runs; aformattemplate changes anequalscheck’s outcome; the never-visible trigger verdict fires; an app with no conditions produces an unchanged report.
PR 2 — Headless resource provider (R7) ∥ (with PR 1)
runtime.ts:HeadlessResourceProviderinterface +InMemoryResourceProvider(design §4.2); theresourceCommandbranch dispatches to it instead of no-opping.harness.ts+types.ts:seedResourceinteraction step;from: "resource"params resolve through the provider; resource widgets report collection state in the final report.- Update the “Not simulated headlessly” paragraph in
CLAUDE.md§nodetool app debugand mini-apps.md “Checking an app” — both currently list resources as unreachable. - Tests: a picker-fed param runs with seeded items and fails with an actionable message when unseeded; a resource operation mutates the collection and the report shows it.
PR 3 — Relocate the simulator to @nodetool-ai/execution (depends: PR 1, PR 2)
- Move
app-spec.ts,runtime.ts,types.ts, and the simulation half ofharness.tstopackages/execution/src/app-debug/; export from the package index the wayexecution/debugalready does.markdown.tsmoves too (it renders the report, needs nothing from cli). - Stays in
packages/cli/src/app-debug/:app-target.ts(DB/file resolution), bundle writing, the command wiring. It re-exports the moved types so@nodetool-ai/cliimporters don’t break. - The harness’s
runOnServer/loadFromDb/loadApplicationinjections already isolate it from cli’s debug internals — the move must not grow new parameters. Whereharness.tsimports../debug/collector.js(previewValue) and../debug/types.js, move those types and the small preview helper into execution as well or inject them; do not drag the whole cli debug module along. - Proof of behavior preservation: script a before/after
app debug --jsonrun over every bundle inpackages/base-nodes/nodetool/examples/apps/and diff (normalize paths/timestamps). Keep the script inscripts/for PR review, delete before merge or park under the eval fixtures.
Phase M2 — the loop
PR 4 — Promote the bridge, extract the tool-loop driver (depends: PR 3)
packages/agents/src/app-build/bridge.ts: move the bridge implementation out ofevals/surfaces/app.tsverbatim — tool contract, catalog use, and the doc-meta passthrough to@nodetool-ai/app-runtimedoc-ops are already correct there. Addui_app_finish(bridge-only termination tool) and aloadDocumententry so repair rounds resume from the current document.evals/surfaces/app.tsbecomes: re-export of the bridge + the eval cases. The file’s header comment about drift updates to describe the new sharing.packages/agents/src/app-build/tool-loop.ts: extract the generic provider-driving loop fromtool-loop-eval.ts(generateLoop overHeadlessTool[], turn cap, transcript capture) into a function both the eval and the author stage call. The eval keeps its scoring; the loop moves.- Tests: existing
app-toolseval passes unchanged (that suite is the regression test for this PR); a unit test drives the bridge throughloadDocument→ edit →finalStateround-trip.
PR 5 — Types, spec stage, interaction derivation ∥ (with PR 4, depends: PR 3)
packages/agents/src/app-build/types.ts:BuildSpec,BuildIssue,BuildComplaint,StageRecord,BuildReportper design §2.spec.ts: oneStepExecutorcall withBuildSpecasoutputSchema; the host-side validation list from design §3.1, implemented againstWIDGET_CATALOGandparseBinding— reuse the supervisor’sfinish_stepbounce loop (it is a generalStepExecutorfeature since supervisor PR 3).specFromFileparses + validates without the LLM.interactions.ts: spec interactions are alreadyInteractionStep[]; this module only completes them — prependsetsteps for declared inputs that no step touches (using spec examples), and derive a minimal default interaction (the existingdefaultInteractionsshape) for any operation the spec forgot, flagging it as derived in the report.- Tests: schema-valid-but-catalog-invalid specs bounce with the right issue; an unpinnable spec fails after 3 bounces; derivation covers a forgotten operation and never overwrites an authored step.
PR 6 — Orchestrator, repair loop, app build CLI (depends: PR 4, PR 5)
The core PR. packages/agents/src/app-build/build.ts + the CLI command.
build.ts—buildApp(opts): Promise<BuildReport>: the stage sequence from design §3.7. Plan wrapsGraphPlannerper operation (existing-workflow binding first, IO-coverage check, one replan). Author runs the tool loop from PR 4 with the spec rendered as system prompt. Check/Run call the execution simulator on the in-memoryApplicationBundle. Judge is a stub returningachieved: truethis PR (lands in PR 7); the verdict says so innotSimulated.- Repair: complaints as full issue sets, fingerprints, oscillation guard, routing (Author default, Plan for graph-IO issues) — design §3.7 verbatim.
- Budgets:
maxRepairs, wall-clock, and USD viaTurnBudgetreservation on every provider call (spec, plan, author turns). Exhaustion →failedwith the budget named.AbortSignalthreads into every stage. - Ledger: stage provider calls attributed to the build id,
node_type: "app-build". - Spans:
app.build+ per-stage children (design §6), via the existing agent span helpers. packages/cli/src/commands/app.ts: thebuildsubcommand, flags and bundle layout per design §5.1. Exit 0 iffverdict.ok.- Tests (
packages/agents/tests/): a scripted provider (the eval harness’s mock pattern) drives a full green build over a template workflow with zero real model calls; a complaint round edits rather than rebuilds (assert the document diff is scoped); oscillation ends the build; each budget exhaustion fails closed; cancel mid-author aborts cleanly. CLI test:app build spec.json --jsonon a deterministic spec emits a parseableBuildReportand writes the bundle layout.
Phase M3 — the score
PR 7 — Judge stage (R8) (depends: PR 6)
packages/agents/src/app-build/judge.ts: per-interaction structured-output call followingevals/goal-judge.ts; inputs and fail-closed rules per design §3.6.--judge-modelflag +NODETOOL_APP_JUDGE_MODEL; default resolution picks a model ≠ builder when possible and records which ran.- Wire into
build.tsbehind--no-judge; judge complaints route to Author. - Tests: scripted judge verdicts flip the build outcome; judge timeout counts as not-achieved; deterministic path never invokes it.
PR 8 — app-build eval suite (R5) (depends: PR 6; judge cases depend: PR 7)
- Cases in
packages/agents/src/evals/app-build-cases.ts: ≥ 8 prompts meeting the PRD §4 bar (each case declares which medium-complexity traits it exercises, and the set covers all of them), plus 2 deterministic spec-file cases over template graphs with exact expected widget values, judge skipped. - Runner in
evals/app-build-eval.ts; one data entry inEVAL_SUITES(packages/cli/src/commands/eval.ts) — follow the file’s “suites are data” rule, no new command block. Metrics and--min-successsemantics per design §5.3. - CI: deterministic cases join the Quality Gate; full suite gets a nightly workflow following the GenSpend sync workflow’s shape (report artifact, no auto-merge decisions).
- Tests: the suite runs end-to-end on the deterministic cases in CI with no provider keys.
Phase M4 — surfaces
PR 9 — build_app server tool + HTTP (R9) (depends: PR 6; better after PR 7)
- Register
build_appnext todebug_workflow;POST /api/applications/buildrunningbuildApp(); long-build polling and cancel throughpackages/websocket/src/debug-sessions.ts. Auth and cost-cap defaults matchdebug_workflow’s posture. - The tool result is the
BuildReport; the bundle imports through the existing bundle-import endpoint, never auto-installs. - Tests: route test with a scripted provider; cancel over HTTP aborts the build; the tool is listed for editor chat.
PR 10 — Supervisor composition (R10) ∥ (with PR 9, depends: PR 6)
--supervise+ supervisor flags onapp buildconfigureExecutionSessionOptions.supervisorfor the Run stage’s kernel executions — the same single integration point every surface uses. Interventions land in the interaction’s run report and roll up intoBuildReport.- Test: a scripted supervisor skip during Run surfaces in the build report and does not count as a build failure when expectations still pass.
PR 11 — P2 conveniences (R11, R12) (depends: PR 8)
--watchon spec-file targets: re-run on save, print a verdict diff — reuse thedebug --watchdiffer.- Example-apps regeneration:
scripts/build-example-apps.mjsgains a mode that runsbuildAppfrom each curated spec and diffs against the shipped bundle, reporting drift without overwriting — the curated bundles stay hand-approved.
Sequencing summary
M1: PR1 ∥ PR2 → PR3
M2: PR4 ∥ PR5 → PR6
M3: PR7 → PR8 (deterministic cases of PR8 can start after PR6)
M4: PR9 ∥ PR10 → PR11
The PRD’s launch gate (§8) is measurable after PR 8; nothing in M4 blocks it.