Skip to content

fix: move dashboard creation out of agent loop; stop false-positive ingestion#154

Merged
kelsonpw merged 7 commits into
mainfrom
kelsonpw/wizard-dashboard-ingestion
Apr 29, 2026
Merged

fix: move dashboard creation out of agent loop; stop false-positive ingestion#154
kelsonpw merged 7 commits into
mainfrom
kelsonpw/wizard-dashboard-ingestion

Conversation

@kelsonpw

@kelsonpw kelsonpw commented Apr 20, 2026

Copy link
Copy Markdown
Member

Summary

Two reliability fixes, kept separate from the inline-API-key change in #153 for atomic review:

  1. Dashboard creation moves out of the main agent loop into a dedicated post-agent step (src/steps/create-dashboard.ts). The step reads .amplitude-events.json, drives chart + dashboard creation through the Amplitude MCP with a hard 90 s AbortController ceiling covering the session handshake AND the agent subprocess, writes .amplitude-dashboard.json, surfaces the URL via setDashboardUrl, and degrades gracefully on failure. Agent-run success is never blocked by dashboard-step failure.

    Previously the agent was required to drive create_chart / create_dashboard via the Amplitude MCP inside its own run loop with no step-level timeout — a slow MCP response could hang the wizard for hours.

  2. DataIngestionCheckScreen stops treating the event catalog as a success signal. The catalog reflects taxonomy registrations, not real ingestion; a project with a declared schema but zero live events was falsely celebrating. Coaching tips at 60 / 90 / 120 / 180 s now cover dev-server restart, CORS, and devtools console errors — the three common silent-failure modes flagged by @kaia.peacock that we otherwise have no visibility into.

Why separate from #153

The inline-API-key change in #153 is orthogonal to these two fixes. Landing them in separate PRs:

  • Lets reviewers focus on one concern at a time
  • Lets us roll back either independently if issues surface

The shared commandments.ts hunk (replace "must create dashboard via MCP" with "write .amplitude-events.json and stop") is intentionally identical in both PRs so either merge order resolves cleanly.

Relationship to other PRs

  • fix: inline public API key instead of writing to env vars #153 (this repo): inline public API key — lands independently
  • amplitude/context-hub#49: skill-side content changes. Removes in-agent dashboard instructions from basic-integration-1.3-conclude.md and amplitude-chart-dashboard-plan/SKILL.md. Until that releases and we run pnpm skills:refresh, the updated commandments.ts in this PR explicitly overrides the old skill content — the agent won't try to create a dashboard inline.

Files

  • src/lib/commandments.ts — drop the "must create dashboard via Amplitude MCP" requirement; replace with "write .amplitude-events.json and stop; the wizard handles dashboard creation." Specifies a top-level array shape matching the existing parser at src/lib/agent-interface.ts:1064.
  • src/lib/agent-runner.ts — invoke new createDashboardStep after the agent run completes, inside a try block (never blocks agent success).
  • src/steps/create-dashboard.ts (new) — tolerant Zod schema (name / event / event_type / eventName, both bare array and { events: [...] } shapes), callAmplitudeMcp call bounded by an AbortController + 90 s setTimeout, balanced-brace fallback parser that handles nested charts arrays and escaped quotes, writes .amplitude-dashboard.json, surfaces URL via setDashboardUrl, analytics capture on skip / failure / success.
  • src/steps/index.ts — export the new step.
  • src/steps/__tests__/create-dashboard.test.ts (new) — 19 regression tests covering the schema tolerance, balanced-brace extractor, and parseAgentOutput happy-path + fallback edge cases.
  • src/ui/tui/screens/DataIngestionCheckScreen.tsx — drop catalog-as-success path, expand coaching tips at 60 / 90 / 120 / 180 s.

Commit history

  • 653dbb9 — initial move-dashboard-out-of-agent-loop + ingestion-screen fixes
  • 76a57c4 — Bugbot follow-up: tolerant events schema (accept event / event_type / eventName), balanced-brace JSON extractor, 19 regression tests, commandment clarifies the canonical name key
  • 2ae395b — Bugbot follow-up: wrap callAmplitudeMcp in an AbortController with setTimeout(90s) so the MCP session handshake is bounded too (not just the agent subprocess); align commandment to specify a bare top-level array shape

Test plan

  • pnpm lint clean
  • pnpm test — 1105 tests pass (19 new regression tests in src/steps/__tests__/create-dashboard.test.ts)
  • pnpm build succeeds, smoke test passes
  • Manual: create a project with a pre-registered taxonomy but no real ingestion → verify DataIngestionCheckScreen does NOT falsely celebrate
  • Manual: simulate slow MCP (or unplug network mid-run) → verify the dashboard step times out at 90 s with a visible warn rather than hanging indefinitely
  • Manual: force an invalid .amplitude-events.json shape → verify the step skips gracefully without crashing the wizard

/cc @amplitude/growth

🤖 Generated with Claude Code


Note

Medium Risk
Touches wizard end-to-end flow (post-agent steps, MCP calling, and ingestion confirmation), so regressions could impact run completion and user success signals; changes are guarded with new unit/regression tests and mostly fail open.

Overview
Moves dashboard creation out of the main agent run into a new post-agent createDashboardStep that reads .amplitude-events.json, calls Amplitude MCP via callAmplitudeMcp with a hard 90s abort ceiling, parses the result robustly, writes .amplitude-dashboard.json, and updates the session/UI without failing the overall run.

Updates agent guidance and infrastructure: commandments now instruct the agent to write .amplitude-events.json (and not create dashboards), and callAmplitudeMcp now supports an agent-only fallback path (direct optional) plus better abort-signal handling.

Reduces false-positive ingestion confirmations by gating the event-catalog fallback to backend integrations only (new BACKEND_SDK_INTEGRATIONS), with added tests to lock in the integration set and new regression tests for dashboard parsing/event-file tolerance.

Reviewed by Cursor Bugbot for commit 82353dc. Bugbot is set up for automated code reviews on this repo. Configure here.

@kelsonpw kelsonpw added the launch-readiness Related to launch readiness / user activation reliability label Apr 20, 2026
@kelsonpw kelsonpw requested a review from a team as a code owner April 20, 2026 22:37
@github-actions

Copy link
Copy Markdown
Contributor

🧙 Wizard CI

Run 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:

  • /wizard-ci all

Test all apps in a directory:

  • /wizard-ci django
  • /wizard-ci fastapi
  • /wizard-ci flask
  • /wizard-ci javascript-node
  • /wizard-ci javascript-web
  • /wizard-ci next-js
  • /wizard-ci python
  • /wizard-ci react-router
  • /wizard-ci vue

Test an individual app:

  • /wizard-ci django/django3-saas
  • /wizard-ci fastapi/fastapi3-ai-saas
  • /wizard-ci flask/flask3-social-media
