Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
a34f2b5
refactor(tui): reach programs only through @programs entries
gewenyu99 Sep 23, 2026
84e5e0a
refactor(tui): reach agent capabilities through programs
gewenyu99 Sep 23, 2026
5b0593e
refactor(shared): delete uncalled setup-utils helpers
gewenyu99 Sep 23, 2026
aedecd2
refactor(shared): move the package.json readers into @utils/package-json
gewenyu99 Sep 23, 2026
752e010
refactor(programs): resolve logins through an injected host
gewenyu99 Sep 23, 2026
dfe51ef
refactor(programs): split the program session from the TUI session
gewenyu99 Sep 23, 2026
d300bf6
refactor(programs): type program hooks against ProgramSession
gewenyu99 Sep 23, 2026
fc85a37
refactor(programs): hoist detection and run composition off the steps
gewenyu99 Sep 23, 2026
56976ff
refactor(tui): move program flows into the TUI
gewenyu99 Sep 23, 2026
9b6d486
refactor(tui): dissolve the legacy session module
gewenyu99 Sep 23, 2026
1553a5b
refactor(tui): stop reaching into the CLI and the UI singleton
gewenyu99 Sep 23, 2026
ddf9b77
docs: point documentation at the TUI flows and session
gewenyu99 Sep 23, 2026
54c0da2
Merge posthog/functional-c1-move into posthog/functional-c2a-tui
gewenyu99 Sep 23, 2026
d5145f3
chore(tui): sync C2a with the restacked C1
gewenyu99 Sep 23, 2026
49cda16
chore(tui): sync C2a with the restacked B stack
gewenyu99 Sep 23, 2026
9270071
chore(tui): sync C2a with the joined C1
gewenyu99 Sep 23, 2026
8e49ee2
Merge posthog/functional-c1-move into posthog/functional-c2a-tui
gewenyu99 Sep 24, 2026
1c0fb33
Merge posthog/functional-c1-move into posthog/functional-c2a-tui
gewenyu99 Sep 24, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .claude/skills/adding-framework-support/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ itself.

| Starting point | Pattern to reuse |
| ------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------- |
| [Next.js](../../../src/programs/frameworks/nextjs/) | `hasDeclaredDependency` from `utils/package-json`, `tryGetPackageJson` from `utils/setup-utils`, and router setup questions |
| [Next.js](../../../src/programs/frameworks/nextjs/) | `hasDeclaredDependency` from `utils/package-json`, `tryGetPackageJson` from `utils/package-json`, and router setup questions |
| [Django](../../../src/programs/frameworks/django/) | Python project files, context gathering, and Python package-manager detection |
| [Laravel](../../../src/programs/frameworks/laravel/) | Composer and framework-specific filesystem signals |
| [Rails](../../../src/programs/frameworks/rails/) | Gemfile detection and Ruby conventions |
Expand Down
6 changes: 3 additions & 3 deletions .claude/skills/adding-skill-program/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -102,9 +102,9 @@ does not preserve these behaviors automatically.

## Screens, prerequisites, and composition

Reuse [AGENT_SKILL_STEPS](../../../src/programs/agent-skill/steps.ts):
Reuse [AGENT_SKILL_FLOW](../../../src/tui/flows/agent-skill.ts):
intro, health check, auth, run, outro, and keep-skills. Auth also applies the
shared [AI opt-in gate](../../../src/programs/ai-opt-in-gate.ts) for agent
shared [AI opt-in gate](../../../src/tui/flows/ai-opt-in-gate.ts) for agent
programs. Override `screenId`, not `screen`, when adapting a step. New screens
need an entry in [ScreenId](../../../src/tui/screen-sequences.ts), a
component, and registration in
Expand All @@ -121,7 +121,7 @@ walks `onReady` by default; set `ciPreRun` only when it needs a different
prerequisite strategy.

