@papi-ai/server 0.7.6 → 0.7.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/dist/index.js +1356 -744
  2. package/dist/prompts.js +46 -7
  3. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -215,6 +215,30 @@ ${after}`;
215
215
  function parseNorthStar(content) {
216
216
  return extractSection(content, "North Star").replace(/^## North Star\s*/m, "").trim();
217
217
  }
218
+ function upsertNorthStarInContent(content, statement) {
219
+ const headingPattern = /^## North Star\s*$/m;
220
+ const start = content.search(headingPattern);
221
+ if (start === -1) {
222
+ const cycleLogIdx = content.search(/^## (?:Cycle Log|Sprint Log)/m);
223
+ const newSection = `## North Star
224
+
225
+ ${statement}
226
+
227
+ `;
228
+ if (cycleLogIdx === -1) {
229
+ return content.trimEnd() + "\n\n" + newSection;
230
+ }
231
+ return content.slice(0, cycleLogIdx) + newSection + content.slice(cycleLogIdx);
232
+ }
233
+ const afterHeading = content.slice(start);
234
+ const nextSection = afterHeading.slice(1).search(/^## /m);
235
+ const sectionEnd = nextSection === -1 ? content.length : start + nextSection + 1;
236
+ return content.slice(0, start) + `## North Star
237
+
238
+ ${statement}
239
+
240
+ ` + content.slice(sectionEnd);
241
+ }
218
242
  function parseDeferred(content) {
219
243
  const section = extractSection(content, "Deferred / Parking Lot");
220
244
  return section.split("\n").filter((line) => line.match(/^-\s+/)).map((line) => line.replace(/^-\s+/, "").trim());
@@ -1428,7 +1452,8 @@ var init_dist2 = __esm({
1428
1452
  task: 1,
1429
1453
  research: 2,
1430
1454
  spike: 2,
1431
- idea: 3
1455
+ idea: 3,
1456
+ discovery: 1
1432
1457
  };
1433
1458
  VALID_EFFORT_SIZES = /* @__PURE__ */ new Set(["XS", "S", "M", "L", "XL"]);
1434
1459
  SECTION_HEADERS = [
@@ -2174,6 +2199,23 @@ ${footer}`);
2174
2199
  async getDecisionUsage(_currentCycle) {
2175
2200
  return [];
2176
2201
  }
2202
+ // --- North Star ---
2203
+ async getCurrentNorthStar() {
2204
+ const content = await this.read("PLANNING_LOG.md");
2205
+ const ns = parseNorthStar(content);
2206
+ return ns || null;
2207
+ }
2208
+ async getNorthStarSetCycle() {
2209
+ return null;
2210
+ }
2211
+ async getNorthStarStaleness() {
2212
+ return null;
2213
+ }
2214
+ async upsertNorthStar(statement, _cycleNumber) {
2215
+ const content = await this.read("PLANNING_LOG.md");
2216
+ const updated = upsertNorthStarInContent(content, statement);
2217
+ await this.write("PLANNING_LOG.md", updated);
2218
+ }
2177
2219
  };
2178
2220
  NONE_PATTERN2 = /^none\b/i;
2179
2221
  }
@@ -4468,6 +4510,7 @@ function rowToTask(row) {
4468
4510
  if (row.stage_id != null) task.stageId = row.stage_id;
4469
4511
  if (row.doc_ref != null) task.docRef = row.doc_ref;
4470
4512
  if (row.source != null) task.source = row.source;
4513
+ if (row.opportunity != null) task.opportunity = row.opportunity;
4471
4514
  return task;
4472
4515
  }
