@papi-ai/server 0.7.11 → 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
@@ -1453,7 +1453,12 @@ var init_dist2 = __esm({
1453
1453
  research: 2,
1454
1454
  spike: 2,
1455
1455
  idea: 3,
1456
- discovery: 1
1456
+ discovery: 1,
1457
+ // Non-code brief types for non-technical Owners (AD-12)
1458
+ "design-brief": 1,
1459
+ "research-brief": 1,
1460
+ "marketing-brief": 1,
1461
+ "ops-brief": 1
1457
1462
  };
1458
1463
  VALID_EFFORT_SIZES = /* @__PURE__ */ new Set(["XS", "S", "M", "L", "XL"]);
1459
1464
  SECTION_HEADERS = [
@@ -1575,11 +1580,13 @@ ${TABLE_SEPARATOR}
1575
1580
  }
1576
1581
  /** Prepend a new cycle log entry at the top of the Cycle Log section. */
1577
1582
  async writeCycleLogEntry(entry) {
1578
- if (!entry.uuid) {
1579
- entry = { ...entry, uuid: randomUUID6() };
1580
- }
1583
+ const patched = {
1584
+ ...entry,
1585
+ uuid: entry.uuid || randomUUID6(),
1586
+ date: entry.date ?? (/* @__PURE__ */ new Date()).toISOString()
1587
+ };
1581
1588
  const content = await this.read("SPRINT_LOG.md");
1582
- await this.write("SPRINT_LOG.md", prependCycleLogEntry(entry, content));
1589
+ await this.write("SPRINT_LOG.md", prependCycleLogEntry(patched, content));
1583
1590
  }
1584
1591
  /** Write a strategy review — for md adapter, delegates to cycle log. */
1585
1592
  async writeStrategyReview(review) {
@@ -2061,6 +2068,102 @@ ${footer}`);
2061
2068
  await this.write("STRATEGY_RECOMMENDATIONS.md", updated);
2062
2069
  }
2063
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
+ // -------------------------------------------------------------------------
2064
2167
  // Decision Events & Scores (markdown persistence)
2065
2168
  // -------------------------------------------------------------------------
2066
2169
  async appendDecisionEvent(event) {
@@ -3708,7 +3811,7 @@ var init_connection = __esm({
3708
3811
 
3709
3812
  // ../../node_modules/postgres/src/subscribe.js
3710
3813
  function Subscribe(postgres2, options) {
3711
- const subscribers = /* @__PURE__ */ new Map(), slot = "postgresjs_" + Math.random().toString(36).slice(2), state = {};
3814
+ const subscribers = /* @__PURE__ */ new Map(), slot = "postgresjs_" + Math.random().toString(36).slice(2), state2 = {};
3712
3815
  let connection2, stream, ended = false;
3713
3816
  const sql = subscribe.sql = postgres2({
3714
3817
  ...options,
@@ -3725,7 +3828,7 @@ function Subscribe(postgres2, options) {
3725
3828
  if (ended)
3726
3829
  return;
3727
3830
  stream = null;
3728
- state.pid = state.secret = void 0;
3831
+ state2.pid = state2.secret = void 0;
3729
3832
  connected(await init(sql, slot, options.publications));
3730
3833
  subscribers.forEach((event) => event.forEach(({ onsubscribe }) => onsubscribe()));
3731
3834
  },
@@ -3756,13 +3859,13 @@ function Subscribe(postgres2, options) {
3756
3859
  connected(x);
3757
3860
  onsubscribe();
3758
3861
  stream && stream.on("error", onerror);
3759
- return { unsubscribe, state, sql };
3862
+ return { unsubscribe, state: state2, sql };
3760
3863
  });
3761
3864
  }
3762
3865
  function connected(x) {
3763
3866
  stream = x.stream;
3764
- state.pid = x.state.pid;
3765
- state.secret = x.state.secret;
3867
+ state2.pid = x.state.pid;
3868
+ state2.secret = x.state.secret;
3766
3869
  }
3767
3870
  async function init(sql2, slot2, publications) {
3768
3871
  if (!publications)
@@ -3774,7 +3877,7 @@ function Subscribe(postgres2, options) {
3774
3877
  const stream2 = await sql2.unsafe(
3775
3878
  `START_REPLICATION SLOT ${slot2} LOGICAL ${x.consistent_point} (proto_version '1', publication_names '${publications}')`
3776
3879
  ).writable();
3777
- const state2 = {
3880
+ const state3 = {
3778
3881
  lsn: Buffer.concat(x.consistent_point.split("/").map((x2) => Buffer.from(("00000000" + x2).slice(-8), "hex")))
3779
3882
  };
3780
3883
  stream2.on("data", data);
@@ -3786,9 +3889,9 @@ function Subscribe(postgres2, options) {
3786
3889
  }
3787
3890
  function data(x2) {
3788
3891
  if (x2[0] === 119) {
3789
- parse(x2.subarray(25), state2, sql2.options.parsers, handle, options.transform);
3892
+ parse(x2.subarray(25), state3, sql2.options.parsers, handle, options.transform);
3790
3893
  } else if (x2[0] === 107 && x2[17]) {
3791
- state2.lsn = x2.subarray(1, 9);
3894
+ state3.lsn = x2.subarray(1, 9);
3792
3895
  pong();
3793
3896
  }
3794
3897
  }
@@ -3804,7 +3907,7 @@ function Subscribe(postgres2, options) {
3804
3907
  function pong() {
3805
3908
  const x2 = Buffer.alloc(34);
3806
3909
  x2[0] = "r".charCodeAt(0);
3807
- x2.fill(state2.lsn, 1);
3910
+ x2.fill(state3.lsn, 1);
3808
3911
  x2.writeBigInt64BE(BigInt(Date.now() - Date.UTC(2e3, 0, 1)) * BigInt(1e3), 25);
3809
3912
  stream2.write(x2);
3810
3913
  }
@@ -3816,12 +3919,12 @@ function Subscribe(postgres2, options) {
3816
3919
  function Time(x) {
3817
3920
  return new Date(Date.UTC(2e3, 0, 1) + Number(x / BigInt(1e3)));
3818
3921
  }
3819
- function parse(x, state, parsers2, handle, transform) {
3922
+ function parse(x, state2, parsers2, handle, transform) {
3820
3923
  const char = (acc, [k, v]) => (acc[k.charCodeAt(0)] = v, acc);
3821
3924
  Object.entries({
3822
3925
  R: (x2) => {
3823
3926
  let i = 1;
3824
- const r = state[x2.readUInt32BE(i)] = {
3927
+ const r = state2[x2.readUInt32BE(i)] = {
3825
3928
  schema: x2.toString("utf8", i += 4, i = x2.indexOf(0, i)) || "pg_catalog",
3826
3929
  table: x2.toString("utf8", i + 1, i = x2.indexOf(0, i + 1)),
3827
3930
  columns: Array(x2.readUInt16BE(i += 2)),
@@ -3848,12 +3951,12 @@ function parse(x, state, parsers2, handle, transform) {
3848
3951
  },
3849
3952
  // Origin
3850
3953
  B: (x2) => {
3851
- state.date = Time(x2.readBigInt64BE(9));
3852
- state.lsn = x2.subarray(1, 9);
3954
+ state2.date = Time(x2.readBigInt64BE(9));
3955
+ state2.lsn = x2.subarray(1, 9);
3853
3956
  },
3854
3957
  I: (x2) => {
3855
3958
  let i = 1;
3856
- const relation = state[x2.readUInt32BE(i)];
3959
+ const relation = state2[x2.readUInt32BE(i)];
3857
3960
  const { row } = tuples(x2, relation.columns, i += 7, transform);
3858
3961
  handle(row, {
3859
3962
  command: "insert",
@@ -3862,7 +3965,7 @@ function parse(x, state, parsers2, handle, transform) {
3862
3965
  },
3863
3966
  D: (x2) => {
3864
3967
  let i = 1;
3865
- const relation = state[x2.readUInt32BE(i)];
3968
+ const relation = state2[x2.readUInt32BE(i)];
3866
3969
  i += 4;
3867
3970
  const key = x2[i] === 75;
3868
3971
  handle(
@@ -3876,7 +3979,7 @@ function parse(x, state, parsers2, handle, transform) {
3876
3979
  },
3877
3980
  U: (x2) => {
3878
3981
  let i = 1;
3879
- const relation = state[x2.readUInt32BE(i)];
3982
+ const relation = state2[x2.readUInt32BE(i)];
3880
3983
  i += 4;
3881
3984
  const key = x2[i] === 75;
3882
3985
  const xs = key || x2[i] === 79 ? tuples(x2, relation.columns, i += 3, transform) : null;
@@ -4612,6 +4715,7 @@ function rowToCycleLogEntry(row) {
4612
4715
  if (row.notes != null) entry.notes = row.notes;
4613
4716
  if (row.task_count != null) entry.taskCount = row.task_count;
4614
4717
  if (row.effort_points != null) entry.effortPoints = row.effort_points;
4718
+ if (row.updated_at != null) entry.date = row.updated_at;
4615
4719
  return entry;
4616
4720
  }
4617
4721
  function rowToPhase(row) {
@@ -6276,7 +6380,7 @@ EXCEPTION WHEN duplicate_object THEN NULL; END $$;
6276
6380
  async getCycleLog(limit) {
6277
6381
  if (limit != null) {
6278
6382
  const rows2 = await this.sql`
6279
- SELECT id, cycle_number, title, content, carry_forward, notes, task_count, effort_points
6383
+ SELECT id, cycle_number, title, content, carry_forward, notes, task_count, effort_points, updated_at
6280
6384
  FROM planning_log_entries
6281
6385
  WHERE project_id = ${this.projectId}
6282
6386
  ORDER BY cycle_number DESC
@@ -6285,7 +6389,7 @@ EXCEPTION WHEN duplicate_object THEN NULL; END $$;
6285
6389
  return rows2.map(rowToCycleLogEntry);
6286
6390
  }
6287
6391
  const rows = await this.sql`
6288
- SELECT id, cycle_number, title, content, carry_forward, notes, task_count, effort_points
6392
+ SELECT id, cycle_number, title, content, carry_forward, notes, task_count, effort_points, updated_at
6289
6393
  FROM planning_log_entries
6290
6394
  WHERE project_id = ${this.projectId}
6291
6395
  ORDER BY cycle_number DESC
@@ -6295,7 +6399,7 @@ EXCEPTION WHEN duplicate_object THEN NULL; END $$;
6295
6399
  }
6296
6400
  async getCycleLogSince(cycleNumber) {
6297
6401
  const rows = await this.sql`
6298
- SELECT id, cycle_number, title, content, carry_forward, notes, task_count, effort_points
6402
+ SELECT id, cycle_number, title, content, carry_forward, notes, task_count, effort_points, updated_at
6299
6403
  FROM planning_log_entries
6300
6404
  WHERE project_id = ${this.projectId}
6301
6405
  AND cycle_number >= ${cycleNumber}
@@ -6346,7 +6450,8 @@ ${newParts.join("\n")}` : newParts.join("\n");
6346
6450
  carry_forward = EXCLUDED.carry_forward,
6347
6451
  notes = EXCLUDED.notes,
6348
6452
  task_count = EXCLUDED.task_count,
6349
- effort_points = EXCLUDED.effort_points
6453
+ effort_points = EXCLUDED.effort_points,
6454
+ updated_at = now()
6350
6455
  `;
6351
6456
  }
