Skip to content

feat(scale): foundation for paginated event plan (>50-event projects); driver follow-up#604

Open
kelsonpw wants to merge 6 commits into
mainfrom
kelson/scale-paginate-event-plan
Open

feat(scale): foundation for paginated event plan (>50-event projects); driver follow-up#604
kelsonpw wants to merge 6 commits into
mainfrom
kelson/scale-paginate-event-plan

Conversation

@kelsonpw
Copy link
Copy Markdown
Member

@kelsonpw kelsonpw commented May 7, 2026

Summary

Convergent recommendation from SCALE_RESEARCH §C.2 and
LLM_RELIABILITY_RESEARCH §B.2: at ≥50 events the single-batch
confirm_event_plan flow runs into context-rot (~120 events) and the
rich tracking-plan doesn't survive context compaction (~200 events).
The fix is to chunk the write phase into ~25-event batches with a fresh
context per chunk.

This PR lands the schema, tools, and persistence groundwork for that
chunked write phase. The multi-session runner driver (driving the agent
through chunks via successive runAgent() calls with reset context) is
deliberately deferred — see "Architectural punt" below.

What changed

confirm_event_plan schema (additive — legacy shape still works)

{
  events: [{ name, description, callsites?: [{filePath, anchor?}] }, ...],
  decision?: 'confirm' | 'edit' | 'skip',  // agent-declared intent
  batchIndex?: number,        // 0-indexed batch number
  totalBatches?: number,      // total batches
  chunkSize?: number,         // informational; default 25
}

When batchIndex and totalBatches are absent the tool behaves exactly
as today (single full plan, single persistence). When present they MUST
satisfy 0 ≤ batchIndex < totalBatches — invalid combinations are
rejected with a structured error string before any persistence work runs.

New propose_event_plan tool

Pure pre-flight that takes a candidate event list and returns suggested
batch boundaries:

{
  "totalEvents": 75,
  "paginated": true,
  "chunkSize": 25,
  "suggestedBatches": [
    { "batchIndex": 0, "totalBatches": 3, "start": 0, "end": 25, "eventNames": [...] },
    { "batchIndex": 1, "totalBatches": 3, "start": 25, "end": 50, "eventNames": [...] },
    { "batchIndex": 2, "totalBatches": 3, "start": 50, "end": 75, "eventNames": [...] }
  ]
}

For ≤50 events the response is a single-batch covering everything (so
the agent can iterate the array unconditionally).

events.json schema additions

Optional callsites: [{filePath, anchor?}] per event. Persisted only
when present — small-project runs round-trip the original
{name, description} shape unchanged. persistEventPlan strips empty
callsites so the file stays scannable.

Per SCALE_RESEARCH §C.1: this is the field that lets the rich plan
survive compaction. After a compact the agent can re-read
.amplitude/events.json and remember where each track call was going to
go without re-deriving placement from scratch.

Threshold + override

Constant Value Override
PAGINATION_THRESHOLD 50 events (compile-time)
DEFAULT_CHUNK_SIZE 25 events AMPLITUDE_WIZARD_EVENT_PLAN_CHUNK_SIZE

The env override is clamped to [5, 100] — out-of-range values fall
back to the default with a debug-log breadcrumb. Defined in
src/lib/event-plan-pagination.ts.

Where pagination kicks in

File Line Role
src/lib/event-plan-pagination.ts new file shouldPaginate, buildBatches, validateBatchMetadata, resolveChunkSize (pure helpers)
src/lib/wizard-tools.ts ~1640 confirm_event_plan validates batch metadata before persistence
src/lib/wizard-tools.ts ~1748 new propose_event_plan tool computes suggested batches
src/lib/agent-runner.ts ~1295 runner-side breadcrumb (analytics + log) when the run crossed the threshold
src/lib/commandments.ts new commandment tells the agent to call propose_event_plan first when the candidate list >50

Tests added

  • src/lib/__tests__/event-plan-pagination.test.ts — 30 unit tests covering
    the threshold, env override (valid / non-numeric / 0 / negative / out-of-range),
    buildBatches correctness (75 events → 3 × 25, 200 events → 8 × 25,
    remainder, fractional chunk size), validateBatchMetadata (legacy shape,
    partial fields, batchIndex >= totalBatches, negative / non-integer values,
    out-of-range chunkSize), and the integration regression case (5 events
    → single batch, behavior unchanged).
  • src/lib/__tests__/wizard-tools.test.ts — 4 new persistEventPlan
    tests covering callsites round-trip, callsite-omission for legacy
    callers, empty-callsites pruning, and per-callsite anchor pruning.
  • WIZARD_TOOL_NAMES allowlist test updated for propose_event_plan.

Architectural punt (rationale)

The full multi-session runner driver — the orchestrator that runs
runAgent() once per batch with a clean context per chunk — is not
in this PR. That refactor lives in src/lib/agent-interface.ts's
runAgent and src/lib/agent-runner.ts's runAgentWizardBody, and
breaks the implicit assumption that one wizard run = one Claude Agent
SDK query() call (SCALE_RESEARCH §C.2 final paragraph). It needs:

  1. A driver loop in the runner that decides chunk boundaries from
    propose_event_plan's response.
  2. Per-batch user prompt construction that hands the agent only that
    batch's events plus the persisted callsites.
  3. Progress accumulation across batches (status spinner, per-batch
    summary, session checkpoint refresh).
  4. Cancellation semantics that compose with the existing
    runtime-start / pre-compact checkpoint flow.

