@papi-ai/server 0.7.12 → 0.7.14

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
@@ -10,7 +10,12 @@ var __export = (target, all) => {
10
10
  };
11
11
 
12
12
  // ../shared/dist/index.js
13
- var VALID_TRANSITIONS;
13
+ function isLiveDecision(d) {
14
+ if (d.superseded === true) return false;
15
+ if (d.outcome != null && RETIRED_DECISION_OUTCOMES.includes(d.outcome)) return false;
16
+ return true;
17
+ }
18
+ var VALID_TRANSITIONS, RETIRED_DECISION_OUTCOMES;
14
19
  var init_dist = __esm({
15
20
  "../shared/dist/index.js"() {
16
21
  "use strict";
@@ -25,6 +30,7 @@ var init_dist = __esm({
25
30
  "Cancelled": [],
26
31
  "Deferred": ["Backlog", "Cancelled"]
27
32
  };
33
+ RETIRED_DECISION_OUTCOMES = ["resolved", "abandoned", "superseded"];
28
34
  }
29
35
  });
30
36
 
@@ -1441,12 +1447,13 @@ async function detectReviewPatterns(reviews, currentCycle, window = 5, clusterer
1441
1447
  function hasReviewPatterns(patterns) {
1442
1448
  return patterns.recurringFeedback.length > 0 || patterns.requestChangesRate >= 50;
1443
1449
  }
1444
- var VALID_TRANSITIONS2, TASK_TYPE_TIERS, VALID_EFFORT_SIZES, SECTION_HEADERS, YAML_MARKER, YAML_START, YAML_END, VALID_EFFORT_SIZES2, HEADER_SENTINEL, TABLE_HEADER, TABLE_SEPARATOR, PREV_TABLE_HEADER, LEGACY_TABLE_HEADER, SECTION_HEADING, FILE_TEMPLATE, COST_SECTION_HEADING, COST_TABLE_SEPARATOR, FILE_HEADING, ACCURACY_HEADER, ACCURACY_SEPARATOR, VELOCITY_HEADER, VELOCITY_SEPARATOR, EFFORT_SCALE, NONE_PATTERN, HEADER_SENTINEL2, VALID_STAGES, VALID_VERDICTS, STAGE_DISPLAY, VALID_STATUSES, PHASES_START, PHASES_END, YAML_MARKER2, YAML_START2, YAML_END2, VALID_STATUSES2, YAML_MARKER3, YAML_START3, YAML_END3, MdFileAdapter, NONE_PATTERN2;
1450
+ var VALID_TRANSITIONS2, isLiveDecision2, TASK_TYPE_TIERS, VALID_EFFORT_SIZES, SECTION_HEADERS, YAML_MARKER, YAML_START, YAML_END, VALID_EFFORT_SIZES2, HEADER_SENTINEL, TABLE_HEADER, TABLE_SEPARATOR, PREV_TABLE_HEADER, LEGACY_TABLE_HEADER, SECTION_HEADING, FILE_TEMPLATE, COST_SECTION_HEADING, COST_TABLE_SEPARATOR, FILE_HEADING, ACCURACY_HEADER, ACCURACY_SEPARATOR, VELOCITY_HEADER, VELOCITY_SEPARATOR, EFFORT_SCALE, NONE_PATTERN, HEADER_SENTINEL2, VALID_STAGES, VALID_VERDICTS, STAGE_DISPLAY, VALID_STATUSES, PHASES_START, PHASES_END, YAML_MARKER2, YAML_START2, YAML_END2, VALID_STATUSES2, YAML_MARKER3, YAML_START3, YAML_END3, MdFileAdapter, NONE_PATTERN2;
1445
1451
  var init_dist2 = __esm({
1446
1452
  "../adapter-md/dist/index.js"() {
1447
1453
  "use strict";
1448
1454
  init_dist();
1449
1455
  VALID_TRANSITIONS2 = VALID_TRANSITIONS;
1456
+ isLiveDecision2 = isLiveDecision;
1450
1457
  TASK_TYPE_TIERS = {
1451
1458
  bug: 1,
1452
1459
  task: 1,
@@ -1557,11 +1564,18 @@ ${TABLE_SEPARATOR}
1557
1564
  async getCycleHealth() {
1558
1565
  return parseCycleHealth(await this.read("PLANNING_LOG.md"));
1559
1566
  }
1560
- /** Read all Active Decisions from ACTIVE_DECISIONS.md. */
1561
- async getActiveDecisions() {
1567
+ /**
1568
+ * Read Active Decisions from ACTIVE_DECISIONS.md.
1569
+ *
1570
+ * Default filters out retired ADs (outcome ∈ abandoned/superseded/resolved or superseded=true).
1571
+ * Pass { includeRetired: true } for management/triage surfaces. See PapiAdapter docstring.
1572
+ */
1573
+ async getActiveDecisions(options) {
1562
1574
  const content = await this.readOptional("ACTIVE_DECISIONS.md");
1563
1575
  if (!content) return [];
1564
- return parseActiveDecisions(content);
1576
+ const all = parseActiveDecisions(content);
1577
+ if (options?.includeRetired) return all;
1578
+ return all.filter(isLiveDecision2);
1565
1579
  }
1566
1580
  /** Read cycle log entries (newest first), optionally limited to {@link limit} entries. */
1567
1581
  async getCycleLog(limit) {
@@ -2068,6 +2082,102 @@ ${footer}`);
2068
2082
  await this.write("STRATEGY_RECOMMENDATIONS.md", updated);
2069
2083
  }
2070
2084
  // -------------------------------------------------------------------------
2085
+ // Strategy Review Agenda (markdown persistence)
2086
+ // -------------------------------------------------------------------------
2087
+ async addAgendaTopic(input) {
2088
+ const id = randomUUID6();
2089
+ const createdAt = (/* @__PURE__ */ new Date()).toISOString();
2090
+ const full = {
2091
+ id,
2092
+ topic: input.topic,
2093
+ source: input.source,
2094
+ sourceCycle: input.sourceCycle,
2095
+ status: "pending",
2096
+ createdAt
2097
+ };
2098
+ const content = await this.readOptional("STRATEGY_REVIEW_AGENDA.md");
2099
+ const header = "# Strategy Review Agenda\n\n<!-- PAPI-ADAPTER: parse the yaml block below -->\n\n<!-- PAPI-YAML-START -->\ntopics:\n";
2100
+ const footer = "<!-- PAPI-YAML-END -->\n";
2101
+ const entry = [
2102
+ ` - id: ${full.id}`,
2103
+ ` topic: ${JSON.stringify(full.topic)}`,
2104
+ ` source: ${full.source}`,
2105
+ full.sourceCycle != null ? ` source_cycle: ${full.sourceCycle}` : null,
2106
+ ` status: ${full.status}`,
2107
+ ` created_at: ${full.createdAt}`
2108
+ ].filter(Boolean).join("\n");
2109
+ if (!content) {
2110
+ await this.write("STRATEGY_REVIEW_AGENDA.md", `${header}${entry}
2111
+ ${footer}`);
2112
+ } else {
2113
+ const insertPoint = content.indexOf("<!-- PAPI-YAML-END -->");
2114
+ if (insertPoint === -1) {
2115
+ await this.write("STRATEGY_REVIEW_AGENDA.md", `${header}${entry}
2116
+ ${footer}`);
2117
+ } else {
2118
+ const updated = content.slice(0, insertPoint) + entry + "\n" + content.slice(insertPoint);
2119
+ await this.write("STRATEGY_REVIEW_AGENDA.md", updated);
2120
+ }
2121
+ }
2122
+ return full;
2123
+ }
2124
+ async getPendingAgendaTopics() {
2125
+ const content = await this.readOptional("STRATEGY_REVIEW_AGENDA.md");
2126
+ if (!content) return [];
2127
+ const yamlStart = content.indexOf("<!-- PAPI-YAML-START -->");
2128
+ const yamlEnd = content.indexOf("<!-- PAPI-YAML-END -->");
2129
+ if (yamlStart === -1 || yamlEnd === -1) return [];
2130
+ const yamlBlock = content.slice(yamlStart + "<!-- PAPI-YAML-START -->".length, yamlEnd).trim();
2131
+ const entries = yamlBlock.split(/(?=\s+-\s+id:)/);
2132
+ const topics = [];
2133
+ for (const block of entries) {
2134
+ const idMatch = block.match(/id:\s+(.+)/);
2135
+ const topicMatch = block.match(/topic:\s+(.+)/);
2136
+ const sourceMatch = block.match(/source:\s+(\S+)/);
2137
+ const statusMatch = block.match(/status:\s+(\S+)/);
2138
+ const createdMatch = block.match(/created_at:\s+(.+)/);
2139
+ const sourceCycleMatch = block.match(/source_cycle:\s+(\d+)/);
2140
+ if (!idMatch || !topicMatch || !sourceMatch || !statusMatch || !createdMatch) continue;
2141
+ if (statusMatch[1].trim() !== "pending") continue;
2142
+ let parsedTopic = topicMatch[1].trim();
2143
+ if (parsedTopic.startsWith('"') && parsedTopic.endsWith('"')) {
2144
+ try {
2145
+ parsedTopic = JSON.parse(parsedTopic);
2146
+ } catch {
2147
+ }
2148
+ }
2149
+ topics.push({
2150
+ id: idMatch[1].trim(),
2151
+ topic: parsedTopic,
2152
+ source: sourceMatch[1].trim(),
2153
+ sourceCycle: sourceCycleMatch ? parseInt(sourceCycleMatch[1], 10) : void 0,
2154
+ status: "pending",
2155
+ createdAt: createdMatch[1].trim()
2156
+ });
2157
+ }
2158
+ return topics;
2159
+ }
2160
+ async markAgendaTopicsAddressed(ids, cycleNumber) {
2161
+ if (ids.length === 0) return;
2162
+ const content = await this.readOptional("STRATEGY_REVIEW_AGENDA.md");
2163
+ if (!content) return;
2164
+ let updated = content;
2165
+ const addressedAt = (/* @__PURE__ */ new Date()).toISOString();
2166
+ for (const id of ids) {
2167
+ const statusPattern = new RegExp(`(\\s+-\\s+id:\\s+${id}\\n(?:.*\\n)*?\\s+status:\\s+)pending`);
2168
+ updated = updated.replace(statusPattern, `$1addressed`);
2169
+ const insertionAnchor = new RegExp(`(\\s+-\\s+id:\\s+${id}\\n(?:.*\\n)*?\\s+created_at:\\s+[^\\n]+)\\n`);
2170
+ const match = updated.match(insertionAnchor);
2171
+ if (match && !match[0].includes("addressed_at:")) {
2172
+ updated = updated.replace(insertionAnchor, `$1
2173
+ addressed_at: ${addressedAt}
2174
+ addressed_in_review: ${cycleNumber}
2175
+ `);
2176
+ }
2177
+ }
2178
+ await this.write("STRATEGY_REVIEW_AGENDA.md", updated);
2179
+ }
2180
+ // -------------------------------------------------------------------------
2071
2181
  // Decision Events & Scores (markdown persistence)
2072
2182
  // -------------------------------------------------------------------------
2073
2183
  async appendDecisionEvent(event) {
@@ -6256,11 +6366,31 @@ EXCEPTION WHEN duplicate_object THEN NULL; END $$;
6256
6366
  lastFullMode: 0
6257
6367
  };
6258
6368
  }
6259
- async getActiveDecisions() {
6369
+ /**
6370
+ * Read Active Decisions for this project.
6371
+ *
6372
+ * Default filters out retired ADs (outcome ∈ abandoned/superseded/resolved or superseded=true).
6373
+ * Pass { includeRetired: true } for management/triage surfaces. See PapiAdapter docstring.
6374
+ *
6375
+ * task-1546 (C242 hot-fix): closes the bug where retired ADs leaked into context surfaces.
6376
+ */
6377
+ async getActiveDecisions(options) {
6378
+ if (options?.includeRetired) {
6379
+ const rows2 = await this.sql`
6380
+ SELECT id, display_id, title, confidence, superseded, superseded_by, created_cycle, modified_cycle, body, outcome, revision_count
6381
+ FROM active_decisions
6382
+ WHERE project_id = ${this.projectId}
6383
+ ORDER BY display_id
6384
+ LIMIT 200
6385
+ `;
6386
+ return rows2.map(rowToActiveDecision);
6387
+ }
6260
6388
  const rows = await this.sql`
6261
6389
  SELECT id, display_id, title, confidence, superseded, superseded_by, created_cycle, modified_cycle, body, outcome, revision_count
6262
6390
  FROM active_decisions
6263
6391
  WHERE project_id = ${this.projectId}
6392
+ AND superseded = false
6393
+ AND (outcome IS NULL OR outcome NOT IN ('abandoned', 'superseded', 'resolved'))
6264
6394
  ORDER BY display_id
6265
6395
  LIMIT 200 -- bounded: ADs are bounded by project lifecycle, 200 is a safe ceiling
6266
6396
  `;
@@ -7569,6 +7699,50 @@ ${newParts.join("\n")}` : newParts.join("\n");
7569
7699
  UPDATE strategy_recommendations
7570
7700
  SET status = 'actioned', dismissal_reason = ${reason}, updated_at = now()
7571
7701
  WHERE id = ${id} AND project_id = ${this.projectId}
7702
+ `;
7703
+ }
7704
+ // -------------------------------------------------------------------------
7705
+ // Strategy Review Agenda
7706
+ // -------------------------------------------------------------------------
7707
+ async addAgendaTopic(input) {
7708
+ const [row] = await this.sql`
7709
+ INSERT INTO strategy_review_agenda (project_id, topic, source, source_cycle)
7710
+ VALUES (${this.projectId}, ${input.topic}, ${input.source}, ${input.sourceCycle ?? null})
7711
+ RETURNING id, topic, source, source_cycle, status, created_at
7712
+ `;
7713
+ return {
7714
+ id: row.id,
7715
+ topic: row.topic,
7716
+ source: row.source,
7717
+ sourceCycle: row.source_cycle ?? void 0,
7718
+ status: row.status,
7719
+ createdAt: row.created_at
7720
+ };
7721
+ }
7722
+ async getPendingAgendaTopics() {
7723
+ const rows = await this.sql`
7724
+ SELECT id, topic, source, source_cycle, status, created_at, addressed_at, addressed_in_review
7725
+ FROM strategy_review_agenda
7726
+ WHERE project_id = ${this.projectId} AND status = 'pending'
7727
+ ORDER BY created_at ASC
7728
+ `;
7729
+ return rows.map((r) => ({
7730
+ id: r.id,
7731
+ topic: r.topic,
7732
+ source: r.source,
7733
+ sourceCycle: r.source_cycle ?? void 0,
7734
+ status: "pending",
7735
+ createdAt: r.created_at,
7736
+ addressedAt: r.addressed_at ?? void 0,
7737
+ addressedInReview: r.addressed_in_review ?? void 0
7738
+ }));
7739
+ }
7740
+ async markAgendaTopicsAddressed(ids, cycleNumber) {
7741
+ if (ids.length === 0) return;
7742
+ await this.sql`
7743
+ UPDATE strategy_review_agenda
7744
+ SET status = 'addressed', addressed_at = now(), addressed_in_review = ${cycleNumber}
7745
+ WHERE project_id = ${this.projectId} AND id = ANY(${ids}::uuid[])
7572
7746
  `;
7573
7747
  }
7574
7748
  // -------------------------------------------------------------------------
@@ -8435,8 +8609,8 @@ Check PAPI_PROJECT_ID in your .mcp.json config. Find your project ID in the PAPI
8435
8609
  getCycleHealth() {
8436
8610
  return this.invoke("getCycleHealth");
8437
8611
  }
8438
- getActiveDecisions() {
8439
- return this.invoke("getActiveDecisions");
8612
+ getActiveDecisions(options) {
8613
+ return this.invoke("getActiveDecisions", [options ?? {}]);
8440
8614
  }
8441
8615
  getCycleLog(limit) {
8442
8616
  return this.invoke("getCycleLog", [limit]);
@@ -8761,6 +8935,7 @@ __export(git_exports, {
8761
8935
  getHeadCommitSha: () => getHeadCommitSha,
8762
8936
  getLatestTag: () => getLatestTag,
8763
8937
  getOriginRepoSlug: () => getOriginRepoSlug,
8938
+ getPullRequestUrl: () => getPullRequestUrl,
8764
8939
  getUnmergedBranches: () => getUnmergedBranches,
8765
8940
  gitPull: () => gitPull,
8766
8941
  gitPush: () => gitPush,
@@ -8770,9 +8945,11 @@ __export(git_exports, {
8770
8945
  isGhAvailable: () => isGhAvailable,
8771
8946
  isGitAvailable: () => isGitAvailable,
8772
8947
  isGitRepo: () => isGitRepo,
8948
+ listGroupedCycleBranches: () => listGroupedCycleBranches,
8773
8949
  mergePullRequest: () => mergePullRequest,
8774
8950
  resolveBaseBranch: () => resolveBaseBranch,
8775
8951
  runAutoCommit: () => runAutoCommit,
8952
+ squashMergePullRequest: () => squashMergePullRequest,
8776
8953
  stageAllAndCommit: () => stageAllAndCommit,
8777
8954
  stageDirAndCommit: () => stageDirAndCommit,
8778
8955
  tagExists: () => tagExists,
@@ -9226,6 +9403,68 @@ function runAutoCommit(projectRoot, commitFn) {
9226
9403
  return `Auto-commit failed: ${err instanceof Error ? err.message : String(err)}`;
9227
9404
  }
9228
9405
  }
9406
+ function getPullRequestUrl(cwd, branch) {
9407
+ try {
9408
+ const output = execFileSync(
9409
+ "gh",
9410
+ ["pr", "view", branch, "--json", "url", "--jq", ".url"],
9411
+ { cwd, encoding: "utf-8" }
9412
+ ).trim();
9413
+ return output || null;
9414
+ } catch {
9415
+ return null;
9416
+ }
9417
+ }
9418
+ function squashMergePullRequest(cwd, branch) {
9419
+ const repo = getOriginRepoSlug(cwd);
9420
+ const baseArgs = ["pr", "merge", branch, "--squash", "--delete-branch"];
9421
+ if (repo) baseArgs.push("--repo", repo);
9422
+ for (let attempt = 1; attempt <= MERGE_MAX_RETRIES; attempt++) {
9423
+ try {
9424
+ execFileSync("gh", baseArgs, { cwd, encoding: "utf-8" });
9425
+ return { success: true, message: `Squash-merged PR for '${branch}' and deleted branch.` };
9426
+ } catch (err) {
9427
+ const msg = err instanceof Error ? err.message : String(err);
9428
+ if (msg.includes("not mergeable") && attempt < MERGE_MAX_RETRIES) {
9429
+ sleepSync(MERGE_RETRY_DELAY_MS);
9430
+ continue;
9431
+ }
9432
+ return { success: false, message: `PR squash-merge failed: ${msg}` };
9433
+ }
9434
+ }
9435
+ return { success: false, message: "PR squash-merge failed: max retries exceeded" };
9436
+ }
9437
+ function listGroupedCycleBranches(cwd, cycleNum, baseBranch) {
9438
+ const prefix = `feat/cycle-${cycleNum}-`;
9439
+ try {
9440
+ const remoteOutput = execFileSync(
9441
+ "git",
9442
+ ["ls-remote", "--heads", "origin", `${prefix}*`],
9443
+ { cwd, encoding: "utf-8" }
9444
+ ).trim();
9445
+ if (!remoteOutput) return [];
9446
+ const remoteBranches = remoteOutput.split("\n").map((line) => line.split(" ")[1]?.replace("refs/heads/", "").trim()).filter((b2) => !!b2 && b2.startsWith(prefix));
9447
+ return remoteBranches.filter((branch) => {
9448
+ try {
9449
+ const branchTip = execFileSync(
9450
+ "git",
9451
+ ["rev-parse", `origin/${branch}`],
9452
+ { cwd, encoding: "utf-8" }
9453
+ ).trim();
9454
+ execFileSync(
9455
+ "git",
9456
+ ["merge-base", "--is-ancestor", branchTip, baseBranch],
9457
+ { cwd, stdio: "ignore" }
9458
+ );
9459
+ return false;
9460
+ } catch {
9461
+ return true;
9462
+ }
9463
+ });
9464
+ } catch {
9465
+ return getUnmergedBranches(cwd, baseBranch).filter((b2) => b2.startsWith(prefix));
9466
+ }
9467
+ }
9229
9468
  function getFilesChangedFromBase(cwd, baseBranch) {
9230
9469
  try {
9231
9470
  const mergeBase = execFileSync("git", ["merge-base", baseBranch, "HEAD"], { cwd, encoding: "utf-8" }).trim();
@@ -9277,6 +9516,8 @@ function loadConfig() {
9277
9516
  const lightMode = process.env.PAPI_LIGHT_MODE === "true";
9278
9517
  const projectOwner = process.env.PAPI_OWNER ?? "Cathal";
9279
9518
  const skipProjectSpecificRules = process.env.PAPI_SKIP_PROJECT_RULES === "true";
9519
+ const userId = process.env.PAPI_USER_ID || void 0;
9520
+ const telemetryEnabled = process.env.PAPI_TELEMETRY !== "off" && process.env.PAPI_TELEMETRY !== "false";
9280
9521
  const papiEndpoint = process.env.PAPI_ENDPOINT;
9281
9522
  const dataEndpoint = process.env.PAPI_DATA_ENDPOINT;
9282
9523
  const databaseUrl = process.env.DATABASE_URL;
@@ -9287,9 +9528,18 @@ function loadConfig() {
9287
9528
  adapterType = "proxy";
9288
9529
  console.error("[papi] PAPI_PROJECT_ID detected \u2014 switching to proxy adapter (md adapter blocked for external users).");
9289
9530
  }
9290
- if (!projectId && !databaseUrl && !papiEndpoint && adapterType === "md") {
9531
+ if (adapterType === "md" && !userId) {
9291
9532
  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."
9533
+ `PAPI requires a free account to run in local mode.
9534
+
9535
+ Create your account at https://getpapi.ai/setup \u2014 it takes under a minute.
9536
+ Your project data stays on your machine. The account lets PAPI identify you
9537
+ and unlocks dashboard features when you're ready.
9538
+
9539
+ After signing up, add this to your .mcp.json env config:
9540
+ "PAPI_USER_ID": "your-email@example.com"
9541
+
9542
+ Already have an account? Make sure PAPI_USER_ID is set in your .mcp.json env config.`
9293
9543
  );
9294
9544
  }
9295
9545
  return {
@@ -9303,7 +9553,9 @@ function loadConfig() {
9303
9553
  papiEndpoint,
9304
9554
  lightMode,
9305
9555
  projectOwner,
9306
- skipProjectSpecificRules
9556
+ skipProjectSpecificRules,
9557
+ userId,
9558
+ telemetryEnabled
9307
9559
  };
9308
9560
  }
9309
9561
 
@@ -9396,7 +9648,25 @@ async function createAdapter(optionsOrType, maybePapiDir) {
9396
9648
  console.error("[papi] Set PAPI_USER_ID in your .mcp.json env to fix this.");
9397
9649
  }
9398
9650
  }
9399
- await pgAdapter.createProject({ id: projectId, slug, name: slug, papi_dir: papiDir, user_id: userId });
9651
+ let skipCreate = false;
9652
+ if (userId) {
9653
+ const bySlug = await pgAdapter.listProjects({ slug });
9654
+ const userDup = bySlug.find((p) => p.user_id === userId);
9655
+ if (userDup) {
9656
+ console.error(`[papi] \u26A0 Project '${slug}' already exists for this user (id: ${userDup.id}).`);
9657
+ console.error(`[papi] Update PAPI_PROJECT_ID=${userDup.id} in .mcp.json to avoid a duplicate.`);
9658
+ skipCreate = true;
9659
+ }
9660
+ }
9661
+ if (!skipCreate) {
9662
+ await pgAdapter.createProject({ id: projectId, slug, name: slug, papi_dir: papiDir, user_id: userId });
9663
+ }
9664
+ } else if (existing.user_id) {
9665
+ const configuredUserId = process.env["PAPI_USER_ID"] ?? detectUserId();
9666
+ if (configuredUserId && existing.user_id !== configuredUserId) {
9667
+ console.error(`[papi] \u26A0 PAPI_PROJECT_ID=${projectId} belongs to a different user.`);
9668
+ console.error("[papi] Run papi setup or update PAPI_PROJECT_ID in .mcp.json.");
9669
+ }
9400
9670
  }
9401
9671
  await pgAdapter.close();
9402
9672
  } catch {
@@ -10299,6 +10569,20 @@ async function sendSlackWebhook(webhookUrl, summary, header = "PAPI Strategy Rev
10299
10569
  }
10300
10570
 
10301
10571
  // src/prompts.ts
10572
+ var AD_REJECTION_RULES = `**AD Minting Guard \u2014 REJECT observations dressed as decisions.**
10573
+
10574
+ An Active Decision expresses a *stance* the project is taking \u2014 a choice between alternatives that constrains future work. Reject any candidate AD whose body asserts:
10575
+ (a) the existence, identity, or status of a person, user, or external entity (e.g. "User X is building Y", "Customer Z is active");
10576
+ (b) a metric or measurement (e.g. "Signups grew 3x last cycle", "Latency dropped to 200ms");
10577
+ (c) a fact about the current state of the world that could be confirmed or denied by a query rather than challenged by argument (e.g. "External user feedback is now flowing", "The /admin route exists").
10578
+
10579
+ If a candidate AD body could be invalidated by running a SQL query, refreshing a dashboard, or checking a log \u2014 it is an observation, not a decision. Capture it as a build report finding, a dogfood observation, a cycle log note, or a registered doc instead. Do NOT mint it as an AD.
10580
+
10581
+ **Positive example (valid AD):** "Pricing tier strategy: free engine + paid intelligence. Decision: keep cycles free, charge for strategy reviews and analytics. Why: telemetry shows engagement clusters around intelligence surfaces, not engine surfaces." \u2014 this is a stance with alternatives.
10582
+
10583
+ **Negative example (reject):** "External user feedback is now flowing. Stonebridge Systems is actively building." \u2014 this is a fact about the current state of the world. Capture as dogfood/signal observation; do not mint.
10584
+
10585
+ This rule applies to: new ADs proposed during planning (Step 9), strategy review AD updates (section 5), and strategy_change AD updates. If you find an existing AD that violates this rule during housekeeping, propose deleting it (action: "delete") with a one-line rationale.`;
10302
10586
  var PLAN_SYSTEM = `You are the PAPI Cycle Planner \u2014 an autonomous planning engine for software projects.
10303
10587
  You receive project context and produce a planning cycle output with a BUILD HANDOFF.
10304
10588
 
@@ -10524,6 +10808,9 @@ Standard planning cycle with full board review.
10524
10808
 
10525
10809
  9. **Active Decisions** \u2014 If any AD needs updating: Type A (confidence change), Type B (modification), or Type C (reversal/supersede).
10526
10810
  **AD Quality Bar:** ADs are for product and architecture choices that constrain future work \u2014 technology selections, data model designs, UX principles, strategic positioning. They are NOT for: process preferences (commit style, PR size), configuration choices (linter rules, tab width), or temporary workarounds. If a decision doesn't affect what gets built or how it's architected, it's not an AD. Apply this bar when proposing new ADs and when triaging existing ones.
10811
+
10812
+ ${AD_REJECTION_RULES}
10813
+
10527
10814
  **\u2192 PERSIST:** EVERY AD you created, updated, or confirmed with changes MUST appear in \`activeDecisions\` array in Part 2. Include the full replacement body with ### heading.
10528
10815
 
10529
10816
  ### Operational Quality Rules
@@ -10761,6 +11048,9 @@ Standard planning cycle with full board review.
10761
11048
 
10762
11049
  9. **Active Decisions** \u2014 If any AD needs updating: Type A (confidence change), Type B (modification), or Type C (reversal/supersede).
10763
11050
  **AD Quality Bar:** ADs are for product and architecture choices that constrain future work \u2014 technology selections, data model designs, UX principles, strategic positioning. They are NOT for: process preferences (commit style, PR size), configuration choices (linter rules, tab width), or temporary workarounds. If a decision doesn't affect what gets built or how it's architected, it's not an AD. Apply this bar when proposing new ADs and when triaging existing ones.
11051
+
11052
+ ${AD_REJECTION_RULES}
11053
+
10764
11054
  **\u2192 PERSIST:** EVERY AD you created, updated, or confirmed with changes MUST appear in \`activeDecisions\` array in Part 2. Include the full replacement body with ### heading.
10765
11055
 
10766
11056
  ### Operational Quality Rules
@@ -10835,6 +11125,9 @@ function buildPlanUserMessage(ctx) {
10835
11125
  }) : PLAN_FULL_INSTRUCTIONS;
10836
11126
  parts.push(instructions);
10837
11127
  }
11128
+ if (ctx.foundationalTasksGuidance) {
11129
+ parts.push("", ctx.foundationalTasksGuidance);
11130
+ }
10838
11131
  if (ctx.skipHandoffs) {
10839
11132
  parts.push(
10840
11133
  "",
@@ -11130,6 +11423,8 @@ You MUST cover these 5 sections. Each is mandatory.
11130
11423
  - Note any hierarchy/phase issues worth correcting (1-2 bullets max)
11131
11424
  - Delete ADs that are legacy, process-level, or redundant without discussion
11132
11425
 
11426
+ ${AD_REJECTION_RULES}
11427
+
11133
11428
  **Registered Documents:** If a "### Registered Documents" section is present in context, scan it for: (a) research findings that contradict current ADs or strategy, (b) unactioned research that should influence the next plan. Reference relevant docs by title in your review. If unregistered docs are listed, flag 1-2 that look strategically relevant and suggest registering them.
11134
11429
 
11135
11430
  ## CONDITIONAL SECTIONS (include only when genuinely useful \u2014 most reviews should have 0-2 of these)
@@ -11356,6 +11651,9 @@ function buildReviewUserMessage(ctx) {
11356
11651
  if (ctx.docActionStaleness) {
11357
11652
  parts.push("### Doc Action Staleness", "", ctx.docActionStaleness, "");
11358
11653
  }
11654
+ if (ctx.pendingAgendaTopics) {
11655
+ 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, "");
11656
+ }
11359
11657
  return parts.join("\n");
11360
11658
  }
11361
11659
  function parseReviewStructuredOutput(raw) {
@@ -11436,6 +11734,8 @@ The JSON must be valid. Only include ADs that need changes \u2014 omit unchanged
11436
11734
  For new ADs, use the next available AD number.
11437
11735
  The body field must be the COMPLETE replacement text for the AD block (including the ### heading line).
11438
11736
 
11737
+ ${AD_REJECTION_RULES}
11738
+
11439
11739
  ## PHASE UPDATES
11440
11740
 
11441
11741
  If the strategic change affects the project's phase structure, include a phaseUpdates array.
@@ -12042,6 +12342,64 @@ function stripTasksForPlan(tasks) {
12042
12342
  hasHandoff: !!buildHandoff
12043
12343
  }));
12044
12344
  }
12345
+ var BRIEF_SECTIONS = [
12346
+ { name: "title", pattern: /^#\s+\S/m },
12347
+ {
12348
+ name: "target audience",
12349
+ pattern: /\b(target users?|audience|for whom|who (it'?s for|uses|it serves|we're building))/i
12350
+ },
12351
+ {
12352
+ name: "problem statement",
12353
+ pattern: /\b(problem|pain point|why it matters|what problem|solves?)/i
12354
+ },
12355
+ {
12356
+ name: "solution / vision",
12357
+ pattern: /\b(solution|approach|vision|what (it|we) do|how it works|value proposition)/i
12358
+ },
12359
+ {
12360
+ name: "GTM / distribution",
12361
+ pattern: /\b(GTM|go[- ]to[- ]market|distribution|channel|pricing|how (users?|people) (discover|find|reach))/i
12362
+ }
12363
+ ];
12364
+ function assessBriefThinness(brief) {
12365
+ const TEMPLATE_MARKER = "*Describe your project's core value proposition here.*";
12366
+ const briefWithoutTemplate = brief.replace(TEMPLATE_MARKER, "");
12367
+ const populated = [];
12368
+ const missing = [];
12369
+ for (const section of BRIEF_SECTIONS) {
12370
+ if (section.pattern.test(briefWithoutTemplate)) populated.push(section.name);
12371
+ else missing.push(section.name);
12372
+ }
12373
+ return { populatedSections: populated, missingSections: missing };
12374
+ }
12375
+ function computeFoundationalTasksGuidance(cycleNumber, brief) {
12376
+ if (cycleNumber > 1) return void 0;
12377
+ const { populatedSections, missingSections } = assessBriefThinness(brief);
12378
+ if (populatedSections.length >= 4) return void 0;
12379
+ const populatedList = populatedSections.length > 0 ? populatedSections.join(", ") : "none";
12380
+ const missingList = missingSections.join(", ");
12381
+ return [
12382
+ "## FOUNDATIONAL TASKS GUIDANCE",
12383
+ "",
12384
+ `The project brief is thin \u2014 only ${populatedSections.length} of 5 key sections are clearly populated (${populatedList}). Missing or weak sections: ${missingList}.`,
12385
+ "",
12386
+ "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.",
12387
+ "",
12388
+ "Rules for foundational tasks:",
12389
+ "- **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).",
12390
+ '- **Target specific missing sections.** Do NOT generate "refine audience" if audience is already populated. Each foundational task must close a specific gap above.',
12391
+ "- **Task type: `research` or `discovery`** \u2014 deliverable is a findings doc, not shipped code.",
12392
+ "- **Effort: S or M** \u2014 foundational work should be timeboxed, not open-ended.",
12393
+ '- **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.',
12394
+ "- **Cap at 5 foundational tasks** to avoid drowning the user.",
12395
+ "- **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.",
12396
+ "",
12397
+ "In the structured output:",
12398
+ "- Emit foundational tasks via the `newTasks` array (mark `reviewed: true`, type `research` or `discovery`).",
12399
+ "- Generate full BUILD HANDOFFs for each in `cycleHandoffs` \u2014 use `new-N` IDs to reference them.",
12400
+ "- User-submitted backlog tasks remain in `cycleHandoffs` as usual \u2014 do NOT drop them in favour of foundational tasks."
12401
+ ].join("\n");
12402
+ }
12045
12403
  function detectBoardFlags(tasks) {
12046
12404
  let hasBugTasks = false;
12047
12405
  let hasResearchTasks = false;
@@ -12320,7 +12678,8 @@ ${lines.join("\n")}`;
12320
12678
  preAssignedTasks: preAssignedTextLean,
12321
12679
  recentlyShippedCapabilities: recentlyShippedLean,
12322
12680
  strategyReviewCadence,
12323
- candidateTaskFullNotes: candidateTaskFullNotesLean
12681
+ candidateTaskFullNotes: candidateTaskFullNotesLean,
12682
+ foundationalTasksGuidance: computeFoundationalTasksGuidance(health.totalCycles, productBrief)
12324
12683
  };
12325
12684
  const { label: leanTierLabel } = applyContextTier(ctx2, health.totalCycles);
12326
12685
  ctx2.contextTier = leanTierLabel;
@@ -12482,7 +12841,8 @@ ${logLines}`);
12482
12841
  preAssignedTasks: preAssignedText,
12483
12842
  recentlyShippedCapabilities: formatRecentlyShippedCapabilities(reports),
12484
12843
  strategyReviewCadence: strategyReviewCadenceFull,
12485
- candidateTaskFullNotes: formatCandidateTaskFullNotes(plannerTasks)
12844
+ candidateTaskFullNotes: formatCandidateTaskFullNotes(plannerTasks),
12845
+ foundationalTasksGuidance: computeFoundationalTasksGuidance(health.totalCycles, productBrief)
12486
12846
  };
12487
12847
  const { label: fullTierLabel } = applyContextTier(ctx, health.totalCycles);
12488
12848
  ctx.contextTier = fullTierLabel;
@@ -13838,7 +14198,8 @@ async function assembleContext2(adapter2, cycleNumber, cyclesSinceLastReview, pr
13838
14198
  docsWithPendingActions
13839
14199
  ] = await Promise.all([
13840
14200
  adapter2.readProductBrief(),
13841
- adapter2.getActiveDecisions(),
14201
+ // Strategy review needs to see retired ADs to triage/restore them as needed.
14202
+ adapter2.getActiveDecisions({ includeRetired: true }),
13842
14203
  adapter2.getBuildReportsSince(lastReviewCycleNum),
13843
14204
  adapter2.getCycleLogSince(lastReviewCycleNum),
13844
14205
  adapter2.queryBoard({
@@ -13863,6 +14224,7 @@ async function assembleContext2(adapter2, cycleNumber, cyclesSinceLastReview, pr
13863
14224
  // Doc registry — docs with pending actions for staleness audit
13864
14225
  adapter2.searchDocs?.({ hasPendingActions: true, limit: 20 })?.catch(() => []) ?? Promise.resolve([])
13865
14226
  ]);
14227
+ const pendingAgendaTopics = await (adapter2.getPendingAgendaTopics?.().catch(() => []) ?? Promise.resolve([]));
13866
14228
  const tasks = [...activeTasks, ...recentDoneTasks];
13867
14229
  const existingAdIds = new Set(decisions.map((d) => d.id));
13868
14230
  const survivingPendingRecs = [];
@@ -14100,6 +14462,15 @@ ${deferred.join("\n")}`);
14100
14462
  }
14101
14463
  }
14102
14464
  } catch {
14465
+ }
14466
+ let pendingAgendaText;
14467
+ if (pendingAgendaTopics.length > 0) {
14468
+ const lines = pendingAgendaTopics.map((t, i) => {
14469
+ const cycleSuffix = t.sourceCycle != null ? ` (queued Cycle ${t.sourceCycle})` : "";
14470
+ return `${i + 1}. ${t.topic} _[${t.source}${cycleSuffix}]_`;
14471
+ });
14472
+ pendingAgendaText = `${pendingAgendaTopics.length} topic(s) queued via strategy_agenda:
14473
+ ${lines.join("\n")}`;
14103
14474
  }
14104
14475
  logDataSourceSummary("strategy_review_audit", [
14105
14476
  { label: "discoveryCanvas", hasData: discoveryCanvasText !== void 0 },
@@ -14140,7 +14511,8 @@ ${deferred.join("\n")}`);
14140
14511
  recentPlans: recentPlansText,
14141
14512
  unregisteredDocs: unregisteredDocsText,
14142
14513
  taskComments: taskCommentsText,
14143
- docActionStaleness: docActionStalenessText
14514
+ docActionStaleness: docActionStalenessText,
14515
+ pendingAgendaTopics: pendingAgendaText
14144
14516
  };
14145
14517
  const BUDGET_SOFT2 = 5e4;
14146
14518
  const BUDGET_HARD2 = 6e4;
@@ -14329,7 +14701,7 @@ ${cleanContent}`;
14329
14701
  try {
14330
14702
  const recs = extractRecommendations(data, cycleNumber);
14331
14703
  if (recs.length > 0) {
14332
- const existingAds = await adapter2.getActiveDecisions().catch(() => []);
14704
+ const existingAds = await adapter2.getActiveDecisions({ includeRetired: true }).catch(() => []);
14333
14705
  const existingAdIds = new Set(existingAds.map((ad) => ad.id));
14334
14706
  const filteredRecs = recs.filter((rec) => {
14335
14707
  if (rec.target && /^AD-\d+$/.test(rec.target)) {
@@ -14455,6 +14827,15 @@ async function processReviewOutput(adapter2, rawOutput, cycleNumber) {
14455
14827
  await adapter2.clearPendingReviewResponse?.();
14456
14828
  } catch {
14457
14829
  }
14830
+ try {
14831
+ if (adapter2.getPendingAgendaTopics && adapter2.markAgendaTopicsAddressed) {
14832
+ const pending = await adapter2.getPendingAgendaTopics();
14833
+ if (pending.length > 0) {
14834
+ await adapter2.markAgendaTopicsAddressed(pending.map((t) => t.id), cycleNumber);
14835
+ }
14836
+ }
14837
+ } catch {
14838
+ }
14458
14839
  const webhookUrl = process.env.PAPI_SLACK_WEBHOOK_URL;
14459
14840
  slackWarning = await sendSlackWebhook(webhookUrl, buildSlackSummary(data));
14460
14841
  }
@@ -14854,7 +15235,8 @@ async function prepareStrategyChange(adapter2, text) {
14854
15235
  try {
14855
15236
  const [brief, decisions, readPhases, boardTasks, reports, previousReviews] = await Promise.all([
14856
15237
  adapter2.readProductBrief(),
14857
- adapter2.getActiveDecisions(),
15238
+ // Strategy review needs ALL ADs (live + retired) for housekeeping.
15239
+ adapter2.getActiveDecisions({ includeRetired: true }),
14858
15240
  adapter2.readPhases(),
14859
15241
  adapter2.queryBoard().catch(() => []),
14860
15242
  adapter2.getRecentBuildReports(15).catch(() => []),
@@ -14899,7 +15281,7 @@ async function captureDecision(adapter2, input) {
14899
15281
  adId = input.adId;
14900
15282
  adAction = "updated";
14901
15283
  } else {
14902
- const existingAds = await adapter2.getActiveDecisions();
15284
+ const existingAds = await adapter2.getActiveDecisions({ includeRetired: true });
14903
15285
  const maxNum = existingAds.reduce((max, ad) => {
14904
15286
  const match = ad.id.match(/^AD-(\d+)$/);
14905
15287
  return match ? Math.max(max, parseInt(match[1], 10)) : max;
@@ -14975,6 +15357,34 @@ var strategyReviewTool = {
14975
15357
  required: []
14976
15358
  }
14977
15359
  };
15360
+ var strategyAgendaTool = {
15361
+ name: "strategy_agenda",
15362
+ 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.',
15363
+ annotations: { readOnlyHint: false, destructiveHint: false },
15364
+ inputSchema: {
15365
+ type: "object",
15366
+ properties: {
15367
+ mode: {
15368
+ type: "string",
15369
+ enum: ["add", "list"],
15370
+ description: '"add" to queue a topic (requires `topic`). "list" returns all pending topics. Defaults to "list" when omitted.'
15371
+ },
15372
+ topic: {
15373
+ type: "string",
15374
+ description: 'The topic to queue (mode "add" only). One sentence describing what the next strategy review should consider.'
15375
+ },
15376
+ source: {
15377
+ type: "string",
15378
+ description: 'Optional origin label \u2014 e.g. "manual", "carry-forward", "idea". Defaults to "manual".'
15379
+ },
15380
+ source_cycle: {
15381
+ type: "number",
15382
+ description: 'Optional cycle number this topic originated from (mode "add" only).'
15383
+ }
15384
+ },
15385
+ required: []
15386
+ }
15387
+ };
14978
15388
  var strategyChangeTool = {
14979
15389
  name: "strategy_change",
14980
15390
  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 +15541,49 @@ ${result.userMessage}
15131
15541
  return errorResponse(err instanceof Error ? err.message : String(err));
15132
15542
  }
15133
15543
  }
15544
+ async function handleStrategyAgenda(adapter2, _config, args) {
15545
+ const mode = args.mode ?? "list";
15546
+ if (!adapter2.addAgendaTopic || !adapter2.getPendingAgendaTopics) {
15547
+ return errorResponse("strategy_agenda is not supported by the current adapter.");
15548
+ }
15549
+ try {
15550
+ if (mode === "add") {
15551
+ const topic = args.topic;
15552
+ if (!topic || !topic.trim()) {
15553
+ return errorResponse('topic is required for mode "add". Describe what the next strategy review should consider.');
15554
+ }
15555
+ const source = (args.source ?? "manual").trim() || "manual";
15556
+ const sourceCycle = typeof args.source_cycle === "number" ? args.source_cycle : void 0;
15557
+ const entry = await adapter2.addAgendaTopic({ topic: topic.trim(), source, sourceCycle });
15558
+ return textResponse(
15559
+ `**Agenda Topic Queued**
15560
+
15561
+ ${entry.topic}
15562
+
15563
+ Source: ${entry.source}${entry.sourceCycle != null ? ` (Cycle ${entry.sourceCycle})` : ""}
15564
+ ID: ${entry.id}
15565
+
15566
+ This topic will surface in the next \`strategy_review\`.`
15567
+ );
15568
+ }
15569
+ const topics = await adapter2.getPendingAgendaTopics();
15570
+ if (topics.length === 0) {
15571
+ return textResponse('No pending agenda topics. Use `strategy_agenda` with `mode: "add"` to queue one.');
15572
+ }
15573
+ const lines = topics.map((t, i) => {
15574
+ const cycleSuffix = t.sourceCycle != null ? ` (Cycle ${t.sourceCycle})` : "";
15575
+ return `${i + 1}. ${t.topic}
15576
+ _source: ${t.source}${cycleSuffix} \xB7 queued ${t.createdAt.slice(0, 10)}_`;
15577
+ });
15578
+ return textResponse(
15579
+ `**Pending Agenda (${topics.length})** \u2014 surfaces at next strategy review
15580
+
15581
+ ${lines.join("\n\n")}`
15582
+ );
15583
+ } catch (err) {
15584
+ return errorResponse(err instanceof Error ? err.message : String(err));
15585
+ }
15586
+ }
15134
15587
  async function handleStrategyChange(adapter2, _config, args) {
15135
15588
  const toolMode = args.mode;
15136
15589
  try {
@@ -15402,7 +15855,7 @@ var boardArchiveTool = {
15402
15855
  };
15403
15856
  var boardEditTool = {
15404
15857
  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.",
15858
+ 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
15859
  annotations: { readOnlyHint: false, destructiveHint: false },
15407
15860
  inputSchema: {
15408
15861
  type: "object",
@@ -15439,7 +15892,12 @@ var boardEditTool = {
15439
15892
  },
15440
15893
  notes: {
15441
15894
  type: "string",
15442
- description: "New notes (replaces existing notes)."
15895
+ description: "Note content. Default behaviour is append \u2014 see notes_mode to control."
15896
+ },
15897
+ notes_mode: {
15898
+ type: "string",
15899
+ enum: ["append", "replace", "clear"],
15900
+ 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
15901
  },
15444
15902
  status: {
15445
15903
  type: "string",
@@ -15642,6 +16100,10 @@ async function handleBoardEdit(adapter2, args) {
15642
16100
  changes.push(field);
15643
16101
  }
15644
16102
  }
16103
+ const notesMode = args.notes_mode;
16104
+ if (notesMode === "clear" && !changes.includes("notes")) {
16105
+ changes.push("notes");
16106
+ }
15645
16107
  if (changes.length === 0) {
15646
16108
  return errorResponse("No fields to update. Pass at least one field (title, priority, complexity, module, epic, phase, notes, status, maturity).");
15647
16109
  }
@@ -15650,6 +16112,35 @@ async function handleBoardEdit(adapter2, args) {
15650
16112
  if (!task) {
15651
16113
  return errorResponse(`Task ${taskId} not found.`);
15652
16114
  }
16115
+ if (changes.includes("notes")) {
16116
+ const incoming = args.notes ?? "";
16117
+ const existing = task.notes ?? "";
16118
+ const mode = notesMode ?? "append";
16119
+ if (mode === "clear") {
16120
+ updates.notes = "";
16121
+ } else if (mode === "replace") {
16122
+ updates.notes = incoming;
16123
+ } else {
16124
+ const trimmed = incoming.trim();
16125
+ if (trimmed.length === 0) {
16126
+ delete updates.notes;
16127
+ const idx = changes.indexOf("notes");
16128
+ if (idx >= 0) changes.splice(idx, 1);
16129
+ } else {
16130
+ const health = await adapter2.getCycleHealth().catch(() => null);
16131
+ const activeCycle = health?.totalCycles ?? null;
16132
+ const date = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
16133
+ const stamp = activeCycle != null ? `[C${activeCycle} ${date}]` : `[${date}]`;
16134
+ const entry = `${stamp} ${trimmed}`;
16135
+ updates.notes = existing.trim().length > 0 ? `${entry}
16136
+
16137
+ ${existing}` : entry;
16138
+ }
16139
+ }
16140
+ if (changes.length === 0) {
16141
+ return errorResponse("No fields to update. Pass at least one field (title, priority, complexity, module, epic, phase, notes, status, maturity).");
16142
+ }
16143
+ }
15653
16144
  if (updates.status === "Backlog" && task.cycle != null) {
15654
16145
  updates.cycle = void 0;
15655
16146
  updates.cycle = null;
@@ -16671,9 +17162,8 @@ async function prepareSetup(adapter2, config2, input) {
16671
17162
  );
16672
17163
  }
16673
17164
  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
- }
17165
+ const briefHasRealContent = existingBrief.trim().length > 0 && !existingBrief.includes(TEMPLATE_MARKER);
17166
+ const briefAlreadyExists = briefHasRealContent && !input.force;
16677
17167
  const detectedCodebaseType = detectCodebaseType(config2.projectRoot);
16678
17168
  const autoDetected = input.existingProject === void 0 || input.existingProject === false;
16679
17169
  const isExistingProject = input.existingProject === true || autoDetected && detectedCodebaseType === "existing_codebase";
@@ -16686,7 +17176,7 @@ async function prepareSetup(adapter2, config2, input) {
16686
17176
  }
16687
17177
  codebaseSummary = formatCodebaseSummary(scan, sourceContents);
16688
17178
  }
16689
- const briefPrompt = {
17179
+ const briefPrompt = briefAlreadyExists ? void 0 : {
16690
17180
  system: PRODUCT_BRIEF_SYSTEM,
16691
17181
  user: buildProductBriefPrompt({
16692
17182
  projectName: input.projectName,
@@ -16740,15 +17230,29 @@ async function prepareSetup(adapter2, config2, input) {
16740
17230
  initialTasksPrompt,
16741
17231
  codebaseSummary,
16742
17232
  detectedCodebaseType,
16743
- autoDetected: autoDetected && detectedCodebaseType !== "new_project"
17233
+ autoDetected: autoDetected && detectedCodebaseType !== "new_project",
17234
+ briefAlreadyExists
16744
17235
  };
16745
17236
  }
16746
17237
  async function applySetup(adapter2, config2, input, briefText, adSeedText, conventionsText, initialTasksText) {
16747
17238
  const createdProject = await scaffoldPapiDir(adapter2, config2, input);
16748
- if (!briefText.trim()) {
16749
- throw new Error("brief_response is required and cannot be empty.");
17239
+ const TEMPLATE_MARKER = "*Describe your project's core value proposition here.*";
17240
+ let effectiveBriefText = briefText;
17241
+ if (!effectiveBriefText.trim()) {
17242
+ let existingBrief = "";
17243
+ try {
17244
+ existingBrief = await adapter2.readProductBrief();
17245
+ } catch {
17246
+ existingBrief = "";
17247
+ }
17248
+ const existingIsReal = existingBrief.trim().length > 0 && !existingBrief.includes(TEMPLATE_MARKER);
17249
+ if (existingIsReal && !input.force) {
17250
+ effectiveBriefText = existingBrief;
17251
+ } else {
17252
+ throw new Error("brief_response is required and cannot be empty.");
17253
+ }
16750
17254
  }
16751
- const { seededAds, warnings } = await applySetupOutputs(adapter2, config2, input, briefText, adSeedText, conventionsText);
17255
+ const { seededAds, warnings } = await applySetupOutputs(adapter2, config2, input, effectiveBriefText, adSeedText, conventionsText);
16752
17256
  let createdTasks = 0;
16753
17257
  if (initialTasksText?.trim()) {
16754
17258
  try {
@@ -16974,10 +17478,7 @@ PAPI needs just 3 things: project name, what it does, and who it's for.`
16974
17478
  const input = extractInput(args);
16975
17479
  try {
16976
17480
  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
- }
17481
+ const briefResponse = args.brief_response ?? "";
16981
17482
  const adSeedResponse = args.ad_seed_response;
16982
17483
  const conventionsResponse = args.conventions_response;
16983
17484
  const initialTasksResponse = args.initial_tasks_response;
@@ -17010,6 +17511,12 @@ PAPI needs just 3 things: project name, what it does, and who it's for.`
17010
17511
  ""
17011
17512
  );
17012
17513
  }
17514
+ if (result.briefAlreadyExists) {
17515
+ sections.push(
17516
+ `**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\`.`,
17517
+ ""
17518
+ );
17519
+ }
17013
17520
  if (inferredDefaults.length > 0) {
17014
17521
  sections.push(
17015
17522
  `**Defaults applied** (override by re-running setup with these fields):`,
@@ -17020,30 +17527,37 @@ PAPI needs just 3 things: project name, what it does, and who it's for.`
17020
17527
  sections.push(
17021
17528
  `Generate the outputs below, then call \`setup\` again with:`,
17022
17529
  `- \`mode\`: "apply"`,
17023
- `- \`brief_response\`: your Product Brief markdown`,
17530
+ result.briefPrompt ? `- \`brief_response\`: your Product Brief markdown` : "",
17024
17531
  result.adSeedPrompt ? `- \`ad_seed_response\`: your AD seed JSON array` : "",
17025
17532
  result.conventionsPrompt ? `- \`conventions_response\`: your conventions markdown` : "",
17026
17533
  result.initialTasksPrompt ? `- \`initial_tasks_response\`: your initial tasks JSON array` : "",
17027
17534
  `- Plus all the original setup fields (project_name, description, target_users${isExisting ? ", existing_project: true" : ""})`,
17028
17535
  "",
17029
- `---`,
17030
- "",
17031
- `### 1. Product Brief`,
17032
- "",
17033
- `<system_prompt>
17536
+ `---`
17537
+ );
17538
+ let sectionNum = 0;
17539
+ if (result.briefPrompt) {
17540
+ sectionNum++;
17541
+ sections.push(
17542
+ "",
17543
+ `### ${sectionNum}. Product Brief`,
17544
+ "",
17545
+ `<system_prompt>
17034
17546
  ${result.briefPrompt.system}
17035
17547
  </system_prompt>`,
17036
- "",
17037
- `<context>
17548
+ "",
17549
+ `<context>
17038
17550
  ${result.briefPrompt.user}
17039
17551
  </context>`
17040
- );
17552
+ );
17553
+ }
17041
17554
  if (result.adSeedPrompt) {
17555
+ sectionNum++;
17042
17556
  sections.push(
17043
17557
  "",
17044
17558
  `---`,
17045
17559
  "",
17046
- `### 2. Active Decision Seeds`,
17560
+ `### ${sectionNum}. Active Decision Seeds`,
17047
17561
  "",
17048
17562
  `<system_prompt>
17049
17563
  ${result.adSeedPrompt.system}
@@ -17055,11 +17569,12 @@ ${result.adSeedPrompt.user}
17055
17569
  );
17056
17570
  }
17057
17571
  if (result.conventionsPrompt) {
17572
+ sectionNum++;
17058
17573
  sections.push(
17059
17574
  "",
17060
17575
  `---`,
17061
17576
  "",
17062
- `### 3. Conventions`,
17577
+ `### ${sectionNum}. Conventions`,
17063
17578
  "",
17064
17579
  `<system_prompt>
17065
17580
  ${result.conventionsPrompt.system}
@@ -17071,11 +17586,12 @@ ${result.conventionsPrompt.user}
17071
17586
  );
17072
17587
  }
17073
17588
  if (result.initialTasksPrompt) {
17589
+ sectionNum++;
17074
17590
  sections.push(
17075
17591
  "",
17076
17592
  `---`,
17077
17593
  "",
17078
- `### 4. Initial Backlog Tasks`,
17594
+ `### ${sectionNum}. Initial Backlog Tasks`,
17079
17595
  "",
17080
17596
  `<system_prompt>
17081
17597
  ${result.initialTasksPrompt.system}
@@ -17182,6 +17698,23 @@ function isNoHandoffError(err) {
17182
17698
  function isBlockedError(err) {
17183
17699
  return err instanceof Error && err.code === "BLOCKED";
17184
17700
  }
17701
+ function computeScopeDriftSignal(predicted, changed) {
17702
+ if (!predicted || predicted.length === 0) return null;
17703
+ if (changed.length === 0) return null;
17704
+ const basename2 = (p) => {
17705
+ const parts = p.split(/[\\/]/);
17706
+ return parts[parts.length - 1] ?? p;
17707
+ };
17708
+ const predictedNames = new Set(predicted.map(basename2).filter(Boolean));
17709
+ const unexpected = changed.filter((c) => !predictedNames.has(basename2(c)));
17710
+ const fraction = unexpected.length / changed.length;
17711
+ const triggered = fraction > 0.5 || unexpected.length > 5;
17712
+ if (!triggered) return null;
17713
+ const sample = unexpected.slice(0, 5).join(", ");
17714
+ const more = unexpected.length > 5 ? ` (+${unexpected.length - 5} more)` : "";
17715
+ const pct = Math.round(fraction * 100);
17716
+ return `${unexpected.length}/${changed.length} changed files (${pct}%) outside FILES LIKELY TOUCHED: ${sample}${more}`;
17717
+ }
17185
17718
  function getUnresolvedDeps(task, allTasks) {
17186
17719
  if (!task.dependsOn) return [];
17187
17720
  const deps = task.dependsOn.split(",").map((d) => d.trim()).filter(Boolean);
@@ -17243,6 +17776,11 @@ async function startBuild(adapter2, config2, taskId, options = {}) {
17243
17776
  if (task.status === "Done" || task.status === "Archived") {
17244
17777
  throw new Error(`Task "${taskId}" (${task.title}) is already ${task.status}. Cannot execute a completed task.`);
17245
17778
  }
17779
+ if (task.status === "In Review") {
17780
+ throw new Error(
17781
+ `Task "${taskId}" (${task.title}) is already In Review \u2014 it has been built and is awaiting sign-off. Run \`review_submit\` instead of re-building. If the build genuinely needs rework, use \`review_submit\` with verdict \`request-changes\` first.`
17782
+ );
17783
+ }
17246
17784
  if (!task.buildHandoff) {
17247
17785
  const err = new Error(`Task "${taskId}" (${task.title}) has no BUILD HANDOFF.`);
17248
17786
  err.code = "NO_HANDOFF";
@@ -17487,18 +18025,37 @@ async function completeBuild(adapter2, config2, taskId, input, options = {}) {
17487
18025
  }
17488
18026
  }
17489
18027
  let autoTriagedCount = 0;
18028
+ const autoTriagedIds = [];
18029
+ const autoTriagedDupes = [];
17490
18030
  if (input.discoveredIssues && input.discoveredIssues !== "None" && typeof adapter2.createTask === "function") {
17491
18031
  const issueLines = input.discoveredIssues.split(/\n|;/).map((s) => s.trim()).filter((s) => s.length > 0);
18032
+ const backlogTitleMap = /* @__PURE__ */ new Map();
18033
+ try {
18034
+ const backlog = await adapter2.queryBoard({ status: ["Backlog"] });
18035
+ for (const t of backlog) {
18036
+ const normalized = t.title.replace(/^\[Auto-triaged\]\s*/i, "").trim().toLowerCase();
18037
+ if (normalized && !backlogTitleMap.has(normalized)) {
18038
+ backlogTitleMap.set(normalized, t.displayId);
18039
+ }
18040
+ }
18041
+ } catch {
18042
+ }
17492
18043
  for (const line of issueLines) {
17493
18044
  const sevMatch = line.match(/^(P[0-3])[\s:]+/i);
17494
18045
  if (!sevMatch) continue;
17495
18046
  const severityLabel = sevMatch[1].toUpperCase();
17496
- const priority = severityLabel === "P0" || severityLabel === "P1" ? "P1 High" : severityLabel === "P2" ? "P2 Medium" : "P3 Low";
18047
+ const priority = severityLabel === "P0" ? "P0 Critical" : severityLabel === "P1" ? "P1 High" : severityLabel === "P2" ? "P2 Medium" : "P3 Low";
17497
18048
  const titleRaw = line.replace(/^P[0-3][\s:]+/i, "").trim();
17498
18049
  const title = titleRaw.length > 120 ? titleRaw.slice(0, 120) : titleRaw;
17499
18050
  if (!title) continue;
18051
+ const normalized = title.toLowerCase();
18052
+ const dupId = backlogTitleMap.get(normalized);
18053
+ if (dupId) {
18054
+ autoTriagedDupes.push(dupId);
18055
+ continue;
18056
+ }
17500
18057
  try {
17501
- await adapter2.createTask({
18058
+ const created = await adapter2.createTask({
17502
18059
  uuid: "",
17503
18060
  displayId: "",
17504
18061
  title: `[Auto-triaged] ${title}`,
@@ -17515,6 +18072,10 @@ async function completeBuild(adapter2, config2, taskId, input, options = {}) {
17515
18072
  createdCycle: cycleNumber
17516
18073
  });
17517
18074
  autoTriagedCount++;
18075
+ if (created?.displayId) {
18076
+ autoTriagedIds.push(created.displayId);
18077
+ backlogTitleMap.set(normalized, created.displayId);
18078
+ }
17518
18079
  } catch {
17519
18080
  }
17520
18081
  }
@@ -17620,6 +18181,8 @@ async function completeBuild(adapter2, config2, taskId, input, options = {}) {
17620
18181
  const baseBranch = resolveBaseBranch(config2.projectRoot, config2.baseBranch);
17621
18182
  const changed = getFilesChangedFromBase(config2.projectRoot, baseBranch);
17622
18183
  if (changed.length > 0) report.filesChanged = changed;
18184
+ const drift = computeScopeDriftSignal(task.buildHandoff?.filesLikelyTouched, changed);
18185
+ if (drift) report.scopeDriftSignal = drift;
17623
18186
  }
17624
18187
  let prLines = [];
17625
18188
  if (options.light) {
@@ -17711,6 +18274,8 @@ async function completeBuild(adapter2, config2, taskId, input, options = {}) {
17711
18274
  dogfoodResolvedCount: dogfoodResolvedCount > 0 ? dogfoodResolvedCount : void 0,
17712
18275
  learningsLinkedCount: learningsLinkedCount > 0 ? learningsLinkedCount : void 0,
17713
18276
  autoTriagedCount: autoTriagedCount > 0 ? autoTriagedCount : void 0,
18277
+ autoTriagedIds: autoTriagedIds.length > 0 ? autoTriagedIds : void 0,
18278
+ autoTriagedDupes: autoTriagedDupes.length > 0 ? autoTriagedDupes : void 0,
17714
18279
  reportWriteVerified
17715
18280
  };
17716
18281
  }
@@ -18188,6 +18753,7 @@ function formatCompleteResult(result) {
18188
18753
  `**Discovered Issues:** ${result.report.discoveredIssues}`,
18189
18754
  `**Architecture Notes:** ${result.report.architectureNotes}`,
18190
18755
  ...result.report.deadEnds ? [`**Dead Ends:** ${result.report.deadEnds}`] : [],
18756
+ ...result.report.scopeDriftSignal ? [`**Scope drift signal:** ${result.report.scopeDriftSignal}`] : [],
18191
18757
  `**Scope Accuracy:** ${result.scopeAccuracy}`,
18192
18758
  "",
18193
18759
  "---",
@@ -18214,8 +18780,16 @@ function formatCompleteResult(result) {
18214
18780
  if (result.learningsLinkedCount) {
18215
18781
  lines.push("", `Linked ${result.learningsLinkedCount} unactioned learning(s) to this task.`);
18216
18782
  }
18217
- if (result.autoTriagedCount) {
18218
- lines.push("", `\u{1F516} Auto-triaged ${result.autoTriagedCount} discovered issue(s) to Backlog.`);
18783
+ if (result.autoTriagedCount || result.autoTriagedDupes?.length) {
18784
+ const parts = [];
18785
+ if (result.autoTriagedCount) {
18786
+ const ids = result.autoTriagedIds?.length ? ` (${result.autoTriagedIds.join(", ")})` : "";
18787
+ parts.push(`\u{1F516} Auto-triaged ${result.autoTriagedCount} discovered issue(s) to Backlog${ids}.`);
18788
+ }
18789
+ if (result.autoTriagedDupes?.length) {
18790
+ parts.push(`Skipped ${result.autoTriagedDupes.length} duplicate issue(s) already in Backlog: ${result.autoTriagedDupes.join(", ")}.`);
18791
+ }
18792
+ lines.push("", parts.join(" "));
18219
18793
  }
18220
18794
  if (result.reportWriteVerified === false) {
18221
18795
  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 +20404,41 @@ function generateChangelog(version, commits) {
19830
20404
  ${commitList}
19831
20405
  `;
19832
20406
  }
19833
- async function createRelease(config2, branch, version, adapter2) {
20407
+ function mergeGroupedCycleBranches(config2, cycleNum, baseBranch) {
20408
+ const branches = listGroupedCycleBranches(config2.projectRoot, cycleNum, baseBranch);
20409
+ if (branches.length === 0) return [];
20410
+ if (!isGhAvailable()) {
20411
+ throw new Error(
20412
+ `Release blocked: ${branches.length} unmerged grouped cycle branch(es) detected (${branches.join(", ")}) but \`gh\` CLI is not available. Install gh and re-run release.`
20413
+ );
20414
+ }
20415
+ const results = [];
20416
+ for (const branch of branches) {
20417
+ let prUrl = getPullRequestUrl(config2.projectRoot, branch);
20418
+ if (!prUrl) {
20419
+ const moduleName = branch.replace(`feat/cycle-${cycleNum}-`, "");
20420
+ const prCreate = createPullRequest(
20421
+ config2.projectRoot,
20422
+ branch,
20423
+ baseBranch,
20424
+ `feat(cycle-${cycleNum}): merge shared cycle branch \u2014 ${moduleName}`,
20425
+ `Automated PR created at release time for shared cycle branch \`${branch}\` (Cycle ${cycleNum}).`
20426
+ );
20427
+ if (!prCreate.success) {
20428
+ results.push({ branch, prUrl: null, status: "failed", message: prCreate.message });
20429
+ continue;
20430
+ }
20431
+ prUrl = prCreate.message;
20432
+ }
20433
+ const merge = squashMergePullRequest(config2.projectRoot, branch);
20434
+ if (merge.success) {
20435
+ deleteLocalBranch(config2.projectRoot, branch);
20436
+ }
20437
+ results.push({ branch, prUrl, status: merge.success ? "merged" : "failed", message: merge.message });
20438
+ }
20439
+ return results;
20440
+ }
20441
+ async function createRelease(config2, branch, version, adapter2, cycleNum) {
19834
20442
  if (!isGitAvailable()) {
19835
20443
  throw new Error("git is not available.");
19836
20444
  }
@@ -19841,10 +20449,13 @@ async function createRelease(config2, branch, version, adapter2) {
19841
20449
  throw new Error("working directory has uncommitted changes. Commit or stash them before releasing.");
19842
20450
  }
19843
20451
  const warnings = [];
20452
+ const resolvedCycleNum = cycleNum && cycleNum > 0 ? cycleNum : (() => {
20453
+ const m = version.match(/^v0\.(\d+)\./);
20454
+ return m ? parseInt(m[1], 10) : 0;
20455
+ })();
19844
20456
  if (adapter2) {
19845
20457
  try {
19846
- const versionMatch = version.match(/^v0\.(\d+)\./);
19847
- const currentCycle = versionMatch ? parseInt(versionMatch[1], 10) : 0;
20458
+ const currentCycle = resolvedCycleNum;
19848
20459
  if (currentCycle > 0) {
19849
20460
  await adapter2.createCycle({
19850
20461
  id: `cycle-${currentCycle}`,
@@ -19873,6 +20484,24 @@ async function createRelease(config2, branch, version, adapter2) {
19873
20484
  warnings.push(`git pull failed: ${pull.message}. Run manually.`);
19874
20485
  }
19875
20486
  }
20487
+ let groupedBranchMerges;
20488
+ if (resolvedCycleNum > 0) {
20489
+ const mergeResults = mergeGroupedCycleBranches(config2, resolvedCycleNum, branch);
20490
+ if (mergeResults.length > 0) {
20491
+ groupedBranchMerges = mergeResults;
20492
+ const failures = mergeResults.filter((r) => r.status === "failed");
20493
+ if (failures.length > 0) {
20494
+ const detail = failures.map((r) => `${r.branch}: ${r.message}`).join("; ");
20495
+ throw new Error(`Release blocked: grouped cycle branch merge failed \u2014 ${detail}. Resolve conflicts and retry release.`);
20496
+ }
20497
+ if (hasRemote(config2.projectRoot)) {
20498
+ const repull = gitPull(config2.projectRoot);
20499
+ if (!repull.success) {
20500
+ warnings.push(`Post-merge pull failed: ${repull.message}. Run manually.`);
20501
+ }
20502
+ }
20503
+ }
20504
+ }
19876
20505
  if (tagExists(config2.projectRoot, version)) {
19877
20506
  throw new Error(`tag "${version}" already exists. Use a different version.`);
19878
20507
  }
@@ -19909,7 +20538,8 @@ async function createRelease(config2, branch, version, adapter2) {
19909
20538
  commitNote,
19910
20539
  tagMessage: tagResult.message,
19911
20540
  pushNotes,
19912
- warnings: warnings.length > 0 ? warnings : void 0
20541
+ warnings: warnings.length > 0 ? warnings : void 0,
20542
+ ...groupedBranchMerges ? { groupedBranchMerges } : {}
19913
20543
  };
19914
20544
  }
19915
20545
 
@@ -19965,7 +20595,9 @@ async function handleRelease(adapter2, config2, args) {
19965
20595
  return errorResponse(`version must start with "v" (got "${version}"). Example: "v0.1.0-alpha"`);
19966
20596
  }
19967
20597
  try {
19968
- const result = await createRelease(config2, branch, version, adapter2);
20598
+ const cycleMatch = version.match(/^v0\.(\d+)\./);
20599
+ const cycleNum = cycleMatch ? parseInt(cycleMatch[1], 10) : void 0;
20600
+ const result = await createRelease(config2, branch, version, adapter2, cycleNum);
19969
20601
  const lines = [
19970
20602
  `## Release ${result.version}`,
19971
20603
  "",
@@ -19978,13 +20610,17 @@ async function handleRelease(adapter2, config2, args) {
19978
20610
  lines.push("", "---", "");
19979
20611
  lines.push(...result.pushNotes);
19980
20612
  }
20613
+ if (result.groupedBranchMerges?.length) {
20614
+ lines.push("", "**Shared cycle branches merged:**");
20615
+ for (const m of result.groupedBranchMerges) {
20616
+ lines.push(`- \`${m.branch}\` \u2014 ${m.message}${m.prUrl ? ` (${m.prUrl})` : ""}`);
20617
+ }
20618
+ }
19981
20619
  if (result.warnings?.length) {
19982
20620
  lines.push("", "\u26A0\uFE0F Warnings: " + result.warnings.join("; "));
19983
20621
  }
19984
20622
  try {
19985
- const cycleMatch = version.match(/^v0\.(\d+)\./);
19986
- const cycleNum = cycleMatch ? parseInt(cycleMatch[1], 10) : 0;
19987
- if (cycleNum > 0) {
20623
+ if (cycleNum && cycleNum > 0) {
19988
20624
  const reports = await adapter2.getBuildReportsSince(cycleNum);
19989
20625
  const EMPTY = /* @__PURE__ */ new Set(["None", "none", "N/A", "", "null"]);
19990
20626
  const issues = reports.filter((r) => r.discoveredIssues && !EMPTY.has(r.discoveredIssues.trim())).map((r) => `- **${r.taskId}** (${r.taskName}): ${r.discoveredIssues}`);
@@ -19997,10 +20633,10 @@ async function handleRelease(adapter2, config2, args) {
19997
20633
  }
19998
20634
  if (rawObservations && rawObservations.length > 0 && adapter2.writeDogfoodEntries) {
19999
20635
  try {
20000
- const cycleMatch = version.match(/^v0\.(\d+)\./);
20001
- const cycleNum = cycleMatch ? parseInt(cycleMatch[1], 10) : 0;
20636
+ const cycleMatch2 = version.match(/^v0\.(\d+)\./);
20637
+ const cycleNum2 = cycleMatch2 ? parseInt(cycleMatch2[1], 10) : 0;
20002
20638
  const entries = rawObservations.map((obs) => ({
20003
- cycleNumber: cycleNum,
20639
+ cycleNumber: cycleNum2,
20004
20640
  category: obs.category,
20005
20641
  content: obs.content,
20006
20642
  sourceTool: "release",
@@ -20256,6 +20892,10 @@ function mergeAfterAccept(config2, taskId) {
20256
20892
  }
20257
20893
  const featureBranch = taskBranchName(taskId);
20258
20894
  const baseBranch = resolveBaseBranch(config2.projectRoot, config2.baseBranch);
20895
+ if (!branchExists(config2.projectRoot, featureBranch)) {
20896
+ lines.push(`Task is on a shared cycle branch \u2014 will be merged at release time.`);
20897
+ return lines;
20898
+ }
20259
20899
  const papiDir = join7(config2.projectRoot, ".papi");
20260
20900
  if (existsSync4(papiDir)) {
20261
20901
  try {
@@ -20431,8 +21071,9 @@ ${result.handoffRegenPrompt.userMessage}
20431
21071
  }
20432
21072
  const version = `v0.${result.currentCycle}.0`;
20433
21073
  const baseBranch = resolveBaseBranch(config2.projectRoot, config2.baseBranch);
20434
- const releaseResult = await createRelease(config2, baseBranch, version, adapter2);
21074
+ const releaseResult = await createRelease(config2, baseBranch, version, adapter2, result.currentCycle);
20435
21075
  const pushInfo = releaseResult.pushNotes.join(" ");
21076
+ const groupedMergeNote = releaseResult.groupedBranchMerges?.length ? "\n" + releaseResult.groupedBranchMerges.map((r) => `- Merged shared branch \`${r.branch}\` via PR: ${r.prUrl ?? "n/a"}`).join("\n") : "";
20436
21077
  autoReleaseNote = `
20437
21078
 
20438
21079
  ---
@@ -20442,7 +21083,7 @@ ${result.handoffRegenPrompt.userMessage}
20442
21083
  - Version: **${releaseResult.version}**
20443
21084
  - ${releaseResult.commitNote}
20444
21085
  - ${releaseResult.tagMessage}
20445
- - ${pushInfo}` + (releaseResult.warnings?.length ? `
21086
+ - ${pushInfo}` + groupedMergeNote + (releaseResult.warnings?.length ? `
20446
21087
  - Warnings: ${releaseResult.warnings.join(", ")}` : "") + `
20447
21088
 
20448
21089
  Run \`plan\` to create Cycle ${result.currentCycle + 1}.`;
@@ -20689,7 +21330,7 @@ function countByStatus(tasks) {
20689
21330
  async function getHealthSummary(adapter2) {
20690
21331
  const health = await adapter2.getCycleHealth();
20691
21332
  const activeTasks = await adapter2.queryBoard({
20692
- status: ["Backlog", "In Cycle", "Ready", "In Progress", "In Review", "Blocked"]
21333
+ status: ["Backlog", "In Cycle", "Ready", "In Progress", "In Review", "Blocked", "Deferred"]
20693
21334
  });
20694
21335
  const logEntries = await adapter2.getCycleLog(3);
20695
21336
  const cycleNumber = health.totalCycles;
@@ -20809,7 +21450,7 @@ ${lines.join("\n")}`;
20809
21450
  }
20810
21451
  let decisionLifecycleSection = "";
20811
21452
  try {
20812
- const decisions = await adapter2.getActiveDecisions();
21453
+ const decisions = await adapter2.getActiveDecisions({ includeRetired: true });
20813
21454
  const lifecycleSummary = formatDecisionLifecycleSummary(decisions);
20814
21455
  if (lifecycleSummary) {
20815
21456
  decisionLifecycleSection = `**Lifecycle:** ${lifecycleSummary}`;
@@ -21575,7 +22216,19 @@ ${versionDrift}` : "";
21575
22216
  const learnings = await adapter2.getCycleLearnings?.({ category: "issue", limit: 30 });
21576
22217
  if (learnings) {
21577
22218
  const byRecency = (a, b2) => (b2.createdAt ?? "").localeCompare(a.createdAt ?? "");
21578
- const unactionedAll = learnings.filter((l) => !l.actionTaken).map((l) => ({ ...l, severity: l.severity ?? "P3" }));
22219
+ const candidateLearnings = learnings.filter((l) => !l.actionTaken);
22220
+ const referencedTaskIds = Array.from(new Set(candidateLearnings.map((l) => l.taskId).filter(Boolean)));
22221
+ let closedTaskIds = /* @__PURE__ */ new Set();
22222
+ if (referencedTaskIds.length > 0) {
22223
+ try {
22224
+ const tasks = await adapter2.getTasks(referencedTaskIds);
22225
+ closedTaskIds = new Set(
22226
+ tasks.filter((t) => t.status === "Done" || t.status === "Cancelled").map((t) => t.id)
22227
+ );
22228
+ } catch {
22229
+ }
22230
+ }
22231
+ const unactionedAll = candidateLearnings.filter((l) => !closedTaskIds.has(l.taskId)).map((l) => ({ ...l, severity: l.severity ?? "P3" }));
21579
22232
  const allAlerts = unactionedAll.filter((l) => l.severity === "P0" || l.severity === "P1").sort(byRecency);
21580
22233
  const allLowSev = unactionedAll.filter((l) => l.severity === "P2" || l.severity === "P3").sort(byRecency);
21581
22234
  const totalP2 = allLowSev.filter((l) => l.severity === "P2").length;
@@ -22488,16 +23141,19 @@ function emitToolCall(projectId, toolName, durationMs, extra) {
22488
23141
  metadata: { duration_ms: durationMs, ...extra }
22489
23142
  });
22490
23143
  }
22491
- function emitMdAdapterPing(toolName, extra) {
23144
+ function emitMdAdapterPing(toolName, extra, userId, projectSlug) {
22492
23145
  if (!isEnabled()) return;
22493
23146
  const installId = getInstallId();
22494
23147
  if (!installId) return;
23148
+ const resolvedUserId = userId ?? process.env["PAPI_USER_ID"] ?? void 0;
22495
23149
  const body = {
22496
23150
  install_id: installId,
22497
23151
  tool_name: toolName,
22498
23152
  papi_version: process.env["npm_package_version"] ?? null,
22499
23153
  metadata: extra ?? {}
22500
23154
  };
23155
+ if (resolvedUserId) body["user_id"] = resolvedUserId;
23156
+ if (projectSlug) body["project_slug"] = projectSlug;
22501
23157
  fetch(`${TELEMETRY_SUPABASE_URL}/rest/v1/md_adapter_pings`, {
22502
23158
  method: "POST",
22503
23159
  headers: {
@@ -22525,6 +23181,7 @@ var TOOLS_REQUIRING_PAPI = /* @__PURE__ */ new Set([
22525
23181
  "plan",
22526
23182
  "strategy_review",
22527
23183
  "strategy_change",
23184
+ "strategy_agenda",
22528
23185
  "board_view",
22529
23186
  "board_deprioritise",
22530
23187
  "board_archive",
@@ -22617,6 +23274,7 @@ function createServer(adapter2, config2) {
22617
23274
  planTool,
22618
23275
  strategyReviewTool,
22619
23276
  strategyChangeTool,
23277
+ strategyAgendaTool,
22620
23278
  boardViewTool,
22621
23279
  boardDeprioritiseTool,
22622
23280
  boardArchiveTool,
@@ -22683,6 +23341,9 @@ function createServer(adapter2, config2) {
22683
23341
  case "strategy_change":
22684
23342
  result = await handleStrategyChange(adapter2, config2, safeArgs);
22685
23343
  break;
23344
+ case "strategy_agenda":
23345
+ result = await handleStrategyAgenda(adapter2, config2, safeArgs);
23346
+ break;
22686
23347
  case "board_view":
22687
23348
  result = await handleBoardView(adapter2, safeArgs);
22688
23349
  break;
@@ -22777,7 +23438,8 @@ function createServer(adapter2, config2) {
22777
23438
  } catch {
22778
23439
  }
22779
23440
  if (config2.adapterType === "md") {
22780
- emitMdAdapterPing(name, { duration_ms: elapsed, success: !isError });
23441
+ const mdProjectSlug = config2.projectRoot ? config2.projectRoot.split("/").pop() : void 0;
23442
+ emitMdAdapterPing(name, { duration_ms: elapsed, success: !isError }, config2.userId, mdProjectSlug);
22781
23443
  }
22782
23444
  const telemetryProjectId = process.env["PAPI_PROJECT_ID"];
22783
23445
  if (telemetryProjectId) {