@papi-ai/server 0.7.3-alpha.1 → 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`
@@ -10000,10 +10003,12 @@ This is Cycle 0 \u2014 the first planning cycle for a brand-new project.
10000
10003
 
10001
10004
  2. **North Star** \u2014 Propose a one-sentence North Star statement, a success metric, and a key metric.
10002
10005
 
10003
- 3. **Initial Board** \u2014 Generate 3-5 tasks:
10004
- - Task 1: Project setup / scaffolding (if needed)
10005
- - Task 2: Core data model or foundational structure
10006
- - 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
10007
10012
  - All tasks: status Backlog, priority P1-P2, reviewed true, phase "Phase 1"
10008
10013
 
10009
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.
@@ -10163,11 +10168,25 @@ var PLAN_FRAGMENT_RESEARCH = `
10163
10168
  var PLAN_FRAGMENT_BUG = `
10164
10169
  **Bug task detection:** When a task's task type is "bug" or the title starts with "Bug:" or "Fix:", apply these rules:
10165
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.
10166
- - 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."
10167
- - 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"`;
10168
10177
  var PLAN_FRAGMENT_IDEA = `
10169
10178
  **Idea task detection:** When a task's task type is "idea", add a scope clarification note to the BUILD HANDOFF:
10170
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.`;
10171
10190
  var PLAN_FRAGMENT_UI = `
10172
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:
10173
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."
@@ -10276,6 +10295,7 @@ Standard planning cycle with full board review.
10276
10295
  if (flags.hasResearchTasks) parts.push(PLAN_FRAGMENT_RESEARCH);
10277
10296
  if (flags.hasBugTasks) parts.push(PLAN_FRAGMENT_BUG);
10278
10297
  if (flags.hasIdeaTasks) parts.push(PLAN_FRAGMENT_IDEA);
10298
+ if (flags.hasSpikeTasks) parts.push(PLAN_FRAGMENT_SPIKE);
10279
10299
  if (flags.hasUITasks) parts.push(PLAN_FRAGMENT_UI);
10280
10300
  parts.push(`
10281
10301
  11. **New Tasks (max 3 per cycle)** \u2014 Actively mine the Recent Build Reports for task candidates. For each report, check:
@@ -10594,6 +10614,15 @@ You MUST cover these 5 sections. Each is mandatory.
10594
10614
  ${compressionJob}
10595
10615
  Note: Hierarchy assessment and structural drift detection are handled within section 5 (AD & Hierarchy Housekeeping). They do not need their own sections.
10596
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
+
10597
10626
  ## OUTPUT FORMAT
10598
10627
 
10599
10628
  Your output has TWO parts:
@@ -11453,21 +11482,24 @@ function detectBoardFlags(tasks) {
11453
11482
  let hasBugTasks = false;
11454
11483
  let hasResearchTasks = false;
11455
11484
  let hasIdeaTasks = false;
11485
+ let hasSpikeTasks = false;
11456
11486
  let hasUITasks = false;
11457
11487
  const uiKeywords = /\b(visual|design|UI|styling|refresh|frontend|landing page|hero|carousel|theme|layout|cockpit|dashboard|page)\b/i;
11458
11488
  for (const t of tasks) {
11459
11489
  if (t.taskType === "bug" || /^(Bug:|Fix:)/i.test(t.title)) hasBugTasks = true;
11460
11490
  if (t.taskType === "research" || /^Research:/i.test(t.title)) hasResearchTasks = true;
11461
11491
  if (t.taskType === "idea") hasIdeaTasks = true;
11492
+ if (t.taskType === "spike" || /^Spike:/i.test(t.title)) hasSpikeTasks = true;
11462
11493
  if (uiKeywords.test(t.title) || uiKeywords.test(t.notes ?? "")) hasUITasks = true;
11463
11494
  }
11464
- return { hasBugTasks, hasResearchTasks, hasIdeaTasks, hasUITasks };
11495
+ return { hasBugTasks, hasResearchTasks, hasIdeaTasks, hasSpikeTasks, hasUITasks };
11465
11496
  }
11466
11497
  function detectBoardFlagsFromText(boardText) {
11467
11498
  return {
11468
11499
  hasBugTasks: /\b(bug|Bug:|Fix:)\b/i.test(boardText),
11469
11500
  hasResearchTasks: /\b(research|Research:)\b/i.test(boardText),
11470
11501
  hasIdeaTasks: /\bidea\b/i.test(boardText),
11502
+ hasSpikeTasks: /\b(spike|Spike:)\b/i.test(boardText),
11471
11503
  hasUITasks: /\b(visual|design|UI|styling|refresh|frontend|landing page|hero|carousel|theme|layout|cockpit|dashboard|page)\b/i.test(boardText)
11472
11504
  };
11473
11505
  }