That's a separate, focused PR. What this PR provides is the
contract: schema, persistence shape, validation, and the propose_event_plan
helper the driver will call. The runner-side analytics breadcrumb
(event plan paginated) lets us measure how often real-world runs cross
the threshold before the driver ships.

Strategic posture

Aligns with MIGRATION_PLAN.md: this is "in-tree evolution" of the
existing wizard surface, additive and backwards-compatible — no new
binding model, no UX overhaul. Small projects (<50 events) get the same
behavior as today.

Test plan

  • pnpm test — all 3118 unit tests pass
  • pnpm test:bdd — all 100 BDD scenarios pass
  • pnpm lint — prettier + eslint clean
  • pnpm build — compiles, smoke test passes
  • Live integration: not run in this PR (driver follow-up will exercise the schema end-to-end)

🤖 Generated with Claude Code


Note

Medium Risk
Introduces new event-plan batching and merge/persistence paths that affect confirm_event_plan behavior and on-disk events.json shape for large plans; mistakes could lead to incorrect plan persistence/merging or agent retry loops, though changes are additive and heavily tested.

Overview
Adds pagination groundwork for large event plans: a new pure event-plan-pagination helper module (threshold >50, configurable chunk size, batch building, and batch-metadata validation) plus a new wizard-tools:propose_event_plan tool that returns suggested batch boundaries for an event list.

Extends confirm_event_plan to accept optional batchIndex/totalBatches/chunkSize and per-event callsites, rejects invalid batch metadata with structured tool errors, and for non-first batches merges newly approved events into the existing .amplitude/events.json by name while preserving callsite annotations. persistEventPlan now normalizes/omits empty callsites, and event-plan-parser adds readLocalEventPlanRich to read/merge the richer schema safely.

Adds runner/agent guidance breadcrumbs: a new commandment describing the paginated flow, and agent-runner logs + emits analytics about whether a run crossed the pagination threshold. Comprehensive unit tests were added/updated to cover batching math, env overrides, rich parsing, callsites persistence, tool allowlists, and the new structured error path.

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

@kelsonpw kelsonpw requested a review from a team as a code owner May 7, 2026 05:20
Copy link
Copy Markdown
Contributor

@cursor cursor Bot left a comment

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: Batch merge loses callsites from earlier batches
    • Added readLocalEventPlanRich() that preserves callsites when reading the on-disk event plan, and switched the batch-merge path to use it instead of readLocalEventPlan() which stripped callsites.

Create PR

Or push these changes by commenting:

@cursor push 09650d5681
Preview (09650d5681)
diff --git a/src/lib/event-plan-parser.ts b/src/lib/event-plan-parser.ts
--- a/src/lib/event-plan-parser.ts
+++ b/src/lib/event-plan-parser.ts
@@ -162,3 +162,104 @@
 
   return parsed.filter((e) => e.name.trim().length > 0);
 }
+
+/**
+ * Like {@link readLocalEventPlan} but preserves optional `callsites` arrays
+ * written by `persistEventPlan`. Used by the batch-merge path so earlier
+ * batches' callsite annotations survive when subsequent batches are merged in.
+ */
+export function readLocalEventPlanRich(installDir: string): Array<{
+  name: string;
+  description: string;
+  callsites?: Array<{ filePath: string; anchor?: string }>;
+}> {
+  const candidates = [
+    getEventsFile(installDir),
+    path.join(installDir, '.amplitude-events.json'),
+  ];
+
+  let winner: string | null = null;
+  let winnerMtime = -Infinity;
+  for (const candidate of candidates) {
+    try {
+      const stat = fs.statSync(candidate);
+      const mtime = stat.mtimeMs;
+      if (mtime > winnerMtime) {
+        winner = candidate;
+        winnerMtime = mtime;
+      }
+    } catch (e) {
+      const err = e as NodeJS.ErrnoException;
+      if (err.code !== 'ENOENT') {
+        logToFile(
+          `[readLocalEventPlanRich] stat ${candidate} failed: ${
+            err.message ?? err
+          }`,
+        );
+      }
+    }
+  }
+
+  if (!winner) return [];
+
+  let raw: string;
+  try {
+    raw = fs.readFileSync(winner, 'utf8');
+  } catch (e) {
+    const err = e as NodeJS.ErrnoException;
+    logToFile(
+      `[readLocalEventPlanRich] read ${winner} failed: ${err.message ?? err}`,
+    );
+    return [];
+  }
+
+  let parsed: unknown;
+  try {
+    parsed = JSON.parse(raw);
+  } catch {
+    logToFile(`[readLocalEventPlanRich] ${winner} could not be parsed as JSON`);
+    return [];
+  }
+
+  if (
+    parsed &&
+    typeof parsed === 'object' &&
+    !Array.isArray(parsed) &&
+    Array.isArray((parsed as { events?: unknown }).events)
+  ) {
+    parsed = (parsed as { events: unknown[] }).events;
+  }
+
+  if (!Array.isArray(parsed)) return [];
+
+  return (parsed as Array<Record<string, unknown>>)
+    .map((e) => {
+      const name =
+        (e.name as string) ??
+        (e.event as string) ??
+        (e.eventName as string) ??
+        (e.event_name as string) ??
+        '';
+      const description =
+        (e.description as string) ??
+        (e.event_description as string) ??
+        (e.eventDescription as string) ??
+        (e.eventDescriptionAndReasoning as string) ??
+        '';
+      const result: {
+        name: string;
+        description: string;
+        callsites?: Array<{ filePath: string; anchor?: string }>;
+      } = {
+        name,
+        description,
+      };
+      if (Array.isArray(e.callsites) && e.callsites.length > 0) {
+        result.callsites = (
+          e.callsites as Array<{ filePath: string; anchor?: string }>
+        ).filter((c) => c && typeof c.filePath === 'string');
+      }
+      return result;
+    })
+    .filter((e) => e.name.trim().length > 0);
+}

