@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
package/dist/index.js
CHANGED
|
@@ -561,11 +561,13 @@ function ensureTagAtHead(cwd, tag, message) {
|
|
|
561
561
|
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.`
|
|
562
562
|
};
|
|
563
563
|
}
|
|
564
|
-
function getLatestTag(cwd) {
|
|
564
|
+
function getLatestTag(cwd, timeoutMs) {
|
|
565
565
|
try {
|
|
566
566
|
return execFileSync("git", ["describe", "--tags", "--abbrev=0"], {
|
|
567
567
|
cwd,
|
|
568
|
-
encoding: "utf-8"
|
|
568
|
+
encoding: "utf-8",
|
|
569
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
570
|
+
...timeoutMs != null ? { timeout: timeoutMs } : {}
|
|
569
571
|
}).trim() || null;
|
|
570
572
|
} catch {
|
|
571
573
|
return null;
|
|
@@ -807,20 +809,26 @@ function findTaskCommitsOnBase(cwd, preferredBase, displayIds) {
|
|
|
807
809
|
try {
|
|
808
810
|
raw = execFileSync(
|
|
809
811
|
"git",
|
|
810
|
-
["log", base, "--format=%h%x01%s", "-n", "1000"],
|
|
812
|
+
["log", base, "--format=%h%x01%s%x01%b%x02", "-n", "1000"],
|
|
811
813
|
{ cwd, encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"] }
|
|
812
814
|
);
|
|
813
815
|
} catch {
|
|
814
816
|
return out;
|
|
815
817
|
}
|
|
816
|
-
const commits = raw.split("
|
|
817
|
-
const
|
|
818
|
-
if (
|
|
819
|
-
return {
|
|
820
|
-
|
|
818
|
+
const commits = raw.split("").map((record) => {
|
|
819
|
+
const parts = record.split("");
|
|
820
|
+
if (parts.length < 2) return null;
|
|
821
|
+
return {
|
|
822
|
+
hash: parts[0].trim(),
|
|
823
|
+
subject: parts[1].trim(),
|
|
824
|
+
body: (parts[2] ?? "").trim()
|
|
825
|
+
};
|
|
826
|
+
}).filter(
|
|
827
|
+
(c) => c !== null && c.hash !== ""
|
|
828
|
+
);
|
|
821
829
|
for (const displayId of displayIds) {
|
|
822
830
|
const re = new RegExp(`(^|[^\\w-])${escapeRegexLiteral(displayId)}([^\\w-]|$)`);
|
|
823
|
-
const hit = commits.find((c) => re.test(c.subject));
|
|
831
|
+
const hit = commits.find((c) => re.test(c.subject)) ?? commits.find((c) => re.test(c.body));
|
|
824
832
|
if (!hit) continue;
|
|
825
833
|
const prMatch = hit.subject.match(/#(\d+)/);
|
|
826
834
|
out.set(displayId, {
|
|
@@ -1406,10 +1414,39 @@ var init_proxy_adapter = __esm({
|
|
|
1406
1414
|
endpoint;
|
|
1407
1415
|
apiKey;
|
|
1408
1416
|
projectId;
|
|
1417
|
+
onAuthRejected;
|
|
1409
1418
|
constructor(config2) {
|
|
1410
1419
|
this.endpoint = config2.endpoint.replace(/\/$/, "");
|
|
1411
1420
|
this.apiKey = config2.apiKey;
|
|
1412
1421
|
this.projectId = config2.projectId ?? "";
|
|
1422
|
+
this.onAuthRejected = config2.onAuthRejected;
|
|
1423
|
+
}
|
|
1424
|
+
/**
|
|
1425
|
+
* task-1773: bearer-only auth probe. Hits the USER-scoped `project-list` route
|
|
1426
|
+
* (no projectId needed), so it answers exactly one question: does the proxy
|
|
1427
|
+
* still accept this bearer?
|
|
1428
|
+
*
|
|
1429
|
+
* Returns the HTTP status, or 0 when the call could not be made at all
|
|
1430
|
+
* (network error / timeout). Callers MUST treat 0 — and any status that is
|
|
1431
|
+
* neither 2xx nor 401 — as "no signal", never as a rejection: a proxy outage
|
|
1432
|
+
* must not masquerade as a revoked token and force every user to re-auth.
|
|
1433
|
+
*/
|
|
1434
|
+
async probeBearerStatus() {
|
|
1435
|
+
try {
|
|
1436
|
+
const response = await fetch(`${this.endpoint}/project-list`, {
|
|
1437
|
+
method: "POST",
|
|
1438
|
+
headers: {
|
|
1439
|
+
"Content-Type": "application/json",
|
|
1440
|
+
"Authorization": `Bearer ${this.apiKey}`
|
|
1441
|
+
},
|
|
1442
|
+
body: "{}",
|
|
1443
|
+
signal: AbortSignal.timeout(5e3)
|
|
1444
|
+
});
|
|
1445
|
+
if (response.status === 401) this.onAuthRejected?.();
|
|
1446
|
+
return response.status;
|
|
1447
|
+
} catch {
|
|
1448
|
+
return 0;
|
|
1449
|
+
}
|
|
1413
1450
|
}
|
|
1414
1451
|
/** Resolved project ID — available after ensureProject() completes. */
|
|
1415
1452
|
getProjectId() {
|
|
@@ -1426,7 +1463,8 @@ var init_proxy_adapter = __esm({
|
|
|
1426
1463
|
return wrapWithForwarding(new _ProxyPapiAdapter({
|
|
1427
1464
|
endpoint: this.endpoint,
|
|
1428
1465
|
apiKey: this.apiKey,
|
|
1429
|
-
projectId
|
|
1466
|
+
projectId,
|
|
1467
|
+
onAuthRejected: this.onAuthRejected
|
|
1430
1468
|
}));
|
|
1431
1469
|
}
|
|
1432
1470
|
/**
|
|
@@ -1513,6 +1551,7 @@ var init_proxy_adapter = __esm({
|
|
|
1513
1551
|
message = errorBody;
|
|
1514
1552
|
}
|
|
1515
1553
|
if (response.status === 401) {
|
|
1554
|
+
this.onAuthRejected?.();
|
|
1516
1555
|
throw new Error(
|
|
1517
1556
|
`Auth: Invalid API key \u2014 PAPI_DATA_API_KEY was rejected by the proxy.
|
|
1518
1557
|
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.
|
|
@@ -2091,6 +2130,7 @@ Check PAPI_PROJECT_ID in your .mcp.json config. Find your project ID in the PAPI
|
|
|
2091
2130
|
} catch {
|
|
2092
2131
|
message = errorBody;
|
|
2093
2132
|
}
|
|
2133
|
+
if (response.status === 401) this.onAuthRejected?.();
|
|
2094
2134
|
throw new Error(`Proxy error (${response.status}) on ${route}: ${message}`);
|
|
2095
2135
|
}
|
|
2096
2136
|
const body = await response.json();
|
|
@@ -2208,7 +2248,7 @@ var init_reap_orphans = __esm({
|
|
|
2208
2248
|
}
|
|
2209
2249
|
});
|
|
2210
2250
|
|
|
2211
|
-
//
|
|
2251
|
+
// ../../../../../Users/cathaloullivan/Ai-App-Projects/PapiUI/node_modules/postgres/src/query.js
|
|
2212
2252
|
function cachedError(xs) {
|
|
2213
2253
|
if (originCache.has(xs))
|
|
2214
2254
|
return originCache.get(xs);
|
|
@@ -2220,7 +2260,7 @@ function cachedError(xs) {
|
|
|
2220
2260
|
}
|
|
2221
2261
|
var originCache, originStackCache, originError, CLOSE, Query;
|
|
2222
2262
|
var init_query = __esm({
|
|
2223
|
-
"
|
|
2263
|
+
"../../../../../Users/cathaloullivan/Ai-App-Projects/PapiUI/node_modules/postgres/src/query.js"() {
|
|
2224
2264
|
"use strict";
|
|
2225
2265
|
originCache = /* @__PURE__ */ new Map();
|
|
2226
2266
|
originStackCache = /* @__PURE__ */ new Map();
|
|
@@ -2351,7 +2391,7 @@ var init_query = __esm({
|
|
|
2351
2391
|
}
|
|
2352
2392
|
});
|
|
2353
2393
|
|
|
2354
|
-
//
|
|
2394
|
+
// ../../../../../Users/cathaloullivan/Ai-App-Projects/PapiUI/node_modules/postgres/src/errors.js
|
|
2355
2395
|
function connection(x, options, socket) {
|
|
2356
2396
|
const { host, port } = socket || options;
|
|
2357
2397
|
const error = Object.assign(
|
|
@@ -2389,7 +2429,7 @@ function notSupported(x) {
|
|
|
2389
2429
|
}
|
|
2390
2430
|
var PostgresError, Errors;
|
|
2391
2431
|
var init_errors = __esm({
|
|
2392
|
-
"
|
|
2432
|
+
"../../../../../Users/cathaloullivan/Ai-App-Projects/PapiUI/node_modules/postgres/src/errors.js"() {
|
|
2393
2433
|
"use strict";
|
|
2394
2434
|
PostgresError = class extends Error {
|
|
2395
2435
|
constructor(x) {
|
|
@@ -2407,7 +2447,7 @@ var init_errors = __esm({
|
|
|
2407
2447
|
}
|
|
2408
2448
|
});
|
|
2409
2449
|
|
|
2410
|
-
//
|
|
2450
|
+
// ../../../../../Users/cathaloullivan/Ai-App-Projects/PapiUI/node_modules/postgres/src/types.js
|
|
2411
2451
|
function handleValue(x, parameters, types2, options) {
|
|
2412
2452
|
let value = x instanceof Parameter ? x.value : x;
|
|
2413
2453
|
if (value === void 0) {
|
|
@@ -2522,7 +2562,7 @@ function createJsonTransform(fn) {
|
|
|
2522
2562
|
}
|
|
2523
2563
|
var types, NotTagged, Identifier, Parameter, Builder, defaultHandlers, builders, serializers, parsers, mergeUserTypes, escapeIdentifier, inferType, escapeBackslash, escapeQuote, arraySerializer, arrayParserState, arrayParser, toCamel, toPascal, toKebab, fromCamel, fromPascal, fromKebab, camel, pascal, kebab;
|
|
2524
2564
|
var init_types = __esm({
|
|
2525
|
-
"
|
|
2565
|
+
"../../../../../Users/cathaloullivan/Ai-App-Projects/PapiUI/node_modules/postgres/src/types.js"() {
|
|
2526
2566
|
"use strict";
|
|
2527
2567
|
init_query();
|
|
2528
2568
|
init_errors();
|
|
@@ -2701,10 +2741,10 @@ var init_types = __esm({
|
|
|
2701
2741
|
}
|
|
2702
2742
|
});
|
|
2703
2743
|
|
|
2704
|
-
//
|
|
2744
|
+
// ../../../../../Users/cathaloullivan/Ai-App-Projects/PapiUI/node_modules/postgres/src/result.js
|
|
2705
2745
|
var Result;
|
|
2706
2746
|
var init_result = __esm({
|
|
2707
|
-
"
|
|
2747
|
+
"../../../../../Users/cathaloullivan/Ai-App-Projects/PapiUI/node_modules/postgres/src/result.js"() {
|
|
2708
2748
|
"use strict";
|
|
2709
2749
|
Result = class extends Array {
|
|
2710
2750
|
constructor() {
|
|
@@ -2724,7 +2764,7 @@ var init_result = __esm({
|
|
|
2724
2764
|
}
|
|
2725
2765
|
});
|
|
2726
2766
|
|
|
2727
|
-
//
|
|
2767
|
+
// ../../../../../Users/cathaloullivan/Ai-App-Projects/PapiUI/node_modules/postgres/src/queue.js
|
|
2728
2768
|
function Queue(initial = []) {
|
|
2729
2769
|
let xs = initial.slice();
|
|
2730
2770
|
let index = 0;
|
|
@@ -2751,13 +2791,13 @@ function Queue(initial = []) {
|
|
|
2751
2791
|
}
|
|
2752
2792
|
var queue_default;
|
|
2753
2793
|
var init_queue = __esm({
|
|
2754
|
-
"
|
|
2794
|
+
"../../../../../Users/cathaloullivan/Ai-App-Projects/PapiUI/node_modules/postgres/src/queue.js"() {
|
|
2755
2795
|
"use strict";
|
|
2756
2796
|
queue_default = Queue;
|
|
2757
2797
|
}
|
|
2758
2798
|
});
|
|
2759
2799
|
|
|
2760
|
-
//
|
|
2800
|
+
// ../../../../../Users/cathaloullivan/Ai-App-Projects/PapiUI/node_modules/postgres/src/bytes.js
|
|
2761
2801
|
function fit(x) {
|
|
2762
2802
|
if (buffer.length - b.i < x) {
|
|
2763
2803
|
const prev = buffer, length = prev.length;
|
|
@@ -2771,7 +2811,7 @@ function reset() {
|
|
|
2771
2811
|
}
|
|
2772
2812
|
var size, buffer, messages, b, bytes_default;
|
|
2773
2813
|
var init_bytes = __esm({
|
|
2774
|
-
"
|
|
2814
|
+
"../../../../../Users/cathaloullivan/Ai-App-Projects/PapiUI/node_modules/postgres/src/bytes.js"() {
|
|
2775
2815
|
"use strict";
|
|
2776
2816
|
size = 256;
|
|
2777
2817
|
buffer = Buffer.allocUnsafe(size);
|
|
@@ -2836,7 +2876,7 @@ var init_bytes = __esm({
|
|
|
2836
2876
|
}
|
|
2837
2877
|
});
|
|
2838
2878
|
|
|
2839
|
-
//
|
|
2879
|
+
// ../../../../../Users/cathaloullivan/Ai-App-Projects/PapiUI/node_modules/postgres/src/connection.js
|
|
2840
2880
|
import net from "net";
|
|
2841
2881
|
import tls from "tls";
|
|
2842
2882
|
import crypto2 from "crypto";
|
|
@@ -3630,7 +3670,7 @@ function timer(fn, seconds) {
|
|
|
3630
3670
|
}
|
|
3631
3671
|
var connection_default, uid, Sync, Flush, SSLRequest, ExecuteUnnamed, DescribeUnnamed, noop, retryRoutines, errorFields;
|
|
3632
3672
|
var init_connection = __esm({
|
|
3633
|
-
"
|
|
3673
|
+
"../../../../../Users/cathaloullivan/Ai-App-Projects/PapiUI/node_modules/postgres/src/connection.js"() {
|
|
3634
3674
|
"use strict";
|
|
3635
3675
|
init_types();
|
|
3636
3676
|
init_errors();
|
|
@@ -3693,7 +3733,7 @@ var init_connection = __esm({
|
|
|
3693
3733
|
}
|
|
3694
3734
|
});
|
|
3695
3735
|
|
|
3696
|
-
//
|
|
3736
|
+
// ../../../../../Users/cathaloullivan/Ai-App-Projects/PapiUI/node_modules/postgres/src/subscribe.js
|
|
3697
3737
|
function Subscribe(postgres2, options) {
|
|
3698
3738
|
const subscribers = /* @__PURE__ */ new Map(), slot = "postgresjs_" + Math.random().toString(36).slice(2), state = {};
|
|
3699
3739
|
let connection2, stream, ended = false;
|
|
@@ -3904,14 +3944,14 @@ function parseEvent(x) {
|
|
|
3904
3944
|
}
|
|
3905
3945
|
var noop2;
|
|
3906
3946
|
var init_subscribe = __esm({
|
|
3907
|
-
"
|
|
3947
|
+
"../../../../../Users/cathaloullivan/Ai-App-Projects/PapiUI/node_modules/postgres/src/subscribe.js"() {
|
|
3908
3948
|
"use strict";
|
|
3909
3949
|
noop2 = () => {
|
|
3910
3950
|
};
|
|
3911
3951
|
}
|
|
3912
3952
|
});
|
|
3913
3953
|
|
|
3914
|
-
//
|
|
3954
|
+
// ../../../../../Users/cathaloullivan/Ai-App-Projects/PapiUI/node_modules/postgres/src/large.js
|
|
3915
3955
|
import Stream2 from "stream";
|
|
3916
3956
|
function largeObject(sql, oid, mode = 131072 | 262144) {
|
|
3917
3957
|
return new Promise(async (resolve4, reject) => {
|
|
@@ -3977,12 +4017,12 @@ function largeObject(sql, oid, mode = 131072 | 262144) {
|
|
|
3977
4017
|
});
|
|
3978
4018
|
}
|
|
3979
4019
|
var init_large = __esm({
|
|
3980
|
-
"
|
|
4020
|
+
"../../../../../Users/cathaloullivan/Ai-App-Projects/PapiUI/node_modules/postgres/src/large.js"() {
|
|
3981
4021
|
"use strict";
|
|
3982
4022
|
}
|
|
3983
4023
|
});
|
|
3984
4024
|
|
|
3985
|
-
//
|
|
4025
|
+
// ../../../../../Users/cathaloullivan/Ai-App-Projects/PapiUI/node_modules/postgres/src/index.js
|
|
3986
4026
|
var src_exports = {};
|
|
3987
4027
|
__export(src_exports, {
|
|
3988
4028
|
default: () => src_default
|
|
@@ -4372,7 +4412,7 @@ function osUsername() {
|
|
|
4372
4412
|
}
|
|
4373
4413
|
var src_default;
|
|
4374
4414
|
var init_src = __esm({
|
|
4375
|
-
"
|
|
4415
|
+
"../../../../../Users/cathaloullivan/Ai-App-Projects/PapiUI/node_modules/postgres/src/index.js"() {
|
|
4376
4416
|
"use strict";
|
|
4377
4417
|
init_types();
|
|
4378
4418
|
init_connection();
|
|
@@ -5385,7 +5425,7 @@ Already have an account? Make sure PAPI_USER_ID is set in your .mcp.json env con
|
|
|
5385
5425
|
import path3 from "path";
|
|
5386
5426
|
import { execSync } from "child_process";
|
|
5387
5427
|
|
|
5388
|
-
//
|
|
5428
|
+
// ../../../../../Users/cathaloullivan/Ai-App-Projects/PapiUI/packages/adapter-md/dist/index.js
|
|
5389
5429
|
import { readFile, writeFile, access } from "fs/promises";
|
|
5390
5430
|
import { randomUUID as randomUUID6 } from "crypto";
|
|
5391
5431
|
import { join } from "path";
|
|
@@ -5647,9 +5687,12 @@ function parsePlanningLog(content, activeDecisionsContent, cycleLogContent) {
|
|
|
5647
5687
|
var VALID_EFFORT_SIZES = /* @__PURE__ */ new Set(["XS", "S", "M", "L", "XL"]);
|
|
5648
5688
|
var SECTION_HEADERS = [
|
|
5649
5689
|
"SCOPE (DO THIS)",
|
|
5690
|
+
"WHY NOT SIMPLER",
|
|
5650
5691
|
"SCOPE BOUNDARY (DO NOT DO THIS)",
|
|
5651
5692
|
"ACCEPTANCE CRITERIA",
|
|
5693
|
+
"PRE-MORTEM",
|
|
5652
5694
|
"SECURITY CONSIDERATIONS",
|
|
5695
|
+
"DEPLOY VERIFICATION",
|
|
5653
5696
|
"PRE-BUILD VERIFICATION",
|
|
5654
5697
|
"FILES LIKELY TOUCHED",
|
|
5655
5698
|
"EFFORT"
|
|
@@ -5688,7 +5731,7 @@ function parseBulletsOnly(text) {
|
|
|
5688
5731
|
return text.split("\n").filter((l) => /^\s*-\s/.test(l)).map((l) => l.replace(/^\s*-\s*/, "").trim()).filter((l) => l.length > 0);
|
|
5689
5732
|
}
|
|
5690
5733
|
function parseChecklist(text) {
|
|
5691
|
-
return text.split("\n").map((l) => l.replace(/^\s
|
|
5734
|
+
return text.split("\n").map((l) => l.replace(/^\s*(?:[-*+]\s*)?(?:\[[ xX]\]\s*)?/, "").trim()).filter((l) => l.length > 0);
|
|
5692
5735
|
}
|
|
5693
5736
|
function parseBuildHandoff(markdown) {
|
|
5694
5737
|
if (typeof markdown !== "string" || !markdown.trim()) return null;
|
|
@@ -6338,13 +6381,21 @@ var EFFORT_SCALE = {
|
|
|
6338
6381
|
XL: 5
|
|
6339
6382
|
};
|
|
6340
6383
|
function effortOrdinal(effort) {
|
|
6384
|
+
if (typeof effort !== "string") return void 0;
|
|
6341
6385
|
const normalized = effort.trim().toUpperCase();
|
|
6342
6386
|
return EFFORT_SCALE[normalized];
|
|
6343
6387
|
}
|
|
6388
|
+
function isUnparsedEffort(effort) {
|
|
6389
|
+
if (typeof effort !== "string" || effort.trim().length === 0) return false;
|
|
6390
|
+
return effortOrdinal(effort) === void 0;
|
|
6391
|
+
}
|
|
6344
6392
|
function calculateCycleMetrics(reports, currentCycle, window = 5) {
|
|
6345
6393
|
const recentReports = reports.filter(
|
|
6346
6394
|
(r) => r.cycle > currentCycle - window && r.cycle <= currentCycle
|
|
6347
6395
|
);
|
|
6396
|
+
const unparsedEffortCount = recentReports.filter(
|
|
6397
|
+
(r) => isUnparsedEffort(r.actualEffort) || isUnparsedEffort(r.estimatedEffort)
|
|
6398
|
+
).length;
|
|
6348
6399
|
const perCycle = /* @__PURE__ */ new Map();
|
|
6349
6400
|
for (const r of recentReports) {
|
|
6350
6401
|
const group = perCycle.get(r.cycle) ?? [];
|
|
@@ -6381,7 +6432,7 @@ function calculateCycleMetrics(reports, currentCycle, window = 5) {
|
|
|
6381
6432
|
effortPoints: reps.reduce((s, r) => s + (effortOrdinal(r.actualEffort) ?? 0), 0)
|
|
6382
6433
|
});
|
|
6383
6434
|
}
|
|
6384
|
-
return { accuracy, velocity };
|
|
6435
|
+
return { accuracy, velocity, unparsedEffortCount };
|
|
6385
6436
|
}
|
|
6386
6437
|
function serializeAccuracyRow(a) {
|
|
6387
6438
|
return `| ${a.cycle} | ${a.reports} | ${a.matchRate}% | ${a.mae} | ${a.bias >= 0 ? "+" : ""}${a.bias} |`;
|
|
@@ -8031,6 +8082,7 @@ async function createAdapter(optionsOrType, maybePapiDir) {
|
|
|
8031
8082
|
case "pg": {
|
|
8032
8083
|
const { PgAdapter, PgPapiAdapter, configFromEnv } = await import("@papi-ai/adapter-pg");
|
|
8033
8084
|
let projectId = process.env["PAPI_PROJECT_ID"];
|
|
8085
|
+
const projectIdWasPreSupplied = Boolean(projectId);
|
|
8034
8086
|
const projectRoot = options.projectRoot ?? process.env["PAPI_PROJECT_DIR"] ?? process.cwd();
|
|
8035
8087
|
let rootHash = null;
|
|
8036
8088
|
let originUrl = null;
|
|
@@ -8094,6 +8146,26 @@ async function createAdapter(optionsOrType, maybePapiDir) {
|
|
|
8094
8146
|
}
|
|
8095
8147
|
const config2 = papiEndpoint ? { connectionString: papiEndpoint } : configFromEnv();
|
|
8096
8148
|
validateDatabaseUrl(config2.connectionString);
|
|
8149
|
+
if (projectIdWasPreSupplied) {
|
|
8150
|
+
const ownershipProbe = new PgAdapter(config2);
|
|
8151
|
+
try {
|
|
8152
|
+
const owned = await ownershipProbe.findProjectById(projectId, resolveUserId);
|
|
8153
|
+
if (!owned) {
|
|
8154
|
+
throw new Error(
|
|
8155
|
+
`PAPI_PROJECT_ID ${projectId} does not belong to you.
|
|
8156
|
+
|
|
8157
|
+
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.
|
|
8158
|
+
|
|
8159
|
+
Fix the PAPI_PROJECT_ID in your .mcp.json (or unset it and let PAPI resolve the project from your git remote), then reconnect.`
|
|
8160
|
+
);
|
|
8161
|
+
}
|
|
8162
|
+
} finally {
|
|
8163
|
+
try {
|
|
8164
|
+
await ownershipProbe.close();
|
|
8165
|
+
} catch {
|
|
8166
|
+
}
|
|
8167
|
+
}
|
|
8168
|
+
}
|
|
8097
8169
|
const { ensureSchema } = await import("@papi-ai/adapter-pg");
|
|
8098
8170
|
try {
|
|
8099
8171
|
await ensureSchema(config2);
|
|
@@ -8461,15 +8533,23 @@ function formatBuildReports(reports, opts) {
|
|
|
8461
8533
|
_\u2026and ${reports.length - capped.length} older build report(s) omitted to bound context size._` : "";
|
|
8462
8534
|
return body + omitted;
|
|
8463
8535
|
}
|
|
8464
|
-
function
|
|
8465
|
-
const
|
|
8536
|
+
function extractTaskReferences(report) {
|
|
8537
|
+
const raw = [report.surprises, report.architectureNotes, report.deadEnds, report.discoveredIssues].filter((s) => typeof s === "string" && s.length > 0).join("\n");
|
|
8538
|
+
const prose = raw.replace(/```[\s\S]*?```|~~~[\s\S]*?~~~/g, " ").replace(/`[^`\n]*`/g, " ");
|
|
8466
8539
|
const own = report.taskId?.toLowerCase();
|
|
8467
|
-
const
|
|
8540
|
+
const resolves = /* @__PURE__ */ new Set();
|
|
8541
|
+
for (const m of prose.matchAll(/\bresolves:\s*((?:task-\d+\b\s*,?\s*)+)/gi)) {
|
|
8542
|
+
for (const idMatch of m[1].matchAll(/\btask-\d+\b/gi)) {
|
|
8543
|
+
const id = idMatch[0].toLowerCase();
|
|
8544
|
+
if (id !== own) resolves.add(id);
|
|
8545
|
+
}
|
|
8546
|
+
}
|
|
8547
|
+
const mentions = /* @__PURE__ */ new Set();
|
|
8468
8548
|
for (const m of prose.matchAll(/\btask-\d+\b/gi)) {
|
|
8469
8549
|
const id = m[0].toLowerCase();
|
|
8470
|
-
if (id !== own)
|
|
8550
|
+
if (id !== own && !resolves.has(id)) mentions.add(id);
|
|
8471
8551
|
}
|
|
8472
|
-
return [...
|
|
8552
|
+
return { resolves: [...resolves].sort(), mentions: [...mentions].sort() };
|
|
8473
8553
|
}
|
|
8474
8554
|
function formatRecentlyShippedCapabilities(reports) {
|
|
8475
8555
|
const completed = reports.filter((r) => r.completed === "Yes" || r.completed === "Partial");
|
|
@@ -8485,16 +8565,26 @@ function formatRecentlyShippedCapabilities(reports) {
|
|
|
8485
8565
|
}
|
|
8486
8566
|
return parts.join("\n");
|
|
8487
8567
|
});
|
|
8568
|
+
const resolvedBy = /* @__PURE__ */ new Map();
|
|
8488
8569
|
const namedBy = /* @__PURE__ */ new Map();
|
|
8489
8570
|
const completedIds = new Set(completed.map((r) => r.taskId?.toLowerCase()).filter(Boolean));
|
|
8571
|
+
const record = (into, ref, namer) => {
|
|
8572
|
+
const namers = into.get(ref) ?? [];
|
|
8573
|
+
namers.push(namer);
|
|
8574
|
+
into.set(ref, namers);
|
|
8575
|
+
};
|
|
8490
8576
|
for (const r of completed) {
|
|
8491
|
-
|
|
8577
|
+
const { resolves, mentions } = extractTaskReferences(r);
|
|
8578
|
+
for (const ref of resolves) {
|
|
8579
|
+
if (completedIds.has(ref)) continue;
|
|
8580
|
+
record(resolvedBy, ref, r.taskId);
|
|
8581
|
+
}
|
|
8582
|
+
for (const ref of mentions) {
|
|
8492
8583
|
if (completedIds.has(ref)) continue;
|
|
8493
|
-
|
|
8494
|
-
namers.push(r.taskId);
|
|
8495
|
-
namedBy.set(ref, namers);
|
|
8584
|
+
record(namedBy, ref, r.taskId);
|
|
8496
8585
|
}
|
|
8497
8586
|
}
|
|
8587
|
+
for (const ref of resolvedBy.keys()) namedBy.delete(ref);
|
|
8498
8588
|
const out = [
|
|
8499
8589
|
`${completed.length} task(s) completed in recent cycles:`,
|
|
8500
8590
|
"",
|
|
@@ -8502,17 +8592,31 @@ function formatRecentlyShippedCapabilities(reports) {
|
|
|
8502
8592
|
"",
|
|
8503
8593
|
"Cross-reference candidate tasks against this list. If >80% of a candidate task's scope appears here, recommend cancellation or scope reduction instead of scheduling."
|
|
8504
8594
|
];
|
|
8595
|
+
if (resolvedBy.size > 0) {
|
|
8596
|
+
out.push(
|
|
8597
|
+
"",
|
|
8598
|
+
"### \u26A0 Declared resolved by a shipped task \u2014 VERIFY, THEN CLOSE",
|
|
8599
|
+
"",
|
|
8600
|
+
"A shipped report explicitly claimed each of these with `resolves: <task-id>`, but",
|
|
8601
|
+
"the task is not itself marked complete. That is a declaration of intent, not proof:",
|
|
8602
|
+
"confirm against the live code, then close it with a boardCorrection rather than",
|
|
8603
|
+
"spending a cycle slot on work that already shipped.",
|
|
8604
|
+
"",
|
|
8605
|
+
...[...resolvedBy.entries()].sort(([a], [b2]) => a.localeCompare(b2)).map(([ref, namers]) => `- **${ref}** \u2014 declared resolved by ${namers.join(", ")}`)
|
|
8606
|
+
);
|
|
8607
|
+
}
|
|
8505
8608
|
if (namedBy.size > 0) {
|
|
8506
8609
|
out.push(
|
|
8507
8610
|
"",
|
|
8508
8611
|
"### \u26A0 Named by a shipped task \u2014 VERIFY BEFORE SCHEDULING",
|
|
8509
8612
|
"",
|
|
8510
|
-
"These task IDs
|
|
8511
|
-
"completed. A discovery is often fixed as a
|
|
8512
|
-
"never marked done, so it survives into this
|
|
8513
|
-
"true (C357 gave task-3043 a P1 slot this way \u2014
|
|
8514
|
-
"
|
|
8515
|
-
|
|
8613
|
+
"These task IDs merely APPEAR in the build reports above \u2014 no report claimed to have",
|
|
8614
|
+
"resolved them, and they are not themselves completed. A discovery is often fixed as a",
|
|
8615
|
+
"side effect of a sibling task's diff and never marked done, so it survives into this",
|
|
8616
|
+
"plan carrying notes that are no longer true (C357 gave task-3043 a P1 slot this way \u2014",
|
|
8617
|
+
"task-2998 had already fixed it). Weaker signal than the section above: a mention can",
|
|
8618
|
+
'equally mean "related to" or "still blocked by". Read the naming report and the live',
|
|
8619
|
+
"code BEFORE scheduling; never close on a mention alone.",
|
|
8516
8620
|
"",
|
|
8517
8621
|
...[...namedBy.entries()].sort(([a], [b2]) => a.localeCompare(b2)).map(([ref, namers]) => `- **${ref}** \u2014 named by ${namers.join(", ")}`)
|
|
8518
8622
|
);
|
|
@@ -9834,6 +9938,17 @@ function coerceToString(value) {
|
|
|
9834
9938
|
if (value === null || value === void 0) return "";
|
|
9835
9939
|
return JSON.stringify(value, null, 2);
|
|
9836
9940
|
}
|
|
9941
|
+
function coerceCarryForward(value) {
|
|
9942
|
+
if (value === null || value === void 0) return { value: null };
|
|
9943
|
+
if (typeof value === "string") {
|
|
9944
|
+
const trimmed = value.trim();
|
|
9945
|
+
return { value: trimmed.length > 0 ? trimmed : null };
|
|
9946
|
+
}
|
|
9947
|
+
const shape = Array.isArray(value) ? "array" : typeof value;
|
|
9948
|
+
const warning = `cycleLogCarryForward was ${shape}, not a string \u2014 DROPPED rather than persisted. Carry-forward is prose that orient parses for the WHAT SHIPS FOR USERS / RELEASE MECHANICS labels; a non-string value cannot carry them. Re-run plan apply with cycleLogCarryForward as a single string (or null) to record one for this cycle.`;
|
|
9949
|
+
console.error(`[plan] ${warning}`);
|
|
9950
|
+
return { value: null, warning };
|
|
9951
|
+
}
|
|
9837
9952
|
function coerceStructuredOutput(parsed) {
|
|
9838
9953
|
const cycleHandoffs = Array.isArray(parsed.cycleHandoffs) ? parsed.cycleHandoffs.map((h) => {
|
|
9839
9954
|
const { taskId: _t, buildHandoff: _b, ...rest } = h;
|
|
@@ -9873,10 +9988,12 @@ function coerceStructuredOutput(parsed) {
|
|
|
9873
9988
|
body: coerceToString(ad.body)
|
|
9874
9989
|
})) : [];
|
|
9875
9990
|
const cycleTaskIds = Array.isArray(parsed.cycleTaskIds) ? parsed.cycleTaskIds.map((id) => coerceToString(id)) : void 0;
|
|
9991
|
+
const carryForward = coerceCarryForward(parsed.cycleLogCarryForward);
|
|
9876
9992
|
return {
|
|
9877
9993
|
cycleLogTitle: coerceToString(parsed.cycleLogTitle),
|
|
9878
9994
|
cycleLogContent: coerceToString(parsed.cycleLogContent),
|
|
9879
|
-
cycleLogCarryForward:
|
|
9995
|
+
cycleLogCarryForward: carryForward.value,
|
|
9996
|
+
...carryForward.warning ? { coercionWarnings: [carryForward.warning] } : {},
|
|
9880
9997
|
cycleLogNotes: parsed.cycleLogNotes === null ? null : coerceToString(parsed.cycleLogNotes),
|
|
9881
9998
|
nextMode: "Full",
|
|
9882
9999
|
boardHealth: coerceToString(parsed.boardHealth),
|
|
@@ -13097,8 +13214,9 @@ async function processLlmOutput(adapter2, config2, rawOutput, mode, cycleNumber,
|
|
|
13097
13214
|
contextHashes,
|
|
13098
13215
|
{ confirmCancellations: planRunMeta?.confirmCancellations === true, ownerUserId: applyScope.callerUserId ?? void 0 }
|
|
13099
13216
|
);
|
|
13100
|
-
|
|
13101
|
-
|
|
13217
|
+
const allWarnings = [...data.coercionWarnings ?? [], ...wbWarnings];
|
|
13218
|
+
if (allWarnings.length > 0) {
|
|
13219
|
+
writeBackWarnings = allWarnings;
|
|
13102
13220
|
}
|
|
13103
13221
|
if (skipped.length > 0) {
|
|
13104
13222
|
skippedCancellations = skipped;
|
|
@@ -15780,6 +15898,65 @@ function extractDecisionEvidence(ad, eventType, warnings) {
|
|
|
15780
15898
|
}
|
|
15781
15899
|
return { evidenceRef, metricDelta };
|
|
15782
15900
|
}
|
|
15901
|
+
function asDecisionBatchApplier(adapter2) {
|
|
15902
|
+
const candidate = adapter2;
|
|
15903
|
+
return typeof candidate.applyActiveDecisionUpdates === "function" ? candidate : void 0;
|
|
15904
|
+
}
|
|
15905
|
+
function routeDecisionUpdate(ad, adapter2, cycleNumber, warnings) {
|
|
15906
|
+
const action = ad.action;
|
|
15907
|
+
let route;
|
|
15908
|
+
if (action === "delete" && adapter2.deleteActiveDecision) {
|
|
15909
|
+
route = "delete";
|
|
15910
|
+
} else if (action === "new" && adapter2.upsertActiveDecision) {
|
|
15911
|
+
route = "upsert";
|
|
15912
|
+
} else {
|
|
15913
|
+
route = "update";
|
|
15914
|
+
}
|
|
15915
|
+
const titleMatch = ad.body.match(/^###\s+\S+:\s*([^\n[]+?)(?:\s*\[|$)/m);
|
|
15916
|
+
const confidenceMatch = ad.body.match(/\[Confidence:\s*(HIGH|MEDIUM|LOW)\]/i);
|
|
15917
|
+
const eventType = action === "delete" ? "invalidated" : action === "confidence_change" ? "confidence_changed" : action === "supersede" ? "superseded" : action === "new" ? "created" : "modified";
|
|
15918
|
+
const evidence = extractDecisionEvidence(ad, eventType, warnings);
|
|
15919
|
+
return {
|
|
15920
|
+
id: ad.id,
|
|
15921
|
+
body: ad.body,
|
|
15922
|
+
route,
|
|
15923
|
+
action,
|
|
15924
|
+
title: titleMatch ? titleMatch[1].trim() : ad.id,
|
|
15925
|
+
confidence: confidenceMatch ? confidenceMatch[1].toUpperCase() : "MEDIUM",
|
|
15926
|
+
event: {
|
|
15927
|
+
decisionId: ad.id,
|
|
15928
|
+
eventType,
|
|
15929
|
+
cycle: cycleNumber,
|
|
15930
|
+
source: "strategy_review",
|
|
15931
|
+
sourceRef: `cycle-${cycleNumber}-review`,
|
|
15932
|
+
detail: `Action: ${action}`,
|
|
15933
|
+
evidenceRef: evidence.evidenceRef,
|
|
15934
|
+
metricDelta: evidence.metricDelta
|
|
15935
|
+
}
|
|
15936
|
+
};
|
|
15937
|
+
}
|
|
15938
|
+
async function applyDecisionUpdates(adapter2, updates, cycleNumber, warnings) {
|
|
15939
|
+
if (updates.length === 0) return;
|
|
15940
|
+
const applies = updates.map((ad) => routeDecisionUpdate(ad, adapter2, cycleNumber, warnings));
|
|
15941
|
+
const batch = asDecisionBatchApplier(adapter2);
|
|
15942
|
+
if (batch) {
|
|
15943
|
+
await batch.applyActiveDecisionUpdates(applies, cycleNumber);
|
|
15944
|
+
return;
|
|
15945
|
+
}
|
|
15946
|
+
for (const apply of applies) {
|
|
15947
|
+
if (apply.route === "delete") {
|
|
15948
|
+
await adapter2.deleteActiveDecision(apply.id);
|
|
15949
|
+
} else if (apply.route === "upsert") {
|
|
15950
|
+
await adapter2.upsertActiveDecision(apply.id, apply.body, apply.title, apply.confidence, cycleNumber);
|
|
15951
|
+
} else {
|
|
15952
|
+
await adapter2.updateActiveDecision(apply.id, apply.body, cycleNumber, apply.action);
|
|
15953
|
+
}
|
|
15954
|
+
try {
|
|
15955
|
+
await adapter2.appendDecisionEvent(apply.event);
|
|
15956
|
+
} catch {
|
|
15957
|
+
}
|
|
15958
|
+
}
|
|
15959
|
+
}
|
|
15783
15960
|
async function writeBack2(adapter2, cycleNumber, data, fullAnalysis, warnings) {
|
|
15784
15961
|
const cleanTitle = data.sessionLogTitle.replace(/^(?:Cycle|Session)\s+\d+\s*—\s*/i, "").trim();
|
|
15785
15962
|
const cleanContent = data.sessionLogContent.replace(/^#{1,3}\s+(?:Cycle|Session)\s+\d+\s*—[^\n]*\n*/i, "").trim();
|
|
@@ -15821,34 +15998,7 @@ ${cleanContent}`;
|
|
|
15821
15998
|
} catch {
|
|
15822
15999
|
}
|
|
15823
16000
|
if (data.activeDecisionUpdates && data.activeDecisionUpdates.length > 0) {
|
|
15824
|
-
await
|
|
15825
|
-
if (ad.action === "delete" && adapter2.deleteActiveDecision) {
|
|
15826
|
-
await adapter2.deleteActiveDecision(ad.id);
|
|
15827
|
-
} else if (ad.action === "new" && adapter2.upsertActiveDecision) {
|
|
15828
|
-
const titleMatch = ad.body.match(/^###\s+\S+:\s*([^\n[]+?)(?:\s*\[|$)/m);
|
|
15829
|
-
const title = titleMatch ? titleMatch[1].trim() : ad.id;
|
|
15830
|
-
const confidenceMatch = ad.body.match(/\[Confidence:\s*(HIGH|MEDIUM|LOW)\]/i);
|
|
15831
|
-
const confidence = confidenceMatch ? confidenceMatch[1].toUpperCase() : "MEDIUM";
|
|
15832
|
-
await adapter2.upsertActiveDecision(ad.id, ad.body, title, confidence, cycleNumber);
|
|
15833
|
-
} else {
|
|
15834
|
-
await adapter2.updateActiveDecision(ad.id, ad.body, cycleNumber, ad.action);
|
|
15835
|
-
}
|
|
15836
|
-
const eventType = ad.action === "delete" ? "invalidated" : ad.action === "confidence_change" ? "confidence_changed" : ad.action === "supersede" ? "superseded" : ad.action === "new" ? "created" : "modified";
|
|
15837
|
-
const evidence = extractDecisionEvidence(ad, eventType, warnings);
|
|
15838
|
-
try {
|
|
15839
|
-
await adapter2.appendDecisionEvent({
|
|
15840
|
-
decisionId: ad.id,
|
|
15841
|
-
eventType,
|
|
15842
|
-
cycle: cycleNumber,
|
|
15843
|
-
source: "strategy_review",
|
|
15844
|
-
sourceRef: `cycle-${cycleNumber}-review`,
|
|
15845
|
-
detail: `Action: ${ad.action}`,
|
|
15846
|
-
evidenceRef: evidence.evidenceRef,
|
|
15847
|
-
metricDelta: evidence.metricDelta
|
|
15848
|
-
});
|
|
15849
|
-
} catch {
|
|
15850
|
-
}
|
|
15851
|
-
}));
|
|
16001
|
+
await applyDecisionUpdates(adapter2, data.activeDecisionUpdates, cycleNumber, warnings);
|
|
15852
16002
|
}
|
|
15853
16003
|
try {
|
|
15854
16004
|
if (adapter2.confirmPendingActiveDecisions) {
|
|
@@ -16606,37 +16756,45 @@ async function prepareStrategyChange(adapter2, text, adapterType) {
|
|
|
16606
16756
|
async function applyStrategyChangeOutput(adapter2, rawLlmOutput, cycleNumber) {
|
|
16607
16757
|
return processStrategyChangeOutput(adapter2, rawLlmOutput, cycleNumber);
|
|
16608
16758
|
}
|
|
16759
|
+
function asDecisionIdAllocator(adapter2) {
|
|
16760
|
+
const candidate = adapter2;
|
|
16761
|
+
return typeof candidate.allocateActiveDecision === "function" ? candidate : void 0;
|
|
16762
|
+
}
|
|
16763
|
+
var AD_ID_PLACEHOLDER = "{{AD_ID}}";
|
|
16764
|
+
function toAdBodyTemplate(body) {
|
|
16765
|
+
return body.replace(/^(\s*#{1,6}\s+)AD-\d+\b/m, `$1${AD_ID_PLACEHOLDER}`);
|
|
16766
|
+
}
|
|
16609
16767
|
async function captureDecision(adapter2, input) {
|
|
16610
16768
|
const health = await adapter2.getCycleHealth();
|
|
16611
16769
|
const cycleNumber = health.totalCycles;
|
|
16612
|
-
|
|
16613
|
-
|
|
16614
|
-
|
|
16615
|
-
|
|
16616
|
-
|
|
16617
|
-
|
|
16618
|
-
|
|
16619
|
-
|
|
16620
|
-
|
|
16621
|
-
|
|
16622
|
-
|
|
16623
|
-
|
|
16624
|
-
adAction = "created";
|
|
16770
|
+
const supersedesId = input.supersedes?.trim() || void 0;
|
|
16771
|
+
if (supersedesId && input.confidenceOnly) {
|
|
16772
|
+
throw new Error("supersedes cannot be combined with confidence_only \u2014 a confidence bump does not replace a decision.");
|
|
16773
|
+
}
|
|
16774
|
+
if (supersedesId) {
|
|
16775
|
+
const all = await adapter2.getActiveDecisions({ includeRetired: true });
|
|
16776
|
+
if (!all.some((d) => d.id === supersedesId)) {
|
|
16777
|
+
throw new Error(`supersedes: ${supersedesId} does not exist on this project. Check the AD id (e.g. "AD-42").`);
|
|
16778
|
+
}
|
|
16779
|
+
if (supersedesId === input.adId) {
|
|
16780
|
+
throw new Error("An AD cannot supersede itself.");
|
|
16781
|
+
}
|
|
16625
16782
|
}
|
|
16626
16783
|
if (input.confidenceOnly) {
|
|
16627
16784
|
if (!input.adId) {
|
|
16628
16785
|
throw new Error('confidence_only requires adId \u2014 provide the AD to update (e.g. "AD-12")');
|
|
16629
16786
|
}
|
|
16787
|
+
const adId2 = input.adId;
|
|
16630
16788
|
if (adapter2.upsertActiveDecision) {
|
|
16631
16789
|
const existing = await adapter2.getActiveDecisions({ includeRetired: false });
|
|
16632
|
-
const current = existing.find((d) => d.id ===
|
|
16790
|
+
const current = existing.find((d) => d.id === adId2);
|
|
16633
16791
|
const preservedBody = current?.body ?? `- **Decision:** ${input.text}`;
|
|
16634
16792
|
const preservedTitle = current?.title ?? input.text.slice(0, 80);
|
|
16635
|
-
await adapter2.upsertActiveDecision(
|
|
16793
|
+
await adapter2.upsertActiveDecision(adId2, preservedBody, preservedTitle, input.confidence, cycleNumber);
|
|
16636
16794
|
}
|
|
16637
16795
|
try {
|
|
16638
16796
|
await adapter2.appendDecisionEvent({
|
|
16639
|
-
decisionId:
|
|
16797
|
+
decisionId: adId2,
|
|
16640
16798
|
eventType: "modified",
|
|
16641
16799
|
cycle: cycleNumber,
|
|
16642
16800
|
source: "strategy_change",
|
|
@@ -16645,18 +16803,66 @@ async function captureDecision(adapter2, input) {
|
|
|
16645
16803
|
});
|
|
16646
16804
|
} catch {
|
|
16647
16805
|
}
|
|
16648
|
-
return { cycleNumber, adId, adAction: "updated" };
|
|
16806
|
+
return { cycleNumber, adId: adId2, adAction: "updated" };
|
|
16649
16807
|
}
|
|
16650
16808
|
const title = input.text.length > 80 ? input.text.slice(0, 77) + "..." : input.text;
|
|
16651
|
-
const
|
|
16809
|
+
const supersedesLine = supersedesId ? `
|
|
16810
|
+
- **Supersedes:** ${supersedesId}` : "";
|
|
16811
|
+
const bodyTemplate = input.adBody ? toAdBodyTemplate(input.adBody) : `### ${AD_ID_PLACEHOLDER}: ${title} [Confidence: ${input.confidence}]
|
|
16652
16812
|
|
|
16653
|
-
- **Decision:** ${input.text}
|
|
16813
|
+
- **Decision:** ${input.text}${supersedesLine}
|
|
16654
16814
|
- **Evidence:** Captured from conversation, Cycle ${cycleNumber}.
|
|
16655
16815
|
- **Status:** Active`;
|
|
16656
|
-
|
|
16657
|
-
|
|
16816
|
+
let adId;
|
|
16817
|
+
let adAction;
|
|
16818
|
+
if (input.adId) {
|
|
16819
|
+
adId = input.adId;
|
|
16820
|
+
adAction = "updated";
|
|
16821
|
+
const adBody = bodyTemplate.split(AD_ID_PLACEHOLDER).join(adId);
|
|
16822
|
+
if (adapter2.upsertActiveDecision) {
|
|
16823
|
+
await adapter2.upsertActiveDecision(adId, adBody, title, input.confidence, cycleNumber);
|
|
16824
|
+
} else {
|
|
16825
|
+
await adapter2.updateActiveDecision(adId, adBody, cycleNumber);
|
|
16826
|
+
}
|
|
16658
16827
|
} else {
|
|
16659
|
-
|
|
16828
|
+
adAction = "created";
|
|
16829
|
+
const allocator = asDecisionIdAllocator(adapter2);
|
|
16830
|
+
if (allocator) {
|
|
16831
|
+
adId = await allocator.allocateActiveDecision(bodyTemplate, title, input.confidence, cycleNumber);
|
|
16832
|
+
} else {
|
|
16833
|
+
const existingAds = await adapter2.getActiveDecisions({ includeRetired: true });
|
|
16834
|
+
const maxNum = existingAds.reduce((max, ad) => {
|
|
16835
|
+
const match = ad.id.match(/^AD-(\d+)$/);
|
|
16836
|
+
return match ? Math.max(max, parseInt(match[1], 10)) : max;
|
|
16837
|
+
}, 0);
|
|
16838
|
+
adId = `AD-${maxNum + 1}`;
|
|
16839
|
+
const adBody = bodyTemplate.split(AD_ID_PLACEHOLDER).join(adId);
|
|
16840
|
+
if (adapter2.upsertActiveDecision) {
|
|
16841
|
+
await adapter2.upsertActiveDecision(adId, adBody, title, input.confidence, cycleNumber);
|
|
16842
|
+
} else {
|
|
16843
|
+
await adapter2.updateActiveDecision(adId, adBody, cycleNumber);
|
|
16844
|
+
}
|
|
16845
|
+
}
|
|
16846
|
+
}
|
|
16847
|
+
if (supersedesId) {
|
|
16848
|
+
const all = await adapter2.getActiveDecisions({ includeRetired: true });
|
|
16849
|
+
const prior = all.find((d) => d.id === supersedesId);
|
|
16850
|
+
const priorBody = prior?.body ?? "";
|
|
16851
|
+
const note = `
|
|
16852
|
+
|
|
16853
|
+
- **Superseded by:** ${adId} (Cycle ${cycleNumber}) \u2014 ${input.text}`;
|
|
16854
|
+
await adapter2.updateActiveDecision(supersedesId, `${priorBody}${note}`, cycleNumber, "supersede");
|
|
16855
|
+
try {
|
|
16856
|
+
await adapter2.appendDecisionEvent({
|
|
16857
|
+
decisionId: supersedesId,
|
|
16858
|
+
eventType: "superseded",
|
|
16859
|
+
cycle: cycleNumber,
|
|
16860
|
+
source: "strategy_change",
|
|
16861
|
+
sourceRef: `cycle-${cycleNumber}-capture`,
|
|
16862
|
+
detail: `Superseded by ${adId}: ${input.text.slice(0, 180)}`
|
|
16863
|
+
});
|
|
16864
|
+
} catch {
|
|
16865
|
+
}
|
|
16660
16866
|
}
|
|
16661
16867
|
try {
|
|
16662
16868
|
await adapter2.appendDecisionEvent({
|
|
@@ -16677,11 +16883,13 @@ async function captureDecision(adapter2, input) {
|
|
|
16677
16883
|
title: `Decision captured: ${adId}`,
|
|
16678
16884
|
content: `**${adAction === "created" ? "New" : "Updated"} Active Decision** \u2014 ${adId}: ${input.text}
|
|
16679
16885
|
|
|
16680
|
-
Confidence: ${input.confidence}. Captured mid-conversation via strategy_change capture mode (Cycle ${cycleNumber}).`
|
|
16886
|
+
Confidence: ${input.confidence}. Captured mid-conversation via strategy_change capture mode (Cycle ${cycleNumber}).` + (supersedesId ? `
|
|
16887
|
+
|
|
16888
|
+
Supersedes ${supersedesId} (retired, kept as history).` : "")
|
|
16681
16889
|
});
|
|
16682
16890
|
} catch {
|
|
16683
16891
|
}
|
|
16684
|
-
return { cycleNumber, adId, adAction };
|
|
16892
|
+
return { cycleNumber, adId, adAction, supersededId: supersedesId };
|
|
16685
16893
|
}
|
|
16686
16894
|
|
|
16687
16895
|
// src/tools/strategy.ts
|
|
@@ -16797,6 +17005,10 @@ var strategyChangeTool = {
|
|
|
16797
17005
|
type: "boolean",
|
|
16798
17006
|
description: `When true (mode "capture" + ad_id required), only update the confidence level \u2014 leave the AD body unchanged. Use when evidence strength changes but the decision itself hasn't shifted.`
|
|
16799
17007
|
},
|
|
17008
|
+
supersedes: {
|
|
17009
|
+
type: "string",
|
|
17010
|
+
description: 'Existing AD ID this new decision replaces, e.g. "AD-42" (mode "capture" only). The named AD is marked superseded and kept as history \u2014 never overwritten. Use this instead of passing ad_id when the decision has CHANGED rather than been refined.'
|
|
17011
|
+
},
|
|
16800
17012
|
north_star: {
|
|
16801
17013
|
type: "string",
|
|
16802
17014
|
description: 'mode "capture" only \u2014 set/update the project North Star statement directly. orient and the project foundation read it. No decision text required when this is provided.'
|
|
@@ -17030,19 +17242,24 @@ orient and the project foundation will read this value.`
|
|
|
17030
17242
|
const confidence = args.confidence ?? "MEDIUM";
|
|
17031
17243
|
const adBody = args.ad_body;
|
|
17032
17244
|
const confidenceOnly = args.confidence_only === true;
|
|
17245
|
+
const supersedes = args.supersedes?.trim();
|
|
17033
17246
|
const result = await captureDecision(adapter2, {
|
|
17034
17247
|
text: text2.trim(),
|
|
17035
17248
|
adId: adId?.trim(),
|
|
17036
17249
|
confidence,
|
|
17037
17250
|
adBody: adBody?.trim(),
|
|
17038
|
-
confidenceOnly
|
|
17251
|
+
confidenceOnly,
|
|
17252
|
+
supersedes
|
|
17039
17253
|
});
|
|
17040
17254
|
const captureLabel = confidenceOnly ? `Updated confidence on **${result.adId}** to ${confidence} (body preserved)` : `${result.adAction === "created" ? "Created" : "Updated"} **${result.adId}**: ${text2.trim()}
|
|
17041
17255
|
Confidence: ${confidence}`;
|
|
17256
|
+
const supersedeLine = result.supersededId ? `
|
|
17257
|
+
|
|
17258
|
+
**${result.supersededId}** marked superseded by ${result.adId} \u2014 retained as history, not overwritten.` : "";
|
|
17042
17259
|
return textResponse(
|
|
17043
17260
|
`**Decision Captured \u2014 Cycle ${result.cycleNumber}**
|
|
17044
17261
|
|
|
17045
|
-
${captureLabel}
|
|
17262
|
+
${captureLabel}${supersedeLine}
|
|
17046
17263
|
|
|
17047
17264
|
Decision event logged.`
|
|
17048
17265
|
);
|
|
@@ -17153,8 +17370,8 @@ async function viewBoard(adapter2, phaseFilter, options) {
|
|
|
17153
17370
|
const bi = PRIORITY_ORDER.indexOf(b2.priority);
|
|
17154
17371
|
const priorityDiff = (ai === -1 ? 999 : ai) - (bi === -1 ? 999 : bi);
|
|
17155
17372
|
if (priorityDiff !== 0) return priorityDiff;
|
|
17156
|
-
const aDate = a.createdAt
|
|
17157
|
-
const bDate = b2.createdAt
|
|
17373
|
+
const aDate = a.createdAt ?? "";
|
|
17374
|
+
const bDate = b2.createdAt ?? "";
|
|
17158
17375
|
return bDate.localeCompare(aDate);
|
|
17159
17376
|
});
|
|
17160
17377
|
const total = filtered.length;
|
|
@@ -17508,6 +17725,10 @@ var boardEditTool = {
|
|
|
17508
17725
|
actual_effort: {
|
|
17509
17726
|
$ref: "#/$defs/effortSize",
|
|
17510
17727
|
description: "task-2182: correct the actual effort on this task's LATEST build report (fixes a mis-recorded actual)."
|
|
17728
|
+
},
|
|
17729
|
+
project: {
|
|
17730
|
+
type: "string",
|
|
17731
|
+
description: "Project id (UUID) or slug whose board this task lives on, overriding the session project for THIS call only. Must be a project on your account \u2014 fails closed otherwise. Use project_switch to change the session default. PASS THIS WHENEVER YOU KNOW WHICH REPO THE SESSION IS IN: on a multi-project account a connection with no project bound cannot be resolved, and PAPI will stop and ask rather than guess."
|
|
17511
17732
|
}
|
|
17512
17733
|
},
|
|
17513
17734
|
required: ["task_id"]
|
|
@@ -17761,6 +17982,14 @@ async function handleBoardEdit(adapter2, args) {
|
|
|
17761
17982
|
if (!taskId) {
|
|
17762
17983
|
return errorResponse("task_id is required.");
|
|
17763
17984
|
}
|
|
17985
|
+
let target = adapter2;
|
|
17986
|
+
let overrideNote = "";
|
|
17987
|
+
try {
|
|
17988
|
+
({ adapter: target, overrideNote } = await resolvePerCallProjectAdapter(adapter2, args));
|
|
17989
|
+
} catch (err) {
|
|
17990
|
+
if (err instanceof ProjectResolutionError) return errorResponse(err.message);
|
|
17991
|
+
throw err;
|
|
17992
|
+
}
|
|
17764
17993
|
const updates = {};
|
|
17765
17994
|
const changes = [];
|
|
17766
17995
|
for (const field of EDITABLE_FIELDS) {
|
|
@@ -17788,7 +18017,7 @@ async function handleBoardEdit(adapter2, args) {
|
|
|
17788
18017
|
updates.cycle = null;
|
|
17789
18018
|
changes.push("cycle");
|
|
17790
18019
|
} else if (typeof rawCycle === "number" && Number.isInteger(rawCycle) && rawCycle > 0) {
|
|
17791
|
-
const health = await
|
|
18020
|
+
const health = await target.getCycleHealth().catch(() => null);
|
|
17792
18021
|
const activeCycle = health?.totalCycles ?? 0;
|
|
17793
18022
|
if (rawCycle > activeCycle + 1) {
|
|
17794
18023
|
return errorResponse(
|
|
@@ -17807,7 +18036,7 @@ async function handleBoardEdit(adapter2, args) {
|
|
|
17807
18036
|
return errorResponse("No fields to update. Pass at least one field (title, priority, complexity, module, epic, phase, notes, status, maturity, cycle).");
|
|
17808
18037
|
}
|
|
17809
18038
|
try {
|
|
17810
|
-
const task = await
|
|
18039
|
+
const task = await target.getTask(taskId);
|
|
17811
18040
|
if (!task) {
|
|
17812
18041
|
return errorResponse(`Task ${taskId} not found.`);
|
|
17813
18042
|
}
|
|
@@ -17826,7 +18055,7 @@ async function handleBoardEdit(adapter2, args) {
|
|
|
17826
18055
|
const idx = changes.indexOf("notes");
|
|
17827
18056
|
if (idx >= 0) changes.splice(idx, 1);
|
|
17828
18057
|
} else {
|
|
17829
|
-
const health = await
|
|
18058
|
+
const health = await target.getCycleHealth().catch(() => null);
|
|
17830
18059
|
const activeCycle = health?.totalCycles ?? null;
|
|
17831
18060
|
const date = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
17832
18061
|
const stamp = activeCycle != null ? `[C${activeCycle} ${date}]` : `[${date}]`;
|
|
@@ -17851,7 +18080,7 @@ ${existing}` : entry;
|
|
|
17851
18080
|
}
|
|
17852
18081
|
let autoAssignedCycle = null;
|
|
17853
18082
|
if (updates.status === "In Cycle") {
|
|
17854
|
-
const health = await
|
|
18083
|
+
const health = await target.getCycleHealth().catch(() => null);
|
|
17855
18084
|
const activeCycle = health?.totalCycles ?? null;
|
|
17856
18085
|
if (activeCycle != null && activeCycle > 0) {
|
|
17857
18086
|
updates.cycle = activeCycle;
|
|
@@ -17863,25 +18092,25 @@ ${existing}` : entry;
|
|
|
17863
18092
|
updates.cancelledBy = "user";
|
|
17864
18093
|
}
|
|
17865
18094
|
if (effortCorrection.estimatedEffort || effortCorrection.actualEffort) {
|
|
17866
|
-
if (!
|
|
18095
|
+
if (!target.correctLatestBuildReportEffort) {
|
|
17867
18096
|
return errorResponse("Correcting build-report effort requires a database adapter (pg). The md adapter does not support it.");
|
|
17868
18097
|
}
|
|
17869
|
-
await
|
|
18098
|
+
await target.correctLatestBuildReportEffort(taskId, effortCorrection);
|
|
17870
18099
|
}
|
|
17871
18100
|
if (Object.keys(updates).length > 0) {
|
|
17872
|
-
await
|
|
18101
|
+
await target.updateTask(taskId, updates);
|
|
17873
18102
|
}
|
|
17874
|
-
if ((updates.status === "Done" || updates.status === "Cancelled") &&
|
|
18103
|
+
if ((updates.status === "Done" || updates.status === "Cancelled") && target.updateDogfoodEntryStatus) {
|
|
17875
18104
|
try {
|
|
17876
|
-
const dogfoodLog = await
|
|
18105
|
+
const dogfoodLog = await target.getDogfoodLog?.(50) ?? [];
|
|
17877
18106
|
const linked = dogfoodLog.filter((e) => e.linkedTaskId === taskId || e.linkedTaskId === task.id);
|
|
17878
18107
|
const newStatus = "resolved";
|
|
17879
|
-
await Promise.all(linked.map((e) =>
|
|
18108
|
+
await Promise.all(linked.map((e) => target.updateDogfoodEntryStatus(e.id, newStatus)));
|
|
17880
18109
|
} catch {
|
|
17881
18110
|
}
|
|
17882
18111
|
}
|
|
17883
18112
|
const lines = [
|
|
17884
|
-
`Updated **${taskId}
|
|
18113
|
+
`Updated **${taskId}**${overrideNote} (${updates.title ?? task.title})`,
|
|
17885
18114
|
"",
|
|
17886
18115
|
`**Changes:** ${changes.map((f) => `${f} \u2192 ${String(updates[f])}`).join(", ")}`
|
|
17887
18116
|
];
|
|
@@ -18685,7 +18914,7 @@ async function scaffoldPapiDir(adapter2, config2, input, collector) {
|
|
|
18685
18914
|
if (useCollector) {
|
|
18686
18915
|
collector.add({
|
|
18687
18916
|
path: ".claude/settings.json",
|
|
18688
|
-
content: JSON.stringify({ permissions: { allow: [
|
|
18917
|
+
content: JSON.stringify({ permissions: { allow: [...PAPI_PERMISSIONS] } }, null, 2) + "\n",
|
|
18689
18918
|
mode: "create",
|
|
18690
18919
|
skip_if_exists: true
|
|
18691
18920
|
});
|
|
@@ -18694,7 +18923,7 @@ async function scaffoldPapiDir(adapter2, config2, input, collector) {
|
|
|
18694
18923
|
}
|
|
18695
18924
|
return true;
|
|
18696
18925
|
}
|
|
18697
|
-
var
|
|
18926
|
+
var PAPI_PERMISSIONS = ["mcp__papi__*", "mcp__plugin_papi_papi__*"];
|
|
18698
18927
|
async function ensurePapiPermission(projectRoot) {
|
|
18699
18928
|
const settingsPath = join9(projectRoot, ".claude", "settings.json");
|
|
18700
18929
|
try {
|
|
@@ -18712,8 +18941,10 @@ async function ensurePapiPermission(projectRoot) {
|
|
|
18712
18941
|
perms.allow = [];
|
|
18713
18942
|
}
|
|
18714
18943
|
const allow = perms.allow;
|
|
18715
|
-
|
|
18716
|
-
allow.
|
|
18944
|
+
for (const permission of PAPI_PERMISSIONS) {
|
|
18945
|
+
if (!allow.includes(permission)) {
|
|
18946
|
+
allow.push(permission);
|
|
18947
|
+
}
|
|
18717
18948
|
}
|
|
18718
18949
|
await mkdir(join9(projectRoot, ".claude"), { recursive: true });
|
|
18719
18950
|
await writeFile2(settingsPath, JSON.stringify(settings, null, 2) + "\n", "utf-8");
|
|
@@ -21750,7 +21981,7 @@ function pathBasename(p) {
|
|
|
21750
21981
|
const parts = p.replace(/\\/g, "/").replace(/\/+$/, "").split("/");
|
|
21751
21982
|
return parts[parts.length - 1] ?? p;
|
|
21752
21983
|
}
|
|
21753
|
-
function
|
|
21984
|
+
function matchedPredictedEntry(changedPath, predicted) {
|
|
21754
21985
|
const changed = changedPath.replace(/\\/g, "/");
|
|
21755
21986
|
const changedLower = changed.toLowerCase();
|
|
21756
21987
|
const changedBase = pathBasename(changedPath);
|
|
@@ -21758,17 +21989,20 @@ function isPathInPredictedScope(changedPath, predicted) {
|
|
|
21758
21989
|
const entry = raw.replace(/\\/g, "/").replace(/\/+$/, "").trim();
|
|
21759
21990
|
if (!entry) continue;
|
|
21760
21991
|
if (entry.includes("*")) {
|
|
21761
|
-
if (globToRegExp(entry).test(changed)) return
|
|
21762
|
-
if (!entry.includes("/") && globToRegExp(entry).test(changedBase)) return
|
|
21992
|
+
if (globToRegExp(entry).test(changed)) return entry;
|
|
21993
|
+
if (!entry.includes("/") && globToRegExp(entry).test(changedBase)) return entry;
|
|
21763
21994
|
continue;
|
|
21764
21995
|
}
|
|
21765
|
-
if (pathBasename(entry) === changedBase) return
|
|
21996
|
+
if (pathBasename(entry) === changedBase) return entry;
|
|
21766
21997
|
const entryLower = entry.toLowerCase();
|
|
21767
21998
|
if (changedLower === entryLower || changedLower.startsWith(`${entryLower}/`)) {
|
|
21768
|
-
return
|
|
21999
|
+
return entry;
|
|
21769
22000
|
}
|
|
21770
22001
|
}
|
|
21771
|
-
return
|
|
22002
|
+
return null;
|
|
22003
|
+
}
|
|
22004
|
+
function isPathInPredictedScope(changedPath, predicted) {
|
|
22005
|
+
return matchedPredictedEntry(changedPath, predicted) !== null;
|
|
21772
22006
|
}
|
|
21773
22007
|
function autoCommit(config2, taskId, taskTitle, predictedFiles) {
|
|
21774
22008
|
const cwd = config2.projectRoot;
|
|
@@ -21794,6 +22028,17 @@ function autoCommit(config2, taskId, taskTitle, predictedFiles) {
|
|
|
21794
22028
|
if (staged.length > 0) {
|
|
21795
22029
|
return safeRun(() => commitStagedOnly(cwd, message)) + ` (selective staging respected: ${staged.length} file(s)).`;
|
|
21796
22030
|
}
|
|
22031
|
+
const checkpoint = readBuildCheckpointIfLocal({ cwd, taskId });
|
|
22032
|
+
const headSha = getHeadCommitSha(cwd);
|
|
22033
|
+
if (checkpoint?.lastCommitSha && headSha && checkpoint.lastCommitSha !== headSha) {
|
|
22034
|
+
const leftover = getModifiedFiles(cwd);
|
|
22035
|
+
if (leftover.length === 0) {
|
|
22036
|
+
return "Auto-commit: skipped (builder already committed; working tree clean).";
|
|
22037
|
+
}
|
|
22038
|
+
const sample = leftover.slice(0, 10).join(", ");
|
|
22039
|
+
const more = leftover.length > 10 ? ` (+${leftover.length - 10} more)` : "";
|
|
22040
|
+
return `Auto-commit: skipped \u2014 you already committed during this build, and ${leftover.length} file(s) are still modified. They were NOT committed, because at this point PAPI cannot tell your deliberately-excluded work from a concurrent session's files (task-3054). Left uncommitted: ${sample}${more}. If any belong to ${taskId}, \`git add\` them and re-run build_execute complete.`;
|
|
22041
|
+
}
|
|
21797
22042
|
const modified = getModifiedFiles(cwd);
|
|
21798
22043
|
if (modified.length === 0) {
|
|
21799
22044
|
return "Auto-commit: skipped (no working-tree changes).";
|
|
@@ -21807,7 +22052,9 @@ function autoCommit(config2, taskId, taskTitle, predictedFiles) {
|
|
|
21807
22052
|
const more = outOfScope.length > 10 ? ` (+${outOfScope.length - 10} more)` : "";
|
|
21808
22053
|
return `${commitResult} (staged all ${modified.length} changed file(s)). \u2139\uFE0F Scope drift: ${outOfScope.length} committed file(s) were outside the handoff's FILES LIKELY TOUCHED \u2014 handoff under-predicted: ${sample}${more}.`;
|
|
21809
22054
|
}
|
|
21810
|
-
|
|
22055
|
+
const matches = modified.slice(0, 5).map((p) => `${p} \u2190 ${matchedPredictedEntry(p, cleanedPredicted) ?? "?"}`).join(", ");
|
|
22056
|
+
const extra = modified.length > 5 ? ` (+${modified.length - 5} more)` : "";
|
|
22057
|
+
return `${commitResult} (staged all ${modified.length} changed file(s), each matched to FILES LIKELY TOUCHED: ${matches}${extra}).`;
|
|
21811
22058
|
}
|
|
21812
22059
|
return `${commitResult} (staged all ${modified.length} changed file(s)).`;
|
|
21813
22060
|
}
|
|
@@ -22851,9 +23098,19 @@ async function completeBuild(adapter2, config2, taskId, input, options = {}, cli
|
|
|
22851
23098
|
return files;
|
|
22852
23099
|
};
|
|
22853
23100
|
const mdFiles = scanDir(docsDir);
|
|
23101
|
+
let branchDocs;
|
|
23102
|
+
try {
|
|
23103
|
+
branchDocs = new Set(
|
|
23104
|
+
getFilesChangedFromBase(config2.projectRoot, "origin/main").filter((f) => f.startsWith("docs/") && f.endsWith(".md"))
|
|
23105
|
+
);
|
|
23106
|
+
} catch {
|
|
23107
|
+
branchDocs = /* @__PURE__ */ new Set();
|
|
23108
|
+
}
|
|
22854
23109
|
const registered = await adapter2.searchDocs({ status: "all", limit: 500 });
|
|
22855
23110
|
const registeredPaths = new Set(registered.map((d) => d.path));
|
|
22856
|
-
const unregistered = mdFiles.filter(
|
|
23111
|
+
const unregistered = mdFiles.filter(
|
|
23112
|
+
(f) => !registeredPaths.has(f) && branchDocs.has(f)
|
|
23113
|
+
);
|
|
22857
23114
|
if (unregistered.length > 0 && adapter2.registerDoc) {
|
|
22858
23115
|
const autoRegistered = [];
|
|
22859
23116
|
const failed = [];
|
|
@@ -23037,7 +23294,7 @@ import { docDeletionBlockMessage } from "@papi-ai/shared";
|
|
|
23037
23294
|
init_git();
|
|
23038
23295
|
|
|
23039
23296
|
// src/services/entitlements.ts
|
|
23040
|
-
import { evaluateContributorGate } from "@papi-ai/shared";
|
|
23297
|
+
import { evaluateContributorGate, isSelfHostedDeployment } from "@papi-ai/shared";
|
|
23041
23298
|
var FREE_PROJECT_CAP = 3;
|
|
23042
23299
|
var DOC_STORAGE_CEILING_BY_TIER = {
|
|
23043
23300
|
free: { bytes: 25 * 1024 * 1024, docs: 200 },
|
|
@@ -23060,6 +23317,7 @@ function isPaidTier(tier) {
|
|
|
23060
23317
|
return tier !== null && PAID_TIERS.has(tier);
|
|
23061
23318
|
}
|
|
23062
23319
|
async function enforceProjectCap(adapter2, target) {
|
|
23320
|
+
if (isSelfHostedDeployment(process.env.PAPI_SELF_HOST)) return null;
|
|
23063
23321
|
const tier = await resolveTier(adapter2);
|
|
23064
23322
|
if (tier === null || isPaidTier(tier)) return null;
|
|
23065
23323
|
if (typeof adapter2.listUserProjects !== "function") return null;
|
|
@@ -23170,7 +23428,11 @@ var docRegisterTool = {
|
|
|
23170
23428
|
},
|
|
23171
23429
|
description: "Actionable findings from the document."
|
|
23172
23430
|
},
|
|
23173
|
-
superseded_by_path: { type: "string", description: "Path of the doc that supersedes this one (sets status to superseded)." }
|
|
23431
|
+
superseded_by_path: { type: "string", description: "Path of the doc that supersedes this one (sets status to superseded)." },
|
|
23432
|
+
project: {
|
|
23433
|
+
type: "string",
|
|
23434
|
+
description: "Project id (UUID) or slug to register this doc under, overriding the session project for THIS call only. Must be a project on your account \u2014 fails closed otherwise. Use project_switch to change the session default. PASS THIS WHENEVER YOU KNOW WHICH REPO THE SESSION IS IN: on a multi-project account a connection with no project bound cannot be resolved, and PAPI will stop and ask rather than guess."
|
|
23435
|
+
}
|
|
23174
23436
|
},
|
|
23175
23437
|
required: ["path", "title", "type", "summary", "cycle"]
|
|
23176
23438
|
}
|
|
@@ -23327,6 +23589,22 @@ async function handleDocRegister(adapter2, args, config2) {
|
|
|
23327
23589
|
continueHint
|
|
23328
23590
|
);
|
|
23329
23591
|
}
|
|
23592
|
+
let target = adapter2;
|
|
23593
|
+
let overrideNote = "";
|
|
23594
|
+
try {
|
|
23595
|
+
({ adapter: target, overrideNote } = await resolvePerCallProjectAdapter(adapter2, args));
|
|
23596
|
+
} catch (err) {
|
|
23597
|
+
if (err instanceof ProjectResolutionError) return errorResponse(err.message);
|
|
23598
|
+
throw err;
|
|
23599
|
+
}
|
|
23600
|
+
if (!target.registerDoc) {
|
|
23601
|
+
return docRegisterSoftFail(
|
|
23602
|
+
adapterType,
|
|
23603
|
+
"adapter-capability-check",
|
|
23604
|
+
"Doc registry not available on the resolved project adapter \u2014 requires the pg/proxy adapter.",
|
|
23605
|
+
"Continue without it \u2014 nothing is blocked."
|
|
23606
|
+
);
|
|
23607
|
+
}
|
|
23330
23608
|
if (!path7.toLowerCase().endsWith(".md")) {
|
|
23331
23609
|
return docRegisterSoftFail(
|
|
23332
23610
|
adapterType,
|
|
@@ -23338,13 +23616,13 @@ async function handleDocRegister(adapter2, args, config2) {
|
|
|
23338
23616
|
try {
|
|
23339
23617
|
let supersededBy;
|
|
23340
23618
|
if (supersededByPath) {
|
|
23341
|
-
const existing = await
|
|
23619
|
+
const existing = await target.getDoc?.(supersededByPath);
|
|
23342
23620
|
if (existing) {
|
|
23343
23621
|
supersededBy = existing.id;
|
|
23344
|
-
await
|
|
23622
|
+
await target.updateDocStatus?.(existing.id, "superseded", void 0);
|
|
23345
23623
|
}
|
|
23346
23624
|
}
|
|
23347
|
-
const entry = await
|
|
23625
|
+
const entry = await target.registerDoc({
|
|
23348
23626
|
title,
|
|
23349
23627
|
type,
|
|
23350
23628
|
path: path7,
|
|
@@ -23370,16 +23648,16 @@ async function handleDocRegister(adapter2, args, config2) {
|
|
|
23370
23648
|
}
|
|
23371
23649
|
if (body === void 0) {
|
|
23372
23650
|
bodyNote = hasLocalWorkspace() ? "\n\n_Body not stored \u2014 the file could not be read from disk. Pass `body` to store it._" : "\n\n_Body not stored \u2014 no local workspace on this session. Pass `body` to store it._";
|
|
23373
|
-
} else if (typeof
|
|
23651
|
+
} else if (typeof target.storeDocBody !== "function") {
|
|
23374
23652
|
bodyNote = "\n\n_Body not stored \u2014 this adapter does not support body storage._";
|
|
23375
23653
|
} else {
|
|
23376
|
-
const decision = await checkDocStorageCap(
|
|
23654
|
+
const decision = await checkDocStorageCap(target, Buffer.byteLength(body, "utf8"));
|
|
23377
23655
|
if (!decision.storeBody) {
|
|
23378
23656
|
bodyNote = `
|
|
23379
23657
|
|
|
23380
23658
|
${decision.message}`;
|
|
23381
23659
|
} else {
|
|
23382
|
-
const result = await
|
|
23660
|
+
const result = await target.storeDocBody({
|
|
23383
23661
|
docId: entry.id,
|
|
23384
23662
|
body,
|
|
23385
23663
|
// Resolved by resolveDocVisibility above — the SAME resolution the
|
|
@@ -23403,7 +23681,7 @@ ${decision.message}`;
|
|
|
23403
23681
|
durability = "";
|
|
23404
23682
|
}
|
|
23405
23683
|
return textResponse(
|
|
23406
|
-
`**Registered:** ${entry.title}
|
|
23684
|
+
`**Registered:** ${entry.title}${overrideNote}
|
|
23407
23685
|
- **Path:** ${entry.path}
|
|
23408
23686
|
- **Type:** ${entry.type} | **Status:** ${entry.status}
|
|
23409
23687
|
- **Visibility:** ${visibilityLabel}
|
|
@@ -23967,7 +24245,7 @@ var buildExecuteTool = {
|
|
|
23967
24245
|
},
|
|
23968
24246
|
fixed_issues: {
|
|
23969
24247
|
type: "array",
|
|
23970
|
-
description: `cycle_learnings UUIDs of discovered issues this build FIXED.
|
|
24248
|
+
description: `cycle_learnings UUIDs of discovered issues this build FIXED. SEND THIS whenever your work closed an issue listed under "OPEN DISCOVERED ISSUES" in the BUILD HANDOFF \u2014 that block prints the exact UUIDs to copy. Nothing else stamps a fix, so an unreported one is indistinguishable from an unfixed issue: the hub's "What PAPI caught" caught\u2192fixed ledger reads zero until this is passed. Stamps resolved_at via the existing discovered_issue_resolve path \u2014 triage-and-fix at build time, no separate tool call. Distinct from resolves_learnings, which only LINKS a learning to this task without closing it. Do not pass UUIDs for issues you did not actually fix. Best-effort and idempotent.`,
|
|
23971
24249
|
items: { type: "string" }
|
|
23972
24250
|
},
|
|
23973
24251
|
production_verification: {
|
|
@@ -24325,7 +24603,7 @@ ${entries}`;
|
|
|
24325
24603
|
|
|
24326
24604
|
**OPEN DISCOVERED ISSUES** (${top.length} shown):
|
|
24327
24605
|
${rows}
|
|
24328
|
-
|
|
24606
|
+
CONTRACT \u2014 on complete, pass \`fixed_issues\` with the UUID of any issue above that this build closed, and say so even if the answer is none. Closing an issue without stamping it leaves the caught\u2192fixed ledger reading zero, which is what it reads today. Do NOT fix out-of-scope issues just to clear the list \u2014 the ask is to REPORT what you closed, not to close more.`;
|
|
24329
24607
|
}
|
|
24330
24608
|
}
|
|
24331
24609
|
} catch {
|
|
@@ -24572,8 +24850,25 @@ Your report was NOT discarded and the task is NOT yet Done \u2014 re-send with \
|
|
|
24572
24850
|
fixedNote = `
|
|
24573
24851
|
|
|
24574
24852
|
\u2705 Marked ${fixedResolvedCount} discovered issue(s) FIXED \u2014 resolved_at stamped, now counted as fixed on the hub's caught\u2192fixed ledger.`;
|
|
24575
|
-
} else
|
|
24576
|
-
|
|
24853
|
+
} else {
|
|
24854
|
+
let candidates = [];
|
|
24855
|
+
try {
|
|
24856
|
+
if (adapter2.getCycleLearnings) {
|
|
24857
|
+
const open = (await adapter2.getCycleLearnings({ category: "issue", limit: 20 })).filter((l) => !l.resolvedAt && l.id);
|
|
24858
|
+
const moduleTag = result.task?.module?.trim().toLowerCase();
|
|
24859
|
+
candidates = open.filter((l) => !moduleTag || l.tags.some((t) => t.toLowerCase() === moduleTag)).slice(0, 5).map((l) => ` - \`${l.id}\` \xB7 ${l.severity ?? "P3"} \xB7 ${l.summary.slice(0, 120)}`);
|
|
24860
|
+
}
|
|
24861
|
+
} catch {
|
|
24862
|
+
}
|
|
24863
|
+
if (candidates.length > 0) {
|
|
24864
|
+
fixedNote = `
|
|
24865
|
+
|
|
24866
|
+
\u2139\uFE0F No \`fixed_issues\` passed. Open issues in this module that this build could have closed:
|
|
24867
|
+
${candidates.join("\n")}
|
|
24868
|
+
If any are now fixed, re-run complete with their UUIDs in \`fixed_issues\` \u2014 nothing else stamps them, so an unreported fix is indistinguishable from an unfixed issue on the hub ledger.`;
|
|
24869
|
+
} else if (discoveredIssues && discoveredIssues.trim() !== "" && !/^none\b/i.test(discoveredIssues.trim())) {
|
|
24870
|
+
fixedNote = "\n\n\u2139\uFE0F This build filed discovered issues but passed no `fixed_issues`. When a future build fixes one, pass its UUID in `fixed_issues` so it counts as FIXED (not just auto-cleared) on the hub ledger.";
|
|
24871
|
+
}
|
|
24577
24872
|
}
|
|
24578
24873
|
return textResponse(formatCompleteResult(result) + fixedNote + docsNote + batchRollupNote);
|
|
24579
24874
|
} catch (err) {
|
|
@@ -25603,6 +25898,13 @@ init_git();
|
|
|
25603
25898
|
|
|
25604
25899
|
// src/services/ad-hoc.ts
|
|
25605
25900
|
import { randomUUID as randomUUID15 } from "crypto";
|
|
25901
|
+
function resolveAdHocBranch(input) {
|
|
25902
|
+
if (input.held) return `feat/${input.taskId}`;
|
|
25903
|
+
const current = input.currentBranch?.trim();
|
|
25904
|
+
if (!current) return void 0;
|
|
25905
|
+
if (input.baseBranch && current === input.baseBranch.trim()) return void 0;
|
|
25906
|
+
return current;
|
|
25907
|
+
}
|
|
25606
25908
|
function resolveAdHocCycle(cycle, latest, latestComplete) {
|
|
25607
25909
|
if (cycle === void 0) return null;
|
|
25608
25910
|
if (typeof cycle === "number") return cycle;
|
|
@@ -25657,8 +25959,32 @@ async function recordAdHoc(adapter2, input) {
|
|
|
25657
25959
|
...targetCycle !== null ? { cycle: targetCycle } : {},
|
|
25658
25960
|
notes: input.notes ? `[ad-hoc] ${input.notes}` : "[ad-hoc]",
|
|
25659
25961
|
taskType: input.taskType || "task",
|
|
25660
|
-
source: "ad_hoc"
|
|
25962
|
+
source: "ad_hoc",
|
|
25963
|
+
// task-2597: record the branch at creation for unheld work — the branch is
|
|
25964
|
+
// already known. Held work needs the allocated display id first (below).
|
|
25965
|
+
...held ? {} : (() => {
|
|
25966
|
+
const branch = resolveAdHocBranch({
|
|
25967
|
+
held: false,
|
|
25968
|
+
taskId: "",
|
|
25969
|
+
currentBranch: input.currentBranch,
|
|
25970
|
+
baseBranch: input.baseBranch
|
|
25971
|
+
});
|
|
25972
|
+
return branch ? { branchName: branch } : {};
|
|
25973
|
+
})()
|
|
25661
25974
|
});
|
|
25975
|
+
if (held) {
|
|
25976
|
+
const branch = resolveAdHocBranch({ held: true, taskId: task.id });
|
|
25977
|
+
if (branch) {
|
|
25978
|
+
try {
|
|
25979
|
+
await adapter2.updateTask(task.id, { branchName: branch });
|
|
25980
|
+
task = { ...task, branchName: branch };
|
|
25981
|
+
} catch (err) {
|
|
25982
|
+
console.error(
|
|
25983
|
+
`[ad-hoc] branch_name persist skipped for ${task.id} (non-fatal): ` + (err instanceof Error ? err.message : String(err))
|
|
25984
|
+
);
|
|
25985
|
+
}
|
|
25986
|
+
}
|
|
25987
|
+
}
|
|
25662
25988
|
}
|
|
25663
25989
|
const report = {
|
|
25664
25990
|
uuid: randomUUID15(),
|
|
@@ -25748,6 +26074,10 @@ var adHocTool = {
|
|
|
25748
26074
|
hold: {
|
|
25749
26075
|
type: "boolean",
|
|
25750
26076
|
description: "task-2477: held-adhoc. When true, do NOT force-complete or commit to main \u2014 record the task In Review pinned to the NEXT cycle (current + 1) so the planner won't re-plan it, and return a branch/PR directive (commit on feat/<task-id>, never main, leave unmerged) so it rides the next cycle's review \u2192 release bundled with planned work. One-call replacement for the two-call ad_hoc + board_edit stopgap. Takes precedence over `cycle`/`stage`."
|
|
26077
|
+
},
|
|
26078
|
+
project: {
|
|
26079
|
+
type: "string",
|
|
26080
|
+
description: "Project id (UUID) or slug to record this work under, overriding the session project for THIS call only. Must be a project on your account \u2014 fails closed otherwise. Use project_switch to change the session default. PASS THIS WHENEVER YOU KNOW WHICH REPO THE SESSION IS IN: on a multi-project account a connection with no project bound cannot be resolved, and PAPI will stop and ask rather than guess."
|
|
25751
26081
|
}
|
|
25752
26082
|
},
|
|
25753
26083
|
required: []
|
|
@@ -25784,7 +26114,18 @@ async function handleAdHoc(adapter2, config2, args) {
|
|
|
25784
26114
|
else if (rawCycle === "current" || rawCycle === "next-if-plan-not-run") cycleArg = rawCycle;
|
|
25785
26115
|
const stageArg = args.stage === "release" ? "release" : void 0;
|
|
25786
26116
|
const holdArg = args.hold === true;
|
|
25787
|
-
|
|
26117
|
+
let target = adapter2;
|
|
26118
|
+
let overrideNote = "";
|
|
26119
|
+
try {
|
|
26120
|
+
({ adapter: target, overrideNote } = await resolvePerCallProjectAdapter(adapter2, args));
|
|
26121
|
+
} catch (err) {
|
|
26122
|
+
if (err instanceof ProjectResolutionError) return errorResponse(err.message);
|
|
26123
|
+
throw err;
|
|
26124
|
+
}
|
|
26125
|
+
const gitUsable = !overrideNote && isGitAvailable() && isGitRepo(config2.projectRoot);
|
|
26126
|
+
const currentBranch = gitUsable ? getCurrentBranch(config2.projectRoot) : null;
|
|
26127
|
+
const baseBranch = gitUsable ? resolveBaseBranch(config2.projectRoot, config2.baseBranch) : null;
|
|
26128
|
+
const result = await recordAdHoc(target, {
|
|
25788
26129
|
title: title || "",
|
|
25789
26130
|
taskId,
|
|
25790
26131
|
notes: rawNotes,
|
|
@@ -25797,9 +26138,11 @@ async function handleAdHoc(adapter2, config2, args) {
|
|
|
25797
26138
|
owner: config2.projectOwner,
|
|
25798
26139
|
cycle: cycleArg,
|
|
25799
26140
|
stage: stageArg,
|
|
25800
|
-
hold: holdArg
|
|
26141
|
+
hold: holdArg,
|
|
26142
|
+
currentBranch,
|
|
26143
|
+
baseBranch
|
|
25801
26144
|
});
|
|
25802
|
-
if (!holdArg &&
|
|
26145
|
+
if (!holdArg && gitUsable) {
|
|
25803
26146
|
try {
|
|
25804
26147
|
stageDirAndCommit(
|
|
25805
26148
|
config2.projectRoot,
|
|
@@ -25817,7 +26160,7 @@ async function handleAdHoc(adapter2, config2, args) {
|
|
|
25817
26160
|
const branch = `feat/${result.task.id}`;
|
|
25818
26161
|
let collisionBlock = "";
|
|
25819
26162
|
try {
|
|
25820
|
-
const board = await
|
|
26163
|
+
const board = await target.queryBoard({ status: ["In Progress"] });
|
|
25821
26164
|
const otherInProgress = board.filter((t) => t.id !== result.task.id && t.displayId !== result.task.id).map((t) => ({ taskId: t.displayId || t.id, branch: (t.branchName ?? "").trim() })).filter((t) => t.branch.length > 0);
|
|
25822
26165
|
const collision = detectWorktreeCollision({
|
|
25823
26166
|
taskId: result.task.id,
|
|
@@ -25841,7 +26184,7 @@ async function handleAdHoc(adapter2, config2, args) {
|
|
|
25841
26184
|
} catch {
|
|
25842
26185
|
}
|
|
25843
26186
|
return textResponse(
|
|
25844
|
-
`**${result.task.id}:** "${result.task.title}" held for review (${effortRaw}, ${priorityRaw}, ${typeLabel}, ${taskModule}).${truncateWarning}${promoNote} Build report attached.
|
|
26187
|
+
`**${result.task.id}:** "${result.task.title}" held for review (${effortRaw}, ${priorityRaw}, ${typeLabel}, ${taskModule})${overrideNote}.${truncateWarning}${promoNote} Build report attached.
|
|
25845
26188
|
|
|
25846
26189
|
## Held for the next cycle \u2014 branch + commit, do NOT merge
|
|
25847
26190
|
The task is recorded **In Review** and pinned to **Cycle ${result.task.cycle}**, so the planner won't re-plan it and it rides that cycle's review \u2192 release bundled with planned work.
|
|
@@ -25859,7 +26202,7 @@ _To correct: board_edit ${result.task.id} with updated fields._`
|
|
|
25859
26202
|
);
|
|
25860
26203
|
}
|
|
25861
26204
|
return textResponse(
|
|
25862
|
-
`**${result.task.id}:** "${result.task.title}" recorded (${effortRaw}, ${priorityRaw}, ${typeLabel}, ${taskModule}).${truncateWarning}${promoNote} Build report attached.
|
|
26205
|
+
`**${result.task.id}:** "${result.task.title}" recorded (${effortRaw}, ${priorityRaw}, ${typeLabel}, ${taskModule})${overrideNote}.${truncateWarning}${promoNote} Build report attached.
|
|
25863
26206
|
_To correct: board_edit ${result.task.id} with updated fields._`
|
|
25864
26207
|
);
|
|
25865
26208
|
}
|
|
@@ -29129,18 +29472,7 @@ async function getHierarchyPosition(adapter2, projectId) {
|
|
|
29129
29472
|
return void 0;
|
|
29130
29473
|
}
|
|
29131
29474
|
}
|
|
29132
|
-
|
|
29133
|
-
try {
|
|
29134
|
-
const { stdout } = await execFileAsync2("git", ["describe", "--tags", "--abbrev=0"], {
|
|
29135
|
-
encoding: "utf-8",
|
|
29136
|
-
cwd: projectRoot,
|
|
29137
|
-
timeout: 2e3
|
|
29138
|
-
});
|
|
29139
|
-
return stdout.trim() || null;
|
|
29140
|
-
} catch {
|
|
29141
|
-
return null;
|
|
29142
|
-
}
|
|
29143
|
-
}
|
|
29475
|
+
var GIT_TAG_TIMEOUT_MS = 2e3;
|
|
29144
29476
|
async function checkNpmVersionDrift() {
|
|
29145
29477
|
try {
|
|
29146
29478
|
const pkgPath = join17(new URL(".", import.meta.url).pathname, "..", "..", "package.json");
|
|
@@ -29486,7 +29818,7 @@ async function handleOrient(adapter2, config2, args = {}, clientName) {
|
|
|
29486
29818
|
// Latest git tag + npm version drift (both exec calls with own timeouts).
|
|
29487
29819
|
// task-2172: git-tag stays (the latest tag is part of the core summary);
|
|
29488
29820
|
// version-drift is enrichment, gated behind `full`/deep_housekeeping.
|
|
29489
|
-
tracked("git-tag", () =>
|
|
29821
|
+
tracked("git-tag", async () => getLatestTag(config2.projectRoot, GIT_TAG_TIMEOUT_MS)),
|
|
29490
29822
|
tracked("npm-version-drift", async () => fullEnrichment ? checkNpmVersionDrift() : void 0),
|
|
29491
29823
|
// Research Signals — research docs with pending actions since last strategy review.
|
|
29492
29824
|
// task-2172: heavy (doc search + AD cross-reference) and rarely actioned
|
|
@@ -32045,6 +32377,7 @@ If this is legitimate, reach out at https://getpapi.ai and we'll lift it \u2014
|
|
|
32045
32377
|
}
|
|
32046
32378
|
|
|
32047
32379
|
// src/server.ts
|
|
32380
|
+
var mdModeWarned = false;
|
|
32048
32381
|
var DEFAULT_TOOL_TIMEOUT_MS = parseInt(process.env.PAPI_TOOL_TIMEOUT_MS ?? "30000", 10);
|
|
32049
32382
|
var LONG_TOOL_TIMEOUT_MS = parseInt(process.env.PAPI_LONG_TOOL_TIMEOUT_MS ?? "180000", 10);
|
|
32050
32383
|
var WEDGE_PENDING_FRACTION = Math.min(1, Math.max(0, parseFloat(process.env.PAPI_WEDGE_PENDING_FRACTION ?? "0.6")));
|
|
@@ -32221,7 +32554,8 @@ function createServer(adapter2, config2) {
|
|
|
32221
32554
|
// task-1801: `resources` capability for the PAPI read surface exposed as MCP resources.
|
|
32222
32555
|
{ capabilities: { tools: {}, prompts: {}, resources: {} }, instructions: UNIVERSAL_FRAME }
|
|
32223
32556
|
);
|
|
32224
|
-
if (config2.adapterType === "md") {
|
|
32557
|
+
if (config2.adapterType === "md" && !mdModeWarned) {
|
|
32558
|
+
mdModeWarned = true;
|
|
32225
32559
|
process.stderr.write(
|
|
32226
32560
|
"\n\u26A0 PAPI is running in md mode \u2014 your cycles are not visible on the hosted dashboard.\n Configure DATABASE_URL or sign up at https://getpapi.ai/setup to enable observability.\n\n"
|
|
32227
32561
|
);
|
|
@@ -32548,6 +32882,7 @@ ${usageLine(decision.usage)}`;
|
|
|
32548
32882
|
// src/transport-http.ts
|
|
32549
32883
|
init_proxy_adapter();
|
|
32550
32884
|
import { createServer as createHttpServer } from "http";
|
|
32885
|
+
import { createHash as createHash7 } from "crypto";
|
|
32551
32886
|
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
32552
32887
|
var BEARER_PREFIX = "papi_";
|
|
32553
32888
|
var BEARER_REGEX = /^(papi_|papi_oauth_)[a-f0-9]{64}$/;
|
|
@@ -32577,6 +32912,7 @@ var FRIENDLY_GET_HTML = `<!doctype html>
|
|
|
32577
32912
|
<code>https://mcp.getpapi.ai/mcp</code>.</p>
|
|
32578
32913
|
<a class="btn" href="https://getpapi.ai/docs/install">See the install guide \u2192</a>
|
|
32579
32914
|
</div></body></html>`;
|
|
32915
|
+
var KNOWN_INSTALL_CLIENTS = /* @__PURE__ */ new Set(["claude-code-plugin"]);
|
|
32580
32916
|
var MAX_BODY_BYTES = 1 * 1024 * 1024;
|
|
32581
32917
|
var IP_RATE_WINDOW_MS = 6e4;
|
|
32582
32918
|
var IP_RATE_MAX = 60;
|
|
@@ -32615,7 +32951,7 @@ function corsHeaders(origin) {
|
|
|
32615
32951
|
return {
|
|
32616
32952
|
"Access-Control-Allow-Origin": origin,
|
|
32617
32953
|
"Access-Control-Allow-Methods": "GET, POST, OPTIONS",
|
|
32618
|
-
"Access-Control-Allow-Headers": "Authorization, Content-Type, x-papi-project-id",
|
|
32954
|
+
"Access-Control-Allow-Headers": "Authorization, Content-Type, x-papi-project-id, x-papi-client",
|
|
32619
32955
|
"Vary": "Origin"
|
|
32620
32956
|
};
|
|
32621
32957
|
}
|
|
@@ -32657,6 +32993,48 @@ function sendError(res, err, extraHeaders = {}) {
|
|
|
32657
32993
|
});
|
|
32658
32994
|
res.end(JSON.stringify(err.body));
|
|
32659
32995
|
}
|
|
32996
|
+
function sendUnauthorized(res, reason, extraHeaders = {}) {
|
|
32997
|
+
const challenge = reason === "invalid_token" ? `Bearer realm="papi", error="invalid_token", error_description="The access token was rejected: it has been revoked or has expired.", resource_metadata="${RESOURCE_METADATA_URL}"` : `Bearer realm="papi", resource_metadata="${RESOURCE_METADATA_URL}"`;
|
|
32998
|
+
sendError(
|
|
32999
|
+
res,
|
|
33000
|
+
{ status: 401, body: { error: "Unauthorized", reason } },
|
|
33001
|
+
{ ...extraHeaders, "WWW-Authenticate": challenge }
|
|
33002
|
+
);
|
|
33003
|
+
}
|
|
33004
|
+
var AUTH_VALID_TTL_MS = 5 * 6e4;
|
|
33005
|
+
var AUTH_REJECTED_TTL_MS = 6e4;
|
|
33006
|
+
var AUTH_CACHE_MAX = 5e3;
|
|
33007
|
+
var authVerdicts = /* @__PURE__ */ new Map();
|
|
33008
|
+
function bearerKey(bearer) {
|
|
33009
|
+
return createHash7("sha256").update(bearer).digest("hex");
|
|
33010
|
+
}
|
|
33011
|
+
function readAuthVerdict(bearer, now = Date.now()) {
|
|
33012
|
+
const hit = authVerdicts.get(bearerKey(bearer));
|
|
33013
|
+
if (!hit) return void 0;
|
|
33014
|
+
if (hit.expires <= now) {
|
|
33015
|
+
authVerdicts.delete(bearerKey(bearer));
|
|
33016
|
+
return void 0;
|
|
33017
|
+
}
|
|
33018
|
+
return hit.verdict;
|
|
33019
|
+
}
|
|
33020
|
+
function recordAuthVerdict(bearer, verdict, now = Date.now()) {
|
|
33021
|
+
if (authVerdicts.size >= AUTH_CACHE_MAX) {
|
|
33022
|
+
for (const [k, v] of authVerdicts) {
|
|
33023
|
+
if (v.expires <= now) authVerdicts.delete(k);
|
|
33024
|
+
}
|
|
33025
|
+
if (authVerdicts.size >= AUTH_CACHE_MAX) {
|
|
33026
|
+
const oldest = authVerdicts.keys().next();
|
|
33027
|
+
if (!oldest.done) authVerdicts.delete(oldest.value);
|
|
33028
|
+
}
|
|
33029
|
+
}
|
|
33030
|
+
const ttl = verdict === "valid" ? AUTH_VALID_TTL_MS : AUTH_REJECTED_TTL_MS;
|
|
33031
|
+
authVerdicts.set(bearerKey(bearer), { verdict, expires: now + ttl });
|
|
33032
|
+
}
|
|
33033
|
+
function classifyAuthProbeStatus(status) {
|
|
33034
|
+
if (status === 401) return "rejected";
|
|
33035
|
+
if (status >= 200 && status < 300) return "valid";
|
|
33036
|
+
return void 0;
|
|
33037
|
+
}
|
|
32660
33038
|
function startHttpTransport(opts) {
|
|
32661
33039
|
const { port, host, baseConfig, pkgVersion: pkgVersion2, dataEndpoint } = opts;
|
|
32662
33040
|
const httpServer = createHttpServer((req, res) => {
|
|
@@ -32758,21 +33136,11 @@ function startHttpTransport(opts) {
|
|
|
32758
33136
|
ip,
|
|
32759
33137
|
status: 401
|
|
32760
33138
|
});
|
|
32761
|
-
|
|
32762
|
-
res,
|
|
32763
|
-
{
|
|
32764
|
-
status: 401,
|
|
32765
|
-
body: {
|
|
32766
|
-
error: "Unauthorized",
|
|
32767
|
-
reason: hasHeader ? "malformed_bearer" : "missing_bearer"
|
|
32768
|
-
}
|
|
32769
|
-
},
|
|
32770
|
-
{
|
|
32771
|
-
"WWW-Authenticate": `Bearer realm="papi", resource_metadata="${RESOURCE_METADATA_URL}"`
|
|
32772
|
-
}
|
|
32773
|
-
);
|
|
33139
|
+
sendUnauthorized(res, hasHeader ? "malformed_bearer" : "missing_bearer");
|
|
32774
33140
|
return;
|
|
32775
33141
|
}
|
|
33142
|
+
const clientHeader = req.headers["x-papi-client"];
|
|
33143
|
+
const installClient = typeof clientHeader === "string" && KNOWN_INSTALL_CLIENTS.has(clientHeader) ? clientHeader : "direct";
|
|
32776
33144
|
const projectIdHeader = req.headers["x-papi-project-id"];
|
|
32777
33145
|
const projectId = typeof projectIdHeader === "string" && projectIdHeader.length > 0 ? projectIdHeader : void 0;
|
|
32778
33146
|
if (req.method !== "POST" && req.method !== "GET") {
|
|
@@ -32815,6 +33183,13 @@ function startHttpTransport(opts) {
|
|
|
32815
33183
|
return;
|
|
32816
33184
|
}
|
|
32817
33185
|
}
|
|
33186
|
+
logEvent({
|
|
33187
|
+
level: "info",
|
|
33188
|
+
msg: "mcp_request",
|
|
33189
|
+
ip,
|
|
33190
|
+
bearer_prefix: bearerPrefix(bearer),
|
|
33191
|
+
install_client: installClient
|
|
33192
|
+
});
|
|
32818
33193
|
void dispatchRequest({
|
|
32819
33194
|
req,
|
|
32820
33195
|
res,
|
|
@@ -32895,6 +33270,18 @@ Example: add \`project="${projects[0].slug}"\` to the tool arguments.`;
|
|
|
32895
33270
|
res.writeHead(200, { "Content-Type": "application/json", ...corsHeaders(origin) });
|
|
32896
33271
|
res.end(JSON.stringify(payload));
|
|
32897
33272
|
}
|
|
33273
|
+
function sendProjectUnverifiable(res, origin, body) {
|
|
33274
|
+
if (res.headersSent) return;
|
|
33275
|
+
const id = (body && typeof body === "object" ? body.id : null) ?? null;
|
|
33276
|
+
const text = 'PAPI couldn\'t verify which project this call belongs to \u2014 the project lookup failed, so it is stopping rather than guessing and writing to the wrong project.\n\nRetry in a moment. If it keeps happening, name the project explicitly with `project="<slug>"` in the tool arguments, or set the x-papi-project-id header.';
|
|
33277
|
+
const payload = {
|
|
33278
|
+
jsonrpc: "2.0",
|
|
33279
|
+
id,
|
|
33280
|
+
result: { content: [{ type: "text", text }], isError: true }
|
|
33281
|
+
};
|
|
33282
|
+
res.writeHead(200, { "Content-Type": "application/json", ...corsHeaders(origin) });
|
|
33283
|
+
res.end(JSON.stringify(payload));
|
|
33284
|
+
}
|
|
32898
33285
|
function resolveEffectiveProjectId(body, headerProjectId) {
|
|
32899
33286
|
const explicitProject = extractProjectOverride(body);
|
|
32900
33287
|
try {
|
|
@@ -32904,17 +33291,59 @@ function resolveEffectiveProjectId(body, headerProjectId) {
|
|
|
32904
33291
|
throw err;
|
|
32905
33292
|
}
|
|
32906
33293
|
}
|
|
33294
|
+
async function resolveAuthVerdict(bearer, dataEndpoint) {
|
|
33295
|
+
const cached2 = readAuthVerdict(bearer);
|
|
33296
|
+
if (cached2) return cached2;
|
|
33297
|
+
const probe = new ProxyPapiAdapter({ endpoint: dataEndpoint, apiKey: bearer });
|
|
33298
|
+
const verdict = classifyAuthProbeStatus(await probe.probeBearerStatus());
|
|
33299
|
+
if (verdict) recordAuthVerdict(bearer, verdict);
|
|
33300
|
+
return verdict;
|
|
33301
|
+
}
|
|
32907
33302
|
async function dispatchRequest(args) {
|
|
32908
33303
|
const { req, res, body, bearer, projectId, ip, baseConfig, dataEndpoint } = args;
|
|
33304
|
+
const calledTool = extractToolName(body);
|
|
33305
|
+
if (calledTool !== void 0) {
|
|
33306
|
+
const authVerdict = await resolveAuthVerdict(bearer, dataEndpoint);
|
|
33307
|
+
if (authVerdict === "rejected") {
|
|
33308
|
+
logEvent({
|
|
33309
|
+
level: "warn",
|
|
33310
|
+
msg: "auth_revoked",
|
|
33311
|
+
ip,
|
|
33312
|
+
bearer_prefix: bearerPrefix(bearer),
|
|
33313
|
+
status: 401,
|
|
33314
|
+
reason: "proxy_rejected_bearer"
|
|
33315
|
+
});
|
|
33316
|
+
if (!res.headersSent) {
|
|
33317
|
+
sendUnauthorized(res, "invalid_token", corsHeaders(req.headers.origin));
|
|
33318
|
+
}
|
|
33319
|
+
return;
|
|
33320
|
+
}
|
|
33321
|
+
}
|
|
32909
33322
|
let effectiveProjectId = resolveEffectiveProjectId(body, projectId);
|
|
32910
33323
|
if (effectiveProjectId === void 0) {
|
|
32911
|
-
const toolName =
|
|
33324
|
+
const toolName = calledTool;
|
|
32912
33325
|
if (toolName && !PROJECT_OPTIONAL_TOOLS.has(toolName)) {
|
|
32913
33326
|
let projects = [];
|
|
33327
|
+
let probeFailed = false;
|
|
32914
33328
|
try {
|
|
32915
|
-
const probe = new ProxyPapiAdapter({
|
|
33329
|
+
const probe = new ProxyPapiAdapter({
|
|
33330
|
+
endpoint: dataEndpoint,
|
|
33331
|
+
apiKey: bearer,
|
|
33332
|
+
onAuthRejected: () => recordAuthVerdict(bearer, "rejected")
|
|
33333
|
+
});
|
|
32916
33334
|
projects = await probe.listUserProjects();
|
|
32917
33335
|
} catch {
|
|
33336
|
+
probeFailed = true;
|
|
33337
|
+
}
|
|
33338
|
+
if (probeFailed) {
|
|
33339
|
+
logEvent({
|
|
33340
|
+
level: "warn",
|
|
33341
|
+
msg: "project_probe_failed",
|
|
33342
|
+
ip,
|
|
33343
|
+
bearer_prefix: bearerPrefix(bearer)
|
|
33344
|
+
});
|
|
33345
|
+
sendProjectUnverifiable(res, req.headers.origin, body);
|
|
33346
|
+
return;
|
|
32918
33347
|
}
|
|
32919
33348
|
if (projects.length === 1) {
|
|
32920
33349
|
effectiveProjectId = projects[0].id;
|
|
@@ -32937,10 +33366,14 @@ async function dispatchRequest(args) {
|
|
|
32937
33366
|
}
|
|
32938
33367
|
}
|
|
32939
33368
|
}
|
|
32940
|
-
const adapter2 =
|
|
33369
|
+
const adapter2 = createProxyAdapter({
|
|
32941
33370
|
endpoint: dataEndpoint,
|
|
32942
33371
|
apiKey: bearer,
|
|
32943
|
-
projectId: effectiveProjectId
|
|
33372
|
+
projectId: effectiveProjectId,
|
|
33373
|
+
// task-1773: a 401 raised mid-tool-call cannot change THIS response — the MCP
|
|
33374
|
+
// transport already owns it — but it marks the bearer so the very next request
|
|
33375
|
+
// short-circuits to a 401 + WWW-Authenticate and the client re-authenticates.
|
|
33376
|
+
onAuthRejected: () => recordAuthVerdict(bearer, "rejected")
|
|
32944
33377
|
});
|
|
32945
33378
|
const requestConfig = {
|
|
32946
33379
|
...baseConfig,
|