@papi-ai/server 0.7.12 → 0.7.13

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
@@ -2068,6 +2068,102 @@ ${footer}`);
2068
2068
  await this.write("STRATEGY_RECOMMENDATIONS.md", updated);
2069
2069
  }
2070
2070
  // -------------------------------------------------------------------------
2071
+ // Strategy Review Agenda (markdown persistence)
2072
+ // -------------------------------------------------------------------------
2073
+ async addAgendaTopic(input) {
2074
+ const id = randomUUID6();
2075
+ const createdAt = (/* @__PURE__ */ new Date()).toISOString();
2076
+ const full = {
2077
+ id,
2078
+ topic: input.topic,
2079
+ source: input.source,
2080
+ sourceCycle: input.sourceCycle,
2081
+ status: "pending",
2082
+ createdAt
2083
+ };
2084
+ const content = await this.readOptional("STRATEGY_REVIEW_AGENDA.md");
2085
+ const header = "# Strategy Review Agenda\n\n<!-- PAPI-ADAPTER: parse the yaml block below -->\n\n<!-- PAPI-YAML-START -->\ntopics:\n";
2086
+ const footer = "<!-- PAPI-YAML-END -->\n";
2087
+ const entry = [
2088
+ ` - id: ${full.id}`,
2089
+ ` topic: ${JSON.stringify(full.topic)}`,
2090
+ ` source: ${full.source}`,
2091
+ full.sourceCycle != null ? ` source_cycle: ${full.sourceCycle}` : null,
2092
+ ` status: ${full.status}`,
2093
+ ` created_at: ${full.createdAt}`
2094
+ ].filter(Boolean).join("\n");
2095
+ if (!content) {
2096
+ await this.write("STRATEGY_REVIEW_AGENDA.md", `${header}${entry}
2097
+ ${footer}`);
2098
+ } else {
2099
+ const insertPoint = content.indexOf("<!-- PAPI-YAML-END -->");
2100
+ if (insertPoint === -1) {
2101
+ await this.write("STRATEGY_REVIEW_AGENDA.md", `${header}${entry}
2102
+ ${footer}`);
2103
+ } else {
2104
+ const updated = content.slice(0, insertPoint) + entry + "\n" + content.slice(insertPoint);
2105
+ await this.write("STRATEGY_REVIEW_AGENDA.md", updated);
2106
+ }
2107
+ }
2108
+ return full;
2109
+ }
2110
+ async getPendingAgendaTopics() {
2111
+ const content = await this.readOptional("STRATEGY_REVIEW_AGENDA.md");
2112
+ if (!content) return [];
2113
+ const yamlStart = content.indexOf("<!-- PAPI-YAML-START -->");
2114
+ const yamlEnd = content.indexOf("<!-- PAPI-YAML-END -->");
2115
+ if (yamlStart === -1 || yamlEnd === -1) return [];
2116
+ const yamlBlock = content.slice(yamlStart + "<!-- PAPI-YAML-START -->".length, yamlEnd).trim();
2117
+ const entries = yamlBlock.split(/(?=\s+-\s+id:)/);
2118
+ const topics = [];
2119
+ for (const block of entries) {
2120
+ const idMatch = block.match(/id:\s+(.+)/);
2121
+ const topicMatch = block.match(/topic:\s+(.+)/);
2122
+ const sourceMatch = block.match(/source:\s+(\S+)/);
2123
+ const statusMatch = block.match(/status:\s+(\S+)/);
2124
+ const createdMatch = block.match(/created_at:\s+(.+)/);
2125
+ const sourceCycleMatch = block.match(/source_cycle:\s+(\d+)/);
2126
+ if (!idMatch || !topicMatch || !sourceMatch || !statusMatch || !createdMatch) continue;
2127
+ if (statusMatch[1].trim() !== "pending") continue;
2128
+ let parsedTopic = topicMatch[1].trim();
2129
+ if (parsedTopic.startsWith('"') && parsedTopic.endsWith('"')) {
2130
+ try {
2131
+ parsedTopic = JSON.parse(parsedTopic);
2132
+ } catch {
2133
+ }
2134
+ }
2135
+ topics.push({
2136
+ id: idMatch[1].trim(),
2137
+ topic: parsedTopic,
2138
+ source: sourceMatch[1].trim(),
2139
+ sourceCycle: sourceCycleMatch ? parseInt(sourceCycleMatch[1], 10) : void 0,
2140
+ status: "pending",
2141
+ createdAt: createdMatch[1].trim()
2142
+ });
2143
+ }
2144
+ return topics;
2145
+ }
2146
+ async markAgendaTopicsAddressed(ids, cycleNumber) {
2147
+ if (ids.length === 0) return;
2148
+ const content = await this.readOptional("STRATEGY_REVIEW_AGENDA.md");
2149
+ if (!content) return;
2150
+ let updated = content;
2151
+ const addressedAt = (/* @__PURE__ */ new Date()).toISOString();
2152
+ for (const id of ids) {
2153
+ const statusPattern = new RegExp(`(\\s+-\\s+id:\\s+${id}\\n(?:.*\\n)*?\\s+status:\\s+)pending`);
2154
+ updated = updated.replace(statusPattern, `$1addressed`);
2155
+ const insertionAnchor = new RegExp(`(\\s+-\\s+id:\\s+${id}\\n(?:.*\\n)*?\\s+created_at:\\s+[^\\n]+)\\n`);
2156
+ const match = updated.match(insertionAnchor);
2157
+ if (match && !match[0].includes("addressed_at:")) {
2158
+ updated = updated.replace(insertionAnchor, `$1
2159
+ addressed_at: ${addressedAt}
2160
+ addressed_in_review: ${cycleNumber}
2161
+ `);
2162
+ }
2163
+ }
2164
+ await this.write("STRATEGY_REVIEW_AGENDA.md", updated);
2165
+ }
2166
+ // -------------------------------------------------------------------------
2071
2167
  // Decision Events & Scores (markdown persistence)
2072
2168
  // -------------------------------------------------------------------------
2073
2169
  async appendDecisionEvent(event) {
@@ -7569,6 +7665,50 @@ ${newParts.join("\n")}` : newParts.join("\n");
7569
7665
  UPDATE strategy_recommendations
7570
7666
  SET status = 'actioned', dismissal_reason = ${reason}, updated_at = now()
7571
7667
  WHERE id = ${id} AND project_id = ${this.projectId}