`requires` currently records metadata; it does not execute or enforce prior
programs. Compose real work through `ProgramStep.run`, with `onRunPrep` and
programs. Compose real work through `ProgramConfig.runSteps`, with `onRunPrep` and
`targetDir` when needed. The
[integration run step](../../../src/programs/posthog-integration/index.ts)
and [self-driving](../../../src/programs/self-driving/) demonstrate this.
Expand Down
2 changes: 1 addition & 1 deletion .claude/skills/ink-tui/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ resolution. Local state is appropriate for presentation details such as tab
selection, not wizard progression.

For new state, first decide whether it belongs in
[WizardSession](../../../src/lib/wizard-session.ts) or display-only store state.
[WizardSession](../../../src/tui/session.ts) or display-only store state.
Use an explicit setter that notifies subscribers. When business logic needs the
operation, extend `WizardUI`, [InkUI](../../../src/tui/ink-ui.ts), and
[LoggingUI](../../../src/headless/renderers/logging-ui.ts) together. Reuse existing enums and
Expand Down
8 changes: 4 additions & 4 deletions .claude/skills/ink-tui/references/ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@

| Surface | Owns |
| ----------------------------------------------------------------- | ----------------------------------------------------------------------------- |
| [WizardSession](../../../../src/lib/wizard-session.ts) | Run configuration, decisions, credentials, and lifecycle state |
| [ProgramStep](../../../../src/programs/program-step.ts) | Screens, visibility/completion predicates, gates, and initialization hooks |
| [WizardSession](../../../../src/tui/session.ts) | Run configuration, decisions, credentials, and lifecycle state |
| [FlowStep](../../../../src/tui/flow.ts) | Screens, visibility/completion predicates, gates, and initialization hooks |
| [screen-sequences.ts](../../../../src/tui/screen-sequences.ts) | `ScreenId`, `Screen`, `Sequence`, and the derived `PROGRAM_SEQUENCES` |
| [WizardRouter](../../../../src/tui/router.ts) | Resolution and the `Overlay` stack |
| [WizardStore](../../../../src/tui/store.ts) | Reactive state, gate promises, display observations, and pending interactions |
Expand All @@ -17,7 +17,7 @@
entries: `id`, `show`, and `isComplete`. Completion defaults to the step's
`gate` when no separate `isComplete` is provided. Steps without a screen are
omitted, and the exit screen is appended. The projection applies
[withAiOptInGate](../../../../src/programs/ai-opt-in-gate.ts) consistently
[withAiOptInGate](../../../../src/tui/flows/ai-opt-in-gate.ts) consistently
with store gate creation.

`WizardRouter.resolve(session)` first checks the overlay stack, then returns the
Expand All @@ -32,7 +32,7 @@ from the agent execution sequence selected by the runner; see

## Gates and initialization

Read `ProgramStep` before adding asynchronous work:
Read `FlowStep` before adding asynchronous work:

- `gate` supplies a blocking checkpoint, while `isComplete` determines when its
screen is finished. Separate them when those conditions differ.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ state that a step's `show`, `isComplete`, or `gate` predicate reads. Use the
existing overlay mechanism for a real overlay.

`requires` documents a prerequisite but does not run it. Detect prerequisites
explicitly or compose work through `ProgramStep.run`. Use store setters for
explicitly or compose work through `ProgramConfig.runSteps`. Use store setters for
shared session data; do not rely on another program having populated it by
accident. Shared detection can be a function called by both `onReady` hooks.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ signatures.
| `postRun(session, credentials)` | Linear success path, before outro |
| `buildOutroData(session, credentials)` | Linear custom outro; otherwise defaults from run metadata |
| `agentFlow` and task skill variants | Orchestrator flow selection and task execution |
| `ProgramStep.run` | Explicit program composition in the outer lifecycle |
| `ProgramConfig.runSteps` | Explicit program composition in the outer lifecycle |
| `ProgramConfig.requires` | Dependency metadata; it does not execute prerequisite programs |

