Conversation
Every module moves under its surface: src/agent (runner, harnesses, switchboard, gateway, security adapters, tool facades), src/store (state, session, programs, tools, detection, frameworks, services, shared), src/tui (everything Ink plus console renderers and program content), src/cli (entry, commands, runners). Same-surface imports are relative; cross-surface imports use the surface aliases @store/*, @agent/*, @tui/*, @cli/*. Vitest projects and the boundary classifier follow the new directories; env.ts owns the headless flag literal. Goldens are byte identical; 19 cross-surface edges remain seeded for the next commit. Generated-By: PostHog Desktop Task-Id: d14e92bb-6ee1-49b5-8502-39cb80079589
Relocate authenticate, headless-mode, telemetry, yara hooks and policy, agent-phase, and self-driving pricing into the store; dispatch-family into the cli. The detection agent and the MCP prompt runner are installed from the entry point, the tools barrel no longer reaches the MCP facade, and the family picker takes a generic child shape. known-violations.json is empty. Generated-By: PostHog Desktop Task-Id: d14e92bb-6ee1-49b5-8502-39cb80079589
…rce them Every import between store, agent, tui, and cli goes through the surface's index.ts, types.ts, or a secondary barrel (@store/programs, @tui/console). The boundary test enforces the matrix and the public entries with no allowlist; tsc -b project references, per-project Vitest ink aliases, and ESLint no-restricted-imports mirror it. bin.ts runs the Node preflight and then imports src/cli/main.ts, which installs the renderer, the detection agent, and the MCP prompt runner lazily so no command loads Ink or the agent SDK before it runs. Tests that reached across surfaces moved to the surface they exercise; docs point at the new paths. Generated-By: PostHog Desktop Task-Id: d14e92bb-6ee1-49b5-8502-39cb80079589
…ntracts Each surface owns a vitest.config.ts built from vitest.shared.ts that resolves only the surfaces it may import and forbids ink outside the tui and the harness. Tests move to the surface whose code they exercise. Every surface gains a contract test and a testing/ double: StoreUI and NullUI implement WizardUI, ProgramConfig extends ProgramRunConfig, runConfigFor emits only the run contract, both harnesses and FakeAgentHarness implement AgentHarness, runAgent is the RunAgent entry, the console renderers are non-interactive WizardUI implementations, UiStore satisfies UiStoreApi, and every command has a unique key. The architecture suite snapshots the names each surface consumes from another and derives the WizardStore members the tui, cli, and harness touch, which must equal STORE_BOUNDARY_MEMBERS. 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. |
The composite surface projects resolve cross-surface imports through built declarations, so lint saw `any` until `tsc -b` had run and CI reported errors that vanish with types present. Boundaries stay enforced by tsc -b and the architecture suite. Generated-By: PostHog Desktop Task-Id: d14e92bb-6ee1-49b5-8502-39cb80079589
Surfaces: why each piece existsTop to bottom is the import direction. A surface sees only the public entries of the layer below; instances cross the other way by injection. The three numbered ways in mark where you start for the full TUI [1], a headless workflow [2], and one agent run alone [3]; dashed lines lead to the cards each way reaches. 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 E1["THREE WAYS IN: how much of the tree each job needs"]
direction LR
E1C1("<b>[1] run the full thing</b><br/>npx @posthog/wizard [command]: bin.ts, @cli/main,<br/>dispatchProgram, run-wizard.ts; the TUI chains one<br/>startRun + runAgent per flow step")
E1C2("<b>[2] run a workflow headlessly</b><br/>wizard [command] --headless-DONOTUSE-EXPERIMENTAL<br/>--api-key --project-id --install-dir: run-non-interactive.ts<br/>FlowStore + HeadlessUI, detect, one runAgent, outro to the log<br/>+ --control-socket: POST /detect, POST /runs, POST /shutdown")
E1C3("<b>[3] run just an agent</b><br/>in process: runAgent(runConfigFor(config), session) from<br/>@agent; no flow, no screens. Over the socket (headless<br/>surface): POST /runs programId = one run on a fresh RunStore")
E1C1 ~~~ E1C2 ~~~ E1C3
end
class E1 wave_entry
class E1C1,E1C2,E1C3 card_entry
subgraph W1["ENTRY: start the process without loading any surface"]
direction LR
W1C1("<b>bin.ts [1]</b><br/>checks Node, then hands off to the composition<br/>root; nothing else may be the npm entry")
W1C2("<b>src/env.ts</b><br/>build-time constants every surface may read,<br/>so no surface imports another just for them")
W1C1 ~~~ W1C2
end
class W1 wave_entry
class W1C1,W1C2 card_entry
subgraph W2["src/cli: the one place that knows all three surfaces"]
direction LR
W2C1("<b>main.ts</b><br/>installs the console renderer and the seams<br/>before any command runs")
W2C2("<b>wizard.ts + commands/ [2]</b><br/>turns argv into one surface choice:<br/>TUI, headless, or controlled by a parent")
W2C3("<b>runners/ [1] [2]</b><br/>sequence independent agent runs and pass<br/>context between them; nothing below composes")
W2C4("<b>control-hooks.ts [3]</b><br/>what a parent may ask this process to do<br/>that the store cannot (P4)")
W2C1 ~~~ W2C2 ~~~ W2C3 ~~~ W2C4
end
class W2 wave_cli
class W2C1,W2C2,W2C3,W2C4 card_cli
subgraph W3["src/tui and src/agent: never import each other, never share a helper"]
direction LR
W3C1("<b>src/tui</b><br/>renders the flow store and the run it mirrors;<br/>UiStore holds presentation state on top")
W3C2("<b>src/agent [3]</b><br/>runs one program; speaks to whoever renders<br/>through WizardUI, never to a screen")
W3C1 ~~~ W3C2
end
class W3 wave_tui
class W3C1,W3C2 card_tui
subgraph W4["src/store: one run's truth, the flow above it, product knowledge as data, no rendering"]
direction LR
W4C1("<b>state/ RunStore + FlowStore</b><br/>one run's state, and the flow that chains runs on<br/>top; the active screen is derived, never navigated to")
W4C2("<b>ui/ WizardUI</b><br/>the agent's whole vocabulary toward a renderer:<br/>StoreUI commits, LoggingUI prints, NullUI is silent")
W4C3("<b>task-stream/</b><br/>publishes the run state outward, store-level<br/>and renderer-blind")
W4C4("<b>programs/</b><br/>what to run and what to show, as data;<br/>the registry never touches Ink")
W4C5("<b>services · tools · detection</b><br/>the wizard's capabilities, product-blind<br/>and renderer-blind")
W4C6("<b>control/ [2] [3]</b><br/>the store put on a socket for parents and<br/>CI (P4)")
W4C1 ~~~ W4C2 ~~~ W4C3 ~~~ W4C4 ~~~ W4C5 ~~~ W4C6
end
class W4 wave_store
class W4C1,W4C2,W4C3,W4C4,W4C5,W4C6 card_store
subgraph W5["ENFORCEMENT: three checks, all required"]
direction LR
W5C1("<b>tsc -b</b><br/>a deep import fails to compile: each surface<br/>references only the surfaces it may import")
W5C2("<b>src/__tests__/architecture</b><br/>the import matrix, public entries, shape files,<br/>startup graph, and consumed names as goldens")
W5C3("<b>smoke-test.sh + chunk manifest</b><br/>the published TUI never carries the control<br/>server; CI diffs the chunk groups")
W5C1 ~~~ W5C2 ~~~ W5C3
end
class W5 wave_guard
class W5C1,W5C2,W5C3 card_guard
W1 -->|"import('@cli/main') the only import bin.ts makes"| W2
W2 -->|"setUI(StoreUI) · startTUI · runAgent · attachControlServer<br/>instances cross by injection, never by import"| W3
W3 -->|"FlowStoreApi (tui reads and commits) · WizardUI + ProgramRunConfig (agent reads and reports)<br/>both import only @store, @store/types, @store/programs"| W4
W4 -->|"a boundary cannot drift in one place alone"| W5
E1 -.->|"[1] npx @posthog/wizard → bin.ts"| W1
E1 -.->|"[1] dispatchProgram → run-wizard.ts: startTUI, then startRun + runAgent per step"| W2
E1 -.->|"[2] --headless-DONOTUSE-EXPERIMENTAL → dispatchProgram → run-non-interactive.ts"| W2
E1 -.->|"[3] POST /runs programId → control-hooks.ts startRun: fresh RunStore, one run"| W2
E1 -.->|"[3] runAgent(runConfigFor(config), session): one run, nothing else loaded"| W3
E1 -.->|"[2] [3] + --control-socket → control/: POST /detect, POST /runs, POST /shutdown"| W4
linkStyle 18 stroke:#2f6fed,stroke-width:2px,stroke-dasharray:6 4;
linkStyle 19 stroke:#2f6fed,stroke-width:2px,stroke-dasharray:6 4;
linkStyle 20 stroke:#e8632c,stroke-width:2px,stroke-dasharray:6 4;
linkStyle 21 stroke:#7a3fe0,stroke-width:2px,stroke-dasharray:6 4;
linkStyle 22 stroke:#7a3fe0,stroke-width:2px,stroke-dasharray:6 4;
linkStyle 23 stroke:#e8632c,stroke-width:2px,stroke-dasharray:6 4;
Interfaces, one sheet eachEvidence: basic integration through the snapshot path (P3 tree)Run on 2026-09-19 against a throwaway copy of Result: 25 frames, screen path FramesIntro frame (ANSI stripped)Outro frame (ANSI stripped)Gates on this PR
|
How to run this steppnpm install
pnpm typecheck # tsc -b tsconfig.solution.json: one composite project per surface
pnpm typecheck:store # or agent, tui, cli, harness, arch
pnpm test # builds, then every Vitest project
pnpm test:store # or agent, tui, cli, harness, arch: one project, its aliases only
pnpm test:arch # import matrix, public entries, shape files, startup graph
pnpm lint # prettier + eslint, typed against the whole-tree tsconfig
pnpm build && pnpm build:ci # both bundles, smoke tests, chunk manifestsA deep import across surfaces fails three times: Snapshot route at this step (the in-process host, replaced in #1278): PROGRAM=posthog-integration E2E_ASK=true POSTHOG_KEY_FILE=… WIZARD_CI_GATEWAY_TOKEN_FILE=… \
PROJECT_ID=<id> POSTHOG_REGION=us APP_DIR=/tmp/app SNAP_OUT=/tmp/snaps \
npx tsx scripts/tui-snapshots.no-jest.tsEach surface's |
Move the tree into
src/store,src/agent,src/tui, andsrc/cli, and enforce the boundaries.Cross-surface imports go only through
@store,@store/types,@store/programs,@agent,@agent/types,@tui,@tui/types, and@tui/console. The architecture suite enforces the matrix with no allowlist,tsc -bproject references and per-surface Vitest projects mirror it, and each surface carries a contract test and atesting/double. Goldens are byte identical to the P0 baselines. Stacked on #1271.Created with PostHog Desktop