@papi-ai/server 0.7.51 → 0.7.53

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.
@@ -25,6 +25,7 @@ __export(git_exports, {
25
25
  detectUnrecordedCommits: () => detectUnrecordedCommits,
26
26
  ensureLatestDevelop: () => ensureLatestDevelop,
27
27
  ensureTagAtHead: () => ensureTagAtHead,
28
+ findTaskCommitsOnBase: () => findTaskCommitsOnBase,
28
29
  getBranchDiff: () => getBranchDiff,
29
30
  getCommitsSinceTag: () => getCommitsSinceTag,
30
31
  getCurrentBranch: () => getCurrentBranch,
@@ -718,11 +719,29 @@ function escapeRegexLiteral(s) {
718
719
  return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
719
720
  }
720
721
  function detectMergedInProgress(cwd, preferredBase, tasks) {
721
- if (!isGitAvailable() || !isGitRepo(cwd)) return [];
722
722
  const inProgress = tasks.filter((t) => t.status === "In Progress");
723
723
  if (inProgress.length === 0) return [];
724
+ const landed = findTaskCommitsOnBase(cwd, preferredBase, inProgress.map((t) => t.displayId));
725
+ const hits = [];
726
+ for (const task of inProgress) {
727
+ const hit = landed.get(task.displayId);
728
+ if (!hit) continue;
729
+ hits.push({
730
+ displayId: task.displayId,
731
+ title: task.title,
732
+ commit: hit.commit,
733
+ subject: hit.subject,
734
+ pr: hit.pr
735
+ });
736
+ }
737
+ return hits;
738
+ }
739
+ function findTaskCommitsOnBase(cwd, preferredBase, displayIds) {
740
+ const out = /* @__PURE__ */ new Map();
741
+ if (displayIds.length === 0) return out;
742
+ if (!isGitAvailable() || !isGitRepo(cwd)) return out;
724
743
  const base = resolveDefaultBranch(cwd, preferredBase);
725
- if (!branchExists(cwd, base)) return [];
744
+ if (!branchExists(cwd, base)) return out;
726
745
  let raw;
727
746
  try {
728
747
  raw = execFileSync(
@@ -731,28 +750,25 @@ function detectMergedInProgress(cwd, preferredBase, tasks) {
731
750
  { cwd, encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"] }
732
751
  );
733
752
  } catch {
734
- return [];
753
+ return out;
735
754
  }
736
755
  const commits = raw.split("\n").map((line) => {
737
756
  const idx = line.indexOf("");
738
757
  if (idx === -1) return null;
739
758
  return { hash: line.slice(0, idx).trim(), subject: line.slice(idx + 1).trim() };
740
759
  }).filter((c) => c !== null && c.hash !== "");
741
- const hits = [];
742
- for (const task of inProgress) {
743
- const re = new RegExp(`(^|[^\\w-])${escapeRegexLiteral(task.displayId)}([^\\w-]|$)`);
760
+ for (const displayId of displayIds) {
761
+ const re = new RegExp(`(^|[^\\w-])${escapeRegexLiteral(displayId)}([^\\w-]|$)`);
744
762
  const hit = commits.find((c) => re.test(c.subject));
745
763
  if (!hit) continue;
746
764
  const prMatch = hit.subject.match(/#(\d+)/);
747
- hits.push({
748
- displayId: task.displayId,
749
- title: task.title,
765
+ out.set(displayId, {
750
766
  commit: hit.hash,
751
767
  subject: hit.subject,
752
768
  pr: prMatch ? `#${prMatch[1]}` : null
753
769
  });
754
770
  }
755
- return hits;
771
+ return out;
756
772
  }
757
773
  function detectUnrecordedCommits(cwd, baseBranch) {
758
774
  if (!isGitAvailable() || !isGitRepo(cwd)) return [];
package/dist/index.js CHANGED
@@ -26,6 +26,7 @@ __export(git_exports, {
26
26
  detectUnrecordedCommits: () => detectUnrecordedCommits,
27
27
  ensureLatestDevelop: () => ensureLatestDevelop,
28
28
  ensureTagAtHead: () => ensureTagAtHead,
29
+ findTaskCommitsOnBase: () => findTaskCommitsOnBase,
29
30
  getBranchDiff: () => getBranchDiff,
30
31
  getCommitsSinceTag: () => getCommitsSinceTag,
31
32
  getCurrentBranch: () => getCurrentBranch,
@@ -719,11 +720,29 @@ function escapeRegexLiteral(s) {
719
720
  return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
720
721
  }
721
722
  function detectMergedInProgress(cwd, preferredBase, tasks) {
722
- if (!isGitAvailable() || !isGitRepo(cwd)) return [];
723
723
  const inProgress = tasks.filter((t) => t.status === "In Progress");
724
724
  if (inProgress.length === 0) return [];
725
+ const landed = findTaskCommitsOnBase(cwd, preferredBase, inProgress.map((t) => t.displayId));
726
+ const hits = [];
727
+ for (const task of inProgress) {
728
+ const hit = landed.get(task.displayId);
729
+ if (!hit) continue;
730
+ hits.push({
731
+ displayId: task.displayId,
732
+ title: task.title,
733
+ commit: hit.commit,
734
+ subject: hit.subject,
735
+ pr: hit.pr
736
+ });
737
+ }
738
+ return hits;
739
+ }
740
+ function findTaskCommitsOnBase(cwd, preferredBase, displayIds) {
741
+ const out = /* @__PURE__ */ new Map();
742
+ if (displayIds.length === 0) return out;
743
+ if (!isGitAvailable() || !isGitRepo(cwd)) return out;
725
744
  const base = resolveDefaultBranch(cwd, preferredBase);
726
- if (!branchExists(cwd, base)) return [];
745
+ if (!branchExists(cwd, base)) return out;
727
746
  let raw;
728
747
  try {
729
748
  raw = execFileSync(
@@ -732,28 +751,25 @@ function detectMergedInProgress(cwd, preferredBase, tasks) {
732
751
  { cwd, encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"] }
733
752
  );
734
753
  } catch {
735
- return [];
754
+ return out;
736
755
  }
737
756
  const commits = raw.split("\n").map((line) => {
738
757
  const idx = line.indexOf("");
739
758
  if (idx === -1) return null;
740
759
  return { hash: line.slice(0, idx).trim(), subject: line.slice(idx + 1).trim() };
741
760
  }).filter((c) => c !== null && c.hash !== "");
742
- const hits = [];
743
- for (const task of inProgress) {
744
- const re = new RegExp(`(^|[^\\w-])${escapeRegexLiteral(task.displayId)}([^\\w-]|$)`);
761
+ for (const displayId of displayIds) {
762
+ const re = new RegExp(`(^|[^\\w-])${escapeRegexLiteral(displayId)}([^\\w-]|$)`);
745
763
  const hit = commits.find((c) => re.test(c.subject));
746
764
  if (!hit) continue;
747
765
  const prMatch = hit.subject.match(/#(\d+)/);
748
- hits.push({
749
- displayId: task.displayId,
750
- title: task.title,
766
+ out.set(displayId, {
751
767
  commit: hit.hash,
752
768
  subject: hit.subject,
753
769
  pr: prMatch ? `#${prMatch[1]}` : null
754
770
  });
755
771
  }
756
- return hits;
772
+ return out;
757
773
  }
758
774
  function detectUnrecordedCommits(cwd, baseBranch) {
759
775
  if (!isGitAvailable() || !isGitRepo(cwd)) return [];
@@ -13096,7 +13112,7 @@ var planPrepareCache = new PerCallerCache();
13096
13112
  var planTool = {
13097
13113
  name: "plan",
13098
13114
  description: 'Run once per cycle to select tasks and generate BUILD HANDOFFs. Call after setup (first time) or after completing all builds AND running release for the previous cycle. Returns prioritised task recommendations with detailed implementation specs. NEVER call when unbuilt cycle tasks exist \u2014 build and release first. First call returns a planning prompt for you to execute (prepare phase). Then call again with mode "apply" and your output to write results. Use skip_handoffs=true for large backlogs \u2014 handoffs are then generated separately via `handoff_generate`.',
13099
- annotations: { readOnlyHint: false, destructiveHint: false },
13115
+ annotations: { title: "Plan Cycle", readOnlyHint: false, destructiveHint: false, openWorldHint: false },
13100
13116
  inputSchema: {
13101
13117
  type: "object",
13102
13118
  properties: {
@@ -13641,15 +13657,34 @@ async function findSimilarTasks(adapter2, ideaTitle) {
13641
13657
  return matches.sort((a, b2) => b2.coverage - a.coverage).slice(0, 3);
13642
13658
  }
13643
13659
  var AD_ALIGNMENT_OVERLAP_THRESHOLD = 3;
13660
+ var DOC_FREQ_CEILING = 0.35;
13661
+ function findUbiquitousWords(ads) {
13662
+ const live = ads.filter((a) => !a.superseded);
13663
+ const ubiquitous = /* @__PURE__ */ new Set();
13664
+ if (live.length < 5) return ubiquitous;
13665
+ const docFreq = /* @__PURE__ */ new Map();
13666
+ for (const ad of live) {
13667
+ for (const w of extractKeywords(`${ad.title} ${ad.body}`)) {
13668
+ docFreq.set(w, (docFreq.get(w) ?? 0) + 1);
13669
+ }
13670
+ }
13671
+ const ceiling = live.length * DOC_FREQ_CEILING;
13672
+ for (const [w, df] of docFreq) {
13673
+ if (df > ceiling) ubiquitous.add(w);
13674
+ }
13675
+ return ubiquitous;
13676
+ }
13644
13677
  function findAdAlignmentMatches(ideaText, ads) {
13645
13678
  const ideaKeywords = extractKeywords(ideaText);
13646
13679
  if (ideaKeywords.size < AD_ALIGNMENT_OVERLAP_THRESHOLD) return [];
13680
+ const ubiquitous = findUbiquitousWords(ads);
13647
13681
  const matches = [];
13648
13682
  for (const ad of ads) {
13649
13683
  if (ad.superseded) continue;
13650
13684
  const adKeywords = extractKeywords(`${ad.title} ${ad.body}`);
13651
13685
  let overlap = 0;
13652
13686
  for (const w of ideaKeywords) {
13687
+ if (ubiquitous.has(w)) continue;
13653
13688
  if (adKeywords.has(w)) overlap++;
13654
13689
  }
13655
13690
  if (overlap >= AD_ALIGNMENT_OVERLAP_THRESHOLD) {
@@ -15494,7 +15529,7 @@ var reviewPrepareCache = new PerCallerCache();
15494
15529
  var strategyReviewTool = {
15495
15530
  name: "strategy_review",
15496
15531
  description: 'Run a Strategy Review \u2014 assesses project direction, velocity, and Active Decisions. Produces recommendations and potential AD updates that feed into the next plan. Offered every 5 cycles; hard-blocked at 7+ overdue cycles. Run it in your current conversation \u2014 only start a fresh one if you are genuinely under context pressure (your host just compacted, you are near the context limit, or the session is heavy with build context), not just because a review is next. First call returns a review prompt for you to execute (prepare phase). Then call again with mode "apply" and your output. Pass `force: true` to run before the cadence gate.',
15497
- annotations: { readOnlyHint: false, destructiveHint: false },
15532
+ annotations: { title: "Run Strategy Review", readOnlyHint: false, destructiveHint: false, openWorldHint: false },
15498
15533
  inputSchema: {
15499
15534
  type: "object",
15500
15535
  properties: {
@@ -15531,7 +15566,7 @@ var strategyReviewTool = {
15531
15566
  var strategyAgendaTool = {
15532
15567
  name: "strategy_agenda",
15533
15568
  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.',
15534
- annotations: { readOnlyHint: false, destructiveHint: false },
15569
+ annotations: { title: "Strategy Agenda", readOnlyHint: false, destructiveHint: false, openWorldHint: false },
15535
15570
  inputSchema: {
15536
15571
  type: "object",
15537
15572
  properties: {
@@ -15559,7 +15594,7 @@ var strategyAgendaTool = {
15559
15594
  var strategyChangeTool = {
15560
15595
  name: "strategy_change",
15561
15596
  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. In "capture" mode, pass north_star to directly set/update the project North Star (no decision text needed).',
15562
- annotations: { readOnlyHint: false, destructiveHint: false },
15597
+ annotations: { title: "Change Strategy", readOnlyHint: false, destructiveHint: false, openWorldHint: false },
15563
15598
  inputSchema: {
15564
15599
  type: "object",
15565
15600
  properties: {
@@ -16018,7 +16053,7 @@ async function archiveTasks(adapter2, phases, statuses) {
16018
16053
  var boardViewTool = {
16019
16054
  name: "board_view",
16020
16055
  description: 'View the Board. To find a SPECIFIC task or subset, FILTER FIRST \u2014 do not dump the whole board: pass task_id for one task (full detail), query="<text>" for a title/notes substring match, or cycle=<n> for one cycle. Combine with status/phase. By default shows active tasks only (excludes Done/Cancelled), sorted by priority, limited to 50; titles are truncated in the table (single-task lookup shows the full title). Use status="all" to see everything, mode="summary" for counts only. Does not call the Anthropic API.',
16021
- annotations: { readOnlyHint: true, destructiveHint: false },
16056
+ annotations: { title: "View Board", readOnlyHint: true, destructiveHint: false, openWorldHint: false },
16022
16057
  inputSchema: {
16023
16058
  type: "object",
16024
16059
  properties: {
@@ -16062,7 +16097,7 @@ var boardViewTool = {
16062
16097
  var boardDeprioritiseTool = {
16063
16098
  name: "board_deprioritise",
16064
16099
  description: `Remove a task from the current cycle. Four actions: "backlog" (not now, maybe later \u2014 preserves handoff), "defer" (valid but premature \u2014 hidden from planner), "block" (waiting on external dependency \u2014 visible on board but skipped by planner), "cancel" (don't want this \u2014 permanently closed with reason). When a user rejects a task, ALWAYS ask which action they want. Does not call the Anthropic API.`,
16065
- annotations: { readOnlyHint: false, destructiveHint: true },
16100
+ annotations: { title: "Deprioritise Task", readOnlyHint: false, destructiveHint: true, openWorldHint: false },
16066
16101
  inputSchema: {
16067
16102
  type: "object",
16068
16103
  properties: {
@@ -16107,7 +16142,7 @@ var boardDeprioritiseTool = {
16107
16142
  var boardArchiveTool = {
16108
16143
  name: "board_archive",
16109
16144
  description: "Archive tasks from the Board to the archive file. When both phase and status are provided, only tasks matching BOTH are archived (AND logic). When only one is provided, all matching tasks are archived. Does not call the Anthropic API.",
16110
- annotations: { readOnlyHint: false, destructiveHint: true },
16145
+ annotations: { title: "Archive Task", readOnlyHint: false, destructiveHint: true, openWorldHint: false },
16111
16146
  inputSchema: {
16112
16147
  type: "object",
16113
16148
  properties: {
@@ -16126,7 +16161,7 @@ var boardArchiveTool = {
16126
16161
  var boardEditTool = {
16127
16162
  name: "board_edit",
16128
16163
  description: "Edit fields on an existing task. Supports title, priority, complexity, module, epic, phase, notes (with notes_mode for append/replace/clear), status, maturity, and cycle (number or null). Pass task_id plus any fields to update. Does not call the Anthropic API.",
16129
- annotations: { readOnlyHint: false, destructiveHint: false },
16164
+ annotations: { title: "Edit Task", readOnlyHint: false, destructiveHint: false, openWorldHint: false },
16130
16165
  inputSchema: {
16131
16166
  type: "object",
16132
16167
  properties: {
@@ -18069,7 +18104,7 @@ async function ensureMcpJsonGitignored(projectRoot) {
18069
18104
  var setupTool = {
18070
18105
  name: "setup",
18071
18106
  description: 'Create a new PAPI-tracked project \u2014 generates your Product Brief, Active Decisions, and CLAUDE.md workflow instructions. Run after configuring your MCP credentials (via `init` or manually from getpapi.ai). Only project_name is required \u2014 description and target_users are derived from README, package.json, and commit history when omitted. Set existing_project: true to adopt an existing codebase. ADOPTING AN EXISTING PROJECT OVER A REMOTE/HOSTED CONNECTOR (no local stdio install): PAPI cannot read your filesystem, so YOU (the client) must gather a `codebase_scan` and pass it in \u2014 list top-level dirs/files, the package manifest, the README (first ~3000 chars), and recent commit subjects. Without it, adoption falls back to asking for description/target_users. On a local stdio install PAPI scans the tree itself, so `codebase_scan` is optional there. First call returns prompts (prepare phase), then call again with mode "apply" and your outputs. After setup, run `plan` to start your first cycle.',
18072
- annotations: { readOnlyHint: false, destructiveHint: false },
18107
+ annotations: { title: "Set Up Project", readOnlyHint: false, destructiveHint: false, openWorldHint: false },
18073
18108
  inputSchema: {
18074
18109
  type: "object",
18075
18110
  properties: {
@@ -19068,15 +19103,35 @@ async function createRelease(config2, branch, version, adapter2, cycleNum, optio
19068
19103
  if (adapter2 && resolvedCycleNum > 0) {
19069
19104
  try {
19070
19105
  const stampedAt = (/* @__PURE__ */ new Date()).toISOString();
19106
+ const resolvedBase = resolveBaseBranch(config2.projectRoot, branch);
19071
19107
  const doneCycleTasks = (await adapter2.queryBoard()).filter(
19072
19108
  (t) => t.cycle === resolvedCycleNum && t.status === "Done"
19073
19109
  );
19110
+ const canVerify = isGitAvailable() && isGitRepo(config2.projectRoot);
19111
+ const landed = canVerify ? findTaskCommitsOnBase(
19112
+ config2.projectRoot,
19113
+ resolvedBase,
19114
+ doneCycleTasks.map((t) => t.displayId)
19115
+ ) : /* @__PURE__ */ new Map();
19116
+ const unverified = [];
19074
19117
  for (const t of doneCycleTasks) {
19118
+ const hit = landed.get(t.displayId);
19119
+ if (canVerify && !hit) {
19120
+ unverified.push(
19121
+ `${t.displayId}${t.branchName ? ` (branch \`${t.branchName}\`)` : " (no branch recorded)"}`
19122
+ );
19123
+ continue;
19124
+ }
19075
19125
  try {
19076
19126
  await adapter2.updateTask(t.id, { mergedAt: stampedAt });
19077
19127
  } catch {
19078
19128
  }
19079
19129
  }
19130
+ if (unverified.length > 0) {
19131
+ warnings.push(
19132
+ `\u26A0\uFE0F DATA-INTEGRITY \u2014 ${unverified.length} task(s) are marked **Done** in cycle ${resolvedCycleNum} but their work is NOT on ${resolvedBase}: ${unverified.join("; ")}. They were NOT stamped as merged. Held ad-hoc work is the usual cause (\`ad_hoc\` + \`board_edit\` leaves the branch unmerged by design). Merge the branch(es), or move the task(s) to the next cycle \u2014 do not leave Done unbacked by git.`
19133
+ );
19134
+ }
19080
19135
  } catch {
19081
19136
  }
19082
19137
  }
@@ -19136,6 +19191,44 @@ async function createRelease(config2, branch, version, adapter2, cycleNum, optio
19136
19191
  };
19137
19192
  }
19138
19193
 
19194
+ // src/services/release-bookkeeping.ts
19195
+ async function beginRelease(tracker, cycleNum) {
19196
+ tracker.setStreamScope({ cycle: cycleNum ?? null });
19197
+ await tracker.recordStep("release_starting");
19198
+ }
19199
+ async function recordReadinessVerified(tracker) {
19200
+ await tracker.recordStep("readiness_verified");
19201
+ }
19202
+ async function recordQualityGate(tracker, decision, caps) {
19203
+ await tracker.recordStep("quality_gate", {
19204
+ status: decision.stepStatus,
19205
+ capabilityKey: "releaseGate",
19206
+ capabilityEnabled: isCapabilityEnabled(caps, "releaseGate"),
19207
+ metadata: decision.metadata
19208
+ });
19209
+ }
19210
+ async function completeRelease(tracker, opts) {
19211
+ tracker.setStreamScope({ cycle: opts.cycleClosed ?? null });
19212
+ await tracker.recordStep("cycle_complete", { metadata: { version: opts.version } });
19213
+ for (const m of opts.branchMerges ?? []) {
19214
+ await tracker.recordStep("branch_merged", {
19215
+ metadata: { branch: m.branch, prUrl: m.prUrl ?? null }
19216
+ });
19217
+ }
19218
+ await tracker.recordStep("changelog", {
19219
+ capabilityKey: "changelog",
19220
+ capabilityEnabled: isCapabilityEnabled(opts.caps, "changelog"),
19221
+ status: opts.changelogEmitted ? "complete" : "active"
19222
+ });
19223
+ if (opts.deployHookEmitted) {
19224
+ await tracker.recordStep("deploy_hook", {
19225
+ capabilityKey: "deployHook",
19226
+ capabilityEnabled: isCapabilityEnabled(opts.caps, "deployHook")
19227
+ });
19228
+ }
19229
+ await tracker.recordStep("released", { metadata: { version: opts.version } });
19230
+ }
19231
+
19139
19232
  // src/tools/release.ts
19140
19233
  init_git();
19141
19234
 
@@ -19354,7 +19447,7 @@ async function postReleaseToX(version, cycleClosed) {
19354
19447
  var releaseTool = {
19355
19448
  name: "release",
19356
19449
  description: "Cut a versioned release \u2014 creates a git tag, generates CHANGELOG.md, and pushes to remote. Pass skipVersion=true to update CHANGELOG and close the cycle without creating a tag or bumping version numbers.",
19357
- annotations: { readOnlyHint: false, destructiveHint: true },
19450
+ annotations: { title: "Cut Release", readOnlyHint: false, destructiveHint: true, openWorldHint: false },
19358
19451
  inputSchema: {
19359
19452
  type: "object",
19360
19453
  properties: {
@@ -19425,9 +19518,16 @@ async function handleRelease(adapter2, config2, args) {
19425
19518
  }
19426
19519
  const tracker = new ProgressTracker("validate-args").bindStream(adapter2, { stage: "release" });
19427
19520
  try {
19428
- await tracker.recordStep("release_starting");
19429
19521
  tracker.mark("owner-identity-guard");
19430
19522
  const gate = await resolveOwnerGate(adapter2, config2);
19523
+ let cycleToClose = null;
19524
+ try {
19525
+ const resolved = await resolveCycleToClose(adapter2, version, gate.callerUserId);
19526
+ cycleToClose = resolved > 0 ? resolved : null;
19527
+ } catch {
19528
+ cycleToClose = null;
19529
+ }
19530
+ await beginRelease(tracker, cycleToClose);
19431
19531
  const productionBaseBranch = resolveBaseBranch(config2.projectRoot, config2.baseBranch);
19432
19532
  if (branch === productionBaseBranch) {
19433
19533
  if (gate.enforced && !gate.callerIsOwner) {
@@ -19492,6 +19592,13 @@ Next: run \`plan\` to start your next cycle.`
19492
19592
  }
19493
19593
  }
19494
19594
  }
19595
+ let caps = {};
19596
+ try {
19597
+ const info = adapter2.getProjectInfo ? await adapter2.getProjectInfo() : null;
19598
+ caps = info?.capabilities ?? {};
19599
+ } catch {
19600
+ caps = {};
19601
+ }
19495
19602
  if (isHostedTransport()) {
19496
19603
  tracker.mark("hosted-close-cycle");
19497
19604
  let closed;
@@ -19504,6 +19611,16 @@ Next: run \`plan\` to start your next cycle.`
19504
19611
  const message = err instanceof Error ? err.message : String(err);
19505
19612
  return errorResponse(message);
19506
19613
  }
19614
+ await recordReadinessVerified(tracker);
19615
+ const hostedGate = evaluateReleaseGate(caps, config2.gateCommand, gateResult);
19616
+ await recordQualityGate(tracker, hostedGate, caps);
19617
+ await completeRelease(tracker, {
19618
+ cycleClosed: closed.resolvedCycleNum > 0 ? closed.resolvedCycleNum : null,
19619
+ version,
19620
+ caps,
19621
+ branchMerges: [],
19622
+ changelogEmitted: false
19623
+ });
19507
19624
  const tagAnnotation = `Release ${version}`;
19508
19625
  const cyclePart = closed.resolvedCycleNum > 0 ? `Cycle ${closed.resolvedCycleNum}` : "The cycle";
19509
19626
  const warningsBlock = closed.warnings.length > 0 ? `
@@ -19564,22 +19681,10 @@ Run \`project_switch <slug>\` to switch the active PAPI project, or verify your
19564
19681
  console.error(`[release] gh CLI not available \u2014 ${pendingGrouped.length} shared branch(es) will be squash-merged via git: ${pendingGrouped.join(", ")}`);
19565
19682
  }
19566
19683
  }
19567
- await tracker.recordStep("readiness_verified");
19568
- let caps = {};
19569
- try {
19570
- const info = adapter2.getProjectInfo ? await adapter2.getProjectInfo() : null;
19571
- caps = info?.capabilities ?? {};
19572
- } catch {
19573
- caps = {};
19574
- }
19684
+ await recordReadinessVerified(tracker);
19575
19685
  tracker.mark("quality-gate");
19576
19686
  const gateDecision = evaluateReleaseGate(caps, config2.gateCommand, gateResult);
19577
- await tracker.recordStep("quality_gate", {
19578
- status: gateDecision.stepStatus,
19579
- capabilityKey: "releaseGate",
19580
- capabilityEnabled: isCapabilityEnabled(caps, "releaseGate"),
19581
- metadata: gateDecision.metadata
19582
- });
19687
+ await recordQualityGate(tracker, gateDecision, caps);
19583
19688
  if (gateDecision.action === "directive") {
19584
19689
  return textResponse(gateDecision.message);
19585
19690
  }
@@ -19592,13 +19697,6 @@ Run \`project_switch <slug>\` to switch the active PAPI project, or verify your
19592
19697
  skipVersion: skipVersion ?? false,
19593
19698
  callerUserId: gate.callerUserId
19594
19699
  });
19595
- tracker.setStreamScope({ cycle: result.cycleClosed ?? null });
19596
- await tracker.recordStep("cycle_complete", { metadata: { version: result.version } });
19597
- for (const m of result.groupedBranchMerges ?? []) {
19598
- await tracker.recordStep("branch_merged", {
19599
- metadata: { branch: m.branch, prUrl: m.prUrl ?? null }
19600
- });
19601
- }
19602
19700
  const lines = [
19603
19701
  `## Release ${result.version}${skipVersion ? " (skip version)" : ""}`,
19604
19702
  "",
@@ -19653,21 +19751,16 @@ Run \`project_switch <slug>\` to switch the active PAPI project, or verify your
19653
19751
  buildCycleUpdateCurationDirective(result.version, result.cycleClosed ?? 0)
19654
19752
  );
19655
19753
  if (cycleUpdateDirective) lines.push(cycleUpdateDirective);
19656
- await tracker.recordStep("changelog", {
19657
- capabilityKey: "changelog",
19658
- capabilityEnabled: isCapabilityEnabled(caps, "changelog"),
19659
- status: cycleUpdateDirective ? "complete" : "active"
19660
- });
19661
19754
  const deployDirective = buildDeployHookDirective(caps, config2.deployCommand);
19662
- if (deployDirective) {
19663
- lines.push(deployDirective);
19664
- await tracker.recordStep("deploy_hook", {
19665
- capabilityKey: "deployHook",
19666
- capabilityEnabled: isCapabilityEnabled(caps, "deployHook"),
19667
- status: "complete",
19668
- metadata: { configured: true }
19669
- });
19670
- }
19755
+ if (deployDirective) lines.push(deployDirective);
19756
+ await completeRelease(tracker, {
19757
+ cycleClosed: result.cycleClosed ?? null,
19758
+ version: result.version,
19759
+ caps,
19760
+ branchMerges: result.groupedBranchMerges ?? [],
19761
+ changelogEmitted: Boolean(cycleUpdateDirective),
19762
+ deployHookEmitted: Boolean(deployDirective)
19763
+ });
19671
19764
  const publishEnabled = isCapabilityEnabled(caps, "publishDirective");
19672
19765
  if (publishEnabled) {
19673
19766
  try {
@@ -19695,7 +19788,6 @@ Run \`project_switch <slug>\` to switch the active PAPI project, or verify your
19695
19788
  const verifyDirective = buildVerifyHealthCheckDirective(caps);
19696
19789
  if (verifyDirective) lines.push(verifyDirective);
19697
19790
  lines.push("", `Next: cycle released! Run \`plan\` to start your next cycle, or \`idea "<what's next>"\` first if your backlog is thin.`);
19698
- await tracker.recordStep("released", { metadata: { version: result.version } });
19699
19791
  const filesToWriteSection = result.filesToWrite ? formatFilesToWriteSection(result.filesToWrite) : "";
19700
19792
  return textResponse(lines.join("\n") + filesToWriteSection);
19701
19793
  } catch (err) {
@@ -19855,32 +19947,94 @@ function capitalizeCompleted(value) {
19855
19947
  };
19856
19948
  return map[value] ?? "No";
19857
19949
  }
19950
+ function stripAnnotations(text) {
19951
+ let out = "";
19952
+ let i = 0;
19953
+ while (i < text.length) {
19954
+ const ch = text[i];
19955
+ if (ch === ")") {
19956
+ i++;
19957
+ continue;
19958
+ }
19959
+ if (ch === "(") {
19960
+ let depth = 0;
19961
+ let j = i;
19962
+ for (; j < text.length; j++) {
19963
+ if (text[j] === "(") depth++;
19964
+ else if (text[j] === ")") {
19965
+ depth--;
19966
+ if (depth === 0) {
19967
+ j++;
19968
+ break;
19969
+ }
19970
+ }
19971
+ }
19972
+ if (depth !== 0) {
19973
+ break;
19974
+ }
19975
+ const inner = text.slice(i + 1, j - 1);
19976
+ const prev = i > 0 ? text[i - 1] : "/";
19977
+ const next = text[j] ?? "";
19978
+ const isRouteGroup = (prev === "/" || i === 0) && !/\s/.test(inner) && next === "/";
19979
+ if (isRouteGroup) {
19980
+ out += text.slice(i, j);
19981
+ i = j;
19982
+ continue;
19983
+ }
19984
+ i = j;
19985
+ continue;
19986
+ }
19987
+ out += ch;
19988
+ i++;
19989
+ }
19990
+ return out;
19991
+ }
19858
19992
  function sanitisePredictedFiles(raw) {
19859
19993
  const out = [];
19860
19994
  for (const entry of raw) {
19861
19995
  if (typeof entry !== "string") continue;
19862
- for (const segment of entry.split(";")) {
19863
- const altMatches = segment.match(/\(\s*or\s+`([^`]+)`\s*\)/gi) ?? [];
19864
- for (const alt of altMatches) {
19865
- const inner = alt.match(/`([^`]+)`/);
19866
- if (inner) out.push(inner[1].trim());
19867
- }
19868
- const cleaned = segment.replace(/`/g, "").replace(/\s*\(.*$/, "").trim();
19869
- if (cleaned) out.push(cleaned);
19996
+ let working = entry;
19997
+ const altMatches = working.match(/\(\s*or\s+`([^`]+)`\s*\)/gi) ?? [];
19998
+ for (const alt of altMatches) {
19999
+ const inner = alt.match(/`([^`]+)`/);
20000
+ if (inner) out.push(inner[1].trim());
20001
+ working = working.replace(alt, " ");
20002
+ }
20003
+ const cleanedEntry = stripAnnotations(working.replace(/`/g, ""));
20004
+ for (const segment of cleanedEntry.split(/[;,]/)) {
20005
+ const cleaned = segment.trim();
20006
+ if (!cleaned) continue;
20007
+ if (/\s/.test(cleaned)) continue;
20008
+ if (!/[A-Za-z0-9]/.test(cleaned)) continue;
20009
+ out.push(cleaned);
19870
20010
  }
19871
20011
  }
19872
20012
  return Array.from(new Set(out));
19873
20013
  }
20014
+ function globToRegExp(entry) {
20015
+ const source = entry.split(/(\*\*|\*)/).map((part) => {
20016
+ if (part === "**") return ".*";
20017
+ if (part === "*") return "[^/]*";
20018
+ return part.replace(/[.+?^${}()|[\]\\]/g, "\\$&");
20019
+ }).join("");
20020
+ return new RegExp(`^${source}$`, "i");
20021
+ }
19874
20022
  function pathBasename(p) {
19875
20023
  const parts = p.replace(/\\/g, "/").replace(/\/+$/, "").split("/");
19876
20024
  return parts[parts.length - 1] ?? p;
19877
20025
  }
19878
20026
  function isPathInPredictedScope(changedPath, predicted) {
19879
- const changedLower = changedPath.replace(/\\/g, "/").toLowerCase();
20027
+ const changed = changedPath.replace(/\\/g, "/");
20028
+ const changedLower = changed.toLowerCase();
19880
20029
  const changedBase = pathBasename(changedPath);
19881
20030
  for (const raw of predicted) {
19882
20031
  const entry = raw.replace(/\\/g, "/").replace(/\/+$/, "").trim();
19883
20032
  if (!entry) continue;
20033
+ if (entry.includes("*")) {
20034
+ if (globToRegExp(entry).test(changed)) return true;
20035
+ if (!entry.includes("/") && globToRegExp(entry).test(changedBase)) return true;
20036
+ continue;
20037
+ }
19884
20038
  if (pathBasename(entry) === changedBase) return true;
19885
20039
  const entryLower = entry.toLowerCase();
19886
20040
  if (changedLower === entryLower || changedLower.startsWith(`${entryLower}/`)) {
@@ -19924,23 +20078,27 @@ function autoCommit(config2, taskId, taskTitle, predictedFiles) {
19924
20078
  };
19925
20079
  const cleanedPredicted = sanitisePredictedFiles(predictedFiles);
19926
20080
  const scoped = modified.filter((p) => isPathInPredictedScope(p, cleanedPredicted));
19927
- if (scoped.length === 0) {
20081
+ const untracked = getUntrackedFiles(cwd);
20082
+ const scopedSet = new Set(scoped);
20083
+ const untrackedInScope = untracked.filter(
20084
+ (p) => !scopedSet.has(p) && isPathInPredictedScope(p, cleanedPredicted)
20085
+ );
20086
+ if (scoped.length === 0 && untrackedInScope.length === 0) {
19928
20087
  const modSample = modified.slice(0, 5).join(", ");
19929
20088
  const predSample = cleanedPredicted.slice(0, 5).join(", ");
19930
20089
  return `Auto-commit: refused \u2014 none of the ${modified.length} modified file(s) intersect FILES LIKELY TOUCHED. Modified: ${modSample}. Expected: ${predSample}. Stage the intended files manually (\`git add <paths>\`) then re-run, or set PAPI_AUTO_COMMIT=false.`;
19931
20090
  }
19932
- const untracked = getUntrackedFiles(cwd);
19933
20091
  const scopedDirs = [...new Set(scoped.map(dirname7).filter((d) => d.length > 0))];
19934
20092
  const isUnderScopedDir = (p) => scopedDirs.some((d) => p === d || p.startsWith(`${d}/`) || p.startsWith(`${d}\\`));
19935
- const scopedSet = new Set(scoped);
20093
+ const inScopeSet = /* @__PURE__ */ new Set([...scoped, ...untrackedInScope]);
19936
20094
  const adjacentUntracked = untracked.filter(
19937
- (p) => !scopedSet.has(p) && isUnderScopedDir(p)
20095
+ (p) => !inScopeSet.has(p) && isUnderScopedDir(p)
19938
20096
  );
19939
- const toStage = [...scoped, ...adjacentUntracked];
20097
+ const toStage = [...scoped, ...untrackedInScope, ...adjacentUntracked];
19940
20098
  const toStageSet = new Set(toStage);
19941
20099
  const droppedUntracked = untracked.filter((p) => !toStageSet.has(p));
19942
20100
  const droppedModified = modified.filter((p) => !scopedSet.has(p));
19943
- let line = safeRun(() => stagePathsAndCommit(cwd, toStage, message)) + ` (scoped to ${scoped.length}/${modified.length} files via FILES LIKELY TOUCHED` + (adjacentUntracked.length > 0 ? ` + ${adjacentUntracked.length} untracked under scoped dir(s)` : "") + `).`;
20101
+ let line = safeRun(() => stagePathsAndCommit(cwd, toStage, message)) + ` (scoped to ${scoped.length}/${modified.length} files via FILES LIKELY TOUCHED` + (untrackedInScope.length > 0 ? ` + ${untrackedInScope.length} new file(s) named in the handoff` : "") + (adjacentUntracked.length > 0 ? ` + ${adjacentUntracked.length} untracked under scoped dir(s)` : "") + `).`;
19944
20102
  if (droppedModified.length > 0) {
19945
20103
  const sample = droppedModified.slice(0, 10).join(", ");
19946
20104
  const more = droppedModified.length > 10 ? ` (+${droppedModified.length - 10} more)` : "";
@@ -20591,15 +20749,16 @@ async function completeBuild(adapter2, config2, taskId, input, options = {}, cli
20591
20749
  const adIds = report.relatedDecisions ?? [];
20592
20750
  const extractLearning = (text, category) => {
20593
20751
  if (!text || text === "None" || text.trim().length === 0) return;
20594
- const dotIdx = text.indexOf(". ");
20595
- const summary = dotIdx > 0 && dotIdx < 120 ? text.slice(0, dotIdx + 1) : text.slice(0, 120);
20596
- const tags = [];
20597
- if (taskModule) tags.push(taskModule.toLowerCase());
20598
20752
  let severity;
20599
20753
  if (category === "issue") {
20600
20754
  const sevMatch = text.match(/^(P[0-3])[\s:]/);
20601
20755
  if (sevMatch) severity = sevMatch[1];
20602
20756
  }
20757
+ const body = severity ? text.replace(/^P[0-3][\s:]+/, "").trim() : text.trim();
20758
+ const dotIdx = body.indexOf(". ");
20759
+ const summary = dotIdx > 0 ? body.slice(0, dotIdx + 1) : body;
20760
+ const tags = [];
20761
+ if (taskModule) tags.push(taskModule.toLowerCase());
20603
20762
  learnings.push({
20604
20763
  taskId: task.id,
20605
20764
  cycleNumber,
@@ -21000,7 +21159,7 @@ import { randomUUID as randomUUID12 } from "crypto";
21000
21159
  var docRegisterTool = {
21001
21160
  name: "doc_register",
21002
21161
  description: "Register or update a document in the doc registry. Called after finalising a research/planning doc, or when build_execute detects unregistered docs. Stores metadata and structured summary \u2014 not full content. Re-registering an existing doc updates its summary, tags, actions, type, and status (upsert). Visibility and owner are not changed on re-register.",
21003
- annotations: { readOnlyHint: false, destructiveHint: false },
21162
+ annotations: { title: "Register Doc", readOnlyHint: false, destructiveHint: false, openWorldHint: false },
21004
21163
  inputSchema: {
21005
21164
  type: "object",
21006
21165
  properties: {
@@ -21033,7 +21192,7 @@ var docRegisterTool = {
21033
21192
  var docSearchTool = {
21034
21193
  name: "doc_search",
21035
21194
  description: "Search the doc registry for documents by type, tags, keyword, or pending actions. Returns summaries, not full content. Use for context gathering in plan, strategy review, and idea dedup.",
21036
- annotations: { readOnlyHint: true, destructiveHint: false },
21195
+ annotations: { title: "Search Docs", readOnlyHint: true, destructiveHint: false, openWorldHint: false },
21037
21196
  inputSchema: {
21038
21197
  type: "object",
21039
21198
  properties: {
@@ -21051,7 +21210,7 @@ var docSearchTool = {
21051
21210
  var docScanTool = {
21052
21211
  name: "doc_scan",
21053
21212
  description: "Scan docs/ and plans directories for unregistered .md files. Returns a list of files not yet in the doc registry. Use this to find docs that need registration.",
21054
- annotations: { readOnlyHint: true, destructiveHint: false },
21213
+ annotations: { title: "Scan Docs", readOnlyHint: true, destructiveHint: false, openWorldHint: false },
21055
21214
  inputSchema: {
21056
21215
  type: "object",
21057
21216
  properties: {
@@ -21298,7 +21457,7 @@ async function handleDocScan(adapter2, config2, args) {
21298
21457
  var docActionPromoteTool = {
21299
21458
  name: "doc_action_promote",
21300
21459
  description: "Promote a single pending action from a registered doc into a Backlog task. The new task gets a `Reference:` line pointing to the source doc, and the doc action is marked resolved with `linkedTaskId` set. Use to close the research-to-action loop \u2014 turn unactioned findings into trackable cycle work. Identify the doc by `doc_path` (preferred) or `doc_id`, and the action by 0-based `action_index` (as listed in `doc_search` output).",
21301
- annotations: { readOnlyHint: false, destructiveHint: false },
21460
+ annotations: { title: "Promote Doc Action", readOnlyHint: false, destructiveHint: false, openWorldHint: false },
21302
21461
  inputSchema: {
21303
21462
  type: "object",
21304
21463
  properties: {
@@ -21397,7 +21556,7 @@ init_git();
21397
21556
  var buildListTool = {
21398
21557
  name: "build_list",
21399
21558
  description: "List cycle tasks that have BUILD HANDOFFs ready for execution. Shows task ID, title, status, priority, and complexity. In Progress tasks appear first, then Backlog. Does not call the Anthropic API.",
21400
- annotations: { readOnlyHint: true, destructiveHint: false },
21559
+ annotations: { title: "List Builds", readOnlyHint: true, destructiveHint: false, openWorldHint: false },
21401
21560
  inputSchema: {
21402
21561
  type: "object",
21403
21562
  properties: {
@@ -21412,7 +21571,7 @@ var buildListTool = {
21412
21571
  var buildDescribeTool = {
21413
21572
  name: "build_describe",
21414
21573
  description: "Show the full BUILD HANDOFF for a specific task, including scope, acceptance criteria, and implementation guidance. Does not call the Anthropic API.",
21415
- annotations: { readOnlyHint: true, destructiveHint: false },
21574
+ annotations: { title: "Describe Build", readOnlyHint: true, destructiveHint: false, openWorldHint: false },
21416
21575
  inputSchema: {
21417
21576
  type: "object",
21418
21577
  properties: {
@@ -21427,7 +21586,7 @@ var buildDescribeTool = {
21427
21586
  var buildExecuteTool = {
21428
21587
  name: "build_execute",
21429
21588
  description: "Start or complete a build task. Call with just task_id to start (returns BUILD HANDOFF, creates feature branch, marks In Progress). After implementing the task, you MUST call build_execute again with all report fields (completed, effort, estimated_effort, surprises, discovered_issues, architecture_notes) to finish \u2014 do not wait for user confirmation between start and complete. Never call on tasks that are already In Review or Done. Does not call the Anthropic API. Set light=true to skip branch/PR creation (commits to current branch). Set PAPI_LIGHT_MODE=true in env to default all builds to light mode.",
21430
- annotations: { readOnlyHint: false, destructiveHint: false },
21589
+ annotations: { title: "Run Build", readOnlyHint: false, destructiveHint: false, openWorldHint: false },
21431
21590
  inputSchema: {
21432
21591
  type: "object",
21433
21592
  properties: {
@@ -21541,7 +21700,7 @@ var buildExecuteTool = {
21541
21700
  var buildCancelTool = {
21542
21701
  name: "build_cancel",
21543
21702
  description: "Cancel a build task with a reason. Sets the task status to Cancelled and records the closure reason. Does not call the Anthropic API.",
21544
- annotations: { readOnlyHint: false, destructiveHint: true },
21703
+ annotations: { title: "Cancel Build", readOnlyHint: false, destructiveHint: true, openWorldHint: false },
21545
21704
  inputSchema: {
21546
21705
  type: "object",
21547
21706
  properties: {
@@ -22089,7 +22248,7 @@ Reason: ${result.reason}`);
22089
22248
  init_git();
22090
22249
  var ideaTool = {
22091
22250
  name: "idea",
22092
- annotations: { readOnlyHint: false, destructiveHint: false },
22251
+ annotations: { title: "Capture Idea", readOnlyHint: false, destructiveHint: false, openWorldHint: false },
22093
22252
  description: "Capture an idea as a Backlog task. The next plan run will triage and scope it. Use anytime to log bugs, feature requests, or improvements without interrupting the current cycle. IMPORTANT: If this idea originates from a research or planning session, you MUST include a Reference: line in notes pointing to the source doc. Without it, the planner has no context and will misinterpret the intent. Does not call the Anthropic API.",
22094
22253
  inputSchema: {
22095
22254
  type: "object",
@@ -22306,7 +22465,7 @@ function collectDiagnostics(config2) {
22306
22465
  var bugTool = {
22307
22466
  name: "bug",
22308
22467
  description: `Report a bug OR submit an idea. Routing: a bug about PAPI itself (a PAPI tool/MCP error, the connector, a handoff/cycle problem) auto-submits UPSTREAM to PAPI maintainers with diagnostics \u2014 you do NOT need to set report=true for these. A bug in the user's OWN project auto-files as a Backlog task on their board. IMPORTANT: report=true is ONLY for a genuine PAPI-product defect \u2014 it is NOT the catch-all for "not about my app". A bug in the user's harness/editor/OS/git/other tooling (e.g. Claude Code, Codex, VS Code, a shell command) is NOT a PAPI bug: file it on the user's own board (report=false / default) or that tool's own tracker, never upstream. Override the routing explicitly: report=true forces upstream (PAPI-product only), report=false forces the user's own board. Set \`type\` ("bug"/"idea") and optional notify-when-fixed / contact-ok consent for upstream submissions. Does not call the Anthropic API.`,
22309
- annotations: { readOnlyHint: false, destructiveHint: false },
22468
+ annotations: { title: "Report Bug", readOnlyHint: false, destructiveHint: false, openWorldHint: false },
22310
22469
  inputSchema: {
22311
22470
  type: "object",
22312
22471
  properties: {
@@ -22563,7 +22722,7 @@ function inferTaskType(description) {
22563
22722
  var adHocTool = {
22564
22723
  name: "ad_hoc",
22565
22724
  description: "Record work done outside the normal cycle. Creates a Done task with a lightweight build report, or associates work with an existing task if task_id is provided (without changing task status \u2014 use build_execute for status transitions). Use for quick fixes, bug patches, or ad-hoc changes. Does not call the Anthropic API.",
22566
- annotations: { readOnlyHint: false, destructiveHint: false },
22725
+ annotations: { title: "Record Ad-Hoc Work", readOnlyHint: false, destructiveHint: false, openWorldHint: false },
22567
22726
  inputSchema: {
22568
22727
  type: "object",
22569
22728
  properties: {
@@ -23053,7 +23212,7 @@ async function applyReconcile(adapter2, corrections) {
23053
23212
  var boardReconcileTool = {
23054
23213
  name: "board_reconcile",
23055
23214
  description: 'Holistic board review \u2014 backlog + deferred tasks in one pass. Surfaces strategic context (ADs, phases, docs), grouping signals, merge candidates, priority drift, and stale tasks. "prepare" assembles context for you to analyse; "apply" commits your decisions after user confirmation. Does not call the Anthropic API.',
23056
- annotations: { readOnlyHint: false, destructiveHint: false },
23215
+ annotations: { title: "Reconcile Board", readOnlyHint: false, destructiveHint: false, openWorldHint: false },
23057
23216
  inputSchema: {
23058
23217
  type: "object",
23059
23218
  properties: {
@@ -23546,7 +23705,7 @@ ${diffBlock}`;
23546
23705
  var reviewListTool = {
23547
23706
  name: "review_list",
23548
23707
  description: "List tasks ready for your sign-off \u2014 shows completed builds waiting for approval or feedback. Does not call the Anthropic API.",
23549
- annotations: { readOnlyHint: true, destructiveHint: false },
23708
+ annotations: { title: "List Reviews", readOnlyHint: true, destructiveHint: false, openWorldHint: false },
23550
23709
  inputSchema: {
23551
23710
  type: "object",
23552
23711
  properties: {
@@ -23561,7 +23720,7 @@ var reviewListTool = {
23561
23720
  var reviewSubmitTool = {
23562
23721
  name: "review_submit",
23563
23722
  description: "Record a review verdict on a completed build (build-acceptance) or task plan (handoff-review). ALWAYS ask the human for their verdict before calling \u2014 never auto-submit without human input. Accept moves the task to Done, request-changes sends it back for rework, reject discards the build. Updates task status based on the verdict. On handoff-review with suggested changes, returns a prompt to revise the BUILD HANDOFF.\n\nDO NOT use this tool as a substitute for review_list. If you need to see what is pending review, call review_list first. If review_list is unavailable in your tool set, STOP and tell the human their MCP integration is incomplete rather than guessing at the next pending task. (SUP-2026-010.)",
23564
- annotations: { readOnlyHint: false, destructiveHint: false },
23723
+ annotations: { title: "Submit Review", readOnlyHint: false, destructiveHint: false, openWorldHint: false },
23565
23724
  inputSchema: {
23566
23725
  type: "object",
23567
23726
  properties: {
@@ -24015,11 +24174,38 @@ Merge or squash those PRs first, then run \`release\` manually.`;
24015
24174
  } catch {
24016
24175
  }
24017
24176
  const version = `v0.${result.currentCycle}.0`;
24018
- const releaseResult = await createRelease(config2, baseBranch, version, adapter2, result.currentCycle);
24019
- await tracker.recordStep("auto_release_triggered", { metadata: { version: releaseResult.version } });
24020
- const pushInfo = releaseResult.pushNotes.join(" ");
24021
- const groupedMergeNote = releaseResult.groupedBranchMerges?.length ? "\n" + releaseResult.groupedBranchMerges.map((r) => `- Merged shared branch \`${r.branch}\` via PR: ${r.prUrl ?? "n/a"}`).join("\n") : "";
24022
- autoReleaseNote = `
24177
+ const autoGate = evaluateReleaseGate(caps, config2.gateCommand, void 0);
24178
+ if (autoGate.action !== "proceed") {
24179
+ autoReleaseNote = `
24180
+
24181
+ ---
24182
+
24183
+ \u26A0\uFE0F **Auto-release skipped** \u2014 a release quality gate is configured (\`${config2.gateCommand}\`), and it cannot be run from inside \`review_submit\`.
24184
+
24185
+ Run \`release\` manually: PAPI will hand you the gate command, then release once you report it green.`;
24186
+ } else {
24187
+ const releaseTracker = new ProgressTracker("auto-release").bindStream(adapter2, { stage: "release" });
24188
+ await beginRelease(releaseTracker, result.currentCycle);
24189
+ const releaseResult = await createRelease(config2, baseBranch, version, adapter2, result.currentCycle);
24190
+ await recordReadinessVerified(releaseTracker);
24191
+ await recordQualityGate(releaseTracker, autoGate, caps);
24192
+ await tracker.recordStep("auto_release_triggered", { metadata: { version: releaseResult.version } });
24193
+ const autoChangelogDirective = buildChangelogDirective(
24194
+ caps,
24195
+ buildCycleUpdateCurationDirective(releaseResult.version, releaseResult.cycleClosed ?? 0)
24196
+ );
24197
+ const autoDeployDirective = buildDeployHookDirective(caps, config2.deployCommand);
24198
+ await completeRelease(releaseTracker, {
24199
+ cycleClosed: releaseResult.cycleClosed ?? null,
24200
+ version: releaseResult.version,
24201
+ caps,
24202
+ branchMerges: releaseResult.groupedBranchMerges ?? [],
24203
+ changelogEmitted: Boolean(autoChangelogDirective),
24204
+ deployHookEmitted: Boolean(autoDeployDirective)
24205
+ });
24206
+ const pushInfo = releaseResult.pushNotes.join(" ");
24207
+ const groupedMergeNote = releaseResult.groupedBranchMerges?.length ? "\n" + releaseResult.groupedBranchMerges.map((r) => `- Merged shared branch \`${r.branch}\` via PR: ${r.prUrl ?? "n/a"}`).join("\n") : "";
24208
+ autoReleaseNote = `
24023
24209
 
24024
24210
  ---
24025
24211
 
@@ -24029,9 +24215,15 @@ Merge or squash those PRs first, then run \`release\` manually.`;
24029
24215
  - ${releaseResult.commitNote}
24030
24216
  - ${releaseResult.tagMessage}
24031
24217
  - ${pushInfo}` + groupedMergeNote + (releaseResult.warnings?.length ? `
24032
- - Warnings: ${releaseResult.warnings.join(", ")}` : "") + `
24218
+ - Warnings: ${releaseResult.warnings.join(", ")}` : "") + // task-2598 (C328): the auto path previously swallowed the curated
24219
+ // cycle-update directive that the manual path emits, so an auto-released
24220
+ // cycle never prompted the Discord post. Same directive, same gate.
24221
+ (autoChangelogDirective ? `
24222
+ ${autoChangelogDirective}` : "") + (autoDeployDirective ? `
24223
+ ${autoDeployDirective}` : "") + `
24033
24224
 
24034
24225
  Run \`plan\` to create Cycle ${result.currentCycle + 1}.`;
24226
+ }
24035
24227
  }
24036
24228
  }
24037
24229
  } catch (err) {
@@ -24113,7 +24305,7 @@ ${statusNote}${autoReviewNote}${securityNote}${unblockNote}${docClosureNote}${re
24113
24305
  var reviewClaimTool = {
24114
24306
  name: "review_claim",
24115
24307
  description: "Claim a Pending Review from the shared cross-user review queue so you are the one reviewing it. Atomic first-claim-wins \u2014 two reviewers cannot grab the same build. After claiming, run review_submit to record your verdict. Owner-or-active-member only. Does not call the Anthropic API.",
24116
- annotations: { readOnlyHint: false, destructiveHint: false },
24308
+ annotations: { title: "Claim Review", readOnlyHint: false, destructiveHint: false, openWorldHint: false },
24117
24309
  inputSchema: {
24118
24310
  type: "object",
24119
24311
  properties: {
@@ -24178,7 +24370,7 @@ import path5 from "path";
24178
24370
  var initTool = {
24179
24371
  name: "init",
24180
24372
  description: "Write the MCP config file that connects this project to PAPI. Generates .mcp.json (Claude Code default) or the equivalent for Cursor, VS Code, Windsurf, OpenCode, Amazon Q, Kilo Code, Gemini CLI, Codex CLI, or Hermes Agent. Config-only \u2014 does not create any project data. Run this first, then run `setup` to create your PAPI project.",
24181
- annotations: { readOnlyHint: false, destructiveHint: false },
24373
+ annotations: { title: "Initialise Project", readOnlyHint: false, destructiveHint: false, openWorldHint: false },
24182
24374
  inputSchema: {
24183
24375
  type: "object",
24184
24376
  properties: {
@@ -25087,6 +25279,104 @@ function formatUnblockSection(candidates) {
25087
25279
  return lines.join("\n");
25088
25280
  }
25089
25281
 
25282
+ // src/lib/deferred-gate.ts
25283
+ var GATE_PHRASES = [
25284
+ "depends on",
25285
+ "depends upon",
25286
+ "gated on",
25287
+ "gated behind",
25288
+ "blocked on",
25289
+ "blocked by",
25290
+ "waiting on",
25291
+ "waiting for",
25292
+ "pending",
25293
+ "once",
25294
+ "after",
25295
+ "until",
25296
+ "requires",
25297
+ "needs",
25298
+ "pull condition",
25299
+ "unblocked by"
25300
+ ];
25301
+ var CONTEXT_WINDOW = 60;
25302
+ var TASK_REF2 = /\btask-\d+\b/gi;
25303
+ var MAX_PER_BUCKET = 8;
25304
+ function extractGatedTaskRefs(notes) {
25305
+ if (!notes) return [];
25306
+ const out = [];
25307
+ const seen = /* @__PURE__ */ new Set();
25308
+ for (const m of notes.matchAll(TASK_REF2)) {
25309
+ const ref = m[0].toLowerCase();
25310
+ if (seen.has(ref)) continue;
25311
+ const start = Math.max(0, (m.index ?? 0) - CONTEXT_WINDOW);
25312
+ const before = notes.slice(start, m.index).toLowerCase();
25313
+ const hit = GATE_PHRASES.find((p) => before.includes(p));
25314
+ if (!hit) continue;
25315
+ seen.add(ref);
25316
+ const evidence = notes.slice(start, (m.index ?? 0) + m[0].length).trim();
25317
+ out.push({ ref, evidence: evidence.length > 90 ? `\u2026${evidence.slice(-90)}` : evidence });
25318
+ }
25319
+ return out;
25320
+ }
25321
+ async function findDeferredGates(adapter2) {
25322
+ const empty = { unblock: [], zombie: [] };
25323
+ let allTasks = [];
25324
+ try {
25325
+ allTasks = await adapter2.queryBoard();
25326
+ } catch {
25327
+ return empty;
25328
+ }
25329
+ const deferred = allTasks.filter((t) => t.status === "Deferred");
25330
+ if (deferred.length === 0) return empty;
25331
+ const byId = /* @__PURE__ */ new Map();
25332
+ for (const t of allTasks) {
25333
+ if (t.displayId) byId.set(t.displayId.toLowerCase(), t);
25334
+ }
25335
+ const sweep = { unblock: [], zombie: [] };
25336
+ for (const task of deferred) {
25337
+ for (const { ref, evidence } of extractGatedTaskRefs(task.notes ?? "")) {
25338
+ const self = task.displayId?.toLowerCase();
25339
+ if (self && ref === self) continue;
25340
+ const upstream = byId.get(ref);
25341
+ if (!upstream) continue;
25342
+ if (upstream.status !== "Done" && upstream.status !== "Cancelled") continue;
25343
+ const gate = {
25344
+ taskId: task.displayId ?? task.id,
25345
+ taskTitle: task.title,
25346
+ upstreamId: upstream.displayId ?? upstream.id,
25347
+ upstreamTitle: upstream.title,
25348
+ upstreamStatus: upstream.status,
25349
+ evidence
25350
+ };
25351
+ const bucket = upstream.status === "Done" ? sweep.unblock : sweep.zombie;
25352
+ if (bucket.length < MAX_PER_BUCKET) bucket.push(gate);
25353
+ break;
25354
+ }
25355
+ }
25356
+ return sweep;
25357
+ }
25358
+ function formatDeferredGateSection(sweep) {
25359
+ if (sweep.unblock.length === 0 && sweep.zombie.length === 0) return "";
25360
+ const lines = ["## Deferred-Gate Sweep"];
25361
+ if (sweep.unblock.length > 0) {
25362
+ lines.push("");
25363
+ lines.push(`**Unblock candidates (${sweep.unblock.length})** \u2014 the upstream task is Done, so the gate has cleared. Promote with \`board_edit\` \u2192 Backlog.`);
25364
+ for (const g of sweep.unblock) {
25365
+ lines.push(`- **${g.taskId}** (${g.taskTitle}) \u2014 gated on **${g.upstreamId}** which is now Done \u2705`);
25366
+ }
25367
+ }
25368
+ if (sweep.zombie.length > 0) {
25369
+ lines.push("");
25370
+ lines.push(`**Zombie candidates (${sweep.zombie.length})** \u2014 the upstream task was CANCELLED, so this gate can never clear. Cancel or re-scope.`);
25371
+ for (const g of sweep.zombie) {
25372
+ lines.push(`- **${g.taskId}** (${g.taskTitle}) \u2014 gated on **${g.upstreamId}** which was Cancelled \u274C`);
25373
+ }
25374
+ }
25375
+ lines.push("");
25376
+ lines.push("_Advisory only \u2014 nothing was changed. Gates are read from freetext notes, so check the reference before acting._");
25377
+ return lines.join("\n");
25378
+ }
25379
+
25090
25380
  // src/tools/agent-list.ts
25091
25381
  import { readdir as readdir2, readFile as readFile7 } from "fs/promises";
25092
25382
  import { join as join14 } from "path";
@@ -25129,7 +25419,7 @@ async function listAgents(projectRoot) {
25129
25419
  var agentListTool = {
25130
25420
  name: "agent_list",
25131
25421
  description: "List the sub-agents discovered in the project's `.claude/agents/*.md` files (read-only). Returns each agent's name and description so you can see which specialised sub-agents are available before dispatching one via the Task/Agent tool. Discovery only \u2014 does not invoke or manage agents.",
25132
- annotations: { readOnlyHint: true, destructiveHint: false },
25422
+ annotations: { title: "List Agents", readOnlyHint: true, destructiveHint: false, openWorldHint: false },
25133
25423
  inputSchema: {
25134
25424
  type: "object",
25135
25425
  properties: {
@@ -25308,7 +25598,7 @@ async function runWithLimit(concurrency, tasks) {
25308
25598
  var orientTool = {
25309
25599
  name: "orient",
25310
25600
  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. PAPI detects build capability from the connecting harness (clientInfo); pass `environment` only to override that detection for git-dependent recommendations (build_execute, release, review_submit).",
25311
- annotations: { readOnlyHint: true, destructiveHint: false },
25601
+ annotations: { title: "Orient Session", readOnlyHint: true, destructiveHint: false, openWorldHint: false },
25312
25602
  inputSchema: {
25313
25603
  type: "object",
25314
25604
  properties: {
@@ -25332,6 +25622,7 @@ var orientTool = {
25332
25622
  var papiTool = {
25333
25623
  ...orientTool,
25334
25624
  name: "papi",
25625
+ annotations: { title: "PAPI Status", readOnlyHint: false, destructiveHint: false, openWorldHint: false },
25335
25626
  description: 'Say "papi" to check in with Papi \u2014 an alias for `orient`. Run this FIRST at session start: it returns your cycle number, task counts, in-progress/in-review work, strategy-review cadence, a velocity snapshot, and the recommended next action. Identical to `orient` (same inputs, same output); use whichever name you prefer. Read-only.'
25336
25627
  };
25337
25628
  function countStalledP1(warnings) {
@@ -26196,6 +26487,17 @@ ${versionDrift}` : "";
26196
26487
  ${section}`;
26197
26488
  } catch {
26198
26489
  }
26490
+ let deferredGateNote = "";
26491
+ if (deepHousekeeping) {
26492
+ try {
26493
+ const sweep = await tracked("deferred-gates", () => findDeferredGates(adapter2))();
26494
+ const section = formatDeferredGateSection(sweep);
26495
+ if (section) deferredGateNote = `
26496
+
26497
+ ${section}`;
26498
+ } catch {
26499
+ }
26500
+ }
26199
26501
  tracker.mark("format-summary");
26200
26502
  const subAgents = await listAgents(config2.projectRoot);
26201
26503
  const [teamSummaryLine, releaseHistoryLine] = await Promise.all([
@@ -26204,7 +26506,7 @@ ${section}`;
26204
26506
  ]);
26205
26507
  const teamSummary = [teamSummaryLine, releaseHistoryLine].filter(Boolean).join("\n") || void 0;
26206
26508
  const deepHint = deepHousekeeping ? "" : "\n\n*Tip: pass `full: true` for Research Signals + version-drift, or `deep_housekeeping: true` to also check orphaned branches, merged-but-In-Progress tasks, unrecorded commits, unregistered docs, and stale skill forks (implies `full`).*";
26207
- return textResponse(projectBannerNote + formatOrientSummary(healthResult, buildInfo, hierarchy, latestTag, config2.projectRoot, environment, subAgents, projectName, teamSummary, clientName) + unblockNote + feedbackResolvedNote + alertsNote + ttfvNote + reconciliationNote + mergedInProgressNote + unrecordedNote + unregisteredDocsNote + staleSkillsNote + researchSignalsNote + recsNote + pendingReviewNote + patternsNote + unactionedIssuesNote + skillProposalsNote + sessionGuidanceNote + versionNote + enrichmentNote + deliveryShapeNote + preBuildCheckNote + deepHint + enrichmentFilesSection);
26509
+ return textResponse(projectBannerNote + formatOrientSummary(healthResult, buildInfo, hierarchy, latestTag, config2.projectRoot, environment, subAgents, projectName, teamSummary, clientName) + unblockNote + deferredGateNote + feedbackResolvedNote + alertsNote + ttfvNote + reconciliationNote + mergedInProgressNote + unrecordedNote + unregisteredDocsNote + staleSkillsNote + researchSignalsNote + recsNote + pendingReviewNote + patternsNote + unactionedIssuesNote + skillProposalsNote + sessionGuidanceNote + versionNote + enrichmentNote + deliveryShapeNote + preBuildCheckNote + deepHint + enrichmentFilesSection);
26208
26510
  } catch (err) {
26209
26511
  const message = err instanceof Error ? err.message : String(err);
26210
26512
  const isKnownFriendly = /^(Orient failed|Project not found|No project|Setup required)/i.test(message);
@@ -26260,7 +26562,7 @@ function enrichClaudeMd(projectRoot, cycleNumber, adapterType, collector, client
26260
26562
  var hierarchyUpdateTool = {
26261
26563
  name: "hierarchy_update",
26262
26564
  description: "Update the status of a phase, stage, or horizon in the project hierarchy (AD-14). Accepts a level (phase, stage, or horizon), a name or ID, and a new status. For stages, optionally set exit_criteria \u2014 a checklist defining when the stage is considered done. Does not call the Anthropic API.",
26263
- annotations: { readOnlyHint: false, destructiveHint: false },
26565
+ annotations: { title: "Update Hierarchy", readOnlyHint: false, destructiveHint: false, openWorldHint: false },
26264
26566
  inputSchema: {
26265
26567
  type: "object",
26266
26568
  properties: {
@@ -26671,7 +26973,7 @@ async function applyZoomOut(adapter2, llmResponse, cycleNumber) {
26671
26973
  var zoomOutTool = {
26672
26974
  name: "zoom_out",
26673
26975
  description: 'Run a Zoom-Out Retrospective \u2014 a higher-level meta-retrospective that sits above strategy reviews. Analyses the full project arc: every cycle, decision, and pivot. Use when you want to step back and see the big picture after many cycles. First call returns a prompt (prepare phase). Then call again with mode "apply" and your output.',
26674
- annotations: { readOnlyHint: false, destructiveHint: false },
26976
+ annotations: { title: "Zoom Out", readOnlyHint: false, destructiveHint: false, openWorldHint: false },
26675
26977
  inputSchema: {
26676
26978
  type: "object",
26677
26979
  properties: {
@@ -26775,7 +27077,7 @@ ${result.userMessage}
26775
27077
  var getSiblingAdsTool = {
26776
27078
  name: "get_sibling_ads",
26777
27079
  description: "Read Active Decisions from sibling PAPI projects that share the same Supabase instance. Requires PAPI_SIBLING_PROJECT_IDS env var (comma-separated project UUIDs). Returns ADs labelled by source project \u2014 useful for cross-project architectural alignment. pg adapter only \u2014 returns an error if using md or proxy adapter.",
26778
- annotations: { readOnlyHint: true, destructiveHint: false },
27080
+ annotations: { title: "Sibling Decisions", readOnlyHint: true, destructiveHint: false, openWorldHint: false },
26779
27081
  inputSchema: {
26780
27082
  type: "object",
26781
27083
  properties: {
@@ -26852,7 +27154,7 @@ var handoffPrepareCache = new PerCallerCache();
26852
27154
  var handoffGenerateTool = {
26853
27155
  name: "handoff_generate",
26854
27156
  description: "Generate BUILD HANDOFFs for cycle tasks that don't have one yet. Run after `plan` (with skip_handoffs=true) or to regenerate stale handoffs. Uses the prepare/apply pattern \u2014 first call returns a prompt, second call persists results.",
26855
- annotations: { readOnlyHint: false, destructiveHint: false },
27157
+ annotations: { title: "Generate Handoff", readOnlyHint: false, destructiveHint: false, openWorldHint: false },
26856
27158
  inputSchema: {
26857
27159
  type: "object",
26858
27160
  properties: {
@@ -27108,7 +27410,7 @@ function buildSummary(task, taskCount) {
27108
27410
  var scopeBriefTool = {
27109
27411
  name: "scope_brief",
27110
27412
  description: "Decompose a brief-class task (Large/XL, too large to build directly) into a structured scope document. Runs an LLM pass to produce sub-tasks, writes docs/scopes/<task-id>.md, registers it in the doc registry, and marks the source task as decomposed. Use before planning a cycle that includes brief-class tasks.",
27111
- annotations: { readOnlyHint: false, destructiveHint: false },
27413
+ annotations: { title: "Scope Brief", readOnlyHint: false, destructiveHint: false, openWorldHint: false },
27112
27414
  inputSchema: {
27113
27415
  type: "object",
27114
27416
  properties: {
@@ -27157,7 +27459,7 @@ async function handleScopeBrief(adapter2, config2, args) {
27157
27459
  var adViewTool = {
27158
27460
  name: "ad_view",
27159
27461
  description: "View one or all Active Decisions with full bodies. Use when you need to read the complete reasoning and evidence behind a specific AD before running strategy_change.",
27160
- annotations: { readOnlyHint: true, destructiveHint: false },
27462
+ annotations: { title: "View Decisions", readOnlyHint: true, destructiveHint: false, openWorldHint: false },
27161
27463
  inputSchema: {
27162
27464
  type: "object",
27163
27465
  properties: {
@@ -27214,7 +27516,7 @@ ${formatted}`);
27214
27516
  var learningActionTool = {
27215
27517
  name: "learning_action",
27216
27518
  description: `Mark a cycle learning as actioned (linking it to a task or idea) or list unactioned learnings. Use "mark" to close out a learning after you've submitted an idea or created a task for it. Use "list" to see which learnings from recent cycles still need follow-up.`,
27217
- annotations: { readOnlyHint: false, destructiveHint: false },
27519
+ annotations: { title: "Action Learning", readOnlyHint: false, destructiveHint: false, openWorldHint: false },
27218
27520
  inputSchema: {
27219
27521
  type: "object",
27220
27522
  properties: {
@@ -27300,7 +27602,7 @@ Use \`learning_action mark\` or submit via \`idea\` to close these out.`
27300
27602
  var discoveredIssueResolveTool = {
27301
27603
  name: "discovered_issue_resolve",
27302
27604
  description: 'Mark a discovered_issue (cycle_learnings row, category="issue") as resolved. Pass the learning_id you saw in orient / learning_action list output. The row stays in the database for history, but default reads exclude it. Use this when the underlying fix has actually landed \u2014 NOT when you just created a follow-up task (that is `learning_action mark` with action_taken="task_created"). Optional `note` is recorded as resolved_by.',
27303
- annotations: { readOnlyHint: false, destructiveHint: false },
27605
+ annotations: { title: "Resolve Issue", readOnlyHint: false, destructiveHint: false, openWorldHint: false },
27304
27606
  inputSchema: {
27305
27607
  type: "object",
27306
27608
  properties: {
@@ -27399,7 +27701,7 @@ var NO_SUPPORT = "Project lifecycle tools require a hosted (proxy) or PostgreSQL
27399
27701
  var projectCreateTool = {
27400
27702
  name: "project_create",
27401
27703
  description: 'Create an EMPTY PAPI project for the current workspace (no plan, no seeded backlog) and return its id. Idempotent: re-running in the same folder (or with the same name) returns the EXISTING project instead of creating a duplicate. Use when "set up papi here" / "create a project" and none matches this folder. No Anthropic API.',
27402
- annotations: { readOnlyHint: false, destructiveHint: false },
27704
+ annotations: { title: "Create Project", readOnlyHint: false, destructiveHint: false, openWorldHint: false },
27403
27705
  inputSchema: {
27404
27706
  type: "object",
27405
27707
  properties: {
@@ -27445,7 +27747,7 @@ Run \`plan\` when you're ready to start a cycle.`
27445
27747
  var projectListTool = {
27446
27748
  name: "project_list",
27447
27749
  description: "List the PAPI projects on your account (id, name, slug, mapped folder). Use to see which project you are about to write to, or to find the id/slug to pass to project_switch or the per-call `project` arg. No Anthropic API.",
27448
- annotations: { readOnlyHint: true, destructiveHint: false },
27750
+ annotations: { title: "List Projects", readOnlyHint: true, destructiveHint: false, openWorldHint: false },
27449
27751
  inputSchema: {
27450
27752
  type: "object",
27451
27753
  properties: {
@@ -27476,7 +27778,7 @@ ${lines.join("\n")}`);
27476
27778
  var projectSwitchTool = {
27477
27779
  name: "project_switch",
27478
27780
  description: 'Select a project you own by id or slug and map the current folder to it (sets papi_dir on local stdio). Use when "switch to golf" / "point papi at <project>". Fails closed if the project is not on your account. No Anthropic API.',
27479
- annotations: { readOnlyHint: false, destructiveHint: false },
27781
+ annotations: { title: "Switch Project", readOnlyHint: false, destructiveHint: false, openWorldHint: false },
27480
27782
  inputSchema: {
27481
27783
  type: "object",
27482
27784
  properties: {
@@ -27511,7 +27813,7 @@ On hosted/remote sessions this is now your DEFAULT project \u2014 calls without
27511
27813
  var contributorAddTool = {
27512
27814
  name: "contributor_add",
27513
27815
  description: "Add a contributor to the current project by email (owner-only). The person must already have a PAPI account. Grants cohort membership on project_contributors \u2014 contributors-tier visibility, no roles yet.",
27514
- annotations: { readOnlyHint: false, destructiveHint: false },
27816
+ annotations: { title: "Add Contributor", readOnlyHint: false, destructiveHint: false, openWorldHint: false },
27515
27817
  inputSchema: {
27516
27818
  type: "object",
27517
27819
  properties: {
@@ -27523,7 +27825,7 @@ var contributorAddTool = {
27523
27825
  var contributorRemoveTool = {
27524
27826
  name: "contributor_remove",
27525
27827
  description: "Remove a contributor from the current project by email (owner-only). Deletes their project_contributors row \u2014 they lose contributors-tier visibility.",
27526
- annotations: { readOnlyHint: false, destructiveHint: true },
27828
+ annotations: { title: "Remove Contributor", readOnlyHint: false, destructiveHint: true, openWorldHint: false },
27527
27829
  inputSchema: {
27528
27830
  type: "object",
27529
27831
  properties: {
@@ -27535,7 +27837,7 @@ var contributorRemoveTool = {
27535
27837
  var contributorListTool = {
27536
27838
  name: "contributor_list",
27537
27839
  description: "List the current project's contributors (any project member). Shows each member's email, display name, role, and join date. Does not call the Anthropic API.",
27538
- annotations: { readOnlyHint: true, destructiveHint: false },
27840
+ annotations: { title: "List Contributors", readOnlyHint: true, destructiveHint: false, openWorldHint: false },
27539
27841
  inputSchema: {
27540
27842
  type: "object",
27541
27843
  properties: {
@@ -27641,7 +27943,7 @@ async function handleContributorList(adapter2, config2, _args) {
27641
27943
  var taskClaimTool = {
27642
27944
  name: "task_claim",
27643
27945
  description: "Claim a task from the shared org Pool into your personal backlog (assignee = you). Atomic first-claim-wins \u2014 a concurrent double-claim is impossible. Cascades the DEPENDS ON chain: claiming a task also claims its not-Done prerequisites; if any prerequisite is already claimed by another member the whole claim is refused (a build unit is never split across owners). Does not call the Anthropic API.",
27644
- annotations: { readOnlyHint: false, destructiveHint: false },
27946
+ annotations: { title: "Claim Task", readOnlyHint: false, destructiveHint: false, openWorldHint: false },
27645
27947
  inputSchema: {
27646
27948
  type: "object",
27647
27949
  properties: {
@@ -27653,7 +27955,7 @@ var taskClaimTool = {
27653
27955
  var taskUnclaimTool = {
27654
27956
  name: "task_unclaim",
27655
27957
  description: "Release a task you claimed back to the shared Pool (clears assignee). Claimer-only and pre-review \u2014 you cannot unclaim another member's task or one that has reached In Review/Done. Does not cascade. Does not call the Anthropic API.",
27656
- annotations: { readOnlyHint: false, destructiveHint: true },
27958
+ annotations: { title: "Unclaim Task", readOnlyHint: false, destructiveHint: true, openWorldHint: false },
27657
27959
  inputSchema: {
27658
27960
  type: "object",
27659
27961
  properties: {
@@ -27790,7 +28092,7 @@ async function handleTaskUnclaim(adapter2, config2, args) {
27790
28092
  var taskMoveTool = {
27791
28093
  name: "task_move",
27792
28094
  description: "Move a task from the current project to another project you own. Reassigns the task a fresh id in the target (collision-free) and carries its build reports, comments, and history with it; the cycle assignment is cleared so it lands in the target project's backlog. You must own (or have write access to) BOTH projects. Destructive-ish and cross-project, so it requires confirm=true \u2014 without it you get a preview only. Does not call the Anthropic API.",
27793
- annotations: { readOnlyHint: false, destructiveHint: true },
28095
+ annotations: { title: "Move Task", readOnlyHint: false, destructiveHint: true, openWorldHint: false },
27794
28096
  inputSchema: {
27795
28097
  type: "object",
27796
28098
  properties: {
@@ -27977,7 +28279,7 @@ async function syncHarnessInventory(adapter2, config2, opts) {
27977
28279
  var inventorySyncTool = {
27978
28280
  name: "inventory_sync",
27979
28281
  description: "Sync this project's harness inventory \u2014 skills, sub-agents, hooks, and MCP tools \u2014 to the database so the dashboard can surface it. Gated by a cheap change-fingerprint: a no-op when the harness hasn't changed since the last sync. Set force=true to re-scan and write regardless. Runs automatically at setup and release; use this for an explicit refresh after editing your harness.",
27980
- annotations: { readOnlyHint: false, destructiveHint: false },
28282
+ annotations: { title: "Sync Inventory", readOnlyHint: false, destructiveHint: false, openWorldHint: false },
27981
28283
  inputSchema: {
27982
28284
  type: "object",
27983
28285
  properties: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@papi-ai/server",
3
- "version": "0.7.51",
3
+ "version": "0.7.53",
4
4
  "description": "PAPI MCP server — AI-powered sprint planning, build execution, and strategy review for software projects",
5
5
  "license": "Elastic-2.0",
6
6
  "mcpName": "io.github.getpapi/papi",