@@ -11944,13 +11976,24 @@ ${cleanContent}`;
11944
11976
  console.error(`[plan-perf] transactionalWriteBack: total=${writeBackMs}ms`);
11945
11977
  const verifyWarnings = [];
11946
11978
  try {
11947
- 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
+ ]);
11948
11983
  const newCycle = cycles.find((s) => s.number === newCycleNumber);
11949
11984
  if (!newCycle) {
11950
- 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
+ }
11951
11994
  }
11952
11995
  } catch {
11953
- verifyWarnings.push("Post-write verification: could not read cycles table");
11996
+ verifyWarnings.push("Post-write verification: could not read cycles/tasks tables");
11954
11997
  }
11955
11998
  const allWarnings = [...result.warnings, ...verifyWarnings];
11956
11999
  const handoffCount = data.cycleHandoffs?.length ?? 0;
@@ -12037,7 +12080,8 @@ ${cleanContent}`;
12037
12080
  cycle: newCycleNumber,
12038
12081
  createdCycle: newCycleNumber,
12039
12082
  why: task.why || "",
12040
- notes: task.notes || ""
12083
+ notes: task.notes || "",
12084
+ source: "llm"
12041
12085
  });
12042
12086
  newTaskIdMap.set(`new-${i + 1}`, created.id);
12043
12087
  if (adapter2.updateCycleLearningActionRef && task.notes) {
@@ -14434,6 +14478,18 @@ ${result.userMessage}
14434
14478
  // src/services/board.ts
14435
14479
  var ACTIVE_STATUSES = ["Backlog", "In Cycle", "Ready", "In Progress", "In Review", "Blocked"];
14436
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
+ };
14437
14493
  async function viewBoard(adapter2, phaseFilter, options) {
14438
14494
  const queryOptions = {};
14439
14495
  const phase = options?.phase ?? phaseFilter;
@@ -14448,9 +14504,16 @@ async function viewBoard(adapter2, phaseFilter, options) {
14448
14504
  Object.keys(queryOptions).length > 0 ? queryOptions : void 0
14449
14505
  );
14450
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;
14451
14510
  const ai = PRIORITY_ORDER.indexOf(a.priority);
14452
14511
  const bi = PRIORITY_ORDER.indexOf(b2.priority);
14453
- 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);
14454
14517
  });
14455
14518
  const total = allTasks.length;
14456
14519
  const offset = options?.offset ?? 0;
@@ -14654,7 +14717,7 @@ function formatBoard(result) {
14654
14717
  if (result.tasks.length === 0) {
14655
14718
  return "No tasks found.";
14656
14719
  }
14657
- 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"];
14658
14721
  const rows = result.tasks.map((t) => [
14659
14722
  t.priority,
14660
14723
  t.id,
@@ -14665,7 +14728,8 @@ function formatBoard(result) {
14665
14728
  t.module ?? "-",
14666
14729
  t.epic ?? "-",
14667
14730
  t.complexity ?? "-",
14668
- t.createdAt ?? "-"
14731
+ t.createdAt ?? "-",
14732
+ t.source ?? "-"
14669
14733
  ]);
14670
14734
  const widths = headers.map(
14671
14735
  (h, i) => Math.max(h.length, ...rows.map((r) => r[i].length))
@@ -15943,7 +16007,8 @@ async function applySetup(adapter2, config2, input, briefText, adSeedText, conve
15943
16007
  phase: task.phase || "Phase 1",
15944
16008
  owner: "TBD",
15945
16009
  reviewed: false,
15946
- notes: task.notes
16010
+ notes: task.notes,
16011
+ source: "llm"
15947
16012
  });
15948
16013
  createdTasks++;
15949
16014
  }
@@ -17522,19 +17587,23 @@ ${lines.join("\n")}
17522
17587
  const VALID_COMPLEXITIES2 = /* @__PURE__ */ new Set(["XS", "Small", "Medium", "Large", "XL"]);
17523
17588
  const priority = input.priority && VALID_PRIORITIES2.has(input.priority) ? input.priority : "P2 Medium";
17524
17589
  const complexity = input.complexity && VALID_COMPLEXITIES2.has(input.complexity) ? input.complexity : "Small";
17525
- const PREFIX_MAP = {
17526
- bug: "bug",
17527
- research: "research",
17528
- feedback: "feedback"
17529
- };
17590
+ const VALID_TYPES = /* @__PURE__ */ new Set(["task", "bug", "research", "idea", "spike"]);
17530
17591
  let taskTitle = input.text;
17531
17592
  let taskType = "idea";
17532
- const prefixMatch = input.text.match(/^\[([a-zA-Z]+)\]\s*/);
17533
- if (prefixMatch) {
17534
- const key = prefixMatch[1].toLowerCase();
17535
- if (key in PREFIX_MAP) {
17536
- taskType = PREFIX_MAP[key];
17537
- 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
+ }
17538
17607
  }
17539
17608
  }
