@papi-ai/server 0.7.4-alpha.3 → 0.7.5

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
@@ -10071,7 +10071,7 @@ Standard planning cycle with full board review.
10071
10071
  - **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
10072
 
10073
10073
  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.
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 from the backlog using the priority rules and cycle sizing rules below.
10075
10075
  **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
10076
  **Otherwise, select by priority level then impact:**
10077
10077
  - **P0 Critical** \u2014 Broken, blocking, or data-loss risk. Always first.
@@ -10095,7 +10095,7 @@ Standard planning cycle with full board review.
10095
10095
  - **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
10096
  - **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
10097
 
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.
10098
+ 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
10099
  **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
10100
  **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
10101
  **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 +10258,7 @@ Standard planning cycle with full board review.
10258
10258
  - **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
10259
 
10260
10260
  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.
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 from the backlog using the priority rules and cycle sizing rules below.
10262
10262
  **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
10263
  **Otherwise, select by priority level then impact:**
10264
10264
  - **P0 Critical** \u2014 Broken, blocking, or data-loss risk. Always first.
@@ -10282,7 +10282,7 @@ Standard planning cycle with full board review.
10282
10282
  - **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
10283
  - **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
10284
 
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.
10285
+ 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
10286
  **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
10287
  **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
10288
  **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 +10341,24 @@ function buildPlanUserMessage(ctx) {
10341
10341
  }) : PLAN_FULL_INSTRUCTIONS;
10342
10342
  parts.push(instructions);
10343
10343
  }
10344
+ if (ctx.skipHandoffs) {
10345
+ parts.push(
10346
+ "",
10347
+ "## SKIP HANDOFFS MODE",
10348
+ "",
10349
+ "**IMPORTANT OVERRIDE:** Do NOT generate BUILD HANDOFF blocks in this plan run.",
10350
+ "Select tasks for the cycle using all the normal criteria (Steps 1-7), but SKIP Step 10 (BUILD HANDOFFs) entirely.",
10351
+ "",
10352
+ "In your Part 2 structured output:",
10353
+ "- Set `cycleHandoffs` to an EMPTY array `[]`",
10354
+ '- Add a `cycleTaskIds` array with the task IDs you selected for the cycle: `["task-123", "task-456", ...]`',
10355
+ "- All other fields (cycleLogTitle, cycleLogContent, newTasks, boardCorrections, activeDecisions, etc.) work as normal.",
10356
+ "",
10357
+ "BUILD HANDOFFs will be generated separately via `handoff_generate` after this plan completes.",
10358
+ "This reduces your cognitive load \u2014 focus on triage, selection, and board management only.",
10359
+ ""
10360
+ );
10361
+ }
10344
10362
  parts.push("", "---", "", "## PROJECT CONTEXT", "");
10345
10363
  parts.push("### Product Brief", "", ctx.productBrief, "");
10346
10364
  if (ctx.northStar) {
@@ -10517,6 +10535,7 @@ function coerceStructuredOutput(parsed) {
10517
10535
  id: coerceToString(ad.id),
10518
10536
  body: coerceToString(ad.body)
10519
10537
  })) : [];
10538
+ const cycleTaskIds = Array.isArray(parsed.cycleTaskIds) ? parsed.cycleTaskIds.map((id) => coerceToString(id)) : void 0;
10520
10539
  return {
10521
10540
  cycleLogTitle: coerceToString(parsed.cycleLogTitle),
10522
10541
  cycleLogContent: coerceToString(parsed.cycleLogContent),
@@ -10527,6 +10546,7 @@ function coerceStructuredOutput(parsed) {
10527
10546
  strategicDirection: coerceToString(parsed.strategicDirection),
10528
10547
  recommendedTaskId: parsed.recommendedTaskId === null ? null : coerceToString(parsed.recommendedTaskId),
10529
10548
  cycleHandoffs,
10549
+ cycleTaskIds,
10530
10550
  newTasks,
10531
10551
  boardCorrections,
10532
10552
  productBrief: parsed.productBrief === null ? null : coerceToString(parsed.productBrief),
@@ -10824,6 +10844,9 @@ function buildReviewUserMessage(ctx) {
10824
10844
  if (ctx.taskComments) {
10825
10845
  parts.push("### Task Discussion (Recent Comments)", "", ctx.taskComments, "");
10826
10846
  }
10847
+ if (ctx.docActionStaleness) {
10848
+ parts.push("### Doc Action Staleness", "", ctx.docActionStaleness, "");
10849
+ }
10827
10850
  return parts.join("\n");
10828
10851
  }
10829
10852
  function parseReviewStructuredOutput(raw) {
@@ -11268,6 +11291,8 @@ function buildPlanSlackSummary(cycleNumber, mode, data) {
11268
11291
  return `${h.taskId}: ${title}`;
11269
11292
  }).join(", ");
11270
11293
  parts.push(`*Recommended:* ${tasks}`);
11294
+ } else if (data.cycleTaskIds && data.cycleTaskIds.length > 0) {
11295
+ parts.push(`*Recommended:* ${data.cycleTaskIds.join(", ")} (handoffs pending)`);
11271
11296
  }
11272
11297
  if (data.strategicDirection) {
11273
11298
  parts.push(`*Direction:* ${data.strategicDirection}`);
@@ -11316,7 +11341,7 @@ function formatPreAssignedTasks(tasks, targetCycle) {
11316
11341
  "",
11317
11342
  ...lines,
11318
11343
  "",
11319
- "These tasks MUST be included in the cycle. Generate BUILD HANDOFFs for each. Fill remaining slots (up to 5 total) from the backlog."
11344
+ "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
11345
  ].join("\n");
11321
11346
  }
11322
11347
  function pushAfterCommit(config2) {
@@ -11614,9 +11639,10 @@ async function assembleContext(adapter2, mode, _config, filters, focus) {
11614
11639
  adapter2.readCycleMetrics(),
11615
11640
  adapter2.getRecentReviews(5)
11616
11641
  ]);
11642
+ let leanBuildReports = [];
11617
11643
  try {
11618
- const reports2 = await adapter2.getRecentBuildReports(50);
11619
- metricsSnapshots2 = computeSnapshotsFromBuildReports(reports2);
11644
+ leanBuildReports = await adapter2.getRecentBuildReports(50);
11645
+ metricsSnapshots2 = computeSnapshotsFromBuildReports(leanBuildReports);
11620
11646
  } catch {
11621
11647
  }
11622
11648
  timings["metricsAndReviews"] = t();
@@ -11645,7 +11671,7 @@ async function assembleContext(adapter2, mode, _config, filters, focus) {
11645
11671
  adapter2.searchDocs?.({ status: "active", limit: 5 }),
11646
11672
  adapter2.getCycleLog(5),
11647
11673
  adapter2.queryBoard({ status: ["Backlog", "In Cycle", "Ready"] }),
11648
- adapter2.getRecentBuildReports(10),
11674
+ Promise.resolve(leanBuildReports.slice(0, 10)),
11649
11675
  adapter2.getContextHashes?.(health.totalCycles) ?? Promise.resolve(null)
11650
11676
  ]);