4473
4516
  function rowToBuildReport(row) {
@@ -4494,6 +4537,7 @@ function rowToBuildReport(row) {
4494
4537
  if (row.handoff_accuracy != null) report.handoffAccuracy = row.handoff_accuracy;
4495
4538
  if (row.brief_implications != null) report.briefImplications = row.brief_implications;
4496
4539
  if (row.dead_ends != null) report.deadEnds = row.dead_ends;
4540
+ if (row.tool_call_count != null) report.toolCallCount = row.tool_call_count;
4497
4541
  return report;
4498
4542
  }
4499
4543
  function rowToReview(row) {
@@ -6094,6 +6138,22 @@ EXCEPTION WHEN duplicate_object THEN NULL; END $$;
6094
6138
  `;
6095
6139
  return rows.length > 0 ? { setCycle: rows[0].set_cycle, setAt: rows[0].set_at } : null;
6096
6140
  }
6141
+ async upsertNorthStar(statement, _cycleNumber) {
6142
+ const [newRow] = await this.sql`
6143
+ INSERT INTO north_stars (project_id, statement, set_at)
6144
+ VALUES (${this.projectId}, ${statement}, now())
6145
+ RETURNING id
6146
+ `;
6147
+ if (newRow) {
6148
+ await this.sql`
6149
+ UPDATE north_stars
6150
+ SET superseded_by_id = ${newRow.id}, superseded_at = now()
6151
+ WHERE project_id = ${this.projectId}
6152
+ AND superseded_by_id IS NULL
6153
+ AND id != ${newRow.id}
6154
+ `;
6155
+ }
6156
+ }
6097
6157
  async getEstimationCalibration() {
6098
6158
  const rows = await this.sql`
6099
6159
  SELECT estimated_effort, actual_effort, accuracy_label, COUNT(*)::text AS count
@@ -6666,7 +6726,7 @@ ${newParts.join("\n")}` : newParts.join("\n");
6666
6726
  async queryBoard(options) {
6667
6727
  if (!options) {
6668
6728
  const rows2 = await this.sql`
6669
- SELECT id, project_id, display_id, title, status, priority, complexity, module, epic, phase, owner, reviewed, cycle, created_cycle, created_at, why, depends_on, notes, closure_reason, state_history, build_handoff, build_report, task_type, maturity, stage_id, doc_ref, source, updated_at
6729
+ SELECT id, project_id, display_id, title, status, priority, complexity, module, epic, phase, owner, reviewed, cycle, created_cycle, created_at, why, depends_on, notes, closure_reason, state_history, build_handoff, build_report, task_type, maturity, stage_id, doc_ref, source, opportunity, updated_at
6670
6730
  FROM cycle_tasks
6671
6731
  WHERE project_id = ${this.projectId}
6672
6732
  ORDER BY display_id
@@ -6707,7 +6767,7 @@ ${newParts.join("\n")}` : newParts.join("\n");
6707
6767
  where = this.sql`${where} AND ${conditions[i]}`;
6708
6768
  }
6709
6769
  const rows = await this.sql`
6710
- SELECT id, project_id, display_id, title, status, priority, complexity, module, epic, phase, owner, reviewed, cycle, created_cycle, created_at, why, depends_on, notes, closure_reason, state_history, build_handoff, build_report, task_type, maturity, stage_id, doc_ref, source, updated_at
6770
+ SELECT id, project_id, display_id, title, status, priority, complexity, module, epic, phase, owner, reviewed, cycle, created_cycle, created_at, why, depends_on, notes, closure_reason, state_history, build_handoff, build_report, task_type, maturity, stage_id, doc_ref, source, opportunity, updated_at
6711
6771
  FROM cycle_tasks WHERE ${where} ORDER BY display_id
6712
6772
  LIMIT 2000 -- matches no-options path ceiling
6713
6773
  `;
@@ -6715,7 +6775,7 @@ ${newParts.join("\n")}` : newParts.join("\n");
6715
6775
  }
6716
6776
  async getTask(id) {
6717
6777
  const [row] = await this.sql`
6718
- SELECT id, project_id, display_id, title, status, priority, complexity, module, epic, phase, owner, reviewed, cycle, created_cycle, created_at, why, depends_on, notes, closure_reason, state_history, build_handoff, build_report, task_type, maturity, stage_id, doc_ref, source, updated_at
6778
+ SELECT id, project_id, display_id, title, status, priority, complexity, module, epic, phase, owner, reviewed, cycle, created_cycle, created_at, why, depends_on, notes, closure_reason, state_history, build_handoff, build_report, task_type, maturity, stage_id, doc_ref, source, opportunity, updated_at
6719
6779
  FROM cycle_tasks
6720
6780
  WHERE project_id = ${this.projectId} AND display_id = ${id}
6721
6781
  LIMIT 1
@@ -6725,7 +6785,7 @@ ${newParts.join("\n")}` : newParts.join("\n");
6725
6785
  async getTasks(ids) {
6726
6786
  if (ids.length === 0) return [];
6727
6787
  const rows = await this.sql`
6728
- SELECT id, project_id, display_id, title, status, priority, complexity, module, epic, phase, owner, reviewed, cycle, created_cycle, created_at, why, depends_on, notes, closure_reason, state_history, build_handoff, build_report, task_type, maturity, stage_id, doc_ref, source, updated_at
6788
+ SELECT id, project_id, display_id, title, status, priority, complexity, module, epic, phase, owner, reviewed, cycle, created_cycle, created_at, why, depends_on, notes, closure_reason, state_history, build_handoff, build_report, task_type, maturity, stage_id, doc_ref, source, opportunity, updated_at
6729
6789
  FROM cycle_tasks
6730
6790
  WHERE project_id = ${this.projectId} AND display_id = ANY(${ids})
6731
6791
  LIMIT 2000 -- matches board ceiling; ids[] won't exceed this in practice
@@ -6745,7 +6805,7 @@ ${newParts.join("\n")}` : newParts.join("\n");
6745
6805
  project_id, display_id, title, status, priority, complexity,
6746
6806
  module, epic, phase, owner, reviewed, cycle, created_cycle,
6747
6807
  why, depends_on, notes, closure_reason, state_history,
6748
- build_handoff, build_report, task_type, maturity, stage_id, doc_ref, source
6808
+ build_handoff, build_report, task_type, maturity, stage_id, doc_ref, source, opportunity
6749
6809
  ) VALUES (
6750
6810
  ${this.projectId}, ${displayId}, ${task.title}, ${task.status}, ${task.priority},
6751
6811
  ${normaliseComplexity(task.complexity)}, ${task.module}, ${task.epic ?? null}, ${task.phase}, ${task.owner},
@@ -6759,7 +6819,8 @@ ${newParts.join("\n")}` : newParts.join("\n");
6759
6819
  ${task.maturity ?? null},
6760
6820
  ${task.stageId ?? null},
6761
6821
  ${task.docRef ?? null},
6762
- ${task.source ?? null}
6822
+ ${task.source ?? null},
6823
+ ${task.opportunity ?? null}
6763
6824
  )
6764
6825
  RETURNING *
6765
6826
  `;
@@ -6791,6 +6852,7 @@ ${newParts.join("\n")}` : newParts.join("\n");
6791
6852
  if (updates.stageId !== void 0) columnMap["stage_id"] = updates.stageId;
6792
6853
  if (updates.docRef !== void 0) columnMap["doc_ref"] = updates.docRef;
6793
6854
  if (updates.source !== void 0) columnMap["source"] = updates.source;
6855
+ if (updates.opportunity !== void 0) columnMap["opportunity"] = updates.opportunity;
6794
6856
  const keys = Object.keys(columnMap);
6795
6857
  if (keys.length === 0) return;
6796
6858
  await this.sql`
@@ -7190,6 +7252,16 @@ ${newParts.join("\n")}` : newParts.join("\n");
7190
7252
  `;
7191
7253
  return rows.map(rowToToolCallMetric);
7192
7254
  }
7255
+ async getToolCallCount(startedAt, completedAt) {
7256
+ const rows = await this.sql`
7257
+ SELECT COUNT(*)::text AS count
7258
+ FROM tool_call_metrics
7259
+ WHERE project_id = ${this.projectId}
7260
+ AND timestamp >= ${startedAt}
7261
+ AND timestamp <= ${completedAt}
7262
+ `;
7263
+ return parseInt(rows[0]?.count ?? "0", 10);
7264
+ }
7193
7265
  // -------------------------------------------------------------------------
7194
7266
  // Cost Summary
7195
7267
  // -------------------------------------------------------------------------
@@ -7929,7 +8001,8 @@ ${newParts.join("\n")}` : newParts.join("\n");
7929
8001
  build_report: task.buildReport ?? null,
7930
8002
  task_type: task.taskType ?? null,
7931
8003
  maturity: task.maturity ?? null,
7932
- stage_id: task.stageId ?? null
8004
+ stage_id: task.stageId ?? null,
8005
+ opportunity: task.opportunity ?? null
7933
8006
  };
7934
8007
  });
7935
8008
  const taskCols = [
@@ -7955,7 +8028,8 @@ ${newParts.join("\n")}` : newParts.join("\n");
7955
8028
  "build_report",
7956
8029
  "task_type",
7957
8030
  "maturity",
7958
- "stage_id"
8031
+ "stage_id",
8032
+ "opportunity"
7959
8033
  ];
7960
8034
  const insertedRows = await tx`
7961
8035
  INSERT INTO cycle_tasks ${tx(taskRows, ...taskCols)}
@@ -8567,6 +8641,9 @@ Check PAPI_PROJECT_ID in your .mcp.json config. Find your project ID in the PAPI
8567
8641
  getNorthStarStaleness() {
8568
8642
  return this.invoke("getNorthStarStaleness");
8569
8643
  }
8644
+ upsertNorthStar(statement, cycleNumber) {
8645
+ return this.invoke("upsertNorthStar", [statement, cycleNumber]);
8646
+ }
8570
8647
  // --- Optional pg-only methods ---
8571
8648
  getEstimationCalibration() {
8572
8649
  return this.invoke("getEstimationCalibration");
@@ -8649,6 +8726,7 @@ __export(git_exports, {
8649
8726
  createAndCheckoutBranch: () => createAndCheckoutBranch,
8650
8727
  createPullRequest: () => createPullRequest,
8651
8728
  createTag: () => createTag,
8729
+ cycleBranchName: () => cycleBranchName,
8652
8730
  deleteLocalBranch: () => deleteLocalBranch,
8653
8731
  detectBoardMismatches: () => detectBoardMismatches,
8654
8732
  detectUnrecordedCommits: () => detectUnrecordedCommits,
@@ -9104,6 +9182,9 @@ function detectUnrecordedCommits(cwd, baseBranch) {
9104
9182
  function taskBranchName(taskId) {
9105
9183
  return `feat/${taskId}`;
9106
9184
  }
9185
+ function cycleBranchName(cycleNumber, module) {
9186
+ return `feat/cycle-${cycleNumber}-${module.toLowerCase().replace(/\s+/g, "-")}`;
9187
+ }
9107
9188
  function getHeadCommitSha(cwd) {
9108
9189
  try {
9109
9190
  return execFileSync("git", ["rev-parse", "HEAD"], { cwd, encoding: "utf-8" }).trim() || null;
@@ -9538,7 +9619,7 @@ function formatDetailedTask(t) {
9538
9619
  return `- **${t.id}:** ${t.title}
9539
9620
  Status: ${t.status} | Priority: ${t.priority} | Complexity: ${t.complexity}${typeTag}
9540
9621
  Module: ${t.module} | Epic: ${t.epic} | Phase: ${t.phase} | Owner: ${t.owner}
9541
- Reviewed: ${t.reviewed}${t.dependsOn ? ` | Depends on: ${t.dependsOn}` : ""}${hasHandoff ? " | Has BUILD HANDOFF: yes" : ""}${t.docRef ? ` | Doc ref: ${t.docRef}` : ""}${notes ? `
9622
+ Reviewed: ${t.reviewed}${t.dependsOn ? ` | Depends on: ${t.dependsOn}` : ""}${hasHandoff ? " | Has BUILD HANDOFF: yes" : ""}${t.docRef ? ` | Doc ref: ${t.docRef}` : ""}${t.opportunity ? ` | Opportunity: ${t.opportunity}` : ""}${notes ? `
9542
9623
  Notes: ${notes}` : ""}`;
9543
9624
  }
9544
9625
  function formatBoardForPlan(tasks, filters, currentCycle) {
@@ -9945,6 +10026,191 @@ function logDataSourceSummary(service, sources) {
9945
10026
  console.error(`[data-sources] ${service}: ${populated.length}/${sources.length} sources have data \u2014 empty: ${emptyLabels}`);
9946
10027
  }
9947
10028
 
10029
+ // src/lib/codebase-scan.ts
10030
+ import { execSync as execSync2 } from "child_process";
10031
+ var STOP_WORDS = /* @__PURE__ */ new Set([
10032
+ "a",
10033
+ "an",
10034
+ "the",
10035
+ "and",
10036
+ "or",
10037
+ "but",
10038
+ "in",
10039
+ "on",
10040
+ "at",
10041
+ "to",
10042
+ "for",
10043
+ "of",
10044
+ "with",
10045
+ "by",
10046
+ "from",
10047
+ "is",
10048
+ "are",
10049
+ "was",
10050
+ "were",
10051
+ "be",
10052
+ "been",
10053
+ "has",
10054
+ "have",
10055
+ "had",
10056
+ "do",
10057
+ "does",
10058
+ "did",
10059
+ "will",
10060
+ "would",
10061
+ "could",
10062
+ "should",
10063
+ "may",
10064
+ "might",
10065
+ "can",
10066
+ "not",
10067
+ "no",
10068
+ "if",
10069
+ "then",
10070
+ "than",
10071
+ "that",
10072
+ "this",
10073
+ "it",
10074
+ "its",
10075
+ "all",
10076
+ "each",
10077
+ "every",
10078
+ "both",
10079
+ "as",
10080
+ "so",
10081
+ "up",
10082
+ "out",
10083
+ "about",
10084
+ "into",
10085
+ "over",
10086
+ "after",
10087
+ "before",
10088
+ "between",
10089
+ "under",
10090
+ "above",
10091
+ "such",
10092
+ "only",
10093
+ "also",
10094
+ "just",
10095
+ "more",
10096
+ "most",
10097
+ "other",
10098
+ "some",
10099
+ "any",
10100
+ "new",
10101
+ "when",
10102
+ "how",
10103
+ "what",
10104
+ "which",
10105
+ "who",
10106
+ "add",
10107
+ "create",
10108
+ "build",
10109
+ "implement",
10110
+ "make",
10111
+ "update",
10112
+ "fix",
10113
+ "use",
10114
+ "via",
10115
+ "show",
10116
+ "display",
10117
+ "view",
10118
+ "page",
10119
+ "data",
10120
+ "based",
10121
+ "using",
10122
+ "task",
10123
+ "feature",
10124
+ "system",
10125
+ "tool",
10126
+ "mode",
10127
+ "field",
10128
+ "type",
10129
+ "status",
10130
+ "current",
10131
+ "default",
10132
+ "existing",
10133
+ "need",
10134
+ "instead",
10135
+ "allow",
10136
+ "change"
10137
+ ]);
10138
+ function extractSearchTerms(title, notes) {
10139
+ const combined = `${title} ${notes ?? ""}`;
10140
+ const camelCase = combined.match(/[a-z][a-zA-Z]{5,}/g) ?? [];
10141
+ const snakeCase = combined.match(/[a-z]+_[a-z_]+/g) ?? [];
10142
+ const hyphenated = combined.match(/[a-z]+-[a-z]+-?[a-z]*/g) ?? [];
10143
+ const filePaths = combined.match(/[\w/.-]+\.(ts|tsx|js|jsx|sql|md)/g) ?? [];
10144
+ const words = combined.toLowerCase().replace(/[^a-z0-9\s_-]/g, " ").split(/\s+/).filter((w) => w.length >= 4 && !STOP_WORDS.has(w));
10145
+ const seen = /* @__PURE__ */ new Set();
10146
+ const terms = [];
10147
+ for (const group of [filePaths, camelCase, snakeCase, hyphenated, words]) {
10148
+ for (const term of group) {
10149
+ const normalized = term.toLowerCase();
10150
+ if (!seen.has(normalized) && normalized.length >= 4) {
10151
+ seen.add(normalized);
10152
+ terms.push(term);
10153
+ }
10154
+ }
10155
+ }
10156
+ return terms.slice(0, 8);
10157
+ }
10158
+ function grepForTerm(projectRoot, term) {
10159
+ try {
10160
+ const result = execSync2(
10161
+ `grep -rl --include='*.ts' --include='*.tsx' --include='*.js' --include='*.sql' --exclude-dir=node_modules --exclude-dir=dist --exclude-dir=.git --exclude-dir=.next ${JSON.stringify(term)} ${JSON.stringify(projectRoot)} 2>/dev/null | head -5`,
10162
+ { encoding: "utf-8", timeout: 3e3 }
10163
+ );
10164
+ return result.trim().split("\n").filter(Boolean).map(
10165
+ (p) => p.replace(projectRoot + "/", "")
10166
+ );
10167
+ } catch {
10168
+ return [];
10169
+ }
10170
+ }
10171
+ function scanCodebaseForTasks(projectRoot, tasks) {
10172
+ if (tasks.length === 0) return "";
10173
+ const startTime = Date.now();
10174
+ const results = [];
10175
+ for (const task of tasks) {
10176
+ const terms = extractSearchTerms(task.title, task.notes);
10177
+ if (terms.length === 0) continue;
10178
+ const matches = [];
10179
+ for (const term of terms) {
10180
+ if (term.length < 4) continue;
10181
+ const files = grepForTerm(projectRoot, term);
10182
+ if (files.length > 0) {
10183
+ matches.push({ term, files });
10184
+ }
10185
+ if (Date.now() - startTime > 5e3) {
10186
+ console.error(`[codebase-scan] timeout after ${Date.now() - startTime}ms \u2014 partial results returned`);
10187
+ break;
10188
+ }
10189
+ }
10190
+ if (matches.length > 0) {
10191
+ results.push({ taskId: task.id, terms, matches });
10192
+ }
10193
+ if (Date.now() - startTime > 5e3) break;
10194
+ }
10195
+ if (results.length === 0) return "";
10196
+ const elapsed = Date.now() - startTime;
10197
+ console.error(`[codebase-scan] scanned ${tasks.length} tasks in ${elapsed}ms \u2014 ${results.length} with matches`);
10198
+ const lines = [
10199
+ `Codebase scan found existing implementations for ${results.length}/${tasks.length} candidate tasks (${elapsed}ms):`,
10200
+ ""
10201
+ ];
10202
+ for (const result of results) {
10203
+ lines.push(`**${result.taskId}:**`);
10204
+ for (const match of result.matches.slice(0, 3)) {
10205
+ const fileList = match.files.slice(0, 3).join(", ");
10206
+ const moreCount = match.files.length > 3 ? ` (+${match.files.length - 3} more)` : "";
10207
+ lines.push(` - "${match.term}" \u2192 ${fileList}${moreCount}`);
10208
+ }
10209
+ lines.push("");
10210
+ }
10211
+ return lines.join("\n").trim();
10212
+ }
10213
+
9948
10214
  // src/lib/slack.ts
9949
10215
  async function sendSlackWebhook(webhookUrl, summary, header = "PAPI Strategy Review") {
9950
10216
  if (!webhookUrl) return void 0;
@@ -10004,6 +10270,9 @@ Task: [title]
10004
10270
  Cycle: [N]
10005
10271
  Why now: [justification]
10006
10272
 
10273
+ DEPENDS ON
10274
+ [Optional \u2014 comma-separated task IDs this task depends on (e.g. "task-123, task-124"). Include only when another task in this same cycle must be built first because this task consumes artifacts it creates (e.g. new adapter method, new type, new migration). The builder will reuse the upstream task's branch so dependent commits stack on the same branch for a single PR. Omit this section entirely if there are no intra-cycle dependencies.]
10275
+
10007
10276
  SCOPE (DO THIS)
10008
10277
  [specific deliverables \u2014 write for the simplest viable path first]
10009
10278
 
@@ -10192,8 +10461,11 @@ Standard planning cycle with full board review.
10192
10461
  - **P3 Low** \u2014 Nice-to-have, speculative, or future-horizon work.
10193
10462
  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.
10194
10463
  **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.
10195
- **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.
10464
+ **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. The historical average effort from Methodology Trends is a reference point for calibration, not a target or floor. A healthy cycle has 6-10 tasks. Cycles with fewer than 5 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 6+ tasks \u2014 undersized cycles waste planning overhead relative to the available work. If fewer than 5 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. Prefer grouping tasks by module or similarity \u2014 reduces context switching and enables shared branches during the build phase.
10465
+ **Theme-driven sizing:** Single-theme cycles (all tasks in the same module or epic) can absorb 25-30 effort points because builders maintain context across tasks. Mixed-theme cycles should stay at 15-20 effort points to limit context switching. Use the theme to determine the budget, not a fixed number.
10196
10466
  **Theme coherence:** After selecting candidate tasks, check whether they form a coherent theme \u2014 all serving one goal, phase, or module. Single-theme cycles produce better build quality and less context switching. If the top candidates touch 3+ unrelated modules or epics, prefer regrouping around the highest-priority theme and deferring the outliers. Mixed-theme cycles are acceptable when justified (e.g. a P0 fix alongside P1 feature work), but the justification must appear in the cycle log. Name the theme in 3-5 words \u2014 it becomes the \`cycleLogTitle\`.
10467
+ **Epic-aware batching:** Epic is the primary grouping signal for theme coherence. When multiple candidate tasks share the same epic (e.g. "Onboarding Redesign", "Dashboard Polish"), prefer co-scheduling them \u2014 they solve connected problems and benefit from shared context during the build. Steps: (1) After filtering by priority, group eligible tasks by epic. (2) If an epic has 3+ eligible tasks, prefer scheduling 2-4 of them together over cherry-picking across epics. (3) Report the epic distribution in the cycle log (e.g. "4 tasks from Onboarding epic, 1 from Platform"). Priority still overrides: a P0 fix from a different epic always takes precedence.
10468
+ **Opportunity clustering:** If backlog tasks have an \`opportunity\` field populated, group them by opportunity before selecting. Tasks sharing the same opportunity solve the same user problem \u2014 co-scheduling them produces more coherent cycles. Report opportunity clusters in the cycle log when present (e.g. "3 tasks clustered under 'planner accuracy' opportunity").
10197
10469
 
10198
10470
  8. **Cycle Log** \u2014 Write 5-10 line entry: what was triaged, what was recommended and why, observations, AD updates. Include a **Priority Recalibration** paragraph if any unreviewed task priorities were changed during triage (Step 2) \u2014 list each by ID with old \u2192 new priority and rationale. Include a **Priority Drift Suggestions** paragraph if reviewed task drift was detected (Step 3).
10199
10471
  **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.
@@ -10212,11 +10484,17 @@ Standard planning cycle with full board review.
10212
10484
  **Scope pre-check:** Before writing the SCOPE section of each handoff, cross-reference the task against the "Recently Shipped Capabilities" section in the context below (if present). For each candidate task: (1) check if the task's title or scope overlaps with any recently shipped task, (2) check if the FILES LIKELY TOUCHED overlap with files already modified in recent builds, (3) check the architecture notes from recent builds for patterns that already cover this task's scope. If >80% of a task's scope appears in recently shipped capabilities, recommend cancellation via \`boardCorrections\` or reduce the handoff scope to only the missing pieces \u2014 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.
10213
10485
  **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.
10214
10486
  **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.
10487
+ **Intra-cycle dependency detection:** After selecting cycle tasks, check every pair for build-order dependencies. Two tasks A and B have an intra-cycle dependency when A must be built before B because B consumes an artifact A creates \u2014 e.g. A adds a new adapter method that B calls, A creates a DB migration B depends on, A introduces a new shared type B imports, A refactors a utility B modifies. Signals: same module + adjacent scope (one is "add X", another is "use X"), or notes explicitly reference the other task. For each dependency detected:
10488
+ - Populate the DEPENDS ON section in the dependent task's BUILD HANDOFF with the upstream task ID(s).
10489
+ - Add a \`boardCorrections\` entry for the dependent task with \`updates.dependsOn\` set to the comma-separated upstream IDs \u2014 this persists the dependency so the builder's runtime can reuse the upstream branch.
10490
+ - Keep the SCOPE sections independent (each task still has its own deliverable) but note the ordering in "Why now" \u2014 e.g. "depends on task-123 completing the adapter method".
10491
+ Do NOT invent dependencies where tasks merely share a module \u2014 only real build-order coupling counts. Linear chains only \u2014 do not attempt to resolve multi-level graphs. When in doubt, omit the dependency and let the builder discover it.
10215
10492
  **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".
10216
10493
  **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).
10217
10494
  **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.
10218
10495
  **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.
10219
10496
  **Pre-mortem:** For projects with 10+ cycles, include a PRE-MORTEM section in every BUILD HANDOFF with 1-3 bullet points: (a) most likely technical blocker based on module history, (b) integration risk with adjacent systems, (c) scope creep signal \u2014 what the builder might be tempted to expand beyond scope. Draw from \`dead_ends\` and \`surprises\` in recent build reports for the same module. Omit this section entirely for projects with fewer than 10 cycles.
10497
+ **Build order in cycle log:** If any intra-cycle dependencies were detected in this cycle, include a "Build Order" paragraph in \`cycleLogNotes\` showing the recommended build sequence as arrow chains (e.g. "Build order: task-123 \u2192 task-124; task-130 standalone"). Skip this paragraph when no dependencies exist.
10220
10498
  **Research task detection:** When a task's title starts with "Research:" or the task type is "research", add a RESEARCH OUTPUT section to the BUILD HANDOFF after ACCEPTANCE CRITERIA:
10221
10499
 
10222
10500
  RESEARCH OUTPUT
@@ -10234,7 +10512,8 @@ Standard planning cycle with full board review.
10234
10512
  **Idea task detection:** When a task's task type is "idea", add a scope clarification note to the BUILD HANDOFF:
10235
10513
  - Add to SCOPE (DO THIS): "This task originated as an idea. Confirm the exact deliverable before implementing \u2014 check task notes and any referenced docs for intent. If scope is unclear, flag it in the build report surprises."
10236
10514
 
10237
- **UI/visual task detection:** When a task's title or notes contain keywords suggesting frontend visual work (e.g. "visual", "design", "UI", "styling", "refresh", "frontend", "landing page", "hero", "carousel", "theme", "layout", "cockpit", "dashboard", "page"), apply these handoff additions:
10515
+ **UI/visual task detection:** Apply these additions ONLY to tasks whose PRIMARY scope is frontend visual work \u2014 the task's main deliverable must be a UI change, new component, visual design, or page. Do NOT apply to backend tasks, DB migrations, or prompt/config changes that merely mention a dashboard or page in passing. Signal: the task would fail if no .tsx/.css files were changed. If uncertain, skip the UI additions.
10516
+ When a task IS a UI task (primary scope is visual/frontend):
10238
10517
  - Add to SCOPE: "Read \`.impeccable.md\` for brand palette, design principles, and audience context before writing any code. Use the \`frontend-design\` skill for implementation."
10239
10518
  - For M/L UI tasks, add to SCOPE: "Use the full UI toolchain: Playground (design preview) \u2192 Frontend-design (build) \u2192 Playwright (verify). The playground is the quality bar. Expect 2-3 iterations."
10240
10519
  - Add to ACCEPTANCE CRITERIA: "[ ] Visually verify rendered output in browser \u2014 provide localhost URL or screenshot to user for review." and "[ ] No raw IDs, abbreviations, or jargon visible without human-readable labels or tooltips."
@@ -10289,6 +10568,11 @@ var PLAN_FRAGMENT_BUG = `
10289
10568
  var PLAN_FRAGMENT_IDEA = `
10290
10569
  **Idea task detection:** When a task's task type is "idea", add a scope clarification note to the BUILD HANDOFF:
10291
10570
  - Add to SCOPE (DO THIS): "This task originated as an idea. Confirm the exact deliverable before implementing \u2014 check task notes and any referenced docs for intent. If scope is unclear, flag it in the build report surprises."`;
10571
+ var PLAN_FRAGMENT_TASK = `
10572
+ **Task type detection:** When a task's task type is "task" (generic implementation task), add these handoff sections:
10573
+ - SCOPE (DO THIS) must include: a clear deliverable statement and what "done" looks like (e.g. "User can X", "Function returns Y", "Page renders Z").
10574
+ - Add to ACCEPTANCE CRITERIA: "[ ] Scope matches handoff \u2014 no unrelated code changed" and "[ ] Out-of-scope items documented if discovered during implementation."
10575
+ - Add a SCOPE BOUNDARY (DO NOT DO THIS) section with at least one explicit exclusion \u2014 state what this task is NOT responsible for.`;
10292
10576
  var PLAN_FRAGMENT_SPIKE = `
10293
10577
  **Spike task detection:** When a task's task type is "spike" or the title starts with "Spike:", apply these rules:
10294
10578
  - Spikes are time-boxed investigations, not implementation tasks. The deliverable is a FINDING, not code.
@@ -10300,7 +10584,8 @@ var PLAN_FRAGMENT_SPIKE = `
10300
10584
  - Keep SCOPE BOUNDARY, SECURITY CONSIDERATIONS, and PRE-BUILD VERIFICATION as normal.
10301
10585
  - Spikes should be estimated conservatively: XS or S. If a spike needs M+ effort, it's not a spike \u2014 reclassify as a research task.`;
10302
10586
  var PLAN_FRAGMENT_UI = `
10303
- **UI/visual task detection:** When a task's title or notes contain keywords suggesting frontend visual work (e.g. "visual", "design", "UI", "styling", "refresh", "frontend", "landing page", "hero", "carousel", "theme", "layout", "cockpit", "dashboard", "page"), apply these handoff additions:
10587
+ **UI/visual task detection:** Apply these additions ONLY to tasks whose PRIMARY scope is frontend visual work \u2014 the task's main deliverable must be a UI change, new component, visual design, or page. Do NOT apply to backend tasks, DB migrations, or prompt/config changes that merely mention a dashboard or page in passing. Signal: the task would fail if no .tsx/.css files were changed. If uncertain, skip the UI additions.
10588
+ When a task IS a UI task (primary scope is visual/frontend):
10304
10589
  - Add to SCOPE: "Read \`.impeccable.md\` for brand palette, design principles, and audience context before writing any code. Use the \`frontend-design\` skill for implementation."
10305
10590
  - For M/L UI tasks, add to SCOPE: "Use the full UI toolchain: Playground (design preview) \u2192 Frontend-design (build) \u2192 Playwright (verify). The playground is the quality bar. Expect 2-3 iterations."
10306
10591
  - Add to ACCEPTANCE CRITERIA: "[ ] Visually verify rendered output in browser \u2014 provide localhost URL or screenshot to user for review." and "[ ] No raw IDs, abbreviations, or jargon visible without human-readable labels or tooltips."
@@ -10379,8 +10664,11 @@ Standard planning cycle with full board review.
10379
10664
  - **P3 Low** \u2014 Nice-to-have, speculative, or future-horizon work.
10380
10665
  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.
10381
10666
  **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.
10382
- **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.
10667
+ **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. The historical average effort from Methodology Trends is a reference point for calibration, not a target or floor. A healthy cycle has 6-10 tasks. Cycles with fewer than 5 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 6+ tasks \u2014 undersized cycles waste planning overhead relative to the available work. If fewer than 5 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. Prefer grouping tasks by module or similarity \u2014 reduces context switching and enables shared branches during the build phase.
10668
+ **Theme-driven sizing:** Single-theme cycles (all tasks in the same module or epic) can absorb 25-30 effort points because builders maintain context across tasks. Mixed-theme cycles should stay at 15-20 effort points to limit context switching. Use the theme to determine the budget, not a fixed number.
10383
10669
  **Theme coherence:** After selecting candidate tasks, check whether they form a coherent theme \u2014 all serving one goal, phase, or module. Single-theme cycles produce better build quality and less context switching. If the top candidates touch 3+ unrelated modules or epics, prefer regrouping around the highest-priority theme and deferring the outliers. Mixed-theme cycles are acceptable when justified (e.g. a P0 fix alongside P1 feature work), but the justification must appear in the cycle log. Name the theme in 3-5 words \u2014 it becomes the \`cycleLogTitle\`.
10670
+ **Epic-aware batching:** Epic is the primary grouping signal for theme coherence. When multiple candidate tasks share the same epic (e.g. "Onboarding Redesign", "Dashboard Polish"), prefer co-scheduling them \u2014 they solve connected problems and benefit from shared context during the build. Steps: (1) After filtering by priority, group eligible tasks by epic. (2) If an epic has 3+ eligible tasks, prefer scheduling 2-4 of them together over cherry-picking across epics. (3) Report the epic distribution in the cycle log (e.g. "4 tasks from Onboarding epic, 1 from Platform"). Priority still overrides: a P0 fix from a different epic always takes precedence.
10671
+ **Opportunity clustering:** If backlog tasks have an \`opportunity\` field populated, group them by opportunity before selecting. Tasks sharing the same opportunity solve the same user problem \u2014 co-scheduling them produces more coherent cycles. Report opportunity clusters in the cycle log when present (e.g. "3 tasks clustered under 'planner accuracy' opportunity").
10384
10672
 
10385
10673
  8. **Cycle Log** \u2014 Write 5-10 line entry: what was triaged, what was recommended and why, observations, AD updates. Include a **Priority Recalibration** paragraph if any unreviewed task priorities were changed during triage (Step 2) \u2014 list each by ID with old \u2192 new priority and rationale. Include a **Priority Drift Suggestions** paragraph if reviewed task drift was detected (Step 3).
10386
10674
  **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.
@@ -10403,11 +10691,14 @@ Standard planning cycle with full board review.
10403
10691
  **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).
10404
10692
  **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.
10405
10693
  **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.
10406
- **Pre-mortem:** For projects with 10+ cycles, include a PRE-MORTEM section in every BUILD HANDOFF with 1-3 bullet points: (a) most likely technical blocker based on module history, (b) integration risk with adjacent systems, (c) scope creep signal \u2014 what the builder might be tempted to expand beyond scope. Draw from \`dead_ends\` and \`surprises\` in recent build reports for the same module. Omit this section entirely for projects with fewer than 10 cycles.`);
10694
+ **Pre-mortem:** For projects with 10+ cycles, include a PRE-MORTEM section in every BUILD HANDOFF with 1-3 bullet points: (a) most likely technical blocker based on module history, (b) integration risk with adjacent systems, (c) scope creep signal \u2014 what the builder might be tempted to expand beyond scope. Draw from \`dead_ends\` and \`surprises\` in recent build reports for the same module. Omit this section entirely for projects with fewer than 10 cycles.
10695
+ **Intra-cycle dependency detection:** After selecting cycle tasks, check every pair for build-order dependencies. Two tasks A and B have an intra-cycle dependency when A must be built before B because B consumes an artifact A creates \u2014 e.g. A adds a new adapter method that B calls, A creates a DB migration B depends on, A introduces a new shared type B imports, A refactors a utility B modifies. Signals: same module + adjacent scope (one is "add X", another is "use X"), or notes explicitly reference the other task. For each dependency detected: (a) populate the DEPENDS ON section in the dependent task's BUILD HANDOFF with the upstream task ID(s); (b) add a \`boardCorrections\` entry for the dependent task with \`updates.dependsOn\` set to the comma-separated upstream IDs \u2014 this persists the dependency so the builder's runtime can reuse the upstream branch; (c) keep SCOPE sections independent but note the ordering in "Why now". Do NOT invent dependencies where tasks merely share a module \u2014 only real build-order coupling counts. Linear chains only \u2014 no multi-level graph resolution. When in doubt, omit.
10696
+ **Build order in cycle log:** If intra-cycle dependencies were detected, include a "Build order:" line in \`cycleLogNotes\` showing the recommended sequence as arrow chains (e.g. "Build order: task-123 \u2192 task-124; task-130 standalone"). Skip when no dependencies exist.`);
10407
10697
  if (flags.hasResearchTasks) parts.push(PLAN_FRAGMENT_RESEARCH);
10408
10698
  if (flags.hasBugTasks) parts.push(PLAN_FRAGMENT_BUG);
10409
10699
  if (flags.hasIdeaTasks) parts.push(PLAN_FRAGMENT_IDEA);
10410
10700
  if (flags.hasSpikeTasks) parts.push(PLAN_FRAGMENT_SPIKE);
10701
+ if (flags.hasTaskTasks) parts.push(PLAN_FRAGMENT_TASK);
10411
10702
  if (flags.hasUITasks) parts.push(PLAN_FRAGMENT_UI);
10412
10703
  parts.push(`
10413
10704
  11. **New Tasks (max 3 per cycle)** \u2014 Actively mine the Recent Build Reports for task candidates. For each report, check:
@@ -10472,6 +10763,9 @@ function buildPlanUserMessage(ctx) {
10472
10763
  );
10473
10764
  }
10474
10765
  parts.push("", "---", "", "## PROJECT CONTEXT", "");
10766
+ if (ctx.contextTier) {
10767
+ parts.push(`**Context tier:** ${ctx.contextTier}`, "");
10768
+ }
10475
10769
  parts.push("### Product Brief", "", ctx.productBrief, "");
10476
10770
  if (ctx.northStar) {
10477
10771
  parts.push("### North Star (current)", "", ctx.northStar, "");
@@ -10489,12 +10783,18 @@ function buildPlanUserMessage(ctx) {
10489
10783
  if (ctx.cycleLog) {
10490
10784
  parts.push("### Cycle Log", "", ctx.cycleLog, "");
10491
10785
  }
10786
+ if (ctx.strategyReviewCadence) {
10787
+ parts.push("### Strategy Review Cadence (computed from DB)", "", ctx.strategyReviewCadence, "");
10788
+ }
10492
10789
  if (ctx.board) {
10493
10790
  parts.push("### Board", "", ctx.board, "");
10494
10791
  }
10495
10792
  if (ctx.preAssignedTasks) {
10496
10793
  parts.push("### Pre-Assigned Tasks", "", ctx.preAssignedTasks, "");
10497
10794
  }
10795
+ if (ctx.codebaseScan) {
10796
+ parts.push("### Codebase Scan (existing implementations)", "", ctx.codebaseScan, "");
10797
+ }
10498
10798
  if (ctx.buildPatterns) {
10499
10799
  parts.push("### Build Patterns", "", ctx.buildPatterns, "");
10500
10800
  }
@@ -10845,7 +11145,8 @@ After your natural language output, include this EXACT format on its own line:
10845
11145
  "category": "friction | methodology | signal | commercial",
10846
11146
  "content": "string \u2014 specific observation from using PAPI on this project (e.g. 'deprioritise clears handoffs unnecessarily, wasting planner tokens')"
10847
11147
  }
10848
- ]
11148
+ ],
11149
+ "northStar": "string or null \u2014 the current North Star statement. Include if you assessed it in section 4 and it is still accurate (copy the current statement verbatim). Include the updated version if you revised it. Use null ONLY if no North Star has ever been set for this project."
10849
11150
  }
10850
11151
  \`\`\`
10851
11152
 
@@ -11032,7 +11333,8 @@ After your natural language output, include this EXACT format on its own line:
11032
11333
  },
11033
11334
  "oldLabel": "string \u2014 only for modify/remove: the previous phase label so tasks can be migrated"
11034
11335
  }
11035
- ]
11336
+ ],
11337
+ "northStar": "string or null \u2014 include the North Star statement if this strategic change defines or revises the project North Star. Use null if the change does not affect the North Star."
11036
11338
  }
11037
11339
  \`\`\`
11038
11340
 
@@ -11088,6 +11390,9 @@ Task: [title]
11088
11390
  Cycle: [N]
11089
11391
  Why now: [justification]
11090
11392
 
11393
+ DEPENDS ON
11394
+ [Optional \u2014 comma-separated task IDs this task depends on (e.g. "task-123, task-124"). Include only when another task in this same cycle must be built first because this task consumes artifacts it creates (e.g. new adapter method, new type, new migration). The builder will reuse the upstream task's branch so dependent commits stack on the same branch for a single PR. Omit this section entirely if there are no intra-cycle dependencies.]
11395
+
11091
11396
  SCOPE (DO THIS)
11092
11397
  [specific deliverables \u2014 write for the simplest viable path first]
11093
11398
 
@@ -11388,6 +11693,32 @@ async function getPrompt(name) {
11388
11693
  }
11389
11694
 
11390
11695
  // src/services/plan.ts
11696
+ function determineContextTier(cycleCount) {
11697
+ if (cycleCount <= 5) return 1;
11698
+ if (cycleCount <= 20) return 2;
11699
+ return 3;
11700
+ }
11701
+ function applyContextTier(ctx, cycleCount) {
11702
+ const tier = determineContextTier(cycleCount);
11703
+ const label = tier === 1 ? "Tier 1 (cycles 1-5)" : tier === 2 ? "Tier 2 (cycles 6-20)" : "Tier 3 (cycles 21+)";
11704
+ if (tier <= 2) {
11705
+ ctx.strategyRecommendations = void 0;
11706
+ ctx.dogfoodEntries = void 0;
11707
+ }
11708
+ if (tier === 1) {
11709
+ ctx.methodologyMetrics = void 0;
11710
+ ctx.carryForwardStaleness = void 0;
11711
+ ctx.discoveryCanvas = void 0;
11712
+ ctx.estimationCalibration = void 0;
11713
+ ctx.buildPatterns = void 0;
11714
+ ctx.reviewPatterns = void 0;
11715
+ ctx.horizonContext = void 0;
11716
+ ctx.registeredDocs = void 0;
11717
+ ctx.recentReviews = void 0;
11718
+ ctx.strategyReviewCadence = void 0;
11719
+ }
11720
+ return { tier, label };
11721
+ }
11391
11722
  function determineMode(totalCycles) {
11392
11723
  if (totalCycles === 0) return "bootstrap";
11393
11724
  return "full";
@@ -11622,6 +11953,7 @@ function detectBoardFlags(tasks) {
11622
11953
  let hasResearchTasks = false;
11623
11954
  let hasIdeaTasks = false;
11624
11955
  let hasSpikeTasks = false;
11956
+ let hasTaskTasks = false;
11625
11957
  let hasUITasks = false;
11626
11958
  const uiKeywords = /\b(visual|design|UI|styling|refresh|frontend|landing page|hero|carousel|theme|layout|cockpit|dashboard|page)\b/i;
11627
11959
  for (const t of tasks) {
@@ -11629,9 +11961,10 @@ function detectBoardFlags(tasks) {
11629
11961
  if (t.taskType === "research" || /^Research:/i.test(t.title)) hasResearchTasks = true;
11630
11962
  if (t.taskType === "idea") hasIdeaTasks = true;
11631
11963
  if (t.taskType === "spike" || /^Spike:/i.test(t.title)) hasSpikeTasks = true;
11964
+ if (t.taskType === "task") hasTaskTasks = true;
11632
11965
  if (uiKeywords.test(t.title) || uiKeywords.test(t.notes ?? "")) hasUITasks = true;
11633
11966
  }
11634
- return { hasBugTasks, hasResearchTasks, hasIdeaTasks, hasSpikeTasks, hasUITasks };
11967
+ return { hasBugTasks, hasResearchTasks, hasIdeaTasks, hasSpikeTasks, hasTaskTasks, hasUITasks };
11635
11968
  }
11636
11969
  function detectBoardFlagsFromText(boardText) {
11637
11970
  return {
@@ -11639,6 +11972,7 @@ function detectBoardFlagsFromText(boardText) {
11639
11972
  hasResearchTasks: /\b(research|Research:)\b/i.test(boardText),
11640
11973
  hasIdeaTasks: /\bidea\b/i.test(boardText),
11641
11974
  hasSpikeTasks: /\b(spike|Spike:)\b/i.test(boardText),
11975
+ hasTaskTasks: /\btask\b/i.test(boardText),
11642
11976
  hasUITasks: /\b(visual|design|UI|styling|refresh|frontend|landing page|hero|carousel|theme|layout|cockpit|dashboard|page)\b/i.test(boardText)
11643
11977
  };
11644
11978
  }
@@ -11849,6 +12183,9 @@ ${lines.join("\n")}`;
11849
12183
  ]);
11850
12184
  timings["total"] = totalTimer();
11851
12185
  console.error(`[plan-perf] assembleContext (lean): ${JSON.stringify(timings)}ms`);
12186
+ const gap = health.cyclesSinceLastStrategyReview;
12187
+ const lastReviewCycle = health.totalCycles - gap;
12188
+ const strategyReviewCadence = gap <= 0 ? `\u2713 Strategy review completed this cycle (C${health.totalCycles}). No carry-forward needed.` : gap < 5 ? `\u2713 Strategy review on track \u2014 last review was C${lastReviewCycle} (${gap} cycle(s) ago). Next due: C${lastReviewCycle + 5}.` : `\u26A0\uFE0F Strategy review overdue \u2014 last review was C${lastReviewCycle} (${gap} cycles ago). Due now.`;
11852
12189
  let ctx2 = {
11853
12190
  mode,
11854
12191
  cycleNumber: health.totalCycles,
@@ -11870,8 +12207,12 @@ ${lines.join("\n")}`;
11870
12207
  boardFlags,
11871
12208
  carryForwardStaleness: carryForwardStalenessLean,
11872
12209
  preAssignedTasks: preAssignedTextLean,
11873
- recentlyShippedCapabilities: recentlyShippedLean
12210
+ recentlyShippedCapabilities: recentlyShippedLean,
12211
+ strategyReviewCadence
11874
12212
  };
12213
+ const { label: leanTierLabel } = applyContextTier(ctx2, health.totalCycles);
12214
+ ctx2.contextTier = leanTierLabel;
12215
+ console.error(`[plan-perf] context tier: ${leanTierLabel} (cycle ${health.totalCycles})`);
11875
12216
  t = startTimer();
11876
12217
  const prevHashes2 = contextHashesResult.status === "fulfilled" ? contextHashesResult.value : null;
11877
12218
  const { ctx: diffedCtx2, newHashes: newHashes2, savedBytes: savedBytes2 } = applyContextDiff(ctx2, prevHashes2);
@@ -11933,7 +12274,8 @@ ${lines.join("\n")}`;
11933
12274
  if (pendingRecsResultFull.status === "fulfilled" && pendingRecsResultFull.value.length > 0) {
11934
12275
  strategyRecommendationsText = formatStrategyRecommendations(pendingRecsResultFull.value);
11935
12276
  }
11936
- const metricsSnapshots = allReportsForPatterns.length > 0 ? computeSnapshotsFromBuildReports(allReportsForPatterns) : rawMetricsSnapshots.filter((s) => s.accuracy.length > 0 || s.velocity.length > 0);
12277
+ const filteredRaw = rawMetricsSnapshots.filter((s) => s.accuracy.length > 0 || s.velocity.length > 0);
12278
+ const metricsSnapshots = filteredRaw.length > 0 ? filteredRaw : computeSnapshotsFromBuildReports(allReportsForPatterns);
11937
12279
  const discoveryCanvasTextFull = discoveryCanvasResultFull.status === "fulfilled" ? discoveryCanvasResultFull.value : void 0;
11938
12280
  const taskCommentsTextFull = taskCommentsResultFull.status === "fulfilled" ? taskCommentsResultFull.value : void 0;
11939
12281
  let registeredDocsTextFull;
@@ -11969,6 +12311,9 @@ ${lines.join("\n")}`;
11969
12311
  const targetCycle = health.totalCycles + 1;
11970
12312
  const preAssigned = strippedTasks.filter((t2) => t2.cycle === targetCycle);
11971
12313
  const preAssignedText = formatPreAssignedTasks(preAssigned, targetCycle);
12314
+ const gapFull = health.cyclesSinceLastStrategyReview;
12315
+ const lastReviewCycleFull = health.totalCycles - gapFull;
12316
+ const strategyReviewCadenceFull = gapFull <= 0 ? `\u2713 Strategy review completed this cycle (C${health.totalCycles}). No carry-forward needed.` : gapFull < 5 ? `\u2713 Strategy review on track \u2014 last review was C${lastReviewCycleFull} (${gapFull} cycle(s) ago). Next due: C${lastReviewCycleFull + 5}.` : `\u26A0\uFE0F Strategy review overdue \u2014 last review was C${lastReviewCycleFull} (${gapFull} cycles ago). Due now.`;
11972
12317
  let ctx = {
11973
12318
  mode,
11974
12319
  cycleNumber: health.totalCycles,
@@ -11992,8 +12337,12 @@ ${lines.join("\n")}`;
11992
12337
  boardFlags: boardFlagsFull,
11993
12338
  carryForwardStaleness: computeCarryForwardStaleness(log),
11994
12339
  preAssignedTasks: preAssignedText,
11995
- recentlyShippedCapabilities: formatRecentlyShippedCapabilities(allReportsForPatterns)
12340
+ recentlyShippedCapabilities: formatRecentlyShippedCapabilities(reports),
12341
+ strategyReviewCadence: strategyReviewCadenceFull
11996
12342
  };
12343
+ const { label: fullTierLabel } = applyContextTier(ctx, health.totalCycles);
12344
+ ctx.contextTier = fullTierLabel;
12345
+ console.error(`[plan-perf] context tier: ${fullTierLabel} (cycle ${health.totalCycles})`);
11997
12346
  const prevHashes = contextHashesResultFull.status === "fulfilled" ? contextHashesResultFull.value : null;
11998
12347
  const { ctx: diffedCtx, newHashes, savedBytes } = applyContextDiff(ctx, prevHashes);
11999
12348
  ctx = diffedCtx;
@@ -12183,7 +12532,15 @@ ${cleanContent}`;
12183
12532
  taskCount: cycleTaskCount > 0 ? cycleTaskCount : void 0,
12184
12533
  effortPoints: cycleEffortPoints > 0 ? cycleEffortPoints : void 0
12185
12534
  });
12186
- const healthPromise = Promise.resolve();
12535
+ const healthUpdates = {
12536
+ totalCycles: newCycleNumber,
12537
+ boardHealth: data.boardHealth,
12538
+ strategicDirection: data.strategicDirection
12539
+ };
12540
+ if (data.nextMode === "Full") {
12541
+ healthUpdates.lastFullMode = newCycleNumber;
12542
+ }
12543
+ const healthPromise = adapter2.setCycleHealth(healthUpdates);
12187
12544
  const newTaskIdMap = /* @__PURE__ */ new Map();
12188
12545
  const createTasksPromise = (async () => {
12189
12546
  if (!data.newTasks || data.newTasks.length === 0) return;
@@ -12603,6 +12960,17 @@ async function preparePlan(adapter2, config2, filters, focus, force, handoffsOnl
12603
12960
  }
12604
12961
  if (skipHandoffs) context.skipHandoffs = true;
12605
12962
  t = startTimer();
12963
+ try {
12964
+ const scanTasks = await adapter2.queryBoard({ status: ["Backlog", "In Cycle", "Ready"] });
12965
+ const candidates = scanTasks.filter((task) => task.priority !== "P3 Low").slice(0, 15).map((task) => ({ id: task.id, title: task.title, notes: task.notes }));
12966
+ const scanResult = scanCodebaseForTasks(config2.projectRoot, candidates);
12967
+ if (scanResult) context.codebaseScan = scanResult;
12968
+ } catch (err) {
12969
+ console.error(`[plan] codebase scan failed (non-critical): ${err instanceof Error ? err.message : err}`);
12970
+ }
12971
+ const scanMs = t();
12972
+ console.error(`[plan-perf] codebaseScan: ${scanMs}ms`);
12973
+ t = startTimer();
12606
12974
  const userMessage = buildPlanUserMessage(context);
12607
12975
  const buildMessageMs = t();
12608
12976
  const totalMs = prepareTimer();
@@ -12779,6 +13147,7 @@ var lastPrepareSkipHandoffs;
12779
13147
  var planTool = {
12780
13148
  name: "plan",
12781
13149
  description: 'Run once per cycle to select tasks and generate BUILD HANDOFFs. Call after setup (first time) or after completing all builds AND running release for the previous cycle. Returns prioritised task recommendations with detailed implementation specs. NEVER call when unbuilt cycle tasks exist \u2014 build and release 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. Use skip_handoffs=true for large backlogs \u2014 handoffs are then generated separately via `handoff_generate`.',
13150
+ annotations: { readOnlyHint: false, destructiveHint: false },
12782
13151
  inputSchema: {
12783
13152
  type: "object",
12784
13153
  properties: {
@@ -12906,24 +13275,30 @@ async function handlePlan(adapter2, config2, args) {
12906
13275
  return errorResponse('llm_response is required for mode "apply". Pass your complete plan output including the <!-- PAPI_STRUCTURED_OUTPUT --> block.');
12907
13276
  }
12908
13277
  const planMode = args.plan_mode || "full";
12909
- const cycleNumber = typeof args.cycle_number === "number" ? args.cycle_number : 0;
13278
+ const rawCycleNumber = args.cycle_number != null ? Number(args.cycle_number) : NaN;
12910
13279
  const strategyReviewWarning = args.strategy_review_warning || "";
12911
13280
  const contextHashes = lastPrepareContextHashes;
12912
13281
  const inputContext = lastPrepareUserMessage;
12913
13282
  const contextBytes = lastPrepareContextBytes;
12914
13283
  const expectedCycleNumber = lastPrepareCycleNumber;
12915
13284
  const skipHandoffsCached = lastPrepareSkipHandoffs;
12916
- lastPrepareContextHashes = void 0;
12917
- lastPrepareUserMessage = void 0;
12918
- lastPrepareContextBytes = void 0;
12919
- lastPrepareCycleNumber = void 0;
12920
- lastPrepareSkipHandoffs = void 0;
12921
13285
  const skipHandoffs = args.skip_handoffs === true || skipHandoffsCached === true;
13286
+ const cycleNumber = !isNaN(rawCycleNumber) ? rawCycleNumber : expectedCycleNumber !== void 0 ? expectedCycleNumber : NaN;
13287
+ if (isNaN(cycleNumber)) {
13288
+ return errorResponse(
13289
+ "cycle_number is required for apply mode. Pass the cycle_number from the prepare phase output."
13290
+ );
13291
+ }
12922
13292
  if (expectedCycleNumber !== void 0 && cycleNumber !== expectedCycleNumber) {
12923
13293
  return errorResponse(
12924
13294
  `cycle_number mismatch: prepare phase returned cycle ${expectedCycleNumber} but apply received ${cycleNumber}. Pass cycle_number: ${expectedCycleNumber} to match the prepare output.`
12925
13295
  );
12926
13296
  }
13297
+ lastPrepareContextHashes = void 0;
13298
+ lastPrepareUserMessage = void 0;
13299
+ lastPrepareContextBytes = void 0;
13300
+ lastPrepareCycleNumber = void 0;
13301
+ lastPrepareSkipHandoffs = void 0;
12927
13302
  const result = await applyPlan(adapter2, config2, llmResponse, planMode, cycleNumber, strategyReviewWarning, contextHashes, { contextBytes: contextBytes ?? void 0, skipHandoffs: skipHandoffs || void 0 });
12928
13303
  let utilisation;
12929
13304
  if (inputContext) {
@@ -13122,7 +13497,7 @@ function classifyRecommendation(text) {
13122
13497
  if (lower.includes("new task") || lower.includes("create task") || lower.includes("add task") || lower.includes("spike")) {
13123
13498
  return "new_task";
13124
13499
  }
13125
- if (lower.includes("process") || lower.includes("workflow") || lower.includes("methodology") || lower.includes("retrospective") || lower.includes("dogfood")) {
13500
+ if (lower.includes("process") || lower.includes("workflow") || lower.includes("methodology") || lower.includes("retrospective") || lower.includes("dogfood") || lower.includes("refine")) {
13126
13501
  return "process_improvement";
13127
13502
  }
13128
13503
  if (lower.includes("infrastructure") || lower.includes("deploy") || lower.includes("ci/cd") || lower.includes("pipeline") || lower.includes("hosting") || lower.includes("database") || lower.includes("migration")) {
@@ -13782,6 +14157,12 @@ ${cleanContent}`;
13782
14157
  } catch {
13783
14158
  }
