31 KiB
Workflow Console Foundation Implementation Plan
For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: Build the first executable web/ slice: a local React console that
connects through Hono to a loopback workflow JSON-RPC server, lists workflow
sources, and shows interpreted output beside raw protocol evidence.
Architecture: A pnpm workspace contains a Vite/React browser app, a Hono
Node server, and a focused TypeScript RPC library. The RPC package owns
Effect schemas, loopback target validation, operation metadata, typed errors,
and upstream execution. Hono is the single Effect.runPromise boundary; React
receives ordinary JSON DTOs and never imports Effect at runtime.
Tech Stack: Node.js 22+, pnpm 11.3.0, TypeScript 6, React 19, Vite 8,
Hono 4, Effect 3, Vitest 4, React Testing Library, and tsx.
Scope And Existing Contracts
Implement only the approved foundation in
docs/superpowers/specs/2026-07-01-workflow-console-foundation-design.md.
The public upstream operations are:
workflow.health
workflow.sources.list
Current upstream shapes are defined by:
src/wf_transport_rpc_http/app.py:workflow.healthreturns{status, store_root}.src/wf_transport_rpc_http/models.py:workflow.sources.listaccepts{cursor?: string, limit?: 1..100}.src/wf_api/source_admin.py: source-list results contain{sources, next_cursor, total}.src/wf_platform/sources.py: each source summary containsid,kind,enabled,description, visibility, permissions, policy, capability counts, previews, andhas_moreflags.
Do not modify Python CORS behavior, add workflow mutations, start the Python server from Node, or expose an arbitrary JSON-RPC relay.
Locked File Layout
web/
package.json
pnpm-lock.yaml
pnpm-workspace.yaml
tsconfig.base.json
README.md
apps/
console/
package.json
index.html
tsconfig.json
vite.config.ts
src/
app/App.tsx
app/state.ts
components/ConnectionHeader.tsx
components/ProtocolEvidence.tsx
components/SourceInventory.tsx
connection/api.ts
connection/contracts.ts
main.tsx
styles/global.css
test/setup.ts
server/
package.json
tsconfig.json
src/app.ts
src/index.ts
src/static.ts
packages/
rpc/
package.json
tsconfig.json
src/errors.ts
src/method-registry.ts
src/protocol.ts
src/service.ts
src/target-policy.ts
src/index.ts
Do not create web/packages/ui or web/apps/presentation in this slice.
Task 1: Scaffold The pnpm Workspace
Files:
-
Create:
web/package.json -
Create:
web/pnpm-workspace.yaml -
Create:
web/tsconfig.base.json -
Create:
web/packages/rpc/package.json -
Create:
web/packages/rpc/tsconfig.json -
Create:
web/packages/rpc/src/index.ts -
Create:
web/apps/server/package.json -
Create:
web/apps/server/tsconfig.json -
Create:
web/apps/server/src/index.ts -
Create:
web/apps/console/package.json -
Create:
web/apps/console/tsconfig.json -
Create:
web/apps/console/index.html -
Create:
web/apps/console/vite.config.ts -
Create:
web/apps/console/src/main.tsx -
Create:
web/apps/console/src/app/App.tsx -
Generate:
web/pnpm-lock.yaml -
Step 1: Create the workspace manifests
Use this root manifest. Keep package versions resolved into the lockfile; do not add npm, Yarn, Turbo, or Nx configuration.
{
"name": "@lda/web",
"private": true,
"packageManager": "[email protected]",
"engines": { "node": ">=22" },
"scripts": {
"dev": "concurrently --kill-others-on-fail --names server,console --prefix-colors blue,green \"pnpm --filter @lda/web-server dev\" \"pnpm --filter @lda/console dev\"",
"test": "pnpm -r --if-present test",
"typecheck": "pnpm -r --if-present typecheck",
"build": "pnpm --filter @lda/workflow-rpc build && pnpm --filter @lda/console build && pnpm --filter @lda/web-server build",
"start": "pnpm --filter @lda/web-server start"
},
"devDependencies": {
"concurrently": "10.0.3",
"typescript": "6.0.3"
}
}
packages:
- apps/*
- packages/*
Set strict shared compiler options in web/tsconfig.base.json:
{
"compilerOptions": {
"target": "ES2023",
"strict": true,
"noUncheckedIndexedAccess": true,
"exactOptionalPropertyTypes": true,
"noImplicitOverride": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true
}
}
- Step 2: Create focused package manifests
@lda/workflow-rpc is private, emits declarations and JavaScript to dist/,
and exports no browser UI:
{
"name": "@lda/workflow-rpc",
"private": true,
"type": "module",
"exports": {
".": {
"types": "./dist/index.d.ts",
"default": "./dist/index.js"
}
},
"scripts": {
"build": "tsc -p tsconfig.json",
"test": "vitest run",
"typecheck": "tsc -p tsconfig.json --noEmit"
},
"dependencies": { "effect": "3.21.4" },
"devDependencies": { "vitest": "4.1.9" }
}
@lda/web-server depends on the RPC package and uses TypeScript path mapping
during tsx development. Compiled production code resolves the built workspace
package normally.
{
"name": "@lda/web-server",
"private": true,
"type": "module",
"scripts": {
"predev": "pnpm --filter @lda/workflow-rpc build",
"dev": "tsx watch src/index.ts",
"prebuild": "pnpm --filter @lda/workflow-rpc build",
"build": "tsc -p tsconfig.json",
"start": "node dist/index.js",
"test": "vitest run",
"pretypecheck": "pnpm --filter @lda/workflow-rpc build",
"typecheck": "tsc -p tsconfig.json --noEmit"
},
"dependencies": {
"@hono/node-server": "2.0.6",
"@lda/workflow-rpc": "workspace:*",
"effect": "3.21.4",
"hono": "4.12.27"
},
"devDependencies": {
"@types/node": "26.1.0",
"tsx": "4.22.4",
"vitest": "4.1.9"
}
}
@lda/console uses no Effect runtime:
{
"name": "@lda/console",
"private": true,
"type": "module",
"scripts": {
"dev": "vite --host 127.0.0.1 --port 5173 --strictPort",
"build": "tsc -b && vite build",
"test": "vitest run",
"typecheck": "tsc -b --pretty false",
"preview": "vite preview --host 127.0.0.1"
},
"dependencies": {
"@fontsource/barlow-condensed": "5.2.8",
"@fontsource-variable/source-sans-3": "5.2.9",
"@fontsource/ibm-plex-mono": "5.2.7",
"react": "19.2.7",
"react-dom": "19.2.7"
},
"devDependencies": {
"@testing-library/jest-dom": "6.9.1",
"@testing-library/react": "16.3.2",
"@testing-library/user-event": "14.6.1",
"@types/react": "19.2.17",
"@types/react-dom": "19.2.3",
"@vitejs/plugin-react": "6.0.3",
"jsdom": "29.1.1",
"vite": "8.1.2",
"vitest": "4.1.9"
}
}
- Step 3: Add TypeScript package configurations
Use module/moduleResolution: "NodeNext", composite: true, declaration
output, rootDir: "src", and outDir: "dist" for RPC and server packages.
The server imports @lda/workflow-rpc through its workspace package export.
Its predev, prebuild, and pretypecheck scripts build that dependency first,
so Node and TypeScript resolve packages/rpc/dist/ without source-path aliases
or a third development watcher. The server config should reference the package:
{
"references": [{ "path": "../../packages/rpc" }]
}
Use moduleResolution: "Bundler", jsx: "react-jsx", DOM libraries,
noEmit: true, and types: ["vite/client", "vitest/globals"] for the console.
- Step 4: Add a minimal bootable console and development proxy
vite.config.ts must proxy /api without rewriting it:
import react from "@vitejs/plugin-react";
import { defineConfig } from "vitest/config";
export default defineConfig({
plugins: [react()],
server: {
proxy: {
"/api": "http://127.0.0.1:8787",
},
},
test: {
environment: "jsdom",
setupFiles: "./src/test/setup.ts",
},
});
The placeholder React page should render lda.chat Workflow Console; the
placeholder server should be a valid module but must not listen until Task 4.
- Step 5: Install and verify the workspace
Run:
pnpm --dir web install
pnpm --dir web typecheck
Expected: lockfile created and all three packages typecheck successfully.
- Step 6: Commit the scaffold
git add web
git commit -m "build: scaffold workflow console workspace"
Task 2: Define Target Policy And JSON-RPC Protocol Schemas
Files:
-
Create:
web/packages/rpc/src/errors.ts -
Create:
web/packages/rpc/src/target-policy.ts -
Create:
web/packages/rpc/src/protocol.ts -
Create:
web/packages/rpc/src/target-policy.test.ts -
Create:
web/packages/rpc/src/protocol.test.ts -
Modify:
web/packages/rpc/src/index.ts -
Step 1: Write failing target-policy tests
Cover these exact cases:
it.each([
["http://127.0.0.1:8765/rpc", "http://127.0.0.1:8765/rpc"],
["http://localhost:8765/rpc", "http://localhost:8765/rpc"],
["http://[::1]:8765/rpc", "http://[::1]:8765/rpc"],
])("accepts loopback target %s", (input, expected) => {
expect(normalizeLoopbackTarget(input)).toBe(expected);
});
it.each([
"https://127.0.0.1:8765/rpc",
"http://example.com:8765/rpc",
"http://user:[email protected]:8765/rpc",
"http://127.0.0.1/rpc",
"http://127.0.0.1:8765/rpc?x=1",
"http://127.0.0.1:8765/rpc#fragment",
])("rejects unsafe target %s", (input) => {
expect(() => normalizeLoopbackTarget(input)).toThrow(InvalidTargetError);
});
Run:
pnpm --dir web --filter @lda/workflow-rpc test -- target-policy.test.ts
Expected: FAIL because the target-policy module does not exist.
- Step 2: Implement loopback normalization
normalizeLoopbackTarget(raw: string): string must:
- call
new URL(raw)and translate parse failures intoInvalidTargetError; - require
protocol === "http:"; - accept only
127.0.0.1,localhost, and[::1]asurl.hostnamevalues; - reject credentials, missing explicit port, query, and fragment;
- reject numeric ports outside
1..65535; - return the normalized
url.toString()value; - retain the path because configured RPC paths are valid public contracts.
Add a short comment explaining that DNS names other than literal localhost
are rejected to avoid DNS-rebinding ambiguity.
- Step 3: Write failing protocol tests
Define tests for:
- a success envelope with matching string id;
- a remote-error envelope with matching string id;
- mismatched ids;
- envelopes containing both
resultanderror; - envelopes containing neither;
- malformed JSON-RPC versions and error objects.
Use public helpers rather than testing Effect internals:
expect(decodeRpcResponse(payload, "req-1")).toEqual({
jsonrpc: "2.0",
id: "req-1",
result: { status: "ok" },
});
- Step 4: Implement protocol schemas and decoder
Use Schema.Struct, Schema.Literal, Schema.Union, and
Schema.decodeUnknownSync; do not hand-roll field type validation. Decode with
{onExcessProperty: "error"} wherever an envelope or operation parameter must
reject unknown keys.
Export:
export type JsonRpcRequest = {
readonly jsonrpc: "2.0";
readonly id: string;
readonly method: string;
readonly params: unknown;
};
export type JsonRpcResponse = JsonRpcSuccess | JsonRpcFailure;
export function decodeRpcResponse(
value: unknown,
expectedId: string,
): JsonRpcResponse;
After schema decoding, explicitly reject a mismatched id. Represent schema or
shape failures as RpcProtocolError, not raw parse exceptions.
- Step 5: Define all tagged errors
Use Data.TaggedError for:
InvalidTargetError
UnknownOperationError
UpstreamConnectionError
UpstreamTimeoutError
UpstreamResponseTooLargeError
RpcProtocolError
RpcRemoteError
RpcDecodeError
Each error carries a safe message; remote/protocol errors may additionally
carry bounded raw evidence. Never put stack traces into error DTOs.
- Step 6: Run tests and commit
pnpm --dir web --filter @lda/workflow-rpc test
pnpm --dir web --filter @lda/workflow-rpc typecheck
git add web/packages/rpc
git commit -m "feat: define workflow rpc protocol boundary"
Expected: all RPC package tests pass.
Task 3: Add The Method Registry And Effect RPC Service
Files:
-
Create:
web/packages/rpc/src/method-registry.ts -
Create:
web/packages/rpc/src/method-registry.test.ts -
Create:
web/packages/rpc/src/service.ts -
Create:
web/packages/rpc/src/service.test.ts -
Modify:
web/packages/rpc/src/index.ts -
Step 1: Write failing registry tests
Assert the exact public operations and metadata:
expect(listOperations()).toEqual([
"workflow.health",
"workflow.sources.list",
]);
expect(resolveOperation("workflow.sources.list")).toMatchObject({
method: "workflow.sources.list",
label: "List sources",
idempotency: "read",
});
expect(() => resolveOperation("workflow.runs.start")).toThrow(
UnknownOperationError,
);
- Step 2: Implement schemas for selected upstream result fields
Use Effect Schema, not handwritten type assertions:
export const HealthResultSchema = Schema.Struct({
status: Schema.Literal("ok"),
store_root: Schema.String,
});
export const SourceSummarySchema = Schema.Struct({
id: Schema.String,
kind: Schema.String,
enabled: Schema.Boolean,
description: Schema.NullOr(Schema.String),
tool_count: Schema.Number,
node_spec_count: Schema.Number,
reducer_count: Schema.Number,
prompt_count: Schema.Number,
resource_count: Schema.Number,
});
export const SourceListResultSchema = Schema.Struct({
sources: Schema.Array(SourceSummarySchema),
next_cursor: Schema.NullOr(Schema.String),
total: Schema.Number,
});
Define parameter schemas with an empty object for health and optional cursor plus bounded integer limit for source list. Unknown parameter keys must be rejected before fetch.
- Step 3: Implement the declarative operation registry
Each entry must own:
type OperationDefinition = {
readonly method: OperationName;
readonly label: string;
readonly explanation: string;
readonly idempotency: "read";
readonly paramsSchema: Schema.Schema<unknown>;
readonly resultSchema: Schema.Schema<unknown>;
readonly equivalentCli: (params: unknown) => string;
readonly interpret: (result: unknown) => unknown;
};
CLI strings are:
uv run wf status
uv run wf source list --limit 50
Include --cursor VALUE only when a cursor is present. The health card may say
Server healthy and expose storeRoot; source interpretation returns compact
rows plus total and nextCursor.
- Step 4: Write failing RPC execution tests with injected fetch
Cover:
- generated string request id and exact JSON-RPC body;
- successful health decoding and interpretation;
- source-list decoding;
- invalid params fail before fetch;
- redirects are rejected;
- remote JSON-RPC errors become
RpcRemoteError; - malformed and mismatched responses become
RpcProtocolError; - result schema mismatch becomes
RpcDecodeError; - timeout becomes
UpstreamTimeoutError; - response body above 4 MiB becomes
UpstreamResponseTooLargeError.
Inject a fetch-compatible function and deterministic id/clock dependencies;
do not monkeypatch globals.
- Step 5: Implement bounded upstream execution as an Effect service
Export a WorkflowRpc Context.Tag whose service has:
readonly execute: (
operation: OperationName,
target: string,
params: unknown,
) => Effect.Effect<OperationExchange, WorkflowRpcError>;
OperationExchange contains:
{
operation: OperationName;
label: string;
interpreted: unknown;
exchange: {
request: JsonRpcRequest;
response: JsonRpcResponse | null;
};
equivalentCli: string;
durationMs: number;
}
Implementation requirements:
- normalize the target on every call;
- resolve and decode operation params before fetch;
- use
redirect: "manual"and reject every300..399response; - use an
AbortControllerwith a five-second timeout; - check
content-lengthwhen present, then stream and count response bytes so the 4 MiB limit is real even without that header; - parse JSON, decode the envelope, map remote errors, decode the operation result, and interpret it;
- preserve request and response evidence for successful and remote-error cases;
- do not retry.
Add comments around bounded body streaming and redirect rejection because they are security behavior, not incidental transport code.
- Step 6: Run tests and commit
pnpm --dir web --filter @lda/workflow-rpc test
pnpm --dir web --filter @lda/workflow-rpc typecheck
git add web/packages/rpc
git commit -m "feat: execute registered workflow rpc operations"
Task 4: Expose The Browser-Facing Hono API
Files:
-
Create:
web/apps/server/src/app.ts -
Create:
web/apps/server/src/app.test.ts -
Modify:
web/apps/server/src/index.ts -
Step 1: Write failing Hono route tests
Build tests through app.request() with an injected operation runner. Cover:
GET /api/health -> 200 {ok:true,status:"ok"}
POST /api/connect -> invokes workflow.health
POST /api/rpc -> invokes workflow.sources.list
POST /api/rpc unknown operation -> 400 before runner call
POST body over 256 KiB -> 413
invalid JSON/body/target -> 400
upstream connection/protocol/decode/remote error -> 502
upstream timeout -> 504
Assert that error DTOs never contain stack.
- Step 2: Define stable browser DTOs and error mapping
Successful connect response:
{
ok: true;
connection: {
status: "connected";
target: string;
serverStatus: "ok";
storeRoot: string;
durationMs: number;
};
exchange: { request: unknown; response: unknown };
equivalentCli: "uv run wf status";
}
Successful operation response uses the approved shape:
{
ok: true;
operation: OperationName;
label: string;
interpreted: unknown;
exchange: { request: unknown; response: unknown };
equivalentCli: string;
durationMs: number;
}
Failure response:
{
ok: false;
error: { code: BrowserErrorCode; message: string };
exchange: { request: unknown | null; response: unknown | null };
}
Use stable codes such as invalid_target, unknown_operation,
upstream_unreachable, upstream_timeout, rpc_remote_error,
rpc_protocol_error, rpc_decode_error, and response_too_large.
- Step 3: Implement
createAppwith dependency injection
Export:
export type RunOperation = (
operation: OperationName,
target: string,
params: unknown,
) => Promise<OperationExchange>;
export function createApp(dependencies: {
readonly runOperation: RunOperation;
}): Hono;
Apply bodyLimit({maxSize: 256 * 1024}) to both POST routes. Parse request
bodies once. /api/connect always calls health with {}. /api/rpc accepts
only the operation-name union exported by the registry.
- Step 4: Make
index.tsthe single production Effect boundary
Create the live RPC layer, construct runOperation with one
Effect.runPromise call per request, create the app, and bind:
serve({
fetch: app.fetch,
hostname: process.env.WEB_HOST ?? "127.0.0.1",
port: Number(process.env.WEB_PORT ?? "8787"),
});
Reject invalid WEB_PORT at startup with one concise error and non-zero exit.
Do not bind all interfaces by default.
- Step 5: Run tests and commit
pnpm --dir web --filter @lda/web-server test
pnpm --dir web --filter @lda/web-server typecheck
git add web/apps/server
git commit -m "feat: expose workflow console api"
Task 5: Build Connection State And Connection UI
Files:
-
Create:
web/apps/console/src/connection/contracts.ts -
Create:
web/apps/console/src/connection/api.ts -
Create:
web/apps/console/src/connection/api.test.ts -
Create:
web/apps/console/src/app/state.ts -
Create:
web/apps/console/src/app/state.test.ts -
Create:
web/apps/console/src/components/ConnectionHeader.tsx -
Create:
web/apps/console/src/components/ConnectionHeader.test.tsx -
Create:
web/apps/console/src/test/setup.ts -
Modify:
web/apps/console/src/app/App.tsx -
Modify:
web/apps/console/src/main.tsx -
Step 1: Define plain browser contracts and API tests
Mirror server DTOs as TypeScript discriminated unions. Do not import Effect or the runtime RPC package. Test that the API client:
-
posts the exact target to
/api/connect; -
posts operation and params to
/api/rpc; -
returns success DTOs;
-
returns typed failure DTOs instead of throwing for expected HTTP failures;
-
throws only for malformed browser-server responses or browser fetch failure.
-
Step 2: Define and test the reducer state machine
Use these phases:
type ConnectionPhase =
| "not_configured"
| "connecting"
| "connected"
| "invalid_target"
| "unreachable"
| "rpc_error"
| "malformed_response";
State retains draftTarget separately from connectedTarget. A failed attempt
updates the phase and message but never discards the entered URL or overwrites
the last successful session target.
Reducer tests must pin:
-
submit transitions to connecting;
-
success records normalized target and evidence;
-
failure retains draft input;
-
reconnect replaces target only on success;
-
restored session target populates the input but remains
not_configured. -
Step 3: Implement the connection header behavior
The form contains:
- label
Workflow JSON-RPC URL; - input defaulting to
http://127.0.0.1:8765/rpcwhen no stored value exists; ConnectorReconnectbutton;- visible phase, server status, store root, and duration after success;
- concise inline failure message after failure.
On successful connect only:
sessionStorage.setItem("lda.workflowConsole.target", normalizedTarget);
On reload, restore the value but do not call the server automatically.
- Step 4: Write component tests
Use Testing Library and user-event to verify:
-
initial default and no automatic request;
-
connecting disables only the submit action, not the input;
-
invalid/unreachable states retain the typed value;
-
successful target is persisted;
-
restored target still requires explicit connect;
-
status text is exposed through an
aria-live="polite"region. -
Step 5: Run tests and commit
pnpm --dir web --filter @lda/console test
pnpm --dir web --filter @lda/console typecheck
git add web/apps/console
git commit -m "feat: add workflow console connection flow"
Task 6: Add Source Inventory, Protocol Evidence, And Visual System
Files:
-
Create:
web/apps/console/src/components/SourceInventory.tsx -
Create:
web/apps/console/src/components/SourceInventory.test.tsx -
Create:
web/apps/console/src/components/ProtocolEvidence.tsx -
Create:
web/apps/console/src/components/ProtocolEvidence.test.tsx -
Create:
web/apps/console/src/styles/global.css -
Modify:
web/apps/console/src/app/App.tsx -
Modify:
web/apps/console/src/app/state.ts -
Modify:
web/apps/console/src/main.tsx -
Step 1: Extend state for source loading and evidence history
After health succeeds, call workflow.sources.list with {limit: 50}. Store:
type EvidenceRecord = {
readonly id: string;
readonly operation: string;
readonly label: string;
readonly equivalentCli: string;
readonly request: unknown;
readonly response: unknown;
readonly durationMs: number;
};
Keep health and source-list records in chronological order. A source-list failure must leave the connection healthy, show a source-specific error, and retain both operation records where evidence exists.
- Step 2: Write and implement source-inventory tests
Test compact rows containing:
- source id and kind;
- enabled/disabled status;
- description when present;
- total count across tools, node specs, reducers, prompts, and resources.
An empty inventory renders No workflow sources reported. A failed inventory
renders its error without replacing the connection status.
- Step 3: Write and implement evidence-drawer tests
The drawer must:
-
remain collapsed by default;
-
list health and source-list operations as selectable records;
-
display equivalent CLI, duration, formatted request JSON, and formatted response JSON;
-
render evidence through
<pre><code>{text}</code></pre>, never HTML; -
show
No response received.for a null response; -
remain keyboard operable with native buttons and
<details>or equivalent accessible disclosure semantics. -
Step 4: Apply the visual direction
Import the three checked-in npm fonts in main.tsx:
import "@fontsource/barlow-condensed/600.css";
import "@fontsource/barlow-condensed/700.css";
import "@fontsource-variable/source-sans-3";
import "@fontsource/ibm-plex-mono/400.css";
import "./styles/global.css";
Define CSS variables for warm paper, ink, slate, signal green, amber, and red. Use a subtle two-axis grid made from CSS linear gradients. Use Barlow Condensed for page and section headings, Source Sans 3 for body text, and IBM Plex Mono for protocol evidence.
Desktop layout:
connection header across full width
source inventory (minmax(0, 1fr)) | protocol evidence (minmax(22rem, 0.7fr))
At widths below 850px, stack inventory and evidence. At widths below 560px,
stack URL input and action. Use one connection-state transition and a short
staggered source-row reveal; respect prefers-reduced-motion.
Do not add dark mode, a CSS framework, gradients in purple hues, generic metric cards, or decorative motion unrelated to state changes.
- Step 5: Run accessibility-oriented component tests and build
pnpm --dir web --filter @lda/console test
pnpm --dir web --filter @lda/console typecheck
pnpm --dir web --filter @lda/console build
Expected: tests pass and Vite emits web/apps/console/dist/.
- Step 6: Commit the console surface
git add web/apps/console
git commit -m "feat: show source inventory and rpc evidence"
Task 7: Serve The Production Console Through Hono
Files:
-
Create:
web/apps/server/src/static.ts -
Create:
web/apps/server/src/static.test.ts -
Modify:
web/apps/server/src/app.ts -
Modify:
web/apps/server/src/index.ts -
Modify:
web/apps/server/tsconfig.json -
Step 1: Write failing production-static tests
Build a temporary console directory containing index.html and one asset.
Create the app with that root and assert:
-
/assets/app.jsserves the asset; -
/some/client/routeservesindex.html; -
/api/unknownremains a JSON 404 and never falls back to HTML; -
a missing console root produces a clear startup error.
-
Step 2: Implement static and SPA fallback behavior
Use @hono/node-server/serve-static. Resolve the default console directory
from import.meta.url, not process.cwd(), so pnpm --dir web start works from
any directory.
Register routes in this order:
- all
/api/*routes; - static files;
- non-API GET fallback to
index.html; - JSON 404 for everything else.
Expose the console root as an injectable createApp option so tests do not
depend on the real build output.
- Step 3: Build and run the production process
pnpm --dir web build
pnpm --dir web start
Expected: one Hono process serves /api/health, /, static assets, and an SPA
fallback from 127.0.0.1:8787.
- Step 4: Commit production serving
git add web/apps/server
git commit -m "feat: serve workflow console production build"
Task 8: Document, Smoke Test, And Close The Slice
Files:
-
Create:
web/README.md -
Modify:
docs/current_roadmap.md -
Modify:
docs/superpowers/specs/2026-07-01-workflow-console-foundation-design.md -
Move after completion:
docs/superpowers/plans/2026-07-02-workflow-console-foundation.mdtodocs/historical/superpowers/plans/2026-07-02-workflow-console-foundation.md -
Step 1: Write the operator runbook
Document these exact flows in web/README.md:
# Terminal 1: workflow JSON-RPC server
uv run wf-rpc-server --config wf.config.json --host 127.0.0.1 --port 8765
# Terminal 2: Vite + Hono development processes
pnpm --dir web install
pnpm --dir web dev
The browser URL is http://127.0.0.1:5173; the pasted target is
http://127.0.0.1:8765/rpc.
Also document:
pnpm --dir web test
pnpm --dir web typecheck
pnpm --dir web build
pnpm --dir web start
State clearly that the server accepts loopback HTTP targets only and that the console does not start or authenticate the Python workflow server.
- Step 2: Run the live smoke test
With the Python server running, verify in the browser:
- the initial page makes no upstream request;
- connect succeeds against
http://127.0.0.1:8765/rpc; - source rows appear;
- raw health and source-list exchanges are selectable;
- equivalent CLI text is visible;
http://example.com:8765/rpcis rejected without upstream fetch;- stopping the Python server produces the unreachable state while preserving the entered URL.
Record only observed behavior in the implementation report. Do not add a default live-server test to Vitest.
- Step 3: Run complete scoped verification
pnpm --dir web test
pnpm --dir web typecheck
pnpm --dir web build
uv run pytest tests\docs -q -n0
uv run ruff check
uv run basedpyright --level error
git diff --check
Expected: all commands pass. If repository-wide Python checks expose a
pre-existing failure, prove it on HEAD, report it precisely, and do not claim
the suite is clean.
- Step 4: Update live docs and archive the completed plan
Mark roadmap item 3 completed and leave items 4-7 pending. Update the
foundation design status to Implemented and add the verification commands.
Move this plan to the historical path and update any live link that points to
the active plan location.
- Step 5: Commit the completed slice
git add web docs/current_roadmap.md docs/superpowers/specs/2026-07-01-workflow-console-foundation-design.md docs/historical/superpowers/plans/2026-07-02-workflow-console-foundation.md
git commit -m "docs: record workflow console foundation"
Final Acceptance Checklist
pnpm --dir web devstarts Vite and Hono with one root command.- The browser does not call workflow JSON-RPC directly.
- Every target is normalized and checked again for each operation.
- Only
workflow.healthandworkflow.sources.listare registered. - Unknown operations and non-loopback targets fail before fetch.
- Upstream redirect, timeout, protocol, remote, decode, and size failures map to stable browser DTOs.
- The UI distinguishes all approved connection states.
- Failed connection input remains editable and preserved.
- Session storage changes only after successful health.
- Source inventory and raw protocol evidence both render.
- Production Hono serves the Vite build and SPA fallback.
- No Python CORS changes, direct store reads, workflow mutations, graph UI, autoplay, replay, agent integration, or presentation app entered scope.