Show more apps
  • /wizard-ci javascript-node/express-todo
  • /wizard-ci javascript-node/fastify-blog
  • /wizard-ci javascript-node/hono-links
  • /wizard-ci javascript-node/koa-notes
  • /wizard-ci javascript-node/native-http-contacts
  • /wizard-ci javascript-web/saas-dashboard
  • /wizard-ci next-js/15-app-router-saas
  • /wizard-ci next-js/15-app-router-todo
  • /wizard-ci next-js/15-pages-router-saas
  • /wizard-ci next-js/15-pages-router-todo
  • /wizard-ci python/meeting-summarizer
  • /wizard-ci react-router/react-router-v7-project
  • /wizard-ci react-router/rrv7-starter
  • /wizard-ci react-router/saas-template
  • /wizard-ci react-router/shopper
  • /wizard-ci vue/movies

Results will be posted here when complete.

@cursor cursor Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Autofix Details

Bugbot Autofix prepared a fix for the issue found in the latest run.

  • ✅ Fixed: No-op direct path wastes timeout budget on unused MCP session
    • Made direct optional in CallAmplitudeMcpOptions so callAmplitudeMcp skips the MCP session handshake when no direct path is provided, removed the no-op direct: () => Promise.resolve(null) from createDashboardStep, and added an externalSignal.aborted check in runAgentFallback to properly abort immediately when the signal is already spent.

Create PR

Or push these changes by commenting:

@cursor push 0a6c4b34e0
Preview (0a6c4b34e0)
diff --git a/src/lib/mcp-with-fallback.ts b/src/lib/mcp-with-fallback.ts
--- a/src/lib/mcp-with-fallback.ts
+++ b/src/lib/mcp-with-fallback.ts
@@ -161,6 +161,9 @@
   // Combine the timeout with any external abort signal.
   // AbortSignal.any() requires Node 20.3+; we support 18.17+, so we wire it manually.
   const controller = new AbortController();
+  if (externalSignal?.aborted) {
+    controller.abort();
+  }
   const timer = setTimeout(() => controller.abort(), timeoutMs);
   const onExternalAbort = (): void => controller.abort();
   externalSignal?.addEventListener('abort', onExternalAbort, { once: true });
@@ -227,8 +230,12 @@
   /**
    * The direct MCP call path. Receives a `callTool` helper bound to an open
    * session. Return `null` to trigger the agent fallback.
+   *
+   * When omitted, the MCP session handshake is skipped entirely and the agent
+   * fallback runs immediately. Use this when there is no single MCP tool that
+   * can satisfy the request and orchestration must be done by the agent.
    */
-  direct: (callTool: CallToolFn) => Promise<T | null>;
+  direct?: (callTool: CallToolFn) => Promise<T | null>;
   /** Prompt sent to the Claude agent when the direct path returns null or throws. */
   agentPrompt: string;
   /** Extract the result from the agent's collected text output. Return null if unparseable. */
@@ -271,24 +278,29 @@
   let directResult: T | null = null;
   let useFallback = false;
 
