@papi-ai/server 0.5.3 → 0.6.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.
package/dist/index.js CHANGED
@@ -1556,7 +1556,8 @@ ${TABLE_SEPARATOR}
1556
1556
  return [];
1557
1557
  }
1558
1558
  /** Update or insert an Active Decision block by ID. */
1559
- async updateActiveDecision(id, body, cycleNumber) {
1559
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
1560
+ async updateActiveDecision(id, body, cycleNumber, _action) {
1560
1561
  const content = await this.readOptional("ACTIVE_DECISIONS.md") || "## Active Decisions\n\n";
1561
1562
  await this.write("ACTIVE_DECISIONS.md", updateActiveDecisionInContent(id, body, content, cycleNumber));
1562
1563
  }
@@ -4442,6 +4443,7 @@ function rowToTask(row) {
4442
4443
  if (row.task_type != null) task.taskType = row.task_type;
4443
4444
  if (row.maturity != null) task.maturity = row.maturity;
4444
4445
  if (row.stage_id != null) task.stageId = row.stage_id;
4446
+ if (row.doc_ref != null) task.docRef = row.doc_ref;
4445
4447
  return task;
4446
4448
  }
4447
4449
  function rowToBuildReport(row) {
@@ -4540,6 +4542,8 @@ function rowToCycleLogEntry(row) {
4540
4542
  };
4541
4543
  if (row.carry_forward != null) entry.carryForward = row.carry_forward;
4542
4544
  if (row.notes != null) entry.notes = row.notes;
4545
+ if (row.task_count != null) entry.taskCount = row.task_count;
4546
+ if (row.effort_points != null) entry.effortPoints = row.effort_points;
4543
4547
  return entry;
4544
4548
  }
4545
4549
  function rowToPhase(row) {
@@ -4670,6 +4674,7 @@ function rowToStage(row) {
4670
4674
  sortOrder: row.sort_order,
4671
4675
  horizonId: row.horizon_id,
4672
4676
  projectId: row.project_id,
4677
+ exitCriteria: row.exit_criteria ?? void 0,
4673
4678
  createdAt: row.created_at,
4674
4679
  updatedAt: row.updated_at
4675
4680
  };
@@ -5366,6 +5371,7 @@ CREATE TABLE IF NOT EXISTS cycle_tasks (
5366
5371
  task_type TEXT,
5367
5372
  maturity TEXT,
5368
5373
  stage_id UUID REFERENCES stages(id),
5374
+ doc_ref TEXT,
5369
5375
  PRIMARY KEY (id),
5370
5376
  UNIQUE (project_id, display_id)
5371
5377
  );
@@ -5411,6 +5417,8 @@ CREATE TABLE IF NOT EXISTS planning_log_entries (
5411
5417
  content TEXT DEFAULT ''::text NOT NULL,
5412
5418
  carry_forward TEXT,
5413
5419
  notes TEXT,
5420
+ task_count INTEGER,
5421
+ effort_points INTEGER,
5414
5422
  created_at TIMESTAMPTZ DEFAULT now() NOT NULL,
5415
5423
  updated_at TIMESTAMPTZ DEFAULT now() NOT NULL,
5416
5424
  user_id UUID,
@@ -6118,6 +6126,19 @@ EXCEPTION WHEN duplicate_object THEN NULL; END $$;
6118
6126
  `;
6119
6127
  return rows.map(rowToActiveDecision);
6120
6128
  }
6129
+ async getSiblingAds(projectIds) {
6130
+ if (projectIds.length === 0) return [];
6131
+ const rows = await this.sql`
6132
+ SELECT *, project_id FROM active_decisions
6133
+ WHERE project_id = ANY(${projectIds}::uuid[])
6134
+ AND superseded = false
6135
+ ORDER BY project_id, display_id
6136
+ `;
6137
+ return rows.map((row) => ({
6138
+ ...rowToActiveDecision(row),
6139
+ sourceProjectId: row.project_id
6140
+ }));
6141
+ }
6121
6142
  async getCycleLog(limit) {
6122
6143
  if (limit != null) {
6123
6144
  const rows2 = await this.sql`
@@ -6168,21 +6189,25 @@ ${newParts.join("\n")}` : newParts.join("\n");
6168
6189
  }
6169
6190
  async writeCycleLogEntry(entry) {
6170
6191
  await this.sql`
6171
- INSERT INTO planning_log_entries (project_id, cycle_number, title, content, carry_forward, notes)
6192
+ INSERT INTO planning_log_entries (project_id, cycle_number, title, content, carry_forward, notes, task_count, effort_points)
6172
6193
  VALUES (
6173
6194
  ${this.projectId},
6174
6195
  ${entry.cycleNumber},
6175
6196
  ${entry.title},
6176
6197
  ${entry.content},
6177
6198
  ${entry.carryForward ?? null},
6178
- ${entry.notes ?? null}
6199
+ ${entry.notes ?? null},
6200
+ ${entry.taskCount ?? null},
6201
+ ${entry.effortPoints ?? null}
6179
6202
  )
6180
6203
  ON CONFLICT (project_id, cycle_number)
6181
6204
  DO UPDATE SET
6182
6205
  title = EXCLUDED.title,
6183
6206
  content = EXCLUDED.content,
6184
6207
  carry_forward = EXCLUDED.carry_forward,
6185
- notes = EXCLUDED.notes
6208
+ notes = EXCLUDED.notes,
6209
+ task_count = EXCLUDED.task_count,
6210
+ effort_points = EXCLUDED.effort_points
6186
6211
  `;
6187
6212
  }
6188
6213
  async writeStrategyReview(review) {
@@ -6487,19 +6512,44 @@ ${newParts.join("\n")}` : newParts.join("\n");
6487
6512
  WHERE id = ${id} AND project_id = ${this.projectId}
6488
6513
  `;
6489
6514
  }
6490
- async updateActiveDecision(id, body, cycleNumber) {
6515
+ async updateActiveDecision(id, body, cycleNumber, action) {
6516
+ const newOutcome = action === "supersede" ? "superseded" : action === "resolve" ? "resolved" : void 0;
6491
6517
  if (cycleNumber != null) {
6492
- await this.sql`
6493
- UPDATE active_decisions
6494
- SET body = ${body}, modified_cycle = ${cycleNumber}
6495
- WHERE project_id = ${this.projectId} AND display_id = ${id}
6496
- `;
6518
+ if (newOutcome != null) {
6519
+ await this.sql`
6520
+ UPDATE active_decisions
6521
+ SET body = ${body}, modified_cycle = ${cycleNumber},
6522
+ revision_count = revision_count + 1,
6523
+ outcome = ${newOutcome}
6524
+ WHERE project_id = ${this.projectId} AND display_id = ${id}
6525
+ `;
6526
+ } else {
6527
+ await this.sql`
6528
+ UPDATE active_decisions
6529
+ SET body = ${body}, modified_cycle = ${cycleNumber},
6530
+ revision_count = revision_count + 1,
6531
+ outcome = CASE WHEN outcome = 'pending' THEN 'revised' ELSE outcome END
6532
+ WHERE project_id = ${this.projectId} AND display_id = ${id}
6533
+ `;
6534
+ }
6497
6535
  } else {
6498
- await this.sql`
6499
- UPDATE active_decisions
6500
- SET body = ${body}
6501
- WHERE project_id = ${this.projectId} AND display_id = ${id}
6502
- `;
6536
+ if (newOutcome != null) {
6537
+ await this.sql`
6538
+ UPDATE active_decisions
6539
+ SET body = ${body},
6540
+ revision_count = revision_count + 1,
6541
+ outcome = ${newOutcome}
6542
+ WHERE project_id = ${this.projectId} AND display_id = ${id}
6543
+ `;
6544
+ } else {
6545
+ await this.sql`
6546
+ UPDATE active_decisions
6547
+ SET body = ${body},
6548
+ revision_count = revision_count + 1,
6549
+ outcome = CASE WHEN outcome = 'pending' THEN 'revised' ELSE outcome END
6550
+ WHERE project_id = ${this.projectId} AND display_id = ${id}
6551
+ `;
6552
+ }
6503
6553
  }
6504
6554
  }
6505
6555
  async upsertActiveDecision(id, body, title, confidence, cycleNumber) {
@@ -6596,7 +6646,7 @@ ${newParts.join("\n")}` : newParts.join("\n");
6596
6646
  project_id, display_id, title, status, priority, complexity,
6597
6647
  module, epic, phase, owner, reviewed, cycle, created_cycle,
6598
6648
  why, depends_on, notes, closure_reason, state_history,
6599
- build_handoff, build_report, task_type, maturity, stage_id
6649
+ build_handoff, build_report, task_type, maturity, stage_id, doc_ref
6600
6650
  ) VALUES (
6601
6651
  ${this.projectId}, ${displayId}, ${task.title}, ${task.status}, ${task.priority},
6602
6652
  ${normaliseComplexity(task.complexity)}, ${task.module}, ${task.epic ?? null}, ${task.phase}, ${task.owner},
@@ -6608,7 +6658,8 @@ ${newParts.join("\n")}` : newParts.join("\n");
6608
6658
  ${task.buildReport ?? null},
6609
6659
  ${task.taskType ?? null},
6610
6660
  ${task.maturity ?? null},
6611
- ${task.stageId ?? null}
6661
+ ${task.stageId ?? null},
6662
+ ${task.docRef ?? null}
6612
6663
  )
6613
6664
  RETURNING *
6614
6665
  `;
@@ -6638,6 +6689,7 @@ ${newParts.join("\n")}` : newParts.join("\n");
6638
6689
  if (updates.taskType !== void 0) columnMap["task_type"] = updates.taskType;
6639
6690
  if (updates.maturity !== void 0) columnMap["maturity"] = updates.maturity;
6640
6691
  if (updates.stageId !== void 0) columnMap["stage_id"] = updates.stageId;
6692
+ if (updates.docRef !== void 0) columnMap["doc_ref"] = updates.docRef;
6641
6693
  const keys = Object.keys(columnMap);
6642
6694
  if (keys.length === 0) return;
6643
6695
  await this.sql`
@@ -6724,6 +6776,86 @@ ${newParts.join("\n")}` : newParts.join("\n");
6724
6776
  return rows.map(rowToBuildReport);
6725
6777
  }
6726
6778
  // -------------------------------------------------------------------------
6779
+ // Cycle Learnings
6780
+ // -------------------------------------------------------------------------
6781
+ async appendCycleLearnings(learnings) {
6782
+ if (learnings.length === 0) return;
6783
+ for (const l of learnings) {
6784
+ await this.sql`
6785
+ INSERT INTO cycle_learnings (
6786
+ project_id, task_id, cycle_number, category, severity,
6787
+ summary, detail, tags, related_decision, action_taken, action_ref
6788
+ ) VALUES (
6789
+ ${this.projectId}, ${l.taskId}, ${l.cycleNumber}, ${l.category}, ${l.severity ?? null},
6790
+ ${l.summary}, ${l.detail ?? null}, ${l.tags}, ${l.relatedDecision ?? null},
6791
+ ${l.actionTaken ?? null}, ${l.actionRef ?? null}
6792
+ )
6793
+ `;
6794
+ }
6795
+ }
6796
+ async getCycleLearnings(opts) {
6797
+ const limit = opts?.limit ?? 50;
6798
+ let rows;
6799
+ if (opts?.cycleNumber && opts?.category) {
6800
+ rows = await this.sql`
6801
+ SELECT * FROM cycle_learnings
6802
+ WHERE project_id = ${this.projectId} AND cycle_number = ${opts.cycleNumber} AND category = ${opts.category}
6803
+ ORDER BY created_at DESC LIMIT ${limit}
6804
+ `;
6805
+ } else if (opts?.cycleNumber) {
6806
+ rows = await this.sql`
6807
+ SELECT * FROM cycle_learnings
6808
+ WHERE project_id = ${this.projectId} AND cycle_number = ${opts.cycleNumber}
6809
+ ORDER BY created_at DESC LIMIT ${limit}
6810
+ `;
6811
+ } else if (opts?.category) {
6812
+ rows = await this.sql`
6813
+ SELECT * FROM cycle_learnings
6814
+ WHERE project_id = ${this.projectId} AND category = ${opts.category}
6815
+ ORDER BY created_at DESC LIMIT ${limit}
6816
+ `;
6817
+ } else {
6818
+ rows = await this.sql`
6819
+ SELECT * FROM cycle_learnings
6820
+ WHERE project_id = ${this.projectId}
6821
+ ORDER BY created_at DESC LIMIT ${limit}
6822
+ `;
6823
+ }
6824
+ return rows.map((r) => ({
6825
+ id: r.id,
6826
+ projectId: r.project_id,
6827
+ taskId: r.task_id,
6828
+ cycleNumber: r.cycle_number,
6829
+ category: r.category,
6830
+ severity: r.severity ?? void 0,
6831
+ summary: r.summary,
6832
+ detail: r.detail ?? void 0,
6833
+ tags: r.tags ?? [],
6834
+ relatedDecision: r.related_decision ?? void 0,
6835
+ actionTaken: r.action_taken ?? void 0,
6836
+ actionRef: r.action_ref ?? void 0,
6837
+ createdAt: r.created_at ? r.created_at.toISOString() : void 0
6838
+ }));
6839
+ }
6840
+ async getCycleLearningPatterns() {
6841
+ const rows = await this.sql`
6842
+ SELECT tag,
6843
+ array_agg(DISTINCT cycle_number ORDER BY cycle_number) as cycles,
6844
+ count(DISTINCT cycle_number)::text as frequency
6845
+ FROM cycle_learnings, unnest(tags) AS tag
6846
+ WHERE project_id = ${this.projectId}
6847
+ GROUP BY tag
6848
+ HAVING count(DISTINCT cycle_number) >= 2
6849
+ ORDER BY count(DISTINCT cycle_number) DESC
6850
+ LIMIT 10
6851
+ `;
6852
+ return rows.map((r) => ({
6853
+ tag: r.tag,
6854
+ cycles: r.cycles,
6855
+ frequency: parseInt(r.frequency, 10)
6856
+ }));
6857
+ }
6858
+ // -------------------------------------------------------------------------
6727
6859
  // Reviews
6728
6860
  // -------------------------------------------------------------------------
6729
6861
  async getRecentReviews(count) {
@@ -7091,6 +7223,12 @@ ${newParts.join("\n")}` : newParts.join("\n");
7091
7223
  await this.sql`
7092
7224
  UPDATE stages SET status = ${status}, updated_at = NOW()
7093
7225
  WHERE id = ${stageId} AND project_id = ${this.projectId}
7226
+ `;
7227
+ }
7228
+ async updateStageExitCriteria(stageId, exitCriteria) {
7229
+ await this.sql`
7230
+ UPDATE stages SET exit_criteria = ${exitCriteria}, updated_at = NOW()
7231
+ WHERE id = ${stageId} AND project_id = ${this.projectId}
7094
7232
  `;
7095
7233
  }
7096
7234
  async updateHorizonStatus(horizonId, status) {
@@ -7436,6 +7574,26 @@ ${r.content}` + (r.carry_forward ? `
7436
7574
  return row?.exists ?? false;
7437
7575
  }
7438
7576
  // -------------------------------------------------------------------------
7577
+ // Plan Runs (telemetry)
7578
+ // -------------------------------------------------------------------------
7579
+ async insertPlanRun(entry) {
7580
+ await this.sql`
7581
+ INSERT INTO plan_runs (
7582
+ project_id, cycle_number, context_bytes, duration_ms,
7583
+ task_count_in, task_count_out, backlog_depth, notes
7584
+ ) VALUES (
7585
+ ${this.projectId},
7586
+ ${entry.cycleNumber},
7587
+ ${entry.contextBytes ?? null},
7588
+ ${entry.durationMs ?? null},
7589
+ ${entry.taskCountIn ?? null},
7590
+ ${entry.taskCountOut ?? null},
7591
+ ${entry.backlogDepth ?? null},
7592
+ ${entry.notes ?? null}
7593
+ )
7594
+ `;
7595
+ }
7596
+ // -------------------------------------------------------------------------
7439
7597
  // Task Comments (for plan context)
7440
7598
  // -------------------------------------------------------------------------
7441
7599
  async getRecentTaskComments(limit = 20) {
@@ -7469,6 +7627,20 @@ ${r.content}` + (r.carry_forward ? `
7469
7627
  cycle_number: ref.cycleNumber
7470
7628
  }));
7471
7629
  await this.sql`INSERT INTO entity_references ${this.sql(values2)}`;
