@papi-ai/server 0.7.38 → 0.7.40

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.
@@ -43,6 +43,7 @@ __export(git_exports, {
43
43
  getStagedFiles: () => getStagedFiles,
44
44
  getTagTarget: () => getTagTarget,
45
45
  getTaskIdsOnBranch: () => getTaskIdsOnBranch,
46
+ getTrackedModifiedFiles: () => getTrackedModifiedFiles,
46
47
  getUnmergedBranches: () => getUnmergedBranches,
47
48
  getUntrackedFiles: () => getUntrackedFiles,
48
49
  gitPull: () => gitPull,
@@ -175,6 +176,25 @@ function getModifiedFiles(cwd) {
175
176
  return [];
176
177
  }
177
178
  }
179
+ function getTrackedModifiedFiles(cwd) {
180
+ try {
181
+ const out = execFileSync("git", ["status", "--porcelain"], {
182
+ cwd,
183
+ encoding: "utf-8"
184
+ }).replace(/\n+$/, "");
185
+ if (!out) return [];
186
+ const paths = /* @__PURE__ */ new Set();
187
+ for (const line of out.split("\n")) {
188
+ if (line.length < 4) continue;
189
+ if (line.startsWith("??")) continue;
190
+ const path3 = line.slice(3).trim();
191
+ if (path3) paths.add(path3);
192
+ }
193
+ return Array.from(paths);
194
+ } catch {
195
+ return [];
196
+ }
197
+ }
178
198
  function getUntrackedFiles(cwd) {
179
199
  try {
180
200
  const out = execFileSync("git", ["ls-files", "--others", "--exclude-standard"], {
@@ -1324,6 +1344,33 @@ Check PAPI_PROJECT_ID in your .mcp.json config. Find your project ID in the PAPI
1324
1344
  appendToolMetric(metric) {
1325
1345
  return this.invoke("appendToolMetric", [metric]);
1326
1346
  }
1347
+ /**
1348
+ * task-2288 (C308): emit a telemetry event via the proxy's /telemetry endpoint
1349
+ * using THIS adapter's per-request bearer (this.apiKey) — the same credential
1350
+ * invoke() uses. This is the fix for the hosted-transport blackout: on the
1351
+ * multi-tenant Railway pod there is no PAPI_DATA_API_KEY env var, so the
1352
+ * env-var emitTelemetryEvent path (lib/telemetry.ts) dropped every event since
1353
+ * 2026-06-16. Fire-and-forget — never throws, never blocks the tool response.
1354
+ * Not routed through invoke() because /telemetry is a distinct endpoint and a
1355
+ * telemetry failure must never surface as a tool error.
1356
+ */
1357
+ emitTelemetry(event) {
1358
+ fetch(`${this.endpoint}/telemetry`, {
1359
+ method: "POST",
1360
+ headers: {
1361
+ "Content-Type": "application/json",
1362
+ "Authorization": `Bearer ${this.apiKey}`
1363
+ },
1364
+ body: JSON.stringify({
1365
+ projectId: event.projectId,
1366
+ toolName: event.toolName,
1367
+ eventType: event.eventType,
1368
+ metadata: event.metadata ?? {}
1369
+ }),
1370
+ signal: AbortSignal.timeout(5e3)
1371
+ }).catch(() => {
1372
+ });
1373
+ }
1327
1374
  readToolMetrics() {
1328
1375
  return this.invoke("readToolMetrics");
1329
1376
  }
@@ -1475,6 +1522,15 @@ Check PAPI_PROJECT_ID in your .mcp.json config. Find your project ID in the PAPI
1475
1522
  submitBugReport(report) {
1476
1523
  return this.invoke("submitBugReport", [report]);
1477
1524
  }
1525
+ // task-2270: notify-back. The data-proxy ignores the client-supplied userId
1526
+ // and scopes to the bearer-validated caller (same pattern as the owner-action
1527
+ // counts), so a user can only ever read/mark their OWN resolved submissions.
1528
+ getUnnotifiedResolvedFeedback(userId) {
1529
+ return this.invoke("getUnnotifiedResolvedFeedback", [userId]);
1530
+ }
1531
+ markFeedbackNotified(ids, userId) {
1532
+ return this.invoke("markFeedbackNotified", [ids, userId]);
1533
+ }
1478
1534
  // --- Doc Registry ---
1479
1535
  registerDoc(entry) {
1480
1536
  return this.invoke("registerDoc", [entry]);
package/dist/index.js CHANGED
@@ -44,6 +44,7 @@ __export(git_exports, {
44
44
  getStagedFiles: () => getStagedFiles,
45
45
  getTagTarget: () => getTagTarget,
46
46
  getTaskIdsOnBranch: () => getTaskIdsOnBranch,
47
+ getTrackedModifiedFiles: () => getTrackedModifiedFiles,
47
48
  getUnmergedBranches: () => getUnmergedBranches,
48
49
  getUntrackedFiles: () => getUntrackedFiles,
49
50
  gitPull: () => gitPull,
@@ -176,6 +177,25 @@ function getModifiedFiles(cwd) {
176
177
  return [];
177
178
  }
178
179
  }
180
+ function getTrackedModifiedFiles(cwd) {
181
+ try {
182
+ const out = execFileSync("git", ["status", "--porcelain"], {
183
+ cwd,
184
+ encoding: "utf-8"
185
+ }).replace(/\n+$/, "");
186
+ if (!out) return [];
187
+ const paths = /* @__PURE__ */ new Set();
188
+ for (const line of out.split("\n")) {
189
+ if (line.length < 4) continue;
190
+ if (line.startsWith("??")) continue;
191
+ const path7 = line.slice(3).trim();
192
+ if (path7) paths.add(path7);
193
+ }
194
+ return Array.from(paths);
195
+ } catch {
196
+ return [];
197
+ }
198
+ }
179
199
  function getUntrackedFiles(cwd) {
180
200
  try {
181
201
  const out = execFileSync("git", ["ls-files", "--others", "--exclude-standard"], {
@@ -1325,6 +1345,33 @@ Check PAPI_PROJECT_ID in your .mcp.json config. Find your project ID in the PAPI
1325
1345
  appendToolMetric(metric) {
1326
1346
  return this.invoke("appendToolMetric", [metric]);
1327
1347
  }
1348
+ /**
1349
+ * task-2288 (C308): emit a telemetry event via the proxy's /telemetry endpoint
1350
+ * using THIS adapter's per-request bearer (this.apiKey) — the same credential
1351
+ * invoke() uses. This is the fix for the hosted-transport blackout: on the
1352
+ * multi-tenant Railway pod there is no PAPI_DATA_API_KEY env var, so the
1353
+ * env-var emitTelemetryEvent path (lib/telemetry.ts) dropped every event since
1354
+ * 2026-06-16. Fire-and-forget — never throws, never blocks the tool response.
1355
+ * Not routed through invoke() because /telemetry is a distinct endpoint and a
1356
+ * telemetry failure must never surface as a tool error.
1357
+ */
1358
+ emitTelemetry(event) {
1359
+ fetch(`${this.endpoint}/telemetry`, {
1360
+ method: "POST",
1361
+ headers: {
1362
+ "Content-Type": "application/json",
1363
+ "Authorization": `Bearer ${this.apiKey}`
1364
+ },
1365
+ body: JSON.stringify({
1366
+ projectId: event.projectId,
1367
+ toolName: event.toolName,
1368
+ eventType: event.eventType,
1369
+ metadata: event.metadata ?? {}
1370
+ }),
1371
+ signal: AbortSignal.timeout(5e3)
1372
+ }).catch(() => {
1373
+ });
1374
+ }
1328
1375
  readToolMetrics() {
1329
1376
  return this.invoke("readToolMetrics");
1330
1377
  }
@@ -1476,6 +1523,15 @@ Check PAPI_PROJECT_ID in your .mcp.json config. Find your project ID in the PAPI
1476
1523
  submitBugReport(report) {
1477
1524
  return this.invoke("submitBugReport", [report]);
1478
1525
  }
1526
+ // task-2270: notify-back. The data-proxy ignores the client-supplied userId
1527
+ // and scopes to the bearer-validated caller (same pattern as the owner-action
1528
+ // counts), so a user can only ever read/mark their OWN resolved submissions.
1529
+ getUnnotifiedResolvedFeedback(userId) {
1530
+ return this.invoke("getUnnotifiedResolvedFeedback", [userId]);
1531
+ }
1532
+ markFeedbackNotified(ids, userId) {
1533
+ return this.invoke("markFeedbackNotified", [ids, userId]);
1534
+ }
1479
1535
  // --- Doc Registry ---
1480
1536
  registerDoc(entry) {
1481
1537
  return this.invoke("registerDoc", [entry]);
@@ -12583,6 +12639,7 @@ function formatPlanResult(result) {
12583
12639
  );
12584
12640
  }
12585
12641
  const lines = [];
12642
+ if (result.projectBanner) lines.push(`> ${result.projectBanner}`, "");
12586
12643
  lines.push(`${result.strategyReviewWarning}${pullLine}**${modeLabel} Mode \u2014 ${cycleLabel}**`);
12587
12644
  if (result.writeSummary) {
12588
12645
  const ws = result.writeSummary;
@@ -12692,7 +12749,9 @@ async function handlePlan(adapter2, config2, args) {
12692
12749
  source: "mcp-server",
12693
12750
  confirmCancellations: args.confirm_cancellations === true
12694
12751
  }, tracker);
12695
- const response = formatPlanResult({ ...result, contextUtilisation: utilisation, contextBytes, skipHandoffs });
12752
+ const planProjectInfo = adapter2.getProjectInfo ? await adapter2.getProjectInfo().catch(() => null) : null;
12753
+ const projectBanner = planProjectInfo ? getProjectConnectionBanner(planProjectInfo.name, planProjectInfo.slug) ?? void 0 : void 0;
12754
+ const response = formatPlanResult({ ...result, contextUtilisation: utilisation, contextBytes, skipHandoffs, projectBanner });
12696
12755
  return {
12697
12756
  ...response,
12698
12757
  ...contextBytes !== void 0 ? { _contextBytes: contextBytes } : {},
@@ -16335,6 +16394,9 @@ function substitute(template, vars) {
16335
16394
  }
16336
16395
  return result;
16337
16396
  }
16397
+ function shouldWriteClaudeMd(clientName) {
16398
+ return !clientName || /claude/i.test(clientName);
16399
+ }
16338
16400
  async function scaffoldPapiDir(adapter2, config2, input, collector) {
16339
16401
  const isPg = config2.adapterType === "pg" || config2.adapterType === "proxy";
16340
16402
  const vars = {
@@ -16400,9 +16462,9 @@ async function scaffoldPapiDir(adapter2, config2, input, collector) {
16400
16462
  if (!docsIndexExists) {
16401
16463
  scaffoldFiles[docsIndexPath] = substitute(DOCS_INDEX_TEMPLATE, vars);
16402
16464
  }
16403
- if (!claudeMdExists) {
16465
+ if (!claudeMdExists && shouldWriteClaudeMd(input.clientName)) {
16404
16466
  scaffoldFiles[claudeMdPath] = substitute(CLAUDE_MD_STUB, vars);
16405
- } else {
16467
+ } else if (claudeMdExists) {
16406
16468
  try {
16407
16469
  const existing = await readFile4(claudeMdPath, "utf-8");
16408
16470
  if (!existing.includes("AGENTS.md")) {
@@ -16581,7 +16643,7 @@ async function applySetupOutputs(adapter2, config2, input, collector, briefText,
16581
16643
  }
16582
16644
  }
16583
16645
  }
16584
- if (conventionsText?.trim()) {
16646
+ if (conventionsText?.trim() && shouldWriteClaudeMd(input.clientName)) {
16585
16647
  const conventionsBlock = `${CONVENTIONS_SENTINEL}
16586
16648
  ${conventionsText.trim()}
16587
16649
  `;
@@ -17026,7 +17088,7 @@ async function applySetup(adapter2, config2, input, briefText, adSeedText, conve
17026
17088
  }
17027
17089
  }
17028
17090
  }
17029
- {
17091
+ if (shouldWriteClaudeMd(input.clientName)) {
17030
17092
  const dogfoodSection = [
17031
17093
  "",
17032
17094
  "## Dogfood Logging",
@@ -17260,7 +17322,8 @@ function extractInput(args) {
17260
17322
  codebaseScan: args.codebase_scan && typeof args.codebase_scan === "object" ? args.codebase_scan : void 0
17261
17323
  };
17262
17324
  }
17263
- function formatSuccessResponse(result, constraints) {
17325
+ function formatSuccessResponse(result, constraints, writesClaudeMd = true) {
17326
+ const harnessFiles = writesClaudeMd ? "AGENTS.md, CLAUDE.md" : "AGENTS.md";
17264
17327
  const prefix = result.createdProject ? `PAPI project "${result.projectName}" initialised and ` : "";
17265
17328
  const briefRegenNote = result.briefRegenerated ? `
17266
17329
 
@@ -17277,7 +17340,7 @@ ${result.seededAds} Active Decision${result.seededAds > 1 ? "s" : ""} seeded bas
17277
17340
  ${[created, skipped].filter(Boolean).join(", ")}.${idea}`;
17278
17341
  })() : '\n\nNo starter tasks yet. Describe what you want to build, or seed the board with `idea "<what you want to build>"`, so your first `plan` has something to work from.';
17279
17342
  const constraintsHint = !constraints ? '\n\nTip: consider adding `constraints` (e.g. "must use PostgreSQL", "HIPAA compliant", "offline-first") to improve Active Decision seeding.' : "";
17280
- const editorNote = result.cursorScaffolded ? "\n\nCursor detected \u2014 `.cursor/rules/papi.mdc` scaffolded alongside CLAUDE.md." : "";
17343
+ const editorNote = result.cursorScaffolded ? "\n\nCursor detected \u2014 `.cursor/rules/papi.mdc` scaffolded alongside your `AGENTS.md` harness." : "";
17281
17344
  const gitignoreNote = result.gitignoreNote ? `
17282
17345
 
17283
17346
  \u{1F512} ${result.gitignoreNote}` : "";
@@ -17289,7 +17352,7 @@ ${result.warnings.map((w) => `- ${w}`).join("\n")}` : "";
17289
17352
  return textResponse(
17290
17353
  `${prefix}Product Brief generated and saved.${briefRegenNote}${adNote}${taskNote}${constraintsHint}${editorNote}${gitignoreNote}${warningsNote}
17291
17354
 
17292
- **Important:** Setup created/modified files (CLAUDE.md, .claude/settings.json, docs/). Commit these changes before running \`build_execute\` \u2014 it requires a clean working directory.
17355
+ **Important:** Setup created/modified files (${harnessFiles}, .claude/settings.json, docs/). Commit these changes before running \`build_execute\` \u2014 it requires a clean working directory.
17293
17356
 
17294
17357
  Tip: See \`docs/templates/example-project-brief.md\` for an example of a well-written brief.
17295
17358
 
@@ -17298,7 +17361,7 @@ Tip: See \`docs/templates/example-project-brief.md\` for an example of a well-wr
17298
17361
  Next step: run \`plan\` to start your first planning cycle.${filesToWriteSection}`
17299
17362
  );
17300
17363
  }
17301
- async function handleSetup(adapter2, config2, args) {
17364
+ async function handleSetup(adapter2, config2, args, clientName) {
17302
17365
  const toolMode = args.mode;
17303
17366
  const REQUIRED_FIELDS = ["project_name"];
17304
17367
  const missing = REQUIRED_FIELDS.filter((f) => !args[f] || typeof args[f] === "string" && !args[f].trim());
@@ -17310,6 +17373,8 @@ PAPI needs the project name. Description and target users are optional \u2014 th
17310
17373
  );
17311
17374
  }
17312
17375
  const input = extractInput(args);
17376
+ input.clientName = clientName;
17377
+ const writesClaudeMd = !clientName || /claude/i.test(clientName);
17313
17378
  const tracker = new ProgressTracker(toolMode === "apply" ? "apply_validate" : "prepare_validate");
17314
17379
  try {
17315
17380
  if (toolMode === "apply") {
@@ -17321,7 +17386,7 @@ PAPI needs the project name. Description and target users are optional \u2014 th
17321
17386
  tracker.mark("apply_setup_writeback");
17322
17387
  const result = await applySetup(adapter2, config2, input, briefResponse, adSeedResponse, conventionsResponse, initialTasksResponse);
17323
17388
  tracker.mark("apply_format_response");
17324
- return formatSuccessResponse(result, args.constraints);
17389
+ return formatSuccessResponse(result, args.constraints, writesClaudeMd);
17325
17390
  }
17326
17391
  {
17327
17392
  tracker.mark("prepare_setup");
@@ -17922,6 +17987,64 @@ To override, pass force=true (emits a telemetry warning).`
17922
17987
  }
17923
17988
  return { resolvedCycleNum, warnings, force, skipVersion };
17924
17989
  }
17990
+ async function contributorAutoPrRelease(config2, adapter2, version, productionBaseBranch, options) {
17991
+ if (!isGhAvailable()) {
17992
+ throw new Error(
17993
+ `Release blocked \u2014 opening a release PR needs the GitHub CLI (\`gh\`) authenticated locally. Install and \`gh auth login\`, then re-run \`release\`. (As a contributor you cannot release to ${productionBaseBranch} directly; the owner merges your PR.)`
17994
+ );
17995
+ }
17996
+ const cycleNum = await resolveCycleToClose(adapter2, version, options?.callerUserId);
17997
+ const branches = listGroupedCycleBranches(config2.projectRoot, cycleNum, productionBaseBranch);
17998
+ if (branches.length === 0) {
17999
+ throw new Error(
18000
+ `Release blocked \u2014 no cycle branch found to open a PR from (cycle ${cycleNum || "?"}). Make sure your work is committed on a \`feat/cycle-\u2026\` branch and pushed, then re-run \`release\`.`
18001
+ );
18002
+ }
18003
+ const prs = [];
18004
+ for (const branch of branches) {
18005
+ const push = gitPush(config2.projectRoot, branch);
18006
+ if (!push.success) {
18007
+ prs.push({ branch, url: null, error: `push failed: ${push.message}` });
18008
+ continue;
18009
+ }
18010
+ const existing = getPullRequestUrl(config2.projectRoot, branch);
18011
+ if (existing) {
18012
+ prs.push({ branch, url: existing });
18013
+ continue;
18014
+ }
18015
+ const title = `Release ${version}: ${branch}`;
18016
+ const body = `Contributor release PR for cycle ${cycleNum || "?"} (\`${branch}\`).
18017
+
18018
+ Opened by a non-owner editor via \`release\` (task-2244). The project owner reviews and merges this into \`${productionBaseBranch}\`; the contributor's own cycle is already marked complete in PAPI.`;
18019
+ const created = createPullRequest(config2.projectRoot, branch, productionBaseBranch, title, body);
18020
+ prs.push(
18021
+ created.success ? { branch, url: created.message.trim() || getPullRequestUrl(config2.projectRoot, branch) } : { branch, url: null, error: created.message }
18022
+ );
18023
+ }
18024
+ if (!prs.some((p) => p.url)) {
18025
+ const detail = prs.map((p) => `${p.branch}: ${p.error ?? "no PR url returned"}`).join("; ");
18026
+ throw new Error(`Release blocked \u2014 could not open any release PR. ${detail}`);
18027
+ }
18028
+ if (adapter2?.recordContributorReleasePr) {
18029
+ for (const p of prs) {
18030
+ if (!p.url) continue;
18031
+ try {
18032
+ await adapter2.recordContributorReleasePr({
18033
+ prUrl: p.url,
18034
+ branch: p.branch,
18035
+ cycle: cycleNum > 0 ? cycleNum : null,
18036
+ contributorUserId: options?.callerUserId ?? null
18037
+ });
18038
+ } catch (err) {
18039
+ console.error(
18040
+ `[release] recordContributorReleasePr failed for ${p.url} (non-blocking): ${err instanceof Error ? err.message : String(err)}`
18041
+ );
18042
+ }
18043
+ }
18044
+ }
18045
+ const closed = await closeCycleState(config2, adapter2, version, void 0, options);
18046
+ return { prs, cycleClosed: closed.resolvedCycleNum, warnings: closed.warnings };
18047
+ }
17925
18048
  async function createRelease(config2, branch, version, adapter2, cycleNum, options) {
17926
18049
  const collector = new FileWriteCollector();
17927
18050
  if (!isGitAvailable()) {
@@ -18296,11 +18419,50 @@ async function handleRelease(adapter2, config2, args) {
18296
18419
  const resolutionNote = gate.resolutionError ? `
18297
18420
 
18298
18421
  Identity resolution failed (${gate.resolutionError}) \u2014 the gate fails closed. Retry once connectivity is restored.` : "";
18299
- return errorResponse(
18300
- `Release to ${productionBaseBranch} is restricted to the project owner.
18422
+ tracker.mark("contributor-role-gate");
18423
+ const callerRole = adapter2.getContributorRole && gate.callerUserId ? await adapter2.getContributorRole(gate.callerUserId).catch(() => null) : null;
18424
+ if (callerRole !== "editor") {
18425
+ const roleNote = callerRole === "viewer" ? `Your role on this project is "viewer", which cannot release. Ask the owner (or an editor) to release, or ask for editor access.` : `Your identity does not match this project's owner, and you do not have an editor role to open a release PR. If you are a contributor, ask the owner for editor access (or push your branch and open a PR manually). If you ARE the owner on a local (pg) setup, set PAPI_USER_ID to your account UUID in .mcp.json; on the hosted/proxy setup your identity comes from your API key \u2014 check you are using YOUR key for YOUR project.`;
18426
+ return errorResponse(
18427
+ `Release to ${productionBaseBranch} is restricted to the project owner.
18301
18428
 
18302
- Your identity does not match this project's owner. If you are a contributor, push your branch and open a PR for the owner to release. If you ARE the owner on a local (pg) setup, set PAPI_USER_ID to your account UUID in .mcp.json; on the hosted/proxy setup your identity comes from your API key \u2014 check you are using YOUR key for YOUR project. Releasing to a non-base branch (e.g. dev) is unaffected.` + resolutionNote
18303
- );
18429
+ ` + roleNote + `
18430
+
18431
+ Releasing to a non-base branch (e.g. dev) is unaffected.` + resolutionNote
18432
+ );
18433
+ }
18434
+ tracker.mark("contributor-auto-pr");
18435
+ try {
18436
+ const cr = await contributorAutoPrRelease(config2, adapter2, version, productionBaseBranch, {
18437
+ force: force ?? false,
18438
+ callerUserId: gate.callerUserId
18439
+ });
18440
+ const opened = cr.prs.filter((p) => p.url);
18441
+ const failed = cr.prs.filter((p) => !p.url);
18442
+ const cyclePart = cr.cycleClosed > 0 ? `Cycle ${cr.cycleClosed}` : "Your cycle";
18443
+ const prLines = opened.map((p) => `- \`${p.branch}\` \u2192 ${p.url}`);
18444
+ const failLines = failed.map((p) => `- \`${p.branch}\` \u2014 \u26A0\uFE0F ${p.error ?? "no PR opened"}`);
18445
+ const warnBlock = cr.warnings.length > 0 ? `
18446
+ \u26A0\uFE0F Warnings: ${cr.warnings.join("; ")}
18447
+ ` : "";
18448
+ return textResponse(
18449
+ `## Release ${version} \u2014 PR opened for owner review
18450
+
18451
+ You released as an **editor**, so PAPI opened a pull request to \`${productionBaseBranch}\` instead of merging directly. The owner reviews and merges it.
18452
+
18453
+ **Pull request(s):**
18454
+ ${prLines.join("\n")}
18455
+ ` + (failLines.length > 0 ? `
18456
+ **Could not open:**
18457
+ ${failLines.join("\n")}
18458
+ ` : "") + warnBlock + `
18459
+ ${cyclePart} is now marked **complete** in PAPI \u2014 your work is shipped from your side. Any changes the owner requests come forward as a fast-follow in your next cycle; your closed cycle is not rewound.
18460
+
18461
+ Next: run \`plan\` to start your next cycle.`
18462
+ );
18463
+ } catch (err) {
18464
+ return errorResponse(err instanceof Error ? err.message : String(err));
18465
+ }
18304
18466
  }
18305
18467
  }
18306
18468
  if (isHostedTransport()) {
@@ -18998,6 +19160,18 @@ async function startBuild(adapter2, config2, taskId, options = {}) {
18998
19160
  branchLines.push(`Reusing shared cycle branch for ${task.complexity} ${task.module} task.`);
18999
19161
  }
19000
19162
  } else {
19163
+ const baseBranch = resolveBaseBranch(config2.projectRoot, config2.baseBranch);
19164
+ if (baseBranch !== config2.baseBranch) {
19165
+ branchLines.push(`Base branch '${config2.baseBranch}' not found \u2014 using '${baseBranch}'.`);
19166
+ }
19167
+ const trackedDirty = getTrackedModifiedFiles(config2.projectRoot);
19168
+ if (trackedDirty.length > 0 && currentBranch !== baseBranch) {
19169
+ const shown = trackedDirty.slice(0, 5).join(", ");
19170
+ const more = trackedDirty.length > 5 ? ` (+${trackedDirty.length - 5} more)` : "";
19171
+ throw new Error(
19172
+ `build_execute won't switch branches: the working tree has uncommitted changes on '${currentBranch}' (this task targets '${featureBranch}'). Another session may be working there, and switching would revert those files. Commit or stash them first, or run build_execute from '${baseBranch}'. Dirty: ${shown}${more}.`
19173
+ );
19174
+ }
19001
19175
  if (hasUncommittedChanges(config2.projectRoot, AUTO_WRITTEN_PATHS)) {
19002
19176
  const { toStash, preservedDocs } = selectAutostashPaths(
19003
19177
  getModifiedFiles(config2.projectRoot),
@@ -19026,10 +19200,6 @@ async function startBuild(adapter2, config2, taskId, options = {}) {
19026
19200
  }
19027
19201
  }
19028
19202
  }
19029
- const baseBranch = resolveBaseBranch(config2.projectRoot, config2.baseBranch);
19030
- if (baseBranch !== config2.baseBranch) {
19031
- branchLines.push(`Base branch '${config2.baseBranch}' not found \u2014 using '${baseBranch}'.`);
19032
- }
19033
19203
  const featureBranchExistsLocally = branchExists(config2.projectRoot, featureBranch);
19034
19204
  const featureBranchOnOrigin = !!originCycleBranch && originCycleBranch === featureBranch;
19035
19205
  const featureBranchExists = featureBranchExistsLocally || featureBranchOnOrigin;
@@ -19991,7 +20161,12 @@ async function handleBuildExecute(adapter2, config2, args) {
19991
20161
  tracker.mark("start_decorate_handoff");
19992
20162
  const branchInfo = result.branchLines.length > 0 ? result.branchLines.map((l) => `> ${l}`).join("\n") + "\n\n" : "";
19993
20163
  const phaseNote = result.phaseChanges.length > 0 ? "\n\n" + result.phaseChanges.map((c) => `Phase auto-updated: ${c.phaseId} ${c.oldStatus} \u2192 ${c.newStatus}`).join("\n") : "";
19994
- const header = `${branchInfo}**Executing ${result.task.id}: ${result.task.title}** \u2014 marked In Progress.
20164
+ const projectInfo = adapter2.getProjectInfo ? await adapter2.getProjectInfo().catch(() => null) : null;
20165
+ const projectBanner = projectInfo ? getProjectConnectionBanner(projectInfo.name, projectInfo.slug) : null;
20166
+ const projectLine = projectBanner ? `> ${projectBanner}
20167
+
20168
+ ` : "";
20169
+ const header = `${projectLine}${branchInfo}**Executing ${result.task.id}: ${result.task.title}** \u2014 marked In Progress.
19995
20170
 
19996
20171
  ---
19997
20172
 
@@ -23919,7 +24094,7 @@ function formatOrientSummary(health, buildInfo, hierarchy, latestTag, projectRoo
23919
24094
  const isGitDependentRec = /\*\*(Build|Review)\*\*/.test(health.recommendedMode);
23920
24095
  if (GIT_DEPENDENT_ENVS.has(environment) && isGitDependentRec) {
23921
24096
  lines.push(`> **Next action:** ${health.recommendedMode}`);
23922
- 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._`);
24097
+ lines.push(`> _Note: \`build_execute\`, \`review_submit\`, and \`release\` require a local session in your AI coding tool 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 your local tool._`);
23923
24098
  } else {
23924
24099
  lines.push(`> **Next action:** ${health.recommendedMode}`);
23925
24100
  }
@@ -24205,6 +24380,22 @@ async function computeReleaseHistory(adapter2) {
24205
24380
  if (released.length === 0) return void 0;
24206
24381
  return `**Recent releases:** ${released.join(" \xB7 ")}`;
24207
24382
  }
24383
+ function formatResolvedFeedback(items) {
24384
+ if (items.length === 0) return "";
24385
+ const MAX = 5;
24386
+ const shown = items.slice(0, MAX);
24387
+ const lines = ["\n\n## \u2705 Your feedback shipped"];
24388
+ for (const it of shown) {
24389
+ const kind = it.type === "idea" ? "Idea" : "Bug";
24390
+ const verb = it.status === "wont_fix" ? "closed" : "resolved";
24391
+ const desc = it.description.length > 90 ? `${it.description.slice(0, 87)}\u2026` : it.description;
24392
+ const note = it.resolutionNote?.trim() ? ` \u2014 ${it.resolutionNote.trim()}` : "";
24393
+ lines.push(`- **${kind}** "${desc}" was ${verb}${note}`);
24394
+ }
24395
+ if (items.length > MAX) lines.push(`- \u2026and ${items.length - MAX} more`);
24396
+ lines.push("_Thanks for the signal \u2014 it shaped what shipped._");
24397
+ return lines.join("\n");
24398
+ }
24208
24399
  async function handleOrient(adapter2, config2, args = {}) {
24209
24400
  const environment = normaliseEnvironment(args.environment);
24210
24401
  const deepHousekeeping = args.deep_housekeeping === true;
@@ -24249,6 +24440,7 @@ async function handleOrient(adapter2, config2, args = {}) {
24249
24440
  backlogHandoffCount: allTasks.filter((t) => t.cycle !== currentCycle).length
24250
24441
  };
24251
24442
  const sharedActiveDecisionsPromise = adapter2.getActiveDecisions();
24443
+ const resolvedFeedbackPromise = adapter2.getUnnotifiedResolvedFeedback ? adapter2.getUnnotifiedResolvedFeedback(config2.userId).catch(() => []) : Promise.resolve([]);
24252
24444
  const [
24253
24445
  proxyVersionOutcome,
24254
24446
  p1BacklogOutcome,
@@ -24627,6 +24819,12 @@ ${versionDrift}` : "";
24627
24819
  const projectName = projectBannerResult.name;
24628
24820
  const sessionGuidanceNote = sessionGuidanceOutcome.status === "fulfilled" ? sessionGuidanceOutcome.value : "";
24629
24821
  const deliveryShapeNote = deliveryShapeOutcome.status === "fulfilled" ? deliveryShapeOutcome.value : "";
24822
+ const resolvedFeedback = await resolvedFeedbackPromise;
24823
+ const feedbackResolvedNote = formatResolvedFeedback(resolvedFeedback);
24824
+ if (resolvedFeedback.length > 0 && adapter2.markFeedbackNotified) {
24825
+ void adapter2.markFeedbackNotified(resolvedFeedback.map((f) => f.id), config2.userId).catch(() => {
24826
+ });
24827
+ }
24630
24828
  const { reconciliationNote, unrecordedNote, unregisteredDocsNote, staleSkillsNote } = deepHousekeepingOutcome.status === "fulfilled" ? deepHousekeepingOutcome.value : { reconciliationNote: "", unrecordedNote: "", unregisteredDocsNote: "", staleSkillsNote: "" };
24631
24829
  let enrichmentNote = "";
24632
24830
  const enrichmentCollector = new FileWriteCollector();
@@ -24675,7 +24873,7 @@ ${section}`;
24675
24873
  ]);
24676
24874
  const teamSummary = [teamSummaryLine, releaseHistoryLine].filter(Boolean).join("\n") || void 0;
24677
24875
  const deepHint = deepHousekeeping ? "" : "\n\n*Tip: pass `full: true` for Research Signals + version-drift, or `deep_housekeeping: true` to also check orphaned branches, unrecorded commits, unregistered docs, and stale skill forks (implies `full`).*";
24678
- return textResponse(projectBannerNote + formatOrientSummary(healthResult, buildInfo, hierarchy, latestTag, config2.projectRoot, environment, subAgents, projectName, teamSummary) + unblockNote + alertsNote + ttfvNote + reconciliationNote + unrecordedNote + unregisteredDocsNote + staleSkillsNote + researchSignalsNote + recsNote + pendingReviewNote + patternsNote + unactionedIssuesNote + skillProposalsNote + sessionGuidanceNote + versionNote + enrichmentNote + deliveryShapeNote + preBuildCheckNote + deepHint + enrichmentFilesSection);
24876
+ return textResponse(projectBannerNote + formatOrientSummary(healthResult, buildInfo, hierarchy, latestTag, config2.projectRoot, environment, subAgents, projectName, teamSummary) + unblockNote + feedbackResolvedNote + alertsNote + ttfvNote + reconciliationNote + unrecordedNote + unregisteredDocsNote + staleSkillsNote + researchSignalsNote + recsNote + pendingReviewNote + patternsNote + unactionedIssuesNote + skillProposalsNote + sessionGuidanceNote + versionNote + enrichmentNote + deliveryShapeNote + preBuildCheckNote + deepHint + enrichmentFilesSection);
24679
24877
  } catch (err) {
24680
24878
  const message = err instanceof Error ? err.message : String(err);
24681
24879
  const isKnownFriendly = /^(Orient failed|Project not found|No project|Setup required)/i.test(message);
@@ -26717,7 +26915,7 @@ function createServer(adapter2, config2) {
26717
26915
  case "board_edit":
26718
26916
  return handleBoardEdit(adapter2, safeArgs);
26719
26917
  case "setup":
26720
- return handleSetup(adapter2, config2, safeArgs);
26918
+ return handleSetup(adapter2, config2, safeArgs, server2.getClientVersion()?.name);
26721
26919
  case "build_list":
26722
26920
  return handleBuildList(adapter2, config2);
26723
26921
  case "build_describe":
@@ -26857,17 +27055,28 @@ ${usageLine(decision.usage)}`;
26857
27055
  }
26858
27056
  const telemetryProjectId = resolveTelemetryProjectId(config2);
26859
27057
  if (telemetryProjectId) {
26860
- emitToolCall(telemetryProjectId, name, elapsed, {
26861
- adapter_type: config2.adapterType
26862
- });
27058
+ const adapterEmit = adapter2.emitTelemetry?.bind(adapter2) ?? null;
27059
+ if (adapterEmit) {
27060
+ adapterEmit({
27061
+ projectId: telemetryProjectId,
27062
+ toolName: name,
27063
+ eventType: "tool_call",
27064
+ metadata: { duration_ms: elapsed, adapter_type: config2.adapterType }
27065
+ });
27066
+ } else {
27067
+ emitToolCall(telemetryProjectId, name, elapsed, {
27068
+ adapter_type: config2.adapterType
27069
+ });
27070
+ }
26863
27071
  const isApplyMode = safeArgs.mode === "apply";
26864
27072
  if (!isError) {
26865
- if (name === "setup" && isApplyMode) {
26866
- emitMilestone(telemetryProjectId, "setup_completed");
26867
- } else if (name === "plan" && isApplyMode) {
26868
- emitMilestone(telemetryProjectId, "plan_completed");
26869
- } else if (name === "release") {
26870
- emitMilestone(telemetryProjectId, "release_completed");
27073
+ const milestone = name === "setup" && isApplyMode ? "setup_completed" : name === "plan" && isApplyMode ? "plan_completed" : name === "release" ? "release_completed" : null;
27074
+ if (milestone) {
27075
+ if (adapterEmit) {
27076
+ adapterEmit({ projectId: telemetryProjectId, toolName: milestone, eventType: "milestone" });
27077
+ } else {
27078
+ emitMilestone(telemetryProjectId, milestone);
27079
+ }
26871
27080
  }
26872
27081
  }
26873
27082
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@papi-ai/server",
3
- "version": "0.7.38",
3
+ "version": "0.7.40",
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.cathalos92/papi",