@papi-ai/server 0.7.4-alpha.4 → 0.7.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -4747,18 +4747,20 @@ var init_dist3 = __esm({
4747
4747
  }
4748
4748
  async getProject(id) {
4749
4749
  const [row] = await this.sql`
4750
- SELECT * FROM projects WHERE id = ${id}
4750
+ SELECT id, slug, name, repo_url, papi_dir, user_id, created_at, updated_at FROM projects WHERE id = ${id}
4751
4751
  `;
4752
4752
  return row ?? null;
4753
4753
  }
4754
4754
  async listProjects(filter) {
4755
4755
  if (filter?.slug) {
4756
4756
  return this.sql`
4757
- SELECT * FROM projects WHERE slug = ${filter.slug} ORDER BY created_at
4757
+ SELECT id, slug, name, repo_url, papi_dir, user_id, created_at, updated_at FROM projects WHERE slug = ${filter.slug} ORDER BY created_at
4758
+ LIMIT 200 -- bounded: project list per user
4758
4759
  `;
4759
4760
  }
4760
4761
  return this.sql`
4761
- SELECT * FROM projects ORDER BY created_at
4762
+ SELECT id, slug, name, repo_url, papi_dir, user_id, created_at, updated_at FROM projects ORDER BY created_at
4763
+ LIMIT 200 -- bounded: project list per user
4762
4764
  `;
4763
4765
  }
4764
4766
  async updateProject(id, updates) {
@@ -4818,7 +4820,7 @@ var init_dist3 = __esm({
4818
4820
  }
4819
4821
  async getSharedDecision(id) {
4820
4822
  const [row] = await this.sql`
4821
- SELECT * FROM shared_decisions WHERE id = ${id}
4823
+ 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
4824
  `;
4823
4825
  return row ?? null;
4824
4826
  }
@@ -4838,7 +4840,8 @@ var init_dist3 = __esm({
4838
4840
  }
4839
4841
  if (conditions.length === 0) {
4840
4842
  return this.sql`
4841
- SELECT * FROM shared_decisions WHERE archived_at IS NULL ORDER BY created_at
4843
+ 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
4844
+ LIMIT 200 -- cross-project shared decisions; 200 is ample
4842
4845
  `;
4843
4846
  }
4844
4847
  let where = conditions[0];
@@ -4846,7 +4849,8 @@ var init_dist3 = __esm({
4846
4849
  where = this.sql`${where} AND ${conditions[i]}`;
4847
4850
  }
4848
4851
  return this.sql`
4849
- SELECT * FROM shared_decisions WHERE ${where} ORDER BY created_at
4852
+ 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
4853
+ LIMIT 200
4850
4854
  `;
4851
4855
  }
4852
4856
  async updateSharedDecision(id, updates) {
@@ -4915,7 +4919,7 @@ var init_dist3 = __esm({
4915
4919
  }
4916
4920
  async getAcknowledgement(id) {
4917
4921
  const [row] = await this.sql`
4918
- SELECT * FROM acknowledgements WHERE id = ${id}
4922
+ SELECT id, decision_id, project_id, status, comments, responded_at, created_at, updated_at FROM acknowledgements WHERE id = ${id}
4919
4923
  `;
4920
4924
  return row ?? null;
4921
4925
  }
@@ -4932,7 +4936,8 @@ var init_dist3 = __esm({
4932
4936
  }
4933
4937
  if (conditions.length === 0) {
4934
4938
  return this.sql`
4935
- SELECT * FROM acknowledgements ORDER BY created_at
4939
+ SELECT id, decision_id, project_id, status, comments, responded_at, created_at, updated_at FROM acknowledgements ORDER BY created_at
4940
+ LIMIT 500 -- acknowledgements grow with decisions × projects
4936
4941
  `;
4937
4942
  }
4938
4943
  let where = conditions[0];
@@ -4940,7 +4945,8 @@ var init_dist3 = __esm({
4940
4945
  where = this.sql`${where} AND ${conditions[i]}`;
4941
4946
  }
4942
4947
  return this.sql`
4943
- SELECT * FROM acknowledgements WHERE ${where} ORDER BY created_at
4948
+ SELECT id, decision_id, project_id, status, comments, responded_at, created_at, updated_at FROM acknowledgements WHERE ${where} ORDER BY created_at
4949
+ LIMIT 500
4944
4950
  `;
4945
4951
  }
4946
4952
  // -------------------------------------------------------------------------
@@ -4956,7 +4962,7 @@ var init_dist3 = __esm({
4956
4962
  }
4957
4963
  async getSharedMilestone(id) {
4958
4964
  const [row] = await this.sql`
4959
- SELECT * FROM shared_milestones WHERE id = ${id}
4965
+ SELECT id, title, description, status, target_date, completed_at, created_at, updated_at, archived_at FROM shared_milestones WHERE id = ${id}
4960
4966
  `;
4961
4967
  return row ?? null;
4962
4968
  }
@@ -4970,7 +4976,8 @@ var init_dist3 = __esm({
4970
4976
  }
4971
4977
  if (conditions.length === 0) {
4972
4978
  return this.sql`
4973
- SELECT * FROM shared_milestones WHERE archived_at IS NULL ORDER BY created_at
4979
+ 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
4980
+ LIMIT 200
4974
4981
  `;
4975
4982
  }
4976
4983
  let where = conditions[0];
@@ -4978,7 +4985,8 @@ var init_dist3 = __esm({
4978
4985
  where = this.sql`${where} AND ${conditions[i]}`;
4979
4986
  }
4980
4987
  return this.sql`
4981
- SELECT * FROM shared_milestones WHERE ${where} ORDER BY created_at
4988
+ SELECT id, title, description, status, target_date, completed_at, created_at, updated_at, archived_at FROM shared_milestones WHERE ${where} ORDER BY created_at
4989
+ LIMIT 200
4982
4990
  `;
4983
4991
  }
4984
4992
  async updateSharedMilestone(id, updates) {
@@ -5016,7 +5024,7 @@ var init_dist3 = __esm({
5016
5024
  }
5017
5025
  async listMilestoneDependencies(milestoneId) {
5018
5026
  return this.sql`
5019
- SELECT * FROM milestone_dependencies WHERE milestone_id = ${milestoneId}
5027
+ SELECT id, milestone_id, depends_on_id FROM milestone_dependencies WHERE milestone_id = ${milestoneId}
5020
5028
  `;
5021
5029
  }
5022
5030
  async deleteMilestoneDependency(id) {
@@ -5045,7 +5053,7 @@ var init_dist3 = __esm({
5045
5053
  }
5046
5054
  async getProjectContribution(id) {
5047
5055
  const [row] = await this.sql`
5048
- SELECT * FROM project_contributions WHERE id = ${id}
5056
+ SELECT id, milestone_id, project_id, status, required_phases, notes, delivered_at, created_at, updated_at FROM project_contributions WHERE id = ${id}
5049
5057
  `;
5050
5058
  return row ?? null;
5051
5059
  }
@@ -5062,7 +5070,8 @@ var init_dist3 = __esm({
5062
5070
  }
5063
5071
  if (conditions.length === 0) {
5064
5072
  return this.sql`
5065
- SELECT * FROM project_contributions ORDER BY created_at
5073
+ SELECT id, milestone_id, project_id, status, required_phases, notes, delivered_at, created_at, updated_at FROM project_contributions ORDER BY created_at
5074
+ LIMIT 500
5066
5075
  `;
5067
5076
  }
5068
5077
  let where = conditions[0];
@@ -5070,7 +5079,8 @@ var init_dist3 = __esm({
5070
5079
  where = this.sql`${where} AND ${conditions[i]}`;
5071
5080
  }
5072
5081
  return this.sql`
5073
- SELECT * FROM project_contributions WHERE ${where} ORDER BY created_at
5082
+ 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
5083
+ LIMIT 500
5074
5084
  `;
5075
5085
  }
5076
5086
  async updateProjectContribution(id, updates) {
@@ -5108,7 +5118,7 @@ var init_dist3 = __esm({
5108
5118
  }
5109
5119
  async getActiveNorthStar(projectId) {
5110
5120
  const [row] = await this.sql`
5111
- SELECT * FROM north_stars
5121
+ SELECT id, project_id, statement, set_at, superseded_by_id, superseded_at, created_at FROM north_stars
5112
5122
  WHERE project_id = ${projectId}
5113
5123
  AND superseded_by_id IS NULL
5114
5124
  ORDER BY created_at DESC
@@ -5126,7 +5136,8 @@ var init_dist3 = __esm({
5126
5136
  }
5127
5137
  if (conditions.length === 0) {
5128
5138
  return this.sql`
5129
- SELECT * FROM north_stars ORDER BY created_at
5139
+ SELECT id, project_id, statement, set_at, superseded_by_id, superseded_at, created_at FROM north_stars ORDER BY created_at
5140
+ LIMIT 200
5130
5141
  `;
5131
5142
  }
5132
5143
  let where = conditions[0];
@@ -5134,7 +5145,8 @@ var init_dist3 = __esm({
5134
5145
  where = this.sql`${where} AND ${conditions[i]}`;
5135
5146
  }
5136
5147
  return this.sql`
5137
- SELECT * FROM north_stars WHERE ${where} ORDER BY created_at
5148
+ SELECT id, project_id, statement, set_at, superseded_by_id, superseded_at, created_at FROM north_stars WHERE ${where} ORDER BY created_at
5149
+ LIMIT 200
5138
5150
  `;
5139
5151
  }
5140
5152
  /**
@@ -5148,7 +5160,7 @@ var init_dist3 = __esm({
5148
5160
  return this.sql.begin(async (_tx) => {
5149
5161
  const tx = _tx;
5150
5162
  const [old] = await tx`
5151
- SELECT * FROM north_stars WHERE id = ${id}
5163
+ SELECT id, project_id, statement, set_at, superseded_by_id, superseded_at, created_at FROM north_stars WHERE id = ${id}
5152
5164
  `;
5153
5165
  if (!old) {
5154
5166
  throw new Error(`NorthStar not found: ${id}`);
@@ -5191,7 +5203,7 @@ var init_dist3 = __esm({
5191
5203
  }
5192
5204
  async getConflictAlert(id) {
5193
5205
  const [row] = await this.sql`
5194
- SELECT * FROM conflict_alerts WHERE id = ${id}
5206
+ 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
5207
  `;
5196
5208
  return row ?? null;
5197
5209
  }
@@ -5205,7 +5217,8 @@ var init_dist3 = __esm({
5205
5217
  }
5206
5218
  if (conditions.length === 0) {
5207
5219
  return this.sql`
5208
- SELECT * FROM conflict_alerts ORDER BY created_at
5220
+ 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
5221
+ LIMIT 200
5209
5222
  `;
5210
5223
  }
5211
5224
  let where = conditions[0];
@@ -5213,7 +5226,8 @@ var init_dist3 = __esm({
5213
5226
  where = this.sql`${where} AND ${conditions[i]}`;
5214
5227
  }
5215
5228
  return this.sql`
5216
- SELECT * FROM conflict_alerts WHERE ${where} ORDER BY created_at
5229
+ 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
5230
+ LIMIT 200
5217
5231
  `;
5218
5232
  }
5219
5233
  async updateConflictAlert(id, updates) {
@@ -6176,19 +6190,23 @@ EXCEPTION WHEN duplicate_object THEN NULL; END $$;
6176
6190
  }
6177
6191
  async getActiveDecisions() {
6178
6192
  const rows = await this.sql`
6179
- SELECT * FROM active_decisions
6193
+ SELECT id, display_id, title, confidence, superseded, superseded_by, created_cycle, modified_cycle, body, outcome, revision_count
6194
+ FROM active_decisions
6180
6195
  WHERE project_id = ${this.projectId}
6181
6196
  ORDER BY display_id
6197
+ LIMIT 200 -- bounded: ADs are bounded by project lifecycle, 200 is a safe ceiling
6182
6198
  `;
6183
6199
  return rows.map(rowToActiveDecision);
6184
6200
  }
6185
6201
  async getSiblingAds(projectIds) {
6186
6202
  if (projectIds.length === 0) return [];
6187
6203
  const rows = await this.sql`
6188
- SELECT *, project_id FROM active_decisions
6204
+ SELECT id, display_id, title, confidence, superseded, superseded_by, created_cycle, modified_cycle, body, outcome, revision_count, project_id
6205
+ FROM active_decisions
6189
6206
  WHERE project_id = ANY(${projectIds}::uuid[])
6190
6207
  AND superseded = false
6191
6208
  ORDER BY project_id, display_id
6209
+ LIMIT 500 -- cross-project query: 500 covers up to 10 projects × 50 ADs each
6192
6210
  `;
6193
6211
  return rows.map((row) => ({
6194
6212
  ...rowToActiveDecision(row),
@@ -6198,7 +6216,8 @@ EXCEPTION WHEN duplicate_object THEN NULL; END $$;
6198
6216
  async getCycleLog(limit) {
6199
6217
  if (limit != null) {
6200
6218
  const rows2 = await this.sql`
6201
- SELECT * FROM planning_log_entries
6219
+ SELECT id, cycle_number, title, content, carry_forward, notes, task_count, effort_points
6220
+ FROM planning_log_entries
6202
6221
  WHERE project_id = ${this.projectId}
6203
6222
  ORDER BY cycle_number DESC
6204
6223
  LIMIT ${limit}
@@ -6206,18 +6225,22 @@ EXCEPTION WHEN duplicate_object THEN NULL; END $$;
6206
6225
  return rows2.map(rowToCycleLogEntry);
6207
6226
  }
6208
6227
  const rows = await this.sql`
6209
- SELECT * FROM planning_log_entries
6228
+ SELECT id, cycle_number, title, content, carry_forward, notes, task_count, effort_points
6229
+ FROM planning_log_entries
6210
6230
  WHERE project_id = ${this.projectId}
6211
6231
  ORDER BY cycle_number DESC
6232
+ LIMIT 500 -- 500 cycles is ~years of history, sufficient ceiling
6212
6233
  `;
6213
6234
  return rows.map(rowToCycleLogEntry);
6214
6235
  }
6215
6236
  async getCycleLogSince(cycleNumber) {
6216
6237
  const rows = await this.sql`
6217
- SELECT * FROM planning_log_entries
6238
+ SELECT id, cycle_number, title, content, carry_forward, notes, task_count, effort_points
6239
+ FROM planning_log_entries
6218
6240
  WHERE project_id = ${this.projectId}
6219
6241
  AND cycle_number >= ${cycleNumber}
6220
6242
  ORDER BY cycle_number DESC
6243
+ LIMIT 500 -- bounded by cycle range, 500 is a safe ceiling
6221
6244
  `;
6222
6245
  return rows.map(rowToCycleLogEntry);
6223
6246
  }
@@ -6449,7 +6472,8 @@ ${newParts.join("\n")}` : newParts.join("\n");
6449
6472
  const sinceCycle = input.sinceCycle ?? 0;
6450
6473
  const hasPending = input.hasPendingActions ?? false;
6451
6474
  const rows = await this.sql`
6452
- SELECT * FROM doc_registry
6475
+ SELECT id, title, type, path, status, summary, tags, cycle_created, cycle_updated, superseded_by, actions, created_at, updated_at
6476
+ FROM doc_registry
6453
6477
  WHERE project_id = ${this.projectId}
6454
6478
  AND (${matchAllStatuses} OR status = ${status})
6455
6479
  AND (${input.type ?? null}::text IS NULL OR type = ${input.type ?? null})
@@ -6479,9 +6503,11 @@ ${newParts.join("\n")}` : newParts.join("\n");
6479
6503
  async getDoc(idOrPath) {
6480
6504
  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
6505
  const rows = isUuid ? await this.sql`
6482
- SELECT * FROM doc_registry WHERE id = ${idOrPath} AND project_id = ${this.projectId}
6506
+ SELECT id, title, type, path, status, summary, tags, cycle_created, cycle_updated, superseded_by, actions, created_at, updated_at
6507
+ FROM doc_registry WHERE id = ${idOrPath} AND project_id = ${this.projectId}
6483
6508
  ` : await this.sql`
6484
- SELECT * FROM doc_registry WHERE path = ${idOrPath} AND project_id = ${this.projectId}
6509
+ SELECT id, title, type, path, status, summary, tags, cycle_created, cycle_updated, superseded_by, actions, created_at, updated_at
6510
+ FROM doc_registry WHERE path = ${idOrPath} AND project_id = ${this.projectId}
6485
6511
  `;
6486
6512
  if (rows.length === 0) return null;
6487
6513
  const r = rows[0];
@@ -6640,9 +6666,11 @@ ${newParts.join("\n")}` : newParts.join("\n");
6640
6666
  async queryBoard(options) {
6641
6667
  if (!options) {
6642
6668
  const rows2 = await this.sql`
6643
- SELECT * FROM cycle_tasks
6669
+ SELECT id, project_id, display_id, title, status, priority, complexity, module, epic, phase, owner, reviewed, cycle, created_cycle, created_at, why, depends_on, notes, closure_reason, state_history, build_handoff, build_report, task_type, maturity, stage_id, doc_ref, source, updated_at
6670
+ FROM cycle_tasks
6644
6671
  WHERE project_id = ${this.projectId}
6645
6672
  ORDER BY display_id
6673
+ LIMIT 2000 -- hard ceiling; single project task count won't approach this
6646
6674
  `;
6647
6675
  return rows2.map(rowToTask);
6648
6676
  }
@@ -6679,22 +6707,28 @@ ${newParts.join("\n")}` : newParts.join("\n");
6679
6707
  where = this.sql`${where} AND ${conditions[i]}`;
6680
6708
  }
6681
6709
  const rows = await this.sql`
6682
- SELECT * FROM cycle_tasks WHERE ${where} ORDER BY display_id
6710
+ SELECT id, project_id, display_id, title, status, priority, complexity, module, epic, phase, owner, reviewed, cycle, created_cycle, created_at, why, depends_on, notes, closure_reason, state_history, build_handoff, build_report, task_type, maturity, stage_id, doc_ref, source, updated_at
6711
+ FROM cycle_tasks WHERE ${where} ORDER BY display_id
6712
+ LIMIT 2000 -- matches no-options path ceiling
6683
6713
  `;
6684
6714
  return rows.map(rowToTask);
6685
6715
  }
6686
6716
  async getTask(id) {
6687
6717
  const [row] = await this.sql`
6688
- SELECT * FROM cycle_tasks
6718
+ SELECT id, project_id, display_id, title, status, priority, complexity, module, epic, phase, owner, reviewed, cycle, created_cycle, created_at, why, depends_on, notes, closure_reason, state_history, build_handoff, build_report, task_type, maturity, stage_id, doc_ref, source, updated_at
6719
+ FROM cycle_tasks
6689
6720
  WHERE project_id = ${this.projectId} AND display_id = ${id}
6721
+ LIMIT 1
6690
6722
  `;
6691
6723
  return row ? rowToTask(row) : null;
6692
6724
  }
6693
6725
  async getTasks(ids) {
6694
6726
  if (ids.length === 0) return [];
6695
6727
  const rows = await this.sql`
6696
- SELECT * FROM cycle_tasks
6728
+ SELECT id, project_id, display_id, title, status, priority, complexity, module, epic, phase, owner, reviewed, cycle, created_cycle, created_at, why, depends_on, notes, closure_reason, state_history, build_handoff, build_report, task_type, maturity, stage_id, doc_ref, source, updated_at
6729
+ FROM cycle_tasks
6697
6730
  WHERE project_id = ${this.projectId} AND display_id = ANY(${ids})
6731
+ LIMIT 2000 -- matches board ceiling; ids[] won't exceed this in practice
6698
6732
  `;
6699
6733
  return rows.map(rowToTask);
6700
6734
  }
@@ -6804,7 +6838,8 @@ ${newParts.join("\n")}` : newParts.join("\n");
6804
6838
  completed, actual_effort, estimated_effort, scope_accuracy,
6805
6839
  surprises, discovered_issues, architecture_notes,
6806
6840
  commit_sha, files_changed, related_decisions, handoff_accuracy,
6807
- corrections_count, brief_implications, dead_ends
6841
+ corrections_count, brief_implications, dead_ends,
6842
+ started_at, completed_at, tool_call_count
6808
6843
  ) VALUES (
6809
6844
  ${this.projectId}, ${displayId}, ${report.taskId}, ${report.taskName},
6810
6845
  ${report.date}, ${report.cycle}, ${report.completed},
@@ -6814,13 +6849,15 @@ ${newParts.join("\n")}` : newParts.join("\n");
6814
6849
  ${report.handoffAccuracy ? this.sql.json(report.handoffAccuracy) : null},
6815
6850
  ${report.correctionsCount ?? 0},
6816
6851
  ${report.briefImplications ? this.sql.json(report.briefImplications) : null},
6817
- ${report.deadEnds ?? null}
6852
+ ${report.deadEnds ?? null},
6853
+ ${report.startedAt ?? null}, ${report.completedAt ?? null}, ${report.toolCallCount ?? 0}
6818
6854
  )
6819
6855
  `;
6820
6856
  }
6821
6857
  async getRecentBuildReports(count) {
6822
6858
  const rows = await this.sql`
6823
- SELECT * FROM build_reports
6859
+ 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
6860
+ FROM build_reports
6824
6861
  WHERE project_id = ${this.projectId}
6825
6862
  ORDER BY created_at DESC
6826
6863
  LIMIT ${count}
@@ -6836,9 +6873,11 @@ ${newParts.join("\n")}` : newParts.join("\n");
6836
6873
  }
6837
6874
  async getBuildReportsSince(cycleNumber) {
6838
6875
  const rows = await this.sql`
6839
- SELECT * FROM build_reports
6876
+ 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
6877
+ FROM build_reports
6840
6878
  WHERE project_id = ${this.projectId} AND cycle >= ${cycleNumber}
6841
6879
  ORDER BY created_at
6880
+ LIMIT 1000 -- bounded by cycle range; 1000 covers ~200 cycles × 5 tasks
6842
6881
  `;
6843
6882
  return rows.map(rowToBuildReport);
6844
6883
  }
@@ -6865,25 +6904,29 @@ ${newParts.join("\n")}` : newParts.join("\n");
6865
6904
  let rows;
6866
6905
  if (opts?.cycleNumber && opts?.category) {
6867
6906
  rows = await this.sql`
6868
- SELECT * FROM cycle_learnings
6907
+ SELECT id, project_id, task_id, cycle_number, category, severity, summary, detail, tags, related_decision, action_taken, action_ref, created_at
6908
+ FROM cycle_learnings
6869
6909
  WHERE project_id = ${this.projectId} AND cycle_number = ${opts.cycleNumber} AND category = ${opts.category}
6870
6910
  ORDER BY created_at DESC LIMIT ${limit}
6871
6911
  `;
6872
6912
  } else if (opts?.cycleNumber) {
6873
6913
  rows = await this.sql`
6874
- SELECT * FROM cycle_learnings
6914
+ SELECT id, project_id, task_id, cycle_number, category, severity, summary, detail, tags, related_decision, action_taken, action_ref, created_at
6915
+ FROM cycle_learnings
6875
6916
  WHERE project_id = ${this.projectId} AND cycle_number = ${opts.cycleNumber}
6876
6917
  ORDER BY created_at DESC LIMIT ${limit}
6877
6918
  `;
6878
6919
  } else if (opts?.category) {
6879
6920
  rows = await this.sql`
6880
- SELECT * FROM cycle_learnings
6921
+ SELECT id, project_id, task_id, cycle_number, category, severity, summary, detail, tags, related_decision, action_taken, action_ref, created_at
6922
+ FROM cycle_learnings
6881
6923
  WHERE project_id = ${this.projectId} AND category = ${opts.category}
6882
6924
  ORDER BY created_at DESC LIMIT ${limit}
6883
6925
  `;
6884
6926
  } else {
6885
6927
  rows = await this.sql`
6886
- SELECT * FROM cycle_learnings
6928
+ SELECT id, project_id, task_id, cycle_number, category, severity, summary, detail, tags, related_decision, action_taken, action_ref, created_at
6929
+ FROM cycle_learnings
6887
6930
  WHERE project_id = ${this.projectId}
6888
6931
  ORDER BY created_at DESC LIMIT ${limit}
6889
6932
  `;
@@ -6937,7 +6980,8 @@ ${newParts.join("\n")}` : newParts.join("\n");
6937
6980
  async getRecentReviews(count) {
6938
6981
  const limit = count ?? 20;
6939
6982
  const rows = await this.sql`
6940
- SELECT * FROM reviews
6983
+ SELECT id, display_id, task_id, stage, reviewer, verdict, cycle, date, comments, handoff_revision, build_commit_sha, auto_review
6984
+ FROM reviews
6941
6985
  WHERE project_id = ${this.projectId}
6942
6986
  ORDER BY created_at DESC
6943
6987
  LIMIT ${limit}
@@ -7071,9 +7115,11 @@ ${newParts.join("\n")}` : newParts.join("\n");
7071
7115
  // -------------------------------------------------------------------------
7072
7116
  async readPhases() {
7073
7117
  const rows = await this.sql`
7074
- SELECT * FROM phases
7118
+ SELECT id, slug, label, description, status, sort_order, stage_id
7119
+ FROM phases
7075
7120
  WHERE project_id = ${this.projectId}
7076
7121
  ORDER BY sort_order
7122
+ LIMIT 200 -- phases are bounded by project structure
7077
7123
  `;
7078
7124
  return rows.map(rowToPhase);
7079
7125
  }
@@ -7123,21 +7169,24 @@ ${newParts.join("\n")}` : newParts.join("\n");
7123
7169
  INSERT INTO tool_call_metrics (
7124
7170
  project_id, timestamp, tool, duration_ms,
7125
7171
  input_tokens, output_tokens, estimated_cost_usd, model, cycle_number,
7126
- context_bytes, context_utilisation
7172
+ context_bytes, context_utilisation, success
7127
7173
  ) VALUES (
7128
7174
  ${this.projectId}, ${metric.timestamp}, ${metric.tool}, ${metric.durationMs},
7129
7175
  ${metric.inputTokens ?? null}, ${metric.outputTokens ?? null},
7130
7176
  ${metric.estimatedCostUsd ?? null}, ${metric.model ?? null},
7131
7177
  ${metric.cycleNumber ?? null},
7132
- ${metric.contextBytes ?? null}, ${metric.contextUtilisation ?? null}
7178
+ ${metric.contextBytes ?? null}, ${metric.contextUtilisation ?? null},
7179
+ ${metric.success ?? true}
7133
7180
  )
7134
7181
  `;
7135
7182
  }
7136
7183
  async readToolMetrics() {
7137
7184
  const rows = await this.sql`
7138
- SELECT * FROM tool_call_metrics
7185
+ SELECT timestamp, tool, duration_ms, input_tokens, output_tokens, estimated_cost_usd, model, cycle_number, context_bytes, context_utilisation
7186
+ FROM tool_call_metrics
7139
7187
  WHERE project_id = ${this.projectId}
7140
7188
  ORDER BY timestamp
7189
+ LIMIT 5000 -- metrics are high-volume; task-1189 adds pagination for dashboard
7141
7190
  `;
7142
7191
  return rows.map(rowToToolCallMetric);
7143
7192
  }
@@ -7146,10 +7195,12 @@ ${newParts.join("\n")}` : newParts.join("\n");
7146
7195
  // -------------------------------------------------------------------------
7147
7196
  async getCostSummary(cycleNumber) {
7148
7197
  const metrics = cycleNumber != null ? await this.sql`
7149
- SELECT * FROM tool_call_metrics
7198
+ SELECT timestamp, tool, duration_ms, input_tokens, output_tokens, estimated_cost_usd, model, cycle_number, context_bytes, context_utilisation
7199
+ FROM tool_call_metrics
7150
7200
  WHERE project_id = ${this.projectId} AND cycle_number = ${cycleNumber}
7151
7201
  ` : await this.sql`
7152
- SELECT * FROM tool_call_metrics
7202
+ SELECT timestamp, tool, duration_ms, input_tokens, output_tokens, estimated_cost_usd, model, cycle_number, context_bytes, context_utilisation
7203
+ FROM tool_call_metrics
7153
7204
  WHERE project_id = ${this.projectId}
7154
7205
  `;
7155
7206
  let totalCostUsd = 0;
@@ -7186,7 +7237,8 @@ ${newParts.join("\n")}` : newParts.join("\n");
7186
7237
  }
7187
7238
  async getCostSnapshots() {
7188
7239
  const rows = await this.sql`
7189
- SELECT * FROM cost_snapshots
7240
+ SELECT cycle, date, total_cost_usd, total_input_tokens, total_output_tokens, total_calls
7241
+ FROM cost_snapshots
7190
7242
  WHERE project_id = ${this.projectId}
7191
7243
  ORDER BY cycle
7192
7244
  `;
@@ -7213,9 +7265,11 @@ ${newParts.join("\n")}` : newParts.join("\n");
7213
7265
  }
7214
7266
  async readCycleMetrics() {
7215
7267
  const rows = await this.sql`
7216
- SELECT * FROM cycle_metrics_snapshots
7268
+ SELECT cycle, date, accuracy, velocity
7269
+ FROM cycle_metrics_snapshots
7217
7270
  WHERE project_id = ${this.projectId}
7218
7271
  ORDER BY cycle
7272
+ LIMIT 500 -- one row per cycle; 500 is ~years of history
7219
7273
  `;
7220
7274
  return rows.map(rowToCycleMetrics);
7221
7275
  }
@@ -7224,9 +7278,11 @@ ${newParts.join("\n")}` : newParts.join("\n");
7224
7278
  // -------------------------------------------------------------------------
7225
7279
  async readCycles() {
7226
7280
  const rows = await this.sql`
7227
- SELECT * FROM cycles
7281
+ SELECT id, number, status, start_date, end_date, goals, board_health, task_ids
7282
+ FROM cycles
7228
7283
  WHERE project_id = ${this.projectId}
7229
7284
  ORDER BY number
7285
+ LIMIT 500 -- one row per cycle; 500 is years of history
7230
7286
  `;
7231
7287
  return rows.map(rowToCycle);
7232
7288
  }
@@ -7273,25 +7329,31 @@ ${newParts.join("\n")}` : newParts.join("\n");
7273
7329
  // -------------------------------------------------------------------------
7274
7330
  async readHorizons() {
7275
7331
  const rows = await this.sql`
7276
- SELECT * FROM horizons
7332
+ SELECT id, slug, label, description, status, sort_order, project_id, created_at, updated_at
7333
+ FROM horizons
7277
7334
  WHERE project_id = ${this.projectId}
7278
7335
  ORDER BY sort_order, created_at
7336
+ LIMIT 50 -- horizons are high-level; 50 is an ample ceiling
7279
7337
  `;
7280
7338
  return rows.map(rowToHorizon);
7281
7339
  }
7282
7340
  async readStages(horizonId) {
7283
7341
  if (horizonId) {
7284
7342
  const rows2 = await this.sql`
7285
- SELECT * FROM stages
7343
+ SELECT id, slug, label, description, status, sort_order, horizon_id, project_id, exit_criteria, created_at, updated_at
7344
+ FROM stages
7286
7345
  WHERE project_id = ${this.projectId} AND horizon_id = ${horizonId}
7287
7346
  ORDER BY sort_order, created_at
7347
+ LIMIT 100 -- stages per horizon are bounded; 100 is ample
7288
7348
  `;
7289
7349
  return rows2.map(rowToStage);
7290
7350
  }
7291
7351
  const rows = await this.sql`
7292
- SELECT * FROM stages
7352
+ SELECT id, slug, label, description, status, sort_order, horizon_id, project_id, exit_criteria, created_at, updated_at
7353
+ FROM stages
7293
7354
  WHERE project_id = ${this.projectId}
7294
7355
  ORDER BY sort_order, created_at
7356
+ LIMIT 200 -- all stages across horizons; 200 is a safe ceiling
7295
7357
  `;
7296
7358
  return rows.map(rowToStage);
7297
7359
  }
@@ -7321,7 +7383,8 @@ ${newParts.join("\n")}` : newParts.join("\n");
7321
7383
  }
7322
7384
  async getActiveStage() {
7323
7385
  const rows = await this.sql`
7324
- SELECT * FROM stages
7386
+ SELECT id, slug, label, description, status, sort_order, horizon_id, project_id, exit_criteria, created_at, updated_at
7387
+ FROM stages
7325
7388
  WHERE project_id = ${this.projectId} AND status = 'Active'
7326
7389
  ORDER BY sort_order
7327
7390
  LIMIT 1
@@ -7433,7 +7496,8 @@ ${newParts.join("\n")}` : newParts.join("\n");
7433
7496
  }
7434
7497
  async getDecisionEvents(decisionId, limit) {
7435
7498
  const rows = await this.sql`
7436
- SELECT * FROM decision_events
7499
+ SELECT id, decision_id, event_type, cycle, source, source_ref, detail, created_at
7500
+ FROM decision_events
7437
7501
  WHERE project_id = ${this.projectId} AND decision_id = ${decisionId}
7438
7502
  ORDER BY created_at DESC
7439
7503
  LIMIT ${limit ?? 50}
@@ -7442,9 +7506,11 @@ ${newParts.join("\n")}` : newParts.join("\n");
7442
7506
  }
7443
7507
  async getDecisionEventsSince(cycle) {
7444
7508
  const rows = await this.sql`
7445
- SELECT * FROM decision_events
7509
+ SELECT id, decision_id, event_type, cycle, source, source_ref, detail, created_at
7510
+ FROM decision_events
7446
7511
  WHERE project_id = ${this.projectId} AND cycle >= ${cycle}
7447
7512
  ORDER BY created_at
7513
+ LIMIT 1000 -- bounded by cycle range; 1000 is ample
7448
7514
  `;
7449
7515
  return rows.map(rowToDecisionEvent);
7450
7516
  }
@@ -7471,7 +7537,8 @@ ${newParts.join("\n")}` : newParts.join("\n");
7471
7537
  }
7472
7538
  async getDecisionScores(decisionId) {
7473
7539
  const rows = await this.sql`
7474
- SELECT * FROM decision_scores
7540
+ SELECT id, decision_id, cycle, effort, risk, reversibility, scale_cost, lock_in, total_score, rationale, created_at
7541
+ FROM decision_scores
7475
7542
  WHERE project_id = ${this.projectId} AND decision_id = ${decisionId}
7476
7543
  ORDER BY cycle
7477
7544
  `;
@@ -7479,7 +7546,7 @@ ${newParts.join("\n")}` : newParts.join("\n");
7479
7546
  }
7480
7547
  async getLatestDecisionScores() {
7481
7548
  const rows = await this.sql`
7482
- SELECT DISTINCT ON (decision_id) *
7549
+ SELECT DISTINCT ON (decision_id) id, decision_id, cycle, effort, risk, reversibility, scale_cost, lock_in, total_score, rationale, created_at
7483
7550
  FROM decision_scores
7484
7551
  WHERE project_id = ${this.projectId}
7485
7552
  ORDER BY decision_id, cycle DESC
@@ -8394,6 +8461,9 @@ Check PAPI_PROJECT_ID in your .mcp.json config. Find your project ID in the PAPI
8394
8461
  readToolMetrics() {
8395
8462
  return this.invoke("readToolMetrics");
8396
8463
  }
8464
+ insertPlanRun(entry) {
8465
+ return this.invoke("insertPlanRun", [entry]);
8466
+ }
8397
8467
  getCostSummary(cycleNumber) {
8398
8468
  return this.invoke("getCostSummary", [cycleNumber]);
8399
8469
  }
@@ -8520,6 +8590,46 @@ Check PAPI_PROJECT_ID in your .mcp.json config. Find your project ID in the PAPI
8520
8590
  submitBugReport(report) {
8521
8591
  return this.invoke("submitBugReport", [report]);
8522
8592
  }
8593
+ // --- Doc Registry ---
8594
+ registerDoc(entry) {
8595
+ return this.invoke("registerDoc", [entry]);
8596
+ }
8597
+ searchDocs(input) {
8598
+ return this.invoke("searchDocs", [input]);
8599
+ }
8600
+ getDoc(idOrPath) {
8601
+ return this.invoke("getDoc", [idOrPath]);
8602
+ }
8603
+ updateDocStatus(id, status, supersededBy) {
8604
+ return this.invoke("updateDocStatus", [id, status, supersededBy]);
8605
+ }
8606
+ // --- Cycle Learnings ---
8607
+ appendCycleLearnings(learnings) {
8608
+ return this.invoke("appendCycleLearnings", [learnings]);
8609
+ }
8610
+ getCycleLearnings(opts) {
8611
+ return this.invoke("getCycleLearnings", [opts]);
8612
+ }
8613
+ getCycleLearningPatterns() {
8614
+ return this.invoke("getCycleLearningPatterns", []);
8615
+ }
8616
+ updateCycleLearningActionRef(learningId, taskDisplayId) {
8617
+ return this.invoke("updateCycleLearningActionRef", [learningId, taskDisplayId]);
8618
+ }
8619
+ // --- Strategy Review Drafts ---
8620
+ savePendingReviewResponse(cycleNumber, rawResponse) {
8621
+ return this.invoke("savePendingReviewResponse", [cycleNumber, rawResponse]);
8622
+ }
8623
+ getPendingReviewResponse() {
8624
+ return this.invoke("getPendingReviewResponse", []);
8625
+ }
8626
+ clearPendingReviewResponse() {
8627
+ return this.invoke("clearPendingReviewResponse", []);
8628
+ }
8629
+ // --- Active Decisions ---
8630
+ confirmPendingActiveDecisions(cycleNumber) {
8631
+ return this.invoke("confirmPendingActiveDecisions", [cycleNumber]);
8632
+ }
8523
8633
  // --- Atomic plan write-back ---
8524
8634
  async planWriteBack(payload) {
8525
8635
  const raw = await this.invoke("planWriteBack", [payload]);
@@ -8975,8 +9085,10 @@ function detectUnrecordedCommits(cwd, baseBranch) {
8975
9085
  // release commits
8976
9086
  /^[a-f0-9]+ Merge /,
8977
9087
  // merge commits from PRs
8978
- /chore\(task-/
9088
+ /chore\(task-/,
8979
9089
  // task-related housekeeping
9090
+ /chore: dogfood log/
9091
+ // automated dogfood log entries post-release
8980
9092
  ];
8981
9093
  return output.split("\n").filter((line) => line.trim() && !CYCLE_PATTERNS.some((p) => p.test(line))).map((line) => {
8982
9094
  const spaceIdx = line.indexOf(" ");
@@ -10071,7 +10183,7 @@ Standard planning cycle with full board review.
10071
10183
  - **What to do with premature tasks:** Leave them in Backlog. Do NOT generate BUILD HANDOFFs for them. If a high-priority task fails the maturity gate due to phase prerequisites or dependencies, note it in the cycle log: "task-XXX deferred \u2014 Phase N prerequisites not met". Raw tasks are NOT premature \u2014 they just need scoping (see Task maturity above).
10072
10184
 
10073
10185
  7. **Recommendation** \u2014 Select tasks for this cycle:
10074
- **Pre-assigned tasks:** If a "Pre-Assigned Tasks" section is provided in the context below, those tasks are ALREADY committed to this cycle by the user. Include them automatically \u2014 do NOT re-evaluate whether they belong. Generate BUILD HANDOFFs for each. Count their effort toward the cycle budget. Then fill remaining slots (up to 5 total) from the backlog using the priority rules below.
10186
+ **Pre-assigned tasks:** If a "Pre-Assigned Tasks" section is provided in the context below, those tasks are ALREADY committed to this cycle by the user. Include them automatically \u2014 do NOT re-evaluate whether they belong. Generate BUILD HANDOFFs for each. Count their effort toward the cycle budget. Then fill remaining slots from the backlog using the priority rules and cycle sizing rules below.
10075
10187
  **If USER DIRECTION is provided above:** Follow the user's stated focus. Pick the highest-impact task that aligns with their direction. The user knows what they need. Only deviate if a genuine P0 Critical fix exists (broken builds, data loss).
10076
10188
  **Otherwise, select by priority level then impact:**
10077
10189
  - **P0 Critical** \u2014 Broken, blocking, or data-loss risk. Always first.
@@ -10095,7 +10207,7 @@ Standard planning cycle with full board review.
10095
10207
  - **Backlog as steering wheel:** Task priority and notes in the backlog are the user's primary control mechanism over what gets planned. Respect the priority rankings and read task notes carefully \u2014 they contain user intent that shapes scope and scheduling.
10096
10208
  - **Planning quality is the bar:** Strategy review depth and plan quality set the standard for the product. Do not cut corners on analysis depth, triage thoroughness, or handoff specificity \u2014 these are what users experience as PAPI's value.
10097
10209
 
10098
- 10. **BUILD HANDOFFs** \u2014 Generate a full BUILD HANDOFF block for the recommended task and up to 4 additional high-priority unblocked tasks (5 total max). Include each handoff in the \`cycleHandoffs\` array in the structured output. The handoffs are written to each task on the board for durability. Remaining tasks will get handoffs in subsequent plans \u2014 do NOT try to cover the entire backlog.
10210
+ 10. **BUILD HANDOFFs** \u2014 Generate a full BUILD HANDOFF block for every task selected for this cycle. Include each handoff in the \`cycleHandoffs\` array in the structured output. The handoffs are written to each task on the board for durability.
10099
10211
  **SKIP existing handoffs:** Tasks marked with "Has BUILD HANDOFF: yes" or "\u2713 handoff" on the board already have a valid handoff from a previous plan. Do NOT regenerate handoffs for these tasks \u2014 omit them from the \`cycleHandoffs\` array entirely. Only generate handoffs for tasks that do NOT have one yet. Exception: if a task's dependencies have been completed since its handoff was written, or a relevant Active Decision has changed, you MAY regenerate its handoff \u2014 but note this explicitly in the cycle log.
10100
10212
  **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
10213
  **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.
@@ -10258,7 +10370,7 @@ Standard planning cycle with full board review.
10258
10370
  - **What to do with premature tasks:** Leave them in Backlog. Do NOT generate BUILD HANDOFFs for them. If a high-priority task fails the maturity gate due to phase prerequisites or dependencies, note it in the cycle log: "task-XXX deferred \u2014 Phase N prerequisites not met". Raw tasks are NOT premature \u2014 they just need scoping (see Task maturity above).
10259
10371
 
10260
10372
  7. **Recommendation** \u2014 Select tasks for this cycle:
10261
- **Pre-assigned tasks:** If a "Pre-Assigned Tasks" section is provided in the context below, those tasks are ALREADY committed to this cycle by the user. Include them automatically \u2014 do NOT re-evaluate whether they belong. Generate BUILD HANDOFFs for each. Count their effort toward the cycle budget. Then fill remaining slots (up to 5 total) from the backlog using the priority rules below.
10373
+ **Pre-assigned tasks:** If a "Pre-Assigned Tasks" section is provided in the context below, those tasks are ALREADY committed to this cycle by the user. Include them automatically \u2014 do NOT re-evaluate whether they belong. Generate BUILD HANDOFFs for each. Count their effort toward the cycle budget. Then fill remaining slots from the backlog using the priority rules and cycle sizing rules below.
10262
10374
  **If USER DIRECTION is provided above:** Follow the user's stated focus. Pick the highest-impact task that aligns with their direction. The user knows what they need. Only deviate if a genuine P0 Critical fix exists (broken builds, data loss).
10263
10375
  **Otherwise, select by priority level then impact:**
10264
10376
  - **P0 Critical** \u2014 Broken, blocking, or data-loss risk. Always first.
@@ -10282,7 +10394,7 @@ Standard planning cycle with full board review.
10282
10394
  - **Backlog as steering wheel:** Task priority and notes in the backlog are the user's primary control mechanism over what gets planned. Respect the priority rankings and read task notes carefully \u2014 they contain user intent that shapes scope and scheduling.
10283
10395
  - **Planning quality is the bar:** Strategy review depth and plan quality set the standard for the product. Do not cut corners on analysis depth, triage thoroughness, or handoff specificity \u2014 these are what users experience as PAPI's value.
10284
10396
 
10285
- 10. **BUILD HANDOFFs** \u2014 Generate a full BUILD HANDOFF block for the recommended task and up to 4 additional high-priority unblocked tasks (5 total max). Include each handoff in the \`cycleHandoffs\` array in the structured output. The handoffs are written to each task on the board for durability. Remaining tasks will get handoffs in subsequent plans \u2014 do NOT try to cover the entire backlog.
10397
+ 10. **BUILD HANDOFFs** \u2014 Generate a full BUILD HANDOFF block for every task selected for this cycle. Include each handoff in the \`cycleHandoffs\` array in the structured output. The handoffs are written to each task on the board for durability.
10286
10398
  **SKIP existing handoffs:** Tasks marked with "Has BUILD HANDOFF: yes" or "\u2713 handoff" on the board already have a valid handoff from a previous plan. Do NOT regenerate handoffs for these tasks \u2014 omit them from the \`cycleHandoffs\` array entirely. Only generate handoffs for tasks that do NOT have one yet. Exception: if a task's dependencies have been completed since its handoff was written, or a relevant Active Decision has changed, you MAY regenerate its handoff \u2014 but note this explicitly in the cycle log.
10287
10399
  **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.
10288
10400
  **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.
@@ -10341,6 +10453,24 @@ function buildPlanUserMessage(ctx) {
10341
10453
  }) : PLAN_FULL_INSTRUCTIONS;
10342
10454
  parts.push(instructions);
10343
10455
  }
10456
+ if (ctx.skipHandoffs) {
10457
+ parts.push(
10458
+ "",
10459
+ "## SKIP HANDOFFS MODE",
10460
+ "",
10461
+ "**IMPORTANT OVERRIDE:** Do NOT generate BUILD HANDOFF blocks in this plan run.",
10462
+ "Select tasks for the cycle using all the normal criteria (Steps 1-7), but SKIP Step 10 (BUILD HANDOFFs) entirely.",
10463
+ "",
10464
+ "In your Part 2 structured output:",
10465
+ "- Set `cycleHandoffs` to an EMPTY array `[]`",
10466
+ '- Add a `cycleTaskIds` array with the task IDs you selected for the cycle: `["task-123", "task-456", ...]`',
10467
+ "- All other fields (cycleLogTitle, cycleLogContent, newTasks, boardCorrections, activeDecisions, etc.) work as normal.",
10468
+ "",
10469
+ "BUILD HANDOFFs will be generated separately via `handoff_generate` after this plan completes.",
10470
+ "This reduces your cognitive load \u2014 focus on triage, selection, and board management only.",
10471
+ ""
10472
+ );
10473
+ }
10344
10474
  parts.push("", "---", "", "## PROJECT CONTEXT", "");
10345
10475
  parts.push("### Product Brief", "", ctx.productBrief, "");
10346
10476
  if (ctx.northStar) {
@@ -10517,6 +10647,7 @@ function coerceStructuredOutput(parsed) {
10517
10647
  id: coerceToString(ad.id),
10518
10648
  body: coerceToString(ad.body)
10519
10649
  })) : [];
10650
+ const cycleTaskIds = Array.isArray(parsed.cycleTaskIds) ? parsed.cycleTaskIds.map((id) => coerceToString(id)) : void 0;
10520
10651
  return {
10521
10652
  cycleLogTitle: coerceToString(parsed.cycleLogTitle),
10522
10653
  cycleLogContent: coerceToString(parsed.cycleLogContent),
@@ -10527,6 +10658,7 @@ function coerceStructuredOutput(parsed) {
10527
10658
  strategicDirection: coerceToString(parsed.strategicDirection),
10528
10659
  recommendedTaskId: parsed.recommendedTaskId === null ? null : coerceToString(parsed.recommendedTaskId),
10529
10660
  cycleHandoffs,
10661
+ cycleTaskIds,
10530
10662
  newTasks,
10531
10663
  boardCorrections,
10532
10664
  productBrief: parsed.productBrief === null ? null : coerceToString(parsed.productBrief),
@@ -10604,6 +10736,8 @@ You MUST cover these 5 sections. Each is mandatory.
10604
10736
  - Note any hierarchy/phase issues worth correcting (1-2 bullets max)
10605
10737
  - Delete ADs that are legacy, process-level, or redundant without discussion
10606
10738
 
10739
+ **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.
10740
+
10607
10741
  ## CONDITIONAL SECTIONS (include only when genuinely useful \u2014 most reviews should have 0-2 of these)
10608
10742
 
10609
10743
  6. **Security Posture Review** \u2014 Only if \`[SECURITY]\` tags exist in recent cycle logs.
@@ -10824,6 +10958,9 @@ function buildReviewUserMessage(ctx) {
10824
10958
  if (ctx.taskComments) {
10825
10959
  parts.push("### Task Discussion (Recent Comments)", "", ctx.taskComments, "");
10826
10960
  }
10961
+ if (ctx.docActionStaleness) {
10962
+ parts.push("### Doc Action Staleness", "", ctx.docActionStaleness, "");
10963
+ }
10827
10964
  return parts.join("\n");
10828
10965
  }
10829
10966
  function parseReviewStructuredOutput(raw) {
@@ -11268,6 +11405,8 @@ function buildPlanSlackSummary(cycleNumber, mode, data) {
11268
11405
  return `${h.taskId}: ${title}`;
11269
11406
  }).join(", ");
11270
11407
  parts.push(`*Recommended:* ${tasks}`);
11408
+ } else if (data.cycleTaskIds && data.cycleTaskIds.length > 0) {
11409
+ parts.push(`*Recommended:* ${data.cycleTaskIds.join(", ")} (handoffs pending)`);
11271
11410
  }
11272
11411
  if (data.strategicDirection) {
11273
11412
  parts.push(`*Direction:* ${data.strategicDirection}`);
@@ -11316,7 +11455,7 @@ function formatPreAssignedTasks(tasks, targetCycle) {
11316
11455
  "",
11317
11456
  ...lines,
11318
11457
  "",
11319
- "These tasks MUST be included in the cycle. Generate BUILD HANDOFFs for each. Fill remaining slots (up to 5 total) from the backlog."
11458
+ "These tasks MUST be included in the cycle. Generate BUILD HANDOFFs for each. Fill remaining slots from the backlog based on cycle sizing rules."
11320
11459
  ].join("\n");
11321
11460
  }
11322
11461
  function pushAfterCommit(config2) {
@@ -11614,9 +11753,10 @@ async function assembleContext(adapter2, mode, _config, filters, focus) {
11614
11753
  adapter2.readCycleMetrics(),
11615
11754
  adapter2.getRecentReviews(5)
11616
11755
  ]);
11756
+ let leanBuildReports = [];
11617
11757
  try {
11618
- const reports2 = await adapter2.getRecentBuildReports(50);
11619
- metricsSnapshots2 = computeSnapshotsFromBuildReports(reports2);
11758
+ leanBuildReports = await adapter2.getRecentBuildReports(50);
11759
+ metricsSnapshots2 = computeSnapshotsFromBuildReports(leanBuildReports);
11620
11760
  } catch {
11621
11761
  }
11622
11762
  timings["metricsAndReviews"] = t();
@@ -11645,7 +11785,7 @@ async function assembleContext(adapter2, mode, _config, filters, focus) {
11645
11785
  adapter2.searchDocs?.({ status: "active", limit: 5 }),
11646
11786
  adapter2.getCycleLog(5),
11647
11787
  adapter2.queryBoard({ status: ["Backlog", "In Cycle", "Ready"] }),
11648
- adapter2.getRecentBuildReports(10),
11788
+ Promise.resolve(leanBuildReports.slice(0, 10)),
11649
11789
  adapter2.getContextHashes?.(health.totalCycles) ?? Promise.resolve(null)
11650
11790
  ]);
11651
11791
  timings["parallelReads"] = t();
@@ -11743,7 +11883,7 @@ ${lines.join("\n")}`;
11743
11883
  return { context: ctx2, contextHashes: newHashes2 };
11744
11884
  }
11745
11885
  t = startTimer();
11746
- const [decisions, reportsSinceCycle, log, tasks, rawMetricsSnapshots, reviews, phases, dogfoodEntries] = await Promise.all([
11886
+ const [decisions, reportsSinceCycle, log, tasks, rawMetricsSnapshots, reviews, phases, dogfoodEntries, allBuildReports] = await Promise.all([
11747
11887
  adapter2.getActiveDecisions(),
11748
11888
  adapter2.getBuildReportsSince(health.totalCycles ?? 0),
11749
11889
  adapter2.getCycleLog(3),
@@ -11751,10 +11891,11 @@ ${lines.join("\n")}`;
11751
11891
  adapter2.readCycleMetrics(),
11752
11892
  adapter2.getRecentReviews(5),
11753
11893
  adapter2.readPhases(),
11754
- readDogfoodEntries(_config.projectRoot, 5, adapter2)
11894
+ readDogfoodEntries(_config.projectRoot, 5, adapter2),
11895
+ adapter2.getRecentBuildReports(50)
11755
11896
  ]);
11756
11897
  timings["fullQueries"] = t();
11757
- const reports = reportsSinceCycle.length > 0 ? reportsSinceCycle : await adapter2.getRecentBuildReports(5);
11898
+ const reports = reportsSinceCycle.length > 0 ? reportsSinceCycle : allBuildReports.slice(0, 5);
11758
11899
  t = startTimer();
11759
11900
  const [
11760
11901
  allReportsResult,
@@ -11765,7 +11906,7 @@ ${lines.join("\n")}`;
11765
11906
  docsResultFull,
11766
11907
  contextHashesResultFull
11767
11908
  ] = await Promise.allSettled([
11768
- adapter2.getRecentBuildReports(50),
11909
+ Promise.resolve(allBuildReports),
11769
11910
  detectReviewPatterns(reviews, health.totalCycles, 5),
11770
11911
  adapter2.getPendingRecommendations(),
11771
11912
  assembleDiscoveryCanvasText(adapter2),
@@ -11883,7 +12024,7 @@ ${cleanContent}`;
11883
12024
  }
11884
12025
  return { taskId: h.taskId, handoff: parsed };
11885
12026
  }).filter((h) => h.handoff != null);
11886
- const cycleTaskIds = (data.cycleHandoffs ?? []).map((h) => h.taskId);
12027
+ const cycleTaskIds = data.cycleTaskIds?.length ? data.cycleTaskIds : (data.cycleHandoffs ?? []).map((h) => h.taskId);
11887
12028
  const cycle = {
11888
12029
  id: `cycle-${newCycleNumber}`,
11889
12030
  number: newCycleNumber,
@@ -11984,12 +12125,12 @@ ${cleanContent}`;
11984
12125
  if (!newCycle) {
11985
12126
  verifyWarnings.push(`Post-write verification FAILED: cycle ${newCycleNumber} entity not found after commit \u2014 data may not have persisted`);
11986
12127
  } else {
11987
- const expectedHandoffs = data.cycleHandoffs?.length ?? 0;
12128
+ const expectedTaskCount = data.cycleTaskIds?.length ?? data.cycleHandoffs?.length ?? 0;
11988
12129
  const actualCycleTasks = boardTasks.filter((t) => t.cycle === newCycleNumber).length;
11989
- if (expectedHandoffs > 0 && actualCycleTasks === 0) {
11990
- verifyWarnings.push(`Post-write verification FAILED: cycle ${newCycleNumber} exists but has 0 tasks assigned (expected ${expectedHandoffs}) \u2014 task cycle assignment may have failed`);
11991
- } else if (expectedHandoffs > 0 && actualCycleTasks < expectedHandoffs) {
11992
- verifyWarnings.push(`Post-write verification WARNING: cycle ${newCycleNumber} has ${actualCycleTasks} tasks but expected ${expectedHandoffs} \u2014 some task assignments may have failed`);
12130
+ if (expectedTaskCount > 0 && actualCycleTasks === 0) {
12131
+ verifyWarnings.push(`Post-write verification FAILED: cycle ${newCycleNumber} exists but has 0 tasks assigned (expected ${expectedTaskCount}) \u2014 task cycle assignment may have failed`);
12132
+ } else if (expectedTaskCount > 0 && actualCycleTasks < expectedTaskCount) {
12133
+ verifyWarnings.push(`Post-write verification WARNING: cycle ${newCycleNumber} has ${actualCycleTasks} tasks but expected ${expectedTaskCount} \u2014 some task assignments may have failed`);
11993
12134
  }
11994
12135
  }
11995
12136
  } catch {
@@ -12000,7 +12141,7 @@ ${cleanContent}`;
12000
12141
  const correctionCount = data.boardCorrections?.length ?? 0;
12001
12142
  const newTaskCount = result.newTaskIdMap.size;
12002
12143
  const adCount = data.activeDecisions?.length ?? 0;
12003
- const taskIds = (data.cycleHandoffs ?? []).map((h) => result.newTaskIdMap.get(h.taskId) ?? h.taskId);
12144
+ const taskIds = data.cycleTaskIds?.length ? data.cycleTaskIds.map((id) => result.newTaskIdMap.get(id) ?? id) : (data.cycleHandoffs ?? []).map((h) => result.newTaskIdMap.get(h.taskId) ?? h.taskId);
12004
12145
  return {
12005
12146
  priorityLockNotes: result.priorityLockNotes,
12006
12147
  newTaskIdMap: result.newTaskIdMap,
@@ -12042,15 +12183,7 @@ ${cleanContent}`;
12042
12183
  taskCount: cycleTaskCount > 0 ? cycleTaskCount : void 0,
12043
12184
  effortPoints: cycleEffortPoints > 0 ? cycleEffortPoints : void 0
12044
12185
  });
12045
- const healthPromise = adapter2.getCycleHealth().then(
12046
- (health) => adapter2.setCycleHealth({
12047
- totalCycles: newCycleNumber,
12048
- cyclesSinceLastStrategyReview: health.cyclesSinceLastStrategyReview + 1,
12049
- lastFullMode: newCycleNumber,
12050
- boardHealth: data.boardHealth,
12051
- strategicDirection: data.strategicDirection
12052
- })
12053
- );
12186
+ const healthPromise = Promise.resolve();
12054
12187
  const newTaskIdMap = /* @__PURE__ */ new Map();
12055
12188
  const createTasksPromise = (async () => {
12056
12189
  if (!data.newTasks || data.newTasks.length === 0) return;
@@ -12241,7 +12374,7 @@ ${cleanContent}`;
12241
12374
  })();
12242
12375
  const cycleEntityPromise = (async () => {
12243
12376
  try {
12244
- const cycleTaskIds = (data.cycleHandoffs ?? []).map((h) => newTaskIdMap.get(h.taskId) ?? h.taskId);
12377
+ const cycleTaskIds = data.cycleTaskIds?.length ? data.cycleTaskIds.map((id) => newTaskIdMap.get(id) ?? id) : (data.cycleHandoffs ?? []).map((h) => newTaskIdMap.get(h.taskId) ?? h.taskId);
12245
12378
  const cycle = {
12246
12379
  id: `cycle-${newCycleNumber}`,
12247
12380
  number: newCycleNumber,
@@ -12271,7 +12404,7 @@ ${cleanContent}`;
12271
12404
  const correctionCount = data.boardCorrections?.length ?? 0;
12272
12405
  const newTaskCount = newTaskIdMap.size;
12273
12406
  const adCount = data.activeDecisions?.length ?? 0;
12274
- const taskIds = (data.cycleHandoffs ?? []).map((h) => newTaskIdMap.get(h.taskId) ?? h.taskId);
12407
+ const taskIds = data.cycleTaskIds?.length ? data.cycleTaskIds.map((id) => newTaskIdMap.get(id) ?? id) : (data.cycleHandoffs ?? []).map((h) => newTaskIdMap.get(h.taskId) ?? h.taskId);
12275
12408
  return {
12276
12409
  priorityLockNotes,
12277
12410
  newTaskIdMap,
@@ -12418,7 +12551,7 @@ async function processLlmOutput(adapter2, config2, rawOutput, mode, cycleNumber,
12418
12551
  writeSummary
12419
12552
  };
12420
12553
  }
12421
- async function preparePlan(adapter2, config2, filters, focus, force, handoffsOnly) {
12554
+ async function preparePlan(adapter2, config2, filters, focus, force, handoffsOnly, skipHandoffs) {
12422
12555
  const prepareTimer = startTimer();
12423
12556
  let t = startTimer();
12424
12557
  const { mode, cycleNumber, strategyReviewWarning } = await validateAndPrepare(adapter2, force);
@@ -12468,6 +12601,7 @@ async function preparePlan(adapter2, config2, filters, focus, force, handoffsOnl
12468
12601
  if (mode !== "bootstrap" && context.productBrief.includes(TEMPLATE_MARKER)) {
12469
12602
  throw new Error("TEMPLATE_BRIEF");
12470
12603
  }
12604
+ if (skipHandoffs) context.skipHandoffs = true;
12471
12605
  t = startTimer();
12472
12606
  const userMessage = buildPlanUserMessage(context);
12473
12607
  const buildMessageMs = t();
@@ -12640,9 +12774,11 @@ async function propagatePhaseStatus(adapter2) {
12640
12774
  var lastPrepareContextHashes;
12641
12775
  var lastPrepareUserMessage;
12642
12776
  var lastPrepareContextBytes;
12777
+ var lastPrepareCycleNumber;
12778
+ var lastPrepareSkipHandoffs;
12643
12779
  var planTool = {
12644
12780
  name: "plan",
12645
- description: 'Run once per cycle to generate BUILD HANDOFFs for up to 5 tasks. 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.',
12781
+ 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`.',
12646
12782
  inputSchema: {
12647
12783
  type: "object",
12648
12784
  properties: {
@@ -12695,6 +12831,10 @@ var planTool = {
12695
12831
  handoffs_only: {
12696
12832
  type: "boolean",
12697
12833
  description: "Skip backlog analysis and task selection. Only generate BUILD HANDOFFs for tasks already assigned to the target cycle. Requires pre-assigned tasks (set cycle number on tasks first). ~30% of normal plan cost."
12834
+ },
12835
+ skip_handoffs: {
12836
+ type: "boolean",
12837
+ description: "Run full planning (triage, task selection, board management) but skip BUILD HANDOFF generation. Selected tasks are assigned to the cycle without handoffs. Run `handoff_generate` after to create handoffs separately. Reduces planner cognitive load for large backlogs."
12698
12838
  }
12699
12839
  },
12700
12840
  required: []
@@ -12733,7 +12873,12 @@ function formatPlanResult(result) {
12733
12873
  if (result.priorityLockNote) lines.push(result.priorityLockNote.trim());
12734
12874
  if (result.slackWarning) lines.push(result.slackWarning);
12735
12875
  if (result.autoCommitNote) lines.push(result.autoCommitNote.trim());
12736
- lines.push("", `Next: run \`build_list\` to see your cycle tasks, then \`build_execute <task_id>\` to start building.`);
12876
+ if (result.skipHandoffs) {
12877
+ const taskCount = result.writeSummary?.taskIds.length ?? 0;
12878
+ lines.push("", `Next: run \`handoff_generate\` to create BUILD HANDOFFs for your ${taskCount} cycle task(s), then \`build_list\` to start building.`);
12879
+ } else {
12880
+ lines.push("", `Next: run \`build_list\` to see your cycle tasks, then \`build_execute <task_id>\` to start building.`);
12881
+ }
12737
12882
  if (result.contextBytes !== void 0) {
12738
12883
  const kb = (result.contextBytes / 1024).toFixed(1);
12739
12884
  lines.push(`---`, `Context: ${kb}KB`);
@@ -12766,10 +12911,20 @@ async function handlePlan(adapter2, config2, args) {
12766
12911
  const contextHashes = lastPrepareContextHashes;
12767
12912
  const inputContext = lastPrepareUserMessage;
12768
12913
  const contextBytes = lastPrepareContextBytes;
12914
+ const expectedCycleNumber = lastPrepareCycleNumber;
12915
+ const skipHandoffsCached = lastPrepareSkipHandoffs;
12769
12916
  lastPrepareContextHashes = void 0;
12770
12917
  lastPrepareUserMessage = void 0;
12771
12918
  lastPrepareContextBytes = void 0;
12772
- const result = await applyPlan(adapter2, config2, llmResponse, planMode, cycleNumber, strategyReviewWarning, contextHashes, { contextBytes: contextBytes ?? void 0 });
12919
+ lastPrepareCycleNumber = void 0;
12920
+ lastPrepareSkipHandoffs = void 0;
12921
+ const skipHandoffs = args.skip_handoffs === true || skipHandoffsCached === true;
12922
+ if (expectedCycleNumber !== void 0 && cycleNumber !== expectedCycleNumber) {
12923
+ return errorResponse(
12924
+ `cycle_number mismatch: prepare phase returned cycle ${expectedCycleNumber} but apply received ${cycleNumber}. Pass cycle_number: ${expectedCycleNumber} to match the prepare output.`
12925
+ );
12926
+ }
12927
+ const result = await applyPlan(adapter2, config2, llmResponse, planMode, cycleNumber, strategyReviewWarning, contextHashes, { contextBytes: contextBytes ?? void 0, skipHandoffs: skipHandoffs || void 0 });
12773
12928
  let utilisation;
12774
12929
  if (inputContext) {
12775
12930
  try {
@@ -12777,7 +12932,7 @@ async function handlePlan(adapter2, config2, args) {
12777
12932
  } catch {
12778
12933
  }
12779
12934
  }
12780
- const response = formatPlanResult({ ...result, contextUtilisation: utilisation, contextBytes });
12935
+ const response = formatPlanResult({ ...result, contextUtilisation: utilisation, contextBytes, skipHandoffs });
12781
12936
  return {
12782
12937
  ...response,
12783
12938
  ...contextBytes !== void 0 ? { _contextBytes: contextBytes } : {},
@@ -12789,10 +12944,13 @@ async function handlePlan(adapter2, config2, args) {
12789
12944
  await propagatePhaseStatus(adapter2);
12790
12945
  } catch {
12791
12946
  }
12792
- const result = await preparePlan(adapter2, config2, filters, focus, force, handoffsOnly);
12947
+ const skipHandoffs = args.skip_handoffs === true;
12948
+ const result = await preparePlan(adapter2, config2, filters, focus, force, handoffsOnly, skipHandoffs);
12793
12949
  lastPrepareContextHashes = result.contextHashes;
12794
12950
  lastPrepareUserMessage = result.userMessage;
12795
12951
  lastPrepareContextBytes = result.contextBytes;
12952
+ lastPrepareCycleNumber = result.cycleNumber;
12953
+ lastPrepareSkipHandoffs = skipHandoffs || void 0;
12796
12954
  const modeLabel = result.mode === "bootstrap" ? "Bootstrap" : "Full";
12797
12955
  const header = result.strategyReviewWarning ? `${result.strategyReviewWarning}
12798
12956
  ` : "";
@@ -13145,7 +13303,8 @@ async function assembleContext2(adapter2, cycleNumber, cyclesSinceLastReview, pr
13145
13303
  decisionUsage,
13146
13304
  recData,
13147
13305
  pendingRecs,
13148
- registeredDocs
13306
+ registeredDocs,
13307
+ docsWithPendingActions
13149
13308
  ] = await Promise.all([
13150
13309
  adapter2.readProductBrief(),
13151
13310
  adapter2.getActiveDecisions(),
@@ -13169,7 +13328,9 @@ async function assembleContext2(adapter2, cycleNumber, cyclesSinceLastReview, pr
13169
13328
  adapter2.getRecommendationEffectiveness?.()?.catch(() => []) ?? Promise.resolve([]),
13170
13329
  adapter2.getPendingRecommendations().catch(() => []),
13171
13330
  // Doc registry — summaries for strategy review context
13172
- adapter2.searchDocs?.({ status: "active", limit: 10 })?.catch(() => []) ?? Promise.resolve([])
13331
+ adapter2.searchDocs?.({ status: "active", limit: 10 })?.catch(() => []) ?? Promise.resolve([]),
13332
+ // Doc registry — docs with pending actions for staleness audit
13333
+ adapter2.searchDocs?.({ hasPendingActions: true, limit: 20 })?.catch(() => []) ?? Promise.resolve([])
13173
13334
  ]);
13174
13335
  const tasks = [...activeTasks, ...recentDoneTasks];
13175
13336
  const existingAdIds = new Set(decisions.map((d) => d.id));
@@ -13371,6 +13532,44 @@ ${unregistered.slice(0, 10).map((f) => `- ${f}`).join("\n")}`;
13371
13532
  }
13372
13533
  } catch {
13373
13534
  }
13535
+ let docActionStalenessText;
13536
+ try {
13537
+ if (docsWithPendingActions && docsWithPendingActions.length > 0) {
13538
+ const STALE_THRESHOLD = 20;
13539
+ const doneTaskIds = new Set(recentDoneTasks.map((t) => t.displayId ?? t.id));
13540
+ const completed = [];
13541
+ const deferred = [];
13542
+ const stale = [];
13543
+ for (const doc of docsWithPendingActions) {
13544
+ const pendingActions = (doc.actions ?? []).filter((a) => a.status === "pending");
13545
+ if (pendingActions.length === 0) continue;
13546
+ const ageInCycles = cycleNumber - (doc.cycleCreated ?? cycleNumber);
13547
+ for (const action of pendingActions) {
13548
+ const line = ` - **${doc.title}** (C${doc.cycleCreated ?? "?"}): ${action.description}${action.linkedTaskId ? ` [\u2192${action.linkedTaskId}]` : ""}`;
13549
+ if (action.linkedTaskId && doneTaskIds.has(action.linkedTaskId)) {
13550
+ completed.push(line);
13551
+ } else if (ageInCycles > STALE_THRESHOLD) {
13552
+ stale.push(line);
13553
+ } else {
13554
+ deferred.push(line);
13555
+ }
13556
+ }
13557
+ }
13558
+ if (completed.length === 0 && deferred.length === 0 && stale.length === 0) {
13559
+ docActionStalenessText = "Doc Actions: all clear \u2014 no pending actions.";
13560
+ } else {
13561
+ const sections = [];
13562
+ if (stale.length > 0) sections.push(`**Stale (>${STALE_THRESHOLD} cycles, no matching Done task):**
13563
+ ${stale.join("\n")}`);
13564
+ if (completed.length > 0) sections.push(`**Completed but not closed (linked task is Done):**
13565
+ ${completed.join("\n")}`);
13566
+ if (deferred.length > 0) sections.push(`**Deferred (<${STALE_THRESHOLD} cycles, no matching Done task):**
13567
+ ${deferred.join("\n")}`);
13568
+ docActionStalenessText = sections.join("\n\n");
13569
+ }
13570
+ }
13571
+ } catch {
13572
+ }
13374
13573
  logDataSourceSummary("strategy_review_audit", [
13375
13574
  { label: "discoveryCanvas", hasData: discoveryCanvasText !== void 0 },
13376
13575
  { label: "briefImplications", hasData: briefImplicationsText !== void 0 },
@@ -13382,7 +13581,8 @@ ${unregistered.slice(0, 10).map((f) => `- ${f}`).join("\n")}`;
13382
13581
  { label: "registeredDocs", hasData: registeredDocsText !== void 0 },
13383
13582
  { label: "recentPlans", hasData: recentPlansText !== void 0 },
13384
13583
  { label: "unregisteredDocs", hasData: unregisteredDocsText !== void 0 },
13385
- { label: "taskComments", hasData: taskCommentsText !== void 0 }
13584
+ { label: "taskComments", hasData: taskCommentsText !== void 0 },
13585
+ { label: "docActionStaleness", hasData: docActionStalenessText !== void 0 }
13386
13586
  ]);
13387
13587
  const context = {
13388
13588
  sessionNumber: cycleNumber,
@@ -13408,7 +13608,8 @@ ${unregistered.slice(0, 10).map((f) => `- ${f}`).join("\n")}`;
13408
13608
  registeredDocs: registeredDocsText,
13409
13609
  recentPlans: recentPlansText,
13410
13610
  unregisteredDocs: unregisteredDocsText,
13411
- taskComments: taskCommentsText
13611
+ taskComments: taskCommentsText,
13612
+ docActionStaleness: docActionStalenessText
13412
13613
  };
13413
13614
  const BUDGET_SOFT2 = 5e4;
13414
13615
  const BUDGET_HARD2 = 6e4;
@@ -13523,21 +13724,6 @@ ${cleanContent}`;
13523
13724
  });
13524
13725
  } catch {
13525
13726
  }
13526
- try {
13527
- const cycleLog = await adapter2.getCycleLogSince(cycleNumber);
13528
- const currentEntry = cycleLog.find((e) => e.cycleNumber === cycleNumber);
13529
- if (currentEntry) {
13530
- const reviewCompleteText = `Strategy review completed Cycle ${cycleNumber}. Next due ~Cycle ${cycleNumber + 5}.`;
13531
- const existingCf = currentEntry.carryForward ?? "";
13532
- const updatedCf = existingCf ? existingCf.replace(/strategy review (?:due|available|overdue)[^.]*\.[^.]*/gi, reviewCompleteText).trim() : reviewCompleteText;
13533
- const finalCf = updatedCf === existingCf ? `${existingCf} ${reviewCompleteText}`.trim() : updatedCf;
13534
- await adapter2.writeCycleLogEntry({
13535
- ...currentEntry,
13536
- carryForward: finalCf
13537
- });
13538
- }
13539
- } catch {
13540
- }
13541
13727
  if (data.activeDecisionUpdates && data.activeDecisionUpdates.length > 0) {
13542
13728
  await Promise.all(data.activeDecisionUpdates.map(async (ad) => {
13543
13729
  if (ad.action === "delete" && adapter2.deleteActiveDecision) {
@@ -14514,8 +14700,8 @@ async function viewBoard(adapter2, phaseFilter, options) {
14514
14700
  const bi = PRIORITY_ORDER.indexOf(b2.priority);
14515
14701
  const priorityDiff = (ai === -1 ? 999 : ai) - (bi === -1 ? 999 : bi);
14516
14702
  if (priorityDiff !== 0) return priorityDiff;
14517
- const aDate = a.createdAt ?? "";
14518
- const bDate = b2.createdAt ?? "";
14703
+ const aDate = a.createdAt ? String(a.createdAt) : "";
14704
+ const bDate = b2.createdAt ? String(b2.createdAt) : "";
14519
14705
  return bDate.localeCompare(aDate);
14520
14706
  });
14521
14707
  const total = allTasks.length;
@@ -15694,6 +15880,7 @@ async function ensurePapiPermission(projectRoot) {
15694
15880
  }
15695
15881
  }
15696
15882
  async function applySetupOutputs(adapter2, config2, input, briefText, adSeedText, conventionsText) {
15883
+ const warnings = [];
15697
15884
  if (config2.adapterType !== "pg") {
15698
15885
  await writeFile2(join4(config2.papiDir, "PRODUCT_BRIEF.md"), briefText, "utf-8");
15699
15886
  }
@@ -15701,6 +15888,10 @@ async function applySetupOutputs(adapter2, config2, input, briefText, adSeedText
15701
15888
  const briefPhases = parsePhases(briefText);
15702
15889
  if (briefPhases.length > 0) {
15703
15890
  await adapter2.writePhases(briefPhases);
15891
+ } else {
15892
+ warnings.push(
15893
+ "Phase parsing produced 0 phases \u2014 the brief may be missing a valid <!-- PHASES:START --> ... <!-- PHASES:END --> block. Run `plan` and the planner will infer phases from your description. To fix: re-run `setup` with a brief that includes a PHASES YAML block."
15894
+ );
15704
15895
  }
15705
15896
  try {
15706
15897
  if (adapter2.createHorizon && adapter2.createStage && adapter2.linkPhasesToStage) {
@@ -15739,7 +15930,19 @@ async function applySetupOutputs(adapter2, config2, input, briefText, adSeedText
15739
15930
  }
15740
15931
  }
15741
15932
  }
15742
- } catch {
15933
+ } catch (err) {
15934
+ const msg = err instanceof Error ? err.message : String(err);
15935
+ warnings.push(
15936
+ `AD seeding failed \u2014 active decisions were not created. Check that your ad_seed_response is valid JSON. Error: ${msg}`
15937
+ );
15938
+ seededAds = 0;
15939
+ }
15940
+ if (seededAds === 0 && adSeedText) {
15941
+ if (!warnings.some((w) => w.startsWith("AD seeding failed"))) {
15942
+ warnings.push(
15943
+ "AD seeding produced 0 active decisions \u2014 the JSON may be valid but empty or missing required `id` and `body` fields."
15944
+ );
15945
+ }
15743
15946
  }
15744
15947
  }
15745
15948
  if (conventionsText?.trim()) {
@@ -15750,7 +15953,7 @@ async function applySetupOutputs(adapter2, config2, input, briefText, adSeedText
15750
15953
  } catch {
15751
15954
  }
15752
15955
  }
15753
- return { seededAds };
15956
+ return { seededAds, warnings };
15754
15957
  }
15755
15958
  var SKIP_PATTERNS = /* @__PURE__ */ new Set([
15756
15959
  "node_modules",
@@ -15988,7 +16191,7 @@ async function applySetup(adapter2, config2, input, briefText, adSeedText, conve
15988
16191
  if (!briefText.trim()) {
15989
16192
  throw new Error("brief_response is required and cannot be empty.");
15990
16193
  }
15991
- const { seededAds } = await applySetupOutputs(adapter2, config2, input, briefText, adSeedText, conventionsText);
16194
+ const { seededAds, warnings } = await applySetupOutputs(adapter2, config2, input, briefText, adSeedText, conventionsText);
15992
16195
  let createdTasks = 0;
15993
16196
  if (initialTasksText?.trim()) {
15994
16197
  try {
@@ -16073,7 +16276,8 @@ async function applySetup(adapter2, config2, input, briefText, adSeedText, conve
16073
16276
  projectName: input.projectName,
16074
16277
  seededAds,
16075
16278
  createdTasks,
16076
- cursorScaffolded
16279
+ cursorScaffolded,
16280
+ warnings: warnings.length > 0 ? warnings : void 0
16077
16281
  };
16078
16282
  }
16079
16283
 
@@ -16182,8 +16386,12 @@ ${result.seededAds} Active Decision${result.seededAds > 1 ? "s" : ""} seeded bas
16182
16386
  ${result.createdTasks} initial backlog task${result.createdTasks > 1 ? "s" : ""} created from codebase analysis.` : "";
16183
16387
  const constraintsHint = !constraints ? '\n\nTip: consider adding `constraints` (e.g. "must use PostgreSQL", "HIPAA compliant", "offline-first") to improve Active Decision seeding.' : "";
16184
16388
  const editorNote = result.cursorScaffolded ? "\n\nCursor detected \u2014 `.cursor/rules/papi.mdc` scaffolded alongside CLAUDE.md." : "";
16389
+ const warningsNote = result.warnings && result.warnings.length > 0 ? `
16390
+
16391
+ \u26A0\uFE0F **Setup warnings (non-blocking):**
16392
+ ${result.warnings.map((w) => `- ${w}`).join("\n")}` : "";
16185
16393
  return textResponse(
16186
- `${prefix}Product Brief generated and saved.${adNote}${taskNote}${constraintsHint}${editorNote}
16394
+ `${prefix}Product Brief generated and saved.${adNote}${taskNote}${constraintsHint}${editorNote}${warningsNote}
16187
16395
 
16188
16396
  **Important:** Setup created/modified files (CLAUDE.md, .claude/settings.json, docs/). Commit these changes before running \`build_execute\` \u2014 it requires a clean working directory.
16189
16397
 
@@ -16345,6 +16553,7 @@ init_git();
16345
16553
  import { randomUUID as randomUUID9 } from "crypto";
16346
16554
  import { readdirSync as readdirSync3, existsSync as existsSync3, readFileSync } from "fs";
16347
16555
  import { join as join5 } from "path";
16556
+ var buildStartTimes = /* @__PURE__ */ new Map();
16348
16557
  function capitalizeCompleted(value) {
16349
16558
  const map = {
16350
16559
  yes: "Yes",
@@ -16550,6 +16759,7 @@ async function startBuild(adapter2, config2, taskId, options = {}) {
16550
16759
  if (task.status !== "In Progress") {
16551
16760
  await adapter2.updateTaskStatus(taskId, "In Progress");
16552
16761
  }
16762
+ buildStartTimes.set(taskId, (/* @__PURE__ */ new Date()).toISOString());
16553
16763
  let phaseChanges = [];
16554
16764
  try {
16555
16765
  phaseChanges = await propagatePhaseStatus(adapter2);
@@ -16615,8 +16825,11 @@ async function completeBuild(adapter2, config2, taskId, input, options = {}) {
16615
16825
  correctionsCount: input.correctionsCount,
16616
16826
  briefImplications: input.briefImplications,
16617
16827
  deadEnds: input.deadEnds,
16618
- iterationCount
16828
+ iterationCount,
16829
+ startedAt: buildStartTimes.get(taskId) ?? void 0,
16830
+ completedAt: now.toISOString()
16619
16831
  };
16832
+ buildStartTimes.delete(taskId);
16620
16833
  if (input.relatedDecisions) {
16621
16834
  const adIds = input.relatedDecisions.split(",").map((s) => s.trim()).filter(Boolean);
16622
16835
  if (adIds.length > 0) report.relatedDecisions = adIds;
@@ -20253,7 +20466,7 @@ var hierarchyUpdateTool = {
20253
20466
  },
20254
20467
  status: {
20255
20468
  type: "string",
20256
- enum: ["active", "completed", "deferred"],
20469
+ enum: ["Not Started", "In Progress", "Done", "Deferred"],
20257
20470
  description: "The new status to set."
20258
20471
  },
20259
20472
  exit_criteria: {
@@ -20265,7 +20478,7 @@ var hierarchyUpdateTool = {
20265
20478
  required: ["level", "name"]
20266
20479
  }
20267
20480
  };
20268
- var VALID_STATUSES3 = /* @__PURE__ */ new Set(["active", "completed", "deferred"]);
20481
+ var VALID_STATUSES3 = /* @__PURE__ */ new Set(["Not Started", "In Progress", "Done", "Deferred"]);
20269
20482
  async function handleHierarchyUpdate(adapter2, args) {
20270
20483
  const level = args.level;
20271
20484
  const name = args.name;
@@ -20281,7 +20494,7 @@ async function handleHierarchyUpdate(adapter2, args) {
20281
20494
  return errorResponse(`Invalid level "${level}". Must be "phase", "stage", or "horizon".`);
20282
20495
  }
20283
20496
  if (status && !VALID_STATUSES3.has(status)) {
20284
- return errorResponse(`Invalid status "${status}". Must be one of: active, completed, deferred.`);
20497
+ return errorResponse(`Invalid status "${status}". Must be one of: Not Started, In Progress, Done, Deferred.`);
20285
20498
  }
20286
20499
  if (exitCriteria !== void 0 && level !== "stage") {
20287
20500
  return errorResponse("exit_criteria can only be set on stages.");
@@ -20880,7 +21093,7 @@ function extractTitle(filePath) {
20880
21093
  }
20881
21094
  async function handleDocScan(adapter2, config2, args) {
20882
21095
  if (!adapter2.searchDocs) {
20883
- return errorResponse("Doc registry not available \u2014 requires pg adapter.");
21096
+ return errorResponse("Doc registry not available on this adapter.");
20884
21097
  }
20885
21098
  const includePlans = args.include_plans ?? false;
20886
21099
  const registered = await adapter2.searchDocs({ limit: 500, status: "all" });
@@ -20995,6 +21208,217 @@ Check that the project IDs are correct and exist in the same Supabase instance.`
20995
21208
  return textResponse(lines.join("\n"));
20996
21209
  }
20997
21210
 
21211
+ // src/services/handoff.ts
21212
+ init_dist2();
21213
+ async function prepareHandoffs(adapter2, _config, taskIds) {
21214
+ const timer2 = startTimer();
21215
+ const cycles = await adapter2.readCycles();
21216
+ const activeCycle = cycles.find((c) => c.status === "active");
21217
+ if (!activeCycle) {
21218
+ throw new Error("No active cycle found. Run `plan` first to create a cycle.");
21219
+ }
21220
+ const cycleNumber = activeCycle.number;
21221
+ const allTasks = await adapter2.queryBoard({ status: ["Backlog", "In Cycle", "Ready", "In Progress"] });
21222
+ let cycleTasks = allTasks.filter((t) => t.cycle === cycleNumber);
21223
+ if (taskIds?.length) {
21224
+ const idSet = new Set(taskIds);
21225
+ cycleTasks = cycleTasks.filter((t) => idSet.has(t.id));
21226
+ }
21227
+ const tasksNeedingHandoffs = cycleTasks.filter((t) => !t.buildHandoff);
21228
+ if (tasksNeedingHandoffs.length === 0) {
21229
+ throw new Error(
21230
+ taskIds?.length ? `All specified tasks already have BUILD HANDOFFs. Nothing to generate.` : `All ${cycleTasks.length} cycle task(s) already have BUILD HANDOFFs. Nothing to generate.`
21231
+ );
21232
+ }
21233
+ const [decisions, reports, brief] = await Promise.all([
21234
+ adapter2.getActiveDecisions(),
21235
+ adapter2.getRecentBuildReports(10),
21236
+ adapter2.readProductBrief()
21237
+ ]);
21238
+ const northStar = await adapter2.getCurrentNorthStar?.() ?? "";
21239
+ const userMessage = buildHandoffsOnlyUserMessage({
21240
+ cycleNumber: cycleNumber - 1,
21241
+ // buildHandoffsOnlyUserMessage adds +1 internally
21242
+ preAssignedTasks: tasksNeedingHandoffs,
21243
+ activeDecisions: formatActiveDecisionsForPlan(decisions),
21244
+ recentBuildReports: formatBuildReports(reports),
21245
+ productBrief: brief,
21246
+ northStar
21247
+ });
21248
+ const contextBytes = Buffer.byteLength(userMessage, "utf-8");
21249
+ const elapsed = timer2();
21250
+ console.error(`[handoff-perf] prepareHandoffs: ${elapsed}ms, ${contextBytes} bytes, ${tasksNeedingHandoffs.length} task(s)`);
21251
+ const systemPrompt = await getPrompt("plan-system");
21252
+ return {
21253
+ cycleNumber,
21254
+ systemPrompt,
21255
+ userMessage,
21256
+ contextBytes,
21257
+ taskCount: tasksNeedingHandoffs.length
21258
+ };
21259
+ }
21260
+ async function applyHandoffs(adapter2, rawLlmOutput, cycleNumber) {
21261
+ const timer2 = startTimer();
21262
+ const { data } = parseStructuredOutput(rawLlmOutput);
21263
+ if (!data) {
21264
+ throw new Error("Could not parse structured output. Ensure your output includes <!-- PAPI_STRUCTURED_OUTPUT --> with valid JSON.");
21265
+ }
21266
+ const handoffs = data.cycleHandoffs ?? [];
21267
+ if (handoffs.length === 0) {
21268
+ throw new Error("No cycleHandoffs found in structured output. Ensure your output includes handoffs in the cycleHandoffs array.");
21269
+ }
21270
+ const taskIdsToWrite = handoffs.map((h) => h.taskId);
21271
+ const existingHandoffSet = /* @__PURE__ */ new Set();
21272
+ try {
21273
+ const tasks = await adapter2.getTasks(taskIdsToWrite);
21274
+ for (const t of tasks) {
21275
+ if (t.buildHandoff) existingHandoffSet.add(t.id);
21276
+ }
21277
+ } catch {
21278
+ }
21279
+ const written = [];
21280
+ let skipped = 0;
21281
+ const warnings = [];
21282
+ for (const handoff of handoffs) {
21283
+ try {
21284
+ if (existingHandoffSet.has(handoff.taskId)) {
21285
+ console.error(`[handoff] skipping ${handoff.taskId} \u2014 already has handoff`);
21286
+ skipped++;
21287
+ continue;
21288
+ }
21289
+ const parsed = parseBuildHandoff(handoff.buildHandoff);
21290
+ if (!parsed) {
21291
+ warnings.push(`Failed to parse handoff for ${handoff.taskId}`);
21292
+ continue;
21293
+ }
21294
+ if (!parsed.createdAt) {
21295
+ parsed.createdAt = (/* @__PURE__ */ new Date()).toISOString();
21296
+ }
21297
+ await adapter2.updateTask(handoff.taskId, { buildHandoff: parsed });
21298
+ written.push(handoff.taskId);
21299
+ } catch (err) {
21300
+ const msg = err instanceof Error ? err.message : String(err);
21301
+ warnings.push(`Failed to write handoff for ${handoff.taskId}: ${msg}`);
21302
+ }
21303
+ }
21304
+ const elapsed = timer2();
21305
+ console.error(`[handoff-perf] applyHandoffs: ${elapsed}ms, ${written.length} written, ${skipped} skipped`);
21306
+ return {
21307
+ cycleNumber,
21308
+ handoffsWritten: written.length,
21309
+ skipped,
21310
+ taskIds: written,
21311
+ warnings
21312
+ };
21313
+ }
21314
+
21315
+ // src/tools/handoff.ts
21316
+ var lastPrepareCycleNumber2;
21317
+ var lastPrepareContextBytes2;
21318
+ var handoffGenerateTool = {
21319
+ name: "handoff_generate",
21320
+ 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.",
21321
+ inputSchema: {
21322
+ type: "object",
21323
+ properties: {
21324
+ mode: {
21325
+ type: "string",
21326
+ enum: ["prepare", "apply"],
21327
+ description: '"prepare" returns the handoff prompt for you to execute. "apply" accepts your generated output and persists handoffs. Defaults to "prepare" when omitted.'
21328
+ },
21329
+ task_ids: {
21330
+ type: "array",
21331
+ items: { type: "string" },
21332
+ description: "Specific task IDs to generate handoffs for. If omitted, generates for all cycle tasks missing handoffs."
21333
+ },
21334
+ llm_response: {
21335
+ type: "string",
21336
+ description: 'Your raw output from executing the handoff prompt (mode "apply" only). Must include both Part 1 (markdown) and Part 2 (structured JSON after <!-- PAPI_STRUCTURED_OUTPUT -->).'
21337
+ },
21338
+ cycle_number: {
21339
+ type: "number",
21340
+ description: 'The cycle number returned from prepare phase (mode "apply" only).'
21341
+ }
21342
+ },
21343
+ required: []
21344
+ }
21345
+ };
21346
+ async function handleHandoffGenerate(adapter2, config2, args) {
21347
+ const toolMode = args.mode;
21348
+ try {
21349
+ if (toolMode === "apply") {
21350
+ const llmResponse = args.llm_response;
21351
+ if (!llmResponse || !llmResponse.trim()) {
21352
+ return errorResponse('llm_response is required for mode "apply". Pass your complete handoff output including the <!-- PAPI_STRUCTURED_OUTPUT --> block.');
21353
+ }
21354
+ const cycleNumber = typeof args.cycle_number === "number" ? args.cycle_number : lastPrepareCycleNumber2;
21355
+ if (cycleNumber === void 0) {
21356
+ return errorResponse('cycle_number is required for mode "apply". Pass the cycle number from the prepare phase.');
21357
+ }
21358
+ const expectedCycleNumber = lastPrepareCycleNumber2;
21359
+ const contextBytes = lastPrepareContextBytes2;
21360
+ lastPrepareCycleNumber2 = void 0;
21361
+ lastPrepareContextBytes2 = void 0;
21362
+ if (expectedCycleNumber !== void 0 && cycleNumber !== expectedCycleNumber) {
21363
+ return errorResponse(
21364
+ `cycle_number mismatch: prepare phase returned cycle ${expectedCycleNumber} but apply received ${cycleNumber}.`
21365
+ );
21366
+ }
21367
+ const result = await applyHandoffs(adapter2, llmResponse, cycleNumber);
21368
+ const lines = [];
21369
+ lines.push(`**Handoff Generation \u2014 Cycle ${result.cycleNumber}**`);
21370
+ lines.push(`${result.handoffsWritten} handoff(s) written: ${result.taskIds.join(", ")}`);
21371
+ if (result.skipped > 0) lines.push(`${result.skipped} task(s) skipped (already had handoffs)`);
21372
+ if (result.warnings.length > 0) lines.push("\u26A0\uFE0F Warnings: " + result.warnings.join("; "));
21373
+ lines.push("", "Next: run `build_list` to see your cycle tasks, then `build_execute <task_id>` to start building.");
21374
+ if (contextBytes !== void 0) {
21375
+ const kb = (contextBytes / 1024).toFixed(1);
21376
+ lines.push("---", `Context: ${kb}KB`);
21377
+ }
21378
+ return textResponse(lines.join("\n"));
21379
+ }
21380
+ {
21381
+ const taskIds = Array.isArray(args.task_ids) ? args.task_ids.filter((id) => typeof id === "string") : void 0;
21382
+ const result = await prepareHandoffs(adapter2, config2, taskIds);
21383
+ lastPrepareCycleNumber2 = result.cycleNumber;
21384
+ lastPrepareContextBytes2 = result.contextBytes;
21385
+ return textResponse(
21386
+ `## PAPI Handoff Generation \u2014 Prepare Phase (Cycle ${result.cycleNumber})
21387
+
21388
+ Generate BUILD HANDOFFs for ${result.taskCount} task(s) that need them.
21389
+
21390
+ **IMPORTANT:** Your output must have TWO parts:
21391
+ 1. Natural language markdown with BUILD HANDOFF blocks
21392
+ 2. After \`<!-- PAPI_STRUCTURED_OUTPUT -->\`, a JSON block with structured data
21393
+
21394
+ When done, call \`handoff_generate\` again with:
21395
+ - \`mode\`: "apply"
21396
+ - \`llm_response\`: your complete output (both parts)
21397
+ - \`cycle_number\`: ${result.cycleNumber}
21398
+
21399
+ ---
21400
+
21401
+ ### System Prompt
21402
+
21403
+ <system_prompt>
21404
+ ${result.systemPrompt}
21405
+ </system_prompt>
21406
+
21407
+ ---
21408
+
21409
+ ### Context
21410
+
21411
+ <context>
21412
+ ${result.userMessage}
21413
+ </context>`
21414
+ );
21415
+ }
21416
+ } catch (err) {
21417
+ const message = err instanceof Error ? err.message : String(err);
21418
+ return errorResponse(message);
21419
+ }
21420
+ }
21421
+
20998
21422
  // src/lib/telemetry.ts
20999
21423
  var TELEMETRY_SUPABASE_URL = "https://guewgygcpcmrcoppihzx.supabase.co";
21000
21424
  var TELEMETRY_API_KEY = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Imd1ZXdneWdjcGNtcmNvcHBpaHp4Iiwicm9sZSI6ImFub24iLCJpYXQiOjE3NzI2Njk2NTMsImV4cCI6MjA4ODI0NTY1M30.V5Jw7wJgiMpSQPa2mt0ftjyye5ynG1qLlam00yPVNJY";
@@ -21062,7 +21486,8 @@ var TOOLS_REQUIRING_PAPI = /* @__PURE__ */ new Set([
21062
21486
  "review_submit",
21063
21487
  "orient",
21064
21488
  "hierarchy_update",
21065
- "zoom_out"
21489
+ "zoom_out",
21490
+ "handoff_generate"
21066
21491
  ]);
21067
21492
  function createServer(adapter2, config2) {
21068
21493
  const server2 = new Server(
@@ -21148,7 +21573,8 @@ function createServer(adapter2, config2) {
21148
21573
  docRegisterTool,
21149
21574
  docSearchTool,
21150
21575
  docScanTool,
21151
- getSiblingAdsTool
21576
+ getSiblingAdsTool,
21577
+ handoffGenerateTool
21152
21578
  ]
21153
21579
  }));
21154
21580
  server2.setRequestHandler(CallToolRequestSchema, async (request) => {
@@ -21264,6 +21690,9 @@ function createServer(adapter2, config2) {
21264
21690
  case "get_sibling_ads":
21265
21691
  result = await handleGetSiblingAds(adapter2, safeArgs);
21266
21692
  break;
21693
+ case "handoff_generate":
21694
+ result = await handleHandoffGenerate(adapter2, config2, safeArgs);
21695
+ break;
21267
21696
  default:
21268
21697
  return { content: [{ type: "text", text: `Unknown tool: ${name}` }] };
21269
21698
  }
@@ -21274,12 +21703,13 @@ function createServer(adapter2, config2) {
21274
21703
  delete result._usage;
21275
21704
  delete result._contextBytes;
21276
21705
  delete result._contextUtilisation;
21277
- if (contextBytes !== void 0 || contextUtilisation !== void 0) {
21278
- try {
21279
- const metric = buildMetric(name, elapsed, usage, void 0, contextBytes, contextUtilisation);
21280
- await adapter2.appendToolMetric(metric);
21281
- } catch {
21282
- }
21706
+ const isError = result.content.some((c) => c.text.startsWith("Error:") || c.text.startsWith("\u274C"));
21707
+ try {
21708
+ const metric = buildMetric(name, elapsed, usage, void 0, contextBytes, contextUtilisation);
21709
+ metric.success = !isError;
21710
+ adapter2.appendToolMetric(metric).catch(() => {
21711
+ });
21712
+ } catch {
21283
21713
  }
21284
21714
  const telemetryProjectId = process.env["PAPI_PROJECT_ID"];
21285
21715
  if (telemetryProjectId) {
@@ -21287,7 +21717,6 @@ function createServer(adapter2, config2) {
21287
21717
  adapter_type: config2.adapterType
21288
21718
  });
21289
21719
  const isApplyMode = safeArgs.mode === "apply";
21290
- const isError = result.content.some((c) => c.text.startsWith("Error:") || c.text.startsWith("\u274C"));
21291
21720
  if (!isError) {
21292
21721
  if (name === "setup" && isApplyMode) {
21293
21722
  emitMilestone(telemetryProjectId, "setup_completed");
@@ -21389,5 +21818,28 @@ If you already have an account, check that both **PAPI_PROJECT_ID** and **PAPI_D
21389
21818
  }]
21390
21819
  }));
21391
21820
  }
21821
+ if (pkgVersion !== "unknown") {
21822
+ (async () => {
21823
+ try {
21824
+ const controller = new AbortController();
21825
+ const timeout = setTimeout(() => controller.abort(), 3e3);
21826
+ const res = await fetch("https://registry.npmjs.org/@papi-ai/server/latest", {
21827
+ signal: controller.signal
21828
+ });
21829
+ clearTimeout(timeout);
21830
+ if (res.ok) {
21831
+ const data = await res.json();
21832
+ const latest = data.version;
21833
+ if (latest && latest !== pkgVersion) {
21834
+ process.stderr.write(
21835
+ `\u26A0 Update available: ${pkgVersion} \u2192 ${latest}. Run: npx @papi-ai/server@latest
21836
+ `
21837
+ );
21838
+ }
21839
+ }
21840
+ } catch {
21841
+ }
21842
+ })();
21843
+ }
21392
21844
  var transport = new StdioServerTransport();
21393
21845
  await server.connect(transport);