7630
+ const adRefs = refs.filter((r) => r.entityType === "active_decision");
7631
+ if (adRefs.length > 0) {
7632
+ const cycleNumber = adRefs.reduce((max, r) => Math.max(max, r.cycleNumber ?? 0), 0);
7633
+ if (cycleNumber > 0) {
7634
+ const adIds = adRefs.map((r) => r.entityId);
7635
+ await this.sql`
7636
+ UPDATE active_decisions
7637
+ SET last_referenced_cycle = ${cycleNumber}
7638
+ WHERE project_id = ${this.projectId}
7639
+ AND display_id = ANY(${adIds})
7640
+ AND (last_referenced_cycle IS NULL OR last_referenced_cycle < ${cycleNumber})
7641
+ `;
7642
+ }
7643
+ }
7472
7644
  }
7473
7645
  async getDecisionUsage(currentCycle) {
7474
7646
  const rows = await this.sql`
@@ -8068,6 +8240,9 @@ var init_proxy_adapter = __esm({
8068
8240
  actionRecommendation(id, cycleNumber) {
8069
8241
  return this.invoke("actionRecommendation", [id, cycleNumber]);
8070
8242
  }
8243
+ dismissRecommendation(id, reason) {
8244
+ return this.invoke("dismissRecommendation", [id, reason]);
8245
+ }
8071
8246
  // --- Decision Events & Scores ---
8072
8247
  appendDecisionEvent(event) {
8073
8248
  return this.invoke("appendDecisionEvent", [event]);
@@ -8301,19 +8476,33 @@ async function createAdapter(optionsOrType, maybePapiDir) {
8301
8476
  }
8302
8477
  case "proxy": {
8303
8478
  const { ProxyPapiAdapter: ProxyPapiAdapter2 } = await Promise.resolve().then(() => (init_proxy_adapter(), proxy_adapter_exports));
8479
+ const dashboardUrl = process.env["PAPI_DASHBOARD_URL"] || "https://papi-web-three.vercel.app";
8304
8480
  const projectId = process.env["PAPI_PROJECT_ID"];
8481
+ const dataApiKey = process.env["PAPI_DATA_API_KEY"];
8482
+ if (!projectId && !dataApiKey) {
8483
+ throw new Error(
8484
+ `PAPI needs an account to store your project data.
8485
+
8486
+ Get started in 3 steps:
8487
+ 1. Sign up at ${dashboardUrl}/login
8488
+ 2. Complete the onboarding wizard \u2014 it generates your .mcp.json config
8489
+ 3. Download the config, place it in your project root, and restart Claude Code
8490
+
8491
+ Already have an account? Make sure PAPI_DATA_API_KEY and PAPI_PROJECT_ID are set in your .mcp.json env config.`
8492
+ );
8493
+ }
8305
8494
  if (!projectId) {
8306
8495
  throw new Error(
8307
- "PAPI_PROJECT_ID is required. Generate a UUID (run `uuidgen` in terminal) and set it in your .mcp.json env config."
8496
+ `PAPI_PROJECT_ID is required.
8497
+ Visit ${dashboardUrl}/onboard to generate your config, or add PAPI_PROJECT_ID to your .mcp.json env config.`
8308
8498
  );
8309
8499
  }
8310
8500
  const dataEndpoint = process.env["PAPI_DATA_ENDPOINT"] || HOSTED_PROXY_ENDPOINT;
8311
- const dataApiKey = process.env["PAPI_DATA_API_KEY"];
8312
8501
  if (!dataApiKey) {
8313
8502
  throw new Error(
8314
8503
  `PAPI_DATA_API_KEY is required for proxy mode.
8315
8504
  To get your API key:
8316
- 1. Sign in at ${process.env["PAPI_DASHBOARD_URL"] || "https://papi-web-three.vercel.app"} with GitHub
8505
+ 1. Sign up or sign in at ${dashboardUrl}/login
8317
8506
  2. Your API key is shown on the onboarding page (save it \u2014 shown only once)
8318
8507
  3. Add PAPI_DATA_API_KEY to your .mcp.json env config
8319
8508
  If you already have a key, set it in your MCP configuration.`
@@ -8345,11 +8534,15 @@ If you already have a key, set it in your MCP configuration.`
8345
8534
  }
8346
8535
 
8347
8536
  // src/server.ts
8348
- import { access as access4 } from "fs/promises";
8537
+ import { access as access4, readdir as readdir2, readFile as readFile5 } from "fs/promises";
8538
+ import { join as join9, dirname } from "path";
8539
+ import { fileURLToPath } from "url";
8349
8540
  import { Server } from "@modelcontextprotocol/sdk/server/index.js";
8350
8541
  import {
8351
8542
  CallToolRequestSchema,
8352
- ListToolsRequestSchema
8543
+ ListToolsRequestSchema,
8544
+ ListPromptsRequestSchema,
8545
+ GetPromptRequestSchema
8353
8546
  } from "@modelcontextprotocol/sdk/types.js";
8354
8547
 
8355
8548
  // src/lib/response.ts
