@papi-ai/server 0.7.2 → 0.7.4-alpha.1

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
@@ -1427,8 +1427,8 @@ var init_dist2 = __esm({
1427
1427
  bug: 1,
1428
1428
  task: 1,
1429
1429
  research: 2,
1430
- idea: 3,
1431
- feedback: 3
1430
+ spike: 2,
1431
+ idea: 3
1432
1432
  };
1433
1433
  VALID_EFFORT_SIZES = /* @__PURE__ */ new Set(["XS", "S", "M", "L", "XL"]);
1434
1434
  SECTION_HEADERS = [
@@ -4467,6 +4467,7 @@ function rowToTask(row) {
4467
4467
  if (row.maturity != null) task.maturity = row.maturity;
4468
4468
  if (row.stage_id != null) task.stageId = row.stage_id;
4469
4469
  if (row.doc_ref != null) task.docRef = row.doc_ref;
4470
+ if (row.source != null) task.source = row.source;
4470
4471
  return task;
4471
4472
  }
4472
4473
  function rowToBuildReport(row) {
@@ -6710,7 +6711,7 @@ ${newParts.join("\n")}` : newParts.join("\n");
6710
6711
  project_id, display_id, title, status, priority, complexity,
6711
6712
  module, epic, phase, owner, reviewed, cycle, created_cycle,
6712
6713
  why, depends_on, notes, closure_reason, state_history,
6713
- build_handoff, build_report, task_type, maturity, stage_id, doc_ref
6714
+ build_handoff, build_report, task_type, maturity, stage_id, doc_ref, source
6714
6715
  ) VALUES (
6715
6716
  ${this.projectId}, ${displayId}, ${task.title}, ${task.status}, ${task.priority},
6716
6717
  ${normaliseComplexity(task.complexity)}, ${task.module}, ${task.epic ?? null}, ${task.phase}, ${task.owner},
@@ -6723,7 +6724,8 @@ ${newParts.join("\n")}` : newParts.join("\n");
6723
6724
  ${task.taskType ?? null},
6724
6725
  ${task.maturity ?? null},
6725
6726
  ${task.stageId ?? null},
6726
- ${task.docRef ?? null}
6727
+ ${task.docRef ?? null},
6728
+ ${task.source ?? null}
6727
6729
  )
6728
6730
  RETURNING *
6729
6731
  `;
@@ -6754,6 +6756,7 @@ ${newParts.join("\n")}` : newParts.join("\n");
6754
6756
  if (updates.maturity !== void 0) columnMap["maturity"] = updates.maturity;
6755
6757
  if (updates.stageId !== void 0) columnMap["stage_id"] = updates.stageId;
6756
6758
  if (updates.docRef !== void 0) columnMap["doc_ref"] = updates.docRef;
6759
+ if (updates.source !== void 0) columnMap["source"] = updates.source;
6757
6760
  const keys = Object.keys(columnMap);
6758
6761
  if (keys.length === 0) return;
6759
6762
  await this.sql`
@@ -8518,8 +8521,10 @@ Check PAPI_PROJECT_ID in your .mcp.json config. Find your project ID in the PAPI
8518
8521
  return this.invoke("submitBugReport", [report]);
8519
8522
  }
8520
8523
  // --- Atomic plan write-back ---
8521
- planWriteBack(payload) {
8522
- return this.invoke("planWriteBack", [payload]);
8524
+ async planWriteBack(payload) {
8525
+ const raw = await this.invoke("planWriteBack", [payload]);
8526
+ const map = new Map(Object.entries(raw.newTaskIdMap ?? {}));
8527
+ return { ...raw, newTaskIdMap: map };
8523
8528
  }
8524
8529
  };
8525
8530
  }
@@ -9998,10 +10003,12 @@ This is Cycle 0 \u2014 the first planning cycle for a brand-new project.
9998
10003
 
9999
10004
  2. **North Star** \u2014 Propose a one-sentence North Star statement, a success metric, and a key metric.
10000
10005
 
10001
- 3. **Initial Board** \u2014 Generate 3-5 tasks:
10002
- - Task 1: Project setup / scaffolding (if needed)
10003
- - Task 2: Core data model or foundational structure
10004
- - Tasks 3-5: First user-facing features, broken into small steps
10006
+ 3. **Initial Board** \u2014 Generate 3-5 tasks based on the project's actual tech stack and goals:
10007
+ - Infer the project type from the brief/description (CLI, web app, mobile app, API, library, game, data pipeline, etc.)
10008
+ - Task 1: Project-appropriate setup (toolchain, dependencies, config \u2014 NOT "scaffolding" if the project already has code)
10009
+ - Task 2: Core functionality that proves the concept works (data model, main loop, core algorithm \u2014 whatever the project needs first)
10010
+ - Tasks 3-5: First deliverables that demonstrate value, broken into small steps appropriate for the project type
10011
+ - Do NOT assume web-app patterns (routes, pages, components) unless the brief explicitly describes a web application
10005
10012
  - All tasks: status Backlog, priority P1-P2, reviewed true, phase "Phase 1"
10006
10013
 
10007
10014
  4. **First Active Decision** \u2014 If the description implies a clear architectural choice, create AD-1 with Confidence: MEDIUM. If no clear choice, skip this.
@@ -10161,11 +10168,25 @@ var PLAN_FRAGMENT_RESEARCH = `
10161
10168
  var PLAN_FRAGMENT_BUG = `
10162
10169
  **Bug task detection:** When a task's task type is "bug" or the title starts with "Bug:" or "Fix:", apply these rules:
10163
10170
  - **Auto-P1:** If the task's current priority is P2 or lower, upgrade it to "P1 High" via a boardCorrections entry in Part 2. Note the upgrade in Part 1 analysis.
10164
- - Add a BLAST RADIUS note to the BUILD HANDOFF SCOPE section: "Bug fix \u2014 minimal blast radius. Change only what is necessary to fix the reported behaviour. Do not refactor surrounding code or expand scope."
10165
- - Add to ACCEPTANCE CRITERIA: "[ ] Fix is targeted \u2014 no unrelated code changed"`;
10171
+ - Replace the standard SCOPE (DO THIS) section with bug-specific sections:
10172
+ - **REPRODUCE:** Exact steps to reproduce the bug before touching any code. If the task notes describe the symptoms, include them. If not, the first build step is "confirm the bug reproduces."
10173
+ - **ROOT CAUSE:** One-sentence hypothesis for the root cause (what is wrong, not what the user sees). The builder must confirm or correct this before implementing a fix.
10174
+ - **MINIMAL FIX:** The smallest code change that resolves the root cause. "Bug fix \u2014 minimal blast radius. Change only what is necessary. Do not refactor surrounding code or expand scope."
10175
+ - **REGRESSION TEST:** How to verify the bug is fixed and won't silently recur. Describe the test (manual or automated) \u2014 the builder must confirm this passes.
10176
+ - Add to ACCEPTANCE CRITERIA: "[ ] Fix is targeted \u2014 no unrelated code changed" and "[ ] Regression test confirms the bug no longer reproduces"`;
10166
10177
  var PLAN_FRAGMENT_IDEA = `
10167
10178
  **Idea task detection:** When a task's task type is "idea", add a scope clarification note to the BUILD HANDOFF:
10168
10179
  - Add to SCOPE (DO THIS): "This task originated as an idea. Confirm the exact deliverable before implementing \u2014 check task notes and any referenced docs for intent. If scope is unclear, flag it in the build report surprises."`;
10180
+ var PLAN_FRAGMENT_SPIKE = `
10181
+ **Spike task detection:** When a task's task type is "spike" or the title starts with "Spike:", apply these rules:
10182
+ - Spikes are time-boxed investigations, not implementation tasks. The deliverable is a FINDING, not code.
10183
+ - Replace the standard BUILD HANDOFF sections with spike-specific sections:
10184
+ - **TIME-BOX:** Maximum effort for this spike (e.g. "Stop after S effort / ~2 hours"). If the question isn't answered by then, the spike is done \u2014 report what you found.
10185
+ - **GOAL:** The specific question this spike answers (one sentence, phrased as a question).
10186
+ - **OUTPUT:** What the spike produces: a written finding (doc in docs/research/ or notes in the build report), optionally a proof-of-concept if code is needed.
10187
+ - **DONE CONDITION:** "Question answered OR time-box hit, whichever comes first."
10188
+ - Keep SCOPE BOUNDARY, SECURITY CONSIDERATIONS, and PRE-BUILD VERIFICATION as normal.
10189
+ - Spikes should be estimated conservatively: XS or S. If a spike needs M+ effort, it's not a spike \u2014 reclassify as a research task.`;
10169
10190
  var PLAN_FRAGMENT_UI = `
10170
10191
  **UI/visual task detection:** When a task's title or notes contain keywords suggesting frontend visual work (e.g. "visual", "design", "UI", "styling", "refresh", "frontend", "landing page", "hero", "carousel", "theme", "layout", "cockpit", "dashboard", "page"), apply these handoff additions:
10171
10192
  - Add to SCOPE: "Read \`.impeccable.md\` for brand palette, design principles, and audience context before writing any code. Use the \`frontend-design\` skill for implementation."
@@ -10274,6 +10295,7 @@ Standard planning cycle with full board review.
10274
10295
  if (flags.hasResearchTasks) parts.push(PLAN_FRAGMENT_RESEARCH);
10275
10296
  if (flags.hasBugTasks) parts.push(PLAN_FRAGMENT_BUG);
10276
10297
  if (flags.hasIdeaTasks) parts.push(PLAN_FRAGMENT_IDEA);
10298
+ if (flags.hasSpikeTasks) parts.push(PLAN_FRAGMENT_SPIKE);
10277
10299
  if (flags.hasUITasks) parts.push(PLAN_FRAGMENT_UI);
10278
10300
  parts.push(`
10279
10301
  11. **New Tasks (max 3 per cycle)** \u2014 Actively mine the Recent Build Reports for task candidates. For each report, check:
@@ -10592,6 +10614,15 @@ You MUST cover these 5 sections. Each is mandatory.
10592
10614
  ${compressionJob}
10593
10615
  Note: Hierarchy assessment and structural drift detection are handled within section 5 (AD & Hierarchy Housekeeping). They do not need their own sections.
10594
10616
 
10617
+ ## DETECT STRATEGIC DECISIONS
10618
+
10619
+ Watch for direction changes, architecture shifts, deprioritisation with reasoning, new principles, or competitive positioning decisions in the project data.
10620
+
10621
+ When detected:
10622
+ 1. Flag it in the review: "Strategic direction change detected \u2014 [description]."
10623
+ 2. Propose an AD update or new AD in the structured output (Part 2 \`activeDecisions\` array).
10624
+ 3. Recommend running \`strategy_change\` if the shift requires immediate action before the next plan.
10625
+
10595
10626
  ## OUTPUT FORMAT
10596
10627
 
10597
10628
  Your output has TWO parts:
@@ -11451,21 +11482,24 @@ function detectBoardFlags(tasks) {
11451
11482
  let hasBugTasks = false;
11452
11483
  let hasResearchTasks = false;
11453
11484
  let hasIdeaTasks = false;
11485
+ let hasSpikeTasks = false;
11454
11486
  let hasUITasks = false;
11455
11487
  const uiKeywords = /\b(visual|design|UI|styling|refresh|frontend|landing page|hero|carousel|theme|layout|cockpit|dashboard|page)\b/i;
11456
11488
  for (const t of tasks) {
11457
11489
  if (t.taskType === "bug" || /^(Bug:|Fix:)/i.test(t.title)) hasBugTasks = true;
11458
11490
  if (t.taskType === "research" || /^Research:/i.test(t.title)) hasResearchTasks = true;
11459
11491
  if (t.taskType === "idea") hasIdeaTasks = true;
11492
+ if (t.taskType === "spike" || /^Spike:/i.test(t.title)) hasSpikeTasks = true;
11460
11493
  if (uiKeywords.test(t.title) || uiKeywords.test(t.notes ?? "")) hasUITasks = true;
11461
11494
  }
11462
- return { hasBugTasks, hasResearchTasks, hasIdeaTasks, hasUITasks };
11495
+ return { hasBugTasks, hasResearchTasks, hasIdeaTasks, hasSpikeTasks, hasUITasks };
11463
11496
  }
11464
11497
  function detectBoardFlagsFromText(boardText) {
11465
11498
  return {
11466
11499
  hasBugTasks: /\b(bug|Bug:|Fix:)\b/i.test(boardText),
11467
11500
  hasResearchTasks: /\b(research|Research:)\b/i.test(boardText),
11468
11501
  hasIdeaTasks: /\bidea\b/i.test(boardText),
11502
+ hasSpikeTasks: /\b(spike|Spike:)\b/i.test(boardText),
11469
11503
  hasUITasks: /\b(visual|design|UI|styling|refresh|frontend|landing page|hero|carousel|theme|layout|cockpit|dashboard|page)\b/i.test(boardText)
11470
11504
  };
11471
11505
  }
@@ -11942,13 +11976,24 @@ ${cleanContent}`;
11942
11976
  console.error(`[plan-perf] transactionalWriteBack: total=${writeBackMs}ms`);
11943
11977
  const verifyWarnings = [];
11944
11978
  try {
11945
- const cycles = await adapter2.readCycles();
11979
+ const [cycles, boardTasks] = await Promise.all([
11980
+ adapter2.readCycles(),
11981
+ adapter2.queryBoard({ status: ["In Cycle", "Backlog", "In Progress", "In Review"] })
11982
+ ]);
11946
11983
  const newCycle = cycles.find((s) => s.number === newCycleNumber);
11947
11984
  if (!newCycle) {
11948
- verifyWarnings.push(`Post-write verification: cycle ${newCycleNumber} entity not found after commit`);
11985
+ verifyWarnings.push(`Post-write verification FAILED: cycle ${newCycleNumber} entity not found after commit \u2014 data may not have persisted`);
11986
+ } else {
11987
+ const expectedHandoffs = data.cycleHandoffs?.length ?? 0;
11988
+ 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`);
11993
+ }
11949
11994
  }
11950
11995
  } catch {
11951
- verifyWarnings.push("Post-write verification: could not read cycles table");
11996
+ verifyWarnings.push("Post-write verification: could not read cycles/tasks tables");
11952
11997
  }
11953
11998
  const allWarnings = [...result.warnings, ...verifyWarnings];
11954
11999
  const handoffCount = data.cycleHandoffs?.length ?? 0;
@@ -12035,7 +12080,8 @@ ${cleanContent}`;
12035
12080
  cycle: newCycleNumber,
12036
12081
  createdCycle: newCycleNumber,
12037
12082
  why: task.why || "",
12038
- notes: task.notes || ""
12083
+ notes: task.notes || "",
12084
+ source: "llm"
12039
12085
  });
12040
12086
  newTaskIdMap.set(`new-${i + 1}`, created.id);
12041
12087
  if (adapter2.updateCycleLearningActionRef && task.notes) {
@@ -14432,6 +14478,18 @@ ${result.userMessage}
14432
14478
  // src/services/board.ts
14433
14479
  var ACTIVE_STATUSES = ["Backlog", "In Cycle", "Ready", "In Progress", "In Review", "Blocked"];
14434
14480
  var PRIORITY_ORDER = ["P0 Critical", "P1 High", "P2 Medium", "P3 Low"];
14481
+ var STATUS_TIER = {
14482
+ "In Progress": 0,
14483
+ "In Review": 1,
14484
+ "Backlog": 2,
14485
+ "In Cycle": 2,
14486
+ "Ready": 2,
14487
+ "Blocked": 3,
14488
+ "Deferred": 4,
14489
+ "Done": 5,
14490
+ "Cancelled": 6,
14491
+ "Archived": 7
14492
+ };
14435
14493
  async function viewBoard(adapter2, phaseFilter, options) {
14436
14494
  const queryOptions = {};
14437
14495
  const phase = options?.phase ?? phaseFilter;
@@ -14446,9 +14504,16 @@ async function viewBoard(adapter2, phaseFilter, options) {
14446
14504
  Object.keys(queryOptions).length > 0 ? queryOptions : void 0
14447
14505
  );
14448
14506
  allTasks.sort((a, b2) => {
14507
+ const aTier = STATUS_TIER[a.status] ?? 4;
14508
+ const bTier = STATUS_TIER[b2.status] ?? 4;
14509
+ if (aTier !== bTier) return aTier - bTier;
14449
14510
  const ai = PRIORITY_ORDER.indexOf(a.priority);
14450
14511
  const bi = PRIORITY_ORDER.indexOf(b2.priority);
14451
- return (ai === -1 ? 999 : ai) - (bi === -1 ? 999 : bi);
14512
+ const priorityDiff = (ai === -1 ? 999 : ai) - (bi === -1 ? 999 : bi);
14513
+ if (priorityDiff !== 0) return priorityDiff;
14514
+ const aDate = a.createdAt ?? "";
14515
+ const bDate = b2.createdAt ?? "";
14516
+ return bDate.localeCompare(aDate);
14452
14517
  });
14453
14518
  const total = allTasks.length;
14454
14519
  const offset = options?.offset ?? 0;
@@ -14652,7 +14717,7 @@ function formatBoard(result) {
14652
14717
  if (result.tasks.length === 0) {
14653
14718
  return "No tasks found.";
14654
14719
  }
14655
- const headers = ["Priority", "Task", "Summary", "Status", "Cycle", "Phase", "Module", "Epic", "Effort", "Created"];
14720
+ const headers = ["Priority", "Task", "Summary", "Status", "Cycle", "Phase", "Module", "Epic", "Effort", "Created", "Source"];
14656
14721
  const rows = result.tasks.map((t) => [
14657
14722
  t.priority,
14658
14723
  t.id,
@@ -14663,7 +14728,8 @@ function formatBoard(result) {
14663
14728
  t.module ?? "-",
14664
14729
  t.epic ?? "-",
14665
14730
  t.complexity ?? "-",
14666
- t.createdAt ?? "-"
14731
+ t.createdAt ?? "-",
14732
+ t.source ?? "-"
14667
14733
  ]);
14668
14734
  const widths = headers.map(
14669
14735
  (h, i) => Math.max(h.length, ...rows.map((r) => r[i].length))
@@ -15941,7 +16007,8 @@ async function applySetup(adapter2, config2, input, briefText, adSeedText, conve
15941
16007
  phase: task.phase || "Phase 1",
15942
16008
  owner: "TBD",
15943
16009
  reviewed: false,
15944
- notes: task.notes
16010
+ notes: task.notes,
16011
+ source: "llm"
15945
16012
  });
15946
16013
  createdTasks++;
15947
16014
  }
@@ -17520,19 +17587,23 @@ ${lines.join("\n")}
17520
17587
  const VALID_COMPLEXITIES2 = /* @__PURE__ */ new Set(["XS", "Small", "Medium", "Large", "XL"]);
17521
17588
  const priority = input.priority && VALID_PRIORITIES2.has(input.priority) ? input.priority : "P2 Medium";
17522
17589
  const complexity = input.complexity && VALID_COMPLEXITIES2.has(input.complexity) ? input.complexity : "Small";
17523
- const PREFIX_MAP = {
17524
- bug: "bug",
17525
- research: "research",
17526
- feedback: "feedback"
17527
- };
17590
+ const VALID_TYPES = /* @__PURE__ */ new Set(["task", "bug", "research", "idea", "spike"]);
17528
17591
  let taskTitle = input.text;
17529
17592
  let taskType = "idea";
17530
- const prefixMatch = input.text.match(/^\[([a-zA-Z]+)\]\s*/);
17531
- if (prefixMatch) {
17532
- const key = prefixMatch[1].toLowerCase();
17533
- if (key in PREFIX_MAP) {
17534
- taskType = PREFIX_MAP[key];
17535
- taskTitle = input.text.slice(prefixMatch[0].length);
17593
+ if (input.type && VALID_TYPES.has(input.type)) {
17594
+ taskType = input.type;
17595
+ } else {
17596
+ const PREFIX_MAP = {
17597
+ bug: "bug",
17598
+ research: "research"
17599
+ };
17600
+ const prefixMatch = input.text.match(/^\[([a-zA-Z]+)\]\s*/);
17601
+ if (prefixMatch) {
17602
+ const key = prefixMatch[1].toLowerCase();
17603
+ if (key in PREFIX_MAP) {
17604
+ taskType = PREFIX_MAP[key];
17605
+ taskTitle = input.text.slice(prefixMatch[0].length);
17606
+ }
17536
17607
  }
17537
17608
  }
17538
17609
  const task = await adapter2.createTask({
@@ -17551,7 +17622,8 @@ ${lines.join("\n")}
17551
17622
  notes: input.notes || "",
17552
17623
  taskType,
17553
17624
  maturity: "raw",
17554
- docRef: input.docRef
17625
+ docRef: input.docRef,
17626
+ source: "llm"
17555
17627
  });
17556
17628
  if (input.notes && adapter2.updateCycleLearningActionRef) {
17557
17629
  const learningRefs = input.notes.match(/learning:([a-f0-9-]+)/gi);
@@ -17659,6 +17731,11 @@ var ideaTool = {
17659
17731
  type: "boolean",
17660
17732
  description: "Force creation even if a high-overlap duplicate or already-done task is detected. Default: false."
17661
17733
  },
17734
+ type: {
17735
+ type: "string",
17736
+ enum: ["task", "bug", "research", "spike"],
17737
+ description: 'Task type. Defaults to "task". Use "bug" for defects, "research" for investigation tasks, "spike" for time-boxed experiments. The planner uses this to generate type-specific BUILD HANDOFFs.'
17738
+ },
17662
17739
  doc_ref: {
17663
17740
  type: "string",
17664
17741
  description: 'Path to a reference document (e.g. "docs/research/foo.md"). Stored as a structured field \u2014 replaces the fragile "Reference:" line in notes.'
@@ -17689,7 +17766,8 @@ async function handleIdea(adapter2, config2, args) {
17689
17766
  notes: rawNotes,
17690
17767
  discovery: args.discovery === true,
17691
17768
  force: args.force === true,
17692
- docRef: args.doc_ref?.trim()
17769
+ docRef: args.doc_ref?.trim(),
17770
+ type: args.type
17693
17771
  };
17694
17772
  const useGit = isGitAvailable() && isGitRepo(config2.projectRoot);
17695
17773
  const currentBranch = useGit ? getCurrentBranch(config2.projectRoot) : null;
@@ -17776,7 +17854,8 @@ async function captureBug(adapter2, input) {
17776
17854
  createdCycle: health.totalCycles,
17777
17855
  notes: input.notes || "",
17778
17856
  taskType: "bug",
17779
- maturity: "investigated"
17857
+ maturity: "investigated",
17858
+ source: "llm"
17780
17859
  });
17781
17860
  }
17782
17861
 
@@ -17947,7 +18026,8 @@ async function recordAdHoc(adapter2, input) {
17947
18026
  reviewed: true,
17948
18027
  createdCycle: cycle,
17949
18028
  notes: input.notes ? `[ad-hoc] ${input.notes}` : "[ad-hoc]",
17950
- taskType: "task"
18029
+ taskType: "task",
18030
+ source: "owner"
17951
18031
  });
17952
18032
  }
17953
18033
  const report = {
@@ -18461,11 +18541,88 @@ When done, call \`board_reconcile\` again with:
18461
18541
  - \`mode\`: "retriage-apply"
18462
18542
  - \`llm_response\`: your complete output (both parts)
18463
18543
  `;
18544
+ async function checkCycleIntegrity(adapter2) {
18545
+ const report = {
18546
+ multiActiveCycles: [],
18547
+ ghostCycles: [],
18548
+ orphanedAssignments: [],
18549
+ autoFixed: []
18550
+ };
18551
+ try {
18552
+ const cycles = await adapter2.readCycles();
18553
+ const allTasks = await adapter2.queryBoard({ status: ["Backlog", "In Cycle", "Ready", "In Progress", "In Review", "Done", "Blocked", "Cancelled", "Deferred"] });
18554
+ const activeCycles = cycles.filter((c) => c.status === "active");
18555
+ if (activeCycles.length > 1) {
18556
+ for (const cycle of activeCycles) {
18557
+ const taskCount = allTasks.filter((t) => t.cycle === cycle.number).length;
18558
+ report.multiActiveCycles.push({ cycleNumber: cycle.number, id: cycle.id, taskCount });
18559
+ }
18560
+ }
18561
+ const validCycleNumbers = new Set(cycles.map((c) => c.number));
18562
+ for (const cycle of cycles) {
18563
+ if (cycle.status === "completed") continue;
18564
+ const taskCount = allTasks.filter((t) => t.cycle === cycle.number).length;
18565
+ if (taskCount === 0 && cycle.status === "active") {
18566
+ report.ghostCycles.push({ cycleNumber: cycle.number, id: cycle.id });
18567
+ }
18568
+ }
18569
+ for (const task of allTasks) {
18570
+ if (task.cycle && !validCycleNumbers.has(task.cycle)) {
18571
+ report.orphanedAssignments.push({
18572
+ taskId: task.id,
18573
+ title: task.title,
18574
+ assignedCycle: task.cycle
18575
+ });
18576
+ }
18577
+ }
18578
+ } catch {
18579
+ }
18580
+ return report;
18581
+ }
18582
+ function formatCycleIntegrityReport(report) {
18583
+ const lines = [];
18584
+ const hasIssues = report.multiActiveCycles.length > 1 || report.ghostCycles.length > 0 || report.orphanedAssignments.length > 0;
18585
+ if (!hasIssues) return "";
18586
+ lines.push("### Cycle Integrity Check");
18587
+ lines.push("");
18588
+ if (report.multiActiveCycles.length > 1) {
18589
+ lines.push(`**Multi-active-cycle detected** (${report.multiActiveCycles.length} active cycles \u2014 needs manual fix):`);
18590
+ const sorted = [...report.multiActiveCycles].sort((a, b2) => b2.taskCount - a.taskCount || b2.cycleNumber - a.cycleNumber);
18591
+ for (const c of sorted) {
18592
+ const tag = c === sorted[0] ? " \u2190 likely current" : " \u2190 candidate for completion";
18593
+ lines.push(`- Cycle ${c.cycleNumber} (${c.taskCount} tasks)${tag}`);
18594
+ }
18595
+ lines.push("Fix: run `UPDATE cycles SET status = 'completed' WHERE number = <N>` for the stale cycle(s).");
18596
+ lines.push("");
18597
+ }
18598
+ if (report.ghostCycles.length > 0) {
18599
+ lines.push("**Ghost cycles** (active but no tasks \u2014 needs manual review):");
18600
+ for (const g of report.ghostCycles) {
18601
+ lines.push(`- Cycle ${g.cycleNumber} (id: ${g.id}) \u2014 active with 0 tasks`);
18602
+ }
18603
+ lines.push("");
18604
+ }
18605
+ if (report.orphanedAssignments.length > 0) {
18606
+ lines.push("**Orphaned cycle assignments** (tasks pointing to non-existent cycles):");
18607
+ for (const o of report.orphanedAssignments) {
18608
+ lines.push(`- **${o.taskId}**: "${o.title}" \u2192 cycle ${o.assignedCycle} (does not exist)`);
18609
+ }
18610
+ lines.push("");
18611
+ }
18612
+ return lines.join("\n") + "\n";
18613
+ }
18464
18614
  async function handleBoardReconcile(adapter2, config2, args) {
18465
18615
  const mode = args.mode ?? "prepare";
18466
18616
  if (mode === "prepare") {
18467
18617
  const context = await prepareReconcile(adapter2);
18618
+ const cycleReport = await checkCycleIntegrity(adapter2);
18619
+ const cycleSection = formatCycleIntegrityReport(cycleReport);
18468
18620
  if (context === "No backlog tasks to reconcile.") {
18621
+ if (cycleSection) {
18622
+ return textResponse(`No backlog tasks to reconcile.
18623
+
18624
+ ${cycleSection}`);
18625
+ }
18469
18626
  return textResponse(context);
18470
18627
  }
18471
18628
  let mismatchSection = "";
@@ -18496,7 +18653,7 @@ async function handleBoardReconcile(adapter2, config2, args) {
18496
18653
  `${RECONCILE_PROMPT}
18497
18654
  ---
18498
18655
 
18499
- ### Backlog Context
18656
+ ${cycleSection}### Backlog Context
18500
18657
 
18501
18658
  ${mismatchSection}${context}
18502
18659
  ---
@@ -19866,9 +20023,10 @@ async function getHierarchyPosition(adapter2) {
19866
20023
  adapter2.queryBoard()
19867
20024
  ]);
19868
20025
  if (horizons.length === 0) return void 0;
19869
- const activeHorizon = horizons.find((h) => h.status === "active") || horizons[0];
19870
- const activeStages = stages.filter((s) => s.horizonId === activeHorizon.id);
19871
- const activeStage = activeStages.find((s) => s.status === "active") || activeStages[0];
20026
+ const isActive = (s) => s.status.toLowerCase() === "active";
20027
+ const activeHorizon = horizons.find(isActive) || horizons[0];
20028
+ const horizonStages = stages.filter((s) => s.horizonId === activeHorizon.id).sort((a, b2) => (a.sortOrder ?? 0) - (b2.sortOrder ?? 0));
20029
+ const activeStage = horizonStages.find(isActive) || horizonStages[0];
19872
20030
  if (!activeStage) return void 0;
19873
20031
  const stagePhases = phases.filter((p) => p.stageId === activeStage.id);
19874
20032
  const activePhases = stagePhases.filter((p) => p.status === "In Progress");
package/dist/prompts.js CHANGED
@@ -133,10 +133,12 @@ This is Cycle 0 \u2014 the first planning cycle for a brand-new project.
133
133
 
134
134
  2. **North Star** \u2014 Propose a one-sentence North Star statement, a success metric, and a key metric.
135
135
 
136
- 3. **Initial Board** \u2014 Generate 3-5 tasks:
137
- - Task 1: Project setup / scaffolding (if needed)
138
- - Task 2: Core data model or foundational structure
139
- - Tasks 3-5: First user-facing features, broken into small steps
136
+ 3. **Initial Board** \u2014 Generate 3-5 tasks based on the project's actual tech stack and goals:
137
+ - Infer the project type from the brief/description (CLI, web app, mobile app, API, library, game, data pipeline, etc.)
138
+ - Task 1: Project-appropriate setup (toolchain, dependencies, config \u2014 NOT "scaffolding" if the project already has code)
139
+ - Task 2: Core functionality that proves the concept works (data model, main loop, core algorithm \u2014 whatever the project needs first)
140
+ - Tasks 3-5: First deliverables that demonstrate value, broken into small steps appropriate for the project type
141
+ - Do NOT assume web-app patterns (routes, pages, components) unless the brief explicitly describes a web application
140
142
  - All tasks: status Backlog, priority P1-P2, reviewed true, phase "Phase 1"
141
143
 
142
144
  4. **First Active Decision** \u2014 If the description implies a clear architectural choice, create AD-1 with Confidence: MEDIUM. If no clear choice, skip this.
@@ -296,11 +298,25 @@ var PLAN_FRAGMENT_RESEARCH = `
296
298
  var PLAN_FRAGMENT_BUG = `
297
299
  **Bug task detection:** When a task's task type is "bug" or the title starts with "Bug:" or "Fix:", apply these rules:
298
300
  - **Auto-P1:** If the task's current priority is P2 or lower, upgrade it to "P1 High" via a boardCorrections entry in Part 2. Note the upgrade in Part 1 analysis.
299
- - Add a BLAST RADIUS note to the BUILD HANDOFF SCOPE section: "Bug fix \u2014 minimal blast radius. Change only what is necessary to fix the reported behaviour. Do not refactor surrounding code or expand scope."
300
- - Add to ACCEPTANCE CRITERIA: "[ ] Fix is targeted \u2014 no unrelated code changed"`;
301
+ - Replace the standard SCOPE (DO THIS) section with bug-specific sections:
302
+ - **REPRODUCE:** Exact steps to reproduce the bug before touching any code. If the task notes describe the symptoms, include them. If not, the first build step is "confirm the bug reproduces."
303
+ - **ROOT CAUSE:** One-sentence hypothesis for the root cause (what is wrong, not what the user sees). The builder must confirm or correct this before implementing a fix.
304
+ - **MINIMAL FIX:** The smallest code change that resolves the root cause. "Bug fix \u2014 minimal blast radius. Change only what is necessary. Do not refactor surrounding code or expand scope."
305
+ - **REGRESSION TEST:** How to verify the bug is fixed and won't silently recur. Describe the test (manual or automated) \u2014 the builder must confirm this passes.
306
+ - Add to ACCEPTANCE CRITERIA: "[ ] Fix is targeted \u2014 no unrelated code changed" and "[ ] Regression test confirms the bug no longer reproduces"`;
301
307
  var PLAN_FRAGMENT_IDEA = `
302
308
  **Idea task detection:** When a task's task type is "idea", add a scope clarification note to the BUILD HANDOFF:
303
309
  - Add to SCOPE (DO THIS): "This task originated as an idea. Confirm the exact deliverable before implementing \u2014 check task notes and any referenced docs for intent. If scope is unclear, flag it in the build report surprises."`;
310
+ var PLAN_FRAGMENT_SPIKE = `
311
+ **Spike task detection:** When a task's task type is "spike" or the title starts with "Spike:", apply these rules:
312
+ - Spikes are time-boxed investigations, not implementation tasks. The deliverable is a FINDING, not code.
313
+ - Replace the standard BUILD HANDOFF sections with spike-specific sections:
314
+ - **TIME-BOX:** Maximum effort for this spike (e.g. "Stop after S effort / ~2 hours"). If the question isn't answered by then, the spike is done \u2014 report what you found.
315
+ - **GOAL:** The specific question this spike answers (one sentence, phrased as a question).
316
+ - **OUTPUT:** What the spike produces: a written finding (doc in docs/research/ or notes in the build report), optionally a proof-of-concept if code is needed.
317
+ - **DONE CONDITION:** "Question answered OR time-box hit, whichever comes first."
318
+ - Keep SCOPE BOUNDARY, SECURITY CONSIDERATIONS, and PRE-BUILD VERIFICATION as normal.
319
+ - Spikes should be estimated conservatively: XS or S. If a spike needs M+ effort, it's not a spike \u2014 reclassify as a research task.`;
304
320
  var PLAN_FRAGMENT_UI = `
305
321
  **UI/visual task detection:** When a task's title or notes contain keywords suggesting frontend visual work (e.g. "visual", "design", "UI", "styling", "refresh", "frontend", "landing page", "hero", "carousel", "theme", "layout", "cockpit", "dashboard", "page"), apply these handoff additions:
306
322
  - Add to SCOPE: "Read \`.impeccable.md\` for brand palette, design principles, and audience context before writing any code. Use the \`frontend-design\` skill for implementation."
@@ -409,6 +425,7 @@ Standard planning cycle with full board review.
409
425
  if (flags.hasResearchTasks) parts.push(PLAN_FRAGMENT_RESEARCH);
410
426
  if (flags.hasBugTasks) parts.push(PLAN_FRAGMENT_BUG);
411
427
  if (flags.hasIdeaTasks) parts.push(PLAN_FRAGMENT_IDEA);
428
+ if (flags.hasSpikeTasks) parts.push(PLAN_FRAGMENT_SPIKE);
412
429
  if (flags.hasUITasks) parts.push(PLAN_FRAGMENT_UI);
413
430
  parts.push(`
414
431
  11. **New Tasks (max 3 per cycle)** \u2014 Actively mine the Recent Build Reports for task candidates. For each report, check:
@@ -727,6 +744,15 @@ You MUST cover these 5 sections. Each is mandatory.
727
744
  ${compressionJob}
728
745
  Note: Hierarchy assessment and structural drift detection are handled within section 5 (AD & Hierarchy Housekeeping). They do not need their own sections.
729
746
 
747
+ ## DETECT STRATEGIC DECISIONS
748
+
749
+ Watch for direction changes, architecture shifts, deprioritisation with reasoning, new principles, or competitive positioning decisions in the project data.
750
+
751
+ When detected:
752
+ 1. Flag it in the review: "Strategic direction change detected \u2014 [description]."
753
+ 2. Propose an AD update or new AD in the structured output (Part 2 \`activeDecisions\` array).
754
+ 3. Recommend running \`strategy_change\` if the shift requires immediate action before the next plan.
755
+
730
756
  ## OUTPUT FORMAT
731
757
 
732
758
  Your output has TWO parts:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@papi-ai/server",
3
- "version": "0.7.2",
3
+ "version": "0.7.4-alpha.1",
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",