@papi-ai/server 0.7.76 → 0.7.78

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.
@@ -560,11 +560,13 @@ function ensureTagAtHead(cwd, tag, message) {
560
560
  message: `tag "${tag}" already exists but points at ${target ? target.slice(0, 7) : "an unknown commit"}, not the current HEAD (${head ? head.slice(0, 7) : "unknown"}). If it is left over from an aborted release, delete it and re-run release: \`git tag -d ${tag}\` (and \`git push origin :refs/tags/${tag}\` if it was pushed). Otherwise use a different version.`
561
561
  };
562
562
  }
563
- function getLatestTag(cwd) {
563
+ function getLatestTag(cwd, timeoutMs) {
564
564
  try {
565
565
  return execFileSync("git", ["describe", "--tags", "--abbrev=0"], {
566
566
  cwd,
567
- encoding: "utf-8"
567
+ encoding: "utf-8",
568
+ stdio: ["ignore", "pipe", "pipe"],
569
+ ...timeoutMs != null ? { timeout: timeoutMs } : {}
568
570
  }).trim() || null;
569
571
  } catch {
570
572
  return null;
@@ -806,20 +808,26 @@ function findTaskCommitsOnBase(cwd, preferredBase, displayIds) {
806
808
  try {
807
809
  raw = execFileSync(
808
810
  "git",
809
- ["log", base, "--format=%h%x01%s", "-n", "1000"],
811
+ ["log", base, "--format=%h%x01%s%x01%b%x02", "-n", "1000"],
810
812
  { cwd, encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"] }
811
813
  );
812
814
  } catch {
813
815
  return out;
814
816
  }
815
- const commits = raw.split("\n").map((line) => {
816
- const idx = line.indexOf("");
817
- if (idx === -1) return null;
818
- return { hash: line.slice(0, idx).trim(), subject: line.slice(idx + 1).trim() };
819
- }).filter((c) => c !== null && c.hash !== "");
817
+ const commits = raw.split("").map((record) => {
818
+ const parts = record.split("");
819
+ if (parts.length < 2) return null;
820
+ return {
821
+ hash: parts[0].trim(),
822
+ subject: parts[1].trim(),
823
+ body: (parts[2] ?? "").trim()
824
+ };
825
+ }).filter(
826
+ (c) => c !== null && c.hash !== ""
827
+ );
820
828
  for (const displayId of displayIds) {
821
829
  const re = new RegExp(`(^|[^\\w-])${escapeRegexLiteral(displayId)}([^\\w-]|$)`);
822
- const hit = commits.find((c) => re.test(c.subject));
830
+ const hit = commits.find((c) => re.test(c.subject)) ?? commits.find((c) => re.test(c.body));
823
831
  if (!hit) continue;
824
832
  const prMatch = hit.subject.match(/#(\d+)/);
825
833
  out.set(displayId, {
@@ -1236,6 +1244,14 @@ var init_proxy_adapter = __esm({
1236
1244
  // (1) local-only
1237
1245
  "close",
1238
1246
  "initRls",
1247
+ // task-3207 (C357): commitBuildComplete, commitReviewSubmit, commitRelease have no
1248
+ // edge case handler and no ALLOWED_METHODS entry — forwarding them 403s at the edge
1249
+ // with no try/catch at the call site, crashing build_execute/review_submit/release
1250
+ // completion for every hosted user. Restores the intended graceful degradation
1251
+ // (separate appendBuildReport + updateTaskStatus calls) until they're atomically wired.
1252
+ "commitBuildComplete",
1253
+ "commitReviewSubmit",
1254
+ "commitRelease",
1239
1255
  // (2) not-yet-wired hosted gaps — shrink as data-proxy handlers land (task-2390).
1240
1256
  // getToolCallCount + updatePhaseStatus are ABSENT: they now have edge handlers and
1241
1257
  // are served through the forwarder (getToolCallCount = SUP-2026-026 handler #1).
@@ -1281,10 +1297,39 @@ var init_proxy_adapter = __esm({
1281
1297
  endpoint;
1282
1298
  apiKey;
1283
1299
  projectId;
1300
+ onAuthRejected;
1284
1301
  constructor(config) {
1285
1302
  this.endpoint = config.endpoint.replace(/\/$/, "");
1286
1303
  this.apiKey = config.apiKey;
1287
1304
  this.projectId = config.projectId ?? "";
1305
+ this.onAuthRejected = config.onAuthRejected;
1306
+ }
1307
+ /**
1308
+ * task-1773: bearer-only auth probe. Hits the USER-scoped `project-list` route
1309
+ * (no projectId needed), so it answers exactly one question: does the proxy
1310
+ * still accept this bearer?
1311
+ *
1312
+ * Returns the HTTP status, or 0 when the call could not be made at all
1313
+ * (network error / timeout). Callers MUST treat 0 — and any status that is
1314
+ * neither 2xx nor 401 — as "no signal", never as a rejection: a proxy outage
1315
+ * must not masquerade as a revoked token and force every user to re-auth.
1316
+ */
1317
+ async probeBearerStatus() {
1318
+ try {
1319
+ const response = await fetch(`${this.endpoint}/project-list`, {
1320
+ method: "POST",
1321
+ headers: {
1322
+ "Content-Type": "application/json",
1323
+ "Authorization": `Bearer ${this.apiKey}`
1324
+ },
1325
+ body: "{}",
1326
+ signal: AbortSignal.timeout(5e3)
1327
+ });
1328
+ if (response.status === 401) this.onAuthRejected?.();
1329
+ return response.status;
1330
+ } catch {
1331
+ return 0;
1332
+ }
1288
1333
  }
1289
1334
  /** Resolved project ID — available after ensureProject() completes. */
1290
1335
  getProjectId() {
@@ -1301,7 +1346,8 @@ var init_proxy_adapter = __esm({
1301
1346
  return wrapWithForwarding(new _ProxyPapiAdapter({
1302
1347
  endpoint: this.endpoint,
1303
1348
  apiKey: this.apiKey,
1304
- projectId
1349
+ projectId,
1350
+ onAuthRejected: this.onAuthRejected
1305
1351
  }));
1306
1352
  }
1307
1353
  /**
@@ -1388,6 +1434,7 @@ var init_proxy_adapter = __esm({
1388
1434
  message = errorBody;
1389
1435
  }
1390
1436
  if (response.status === 401) {
1437
+ this.onAuthRejected?.();
1391
1438
  throw new Error(
1392
1439
  `Auth: Invalid API key \u2014 PAPI_DATA_API_KEY was rejected by the proxy.
1393
1440
  This usually means the key was revoked or replaced. Mint a fresh key in the Connect panel on your PAPI dashboard (https://getpapi.ai/hub), then update PAPI_DATA_API_KEY in your .mcp.json.
@@ -1966,6 +2013,7 @@ Check PAPI_PROJECT_ID in your .mcp.json config. Find your project ID in the PAPI
1966
2013
  } catch {
1967
2014
  message = errorBody;
1968
2015
  }
2016
+ if (response.status === 401) this.onAuthRejected?.();
1969
2017
  throw new Error(`Proxy error (${response.status}) on ${route}: ${message}`);
1970
2018
  }
1971
2019
  const body = await response.json();
@@ -2020,7 +2068,7 @@ import { pathToFileURL } from "url";
2020
2068
  import path2 from "path";
2021
2069
  import { execSync } from "child_process";
2022
2070
 
2023
- // ../adapter-md/dist/index.js
2071
+ // ../../../../../Users/cathaloullivan/Ai-App-Projects/PapiUI/packages/adapter-md/dist/index.js
2024
2072
  import { readFile, writeFile, access } from "fs/promises";
2025
2073
  import { randomUUID as randomUUID6 } from "crypto";
2026
2074
  import { join } from "path";
@@ -2282,9 +2330,12 @@ function parsePlanningLog(content, activeDecisionsContent, cycleLogContent) {
2282
2330
  var VALID_EFFORT_SIZES = /* @__PURE__ */ new Set(["XS", "S", "M", "L", "XL"]);
2283
2331
  var SECTION_HEADERS = [
2284
2332
  "SCOPE (DO THIS)",
2333
+ "WHY NOT SIMPLER",
2285
2334
  "SCOPE BOUNDARY (DO NOT DO THIS)",
2286
2335
  "ACCEPTANCE CRITERIA",
2336
+ "PRE-MORTEM",
2287
2337
  "SECURITY CONSIDERATIONS",
2338
+ "DEPLOY VERIFICATION",
2288
2339
  "PRE-BUILD VERIFICATION",
2289
2340
  "FILES LIKELY TOUCHED",
2290
2341
  "EFFORT"
@@ -2323,7 +2374,7 @@ function parseBulletsOnly(text) {
2323
2374
  return text.split("\n").filter((l) => /^\s*-\s/.test(l)).map((l) => l.replace(/^\s*-\s*/, "").trim()).filter((l) => l.length > 0);
2324
2375
  }
2325
2376
  function parseChecklist(text) {
2326
- return text.split("\n").map((l) => l.replace(/^\s*\[[ x]]\s*/, "").trim()).filter((l) => l.length > 0);
2377
+ return text.split("\n").map((l) => l.replace(/^\s*(?:[-*+]\s*)?(?:\[[ xX]\]\s*)?/, "").trim()).filter((l) => l.length > 0);
2327
2378
  }
2328
2379
  function parseBuildHandoff(markdown) {
2329
2380
  if (typeof markdown !== "string" || !markdown.trim()) return null;
@@ -2943,6 +2994,67 @@ var ACCURACY_HEADER = "| Cycle | Reports | Match Rate | MAE | Bias |";
2943
2994
  var ACCURACY_SEPARATOR = "|--------|---------|------------|-----|------|";
2944
2995
  var VELOCITY_HEADER = "| Cycle | Completed | Partial | Failed | Effort Points |";
2945
2996
  var VELOCITY_SEPARATOR = "|--------|-----------|---------|--------|---------------|";
2997
+ var EFFORT_SCALE = {
2998
+ XS: 1,
2999
+ S: 2,
3000
+ M: 3,
3001
+ L: 4,
3002
+ XL: 5
3003
+ };
3004
+ function effortOrdinal(effort) {
3005
+ if (typeof effort !== "string") return void 0;
3006
+ const normalized = effort.trim().toUpperCase();
3007
+ return EFFORT_SCALE[normalized];
3008
+ }
3009
+ function isUnparsedEffort(effort) {
3010
+ if (typeof effort !== "string" || effort.trim().length === 0) return false;
3011
+ return effortOrdinal(effort) === void 0;
3012
+ }
3013
+ function calculateCycleMetrics(reports, currentCycle, window = 5) {
3014
+ const recentReports = reports.filter(
3015
+ (r) => r.cycle > currentCycle - window && r.cycle <= currentCycle
3016
+ );
3017
+ const unparsedEffortCount = recentReports.filter(
3018
+ (r) => isUnparsedEffort(r.actualEffort) || isUnparsedEffort(r.estimatedEffort)
3019
+ ).length;
3020
+ const perCycle = /* @__PURE__ */ new Map();
3021
+ for (const r of recentReports) {
3022
+ const group = perCycle.get(r.cycle) ?? [];
3023
+ group.push(r);
3024
+ perCycle.set(r.cycle, group);
3025
+ }
3026
+ const accuracy = [];
3027
+ const velocity = [];
3028
+ const sortedCycles = [...perCycle.keys()].sort((a, b) => a - b);
3029
+ for (const cycle of sortedCycles) {
3030
+ const reps = perCycle.get(cycle);
3031
+ const deltas = [];
3032
+ for (const r of reps) {
3033
+ const actual = effortOrdinal(r.actualEffort);
3034
+ const estimated = effortOrdinal(r.estimatedEffort);
3035
+ if (actual !== void 0 && estimated !== void 0) {
3036
+ deltas.push(actual - estimated);
3037
+ }
3038
+ }
3039
+ if (deltas.length > 0) {
3040
+ accuracy.push({
3041
+ cycle,
3042
+ reports: deltas.length,
3043
+ matchRate: Math.round(deltas.filter((d) => d === 0).length / deltas.length * 100),
3044
+ mae: Math.round(deltas.reduce((s, d) => s + Math.abs(d), 0) / deltas.length * 10) / 10,
3045
+ bias: Math.round(deltas.reduce((s, d) => s + d, 0) / deltas.length * 10) / 10
3046
+ });
3047
+ }
3048
+ velocity.push({
3049
+ cycle,
3050
+ completed: reps.filter((r) => r.completed === "Yes").length,
3051
+ partial: reps.filter((r) => r.completed === "Partial").length,
3052
+ failed: reps.filter((r) => r.completed === "No").length,
3053
+ effortPoints: reps.reduce((s, r) => s + (effortOrdinal(r.actualEffort) ?? 0), 0)
3054
+ });
3055
+ }
3056
+ return { accuracy, velocity, unparsedEffortCount };
3057
+ }
2946
3058
  function serializeAccuracyRow(a) {
2947
3059
  return `| ${a.cycle} | ${a.reports} | ${a.matchRate}% | ${a.mae} | ${a.bias >= 0 ? "+" : ""}${a.bias} |`;
2948
3060
  }
@@ -4367,6 +4479,7 @@ async function createAdapter(optionsOrType, maybePapiDir) {
4367
4479
  case "pg": {
4368
4480
  const { PgAdapter, PgPapiAdapter, configFromEnv } = await import("@papi-ai/adapter-pg");
4369
4481
  let projectId = process.env["PAPI_PROJECT_ID"];
4482
+ const projectIdWasPreSupplied = Boolean(projectId);
4370
4483
  const projectRoot = options.projectRoot ?? process.env["PAPI_PROJECT_DIR"] ?? process.cwd();
4371
4484
  let rootHash = null;
4372
4485
  let originUrl = null;
@@ -4430,6 +4543,26 @@ async function createAdapter(optionsOrType, maybePapiDir) {
4430
4543
  }
4431
4544
  const config = papiEndpoint ? { connectionString: papiEndpoint } : configFromEnv();
4432
4545
  validateDatabaseUrl(config.connectionString);
4546
+ if (projectIdWasPreSupplied) {
4547
+ const ownershipProbe = new PgAdapter(config);
4548
+ try {
4549
+ const owned = await ownershipProbe.findProjectById(projectId, resolveUserId);
4550
+ if (!owned) {
4551
+ throw new Error(
4552
+ `PAPI_PROJECT_ID ${projectId} does not belong to you.
4553
+
4554
+ The project exists under a different owner, or does not exist at all. PAPI refuses to attach to a project you do not own \u2014 writing to it would put your cycles, tasks and Active Decisions into somebody else's project.
4555
+
4556
+ Fix the PAPI_PROJECT_ID in your .mcp.json (or unset it and let PAPI resolve the project from your git remote), then reconnect.`
4557
+ );
4558
+ }
4559
+ } finally {
4560
+ try {
4561
+ await ownershipProbe.close();
4562
+ } catch {
4563
+ }
4564
+ }
4565
+ }
4433
4566
  const { ensureSchema } = await import("@papi-ai/adapter-pg");
4434
4567
  try {
4435
4568
  await ensureSchema(config);
@@ -4726,13 +4859,14 @@ function computeSnapshotsFromBuildReports(reports, tasks) {
4726
4859
  const cycleReports = reportsByCycle.get(sn) ?? [];
4727
4860
  const cycleTaskRows = tasksByCycle.get(sn);
4728
4861
  const withEffort = cycleReports.filter((r) => r.estimatedEffort && r.actualEffort);
4729
- const accurate = withEffort.filter((r) => r.estimatedEffort === r.actualEffort).length;
4730
- const matchRate = withEffort.length > 0 ? Math.round(accurate / withEffort.length * 100) : 0;
4862
+ const [computedAccuracy] = calculateCycleMetrics(withEffort, sn, 1).accuracy;
4731
4863
  const { completed, total, plannedPoints, deliveredPoints } = computeCycleEffort(cycleTaskRows, cycleReports);
4732
4864
  snapshots.push({
4733
4865
  cycle: sn,
4734
4866
  date: (/* @__PURE__ */ new Date()).toISOString(),
4735
- accuracy: [{ cycle: sn, reports: cycleReports.length, matchRate, mae: 0, bias: 0 }],
4867
+ // No report in this cycle carried BOTH an estimate and an actual, so there
4868
+ // is genuinely nothing to measure. Zeros here mean "no data", not "no bias".
4869
+ accuracy: [computedAccuracy ?? { cycle: sn, reports: 0, matchRate: 0, mae: 0, bias: 0 }],
4736
4870
  velocity: [{
4737
4871
  cycle: sn,
4738
4872
  completed,