17540
17609
  const task = await adapter2.createTask({
@@ -17553,7 +17622,8 @@ ${lines.join("\n")}
17553
17622
  notes: input.notes || "",
17554
17623
  taskType,
17555
17624
  maturity: "raw",
17556
- docRef: input.docRef
17625
+ docRef: input.docRef,
17626
+ source: "llm"
17557
17627
  });
17558
17628
  if (input.notes && adapter2.updateCycleLearningActionRef) {
17559
17629
  const learningRefs = input.notes.match(/learning:([a-f0-9-]+)/gi);
@@ -17661,6 +17731,11 @@ var ideaTool = {
17661
17731
  type: "boolean",
17662
17732
  description: "Force creation even if a high-overlap duplicate or already-done task is detected. Default: false."
17663
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
+ },
17664
17739
  doc_ref: {
17665
17740
  type: "string",
17666
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.'
@@ -17691,7 +17766,8 @@ async function handleIdea(adapter2, config2, args) {
17691
17766
  notes: rawNotes,
17692
17767
  discovery: args.discovery === true,
17693
17768
  force: args.force === true,
17694
- docRef: args.doc_ref?.trim()
17769
+ docRef: args.doc_ref?.trim(),
17770
+ type: args.type
17695
17771
  };
17696
17772
  const useGit = isGitAvailable() && isGitRepo(config2.projectRoot);
17697
17773
  const currentBranch = useGit ? getCurrentBranch(config2.projectRoot) : null;
@@ -17778,7 +17854,8 @@ async function captureBug(adapter2, input) {
17778
17854
  createdCycle: health.totalCycles,
17779
17855
  notes: input.notes || "",
17780
17856
  taskType: "bug",
17781
- maturity: "investigated"
17857
+ maturity: "investigated",
17858
+ source: "llm"
17782
17859
  });
17783
17860
  }
17784
17861
 
@@ -17949,7 +18026,8 @@ async function recordAdHoc(adapter2, input) {
17949
18026
  reviewed: true,
17950
18027
  createdCycle: cycle,
17951
18028
  notes: input.notes ? `[ad-hoc] ${input.notes}` : "[ad-hoc]",
17952
- taskType: "task"
18029
+ taskType: "task",
18030
+ source: "owner"
17953
18031
  });
17954
18032
  }
17955
18033
  const report = {
@@ -18463,11 +18541,88 @@ When done, call \`board_reconcile\` again with:
18463
18541
  - \`mode\`: "retriage-apply"
18464
18542
  - \`llm_response\`: your complete output (both parts)
18465
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
+ }
18466
18614
  async function handleBoardReconcile(adapter2, config2, args) {
18467
18615
  const mode = args.mode ?? "prepare";
18468
18616
  if (mode === "prepare") {
18469
18617
  const context = await prepareReconcile(adapter2);
18618
+ const cycleReport = await checkCycleIntegrity(adapter2);
18619
+ const cycleSection = formatCycleIntegrityReport(cycleReport);
18470
18620
  if (context === "No backlog tasks to reconcile.") {
18621
+ if (cycleSection) {
18622
+ return textResponse(`No backlog tasks to reconcile.
18623
+
18624
+ ${cycleSection}`);
18625
+ }
18471
18626
  return textResponse(context);
18472
18627
  }
18473
18628
  let mismatchSection = "";
@@ -18498,7 +18653,7 @@ async function handleBoardReconcile(adapter2, config2, args) {
18498
18653
  `${RECONCILE_PROMPT}
18499
18654
  ---
18500
18655
 
18501
- ### Backlog Context
18656
+ ${cycleSection}### Backlog Context
18502
18657
 
18503
18658
  ${mismatchSection}${context}
18504
18659
  ---
@@ -19868,9 +20023,10 @@ async function getHierarchyPosition(adapter2) {
19868
20023
  adapter2.queryBoard()
19869
20024
  ]);
19870
20025
  if (horizons.length === 0) return void 0;
19871
- const activeHorizon = horizons.find((h) => h.status === "active") || horizons[0];
19872
- const activeStages = stages.filter((s) => s.horizonId === activeHorizon.id);
19873
- 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];
19874
20030
  if (!activeStage) return void 0;
19875
20031
  const stagePhases = phases.filter((p) => p.stageId === activeStage.id);
19876
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.3-alpha.1",
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",