diff --git a/src/lib/wizard-tools.ts b/src/lib/wizard-tools.ts
--- a/src/lib/wizard-tools.ts
+++ b/src/lib/wizard-tools.ts
@@ -14,8 +14,11 @@
 import { z } from 'zod';
 import { logToFile } from '../utils/debug';
 import { atomicWriteJSON } from '../utils/atomic-write';
-import { readLocalEventPlan } from './event-plan-parser.js';
 import {
+  readLocalEventPlan,
+  readLocalEventPlanRich,
+} from './event-plan-parser.js';
+import {
   ensureDir,
   getDashboardFile,
   getEventsFile,
@@ -1785,10 +1788,16 @@
         let toPersist: typeof events = events;
         if (!isFirstBatch) {
           // Merge with existing on-disk plan so we don't lose earlier batches.
-          const existing = readLocalEventPlan(workingDirectory);
+          // Use the rich reader that preserves callsites so annotations from
+          // prior batches accumulate across compaction boundaries.
+          const existing = readLocalEventPlanRich(workingDirectory);
           const byName = new Map<
             string,
-            { name: string; description: string }
+            {
+              name: string;
+              description: string;
+              callsites?: Array<{ filePath: string; anchor?: string }>;
+            }
           >();
           for (const e of existing) byName.set(e.name, e);
           for (const e of events) byName.set(e.name, e);

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

Comment thread src/lib/wizard-tools.ts
@kelsonpw
Copy link
Copy Markdown
Member Author

kelsonpw commented May 7, 2026

@cursor push 09650d5

Copy link
Copy Markdown
Contributor

@cursor cursor Bot left a comment

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: Duplicated file-discovery logic in readLocalEventPlanRich
    • Extracted a shared readFreshestEventPlanFile helper that encapsulates the candidate enumeration, stat loop with mtime comparison, winner selection, and file reading, eliminating ~35 lines of duplication between readLocalEventPlan and readLocalEventPlanRich.

Create PR

Or push these changes by commenting:

@cursor push ff9e71f6b5
Preview (ff9e71f6b5)
diff --git a/src/lib/event-plan-parser.ts b/src/lib/event-plan-parser.ts
--- a/src/lib/event-plan-parser.ts
+++ b/src/lib/event-plan-parser.ts
@@ -93,25 +93,14 @@
 }
 
 /**
- * Read the agent-written event plan from a project's install dir.
- *
- * Tries the canonical path first (`<installDir>/.amplitude/events.json`),
- * then the legacy dotfile (`<installDir>/.amplitude-events.json`) that
- * older context-hub integration skills still emit. Returns whichever has
- * the more recent mtime so a stale canonical from a previous run can't
- * shadow a fresh legacy write — mirroring the `pickFreshestExisting`
- * logic in `agent-interface.ts`.
- *
- * Returns `[]` for any non-fatal failure (file missing, malformed JSON,
- * schema mismatch). Empty entries (no name) are dropped. Logs to the
- * debug file so issues are recoverable post-mortem.
- *
- * Used by the Event Verification screen to render the planned tracking
- * list inline so users can see what they need to trigger.
+ * Find the freshest event-plan file among canonical and legacy paths,
+ * read it, and return its raw content. Returns `null` when no candidate
+ * exists or the read fails.
  */
-export function readLocalEventPlan(
+function readFreshestEventPlanFile(
   installDir: string,
-): Array<{ name: string; description: string }> {
+  caller: string,
+): string | null {
   const candidates = [
     getEventsFile(installDir),
     path.join(installDir, '.amplitude-events.json'),
@@ -131,32 +120,49 @@
       const err = e as NodeJS.ErrnoException;
       if (err.code !== 'ENOENT') {
         logToFile(
-          `[readLocalEventPlan] stat ${candidate} failed: ${
-            err.message ?? err
-          }`,
+          `[${caller}] stat ${candidate} failed: ${err.message ?? err}`,
         );
       }
     }
   }
 
-  if (!winner) return [];
+  if (!winner) return null;
 
-  let raw: string;
   try {
-    raw = fs.readFileSync(winner, 'utf8');
+    return fs.readFileSync(winner, 'utf8');
   } catch (e) {
     const err = e as NodeJS.ErrnoException;
-    logToFile(
-      `[readLocalEventPlan] read ${winner} failed: ${err.message ?? err}`,
-    );
-    return [];
+    logToFile(`[${caller}] read ${winner} failed: ${err.message ?? err}`);
+    return null;
   }
+}
 
+/**
+ * Read the agent-written event plan from a project's install dir.
+ *
+ * Tries the canonical path first (`<installDir>/.amplitude/events.json`),
+ * then the legacy dotfile (`<installDir>/.amplitude-events.json`) that
+ * older context-hub integration skills still emit. Returns whichever has
+ * the more recent mtime so a stale canonical from a previous run can't
+ * shadow a fresh legacy write — mirroring the `pickFreshestExisting`
+ * logic in `agent-interface.ts`.
+ *
+ * Returns `[]` for any non-fatal failure (file missing, malformed JSON,
+ * schema mismatch). Empty entries (no name) are dropped. Logs to the
+ * debug file so issues are recoverable post-mortem.
+ *
+ * Used by the Event Verification screen to render the planned tracking
+ * list inline so users can see what they need to trigger.
+ */
+export function readLocalEventPlan(
+  installDir: string,
+): Array<{ name: string; description: string }> {
+  const raw = readFreshestEventPlanFile(installDir, 'readLocalEventPlan');
+  if (raw === null) return [];
+
   const parsed = parseEventPlanContent(raw);
   if (parsed === null) {
-    logToFile(
-      `[readLocalEventPlan] ${winner} could not be parsed as an event plan`,
-    );
+    logToFile(`[readLocalEventPlan] event plan file could not be parsed`);
     return [];
   }
 
@@ -173,51 +179,16 @@
   description: string;
   callsites?: Array<{ filePath: string; anchor?: string }>;
 }> {
-  const candidates = [
-    getEventsFile(installDir),
-    path.join(installDir, '.amplitude-events.json'),
-  ];
+  const raw = readFreshestEventPlanFile(installDir, 'readLocalEventPlanRich');
+  if (raw === null) return [];
 
-  let winner: string | null = null;
-  let winnerMtime = -Infinity;
-  for (const candidate of candidates) {
-    try {
-      const stat = fs.statSync(candidate);
-      const mtime = stat.mtimeMs;
-      if (mtime > winnerMtime) {
-        winner = candidate;
-        winnerMtime = mtime;
-      }
-    } catch (e) {
-      const err = e as NodeJS.ErrnoException;
-      if (err.code !== 'ENOENT') {
-        logToFile(
-          `[readLocalEventPlanRich] stat ${candidate} failed: ${
-            err.message ?? err
-          }`,
-        );
-      }
-    }
-  }
-
-  if (!winner) return [];
-
-  let raw: string;
-  try {
-    raw = fs.readFileSync(winner, 'utf8');
-  } catch (e) {
-    const err = e as NodeJS.ErrnoException;
-    logToFile(
-      `[readLocalEventPlanRich] read ${winner} failed: ${err.message ?? err}`,
-    );
-    return [];
-  }
-
   let parsed: unknown;
   try {
     parsed = JSON.parse(raw);
   } catch {
-    logToFile(`[readLocalEventPlanRich] ${winner} could not be parsed as JSON`);
+    logToFile(
+      `[readLocalEventPlanRich] event plan file could not be parsed as JSON`,
+    );
     return [];
   }

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

Comment thread src/lib/event-plan-parser.ts Outdated
@kelsonpw kelsonpw force-pushed the kelson/scale-paginate-event-plan branch from 328c097 to ee14860 Compare May 7, 2026 15:32
Copy link
Copy Markdown
Contributor

@cursor cursor Bot left a comment

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: readLocalEventPlanRich crashes on non-string field values
    • Replaced unsafe as string casts with a runtime str() helper that returns undefined for non-string values, allowing ?? to correctly fall through to defaults.
  • ✅ Fixed: readLocalEventPlanRich may return empty callsites array
    • Added a post-filter length check so result.callsites is only assigned when the filtered array has at least one valid entry.

Create PR

Or push these changes by commenting:

@cursor push e8c745269a
Preview (e8c745269a)
diff --git a/src/lib/event-plan-parser.ts b/src/lib/event-plan-parser.ts
--- a/src/lib/event-plan-parser.ts
+++ b/src/lib/event-plan-parser.ts
@@ -209,19 +209,22 @@
 
   if (!Array.isArray(parsed)) return [];
 
+  const str = (v: unknown): string | undefined =>
+    typeof v === 'string' ? v : undefined;
+
   return (parsed as Array<Record<string, unknown>>)
     .map((e) => {
       const name =
-        (e.name as string) ??
-        (e.event as string) ??
-        (e.eventName as string) ??
-        (e.event_name as string) ??
+        str(e.name) ??
+        str(e.event) ??
+        str(e.eventName) ??
+        str(e.event_name) ??
         '';
       const description =
-        (e.description as string) ??
-        (e.event_description as string) ??
-        (e.eventDescription as string) ??
-        (e.eventDescriptionAndReasoning as string) ??
+        str(e.description) ??
+        str(e.event_description) ??
+        str(e.eventDescription) ??
+        str(e.eventDescriptionAndReasoning) ??
         '';
       const result: {
         name: string;
@@ -232,9 +235,12 @@
         description,
       };
       if (Array.isArray(e.callsites) && e.callsites.length > 0) {
-        result.callsites = (
+        const filtered = (
           e.callsites as Array<{ filePath: string; anchor?: string }>
         ).filter((c) => c && typeof c.filePath === 'string');
+        if (filtered.length > 0) {
+          result.callsites = filtered;
+        }
       }
       return result;
     })

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

Comment thread src/lib/event-plan-parser.ts
Comment thread src/lib/event-plan-parser.ts
@kelsonpw kelsonpw force-pushed the kelson/scale-paginate-event-plan branch 2 times, most recently from 7361393 to aaf6818 Compare May 7, 2026 16:43
Copy link
Copy Markdown
Contributor

@cursor cursor Bot left a comment

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: Batch validation error uses bare string, not structured helper
    • Replaced the bare { content: [{ type: 'text', text: '...' }] } response with toWizardToolErrorContent() which adds isError: true and the structured { success, error, guidance, suggestedTool, context } envelope, consistent with every other error path in the file.

Create PR

Or push these changes by commenting:

@cursor push 7ecd7717ff
Preview (7ecd7717ff)
diff --git a/src/lib/wizard-tools.ts b/src/lib/wizard-tools.ts
--- a/src/lib/wizard-tools.ts
+++ b/src/lib/wizard-tools.ts
@@ -1947,14 +1947,13 @@
       });
       if (batchError) {
         logToFile(`confirm_event_plan: rejected batch metadata: ${batchError}`);
-        return {
-          content: [
-            {
-              type: 'text' as const,
-              text: `error: invalid batch metadata — ${batchError}. Re-call without batch fields to fall back to single-batch behavior, or fix the metadata and retry.`,
-            },
-          ],
-        };
+        return toWizardToolErrorContent({
+          error: `invalid batch metadata — ${batchError}`,
+          guidance:
+            'Re-call confirm_event_plan without batch fields to fall back to single-batch behavior, or fix the metadata and retry.',
+          suggestedTool: 'mcp__wizard-tools__confirm_event_plan',
+          context: `batchIndex: ${args.batchIndex}; totalBatches: ${args.totalBatches}; chunkSize: ${args.chunkSize}; eventCount: ${args.events.length}`,
+        });
       }
       // Soft-gate the name format. Agents historically saw conflicting
       // guidance (commandments said Title Case, the tool schema said

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

Comment thread src/lib/wizard-tools.ts Outdated
kelsonpw added a commit that referenced this pull request May 7, 2026
…ection

Addresses Cursor Bugbot #604: confirm_event_plan's batch-metadata
validation path returned a bare-text content block instead of going
through toWizardToolErrorContent. Without the structured envelope the
agent SDK doesn't see isError=true and the agent has no deterministic
recovery path — it tends to retry the same malformed call until the
circuit breaker fires.

Switch to toWizardToolErrorContent so the response includes the
standard {success:false, error, guidance, suggestedTool, context}
shape and isError=true. Adds a regression test that exercises the
batchIndex-without-totalBatches case.
@kelsonpw kelsonpw force-pushed the kelson/scale-paginate-event-plan branch from aaf6818 to 2293362 Compare May 7, 2026 17:21
Copy link
Copy Markdown
Contributor

@cursor cursor Bot left a comment

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: Schema min(1) contradicts validator minimum of 5
    • Changed the zod schema's .min(1) to .min(5) for chunkSize in confirm_event_plan to match the MIN_CHUNK_SIZE enforced by validateBatchMetadata.

Create PR

Or push these changes by commenting:

@cursor push 3b40d961f5
Preview (3b40d961f5)
diff --git a/src/lib/wizard-tools.ts b/src/lib/wizard-tools.ts
--- a/src/lib/wizard-tools.ts
+++ b/src/lib/wizard-tools.ts
@@ -1911,7 +1911,7 @@
       chunkSize: z
         .number()
         .int()
-        .min(1)
+        .min(5)
         .optional()
         .describe(
           'Events per batch (declared by the agent for transparency). Default 25; configurable via AMPLITUDE_WIZARD_EVENT_PLAN_CHUNK_SIZE. The runner is the source of truth for chunking — this field is informational.',

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

Comment thread src/lib/wizard-tools.ts
kelsonpw added a commit that referenced this pull request May 7, 2026
…ection

Addresses Cursor Bugbot #604: confirm_event_plan's batch-metadata
validation path returned a bare-text content block instead of going
through toWizardToolErrorContent. Without the structured envelope the
agent SDK doesn't see isError=true and the agent has no deterministic
recovery path — it tends to retry the same malformed call until the
circuit breaker fires.

Switch to toWizardToolErrorContent so the response includes the
standard {success:false, error, guidance, suggestedTool, context}
shape and isError=true. Adds a regression test that exercises the
batchIndex-without-totalBatches case.
@kelsonpw kelsonpw force-pushed the kelson/scale-paginate-event-plan branch from 2293362 to 0abe035 Compare May 7, 2026 19:51
Copy link
Copy Markdown
Contributor

@cursor cursor Bot left a comment

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.

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

Fix All in Cursor

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

  • ✅ Fixed: Unguarded analytics block can prevent critical event commit
    • Wrapped the pagination analytics block (dynamic import + computation) in a try/catch so any failure is logged but cannot prevent commitPlannedEventsStep from executing.
  • ✅ Fixed: Proposed chunkSize can be below validation minimum
    • Changed propose_event_plan's response payload to use the resolved chunkSize (always >= MIN_CHUNK_SIZE) instead of effectiveChunk, which could be below MIN_CHUNK_SIZE for small projects.

Create PR

Or push these changes by commenting:

@cursor push b502a5376e
Preview (b502a5376e)
diff --git a/src/lib/agent-runner.ts b/src/lib/agent-runner.ts
--- a/src/lib/agent-runner.ts
+++ b/src/lib/agent-runner.ts
@@ -1531,7 +1531,7 @@
   // codebases hit the wizard before the chunked write phase is end-to-end
   // wired through the SDK (the schema + tools land in this PR; the
   // multi-session driver is a follow-up — see PR description).
-  {
+  try {
     const eventCount = agentResult.plannedEvents?.length ?? 0;
     const { shouldPaginate, resolveChunkSize, buildBatches } = await import(
       './event-plan-pagination.js'
@@ -1551,6 +1551,12 @@
       'total batches': totalBatches,
       integration: config.metadata.integration,
     });
+  } catch (err) {
+    logToFile(
+      `[agent-runner] pagination analytics threw: ${
+        err instanceof Error ? err.message : String(err)
+      }`,
+    );
   }
 
   const plannedEventsSummary = await commitPlannedEventsStep(

diff --git a/src/lib/wizard-tools.ts b/src/lib/wizard-tools.ts
--- a/src/lib/wizard-tools.ts
+++ b/src/lib/wizard-tools.ts
@@ -2257,7 +2257,7 @@
       const payload = {
         totalEvents,
         paginated,
-        chunkSize: effectiveChunk,
+        chunkSize,
         suggestedBatches,
       };
       logToFile(

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

Reviewed by Cursor Bugbot for commit 0abe035. Configure here.

Comment thread src/lib/agent-runner.ts
Comment thread src/lib/wizard-tools.ts
kelsonpw added a commit that referenced this pull request May 7, 2026
…ection

Addresses Cursor Bugbot #604: confirm_event_plan's batch-metadata
validation path returned a bare-text content block instead of going
through toWizardToolErrorContent. Without the structured envelope the
agent SDK doesn't see isError=true and the agent has no deterministic
recovery path — it tends to retry the same malformed call until the
circuit breaker fires.

Switch to toWizardToolErrorContent so the response includes the
standard {success:false, error, guidance, suggestedTool, context}
shape and isError=true. Adds a regression test that exercises the
batchIndex-without-totalBatches case.
@kelsonpw kelsonpw force-pushed the kelson/scale-paginate-event-plan branch from 0abe035 to 33c8054 Compare May 7, 2026 21:22
kelsonpw added a commit that referenced this pull request May 8, 2026
…ection

Addresses Cursor Bugbot #604: confirm_event_plan's batch-metadata
validation path returned a bare-text content block instead of going
through toWizardToolErrorContent. Without the structured envelope the
agent SDK doesn't see isError=true and the agent has no deterministic
recovery path — it tends to retry the same malformed call until the
circuit breaker fires.

Switch to toWizardToolErrorContent so the response includes the
standard {success:false, error, guidance, suggestedTool, context}
shape and isError=true. Adds a regression test that exercises the
batchIndex-without-totalBatches case.
@kelsonpw kelsonpw force-pushed the kelson/scale-paginate-event-plan branch from 33c8054 to ac80e4b Compare May 8, 2026 17:18
kelsonpw added a commit that referenced this pull request May 8, 2026
…ection

Addresses Cursor Bugbot #604: confirm_event_plan's batch-metadata
validation path returned a bare-text content block instead of going
through toWizardToolErrorContent. Without the structured envelope the
agent SDK doesn't see isError=true and the agent has no deterministic
recovery path — it tends to retry the same malformed call until the
circuit breaker fires.

Switch to toWizardToolErrorContent so the response includes the
standard {success:false, error, guidance, suggestedTool, context}
shape and isError=true. Adds a regression test that exercises the
batchIndex-without-totalBatches case.
@kelsonpw kelsonpw force-pushed the kelson/scale-paginate-event-plan branch from ac80e4b to 6aec44c Compare May 8, 2026 17:29
kelsonpw added a commit that referenced this pull request May 8, 2026
…ection

Addresses Cursor Bugbot #604: confirm_event_plan's batch-metadata
validation path returned a bare-text content block instead of going
through toWizardToolErrorContent. Without the structured envelope the
agent SDK doesn't see isError=true and the agent has no deterministic
recovery path — it tends to retry the same malformed call until the
circuit breaker fires.

Switch to toWizardToolErrorContent so the response includes the
standard {success:false, error, guidance, suggestedTool, context}
shape and isError=true. Adds a regression test that exercises the
batchIndex-without-totalBatches case.
@kelsonpw kelsonpw force-pushed the kelson/scale-paginate-event-plan branch from 6aec44c to c31bc99 Compare May 8, 2026 17:55
kelsonpw added a commit that referenced this pull request May 8, 2026
…ection

Addresses Cursor Bugbot #604: confirm_event_plan's batch-metadata
validation path returned a bare-text content block instead of going
through toWizardToolErrorContent. Without the structured envelope the
agent SDK doesn't see isError=true and the agent has no deterministic
recovery path — it tends to retry the same malformed call until the
circuit breaker fires.

Switch to toWizardToolErrorContent so the response includes the
standard {success:false, error, guidance, suggestedTool, context}
shape and isError=true. Adds a regression test that exercises the
batchIndex-without-totalBatches case.
@kelsonpw kelsonpw force-pushed the kelson/scale-paginate-event-plan branch from c31bc99 to b50ef95 Compare May 8, 2026 18:09
kelsonpw added a commit that referenced this pull request May 8, 2026
…ection

Addresses Cursor Bugbot #604: confirm_event_plan's batch-metadata
validation path returned a bare-text content block instead of going
through toWizardToolErrorContent. Without the structured envelope the
agent SDK doesn't see isError=true and the agent has no deterministic
recovery path — it tends to retry the same malformed call until the
circuit breaker fires.

Switch to toWizardToolErrorContent so the response includes the
standard {success:false, error, guidance, suggestedTool, context}
shape and isError=true. Adds a regression test that exercises the
batchIndex-without-totalBatches case.
@kelsonpw kelsonpw force-pushed the kelson/scale-paginate-event-plan branch from b50ef95 to e1887d1 Compare May 8, 2026 18:49
kelsonpw added a commit that referenced this pull request May 8, 2026
…ection

Addresses Cursor Bugbot #604: confirm_event_plan's batch-metadata
validation path returned a bare-text content block instead of going
through toWizardToolErrorContent. Without the structured envelope the
agent SDK doesn't see isError=true and the agent has no deterministic
recovery path — it tends to retry the same malformed call until the
circuit breaker fires.

Switch to toWizardToolErrorContent so the response includes the
standard {success:false, error, guidance, suggestedTool, context}
shape and isError=true. Adds a regression test that exercises the
batchIndex-without-totalBatches case.
@kelsonpw kelsonpw force-pushed the kelson/scale-paginate-event-plan branch from e1887d1 to 3157e31 Compare May 8, 2026 19:13
kelsonpw added a commit that referenced this pull request May 8, 2026
…ection

Addresses Cursor Bugbot #604: confirm_event_plan's batch-metadata
validation path returned a bare-text content block instead of going
through toWizardToolErrorContent. Without the structured envelope the
agent SDK doesn't see isError=true and the agent has no deterministic
recovery path — it tends to retry the same malformed call until the
circuit breaker fires.

Switch to toWizardToolErrorContent so the response includes the
standard {success:false, error, guidance, suggestedTool, context}
shape and isError=true. Adds a regression test that exercises the
batchIndex-without-totalBatches case.
@kelsonpw kelsonpw force-pushed the kelson/scale-paginate-event-plan branch from 3157e31 to 9e0c4ea Compare May 8, 2026 19:29
kelsonpw added a commit that referenced this pull request May 8, 2026
…ection

Addresses Cursor Bugbot #604: confirm_event_plan's batch-metadata
validation path returned a bare-text content block instead of going
through toWizardToolErrorContent. Without the structured envelope the
agent SDK doesn't see isError=true and the agent has no deterministic
recovery path — it tends to retry the same malformed call until the
circuit breaker fires.

Switch to toWizardToolErrorContent so the response includes the
standard {success:false, error, guidance, suggestedTool, context}
shape and isError=true. Adds a regression test that exercises the
batchIndex-without-totalBatches case.
@kelsonpw kelsonpw force-pushed the kelson/scale-paginate-event-plan branch 2 times, most recently from fe1b6d0 to 7815e26 Compare May 8, 2026 22:30
kelsonpw added a commit that referenced this pull request May 8, 2026
…ection

Addresses Cursor Bugbot #604: confirm_event_plan's batch-metadata
validation path returned a bare-text content block instead of going
through toWizardToolErrorContent. Without the structured envelope the
agent SDK doesn't see isError=true and the agent has no deterministic
recovery path — it tends to retry the same malformed call until the
circuit breaker fires.

Switch to toWizardToolErrorContent so the response includes the
standard {success:false, error, guidance, suggestedTool, context}
shape and isError=true. Adds a regression test that exercises the
batchIndex-without-totalBatches case.
@kelsonpw kelsonpw changed the title feat(scale): paginate event plan for >50-event projects (chunks of 25) feat(scale): foundation for paginated event plan (>50-event projects); driver follow-up May 9, 2026
@kelsonpw kelsonpw force-pushed the kelson/scale-paginate-event-plan branch from e64b786 to 33c0f8e Compare May 14, 2026 18:02
kelsonpw added a commit that referenced this pull request May 14, 2026
…ection

Addresses Cursor Bugbot #604: confirm_event_plan's batch-metadata
validation path returned a bare-text content block instead of going
through toWizardToolErrorContent. Without the structured envelope the
agent SDK doesn't see isError=true and the agent has no deterministic
recovery path — it tends to retry the same malformed call until the
circuit breaker fires.

Switch to toWizardToolErrorContent so the response includes the
standard {success:false, error, guidance, suggestedTool, context}
shape and isError=true. Adds a regression test that exercises the
batchIndex-without-totalBatches case.
@kelsonpw kelsonpw force-pushed the kelson/scale-paginate-event-plan branch from 33c0f8e to eba4775 Compare May 14, 2026 19:05
kelsonpw added a commit that referenced this pull request May 14, 2026
…ection

Addresses Cursor Bugbot #604: confirm_event_plan's batch-metadata
validation path returned a bare-text content block instead of going
through toWizardToolErrorContent. Without the structured envelope the
agent SDK doesn't see isError=true and the agent has no deterministic
recovery path — it tends to retry the same malformed call until the
circuit breaker fires.

Switch to toWizardToolErrorContent so the response includes the
standard {success:false, error, guidance, suggestedTool, context}
shape and isError=true. Adds a regression test that exercises the
batchIndex-without-totalBatches case.
@kelsonpw kelsonpw force-pushed the kelson/scale-paginate-event-plan branch from eba4775 to d28577b Compare May 14, 2026 22:01
kelsonpw added a commit that referenced this pull request May 15, 2026
…ection

Addresses Cursor Bugbot #604: confirm_event_plan's batch-metadata
validation path returned a bare-text content block instead of going
through toWizardToolErrorContent. Without the structured envelope the
agent SDK doesn't see isError=true and the agent has no deterministic
recovery path — it tends to retry the same malformed call until the
circuit breaker fires.

Switch to toWizardToolErrorContent so the response includes the
standard {success:false, error, guidance, suggestedTool, context}
shape and isError=true. Adds a regression test that exercises the
batchIndex-without-totalBatches case.
@kelsonpw kelsonpw force-pushed the kelson/scale-paginate-event-plan branch from a59ec22 to d802810 Compare May 15, 2026 23:54
@kelsonpw kelsonpw removed the request for review from a team May 18, 2026 18:05
kelsonpw and others added 6 commits May 22, 2026 11:06
SCALE_RESEARCH §C.2 + LLM_RELIABILITY_RESEARCH §B.2 converge on the same
recommendation: at ≥50 events the single-batch confirm_event_plan flow
runs into context-rot (~120 events) and the rich tracking-plan doesn't
survive context compaction (~200 events). The fix is to chunk the write
phase into ~25-event batches with a fresh context per chunk.

This PR lands the schema, tools, and persistence groundwork; the
multi-session runner driver is a follow-up (rationale in PR description).

Schema extensions to confirm_event_plan (additive — legacy shape still
works as today):
  - decision?: 'confirm' | 'edit' | 'skip'  -- agent self-declared intent
  - batchIndex?: number       -- 0-indexed batch when paginated
  - totalBatches?: number     -- total batches in the paginated plan
  - chunkSize?: number        -- events per batch (informational)
  - events[].callsites?: [{filePath, anchor?}]  -- per-event call sites,
    persisted to .amplitude/events.json so the rich plan survives compaction

New propose_event_plan tool: pure pre-flight that takes a candidate event
list and returns suggested batch boundaries
({ totalEvents, paginated, chunkSize, suggestedBatches: [...] }).

events.json now optionally carries callsites[] per event (additive — small
projects remain {name, description} only). persistEventPlan strips empty
callsites so the file stays scannable.

Threshold + override:
  - PAGINATION_THRESHOLD = 50 (event count)
  - DEFAULT_CHUNK_SIZE = 25
  - AMPLITUDE_WIZARD_EVENT_PLAN_CHUNK_SIZE env override (clamped [5, 100])

Runner-side: after the agent finishes, the runner logs/captures whether
the run crossed the pagination threshold (analytics breadcrumb
'event plan paginated'). Useful for measuring how often >50-event
codebases hit the wizard before the multi-session driver lands.

Validation: confirm_event_plan rejects malformed batch metadata
(batchIndex without totalBatches, batchIndex >= totalBatches, negative
values, empty events with batch fields, out-of-range chunkSize).

Tests: 30 new pagination unit tests + 4 new persistEventPlan callsite
tests + the WIZARD_TOOL_NAMES allowlist updated for propose_event_plan.
All 3118 existing unit tests + 100 BDD scenarios still pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The batch-merge logic used readLocalEventPlan() which delegates to
parseEventPlanContent() — a function that maps events to only
{name, description}, stripping any persisted callsites. When batch 2+
merged with earlier batches, all callsite annotations from prior
batches were permanently lost.

Add readLocalEventPlanRich() that parses the on-disk JSON preserving
the optional callsites array, and use it in the merge path so
call-site annotations correctly accumulate across batches.

Applied via @cursor push command
…elper

readLocalEventPlan and readLocalEventPlanRich shared ~35 lines of
verbatim file-discovery (canonical / legacy candidate stat, freshest-by-
mtime winner selection, read with logged failure paths). Lift the
duplication into a private readFreshestEventPlanFile(installDir, label)
helper so future callers don't drift. Behaviour is unchanged — both
public readers continue to log under their own label and return [] on
any non-fatal failure.
… JSON

A user-edited `events.json` with non-string values (numeric `name`, `null`
description, object-typed fields) crashed the batch-merge path inside
`String.prototype.trim`. Coerce field values to safe strings, drop entries
whose name can't be coerced, and guard non-object array entries / malformed
callsites instead of trusting `as string` casts.

Also align the return contract with `parseEventPlanContent`: return `null`
for "missing or unparseable" and `[]` for "parsed cleanly but contained
zero events" so the merge call site can distinguish the two — currently
both produced `[]`. Update the only caller in `wizard-tools.ts` to default
to `[]` when the rich reader returns `null` (nothing to merge).

Adds regression tests covering numeric / null / object / boolean field
values, primitive top-level entries, malformed callsites, and the new
null-vs-empty distinction.
…ection

Addresses Cursor Bugbot #604: confirm_event_plan's batch-metadata
validation path returned a bare-text content block instead of going
through toWizardToolErrorContent. Without the structured envelope the
agent SDK doesn't see isError=true and the agent has no deterministic
recovery path — it tends to retry the same malformed call until the
circuit breaker fires.

Switch to toWizardToolErrorContent so the response includes the
standard {success:false, error, guidance, suggestedTool, context}
shape and isError=true. Adds a regression test that exercises the
batchIndex-without-totalBatches case.
- agent-runner: wrap pagination analytics block in try/catch so a dynamic
  import or analytics throw can never block the critical
  commitPlannedEventsStep that follows.
- wizard-tools: align confirm_event_plan chunkSize zod schema with the
  business validator — schema now enforces [MIN_CHUNK_SIZE, MAX_CHUNK_SIZE]
  (5..100) instead of min(1), so the agent gets a clear API contract.
- wizard-tools: propose_event_plan now echoes the resolved (validator-
  compatible) chunkSize unconditionally instead of returning totalEvents
  for non-paginated plans, which could yield 1–4 on tiny projects and be
  rejected by confirm_event_plan.
- event-plan-pagination: export MIN_CHUNK_SIZE / MAX_CHUNK_SIZE so the
  schema can reference the same constants the validator uses.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@kelsonpw kelsonpw force-pushed the kelson/scale-paginate-event-plan branch from d802810 to 6411a90 Compare May 22, 2026 18:08
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants