Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
67 commits
Select commit Hold shift + click to select a range
2f959db
test(tui): capture status changes and final flow result
gewenyu99 Sep 22, 2026
d53491b
test(programs): preserve gate state and resolve e2e aliases
gewenyu99 Sep 22, 2026
c15f629
test(e2e): run live TUI parity on temporary app copies
gewenyu99 Sep 22, 2026
e947ed0
chore(programs): sync B3 with gateway-owned B2
gewenyu99 Sep 22, 2026
ccea2e7
refactor(programs): cut deck and task-stream UI dependencies
gewenyu99 Sep 22, 2026
f585791
refactor(programs): narrow legacy session dependencies
gewenyu99 Sep 22, 2026
220c846
refactor(programs): cut CLI dispatch and option imports
gewenyu99 Sep 22, 2026
a0046d1
refactor(programs): move framework labels behind config boundary
gewenyu99 Sep 22, 2026
835ed29
test(tui): isolate handoff keyboard parity from GitHub polling
gewenyu99 Sep 22, 2026
9157001
refactor(programs): narrow completion and run inputs
gewenyu99 Sep 22, 2026
81233fc
refactor(programs): pass authentication UI projection explicitly
gewenyu99 Sep 22, 2026
5377d83
refactor(programs): project live completion context at hook time
gewenyu99 Sep 22, 2026
241d3d4
refactor(programs): pass detection inputs and progress explicitly
gewenyu99 Sep 22, 2026
96f8ad6
refactor(ui): expose agent progress bridge at UI entry
gewenyu99 Sep 22, 2026
890cb02
refactor(programs): declare composed child runs by id
gewenyu99 Sep 22, 2026
cd2b9b3
fix(ui): consolidate progress bridge exports
gewenyu99 Sep 22, 2026
e5f9c8f
refactor(programs): narrow auth and warehouse inputs
gewenyu99 Sep 22, 2026
9a75ea4
refactor(programs): define explicit host capabilities
gewenyu99 Sep 22, 2026
d162457
refactor(programs): inject UI effects into project and symbol flows
gewenyu99 Sep 22, 2026
654951d
refactor(runners): move legacy program host out of programs
gewenyu99 Sep 22, 2026
b9cbbaa
docs: point legacy runner references to host adapter
gewenyu99 Sep 22, 2026
7a1dca2
refactor(programs): pass feature effects through host capabilities
gewenyu99 Sep 22, 2026
14c79b9
fix(programs): preserve host callback receivers
gewenyu99 Sep 22, 2026
630c05e
test(programs): assert forwarded host callbacks
gewenyu99 Sep 22, 2026
3fedc93
fix: cover run decks and audit runner bridge
gewenyu99 Sep 23, 2026
86698e8
Merge B3 TUI review fixes
gewenyu99 Sep 23, 2026
3dabc73
Merge A3 through B2 review fixes into B3
gewenyu99 Sep 23, 2026
1c989f1
Fix and typecheck live e2e host and harness
gewenyu99 Sep 23, 2026
ba12c43
Merge B3 live harness review fixes
gewenyu99 Sep 23, 2026
a5aee85
docs: remove ownership rows for deleted deck wrappers
gewenyu99 Sep 23, 2026
47c22b1
test: expect deferred skill cleanup in composed runs
gewenyu99 Sep 23, 2026
a9d63de
Merge standalone skill safety and final runner cleanup into B3
gewenyu99 Sep 23, 2026
eee6d58
test(e2e): replace the jest suite with three surface e2e routes
gewenyu99 Sep 23, 2026
e190a92
chore(programs): sync B3 with B2 and the A3 terminal-failure fix
gewenyu99 Sep 23, 2026
1acd24b
chore(e2e): bring the surface e2e routes into B3
gewenyu99 Sep 23, 2026
3acd86f
Merge updated agent outcomes and skill safety into B3
gewenyu99 Sep 23, 2026
8392b2e
Merge updated B2 ancestry into B3
gewenyu99 Sep 23, 2026
45b133e
chore(e2e): join the restacked B3 with the remote B3 review fixes
gewenyu99 Sep 23, 2026
3e9aa97
chore(programs): bring the joined B2 into B3
gewenyu99 Sep 23, 2026
a2e50d5
chore(e2e): delete the live-app command check and explain the snapsho…
gewenyu99 Sep 23, 2026
c142657
Merge posthog/functional-b-integration into posthog/functional-b3-parity
gewenyu99 Sep 23, 2026
17c02e4
Merge posthog/functional-b-integration into posthog/functional-b3-parity
gewenyu99 Sep 23, 2026
fbddbb2
Merge posthog/functional-b-integration into posthog/functional-b3-parity
gewenyu99 Sep 23, 2026
a4919dc
docs(programs): add the programs reference for the current contracts
gewenyu99 Sep 23, 2026
9044670
chore(e2e): move the headless program and agent routes to the workbench
gewenyu99 Sep 24, 2026
af5c8da
Merge posthog/functional-b-integration into posthog/functional-b3-parity
gewenyu99 Sep 24, 2026
50fe9e4
Merge posthog/functional-b-integration into posthog/functional-b3-parity
gewenyu99 Sep 24, 2026
746c704
Merge posthog/functional-b-integration into posthog/functional-b3-parity
gewenyu99 Sep 24, 2026
b6e02f4
docs: return the B3 doc changes to their B2 versions for B4
gewenyu99 Sep 24, 2026
f1b30d7
revert(e2e): keep the jest e2e suite until it moves to the workbench …
gewenyu99 Sep 24, 2026
fc31c4c
Merge the runProgram surface shell (a467ba90) as the new base
gewenyu99 Sep 24, 2026
1e88b92
chore(programs): return the tree to the runProgram shell
gewenyu99 Sep 24, 2026
d6df82e
feat(programs): implement runProgram and ProgramStore
gewenyu99 Sep 24, 2026
4bd59e7
refactor(programs): run the legacy adapter through runProgram
gewenyu99 Sep 24, 2026
25f0247
refactor(programs): run agentic detection through runAgent
gewenyu99 Sep 24, 2026
6698adc
test(programs): cover runProgram, the store, the adapter and detection
gewenyu99 Sep 24, 2026
81408df
Merge posthog/functional-b-integration into posthog/functional-b3-parity
gewenyu99 Sep 24, 2026
4556520
refactor(programs): call it runProgram, keep host for callers
gewenyu99 Sep 24, 2026
d9d8984
Merge posthog/functional-b-integration into posthog/functional-b3-parity
gewenyu99 Sep 25, 2026
1c1ecd6
Merge posthog/functional-b-integration into posthog/functional-b3-parity
gewenyu99 Sep 25, 2026
5ab8f66
refactor(programs): call it the runner, not the host
gewenyu99 Sep 25, 2026
b48d672
Merge posthog/functional-b-integration into posthog/functional-b3-parity
gewenyu99 Sep 25, 2026
c2e22bd
Merge posthog/functional-b-integration into posthog/functional-b3-parity
gewenyu99 Sep 25, 2026
f550e22
refactor(programs): current notes, caller not host, temporary adapter…
gewenyu99 Sep 25, 2026
c31e78d
Merge posthog/functional-b-integration into posthog/functional-b3-parity
gewenyu99 Sep 25, 2026
f94e757
Merge posthog/functional-b-integration into posthog/functional-b3-parity
gewenyu99 Sep 25, 2026
f2e42ac
fix(programs): keep runProgram off the session, keep rotations and di…
gewenyu99 Sep 25, 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
6 changes: 3 additions & 3 deletions src/agent/gateway-session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,9 +73,9 @@ export function configureGatewayCredentialsForCI(
};
}

