@papi-ai/server 0.7.85 → 0.7.103

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.
@@ -30,7 +30,6 @@ function resolveDeploymentProfile(env) {
30
30
  dataEndpoint: cleanUrl(env.dataEndpoint) ?? (hostedSupabaseUrl ? `${hostedSupabaseUrl}/functions/v1/data-proxy` : void 0) ?? (selfHosted ? void 0 : `${HOSTED_SUPABASE_URL}/functions/v1/data-proxy`),
31
31
  providers: {
32
32
  vercel: optionalBoolean(env.vercelEnabled, !selfHosted),
33
- railway: optionalBoolean(env.railwayEnabled, !selfHosted),
34
33
  managedSupabase: optionalBoolean(env.managedSupabaseEnabled, !selfHosted)
35
34
  },
36
35
  auth: {
@@ -100,6 +99,21 @@ var init_dist = __esm({
100
99
  "../shared/dist/index.js"() {
101
100
  "use strict";
102
101
  CAPABILITY_REGISTRY = [
102
+ // task-3384: the PLAN step. The landing page has advertised these three since
103
+ // the v5 control beat (components/marketing/landing/ControlC.tsx) with a code
104
+ // comment reading "shipping soon, shown now" — the registry had no plan-step
105
+ // entry at all, so the Planning group rendered empty and was dropped. These
106
+ // are NOT new behaviour: each one gates a section of the planner instructions
107
+ // that already runs on every full-mode plan, so ON reproduces today exactly.
108
+ //
109
+ // All three default ON. Turning one off removes its step from the planner
110
+ // prompt, which is the whole mechanism — the planner does what the prompt
111
+ // tells it to, so an omitted step is genuinely not performed (and the matching
112
+ // plan-prepare progress checkpoint is not recorded, so the hub does not claim
113
+ // a step that never ran).
114
+ { key: "planHealthCheck", label: "Health check", description: "Flags cycle gaps, stale tasks and AD conflicts before planning.", step: "plan" },
115
+ { key: "backlogTriage", label: "Backlog triage", description: "Cleans up and prioritises unreviewed backlog items during planning.", step: "plan" },
116
+ { key: "buildOrder", label: "Build order", description: "Detects intra-cycle dependencies and sequences the build.", step: "plan" },
103
117
  { key: "prReviewer", label: "Auto code review", description: "Runs a code review of the branch diff before accepting.", step: "review" },
104
118
  { key: "securityScan", label: "Security scan", description: "Flags a security pass on risk-tier changes at review time.", step: "review" },
105
119
  { key: "changelog", label: "Changelog & cycle update", description: "Curates a cycle-update post when a release ships.", step: "release" },
@@ -154,7 +168,7 @@ var init_dist = __esm({
154
168
  CAPABILITY_KEYS = CAPABILITY_REGISTRY.map((c) => c.key);
155
169
  HOSTED_APP_URL = "https://getpapi.ai";
156
170
  HOSTED_MCP_URL = "https://mcp.getpapi.ai";
157
- HOSTED_SUPABASE_URL = "https://guewgygcpcmrcoppihzx.supabase.co";
171
+ HOSTED_SUPABASE_URL = "https://api.getpapi.ai";
158
172
  WAB_WINDOW_DAYS = 7;
159
173
  WAB_WEEK_MS = WAB_WINDOW_DAYS * 24 * 60 * 60 * 1e3;
160
174
  EFFORT_SCALE = {
@@ -175,6 +189,7 @@ __export(git_exports, {
175
189
  checkoutBranch: () => checkoutBranch,
176
190
  commitSinglePath: () => commitSinglePath,
177
191
  commitStagedOnly: () => commitStagedOnly,
192
+ computeAheadBehind: () => computeAheadBehind,
178
193
  createAndCheckoutBranch: () => createAndCheckoutBranch,
179
194
  createPullRequest: () => createPullRequest,
180
195
  createTag: () => createTag,
@@ -185,8 +200,10 @@ __export(git_exports, {
185
200
  detectUnrecordedCommits: () => detectUnrecordedCommits,
186
201
  ensureLatestDevelop: () => ensureLatestDevelop,
187
202
  ensureTagAtHead: () => ensureTagAtHead,
203
+ fetchBaseBranch: () => fetchBaseBranch,
188
204
  findContributorReleasePullRequests: () => findContributorReleasePullRequests,
189
205
  findTaskCommitsOnBase: () => findTaskCommitsOnBase,
206
+ getBaseDivergence: () => getBaseDivergence,
190
207
  getBranchDiff: () => getBranchDiff,
191
208
  getCommitFiles: () => getCommitFiles,
192
209
  getCommitsSinceTag: () => getCommitsSinceTag,
@@ -221,6 +238,7 @@ __export(git_exports, {
221
238
  hasUnpushedCommits: () => hasUnpushedCommits,
222
239
  isBranchContentAlreadyInBase: () => isBranchContentAlreadyInBase,
223
240
  isBranchMergedInto: () => isBranchMergedInto,
241
+ isCommitReachable: () => isCommitReachable,
224
242
  isGhAvailable: () => isGhAvailable,
225
243
  isGitAvailable: () => isGitAvailable,
226
244
  isGitRepo: () => isGitRepo,
@@ -1283,6 +1301,19 @@ function isBranchMergedInto(cwd, branch, baseBranch) {
1283
1301
  return false;
1284
1302
  }
1285
1303
  }
1304
+ function isCommitReachable(cwd, commit, baseBranch) {
1305
+ const recordedCommit = commit.trim();
1306
+ if (!recordedCommit) return false;
1307
+ try {
1308
+ execFileSync("git", ["merge-base", "--is-ancestor", recordedCommit, baseBranch], {
1309
+ cwd,
1310
+ stdio: "ignore"
1311
+ });
1312
+ return true;
1313
+ } catch {
1314
+ return false;
1315
+ }
1316
+ }
1286
1317
  function isBranchContentAlreadyInBase(cwd, branch, baseBranch) {
1287
1318
  const resolveCommit = (ref) => {
1288
1319
  try {
@@ -1309,6 +1340,55 @@ function isBranchContentAlreadyInBase(cwd, branch, baseBranch) {
1309
1340
  return false;
1310
1341
  }
1311
1342
  }
1343
+ function fetchBaseBranch(cwd, baseBranch, timeoutMs = GIT_FETCH_TIMEOUT_MS) {
1344
+ if (!isGitAvailable() || !isGitRepo(cwd) || !hasRemote(cwd)) {
1345
+ return { fetched: false, compareRef: baseBranch };
1346
+ }
1347
+ const result = spawnSync("git", ["fetch", "--quiet", "origin", baseBranch], {
1348
+ cwd,
1349
+ encoding: "utf-8",
1350
+ timeout: timeoutMs
1351
+ });
1352
+ if (result.error || result.status !== 0) {
1353
+ const raw = result.error?.message ?? ((result.stderr || "").trim() || `git exited ${result.status}`);
1354
+ const isTimeout = result.signal === "SIGTERM" || /ETIMEDOUT/.test(raw);
1355
+ return {
1356
+ fetched: false,
1357
+ compareRef: baseBranch,
1358
+ warning: isTimeout ? `Fetch from origin timed out after ${Math.round(timeoutMs / 1e3)}s \u2014 comparing against the local '${baseBranch}' ref, which may be stale.` : `Fetch from origin failed \u2014 comparing against the local '${baseBranch}' ref, which may be stale. (${raw})`
1359
+ };
1360
+ }
1361
+ return { fetched: true, compareRef: `origin/${baseBranch}` };
1362
+ }
1363
+ function computeAheadBehind(cwd, compareRef, ref = "HEAD") {
1364
+ try {
1365
+ const out = execFileSync(
1366
+ "git",
1367
+ ["rev-list", "--left-right", "--count", `${compareRef}...${ref}`],
1368
+ { cwd, encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"] }
1369
+ ).trim();
1370
+ const [behindStr, aheadStr] = out.split(/\s+/);
1371
+ const behind = parseInt(behindStr, 10);
1372
+ const ahead = parseInt(aheadStr, 10);
1373
+ if (Number.isNaN(behind) || Number.isNaN(ahead)) return null;
1374
+ return { ahead, behind };
1375
+ } catch {
1376
+ return null;
1377
+ }
1378
+ }
1379
+ function getBaseDivergence(cwd, preferredBase, opts) {
1380
+ const baseBranch = resolveBaseBranch(cwd, preferredBase);
1381
+ const fetch2 = fetchBaseBranch(cwd, baseBranch, opts?.timeoutMs);
1382
+ const divergence = computeAheadBehind(cwd, fetch2.compareRef, opts?.ref ?? "HEAD");
1383
+ return {
1384
+ baseBranch,
1385
+ compareRef: fetch2.compareRef,
1386
+ fetched: fetch2.fetched,
1387
+ ahead: divergence?.ahead ?? null,
1388
+ behind: divergence?.behind ?? null,
1389
+ warning: fetch2.warning
1390
+ };
1391
+ }
1312
1392
  function pickModuleCycleBranch(candidates, cycleNumber, module, memberSlug) {
1313
1393
  if (candidates.length === 0) return void 0;
1314
1394
  const expected = cycleBranchName(cycleNumber, module, memberSlug);
@@ -1413,7 +1493,7 @@ function getRemoteBranchFiles(cwd, branch, baseBranch) {
1413
1493
  return [];
1414
1494
  }
1415
1495
  }
1416
- var AUTO_WRITTEN_PATHS, GIT_NETWORK_TIMEOUT_MS, MERGE_RETRY_DELAY_MS, MERGE_MAX_RETRIES;
1496
+ var AUTO_WRITTEN_PATHS, GIT_NETWORK_TIMEOUT_MS, MERGE_RETRY_DELAY_MS, MERGE_MAX_RETRIES, GIT_FETCH_TIMEOUT_MS;
1417
1497
  var init_git = __esm({
1418
1498
  "src/lib/git.ts"() {
1419
1499
  "use strict";
@@ -1421,6 +1501,7 @@ var init_git = __esm({
1421
1501
  GIT_NETWORK_TIMEOUT_MS = 6e4;
1422
1502
  MERGE_RETRY_DELAY_MS = 2e3;
1423
1503
  MERGE_MAX_RETRIES = 3;
1504
+ GIT_FETCH_TIMEOUT_MS = 5e3;
1424
1505
  }
1425
1506
  });
1426
1507
 
@@ -1471,6 +1552,16 @@ __export(proxy_adapter_exports, {
1471
1552
  createProxyAdapter: () => createProxyAdapter,
1472
1553
  wrapWithForwarding: () => wrapWithForwarding
1473
1554
  });
1555
+ function formatRateLimitError(route, response, errorBody, displayMessage = errorBody) {
1556
+ let reason;
1557
+ try {
1558
+ const parsed = JSON.parse(errorBody);
1559
+ if (typeof parsed.reason === "string") reason = parsed.reason;
1560
+ } catch {
1561
+ }
1562
+ const retryAfter = response.headers.get("Retry-After");
1563
+ return `Proxy rate limit on ${route}${reason ? ` (${reason})` : ""}${retryAfter ? `; retry after ${retryAfter}s` : ""}: ${displayMessage}`;
1564
+ }
1474
1565
  function snakeToCamel(str) {
1475
1566
  return str.replace(/_([a-z0-9])/g, (_, c) => c.toUpperCase());
1476
1567
  }
@@ -1512,7 +1603,7 @@ function wrapWithForwarding(instance) {
1512
1603
  return new Proxy(instance, {
1513
1604
  get(target, prop, receiver) {
1514
1605
  const existing = Reflect.get(target, prop, receiver);
1515
- if (existing !== void 0) return existing;
1606
+ if (Reflect.has(target, prop)) return existing;
1516
1607
  if (typeof prop !== "string" || prop === "then" || prop.startsWith("_") || NO_FORWARD.has(prop)) {
1517
1608
  return existing;
1518
1609
  }
@@ -1644,8 +1735,14 @@ var init_proxy_adapter = __esm({
1644
1735
  "linkPhasesToStage",
1645
1736
  "listContributors",
1646
1737
  "listConventions",
1738
+ "listDecisionPositions",
1739
+ "upsertDecisionPosition",
1740
+ "proposeDecision",
1741
+ "resolveDecisionState",
1742
+ "getUserEmailsByIds",
1647
1743
  "listMyBugReports",
1648
1744
  "listOwnerActionsForBlockerScan",
1745
+ "listOwnerActionsForOwner",
1649
1746
  "logEntityReferences",
1650
1747
  "markAgendaTopicsAddressed",
1651
1748
  "markCycleLearningResolved",
@@ -1709,6 +1806,12 @@ var init_proxy_adapter = __esm({
1709
1806
  // (1) local-only
1710
1807
  "close",
1711
1808
  "initRls",
1809
+ // task-3667: the exit-criteria evaluators probe adapter.sql to pick their
1810
+ // transport branch (direct SQL vs adapter reads). Forwarding `sql` to the
1811
+ // edge 403'd INSIDE sqlOf — every SQL-backed evaluator then threw
1812
+ // 'evaluator failed' instead of engaging its hosted branch or degrading to
1813
+ // an honest unevaluated reason. `sql` is local-only by definition.
1814
+ "sql",
1712
1815
  // task-3207 (C357): commitBuildComplete, commitReviewSubmit, commitRelease have no
1713
1816
  // edge case handler and no ALLOWED_METHODS entry — forwarding them 403s at the edge
1714
1817
  // with no try/catch at the call site, crashing build_execute/review_submit/release
@@ -1795,6 +1898,15 @@ var init_proxy_adapter = __esm({
1795
1898
  this.projectId = config.projectId ?? "";
1796
1899
  this.onAuthRejected = config.onAuthRejected;
1797
1900
  }
1901
+ /**
1902
+ * Headers shared by every authenticated proxy route.
1903
+ */
1904
+ authenticatedHeaders() {
1905
+ return {
1906
+ "Content-Type": "application/json",
1907
+ "Authorization": `Bearer ${this.apiKey}`
1908
+ };
1909
+ }
1798
1910
  /**
1799
1911
  * Does the hosted path genuinely serve `name`? (task-3022, C362)
1800
1912
  *
@@ -1833,10 +1945,7 @@ var init_proxy_adapter = __esm({
1833
1945
  try {
1834
1946
  const response = await fetch(`${this.endpoint}/project-list`, {
1835
1947
  method: "POST",
1836
- headers: {
1837
- "Content-Type": "application/json",
1838
- "Authorization": `Bearer ${this.apiKey}`
1839
- },
1948
+ headers: this.authenticatedHeaders(),
1840
1949
  body: "{}",
1841
1950
  signal: AbortSignal.timeout(5e3)
1842
1951
  });
@@ -1875,10 +1984,7 @@ var init_proxy_adapter = __esm({
1875
1984
  const url = `${this.endpoint}/ensure-project`;
1876
1985
  const response = await fetch(url, {
1877
1986
  method: "POST",
1878
- headers: {
1879
- "Content-Type": "application/json",
1880
- "Authorization": `Bearer ${this.apiKey}`
1881
- },
1987
+ headers: this.authenticatedHeaders(),
1882
1988
  body: JSON.stringify({
1883
1989
  ...projectName ? { projectName } : {},
1884
1990
  ...repoUrl ? { repoUrl } : {},
@@ -1898,6 +2004,9 @@ var init_proxy_adapter = __esm({
1898
2004
  } catch {
1899
2005
  message = errorBody;
1900
2006
  }
2007
+ if (response.status === 429) {
2008
+ throw new Error(formatRateLimitError("ensureProject", response, errorBody, message));
2009
+ }
1901
2010
  throw new Error(`Auto-provision failed (${response.status}): ${message}`);
1902
2011
  }
1903
2012
  const body = await response.json();
@@ -1918,10 +2027,7 @@ var init_proxy_adapter = __esm({
1918
2027
  try {
1919
2028
  response = await fetch(url, {
1920
2029
  method: "POST",
1921
- headers: {
1922
- "Content-Type": "application/json",
1923
- "Authorization": `Bearer ${this.apiKey}`
1924
- },
2030
+ headers: this.authenticatedHeaders(),
1925
2031
  body: JSON.stringify({
1926
2032
  projectId: this.projectId,
1927
2033
  method,
@@ -1964,6 +2070,9 @@ Check PAPI_PROJECT_ID in your .mcp.json config. Find your project ID in the PAPI
1964
2070
  (${response.status} on ${method}: ${message})`
1965
2071
  );
1966
2072
  }
2073
+ if (response.status === 429) {
2074
+ throw new Error(formatRateLimitError(method, response, errorBody, message));
2075
+ }
1967
2076
  throw new Error(`Proxy error (${response.status}) on ${method}: ${message}`);
1968
2077
  }
1969
2078
  const body = await response.json();
@@ -2025,10 +2134,7 @@ Check PAPI_PROJECT_ID in your .mcp.json config. Find your project ID in the PAPI
2025
2134
  try {
2026
2135
  const response = await fetch(`${this.endpoint}/invoke`, {
2027
2136
  method: "POST",
2028
- headers: {
2029
- "Content-Type": "application/json",
2030
- "Authorization": `Bearer ${this.apiKey}`
2031
- },
2137
+ headers: this.authenticatedHeaders(),
2032
2138
  body: JSON.stringify({ projectId, method: "projectExists", args: [] }),
2033
2139
  signal: AbortSignal.timeout(5e3)
2034
2140
  });
@@ -2059,8 +2165,8 @@ Check PAPI_PROJECT_ID in your .mcp.json config. Find your project ID in the PAPI
2059
2165
  writeCycleLogEntry(entry) {
2060
2166
  return this.invoke("writeCycleLogEntry", [entry]);
2061
2167
  }
2062
- updateActiveDecision(id, body, cycleNumber) {
2063
- return this.invoke("updateActiveDecision", [id, body, cycleNumber]);
2168
+ updateActiveDecision(id, body, cycleNumber, action) {
2169
+ return this.invoke("updateActiveDecision", [id, body, cycleNumber, action]);
2064
2170
  }
2065
2171
  upsertActiveDecision(id, body, title, confidence, cycleNumber) {
2066
2172
  return this.invoke("upsertActiveDecision", [id, body, title, confidence, cycleNumber]);
@@ -2187,7 +2293,7 @@ Check PAPI_PROJECT_ID in your .mcp.json config. Find your project ID in the PAPI
2187
2293
  * task-2288 (C308): emit a telemetry event via the proxy's /telemetry endpoint
2188
2294
  * using THIS adapter's per-request bearer (this.apiKey) — the same credential
2189
2295
  * invoke() uses. This is the fix for the hosted-transport blackout: on the
2190
- * multi-tenant Railway pod there is no PAPI_DATA_API_KEY env var, so the
2296
+ * multi-tenant server has no PAPI_DATA_API_KEY env var, so the
2191
2297
  * env-var emitTelemetryEvent path (lib/telemetry.ts) dropped every event since
2192
2298
  * 2026-06-16. Fire-and-forget — never throws, never blocks the tool response.
2193
2299
  * Not routed through invoke() because /telemetry is a distinct endpoint and a
@@ -2203,10 +2309,7 @@ Check PAPI_PROJECT_ID in your .mcp.json config. Find your project ID in the PAPI
2203
2309
  if (event.projectId) payload["projectId"] = event.projectId;
2204
2310
  fetch(`${this.endpoint}/telemetry`, {
2205
2311
  method: "POST",
2206
- headers: {
2207
- "Content-Type": "application/json",
2208
- "Authorization": `Bearer ${this.apiKey}`
2209
- },
2312
+ headers: this.authenticatedHeaders(),
2210
2313
  body: JSON.stringify(payload),
2211
2314
  signal: AbortSignal.timeout(5e3)
2212
2315
  }).then((res) => {
@@ -2293,6 +2396,19 @@ Check PAPI_PROJECT_ID in your .mcp.json config. Find your project ID in the PAPI
2293
2396
  getLatestDecisionScores() {
2294
2397
  return this.invoke("getLatestDecisionScores");
2295
2398
  }
2399
+ // --- Decision lifecycle — propose / resolve / positions (task-3408, C366) ---
2400
+ proposeDecision(id, decisionType, proposedByUserId, cycleNumber) {
2401
+ return this.invoke("proposeDecision", [id, decisionType, proposedByUserId, cycleNumber]);
2402
+ }
2403
+ resolveDecisionState(id, state, actorUserId, cycleNumber) {
2404
+ return this.invoke("resolveDecisionState", [id, state, actorUserId, cycleNumber]);
2405
+ }
2406
+ listDecisionPositions(decisionId) {
2407
+ return this.invoke("listDecisionPositions", [decisionId]);
2408
+ }
2409
+ upsertDecisionPosition(decisionId, userId, status, comments) {
2410
+ return this.invoke("upsertDecisionPosition", [decisionId, userId, status, comments ?? null]);
2411
+ }
2296
2412
  logEntityReferences(refs) {
2297
2413
  return this.invoke("logEntityReferences", [refs]);
2298
2414
  }
@@ -2488,7 +2604,18 @@ Check PAPI_PROJECT_ID in your .mcp.json config. Find your project ID in the PAPI
2488
2604
  }
2489
2605
  // --- Project metadata (path-identity guardrail) ---
2490
2606
  async getProjectInfo() {
2491
- return this.invoke("getProjectInfo", []);
2607
+ const raw = await this.invoke("getProjectInfo", []);
2608
+ if (!raw) return null;
2609
+ return {
2610
+ name: raw.name,
2611
+ slug: raw.slug,
2612
+ papi_dir: raw.papi_dir ?? raw.papiDir ?? null,
2613
+ repo_url: raw.repo_url ?? raw.repoUrl ?? null,
2614
+ capabilities: raw.capabilities ?? {},
2615
+ discord_channel_id: raw.discord_channel_id ?? raw.discordChannelId ?? null,
2616
+ decision_resolution_mode: raw.decision_resolution_mode ?? raw.decisionResolutionMode ?? "owner",
2617
+ decision_type_resolvers: raw.decision_type_resolvers ?? raw.decisionTypeResolvers ?? {}
2618
+ };
2492
2619
  }
2493
2620
  /**
2494
2621
  * task-2052 (C288): owner-gate identity for the proxy transport. Both sides
@@ -2528,10 +2655,7 @@ Check PAPI_PROJECT_ID in your .mcp.json config. Find your project ID in the PAPI
2528
2655
  async postRoute(route, payload) {
2529
2656
  const response = await fetch(`${this.endpoint}/${route}`, {
2530
2657
  method: "POST",
2531
- headers: {
2532
- "Content-Type": "application/json",
2533
- "Authorization": `Bearer ${this.apiKey}`
2534
- },
2658
+ headers: this.authenticatedHeaders(),
2535
2659
  body: JSON.stringify(payload),
2536
2660
  signal: AbortSignal.timeout(15e3)
2537
2661
  });
@@ -2543,6 +2667,9 @@ Check PAPI_PROJECT_ID in your .mcp.json config. Find your project ID in the PAPI
2543
2667
  } catch {
2544
2668
  message = errorBody;
2545
2669
  }
2670
+ if (response.status === 429) {
2671
+ throw new Error(formatRateLimitError(route, response, errorBody, message));
2672
+ }
2546
2673
  if (response.status === 401) this.onAuthRejected?.();
2547
2674
  throw new Error(`Proxy error (${response.status}) on ${route}: ${message}`);
2548
2675
  }
@@ -2692,7 +2819,6 @@ function serverDeploymentProfile() {
2692
2819
  dataEndpoint: process.env["PAPI_DATA_ENDPOINT"],
2693
2820
  hostedSupabaseUrl: process.env["PAPI_HOSTED_SUPABASE_URL"],
2694
2821
  vercelEnabled: process.env["PAPI_ENABLE_VERCEL"],
2695
- railwayEnabled: process.env["PAPI_ENABLE_RAILWAY"],
2696
2822
  managedSupabaseEnabled: process.env["PAPI_ENABLE_MANAGED_SUPABASE"]
2697
2823
  });
2698
2824
  }
@@ -2863,16 +2989,21 @@ Fix the PAPI_PROJECT_ID in your .mcp.json (or unset it and let PAPI resolve the
2863
2989
  });
2864
2990
  }
2865
2991
  } else {
2866
- if (!existing.root_commit_hash && rootHash) {
2992
+ if (!existing.root_commit_hash && rootHash || !existing.repo_url && originUrl) {
2867
2993
  try {
2868
- await pgAdapter.updateProject(projectId, {
2869
- root_commit_hash: rootHash,
2870
- repo_url: originUrl ?? void 0,
2871
- resolution_method: "root_hash"
2872
- });
2994
+ const update = {};
2995
+ if (!existing.root_commit_hash && rootHash) {
2996
+ update.root_commit_hash = rootHash;
2997
+ update.resolution_method = "root_hash";
2998
+ }
2999
+ if (!existing.repo_url && originUrl) {
3000
+ update.repo_url = originUrl;
3001
+ if (!update.resolution_method && !existing.resolution_method) update.resolution_method = "repo_url";
3002
+ }
3003
+ await pgAdapter.updateProject(projectId, update);
2873
3004
  } catch (err) {
2874
3005
  console.error(
2875
- `[papi] \u26A0 Failed to backfill root_commit_hash: ${err instanceof Error ? err.message : String(err)}`
3006
+ `[papi] \u26A0 Failed to backfill project resolution signals: ${err instanceof Error ? err.message : String(err)}`
2876
3007
  );
2877
3008
  }
2878
3009
  }
@@ -2913,7 +3044,7 @@ Fix the PAPI_PROJECT_ID in your .mcp.json (or unset it and let PAPI resolve the
2913
3044
  } catch {
2914
3045
  }
2915
3046
  }
2916
- const adapter = new PgPapiAdapter(config, projectId);
3047
+ const adapter = new PgPapiAdapter({ ...config, userId: resolveUserId }, projectId);
2917
3048
  try {
2918
3049
  await adapter.initRls();
2919
3050
  } catch {