7668
+ `;
7669
+ }
7670
+ // -------------------------------------------------------------------------
7671
+ // Strategy Review Agenda
7672
+ // -------------------------------------------------------------------------
7673
+ async addAgendaTopic(input) {
7674
+ const [row] = await this.sql`
7675
+ INSERT INTO strategy_review_agenda (project_id, topic, source, source_cycle)
7676
+ VALUES (${this.projectId}, ${input.topic}, ${input.source}, ${input.sourceCycle ?? null})
7677
+ RETURNING id, topic, source, source_cycle, status, created_at
7678
+ `;
7679
+ return {
7680
+ id: row.id,
7681
+ topic: row.topic,
7682
+ source: row.source,
7683
+ sourceCycle: row.source_cycle ?? void 0,
7684
+ status: row.status,
7685
+ createdAt: row.created_at
7686
+ };
7687
+ }
7688
+ async getPendingAgendaTopics() {
7689
+ const rows = await this.sql`
7690
+ SELECT id, topic, source, source_cycle, status, created_at, addressed_at, addressed_in_review
7691
+ FROM strategy_review_agenda
7692
+ WHERE project_id = ${this.projectId} AND status = 'pending'
7693
+ ORDER BY created_at ASC
7694
+ `;
7695
+ return rows.map((r) => ({
7696
+ id: r.id,
7697
+ topic: r.topic,
7698
+ source: r.source,
7699
+ sourceCycle: r.source_cycle ?? void 0,
7700
+ status: "pending",
7701
+ createdAt: r.created_at,
7702
+ addressedAt: r.addressed_at ?? void 0,
7703
+ addressedInReview: r.addressed_in_review ?? void 0
7704
+ }));
7705
+ }
7706
+ async markAgendaTopicsAddressed(ids, cycleNumber) {
7707
+ if (ids.length === 0) return;
7708
+ await this.sql`
7709
+ UPDATE strategy_review_agenda
7710
+ SET status = 'addressed', addressed_at = now(), addressed_in_review = ${cycleNumber}
7711
+ WHERE project_id = ${this.projectId} AND id = ANY(${ids}::uuid[])
7572
7712
  `;
7573
7713
  }
7574
7714
  // -------------------------------------------------------------------------
@@ -8761,6 +8901,7 @@ __export(git_exports, {
8761
8901
  getHeadCommitSha: () => getHeadCommitSha,
8762
8902
  getLatestTag: () => getLatestTag,
8763
8903
  getOriginRepoSlug: () => getOriginRepoSlug,
8904
+ getPullRequestUrl: () => getPullRequestUrl,
8764
8905
  getUnmergedBranches: () => getUnmergedBranches,
8765
8906
  gitPull: () => gitPull,
8766
8907
  gitPush: () => gitPush,
@@ -8770,9 +8911,11 @@ __export(git_exports, {
8770
8911
  isGhAvailable: () => isGhAvailable,
8771
8912
  isGitAvailable: () => isGitAvailable,
8772
8913
  isGitRepo: () => isGitRepo,
8914
+ listGroupedCycleBranches: () => listGroupedCycleBranches,
8773
8915
  mergePullRequest: () => mergePullRequest,
8774
8916
  resolveBaseBranch: () => resolveBaseBranch,
8775
8917
  runAutoCommit: () => runAutoCommit,
8918
+ squashMergePullRequest: () => squashMergePullRequest,
8776
8919
  stageAllAndCommit: () => stageAllAndCommit,
8777
8920
  stageDirAndCommit: () => stageDirAndCommit,
8778
8921
  tagExists: () => tagExists,
@@ -9226,6 +9369,68 @@ function runAutoCommit(projectRoot, commitFn) {
9226
9369
  return `Auto-commit failed: ${err instanceof Error ? err.message : String(err)}`;
9227
9370
  }
9228
9371
  }
9372
+ function getPullRequestUrl(cwd, branch) {
9373
+ try {
9374
+ const output = execFileSync(
9375
+ "gh",
9376
+ ["pr", "view", branch, "--json", "url", "--jq", ".url"],
9377
+ { cwd, encoding: "utf-8" }
9378
+ ).trim();
9379
+ return output || null;
9380
+ } catch {
9381
+ return null;
9382
+ }
9383
+ }
9384
+ function squashMergePullRequest(cwd, branch) {
9385
+ const repo = getOriginRepoSlug(cwd);
9386
+ const baseArgs = ["pr", "merge", branch, "--squash", "--delete-branch"];
9387
+ if (repo) baseArgs.push("--repo", repo);
9388
+ for (let attempt = 1; attempt <= MERGE_MAX_RETRIES; attempt++) {
9389
+ try {
9390
+ execFileSync("gh", baseArgs, { cwd, encoding: "utf-8" });
9391
+ return { success: true, message: `Squash-merged PR for '${branch}' and deleted branch.` };
9392
+ } catch (err) {
9393
+ const msg = err instanceof Error ? err.message : String(err);
9394
+ if (msg.includes("not mergeable") && attempt < MERGE_MAX_RETRIES) {
9395
+ sleepSync(MERGE_RETRY_DELAY_MS);
9396
+ continue;
9397
+ }
9398
+ return { success: false, message: `PR squash-merge failed: ${msg}` };
9399
+ }
9400
+ }
9401
+ return { success: false, message: "PR squash-merge failed: max retries exceeded" };
9402
+ }
9403
+ function listGroupedCycleBranches(cwd, cycleNum, baseBranch) {
9404
+ const prefix = `feat/cycle-${cycleNum}-`;
9405
+ try {
9406
+ const remoteOutput = execFileSync(
9407
+ "git",
9408
+ ["ls-remote", "--heads", "origin", `${prefix}*`],
9409
+ { cwd, encoding: "utf-8" }
9410
+ ).trim();
9411
+ if (!remoteOutput) return [];
9412
+ const remoteBranches = remoteOutput.split("\n").map((line) => line.split(" ")[1]?.replace("refs/heads/", "").trim()).filter((b2) => !!b2 && b2.startsWith(prefix));
9413
+ return remoteBranches.filter((branch) => {
9414
+ try {
9415
+ const branchTip = execFileSync(
9416
+ "git",
9417
+ ["rev-parse", `origin/${branch}`],
9418
+ { cwd, encoding: "utf-8" }
9419
+ ).trim();
9420
+ execFileSync(
9421
+ "git",
9422
+ ["merge-base", "--is-ancestor", branchTip, baseBranch],
9423
+ { cwd, stdio: "ignore" }
9424
+ );
9425
+ return false;
9426
+ } catch {
9427
+ return true;
9428
+ }
9429
+ });
9430
+ } catch {
9431
+ return getUnmergedBranches(cwd, baseBranch).filter((b2) => b2.startsWith(prefix));
9432
+ }
9433
+ }
9229
9434
  function getFilesChangedFromBase(cwd, baseBranch) {
9230
9435
  try {
9231
9436
  const mergeBase = execFileSync("git", ["merge-base", baseBranch, "HEAD"], { cwd, encoding: "utf-8" }).trim();
@@ -9277,6 +9482,8 @@ function loadConfig() {
9277
9482
  const lightMode = process.env.PAPI_LIGHT_MODE === "true";
9278
9483
  const projectOwner = process.env.PAPI_OWNER ?? "Cathal";
9279
9484
  const skipProjectSpecificRules = process.env.PAPI_SKIP_PROJECT_RULES === "true";
9485
+ const userId = process.env.PAPI_USER_ID || void 0;
9486
+ const telemetryEnabled = process.env.PAPI_TELEMETRY !== "off" && process.env.PAPI_TELEMETRY !== "false";
9280
9487
  const papiEndpoint = process.env.PAPI_ENDPOINT;
9281
9488
  const dataEndpoint = process.env.PAPI_DATA_ENDPOINT;
9282
9489
  const databaseUrl = process.env.DATABASE_URL;
@@ -9287,9 +9494,18 @@ function loadConfig() {
9287
9494
  adapterType = "proxy";
9288
9495
  console.error("[papi] PAPI_PROJECT_ID detected \u2014 switching to proxy adapter (md adapter blocked for external users).");
9289
9496
  }
9290
- if (!projectId && !databaseUrl && !papiEndpoint && adapterType === "md") {
9497
+ if (adapterType === "md" && !userId) {
9291
9498
  throw new Error(
9292
- "PAPI_PROJECT_ID is required to connect to your project.\n\nGet yours at https://getpapi.ai/setup\n\nAlready have one? Make sure PAPI_PROJECT_ID is set in your .mcp.json env config."
9499
+ `PAPI requires a free account to run in local mode.
9500
+
9501
+ Create your account at https://getpapi.ai/setup \u2014 it takes under a minute.
9502
+ Your project data stays on your machine. The account lets PAPI identify you
9503
+ and unlocks dashboard features when you're ready.
9504
+
9505
+ After signing up, add this to your .mcp.json env config:
9506
+ "PAPI_USER_ID": "your-email@example.com"
9507
+
9508
+ Already have an account? Make sure PAPI_USER_ID is set in your .mcp.json env config.`
9293
9509
  );
9294
9510
  }
9295
9511
  return {
@@ -9303,7 +9519,9 @@ function loadConfig() {
9303
9519
  papiEndpoint,
9304
9520
  lightMode,
9305
9521
  projectOwner,
9306
- skipProjectSpecificRules
9522
+ skipProjectSpecificRules,
9523
+ userId,
9524
+ telemetryEnabled
9307
9525
  };
9308
9526
  }
9309
9527
 
@@ -9396,7 +9614,25 @@ async function createAdapter(optionsOrType, maybePapiDir) {
9396
9614
  console.error("[papi] Set PAPI_USER_ID in your .mcp.json env to fix this.");
9397
9615
  }
9398
9616
  }
9399
- await pgAdapter.createProject({ id: projectId, slug, name: slug, papi_dir: papiDir, user_id: userId });
9617
+ let skipCreate = false;
9618
+ if (userId) {
9619
+ const bySlug = await pgAdapter.listProjects({ slug });
9620
+ const userDup = bySlug.find((p) => p.user_id === userId);
9621
+ if (userDup) {
9622
+ console.error(`[papi] \u26A0 Project '${slug}' already exists for this user (id: ${userDup.id}).`);
9623
+ console.error(`[papi] Update PAPI_PROJECT_ID=${userDup.id} in .mcp.json to avoid a duplicate.`);
9624
+ skipCreate = true;
9625
+ }
9626
+ }
9627
+ if (!skipCreate) {
9628
+ await pgAdapter.createProject({ id: projectId, slug, name: slug, papi_dir: papiDir, user_id: userId });
9629
+ }
9630
+ } else if (existing.user_id) {
9631
+ const configuredUserId = process.env["PAPI_USER_ID"] ?? detectUserId();
9632
+ if (configuredUserId && existing.user_id !== configuredUserId) {
9633
+ console.error(`[papi] \u26A0 PAPI_PROJECT_ID=${projectId} belongs to a different user.`);
9634
+ console.error("[papi] Run papi setup or update PAPI_PROJECT_ID in .mcp.json.");
9635
+ }
9400
9636
  }
9401
9637
  await pgAdapter.close();
9402
9638
  } catch {
@@ -10835,6 +11071,9 @@ function buildPlanUserMessage(ctx) {
10835
11071
  }) : PLAN_FULL_INSTRUCTIONS;
10836
11072
  parts.push(instructions);
10837
11073
  }
11074
+ if (ctx.foundationalTasksGuidance) {
11075
+ parts.push("", ctx.foundationalTasksGuidance);
11076
+ }
10838
11077
  if (ctx.skipHandoffs) {
10839
11078
  parts.push(
10840
11079
  "",
@@ -11356,6 +11595,9 @@ function buildReviewUserMessage(ctx) {
11356
11595
  if (ctx.docActionStaleness) {
11357
11596
  parts.push("### Doc Action Staleness", "", ctx.docActionStaleness, "");
11358
11597
  }
11598
+ if (ctx.pendingAgendaTopics) {
11599
+ parts.push("### Queued Agenda Topics", "", "_Topics queued via `strategy_agenda` since the last review. Address each one in this review \u2014 they will be auto-marked as addressed on apply._", "", ctx.pendingAgendaTopics, "");
11600
+ }
11359
11601
  return parts.join("\n");
11360
11602
  }
11361
11603
  function parseReviewStructuredOutput(raw) {
@@ -12042,6 +12284,64 @@ function stripTasksForPlan(tasks) {
12042
12284
  hasHandoff: !!buildHandoff
12043
12285
  }));
12044
12286
  }
12287
+ var BRIEF_SECTIONS = [
12288
+ { name: "title", pattern: /^#\s+\S/m },
12289
+ {
12290
+ name: "target audience",
12291
+ pattern: /\b(target users?|audience|for whom|who (it'?s for|uses|it serves|we're building))/i
12292
+ },
12293
+ {
12294
+ name: "problem statement",
12295
+ pattern: /\b(problem|pain point|why it matters|what problem|solves?)/i
12296
+ },
12297
+ {
12298
+ name: "solution / vision",
12299
+ pattern: /\b(solution|approach|vision|what (it|we) do|how it works|value proposition)/i
12300
+ },
12301
+ {
12302
+ name: "GTM / distribution",
12303
+ pattern: /\b(GTM|go[- ]to[- ]market|distribution|channel|pricing|how (users?|people) (discover|find|reach))/i
12304
+ }
12305
+ ];
12306
+ function assessBriefThinness(brief) {
12307
+ const TEMPLATE_MARKER = "*Describe your project's core value proposition here.*";
12308
+ const briefWithoutTemplate = brief.replace(TEMPLATE_MARKER, "");
12309
+ const populated = [];
12310
+ const missing = [];
12311
+ for (const section of BRIEF_SECTIONS) {
12312
+ if (section.pattern.test(briefWithoutTemplate)) populated.push(section.name);
12313
+ else missing.push(section.name);
12314
+ }
12315
+ return { populatedSections: populated, missingSections: missing };
12316
+ }
12317
+ function computeFoundationalTasksGuidance(cycleNumber, brief) {
12318
+ if (cycleNumber > 1) return void 0;
12319
+ const { populatedSections, missingSections } = assessBriefThinness(brief);
12320
+ if (populatedSections.length >= 4) return void 0;
12321
+ const populatedList = populatedSections.length > 0 ? populatedSections.join(", ") : "none";
12322
+ const missingList = missingSections.join(", ");
12323
+ return [
12324
+ "## FOUNDATIONAL TASKS GUIDANCE",
12325
+ "",
12326
+ `The project brief is thin \u2014 only ${populatedSections.length} of 5 key sections are clearly populated (${populatedList}). Missing or weak sections: ${missingList}.`,
12327
+ "",
12328
+ "Generate 3-5 foundational research/discovery tasks targeting the MISSING sections. These help the Owner fill in their brief before the product gets built on top of shaky foundations.",
12329
+ "",
12330
+ "Rules for foundational tasks:",
12331
+ "- **ADDITIONS, not replacements.** Include them ALONGSIDE the user's backlog items. If the user submitted 3 ideas, the cycle should have 3 user items + 3-5 foundational items (total 6-8 tasks).",
12332
+ '- **Target specific missing sections.** Do NOT generate "refine audience" if audience is already populated. Each foundational task must close a specific gap above.',
12333
+ "- **Task type: `research` or `discovery`** \u2014 deliverable is a findings doc, not shipped code.",
12334
+ "- **Effort: S or M** \u2014 foundational work should be timeboxed, not open-ended.",
12335
+ '- **WHY in the handoff must tie to the specific gap.** Example: "Brief is thin on GTM \u2014 knowing how users discover the product is a prerequisite for scoping distribution work." Not generic.',
12336
+ "- **Cap at 5 foundational tasks** to avoid drowning the user.",
12337
+ "- **Cycle 2+ will NOT receive this guidance** \u2014 even if the brief stays thin. Foundational work is a one-shot onboarding primitive, not a recurring pattern.",
12338
+ "",
12339
+ "In the structured output:",
12340
+ "- Emit foundational tasks via the `newTasks` array (mark `reviewed: true`, type `research` or `discovery`).",
12341
+ "- Generate full BUILD HANDOFFs for each in `cycleHandoffs` \u2014 use `new-N` IDs to reference them.",
12342
+ "- User-submitted backlog tasks remain in `cycleHandoffs` as usual \u2014 do NOT drop them in favour of foundational tasks."
12343
+ ].join("\n");
12344
+ }
12045
12345
  function detectBoardFlags(tasks) {
12046
12346
  let hasBugTasks = false;
12047
12347
  let hasResearchTasks = false;
@@ -12320,7 +12620,8 @@ ${lines.join("\n")}`;
12320
12620
  preAssignedTasks: preAssignedTextLean,