13784
14159
  }
14160
+ if (data.northStar && adapter2.upsertNorthStar) {
14161
+ try {
14162
+ await adapter2.upsertNorthStar(data.northStar, cycleNumber);
14163
+ } catch {
14164
+ }
14165
+ }
13785
14166
  const compressionThreshold = cycleNumber - 5;
13786
14167
  if (compressionThreshold > 0 && data.sessionLogCompressionSummary) {
13787
14168
  await adapter2.compressCycleLog(compressionThreshold, data.sessionLogCompressionSummary);
@@ -13841,7 +14222,7 @@ ${cleanContent}`;
13841
14222
  try {
13842
14223
  const canvas = await adapter2.readDiscoveryCanvas();
13843
14224
  const updates = {};
13844
- let populatedSections = [];
14225
+ const populatedSections = [];
13845
14226
  if (!canvas.landscapeReferences || canvas.landscapeReferences.length === 0) {
13846
14227
  if (data.activeDecisionUpdates?.length) {
13847
14228
  const entries = data.activeDecisionUpdates.filter((ad) => ad.body && ad.action !== "delete").slice(0, 3).map((ad) => ({ name: ad.id, notes: ad.body.slice(0, 200) }));
@@ -14104,10 +14485,17 @@ function formatVelocitySummary(reports, cycleCount) {
14104
14485
  function formatRecentReportsSummary(reports, count) {
14105
14486
  const recent = reports.sort((a, b2) => b2.cycle - a.cycle || b2.date.localeCompare(a.date)).slice(0, count);
14106
14487
  if (recent.length === 0) return "No recent build reports.";
14488
+ const trunc = (s, max) => s && s !== "None" ? s.length > max ? s.slice(0, max) + "..." : s : null;
14107
14489
  return recent.map((r) => {
14108
14490
  const effort = `${r.actualEffort} vs ${r.estimatedEffort}`;
14109
- const surprises = r.surprises && r.surprises !== "None" ? ` \u2014 ${r.surprises.slice(0, 80)}${r.surprises.length > 80 ? "..." : ""}` : "";
14110
- return `- S${r.cycle} ${r.taskName}: ${effort}${surprises}`;
14491
+ const lines = [`- C${r.cycle} ${r.taskName}: ${effort}`];
14492
+ const surprises = trunc(r.surprises, 200);
14493
+ if (surprises) lines.push(` _Surprises:_ ${surprises}`);
14494
+ const issues = trunc(r.discoveredIssues, 200);
14495
+ if (issues) lines.push(` _Issues:_ ${issues}`);
14496
+ const arch = trunc(r.architectureNotes, 200);
14497
+ if (arch) lines.push(` _Architecture:_ ${arch}`);
14498
+ return lines.join("\n");
14111
14499
  }).join("\n");
14112
14500
  }
14113
14501
  function formatPhasesForReview(phases, currentCycle) {
@@ -14133,7 +14521,7 @@ async function formatHierarchyForReview(adapter2, currentCycle, prefetchedTasks)
14133
14521
  } catch {
14134
14522
  }
14135
14523
  if (horizons.length === 0 && phases.length === 0) return void 0;
14136
- let tasksByPhase = /* @__PURE__ */ new Map();
14524
+ const tasksByPhase = /* @__PURE__ */ new Map();
14137
14525
  try {
14138
14526
  const allTasks = prefetchedTasks ?? await adapter2.queryBoard();
14139
14527
  for (const t of allTasks) {
@@ -14278,6 +14666,12 @@ ${cleanContent}`;
14278
14666
  const currentPhases = await adapter2.readPhases();
14279
14667
  await applyPhaseUpdates(adapter2, currentPhases, data.phaseUpdates);
14280
14668
  }
14669
+ if (data.northStar && adapter2.upsertNorthStar) {
14670
+ try {
14671
+ await adapter2.upsertNorthStar(data.northStar, cycleNumber);
14672
+ } catch {
14673
+ }
14674
+ }
14281
14675
  } catch (err) {
14282
14676
  writeBackFailed = err instanceof Error ? err.message : String(err);
14283
14677
  }