6352
6457
  async writeStrategyReview(review) {
@@ -7560,6 +7665,50 @@ ${newParts.join("\n")}` : newParts.join("\n");
7560
7665
  UPDATE strategy_recommendations
7561
7666
  SET status = 'actioned', dismissal_reason = ${reason}, updated_at = now()
7562
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[])
7563
7712
  `;
7564
7713
  }
7565
7714
  // -------------------------------------------------------------------------
@@ -7813,7 +7962,8 @@ ${r.content}` + (r.carry_forward ? `
7813
7962
  await this.sql`
7814
7963
  INSERT INTO plan_runs (
7815
7964
  project_id, cycle_number, context_bytes, duration_ms,
7816
- task_count_in, task_count_out, backlog_depth, notes
7965
+ task_count_in, task_count_out, backlog_depth, notes,
7966
+ token_usage, source
7817
7967
  ) VALUES (
7818
7968
  ${this.projectId},
7819
7969
  ${entry.cycleNumber},
@@ -7822,7 +7972,9 @@ ${r.content}` + (r.carry_forward ? `
7822
7972
  ${entry.taskCountIn ?? null},
7823
7973
  ${entry.taskCountOut ?? null},
7824
7974
  ${entry.backlogDepth ?? null},
7825
- ${entry.notes ?? null}
7975
+ ${entry.notes ?? null},
7976
+ ${entry.tokenUsage ? this.sql.json(entry.tokenUsage) : null},
7977
+ ${entry.source ?? null}
7826
7978
  )
7827
7979
  `;
7828
7980
  }
@@ -7958,7 +8110,8 @@ ${r.content}` + (r.carry_forward ? `
7958
8110
  title = EXCLUDED.title,
7959
8111
  content = EXCLUDED.content,
7960
8112
  carry_forward = EXCLUDED.carry_forward,
7961
- notes = EXCLUDED.notes
8113
+ notes = EXCLUDED.notes,
8114
+ updated_at = now()
7962
8115
  `;
7963
8116
  if (payload.healthUpdates.boardHealth != null || payload.healthUpdates.strategicDirection != null) {
7964
8117
  const [latest] = await tx`