12321
12621
  recentlyShippedCapabilities: recentlyShippedLean,
12322
12622
  strategyReviewCadence,
12323
- candidateTaskFullNotes: candidateTaskFullNotesLean
12623
+ candidateTaskFullNotes: candidateTaskFullNotesLean,
12624
+ foundationalTasksGuidance: computeFoundationalTasksGuidance(health.totalCycles, productBrief)
12324
12625
  };
12325
12626
  const { label: leanTierLabel } = applyContextTier(ctx2, health.totalCycles);
12326
12627
  ctx2.contextTier = leanTierLabel;
@@ -12482,7 +12783,8 @@ ${logLines}`);
12482
12783
  preAssignedTasks: preAssignedText,
12483
12784
  recentlyShippedCapabilities: formatRecentlyShippedCapabilities(reports),
12484
12785
  strategyReviewCadence: strategyReviewCadenceFull,
12485
- candidateTaskFullNotes: formatCandidateTaskFullNotes(plannerTasks)
12786
+ candidateTaskFullNotes: formatCandidateTaskFullNotes(plannerTasks),
12787
+ foundationalTasksGuidance: computeFoundationalTasksGuidance(health.totalCycles, productBrief)
12486
12788
  };
12487
12789
  const { label: fullTierLabel } = applyContextTier(ctx, health.totalCycles);
12488
12790
  ctx.contextTier = fullTierLabel;
@@ -13863,6 +14165,7 @@ async function assembleContext2(adapter2, cycleNumber, cyclesSinceLastReview, pr
13863
14165
  // Doc registry — docs with pending actions for staleness audit
13864
14166
  adapter2.searchDocs?.({ hasPendingActions: true, limit: 20 })?.catch(() => []) ?? Promise.resolve([])
13865
14167
  ]);
14168
+ const pendingAgendaTopics = await (adapter2.getPendingAgendaTopics?.().catch(() => []) ?? Promise.resolve([]));
13866
14169
  const tasks = [...activeTasks, ...recentDoneTasks];
13867
14170
  const existingAdIds = new Set(decisions.map((d) => d.id));
13868
14171
  const survivingPendingRecs = [];
@@ -14100,6 +14403,15 @@ ${deferred.join("\n")}`);
14100
14403
  }