@@ -14400,6 +14794,7 @@ var lastReviewContextBytes;
14400
14794
  var strategyReviewTool = {
14401
14795
  name: "strategy_review",
14402
14796
  description: 'Run a Strategy Review \u2014 assesses project direction, velocity, and Active Decisions. Produces recommendations and potential AD updates that feed into the next plan. Offered every 5 cycles; hard-blocked at 7+ overdue cycles. Run in its own dedicated session \u2014 do not mix with building. First call returns a review prompt for you to execute (prepare phase). Then call again with mode "apply" and your output. Pass `force: true` to run before the cadence gate.',
14797
+ annotations: { readOnlyHint: false, destructiveHint: false },
14403
14798
  inputSchema: {
14404
14799
  type: "object",
14405
14800
  properties: {
@@ -14427,6 +14822,7 @@ var strategyReviewTool = {
14427
14822
  var strategyChangeTool = {
14428
14823
  name: "strategy_change",
14429
14824
  description: 'Apply a strategic shift to the project. Three modes: "capture" for lightweight mid-conversation decision capture (no LLM round-trip), "prepare" to get a change prompt for full analysis, "apply" to persist analysis output. Use "capture" when you detect a strategic decision in conversation and want to persist it quickly without disrupting the build flow.',
14825
+ annotations: { readOnlyHint: false, destructiveHint: false },
14430
14826
  inputSchema: {
14431
14827
  type: "object",
14432
14828
  properties: {
@@ -14764,6 +15160,7 @@ async function archiveTasks(adapter2, phases, statuses) {
14764
15160
  var boardViewTool = {
14765
15161
  name: "board_view",
14766
15162
  description: 'View the Board. By default shows active tasks only (excludes Done/Cancelled), sorted by priority, limited to 50. Use status="all" to see everything. Use mode="summary" for counts only (no task details). Does not call the Anthropic API.',
15163
+ annotations: { readOnlyHint: true, destructiveHint: false },
14767
15164
  inputSchema: {
14768
15165
  type: "object",
14769
15166
  properties: {
@@ -14795,6 +15192,7 @@ var boardViewTool = {
14795
15192
  var boardDeprioritiseTool = {
14796
15193
  name: "board_deprioritise",
14797
15194
  description: `Remove a task from the current cycle. Four actions: "backlog" (not now, maybe later \u2014 preserves handoff), "defer" (valid but premature \u2014 hidden from planner), "block" (waiting on external dependency \u2014 visible on board but skipped by planner), "cancel" (don't want this \u2014 permanently closed with reason). When a user rejects a task, ALWAYS ask which action they want. Does not call the Anthropic API.`,
15195
+ annotations: { readOnlyHint: false, destructiveHint: true },
14798
15196
  inputSchema: {
14799
15197
  type: "object",
14800
15198
  properties: {
@@ -14830,6 +15228,7 @@ var boardDeprioritiseTool = {
14830
15228
  var boardArchiveTool = {
14831
15229
  name: "board_archive",
14832
15230
  description: "Archive tasks from the Board to the archive file. When both phase and status are provided, only tasks matching BOTH are archived (AND logic). When only one is provided, all matching tasks are archived. Does not call the Anthropic API.",
15231
+ annotations: { readOnlyHint: false, destructiveHint: true },
14833
15232
  inputSchema: {
14834
15233
  type: "object",
14835
15234
  properties: {
@@ -14848,6 +15247,7 @@ var boardArchiveTool = {
14848
15247
  var boardEditTool = {
14849
15248
  name: "board_edit",
14850
15249
  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.",
15250
+ annotations: { readOnlyHint: false, destructiveHint: false },
14851
15251
  inputSchema: {
14852
15252
  type: "object",
14853
15253
  properties: {
@@ -15114,7 +15514,7 @@ async function handleBoardEdit(adapter2, args) {
15114
15514
  try {
15115
15515
  const dogfoodLog = await adapter2.getDogfoodLog?.(50) ?? [];
15116
15516
  const linked = dogfoodLog.filter((e) => e.linkedTaskId === taskId || e.linkedTaskId === task.id);
15117
- const newStatus = updates.status === "Done" ? "actioned" : "dismissed";
15517
+ const newStatus = "resolved";
15118
15518
  await Promise.all(linked.map((e) => adapter2.updateDogfoodEntryStatus(e.id, newStatus)));
15119
15519
  } catch {
15120
15520
  }
@@ -15486,6 +15886,7 @@ When the system compresses prior messages, immediately:
15486
15886
 
15487
15887
  - **XS/S tasks in the same cycle and module:** Group on shared branch. One PR, one merge.
15488
15888
  - **M/L tasks or different modules:** Own branch per task. Isolated PRs.
15889
+ - **Dependent tasks (any size):** When a task's BUILD HANDOFF lists a \`DEPENDS ON\` task from the same cycle, \`build_execute\` automatically reuses the upstream task's branch so commits stack for a single PR. Do not create a separate branch manually.
15489
15890
  - **Commit per task within grouped branches** \u2014 traceable git history.
15490
15891
  - **Never use \`build_execute\` with \`light=true\` on shared branches.** Light mode commits directly to the current branch without creating a PR. When a shared branch is squash-merged, those commits are collapsed \u2014 any CLAUDE.md or documentation changes are stripped. Use light mode only on isolated single-task branches where no squash-merge will occur.
15491
15892
 
@@ -16285,6 +16686,7 @@ async function applySetup(adapter2, config2, input, briefText, adSeedText, conve
16285
16686
  var setupTool = {
16286
16687
  name: "setup",
16287
16688
  description: `Initialise a new PAPI project or adopt an existing codebase. Only 3 inputs needed: project name, what it does, and who it's for. Set existing_project: true to adopt an existing codebase \u2014 PAPI scans the project structure and generates a context-aware brief, hierarchy, and initial backlog tasks. First call returns prompts (prepare phase), then call again with mode "apply" and your outputs. After setup, run plan to start your first cycle.`,
16689
+ annotations: { readOnlyHint: false, destructiveHint: false },
16288
16690
  inputSchema: {
16289
16691
  type: "object",
16290
16692
  properties: {
@@ -16554,6 +16956,8 @@ import { randomUUID as randomUUID9 } from "crypto";
16554
16956
  import { readdirSync as readdirSync3, existsSync as existsSync3, readFileSync } from "fs";
16555
16957
  import { join as join5 } from "path";
16556
16958
  var buildStartTimes = /* @__PURE__ */ new Map();
16959
+ var taskBranchMap = /* @__PURE__ */ new Map();
16960
+ var SHARED_BRANCH_COMPLEXITIES = /* @__PURE__ */ new Set(["XS", "Small"]);
16557
16961
  function capitalizeCompleted(value) {
16558
16962
  const map = {
16559
16963
  yes: "Yes",
@@ -16573,7 +16977,7 @@ function pushAndCreatePR(config2, taskId, taskTitle) {
16573
16977
  if (!isGitAvailable() || !isGitRepo(config2.projectRoot)) {
16574
16978
  return lines;
16575
16979
  }
16576
- const featureBranch = taskBranchName(taskId);
16980
+ const featureBranch = taskBranchMap.get(taskId) ?? taskBranchName(taskId);
16577
16981
  const currentBranch = getCurrentBranch(config2.projectRoot);
16578
16982
  if (currentBranch !== featureBranch) {
16579
16983
  return lines;
@@ -16705,10 +17109,38 @@ async function startBuild(adapter2, config2, taskId, options = {}) {
16705
17109
  if (options.light) {
16706
17110
  branchLines.push("Light mode: skipping branch creation \u2014 working on current branch.");
16707
17111
  } else if (config2.autoCommit && isGitAvailable() && isGitRepo(config2.projectRoot)) {
16708
- const featureBranch = taskBranchName(taskId);
17112
+ const cycleHealth = await adapter2.getCycleHealth().catch(() => null);
17113
+ const cycleNumber = cycleHealth?.totalCycles ?? 0;
17114
+ let depBranchReuse = null;
17115
+ if (task.dependsOn) {
17116
+ const depIds = task.dependsOn.split(",").map((d) => d.trim()).filter(Boolean);
17117
+ for (const depId of depIds) {
17118
+ const mappedBranch = taskBranchMap.get(depId);
17119
+ if (mappedBranch && branchExists(config2.projectRoot, mappedBranch)) {
17120
+ depBranchReuse = { branch: mappedBranch, upstreamId: depId };
17121
+ break;
17122
+ }
17123
+ const fallbackBranch = taskBranchName(depId);
17124
+ if (branchExists(config2.projectRoot, fallbackBranch)) {
17125
+ depBranchReuse = { branch: fallbackBranch, upstreamId: depId };
17126
+ break;
17127
+ }
17128
+ }
17129
+ }
17130
+ const useSharedBranch = !depBranchReuse && SHARED_BRANCH_COMPLEXITIES.has(task.complexity) && !!task.module && cycleNumber > 0;
17131
+ const featureBranch = depBranchReuse ? depBranchReuse.branch : useSharedBranch ? cycleBranchName(cycleNumber, task.module) : taskBranchName(taskId);
17132
+ if (depBranchReuse) {
17133
+ branchLines.push(
17134
+ `Reusing branch '${depBranchReuse.branch}' from dependency ${depBranchReuse.upstreamId} \u2014 commits will stack for a single PR.`
17135
+ );
17136
+ }
17137
+ taskBranchMap.set(taskId, featureBranch);
16709
17138
  const currentBranch = getCurrentBranch(config2.projectRoot);
16710
17139
  if (currentBranch === featureBranch) {
16711
17140
  branchLines.push(`Already on branch '${featureBranch}'.`);
17141
+ if (useSharedBranch) {
17142
+ branchLines.push(`Reusing shared cycle branch for ${task.complexity} ${task.module} task.`);
17143
+ }
16712
17144
  } else {
16713
17145
  if (hasUncommittedChanges(config2.projectRoot, AUTO_WRITTEN_PATHS)) {
16714
17146
  throw new Error("Working directory has uncommitted changes. Please commit or stash them before running `build_execute`.");
@@ -16717,13 +17149,14 @@ async function startBuild(adapter2, config2, taskId, options = {}) {
16717
17149
  if (baseBranch !== config2.baseBranch) {
16718
17150
  branchLines.push(`Base branch '${config2.baseBranch}' not found \u2014 using '${baseBranch}'.`);
16719
17151
  }
16720
- if (currentBranch !== baseBranch) {
17152
+ const featureBranchExists = branchExists(config2.projectRoot, featureBranch);
17153
+ if (currentBranch !== baseBranch && !featureBranchExists) {
16721
17154
  const checkout = checkoutBranch(config2.projectRoot, baseBranch);
16722
17155
  if (!checkout.success) {
16723
17156
  branchLines.push(`Warning: ${checkout.message} Proceeding on current branch '${currentBranch}'.`);
16724
17157
  }
16725
17158
  }
16726
- if (hasRemote(config2.projectRoot)) {
17159
+ if (hasRemote(config2.projectRoot) && !featureBranchExists) {
16727
17160
  const pull = gitPull(config2.projectRoot);
16728
17161
  branchLines.push(pull.success ? pull.message : `Warning: ${pull.message}`);
16729
17162
  }
@@ -16733,10 +17166,10 @@ async function startBuild(adapter2, config2, taskId, options = {}) {
16733
17166
  `Warning: ${unmerged.length} unmerged feature branch${unmerged.length === 1 ? "" : "es"}: ${unmerged.join(", ")}. New branch may diverge if these contain changes needed here.`
16734
17167
  );
16735
17168
  }
16736
- if (branchExists(config2.projectRoot, featureBranch)) {
17169
+ if (featureBranchExists) {
16737
17170
  const checkout = checkoutBranch(config2.projectRoot, featureBranch);
16738
17171
  branchLines.push(
16739
- checkout.success ? `Checked out existing branch '${featureBranch}'.` : `Warning: ${checkout.message}`
17172
+ checkout.success ? useSharedBranch ? `Checked out shared cycle branch '${featureBranch}' \u2014 reusing for ${task.complexity} ${task.module} task.` : `Checked out existing branch '${featureBranch}'.` : `Warning: ${checkout.message}`
16740
17173
  );
16741
17174
  if (checkout.success) {
16742
17175
  branchLines.push(
@@ -16746,7 +17179,7 @@ async function startBuild(adapter2, config2, taskId, options = {}) {
16746
17179
  } else {
16747
17180
  const create = createAndCheckoutBranch(config2.projectRoot, featureBranch);
16748
17181
  branchLines.push(
16749
- create.success ? `Created branch '${featureBranch}'.` : `Warning: ${create.message}`
17182
+ create.success ? useSharedBranch ? `Created shared cycle branch '${featureBranch}' for ${task.module} XS/S tasks.` : `Created branch '${featureBranch}'.` : `Warning: ${create.message}`
16750
17183
  );
16751
17184
  if (create.success) {
16752
17185
  branchLines.push(
@@ -16830,11 +17263,27 @@ async function completeBuild(adapter2, config2, taskId, input, options = {}) {
16830
17263
  completedAt: now.toISOString()
16831
17264
  };
16832
17265
  buildStartTimes.delete(taskId);
17266
+ taskBranchMap.delete(taskId);
16833
17267
  if (input.relatedDecisions) {
16834
17268
  const adIds = input.relatedDecisions.split(",").map((s) => s.trim()).filter(Boolean);
16835
17269
  if (adIds.length > 0) report.relatedDecisions = adIds;
16836
17270
  }
17271
+ if (report.startedAt && report.completedAt && typeof adapter2.getToolCallCount === "function") {
17272
+ try {
17273
+ const count = await adapter2.getToolCallCount(report.startedAt, report.completedAt);
17274
+ if (count > 0) report.toolCallCount = count;
17275
+ } catch {
17276
+ }
17277
+ }
16837
17278
  await adapter2.appendBuildReport(report);
17279
+ let reportWriteVerified;
17280
+ if (typeof adapter2.getBuildReportCountForTask === "function") {
17281
+ try {
17282
+ const postWriteCount = await adapter2.getBuildReportCountForTask(taskId);
17283
+ reportWriteVerified = postWriteCount >= iterationCount;
17284
+ } catch {
17285
+ }
17286
+ }
16838
17287
  if (adapter2.appendCycleLearnings) {
16839
17288
  const learnings = [];
16840
17289
  const taskModule = task.module ?? "";
@@ -16881,6 +17330,39 @@ async function completeBuild(adapter2, config2, taskId, input, options = {}) {
16881
17330
  }
16882
17331
  }
16883
17332
  }
17333
+ let autoTriagedCount = 0;
17334
+ if (input.discoveredIssues && input.discoveredIssues !== "None" && typeof adapter2.createTask === "function") {
17335
+ const issueLines = input.discoveredIssues.split(/\n|;/).map((s) => s.trim()).filter((s) => s.length > 0);
17336
+ for (const line of issueLines) {
17337
+ const sevMatch = line.match(/^(P[0-3])[\s:]+/i);
17338
+ if (!sevMatch) continue;
17339
+ const severityLabel = sevMatch[1].toUpperCase();
17340
+ const priority = severityLabel === "P0" || severityLabel === "P1" ? "P1 High" : severityLabel === "P2" ? "P2 Medium" : "P3 Low";
17341
+ const titleRaw = line.replace(/^P[0-3][\s:]+/i, "").trim();
17342
+ const title = titleRaw.length > 120 ? titleRaw.slice(0, 120) : titleRaw;
17343
+ if (!title) continue;
17344
+ try {
17345
+ await adapter2.createTask({
17346
+ uuid: "",
17347
+ displayId: "",
17348
+ title: `[Auto-triaged] ${title}`,
17349
+ status: "Backlog",
17350
+ priority,
17351
+ complexity: "Small",
17352
+ module: task.module ?? "",
17353
+ phase: task.phase ?? "",
17354
+ owner: "papi",
17355
+ reviewed: false,
17356
+ taskType: "discovery",
17357
+ source: "build_complete",
17358
+ notes: `Origin: ${task.displayId} (${task.title}), cycle ${cycleNumber}. Original issue: ${line}`,
17359
+ createdCycle: cycleNumber
17360
+ });
17361
+ autoTriagedCount++;
17362
+ } catch {
17363
+ }
17364
+ }
17365
+ }
16884
17366
  if (adapter2.updateCycleLearningActionRef && task.notes) {
16885
17367
  const learningRefs = task.notes.match(/learning:([a-f0-9-]+)/gi);
16886
17368
  if (learningRefs) {
@@ -16923,6 +17405,52 @@ async function completeBuild(adapter2, config2, taskId, input, options = {}) {
16923
17405
  await adapter2.updateTaskStatus(taskId, "In Review");
16924
17406
  }
16925
17407
  }
17408
+ let dogfoodResolvedCount = 0;
17409
+ if (input.completed === "yes" && adapter2.getDogfoodLog && adapter2.updateDogfoodEntryStatus) {
17410
+ try {
17411
+ const dogfoodLog = await adapter2.getDogfoodLog(50);
17412
+ const linked = dogfoodLog.filter(
17413
+ (e) => (e.linkedTaskId === taskId || e.linkedTaskId === task.id) && e.status !== "resolved"
17414
+ );
17415
+ if (linked.length > 0) {
17416
+ await Promise.all(linked.map((e) => adapter2.updateDogfoodEntryStatus(e.id, "resolved")));
17417
+ dogfoodResolvedCount = linked.length;
17418
+ }
17419
+ } catch {
17420
+ }
17421
+ }
17422
+ let learningsLinkedCount = 0;
17423
+ if (input.completed === "yes" && adapter2.getCycleLearnings && adapter2.updateCycleLearningActionRef) {
17424
+ try {
17425
+ const recentLearnings = await adapter2.getCycleLearnings({ limit: 30 });
17426
+ const unactioned = recentLearnings.filter(
17427
+ (l) => !l.actionRef && l.cycleNumber >= cycleNumber - 5
17428
+ );
17429
+ if (unactioned.length > 0) {
17430
+ const taskText = `${task.title} ${task.notes ?? ""}`.toLowerCase();
17431
+ const taskWords = new Set(
17432
+ taskText.match(/\b[a-z]{4,}\b/g) ?? []
17433
+ );
17434
+ const taskModule = (task.module ?? "").toLowerCase();
17435
+ for (const learning of unactioned) {
17436
+ const learningModule = (learning.tags[0] ?? "").toLowerCase();
17437
+ if (!taskModule || !learningModule || taskModule !== learningModule) continue;
17438
+ const learningText = `${learning.summary} ${learning.detail ?? ""}`.toLowerCase();
17439
+ const learningWords = learningText.match(/\b[a-z]{4,}\b/g) ?? [];
17440
+ const hasKeywordOverlap = learningWords.some((w) => taskWords.has(w));
17441
+ if (!hasKeywordOverlap) continue;
17442
+ if (learning.id) {
17443
+ try {
17444
+ await adapter2.updateCycleLearningActionRef(learning.id, task.id);
17445
+ learningsLinkedCount++;
17446
+ } catch {
17447
+ }
17448
+ }
17449
+ }
17450
+ }
17451
+ } catch {
17452
+ }
17453
+ }
16926
17454
  const statusNote = input.completed === "yes" ? options.light ? `Task "${task.title}" (${taskId}) marked Done (light mode \u2014 no review needed).` : `Task "${task.title}" (${taskId}) marked In Review \u2014 ready for your sign-off via \`review_submit\`.` : `Task "${task.title}" (${taskId}) status unchanged (completed: ${input.completed}).`;
16927
17455
  let commitLine;
16928
17456
  if (config2.autoCommit) {
@@ -17023,7 +17551,11 @@ async function completeBuild(adapter2, config2, taskId, input, options = {}) {
17023
17551
  completed: input.completed,
17024
17552
  scopeAccuracy: input.scopeAccuracy,
17025
17553
  phaseChanges,
17026
- docWarning
17554
+ docWarning,
17555
+ dogfoodResolvedCount: dogfoodResolvedCount > 0 ? dogfoodResolvedCount : void 0,
17556
+ learningsLinkedCount: learningsLinkedCount > 0 ? learningsLinkedCount : void 0,
17557
+ autoTriagedCount: autoTriagedCount > 0 ? autoTriagedCount : void 0,
17558
+ reportWriteVerified
17027
17559
  };
17028
17560
  }
17029
17561
  async function cancelBuild(adapter2, taskId, reason) {
@@ -17116,6 +17648,7 @@ ${instructions}`;
17116
17648
  var buildListTool = {
17117
17649
  name: "build_list",
17118
17650
  description: "List cycle tasks that have BUILD HANDOFFs ready for execution. Shows task ID, title, status, priority, and complexity. In Progress tasks appear first, then Backlog. Does not call the Anthropic API.",
17651
+ annotations: { readOnlyHint: true, destructiveHint: false },
17119
17652
  inputSchema: {
17120
17653
  type: "object",
17121
17654
  properties: {},
@@ -17125,6 +17658,7 @@ var buildListTool = {
17125
17658
  var buildDescribeTool = {
17126
17659
  name: "build_describe",
17127
17660
  description: "Show the full BUILD HANDOFF for a specific task, including scope, acceptance criteria, and implementation guidance. Does not call the Anthropic API.",
17661
+ annotations: { readOnlyHint: true, destructiveHint: false },
17128
17662
  inputSchema: {
17129
17663
  type: "object",
17130
17664
  properties: {
@@ -17139,6 +17673,7 @@ var buildDescribeTool = {
17139
17673
  var buildExecuteTool = {
17140
17674
  name: "build_execute",
17141
17675
  description: "Start or complete a build task. Call with just task_id to start (returns BUILD HANDOFF, creates feature branch, marks In Progress). After implementing the task, you MUST call build_execute again with all report fields (completed, effort, estimated_effort, surprises, discovered_issues, architecture_notes) to finish \u2014 do not wait for user confirmation between start and complete. Never call on tasks that are already In Review or Done. Does not call the Anthropic API. Set light=true to skip branch/PR creation (commits to current branch). Set PAPI_LIGHT_MODE=true in env to default all builds to light mode.",
17676
+ annotations: { readOnlyHint: false, destructiveHint: false },
17142
17677
  inputSchema: {
17143
17678
  type: "object",
17144
17679
  properties: {
@@ -17171,7 +17706,7 @@ var buildExecuteTool = {
17171
17706
  },
17172
17707
  discovered_issues: {
17173
17708
  type: "string",
17174
- description: `Problems found DURING this build that are OUTSIDE this task's scope. Include severity (P0-P3). Good: "P2: Auth middleware doesn't validate token expiry \u2014 affects all protected routes." Bad: "Had to install a dependency." Only real bugs or gaps that need their own task. Use "None" if none. Required for complete.`
17709
+ description: `Problems found DURING this build that are OUTSIDE this task's scope. Include severity (P0-P3). Good: "P2: Auth middleware doesn't validate token expiry \u2014 affects all protected routes." Bad: "Had to install a dependency." Only real bugs or gaps that need their own task. Use "None" if none. Required for complete. TIP: When submitting a follow-up idea for a discovered issue, include "learning:<uuid>" in the idea notes to link it to this cycle learning entry \u2014 use the UUID returned in the build completion output.`
17175
17710
  },
17176
17711
  architecture_notes: {
17177
17712
  type: "string",
@@ -17223,6 +17758,7 @@ var buildExecuteTool = {
17223
17758
  var buildCancelTool = {
17224
17759
  name: "build_cancel",
17225
17760
  description: "Cancel a build task with a reason. Sets the task status to Cancelled and records the closure reason. Does not call the Anthropic API.",
17761
+ annotations: { readOnlyHint: false, destructiveHint: true },
17226
17762
  inputSchema: {
17227
17763
  type: "object",
17228
17764
  properties: {
@@ -17390,9 +17926,28 @@ If >80% of the scope is already implemented, call \`build_execute\` with complet
17390
17926
  adSection = formatRelevantADs(relevant);
17391
17927
  } catch {
17392
17928
  }
17929
+ let dogfoodSection = "";
17930
+ try {
17931
+ if (adapter2.getDogfoodLog) {
17932
+ const dogfoodLog = await adapter2.getDogfoodLog(50);
17933
+ const linked = dogfoodLog.filter(
17934
+ (e) => e.linkedTaskId === result.task.id || e.linkedTaskId === result.task.displayId
17935
+ );
17936
+ if (linked.length > 0) {
17937
+ const entries = linked.map((e) => `- [${e.category}] ${e.content}`).join("\n");
17938
+ dogfoodSection = `
17939
+
17940
+ ---
17941
+
17942
+ **DOGFOOD CONTEXT** \u2014 This task was linked to ${linked.length} observation(s):
17943
+ ${entries}`;
17944
+ }
17945
+ }
17946
+ } catch {
17947
+ }
17393
17948
  const moduleInstructions = getModuleInstructions(result.task.module);
17394
17949
  const moduleContext = await getModuleContext(adapter2, result.task);
17395
- return textResponse(header + serializeBuildHandoff(result.task.buildHandoff) + adSection + moduleInstructions + moduleContext + verificationNote + chainInstruction + phaseNote);
17950
+ return textResponse(header + serializeBuildHandoff(result.task.buildHandoff) + adSection + moduleInstructions + moduleContext + dogfoodSection + verificationNote + chainInstruction + phaseNote);
17396
17951
  } catch (err) {
17397
17952
  if (isNoHandoffError(err)) {
17398
17953
  const lines = [
@@ -17499,6 +18054,18 @@ function formatCompleteResult(result) {
17499
18054
  lines.push(`Phase auto-updated: ${c.phaseId} ${c.oldStatus} \u2192 ${c.newStatus}`);
17500
18055
  }
17501
18056
  }
18057
+ if (result.dogfoodResolvedCount) {
18058
+ lines.push("", `Resolved ${result.dogfoodResolvedCount} dogfood observation(s) linked to this task.`);
18059
+ }
18060
+ if (result.learningsLinkedCount) {
18061
+ lines.push("", `Linked ${result.learningsLinkedCount} unactioned learning(s) to this task.`);
18062
+ }
18063
+ if (result.autoTriagedCount) {
18064
+ lines.push("", `\u{1F516} Auto-triaged ${result.autoTriagedCount} discovered issue(s) to Backlog.`);
18065
+ }
18066
+ if (result.reportWriteVerified === false) {
18067
+ lines.push("", "\u26A0\uFE0F Build report write could not be verified \u2014 the report may not have been persisted. Run `build_list` to check, and resubmit if missing.");
18068
+ }
17502
18069
  if (result.docWarning) {
17503
18070
  lines.push("", `\u{1F4C4} ${result.docWarning}`);
17504
18071
  }
@@ -17638,7 +18205,7 @@ function resolveCurrentPhase(phases) {
17638
18205
  const sorted = [...phases].sort((a, b2) => a.order - b2.order);
17639
18206
  return sorted[0].label;
17640
18207
  }
17641
- var STOP_WORDS = /* @__PURE__ */ new Set([
18208
+ var STOP_WORDS2 = /* @__PURE__ */ new Set([
17642
18209
  "a",
17643
18210
  "an",
17644
18211
  "the",
@@ -17732,7 +18299,7 @@ var STOP_WORDS = /* @__PURE__ */ new Set([
17732
18299
  ]);
17733
18300
  function extractKeywords(text) {
17734
18301
  return new Set(
17735
- text.toLowerCase().replace(/[^a-z0-9\s]/g, " ").split(/\s+/).filter((w) => w.length > 2 && !STOP_WORDS.has(w))
18302
+ text.toLowerCase().replace(/[^a-z0-9\s]/g, " ").split(/\s+/).filter((w) => w.length > 2 && !STOP_WORDS2.has(w))
17736
18303
  );
17737
18304
  }
17738
18305
  async function findSimilarTasks(adapter2, ideaTitle) {
@@ -17803,9 +18370,10 @@ ${lines.join("\n")}
17803
18370
  const VALID_COMPLEXITIES2 = /* @__PURE__ */ new Set(["XS", "Small", "Medium", "Large", "XL"]);
17804
18371
  const priority = input.priority && VALID_PRIORITIES2.has(input.priority) ? input.priority : "P2 Medium";
17805
18372
  const complexity = input.complexity && VALID_COMPLEXITIES2.has(input.complexity) ? input.complexity : "Small";
17806
- const VALID_TYPES = /* @__PURE__ */ new Set(["task", "bug", "research", "idea", "spike"]);
18373
+ const VALID_TYPES = /* @__PURE__ */ new Set(["task", "bug", "research", "idea", "spike", "discovery"]);
17807
18374
  let taskTitle = input.text;
17808
18375
  let taskType = "idea";
18376
+ let typeInferred = false;
17809
18377
  if (input.type && VALID_TYPES.has(input.type)) {
17810
18378
  taskType = input.type;
17811
18379
  } else {
@@ -17820,6 +18388,20 @@ ${lines.join("\n")}
17820
18388
  taskType = PREFIX_MAP[key];
17821
18389
  taskTitle = input.text.slice(prefixMatch[0].length);
17822
18390
  }
18391
+ } else {
18392
+ const searchText = `${input.text} ${input.notes ?? ""}`.toLowerCase();
18393
+ if (/\b(bug|fix|broken|crash|error)\b/.test(searchText)) {
18394
+ taskType = "bug";
18395
+ } else if (/\b(research|investigate|explore|spike)\b/.test(searchText)) {
18396
+ taskType = "research";
18397
+ } else if (/\b(performance|optimize|speed|latency)\b/.test(searchText)) {
18398
+ taskType = "task";
18399
+ } else if (/\b(verify|confirm)\b/.test(searchText)) {
18400
+ taskType = "spike";
18401
+ } else {
18402
+ taskType = "task";
18403
+ }
18404
+ typeInferred = true;
17823
18405
  }
17824
18406
  }
17825
18407
  const task = await adapter2.createTask({
@@ -17839,7 +18421,8 @@ ${lines.join("\n")}
17839
18421
  taskType,
17840
18422
  maturity: "raw",
17841
18423
  docRef: input.docRef,
17842
- source: "llm"
18424
+ source: "llm",
18425
+ opportunity: input.opportunity
17843
18426
  });
17844
18427
  if (input.notes && adapter2.updateCycleLearningActionRef) {
17845
18428
  const learningRefs = input.notes.match(/learning:([a-f0-9-]+)/gi);
@@ -17865,7 +18448,27 @@ ${lines.join("\n")}
17865
18448
  }
17866
18449
  }
17867
18450
  }
17868
- return { routing: "task", task, message: `${task.id}: "${task.title}" \u2014 added to backlog` };
18451
+ if (adapter2.getDogfoodLog && adapter2.updateDogfoodEntryStatus) {
18452
+ try {
18453
+ const dogfoodLog = await adapter2.getDogfoodLog(50);
18454
+ const unlinked = dogfoodLog.filter((e) => e.status === "observed" && !e.linkedTaskId);
18455
+ if (unlinked.length > 0) {
18456
+ const taskText = `${task.title} ${input.notes ?? ""}`.toLowerCase();
18457
+ const taskKeywords = taskText.match(/\b[a-z]{4,}\b/g) ?? [];
18458
+ const taskKeywordSet = new Set(taskKeywords);
18459
+ for (const entry of unlinked) {
18460
+ const entryKeywords = entry.content.toLowerCase().match(/\b[a-z]{4,}\b/g) ?? [];
18461
+ const overlap = entryKeywords.filter((w) => taskKeywordSet.has(w));
18462
+ if (overlap.length >= 2) {
18463
+ await adapter2.updateDogfoodEntryStatus(entry.id, "backlog-created", task.id);
18464
+ }
18465
+ }
18466
+ }
18467
+ } catch {
18468
+ }
18469
+ }
18470
+ const typeNote = typeInferred ? ` [type: ${taskType} \u2014 inferred from text]` : "";
18471
+ return { routing: "task", task, message: `${task.id}: "${task.title}" \u2014 added to backlog${typeNote}` };
17869
18472
  }
17870
18473
  var CANVAS_SECTION_LABELS = {
17871
18474
  landscape: "Landscape References",
@@ -17905,6 +18508,7 @@ async function routeToDiscovery(adapter2, section, input) {
17905
18508
  // src/tools/idea.ts
17906
18509
  var ideaTool = {
17907
18510
  name: "idea",
18511
+ annotations: { readOnlyHint: false, destructiveHint: false },
17908
18512
  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.",
17909
18513
  inputSchema: {
17910
18514
  type: "object",
@@ -17915,7 +18519,7 @@ var ideaTool = {
17915
18519
  },
17916
18520
  notes: {
17917
18521
  type: "string",
17918
- 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.'
18522
+ 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. TIP: If this idea addresses a known cycle learning, include "learning:<uuid>" in the notes (e.g. "learning:abc12345-..."). This links the idea to the learning entry and marks it actioned in the pipeline. Example: "Addresses recurring friction. learning:3f9a1c2e-..."'
17919
18523
  },
17920
18524
  module: {
17921
18525
  type: "string",
@@ -17949,12 +18553,16 @@ var ideaTool = {
17949
18553
  },
17950
18554
  type: {
17951
18555
  type: "string",
17952
- enum: ["task", "bug", "research", "spike"],
17953
- description: 'Task type. Defaults to "task". Use "bug" for defects, "research" for investigation tasks, "spike" for time-boxed experiments. The planner uses this to generate type-specific BUILD HANDOFFs.'
18556
+ enum: ["task", "bug", "research", "spike", "discovery"],
18557
+ description: 'Task type. Defaults to "task". Use "bug" for defects, "research" for investigation tasks, "spike" for time-boxed experiments, "discovery" for issues found during a build that need their own task. The planner uses this to generate type-specific BUILD HANDOFFs.'
17954
18558
  },
17955
18559
  doc_ref: {
17956
18560
  type: "string",
17957
18561
  description: 'Path to a reference document (e.g. "docs/research/foo.md"). Stored as a structured field \u2014 replaces the fragile "Reference:" line in notes.'
18562
+ },
18563
+ opportunity: {
18564
+ type: "string",
18565
+ description: "What user problem does this solve? Auto-fill from problem context in notes when submitting ideas that describe a user pain point. The planner uses this to cluster backlog tasks by opportunity."
17958
18566
  }
17959
18567
  },
17960
18568
  required: ["text"]
@@ -17983,7 +18591,8 @@ async function handleIdea(adapter2, config2, args) {
17983
18591
  discovery: args.discovery === true,
17984
18592
  force: args.force === true,
17985
18593
  docRef: args.doc_ref?.trim(),
17986
- type: args.type
18594
+ type: args.type,
18595
+ opportunity: args.opportunity?.trim()
17987
18596
  };
17988
18597
  const useGit = isGitAvailable() && isGitRepo(config2.projectRoot);
17989
18598
  const currentBranch = useGit ? getCurrentBranch(config2.projectRoot) : null;
@@ -18090,6 +18699,7 @@ function collectDiagnostics(config2) {
18090
18699
  var bugTool = {
18091
18700
  name: "bug",
18092
18701
  description: "Report a bug. Two modes: (1) Default \u2014 creates a Backlog task with severity-based priority for the project board. (2) With report=true \u2014 submits a diagnostic bug report with system info for cross-project visibility (external user issue reporting). Does not call the Anthropic API.",
18702
+ annotations: { readOnlyHint: false, destructiveHint: false },
18093
18703
  inputSchema: {
18094
18704
  type: "object",
18095
18705
  properties: {
@@ -18270,6 +18880,7 @@ var VALID_EFFORTS = ["XS", "S", "M", "L", "XL"];
18270
18880
  var adHocTool = {
18271
18881
  name: "ad_hoc",
18272
18882
  description: "Record work done outside the normal cycle. Creates a Done task with a lightweight build report, or associates work with an existing task if task_id is provided (without changing task status \u2014 use build_execute for status transitions). Use for quick fixes, bug patches, or ad-hoc changes. Does not call the Anthropic API.",
18883
+ annotations: { readOnlyHint: false, destructiveHint: false },
18273
18884
  inputSchema: {
18274
18885
  type: "object",
18275
18886
  properties: {
@@ -18480,9 +19091,9 @@ async function prepareReconcile(adapter2) {
18480
19091
  }
18481
19092
  return lines.join("\n");
18482
19093
  }
18483
- var STOP_WORDS2 = /* @__PURE__ */ new Set(["the", "a", "an", "and", "or", "for", "in", "on", "to", "of", "is", "with", "from", "by", "vs", "not", "no", "do"]);
19094
+ var STOP_WORDS3 = /* @__PURE__ */ new Set(["the", "a", "an", "and", "or", "for", "in", "on", "to", "of", "is", "with", "from", "by", "vs", "not", "no", "do"]);
18484
19095
  function tokenize(s) {
18485
- return s.toLowerCase().replace(/[^a-z0-9\s-]/g, "").split(/\s+/).filter((w) => w.length > 2 && !STOP_WORDS2.has(w));
19096
+ return s.toLowerCase().replace(/[^a-z0-9\s-]/g, "").split(/\s+/).filter((w) => w.length > 2 && !STOP_WORDS3.has(w));
18486
19097
  }
18487
19098
  function titleKeywords(title) {
18488
19099
  return new Set(tokenize(title));
@@ -18659,6 +19270,7 @@ async function applyRetriage(adapter2, retriages) {
18659
19270
  var boardReconcileTool = {
18660
19271
  name: "board_reconcile",
18661
19272
  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.',
19273
+ annotations: { readOnlyHint: false, destructiveHint: false },
18662
19274
  inputSchema: {
18663
19275
  type: "object",
18664
19276
  properties: {
@@ -18989,451 +19601,118 @@ Assess each task above and produce your retriage output. Then call \`board_recon
18989
19601
  return errorResponse(`Unknown mode: ${mode}. Use "prepare", "apply", "retriage-prepare", or "retriage-apply".`);
18990
19602
  }
18991
19603
 
18992
- // src/services/health.ts
18993
- function computeHealthScore(cycleNumber, snapshots, activeTasks, decisionUsage) {
18994
- if (cycleNumber < 3) return null;
18995
- const scores = [];
18996
- const recentSnaps = snapshots.slice(-3);
18997
- const baselineSnaps = snapshots.slice(-10);
18998
- if (recentSnaps.length > 0 && baselineSnaps.length > 0) {
18999
- const avg = (snaps) => snaps.reduce((s, sn) => s + (sn.velocity[0]?.effortPoints ?? 0), 0) / snaps.length;
19000
- const recentAvg = avg(recentSnaps);
19001
- const baselineAvg = avg(baselineSnaps);
19002
- const velocityScore = baselineAvg > 0 ? Math.min(100, Math.round(recentAvg / baselineAvg * 100)) : 50;
19003
- scores.push({ name: "Velocity", score: velocityScore, weight: 0.25 });
19004
- } else {
19005
- scores.push({ name: "Velocity", score: 50, weight: 0.25 });
19006
- }
19007
- if (recentSnaps.length > 0) {
19008
- const avgMatchRate = recentSnaps.reduce((s, sn) => s + (sn.accuracy[0]?.matchRate ?? 0), 0) / recentSnaps.length;
19009
- scores.push({ name: "Estimation accuracy", score: Math.round(avgMatchRate), weight: 0.25 });
19010
- } else {
19011
- scores.push({ name: "Estimation accuracy", score: 50, weight: 0.25 });
19012
- }
19013
- const inReviewCount = activeTasks.filter((t) => t.status === "In Review").length;
19014
- const reviewScore = inReviewCount === 0 ? 100 : inReviewCount <= 2 ? 60 : 20;
19015
- scores.push({ name: "Review throughput", score: reviewScore, weight: 0.2 });
19016
- const backlogTasks = activeTasks.filter((t) => t.status === "Backlog");
19017
- if (backlogTasks.length > 0) {
19018
- const criticalCount = backlogTasks.filter(
19019
- (t) => t.priority === "P0 Critical" || t.priority === "P1 High"
19020
- ).length;
19021
- const criticalRatio = criticalCount / backlogTasks.length;
19022
- const backlogScore = criticalRatio > 0.5 ? 40 : criticalRatio > 0.3 ? 70 : 90;
19023
- scores.push({ name: "Backlog health", score: backlogScore, weight: 0.15 });
19024
- } else {
19025
- scores.push({ name: "Backlog health", score: 80, weight: 0.15 });
19604
+ // src/services/release.ts
19605
+ init_git();
19606
+ import { writeFile as writeFile3 } from "fs/promises";
19607
+ import { join as join6 } from "path";
19608
+ var INITIAL_RELEASE_NOTES = `# Changelog
19609
+
19610
+ ## v0.1.0-alpha \u2014 Initial Release
19611
+
19612
+ PAPI MCP Server \u2014 the AI-powered project planning framework.
19613
+
19614
+ ### Commands
19615
+ - **setup** \u2014 Initialise a new PAPI project with Product Brief generation
19616
+ - **plan** \u2014 Run cycle planning with embedded BUILD HANDOFFs (Bootstrap + Full modes)
19617
+ - **build_list / build_describe / build_execute / build_cancel** \u2014 Manage build tasks
19618
+ - **board_view / board_deprioritise / board_archive** \u2014 View and manage the Board
19619
+ - **strategy_review / strategy_change** \u2014 Run Strategy Reviews and apply strategic changes
19620
+ - **review_list / review_submit** \u2014 Human review loop for handoffs and builds
19621
+ - **idea** \u2014 Capture ideas as backlog tasks for future triage
19622
+ - **health** \u2014 Cycle Health Summary dashboard
19623
+ - **release** \u2014 Cut versioned releases with git tags and changelogs
19624
+
19625
+ ### Features
19626
+ - .md file persistence in .papi/ directory
19627
+ - Bootstrap + Full planning modes with Anthropic API integration
19628
+ - Embedded BUILD HANDOFFs with dual write-back build reports
19629
+ - Auto-commit and auto-PR after builds
19630
+ - Board corrections and Active Decision persistence
19631
+ - Single-purpose MCP tools for optimal LLM tool selection
19632
+ - Consistent error handling across all tools
19633
+ `;
19634
+ function generateChangelog(version, commits) {
19635
+ const date = (/* @__PURE__ */ new Date()).toISOString();
19636
+ const commitList = commits.map((c) => `- ${c}`).join("\n");
19637
+ return `# Changelog
19638
+
19639
+ ## ${version} \u2014 ${date}
19640
+
19641
+ ${commitList}
19642
+ `;
19643
+ }
19644
+ async function createRelease(config2, branch, version, adapter2) {
19645
+ if (!isGitAvailable()) {
19646
+ throw new Error("git is not available.");
19026
19647
  }
19027
- if (decisionUsage.length > 0) {
19028
- const staleCount = decisionUsage.filter((u) => u.cyclesSinceLastReference >= 10).length;
19029
- const freshRatio = (decisionUsage.length - staleCount) / decisionUsage.length;
19030
- scores.push({ name: "AD freshness", score: Math.round(freshRatio * 100), weight: 0.15 });
19031
- } else {
19032
- scores.push({ name: "AD freshness", score: 70, weight: 0.15 });
19648
+ if (!isGitRepo(config2.projectRoot)) {
19649
+ throw new Error("not a git repository.");
19033
19650
  }
19034
- const totalScore = Math.round(scores.reduce((sum, s) => sum + s.score * s.weight, 0));
19035
- const status = totalScore >= 70 ? "GREEN" : totalScore >= 50 ? "AMBER" : "RED";
19036
- const worst = scores.reduce((min, s) => s.score < min.score ? s : min, scores[0]);
19037
- const reason = status === "GREEN" ? "All components healthy" : `${worst.name} below target (${worst.score}/100)`;
19038
- return { score: totalScore, status, reason };
19039
- }
19040
- function countByStatus(tasks) {
19041
- const counts = /* @__PURE__ */ new Map();
19042
- for (const task of tasks) {
19043
- counts.set(task.status, (counts.get(task.status) ?? 0) + 1);
19651
+ if (hasUncommittedChanges(config2.projectRoot, AUTO_WRITTEN_PATHS)) {
19652
+ throw new Error("working directory has uncommitted changes. Commit or stash them before releasing.");
19044
19653
  }
19045
- return counts;
19046
- }
19047
- async function getHealthSummary(adapter2) {
19048
- const health = await adapter2.getCycleHealth();
19049
- const activeTasks = await adapter2.queryBoard({
19050
- status: ["Backlog", "In Cycle", "Ready", "In Progress", "In Review", "Blocked"]
19051
- });
19052
- const logEntries = await adapter2.getCycleLog(3);
19053
- const cycleNumber = health.totalCycles;
19054
- const cyclesSinceReview = health.cyclesSinceLastStrategyReview;
19055
- const reviewDue = health.strategyReviewDue;
19056
- const reviewGateBlocking = cyclesSinceReview >= 5;
19057
- const reviewWarning = reviewGateBlocking ? `\u26A0\uFE0F GATE \u2014 ${cyclesSinceReview} cycles since last Strategy Review. \`plan\` is blocked until \`strategy_review\` runs (or \`force: true\`).` : `\u2713 On track \u2014 ${cyclesSinceReview} cycle(s) since last review. Next due: ${reviewDue}`;
19058
- const deferredCount = activeTasks.filter((t) => t.status === "Deferred").length;
19059
- const nonDeferredTasks = activeTasks.filter((t) => t.status !== "Deferred");
19060
- const statusCounts = countByStatus(nonDeferredTasks);
19061
- let boardSummary;
19062
- if (nonDeferredTasks.length === 0 && deferredCount === 0) {
19063
- boardSummary = "0 tasks \u2014 board may need reloading";
19064
- } else {
19065
- const parts = [];
19066
- for (const [status, count] of statusCounts) {
19067
- parts.push(`${count} ${status}`);
19068
- }
19069
- boardSummary = `${nonDeferredTasks.length} active tasks \u2014 ${parts.join(", ")}`;
19070
- if (deferredCount > 0) {
19071
- boardSummary += ` + ${deferredCount} deferred`;
19654
+ const warnings = [];
19655
+ if (adapter2) {
19656
+ try {
19657
+ const versionMatch = version.match(/^v0\.(\d+)\./);
19658
+ const currentCycle = versionMatch ? parseInt(versionMatch[1], 10) : 0;
19659
+ if (currentCycle > 0) {
19660
+ await adapter2.createCycle({
19661
+ id: `cycle-${currentCycle}`,
19662
+ number: currentCycle,
19663
+ status: "complete",
19664
+ startDate: (/* @__PURE__ */ new Date()).toISOString(),
19665
+ endDate: (/* @__PURE__ */ new Date()).toISOString(),
19666
+ goals: [],
19667
+ boardHealth: "",
19668
+ taskIds: []
19669
+ });
19670
+ }
19671
+ } catch (err) {
19672
+ const msg = `createCycle (mark complete) failed: ${err instanceof Error ? err.message : String(err)}`;
19673
+ console.error(`[release] ${msg}`);
19674
+ warnings.push(msg);
19072
19675
  }
19073
19676
  }
19074
- const inProgressTasks = activeTasks.filter((t) => t.status === "In Progress");
19075
- const staleTasks = inProgressTasks.length > 0 ? `${inProgressTasks.length} task(s) In Progress: ${inProgressTasks.map((t) => t.id).join(", ")}` : "No tasks currently In Progress";
19076
- const inReviewTasks = activeTasks.filter((t) => t.status === "In Review");
19077
- const inReviewSummary = inReviewTasks.length > 0 ? `${inReviewTasks.length} task(s) ready for sign-off: ${inReviewTasks.map((t) => t.id).join(", ")}` : "No tasks waiting for sign-off";
19078
- let carryForward = "None found";
19079
- if (logEntries.length > 0) {
19080
- const latest = logEntries[0];
19081
- if (latest.carryForward) {
19082
- carryForward = latest.carryForward;
19083
- } else {
19084
- carryForward = `No carry-forward in Cycle ${latest.cycleNumber}`;
19085
- }
19677
+ const checkout = checkoutBranch(config2.projectRoot, branch);
19678
+ if (!checkout.success) {
19679
+ throw new Error(checkout.message);
19086
19680
  }
19087
- let recommendedMode;
19088
- const reasons = [];
19089
- if (reviewGateBlocking) {
19090
- reasons.push(`Strategy Review overdue (${cyclesSinceReview} cycles)`);
19681
+ if (hasRemote(config2.projectRoot)) {
19682
+ const pull = gitPull(config2.projectRoot);
19683
+ if (!pull.success) {
19684
+ warnings.push(`git pull failed: ${pull.message}. Run manually.`);
19685
+ }
19091
19686
  }
19092
- if (activeTasks.length === 0) {
19093
- reasons.push("Board is empty \u2014 needs task reload/triage");
19687
+ if (tagExists(config2.projectRoot, version)) {
19688
+ throw new Error(`tag "${version}" already exists. Use a different version.`);
19094
19689
  }
19095
- const unbuiltCycleTasks = activeTasks.filter(
19096
- (t) => t.cycle === cycleNumber && (t.status === "In Cycle" || t.status === "Ready")
19097
- );
19098
- const inProgressCycleTasks = activeTasks.filter(
19099
- (t) => t.cycle === cycleNumber && t.status === "In Progress"
19100
- );
19101
- const inReviewCycleTasks = activeTasks.filter(
19102
- (t) => t.cycle === cycleNumber && t.status === "In Review"
19103
- );
19104
- if (reasons.length > 0) {
19105
- recommendedMode = `**Full** \u2014 ${reasons.join("; ")}`;
19106
- } else if (unbuiltCycleTasks.length > 0) {
19107
- recommendedMode = `**Build** \u2014 ${unbuiltCycleTasks.length} cycle task(s) not yet started`;
19108
- } else if (inProgressCycleTasks.length > 0) {
19109
- recommendedMode = `**Build** \u2014 ${inProgressCycleTasks.length} task(s) in progress`;
19110
- } else if (inReviewCycleTasks.length > 0) {
19111
- recommendedMode = `**Review** \u2014 ${inReviewCycleTasks.length} task(s) awaiting review`;
19690
+ const latestTag = getLatestTag(config2.projectRoot);
19691
+ let changelogContent;
19692
+ if (!latestTag) {
19693
+ changelogContent = INITIAL_RELEASE_NOTES.replace("v0.1.0-alpha", version);
19112
19694
  } else {
19113
- recommendedMode = `**Full** \u2014 ready for next cycle`;
19695
+ const commits = getCommitsSinceTag(config2.projectRoot, latestTag);
19696
+ changelogContent = generateChangelog(version, commits);
19114
19697
  }
19115
- let metricsSection;
19116
- let derivedMetricsSection = "";
19117
- let snapshots = [];
19118
- try {
19119
- try {
19120
- const reports = await adapter2.getRecentBuildReports(50);
19121
- snapshots = computeSnapshotsFromBuildReports(reports);
19122
- } catch {
19123
- }
19124
- metricsSection = formatCycleMetrics(snapshots);
19125
- derivedMetricsSection = formatDerivedMetrics(snapshots, activeTasks);
19126
- } catch (_err) {
19127
- metricsSection = "Could not read methodology metrics.";
19698
+ const changelogPath = join6(config2.projectRoot, "CHANGELOG.md");
19699
+ await writeFile3(changelogPath, changelogContent, "utf-8");
19700
+ const commitResult = stageAllAndCommit(config2.projectRoot, `release: ${version}`);
19701
+ const commitNote = commitResult.committed ? `Committed CHANGELOG.md.` : `CHANGELOG.md: ${commitResult.message}`;
19702
+ const tagResult = createTag(config2.projectRoot, version, `Release ${version}`);
19703
+ if (!tagResult.success) {
19704
+ throw new Error(tagResult.message);
19128
19705
  }
19129
- try {
19130
- const recentReports = await adapter2.getRecentBuildReports(50);
19131
- if (recentReports.length > 0) {
19132
- const taskCounts = /* @__PURE__ */ new Map();
19133
- for (const r of recentReports) {
19134
- taskCounts.set(r.taskId, (taskCounts.get(r.taskId) ?? 0) + 1);
19135
- }
19136
- const iterCounts = [...taskCounts.values()];
19137
- const avgIter = iterCounts.reduce((s, c) => s + c, 0) / iterCounts.length;
19138
- const multiIterTasks = iterCounts.filter((c) => c > 1).length;
19139
- if (avgIter > 1 || multiIterTasks > 0) {
19140
- derivedMetricsSection += `
19141
-
19142
- **Rework**
19143
- - Average iterations: ${avgIter.toFixed(1)} (${multiIterTasks} task${multiIterTasks !== 1 ? "s" : ""} with pushbacks)`;
19144
- }
19145
- }
19146
- } catch {
19147
- }
19148
- const costSection = "Disabled \u2014 local MCP, no API costs.";
19149
- let decisionUsageSection = "";
19150
- let decisionUsageEntries = [];
19151
- try {
19152
- const usage = await adapter2.getDecisionUsage(cycleNumber);
19153
- decisionUsageEntries = usage;
19154
- if (usage.length > 0) {
19155
- const stale = usage.filter((u) => u.cyclesSinceLastReference >= 5);
19156
- if (stale.length > 0) {
19157
- const lines = stale.map(
19158
- (u) => `- ${u.decisionId}: last referenced Cycle ${u.lastReferencedCycle} (${u.cyclesSinceLastReference} cycles ago)`
19159
- );
19160
- decisionUsageSection = `**Stale ADs (5+ cycles unreferenced):**
19161
- ${lines.join("\n")}`;
19162
- } else {
19163
- decisionUsageSection = `All ${usage.length} tracked ADs referenced within last 5 cycles.`;
19164
- }
19165
- }
19166
- } catch {
19167
- }
19168
- let decisionLifecycleSection = "";
19169
- try {
19170
- const decisions = await adapter2.getActiveDecisions();
19171
- const lifecycleSummary = formatDecisionLifecycleSummary(decisions);
19172
- if (lifecycleSummary) {
19173
- decisionLifecycleSection = `**Lifecycle:** ${lifecycleSummary}`;
19174
- }
19175
- } catch {
19176
- }
19177
- const decisionScoresSection = "";
19178
- let contextUtilisationSection = "";
19179
- try {
19180
- const utilData = await adapter2.getContextUtilisation?.();
19181
- if (utilData && utilData.length > 0) {
19182
- const lines = utilData.filter((u) => u.cycleNumber === cycleNumber).map((u) => `- ${u.tool}: ${(u.avgUtilisation * 100).toFixed(0)}% utilisation (${(u.avgContextBytes / 1024).toFixed(1)}KB avg context)`);
19183
- if (lines.length > 0) {
19184
- contextUtilisationSection = `**Current cycle:**
19185
- ${lines.join("\n")}`;
19186
- }
19187
- }
19188
- } catch {
19189
- }
19190
- let northStarSection = "";
19191
- try {
19192
- const staleness = await adapter2.getNorthStarStaleness?.();
19193
- if (staleness) {
19194
- const cycleGap = cycleNumber - staleness.setCycle;
19195
- const daysSinceSet = Math.floor((Date.now() - new Date(staleness.setAt).getTime()) / (1e3 * 60 * 60 * 24));
19196
- northStarSection = `\u2713 North Star set Cycle ${staleness.setCycle} (${cycleGap} cycles, ${daysSinceSet} days ago)`;
19197
- } else {
19198
- const setAtCycle = await adapter2.getNorthStarSetCycle?.();
19199
- if (setAtCycle != null) {
19200
- northStarSection = `\u2713 North Star set Cycle ${setAtCycle}`;
19201
- } else if (adapter2.getCurrentNorthStar) {
19202
- const ns = await adapter2.getCurrentNorthStar();
19203
- northStarSection = ns ? "" : "\u26A0\uFE0F No North Star set \u2014 consider defining one";
19204
- }
19205
- }
19206
- } catch {
19207
- }
19208
- const healthResult = computeHealthScore(cycleNumber, snapshots, activeTasks, decisionUsageEntries);
19209
- return {
19210
- cycleNumber,
19211
- latestCycleStatus: health.latestCycleStatus,
19212
- connectionStatus: getConnectionStatus(),
19213
- reviewWarning,
19214
- boardSummary,
19215
- staleTasks,
19216
- inReviewSummary,
19217
- carryForward,
19218
- recommendedMode,
19219
- metricsSection,
19220
- derivedMetricsSection,
19221
- costSection,
19222
- decisionUsageSection,
19223
- decisionLifecycleSection,
19224
- decisionScoresSection,
19225
- contextUtilisationSection,
19226
- northStarSection,
19227
- healthScore: healthResult?.score ?? null,
19228
- healthStatus: healthResult?.status ?? null,
19229
- healthReason: healthResult?.reason ?? null
19230
- };
19231
- }
19232
-
19233
- // src/tools/health.ts
19234
- var healthTool = {
19235
- name: "health",
19236
- description: "Cycle Health Summary \u2014 shows current cycle number, Strategy Review cadence status (AD-5), board health (task counts by status, stale tasks), last carry-forward items, and recommended next mode. Read-only, does not modify any files.",
19237
- inputSchema: {
19238
- type: "object",
19239
- properties: {},
19240
- required: []
19241
- }
19242
- };
19243
- function formatHealthSummary(summary) {
19244
- const lines = [];
19245
- lines.push(`# Cycle ${summary.cycleNumber} \u2014 Health`);
19246
- lines.push("");
19247
- if (summary.connectionStatus !== "offline") {
19248
- const statusIcon = summary.connectionStatus === "connected" ? "\u2713" : "\u26A0\uFE0F";
19249
- const statusLabel = summary.connectionStatus === "connected" ? "Supabase connected" : "Supabase degraded \u2014 data may be stale. Check DATABASE_URL in .mcp.json";
19250
- lines.push(`**Connection:** ${statusIcon} ${statusLabel}`);
19251
- lines.push("");
19252
- }
19253
- lines.push(`> **Next action:** ${summary.recommendedMode}`);
19254
- lines.push("");
19255
- lines.push(`**Strategy Review:** ${summary.reviewWarning}`);
19256
- lines.push("");
19257
- lines.push(`## Board`);
19258
- lines.push(summary.boardSummary);
19259
- const hasInProgress = !summary.staleTasks.startsWith("No tasks");
19260
- const hasInReview = !summary.inReviewSummary.startsWith("No tasks");
19261
- if (hasInProgress || hasInReview) {
19262
- lines.push("");
19263
- if (hasInProgress) lines.push(`- **In Progress:** ${summary.staleTasks}`);
19264
- if (hasInReview) lines.push(`- **In Review:** ${summary.inReviewSummary}`);
19265
- }
19266
- lines.push("");
19267
- const hasCarryForward = summary.carryForward !== "None found" && !summary.carryForward.startsWith("No carry-forward");
19268
- if (hasCarryForward) {
19269
- lines.push(`## Carry-Forward`);
19270
- lines.push(summary.carryForward);
19271
- lines.push("");
19272
- }
19273
- const hasMetrics = summary.metricsSection !== "Could not read methodology metrics." && !summary.metricsSection.includes("undefined");
19274
- if (hasMetrics) {
19275
- lines.push(`## Trends`);
19276
- lines.push(summary.metricsSection);
19277
- lines.push("");
19278
- }
19279
- if (summary.derivedMetricsSection) {
19280
- lines.push(`## Insights`);
19281
- lines.push(summary.derivedMetricsSection);
19282
- lines.push("");
19283
- }
19284
- const hasCost = summary.costSection !== "No metrics data yet." && summary.costSection !== "Could not read metrics data.";
19285
- if (hasCost) {
19286
- lines.push(`## Cost`);
19287
- lines.push(summary.costSection);
19288
- lines.push("");
19289
- }
19290
- if (summary.northStarSection) {
19291
- lines.push(`## North Star`);
19292
- lines.push(summary.northStarSection);
19293
- lines.push("");
19294
- }
19295
- if (summary.decisionUsageSection) {
19296
- lines.push(`## Decision Usage`);
19297
- lines.push(summary.decisionUsageSection);
19298
- if (summary.decisionLifecycleSection) {
19299
- lines.push(summary.decisionLifecycleSection);
19300
- }
19301
- lines.push("");
19302
- }
19303
- if (summary.contextUtilisationSection) {
19304
- lines.push(`## Context Utilisation`);
19305
- lines.push(summary.contextUtilisationSection);
19306
- lines.push("");
19307
- }
19308
- if (summary.decisionScoresSection) {
19309
- lines.push(`## Decision Scores`);
19310
- lines.push(summary.decisionScoresSection);
19311
- lines.push("");
19312
- }
19313
- return lines.join("\n").trimEnd();
19314
- }
19315
- async function handleHealth(adapter2) {
19316
- try {
19317
- const summary = await getHealthSummary(adapter2);
19318
- return textResponse(formatHealthSummary(summary));
19319
- } catch (err) {
19320
- const message = err instanceof Error ? err.message : String(err);
19321
- return errorResponse(`Could not read cycle health. Run \`setup\` first to initialise your project. (${message})`);
19322
- }
19323
- }
19324
-
19325
- // src/services/release.ts
19326
- init_git();
19327
- import { writeFile as writeFile3 } from "fs/promises";
19328
- import { join as join6 } from "path";
19329
- var INITIAL_RELEASE_NOTES = `# Changelog
19330
-
19331
- ## v0.1.0-alpha \u2014 Initial Release
19332
-
19333
- PAPI MCP Server \u2014 the AI-powered project planning framework.
19334
-
19335
- ### Commands
19336
- - **setup** \u2014 Initialise a new PAPI project with Product Brief generation
19337
- - **plan** \u2014 Run cycle planning with embedded BUILD HANDOFFs (Bootstrap + Full modes)
19338
- - **build_list / build_describe / build_execute / build_cancel** \u2014 Manage build tasks
19339
- - **board_view / board_deprioritise / board_archive** \u2014 View and manage the Board
19340
- - **strategy_review / strategy_change** \u2014 Run Strategy Reviews and apply strategic changes
19341
- - **review_list / review_submit** \u2014 Human review loop for handoffs and builds
19342
- - **idea** \u2014 Capture ideas as backlog tasks for future triage
19343
- - **health** \u2014 Cycle Health Summary dashboard
19344
- - **release** \u2014 Cut versioned releases with git tags and changelogs
19345
-
19346
- ### Features
19347
- - .md file persistence in .papi/ directory
19348
- - Bootstrap + Full planning modes with Anthropic API integration
19349
- - Embedded BUILD HANDOFFs with dual write-back build reports
19350
- - Auto-commit and auto-PR after builds
19351
- - Board corrections and Active Decision persistence
19352
- - Single-purpose MCP tools for optimal LLM tool selection
19353
- - Consistent error handling across all tools
19354
- `;
19355
- function generateChangelog(version, commits) {
19356
- const date = (/* @__PURE__ */ new Date()).toISOString();
19357
- const commitList = commits.map((c) => `- ${c}`).join("\n");
19358
- return `# Changelog
19359
-
19360
- ## ${version} \u2014 ${date}
19361
-
19362
- ${commitList}
19363
- `;
19364
- }
19365
- async function createRelease(config2, branch, version, adapter2) {
19366
- if (!isGitAvailable()) {
19367
- throw new Error("git is not available.");
19368
- }
19369
- if (!isGitRepo(config2.projectRoot)) {
19370
- throw new Error("not a git repository.");
19371
- }
19372
- if (hasUncommittedChanges(config2.projectRoot, AUTO_WRITTEN_PATHS)) {
19373
- throw new Error("working directory has uncommitted changes. Commit or stash them before releasing.");
19374
- }
19375
- const warnings = [];
19376
- if (adapter2) {
19377
- try {
19378
- const versionMatch = version.match(/^v0\.(\d+)\./);
19379
- const currentCycle = versionMatch ? parseInt(versionMatch[1], 10) : 0;
19380
- if (currentCycle > 0) {
19381
- await adapter2.createCycle({
19382
- id: `cycle-${currentCycle}`,
19383
- number: currentCycle,
19384
- status: "complete",
19385
- startDate: (/* @__PURE__ */ new Date()).toISOString(),
19386
- endDate: (/* @__PURE__ */ new Date()).toISOString(),
19387
- goals: [],
19388
- boardHealth: "",
19389
- taskIds: []
19390
- });
19391
- }
19392
- } catch (err) {
19393
- const msg = `createCycle (mark complete) failed: ${err instanceof Error ? err.message : String(err)}`;
19394
- console.error(`[release] ${msg}`);
19395
- warnings.push(msg);
19396
- }
19397
- }
19398
- const checkout = checkoutBranch(config2.projectRoot, branch);
19399
- if (!checkout.success) {
19400
- throw new Error(checkout.message);
19401
- }
19402
- if (hasRemote(config2.projectRoot)) {
19403
- const pull = gitPull(config2.projectRoot);
19404
- if (!pull.success) {
19405
- warnings.push(`git pull failed: ${pull.message}. Run manually.`);
19406
- }
19407
- }
19408
- if (tagExists(config2.projectRoot, version)) {
19409
- throw new Error(`tag "${version}" already exists. Use a different version.`);
19410
- }
19411
- const latestTag = getLatestTag(config2.projectRoot);
19412
- let changelogContent;
19413
- if (!latestTag) {
19414
- changelogContent = INITIAL_RELEASE_NOTES.replace("v0.1.0-alpha", version);
19415
- } else {
19416
- const commits = getCommitsSinceTag(config2.projectRoot, latestTag);
19417
- changelogContent = generateChangelog(version, commits);
19418
- }
19419
- const changelogPath = join6(config2.projectRoot, "CHANGELOG.md");
19420
- await writeFile3(changelogPath, changelogContent, "utf-8");
19421
- const commitResult = stageAllAndCommit(config2.projectRoot, `release: ${version}`);
19422
- const commitNote = commitResult.committed ? `Committed CHANGELOG.md.` : `CHANGELOG.md: ${commitResult.message}`;
19423
- const tagResult = createTag(config2.projectRoot, version, `Release ${version}`);
19424
- if (!tagResult.success) {
19425
- throw new Error(tagResult.message);
19426
- }
19427
- const pushNotes = [];
19428
- if (hasRemote(config2.projectRoot)) {
19429
- const branchPush = gitPush(config2.projectRoot, branch);
19430
- pushNotes.push(branchPush.success ? `Pushed '${branch}' to origin.` : `Push branch failed: ${branchPush.message}`);
19431
- if (!branchPush.success) warnings.push(branchPush.message);
19432
- const tagPush = gitPush(config2.projectRoot, version);
19433
- pushNotes.push(tagPush.success ? `Pushed tag '${version}' to origin.` : `Push tag failed: ${tagPush.message}`);
19434
- if (!tagPush.success) warnings.push(tagPush.message);
19435
- } else {
19436
- pushNotes.push("Push: skipped (no remote).");
19706
+ const pushNotes = [];
19707
+ if (hasRemote(config2.projectRoot)) {
19708
+ const branchPush = gitPush(config2.projectRoot, branch);
19709
+ pushNotes.push(branchPush.success ? `Pushed '${branch}' to origin.` : `Push branch failed: ${branchPush.message}`);
19710
+ if (!branchPush.success) warnings.push(branchPush.message);
19711
+ const tagPush = gitPush(config2.projectRoot, version);
19712
+ pushNotes.push(tagPush.success ? `Pushed tag '${version}' to origin.` : `Push tag failed: ${tagPush.message}`);
19713
+ if (!tagPush.success) warnings.push(tagPush.message);
19714
+ } else {
19715
+ pushNotes.push("Push: skipped (no remote).");
19437
19716
  }
19438
19717
  return {
19439
19718
  version,
@@ -19449,6 +19728,7 @@ async function createRelease(config2, branch, version, adapter2) {
19449
19728
  var releaseTool = {
19450
19729
  name: "release",
19451
19730
  description: "Cut a versioned release \u2014 creates a git tag, generates CHANGELOG.md, and pushes to remote.",
19731
+ annotations: { readOnlyHint: false, destructiveHint: true },
19452
19732
  inputSchema: {
19453
19733
  type: "object",
19454
19734
  properties: {
@@ -19459,6 +19739,27 @@ var releaseTool = {
19459
19739
  version: {
19460
19740
  type: "string",
19461
19741
  description: 'The version tag to create (e.g. "v0.1.0-alpha"). Must start with "v".'
19742
+ },
19743
+ observations: {
19744
+ type: "array",
19745
+ description: "Optional dogfood observations from this cycle to persist to the DB. Each entry records friction, methodology signals, or commercial insights.",
19746
+ items: {
19747
+ type: "object",
19748
+ properties: {
19749
+ content: { type: "string", description: "The observation text." },
19750
+ category: {
19751
+ type: "string",
19752
+ enum: ["friction", "methodology", "signal", "commercial"],
19753
+ description: "Observation category."
19754
+ },
19755
+ severity: {
19756
+ type: "string",
19757
+ enum: ["P0", "P1", "P2", "P3"],
19758
+ description: "Optional severity for friction/signal observations."
19759
+ }
19760
+ },
19761
+ required: ["content", "category"]
19762
+ }
19462
19763
  }
19463
19764
  },
19464
19765
  required: ["branch", "version"]
@@ -19467,6 +19768,7 @@ var releaseTool = {
19467
19768
  async function handleRelease(adapter2, config2, args) {
19468
19769
  const branch = args.branch;
19469
19770
  const version = args.version;
19771
+ const rawObservations = args.observations;
19470
19772
  if (!branch || !version) {
19471
19773
  return errorResponse('both branch and version are required. Example: release branch="main" version="v0.1.0-alpha"');
19472
19774
  }
@@ -19504,6 +19806,23 @@ async function handleRelease(adapter2, config2, args) {
19504
19806
  }
19505
19807
  } catch {
19506
19808
  }
19809
+ if (rawObservations && rawObservations.length > 0 && adapter2.writeDogfoodEntries) {
19810
+ try {
19811
+ const cycleMatch = version.match(/^v0\.(\d+)\./);
19812
+ const cycleNum = cycleMatch ? parseInt(cycleMatch[1], 10) : 0;
19813
+ const entries = rawObservations.map((obs) => ({
19814
+ cycleNumber: cycleNum,
19815
+ category: obs.category,
19816
+ content: obs.content,
19817
+ sourceTool: "release",
19818
+ status: "observed"
19819
+ }));
19820
+ await adapter2.writeDogfoodEntries(entries);
19821
+ lines.push("", `Dogfood: ${entries.length} observation(s) saved to DB.`);
19822
+ } catch {
19823
+ lines.push("", "\u26A0\uFE0F Dogfood observations could not be saved to DB \u2014 log them manually in DOGFOOD_LOG.md.");
19824
+ }
19825
+ }
19507
19826
  lines.push("", `Next: cycle released! Run \`plan\` to start your next planning cycle.`);
19508
19827
  return textResponse(lines.join("\n"));
19509
19828
  } catch (err) {
@@ -19629,7 +19948,6 @@ async function submitReview(adapter2, input) {
19629
19948
  handoffRegenPrompt = await prepareHandoffRegen(task, input.comments);
19630
19949
  }
19631
19950
  const stageLabel = input.stage === "handoff-review" ? "Handoff Review" : "Build Acceptance";
19632
- const slackWarning = void 0;
19633
19951
  let phaseChanges = [];
19634
19952
  if (newStatus) {
19635
19953
  try {
@@ -19645,7 +19963,6 @@ async function submitReview(adapter2, input) {
19645
19963
  newStatus,
19646
19964
  unblockedTasks,
19647
19965
  handoffRegenerated,
19648
- slackWarning,
19649
19966
  handoffRegenPrompt,
19650
19967
  currentCycle: cycle,
19651
19968
  phaseChanges
@@ -19656,6 +19973,7 @@ async function submitReview(adapter2, input) {
19656
19973
  var reviewListTool = {
19657
19974
  name: "review_list",
19658
19975
  description: "List tasks ready for your sign-off \u2014 shows completed builds waiting for approval or feedback. Does not call the Anthropic API.",
19976
+ annotations: { readOnlyHint: true, destructiveHint: false },
19659
19977
  inputSchema: {
19660
19978
  type: "object",
19661
19979
  properties: {},
@@ -19665,6 +19983,7 @@ var reviewListTool = {
19665
19983
  var reviewSubmitTool = {
19666
19984
  name: "review_submit",
19667
19985
  description: "Record a review verdict on a completed build (build-acceptance) or task plan (handoff-review). ALWAYS ask the human for their verdict before calling \u2014 never auto-submit without human input. Accept moves the task to Done, request-changes sends it back for rework, reject discards the build. Updates task status based on the verdict. On handoff-review with suggested changes, returns a prompt to revise the BUILD HANDOFF.",
19986
+ annotations: { readOnlyHint: false, destructiveHint: false },
19668
19987
  inputSchema: {
19669
19988
  type: "object",
19670
19989
  properties: {
@@ -19694,10 +20013,6 @@ var reviewSubmitTool = {
19694
20013
  type: "string",
19695
20014
  description: "Your locally-generated BUILD HANDOFF regen output. Pass this to save a handoff that was regenerated in local mode (no API key)."
19696
20015
  },
19697
- notify: {
19698
- type: "boolean",
19699
- description: "Send Slack notification. Default true. Set false for batch middle reviews to avoid spam."
19700
- },
19701
20016
  auto_review: {
19702
20017
  type: "object",
19703
20018
  description: "Optional automated code review results to attach to this review. Run PR analysis first, then pass findings here.",
@@ -19814,7 +20129,6 @@ async function handleReviewSubmit(adapter2, config2, args) {
19814
20129
  const verdict = args.verdict;
19815
20130
  const comments = args.comments;
19816
20131
  const reviewer = args.reviewer ?? "human";
19817
- const notify = args.notify !== false;
19818
20132
  const rawAutoReview = args.auto_review;
19819
20133
  let autoReview;
19820
20134
  if (rawAutoReview?.verdict && rawAutoReview?.summary && Array.isArray(rawAutoReview?.findings)) {
@@ -19857,7 +20171,7 @@ async function handleReviewSubmit(adapter2, config2, args) {
19857
20171
  try {
19858
20172
  const result = await submitReview(
19859
20173
  adapter2,
19860
- { taskId, stage, verdict, comments, reviewer, notify, autoReview }
20174
+ { taskId, stage, verdict, comments, reviewer, autoReview }
19861
20175
  );
19862
20176
  const statusNote = result.newStatus ? ` Task status updated to **${result.newStatus}**.` : " Task status unchanged.";
19863
20177
  const unblockNote = result.unblockedTasks.length > 0 ? `
@@ -19890,9 +20204,6 @@ ${result.handoffRegenPrompt.userMessage}
19890
20204
  mergeNote = "\n\n" + mergeLines.map((l) => `> ${l}`).join("\n");
19891
20205
  }
19892
20206
  }
19893
- const slackNote = result.slackWarning ? `
19894
-
19895
- ${result.slackWarning}` : "";
19896
20207
  let autoReleaseNote = "";
19897
20208
  if (stage === "build-acceptance" && verdict === "accept" && result.newStatus === "Done" && result.currentCycle > 0) {
19898
20209
  try {
@@ -19943,7 +20254,7 @@ Next: address the feedback, then run \`build_execute ${taskId}\` to resubmit.`;
19943
20254
  - **Verdict:** ${result.verdict}
19944
20255
  - **Comments:** ${result.comments}
19945
20256
 
19946
- ${statusNote}${autoReviewNote}${unblockNote}${regenNote}${mergeNote}${slackNote}${autoReleaseNote}${nextStepNote}${phaseNote}`
20257
+ ${statusNote}${autoReviewNote}${unblockNote}${regenNote}${mergeNote}${autoReleaseNote}${nextStepNote}${phaseNote}`
19947
20258
  );
19948
20259
  } catch (err) {
19949
20260
  return errorResponse(err instanceof Error ? err.message : String(err));
@@ -19957,6 +20268,7 @@ import path4 from "path";
19957
20268
  var initTool = {
19958
20269
  name: "init",
19959
20270
  description: "Initialise PAPI in the current project. Generates a .mcp.json config file with pg adapter settings pointed at the hosted Supabase instance. Run once per project to get started.",
20271
+ annotations: { readOnlyHint: false, destructiveHint: false },
19960
20272
  inputSchema: {
19961
20273
  type: "object",
19962
20274
  properties: {
@@ -20082,32 +20394,491 @@ Your existing API key and project ID have been saved to .mcp.json.
20082
20394
  ].join("\n");
20083
20395
  return textResponse(output2);
20084
20396
  }
20085
- const output = [
20086
- `# PAPI \u2014 Account Required`,
20087
- "",
20088
- `PAPI needs an account to store your project data.`,
20089
- "",
20090
- "## Get Started in 3 Steps",
20091
- "",
20092
- "1. **Sign up** at https://getpapi.ai/login",
20093
- "2. **Complete the onboarding wizard** \u2014 it generates your `.mcp.json` config with your API key and project ID",
20094
- "3. **Download the config**, place it in your project root, and restart your MCP client",
20095
- "",
20096
- "The onboarding wizard generates everything you need \u2014 no manual configuration required.",
20097
- "",
20098
- `> Already have an account? Make sure both \`PAPI_PROJECT_ID\` and \`PAPI_DATA_API_KEY\` are set in your .mcp.json.`
20099
- ].join("\n");
20100
- return textResponse(output);
20397
+ const output = [
20398
+ `# PAPI \u2014 Account Required`,
20399
+ "",
20400
+ `PAPI needs an account to store your project data.`,
20401
+ "",
20402
+ "## Get Started in 3 Steps",
20403
+ "",
20404
+ "1. **Sign up** at https://getpapi.ai/login",
20405
+ "2. **Complete the onboarding wizard** \u2014 it generates your `.mcp.json` config with your API key and project ID",
20406
+ "3. **Download the config**, place it in your project root, and restart your MCP client",
20407
+ "",
20408
+ "The onboarding wizard generates everything you need \u2014 no manual configuration required.",
20409
+ "",
20410
+ `> Already have an account? Make sure both \`PAPI_PROJECT_ID\` and \`PAPI_DATA_API_KEY\` are set in your .mcp.json.`
20411
+ ].join("\n");
20412
+ return textResponse(output);
20413
+ }
20414
+
20415
+ // src/services/health.ts
20416
+ function computeHealthScore(cycleNumber, snapshots, activeTasks, decisionUsage) {
20417
+ if (cycleNumber < 3) return null;
20418
+ const scores = [];
20419
+ const recentSnaps = snapshots.slice(-3);
20420
+ const baselineSnaps = snapshots.slice(-10);
20421
+ if (recentSnaps.length > 0 && baselineSnaps.length > 0) {
20422
+ const avg = (snaps) => snaps.reduce((s, sn) => s + (sn.velocity[0]?.effortPoints ?? 0), 0) / snaps.length;
20423
+ const recentAvg = avg(recentSnaps);
20424
+ const baselineAvg = avg(baselineSnaps);
20425
+ const velocityScore = baselineAvg > 0 ? Math.min(100, Math.round(recentAvg / baselineAvg * 100)) : 50;
20426
+ scores.push({ name: "Velocity", score: velocityScore, weight: 0.25 });
20427
+ } else {
20428
+ scores.push({ name: "Velocity", score: 50, weight: 0.25 });
20429
+ }
20430
+ if (recentSnaps.length > 0) {
20431
+ const avgMatchRate = recentSnaps.reduce((s, sn) => s + (sn.accuracy[0]?.matchRate ?? 0), 0) / recentSnaps.length;
20432
+ scores.push({ name: "Estimation accuracy", score: Math.round(avgMatchRate), weight: 0.25 });
20433
+ } else {
20434
+ scores.push({ name: "Estimation accuracy", score: 50, weight: 0.25 });
20435
+ }
20436
+ const inReviewCount = activeTasks.filter((t) => t.status === "In Review").length;
20437
+ const reviewScore = inReviewCount === 0 ? 100 : inReviewCount <= 2 ? 60 : 20;
20438
+ scores.push({ name: "Review throughput", score: reviewScore, weight: 0.2 });
20439
+ const backlogTasks = activeTasks.filter((t) => t.status === "Backlog");
20440
+ if (backlogTasks.length > 0) {
20441
+ const criticalCount = backlogTasks.filter(
20442
+ (t) => t.priority === "P0 Critical" || t.priority === "P1 High"
20443
+ ).length;
20444
+ const criticalRatio = criticalCount / backlogTasks.length;
20445
+ const backlogScore = criticalRatio > 0.5 ? 40 : criticalRatio > 0.3 ? 70 : 90;
20446
+ scores.push({ name: "Backlog health", score: backlogScore, weight: 0.15 });
20447
+ } else {
20448
+ scores.push({ name: "Backlog health", score: 80, weight: 0.15 });
20449
+ }
20450
+ if (decisionUsage.length > 0) {
20451
+ const staleCount = decisionUsage.filter((u) => u.cyclesSinceLastReference >= 10).length;
20452
+ const freshRatio = (decisionUsage.length - staleCount) / decisionUsage.length;
20453
+ scores.push({ name: "AD freshness", score: Math.round(freshRatio * 100), weight: 0.15 });
20454
+ } else {
20455
+ scores.push({ name: "AD freshness", score: 70, weight: 0.15 });
20456
+ }
20457
+ const totalScore = Math.round(scores.reduce((sum, s) => sum + s.score * s.weight, 0));
20458
+ const status = totalScore >= 70 ? "GREEN" : totalScore >= 50 ? "AMBER" : "RED";
20459
+ const worst = scores.reduce((min, s) => s.score < min.score ? s : min, scores[0]);
20460
+ const reason = status === "GREEN" ? "All components healthy" : `${worst.name} below target (${worst.score}/100)`;
20461
+ return { score: totalScore, status, reason };
20462
+ }
20463
+ function countByStatus(tasks) {
20464
+ const counts = /* @__PURE__ */ new Map();
20465
+ for (const task of tasks) {
20466
+ counts.set(task.status, (counts.get(task.status) ?? 0) + 1);
20467
+ }
20468
+ return counts;
20469
+ }
20470
+ async function getHealthSummary(adapter2) {
20471
+ const health = await adapter2.getCycleHealth();
20472
+ const activeTasks = await adapter2.queryBoard({
20473
+ status: ["Backlog", "In Cycle", "Ready", "In Progress", "In Review", "Blocked"]
20474
+ });
20475
+ const logEntries = await adapter2.getCycleLog(3);
20476
+ const cycleNumber = health.totalCycles;
20477
+ const cyclesSinceReview = health.cyclesSinceLastStrategyReview;
20478
+ const reviewDue = health.strategyReviewDue;
20479
+ const reviewGateBlocking = cyclesSinceReview >= 5;
20480
+ const reviewWarning = reviewGateBlocking ? `\u26A0\uFE0F GATE \u2014 ${cyclesSinceReview} cycles since last Strategy Review. \`plan\` is blocked until \`strategy_review\` runs (or \`force: true\`).` : `\u2713 On track \u2014 ${cyclesSinceReview} cycle(s) since last review. Next due: ${reviewDue}`;
20481
+ const deferredCount = activeTasks.filter((t) => t.status === "Deferred").length;
20482
+ const nonDeferredTasks = activeTasks.filter((t) => t.status !== "Deferred");
20483
+ const statusCounts = countByStatus(nonDeferredTasks);
20484
+ let boardSummary;
20485
+ if (nonDeferredTasks.length === 0 && deferredCount === 0) {
20486
+ boardSummary = "0 tasks \u2014 board may need reloading";
20487
+ } else {
20488
+ const parts = [];
20489
+ for (const [status, count] of statusCounts) {
20490
+ parts.push(`${count} ${status}`);
20491
+ }
20492
+ boardSummary = `${nonDeferredTasks.length} active tasks \u2014 ${parts.join(", ")}`;
20493
+ if (deferredCount > 0) {
20494
+ boardSummary += ` + ${deferredCount} deferred`;
20495
+ }
20496
+ }
20497
+ const inProgressTasks = activeTasks.filter((t) => t.status === "In Progress");
20498
+ const staleTasks = inProgressTasks.length > 0 ? `${inProgressTasks.length} task(s) In Progress: ${inProgressTasks.map((t) => t.id).join(", ")}` : "No tasks currently In Progress";
20499
+ const inReviewTasks = activeTasks.filter((t) => t.status === "In Review");
20500
+ const inReviewSummary = inReviewTasks.length > 0 ? `${inReviewTasks.length} task(s) ready for sign-off: ${inReviewTasks.map((t) => t.id).join(", ")}` : "No tasks waiting for sign-off";
20501
+ let carryForward = "None found";
20502
+ if (logEntries.length > 0) {
20503
+ const latest = logEntries[0];
20504
+ if (latest.carryForward) {
20505
+ carryForward = latest.carryForward;
20506
+ } else {
20507
+ carryForward = `No carry-forward in Cycle ${latest.cycleNumber}`;
20508
+ }
20509
+ }
20510
+ let recommendedMode;
20511
+ const reasons = [];
20512
+ if (reviewGateBlocking) {
20513
+ reasons.push(`Strategy Review overdue (${cyclesSinceReview} cycles)`);
20514
+ }
20515
+ if (activeTasks.length === 0) {
20516
+ reasons.push("Board is empty \u2014 needs task reload/triage");
20517
+ }
20518
+ const unbuiltCycleTasks = activeTasks.filter(
20519
+ (t) => t.cycle === cycleNumber && (t.status === "In Cycle" || t.status === "Ready")
20520
+ );
20521
+ const inProgressCycleTasks = activeTasks.filter(
20522
+ (t) => t.cycle === cycleNumber && t.status === "In Progress"
20523
+ );
20524
+ const inReviewCycleTasks = activeTasks.filter(
20525
+ (t) => t.cycle === cycleNumber && t.status === "In Review"
20526
+ );
20527
+ if (reasons.length > 0) {
20528
+ recommendedMode = `**Full** \u2014 ${reasons.join("; ")}`;
20529
+ } else if (unbuiltCycleTasks.length > 0) {
20530
+ recommendedMode = `**Build** \u2014 ${unbuiltCycleTasks.length} cycle task(s) not yet started`;
20531
+ } else if (inProgressCycleTasks.length > 0) {
20532
+ recommendedMode = `**Build** \u2014 ${inProgressCycleTasks.length} task(s) in progress`;
20533
+ } else if (inReviewCycleTasks.length > 0) {
20534
+ recommendedMode = `**Review** \u2014 ${inReviewCycleTasks.length} task(s) awaiting review`;
20535
+ } else {
20536
+ recommendedMode = `**Full** \u2014 ready for next cycle`;
20537
+ }
20538
+ let metricsSection;
20539
+ let derivedMetricsSection = "";
20540
+ let snapshots = [];
20541
+ try {
20542
+ try {
20543
+ const reports = await adapter2.getRecentBuildReports(50);
20544
+ snapshots = computeSnapshotsFromBuildReports(reports);
20545
+ } catch {
20546
+ }
20547
+ metricsSection = formatCycleMetrics(snapshots);
20548
+ derivedMetricsSection = formatDerivedMetrics(snapshots, activeTasks);
20549
+ } catch (_err) {
20550
+ metricsSection = "Could not read methodology metrics.";
20551
+ }
20552
+ try {
20553
+ const recentReports = await adapter2.getRecentBuildReports(50);
20554
+ if (recentReports.length > 0) {
20555
+ const taskCounts = /* @__PURE__ */ new Map();
20556
+ for (const r of recentReports) {
20557
+ taskCounts.set(r.taskId, (taskCounts.get(r.taskId) ?? 0) + 1);
20558
+ }
20559
+ const iterCounts = [...taskCounts.values()];
20560
+ const avgIter = iterCounts.reduce((s, c) => s + c, 0) / iterCounts.length;
20561
+ const multiIterTasks = iterCounts.filter((c) => c > 1).length;
20562
+ if (avgIter > 1 || multiIterTasks > 0) {
20563
+ derivedMetricsSection += `
20564
+
20565
+ **Rework**
20566
+ - Average iterations: ${avgIter.toFixed(1)} (${multiIterTasks} task${multiIterTasks !== 1 ? "s" : ""} with pushbacks)`;
20567
+ }
20568
+ }
20569
+ } catch {
20570
+ }
20571
+ const costSection = "Disabled \u2014 local MCP, no API costs.";
20572
+ let decisionUsageSection = "";
20573
+ let decisionUsageEntries = [];
20574
+ try {
20575
+ const usage = await adapter2.getDecisionUsage(cycleNumber);
20576
+ decisionUsageEntries = usage;
20577
+ if (usage.length > 0) {
20578
+ const stale = usage.filter((u) => u.cyclesSinceLastReference >= 5);
20579
+ if (stale.length > 0) {
20580
+ const lines = stale.map(
20581
+ (u) => `- ${u.decisionId}: last referenced Cycle ${u.lastReferencedCycle} (${u.cyclesSinceLastReference} cycles ago)`
20582
+ );
20583
+ decisionUsageSection = `**Stale ADs (5+ cycles unreferenced):**
20584
+ ${lines.join("\n")}`;
20585
+ } else {
20586
+ decisionUsageSection = `All ${usage.length} tracked ADs referenced within last 5 cycles.`;
20587
+ }
20588
+ }
20589
+ } catch {
20590
+ }
20591
+ let decisionLifecycleSection = "";
20592
+ try {
20593
+ const decisions = await adapter2.getActiveDecisions();
20594
+ const lifecycleSummary = formatDecisionLifecycleSummary(decisions);
20595
+ if (lifecycleSummary) {
20596
+ decisionLifecycleSection = `**Lifecycle:** ${lifecycleSummary}`;
20597
+ }
20598
+ } catch {
20599
+ }
20600
+ const decisionScoresSection = "";
20601
+ let contextUtilisationSection = "";
20602
+ try {
20603
+ const utilData = await adapter2.getContextUtilisation?.();
20604
+ if (utilData && utilData.length > 0) {
20605
+ const lines = utilData.filter((u) => u.cycleNumber === cycleNumber).map((u) => `- ${u.tool}: ${(u.avgUtilisation * 100).toFixed(0)}% utilisation (${(u.avgContextBytes / 1024).toFixed(1)}KB avg context)`);
20606
+ if (lines.length > 0) {
20607
+ contextUtilisationSection = `**Current cycle:**
20608
+ ${lines.join("\n")}`;
20609
+ }
20610
+ }
20611
+ } catch {
20612
+ }
20613
+ let northStarSection = "";
20614
+ try {
20615
+ const staleness = await adapter2.getNorthStarStaleness?.();
20616
+ if (staleness) {
20617
+ const cycleGap = cycleNumber - staleness.setCycle;
20618
+ const daysSinceSet = Math.floor((Date.now() - new Date(staleness.setAt).getTime()) / (1e3 * 60 * 60 * 24));
20619
+ northStarSection = `\u2713 North Star set Cycle ${staleness.setCycle} (${cycleGap} cycles, ${daysSinceSet} days ago)`;
20620
+ } else {
20621
+ const setAtCycle = await adapter2.getNorthStarSetCycle?.();
20622
+ if (setAtCycle != null) {
20623
+ northStarSection = `\u2713 North Star set Cycle ${setAtCycle}`;
20624
+ } else if (adapter2.getCurrentNorthStar) {
20625
+ const ns = await adapter2.getCurrentNorthStar();
20626
+ northStarSection = ns ? "" : "\u26A0\uFE0F No North Star set \u2014 consider defining one";
20627
+ }
20628
+ }
20629
+ } catch {
20630
+ }
20631
+ const healthResult = computeHealthScore(cycleNumber, snapshots, activeTasks, decisionUsageEntries);
20632
+ return {
20633
+ cycleNumber,
20634
+ latestCycleStatus: health.latestCycleStatus,
20635
+ connectionStatus: getConnectionStatus(),
20636
+ reviewWarning,
20637
+ boardSummary,
20638
+ staleTasks,
20639
+ inReviewSummary,
20640
+ carryForward,
20641
+ recommendedMode,
20642
+ metricsSection,
20643
+ derivedMetricsSection,
20644
+ costSection,
20645
+ decisionUsageSection,
20646
+ decisionLifecycleSection,
20647
+ decisionScoresSection,
20648
+ contextUtilisationSection,
20649
+ northStarSection,
20650
+ healthScore: healthResult?.score ?? null,
20651
+ healthStatus: healthResult?.status ?? null,
20652
+ healthReason: healthResult?.reason ?? null
20653
+ };
20654
+ }
20655
+
20656
+ // src/tools/orient.ts
20657
+ init_git();
20658
+
20659
+ // src/tools/doc-registry.ts
20660
+ import { readdirSync as readdirSync4, existsSync as existsSync5, readFileSync as readFileSync2 } from "fs";
20661
+ import { join as join8, relative } from "path";
20662
+ import { homedir as homedir2 } from "os";
20663
+ var docRegisterTool = {
20664
+ name: "doc_register",
20665
+ 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.",
20666
+ annotations: { readOnlyHint: false, destructiveHint: false },
20667
+ inputSchema: {
20668
+ type: "object",
20669
+ properties: {
20670
+ path: { type: "string", description: 'Relative path from project root (e.g. "docs/research/funding-landscape.md").' },
20671
+ title: { type: "string", description: "Document title." },
20672
+ type: { type: "string", enum: ["research", "audit", "spec", "guide", "architecture", "positioning", "framework", "reference"], description: "Document type." },
20673
+ status: { type: "string", enum: ["active", "draft", "superseded", "actioned", "legacy", "archived"], description: 'Document status. Defaults to "active".' },
20674
+ summary: { type: "string", description: 'Structured 2-4 sentence summary. Format: "Conclusions: ... Open questions: ... Unactioned: ..."' },
20675
+ tags: { type: "array", items: { type: "string" }, description: "Tags from project vocabulary." },
20676
+ cycle: { type: "number", description: "Current cycle number." },
20677
+ actions: {
20678
+ type: "array",
20679
+ items: {
20680
+ type: "object",
20681
+ properties: {
20682
+ description: { type: "string" },
20683
+ status: { type: "string", enum: ["pending", "resolved"] },
20684
+ linkedTaskId: { type: "string" }
20685
+ },
20686
+ required: ["description", "status"]
20687
+ },
20688
+ description: "Actionable findings from the document."
20689
+ },
20690
+ superseded_by_path: { type: "string", description: "Path of the doc that supersedes this one (sets status to superseded)." }
20691
+ },
20692
+ required: ["path", "title", "type", "summary", "cycle"]
20693
+ }
20694
+ };
20695
+ var docSearchTool = {
20696
+ name: "doc_search",
20697
+ description: "Search the doc registry for documents by type, tags, keyword, or pending actions. Returns summaries, not full content. Use for context gathering in plan, strategy review, and idea dedup.",
20698
+ annotations: { readOnlyHint: true, destructiveHint: false },
20699
+ inputSchema: {
20700
+ type: "object",
20701
+ properties: {
20702
+ type: { type: "string", description: 'Filter by doc type (e.g. "research", "architecture").' },
20703
+ status: { type: "string", description: 'Filter by status. Defaults to "active".' },
20704
+ tags: { type: "array", items: { type: "string" }, description: "Filter by tags (OR match)." },
20705
+ keyword: { type: "string", description: "Search title and summary text." },
20706
+ has_pending_actions: { type: "boolean", description: "Only docs with unresolved action items." },
20707
+ since_cycle: { type: "number", description: "Docs updated since this cycle." },
20708
+ limit: { type: "number", description: "Max results (default: 10)." }
20709
+ },
20710
+ required: []
20711
+ }
20712
+ };
20713
+ var docScanTool = {
20714
+ name: "doc_scan",
20715
+ 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.",
20716
+ annotations: { readOnlyHint: true, destructiveHint: false },
20717
+ inputSchema: {
20718
+ type: "object",
20719
+ properties: {
20720
+ include_plans: {
20721
+ type: "boolean",
20722
+ description: "Also scan ~/.claude/plans/ for plan files (default: false)."
20723
+ }
20724
+ },
20725
+ required: []
20726
+ }
20727
+ };
20728
+ async function handleDocRegister(adapter2, args) {
20729
+ if (!adapter2.registerDoc) {
20730
+ return errorResponse("Doc registry not available \u2014 requires pg adapter.");
20731
+ }
20732
+ const path5 = args.path;
20733
+ const title = args.title;
20734
+ const type = args.type;
20735
+ const status = args.status ?? "active";
20736
+ const summary = args.summary;
20737
+ const tags = args.tags ?? [];
20738
+ const cycle = args.cycle;
20739
+ const actions = args.actions;
20740
+ const supersededByPath = args.superseded_by_path;
20741
+ if (!path5 || !title || !type || !summary || !cycle) {
20742
+ return errorResponse("Required fields: path, title, type, summary, cycle.");
20743
+ }
20744
+ let supersededBy;
20745
+ if (supersededByPath) {
20746
+ const existing = await adapter2.getDoc?.(supersededByPath);
20747
+ if (existing) {
20748
+ supersededBy = existing.id;
20749
+ await adapter2.updateDocStatus?.(existing.id, "superseded", void 0);
20750
+ }
20751
+ }
20752
+ const entry = await adapter2.registerDoc({
20753
+ title,
20754
+ type,
20755
+ path: path5,
20756
+ status: supersededByPath ? "superseded" : status,
20757
+ summary,
20758
+ tags,
20759
+ cycleCreated: cycle,
20760
+ cycleUpdated: cycle,
20761
+ supersededBy,
20762
+ actions
20763
+ });
20764
+ return textResponse(
20765
+ `**Registered:** ${entry.title}
20766
+ - **Path:** ${entry.path}
20767
+ - **Type:** ${entry.type} | **Status:** ${entry.status}
20768
+ - **Tags:** ${entry.tags.length > 0 ? entry.tags.join(", ") : "none"}
20769
+ - **Actions:** ${actions?.length ?? 0} items
20770
+ - **ID:** ${entry.id}`
20771
+ );
20772
+ }
20773
+ async function handleDocSearch(adapter2, args) {
20774
+ if (!adapter2.searchDocs) {
20775
+ return errorResponse("Doc registry not available \u2014 requires pg adapter.");
20776
+ }
20777
+ const input = {
20778
+ type: args.type,
20779
+ status: args.status,
20780
+ tags: args.tags,
20781
+ keyword: args.keyword,
20782
+ hasPendingActions: args.has_pending_actions,
20783
+ sinceCycle: args.since_cycle,
20784
+ limit: args.limit
20785
+ };
20786
+ const docs = await adapter2.searchDocs(input);
20787
+ if (docs.length === 0) {
20788
+ return textResponse("No documents found matching the search criteria.");
20789
+ }
20790
+ const lines = docs.map((d) => {
20791
+ const actionCount = d.actions?.filter((a) => a.status === "pending").length ?? 0;
20792
+ const actionNote = actionCount > 0 ? ` | ${actionCount} pending action(s)` : "";
20793
+ return `### ${d.title}
20794
+ **Type:** ${d.type} | **Status:** ${d.status} | **Cycle:** ${d.cycleCreated}${d.cycleUpdated ? `\u2192${d.cycleUpdated}` : ""}${actionNote}
20795
+ **Path:** ${d.path}
20796
+ **Tags:** ${d.tags.length > 0 ? d.tags.join(", ") : "none"}
20797
+ ${d.summary}
20798
+ `;
20799
+ });
20800
+ return textResponse(`**${docs.length} document(s) found:**
20801
+
20802
+ ${lines.join("\n---\n\n")}`);
20803
+ }
20804
+ function scanMdFiles(dir, rootDir) {
20805
+ if (!existsSync5(dir)) return [];
20806
+ const files = [];
20807
+ try {
20808
+ const entries = readdirSync4(dir, { withFileTypes: true });
20809
+ for (const entry of entries) {
20810
+ const full = join8(dir, entry.name);
20811
+ if (entry.isDirectory()) {
20812
+ files.push(...scanMdFiles(full, rootDir));
20813
+ } else if (entry.name.endsWith(".md")) {
20814
+ files.push(relative(rootDir, full));
20815
+ }
20816
+ }
20817
+ } catch {
20818
+ }
20819
+ return files;
20820
+ }
20821
+ function extractTitle(filePath) {
20822
+ try {
20823
+ const content = readFileSync2(filePath, "utf-8").slice(0, 1e3);
20824
+ const fmMatch = content.match(/^---[\s\S]*?title:\s*(.+?)$/m);
20825
+ if (fmMatch) return fmMatch[1].trim().replace(/^["']|["']$/g, "");
20826
+ const headingMatch = content.match(/^#+\s+(.+)$/m);
20827
+ if (headingMatch) return headingMatch[1].trim();
20828
+ } catch {
20829
+ }
20830
+ return void 0;
20831
+ }
20832
+ async function handleDocScan(adapter2, config2, args) {
20833
+ if (!adapter2.searchDocs) {
20834
+ return errorResponse("Doc registry not available on this adapter.");
20835
+ }
20836
+ const includePlans = args.include_plans ?? false;
20837
+ const registered = await adapter2.searchDocs({ limit: 500, status: "all" });
20838
+ const registeredPaths = new Set(registered.map((d) => d.path));
20839
+ const docsDir = join8(config2.projectRoot, "docs");
20840
+ const docsFiles = scanMdFiles(docsDir, config2.projectRoot);
20841
+ const unregisteredDocs = docsFiles.filter((f) => !registeredPaths.has(f));
20842
+ let unregisteredPlans = [];
20843
+ if (includePlans) {
20844
+ const plansDir = join8(homedir2(), ".claude", "plans");
20845
+ if (existsSync5(plansDir)) {
20846
+ const planFiles = scanMdFiles(plansDir, plansDir);
20847
+ unregisteredPlans = planFiles.map((f) => `plans/${f}`).filter((f) => !registeredPaths.has(f)).map((f) => ({
20848
+ path: f,
20849
+ title: extractTitle(join8(plansDir, f.replace("plans/", "")))
20850
+ }));
20851
+ }
20852
+ }
20853
+ const lines = [];
20854
+ if (unregisteredDocs.length === 0 && unregisteredPlans.length === 0) {
20855
+ return textResponse("All docs are registered. No unregistered files found.");
20856
+ }
20857
+ if (unregisteredDocs.length > 0) {
20858
+ lines.push(`## Unregistered Docs (${unregisteredDocs.length})`);
20859
+ for (const f of unregisteredDocs) {
20860
+ const title = extractTitle(join8(config2.projectRoot, f));
20861
+ lines.push(`- \`${f}\`${title ? ` \u2014 ${title}` : ""}`);
20862
+ }
20863
+ }
20864
+ if (unregisteredPlans.length > 0) {
20865
+ lines.push("", `## Unregistered Plans (${unregisteredPlans.length})`);
20866
+ for (const p of unregisteredPlans) {
20867
+ lines.push(`- \`${p.path}\`${p.title ? ` \u2014 ${p.title}` : ""}`);
20868
+ }
20869
+ }
20870
+ lines.push("", `Use \`doc_register\` to register these files.`);
20871
+ return textResponse(lines.join("\n"));
20101
20872
  }
20102
20873
 
20103
20874
  // src/tools/orient.ts
20104
- init_git();
20105
20875
  import { execFileSync as execFileSync3 } from "child_process";
20106
- import { readFileSync as readFileSync2, writeFileSync, existsSync as existsSync5 } from "fs";
20107
- import { join as join8 } from "path";
20876
+ import { readFileSync as readFileSync3, writeFileSync, existsSync as existsSync6 } from "fs";
20877
+ import { join as join9 } from "path";
20108
20878
  var orientTool = {
20109
20879
  name: "orient",
20110
20880
  description: "Session orientation \u2014 run this FIRST at session start before any other tool. Single call that replaces build_list + health. Returns: cycle number, task counts by status, in-progress/in-review tasks, strategy review cadence, velocity snapshot, recommended next action, and a release reminder when all cycle tasks are Done but release has not run. Read-only, does not modify any files.",
20881
+ annotations: { readOnlyHint: true, destructiveHint: false },
20111
20882
  inputSchema: {
20112
20883
  type: "object",
20113
20884
  properties: {},
@@ -20283,8 +21054,8 @@ function getLatestGitTag(projectRoot) {
20283
21054
  }
20284
21055
  function checkNpmVersionDrift() {
20285
21056
  try {
20286
- const pkgPath = join8(new URL(".", import.meta.url).pathname, "..", "..", "package.json");
20287
- const pkg = JSON.parse(readFileSync2(pkgPath, "utf-8"));
21057
+ const pkgPath = join9(new URL(".", import.meta.url).pathname, "..", "..", "package.json");
21058
+ const pkg = JSON.parse(readFileSync3(pkgPath, "utf-8"));
20288
21059
  const localVersion = pkg.version;
20289
21060
  const packageName = pkg.name;
20290
21061
  const published = execFileSync3("npm", ["view", packageName, "version"], {
@@ -20324,6 +21095,17 @@ async function handleOrient(adapter2, config2) {
20324
21095
  if (!cycleIsComplete && cycleTotal === 0 && cycleDone > 0) {
20325
21096
  buildResult.warnings.unshift(`\u26A0\uFE0F Cycle ${currentCycle} is complete \u2014 all ${cycleDone} task${cycleDone !== 1 ? "s" : ""} Done. Release has not been run. Run \`release\` now.`);
20326
21097
  }
21098
+ try {
21099
+ const p1BacklogTasks = await adapter2.queryBoard({ status: ["Backlog"], priority: ["P1 High"] });
21100
+ const stalledP1 = p1BacklogTasks.filter(
21101
+ (t) => t.createdCycle != null && currentCycle - t.createdCycle >= 3
21102
+ );
21103
+ if (stalledP1.length > 0) {
21104
+ const ids = stalledP1.map((t) => `${t.displayId} (${currentCycle - (t.createdCycle ?? currentCycle)}+ cycles)`).join(", ");
21105
+ buildResult.warnings.push(`\u26A0\uFE0F P1 tasks stalled 3+ cycles: ${ids}`);
21106
+ }
21107
+ } catch {
21108
+ }
20327
21109
  const inProgressItems = buildResult.inProgress.map(
20328
21110
  (t) => `- **${t.id}:** ${t.title} (${t.priority} | ${t.complexity})`
20329
21111
  );
@@ -20379,8 +21161,15 @@ ${versionDrift}` : "";
20379
21161
  try {
20380
21162
  const unrecorded = detectUnrecordedCommits(config2.projectRoot, config2.baseBranch);
20381
21163
  if (unrecorded.length > 0) {
21164
+ const doneTasks = await adapter2.queryBoard({ status: ["Done"] });
21165
+ const adHocDoneTasks = doneTasks.filter((t) => t.cycle == null);
21166
+ const alreadyRecorded = adHocDoneTasks.length >= unrecorded.length;
20382
21167
  const lines = ["\n\n## Unrecorded Work"];
20383
- lines.push(`${unrecorded.length} commit(s) on ${config2.baseBranch} since last release not captured by \`build_execute\`. Run \`ad_hoc\` to record them.`);
21168
+ if (alreadyRecorded) {
21169
+ lines.push(`${unrecorded.length} commit(s) on ${config2.baseBranch} since last release not matched to \`build_execute\` commits. ${adHocDoneTasks.length} ad_hoc task(s) already recorded \u2014 this work may already be captured. Verify before running \`ad_hoc\` again.`);
21170
+ } else {
21171
+ lines.push(`${unrecorded.length} commit(s) on ${config2.baseBranch} since last release not captured by \`build_execute\`. Run \`ad_hoc\` to record them.`);
21172
+ }
20384
21173
  for (const c of unrecorded) {
20385
21174
  lines.push(`- \`${c.hash}\` ${c.message}`);
20386
21175
  }
@@ -20388,6 +21177,23 @@ ${versionDrift}` : "";
20388
21177
  }
20389
21178
  } catch {
20390
21179
  }
21180
+ let unregisteredDocsNote = "";
21181
+ try {
21182
+ if (adapter2.searchDocs) {
21183
+ const docsDir = join9(config2.projectRoot, "docs");
21184
+ const docsFiles = scanMdFiles(docsDir, config2.projectRoot);
21185
+ if (docsFiles.length > 0) {
21186
+ const registered = await adapter2.searchDocs({ limit: 500, status: "all" });
21187
+ const registeredPaths = new Set(registered.map((d) => d.path));
21188
+ const unregisteredCount = docsFiles.filter((f) => !registeredPaths.has(f)).length;
21189
+ if (unregisteredCount > 0) {
21190
+ unregisteredDocsNote = `
21191
+ \u26A0\uFE0F **${unregisteredCount} unregistered doc(s) in docs/** \u2014 run \`doc_scan\` to review, then \`doc_register\` to index them.`;
21192
+ }
21193
+ }
21194
+ }
21195
+ } catch {
21196
+ }
20391
21197
  let recsNote = "";
20392
21198
  try {
20393
21199
  const pendingRecs = await adapter2.getPendingRecommendations();
@@ -20421,16 +21227,33 @@ ${versionDrift}` : "";
20421
21227
  }
20422
21228
  } catch {
20423
21229
  }
20424
- return textResponse(formatOrientSummary(healthResult, buildInfo, hierarchy, latestTag, config2.projectRoot) + ttfvNote + reconciliationNote + unrecordedNote + recsNote + pendingReviewNote + patternsNote + versionNote + enrichmentNote);
21230
+ let unactionedIssuesNote = "";
21231
+ try {
21232
+ const learnings = await adapter2.getCycleLearnings?.({ category: "issue", limit: 20 });
21233
+ if (learnings) {
21234
+ const unactioned = learnings.filter((l) => !l.actionTaken && l.severity && ["P0", "P1", "P2"].includes(l.severity)).slice(0, 5);
21235
+ if (unactioned.length > 0) {
21236
+ const lines = ["\n\n## Unactioned Issues"];
21237
+ for (const issue of unactioned) {
21238
+ const desc = issue.summary.length > 100 ? `${issue.summary.slice(0, 97)}\u2026` : issue.summary;
21239
+ lines.push(`- **${issue.severity}** (C${issue.cycleNumber} / ${issue.taskId}): ${desc}`);
21240
+ }
21241
+ lines.push("_Run `idea` to log these as backlog tasks, or `board_edit` if already handled._");
21242
+ unactionedIssuesNote = lines.join("\n");
21243
+ }
21244
+ }
21245
+ } catch {
21246
+ }
21247
+ return textResponse(formatOrientSummary(healthResult, buildInfo, hierarchy, latestTag, config2.projectRoot) + ttfvNote + reconciliationNote + unrecordedNote + unregisteredDocsNote + recsNote + pendingReviewNote + patternsNote + unactionedIssuesNote + versionNote + enrichmentNote);
20425
21248
  } catch (err) {
20426
21249
  const message = err instanceof Error ? err.message : String(err);
20427
21250
  return errorResponse(`Orient failed: ${message}`);
20428
21251
  }
20429
21252
  }
20430
21253
  function enrichClaudeMd(projectRoot, cycleNumber) {
20431
- const claudeMdPath = join8(projectRoot, "CLAUDE.md");
20432
- if (!existsSync5(claudeMdPath)) return "";
20433
- const content = readFileSync2(claudeMdPath, "utf-8");
21254
+ const claudeMdPath = join9(projectRoot, "CLAUDE.md");
21255
+ if (!existsSync6(claudeMdPath)) return "";
21256
+ const content = readFileSync3(claudeMdPath, "utf-8");
20434
21257
  const additions = [];
20435
21258
  if (cycleNumber >= 6 && !content.includes(CLAUDE_MD_ENRICHMENT_SENTINEL_T1)) {
20436
21259
  additions.push(CLAUDE_MD_TIER_1);
@@ -20452,6 +21275,7 @@ function enrichClaudeMd(projectRoot, cycleNumber) {
20452
21275
  var hierarchyUpdateTool = {
20453
21276
  name: "hierarchy_update",
20454
21277
  description: "Update the status of a phase, stage, or horizon in the project hierarchy (AD-14). Accepts a level (phase, stage, or horizon), a name or ID, and a new status. For stages, optionally set exit_criteria \u2014 a checklist defining when the stage is considered done. Does not call the Anthropic API.",
21278
+ annotations: { readOnlyHint: false, destructiveHint: false },
20455
21279
  inputSchema: {
20456
21280
  type: "object",
20457
21281
  properties: {
@@ -20847,6 +21671,7 @@ async function applyZoomOut(adapter2, llmResponse, cycleNumber) {
20847
21671
  var zoomOutTool = {
20848
21672
  name: "zoom_out",
20849
21673
  description: 'Run a Zoom-Out Retrospective \u2014 a higher-level meta-retrospective that sits above strategy reviews. Analyses the full project arc: every cycle, decision, and pivot. Use when you want to step back and see the big picture after many cycles. First call returns a prompt (prepare phase). Then call again with mode "apply" and your output.',
21674
+ annotations: { readOnlyHint: false, destructiveHint: false },
20850
21675
  inputSchema: {
20851
21676
  type: "object",
20852
21677
  properties: {
@@ -20921,222 +21746,11 @@ ${result.userMessage}
20921
21746
  }
20922
21747
  }
20923
21748
 
20924
- // src/tools/doc-registry.ts
20925
- import { readdirSync as readdirSync4, existsSync as existsSync6, readFileSync as readFileSync3 } from "fs";
20926
- import { join as join9, relative } from "path";
20927
- import { homedir as homedir2 } from "os";
20928
- var docRegisterTool = {
20929
- name: "doc_register",
20930
- 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.",
20931
- inputSchema: {
20932
- type: "object",
20933
- properties: {
20934
- path: { type: "string", description: 'Relative path from project root (e.g. "docs/research/funding-landscape.md").' },
20935
- title: { type: "string", description: "Document title." },
20936
- type: { type: "string", enum: ["research", "audit", "spec", "guide", "architecture", "positioning", "framework", "reference"], description: "Document type." },
20937
- status: { type: "string", enum: ["active", "draft", "superseded", "actioned", "legacy", "archived"], description: 'Document status. Defaults to "active".' },
20938
- summary: { type: "string", description: 'Structured 2-4 sentence summary. Format: "Conclusions: ... Open questions: ... Unactioned: ..."' },
20939
- tags: { type: "array", items: { type: "string" }, description: "Tags from project vocabulary." },
20940
- cycle: { type: "number", description: "Current cycle number." },
20941
- actions: {
20942
- type: "array",
20943
- items: {
20944
- type: "object",
20945
- properties: {
20946
- description: { type: "string" },
20947
- status: { type: "string", enum: ["pending", "resolved"] },
20948
- linkedTaskId: { type: "string" }
20949
- },
20950
- required: ["description", "status"]
20951
- },
20952
- description: "Actionable findings from the document."
20953
- },
20954
- superseded_by_path: { type: "string", description: "Path of the doc that supersedes this one (sets status to superseded)." }
20955
- },
20956
- required: ["path", "title", "type", "summary", "cycle"]
20957
- }
20958
- };
20959
- var docSearchTool = {
20960
- name: "doc_search",
20961
- description: "Search the doc registry for documents by type, tags, keyword, or pending actions. Returns summaries, not full content. Use for context gathering in plan, strategy review, and idea dedup.",
20962
- inputSchema: {
20963
- type: "object",
20964
- properties: {
20965
- type: { type: "string", description: 'Filter by doc type (e.g. "research", "architecture").' },
20966
- status: { type: "string", description: 'Filter by status. Defaults to "active".' },
20967
- tags: { type: "array", items: { type: "string" }, description: "Filter by tags (OR match)." },
20968
- keyword: { type: "string", description: "Search title and summary text." },
20969
- has_pending_actions: { type: "boolean", description: "Only docs with unresolved action items." },
20970
- since_cycle: { type: "number", description: "Docs updated since this cycle." },
20971
- limit: { type: "number", description: "Max results (default: 10)." }
20972
- },
20973
- required: []
20974
- }
20975
- };
20976
- var docScanTool = {
20977
- name: "doc_scan",
20978
- 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.",
20979
- inputSchema: {
20980
- type: "object",
20981
- properties: {
20982
- include_plans: {
20983
- type: "boolean",
20984
- description: "Also scan ~/.claude/plans/ for plan files (default: false)."
20985
- }
20986
- },
20987
- required: []
20988
- }
20989
- };
20990
- async function handleDocRegister(adapter2, args) {
20991
- if (!adapter2.registerDoc) {
20992
- return errorResponse("Doc registry not available \u2014 requires pg adapter.");
20993
- }
20994
- const path5 = args.path;
20995
- const title = args.title;
20996
- const type = args.type;
20997
- const status = args.status ?? "active";
20998
- const summary = args.summary;
20999
- const tags = args.tags ?? [];
21000
- const cycle = args.cycle;
21001
- const actions = args.actions;
21002
- const supersededByPath = args.superseded_by_path;
21003
- if (!path5 || !title || !type || !summary || !cycle) {
21004
- return errorResponse("Required fields: path, title, type, summary, cycle.");
21005
- }
21006
- let supersededBy;
21007
- if (supersededByPath) {
21008
- const existing = await adapter2.getDoc?.(supersededByPath);
21009
- if (existing) {
21010
- supersededBy = existing.id;
21011
- await adapter2.updateDocStatus?.(existing.id, "superseded", void 0);
21012
- }
21013
- }
21014
- const entry = await adapter2.registerDoc({
21015
- title,
21016
- type,
21017
- path: path5,
21018
- status: supersededByPath ? "superseded" : status,
21019
- summary,
21020
- tags,
21021
- cycleCreated: cycle,
21022
- cycleUpdated: cycle,
21023
- supersededBy,
21024
- actions
21025
- });
21026
- return textResponse(
21027
- `**Registered:** ${entry.title}
21028
- - **Path:** ${entry.path}
21029
- - **Type:** ${entry.type} | **Status:** ${entry.status}
21030
- - **Tags:** ${entry.tags.length > 0 ? entry.tags.join(", ") : "none"}
21031
- - **Actions:** ${actions?.length ?? 0} items
21032
- - **ID:** ${entry.id}`
21033
- );
21034
- }
21035
- async function handleDocSearch(adapter2, args) {
21036
- if (!adapter2.searchDocs) {
21037
- return errorResponse("Doc registry not available \u2014 requires pg adapter.");
21038
- }
21039
- const input = {
21040
- type: args.type,
21041
- status: args.status,
21042
- tags: args.tags,
21043
- keyword: args.keyword,
21044
- hasPendingActions: args.has_pending_actions,
21045
- sinceCycle: args.since_cycle,
21046
- limit: args.limit
21047
- };
21048
- const docs = await adapter2.searchDocs(input);
21049
- if (docs.length === 0) {
21050
- return textResponse("No documents found matching the search criteria.");
21051
- }
21052
- const lines = docs.map((d) => {
21053
- const actionCount = d.actions?.filter((a) => a.status === "pending").length ?? 0;
21054
- const actionNote = actionCount > 0 ? ` | ${actionCount} pending action(s)` : "";
21055
- return `### ${d.title}
21056
- **Type:** ${d.type} | **Status:** ${d.status} | **Cycle:** ${d.cycleCreated}${d.cycleUpdated ? `\u2192${d.cycleUpdated}` : ""}${actionNote}
21057
- **Path:** ${d.path}
21058
- **Tags:** ${d.tags.length > 0 ? d.tags.join(", ") : "none"}
21059
- ${d.summary}
21060
- `;
21061
- });
21062
- return textResponse(`**${docs.length} document(s) found:**
21063
-
21064
- ${lines.join("\n---\n\n")}`);
21065
- }
21066
- function scanMdFiles(dir, rootDir) {
21067
- if (!existsSync6(dir)) return [];
21068
- const files = [];
21069
- try {
21070
- const entries = readdirSync4(dir, { withFileTypes: true });
21071
- for (const entry of entries) {
21072
- const full = join9(dir, entry.name);
21073
- if (entry.isDirectory()) {
21074
- files.push(...scanMdFiles(full, rootDir));
21075
- } else if (entry.name.endsWith(".md")) {
21076
- files.push(relative(rootDir, full));
21077
- }
21078
- }
21079
- } catch {
21080
- }
21081
- return files;
21082
- }
21083
- function extractTitle(filePath) {
21084
- try {
21085
- const content = readFileSync3(filePath, "utf-8").slice(0, 1e3);
21086
- const fmMatch = content.match(/^---[\s\S]*?title:\s*(.+?)$/m);
21087
- if (fmMatch) return fmMatch[1].trim().replace(/^["']|["']$/g, "");
21088
- const headingMatch = content.match(/^#+\s+(.+)$/m);
21089
- if (headingMatch) return headingMatch[1].trim();
21090
- } catch {
21091
- }
21092
- return void 0;
21093
- }
21094
- async function handleDocScan(adapter2, config2, args) {
21095
- if (!adapter2.searchDocs) {
21096
- return errorResponse("Doc registry not available on this adapter.");
21097
- }
21098
- const includePlans = args.include_plans ?? false;
21099
- const registered = await adapter2.searchDocs({ limit: 500, status: "all" });
21100
- const registeredPaths = new Set(registered.map((d) => d.path));
21101
- const docsDir = join9(config2.projectRoot, "docs");
21102
- const docsFiles = scanMdFiles(docsDir, config2.projectRoot);
21103
- const unregisteredDocs = docsFiles.filter((f) => !registeredPaths.has(f));
21104
- let unregisteredPlans = [];
21105
- if (includePlans) {
21106
- const plansDir = join9(homedir2(), ".claude", "plans");
21107
- if (existsSync6(plansDir)) {
21108
- const planFiles = scanMdFiles(plansDir, plansDir);
21109
- unregisteredPlans = planFiles.map((f) => `plans/${f}`).filter((f) => !registeredPaths.has(f)).map((f) => ({
21110
- path: f,
21111
- title: extractTitle(join9(plansDir, f.replace("plans/", "")))
21112
- }));
21113
- }
21114
- }
21115
- const lines = [];
21116
- if (unregisteredDocs.length === 0 && unregisteredPlans.length === 0) {
21117
- return textResponse("All docs are registered. No unregistered files found.");
21118
- }
21119
- if (unregisteredDocs.length > 0) {
21120
- lines.push(`## Unregistered Docs (${unregisteredDocs.length})`);
21121
- for (const f of unregisteredDocs) {
21122
- const title = extractTitle(join9(config2.projectRoot, f));
21123
- lines.push(`- \`${f}\`${title ? ` \u2014 ${title}` : ""}`);
21124
- }
21125
- }
21126
- if (unregisteredPlans.length > 0) {
21127
- lines.push("", `## Unregistered Plans (${unregisteredPlans.length})`);
21128
- for (const p of unregisteredPlans) {
21129
- lines.push(`- \`${p.path}\`${p.title ? ` \u2014 ${p.title}` : ""}`);
21130
- }
21131
- }
21132
- lines.push("", `Use \`doc_register\` to register these files.`);
21133
- return textResponse(lines.join("\n"));
21134
- }
21135
-
21136
21749
  // src/tools/sibling-ads.ts
21137
21750
  var getSiblingAdsTool = {
21138
21751
  name: "get_sibling_ads",
21139
21752
  description: "Read Active Decisions from sibling PAPI projects that share the same Supabase instance. Requires PAPI_SIBLING_PROJECT_IDS env var (comma-separated project UUIDs). Returns ADs labelled by source project \u2014 useful for cross-project architectural alignment. pg adapter only \u2014 returns an error if using md or proxy adapter.",
21753
+ annotations: { readOnlyHint: true, destructiveHint: false },
21140
21754
  inputSchema: {
21141
21755
  type: "object",
21142
21756
  properties: {
@@ -21318,6 +21932,7 @@ var lastPrepareContextBytes2;
21318
21932
  var handoffGenerateTool = {
21319
21933
  name: "handoff_generate",
21320
21934
  description: "Generate BUILD HANDOFFs for cycle tasks that don't have one yet. Run after `plan` (with skip_handoffs=true) or to regenerate stale handoffs. Uses the prepare/apply pattern \u2014 first call returns a prompt, second call persists results.",
21935
+ annotations: { readOnlyHint: false, destructiveHint: false },
21321
21936
  inputSchema: {
21322
21937
  type: "object",
21323
21938
  properties: {
@@ -21428,12 +22043,14 @@ function isEnabled() {
21428
22043
  }
21429
22044
  function emitTelemetryEvent(event) {
21430
22045
  if (!isEnabled()) return;
22046
+ const userId = event.user_id ?? process.env["PAPI_USER_ID"] ?? void 0;
21431
22047
  const body = {
21432
22048
  project_id: event.project_id,
21433
22049
  tool_name: event.tool_name,
21434
22050
  event_type: event.event_type,
21435
22051
  metadata: event.metadata ?? {}
21436
22052
  };
22053
+ if (userId) body["user_id"] = userId;
21437
22054
  fetch(`${TELEMETRY_SUPABASE_URL}/rest/v1/telemetry_events`, {
21438
22055
  method: "POST",
21439
22056
  headers: {
@@ -21480,7 +22097,6 @@ var TOOLS_REQUIRING_PAPI = /* @__PURE__ */ new Set([
21480
22097
  "idea",
21481
22098
  "bug",
21482
22099
  "ad_hoc",
21483
- "health",
21484
22100
  "board_reconcile",
21485
22101
  "review_list",
21486
22102
  "review_submit",
@@ -21562,7 +22178,6 @@ function createServer(adapter2, config2) {
21562
22178
  bugTool,
21563
22179
  adHocTool,
21564
22180
  boardReconcileTool,
21565
- healthTool,
21566
22181
  releaseTool,
21567
22182
  reviewListTool,
21568
22183
  reviewSubmitTool,
@@ -21654,9 +22269,6 @@ function createServer(adapter2, config2) {
21654
22269
  case "board_reconcile":
21655
22270
  result = await handleBoardReconcile(adapter2, config2, safeArgs);
21656
22271
  break;
21657
- case "health":
21658
- result = await handleHealth(adapter2);
21659
- break;
21660
22272
  case "release":
21661
22273
  result = await handleRelease(adapter2, config2, safeArgs);
21662
22274
  break;