11651
11677
  timings["parallelReads"] = t();
@@ -11743,7 +11769,7 @@ ${lines.join("\n")}`;
11743
11769
  return { context: ctx2, contextHashes: newHashes2 };
11744
11770
  }
11745
11771
  t = startTimer();
11746
- const [decisions, reportsSinceCycle, log, tasks, rawMetricsSnapshots, reviews, phases, dogfoodEntries] = await Promise.all([
11772
+ const [decisions, reportsSinceCycle, log, tasks, rawMetricsSnapshots, reviews, phases, dogfoodEntries, allBuildReports] = await Promise.all([
11747
11773
  adapter2.getActiveDecisions(),
11748
11774
  adapter2.getBuildReportsSince(health.totalCycles ?? 0),
11749
11775
  adapter2.getCycleLog(3),
@@ -11751,10 +11777,11 @@ ${lines.join("\n")}`;
11751
11777
  adapter2.readCycleMetrics(),
11752
11778
  adapter2.getRecentReviews(5),
11753
11779
  adapter2.readPhases(),
11754
- readDogfoodEntries(_config.projectRoot, 5, adapter2)
11780
+ readDogfoodEntries(_config.projectRoot, 5, adapter2),
11781
+ adapter2.getRecentBuildReports(50)
11755
11782
  ]);
11756
11783
  timings["fullQueries"] = t();
11757
- const reports = reportsSinceCycle.length > 0 ? reportsSinceCycle : await adapter2.getRecentBuildReports(5);
11784
+ const reports = reportsSinceCycle.length > 0 ? reportsSinceCycle : allBuildReports.slice(0, 5);
11758
11785
  t = startTimer();
11759
11786
  const [
11760
11787
  allReportsResult,
@@ -11765,7 +11792,7 @@ ${lines.join("\n")}`;
11765
11792
  docsResultFull,
11766
11793
  contextHashesResultFull
11767
11794
  ] = await Promise.allSettled([
11768
- adapter2.getRecentBuildReports(50),
11795
+ Promise.resolve(allBuildReports),
11769
11796
  detectReviewPatterns(reviews, health.totalCycles, 5),
11770
11797
  adapter2.getPendingRecommendations(),
11771
11798
  assembleDiscoveryCanvasText(adapter2),
@@ -11883,7 +11910,7 @@ ${cleanContent}`;
11883
11910
  }
11884
11911
  return { taskId: h.taskId, handoff: parsed };
11885
11912
  }).filter((h) => h.handoff != null);
11886
- const cycleTaskIds = (data.cycleHandoffs ?? []).map((h) => h.taskId);
11913
+ const cycleTaskIds = data.cycleTaskIds?.length ? data.cycleTaskIds : (data.cycleHandoffs ?? []).map((h) => h.taskId);
11887
11914
  const cycle = {
11888
11915
  id: `cycle-${newCycleNumber}`,
11889
11916
  number: newCycleNumber,
@@ -11984,12 +12011,12 @@ ${cleanContent}`;
11984
12011
  if (!newCycle) {
11985
12012
  verifyWarnings.push(`Post-write verification FAILED: cycle ${newCycleNumber} entity not found after commit \u2014 data may not have persisted`);
11986
12013
  } else {
11987
- const expectedHandoffs = data.cycleHandoffs?.length ?? 0;
12014
+ const expectedTaskCount = data.cycleTaskIds?.length ?? data.cycleHandoffs?.length ?? 0;
11988
12015
  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`);
12016
+ if (expectedTaskCount > 0 && actualCycleTasks === 0) {
12017
+ verifyWarnings.push(`Post-write verification FAILED: cycle ${newCycleNumber} exists but has 0 tasks assigned (expected ${expectedTaskCount}) \u2014 task cycle assignment may have failed`);
12018
+ } else if (expectedTaskCount > 0 && actualCycleTasks < expectedTaskCount) {
12019
+ verifyWarnings.push(`Post-write verification WARNING: cycle ${newCycleNumber} has ${actualCycleTasks} tasks but expected ${expectedTaskCount} \u2014 some task assignments may have failed`);
11993
12020
  }
11994
12021
  }
11995
12022
  } catch {
@@ -12000,7 +12027,7 @@ ${cleanContent}`;
12000
12027
  const correctionCount = data.boardCorrections?.length ?? 0;
12001
12028
  const newTaskCount = result.newTaskIdMap.size;
12002
12029
  const adCount = data.activeDecisions?.length ?? 0;
12003
- const taskIds = (data.cycleHandoffs ?? []).map((h) => result.newTaskIdMap.get(h.taskId) ?? h.taskId);
12030
+ 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
12031
  return {
12005
12032
  priorityLockNotes: result.priorityLockNotes,
12006
12033
  newTaskIdMap: result.newTaskIdMap,
@@ -12042,15 +12069,7 @@ ${cleanContent}`;
12042
12069
  taskCount: cycleTaskCount > 0 ? cycleTaskCount : void 0,
12043
12070
  effortPoints: cycleEffortPoints > 0 ? cycleEffortPoints : void 0
12044
12071
  });
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
- );
12072
+ const healthPromise = Promise.resolve();
12054
12073
  const newTaskIdMap = /* @__PURE__ */ new Map();