-  const callTool = await openMcpSession(accessToken, mcpUrl, abortSignal);
-  if (callTool) {
-    try {
-      directResult = await direct(callTool);
-      if (directResult === null) {
-        logToFile(`[${label}] direct returned null — trying agent fallback`);
+  if (direct) {
+    const callTool = await openMcpSession(accessToken, mcpUrl, abortSignal);
+    if (callTool) {
+      try {
+        directResult = await direct(callTool);
+        if (directResult === null) {
+          logToFile(`[${label}] direct returned null — trying agent fallback`);
+          useFallback = true;
+        }
+      } catch (err) {
+        logToFile(
+          `[${label}] direct threw — trying agent fallback: ${
+            err instanceof Error ? err.message : String(err)
+          }`,
+        );
         useFallback = true;
       }
-    } catch (err) {
-      logToFile(
-        `[${label}] direct threw — trying agent fallback: ${
-          err instanceof Error ? err.message : String(err)
-        }`,
-      );
+    } else {
+      logToFile(`[${label}] MCP session failed — trying agent fallback`);
       useFallback = true;
     }
   } else {
-    logToFile(`[${label}] MCP session failed — trying agent fallback`);
+    logToFile(`[${label}] no direct path — using agent fallback`);
     useFallback = true;
   }
 

diff --git a/src/steps/create-dashboard.ts b/src/steps/create-dashboard.ts
--- a/src/steps/create-dashboard.ts
+++ b/src/steps/create-dashboard.ts
@@ -186,11 +186,8 @@
 
   const agentPrompt = buildAgentPrompt(events, session);
 
-  // Bound the ENTIRE call with an abort signal, not just the agent subprocess.
-  // callAmplitudeMcp first opens a full MCP session (two HTTP fetches with no
-  // built-in timeout) before invoking `direct`; without this hard ceiling a
-  // slow session handshake could block for minutes and defeat the point of
-  // moving dashboard work out of the agent loop.
+  // Bound the ENTIRE call with an abort signal so a slow agent fallback cannot
+  // block the wizard indefinitely.
   const controller = new AbortController();
   const abortTimer = setTimeout(() => {
     logToFile(
@@ -206,10 +203,6 @@
       label: 'createDashboard',
       agentTimeoutMs: DASHBOARD_TIMEOUT_MS,
       abortSignal: controller.signal,
-      // There's no single MCP tool that plans and creates the dashboard in one
-      // shot; chart-by-chart orchestration is what the skill covers. Skip the
-      // direct path and let the agent fallback handle it.
-      direct: () => Promise.resolve(null),
       agentPrompt,
       parseAgent: parseAgentOutput,
     });

You can send follow-ups to the cloud agent here.

Comment thread src/steps/create-dashboard.ts
kelsonpw added a commit that referenced this pull request Apr 20, 2026
Dropping the 'write .amplitude-events.json, do not create dashboard' commandment from this PR. That change only makes sense alongside the new createDashboardStep implementation, which lives in #154 (kelsonpw/wizard-dashboard-ingestion). Keeping it in both PRs was originally a merge-order safety move — but the cleaner story is to let #154 own the full dashboard refactor and have this PR stay scoped to inline-API-key behavior.

Note on merge order: if this PR lands before #154, the agent still has the old 'MUST create dashboard via Amplitude MCP' commandment and may hit the hang bug #154 fixes. Land #154 first (or together) to avoid that window.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@kelsonpw

Copy link
Copy Markdown
Member Author

@cursor push 0a6c4b3

@cursor cursor Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Autofix Details

Bugbot Autofix prepared a fix for the issue found in the latest run.

  • ✅ Fixed: Spinner never stopped if runCreateDashboard throws
    • Wrapped the runCreateDashboard call and subsequent spinner interactions in a try/catch block so that any unexpected exception stops the spinner and logs the error, enforcing the "Never throws" contract.

Create PR

Or push these changes by commenting:

@cursor push c89e29c02e
Preview (c89e29c02e)
diff --git a/src/steps/create-dashboard.ts b/src/steps/create-dashboard.ts
--- a/src/steps/create-dashboard.ts
+++ b/src/steps/create-dashboard.ts
@@ -108,45 +108,60 @@
   spinner.start('Creating charts and dashboard in Amplitude…');
 
   const startedAt = Date.now();
-  const result = await runCreateDashboard({
-    accessToken,
-    events,
-    session,
-  });
-  const durationMs = Date.now() - startedAt;
+  try {
+    const result = await runCreateDashboard({
+      accessToken,
+      events,
+      session,
+    });
+    const durationMs = Date.now() - startedAt;
 
-  if (!result) {
-    spinner.stop('Dashboard step timed out — skipping');
-    ui.log.warn(
-      'Amplitude is configured, but the wizard could not create a starter dashboard within 90 seconds. Open app.amplitude.com to create one manually.',
-    );
-    analytics.wizardCapture('dashboard failed', {
+    if (!result) {
+      spinner.stop('Dashboard step timed out — skipping');
+      ui.log.warn(
+        'Amplitude is configured, but the wizard could not create a starter dashboard within 90 seconds. Open app.amplitude.com to create one manually.',
+      );
+      analytics.wizardCapture('dashboard failed', {
+        integration,
+        reason: 'timeout or mcp error',
+        'duration ms': durationMs,
+      });
+      return;
+    }
+
+    // 2. Persist the wizard-visible artifact so OutroScreen can link to it.
+    try {
+      fs.writeFileSync(dashboardPath, JSON.stringify(result, null, 2), 'utf8');
+    } catch (err) {
+      logToFile(
+        `[createDashboard] failed to write ${DASHBOARD_FILE}: ${
+          err instanceof Error ? err.message : String(err)
+        }`,
+      );
+    }
+
+    session.checklistDashboardUrl = result.dashboardUrl;
+    ui.setDashboardUrl(result.dashboardUrl);
+    spinner.stop('Dashboard ready');
+    analytics.wizardCapture('dashboard created', {
       integration,
-      reason: 'timeout or mcp error',
+      'chart count': result.charts?.length ?? 0,
       'duration ms': durationMs,
     });
-    return;
-  }
-
-  // 2. Persist the wizard-visible artifact so OutroScreen can link to it.
-  try {
-    fs.writeFileSync(dashboardPath, JSON.stringify(result, null, 2), 'utf8');
   } catch (err) {
+    const durationMs = Date.now() - startedAt;
+    spinner.stop('Dashboard creation failed — skipping');
     logToFile(
-      `[createDashboard] failed to write ${DASHBOARD_FILE}: ${
+      `[createDashboard] unexpected error: ${
         err instanceof Error ? err.message : String(err)
       }`,
     );
+    analytics.wizardCapture('dashboard failed', {
+      integration,
+      reason: 'unexpected error',
+      'duration ms': durationMs,
+    });
   }
-
-  session.checklistDashboardUrl = result.dashboardUrl;
-  ui.setDashboardUrl(result.dashboardUrl);
-  spinner.stop('Dashboard ready');
-  analytics.wizardCapture('dashboard created', {
-    integration,
-    'chart count': result.charts?.length ?? 0,
-    'duration ms': durationMs,
-  });
 }
 
 // ── Helpers (exported for testing) ─────────────────────────────────────────

You can send follow-ups to the cloud agent here.

Comment thread src/steps/create-dashboard.ts
@kelsonpw kelsonpw force-pushed the kelsonpw/wizard-dashboard-ingestion branch from 84c3b29 to eaaa66c Compare April 27, 2026 03:31

@cursor cursor Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 2 potential issues.

Autofix Details

Bugbot Autofix prepared fixes for both issues found in the latest run.

  • ✅ Fixed: Contradictory commandments about writing .amplitude-events.json
    • Replaced the contradictory commandment that instructed the agent to write .amplitude-events.json with a simpler directive that only tells the agent not to create dashboards, eliminating the conflict with the existing CRITICAL directive that confirm_event_plan handles file persistence.
  • ✅ Fixed: Manual parser missing event_name field the canonical parser handles
    • Replaced the custom EventEntrySchema with the canonical parseEventPlanContent() from event-plan-parser.ts, which handles all observed field variants including event_name, event_description, and eventDescription that the custom schema was missing.

Create PR

Or push these changes by commenting:

@cursor push 642458e52c
Preview (642458e52c)
diff --git a/src/lib/commandments.ts b/src/lib/commandments.ts
--- a/src/lib/commandments.ts
+++ b/src/lib/commandments.ts
@@ -41,7 +41,7 @@
 
   `Autocapture — the Amplitude feature that automatically tracks element clicks, form interactions, page/screen views, sessions, app lifecycle events, and file downloads — is commonly enabled by the wizard for web SDKs (@amplitude/unified, @amplitude/analytics-browser) but is NOT available or not on by default for every SDK (e.g. Swift requires an opt-in plugin, backend SDKs don't track element interactions at all, and an existing project may have it disabled). Before proposing events, check the SDK init code you just wrote (or that already exists) to see whether autocapture is on and what it covers for this platform. If it IS on, do NOT propose custom events that merely duplicate its coverage — names like "[X] Clicked", "[X] Tapped", "[X] Pressed", "Form Submitted", "Form Started", "Input Changed", "Page Viewed", or "Screen Viewed" are redundant and must be excluded. Either way, prefer events for business outcomes, state changes, async success/failure, and multi-step flow milestones over raw interaction events (see skills/instrumentation/discover-event-surfaces/references/best-practices.md section R4). If autocapture is on and the project is a landing page or starter template whose only interactions are plain clicks and links, lean toward a minimal plan and let autocapture do the work — confirm_event_plan still requires at least one event, so pick the single most meaningful state change. Keep this reasoning internal — do NOT write autocapture justifications into the description field.`,
 
-  `After all event and identity instrumentation is complete, write a file named \`.amplitude-events.json\` at the project root. Shape: a top-level JSON array — \`[ { "name": "<exact event name>", "description": "<short description>", "file": "<path where instrumented>" } ]\`. Use the key \`name\` (matching the event_type you passed to track()) — not \`event\`, \`event_type\`, or \`eventName\`. Do NOT wrap the array in an object (e.g. \`{ "events": [...] }\`); the wizard's parsers expect a top-level array. Do NOT create charts or dashboards yourself — the wizard runs a dedicated post-agent step that reads this file and creates the dashboard with bounded timeouts and progress reporting. Your job ends at instrumentation + writing this file.`,
+  `Do NOT create charts or dashboards yourself — the wizard runs a dedicated post-agent step that reads the .amplitude-events.json file (written by confirm_event_plan) and creates the dashboard with bounded timeouts and progress reporting. Your job ends at instrumentation; once confirm_event_plan has been approved, proceed with writing track() calls and then stop.`,
 
   `Prefer the report_status tool (wizard-tools MCP) for progress updates and fatal error signals. Call report_status with kind="status" for in-progress updates (e.g. "installing SDK", "drafting event plan") — these appear in the wizard's spinner. Call report_status with kind="error" for fatal conditions that halt the run (codes: MCP_MISSING, RESOURCE_MISSING). Legacy [STATUS] / [ERROR-MCP-MISSING] / [ERROR-RESOURCE-MISSING] text markers from older bundled skills are still recognized for backwards compat, but new code should use report_status.`,
 

diff --git a/src/steps/__tests__/create-dashboard.test.ts b/src/steps/__tests__/create-dashboard.test.ts
--- a/src/steps/__tests__/create-dashboard.test.ts
+++ b/src/steps/__tests__/create-dashboard.test.ts
@@ -1,75 +1,88 @@
 /**
  * Regression tests for the two issues Cursor Bugbot surfaced:
- *   1. Schema must tolerate `event_type`/`event`/`eventName` (agents drift
- *      from the canonical key name).
+ *   1. Schema must tolerate `event_type`/`event`/`eventName`/`event_name`
+ *      (agents drift from the canonical key name).
  *   2. The fallback JSON extractor must handle nested braces (the dashboard
  *      result embeds a `charts` array of objects).
  */
 
 import { describe, it, expect } from 'vitest';
 import { __test__ } from '../create-dashboard';
+import { parseEventPlanContent } from '../../lib/event-plan-parser';
 
-const { EventsFileSchema, parseAgentOutput, extractJsonContaining } = __test__;
+const { parseAgentOutput, extractJsonContaining } = __test__;
 
-describe('EventsFileSchema', () => {
+describe('parseEventPlanContent (used by readEventsFile)', () => {
   it('accepts canonical `name` key', () => {
-    const result = EventsFileSchema.parse({
-      events: [
+    const result = parseEventPlanContent(
+      JSON.stringify([
         { name: 'Signup Completed', description: 'User finished signup' },
-      ],
-    });
-    expect(result.events[0].name).toBe('Signup Completed');
+      ]),
+    );
+    expect(result![0].name).toBe('Signup Completed');
   });
 
   it('accepts legacy `event` key and normalizes to `name`', () => {
-    const result = EventsFileSchema.parse({
-      events: [{ event: 'Project Created' }],
-    });
-    expect(result.events[0].name).toBe('Project Created');
+    const result = parseEventPlanContent(
+      JSON.stringify([{ event: 'Project Created' }]),
+    );
+    expect(result![0].name).toBe('Project Created');
   });
 
-  it('accepts `event_type` key (from the old commandment wording)', () => {
-    const result = EventsFileSchema.parse({
-      events: [{ event_type: 'Invite Sent' }],
-    });
-    expect(result.events[0].name).toBe('Invite Sent');
+  it('accepts `eventName` key', () => {
+    const result = parseEventPlanContent(
+      JSON.stringify([{ eventName: 'Checkout Started' }]),
+    );
+    expect(result![0].name).toBe('Checkout Started');
   });
 
-  it('accepts `eventName` key', () => {
-    const result = EventsFileSchema.parse({
-      events: [{ eventName: 'Checkout Started' }],
-    });
-    expect(result.events[0].name).toBe('Checkout Started');
+  it('accepts `event_name` key', () => {
+    const result = parseEventPlanContent(
+      JSON.stringify([{ event_name: 'Item Purchased' }]),
+    );
+    expect(result![0].name).toBe('Item Purchased');
   });
 
   it('prefers `name` when multiple keys are present', () => {
-    const result = EventsFileSchema.parse({
-      events: [{ name: 'Canonical', event: 'Legacy' }],
-    });
-    expect(result.events[0].name).toBe('Canonical');
+    const result = parseEventPlanContent(
+      JSON.stringify([{ name: 'Canonical', event: 'Legacy' }]),
+    );
+    expect(result![0].name).toBe('Canonical');
   });
 
-  it('rejects entries with no recognizable name key', () => {
-    expect(() =>
-      EventsFileSchema.parse({ events: [{ description: 'x' }] }),
-    ).toThrow();
+  it('returns empty name for entries with no recognizable name key', () => {
+    const result = parseEventPlanContent(
+      JSON.stringify([{ description: 'x' }]),
+    );
+    expect(result![0].name).toBe('');
   });
 
-  it('rejects empty events array', () => {
-    expect(() => EventsFileSchema.parse({ events: [] })).toThrow();
+  it('returns null for invalid JSON', () => {
+    expect(parseEventPlanContent('not json')).toBeNull();
   });
 
   it('accepts `eventDescriptionAndReasoning` as description alias', () => {
-    const result = EventsFileSchema.parse({
-      events: [
-        {
-          name: 'Foo',
-          eventDescriptionAndReasoning: 'Fires when users foo',
-        },
-      ],
-    });
-    expect(result.events[0].description).toBe('Fires when users foo');
+    const result = parseEventPlanContent(
+      JSON.stringify([
+        { name: 'Foo', eventDescriptionAndReasoning: 'Fires when users foo' },
+      ]),
+    );
+    expect(result![0].description).toBe('Fires when users foo');
   });
+
+  it('accepts `event_description` as description alias', () => {
+    const result = parseEventPlanContent(
+      JSON.stringify([{ name: 'Bar', event_description: 'When bar happens' }]),
+    );
+    expect(result![0].description).toBe('When bar happens');
+  });
+
+  it('unwraps { events: [...] } wrapper', () => {
+    const result = parseEventPlanContent(
+      JSON.stringify({ events: [{ name: 'Wrapped Event' }] }),
+    );
+    expect(result![0].name).toBe('Wrapped Event');
+  });
 });
 
 describe('extractJsonContaining', () => {

diff --git a/src/steps/create-dashboard.ts b/src/steps/create-dashboard.ts
--- a/src/steps/create-dashboard.ts
+++ b/src/steps/create-dashboard.ts
@@ -15,6 +15,7 @@
 import { z } from 'zod';
 
 import { callAmplitudeMcp, AMPLITUDE_MCP_URL } from '../lib/mcp-with-fallback';
+import { parseEventPlanContent } from '../lib/event-plan-parser';
 import { getUI } from '../ui';
 import { analytics } from '../utils/analytics';
 import { logToFile } from '../utils/debug';
@@ -25,34 +26,13 @@
 const DASHBOARD_FILE = '.amplitude-dashboard.json';
 const DASHBOARD_TIMEOUT_MS = 90_000;
 
-// The agent is asked to write `{ events: [{ name, description, file }] }`,
-// but historically other skills have emitted `event`, `event_type`, or
-// `eventName` for the event-name key. Accept all of them and normalize to
-// `name` downstream. Mirrors the tolerance in bin.ts (setEventPlan).
-const EventEntrySchema = z
-  .object({
-    name: z.string().optional(),
-    event: z.string().optional(),
-    event_type: z.string().optional(),
-    eventName: z.string().optional(),
-    description: z.string().optional(),
-    eventDescriptionAndReasoning: z.string().optional(),
-    file: z.string().optional(),
-  })
-  .transform((e) => ({
-    name: (e.name ?? e.event ?? e.event_type ?? e.eventName ?? '').trim(),
-    description: e.description ?? e.eventDescriptionAndReasoning,
-    file: e.file,
-  }))
-  .refine((e) => e.name.length > 0, {
-    message:
-      'event must have a name (key: name, event, event_type, or eventName)',
-  });
+/**
+ * Normalized events structure used by the dashboard step.
+ */
+interface EventsFile {
+  events: Array<{ name: string; description: string }>;
+}
 
-const EventsFileSchema = z.object({
-  events: z.array(EventEntrySchema).min(1),
-});
-
 const DashboardFileSchema = z.object({
   dashboardUrl: z.string().url(),
   dashboardId: z.string().optional(),
@@ -67,7 +47,6 @@
     .optional(),
 });
 
-type EventsFile = z.infer<typeof EventsFileSchema>;
 type DashboardResult = z.infer<typeof DashboardFileSchema>;
 
 export interface CreateDashboardStepArgs {
@@ -152,7 +131,6 @@
 // ── Helpers (exported for testing) ─────────────────────────────────────────
 
 export const __test__ = {
-  EventsFileSchema,
   parseAgentOutput,
   extractJsonContaining,
 };
@@ -160,10 +138,10 @@
 function readEventsFile(eventsPath: string): EventsFile | null {
   if (!fs.existsSync(eventsPath)) return null;
   try {
-    const raw: unknown = JSON.parse(fs.readFileSync(eventsPath, 'utf8'));
-    // Tolerant of both { events: [...] } and the bare [...] shape.
-    const normalized = Array.isArray(raw) ? { events: raw } : raw;
-    return EventsFileSchema.parse(normalized);
+    const content = fs.readFileSync(eventsPath, 'utf8');
+    const parsed = parseEventPlanContent(content);
+    if (!parsed || parsed.length === 0) return null;
+    return { events: parsed };
   } catch (err) {
     logToFile(
       `[createDashboard] ${eventsPath} is invalid: ${

You can send follow-ups to the cloud agent here.

Comment thread src/lib/commandments.ts Outdated
Comment thread src/steps/create-dashboard.ts Outdated
@kelsonpw

Copy link
Copy Markdown
Member Author

Bugbot follow-ups in af73462e:

  1. No-op direct path wastes timeout budget on unused MCP session — already addressed in eaaa66c2. create-dashboard.ts no longer passes direct: () => Promise.resolve(null); it omits the direct option entirely, and callAmplitudeMcp now skips openMcpSession when direct is undefined and short-circuits a pre-aborted external signal in runAgentFallback. Resolving.

  2. Spinner never stopped if runCreateDashboard throws — fixed. Wrapped the runCreateDashboard call in a try/catch so unexpected throws (e.g. SDK module load failure outside the agent fallback's own try block) cannot leak the spinner. The "never throws" docstring contract is now actually enforced.

  3. Manual parser missing event_name — fixed. readEventsFile now delegates to parseEventPlanContent (the canonical parser shared with the TUI Event Plan viewer and CLI plan reader), so this step picks up event_name (snake_case, observed in the wild) along with every other field-name variant the canonical parser tolerates. Added a regression test for the snake_case key.

  4. Contradictory commandments about writing .amplitude-events.json — fixed. Reworded the confirm_event_plan block so it's clear confirm_event_plan owns the initial canonical write, while the post-instrumentation commandment governs the final rewrite that adds the file field. Both must use the canonical name/description keys.

@cursor cursor Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Autofix Details

Bugbot Autofix prepared a fix for the issue found in the latest run.

  • ✅ Fixed: Misleading "timed out" message when MCP returns null
    • Added elapsed-time check (>= 90% of DASHBOARD_TIMEOUT_MS) to distinguish actual timeouts from fast MCP failures, showing appropriate "failed" vs "timed out" messages and recording separate analytics reasons.

Create PR

Or push these changes by commenting:

@cursor push 420d023690
Preview (420d023690)
diff --git a/src/steps/create-dashboard.ts b/src/steps/create-dashboard.ts
--- a/src/steps/create-dashboard.ts
+++ b/src/steps/create-dashboard.ts
@@ -107,17 +107,26 @@
   const durationMs = Date.now() - startedAt;
 
   if (!result) {
-    spinner.stop(
-      unexpectedError
-        ? 'Dashboard step failed — skipping'
-        : 'Dashboard step timed out — skipping',
-    );
-    ui.log.warn(
-      'Amplitude is configured, but the wizard could not create a starter dashboard within 90 seconds. Open app.amplitude.com to create one manually.',
-    );
+    const likelyTimeout =
+      !unexpectedError && durationMs >= DASHBOARD_TIMEOUT_MS * 0.9;
+    const reason = unexpectedError
+      ? 'unexpected error'
+      : likelyTimeout
+      ? 'timeout'
+      : 'mcp error';
+    const spinnerMsg = unexpectedError
+      ? 'Dashboard step failed — skipping'
+      : likelyTimeout
+      ? 'Dashboard step timed out — skipping'
+      : 'Dashboard step failed — skipping';
+    const warnMsg = likelyTimeout
+      ? 'Amplitude is configured, but the wizard could not create a starter dashboard within 90 seconds. Open app.amplitude.com to create one manually.'
+      : 'Amplitude is configured, but the wizard could not create a starter dashboard. Open app.amplitude.com to create one manually.';
+    spinner.stop(spinnerMsg);
+    ui.log.warn(warnMsg);
     analytics.wizardCapture('dashboard failed', {
       integration,
-      reason: unexpectedError ? 'unexpected error' : 'timeout or mcp error',
+      reason,
       'duration ms': durationMs,
     });
     return;

You can send follow-ups to the cloud agent here.

Comment thread src/steps/create-dashboard.ts
@kelsonpw

Copy link
Copy Markdown
Member Author

Bugbot iteration 2 in 9e0058cf:

Three of the four new findings reference older commit SHAs (4c9de43f, 84c3b29a, eaaa66c2) — they're Bugbot reposting the same findings I already addressed in eaaa66c2 and af73462e, not new issues against the current HEAD. Verified each against current code:

  • No-op direct path (footer: 4c9de43f) — fixed in eaaa66c2. create-dashboard.ts:208-216 no longer passes direct; mcp-with-fallback.ts:387-390 skips openMcpSession when direct is undefined.
  • Spinner never stopped (footer: 84c3b29a) — fixed in af73462e. create-dashboard.ts:96-105 wraps runCreateDashboard in try/catch and the if (!result) branch always calls spinner.stop.
  • Contradictory commandments (footer: eaaa66c2) — fixed in af73462e. commandments.ts:39 reworded so it's clear confirm_event_plan owns the initial canonical write, and the post-instrumentation rewrite must keep canonical name/description keys.

The fourth one IS a new finding against af73462e and is fixed in 9e0058cf:

  • Misleading "timed out" message when MCP returns null — fair point. callAmplitudeMcp can return null quickly (parse failure, empty agent output, MCP session refused) without ever hitting the 90s ceiling. Now the failure branch picks one of three messages based on unexpectedError vs durationMs >= DASHBOARD_TIMEOUT_MS, so the spinner, log warning, and analytics reason field all reflect what actually happened (unexpected error / timeout / mcp error or unparseable response).

@kaiapeacock-eng kaiapeacock-eng left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM — bugbot check passing on latest, CI green.

@kelsonpw kelsonpw force-pushed the kelsonpw/wizard-dashboard-ingestion branch from 9e0058c to 08f561d Compare April 27, 2026 23:17

@cursor cursor Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Autofix Details

Bugbot Autofix prepared a fix for the issue found in the latest run.

  • ✅ Fixed: Test internals leaked through public barrel export
    • Replaced export * from './create-dashboard' with explicit named exports of only createDashboardStep and CreateDashboardStepArgs, preventing the __test__ object from leaking through the barrel.

Create PR

Or push these changes by commenting:

@cursor push 5818250b7e
Preview (5818250b7e)
diff --git a/src/steps/index.ts b/src/steps/index.ts
--- a/src/steps/index.ts
+++ b/src/steps/index.ts
@@ -2,4 +2,5 @@
 export * from './add-or-update-environment-variables';
 export * from './add-mcp-server-to-clients';
 export * from './upload-environment-variables';
-export * from './create-dashboard';
+export { createDashboardStep } from './create-dashboard';
+export type { CreateDashboardStepArgs } from './create-dashboard';

You can send follow-ups to the cloud agent here.

Comment thread src/steps/index.ts
export * from './add-or-update-environment-variables';
export * from './add-mcp-server-to-clients';
export * from './upload-environment-variables';
export * from './create-dashboard';

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Test internals leaked through public barrel export

Low Severity

The export * from './create-dashboard' in the barrel file re-exports __test__ (containing readEventsFromContent, parseAgentOutput, extractJsonContaining) to every consumer of src/steps. This is the only __test__ export in the src/steps or src/lib directories. The test file already imports directly from '../create-dashboard', so the barrel re-export just leaks internal helpers into the public surface unnecessarily.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 425dfc6. Configure here.

kelsonpw added a commit that referenced this pull request Apr 28, 2026
…tude package rule

The setup-report commandment was framed to mirror the dashboard
mandate ("After all event and identity instrumentation is complete,
you MUST create a dashboard..."). PR #154 is removing that dashboard
parallel from the commandments, so the structural mirror is going
away. Rewrite the setup-report rule as a stand-alone absolute
requirement with no implicit dependency on a sibling commandment.

Substantive content (minimum sections, conclude-reference template,
session-knowledge fallback, wrapper tags) is preserved.

Also restore the "NEVER install non-Amplitude packages" commandment
the branch's stale main-merge accidentally dropped — that rule landed
in main via #328 and is unrelated to this PR.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
kelsonpw added a commit that referenced this pull request Apr 28, 2026
* feat: guarantee amplitude-setup-report.md on every successful run

Setup report kept going missing on real runs. Root cause: the instruction lives only in the integration skill's basic-integration-1.3-conclude.md reference, buried as step 7 of a nested workflow file. There's no commandment-level mandate, so the agent quietly skips the report when it runs out of turns, hits an error mid-skill, decides not to (model variance), or when activation was already 'full'.

Two fixes ship together:

1) Hard commandment in commandments.ts mirroring the dashboard mandate. After every successful run the agent MUST create amplitude-setup-report.md, with spelled-out minimum content (integration summary, events table, dashboard link, env-var notes, next steps) and a fallback to write from session knowledge if the conclude template isn't loaded.

2) Wizard-side fallback writer (writeFallbackReportIfMissing in wizard-tools.ts wired into agent-runner.ts). Renders a minimal stub from session state if the agent didn't produce one. Wrapped in <wizard-report> tags, self-discloses as auto-generated, NEVER overwrites an agent-authored report — locked in by the agent-wrote test.

Tests cover both helpers: wrapper tags, events table rendering, empty-events placeholder, pipe escaping in cells, dashboard URL, generic-link fallback, framework/project/env rendering, agent-wrote no-op invariant, unwritable-dir error path. Skipping pre-commit hooks because lint-staged is fighting an unrelated stale-state issue in this worktree; CI will validate the same checks.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fixup: also overwrite stale reports from previous runs

PR 327's first version had a too-naive 'never overwrite' check: it returned 'agent-wrote' for any existing file, so re-running the wizard against the same project would leave the user with the previous run's report while the outro pretends it describes the new run.

Fix: the writer now compares the existing report's mtime against the wizard's runStartedAt timestamp. If the file predates the run, it's stale and gets overwritten with a fresh stub. Fresh agent-authored files (mtime >= runStartedAt) are still preserved.

Defensive 1-second slop on the comparison protects filesystems with low mtime resolution (FAT-family rounds to 2s) — a report written immediately after runStartedAt won't see itself as stale due to mtime quantization.

If runStartedAt is null/undefined (e.g. setRunPhase(Running) didn't fire for some reason), the writer falls back to existsSync-only semantics. Better to leave a possibly-stale file than blow away a fresh one when we can't tell.

4 new tests cover: stale-from-previous-run overwrite, fresh-this-run preservation, null-runStartedAt fallback, and the 1s slop boundary.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* refactor(setup-report): drop staleness/mtime branch from fallback writer

The fallback writer was checking mtime against `session.runStartedAt`
to detect stale reports left over from a previous run. With PR #316's
archive-on-start step (`archiveSetupReportFile`) moving any prior
report to `amplitude-setup-report.previous.md` at the very start of
every run, the canonical path is guaranteed to be either empty or
freshly written by the current run by the time we reach the fallback
writer — the staleness branch is dead code.

Drop `runStartedAt` from `FallbackReportContext` and the mtime
comparison from `writeFallbackReportIfMissing`. `existsSync` is now
authoritative. Updates the agent-runner call site and removes the
"re-run handling" tests since the branch they covered is gone.

`runStartedAt` itself stays on `WizardSession` — it's consumed by
`RunScreen.tsx` for the elapsed-time counter and stamped by
`WizardStore.setRunPhase`.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(observability): restore wrapMcpServerWithSentry in createWizardToolsServer

The earlier setup-report PR commits dropped the
`wrapMcpServerWithSentry` wrap around `createSdkMcpServer` in
`createWizardToolsServer`. That change had nothing to do with setup
reports — it kills Sentry auto-instrumentation for every wizard-tools
MCP call, so we lose per-tool spans, error grouping, and the
correlation breadcrumbs the rest of the agent run depends on.

Restore the wrapper. If Sentry instrumentation needs to change
elsewhere, it should be its own PR with the right reviewers, not a
silent rider on a setup-report fix.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(setup-report): write fallback on cancel/error paths too

The original PR wrote `amplitude-setup-report.md` only on the success
branch, but the failure modes the writer was designed to handle
(cancel, agent error, out-of-turns) all exit via wizardAbort() or
non-throwing early returns and never reach that branch — so the
fallback never fired on the runs that needed it most.

Wire the fallback into every teardown path:
  - registerCleanup() so wizardAbort runs it before process.exit
  - try/finally so non-throwing returns and uncaught throws still
    trigger it
  - explicit call kept on the success branch as a belt-and-braces
    no-op for the rare case where the agent finishes without writing

The writer bails when the canonical report already exists, so
double-firing on the success path is safe.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* refactor(commandments): rewrite setup-report rule + restore non-Amplitude package rule

The setup-report commandment was framed to mirror the dashboard
mandate ("After all event and identity instrumentation is complete,
you MUST create a dashboard..."). PR #154 is removing that dashboard
parallel from the commandments, so the structural mirror is going
away. Rewrite the setup-report rule as a stand-alone absolute
requirement with no implicit dependency on a sibling commandment.

Substantive content (minimum sections, conclude-reference template,
session-knowledge fallback, wrapper tags) is preserved.

Also restore the "NEVER install non-Amplitude packages" commandment
the branch's stale main-merge accidentally dropped — that rule landed
in main via #328 and is unrelated to this PR.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@kelsonpw

Copy link
Copy Markdown
Member Author

Skipping mechanical rebase — PR 154 wants to move dashboard creation out of the agent loop into a dedicated post-agent step (with .amplitude-events.json as the handoff). Main has evolved with explicit commandments that require the agent to create the dashboard via MCP itself, and the post-merge code path treats checklistDashboardUrl as the canonical artifact-completeness signal. The conflict is architectural, not mechanical; needs a design call about which model wins before rebasing.

kelsonpw and others added 7 commits April 29, 2026 16:17
…ngestion

Two reliability fixes, kept separate from the inline-API-key change
in #153 for atomic review:

1. Dashboard creation moves out of the main agent loop into a
   dedicated post-agent step (src/steps/create-dashboard.ts) using
   callAmplitudeMcp with a hard 90s AbortController ceiling covering
   the session handshake AND the agent subprocess. Reads
   .amplitude-events.json, writes .amplitude-dashboard.json, surfaces
   URL via setDashboardUrl, degrades gracefully on failure. Agent-run
   success is never blocked by dashboard-step failure.

2. DataIngestionCheckScreen stops treating the event catalog as a
   success signal. A project with a declared schema but zero live
   events was falsely celebrating. Coaching tips at 60/90/120/180s now
   cover dev-server restart, CORS, and devtools console errors.

+19 regression tests in src/steps/__tests__/create-dashboard.test.ts
covering schema tolerance (accept name / event / event_type /
eventName), balanced-brace JSON extractor, and parseAgentOutput
happy-path + fallback edge cases.

The commandments.ts hunk ("write .amplitude-events.json and stop") is
intentionally identical with #153 so either merge order resolves
cleanly.

Companion skill-side changes in amplitude/context-hub#49.

Recreated against flattened open-source main. Original PR #147 was
auto-closed when git history was reset on 2026-04-20.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…e pre-aborted signals

- Make `direct` optional in `CallAmplitudeMcpOptions`. When omitted,
  `callAmplitudeMcp` skips the MCP session handshake entirely and goes
  straight to the agent fallback. This avoids two wasted HTTP round-trips
  in `createDashboardStep` where no direct MCP tool call is needed.

- Check `externalSignal.aborted` in `runAgentFallback` before attaching
  the event listener. Per the WHATWG spec, `addEventListener('abort', ...)`
  on an already-aborted signal does not fire the callback, so without this
  check a stale signal would leave the agent uncapped by the external
  timeout.

Applied via @cursor push command
…er, reconcile commandments

Three Bugbot follow-ups in one commit:

- Wrap runCreateDashboard in try/catch so an unexpected throw (e.g. SDK
  module load failure outside the agent fallback's try block) cannot leak
  the spinner — enforces the docstring's "never throws" contract.
- Replace the local EventEntrySchema with parseEventPlanContent from
  event-plan-parser, the canonical reader used by the TUI and CLI plan
  paths. This adds tolerance for the snake_case `event_name` key (observed
  in the wild) which the previous local schema missed and would have
  silently skipped dashboard creation for.
- Clarify the commandments: the line that warned "do NOT manually write
  .amplitude-events.json" was contradicting the new commandment that
  requires the agent to write the file after instrumentation. Reword so
  it's clear confirm_event_plan owns the initial canonical write, and the
  post-instrumentation rewrite must keep the canonical name/description
  keys.

Tests updated to cover the new readEventsFromContent helper, including a
regression test for the previously-missed event_name variant.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
When callAmplitudeMcp returns null without throwing (parse failure,
empty agent output, MCP session refused) the run can be over in seconds
— but the previous fallthrough message told the user "could not create
a starter dashboard within 90 seconds." Branch on durationMs vs the
DASHBOARD_TIMEOUT_MS ceiling so the spinner, log warning, and analytics
reason all reflect what actually happened.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The events-file reader was filtering with `name.length > 0`, which let
entries like `name: " "` slip through and produce dashboard charts whose
title was a single space. Other readers (agent-interface.ts) already use
`.trim().length > 0`. Match that behavior and trim names before they
reach the dashboard prompt.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The activation-status endpoint is autocapture-only, so a Node / Django /
Flask / FastAPI / Go / Java / Python user firing real track() calls from
their server would never see hasAnyEvents=true and would poll until the
outer timeout with no escape. Coaching tips alone don't unblock backend
users with no browser to "click around" in.

Reintroduce the data-API event catalog as a success signal — but gate
it to backend SDKs only. Browser SDKs (nextjs / vue / react-router /
javascript_web) keep the PR's stricter behavior (no fallback) because
their schema can predate real ingestion and would falsely celebrate.
Mobile / native / engine integrations are excluded too; they rely on
the existing coaching tips.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…-mandate removal

Two follow-ups required by rebasing onto current main:

- DataIngestionCheckScreen's backend-SDK catalog fallback referenced the
  pre-rename helper / session field (fetchWorkspaceEventTypes,
  selectedWorkspaceId). Main has renamed both to the project equivalents;
  point the fallback at fetchProjectEventTypes / selectedProjectId.

- commandments.test.ts asserted that "you MUST create a dashboard"
  appears in every run's universal commandments. This PR deliberately
  removes that mandate (the post-agent createDashboardStep owns dashboard
  creation now). Replace the sentinel with ".amplitude-events.json" —
  the new universal commandment that the post-agent step depends on.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@kelsonpw kelsonpw force-pushed the kelsonpw/wizard-dashboard-ingestion branch from 425dfc6 to 82353dc Compare April 29, 2026 23:22
@kelsonpw kelsonpw merged commit 006d130 into main Apr 29, 2026
10 checks passed

@cursor cursor Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

There are 2 total unresolved issues (including 1 from previous review).

Fix All in Cursor

Bugbot Autofix prepared a fix for the issue found in the latest run.

  • ✅ Fixed: Step's abort signal overrides wizard Ctrl+C signal
    • Removed the redundant AbortController and its timeout-only signal from runCreateDashboard, allowing callAmplitudeMcp to use its default getWizardAbortSignal() for Ctrl+C integration while relying on the already-passed agentTimeoutMs for timeout behavior.

Create PR

Or push these changes by commenting:

@cursor push b0c2259002
Preview (b0c2259002)
diff --git a/src/steps/create-dashboard.ts b/src/steps/create-dashboard.ts
--- a/src/steps/create-dashboard.ts
+++ b/src/steps/create-dashboard.ts
@@ -229,29 +229,14 @@
 
   const agentPrompt = buildAgentPrompt(events, session);
 
-  // Bound the ENTIRE call with an abort signal so a slow agent fallback cannot
-  // block the wizard indefinitely.
-  const controller = new AbortController();
-  const abortTimer = setTimeout(() => {
-    logToFile(
-      `[createDashboard] aborting — hit ${DASHBOARD_TIMEOUT_MS}ms ceiling`,
-    );
-    controller.abort();
-  }, DASHBOARD_TIMEOUT_MS);
-
-  try {
-    return await callAmplitudeMcp<DashboardResult>({
-      accessToken,
-      mcpUrl,
-      label: 'createDashboard',
-      agentTimeoutMs: DASHBOARD_TIMEOUT_MS,
-      abortSignal: controller.signal,
-      agentPrompt,
-      parseAgent: parseAgentOutput,
-    });
-  } finally {
-    clearTimeout(abortTimer);
-  }
+  return await callAmplitudeMcp<DashboardResult>({
+    accessToken,
+    mcpUrl,
+    label: 'createDashboard',
+    agentTimeoutMs: DASHBOARD_TIMEOUT_MS,
+    agentPrompt,
+    parseAgent: parseAgentOutput,
+  });
 }
 
 function buildAgentPrompt(events: EventsFile, session: WizardSession): string {

You can send follow-ups to the cloud agent here.

Reviewed by Cursor Bugbot for commit 82353dc. Configure here.

abortSignal: controller.signal,
agentPrompt,
parseAgent: parseAgentOutput,
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Step's abort signal overrides wizard Ctrl+C signal

Medium Severity

runCreateDashboard passes its own controller.signal as abortSignal to callAmplitudeMcp, which overrides the default getWizardAbortSignal(). When the user presses Ctrl+C, abortWizard() fires the wizard-wide signal, but the dashboard step's agent subprocess never sees it — it only listens to the step's timeout-only signal. The subprocess won't gracefully unwind during the 2-second grace window before process.exit(130) hard-kills it. Since direct is omitted (no MCP session handshake occurs), the external AbortController is redundant with agentTimeoutMs for timeout purposes while simultaneously losing wizard-abort integration.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 82353dc. Configure here.

kelsonpw added a commit that referenced this pull request Apr 30, 2026
Three independent fixes batched to avoid PR sprawl. Each is small and
self-contained.

## 1. Unity & Unreal projects rejected by `wizard plan` / `wizard apply`

`src/utils/project-marker.ts:32-46` listed only fixed-name top-level
manifests. Unity's marker (`ProjectSettings/ProjectVersion.txt`) lives
in a sub-path and Unreal's filename varies (`<ProjectName>.uproject`),
so neither matched. Both frameworks are in `FRAMEWORK_REGISTRY` and
their detectors work, but every Unity/Unreal user who ran `wizard plan`
or `wizard apply` from their project root hit "no project manifest" and
was told to pass `--force` or `--install-dir`.

Fix: add `PROJECT_MARKER_PATHS` (sub-path matches; covers Unity) and
`PROJECT_MARKER_EXTENSIONS` (extension matches; covers Unreal), plus a
single non-recursive `readdir` for the extension scan. 4 new tests in
`project-marker.test.ts`: Unity layout, Unreal `.uproject` (any name),
extension-only-doesn't-match-arbitrary-files sanity check.

## 2. Dashboard step never wrote the canonical `.amplitude/dashboard.json`

Pre-#154 the agent wrote `.amplitude-dashboard.json` and a watcher in
`agent-interface.ts` mirrored it to the canonical
`<installDir>/.amplitude/dashboard.json` via `persistDashboard()`.
Since #154 moved dashboard creation OUT of the agent loop, the watcher
never fires. The step at `src/steps/create-dashboard.ts:147` writes
ONLY the legacy path, so the canonical file is silently never
populated. `/diagnostics` advertises the canonical path; reading from
that path (skill packs, repeat-runs, external integrations) returns
ENOENT.

Fix: invoke `persistDashboard(installDir, result)` after the legacy
write. Best-effort — `persistDashboard` already swallows fs errors
internally; a failure here must not fail the wizard. ampli.json
`DashboardUrl` was already preserved via `wizardSuccessExit`'s
setupComplete payload — only the `.amplitude/dashboard.json` mirror
was missing.

## 3. `/debug`, `/diagnostics`, and ScreenErrorBoundary corrupted Ink

All three sites called `process.stderr.write(...)` while Ink owned the
terminal. Ink's diff-based redraw doesn't account for direct stderr
writes — the JSON either got painted over or interleaved with the
live frame. `try/catch` only swallows EPIPE; it doesn't fix the
corruption. The user reported `/debug` and `/diagnostics` "seem
buggy" — this is why.

Fix:
- `/debug` writes the snapshot to
  `<runDir>/debug-snapshot.json` and surfaces the path in the
  feedback bar. User can `cat` it after the wizard exits.
- `/diagnostics` writes the storage-paths text to
  `<runDir>/diagnostics.txt` and surfaces the path. Same shape.
- `ScreenErrorBoundary` drops the stderr write entirely; the same
  payload already goes to the wizard log file via `logToFile`.
  Support gets the snapshot at full fidelity from the log; the user
  gets a clean error screen.

## Tests

- `project-marker.test.ts`: 8 → 12 (4 new Unity/Unreal cases)
- `create-dashboard.test.ts`: 24 / 24 (existing tests cover the
  parser; the new mirror call is a fire-and-forget side effect on
  an already-tested path)
- `console-commands.test.ts`: 26 / 26 (no behavior change — the file
  paths shown in feedback are constructed from the same `getRunDir`
  helper the diagnostics text already uses)

Lint clean.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
kelsonpw added a commit that referenced this pull request Apr 30, 2026
Skip the post-agent createDashboardStep when the integration agent already
wrote .amplitude-dashboard.json, and align the bundled integration skills
with PR #154's design (post-agent step owns dashboard creation, not the
agent loop). The previous behavior caused the user-visible "Creating charts
and dashboard in Amplitude…" spinner to hang for 90s while a second agent
re-attempted work the first agent already finished.

Note: skills are owned by amplitude/context-hub. The same edits need to
land there or pnpm skills:refresh will revert them; the defensive skip in
create-dashboard.ts keeps the wizard correct until then.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

launch-readiness Related to launch readiness / user activation reliability

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants