@papi-ai/server 0.5.1 → 0.5.3

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
@@ -845,28 +845,6 @@ function parseCostSnapshots(content) {
845
845
  }
846
846
  return snapshots;
847
847
  }
848
- function serializeCostSnapshot(snapshot) {
849
- return `| ${snapshot.cycle} | ${snapshot.date} | ${snapshot.totalCostUsd.toFixed(4)} | ${formatNumber(snapshot.totalInputTokens)} | ${formatNumber(snapshot.totalOutputTokens)} | ${formatNumber(snapshot.totalCalls)} |`;
850
- }
851
- function writeCostSnapshotToContent(snapshot, content) {
852
- if (!content.includes(COST_SECTION_HEADING)) {
853
- return content.trimEnd() + "\n\n" + COST_SECTION_HEADING + "\n\n" + COST_TABLE_HEADER + "\n" + COST_TABLE_SEPARATOR + "\n" + serializeCostSnapshot(snapshot) + "\n";
854
- }
855
- const lines = content.split("\n");
856
- const cyclePrefix = `| ${snapshot.cycle} |`;
857
- let replaced = false;
858
- for (let i = 0; i < lines.length; i++) {
859
- if (lines[i].startsWith(cyclePrefix)) {
860
- lines[i] = serializeCostSnapshot(snapshot);
861
- replaced = true;
862
- break;
863
- }
864
- }
865
- if (replaced) {
866
- return lines.join("\n");
867
- }
868
- return content.trimEnd() + "\n" + serializeCostSnapshot(snapshot) + "\n";
869
- }
870
848
  function effortOrdinal(effort) {
871
849
  const normalized = effort.trim().toUpperCase();
872
850
  return EFFORT_SCALE[normalized];
@@ -1416,7 +1394,7 @@ async function detectReviewPatterns(reviews, currentCycle, window = 5, clusterer
1416
1394
  function hasReviewPatterns(patterns) {
1417
1395
  return patterns.recurringFeedback.length > 0 || patterns.requestChangesRate >= 50;
1418
1396
  }
1419
- var VALID_TRANSITIONS2, TASK_TYPE_TIERS, VALID_EFFORT_SIZES, SECTION_HEADERS, YAML_MARKER, YAML_START, YAML_END, VALID_EFFORT_SIZES2, HEADER_SENTINEL, TABLE_HEADER, TABLE_SEPARATOR, PREV_TABLE_HEADER, LEGACY_TABLE_HEADER, SECTION_HEADING, FILE_TEMPLATE, COST_SECTION_HEADING, COST_TABLE_HEADER, COST_TABLE_SEPARATOR, FILE_HEADING, ACCURACY_HEADER, ACCURACY_SEPARATOR, VELOCITY_HEADER, VELOCITY_SEPARATOR, EFFORT_SCALE, NONE_PATTERN, HEADER_SENTINEL2, VALID_STAGES, VALID_VERDICTS, STAGE_DISPLAY, VALID_STATUSES, PHASES_START, PHASES_END, YAML_MARKER2, YAML_START2, YAML_END2, VALID_STATUSES2, YAML_MARKER3, YAML_START3, YAML_END3, MdFileAdapter, NONE_PATTERN2;
1397
+ var VALID_TRANSITIONS2, TASK_TYPE_TIERS, VALID_EFFORT_SIZES, SECTION_HEADERS, YAML_MARKER, YAML_START, YAML_END, VALID_EFFORT_SIZES2, HEADER_SENTINEL, TABLE_HEADER, TABLE_SEPARATOR, PREV_TABLE_HEADER, LEGACY_TABLE_HEADER, SECTION_HEADING, FILE_TEMPLATE, COST_SECTION_HEADING, COST_TABLE_SEPARATOR, FILE_HEADING, ACCURACY_HEADER, ACCURACY_SEPARATOR, VELOCITY_HEADER, VELOCITY_SEPARATOR, EFFORT_SCALE, NONE_PATTERN, HEADER_SENTINEL2, VALID_STAGES, VALID_VERDICTS, STAGE_DISPLAY, VALID_STATUSES, PHASES_START, PHASES_END, YAML_MARKER2, YAML_START2, YAML_END2, VALID_STATUSES2, YAML_MARKER3, YAML_START3, YAML_END3, MdFileAdapter, NONE_PATTERN2;
1420
1398
  var init_dist2 = __esm({
1421
1399
  "../adapter-md/dist/index.js"() {
1422
1400
  "use strict";
@@ -1457,7 +1435,6 @@ ${TABLE_HEADER}
1457
1435
  ${TABLE_SEPARATOR}
1458
1436
  `;
1459
1437
  COST_SECTION_HEADING = "## Cost Summary";
1460
- COST_TABLE_HEADER = "| Cycle | Date | Total Cost ($) | Input Tokens | Output Tokens | Calls |";
1461
1438
  COST_TABLE_SEPARATOR = "|--------|------|----------------|--------------|---------------|-------|";
1462
1439
  FILE_HEADING = "# Cycle Methodology Metrics";
1463
1440
  ACCURACY_HEADER = "| Cycle | Reports | Match Rate | MAE | Bias |";
@@ -1738,6 +1715,11 @@ ${TABLE_SEPARATOR}
1738
1715
  const reports = parseBuildReports(await this.read("BUILD_REPORTS.md"));
1739
1716
  return reports.slice(0, count);
1740
1717
  }
1718
+ /** Return the number of build reports for a specific task. */
1719
+ async getBuildReportCountForTask(taskId) {
1720
+ const reports = parseBuildReports(await this.read("BUILD_REPORTS.md"));
1721
+ return reports.filter((r) => r.taskId === taskId).length;
1722
+ }
1741
1723
  /** Return all build reports from cycles >= {@link cycleNumber}. */
1742
1724
  async getBuildReportsSince(cycleNumber) {
1743
1725
  const reports = parseBuildReports(await this.read("BUILD_REPORTS.md"));
@@ -1898,11 +1880,6 @@ ${newSection}
1898
1880
  const metrics = await this.readToolMetrics();
1899
1881
  return aggregateCostSummary(metrics, cycleNumber);
1900
1882
  }
1901
- /** Write a cost snapshot to the Cost Summary section of METRICS.md. */
1902
- async writeCostSnapshot(snapshot) {
1903
- const content = await this.readOptional("METRICS.md");
1904
- await this.write("METRICS.md", writeCostSnapshotToContent(snapshot, content));
1905
- }
1906
1883
  /** Read all cost snapshots from the Cost Summary section of METRICS.md. */
1907
1884
  async getCostSnapshots() {
1908
1885
  const content = await this.readOptional("METRICS.md");
@@ -5585,6 +5562,8 @@ CREATE TABLE IF NOT EXISTS strategy_reviews (
5585
5562
  full_analysis TEXT,
5586
5563
  velocity_assessment TEXT,
5587
5564
  structured_data JSONB,
5565
+ review_number INTEGER,
5566
+ review_type TEXT,
5588
5567
  PRIMARY KEY (id)
5589
5568
  );
5590
5569
 
@@ -6207,11 +6186,21 @@ ${newParts.join("\n")}` : newParts.join("\n");
6207
6186
  `;
6208
6187
  }
6209
6188
  async writeStrategyReview(review) {
6189
+ let reviewNumber = review.reviewNumber ?? null;
6190
+ if (reviewNumber == null && review.cycleNumber > 0) {
6191
+ const [row] = await this.sql`
6192
+ SELECT MAX(review_number) as max_num FROM strategy_reviews
6193
+ WHERE project_id = ${this.projectId} AND cycle_number > 0
6194
+ `;
6195
+ reviewNumber = (row?.max_num ?? 0) + 1;
6196
+ }
6197
+ const reviewType = review.reviewType ?? "scheduled";
6210
6198
  await this.sql`
6211
6199
  INSERT INTO strategy_reviews (
6212
6200
  project_id, cycle_number, cycle_range, title, content, notes,
6213
6201
  board_health, strategic_direction, recommendations,
6214
- full_analysis, velocity_assessment, structured_data
6202
+ full_analysis, velocity_assessment, structured_data,
6203
+ review_number, review_type
6215
6204
  )
6216
6205
  VALUES (
6217
6206
  ${this.projectId},
@@ -6222,10 +6211,12 @@ ${newParts.join("\n")}` : newParts.join("\n");
6222
6211
  ${review.notes ?? null},
6223
6212
  ${review.boardHealth ?? null},
6224
6213
  ${review.strategicDirection ?? null},
6225
- ${review.recommendations ? JSON.stringify(review.recommendations) : null},
6214
+ ${review.recommendations ? this.sql.json(review.recommendations) : null},
6226
6215
  ${review.fullAnalysis ?? null},
6227
6216
  ${review.velocityAssessment ?? null},
6228
- ${review.structuredData ? JSON.stringify(review.structuredData) : null}
6217
+ ${review.structuredData ? this.sql.json(review.structuredData) : null},
6218
+ ${reviewNumber},
6219
+ ${reviewType}
6229
6220
  )
6230
6221
  ON CONFLICT (project_id, cycle_number)
6231
6222
  DO UPDATE SET
@@ -6238,7 +6229,9 @@ ${newParts.join("\n")}` : newParts.join("\n");
6238
6229
  recommendations = EXCLUDED.recommendations,
6239
6230
  full_analysis = EXCLUDED.full_analysis,
6240
6231
  velocity_assessment = EXCLUDED.velocity_assessment,
6241
- structured_data = EXCLUDED.structured_data
6232
+ structured_data = EXCLUDED.structured_data,
6233
+ review_number = EXCLUDED.review_number,
6234
+ review_type = EXCLUDED.review_type
6242
6235
  `;
6243
6236
  }
6244
6237
  async getLastStrategyReviewCycle() {
@@ -6255,7 +6248,8 @@ ${newParts.join("\n")}` : newParts.join("\n");
6255
6248
  const rows2 = await this.sql`
6256
6249
  SELECT cycle_number, cycle_range, title, content, notes,
6257
6250
  board_health, strategic_direction, full_analysis,
6258
- velocity_assessment, structured_data, created_at
6251
+ velocity_assessment, structured_data, created_at,
6252
+ review_number, review_type
6259
6253
  FROM strategy_reviews
6260
6254
  WHERE project_id = ${this.projectId} AND cycle_number > 0
6261
6255
  ORDER BY cycle_number DESC
@@ -6272,13 +6266,15 @@ ${newParts.join("\n")}` : newParts.join("\n");
6272
6266
  fullAnalysis: r.full_analysis ?? void 0,
6273
6267
  velocityAssessment: r.velocity_assessment ?? void 0,
6274
6268
  structuredData: r.structured_data ?? void 0,
6275
- createdAt: r.created_at ?? void 0
6269
+ createdAt: r.created_at ?? void 0,
6270
+ reviewNumber: r.review_number ?? void 0,
6271
+ reviewType: r.review_type ?? void 0
6276
6272
  }));
6277
6273
  }
6278
6274
  const rows = await this.sql`
6279
6275
  SELECT cycle_number, cycle_range, title, content, notes,
6280
6276
  board_health, strategic_direction, velocity_assessment,
6281
- structured_data, created_at
6277
+ structured_data, created_at, review_number, review_type
6282
6278
  FROM strategy_reviews
6283
6279
  WHERE project_id = ${this.projectId}
6284
6280
  ORDER BY cycle_number DESC
@@ -6294,7 +6290,9 @@ ${newParts.join("\n")}` : newParts.join("\n");
6294
6290
  strategicDirection: r.strategic_direction ?? void 0,
6295
6291
  velocityAssessment: r.velocity_assessment ?? void 0,
6296
6292
  structuredData: r.structured_data ?? void 0,
6297
- createdAt: r.created_at ?? void 0
6293
+ createdAt: r.created_at ?? void 0,
6294
+ reviewNumber: r.review_number ?? void 0,
6295
+ reviewType: r.review_type ?? void 0
6298
6296
  }));
6299
6297
  }
6300
6298
  async savePendingReviewResponse(cycleNumber, rawResponse) {
@@ -6340,7 +6338,7 @@ ${newParts.join("\n")}` : newParts.join("\n");
6340
6338
  ${entry.status}, ${entry.summary}, ${entry.tags},
6341
6339
  ${entry.cycleCreated}, ${entry.cycleUpdated ?? null},
6342
6340
  ${entry.supersededBy ?? null},
6343
- ${entry.actions ? JSON.stringify(entry.actions) : "[]"}
6341
+ ${entry.actions ? this.sql.json(entry.actions) : this.sql.json([])}
6344
6342
  )
6345
6343
  ON CONFLICT (project_id, path)
6346
6344
  DO UPDATE SET
@@ -6605,8 +6603,8 @@ ${newParts.join("\n")}` : newParts.join("\n");
6605
6603
  ${task.reviewed}, ${task.cycle ?? null}, ${task.createdCycle ?? null},
6606
6604
  ${task.why ?? null}, ${task.dependsOn ?? null}, ${task.notes ?? null},
6607
6605
  ${task.closureReason ?? null},
6608
- ${JSON.stringify(task.stateHistory ?? [])},
6609
- ${task.buildHandoff ? JSON.stringify(task.buildHandoff) : null},
6606
+ ${this.sql.json(task.stateHistory ?? [])},
6607
+ ${task.buildHandoff ? this.sql.json(task.buildHandoff) : null},
6610
6608
  ${task.buildReport ?? null},
6611
6609
  ${task.taskType ?? null},
6612
6610
  ${task.maturity ?? null},
@@ -6694,9 +6692,9 @@ ${newParts.join("\n")}` : newParts.join("\n");
6694
6692
  ${normaliseEffort(report.actualEffort)}, ${normaliseEffort(report.estimatedEffort)}, ${report.scopeAccuracy},
6695
6693
  ${report.surprises}, ${report.discoveredIssues}, ${report.architectureNotes},
6696
6694
  ${report.commitSha ?? null}, ${report.filesChanged ?? []}, ${report.relatedDecisions ?? []},
6697
- ${report.handoffAccuracy ? JSON.stringify(report.handoffAccuracy) : null},
6695
+ ${report.handoffAccuracy ? this.sql.json(report.handoffAccuracy) : null},
6698
6696
  ${report.correctionsCount ?? 0},
6699
- ${report.briefImplications ? JSON.stringify(report.briefImplications) : null},
6697
+ ${report.briefImplications ? this.sql.json(report.briefImplications) : null},
6700
6698
  ${report.deadEnds ?? null}
6701
6699
  )
6702
6700
  `;
@@ -6710,6 +6708,13 @@ ${newParts.join("\n")}` : newParts.join("\n");
6710
6708
  `;
6711
6709
  return rows.map(rowToBuildReport);
6712
6710
  }
6711
+ async getBuildReportCountForTask(taskId) {
6712
+ const rows = await this.sql`
6713
+ SELECT COUNT(*)::text AS count FROM build_reports
6714
+ WHERE project_id = ${this.projectId} AND task_id = ${taskId}
6715
+ `;
6716
+ return parseInt(rows[0]?.count ?? "0", 10);
6717
+ }
6713
6718
  async getBuildReportsSince(cycleNumber) {
6714
6719
  const rows = await this.sql`
6715
6720
  SELECT * FROM build_reports
@@ -6748,7 +6753,7 @@ ${newParts.join("\n")}` : newParts.join("\n");
6748
6753
  ${review.reviewer}, ${review.verdict}, ${review.cycle},
6749
6754
  ${review.date}, ${review.comments},
6750
6755
  ${review.handoffRevision ?? null}, ${review.buildCommitSha ?? null},
6751
- ${review.autoReview ? JSON.stringify(review.autoReview) : null}
6756
+ ${review.autoReview ? this.sql.json(review.autoReview) : null}
6752
6757
  )
6753
6758
  `;
6754
6759
  }
@@ -6971,25 +6976,6 @@ ${newParts.join("\n")}` : newParts.join("\n");
6971
6976
  avgCostPerCall: metrics.length > 0 ? totalCostUsd / metrics.length : 0
6972
6977
  };
6973
6978
  }
6974
- async writeCostSnapshot(snapshot) {
6975
- await this.sql`
6976
- INSERT INTO cost_snapshots (
6977
- project_id, cycle, date, total_cost_usd,
6978
- total_input_tokens, total_output_tokens, total_calls
6979
- ) VALUES (
6980
- ${this.projectId}, ${snapshot.cycle}, ${snapshot.date},
6981
- ${snapshot.totalCostUsd}, ${snapshot.totalInputTokens},
6982
- ${snapshot.totalOutputTokens}, ${snapshot.totalCalls}
6983
- )
6984
- ON CONFLICT (project_id, cycle)
6985
- DO UPDATE SET
6986
- date = EXCLUDED.date,
6987
- total_cost_usd = EXCLUDED.total_cost_usd,
6988
- total_input_tokens = EXCLUDED.total_input_tokens,
6989
- total_output_tokens = EXCLUDED.total_output_tokens,
6990
- total_calls = EXCLUDED.total_calls
6991
- `;
6992
- }
6993
6979
  async getCostSnapshots() {
6994
6980
  const rows = await this.sql`
6995
6981
  SELECT * FROM cost_snapshots
@@ -7054,7 +7040,7 @@ ${newParts.join("\n")}` : newParts.join("\n");
7054
7040
  ${this.projectId}, ${cycle.number}, ${cycle.status},
7055
7041
  ${cycle.startDate}, ${cycle.endDate ?? null},
7056
7042
  ${cycle.goals}, ${cycle.boardHealth}, ${resolvedTaskIds},
7057
- ${cycle.contextHashes ? JSON.stringify(cycle.contextHashes) : null}
7043
+ ${cycle.contextHashes ? this.sql.json(cycle.contextHashes) : null}
7058
7044
  )
7059
7045
  ON CONFLICT (project_id, number)
7060
7046
  DO UPDATE SET
@@ -7111,6 +7097,12 @@ ${newParts.join("\n")}` : newParts.join("\n");
7111
7097
  await this.sql`
7112
7098
  UPDATE horizons SET status = ${status}, updated_at = NOW()
7113
7099
  WHERE id = ${horizonId} AND project_id = ${this.projectId}
7100
+ `;
7101
+ }
7102
+ async updatePhaseStatus(phaseId, status) {
7103
+ await this.sql`
7104
+ UPDATE phases SET status = ${status}, updated_at = NOW()
7105
+ WHERE id = ${phaseId} AND project_id = ${this.projectId}
7114
7106
  `;
7115
7107
  }
7116
7108
  async getActiveStage() {
@@ -7295,11 +7287,14 @@ ${newParts.join("\n")}` : newParts.join("\n");
7295
7287
  const over = acc?.over ?? "0";
7296
7288
  const matchRate = total > 0 ? Math.round(parseInt(matches, 10) / total * 100) : 0;
7297
7289
  const velocityStr = raw.velocity.map((r) => `Cycle ${r.cycle}: ${r.count} tasks`).join(", ");
7298
- const topSurprises = raw.surprises.slice(0, 3).map((s) => `- ${s.length > 150 ? s.slice(0, 150) + "..." : s}`).join("\n");
7290
+ const topSurprises = raw.surprises.filter((s) => s.text && !["None", "none", "N/A", ""].includes(s.text)).slice(0, 3).map((s) => `- ${s.text.length > 150 ? s.text.slice(0, 150) + "..." : s.text}`).join("\n");
7291
+ const topDeadEnds = raw.surprises.filter((s) => s.deadEnds && !["None", "none", "N/A", ""].includes(s.deadEnds)).slice(0, 3).map((s) => `- ${s.deadEnds.length > 150 ? s.deadEnds.slice(0, 150) + "..." : s.deadEnds}`).join("\n");
7299
7292
  const buildIntelligence = `**Estimation:** ${matchRate}% match rate (${matches}/${total}), ${under} under-estimated, ${over} over-estimated.
7300
7293
  **Velocity (last 5 cycles):** ${velocityStr || "No data"}
7301
7294
  ` + (topSurprises ? `**Recent surprises:**
7302
- ${topSurprises}` : "");
7295
+ ${topSurprises}
7296
+ ` : "") + (topDeadEnds ? `**Recent dead ends:**
7297
+ ${topDeadEnds}` : "");
7303
7298
  const cycleLog = raw.cycleLog.length === 0 ? "No cycle log entries yet." : raw.cycleLog.map(
7304
7299
  (r) => `### Cycle ${r.cycle_number} \u2014 ${r.title}
7305
7300
  ${r.content}` + (r.carry_forward ? `
@@ -7384,12 +7379,12 @@ ${r.content}` + (r.carry_forward ? `
7384
7379
  ORDER BY cycle DESC
7385
7380
  LIMIT 5
7386
7381
  `,
7387
- // Build intelligence: recent surprises
7382
+ // Build intelligence: recent surprises + dead ends
7388
7383
  this.sql`
7389
- SELECT surprises
7384
+ SELECT surprises, dead_ends
7390
7385
  FROM build_reports
7391
7386
  WHERE project_id = ${this.projectId}
7392
- AND surprises NOT IN ('None', 'none', 'N/A', '')
7387
+ AND (surprises NOT IN ('None', 'none', 'N/A', '') OR dead_ends IS NOT NULL)
7393
7388
  ORDER BY cycle DESC
7394
7389
  LIMIT 10
7395
7390
  `,
@@ -7423,7 +7418,7 @@ ${r.content}` + (r.carry_forward ? `
7423
7418
  board: [...boardRows],
7424
7419
  accuracy: accuracyRows[0] ?? { total: "0", matches: "0", over: "0", under: "0" },
7425
7420
  velocity: [...velocityRows],
7426
- surprises: surpriseRows.map((r) => r.surprises),
7421
+ surprises: surpriseRows.map((r) => ({ text: r.surprises, deadEnds: r.dead_ends })),
7427
7422
  cycleLog: [...logRows],
7428
7423
  activeDecisions: [...adRows]
7429
7424
  });
@@ -7582,8 +7577,8 @@ ${newParts.join("\n")}` : newParts.join("\n");
7582
7577
  depends_on: task.dependsOn ?? null,
7583
7578
  notes: task.notes ?? null,
7584
7579
  closure_reason: task.closureReason ?? null,
7585
- state_history: JSON.stringify(task.stateHistory ?? []),
7586
- build_handoff: task.buildHandoff ? JSON.stringify(task.buildHandoff) : null,
7580
+ state_history: this.sql.json(task.stateHistory ?? []),
7581
+ build_handoff: task.buildHandoff ? this.sql.json(task.buildHandoff) : null,
7587
7582
  build_report: task.buildReport ?? null,
7588
7583
  task_type: task.taskType ?? null,
7589
7584
  maturity: task.maturity ?? null,
@@ -7762,7 +7757,7 @@ ${newParts.join("\n")}` : newParts.join("\n");
7762
7757
  ${projectId}, ${payload.cycle.number}, ${payload.cycle.status},
7763
7758
  ${payload.cycle.startDate}, ${payload.cycle.endDate ?? null},
7764
7759
  ${payload.cycle.goals}, ${payload.cycle.boardHealth}, ${sprintTaskIds},
7765
- ${payload.cycle.contextHashes ? JSON.stringify(payload.cycle.contextHashes) : null}
7760
+ ${payload.cycle.contextHashes ? this.sql.json(payload.cycle.contextHashes) : null}
7766
7761
  )
7767
7762
  ON CONFLICT (project_id, number)
7768
7763
  DO UPDATE SET
@@ -7792,10 +7787,57 @@ var proxy_adapter_exports = {};
7792
7787
  __export(proxy_adapter_exports, {
7793
7788
  ProxyPapiAdapter: () => ProxyPapiAdapter
7794
7789
  });
7795
- var ProxyPapiAdapter;
7790
+ function snakeToCamel(str) {
7791
+ return str.replace(/_([a-z0-9])/g, (_, c) => c.toUpperCase());
7792
+ }
7793
+ function transformKeys(obj) {
7794
+ if (obj === null || obj === void 0) return obj;
7795
+ if (Array.isArray(obj)) return obj.map(transformKeys);
7796
+ if (typeof obj === "object" && obj !== null) {
7797
+ const result = {};
7798
+ for (const [key, value] of Object.entries(obj)) {
7799
+ const camelKey = snakeToCamel(key);
7800
+ result[camelKey] = JSONB_PASSTHROUGH_KEYS.has(camelKey) ? value : transformKeys(value);
7801
+ }
7802
+ return result;
7803
+ }
7804
+ return obj;
7805
+ }
7806
+ function fixDisplayIdEntity(obj) {
7807
+ if (obj.displayId !== void 0) {
7808
+ obj.uuid = obj.id;
7809
+ obj.id = obj.displayId;
7810
+ }
7811
+ return obj;
7812
+ }
7813
+ function fixDisplayIdEntities(data) {
7814
+ if (Array.isArray(data)) return data.map((item) => fixDisplayIdEntity(item));
7815
+ if (data && typeof data === "object") return fixDisplayIdEntity(data);
7816
+ return data;
7817
+ }
7818
+ var JSONB_PASSTHROUGH_KEYS, DISPLAY_ID_METHODS, ProxyPapiAdapter;
7796
7819
  var init_proxy_adapter = __esm({
7797
7820
  "src/proxy-adapter.ts"() {
7798
7821
  "use strict";
7822
+ JSONB_PASSTHROUGH_KEYS = /* @__PURE__ */ new Set([
7823
+ "buildHandoff",
7824
+ "stateHistory",
7825
+ "handoffAccuracy",
7826
+ "briefImplications",
7827
+ "autoReview",
7828
+ "structuredData",
7829
+ "data"
7830
+ ]);
7831
+ DISPLAY_ID_METHODS = /* @__PURE__ */ new Set([
7832
+ "queryBoard",
7833
+ "getTask",
7834
+ "getTasks",
7835
+ "createTask",
7836
+ "getRecentBuildReports",
7837
+ "getBuildReportsSince",
7838
+ "getRecentReviews",
7839
+ "getActiveDecisions"
7840
+ ]);
7799
7841
  ProxyPapiAdapter = class {
7800
7842
  endpoint;
7801
7843
  apiKey;
@@ -7808,6 +7850,7 @@ var init_proxy_adapter = __esm({
7808
7850
  /**
7809
7851
  * Send an adapter method call to the proxy Edge Function.
7810
7852
  * Serializes { projectId, method, args } and deserializes the response.
7853
+ * Results are transformed from snake_case to camelCase to match pg adapter output.
7811
7854
  */
7812
7855
  async invoke(method, args = []) {
7813
7856
  const url = `${this.endpoint}/invoke`;
@@ -7838,7 +7881,11 @@ var init_proxy_adapter = __esm({
7838
7881
  if (!body.ok && body.error) {
7839
7882
  throw new Error(`Proxy error on ${method}: ${body.error}`);
7840
7883
  }
7841
- return body.result;
7884
+ let result = transformKeys(body.result);
7885
+ if (DISPLAY_ID_METHODS.has(method)) {
7886
+ result = fixDisplayIdEntities(result);
7887
+ }
7888
+ return result;
7842
7889
  }
7843
7890
  /** Check if the proxy is reachable. */
7844
7891
  async probeConnection() {
@@ -7985,9 +8032,6 @@ var init_proxy_adapter = __esm({
7985
8032
  getCostSummary(cycleNumber) {
7986
8033
  return this.invoke("getCostSummary", [cycleNumber]);
7987
8034
  }
7988
- writeCostSnapshot(snapshot) {
7989
- return this.invoke("writeCostSnapshot", [snapshot]);
7990
- }
7991
8035
  getCostSnapshots() {
7992
8036
  return this.invoke("getCostSnapshots");
7993
8037
  }
@@ -8148,6 +8192,20 @@ function loadConfig() {
8148
8192
  // src/adapter-factory.ts
8149
8193
  init_dist2();
8150
8194
  import path2 from "path";
8195
+ import { execSync } from "child_process";
8196
+ function detectUserId() {
8197
+ try {
8198
+ const email = execSync("git config user.email", { encoding: "utf8", timeout: 5e3 }).trim();
8199
+ if (email) return email;
8200
+ } catch {
8201
+ }
8202
+ try {
8203
+ const ghUser = execSync("gh api user --jq .email", { encoding: "utf8", timeout: 1e4 }).trim();
8204
+ if (ghUser && ghUser !== "null") return ghUser;
8205
+ } catch {
8206
+ }
8207
+ return void 0;
8208
+ }
8151
8209
  var HOSTED_PROXY_ENDPOINT = "https://guewgygcpcmrcoppihzx.supabase.co/functions/v1/data-proxy";
8152
8210
  var PLACEHOLDER_PATTERNS = [
8153
8211
  "<YOUR_DATABASE_URL>",
@@ -8208,7 +8266,18 @@ async function createAdapter(optionsOrType, maybePapiDir) {
8208
8266
  if (!existing) {
8209
8267
  const projectRoot = options.projectRoot ?? process.env["PAPI_PROJECT_DIR"] ?? "";
8210
8268
  const slug = path2.basename(projectRoot) || "unnamed";
8211
- const userId = process.env["PAPI_USER_ID"] ?? void 0;
8269
+ let userId = process.env["PAPI_USER_ID"] ?? void 0;
8270
+ if (!userId) {
8271
+ userId = detectUserId();
8272
+ if (userId) {
8273
+ console.error(`[papi] Auto-detected user identity: ${userId}`);
8274
+ console.error("[papi] Set PAPI_USER_ID in .mcp.json to make this explicit.");
8275
+ } else {
8276
+ console.error("[papi] \u26A0 No PAPI_USER_ID set and auto-detection failed.");
8277
+ console.error("[papi] Project will have no user scope \u2014 it may be visible to all dashboard users.");
8278
+ console.error("[papi] Set PAPI_USER_ID in your .mcp.json env to fix this.");
8279
+ }
8280
+ }
8212
8281
  await pgAdapter.createProject({ id: projectId, slug, name: slug, papi_dir: papiDir, user_id: userId });
8213
8282
  }
8214
8283
  await pgAdapter.close();
@@ -8242,7 +8311,12 @@ async function createAdapter(optionsOrType, maybePapiDir) {
8242
8311
  const dataApiKey = process.env["PAPI_DATA_API_KEY"];
8243
8312
  if (!dataApiKey) {
8244
8313
  throw new Error(
8245
- "PAPI_DATA_API_KEY is required for proxy mode.\nTo get your API key:\n 1. Sign in at https://papi-web-three.vercel.app with GitHub\n 2. Your API key is shown on the onboarding page (save it \u2014 shown only once)\n 3. Add PAPI_DATA_API_KEY to your .mcp.json env config\nIf you already have a key, set it in your MCP configuration."
8314
+ `PAPI_DATA_API_KEY is required for proxy mode.
8315
+ To get your API key:
8316
+ 1. Sign in at ${process.env["PAPI_DASHBOARD_URL"] || "https://papi-web-three.vercel.app"} with GitHub
8317
+ 2. Your API key is shown on the onboarding page (save it \u2014 shown only once)
8318
+ 3. Add PAPI_DATA_API_KEY to your .mcp.json env config
8319
+ If you already have a key, set it in your MCP configuration.`
8246
8320
  );
8247
8321
  }
8248
8322
  const adapter2 = new ProxyPapiAdapter2({
@@ -8599,6 +8673,61 @@ function formatReviews(reviews) {
8599
8673
  - **Comments:** ${r.comments}`
8600
8674
  ).join("\n\n---\n\n");
8601
8675
  }
8676
+ function formatTaskComments(comments, taskIds, heading = "## Task Comments") {
8677
+ const relevant = comments.filter((c) => taskIds.has(c.taskId));
8678
+ if (relevant.length === 0) return "";
8679
+ const byTask = /* @__PURE__ */ new Map();
8680
+ for (const c of relevant) {
8681
+ const list = byTask.get(c.taskId) ?? [];
8682
+ list.push(c);
8683
+ byTask.set(c.taskId, list);
8684
+ }
8685
+ const lines = ["", heading];
8686
+ for (const [taskId, taskComments] of byTask) {
8687
+ for (const c of taskComments.slice(0, 3)) {
8688
+ const date = c.createdAt.split("T")[0];
8689
+ const text = c.content.length > 200 ? c.content.slice(0, 200) + "..." : c.content;
8690
+ lines.push(`- **${taskId}** \u2014 ${c.author} (${date}): "${text}"`);
8691
+ }
8692
+ }
8693
+ return lines.join("\n");
8694
+ }
8695
+ function formatDiscoveryCanvas(canvas) {
8696
+ const sections = [];
8697
+ if (canvas.landscapeReferences && canvas.landscapeReferences.length > 0) {
8698
+ sections.push("**Landscape & References:**");
8699
+ for (const ref of canvas.landscapeReferences) {
8700
+ const url = ref.url ? ` (${ref.url})` : "";
8701
+ const notes = ref.notes ? ` \u2014 ${ref.notes}` : "";
8702
+ sections.push(`- ${ref.name}${url}${notes}`);
8703
+ }
8704
+ }
8705
+ if (canvas.userJourneys && canvas.userJourneys.length > 0) {
8706
+ sections.push("**User Journeys:**");
8707
+ for (const j of canvas.userJourneys) {
8708
+ const priority = j.priority ? ` [${j.priority}]` : "";
8709
+ sections.push(`- **${j.persona}:** ${j.journey}${priority}`);
8710
+ }
8711
+ }
8712
+ if (canvas.mvpBoundary) {
8713
+ sections.push("**MVP Boundary:**", canvas.mvpBoundary);
8714
+ }
8715
+ if (canvas.assumptionsOpenQuestions && canvas.assumptionsOpenQuestions.length > 0) {
8716
+ sections.push("**Assumptions & Open Questions:**");
8717
+ for (const a of canvas.assumptionsOpenQuestions) {
8718
+ const evidence = a.evidence ? ` Evidence: ${a.evidence}` : "";
8719
+ sections.push(`- [${a.status}] ${a.text}${evidence}`);
8720
+ }
8721
+ }
8722
+ if (canvas.successSignals && canvas.successSignals.length > 0) {
8723
+ sections.push("**Success Signals:**");
8724
+ for (const s of canvas.successSignals) {
8725
+ const metric = s.metric ? ` (${s.metric}` + (s.target ? `, target: ${s.target})` : ")") : "";
8726
+ sections.push(`- ${s.signal}${metric}`);
8727
+ }
8728
+ }
8729
+ return sections.length > 0 ? sections.join("\n") : void 0;
8730
+ }
8602
8731
 
8603
8732
  // src/lib/git.ts
8604
8733
  import { execFileSync } from "child_process";
@@ -8623,6 +8752,11 @@ function isGitRepo(cwd) {
8623
8752
  }
8624
8753
  }