Do not migrate a linear program merely by changing its binding if it depends on
Expand Down
1 change: 0 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -513,7 +513,6 @@ Path aliases defined in `tsconfig.build.json`, resolved by tsdown:
| Alias | Maps to |
|---|---|
| `@env` | `src/env.ts` |
| `@lib/*` | `src/lib/*` |
| `@agent` | `src/agent/index.ts`, the agent's runtime entry; the only agent import outside `src/agent` besides types |
| `@agent/types` | `src/agent/types.ts`, type-only |
| `@agent/*` | `src/agent/*`, inside `src/agent` and its tests only |
Expand Down
10 changes: 6 additions & 4 deletions e2e-harness/__tests__/e2e-flow-snapshot.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@
import { WizardStore } from '@tui/store';
import { InkUI } from '@tui/ink-ui';
import { setUI } from '@ui/index';
import { buildSession, RunPhase } from '@lib/wizard-session';
import { Integration } from '@shared/constants';
import { HostResolution } from '@shared/host-resolution';
import { FRAMEWORK_REGISTRY } from '@programs/registry';
Expand All @@ -27,6 +26,9 @@ import { SELF_DRIVING_INTEGRATE_PATH_KEY } from '@programs/self-driving/detect';
import { WizardCiDriver } from '../wizard-ci-driver';
import { decideE2eAction, type WizardE2eProfile } from '../e2e-profile';
import { profileFor } from '../profiles';
import { rawProgramFlow } from '@tui/flows/index';
import { buildSession } from '@tui/session';
import { RunPhase } from '@shared/run-state';

/**
* Walk a program flow offline using an e2e profile, injecting the external
Expand Down Expand Up @@ -113,14 +115,14 @@ function traceFlow(
// child program, e.g. self-driving's integrate-run) and the program's own
// run. Complete the active run step the way the runner would: a composed
// step via completeRunStep, the main run via runPhase.
const steps = getProgramConfig(store.router.activeProgram).steps;
const runStep = steps.find(
const config = getProgramConfig(store.router.activeProgram);
const runStep = rawProgramFlow(config.id).find(
(s) =>
s.screenId === 'run' &&
(!s.show || s.show(store.session)) &&
(!s.isComplete || !s.isComplete(store.session)),
);
if (runStep?.runProgramId) {
if (runStep && config.runSteps?.[runStep.id]?.runProgramId) {
store.completeRunStep(runStep.id);
} else {
store.setRunPhase(RunPhase.Completed);
Expand Down
2 changes: 1 addition & 1 deletion e2e-harness/__tests__/e2e-profile-ask.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@
*/