12055
12074
  const createTasksPromise = (async () => {
12056
12075
  if (!data.newTasks || data.newTasks.length === 0) return;
@@ -12241,7 +12260,7 @@ ${cleanContent}`;
12241
12260
  })();
12242
12261
  const cycleEntityPromise = (async () => {
12243
12262
  try {
12244
- const cycleTaskIds = (data.cycleHandoffs ?? []).map((h) => newTaskIdMap.get(h.taskId) ?? h.taskId);
12263
+ const cycleTaskIds = data.cycleTaskIds?.length ? data.cycleTaskIds.map((id) => newTaskIdMap.get(id) ?? id) : (data.cycleHandoffs ?? []).map((h) => newTaskIdMap.get(h.taskId) ?? h.taskId);
12245
12264
  const cycle = {
12246
12265
  id: `cycle-${newCycleNumber}`,
12247
12266
  number: newCycleNumber,
@@ -12271,7 +12290,7 @@ ${cleanContent}`;
12271
12290
  const correctionCount = data.boardCorrections?.length ?? 0;
12272
12291
  const newTaskCount = newTaskIdMap.size;
12273
12292
  const adCount = data.activeDecisions?.length ?? 0;
12274
- const taskIds = (data.cycleHandoffs ?? []).map((h) => newTaskIdMap.get(h.taskId) ?? h.taskId);
12293
+ const taskIds = data.cycleTaskIds?.length ? data.cycleTaskIds.map((id) => newTaskIdMap.get(id) ?? id) : (data.cycleHandoffs ?? []).map((h) => newTaskIdMap.get(h.taskId) ?? h.taskId);
12275
12294
  return {
12276
12295
  priorityLockNotes,
12277
12296
  newTaskIdMap,
@@ -12418,7 +12437,7 @@ async function processLlmOutput(adapter2, config2, rawOutput, mode, cycleNumber,
12418
12437
  writeSummary
12419
12438
  };
12420
12439
  }
12421
- async function preparePlan(adapter2, config2, filters, focus, force, handoffsOnly) {
12440
+ async function preparePlan(adapter2, config2, filters, focus, force, handoffsOnly, skipHandoffs) {
12422
12441
  const prepareTimer = startTimer();
12423
12442
  let t = startTimer();
12424
12443
  const { mode, cycleNumber, strategyReviewWarning } = await validateAndPrepare(adapter2, force);
@@ -12468,6 +12487,7 @@ async function preparePlan(adapter2, config2, filters, focus, force, handoffsOnl
12468
12487
  if (mode !== "bootstrap" && context.productBrief.includes(TEMPLATE_MARKER)) {
12469
12488
  throw new Error("TEMPLATE_BRIEF");
12470
12489
  }
12490
+ if (skipHandoffs) context.skipHandoffs = true;
12471
12491
  t = startTimer();
12472
12492
  const userMessage = buildPlanUserMessage(context);
12473
12493
  const buildMessageMs = t();
@@ -12640,9 +12660,11 @@ async function propagatePhaseStatus(adapter2) {
12640
12660
  var lastPrepareContextHashes;
12641
12661
  var lastPrepareUserMessage;
12642
12662
  var lastPrepareContextBytes;
12663
+ var lastPrepareCycleNumber;
12664
+ var lastPrepareSkipHandoffs;
12643
12665
  var planTool = {
12644
12666
  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.',
12667
+ 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
12668
  inputSchema: {
12647
12669
  type: "object",
12648
12670
  properties: {
@@ -12695,6 +12717,10 @@ var planTool = {
12695
12717
  handoffs_only: {
12696
12718
  type: "boolean",
12697
12719
  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."
12720
+ },
12721
+ skip_handoffs: {
12722
+ type: "boolean",
12723
+ 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
12724
  }
12699
12725
  },
12700
12726
  required: []
@@ -12733,7 +12759,12 @@ function formatPlanResult(result) {
12733
12759
  if (result.priorityLockNote) lines.push(result.priorityLockNote.trim());
12734
12760
  if (result.slackWarning) lines.push(result.slackWarning);
12735
12761
  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.`);
12762
+ if (result.skipHandoffs) {
12763
+ const taskCount = result.writeSummary?.taskIds.length ?? 0;
12764
+ lines.push("", `Next: run \`handoff_generate\` to create BUILD HANDOFFs for your ${taskCount} cycle task(s), then \`build_list\` to start building.`);
12765
+ } else {
12766
+ lines.push("", `Next: run \`build_list\` to see your cycle tasks, then \`build_execute <task_id>\` to start building.`);
12767
+ }
12737
12768
  if (result.contextBytes !== void 0) {
12738
12769
  const kb = (result.contextBytes / 1024).toFixed(1);
12739
12770
  lines.push(`---`, `Context: ${kb}KB`);
@@ -12766,10 +12797,20 @@ async function handlePlan(adapter2, config2, args) {
12766
12797
  const contextHashes = lastPrepareContextHashes;
12767
12798
  const inputContext = lastPrepareUserMessage;
12768
12799
  const contextBytes = lastPrepareContextBytes;
12800
+ const expectedCycleNumber = lastPrepareCycleNumber;
12801
+ const skipHandoffsCached = lastPrepareSkipHandoffs;
12769
12802
  lastPrepareContextHashes = void 0;
12770
12803
  lastPrepareUserMessage = void 0;
12771
12804
  lastPrepareContextBytes = void 0;
12772
- const result = await applyPlan(adapter2, config2, llmResponse, planMode, cycleNumber, strategyReviewWarning, contextHashes, { contextBytes: contextBytes ?? void 0 });
12805
+ lastPrepareCycleNumber = void 0;
12806
+ lastPrepareSkipHandoffs = void 0;
12807
+ const skipHandoffs = args.skip_handoffs === true || skipHandoffsCached === true;
12808
+ if (expectedCycleNumber !== void 0 && cycleNumber !== expectedCycleNumber) {
12809
+ return errorResponse(
12810
+ `cycle_number mismatch: prepare phase returned cycle ${expectedCycleNumber} but apply received ${cycleNumber}. Pass cycle_number: ${expectedCycleNumber} to match the prepare output.`
12811
+ );
12812
+ }
12813
+ const result = await applyPlan(adapter2, config2, llmResponse, planMode, cycleNumber, strategyReviewWarning, contextHashes, { contextBytes: contextBytes ?? void 0, skipHandoffs: skipHandoffs || void 0 });
12773
12814
  let utilisation;
12774
12815
  if (inputContext) {
12775
12816
  try {
@@ -12777,7 +12818,7 @@ async function handlePlan(adapter2, config2, args) {
12777
12818
  } catch {
12778
12819
  }
12779
12820
  }
12780
- const response = formatPlanResult({ ...result, contextUtilisation: utilisation, contextBytes });
12821
+ const response = formatPlanResult({ ...result, contextUtilisation: utilisation, contextBytes, skipHandoffs });
12781
12822
  return {
12782
12823
  ...response,
12783
12824
  ...contextBytes !== void 0 ? { _contextBytes: contextBytes } : {},
@@ -12789,10 +12830,13 @@ async function handlePlan(adapter2, config2, args) {
12789
12830
  await propagatePhaseStatus(adapter2);
12790
12831
  } catch {
12791
12832
  }
12792
- const result = await preparePlan(adapter2, config2, filters, focus, force, handoffsOnly);
12833
+ const skipHandoffs = args.skip_handoffs === true;
12834
+ const result = await preparePlan(adapter2, config2, filters, focus, force, handoffsOnly, skipHandoffs);
12793
12835
  lastPrepareContextHashes = result.contextHashes;
12794
12836
  lastPrepareUserMessage = result.userMessage;
12795
12837
  lastPrepareContextBytes = result.contextBytes;
12838
+ lastPrepareCycleNumber = result.cycleNumber;
12839
+ lastPrepareSkipHandoffs = skipHandoffs || void 0;
12796
12840
  const modeLabel = result.mode === "bootstrap" ? "Bootstrap" : "Full";
12797
12841
  const header = result.strategyReviewWarning ? `${result.strategyReviewWarning}
12798
12842
  ` : "";
@@ -13145,7 +13189,8 @@ async function assembleContext2(adapter2, cycleNumber, cyclesSinceLastReview, pr
13145
13189
  decisionUsage,
13146
13190
  recData,
13147
13191
  pendingRecs,
13148
- registeredDocs
13192
+ registeredDocs,
13193
+ docsWithPendingActions
13149
13194
  ] = await Promise.all([
13150
13195
  adapter2.readProductBrief(),
13151
13196
  adapter2.getActiveDecisions(),
@@ -13169,7 +13214,9 @@ async function assembleContext2(adapter2, cycleNumber, cyclesSinceLastReview, pr
13169
13214
  adapter2.getRecommendationEffectiveness?.()?.catch(() => []) ?? Promise.resolve([]),
13170
13215
  adapter2.getPendingRecommendations().catch(() => []),
13171
13216
  // Doc registry — summaries for strategy review context
13172
- adapter2.searchDocs?.({ status: "active", limit: 10 })?.catch(() => []) ?? Promise.resolve([])
13217
+ adapter2.searchDocs?.({ status: "active", limit: 10 })?.catch(() => []) ?? Promise.resolve([]),
13218
+ // Doc registry — docs with pending actions for staleness audit
13219
+ adapter2.searchDocs?.({ hasPendingActions: true, limit: 20 })?.catch(() => []) ?? Promise.resolve([])
13173
13220
  ]);
13174
13221
  const tasks = [...activeTasks, ...recentDoneTasks];
13175
13222
  const existingAdIds = new Set(decisions.map((d) => d.id));
@@ -13371,6 +13418,44 @@ ${unregistered.slice(0, 10).map((f) => `- ${f}`).join("\n")}`;
13371
13418
  }
13372
13419
  } catch {
13373
13420
  }
13421
+ let docActionStalenessText;
13422
+ try {
13423
+ if (docsWithPendingActions && docsWithPendingActions.length > 0) {
13424
+ const STALE_THRESHOLD = 20;
13425
+ const doneTaskIds = new Set(recentDoneTasks.map((t) => t.displayId ?? t.id));
13426
+ const completed = [];
13427
+ const deferred = [];
13428
+ const stale = [];
13429
+ for (const doc of docsWithPendingActions) {
13430
+ const pendingActions = (doc.actions ?? []).filter((a) => a.status === "pending");
13431
+ if (pendingActions.length === 0) continue;
13432
+ const ageInCycles = cycleNumber - (doc.cycleCreated ?? cycleNumber);
13433
+ for (const action of pendingActions) {
13434
+ const line = ` - **${doc.title}** (C${doc.cycleCreated ?? "?"}): ${action.description}${action.linkedTaskId ? ` [\u2192${action.linkedTaskId}]` : ""}`;
13435
+ if (action.linkedTaskId && doneTaskIds.has(action.linkedTaskId)) {
13436
+ completed.push(line);
13437
+ } else if (ageInCycles > STALE_THRESHOLD) {
13438
+ stale.push(line);
13439
+ } else {
13440
+ deferred.push(line);
13441
+ }
13442
+ }
13443
+ }
13444
+ if (completed.length === 0 && deferred.length === 0 && stale.length === 0) {
13445
+ docActionStalenessText = "Doc Actions: all clear \u2014 no pending actions.";
13446
+ } else {
13447
+ const sections = [];
13448
+ if (stale.length > 0) sections.push(`**Stale (>${STALE_THRESHOLD} cycles, no matching Done task):**
13449
+ ${stale.join("\n")}`);
13450
+ if (completed.length > 0) sections.push(`**Completed but not closed (linked task is Done):**
13451
+ ${completed.join("\n")}`);
13452
+ if (deferred.length > 0) sections.push(`**Deferred (<${STALE_THRESHOLD} cycles, no matching Done task):**
13453
+ ${deferred.join("\n")}`);
13454
+ docActionStalenessText = sections.join("\n\n");
13455
+ }
13456
+ }
13457
+ } catch {
13458
+ }
13374
13459
  logDataSourceSummary("strategy_review_audit", [
13375
13460
  { label: "discoveryCanvas", hasData: discoveryCanvasText !== void 0 },
13376
13461
  { label: "briefImplications", hasData: briefImplicationsText !== void 0 },
@@ -13382,7 +13467,8 @@ ${unregistered.slice(0, 10).map((f) => `- ${f}`).join("\n")}`;
13382
13467
  { label: "registeredDocs", hasData: registeredDocsText !== void 0 },
13383
13468
  { label: "recentPlans", hasData: recentPlansText !== void 0 },
13384
13469
  { label: "unregisteredDocs", hasData: unregisteredDocsText !== void 0 },
13385
- { label: "taskComments", hasData: taskCommentsText !== void 0 }
13470
+ { label: "taskComments", hasData: taskCommentsText !== void 0 },
13471
+ { label: "docActionStaleness", hasData: docActionStalenessText !== void 0 }
13386
13472
  ]);
13387
13473
  const context = {
13388
13474
  sessionNumber: cycleNumber,
@@ -13408,7 +13494,8 @@ ${unregistered.slice(0, 10).map((f) => `- ${f}`).join("\n")}`;
13408
13494
  registeredDocs: registeredDocsText,
13409
13495
  recentPlans: recentPlansText,
13410
13496
  unregisteredDocs: unregisteredDocsText,
13411
- taskComments: taskCommentsText
13497
+ taskComments: taskCommentsText,
13498
+ docActionStaleness: docActionStalenessText
13412
13499
  };
13413
13500
  const BUDGET_SOFT2 = 5e4;
13414
13501
  const BUDGET_HARD2 = 6e4;
@@ -14514,8 +14601,8 @@ async function viewBoard(adapter2, phaseFilter, options) {
14514
14601
  const bi = PRIORITY_ORDER.indexOf(b2.priority);
14515
14602
  const priorityDiff = (ai === -1 ? 999 : ai) - (bi === -1 ? 999 : bi);
14516
14603
  if (priorityDiff !== 0) return priorityDiff;
14517
- const aDate = a.createdAt ?? "";
14518
- const bDate = b2.createdAt ?? "";
14604
+ const aDate = a.createdAt ? String(a.createdAt) : "";
14605
+ const bDate = b2.createdAt ? String(b2.createdAt) : "";
14519
14606
  return bDate.localeCompare(aDate);
14520
14607
  });
14521
14608
  const total = allTasks.length;
@@ -15694,6 +15781,7 @@ async function ensurePapiPermission(projectRoot) {
15694
15781
  }
15695
15782
  }
15696
15783
  async function applySetupOutputs(adapter2, config2, input, briefText, adSeedText, conventionsText) {
15784
+ const warnings = [];
15697
15785
  if (config2.adapterType !== "pg") {
15698
15786
  await writeFile2(join4(config2.papiDir, "PRODUCT_BRIEF.md"), briefText, "utf-8");
15699
15787
  }
@@ -15701,6 +15789,10 @@ async function applySetupOutputs(adapter2, config2, input, briefText, adSeedText
15701
15789
  const briefPhases = parsePhases(briefText);
15702
15790
  if (briefPhases.length > 0) {
15703
15791
  await adapter2.writePhases(briefPhases);
15792
+ } else {
15793
+ warnings.push(
15794
+ "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."
15795
+ );
15704
15796
  }
15705
15797
  try {
15706
15798
  if (adapter2.createHorizon && adapter2.createStage && adapter2.linkPhasesToStage) {
@@ -15739,7 +15831,19 @@ async function applySetupOutputs(adapter2, config2, input, briefText, adSeedText
15739
15831
  }
15740
15832
  }
15741
15833
  }
15742
- } catch {
15834
+ } catch (err) {
15835
+ const msg = err instanceof Error ? err.message : String(err);
15836
+ warnings.push(
15837
+ `AD seeding failed \u2014 active decisions were not created. Check that your ad_seed_response is valid JSON. Error: ${msg}`
15838
+ );
15839
+ seededAds = 0;
15840
+ }
15841
+ if (seededAds === 0 && adSeedText) {
15842
+ if (!warnings.some((w) => w.startsWith("AD seeding failed"))) {
15843
+ warnings.push(
15844
+ "AD seeding produced 0 active decisions \u2014 the JSON may be valid but empty or missing required `id` and `body` fields."
15845
+ );
15846
+ }
15743
15847
  }
15744
15848
  }
15745
15849
  if (conventionsText?.trim()) {
@@ -15750,7 +15854,7 @@ async function applySetupOutputs(adapter2, config2, input, briefText, adSeedText
15750
15854
  } catch {
15751
15855
  }
15752
15856
  }
15753
- return { seededAds };
15857
+ return { seededAds, warnings };
15754
15858
  }
15755
15859
  var SKIP_PATTERNS = /* @__PURE__ */ new Set([
15756
15860
  "node_modules",
@@ -15988,7 +16092,7 @@ async function applySetup(adapter2, config2, input, briefText, adSeedText, conve
15988
16092
  if (!briefText.trim()) {
15989
16093
  throw new Error("brief_response is required and cannot be empty.");
15990
16094
  }
15991
- const { seededAds } = await applySetupOutputs(adapter2, config2, input, briefText, adSeedText, conventionsText);
16095
+ const { seededAds, warnings } = await applySetupOutputs(adapter2, config2, input, briefText, adSeedText, conventionsText);
15992
16096
  let createdTasks = 0;
15993
16097
  if (initialTasksText?.trim()) {
15994
16098
  try {
@@ -16073,7 +16177,8 @@ async function applySetup(adapter2, config2, input, briefText, adSeedText, conve
16073
16177
  projectName: input.projectName,
16074
16178
  seededAds,
16075
16179
  createdTasks,
16076
- cursorScaffolded
16180
+ cursorScaffolded,
16181
+ warnings: warnings.length > 0 ? warnings : void 0
16077
16182
  };
16078
16183
  }
16079
16184
 
@@ -16182,8 +16287,12 @@ ${result.seededAds} Active Decision${result.seededAds > 1 ? "s" : ""} seeded bas
16182
16287
  ${result.createdTasks} initial backlog task${result.createdTasks > 1 ? "s" : ""} created from codebase analysis.` : "";
16183
16288
  const constraintsHint = !constraints ? '\n\nTip: consider adding `constraints` (e.g. "must use PostgreSQL", "HIPAA compliant", "offline-first") to improve Active Decision seeding.' : "";
16184
16289
  const editorNote = result.cursorScaffolded ? "\n\nCursor detected \u2014 `.cursor/rules/papi.mdc` scaffolded alongside CLAUDE.md." : "";
16290
+ const warningsNote = result.warnings && result.warnings.length > 0 ? `
16291
+
16292
+ \u26A0\uFE0F **Setup warnings (non-blocking):**
16293
+ ${result.warnings.map((w) => `- ${w}`).join("\n")}` : "";
16185
16294
  return textResponse(
16186
- `${prefix}Product Brief generated and saved.${adNote}${taskNote}${constraintsHint}${editorNote}
16295
+ `${prefix}Product Brief generated and saved.${adNote}${taskNote}${constraintsHint}${editorNote}${warningsNote}
16187
16296
 
16188
16297
  **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
16298
 
@@ -20995,6 +21104,217 @@ Check that the project IDs are correct and exist in the same Supabase instance.`
20995
21104
  return textResponse(lines.join("\n"));
20996
21105
  }
20997
21106
 
21107
+ // src/services/handoff.ts
21108
+ init_dist2();
21109
+ async function prepareHandoffs(adapter2, _config, taskIds) {
21110
+ const timer2 = startTimer();
21111
+ const cycles = await adapter2.readCycles();
21112
+ const activeCycle = cycles.find((c) => c.status === "active");
21113
+ if (!activeCycle) {
21114
+ throw new Error("No active cycle found. Run `plan` first to create a cycle.");
21115
+ }
21116
+ const cycleNumber = activeCycle.number;
21117
+ const allTasks = await adapter2.queryBoard({ status: ["Backlog", "In Cycle", "Ready", "In Progress"] });
21118
+ let cycleTasks = allTasks.filter((t) => t.cycle === cycleNumber);
21119
+ if (taskIds?.length) {
21120
+ const idSet = new Set(taskIds);
21121
+ cycleTasks = cycleTasks.filter((t) => idSet.has(t.id));
21122
+ }
21123
+ const tasksNeedingHandoffs = cycleTasks.filter((t) => !t.buildHandoff);
21124
+ if (tasksNeedingHandoffs.length === 0) {
21125
+ throw new Error(
21126
+ 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.`
21127
+ );
21128
+ }
21129
+ const [decisions, reports, brief] = await Promise.all([
21130
+ adapter2.getActiveDecisions(),
21131
+ adapter2.getRecentBuildReports(10),
21132
+ adapter2.readProductBrief()
21133
+ ]);
21134
+ const northStar = await adapter2.getCurrentNorthStar?.() ?? "";
21135
+ const userMessage = buildHandoffsOnlyUserMessage({
21136
+ cycleNumber: cycleNumber - 1,
21137
+ // buildHandoffsOnlyUserMessage adds +1 internally
21138
+ preAssignedTasks: tasksNeedingHandoffs,
21139
+ activeDecisions: formatActiveDecisionsForPlan(decisions),
21140
+ recentBuildReports: formatBuildReports(reports),
21141
+ productBrief: brief,
21142
+ northStar
21143
+ });
21144
+ const contextBytes = Buffer.byteLength(userMessage, "utf-8");
21145
+ const elapsed = timer2();
21146
+ console.error(`[handoff-perf] prepareHandoffs: ${elapsed}ms, ${contextBytes} bytes, ${tasksNeedingHandoffs.length} task(s)`);
21147
+ const systemPrompt = await getPrompt("plan-system");
21148
+ return {
21149
+ cycleNumber,
21150
+ systemPrompt,
21151
+ userMessage,
21152
+ contextBytes,
21153
+ taskCount: tasksNeedingHandoffs.length
21154
+ };
21155
+ }
21156
+ async function applyHandoffs(adapter2, rawLlmOutput, cycleNumber) {
21157
+ const timer2 = startTimer();
21158
+ const { data } = parseStructuredOutput(rawLlmOutput);
21159
+ if (!data) {
21160
+ throw new Error("Could not parse structured output. Ensure your output includes <!-- PAPI_STRUCTURED_OUTPUT --> with valid JSON.");
21161
+ }
21162
+ const handoffs = data.cycleHandoffs ?? [];
21163
+ if (handoffs.length === 0) {
21164
+ throw new Error("No cycleHandoffs found in structured output. Ensure your output includes handoffs in the cycleHandoffs array.");
21165
+ }
21166
+ const taskIdsToWrite = handoffs.map((h) => h.taskId);
21167
+ const existingHandoffSet = /* @__PURE__ */ new Set();
21168
+ try {
21169
+ const tasks = await adapter2.getTasks(taskIdsToWrite);
21170
+ for (const t of tasks) {
21171
+ if (t.buildHandoff) existingHandoffSet.add(t.id);
21172
+ }
21173
+ } catch {
21174
+ }
21175
+ const written = [];
21176
+ let skipped = 0;
21177
+ const warnings = [];
21178
+ for (const handoff of handoffs) {
21179
+ try {
21180
+ if (existingHandoffSet.has(handoff.taskId)) {
21181
+ console.error(`[handoff] skipping ${handoff.taskId} \u2014 already has handoff`);
21182
+ skipped++;
21183
+ continue;
21184
+ }
21185
+ const parsed = parseBuildHandoff(handoff.buildHandoff);
21186
+ if (!parsed) {
21187
+ warnings.push(`Failed to parse handoff for ${handoff.taskId}`);
21188
+ continue;
21189
+ }
21190
+ if (!parsed.createdAt) {
21191
+ parsed.createdAt = (/* @__PURE__ */ new Date()).toISOString();
21192
+ }
21193
+ await adapter2.updateTask(handoff.taskId, { buildHandoff: parsed });
21194
+ written.push(handoff.taskId);
21195
+ } catch (err) {
21196
+ const msg = err instanceof Error ? err.message : String(err);
21197
+ warnings.push(`Failed to write handoff for ${handoff.taskId}: ${msg}`);
21198
+ }
21199
+ }
21200
+ const elapsed = timer2();
21201
+ console.error(`[handoff-perf] applyHandoffs: ${elapsed}ms, ${written.length} written, ${skipped} skipped`);
21202
+ return {
21203
+ cycleNumber,
21204
+ handoffsWritten: written.length,
21205
+ skipped,
21206
+ taskIds: written,
21207
+ warnings
21208
+ };
21209
+ }
21210
+
21211
+ // src/tools/handoff.ts
21212
+ var lastPrepareCycleNumber2;
21213
+ var lastPrepareContextBytes2;
21214
+ var handoffGenerateTool = {
21215
+ name: "handoff_generate",
21216
+ 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.",
21217
+ inputSchema: {
21218
+ type: "object",
21219
+ properties: {
21220
+ mode: {
21221
+ type: "string",
21222
+ enum: ["prepare", "apply"],
21223
+ description: '"prepare" returns the handoff prompt for you to execute. "apply" accepts your generated output and persists handoffs. Defaults to "prepare" when omitted.'
21224
+ },
21225
+ task_ids: {
21226
+ type: "array",
21227
+ items: { type: "string" },
21228
+ description: "Specific task IDs to generate handoffs for. If omitted, generates for all cycle tasks missing handoffs."
21229
+ },
21230
+ llm_response: {
21231
+ type: "string",
21232
+ 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 -->).'
21233
+ },
21234
+ cycle_number: {
21235
+ type: "number",
21236
+ description: 'The cycle number returned from prepare phase (mode "apply" only).'
21237
+ }
21238
+ },
21239
+ required: []
21240
+ }
21241
+ };
21242
+ async function handleHandoffGenerate(adapter2, config2, args) {
21243
+ const toolMode = args.mode;
21244
+ try {
21245
+ if (toolMode === "apply") {
21246
+ const llmResponse = args.llm_response;
21247
+ if (!llmResponse || !llmResponse.trim()) {
21248
+ return errorResponse('llm_response is required for mode "apply". Pass your complete handoff output including the <!-- PAPI_STRUCTURED_OUTPUT --> block.');
21249
+ }
21250
+ const cycleNumber = typeof args.cycle_number === "number" ? args.cycle_number : lastPrepareCycleNumber2;
21251
+ if (cycleNumber === void 0) {
21252
+ return errorResponse('cycle_number is required for mode "apply". Pass the cycle number from the prepare phase.');
21253
+ }
21254
+ const expectedCycleNumber = lastPrepareCycleNumber2;
21255
+ const contextBytes = lastPrepareContextBytes2;
21256
+ lastPrepareCycleNumber2 = void 0;
21257
+ lastPrepareContextBytes2 = void 0;
21258
+ if (expectedCycleNumber !== void 0 && cycleNumber !== expectedCycleNumber) {
21259
+ return errorResponse(
21260
+ `cycle_number mismatch: prepare phase returned cycle ${expectedCycleNumber} but apply received ${cycleNumber}.`
21261
+ );
21262
+ }
21263
+ const result = await applyHandoffs(adapter2, llmResponse, cycleNumber);
21264
+ const lines = [];
21265
+ lines.push(`**Handoff Generation \u2014 Cycle ${result.cycleNumber}**`);
21266
+ lines.push(`${result.handoffsWritten} handoff(s) written: ${result.taskIds.join(", ")}`);
21267
+ if (result.skipped > 0) lines.push(`${result.skipped} task(s) skipped (already had handoffs)`);
21268
+ if (result.warnings.length > 0) lines.push("\u26A0\uFE0F Warnings: " + result.warnings.join("; "));
21269
+ lines.push("", "Next: run `build_list` to see your cycle tasks, then `build_execute <task_id>` to start building.");
21270
+ if (contextBytes !== void 0) {
21271
+ const kb = (contextBytes / 1024).toFixed(1);
21272
+ lines.push("---", `Context: ${kb}KB`);
21273
+ }
21274
+ return textResponse(lines.join("\n"));
21275
+ }
21276
+ {
21277
+ const taskIds = Array.isArray(args.task_ids) ? args.task_ids.filter((id) => typeof id === "string") : void 0;
21278
+ const result = await prepareHandoffs(adapter2, config2, taskIds);
21279
+ lastPrepareCycleNumber2 = result.cycleNumber;
21280
+ lastPrepareContextBytes2 = result.contextBytes;
21281
+ return textResponse(
21282
+ `## PAPI Handoff Generation \u2014 Prepare Phase (Cycle ${result.cycleNumber})
21283
+
21284
+ Generate BUILD HANDOFFs for ${result.taskCount} task(s) that need them.
21285
+
21286
+ **IMPORTANT:** Your output must have TWO parts:
21287
+ 1. Natural language markdown with BUILD HANDOFF blocks
21288
+ 2. After \`<!-- PAPI_STRUCTURED_OUTPUT -->\`, a JSON block with structured data
21289
+
21290
+ When done, call \`handoff_generate\` again with:
21291
+ - \`mode\`: "apply"
21292
+ - \`llm_response\`: your complete output (both parts)
21293
+ - \`cycle_number\`: ${result.cycleNumber}
21294
+
21295
+ ---
21296
+
21297
+ ### System Prompt
21298
+
21299
+ <system_prompt>
21300
+ ${result.systemPrompt}
21301
+ </system_prompt>
21302
+
21303
+ ---
21304
+
21305
+ ### Context
21306
+
21307
+ <context>
21308
+ ${result.userMessage}
21309
+ </context>`
21310
+ );
21311
+ }
21312
+ } catch (err) {
21313
+ const message = err instanceof Error ? err.message : String(err);
21314
+ return errorResponse(message);
21315
+ }
21316
+ }
21317
+
20998
21318
  // src/lib/telemetry.ts
20999
21319
  var TELEMETRY_SUPABASE_URL = "https://guewgygcpcmrcoppihzx.supabase.co";
21000
21320
  var TELEMETRY_API_KEY = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Imd1ZXdneWdjcGNtcmNvcHBpaHp4Iiwicm9sZSI6ImFub24iLCJpYXQiOjE3NzI2Njk2NTMsImV4cCI6MjA4ODI0NTY1M30.V5Jw7wJgiMpSQPa2mt0ftjyye5ynG1qLlam00yPVNJY";
@@ -21062,7 +21382,8 @@ var TOOLS_REQUIRING_PAPI = /* @__PURE__ */ new Set([
21062
21382
  "review_submit",
21063
21383
  "orient",
21064
21384
  "hierarchy_update",
21065
- "zoom_out"
21385
+ "zoom_out",
21386
+ "handoff_generate"
21066
21387
  ]);
21067
21388
  function createServer(adapter2, config2) {
21068
21389
  const server2 = new Server(
@@ -21148,7 +21469,8 @@ function createServer(adapter2, config2) {
21148
21469
  docRegisterTool,
21149
21470
  docSearchTool,
21150
21471
  docScanTool,
21151
- getSiblingAdsTool
21472
+ getSiblingAdsTool,
21473
+ handoffGenerateTool
21152
21474
  ]
21153
21475
  }));
21154
21476
  server2.setRequestHandler(CallToolRequestSchema, async (request) => {
@@ -21264,6 +21586,9 @@ function createServer(adapter2, config2) {
21264
21586
  case "get_sibling_ads":
21265
21587
  result = await handleGetSiblingAds(adapter2, safeArgs);
21266
21588
  break;
21589
+ case "handoff_generate":
21590
+ result = await handleHandoffGenerate(adapter2, config2, safeArgs);
21591
+ break;
21267
21592
  default:
21268
21593
  return { content: [{ type: "text", text: `Unknown tool: ${name}` }] };
21269
21594
  }
@@ -21389,5 +21714,28 @@ If you already have an account, check that both **PAPI_PROJECT_ID** and **PAPI_D
21389
21714
  }]
21390
21715
  }));
21391
21716
  }
21717
+ if (pkgVersion !== "unknown") {
21718
+ (async () => {
21719
+ try {
21720
+ const controller = new AbortController();
21721
+ const timeout = setTimeout(() => controller.abort(), 3e3);
21722
+ const res = await fetch("https://registry.npmjs.org/@papi-ai/server/latest", {
21723
+ signal: controller.signal
21724
+ });
21725
+ clearTimeout(timeout);
21726
+ if (res.ok) {
21727
+ const data = await res.json();
21728
+ const latest = data.version;
21729
+ if (latest && latest !== pkgVersion) {
21730
+ process.stderr.write(
21731
+ `\u26A0 Update available: ${pkgVersion} \u2192 ${latest}. Run: npx @papi-ai/server@latest
21732
+ `
21733
+ );
21734
+ }
21735
+ }
21736
+ } catch {
21737
+ }
21738
+ })();
21739
+ }
21392
21740
  var transport = new StdioServerTransport();
21393
21741
  await server.connect(transport);
package/dist/prompts.js CHANGED
@@ -201,7 +201,7 @@ Standard planning cycle with full board review.
201
201
  - **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).
202
202
 
203
203
  7. **Recommendation** \u2014 Select tasks for this cycle:
204
- **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.
204
+ **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.
205
205
  **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).
206
206
  **Otherwise, select by priority level then impact:**
207
207
  - **P0 Critical** \u2014 Broken, blocking, or data-loss risk. Always first.
@@ -225,7 +225,7 @@ Standard planning cycle with full board review.
225
225
  - **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.
226
226
  - **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.
227
227
 
228
- 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.
228
+ 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.
229
229
  **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.
230
230
  **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.
231
231
  **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.
@@ -388,7 +388,7 @@ Standard planning cycle with full board review.
388
388
  - **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).
389
389
 
390
390
  7. **Recommendation** \u2014 Select tasks for this cycle:
391
- **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.
391
+ **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.
392
392
  **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).
