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
9ad2691
feat(error-tracking): add the error-tracking program on the orchestra…
ablaszkiewicz Sep 1, 2026
10cf2dc
feat(error-tracking): route the program to the orchestrator on pi
ablaszkiewicz Sep 1, 2026
308374b
feat(error-tracking): make the program e2e-drivable
ablaszkiewicz Sep 1, 2026
0d6407e
fix(e2e): drive the error-tracking intro in decideE2eAction
ablaszkiewicz Sep 2, 2026
aaf908c
fix(e2e): initialize the local-dev singleton in the tui-host
ablaszkiewicz Sep 2, 2026
3cc2fdc
fix(pi): gitignore the env file set_env_values writes
ablaszkiewicz Sep 10, 2026
97799fb
Merge origin/main into error-tracking-program
ablaszkiewicz Sep 14, 2026
84e1c6b
fix(error-tracking): stop on no framework, share the posthog-cli pre-…
ablaszkiewicz Sep 14, 2026
07a3282
fix(error-tracking): point the intro's more-info at the capture step …
ablaszkiewicz Sep 14, 2026
f7aadbf
fix(wizard-tools): accept POSTHOG_KEY when the project already reads it
ablaszkiewicz Sep 14, 2026
c4de6d7
fix(error-tracking): stop requesting product enablement scopes
ablaszkiewicz Sep 14, 2026
47370d4
fix(error-tracking): install posthog-cli after the intro gate, stop KMP
ablaszkiewicz Sep 14, 2026
6596863
fix(tui): resolve a bundled skill by its group for More info
ablaszkiewicz Sep 14, 2026
ba85e0e
fix(wizard-tools): scan shell and build files for POSTHOG_KEY reads
ablaszkiewicz Sep 14, 2026
299f5da
feat(error-tracking): pick the project after login, add the learn dec…
ablaszkiewicz Sep 15, 2026
8ae9a69
fix: copy
ablaszkiewicz Sep 15, 2026
a0b76b8
fix: copy
ablaszkiewicz Sep 15, 2026
c080f47
refactor(detection): share the scan-to-framework match between pickers
ablaszkiewicz Sep 15, 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
5 changes: 4 additions & 1 deletion .claude/skills/adding-skill-program/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,10 @@ programs. Compose real work through `ProgramStep.run`, with `onRunPrep` and
`targetDir` when needed. The
[integration run step](../../../src/lib/programs/posthog-integration/index.ts)
and [self-driving](../../../src/lib/programs/self-driving/) demonstrate this.
Composed sub-runs are structurally linear; orchestrators cannot nest.
Composed sub-runs are structurally linear; orchestrators cannot nest. A host run
step without `run` can also set `targetDir` and `onRunPrep` to scope the
program's own agent to a picked project and keep its sequence, as
[error-tracking](../../../src/lib/programs/error-tracking/) does.

## Validate the affected path