// TODO(B2): CI credential loading belongs to the headless provider, not the
// agent. Leaves with the rest of this module once RunInput carries resolved
// inference auth.
// TODO: CI credential loading belongs outside the agent. It leaves with the rest
// of this module once RunInput carries resolved inference auth, later in the
// refactor.
export function configureGatewayFromCIEnvironment(
projectId: number,
region: CloudRegion,
Expand Down
20 changes: 5 additions & 15 deletions src/agent/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,22 +30,12 @@ export { resolveBinding, shouldDisableAsk } from './runner';
export { LONGER_ASK_TIMEOUT_MS } from './wizard-ask-bridge';

/**
* Leaves in B2. Programs own credentials and the legacy adapter dies.
* initializeAgent, executeAgent and buildRunTags are the pre-runAgent surface
* that detection/agentic.ts and run-agent-legacy.ts still call; they go
* through runAgent or leave with detection, and AgentErrorType, which
* classifies executeAgent's failures, goes with them.
* configureGatewayFromCIEnvironment is CI inference auth the headless provider
* owns. flushScanReport becomes a progress event rather than a call.
* downloadSkill leaves once the skill scan runs at load and skill install
* becomes shared.
* Leaves later in the refactor. buildRunTags builds the trace tags runProgram
* and agentic detection send. configureGatewayFromCIEnvironment loads the CI
* gateway token. flushScanReport becomes a progress event rather than a call.
* downloadSkill leaves once skill install becomes shared.
*/
export {
AgentErrorType,
buildRunTags,
initializeAgent,
runAgent as executeAgent,
} from './agent-interface';
export { buildRunTags } from './agent-interface';
export { configureGatewayFromCIEnvironment } from './gateway-session';
export { flushScanReport } from './yara-hooks';
export { downloadSkill } from './tools';
Expand Down
8 changes: 4 additions & 4 deletions src/agent/progress.ts
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,7 @@ export interface SpinnerHandle {
}

/**
* Context the agent attaches to a 401 so the host can pick the right copy.
* Context the agent attaches to a 401 so the caller can pick the right copy.
*
* `hasSettingsConflict` is true when a Claude Code settings file (project,
* project-local, the user's global config, or managed) actually overrides the
Expand Down Expand Up @@ -185,7 +185,7 @@ export interface AuthErrorDetail {
logFilePath: string;
}

/** One task as the host renders it. The same shape `WizardUI.syncTodos` takes. */
/** One task as the caller renders it. The same shape `WizardUI.syncTodos` takes. */
export interface TaskSnapshot {
id?: string;
source?: string;
Expand All @@ -207,7 +207,7 @@ export type ProgressLogLevel = 'info' | 'warn' | 'error' | 'success' | 'step';
export type AgentProgress =
/** The run's main work has started (`WizardUI.startRun`). */
| { kind: 'lifecycle'; phase: 'started' }
/** The run finished and the host may show its outro (`WizardUI.outro`). */
/** The run finished and the caller may show its outro (`WizardUI.outro`). */
| { kind: 'lifecycle'; phase: 'completed'; message: string }
/** The run spinner (`WizardUI.spinner()`), one handle per run. */
| {
Expand Down Expand Up @@ -246,7 +246,7 @@ export type ProgressEmitter = (event: AgentProgress) => void;
* so `wizard_ask` returns its existing "not available" error, and an optional
* task notice is declined — the same path a `--ci` run takes today.
* Each request's `signal` aborts when that request times out, the run's
* signal aborts, or another task fails the run. On that abort the host
* signal aborts, or another task fails the run. On that abort the caller
* dismisses that request alone, and that dismissal must not throw: abort
* listeners run where the agent cannot catch them, so Node would rethrow the
* error as an uncaught exception.
Expand Down
1 change: 1 addition & 0 deletions src/agent/runner/harness/anthropic/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ export const anthropicBackend: AgentHarness = {
abortCases: config.abortCases,
emitStepEvents: config.trackStepProgress ?? false,
resolveStepKey: config.resolveStepKey,
requestRemark: config.requestRemark,
triageProvider: boot.triageProvider,
},
middleware,
Expand Down
16 changes: 15 additions & 1 deletion src/agent/runner/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,10 @@ import type {
} from './shared/types';
import { prepareRun } from './shared/bootstrap';
import { createProgressCollector } from './shared/progress-collector';
import {
createTranscriptTail,
type TranscriptTail,
} from './shared/transcript-tail';
import { getSequence } from './switchboard';
import { flushScanReport } from '@agent/yara-hooks';

Expand Down Expand Up @@ -80,9 +84,15 @@ export async function runAgent(
options: RunAgentOptions = {},
): Promise<RunResult> {
let collector: ReturnType<typeof createProgressCollector> | undefined;
let transcript: TranscriptTail | undefined;
const snapshot = (): RunResult['snapshot'] => {
try {
if (collector) return collector.snapshot();
if (collector) {
const collected = collector.snapshot();
return transcript
? { ...collected, transcriptTail: transcript.text() }
: collected;
}
} catch {
// A partial snapshot must not replace the run's primary failure.
}
Expand All @@ -98,6 +108,8 @@ export async function runAgent(
};
};
const flushReport = (): void => {
// A deferred report keeps counting this run's scans toward the program run's.
if (config.scanReport === 'defer') return;
try {
const report = flushScanReport({ yaraReport: input.flags.yaraReport });
if (report)
Expand All @@ -110,6 +122,7 @@ export async function runAgent(
try {
collector = createProgressCollector(options.onProgress);
const { emit } = collector;
if (config.run.collectTranscript) transcript = createTranscriptTail(emit);
const log = (message: string) =>
emit({ kind: 'log', level: 'info', message });
if (options.signal?.aborted) {
Expand Down Expand Up @@ -143,6 +156,7 @@ export async function runAgent(
emit,
interaction: options.interaction,
signal: options.signal,
transcript,
});
result = {
...(options.signal?.aborted &&
Expand Down
35 changes: 25 additions & 10 deletions src/agent/runner/sequence/linear.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,19 +19,20 @@ import { AGENT_ERROR_CODE } from '@agent/error-map';
import { analytics } from '@utils/analytics';
import { formatYaraAbortMessage } from '@agent/yara-hooks';
import { installSkillById } from '@agent/tools';
import { assemblePrompt } from '../../agent-prompt';
import { assemblePrompt, type PromptContext } from '../../agent-prompt';
import type { SequenceResult, SequenceContext } from '../shared/types';
import { failed, installFailure } from '../shared/errors';
import { RunOutcome } from '../shared/types';
import { shouldDisableAsk, runOptions } from '../shared/bootstrap';
import { createEmitSpinner } from '../shared/progress-collector';
import { createAskBridge } from '../shared/ask';
import { withTranscript } from '../shared/transcript-tail';
import { getHarness } from '../switchboard';

export async function runLinearProgram(
context: SequenceContext,
): Promise<SequenceResult> {
// Aborts on the host's signal or when the run ends, so no ask outlives it.
// Aborts on the caller's signal or when the run ends, so no ask outlives it.
const controller = new AbortController();
const abortFromHost = () => controller.abort();
context.signal?.addEventListener('abort', abortFromHost, { once: true });
Expand All @@ -44,9 +45,17 @@ export async function runLinearProgram(
}
}

/** The host's `signal` decides the outcome; `runSignal` also ends with the run. */
/** The caller's `signal` decides the outcome; `runSignal` also ends with the run. */
async function executeLinear(
{ config, input, boot, emit, interaction, signal }: SequenceContext,
{
config,
input,
boot,
emit,
interaction,
signal,
transcript,
}: SequenceContext,
runSignal: AbortSignal,
): Promise<SequenceResult> {
const { run, composed } = config;
Expand Down Expand Up @@ -96,12 +105,15 @@ async function executeLinear(
signal: runSignal,
});

const middleware = input.flags.benchmark
? createBenchmarkPipeline(emit, spinner, runOptions(input))
: undefined;
const middleware = withTranscript(
input.flags.benchmark
? createBenchmarkPipeline(emit, spinner, runOptions(input))
: undefined,
transcript,
);

// 7. Build prompt
const prompt = assemblePrompt(run, {
const promptContext: PromptContext = {
projectId,
projectApiKey,
host,
Expand All @@ -115,7 +127,10 @@ async function executeLinear(
surveys: project.surveys_opt_in ?? null,
}
: null,
});
};
const prompt = run.prompt
? run.prompt(promptContext)
: assemblePrompt(run, promptContext);
logToFile(`[agent-runner] prompt assembled (${prompt.length} chars)`);
if (signal?.aborted) return aborted();

Expand Down Expand Up @@ -178,7 +193,7 @@ async function executeLinear(
matched: matched?.message ?? null,
});
return {
// An agent that stops itself failed the run; only the host's signal cancels it.
// An agent that stops itself failed the run; only the caller's signal cancels it.
outcome: signal?.aborted ? RunOutcome.Aborted : RunOutcome.Failed,
failure: {
message: matched?.message ?? `${run.integrationLabel} aborted`,
Expand Down
2 changes: 1 addition & 1 deletion src/agent/runner/shared/bootstrap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ export async function prepareRun(
// readers re-resolve through the cache, which re-mints past the refresh
// point.
const currentGatewayAuth = () =>
// TODO(B2): the agent must not mint inference auth. It receives the
// TODO: the agent must not mint inference auth. It receives the
// PostHog token here and derives a gateway token from it, re-minting near
// expiry. Programs own credentials (stack plan 4.5): pass a resolved
// inference-auth provider on RunInput.credentials and move
Expand Down
89 changes: 89 additions & 0 deletions src/agent/runner/shared/transcript-tail.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
/** The SDK message observer behind `collectTranscript`: a capped text tail plus one `activity` line per step. */

import type { ProgressEmitter } from '@agent/progress';
import type { RunMiddleware } from '../harness/types';

/** Only the tail is kept: a caller's report is the run's last output. */
export const TRANSCRIPT_TAIL_CHARS = 256 * 1024;
const ACTIVITY_LINE_CHARS = 100;

export interface TranscriptTail extends RunMiddleware {
/** The kept assistant text, one block per line, then the final result. */
text(): string;
}

export function createTranscriptTail(emit: ProgressEmitter): TranscriptTail {
const collected: string[] = [];
let collectedChars = 0;
let resultText = '';

const collect = (text: string): void => {
collected.push(text);
collectedChars += text.length;
while (collectedChars > TRANSCRIPT_TAIL_CHARS && collected.length > 1) {
collectedChars -= collected.shift()?.length ?? 0;
}
};
const activity = (line: string): void => emit({ kind: 'activity', line });

return {
onMessage(message: any): void {
if (message?.type === 'assistant') {
const content = message.message?.content;
if (!Array.isArray(content)) return;
for (const block of content) {
if (block?.type === 'text' && typeof block.text === 'string') {
collect(block.text);
const line = block.text.trim();
if (line) {
activity(
line.length > ACTIVITY_LINE_CHARS
? `${line.slice(0, ACTIVITY_LINE_CHARS)}…`
: line,
);
}
} else if (block?.type === 'tool_use') {
activity(formatToolUse(block));
}
}
} else if (
message?.type === 'result' &&
typeof message.result === 'string'
) {
resultText = message.result;
}
},
finalize: () => undefined,
text: () => `${collected.join('\n')}\n${resultText}`,
};
}

/** Put the tail in front of any other middleware, so both see every message. */
export function withTranscript(
middleware: RunMiddleware | undefined,
transcript: TranscriptTail | undefined,
): RunMiddleware | undefined {
if (!transcript) return middleware;
if (!middleware) return transcript;
return {
onMessage(message) {
transcript.onMessage(message);
middleware.onMessage(message);
},
finalize(resultMessage, totalDurationMs) {
transcript.finalize(resultMessage, totalDurationMs);
return middleware.finalize(resultMessage, totalDurationMs);
},
};
}

function formatToolUse(block: any): string {
const name = typeof block?.name === 'string' ? block.name : 'tool';
const input = (block?.input ?? {}) as Record<string, unknown>;
const detail =
(input.file_path as string) ||
(input.pattern as string) ||
(input.path as string) ||
'';
return detail ? `${name} ${detail}` : name;
}
5 changes: 4 additions & 1 deletion src/agent/runner/shared/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import type { LLMProvider } from '@posthog/warlock';
import type { AgentInteraction, ProgressEmitter } from '@agent/progress';
import type { EffortLevel } from '../switchboard/models';
import type { SwitchboardCtx } from '../switchboard';
import type { TranscriptTail } from './transcript-tail';

export type { PromptContext, Credentials };

Expand Down Expand Up @@ -152,7 +153,7 @@ export interface RunConfig {
programId: string;
/** The run definition. A program's session-taking hooks are the caller's, see `hooks`. */
run: AgentRunDefinition;
/** A composed sub-run leaves the terminal outro to its host. */
/** A composed sub-run leaves the terminal outro to its caller. */
composed: boolean;
/** Run-level sequence, harness and model. */
binding: ResolvedBinding;
Expand Down Expand Up @@ -332,4 +333,6 @@ export interface SequenceContext {
emit: ProgressEmitter;
interaction: AgentInteraction | undefined;
signal?: AbortSignal;
/** Present when the run definition sets `collectTranscript`. */
transcript?: TranscriptTail;
}
4 changes: 2 additions & 2 deletions src/agent/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,10 +33,10 @@ export type {
/** Leaves when the bindings table moves to programs. */
export type { ProgramBinding, SwitchboardCtx } from './runner';

/** Leaves in B2 with downloadSkill. */
/** Leaves with downloadSkill, later in the refactor. */
export type { InstallSkillResult } from './tools';

/** Leaves in B2 with the legacy adapter that records it. */
/** Leaves with the legacy adapter that records it. */
export type { TaskOutcome } from './runner';

/** Leaves in C2 with runMcpPromptViaSdk. */
Expand Down
2 changes: 1 addition & 1 deletion src/lib/wizard-session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ import type {
} from '@agent/types';
// Leaf module on purpose: shared analytics imports this file, so the agent
// entry would form a module cycle here.
// eslint-disable-next-line @typescript-eslint/no-restricted-imports -- B2: the session becomes a TUI projection
// eslint-disable-next-line @typescript-eslint/no-restricted-imports -- the session becomes a TUI projection later in the refactor
import { OutroKind } from '@agent/progress';
import { DiscoveredFeature } from '@shared/discovered-feature';

Expand Down
Loading
Loading