@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
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, {
|
|
@@ -1353,6 +1361,14 @@ var init_proxy_adapter = __esm({
|
|
|
1353
1361
|
// (1) local-only
|
|
1354
1362
|
"close",
|
|
1355
1363
|
"initRls",
|
|
1364
|
+
// task-3207 (C357): commitBuildComplete, commitReviewSubmit, commitRelease have no
|
|
1365
|
+
// edge case handler and no ALLOWED_METHODS entry — forwarding them 403s at the edge
|
|
1366
|
+
// with no try/catch at the call site, crashing build_execute/review_submit/release
|
|
1367
|
+
// completion for every hosted user. Restores the intended graceful degradation
|
|
1368
|
+
// (separate appendBuildReport + updateTaskStatus calls) until they're atomically wired.
|
|
1369
|
+
"commitBuildComplete",
|
|
1370
|
+
"commitReviewSubmit",
|
|
1371
|
+
"commitRelease",
|
|
1356
1372
|
// (2) not-yet-wired hosted gaps — shrink as data-proxy handlers land (task-2390).
|
|
1357
1373
|
// getToolCallCount + updatePhaseStatus are ABSENT: they now have edge handlers and
|
|
1358
1374
|
// are served through the forwarder (getToolCallCount = SUP-2026-026 handler #1).
|
|
@@ -1398,10 +1414,39 @@ var init_proxy_adapter = __esm({
|
|
|
1398
1414
|
endpoint;
|
|
1399
1415
|
apiKey;
|
|
1400
1416
|
projectId;
|
|
1417
|
+
onAuthRejected;
|
|
1401
1418
|
constructor(config2) {
|
|
1402
1419
|
this.endpoint = config2.endpoint.replace(/\/$/, "");
|
|
1403
1420
|
this.apiKey = config2.apiKey;
|
|
1404
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
|
+
}
|
|
1405
1450
|
}
|
|
1406
1451
|
/** Resolved project ID — available after ensureProject() completes. */
|
|
1407
1452
|
getProjectId() {
|
|
@@ -1418,7 +1463,8 @@ var init_proxy_adapter = __esm({
|
|
|
1418
1463
|
return wrapWithForwarding(new _ProxyPapiAdapter({
|
|
1419
1464
|
endpoint: this.endpoint,
|
|
1420
1465
|
apiKey: this.apiKey,
|
|
1421
|
-
projectId
|
|
1466
|
+
projectId,
|
|
1467
|
+
onAuthRejected: this.onAuthRejected
|
|
1422
1468
|
}));
|
|
1423
1469
|
}
|
|
1424
1470
|
/**
|
|
@@ -1505,6 +1551,7 @@ var init_proxy_adapter = __esm({
|
|
|
1505
1551
|
message = errorBody;
|
|
1506
1552
|
}
|
|
1507
1553
|
if (response.status === 401) {
|
|
1554
|
+
this.onAuthRejected?.();
|
|
1508
1555
|
throw new Error(
|
|
1509
1556
|
`Auth: Invalid API key \u2014 PAPI_DATA_API_KEY was rejected by the proxy.
|
|
1510
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.
|
|
@@ -2083,6 +2130,7 @@ Check PAPI_PROJECT_ID in your .mcp.json config. Find your project ID in the PAPI
|
|
|
2083
2130
|
} catch {
|
|
2084
2131
|
message = errorBody;
|
|
2085
2132
|
}
|
|
2133
|
+
if (response.status === 401) this.onAuthRejected?.();
|
|
2086
2134
|
throw new Error(`Proxy error (${response.status}) on ${route}: ${message}`);
|
|
2087
2135
|
}
|
|
2088
2136
|
const body = await response.json();
|
|
@@ -2200,7 +2248,7 @@ var init_reap_orphans = __esm({
|
|
|
2200
2248
|
}
|
|
2201
2249
|
});
|
|
2202
2250
|
|
|
2203
|
-
//
|
|
2251
|
+
// ../../../../../Users/cathaloullivan/Ai-App-Projects/PapiUI/node_modules/postgres/src/query.js
|
|
2204
2252
|
function cachedError(xs) {
|
|
2205
2253
|
if (originCache.has(xs))
|
|
2206
2254
|
return originCache.get(xs);
|
|
@@ -2212,7 +2260,7 @@ function cachedError(xs) {
|
|
|
2212
2260
|
}
|
|
2213
2261
|
var originCache, originStackCache, originError, CLOSE, Query;
|
|
2214
2262
|
var init_query = __esm({
|
|
2215
|
-
"
|
|
2263
|
+
"../../../../../Users/cathaloullivan/Ai-App-Projects/PapiUI/node_modules/postgres/src/query.js"() {
|
|
2216
2264
|
"use strict";
|
|
2217
2265
|
originCache = /* @__PURE__ */ new Map();
|
|
2218
2266
|
originStackCache = /* @__PURE__ */ new Map();
|
|
@@ -2343,7 +2391,7 @@ var init_query = __esm({
|
|
|
2343
2391
|
}
|
|
2344
2392
|
});
|
|
2345
2393
|
|
|
2346
|
-
//
|
|
2394
|
+
// ../../../../../Users/cathaloullivan/Ai-App-Projects/PapiUI/node_modules/postgres/src/errors.js
|
|
2347
2395
|
function connection(x, options, socket) {
|
|
2348
2396
|
const { host, port } = socket || options;
|
|
2349
2397
|
const error = Object.assign(
|
|
@@ -2381,7 +2429,7 @@ function notSupported(x) {
|
|
|
2381
2429
|
}
|
|
2382
2430
|
var PostgresError, Errors;
|
|
2383
2431
|
var init_errors = __esm({
|
|
2384
|
-
"
|
|
2432
|
+
"../../../../../Users/cathaloullivan/Ai-App-Projects/PapiUI/node_modules/postgres/src/errors.js"() {
|
|
2385
2433
|
"use strict";
|
|
2386
2434
|
PostgresError = class extends Error {
|
|
2387
2435
|
constructor(x) {
|
|
@@ -2399,7 +2447,7 @@ var init_errors = __esm({
|
|
|
2399
2447
|
}
|
|
2400
2448
|
});
|
|
2401
2449
|
|
|
2402
|
-
//
|
|
2450
|
+
// ../../../../../Users/cathaloullivan/Ai-App-Projects/PapiUI/node_modules/postgres/src/types.js
|
|
2403
2451
|
function handleValue(x, parameters, types2, options) {
|
|
2404
2452
|
let value = x instanceof Parameter ? x.value : x;
|
|
2405
2453
|
if (value === void 0) {
|
|
@@ -2514,7 +2562,7 @@ function createJsonTransform(fn) {
|
|
|
2514
2562
|
}
|
|
2515
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;
|
|
2516
2564
|
var init_types = __esm({
|
|
2517
|
-
"
|
|
2565
|
+
"../../../../../Users/cathaloullivan/Ai-App-Projects/PapiUI/node_modules/postgres/src/types.js"() {
|
|
2518
2566
|
"use strict";
|
|
2519
2567
|
init_query();
|
|
2520
2568
|
init_errors();
|
|
@@ -2693,10 +2741,10 @@ var init_types = __esm({
|
|
|
2693
2741
|
}
|
|
2694
2742
|
});
|
|
2695
2743
|
|
|
2696
|
-
//
|
|
2744
|
+
// ../../../../../Users/cathaloullivan/Ai-App-Projects/PapiUI/node_modules/postgres/src/result.js
|
|
2697
2745
|
var Result;
|
|
2698
2746
|
var init_result = __esm({
|
|
2699
|
-
"
|
|
2747
|
+
"../../../../../Users/cathaloullivan/Ai-App-Projects/PapiUI/node_modules/postgres/src/result.js"() {
|
|
2700
2748
|
"use strict";
|
|
2701
2749
|
Result = class extends Array {
|
|
2702
2750
|
constructor() {
|
|
@@ -2716,7 +2764,7 @@ var init_result = __esm({
|
|
|
2716
2764
|
}
|
|
2717
2765
|
});
|
|
2718
2766
|
|
|
2719
|
-
//
|
|
2767
|
+
// ../../../../../Users/cathaloullivan/Ai-App-Projects/PapiUI/node_modules/postgres/src/queue.js
|
|
2720
2768
|
function Queue(initial = []) {
|
|
2721
2769
|
let xs = initial.slice();
|
|
2722
2770
|
let index = 0;
|
|
@@ -2743,13 +2791,13 @@ function Queue(initial = []) {
|
|
|
2743
2791
|
}
|
|
2744
2792
|
var queue_default;
|
|
2745
2793
|
var init_queue = __esm({
|
|
2746
|
-
"
|
|
2794
|
+
"../../../../../Users/cathaloullivan/Ai-App-Projects/PapiUI/node_modules/postgres/src/queue.js"() {
|
|
2747
2795
|
"use strict";
|
|
2748
2796
|
queue_default = Queue;
|
|
2749
2797
|
}
|
|
2750
2798
|
});
|
|
2751
2799
|
|
|
2752
|
-
//
|
|
2800
|
+
// ../../../../../Users/cathaloullivan/Ai-App-Projects/PapiUI/node_modules/postgres/src/bytes.js
|
|
2753
2801
|
function fit(x) {
|
|
2754
2802
|
if (buffer.length - b.i < x) {
|
|
2755
2803
|
const prev = buffer, length = prev.length;
|
|
@@ -2763,7 +2811,7 @@ function reset() {
|
|
|
2763
2811
|
}
|
|
2764
2812
|
var size, buffer, messages, b, bytes_default;
|
|
2765
2813
|
var init_bytes = __esm({
|
|
2766
|
-
"
|
|
2814
|
+
"../../../../../Users/cathaloullivan/Ai-App-Projects/PapiUI/node_modules/postgres/src/bytes.js"() {
|
|
2767
2815
|
"use strict";
|
|
2768
2816
|
size = 256;
|
|
2769
2817
|
buffer = Buffer.allocUnsafe(size);
|
|
@@ -2828,7 +2876,7 @@ var init_bytes = __esm({
|
|
|
2828
2876
|
}
|
|
2829
2877
|
});
|
|
2830
2878
|
|
|
2831
|
-
//
|
|
2879
|
+
// ../../../../../Users/cathaloullivan/Ai-App-Projects/PapiUI/node_modules/postgres/src/connection.js
|
|
2832
2880
|
import net from "net";
|
|
2833
2881
|
import tls from "tls";
|
|
2834
2882
|
import crypto2 from "crypto";
|
|
@@ -3622,7 +3670,7 @@ function timer(fn, seconds) {
|
|
|
3622
3670
|
}
|
|
3623
3671
|
var connection_default, uid, Sync, Flush, SSLRequest, ExecuteUnnamed, DescribeUnnamed, noop, retryRoutines, errorFields;
|
|
3624
3672
|
var init_connection = __esm({
|
|
3625
|
-
"
|
|
3673
|
+
"../../../../../Users/cathaloullivan/Ai-App-Projects/PapiUI/node_modules/postgres/src/connection.js"() {
|
|
3626
3674
|
"use strict";
|
|
3627
3675
|
init_types();
|
|
3628
3676
|
init_errors();
|
|
@@ -3685,7 +3733,7 @@ var init_connection = __esm({
|
|
|
3685
3733
|
}
|
|
3686
3734
|
});
|
|
3687
3735
|
|
|
3688
|
-
//
|
|
3736
|
+
// ../../../../../Users/cathaloullivan/Ai-App-Projects/PapiUI/node_modules/postgres/src/subscribe.js
|
|
3689
3737
|
function Subscribe(postgres2, options) {
|
|
3690
3738
|
const subscribers = /* @__PURE__ */ new Map(), slot = "postgresjs_" + Math.random().toString(36).slice(2), state = {};
|
|
3691
3739
|
let connection2, stream, ended = false;
|
|
@@ -3896,14 +3944,14 @@ function parseEvent(x) {
|
|
|
3896
3944
|
}
|
|
3897
3945
|
var noop2;
|
|
3898
3946
|
var init_subscribe = __esm({
|
|
3899
|
-
"
|
|
3947
|
+
"../../../../../Users/cathaloullivan/Ai-App-Projects/PapiUI/node_modules/postgres/src/subscribe.js"() {
|
|
3900
3948
|
"use strict";
|
|
3901
3949
|
noop2 = () => {
|
|
3902
3950
|
};
|
|
3903
3951
|
}
|
|
3904
3952
|
});
|
|
3905
3953
|
|
|
3906
|
-
//
|
|
3954
|
+
// ../../../../../Users/cathaloullivan/Ai-App-Projects/PapiUI/node_modules/postgres/src/large.js
|
|
3907
3955
|
import Stream2 from "stream";
|
|
3908
3956
|
function largeObject(sql, oid, mode = 131072 | 262144) {
|
|
3909
3957
|
return new Promise(async (resolve4, reject) => {
|
|
@@ -3969,12 +4017,12 @@ function largeObject(sql, oid, mode = 131072 | 262144) {
|
|
|
3969
4017
|
});
|
|
3970
4018
|
}
|
|
3971
4019
|
var init_large = __esm({
|
|
3972
|
-
"
|
|
4020
|
+
"../../../../../Users/cathaloullivan/Ai-App-Projects/PapiUI/node_modules/postgres/src/large.js"() {
|
|
3973
4021
|
"use strict";
|
|
3974
4022
|
}
|
|
3975
4023
|
});
|
|
3976
4024
|
|
|
3977
|
-
//
|
|
4025
|
+
// ../../../../../Users/cathaloullivan/Ai-App-Projects/PapiUI/node_modules/postgres/src/index.js
|
|
3978
4026
|
var src_exports = {};
|
|
3979
4027
|
__export(src_exports, {
|
|
3980
4028
|
default: () => src_default
|
|
@@ -4364,7 +4412,7 @@ function osUsername() {
|
|
|
4364
4412
|
}
|
|
4365
4413
|
var src_default;
|
|
4366
4414
|
var init_src = __esm({
|
|
4367
|
-
"
|
|
4415
|
+
"../../../../../Users/cathaloullivan/Ai-App-Projects/PapiUI/node_modules/postgres/src/index.js"() {
|
|
4368
4416
|
"use strict";
|
|
4369
4417
|
init_types();
|
|
4370
4418
|
init_connection();
|
|
@@ -5259,6 +5307,15 @@ var HELP_FOOTER_MD = `
|
|
|
5259
5307
|
var STRATEGY_REVIEW_OFFER_GAP = 5;
|
|
5260
5308
|
var STRATEGY_REVIEW_BLOCK_GAP = 7;
|
|
5261
5309
|
var ZOOM_OUT_OFFER_GAP = 25;
|
|
5310
|
+
function isDatabaseBackedAdapter(known) {
|
|
5311
|
+
if (known) return known === "pg" || known === "proxy";
|
|
5312
|
+
try {
|
|
5313
|
+
const { adapterType } = loadConfig();
|
|
5314
|
+
return adapterType === "pg" || adapterType === "proxy";
|
|
5315
|
+
} catch {
|
|
5316
|
+
return process.env.PAPI_ADAPTER !== "md";
|
|
5317
|
+
}
|
|
5318
|
+
}
|
|
5262
5319
|
function loadConfig() {
|
|
5263
5320
|
const projectArgIdx = process.argv.indexOf("--project");
|
|
5264
5321
|
const configuredRoot = projectArgIdx !== -1 ? process.argv[projectArgIdx + 1] : process.env.PAPI_PROJECT_DIR;
|
|
@@ -5368,7 +5425,7 @@ Already have an account? Make sure PAPI_USER_ID is set in your .mcp.json env con
|
|
|
5368
5425
|
import path3 from "path";
|
|
5369
5426
|
import { execSync } from "child_process";
|
|
5370
5427
|
|
|
5371
|
-
//
|
|
5428
|
+
// ../../../../../Users/cathaloullivan/Ai-App-Projects/PapiUI/packages/adapter-md/dist/index.js
|
|
5372
5429
|
import { readFile, writeFile, access } from "fs/promises";
|
|
5373
5430
|
import { randomUUID as randomUUID6 } from "crypto";
|
|
5374
5431
|
import { join } from "path";
|
|
@@ -5630,9 +5687,12 @@ function parsePlanningLog(content, activeDecisionsContent, cycleLogContent) {
|
|
|
5630
5687
|
var VALID_EFFORT_SIZES = /* @__PURE__ */ new Set(["XS", "S", "M", "L", "XL"]);
|
|
5631
5688
|
var SECTION_HEADERS = [
|
|
5632
5689
|
"SCOPE (DO THIS)",
|
|
5690
|
+
"WHY NOT SIMPLER",
|
|
5633
5691
|
"SCOPE BOUNDARY (DO NOT DO THIS)",
|
|
5634
5692
|
"ACCEPTANCE CRITERIA",
|
|
5693
|
+
"PRE-MORTEM",
|
|
5635
5694
|
"SECURITY CONSIDERATIONS",
|
|
5695
|
+
"DEPLOY VERIFICATION",
|
|
5636
5696
|
"PRE-BUILD VERIFICATION",
|
|
5637
5697
|
"FILES LIKELY TOUCHED",
|
|
5638
5698
|
"EFFORT"
|
|
@@ -5671,7 +5731,7 @@ function parseBulletsOnly(text) {
|
|
|
5671
5731
|
return text.split("\n").filter((l) => /^\s*-\s/.test(l)).map((l) => l.replace(/^\s*-\s*/, "").trim()).filter((l) => l.length > 0);
|
|
5672
5732
|
}
|
|
5673
5733
|
function parseChecklist(text) {
|
|
5674
|
-
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);
|
|
5675
5735
|
}
|
|
5676
5736
|
function parseBuildHandoff(markdown) {
|
|
5677
5737
|
if (typeof markdown !== "string" || !markdown.trim()) return null;
|
|
@@ -6321,9 +6381,59 @@ var EFFORT_SCALE = {
|
|
|
6321
6381
|
XL: 5
|
|
6322
6382
|
};
|
|
6323
6383
|
function effortOrdinal(effort) {
|
|
6384
|
+
if (typeof effort !== "string") return void 0;
|
|
6324
6385
|
const normalized = effort.trim().toUpperCase();
|
|
6325
6386
|
return EFFORT_SCALE[normalized];
|
|
6326
6387
|
}
|
|
6388
|
+
function isUnparsedEffort(effort) {
|
|
6389
|
+
if (typeof effort !== "string" || effort.trim().length === 0) return false;
|
|
6390
|
+
return effortOrdinal(effort) === void 0;
|
|
6391
|
+
}
|
|
6392
|
+
function calculateCycleMetrics(reports, currentCycle, window = 5) {
|
|
6393
|
+
const recentReports = reports.filter(
|
|
6394
|
+
(r) => r.cycle > currentCycle - window && r.cycle <= currentCycle
|
|
6395
|
+
);
|
|
6396
|
+
const unparsedEffortCount = recentReports.filter(
|
|
6397
|
+
(r) => isUnparsedEffort(r.actualEffort) || isUnparsedEffort(r.estimatedEffort)
|
|
6398
|
+
).length;
|
|
6399
|
+
const perCycle = /* @__PURE__ */ new Map();
|
|
6400
|
+
for (const r of recentReports) {
|
|
6401
|
+
const group = perCycle.get(r.cycle) ?? [];
|
|
6402
|
+
group.push(r);
|
|
6403
|
+
perCycle.set(r.cycle, group);
|
|
6404
|
+
}
|
|
6405
|
+
const accuracy = [];
|
|
6406
|
+
const velocity = [];
|
|
6407
|
+
const sortedCycles = [...perCycle.keys()].sort((a, b2) => a - b2);
|
|
6408
|
+
for (const cycle of sortedCycles) {
|
|
6409
|
+
const reps = perCycle.get(cycle);
|
|
6410
|
+
const deltas = [];
|
|
6411
|
+
for (const r of reps) {
|
|
6412
|
+
const actual = effortOrdinal(r.actualEffort);
|
|
6413
|
+
const estimated = effortOrdinal(r.estimatedEffort);
|
|
6414
|
+
if (actual !== void 0 && estimated !== void 0) {
|
|
6415
|
+
deltas.push(actual - estimated);
|
|
6416
|
+
}
|
|
6417
|
+
}
|
|
6418
|
+
if (deltas.length > 0) {
|
|
6419
|
+
accuracy.push({
|
|
6420
|
+
cycle,
|
|
6421
|
+
reports: deltas.length,
|
|
6422
|
+
matchRate: Math.round(deltas.filter((d) => d === 0).length / deltas.length * 100),
|
|
6423
|
+
mae: Math.round(deltas.reduce((s, d) => s + Math.abs(d), 0) / deltas.length * 10) / 10,
|
|
6424
|
+
bias: Math.round(deltas.reduce((s, d) => s + d, 0) / deltas.length * 10) / 10
|
|
6425
|
+
});
|
|
6426
|
+
}
|
|
6427
|
+
velocity.push({
|
|
6428
|
+
cycle,
|
|
6429
|
+
completed: reps.filter((r) => r.completed === "Yes").length,
|
|
6430
|
+
partial: reps.filter((r) => r.completed === "Partial").length,
|
|
6431
|
+
failed: reps.filter((r) => r.completed === "No").length,
|
|
6432
|
+
effortPoints: reps.reduce((s, r) => s + (effortOrdinal(r.actualEffort) ?? 0), 0)
|
|
6433
|
+
});
|
|
6434
|
+
}
|
|
6435
|
+
return { accuracy, velocity, unparsedEffortCount };
|
|
6436
|
+
}
|
|
6327
6437
|
function serializeAccuracyRow(a) {
|
|
6328
6438
|
return `| ${a.cycle} | ${a.reports} | ${a.matchRate}% | ${a.mae} | ${a.bias >= 0 ? "+" : ""}${a.bias} |`;
|
|
6329
6439
|
}
|
|
@@ -7972,6 +8082,7 @@ async function createAdapter(optionsOrType, maybePapiDir) {
|
|
|
7972
8082
|
case "pg": {
|
|
7973
8083
|
const { PgAdapter, PgPapiAdapter, configFromEnv } = await import("@papi-ai/adapter-pg");
|
|
7974
8084
|
let projectId = process.env["PAPI_PROJECT_ID"];
|
|
8085
|
+
const projectIdWasPreSupplied = Boolean(projectId);
|
|
7975
8086
|
const projectRoot = options.projectRoot ?? process.env["PAPI_PROJECT_DIR"] ?? process.cwd();
|
|
7976
8087
|
let rootHash = null;
|
|
7977
8088
|
let originUrl = null;
|
|
@@ -8035,6 +8146,26 @@ async function createAdapter(optionsOrType, maybePapiDir) {
|
|
|
8035
8146
|
}
|
|
8036
8147
|
const config2 = papiEndpoint ? { connectionString: papiEndpoint } : configFromEnv();
|
|
8037
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
|
+
}
|
|
8038
8169
|
const { ensureSchema } = await import("@papi-ai/adapter-pg");
|
|
8039
8170
|
try {
|
|
8040
8171
|
await ensureSchema(config2);
|
|
@@ -8402,6 +8533,24 @@ function formatBuildReports(reports, opts) {
|
|
|
8402
8533
|
_\u2026and ${reports.length - capped.length} older build report(s) omitted to bound context size._` : "";
|
|
8403
8534
|
return body + omitted;
|
|
8404
8535
|
}
|
|
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, " ");
|
|
8539
|
+
const own = report.taskId?.toLowerCase();
|
|
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();
|
|
8548
|
+
for (const m of prose.matchAll(/\btask-\d+\b/gi)) {
|
|
8549
|
+
const id = m[0].toLowerCase();
|
|
8550
|
+
if (id !== own && !resolves.has(id)) mentions.add(id);
|
|
8551
|
+
}
|
|
8552
|
+
return { resolves: [...resolves].sort(), mentions: [...mentions].sort() };
|
|
8553
|
+
}
|
|
8405
8554
|
function formatRecentlyShippedCapabilities(reports) {
|
|
8406
8555
|
const completed = reports.filter((r) => r.completed === "Yes" || r.completed === "Partial");
|
|
8407
8556
|
if (completed.length === 0) return void 0;
|
|
@@ -8416,13 +8565,63 @@ function formatRecentlyShippedCapabilities(reports) {
|
|
|
8416
8565
|
}
|
|
8417
8566
|
return parts.join("\n");
|
|
8418
8567
|
});
|
|
8419
|
-
|
|
8568
|
+
const resolvedBy = /* @__PURE__ */ new Map();
|
|
8569
|
+
const namedBy = /* @__PURE__ */ new Map();
|
|
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
|
+
};
|
|
8576
|
+
for (const r of completed) {
|
|
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) {
|
|
8583
|
+
if (completedIds.has(ref)) continue;
|
|
8584
|
+
record(namedBy, ref, r.taskId);
|
|
8585
|
+
}
|
|
8586
|
+
}
|
|
8587
|
+
for (const ref of resolvedBy.keys()) namedBy.delete(ref);
|
|
8588
|
+
const out = [
|
|
8420
8589
|
`${completed.length} task(s) completed in recent cycles:`,
|
|
8421
8590
|
"",
|
|
8422
8591
|
...lines,
|
|
8423
8592
|
"",
|
|
8424
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."
|
|
8425
|
-
]
|
|
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
|
+
}
|
|
8608
|
+
if (namedBy.size > 0) {
|
|
8609
|
+
out.push(
|
|
8610
|
+
"",
|
|
8611
|
+
"### \u26A0 Named by a shipped task \u2014 VERIFY BEFORE SCHEDULING",
|
|
8612
|
+
"",
|
|
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.",
|
|
8620
|
+
"",
|
|
8621
|
+
...[...namedBy.entries()].sort(([a], [b2]) => a.localeCompare(b2)).map(([ref, namers]) => `- **${ref}** \u2014 named by ${namers.join(", ")}`)
|
|
8622
|
+
);
|
|
8623
|
+
}
|
|
8624
|
+
return out.join("\n");
|
|
8426
8625
|
}
|
|
8427
8626
|
function formatCycleLog(entries) {
|
|
8428
8627
|
if (entries.length === 0) return "No cycle log entries yet.";
|
|
@@ -8669,13 +8868,14 @@ function computeSnapshotsFromBuildReports(reports, tasks) {
|
|
|
8669
8868
|
const cycleReports = reportsByCycle.get(sn) ?? [];
|
|
8670
8869
|
const cycleTaskRows = tasksByCycle.get(sn);
|
|
8671
8870
|
const withEffort = cycleReports.filter((r) => r.estimatedEffort && r.actualEffort);
|
|
8672
|
-
const
|
|
8673
|
-
const matchRate = withEffort.length > 0 ? Math.round(accurate / withEffort.length * 100) : 0;
|
|
8871
|
+
const [computedAccuracy] = calculateCycleMetrics(withEffort, sn, 1).accuracy;
|
|
8674
8872
|
const { completed, total, plannedPoints, deliveredPoints } = computeCycleEffort(cycleTaskRows, cycleReports);
|
|
8675
8873
|
snapshots.push({
|
|
8676
8874
|
cycle: sn,
|
|
8677
8875
|
date: (/* @__PURE__ */ new Date()).toISOString(),
|
|
8678
|
-
|
|
8876
|
+
// No report in this cycle carried BOTH an estimate and an actual, so there
|
|
8877
|
+
// is genuinely nothing to measure. Zeros here mean "no data", not "no bias".
|
|
8878
|
+
accuracy: [computedAccuracy ?? { cycle: sn, reports: 0, matchRate: 0, mae: 0, bias: 0 }],
|
|
8679
8879
|
velocity: [{
|
|
8680
8880
|
cycle: sn,
|
|
8681
8881
|
completed,
|
|
@@ -9196,7 +9396,7 @@ var PLAN_FRAGMENT_RESEARCH = `
|
|
|
9196
9396
|
var PLAN_FRAGMENT_BUG = `
|
|
9197
9397
|
**Bug task detection:** When a task's task type is "bug" or the title starts with "Bug:" or "Fix:", apply these rules:
|
|
9198
9398
|
- **Auto-P1:** If the task's current priority is P2 or lower, upgrade it to "P1 High" via a boardCorrections entry in Part 2. Note the upgrade in Part 1 analysis.
|
|
9199
|
-
-
|
|
9399
|
+
- Inside SCOPE (DO THIS), use these bug-specific subsections. KEEP the "SCOPE (DO THIS)" header and put these subsections INSIDE it \u2014 the handoff parser only recognises the standard headers (packages/adapter-md build-handoff.ts SECTION_HEADERS) and a handoff with no SCOPE section is rejected outright (task-3050, C357):
|
|
9200
9400
|
- **REPRODUCE:** Exact steps to reproduce the bug before touching any code. If the task notes describe the symptoms, include them. If not, the first build step is "confirm the bug reproduces."
|
|
9201
9401
|
- **ROOT CAUSE:** One-sentence hypothesis for the root cause (what is wrong, not what the user sees). The builder must confirm or correct this before implementing a fix.
|
|
9202
9402
|
- **MINIMAL FIX:** The smallest code change that resolves the root cause. "Bug fix \u2014 minimal blast radius. Change only what is necessary. Do not refactor surrounding code or expand scope."
|
|
@@ -9221,7 +9421,7 @@ var PLAN_FRAGMENT_SPIKE = `
|
|
|
9221
9421
|
- Keep SCOPE BOUNDARY, SECURITY CONSIDERATIONS, and PRE-BUILD VERIFICATION as normal.
|
|
9222
9422
|
- Spikes should be estimated conservatively: XS or S. If a spike needs M+ effort, it's not a spike \u2014 reclassify as a research task.`;
|
|
9223
9423
|
var PLAN_FRAGMENT_DESIGN_BRIEF = `
|
|
9224
|
-
**Design brief task detection:** When a task's task type is "design-brief", generate a DESIGN BRIEF handoff.
|
|
9424
|
+
**Design brief task detection:** When a task's task type is "design-brief", generate a DESIGN BRIEF handoff. Inside SCOPE (DO THIS), use these type-specific subsections. KEEP the "SCOPE (DO THIS)" header and put these subsections INSIDE it \u2014 the handoff parser only recognises the standard headers (packages/adapter-md build-handoff.ts SECTION_HEADERS) and a handoff with no SCOPE section is rejected outright (task-3050, C357):
|
|
9225
9425
|
- AUDIENCE: Who this design is for \u2014 persona and context of use (e.g. "non-technical Owner, first dashboard visit")
|
|
9226
9426
|
- BRAND CONSTRAINTS: Palette, typography, tone \u2014 pull from \`.impeccable.md\` (dev patterns, anti-patterns, component rules) AND \`docs/branding/brand-book.html\` (brand identity, positioning, voice canon) if present. If neither exists, state "No brand doc \u2014 Owner should define constraints before starting."
|
|
9227
9427
|
- DELIVERABLE FORMAT: What the output looks like \u2014 design handoff package / annotated mockup / style spec. Be specific so the person doing the work knows what "done" means.
|
|
@@ -9229,7 +9429,7 @@ var PLAN_FRAGMENT_DESIGN_BRIEF = `
|
|
|
9229
9429
|
Keep SCOPE BOUNDARY, ACCEPTANCE CRITERIA, SECURITY CONSIDERATIONS, and PRE-BUILD VERIFICATION sections as normal.
|
|
9230
9430
|
Add to ACCEPTANCE CRITERIA: "[ ] Deliverable format confirmed with Owner before starting" and "[ ] Design output is self-contained \u2014 includes enough context for a developer to implement without further clarification."`;
|
|
9231
9431
|
var PLAN_FRAGMENT_RESEARCH_BRIEF = `
|
|
9232
|
-
**Research brief task detection:** When a task's task type is "research-brief", generate a RESEARCH BRIEF handoff.
|
|
9432
|
+
**Research brief task detection:** When a task's task type is "research-brief", generate a RESEARCH BRIEF handoff. Inside SCOPE (DO THIS), use these subsections. KEEP the "SCOPE (DO THIS)" header and put these subsections INSIDE it \u2014 the handoff parser only recognises the standard headers (packages/adapter-md build-handoff.ts SECTION_HEADERS) and a handoff with no SCOPE section is rejected outright (task-3050, C357):
|
|
9233
9433
|
- GOAL: The specific question this research answers \u2014 one sentence, phrased as a question (e.g. "What onboarding patterns do our top 3 competitors use?")
|
|
9234
9434
|
- TIME-BOX: Maximum effort allowed \u2014 XS or S. Stop when the time-box is hit and report what was found, even if incomplete.
|
|
9235
9435
|
- OUTPUT: Where findings land \u2014 a doc at \`docs/research/[topic]-findings.md\` or inline in the build report. State the path.
|
|
@@ -9237,7 +9437,7 @@ var PLAN_FRAGMENT_RESEARCH_BRIEF = `
|
|
|
9237
9437
|
Keep SCOPE BOUNDARY, ACCEPTANCE CRITERIA, SECURITY CONSIDERATIONS, and PRE-BUILD VERIFICATION as normal.
|
|
9238
9438
|
Add to ACCEPTANCE CRITERIA: "[ ] Question answered OR time-box hit \u2014 whichever comes first" and "[ ] Findings doc saved before any follow-up tasks are submitted."`;
|
|
9239
9439
|
var PLAN_FRAGMENT_MARKETING_BRIEF = `
|
|
9240
|
-
**Marketing brief task detection:** When a task's task type is "marketing-brief", generate a MARKETING BRIEF handoff.
|
|
9440
|
+
**Marketing brief task detection:** When a task's task type is "marketing-brief", generate a MARKETING BRIEF handoff. Inside SCOPE (DO THIS), use these subsections. KEEP the "SCOPE (DO THIS)" header and put these subsections INSIDE it \u2014 the handoff parser only recognises the standard headers (packages/adapter-md build-handoff.ts SECTION_HEADERS) and a handoff with no SCOPE section is rejected outright (task-3050, C357):
|
|
9241
9441
|
- AUDIENCE: Who this marketing content targets \u2014 persona, awareness level, channel context (e.g. "cold Discord visitor, zero PAPI context")
|
|
9242
9442
|
- CHANNEL: Where this content lives \u2014 Discord, landing page, email, social, etc.
|
|
9243
9443
|
- MESSAGE FRAME: The core message to land \u2014 one sentence. What does the reader need to believe after seeing this? (e.g. "PAPI makes AI-assisted building systematic, not chaotic.")
|
|
@@ -9245,7 +9445,7 @@ var PLAN_FRAGMENT_MARKETING_BRIEF = `
|
|
|
9245
9445
|
Keep SCOPE BOUNDARY, ACCEPTANCE CRITERIA, SECURITY CONSIDERATIONS, and PRE-BUILD VERIFICATION as normal.
|
|
9246
9446
|
Add to ACCEPTANCE CRITERIA: "[ ] Message Frame confirmed with Owner before drafting" and "[ ] Final content reviewed by Owner before publishing."`;
|
|
9247
9447
|
var PLAN_FRAGMENT_OPS_BRIEF = `
|
|
9248
|
-
**Ops brief task detection:** When a task's task type is "ops-brief", generate an OPS BRIEF handoff.
|
|
9448
|
+
**Ops brief task detection:** When a task's task type is "ops-brief", generate an OPS BRIEF handoff. Inside SCOPE (DO THIS), use these subsections. KEEP the "SCOPE (DO THIS)" header and put these subsections INSIDE it \u2014 the handoff parser only recognises the standard headers (packages/adapter-md build-handoff.ts SECTION_HEADERS) and a handoff with no SCOPE section is rejected outright (task-3050, C357):
|
|
9249
9449
|
- SYSTEM: Which system or service this ops task touches \u2014 Vercel, Railway, Supabase, GitHub Actions, DNS, etc.
|
|
9250
9450
|
- RISK: What could go wrong \u2014 data loss, downtime, broken deployments. Include estimated blast radius (e.g. "affects all authenticated users").
|
|
9251
9451
|
- ROLLBACK PLAN: Exact steps to undo the change if something breaks. Must be specific enough to execute under pressure.
|
|
@@ -9738,6 +9938,17 @@ function coerceToString(value) {
|
|
|
9738
9938
|
if (value === null || value === void 0) return "";
|
|
9739
9939
|
return JSON.stringify(value, null, 2);
|
|
9740
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
|
+
}
|
|
9741
9952
|
function coerceStructuredOutput(parsed) {
|
|
9742
9953
|
const cycleHandoffs = Array.isArray(parsed.cycleHandoffs) ? parsed.cycleHandoffs.map((h) => {
|
|
9743
9954
|
const { taskId: _t, buildHandoff: _b, ...rest } = h;
|
|
@@ -9777,10 +9988,12 @@ function coerceStructuredOutput(parsed) {
|
|
|
9777
9988
|
body: coerceToString(ad.body)
|
|
9778
9989
|
})) : [];
|
|
9779
9990
|
const cycleTaskIds = Array.isArray(parsed.cycleTaskIds) ? parsed.cycleTaskIds.map((id) => coerceToString(id)) : void 0;
|
|
9991
|
+
const carryForward = coerceCarryForward(parsed.cycleLogCarryForward);
|
|
9780
9992
|
return {
|
|
9781
9993
|
cycleLogTitle: coerceToString(parsed.cycleLogTitle),
|
|
9782
9994
|
cycleLogContent: coerceToString(parsed.cycleLogContent),
|
|
9783
|
-
cycleLogCarryForward:
|
|
9995
|
+
cycleLogCarryForward: carryForward.value,
|
|
9996
|
+
...carryForward.warning ? { coercionWarnings: [carryForward.warning] } : {},
|
|
9784
9997
|
cycleLogNotes: parsed.cycleLogNotes === null ? null : coerceToString(parsed.cycleLogNotes),
|
|
9785
9998
|
nextMode: "Full",
|
|
9786
9999
|
boardHealth: coerceToString(parsed.boardHealth),
|
|
@@ -10888,8 +11101,9 @@ async function applyHandoffs(adapter2, rawLlmOutput, cycleNumber, force = false)
|
|
|
10888
11101
|
}
|
|
10889
11102
|
const invalidFields = validateHandoffScope(parsed);
|
|
10890
11103
|
if (invalidFields.length > 0) {
|
|
11104
|
+
const scopeMissing = invalidFields.includes("scope");
|
|
10891
11105
|
warnings.push(
|
|
10892
|
-
`Rejected handoff for ${handoff.taskId}: missing or empty ${invalidFields.join(", ")}. Handoffs without explicit scope produce ambiguous builds.`
|
|
11106
|
+
`Rejected handoff for ${handoff.taskId}: missing or empty ${invalidFields.join(", ")}. Handoffs without explicit scope produce ambiguous builds.` + (scopeMissing ? ` If this is a bug/design-brief/research-brief/marketing-brief/ops-brief task, KEEP the "SCOPE (DO THIS)" header and nest the type-specific sections (REPRODUCE / ROOT CAUSE / MINIMAL FIX / \u2026) inside it \u2014 the parser only recognises the standard headers, so replacing SCOPE outright drops it entirely.` : "")
|
|
10893
11107
|
);
|
|
10894
11108
|
continue;
|
|
10895
11109
|
}
|
|
@@ -12894,7 +13108,7 @@ async function assertSingleActiveCycle(adapter2, opts = {}) {
|
|
|
12894
13108
|
}
|
|
12895
13109
|
return notes;
|
|
12896
13110
|
}
|
|
12897
|
-
async function validateAndPrepare(adapter2, force, callerUserId) {
|
|
13111
|
+
async function validateAndPrepare(adapter2, force, callerUserId, adapterType) {
|
|
12898
13112
|
let mode;
|
|
12899
13113
|
let cycleNumber;
|
|
12900
13114
|
let strategyReviewWarning = "";
|
|
@@ -12956,7 +13170,7 @@ Run \`strategy_review\` first, or pass \`force: true\` to bypass this gate.`
|
|
|
12956
13170
|
if (err instanceof Error && (err.message.startsWith("Strategy Review") || err.message.startsWith("Cycle ") || err.message.startsWith("Stale reviews"))) {
|
|
12957
13171
|
throw err;
|
|
12958
13172
|
}
|
|
12959
|
-
const isPg =
|
|
13173
|
+
const isPg = isDatabaseBackedAdapter(adapterType);
|
|
12960
13174
|
throw new Error(
|
|
12961
13175
|
isPg ? "Could not read cycle health from the database. Check your DATABASE_URL and verify the project exists." : "Could not read cycle health from PLANNING_LOG.md. Run setup first to initialise your PAPI project."
|
|
12962
13176
|
);
|
|
@@ -13000,8 +13214,9 @@ async function processLlmOutput(adapter2, config2, rawOutput, mode, cycleNumber,
|
|
|
13000
13214
|
contextHashes,
|
|
13001
13215
|
{ confirmCancellations: planRunMeta?.confirmCancellations === true, ownerUserId: applyScope.callerUserId ?? void 0 }
|
|
13002
13216
|
);
|
|
13003
|
-
|
|
13004
|
-
|
|
13217
|
+
const allWarnings = [...data.coercionWarnings ?? [], ...wbWarnings];
|
|
13218
|
+
if (allWarnings.length > 0) {
|
|
13219
|
+
writeBackWarnings = allWarnings;
|
|
13005
13220
|
}
|
|
13006
13221
|
if (skipped.length > 0) {
|
|
13007
13222
|
skippedCancellations = skipped;
|
|
@@ -13074,7 +13289,7 @@ async function preparePlan(adapter2, config2, filters, focus, force, handoffsOnl
|
|
|
13074
13289
|
tracker?.mark("validate_and_prepare");
|
|
13075
13290
|
let t = startTimer();
|
|
13076
13291
|
const prepareScope = await resolvePlanScope(adapter2, config2);
|
|
13077
|
-
const { mode, cycleNumber, strategyReviewWarning } = await validateAndPrepare(adapter2, force, prepareScope.callerUserId);
|
|
13292
|
+
const { mode, cycleNumber, strategyReviewWarning } = await validateAndPrepare(adapter2, force, prepareScope.callerUserId, config2.adapterType);
|
|
13078
13293
|
const validateMs = t();
|
|
13079
13294
|
const incomingCycle = cycleNumber + 1;
|
|
13080
13295
|
tracker?.setStreamScope({ cycle: incomingCycle });
|
|
@@ -15683,6 +15898,65 @@ function extractDecisionEvidence(ad, eventType, warnings) {
|
|
|
15683
15898
|
}
|
|
15684
15899
|
return { evidenceRef, metricDelta };
|
|
15685
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
|
+
}
|
|
15686
15960
|
async function writeBack2(adapter2, cycleNumber, data, fullAnalysis, warnings) {
|
|
15687
15961
|
const cleanTitle = data.sessionLogTitle.replace(/^(?:Cycle|Session)\s+\d+\s*—\s*/i, "").trim();
|
|
15688
15962
|
const cleanContent = data.sessionLogContent.replace(/^#{1,3}\s+(?:Cycle|Session)\s+\d+\s*—[^\n]*\n*/i, "").trim();
|
|
@@ -15724,34 +15998,7 @@ ${cleanContent}`;
|
|
|
15724
15998
|
} catch {
|
|
15725
15999
|
}
|
|
15726
16000
|
if (data.activeDecisionUpdates && data.activeDecisionUpdates.length > 0) {
|
|
15727
|
-
await
|
|
15728
|
-
if (ad.action === "delete" && adapter2.deleteActiveDecision) {
|
|
15729
|
-
await adapter2.deleteActiveDecision(ad.id);
|
|
15730
|
-
} else if (ad.action === "new" && adapter2.upsertActiveDecision) {
|
|
15731
|
-
const titleMatch = ad.body.match(/^###\s+\S+:\s*([^\n[]+?)(?:\s*\[|$)/m);
|
|
15732
|
-
const title = titleMatch ? titleMatch[1].trim() : ad.id;
|
|
15733
|
-
const confidenceMatch = ad.body.match(/\[Confidence:\s*(HIGH|MEDIUM|LOW)\]/i);
|
|
15734
|
-
const confidence = confidenceMatch ? confidenceMatch[1].toUpperCase() : "MEDIUM";
|
|
15735
|
-
await adapter2.upsertActiveDecision(ad.id, ad.body, title, confidence, cycleNumber);
|
|
15736
|
-
} else {
|
|
15737
|
-
await adapter2.updateActiveDecision(ad.id, ad.body, cycleNumber, ad.action);
|
|
15738
|
-
}
|
|
15739
|
-
const eventType = ad.action === "delete" ? "invalidated" : ad.action === "confidence_change" ? "confidence_changed" : ad.action === "supersede" ? "superseded" : ad.action === "new" ? "created" : "modified";
|
|
15740
|
-
const evidence = extractDecisionEvidence(ad, eventType, warnings);
|
|
15741
|
-
try {
|
|
15742
|
-
await adapter2.appendDecisionEvent({
|
|
15743
|
-
decisionId: ad.id,
|
|
15744
|
-
eventType,
|
|
15745
|
-
cycle: cycleNumber,
|
|
15746
|
-
source: "strategy_review",
|
|
15747
|
-
sourceRef: `cycle-${cycleNumber}-review`,
|
|
15748
|
-
detail: `Action: ${ad.action}`,
|
|
15749
|
-
evidenceRef: evidence.evidenceRef,
|
|
15750
|
-
metricDelta: evidence.metricDelta
|
|
15751
|
-
});
|
|
15752
|
-
} catch {
|
|
15753
|
-
}
|
|
15754
|
-
}));
|
|
16001
|
+
await applyDecisionUpdates(adapter2, data.activeDecisionUpdates, cycleNumber, warnings);
|
|
15755
16002
|
}
|
|
15756
16003
|
try {
|
|
15757
16004
|
if (adapter2.confirmPendingActiveDecisions) {
|
|
@@ -16007,7 +16254,7 @@ async function prepareStrategyReview(adapter2, force, projectRoot, adapterType,
|
|
|
16007
16254
|
};
|
|
16008
16255
|
}
|
|
16009
16256
|
} catch {
|
|
16010
|
-
const isPg =
|
|
16257
|
+
const isPg = isDatabaseBackedAdapter(adapterType);
|
|
16011
16258
|
throw new Error(
|
|
16012
16259
|
isPg ? "Could not read cycle health from the database. Check your DATABASE_URL and verify the project exists." : "Could not read cycle health from PLANNING_LOG.md. Run setup first to initialise your PAPI project."
|
|
16013
16260
|
);
|
|
@@ -16451,13 +16698,13 @@ ${cleanContent}`;
|
|
|
16451
16698
|
${evidenceWarnings.map((w) => `- ${w}`).join("\n")}` : displayText;
|
|
16452
16699
|
return { cycleNumber, displayText: fullText, writeBackFailed };
|
|
16453
16700
|
}
|
|
16454
|
-
async function prepareStrategyChange(adapter2, text) {
|
|
16701
|
+
async function prepareStrategyChange(adapter2, text, adapterType) {
|
|
16455
16702
|
let cycleNumber;
|
|
16456
16703
|
try {
|
|
16457
16704
|
const health = await adapter2.getCycleHealth();
|
|
16458
16705
|
cycleNumber = health.totalCycles;
|
|
16459
16706
|
} catch {
|
|
16460
|
-
const isPg =
|
|
16707
|
+
const isPg = isDatabaseBackedAdapter(adapterType);
|
|
16461
16708
|
throw new Error(
|
|
16462
16709
|
isPg ? "Could not read cycle health from the database. Check your DATABASE_URL and verify the project exists." : "Could not read cycle health from PLANNING_LOG.md. Run setup first to initialise your PAPI project."
|
|
16463
16710
|
);
|
|
@@ -16509,37 +16756,45 @@ async function prepareStrategyChange(adapter2, text) {
|
|
|
16509
16756
|
async function applyStrategyChangeOutput(adapter2, rawLlmOutput, cycleNumber) {
|
|
16510
16757
|
return processStrategyChangeOutput(adapter2, rawLlmOutput, cycleNumber);
|
|
16511
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
|
+
}
|
|
16512
16767
|
async function captureDecision(adapter2, input) {
|
|
16513
16768
|
const health = await adapter2.getCycleHealth();
|
|
16514
16769
|
const cycleNumber = health.totalCycles;
|
|
16515
|
-
|
|
16516
|
-
|
|
16517
|
-
|
|
16518
|
-
|
|
16519
|
-
|
|
16520
|
-
|
|
16521
|
-
|
|
16522
|
-
|
|
16523
|
-
|
|
16524
|
-
|
|
16525
|
-
|
|
16526
|
-
|
|
16527
|
-
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
|
+
}
|
|
16528
16782
|
}
|
|
16529
16783
|
if (input.confidenceOnly) {
|
|
16530
16784
|
if (!input.adId) {
|
|
16531
16785
|
throw new Error('confidence_only requires adId \u2014 provide the AD to update (e.g. "AD-12")');
|
|
16532
16786
|
}
|
|
16787
|
+
const adId2 = input.adId;
|
|
16533
16788
|
if (adapter2.upsertActiveDecision) {
|
|
16534
16789
|
const existing = await adapter2.getActiveDecisions({ includeRetired: false });
|
|
16535
|
-
const current = existing.find((d) => d.id ===
|
|
16790
|
+
const current = existing.find((d) => d.id === adId2);
|
|
16536
16791
|
const preservedBody = current?.body ?? `- **Decision:** ${input.text}`;
|
|
16537
16792
|
const preservedTitle = current?.title ?? input.text.slice(0, 80);
|
|
16538
|
-
await adapter2.upsertActiveDecision(
|
|
16793
|
+
await adapter2.upsertActiveDecision(adId2, preservedBody, preservedTitle, input.confidence, cycleNumber);
|
|
16539
16794
|
}
|
|
16540
16795
|
try {
|
|
16541
16796
|
await adapter2.appendDecisionEvent({
|
|
16542
|
-
decisionId:
|
|
16797
|
+
decisionId: adId2,
|
|
16543
16798
|
eventType: "modified",
|
|
16544
16799
|
cycle: cycleNumber,
|
|
16545
16800
|
source: "strategy_change",
|
|
@@ -16548,18 +16803,66 @@ async function captureDecision(adapter2, input) {
|
|
|
16548
16803
|
});
|
|
16549
16804
|
} catch {
|
|
16550
16805
|
}
|
|
16551
|
-
return { cycleNumber, adId, adAction: "updated" };
|
|
16806
|
+
return { cycleNumber, adId: adId2, adAction: "updated" };
|
|
16552
16807
|
}
|
|
16553
16808
|
const title = input.text.length > 80 ? input.text.slice(0, 77) + "..." : input.text;
|
|
16554
|
-
const
|
|
16809
|
+
const supersedesLine = supersedesId ? `
|
|
16810
|
+
- **Supersedes:** ${supersedesId}` : "";
|
|
16811
|
+
const bodyTemplate = input.adBody ? toAdBodyTemplate(input.adBody) : `### ${AD_ID_PLACEHOLDER}: ${title} [Confidence: ${input.confidence}]
|
|
16555
16812
|
|
|
16556
|
-
- **Decision:** ${input.text}
|
|
16813
|
+
- **Decision:** ${input.text}${supersedesLine}
|
|
16557
16814
|
- **Evidence:** Captured from conversation, Cycle ${cycleNumber}.
|
|
16558
16815
|
- **Status:** Active`;
|
|
16559
|
-
|
|
16560
|
-
|
|
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
|
+
}
|
|
16561
16827
|
} else {
|
|
16562
|
-
|
|
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
|
+
}
|
|
16563
16866
|
}
|
|
16564
16867
|
try {
|
|
16565
16868
|
await adapter2.appendDecisionEvent({
|
|
@@ -16580,11 +16883,13 @@ async function captureDecision(adapter2, input) {
|
|
|
16580
16883
|
title: `Decision captured: ${adId}`,
|
|
16581
16884
|
content: `**${adAction === "created" ? "New" : "Updated"} Active Decision** \u2014 ${adId}: ${input.text}
|
|
16582
16885
|
|
|
16583
|
-
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).` : "")
|
|
16584
16889
|
});
|
|
16585
16890
|
} catch {
|
|
16586
16891
|
}
|
|
16587
|
-
return { cycleNumber, adId, adAction };
|
|
16892
|
+
return { cycleNumber, adId, adAction, supersededId: supersedesId };
|
|
16588
16893
|
}
|
|
16589
16894
|
|
|
16590
16895
|
// src/tools/strategy.ts
|
|
@@ -16700,6 +17005,10 @@ var strategyChangeTool = {
|
|
|
16700
17005
|
type: "boolean",
|
|
16701
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.`
|
|
16702
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
|
+
},
|
|
16703
17012
|
north_star: {
|
|
16704
17013
|
type: "string",
|
|
16705
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.'
|
|
@@ -16933,19 +17242,24 @@ orient and the project foundation will read this value.`
|
|
|
16933
17242
|
const confidence = args.confidence ?? "MEDIUM";
|
|
16934
17243
|
const adBody = args.ad_body;
|
|
16935
17244
|
const confidenceOnly = args.confidence_only === true;
|
|
17245
|
+
const supersedes = args.supersedes?.trim();
|
|
16936
17246
|
const result = await captureDecision(adapter2, {
|
|
16937
17247
|
text: text2.trim(),
|
|
16938
17248
|
adId: adId?.trim(),
|
|
16939
17249
|
confidence,
|
|
16940
17250
|
adBody: adBody?.trim(),
|
|
16941
|
-
confidenceOnly
|
|
17251
|
+
confidenceOnly,
|
|
17252
|
+
supersedes
|
|
16942
17253
|
});
|
|
16943
17254
|
const captureLabel = confidenceOnly ? `Updated confidence on **${result.adId}** to ${confidence} (body preserved)` : `${result.adAction === "created" ? "Created" : "Updated"} **${result.adId}**: ${text2.trim()}
|
|
16944
17255
|
Confidence: ${confidence}`;
|
|
17256
|
+
const supersedeLine = result.supersededId ? `
|
|
17257
|
+
|
|
17258
|
+
**${result.supersededId}** marked superseded by ${result.adId} \u2014 retained as history, not overwritten.` : "";
|
|
16945
17259
|
return textResponse(
|
|
16946
17260
|
`**Decision Captured \u2014 Cycle ${result.cycleNumber}**
|
|
16947
17261
|
|
|
16948
|
-
${captureLabel}
|
|
17262
|
+
${captureLabel}${supersedeLine}
|
|
16949
17263
|
|
|
16950
17264
|
Decision event logged.`
|
|
16951
17265
|
);
|
|
@@ -16976,7 +17290,7 @@ Decision event logged.`
|
|
|
16976
17290
|
return errorResponse("text is required for strategy_change. Describe the strategic shift to apply.");
|
|
16977
17291
|
}
|
|
16978
17292
|
{
|
|
16979
|
-
const result = await prepareStrategyChange(adapter2, text);
|
|
17293
|
+
const result = await prepareStrategyChange(adapter2, text, _config.adapterType);
|
|
16980
17294
|
return textResponse(
|
|
16981
17295
|
`## PAPI Strategy Change \u2014 Prepare Phase (Cycle ${result.cycleNumber})
|
|
16982
17296
|
|
|
@@ -17056,8 +17370,8 @@ async function viewBoard(adapter2, phaseFilter, options) {
|
|
|
17056
17370
|
const bi = PRIORITY_ORDER.indexOf(b2.priority);
|
|
17057
17371
|
const priorityDiff = (ai === -1 ? 999 : ai) - (bi === -1 ? 999 : bi);
|
|
17058
17372
|
if (priorityDiff !== 0) return priorityDiff;
|
|
17059
|
-
const aDate = a.createdAt
|
|
17060
|
-
const bDate = b2.createdAt
|
|
17373
|
+
const aDate = a.createdAt ?? "";
|
|
17374
|
+
const bDate = b2.createdAt ?? "";
|
|
17061
17375
|
return bDate.localeCompare(aDate);
|
|
17062
17376
|
});
|
|
17063
17377
|
const total = filtered.length;
|
|
@@ -17411,6 +17725,10 @@ var boardEditTool = {
|
|
|
17411
17725
|
actual_effort: {
|
|
17412
17726
|
$ref: "#/$defs/effortSize",
|
|
17413
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."
|
|
17414
17732
|
}
|
|
17415
17733
|
},
|
|
17416
17734
|
required: ["task_id"]
|
|
@@ -17664,6 +17982,14 @@ async function handleBoardEdit(adapter2, args) {
|
|
|
17664
17982
|
if (!taskId) {
|
|
17665
17983
|
return errorResponse("task_id is required.");
|
|
17666
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
|
+
}
|
|
17667
17993
|
const updates = {};
|
|
17668
17994
|
const changes = [];
|
|
17669
17995
|
for (const field of EDITABLE_FIELDS) {
|
|
@@ -17691,7 +18017,7 @@ async function handleBoardEdit(adapter2, args) {
|
|
|
17691
18017
|
updates.cycle = null;
|
|
17692
18018
|
changes.push("cycle");
|
|
17693
18019
|
} else if (typeof rawCycle === "number" && Number.isInteger(rawCycle) && rawCycle > 0) {
|
|
17694
|
-
const health = await
|
|
18020
|
+
const health = await target.getCycleHealth().catch(() => null);
|
|
17695
18021
|
const activeCycle = health?.totalCycles ?? 0;
|
|
17696
18022
|
if (rawCycle > activeCycle + 1) {
|
|
17697
18023
|
return errorResponse(
|
|
@@ -17710,7 +18036,7 @@ async function handleBoardEdit(adapter2, args) {
|
|
|
17710
18036
|
return errorResponse("No fields to update. Pass at least one field (title, priority, complexity, module, epic, phase, notes, status, maturity, cycle).");
|
|
17711
18037
|
}
|
|
17712
18038
|
try {
|
|
17713
|
-
const task = await
|
|
18039
|
+
const task = await target.getTask(taskId);
|
|
17714
18040
|
if (!task) {
|
|
17715
18041
|
return errorResponse(`Task ${taskId} not found.`);
|
|
17716
18042
|
}
|
|
@@ -17729,7 +18055,7 @@ async function handleBoardEdit(adapter2, args) {
|
|
|
17729
18055
|
const idx = changes.indexOf("notes");
|
|
17730
18056
|
if (idx >= 0) changes.splice(idx, 1);
|
|
17731
18057
|
} else {
|
|
17732
|
-
const health = await
|
|
18058
|
+
const health = await target.getCycleHealth().catch(() => null);
|
|
17733
18059
|
const activeCycle = health?.totalCycles ?? null;
|
|
17734
18060
|
const date = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
17735
18061
|
const stamp = activeCycle != null ? `[C${activeCycle} ${date}]` : `[${date}]`;
|
|
@@ -17754,7 +18080,7 @@ ${existing}` : entry;
|
|
|
17754
18080
|
}
|
|
17755
18081
|
let autoAssignedCycle = null;
|
|
17756
18082
|
if (updates.status === "In Cycle") {
|
|
17757
|
-
const health = await
|
|
18083
|
+
const health = await target.getCycleHealth().catch(() => null);
|
|
17758
18084
|
const activeCycle = health?.totalCycles ?? null;
|
|
17759
18085
|
if (activeCycle != null && activeCycle > 0) {
|
|
17760
18086
|
updates.cycle = activeCycle;
|
|
@@ -17766,25 +18092,25 @@ ${existing}` : entry;
|
|
|
17766
18092
|
updates.cancelledBy = "user";
|
|
17767
18093
|
}
|
|
17768
18094
|
if (effortCorrection.estimatedEffort || effortCorrection.actualEffort) {
|
|
17769
|
-
if (!
|
|
18095
|
+
if (!target.correctLatestBuildReportEffort) {
|
|
17770
18096
|
return errorResponse("Correcting build-report effort requires a database adapter (pg). The md adapter does not support it.");
|
|
17771
18097
|
}
|
|
17772
|
-
await
|
|
18098
|
+
await target.correctLatestBuildReportEffort(taskId, effortCorrection);
|
|
17773
18099
|
}
|
|
17774
18100
|
if (Object.keys(updates).length > 0) {
|
|
17775
|
-
await
|
|
18101
|
+
await target.updateTask(taskId, updates);
|
|
17776
18102
|
}
|
|
17777
|
-
if ((updates.status === "Done" || updates.status === "Cancelled") &&
|
|
18103
|
+
if ((updates.status === "Done" || updates.status === "Cancelled") && target.updateDogfoodEntryStatus) {
|
|
17778
18104
|
try {
|
|
17779
|
-
const dogfoodLog = await
|
|
18105
|
+
const dogfoodLog = await target.getDogfoodLog?.(50) ?? [];
|
|
17780
18106
|
const linked = dogfoodLog.filter((e) => e.linkedTaskId === taskId || e.linkedTaskId === task.id);
|
|
17781
18107
|
const newStatus = "resolved";
|
|
17782
|
-
await Promise.all(linked.map((e) =>
|
|
18108
|
+
await Promise.all(linked.map((e) => target.updateDogfoodEntryStatus(e.id, newStatus)));
|
|
17783
18109
|
} catch {
|
|
17784
18110
|
}
|
|
17785
18111
|
}
|
|
17786
18112
|
const lines = [
|
|
17787
|
-
`Updated **${taskId}
|
|
18113
|
+
`Updated **${taskId}**${overrideNote} (${updates.title ?? task.title})`,
|
|
17788
18114
|
"",
|
|
17789
18115
|
`**Changes:** ${changes.map((f) => `${f} \u2192 ${String(updates[f])}`).join(", ")}`
|
|
17790
18116
|
];
|
|
@@ -18588,7 +18914,7 @@ async function scaffoldPapiDir(adapter2, config2, input, collector) {
|
|
|
18588
18914
|
if (useCollector) {
|
|
18589
18915
|
collector.add({
|
|
18590
18916
|
path: ".claude/settings.json",
|
|
18591
|
-
content: JSON.stringify({ permissions: { allow: [
|
|
18917
|
+
content: JSON.stringify({ permissions: { allow: [...PAPI_PERMISSIONS] } }, null, 2) + "\n",
|
|
18592
18918
|
mode: "create",
|
|
18593
18919
|
skip_if_exists: true
|
|
18594
18920
|
});
|
|
@@ -18597,7 +18923,7 @@ async function scaffoldPapiDir(adapter2, config2, input, collector) {
|
|
|
18597
18923
|
}
|
|
18598
18924
|
return true;
|
|
18599
18925
|
}
|
|
18600
|
-
var
|
|
18926
|
+
var PAPI_PERMISSIONS = ["mcp__papi__*", "mcp__plugin_papi_papi__*"];
|
|
18601
18927
|
async function ensurePapiPermission(projectRoot) {
|
|
18602
18928
|
const settingsPath = join9(projectRoot, ".claude", "settings.json");
|
|
18603
18929
|
try {
|
|
@@ -18615,8 +18941,10 @@ async function ensurePapiPermission(projectRoot) {
|
|
|
18615
18941
|
perms.allow = [];
|
|
18616
18942
|
}
|
|
18617
18943
|
const allow = perms.allow;
|
|
18618
|
-
|
|
18619
|
-
allow.
|
|
18944
|
+
for (const permission of PAPI_PERMISSIONS) {
|
|
18945
|
+
if (!allow.includes(permission)) {
|
|
18946
|
+
allow.push(permission);
|
|
18947
|
+
}
|
|
18620
18948
|
}
|
|
18621
18949
|
await mkdir(join9(projectRoot, ".claude"), { recursive: true });
|
|
18622
18950
|
await writeFile2(settingsPath, JSON.stringify(settings, null, 2) + "\n", "utf-8");
|
|
@@ -21653,7 +21981,7 @@ function pathBasename(p) {
|
|
|
21653
21981
|
const parts = p.replace(/\\/g, "/").replace(/\/+$/, "").split("/");
|
|
21654
21982
|
return parts[parts.length - 1] ?? p;
|
|
21655
21983
|
}
|
|
21656
|
-
function
|
|
21984
|
+
function matchedPredictedEntry(changedPath, predicted) {
|
|
21657
21985
|
const changed = changedPath.replace(/\\/g, "/");
|
|
21658
21986
|
const changedLower = changed.toLowerCase();
|
|
21659
21987
|
const changedBase = pathBasename(changedPath);
|
|
@@ -21661,17 +21989,20 @@ function isPathInPredictedScope(changedPath, predicted) {
|
|
|
21661
21989
|
const entry = raw.replace(/\\/g, "/").replace(/\/+$/, "").trim();
|
|
21662
21990
|
if (!entry) continue;
|
|
21663
21991
|
if (entry.includes("*")) {
|
|
21664
|
-
if (globToRegExp(entry).test(changed)) return
|
|
21665
|
-
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;
|
|
21666
21994
|
continue;
|
|
21667
21995
|
}
|
|
21668
|
-
if (pathBasename(entry) === changedBase) return
|
|
21996
|
+
if (pathBasename(entry) === changedBase) return entry;
|
|
21669
21997
|
const entryLower = entry.toLowerCase();
|
|
21670
21998
|
if (changedLower === entryLower || changedLower.startsWith(`${entryLower}/`)) {
|
|
21671
|
-
return
|
|
21999
|
+
return entry;
|
|
21672
22000
|
}
|
|
21673
22001
|
}
|
|
21674
|
-
return
|
|
22002
|
+
return null;
|
|
22003
|
+
}
|
|
22004
|
+
function isPathInPredictedScope(changedPath, predicted) {
|
|
22005
|
+
return matchedPredictedEntry(changedPath, predicted) !== null;
|
|
21675
22006
|
}
|
|
21676
22007
|
function autoCommit(config2, taskId, taskTitle, predictedFiles) {
|
|
21677
22008
|
const cwd = config2.projectRoot;
|
|
@@ -21697,6 +22028,17 @@ function autoCommit(config2, taskId, taskTitle, predictedFiles) {
|
|
|
21697
22028
|
if (staged.length > 0) {
|
|
21698
22029
|
return safeRun(() => commitStagedOnly(cwd, message)) + ` (selective staging respected: ${staged.length} file(s)).`;
|
|
21699
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
|
+
}
|
|
21700
22042
|
const modified = getModifiedFiles(cwd);
|
|
21701
22043
|
if (modified.length === 0) {
|
|
21702
22044
|
return "Auto-commit: skipped (no working-tree changes).";
|
|
@@ -21710,7 +22052,9 @@ function autoCommit(config2, taskId, taskTitle, predictedFiles) {
|
|
|
21710
22052
|
const more = outOfScope.length > 10 ? ` (+${outOfScope.length - 10} more)` : "";
|
|
21711
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}.`;
|
|
21712
22054
|
}
|
|
21713
|
-
|
|
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}).`;
|
|
21714
22058
|
}
|
|
21715
22059
|
return `${commitResult} (staged all ${modified.length} changed file(s)).`;
|
|
21716
22060
|
}
|
|
@@ -22754,9 +23098,19 @@ async function completeBuild(adapter2, config2, taskId, input, options = {}, cli
|
|
|
22754
23098
|
return files;
|
|
22755
23099
|
};
|
|
22756
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
|
+
}
|
|
22757
23109
|
const registered = await adapter2.searchDocs({ status: "all", limit: 500 });
|
|
22758
23110
|
const registeredPaths = new Set(registered.map((d) => d.path));
|
|
22759
|
-
const unregistered = mdFiles.filter(
|
|
23111
|
+
const unregistered = mdFiles.filter(
|
|
23112
|
+
(f) => !registeredPaths.has(f) && branchDocs.has(f)
|
|
23113
|
+
);
|
|
22760
23114
|
if (unregistered.length > 0 && adapter2.registerDoc) {
|
|
22761
23115
|
const autoRegistered = [];
|
|
22762
23116
|
const failed = [];
|
|
@@ -22940,7 +23294,7 @@ import { docDeletionBlockMessage } from "@papi-ai/shared";
|
|
|
22940
23294
|
init_git();
|
|
22941
23295
|
|
|
22942
23296
|
// src/services/entitlements.ts
|
|
22943
|
-
import { evaluateContributorGate } from "@papi-ai/shared";
|
|
23297
|
+
import { evaluateContributorGate, isSelfHostedDeployment } from "@papi-ai/shared";
|
|
22944
23298
|
var FREE_PROJECT_CAP = 3;
|
|
22945
23299
|
var DOC_STORAGE_CEILING_BY_TIER = {
|
|
22946
23300
|
free: { bytes: 25 * 1024 * 1024, docs: 200 },
|
|
@@ -22963,6 +23317,7 @@ function isPaidTier(tier) {
|
|
|
22963
23317
|
return tier !== null && PAID_TIERS.has(tier);
|
|
22964
23318
|
}
|
|
22965
23319
|
async function enforceProjectCap(adapter2, target) {
|
|
23320
|
+
if (isSelfHostedDeployment(process.env.PAPI_SELF_HOST)) return null;
|
|
22966
23321
|
const tier = await resolveTier(adapter2);
|
|
22967
23322
|
if (tier === null || isPaidTier(tier)) return null;
|
|
22968
23323
|
if (typeof adapter2.listUserProjects !== "function") return null;
|
|
@@ -23073,7 +23428,11 @@ var docRegisterTool = {
|
|
|
23073
23428
|
},
|
|
23074
23429
|
description: "Actionable findings from the document."
|
|
23075
23430
|
},
|
|
23076
|
-
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
|
+
}
|
|
23077
23436
|
},
|
|
23078
23437
|
required: ["path", "title", "type", "summary", "cycle"]
|
|
23079
23438
|
}
|
|
@@ -23230,6 +23589,22 @@ async function handleDocRegister(adapter2, args, config2) {
|
|
|
23230
23589
|
continueHint
|
|
23231
23590
|
);
|
|
23232
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
|
+
}
|
|
23233
23608
|
if (!path7.toLowerCase().endsWith(".md")) {
|
|
23234
23609
|
return docRegisterSoftFail(
|
|
23235
23610
|
adapterType,
|
|
@@ -23241,13 +23616,13 @@ async function handleDocRegister(adapter2, args, config2) {
|
|
|
23241
23616
|
try {
|
|
23242
23617
|
let supersededBy;
|
|
23243
23618
|
if (supersededByPath) {
|
|
23244
|
-
const existing = await
|
|
23619
|
+
const existing = await target.getDoc?.(supersededByPath);
|
|
23245
23620
|
if (existing) {
|
|
23246
23621
|
supersededBy = existing.id;
|
|
23247
|
-
await
|
|
23622
|
+
await target.updateDocStatus?.(existing.id, "superseded", void 0);
|
|
23248
23623
|
}
|
|
23249
23624
|
}
|
|
23250
|
-
const entry = await
|
|
23625
|
+
const entry = await target.registerDoc({
|
|
23251
23626
|
title,
|
|
23252
23627
|
type,
|
|
23253
23628
|
path: path7,
|
|
@@ -23273,16 +23648,16 @@ async function handleDocRegister(adapter2, args, config2) {
|
|
|
23273
23648
|
}
|
|
23274
23649
|
if (body === void 0) {
|
|
23275
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._";
|
|
23276
|
-
} else if (typeof
|
|
23651
|
+
} else if (typeof target.storeDocBody !== "function") {
|
|
23277
23652
|
bodyNote = "\n\n_Body not stored \u2014 this adapter does not support body storage._";
|
|
23278
23653
|
} else {
|
|
23279
|
-
const decision = await checkDocStorageCap(
|
|
23654
|
+
const decision = await checkDocStorageCap(target, Buffer.byteLength(body, "utf8"));
|
|
23280
23655
|
if (!decision.storeBody) {
|
|
23281
23656
|
bodyNote = `
|
|
23282
23657
|
|
|
23283
23658
|
${decision.message}`;
|
|
23284
23659
|
} else {
|
|
23285
|
-
const result = await
|
|
23660
|
+
const result = await target.storeDocBody({
|
|
23286
23661
|
docId: entry.id,
|
|
23287
23662
|
body,
|
|
23288
23663
|
// Resolved by resolveDocVisibility above — the SAME resolution the
|
|
@@ -23306,7 +23681,7 @@ ${decision.message}`;
|
|
|
23306
23681
|
durability = "";
|
|
23307
23682
|
}
|
|
23308
23683
|
return textResponse(
|
|
23309
|
-
`**Registered:** ${entry.title}
|
|
23684
|
+
`**Registered:** ${entry.title}${overrideNote}
|
|
23310
23685
|
- **Path:** ${entry.path}
|
|
23311
23686
|
- **Type:** ${entry.type} | **Status:** ${entry.status}
|
|
23312
23687
|
- **Visibility:** ${visibilityLabel}
|
|
@@ -23870,7 +24245,7 @@ var buildExecuteTool = {
|
|
|
23870
24245
|
},
|
|
23871
24246
|
fixed_issues: {
|
|
23872
24247
|
type: "array",
|
|
23873
|
-
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.`,
|
|
23874
24249
|
items: { type: "string" }
|
|
23875
24250
|
},
|
|
23876
24251
|
production_verification: {
|
|
@@ -24228,7 +24603,7 @@ ${entries}`;
|
|
|
24228
24603
|
|
|
24229
24604
|
**OPEN DISCOVERED ISSUES** (${top.length} shown):
|
|
24230
24605
|
${rows}
|
|
24231
|
-
|
|
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.`;
|
|
24232
24607
|
}
|
|
24233
24608
|
}
|
|
24234
24609
|
} catch {
|
|
@@ -24475,8 +24850,25 @@ Your report was NOT discarded and the task is NOT yet Done \u2014 re-send with \
|
|
|
24475
24850
|
fixedNote = `
|
|
24476
24851
|
|
|
24477
24852
|
\u2705 Marked ${fixedResolvedCount} discovered issue(s) FIXED \u2014 resolved_at stamped, now counted as fixed on the hub's caught\u2192fixed ledger.`;
|
|
24478
|
-
} else
|
|
24479
|
-
|
|
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
|
+
}
|
|
24480
24872
|
}
|
|
24481
24873
|
return textResponse(formatCompleteResult(result) + fixedNote + docsNote + batchRollupNote);
|
|
24482
24874
|
} catch (err) {
|
|
@@ -25506,6 +25898,13 @@ init_git();
|
|
|
25506
25898
|
|
|
25507
25899
|
// src/services/ad-hoc.ts
|
|
25508
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
|
+
}
|
|
25509
25908
|
function resolveAdHocCycle(cycle, latest, latestComplete) {
|
|
25510
25909
|
if (cycle === void 0) return null;
|
|
25511
25910
|
if (typeof cycle === "number") return cycle;
|
|
@@ -25560,8 +25959,32 @@ async function recordAdHoc(adapter2, input) {
|
|
|
25560
25959
|
...targetCycle !== null ? { cycle: targetCycle } : {},
|
|
25561
25960
|
notes: input.notes ? `[ad-hoc] ${input.notes}` : "[ad-hoc]",
|
|
25562
25961
|
taskType: input.taskType || "task",
|
|
25563
|
-
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
|
+
})()
|
|
25564
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
|
+
}
|
|
25565
25988
|
}
|
|
25566
25989
|
const report = {
|
|
25567
25990
|
uuid: randomUUID15(),
|
|
@@ -25651,6 +26074,10 @@ var adHocTool = {
|
|
|
25651
26074
|
hold: {
|
|
25652
26075
|
type: "boolean",
|
|
25653
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."
|
|
25654
26081
|
}
|
|
25655
26082
|
},
|
|
25656
26083
|
required: []
|
|
@@ -25687,7 +26114,18 @@ async function handleAdHoc(adapter2, config2, args) {
|
|
|
25687
26114
|
else if (rawCycle === "current" || rawCycle === "next-if-plan-not-run") cycleArg = rawCycle;
|
|
25688
26115
|
const stageArg = args.stage === "release" ? "release" : void 0;
|
|
25689
26116
|
const holdArg = args.hold === true;
|
|
25690
|
-
|
|
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, {
|
|
25691
26129
|
title: title || "",
|
|
25692
26130
|
taskId,
|
|
25693
26131
|
notes: rawNotes,
|
|
@@ -25700,9 +26138,11 @@ async function handleAdHoc(adapter2, config2, args) {
|
|
|
25700
26138
|
owner: config2.projectOwner,
|
|
25701
26139
|
cycle: cycleArg,
|
|
25702
26140
|
stage: stageArg,
|
|
25703
|
-
hold: holdArg
|
|
26141
|
+
hold: holdArg,
|
|
26142
|
+
currentBranch,
|
|
26143
|
+
baseBranch
|
|
25704
26144
|
});
|
|
25705
|
-
if (!holdArg &&
|
|
26145
|
+
if (!holdArg && gitUsable) {
|
|
25706
26146
|
try {
|
|
25707
26147
|
stageDirAndCommit(
|
|
25708
26148
|
config2.projectRoot,
|
|
@@ -25720,7 +26160,7 @@ async function handleAdHoc(adapter2, config2, args) {
|
|
|
25720
26160
|
const branch = `feat/${result.task.id}`;
|
|
25721
26161
|
let collisionBlock = "";
|
|
25722
26162
|
try {
|
|
25723
|
-
const board = await
|
|
26163
|
+
const board = await target.queryBoard({ status: ["In Progress"] });
|
|
25724
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);
|
|
25725
26165
|
const collision = detectWorktreeCollision({
|
|
25726
26166
|
taskId: result.task.id,
|
|
@@ -25744,7 +26184,7 @@ async function handleAdHoc(adapter2, config2, args) {
|
|
|
25744
26184
|
} catch {
|
|
25745
26185
|
}
|
|
25746
26186
|
return textResponse(
|
|
25747
|
-
`**${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.
|
|
25748
26188
|
|
|
25749
26189
|
## Held for the next cycle \u2014 branch + commit, do NOT merge
|
|
25750
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.
|
|
@@ -25762,7 +26202,7 @@ _To correct: board_edit ${result.task.id} with updated fields._`
|
|
|
25762
26202
|
);
|
|
25763
26203
|
}
|
|
25764
26204
|
return textResponse(
|
|
25765
|
-
`**${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.
|
|
25766
26206
|
_To correct: board_edit ${result.task.id} with updated fields._`
|
|
25767
26207
|
);
|
|
25768
26208
|
}
|
|
@@ -29032,18 +29472,7 @@ async function getHierarchyPosition(adapter2, projectId) {
|
|
|
29032
29472
|
return void 0;
|
|
29033
29473
|
}
|
|
29034
29474
|
}
|
|
29035
|
-
|
|
29036
|
-
try {
|
|
29037
|
-
const { stdout } = await execFileAsync2("git", ["describe", "--tags", "--abbrev=0"], {
|
|
29038
|
-
encoding: "utf-8",
|
|
29039
|
-
cwd: projectRoot,
|
|
29040
|
-
timeout: 2e3
|
|
29041
|
-
});
|
|
29042
|
-
return stdout.trim() || null;
|
|
29043
|
-
} catch {
|
|
29044
|
-
return null;
|
|
29045
|
-
}
|
|
29046
|
-
}
|
|
29475
|
+
var GIT_TAG_TIMEOUT_MS = 2e3;
|
|
29047
29476
|
async function checkNpmVersionDrift() {
|
|
29048
29477
|
try {
|
|
29049
29478
|
const pkgPath = join17(new URL(".", import.meta.url).pathname, "..", "..", "package.json");
|
|
@@ -29389,7 +29818,7 @@ async function handleOrient(adapter2, config2, args = {}, clientName) {
|
|
|
29389
29818
|
// Latest git tag + npm version drift (both exec calls with own timeouts).
|
|
29390
29819
|
// task-2172: git-tag stays (the latest tag is part of the core summary);
|
|
29391
29820
|
// version-drift is enrichment, gated behind `full`/deep_housekeeping.
|
|
29392
|
-
tracked("git-tag", () =>
|
|
29821
|
+
tracked("git-tag", async () => getLatestTag(config2.projectRoot, GIT_TAG_TIMEOUT_MS)),
|
|
29393
29822
|
tracked("npm-version-drift", async () => fullEnrichment ? checkNpmVersionDrift() : void 0),
|
|
29394
29823
|
// Research Signals — research docs with pending actions since last strategy review.
|
|
29395
29824
|
// task-2172: heavy (doc search + AD cross-reference) and rarely actioned
|
|
@@ -31948,6 +32377,7 @@ If this is legitimate, reach out at https://getpapi.ai and we'll lift it \u2014
|
|
|
31948
32377
|
}
|
|
31949
32378
|
|
|
31950
32379
|
// src/server.ts
|
|
32380
|
+
var mdModeWarned = false;
|
|
31951
32381
|
var DEFAULT_TOOL_TIMEOUT_MS = parseInt(process.env.PAPI_TOOL_TIMEOUT_MS ?? "30000", 10);
|
|
31952
32382
|
var LONG_TOOL_TIMEOUT_MS = parseInt(process.env.PAPI_LONG_TOOL_TIMEOUT_MS ?? "180000", 10);
|
|
31953
32383
|
var WEDGE_PENDING_FRACTION = Math.min(1, Math.max(0, parseFloat(process.env.PAPI_WEDGE_PENDING_FRACTION ?? "0.6")));
|
|
@@ -32124,7 +32554,8 @@ function createServer(adapter2, config2) {
|
|
|
32124
32554
|
// task-1801: `resources` capability for the PAPI read surface exposed as MCP resources.
|
|
32125
32555
|
{ capabilities: { tools: {}, prompts: {}, resources: {} }, instructions: UNIVERSAL_FRAME }
|
|
32126
32556
|
);
|
|
32127
|
-
if (config2.adapterType === "md") {
|
|
32557
|
+
if (config2.adapterType === "md" && !mdModeWarned) {
|
|
32558
|
+
mdModeWarned = true;
|
|
32128
32559
|
process.stderr.write(
|
|
32129
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"
|
|
32130
32561
|
);
|
|
@@ -32451,6 +32882,7 @@ ${usageLine(decision.usage)}`;
|
|
|
32451
32882
|
// src/transport-http.ts
|
|
32452
32883
|
init_proxy_adapter();
|
|
32453
32884
|
import { createServer as createHttpServer } from "http";
|
|
32885
|
+
import { createHash as createHash7 } from "crypto";
|
|
32454
32886
|
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
32455
32887
|
var BEARER_PREFIX = "papi_";
|
|
32456
32888
|
var BEARER_REGEX = /^(papi_|papi_oauth_)[a-f0-9]{64}$/;
|
|
@@ -32480,6 +32912,7 @@ var FRIENDLY_GET_HTML = `<!doctype html>
|
|
|
32480
32912
|
<code>https://mcp.getpapi.ai/mcp</code>.</p>
|
|
32481
32913
|
<a class="btn" href="https://getpapi.ai/docs/install">See the install guide \u2192</a>
|
|
32482
32914
|
</div></body></html>`;
|
|
32915
|
+
var KNOWN_INSTALL_CLIENTS = /* @__PURE__ */ new Set(["claude-code-plugin"]);
|
|
32483
32916
|
var MAX_BODY_BYTES = 1 * 1024 * 1024;
|
|
32484
32917
|
var IP_RATE_WINDOW_MS = 6e4;
|
|
32485
32918
|
var IP_RATE_MAX = 60;
|
|
@@ -32518,7 +32951,7 @@ function corsHeaders(origin) {
|
|
|
32518
32951
|
return {
|
|
32519
32952
|
"Access-Control-Allow-Origin": origin,
|
|
32520
32953
|
"Access-Control-Allow-Methods": "GET, POST, OPTIONS",
|
|
32521
|
-
"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",
|
|
32522
32955
|
"Vary": "Origin"
|
|
32523
32956
|
};
|
|
32524
32957
|
}
|
|
@@ -32560,6 +32993,48 @@ function sendError(res, err, extraHeaders = {}) {
|
|
|
32560
32993
|
});
|
|
32561
32994
|
res.end(JSON.stringify(err.body));
|
|
32562
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
|
+
}
|
|
32563
33038
|
function startHttpTransport(opts) {
|
|
32564
33039
|
const { port, host, baseConfig, pkgVersion: pkgVersion2, dataEndpoint } = opts;
|
|
32565
33040
|
const httpServer = createHttpServer((req, res) => {
|
|
@@ -32661,21 +33136,11 @@ function startHttpTransport(opts) {
|
|
|
32661
33136
|
ip,
|
|
32662
33137
|
status: 401
|
|
32663
33138
|
});
|
|
32664
|
-
|
|
32665
|
-
res,
|
|
32666
|
-
{
|
|
32667
|
-
status: 401,
|
|
32668
|
-
body: {
|
|
32669
|
-
error: "Unauthorized",
|
|
32670
|
-
reason: hasHeader ? "malformed_bearer" : "missing_bearer"
|
|
32671
|
-
}
|
|
32672
|
-
},
|
|
32673
|
-
{
|
|
32674
|
-
"WWW-Authenticate": `Bearer realm="papi", resource_metadata="${RESOURCE_METADATA_URL}"`
|
|
32675
|
-
}
|
|
32676
|
-
);
|
|
33139
|
+
sendUnauthorized(res, hasHeader ? "malformed_bearer" : "missing_bearer");
|
|
32677
33140
|
return;
|
|
32678
33141
|
}
|
|
33142
|
+
const clientHeader = req.headers["x-papi-client"];
|
|
33143
|
+
const installClient = typeof clientHeader === "string" && KNOWN_INSTALL_CLIENTS.has(clientHeader) ? clientHeader : "direct";
|
|
32679
33144
|
const projectIdHeader = req.headers["x-papi-project-id"];
|
|
32680
33145
|
const projectId = typeof projectIdHeader === "string" && projectIdHeader.length > 0 ? projectIdHeader : void 0;
|
|
32681
33146
|
if (req.method !== "POST" && req.method !== "GET") {
|
|
@@ -32718,6 +33183,13 @@ function startHttpTransport(opts) {
|
|
|
32718
33183
|
return;
|
|
32719
33184
|
}
|
|
32720
33185
|
}
|
|
33186
|
+
logEvent({
|
|
33187
|
+
level: "info",
|
|
33188
|
+
msg: "mcp_request",
|
|
33189
|
+
ip,
|
|
33190
|
+
bearer_prefix: bearerPrefix(bearer),
|
|
33191
|
+
install_client: installClient
|
|
33192
|
+
});
|
|
32721
33193
|
void dispatchRequest({
|
|
32722
33194
|
req,
|
|
32723
33195
|
res,
|
|
@@ -32798,6 +33270,18 @@ Example: add \`project="${projects[0].slug}"\` to the tool arguments.`;
|
|
|
32798
33270
|
res.writeHead(200, { "Content-Type": "application/json", ...corsHeaders(origin) });
|
|
32799
33271
|
res.end(JSON.stringify(payload));
|
|
32800
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
|
+
}
|
|
32801
33285
|
function resolveEffectiveProjectId(body, headerProjectId) {
|
|
32802
33286
|
const explicitProject = extractProjectOverride(body);
|
|
32803
33287
|
try {
|
|
@@ -32807,17 +33291,59 @@ function resolveEffectiveProjectId(body, headerProjectId) {
|
|
|
32807
33291
|
throw err;
|
|
32808
33292
|
}
|
|
32809
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
|
+
}
|
|
32810
33302
|
async function dispatchRequest(args) {
|
|
32811
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
|
+
}
|
|
32812
33322
|
let effectiveProjectId = resolveEffectiveProjectId(body, projectId);
|
|
32813
33323
|
if (effectiveProjectId === void 0) {
|
|
32814
|
-
const toolName =
|
|
33324
|
+
const toolName = calledTool;
|
|
32815
33325
|
if (toolName && !PROJECT_OPTIONAL_TOOLS.has(toolName)) {
|
|
32816
33326
|
let projects = [];
|
|
33327
|
+
let probeFailed = false;
|
|
32817
33328
|
try {
|
|
32818
|
-
const probe = new ProxyPapiAdapter({
|
|
33329
|
+
const probe = new ProxyPapiAdapter({
|
|
33330
|
+
endpoint: dataEndpoint,
|
|
33331
|
+
apiKey: bearer,
|
|
33332
|
+
onAuthRejected: () => recordAuthVerdict(bearer, "rejected")
|
|
33333
|
+
});
|
|
32819
33334
|
projects = await probe.listUserProjects();
|
|
32820
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;
|
|
32821
33347
|
}
|
|
32822
33348
|
if (projects.length === 1) {
|
|
32823
33349
|
effectiveProjectId = projects[0].id;
|
|
@@ -32840,10 +33366,14 @@ async function dispatchRequest(args) {
|
|
|
32840
33366
|
}
|
|
32841
33367
|
}
|
|
32842
33368
|
}
|
|
32843
|
-
const adapter2 =
|
|
33369
|
+
const adapter2 = createProxyAdapter({
|
|
32844
33370
|
endpoint: dataEndpoint,
|
|
32845
33371
|
apiKey: bearer,
|
|
32846
|
-
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")
|
|
32847
33377
|
});
|
|
32848
33378
|
const requestConfig = {
|
|
32849
33379
|
...baseConfig,
|