import { Overlay, ScreenId } from '@tui/router';
import type { AskQuestion } from '@lib/wizard-session';
import {
DEFAULT_E2E_PROFILE,
E2E_ANSWER_SENTINEL,
Expand All @@ -20,6 +19,7 @@ import {
import { profileFor, resolveE2eProfile } from '../profiles';
import { Program } from '@programs';
import type { CiState } from '../wizard-ci-driver';
import type { AskQuestion } from '@agent/types';

const text = (id: string, prompt = id): AskQuestion => ({
id,
Expand Down
6 changes: 4 additions & 2 deletions e2e-harness/__tests__/e2e-result.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,6 @@
import fs from 'fs';
import os from 'os';
import path from 'path';
import { OutroKind, RunPhase } from '@lib/wizard-session';
import type { AskQuestion, WizardSession } from '@lib/wizard-session';
import { DETECTED_WAREHOUSE_SOURCES_KEY } from '@programs/warehouse-source/detect';
import { Overlay } from '@tui/router';
import { TASK_OUTCOMES_KEY } from '@agent';
Expand All @@ -27,6 +25,10 @@ import {
} from '../e2e-result';
import { DEFAULT_E2E_PROFILE, decideE2eAction } from '../e2e-profile';
import type { CiState } from '../wizard-ci-driver';
import { OutroKind } from '@shared/outro';
import { RunPhase } from '@shared/run-state';
import type { AskQuestion } from '@agent/types';
import type { WizardSession } from '@tui/session';

const SECRET = 'sk_live_do_not_leak_9f2b';

Expand Down
5 changes: 3 additions & 2 deletions e2e-harness/__tests__/wizard-ci-driver.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@
import { WizardStore } from '@tui/store';
import { InkUI } from '@tui/ink-ui';
import { setUI } from '@ui/index';
import { buildSession, RunPhase, McpOutcome } from '@lib/wizard-session';
import { HostResolution } from '@shared/host-resolution';
import { Integration } from '@shared/constants';
import { FRAMEWORK_REGISTRY } from '@programs/registry';
Expand All @@ -22,7 +21,9 @@ import { Program } from '@programs';
import { WizardCiDriver, UnknownActionError } from '../wizard-ci-driver';
import { ACTION_REGISTRY, NO_ACTION_SCREENS } from '../action-registry';
import { SOURCE_MAPS_CONTEXT_KEYS } from '@programs/error-tracking-upload-source-maps/index';
import { OutroKind } from '@lib/wizard-session';
import { buildSession } from '@tui/session';
import { RunPhase, McpOutcome } from '@shared/run-state';
import { OutroKind } from '@shared/outro';

function freshStore(): WizardStore {
const store = new WizardStore(Program.PostHogIntegration);
Expand Down
5 changes: 3 additions & 2 deletions e2e-harness/action-registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,6 @@

import type { WizardStore } from '@tui/store';
import { ScreenId, Overlay, type ScreenName } from '@tui/router';
import { McpOutcome, OutroKind } from '@lib/wizard-session';
import type { AskAnswers } from '@lib/wizard-session';
import {
SOURCE_MAPS_CONTEXT_KEYS,
VARIANT_DISPLAY_NAME,
Expand All @@ -24,6 +22,9 @@ import {
GITHUB_REQUIRED_BODY,
GITHUB_REQUIRED_MESSAGE,
} from '@programs/self-driving/detect';
import { McpOutcome } from '@shared/run-state';
import { OutroKind } from '@shared/outro';
import type { AskAnswers } from '@agent/types';

/** One commit action legal on a given screen. */
export interface DriverAction {
Expand Down
2 changes: 1 addition & 1 deletion e2e-harness/e2e-profile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@
*/

import { ScreenId, Overlay, type ScreenName } from '@tui/router';
import type { AskAnswers, AskQuestion } from '@lib/wizard-session';
import type { CiState } from './wizard-ci-driver.js';
import type { AskAnswers, AskQuestion } from '@agent/types';

/** Which option to pick for a setup disambiguation question. */
export type SetupChoice = 'first' | 'last';
Expand Down
3 changes: 2 additions & 1 deletion e2e-harness/e2e-result.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,14 @@

import fs from 'fs';
import path from 'path';
import { OutroKind, type WizardSession } from '@lib/wizard-session';
import { RunPhase } from '@shared/run-state';
import { TASK_OUTCOMES_KEY } from '@agent';
import type { TaskOutcome } from '@agent/types';
import { DETECTED_WAREHOUSE_SOURCES_KEY } from '@programs/warehouse-source/detect';
import type { DetectedSource } from '@programs/warehouse-sources/types';
import type { E2eDecisionReport } from './e2e-profile.js';
import { OutroKind } from '@shared/outro';
import type { WizardSession } from '@tui/session';

/** One `wizard_ask` batch the run was shown. */
export interface E2eAskRecord {
Expand Down
3 changes: 2 additions & 1 deletion e2e-harness/wizard-ci-driver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,9 @@

import type { WizardStore } from '@tui/store';
import type { ScreenName } from '@tui/router';
import type { PendingQuestion, RunPhase } from '@lib/wizard-session';
import { actionsForScreen, MissingParamError } from './action-registry.js';
import type { PendingQuestion } from '@agent/types';
import type { RunPhase } from '@shared/run-state';

/** A setup question projected for the harness (no `detect` fn, no closures). */
export interface SetupQuestionView {
Expand Down
67 changes: 21 additions & 46 deletions scripts/tui-host.no-jest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,13 @@ import fs from 'fs';
import net from 'net';
import { spawnSync } from 'child_process';
import { startTUI } from '@tui/start-tui';
import { rawProgramFlow } from '@tui/flows/index';
import { advanceStep } from '@cli/runners/run-wizard';
import { cliAuthHost } from '@cli/runners/auth-host';
import { getUI } from '@ui';
import { VERSION } from '@shared/version';
import { Program, getProgramConfig, type ProgramId } from '@programs';
import type { Harness, Sequence } from '@shared/constants';
import { buildSession } from '@lib/wizard-session';
import { initLocalDev } from '@shared/local-dev';
import { loadCiInferenceAuthProvider } from '@cli/runners/ci-inference-auth';
import type { InferenceAuthProvider } from '@agent/types';
Expand All @@ -33,7 +35,7 @@ import {
} from '@programs/task-stream/index';
import { getAuditChecks } from '@programs/audit/types';
import { authenticate } from '@programs/authenticate';
import { getOrAskForProjectData } from '@utils/setup-utils';
import { getOrAskForProjectData } from '@programs/project-data';
import { logToFile } from '@utils/debug';
import { join } from 'path';
import { detectFramework } from '@programs/detection/index';
Expand All @@ -60,6 +62,7 @@ import {
readReportFile,
} from '@e2e-harness/e2e-result';
import { tuiSnapshotSignature } from '@e2e-harness/tui-snapshot-signature';
import { buildSession } from '@tui/session';

const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms));
const mark = (m: string) => logToFile(`[tui-host] ${m}`);
Expand Down Expand Up @@ -278,13 +281,16 @@ async function main() {
// Resolve credentials from the phx key (same bearer as an OAuth token) and set
// them on the store — advances the auth screen with no browser, no keystrokes.
const authByState = async () => {
const d = await getOrAskForProjectData({
signup: false,
ci: true,
apiKey,
projectId: Number(projectId),
programId,
});
const d = await getOrAskForProjectData(
{
signup: false,
ci: true,
apiKey,
projectId: Number(projectId),
programId,
},
cliAuthHost(),
);
store.setCredentials({
accessToken: d.accessToken,
projectApiKey: d.projectApiKey,
Expand All @@ -306,45 +312,14 @@ async function main() {
// or scope their own run to a picked project (error-tracking).
// `authenticate` here resolves the phx key, not OAuth, since the session is
// built with ci + apiKey.
if (programConfig.steps.some((s) => s.runProgramId || s.targetDir)) {
const runSessionFor = async (
step: (typeof programConfig.steps)[number],
) => {
const live = store.session;
const previousLabel = live.detectedFrameworkLabel;
const runSession = step.targetDir
? {
...live,
installDir: step.targetDir(live),
frameworkContext: { ...live.frameworkContext },
}
: live;
if (step.onRunPrep) await step.onRunPrep(runSession);
if (
runSession.detectedFrameworkLabel &&
runSession.detectedFrameworkLabel !== previousLabel
) {
store.setDetectedFramework(runSession.detectedFrameworkLabel);
}
return runSession;
};
for (const step of programConfig.steps) {
if (
programConfig.runSteps &&
Object.keys(programConfig.runSteps).length > 0
) {
for (const step of rawProgramFlow(programConfig.id)) {
if (step.screenId === 'outro') break;
if (step.show && !step.show(store.session)) continue;
if (step.screenId === 'auth') {
await authenticate(store.session, programConfig.id, getUI());
} else if (step.runProgramId) {
await runProgramAgent(
getProgramConfig(step.runProgramId),
await runSessionFor(step),
{ composed: true },
);
store.completeRunStep(step.id);
} else if (step.screenId === 'run') {
await runProgramAgent(programConfig, await runSessionFor(step));
} else if (step.isComplete) {
await store.waitUntil(step.isComplete);
}
await advanceStep(step, store, programConfig);
}
} else {
await runProgramAgent(programConfig, store.session);
Expand Down
1 change: 0 additions & 1 deletion src/__tests__/architecture/import-boundaries.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -376,7 +376,6 @@ it('keeps the callable runProgram closure free of UI, session and legacy imports
file.startsWith('src/ui/') ||
file.startsWith('src/tui/') ||
file.startsWith('src/headless/') ||
file.startsWith('src/lib/wizard-session') ||
file.startsWith('src/cli/runners/') ||
file.startsWith('src/cli/commands/') ||
file.startsWith('src/programs/task-stream/'),
Expand Down
Loading
Loading