@papi-ai/server 0.7.81 → 0.7.82

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.
@@ -63,6 +63,7 @@ __export(git_exports, {
63
63
  isGhAvailable: () => isGhAvailable,
64
64
  isGitAvailable: () => isGitAvailable,
65
65
  isGitRepo: () => isGitRepo,
66
+ isPathCommittedAnywhere: () => isPathCommittedAnywhere,
66
67
  isPathIgnored: () => isPathIgnored,
67
68
  isPathTracked: () => isPathTracked,
68
69
  listGroupedCycleBranches: () => listGroupedCycleBranches,
@@ -102,6 +103,22 @@ function isGitRepo(cwd) {
102
103
  return false;
103
104
  }
104
105
  }
106
+ function isPathCommittedAnywhere(cwd, path3) {
107
+ const run = (args) => {
108
+ try {
109
+ return execFileSync("git", args, { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
110
+ } catch {
111
+ return null;
112
+ }
113
+ };
114
+ const commit = run(["log", "--all", "--format=%H", "-1", "--", path3]);
115
+ if (!commit) return false;
116
+ const committedBlob = run(["rev-parse", `${commit}:${path3}`]);
117
+ if (!committedBlob) return false;
118
+ const onDiskBlob = run(["hash-object", "--", path3]);
119
+ if (!onDiskBlob) return false;
120
+ return committedBlob === onDiskBlob;
121
+ }
105
122
  function isPathTracked(cwd, path3) {
106
123
  try {
107
124
  execFileSync("git", ["ls-files", "--error-unmatch", "--", path3], {
@@ -1285,7 +1302,7 @@ function wrapWithForwarding(instance) {
1285
1302
  function createProxyAdapter(config) {
1286
1303
  return wrapWithForwarding(new ProxyPapiAdapter(config));
1287
1304
  }
1288
- var JSONB_PASSTHROUGH_KEYS, DISPLAY_ID_METHODS, NO_FORWARD, ProxyPapiAdapter;
1305
+ var JSONB_PASSTHROUGH_KEYS, DISPLAY_ID_METHODS, HOSTED_SERVED, NO_FORWARD, ProxyPapiAdapter;
1289
1306
  var init_proxy_adapter = __esm({
1290
1307
  "src/proxy-adapter.ts"() {
1291
1308
  "use strict";
@@ -1313,6 +1330,156 @@ var init_proxy_adapter = __esm({
1313
1330
  "getRecentReviews",
1314
1331
  "getActiveDecisions"
1315
1332
  ]);
1333
+ HOSTED_SERVED = /* @__PURE__ */ new Set([
1334
+ "actionRecommendation",
1335
+ "addAgendaTopic",
1336
+ "addContributorByEmail",
1337
+ "appendBuildReport",
1338
+ "appendCycleLearnings",
1339
+ "appendCycleMetrics",
1340
+ "appendDecisionEvent",
1341
+ "appendToolMetric",
1342
+ "archiveTasks",
1343
+ "claimTask",
1344
+ "clearPendingReviewResponse",
1345
+ "compressBuildReports",
1346
+ "compressCycleLog",
1347
+ "confirmPendingActiveDecisions",
1348
+ "correctLatestBuildReportEffort",
1349
+ "countDueOwnerActions",
1350
+ "countNudgedOwnerActions",
1351
+ "countOpenOwnerActions",
1352
+ "countOwnerActionsBlockingTasks",
1353
+ "countPlanRunsForCycle",
1354
+ "createConvention",
1355
+ "createCycle",
1356
+ "createHorizon",
1357
+ "createOwnerAction",
1358
+ "createStage",
1359
+ "createTask",
1360
+ "deleteActiveDecision",
1361
+ "deleteConvention",
1362
+ "deleteDoc",
1363
+ "dismissRecommendation",
1364
+ "findPendingDocActionsForTask",
1365
+ "getActiveDecisions",
1366
+ "getActiveStage",
1367
+ "getBuildReportCountForTask",
1368
+ "getBuildReportsSince",
1369
+ "getContextHashes",
1370
+ "getContextUtilisation",
1371
+ "getCostSnapshots",
1372
+ "getCostSummary",
1373
+ "getCurrentNorthStar",
1374
+ "getCycleHealth",
1375
+ "getCycleLearningPatterns",
1376
+ "getCycleLearnings",
1377
+ "getCycleLog",
1378
+ "getCycleLogSince",
1379
+ "getDecisionEvents",
1380
+ "getDecisionEventsSince",
1381
+ "getDecisionScorePatterns",
1382
+ "getDecisionScores",
1383
+ "getDecisionUsage",
1384
+ "getDoc",
1385
+ "getDocBody",
1386
+ "getDocBodyUsage",
1387
+ "getDogfoodLog",
1388
+ "getEstimationCalibration",
1389
+ "getFailedBuildAttemptsForTask",
1390
+ "getHarnessInventory",
1391
+ "getHarnessState",
1392
+ "getLastStrategyReviewCycle",
1393
+ "getLatestDecisionScores",
1394
+ "getModelOutcomeStats",
1395
+ "getModuleEstimationStats",
1396
+ "getNorthStarSetCycle",
1397
+ "getNorthStarStaleness",
1398
+ "getOwnerIdentity",
1399
+ "getPendingAgendaTopics",
1400
+ "getPendingRecommendations",
1401
+ "getPendingReviewResponse",
1402
+ "getPlanContextSummary",
1403
+ "getProjectInfo",
1404
+ "getProjectOwnerUserId",
1405
+ "getRecentBuildReports",
1406
+ "getRecentReviews",
1407
+ "getRecentTaskComments",
1408
+ "getRecommendationEffectiveness",
1409
+ "getStrategyReviews",
1410
+ "getTask",
1411
+ "getTasks",
1412
+ "getToolCallCount",
1413
+ "getUnactionedDogfoodEntries",
1414
+ "getUnnotifiedResolvedFeedback",
1415
+ "hasToolMilestone",
1416
+ "insertPlanRun",
1417
+ "insertToolRun",
1418
+ "linkOwnerActionToTask",
1419
+ "linkPhasesToStage",
1420
+ "listContributors",
1421
+ "listConventions",
1422
+ "listMyBugReports",
1423
+ "listOwnerActionsForBlockerScan",
1424
+ "logEntityReferences",
1425
+ "markAgendaTopicsAddressed",
1426
+ "markCycleLearningResolved",
1427
+ "markFeedbackNotified",
1428
+ "moveTask",
1429
+ "planWriteBack",
1430
+ "projectExists",
1431
+ "queryBoard",
1432
+ "readCycleMetrics",
1433
+ "readCycles",
1434
+ "readCycles",
1435
+ "readDiscoveryCanvas",
1436
+ "readHorizons",
1437
+ "readPhases",
1438
+ "readPlanningLog",
1439
+ "readProductBrief",
1440
+ "readRegistries",
1441
+ "readStages",
1442
+ "readToolMetrics",
1443
+ "recordProgressStep",
1444
+ "recordTransition",
1445
+ "registerDoc",
1446
+ "removeContributorByEmail",
1447
+ "reorderDocs",
1448
+ "replaceHarnessInventory",
1449
+ "resolveLearningsForDoneTasks",
1450
+ "savePendingReviewResponse",
1451
+ "searchDocs",
1452
+ "setCriterionMet",
1453
+ "setCycleHealth",
1454
+ "setHarnessState",
1455
+ "setProjectPapiDir",
1456
+ "storeDocBody",
1457
+ "submitBugReport",
1458
+ "unclaimTask",
1459
+ "updateActiveDecision",
1460
+ "updateCycleLearningActionRef",
1461
+ "updateDiscoveryCanvas",
1462
+ "updateDocAction",
1463
+ "updateDocStatus",
1464
+ "updateDogfoodEntryStatus",
1465
+ "updateHorizonStatus",
1466
+ "updatePhaseStatus",
1467
+ "updateProductBrief",
1468
+ "updateRegistries",
1469
+ "updateStageExitCriteria",
1470
+ "updateStageStatus",
1471
+ "updateTask",
1472
+ "updateTaskStatus",
1473
+ "upsertActiveDecision",
1474
+ "upsertNorthStar",
1475
+ "writeCycleLogEntry",
1476
+ "writeDecisionScore",
1477
+ "writeDogfoodEntries",
1478
+ "writePhases",
1479
+ "writeRecommendation",
1480
+ "writeReview",
1481
+ "writeStrategyReview"
1482
+ ]);
1316
1483
  NO_FORWARD = /* @__PURE__ */ new Set([
1317
1484
  // (1) local-only
1318
1485
  "close",
@@ -1399,6 +1566,30 @@ var init_proxy_adapter = __esm({
1399
1566
  this.projectId = config.projectId ?? "";
1400
1567
  this.onAuthRejected = config.onAuthRejected;
1401
1568
  }
1569
+ /**
1570
+ * Does the hosted path genuinely serve `name`? (task-3022, C362)
1571
+ *
1572
+ * This is the honest answer to the question `typeof adapter.name === 'function'`
1573
+ * USED to ask and could not answer: the get-trap below manufactures a function
1574
+ * for any name absent from NO_FORWARD, so a structural probe reads true for
1575
+ * pg-only methods and the caller then earns a 403 from the edge.
1576
+ *
1577
+ * Answered from NO_FORWARD itself, deliberately — the same set the forwarder
1578
+ * consults — so this can never disagree with what an actual call would do. A
1579
+ * name in NO_FORWARD is either local-only or not yet wired hosted; either way
1580
+ * the caller must take its fallback. The proxy-parity test guarantees every
1581
+ * method is an explicit wrapper, in NO_FORWARD, or in the edge allowlist, so
1582
+ * "not in NO_FORWARD" is a sound proxy for "served hosted".
1583
+ *
1584
+ * A REAL method, not a forwarded one: Reflect.get in the trap returns it before
1585
+ * the forwarding branch is reached, so callers get this implementation rather
1586
+ * than a forwarder that would POST "supportsMethod" to the edge.
1587
+ */
1588
+ supportsMethod(name) {
1589
+ if (NO_FORWARD.has(name)) return false;
1590
+ if (name in _ProxyPapiAdapter.prototype) return true;
1591
+ return HOSTED_SERVED.has(name);
1592
+ }
1402
1593
  /**
1403
1594
  * task-1773: bearer-only auth probe. Hits the USER-scoped `project-list` route
1404
1595
  * (no projectId needed), so it answers exactly one question: does the proxy
package/dist/index.js CHANGED
@@ -64,6 +64,7 @@ __export(git_exports, {
64
64
  isGhAvailable: () => isGhAvailable,
65
65
  isGitAvailable: () => isGitAvailable,
66
66
  isGitRepo: () => isGitRepo,
67
+ isPathCommittedAnywhere: () => isPathCommittedAnywhere,
67
68
  isPathIgnored: () => isPathIgnored,
68
69
  isPathTracked: () => isPathTracked,
69
70
  listGroupedCycleBranches: () => listGroupedCycleBranches,
@@ -103,6 +104,22 @@ function isGitRepo(cwd) {
103
104
  return false;
104
105
  }
105
106
  }
107
+ function isPathCommittedAnywhere(cwd, path7) {
108
+ const run = (args) => {
109
+ try {
110
+ return execFileSync("git", args, { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
111
+ } catch {
112
+ return null;
113
+ }
114
+ };
115
+ const commit = run(["log", "--all", "--format=%H", "-1", "--", path7]);
116
+ if (!commit) return false;
117
+ const committedBlob = run(["rev-parse", `${commit}:${path7}`]);
118
+ if (!committedBlob) return false;
119
+ const onDiskBlob = run(["hash-object", "--", path7]);
120
+ if (!onDiskBlob) return false;
121
+ return committedBlob === onDiskBlob;
122
+ }
106
123
  function isPathTracked(cwd, path7) {
107
124
  try {
108
125
  execFileSync("git", ["ls-files", "--error-unmatch", "--", path7], {
@@ -1342,7 +1359,7 @@ function wrapWithForwarding(instance) {
1342
1359
  function createProxyAdapter(config2) {
1343
1360
  return wrapWithForwarding(new ProxyPapiAdapter(config2));
1344
1361
  }
1345
- var JSONB_PASSTHROUGH_KEYS, DISPLAY_ID_METHODS, NO_FORWARD, ProxyPapiAdapter;
1362
+ var JSONB_PASSTHROUGH_KEYS, DISPLAY_ID_METHODS, HOSTED_SERVED, NO_FORWARD, ProxyPapiAdapter;
1346
1363
  var init_proxy_adapter = __esm({
1347
1364
  "src/proxy-adapter.ts"() {
1348
1365
  "use strict";
@@ -1370,6 +1387,156 @@ var init_proxy_adapter = __esm({
1370
1387
  "getRecentReviews",
1371
1388
  "getActiveDecisions"
1372
1389
  ]);
1390
+ HOSTED_SERVED = /* @__PURE__ */ new Set([
1391
+ "actionRecommendation",
1392
+ "addAgendaTopic",
1393
+ "addContributorByEmail",
1394
+ "appendBuildReport",
1395
+ "appendCycleLearnings",
1396
+ "appendCycleMetrics",
1397
+ "appendDecisionEvent",
1398
+ "appendToolMetric",
1399
+ "archiveTasks",
1400
+ "claimTask",
1401
+ "clearPendingReviewResponse",
1402
+ "compressBuildReports",
1403
+ "compressCycleLog",
1404
+ "confirmPendingActiveDecisions",
1405
+ "correctLatestBuildReportEffort",
1406
+ "countDueOwnerActions",
1407
+ "countNudgedOwnerActions",
1408
+ "countOpenOwnerActions",
1409
+ "countOwnerActionsBlockingTasks",
1410
+ "countPlanRunsForCycle",
1411
+ "createConvention",
1412
+ "createCycle",
1413
+ "createHorizon",
1414
+ "createOwnerAction",
1415
+ "createStage",
1416
+ "createTask",
1417
+ "deleteActiveDecision",
1418
+ "deleteConvention",
1419
+ "deleteDoc",
1420
+ "dismissRecommendation",
1421
+ "findPendingDocActionsForTask",
1422
+ "getActiveDecisions",
1423
+ "getActiveStage",
1424
+ "getBuildReportCountForTask",
1425
+ "getBuildReportsSince",
1426
+ "getContextHashes",
1427
+ "getContextUtilisation",
1428
+ "getCostSnapshots",
1429
+ "getCostSummary",
1430
+ "getCurrentNorthStar",
1431
+ "getCycleHealth",
1432
+ "getCycleLearningPatterns",
1433
+ "getCycleLearnings",
1434
+ "getCycleLog",
1435
+ "getCycleLogSince",
1436
+ "getDecisionEvents",
1437
+ "getDecisionEventsSince",
1438
+ "getDecisionScorePatterns",
1439
+ "getDecisionScores",
1440
+ "getDecisionUsage",
1441
+ "getDoc",
1442
+ "getDocBody",
1443
+ "getDocBodyUsage",
1444
+ "getDogfoodLog",
1445
+ "getEstimationCalibration",
1446
+ "getFailedBuildAttemptsForTask",
1447
+ "getHarnessInventory",
1448
+ "getHarnessState",
1449
+ "getLastStrategyReviewCycle",
1450
+ "getLatestDecisionScores",
1451
+ "getModelOutcomeStats",
1452
+ "getModuleEstimationStats",
1453
+ "getNorthStarSetCycle",
1454
+ "getNorthStarStaleness",
1455
+ "getOwnerIdentity",
1456
+ "getPendingAgendaTopics",
1457
+ "getPendingRecommendations",
1458
+ "getPendingReviewResponse",
1459
+ "getPlanContextSummary",
1460
+ "getProjectInfo",
1461
+ "getProjectOwnerUserId",
1462
+ "getRecentBuildReports",
1463
+ "getRecentReviews",
1464
+ "getRecentTaskComments",
1465
+ "getRecommendationEffectiveness",
1466
+ "getStrategyReviews",
1467
+ "getTask",
1468
+ "getTasks",
1469
+ "getToolCallCount",
1470
+ "getUnactionedDogfoodEntries",
1471
+ "getUnnotifiedResolvedFeedback",
1472
+ "hasToolMilestone",
1473
+ "insertPlanRun",
1474
+ "insertToolRun",
1475
+ "linkOwnerActionToTask",
1476
+ "linkPhasesToStage",
1477
+ "listContributors",
1478
+ "listConventions",
1479
+ "listMyBugReports",
1480
+ "listOwnerActionsForBlockerScan",
1481
+ "logEntityReferences",
1482
+ "markAgendaTopicsAddressed",
1483
+ "markCycleLearningResolved",
1484
+ "markFeedbackNotified",
1485
+ "moveTask",
1486
+ "planWriteBack",
1487
+ "projectExists",
1488
+ "queryBoard",
1489
+ "readCycleMetrics",
1490
+ "readCycles",
1491
+ "readCycles",
1492
+ "readDiscoveryCanvas",
1493
+ "readHorizons",
1494
+ "readPhases",
1495
+ "readPlanningLog",
1496
+ "readProductBrief",
1497
+ "readRegistries",
1498
+ "readStages",
1499
+ "readToolMetrics",
1500
+ "recordProgressStep",
1501
+ "recordTransition",
1502
+ "registerDoc",
1503
+ "removeContributorByEmail",
1504
+ "reorderDocs",
1505
+ "replaceHarnessInventory",
1506
+ "resolveLearningsForDoneTasks",
1507
+ "savePendingReviewResponse",
1508
+ "searchDocs",
1509
+ "setCriterionMet",
1510
+ "setCycleHealth",
1511
+ "setHarnessState",
1512
+ "setProjectPapiDir",
1513
+ "storeDocBody",
1514
+ "submitBugReport",
1515
+ "unclaimTask",
1516
+ "updateActiveDecision",
1517
+ "updateCycleLearningActionRef",
1518
+ "updateDiscoveryCanvas",
1519
+ "updateDocAction",
1520
+ "updateDocStatus",
1521
+ "updateDogfoodEntryStatus",
1522
+ "updateHorizonStatus",
1523
+ "updatePhaseStatus",
1524
+ "updateProductBrief",
1525
+ "updateRegistries",
1526
+ "updateStageExitCriteria",
1527
+ "updateStageStatus",
1528
+ "updateTask",
1529
+ "updateTaskStatus",
1530
+ "upsertActiveDecision",
1531
+ "upsertNorthStar",
1532
+ "writeCycleLogEntry",
1533
+ "writeDecisionScore",
1534
+ "writeDogfoodEntries",
1535
+ "writePhases",
1536
+ "writeRecommendation",
1537
+ "writeReview",
1538
+ "writeStrategyReview"
1539
+ ]);
1373
1540
  NO_FORWARD = /* @__PURE__ */ new Set([
1374
1541
  // (1) local-only
1375
1542
  "close",
@@ -1456,6 +1623,30 @@ var init_proxy_adapter = __esm({
1456
1623
  this.projectId = config2.projectId ?? "";
1457
1624
  this.onAuthRejected = config2.onAuthRejected;
1458
1625
  }
1626
+ /**
1627
+ * Does the hosted path genuinely serve `name`? (task-3022, C362)
1628
+ *
1629
+ * This is the honest answer to the question `typeof adapter.name === 'function'`
1630
+ * USED to ask and could not answer: the get-trap below manufactures a function
1631
+ * for any name absent from NO_FORWARD, so a structural probe reads true for
1632
+ * pg-only methods and the caller then earns a 403 from the edge.
1633
+ *
1634
+ * Answered from NO_FORWARD itself, deliberately — the same set the forwarder
1635
+ * consults — so this can never disagree with what an actual call would do. A
1636
+ * name in NO_FORWARD is either local-only or not yet wired hosted; either way
1637
+ * the caller must take its fallback. The proxy-parity test guarantees every
1638
+ * method is an explicit wrapper, in NO_FORWARD, or in the edge allowlist, so
1639
+ * "not in NO_FORWARD" is a sound proxy for "served hosted".
1640
+ *
1641
+ * A REAL method, not a forwarded one: Reflect.get in the trap returns it before
1642
+ * the forwarding branch is reached, so callers get this implementation rather
1643
+ * than a forwarder that would POST "supportsMethod" to the edge.
1644
+ */
1645
+ supportsMethod(name) {
1646
+ if (NO_FORWARD.has(name)) return false;
1647
+ if (name in _ProxyPapiAdapter.prototype) return true;
1648
+ return HOSTED_SERVED.has(name);
1649
+ }
1459
1650
  /**
1460
1651
  * task-1773: bearer-only auth probe. Hits the USER-scoped `project-list` route
1461
1652
  * (no projectId needed), so it answers exactly one question: does the proxy
@@ -6193,7 +6384,78 @@ function validateFindings(findings) {
6193
6384
  return { ok: violations.length === 0, violations };
6194
6385
  }
6195
6386
  var WAB_WINDOW_DAYS = 7;
6387
+ var WAB_DEFAULT_WEEKS = 8;
6196
6388
  var WAB_WEEK_MS = WAB_WINDOW_DAYS * 24 * 60 * 60 * 1e3;
6389
+ function wabCompletionMs(row) {
6390
+ const ts = row.end_date ?? row.updated_at;
6391
+ if (!ts) return null;
6392
+ const ms = Date.parse(ts);
6393
+ return Number.isNaN(ms) ? null : ms;
6394
+ }
6395
+ function bucketWabSeries(rows, ownerUserId, nowMs, weeks = WAB_DEFAULT_WEEKS) {
6396
+ const buckets = Array.from({ length: weeks }, () => /* @__PURE__ */ new Set());
6397
+ for (const row of rows) {
6398
+ if (!row.user_id) continue;
6399
+ const ms = wabCompletionMs(row);
6400
+ if (ms === null) continue;
6401
+ const idx = Math.floor((nowMs - ms) / WAB_WEEK_MS);
6402
+ if (idx < 0 || idx >= weeks) continue;
6403
+ buckets[idx].add(row.user_id);
6404
+ }
6405
+ const out = [];
6406
+ for (let idx = weeks - 1; idx >= 0; idx--) {
6407
+ const owners = buckets[idx];
6408
+ const external = ownerUserId ? [...owners].filter((id) => id !== ownerUserId).length : owners.size;
6409
+ out.push({
6410
+ weekStart: new Date(nowMs - (idx + 1) * WAB_WEEK_MS).toISOString(),
6411
+ total: owners.size,
6412
+ external
6413
+ });
6414
+ }
6415
+ return out;
6416
+ }
6417
+ var PRODUCT_MARKER = /WHAT SHIPS FOR USERS\b[^\n:]*:/i;
6418
+ var MECHANICS_MARKER = /RELEASE MECHANICS\b[^\n:]*:/i;
6419
+ function splitCarryForward(raw) {
6420
+ const productMatch = PRODUCT_MARKER.exec(raw);
6421
+ const mechanicsMatch = MECHANICS_MARKER.exec(raw);
6422
+ if (!productMatch && !mechanicsMatch) {
6423
+ return { product: raw.trim(), mechanics: "", split: false };
6424
+ }
6425
+ if (!productMatch && mechanicsMatch) {
6426
+ const head = raw.slice(0, mechanicsMatch.index).trim();
6427
+ const tail = raw.slice(mechanicsMatch.index + mechanicsMatch[0].length).trim();
6428
+ return { product: head, mechanics: tail, split: true };
6429
+ }
6430
+ const productStart = productMatch.index + productMatch[0].length;
6431
+ if (!mechanicsMatch) {
6432
+ return { product: raw.slice(productStart).trim(), mechanics: "", split: true };
6433
+ }
6434
+ if (mechanicsMatch.index < productMatch.index) {
6435
+ return {
6436
+ product: raw.slice(productStart).trim(),
6437
+ mechanics: raw.slice(mechanicsMatch.index + mechanicsMatch[0].length, productMatch.index).trim(),
6438
+ split: true
6439
+ };
6440
+ }
6441
+ return {
6442
+ product: raw.slice(productStart, mechanicsMatch.index).trim(),
6443
+ mechanics: raw.slice(mechanicsMatch.index + mechanicsMatch[0].length).trim(),
6444
+ split: true
6445
+ };
6446
+ }
6447
+ function renderCarryForward(raw, decorate = (t) => t) {
6448
+ const { product, mechanics, split } = splitCarryForward(raw);
6449
+ if (!split) return [decorate(product)];
6450
+ const lines = [];
6451
+ if (product) lines.push(decorate(product));
6452
+ if (mechanics) {
6453
+ if (product) lines.push("");
6454
+ lines.push("**Release mechanics** \u2014 needed at release, not now:");
6455
+ lines.push(decorate(mechanics));
6456
+ }
6457
+ return lines;
6458
+ }
6197
6459
  var CHECK_VALUE_MAX = 200;
6198
6460
  function codeSpanSafe(value) {
6199
6461
  const flat = value.replace(/[`\r\n]+/g, " ").replace(/\s+/g, " ").trim();
@@ -9860,6 +10122,13 @@ function formatBlockerWaiting(blocker) {
9860
10122
  }
9861
10123
  }
9862
10124
 
10125
+ // src/lib/adapter-capability.ts
10126
+ function adapterSupports(adapter2, method) {
10127
+ const declared = adapter2.supportsMethod;
10128
+ if (typeof declared === "function") return declared.call(adapter2, method);
10129
+ return typeof adapter2[method] === "function";
10130
+ }
10131
+
9863
10132
  // src/lib/tool-telemetry.ts
9864
10133
  var INSTRUMENTED_TOOLS = /* @__PURE__ */ new Set([
9865
10134
  "plan",
@@ -9882,7 +10151,7 @@ function measureResultBytes(content) {
9882
10151
  }
9883
10152
  function recordToolRun(adapter2, sample) {
9884
10153
  if (sample.toolName !== PLAN_PREPARE_TOOL_NAME && !WRAPPER_INSTRUMENTED_TOOLS.has(sample.toolName)) return;
9885
- if (typeof adapter2.insertToolRun !== "function") return;
10154
+ if (!adapterSupports(adapter2, "insertToolRun")) return;
9886
10155
  try {
9887
10156
  const promise = adapter2.insertToolRun({
9888
10157
  toolName: sample.toolName,
@@ -9956,7 +10225,7 @@ function isProjectOwner(callerUserId, ownerUserId) {
9956
10225
  return caller === owner;
9957
10226
  }
9958
10227
  async function resolveOwnerGate(adapter2, config2) {
9959
- if (typeof adapter2.getOwnerIdentity === "function") {
10228
+ if (adapterSupports(adapter2, "getOwnerIdentity")) {
9960
10229
  try {
9961
10230
  const identity = await adapter2.getOwnerIdentity();
9962
10231
  const callerUserId = identity.callerUserId ?? config2.userId ?? null;
@@ -9978,7 +10247,7 @@ async function resolveOwnerGate(adapter2, config2) {
9978
10247
  };
9979
10248
  }
9980
10249
  }
9981
- if (typeof adapter2.getProjectOwnerUserId === "function") {
10250
+ if (adapterSupports(adapter2, "getProjectOwnerUserId")) {
9982
10251
  let ownerUserId = null;
9983
10252
  let resolutionError;
9984
10253
  try {
@@ -18897,7 +19166,7 @@ function isDecisionPending(task) {
18897
19166
  return d != null && d.answer == null;
18898
19167
  }
18899
19168
  async function countFailedAttempts(adapter2, taskId) {
18900
- if (typeof adapter2.getFailedBuildAttemptsForTask !== "function") return null;
19169
+ if (!adapterSupports(adapter2, "getFailedBuildAttemptsForTask")) return null;
18901
19170
  try {
18902
19171
  return await adapter2.getFailedBuildAttemptsForTask(taskId);
18903
19172
  } catch {
@@ -19310,7 +19579,7 @@ To override, pass force=true (emits a telemetry warning).`
19310
19579
  boardHealth: existing?.boardHealth ?? "",
19311
19580
  taskIds: existing?.taskIds ?? []
19312
19581
  };
19313
- if (typeof adapter2.commitRelease === "function") {
19582
+ if (adapterSupports(adapter2, "commitRelease")) {
19314
19583
  try {
19315
19584
  await adapter2.commitRelease({ cycle: completedCycle, snapshot: snapshot ?? null });
19316
19585
  } catch (err) {
@@ -20868,7 +21137,7 @@ async function listBuilds(adapter2, config2) {
20868
21137
  const [{ result: tasks, warnings }, health] = await Promise.all([
20869
21138
  withBaseBranchSync(
20870
21139
  { projectRoot: config2.projectRoot, baseBranch: config2.baseBranch },
20871
- () => adapter2.queryBoard()
21140
+ () => adapter2.queryBoard({ compact: true })
20872
21141
  ),
20873
21142
  adapter2.getCycleHealth().catch(() => null)
20874
21143
  ]);
@@ -20876,14 +21145,16 @@ async function listBuilds(adapter2, config2) {
20876
21145
  if (tasks.length === 0) {
20877
21146
  return { sorted: [], inProgress: [], backlog: [], blocked: [], warnings, isEmpty: true, noHandoffs: false, currentCycle, totalTasks: tasks.length };
20878
21147
  }
20879
- const withHandoff = tasks.filter((t) => {
20880
- if (!t.buildHandoff) return false;
21148
+ const candidates = tasks.filter((t) => {
20881
21149
  if (t.status === "Done" || t.status === "Cancelled" || t.status === "Blocked" || t.status === "Backlog" || t.status === "Deferred" || t.status === "Archived") {
20882
21150
  return false;
20883
21151
  }
20884
21152
  if (currentCycle > 0 && t.cycle !== currentCycle) return false;
20885
21153
  return true;
20886
21154
  });
21155
+ const hydrated = candidates.length > 0 ? await adapter2.getTasks(candidates.map((t) => t.id)) : [];
21156
+ const byId = new Map(hydrated.map((t) => [t.id, t]));
21157
+ const withHandoff = candidates.map((t) => byId.get(t.id)).filter((t) => Boolean(t?.buildHandoff));
20887
21158
  if (withHandoff.length === 0) {
20888
21159
  return { sorted: [], inProgress: [], backlog: [], blocked: [], warnings, isEmpty: false, noHandoffs: true, currentCycle, totalTasks: tasks.length };
20889
21160
  }
@@ -21381,7 +21652,7 @@ async function completeBuild(adapter2, config2, taskId, input, options = {}, cli
21381
21652
  assertDeployVerification(config2, input, { deployingNow: options.light === true });
21382
21653
  const [healthResult, priorCount] = await Promise.all([
21383
21654
  adapter2.getCycleHealth().catch(() => ({ totalCycles: 0 })),
21384
- typeof adapter2.getBuildReportCountForTask === "function" ? adapter2.getBuildReportCountForTask(taskId).catch(() => 0) : Promise.resolve(0)
21655
+ adapterSupports(adapter2, "getBuildReportCountForTask") ? adapter2.getBuildReportCountForTask(taskId).catch(() => 0) : Promise.resolve(0)
21385
21656
  ]);
21386
21657
  const cycleNumber = healthResult.totalCycles;
21387
21658
  const iterationCount = priorCount + 1;
@@ -21425,7 +21696,7 @@ async function completeBuild(adapter2, config2, taskId, input, options = {}, cli
21425
21696
  console.error(
21426
21697
  `[build] task ${taskId} related_decisions: ${relatedAdIds.length} AD(s) (${relatedAdIds.length === 0 ? "none" : relatedInferred ? "inferred from handoff" : "builder-provided"}).`
21427
21698
  );
21428
- if (report.startedAt && report.completedAt && typeof adapter2.getToolCallCount === "function") {
21699
+ if (report.startedAt && report.completedAt && adapterSupports(adapter2, "getToolCallCount")) {
21429
21700
  try {
21430
21701
  const count = await adapter2.getToolCallCount(report.startedAt, report.completedAt);
21431
21702
  if (count > 0) report.toolCallCount = count;
@@ -21438,7 +21709,7 @@ async function completeBuild(adapter2, config2, taskId, input, options = {}, cli
21438
21709
  const buildReportSummary = `${capitalizeCompleted(input.completed)}. Effort ${input.effort} vs estimated ${input.estimatedEffort}.${iterNote}${surpriseNote}${issueNote}`;
21439
21710
  const statusChange = input.completed === "yes" ? { from: task.status, to: options.light ? "Done" : "In Review" } : void 0;
21440
21711
  let atomicCommitDone = false;
21441
- if (typeof adapter2.commitBuildComplete === "function") {
21712
+ if (adapterSupports(adapter2, "commitBuildComplete")) {
21442
21713
  await adapter2.commitBuildComplete({
21443
21714
  report,
21444
21715
  taskId,
@@ -21451,7 +21722,7 @@ async function completeBuild(adapter2, config2, taskId, input, options = {}, cli
21451
21722
  await adapter2.appendBuildReport(report);
21452
21723
  }
21453
21724
  let reportWriteVerified;
21454
- if (typeof adapter2.getBuildReportCountForTask === "function") {
21725
+ if (adapterSupports(adapter2, "getBuildReportCountForTask")) {
21455
21726
  try {
21456
21727
  const postWriteCount = await adapter2.getBuildReportCountForTask(taskId);
21457
21728
  reportWriteVerified = postWriteCount >= iterationCount;
@@ -21569,7 +21840,7 @@ async function completeBuild(adapter2, config2, taskId, input, options = {}, cli
21569
21840
  if (row.findingKey) insertedByKey.set(row.findingKey, row.inserted);
21570
21841
  }
21571
21842
  const haveInsertSignal = appendedRows.length > 0;
21572
- if (findingRows.length > 0 && typeof adapter2.createTask === "function") {
21843
+ if (findingRows.length > 0 && adapterSupports(adapter2, "createTask")) {
21573
21844
  for (const { finding, learning } of findingRows) {
21574
21845
  if (learning.category !== "issue") continue;
21575
21846
  if (finding.disposition !== "filed") continue;
@@ -22390,7 +22661,7 @@ var MAX_DOC_BODY_BYTES = 2 * 1024 * 1024;
22390
22661
  var PAID_TIERS = /* @__PURE__ */ new Set(["pro", "team"]);
22391
22662
  var PRICING_URL = "https://getpapi.ai/pricing";
22392
22663
  async function resolveTier(adapter2) {
22393
- if (typeof adapter2.getMeteredUsage !== "function") return null;
22664
+ if (!adapterSupports(adapter2, "getMeteredUsage")) return null;
22394
22665
  try {
22395
22666
  const usage = await adapter2.getMeteredUsage();
22396
22667
  return usage?.tier ?? null;
@@ -22405,7 +22676,7 @@ async function enforceProjectCap(adapter2, target) {
22405
22676
  if (isSelfHostedDeployment(process.env.PAPI_SELF_HOST)) return null;
22406
22677
  const tier = await resolveTier(adapter2);
22407
22678
  if (tier === null || isPaidTier(tier)) return null;
22408
- if (typeof adapter2.listUserProjects !== "function") return null;
22679
+ if (!adapterSupports(adapter2, "listUserProjects")) return null;
22409
22680
  let projects;
22410
22681
  try {
22411
22682
  projects = await adapter2.listUserProjects();
@@ -22448,7 +22719,7 @@ The registry entry was saved and everything else is unaffected. Split the docume
22448
22719
  const tier = await resolveTier(adapter2);
22449
22720
  if (tier === null) return { storeBody: true };
22450
22721
  const ceiling = DOC_STORAGE_CEILING_BY_TIER[tier] ?? DOC_STORAGE_CEILING_BY_TIER.free;
22451
- if (typeof adapter2.getDocBodyUsage !== "function") return { storeBody: true };
22722
+ if (!adapterSupports(adapter2, "getDocBodyUsage")) return { storeBody: true };
22452
22723
  let usage;
22453
22724
  try {
22454
22725
  usage = await adapter2.getDocBodyUsage();
@@ -22625,6 +22896,7 @@ The registry stores metadata and a summary \u2014 not the body. This doc has one
22625
22896
  );
22626
22897
  }
22627
22898
  if (isPathTracked(projectRoot, path7)) return "";
22899
+ if (isPathCommittedAnywhere(projectRoot, path7)) return "";
22628
22900
  if (isPathIgnored(projectRoot, path7)) {
22629
22901
  return warn(
22630
22902
  `\`${path7}\` is excluded by .gitignore, so it cannot be committed (docs/private/ is ignored by design).`,
@@ -22826,7 +23098,7 @@ function resolveRestoreTarget(projectRoot, docPath) {
22826
23098
  return { abs };
22827
23099
  }
22828
23100
  async function handleDocRead(adapter2, config2, args) {
22829
- if (!adapter2.getDoc || typeof adapter2.getDocBody !== "function") {
23101
+ if (!adapter2.getDoc || !adapterSupports(adapter2, "getDocBody")) {
22830
23102
  return errorResponse(
22831
23103
  "Doc bodies are not available on this adapter \u2014 requires the pg/proxy adapter. Nothing is blocked; the file on disk (if any) is still the copy you have."
22832
23104
  );
@@ -23985,7 +24257,7 @@ ${checklist}
23985
24257
  });
23986
24258
  }
23987
24259
  let fixedResolvedCount = 0;
23988
- if (fixedIssues && fixedIssues.length > 0 && typeof adapter2.markCycleLearningResolved === "function") {
24260
+ if (fixedIssues && fixedIssues.length > 0 && adapterSupports(adapter2, "markCycleLearningResolved")) {
23989
24261
  const resolvedBy = `build:${result.task.displayId ?? taskId}`;
23990
24262
  for (const learningId of fixedIssues) {
23991
24263
  try {
@@ -26194,7 +26466,7 @@ Re-run build_execute complete with a production_verification field, then re-subm
26194
26466
  }
26195
26467
  const newStatus = resolveStatus(input.stage, input.verdict);
26196
26468
  const statusChange = newStatus ? { from: task.status, to: newStatus } : void 0;
26197
- if (typeof adapter2.commitReviewSubmit === "function") {
26469
+ if (adapterSupports(adapter2, "commitReviewSubmit")) {
26198
26470
  await adapter2.commitReviewSubmit({
26199
26471
  review,
26200
26472
  taskId: input.taskId,
@@ -26225,7 +26497,7 @@ Re-run build_execute complete with a production_verification field, then re-subm
26225
26497
  }
26226
26498
  }
26227
26499
  const closedDocActions = [];
26228
- if (newStatus === "Done" && typeof adapter2.findPendingDocActionsForTask === "function" && typeof adapter2.updateDocAction === "function") {
26500
+ if (newStatus === "Done" && adapterSupports(adapter2, "findPendingDocActionsForTask") && adapterSupports(adapter2, "updateDocAction")) {
26229
26501
  try {
26230
26502
  const pending = await adapter2.findPendingDocActionsForTask(input.taskId);
26231
26503
  for (const match of pending) {
@@ -26236,7 +26508,7 @@ Re-run build_execute complete with a production_verification field, then re-subm
26236
26508
  console.error(`[doc-closure] failed to resolve doc action ${match.docId}#${match.actionIndex}: ${err instanceof Error ? err.message : String(err)}`);
26237
26509
  }
26238
26510
  }
26239
- if (closedDocActions.length > 0 && typeof adapter2.appendCycleLearnings === "function") {
26511
+ if (closedDocActions.length > 0 && adapterSupports(adapter2, "appendCycleLearnings")) {
26240
26512
  try {
26241
26513
  await adapter2.appendCycleLearnings([{
26242
26514
  taskId: input.taskId,
@@ -26497,6 +26769,19 @@ function mergeAfterAccept(config2, taskId) {
26497
26769
  };
26498
26770
  }
26499
26771
  const details = [];
26772
+ if (isBranchMergedInto(config2.projectRoot, featureBranch, baseBranch)) {
26773
+ details.push(`Branch '${featureBranch}' is already merged into '${baseBranch}'.`);
26774
+ return { merged: true, skipped: false, message: `Already merged: \`${featureBranch}\` \u2192 \`${baseBranch}\`.`, details };
26775
+ }
26776
+ if (isBranchContentAlreadyInBase(config2.projectRoot, featureBranch, baseBranch)) {
26777
+ details.push(`Branch '${featureBranch}' adds nothing to '${baseBranch}' \u2014 its commits already landed there.`);
26778
+ return {
26779
+ merged: true,
26780
+ skipped: false,
26781
+ message: `Already on \`${baseBranch}\` \u2014 no PR merge needed (these commits were folded into another branch and landed with it).`,
26782
+ details
26783
+ };
26784
+ }
26500
26785
  const papiDir = join15(config2.projectRoot, ".papi");
26501
26786
  if (existsSync10(papiDir)) {
26502
26787
  try {
@@ -26841,7 +27126,7 @@ ${overlap}`;
26841
27126
  \u2705 Verdict recorded. All cycle tasks are Done, but **auto-release is owner-only** \u2014 your identity does not match this project's owner, so no release was cut. Push your branch and open a PR for the owner to run \`release\`.${resolutionNote}`;
26842
27127
  } else if (cycleTasks.length > 0 && cycleTasks.every((t) => t.status === "Done")) {
26843
27128
  let planRunCount = null;
26844
- if (typeof adapter2.countPlanRunsForCycle === "function") {
27129
+ if (adapterSupports(adapter2, "countPlanRunsForCycle")) {
26845
27130
  try {
26846
27131
  planRunCount = await adapter2.countPlanRunsForCycle(result.currentCycle);
26847
27132
  } catch {
@@ -27044,7 +27329,7 @@ async function resolveReviewerIdentity(adapter2, config2) {
27044
27329
  const note = gate.resolutionError ? ` (${gate.resolutionError})` : "";
27045
27330
  return { error: "Reviewing requires a resolvable user identity, but none was found" + note + ". Set PAPI_USER_ID to your account UUID (local) \u2014 hosted sessions derive it from the bearer token." };
27046
27331
  }
27047
- if (!gate.callerIsOwner && typeof adapter2.listContributors === "function") {
27332
+ if (!gate.callerIsOwner && adapterSupports(adapter2, "listContributors")) {
27048
27333
  try {
27049
27334
  const members = await adapter2.listContributors();
27050
27335
  if (!members.some((m) => m.userId === callerUserId)) {
@@ -27059,7 +27344,7 @@ async function resolveReviewerIdentity(adapter2, config2) {
27059
27344
  async function handleReviewClaim(adapter2, config2, args) {
27060
27345
  const taskId = typeof args.task_id === "string" ? args.task_id.trim() : "";
27061
27346
  if (!taskId) return errorResponse('A task_id is required. Example: review_claim task_id="task-2072"');
27062
- if (typeof adapter2.claimReview !== "function") {
27347
+ if (!adapterSupports(adapter2, "claimReview")) {
27063
27348
  return errorResponse("The shared review queue is not available on this adapter.");
27064
27349
  }
27065
27350
  const identity = await resolveReviewerIdentity(adapter2, config2);
@@ -28008,50 +28293,6 @@ function formatUnblockSection(candidates) {
28008
28293
  return lines.join("\n");
28009
28294
  }
28010
28295
 
28011
- // src/lib/carry-forward-shape.ts
28012
- var PRODUCT_MARKER = /WHAT SHIPS FOR USERS\b[^\n:]*:/i;
28013
- var MECHANICS_MARKER = /RELEASE MECHANICS\b[^\n:]*:/i;
28014
- function splitCarryForward(raw) {
28015
- const productMatch = PRODUCT_MARKER.exec(raw);
28016
- const mechanicsMatch = MECHANICS_MARKER.exec(raw);
28017
- if (!productMatch && !mechanicsMatch) {
28018
- return { product: raw.trim(), mechanics: "", split: false };
28019
- }
28020
- if (!productMatch && mechanicsMatch) {
28021
- const head = raw.slice(0, mechanicsMatch.index).trim();
28022
- const tail = raw.slice(mechanicsMatch.index + mechanicsMatch[0].length).trim();
28023
- return { product: head, mechanics: tail, split: true };
28024
- }
28025
- const productStart = productMatch.index + productMatch[0].length;
28026
- if (!mechanicsMatch) {
28027
- return { product: raw.slice(productStart).trim(), mechanics: "", split: true };
28028
- }
28029
- if (mechanicsMatch.index < productMatch.index) {
28030
- return {
28031
- product: raw.slice(productStart).trim(),
28032
- mechanics: raw.slice(mechanicsMatch.index + mechanicsMatch[0].length, productMatch.index).trim(),
28033
- split: true
28034
- };
28035
- }
28036
- return {
28037
- product: raw.slice(productStart, mechanicsMatch.index).trim(),
28038
- mechanics: raw.slice(mechanicsMatch.index + mechanicsMatch[0].length).trim(),
28039
- split: true
28040
- };
28041
- }
28042
- function renderCarryForward(raw, decorate = (t) => t) {
28043
- const { product, mechanics, split } = splitCarryForward(raw);
28044
- if (!split) return [decorate(product)];
28045
- const lines = [];
28046
- if (product) lines.push(decorate(product));
28047
- if (mechanics) {
28048
- if (product) lines.push("");
28049
- lines.push("**Release mechanics** \u2014 needed at release, not now:");
28050
- lines.push(decorate(mechanics));
28051
- }
28052
- return lines;
28053
- }
28054
-
28055
28296
  // src/lib/deferred-gate.ts
28056
28297
  var GATE_PHRASES = [
28057
28298
  "depends on",
@@ -28372,12 +28613,39 @@ var evaluateSelfServeActivation = async (adapter2, projectId) => {
28372
28613
  unevaluatedReason: `mechanically satisfied (${users} non-owner accounts planned in their own project) \u2014 but "ZERO owner intervention" is a human judgement, so this needs an owner tick via set_criterion_met`
28373
28614
  } : { met: false, evidence: `0 non-owner accounts with a planned cycle. Counted ${(/* @__PURE__ */ new Date()).toISOString()}.` };
28374
28615
  };
28616
+ var evaluateWeeklyActiveBuilders = async (adapter2, projectId) => {
28617
+ const sql = sqlOf(adapter2);
28618
+ if (!sql) return { met: null, unevaluatedReason: "no direct SQL access on this adapter (hosted path)" };
28619
+ const owner = await sql`
28620
+ SELECT user_id FROM projects WHERE id = ${projectId}
28621
+ `;
28622
+ const ownerUserId = owner[0]?.user_id ?? null;
28623
+ const rows = await sql`
28624
+ SELECT c.user_id, c.end_date, c.updated_at
28625
+ FROM cycles c
28626
+ JOIN projects p ON p.id = c.project_id
28627
+ JOIN auth.users u ON u.id = p.user_id
28628
+ WHERE c.status = 'complete'
28629
+ AND c.user_id IS NOT NULL
28630
+ AND u.email NOT LIKE ${TEST_ACCOUNT_PATTERN}
28631
+ `;
28632
+ const series = bucketWabSeries(rows, ownerUserId, Date.now(), WAB_DEFAULT_WEEKS);
28633
+ const peak = series.reduce((max, w) => Math.max(max, w.external), 0);
28634
+ let consecutive = 0;
28635
+ let bestRun = 0;
28636
+ for (const week of series) {
28637
+ consecutive = week.external >= 3 ? consecutive + 1 : 0;
28638
+ bestRun = Math.max(bestRun, consecutive);
28639
+ }
28640
+ const counted = `Peak ${peak} external builder(s) in any trailing week; longest run at >=3 is ${bestRun} consecutive week(s) over the last ${WAB_DEFAULT_WEEKS}. Owner and ${TEST_ACCOUNT_PATTERN} excluded. Counted ${(/* @__PURE__ */ new Date()).toISOString()}.`;
28641
+ return bestRun >= 2 ? { met: true, evidence: counted } : { met: false, evidence: counted };
28642
+ };
28375
28643
  var EVALUATORS = {
28376
28644
  "c4a11d42-b222-45af-a470-08970e1bd6a9": evaluateSelfServeActivation,
28377
- "59751bcb-f719-4cf6-a4d4-608770999a22": evaluateUnaidedFullLoop
28645
+ "59751bcb-f719-4cf6-a4d4-608770999a22": evaluateUnaidedFullLoop,
28646
+ "e9d984f4-1e75-4486-8605-5dfd07412915": evaluateWeeklyActiveBuilders
28378
28647
  };
28379
28648
  var UNEVALUATABLE = {
28380
- "e9d984f4-1e75-4486-8605-5dfd07412915": "no evaluator here \u2014 the canonical completion-keyed definition lives in lib/weekly-active-builders.ts (Next app) and must not be re-derived in the server",
28381
28649
  "c596a082-9ad3-4089-b497-46cd189e892b": "threshold is provisional pending an owner ruling \u2014 no settled bar to evaluate against"
28382
28650
  };
28383
28651
  async function evaluateExitCriteria(adapter2, projectId, criteria) {
@@ -28787,7 +29055,10 @@ async function getHierarchyPosition(adapter2, projectId) {
28787
29055
  adapter2.readHorizons?.() ?? [],
28788
29056
  adapter2.readStages?.() ?? [],
28789
29057
  adapter2.readPhases(),
28790
- adapter2.queryBoard()
29058
+ // Compact: the only fields read below are `phase` and `status`, for the
29059
+ // per-phase done/total tally. A non-compact pull dragged every handoff and
29060
+ // build report across the wire for two columns.
29061
+ adapter2.queryBoard({ compact: true })
28791
29062
  ]);
28792
29063
  if (horizons.length === 0) return void 0;
28793
29064
  const isInProgress = (s) => s.status === "In Progress";
@@ -28907,7 +29178,7 @@ function formatDiscoveredIssuesBlocks(candidateLearnings, closedTaskIds) {
28907
29178
  return { alertsNote, unactionedIssuesNote };
28908
29179
  }
28909
29180
  async function computeTeamSummary(adapter2, contributorsInput) {
28910
- if (typeof adapter2.listContributors !== "function") return void 0;
29181
+ if (!adapterSupports(adapter2, "listContributors")) return void 0;
28911
29182
  let members;
28912
29183
  try {
28913
29184
  members = (await (contributorsInput ?? adapter2.listContributors())).length;
@@ -28928,7 +29199,7 @@ async function computeTeamSummary(adapter2, contributorsInput) {
28928
29199
  return `**Team:** ${members} members \xB7 ${pool} in pool \xB7 ${inFlight} in flight \xB7 ${reviewQueue} in review`;
28929
29200
  }
28930
29201
  async function computeReleaseHistory(adapter2, contributorsInput) {
28931
- if (typeof adapter2.listContributors !== "function") return void 0;
29202
+ if (!adapterSupports(adapter2, "listContributors")) return void 0;
28932
29203
  let contributors;
28933
29204
  try {
28934
29205
  contributors = await (contributorsInput ?? adapter2.listContributors());
@@ -28968,7 +29239,29 @@ function formatResolvedFeedback(items) {
28968
29239
  lines.push("_Thanks for the signal \u2014 it shaped what shipped._");
28969
29240
  return lines.join("\n");
28970
29241
  }
28971
- async function handleOrient(adapter2, config2, args = {}, clientName) {
29242
+ function withBoardMemo(adapter2) {
29243
+ const inflight = /* @__PURE__ */ new Map();
29244
+ const queryBoard = (options) => {
29245
+ const key = JSON.stringify(options ?? null);
29246
+ const hit = inflight.get(key);
29247
+ if (hit) return hit;
29248
+ const pending = adapter2.queryBoard(options).catch((err) => {
29249
+ inflight.delete(key);
29250
+ throw err;
29251
+ });
29252
+ inflight.set(key, pending);
29253
+ return pending;
29254
+ };
29255
+ return new Proxy(adapter2, {
29256
+ get(target, prop, receiver) {
29257
+ if (prop === "queryBoard") return queryBoard;
29258
+ const value = Reflect.get(target, prop, receiver);
29259
+ return typeof value === "function" ? value.bind(target) : value;
29260
+ }
29261
+ });
29262
+ }
29263
+ async function handleOrient(rawAdapter, config2, args = {}, clientName) {
29264
+ const adapter2 = withBoardMemo(rawAdapter);
28972
29265
  const environment = normaliseEnvironment(args.environment);
28973
29266
  const deepHousekeeping = args.deep_housekeeping === true;
28974
29267
  const fullEnrichment = args.full === true || deepHousekeeping;
@@ -29464,7 +29757,7 @@ ${versionDrift}` : "";
29464
29757
  }
29465
29758
  }
29466
29759
  tracker.mark("parallel-tail");
29467
- const sharedContributorsPromise = typeof adapter2.listContributors === "function" ? adapter2.listContributors().catch(() => []) : Promise.resolve([]);
29760
+ const sharedContributorsPromise = adapterSupports(adapter2, "listContributors") ? adapter2.listContributors().catch(() => []) : Promise.resolve([]);
29468
29761
  const [unblockCandidates, subAgents, teamSummaryLine, releaseHistoryLine, carryForwardRefs] = await Promise.all([
29469
29762
  tracked("unblock-candidates", () => findUnblockCandidates(adapter2, currentCycle))().catch(() => []),
29470
29763
  // task-1866: discover project sub-agents for the orient surface (read-only, never throws).
@@ -31071,7 +31364,7 @@ var contributorListTool = {
31071
31364
  }
31072
31365
  };
31073
31366
  function denyUnlessCohortCapable(adapter2) {
31074
- if (typeof adapter2.listContributors !== "function") {
31367
+ if (!adapterSupports(adapter2, "listContributors")) {
31075
31368
  return "Contributor management is not available on this adapter (requires the pg or proxy adapter \u2014 the local md adapter has no cohort).";
31076
31369
  }
31077
31370
  return null;
@@ -31092,7 +31385,7 @@ async function denyUnlessMember(adapter2, config2) {
31092
31385
  const gate = await resolveOwnerGate(adapter2, config2);
31093
31386
  if (!gate.enforced) return null;
31094
31387
  if (gate.callerIsOwner) return null;
31095
- const callerRole = gate.callerUserId && typeof adapter2.getContributorRole === "function" ? await adapter2.getContributorRole(gate.callerUserId).catch(() => null) : null;
31388
+ const callerRole = gate.callerUserId && adapterSupports(adapter2, "getContributorRole") ? await adapter2.getContributorRole(gate.callerUserId).catch(() => null) : null;
31096
31389
  if (callerRole) return null;
31097
31390
  const note = gate.resolutionError ? ` (Identity resolution failed: ${gate.resolutionError} \u2014 the gate fails closed; retry once connectivity is restored.)` : "";
31098
31391
  return `Listing contributors is restricted to project members. Your identity does not match this project's owner or any contributor.${note}`;
@@ -31193,7 +31486,7 @@ function requireTaskId(args) {
31193
31486
  return id.length > 0 ? id : null;
31194
31487
  }
31195
31488
  async function resolveClaimerIdentity(adapter2, config2) {
31196
- if (typeof adapter2.claimTask !== "function") {
31489
+ if (!adapterSupports(adapter2, "claimTask")) {
31197
31490
  return { error: "Claiming is not available on this adapter." };
31198
31491
  }
31199
31492
  const gate = await resolveOwnerGate(adapter2, config2);
@@ -31204,7 +31497,7 @@ async function resolveClaimerIdentity(adapter2, config2) {
31204
31497
  error: "Claiming requires a resolvable user identity, but none was found" + note + ". Set PAPI_USER_ID to your account UUID (local) \u2014 hosted sessions derive it from your bearer token."
31205
31498
  };
31206
31499
  }
31207
- if (!gate.callerIsOwner && typeof adapter2.listContributors === "function") {
31500
+ if (!gate.callerIsOwner && adapterSupports(adapter2, "listContributors")) {
31208
31501
  try {
31209
31502
  const members = await adapter2.listContributors();
31210
31503
  if (!members.some((m) => m.userId === callerUserId)) {
@@ -31290,7 +31583,7 @@ ${claimedList}`
31290
31583
  async function handleTaskUnclaim(adapter2, config2, args) {
31291
31584
  const taskId = requireTaskId(args);
31292
31585
  if (!taskId) return errorResponse('A task_id is required. Example: task_unclaim task_id="task-2071"');
31293
- if (typeof adapter2.unclaimTask !== "function") {
31586
+ if (!adapterSupports(adapter2, "unclaimTask")) {
31294
31587
  return errorResponse("Unclaiming is not available on this adapter.");
31295
31588
  }
31296
31589
  const identity = await resolveClaimerIdentity(adapter2, config2);
@@ -31341,7 +31634,7 @@ async function handleTaskMove(adapter2, config2, args) {
31341
31634
  if (!taskId) return errorResponse('A task_id is required. Example: task_move task_id="task-42" target_project="other-project" confirm=true');
31342
31635
  const targetProject = requireStr(args.target_project);
31343
31636
  if (!targetProject) return errorResponse("A target_project (slug or UUID) is required \u2014 the project you want to move the task into.");
31344
- if (typeof adapter2.moveTask !== "function") {
31637
+ if (!adapterSupports(adapter2, "moveTask")) {
31345
31638
  return errorResponse("Cross-project move is not available on this adapter.");
31346
31639
  }
31347
31640
  const gate = await resolveOwnerGate(adapter2, config2);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@papi-ai/server",
3
- "version": "0.7.81",
3
+ "version": "0.7.82",
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",