@@ -8473,7 +8666,7 @@ function formatDetailedTask(t) {
8473
8666
  return `- **${t.id}:** ${t.title}
8474
8667
  Status: ${t.status} | Priority: ${t.priority} | Complexity: ${t.complexity}${typeTag}
8475
8668
  Module: ${t.module} | Epic: ${t.epic} | Phase: ${t.phase} | Owner: ${t.owner}
8476
- Reviewed: ${t.reviewed}${t.dependsOn ? ` | Depends on: ${t.dependsOn}` : ""}${hasHandoff ? " | Has BUILD HANDOFF: yes" : ""}${notes ? `
8669
+ Reviewed: ${t.reviewed}${t.dependsOn ? ` | Depends on: ${t.dependsOn}` : ""}${hasHandoff ? " | Has BUILD HANDOFF: yes" : ""}${t.docRef ? ` | Doc ref: ${t.docRef}` : ""}${notes ? `
8477
8670
  Notes: ${notes}` : ""}`;
8478
8671
  }
8479
8672
  function formatBoardForPlan(tasks, filters, currentCycle) {
@@ -8542,6 +8735,36 @@ function trendArrow(current, previous, higherIsBetter) {
8542
8735
  const improving = higherIsBetter ? current > previous : current < previous;
8543
8736
  return improving ? " \u2191" : " \u2193";
8544
8737
  }
8738
+ var EFFORT_MAP2 = { XS: 1, S: 2, M: 3, L: 5, XL: 8 };
8739
+ function computeSnapshotsFromBuildReports(reports) {
8740
+ if (reports.length === 0) return [];
8741
+ const byCycleMap = /* @__PURE__ */ new Map();
8742
+ for (const r of reports) {
8743
+ const existing = byCycleMap.get(r.cycle) ?? [];
8744
+ existing.push(r);
8745
+ byCycleMap.set(r.cycle, existing);
8746
+ }
8747
+ const snapshots = [];
8748
+ for (const [sn, cycleReports] of byCycleMap) {
8749
+ const completed = cycleReports.filter((r) => r.completed === "Yes").length;
8750
+ const total = cycleReports.length;
8751
+ const withEffort = cycleReports.filter((r) => r.estimatedEffort && r.actualEffort);
8752
+ const accurate = withEffort.filter((r) => r.estimatedEffort === r.actualEffort).length;
8753
+ const matchRate = withEffort.length > 0 ? Math.round(accurate / withEffort.length * 100) : 0;
8754
+ let effortPoints = 0;
8755
+ for (const r of cycleReports) {
8756
+ effortPoints += EFFORT_MAP2[r.actualEffort] ?? 3;
8757
+ }
8758
+ snapshots.push({
8759
+ cycle: sn,
8760
+ date: (/* @__PURE__ */ new Date()).toISOString(),
8761
+ accuracy: [{ cycle: sn, reports: total, matchRate, mae: 0, bias: 0 }],
8762
+ velocity: [{ cycle: sn, completed, partial: 0, failed: total - completed, effortPoints }]
8763
+ });
8764
+ }
8765
+ snapshots.sort((a, b2) => a.cycle - b2.cycle);
8766
+ return snapshots;
8767
+ }
8545
8768
  function formatCycleMetrics(snapshots) {
8546
8769
  if (snapshots.length === 0) return "No methodology metrics yet.";
8547
8770
  const latest = snapshots[snapshots.length - 1];
@@ -9298,7 +9521,7 @@ After your natural language output, include this EXACT format on its own line:
9298
9521
  <!-- PAPI_STRUCTURED_OUTPUT -->
9299
9522
  \`\`\`json
9300
9523
  {
9301
- "cycleLogTitle": "string \u2014 short descriptive title WITHOUT 'Cycle N' prefix (e.g. 'Board Triage \u2014 Bug Fix' not 'Cycle 5 \u2014 Board Triage \u2014 Bug Fix')",
9524
+ "cycleLogTitle": "string \u2014 short descriptive title WITHOUT 'Cycle N' prefix. Should capture the cycle theme in 3-5 words (e.g. 'MCP Quality + Product Readiness' not 'Cycle 5 \u2014 Board Triage \u2014 Bug Fix'). This is the canonical theme label for the cycle.",
9302
9525
  "cycleLogContent": "string \u2014 5-10 line cycle log body in markdown, NO heading (the ### heading is generated automatically)",
9303
9526
  "cycleLogCarryForward": "string or null \u2014 carry-forward items for next cycle",
9304
9527
  "cycleLogNotes": "string or null \u2014 1-3 lines of cycle-level observations: estimation accuracy, recurring blockers, velocity trends, dependency signals. Omit if no noteworthy observations.",
@@ -9439,6 +9662,7 @@ Standard planning cycle with full board review.
9439
9662
  Within the same priority level, prefer tasks with the highest **impact-to-effort ratio**. Impact is measured by: (a) strategic alignment \u2014 does it advance the current horizon/phase? (b) unlocks other work \u2014 are tasks blocked by this? (c) user-facing \u2014 does it change what users see? (d) compounds over time \u2014 does it make future cycles faster? A high-impact Medium task beats a low-impact Small task at the same priority level. Justify in 2-3 sentences.
9440
9663
  **Blocked tasks:** Tasks with status "Blocked" MUST be skipped during task selection \u2014 they are waiting on external dependencies or gates and cannot be built. Do NOT generate BUILD HANDOFFs for blocked tasks. Do NOT recommend blocked tasks. If a blocked task's gate has been resolved (check the notes and recent build reports), emit a \`boardCorrections\` entry to move it back to Backlog. Report blocked task count in the cycle log.
9441
9664
  **Cycle sizing:** Size the cycle based on what the selected tasks actually require \u2014 not a fixed budget. Select the highest-priority unblocked tasks, estimate each one's effort from its scope, and let the total emerge from the tasks themselves. The historical average effort from Methodology Trends is a reference point for calibration, not a target or floor. A healthy cycle has 4-6 tasks. Cycles with fewer than 4 tasks require explicit justification in the cycle log \u2014 explain why more tasks could not be included. When the backlog has 10+ tasks, the cycle SHOULD have 5+ tasks \u2014 undersized cycles waste planning overhead relative to the available work. If fewer than 4 tasks qualify after filtering (blocked, deferred, raw), check Deferred tasks \u2014 some may be ready to un-defer via a \`boardCorrections\` entry. A 1-task cycle is almost never correct.
9665
+ **Theme coherence:** After selecting candidate tasks, check whether they form a coherent theme \u2014 all serving one goal, phase, or module. Single-theme cycles produce better build quality and less context switching. If the top candidates touch 3+ unrelated modules or epics, prefer regrouping around the highest-priority theme and deferring the outliers. Mixed-theme cycles are acceptable when justified (e.g. a P0 fix alongside P1 feature work), but the justification must appear in the cycle log. Name the theme in 3-5 words \u2014 it becomes the \`cycleLogTitle\`.
9442
9666
 
9443
9667
  8. **Cycle Log** \u2014 Write 5-10 line entry: what was triaged, what was recommended and why, observations, AD updates.
9444
9668
  **Cycle Notes** \u2014 Optionally include 1-3 lines of cycle-level observations in \`cycleLogNotes\`: estimation accuracy patterns, recurring blockers, velocity trends, or dependency signals. These notes persist across cycles so future planning runs can learn from them. Use null if there are no noteworthy observations this cycle.
@@ -9461,10 +9685,29 @@ Standard planning cycle with full board review.
9461
9685
  **Estimation calibration:** Estimate **XS** for: copy/text-only changes, single string replacements, config tweaks, and any task where the scope is "change words in an existing file" with no logic changes. Estimate **S** for: wiring existing adapter methods, adding API routes following established patterns, modifying prompts, or documentation-only changes. Default to S for pattern-following work. Only use M when genuine new architecture, new DB tables, or multi-file architectural changes are needed. Historical data shows systematic over-estimation (198 over vs 8 under out of 528 tasks) \u2014 when in doubt, estimate smaller. If an "Estimation Calibration (Historical)" section is provided in the context below, use its data to adjust your estimates \u2014 it shows how often each estimated size matched the actual effort. Pay special attention to systematic over/under-estimation patterns (e.g. if M\u2192S happens frequently, estimate S instead of M for similar work).
9462
9686
  **Reference docs:** If a task's notes include a \`Reference:\` path (e.g. \`Reference: docs/architecture/papi-brain-v1.md\`), include a REFERENCE DOCS section in the BUILD HANDOFF with those paths. This tells the builder to read the referenced doc for background context before implementing. Do NOT omit or summarise the reference \u2014 pass it through so the builder can access the full document. Only tasks with explicit \`Reference:\` paths in their notes should have this section.
9463
9687
  **Pre-build verification:** EVERY handoff MUST include a PRE-BUILD VERIFICATION section listing 2-5 specific file paths the builder should read before implementing. Derive these from FILES LIKELY TOUCHED \u2014 pick the files most likely to already contain the target functionality. This is the #1 prevention mechanism for wasted build slots (C120, C125, C126 all scheduled already-shipped work). If the builder finds >80% of the scope already implemented, they report "already built" instead of re-implementing.
9464
- **UI/visual task detection:** When a task's title or notes contain keywords suggesting frontend visual work (e.g. "visual", "design", "UI", "styling", "refresh", "frontend", "landing page", "hero", "carousel", "theme", "layout"), apply these handoff additions:
9465
- - Add to SCOPE: "Use the \`frontend-design\` skill for implementation \u2014 it produces higher-quality visual output than manual styling."
9466
- - Add to ACCEPTANCE CRITERIA: "[ ] Visually verify rendered output in browser before reporting done \u2014 provide localhost URL or screenshot to the user for review."
9467
- - If the task involves image selection (carousels, hero sections, galleries), add to SCOPE: "Include brand/theme direction constraints for image selection \u2014 specify the visual mood, style references, and what to avoid (e.g. no generic stock portraits)."
9688
+ **Research task detection:** When a task's title starts with "Research:" or the task type is "research", add a RESEARCH OUTPUT section to the BUILD HANDOFF after ACCEPTANCE CRITERIA:
9689
+
9690
+ RESEARCH OUTPUT
9691
+ Deliverable: docs/research/[topic]-findings.md (draft path)
9692
+ Review status: pending owner approval
9693
+ Follow-up tasks: DO NOT submit to backlog until owner confirms findings are actionable
9694
+
9695
+ Also add to ACCEPTANCE CRITERIA: "[ ] Findings doc drafted and saved to docs/research/ before submitting any follow-up ideas"
9696
+
9697
+ **Bug task detection:** When a task's task type is "bug" or the title starts with "Bug:" or "Fix:", apply these rules:
9698
+ - **Auto-P1:** If the task's current priority is P2 or lower, upgrade it to "P1 High" via a boardCorrections entry in Part 2. Note the upgrade in Part 1 analysis.
9699
+ - Add a BLAST RADIUS note to the BUILD HANDOFF SCOPE section: "Bug fix \u2014 minimal blast radius. Change only what is necessary to fix the reported behaviour. Do not refactor surrounding code or expand scope."
9700
+ - Add to ACCEPTANCE CRITERIA: "[ ] Fix is targeted \u2014 no unrelated code changed"
9701
+
9702
+ **Idea task detection:** When a task's task type is "idea", add a scope clarification note to the BUILD HANDOFF:
9703
+ - Add to SCOPE (DO THIS): "This task originated as an idea. Confirm the exact deliverable before implementing \u2014 check task notes and any referenced docs for intent. If scope is unclear, flag it in the build report surprises."
9704
+
9705
+ **UI/visual task detection:** When a task's title or notes contain keywords suggesting frontend visual work (e.g. "visual", "design", "UI", "styling", "refresh", "frontend", "landing page", "hero", "carousel", "theme", "layout", "cockpit", "dashboard", "page"), apply these handoff additions:
9706
+ - Add to SCOPE: "Read \`.impeccable.md\` for brand palette, design principles, and audience context before writing any code. Use the \`frontend-design\` skill for implementation."
9707
+ - For M/L UI tasks, add to SCOPE: "Use the full UI toolchain: Playground (design preview) \u2192 Frontend-design (build) \u2192 Playwright (verify). The playground is the quality bar. Expect 2-3 iterations."
9708
+ - Add to ACCEPTANCE CRITERIA: "[ ] Visually verify rendered output in browser \u2014 provide localhost URL or screenshot to user for review." and "[ ] No raw IDs, abbreviations, or jargon visible without human-readable labels or tooltips."
9709
+ - If the task involves image selection, add to SCOPE: "Include brand/theme direction constraints for image selection."
9710
+ The planner's job is scoping, not design direction. Design decisions happen at build time via \`.impeccable.md\` and the frontend-design skill \u2014 don't try to write design specs in the handoff.
9468
9711
 
9469
9712
  11. **New Tasks (max 3 per cycle)** \u2014 Actively mine the Recent Build Reports for task candidates. For each report, check:
9470
9713
  - **Discovered Issues:** If a build report lists a discovered issue and no existing board task covers it, propose a new task.
@@ -9659,115 +9902,84 @@ IMPORTANT: You are running as a non-interactive API call. Do NOT ask the user qu
9659
9902
 
9660
9903
  ## OUTPUT PRINCIPLES
9661
9904
 
9662
- - **Full depth, not thin summaries.** Each mandatory section must be substantive \u2014 multiple paragraphs with specific evidence, not compressed bullet points. The reader should understand cross-cycle patterns, not just individual cycle events. If a section would be under 3 sentences, you haven't gone deep enough.
9905
+ - **Product impact first, process second.** The reader wants to know: what got better for users, what's broken, what opportunities exist. Internal machinery (AD wording, taxonomy labels, hierarchy status) is secondary \u2014 handle it in a compact appendix, not the main review.
9663
9906
  - **Lead with insight, not data recitation.** Open each section with the strategic takeaway or pattern, THEN support it with cycle data and task references. Bad: "C131 built task-700, C132 built task-710." Good: "The last 5 cycles show a clear shift from infrastructure to user-facing work \u2014 80% of tasks were dashboard or onboarding, up from 30% in the prior review window."
9664
9907
  - **Cycle data first, conversation context second.** Base your review on build reports, cycle logs, board state, and ADs \u2014 not on whatever was discussed earlier in the conversation. If recent conversation context conflicts with the data, flag it but trust the data.
9665
- - **Every conditional section earns its place.** If a conditional section has nothing meaningful to say, skip it entirely. Do not write "No issues found" or "No concerns" \u2014 just omit the section. But the 6 mandatory sections MUST appear with full depth regardless.
9908
+ - **Be concise and scannable.** Use short paragraphs, bullet points, and clear headings. Avoid walls of text. The review should be readable in 3 minutes, not 15. Format cycle summaries as compact bullet points, not multi-paragraph narratives.
9909
+ - **Every conditional section earns its place.** If a conditional section has nothing meaningful to say, skip it entirely. Do not write "No issues found" or "No concerns" \u2014 just omit the section.
9910
+ - **AD housekeeping is an appendix, not the centerpiece.** Just list changes and make them. Don't score every AD individually. Don't ask for approval on wording tweaks \u2014 small changes (confidence bumps, deleting stale ADs, fixing wording) should just happen. Only flag ADs that represent a genuine strategic question.
9666
9911
 
9667
9912
  ## TWO-PHASE DELIVERY
9668
9913
 
9669
- This review is delivered in two phases:
9670
- 1. **Phase 1 (this output):** Present the full review \u2014 all 6 mandatory sections with complete analysis, plus any relevant conditional sections. Do NOT compress, summarise, or abbreviate. The user needs to read and discuss the full review before actions are taken.
9671
- 2. **Phase 2 (after user discussion):** The structured action breakdown in Part 2 captures concrete next steps. But the user may refine, reject, or add to these after reading Phase 1. The structured output represents your best autonomous assessment \u2014 the user's feedback in conversation refines it.
9914
+ 1. **Phase 1 (this output):** Present the review \u2014 all 5 mandatory sections plus relevant conditional sections. Be thorough on product gaps and opportunities, compact on housekeeping. The user will discuss and refine before the structured output is applied.
9915
+ 2. **Phase 2 (after user discussion):** The structured data in Part 2 captures actions. The user may modify these after reading Phase 1.
9672
9916
 
9673
- Present the full review first. Let the analysis breathe. The user will discuss, push back, and refine before acting on the structured output.
9917
+ The review should be readable in one sitting. Don't pad sections for depth \u2014 earn every paragraph with a specific insight or actionable finding.
9674
9918
 
9675
9919
  ## YOUR JOB \u2014 STRUCTURED COVERAGE
9676
9920
 
9677
- You MUST cover these 6 sections. Each is mandatory unless explicitly noted as conditional.
9921
+ You MUST cover these 5 sections. Each is mandatory.
9678
9922
 
9679
- 1. **Cycle-by-Cycle Impact Summary** \u2014 For each cycle since the last review, summarise what was strategically significant \u2014 not just velocity numbers. What capability was added? What blocker was removed? What direction shifted? Reference task IDs. This is the most important section \u2014 it tells the reader what actually happened and why it mattered.
9923
+ 1. **What Got Built & Why It Matters** \u2014 Compact summary of each cycle since last review. For each cycle: 1-2 bullet points on what shipped and the strategic significance. Reference task IDs. Don't write multi-paragraph narratives per cycle \u2014 keep it scannable. End with the cross-cycle pattern or theme.
9680
9924
 
9681
- 2. **Horizon & Phase Progress** \u2014 Is the current horizon/phase plan on track? What phase are we in? What's been completed? What's blocked? If a phase prerequisite is unmet or a decision is pending that blocks the next phase, flag it here. Reference the Forward Horizon if available.
9925
+ 2. **Product Gaps & User Experience** \u2014 This is the MOST IMPORTANT section. Answer these questions with specifics:
9926
+ - If a new user tried this product tomorrow, what would confuse or break for them?
9927
+ - What features are broken, half-built, or misleading on the dashboard/UI?
9928
+ - What's missing that would make the product noticeably better?
9929
+ - What user experience friction has been reported or observed in dogfood/build reports?
9930
+ Surface real problems, not theoretical ones. Reference specific pages, components, or flows that need attention.
9682
9931
 
9683
- 3. **New Tasks & Ideas Since Last Review** \u2014 Count new backlog tasks added since the last review. Assess alignment: are they supporting the current phase/horizon, or drifting toward unrelated work? Flag any clustering patterns (e.g. "5 of 7 new tasks are MCP Server improvements \u2014 this is on-strategy" or "3 new tasks are commercial features with no alpha testers \u2014 premature").
9932
+ 3. **Opportunities & Growth** \u2014 What's not being built or explored that could move the needle?
9933
+ - Marketing, distribution, community, content opportunities
9934
+ - Methodology improvements that would make cycles more efficient
9935
+ - Features or improvements that would differentiate from competitors
9936
+ - Things the project owner mentioned wanting but that haven't been prioritised
9684
9937
 
9685
- 4. **What Changed Strategically** \u2014 Decisions made, direction shifts, carry-forward items resolved or created since last review. Did any strategy_change calls happen? Were any ADs created, modified, or superseded? This section answers: "If I missed the last N cycles, what changed about where this project is going?"
9938
+ 4. **Strategic Direction Check** \u2014 Brief assessment (not a deep dive):
9939
+ - Is the North Star still accurate? (1-2 sentences, not a multi-paragraph analysis)
9940
+ - Has the target user or problem statement changed?
9941
+ - What carry-forward items are stuck and why?
9942
+ - If the product brief needs updating, include the update in Part 2.
9686
9943
 
9687
- 5. **North Star & Product Direction** \u2014 Go beyond "is the North Star still accurate?" and challenge the product direction:
9688
- - Is the product brief still an accurate description of what this product IS and WHERE it's going? If ADs have been created or superseded since the brief was last updated, the brief may be wrong.
9689
- - Has the target user changed? Has the scope expanded or contracted in ways the brief doesn't capture?
9690
- - Are we building for the right problem? Has evidence emerged (from builds, feedback, or market) that the core problem statement needs revision?
9691
- - Assess North Star drift: Does the North Star's key metric and success definition still align with the current phase, active ADs, and recent build directions? A North Star is drifted when: the metric it tracks is no longer the team's focus, the success criteria reference capabilities that have been deprioritised, or ADs have shifted the product direction away from what the North Star describes. Cycle count since last update is a secondary signal \u2014 a stable, accurate North Star is not stale regardless of age.
9692
- If this analysis reveals the brief needs updating, you MUST include updated content in \`productBriefUpdates\` in Part 2. Don't just note "the brief is stale" \u2014 write the update.
9944
+ 5. **AD & Hierarchy Housekeeping** \u2014 Compact appendix. NOT the focus of the review.
9945
+ - List ADs being deleted, modified, or created \u2014 with the change, not a per-AD essay
9946
+ - Just make small changes (confidence bumps, stale AD deletion, wording fixes) \u2014 don't ask for approval
9947
+ - Only flag ADs that represent a genuine strategic question requiring owner input
9948
+ - Note any hierarchy/phase issues worth correcting (1-2 bullets max)
9949
+ - Delete ADs that are legacy, process-level, or redundant without discussion
9693
9950
 
9694
- 6. **Active Decision Review + Scoring** \u2014 For each non-superseded AD: is the confidence level still correct? Has evidence emerged that changes anything? Score on 5 dimensions (1-5, lower = better):
9695
- - **effort** \u2014 Implementation cost (1=trivial, 5=major project)
9696
- - **risk** \u2014 Likelihood of failure or rework (1=safe, 5=unproven)
9697
- - **reversibility** \u2014 How hard to undo (1=trivial rollback, 5=permanent)
9698
- - **scale_cost** \u2014 What this costs at 10x/100x users or data (1=negligible, 5=bottleneck)
9699
- - **lock_in** \u2014 Dependency on a specific vendor/tool (1=swappable, 5=deeply coupled)
9700
- Only score ADs where you have enough context to evaluate meaningfully \u2014 skip ADs where scoring would be guesswork.
9701
- **AD Quality Bar:** ADs are for product and architecture choices that constrain future work \u2014 technology selections, data model designs, UX principles, strategic positioning. They are NOT for: process preferences (commit style, PR size), configuration choices (linter rules, tab width), or temporary workarounds. If a decision doesn't affect what gets built or how it's architected, it's not an AD. Flag any existing ADs that fail this bar for deletion via \`activeDecisionUpdates\` with action \`delete\`.
9702
- **IMPORTANT:** If your analysis recommends changing an AD's confidence, modifying its body, or creating a new AD, you MUST include it in \`activeDecisionUpdates\` in Part 2. Analysis without persistence is waste \u2014 the next plan won't see your recommendation unless it's in the structured output.
9951
+ ## CONDITIONAL SECTIONS (include only when genuinely useful \u2014 most reviews should have 0-2 of these)
9703
9952
 
9704
- ## CONDITIONAL SECTIONS (include only when relevant)
9953
+ 6. **Security Posture Review** \u2014 Only if \`[SECURITY]\` tags exist in recent cycle logs.
9705
9954
 
9706
- 7. **Security Posture Review** \u2014 Only if \`[SECURITY]\` tags exist in recent cycle logs. List flagged concerns, resolution status, trend, and recommendations.
9955
+ 7. **Dogfood Friction \u2192 Task Conversion** \u2014 Scan dogfood observations for recurring friction (2+ occurrences without a board task). Convert up to 3 to task proposals via \`actionItems\` in Part 2. Skip if nothing recurring.
9707
9956
 
9708
- 8. **Dogfood Friction \u2192 Task Conversion** \u2014 Scan dogfood log entries (if provided in context) for recurring friction points. For each friction entry, decide: convert to task (yes/no). Convert when a friction point has appeared 2+ times without a corresponding board task. Cap at 3 task proposals per review.
9709
- **How to convert friction to tasks:**
9710
- - In Part 1, write a "Dogfood Friction \u2192 Tasks" subsection listing each friction entry and your decision (convert or skip with reason).
9711
- - For each converted friction, add an entry to \`actionItems\` in Part 2 with \`type: "submit"\` and a descriptive \`description\` that includes the task title and scope. Example: \`{"description": "Submit task: Fix deprioritise clearing handoffs unnecessarily \u2014 add flag to preserve handoff on deprioritise", "type": "submit", "target": null}\`
9712
- - If friction points have been addressed by recent builds, note the resolution and skip them.
9713
- - This closes the loop between "we noticed a problem" and "we created a task to fix it."
9957
+ 8. **Architecture Health** \u2014 Only flag genuine broken data paths, config drift, or dead code. Keep it brief \u2014 bullet points, not paragraphs. Skip entirely if nothing found.
9714
9958
  ${compressionJob}
9715
- 9. **Architecture Health Check** \u2014 Scan the project context for structural issues that silently degrade quality. Only flag genuine findings \u2014 do not add boilerplate. Check for:
9716
- - **Broken data paths** \u2014 DB tables that exist but aren't being read by the dashboard, file reads returning empty, API routes with no consumers. Cycle 42 showed an empty product brief going undetected for multiple cycles \u2014 this check catches that class of problem.
9717
- - **Adapter parity gaps** \u2014 Features implemented in the pg adapter but missing from md (or vice versa). Both adapters must implement the same PapiAdapter interface, but runtime behavior can diverge.
9718
- - **Config drift** \u2014 Environment variables referenced in code but not documented, stale .env.example entries, MCP config mismatches between what the server expects and what setup/init generates.
9719
- - **Dead dependencies** \u2014 Packages in package.json that are no longer imported anywhere. These add install time and attack surface.
9720
- - **Stale prompts or instructions** \u2014 Cycle numbers, AD references, or project-state assumptions in prompts.ts or CLAUDE.md that no longer match reality.
9721
- - **Stage readiness gaps** \u2014 If the project is approaching or entering an access-widening stage (e.g. Alpha Distribution, Alpha Cohort, Public Launch), check that auth/security phases are complete. Stages that widen who can access the product must have auth hardening and security review as prerequisites \u2014 not post-hoc discoveries.
9722
- Report findings in a brief "Architecture Health" section in Part 1. If no issues found, skip the section entirely \u2014 do not write "No issues found".
9723
-
9724
- 10. **Discovery Canvas Audit** \u2014 If a Discovery Canvas section is provided in context, audit it for completeness and staleness. For each of the 5 canvas sections (Landscape & References, User Journeys, MVP Boundary, Assumptions & Open Questions, Success Signals):
9725
- - If the section is **empty** and the project has run 5+ cycles, flag it as a gap and suggest a specific enrichment prompt (e.g. "Consider defining your MVP boundary \u2014 what's in v1 and what's deferred?").
9726
- - If the section has content, assess whether it's still accurate given recent builds and decisions. Flag stale assumptions or outdated references.
9727
- - If no Discovery Canvas is provided in context, note that the canvas hasn't been initialized and recommend starting with the highest-value section for the project's maturity.
9728
- Report findings in a "Discovery Canvas Audit" section in Part 1. Persist findings in the \`discoveryGaps\` array in Part 2. If no gaps found, omit the section and use an empty array.
9729
-
9730
- 11. **Hierarchy Assessment** \u2014 If hierarchy data (Horizons \u2192 Stages \u2192 Phases with task counts) is provided in context, assess the full project structure:
9731
- **Phase-level:**
9732
- - A phase marked "In Progress" with all tasks Done \u2192 flag as ready to close.
9733
- - A phase marked "Done" with active Backlog/In Progress tasks \u2192 flag as incorrectly closed.
9734
- - A phase marked "Not Started" while later-ordered phases are active \u2192 flag as out-of-sequence.
9735
- - If builds in this review window created tasks that don't fit existing phases \u2192 suggest a new phase.
9736
- **Stage-level:**
9737
- - If all phases in a stage are Done \u2192 flag the stage as ready to complete. This is a significant milestone.
9738
- - If the current stage has been active for 15+ cycles \u2192 assess whether it should be split or whether progress is genuinely slow.
9739
- - If work is happening in phases that belong to a future stage while the current stage has incomplete phases \u2192 flag as scope leak.
9740
- **Horizon-level:**
9741
- - If all stages in the active horizon are complete \u2192 flag for Horizon Review (biggest-picture reflection).
9742
- - If no phase data is provided, skip this section.
9743
- Report findings in a "Hierarchy Assessment" section in Part 1. Persist findings in the \`stalePhases\` array in Part 2 (include stage/horizon observations too). If no issues found, omit the section and use an empty array.
9744
-
9745
- 12. **Structural Drift Detection** \u2014 If decision usage data is provided in context, identify structural decay using drift-based criteria (not pure cycle counts):
9746
- - **AD drift:** An AD is drifted when its content contradicts recent build evidence, references architecture/capabilities that no longer exist, or has been made redundant by newer ADs. Reference frequency is a secondary signal \u2014 an unreferenced AD that is still accurate is not necessarily stale; an AD referenced last cycle that contradicts shipped code IS drifted.
9747
- - **Carry-forward drift:** Carry-forward items that have persisted across **3+ cycles** without resolution \u2192 flag as stuck.
9748
- - **Confidence drift:** ADs with LOW confidence that have not gained supporting evidence within 5 cycles \u2192 flag as unvalidated. ADs where build reports contradict the decision \u2192 flag as confidence should decrease.
9749
- Use decision usage data as a secondary signal (unreferenced ADs are more likely to be drifted, but verify by checking content alignment). Report findings in a "Structural Drift" section in Part 1. Persist findings in the \`staleDecisions\` array in Part 2. If no issues found, omit the section and use an empty array.
9959
+ Note: Hierarchy assessment and structural drift detection are handled within section 5 (AD & Hierarchy Housekeeping). They do not need their own sections.
9750
9960
 
9751
9961
  ## OUTPUT FORMAT
9752
9962
 
9753
9963
  Your output has TWO parts:
9754
9964
 
9755
9965
  ### Part 1: Natural Language Output
9756
- Write your full Strategy Review in markdown. Cover the 6 mandatory sections in order:
9757
- 1. **Cycle-by-Cycle Impact Summary** \u2014 what was built and why it mattered
9758
- 2. **Horizon & Phase Progress** \u2014 current phase status, blockers, next phase readiness
9759
- 3. **New Tasks & Ideas** \u2014 count, alignment assessment, clustering patterns
9760
- 4. **What Changed Strategically** \u2014 decisions, direction shifts, carry-forward resolutions
9761
- 5. **North Star Validation** \u2014 still accurate? validated or stale?
9762
- 6. **Active Decision Review + Scoring** \u2014 per-AD assessment with scores
9763
-
9764
- Then include conditional sections only if relevant:
9966
+ Write your Strategy Review in markdown. Cover the 5 mandatory sections in order:
9967
+ 1. **What Got Built & Why It Matters** \u2014 compact cycle summaries (bullets, not essays)
9968
+ 2. **Product Gaps & User Experience** \u2014 THE MAIN EVENT. What's broken, confusing, or missing for users.
9969
+ 3. **Opportunities & Growth** \u2014 what could move the needle that we're not doing
9970
+ 4. **Strategic Direction Check** \u2014 brief North Star + carry-forward + brief update check
9971
+ 5. **AD & Hierarchy Housekeeping** \u2014 compact appendix of changes being made
9972
+
9973
+ Then include conditional sections only if genuinely useful:
9765
9974
  - **Security Posture Review** \u2014 only if [SECURITY] tags exist
9766
- - **Dogfood Friction \u2192 Tasks** \u2014 only if dogfood entries show recurring unaddressed friction
9767
- - **Architecture Health** \u2014 only if issues found
9768
- - **Discovery Canvas Audit** \u2014 only if gaps or staleness found
9769
- - **Hierarchy Assessment** \u2014 only if hierarchy staleness, phase closure, or stage progression signals detected
9770
- - **Structural Drift** \u2014 only if drifted ADs or stuck carry-forwards found${compressionPart1}
9975
+ - **Dogfood Friction \u2192 Tasks** \u2014 only if recurring unaddressed friction
9976
+ - **Architecture Health** \u2014 only if broken data paths or config drift found${compressionPart1}
9977
+
9978
+ **FORMAT GUIDELINES:**
9979
+ - The entire review should be readable in 3-5 minutes
9980
+ - Use bullet points and short paragraphs, not walls of text
9981
+ - Section 2 (Product Gaps) should be the longest section
9982
+ - Section 5 (AD Housekeeping) should be the shortest \u2014 just a change list
9771
9983
 
9772
9984
  ### Part 2: Structured Data Block
9773
9985
  After your natural language output, include this EXACT format on its own line:
@@ -9848,9 +10060,9 @@ Everything in Part 1 (natural language) is **display-only**. Part 2 (structured
9848
10060
 
9849
10061
  **If you analysed it in Part 1, it MUST appear in Part 2 to persist. Empty arrays/null = nothing saved.**
9850
10062
 
9851
- - Recommended AD changes in Part 1? \u2192 Put them in \`activeDecisionUpdates\` with full body including ### heading. Use \`delete\` action (with empty body) to permanently remove non-strategic ADs (implementation details, resolved decisions, library choices). Use \`supersede\` when a decision is replaced by a new one.
9852
- - Scored ADs in Part 1? \u2192 Put scores in \`decisionScores\` array with id, dimensions, and rationale
9853
- - Identified proven insights, direction changes, or deprecated approaches? \u2192 Put the full updated product brief in \`productBriefUpdates\`. This is how strategic learnings get locked into the project's institutional memory. Common triggers: a phase completing, a hypothesis being validated/invalidated, a new constraint emerging, or the North Star evolving.${compressionPersistence}
10063
+ - AD changes listed in section 5? \u2192 Put them in \`activeDecisionUpdates\` with full body including ### heading. Use \`delete\` action (with empty body) to permanently remove non-strategic ADs. Small changes (confidence bumps, stale deletions) don't need justification essays \u2014 just make them.
10064
+ - Only score ADs in \`decisionScores\` if the review surfaced a genuine strategic question about that AD. Do NOT score every AD \u2014 most reviews should have 0-3 scores at most.
10065
+ - Product brief needs updating? \u2192 Put the full updated brief in \`productBriefUpdates\`.${compressionPersistence}
9854
10066
  - Wrote a strategy review in Part 1? \u2192 \`sessionLogTitle\`, \`sessionLogContent\`, \`velocityAssessment\`, \`strategicRecommendations\` must all be populated
9855
10067
  - Made recommendations in Part 1? \u2192 Extract each into \`actionItems\` with a specific type (resolve/submit/close/investigate/defer) and target (AD-N, task-NNN, phase name, or null). Every recommendation must have an action item \u2014 this is how they get tracked and surfaced to the next plan
9856
10068
  - Converted dogfood friction to tasks in Part 1? \u2192 Each converted friction must appear as an \`actionItem\` with \`type: "submit"\`. If it's not in \`actionItems\`, it won't be tracked \u2014 the next plan will never see it
@@ -9944,6 +10156,9 @@ function buildReviewUserMessage(ctx) {
9944
10156
  if (ctx.unregisteredDocs) {
9945
10157
  parts.push("### Unregistered Docs", "", ctx.unregisteredDocs, "");
9946
10158
  }
10159
+ if (ctx.taskComments) {
10160
+ parts.push("### Task Discussion (Recent Comments)", "", ctx.taskComments, "");
10161
+ }
9947
10162
  return parts.join("\n");
9948
10163
  }
9949
10164
  function parseReviewStructuredOutput(raw) {
@@ -10657,10 +10872,15 @@ async function assembleContext(adapter2, mode, _config, filters, focus) {
10657
10872
  timings["getPlanContextSummary"] = t();
10658
10873
  if (leanSummary) {
10659
10874
  t = startTimer();
10660
- const [metricsSnapshots2, reviews2] = await Promise.all([
10875
+ let [metricsSnapshots2, reviews2] = await Promise.all([
10661
10876
  adapter2.readCycleMetrics(),
10662
10877
  adapter2.getRecentReviews(5)
10663
10878
  ]);
10879
+ try {
10880
+ const reports2 = await adapter2.getRecentBuildReports(50);
10881
+ metricsSnapshots2 = computeSnapshotsFromBuildReports(reports2);
10882
+ } catch {
10883
+ }
10664
10884
  timings["metricsAndReviews"] = t();
10665
10885
  t = startTimer();
10666
10886
  let reviewPatternsText2;
@@ -10729,7 +10949,7 @@ async function assembleContext(adapter2, mode, _config, filters, focus) {
10729
10949
  return { context: ctx2, contextHashes: newHashes2 };
10730
10950
  }
10731
10951
  t = startTimer();
10732
- const [decisions, reportsSinceCycle, log, tasks, metricsSnapshots, reviews, phases, dogfoodEntries] = await Promise.all([
10952
+ const [decisions, reportsSinceCycle, log, tasks, rawMetricsSnapshots, reviews, phases, dogfoodEntries] = await Promise.all([
10733
10953
  adapter2.getActiveDecisions(),
10734
10954
  adapter2.getBuildReportsSince(health.totalCycles),
10735
10955
  adapter2.getCycleLog(3),
@@ -10743,8 +10963,9 @@ async function assembleContext(adapter2, mode, _config, filters, focus) {
10743
10963
  const reports = reportsSinceCycle.length > 0 ? reportsSinceCycle : await adapter2.getRecentBuildReports(5);
10744
10964
  t = startTimer();
10745
10965
  let buildPatternsText;
10966
+ let allReportsForPatterns = [];
10746
10967
  try {
10747
- const allReportsForPatterns = await adapter2.getRecentBuildReports(50);
10968
+ allReportsForPatterns = await adapter2.getRecentBuildReports(50);
10748
10969
  const patterns = await detectBuildPatterns(allReportsForPatterns, health.totalCycles, 5);
10749
10970
  buildPatternsText = hasBuildPatterns(patterns) ? formatBuildPatterns(patterns) : void 0;
10750
10971
  } catch {
@@ -10764,6 +10985,7 @@ async function assembleContext(adapter2, mode, _config, filters, focus) {
10764
10985
  } catch {
10765
10986
  }
10766
10987
  timings["advisory"] = t();
10988
+ const metricsSnapshots = allReportsForPatterns.length > 0 ? computeSnapshotsFromBuildReports(allReportsForPatterns) : rawMetricsSnapshots.filter((s) => s.accuracy.length > 0 || s.velocity.length > 0);
10767
10989
  logDataSourceSummary("plan (full)", [
10768
10990
  { label: "cycleHealth", hasData: !!health },
10769
10991
  { label: "productBrief", hasData: warnIfEmpty("productBrief", productBrief) },
@@ -10822,7 +11044,7 @@ ${cleanContent}`;
10822
11044
  pendingRecIds = pending.map((r) => r.id);
10823
11045
  } catch {
10824
11046
  }