14101
14404
  }
14102
14405
  } catch {
14406
+ }
14407
+ let pendingAgendaText;
14408
+ if (pendingAgendaTopics.length > 0) {
14409
+ const lines = pendingAgendaTopics.map((t, i) => {
14410
+ const cycleSuffix = t.sourceCycle != null ? ` (queued Cycle ${t.sourceCycle})` : "";
14411
+ return `${i + 1}. ${t.topic} _[${t.source}${cycleSuffix}]_`;
14412
+ });
14413
+ pendingAgendaText = `${pendingAgendaTopics.length} topic(s) queued via strategy_agenda:
14414
+ ${lines.join("\n")}`;
14103
14415
  }
14104
14416
  logDataSourceSummary("strategy_review_audit", [
14105
14417
  { label: "discoveryCanvas", hasData: discoveryCanvasText !== void 0 },
@@ -14140,7 +14452,8 @@ ${deferred.join("\n")}`);
14140
14452
  recentPlans: recentPlansText,
14141
14453
  unregisteredDocs: unregisteredDocsText,
14142
14454
  taskComments: taskCommentsText,
14143
- docActionStaleness: docActionStalenessText
14455
+ docActionStaleness: docActionStalenessText,
14456
+ pendingAgendaTopics: pendingAgendaText
14144
14457
  };
14145
14458
  const BUDGET_SOFT2 = 5e4;
14146
14459
  const BUDGET_HARD2 = 6e4;
@@ -14455,6 +14768,15 @@ async function processReviewOutput(adapter2, rawOutput, cycleNumber) {
14455
14768
  await adapter2.clearPendingReviewResponse?.();
14456
14769
  } catch {
14457
14770
  }
14771
+ try {
14772
+ if (adapter2.getPendingAgendaTopics && adapter2.markAgendaTopicsAddressed) {
14773
+ const pending = await adapter2.getPendingAgendaTopics();
14774
+ if (pending.length > 0) {
14775
+ await adapter2.markAgendaTopicsAddressed(pending.map((t) => t.id), cycleNumber);
14776
+ }
14777
+ }
14778
+ } catch {
14779
+ }
14458
14780
  const webhookUrl = process.env.PAPI_SLACK_WEBHOOK_URL;
14459
14781
  slackWarning = await sendSlackWebhook(webhookUrl, buildSlackSummary(data));
14460
14782
  }
@@ -14975,6 +15297,34 @@ var strategyReviewTool = {
14975
15297
  required: []
14976
15298
  }
14977
15299
  };
15300
+ var strategyAgendaTool = {
15301
+ name: "strategy_agenda",
15302
+ description: 'Queue topics for the next strategy review. Topics surface as input in the next `strategy_review` prepare phase and are automatically marked as addressed after the review completes. Two modes: "add" to queue a topic, "list" to see pending topics. Use this when you spot a strategic question during a build \u2014 capture the topic now instead of losing it.',
15303
+ annotations: { readOnlyHint: false, destructiveHint: false },
15304
+ inputSchema: {
15305
+ type: "object",
15306
+ properties: {
15307
+ mode: {
15308
+ type: "string",
15309
+ enum: ["add", "list"],
15310
+ description: '"add" to queue a topic (requires `topic`). "list" returns all pending topics. Defaults to "list" when omitted.'
15311
+ },
15312
+ topic: {
15313
+ type: "string",
15314
+ description: 'The topic to queue (mode "add" only). One sentence describing what the next strategy review should consider.'
15315
+ },
15316
+ source: {
15317
+ type: "string",
15318
+ description: 'Optional origin label \u2014 e.g. "manual", "carry-forward", "idea". Defaults to "manual".'
15319
+ },
15320
+ source_cycle: {
15321
+ type: "number",
15322
+ description: 'Optional cycle number this topic originated from (mode "add" only).'
15323
+ }
15324
+ },
15325
+ required: []
15326
+ }
15327
+ };
14978
15328
  var strategyChangeTool = {
14979
15329
  name: "strategy_change",
14980
15330
  description: 'Apply a strategic shift to the project. Three modes: "capture" for lightweight mid-conversation decision capture (no LLM round-trip), "prepare" to get a change prompt for full analysis, "apply" to persist analysis output. Use "capture" when you detect a strategic decision in conversation and want to persist it quickly without disrupting the build flow.',
@@ -15131,6 +15481,49 @@ ${result.userMessage}
15131
15481
  return errorResponse(err instanceof Error ? err.message : String(err));
15132
15482
  }
15133
15483
  }
15484
+ async function handleStrategyAgenda(adapter2, _config, args) {
15485
+ const mode = args.mode ?? "list";
15486
+ if (!adapter2.addAgendaTopic || !adapter2.getPendingAgendaTopics) {
15487
+ return errorResponse("strategy_agenda is not supported by the current adapter.");
15488
+ }
15489
+ try {
15490
+ if (mode === "add") {
15491
+ const topic = args.topic;
15492
+ if (!topic || !topic.trim()) {
15493
+ return errorResponse('topic is required for mode "add". Describe what the next strategy review should consider.');
15494
+ }
15495
+ const source = (args.source ?? "manual").trim() || "manual";
15496
+ const sourceCycle = typeof args.source_cycle === "number" ? args.source_cycle : void 0;
15497
+ const entry = await adapter2.addAgendaTopic({ topic: topic.trim(), source, sourceCycle });
15498
+ return textResponse(
15499
+ `**Agenda Topic Queued**
15500
+
15501
+ ${entry.topic}
15502
+
15503
+ Source: ${entry.source}${entry.sourceCycle != null ? ` (Cycle ${entry.sourceCycle})` : ""}
15504
+ ID: ${entry.id}
15505
+
15506
+ This topic will surface in the next \`strategy_review\`.`
15507
+ );
15508
+ }
15509
+ const topics = await adapter2.getPendingAgendaTopics();
15510
+ if (topics.length === 0) {
15511
+ return textResponse('No pending agenda topics. Use `strategy_agenda` with `mode: "add"` to queue one.');
15512
+ }
15513
+ const lines = topics.map((t, i) => {
15514
+ const cycleSuffix = t.sourceCycle != null ? ` (Cycle ${t.sourceCycle})` : "";
15515
+ return `${i + 1}. ${t.topic}
15516
+ _source: ${t.source}${cycleSuffix} \xB7 queued ${t.createdAt.slice(0, 10)}_`;
15517
+ });
15518
+ return textResponse(
15519
+ `**Pending Agenda (${topics.length})** \u2014 surfaces at next strategy review
15520
+
15521
+ ${lines.join("\n\n")}`
15522
+ );
15523
+ } catch (err) {
15524
+ return errorResponse(err instanceof Error ? err.message : String(err));
15525
+ }
15526
+ }
15134
15527
  async function handleStrategyChange(adapter2, _config, args) {
15135
15528
  const toolMode = args.mode;
15136
15529
  try {
@@ -15402,7 +15795,7 @@ var boardArchiveTool = {
15402
15795
  };
15403
15796
  var boardEditTool = {
15404
15797
  name: "board_edit",
15405
- description: "Edit fields on an existing task. Supports title, priority, complexity, module, epic, phase, notes, status, and maturity. Pass task_id plus any fields to update. Does not call the Anthropic API.",
15798
+ description: "Edit fields on an existing task. Supports title, priority, complexity, module, epic, phase, notes (with notes_mode for append/replace/clear), status, and maturity. Pass task_id plus any fields to update. Does not call the Anthropic API.",
15406
15799
  annotations: { readOnlyHint: false, destructiveHint: false },
15407
15800
  inputSchema: {
15408
15801
  type: "object",
@@ -15439,7 +15832,12 @@ var boardEditTool = {
15439
15832
  },
15440
15833
  notes: {
15441
15834
  type: "string",
15442
- description: "New notes (replaces existing notes)."
15835
+ description: "Note content. Default behaviour is append \u2014 see notes_mode to control."
15836
+ },
15837
+ notes_mode: {
15838
+ type: "string",
15839
+ enum: ["append", "replace", "clear"],
15840
+ description: "How to apply the notes value. append (default) = add a new dated entry above existing notes; replace = overwrite all existing notes; clear = empty the notes field (notes value ignored)."
15443
15841
  },
15444
15842
  status: {
15445
15843
  type: "string",
@@ -15642,6 +16040,10 @@ async function handleBoardEdit(adapter2, args) {
15642
16040
  changes.push(field);
15643
16041
  }
15644
16042
  }
16043
+ const notesMode = args.notes_mode;
16044
+ if (notesMode === "clear" && !changes.includes("notes")) {
16045
+ changes.push("notes");
16046
+ }
15645
16047
  if (changes.length === 0) {
15646
16048
  return errorResponse("No fields to update. Pass at least one field (title, priority, complexity, module, epic, phase, notes, status, maturity).");
15647
16049
  }
@@ -15650,6 +16052,35 @@ async function handleBoardEdit(adapter2, args) {
15650
16052
  if (!task) {
15651
16053
  return errorResponse(`Task ${taskId} not found.`);
15652
16054
  }
16055
+ if (changes.includes("notes")) {
16056
+ const incoming = args.notes ?? "";
16057
+ const existing = task.notes ?? "";
16058
+ const mode = notesMode ?? "append";
16059
+ if (mode === "clear") {
16060
+ updates.notes = "";
16061
+ } else if (mode === "replace") {
16062
+ updates.notes = incoming;
16063
+ } else {
16064
+ const trimmed = incoming.trim();
16065
+ if (trimmed.length === 0) {
16066
+ delete updates.notes;
16067
+ const idx = changes.indexOf("notes");
16068
+ if (idx >= 0) changes.splice(idx, 1);
16069
+ } else {
16070
+ const health = await adapter2.getCycleHealth().catch(() => null);
16071
+ const activeCycle = health?.totalCycles ?? null;
16072
+ const date = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
16073
+ const stamp = activeCycle != null ? `[C${activeCycle} ${date}]` : `[${date}]`;
16074
+ const entry = `${stamp} ${trimmed}`;
16075
+ updates.notes = existing.trim().length > 0 ? `${entry}
16076
+
16077
+ ${existing}` : entry;
16078
+ }
16079
+ }
16080
+ if (changes.length === 0) {
16081
+ return errorResponse("No fields to update. Pass at least one field (title, priority, complexity, module, epic, phase, notes, status, maturity).");
16082
+ }
16083
+ }
15653
16084
  if (updates.status === "Backlog" && task.cycle != null) {
15654
16085
  updates.cycle = void 0;
15655
16086
  updates.cycle = null;
@@ -16671,9 +17102,8 @@ async function prepareSetup(adapter2, config2, input) {
16671
17102
  );
16672
17103
  }
16673
17104
  const TEMPLATE_MARKER = "*Describe your project's core value proposition here.*";
16674
- if (existingBrief.trim() && !existingBrief.includes(TEMPLATE_MARKER) && !input.force) {
16675
- throw new Error("PRODUCT_BRIEF.md already contains a generated Product Brief. Running setup again would overwrite it.\n\nTo proceed anyway, run setup with force: true.");
16676
- }
17105
+ const briefHasRealContent = existingBrief.trim().length > 0 && !existingBrief.includes(TEMPLATE_MARKER);
17106
+ const briefAlreadyExists = briefHasRealContent && !input.force;
16677
17107
  const detectedCodebaseType = detectCodebaseType(config2.projectRoot);
16678
17108
  const autoDetected = input.existingProject === void 0 || input.existingProject === false;
16679
17109
  const isExistingProject = input.existingProject === true || autoDetected && detectedCodebaseType === "existing_codebase";
@@ -16686,7 +17116,7 @@ async function prepareSetup(adapter2, config2, input) {
16686
17116
  }
16687
17117
  codebaseSummary = formatCodebaseSummary(scan, sourceContents);
16688
17118
  }
16689
- const briefPrompt = {
17119
+ const briefPrompt = briefAlreadyExists ? void 0 : {
16690
17120
  system: PRODUCT_BRIEF_SYSTEM,
16691
17121
  user: buildProductBriefPrompt({
16692
17122
  projectName: input.projectName,
@@ -16740,15 +17170,29 @@ async function prepareSetup(adapter2, config2, input) {
16740
17170
  initialTasksPrompt,
16741
17171
  codebaseSummary,
16742
17172
  detectedCodebaseType,
16743
- autoDetected: autoDetected && detectedCodebaseType !== "new_project"
17173
+ autoDetected: autoDetected && detectedCodebaseType !== "new_project",
17174
+ briefAlreadyExists
16744
17175
  };
16745
17176
  }
16746
17177
  async function applySetup(adapter2, config2, input, briefText, adSeedText, conventionsText, initialTasksText) {
16747
17178
  const createdProject = await scaffoldPapiDir(adapter2, config2, input);
16748
- if (!briefText.trim()) {
16749
- throw new Error("brief_response is required and cannot be empty.");
17179
+ const TEMPLATE_MARKER = "*Describe your project's core value proposition here.*";
17180
+ let effectiveBriefText = briefText;
17181
+ if (!effectiveBriefText.trim()) {
17182
+ let existingBrief = "";
17183
+ try {
17184
+ existingBrief = await adapter2.readProductBrief();
17185
+ } catch {
17186
+ existingBrief = "";
17187
+ }
17188
+ const existingIsReal = existingBrief.trim().length > 0 && !existingBrief.includes(TEMPLATE_MARKER);
17189
+ if (existingIsReal && !input.force) {
17190
+ effectiveBriefText = existingBrief;
17191
+ } else {
17192
+ throw new Error("brief_response is required and cannot be empty.");
17193
+ }
16750
17194
  }
16751
- const { seededAds, warnings } = await applySetupOutputs(adapter2, config2, input, briefText, adSeedText, conventionsText);
17195
+ const { seededAds, warnings } = await applySetupOutputs(adapter2, config2, input, effectiveBriefText, adSeedText, conventionsText);
16752
17196
  let createdTasks = 0;
16753
17197
  if (initialTasksText?.trim()) {
16754
17198
  try {
@@ -16974,10 +17418,7 @@ PAPI needs just 3 things: project name, what it does, and who it's for.`
16974
17418
  const input = extractInput(args);
16975
17419
  try {
16976
17420
  if (toolMode === "apply") {
16977
- const briefResponse = args.brief_response;
16978
- if (!briefResponse || !briefResponse.trim()) {
16979
- return errorResponse('brief_response is required for mode "apply".');
16980
- }
17421
+ const briefResponse = args.brief_response ?? "";
16981
17422
  const adSeedResponse = args.ad_seed_response;
16982
17423
  const conventionsResponse = args.conventions_response;
16983
17424
  const initialTasksResponse = args.initial_tasks_response;
@@ -17010,6 +17451,12 @@ PAPI needs just 3 things: project name, what it does, and who it's for.`
17010
17451
  ""
17011
17452
  );
17012
17453
  }
17454
+ if (result.briefAlreadyExists) {
17455
+ sections.push(
17456
+ `**Existing brief detected:** this project already has a Product Brief (e.g. from the web wizard). Setup will preserve it and skip brief generation \u2014 no \`brief_response\` needed in the apply step. To overwrite the brief instead, re-run setup with \`force: true\`.`,
17457
+ ""
17458
+ );
17459
+ }
17013
17460
  if (inferredDefaults.length > 0) {
17014
17461
  sections.push(
17015
17462
  `**Defaults applied** (override by re-running setup with these fields):`,
@@ -17020,30 +17467,37 @@ PAPI needs just 3 things: project name, what it does, and who it's for.`
17020
17467
  sections.push(
17021
17468
  `Generate the outputs below, then call \`setup\` again with:`,
17022
17469
  `- \`mode\`: "apply"`,
17023
- `- \`brief_response\`: your Product Brief markdown`,
17470
+ result.briefPrompt ? `- \`brief_response\`: your Product Brief markdown` : "",
17024
17471
  result.adSeedPrompt ? `- \`ad_seed_response\`: your AD seed JSON array` : "",
17025
17472
  result.conventionsPrompt ? `- \`conventions_response\`: your conventions markdown` : "",
17026
17473
  result.initialTasksPrompt ? `- \`initial_tasks_response\`: your initial tasks JSON array` : "",
17027
17474
  `- Plus all the original setup fields (project_name, description, target_users${isExisting ? ", existing_project: true" : ""})`,
17028
17475
  "",
17029
- `---`,
17030
- "",
17031
- `### 1. Product Brief`,
17032
- "",
17033
- `<system_prompt>
17476
+ `---`
17477
+ );
17478
+ let sectionNum = 0;
17479
+ if (result.briefPrompt) {
17480
+ sectionNum++;
17481
+ sections.push(
17482
+ "",
17483
+ `### ${sectionNum}. Product Brief`,
17484
+ "",
17485
+ `<system_prompt>
17034
17486
  ${result.briefPrompt.system}
17035
17487
  </system_prompt>`,
17036
- "",
17037
- `<context>
17488
+ "",
17489
+ `<context>
17038
17490
  ${result.briefPrompt.user}
17039
17491
  </context>`
17040
- );
17492
+ );
17493
+ }
17041
17494
  if (result.adSeedPrompt) {
17495
+ sectionNum++;
17042
17496
  sections.push(
17043
17497
  "",
17044
17498
  `---`,
17045
17499
  "",
17046
- `### 2. Active Decision Seeds`,
17500
+ `### ${sectionNum}. Active Decision Seeds`,
17047
17501
  "",
17048
17502
  `<system_prompt>
17049
17503
  ${result.adSeedPrompt.system}
@@ -17055,11 +17509,12 @@ ${result.adSeedPrompt.user}
17055
17509
  );
17056
17510
  }
17057
17511
  if (result.conventionsPrompt) {
17512
+ sectionNum++;
17058
17513
  sections.push(
17059
17514
  "",
17060
17515
  `---`,
17061
17516
  "",
17062
- `### 3. Conventions`,
17517
+ `### ${sectionNum}. Conventions`,
17063
17518
  "",
17064
17519
  `<system_prompt>
17065
17520
  ${result.conventionsPrompt.system}
@@ -17071,11 +17526,12 @@ ${result.conventionsPrompt.user}
17071
17526
  );
17072
17527
  }
17073
17528
  if (result.initialTasksPrompt) {
17529
+ sectionNum++;
17074
17530
  sections.push(
17075
17531
  "",
17076
17532
  `---`,
17077
17533
  "",
17078
- `### 4. Initial Backlog Tasks`,
17534
+ `### ${sectionNum}. Initial Backlog Tasks`,
17079
17535
  "",
17080
17536
  `<system_prompt>
17081
17537
  ${result.initialTasksPrompt.system}
@@ -17487,18 +17943,37 @@ async function completeBuild(adapter2, config2, taskId, input, options = {}) {
17487
17943
  }
17488
17944
  }
17489
17945
  let autoTriagedCount = 0;
17946
+ const autoTriagedIds = [];
17947
+ const autoTriagedDupes = [];
17490
17948
  if (input.discoveredIssues && input.discoveredIssues !== "None" && typeof adapter2.createTask === "function") {
17491
17949
  const issueLines = input.discoveredIssues.split(/\n|;/).map((s) => s.trim()).filter((s) => s.length > 0);
17950
+ const backlogTitleMap = /* @__PURE__ */ new Map();
17951
+ try {
17952
+ const backlog = await adapter2.queryBoard({ status: ["Backlog"] });
17953
+ for (const t of backlog) {
17954
+ const normalized = t.title.replace(/^\[Auto-triaged\]\s*/i, "").trim().toLowerCase();
17955
+ if (normalized && !backlogTitleMap.has(normalized)) {
17956
+ backlogTitleMap.set(normalized, t.displayId);
17957
+ }
17958
+ }
17959
+ } catch {
17960
+ }
17492
17961
  for (const line of issueLines) {
17493
17962
  const sevMatch = line.match(/^(P[0-3])[\s:]+/i);
17494
17963
  if (!sevMatch) continue;
17495
17964
  const severityLabel = sevMatch[1].toUpperCase();
17496
- const priority = severityLabel === "P0" || severityLabel === "P1" ? "P1 High" : severityLabel === "P2" ? "P2 Medium" : "P3 Low";
17965
+ const priority = severityLabel === "P0" ? "P0 Critical" : severityLabel === "P1" ? "P1 High" : severityLabel === "P2" ? "P2 Medium" : "P3 Low";
17497
17966
  const titleRaw = line.replace(/^P[0-3][\s:]+/i, "").trim();
17498
17967
  const title = titleRaw.length > 120 ? titleRaw.slice(0, 120) : titleRaw;
17499
17968
  if (!title) continue;
17969
+ const normalized = title.toLowerCase();
17970
+ const dupId = backlogTitleMap.get(normalized);
17971
+ if (dupId) {
17972
+ autoTriagedDupes.push(dupId);
17973
+ continue;
17974
+ }
17500
17975
  try {
17501
- await adapter2.createTask({
17976
+ const created = await adapter2.createTask({
17502
17977
  uuid: "",
17503
17978
  displayId: "",
17504
17979
  title: `[Auto-triaged] ${title}`,
@@ -17515,6 +17990,10 @@ async function completeBuild(adapter2, config2, taskId, input, options = {}) {
17515
17990
  createdCycle: cycleNumber
17516
17991
  });
17517
17992
  autoTriagedCount++;
17993
+ if (created?.displayId) {
17994
+ autoTriagedIds.push(created.displayId);
17995
+ backlogTitleMap.set(normalized, created.displayId);
17996
+ }
17518
17997
  } catch {
17519
17998
  }
17520
17999
  }
@@ -17711,6 +18190,8 @@ async function completeBuild(adapter2, config2, taskId, input, options = {}) {
17711
18190
  dogfoodResolvedCount: dogfoodResolvedCount > 0 ? dogfoodResolvedCount : void 0,
17712
18191
  learningsLinkedCount: learningsLinkedCount > 0 ? learningsLinkedCount : void 0,
17713
18192
  autoTriagedCount: autoTriagedCount > 0 ? autoTriagedCount : void 0,
18193
+ autoTriagedIds: autoTriagedIds.length > 0 ? autoTriagedIds : void 0,
18194
+ autoTriagedDupes: autoTriagedDupes.length > 0 ? autoTriagedDupes : void 0,
17714
18195
  reportWriteVerified
17715
18196
  };
17716
18197
  }
@@ -18214,8 +18695,16 @@ function formatCompleteResult(result) {
18214
18695
  if (result.learningsLinkedCount) {
18215
18696
  lines.push("", `Linked ${result.learningsLinkedCount} unactioned learning(s) to this task.`);
18216
18697
  }
18217
- if (result.autoTriagedCount) {
18218
- lines.push("", `\u{1F516} Auto-triaged ${result.autoTriagedCount} discovered issue(s) to Backlog.`);
18698
+ if (result.autoTriagedCount || result.autoTriagedDupes?.length) {
18699
+ const parts = [];
18700
+ if (result.autoTriagedCount) {
18701
+ const ids = result.autoTriagedIds?.length ? ` (${result.autoTriagedIds.join(", ")})` : "";
18702
+ parts.push(`\u{1F516} Auto-triaged ${result.autoTriagedCount} discovered issue(s) to Backlog${ids}.`);
18703
+ }
18704
+ if (result.autoTriagedDupes?.length) {
18705
+ parts.push(`Skipped ${result.autoTriagedDupes.length} duplicate issue(s) already in Backlog: ${result.autoTriagedDupes.join(", ")}.`);
18706
+ }
18707
+ lines.push("", parts.join(" "));
18219
18708
  }
18220
18709
  if (result.reportWriteVerified === false) {
18221
18710
  lines.push("", "\u26A0\uFE0F Build report write could not be verified \u2014 the report may not have been persisted. Run `build_list` to check, and resubmit if missing.");
@@ -19830,7 +20319,41 @@ function generateChangelog(version, commits) {
19830
20319
  ${commitList}
19831
20320
  `;
19832
20321
  }
19833
- async function createRelease(config2, branch, version, adapter2) {
20322
+ function mergeGroupedCycleBranches(config2, cycleNum, baseBranch) {
20323
+ const branches = listGroupedCycleBranches(config2.projectRoot, cycleNum, baseBranch);
20324
+ if (branches.length === 0) return [];
20325
+ if (!isGhAvailable()) {
20326
+ throw new Error(
20327
+ `Release blocked: ${branches.length} unmerged grouped cycle branch(es) detected (${branches.join(", ")}) but \`gh\` CLI is not available. Install gh and re-run release.`
20328
+ );
20329
+ }
20330
+ const results = [];
20331
+ for (const branch of branches) {
20332
+ let prUrl = getPullRequestUrl(config2.projectRoot, branch);
20333
+ if (!prUrl) {
20334
+ const moduleName = branch.replace(`feat/cycle-${cycleNum}-`, "");
20335
+ const prCreate = createPullRequest(
20336
+ config2.projectRoot,
20337
+ branch,
20338
+ baseBranch,
20339
+ `feat(cycle-${cycleNum}): merge shared cycle branch \u2014 ${moduleName}`,
20340
+ `Automated PR created at release time for shared cycle branch \`${branch}\` (Cycle ${cycleNum}).`
20341
+ );
20342
+ if (!prCreate.success) {
20343
+ results.push({ branch, prUrl: null, status: "failed", message: prCreate.message });
20344
+ continue;
20345
+ }
20346
+ prUrl = prCreate.message;
20347
+ }
20348
+ const merge = squashMergePullRequest(config2.projectRoot, branch);
20349
+ if (merge.success) {
20350
+ deleteLocalBranch(config2.projectRoot, branch);
20351
+ }
20352
+ results.push({ branch, prUrl, status: merge.success ? "merged" : "failed", message: merge.message });
20353
+ }
20354
+ return results;
20355
+ }
20356
+ async function createRelease(config2, branch, version, adapter2, cycleNum) {
19834
20357
  if (!isGitAvailable()) {
19835
20358
  throw new Error("git is not available.");
19836
20359
  }
@@ -19841,10 +20364,13 @@ async function createRelease(config2, branch, version, adapter2) {
19841
20364
  throw new Error("working directory has uncommitted changes. Commit or stash them before releasing.");
19842
20365
  }
19843
20366
  const warnings = [];
20367
+ const resolvedCycleNum = cycleNum && cycleNum > 0 ? cycleNum : (() => {
20368
+ const m = version.match(/^v0\.(\d+)\./);
20369
+ return m ? parseInt(m[1], 10) : 0;
20370
+ })();
19844
20371
  if (adapter2) {
19845
20372
  try {
19846
- const versionMatch = version.match(/^v0\.(\d+)\./);
19847
- const currentCycle = versionMatch ? parseInt(versionMatch[1], 10) : 0;
20373
+ const currentCycle = resolvedCycleNum;
19848
20374
  if (currentCycle > 0) {
19849
20375
  await adapter2.createCycle({
19850
20376
  id: `cycle-${currentCycle}`,
@@ -19873,6 +20399,24 @@ async function createRelease(config2, branch, version, adapter2) {
19873
20399
  warnings.push(`git pull failed: ${pull.message}. Run manually.`);
19874
20400
  }
19875
20401
  }
20402
+ let groupedBranchMerges;
20403
+ if (resolvedCycleNum > 0) {
20404
+ const mergeResults = mergeGroupedCycleBranches(config2, resolvedCycleNum, branch);
20405
+ if (mergeResults.length > 0) {
20406
+ groupedBranchMerges = mergeResults;
20407
+ const failures = mergeResults.filter((r) => r.status === "failed");
20408
+ if (failures.length > 0) {
20409
+ const detail = failures.map((r) => `${r.branch}: ${r.message}`).join("; ");
20410
+ throw new Error(`Release blocked: grouped cycle branch merge failed \u2014 ${detail}. Resolve conflicts and retry release.`);
20411
+ }
20412
+ if (hasRemote(config2.projectRoot)) {
20413
+ const repull = gitPull(config2.projectRoot);
20414
+ if (!repull.success) {
20415
+ warnings.push(`Post-merge pull failed: ${repull.message}. Run manually.`);
20416
+ }
20417
+ }
20418
+ }
20419
+ }
19876
20420
  if (tagExists(config2.projectRoot, version)) {
19877
20421
  throw new Error(`tag "${version}" already exists. Use a different version.`);
19878
20422
  }
@@ -19909,7 +20453,8 @@ async function createRelease(config2, branch, version, adapter2) {
19909
20453
  commitNote,
19910
20454
  tagMessage: tagResult.message,
19911
20455
  pushNotes,
19912
- warnings: warnings.length > 0 ? warnings : void 0
20456
+ warnings: warnings.length > 0 ? warnings : void 0,
20457
+ ...groupedBranchMerges ? { groupedBranchMerges } : {}
19913
20458
  };
19914
20459
  }
19915
20460
 
@@ -19965,7 +20510,9 @@ async function handleRelease(adapter2, config2, args) {
19965
20510
  return errorResponse(`version must start with "v" (got "${version}"). Example: "v0.1.0-alpha"`);
19966
20511
  }
19967
20512
  try {
19968
- const result = await createRelease(config2, branch, version, adapter2);
20513
+ const cycleMatch = version.match(/^v0\.(\d+)\./);
20514
+ const cycleNum = cycleMatch ? parseInt(cycleMatch[1], 10) : void 0;
20515
+ const result = await createRelease(config2, branch, version, adapter2, cycleNum);
19969
20516
  const lines = [
19970
20517
  `## Release ${result.version}`,
19971
20518
  "",
@@ -19978,13 +20525,17 @@ async function handleRelease(adapter2, config2, args) {
19978
20525
  lines.push("", "---", "");
19979
20526
  lines.push(...result.pushNotes);
19980
20527
  }
20528
+ if (result.groupedBranchMerges?.length) {
20529
+ lines.push("", "**Shared cycle branches merged:**");
20530
+ for (const m of result.groupedBranchMerges) {
20531
+ lines.push(`- \`${m.branch}\` \u2014 ${m.message}${m.prUrl ? ` (${m.prUrl})` : ""}`);
20532
+ }
20533
+ }
19981
20534
  if (result.warnings?.length) {
19982
20535
  lines.push("", "\u26A0\uFE0F Warnings: " + result.warnings.join("; "));
19983
20536
  }
19984
20537
  try {
19985
- const cycleMatch = version.match(/^v0\.(\d+)\./);
19986
- const cycleNum = cycleMatch ? parseInt(cycleMatch[1], 10) : 0;
19987
- if (cycleNum > 0) {
20538
+ if (cycleNum && cycleNum > 0) {
19988
20539
  const reports = await adapter2.getBuildReportsSince(cycleNum);
19989
20540
  const EMPTY = /* @__PURE__ */ new Set(["None", "none", "N/A", "", "null"]);
19990
20541
  const issues = reports.filter((r) => r.discoveredIssues && !EMPTY.has(r.discoveredIssues.trim())).map((r) => `- **${r.taskId}** (${r.taskName}): ${r.discoveredIssues}`);
@@ -19997,10 +20548,10 @@ async function handleRelease(adapter2, config2, args) {
19997
20548
  }
19998
20549
  if (rawObservations && rawObservations.length > 0 && adapter2.writeDogfoodEntries) {
19999
20550
  try {
20000
- const cycleMatch = version.match(/^v0\.(\d+)\./);
20001
- const cycleNum = cycleMatch ? parseInt(cycleMatch[1], 10) : 0;
20551
+ const cycleMatch2 = version.match(/^v0\.(\d+)\./);
20552
+ const cycleNum2 = cycleMatch2 ? parseInt(cycleMatch2[1], 10) : 0;
20002
20553
  const entries = rawObservations.map((obs) => ({
20003
- cycleNumber: cycleNum,
20554
+ cycleNumber: cycleNum2,
20004
20555
  category: obs.category,
20005
20556
  content: obs.content,
20006
20557
  sourceTool: "release",
@@ -20256,6 +20807,10 @@ function mergeAfterAccept(config2, taskId) {
20256
20807
  }
20257
20808
  const featureBranch = taskBranchName(taskId);
20258
20809
  const baseBranch = resolveBaseBranch(config2.projectRoot, config2.baseBranch);
20810
+ if (!branchExists(config2.projectRoot, featureBranch)) {
20811
+ lines.push(`Task is on a shared cycle branch \u2014 will be merged at release time.`);
20812
+ return lines;
20813
+ }
20259
20814
  const papiDir = join7(config2.projectRoot, ".papi");
20260
20815
  if (existsSync4(papiDir)) {
20261
20816
  try {
@@ -20431,8 +20986,9 @@ ${result.handoffRegenPrompt.userMessage}
20431
20986
  }
20432
20987
  const version = `v0.${result.currentCycle}.0`;
20433
20988
  const baseBranch = resolveBaseBranch(config2.projectRoot, config2.baseBranch);
20434
- const releaseResult = await createRelease(config2, baseBranch, version, adapter2);
20989
+ const releaseResult = await createRelease(config2, baseBranch, version, adapter2, result.currentCycle);
20435
20990
  const pushInfo = releaseResult.pushNotes.join(" ");
20991
+ const groupedMergeNote = releaseResult.groupedBranchMerges?.length ? "\n" + releaseResult.groupedBranchMerges.map((r) => `- Merged shared branch \`${r.branch}\` via PR: ${r.prUrl ?? "n/a"}`).join("\n") : "";
20436
20992
  autoReleaseNote = `
20437
20993
 
20438
20994
  ---
@@ -20442,7 +20998,7 @@ ${result.handoffRegenPrompt.userMessage}
20442
20998
  - Version: **${releaseResult.version}**
20443
20999
  - ${releaseResult.commitNote}
20444
21000
  - ${releaseResult.tagMessage}
20445
- - ${pushInfo}` + (releaseResult.warnings?.length ? `
21001
+ - ${pushInfo}` + groupedMergeNote + (releaseResult.warnings?.length ? `
20446
21002
  - Warnings: ${releaseResult.warnings.join(", ")}` : "") + `
20447
21003
 
20448
21004
  Run \`plan\` to create Cycle ${result.currentCycle + 1}.`;
@@ -20689,7 +21245,7 @@ function countByStatus(tasks) {
20689
21245
  async function getHealthSummary(adapter2) {
20690
21246
  const health = await adapter2.getCycleHealth();
20691
21247
  const activeTasks = await adapter2.queryBoard({
20692
- status: ["Backlog", "In Cycle", "Ready", "In Progress", "In Review", "Blocked"]
21248
+ status: ["Backlog", "In Cycle", "Ready", "In Progress", "In Review", "Blocked", "Deferred"]
20693
21249
  });
20694
21250
  const logEntries = await adapter2.getCycleLog(3);
20695
21251
  const cycleNumber = health.totalCycles;
@@ -22488,16 +23044,19 @@ function emitToolCall(projectId, toolName, durationMs, extra) {
22488
23044
  metadata: { duration_ms: durationMs, ...extra }
22489
23045
  });
22490
23046
  }
22491
- function emitMdAdapterPing(toolName, extra) {
23047
+ function emitMdAdapterPing(toolName, extra, userId, projectSlug) {
22492
23048
  if (!isEnabled()) return;
22493
23049
  const installId = getInstallId();
22494
23050
  if (!installId) return;
23051
+ const resolvedUserId = userId ?? process.env["PAPI_USER_ID"] ?? void 0;
22495
23052
  const body = {
22496
23053
  install_id: installId,
22497
23054
  tool_name: toolName,
22498
23055
  papi_version: process.env["npm_package_version"] ?? null,
22499
23056
  metadata: extra ?? {}
22500
23057
  };
23058
+ if (resolvedUserId) body["user_id"] = resolvedUserId;
23059
+ if (projectSlug) body["project_slug"] = projectSlug;
22501
23060
  fetch(`${TELEMETRY_SUPABASE_URL}/rest/v1/md_adapter_pings`, {
22502
23061
  method: "POST",
22503
23062
  headers: {
@@ -22525,6 +23084,7 @@ var TOOLS_REQUIRING_PAPI = /* @__PURE__ */ new Set([
22525
23084
  "plan",
22526
23085
  "strategy_review",
22527
23086
  "strategy_change",
23087
+ "strategy_agenda",
22528
23088
  "board_view",
22529
23089
  "board_deprioritise",
22530
23090
  "board_archive",
@@ -22617,6 +23177,7 @@ function createServer(adapter2, config2) {
22617
23177
  planTool,
22618
23178
  strategyReviewTool,
22619
23179
  strategyChangeTool,
23180
+ strategyAgendaTool,
22620
23181
  boardViewTool,
22621
23182
  boardDeprioritiseTool,
22622
23183
  boardArchiveTool,
@@ -22683,6 +23244,9 @@ function createServer(adapter2, config2) {
22683
23244
  case "strategy_change":
22684
23245
  result = await handleStrategyChange(adapter2, config2, safeArgs);
22685
23246
  break;
23247
+ case "strategy_agenda":
23248
+ result = await handleStrategyAgenda(adapter2, config2, safeArgs);
23249
+ break;
22686
23250
  case "board_view":
22687
23251
  result = await handleBoardView(adapter2, safeArgs);
22688
23252
  break;
@@ -22777,7 +23341,8 @@ function createServer(adapter2, config2) {
22777
23341
  } catch {
22778
23342
  }
22779
23343
  if (config2.adapterType === "md") {
22780
- emitMdAdapterPing(name, { duration_ms: elapsed, success: !isError });
23344
+ const mdProjectSlug = config2.projectRoot ? config2.projectRoot.split("/").pop() : void 0;
23345
+ emitMdAdapterPing(name, { duration_ms: elapsed, success: !isError }, config2.userId, mdProjectSlug);
22781
23346
  }
22782
23347
  const telemetryProjectId = process.env["PAPI_PROJECT_ID"];
22783
23348
  if (telemetryProjectId) {
package/dist/prompts.js CHANGED
@@ -535,6 +535,9 @@ function buildPlanUserMessage(ctx) {
535
535
  }) : PLAN_FULL_INSTRUCTIONS;
536
536
  parts.push(instructions);
537
537
  }
538
+ if (ctx.foundationalTasksGuidance) {
539
+ parts.push("", ctx.foundationalTasksGuidance);
540
+ }
538
541
  if (ctx.skipHandoffs) {
539
542
  parts.push(
540
543
  "",
@@ -1056,6 +1059,9 @@ function buildReviewUserMessage(ctx) {
1056
1059
  if (ctx.docActionStaleness) {
1057
1060
  parts.push("### Doc Action Staleness", "", ctx.docActionStaleness, "");
1058
1061
  }
1062
+ if (ctx.pendingAgendaTopics) {
1063
+ parts.push("### Queued Agenda Topics", "", "_Topics queued via `strategy_agenda` since the last review. Address each one in this review \u2014 they will be auto-marked as addressed on apply._", "", ctx.pendingAgendaTopics, "");
1064
+ }
1059
1065
  return parts.join("\n");
1060
1066
  }
1061
1067
  function parseReviewStructuredOutput(raw) {
package/package.json CHANGED
@@ -1,7 +1,6 @@
1
1
  {
2
2
  "name": "@papi-ai/server",
3
- "version": "0.7.12",
4
- "mcpName": "io.github.cathalos92/papi",
3
+ "version": "0.7.13",
5
4
  "description": "PAPI MCP server — AI-powered sprint planning, build execution, and strategy review for software projects",
6
5
  "license": "Elastic-2.0",
7
6
  "type": "module",