@papi-ai/server 0.7.5 → 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 +1541 -825
  2. package/dist/prompts.js +48 -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) {
@@ -4747,18 +4791,20 @@ var init_dist3 = __esm({
4747
4791
  }
4748
4792
  async getProject(id) {
4749
4793
  const [row] = await this.sql`
4750
- SELECT * FROM projects WHERE id = ${id}
4794
+ SELECT id, slug, name, repo_url, papi_dir, user_id, created_at, updated_at FROM projects WHERE id = ${id}
4751
4795
  `;
4752
4796
  return row ?? null;
4753
4797
  }
4754
4798
  async listProjects(filter) {
4755
4799
  if (filter?.slug) {
4756
4800
  return this.sql`
4757
- SELECT * FROM projects WHERE slug = ${filter.slug} ORDER BY created_at
4801
+ SELECT id, slug, name, repo_url, papi_dir, user_id, created_at, updated_at FROM projects WHERE slug = ${filter.slug} ORDER BY created_at
4802
+ LIMIT 200 -- bounded: project list per user
4758
4803
  `;
4759
4804
  }
4760
4805
  return this.sql`
4761
- SELECT * FROM projects ORDER BY created_at
4806
+ SELECT id, slug, name, repo_url, papi_dir, user_id, created_at, updated_at FROM projects ORDER BY created_at
4807
+ LIMIT 200 -- bounded: project list per user
4762
4808
  `;
4763
4809
  }
4764
4810
  async updateProject(id, updates) {
@@ -4818,7 +4864,7 @@ var init_dist3 = __esm({
4818
4864
  }
4819
4865
  async getSharedDecision(id) {
4820
4866
  const [row] = await this.sql`
4821
- SELECT * FROM shared_decisions WHERE id = ${id}
4867
+ SELECT id, display_id, title, decision, confidence, status, origin_project_id, evidence, superseded_by_id, created_at, updated_at, archived_at FROM shared_decisions WHERE id = ${id}
4822
4868
  `;
4823
4869
  return row ?? null;
4824
4870
  }
@@ -4838,7 +4884,8 @@ var init_dist3 = __esm({
4838
4884
  }
4839
4885
  if (conditions.length === 0) {
4840
4886
  return this.sql`
4841
- SELECT * FROM shared_decisions WHERE archived_at IS NULL ORDER BY created_at
4887
+ SELECT id, display_id, title, decision, confidence, status, origin_project_id, evidence, superseded_by_id, created_at, updated_at, archived_at FROM shared_decisions WHERE archived_at IS NULL ORDER BY created_at
4888
+ LIMIT 200 -- cross-project shared decisions; 200 is ample
4842
4889
  `;
4843
4890
  }
4844
4891
  let where = conditions[0];
@@ -4846,7 +4893,8 @@ var init_dist3 = __esm({
4846
4893
  where = this.sql`${where} AND ${conditions[i]}`;
4847
4894
  }
4848
4895
  return this.sql`
4849
- SELECT * FROM shared_decisions WHERE ${where} ORDER BY created_at
4896
+ SELECT id, display_id, title, decision, confidence, status, origin_project_id, evidence, superseded_by_id, created_at, updated_at, archived_at FROM shared_decisions WHERE ${where} ORDER BY created_at
4897
+ LIMIT 200
4850
4898
  `;
4851
4899
  }
4852
4900
  async updateSharedDecision(id, updates) {
@@ -4915,7 +4963,7 @@ var init_dist3 = __esm({
4915
4963
  }
4916
4964
  async getAcknowledgement(id) {
4917
4965
  const [row] = await this.sql`
4918
- SELECT * FROM acknowledgements WHERE id = ${id}
4966
+ SELECT id, decision_id, project_id, status, comments, responded_at, created_at, updated_at FROM acknowledgements WHERE id = ${id}
4919
4967
  `;
4920
4968
  return row ?? null;
4921
4969
  }
@@ -4932,7 +4980,8 @@ var init_dist3 = __esm({
4932
4980
  }
4933
4981
  if (conditions.length === 0) {
4934
4982
  return this.sql`
4935
- SELECT * FROM acknowledgements ORDER BY created_at
4983
+ SELECT id, decision_id, project_id, status, comments, responded_at, created_at, updated_at FROM acknowledgements ORDER BY created_at
4984
+ LIMIT 500 -- acknowledgements grow with decisions × projects
4936
4985
  `;
4937
4986
  }
4938
4987
  let where = conditions[0];
@@ -4940,7 +4989,8 @@ var init_dist3 = __esm({
4940
4989
  where = this.sql`${where} AND ${conditions[i]}`;
4941
4990
  }
4942
4991
  return this.sql`
4943
- SELECT * FROM acknowledgements WHERE ${where} ORDER BY created_at
4992
+ SELECT id, decision_id, project_id, status, comments, responded_at, created_at, updated_at FROM acknowledgements WHERE ${where} ORDER BY created_at
4993
+ LIMIT 500
4944
4994
  `;
4945
4995
  }
4946
4996
  // -------------------------------------------------------------------------
@@ -4956,7 +5006,7 @@ var init_dist3 = __esm({
4956
5006
  }
4957
5007
  async getSharedMilestone(id) {
4958
5008
  const [row] = await this.sql`
4959
- SELECT * FROM shared_milestones WHERE id = ${id}
5009
+ SELECT id, title, description, status, target_date, completed_at, created_at, updated_at, archived_at FROM shared_milestones WHERE id = ${id}
4960
5010
  `;
4961
5011
  return row ?? null;
4962
5012
  }
@@ -4970,7 +5020,8 @@ var init_dist3 = __esm({
4970
5020
  }
4971
5021
  if (conditions.length === 0) {
4972
5022
  return this.sql`
4973
- SELECT * FROM shared_milestones WHERE archived_at IS NULL ORDER BY created_at
5023
+ SELECT id, title, description, status, target_date, completed_at, created_at, updated_at, archived_at FROM shared_milestones WHERE archived_at IS NULL ORDER BY created_at
5024
+ LIMIT 200
4974
5025
  `;
4975
5026
  }
4976
5027
  let where = conditions[0];
@@ -4978,7 +5029,8 @@ var init_dist3 = __esm({
4978
5029
  where = this.sql`${where} AND ${conditions[i]}`;
4979
5030
  }
4980
5031
  return this.sql`
4981
- SELECT * FROM shared_milestones WHERE ${where} ORDER BY created_at
5032
+ SELECT id, title, description, status, target_date, completed_at, created_at, updated_at, archived_at FROM shared_milestones WHERE ${where} ORDER BY created_at
5033
+ LIMIT 200
4982
5034
  `;
4983
5035
  }
4984
5036
  async updateSharedMilestone(id, updates) {
@@ -5016,7 +5068,7 @@ var init_dist3 = __esm({
5016
5068
  }
5017
5069
  async listMilestoneDependencies(milestoneId) {
5018
5070
  return this.sql`
5019
- SELECT * FROM milestone_dependencies WHERE milestone_id = ${milestoneId}
5071
+ SELECT id, milestone_id, depends_on_id FROM milestone_dependencies WHERE milestone_id = ${milestoneId}
5020
5072
  `;
5021
5073
  }
5022
5074
  async deleteMilestoneDependency(id) {
@@ -5045,7 +5097,7 @@ var init_dist3 = __esm({
5045
5097
  }
5046
5098
  async getProjectContribution(id) {
5047
5099
  const [row] = await this.sql`
5048
- SELECT * FROM project_contributions WHERE id = ${id}
5100
+ SELECT id, milestone_id, project_id, status, required_phases, notes, delivered_at, created_at, updated_at FROM project_contributions WHERE id = ${id}
5049
5101
  `;
5050
5102
  return row ?? null;
5051
5103
  }
@@ -5062,7 +5114,8 @@ var init_dist3 = __esm({
5062
5114
  }
5063
5115
  if (conditions.length === 0) {
5064
5116
  return this.sql`
5065
- SELECT * FROM project_contributions ORDER BY created_at
5117
+ SELECT id, milestone_id, project_id, status, required_phases, notes, delivered_at, created_at, updated_at FROM project_contributions ORDER BY created_at
5118
+ LIMIT 500
5066
5119
  `;
5067
5120
  }
5068
5121
  let where = conditions[0];
@@ -5070,7 +5123,8 @@ var init_dist3 = __esm({
5070
5123
  where = this.sql`${where} AND ${conditions[i]}`;
5071
5124
  }
5072
5125
  return this.sql`
5073
- SELECT * FROM project_contributions WHERE ${where} ORDER BY created_at
5126
+ SELECT id, milestone_id, project_id, status, required_phases, notes, delivered_at, created_at, updated_at FROM project_contributions WHERE ${where} ORDER BY created_at
5127
+ LIMIT 500
5074
5128
  `;
5075
5129
  }
5076
5130
  async updateProjectContribution(id, updates) {
@@ -5108,7 +5162,7 @@ var init_dist3 = __esm({
5108
5162
  }
5109
5163
  async getActiveNorthStar(projectId) {
5110
5164
  const [row] = await this.sql`
5111
- SELECT * FROM north_stars
5165
+ SELECT id, project_id, statement, set_at, superseded_by_id, superseded_at, created_at FROM north_stars
5112
5166
  WHERE project_id = ${projectId}
5113
5167
  AND superseded_by_id IS NULL
5114
5168
  ORDER BY created_at DESC
@@ -5126,7 +5180,8 @@ var init_dist3 = __esm({
5126
5180
  }
5127
5181
  if (conditions.length === 0) {
5128
5182
  return this.sql`
5129
- SELECT * FROM north_stars ORDER BY created_at
5183
+ SELECT id, project_id, statement, set_at, superseded_by_id, superseded_at, created_at FROM north_stars ORDER BY created_at
5184
+ LIMIT 200
5130
5185
  `;
5131
5186
  }
5132
5187
  let where = conditions[0];
@@ -5134,7 +5189,8 @@ var init_dist3 = __esm({
5134
5189
  where = this.sql`${where} AND ${conditions[i]}`;
5135
5190
  }
5136
5191
  return this.sql`
5137
- SELECT * FROM north_stars WHERE ${where} ORDER BY created_at
5192
+ SELECT id, project_id, statement, set_at, superseded_by_id, superseded_at, created_at FROM north_stars WHERE ${where} ORDER BY created_at
5193
+ LIMIT 200
5138
5194
  `;
5139
5195
  }
5140
5196
  /**
@@ -5148,7 +5204,7 @@ var init_dist3 = __esm({
5148
5204
  return this.sql.begin(async (_tx) => {
5149
5205
  const tx = _tx;
5150
5206
  const [old] = await tx`
5151
- SELECT * FROM north_stars WHERE id = ${id}
5207
+ SELECT id, project_id, statement, set_at, superseded_by_id, superseded_at, created_at FROM north_stars WHERE id = ${id}
5152
5208
  `;
5153
5209
  if (!old) {
5154
5210
  throw new Error(`NorthStar not found: ${id}`);
@@ -5191,7 +5247,7 @@ var init_dist3 = __esm({
5191
5247
  }
5192
5248
  async getConflictAlert(id) {
5193
5249
  const [row] = await this.sql`
5194
- SELECT * FROM conflict_alerts WHERE id = ${id}
5250
+ SELECT id, conflict_type, title, description, status, decision_a_id, decision_b_id, north_star_id, resolution_decision_id, raised_by, created_at, updated_at, resolved_at FROM conflict_alerts WHERE id = ${id}
5195
5251
  `;
5196
5252
  return row ?? null;
5197
5253
  }
@@ -5205,7 +5261,8 @@ var init_dist3 = __esm({
5205
5261
  }
5206
5262
  if (conditions.length === 0) {
5207
5263
  return this.sql`
5208
- SELECT * FROM conflict_alerts ORDER BY created_at
5264
+ SELECT id, conflict_type, title, description, status, decision_a_id, decision_b_id, north_star_id, resolution_decision_id, raised_by, created_at, updated_at, resolved_at FROM conflict_alerts ORDER BY created_at
5265
+ LIMIT 200
5209
5266
  `;
5210
5267
  }
5211
5268
  let where = conditions[0];
@@ -5213,7 +5270,8 @@ var init_dist3 = __esm({
5213
5270
  where = this.sql`${where} AND ${conditions[i]}`;
5214
5271
  }
5215
5272
  return this.sql`
5216
- SELECT * FROM conflict_alerts WHERE ${where} ORDER BY created_at
5273
+ SELECT id, conflict_type, title, description, status, decision_a_id, decision_b_id, north_star_id, resolution_decision_id, raised_by, created_at, updated_at, resolved_at FROM conflict_alerts WHERE ${where} ORDER BY created_at
5274
+ LIMIT 200
5217
5275
  `;
5218
5276
  }
5219
5277
  async updateConflictAlert(id, updates) {
@@ -6080,6 +6138,22 @@ EXCEPTION WHEN duplicate_object THEN NULL; END $$;
6080
6138
  `;
6081
6139
  return rows.length > 0 ? { setCycle: rows[0].set_cycle, setAt: rows[0].set_at } : null;
6082
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
+ }
6083
6157
  async getEstimationCalibration() {
6084
6158
  const rows = await this.sql`
6085
6159
  SELECT estimated_effort, actual_effort, accuracy_label, COUNT(*)::text AS count
@@ -6176,19 +6250,23 @@ EXCEPTION WHEN duplicate_object THEN NULL; END $$;
6176
6250
  }
6177
6251
  async getActiveDecisions() {
6178
6252
  const rows = await this.sql`
6179
- SELECT * FROM active_decisions
6253
+ SELECT id, display_id, title, confidence, superseded, superseded_by, created_cycle, modified_cycle, body, outcome, revision_count
6254
+ FROM active_decisions
6180
6255
  WHERE project_id = ${this.projectId}
6181
6256
  ORDER BY display_id
6257
+ LIMIT 200 -- bounded: ADs are bounded by project lifecycle, 200 is a safe ceiling
6182
6258
  `;
6183
6259
  return rows.map(rowToActiveDecision);
6184
6260
  }
6185
6261
  async getSiblingAds(projectIds) {
6186
6262
  if (projectIds.length === 0) return [];
6187
6263
  const rows = await this.sql`
6188
- SELECT *, project_id FROM active_decisions
6264
+ SELECT id, display_id, title, confidence, superseded, superseded_by, created_cycle, modified_cycle, body, outcome, revision_count, project_id
6265
+ FROM active_decisions
6189
6266
  WHERE project_id = ANY(${projectIds}::uuid[])
6190
6267
  AND superseded = false
6191
6268
  ORDER BY project_id, display_id
6269
+ LIMIT 500 -- cross-project query: 500 covers up to 10 projects × 50 ADs each
6192
6270
  `;
6193
6271
  return rows.map((row) => ({
6194
6272
  ...rowToActiveDecision(row),
@@ -6198,7 +6276,8 @@ EXCEPTION WHEN duplicate_object THEN NULL; END $$;
6198
6276
  async getCycleLog(limit) {
6199
6277
  if (limit != null) {
6200
6278
  const rows2 = await this.sql`
6201
- SELECT * FROM planning_log_entries
6279
+ SELECT id, cycle_number, title, content, carry_forward, notes, task_count, effort_points
6280
+ FROM planning_log_entries
6202
6281
  WHERE project_id = ${this.projectId}
6203
6282
  ORDER BY cycle_number DESC
6204
6283
  LIMIT ${limit}
@@ -6206,18 +6285,22 @@ EXCEPTION WHEN duplicate_object THEN NULL; END $$;
6206
6285
  return rows2.map(rowToCycleLogEntry);
6207
6286
  }
6208
6287
  const rows = await this.sql`
6209
- SELECT * FROM planning_log_entries
6288
+ SELECT id, cycle_number, title, content, carry_forward, notes, task_count, effort_points
6289
+ FROM planning_log_entries
6210
6290
  WHERE project_id = ${this.projectId}
6211
6291
  ORDER BY cycle_number DESC
6292
+ LIMIT 500 -- 500 cycles is ~years of history, sufficient ceiling
6212
6293
  `;
6213
6294
  return rows.map(rowToCycleLogEntry);
6214
6295
  }
6215
6296
  async getCycleLogSince(cycleNumber) {
6216
6297
  const rows = await this.sql`
6217
- SELECT * FROM planning_log_entries
6298
+ SELECT id, cycle_number, title, content, carry_forward, notes, task_count, effort_points
6299
+ FROM planning_log_entries
6218
6300
  WHERE project_id = ${this.projectId}
6219
6301
  AND cycle_number >= ${cycleNumber}
6220
6302
  ORDER BY cycle_number DESC
6303
+ LIMIT 500 -- bounded by cycle range, 500 is a safe ceiling
6221
6304
  `;
6222
6305
  return rows.map(rowToCycleLogEntry);
6223
6306
  }
@@ -6449,7 +6532,8 @@ ${newParts.join("\n")}` : newParts.join("\n");
6449
6532
  const sinceCycle = input.sinceCycle ?? 0;
6450
6533
  const hasPending = input.hasPendingActions ?? false;
6451
6534
  const rows = await this.sql`
6452
- SELECT * FROM doc_registry
6535
+ SELECT id, title, type, path, status, summary, tags, cycle_created, cycle_updated, superseded_by, actions, created_at, updated_at
6536
+ FROM doc_registry
6453
6537
  WHERE project_id = ${this.projectId}
6454
6538
  AND (${matchAllStatuses} OR status = ${status})
6455
6539
  AND (${input.type ?? null}::text IS NULL OR type = ${input.type ?? null})
@@ -6479,9 +6563,11 @@ ${newParts.join("\n")}` : newParts.join("\n");
6479
6563
  async getDoc(idOrPath) {
6480
6564
  const isUuid = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(idOrPath);
6481
6565
  const rows = isUuid ? await this.sql`
6482
- SELECT * FROM doc_registry WHERE id = ${idOrPath} AND project_id = ${this.projectId}
6566
+ SELECT id, title, type, path, status, summary, tags, cycle_created, cycle_updated, superseded_by, actions, created_at, updated_at
6567
+ FROM doc_registry WHERE id = ${idOrPath} AND project_id = ${this.projectId}
6483
6568
  ` : await this.sql`
6484
- SELECT * FROM doc_registry WHERE path = ${idOrPath} AND project_id = ${this.projectId}
6569
+ SELECT id, title, type, path, status, summary, tags, cycle_created, cycle_updated, superseded_by, actions, created_at, updated_at
6570
+ FROM doc_registry WHERE path = ${idOrPath} AND project_id = ${this.projectId}
6485
6571
  `;
6486
6572
  if (rows.length === 0) return null;
6487
6573
  const r = rows[0];
@@ -6640,9 +6726,11 @@ ${newParts.join("\n")}` : newParts.join("\n");
6640
6726
  async queryBoard(options) {
6641
6727
  if (!options) {
6642
6728
  const rows2 = await this.sql`
6643
- SELECT * FROM cycle_tasks
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
6730
+ FROM cycle_tasks
6644
6731
  WHERE project_id = ${this.projectId}
6645
6732
  ORDER BY display_id
6733
+ LIMIT 2000 -- hard ceiling; single project task count won't approach this
6646
6734
  `;
6647
6735
  return rows2.map(rowToTask);
6648
6736
  }
@@ -6679,22 +6767,28 @@ ${newParts.join("\n")}` : newParts.join("\n");
6679
6767
  where = this.sql`${where} AND ${conditions[i]}`;
6680
6768
  }
6681
6769
  const rows = await this.sql`
6682
- SELECT * FROM cycle_tasks WHERE ${where} ORDER BY display_id
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
6771
+ FROM cycle_tasks WHERE ${where} ORDER BY display_id
6772
+ LIMIT 2000 -- matches no-options path ceiling
6683
6773
  `;
6684
6774
  return rows.map(rowToTask);
6685
6775
  }
6686
6776
  async getTask(id) {
6687
6777
  const [row] = await this.sql`
6688
- SELECT * FROM cycle_tasks
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
6779
+ FROM cycle_tasks
6689
6780
  WHERE project_id = ${this.projectId} AND display_id = ${id}
6781
+ LIMIT 1
6690
6782
  `;
6691
6783
  return row ? rowToTask(row) : null;
6692
6784
  }
6693
6785
  async getTasks(ids) {
6694
6786
  if (ids.length === 0) return [];
6695
6787
  const rows = await this.sql`
6696
- SELECT * FROM cycle_tasks
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
6789
+ FROM cycle_tasks
6697
6790
  WHERE project_id = ${this.projectId} AND display_id = ANY(${ids})
6791
+ LIMIT 2000 -- matches board ceiling; ids[] won't exceed this in practice
6698
6792
  `;
6699
6793
  return rows.map(rowToTask);
6700
6794
  }
@@ -6711,7 +6805,7 @@ ${newParts.join("\n")}` : newParts.join("\n");
6711
6805
  project_id, display_id, title, status, priority, complexity,
6712
6806
  module, epic, phase, owner, reviewed, cycle, created_cycle,
6713
6807
  why, depends_on, notes, closure_reason, state_history,
6714
- 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
6715
6809
  ) VALUES (
6716
6810
  ${this.projectId}, ${displayId}, ${task.title}, ${task.status}, ${task.priority},
6717
6811
  ${normaliseComplexity(task.complexity)}, ${task.module}, ${task.epic ?? null}, ${task.phase}, ${task.owner},
@@ -6725,7 +6819,8 @@ ${newParts.join("\n")}` : newParts.join("\n");
6725
6819
  ${task.maturity ?? null},
6726
6820
  ${task.stageId ?? null},
6727
6821
  ${task.docRef ?? null},
6728
- ${task.source ?? null}
6822
+ ${task.source ?? null},
6823
+ ${task.opportunity ?? null}
6729
6824
  )
6730
6825
  RETURNING *
6731
6826
  `;
@@ -6757,6 +6852,7 @@ ${newParts.join("\n")}` : newParts.join("\n");
6757
6852
  if (updates.stageId !== void 0) columnMap["stage_id"] = updates.stageId;
6758
6853
  if (updates.docRef !== void 0) columnMap["doc_ref"] = updates.docRef;
6759
6854
  if (updates.source !== void 0) columnMap["source"] = updates.source;
6855
+ if (updates.opportunity !== void 0) columnMap["opportunity"] = updates.opportunity;
6760
6856
  const keys = Object.keys(columnMap);
6761
6857
  if (keys.length === 0) return;
6762
6858
  await this.sql`
@@ -6804,7 +6900,8 @@ ${newParts.join("\n")}` : newParts.join("\n");
6804
6900
  completed, actual_effort, estimated_effort, scope_accuracy,
6805
6901
  surprises, discovered_issues, architecture_notes,
6806
6902
  commit_sha, files_changed, related_decisions, handoff_accuracy,
6807
- corrections_count, brief_implications, dead_ends
6903
+ corrections_count, brief_implications, dead_ends,
6904
+ started_at, completed_at, tool_call_count
6808
6905
  ) VALUES (
6809
6906
  ${this.projectId}, ${displayId}, ${report.taskId}, ${report.taskName},
6810
6907
  ${report.date}, ${report.cycle}, ${report.completed},
@@ -6814,13 +6911,15 @@ ${newParts.join("\n")}` : newParts.join("\n");
6814
6911
  ${report.handoffAccuracy ? this.sql.json(report.handoffAccuracy) : null},
6815
6912
  ${report.correctionsCount ?? 0},
6816
6913
  ${report.briefImplications ? this.sql.json(report.briefImplications) : null},
6817
- ${report.deadEnds ?? null}
6914
+ ${report.deadEnds ?? null},
6915
+ ${report.startedAt ?? null}, ${report.completedAt ?? null}, ${report.toolCallCount ?? 0}
6818
6916
  )
6819
6917
  `;
6820
6918
  }
6821
6919
  async getRecentBuildReports(count) {
6822
6920
  const rows = await this.sql`
6823
- SELECT * FROM build_reports
6921
+ SELECT id, display_id, task_id, task_name, date, cycle, completed, actual_effort, estimated_effort, scope_accuracy, surprises, discovered_issues, architecture_notes, commit_sha, files_changed, related_decisions, corrections_count, handoff_accuracy, brief_implications, dead_ends, created_at
6922
+ FROM build_reports
6824
6923
  WHERE project_id = ${this.projectId}
6825
6924
  ORDER BY created_at DESC
6826
6925
  LIMIT ${count}
@@ -6836,9 +6935,11 @@ ${newParts.join("\n")}` : newParts.join("\n");
6836
6935
  }
6837
6936
  async getBuildReportsSince(cycleNumber) {
6838
6937
  const rows = await this.sql`
6839
- SELECT * FROM build_reports
6938
+ SELECT id, display_id, task_id, task_name, date, cycle, completed, actual_effort, estimated_effort, scope_accuracy, surprises, discovered_issues, architecture_notes, commit_sha, files_changed, related_decisions, corrections_count, handoff_accuracy, brief_implications, dead_ends, created_at
6939
+ FROM build_reports
6840
6940
  WHERE project_id = ${this.projectId} AND cycle >= ${cycleNumber}
6841
6941
  ORDER BY created_at
6942
+ LIMIT 1000 -- bounded by cycle range; 1000 covers ~200 cycles × 5 tasks
6842
6943
  `;
6843
6944
  return rows.map(rowToBuildReport);
6844
6945
  }
@@ -6865,25 +6966,29 @@ ${newParts.join("\n")}` : newParts.join("\n");
6865
6966
  let rows;
6866
6967
  if (opts?.cycleNumber && opts?.category) {
6867
6968
  rows = await this.sql`
6868
- SELECT * FROM cycle_learnings
6969
+ SELECT id, project_id, task_id, cycle_number, category, severity, summary, detail, tags, related_decision, action_taken, action_ref, created_at
6970
+ FROM cycle_learnings
6869
6971
  WHERE project_id = ${this.projectId} AND cycle_number = ${opts.cycleNumber} AND category = ${opts.category}
6870
6972
  ORDER BY created_at DESC LIMIT ${limit}
6871
6973
  `;
6872
6974
  } else if (opts?.cycleNumber) {
6873
6975
  rows = await this.sql`
6874
- SELECT * FROM cycle_learnings
6976
+ SELECT id, project_id, task_id, cycle_number, category, severity, summary, detail, tags, related_decision, action_taken, action_ref, created_at
6977
+ FROM cycle_learnings
6875
6978
  WHERE project_id = ${this.projectId} AND cycle_number = ${opts.cycleNumber}
6876
6979
  ORDER BY created_at DESC LIMIT ${limit}
6877
6980
  `;
6878
6981
  } else if (opts?.category) {
6879
6982
  rows = await this.sql`
6880
- SELECT * FROM cycle_learnings
6983
+ SELECT id, project_id, task_id, cycle_number, category, severity, summary, detail, tags, related_decision, action_taken, action_ref, created_at
6984
+ FROM cycle_learnings
6881
6985
  WHERE project_id = ${this.projectId} AND category = ${opts.category}
6882
6986
  ORDER BY created_at DESC LIMIT ${limit}
6883
6987
  `;
6884
6988
  } else {
6885
6989
  rows = await this.sql`
6886
- SELECT * FROM cycle_learnings
6990
+ SELECT id, project_id, task_id, cycle_number, category, severity, summary, detail, tags, related_decision, action_taken, action_ref, created_at
6991
+ FROM cycle_learnings
6887
6992
  WHERE project_id = ${this.projectId}
6888
6993
  ORDER BY created_at DESC LIMIT ${limit}
6889
6994
  `;
@@ -6937,7 +7042,8 @@ ${newParts.join("\n")}` : newParts.join("\n");
6937
7042
  async getRecentReviews(count) {
6938
7043
  const limit = count ?? 20;
6939
7044
  const rows = await this.sql`
6940
- SELECT * FROM reviews
7045
+ SELECT id, display_id, task_id, stage, reviewer, verdict, cycle, date, comments, handoff_revision, build_commit_sha, auto_review
7046
+ FROM reviews
6941
7047
  WHERE project_id = ${this.projectId}
6942
7048
  ORDER BY created_at DESC
6943
7049
  LIMIT ${limit}
@@ -7071,9 +7177,11 @@ ${newParts.join("\n")}` : newParts.join("\n");
7071
7177
  // -------------------------------------------------------------------------
7072
7178
  async readPhases() {
7073
7179
  const rows = await this.sql`
7074
- SELECT * FROM phases
7180
+ SELECT id, slug, label, description, status, sort_order, stage_id
7181
+ FROM phases
7075
7182
  WHERE project_id = ${this.projectId}
7076
7183
  ORDER BY sort_order
7184
+ LIMIT 200 -- phases are bounded by project structure
7077
7185
  `;
7078
7186
  return rows.map(rowToPhase);
7079
7187
  }
@@ -7123,33 +7231,48 @@ ${newParts.join("\n")}` : newParts.join("\n");
7123
7231
  INSERT INTO tool_call_metrics (
7124
7232
  project_id, timestamp, tool, duration_ms,
7125
7233
  input_tokens, output_tokens, estimated_cost_usd, model, cycle_number,
7126
- context_bytes, context_utilisation
7234
+ context_bytes, context_utilisation, success
7127
7235
  ) VALUES (
7128
7236
  ${this.projectId}, ${metric.timestamp}, ${metric.tool}, ${metric.durationMs},
7129
7237
  ${metric.inputTokens ?? null}, ${metric.outputTokens ?? null},
7130
7238
  ${metric.estimatedCostUsd ?? null}, ${metric.model ?? null},
7131
7239
  ${metric.cycleNumber ?? null},
7132
- ${metric.contextBytes ?? null}, ${metric.contextUtilisation ?? null}
7240
+ ${metric.contextBytes ?? null}, ${metric.contextUtilisation ?? null},
7241
+ ${metric.success ?? true}
7133
7242
  )
7134
7243
  `;
7135
7244
  }
7136
7245
  async readToolMetrics() {
7137
7246
  const rows = await this.sql`
7138
- SELECT * FROM tool_call_metrics
7247
+ SELECT timestamp, tool, duration_ms, input_tokens, output_tokens, estimated_cost_usd, model, cycle_number, context_bytes, context_utilisation
7248
+ FROM tool_call_metrics
7139
7249
  WHERE project_id = ${this.projectId}
7140
7250
  ORDER BY timestamp
7251
+ LIMIT 5000 -- metrics are high-volume; task-1189 adds pagination for dashboard
7141
7252
  `;
7142
7253
  return rows.map(rowToToolCallMetric);
7143
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
+ }
7144
7265
  // -------------------------------------------------------------------------
7145
7266
  // Cost Summary
7146
7267
  // -------------------------------------------------------------------------
7147
7268
  async getCostSummary(cycleNumber) {
7148
7269
  const metrics = cycleNumber != null ? await this.sql`
7149
- SELECT * FROM tool_call_metrics
7270
+ SELECT timestamp, tool, duration_ms, input_tokens, output_tokens, estimated_cost_usd, model, cycle_number, context_bytes, context_utilisation
7271
+ FROM tool_call_metrics
7150
7272
  WHERE project_id = ${this.projectId} AND cycle_number = ${cycleNumber}
7151
7273
  ` : await this.sql`
7152
- SELECT * FROM tool_call_metrics
7274
+ SELECT timestamp, tool, duration_ms, input_tokens, output_tokens, estimated_cost_usd, model, cycle_number, context_bytes, context_utilisation
7275
+ FROM tool_call_metrics
7153
7276
  WHERE project_id = ${this.projectId}
7154
7277
  `;
7155
7278
  let totalCostUsd = 0;
@@ -7186,7 +7309,8 @@ ${newParts.join("\n")}` : newParts.join("\n");
7186
7309
  }
7187
7310
  async getCostSnapshots() {
7188
7311
  const rows = await this.sql`
7189
- SELECT * FROM cost_snapshots
7312
+ SELECT cycle, date, total_cost_usd, total_input_tokens, total_output_tokens, total_calls
7313
+ FROM cost_snapshots
7190
7314
  WHERE project_id = ${this.projectId}
7191
7315
  ORDER BY cycle
7192
7316
  `;
@@ -7213,9 +7337,11 @@ ${newParts.join("\n")}` : newParts.join("\n");
7213
7337
  }
7214
7338
  async readCycleMetrics() {
7215
7339
  const rows = await this.sql`
7216
- SELECT * FROM cycle_metrics_snapshots
7340
+ SELECT cycle, date, accuracy, velocity
7341
+ FROM cycle_metrics_snapshots
7217
7342
  WHERE project_id = ${this.projectId}
7218
7343
  ORDER BY cycle
7344
+ LIMIT 500 -- one row per cycle; 500 is ~years of history
7219
7345
  `;
7220
7346
  return rows.map(rowToCycleMetrics);
7221
7347
  }
@@ -7224,9 +7350,11 @@ ${newParts.join("\n")}` : newParts.join("\n");
7224
7350
  // -------------------------------------------------------------------------
7225
7351
  async readCycles() {
7226
7352
  const rows = await this.sql`
7227
- SELECT * FROM cycles
7353
+ SELECT id, number, status, start_date, end_date, goals, board_health, task_ids
7354
+ FROM cycles
7228
7355
  WHERE project_id = ${this.projectId}
7229
7356
  ORDER BY number
7357
+ LIMIT 500 -- one row per cycle; 500 is years of history
7230
7358
  `;
7231
7359
  return rows.map(rowToCycle);
7232
7360
  }
@@ -7273,25 +7401,31 @@ ${newParts.join("\n")}` : newParts.join("\n");
7273
7401
  // -------------------------------------------------------------------------
7274
7402
  async readHorizons() {
7275
7403
  const rows = await this.sql`
7276
- SELECT * FROM horizons
7404
+ SELECT id, slug, label, description, status, sort_order, project_id, created_at, updated_at
7405
+ FROM horizons
7277
7406
  WHERE project_id = ${this.projectId}
7278
7407
  ORDER BY sort_order, created_at
7408
+ LIMIT 50 -- horizons are high-level; 50 is an ample ceiling
7279
7409
  `;
7280
7410
  return rows.map(rowToHorizon);
7281
7411
  }
7282
7412
  async readStages(horizonId) {
7283
7413
  if (horizonId) {
7284
7414
  const rows2 = await this.sql`
7285
- SELECT * FROM stages
7415
+ SELECT id, slug, label, description, status, sort_order, horizon_id, project_id, exit_criteria, created_at, updated_at
7416
+ FROM stages
7286
7417
  WHERE project_id = ${this.projectId} AND horizon_id = ${horizonId}
7287
7418
  ORDER BY sort_order, created_at
7419
+ LIMIT 100 -- stages per horizon are bounded; 100 is ample
7288
7420
  `;
7289
7421
  return rows2.map(rowToStage);
7290
7422
  }
7291
7423
  const rows = await this.sql`
7292
- SELECT * FROM stages
7424
+ SELECT id, slug, label, description, status, sort_order, horizon_id, project_id, exit_criteria, created_at, updated_at
7425
+ FROM stages
7293
7426
  WHERE project_id = ${this.projectId}
7294
7427
  ORDER BY sort_order, created_at
7428
+ LIMIT 200 -- all stages across horizons; 200 is a safe ceiling
7295
7429
  `;
7296
7430
  return rows.map(rowToStage);
7297
7431
  }
@@ -7321,7 +7455,8 @@ ${newParts.join("\n")}` : newParts.join("\n");
7321
7455
  }
7322
7456
  async getActiveStage() {
7323
7457
  const rows = await this.sql`
7324
- SELECT * FROM stages
7458
+ SELECT id, slug, label, description, status, sort_order, horizon_id, project_id, exit_criteria, created_at, updated_at
7459
+ FROM stages
7325
7460
  WHERE project_id = ${this.projectId} AND status = 'Active'
7326
7461
  ORDER BY sort_order
7327
7462
  LIMIT 1
@@ -7433,7 +7568,8 @@ ${newParts.join("\n")}` : newParts.join("\n");
7433
7568
  }
7434
7569
  async getDecisionEvents(decisionId, limit) {
7435
7570
  const rows = await this.sql`
7436
- SELECT * FROM decision_events
7571
+ SELECT id, decision_id, event_type, cycle, source, source_ref, detail, created_at
7572
+ FROM decision_events
7437
7573
  WHERE project_id = ${this.projectId} AND decision_id = ${decisionId}
7438
7574
  ORDER BY created_at DESC
7439
7575
  LIMIT ${limit ?? 50}
@@ -7442,9 +7578,11 @@ ${newParts.join("\n")}` : newParts.join("\n");
7442
7578
  }
7443
7579
  async getDecisionEventsSince(cycle) {
7444
7580
  const rows = await this.sql`
7445
- SELECT * FROM decision_events
7581
+ SELECT id, decision_id, event_type, cycle, source, source_ref, detail, created_at
7582
+ FROM decision_events
7446
7583
  WHERE project_id = ${this.projectId} AND cycle >= ${cycle}
7447
7584
  ORDER BY created_at
7585
+ LIMIT 1000 -- bounded by cycle range; 1000 is ample
7448
7586
  `;
7449
7587
  return rows.map(rowToDecisionEvent);
7450
7588
  }
@@ -7471,7 +7609,8 @@ ${newParts.join("\n")}` : newParts.join("\n");
7471
7609
  }
7472
7610
  async getDecisionScores(decisionId) {
7473
7611
  const rows = await this.sql`
7474
- SELECT * FROM decision_scores
7612
+ SELECT id, decision_id, cycle, effort, risk, reversibility, scale_cost, lock_in, total_score, rationale, created_at
7613
+ FROM decision_scores
7475
7614
  WHERE project_id = ${this.projectId} AND decision_id = ${decisionId}
7476
7615
  ORDER BY cycle
7477
7616
  `;
@@ -7479,7 +7618,7 @@ ${newParts.join("\n")}` : newParts.join("\n");
7479
7618
  }
7480
7619
  async getLatestDecisionScores() {
7481
7620
  const rows = await this.sql`
7482
- SELECT DISTINCT ON (decision_id) *
7621
+ SELECT DISTINCT ON (decision_id) id, decision_id, cycle, effort, risk, reversibility, scale_cost, lock_in, total_score, rationale, created_at
7483
7622
  FROM decision_scores
7484
7623
  WHERE project_id = ${this.projectId}
7485
7624
  ORDER BY decision_id, cycle DESC
@@ -7862,7 +8001,8 @@ ${newParts.join("\n")}` : newParts.join("\n");
7862
8001
  build_report: task.buildReport ?? null,
7863
8002
  task_type: task.taskType ?? null,
7864
8003
  maturity: task.maturity ?? null,
7865
- stage_id: task.stageId ?? null
8004
+ stage_id: task.stageId ?? null,
8005
+ opportunity: task.opportunity ?? null
7866
8006
  };
7867
8007
  });
7868
8008
  const taskCols = [
@@ -7888,7 +8028,8 @@ ${newParts.join("\n")}` : newParts.join("\n");
7888
8028
  "build_report",
7889
8029
  "task_type",
7890
8030
  "maturity",
7891
- "stage_id"
8031
+ "stage_id",
8032
+ "opportunity"
7892
8033
  ];
7893
8034
  const insertedRows = await tx`
7894
8035
  INSERT INTO cycle_tasks ${tx(taskRows, ...taskCols)}
@@ -8394,6 +8535,9 @@ Check PAPI_PROJECT_ID in your .mcp.json config. Find your project ID in the PAPI
8394
8535
  readToolMetrics() {
8395
8536
  return this.invoke("readToolMetrics");
8396
8537
  }
8538
+ insertPlanRun(entry) {
8539
+ return this.invoke("insertPlanRun", [entry]);
8540
+ }
8397
8541
  getCostSummary(cycleNumber) {
8398
8542
  return this.invoke("getCostSummary", [cycleNumber]);
8399
8543
  }
@@ -8497,6 +8641,9 @@ Check PAPI_PROJECT_ID in your .mcp.json config. Find your project ID in the PAPI
8497
8641
  getNorthStarStaleness() {
8498
8642
  return this.invoke("getNorthStarStaleness");
8499
8643
  }
8644
+ upsertNorthStar(statement, cycleNumber) {
8645
+ return this.invoke("upsertNorthStar", [statement, cycleNumber]);
8646
+ }
8500
8647
  // --- Optional pg-only methods ---
8501
8648
  getEstimationCalibration() {
8502
8649
  return this.invoke("getEstimationCalibration");
@@ -8520,6 +8667,46 @@ Check PAPI_PROJECT_ID in your .mcp.json config. Find your project ID in the PAPI
8520
8667
  submitBugReport(report) {
8521
8668
  return this.invoke("submitBugReport", [report]);
8522
8669
  }
8670
+ // --- Doc Registry ---
8671
+ registerDoc(entry) {
8672
+ return this.invoke("registerDoc", [entry]);
8673
+ }
8674
+ searchDocs(input) {
8675
+ return this.invoke("searchDocs", [input]);
8676
+ }
8677
+ getDoc(idOrPath) {
8678
+ return this.invoke("getDoc", [idOrPath]);
8679
+ }
8680
+ updateDocStatus(id, status, supersededBy) {
8681
+ return this.invoke("updateDocStatus", [id, status, supersededBy]);
8682
+ }
8683
+ // --- Cycle Learnings ---
8684
+ appendCycleLearnings(learnings) {
8685
+ return this.invoke("appendCycleLearnings", [learnings]);
8686
+ }
8687
+ getCycleLearnings(opts) {
8688
+ return this.invoke("getCycleLearnings", [opts]);
8689
+ }
8690
+ getCycleLearningPatterns() {
8691
+ return this.invoke("getCycleLearningPatterns", []);
8692
+ }
8693
+ updateCycleLearningActionRef(learningId, taskDisplayId) {
8694
+ return this.invoke("updateCycleLearningActionRef", [learningId, taskDisplayId]);
8695
+ }
8696
+ // --- Strategy Review Drafts ---
8697
+ savePendingReviewResponse(cycleNumber, rawResponse) {
8698
+ return this.invoke("savePendingReviewResponse", [cycleNumber, rawResponse]);
8699
+ }
8700
+ getPendingReviewResponse() {
8701
+ return this.invoke("getPendingReviewResponse", []);
8702
+ }
8703
+ clearPendingReviewResponse() {
8704
+ return this.invoke("clearPendingReviewResponse", []);
8705
+ }
8706
+ // --- Active Decisions ---
8707
+ confirmPendingActiveDecisions(cycleNumber) {
8708
+ return this.invoke("confirmPendingActiveDecisions", [cycleNumber]);
8709
+ }
8523
8710
  // --- Atomic plan write-back ---
8524
8711
  async planWriteBack(payload) {
8525
8712
  const raw = await this.invoke("planWriteBack", [payload]);
@@ -8539,6 +8726,7 @@ __export(git_exports, {
8539
8726
  createAndCheckoutBranch: () => createAndCheckoutBranch,
8540
8727
  createPullRequest: () => createPullRequest,
8541
8728
  createTag: () => createTag,
8729
+ cycleBranchName: () => cycleBranchName,
8542
8730
  deleteLocalBranch: () => deleteLocalBranch,
8543
8731
  detectBoardMismatches: () => detectBoardMismatches,
8544
8732
  detectUnrecordedCommits: () => detectUnrecordedCommits,
@@ -8975,8 +9163,10 @@ function detectUnrecordedCommits(cwd, baseBranch) {
8975
9163
  // release commits
8976
9164
  /^[a-f0-9]+ Merge /,
8977
9165
  // merge commits from PRs
8978
- /chore\(task-/
9166
+ /chore\(task-/,
8979
9167
  // task-related housekeeping
9168
+ /chore: dogfood log/
9169
+ // automated dogfood log entries post-release
8980
9170
  ];
8981
9171
  return output.split("\n").filter((line) => line.trim() && !CYCLE_PATTERNS.some((p) => p.test(line))).map((line) => {
8982
9172
  const spaceIdx = line.indexOf(" ");
@@ -8992,6 +9182,9 @@ function detectUnrecordedCommits(cwd, baseBranch) {
8992
9182
  function taskBranchName(taskId) {
8993
9183
  return `feat/${taskId}`;
8994
9184
  }
9185
+ function cycleBranchName(cycleNumber, module) {
9186
+ return `feat/cycle-${cycleNumber}-${module.toLowerCase().replace(/\s+/g, "-")}`;
9187
+ }
8995
9188
  function getHeadCommitSha(cwd) {
8996
9189
  try {
8997
9190
  return execFileSync("git", ["rev-parse", "HEAD"], { cwd, encoding: "utf-8" }).trim() || null;
@@ -9426,7 +9619,7 @@ function formatDetailedTask(t) {
9426
9619
  return `- **${t.id}:** ${t.title}
9427
9620
  Status: ${t.status} | Priority: ${t.priority} | Complexity: ${t.complexity}${typeTag}
9428
9621
  Module: ${t.module} | Epic: ${t.epic} | Phase: ${t.phase} | Owner: ${t.owner}
9429
- 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 ? `
9430
9623
  Notes: ${notes}` : ""}`;
9431
9624
  }
9432
9625
  function formatBoardForPlan(tasks, filters, currentCycle) {
@@ -9833,6 +10026,191 @@ function logDataSourceSummary(service, sources) {
9833
10026
  console.error(`[data-sources] ${service}: ${populated.length}/${sources.length} sources have data \u2014 empty: ${emptyLabels}`);
9834
10027
  }
9835
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
+
9836
10214
  // src/lib/slack.ts
9837
10215
  async function sendSlackWebhook(webhookUrl, summary, header = "PAPI Strategy Review") {
9838
10216
  if (!webhookUrl) return void 0;
@@ -9892,6 +10270,9 @@ Task: [title]
9892
10270
  Cycle: [N]
9893
10271
  Why now: [justification]
9894
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
+
9895
10276
  SCOPE (DO THIS)
9896
10277
  [specific deliverables \u2014 write for the simplest viable path first]
9897
10278
 
@@ -10080,8 +10461,11 @@ Standard planning cycle with full board review.
10080
10461
  - **P3 Low** \u2014 Nice-to-have, speculative, or future-horizon work.
10081
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.
10082
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.
10083
- **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.
10084
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").
10085
10469
 
10086
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).
10087
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.
@@ -10100,11 +10484,17 @@ Standard planning cycle with full board review.
10100
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.
10101
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.
10102
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.
10103
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".
10104
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).
10105
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.
10106
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.
10107
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.
10108
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:
10109
10499
 
10110
10500
  RESEARCH OUTPUT
@@ -10122,7 +10512,8 @@ Standard planning cycle with full board review.
10122
10512
  **Idea task detection:** When a task's task type is "idea", add a scope clarification note to the BUILD HANDOFF:
10123
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."
10124
10514
 
10125
- **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):
10126
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."
10127
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."
10128
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."
@@ -10177,6 +10568,11 @@ var PLAN_FRAGMENT_BUG = `
10177
10568
  var PLAN_FRAGMENT_IDEA = `
10178
10569
  **Idea task detection:** When a task's task type is "idea", add a scope clarification note to the BUILD HANDOFF:
10179
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.`;
10180
10576
  var PLAN_FRAGMENT_SPIKE = `
10181
10577
  **Spike task detection:** When a task's task type is "spike" or the title starts with "Spike:", apply these rules:
10182
10578
  - Spikes are time-boxed investigations, not implementation tasks. The deliverable is a FINDING, not code.
@@ -10188,7 +10584,8 @@ var PLAN_FRAGMENT_SPIKE = `
10188
10584
  - Keep SCOPE BOUNDARY, SECURITY CONSIDERATIONS, and PRE-BUILD VERIFICATION as normal.
10189
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.`;
10190
10586
  var PLAN_FRAGMENT_UI = `
10191
- **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):
10192
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."
10193
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."
10194
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."
@@ -10267,8 +10664,11 @@ Standard planning cycle with full board review.
10267
10664
  - **P3 Low** \u2014 Nice-to-have, speculative, or future-horizon work.
10268
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.
10269
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.
10270
- **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.
10271
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").
10272
10672
 
10273
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).
10274
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.
@@ -10291,11 +10691,14 @@ Standard planning cycle with full board review.
10291
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).
10292
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.
10293
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.
10294
- **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.`);
10295
10697
  if (flags.hasResearchTasks) parts.push(PLAN_FRAGMENT_RESEARCH);
10296
10698
  if (flags.hasBugTasks) parts.push(PLAN_FRAGMENT_BUG);
10297
10699
  if (flags.hasIdeaTasks) parts.push(PLAN_FRAGMENT_IDEA);
10298
10700
  if (flags.hasSpikeTasks) parts.push(PLAN_FRAGMENT_SPIKE);
10701
+ if (flags.hasTaskTasks) parts.push(PLAN_FRAGMENT_TASK);
10299
10702
  if (flags.hasUITasks) parts.push(PLAN_FRAGMENT_UI);
10300
10703
  parts.push(`
10301
10704
  11. **New Tasks (max 3 per cycle)** \u2014 Actively mine the Recent Build Reports for task candidates. For each report, check:
@@ -10360,6 +10763,9 @@ function buildPlanUserMessage(ctx) {
10360
10763
  );
10361
10764
  }
10362
10765
  parts.push("", "---", "", "## PROJECT CONTEXT", "");
10766
+ if (ctx.contextTier) {
10767
+ parts.push(`**Context tier:** ${ctx.contextTier}`, "");
10768
+ }
10363
10769
  parts.push("### Product Brief", "", ctx.productBrief, "");
10364
10770
  if (ctx.northStar) {
10365
10771
  parts.push("### North Star (current)", "", ctx.northStar, "");
@@ -10377,12 +10783,18 @@ function buildPlanUserMessage(ctx) {
10377
10783
  if (ctx.cycleLog) {
10378
10784
  parts.push("### Cycle Log", "", ctx.cycleLog, "");
10379
10785
  }
10786
+ if (ctx.strategyReviewCadence) {
10787
+ parts.push("### Strategy Review Cadence (computed from DB)", "", ctx.strategyReviewCadence, "");
10788
+ }
10380
10789
  if (ctx.board) {
10381
10790
  parts.push("### Board", "", ctx.board, "");
10382
10791
  }
10383
10792
  if (ctx.preAssignedTasks) {
10384
10793
  parts.push("### Pre-Assigned Tasks", "", ctx.preAssignedTasks, "");
10385
10794
  }
10795
+ if (ctx.codebaseScan) {
10796
+ parts.push("### Codebase Scan (existing implementations)", "", ctx.codebaseScan, "");
10797
+ }
10386
10798
  if (ctx.buildPatterns) {
10387
10799
  parts.push("### Build Patterns", "", ctx.buildPatterns, "");
10388
10800
  }
@@ -10624,6 +11036,8 @@ You MUST cover these 5 sections. Each is mandatory.
10624
11036
  - Note any hierarchy/phase issues worth correcting (1-2 bullets max)
10625
11037
  - Delete ADs that are legacy, process-level, or redundant without discussion
10626
11038
 
11039
+ **Registered Documents:** If a "### Registered Documents" section is present in context, scan it for: (a) research findings that contradict current ADs or strategy, (b) unactioned research that should influence the next plan. Reference relevant docs by title in your review. If unregistered docs are listed, flag 1-2 that look strategically relevant and suggest registering them.
11040
+
10627
11041
  ## CONDITIONAL SECTIONS (include only when genuinely useful \u2014 most reviews should have 0-2 of these)
10628
11042
 
10629
11043
  6. **Security Posture Review** \u2014 Only if \`[SECURITY]\` tags exist in recent cycle logs.
@@ -10731,7 +11145,8 @@ After your natural language output, include this EXACT format on its own line:
10731
11145
  "category": "friction | methodology | signal | commercial",
10732
11146
  "content": "string \u2014 specific observation from using PAPI on this project (e.g. 'deprioritise clears handoffs unnecessarily, wasting planner tokens')"
10733
11147
  }
10734
- ]
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."
10735
11150
  }
10736
11151
  \`\`\`
10737
11152
 
@@ -10918,7 +11333,8 @@ After your natural language output, include this EXACT format on its own line:
10918
11333
  },
10919
11334
  "oldLabel": "string \u2014 only for modify/remove: the previous phase label so tasks can be migrated"
10920
11335
  }
10921
- ]
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."
10922
11338
  }
10923
11339
  \`\`\`
10924
11340
 
@@ -10974,6 +11390,9 @@ Task: [title]
10974
11390
  Cycle: [N]
10975
11391
  Why now: [justification]
10976
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
+
10977
11396
  SCOPE (DO THIS)
10978
11397
  [specific deliverables \u2014 write for the simplest viable path first]
10979
11398
 
@@ -11274,6 +11693,32 @@ async function getPrompt(name) {
11274
11693
  }
11275
11694
 
11276
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
+ }
11277
11722
  function determineMode(totalCycles) {
11278
11723
  if (totalCycles === 0) return "bootstrap";
11279
11724
  return "full";
@@ -11508,6 +11953,7 @@ function detectBoardFlags(tasks) {
11508
11953
  let hasResearchTasks = false;
11509
11954
  let hasIdeaTasks = false;
11510
11955
  let hasSpikeTasks = false;
11956
+ let hasTaskTasks = false;
11511
11957
  let hasUITasks = false;
11512
11958
  const uiKeywords = /\b(visual|design|UI|styling|refresh|frontend|landing page|hero|carousel|theme|layout|cockpit|dashboard|page)\b/i;
11513
11959
  for (const t of tasks) {
@@ -11515,9 +11961,10 @@ function detectBoardFlags(tasks) {
11515
11961
  if (t.taskType === "research" || /^Research:/i.test(t.title)) hasResearchTasks = true;
11516
11962
  if (t.taskType === "idea") hasIdeaTasks = true;
11517
11963
  if (t.taskType === "spike" || /^Spike:/i.test(t.title)) hasSpikeTasks = true;
11964
+ if (t.taskType === "task") hasTaskTasks = true;
11518
11965
  if (uiKeywords.test(t.title) || uiKeywords.test(t.notes ?? "")) hasUITasks = true;
11519
11966
  }
11520
- return { hasBugTasks, hasResearchTasks, hasIdeaTasks, hasSpikeTasks, hasUITasks };
11967
+ return { hasBugTasks, hasResearchTasks, hasIdeaTasks, hasSpikeTasks, hasTaskTasks, hasUITasks };
11521
11968
  }
11522
11969
  function detectBoardFlagsFromText(boardText) {
11523
11970
  return {
@@ -11525,6 +11972,7 @@ function detectBoardFlagsFromText(boardText) {
11525
11972
  hasResearchTasks: /\b(research|Research:)\b/i.test(boardText),
11526
11973
  hasIdeaTasks: /\bidea\b/i.test(boardText),
11527
11974
  hasSpikeTasks: /\b(spike|Spike:)\b/i.test(boardText),
11975
+ hasTaskTasks: /\btask\b/i.test(boardText),
11528
11976
  hasUITasks: /\b(visual|design|UI|styling|refresh|frontend|landing page|hero|carousel|theme|layout|cockpit|dashboard|page)\b/i.test(boardText)
11529
11977
  };
11530
11978
  }
@@ -11735,6 +12183,9 @@ ${lines.join("\n")}`;
11735
12183
  ]);
11736
12184
  timings["total"] = totalTimer();
11737
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.`;
11738
12189
  let ctx2 = {
11739
12190
  mode,
11740
12191
  cycleNumber: health.totalCycles,
@@ -11756,8 +12207,12 @@ ${lines.join("\n")}`;
11756
12207
  boardFlags,
11757
12208
  carryForwardStaleness: carryForwardStalenessLean,
11758
12209
  preAssignedTasks: preAssignedTextLean,
11759
- recentlyShippedCapabilities: recentlyShippedLean
12210
+ recentlyShippedCapabilities: recentlyShippedLean,
12211
+ strategyReviewCadence
11760
12212
  };
12213
+ const { label: leanTierLabel } = applyContextTier(ctx2, health.totalCycles);
12214
+ ctx2.contextTier = leanTierLabel;
12215
+ console.error(`[plan-perf] context tier: ${leanTierLabel} (cycle ${health.totalCycles})`);
11761
12216
  t = startTimer();
11762
12217
  const prevHashes2 = contextHashesResult.status === "fulfilled" ? contextHashesResult.value : null;
11763
12218
  const { ctx: diffedCtx2, newHashes: newHashes2, savedBytes: savedBytes2 } = applyContextDiff(ctx2, prevHashes2);
@@ -11819,7 +12274,8 @@ ${lines.join("\n")}`;
11819
12274
  if (pendingRecsResultFull.status === "fulfilled" && pendingRecsResultFull.value.length > 0) {
11820
12275
  strategyRecommendationsText = formatStrategyRecommendations(pendingRecsResultFull.value);
11821
12276
  }
11822
- 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);
11823
12279
  const discoveryCanvasTextFull = discoveryCanvasResultFull.status === "fulfilled" ? discoveryCanvasResultFull.value : void 0;
11824
12280
  const taskCommentsTextFull = taskCommentsResultFull.status === "fulfilled" ? taskCommentsResultFull.value : void 0;
11825
12281
  let registeredDocsTextFull;
@@ -11855,6 +12311,9 @@ ${lines.join("\n")}`;
11855
12311
  const targetCycle = health.totalCycles + 1;
11856
12312
  const preAssigned = strippedTasks.filter((t2) => t2.cycle === targetCycle);
11857
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.`;
11858
12317
  let ctx = {
11859
12318
  mode,
11860
12319
  cycleNumber: health.totalCycles,
@@ -11878,8 +12337,12 @@ ${lines.join("\n")}`;
11878
12337
  boardFlags: boardFlagsFull,
11879
12338
  carryForwardStaleness: computeCarryForwardStaleness(log),
11880
12339
  preAssignedTasks: preAssignedText,
11881
- recentlyShippedCapabilities: formatRecentlyShippedCapabilities(allReportsForPatterns)
12340
+ recentlyShippedCapabilities: formatRecentlyShippedCapabilities(reports),
12341
+ strategyReviewCadence: strategyReviewCadenceFull
11882
12342
  };
12343
+ const { label: fullTierLabel } = applyContextTier(ctx, health.totalCycles);
12344
+ ctx.contextTier = fullTierLabel;
12345
+ console.error(`[plan-perf] context tier: ${fullTierLabel} (cycle ${health.totalCycles})`);
11883
12346
  const prevHashes = contextHashesResultFull.status === "fulfilled" ? contextHashesResultFull.value : null;
11884
12347
  const { ctx: diffedCtx, newHashes, savedBytes } = applyContextDiff(ctx, prevHashes);
11885
12348
  ctx = diffedCtx;
@@ -12069,7 +12532,15 @@ ${cleanContent}`;
12069
12532
  taskCount: cycleTaskCount > 0 ? cycleTaskCount : void 0,
12070
12533
  effortPoints: cycleEffortPoints > 0 ? cycleEffortPoints : void 0
12071
12534
  });
12072
- 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);
12073
12544
  const newTaskIdMap = /* @__PURE__ */ new Map();
12074
12545
  const createTasksPromise = (async () => {
12075
12546
  if (!data.newTasks || data.newTasks.length === 0) return;
@@ -12489,6 +12960,17 @@ async function preparePlan(adapter2, config2, filters, focus, force, handoffsOnl
12489
12960
  }
12490
12961
  if (skipHandoffs) context.skipHandoffs = true;
12491
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();
12492
12974
  const userMessage = buildPlanUserMessage(context);
12493
12975
  const buildMessageMs = t();
12494
12976
  const totalMs = prepareTimer();
@@ -12665,6 +13147,7 @@ var lastPrepareSkipHandoffs;
12665
13147
  var planTool = {
12666
13148
  name: "plan",
12667
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 },
12668
13151
  inputSchema: {
12669
13152
  type: "object",
12670
13153
  properties: {
@@ -12792,24 +13275,30 @@ async function handlePlan(adapter2, config2, args) {
12792
13275
  return errorResponse('llm_response is required for mode "apply". Pass your complete plan output including the <!-- PAPI_STRUCTURED_OUTPUT --> block.');
12793
13276
  }
12794
13277
  const planMode = args.plan_mode || "full";
12795
- const cycleNumber = typeof args.cycle_number === "number" ? args.cycle_number : 0;
13278
+ const rawCycleNumber = args.cycle_number != null ? Number(args.cycle_number) : NaN;
12796
13279
  const strategyReviewWarning = args.strategy_review_warning || "";
12797
13280
  const contextHashes = lastPrepareContextHashes;
12798
13281
  const inputContext = lastPrepareUserMessage;
12799
13282
  const contextBytes = lastPrepareContextBytes;
12800
13283
  const expectedCycleNumber = lastPrepareCycleNumber;
12801
13284
  const skipHandoffsCached = lastPrepareSkipHandoffs;
12802
- lastPrepareContextHashes = void 0;
12803
- lastPrepareUserMessage = void 0;
12804
- lastPrepareContextBytes = void 0;
12805
- lastPrepareCycleNumber = void 0;
12806
- lastPrepareSkipHandoffs = void 0;
12807
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
+ }
12808
13292
  if (expectedCycleNumber !== void 0 && cycleNumber !== expectedCycleNumber) {
12809
13293
  return errorResponse(
12810
13294
  `cycle_number mismatch: prepare phase returned cycle ${expectedCycleNumber} but apply received ${cycleNumber}. Pass cycle_number: ${expectedCycleNumber} to match the prepare output.`
12811
13295
  );
12812
13296
  }
13297
+ lastPrepareContextHashes = void 0;
13298
+ lastPrepareUserMessage = void 0;
13299
+ lastPrepareContextBytes = void 0;
13300
+ lastPrepareCycleNumber = void 0;
13301
+ lastPrepareSkipHandoffs = void 0;
12813
13302
  const result = await applyPlan(adapter2, config2, llmResponse, planMode, cycleNumber, strategyReviewWarning, contextHashes, { contextBytes: contextBytes ?? void 0, skipHandoffs: skipHandoffs || void 0 });
12814
13303
  let utilisation;
12815
13304
  if (inputContext) {
@@ -13008,7 +13497,7 @@ function classifyRecommendation(text) {
13008
13497
  if (lower.includes("new task") || lower.includes("create task") || lower.includes("add task") || lower.includes("spike")) {
13009
13498
  return "new_task";
13010
13499
  }
13011
- 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")) {
13012
13501
  return "process_improvement";
13013
13502
  }
13014
13503
  if (lower.includes("infrastructure") || lower.includes("deploy") || lower.includes("ci/cd") || lower.includes("pipeline") || lower.includes("hosting") || lower.includes("database") || lower.includes("migration")) {
@@ -13610,21 +14099,6 @@ ${cleanContent}`;
13610
14099
  });
13611
14100
  } catch {
13612
14101
  }
13613
- try {
13614
- const cycleLog = await adapter2.getCycleLogSince(cycleNumber);
13615
- const currentEntry = cycleLog.find((e) => e.cycleNumber === cycleNumber);
13616
- if (currentEntry) {
13617
- const reviewCompleteText = `Strategy review completed Cycle ${cycleNumber}. Next due ~Cycle ${cycleNumber + 5}.`;
13618
- const existingCf = currentEntry.carryForward ?? "";
13619
- const updatedCf = existingCf ? existingCf.replace(/strategy review (?:due|available|overdue)[^.]*\.[^.]*/gi, reviewCompleteText).trim() : reviewCompleteText;
13620
- const finalCf = updatedCf === existingCf ? `${existingCf} ${reviewCompleteText}`.trim() : updatedCf;
13621
- await adapter2.writeCycleLogEntry({
13622
- ...currentEntry,
13623
- carryForward: finalCf
13624
- });
13625
- }
13626
- } catch {
13627
- }
13628
14102
  if (data.activeDecisionUpdates && data.activeDecisionUpdates.length > 0) {
13629
14103
  await Promise.all(data.activeDecisionUpdates.map(async (ad) => {
13630
14104
  if (ad.action === "delete" && adapter2.deleteActiveDecision) {
@@ -13683,6 +14157,12 @@ ${cleanContent}`;
13683
14157
  } catch {
13684
14158
  }
13685
14159
  }
14160
+ if (data.northStar && adapter2.upsertNorthStar) {
14161
+ try {
14162
+ await adapter2.upsertNorthStar(data.northStar, cycleNumber);
14163
+ } catch {
14164
+ }
14165
+ }
13686
14166
  const compressionThreshold = cycleNumber - 5;
13687
14167
  if (compressionThreshold > 0 && data.sessionLogCompressionSummary) {
13688
14168
  await adapter2.compressCycleLog(compressionThreshold, data.sessionLogCompressionSummary);
@@ -13742,7 +14222,7 @@ ${cleanContent}`;
13742
14222
  try {
13743
14223
  const canvas = await adapter2.readDiscoveryCanvas();
13744
14224
  const updates = {};
13745
- let populatedSections = [];
14225
+ const populatedSections = [];
13746
14226
  if (!canvas.landscapeReferences || canvas.landscapeReferences.length === 0) {
13747
14227
  if (data.activeDecisionUpdates?.length) {
13748
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) }));
@@ -14005,10 +14485,17 @@ function formatVelocitySummary(reports, cycleCount) {
14005
14485
  function formatRecentReportsSummary(reports, count) {
14006
14486
  const recent = reports.sort((a, b2) => b2.cycle - a.cycle || b2.date.localeCompare(a.date)).slice(0, count);
14007
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;
14008
14489
  return recent.map((r) => {
14009
14490
  const effort = `${r.actualEffort} vs ${r.estimatedEffort}`;
14010
- const surprises = r.surprises && r.surprises !== "None" ? ` \u2014 ${r.surprises.slice(0, 80)}${r.surprises.length > 80 ? "..." : ""}` : "";
14011
- 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");
14012
14499
  }).join("\n");
14013
14500
  }
14014
14501
  function formatPhasesForReview(phases, currentCycle) {
@@ -14034,7 +14521,7 @@ async function formatHierarchyForReview(adapter2, currentCycle, prefetchedTasks)
14034
14521
  } catch {
14035
14522
  }
14036
14523
  if (horizons.length === 0 && phases.length === 0) return void 0;
14037
- let tasksByPhase = /* @__PURE__ */ new Map();
14524
+ const tasksByPhase = /* @__PURE__ */ new Map();
14038
14525
  try {
14039
14526
  const allTasks = prefetchedTasks ?? await adapter2.queryBoard();
14040
14527
  for (const t of allTasks) {
@@ -14179,6 +14666,12 @@ ${cleanContent}`;
14179
14666
  const currentPhases = await adapter2.readPhases();
14180
14667
  await applyPhaseUpdates(adapter2, currentPhases, data.phaseUpdates);
14181
14668
  }
14669
+ if (data.northStar && adapter2.upsertNorthStar) {
14670
+ try {
14671
+ await adapter2.upsertNorthStar(data.northStar, cycleNumber);
14672
+ } catch {
14673
+ }
14674
+ }
14182
14675
  } catch (err) {
14183
14676
  writeBackFailed = err instanceof Error ? err.message : String(err);
14184
14677
  }
@@ -14301,6 +14794,7 @@ var lastReviewContextBytes;
14301
14794
  var strategyReviewTool = {
14302
14795
  name: "strategy_review",
14303
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 },
14304
14798
  inputSchema: {
14305
14799
  type: "object",
14306
14800
  properties: {
@@ -14328,6 +14822,7 @@ var strategyReviewTool = {
14328
14822
  var strategyChangeTool = {
14329
14823
  name: "strategy_change",
14330
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 },
14331
14826
  inputSchema: {
14332
14827
  type: "object",
14333
14828
  properties: {
@@ -14665,6 +15160,7 @@ async function archiveTasks(adapter2, phases, statuses) {
14665
15160
  var boardViewTool = {
14666
15161
  name: "board_view",
14667
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 },
14668
15164
  inputSchema: {
14669
15165
  type: "object",
14670
15166
  properties: {
@@ -14696,6 +15192,7 @@ var boardViewTool = {
14696
15192
  var boardDeprioritiseTool = {
14697
15193
  name: "board_deprioritise",
14698
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 },
14699
15196
  inputSchema: {
14700
15197
  type: "object",
14701
15198
  properties: {
@@ -14731,6 +15228,7 @@ var boardDeprioritiseTool = {
14731
15228
  var boardArchiveTool = {
14732
15229
  name: "board_archive",
14733
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 },
14734
15232
  inputSchema: {
14735
15233
  type: "object",
14736
15234
  properties: {
@@ -14749,6 +15247,7 @@ var boardArchiveTool = {
14749
15247
  var boardEditTool = {
14750
15248
  name: "board_edit",
14751
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 },
14752
15251
  inputSchema: {
14753
15252
  type: "object",
14754
15253
  properties: {
@@ -15015,7 +15514,7 @@ async function handleBoardEdit(adapter2, args) {
15015
15514
  try {
15016
15515
  const dogfoodLog = await adapter2.getDogfoodLog?.(50) ?? [];
15017
15516
  const linked = dogfoodLog.filter((e) => e.linkedTaskId === taskId || e.linkedTaskId === task.id);
15018
- const newStatus = updates.status === "Done" ? "actioned" : "dismissed";
15517
+ const newStatus = "resolved";
15019
15518
  await Promise.all(linked.map((e) => adapter2.updateDogfoodEntryStatus(e.id, newStatus)));
15020
15519
  } catch {
15021
15520
  }
@@ -15387,6 +15886,7 @@ When the system compresses prior messages, immediately:
15387
15886
 
15388
15887
  - **XS/S tasks in the same cycle and module:** Group on shared branch. One PR, one merge.
15389
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.
15390
15890
  - **Commit per task within grouped branches** \u2014 traceable git history.
15391
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.
15392
15892
 
@@ -16186,6 +16686,7 @@ async function applySetup(adapter2, config2, input, briefText, adSeedText, conve
16186
16686
  var setupTool = {
16187
16687
  name: "setup",
16188
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 },
16189
16690
  inputSchema: {
16190
16691
  type: "object",
16191
16692
  properties: {
@@ -16454,6 +16955,9 @@ init_git();
16454
16955
  import { randomUUID as randomUUID9 } from "crypto";
16455
16956
  import { readdirSync as readdirSync3, existsSync as existsSync3, readFileSync } from "fs";
16456
16957
  import { join as join5 } from "path";
16958
+ var buildStartTimes = /* @__PURE__ */ new Map();
16959
+ var taskBranchMap = /* @__PURE__ */ new Map();
16960
+ var SHARED_BRANCH_COMPLEXITIES = /* @__PURE__ */ new Set(["XS", "Small"]);
16457
16961
  function capitalizeCompleted(value) {
16458
16962
  const map = {
16459
16963
  yes: "Yes",
@@ -16473,7 +16977,7 @@ function pushAndCreatePR(config2, taskId, taskTitle) {
16473
16977
  if (!isGitAvailable() || !isGitRepo(config2.projectRoot)) {
16474
16978
  return lines;
16475
16979
  }
16476
- const featureBranch = taskBranchName(taskId);
16980
+ const featureBranch = taskBranchMap.get(taskId) ?? taskBranchName(taskId);
16477
16981
  const currentBranch = getCurrentBranch(config2.projectRoot);
16478
16982
  if (currentBranch !== featureBranch) {
16479
16983
  return lines;
@@ -16605,10 +17109,38 @@ async function startBuild(adapter2, config2, taskId, options = {}) {
16605
17109
  if (options.light) {
16606
17110
  branchLines.push("Light mode: skipping branch creation \u2014 working on current branch.");
16607
17111
  } else if (config2.autoCommit && isGitAvailable() && isGitRepo(config2.projectRoot)) {
16608
- 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);
16609
17138
  const currentBranch = getCurrentBranch(config2.projectRoot);
16610
17139
  if (currentBranch === featureBranch) {
16611
17140
  branchLines.push(`Already on branch '${featureBranch}'.`);
17141
+ if (useSharedBranch) {
17142
+ branchLines.push(`Reusing shared cycle branch for ${task.complexity} ${task.module} task.`);
17143
+ }
16612
17144
  } else {
16613
17145
  if (hasUncommittedChanges(config2.projectRoot, AUTO_WRITTEN_PATHS)) {
16614
17146
  throw new Error("Working directory has uncommitted changes. Please commit or stash them before running `build_execute`.");
@@ -16617,13 +17149,14 @@ async function startBuild(adapter2, config2, taskId, options = {}) {
16617
17149
  if (baseBranch !== config2.baseBranch) {
16618
17150
  branchLines.push(`Base branch '${config2.baseBranch}' not found \u2014 using '${baseBranch}'.`);
16619
17151
  }
16620
- if (currentBranch !== baseBranch) {
17152
+ const featureBranchExists = branchExists(config2.projectRoot, featureBranch);
17153
+ if (currentBranch !== baseBranch && !featureBranchExists) {
16621
17154
  const checkout = checkoutBranch(config2.projectRoot, baseBranch);
16622
17155
  if (!checkout.success) {
16623
17156
  branchLines.push(`Warning: ${checkout.message} Proceeding on current branch '${currentBranch}'.`);
16624
17157
  }
16625
17158
  }
16626
- if (hasRemote(config2.projectRoot)) {
17159
+ if (hasRemote(config2.projectRoot) && !featureBranchExists) {
16627
17160
  const pull = gitPull(config2.projectRoot);
16628
17161
  branchLines.push(pull.success ? pull.message : `Warning: ${pull.message}`);
16629
17162
  }
@@ -16633,10 +17166,10 @@ async function startBuild(adapter2, config2, taskId, options = {}) {
16633
17166
  `Warning: ${unmerged.length} unmerged feature branch${unmerged.length === 1 ? "" : "es"}: ${unmerged.join(", ")}. New branch may diverge if these contain changes needed here.`
16634
17167
  );
16635
17168
  }
16636
- if (branchExists(config2.projectRoot, featureBranch)) {
17169
+ if (featureBranchExists) {
16637
17170
  const checkout = checkoutBranch(config2.projectRoot, featureBranch);
16638
17171
  branchLines.push(
16639
- 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}`
16640
17173
  );
16641
17174
  if (checkout.success) {
16642
17175
  branchLines.push(
@@ -16646,7 +17179,7 @@ async function startBuild(adapter2, config2, taskId, options = {}) {
16646
17179
  } else {
16647
17180
  const create = createAndCheckoutBranch(config2.projectRoot, featureBranch);
16648
17181
  branchLines.push(
16649
- 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}`
16650
17183
  );
16651
17184
  if (create.success) {
16652
17185
  branchLines.push(
@@ -16659,6 +17192,7 @@ async function startBuild(adapter2, config2, taskId, options = {}) {
16659
17192
  if (task.status !== "In Progress") {
16660
17193
  await adapter2.updateTaskStatus(taskId, "In Progress");
16661
17194
  }
17195
+ buildStartTimes.set(taskId, (/* @__PURE__ */ new Date()).toISOString());
16662
17196
  let phaseChanges = [];
16663
17197
  try {
16664
17198
  phaseChanges = await propagatePhaseStatus(adapter2);
@@ -16724,13 +17258,32 @@ async function completeBuild(adapter2, config2, taskId, input, options = {}) {
16724
17258
  correctionsCount: input.correctionsCount,
16725
17259
  briefImplications: input.briefImplications,
16726
17260
  deadEnds: input.deadEnds,
16727
- iterationCount
17261
+ iterationCount,
17262
+ startedAt: buildStartTimes.get(taskId) ?? void 0,
17263
+ completedAt: now.toISOString()
16728
17264
  };
17265
+ buildStartTimes.delete(taskId);
17266
+ taskBranchMap.delete(taskId);
16729
17267
  if (input.relatedDecisions) {
16730
17268
  const adIds = input.relatedDecisions.split(",").map((s) => s.trim()).filter(Boolean);
16731
17269
  if (adIds.length > 0) report.relatedDecisions = adIds;
16732
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
+ }
16733
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
+ }
16734
17287
  if (adapter2.appendCycleLearnings) {
16735
17288
  const learnings = [];
16736
17289
  const taskModule = task.module ?? "";
@@ -16777,6 +17330,39 @@ async function completeBuild(adapter2, config2, taskId, input, options = {}) {
16777
17330
  }
16778
17331
  }
16779
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
+ }
16780
17366
  if (adapter2.updateCycleLearningActionRef && task.notes) {
16781
17367
  const learningRefs = task.notes.match(/learning:([a-f0-9-]+)/gi);
16782
17368
  if (learningRefs) {
@@ -16819,14 +17405,60 @@ async function completeBuild(adapter2, config2, taskId, input, options = {}) {
16819
17405
  await adapter2.updateTaskStatus(taskId, "In Review");
16820
17406
  }
16821
17407
  }
16822
- 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}).`;
16823
- let commitLine;
16824
- if (config2.autoCommit) {
16825
- commitLine = autoCommit(config2, taskId, task.title);
16826
- } else {
16827
- commitLine = "Auto-commit: skipped (PAPI_AUTO_COMMIT=false).";
16828
- }
16829
- if (isGitAvailable() && isGitRepo(config2.projectRoot)) {
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
+ }
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}).`;
17455
+ let commitLine;
17456
+ if (config2.autoCommit) {
17457
+ commitLine = autoCommit(config2, taskId, task.title);
17458
+ } else {
17459
+ commitLine = "Auto-commit: skipped (PAPI_AUTO_COMMIT=false).";
17460
+ }
17461
+ if (isGitAvailable() && isGitRepo(config2.projectRoot)) {
16830
17462
  const sha = getHeadCommitSha(config2.projectRoot);
16831
17463
  if (sha) report.commitSha = sha;
16832
17464
  const baseBranch = resolveBaseBranch(config2.projectRoot, config2.baseBranch);
@@ -16919,7 +17551,11 @@ async function completeBuild(adapter2, config2, taskId, input, options = {}) {
16919
17551
  completed: input.completed,
16920
17552
  scopeAccuracy: input.scopeAccuracy,
16921
17553
  phaseChanges,
16922
- 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
16923
17559
  };
16924
17560
  }
16925
17561
  async function cancelBuild(adapter2, taskId, reason) {
@@ -17012,6 +17648,7 @@ ${instructions}`;
17012
17648
  var buildListTool = {
17013
17649
  name: "build_list",
17014
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 },
17015
17652
  inputSchema: {
17016
17653
  type: "object",
17017
17654
  properties: {},
@@ -17021,6 +17658,7 @@ var buildListTool = {
17021
17658
  var buildDescribeTool = {
17022
17659
  name: "build_describe",
17023
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 },
17024
17662
  inputSchema: {
17025
17663
  type: "object",
17026
17664
  properties: {
@@ -17035,6 +17673,7 @@ var buildDescribeTool = {
17035
17673
  var buildExecuteTool = {
17036
17674
  name: "build_execute",
17037
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 },
17038
17677
  inputSchema: {
17039
17678
  type: "object",
17040
17679
  properties: {
@@ -17067,7 +17706,7 @@ var buildExecuteTool = {
17067
17706
  },
17068
17707
  discovered_issues: {
17069
17708
  type: "string",
17070
- 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.`
17071
17710
  },
17072
17711
  architecture_notes: {
17073
17712
  type: "string",
@@ -17119,6 +17758,7 @@ var buildExecuteTool = {
17119
17758
  var buildCancelTool = {
17120
17759
  name: "build_cancel",
17121
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 },
17122
17762
  inputSchema: {
17123
17763
  type: "object",
17124
17764
  properties: {
@@ -17286,9 +17926,28 @@ If >80% of the scope is already implemented, call \`build_execute\` with complet
17286
17926
  adSection = formatRelevantADs(relevant);
17287
17927
  } catch {
17288
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
+ }
17289
17948
  const moduleInstructions = getModuleInstructions(result.task.module);
17290
17949
  const moduleContext = await getModuleContext(adapter2, result.task);
17291
- 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);
17292
17951
  } catch (err) {
17293
17952
  if (isNoHandoffError(err)) {
17294
17953
  const lines = [
@@ -17395,6 +18054,18 @@ function formatCompleteResult(result) {
17395
18054
  lines.push(`Phase auto-updated: ${c.phaseId} ${c.oldStatus} \u2192 ${c.newStatus}`);
17396
18055
  }
17397
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
+ }
17398
18069
  if (result.docWarning) {
17399
18070
  lines.push("", `\u{1F4C4} ${result.docWarning}`);
17400
18071
  }
@@ -17534,7 +18205,7 @@ function resolveCurrentPhase(phases) {
17534
18205
  const sorted = [...phases].sort((a, b2) => a.order - b2.order);
17535
18206
  return sorted[0].label;
17536
18207
  }
17537
- var STOP_WORDS = /* @__PURE__ */ new Set([
18208
+ var STOP_WORDS2 = /* @__PURE__ */ new Set([
17538
18209
  "a",
17539
18210
  "an",
17540
18211
  "the",
@@ -17628,7 +18299,7 @@ var STOP_WORDS = /* @__PURE__ */ new Set([
17628
18299
  ]);
17629
18300
  function extractKeywords(text) {
17630
18301
  return new Set(
17631
- 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))
17632
18303
  );
17633
18304
  }
17634
18305
  async function findSimilarTasks(adapter2, ideaTitle) {
@@ -17699,9 +18370,10 @@ ${lines.join("\n")}
17699
18370
  const VALID_COMPLEXITIES2 = /* @__PURE__ */ new Set(["XS", "Small", "Medium", "Large", "XL"]);
17700
18371
  const priority = input.priority && VALID_PRIORITIES2.has(input.priority) ? input.priority : "P2 Medium";
17701
18372
  const complexity = input.complexity && VALID_COMPLEXITIES2.has(input.complexity) ? input.complexity : "Small";
17702
- const VALID_TYPES = /* @__PURE__ */ new Set(["task", "bug", "research", "idea", "spike"]);
18373
+ const VALID_TYPES = /* @__PURE__ */ new Set(["task", "bug", "research", "idea", "spike", "discovery"]);
17703
18374
  let taskTitle = input.text;
17704
18375
  let taskType = "idea";
18376
+ let typeInferred = false;
17705
18377
  if (input.type && VALID_TYPES.has(input.type)) {
17706
18378
  taskType = input.type;
17707
18379
  } else {
@@ -17716,6 +18388,20 @@ ${lines.join("\n")}
17716
18388
  taskType = PREFIX_MAP[key];
17717
18389
  taskTitle = input.text.slice(prefixMatch[0].length);
17718
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;
17719
18405
  }
17720
18406
  }
17721
18407
  const task = await adapter2.createTask({
@@ -17735,7 +18421,8 @@ ${lines.join("\n")}
17735
18421
  taskType,
17736
18422
  maturity: "raw",
17737
18423
  docRef: input.docRef,
17738
- source: "llm"
18424
+ source: "llm",
18425
+ opportunity: input.opportunity
17739
18426
  });
17740
18427
  if (input.notes && adapter2.updateCycleLearningActionRef) {
17741
18428
  const learningRefs = input.notes.match(/learning:([a-f0-9-]+)/gi);
@@ -17761,7 +18448,27 @@ ${lines.join("\n")}
17761
18448
  }
17762
18449
  }
17763
18450
  }
17764
- 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}` };
17765
18472
  }
17766
18473
  var CANVAS_SECTION_LABELS = {
17767
18474
  landscape: "Landscape References",
@@ -17801,6 +18508,7 @@ async function routeToDiscovery(adapter2, section, input) {
17801
18508
  // src/tools/idea.ts
17802
18509
  var ideaTool = {
17803
18510
  name: "idea",
18511
+ annotations: { readOnlyHint: false, destructiveHint: false },
17804
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.",
17805
18513
  inputSchema: {
17806
18514
  type: "object",
@@ -17811,7 +18519,7 @@ var ideaTool = {
17811
18519
  },
17812
18520
  notes: {
17813
18521
  type: "string",
17814
- 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-..."'
17815
18523
  },
17816
18524
  module: {
17817
18525
  type: "string",
@@ -17845,12 +18553,16 @@ var ideaTool = {
17845
18553
  },
17846
18554
  type: {
17847
18555
  type: "string",
17848
- enum: ["task", "bug", "research", "spike"],
17849
- 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.'
17850
18558
  },
17851
18559
  doc_ref: {
17852
18560
  type: "string",
17853
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."
17854
18566
  }
17855
18567
  },
17856
18568
  required: ["text"]
@@ -17879,7 +18591,8 @@ async function handleIdea(adapter2, config2, args) {
17879
18591
  discovery: args.discovery === true,
17880
18592
  force: args.force === true,
17881
18593
  docRef: args.doc_ref?.trim(),
17882
- type: args.type
18594
+ type: args.type,
18595
+ opportunity: args.opportunity?.trim()
17883
18596
  };
17884
18597
  const useGit = isGitAvailable() && isGitRepo(config2.projectRoot);
17885
18598
  const currentBranch = useGit ? getCurrentBranch(config2.projectRoot) : null;
@@ -17986,6 +18699,7 @@ function collectDiagnostics(config2) {
17986
18699
  var bugTool = {
17987
18700
  name: "bug",
17988
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 },
17989
18703
  inputSchema: {
17990
18704
  type: "object",
17991
18705
  properties: {
@@ -18166,6 +18880,7 @@ var VALID_EFFORTS = ["XS", "S", "M", "L", "XL"];
18166
18880
  var adHocTool = {
18167
18881
  name: "ad_hoc",
18168
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 },
18169
18884
  inputSchema: {
18170
18885
  type: "object",
18171
18886
  properties: {
@@ -18376,9 +19091,9 @@ async function prepareReconcile(adapter2) {
18376
19091
  }
18377
19092
  return lines.join("\n");
18378
19093
  }
18379
- 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"]);
18380
19095
  function tokenize(s) {
18381
- 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));
18382
19097
  }
18383
19098
  function titleKeywords(title) {
18384
19099
  return new Set(tokenize(title));
@@ -18555,6 +19270,7 @@ async function applyRetriage(adapter2, retriages) {
18555
19270
  var boardReconcileTool = {
18556
19271
  name: "board_reconcile",
18557
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 },
18558
19274
  inputSchema: {
18559
19275
  type: "object",
18560
19276
  properties: {
@@ -18885,432 +19601,99 @@ Assess each task above and produce your retriage output. Then call \`board_recon
18885
19601
  return errorResponse(`Unknown mode: ${mode}. Use "prepare", "apply", "retriage-prepare", or "retriage-apply".`);
18886
19602
  }
18887
19603
 
18888
- // src/services/health.ts
18889
- function computeHealthScore(cycleNumber, snapshots, activeTasks, decisionUsage) {
18890
- if (cycleNumber < 3) return null;
18891
- const scores = [];
18892
- const recentSnaps = snapshots.slice(-3);
18893
- const baselineSnaps = snapshots.slice(-10);
18894
- if (recentSnaps.length > 0 && baselineSnaps.length > 0) {
18895
- const avg = (snaps) => snaps.reduce((s, sn) => s + (sn.velocity[0]?.effortPoints ?? 0), 0) / snaps.length;
18896
- const recentAvg = avg(recentSnaps);
18897
- const baselineAvg = avg(baselineSnaps);
18898
- const velocityScore = baselineAvg > 0 ? Math.min(100, Math.round(recentAvg / baselineAvg * 100)) : 50;
18899
- scores.push({ name: "Velocity", score: velocityScore, weight: 0.25 });
18900
- } else {
18901
- scores.push({ name: "Velocity", score: 50, weight: 0.25 });
18902
- }
18903
- if (recentSnaps.length > 0) {
18904
- const avgMatchRate = recentSnaps.reduce((s, sn) => s + (sn.accuracy[0]?.matchRate ?? 0), 0) / recentSnaps.length;
18905
- scores.push({ name: "Estimation accuracy", score: Math.round(avgMatchRate), weight: 0.25 });
18906
- } else {
18907
- scores.push({ name: "Estimation accuracy", score: 50, weight: 0.25 });
18908
- }
18909
- const inReviewCount = activeTasks.filter((t) => t.status === "In Review").length;
18910
- const reviewScore = inReviewCount === 0 ? 100 : inReviewCount <= 2 ? 60 : 20;
18911
- scores.push({ name: "Review throughput", score: reviewScore, weight: 0.2 });
18912
- const backlogTasks = activeTasks.filter((t) => t.status === "Backlog");
18913
- if (backlogTasks.length > 0) {
18914
- const criticalCount = backlogTasks.filter(
18915
- (t) => t.priority === "P0 Critical" || t.priority === "P1 High"
18916
- ).length;
18917
- const criticalRatio = criticalCount / backlogTasks.length;
18918
- const backlogScore = criticalRatio > 0.5 ? 40 : criticalRatio > 0.3 ? 70 : 90;
18919
- scores.push({ name: "Backlog health", score: backlogScore, weight: 0.15 });
18920
- } else {
18921
- 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.");
18922
19647
  }
18923
- if (decisionUsage.length > 0) {
18924
- const staleCount = decisionUsage.filter((u) => u.cyclesSinceLastReference >= 10).length;
18925
- const freshRatio = (decisionUsage.length - staleCount) / decisionUsage.length;
18926
- scores.push({ name: "AD freshness", score: Math.round(freshRatio * 100), weight: 0.15 });
18927
- } else {
18928
- scores.push({ name: "AD freshness", score: 70, weight: 0.15 });
19648
+ if (!isGitRepo(config2.projectRoot)) {
19649
+ throw new Error("not a git repository.");
18929
19650
  }
18930
- const totalScore = Math.round(scores.reduce((sum, s) => sum + s.score * s.weight, 0));
18931
- const status = totalScore >= 70 ? "GREEN" : totalScore >= 50 ? "AMBER" : "RED";
18932
- const worst = scores.reduce((min, s) => s.score < min.score ? s : min, scores[0]);
18933
- const reason = status === "GREEN" ? "All components healthy" : `${worst.name} below target (${worst.score}/100)`;
18934
- return { score: totalScore, status, reason };
18935
- }
18936
- function countByStatus(tasks) {
18937
- const counts = /* @__PURE__ */ new Map();
18938
- for (const task of tasks) {
18939
- 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.");
18940
19653
  }
18941
- return counts;
18942
- }
18943
- async function getHealthSummary(adapter2) {
18944
- const health = await adapter2.getCycleHealth();
18945
- const activeTasks = await adapter2.queryBoard({
18946
- status: ["Backlog", "In Cycle", "Ready", "In Progress", "In Review", "Blocked"]
18947
- });
18948
- const logEntries = await adapter2.getCycleLog(3);
18949
- const cycleNumber = health.totalCycles;
18950
- const cyclesSinceReview = health.cyclesSinceLastStrategyReview;
18951
- const reviewDue = health.strategyReviewDue;
18952
- const reviewGateBlocking = cyclesSinceReview >= 5;
18953
- 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}`;
18954
- const deferredCount = activeTasks.filter((t) => t.status === "Deferred").length;
18955
- const nonDeferredTasks = activeTasks.filter((t) => t.status !== "Deferred");
18956
- const statusCounts = countByStatus(nonDeferredTasks);
18957
- let boardSummary;
18958
- if (nonDeferredTasks.length === 0 && deferredCount === 0) {
18959
- boardSummary = "0 tasks \u2014 board may need reloading";
18960
- } else {
18961
- const parts = [];
18962
- for (const [status, count] of statusCounts) {
18963
- parts.push(`${count} ${status}`);
18964
- }
18965
- boardSummary = `${nonDeferredTasks.length} active tasks \u2014 ${parts.join(", ")}`;
18966
- if (deferredCount > 0) {
18967
- 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);
18968
19675
  }
18969
19676
  }
18970
- const inProgressTasks = activeTasks.filter((t) => t.status === "In Progress");
18971
- const staleTasks = inProgressTasks.length > 0 ? `${inProgressTasks.length} task(s) In Progress: ${inProgressTasks.map((t) => t.id).join(", ")}` : "No tasks currently In Progress";
18972
- const inReviewTasks = activeTasks.filter((t) => t.status === "In Review");
18973
- 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";
18974
- let carryForward = "None found";
18975
- if (logEntries.length > 0) {
18976
- const latest = logEntries[0];
18977
- if (latest.carryForward) {
18978
- carryForward = latest.carryForward;
18979
- } else {
18980
- carryForward = `No carry-forward in Cycle ${latest.cycleNumber}`;
18981
- }
19677
+ const checkout = checkoutBranch(config2.projectRoot, branch);
19678
+ if (!checkout.success) {
19679
+ throw new Error(checkout.message);
18982
19680
  }
18983
- let recommendedMode;
18984
- const reasons = [];
18985
- if (reviewGateBlocking) {
18986
- 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
+ }
18987
19686
  }
18988
- if (activeTasks.length === 0) {
18989
- 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.`);
18990
19689
  }
18991
- const unbuiltCycleTasks = activeTasks.filter(
18992
- (t) => t.cycle === cycleNumber && (t.status === "In Cycle" || t.status === "Ready")
18993
- );
18994
- const inProgressCycleTasks = activeTasks.filter(
18995
- (t) => t.cycle === cycleNumber && t.status === "In Progress"
18996
- );
18997
- const inReviewCycleTasks = activeTasks.filter(
18998
- (t) => t.cycle === cycleNumber && t.status === "In Review"
18999
- );
19000
- if (reasons.length > 0) {
19001
- recommendedMode = `**Full** \u2014 ${reasons.join("; ")}`;
19002
- } else if (unbuiltCycleTasks.length > 0) {
19003
- recommendedMode = `**Build** \u2014 ${unbuiltCycleTasks.length} cycle task(s) not yet started`;
19004
- } else if (inProgressCycleTasks.length > 0) {
19005
- recommendedMode = `**Build** \u2014 ${inProgressCycleTasks.length} task(s) in progress`;
19006
- } else if (inReviewCycleTasks.length > 0) {
19007
- 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);
19008
19694
  } else {
19009
- recommendedMode = `**Full** \u2014 ready for next cycle`;
19010
- }
19011
- let metricsSection;
19012
- let derivedMetricsSection = "";
19013
- let snapshots = [];
19014
- try {
19015
- try {
19016
- const reports = await adapter2.getRecentBuildReports(50);
19017
- snapshots = computeSnapshotsFromBuildReports(reports);
19018
- } catch {
19019
- }
19020
- metricsSection = formatCycleMetrics(snapshots);
19021
- derivedMetricsSection = formatDerivedMetrics(snapshots, activeTasks);
19022
- } catch (_err) {
19023
- metricsSection = "Could not read methodology metrics.";
19024
- }
19025
- try {
19026
- const recentReports = await adapter2.getRecentBuildReports(50);
19027
- if (recentReports.length > 0) {
19028
- const taskCounts = /* @__PURE__ */ new Map();
19029
- for (const r of recentReports) {
19030
- taskCounts.set(r.taskId, (taskCounts.get(r.taskId) ?? 0) + 1);
19031
- }
19032
- const iterCounts = [...taskCounts.values()];
19033
- const avgIter = iterCounts.reduce((s, c) => s + c, 0) / iterCounts.length;
19034
- const multiIterTasks = iterCounts.filter((c) => c > 1).length;
19035
- if (avgIter > 1 || multiIterTasks > 0) {
19036
- derivedMetricsSection += `
19037
-
19038
- **Rework**
19039
- - Average iterations: ${avgIter.toFixed(1)} (${multiIterTasks} task${multiIterTasks !== 1 ? "s" : ""} with pushbacks)`;
19040
- }
19041
- }
19042
- } catch {
19043
- }
19044
- const costSection = "Disabled \u2014 local MCP, no API costs.";
19045
- let decisionUsageSection = "";
19046
- let decisionUsageEntries = [];
19047
- try {
19048
- const usage = await adapter2.getDecisionUsage(cycleNumber);
19049
- decisionUsageEntries = usage;
19050
- if (usage.length > 0) {
19051
- const stale = usage.filter((u) => u.cyclesSinceLastReference >= 5);
19052
- if (stale.length > 0) {
19053
- const lines = stale.map(
19054
- (u) => `- ${u.decisionId}: last referenced Cycle ${u.lastReferencedCycle} (${u.cyclesSinceLastReference} cycles ago)`
19055
- );
19056
- decisionUsageSection = `**Stale ADs (5+ cycles unreferenced):**
19057
- ${lines.join("\n")}`;
19058
- } else {
19059
- decisionUsageSection = `All ${usage.length} tracked ADs referenced within last 5 cycles.`;
19060
- }
19061
- }
19062
- } catch {
19063
- }
19064
- let decisionLifecycleSection = "";
19065
- try {
19066
- const decisions = await adapter2.getActiveDecisions();
19067
- const lifecycleSummary = formatDecisionLifecycleSummary(decisions);
19068
- if (lifecycleSummary) {
19069
- decisionLifecycleSection = `**Lifecycle:** ${lifecycleSummary}`;
19070
- }
19071
- } catch {
19072
- }
19073
- const decisionScoresSection = "";
19074
- let contextUtilisationSection = "";
19075
- try {
19076
- const utilData = await adapter2.getContextUtilisation?.();
19077
- if (utilData && utilData.length > 0) {
19078
- 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)`);
19079
- if (lines.length > 0) {
19080
- contextUtilisationSection = `**Current cycle:**
19081
- ${lines.join("\n")}`;
19082
- }
19083
- }
19084
- } catch {
19085
- }
19086
- let northStarSection = "";
19087
- try {
19088
- const staleness = await adapter2.getNorthStarStaleness?.();
19089
- if (staleness) {
19090
- const cycleGap = cycleNumber - staleness.setCycle;
19091
- const daysSinceSet = Math.floor((Date.now() - new Date(staleness.setAt).getTime()) / (1e3 * 60 * 60 * 24));
19092
- northStarSection = `\u2713 North Star set Cycle ${staleness.setCycle} (${cycleGap} cycles, ${daysSinceSet} days ago)`;
19093
- } else {
19094
- const setAtCycle = await adapter2.getNorthStarSetCycle?.();
19095
- if (setAtCycle != null) {
19096
- northStarSection = `\u2713 North Star set Cycle ${setAtCycle}`;
19097
- } else if (adapter2.getCurrentNorthStar) {
19098
- const ns = await adapter2.getCurrentNorthStar();
19099
- northStarSection = ns ? "" : "\u26A0\uFE0F No North Star set \u2014 consider defining one";
19100
- }
19101
- }
19102
- } catch {
19103
- }
19104
- const healthResult = computeHealthScore(cycleNumber, snapshots, activeTasks, decisionUsageEntries);
19105
- return {
19106
- cycleNumber,
19107
- latestCycleStatus: health.latestCycleStatus,
19108
- connectionStatus: getConnectionStatus(),
19109
- reviewWarning,
19110
- boardSummary,
19111
- staleTasks,
19112
- inReviewSummary,
19113
- carryForward,
19114
- recommendedMode,
19115
- metricsSection,
19116
- derivedMetricsSection,
19117
- costSection,
19118
- decisionUsageSection,
19119
- decisionLifecycleSection,
19120
- decisionScoresSection,
19121
- contextUtilisationSection,
19122
- northStarSection,
19123
- healthScore: healthResult?.score ?? null,
19124
- healthStatus: healthResult?.status ?? null,
19125
- healthReason: healthResult?.reason ?? null
19126
- };
19127
- }
19128
-
19129
- // src/tools/health.ts
19130
- var healthTool = {
19131
- name: "health",
19132
- 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.",
19133
- inputSchema: {
19134
- type: "object",
19135
- properties: {},
19136
- required: []
19137
- }
19138
- };
19139
- function formatHealthSummary(summary) {
19140
- const lines = [];
19141
- lines.push(`# Cycle ${summary.cycleNumber} \u2014 Health`);
19142
- lines.push("");
19143
- if (summary.connectionStatus !== "offline") {
19144
- const statusIcon = summary.connectionStatus === "connected" ? "\u2713" : "\u26A0\uFE0F";
19145
- const statusLabel = summary.connectionStatus === "connected" ? "Supabase connected" : "Supabase degraded \u2014 data may be stale. Check DATABASE_URL in .mcp.json";
19146
- lines.push(`**Connection:** ${statusIcon} ${statusLabel}`);
19147
- lines.push("");
19148
- }
19149
- lines.push(`> **Next action:** ${summary.recommendedMode}`);
19150
- lines.push("");
19151
- lines.push(`**Strategy Review:** ${summary.reviewWarning}`);
19152
- lines.push("");
19153
- lines.push(`## Board`);
19154
- lines.push(summary.boardSummary);
19155
- const hasInProgress = !summary.staleTasks.startsWith("No tasks");
19156
- const hasInReview = !summary.inReviewSummary.startsWith("No tasks");
19157
- if (hasInProgress || hasInReview) {
19158
- lines.push("");
19159
- if (hasInProgress) lines.push(`- **In Progress:** ${summary.staleTasks}`);
19160
- if (hasInReview) lines.push(`- **In Review:** ${summary.inReviewSummary}`);
19161
- }
19162
- lines.push("");
19163
- const hasCarryForward = summary.carryForward !== "None found" && !summary.carryForward.startsWith("No carry-forward");
19164
- if (hasCarryForward) {
19165
- lines.push(`## Carry-Forward`);
19166
- lines.push(summary.carryForward);
19167
- lines.push("");
19168
- }
19169
- const hasMetrics = summary.metricsSection !== "Could not read methodology metrics." && !summary.metricsSection.includes("undefined");
19170
- if (hasMetrics) {
19171
- lines.push(`## Trends`);
19172
- lines.push(summary.metricsSection);
19173
- lines.push("");
19174
- }
19175
- if (summary.derivedMetricsSection) {
19176
- lines.push(`## Insights`);
19177
- lines.push(summary.derivedMetricsSection);
19178
- lines.push("");
19179
- }
19180
- const hasCost = summary.costSection !== "No metrics data yet." && summary.costSection !== "Could not read metrics data.";
19181
- if (hasCost) {
19182
- lines.push(`## Cost`);
19183
- lines.push(summary.costSection);
19184
- lines.push("");
19185
- }
19186
- if (summary.northStarSection) {
19187
- lines.push(`## North Star`);
19188
- lines.push(summary.northStarSection);
19189
- lines.push("");
19190
- }
19191
- if (summary.decisionUsageSection) {
19192
- lines.push(`## Decision Usage`);
19193
- lines.push(summary.decisionUsageSection);
19194
- if (summary.decisionLifecycleSection) {
19195
- lines.push(summary.decisionLifecycleSection);
19196
- }
19197
- lines.push("");
19198
- }
19199
- if (summary.contextUtilisationSection) {
19200
- lines.push(`## Context Utilisation`);
19201
- lines.push(summary.contextUtilisationSection);
19202
- lines.push("");
19203
- }
19204
- if (summary.decisionScoresSection) {
19205
- lines.push(`## Decision Scores`);
19206
- lines.push(summary.decisionScoresSection);
19207
- lines.push("");
19208
- }
19209
- return lines.join("\n").trimEnd();
19210
- }
19211
- async function handleHealth(adapter2) {
19212
- try {
19213
- const summary = await getHealthSummary(adapter2);
19214
- return textResponse(formatHealthSummary(summary));
19215
- } catch (err) {
19216
- const message = err instanceof Error ? err.message : String(err);
19217
- return errorResponse(`Could not read cycle health. Run \`setup\` first to initialise your project. (${message})`);
19218
- }
19219
- }
19220
-
19221
- // src/services/release.ts
19222
- init_git();
19223
- import { writeFile as writeFile3 } from "fs/promises";
19224
- import { join as join6 } from "path";
19225
- var INITIAL_RELEASE_NOTES = `# Changelog
19226
-
19227
- ## v0.1.0-alpha \u2014 Initial Release
19228
-
19229
- PAPI MCP Server \u2014 the AI-powered project planning framework.
19230
-
19231
- ### Commands
19232
- - **setup** \u2014 Initialise a new PAPI project with Product Brief generation
19233
- - **plan** \u2014 Run cycle planning with embedded BUILD HANDOFFs (Bootstrap + Full modes)
19234
- - **build_list / build_describe / build_execute / build_cancel** \u2014 Manage build tasks
19235
- - **board_view / board_deprioritise / board_archive** \u2014 View and manage the Board
19236
- - **strategy_review / strategy_change** \u2014 Run Strategy Reviews and apply strategic changes
19237
- - **review_list / review_submit** \u2014 Human review loop for handoffs and builds
19238
- - **idea** \u2014 Capture ideas as backlog tasks for future triage
19239
- - **health** \u2014 Cycle Health Summary dashboard
19240
- - **release** \u2014 Cut versioned releases with git tags and changelogs
19241
-
19242
- ### Features
19243
- - .md file persistence in .papi/ directory
19244
- - Bootstrap + Full planning modes with Anthropic API integration
19245
- - Embedded BUILD HANDOFFs with dual write-back build reports
19246
- - Auto-commit and auto-PR after builds
19247
- - Board corrections and Active Decision persistence
19248
- - Single-purpose MCP tools for optimal LLM tool selection
19249
- - Consistent error handling across all tools
19250
- `;
19251
- function generateChangelog(version, commits) {
19252
- const date = (/* @__PURE__ */ new Date()).toISOString();
19253
- const commitList = commits.map((c) => `- ${c}`).join("\n");
19254
- return `# Changelog
19255
-
19256
- ## ${version} \u2014 ${date}
19257
-
19258
- ${commitList}
19259
- `;
19260
- }
19261
- async function createRelease(config2, branch, version, adapter2) {
19262
- if (!isGitAvailable()) {
19263
- throw new Error("git is not available.");
19264
- }
19265
- if (!isGitRepo(config2.projectRoot)) {
19266
- throw new Error("not a git repository.");
19267
- }
19268
- if (hasUncommittedChanges(config2.projectRoot, AUTO_WRITTEN_PATHS)) {
19269
- throw new Error("working directory has uncommitted changes. Commit or stash them before releasing.");
19270
- }
19271
- const warnings = [];
19272
- if (adapter2) {
19273
- try {
19274
- const versionMatch = version.match(/^v0\.(\d+)\./);
19275
- const currentCycle = versionMatch ? parseInt(versionMatch[1], 10) : 0;
19276
- if (currentCycle > 0) {
19277
- await adapter2.createCycle({
19278
- id: `cycle-${currentCycle}`,
19279
- number: currentCycle,
19280
- status: "complete",
19281
- startDate: (/* @__PURE__ */ new Date()).toISOString(),
19282
- endDate: (/* @__PURE__ */ new Date()).toISOString(),
19283
- goals: [],
19284
- boardHealth: "",
19285
- taskIds: []
19286
- });
19287
- }
19288
- } catch (err) {
19289
- const msg = `createCycle (mark complete) failed: ${err instanceof Error ? err.message : String(err)}`;
19290
- console.error(`[release] ${msg}`);
19291
- warnings.push(msg);
19292
- }
19293
- }
19294
- const checkout = checkoutBranch(config2.projectRoot, branch);
19295
- if (!checkout.success) {
19296
- throw new Error(checkout.message);
19297
- }
19298
- if (hasRemote(config2.projectRoot)) {
19299
- const pull = gitPull(config2.projectRoot);
19300
- if (!pull.success) {
19301
- warnings.push(`git pull failed: ${pull.message}. Run manually.`);
19302
- }
19303
- }
19304
- if (tagExists(config2.projectRoot, version)) {
19305
- throw new Error(`tag "${version}" already exists. Use a different version.`);
19306
- }
19307
- const latestTag = getLatestTag(config2.projectRoot);
19308
- let changelogContent;
19309
- if (!latestTag) {
19310
- changelogContent = INITIAL_RELEASE_NOTES.replace("v0.1.0-alpha", version);
19311
- } else {
19312
- const commits = getCommitsSinceTag(config2.projectRoot, latestTag);
19313
- changelogContent = generateChangelog(version, commits);
19695
+ const commits = getCommitsSinceTag(config2.projectRoot, latestTag);
19696
+ changelogContent = generateChangelog(version, commits);
19314
19697
  }
19315
19698
  const changelogPath = join6(config2.projectRoot, "CHANGELOG.md");
19316
19699
  await writeFile3(changelogPath, changelogContent, "utf-8");
@@ -19345,6 +19728,7 @@ async function createRelease(config2, branch, version, adapter2) {
19345
19728
  var releaseTool = {
19346
19729
  name: "release",
19347
19730
  description: "Cut a versioned release \u2014 creates a git tag, generates CHANGELOG.md, and pushes to remote.",
19731
+ annotations: { readOnlyHint: false, destructiveHint: true },
19348
19732
  inputSchema: {
19349
19733
  type: "object",
19350
19734
  properties: {
@@ -19355,14 +19739,36 @@ var releaseTool = {
19355
19739
  version: {
19356
19740
  type: "string",
19357
19741
  description: 'The version tag to create (e.g. "v0.1.0-alpha"). Must start with "v".'
19358
- }
19359
- },
19360
- required: ["branch", "version"]
19361
- }
19362
- };
19363
- async function handleRelease(adapter2, config2, args) {
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
+ }
19763
+ }
19764
+ },
19765
+ required: ["branch", "version"]
19766
+ }
19767
+ };
19768
+ async function handleRelease(adapter2, config2, args) {
19364
19769
  const branch = args.branch;
19365
19770
  const version = args.version;
19771
+ const rawObservations = args.observations;
19366
19772
  if (!branch || !version) {
19367
19773
  return errorResponse('both branch and version are required. Example: release branch="main" version="v0.1.0-alpha"');
19368
19774
  }
@@ -19400,6 +19806,23 @@ async function handleRelease(adapter2, config2, args) {
19400
19806
  }
19401
19807
  } catch {
19402
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
+ }
19403
19826
  lines.push("", `Next: cycle released! Run \`plan\` to start your next planning cycle.`);
19404
19827
  return textResponse(lines.join("\n"));
19405
19828
  } catch (err) {
@@ -19525,7 +19948,6 @@ async function submitReview(adapter2, input) {
19525
19948
  handoffRegenPrompt = await prepareHandoffRegen(task, input.comments);
19526
19949
  }
19527
19950
  const stageLabel = input.stage === "handoff-review" ? "Handoff Review" : "Build Acceptance";
19528
- const slackWarning = void 0;
19529
19951
  let phaseChanges = [];
19530
19952
  if (newStatus) {
19531
19953
  try {
@@ -19541,7 +19963,6 @@ async function submitReview(adapter2, input) {
19541
19963
  newStatus,
19542
19964
  unblockedTasks,
19543
19965
  handoffRegenerated,
19544
- slackWarning,
19545
19966
  handoffRegenPrompt,
19546
19967
  currentCycle: cycle,
19547
19968
  phaseChanges
@@ -19552,6 +19973,7 @@ async function submitReview(adapter2, input) {
19552
19973
  var reviewListTool = {
19553
19974
  name: "review_list",
19554
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 },
19555
19977
  inputSchema: {
19556
19978
  type: "object",
19557
19979
  properties: {},
@@ -19561,6 +19983,7 @@ var reviewListTool = {
19561
19983
  var reviewSubmitTool = {
19562
19984
  name: "review_submit",
19563
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 },
19564
19987
  inputSchema: {
19565
19988
  type: "object",
19566
19989
  properties: {
@@ -19590,10 +20013,6 @@ var reviewSubmitTool = {
19590
20013
  type: "string",
19591
20014
  description: "Your locally-generated BUILD HANDOFF regen output. Pass this to save a handoff that was regenerated in local mode (no API key)."
19592
20015
  },
19593
- notify: {
19594
- type: "boolean",
19595
- description: "Send Slack notification. Default true. Set false for batch middle reviews to avoid spam."
19596
- },
19597
20016
  auto_review: {
19598
20017
  type: "object",
19599
20018
  description: "Optional automated code review results to attach to this review. Run PR analysis first, then pass findings here.",
@@ -19710,7 +20129,6 @@ async function handleReviewSubmit(adapter2, config2, args) {
19710
20129
  const verdict = args.verdict;
19711
20130
  const comments = args.comments;
19712
20131
  const reviewer = args.reviewer ?? "human";
19713
- const notify = args.notify !== false;
19714
20132
  const rawAutoReview = args.auto_review;
19715
20133
  let autoReview;
19716
20134
  if (rawAutoReview?.verdict && rawAutoReview?.summary && Array.isArray(rawAutoReview?.findings)) {
@@ -19753,7 +20171,7 @@ async function handleReviewSubmit(adapter2, config2, args) {
19753
20171
  try {
19754
20172
  const result = await submitReview(
19755
20173
  adapter2,
19756
- { taskId, stage, verdict, comments, reviewer, notify, autoReview }
20174
+ { taskId, stage, verdict, comments, reviewer, autoReview }
19757
20175
  );
19758
20176
  const statusNote = result.newStatus ? ` Task status updated to **${result.newStatus}**.` : " Task status unchanged.";
19759
20177
  const unblockNote = result.unblockedTasks.length > 0 ? `
@@ -19786,9 +20204,6 @@ ${result.handoffRegenPrompt.userMessage}
19786
20204
  mergeNote = "\n\n" + mergeLines.map((l) => `> ${l}`).join("\n");
19787
20205
  }
19788
20206
  }
19789
- const slackNote = result.slackWarning ? `
19790
-
19791
- ${result.slackWarning}` : "";
19792
20207
  let autoReleaseNote = "";
19793
20208
  if (stage === "build-acceptance" && verdict === "accept" && result.newStatus === "Done" && result.currentCycle > 0) {
19794
20209
  try {
@@ -19839,7 +20254,7 @@ Next: address the feedback, then run \`build_execute ${taskId}\` to resubmit.`;
19839
20254
  - **Verdict:** ${result.verdict}
19840
20255
  - **Comments:** ${result.comments}
19841
20256
 
19842
- ${statusNote}${autoReviewNote}${unblockNote}${regenNote}${mergeNote}${slackNote}${autoReleaseNote}${nextStepNote}${phaseNote}`
20257
+ ${statusNote}${autoReviewNote}${unblockNote}${regenNote}${mergeNote}${autoReleaseNote}${nextStepNote}${phaseNote}`
19843
20258
  );
19844
20259
  } catch (err) {
19845
20260
  return errorResponse(err instanceof Error ? err.message : String(err));
@@ -19853,6 +20268,7 @@ import path4 from "path";
19853
20268
  var initTool = {
19854
20269
  name: "init",
19855
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 },
19856
20272
  inputSchema: {
19857
20273
  type: "object",
19858
20274
  properties: {
@@ -19978,32 +20394,491 @@ Your existing API key and project ID have been saved to .mcp.json.
19978
20394
  ].join("\n");
19979
20395
  return textResponse(output2);
19980
20396
  }
19981
- const output = [
19982
- `# PAPI \u2014 Account Required`,
19983
- "",
19984
- `PAPI needs an account to store your project data.`,
19985
- "",
19986
- "## Get Started in 3 Steps",
19987
- "",
19988
- "1. **Sign up** at https://getpapi.ai/login",
19989
- "2. **Complete the onboarding wizard** \u2014 it generates your `.mcp.json` config with your API key and project ID",
19990
- "3. **Download the config**, place it in your project root, and restart your MCP client",
19991
- "",
19992
- "The onboarding wizard generates everything you need \u2014 no manual configuration required.",
19993
- "",
19994
- `> Already have an account? Make sure both \`PAPI_PROJECT_ID\` and \`PAPI_DATA_API_KEY\` are set in your .mcp.json.`
19995
- ].join("\n");
19996
- 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"));
19997
20872
  }
19998
20873
 
19999
20874
  // src/tools/orient.ts
20000
- init_git();
20001
20875
  import { execFileSync as execFileSync3 } from "child_process";
20002
- import { readFileSync as readFileSync2, writeFileSync, existsSync as existsSync5 } from "fs";
20003
- import { join as join8 } from "path";
20876
+ import { readFileSync as readFileSync3, writeFileSync, existsSync as existsSync6 } from "fs";
20877
+ import { join as join9 } from "path";
20004
20878
  var orientTool = {
20005
20879
  name: "orient",
20006
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 },
20007
20882
  inputSchema: {
20008
20883
  type: "object",
20009
20884
  properties: {},
@@ -20179,8 +21054,8 @@ function getLatestGitTag(projectRoot) {
20179
21054
  }
20180
21055
  function checkNpmVersionDrift() {
20181
21056
  try {
20182
- const pkgPath = join8(new URL(".", import.meta.url).pathname, "..", "..", "package.json");
20183
- 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"));
20184
21059
  const localVersion = pkg.version;
20185
21060
  const packageName = pkg.name;
20186
21061
  const published = execFileSync3("npm", ["view", packageName, "version"], {
@@ -20220,6 +21095,17 @@ async function handleOrient(adapter2, config2) {
20220
21095
  if (!cycleIsComplete && cycleTotal === 0 && cycleDone > 0) {
20221
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.`);
20222
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
+ }
20223
21109
  const inProgressItems = buildResult.inProgress.map(
20224
21110
  (t) => `- **${t.id}:** ${t.title} (${t.priority} | ${t.complexity})`
20225
21111
  );
@@ -20275,8 +21161,15 @@ ${versionDrift}` : "";
20275
21161
  try {
20276
21162
  const unrecorded = detectUnrecordedCommits(config2.projectRoot, config2.baseBranch);
20277
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;
20278
21167
  const lines = ["\n\n## Unrecorded Work"];
20279
- 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
+ }
20280
21173
  for (const c of unrecorded) {
20281
21174
  lines.push(`- \`${c.hash}\` ${c.message}`);
20282
21175
  }
@@ -20284,6 +21177,23 @@ ${versionDrift}` : "";
20284
21177
  }
20285
21178
  } catch {
20286
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
+ }
20287
21197
  let recsNote = "";
20288
21198
  try {
20289
21199
  const pendingRecs = await adapter2.getPendingRecommendations();
@@ -20317,16 +21227,33 @@ ${versionDrift}` : "";
20317
21227
  }
20318
21228
  } catch {
20319
21229
  }
20320
- 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);
20321
21248
  } catch (err) {
20322
21249
  const message = err instanceof Error ? err.message : String(err);
20323
21250
  return errorResponse(`Orient failed: ${message}`);
20324
21251
  }
20325
21252
  }
20326
21253
  function enrichClaudeMd(projectRoot, cycleNumber) {
20327
- const claudeMdPath = join8(projectRoot, "CLAUDE.md");
20328
- if (!existsSync5(claudeMdPath)) return "";
20329
- 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");
20330
21257
  const additions = [];
20331
21258
  if (cycleNumber >= 6 && !content.includes(CLAUDE_MD_ENRICHMENT_SENTINEL_T1)) {
20332
21259
  additions.push(CLAUDE_MD_TIER_1);
@@ -20348,6 +21275,7 @@ function enrichClaudeMd(projectRoot, cycleNumber) {
20348
21275
  var hierarchyUpdateTool = {
20349
21276
  name: "hierarchy_update",
20350
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 },
20351
21279
  inputSchema: {
20352
21280
  type: "object",
20353
21281
  properties: {
@@ -20362,7 +21290,7 @@ var hierarchyUpdateTool = {
20362
21290
  },
20363
21291
  status: {
20364
21292
  type: "string",
20365
- enum: ["active", "completed", "deferred"],
21293
+ enum: ["Not Started", "In Progress", "Done", "Deferred"],
20366
21294
  description: "The new status to set."
20367
21295
  },
20368
21296
  exit_criteria: {
@@ -20374,7 +21302,7 @@ var hierarchyUpdateTool = {
20374
21302
  required: ["level", "name"]
20375
21303
  }
20376
21304
  };
20377
- var VALID_STATUSES3 = /* @__PURE__ */ new Set(["active", "completed", "deferred"]);
21305
+ var VALID_STATUSES3 = /* @__PURE__ */ new Set(["Not Started", "In Progress", "Done", "Deferred"]);
20378
21306
  async function handleHierarchyUpdate(adapter2, args) {
20379
21307
  const level = args.level;
20380
21308
  const name = args.name;
@@ -20390,7 +21318,7 @@ async function handleHierarchyUpdate(adapter2, args) {
20390
21318
  return errorResponse(`Invalid level "${level}". Must be "phase", "stage", or "horizon".`);
20391
21319
  }
20392
21320
  if (status && !VALID_STATUSES3.has(status)) {
20393
- return errorResponse(`Invalid status "${status}". Must be one of: active, completed, deferred.`);
21321
+ return errorResponse(`Invalid status "${status}". Must be one of: Not Started, In Progress, Done, Deferred.`);
20394
21322
  }
20395
21323
  if (exitCriteria !== void 0 && level !== "stage") {
20396
21324
  return errorResponse("exit_criteria can only be set on stages.");
@@ -20743,6 +21671,7 @@ async function applyZoomOut(adapter2, llmResponse, cycleNumber) {
20743
21671
  var zoomOutTool = {
20744
21672
  name: "zoom_out",
20745
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 },
20746
21675
  inputSchema: {
20747
21676
  type: "object",
20748
21677
  properties: {
@@ -20817,222 +21746,11 @@ ${result.userMessage}
20817
21746
  }
20818
21747
  }
20819
21748
 
20820
- // src/tools/doc-registry.ts
20821
- import { readdirSync as readdirSync4, existsSync as existsSync6, readFileSync as readFileSync3 } from "fs";
20822
- import { join as join9, relative } from "path";
20823
- import { homedir as homedir2 } from "os";
20824
- var docRegisterTool = {
20825
- name: "doc_register",
20826
- 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.",
20827
- inputSchema: {
20828
- type: "object",
20829
- properties: {
20830
- path: { type: "string", description: 'Relative path from project root (e.g. "docs/research/funding-landscape.md").' },
20831
- title: { type: "string", description: "Document title." },
20832
- type: { type: "string", enum: ["research", "audit", "spec", "guide", "architecture", "positioning", "framework", "reference"], description: "Document type." },
20833
- status: { type: "string", enum: ["active", "draft", "superseded", "actioned", "legacy", "archived"], description: 'Document status. Defaults to "active".' },
20834
- summary: { type: "string", description: 'Structured 2-4 sentence summary. Format: "Conclusions: ... Open questions: ... Unactioned: ..."' },
20835
- tags: { type: "array", items: { type: "string" }, description: "Tags from project vocabulary." },
20836
- cycle: { type: "number", description: "Current cycle number." },
20837
- actions: {
20838
- type: "array",
20839
- items: {
20840
- type: "object",
20841
- properties: {
20842
- description: { type: "string" },
20843
- status: { type: "string", enum: ["pending", "resolved"] },
20844
- linkedTaskId: { type: "string" }
20845
- },
20846
- required: ["description", "status"]
20847
- },
20848
- description: "Actionable findings from the document."
20849
- },
20850
- superseded_by_path: { type: "string", description: "Path of the doc that supersedes this one (sets status to superseded)." }
20851
- },
20852
- required: ["path", "title", "type", "summary", "cycle"]
20853
- }
20854
- };
20855
- var docSearchTool = {
20856
- name: "doc_search",
20857
- 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.",
20858
- inputSchema: {
20859
- type: "object",
20860
- properties: {
20861
- type: { type: "string", description: 'Filter by doc type (e.g. "research", "architecture").' },
20862
- status: { type: "string", description: 'Filter by status. Defaults to "active".' },
20863
- tags: { type: "array", items: { type: "string" }, description: "Filter by tags (OR match)." },
20864
- keyword: { type: "string", description: "Search title and summary text." },
20865
- has_pending_actions: { type: "boolean", description: "Only docs with unresolved action items." },
20866
- since_cycle: { type: "number", description: "Docs updated since this cycle." },
20867
- limit: { type: "number", description: "Max results (default: 10)." }
20868
- },
20869
- required: []
20870
- }
20871
- };
20872
- var docScanTool = {
20873
- name: "doc_scan",
20874
- 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.",
20875
- inputSchema: {
20876
- type: "object",
20877
- properties: {
20878
- include_plans: {
20879
- type: "boolean",
20880
- description: "Also scan ~/.claude/plans/ for plan files (default: false)."
20881
- }
20882
- },
20883
- required: []
20884
- }
20885
- };
20886
- async function handleDocRegister(adapter2, args) {
20887
- if (!adapter2.registerDoc) {
20888
- return errorResponse("Doc registry not available \u2014 requires pg adapter.");
20889
- }
20890
- const path5 = args.path;
20891
- const title = args.title;
20892
- const type = args.type;
20893
- const status = args.status ?? "active";
20894
- const summary = args.summary;
20895
- const tags = args.tags ?? [];
20896
- const cycle = args.cycle;
20897
- const actions = args.actions;
20898
- const supersededByPath = args.superseded_by_path;
20899
- if (!path5 || !title || !type || !summary || !cycle) {
20900
- return errorResponse("Required fields: path, title, type, summary, cycle.");
20901
- }
20902
- let supersededBy;
20903
- if (supersededByPath) {
20904
- const existing = await adapter2.getDoc?.(supersededByPath);
20905
- if (existing) {
20906
- supersededBy = existing.id;
20907
- await adapter2.updateDocStatus?.(existing.id, "superseded", void 0);
20908
- }
20909
- }
20910
- const entry = await adapter2.registerDoc({
20911
- title,
20912
- type,
20913
- path: path5,
20914
- status: supersededByPath ? "superseded" : status,
20915
- summary,
20916
- tags,
20917
- cycleCreated: cycle,
20918
- cycleUpdated: cycle,
20919
- supersededBy,
20920
- actions
20921
- });
20922
- return textResponse(
20923
- `**Registered:** ${entry.title}
20924
- - **Path:** ${entry.path}
20925
- - **Type:** ${entry.type} | **Status:** ${entry.status}
20926
- - **Tags:** ${entry.tags.length > 0 ? entry.tags.join(", ") : "none"}
20927
- - **Actions:** ${actions?.length ?? 0} items
20928
- - **ID:** ${entry.id}`
20929
- );
20930
- }
20931
- async function handleDocSearch(adapter2, args) {
20932
- if (!adapter2.searchDocs) {
20933
- return errorResponse("Doc registry not available \u2014 requires pg adapter.");
20934
- }
20935
- const input = {
20936
- type: args.type,
20937
- status: args.status,
20938
- tags: args.tags,
20939
- keyword: args.keyword,
20940
- hasPendingActions: args.has_pending_actions,
20941
- sinceCycle: args.since_cycle,
20942
- limit: args.limit
20943
- };
20944
- const docs = await adapter2.searchDocs(input);
20945
- if (docs.length === 0) {
20946
- return textResponse("No documents found matching the search criteria.");
20947
- }
20948
- const lines = docs.map((d) => {
20949
- const actionCount = d.actions?.filter((a) => a.status === "pending").length ?? 0;
20950
- const actionNote = actionCount > 0 ? ` | ${actionCount} pending action(s)` : "";
20951
- return `### ${d.title}
20952
- **Type:** ${d.type} | **Status:** ${d.status} | **Cycle:** ${d.cycleCreated}${d.cycleUpdated ? `\u2192${d.cycleUpdated}` : ""}${actionNote}
20953
- **Path:** ${d.path}
20954
- **Tags:** ${d.tags.length > 0 ? d.tags.join(", ") : "none"}
20955
- ${d.summary}
20956
- `;
20957
- });
20958
- return textResponse(`**${docs.length} document(s) found:**
20959
-
20960
- ${lines.join("\n---\n\n")}`);
20961
- }
20962
- function scanMdFiles(dir, rootDir) {
20963
- if (!existsSync6(dir)) return [];
20964
- const files = [];
20965
- try {
20966
- const entries = readdirSync4(dir, { withFileTypes: true });
20967
- for (const entry of entries) {
20968
- const full = join9(dir, entry.name);
20969
- if (entry.isDirectory()) {
20970
- files.push(...scanMdFiles(full, rootDir));
20971
- } else if (entry.name.endsWith(".md")) {
20972
- files.push(relative(rootDir, full));
20973
- }
20974
- }
20975
- } catch {
20976
- }
20977
- return files;
20978
- }
20979
- function extractTitle(filePath) {
20980
- try {
20981
- const content = readFileSync3(filePath, "utf-8").slice(0, 1e3);
20982
- const fmMatch = content.match(/^---[\s\S]*?title:\s*(.+?)$/m);
20983
- if (fmMatch) return fmMatch[1].trim().replace(/^["']|["']$/g, "");
20984
- const headingMatch = content.match(/^#+\s+(.+)$/m);
20985
- if (headingMatch) return headingMatch[1].trim();
20986
- } catch {
20987
- }
20988
- return void 0;
20989
- }
20990
- async function handleDocScan(adapter2, config2, args) {
20991
- if (!adapter2.searchDocs) {
20992
- return errorResponse("Doc registry not available \u2014 requires pg adapter.");
20993
- }
20994
- const includePlans = args.include_plans ?? false;
20995
- const registered = await adapter2.searchDocs({ limit: 500, status: "all" });
20996
- const registeredPaths = new Set(registered.map((d) => d.path));
20997
- const docsDir = join9(config2.projectRoot, "docs");
20998
- const docsFiles = scanMdFiles(docsDir, config2.projectRoot);
20999
- const unregisteredDocs = docsFiles.filter((f) => !registeredPaths.has(f));
21000
- let unregisteredPlans = [];
21001
- if (includePlans) {
21002
- const plansDir = join9(homedir2(), ".claude", "plans");
21003
- if (existsSync6(plansDir)) {
21004
- const planFiles = scanMdFiles(plansDir, plansDir);
21005
- unregisteredPlans = planFiles.map((f) => `plans/${f}`).filter((f) => !registeredPaths.has(f)).map((f) => ({
21006
- path: f,
21007
- title: extractTitle(join9(plansDir, f.replace("plans/", "")))
21008
- }));
21009
- }
21010
- }
21011
- const lines = [];
21012
- if (unregisteredDocs.length === 0 && unregisteredPlans.length === 0) {
21013
- return textResponse("All docs are registered. No unregistered files found.");
21014
- }
21015
- if (unregisteredDocs.length > 0) {
21016
- lines.push(`## Unregistered Docs (${unregisteredDocs.length})`);
21017
- for (const f of unregisteredDocs) {
21018
- const title = extractTitle(join9(config2.projectRoot, f));
21019
- lines.push(`- \`${f}\`${title ? ` \u2014 ${title}` : ""}`);
21020
- }
21021
- }
21022
- if (unregisteredPlans.length > 0) {
21023
- lines.push("", `## Unregistered Plans (${unregisteredPlans.length})`);
21024
- for (const p of unregisteredPlans) {
21025
- lines.push(`- \`${p.path}\`${p.title ? ` \u2014 ${p.title}` : ""}`);
21026
- }
21027
- }
21028
- lines.push("", `Use \`doc_register\` to register these files.`);
21029
- return textResponse(lines.join("\n"));
21030
- }
21031
-
21032
21749
  // src/tools/sibling-ads.ts
21033
21750
  var getSiblingAdsTool = {
21034
21751
  name: "get_sibling_ads",
21035
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 },
21036
21754
  inputSchema: {
21037
21755
  type: "object",
21038
21756
  properties: {
@@ -21214,6 +21932,7 @@ var lastPrepareContextBytes2;
21214
21932
  var handoffGenerateTool = {
21215
21933
  name: "handoff_generate",
21216
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 },
21217
21936
  inputSchema: {
21218
21937
  type: "object",
21219
21938
  properties: {
@@ -21324,12 +22043,14 @@ function isEnabled() {
21324
22043
  }
21325
22044
  function emitTelemetryEvent(event) {
21326
22045
  if (!isEnabled()) return;
22046
+ const userId = event.user_id ?? process.env["PAPI_USER_ID"] ?? void 0;
21327
22047
  const body = {
21328
22048
  project_id: event.project_id,
21329
22049
  tool_name: event.tool_name,
21330
22050
  event_type: event.event_type,
21331
22051
  metadata: event.metadata ?? {}
21332
22052
  };
22053
+ if (userId) body["user_id"] = userId;
21333
22054
  fetch(`${TELEMETRY_SUPABASE_URL}/rest/v1/telemetry_events`, {
21334
22055
  method: "POST",
21335
22056
  headers: {
@@ -21376,7 +22097,6 @@ var TOOLS_REQUIRING_PAPI = /* @__PURE__ */ new Set([
21376
22097
  "idea",
21377
22098
  "bug",
21378
22099
  "ad_hoc",
21379
- "health",
21380
22100
  "board_reconcile",
21381
22101
  "review_list",
21382
22102
  "review_submit",
@@ -21458,7 +22178,6 @@ function createServer(adapter2, config2) {
21458
22178
  bugTool,
21459
22179
  adHocTool,
21460
22180
  boardReconcileTool,
21461
- healthTool,
21462
22181
  releaseTool,
21463
22182
  reviewListTool,
21464
22183
  reviewSubmitTool,
@@ -21550,9 +22269,6 @@ function createServer(adapter2, config2) {
21550
22269
  case "board_reconcile":
21551
22270
  result = await handleBoardReconcile(adapter2, config2, safeArgs);
21552
22271
  break;
21553
- case "health":
21554
- result = await handleHealth(adapter2);
21555
- break;
21556
22272
  case "release":
21557
22273
  result = await handleRelease(adapter2, config2, safeArgs);
21558
22274
  break;
@@ -21599,12 +22315,13 @@ function createServer(adapter2, config2) {
21599
22315
  delete result._usage;
21600
22316
  delete result._contextBytes;
21601
22317
  delete result._contextUtilisation;
21602
- if (contextBytes !== void 0 || contextUtilisation !== void 0) {
21603
- try {
21604
- const metric = buildMetric(name, elapsed, usage, void 0, contextBytes, contextUtilisation);
21605
- await adapter2.appendToolMetric(metric);
21606
- } catch {
21607
- }
22318
+ const isError = result.content.some((c) => c.text.startsWith("Error:") || c.text.startsWith("\u274C"));
22319
+ try {
22320
+ const metric = buildMetric(name, elapsed, usage, void 0, contextBytes, contextUtilisation);
22321
+ metric.success = !isError;
22322
+ adapter2.appendToolMetric(metric).catch(() => {
22323
+ });
22324
+ } catch {
21608
22325
  }
21609
22326
  const telemetryProjectId = process.env["PAPI_PROJECT_ID"];
21610
22327
  if (telemetryProjectId) {
@@ -21612,7 +22329,6 @@ function createServer(adapter2, config2) {
21612
22329
  adapter_type: config2.adapterType
21613
22330
  });
21614
22331
  const isApplyMode = safeArgs.mode === "apply";
21615
- const isError = result.content.some((c) => c.text.startsWith("Error:") || c.text.startsWith("\u274C"));
21616
22332
  if (!isError) {
21617
22333
  if (name === "setup" && isApplyMode) {
21618
22334
  emitMilestone(telemetryProjectId, "setup_completed");