@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.
- package/dist/backfill-cycle-metrics.js +149 -15
- package/dist/index.js +714 -184
- package/dist/prompts.js +20 -6
- 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, {
|
|
@@ -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
|
-
//
|
|
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
|
|
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
|
|
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
|
-
|
|
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,
|