10825
- const handoffs = (data.cycleHandoffs ?? []).map((h) => {
11047
+ const handoffs2 = (data.cycleHandoffs ?? []).map((h) => {
10826
11048
  const parsed = parseBuildHandoff(h.buildHandoff);
10827
11049
  if (parsed && !parsed.createdAt) {
10828
11050
  parsed.createdAt = (/* @__PURE__ */ new Date()).toISOString();
@@ -10854,6 +11076,15 @@ ${cleanContent}`;
10854
11076
  });
10855
11077
  }
10856
11078
  const reviewedTaskIds = (data.boardCorrections ?? []).filter((c) => "priority" in c.updates).map((c) => c.taskId);
11079
+ let activeStageId;
11080
+ try {
11081
+ const activeStage = await adapter2.getActiveStage?.();
11082
+ activeStageId = activeStage?.id;
11083
+ } catch {
11084
+ }
11085
+ const effortMapLegacy = { XS: 1, S: 2, M: 3, L: 5, XL: 8 };
11086
+ const legacyTaskCount = handoffs2.length;
11087
+ const legacyEffortPoints = handoffs2.reduce((sum, h) => sum + (effortMapLegacy[h.handoff.effort] ?? 3), 0);
10857
11088
  const payload = {
10858
11089
  cycleNumber: newCycleNumber,
10859
11090
  cycleLog: {
@@ -10862,7 +11093,9 @@ ${cleanContent}`;
10862
11093
  title: cleanTitle,
10863
11094
  content: logBlock,
10864
11095
  carryForward: data.cycleLogCarryForward ?? void 0,
10865
- notes: data.cycleLogNotes ?? void 0
11096
+ notes: data.cycleLogNotes ?? void 0,
11097
+ taskCount: legacyTaskCount > 0 ? legacyTaskCount : void 0,
11098
+ effortPoints: legacyEffortPoints > 0 ? legacyEffortPoints : void 0
10866
11099
  },
10867
11100
  healthUpdates: {
10868
11101
  totalCycles: newCycleNumber,
@@ -10882,19 +11115,26 @@ ${cleanContent}`;
10882
11115
  cycle: newCycleNumber,
10883
11116
  createdCycle: newCycleNumber,
10884
11117
  why: t.why || "",
10885
- notes: t.notes || ""
10886
- })),
10887
- boardCorrections: (data.boardCorrections ?? []).map((c) => ({
10888
- taskId: c.taskId,
10889
- updates: c.updates
11118
+ notes: t.notes || "",
11119
+ stageId: activeStageId
10890
11120
  })),
11121
+ boardCorrections: (data.boardCorrections ?? []).map((c) => {
11122
+ const updates = c.updates;
11123
+ if ((updates.status === "Deferred" || updates.status === "Cancelled") && !updates.closureReason) {
11124
+ updates.closureReason = "No reason provided";
11125
+ }
11126
+ return {
11127
+ taskId: c.taskId,
11128
+ updates
11129
+ };
11130
+ }),
10891
11131
  phases,
10892
11132
  activeDecisions: (data.activeDecisions ?? []).map((ad) => ({
10893
11133
  id: ad.id,
10894
11134
  body: ad.body
10895
11135
  })),
10896
11136
  pendingRecommendationIds: pendingRecIds,
10897
- handoffs,
11137
+ handoffs: handoffs2,
10898
11138
  cycle,
10899
11139
  reviewedTaskIds
10900
11140
  };
@@ -10942,13 +11182,18 @@ async function writeBack(adapter2, _mode, cycleNumber, data, contextHashes) {
10942
11182
  const logBlock = `### Cycle ${newCycleNumber} \u2014 ${cleanTitle}
10943
11183
 
10944
11184
  ${cleanContent}`;
11185
+ const effortMap = { XS: 1, S: 2, M: 3, L: 5, XL: 8 };
11186
+ const cycleTaskCount = handoffs.length;
11187
+ const cycleEffortPoints = handoffs.reduce((sum, h) => sum + (effortMap[h.handoff.effort] ?? 3), 0);
10945
11188
  const cycleLogPromise = adapter2.writeCycleLogEntry({
10946
11189
  uuid: randomUUID7(),
10947
11190
  cycleNumber: newCycleNumber,
10948
11191
  title: cleanTitle,
10949
11192
  content: logBlock,
10950
11193
  carryForward: data.cycleLogCarryForward ?? void 0,
10951
- notes: data.cycleLogNotes ?? void 0
11194
+ notes: data.cycleLogNotes ?? void 0,
11195
+ taskCount: cycleTaskCount > 0 ? cycleTaskCount : void 0,
11196
+ effortPoints: cycleEffortPoints > 0 ? cycleEffortPoints : void 0
10952
11197
  });
10953
11198
  const healthPromise = adapter2.getCycleHealth().then(
10954
11199
  (health) => adapter2.setCycleHealth({
@@ -11024,6 +11269,9 @@ ${cleanContent}`;
11024
11269
  `${correction.taskId}: planner suggested priority change to ${_stripped} \u2014 blocked (reviewed task)`
11025
11270
  );
11026
11271
  }
11272
+ if ((updates.status === "Deferred" || updates.status === "Cancelled") && !updates.closureReason) {
11273
+ updates.closureReason = "No reason provided";
11274
+ }
11027
11275
  if (Object.keys(updates).length > 0) {
11028
11276
  await adapter2.updateTask(correction.taskId, updates);
11029
11277
  }
@@ -11213,7 +11461,8 @@ Run \`strategy_review\` first, or pass \`force: true\` to bypass this gate.`
11213
11461
  }
11214
11462
  return { mode, cycleNumber, strategyReviewWarning };
11215
11463
  }
11216
- async function processLlmOutput(adapter2, config2, rawOutput, mode, cycleNumber, contextHashes) {
11464
+ async function processLlmOutput(adapter2, config2, rawOutput, mode, cycleNumber, contextHashes, planRunMeta) {
11465
+ const applyStartMs = Date.now();
11217
11466
  const { displayText, data } = parseStructuredOutput(rawOutput);
11218
11467
  let resolvedDisplayText = displayText;
11219
11468
  let autoCommitNote = "";
@@ -11236,6 +11485,18 @@ async function processLlmOutput(adapter2, config2, rawOutput, mode, cycleNumber,
11236
11485
  for (const [placeholder, realId] of newTaskIdMap) {
11237
11486
  resolvedDisplayText = resolvedDisplayText.replaceAll(placeholder, realId);
11238
11487
  }
11488
+ if (adapter2.insertPlanRun) {
11489
+ const durationMs = Date.now() - applyStartMs + (planRunMeta?.prepareStartMs !== void 0 ? Date.now() - planRunMeta.prepareStartMs : 0);
11490
+ adapter2.insertPlanRun({
11491
+ cycleNumber: cycleNumber + 1,
11492
+ contextBytes: planRunMeta?.contextBytes,
11493
+ durationMs,
11494
+ taskCountIn: planRunMeta?.taskCountIn,
11495
+ taskCountOut: (writeSummary?.handoffs ?? 0) + (writeSummary?.newTasks ?? 0),
11496
+ backlogDepth: planRunMeta?.backlogDepth
11497
+ }).catch(() => {
11498
+ });
11499
+ }
11239
11500
  if (mode === "bootstrap") {
11240
11501
  try {
11241
11502
  await adapter2.appendToolMetric({
@@ -11321,7 +11582,7 @@ async function preparePlan(adapter2, config2, filters, focus, force) {
11321
11582
  contextHashes
11322
11583
  };
11323
11584
  }
11324
- async function applyPlan(adapter2, config2, rawLlmOutput, mode, cycleNumber, strategyReviewWarning, contextHashes) {
11585
+ async function applyPlan(adapter2, config2, rawLlmOutput, mode, cycleNumber, strategyReviewWarning, contextHashes, planRunMeta) {
11325
11586
  const applyTimer = startTimer();
11326
11587
  console.error(`[plan-perf] applyPlan: start (llm_response=${rawLlmOutput.length} chars)`);
11327
11588
  const APPLY_TIMEOUT_MS = 12e4;
@@ -11330,7 +11591,7 @@ async function applyPlan(adapter2, config2, rawLlmOutput, mode, cycleNumber, str
11330
11591
  );
11331
11592
  const workPromise = (async () => {
11332
11593
  if (config2.adapterType === "pg") {
11333
- const result = await processLlmOutput(adapter2, config2, rawLlmOutput, mode, cycleNumber, contextHashes);
11594
+ const result = await processLlmOutput(adapter2, config2, rawLlmOutput, mode, cycleNumber, contextHashes, planRunMeta);
11334
11595
  const applyMs2 = applyTimer();
11335
11596
  console.error(`[plan-perf] applyPlan: total=${applyMs2}ms (pg, no git sync)`);
11336
11597
  return { ...result, pullNote: "", strategyReviewWarning };
@@ -11338,7 +11599,7 @@ async function applyPlan(adapter2, config2, rawLlmOutput, mode, cycleNumber, str
11338
11599
  const syncResult = await withBaseBranchSync(
11339
11600
  { projectRoot: config2.projectRoot, baseBranch: config2.baseBranch, abortOnConflict: true },
11340
11601
  async () => {
11341
- return processLlmOutput(adapter2, config2, rawLlmOutput, mode, cycleNumber, contextHashes);
11602
+ return processLlmOutput(adapter2, config2, rawLlmOutput, mode, cycleNumber, contextHashes, planRunMeta);
11342
11603
  }
11343
11604
  );
11344
11605
  if (syncResult.abort) {
@@ -11549,6 +11810,7 @@ function formatPlanResult(result) {
11549
11810
  if (result.priorityLockNote) lines.push(result.priorityLockNote.trim());
11550
11811
  if (result.slackWarning) lines.push(result.slackWarning);
11551
11812
  if (result.autoCommitNote) lines.push(result.autoCommitNote.trim());
11813
+ lines.push("", `Next: run \`build_list\` to see your cycle tasks, then \`build_execute <task_id>\` to start building.`);
11552
11814
  const response = textResponse(lines.join("\n"));
11553
11815
  if (result.contextUtilisation !== void 0) {
11554
11816
  return { ...response, _contextUtilisation: result.contextUtilisation };
@@ -11579,7 +11841,7 @@ async function handlePlan(adapter2, config2, args) {
11579
11841
  lastPrepareContextHashes = void 0;
11580
11842
  lastPrepareUserMessage = void 0;
11581
11843
  lastPrepareContextBytes = void 0;
11582
- const result = await applyPlan(adapter2, config2, llmResponse, planMode, cycleNumber, strategyReviewWarning, contextHashes);
11844
+ const result = await applyPlan(adapter2, config2, llmResponse, planMode, cycleNumber, strategyReviewWarning, contextHashes, { contextBytes: contextBytes ?? void 0 });
11583
11845
  let utilisation;
11584
11846
  if (inputContext) {
11585
11847
  try {
@@ -11890,7 +12152,7 @@ ${phaseSummary}`);
11890
12152
  return parts.join("\n");
11891
12153
  }
11892
12154
  function formatTaskCompact(t) {
11893
- const notesSnippet = t.notes ? t.notes.slice(0, 200) + (t.notes.length > 200 ? "..." : "") : "";
12155
+ const notesSnippet = t.notes ?? "";
11894
12156
  return `- **${t.id}:** ${t.title}
11895
12157
  Status: ${t.status} | Priority: ${t.priority} | Complexity: ${t.complexity}
11896
12158
  Module: ${t.module} | Epic: ${t.epic} | Phase: ${t.phase} | Owner: ${t.owner}
@@ -12125,6 +12387,39 @@ ${unregistered.slice(0, 10).map((f) => `- ${f}`).join("\n")}`;
12125
12387
  }
12126
12388
  } catch {
12127
12389
  }
12390
+ let taskCommentsText;
12391
+ try {
12392
+ const comments = await adapter2.getRecentTaskComments?.(50);
12393
+ if (comments && comments.length > 0) {
12394
+ const doneTaskIds = new Set(recentDoneTasks.map((t) => t.id));
12395
+ const inReviewTasks = activeTasks.filter((t) => t.status === "In Review");
12396
+ const reviewWindowTaskIds = /* @__PURE__ */ new Set([
12397
+ ...doneTaskIds,
12398
+ ...inReviewTasks.map((t) => t.id)
12399
+ ]);
12400
+ const filtered = comments.filter((c) => reviewWindowTaskIds.has(c.taskId));
12401
+ if (filtered.length > 0) {
12402
+ const lines = [];
12403
+ const byTask = /* @__PURE__ */ new Map();
12404
+ for (const c of filtered) {
12405
+ const group = byTask.get(c.taskId) ?? [];
12406
+ group.push(c);
12407
+ byTask.set(c.taskId, group);
12408
+ }
12409
+ for (const [taskId, taskComments] of byTask) {
12410
+ lines.push(`**${taskId}:**`);
12411
+ for (const c of taskComments) {
12412
+ const date = c.createdAt ? c.createdAt.slice(0, 10) : "";
12413
+ lines.push(` - [${c.author}${date ? `, ${date}` : ""}] ${c.content}`);
12414
+ }
12415
+ }
12416
+ const raw = lines.join("\n");
12417
+ const MAX_CHARS = 8e3;
12418
+ taskCommentsText = raw.length > MAX_CHARS ? "(older comments truncated)\n" + raw.slice(raw.length - MAX_CHARS) : raw;
12419
+ }
12420
+ }
12421
+ } catch {
12422
+ }
12128
12423
  logDataSourceSummary("strategy_review_audit", [
12129
12424
  { label: "discoveryCanvas", hasData: discoveryCanvasText !== void 0 },
12130
12425
  { label: "briefImplications", hasData: briefImplicationsText !== void 0 },
@@ -12135,7 +12430,8 @@ ${unregistered.slice(0, 10).map((f) => `- ${f}`).join("\n")}`;
12135
12430
  { label: "adHocCommits", hasData: adHocCommitsText !== void 0 },
12136
12431
  { label: "registeredDocs", hasData: registeredDocsText !== void 0 },
12137
12432
  { label: "recentPlans", hasData: recentPlansText !== void 0 },
12138
- { label: "unregisteredDocs", hasData: unregisteredDocsText !== void 0 }
12433
+ { label: "unregisteredDocs", hasData: unregisteredDocsText !== void 0 },
12434
+ { label: "taskComments", hasData: taskCommentsText !== void 0 }
12139
12435
  ]);
12140
12436
  const context = {
12141
12437
  sessionNumber: cycleNumber,
@@ -12160,7 +12456,8 @@ ${unregistered.slice(0, 10).map((f) => `- ${f}`).join("\n")}`;
12160
12456
  pendingRecommendations: pendingRecsText,
12161
12457
  registeredDocs: registeredDocsText,
12162
12458
  recentPlans: recentPlansText,
12163
- unregisteredDocs: unregisteredDocsText
12459
+ unregisteredDocs: unregisteredDocsText,
12460
+ taskComments: taskCommentsText
12164
12461
  };
12165
12462
  const BUDGET_SOFT2 = 5e4;
12166
12463
  const BUDGET_HARD2 = 6e4;
@@ -12292,7 +12589,7 @@ ${cleanContent}`;
12292
12589
  if (ad.action === "delete" && adapter2.deleteActiveDecision) {
12293
12590
  await adapter2.deleteActiveDecision(ad.id);
12294
12591
  } else {
12295
- await adapter2.updateActiveDecision(ad.id, ad.body, cycleNumber);
12592
+ await adapter2.updateActiveDecision(ad.id, ad.body, cycleNumber, ad.action);
12296
12593
  }
12297
12594
  const eventType = ad.action === "delete" ? "deleted" : ad.action === "confidence_change" ? "confidence_changed" : ad.action === "supersede" ? "superseded" : ad.action === "new" ? "created" : "modified";
12298
12595
  try {
@@ -12366,6 +12663,41 @@ ${cleanContent}`;
12366
12663
  } catch {
12367
12664
  }
12368
12665
  }
12666
+ try {
12667
+ const canvas = await adapter2.readDiscoveryCanvas();
12668
+ const updates = {};
12669
+ let populatedSections = [];
12670
+ if (!canvas.landscapeReferences || canvas.landscapeReferences.length === 0) {
12671
+ if (data.activeDecisionUpdates?.length) {
12672
+ const entries = data.activeDecisionUpdates.filter((ad) => ad.body && ad.action !== "delete").slice(0, 3).map((ad) => ({ name: ad.id, notes: ad.body.slice(0, 200) }));
12673
+ if (entries.length > 0) {
12674
+ updates.landscapeReferences = entries;
12675
+ populatedSections.push("Landscape & References");
12676
+ }
12677
+ }
12678
+ }
12679
+ if (!canvas.mvpBoundary && data.strategicDirection) {
12680
+ updates.mvpBoundary = `Auto-inferred from strategy review: ${data.strategicDirection}`;
12681
+ populatedSections.push("MVP Boundary");
12682
+ }
12683
+ if ((!canvas.assumptionsOpenQuestions || canvas.assumptionsOpenQuestions.length === 0) && data.discoveryGaps?.length) {
12684
+ updates.assumptionsOpenQuestions = data.discoveryGaps.map((gap) => ({
12685
+ text: gap.suggestion,
12686
+ status: "open",
12687
+ evidence: `Auto-inferred: ${gap.section} was ${gap.status}`
12688
+ }));
12689
+ populatedSections.push("Assumptions & Open Questions");
12690
+ }
12691
+ if ((!canvas.successSignals || canvas.successSignals.length === 0) && data.velocityAssessment) {
12692
+ updates.successSignals = [{ signal: "Velocity trend", metric: "tasks/cycle", target: data.velocityAssessment.slice(0, 200) }];
12693
+ populatedSections.push("Success Signals");
12694
+ }
12695
+ if (populatedSections.length > 0) {
12696
+ await adapter2.updateDiscoveryCanvas(updates);
12697
+ }
12698
+ data._canvasPopulated = populatedSections;
12699
+ } catch {
12700
+ }
12369
12701
  try {
12370
12702
  const [phases, boardTasks] = await Promise.all([
12371
12703
  adapter2.readPhases(),
@@ -12417,9 +12749,18 @@ ${report}`;
12417
12749
  }
12418
12750
  } catch {
12419
12751
  }
12752
+ let canvasSection = "";
12753
+ if (data) {
12754
+ const populated = data._canvasPopulated;
12755
+ if (populated && populated.length > 0) {
12756
+ canvasSection = `
12757
+
12758
+ **Discovery Canvas Auto-Populated:** ${populated.join(", ")}`;
12759
+ }
12760
+ }
12420
12761
  const fullText = phaseChanges?.length ? `${displayText}
12421
12762
 
12422
- ${formatPhaseChanges(phaseChanges)}${valueReportSection}` : `${displayText}${valueReportSection}`;
12763
+ ${formatPhaseChanges(phaseChanges)}${valueReportSection}${canvasSection}` : `${displayText}${valueReportSection}${canvasSection}`;
12423
12764
  return {
12424
12765
  cycleNumber,
12425
12766
  displayText: fullText,
@@ -12731,7 +13072,7 @@ ${cleanContent}`;
12731
13072
  if (ad.action === "delete" && adapter2.deleteActiveDecision) {
12732
13073
  await adapter2.deleteActiveDecision(ad.id);
12733
13074
  } else {
12734
- await adapter2.updateActiveDecision(ad.id, ad.body, cycleNumber);
13075
+ await adapter2.updateActiveDecision(ad.id, ad.body, cycleNumber, ad.action);
12735
13076
  }
12736
13077
  const eventType = ad.action === "delete" ? "deleted" : ad.action === "confidence_change" ? "confidence_changed" : ad.action === "supersede" ? "superseded" : ad.action === "new" ? "created" : "modified";
12737
13078
  try {
@@ -13532,12 +13873,34 @@ async function handleBoardEdit(adapter2, args) {
13532
13873
  updates.cycle = null;
13533
13874
  if (!changes.includes("cycle")) changes.push("cycle \u2192 cleared");
13534
13875
  }
13876
+ let autoAssignedCycle = null;
13877
+ if (updates.status === "In Cycle") {
13878
+ const health = await adapter2.getCycleHealth().catch(() => null);
13879
+ const activeCycle = health?.totalCycles ?? null;
13880
+ if (activeCycle != null && activeCycle > 0) {
13881
+ updates.cycle = activeCycle;
13882
+ autoAssignedCycle = activeCycle;
13883
+ if (!changes.includes("cycle")) changes.push(`cycle \u2192 ${activeCycle}`);
13884
+ }
13885
+ }
13535
13886
  await adapter2.updateTask(taskId, updates);
13887
+ if ((updates.status === "Done" || updates.status === "Cancelled") && adapter2.updateDogfoodEntryStatus) {
13888
+ try {
13889
+ const dogfoodLog = await adapter2.getDogfoodLog?.(50) ?? [];
13890
+ const linked = dogfoodLog.filter((e) => e.linkedTaskId === taskId || e.linkedTaskId === task.id);
13891
+ const newStatus = updates.status === "Done" ? "actioned" : "dismissed";
13892
+ await Promise.all(linked.map((e) => adapter2.updateDogfoodEntryStatus(e.id, newStatus)));
13893
+ } catch {
13894
+ }
13895
+ }
13536
13896
  const lines = [
13537
13897
  `Updated **${taskId}** (${updates.title ?? task.title})`,
13538
13898
  "",
13539
13899
  `**Changes:** ${changes.map((f) => `${f} \u2192 ${String(updates[f])}`).join(", ")}`
13540
13900
  ];
13901
+ if (autoAssignedCycle != null) {
13902
+ lines.push("", `Auto-assigned to Cycle ${autoAssignedCycle}.`);
13903
+ }
13541
13904
  return textResponse(lines.join("\n"));
13542
13905
  } catch (err) {
13543
13906
  return errorResponse(err instanceof Error ? err.message : String(err));
@@ -13878,6 +14241,7 @@ These rules come from 80+ cycles of dogfooding. They prevent the most common sou
13878
14241
  - **Audit before widening access.** Before any build that adds endpoints, modifies auth/RLS, introduces new user types, or changes access controls \u2014 review the security implications first. Fix findings before shipping.
13879
14242
  - **Flag access-widening changes.** If a build touches auth, RLS policies, API keys, or user-facing access, note "Security surface reviewed" in the build report's \`discovered_issues\` or \`architecture_notes\`.
13880
14243
  - **Never ship secrets.** Do not commit .env files, API keys, or credentials. Check \`.gitignore\` covers sensitive files before pushing.
14244
+ - **Telemetry opt-out.** PAPI collects anonymous usage data (tool name, duration, project ID). To disable, add \`"PAPI_TELEMETRY": "off"\` to the \`env\` block in your \`.mcp.json\`.
13881
14245
 
13882
14246
  ### Planning & Scope
13883
14247
  - **Don't ask premature questions.** If the project is in early cycles, don't ask about deployment accounts, hosting providers, OAuth setup, or commercial features. Focus on building core functionality first.
@@ -14031,6 +14395,44 @@ var DOCS_INDEX_TEMPLATE = `# {{project_name}} \u2014 Document Index
14031
14395
  ### Consumer column
14032
14396
  Who or what reads this doc: \`planner\`, \`builder\`, \`strategy-review\`, \`user\`, \`onboarding\`.
14033
14397
  `;
14398
+ var CURSOR_RULES_TEMPLATE = `---
14399
+ description: PAPI \u2014 Persistent Agentic Planning Intelligence
14400
+ globs:
14401
+ alwaysApply: true
14402
+ ---
14403
+
14404
+ # {{project_name}} \u2014 PAPI Workflow
14405
+
14406
+ PAPI gives your project structured planning, building, and reviewing. The agent manages the cycle workflow automatically.
14407
+
14408
+ ## Session Start
14409
+
14410
+ Run \`orient\` at the start of every session to get cycle state, in-progress tasks, and recommended next action.
14411
+
14412
+ ## The Cycle
14413
+
14414
+ \`plan\` \u2192 \`build_list\` \u2192 \`build_execute\` (start) \u2192 implement \u2192 \`build_execute\` (complete) \u2192 \`review_list\` \u2192 \`review_submit\` \u2192 \`release\`
14415
+
14416
+ - **plan** \u2014 Generate BUILD HANDOFFs for the next cycle. Run once per cycle.
14417
+ - **build_execute \\<task-id\\>** (start) \u2014 Create feature branch, return BUILD HANDOFF.
14418
+ - **build_execute \\<task-id\\>** (complete) \u2014 Submit build report after implementing.
14419
+ - **review_submit** \u2014 Accept, request changes, or reject completed builds.
14420
+ - **release** \u2014 Merge all done tasks when the cycle is complete.
14421
+
14422
+ ## Quick Commands
14423
+
14424
+ - \`orient\` \u2014 Current cycle state and recommended next action
14425
+ - \`idea\` \u2014 Add a task or bug to the backlog
14426
+ - \`board_view\` \u2014 See all active tasks
14427
+ - \`health\` \u2014 Deep project metrics
14428
+
14429
+ ## Rules
14430
+
14431
+ - Run tools automatically \u2014 don't ask the user to invoke MCP tools manually
14432
+ - Stay within BUILD HANDOFF scope \u2014 don't add unrequested features
14433
+ - Commit per task \u2014 traceable git history
14434
+ - After build_execute completes: run \`review_submit\` with the verdict before presenting for human review
14435
+ `;
14034
14436
 
14035
14437
  // src/services/setup.ts
14036
14438
  var FILE_TEMPLATES = {
@@ -14112,6 +14514,23 @@ async function scaffoldPapiDir(adapter2, config2, input) {
14112
14514
  } catch {
14113
14515
  }
14114
14516
  }
14517
+ const cursorDir = join3(config2.projectRoot, ".cursor");
14518
+ let cursorDetected = false;
14519
+ try {
14520
+ await access2(cursorDir);
14521
+ cursorDetected = true;
14522
+ } catch {
14523
+ }
14524
+ if (cursorDetected) {
14525
+ const cursorRulesDir = join3(cursorDir, "rules");
14526
+ const cursorRulesPath = join3(cursorRulesDir, "papi.mdc");
14527
+ await mkdir(cursorRulesDir, { recursive: true });
14528
+ try {
14529
+ await access2(cursorRulesPath);
14530
+ } catch {
14531
+ scaffoldFiles[cursorRulesPath] = substitute(CURSOR_RULES_TEMPLATE, vars);
14532
+ }
14533
+ }
14115
14534
  for (const [filepath, content] of Object.entries(scaffoldFiles)) {
14116
14535
  await writeFile2(filepath, content, "utf-8");
14117
14536
  }
@@ -14512,11 +14931,18 @@ async function applySetup(adapter2, config2, input, briefText, adSeedText, conve
14512
14931
  });
14513
14932
  } catch {
14514
14933
  }
14934
+ let cursorScaffolded = false;
14935
+ try {
14936
+ await access2(join3(config2.projectRoot, ".cursor", "rules", "papi.mdc"));
14937
+ cursorScaffolded = true;
14938
+ } catch {
14939
+ }
14515
14940
  return {
14516
14941
  createdProject,
14517
14942
  projectName: input.projectName,
14518
14943
  seededAds,
14519
- createdTasks
14944
+ createdTasks,
14945
+ cursorScaffolded
14520
14946
  };
14521
14947
  }
14522
14948
 
@@ -14624,13 +15050,16 @@ ${result.seededAds} Active Decision${result.seededAds > 1 ? "s" : ""} seeded bas
14624
15050
 
14625
15051
  ${result.createdTasks} initial backlog task${result.createdTasks > 1 ? "s" : ""} created from codebase analysis.` : "";
14626
15052
  const constraintsHint = !constraints ? '\n\nTip: consider adding `constraints` (e.g. "must use PostgreSQL", "HIPAA compliant", "offline-first") to improve Active Decision seeding.' : "";
15053
+ const editorNote = result.cursorScaffolded ? "\n\nCursor detected \u2014 `.cursor/rules/papi.mdc` scaffolded alongside CLAUDE.md." : "";
14627
15054
  return textResponse(
14628
- `${prefix}Product Brief generated and saved.${adNote}${taskNote}${constraintsHint}
15055
+ `${prefix}Product Brief generated and saved.${adNote}${taskNote}${constraintsHint}${editorNote}
14629
15056
 
14630
15057
  **Important:** Setup created/modified files (CLAUDE.md, .claude/settings.json, docs/). Commit these changes before running \`build_execute\` \u2014 it requires a clean working directory.
14631
15058
 
14632
15059
  Tip: See \`docs/templates/example-project-brief.md\` for an example of a well-written brief.
14633
15060
 
15061
+ **Telemetry notice:** PAPI collects anonymous usage data (tool name, duration, project ID) to improve the product. No code, file contents, or personal data is collected. To opt out, add \`"PAPI_TELEMETRY": "off"\` to the \`env\` block in your \`.mcp.json\`.
15062
+
14634
15063
  Next step: run \`plan\` to start your first planning cycle.`
14635
15064
  );
14636
15065
  }
@@ -15022,6 +15451,52 @@ async function completeBuild(adapter2, config2, taskId, input, options = {}) {
15022
15451
  if (adIds.length > 0) report.relatedDecisions = adIds;
15023
15452
  }
15024
15453
  await adapter2.appendBuildReport(report);
15454
+ if (adapter2.appendCycleLearnings) {
15455
+ const learnings = [];
15456
+ const taskModule = task.module ?? "";
15457
+ const adIds = report.relatedDecisions ?? [];
15458
+ const extractLearning = (text, category) => {
15459
+ if (!text || text === "None" || text.trim().length === 0) return;
15460
+ const dotIdx = text.indexOf(". ");
15461
+ const summary = dotIdx > 0 && dotIdx < 120 ? text.slice(0, dotIdx + 1) : text.slice(0, 120);
15462
+ const tags = [];
15463
+ if (taskModule) tags.push(taskModule.toLowerCase());
15464
+ let severity;
15465
+ if (category === "issue") {
15466
+ const sevMatch = text.match(/^(P[0-3])[\s:]/);
15467
+ if (sevMatch) severity = sevMatch[1];
15468
+ }
15469
+ learnings.push({
15470
+ taskId: task.id,
15471
+ cycleNumber,
15472
+ category,
15473
+ severity,
15474
+ summary,
15475
+ detail: text,
15476
+ tags,
15477
+ relatedDecision: adIds[0]
15478
+ });
15479
+ };
15480
+ extractLearning(input.surprises, "surprise");
15481
+ extractLearning(input.discoveredIssues, "issue");
15482
+ extractLearning(input.architectureNotes, "architecture");
15483
+ extractLearning(input.deadEnds, "dead_end");
15484
+ if (input.scopeAccuracy && input.scopeAccuracy !== "accurate") {
15485
+ learnings.push({
15486
+ taskId: task.id,
15487
+ cycleNumber,
15488
+ category: "estimation_miss",
15489
+ summary: `${input.scopeAccuracy}: effort was ${input.effort} vs estimated ${input.estimatedEffort}`,
15490
+ tags: taskModule ? [taskModule.toLowerCase()] : []
15491
+ });
15492
+ }
15493
+ if (learnings.length > 0) {
15494
+ try {
15495
+ await adapter2.appendCycleLearnings(learnings);
15496
+ } catch {
15497
+ }
15498
+ }
15499
+ }
15025
15500
  if (report.relatedDecisions && report.relatedDecisions.length > 0) {
15026
15501
  for (const adId of report.relatedDecisions) {
15027
15502
  try {
@@ -15137,6 +15612,80 @@ async function cancelBuild(adapter2, taskId, reason) {
15137
15612
  return { task, reason };
15138
15613
  }
15139
15614
 
15615
+ // src/tools/module-instructions.ts
15616
+ var MODULE_INSTRUCTIONS = {
15617
+ Dashboard: `**\u26A0\uFE0F MANDATORY \u2014 Dashboard Module Rules (skip = rework)**
15618
+
15619
+ **STEP 1 \u2014 BEFORE writing any code:**
15620
+ Read \`.impeccable.md\` in full. It contains the palette, typography system, audience definitions, brand personality, and design principles. Every visual decision must reference this file. If your output contradicts .impeccable.md, it is wrong.
15621
+
15622
+ Typography rule: **Mono font is for task IDs and code only.** All labels, titles, column headers, status text, and body copy use sans-serif. Four-role type system: Display (40\u201356px, gradient) / Heading (18\u201322px, semibold) / Body (14\u201316px, sans) / Mono (12\u201313px, IDs and code only).
15623
+
15624
+ **STEP 2 \u2014 Use the \`frontend-design\` skill for ALL visual implementation.**
15625
+ DO NOT write CSS, Tailwind classes, or component markup manually. Run the frontend-design skill and let it generate the visual layer. Manual styling produces generic output that fails review every time. This is non-negotiable.
15626
+ For M/L tasks: use the full toolchain \u2014 Playground (design preview) \u2192 Frontend-design (build) \u2192 Playwright (verify). The playground output is the quality bar. If the build looks worse than the playground, the build is wrong.
15627
+
15628
+ **STEP 3 \u2014 Visual quality checklist (verify before committing):**
15629
+ - [ ] Font sizes match the page context \u2014 don't use tiny text (xs/2xs) when surrounding components use sm/base
15630
+ - [ ] Alignment and spacing are consistent \u2014 no orphaned margins, no elements floating off-grid
15631
+ - [ ] Color usage follows .impeccable.md \u2014 stat values use primary/accent colors, NOT gray. Status colors are applied to status indicators.
15632
+ - [ ] Content has hierarchy \u2014 primary info is large and colored, secondary is smaller and muted, tertiary is behind interaction
15633
+ - [ ] No raw task IDs without names, no abbreviations without tooltips, no jargon a non-dev wouldn't understand
15634
+ - [ ] Percentages and numbers have context \u2014 "96%" means nothing without "96% of what" and "is that good?"
15635
+ - [ ] Empty states guide the user with next actions, not just "No data"
15636
+ - [ ] Text doesn't truncate unless there's a clear expand/tooltip interaction
15637
+ - [ ] The component looks intentional next to its siblings \u2014 same surface levels, border treatment, spacing rhythm
15638
+ - [ ] Provide localhost URL or screenshot to user for visual review
15639
+
15640
+ **STEP 4 \u2014 Production concerns:**
15641
+ - New API fetches need cache headers \u2014 check existing pattern in route.ts
15642
+ - Collapsed/hidden sections must lazy-load data, not fetch on page mount
15643
+ - No duplicate data fetching \u2014 check if polling, Realtime, or page load already covers this data
15644
+ - Consider Vercel free tier limits before adding polling or subscriptions`,
15645
+ "MCP Server": `**MODULE INSTRUCTIONS \u2014 MCP Server**
15646
+ - Test changes locally before shipping \u2014 run the tool you modified against a real project.
15647
+ - For tools with prepare\u2192apply flows (plan, strategy_review), verify the full round-trip works.
15648
+ - Don't break existing tool input signatures \u2014 external users depend on them.
15649
+ - Build the server after changes: \`npm run build --workspace=@papi-ai/server\`
15650
+ - Check that tool descriptions are clear \u2014 LLMs read these to decide when and how to call the tool.
15651
+ - If modifying prompts.ts, verify the planner LLM output still matches the expected structured format.
15652
+ - Service logic goes in services/, tool handler + formatting goes in tools/ \u2014 keep the separation clean.`,
15653
+ Core: `**MODULE INSTRUCTIONS \u2014 Core**
15654
+ - Check adapter-pg implementation, not adapter-md. adapter-md is legacy.
15655
+ - Verify the full write\u2192DB\u2192read\u2192consumer path for any data changes.
15656
+ - Run migrations on dev before prod. Test with \`execute_sql\` via Supabase MCP.
15657
+ - When adding adapter interface methods, implement in BOTH adapter-md and adapter-pg.
15658
+ - Build order matters: adapter-md \u2192 adapter-pg \u2192 server.
15659
+ - .papi/ files may be stale \u2014 DB via MCP tools is the source of truth.`,
15660
+ Auth: `**MODULE INSTRUCTIONS \u2014 Auth**
15661
+ - NEVER expose the Supabase service role key in client-side code or API responses.
15662
+ - Test with both authenticated and unauthenticated requests.
15663
+ - Check RLS policies \u2014 service role key bypasses all RLS, so app-layer checks are critical.
15664
+ - /admin and /api/telemetry MUST be owner-only (cathalos92), never any authenticated user.
15665
+ - Multi-provider OAuth: GitHub + Email active, Google in backlog. Test all active providers.`,
15666
+ Architecture: `**MODULE INSTRUCTIONS \u2014 Architecture**
15667
+ - Research tasks produce docs, not code. Deliverable is a findings document.
15668
+ - Save docs to docs/research/ or docs/architecture/ with frontmatter (title, type, created, cycle, status).
15669
+ - Register docs via \`doc_register\` after finalization \u2014 not during drafting.
15670
+ - Do NOT submit follow-up ideas to backlog until the owner confirms findings are actionable.
15671
+ - Include absolute dates and cycle numbers in all documents for future LLM context.`,
15672
+ Data: `**MODULE INSTRUCTIONS \u2014 Data**
15673
+ - SQL views and functions run in Supabase \u2014 test via \`execute_sql\` before deploying.
15674
+ - Verify query performance on real data volumes, not just test data.
15675
+ - Cross-project queries must filter by project_id to prevent data leakage.
15676
+ - When creating views, ensure they respect the same access patterns as the underlying tables.`
15677
+ };
15678
+ function getModuleInstructions(module) {
15679
+ if (!module) return "";
15680
+ const instructions = MODULE_INSTRUCTIONS[module];
15681
+ if (!instructions) return "";
15682
+ return `
15683
+
15684
+ ---
15685
+
15686
+ ${instructions}`;
15687
+ }
15688
+
15140
15689
  // src/tools/build.ts
15141
15690
  var buildListTool = {
15142
15691
  name: "build_list",
@@ -15192,15 +15741,15 @@ var buildExecuteTool = {
15192
15741
  },
15193
15742
  surprises: {
15194
15743
  type: "string",
15195
- description: 'Unexpected complexity or findings. Use "None" if none. Required for complete.'
15744
+ description: 'What was DIFFERENT from what you expected? Scope changes, wrong assumptions, unexpected complexity, or missing infrastructure. NOT implementation details. Good: "Assumed table had status column but it uses state \u2014 required migration." Bad: "Used CSS keyframes instead of library." If your surprise is about implementation mechanics (which library, which pattern), it is not a surprise \u2014 use "None". Required for complete.'
15196
15745
  },
15197
15746
  discovered_issues: {
15198
15747
  type: "string",
15199
- description: 'New bugs or tasks found during the build. Use "None" if none. Required for complete.'
15748
+ description: `Problems found DURING this build that are OUTSIDE this task's scope. Include severity (P0-P3). Good: "P2: Auth middleware doesn't validate token expiry \u2014 affects all protected routes." Bad: "Had to install a dependency." Only real bugs or gaps that need their own task. Use "None" if none. Required for complete.`
15200
15749
  },
15201
15750
  architecture_notes: {
15202
15751
  type: "string",
15203
- description: 'Changes that should update Active Decisions or Product Brief. Use "None" if none. Required for complete.'
15752
+ description: 'Patterns established or decisions made during this build that AFFECT FUTURE WORK. Good: "Created shared service layer for cockpit data \u2014 all cockpit components should use it." Bad: "Used React hooks." Only include if future builders need to know this to avoid rework or follow the established pattern. Use "None" if none. Required for complete.'
15204
15753
  },
15205
15754
  scope_accuracy: {
15206
15755
  type: "string",
@@ -15281,6 +15830,30 @@ function filterRelevantADs(ads, task) {
15281
15830
  return keywords.some((kw) => text.includes(kw));
15282
15831
  });
15283
15832
  }
15833
+ async function getModuleContext(adapter2, task) {
15834
+ if (!task.module) return "";
15835
+ try {
15836
+ const [reports, allTasks] = await Promise.all([
15837
+ adapter2.getRecentBuildReports(30),
15838
+ adapter2.queryBoard()
15839
+ ]);
15840
+ const taskModuleMap = new Map(allTasks.map((t) => [t.id, t.module]));
15841
+ const moduleReports = reports.filter((r) => taskModuleMap.get(r.taskId) === task.module).slice(0, 5);
15842
+ if (moduleReports.length === 0) return "";
15843
+ const lines = ["\n\n---\n\n**RECENT MODULE CONTEXT** (last builds in " + task.module + "):"];
15844
+ for (const r of moduleReports) {
15845
+ const parts = [`- **${r.taskId}:** ${r.taskName} (Cycle ${r.cycle})`];
15846
+ if (r.surprises && r.surprises !== "None") parts.push(` Surprises: ${r.surprises}`);
15847
+ if (r.discoveredIssues && r.discoveredIssues !== "None") parts.push(` Issues: ${r.discoveredIssues}`);
15848
+ if (r.architectureNotes && r.architectureNotes !== "None") parts.push(` Architecture: ${r.architectureNotes}`);
15849
+ if (parts.length > 1) lines.push(parts.join("\n"));
15850
+ }
15851
+ if (lines.length <= 1) return "";
15852
+ return lines.join("\n");
15853
+ } catch {
15854
+ return "";
15855
+ }
15856
+ }
15284
15857
  function formatRelevantADs(ads) {
15285
15858
  if (ads.length === 0) return "";
15286
15859
  const lines = ["\n\n---\n\n**ACTIVE DECISIONS (relevant):**"];
@@ -15383,7 +15956,7 @@ async function handleBuildExecute(adapter2, config2, args) {
15383
15956
  **PRE-BUILD VERIFICATION:** Before writing any code, read these files and check if the functionality already exists:
15384
15957
  ${verificationFiles.map((f) => `- ${f}`).join("\n")}
15385
15958
  If >80% of the scope is already implemented, call \`build_execute\` with completed="yes" and note "already built" in surprises instead of re-implementing.` : "";
15386
- const chainInstruction = "\n\n---\n\n**IMPORTANT:** After implementing this task, immediately call `build_execute` again with report fields (`completed`, `effort`, `estimated_effort`, `surprises`, `discovered_issues`, `architecture_notes`) to complete the build. Do not wait for user confirmation.";
15959
+ const chainInstruction = '\n\n---\n\n**IMPORTANT:** After implementing this task, immediately call `build_execute` again with report fields (`completed`, `effort`, `estimated_effort`, `surprises`, `discovered_issues`, `architecture_notes`) to complete the build. Do not wait for user confirmation.\n\n**Build Report Quality Bar:**\n- **surprises**: What was DIFFERENT from expected \u2014 wrong assumptions, scope changes, missing infrastructure. NOT implementation mechanics ("used X library").\n- **discovered_issues**: Bugs/gaps OUTSIDE this task\'s scope, with severity (P0-P3). NOT "had to install a dependency".\n- **architecture_notes**: Patterns/decisions that AFFECT FUTURE WORK. NOT "used React hooks".\n- If nothing meaningful to report for a field, use "None" \u2014 empty signal is better than noise.';
15387
15960
  let adSection = "";
15388
15961
  try {
15389
15962
  const allADs = await adapter2.getActiveDecisions();
@@ -15391,7 +15964,9 @@ If >80% of the scope is already implemented, call \`build_execute\` with complet
15391
15964
  adSection = formatRelevantADs(relevant);
15392
15965
  } catch {
15393
15966
  }
15394
- return textResponse(header + serializeBuildHandoff(result.task.buildHandoff) + adSection + verificationNote + chainInstruction + phaseNote);
15967
+ const moduleInstructions = getModuleInstructions(result.task.module);
15968
+ const moduleContext = await getModuleContext(adapter2, result.task);
15969
+ return textResponse(header + serializeBuildHandoff(result.task.buildHandoff) + adSection + moduleInstructions + moduleContext + verificationNote + chainInstruction + phaseNote);
15395
15970
  } catch (err) {
15396
15971
  if (isNoHandoffError(err)) {
15397
15972
  const lines = [
@@ -15501,22 +16076,39 @@ function formatCompleteResult(result) {
15501
16076
  if (result.docWarning) {
15502
16077
  lines.push("", `\u{1F4C4} ${result.docWarning}`);
15503
16078
  }
16079
+ const isResearchTask = result.task.title.toLowerCase().startsWith("research:") || result.task.taskType === "research" || result.task.complexity === "Research";
16080
+ if (result.completed === "yes" && isResearchTask) {
16081
+ lines.push(
16082
+ "",
16083
+ "---",
16084
+ "",
16085
+ "## \u26A0\uFE0F Research Task Complete \u2014 Review Gate",
16086
+ "",
16087
+ "Before submitting follow-up ideas to the backlog:",
16088
+ "",
16089
+ "1. **Draft a findings doc** and save it to `docs/` (e.g. `docs/research/[topic]-findings.md`)",
16090
+ "2. **Surface it to the owner** \u2014 share the doc path and key takeaways for review",
16091
+ "3. **Only submit backlog ideas after the owner confirms** the findings are actionable",
16092
+ "",
16093
+ "Do NOT call `idea` for follow-up tasks until step 3 is complete."
16094
+ );
16095
+ }
15504
16096
  const hasDiscoveredIssues = result.discoveredIssues !== "None" && result.discoveredIssues.trim() !== "";
15505
16097
  const remaining = result.cycleProgress.total - result.cycleProgress.completed;
15506
16098
  if (result.completed !== "yes") {
15507
16099
  lines.push("", `Next: run \`build_execute ${result.task.id}\` again with updated report fields to complete this task.`);
15508
16100
  } else if (remaining > 0) {
15509
16101
  if (hasDiscoveredIssues) {
15510
- lines.push("", `Next: discovered issues logged \u2014 next \`plan\` will triage them. Run \`build_list\` to see ${remaining} remaining cycle task${remaining === 1 ? "" : "s"}, or \`plan\` to replan.`);
16102
+ lines.push("", `Next: discovered issues logged \u2014 next \`plan\` will triage them. Run \`build_list\` to see ${remaining} remaining cycle task${remaining === 1 ? "" : "s"}, then \`build_execute <task_id>\` to start the next one.`);
15511
16103
  } else {
15512
16104
  lines.push("", `Next: run \`build_list\` to see ${remaining} remaining cycle task${remaining === 1 ? "" : "s"}, then \`build_execute <task_id>\` to start the next one.`);
15513
16105
  }
15514
16106
  lines.push("", "*Tip: Start a new chat for your next build to keep context fresh and avoid hitting context window limits.*");
15515
16107
  } else {
15516
16108
  if (hasDiscoveredIssues) {
15517
- lines.push("", "All cycle tasks complete. Discovered issues logged \u2014 run `plan` to start the next cycle and triage them.");
16109
+ lines.push("", "All cycle tasks complete. Discovered issues logged \u2014 run `review_list` then `review_submit` to review builds, then `release` to close the cycle.");
15518
16110
  } else {
15519
- lines.push("", "All cycle tasks complete. Run `plan` to start the next planning cycle.");
16111
+ lines.push("", "All cycle tasks complete. Run `review_list` then `review_submit` to review builds, then `release` to close the cycle.");
15520
16112
  }
15521
16113
  lines.push("", "*Tip: Start a new chat for your next session to keep context fresh.*");
15522
16114
  }
@@ -15797,8 +16389,21 @@ ${lines.join("\n")}
15797
16389
  createdCycle: health.totalCycles,
15798
16390
  notes: input.notes || "",
15799
16391
  taskType: "idea",
15800
- maturity: "raw"
16392
+ maturity: "raw",
16393
+ docRef: input.docRef
15801
16394
  });
16395
+ if (input.notes && adapter2.updateDogfoodEntryStatus) {
16396
+ const dogfoodRefs = input.notes.match(/dogfood:([a-f0-9-]+)/gi);
16397
+ if (dogfoodRefs) {
16398
+ for (const ref of dogfoodRefs) {
16399
+ const entryId = ref.split(":")[1];
16400
+ try {
16401
+ await adapter2.updateDogfoodEntryStatus(entryId, "backlog-created", task.id);
16402
+ } catch {
16403
+ }
16404
+ }
16405
+ }
16406
+ }
15802
16407
  return { routing: "task", task, message: `${task.id}: "${task.title}" \u2014 added to backlog` };
15803
16408
  }
15804
16409
  var CANVAS_SECTION_LABELS = {
@@ -15880,6 +16485,10 @@ var ideaTool = {
15880
16485
  force: {
15881
16486
  type: "boolean",
15882
16487
  description: "Force creation even if a high-overlap duplicate or already-done task is detected. Default: false."
16488
+ },
16489
+ doc_ref: {
16490
+ type: "string",
16491
+ description: 'Path to a reference document (e.g. "docs/research/foo.md"). Stored as a structured field \u2014 replaces the fragile "Reference:" line in notes.'
15883
16492
  }
15884
16493
  },
15885
16494
  required: ["text"]
@@ -15906,7 +16515,8 @@ async function handleIdea(adapter2, config2, args) {
15906
16515
  complexity: args.complexity,
15907
16516
  notes: rawNotes,
15908
16517
  discovery: args.discovery === true,
15909
- force: args.force === true
16518
+ force: args.force === true,
16519
+ docRef: args.doc_ref?.trim()
15910
16520
  };
15911
16521
  const useGit = isGitAvailable() && isGitRepo(config2.projectRoot);
15912
16522
  const currentBranch = useGit ? getCurrentBranch(config2.projectRoot) : null;
@@ -16827,42 +17437,11 @@ async function getHealthSummary(adapter2) {
16827
17437
  let metricsSection;
16828
17438
  let derivedMetricsSection = "";
16829
17439
  try {
16830
- let snapshots = await adapter2.readCycleMetrics();
16831
- snapshots = snapshots.filter(
16832
- (s) => s.accuracy.length > 0 || s.velocity.length > 0
16833
- );
16834
- if (snapshots.length === 0) {
16835
- try {
16836
- const reports = await adapter2.getRecentBuildReports(50);
16837
- if (reports.length > 0) {
16838
- const byCycleMap = /* @__PURE__ */ new Map();
16839
- for (const r of reports) {
16840
- const existing = byCycleMap.get(r.cycle) ?? [];
16841
- existing.push(r);
16842
- byCycleMap.set(r.cycle, existing);
16843
- }
16844
- const effortMap = { XS: 1, S: 2, M: 3, L: 5, XL: 8 };
16845
- for (const [sn, cycleReports] of byCycleMap) {
16846
- const completed = cycleReports.filter((r) => r.completed === "Yes").length;
16847
- const total = cycleReports.length;
16848
- const withEffort = cycleReports.filter((r) => r.estimatedEffort && r.actualEffort);
16849
- const accurate = withEffort.filter((r) => r.estimatedEffort === r.actualEffort).length;
16850
- const matchRate = withEffort.length > 0 ? Math.round(accurate / withEffort.length * 100) : 0;
16851
- let effortPoints = 0;
16852
- for (const r of cycleReports) {
16853
- effortPoints += effortMap[r.actualEffort] ?? 3;
16854
- }
16855
- snapshots.push({
16856
- cycle: sn,
16857
- date: (/* @__PURE__ */ new Date()).toISOString(),
16858
- accuracy: [{ cycle: sn, reports: total, matchRate, mae: 0, bias: 0 }],
16859
- velocity: [{ cycle: sn, completed, partial: 0, failed: total - completed, effortPoints }]
16860
- });
16861
- }
16862
- snapshots.sort((a, b2) => a.cycle - b2.cycle);
16863
- }
16864
- } catch {
16865
- }
17440
+ let snapshots = [];
17441
+ try {
17442
+ const reports = await adapter2.getRecentBuildReports(50);
17443
+ snapshots = computeSnapshotsFromBuildReports(reports);
17444
+ } catch {
16866
17445
  }
16867
17446
  metricsSection = formatCycleMetrics(snapshots);
16868
17447
  derivedMetricsSection = formatDerivedMetrics(snapshots, activeTasks);
@@ -17240,6 +17819,7 @@ async function handleRelease(adapter2, config2, args) {
17240
17819
  }
17241
17820
  } catch {
17242
17821
  }
17822
+ lines.push("", `Next: cycle released! Run \`plan\` to start your next planning cycle.`);
17243
17823
  return textResponse(lines.join("\n"));
17244
17824
  } catch (err) {
17245
17825
  return errorResponse(err instanceof Error ? err.message : String(err));
@@ -17661,13 +18241,23 @@ Run \`plan\` to create Cycle ${result.currentCycle + 1}.`;
17661
18241
  const autoReviewNote = autoReview ? `
17662
18242
 
17663
18243
  **Auto-Review:** ${autoReview.verdict} \u2014 ${autoReview.summary}` + (autoReview.findings.length > 0 ? ` (${autoReview.findings.length} finding${autoReview.findings.length === 1 ? "" : "s"})` : "") : "";
18244
+ let nextStepNote = "";
18245
+ if (!autoReleaseNote && stage === "build-acceptance") {
18246
+ if (verdict === "accept") {
18247
+ nextStepNote = "\n\nNext: run `review_list` to check for more pending reviews, or `release` when all reviews are done.";
18248
+ } else {
18249
+ nextStepNote = `
18250
+
18251
+ Next: address the feedback, then run \`build_execute ${taskId}\` to resubmit.`;
18252
+ }
18253
+ }
17664
18254
  return textResponse(
17665
18255
  `**${result.stageLabel}** recorded for ${result.taskId}.
17666
18256
 
17667
18257
  - **Verdict:** ${result.verdict}
17668
18258
  - **Comments:** ${result.comments}
17669
18259
 
17670
- ${statusNote}${autoReviewNote}${unblockNote}${regenNote}${mergeNote}${slackNote}${autoReleaseNote}${phaseNote}`
18260
+ ${statusNote}${autoReviewNote}${unblockNote}${regenNote}${mergeNote}${slackNote}${autoReleaseNote}${nextStepNote}${phaseNote}`
17671
18261
  );
17672
18262
  } catch (err) {
17673
18263
  return errorResponse(err instanceof Error ? err.message : String(err));
@@ -17817,6 +18407,9 @@ function formatOrientSummary(health, buildInfo, hierarchy) {
17817
18407
  if (hierarchy.phasesNearingClosure.length > 0) {
17818
18408
  lines.push(`**Nearing Closure:** ${hierarchy.phasesNearingClosure.join(", ")}`);
17819
18409
  }
18410
+ if (hierarchy.stageExitCriteria && hierarchy.stageExitCriteria.length > 0) {
18411
+ lines.push(`**Stage Exit Criteria:** ${hierarchy.stageExitCriteria.map((c) => `[ ] ${c}`).join(" | ")}`);
18412
+ }
17820
18413
  lines.push("");
17821
18414
  }
17822
18415
  if (health.northStarSection) {
@@ -17917,7 +18510,8 @@ async function getHierarchyPosition(adapter2) {
17917
18510
  horizon: activeHorizon.label,
17918
18511
  stage: activeStage.label,
17919
18512
  activePhases: activePhases.map((p) => p.label),
17920
- phasesNearingClosure: nearingClosure.map((p) => p.label)
18513
+ phasesNearingClosure: nearingClosure.map((p) => p.label),
18514
+ stageExitCriteria: activeStage.exitCriteria
17921
18515
  };
17922
18516
  } catch {
17923
18517
  return void 0;
@@ -18020,7 +18614,17 @@ ${versionDrift}` : "";
18020
18614
  enrichmentNote = enrichClaudeMd(config2.projectRoot, healthResult.cycleNumber);
18021
18615
  } catch {
18022
18616
  }
18023
- return textResponse(formatOrientSummary(healthResult, buildInfo, hierarchy) + ttfvNote + recsNote + pendingReviewNote + versionNote + enrichmentNote);
18617
+ let patternsNote = "";
18618
+ try {
18619
+ const patterns = await adapter2.getCycleLearningPatterns?.();
18620
+ if (patterns && patterns.length > 0) {
18621
+ const top = patterns.slice(0, 3).map((p) => `${p.tag} (${p.frequency} cycles)`).join(", ");
18622
+ patternsNote = `
18623
+ **Recurring patterns:** ${top}`;
18624
+ }
18625
+ } catch {
18626
+ }
18627
+ return textResponse(formatOrientSummary(healthResult, buildInfo, hierarchy) + ttfvNote + recsNote + pendingReviewNote + patternsNote + versionNote + enrichmentNote);
18024
18628
  } catch (err) {
18025
18629
  const message = err instanceof Error ? err.message : String(err);
18026
18630
  return errorResponse(`Orient failed: ${message}`);
@@ -18050,7 +18654,7 @@ function enrichClaudeMd(projectRoot, cycleNumber) {
18050
18654
  // src/tools/hierarchy.ts
18051
18655
  var hierarchyUpdateTool = {
18052
18656
  name: "hierarchy_update",
18053
- description: "Update the status of a phase, stage, or horizon in the project hierarchy (AD-14). Accepts a level (phase, stage, or horizon), a name or ID, and a new status. Does not call the Anthropic API.",
18657
+ description: "Update the status of a phase, stage, or horizon in the project hierarchy (AD-14). Accepts a level (phase, stage, or horizon), a name or ID, and a new status. For stages, optionally set exit_criteria \u2014 a checklist defining when the stage is considered done. Does not call the Anthropic API.",
18054
18658
  inputSchema: {
18055
18659
  type: "object",
18056
18660
  properties: {
@@ -18067,9 +18671,14 @@ var hierarchyUpdateTool = {
18067
18671
  type: "string",
18068
18672
  enum: ["active", "completed", "deferred"],
18069
18673
  description: "The new status to set."
18674
+ },
18675
+ exit_criteria: {
18676
+ type: "array",
18677
+ items: { type: "string" },
18678
+ description: 'Checklist defining when this stage is done (stages only). Each item is a completion condition, e.g. "All P0 tasks shipped". Replaces existing criteria.'
18070
18679
  }
18071
18680
  },
18072
- required: ["level", "name", "status"]
18681
+ required: ["level", "name"]
18073
18682
  }
18074
18683
  };
18075
18684
  var VALID_STATUSES3 = /* @__PURE__ */ new Set(["active", "completed", "deferred"]);
@@ -18077,15 +18686,22 @@ async function handleHierarchyUpdate(adapter2, args) {
18077
18686
  const level = args.level;
18078
18687
  const name = args.name;
18079
18688
  const status = args.status;
18080
- if (!level || !name || !status) {
18081
- return errorResponse("Missing required parameters: level, name, status.");
18689
+ const exitCriteria = args.exit_criteria;
18690
+ if (!level || !name) {
18691
+ return errorResponse("Missing required parameters: level, name.");
18692
+ }
18693
+ if (!status && !exitCriteria) {
18694
+ return errorResponse("Nothing to update. Provide at least one of: status, exit_criteria.");
18082
18695
  }
18083
18696
  if (level !== "phase" && level !== "stage" && level !== "horizon") {
18084
18697
  return errorResponse(`Invalid level "${level}". Must be "phase", "stage", or "horizon".`);
18085
18698
  }
18086
- if (!VALID_STATUSES3.has(status)) {
18699
+ if (status && !VALID_STATUSES3.has(status)) {
18087
18700
  return errorResponse(`Invalid status "${status}". Must be one of: active, completed, deferred.`);
18088
18701
  }
18702
+ if (exitCriteria !== void 0 && level !== "stage") {
18703
+ return errorResponse("exit_criteria can only be set on stages.");
18704
+ }
18089
18705
  try {
18090
18706
  if (level === "phase") {
18091
18707
  if (!adapter2.readPhases || !adapter2.updatePhaseStatus) {
@@ -18107,7 +18723,7 @@ async function handleHierarchyUpdate(adapter2, args) {
18107
18723
  return textResponse(`Phase updated: **${phase.label}** ${oldStatus2} \u2192 ${status}`);
18108
18724
  }
18109
18725
  if (level === "stage") {
18110
- if (!adapter2.readStages || !adapter2.updateStageStatus) {
18726
+ if (!adapter2.readStages) {
18111
18727
  return errorResponse("Stage management is not supported by the current adapter.");
18112
18728
  }
18113
18729
  const stages = await adapter2.readStages();
@@ -18118,12 +18734,28 @@ async function handleHierarchyUpdate(adapter2, args) {
18118
18734
  const available = stages.map((s) => s.label).join(", ");
18119
18735
  return errorResponse(`Stage "${name}" not found. Available stages: ${available || "none"}`);
18120
18736
  }
18121
- if (stage.status === status) {
18122
- return textResponse(`Stage "${stage.label}" is already "${status}". No change made.`);
18737
+ const resultLines = [];
18738
+ if (status) {
18739
+ if (!adapter2.updateStageStatus) {
18740
+ return errorResponse("Stage status updates are not supported by the current adapter.");
18741
+ }
18742
+ if (stage.status === status) {
18743
+ resultLines.push(`Stage "${stage.label}" is already "${status}".`);
18744
+ } else {
18745
+ const oldStatus2 = stage.status;
18746
+ await adapter2.updateStageStatus(stage.id, status);
18747
+ resultLines.push(`Stage updated: **${stage.label}** ${oldStatus2} \u2192 ${status}`);
18748
+ }
18749
+ }
18750
+ if (exitCriteria !== void 0) {
18751
+ if (!adapter2.updateStageExitCriteria) {
18752
+ return errorResponse("Exit criteria updates are not supported by the current adapter.");
18753
+ }
18754
+ await adapter2.updateStageExitCriteria(stage.id, exitCriteria);
18755
+ resultLines.push(`Exit criteria set (${exitCriteria.length} item${exitCriteria.length !== 1 ? "s" : ""}):`);
18756
+ exitCriteria.forEach((c) => resultLines.push(` - ${c}`));
18123
18757
  }
18124
- const oldStatus2 = stage.status;
18125
- await adapter2.updateStageStatus(stage.id, status);
18126
- return textResponse(`Stage updated: **${stage.label}** ${oldStatus2} \u2192 ${status}`);
18758
+ return textResponse(resultLines.join("\n"));
18127
18759
  }
18128
18760
  if (!adapter2.readHorizons || !adapter2.updateHorizonStatus) {
18129
18761
  return errorResponse("Horizon management is not supported by the current adapter.");
@@ -18698,11 +19330,87 @@ async function handleDocScan(adapter2, config2, args) {
18698
19330
  return textResponse(lines.join("\n"));
18699
19331
  }
18700
19332
 
19333
+ // src/tools/sibling-ads.ts
19334
+ var getSiblingAdsTool = {
19335
+ name: "get_sibling_ads",
19336
+ description: "Read Active Decisions from sibling PAPI projects that share the same Supabase instance. Requires PAPI_SIBLING_PROJECT_IDS env var (comma-separated project UUIDs). Returns ADs labelled by source project \u2014 useful for cross-project architectural alignment. pg adapter only \u2014 returns an error if using md or proxy adapter.",
19337
+ inputSchema: {
19338
+ type: "object",
19339
+ properties: {
19340
+ project_ids: {
19341
+ type: "array",
19342
+ items: { type: "string" },
19343
+ description: "Optional explicit list of sibling project UUIDs to query. If omitted, falls back to PAPI_SIBLING_PROJECT_IDS env var."
19344
+ }
19345
+ },
19346
+ required: []
19347
+ }
19348
+ };
19349
+ async function handleGetSiblingAds(adapter2, args) {
19350
+ if (!adapter2.getSiblingAds) {
19351
+ return errorResponse(
19352
+ "get_sibling_ads is only available with the pg adapter (direct Supabase connection). Set PAPI_ENDPOINT or DATABASE_URL to enable."
19353
+ );
19354
+ }
19355
+ let projectIds = [];
19356
+ const argIds = args["project_ids"];
19357
+ if (Array.isArray(argIds) && argIds.length > 0) {
19358
+ projectIds = argIds.filter((id) => typeof id === "string");
19359
+ } else {
19360
+ const envIds = process.env["PAPI_SIBLING_PROJECT_IDS"];
19361
+ if (envIds) {
19362
+ projectIds = envIds.split(",").map((id) => id.trim()).filter(Boolean);
19363
+ }
19364
+ }
19365
+ if (projectIds.length === 0) {
19366
+ return errorResponse(
19367
+ "No sibling project IDs provided. Pass project_ids in the tool call, or set PAPI_SIBLING_PROJECT_IDS=<uuid1>,<uuid2> in your .mcp.json env."
19368
+ );
19369
+ }
19370
+ const ads = await adapter2.getSiblingAds(projectIds);
19371
+ if (ads.length === 0) {
19372
+ return textResponse(
19373
+ `No active decisions found for sibling projects: ${projectIds.join(", ")}
19374
+
19375
+ Check that the project IDs are correct and exist in the same Supabase instance.`
19376
+ );
19377
+ }
19378
+ const byProject = /* @__PURE__ */ new Map();
19379
+ for (const ad of ads) {
19380
+ const group = byProject.get(ad.sourceProjectId) ?? [];
19381
+ group.push(ad);
19382
+ byProject.set(ad.sourceProjectId, group);
19383
+ }
19384
+ const lines = ["# Sibling Project Active Decisions", ""];
19385
+ lines.push(`**Source projects:** ${projectIds.join(", ")}`);
19386
+ lines.push(`**Total ADs:** ${ads.length}`);
19387
+ lines.push("");
19388
+ for (const [projectId, projectAds] of byProject) {
19389
+ lines.push(`## Project: \`${projectId}\``);
19390
+ lines.push(`*${projectAds.length} active decision${projectAds.length !== 1 ? "s" : ""}*`);
19391
+ lines.push("");
19392
+ for (const ad of projectAds) {
19393
+ lines.push(`### ${ad.displayId}: ${ad.title} [Confidence: ${ad.confidence.toUpperCase()}]`);
19394
+ if (ad.body) {
19395
+ const excerpt = ad.body.length > 300 ? ad.body.slice(0, 300) + "\u2026" : ad.body;
19396
+ lines.push(excerpt);
19397
+ }
19398
+ lines.push("");
19399
+ }
19400
+ }
19401
+ lines.push("---");
19402
+ lines.push(
19403
+ "_These ADs are from sibling projects and are read-only. To share learnings, reference relevant ADs in your task notes or update your own ADs accordingly._"
19404
+ );
19405
+ return textResponse(lines.join("\n"));
19406
+ }
19407
+
18701
19408
  // src/lib/telemetry.ts
18702
19409
  var TELEMETRY_SUPABASE_URL = "https://guewgygcpcmrcoppihzx.supabase.co";
18703
19410
  var TELEMETRY_API_KEY = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Imd1ZXdneWdjcGNtcmNvcHBpaHp4Iiwicm9sZSI6ImFub24iLCJpYXQiOjE3NzI2Njk2NTMsImV4cCI6MjA4ODI0NTY1M30.V5Jw7wJgiMpSQPa2mt0ftjyye5ynG1qLlam00yPVNJY";
18704
19411
  function isEnabled() {
18705
- return process.env["PAPI_TELEMETRY"] !== "false";
19412
+ const val = process.env["PAPI_TELEMETRY"];
19413
+ return val !== "false" && val !== "off";
18706
19414
  }
18707
19415
  function emitTelemetryEvent(event) {
18708
19416
  if (!isEnabled()) return;
@@ -18769,8 +19477,58 @@ var TOOLS_REQUIRING_PAPI = /* @__PURE__ */ new Set([
18769
19477
  function createServer(adapter2, config2) {
18770
19478
  const server2 = new Server(
18771
19479
  { name: "papi", version: "0.1.0" },
18772
- { capabilities: { tools: {} } }
19480
+ { capabilities: { tools: {}, prompts: {} } }
18773
19481
  );
19482
+ const __filename = fileURLToPath(import.meta.url);
19483
+ const __dirname = dirname(__filename);
19484
+ const skillsDir = join9(__dirname, "..", "skills");
19485
+ function parseSkillFrontmatter(content) {
19486
+ const match = content.match(/^---\n([\s\S]*?)\n---/);
19487
+ if (!match) return null;
19488
+ const fm = match[1];
19489
+ const nameMatch = fm.match(/^name:\s*(.+)$/m);
19490
+ const descMatch = fm.match(/^description:\s*[>|]?\n?([\s\S]*?)(?=\n\w|\n---)/m);
19491
+ if (!nameMatch) return null;
19492
+ const name = nameMatch[1].trim();
19493
+ const description = descMatch ? descMatch[1].replace(/^\s{2}/gm, "").replace(/\n+$/, "").replace(/\n/g, " ").trim() : "";
19494
+ return { name, description };
19495
+ }
19496
+ server2.setRequestHandler(ListPromptsRequestSchema, async () => {
19497
+ try {
19498
+ const files = await readdir2(skillsDir);
19499
+ const mdFiles = files.filter((f) => f.endsWith(".md"));
19500
+ const prompts = [];
19501
+ for (const file of mdFiles) {
19502
+ const content = await readFile5(join9(skillsDir, file), "utf-8");
19503
+ const meta = parseSkillFrontmatter(content);
19504
+ if (meta) {
19505
+ prompts.push({ name: meta.name, description: meta.description });
19506
+ }
19507
+ }
19508
+ return { prompts };
19509
+ } catch {
19510
+ return { prompts: [] };
19511
+ }
19512
+ });
19513
+ server2.setRequestHandler(GetPromptRequestSchema, async (request) => {
19514
+ const { name } = request.params;
19515
+ try {
19516
+ const files = await readdir2(skillsDir);
19517
+ for (const file of files.filter((f) => f.endsWith(".md"))) {
19518
+ const content = await readFile5(join9(skillsDir, file), "utf-8");
19519
+ const meta = parseSkillFrontmatter(content);
19520
+ if (meta?.name === name) {
19521
+ const body = content.replace(/^---\n[\s\S]*?\n---\n*/, "");
19522
+ return {
19523
+ description: meta.description,
19524
+ messages: [{ role: "user", content: { type: "text", text: body } }]
19525
+ };
19526
+ }
19527
+ }
19528
+ } catch {
19529
+ }
19530
+ throw new Error(`Prompt not found: ${name}`);
19531
+ });
18774
19532
  server2.setRequestHandler(ListToolsRequestSchema, async () => ({
18775
19533
  tools: [
18776
19534
  planTool,
@@ -18799,7 +19557,8 @@ function createServer(adapter2, config2) {
18799
19557
  zoomOutTool,
18800
19558
  docRegisterTool,
18801
19559
  docSearchTool,
18802
- docScanTool
19560
+ docScanTool,
19561
+ getSiblingAdsTool
18803
19562
  ]
18804
19563
  }));
18805
19564
  server2.setRequestHandler(CallToolRequestSchema, async (request) => {
@@ -18912,6 +19671,9 @@ function createServer(adapter2, config2) {
18912
19671
  case "doc_scan":
18913
19672
  result = await handleDocScan(adapter2, config2, safeArgs);
18914
19673
  break;
19674
+ case "get_sibling_ads":
19675
+ result = await handleGetSiblingAds(adapter2, safeArgs);
19676
+ break;
18915
19677
  default:
18916
19678
  return { content: [{ type: "text", text: `Unknown tool: ${name}` }] };
18917
19679
  }