@papi-ai/server 0.7.77 → 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.
- package/dist/backfill-cycle-metrics.js +85 -13
- package/dist/index.js +615 -182
- package/dist/prompts.js +15 -1
- package/package.json +1 -1
|
@@ -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("
|
|
816
|
-
const
|
|
817
|
-
if (
|
|
818
|
-
return {
|
|
819
|
-
|
|
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, {
|
|
@@ -1289,10 +1297,39 @@ var init_proxy_adapter = __esm({
|
|
|
1289
1297
|
endpoint;
|
|
1290
1298
|
apiKey;
|
|
1291
1299
|
projectId;
|
|
1300
|
+
onAuthRejected;
|
|
1292
1301
|
constructor(config) {
|
|
1293
1302
|
this.endpoint = config.endpoint.replace(/\/$/, "");
|
|
1294
1303
|
this.apiKey = config.apiKey;
|
|
1295
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
|
+
}
|
|
1296
1333
|
}
|
|
1297
1334
|
/** Resolved project ID — available after ensureProject() completes. */
|
|
1298
1335
|
getProjectId() {
|
|
@@ -1309,7 +1346,8 @@ var init_proxy_adapter = __esm({
|
|
|
1309
1346
|
return wrapWithForwarding(new _ProxyPapiAdapter({
|
|
1310
1347
|
endpoint: this.endpoint,
|
|
1311
1348
|
apiKey: this.apiKey,
|
|
1312
|
-
projectId
|
|
1349
|
+
projectId,
|
|
1350
|
+
onAuthRejected: this.onAuthRejected
|
|
1313
1351
|
}));
|
|
1314
1352
|
}
|
|
1315
1353
|
/**
|
|
@@ -1396,6 +1434,7 @@ var init_proxy_adapter = __esm({
|
|
|
1396
1434
|
message = errorBody;
|
|
1397
1435
|
}
|
|
1398
1436
|
if (response.status === 401) {
|
|
1437
|
+
this.onAuthRejected?.();
|
|
1399
1438
|
throw new Error(
|
|
1400
1439
|
`Auth: Invalid API key \u2014 PAPI_DATA_API_KEY was rejected by the proxy.
|
|
1401
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.
|
|
@@ -1974,6 +2013,7 @@ Check PAPI_PROJECT_ID in your .mcp.json config. Find your project ID in the PAPI
|
|
|
1974
2013
|
} catch {
|
|
1975
2014
|
message = errorBody;
|
|
1976
2015
|
}
|
|
2016
|
+
if (response.status === 401) this.onAuthRejected?.();
|
|
1977
2017
|
throw new Error(`Proxy error (${response.status}) on ${route}: ${message}`);
|
|
1978
2018
|
}
|
|
1979
2019
|
const body = await response.json();
|
|
@@ -2028,7 +2068,7 @@ import { pathToFileURL } from "url";
|
|
|
2028
2068
|
import path2 from "path";
|
|
2029
2069
|
import { execSync } from "child_process";
|
|
2030
2070
|
|
|
2031
|
-
//
|
|
2071
|
+
// ../../../../../Users/cathaloullivan/Ai-App-Projects/PapiUI/packages/adapter-md/dist/index.js
|
|
2032
2072
|
import { readFile, writeFile, access } from "fs/promises";
|
|
2033
2073
|
import { randomUUID as randomUUID6 } from "crypto";
|
|
2034
2074
|
import { join } from "path";
|
|
@@ -2290,9 +2330,12 @@ function parsePlanningLog(content, activeDecisionsContent, cycleLogContent) {
|
|
|
2290
2330
|
var VALID_EFFORT_SIZES = /* @__PURE__ */ new Set(["XS", "S", "M", "L", "XL"]);
|
|
2291
2331
|
var SECTION_HEADERS = [
|
|
2292
2332
|
"SCOPE (DO THIS)",
|
|
2333
|
+
"WHY NOT SIMPLER",
|
|
2293
2334
|
"SCOPE BOUNDARY (DO NOT DO THIS)",
|
|
2294
2335
|
"ACCEPTANCE CRITERIA",
|
|
2336
|
+
"PRE-MORTEM",
|
|
2295
2337
|
"SECURITY CONSIDERATIONS",
|
|
2338
|
+
"DEPLOY VERIFICATION",
|
|
2296
2339
|
"PRE-BUILD VERIFICATION",
|
|
2297
2340
|
"FILES LIKELY TOUCHED",
|
|
2298
2341
|
"EFFORT"
|
|
@@ -2331,7 +2374,7 @@ function parseBulletsOnly(text) {
|
|
|
2331
2374
|
return text.split("\n").filter((l) => /^\s*-\s/.test(l)).map((l) => l.replace(/^\s*-\s*/, "").trim()).filter((l) => l.length > 0);
|
|
2332
2375
|
}
|
|
2333
2376
|
function parseChecklist(text) {
|
|
2334
|
-
return text.split("\n").map((l) => l.replace(/^\s
|
|
2377
|
+
return text.split("\n").map((l) => l.replace(/^\s*(?:[-*+]\s*)?(?:\[[ xX]\]\s*)?/, "").trim()).filter((l) => l.length > 0);
|
|
2335
2378
|
}
|
|
2336
2379
|
function parseBuildHandoff(markdown) {
|
|
2337
2380
|
if (typeof markdown !== "string" || !markdown.trim()) return null;
|
|
@@ -2959,13 +3002,21 @@ var EFFORT_SCALE = {
|
|
|
2959
3002
|
XL: 5
|
|
2960
3003
|
};
|
|
2961
3004
|
function effortOrdinal(effort) {
|
|
3005
|
+
if (typeof effort !== "string") return void 0;
|
|
2962
3006
|
const normalized = effort.trim().toUpperCase();
|
|
2963
3007
|
return EFFORT_SCALE[normalized];
|
|
2964
3008
|
}
|
|
3009
|
+
function isUnparsedEffort(effort) {
|
|
3010
|
+
if (typeof effort !== "string" || effort.trim().length === 0) return false;
|
|
3011
|
+
return effortOrdinal(effort) === void 0;
|
|
3012
|
+
}
|
|
2965
3013
|
function calculateCycleMetrics(reports, currentCycle, window = 5) {
|
|
2966
3014
|
const recentReports = reports.filter(
|
|
2967
3015
|
(r) => r.cycle > currentCycle - window && r.cycle <= currentCycle
|
|
2968
3016
|
);
|
|
3017
|
+
const unparsedEffortCount = recentReports.filter(
|
|
3018
|
+
(r) => isUnparsedEffort(r.actualEffort) || isUnparsedEffort(r.estimatedEffort)
|
|
3019
|
+
).length;
|
|
2969
3020
|
const perCycle = /* @__PURE__ */ new Map();
|
|
2970
3021
|
for (const r of recentReports) {
|
|
2971
3022
|
const group = perCycle.get(r.cycle) ?? [];
|
|
@@ -3002,7 +3053,7 @@ function calculateCycleMetrics(reports, currentCycle, window = 5) {
|
|
|
3002
3053
|
effortPoints: reps.reduce((s, r) => s + (effortOrdinal(r.actualEffort) ?? 0), 0)
|
|
3003
3054
|
});
|
|
3004
3055
|
}
|
|
3005
|
-
return { accuracy, velocity };
|
|
3056
|
+
return { accuracy, velocity, unparsedEffortCount };
|
|
3006
3057
|
}
|
|
3007
3058
|
function serializeAccuracyRow(a) {
|
|
3008
3059
|
return `| ${a.cycle} | ${a.reports} | ${a.matchRate}% | ${a.mae} | ${a.bias >= 0 ? "+" : ""}${a.bias} |`;
|
|
@@ -4428,6 +4479,7 @@ async function createAdapter(optionsOrType, maybePapiDir) {
|
|
|
4428
4479
|
case "pg": {
|
|
4429
4480
|
const { PgAdapter, PgPapiAdapter, configFromEnv } = await import("@papi-ai/adapter-pg");
|
|
4430
4481
|
let projectId = process.env["PAPI_PROJECT_ID"];
|
|
4482
|
+
const projectIdWasPreSupplied = Boolean(projectId);
|
|
4431
4483
|
const projectRoot = options.projectRoot ?? process.env["PAPI_PROJECT_DIR"] ?? process.cwd();
|
|
4432
4484
|
let rootHash = null;
|
|
4433
4485
|
let originUrl = null;
|
|
@@ -4491,6 +4543,26 @@ async function createAdapter(optionsOrType, maybePapiDir) {
|
|
|
4491
4543
|
}
|
|
4492
4544
|
const config = papiEndpoint ? { connectionString: papiEndpoint } : configFromEnv();
|
|
4493
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
|
+
}
|
|
4494
4566
|
const { ensureSchema } = await import("@papi-ai/adapter-pg");
|
|
4495
4567
|
try {
|
|
4496
4568
|
await ensureSchema(config);
|