@papi-ai/server 0.7.4-alpha.4 → 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 +397 -49
- package/dist/prompts.js +27 -4
- package/package.json +1 -1
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
11619
|
-
metricsSnapshots2 = computeSnapshotsFromBuildReports(
|
|
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
|
-
|
|
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 :
|
|
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
|
-
|
|
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
|
|
12014
|
+
const expectedTaskCount = data.cycleTaskIds?.length ?? data.cycleHandoffs?.length ?? 0;
|
|
11988
12015
|
const actualCycleTasks = boardTasks.filter((t) => t.cycle === newCycleNumber).length;
|
|
11989
|
-
if (
|
|
11990
|
-
verifyWarnings.push(`Post-write verification FAILED: cycle ${newCycleNumber} exists but has 0 tasks assigned (expected ${
|
|
11991
|
-
} else if (
|
|
11992
|
-
verifyWarnings.push(`Post-write verification WARNING: cycle ${newCycleNumber} has ${actualCycleTasks} tasks but expected ${
|
|
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 =
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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