Conversation
`--control-socket <path>` attaches an HTTP/1.1 server that reads one store and commits through its setters: health, state with long polling, actions, credentials, run arming on the TUI surface, detection and independent runs on the headless surface, a run ledger, and shutdown. The store owns the server, the projection, the generic actions, and the ledger; programs add their own actions through FlowStep.controlActions; the cli supplies the hooks that resolve credentials, run detection, and start one agent run with explicit context. Published builds keep the server for headless runs and refuse the flag on the TUI, which never loads it; the smoke test audits the built chunks and CI diffs the chunk manifests against the committed fixtures. `--e2e-ask` replaces the E2E_ASK environment read in dev builds. Generated-By: PostHog Desktop Task-Id: d14e92bb-6ee1-49b5-8502-39cb80079589
🧙 Wizard CIRun the Wizard CI and test your changes against wizard-workbench example apps by replying with a GitHub comment using one of the following commands: Test all apps:
Test all apps in a directory:
Test an individual app:
Show more apps
Test against a Context Mill branch:
Add Results will be posted here when complete. |
Each POST /runs clears the previous run's outcome, publishes to a task stream session of its own, and records its task list in the ledger, so nothing from one run leaks into the next while credentials and framework context persist. The headless runner attaches no process-level stream when a socket is served. scripts/controlled-headless-smoke.no-jest.ts drives the surface end to end. Generated-By: PostHog Desktop Task-Id: d14e92bb-6ee1-49b5-8502-39cb80079589
Control API: why each piece existsTop to bottom is the direction of a request. Every box says what it is for in running the TUI, the store, and the agent apart; every arrow names the contract the data crosses. flowchart TB
classDef wave_entry fill:#fff8e6,stroke:#d9a400,stroke-width:2px,color:#d9a400;
classDef card_entry fill:#fff1c7,stroke:#d9a400,stroke-width:1px,color:#1d1d1f;
classDef wave_cli fill:#fff1e8,stroke:#e8632c,stroke-width:2px,color:#e8632c;
classDef card_cli fill:#ffe1cc,stroke:#e8632c,stroke-width:1px,color:#1d1d1f;
classDef wave_tui fill:#e9f0ff,stroke:#2f6fed,stroke-width:2px,color:#2f6fed;
classDef card_tui fill:#d6e4ff,stroke:#2f6fed,stroke-width:1px,color:#1d1d1f;
classDef wave_agent fill:#f1e9ff,stroke:#7a3fe0,stroke-width:2px,color:#7a3fe0;
classDef card_agent fill:#e6d8ff,stroke:#7a3fe0,stroke-width:1px,color:#1d1d1f;
classDef wave_store fill:#e8f8ee,stroke:#1f9d55,stroke-width:2px,color:#1f9d55;
classDef card_store fill:#d4f2df,stroke:#1f9d55,stroke-width:1px,color:#1d1d1f;
classDef wave_control fill:#e4f5f3,stroke:#148f7a,stroke-width:2px,color:#148f7a;
classDef card_control fill:#cfeeea,stroke:#148f7a,stroke-width:1px,color:#1d1d1f;
classDef wave_harness fill:#f2f2f2,stroke:#6e6e73,stroke-width:2px,color:#6e6e73;
classDef card_harness fill:#e6e6e6,stroke:#6e6e73,stroke-width:1px,color:#1d1d1f;
classDef wave_guard fill:#fbe9e7,stroke:#c0392b,stroke-width:2px,color:#c0392b;
classDef card_guard fill:#f8d7d2,stroke:#c0392b,stroke-width:1px,color:#1d1d1f;
subgraph W1["PARENT: runs the wizard with no keyboard and no Ink of its own"]
direction LR
W1C1("<b>ControlClient / curl</b><br/>one typed call per route, so a parent<br/>never learns the store or the screens")
W1C2("<b>snapshot route + MCP proxy</b><br/>CI and coding agents drive the real TUI<br/>through the socket, never its stdin")
W1C1 ~~~ W1C2
end
class W1 wave_harness
class W1C1,W1C2 card_harness
subgraph W2["src/store/control: the store put on a socket, nothing more"]
direction LR
W2C1("<b>server.ts</b><br/>one route per store question or setter;<br/>hooks only where the store has no answer")
W2C2("<b>state.ts</b><br/>projects the committed session whitelist<br/>so a parent sees what the TUI sees, no secrets")
W2C3("<b>actions.ts</b><br/>each action is the setter the screen key<br/>handler calls: parity with the keyboard")
W2C4("<b>runs.ts + driver.ts</b><br/>remembers each independent run and its end<br/>state; waits on the version instead of polling")
W2C1 ~~~ W2C2 ~~~ W2C3 ~~~ W2C4
end
class W2 wave_control
class W2C1,W2C2,W2C3,W2C4 card_control
subgraph W3["src/store: one run's truth, the flow that chains runs, and what publishes them"]
direction LR
W3C1("<b>state/ RunStore</b><br/>the committed state of one agent run: its session<br/>copy, tasks, phase, outro, the questions it asks")
W3C2("<b>state/ FlowStore</b><br/>chains runs on top: the flow, gates, interrupts, the<br/>session every run inherits; mirrors the active run")
W3C3("<b>task-stream/</b><br/>store-level publisher: mirrors the run state<br/>outward to PostHog and files, renderer-blind")
W3C4("<b>ui/ WizardUI + StoreUI</b><br/>the only way an agent writes: StoreUI turns<br/>its calls into store commits")
W3C1 ~~~ W3C2 ~~~ W3C3 ~~~ W3C4
end
class W3 wave_store
class W3C1,W3C2,W3C3,W3C4 card_store
subgraph W4["src/cli: owns the store and composes runs above it"]
direction LR
W4C1("<b>control-hooks.ts</b><br/>does the parent's bidding: auth, detection,<br/>one scoped run at a time on a fresh RunStore, exit")
W4C2("<b>runners/</b><br/>create the store, attach the server where a<br/>surface runs (TUI in dev builds, headless everywhere)")
W4C3("<b>the TUI on top</b><br/>chains agent runs through FlowStore.startRun;<br/>UiStore holds presentation above, never inside a run")
W4C1 ~~~ W4C2 ~~~ W4C3
end
class W4 wave_cli
class W4C1,W4C2,W4C3 card_cli
subgraph W5["src/agent: one run, no screen, no socket"]
direction LR
W5C1("<b>runAgent</b><br/>runs one program from its run config; writes<br/>back only through WizardUI, never to a store or screen directly")
end
class W5 wave_agent
class W5C1 card_agent
W1 -->|"HTTP/1.1 over a 0600 unix socket, JSON<br/>the whole parent contract: read state, commit one action, ask for what the store cannot do"| W2
W2 -->|"reads ControlState · commits through exactly one setter · requestRun<br/>the mirror is the store itself; nothing is computed for the wire"| W3
W3 -->|"ControlHooks: credentials, detection, one run, exit<br/>the parts above the store, answered by the layer that owns the store"| W4
W4 -->|"RunAgent(runConfig, runSession, composed)<br/>one independent run: the agent sees its run config and session copy, nothing else"| W5
Interfaces, one sheet eachHow to run this stepHeadless surface, every build (the key travels in the environment, an issued gateway bearer replaces the mint): POSTHOG_WIZARD_API_KEY=phx_... WIZARD_CI_GATEWAY_TOKEN_FILE=/path/to/token.txt \
npx tsx bin.ts --headless-DONOTUSE-EXPERIMENTAL --control-socket /tmp/w/w.sock \
--project-id <id> --region us --install-dir /tmp/app
mkdir -p /tmp/w
curl -s --unix-socket /tmp/w/w.sock http://localhost/health
curl -s --unix-socket /tmp/w/w.sock -X POST -H 'content-type: application/json' -d '{}' http://localhost/detect
curl -s --unix-socket /tmp/w/w.sock -X POST -H 'content-type: application/json' \
-d '{"programId":"posthog-integration"}' http://localhost/runs
curl -s --unix-socket /tmp/w/w.sock 'http://localhost/state?wait=60000&since=0' | jq '.state.session.runPhase, .state.tasks'
curl -s --unix-socket /tmp/w/w.sock http://localhost/runs
curl -s --unix-socket /tmp/w/w.sock -X POST http://localhost/shutdownThe same sequence, scripted, printing every request and a redacted view of every response (the first transcript below is its output): APP_DIR=/tmp/app PROJECT_ID=<id> POSTHOG_REGION=us POSTHOG_KEY_FILE=/path/to/phx-key.txt \
WIZARD_CI_GATEWAY_TOKEN_FILE=/path/to/token.txt \
npx tsx scripts/controlled-headless-smoke.no-jest.ts posthog-integration metricsControlled TUI, dev builds only (the real screens in a terminal, a parent on the socket): POSTHOG_WIZARD_API_KEY=phx_... npx tsx bin.ts --ci --control-socket /tmp/w/w.sock --project-id <id> --region us --install-dir /tmp/app
curl -s --unix-socket /tmp/w/w.sock http://localhost/state | jq '.state.currentScreen, [.state.actions[].id]'
curl -s --unix-socket /tmp/w/w.sock -X POST -H 'content-type: application/json' -d '{}' http://localhost/actions/confirm_setup
curl -s --unix-socket /tmp/w/w.sock -X POST http://localhost/credentials
curl -s --unix-socket /tmp/w/w.sock -X POST http://localhost/runPublished builds refuse Evidence: controlled headless, basic integration then metrics, two independent runs over one socketRun on 2026-09-19 against a throwaway copy of Transcript (long polls collapsed to the last line per run)Evidence: controlled TUI in a pseudo-terminal (no agent run)The real TUI under node-pty, driven only over the socket: intro, readiness probe, auth resolved by Bundle audit
|
A headless run with WIZARD_CI_GATEWAY_TOKEN_FILE in its environment configures the gateway from that file, as --ci does, instead of minting a token. Generated-By: PostHog Desktop Task-Id: d14e92bb-6ee1-49b5-8502-39cb80079589
The control hooks mark a run that ended while running as completed and mark detection complete after a program's ciPreRun, so the ledger, the stream, and the state agree with the headless runner. The chunk manifest is keyed by source-module group instead of chunk file name, because rolldown's chunk names differ by platform; CI's fixture diff now compares content. The development skill documents the surfaces and how to drive the control API. Generated-By: PostHog Desktop Task-Id: d14e92bb-6ee1-49b5-8502-39cb80079589
GET /state is the committed session whitelist (CONTROL_SESSION_KEYS, credentials as a flag), the run atoms, and the actions legal on the current screen. The run status, the overlay flag, the context digest, and the reduced views go; a ledger record's result is the state at the end of the run. POST /run is requestRun on the store, so ControlHooks shrinks to what the store cannot do itself: credentials, detection, one run, shutdown. Generated-By: PostHog Desktop Task-Id: d14e92bb-6ee1-49b5-8502-39cb80079589
Routes that rewrite the session or end the process (POST /credentials, /detect, /shutdown) answer 409 while a run is in flight; a stale-socket probe only unlinks a socket; close() answers pending long polls before it drops connections and unlinks synchronously; the 413 reaches the client; a bad action id is a 400; the ledger records the resolved install dir and survives a hook that throws synchronously. Detection publishes what ciPreRun wrote to its copy as well as what it committed; every run calls runCleanups and keys its task stream by its own skill; the headless destination reads credentials from the live store; the CI gateway bearer is dev-build only and skipped for programs without an agent run. The default intro's confirm_setup follows the scan-sharing toggle like Enter, and action params are validated instead of coerced. Generated-By: PostHog Desktop Task-Id: d14e92bb-6ee1-49b5-8502-39cb80079589
The bundle audit compares these with the manifests CI builds; the new modules and the regrouped chunks belong in them. A failed comparison now uploads its manifests and reports both files. Generated-By: PostHog Desktop Task-Id: d14e92bb-6ee1-49b5-8502-39cb80079589
Serve a control API over a unix socket behind
--control-socket.HTTP/1.1, JSON in and out: state with long polling, actions through store setters, credentials, run arming on the TUI surface, detection and independent runs with a ledger on the headless surface, shutdown. Published builds keep it for headless runs and refuse the flag on the TUI, whose bundle never contains the server;
scripts/smoke-test.shaudits the chunks and CI diffs the chunk manifests against the committed fixtures. Stacked on #1276.Created with PostHog Desktop