393
393
  **Otherwise, select by priority level then impact:**
394
394
  - **P0 Critical** \u2014 Broken, blocking, or data-loss risk. Always first.
@@ -412,7 +412,7 @@ Standard planning cycle with full board review.
412
412
  - **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.
413
413
  - **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.
414
414
 
415
- 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.
415
+ 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.
416
416
  **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.
417
417
  **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.
418
418
  **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.
@@ -471,6 +471,24 @@ function buildPlanUserMessage(ctx) {
471
471
  }) : PLAN_FULL_INSTRUCTIONS;
472
472
  parts.push(instructions);
473
473
  }
474
+ if (ctx.skipHandoffs) {
475
+ parts.push(
476
+ "",
477
+ "## SKIP HANDOFFS MODE",
478
+ "",
479
+ "**IMPORTANT OVERRIDE:** Do NOT generate BUILD HANDOFF blocks in this plan run.",
480
+ "Select tasks for the cycle using all the normal criteria (Steps 1-7), but SKIP Step 10 (BUILD HANDOFFs) entirely.",
481
+ "",
482
+ "In your Part 2 structured output:",
483
+ "- Set `cycleHandoffs` to an EMPTY array `[]`",
484
+ '- Add a `cycleTaskIds` array with the task IDs you selected for the cycle: `["task-123", "task-456", ...]`',
485
+ "- All other fields (cycleLogTitle, cycleLogContent, newTasks, boardCorrections, activeDecisions, etc.) work as normal.",
486
+ "",
487
+ "BUILD HANDOFFs will be generated separately via `handoff_generate` after this plan completes.",
488
+ "This reduces your cognitive load \u2014 focus on triage, selection, and board management only.",
489
+ ""
490
+ );
491
+ }
474
492
  parts.push("", "---", "", "## PROJECT CONTEXT", "");