8625
8754
  function stageDirAndCommit(cwd, dir, message) {
8755
+ try {
8756
+ execFileSync("git", ["check-ignore", "-q", dir], { cwd });
8757
+ return { committed: false, message: `Skipped commit \u2014 '${dir}' is gitignored.` };
8758
+ } catch {
8759
+ }
8626
8760
  execFileSync("git", ["add", dir], { cwd });
8627
8761
  const staged = execFileSync("git", ["diff", "--cached", "--name-only"], {
8628
8762
  cwd,
@@ -8932,6 +9066,16 @@ function getHeadCommitSha(cwd) {
8932
9066
  return null;
8933
9067
  }
8934
9068
  }
9069
+ function runAutoCommit(projectRoot, commitFn) {
9070
+ if (!isGitAvailable()) return "Auto-commit: skipped (git not found).";
9071
+ if (!isGitRepo(projectRoot)) return "Auto-commit: skipped (not a git repository).";
9072
+ try {
9073
+ const result = commitFn();
9074
+ return result.committed ? `Auto-committed: ${result.message}` : `Auto-commit: ${result.message}`;
9075
+ } catch (err) {
9076
+ return `Auto-commit failed: ${err instanceof Error ? err.message : String(err)}`;
9077
+ }
9078
+ }
8935
9079
  function getFilesChangedFromBase(cwd, baseBranch) {
8936
9080
  try {
8937
9081
  const mergeBase = execFileSync("git", ["merge-base", baseBranch, "HEAD"], { cwd, encoding: "utf-8" }).trim();
@@ -9172,6 +9316,27 @@ After your natural language output, include this EXACT format on its own line:
9172
9316
 
9173
9317
  The JSON must be valid. Use null for optional fields that don't apply.
9174
9318
 
9319
+ ## GUIDING PRINCIPLES
9320
+
9321
+ These principles come from 150+ cycles of dogfooding. They shape how the planner should think about planning:
9322
+
9323
+ - **Validate before advancing.** Don't push forward when things aren't proven.
9324
+ - **Every artifact needs a consumer.** If not consumed by the next cycle, it's waste.
9325
+ - **Upstream learning.** Every build informs the next plan.
9326
+ - **Commands surfaced, not memorized.** Always show what's next.
9327
+ - **Tough advisor, not cheerleader.** Push back on scope creep and bad ideas.
9328
+ - **BUILD HANDOFFs are the differentiator.** A third LLM executed tasks from handoffs alone.
9329
+ - **The methodology works.** Plan/build/review cycle produces compounding velocity.
9330
+
9331
+ ## DETECT STRATEGIC DECISIONS
9332
+
9333
+ Watch for direction changes, architecture shifts, deprioritisation with reasoning, new principles, or competitive positioning decisions in the project context.
9334
+
9335
+ When detected:
9336
+ 1. Flag it in the cycle log: "Strategic direction change detected \u2014 [description]."
9337
+ 2. If confirmed by evidence (build reports, AD changes, carry-forward), propose an AD update or new AD in the structured output.
9338
+ 3. If mid-cycle context suggests a pivot, note it for the next strategy review rather than over-reacting in the plan.
9339
+
9175
9340
  ## PERSISTENCE RULES \u2014 READ THIS CAREFULLY
9176
9341
 
9177
9342
  Everything in Part 1 (natural language) is **display-only**. Part 2 (structured JSON) is what gets written to files.
@@ -9235,12 +9400,18 @@ Standard planning cycle with full board review.
9235
9400
  1. **Cycle Health Check** \u2014 Flag issues: >7 day gaps, unprocessed discovered issues, AD conflicts, stale In Progress tasks (3+ cycles).
9236
9401
  **\u26A0\uFE0F CARRY-FORWARD STALENESS:** Check the latest carry-forward text for items containing "stale", "already exists", "already implemented", or "already built". For each such item that references a specific task ID, check whether the task is still in Backlog. If a carry-forward says a task's deliverables already exist but the task is still Backlog, emit a \`boardCorrections\` entry setting it to Done with \`closureReason: "Auto-closed \u2014 carry-forward indicates deliverables already exist"\`. Log in the cycle log: "Auto-closed task-XXX \u2014 carry-forward confirmed deliverables exist." This prevents scheduling already-shipped tasks.
9237
9402
 
9238
- 2. **Inbox Triage** \u2014 Find unreviewed tasks (reviewed = false). For each: clean title, fill all fields, check for duplicates, verify alignment with Active Decisions. You MAY set priority on unreviewed tasks during triage. If a task is clearly obsolete, duplicated, or rejected, set its status to "Cancelled" with a \`closureReason\` explaining why.
9403
+ 2. **Inbox Triage** \u2014 Find unreviewed tasks (reviewed = false). For each: clean title, fill all fields, check for duplicates, verify alignment with Active Decisions. You MUST set priority on unreviewed tasks during triage using these criteria:
9404
+ - **P0 Critical** \u2014 Broken, blocking, or data-loss risk. Fix now.
9405
+ - **P1 High** \u2014 Strategically aligned: directly advances the current horizon/phase goals or Active Decisions.
9406
+ - **P2 Medium** \u2014 Valuable but not strategically urgent: quality improvements, efficiency, polish, infrastructure.
9407
+ - **P3 Low** \u2014 Nice-to-have, speculative, or future-horizon work.
9408
+ Also set complexity using the full range \u2014 **XS, Small, Medium, Large, XL** \u2014 based on actual scope, not conservatively. XS = single-line or config change. Small = one file, < 50 lines. Medium = 2-5 files. Large = cross-module, multiple components. XL = architectural, multi-day.
9409
+ If a task is clearly obsolete, duplicated, or rejected, set its status to "Cancelled" with a \`closureReason\` explaining why.
9239
9410
  **\u2192 PERSIST:** For each task you set reviewed: true, corrected fields on, or marked "Cancelled", include it in \`boardCorrections\` in Part 2.
9240
9411
 
9241
9412
  3. **Board Integrity** \u2014 All tasks have complete fields? Priority still accurate? Duplicates? Stale In Progress tasks?
9242
9413
  **\u2192 PERSIST:** Include any field corrections (status updates, field fixes) in \`boardCorrections\` in Part 2.
9243
- **\u26A0\uFE0F PRIORITY LOCK RULE:** Do NOT change the priority of any task that has \`reviewed: true\`. Reviewed tasks have had their priority confirmed by a human. If you believe a reviewed task's priority should change, note your recommendation in the cycle log but do NOT include a priority change in \`boardCorrections\`. You may only set priority on unreviewed tasks (during triage) or on newly created tasks (\`newTasks\` array).
9414
+ **\u26A0\uFE0F PRIORITY LOCK RULE:** Do NOT change the priority of any task that has \`reviewed: true\`. Reviewed tasks have had their priority confirmed by a human. If you believe a reviewed task's priority should change, note your recommendation in the cycle log but do NOT include a priority change in \`boardCorrections\`. You may only set priority on unreviewed tasks (during triage) or on newly created tasks (\`newTasks\` array). Priority values: P0 Critical, P1 High, P2 Medium, P3 Low.
9244
9415
 
9245
9416
  4. **Security Posture Check** \u2014 Review recently completed tasks and current board state for security concerns. Only flag genuine issues \u2014 do not add boilerplate security notes every cycle. Look for:
9246
9417
  - Data exposure risks introduced by recent builds (PII in logs, secrets in storage/config)
@@ -9255,19 +9426,17 @@ Standard planning cycle with full board review.
9255
9426
  - **Cycle number as signal:** A Cycle 3 project should not be scheduling OAuth, billing, or analytics tasks. Early cycles focus on core functionality and proving the concept works.
9256
9427
  - **Phase prerequisites:** If the board has phases, tasks from later phases should only be scheduled when earlier phases have completed tasks (check Done count per phase). A task in "Phase 4: Monetisation" is premature if Phase 2 tasks are still in Backlog.
9257
9428
  - **Dependency chain:** If a task's \`dependsOn\` references incomplete tasks, it cannot be scheduled regardless of priority.
9258
- - **Task maturity:** Tasks with \`maturity: "raw"\` are unscoped ideas \u2014 they lack clear acceptance criteria and scope. Do NOT schedule raw tasks or generate BUILD HANDOFFs for them. Instead, either: (a) upgrade them to \`maturity: "investigated"\` via a \`boardCorrections\` entry if you can derive clear scope from the title and context, or (b) leave them in Backlog and note in the cycle log that they need investigation. Tasks with \`maturity: "ready"\` or no maturity field are considered cycle-ready. Tasks with \`maturity: "investigated"\` have been scoped but may still need refinement \u2014 schedule them if priority warrants it.
9259
- - **What to do with premature tasks:** Leave them in Backlog. Do NOT generate BUILD HANDOFFs for them. If a high-priority task fails the maturity gate, note it in the cycle log: "task-XXX deferred \u2014 Phase N prerequisites not met" or "task-XXX deferred \u2014 raw idea, needs investigation."
9429
+ - **Task maturity:** Tasks with \`maturity: "raw"\` are unscoped ideas from the idea tool. The planner IS the scoping mechanism \u2014 scope them as part of planning. For raw tasks selected for a cycle: (a) derive clear scope, acceptance criteria, and effort from the title, notes, and project context, (b) upgrade them to \`maturity: "investigated"\` via a \`boardCorrections\` entry, and (c) generate a BUILD HANDOFF as normal. For research-type raw tasks, scope the handoff as an investigation task \u2014 the deliverable is findings + follow-up backlog tasks, not code. Only leave a raw task unscheduled if you genuinely cannot derive scope from the available context \u2014 note why in the cycle log. Tasks with \`maturity: "ready"\` or no maturity field are considered cycle-ready. Tasks with \`maturity: "investigated"\` have been scoped but may still need refinement \u2014 schedule them if priority warrants it.
9430
+ - **What to do with premature tasks:** Leave them in Backlog. Do NOT generate BUILD HANDOFFs for them. If a high-priority task fails the maturity gate due to phase prerequisites or dependencies, note it in the cycle log: "task-XXX deferred \u2014 Phase N prerequisites not met". Raw tasks are NOT premature \u2014 they just need scoping (see Task maturity above).
9260
9431
 
9261
9432
  7. **Recommendation** \u2014 Pick ONE task to recommend:
9262
- **If USER DIRECTION is provided above:** Follow the user's stated focus. Pick the highest-impact task that aligns with their direction. The user knows what they need \u2014 do not override their direction with the tier system. Only deviate if a genuine Tier 0 critical fix exists (broken builds, data loss).
9263
- **Otherwise, use priority tiers:**
9264
- - Tier 0: Critical fixes from Build Reports
9265
- - Tier 1: User feedback aligned with Active Decisions
9266
- - Tier 2: Activation blockers
9267
- - Tier 3: Usage multipliers (infra/UX improvements)
9268
- - Tier 4: Data visualization
9269
- - Tier 5: New capability
9270
- Within a tier: smaller effort wins. Justify in 2-3 sentences.
9433
+ **If USER DIRECTION is provided above:** Follow the user's stated focus. Pick the highest-impact task that aligns with their direction. The user knows what they need. Only deviate if a genuine P0 Critical fix exists (broken builds, data loss).
9434
+ **Otherwise, select by priority level then impact:**
9435
+ - **P0 Critical** \u2014 Broken, blocking, or data-loss risk. Always first.
9436
+ - **P1 High** \u2014 Strategically aligned: directly advances the current horizon, phase, or Active Decision goals.
9437
+ - **P2 Medium** \u2014 Valuable but not strategically urgent: quality improvements, efficiency, polish, infra.
9438
+ - **P3 Low** \u2014 Nice-to-have, speculative, or future-horizon work.
9439
+ 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.
9271
9440
  **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.
9272
9441
  **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.
9273
9442
 
@@ -9275,15 +9444,21 @@ Standard planning cycle with full board review.
9275
9444
  **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.
9276
9445
 
9277
9446
  9. **Active Decisions** \u2014 If any AD needs updating: Type A (confidence change), Type B (modification), or Type C (reversal/supersede).
9447
+ **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. Apply this bar when proposing new ADs and when triaging existing ones.
9278
9448
  **\u2192 PERSIST:** EVERY AD you created, updated, or confirmed with changes MUST appear in \`activeDecisions\` array in Part 2. Include the full replacement body with ### heading.
9279
9449
 
9450
+ ### Operational Quality Rules
9451
+ - **Idea similarity pause:** When the idea tool finds similar tasks during planning, stop and explain the overlap \u2014 do not silently ignore the similarity warning. Duplicates bloat the board and waste build slots.
9452
+ - **Backlog as steering wheel:** Task priority and notes in the backlog are the user's primary control mechanism over what gets planned. Respect the priority rankings and read task notes carefully \u2014 they contain user intent that shapes scope and scheduling.
9453
+ - **Planning quality is the bar:** Strategy review depth and plan quality set the standard for the product. Do not cut corners on analysis depth, triage thoroughness, or handoff specificity \u2014 these are what users experience as PAPI's value.
9454
+
9280
9455
  10. **BUILD HANDOFFs** \u2014 Generate a full BUILD HANDOFF block for the recommended task and up to 4 additional high-priority unblocked tasks (5 total max). Include each handoff in the \`cycleHandoffs\` array in the structured output. The handoffs are written to each task on the board for durability. Remaining tasks will get handoffs in subsequent plans \u2014 do NOT try to cover the entire backlog.
9281
9456
  **SKIP existing handoffs:** Tasks marked with "Has BUILD HANDOFF: yes" or "\u2713 handoff" on the board already have a valid handoff from a previous plan. Do NOT regenerate handoffs for these tasks \u2014 omit them from the \`cycleHandoffs\` array entirely. Only generate handoffs for tasks that do NOT have one yet. Exception: if a task's dependencies have been completed since its handoff was written, or a relevant Active Decision has changed, you MAY regenerate its handoff \u2014 but note this explicitly in the cycle log.
9282
9457
  **Scope pre-check:** Before writing the SCOPE section of each handoff, check whether the described functionality already exists based on the task's context, recent build reports, and the FILES LIKELY TOUCHED. If the infrastructure likely exists (e.g. a status type, a DB constraint, an API route), reduce the scope to only the missing pieces and explicitly note what already exists. C126 task-728 was over-scoped because the planner assumed Blocked status needed creating from scratch \u2014 it already existed in types, DB, orient, and build_list. Over-scoped handoffs waste builder time on verification and cause estimation mismatches.
9283
9458
  **Simplest Viable Path rule:** Before writing each BUILD HANDOFF, identify the simplest approach that satisfies the task's goal \u2014 the minimum change, fewest new abstractions, and smallest blast radius. Write the SCOPE (DO THIS) section for that simplest path FIRST. If you believe a more complex approach is warranted (new abstractions, multi-file refactors, framework changes), you MUST include a "WHY NOT SIMPLER" line in the handoff explaining why the simple path is insufficient. If you cannot articulate a concrete reason, use the simpler path. Pay special attention to tasks involving auth, data access, multi-user features, and infrastructure \u2014 these are the most common over-engineering targets.
9284
- **Maturity gate applies here:** Do NOT generate BUILD HANDOFFs for tasks that failed the maturity gate in step 6. This includes raw tasks (\`maturity: "raw"\`) and tasks whose phase prerequisites are not met. Only cycle-ready tasks should receive handoffs.
9459
+ **Maturity gate applies here:** Do NOT generate BUILD HANDOFFs for tasks that failed the maturity gate in step 6 (phase prerequisites not met, dependency chain incomplete). Raw tasks that the planner has scoped and upgraded to "investigated" in step 6 ARE eligible for handoffs.
9285
9460
  **Security section guidance:** Each handoff includes a SECURITY CONSIDERATIONS section. Populate it when the task involves: data exposure risks (PII, secrets in logs/storage), secrets or credentials handling (API keys, tokens, env vars), auth/access control changes, or dependency security risks (new packages, version changes). For pure refactoring, documentation, prompt-text, or UI-only tasks, write "None \u2014 no security-relevant changes".
9286
- **Estimation calibration:** Tasks that wire existing adapter methods, add API routes following established patterns, modify prompts, or make documentation-only changes should be estimated **S** unless they require new abstractions, new DB tables, or multi-file architectural changes. Default to S for pattern-following work. Only use M when genuine new architecture is needed. 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).
9461
+ **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).
9287
9462
  **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.
9288
9463
  **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.
9289
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:
@@ -9327,7 +9502,7 @@ function buildPlanUserMessage(ctx) {
9327
9502
  parts.push(
9328
9503
  `## USER DIRECTION`,
9329
9504
  "",
9330
- `The user has provided the following direction for this cycle. This OVERRIDES the autonomous priority tier system in Step 5. Prioritise tasks that align with this direction, even if lower-priority tasks exist on the board.`,
9505
+ `The user has provided the following direction for this cycle. This OVERRIDES the autonomous priority-based selection in Step 7. Prioritise tasks that align with this direction, even if lower-priority tasks exist on the board.`,
9331
9506
  "",
9332
9507
  `> ${ctx.focus}`,
9333
9508
  ""
@@ -9484,9 +9659,18 @@ IMPORTANT: You are running as a non-interactive API call. Do NOT ask the user qu
9484
9659
 
9485
9660
  ## OUTPUT PRINCIPLES
9486
9661
 
9487
- - **Concise and actionable.** Lead with findings, not data recitation. Each recommendation must include a specific action, not a vague suggestion.
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.
9663
+ - **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."
9488
9664
  - **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.
9489
- - **Every section earns its place.** If a section has nothing meaningful to say, skip it entirely. Do not write "No issues found" or "No concerns" \u2014 just omit the section.
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.
9666
+
9667
+ ## TWO-PHASE DELIVERY
9668
+
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.
9672
+
9673
+ Present the full review first. Let the analysis breathe. The user will discuss, push back, and refine before acting on the structured output.
9490
9674
 
9491
9675
  ## YOUR JOB \u2014 STRUCTURED COVERAGE
9492
9676
 
@@ -9514,6 +9698,7 @@ You MUST cover these 6 sections. Each is mandatory unless explicitly noted as co
9514
9698
  - **scale_cost** \u2014 What this costs at 10x/100x users or data (1=negligible, 5=bottleneck)
9515
9699
  - **lock_in** \u2014 Dependency on a specific vendor/tool (1=swappable, 5=deeply coupled)
9516
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\`.
9517
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.
9518
9703
 
9519
9704
  ## CONDITIONAL SECTIONS (include only when relevant)
@@ -9533,6 +9718,7 @@ ${compressionJob}
9533
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.
9534
9719
  - **Dead dependencies** \u2014 Packages in package.json that are no longer imported anywhere. These add install time and attack surface.
9535
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.
9536
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".
9537
9723
 
9538
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):
@@ -9749,6 +9935,15 @@ function buildReviewUserMessage(ctx) {
9749
9935
  if (ctx.pendingRecommendations) {
9750
9936
  parts.push("### Pending Strategy Recommendations", "", ctx.pendingRecommendations, "");
9751
9937
  }
9938
+ if (ctx.registeredDocs) {
9939
+ parts.push("### Registered Documents", "", ctx.registeredDocs, "");
9940
+ }
9941
+ if (ctx.recentPlans) {
9942
+ parts.push("### Recent Plans (since last review)", "", ctx.recentPlans, "");
9943
+ }
9944
+ if (ctx.unregisteredDocs) {
9945
+ parts.push("### Unregistered Docs", "", ctx.unregisteredDocs, "");
9946
+ }
9752
9947
  return parts.join("\n");
9753
9948
  }
9754
9949
  function parseReviewStructuredOutput(raw) {
@@ -10013,6 +10208,7 @@ The body format for each AD:
10013
10208
  - Each AD should be actionable and falsifiable (something the team could decide differently)
10014
10209
  - Cover different concerns: architecture, data, deployment, testing strategy, API design, etc.
10015
10210
  - Keep each AD body to 4-6 lines \u2014 concise and scannable
10211
+ - **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, configuration choices, or temporary workarounds.
10016
10212
 
10017
10213
  Return ONLY valid JSON \u2014 no preamble, no code fences, no explanation.`;
10018
10214
  function buildAdSeedPrompt(ctx) {
@@ -10090,8 +10286,8 @@ IMPORTANT: You are running as a non-interactive API call. Do NOT ask questions.
10090
10286
 
10091
10287
  Return a JSON array of 3-10 tasks. Each task must have:
10092
10288
  - "title": Clear, actionable task title (start with a verb)
10093
- - "priority": "P1 High", "P2 Medium", or "P3 Low"
10094
- - "complexity": "XS", "Small", "Medium", or "Large"
10289
+ - "priority": "P0 Critical", "P1 High", "P2 Medium", or "P3 Low"
10290
+ - "complexity": "XS", "Small", "Medium", "Large", or "XL"
10095
10291
  - "module": A module name inferred from the codebase (e.g. "Core", "API", "Frontend", "Infra", "Tests")
10096
10292
  - "phase": A phase name (e.g. "Phase 1", "Phase 2")
10097
10293
  - "notes": 1-2 sentences of context about why this task matters
@@ -10101,8 +10297,8 @@ Return a JSON array of 3-10 tasks. Each task must have:
10101
10297
  - Focus on gaps and improvements visible from the codebase structure \u2014 not features the user hasn't asked for
10102
10298
  - Common gap categories: missing tests, missing documentation, config improvements, dependency updates, code quality, security hardening
10103
10299
  - Do NOT suggest adding PAPI itself or PAPI-specific tasks \u2014 those are handled by the setup flow
10104
- - Prioritise tasks that reduce risk or unblock future work (P1) over nice-to-haves (P3)
10105
- - Keep complexity estimates conservative \u2014 Small for most tasks, Medium for multi-file changes
10300
+ - Prioritise tasks that reduce risk or unblock future work (P0/P1) over nice-to-haves (P3)
10301
+ - Use the full complexity range: XS (config/one-liner), Small (one file), Medium (2-5 files), Large (cross-module), XL (architectural)
10106
10302
  - Tasks should be specific enough to execute without further investigation
10107
10303
  - Maximum 10 tasks \u2014 fewer is better if the codebase is well-maintained`;
10108
10304
  function buildInitialTasksPrompt(inputs) {
@@ -10216,23 +10412,44 @@ function pushAfterCommit(config2) {
10216
10412
  return push.success ? `> ${push.message}` : `> Warning: ${push.message}`;
10217
10413
  }
10218
10414
  function autoCommitPapi(config2, cycleNumber, mode) {
10219
- if (!isGitAvailable()) {
10220
- return "Auto-commit: skipped (git not found).";
10221
- }
10222
- if (!isGitRepo(config2.projectRoot)) {
10223
- return "Auto-commit: skipped (not a git repository).";
10415
+ const modeLabel = mode === "bootstrap" ? "Bootstrap" : "Full";
10416
+ return runAutoCommit(
10417
+ config2.projectRoot,
10418
+ () => stageDirAndCommit(config2.projectRoot, ".papi", `papi: Cycle ${cycleNumber} plan \u2014 ${modeLabel}`)
10419
+ );
10420
+ }
10421
+ async function assembleTaskComments(adapter2) {
10422
+ try {
10423
+ const comments = await adapter2.getRecentTaskComments?.(20);
10424
+ if (comments && comments.length > 0) {
10425
+ const byTask = /* @__PURE__ */ new Map();
10426
+ for (const c of comments) {
10427
+ const list = byTask.get(c.taskId) ?? [];
10428
+ list.push(c);
10429
+ byTask.set(c.taskId, list);
10430
+ }
10431
+ const lines = [];
10432
+ for (const [taskId, taskComments] of byTask) {
10433
+ const limited = taskComments.slice(0, 5);
10434
+ for (const c of limited) {
10435
+ const truncated = c.content.length > 200 ? c.content.slice(0, 200) + "..." : c.content;
10436
+ const date = c.createdAt.split("T")[0];
10437
+ lines.push(`- **${taskId}** \u2014 ${c.author} (${date}): "${truncated}"`);
10438
+ }
10439
+ }
10440
+ return lines.join("\n");
10441
+ }
10442
+ } catch {
10224
10443
  }
10444
+ return void 0;
10445
+ }
10446
+ async function assembleDiscoveryCanvasText(adapter2) {
10225
10447
  try {
10226
- const modeLabel = mode === "bootstrap" ? "Bootstrap" : "Full";
10227
- const result = stageDirAndCommit(
10228
- config2.projectRoot,
10229
- ".papi",
10230
- `papi: Cycle ${cycleNumber} plan \u2014 ${modeLabel}`
10231
- );
10232
- return result.committed ? `Auto-committed: ${result.message}` : `Auto-commit: ${result.message}`;
10233
- } catch (err) {
10234
- return `Auto-commit failed: ${err instanceof Error ? err.message : String(err)}`;
10448
+ const canvas = await adapter2.readDiscoveryCanvas();
10449
+ return formatDiscoveryCanvas(canvas);
10450
+ } catch {
10235
10451
  }
10452
+ return void 0;
10236
10453
  }
10237
10454
  var REC_EXPIRY_CYCLES = 3;
10238
10455
  function formatStrategyRecommendations(recs, currentCycle) {
@@ -10274,42 +10491,6 @@ function formatStrategyRecommendations(recs, currentCycle) {
10274
10491
  }
10275
10492
  return sections.join("\n");
10276
10493
  }
10277
- function formatDiscoveryCanvas(canvas) {
10278
- const sections = [];
10279
- if (canvas.landscapeReferences && canvas.landscapeReferences.length > 0) {
10280
- sections.push("**Landscape & References:**");
10281
- for (const ref of canvas.landscapeReferences) {
10282
- const url = ref.url ? ` (${ref.url})` : "";
10283
- const notes = ref.notes ? ` \u2014 ${ref.notes}` : "";
10284
- sections.push(`- ${ref.name}${url}${notes}`);
10285
- }
10286
- }
10287
- if (canvas.userJourneys && canvas.userJourneys.length > 0) {
10288
- sections.push("**User Journeys:**");
10289
- for (const j of canvas.userJourneys) {
10290
- const priority = j.priority ? ` [${j.priority}]` : "";
10291
- sections.push(`- **${j.persona}:** ${j.journey}${priority}`);
10292
- }
10293
- }
10294
- if (canvas.mvpBoundary) {
10295
- sections.push("**MVP Boundary:**", canvas.mvpBoundary);
10296
- }
10297
- if (canvas.assumptionsOpenQuestions && canvas.assumptionsOpenQuestions.length > 0) {
10298
- sections.push("**Assumptions & Open Questions:**");
10299
- for (const a of canvas.assumptionsOpenQuestions) {
10300
- const evidence = a.evidence ? ` Evidence: ${a.evidence}` : "";
10301
- sections.push(`- [${a.status}] ${a.text}${evidence}`);
10302
- }
10303
- }
10304
- if (canvas.successSignals && canvas.successSignals.length > 0) {
10305
- sections.push("**Success Signals:**");
10306
- for (const s of canvas.successSignals) {
10307
- const metric = s.metric ? ` (${s.metric}` + (s.target ? `, target: ${s.target})` : ")") : "";
10308
- sections.push(`- ${s.signal}${metric}`);
10309
- }
10310
- }
10311
- return sections.length > 0 ? sections.join("\n") : void 0;
10312
- }
10313
10494
  function formatEstimationCalibration(rows) {
10314
10495
  const total = rows.reduce((sum, r) => sum + r.count, 0);
10315
10496
  const accurate = rows.filter((r) => r.accuracyLabel === "accurate").reduce((sum, r) => sum + r.count, 0);
@@ -10509,35 +10690,8 @@ async function assembleContext(adapter2, mode, _config, filters, focus) {
10509
10690
  ]);
10510
10691
  timings["total"] = totalTimer();
10511
10692
  console.error(`[plan-perf] assembleContext (lean): ${JSON.stringify(timings)}ms`);
10512
- let taskCommentsText;
10513
- try {
10514
- const comments = await adapter2.getRecentTaskComments?.(20);
10515
- if (comments && comments.length > 0) {
10516
- const byTask = /* @__PURE__ */ new Map();
10517
- for (const c of comments) {
10518
- const list = byTask.get(c.taskId) ?? [];
10519
- list.push(c);
10520
- byTask.set(c.taskId, list);
10521
- }
10522
- const lines = [];
10523
- for (const [taskId, taskComments] of byTask) {
10524
- const limited = taskComments.slice(0, 5);
10525
- for (const c of limited) {
10526
- const truncated = c.content.length > 200 ? c.content.slice(0, 200) + "..." : c.content;
10527
- const date = c.createdAt.split("T")[0];
10528
- lines.push(`- **${taskId}** \u2014 ${c.author} (${date}): "${truncated}"`);
10529
- }
10530
- }
10531
- taskCommentsText = lines.join("\n");
10532
- }
10533
- } catch {
10534
- }
10535
- let discoveryCanvasText;
10536
- try {
10537
- const canvas = await adapter2.readDiscoveryCanvas();
10538
- discoveryCanvasText = formatDiscoveryCanvas(canvas);
10539
- } catch {
10540
- }
10693
+ const taskCommentsText = await assembleTaskComments(adapter2);
10694
+ const discoveryCanvasText = await assembleDiscoveryCanvasText(adapter2);
10541
10695
  let estimationCalibrationText;
10542
10696
  try {
10543
10697
  const calibrationRows = await adapter2.getEstimationCalibration?.();
@@ -10623,35 +10777,8 @@ async function assembleContext(adapter2, mode, _config, filters, focus) {
10623
10777
  ]);
10624
10778
  timings["total"] = totalTimer();
10625
10779
  console.error(`[plan-perf] assembleContext (full): ${JSON.stringify(timings)}ms`);
10626
- let discoveryCanvasTextFull;
10627
- try {
10628
- const canvas = await adapter2.readDiscoveryCanvas();
10629
- discoveryCanvasTextFull = formatDiscoveryCanvas(canvas);
10630
- } catch {
10631
- }
10632
- let taskCommentsTextFull;
10633
- try {
10634
- const comments = await adapter2.getRecentTaskComments?.(20);
10635
- if (comments && comments.length > 0) {
10636
- const byTask = /* @__PURE__ */ new Map();
10637
- for (const c of comments) {
10638
- const list = byTask.get(c.taskId) ?? [];
10639
- list.push(c);
10640
- byTask.set(c.taskId, list);
10641
- }
10642
- const lines = [];
10643
- for (const [taskId, taskComments] of byTask) {
10644
- const limited = taskComments.slice(0, 5);
10645
- for (const c of limited) {
10646
- const truncated = c.content.length > 200 ? c.content.slice(0, 200) + "..." : c.content;
10647
- const date = c.createdAt.split("T")[0];
10648
- lines.push(`- **${taskId}** \u2014 ${c.author} (${date}): "${truncated}"`);
10649
- }
10650
- }
10651
- taskCommentsTextFull = lines.join("\n");
10652
- }
10653
- } catch {
10654
- }
10780
+ const discoveryCanvasTextFull = await assembleDiscoveryCanvasText(adapter2);
10781
+ const taskCommentsTextFull = await assembleTaskComments(adapter2);
10655
10782
  let ctx = {
10656
10783
  mode,
10657
10784
  cycleNumber: health.totalCycles,
@@ -10746,7 +10873,7 @@ ${cleanContent}`;
10746
10873
  title: t.title,
10747
10874
  status: t.status || "Backlog",
10748
10875
  priority: t.priority || "P1 High",
10749
- complexity: t.complexity || "Medium",
10876
+ complexity: t.complexity || "Small",
10750
10877
  module: t.module || "Core",
10751
10878
  epic: t.epic || "Platform",
10752
10879
  phase: t.phase || "Phase 1",
@@ -10852,7 +10979,7 @@ ${cleanContent}`;
10852
10979
  title: task.title,
10853
10980
  status: task.status || "Backlog",
10854
10981
  priority: task.priority || "P1 High",
10855
- complexity: task.complexity || "Medium",
10982
+ complexity: task.complexity || "Small",
10856
10983
  module: task.module || "Core",
10857
10984
  epic: task.epic || "Platform",
10858
10985
  phase: task.phase || "Phase 1",
@@ -11229,40 +11356,140 @@ async function applyPlan(adapter2, config2, rawLlmOutput, mode, cycleNumber, str
11229
11356
  return Promise.race([workPromise, timeoutPromise]);
11230
11357
  }
11231
11358
 
11232
- // src/tools/plan.ts
11233
- var lastPrepareContextHashes;
11234
- var lastPrepareUserMessage;
11235
- var lastPrepareContextBytes;
11236
- var planTool = {
11237
- name: "plan",
11238
- description: 'Run once per cycle to generate BUILD HANDOFFs for up to 10 tasks. Call after setup (first time) or after completing all builds from the previous cycle. Returns prioritised task recommendations with detailed implementation specs. Do not call between builds \u2014 use build_execute to work through existing handoffs first. First call returns a planning prompt for you to execute (prepare phase). Then call again with mode "apply" and your output to write results.',
11239
- inputSchema: {
11240
- type: "object",
11241
- properties: {
11242
- mode: {
11243
- type: "string",
11244
- enum: ["prepare", "apply"],
11245
- description: '"prepare" returns the planning prompt for you to execute. "apply" accepts your generated output and persists the results. Defaults to "prepare" when omitted.'
11246
- },
11247
- llm_response: {
11248
- type: "string",
11249
- description: 'Your raw output from executing the plan prompt (mode "apply" only). Must include both Part 1 (markdown) and Part 2 (structured JSON after <!-- PAPI_STRUCTURED_OUTPUT -->).'
11250
- },
11251
- plan_mode: {
11252
- type: "string",
11253
- enum: ["bootstrap", "full"],
11254
- description: 'The plan mode returned from prepare phase (mode "apply" only).'
11255
- },
11256
- cycle_number: {
11257
- type: "number",
11258
- description: 'The cycle number returned from prepare phase (mode "apply" only).'
11259
- },
11260
- strategy_review_warning: {
11261
- type: "string",
11262
- description: 'The strategy review warning returned from prepare phase (mode "apply" only). Pass empty string if none.'
11263
- },
11264
- phase: {
11265
- type: "string",
11359
+ // src/lib/phase-realign.ts
11360
+ function extractPhaseNumber(phaseField) {
11361
+ const match = phaseField.match(/Phase\s+(\d+)/i);
11362
+ return match ? parseInt(match[1], 10) : null;
11363
+ }
11364
+ var STATUS_ORDER = {
11365
+ "Not Started": 0,
11366
+ "In Progress": 1,
11367
+ "Done": 2,
11368
+ "Deferred": 3
11369
+ // Manual override — protected from auto-propagation
11370
+ };
11371
+ function isForwardTransition(currentStatus, newStatus) {
11372
+ const currentOrder = STATUS_ORDER[currentStatus] ?? 99;
11373
+ const newOrder = STATUS_ORDER[newStatus] ?? 99;
11374
+ return newOrder >= currentOrder;
11375
+ }
11376
+ function derivePhaseStatus(tasks) {
11377
+ if (tasks.length === 0) return null;
11378
+ const terminal = /* @__PURE__ */ new Set(["Done", "Cancelled"]);
11379
+ const active = /* @__PURE__ */ new Set(["In Progress", "In Review"]);
11380
+ const allTerminal = tasks.every((t) => terminal.has(t.status));
11381
+ if (allTerminal) return "Done";
11382
+ const hasActive = tasks.some((t) => active.has(t.status));
11383
+ if (hasActive) return "In Progress";
11384
+ const hasDone = tasks.some((t) => terminal.has(t.status));
11385
+ if (hasDone) return "In Progress";
11386
+ return "Not Started";
11387
+ }
11388
+ function realignPhases(phases, tasks) {
11389
+ if (phases.length === 0) {
11390
+ return { changes: [], updatedPhases: phases };
11391
+ }
11392
+ const tasksByLabel = /* @__PURE__ */ new Map();
11393
+ const tasksByOrder = /* @__PURE__ */ new Map();
11394
+ for (const task of tasks) {
11395
+ if (task.phase) {
11396
+ const existing = tasksByLabel.get(task.phase) ?? [];
11397
+ existing.push(task);
11398
+ tasksByLabel.set(task.phase, existing);
11399
+ }
11400
+ const num = extractPhaseNumber(task.phase);
11401
+ if (num !== null) {
11402
+ const existing = tasksByOrder.get(num) ?? [];
11403
+ existing.push(task);
11404
+ tasksByOrder.set(num, existing);
11405
+ }
11406
+ }
11407
+ const changes = [];
11408
+ const result = [];
11409
+ for (const phase of phases) {
11410
+ const phaseTasks = tasksByLabel.get(phase.label) ?? tasksByOrder.get(phase.order) ?? [];
11411
+ const derivedStatus = derivePhaseStatus(phaseTasks);
11412
+ if (derivedStatus !== null && derivedStatus !== phase.status && isForwardTransition(phase.status, derivedStatus)) {
11413
+ changes.push({
11414
+ phaseId: phase.id,
11415
+ oldStatus: phase.status,
11416
+ newStatus: derivedStatus
11417
+ });
11418
+ result.push({ ...phase, status: derivedStatus });
11419
+ } else {
11420
+ result.push(phase);
11421
+ }
11422
+ }
11423
+ return { changes, updatedPhases: result };
11424
+ }
11425
+ function formatPhaseChanges(changes) {
11426
+ if (changes.length === 0) return "";
11427
+ const lines = ["**Phase Realignment:**"];
11428
+ for (const c of changes) {
11429
+ lines.push(`- ${c.phaseId}: ${c.oldStatus} \u2192 ${c.newStatus}`);
11430
+ }
11431
+ return lines.join("\n");
11432
+ }
11433
+ async function propagatePhaseStatus(adapter2) {
11434
+ const [phases, tasks] = await Promise.all([
11435
+ adapter2.readPhases(),
11436
+ adapter2.queryBoard()
11437
+ ]);
11438
+ if (phases.length === 0) return [];
11439
+ const horizons = await adapter2.readHorizons?.() ?? [];
11440
+ const stages = await adapter2.readStages?.() ?? [];
11441
+ let eligiblePhases = phases;
11442
+ if (horizons.length > 1 && stages.length > 0) {
11443
+ const h1 = horizons.reduce((min, h) => h.sortOrder < min.sortOrder ? h : min, horizons[0]);
11444
+ const h1StageIds = new Set(stages.filter((s) => s.horizonId === h1.id).map((s) => s.id));
11445
+ eligiblePhases = phases.filter((p) => !p.stageId || h1StageIds.has(p.stageId));
11446
+ }
11447
+ const { changes, updatedPhases } = realignPhases(eligiblePhases, tasks);
11448
+ if (changes.length > 0) {
11449
+ const updatedIds = new Set(updatedPhases.map((p) => p.id));
11450
+ const mergedPhases = [
11451
+ ...updatedPhases,
11452
+ ...phases.filter((p) => !updatedIds.has(p.id))
11453
+ ];
11454
+ await adapter2.writePhases(mergedPhases);
11455
+ }
11456
+ return changes;
11457
+ }
11458
+
11459
+ // src/tools/plan.ts
11460
+ var lastPrepareContextHashes;
11461
+ var lastPrepareUserMessage;
11462
+ var lastPrepareContextBytes;
11463
+ var planTool = {
11464
+ name: "plan",
11465
+ description: 'Run once per cycle to generate BUILD HANDOFFs for up to 10 tasks. Call after setup (first time) or after completing all builds from the previous cycle. Returns prioritised task recommendations with detailed implementation specs. Do not call between builds \u2014 use build_execute to work through existing handoffs first. First call returns a planning prompt for you to execute (prepare phase). Then call again with mode "apply" and your output to write results.',
11466
+ inputSchema: {
11467
+ type: "object",
11468
+ properties: {
11469
+ mode: {
11470
+ type: "string",
11471
+ enum: ["prepare", "apply"],
11472
+ description: '"prepare" returns the planning prompt for you to execute. "apply" accepts your generated output and persists the results. Defaults to "prepare" when omitted.'
11473
+ },
11474
+ llm_response: {
11475
+ type: "string",
11476
+ description: 'Your raw output from executing the plan prompt (mode "apply" only). Must include both Part 1 (markdown) and Part 2 (structured JSON after <!-- PAPI_STRUCTURED_OUTPUT -->).'
11477
+ },
11478
+ plan_mode: {
11479
+ type: "string",
11480
+ enum: ["bootstrap", "full"],
11481
+ description: 'The plan mode returned from prepare phase (mode "apply" only).'
11482
+ },
11483
+ cycle_number: {
11484
+ type: "number",
11485
+ description: 'The cycle number returned from prepare phase (mode "apply" only).'
11486
+ },
11487
+ strategy_review_warning: {
11488
+ type: "string",
11489
+ description: 'The strategy review warning returned from prepare phase (mode "apply" only). Pass empty string if none.'
11490
+ },
11491
+ phase: {
11492
+ type: "string",
11266
11493
  description: 'Filter board tasks to only this phase (e.g. "Phase 8"). Other context (build reports, ADs) is unaffected.'
11267
11494
  },
11268
11495
  module: {
@@ -11368,6 +11595,10 @@ async function handlePlan(adapter2, config2, args) {
11368
11595
  };
11369
11596
  }
11370
11597
  {
11598
+ try {
11599
+ await propagatePhaseStatus(adapter2);
11600
+ } catch {
11601
+ }
11371
11602
  const result = await preparePlan(adapter2, config2, filters, focus, force);
11372
11603
  lastPrepareContextHashes = result.contextHashes;
11373
11604
  lastPrepareUserMessage = result.userMessage;
@@ -11422,81 +11653,9 @@ ${result.userMessage}
11422
11653
  init_dist2();
11423
11654
  import { randomUUID as randomUUID8, createHash as createHash2 } from "crypto";
11424
11655
  import { execFileSync as execFileSync2 } from "child_process";
11425
-
11426
- // src/lib/phase-realign.ts
11427
- function extractPhaseNumber(phaseField) {
11428
- const match = phaseField.match(/Phase\s+(\d+)/i);
11429
- return match ? parseInt(match[1], 10) : null;
11430
- }
11431
- function derivePhaseStatus(tasks) {
11432
- if (tasks.length === 0) return null;
11433
- const terminal = /* @__PURE__ */ new Set(["Done", "Cancelled"]);
11434
- const active = /* @__PURE__ */ new Set(["In Progress", "In Review"]);
11435
- const allTerminal = tasks.every((t) => terminal.has(t.status));
11436
- if (allTerminal) return "Done";
11437
- const hasActive = tasks.some((t) => active.has(t.status));
11438
- if (hasActive) return "In Progress";
11439
- const hasDone = tasks.some((t) => terminal.has(t.status));
11440
- if (hasDone) return "In Progress";
11441
- return "Not Started";
11442
- }
11443
- function realignPhases(phases, tasks) {
11444
- if (phases.length === 0) {
11445
- return { changes: [], updatedPhases: phases };
11446
- }
11447
- const tasksByLabel = /* @__PURE__ */ new Map();
11448
- const tasksByOrder = /* @__PURE__ */ new Map();
11449
- for (const task of tasks) {
11450
- if (task.phase) {
11451
- const existing = tasksByLabel.get(task.phase) ?? [];
11452
- existing.push(task);
11453
- tasksByLabel.set(task.phase, existing);
11454
- }
11455
- const num = extractPhaseNumber(task.phase);
11456
- if (num !== null) {
11457
- const existing = tasksByOrder.get(num) ?? [];
11458
- existing.push(task);
11459
- tasksByOrder.set(num, existing);
11460
- }
11461
- }
11462
- const changes = [];
11463
- const result = [];
11464
- for (const phase of phases) {
11465
- const phaseTasks = tasksByLabel.get(phase.label) ?? tasksByOrder.get(phase.order) ?? [];
11466
- const derivedStatus = derivePhaseStatus(phaseTasks);
11467
- if (derivedStatus !== null && derivedStatus !== phase.status) {
11468
- changes.push({
11469
- phaseId: phase.id,
11470
- oldStatus: phase.status,
11471
- newStatus: derivedStatus
11472
- });
11473
- result.push({ ...phase, status: derivedStatus });
11474
- } else {
11475
- result.push(phase);
11476
- }
11477
- }
11478
- return { changes, updatedPhases: result };
11479
- }
11480
- function formatPhaseChanges(changes) {
11481
- if (changes.length === 0) return "";
11482
- const lines = ["**Phase Realignment:**"];
11483
- for (const c of changes) {
11484
- lines.push(`- ${c.phaseId}: ${c.oldStatus} \u2192 ${c.newStatus}`);
11485
- }
11486
- return lines.join("\n");
11487
- }
11488
- async function propagatePhaseStatus(adapter2) {
11489
- const [phases, tasks] = await Promise.all([
11490
- adapter2.readPhases(),
11491
- adapter2.queryBoard()
11492
- ]);
11493
- if (phases.length === 0) return [];
11494
- const { changes, updatedPhases } = realignPhases(phases, tasks);
11495
- if (changes.length > 0) {
11496
- await adapter2.writePhases(updatedPhases);
11497
- }
11498
- return changes;
11499
- }
11656
+ import { existsSync, readdirSync, statSync } from "fs";
11657
+ import { join as join2 } from "path";
11658
+ import { homedir } from "os";
11500
11659
 
11501
11660
  // src/lib/value-report.ts
11502
11661
  var MIN_SNAPSHOTS = 5;
@@ -11793,7 +11952,8 @@ async function assembleContext2(adapter2, cycleNumber, cyclesSinceLastReview, pr
11793
11952
  canvas,
11794
11953
  decisionUsage,
11795
11954
  recData,
11796
- pendingRecs
11955
+ pendingRecs,
11956
+ registeredDocs
11797
11957
  ] = await Promise.all([
11798
11958
  adapter2.readProductBrief(),
11799
11959
  adapter2.getActiveDecisions(),
@@ -11815,7 +11975,9 @@ async function assembleContext2(adapter2, cycleNumber, cyclesSinceLastReview, pr
11815
11975
  adapter2.readDiscoveryCanvas().catch(() => ({})),
11816
11976
  adapter2.getDecisionUsage(cycleNumber).catch(() => []),
11817
11977
  adapter2.getRecommendationEffectiveness?.()?.catch(() => []) ?? Promise.resolve([]),
11818
- adapter2.getPendingRecommendations().catch(() => [])
11978
+ adapter2.getPendingRecommendations().catch(() => []),
11979
+ // Doc registry — summaries for strategy review context
11980
+ adapter2.searchDocs?.({ status: "active", limit: 10 })?.catch(() => []) ?? Promise.resolve([])
11819
11981
  ]);
11820
11982
  const tasks = [...activeTasks, ...recentDoneTasks];
11821
11983
  const recentLog = log;
@@ -11834,7 +11996,7 @@ async function assembleContext2(adapter2, cycleNumber, cyclesSinceLastReview, pr
11834
11996
  const previousReviewsText = formatPreviousReviews(previousStrategyReviews);
11835
11997
  const cappedBrief = capProductBrief2(productBrief);
11836
11998
  const smartBoard = formatBoardForReviewSmart(tasks, lastReviewCycleNum);
11837
- const buildReportsText = buildPatternsText ? formatRecentReportsSummary(reports, 10) : formatBuildReports(reports);
11999
+ const buildReportsText = formatRecentReportsSummary(reports, 10);
11838
12000
  logDataSourceSummary("strategy_review", [
11839
12001
  { label: "productBrief", hasData: warnIfEmpty("readProductBrief", productBrief) },
11840
12002
  { label: "activeDecisions", hasData: warnIfEmpty("getActiveDecisions", decisions) },
@@ -11849,7 +12011,7 @@ async function assembleContext2(adapter2, cycleNumber, cyclesSinceLastReview, pr
11849
12011
  ]);
11850
12012
  let discoveryCanvasText;
11851
12013
  try {
11852
- const fullCanvasText = formatDiscoveryCanvasForReview(canvas);
12014
+ const fullCanvasText = formatDiscoveryCanvas(canvas);
11853
12015
  if (fullCanvasText) {
11854
12016
  const canvasHash = createHash2("md5").update(fullCanvasText).digest("hex");
11855
12017
  const lastReview = previousStrategyReviews?.[0];
@@ -11865,7 +12027,7 @@ async function assembleContext2(adapter2, cycleNumber, cyclesSinceLastReview, pr
11865
12027
  const briefImplicationsFromBuilds = reports.filter((r) => Array.isArray(r.briefImplications) && r.briefImplications.length > 0).flatMap((r) => r.briefImplications.map((bi) => ({ ...bi, taskName: r.taskName, cycle: r.cycle })));
11866
12028
  let briefImplicationsText;
11867
12029
  if (briefImplicationsFromBuilds.length > 0) {
11868
- briefImplicationsText = "**Build-Discovered Evidence:**\n" + briefImplicationsFromBuilds.map((bi) => `- [S${bi.cycle} ${bi.taskName}] [${bi.canvasSection}/${bi.type}] ${bi.detail}`).join("\n");
12030
+ briefImplicationsText = "**Build-Discovered Evidence:**\n" + briefImplicationsFromBuilds.map((bi) => `- [C${bi.cycle} ${bi.taskName}] [${bi.canvasSection}/${bi.type}] ${bi.detail}`).join("\n");
11869
12031
  }
11870
12032
  let phasesText;
11871
12033
  try {
@@ -11908,6 +12070,61 @@ ${lines.join("\n")}`;
11908
12070
  adHocCommitsText = getAdHocCommits(projectRoot, sinceTag);
11909
12071
  } catch {
11910
12072
  }
12073
+ let registeredDocsText;
12074
+ try {
12075
+ if (registeredDocs && registeredDocs.length > 0) {
12076
+ const lines = registeredDocs.map(
12077
+ (d) => `- **${d.title}** (${d.type}, ${d.status}) \u2014 ${d.summary}`
12078
+ );
12079
+ registeredDocsText = `${registeredDocs.length} registered doc(s):
12080
+ ${lines.join("\n")}`;
12081
+ }
12082
+ } catch {
12083
+ }
12084
+ let recentPlansText;
12085
+ try {
12086
+ const plansDir = join2(homedir(), ".claude", "plans");
12087
+ if (existsSync(plansDir)) {
12088
+ const lastReviewDate = previousStrategyReviews?.[0]?.date ? new Date(previousStrategyReviews[0].date) : /* @__PURE__ */ new Date(0);
12089
+ const planFiles = readdirSync(plansDir).filter((f) => f.endsWith(".md")).map((f) => {
12090
+ const fullPath = join2(plansDir, f);
12091
+ const stat2 = statSync(fullPath);
12092
+ return { name: f, modified: stat2.mtime, size: stat2.size };
12093
+ }).filter((f) => f.modified > lastReviewDate).sort((a, b2) => b2.modified.getTime() - a.modified.getTime()).slice(0, 15);
12094
+ if (planFiles.length > 0) {
12095
+ const lines = planFiles.map((f) => {
12096
+ const kb = (f.size / 1024).toFixed(1);
12097
+ return `- ${f.name} (${kb}KB, ${f.modified.toISOString().slice(0, 10)})`;
12098
+ });
12099
+ recentPlansText = `${planFiles.length} plan file(s) modified since last review:
12100
+ ${lines.join("\n")}`;
12101
+ }
12102
+ }
12103
+ } catch {
12104
+ }
12105
+ let unregisteredDocsText;
12106
+ try {
12107
+ const docsDir = join2(projectRoot, "docs");
12108
+ if (existsSync(docsDir)) {
12109
+ const registeredPaths = new Set(
12110
+ (registeredDocs ?? []).map((d) => d.path).filter(Boolean)
12111
+ );
12112
+ const allDocFiles = [];
12113
+ const scanDir = (dir, prefix) => {
12114
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
12115
+ if (entry.isDirectory()) scanDir(join2(dir, entry.name), `${prefix}${entry.name}/`);
12116
+ else if (entry.name.endsWith(".md")) allDocFiles.push(`${prefix}${entry.name}`);
12117
+ }
12118
+ };
12119
+ scanDir(docsDir, "docs/");
12120
+ const unregistered = allDocFiles.filter((f) => !registeredPaths.has(f));
12121
+ if (unregistered.length > 0) {
12122
+ unregisteredDocsText = `${unregistered.length} unregistered doc(s) in docs/:
12123
+ ${unregistered.slice(0, 10).map((f) => `- ${f}`).join("\n")}`;
12124
+ }
12125
+ }
12126
+ } catch {
12127
+ }
11911
12128
  logDataSourceSummary("strategy_review_audit", [
11912
12129
  { label: "discoveryCanvas", hasData: discoveryCanvasText !== void 0 },
11913
12130
  { label: "briefImplications", hasData: briefImplicationsText !== void 0 },
@@ -11915,7 +12132,10 @@ ${lines.join("\n")}`;
11915
12132
  { label: "decisionUsage", hasData: decisionUsageText !== void 0 },
11916
12133
  { label: "recEffectiveness", hasData: recEffectivenessText !== void 0 },
11917
12134
  { label: "pendingRecs", hasData: pendingRecsText !== void 0 },
11918
- { label: "adHocCommits", hasData: adHocCommitsText !== void 0 }
12135
+ { label: "adHocCommits", hasData: adHocCommitsText !== void 0 },
12136
+ { label: "registeredDocs", hasData: registeredDocsText !== void 0 },
12137
+ { label: "recentPlans", hasData: recentPlansText !== void 0 },
12138
+ { label: "unregisteredDocs", hasData: unregisteredDocsText !== void 0 }
11919
12139
  ]);
11920
12140
  const context = {
11921
12141
  sessionNumber: cycleNumber,
@@ -11937,10 +12157,13 @@ ${lines.join("\n")}`;
11937
12157
  northStar: currentNorthStar ?? void 0,
11938
12158
  recommendationEffectiveness: recEffectivenessText,
11939
12159
  adHocCommits: adHocCommitsText,
11940
- pendingRecommendations: pendingRecsText
12160
+ pendingRecommendations: pendingRecsText,
12161
+ registeredDocs: registeredDocsText,
12162
+ recentPlans: recentPlansText,
12163
+ unregisteredDocs: unregisteredDocsText
11941
12164
  };
11942
- const BUDGET_SOFT2 = 8e4;
11943
- const BUDGET_HARD2 = 1e5;
12165
+ const BUDGET_SOFT2 = 5e4;
12166
+ const BUDGET_HARD2 = 6e4;
11944
12167
  const compressionSteps = [];
11945
12168
  function measureContext(ctx) {
11946
12169
  return Object.values(ctx).filter((v) => typeof v === "string").reduce((sum, s) => sum + s.length, 0);
@@ -11969,13 +12192,31 @@ ${lines.join("\n")}`;
11969
12192
  }
11970
12193
  }
11971
12194
  if (contextSize > BUDGET_SOFT2 && context.allBuildReports) {
11972
- const summary = formatRecentReportsSummary(reports, 10);
12195
+ const summary = formatRecentReportsSummary(reports, 5);
11973
12196
  if (summary.length < context.allBuildReports.length) {
11974
12197
  context.allBuildReports = summary;
11975
12198
  contextSize = measureContext(context);
11976
- compressionSteps.push("Step 3: build reports summarized");
12199
+ compressionSteps.push("Step 3: build reports capped to 5");
12200
+ }
12201
+ }
12202
+ if (contextSize > BUDGET_SOFT2 && context.sessionLog) {
12203
+ const logLines = context.sessionLog.split("\n---\n");
12204
+ if (logLines.length > 5) {
12205
+ context.sessionLog = logLines.slice(0, 5).join("\n---\n") + `
12206
+
12207
+ *(${logLines.length - 5} older cycle log entries omitted for context budget)*`;
12208
+ contextSize = measureContext(context);
12209
+ compressionSteps.push(`Step 4: cycle log capped to 5 (was ${logLines.length})`);
11977
12210
  }
11978
12211
  }
12212
+ if (contextSize > BUDGET_SOFT2 && context.board) {
12213
+ context.board = context.board.replace(
12214
+ / Notes: .{100,}/g,
12215
+ (match) => match.slice(0, match.indexOf("Notes: ") + 107) + "..."
12216
+ );
12217
+ contextSize = measureContext(context);
12218
+ compressionSteps.push("Step 5: board task notes truncated to 100 chars");
12219
+ }
11979
12220
  if (contextSize > BUDGET_HARD2) {
11980
12221
  const entries = Object.entries(context).filter((e) => typeof e[1] === "string").sort((a, b2) => b2[1].length - a[1].length);
11981
12222
  if (entries.length > 0) {
@@ -11984,7 +12225,7 @@ ${lines.join("\n")}`;
11984
12225
  const newLength = Math.max(value.length - excess, 1e3);
11985
12226
  context[key] = value.slice(0, newLength) + "\n\n[truncated \u2014 context budget exceeded]";
11986
12227
  contextSize = measureContext(context);
11987
- compressionSteps.push(`Step 4: truncated ${key} (was ${value.length} chars)`);
12228
+ compressionSteps.push(`Step 6: truncated ${key} (was ${value.length} chars)`);
11988
12229
  }
11989
12230
  }
11990
12231
  const estimatedTokens = Math.ceil(contextSize / 4);
@@ -12016,7 +12257,7 @@ ${cleanContent}`;
12016
12257
  const sd = data;
12017
12258
  try {
12018
12259
  const currentCanvas = await adapter2.readDiscoveryCanvas();
12019
- const canvasText = formatDiscoveryCanvasForReview(currentCanvas);
12260
+ const canvasText = formatDiscoveryCanvas(currentCanvas);
12020
12261
  if (canvasText) {
12021
12262
  return { ...sd, canvasHash: createHash2("md5").update(canvasText).digest("hex") };
12022
12263
  }
@@ -12257,7 +12498,8 @@ ${pending.rawResponse}`
12257
12498
  return {
12258
12499
  cycleNumber,
12259
12500
  systemPrompt,
12260
- userMessage
12501
+ userMessage,
12502
+ contextSizeChars: userMessage.length
12261
12503
  };
12262
12504
  }
12263
12505
  async function applyStrategyReviewOutput(adapter2, rawLlmOutput, cycleNumber) {
@@ -12341,42 +12583,6 @@ function formatRecentReportsSummary(reports, count) {
12341
12583
  return `- S${r.cycle} ${r.taskName}: ${effort}${surprises}`;
12342
12584
  }).join("\n");
12343
12585
  }
12344
- function formatDiscoveryCanvasForReview(canvas) {
12345
- const sections = [];
12346
- if (canvas.landscapeReferences && canvas.landscapeReferences.length > 0) {
12347
- sections.push("**Landscape & References:**");
12348
- for (const ref of canvas.landscapeReferences) {
12349
- const url = ref.url ? ` (${ref.url})` : "";
12350
- const notes = ref.notes ? ` \u2014 ${ref.notes}` : "";
12351
- sections.push(`- ${ref.name}${url}${notes}`);
12352
- }
12353
- }
12354
- if (canvas.userJourneys && canvas.userJourneys.length > 0) {
12355
- sections.push("**User Journeys:**");
12356
- for (const j of canvas.userJourneys) {
12357
- const priority = j.priority ? ` [${j.priority}]` : "";
12358
- sections.push(`- **${j.persona}:** ${j.journey}${priority}`);
12359
- }
12360
- }
12361
- if (canvas.mvpBoundary) {
12362
- sections.push("**MVP Boundary:**", canvas.mvpBoundary);
12363
- }
12364
- if (canvas.assumptionsOpenQuestions && canvas.assumptionsOpenQuestions.length > 0) {
12365
- sections.push("**Assumptions & Open Questions:**");
12366
- for (const a of canvas.assumptionsOpenQuestions) {
12367
- const evidence = a.evidence ? ` Evidence: ${a.evidence}` : "";
12368
- sections.push(`- [${a.status}] ${a.text}${evidence}`);
12369
- }
12370
- }
12371
- if (canvas.successSignals && canvas.successSignals.length > 0) {
12372
- sections.push("**Success Signals:**");
12373
- for (const s of canvas.successSignals) {
12374
- const metric = s.metric ? ` (${s.metric}` + (s.target ? `, target: ${s.target})` : ")") : "";
12375
- sections.push(`- ${s.signal}${metric}`);
12376
- }
12377
- }
12378
- return sections.length > 0 ? sections.join("\n") : void 0;
12379
- }
12380
12586
  function formatPhasesForReview(phases, currentCycle) {
12381
12587
  if (phases.length === 0) return void 0;
12382
12588
  const lines = [];
@@ -12422,9 +12628,6 @@ async function formatHierarchyForReview(adapter2, currentCycle, prefetchedTasks)
12422
12628
  existing.push(s);
12423
12629
  stagesByHorizon.set(s.horizonId, existing);
12424
12630
  }
12425
- const phasesByStage = /* @__PURE__ */ new Map();
12426
- for (const p of phases) {
12427
- }
12428
12631
  for (const h of horizons) {
12429
12632
  lines.push(`### ${h.label} \u2014 ${h.status}`);
12430
12633
  if (h.description) lines.push(`> ${h.description}`);
@@ -12496,6 +12699,11 @@ function buildStrategyChangeUserMessage(cycleNumber, text, productBrief, activeD
12496
12699
  if (buildReports && buildReports.length > 0) {
12497
12700
  parts.push("### Recent Velocity (last 3 cycles)", "", formatVelocitySummary(buildReports, 3), "");
12498
12701
  parts.push("### Recent Build Reports", "", formatRecentReportsSummary(buildReports, 5), "");
12702
+ const briefImplicationsFromBuilds = buildReports.filter((r) => Array.isArray(r.briefImplications) && r.briefImplications.length > 0).flatMap((r) => r.briefImplications.map((bi) => ({ ...bi, taskName: r.taskName, cycle: r.cycle })));
12703
+ if (briefImplicationsFromBuilds.length > 0) {
12704
+ const briefImplicationsText = "**Build-Discovered Evidence:**\n" + briefImplicationsFromBuilds.map((bi) => `- [C${bi.cycle} ${bi.taskName}] [${bi.canvasSection}/${bi.type}] ${bi.detail}`).join("\n");
12705
+ parts.push("### Brief Implications (from builds)", "", briefImplicationsText, "");
12706
+ }
12499
12707
  }
12500
12708
  if (previousReviews) {
12501
12709
  parts.push("### Previous Strategy Reviews", "", previousReviews, "");
@@ -12581,12 +12789,14 @@ async function prepareStrategyChange(adapter2, text) {
12581
12789
  tasks = boardTasks;
12582
12790
  buildReports = reports;
12583
12791
  previousReviewsText = formatPreviousReviews(previousReviews);
12792
+ const hasBriefImplications = reports.some((r) => Array.isArray(r.briefImplications) && r.briefImplications.length > 0);
12584
12793
  logDataSourceSummary("strategy_change", [
12585
12794
  { label: "productBrief", hasData: warnIfEmpty("readProductBrief", brief) },
12586
12795
  { label: "activeDecisions", hasData: warnIfEmpty("getActiveDecisions", decisions) },
12587
12796
  { label: "phases", hasData: warnIfEmpty("readPhases", readPhases) },
12588
12797
  { label: "boardTasks", hasData: warnIfEmpty("queryBoard", boardTasks) },
12589
12798
  { label: "buildReports", hasData: warnIfEmpty("getRecentBuildReports", reports) },
12799
+ { label: "briefImplications", hasData: hasBriefImplications },
12590
12800
  { label: "previousReviews", hasData: previousReviewsText !== void 0 }
12591
12801
  ]);
12592
12802
  } catch (err) {
@@ -12788,11 +12998,14 @@ ${result.slackWarning}` : "";
12788
12998
  }
12789
12999
  lastReviewUserMessage = result.userMessage;
12790
13000
  lastReviewContextBytes = Buffer.byteLength(result.userMessage, "utf-8");
13001
+ const sizeNote = result.contextSizeChars ? `
13002
+ **Context size:** ${result.contextSizeChars.toLocaleString()} chars (~${Math.ceil(result.contextSizeChars / 4).toLocaleString()} tokens)
13003
+ ` : "";
12791
13004
  return textResponse(
12792
13005
  `## PAPI Strategy Review \u2014 Prepare Phase (Cycle ${result.cycleNumber})
12793
13006
 
12794
13007
  Follow the system prompt and context below to generate a strategy review.
12795
-
13008
+ ` + sizeNote + `
12796
13009
  When done, call \`strategy_review\` again with:
12797
13010
  - \`mode\`: "apply"
12798
13011
  - \`llm_response\`: your complete output
@@ -13066,6 +13279,60 @@ var boardArchiveTool = {
13066
13279
  required: []
13067
13280
  }
13068
13281
  };
13282
+ var boardEditTool = {
13283
+ name: "board_edit",
13284
+ description: "Edit fields on an existing task. Supports title, priority, complexity, module, epic, phase, notes, status, and maturity. Pass task_id plus any fields to update. Does not call the Anthropic API.",
13285
+ inputSchema: {
13286
+ type: "object",
13287
+ properties: {
13288
+ task_id: {
13289
+ type: "string",
13290
+ description: 'The task ID to edit (e.g. "task-42").'
13291
+ },
13292
+ title: {
13293
+ type: "string",
13294
+ description: "New task title."
13295
+ },
13296
+ priority: {
13297
+ type: "string",
13298
+ enum: ["P0 Critical", "P1 High", "P2 Medium", "P3 Low"],
13299
+ description: "New priority level."
13300
+ },
13301
+ complexity: {
13302
+ type: "string",
13303
+ enum: ["XS", "Small", "Medium", "Large", "XL"],
13304
+ description: "New complexity/effort estimate."
13305
+ },
13306
+ module: {
13307
+ type: "string",
13308
+ description: "New module assignment."
13309
+ },
13310
+ epic: {
13311
+ type: "string",
13312
+ description: "New epic assignment."
13313
+ },
13314
+ phase: {
13315
+ type: "string",
13316
+ description: "New phase assignment."
13317
+ },
13318
+ notes: {
13319
+ type: "string",
13320
+ description: "New notes (replaces existing notes)."
13321
+ },
13322
+ status: {
13323
+ type: "string",
13324
+ enum: ["Backlog", "In Cycle", "Ready", "In Progress", "In Review", "Done", "Blocked", "Cancelled", "Deferred"],
13325
+ description: "New status. Must be a valid transition from the current status."
13326
+ },
13327
+ maturity: {
13328
+ type: "string",
13329
+ enum: ["raw", "investigated", "ready"],
13330
+ description: "New maturity level."
13331
+ }
13332
+ },
13333
+ required: ["task_id"]
13334
+ }
13335
+ };
13069
13336
  function pad(value, width) {
13070
13337
  return value.length >= width ? value : value + " ".repeat(width - value.length);
13071
13338
  }
@@ -13134,7 +13401,17 @@ async function handleBoardView(adapter2, args) {
13134
13401
  limit: args.limit,
13135
13402
  offset: args.offset
13136
13403
  });
13137
- return textResponse(formatBoard(result));
13404
+ let output = formatBoard(result);
13405
+ try {
13406
+ const comments = await adapter2.getRecentTaskComments?.(30);
13407
+ if (comments && comments.length > 0) {
13408
+ const taskIds = new Set(result.tasks.map((t) => t.id));
13409
+ const section = formatTaskComments(comments, taskIds, "**Task Comments:**");
13410
+ if (section) output += "\n" + section;
13411
+ }
13412
+ } catch {
13413
+ }
13414
+ return textResponse(output);
13138
13415
  }
13139
13416
  async function handleBoardDeprioritise(adapter2, args) {
13140
13417
  const taskId = args.task_id;
@@ -13228,11 +13505,49 @@ async function handleBoardArchive(adapter2, args) {
13228
13505
  ];
13229
13506
  return textResponse(lines.join("\n"));
13230
13507
  }
13508
+ var EDITABLE_FIELDS = ["title", "priority", "complexity", "module", "epic", "phase", "notes", "status", "maturity"];
13509
+ async function handleBoardEdit(adapter2, args) {
13510
+ const taskId = args.task_id;
13511
+ if (!taskId) {
13512
+ return errorResponse("task_id is required.");
13513
+ }
13514
+ const updates = {};
13515
+ const changes = [];
13516
+ for (const field of EDITABLE_FIELDS) {
13517
+ if (args[field] !== void 0 && args[field] !== null) {
13518
+ updates[field] = args[field];
13519
+ changes.push(field);
13520
+ }
13521
+ }
13522
+ if (changes.length === 0) {
13523
+ return errorResponse("No fields to update. Pass at least one field (title, priority, complexity, module, epic, phase, notes, status, maturity).");
13524
+ }
13525
+ try {
13526
+ const task = await adapter2.getTask(taskId);
13527
+ if (!task) {
13528
+ return errorResponse(`Task ${taskId} not found.`);
13529
+ }
13530
+ if (updates.status === "Backlog" && task.cycle != null) {
13531
+ updates.cycle = void 0;
13532
+ updates.cycle = null;
13533
+ if (!changes.includes("cycle")) changes.push("cycle \u2192 cleared");
13534
+ }
13535
+ await adapter2.updateTask(taskId, updates);
13536
+ const lines = [
13537
+ `Updated **${taskId}** (${updates.title ?? task.title})`,
13538
+ "",
13539
+ `**Changes:** ${changes.map((f) => `${f} \u2192 ${String(updates[f])}`).join(", ")}`
13540
+ ];
13541
+ return textResponse(lines.join("\n"));
13542
+ } catch (err) {
13543
+ return errorResponse(err instanceof Error ? err.message : String(err));
13544
+ }
13545
+ }
13231
13546
 
13232
13547
  // src/services/setup.ts
13233
13548
  init_dist2();
13234
13549
  import { mkdir, writeFile as writeFile2, readFile as readFile3, readdir, access as access2, stat } from "fs/promises";
13235
- import { join as join2, basename, extname } from "path";
13550
+ import { join as join3, basename, extname } from "path";
13236
13551
 
13237
13552
  // src/templates.ts
13238
13553
  var PLANNING_LOG_TEMPLATE = `# PAPI Planning Log
@@ -13386,15 +13701,15 @@ PAPI tools follow structured flows. The agent manages the cycle workflow automat
13386
13701
  - **Run tools automatically** \u2014 don't ask the user to invoke MCP tools manually
13387
13702
  - Before implementing: silently run \`build_execute <task_id>\` (start phase)
13388
13703
  - After implementing: run \`build_execute <task_id>\` (complete phase) with report fields
13389
- - After build_execute completes: run \`/papi-audit\` to check for bugs, convention violations, and doc drift
13390
- - After \`/papi-audit\` with findings: *MUST* automatically run \`review_submit\` with verdict \`request-changes\` and a concise summary of the audit findings as the changes requested \u2014 the builder fixes these before the task goes to human review
13391
- - After \`/papi-audit\` clean: present for human review \u2014 "Ready for your review \u2014 approve or request changes?"
13704
+ - After build_execute completes: audit the branch changes for bugs, convention violations, and doc drift (see Post-Build Audit below)
13705
+ - After audit with findings: *MUST* automatically run \`review_submit\` with verdict \`request-changes\` and a concise summary of the audit findings as the changes requested \u2014 the builder fixes these before the task goes to human review
13706
+ - After audit clean: present for human review \u2014 "Ready for your review \u2014 approve or request changes?"
13392
13707
  - User approves/requests changes \u2192 run \`review_submit\` behind the scenes
13393
13708
 
13394
13709
  ### The Cycle (main flow)
13395
13710
 
13396
13711
  \`\`\`
13397
- plan \u2192 build_list \u2192 build_execute \u2192 /papi-audit \u2192 review_list \u2192 review_submit \u2192 build_list
13712
+ plan \u2192 build_list \u2192 build_execute \u2192 audit \u2192 review_list \u2192 review_submit \u2192 build_list
13398
13713
  \`\`\`
13399
13714
 
13400
13715
  1. **plan** \u2014 Run at the start of each cycle to generate the cycle plan and populate the board.
@@ -13404,8 +13719,8 @@ plan \u2192 build_list \u2192 build_execute \u2192 /papi-audit \u2192 review_lis
13404
13719
  3. **build_execute** (start) \u2014 Creates a feature branch and marks the task In Progress. Returns the build handoff.
13405
13720
  Next: Implement the task, then \`build_execute <task_id>\` again with report fields to complete.
13406
13721
  4. **build_execute** (complete) \u2014 Submits the build report, commits, and marks the task In Review.
13407
- Next: Run \`/papi-audit\` automatically.
13408
- 5. **/papi-audit** \u2014 Audits the branch for bugs, convention violations, and doc drift.
13722
+ Next: Run the post-build audit automatically.
13723
+ 5. **Post-build audit** \u2014 Review branch changes for bugs, convention violations, and doc drift (see Post-Build Audit section below).
13409
13724
  Next: If findings exist, run \`review_submit\` with \`request-changes\` and the audit findings. If clean, proceed to \`review_list\`.
13410
13725
  6. **review_list** \u2014 Shows tasks pending human review (handoff-review or build-acceptance).
13411
13726
  Next: \`review_submit\` to approve, accept, or request changes.
@@ -13454,15 +13769,99 @@ setup \u2192 plan
13454
13769
  | \`plan\` | \`build_list\` |
13455
13770
  | \`build_list\` | \`build_execute <task_id>\` |
13456
13771
  | \`build_execute\` (start) | Implement, then \`build_execute\` (complete) |
13457
- | \`build_execute\` (complete) | \`/papi-audit\` (automatic) |
13458
- | \`/papi-audit\` (findings) | \`review_submit\` with \`request-changes\` |
13459
- | \`/papi-audit\` (clean) | \`review_list\` |
13772
+ | \`build_execute\` (complete) | Post-build audit (automatic) |
13773
+ | Audit (findings) | \`review_submit\` with \`request-changes\` |
13774
+ | Audit (clean) | \`review_list\` |
13460
13775
  | \`review_list\` | \`review_submit\` |
13461
13776
  | \`review_submit\` (approve/accept) | \`build_list\` |
13462
13777
  | \`review_submit\` (request-changes) | \`build_execute\` (redo) or \`build_list\` |
13463
13778
  | \`strategy_review\` | \`strategy_change\` (if needed) |
13464
13779
  | \`idea\` | Next \`plan\` picks it up |
13465
13780
 
13781
+ ## Post-Build Audit
13782
+
13783
+ After every \`build_execute\` (complete), audit the branch before presenting for human review. This catches bugs and convention violations early.
13784
+
13785
+ 1. **Identify changed files:** Run \`git diff origin/main --name-only\` to find modified files. If no changes, report "No changes to audit" and skip.
13786
+ 2. **Review each changed file** for:
13787
+ - Logic errors, off-by-one mistakes, incorrect conditions
13788
+ - Unhandled edge cases (null, undefined, empty inputs)
13789
+ - Convention violations defined in this CLAUDE.md
13790
+ - Incorrect type narrowing or unsafe casts
13791
+ 3. **Documentation check:** If any \`docs/\` files describe behaviour that the change modified, flag as "Doc drift".
13792
+ 4. **Report:** For each issue: file path, severity (Bug/Convention/Doc drift), what's wrong, how to fix.
13793
+ 5. **If findings exist:** Run \`review_submit\` with \`request-changes\` and the findings. Fix before human review.
13794
+ 6. **If clean:** Present for human review \u2014 "Ready for your review \u2014 approve or request changes?"
13795
+
13796
+ ## When to Start a New Conversation
13797
+
13798
+ Start a fresh window when:
13799
+ - **After a release** \u2014 cycle is done, context is heavy. New window orients in seconds via \`orient\`.
13800
+ - **After 3+ tasks built** \u2014 accumulated file reads, diffs, and discussions bloat context. Quality degrades.
13801
+ - **Switching modes** \u2014 going from building to planning, or from strategy review to building. Each mode benefits from clean context.
13802
+ - **After context compression fires** \u2014 if you notice earlier messages are missing, the window is getting stale. Open fresh.
13803
+
13804
+ Stay in the same window when:
13805
+ - Building sequential tasks in a batch (especially XS/S tasks)
13806
+ - Mid-task and not yet complete
13807
+ - Having a strategic discussion that informs the next action
13808
+
13809
+ **Rule of thumb:** If you've been in the same window for 30+ minutes or 3+ tasks, it's time for a fresh one.
13810
+
13811
+ ## Housekeeping \u2014 Fix Orphaned Tasks
13812
+
13813
+ On session start, silently check for and fix:
13814
+ 1. **Orphaned branches:** \`git branch | grep feat/task\` \u2014 cross-reference with board status. Fix by merging or flagging.
13815
+ 2. **In Review tasks with no PR:** If branch is already merged to main, the review_submit step was missed.
13816
+ 3. **Stale In Progress:** Branch has no recent commits \u2014 flag it.
13817
+ 4. **Config mismatches:** \`.mcp.json\` has DATABASE_URL but PAPI_ADAPTER is still \`md\` \u2014 flag it.
13818
+
13819
+ **Do this automatically and silently.** Report what you found and fixed.
13820
+
13821
+ ## Plumbing Is Autonomous
13822
+
13823
+ Board status updates, branch cleanup, orphaned task fixes, commit/PR/merge for housekeeping \u2014 these are mechanical plumbing. **Do them end-to-end without stopping to ask.** Report after the fact.
13824
+
13825
+ ## Context Compression Recovery
13826
+
13827
+ When the system compresses prior messages, immediately:
13828
+ 1. **Run \`orient\`** \u2014 single call for cycle state
13829
+ 2. Check your todo list for in-progress work
13830
+ 3. Run housekeeping checks
13831
+ 4. **NEVER re-build a task that is already In Review or Done.**
13832
+ 5. Continue where you left off \u2014 don't restart or re-plan
13833
+
13834
+ ## Branching & PR Convention
13835
+
13836
+ - **XS/S tasks in the same cycle and module:** Group on shared branch. One PR, one merge.
13837
+ - **M/L tasks or different modules:** Own branch per task. Isolated PRs.
13838
+ - **Commit per task within grouped branches** \u2014 traceable git history.
13839
+
13840
+ ## Quick Work vs PAPI Work
13841
+
13842
+ PAPI is for planned work. Quick fixes \u2014 just do them. No need for plan or build_execute.
13843
+
13844
+ **After completing quick/ad-hoc work** (bug fixes, config changes, small improvements done outside the cycle), call \`ad_hoc\` to record it. This creates a Done task + build report so the work appears in cycle history and metrics. Don't skip this \u2014 unrecorded work is invisible work.
13845
+
13846
+ ## Data Integrity
13847
+
13848
+ - **Use MCP tools for all project data operations.** DB is the source of truth when using the pg adapter.
13849
+ - Do NOT read \`.papi/\` files for context \u2014 use MCP tools.
13850
+ - \`.papi/\` files may be stale when using pg adapter. This is expected.
13851
+
13852
+ ## Code Before Claims \u2014 No Assumptions
13853
+
13854
+ **Before making any claim about how the codebase works, read the relevant file first.**
13855
+
13856
+ This includes:
13857
+ - How a feature is implemented ("it works like X") \u2192 read the source
13858
+ - Whether something exists ("there's no baseline migration") \u2192 check the directory
13859
+ - Whether a flow is broken or working \u2192 trace it in code
13860
+ - What a user would experience \u2192 check the actual page/component
13861
+
13862
+ Do NOT rely on memory, prior conversation, or inference. Read first, then answer.
13863
+ If the answer requires checking 2-3 files, check them all before responding.
13864
+
13466
13865
  ## Process Rules
13467
13866
 
13468
13867
  These rules come from 80+ cycles of dogfooding. They prevent the most common sources of wasted time and rework.
@@ -13475,6 +13874,11 @@ These rules come from 80+ cycles of dogfooding. They prevent the most common sou
13475
13874
  - **Test after every build.** Run the project's test suite after implementing. Suggest follow-up tasks from learnings when meaningful.
13476
13875
  - **Build patiently.** Validate each phase against the last. Don't rush through implementation \u2014 test through the UI, not just the API.
13477
13876
 
13877
+ ### Security
13878
+ - **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
+ - **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
+ - **Never ship secrets.** Do not commit .env files, API keys, or credentials. Check \`.gitignore\` covers sensitive files before pushing.
13881
+
13478
13882
  ### Planning & Scope
13479
13883
  - **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.
13480
13884
  - **Split large ideas.** If an idea has 3+ concerns, submit it as 2-3 separate ideas so the planner creates properly scoped tasks \u2014 not kitchen-sink handoffs.
@@ -13483,12 +13887,73 @@ These rules come from 80+ cycles of dogfooding. They prevent the most common sou
13483
13887
  ### Communication
13484
13888
  - **Show task names, not just IDs.** When summarising board state or reconciliation, include task names \u2014 e.g. "task-42: Add supplier form" not just "task-42".
13485
13889
  - **Surface the next command.** After each step, tell the user what comes next. Commands should be surfaced, not memorised.
13890
+
13891
+ ### Stage Readiness
13892
+ - **Access-widening stages require auth/security phases.** Before declaring a stage complete, check if it widens who can access the product (e.g. Alpha Distribution, Alpha Cohort). If so, auth hardening and security review must be completed first \u2014 not discovered after the fact.
13893
+ - **Pattern:** Audit access surface \u2192 fix vulnerabilities \u2192 then widen access. Never ship access-widening without a security phase.
13894
+ `;
13895
+ var CLAUDE_MD_ENRICHMENT_SENTINEL_T1 = "<!-- PAPI_ENRICHMENT_TIER_1 -->";
13896
+ var CLAUDE_MD_ENRICHMENT_SENTINEL_T2 = "<!-- PAPI_ENRICHMENT_TIER_2 -->";
13897
+ var CLAUDE_MD_TIER_1 = `
13898
+ ${CLAUDE_MD_ENRICHMENT_SENTINEL_T1}
13899
+
13900
+ ## Batch Building (unlocked at cycle 6)
13901
+
13902
+ For cycles with multiple XS/S tasks, batch build them without stopping between each:
13903
+ - Build all XS/S tasks first, then M/L tasks
13904
+ - Group tasks touching the same module onto a shared branch where possible
13905
+ - One commit per task for traceable history, even on shared branches
13906
+ - After all tasks built, batch review them together
13907
+
13908
+ ## Strategy Reviews
13909
+
13910
+ Every 5 cycles, PAPI offers a strategy review \u2014 a deep analysis of velocity, estimation accuracy, active decisions, and project direction.
13911
+
13912
+ - **Don't skip them.** They're where compounding value comes from.
13913
+ - Strategy reviews run in their own session \u2014 don't mix with building.
13914
+ - Reviews produce recommendations that feed into the next plan.
13915
+ - If the review recommends AD changes, use \`strategy_change\` to apply them.
13916
+
13917
+ ## Active Decision Lifecycle
13918
+
13919
+ Active Decisions (ADs) track architectural and product choices with confidence levels (LOW \u2192 MEDIUM \u2192 HIGH).
13920
+
13921
+ - Check ADs before making architectural choices \u2014 run \`health\` for the AD summary.
13922
+ - ADs are for product/architecture choices only, not process preferences.
13923
+ - When new evidence appears, update AD confidence via \`strategy_change\`.
13924
+ - Supersede rather than overwrite \u2014 old decisions stay as history.
13925
+ `;
13926
+ var CLAUDE_MD_TIER_2 = `
13927
+ ${CLAUDE_MD_ENRICHMENT_SENTINEL_T2}
13928
+
13929
+ ## Idea Pipeline (unlocked at cycle 21)
13930
+
13931
+ The \`idea\` tool is your backlog intake \u2014 not just for features, but bugs, research, and big ideas.
13932
+
13933
+ - When you discover something during a build, submit it via \`idea\` rather than stopping to fix it.
13934
+ - Include a \`Reference:\` line pointing to relevant docs so the planner has context.
13935
+ - Split large ideas into 2-3 focused submissions for better planner scoping.
13936
+ - The backlog is the steering wheel \u2014 priority + notes shape what gets planned next.
13937
+
13938
+ ## Doc Registry
13939
+
13940
+ Docs are first-class entities. When research or planning produces a stable document:
13941
+ - Register it with \`doc_register\` after it's finalised.
13942
+ - Doc summaries travel with tool context \u2014 the planner and strategy review can find relevant docs.
13943
+ - Keep docs current \u2014 update the review header after any change.
13944
+
13945
+ ## Advanced Patterns
13946
+
13947
+ - **Cross-project awareness:** If running multiple PAPI projects, learnings transfer across them via shared patterns and the doc registry.
13948
+ - **Dogfood friction:** When something feels painful in the workflow, note it \u2014 the \`idea\` tool turns friction into improvements.
13949
+ - **Deferred tasks are intentional:** Tasks moved to Deferred aren't forgotten \u2014 they're parked for the right time.
13950
+ - **Carry-forward items:** Each plan notes carry-forward from the previous cycle. Check them before planning.
13486
13951
  `;
13487
13952
  var PAPI_AUDIT_COMMAND_TEMPLATE = `Audit the latest changes in this branch for bugs and compliance with the project's conventions defined in CLAUDE.md.
13488
13953
 
13489
13954
  ## Steps
13490
13955
 
13491
- 1. **Identify changed files**: Run \`git diff develop --name-only\` to find all files modified on this branch.
13956
+ 1. **Identify changed files**: Run \`git diff main --name-only\` to find all files modified on this branch.
13492
13957
 
13493
13958
  2. **Read each changed file** and review it for:
13494
13959
 
@@ -13605,22 +14070,22 @@ async function scaffoldPapiDir(adapter2, config2, input) {
13605
14070
  await mkdir(config2.papiDir, { recursive: true });
13606
14071
  for (const [filename, template] of Object.entries(FILE_TEMPLATES)) {
13607
14072
  const content = substitute(template, vars);
13608
- await writeFile2(join2(config2.papiDir, filename), content, "utf-8");
14073
+ await writeFile2(join3(config2.papiDir, filename), content, "utf-8");
13609
14074
  }
13610
14075
  }
13611
14076
  }
13612
- const commandsDir = join2(config2.projectRoot, ".claude", "commands");
13613
- const docsDir = join2(config2.projectRoot, "docs");
14077
+ const commandsDir = join3(config2.projectRoot, ".claude", "commands");
14078
+ const docsDir = join3(config2.projectRoot, "docs");
13614
14079
  await mkdir(commandsDir, { recursive: true });
13615
14080
  await mkdir(docsDir, { recursive: true });
13616
- const claudeMdPath = join2(config2.projectRoot, "CLAUDE.md");
14081
+ const claudeMdPath = join3(config2.projectRoot, "CLAUDE.md");
13617
14082
  let claudeMdExists = false;
13618
14083
  try {
13619
14084
  await access2(claudeMdPath);
13620
14085
  claudeMdExists = true;
13621
14086
  } catch {
13622
14087
  }
13623
- const docsIndexPath = join2(docsDir, "INDEX.md");
14088
+ const docsIndexPath = join3(docsDir, "INDEX.md");
13624
14089
  let docsIndexExists = false;
13625
14090
  try {
13626
14091
  await access2(docsIndexPath);
@@ -13628,9 +14093,9 @@ async function scaffoldPapiDir(adapter2, config2, input) {
13628
14093
  } catch {
13629
14094
  }
13630
14095
  const scaffoldFiles = {
13631
- [join2(commandsDir, "papi-audit.md")]: PAPI_AUDIT_COMMAND_TEMPLATE,
13632
- [join2(commandsDir, "test.md")]: TEST_COMMAND_TEMPLATE,
13633
- [join2(docsDir, "README.md")]: substitute(DOCS_README_TEMPLATE, vars)
14096
+ [join3(commandsDir, "papi-audit.md")]: PAPI_AUDIT_COMMAND_TEMPLATE,
14097
+ [join3(commandsDir, "test.md")]: TEST_COMMAND_TEMPLATE,
14098
+ [join3(docsDir, "README.md")]: substitute(DOCS_README_TEMPLATE, vars)
13634
14099
  };
13635
14100
  if (!docsIndexExists) {
13636
14101
  scaffoldFiles[docsIndexPath] = substitute(DOCS_INDEX_TEMPLATE, vars);
@@ -13665,7 +14130,7 @@ async function scaffoldPapiDir(adapter2, config2, input) {
13665
14130
  }
13666
14131
  var PAPI_PERMISSION = "mcp__papi__*";
13667
14132
  async function ensurePapiPermission(projectRoot) {
13668
- const settingsPath = join2(projectRoot, ".claude", "settings.json");
14133
+ const settingsPath = join3(projectRoot, ".claude", "settings.json");
13669
14134
  try {
13670
14135
  let settings = {};
13671
14136
  try {
@@ -13684,14 +14149,14 @@ async function ensurePapiPermission(projectRoot) {
13684
14149
  if (!allow.includes(PAPI_PERMISSION)) {
13685
14150
  allow.push(PAPI_PERMISSION);
13686
14151
  }
13687
- await mkdir(join2(projectRoot, ".claude"), { recursive: true });
14152
+ await mkdir(join3(projectRoot, ".claude"), { recursive: true });
13688
14153
  await writeFile2(settingsPath, JSON.stringify(settings, null, 2) + "\n", "utf-8");
13689
14154
  } catch {
13690
14155
  }
13691
14156
  }
13692
14157
  async function applySetupOutputs(adapter2, config2, input, briefText, adSeedText, conventionsText) {
13693
14158
  if (config2.adapterType !== "pg") {
13694
- await writeFile2(join2(config2.papiDir, "PRODUCT_BRIEF.md"), briefText, "utf-8");
14159
+ await writeFile2(join3(config2.papiDir, "PRODUCT_BRIEF.md"), briefText, "utf-8");
13695
14160
  }
13696
14161
  await adapter2.updateProductBrief(briefText);
13697
14162
  const briefPhases = parsePhases(briefText);
@@ -13735,7 +14200,7 @@ async function applySetupOutputs(adapter2, config2, input, briefText, adSeedText
13735
14200
  }
13736
14201
  if (conventionsText?.trim()) {
13737
14202
  try {
13738
- const claudeMdPath = join2(config2.projectRoot, "CLAUDE.md");
14203
+ const claudeMdPath = join3(config2.projectRoot, "CLAUDE.md");
13739
14204
  const existing = await readFile3(claudeMdPath, "utf-8");
13740
14205
  await writeFile2(claudeMdPath, existing + "\n" + conventionsText.trim() + "\n", "utf-8");
13741
14206
  } catch {
@@ -13803,13 +14268,13 @@ async function scanCodebase(projectRoot) {
13803
14268
  }
13804
14269
  let packageJson;
13805
14270
  try {
13806
- const content = await readFile3(join2(projectRoot, "package.json"), "utf-8");
14271
+ const content = await readFile3(join3(projectRoot, "package.json"), "utf-8");
13807
14272
  packageJson = JSON.parse(content);
13808
14273
  } catch {
13809
14274
  }
13810
14275
  let readme;
13811
14276
  for (const name of ["README.md", "readme.md", "README.txt", "README"]) {
13812
- const content = await safeReadFile(join2(projectRoot, name), 5e3);
14277
+ const content = await safeReadFile(join3(projectRoot, name), 5e3);
13813
14278
  if (content) {
13814
14279
  readme = content;
13815
14280
  break;
@@ -13819,7 +14284,7 @@ async function scanCodebase(projectRoot) {
13819
14284
  let totalFiles = topLevelFiles.length;
13820
14285
  for (const dir of topLevelDirs) {
13821
14286
  try {
13822
- const entries = await readdir(join2(projectRoot, dir), { withFileTypes: true });
14287
+ const entries = await readdir(join3(projectRoot, dir), { withFileTypes: true });
13823
14288
  const files = entries.filter((e) => e.isFile());
13824
14289
  const extensions = [...new Set(files.map((f) => extname(f.name).toLowerCase()).filter(Boolean))];
13825
14290
  totalFiles += files.length;
@@ -14005,7 +14470,7 @@ async function applySetup(adapter2, config2, input, briefText, adSeedText, conve
14005
14470
  }
14006
14471
  }
14007
14472
  try {
14008
- const claudeMdPath = join2(config2.projectRoot, "CLAUDE.md");
14473
+ const claudeMdPath = join3(config2.projectRoot, "CLAUDE.md");
14009
14474
  const existing = await readFile3(claudeMdPath, "utf-8");
14010
14475
  if (!existing.includes("Dogfood Logging")) {
14011
14476
  const dogfoodSection = [
@@ -14306,8 +14771,8 @@ init_dist2();
14306
14771
 
14307
14772
  // src/services/build.ts
14308
14773
  import { randomUUID as randomUUID9 } from "crypto";
14309
- import { readdirSync, existsSync } from "fs";
14310
- import { join as join3 } from "path";
14774
+ import { readdirSync as readdirSync2, existsSync as existsSync2 } from "fs";
14775
+ import { join as join4 } from "path";
14311
14776
  function capitalizeCompleted(value) {
14312
14777
  const map = {
14313
14778
  yes: "Yes",
@@ -14316,25 +14781,11 @@ function capitalizeCompleted(value) {
14316
14781
  };
14317
14782
  return map[value] ?? "No";
14318
14783
  }
14319
- function formatDate(date) {
14320
- return date.toISOString();
14321
- }
14322
14784
  function autoCommit(config2, taskId, taskTitle) {
14323
- if (!isGitAvailable()) {
14324
- return "Auto-commit: skipped (git not found).";
14325
- }
14326
- if (!isGitRepo(config2.projectRoot)) {
14327
- return "Auto-commit: skipped (not a git repository).";
14328
- }
14329
- try {
14330
- const result = stageAllAndCommit(
14331
- config2.projectRoot,
14332
- `feat(${taskId}): ${taskTitle}`
14333
- );
14334
- return result.committed ? `Auto-committed: ${result.message}` : `Auto-commit: ${result.message}`;
14335
- } catch (err) {
14336
- return `Auto-commit failed: ${err instanceof Error ? err.message : String(err)}`;
14337
- }
14785
+ return runAutoCommit(
14786
+ config2.projectRoot,
14787
+ () => stageAllAndCommit(config2.projectRoot, `feat(${taskId}): ${taskTitle}`)
14788
+ );
14338
14789
  }
14339
14790
  function pushAndCreatePR(config2, taskId, taskTitle) {
14340
14791
  const lines = [];
@@ -14539,27 +14990,19 @@ async function completeBuild(adapter2, config2, taskId, input, options = {}) {
14539
14990
  if (!task) {
14540
14991
  throw new Error(`Task "${taskId}" not found on the Cycle Board.`);
14541
14992
  }
14542
- let cycleNumber;
14543
- try {
14544
- const health = await adapter2.getCycleHealth();
14545
- cycleNumber = health.totalCycles;
14546
- } catch {
14547
- cycleNumber = 0;
14548
- }
14993
+ const [healthResult, priorCount] = await Promise.all([
14994
+ adapter2.getCycleHealth().catch(() => ({ totalCycles: 0 })),
14995
+ adapter2.getBuildReportCountForTask(taskId).catch(() => 0)
14996
+ ]);
14997
+ const cycleNumber = healthResult.totalCycles;
14998
+ const iterationCount = priorCount + 1;
14549
14999
  const now = /* @__PURE__ */ new Date();
14550
- let iterationCount = 1;
14551
- try {
14552
- const priorReports = await adapter2.getRecentBuildReports(200);
14553
- const priorForTask = priorReports.filter((r) => r.taskId === taskId);
14554
- iterationCount = priorForTask.length + 1;
14555
- } catch {
14556
- }
14557
15000
  const report = {
14558
15001
  uuid: randomUUID9(),
14559
15002
  createdAt: now.toISOString(),
14560
15003
  taskId: task.id,
14561
15004
  taskName: task.title,
14562
- date: formatDate(now),
15005
+ date: now.toISOString(),
14563
15006
  cycle: cycleNumber,
14564
15007
  completed: capitalizeCompleted(input.completed),
14565
15008
  actualEffort: input.effort,
@@ -14644,13 +15087,13 @@ async function completeBuild(adapter2, config2, taskId, input, options = {}) {
14644
15087
  let docWarning;
14645
15088
  try {
14646
15089
  if (adapter2.searchDocs) {
14647
- const docsDir = join3(config2.projectRoot, "docs");
14648
- if (existsSync(docsDir)) {
15090
+ const docsDir = join4(config2.projectRoot, "docs");
15091
+ if (existsSync2(docsDir)) {
14649
15092
  const scanDir = (dir) => {
14650
- const entries = readdirSync(dir, { withFileTypes: true });
15093
+ const entries = readdirSync2(dir, { withFileTypes: true });
14651
15094
  const files = [];
14652
15095
  for (const e of entries) {
14653
- const full = join3(dir, e.name);
15096
+ const full = join4(dir, e.name);
14654
15097
  if (e.isDirectory()) files.push(...scanDir(full));
14655
15098
  else if (e.name.endsWith(".md")) files.push(full.replace(config2.projectRoot + "/", ""));
14656
15099
  }
@@ -14784,11 +15227,11 @@ var buildExecuteTool = {
14784
15227
  },
14785
15228
  dead_ends: {
14786
15229
  type: "string",
14787
- description: "Failed approaches tried during the build and why they failed. Helps future builders avoid repeating blind alleys. Optional."
15230
+ description: `Approaches tried and rejected during the build, with WHY they failed. Example: "Tried using Supabase realtime subscriptions but Edge Functions can't hold persistent connections \u2014 switched to polling." Include whenever you abandoned an approach. Future builds and plans reference this to avoid repeating blind alleys.`
14788
15231
  },
14789
15232
  brief_implications: {
14790
15233
  type: "array",
14791
- description: "Discovery learnings from this build that feed back into planning. Each entry targets a discovery canvas section. Optional \u2014 only include when a build reveals something about assumptions, user journeys, MVP boundary, or competitive landscape.",
15234
+ description: "Strategic learnings discovered during this build that the planner and strategy review should know about. Include when a build reveals: (1) something about assumptions that were wrong, (2) competitive/landscape insights, (3) user journey friction discovered during implementation, (4) MVP boundary implications (something that must/must-not be in v1), or (5) new success signal data. Each entry feeds into the Discovery Canvas and informs future planning.",
14792
15235
  items: {
14793
15236
  type: "object",
14794
15237
  properties: {
@@ -14824,6 +15267,30 @@ function formatListItem(task) {
14824
15267
  return `- **${task.id}:** ${task.title}
14825
15268
  Status: ${task.status} | Priority: ${task.priority} | Complexity: ${task.complexity}`;
14826
15269
  }
15270
+ function filterRelevantADs(ads, task) {
15271
+ const keywords = [];
15272
+ if (task.module) keywords.push(task.module.toLowerCase());
15273
+ if (task.epic) keywords.push(task.epic.toLowerCase());
15274
+ if (task.phase) keywords.push(task.phase.toLowerCase());
15275
+ const titleWords = task.title.toLowerCase().split(/\s+/).filter((w) => w.length > 3);
15276
+ keywords.push(...titleWords);
15277
+ if (keywords.length === 0) return [];
15278
+ return ads.filter((ad) => {
15279
+ if (ad.superseded) return false;
15280
+ const text = `${ad.title} ${ad.body}`.toLowerCase();
15281
+ return keywords.some((kw) => text.includes(kw));
15282
+ });
15283
+ }
15284
+ function formatRelevantADs(ads) {
15285
+ if (ads.length === 0) return "";
15286
+ const lines = ["\n\n---\n\n**ACTIVE DECISIONS (relevant):**"];
15287
+ for (const ad of ads) {
15288
+ const bodyLines = ad.body.split("\n").filter((l) => l.trim() && !l.startsWith("#"));
15289
+ const summary = bodyLines[0]?.trim().slice(0, 120) ?? "";
15290
+ lines.push(`- **${ad.displayId}: ${ad.title}** [${ad.confidence}] \u2014 ${summary}`);
15291
+ }
15292
+ return lines.join("\n");
15293
+ }
14827
15294
  function hasReportFields(args) {
14828
15295
  return !!(args.completed || args.effort || args.estimated_effort || args.surprises || args.discovered_issues || args.architecture_notes);
14829
15296
  }
@@ -14861,6 +15328,15 @@ async function handleBuildList(adapter2, config2) {
14861
15328
  Waiting on: ${unresolvedDeps.join(", ")}`);
14862
15329
  }
14863
15330
  }
15331
+ try {
15332
+ const comments = await adapter2.getRecentTaskComments?.(30);
15333
+ if (comments && comments.length > 0) {
15334
+ const taskIds = new Set([...result.sorted, ...result.blocked.map((b2) => b2.task)].map((t) => t.id));
15335
+ const section = formatTaskComments(comments, taskIds);
15336
+ if (section) lines.push(section);
15337
+ }
15338
+ } catch {
15339
+ }
14864
15340
  return textResponse(lines.join("\n"));
14865
15341
  }
14866
15342
  async function handleBuildDescribe(adapter2, args) {
@@ -14908,7 +15384,14 @@ async function handleBuildExecute(adapter2, config2, args) {
14908
15384
  ${verificationFiles.map((f) => `- ${f}`).join("\n")}
14909
15385
  If >80% of the scope is already implemented, call \`build_execute\` with completed="yes" and note "already built" in surprises instead of re-implementing.` : "";
14910
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.";
14911
- return textResponse(header + serializeBuildHandoff(result.task.buildHandoff) + verificationNote + chainInstruction + phaseNote);
15387
+ let adSection = "";
15388
+ try {
15389
+ const allADs = await adapter2.getActiveDecisions();
15390
+ const relevant = filterRelevantADs(allADs, result.task);
15391
+ adSection = formatRelevantADs(relevant);
15392
+ } catch {
15393
+ }
15394
+ return textResponse(header + serializeBuildHandoff(result.task.buildHandoff) + adSection + verificationNote + chainInstruction + phaseNote);
14912
15395
  } catch (err) {
14913
15396
  if (isNoHandoffError(err)) {
14914
15397
  const lines = [
@@ -15266,36 +15749,28 @@ async function captureIdea(adapter2, input) {
15266
15749
  return routeToDiscovery(adapter2, routing, input);
15267
15750
  }
15268
15751
  }
15269
- let similarWarning = "";
15270
- try {
15271
- const similar = await findSimilarTasks(adapter2, input.text);
15272
- if (similar.length > 0) {
15273
- const highOverlap = similar.filter((s) => s.coverage >= 0.7);
15274
- if (highOverlap.length > 0 && !input.force) {
15275
- const lines = highOverlap.map(
15752
+ if (!input.force) {
15753
+ try {
15754
+ const similar = await findSimilarTasks(adapter2, input.text);
15755
+ if (similar.length > 0) {
15756
+ const lines = similar.map(
15276
15757
  (s) => ` - **${s.id}** [${s.status}]: "${s.title}" (${Math.round(s.coverage * 100)}% keyword overlap)`
15277
15758
  );
15278
- const doneMatch = highOverlap.find((s) => s.status === "Done");
15279
- const reason = doneMatch ? `This looks like it was **already done** as ${doneMatch.id}.` : `This looks like a **duplicate** of ${highOverlap[0].id}.`;
15759
+ const highOverlap = similar.filter((s) => s.coverage >= 0.7);
15760
+ const doneMatch = similar.find((s) => s.status === "Done");
15761
+ const reason = doneMatch ? `This looks like it was **already done** as ${doneMatch.id}.` : highOverlap.length > 0 ? `This looks like a **duplicate** of ${highOverlap[0].id}.` : `Similar tasks already exist on the board.`;
15280
15762
  return {
15281
15763
  routing: "task",
15282
- message: `\u26D4 **Blocked \u2014 ${reason}**
15764
+ message: `\u26A0\uFE0F **Paused \u2014 ${reason}**
15283
15765
 
15284
- High-overlap tasks:
15766
+ Similar tasks found:
15285
15767
  ${lines.join("\n")}
15286
15768
 
15287
- If this is genuinely different, re-run with \`force: true\`.`
15769
+ **STOP: Ask the user whether to proceed.** Explain the overlap and let them decide. If the user confirms this is genuinely different, re-run with \`force: true\`. Do NOT proceed without user confirmation.`
15288
15770
  };
15289
15771
  }
15290
- const warnLines = similar.map(
15291
- (s) => ` - **${s.id}** [${s.status}]: "${s.title}" (${Math.round(s.coverage * 100)}% keyword overlap)`
15292
- );
15293
- similarWarning = `
15294
-
15295
- \u26A0\uFE0F **Similar tasks found** \u2014 check before scheduling:
15296
- ${warnLines.join("\n")}`;
15772
+ } catch {
15297
15773
  }
15298
- } catch {
15299
15774
  }
15300
15775
  const [health, phases] = await Promise.all([
15301
15776
  adapter2.getCycleHealth(),
@@ -15303,13 +15778,17 @@ ${warnLines.join("\n")}`;
15303
15778
  ]);
15304
15779
  warnIfEmpty("getCycleHealth (idea)", health);
15305
15780
  const phase = input.phase || resolveCurrentPhase(phases);
15781
+ const VALID_PRIORITIES2 = /* @__PURE__ */ new Set(["P0 Critical", "P1 High", "P2 Medium", "P3 Low"]);
15782
+ const VALID_COMPLEXITIES2 = /* @__PURE__ */ new Set(["XS", "Small", "Medium", "Large", "XL"]);
15783
+ const priority = input.priority && VALID_PRIORITIES2.has(input.priority) ? input.priority : "P2 Medium";
15784
+ const complexity = input.complexity && VALID_COMPLEXITIES2.has(input.complexity) ? input.complexity : "Small";
15306
15785
  const task = await adapter2.createTask({
15307
15786
  uuid: randomUUID10(),
15308
15787
  displayId: "",
15309
15788
  title: input.text,
15310
15789
  status: "Backlog",
15311
- priority: "P3 Low",
15312
- complexity: "Medium",
15790
+ priority,
15791
+ complexity,
15313
15792
  module: input.module || "Core",
15314
15793
  epic: input.epic || "Platform",
15315
15794
  phase,
@@ -15320,7 +15799,7 @@ ${warnLines.join("\n")}`;
15320
15799
  taskType: "idea",
15321
15800
  maturity: "raw"
15322
15801
  });
15323
- return { routing: "task", task, message: `${task.id}: "${task.title}" \u2014 added to backlog${similarWarning}` };
15802
+ return { routing: "task", task, message: `${task.id}: "${task.title}" \u2014 added to backlog` };
15324
15803
  }
15325
15804
  var CANVAS_SECTION_LABELS = {
15326
15805
  landscape: "Landscape References",
@@ -15360,7 +15839,7 @@ async function routeToDiscovery(adapter2, section, input) {
15360
15839
  // src/tools/idea.ts
15361
15840
  var ideaTool = {
15362
15841
  name: "idea",
15363
- description: "Capture an idea as a Backlog task. The next plan run will triage and scope it. Use anytime to log bugs, feature requests, or improvements without interrupting the current cycle. Does not call the Anthropic API.",
15842
+ description: "Capture an idea as a Backlog task. The next plan run will triage and scope it. Use anytime to log bugs, feature requests, or improvements without interrupting the current cycle. IMPORTANT: If this idea originates from a research or planning session, you MUST include a Reference: line in notes pointing to the source doc. Without it, the planner has no context and will misinterpret the intent. Does not call the Anthropic API.",
15364
15843
  inputSchema: {
15365
15844
  type: "object",
15366
15845
  properties: {
@@ -15370,7 +15849,7 @@ var ideaTool = {
15370
15849
  },
15371
15850
  notes: {
15372
15851
  type: "string",
15373
- description: 'Additional context, constraints, or reasoning. For M/L ideas that originated from research or scoping sessions, include a Reference: line (e.g. "Reference: docs/architecture/papi-brain-v1.md") so the planner can pass it through to the BUILD HANDOFF for the builder to read.'
15852
+ description: 'Additional context, constraints, or reasoning. MANDATORY: If this idea comes from a research or planning session, include a "Reference: <path>" line pointing to the source doc. Tasks submitted without references get misinterpreted by the planner \u2014 this is the #1 cause of wasted build slots (C146: task-807 was scoped as landing page copy when it was actually a dashboard UX task, because the source research doc was missing). Use doc_search to find relevant docs before submitting.'
15374
15853
  },
15375
15854
  module: {
15376
15855
  type: "string",
@@ -15384,6 +15863,16 @@ var ideaTool = {
15384
15863
  type: "string",
15385
15864
  description: 'Target phase (default: "Unscoped").'
15386
15865
  },
15866
+ priority: {
15867
+ type: "string",
15868
+ enum: ["P0 Critical", "P1 High", "P2 Medium", "P3 Low"],
15869
+ description: 'Priority level. P0 = broken/blocking. P1 = strategically aligned with current goals. P2 = valuable but not urgent. P3 = nice-to-have/speculative. Default: "P2 Medium". Assess based on strategic alignment and impact, not just effort.'
15870
+ },
15871
+ complexity: {
15872
+ type: "string",
15873
+ enum: ["XS", "Small", "Medium", "Large", "XL"],
15874
+ description: 'Estimated complexity. XS = config/one-liner. Small = one file. Medium = 2-5 files. Large = cross-module. XL = architectural. Default: "Small".'
15875
+ },
15387
15876
  discovery: {
15388
15877
  type: "boolean",
15389
15878
  description: "When true, classify the idea and route to Discovery Canvas instead of backlog. Default: false (always creates a backlog task)."
@@ -15413,6 +15902,8 @@ async function handleIdea(adapter2, config2, args) {
15413
15902
  module: args.module,
15414
15903
  epic: args.epic,
15415
15904
  phase: args.phase,
15905
+ priority: args.priority,
15906
+ complexity: args.complexity,
15416
15907
  notes: rawNotes,
15417
15908
  discovery: args.discovery === true,
15418
15909
  force: args.force === true
@@ -15435,7 +15926,24 @@ async function handleIdea(adapter2, config2, args) {
15435
15926
  if (result.routing === "task") {
15436
15927
  const branchNote = onFeatureBranch ? ` on ${currentBranch} for next cycle planning.` : " for next cycle planning.";
15437
15928
  const truncateWarning = notesTruncated ? ` (notes truncated to ${MAX_NOTES_LENGTH} chars)` : "";
15438
- return textResponse(`${result.message}${branchNote}${truncateWarning}`);
15929
+ const hasReference = rawNotes?.toLowerCase().includes("reference:") ?? false;
15930
+ let refNudge = "";
15931
+ if (!hasReference && result.task && adapter2.searchDocs) {
15932
+ try {
15933
+ const keywords = text.split(/\s+/).filter((w) => w.length > 3).slice(0, 3).join(" ");
15934
+ const relatedDocs = await adapter2.searchDocs({ keyword: keywords, limit: 3 });
15935
+ if (relatedDocs.length > 0) {
15936
+ const docList = relatedDocs.map((d) => ` - ${d.path} \u2014 ${d.title}`).join("\n");
15937
+ refNudge = `
15938
+
15939
+ \u26A0\uFE0F **No Reference: line in notes.** The planner generates better handoffs when ideas link to source docs. Potentially relevant docs:
15940
+ ${docList}
15941
+ Re-submit with \`notes: "... Reference: <path>"\` to link one, or ignore if none are relevant.`;
15942
+ }
15943
+ } catch {
15944
+ }
15945
+ }
15946
+ return textResponse(`${result.message}${branchNote}${truncateWarning}${refNudge}`);
15439
15947
  }
15440
15948
  return textResponse(result.message);
15441
15949
  }
@@ -15452,9 +15960,9 @@ function resolveCurrentPhase2(phases) {
15452
15960
  function severityToPriority(severity) {
15453
15961
  switch (severity) {
15454
15962
  case "critical":
15455
- return "P1 Critical";
15963
+ return "P0 Critical";
15456
15964
  case "major":
15457
- return "P2 Medium";
15965
+ return "P1 High";
15458
15966
  default:
15459
15967
  return "P2 Medium";
15460
15968
  }
@@ -15914,18 +16422,110 @@ async function applyReconcile(adapter2, corrections) {
15914
16422
  }
15915
16423
  return { applied, skipped, details, phaseChanges };
15916
16424
  }
16425
+ var VALID_PRIORITIES = /* @__PURE__ */ new Set(["P0 Critical", "P1 High", "P2 Medium", "P3 Low"]);
16426
+ var VALID_COMPLEXITIES = /* @__PURE__ */ new Set(["XS", "Small", "Medium", "Large", "XL"]);
16427
+ async function prepareRetriage(adapter2) {
16428
+ const health = await adapter2.getCycleHealth();
16429
+ const currentCycle = health.totalCycles;
16430
+ const allTasks = await adapter2.queryBoard({
16431
+ status: ["Backlog", "In Cycle", "Ready", "Blocked"]
16432
+ });
16433
+ if (allTasks.length === 0) {
16434
+ return "No backlog tasks to retriage.";
16435
+ }
16436
+ const lines = [];
16437
+ lines.push(`## Board Retriage \u2014 Cycle ${currentCycle}`);
16438
+ lines.push("");
16439
+ lines.push(`**${allTasks.length} tasks** to reassess priority and complexity.`);
16440
+ lines.push("");
16441
+ try {
16442
+ const ads = await adapter2.readActiveDecisions();
16443
+ if (ads.length > 0) {
16444
+ lines.push("### Active Decisions (strategic context)");
16445
+ for (const ad of ads.slice(0, 10)) {
16446
+ lines.push(`- **${ad.id}:** ${ad.title} [${ad.confidence}]`);
16447
+ }
16448
+ lines.push("");
16449
+ }
16450
+ } catch {
16451
+ }
16452
+ try {
16453
+ const phases = await adapter2.readPhases();
16454
+ const inProgress = phases.filter((p) => p.status === "In Progress");
16455
+ if (inProgress.length > 0) {
16456
+ lines.push("### Active Phases");
16457
+ for (const p of inProgress) {
16458
+ lines.push(`- ${p.label} (${p.status})`);
16459
+ }
16460
+ lines.push("");
16461
+ }
16462
+ } catch {
16463
+ }
16464
+ lines.push("### All Tasks to Retriage");
16465
+ lines.push("");
16466
+ for (const t of allTasks) {
16467
+ const age = currentCycle - (t.createdCycle ?? 0);
16468
+ const notes = t.notes ? ` \u2014 ${t.notes.slice(0, 120)}` : "";
16469
+ lines.push(`- **${t.id}:** ${t.title} [current: ${t.priority} | ${t.complexity} | ${t.module} | ${t.phase}] (${age} cycles old)${notes}`);
16470
+ }
16471
+ return lines.join("\n");
16472
+ }
16473
+ async function applyRetriage(adapter2, retriages) {
16474
+ const details = [];
16475
+ let applied = 0;
16476
+ let skipped = 0;
16477
+ let unchanged = 0;
16478
+ for (const r of retriages) {
16479
+ try {
16480
+ const task = await adapter2.getTask(r.taskId);
16481
+ if (!task) {
16482
+ details.push(`${r.taskId}: skipped \u2014 not found`);
16483
+ skipped++;
16484
+ continue;
16485
+ }
16486
+ if (!VALID_PRIORITIES.has(r.priority)) {
16487
+ details.push(`${r.taskId}: skipped \u2014 invalid priority "${r.priority}"`);
16488
+ skipped++;
16489
+ continue;
16490
+ }
16491
+ if (!VALID_COMPLEXITIES.has(r.complexity)) {
16492
+ details.push(`${r.taskId}: skipped \u2014 invalid complexity "${r.complexity}"`);
16493
+ skipped++;
16494
+ continue;
16495
+ }
16496
+ if (task.priority === r.priority && task.complexity === r.complexity) {
16497
+ details.push(`${r.taskId}: unchanged \u2014 already ${r.priority} / ${r.complexity}`);
16498
+ unchanged++;
16499
+ continue;
16500
+ }
16501
+ const changes = [];
16502
+ if (task.priority !== r.priority) changes.push(`priority ${task.priority} \u2192 ${r.priority}`);
16503
+ if (task.complexity !== r.complexity) changes.push(`complexity ${task.complexity} \u2192 ${r.complexity}`);
16504
+ await adapter2.updateTask(r.taskId, {
16505
+ priority: r.priority,
16506
+ complexity: r.complexity
16507
+ });
16508
+ details.push(`${r.taskId}: ${changes.join(", ")} \u2014 ${r.reason}`);
16509
+ applied++;
16510
+ } catch (err) {
16511
+ details.push(`${r.taskId}: error \u2014 ${err instanceof Error ? err.message : String(err)}`);
16512
+ skipped++;
16513
+ }
16514
+ }
16515
+ return { applied, skipped, unchanged, details };
16516
+ }
15917
16517
 
15918
16518
  // src/tools/board-reconcile.ts
15919
16519
  var boardReconcileTool = {
15920
16520
  name: "board_reconcile",
15921
- description: "Holistic backlog review to group, merge, cancel, or defer stale tasks. Prepare phase returns backlog context for analysis. Apply phase accepts corrections. Does not call the Anthropic API.",
16521
+ description: 'Holistic backlog review to group, merge, cancel, defer, or retriage tasks. "prepare"/"apply" for cleanup. "retriage-prepare"/"retriage-apply" to reassess priority and complexity on existing backlog tasks. Does not call the Anthropic API.',
15922
16522
  inputSchema: {
15923
16523
  type: "object",
15924
16524
  properties: {
15925
16525
  mode: {
15926
16526
  type: "string",
15927
- enum: ["prepare", "apply"],
15928
- description: '"prepare" returns backlog context for analysis. "apply" accepts corrections. Defaults to "prepare".'
16527
+ enum: ["prepare", "apply", "retriage-prepare", "retriage-apply"],
16528
+ description: '"prepare"/"apply" for cleanup. "retriage-prepare"/"retriage-apply" to reassess priority and complexity on backlog tasks. Defaults to "prepare".'
15929
16529
  },
15930
16530
  llm_response: {
15931
16531
  type: "string",
@@ -15976,6 +16576,47 @@ When done, call \`board_reconcile\` again with:
15976
16576
  - \`mode\`: "apply"
15977
16577
  - \`llm_response\`: your complete output (both parts)
15978
16578
  `;
16579
+ var RETRIAGE_PROMPT = `You are the PAPI Board Retriager. Reassess the priority and complexity of every backlog task below using these criteria:
16580
+
16581
+ ## Priority Levels
16582
+ - **P0 Critical** \u2014 Broken, blocking, or data-loss risk. Fix now.
16583
+ - **P1 High** \u2014 Strategically aligned: directly advances the current horizon/phase goals or Active Decisions.
16584
+ - **P2 Medium** \u2014 Valuable but not strategically urgent: quality improvements, efficiency, polish, infrastructure.
16585
+ - **P3 Low** \u2014 Nice-to-have, speculative, or future-horizon work.
16586
+
16587
+ ## Complexity Levels
16588
+ - **XS** \u2014 Config change, one-liner, toggle.
16589
+ - **Small** \u2014 One file, < 50 lines changed.
16590
+ - **Medium** \u2014 2-5 files, moderate scope.
16591
+ - **Large** \u2014 Cross-module, multiple components.
16592
+ - **XL** \u2014 Architectural, multi-day effort.
16593
+
16594
+ ## Rules
16595
+ - Assess priority based on **strategic alignment** (does it advance current goals?), **unlocks other work** (are tasks blocked by this?), **user-facing impact**, and **compounding value** (does it make future work faster?).
16596
+ - Assess complexity based on the **actual scope of the change**, not conservatively. Use the full range.
16597
+ - If a task's current priority and complexity are already correct, still include it with the same values \u2014 this confirms the assessment.
16598
+
16599
+ Your output must have TWO parts:
16600
+
16601
+ ### Part 1: Analysis
16602
+ Brief markdown analysis of how priorities should shift and why.
16603
+
16604
+ ### Part 2: Structured Output
16605
+ After \`<!-- PAPI_RETRIAGE_OUTPUT -->\`, a JSON block:
16606
+
16607
+ \`\`\`json
16608
+ {
16609
+ "retriages": [
16610
+ {"taskId": "task-123", "priority": "P1 High", "complexity": "Medium", "reason": "Directly advances Phase 2 goals"},
16611
+ {"taskId": "task-124", "priority": "P3 Low", "complexity": "Small", "reason": "Nice-to-have, no strategic urgency"}
16612
+ ]
16613
+ }
16614
+ \`\`\`
16615
+
16616
+ When done, call \`board_reconcile\` again with:
16617
+ - \`mode\`: "retriage-apply"
16618
+ - \`llm_response\`: your complete output (both parts)
16619
+ `;
15979
16620
  async function handleBoardReconcile(adapter2, config2, args) {
15980
16621
  const mode = args.mode ?? "prepare";
15981
16622
  if (mode === "prepare") {
@@ -16041,7 +16682,70 @@ Analyze the backlog above and produce your reconciliation output. Then call \`bo
16041
16682
  }
16042
16683
  return textResponse(lines.join("\n"));
16043
16684
  }
16044
- return errorResponse(`Unknown mode: ${mode}. Use "prepare" or "apply".`);
16685
+ if (mode === "retriage-prepare") {
16686
+ const context = await prepareRetriage(adapter2);
16687
+ if (context === "No backlog tasks to retriage.") {
16688
+ return textResponse(context);
16689
+ }
16690
+ return textResponse(
16691
+ `${RETRIAGE_PROMPT}
16692
+ ---
16693
+
16694
+ ### Backlog Context
16695
+
16696
+ ${context}
16697
+ ---
16698
+
16699
+ Assess each task above and produce your retriage output. Then call \`board_reconcile\` with mode "retriage-apply".`
16700
+ );
16701
+ }
16702
+ if (mode === "retriage-apply") {
16703
+ const llmResponse = args.llm_response;
16704
+ if (!llmResponse?.trim()) {
16705
+ return errorResponse("llm_response is required for retriage-apply mode.");
16706
+ }
16707
+ const marker = "<!-- PAPI_RETRIAGE_OUTPUT -->";
16708
+ const markerIdx = llmResponse.indexOf(marker);
16709
+ if (markerIdx === -1) {
16710
+ return errorResponse("Missing <!-- PAPI_RETRIAGE_OUTPUT --> marker in response.");
16711
+ }
16712
+ const jsonPart = llmResponse.slice(markerIdx + marker.length);
16713
+ const jsonMatch = jsonPart.match(/```json\s*([\s\S]*?)\s*```/);
16714
+ if (!jsonMatch) {
16715
+ return errorResponse("No JSON block found after <!-- PAPI_RETRIAGE_OUTPUT --> marker.");
16716
+ }
16717
+ let retriages;
16718
+ try {
16719
+ const parsed = JSON.parse(jsonMatch[1]);
16720
+ retriages = parsed.retriages;
16721
+ if (!Array.isArray(retriages)) {
16722
+ return errorResponse("retriages must be an array.");
16723
+ }
16724
+ } catch (err) {
16725
+ return errorResponse(`Invalid JSON: ${err instanceof Error ? err.message : String(err)}`);
16726
+ }
16727
+ const result = await applyRetriage(adapter2, retriages);
16728
+ if (isGitAvailable() && isGitRepo(config2.projectRoot)) {
16729
+ try {
16730
+ stageDirAndCommit(
16731
+ config2.projectRoot,
16732
+ config2.papiDir,
16733
+ `chore: board retriage \u2014 ${result.applied} tasks updated`
16734
+ );
16735
+ } catch {
16736
+ }
16737
+ }
16738
+ const lines = [];
16739
+ lines.push(`## Board Retriage Complete`);
16740
+ lines.push("");
16741
+ lines.push(`**${result.applied} tasks updated**, ${result.skipped} skipped, ${result.unchanged} unchanged.`);
16742
+ lines.push("");
16743
+ for (const d of result.details) {
16744
+ lines.push(`- ${d}`);
16745
+ }
16746
+ return textResponse(lines.join("\n"));
16747
+ }
16748
+ return errorResponse(`Unknown mode: ${mode}. Use "prepare", "apply", "retriage-prepare", or "retriage-apply".`);
16045
16749
  }
16046
16750
 
16047
16751
  // src/services/health.ts
@@ -16357,7 +17061,7 @@ async function handleHealth(adapter2) {
16357
17061
 
16358
17062
  // src/services/release.ts
16359
17063
  import { writeFile as writeFile3 } from "fs/promises";
16360
- import { join as join4 } from "path";
17064
+ import { join as join5 } from "path";
16361
17065
  var INITIAL_RELEASE_NOTES = `# Changelog
16362
17066
 
16363
17067
  ## v0.1.0-alpha \u2014 Initial Release
@@ -16448,7 +17152,7 @@ async function createRelease(config2, branch, version, adapter2) {
16448
17152
  const commits = getCommitsSinceTag(config2.projectRoot, latestTag);
16449
17153
  changelogContent = generateChangelog(version, commits);
16450
17154
  }
16451
- const changelogPath = join4(config2.projectRoot, "CHANGELOG.md");
17155
+ const changelogPath = join5(config2.projectRoot, "CHANGELOG.md");
16452
17156
  await writeFile3(changelogPath, changelogContent, "utf-8");
16453
17157
  const commitResult = stageAllAndCommit(config2.projectRoot, `release: ${version}`);
16454
17158
  const commitNote = commitResult.committed ? `Committed CHANGELOG.md.` : `CHANGELOG.md: ${commitResult.message}`;
@@ -16522,6 +17226,20 @@ async function handleRelease(adapter2, config2, args) {
16522
17226
  if (result.warnings?.length) {
16523
17227
  lines.push("", "\u26A0\uFE0F Warnings: " + result.warnings.join("; "));
16524
17228
  }
17229
+ try {
17230
+ const cycleMatch = version.match(/^v0\.(\d+)\./);
17231
+ const cycleNum = cycleMatch ? parseInt(cycleMatch[1], 10) : 0;
17232
+ if (cycleNum > 0) {
17233
+ const reports = await adapter2.getBuildReportsSince(cycleNum);
17234
+ const EMPTY = /* @__PURE__ */ new Set(["None", "none", "N/A", "", "null"]);
17235
+ const issues = reports.filter((r) => r.discoveredIssues && !EMPTY.has(r.discoveredIssues.trim())).map((r) => `- **${r.taskId}** (${r.taskName}): ${r.discoveredIssues}`);
17236
+ if (issues.length > 0) {
17237
+ lines.push("", "---", "", `## Discovered Issues (${issues.length})`, "", ...issues);
17238
+ lines.push("", "*These issues were logged during builds \u2014 triage them in the next plan.*");
17239
+ }
17240
+ }
17241
+ } catch {
17242
+ }
16525
17243
  return textResponse(lines.join("\n"));
16526
17244
  } catch (err) {
16527
17245
  return errorResponse(err instanceof Error ? err.message : String(err));
@@ -16529,8 +17247,8 @@ async function handleRelease(adapter2, config2, args) {
16529
17247
  }
16530
17248
 
16531
17249
  // src/tools/review.ts
16532
- import { existsSync as existsSync2 } from "fs";
16533
- import { join as join5 } from "path";
17250
+ import { existsSync as existsSync3 } from "fs";
17251
+ import { join as join6 } from "path";
16534
17252
 
16535
17253
  // src/services/review.ts
16536
17254
  init_dist2();
@@ -16768,8 +17486,8 @@ function mergeAfterAccept(config2, taskId) {
16768
17486
  }
16769
17487
  const featureBranch = taskBranchName(taskId);
16770
17488
  const baseBranch = resolveBaseBranch(config2.projectRoot, config2.baseBranch);
16771
- const papiDir = join5(config2.projectRoot, ".papi");
16772
- if (existsSync2(papiDir)) {
17489
+ const papiDir = join6(config2.projectRoot, ".papi");
17490
+ if (existsSync3(papiDir)) {
16773
17491
  try {
16774
17492
  const commitResult = stageDirAndCommit(
16775
17493
  config2.projectRoot,
@@ -17059,8 +17777,8 @@ Path: ${mcpJsonPath}`
17059
17777
 
17060
17778
  // src/tools/orient.ts
17061
17779
  import { execFileSync as execFileSync3 } from "child_process";
17062
- import { readFileSync } from "fs";
17063
- import { join as join6 } from "path";
17780
+ import { readFileSync, writeFileSync, existsSync as existsSync4 } from "fs";
17781
+ import { join as join7 } from "path";
17064
17782
  var orientTool = {
17065
17783
  name: "orient",
17066
17784
  description: "Session orientation \u2014 single call that replaces build_list + health. Returns: cycle number, task counts by status, in-progress/in-review tasks, strategy review cadence, velocity snapshot, and recommended next action. Read-only, does not modify any files.",
@@ -17207,7 +17925,7 @@ async function getHierarchyPosition(adapter2) {
17207
17925
  }
17208
17926
  function checkNpmVersionDrift() {
17209
17927
  try {
17210
- const pkgPath = join6(new URL(".", import.meta.url).pathname, "..", "..", "package.json");
17928
+ const pkgPath = join7(new URL(".", import.meta.url).pathname, "..", "..", "package.json");
17211
17929
  const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
17212
17930
  const localVersion = pkg.version;
17213
17931
  const packageName = pkg.name;
@@ -17226,6 +17944,10 @@ function checkNpmVersionDrift() {
17226
17944
  }
17227
17945
  async function handleOrient(adapter2, config2) {
17228
17946
  try {
17947
+ try {
17948
+ await propagatePhaseStatus(adapter2);
17949
+ } catch {
17950
+ }
17229
17951
  const [buildResult, healthResult, hierarchy] = await Promise.all([
17230
17952
  listBuilds(adapter2, config2),
17231
17953
  getHealthSummary(adapter2),
@@ -17293,23 +18015,48 @@ ${versionDrift}` : "";
17293
18015
  }
17294
18016
  } catch {
17295
18017
  }
17296
- return textResponse(formatOrientSummary(healthResult, buildInfo, hierarchy) + ttfvNote + recsNote + pendingReviewNote + versionNote);
18018
+ let enrichmentNote = "";
18019
+ try {
18020
+ enrichmentNote = enrichClaudeMd(config2.projectRoot, healthResult.cycleNumber);
18021
+ } catch {
18022
+ }
18023
+ return textResponse(formatOrientSummary(healthResult, buildInfo, hierarchy) + ttfvNote + recsNote + pendingReviewNote + versionNote + enrichmentNote);
17297
18024
  } catch (err) {
17298
18025
  const message = err instanceof Error ? err.message : String(err);
17299
18026
  return errorResponse(`Orient failed: ${message}`);
17300
18027
  }
17301
18028
  }
18029
+ function enrichClaudeMd(projectRoot, cycleNumber) {
18030
+ const claudeMdPath = join7(projectRoot, "CLAUDE.md");
18031
+ if (!existsSync4(claudeMdPath)) return "";
18032
+ const content = readFileSync(claudeMdPath, "utf-8");
18033
+ const additions = [];
18034
+ if (cycleNumber >= 6 && !content.includes(CLAUDE_MD_ENRICHMENT_SENTINEL_T1)) {
18035
+ additions.push(CLAUDE_MD_TIER_1);
18036
+ }
18037
+ if (cycleNumber >= 21 && !content.includes(CLAUDE_MD_ENRICHMENT_SENTINEL_T2)) {
18038
+ additions.push(CLAUDE_MD_TIER_2);
18039
+ }
18040
+ if (additions.length === 0) return "";
18041
+ writeFileSync(claudeMdPath, content + additions.join(""), "utf-8");
18042
+ const tierNames = [];
18043
+ if (additions.some((a) => a.includes(CLAUDE_MD_ENRICHMENT_SENTINEL_T1))) tierNames.push("Established (batch building, strategy reviews, AD lifecycle)");
18044
+ if (additions.some((a) => a.includes(CLAUDE_MD_ENRICHMENT_SENTINEL_T2))) tierNames.push("Mature (idea pipeline, doc registry, advanced patterns)");
18045
+ return `
18046
+
18047
+ \u{1F4DD} **CLAUDE.md enriched** \u2014 added ${tierNames.join(" + ")} guidance for cycle ${cycleNumber}+ projects.`;
18048
+ }
17302
18049
 
17303
18050
  // src/tools/hierarchy.ts
17304
18051
  var hierarchyUpdateTool = {
17305
18052
  name: "hierarchy_update",
17306
- description: "Update the status of a stage or horizon in the project hierarchy (AD-14). Accepts a level (stage or horizon), a name or ID, and a new status. Does not call the Anthropic API.",
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.",
17307
18054
  inputSchema: {
17308
18055
  type: "object",
17309
18056
  properties: {
17310
18057
  level: {
17311
18058
  type: "string",
17312
- enum: ["stage", "horizon"],
18059
+ enum: ["phase", "stage", "horizon"],
17313
18060
  description: "Which hierarchy level to update."
17314
18061
  },
17315
18062
  name: {
@@ -17333,13 +18080,32 @@ async function handleHierarchyUpdate(adapter2, args) {
17333
18080
  if (!level || !name || !status) {
17334
18081
  return errorResponse("Missing required parameters: level, name, status.");
17335
18082
  }
17336
- if (level !== "stage" && level !== "horizon") {
17337
- return errorResponse(`Invalid level "${level}". Must be "stage" or "horizon".`);
18083
+ if (level !== "phase" && level !== "stage" && level !== "horizon") {
18084
+ return errorResponse(`Invalid level "${level}". Must be "phase", "stage", or "horizon".`);
17338
18085
  }
17339
18086
  if (!VALID_STATUSES3.has(status)) {
17340
18087
  return errorResponse(`Invalid status "${status}". Must be one of: active, completed, deferred.`);
17341
18088
  }
17342
18089
  try {
18090
+ if (level === "phase") {
18091
+ if (!adapter2.readPhases || !adapter2.updatePhaseStatus) {
18092
+ return errorResponse("Phase management is not supported by the current adapter.");
18093
+ }
18094
+ const phases = await adapter2.readPhases();
18095
+ const phase = phases.find(
18096
+ (p) => p.label.toLowerCase() === name.toLowerCase() || p.id === name || p.slug === name
18097
+ );
18098
+ if (!phase) {
18099
+ const available = phases.map((p) => p.label).join(", ");
18100
+ return errorResponse(`Phase "${name}" not found. Available phases: ${available || "none"}`);
18101
+ }
18102
+ if (phase.status === status) {
18103
+ return textResponse(`Phase "${phase.label}" is already "${status}". No change made.`);
18104
+ }
18105
+ const oldStatus2 = phase.status;
18106
+ await adapter2.updatePhaseStatus(phase.id, status);
18107
+ return textResponse(`Phase updated: **${phase.label}** ${oldStatus2} \u2192 ${status}`);
18108
+ }
17343
18109
  if (level === "stage") {
17344
18110
  if (!adapter2.readStages || !adapter2.updateStageStatus) {
17345
18111
  return errorResponse("Stage management is not supported by the current adapter.");
@@ -17721,6 +18487,9 @@ ${result.userMessage}
17721
18487
  }
17722
18488
 
17723
18489
  // src/tools/doc-registry.ts
18490
+ import { readdirSync as readdirSync3, existsSync as existsSync5, readFileSync as readFileSync2 } from "fs";
18491
+ import { join as join8, relative } from "path";
18492
+ import { homedir as homedir2 } from "os";
17724
18493
  var docRegisterTool = {
17725
18494
  name: "doc_register",
17726
18495
  description: "Register a document in the doc registry. Called after finalising a research/planning doc, or when build_execute detects unregistered docs. Stores metadata and structured summary \u2014 not full content.",
@@ -17769,6 +18538,20 @@ var docSearchTool = {
17769
18538
  required: []
17770
18539
  }
17771
18540
  };
18541
+ var docScanTool = {
18542
+ name: "doc_scan",
18543
+ description: "Scan docs/ and plans directories for unregistered .md files. Returns a list of files not yet in the doc registry. Use this to find docs that need registration.",
18544
+ inputSchema: {
18545
+ type: "object",
18546
+ properties: {
18547
+ include_plans: {
18548
+ type: "boolean",
18549
+ description: "Also scan ~/.claude/plans/ for plan files (default: false)."
18550
+ }
18551
+ },
18552
+ required: []
18553
+ }
18554
+ };
17772
18555
  async function handleDocRegister(adapter2, args) {
17773
18556
  if (!adapter2.registerDoc) {
17774
18557
  return errorResponse("Doc registry not available \u2014 requires pg adapter.");
@@ -17845,6 +18628,75 @@ ${d.summary}
17845
18628
 
17846
18629
  ${lines.join("\n---\n\n")}`);
17847
18630
  }
18631
+ function scanMdFiles(dir, rootDir) {
18632
+ if (!existsSync5(dir)) return [];
18633
+ const files = [];
18634
+ try {
18635
+ const entries = readdirSync3(dir, { withFileTypes: true });
18636
+ for (const entry of entries) {
18637
+ const full = join8(dir, entry.name);
18638
+ if (entry.isDirectory()) {
18639
+ files.push(...scanMdFiles(full, rootDir));
18640
+ } else if (entry.name.endsWith(".md")) {
18641
+ files.push(relative(rootDir, full));
18642
+ }
18643
+ }
18644
+ } catch {
18645
+ }
18646
+ return files;
18647
+ }
18648
+ function extractTitle(filePath) {
18649
+ try {
18650
+ const content = readFileSync2(filePath, "utf-8").slice(0, 1e3);
18651
+ const fmMatch = content.match(/^---[\s\S]*?title:\s*(.+?)$/m);
18652
+ if (fmMatch) return fmMatch[1].trim().replace(/^["']|["']$/g, "");
18653
+ const headingMatch = content.match(/^#+\s+(.+)$/m);
18654
+ if (headingMatch) return headingMatch[1].trim();
18655
+ } catch {
18656
+ }
18657
+ return void 0;
18658
+ }
18659
+ async function handleDocScan(adapter2, config2, args) {
18660
+ if (!adapter2.searchDocs) {
18661
+ return errorResponse("Doc registry not available \u2014 requires pg adapter.");
18662
+ }
18663
+ const includePlans = args.include_plans ?? false;
18664
+ const registered = await adapter2.searchDocs({ limit: 500 });
18665
+ const registeredPaths = new Set(registered.map((d) => d.path));
18666
+ const docsDir = join8(config2.projectRoot, "docs");
18667
+ const docsFiles = scanMdFiles(docsDir, config2.projectRoot);
18668
+ const unregisteredDocs = docsFiles.filter((f) => !registeredPaths.has(f));
18669
+ let unregisteredPlans = [];
18670
+ if (includePlans) {
18671
+ const plansDir = join8(homedir2(), ".claude", "plans");
18672
+ if (existsSync5(plansDir)) {
18673
+ const planFiles = scanMdFiles(plansDir, plansDir);
18674
+ unregisteredPlans = planFiles.map((f) => `plans/${f}`).filter((f) => !registeredPaths.has(f)).map((f) => ({
18675
+ path: f,
18676
+ title: extractTitle(join8(plansDir, f.replace("plans/", "")))
18677
+ }));
18678
+ }
18679
+ }
18680
+ const lines = [];
18681
+ if (unregisteredDocs.length === 0 && unregisteredPlans.length === 0) {
18682
+ return textResponse("All docs are registered. No unregistered files found.");
18683
+ }
18684
+ if (unregisteredDocs.length > 0) {
18685
+ lines.push(`## Unregistered Docs (${unregisteredDocs.length})`);
18686
+ for (const f of unregisteredDocs) {
18687
+ const title = extractTitle(join8(config2.projectRoot, f));
18688
+ lines.push(`- \`${f}\`${title ? ` \u2014 ${title}` : ""}`);
18689
+ }
18690
+ }
18691
+ if (unregisteredPlans.length > 0) {
18692
+ lines.push("", `## Unregistered Plans (${unregisteredPlans.length})`);
18693
+ for (const p of unregisteredPlans) {
18694
+ lines.push(`- \`${p.path}\`${p.title ? ` \u2014 ${p.title}` : ""}`);
18695
+ }
18696
+ }
18697
+ lines.push("", `Use \`doc_register\` to register these files.`);
18698
+ return textResponse(lines.join("\n"));
18699
+ }
17848
18700
 
17849
18701
  // src/lib/telemetry.ts
17850
18702
  var TELEMETRY_SUPABASE_URL = "https://guewgygcpcmrcoppihzx.supabase.co";
@@ -17898,6 +18750,7 @@ var TOOLS_REQUIRING_PAPI = /* @__PURE__ */ new Set([
17898
18750
  "board_view",
17899
18751
  "board_deprioritise",
17900
18752
  "board_archive",
18753
+ "board_edit",
17901
18754
  "build_list",
17902
18755
  "build_describe",
17903
18756
  "build_execute",
@@ -17926,6 +18779,7 @@ function createServer(adapter2, config2) {
17926
18779
  boardViewTool,
17927
18780
  boardDeprioritiseTool,
17928
18781
  boardArchiveTool,
18782
+ boardEditTool,
17929
18783
  setupTool,
17930
18784
  buildListTool,
17931
18785
  buildDescribeTool,
@@ -17944,7 +18798,8 @@ function createServer(adapter2, config2) {
17944
18798
  hierarchyUpdateTool,
17945
18799
  zoomOutTool,
17946
18800
  docRegisterTool,
17947
- docSearchTool
18801
+ docSearchTool,
18802
+ docScanTool
17948
18803
  ]
17949
18804
  }));
17950
18805
  server2.setRequestHandler(CallToolRequestSchema, async (request) => {
@@ -17994,6 +18849,9 @@ function createServer(adapter2, config2) {
17994
18849
  case "board_archive":
17995
18850
  result = await handleBoardArchive(adapter2, safeArgs);
17996
18851
  break;
18852
+ case "board_edit":
18853
+ result = await handleBoardEdit(adapter2, safeArgs);
18854
+ break;
17997
18855
  case "setup":
17998
18856
  result = await handleSetup(adapter2, config2, safeArgs);
17999
18857
  break;
@@ -18051,6 +18909,9 @@ function createServer(adapter2, config2) {
18051
18909
  case "doc_search":
18052
18910
  result = await handleDocSearch(adapter2, safeArgs);
18053
18911
  break;
18912
+ case "doc_scan":
18913
+ result = await handleDocScan(adapter2, config2, safeArgs);
18914
+ break;
18054
18915
  default:
18055
18916
  return { content: [{ type: "text", text: `Unknown tool: ${name}` }] };
18056
18917
  }