@papi-ai/server 0.4.0-alpha → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/index.js +353 -134
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -4396,7 +4396,9 @@ function definedEntries(obj) {
4396
4396
  async function ensureSchema(config2) {
4397
4397
  const sql = src_default(config2.connectionString, {
4398
4398
  max: 1,
4399
- connect_timeout: 10
4399
+ connect_timeout: 10,
4400
+ prepare: false
4401
+ // Supabase PgBouncer transaction mode invalidates prepared statements
4400
4402
  });
4401
4403
  try {
4402
4404
  const [result] = await sql`
@@ -4702,8 +4704,10 @@ var init_dist3 = __esm({
4702
4704
  max: config2.maxConnections ?? 10,
4703
4705
  idle_timeout: 20,
4704
4706
  // Release idle connections after 20s
4705
- connect_timeout: 10
4707
+ connect_timeout: 10,
4706
4708
  // Fail fast if DB unreachable
4709
+ prepare: false
4710
+ // Supabase PgBouncer transaction mode invalidates prepared statements
4707
4711
  });
4708
4712
  }
4709
4713
  // -------------------------------------------------------------------------
@@ -4711,12 +4715,12 @@ var init_dist3 = __esm({
4711
4715
  // -------------------------------------------------------------------------
4712
4716
  async createProject(input) {
4713
4717
  const [row] = input.id ? await this.sql`
4714
- INSERT INTO projects (id, slug, name, repo_url, papi_dir)
4715
- VALUES (${input.id}, ${input.slug}, ${input.name}, ${input.repo_url ?? null}, ${input.papi_dir ?? null})
4718
+ INSERT INTO projects (id, slug, name, repo_url, papi_dir, user_id)
4719
+ VALUES (${input.id}, ${input.slug}, ${input.name}, ${input.repo_url ?? null}, ${input.papi_dir ?? null}, ${input.user_id ?? null})
4716
4720
  RETURNING *
4717
4721
  ` : await this.sql`
4718
- INSERT INTO projects (slug, name, repo_url, papi_dir)
4719
- VALUES (${input.slug}, ${input.name}, ${input.repo_url ?? null}, ${input.papi_dir ?? null})
4722
+ INSERT INTO projects (slug, name, repo_url, papi_dir, user_id)
4723
+ VALUES (${input.slug}, ${input.name}, ${input.repo_url ?? null}, ${input.papi_dir ?? null}, ${input.user_id ?? null})
4720
4724
  RETURNING *
4721
4725
  `;
4722
4726
  return row;
@@ -5224,6 +5228,7 @@ CREATE TABLE IF NOT EXISTS projects (
5224
5228
  name TEXT NOT NULL,
5225
5229
  repo_url TEXT,
5226
5230
  papi_dir TEXT,
5231
+ user_id UUID,
5227
5232
  created_at TIMESTAMPTZ DEFAULT now() NOT NULL,
5228
5233
  updated_at TIMESTAMPTZ DEFAULT now() NOT NULL,
5229
5234
  site_url TEXT,
@@ -5630,6 +5635,7 @@ CREATE TABLE IF NOT EXISTS entity_references (
5630
5635
  CREATE TABLE IF NOT EXISTS dogfood_log (
5631
5636
  id UUID DEFAULT gen_random_uuid() NOT NULL,
5632
5637
  project_id UUID NOT NULL REFERENCES projects(id),
5638
+ user_id UUID,
5633
5639
  cycle_number INTEGER NOT NULL,
5634
5640
  category TEXT NOT NULL,
5635
5641
  content TEXT NOT NULL,
@@ -5654,6 +5660,7 @@ CREATE TABLE IF NOT EXISTS task_comments (
5654
5660
  CREATE TABLE IF NOT EXISTS task_transitions (
5655
5661
  id UUID DEFAULT gen_random_uuid() NOT NULL,
5656
5662
  project_id UUID NOT NULL REFERENCES projects(id),
5663
+ user_id UUID,
5657
5664
  task_id VARCHAR(50) NOT NULL,
5658
5665
  from_status VARCHAR(50) NOT NULL,
5659
5666
  to_status VARCHAR(50) NOT NULL,
@@ -5962,6 +5969,10 @@ EXCEPTION WHEN duplicate_object THEN NULL; END $$;
5962
5969
  max: 5,
5963
5970
  idle_timeout: 20,
5964
5971
  connect_timeout: 10,
5972
+ // Supabase uses PgBouncer in transaction mode — prepared statements are
5973
+ // bound to backend connections and become invalid when PgBouncer reassigns.
5974
+ // This caused "prepared statement does not exist" errors on strategy_review.
5975
+ prepare: false,
5965
5976
  connection: {
5966
5977
  // 30s statement timeout prevents hung queries from blocking the server
5967
5978
  statement_timeout: 3e4
@@ -6077,20 +6088,23 @@ EXCEPTION WHEN duplicate_object THEN NULL; END $$;
6077
6088
  }));
6078
6089
  }
6079
6090
  async getCycleHealth() {
6080
- const cycles = await this.sql`
6081
- SELECT number, status FROM cycles
6082
- WHERE project_id = ${this.projectId}
6083
- ORDER BY number DESC
6084
- `;
6085
- const totalCycles = cycles.length > 0 ? cycles[0].number : 0;
6086
- const latestCycleStatus = cycles.length > 0 ? cycles[0].status : void 0;
6087
- const [lastStrategy] = await this.sql`
6088
- SELECT cycle_number, board_health, strategic_direction FROM strategy_reviews
6089
- WHERE project_id = ${this.projectId}
6090
- ORDER BY cycle_number DESC
6091
- LIMIT 1
6091
+ const [row] = await this.sql`
6092
+ SELECT
6093
+ COALESCE((SELECT number FROM cycles WHERE project_id = ${this.projectId} ORDER BY number DESC LIMIT 1), 0) AS total_cycles,
6094
+ (SELECT status FROM cycles WHERE project_id = ${this.projectId} ORDER BY number DESC LIMIT 1) AS latest_status,
6095
+ sr.cycle_number AS last_review_cycle,
6096
+ sr.board_health,
6097
+ sr.strategic_direction
6098
+ FROM (SELECT 1) AS _dummy
6099
+ LEFT JOIN LATERAL (
6100
+ SELECT cycle_number, board_health, strategic_direction
6101
+ FROM strategy_reviews WHERE project_id = ${this.projectId}
6102
+ ORDER BY cycle_number DESC LIMIT 1
6103
+ ) sr ON true
6092
6104
  `;
6093
- const lastStrategyReviewCycle = lastStrategy?.cycle_number ?? 0;
6105
+ const totalCycles = row?.total_cycles ?? 0;
6106
+ const latestCycleStatus = row?.latest_status ?? void 0;
6107
+ const lastStrategyReviewCycle = row?.last_review_cycle ?? 0;
6094
6108
  const gap = totalCycles - lastStrategyReviewCycle;
6095
6109
  const strategyReviewDue = gap >= 5 ? `Cycle ${totalCycles}` : `Cycle ${lastStrategyReviewCycle + 5}`;
6096
6110
  return {
@@ -6098,8 +6112,8 @@ EXCEPTION WHEN duplicate_object THEN NULL; END $$;
6098
6112
  latestCycleStatus,
6099
6113
  cyclesSinceLastStrategyReview: gap,
6100
6114
  strategyReviewDue,
6101
- boardHealth: lastStrategy?.board_health ?? "",
6102
- strategicDirection: lastStrategy?.strategic_direction ?? "",
6115
+ boardHealth: row?.board_health ?? "",
6116
+ strategicDirection: row?.strategic_direction ?? "",
6103
6117
  lastFullMode: 0
6104
6118
  };
6105
6119
  }
@@ -6271,20 +6285,16 @@ ${newParts.join("\n")}` : newParts.join("\n");
6271
6285
  }
6272
6286
  async writeDogfoodEntries(entries) {
6273
6287
  if (entries.length === 0) return;
6274
- for (const entry of entries) {
6275
- await this.sql`
6276
- INSERT INTO dogfood_log (project_id, cycle_number, category, content, source_tool, status, linked_task_id)
6277
- VALUES (
6278
- ${this.projectId},
6279
- ${entry.cycleNumber},
6280
- ${entry.category},
6281
- ${entry.content},
6282
- ${entry.sourceTool ?? "strategy_review"},
6283
- ${entry.status ?? "observed"},
6284
- ${entry.linkedTaskId ?? null}
6285
- )
6286
- `;
6287
- }
6288
+ const values2 = entries.map((entry) => ({
6289
+ project_id: this.projectId,
6290
+ cycle_number: entry.cycleNumber,
6291
+ category: entry.category,
6292
+ content: entry.content,
6293
+ source_tool: entry.sourceTool ?? "strategy_review",
6294
+ status: entry.status ?? "observed",
6295
+ linked_task_id: entry.linkedTaskId ?? null
6296
+ }));
6297
+ await this.sql`INSERT INTO dogfood_log ${this.sql(values2)}`;
6288
6298
  }
6289
6299
  async getDogfoodLog(limit = 10) {
6290
6300
  const rows = await this.sql`
@@ -6708,24 +6718,41 @@ ${newParts.join("\n")}` : newParts.join("\n");
6708
6718
  return rows.map(rowToPhase);
6709
6719
  }
6710
6720
  async writePhases(phases) {
6711
- for (const phase of phases) {
6712
- if (phase.stageId) {
6713
- await this.sql`
6714
- INSERT INTO phases (project_id, slug, label, description, status, sort_order, stage_id)
6715
- VALUES (${this.projectId}, ${phase.slug}, ${phase.label}, ${phase.description}, ${phase.status}, ${phase.order}, ${phase.stageId})
6716
- ON CONFLICT (project_id, slug)
6717
- DO UPDATE SET label = ${phase.label}, description = ${phase.description},
6718
- status = ${phase.status}, sort_order = ${phase.order}, stage_id = ${phase.stageId}
6719
- `;
6720
- } else {
6721
- await this.sql`
6722
- INSERT INTO phases (project_id, slug, label, description, status, sort_order)
6723
- VALUES (${this.projectId}, ${phase.slug}, ${phase.label}, ${phase.description}, ${phase.status}, ${phase.order})
6724
- ON CONFLICT (project_id, slug)
6725
- DO UPDATE SET label = ${phase.label}, description = ${phase.description},
6726
- status = ${phase.status}, sort_order = ${phase.order}
6727
- `;
6728
- }
6721
+ if (phases.length === 0) return;
6722
+ const withStage = phases.filter((p) => p.stageId);
6723
+ const withoutStage = phases.filter((p) => !p.stageId);
6724
+ if (withStage.length > 0) {
6725
+ const values2 = withStage.map((p) => ({
6726
+ project_id: this.projectId,
6727
+ slug: p.slug,
6728
+ label: p.label,
6729
+ description: p.description,
6730
+ status: p.status,
6731
+ sort_order: p.order,
6732
+ stage_id: p.stageId
6733
+ }));
6734
+ await this.sql`
6735
+ INSERT INTO phases ${this.sql(values2)}
6736
+ ON CONFLICT (project_id, slug)
6737
+ DO UPDATE SET label = EXCLUDED.label, description = EXCLUDED.description,
6738
+ status = EXCLUDED.status, sort_order = EXCLUDED.sort_order, stage_id = EXCLUDED.stage_id
6739
+ `;
6740
+ }
6741
+ if (withoutStage.length > 0) {
6742
+ const values2 = withoutStage.map((p) => ({
6743
+ project_id: this.projectId,
6744
+ slug: p.slug,
6745
+ label: p.label,
6746
+ description: p.description,
6747
+ status: p.status,
6748
+ sort_order: p.order
6749
+ }));
6750
+ await this.sql`
6751
+ INSERT INTO phases ${this.sql(values2)}
6752
+ ON CONFLICT (project_id, slug)
6753
+ DO UPDATE SET label = EXCLUDED.label, description = EXCLUDED.description,
6754
+ status = EXCLUDED.status, sort_order = EXCLUDED.sort_order
6755
+ `;
6729
6756
  }
6730
6757
  }
6731
6758
  // -------------------------------------------------------------------------
@@ -7291,17 +7318,15 @@ ${r.content}` + (r.carry_forward ? `
7291
7318
  // -------------------------------------------------------------------------
7292
7319
  async logEntityReferences(refs) {
7293
7320
  if (refs.length === 0) return;
7294
- for (const ref of refs) {
7295
- await this.sql`
7296
- INSERT INTO entity_references (
7297
- project_id, entity_type, entity_id, reference_context,
7298
- tool_name, cycle_number
7299
- ) VALUES (
7300
- ${this.projectId}, ${ref.entityType}, ${ref.entityId},
7301
- ${ref.referenceContext}, ${ref.toolName}, ${ref.cycleNumber}
7302
- )
7303
- `;
7304
- }
7321
+ const values2 = refs.map((ref) => ({
7322
+ project_id: this.projectId,
7323
+ entity_type: ref.entityType,
7324
+ entity_id: ref.entityId,
7325
+ reference_context: ref.referenceContext,
7326
+ tool_name: ref.toolName,
7327
+ cycle_number: ref.cycleNumber
7328
+ }));
7329
+ await this.sql`INSERT INTO entity_references ${this.sql(values2)}`;
7305
7330
  }
7306
7331
  async getDecisionUsage(currentSprint) {
7307
7332
  const rows = await this.sql`
@@ -8035,7 +8060,8 @@ async function createAdapter(optionsOrType, maybePapiDir) {
8035
8060
  if (!existing) {
8036
8061
  const projectRoot = options.projectRoot ?? process.env["PAPI_PROJECT_DIR"] ?? "";
8037
8062
  const slug = path2.basename(projectRoot) || "unnamed";
8038
- await pgAdapter.createProject({ id: projectId, slug, name: slug, papi_dir: papiDir });
8063
+ const userId = process.env["PAPI_USER_ID"] ?? void 0;
8064
+ await pgAdapter.createProject({ id: projectId, slug, name: slug, papi_dir: papiDir, user_id: userId });
8039
8065
  }
8040
8066
  await pgAdapter.close();
8041
8067
  } catch {
@@ -8130,15 +8156,25 @@ function formatActiveDecisionsForReview(decisions) {
8130
8156
  if (decisions.length === 0) return "No active decisions.";
8131
8157
  return decisions.map((d) => {
8132
8158
  const lifecycle = d.outcome && d.outcome !== "pending" ? ` | Outcome: ${d.outcome}` + (d.revisionCount ? ` | ${d.revisionCount} revision(s)` : "") : "";
8159
+ const summary = extractDecisionSummary(d.body);
8160
+ const summaryLine = summary ? `
8161
+ Summary: ${summary}` : "";
8133
8162
  if (d.superseded) {
8134
- return `### ${d.id}: ${d.title} [SUPERSEDED by ${d.supersededBy}${lifecycle}]
8135
-
8136
- ${d.body}`;
8163
+ return `- ${d.id}: ${d.title} [SUPERSEDED by ${d.supersededBy}${lifecycle}]${summaryLine}`;
8137
8164
  }
8138
- return `### ${d.id}: ${d.title} [Confidence: ${d.confidence}${lifecycle}]
8139
-
8140
- ${d.body}`;
8141
- }).join("\n\n");
8165
+ return `- ${d.id}: ${d.title} [${d.confidence}${lifecycle}]${summaryLine}`;
8166
+ }).join("\n");
8167
+ }
8168
+ function extractDecisionSummary(body) {
8169
+ if (!body) return "";
8170
+ const lines = body.split("\n");
8171
+ for (const line of lines) {
8172
+ const trimmed = line.trim();
8173
+ if (!trimmed || trimmed.startsWith("#") || trimmed.startsWith("---")) continue;
8174
+ const clean = trimmed.replace(/\*\*/g, "").replace(/\*/g, "");
8175
+ return clean.length > 150 ? clean.slice(0, 147) + "..." : clean;
8176
+ }
8177
+ return "";
8142
8178
  }
8143
8179
  function formatDecisionLifecycleSummary(decisions) {
8144
8180
  const active = decisions.filter((d) => !d.superseded);
@@ -9858,7 +9894,8 @@ Start with a level-2 heading for each conventions section. Example sections:
9858
9894
  - If the project type implies a tech stack (e.g. mobile-app \u2192 React Native/Flutter), suggest conventions for the most likely stack
9859
9895
  - Include build & test commands if inferrable (e.g. "npm test", "pytest", "cargo test")
9860
9896
  - Do NOT repeat the workflow or documentation maintenance sections \u2014 those are already included
9861
- - Do NOT include a top-level heading \u2014 the file already has one`;
9897
+ - Do NOT include a top-level heading \u2014 the file already has one
9898
+ - Do NOT include a dogfood logging section \u2014 that is added separately during setup`;
9862
9899
  function buildConventionsPrompt(ctx) {
9863
9900
  const parts = [
9864
9901
  "Generate coding conventions for this project's CLAUDE.md file.",
@@ -11202,7 +11239,7 @@ ${result.userMessage}
11202
11239
 
11203
11240
  // src/services/strategy.ts
11204
11241
  init_dist2();
11205
- import { randomUUID as randomUUID8 } from "crypto";
11242
+ import { randomUUID as randomUUID8, createHash as createHash2 } from "crypto";
11206
11243
 
11207
11244
  // src/lib/phase-realign.ts
11208
11245
  function extractPhaseNumber(phaseField) {
@@ -11426,16 +11463,33 @@ function formatPreviousReviews(reviews) {
11426
11463
  if (reviews.length === 0) return void 0;
11427
11464
  return reviews.map((r) => {
11428
11465
  const range = r.cycleRange ? `Cycles ${r.cycleRange}` : `Cycle ${r.cycleNumber}`;
11429
- const parts = [`#### ${range} \u2014 ${r.title}`];
11466
+ const parts = [`- **${range} \u2014 ${r.title}**`];
11430
11467
  const meta = [];
11431
- if (r.boardHealth) meta.push(`**Board Health:** ${r.boardHealth}`);
11432
- if (r.strategicDirection) meta.push(`**Strategic Direction:** ${r.strategicDirection}`);
11433
- if (r.velocityAssessment) meta.push(`**Velocity:** ${r.velocityAssessment}`);
11434
- if (meta.length > 0) parts.push("", meta.join("\n"));
11435
- const content = r.content.replace(/^###\s+.*\n\n/m, "");
11436
- parts.push("", content);
11468
+ if (r.strategicDirection) meta.push(`Direction: ${r.strategicDirection}`);
11469
+ if (r.boardHealth) meta.push(`Board: ${r.boardHealth}`);
11470
+ if (r.velocityAssessment) meta.push(`Velocity: ${r.velocityAssessment}`);
11471
+ if (meta.length > 0) parts.push(` ${meta.join(" | ")}`);
11472
+ const topRec = extractTopRecommendation(r.content);
11473
+ if (topRec) parts.push(` Top rec: ${topRec}`);
11437
11474
  return parts.join("\n");
11438
- }).join("\n\n---\n\n");
11475
+ }).join("\n");
11476
+ }
11477
+ function extractTopRecommendation(content) {
11478
+ if (!content) return void 0;
11479
+ const lines = content.split("\n");
11480
+ let inRecommendations = false;
11481
+ for (const line of lines) {
11482
+ const trimmed = line.trim();
11483
+ if (/recommend/i.test(trimmed) && (trimmed.startsWith("#") || trimmed.startsWith("**"))) {
11484
+ inRecommendations = true;
11485
+ continue;
11486
+ }
11487
+ if (inRecommendations && (trimmed.startsWith("-") || trimmed.startsWith("1"))) {
11488
+ const clean = trimmed.replace(/^[-\d.)\s]+/, "").replace(/\*\*/g, "");
11489
+ return clean.length > 150 ? clean.slice(0, 147) + "..." : clean;
11490
+ }
11491
+ }
11492
+ return void 0;
11439
11493
  }
11440
11494
  function capProductBrief2(brief, maxChars = 2e3) {
11441
11495
  if (brief.length <= maxChars) return brief;
@@ -11462,7 +11516,16 @@ function formatBoardForReviewSmart(tasks, lastReviewCycle) {
11462
11516
  parts.push(`
11463
11517
  ### Completed Since Last Review \u2014 Cycle ${lastReviewCycle}+ (${recentDone.length})
11464
11518
  `);
11465
- parts.push(...recentDone.map((t) => formatTaskCompact(t)));
11519
+ const byModule = /* @__PURE__ */ new Map();
11520
+ for (const t of recentDone) {
11521
+ const mod = t.module || "Core";
11522
+ if (!byModule.has(mod)) byModule.set(mod, []);
11523
+ byModule.get(mod).push(t);
11524
+ }
11525
+ for (const [mod, modTasks] of byModule) {
11526
+ const titles = modTasks.map((t) => `${t.id}: ${t.title}`).join(", ");
11527
+ parts.push(`**${mod}** (${modTasks.length}): ${titles}`);
11528
+ }
11466
11529
  }
11467
11530
  if (olderDone.length > 0) {
11468
11531
  const byPhase = /* @__PURE__ */ new Map();
@@ -11495,22 +11558,41 @@ function formatTaskCompact(t) {
11495
11558
  }
11496
11559
  async function assembleContext2(adapter2, cycleNumber, cyclesSinceLastReview, projectRoot) {
11497
11560
  const lastReviewCycleNum = cycleNumber - cyclesSinceLastReview;
11498
- const [productBrief, decisions, reports, log, activeTasks, recentDoneTasks, reviews, dogfoodLog, previousStrategyReviews, currentNorthStar] = await Promise.all([
11561
+ const [
11562
+ productBrief,
11563
+ decisions,
11564
+ reports,
11565
+ log,
11566
+ activeTasks,
11567
+ recentDoneTasks,
11568
+ reviews,
11569
+ dogfoodLog,
11570
+ previousStrategyReviews,
11571
+ currentNorthStar,
11572
+ canvas,
11573
+ decisionUsage,
11574
+ recData
11575
+ ] = await Promise.all([
11499
11576
  adapter2.readProductBrief(),
11500
11577
  adapter2.getActiveDecisions(),
11501
11578
  adapter2.getBuildReportsSince(lastReviewCycleNum),
11502
11579
  adapter2.getCycleLogSince(lastReviewCycleNum),
11503
11580
  adapter2.queryBoard({
11504
- status: ["Backlog", "In Cycle", "In Progress", "In Review", "Blocked", "Deferred"]
11581
+ status: ["Backlog", "In Cycle", "In Progress", "In Review", "Blocked"]
11582
+ // Deferred tasks excluded — they're intentionally deprioritised and add context noise
11505
11583
  }),
11506
11584
  adapter2.queryBoard({
11507
11585
  status: ["Done"],
11508
11586
  cycleSince: lastReviewCycleNum
11509
11587
  }),
11510
- adapter2.getRecentReviews(20),
11588
+ adapter2.getRecentReviews(5),
11511
11589
  readDogfoodEntries(projectRoot, 10, adapter2),
11512
11590
  adapter2.getStrategyReviews(3),
11513
- adapter2.getCurrentNorthStar?.() ?? Promise.resolve(null)
11591
+ adapter2.getCurrentNorthStar?.() ?? Promise.resolve(null),
11592
+ // Previously sequential — now parallel
11593
+ adapter2.readDiscoveryCanvas().catch(() => ({})),
11594
+ adapter2.getDecisionUsage(cycleNumber).catch(() => []),
11595
+ adapter2.getRecommendationEffectiveness?.()?.catch(() => []) ?? Promise.resolve([])
11514
11596
  ]);
11515
11597
  const tasks = [...activeTasks, ...recentDoneTasks];
11516
11598
  const recentLog = log;
@@ -11544,8 +11626,17 @@ async function assembleContext2(adapter2, cycleNumber, cyclesSinceLastReview, pr
11544
11626
  ]);
11545
11627
  let discoveryCanvasText;
11546
11628
  try {
11547
- const canvas = await adapter2.readDiscoveryCanvas();
11548
- discoveryCanvasText = formatDiscoveryCanvasForReview(canvas);
11629
+ const fullCanvasText = formatDiscoveryCanvasForReview(canvas);
11630
+ if (fullCanvasText) {
11631
+ const canvasHash = createHash2("md5").update(fullCanvasText).digest("hex");
11632
+ const lastReview = previousStrategyReviews?.[0];
11633
+ const prevHash = lastReview?.structuredData?.canvasHash;
11634
+ if (prevHash && prevHash === canvasHash) {
11635
+ discoveryCanvasText = "Discovery Canvas: No changes since last review.";
11636
+ } else {
11637
+ discoveryCanvasText = fullCanvasText;
11638
+ }
11639
+ }
11549
11640
  } catch {
11550
11641
  }
11551
11642
  const briefImplicationsFromBuilds = reports.filter((r) => Array.isArray(r.briefImplications) && r.briefImplications.length > 0).flatMap((r) => r.briefImplications.map((bi) => ({ ...bi, taskName: r.taskName, cycle: r.cycle })));
@@ -11555,7 +11646,7 @@ async function assembleContext2(adapter2, cycleNumber, cyclesSinceLastReview, pr
11555
11646
  }
11556
11647
  let phasesText;
11557
11648
  try {
11558
- phasesText = await formatHierarchyForReview(adapter2, cycleNumber);
11649
+ phasesText = await formatHierarchyForReview(adapter2, cycleNumber, tasks);
11559
11650
  if (!phasesText) {
11560
11651
  const phases = await adapter2.readPhases();
11561
11652
  phasesText = formatPhasesForReview(phases, cycleNumber);
@@ -11564,13 +11655,11 @@ async function assembleContext2(adapter2, cycleNumber, cyclesSinceLastReview, pr
11564
11655
  }
11565
11656
  let decisionUsageText;
11566
11657
  try {
11567
- const usage = await adapter2.getDecisionUsage(cycleNumber);
11568
- decisionUsageText = formatDecisionUsageForReview(usage);
11658
+ decisionUsageText = formatDecisionUsageForReview(decisionUsage);
11569
11659
  } catch {
11570
11660
  }
11571
11661
  let recEffectivenessText;
11572
11662
  try {
11573
- const recData = await adapter2.getRecommendationEffectiveness?.();
11574
11663
  if (recData && recData.length > 0) {
11575
11664
  recEffectivenessText = "| Type | Total | Actioned | Pending | Acceptance Rate | Avg Cycles to Action |\n|------|-------|----------|---------|-----------------|---------------------|\n" + recData.map(
11576
11665
  (r) => `| ${r.type} | ${r.total} | ${r.actioned} | ${r.pending} | ${r.acceptanceRate}% | ${r.avgCyclesToAction ?? "N/A"} |`
@@ -11678,7 +11767,18 @@ ${cleanContent}`;
11678
11767
  strategicDirection: data.strategicDirection,
11679
11768
  fullAnalysis: fullAnalysis ?? void 0,
11680
11769
  velocityAssessment: data.velocityAssessment ?? void 0,
11681
- structuredData: data
11770
+ structuredData: await (async () => {
11771
+ const sd = data;
11772
+ try {
11773
+ const currentCanvas = await adapter2.readDiscoveryCanvas();
11774
+ const canvasText = formatDiscoveryCanvasForReview(currentCanvas);
11775
+ if (canvasText) {
11776
+ return { ...sd, canvasHash: createHash2("md5").update(canvasText).digest("hex") };
11777
+ }
11778
+ } catch {
11779
+ }
11780
+ return sd;
11781
+ })()
11682
11782
  });
11683
11783
  await adapter2.setCycleHealth({
11684
11784
  cyclesSinceLastStrategyReview: 0,
@@ -11702,7 +11802,7 @@ ${cleanContent}`;
11702
11802
  } catch {
11703
11803
  }
11704
11804
  if (data.activeDecisionUpdates && data.activeDecisionUpdates.length > 0) {
11705
- for (const ad of data.activeDecisionUpdates) {
11805
+ await Promise.all(data.activeDecisionUpdates.map(async (ad) => {
11706
11806
  if (ad.action === "delete" && adapter2.deleteActiveDecision) {
11707
11807
  await adapter2.deleteActiveDecision(ad.id);
11708
11808
  } else {
@@ -11720,10 +11820,10 @@ ${cleanContent}`;
11720
11820
  });
11721
11821
  } catch {
11722
11822
  }
11723
- }
11823
+ }));
11724
11824
  }
11725
11825
  if (data.decisionScores && data.decisionScores.length > 0) {
11726
- for (const score of data.decisionScores) {
11826
+ await Promise.all(data.decisionScores.map(async (score) => {
11727
11827
  try {
11728
11828
  await adapter2.writeDecisionScore({
11729
11829
  decisionId: score.id,
@@ -11745,7 +11845,7 @@ ${cleanContent}`;
11745
11845
  });
11746
11846
  } catch {
11747
11847
  }
11748
- }
11848
+ }));
11749
11849
  }
11750
11850
  if (data.productBriefUpdates) {
11751
11851
  try {
@@ -11762,8 +11862,8 @@ ${cleanContent}`;
11762
11862
  }
11763
11863
  try {
11764
11864
  const recs = extractRecommendations(data, cycleNumber);
11765
- for (const rec of recs) {
11766
- await adapter2.writeRecommendation(rec);
11865
+ if (recs.length > 0) {
11866
+ await Promise.all(recs.map((rec) => adapter2.writeRecommendation(rec)));
11767
11867
  }
11768
11868
  } catch {
11769
11869
  }
@@ -12012,20 +12112,22 @@ function formatPhasesForReview(phases, currentCycle) {
12012
12112
  lines.push(`_Current cycle: ${currentCycle}. Use phase status + board task distribution to detect staleness._`);
12013
12113
  return lines.join("\n");
12014
12114
  }
12015
- async function formatHierarchyForReview(adapter2, currentCycle) {
12115
+ async function formatHierarchyForReview(adapter2, currentCycle, prefetchedTasks) {
12016
12116
  let horizons = [];
12017
12117
  let stages = [];
12018
12118
  let phases = [];
12019
12119
  try {
12020
- horizons = await adapter2.readHorizons();
12021
- stages = await adapter2.readStages();
12022
- phases = await adapter2.readPhases();
12120
+ [horizons, stages, phases] = await Promise.all([
12121
+ adapter2.readHorizons(),
12122
+ adapter2.readStages(),
12123
+ adapter2.readPhases()
12124
+ ]);
12023
12125
  } catch {
12024
12126
  }
12025
12127
  if (horizons.length === 0 && phases.length === 0) return void 0;
12026
12128
  let tasksByPhase = /* @__PURE__ */ new Map();
12027
12129
  try {
12028
- const allTasks = await adapter2.queryBoard();
12130
+ const allTasks = prefetchedTasks ?? await adapter2.queryBoard();
12029
12131
  for (const t of allTasks) {
12030
12132
  const phase = t.phase || "Unscoped";
12031
12133
  const existing = tasksByPhase.get(phase) || { total: 0, done: 0, backlog: 0, inProgress: 0 };
@@ -13604,6 +13706,41 @@ async function applySetup(adapter2, config2, input, briefText, adSeedText, conve
13604
13706
  } catch {
13605
13707
  }
13606
13708
  }
13709
+ try {
13710
+ const claudeMdPath = join2(config2.projectRoot, "CLAUDE.md");
13711
+ const existing = await readFile3(claudeMdPath, "utf-8");
13712
+ if (!existing.includes("Dogfood Logging")) {
13713
+ const dogfoodSection = [
13714
+ "",
13715
+ "## Dogfood Logging",
13716
+ "",
13717
+ "After each `release`, append a dogfood entry capturing observations from the cycle.",
13718
+ "Call the adapter method with structured entries for each observation:",
13719
+ "",
13720
+ "- **friction** \u2014 workflow pain points, confusing flows, things that broke or slowed you down",
13721
+ "- **methodology** \u2014 what worked or didn't in the plan/build/review cycle",
13722
+ "- **signal** \u2014 indicators of product-market fit, user value, or growth potential",
13723
+ "- **commercial** \u2014 cost, pricing, or business model observations",
13724
+ "",
13725
+ "This is autonomous plumbing \u2014 log observations after release without asking.",
13726
+ ""
13727
+ ].join("\n");
13728
+ await writeFile2(claudeMdPath, existing + dogfoodSection, "utf-8");
13729
+ }
13730
+ } catch {
13731
+ }
13732
+ if (adapter2.writeDogfoodEntries) {
13733
+ try {
13734
+ await adapter2.writeDogfoodEntries([{
13735
+ cycleNumber: 0,
13736
+ category: "signal",
13737
+ content: "Project setup completed \u2014 PAPI is configured and ready for first plan.",
13738
+ sourceTool: "setup",
13739
+ status: "observed"
13740
+ }]);
13741
+ } catch {
13742
+ }
13743
+ }
13607
13744
  try {
13608
13745
  await adapter2.appendToolMetric({
13609
13746
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
@@ -15093,13 +15230,9 @@ async function recordAdHoc(adapter2, input) {
15093
15230
  if (!existing) {
15094
15231
  throw new Error(`Task "${input.taskId}" not found on the board. Check the task ID and try again.`);
15095
15232
  }
15096
- if (existing.status === "Done") {
15097
- throw new Error(`Task "${input.taskId}" (${existing.title}) is already Done.`);
15098
- }
15099
15233
  await adapter2.updateTask(input.taskId, {
15100
- status: "Done",
15101
15234
  notes: existing.notes ? `${existing.notes}
15102
- [ad-hoc] ${input.notes || "Completed via ad_hoc"}` : `[ad-hoc] ${input.notes || "Completed via ad_hoc"}`
15235
+ [ad-hoc] ${input.notes || "Work recorded via ad_hoc"}` : `[ad-hoc] ${input.notes || "Work recorded via ad_hoc"}`
15103
15236
  });
15104
15237
  task = await adapter2.getTask(input.taskId);
15105
15238
  } else {
@@ -15144,7 +15277,7 @@ async function recordAdHoc(adapter2, input) {
15144
15277
  var VALID_EFFORTS = ["XS", "S", "M", "L", "XL"];
15145
15278
  var adHocTool = {
15146
15279
  name: "ad_hoc",
15147
- description: "Record work done outside the normal cycle. Creates a Done task with a lightweight build report, or completes an existing task if task_id is provided. Use for quick fixes, bug patches, or ad-hoc changes. Use task_id to complete an existing board task instead of creating a new one. Does not call the Anthropic API.",
15280
+ description: "Record work done outside the normal cycle. Creates a Done task with a lightweight build report, or associates work with an existing task if task_id is provided (without changing task status \u2014 use build_execute for status transitions). Use for quick fixes, bug patches, or ad-hoc changes. Does not call the Anthropic API.",
15148
15281
  inputSchema: {
15149
15282
  type: "object",
15150
15283
  properties: {
@@ -15154,7 +15287,7 @@ var adHocTool = {
15154
15287
  },
15155
15288
  task_id: {
15156
15289
  type: "string",
15157
- description: 'Existing task ID to complete (e.g. "task-42"). When provided, marks the existing task as Done and attaches a build report instead of creating a new task.'
15290
+ description: 'Existing task ID to associate this work with (e.g. "task-42"). When provided, appends notes and attaches a build report to the existing task without changing its status.'
15158
15291
  },
15159
15292
  notes: {
15160
15293
  type: "string",
@@ -15214,7 +15347,7 @@ async function handleAdHoc(adapter2, config2, args) {
15214
15347
  }
15215
15348
  const truncateWarning = notesTruncated ? ` (notes truncated to ${MAX_NOTES_LENGTH} chars)` : "";
15216
15349
  return textResponse(
15217
- `**${result.task.id}:** "${result.task.title}" \u2014 recorded as ad-hoc (${effortRaw}). Task marked Done with build report.${truncateWarning}`
15350
+ `**${result.task.id}:** "${result.task.title}" \u2014 recorded as ad-hoc (${effortRaw}). Build report attached.${truncateWarning}`
15218
15351
  );
15219
15352
  }
15220
15353
 
@@ -15908,8 +16041,8 @@ async function createRelease(config2, branch, version, adapter2) {
15908
16041
  const warnings = [];
15909
16042
  if (adapter2) {
15910
16043
  try {
15911
- const health = await adapter2.getCycleHealth();
15912
- const currentCycle = health.totalCycles;
16044
+ const versionMatch = version.match(/^v0\.(\d+)\./);
16045
+ const currentCycle = versionMatch ? parseInt(versionMatch[1], 10) : 0;
15913
16046
  if (currentCycle > 0) {
15914
16047
  await adapter2.createCycle({
15915
16048
  id: `cycle-${currentCycle}`,
@@ -16029,6 +16162,10 @@ async function handleRelease(adapter2, config2, args) {
16029
16162
  }
16030
16163
  }
16031
16164
 
16165
+ // src/tools/review.ts
16166
+ import { existsSync } from "fs";
16167
+ import { join as join4 } from "path";
16168
+
16032
16169
  // src/services/review.ts
16033
16170
  init_dist2();
16034
16171
  import { randomUUID as randomUUID13 } from "crypto";
@@ -16075,11 +16212,15 @@ async function submitReview(adapter2, input) {
16075
16212
  throw new Error(`Task ${input.taskId} not found.`);
16076
16213
  }
16077
16214
  let cycle;
16078
- try {
16079
- const health = await adapter2.getCycleHealth();
16080
- cycle = health.totalCycles;
16081
- } catch {
16082
- cycle = 0;
16215
+ if (task.cycle && task.cycle > 0) {
16216
+ cycle = task.cycle;
16217
+ } else {
16218
+ try {
16219
+ const health = await adapter2.getCycleHealth();
16220
+ cycle = health.totalCycles;
16221
+ } catch {
16222
+ cycle = 0;
16223
+ }
16083
16224
  }
16084
16225
  const date = (/* @__PURE__ */ new Date()).toISOString();
16085
16226
  const review = {
@@ -16261,21 +16402,24 @@ function mergeAfterAccept(config2, taskId) {
16261
16402
  }
16262
16403
  const featureBranch = taskBranchName(taskId);
16263
16404
  const baseBranch = resolveBaseBranch(config2.projectRoot, config2.baseBranch);
16264
- try {
16265
- const commitResult = stageDirAndCommit(
16266
- config2.projectRoot,
16267
- ".papi",
16268
- `chore(${taskId}): record build-acceptance review`
16269
- );
16270
- if (commitResult.committed) {
16271
- const push = gitPush(config2.projectRoot, featureBranch);
16272
- if (!push.success) {
16273
- lines.push(`Push failed: ${push.message}`);
16274
- return lines;
16405
+ const papiDir = join4(config2.projectRoot, ".papi");
16406
+ if (existsSync(papiDir)) {
16407
+ try {
16408
+ const commitResult = stageDirAndCommit(
16409
+ config2.projectRoot,
16410
+ ".papi",
16411
+ `chore(${taskId}): record build-acceptance review`
16412
+ );
16413
+ if (commitResult.committed) {
16414
+ const push = gitPush(config2.projectRoot, featureBranch);
16415
+ if (!push.success) {
16416
+ lines.push(`Push failed: ${push.message}`);
16417
+ return lines;
16418
+ }
16275
16419
  }
16420
+ } catch (err) {
16421
+ lines.push(`Pre-merge commit failed: ${err instanceof Error ? err.message : String(err)}`);
16276
16422
  }
16277
- } catch (err) {
16278
- lines.push(`Pre-merge commit failed: ${err instanceof Error ? err.message : String(err)}`);
16279
16423
  }
16280
16424
  const merge = mergePullRequest(config2.projectRoot, featureBranch);
16281
16425
  if (!merge.success) {
@@ -16283,6 +16427,20 @@ function mergeAfterAccept(config2, taskId) {
16283
16427
  return lines;
16284
16428
  }
16285
16429
  lines.push(merge.message);
16430
+ if (hasUncommittedChanges(config2.projectRoot, AUTO_WRITTEN_PATHS)) {
16431
+ try {
16432
+ const cleanup = stageDirAndCommit(
16433
+ config2.projectRoot,
16434
+ ".",
16435
+ `chore(${taskId}): commit remaining changes before branch switch`
16436
+ );
16437
+ if (cleanup.committed) {
16438
+ lines.push("Committed uncommitted changes before switching branches.");
16439
+ }
16440
+ } catch {
16441
+ lines.push("Warning: uncommitted changes detected \u2014 branch switch may fail.");
16442
+ }
16443
+ }
16286
16444
  const checkout = checkoutBranch(config2.projectRoot, baseBranch);
16287
16445
  if (checkout.success) {
16288
16446
  const pull = gitPull(config2.projectRoot);
@@ -16292,7 +16450,7 @@ function mergeAfterAccept(config2, taskId) {
16292
16450
  lines.push(del.message);
16293
16451
  }
16294
16452
  } else {
16295
- lines.push(`Could not switch to '${baseBranch}': ${checkout.message}`);
16453
+ lines.push(`Could not switch to '${baseBranch}': ${checkout.message}. You may need to manually run: git checkout ${baseBranch}`);
16296
16454
  }
16297
16455
  return lines;
16298
16456
  }
@@ -17153,6 +17311,50 @@ ${result.userMessage}
17153
17311
  }
17154
17312
  }
17155
17313
 
17314
+ // src/lib/telemetry.ts
17315
+ var TELEMETRY_SUPABASE_URL = "https://guewgygcpcmrcoppihzx.supabase.co";
17316
+ var TELEMETRY_API_KEY = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Imd1ZXdneWdjcGNtcmNvcHBpaHp4Iiwicm9sZSI6ImFub24iLCJpYXQiOjE3NzI2Njk2NTMsImV4cCI6MjA4ODI0NTY1M30.V5Jw7wJgiMpSQPa2mt0ftjyye5ynG1qLlam00yPVNJY";
17317
+ function isEnabled() {
17318
+ return process.env["PAPI_TELEMETRY"] !== "false";
17319
+ }
17320
+ function emitTelemetryEvent(event) {
17321
+ if (!isEnabled()) return;
17322
+ const body = {
17323
+ project_id: event.project_id,
17324
+ tool_name: event.tool_name,
17325
+ event_type: event.event_type,
17326
+ metadata: event.metadata ?? {}
17327
+ };
17328
+ fetch(`${TELEMETRY_SUPABASE_URL}/rest/v1/telemetry_events`, {
17329
+ method: "POST",
17330
+ headers: {
17331
+ "Content-Type": "application/json",
17332
+ "apikey": TELEMETRY_API_KEY,
17333
+ "Authorization": `Bearer ${TELEMETRY_API_KEY}`,
17334
+ "Prefer": "return=minimal"
17335
+ },
17336
+ body: JSON.stringify(body),
17337
+ signal: AbortSignal.timeout(5e3)
17338
+ }).catch(() => {
17339
+ });
17340
+ }
17341
+ function emitToolCall(projectId, toolName, durationMs, extra) {
17342
+ emitTelemetryEvent({
17343
+ project_id: projectId,
17344
+ tool_name: toolName,
17345
+ event_type: "tool_call",
17346
+ metadata: { duration_ms: durationMs, ...extra }
17347
+ });
17348
+ }
17349
+ function emitMilestone(projectId, milestone, extra) {
17350
+ emitTelemetryEvent({
17351
+ project_id: projectId,
17352
+ tool_name: milestone,
17353
+ event_type: "milestone",
17354
+ metadata: extra
17355
+ });
17356
+ }
17357
+
17156
17358
  // src/server.ts
17157
17359
  var TOOLS_REQUIRING_PAPI = /* @__PURE__ */ new Set([
17158
17360
  "plan",
@@ -17323,6 +17525,23 @@ function createServer(adapter2, config2) {
17323
17525
  } catch {
17324
17526
  }
17325
17527
  }
17528
+ const telemetryProjectId = process.env["PAPI_PROJECT_ID"];
17529
+ if (telemetryProjectId) {
17530
+ emitToolCall(telemetryProjectId, name, elapsed, {
17531
+ adapter_type: config2.adapterType
17532
+ });
17533
+ const isApplyMode = safeArgs.mode === "apply";
17534
+ const isError = result.content.some((c) => c.text.startsWith("Error:") || c.text.startsWith("\u274C"));
17535
+ if (!isError) {
17536
+ if (name === "setup" && isApplyMode) {
17537
+ emitMilestone(telemetryProjectId, "setup_completed");
17538
+ } else if (name === "plan" && isApplyMode) {
17539
+ emitMilestone(telemetryProjectId, "plan_completed");
17540
+ } else if (name === "release") {
17541
+ emitMilestone(telemetryProjectId, "release_completed");
17542
+ }
17543
+ }
17544
+ }
17326
17545
  const footer = formatMetricsFooter(elapsed, usage, contextBytes);
17327
17546
  result.content.push({ type: "text", text: footer });
17328
17547
  return result;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@papi-ai/server",
3
- "version": "0.4.0-alpha",
3
+ "version": "0.5.0",
4
4
  "description": "PAPI MCP server — AI-powered sprint planning, build execution, and strategy review for software projects",
5
5
  "license": "Elastic-2.0",
6
6
  "type": "module",