475
493
  parts.push("### Product Brief", "", ctx.productBrief, "");
476
494
  if (ctx.northStar) {
@@ -647,6 +665,7 @@ function coerceStructuredOutput(parsed) {
647
665
  id: coerceToString(ad.id),
648
666
  body: coerceToString(ad.body)
649
667
  })) : [];
668
+ const cycleTaskIds = Array.isArray(parsed.cycleTaskIds) ? parsed.cycleTaskIds.map((id) => coerceToString(id)) : void 0;
650
669
  return {
651
670
  cycleLogTitle: coerceToString(parsed.cycleLogTitle),
652
671
  cycleLogContent: coerceToString(parsed.cycleLogContent),
@@ -657,6 +676,7 @@ function coerceStructuredOutput(parsed) {
657
676
  strategicDirection: coerceToString(parsed.strategicDirection),
658
677
  recommendedTaskId: parsed.recommendedTaskId === null ? null : coerceToString(parsed.recommendedTaskId),
659
678
  cycleHandoffs,
679
+ cycleTaskIds,
660
680
  newTasks,
661
681
  boardCorrections,
662
682
  productBrief: parsed.productBrief === null ? null : coerceToString(parsed.productBrief),
@@ -954,6 +974,9 @@ function buildReviewUserMessage(ctx) {
954
974
  if (ctx.taskComments) {
955
975
  parts.push("### Task Discussion (Recent Comments)", "", ctx.taskComments, "");
956
976
  }
977
+ if (ctx.docActionStaleness) {
978
+ parts.push("### Doc Action Staleness", "", ctx.docActionStaleness, "");
979
+ }
957
980
  return parts.join("\n");
958
981
  }
959
982
  function parseReviewStructuredOutput(raw) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@papi-ai/server",
3
- "version": "0.7.4-alpha.3",
3
+ "version": "0.7.5",
4
4
  "description": "PAPI MCP server — AI-powered sprint planning, build execution, and strategy review for software projects",
5
5
  "license": "Elastic-2.0",
6
6
  "type": "module",