@@ -8748,6 +8901,7 @@ __export(git_exports, {
8748
8901
  getHeadCommitSha: () => getHeadCommitSha,
8749
8902
  getLatestTag: () => getLatestTag,
8750
8903
  getOriginRepoSlug: () => getOriginRepoSlug,
8904
+ getPullRequestUrl: () => getPullRequestUrl,
8751
8905
  getUnmergedBranches: () => getUnmergedBranches,
8752
8906
  gitPull: () => gitPull,
8753
8907
  gitPush: () => gitPush,
@@ -8757,9 +8911,11 @@ __export(git_exports, {
8757
8911
  isGhAvailable: () => isGhAvailable,
8758
8912
  isGitAvailable: () => isGitAvailable,
8759
8913
  isGitRepo: () => isGitRepo,
8914
+ listGroupedCycleBranches: () => listGroupedCycleBranches,
8760
8915
  mergePullRequest: () => mergePullRequest,
8761
8916
  resolveBaseBranch: () => resolveBaseBranch,
8762
8917
  runAutoCommit: () => runAutoCommit,
8918
+ squashMergePullRequest: () => squashMergePullRequest,
8763
8919
  stageAllAndCommit: () => stageAllAndCommit,
8764
8920
  stageDirAndCommit: () => stageDirAndCommit,
8765
8921
  tagExists: () => tagExists,
@@ -9213,6 +9369,68 @@ function runAutoCommit(projectRoot, commitFn) {
9213
9369
  return `Auto-commit failed: ${err instanceof Error ? err.message : String(err)}`;
9214
9370
  }
9215
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
+ }
9216
9434
  function getFilesChangedFromBase(cwd, baseBranch) {
9217
9435
  try {
9218
9436
  const mergeBase = execFileSync("git", ["merge-base", baseBranch, "HEAD"], { cwd, encoding: "utf-8" }).trim();
@@ -9234,9 +9452,9 @@ var init_git = __esm({
9234
9452
  });
9235
9453
 
9236
9454
  // src/index.ts
9237
- import { readFileSync as readFileSync4 } from "fs";
9455
+ import { readFileSync as readFileSync6 } from "fs";
9238
9456
  import { createServer as createHttpServer } from "http";
9239
- import { dirname as dirname2, join as join11 } from "path";
9457
+ import { dirname as dirname2, join as join13 } from "path";
9240
9458
  import { fileURLToPath as fileURLToPath2 } from "url";
9241
9459
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
9242
9460
  import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
@@ -9264,11 +9482,32 @@ function loadConfig() {
9264
9482
  const lightMode = process.env.PAPI_LIGHT_MODE === "true";
9265
9483
  const projectOwner = process.env.PAPI_OWNER ?? "Cathal";
9266
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";
9267
9487
  const papiEndpoint = process.env.PAPI_ENDPOINT;
9268
9488
  const dataEndpoint = process.env.PAPI_DATA_ENDPOINT;
9269
9489
  const databaseUrl = process.env.DATABASE_URL;
9270
9490
  const explicitAdapter = process.env.PAPI_ADAPTER;
9271
- const adapterType = papiEndpoint ? "pg" : databaseUrl && explicitAdapter === "pg" ? "pg" : dataEndpoint ? "proxy" : explicitAdapter ? explicitAdapter : databaseUrl ? "pg" : "proxy";
9491
+ const projectId = process.env.PAPI_PROJECT_ID;
9492
+ let adapterType = papiEndpoint ? "pg" : databaseUrl && explicitAdapter === "pg" ? "pg" : dataEndpoint ? "proxy" : explicitAdapter ? explicitAdapter : databaseUrl ? "pg" : "proxy";
9493
+ if (projectId && !databaseUrl && !papiEndpoint && adapterType === "md") {
9494
+ adapterType = "proxy";
9495
+ console.error("[papi] PAPI_PROJECT_ID detected \u2014 switching to proxy adapter (md adapter blocked for external users).");
9496
+ }
9497
+ if (adapterType === "md" && !userId) {
9498
+ throw new Error(
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.`
9509
+ );
9510
+ }
9272
9511
  return {
9273
9512
  projectRoot,
9274
9513
  papiDir: path.join(projectRoot, ".papi"),
@@ -9280,7 +9519,9 @@ function loadConfig() {
9280
9519
  papiEndpoint,
9281
9520
  lightMode,
9282
9521
  projectOwner,
9283
- skipProjectSpecificRules
9522
+ skipProjectSpecificRules,
9523
+ userId,
9524
+ telemetryEnabled
9284
9525
  };
9285
9526
  }
9286
9527
 
@@ -9373,7 +9614,25 @@ async function createAdapter(optionsOrType, maybePapiDir) {
9373
9614
  console.error("[papi] Set PAPI_USER_ID in your .mcp.json env to fix this.");
9374
9615
  }
9375
9616
  }
9376
- 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
+ }
9377
9636
  }
9378
9637
  await pgAdapter.close();
9379
9638
  } catch {
@@ -9482,8 +9741,9 @@ Check PAPI_PROJECT_ID in your .mcp.json config. Find your project ID in the PAPI
9482
9741
  }
9483
9742
 
9484
9743
  // src/server.ts
9744
+ import { readFileSync as readFileSync5 } from "fs";
9485
9745
  import { access as access4, readdir as readdir2, readFile as readFile5 } from "fs/promises";
9486
- import { join as join10, dirname } from "path";
9746
+ import { join as join12, dirname } from "path";
9487
9747
  import { fileURLToPath } from "url";
9488
9748
  import { Server } from "@modelcontextprotocol/sdk/server/index.js";
9489
9749
  import {
@@ -9689,6 +9949,17 @@ ${formatted}`;
9689
9949
  }
9690
9950
  return sections.join("\n\n");
9691
9951
  }
9952
+ function formatCandidateTaskFullNotes(tasks) {
9953
+ const candidates = tasks.filter((t) => !PLAN_EXCLUDED_STATUSES.has(t.status)).filter((t) => (t.notes?.length ?? 0) > PLAN_NOTES_MAX_LENGTH);
9954
+ if (candidates.length === 0) return void 0;
9955
+ const lines = candidates.map((t) => `**${t.id}** \u2014 ${t.title}
9956
+ ${t.notes}`);
9957
+ return [
9958
+ `${candidates.length} candidate task(s) have notes longer than ${PLAN_NOTES_MAX_LENGTH} chars. Full untruncated notes below \u2014 reference these when generating BUILD HANDOFFs so submitter context, constraints, and reasoning are preserved. The Board section above uses truncated notes for concise task selection; this section supplies the missing detail for tasks you choose to schedule.`,
9959
+ "",
9960
+ ...lines
9961
+ ].join("\n\n");
9962
+ }
9692
9963
  function formatBoardForReview(tasks) {
9693
9964
  if (tasks.length === 0) return "No tasks on the board.";
9694
9965
  return tasks.map(
@@ -10510,6 +10781,7 @@ Standard planning cycle with full board review.
10510
10781
  **Security section guidance:** Each handoff includes a SECURITY CONSIDERATIONS section. Populate it when the task involves: data exposure risks (PII, secrets in logs/storage), secrets or credentials handling (API keys, tokens, env vars), auth/access control changes, or dependency security risks (new packages, version changes). For pure refactoring, documentation, prompt-text, or UI-only tasks, write "None \u2014 no security-relevant changes".
10511
10782
  **Estimation calibration:** Estimate **XS** for: copy/text-only changes, single string replacements, config tweaks, and any task where the scope is "change words in an existing file" with no logic changes. Estimate **S** for: wiring existing adapter methods, adding API routes following established patterns, modifying prompts, or documentation-only changes. Default to S for pattern-following work. Only use M when genuine new architecture, new DB tables, or multi-file architectural changes are needed. Historical data shows systematic over-estimation (198 over vs 8 under out of 528 tasks) \u2014 when in doubt, estimate smaller. If an "Estimation Calibration (Historical)" section is provided in the context below, use its data to adjust your estimates \u2014 it shows how often each estimated size matched the actual effort. Pay special attention to systematic over/under-estimation patterns (e.g. if M\u2192S happens frequently, estimate S instead of M for similar work).
10512
10783
  **Reference docs:** If a task's notes include a \`Reference:\` path (e.g. \`Reference: docs/architecture/papi-brain-v1.md\`), include a REFERENCE DOCS section in the BUILD HANDOFF with those paths. This tells the builder to read the referenced doc for background context before implementing. Do NOT omit or summarise the reference \u2014 pass it through so the builder can access the full document. Only tasks with explicit \`Reference:\` paths in their notes should have this section.
10784
+ **Full notes lookup:** Notes in the Board section are truncated to 300 chars for concise task selection. When generating a BUILD HANDOFF for a task, check the "Full Notes for Candidate Tasks" section (if present in context) for that task's complete untruncated notes before writing SCOPE, SCOPE BOUNDARY, and PRE-MORTEM. Submitter context, constraints, and reasoning often live past the 300-char cutoff and must not be dropped from the handoff.
10513
10785
  **Pre-build verification:** EVERY handoff MUST include a PRE-BUILD VERIFICATION section listing 2-5 specific file paths the builder should read before implementing. Derive these from FILES LIKELY TOUCHED \u2014 pick the files most likely to already contain the target functionality. This is the #1 prevention mechanism for wasted build slots (C120, C125, C126 all scheduled already-shipped work). If the builder finds >80% of the scope already implemented, they report "already built" instead of re-implementing.
10514
10786
  **Pre-mortem:** For projects with 10+ cycles, include a PRE-MORTEM section in every BUILD HANDOFF with 1-3 bullet points: (a) most likely technical blocker based on module history, (b) integration risk with adjacent systems, (c) scope creep signal \u2014 what the builder might be tempted to expand beyond scope. Draw from \`dead_ends\` and \`surprises\` in recent build reports for the same module. Omit this section entirely for projects with fewer than 10 cycles.
10515
10787
  **Build order in cycle log:** If any intra-cycle dependencies were detected in this cycle, include a "Build Order" paragraph in \`cycleLogNotes\` showing the recommended build sequence as arrow chains (e.g. "Build order: task-123 \u2192 task-124; task-130 standalone"). Skip this paragraph when no dependencies exist.
@@ -10740,6 +11012,7 @@ Standard planning cycle with full board review.
10740
11012
  **Security section guidance:** Each handoff includes a SECURITY CONSIDERATIONS section. Populate it when the task involves: data exposure risks (PII, secrets in logs/storage), secrets or credentials handling (API keys, tokens, env vars), auth/access control changes, or dependency security risks (new packages, version changes). For pure refactoring, documentation, prompt-text, or UI-only tasks, write "None \u2014 no security-relevant changes".
10741
11013
  **Estimation calibration:** Estimate **XS** for: copy/text-only changes, single string replacements, config tweaks, and any task where the scope is "change words in an existing file" with no logic changes. Estimate **S** for: wiring existing adapter methods, adding API routes following established patterns, modifying prompts, or documentation-only changes. Default to S for pattern-following work. Only use M when genuine new architecture, new DB tables, or multi-file architectural changes are needed. Historical data shows systematic over-estimation (198 over vs 8 under out of 528 tasks) \u2014 when in doubt, estimate smaller. If an "Estimation Calibration (Historical)" section is provided in the context below, use its data to adjust your estimates \u2014 it shows how often each estimated size matched the actual effort. Pay special attention to systematic over/under-estimation patterns (e.g. if M\u2192S happens frequently, estimate S instead of M for similar work).
10742
11014
  **Reference docs:** If a task's notes include a \`Reference:\` path (e.g. \`Reference: docs/architecture/papi-brain-v1.md\`), include a REFERENCE DOCS section in the BUILD HANDOFF with those paths. This tells the builder to read the referenced doc for background context before implementing. Do NOT omit or summarise the reference \u2014 pass it through so the builder can access the full document. Only tasks with explicit \`Reference:\` paths in their notes should have this section.
11015
+ **Full notes lookup:** Notes in the Board section are truncated to 300 chars for concise task selection. When generating a BUILD HANDOFF for a task, check the "Full Notes for Candidate Tasks" section (if present in context) for that task's complete untruncated notes before writing SCOPE, SCOPE BOUNDARY, and PRE-MORTEM. Submitter context, constraints, and reasoning often live past the 300-char cutoff and must not be dropped from the handoff.
10743
11016
  **Pre-build verification:** EVERY handoff MUST include a PRE-BUILD VERIFICATION section listing 2-5 specific file paths the builder should read before implementing. Derive these from FILES LIKELY TOUCHED \u2014 pick the files most likely to already contain the target functionality. This is the #1 prevention mechanism for wasted build slots (C120, C125, C126 all scheduled already-shipped work). If the builder finds >80% of the scope already implemented, they report "already built" instead of re-implementing.
10744
11017
  **Pre-mortem:** For projects with 10+ cycles, include a PRE-MORTEM section in every BUILD HANDOFF with 1-3 bullet points: (a) most likely technical blocker based on module history, (b) integration risk with adjacent systems, (c) scope creep signal \u2014 what the builder might be tempted to expand beyond scope. Draw from \`dead_ends\` and \`surprises\` in recent build reports for the same module. Omit this section entirely for projects with fewer than 10 cycles.
10745
11018
  **Intra-cycle dependency detection:** After selecting cycle tasks, check every pair for build-order dependencies. Two tasks A and B have an intra-cycle dependency when A must be built before B because B consumes an artifact A creates \u2014 e.g. A adds a new adapter method that B calls, A creates a DB migration B depends on, A introduces a new shared type B imports, A refactors a utility B modifies. Signals: same module + adjacent scope (one is "add X", another is "use X"), or notes explicitly reference the other task. For each dependency detected: (a) populate the DEPENDS ON section in the dependent task's BUILD HANDOFF with the upstream task ID(s); (b) add a \`boardCorrections\` entry for the dependent task with \`updates.dependsOn\` set to the comma-separated upstream IDs \u2014 this persists the dependency so the builder's runtime can reuse the upstream branch; (c) keep SCOPE sections independent but note the ordering in "Why now". Do NOT invent dependencies where tasks merely share a module \u2014 only real build-order coupling counts. Linear chains only \u2014 no multi-level graph resolution. When in doubt, omit.
@@ -10798,6 +11071,9 @@ function buildPlanUserMessage(ctx) {
10798
11071
  }) : PLAN_FULL_INSTRUCTIONS;
10799
11072
  parts.push(instructions);
10800
11073
  }
11074
+ if (ctx.foundationalTasksGuidance) {
11075
+ parts.push("", ctx.foundationalTasksGuidance);
11076
+ }
10801
11077
  if (ctx.skipHandoffs) {
10802
11078
  parts.push(
10803
11079
  "",
@@ -10843,6 +11119,9 @@ function buildPlanUserMessage(ctx) {
10843
11119
  if (ctx.board) {
10844
11120
  parts.push("### Board", "", ctx.board, "");
10845
11121
  }
11122
+ if (ctx.candidateTaskFullNotes) {
11123
+ parts.push("### Full Notes for Candidate Tasks", "", ctx.candidateTaskFullNotes, "");
11124
+ }
10846
11125
  if (ctx.preAssignedTasks) {
10847
11126
  parts.push("### Pre-Assigned Tasks", "", ctx.preAssignedTasks, "");
10848
11127
  }
@@ -11316,6 +11595,9 @@ function buildReviewUserMessage(ctx) {
11316
11595
  if (ctx.docActionStaleness) {
11317
11596
  parts.push("### Doc Action Staleness", "", ctx.docActionStaleness, "");
11318
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
+ }
11319
11601
  return parts.join("\n");
11320
11602
  }
11321
11603
  function parseReviewStructuredOutput(raw) {
@@ -12002,6 +12284,64 @@ function stripTasksForPlan(tasks) {
12002
12284
  hasHandoff: !!buildHandoff
12003
12285
  }));
12004
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
+ }
12005
12345
  function detectBoardFlags(tasks) {
12006
12346
  let hasBugTasks = false;
12007
12347
  let hasResearchTasks = false;
@@ -12238,6 +12578,10 @@ ${lines.join("\n")}`;
12238
12578
  if (reportsForCapsResult.status === "fulfilled") {
12239
12579
  recentlyShippedLean = formatRecentlyShippedCapabilities(reportsForCapsResult.value);
12240
12580
  }
12581
+ let candidateTaskFullNotesLean;
12582
+ if (preAssignedResult.status === "fulfilled") {
12583
+ candidateTaskFullNotesLean = formatCandidateTaskFullNotes(preAssignedResult.value);
12584
+ }
12241
12585
  logDataSourceSummary("plan (lean)", [
12242
12586
  { label: "cycleHealth", hasData: !!health },
12243
12587
  { label: "productBrief", hasData: warnIfEmpty("productBrief", productBrief) },
@@ -12275,7 +12619,9 @@ ${lines.join("\n")}`;
12275
12619
  carryForwardStaleness: carryForwardStalenessLean,
12276
12620
  preAssignedTasks: preAssignedTextLean,
12277
12621
  recentlyShippedCapabilities: recentlyShippedLean,
12278
- strategyReviewCadence
12622
+ strategyReviewCadence,
12623
+ candidateTaskFullNotes: candidateTaskFullNotesLean,
12624
+ foundationalTasksGuidance: computeFoundationalTasksGuidance(health.totalCycles, productBrief)
12279
12625
  };
12280
12626
  const { label: leanTierLabel } = applyContextTier(ctx2, health.totalCycles);
12281
12627
  ctx2.contextTier = leanTierLabel;
@@ -12436,7 +12782,9 @@ ${logLines}`);
12436
12782
  carryForwardStaleness: computeCarryForwardStaleness(log),
12437
12783
  preAssignedTasks: preAssignedText,
12438
12784
  recentlyShippedCapabilities: formatRecentlyShippedCapabilities(reports),
12439
- strategyReviewCadence: strategyReviewCadenceFull
12785
+ strategyReviewCadence: strategyReviewCadenceFull,
12786
+ candidateTaskFullNotes: formatCandidateTaskFullNotes(plannerTasks),
12787
+ foundationalTasksGuidance: computeFoundationalTasksGuidance(health.totalCycles, productBrief)
12440
12788
  };
12441
12789
  const { label: fullTierLabel } = applyContextTier(ctx, health.totalCycles);
12442
12790
  ctx.contextTier = fullTierLabel;
@@ -13817,6 +14165,7 @@ async function assembleContext2(adapter2, cycleNumber, cyclesSinceLastReview, pr
13817
14165
  // Doc registry — docs with pending actions for staleness audit
13818
14166
  adapter2.searchDocs?.({ hasPendingActions: true, limit: 20 })?.catch(() => []) ?? Promise.resolve([])
13819
14167
  ]);
14168
+ const pendingAgendaTopics = await (adapter2.getPendingAgendaTopics?.().catch(() => []) ?? Promise.resolve([]));
13820
14169
  const tasks = [...activeTasks, ...recentDoneTasks];
13821
14170
  const existingAdIds = new Set(decisions.map((d) => d.id));
13822
14171
  const survivingPendingRecs = [];
@@ -14054,6 +14403,15 @@ ${deferred.join("\n")}`);
14054
14403
  }
14055
14404
  }
14056
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")}`;
14057
14415
  }
14058
14416
  logDataSourceSummary("strategy_review_audit", [
14059
14417
  { label: "discoveryCanvas", hasData: discoveryCanvasText !== void 0 },
@@ -14094,7 +14452,8 @@ ${deferred.join("\n")}`);
14094
14452
  recentPlans: recentPlansText,
14095
14453
  unregisteredDocs: unregisteredDocsText,
14096
14454
  taskComments: taskCommentsText,
14097
- docActionStaleness: docActionStalenessText
14455
+ docActionStaleness: docActionStalenessText,
14456
+ pendingAgendaTopics: pendingAgendaText
14098
14457
  };
14099
14458
  const BUDGET_SOFT2 = 5e4;
14100
14459
  const BUDGET_HARD2 = 6e4;
@@ -14409,6 +14768,15 @@ async function processReviewOutput(adapter2, rawOutput, cycleNumber) {
14409
14768
  await adapter2.clearPendingReviewResponse?.();
14410
14769
  } catch {
14411
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
+ }
14412
14780
  const webhookUrl = process.env.PAPI_SLACK_WEBHOOK_URL;
14413
14781
  slackWarning = await sendSlackWebhook(webhookUrl, buildSlackSummary(data));
14414
14782
  }
@@ -14929,6 +15297,34 @@ var strategyReviewTool = {
14929
15297
  required: []
14930
15298
  }
14931
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
+ };
14932
15328
  var strategyChangeTool = {
14933
15329
  name: "strategy_change",
14934
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.',
@@ -15085,6 +15481,49 @@ ${result.userMessage}
15085
15481
  return errorResponse(err instanceof Error ? err.message : String(err));
15086
15482
  }
15087
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
+ }
15088
15527
  async function handleStrategyChange(adapter2, _config, args) {
15089
15528
  const toolMode = args.mode;
15090
15529
  try {
@@ -15356,7 +15795,7 @@ var boardArchiveTool = {
15356
15795
  };
15357
15796
  var boardEditTool = {
15358
15797
  name: "board_edit",
15359
- 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.",
15360
15799
  annotations: { readOnlyHint: false, destructiveHint: false },
15361
15800
  inputSchema: {
15362
15801
  type: "object",
@@ -15393,7 +15832,12 @@ var boardEditTool = {
15393
15832
  },
15394
15833
  notes: {
15395
15834
  type: "string",
15396
- 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)."
15397
15841
  },
15398
15842
  status: {
15399
15843
  type: "string",
@@ -15596,6 +16040,10 @@ async function handleBoardEdit(adapter2, args) {
15596
16040
  changes.push(field);
15597
16041
  }
15598
16042
  }
16043
+ const notesMode = args.notes_mode;
16044
+ if (notesMode === "clear" && !changes.includes("notes")) {
16045
+ changes.push("notes");
16046
+ }
15599
16047
  if (changes.length === 0) {
15600
16048
  return errorResponse("No fields to update. Pass at least one field (title, priority, complexity, module, epic, phase, notes, status, maturity).");
15601
16049
  }
@@ -15604,6 +16052,35 @@ async function handleBoardEdit(adapter2, args) {
15604
16052
  if (!task) {
15605
16053
  return errorResponse(`Task ${taskId} not found.`);
15606
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
+ }
15607
16084
  if (updates.status === "Backlog" && task.cycle != null) {
15608
16085
  updates.cycle = void 0;
15609
16086
  updates.cycle = null;
@@ -16625,9 +17102,8 @@ async function prepareSetup(adapter2, config2, input) {
16625
17102
  );
16626
17103
  }
16627
17104
  const TEMPLATE_MARKER = "*Describe your project's core value proposition here.*";
16628
- if (existingBrief.trim() && !existingBrief.includes(TEMPLATE_MARKER) && !input.force) {
16629
- 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.");
16630
- }
17105
+ const briefHasRealContent = existingBrief.trim().length > 0 && !existingBrief.includes(TEMPLATE_MARKER);
17106
+ const briefAlreadyExists = briefHasRealContent && !input.force;
16631
17107
  const detectedCodebaseType = detectCodebaseType(config2.projectRoot);
16632
17108
  const autoDetected = input.existingProject === void 0 || input.existingProject === false;
16633
17109
  const isExistingProject = input.existingProject === true || autoDetected && detectedCodebaseType === "existing_codebase";
@@ -16640,7 +17116,7 @@ async function prepareSetup(adapter2, config2, input) {
16640
17116
  }
16641
17117
  codebaseSummary = formatCodebaseSummary(scan, sourceContents);
16642
17118
  }
16643
- const briefPrompt = {
17119
+ const briefPrompt = briefAlreadyExists ? void 0 : {
16644
17120
  system: PRODUCT_BRIEF_SYSTEM,
16645
17121
  user: buildProductBriefPrompt({
16646
17122
  projectName: input.projectName,
@@ -16694,15 +17170,29 @@ async function prepareSetup(adapter2, config2, input) {
16694
17170
  initialTasksPrompt,
16695
17171
  codebaseSummary,
16696
17172
  detectedCodebaseType,
16697
- autoDetected: autoDetected && detectedCodebaseType !== "new_project"
17173
+ autoDetected: autoDetected && detectedCodebaseType !== "new_project",
17174
+ briefAlreadyExists
16698
17175
  };
16699
17176
  }
16700
17177
  async function applySetup(adapter2, config2, input, briefText, adSeedText, conventionsText, initialTasksText) {
16701
17178
  const createdProject = await scaffoldPapiDir(adapter2, config2, input);
16702
- if (!briefText.trim()) {
16703
- 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
+ }
16704
17194
  }
16705
- const { seededAds, warnings } = await applySetupOutputs(adapter2, config2, input, briefText, adSeedText, conventionsText);
17195
+ const { seededAds, warnings } = await applySetupOutputs(adapter2, config2, input, effectiveBriefText, adSeedText, conventionsText);
16706
17196
  let createdTasks = 0;
16707
17197
  if (initialTasksText?.trim()) {
16708
17198
  try {
@@ -16928,10 +17418,7 @@ PAPI needs just 3 things: project name, what it does, and who it's for.`
16928
17418
  const input = extractInput(args);
16929
17419
  try {
16930
17420
  if (toolMode === "apply") {
16931
- const briefResponse = args.brief_response;
16932
- if (!briefResponse || !briefResponse.trim()) {
16933
- return errorResponse('brief_response is required for mode "apply".');
16934
- }
17421
+ const briefResponse = args.brief_response ?? "";
16935
17422
  const adSeedResponse = args.ad_seed_response;
16936
17423
  const conventionsResponse = args.conventions_response;
16937
17424
  const initialTasksResponse = args.initial_tasks_response;
@@ -16964,6 +17451,12 @@ PAPI needs just 3 things: project name, what it does, and who it's for.`
16964
17451
  ""
16965
17452
  );
16966
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
+ }
16967
17460
  if (inferredDefaults.length > 0) {
16968
17461
  sections.push(
16969
17462
  `**Defaults applied** (override by re-running setup with these fields):`,
@@ -16974,30 +17467,37 @@ PAPI needs just 3 things: project name, what it does, and who it's for.`
16974
17467
  sections.push(
16975
17468
  `Generate the outputs below, then call \`setup\` again with:`,
16976
17469
  `- \`mode\`: "apply"`,
16977
- `- \`brief_response\`: your Product Brief markdown`,
17470
+ result.briefPrompt ? `- \`brief_response\`: your Product Brief markdown` : "",
16978
17471
  result.adSeedPrompt ? `- \`ad_seed_response\`: your AD seed JSON array` : "",
16979
17472
  result.conventionsPrompt ? `- \`conventions_response\`: your conventions markdown` : "",
16980
17473
  result.initialTasksPrompt ? `- \`initial_tasks_response\`: your initial tasks JSON array` : "",
16981
17474
  `- Plus all the original setup fields (project_name, description, target_users${isExisting ? ", existing_project: true" : ""})`,
16982
17475
  "",
16983
- `---`,
16984
- "",
16985
- `### 1. Product Brief`,
16986
- "",
16987
- `<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>
16988
17486
  ${result.briefPrompt.system}
16989
17487
  </system_prompt>`,
16990
- "",
16991
- `<context>
17488
+ "",
17489
+ `<context>
16992
17490
  ${result.briefPrompt.user}
16993
17491
  </context>`
16994
- );
17492
+ );
17493
+ }
16995
17494
  if (result.adSeedPrompt) {
17495
+ sectionNum++;
16996
17496
  sections.push(
16997
17497
  "",
16998
17498
  `---`,
16999
17499
  "",
17000
- `### 2. Active Decision Seeds`,
17500
+ `### ${sectionNum}. Active Decision Seeds`,
17001
17501
  "",
17002
17502
  `<system_prompt>
17003
17503
  ${result.adSeedPrompt.system}
@@ -17009,11 +17509,12 @@ ${result.adSeedPrompt.user}
17009
17509
  );
17010
17510
  }
17011
17511
  if (result.conventionsPrompt) {
17512
+ sectionNum++;
17012
17513
  sections.push(
17013
17514
  "",
17014
17515
  `---`,
17015
17516
  "",
17016
- `### 3. Conventions`,
17517
+ `### ${sectionNum}. Conventions`,
17017
17518
  "",
17018
17519
  `<system_prompt>
17019
17520
  ${result.conventionsPrompt.system}
@@ -17025,11 +17526,12 @@ ${result.conventionsPrompt.user}
17025
17526
  );
17026
17527
  }
17027
17528
  if (result.initialTasksPrompt) {
17529
+ sectionNum++;
17028
17530
  sections.push(
17029
17531
  "",
17030
17532
  `---`,
17031
17533
  "",
17032
- `### 4. Initial Backlog Tasks`,
17534
+ `### ${sectionNum}. Initial Backlog Tasks`,
17033
17535
  "",
17034
17536
  `<system_prompt>
17035
17537
  ${result.initialTasksPrompt.system}
@@ -17441,18 +17943,37 @@ async function completeBuild(adapter2, config2, taskId, input, options = {}) {
17441
17943
  }
17442
17944
  }
17443
17945
  let autoTriagedCount = 0;
17946
+ const autoTriagedIds = [];
17947
+ const autoTriagedDupes = [];
17444
17948
  if (input.discoveredIssues && input.discoveredIssues !== "None" && typeof adapter2.createTask === "function") {
17445
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
+ }
17446
17961
  for (const line of issueLines) {
17447
17962
  const sevMatch = line.match(/^(P[0-3])[\s:]+/i);
17448
17963
  if (!sevMatch) continue;
17449
17964
  const severityLabel = sevMatch[1].toUpperCase();
17450
- 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";
17451
17966
  const titleRaw = line.replace(/^P[0-3][\s:]+/i, "").trim();
17452
17967
  const title = titleRaw.length > 120 ? titleRaw.slice(0, 120) : titleRaw;
17453
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
+ }
17454
17975
  try {
17455
- await adapter2.createTask({
17976
+ const created = await adapter2.createTask({
17456
17977
  uuid: "",
17457
17978
  displayId: "",
17458
17979
  title: `[Auto-triaged] ${title}`,
@@ -17469,6 +17990,10 @@ async function completeBuild(adapter2, config2, taskId, input, options = {}) {
17469
17990
  createdCycle: cycleNumber
17470
17991
  });
17471
17992
  autoTriagedCount++;
17993
+ if (created?.displayId) {
17994
+ autoTriagedIds.push(created.displayId);
17995
+ backlogTitleMap.set(normalized, created.displayId);
17996
+ }
17472
17997
  } catch {
17473
17998
  }
17474
17999
  }
@@ -17665,6 +18190,8 @@ async function completeBuild(adapter2, config2, taskId, input, options = {}) {
17665
18190
  dogfoodResolvedCount: dogfoodResolvedCount > 0 ? dogfoodResolvedCount : void 0,
17666
18191
  learningsLinkedCount: learningsLinkedCount > 0 ? learningsLinkedCount : void 0,
17667
18192
  autoTriagedCount: autoTriagedCount > 0 ? autoTriagedCount : void 0,
18193
+ autoTriagedIds: autoTriagedIds.length > 0 ? autoTriagedIds : void 0,
18194
+ autoTriagedDupes: autoTriagedDupes.length > 0 ? autoTriagedDupes : void 0,
17668
18195
  reportWriteVerified
17669
18196
  };
17670
18197
  }
@@ -18168,8 +18695,16 @@ function formatCompleteResult(result) {
18168
18695
  if (result.learningsLinkedCount) {
18169
18696
  lines.push("", `Linked ${result.learningsLinkedCount} unactioned learning(s) to this task.`);
18170
18697
  }
18171
- if (result.autoTriagedCount) {
18172
- 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(" "));
18173
18708
  }
18174
18709
  if (result.reportWriteVerified === false) {
18175
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.");
@@ -19784,7 +20319,41 @@ function generateChangelog(version, commits) {
19784
20319
  ${commitList}
19785
20320
  `;
19786
20321
  }
19787
- 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) {
19788
20357
  if (!isGitAvailable()) {
19789
20358
  throw new Error("git is not available.");
19790
20359
  }
@@ -19795,10 +20364,13 @@ async function createRelease(config2, branch, version, adapter2) {
19795
20364
  throw new Error("working directory has uncommitted changes. Commit or stash them before releasing.");
19796
20365
  }
19797
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
+ })();
19798
20371
  if (adapter2) {
19799
20372
  try {
19800
- const versionMatch = version.match(/^v0\.(\d+)\./);
19801
- const currentCycle = versionMatch ? parseInt(versionMatch[1], 10) : 0;
20373
+ const currentCycle = resolvedCycleNum;
19802
20374
  if (currentCycle > 0) {
19803
20375
  await adapter2.createCycle({
19804
20376
  id: `cycle-${currentCycle}`,
@@ -19827,6 +20399,24 @@ async function createRelease(config2, branch, version, adapter2) {
19827
20399
  warnings.push(`git pull failed: ${pull.message}. Run manually.`);
19828
20400
  }
19829
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
+ }
19830
20420
  if (tagExists(config2.projectRoot, version)) {
19831
20421
  throw new Error(`tag "${version}" already exists. Use a different version.`);
19832
20422
  }
@@ -19863,7 +20453,8 @@ async function createRelease(config2, branch, version, adapter2) {
19863
20453
  commitNote,
19864
20454
  tagMessage: tagResult.message,
19865
20455
  pushNotes,
19866
- warnings: warnings.length > 0 ? warnings : void 0
20456
+ warnings: warnings.length > 0 ? warnings : void 0,
20457
+ ...groupedBranchMerges ? { groupedBranchMerges } : {}
19867
20458
  };
19868
20459
  }
19869
20460
 
@@ -19919,7 +20510,9 @@ async function handleRelease(adapter2, config2, args) {
19919
20510
  return errorResponse(`version must start with "v" (got "${version}"). Example: "v0.1.0-alpha"`);
19920
20511
  }
19921
20512
  try {
19922
- 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);
19923
20516
  const lines = [
19924
20517
  `## Release ${result.version}`,
19925
20518
  "",
@@ -19932,13 +20525,17 @@ async function handleRelease(adapter2, config2, args) {
19932
20525
  lines.push("", "---", "");
19933
20526
  lines.push(...result.pushNotes);
19934
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
+ }
19935
20534
  if (result.warnings?.length) {
19936
20535
  lines.push("", "\u26A0\uFE0F Warnings: " + result.warnings.join("; "));
19937
20536
  }
19938
20537
  try {
19939
- const cycleMatch = version.match(/^v0\.(\d+)\./);
19940
- const cycleNum = cycleMatch ? parseInt(cycleMatch[1], 10) : 0;
19941
- if (cycleNum > 0) {
20538
+ if (cycleNum && cycleNum > 0) {
19942
20539
  const reports = await adapter2.getBuildReportsSince(cycleNum);
19943
20540
  const EMPTY = /* @__PURE__ */ new Set(["None", "none", "N/A", "", "null"]);
19944
20541
  const issues = reports.filter((r) => r.discoveredIssues && !EMPTY.has(r.discoveredIssues.trim())).map((r) => `- **${r.taskId}** (${r.taskName}): ${r.discoveredIssues}`);
@@ -19951,10 +20548,10 @@ async function handleRelease(adapter2, config2, args) {
19951
20548
  }
19952
20549
  if (rawObservations && rawObservations.length > 0 && adapter2.writeDogfoodEntries) {
19953
20550
  try {
19954
- const cycleMatch = version.match(/^v0\.(\d+)\./);
19955
- const cycleNum = cycleMatch ? parseInt(cycleMatch[1], 10) : 0;
20551
+ const cycleMatch2 = version.match(/^v0\.(\d+)\./);
20552
+ const cycleNum2 = cycleMatch2 ? parseInt(cycleMatch2[1], 10) : 0;
19956
20553
  const entries = rawObservations.map((obs) => ({
19957
- cycleNumber: cycleNum,
20554
+ cycleNumber: cycleNum2,
19958
20555
  category: obs.category,
19959
20556
  content: obs.content,
19960
20557
  sourceTool: "release",
@@ -20210,6 +20807,10 @@ function mergeAfterAccept(config2, taskId) {
20210
20807
  }
20211
20808
  const featureBranch = taskBranchName(taskId);
20212
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
+ }
20213
20814
  const papiDir = join7(config2.projectRoot, ".papi");
20214
20815
  if (existsSync4(papiDir)) {
20215
20816
  try {
@@ -20385,8 +20986,9 @@ ${result.handoffRegenPrompt.userMessage}
20385
20986
  }
20386
20987
  const version = `v0.${result.currentCycle}.0`;
20387
20988
  const baseBranch = resolveBaseBranch(config2.projectRoot, config2.baseBranch);
20388
- const releaseResult = await createRelease(config2, baseBranch, version, adapter2);
20989
+ const releaseResult = await createRelease(config2, baseBranch, version, adapter2, result.currentCycle);
20389
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") : "";
20390
20992
  autoReleaseNote = `
20391
20993
 
20392
20994
  ---
@@ -20396,7 +20998,7 @@ ${result.handoffRegenPrompt.userMessage}
20396
20998
  - Version: **${releaseResult.version}**
20397
20999
  - ${releaseResult.commitNote}
20398
21000
  - ${releaseResult.tagMessage}
20399
- - ${pushInfo}` + (releaseResult.warnings?.length ? `
21001
+ - ${pushInfo}` + groupedMergeNote + (releaseResult.warnings?.length ? `
20400
21002
  - Warnings: ${releaseResult.warnings.join(", ")}` : "") + `
20401
21003
 
20402
21004
  Run \`plan\` to create Cycle ${result.currentCycle + 1}.`;
@@ -20643,7 +21245,7 @@ function countByStatus(tasks) {
20643
21245
  async function getHealthSummary(adapter2) {
20644
21246
  const health = await adapter2.getCycleHealth();
20645
21247
  const activeTasks = await adapter2.queryBoard({
20646
- status: ["Backlog", "In Cycle", "Ready", "In Progress", "In Review", "Blocked"]
21248
+ status: ["Backlog", "In Cycle", "Ready", "In Progress", "In Review", "Blocked", "Deferred"]
20647
21249
  });
20648
21250
  const logEntries = await adapter2.getCycleLog(3);
20649
21251
  const cycleNumber = health.totalCycles;
@@ -21044,21 +21646,94 @@ async function handleDocScan(adapter2, config2, args) {
21044
21646
  return textResponse(lines.join("\n"));
21045
21647
  }
21046
21648
 
21649
+ // src/services/session-guidance.ts
21650
+ import { existsSync as existsSync6 } from "fs";
21651
+ import { join as join9 } from "path";
21652
+ var state = {
21653
+ toolCallCount: 0,
21654
+ lastOrientAt: null,
21655
+ releaseSinceLastOrient: false,
21656
+ sessionStartedAt: Date.now()
21657
+ };
21658
+ var CONTEXT_BLOAT_CALL_THRESHOLD = 40;
21659
+ var ORIENT_GAP_MS = 3 * 60 * 60 * 1e3;
21660
+ function recordToolCall(name) {
21661
+ state.toolCallCount++;
21662
+ if (name === "release") state.releaseSinceLastOrient = true;
21663
+ }
21664
+ function markOrient() {
21665
+ state.lastOrientAt = Date.now();
21666
+ state.releaseSinceLastOrient = false;
21667
+ }
21668
+ async function buildSessionGuidance(adapter2, projectRoot) {
21669
+ const signals = [];
21670
+ try {
21671
+ if (adapter2.searchDocs) {
21672
+ const researchDir = join9(projectRoot, "docs", "research");
21673
+ if (existsSync6(researchDir)) {
21674
+ const files = scanMdFiles(researchDir, projectRoot);
21675
+ if (files.length > 0) {
21676
+ const registered = await adapter2.searchDocs({ limit: 500, status: "all" });
21677
+ const registeredPaths = new Set(registered.map((d) => d.path));
21678
+ const unregistered = files.filter((f) => !registeredPaths.has(f));
21679
+ if (unregistered.length > 0) {
21680
+ signals.push(
21681
+ `${unregistered.length} research doc(s) in docs/research/ not registered \u2014 run \`doc_register\` so the planner can surface them.`
21682
+ );
21683
+ }
21684
+ }
21685
+ }
21686
+ }
21687
+ } catch {
21688
+ }
21689
+ if (state.toolCallCount > CONTEXT_BLOAT_CALL_THRESHOLD) {
21690
+ signals.push(
21691
+ `${state.toolCallCount} tool calls this session \u2014 context may be bloated. Consider starting a fresh window.`
21692
+ );
21693
+ }
21694
+ if (state.lastOrientAt && Date.now() - state.lastOrientAt > ORIENT_GAP_MS) {
21695
+ const hours = Math.round((Date.now() - state.lastOrientAt) / (60 * 60 * 1e3));
21696
+ signals.push(
21697
+ `${hours}h since last orient \u2014 session may be stale. Consider a fresh window for best results.`
21698
+ );
21699
+ }
21700
+ if (state.releaseSinceLastOrient) {
21701
+ signals.push(
21702
+ "Release just ran \u2014 start a fresh session before the next `plan` to keep planning context clean."
21703
+ );
21704
+ }
21705
+ return signals.slice(0, 3);
21706
+ }
21707
+
21047
21708
  // src/tools/orient.ts
21048
21709
  import { execFileSync as execFileSync3 } from "child_process";
21049
- import { readFileSync as readFileSync3, writeFileSync, existsSync as existsSync6 } from "fs";
21050
- import { join as join9 } from "path";
21710
+ import { readFileSync as readFileSync3, writeFileSync, existsSync as existsSync7 } from "fs";
21711
+ import { join as join10 } from "path";
21712
+ var GIT_DEPENDENT_ENVS = /* @__PURE__ */ new Set(["cowork", "api"]);
21713
+ var VALID_ENVS = /* @__PURE__ */ new Set(["claude-code", "cowork", "api", "unknown"]);
21714
+ function normaliseEnvironment(raw) {
21715
+ if (typeof raw === "string" && VALID_ENVS.has(raw)) {
21716
+ return raw;
21717
+ }
21718
+ return "unknown";
21719
+ }
21051
21720
  var orientTool = {
21052
21721
  name: "orient",
21053
- description: "Session orientation \u2014 run this FIRST at session start before any other tool. Single call that replaces build_list + health. Returns: cycle number, task counts by status, in-progress/in-review tasks, strategy review cadence, velocity snapshot, recommended next action, and a release reminder when all cycle tasks are Done but release has not run. Read-only, does not modify any files.",
21722
+ description: "Session orientation \u2014 run this FIRST at session start before any other tool. Single call that replaces build_list + health. Returns: cycle number, task counts by status, in-progress/in-review tasks, strategy review cadence, velocity snapshot, recommended next action, and a release reminder when all cycle tasks are Done but release has not run. Read-only, does not modify any files. Pass `environment` to qualify git-dependent recommendations (build_execute, release, review_submit) for non-Claude-Code callers.",
21054
21723
  annotations: { readOnlyHint: true, destructiveHint: false },
21055
21724
  inputSchema: {
21056
21725
  type: "object",
21057
- properties: {},
21726
+ properties: {
21727
+ environment: {
21728
+ type: "string",
21729
+ enum: ["claude-code", "cowork", "api", "unknown"],
21730
+ description: 'Caller environment. "claude-code" (local CLI with git) sees all recommendations unchanged. "cowork" or "api" (hosted/remote, no local git) suppresses/qualifies build_execute, release, and review_submit recommendations because those require a local Claude Code session to execute. Default "unknown" = show everything (safe default).'
21731
+ }
21732
+ },
21058
21733
  required: []
21059
21734
  }
21060
21735
  };
21061
- function formatOrientSummary(health, buildInfo, hierarchy, latestTag, projectRoot) {
21736
+ function formatOrientSummary(health, buildInfo, hierarchy, latestTag, projectRoot, environment = "unknown") {
21062
21737
  const lines = [];
21063
21738
  const cycleIsComplete = health.latestCycleStatus === "complete";
21064
21739
  const tagSuffix = latestTag ? ` \u2014 ${latestTag}` : "";
@@ -21076,7 +21751,13 @@ function formatOrientSummary(health, buildInfo, hierarchy, latestTag, projectRoo
21076
21751
  }
21077
21752
  lines.push("");
21078
21753
  }
21079
- lines.push(`> **Next action:** ${health.recommendedMode}`);
21754
+ const isGitDependentRec = /\*\*(Build|Review)\*\*/.test(health.recommendedMode);
21755
+ if (GIT_DEPENDENT_ENVS.has(environment) && isGitDependentRec) {
21756
+ lines.push(`> **Next action:** ${health.recommendedMode}`);
21757
+ lines.push(`> _Note: \`build_execute\`, \`review_submit\`, and \`release\` require a local Claude Code session with git access. From \`${environment}\` you can view state, log ideas, and edit the board \u2014 but the build/review/release loop must run through Claude Code._`);
21758
+ } else {
21759
+ lines.push(`> **Next action:** ${health.recommendedMode}`);
21760
+ }
21080
21761
  lines.push("");
21081
21762
  lines.push(`**Strategy Review:** ${health.reviewWarning}`);
21082
21763
  lines.push("");
@@ -21227,7 +21908,7 @@ function getLatestGitTag(projectRoot) {
21227
21908
  }
21228
21909
  function checkNpmVersionDrift() {
21229
21910
  try {
21230
- const pkgPath = join9(new URL(".", import.meta.url).pathname, "..", "..", "package.json");
21911
+ const pkgPath = join10(new URL(".", import.meta.url).pathname, "..", "..", "package.json");
21231
21912
  const pkg = JSON.parse(readFileSync3(pkgPath, "utf-8"));
21232
21913
  const localVersion = pkg.version;
21233
21914
  const packageName = pkg.name;
@@ -21244,7 +21925,8 @@ function checkNpmVersionDrift() {
21244
21925
  return null;
21245
21926
  }
21246
21927
  }
21247
- async function handleOrient(adapter2, config2) {
21928
+ async function handleOrient(adapter2, config2, args = {}) {
21929
+ const environment = normaliseEnvironment(args.environment);
21248
21930
  try {
21249
21931
  try {
21250
21932
  await propagatePhaseStatus(adapter2);
@@ -21353,7 +22035,7 @@ ${versionDrift}` : "";
21353
22035
  let unregisteredDocsNote = "";
21354
22036
  try {
21355
22037
  if (adapter2.searchDocs) {
21356
- const docsDir = join9(config2.projectRoot, "docs");
22038
+ const docsDir = join10(config2.projectRoot, "docs");
21357
22039
  const docsFiles = scanMdFiles(docsDir, config2.projectRoot);
21358
22040
  if (docsFiles.length > 0) {
21359
22041
  const registered = await adapter2.searchDocs({ limit: 500, status: "all" });
@@ -21443,13 +22125,35 @@ ${versionDrift}` : "";
21443
22125
  }
21444
22126
  } catch {
21445
22127
  }
22128
+ let alertsNote = "";
21446
22129
  let unactionedIssuesNote = "";
21447
22130
  try {
21448
- const learnings = await adapter2.getCycleLearnings?.({ category: "issue", limit: 20 });
22131
+ const learnings = await adapter2.getCycleLearnings?.({ category: "issue", limit: 30 });
21449
22132
  if (learnings) {
21450
- const unactioned = learnings.filter((l) => !l.actionTaken && l.severity && ["P0", "P1", "P2"].includes(l.severity)).slice(0, 5);
21451
- if (unactioned.length > 0) {
21452
- const lines = ["\n\n## Unactioned Issues"];
22133
+ const byRecency = (a, b2) => (b2.createdAt ?? "").localeCompare(a.createdAt ?? "");
22134
+ const unactionedAll = learnings.filter((l) => !l.actionTaken).map((l) => ({ ...l, severity: l.severity ?? "P3" }));
22135
+ const allAlerts = unactionedAll.filter((l) => l.severity === "P0" || l.severity === "P1").sort(byRecency);
22136
+ const allLowSev = unactionedAll.filter((l) => l.severity === "P2" || l.severity === "P3").sort(byRecency);
22137
+ const totalP2 = allLowSev.filter((l) => l.severity === "P2").length;
22138
+ const totalP3 = allLowSev.filter((l) => l.severity === "P3").length;
22139
+ const ALERT_CAP = 10;
22140
+ const UNACTIONED_CAP = 5;
22141
+ const alerts = allAlerts.slice(0, ALERT_CAP);
22142
+ const unactioned = allLowSev.slice(0, UNACTIONED_CAP);
22143
+ if (allAlerts.length > 0) {
22144
+ const header = allAlerts.length > ALERT_CAP ? `${allAlerts.length} P0/P1 discovered issues awaiting action (showing ${ALERT_CAP} most recent).` : `${allAlerts.length} P0/P1 discovered issue${allAlerts.length !== 1 ? "s" : ""} awaiting action.`;
22145
+ const lines = ["\n\n## \u{1F6A8} Alerts", header];
22146
+ for (const issue of alerts) {
22147
+ const desc = issue.summary.length > 100 ? `${issue.summary.slice(0, 97)}\u2026` : issue.summary;
22148
+ lines.push(`- **${issue.severity}** (C${issue.cycleNumber} / ${issue.taskId}): ${desc}`);
22149
+ }
22150
+ lines.push("_Escalate: run `idea` with P1 priority, or `board_edit` if already handled._");
22151
+ alertsNote = lines.join("\n");
22152
+ }
22153
+ if (allLowSev.length > 0) {
22154
+ const totalLow = totalP2 + totalP3;
22155
+ const header = totalLow > UNACTIONED_CAP ? `${totalP2} P2 \xB7 ${totalP3} P3 (showing ${UNACTIONED_CAP} most recent)` : `${totalP2} P2 \xB7 ${totalP3} P3`;
22156
+ const lines = ["\n\n## Unactioned Issues", header];
21453
22157
  for (const issue of unactioned) {
21454
22158
  const desc = issue.summary.length > 100 ? `${issue.summary.slice(0, 97)}\u2026` : issue.summary;
21455
22159
  lines.push(`- **${issue.severity}** (C${issue.cycleNumber} / ${issue.taskId}): ${desc}`);
@@ -21460,15 +22164,26 @@ ${versionDrift}` : "";
21460
22164
  }
21461
22165
  } catch {
21462
22166
  }
21463
- return textResponse(formatOrientSummary(healthResult, buildInfo, hierarchy, latestTag, config2.projectRoot) + ttfvNote + reconciliationNote + unrecordedNote + unregisteredDocsNote + researchSignalsNote + recsNote + pendingReviewNote + patternsNote + unactionedIssuesNote + versionNote + enrichmentNote);
22167
+ let sessionGuidanceNote = "";
22168
+ try {
22169
+ const signals = await buildSessionGuidance(adapter2, config2.projectRoot);
22170
+ if (signals.length > 0) {
22171
+ const lines = ["\n\n## Session Guidance"];
22172
+ for (const s of signals) lines.push(`- ${s}`);
22173
+ sessionGuidanceNote = lines.join("\n");
22174
+ }
22175
+ markOrient();
22176
+ } catch {
22177
+ }
22178
+ return textResponse(formatOrientSummary(healthResult, buildInfo, hierarchy, latestTag, config2.projectRoot, environment) + alertsNote + ttfvNote + reconciliationNote + unrecordedNote + unregisteredDocsNote + researchSignalsNote + recsNote + pendingReviewNote + patternsNote + unactionedIssuesNote + sessionGuidanceNote + versionNote + enrichmentNote);
21464
22179
  } catch (err) {
21465
22180
  const message = err instanceof Error ? err.message : String(err);
21466
22181
  return errorResponse(`Orient failed: ${message}`);
21467
22182
  }
21468
22183
  }
21469
22184
  function enrichClaudeMd(projectRoot, cycleNumber) {
21470
- const claudeMdPath = join9(projectRoot, "CLAUDE.md");
21471
- if (!existsSync6(claudeMdPath)) return "";
22185
+ const claudeMdPath = join10(projectRoot, "CLAUDE.md");
22186
+ if (!existsSync7(claudeMdPath)) return "";
21472
22187
  const content = readFileSync3(claudeMdPath, "utf-8");
21473
22188
  const additions = [];
21474
22189
  if (cycleNumber >= 6 && !content.includes(CLAUDE_MD_ENRICHMENT_SENTINEL_T1)) {
@@ -22250,6 +22965,47 @@ ${result.userMessage}
22250
22965
  }
22251
22966
  }
22252
22967
 
22968
+ // src/lib/install-id.ts
22969
+ import { randomUUID as randomUUID15 } from "crypto";
22970
+ import { mkdirSync, readFileSync as readFileSync4, writeFileSync as writeFileSync2, chmodSync } from "fs";
22971
+ import { homedir as homedir3 } from "os";
22972
+ import { join as join11 } from "path";
22973
+ var PAPI_HOME_DIR = join11(homedir3(), ".papi");
22974
+ var INSTALL_ID_FILE = join11(PAPI_HOME_DIR, "install-id.json");
22975
+ var cachedInstallId = null;
22976
+ function isValidUuid(s) {
22977
+ return typeof s === "string" && /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(s);
22978
+ }
22979
+ function getInstallId() {
22980
+ if (cachedInstallId) return cachedInstallId;
22981
+ try {
22982
+ const raw = readFileSync4(INSTALL_ID_FILE, "utf-8");
22983
+ const parsed = JSON.parse(raw);
22984
+ if (isValidUuid(parsed.install_id)) {
22985
+ cachedInstallId = parsed.install_id;
22986
+ return cachedInstallId;
22987
+ }
22988
+ } catch {
22989
+ }
22990
+ try {
22991
+ mkdirSync(PAPI_HOME_DIR, { recursive: true, mode: 448 });
22992
+ const id = randomUUID15();
22993
+ const contents = {
22994
+ install_id: id,
22995
+ created_at: (/* @__PURE__ */ new Date()).toISOString()
22996
+ };
22997
+ writeFileSync2(INSTALL_ID_FILE, JSON.stringify(contents, null, 2), { mode: 384 });
22998
+ try {
22999
+ chmodSync(INSTALL_ID_FILE, 384);
23000
+ } catch {
23001
+ }
23002
+ cachedInstallId = id;
23003
+ return cachedInstallId;
23004
+ } catch {
23005
+ return null;
23006
+ }
23007
+ }
23008
+
22253
23009
  // src/lib/telemetry.ts
22254
23010
  var TELEMETRY_SUPABASE_URL = "https://guewgygcpcmrcoppihzx.supabase.co";
22255
23011
  var TELEMETRY_API_KEY = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Imd1ZXdneWdjcGNtcmNvcHBpaHp4Iiwicm9sZSI6ImFub24iLCJpYXQiOjE3NzI2Njk2NTMsImV4cCI6MjA4ODI0NTY1M30.V5Jw7wJgiMpSQPa2mt0ftjyye5ynG1qLlam00yPVNJY";
@@ -22288,6 +23044,32 @@ function emitToolCall(projectId, toolName, durationMs, extra) {
22288
23044
  metadata: { duration_ms: durationMs, ...extra }
22289
23045
  });
22290
23046
  }
23047
+ function emitMdAdapterPing(toolName, extra, userId, projectSlug) {
23048
+ if (!isEnabled()) return;
23049
+ const installId = getInstallId();
23050
+ if (!installId) return;
23051
+ const resolvedUserId = userId ?? process.env["PAPI_USER_ID"] ?? void 0;
23052
+ const body = {
23053
+ install_id: installId,
23054
+ tool_name: toolName,
23055
+ papi_version: process.env["npm_package_version"] ?? null,
23056
+ metadata: extra ?? {}
23057
+ };
23058
+ if (resolvedUserId) body["user_id"] = resolvedUserId;
23059
+ if (projectSlug) body["project_slug"] = projectSlug;
23060
+ fetch(`${TELEMETRY_SUPABASE_URL}/rest/v1/md_adapter_pings`, {
23061
+ method: "POST",
23062
+ headers: {
23063
+ "Content-Type": "application/json",
23064
+ "apikey": TELEMETRY_API_KEY,
23065
+ "Authorization": `Bearer ${TELEMETRY_API_KEY}`,
23066
+ "Prefer": "return=minimal"
23067
+ },
23068
+ body: JSON.stringify(body),
23069
+ signal: AbortSignal.timeout(5e3)
23070
+ }).catch(() => {
23071
+ });
23072
+ }
22291
23073
  function emitMilestone(projectId, milestone, extra) {
22292
23074
  emitTelemetryEvent({
22293
23075
  project_id: projectId,
@@ -22302,6 +23084,7 @@ var TOOLS_REQUIRING_PAPI = /* @__PURE__ */ new Set([
22302
23084
  "plan",
22303
23085
  "strategy_review",
22304
23086
  "strategy_change",
23087
+ "strategy_agenda",
22305
23088
  "board_view",
22306
23089
  "board_deprioritise",
22307
23090
  "board_archive",
@@ -22322,13 +23105,26 @@ var TOOLS_REQUIRING_PAPI = /* @__PURE__ */ new Set([
22322
23105
  "handoff_generate"
22323
23106
  ]);
22324
23107
  function createServer(adapter2, config2) {
23108
+ const __pkgFilename = fileURLToPath(import.meta.url);
23109
+ const __pkgDir = dirname(__pkgFilename);
23110
+ let serverVersion = "unknown";
23111
+ try {
23112
+ const pkg = JSON.parse(readFileSync5(join12(__pkgDir, "..", "package.json"), "utf-8"));
23113
+ serverVersion = pkg.version ?? "unknown";
23114
+ } catch {
23115
+ }
22325
23116
  const server2 = new Server(
22326
- { name: "papi", version: "0.1.0" },
23117
+ { name: "papi", version: serverVersion },
22327
23118
  { capabilities: { tools: {}, prompts: {} } }
22328
23119
  );
23120
+ if (config2.adapterType === "md") {
23121
+ process.stderr.write(
23122
+ "\n\u26A0 PAPI is running in md mode \u2014 your cycles are not visible on the hosted dashboard.\n Configure DATABASE_URL or sign up at https://getpapi.ai/setup to enable observability.\n\n"
23123
+ );
23124
+ }
22329
23125
  const __filename = fileURLToPath(import.meta.url);
22330
23126
  const __dirname2 = dirname(__filename);
22331
- const skillsDir = join10(__dirname2, "..", "skills");
23127
+ const skillsDir = join12(__dirname2, "..", "skills");
22332
23128
  function parseSkillFrontmatter(content) {
22333
23129
  const match = content.match(/^---\n([\s\S]*?)\n---/);
22334
23130
  if (!match) return null;
@@ -22346,7 +23142,7 @@ function createServer(adapter2, config2) {
22346
23142
  const mdFiles = files.filter((f) => f.endsWith(".md"));
22347
23143
  const prompts = [];
22348
23144
  for (const file of mdFiles) {
22349
- const content = await readFile5(join10(skillsDir, file), "utf-8");
23145
+ const content = await readFile5(join12(skillsDir, file), "utf-8");
22350
23146
  const meta = parseSkillFrontmatter(content);
22351
23147
  if (meta) {
22352
23148
  prompts.push({ name: meta.name, description: meta.description });
@@ -22362,7 +23158,7 @@ function createServer(adapter2, config2) {
22362
23158
  try {
22363
23159
  const files = await readdir2(skillsDir);
22364
23160
  for (const file of files.filter((f) => f.endsWith(".md"))) {
22365
- const content = await readFile5(join10(skillsDir, file), "utf-8");
23161
+ const content = await readFile5(join12(skillsDir, file), "utf-8");
22366
23162
  const meta = parseSkillFrontmatter(content);
22367
23163
  if (meta?.name === name) {
22368
23164
  const body = content.replace(/^---\n[\s\S]*?\n---\n*/, "");
@@ -22381,6 +23177,7 @@ function createServer(adapter2, config2) {
22381
23177
  planTool,
22382
23178
  strategyReviewTool,
22383
23179
  strategyChangeTool,
23180
+ strategyAgendaTool,
22384
23181
  boardViewTool,
22385
23182
  boardDeprioritiseTool,
22386
23183
  boardArchiveTool,
@@ -22435,6 +23232,7 @@ function createServer(adapter2, config2) {
22435
23232
  }
22436
23233
  }
22437
23234
  const timer2 = startTimer();
23235
+ recordToolCall(name);
22438
23236
  let result;
22439
23237
  switch (name) {
22440
23238
  case "plan":
@@ -22446,6 +23244,9 @@ function createServer(adapter2, config2) {
22446
23244
  case "strategy_change":
22447
23245
  result = await handleStrategyChange(adapter2, config2, safeArgs);
22448
23246
  break;
23247
+ case "strategy_agenda":
23248
+ result = await handleStrategyAgenda(adapter2, config2, safeArgs);
23249
+ break;
22449
23250
  case "board_view":
22450
23251
  result = await handleBoardView(adapter2, safeArgs);
22451
23252
  break;
@@ -22498,7 +23299,7 @@ function createServer(adapter2, config2) {
22498
23299
  result = await handleInit(config2, safeArgs);
22499
23300
  break;
22500
23301
  case "orient":
22501
- result = await handleOrient(adapter2, config2);
23302
+ result = await handleOrient(adapter2, config2, safeArgs);
22502
23303
  break;
22503
23304
  case "hierarchy_update":
22504
23305
  result = await handleHierarchyUpdate(adapter2, safeArgs);
@@ -22539,6 +23340,10 @@ function createServer(adapter2, config2) {
22539
23340
  });
22540
23341
  } catch {
22541
23342
  }
23343
+ if (config2.adapterType === "md") {
23344
+ const mdProjectSlug = config2.projectRoot ? config2.projectRoot.split("/").pop() : void 0;
23345
+ emitMdAdapterPing(name, { duration_ms: elapsed, success: !isError }, config2.userId, mdProjectSlug);
23346
+ }
22542
23347
  const telemetryProjectId = process.env["PAPI_PROJECT_ID"];
22543
23348
  if (telemetryProjectId) {
22544
23349
  emitToolCall(telemetryProjectId, name, elapsed, {
@@ -22566,7 +23371,7 @@ function createServer(adapter2, config2) {
22566
23371
  var __dirname = dirname2(fileURLToPath2(import.meta.url));
22567
23372
  var pkgVersion = "unknown";
22568
23373
  try {
22569
- const pkg = JSON.parse(readFileSync4(join11(__dirname, "..", "package.json"), "utf-8"));
23374
+ const pkg = JSON.parse(readFileSync6(join13(__dirname, "..", "package.json"), "utf-8"));
22570
23375
  pkgVersion = pkg.version;
22571
23376
  } catch {
22572
23377
  }