Expand Down
2 changes: 2 additions & 0 deletions bin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ import { warehouseCommand } from './src/commands/warehouse';
import { selfDrivingCommand } from './src/commands/self-driving';
import { slackCommand } from './src/commands/slack';
import { uploadSourcemapsCommand } from './src/commands/upload-sourcemaps';
import { errorTrackingCommand } from './src/commands/error-tracking';
import { skillCommand } from './src/commands/skill';
import { cliCommand } from './src/commands/cli';
import { recoverOrphanedSettingsBackups } from './src/lib/agent/claude-settings';
Expand Down Expand Up @@ -114,5 +115,6 @@ Wizard.use(basicIntegrationCommand)
.use(selfDrivingCommand)
.use(slackCommand)
.use(uploadSourcemapsCommand)
.use(errorTrackingCommand)
.use(skillCommand)
.init();
3 changes: 3 additions & 0 deletions e2e-harness/action-registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,8 @@ export const NO_ACTION_SCREENS: ReadonlySet<ScreenName> = new Set<ScreenName>([
ScreenId.SelfDrivingIntegrationCheck,
ScreenId.SelfDrivingIntegrationDetect,
ScreenId.SelfDrivingHandoff,
// The e2e host injects the pick, as it does for self-driving's detect screen.
ScreenId.ErrorTrackingDetect,
Overlay.ManagedSettings,
Overlay.AuthError,
Overlay.SessionTimeout,
Expand All @@ -108,6 +110,7 @@ export const ACTION_REGISTRY: Partial<Record<ScreenName, DriverAction[]>> = {
[ScreenId.AgentSkillIntro]: [confirmSetupAction],
[ScreenId.AiObservabilityIntro]: [confirmSetupAction],
[ScreenId.MetricsIntro]: [confirmSetupAction],
[ScreenId.ErrorTrackingIntro]: [confirmSetupAction],
[ScreenId.AuditIntro]: [confirmSetupAction],
[ScreenId.DoctorIntro]: [confirmSetupAction],
[ScreenId.WarehouseIntro]: [confirmSetupAction],
Expand Down
1 change: 1 addition & 0 deletions e2e-harness/e2e-profile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -283,6 +283,7 @@ export function decideE2eAction(
case ScreenId.AgentSkillIntro:
case ScreenId.AiObservabilityIntro:
case ScreenId.MetricsIntro:
case ScreenId.ErrorTrackingIntro:
case ScreenId.AuditIntro:
case ScreenId.SourceMapsIntro:
case ScreenId.DoctorIntro:
Expand Down
3 changes: 3 additions & 0 deletions e2e-harness/profiles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import metricsE2e from '@lib/programs/metrics/test/e2e.json';
import replayVisionE2e from '@lib/programs/replay-vision/test/e2e.json';
import selfDrivingE2e from '@lib/programs/self-driving/test/e2e.json';
import sourceMapsE2e from '@lib/programs/error-tracking-upload-source-maps/test/e2e.json';
import errorTrackingE2e from '@lib/programs/error-tracking/test/e2e.json';
import warehouseSourceE2e from '@lib/programs/warehouse-source/test/e2e.json';

const PROFILES: Partial<Record<ProgramId, WizardE2eProfile>> = {
Expand All @@ -36,6 +37,7 @@ const PROFILES: Partial<Record<ProgramId, WizardE2eProfile>> = {
[Program.SelfDriving]: selfDrivingE2e.profile as WizardE2eProfile,
[Program.ErrorTrackingUploadSourceMaps]:
sourceMapsE2e.profile as WizardE2eProfile,
[Program.ErrorTracking]: errorTrackingE2e.profile as WizardE2eProfile,
[Program.WarehouseSource]: warehouseSourceE2e.profile as WizardE2eProfile,
};

Expand All @@ -46,6 +48,7 @@ const VARIATIONS: Partial<Record<ProgramId, WizardE2eVariation[]>> = {
aiObservabilityE2e.variations as WizardE2eVariation[],
[Program.Metrics]: metricsE2e.variations as WizardE2eVariation[],
[Program.ReplayVision]: replayVisionE2e.variations as WizardE2eVariation[],
[Program.ErrorTracking]: errorTrackingE2e.variations as WizardE2eVariation[],
[Program.WarehouseSource]:
warehouseSourceE2e.variations as WizardE2eVariation[],
};
Expand Down
64 changes: 51 additions & 13 deletions scripts/tui-host.no-jest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import {
} from '@lib/programs/program-registry';
import type { Harness, Sequence } from '@lib/constants';
import { buildSession } from '@lib/wizard-session';
import { initLocalDev } from '@lib/local-dev';
import { configureGatewayFromCIEnvironment } from '@lib/gateway-session';
import { runAgent } from '@lib/agent/agent-runner';
import { authenticate } from '@lib/agent/runner/shared/authenticate';
Expand All @@ -35,6 +36,7 @@ import { detectFramework } from '@lib/detection/index';
import { FRAMEWORK_REGISTRY } from '@lib/registry';
import type { Integration } from '@lib/constants';
import { SELF_DRIVING_INTEGRATE_PATH_KEY } from '@lib/programs/self-driving/detect';
import { ERROR_TRACKING_PROJECT_PATH_KEY } from '@lib/programs/error-tracking/detect-agentic';
import {
detectSourceMapsPrerequisites,
SOURCE_MAPS_CONTEXT_KEYS,
Expand Down Expand Up @@ -196,6 +198,17 @@ async function main() {
// requires-interactive-mode the moment they need to ask a question.
process.env.WIZARD_ASK_AUTODRIVE = '1';

// The bin initializes the local-dev singleton from its yargs middleware;
// this host bypasses yargs, so `getSkillsBaseUrl()` would silently resolve
// to production even when the session carries the local flags. Initialize it
// here from the same env-backed spellings, before anything reads it.
initLocalDev({
localDev: process.env.POSTHOG_WIZARD_LOCAL_DEV === 'true',
localMcp: envFlag('POSTHOG_WIZARD_LOCAL_MCP'),
localContextMill: envFlag('POSTHOG_WIZARD_LOCAL_CONTEXT_MILL'),
localPosthog: envFlag('POSTHOG_WIZARD_LOCAL_POSTHOG'),
});

const { store } = startTUI(VERSION, programId);
store.session = buildSession({
installDir: process.env.APP_DIR!,
Expand Down Expand Up @@ -262,29 +275,35 @@ async function main() {
await store.getGate('health-check');

// Mirror run-wizard's composed walk for programs whose steps splice in
// their own run steps (self-driving: detect → integrate → handoff → run).
// their own run steps (self-driving: detect → integrate → handoff → run),
// 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.run)) {
if (programConfig.steps.some((s) => s.run || s.targetDir)) {
const runSessionFor = async (
step: (typeof programConfig.steps)[number],
) => {
const live = store.session;
const runSession = step.targetDir
? {
...live,
installDir: step.targetDir(live),
frameworkContext: { ...live.frameworkContext },
}
: live;
if (step.onRunPrep) await step.onRunPrep(runSession);
return runSession;
};
for (const step of programConfig.steps) {
if (step.screenId === 'outro') break;
if (step.show && !step.show(store.session)) continue;
if (step.screenId === 'auth') {
await authenticate(store.session, programConfig.id);
} else if (step.run) {
const live = store.session;
const runSession = step.targetDir
? {
...live,
installDir: step.targetDir(live),
frameworkContext: { ...live.frameworkContext },
}
: live;
if (step.onRunPrep) await step.onRunPrep(runSession);
await step.run(runSession);
await step.run(await runSessionFor(step));
store.completeRunStep(step.id);
} else if (step.screenId === 'run') {
await runAgent(programConfig, store.session);
await runAgent(programConfig, await runSessionFor(step));
} else if (step.isComplete) {
await store.waitUntil(step.isComplete);
}
Expand Down Expand Up @@ -466,6 +485,25 @@ async function main() {
continue;
}

// Headless error-tracking detect: the same pick injection as above, into
// the error-tracking path key, so the run is scoped to the picked app.
if (
state.currentScreen === ScreenId.ErrorTrackingDetect &&
state.session.integration == null
) {
const pick = await pickIntegrationTarget(store.session.installDir);
if (!pick) {
mark('error-tracking detect found no framework to set up');
process.exit(1);
}
store.setFrameworkContext(ERROR_TRACKING_PROJECT_PATH_KEY, pick.path);
store.setFrameworkConfig(
pick.integration,
FRAMEWORK_REGISTRY[pick.integration],
);
continue;
}

// Headless source-maps detect: the screen's candidate list lives in
// its own agentic report (React state), so compute the pick here with
// the static prerequisite detector — right for a single-app fixture —
Expand Down
15 changes: 15 additions & 0 deletions src/commands/error-tracking.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { errorTrackingConfig } from '@lib/programs/error-tracking/index';

import type { Command } from './command';
import { nativeCommandFactory } from './factories/native-command-factory';

/**
* `wizard error-tracking` — flat skill command, set up error tracking today.
*
* Wires up exception capture and — where the platform needs it — source-map /
* debug-symbol upload. Runs the `error-tracking` orchestrator flow, which
* reuses the integration-v2 install/init mini-agents when the repo has no
* PostHog integration yet, so it works on uninstrumented projects too.
*/
export const errorTrackingCommand: Command =
nativeCommandFactory(errorTrackingConfig);
16 changes: 14 additions & 2 deletions src/lib/agent/runner/__tests__/switchboard.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ describe('switchboard PROGRAM_BINDINGS', () => {
if (program === 'error-tracking-upload-source-maps') continue; // pinned below
if (program === 'metrics') continue; // pinned below
if (program === 'replay-vision') continue; // pinned below
if (program === 'error-tracking') continue; // pinned below
expect(resolveBinding({ program, flags: {} })).toEqual(DEFAULT_RESOLVED);
}
});
Expand Down Expand Up @@ -117,6 +118,17 @@ describe('switchboard PROGRAM_BINDINGS', () => {
},
trace: { harness: 'binding', model: 'binding', sequence: 'binding' },
},
{
name: 'binds error-tracking to the orchestrator on pi; stage models come from the flow frontmatter',
ctx: { program: 'error-tracking', flags: {} },
binding: {
sequence: Sequence.orchestrator,
harness: Harness.pi,
model: DEFAULT_AGENT_MODEL,
thinkingLevel: undefined,
},
trace: { harness: 'binding', model: 'binding', sequence: 'binding' },
},
{
name: 'falls back to DEFAULT_BINDING for an unmapped program',
ctx: { program: 'not-a-program', flags: {} },
Expand Down Expand Up @@ -223,7 +235,7 @@ describe('switchboard composed clamp', () => {
};
// The flag routes posthog-integration's harness to pi; the composed
// clamp holds every sequence at linear — the orchestrator bindings
// (metrics, replay-vision) included; other axes keep their bindings.
// (metrics, replay-vision, error-tracking) included; other axes keep their bindings.
expect(resolveBinding(ctx)).toEqual(
program === 'ai-observability'
? {
Expand All @@ -232,7 +244,7 @@ describe('switchboard composed clamp', () => {
model: SONNET_5_MODEL,
thinkingLevel: undefined,
}
: program === 'metrics'
: program === 'metrics' || program === 'error-tracking'
? {
...DEFAULT_RESOLVED,
model: DEFAULT_AGENT_MODEL,
Expand Down
87 changes: 86 additions & 1 deletion src/lib/agent/runner/harness/pi/__tests__/tools.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
* value — and set_env_values resolves refs host-side into the .env file.
*/
import { mkdtempSync } from 'node:fs';
import { readFile } from 'node:fs/promises';
import { mkdir, readFile, writeFile } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { describe, it, expect, vi } from 'vitest';
Expand Down Expand Up @@ -287,6 +287,91 @@ describe('pi set_env_values — resolves vault refs host-side', () => {
expect(env).toContain(`ZENDESK_TOKEN=${SECRET}`);
});

it('gitignores the env file it just wrote, like the MCP facade does', async () => {
// An iOS/Android project's .gitignore lists xcuserdata or build/, never
// .env — so without this pass the personal API key the flow writes is
// staged by the next `git add`.
const { setEnvValues, workingDirectory } = makeTools({});
await writeFile(join(workingDirectory, '.gitignore'), 'xcuserdata/\n');

await call(setEnvValues, {
filePath: '.env',
values: { POSTHOG_CLI_HOST: 'https://us.posthog.com' },
});

const gitignore = await readFile(
join(workingDirectory, '.gitignore'),
'utf8',
);
expect(gitignore.split('\n')).toContain('.env');
expect(gitignore).toContain('xcuserdata/');
});

it('refuses POSTHOG_KEY in a project that does not read it', async () => {
const { setEnvValues, workingDirectory } = makeTools({});

const result = await call(setEnvValues, {
filePath: '.env',
values: { POSTHOG_KEY: 'phc_test' },
});

expect(textOf(result)).toContain('is not a valid PostHog env var name');
await expect(
readFile(join(workingDirectory, '.env'), 'utf8'),
).rejects.toThrow();
});

it('keeps POSTHOG_KEY when the project code already reads it', async () => {
// Refusing here forces a rename of working code, and a deploy step that
// still passes POSTHOG_KEY then starts the app with an empty token.
const { setEnvValues, workingDirectory } = makeTools({});
await mkdir(join(workingDirectory, 'src'));
await writeFile(
join(workingDirectory, 'src', 'index.ts'),
"const client = new PostHog(process.env.POSTHOG_KEY ?? '');\n",
);

const result = await call(setEnvValues, {
filePath: '.env',
values: { POSTHOG_KEY: 'phc_test' },
});

expect(textOf(result)).toContain('Wrote 1 key(s)');
expect(await readFile(join(workingDirectory, '.env'), 'utf8')).toMatch(
/^POSTHOG_KEY=.*phc_test/m,
);
});

it('keeps POSTHOG_KEY when only a startup script reads it', async () => {
const { setEnvValues, workingDirectory } = makeTools({});
await writeFile(
join(workingDirectory, 'start.sh'),
'#!/bin/sh\nAPP_TOKEN="$POSTHOG_KEY" exec ./server\n',
);

const result = await call(setEnvValues, {
filePath: '.env',
values: { POSTHOG_KEY: 'phc_test' },
});

expect(textOf(result)).toContain('Wrote 1 key(s)');
});

it('does not count NEXT_PUBLIC_POSTHOG_KEY as a read of POSTHOG_KEY', async () => {
const { setEnvValues, workingDirectory } = makeTools({});
await writeFile(
join(workingDirectory, 'providers.tsx'),
'posthog.init(process.env.NEXT_PUBLIC_POSTHOG_KEY!);\n',
);

const result = await call(setEnvValues, {
filePath: '.env',
values: { POSTHOG_KEY: 'phc_test' },
});

expect(textOf(result)).toContain('is not a valid PostHog env var name');
});

it('mixed values map: literal + secretRef written together, secret still never in output', async () => {
const { wizardAsk, setEnvValues, workingDirectory } = makeTools({
token: SECRET,
Expand Down
16 changes: 9 additions & 7 deletions src/lib/agent/runner/harness/pi/tools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,13 +26,15 @@ import {
WIZARD_TOOL_NAMES,
checkEnvKeys as checkEnvKeysCore,
createAskAccounting,
ensureGitignoreCoverage,
fetchSkillMenu,
installSkillById,
mergeEnvValues,
normaliseAskSubject,
resolveEnvPath,
resolveEnvSecretRefs,
templateEnvWriteRefusal,
legacyKeyNameRefusal,
vaultSensitiveAnswers,
WIZARD_ASK_SENSITIVE_DESCRIPTION,
WIZARD_ASK_SUBJECT_DESCRIPTION,
Expand Down Expand Up @@ -209,14 +211,11 @@ export function createWizardPiTools(ctx: PiToolsContext): ToolDefinition[] {
),
}),
async execute(_id, args) {
const forbidden = Object.keys(args.values).find(
(k) => k.toUpperCase() === 'POSTHOG_KEY',
const keyRefusal = legacyKeyNameRefusal(
workingDirectory,
Object.keys(args.values),
);
if (forbidden) {
return text(
`Error: "${forbidden}" is not a valid PostHog env var name. Use the framework-specific key (e.g. NEXT_PUBLIC_POSTHOG_PROJECT_TOKEN).`,
);
}
if (keyRefusal) return text(keyRefusal);
// Resolve secret refs host-side; the value never reaches the agent.
const resolution = resolveEnvSecretRefs(args.values, secretVault);
if (!resolution.ok) {
Expand All @@ -241,6 +240,9 @@ export function createWizardPiTools(ctx: PiToolsContext): ToolDefinition[] {
if (!fs.existsSync(dir))
await fs.promises.mkdir(dir, { recursive: true });
await fs.promises.writeFile(resolved, merged, 'utf8');
// Same post-write pass as the MCP facade: a credential file the
// project does not ignore yet gets committed by the next `git add`.
ensureGitignoreCoverage(workingDirectory, path.basename(resolved));
logToFile(
`[pi] set_env_values: ${resolved} keys=${Object.keys(args.values).join(
',',
Expand Down
Loading
Loading